Programming
Microcontrollers in C
Second Edition
Ted Van Sickle
A Volume in the EMBEDDED TECHNOLOGY™ Series
LLH
Technology
Publishing
Eagle Rock, Virginia
www.LLH-Publishing.com
Programming Microcontrollers in C © 2001 by LLH Technology Publishing.
All rights reserved. No part of this book may be reproduced, in any form or means
whatsoever, without written permission from the publisher. While every precaution has
been taken in the preparation of this book, the publisher and author assume no
responsibility for errors or omissions. Neither is any liability assumed for damages
resulting from the use of the information contained herein.
ISBN: 1-878707-57-4
Library of Congress Control Number: 00-134094
Printed in the United States of America
10 987654321
Project management and developmental editing:
Carol S. Lewis, LLH Technology Publishing
Interior design and production services:
Kelly Johnson, El Cajon, California
Cover design: Brian McMurdo, Valley Center, California
Publishing
Visit us on the web: www.LLH-Publishing.com
1 Introduction to C 1
Some Simple Programs 1
Names 8
Types and Type Declarations 9
Storage Classes, Linkage, and Scope 12
Character Constants 15
Arrays 18
Other types 20
Operators and Expressions 24
Increment and Decrement Operators 30
Precedence and Associativity 34
Program Flow and Control 36
Functions 51
Recursion 61
Summary 63
2 Advanced C Topics 65
Pointers 65
Multidimensional Arrays 80
Structures 87
More Structures 107
Input and Output 110
Memory Management 114
Miscellaneous Functions 116
Summary 121
3 What Are Microcontrollers? 123
Microcontroller Memory 127
Input/ Output 129
Programming Microcontrollers 134
Coding Tips for Microcontrollers 137
4 Small 8- Bit Systems 149
Microcontroller Memory 153
Timers 166
Analog- to- Digital Converter Operation 195
Pulse Width Modulator System 201
Other Program Items 207
Summary 209
5 Programming Large 8- Bit Systems 211
Header File 211
Sorting Programs 230
Data Compression 237
Timer Operations 245
Summary 285
6 Large Microcontrollers 287
TheMC68HC16 288
System Integration Module ( SIM) 296
A Pulse Width Modulation Program 299
Cosmic MC68HC1 6 Compiler 305
Table Look- Up 319
Digital Signal Processor Operations 326
Other MC68HC1 6 Considerations 345
7 Advanced Topics in Programming Embedded
Systems ( M68HC12) 347
Numeric Encoding 352
Numeric Decoding 354
Coding the alpha data 356
The Monitor Program 370
The SAVEIT() Routine 376
The printout() and the printafter() Functions 378
Reset 381
Input/ Output Functions 382
Putting It All Together 386
Summary 391
8 MCORE, a RISC Machine 393
Delay Routine 395
Delays Revisited 401
Serial Input/ Output 404
Handling Interrupts 413
A Clock Program 419
Keyboard 432
Integrating Keyboard and Clock 440
Adding a Display 442
Summary 446
Index 447
Introduction to Second Edition
Today, even more than when the first edition of this book was written,
the use of microcontrollers has expanded to an almost unbelievable level.
A typical car has 15 microcontrollers. A modern home can have more than
50 microcontrollers controlling everything from the thermostat, to the
furnace, to the microwave. Microcontrollers are everywhere! In the mean-
time, many new chips have been placed on the market as well.
Also, there have been significant modifications to our programming
languages. The standard C language is now called C99 rather than C89.
There have been several changes in the language, but most of these
changes will not be available to us for some time. Many of the modifica-
tions to the language will be of little use to programs for embedded
systems. For example, complex arithmetic has been added to the lan-
guage. It is rare that we use even floating-point arithmetic, and I have
never seen an application for an embedded system where complex arith-
metic was needed. However, other additions allow improved optimization
processes, such as the restrict keyword and the static keyword used to
modify the index of an array. Other changes have less impact on the
generation of code, such as the // opening to a single line comment. Also,
today you will have no implicit int return from a function. All in all,
expect the new versions of C compilers to be significant improvements
over the older versions. Also, expect that the new compilers will not break
older code. The features of the new standard should begin showing up in
any new version of the compilers that you use.
The C++ standard committee has completed its work on the first
language standard for C++. There is much clamor about the use of C++ for
embedded systems. C++ as it stands is an excellent language, but it is
aimed primarily at large system programs, not the small programs that we
will be developing into the future. C still remains the grand champion at
giving us embedded programmers the detailed control over the computer
that we need and that other computer languages seem to overlook.
The first six chapters of the book have been revised and any errors that
were found were corrected. Every chapter has been altered, but not so
much that you would not recognize it. Chapter 7 has been added. In that
chapter, a relatively complex program is developed to run on the
M68HC912B32. The development system was based on this chip and it
had no significant RAM to hold the code during development. Therefore,
all of the code was completely designed, coded, and tested on a DOS-
VII
viii Introduction to Second Edition
based system. Extensive tests were completed to make certain that there
were no hidden bugs. The modules were small and easy to test. Each
module was tested with a program written to exercise all parts of the
module. When the several modules were integrated into a single program,
the program worked in the DOS -based system. All changes needed to
convert this program were implemented under the control of conditional
compiler commands. When the program was converted to the M68HC12
version and compiled, it loaded correctly and ran.
Chapter 8 introduces a new chip for Motorola, the MMC2001. This
chip is a RISC chip. Many of the good things to be said of RISC
configurations are absolutely true. This chip is very fast. Each of its
instructions requires only one word, 32 bits, of memory. Almost all
instructions execute in a single clock cycle. The chip that I used here ran at
32 mHz, and you could not feel any temperature rise on the chip. It is from
a great family of chips that should become a future standard.
The first edition of this book had several appendices. These were
needed to show general background material that the reader should not be
expected to know. Also, quite a few specialized header files used to
interconnect the program to the peripheral components on the
microcontroller were included. Also, with the first edition, there was a
card with which the reader could order two diskettes that contained all of
the source code in the book, demonstration compilers that would compile
the source code, and other useful information. All of these things have
been included on the CD-ROM that comes with this edition. Additionally,
you will find PDF versions of all appropriate Motorola data manuals and
reference manuals for all of the chips discussed in the book. Also included
are copies of all header files used with the programs, and some more that
will probably be useful to you.
Introduction to First Edition
Early detractors of the C language often said that C was little more
than an over-grown assembler. Those early disparaging remarks were to
some extent true and also prophetic. C is indeed a high level language and
retains much of the contact with the underlying computer hardware that is
usually lost with a high level language. It is this computer relevance that
makes people say that C is a transform of an assembler, but this computer
relevance also makes C the ideal high level language vehicle to deal with
microcontrollers. With C we have all of the advantages of an easily
understood language, a widely standardized language, a language where
programmers are readily available, a language where any trained program-
mer can understand the work of another, and a language that is very
productive.
The main purpose of this book is to explore the use of C as a
programming tool for microcontrollers. We assume that you are familiar
with the basic concepts of programming. A background in C is not
necessary, but some experience with a programming language is required.
I have been teaching C programming for microcontrollers for several
years, and have found that my students are usually excellent programmers
with many years of experience programming microcontrollers in assembly
language. Most have little need or interest in learning a new language. I
have never had a class yet where I was able to jump into programming
microcontrollers without providing substantial background in the C lan-
guage. In many instances, students believe that a high-level language like
C and microcontrollers are incompatible. This forces me, unfortunately, to
turn part of my class into a sales presentation to convince some students
that microcontrollers and C have a future together. I am usually able to
show that the benefits gained from using C far outweigh the costs attrib-
uted to its use. The first two chapters are included for those who are
unfamiliar with C. If you are already familiar with C, feel free to skip
ahead to Chapter 3.
C is a very powerful high level language that allows the programmer
access to the inner workings of the computer. Access to computer details,
memory maps, register bits, and so forth, are not usually available with
high level languages. These features are hidden deliberately from the
programmer to make the languages universal and portable between ma-
chines. The authors of C decided that it is desirable to have access to the
heart of the machine because it was intended to use C to write operating
systems. An operating system must be master of all aspects of the machine
ix
Introduction to First Edition
it is controlling. Therefore, no aspect of the machine could be hidden from
the programmer. Features like bit manipulation, bit field manipulation,
direct memory addressing, and the ability to manipulate function ad-
dresses as pointers have been included in C. All of these features are used
in programming microcontrollers. In fact, C is probably the only popular
high level language that can be conveniently used for a microcontroller.
Every effort has been made to present the C aspects of programming
these machines clearly. Example programs and listings along with their
compiled results are presented whenever needed. If there are problems
hidden in the C code, these problems are explored and alternate methods
of writing the code are shown. General rules that will result in more
compact code or quicker execution of the code are developed. Example
programs that demonstrate the basis for these rules will be shown.
C is a rich and powerful language. Beyond the normal high level
language capability, C makes extensive use of pointers and address indi-
rection that is usually available only with assembly language. C also
provides you with a complete set of bit operations, including bit manipula-
tions and bit fields in addition to logical bit operations. In C, the program-
mer knows much about the memory map which is often under program-
mer control. A C programmer can readily write a byte to a control register
of a peripheral component to the computer. These assembly language-like
features of the C language serve to make C the high level language of
choice for the microcontroller programmer.
As a language, C has suffered many well-intended upgrades and
changes. It was written early in the 1970s by Dennis Ritchie of Bell
Laboratories. As originally written, C was a "free wheeling" language
with few constraints on the programmer. It was assumed that any pro-
grammer using the language would be competent, so there was little need
for the controls and hand-holding done by popular compilers of the day.
Therefore, C was a typed language but it was not strongly typed. All
function returns were assumed to be integer unless otherwise specified.
Function arguments were typed, but these types were never checked for
validity when the functions were called. The programmer could specify an
integer argument and then pass a floating point number as the argument.
These kinds of errors are made easily by the best programmer, and they
are usually very difficult to find when debugging the program.
Another set of problems with the language was the library functions
that always accompanied a compiler. No standard library was specified. C
does not have built-in input/output capability. Therefore, the basic C
standard contained the specifications for a set of functions needed to
provide sensible input/output to the language. A few other features such as
a math library, a string handling library, and so forth started out with the
Introduction to First Edition xi
language. But these and other features were included along with other
enhancements in a helter-skelter manner in different compilers as new
compiler versions were created.
In 1983, an ANSI Committee (The X3J11 ANSI C Standards Com-
mittee) was convened to standardize the C language. The key results of the
work of this committee has been to create a strongly typed language with a
clear standard library. One of the constraints that the ANSI committee
placed upon itself was that the existing base of C code must compile error
free with an ANSI C compiler. Therefore, all of the ANSI dictated typing
requirements are optional under an ANSI C compiler. In this text, it is
always assumed that an ANSI compliant compiler will be used, and the
ANSI C form will be used throughout.
C compilers for microcontrollers — especially the small devices —
must compromise some of the features of a compiler for a large computer.
The small machines have limited resources that are required to implement
some of the code generated by a compiler for a large computer. When the
computer is large, the compiler writer need not worry about such problems
as limited stack space or a small register set. But when the computer is
small, these limitations will often force the compiler writer to take extraor-
dinary steps just to be able to have a compiler. In this book, we will
discuss the C programming language, not an abbreviated version that you
might expect to use with some of the smaller microcontrollers. In the
range of all microcontrollers, you will find components with limited
register sets, memory, and other computer necessary peripherals. You will
also find computers with many megabytes of memory space, and all of the
other important computer features usually found only on a large computer.
Therefore, we will discuss the language C for the large computer, and
when language features must be abbreviated because of computer limita-
tions, these points will be brought out.
All of the programs found in this book have been compiled and tested.
Usually source code that has been compiled has been copied directly from
computer disks into the text so that there should be few errors caused by
hand copying of the programs into the text. The compilers used to test
these programs are available from Byte Craft Ltd. of Hamilton, Ontario,
Canada (for the MC68HC05) and Intermetrics of Cambridge, Massachu-
setts (for the MC68HC11 and MC68HC16). If you wish to develop
serious programs for any of these microcontrollers, you should purchase
the appropriate compiler from the supplier.
How does one partition a book on C programming for microcontrollers?
First, the text must contain a good background on the C language. Second,
it is necessary to include a rather extensive background on some
microcontrollers. Finally, C must be used to demonstrate the creation of
code for the specified microcontrollers. This approach is used here. The C
xii Introduction to First Edition
background is complete. The background on the chosen microcontrollers
is presented briefly, as this book is not intended to be a text on
microcontrollers. Therefore, the chapters that cover specific microcontrollers
are to the point. The references found in each chapter contain texts and
data books that will cover the various microcontrollers discussed. This
book grew out of my teaching activities, so chapters include several
exercises suitable for classroom as well as individual use. The only way to
learn programming is to program, and the exercises are designed to let you
put the material in each chapter to use in typical microcontroller program-
ming situations.
Chapters 1 and 2 contain a background on ANSI C. Data in these
chapters is basic to all C programs. There is no specific coverage for
microcontroller programming. Chapter 3 contains a brief background on
microcontrollers, and it also contains general programming guidelines that
should be used when writing code for microcontrollers.
Chapter 4 is devoted to writing programs for the MC68HC05 family.
In this chapter, the use of microcontroller specific header files is intro-
duced. These header files are written for a specific part, and must be
included in any program for the part.
In Chapter 5 you will find techniques for programming the MC68HC1 1
family of parts. Several of the peripherals on these parts are examined, and
code to access these peripherals is written.
More complex microcontrollers are found in the MC68HC16 and the
MC68300 families. Programming the MC68HC16 is discussed in Chapter
6. This part contains an internal bus with several peripherals placed on this
bus. Access to these peripherals is through memory mapped registers and
how these peripherals are accessed will be found in Chapter 6.
There are several appendices. Appendix A contains several header
files that are useful in programming MC68HC05 programs. Appendix B
contains some code that demonstrates the power of the types defined by
structures, and how these types can be made into very convenient new
types by the typedef keyword.
One of the advantages of a high level language is that it isolates the
programmer from the details of the computer being programmed. There
are both plusses and minuses to this idea. First, as a programmer, you do
not need to know details of the register map and the programmers model
of the computer being programmed because the language takes care of
these details for you. On the other hand, microcontrollers all have periph-
erals and other components that must be accessed by the program. The
programmer must be able to write C code that will set and reset bits and
flags in control registers for these parts. It would be desirable to write this
book with no detailed discussion of the insides of the microcontrollers you
Introduction to First Edition xiii
will be programming; however, I could not do it. I needed a careful
discussion of the ways peripheral components are used. Appendix C and
Appendix E contain detailed descriptions of the MC68HC11 and the
MC68HC16 family parts respectively. I am particularly indebted to
Motorola Semiconductor Products, Inc. for the contents of Appendix E.
This Appendix is a very slightly modified version of the Appendix D
found in the MC68HC16Z1 users manual.
Appendix C contains a header file for the MC68HCllEx series, and
Appendix F contains several header files needed to program the MC68HC16
components.
This book has taken entirely too much time to write. As the author, it
is my fault, and I have been a burden to those around me while I have
labored on this task. The basis for the text comes from about three years of
teaching classes on programming microcontrollers in C. This class has
been taught as a three or four day course, mainly to Motorola customers. I
am amazed that it is possible to learn from every class that I teach. During
the time I have been writing, I have learned object oriented programming
and the C++ language, and I have also taught classes on this subject. It is
difficult to move from one language to another, especially languages with
similar roots like C and C++, and not get them mixed up. I am comfort-
able that this book is on C without C++ spilling into the material.
I have received much help in writing this book. My dear wife, who
understands nothing about computers, has read most of the book and made
comments about the contents. If this text is more readable than usual, it is
her contribution. Any problems that you find are my responsibility entirely.
Motorola has provided me much time and support that I appreciate.
Most of the photographs found in the book are from Motorola files. My
manager, Neil Krohn, has encouraged me at every phase in the preparation
of this manuscript. Neil and Motorola deserve my heartfelt thanks.
What's on the CD-ROM?
Programs
The programs on this CD-ROM will help you learn how to program
small embedded control systems. The directory named Programs contains
all of the programs from the book. Programs from each chapter are
grouped together in directories named Chapterl, Chapter2, etc., where the
number corresponds to the book chapter in which the code is found. The
subdirectory Header- 1, or Header Files, contains a series of directories
that contain the specific header files needed to connect your compiled
code to the peripherals found on the indicated chips. These header files
have been used extensively, but you will probably still find an occasional
bug in them. If you do find a bug, please notify me at the email address
below.
There are demonstration compilers for the M6805, the M68HC1 1, and
the M68HC16 families of chips. The Byte Craft Limited compiler is
placed in directory C6805. Instructions for use of this compiler can be
obtained by merely typing \c6805\c6805 with no arguments and the
instruction sheet will appear.
The two Intermetrics demonstration compilers are placed in
HC11DEMO and HC16DEMO respectively. When using one of these
compilers, the directory name should be placed in the system path. Only
one of the demo directories should be in the path at a time because the two
compilers both use the same function names. Confusion will reign if both
directories are in the path at the same time. In the Software directory, you
will find files named HC16BOOK.TXT and HCllBOOK.TXT. These
files are transcriptions of the books normally shipped with the Demo Kit
packages from Intermetrics. There is no convenient means to copy the
several figures found in these books into these ASCII files. Therefore, the
files are complete with the exception of the figures. The text describes the
contents of the figures. I am sorry for any inconvenience caused by these
necessary omissions. Also, the contents of these books contain discus-
sions of how you should install the various programs contained in the
Demo Kits. These compilers are already installed on the CD-ROM, but
the basic programs from which they are installed are found in the directo-
ries HC16 and HC11. You can reinstall these demonstration compilers
from the programs in these directories if you wish.
xv
xvi What's on the CD-ROM?
Intermetrics no longer supports the compilers found on the CD-ROM.
If you wish continued support with these compilers, you should contact
COSMIC Software at
Cosmic Software
400 W. Cummings Park STE6000
Woburn, MA 01801-6512
781 932 2556 x 15
Motorola Reference Manuals and Data Manuals
The CD-ROM contains full copies of several Motorola M68HC11
reference manuals and data manuals, along with similar information for
the M68HC05, M68HC08, M68HC12, M68HC16, and M683XX family
of chips, and the MCORE family. These reference materials have been
provided with the permission of Motorola and are there for your use.
eBook
Also included on the CD-ROM is a full, searchable eBook version of
the text in Adobe pdf format. In addition, there are sample chapters of
other electronics engineering references available in both eBook and
print versions from LLH Technology Publishing.
Good luck on your venture into C.
Ted Van Sickle
e-mail: tvansickle@a-sync.com
http://www.a-sync.com/
Chapter 1
Introduction to C
Programming is a contact sport. Programming theory is interest-
ing, but you must sit at a keyboard and write code to become a
programmer. The aim of this introductory section is to give you a
brief glimpse of C so that you can quickly write simple programs.
Later sections will revisit many of the concepts outlined here and
provide a more in-depth look at what you are doing. For now, let's
start writing code.
Some Simple Programs
C is a function based language. You will see that C uses far more
functions than other procedural languages. In fact, any C program it-
self is merely a function. This function has a name declared by the
language. In C, parameters are passed to functions as arguments. A
function consists of a name followed by parentheses enclosing argu-
ments, or perhaps an empty pair of parentheses if the function requires
no arguments. If there are several arguments to be passed, the argu-
ments are separated by commas.
The mandatory program function name in C is main. Every C pro-
gram must have a function named main, and this function is the one
executed when the program is run. Examine the following program:
# include <stdio .h>
int main (void)
{
printf ( "Microcontrollers run the world!\n");
return ;
}
Chapter 1 Introduction to C
This program contains all of the elements of a C program. Note
first that C is a "free form" language. Spaces, carriage returns, tabs,
and so forth are for the programmer's convenience and are ignored
by the compiler. The first line of the program
# include <stdio .h>
is called a, preprocessor command. Preprocessor commands are iden-
tified by the # at the beginning of the line. In this case, # include
tells the preprocessor to open the file stdio . h and read it into the
program to be compiled with the remainder of the program. The file
name is surrounded by angle brackets < >. These delimiters tell the
compiler to search for the file in a region designated by the operating
system as SET INCLUDE. Had the file name been delimited by
double quotes, w u , the operating system would have searched only
the default directory for the file. The default directory is, of course,
the directory from which you are operating.
The next line of the program is a definition for a function named
main. In ANSI C, as opposed to classic C, each function definition
must inform the compiler of the return type from the function, and
the type of the function's arguments. In this case, the function main
has to return an integer and it expects no arguments. The type int
preceding the function name indicates that it returns an integer and
that no arguments to the function are expected.
The line following the function definition contains an opening
brace { . This brace designates the beginning of a block or a com-
pound statement. The next line of the program contains a function
call to the function print f ( ) . This function is made available to
the program by the inclusion of the header file stdio . h, and it is a
function that writes a message to the computer terminal screen. In
this case, the message to be sent to the screen is
Microcontrollers run the world!
The escape character x \n' at the end of the message informs the
program to insert a new line at that point. The complete message
including the new line escape character is enclosed in double quotes.
These double quotes identify a string, and the string is the argument
to the function print f ( ) . Note that the statement beginning with
print f is closed with a semicolon. In C, every statement is termi-
nated with a semicolon.
Some Simple Programs
After the message is sent to the screen, there is nothing more for
the program to do, so the program is terminated by executing the
statement return ; . This statement returns the value back to
the calling program, which is the operating system. Also, execution
of the return statement will cause all open files to be closed. If there
were no return statement at the end of the program, the normal pro-
cessing at the end of the program would close open files, but there
would be no value returned to the calling program.
This is an area where there is much discussion and many dissent-
ing viewpoints. Early C did not require that main return a value to
the calling program. When the C89 standard was written, it required
that main return an int. Unfortunately, many people, set in their
ways, have refused to adhere to the standard nomenclature in this
case and they often use void main (void) instead of the form
above. Most compilers will ignore this form and allow the void
main (void) function call. For some reason, this form angers many
code reviewers, so you should use the correct form shown above.
The program is closed by the inclusion of a closing brace, }, at
the end. There could be many statements within the block following
main ( ) creating a program of any complexity. The closing brace is
the terminator of a compound statement. The compound statement is
the only case in C where a complete statement closure does not re-
quire a semicolon.
Another program example is as follows:
# include <stdio . h>
int
main
(void)
{
int a,
b,c,
-d;
a=
= 10;
b =
= 5;
c=
= 2;
d=a*b*c;
printf("a * b * c = %d\n" , d) ;
Chapter 1 Introduction to C
d=a*b+c;
printf( u a * b + c = %d\n" , d) ;
d=a+b*c;
printf( w a + b * c = %d\n" , d) /
return ;
}
Before discussing this bit of code, we need to talk about the num-
bers used in it. Like most high-level languages, C provides for different
classes of numbers. These classes can each be variable types. One
class is the integer type and a second is the floating point type. We
will examine these number classes in more detail later, but for now
let us concentrate on the integer types. Integer numbers usually have
a numeric range of about ±2 ( n ~ l \ where n is the number of bits that
contains the integer type. Integers are also called integral types. Inte-
gral types do not "understand" or permit fractions. Any fraction that
results from a division operation will be truncated and disappear from
the calculation. All variables must be declared or defined to be a
specific type prior to their use in a program.
The first line of code in main
int a,b,c,d;
declares the variables a , b, c, and d to be integer types. This par-
ticular statement is both a declaration and a definition statement. A
definition statement causes memory to be allocated for each vari-
able, and a label name to be assigned each location. A declaration
statement does not cause memory allocation, but rather it merely
provides information as to the nature of the variable to the compiler.
We will see more of definition and declaration statements later.
The three assignment statements
a=10;
b=5;
C = 2;
assign initial values to the variables a, b, and c. The equal sign
signifies assignment. The value 1 is placed in the memory location
designated as a, etc. The next statement
d=a*b*c;
Some Simple Programs
notifies the compiler to generate code that will cause the integer stored
in location a to be multiplied by the integer in b and the result of that
product to be multiplied by the integer found in c. Usually, the name
a , b , or c is used to designate the content of the memory location
assigned to the label name. This integer result will be stored in the
location identified by d.
The print statement
printf( w a * b * c = %d\n" , d) ;
is similar to the same statement in the first example. In this case,
however, the data string
a * b * c = %d\n"
w
contains a printer command character %d. This character notifies the
print f function that it is to take the first argument following the
data string, convert it to a decimal value, and print it out to the screen.
The result of this line of code will be
b * c = 100
printed on the screen.
The line of code
d=a*b+c;
demonstrates another characteristic of the language. Each operator
is assigned a precedence that determines the order in which an ex-
pression is evaluated. The parenthesis operators are of the highest
precedence. The precedence of the * operator is higher than that of
the + operator, so this expression will be evaluated as
d= (a*b) +c;
In other words, the product indicated by * will be executed prior
to the addition indicated by the +. The expression that follows later
in the code
d=a+b*c;
will be evaluated as
d=a+ (b*c) ;
causing the result of the third calculation to differ from that of the
second.
Chapter 1 Introduction to C
The result obtained when running this program is as follows.
a * b * c = 100
a * b + c = 52
a + b * c = 20
Here is another example that demonstrates a primitive looping
construct:
# include <stdio .h>
int main (void)
{
int i ;
i=l;
printf ("\ti\ti\ti\n" ) ;
printf ("\t\t Squared Cubed\n\n" ) ;
while (i<ll)
{
printf ("\t%d\t%d\t%d\n", i, i*i, i*i*i) ;
i = i + 1 ;
}
return ;
This example was designed to produce a simple table of the val-
ues of the first ten integers, these values squared, and these values
cubed. The lines
printf ( "\ti\ti\ti\n" ) ;
printf ("\t\t Squared Cubed\n\n" ) ;
combine to produce a header that identifies the contents of the three
columns generated by the program. The escape character \t is a tab
character that causes the screen cursor to skip to the next tab posi-
tion. The default tab value in C is eight spaces.
The command
while (i<ll)
causes the argument of the while to be evaluated immediately, and
if the argument is TRUE, the statement following the whi le will be
Some Simple Programs
executed. The argument should be read "i is less than 11." The ini-
tially assigned value for i was 1, so the argument is TRUE. The
compound statement
printf ("\t%d\t%d\t%d\n" , i, i*i, i*i*i) ;
i = i + 1 ;
will start execution with the value of i being equal to 1 . Once this
statement is evaluated, control is passed back to the while and its
argument is evaluated. If the argument is TRUE, the statement fol-
lowing will be evaluated again. This sequence will repeat until the
argument evaluates as FALSE.
In this expression, the string argument of the printf function
contains three %d commands. Each %d command causes the corre-
sponding argument following the string to be printed to the screen.
There are tab characters, \t, to separate the various printed values
on the screen. The first %d will cause the value of i to be printed
on the screen. The second %d will cause the value i* i, or i 2 , to
be printed to the screen. The third %d will print the value of i * i * i,
or i 3 to be printed. When C executes the function call, the values
of the arguments are calculated prior to the call, so arguments like
i*i are evaluated by the calling program and passed by value to
the function.
The statement
i = i + l ;
is an example of the use of both precedence and association — the
direction in which expressions are evaluated — in C. The equal sign
here is an operator just like the + symbol. The + operator is evaluated
from left to right, and the = operator is evaluated from right to left.
Also, the + operator has higher precedence than the = operator. There-
fore, the above statement will add one to the value stored in i and
then assign this new value to the variable i . This expression simply
increments the variable i.
The above statement is the terminating statement of the com-
pound statement following the while. Since i had an initial value
of 1 , control will be returned to the while with a value of 2 for i . 2,
8 Chapter 1 Introduction to C
of course, is less than 1 1, so the statement following the while will
be executed again and new values will be printed to the screen. This
sequence will be repeated until the incremented value for i equals
11, at which time i<ll will be FALSE. At that point in the pro-
gram, the statement following the while will be skipped, and the
program will have reached its end. The result of executing the above
program is shown in the following table:
1
l
1
squared
cubed
1
1
1
2
4
8
3
9
27
4
16
64
5
25
125
6
36
216
7
49
343
8
64
512
9
81
729
10
100
1000
EXERCISES
Names
1 . Write, compile, and execute each of the example programs shown
in this section.
2. Write a program to calculate the Fahrenheit temperature for the Cel-
sius values between 0° degrees and 100° in steps of 10° each. The
conversion formula is F=9*C/5+32. Use integer variables, and ex-
amine the result when you use F=C*(9/ 5) + 32. What went wrong?
Variables, constants and functions in C are named, and the pro-
gram controls operations on these named variables and constants.
Variables and constants are called operands. Names can be as many
as 31 characters long. The characters that make up the name can be
the upper and the lower case letters, the digits through 9, and the
underscore character x _ ' . There are several defined constants and
functions that are used by the compiler. All of these names begin
Names 9
with an underscore. Because of this convention, you should avoid
the use of an underscore as the first character for either function or
variable names in your code. This approach will completely avoid
name conflict with these hidden or unexpected names. Compilers
usually allow the names to be unique in the first 31 characters. Un-
fortunately, some linkers used to link various program modules require
that the names be unique in the first six or eight characters, depend-
ing on the linker.
C has a collection of keywords that cannot be used for names.
These keywords are listed below:
KEYWORDS
auto
double
int
struct
break
else
long
switch
case
enum
register
typedef
char
extern
return
union
const
float
short
unsigned
continue
for
signed
void
default
goto
sizeof
volatile
do
if
static
while
Types and Type Declarations
C has only a few built-in types. Here they are:
char — is usually eight bits. The character is the smallest stor-
age unit.
int — an integer is usually the size of the basic unit of storage
for the machine. An int must be at least 16 bits wide.
float — a single precision floating-point number.
double — a double precision floating-point number.
Additional qualifiers are used to modify the basic types. These
qualifiers include:
short — modifies an int, and is a variable whose width is no
greater than that of the int. For example, with a compiler with a 32
bit int a short int could be 16 bits. You will find examples
where short and int are the same size.
10 Chapter 1 Introduction to C
long — modifies an int, and is a variable size whose width is
no less than that of an int. For example, on a 16-bit machine, an
int might be 16 bits, and a long int could be 32 bits, long can
also modify a double to specify an extended precision floating-point
number. You will find examples where a long and an int are the
same size.
signed — modifies all integral numbers and produces a range
of numbers that contains both positive and negative numbers. For
example, if the type char is 8 bits, a signed char can contain the
range of numbers -128 to +127. Default for char and int is
signed when they are declared.
uns igned — modifies all integral numbers and produces a range
of numbers that are positive only. For example, if the type char is 8
bits, an unsigned char can contain the range of numbers to
+255. It is not necessary to include the type int with the qualifiers
short or long. Thus, the following statements are the same:
long int a,c;
short int d;
and
long a,c;
short d;
When a variable is defined, space is allocated in memory for its
storage. The basic variable size is implementation dependent, and
especially for microcontrollers, you will find that this variability will
show up when you change from one microcomputer to another.
Each variable must be defined prior to being used. A variable
may be defined at the beginning of any code block, and the variable's
scope is the block in which it is defined. When the block in which the
variable is defined is exited, the variable goes out of existence. There
is no problem with defining variables with the same name in differ-
ent blocks. The compiler will make certain that these variables do
not get mixed up in the execution of the code.
An additional qualifier is cons t. When cons t is used as a quali-
fier on the declaration of any variable, an initialization value must be
declared. This value cannot be changed by the program. Therefore
the declaration
Types and Type Declarations 11
const double PI = 3.14159265;
will create the value for the mathematical constant pi and store it in
the location provided for PI. Any attempt to change the value of PI
by the program will cause compiler error.
Conventions for writing constants are straightforward. A simple
number with no decimal point is an int. To make a number long,
you must suffix it with an 1 or an L. For example, 6 04 7 is an int and
6 04 7L is a long. The u or U suffix on a number will cause creation
of a proper unsigned number.
A floating-point number must contain a decimal point or an ex-
ponent or both. The numbers 1.114 and 17.3e-5 are examples of
floating point numbers. All floating point numbers are of the type
double unless a suffix is appended to the number. Any number
suffixed with an f or an F is a single precision floating-point num-
ber, and a suffix of 1 or L on a floating-point number will generate a
type long double. Octal (base 8) and hexadecimal (base 16)
numbers can be created. Any number that is prefixed with a — a
leading zero — is taken to be an octal number. Hexadecimal numbers
are prefixed with a Ox or a OX. The rules above for L and U also
apply to octal and hexadecimal numbers.
The final type qualifier is volatile. The qualifier volatile
instructs the compiler to NOT optimize any code involving the vari-
able. In execution of an expression, a side effect refers to the fact that
the expression alters something. The side effect of the following state-
ment
a=b+c;
is that the stored value of a is changed. A sequence point is a point in
the code where all side effects of previous evaluations are completed
and no side effects from subsequent evaluations will have taken place.
An important consideration of the optimization is that if an expression
has no side effects, it can be eliminated by the compiler. Therefore, if
a statement involves no sequence point, or alters no memory, it is sub-
ject to being discarded by the compiler. This operation is not particularly
bad when writing normal code, but when working with microcontrollers
where events can occur as a result of hardware operations, not the
program, this optimization can utterly destroy a program. For example,
whenever the hardware can alter a stored value, the compiler should
12 Chapter 1 Introduction to C
be able to discard accesses to that value because the program never
alters the value. In such a circumstance, if you had an analog-to-digital
converter peripheral in your system, the program would never be re-
quired to read its return value more than once. "The program did not
change the value stored in the input location subsequent to the first
read, therefore its value has not changed and it is not necessary to read
the location again." This will always produce wrong results. The key
word vol at i 1 e indicates to the program that a variable must not be
optimized. Therefore, if the input location is identified as a vola-
tile variable, it will not be optimized and the problem will go away.
As a point of interest, accessing a volatile object, modifying an
object, modifying a file, or calling a function that does any of those
operations are all defined as side effects by the standard.
Storage Classes, Linkage, and Scope
Additional modifiers are called storage classes and designate
where a variable is to be stored and how it is initialized. These stor-
age classes are auto (for automatic), static, and malloced.
The first two storage classes are described in the following sections.
The storage class malloc provides dynamic memory allocation and
is discussed in detail in Chapter 2.
Automatic variables
For local variables defined within a function, the default storage
class is auto. An automatic variable has the scope of the block in
which it is defined, and it is uninitialized when it is created. Auto-
matic variables are usually stored on the program stack, so space for
the variable is created when the function is entered. When the stack
is cleaned up prior to the return at the end of the function, all vari-
ables stored on the stack are deleted.
As we saw in our first program example, variables can be initial-
ized at the time of declaration by assigning the variable an initial value:
int rupt=17;
An automatic variable will be assigned its initial value each time
the block in which it is declared is entered. If the variable is not
initialized at declaration, it will contain the contents of uninitialized
memory, which can be any value.
Storage Classes, Linkage, and Scope 13
Another class of variable isregister.A register class variable
is automatic, i.e., it comes into being at the beginning of the block in
which it is defined and it goes out of scope at the end of the block. If
a register is available in the computer, a register variable
will be stored in a register. To define a register variable, you should
use the form
register int roger=10;
These variables can be long, short, int, or char.
When a register is not available, a register variable will be
stored just like any other automatic variable. A programmer might
consider the use of register variables in code that contains "tight
loops" to save the time of memory accesses while executing the loop.
A bit of advice. Compilers have improved continuously over the past
years. With today's compilers, the optimizers are so efficient that the
compiler can probably do a better job of assigning register vari-
ables than the programmer. Therefore, it makes little sense to specify a
lot of regi s t er variables just to improve the efficiency of your code.
Static variables
Sometimes you might want to assign a value to a variable and
have it retain that value for later function calls. Such a variable can
be created by calling it s t at i c at its definition. There are two groups
of static variables: Local static variables which have a scope
of the function in which they are defined, and global or external
static class variables. Unless otherwise declared, all static class
variables are initialized to when they are created.
There are two groups of external static variables. Any exter-
nal variable is astatic class variable. It is automatically initialized
to the value when the program is loaded unless the value is other-
wise declared in the definition statement. An external variable that is
declared as static in its definition statement like
static int redoubt;
will have file scope. Remember normal external variables can be
accessed from any module in the entire program. A static exter-
nal variable can be accessed only from within the file in which it is
defined. Note that static variables are not stored on the stack, but
rather stored inastatic data memory area.
14 Chapter 1 Introduction to C
Inside of a function, the following declaration is made:
static int keep = 1;
When the program is loaded and executed, the value 1 is assigned
to keep. Thereafter, each time the function is entered, keep will
not be initialized but will retain the value assigned to it the last time
the function was executed.
Global variables can be designated as static. A global vari-
able that is stat ic is similar to a conventional global variable with
the exception that it can be accessed only from the file in which it is
declared.
If there is an external variable that is declared in one file that is to
be accessed by a function defined in another file, the function must
notify the compiler that the variable is external with the use of the
keyword extern. The following is an example of such an access.
In file 1:
int able;
int main (void)
{
long quickstart (void) ;
long r;
able=17;
l=quickstart ( ) ;
}
In file 2:
long quickstart (void)
{
extern int able;
/* do something with able */
Character Constants 15
return result;
}
When the file 1 is compiled, the variable able is marked as
external, and memory is allocated for its storage. When the file 2 is
compiled, the variable able is recognized to be external because of
the extern keyword, and no memory is allocated for the variable.
When the link phase of the compilation is completed, all address
references to able in file 2 will be assigned the address of able that
was defined in file 1. The example above in which the declaration
extern int able;
allowed access to able from the file 2 will not work if able had
been declared as follows in file 1 :
static int able;
Character Constants
Character constants or escape sequences are data that can be stored
in memory locations designated as char. A character constant is
identified by a backslash preceding the character. We have seen the
use of the character constants x \n' and v \t' in previous examples.
Several of these escape sequences shown in the following table have
predefined meanings.
Escape
Meaning
Sequence
\a
bell character
\b
backspace
\f
form feed
\n
new line
\r
carriage return
\v
vertical tab
\t
horizontal tab
\?
question mark
w
back slash
V
single quote
\"
double quote
\ooo
octal number
\xxx
hexadecimal number
16 Chapter 1 Introduction to C
If these constants are used within a program, they must be identified
by quotes. In the earlier example, the new line character was a part of
a string. Therefore, it effectively was contained in quotes. If a single
character constant is to be generated, the constant must be included in
single quotes. For example, a test might include a statement like
if (c!='\t # )
• • • •
This statement causes the variable c to be compared with the constant
x \ t ' , and the statement following the i f will be executed if they are
not the same. Another preprocessor command is #def ine. With the
#def ine command, you can define a character sequence that will be
placed in your code sequence whenever it is encountered. If you have
character constants that you wish to use in your code, these constants
can be identified as
#define CR x \xOd'
#define LF x \xOa'
#define BELL x \x07'
#define NULL x \x0 0'
and so forth.
We'll discuss the #def ine preprocessor command further later.
The following program shows use of an escape character.
/* Count lines of text in an input */
# include <stdio .h>
int main (void)
{
int c,nl=0; /* the number of lines is in nl */
while ( (c=getchar ( ) ) ! =EOF)
if (c=='\n' )
nl + + ;
printf ( "The number of lines is %d\n",nl);
return ;
Character Constants 17
Often you will want to leave "clues" as to what the program or
line of code is supposed to do. Comments within the code provide
this documentation. A C comment is delimited by
/* */
and the comment can contain anything except another comment. In
other words, comments may NOT be nested. The first line of code
in the above program is a comment, and the sixth line contains both
code and a comment. The compiler ignores all information inside
the comment delimiters.
This program uses two integer variables c and nl. The variable
c is the temporary storage location in which input data are stored,
and nl is where the number of input lines are counted.
The while statement contains a rather complicated argument.
At any point in a C program when a value is calculated, it can be
stored in a specified location. For example, in the whi le expression
while ( (c=getchar() ) != EOF)
the inner expression
c=getchar ()
causes the function get char ( ) to be executed. The return from
getchar ( ) is a character from the input stream. This character is
assigned to the variable c. After this operation is completed, the re-
sult returned from getchar ( ) is compared with the constant EOF.
EOF means end-of-file, and it is the value returned by getchar ( )
when a program tries to read beyond the end of the data stream. It is
defined in the file stdio . h. The symbol ! = is read "is not equal
to." Therefore, the argument of the while will be TRUE so long as
getchar ( ) does not return an EOF and the statement following
the while will be continually executed until an EOF is returned.
Operators in an expression that have the higher precedence will be
executed before the lower precedence operators. In the expression
c= getchar () != EOF
the operator ! = has a higher precedence than that of the = operator.
Therefore, when this expression is evaluated, the logical portion of
the expression will be evaluated first, and the result of the logical
18 Chapter 1 Introduction to C
evaluation — either TRUE or FALSE — will be assigned to the vari-
able c. This result is of course incorrect. To avoid this problem, use
(c = getcharO) != EOF
as the while argument. In this case, the parentheses group the
c=getchar ( ) expression and it will be completed prior to execu-
tion of the comparison. The variable c will have the correct value as
returned from the input stream. If the above expression is logically
true, then the value that was returned from the input stream is tested
to determine if it is a new line character. If a new line character is
found, the counter nl is incremented. Otherwise, the next character
is read in and the sequence repeated until an EOF is returned from
the get char ( ) . Whenever an assignment is executed inside of
another expression, always enclose the complete assignment expres-
sion in parentheses.
The final statement in the program
printf ( "The number of lines is %d\n",nl);
prints out the number of new line characters detected in reading the
input file.
Arrays
An array is a collection of like types of data that are stored in
consecutive memory locations. An array is designated at declaration
time by appending a pair of square brackets to the array name. If the
size of the array is to be determined at the declaration, the square
brackets can contain the number of elements in the array. Following
are proper array declarations.
extern int a [] ;
long rd[100] ;
float temperatures [1000] ;
char st [] ={ "Make a character array"};
float pressure[]={ 1.1, 2.3, 3.9, 3.7, 2.5, 1.5,
0.4};
As you can see, the size of an array must be designated in some
manner before you can use empty square brackets in the designation.
In the first case above, the array a [ ] is defined in global memory, so
all that is necessary for the compiler to know is that a [ ] is an array.
Arrays 1 9
The argument of an array is sometimes called its index. It is a num-
ber that selects a specific entry into an array. Array arguments start
with zero always. Therefore, when an array of 100 elements is cre-
ated, these elements are accessed by using the arguments to 99.
The standard requires that the first element beyond the end of the
array be accessible as an array entry. Attempts to access elements
beyond that will give undefined results.
Arrays can be initialized at declaration. The initialization values
must be enclosed in braces, and if there are several individual nu-
merical values, these values must be separated by commas. In the
case of a string initialization, it is necessary to include the string in
quotes and also enclose the string along with its quotation marks
within the braces. In both of these cases, the size of the array is
calculated at compile time, and it is unnecessary for the programmer
to figure the size of the array.
A string is a special case of an array. Whenever a string is gener-
ated in C, an array of characters is created. The length of the array is
one greater than the length of the string. The individual characters
from the string are placed in the array entries. To be a proper C
string, the array's last character must be a zero or a null. All strings
in C are null terminated. If you as a programmer create a string in
your program, you must append a null on the end of the character
array to be guaranteed that C will treat the array as a string.
If the programmer should specify an array size and then initial-
ize a portion of the array like
int time [6] ={1,5,3,4} ;
the compiler will initialize the first four members of the array with
the specified values and initialize the remainder of the array with
zero values. This approach allows you to initialize any array with all
zero values by
long ziggy [100] ={0} ;
which will fill all of the elements of the array z iggy [ ] with zeros.
C provides you with no array boundary checking. It is the
programmer's responsibility to guarantee that array arguments do
not violate the boundaries of the array.
20 Chapter 1 Introduction to C
Other types
There are mechanisms for creating other types in C. The three
other types are enum, union, and struct. It is often quite conve-
nient to make use of the data types to accomplish things that are difficult
with the normal types available. We will see how to use these types in
this section.
The enum
The name enum is used in C in a manner similar to the #de-
f ine preprocessor command. The enum statement
enum state { OUT, IN};
produces the same result as
#define OUT
#define IN 1
Here, the name state is called the tag name. In this case OUT will be
given a value of and IN a value 1. In the enum{ } form, unless
specifically assigned, the members will be given successively increas-
ing values and the first will be given a value 0. Values can be assigned
by an enum{ } ;
enum months {Jan =l,Feb, Mar, April, May, June,
July, Aug, Sept, Oct, Nov, Dec};
will cause Jan to be 1, Feb 2, and so forth up to Dec which will be 12.
Each member can be assigned a different value, but whenever the
programmer assignments stop, the values assigned to the variables
following will be successively increased. These values are, by de-
fault, of the int type. The name months in the above expression is
called a tag name. An enum creates a new type and you might have
several enums in your code that you would wish to create as in-
stances. The key word enum with its tag name identifies the specific
enum when it is used as a type identifier in a definition statement.
Another example
enum ( FALSE , TRUE , Sun= 1 , Mon ,
Tues,Wed,Thur, Fri, Sat) ;
Other Types 21
will result in FALSE being 0, TRUE 1, Sun 1 , Mon 2, and so
forth to Sat 7. Note that it is not necessary to assign a tag name to
an enum.
An enum is typed at declaration time. Therefore, the values cre-
ated by an enum are indeed numerical values. This differs from the
# define because the statement
#define FALSE
will cause the character '0' to be inserted into the source code when-
ever the label FALSE is encountered. As such, the #de f ine construct
is a character substitution technique or a macro expansion. The re-
sult of an enum is a numerical substitution. The # define construct,
being a simple character substitution, has no typing attached to its
arguments. Constants created by an enum are typed, and therefore,
will avoid many of the potential hazards of dealing with untyped
variables.
Let us examine how one might use a type created with an enum
construct. The following enum defines two constants
enum direction {LEFT, RIGHT} ;
In a program, a definition statement
enum direction d;
will cause a variable d to be created. The acceptable values for d are
the names LEFT and RIGHT. We know, of course, that the numerical
value for LEFT is and the value for RIGHT. Within your program,
you can assign and test the value of d. For example,
if (d==LEFT)
do something
or
if (d==RIGHT)
do something else
or
d = RIGHT;
As stated earlier, the acceptable values for d are LEFT and RIGHT.
There is no checking within the program to see if the programmer
22 Chapter 1 Introduction to C
has indeed kept the trust. Therefore, it is possible to assign any inte-
ger value to d, and the program will compile. It probably will not
work correctly, however.
The Union
The union was invented when memory was very dear. The main
purpose of the union was to allow the storing of several variables at
a single memory location. A union has a tag name much the same
as the enum above.
union several
{
long biggie;
int middle_size;
char little, another_char ;
short little_bigger ;
};
The union several contains several members. These members
are not necessarily of the same type and there can be multiple in-
stances of the same type. To create an instance of such a union, you
need a definition statement. This statement can be external or imme-
diately following the opening of a code block, and hence local. Such
a statement might be
union several these;
This definition causes a union several named these to be cre-
ated with memory allocated. To access the members of the union,
you can use the dot operator as
these. biggie = something;
or
another = these . another_char;
An interesting feature of a union. If you should check the size
of a union, you would find that it is the size of the largest of its
members. Whenever you access, either read or write, a union, the
proper size data is written or read, and it overwrites any other data
that might be found in the memory location. Therefore, you can use
a union for storage of only one of its members at a time and writing
Other Types 23
anything to the union destroys any data previously stored to the
union.
The struct
Yet another type is the struct. The struct is a collection of
things much like the array. In the case of the struct, there are two
major differences. A struct can contain different types, and the
struct itself is a first class type. An array must be a collection of
like types, and an array is NOT a type, so the type-like things you
can do with a struct are not available for an array.
You create a struct in much the same form as was seen with a
union. You may use a tag name.
struct able
{
char a,b;
int c,d;
};
This struct is made up of two characters and two integers. If you
wish to define an instance of the struct, you should use
struct able here:
Access the members of the struct with the dot operator like
here . a = x a' ;
here.b = 16;
here.c = 32 000;
here.d = -16500;
We will see more of struct in Chapter 2 where you will learn
how to make use of the new types created by struct.
EXERCISES
1 . Write a program that reads all of the characters from an input file
and prints the characters on the screen. Use the get char ( ) func-
tion used earlier to read the inputs and the put char ( c ) to print
the results to the screen.
2. Modify the above program to count the number of characters in an
input stream.
24 Chapter 1 Introduction to C
3. Write a program that reads the characters from an input file and
counts in an array the occurrences of each letter. Make the pro-
gram "case insensitive" by treating all upper case letters as lower
case.
Operators and Expressions
The variables and constants discussed in the previous section are
classed as operands. They are values or objects that are operated
upon by a program. The operations that take place are specified by
operators. This section contains a discussion of the several opera-
tors.
Operators abound in C. All of the symbols involved in the lan-
guage are operators. Each has a precedence and an associativity. This
section is concerned with how operators and operands are put to-
gether to interact in a manner desired by the programmer.
Arithmetic Operators
The arithmetic operators are those used to perform arithmetic
operations. These operators are:
/
%
These operators are called binary operators because they are al-
ways used with two operands. These operands are placed on either
side of the operator. The symbol + designates arithmetic addition,
and the - symbol designates subtraction. The symbols * and /
designate multiplication and division, respectively. These opera-
tors are clearly different for different variable types. The compiler
understands these differences and creates correct code for the
operand types involved. The modulus operator % returns the re-
mainder after an integer division. The modulus operator works only
on integer types — int , char , and long. It cannot be applied to
types float , double or long double.
Operators and Expressions 25
Two unary operators are + and -. These operators are of higher
precedence than the normal arithmetic operators. They operate on
only the operand written to the right of the operator and are therefore
called unary. The unary minus sign causes the negative value of the
operand to be calculated, and the unary positive sign causes no cal-
culation to take place.
Among the binary operators, * , / , and % have equal prece-
dence, which is higher that of + and - . The unary operators + and -
have a higher precedence than * , / , or %. The arithmetic operators
will work with any of the arithmetic types. Because the operations
needed for an integer operation differ from those needed for the cor-
responding double operation, the compiler will place the proper
arithmetic routines in the code to perform the specified operation.
The concept of a fraction is almost unknown to an integer type.
If a division of two integers is executed, the result is rounded toward
zero. Therefore, the result of 1/2 is as is 9999/10000. This charac-
teristic is often used in programming.
The only way that you can handle fractions with integer opera-
tions is to make use of the modulus operation. The result of a %b is
the remainder that is left over after a is divided by b. The modulus
operation can provide insight into the fractional value of what is left
over after an integer divide.
EXERCISES
1. Write a program that evaluates
f(x) = X 2 -3X + 2
for values of X in < X < 3 in steps of 0. 1 .
2. The roots of a quadratic equation can be evaluated by the equation
x = (-b + sqrt(b 2 - 4ac))/2a
and
x = (-b - sqrt(b 2 - 4ac))/2a
where the quadratic equation is ax 2 + bx + c = 0. Write a pro-
gram that will evaluate the roots of such an equation. Note that the
term sqrt (b 2 - 4ac ) is called the discriminant. If its argument
26 Chapter 1 Introduction to C
is not positive, the square root of a negative number is imaginary
and the equation has complex roots. Handle both real and complex
roots in your program.
Relational or Logical Operators
The relational operators are all binary operators. When contained
in an expression, the program will evaluate the left operand and then
the right operand. These operands will be compared, and if the com-
parison shows that the meaning of the operator is correct, the program
will return 1. Otherwise, the program will return a 0. In the vocabu-
lary of C, FALSE is always zero. If calculated by a logical expression,
TRUE will always be one. However, if the argument of a conditional
expression is anything but zero, it will respond as if the argument is
TRUE. In other words, FALSE is always zero and TRUE is anything
else. The relational operators are:
< (less than)
< = (less than or equal to)
> (greater than)
>= (greater than or equal to)
These operators all have the same precedence, which is slightly higher
than the following equality operators:
(is equal to)
! = (is not equal to)
The logical operators are && and | | . The first operator indicates a
logical AND and the second a logical OR. A logical AND will return
TRUE if both of its operands are TRUE, and a logical OR will return
TRUE if either of its operands is TRUE. The logical OR has lower
precedence than the logical AND. The precedence of the logical AND
is lower than the precedence of the relational operators and the equal-
ity operators.
In the evaluation of long logical expressions, the program starts
on the left side of the expression and evaluates the expression until it
knows whether the whole expression is true or false, and it then exits
the evaluation and returns a proper value. For example, suppose there
Operators and Expressions 27
is a character c, and it is necessary to determine if this character is a
letter. In such a case, the following logical expression might be used:
if ( c >= X A' ScSc c <= X Z' | | c >= x a' && c <= x z')
The logical and operator && has lower precedence than any of the
relational operators, so the relational expressions will each be evalu-
ated prior to the && operations. If upon entering this expression, c is
equal to the character x 5 ' , which is arithmetically smaller than any
of the letters, the first term c >= x A' will be FALSE. Therefore, the
result of the first logical and expression is known to be FALSE
without evaluating the term c < = x Z ' . The evaluation will then
skip to the third term c >= x a' , and the term c <= X Z' will not be
evaluated. In this case, the character x 5 ' will be smaller than the
character x a ' so that the second and expression will also be FALSE.
Therefore, the logical value will be known after evaluation of only
two of the logical terms of the argument rather than having to evalu-
ate all four of the terms.
EXERCISES
1 . Write a function that converts a character that is a letter to lower
case.
2. Leap years occur every four years unless the year happens to be
divisible by 100. Any year divisible by 400 is a leap year, however.
Write a logical expression that will return TRUE if the given year
is a leap year and FALSE if it is not.
Type Conversions Within Expressions
Implied in our earlier discussions on variable types, different data
types not only occupy different width in memory, some may be com-
pletely incompatible when attempting to execute operations involving
mixed data types. In earlier languages, it was up to the programmer
to guarantee that the data types involved with an operation were the
same. C resolves this problem, and the compiler will select the proper
data type to complete operations on mixed data types.
Each data type has an implied width. When an operation is to be
executed on mixed data types, the widths of the two types are evalu-
ated, and the lesser width operand is promoted to the type of the
28 Chapter 1 Introduction to C
greater width operand prior to execution of the operation. Thus, if
the program called for d = a * b, where disof type long, a is
type int, and b is type long, a will be converted to the type long
prior to the multiplication.
This logic carries over to mixing of float and double types
as well. If for example a program called for the division a/b where
a is of the type int and b is of the type double, the program would
convert a to the type double before execution of the divide.
There might be times when the programmer will want to change
the type of a variable. C provides a cast operator which forces the
program to convert the type of a variable to a different type. This
unary operator has the form.
(type name) expression
where the results of the evaluation of the expression will be con-
verted to the named type contained within the parentheses preceding
the expression.
Bitwise Operators
Operators that work on the individual bits within a variable are
called bitwise operators. Following is a table of all of these opera-
tors:
& bitwise AND > > right shift
| bitwise Inclusive OR < < left shift
bitwise Exclusive OR ~ one's complement
The first three bitwise operators are traditional binary operators.
These binary operators operate in integer type (char, int, long,
etc.) operands, and the two operands must be of the same type.
If a bitwise AND is executed, those locations in the result where
both operands have bit values of 1 will have a value of 1. All other
locations will be 0. For a bitwise inclusive OR, each bit in the
result will be 1 when either or both operand bits are 1. All locations
where both operand bits are will be 0. The exclusive OR is
similar to an addition with no carry. Whenever the bits in the oper-
ands are different, the result bit will be 1. If both operand bits are the
same, either both bits 1 or both bits 0, the result will be 0.
Operators and Expressions 29
The right shift operator and the left shift operator are also bi-
nary operators. Here the types of the operands need not be the same.
The expression
x >> 3
causes the variable x to be shifted to the right by three bits prior to its
use. Likewise,
y << 5
will cause y to be shifted to the left by five bits. In all number sys-
tems, a left shift by one digit corresponds to a multiplication by the
number base. Similarly, a shift to the right by one digit causes a
division by the number base. We are using the binary system in this
case, so a shift left by one bit causes the number to be multiplied by
two. Unlike most number systems, the binary system (or two's
complement system) allows the sign of the number to be contained
in the binary representation of the number itself. These consider-
ations lead to two different types of shifts for a system of binary
numbers. A shift in which bits vacated by the shift are replaced by
zeros is called a logical shift. All left shifts are logical shifts. As the
shift progresses toward the left, bits that fill the number from the
right will all be zero. Bits that shift out of the number on the left side
are lost. A right shift can be either a logical or an arithmetic shift. If
the type being shifted is signed, the sign bit — which is the leftmost
bit — will propagate, retaining a number of the same sign. This is an
arithmetic sign. If the number being shifted is unsigned, zeros are
filled into the number from the left as the shift proceeds. In all cases,
bits shifted out of a number by a shift operation will be lost. The
one's complement operator ~ is a unary operator that causes the bits
in a variable to be reversed. Every 1 is replaced by a 0, and every is
replaced by a 1 . The bitwise AND and OR operations are used to turn
bits on and off. Suppose that we have a character variable r, and we
wish to turn the least significant three bits off. Try
r = r & ~7;
In this case, the number 7 has each of the least significant bits turned
on or 1. Therefore, the term ~7 has all of the bits in the number but
the least significant turned on and these three bits are turned off or 0.
30 Chapter 1 Introduction to C
When this mask is ANDed with r, all of the bits of r , with the
exception of the least significant three bits, will be ANDed with a 1,
and these bit values will remain unchanged. The least significant
three bits will be ANDed with and the result in these three bits will
be 0. The bitwise OR will turn bits on. Suppose you wanted to turn
bits 2 and 3 of r above on. Here you would use
r = r | 0x0c;
The hexadecimal number 0x0 c is a number that has bits 2 and 3
turned on and all other bits turned off. This OR operation will leave
bits 2 and 3 on and all other bits will remain unchanged. Suppose
that you want to complement a bit in a variable. For example, bit of
the memory location PORTA must be toggled each time a certain
routine is entered. The expression
PORTA = PORTA A 1;
will perform this operation. All of the bits except for bit 1 of PORTA
will remain unchanged because the exclusive OR of any bit with a
will not change the bit value. However, if bit 1 is 1 in PORTA the
exclusive OR will force this bit to 0. If this bit is 0, the exclusive OR
will force this bit to a 1 . Therefore, the above expression will comple-
ment bit of PORTA each time it is executed.
The bitwise operators & , | , and A are of lower precedence
than the equality operators, and higher precedence than the logical
AND operator. The bit shift operators are of the same precedence, of
lower precedence than the arithmetic operators + and - , and of higher
precedence than the relational operators.
Increment and Decrement Operators
When the C language was written, every effort was made to write
a language that is concise and yet unambiguous. Several powerful
short-hand operators were included in the language that will shorten
the program. The increment and decrement operators are examples
of such short-hand operators. In the examples earlier there were in-
stances of expressions such as
i = i + 1 ;
Increment and Decrement Operators 31
Here the i value stored in memory is replaced by one more than the
value found there at the beginning of execution of the expression.
The C expression
+ + i
will do exactly the same thing. The increment operator ++ causes 1
to be added to the value in the memory location i. The decrement
operator - - causes 1 to be subtracted from the value in the memory
location. The increment and decrement operators can be either pre-
fix or postfix operators. If, like above, the ++ operator precedes the
variable, it is called a prefix operator. If the variable is used in an
expression, it will be incremented prior to its use. For example, sup-
pose i = 5. Then the expression
j = 2 * ++i
will leave a 12 for the value j and 6 for i. On the other hand, if i
again is 5, the expression
j = 2 * i--;
will leave a value of 10 for j and 4 for I.
An easy way to see how the preincrement and the postincrement
works is as follows: Suppose that you have a pair of statements
<statement with j>
These statements can be replaced with
<statement with ++j>
The preincrement means that you should replace j with j+1 before
you evaluate the expression. Likewise the statements
<statement with j>
can be replaced with
<statement with j++>
with the post increment, you should evaluate the expression and then
replace j with j+1.
32 Chapter 1 Introduction to C
Often somebody will wonder what will happen if you have mul-
tiple increments, either pre or post, of a variable within a single
expression. There is an easy answer for that question. Do not do it.
The standard provides that between sequence points, an object shall
have its value modified at most once and the prior value of the object
shall be accessed only to determine its value. Interpretations of the
above requirements disallow statements such as
J = j++;
or
a[j] = j++;
or
m = j ++ + ++j ;
Assignment Operators
Another shorthand that was included in C is called the assign-
ment operator. When you are programming, you will find that
expressions such as
i = i+2;
or
X = X<<1;
are used often. Almost any binary operator can be found on the right
side of the expression. A special set of operators was created in C to
simplify these expressions. The first expression can be written
i += 2;
and the second
X < < = 1 ;
These expressions use what is defined as an assignment operator.
The operators that can be used in assignment operators are
+ »
«
Increment and Decrement Operators 33
/
o,
o
If you have two expressions el and e2, and let the operand $ repre-
sent any binary C operator, then
el $= e2;
is equivalent to
el = (el) $ (e2) ;
The precedence of all of the operator assignments are the same and
less than the precedence of the conditional operator discussed in the
next section. These operators assignments and the = operator are
associated from right to left.
The Conditional Expression
Another code sequence found frequently is
if (expl)
exp2 ;
else
exp3 ;
The logical expression expl is evaluated. If that expression is TRUE ,
exp2 is executed. Otherwise, exp3 is executed. In the compact no-
tation of C, the above code sequence can be written
expl ? exp2 : exp3 ;
This expression is read if expl is TRUE, execute exp2. Otherwise,
execute exp3. Another way of stating this is that if expl is TRUE,
the value of the expression is exp2; otherwise the value of the ex-
pression is exp3.
The conditional expression is found often in macro definitions,
which we'll discuss later.
EXERCISES
1. Write a program to determine if a number is even or odd.
2. Write a function that determines the number of bits in an integer
on your machine.
34 Chapter 1 Introduction to C
3. Write a program that will rotate the bits in the number 0x5 aa5 to
the left by n bits. A rotate differs from a shift in that the most
significant bit will be shifted into the least significant bit during
the rotation. A shift merely shifts zeros into the least significant
bit.
4. An arithmetic right shift propagates the most significant bit to the
right when the number is shifted right. If zeros are shifted into the
most significant bit, the shift is called a logical right shift. Write a
program that determines whether your compiler implements a logi-
cal or arithmetic right shift with the operator » with both signed
and unsigned arithmetic.
5. Write a function upper(c) that returns the upper case letter if the
character c is a lower case letter. Otherwise it shall return the char-
acter c.
6. If you used the if() else construct in problem 4, rewrite the func-
tion to use the conditional expression.
Precedence and Associativity
Here is a summary of the rules of both precedence and associa-
tion of all C operators. The higher an operator falls in the table, the
higher its precedence. Operators that fall on the same line are all of
the same precedence. All symbols used in C are operators. There-
fore, the operator () refers to the parentheses enclosing the arguments
to a function call. The operator [] refers to the brackets enclosing the
argument of an array. The period operator . and the comma operator ,
will both be discussed when introduced. Likewise, the -> and the
sizeof operators will be introduced later.
Operator Associativity
() [] -> . left to right
! -++ — +-*& (type) sizeof right to left
* / % left to right
+ - left to right
< < > > left to right
Precedence and Associativity 35
< < = > = > left to right
= = != left to right
& left to right
left to right
| left to right
&& left to right
| | left to right
? : right to left
= += -= *= /= %= 8c= A = |= <<= >>= right to left
, left to right
Note the very high precedence of the parentheses and the square
brackets. It is the high precedence of these operators that allows the
programmer to force operations that are not in line with the normal
precedence of the language. The second highest precedence is the
list of unary operators. These operators are all associated from right
to left.
EXERCISES
1 . Which of the following words are valid names for use in a C pro-
gram?
able toots
What_day_is_it WindowBar
_calloc 8arnold
Hurting? value
constant Constant
sizeof continue
2. Write a program to evaluate the constant
( 1.0377x107 + 3.1822x103 ) / ( 7.221x104 + 22.1x106 )
The answer will be 0.468162.
3. Write a function that raises the integer x to the power n. Name the
function x_to_the_n, and write a program that evaluates
x to the n for several different values of both x and n.
36 Chapter 1 Introduction to C
4. Write a program that will examine a specified year and determine
if it is a leap year.
5. Write a program that will count the number of digits in an input
file. Record and print out the number of occurrences of each digit.
6. In C the term "white space" refers to the occurrence of a space, a
tab character, or a new line character. Write a program that will
evaluate the number of white space characters in an input file.
Program Flow and Control
Program flow and control comprise several different means to
control the execution of a program. Looping constructs, for example,
control the repeated execution of a program segment while adjusting
parameters used in the execution at either the beginning or the end of
the loop. Two way branches are created by if/else statements,
and the choice of one of many operations can be accomplished with
the else if or switch/case statements. The following para-
graphs will provide a quick look at each of these program flow and
control methods.
The While Statement
There are three looping constructs available to the C programmer:
the while ( ) statement, the for ( ; ; ) statement and the do/
while ( ) statement. The following program demonstrates the use of
the whi le looping construct along with some other concepts. We have
seen the whi le statement earlier, but the following program will pro-
vide a new look at its use.
# include <stdio .h>
int main (void)
{
int guess, i ;
i=l;
guess = 5;
while (guess != i)
{
Program Flow and Control 37
1 = guess;
guess = (i + (10000/i) ) /2 ;
}
printf ( u The square root of 10000 is
%d\n" , guess) ;
return ;
}
As in the first example, the # include statement is used to bring
standard input/output features into the program, and the program
starts with the function definition main ( ) . Inside of the main pro-
gram, the first statement is
int guess, i;
This statement defines the variables guess and i as integers. No
value is assigned to i at this time, but a space in memory is allocated
to guess and i and the space is sufficient to store an integer. The
first executable statement in the program is
i=l;
This statement is called an assignment statement. The equal sign here
is a misnomer. The statement is read "replace the contents of the
memory location assigned to i with a 1." The next statement
guess = 5;
assigns a value 5 to the variable guess. The statement
while (guess != i)
invokes a looping operation. The while operation will cause the
statement following to execute repeatedly. At the beginning of each
loop execution, the while argument guess ! =i is checked. This
argument is read "guess is not equal to i." So long as this argument is
TRUE, the statement following the while will be executed. When
guess becomes equal to i, the statement following the while will
be skipped.
The while is followed by a compound statement that contains
two statements:
{
i=guess;
38 Chapter 1 Introduction to C
guess = (i + (10000/i) ) /2 ;
}
This calculation is known as a Newton loop. It states that if i is a
guess at the square root of 10000, then (i+ (10000/i) ) /2 is a
better guess. The loop will continue to execute until i is exactly
equal to gue s s . At this time the compound statement will be skipped.
When the statement following the while is skipped, program
control is passed to the statement
printf ( "The square root of 10000 is %d\n" , guess) ;
This statement prints out the value of the last guess, which will be
the square root of 10000.
The For Loop
Many times, a sequence of code like
statementl;
while (statement2)
{
statement3 ;
}
will be found. This exact sequence was seen in the above example.
There is a shorthand version of this sequence that can be used. It is as
follows:
for (statementl ; statement2 ; statement3)
The for construct takes three arguments, each separated by semi-
colons. In operation, the for construct is compiled exactly the same
as the above sequence. In other words, statementl is executed
followed by a standard while with statement 2 as its argument.
The compound statement that follows will have statement 3 placed
at its end, so that statements is executed just prior to completion
of the statement following the while construct. The for construct
can be used to write the above program in the following manner:
# include <stdio .h>
Program Flow and Control 39
int main (void)
{
int guess, i ;
for (i=l , guess=5 ; i ! =guess ; )
{
i=guess ;
guess= (i+(10000/i))/2;
}
printf ( x% The square root of 10000 =
%d\n" , guess) ;
return ;
}
Recall that the for allows three arguments. Not all arguments
are necessary for proper execution of the for. In this case, only two
arguments are included. The first argument is really two initializa-
tion arguments separated by a comma operator. When the comma
operator is used, the statements separated by commas are each evalu-
ated until the semicolon is found. At this time, the initialization is
terminated. By the way, the comma operator can be used in normal
code sequences so that you can string several statements in a row
without separating them with semicolons. The second argument of
the for construct is i ! = guess. The for loop will execute so
long as this expression is TRUE. Note that there is no third statement
in the for invocation.
This argument is where you would normally place the change in
i that is to take place at the end of each loop. In this case, the opera-
tion on i is i=guess. If this expression were used for the third
argument, at the end of the first loop, the second argument would be
FALSE, and execution of the calculation would be prematurely ter-
minated.
The Do/While Construct
Another looping structure is the do /while loop. Recall that the
argument of a whi le statement is tested prior to executing the state-
ment following. If the argument of the while is FALSE to begin
with, the statement following will never be executed. Sometimes, it is
40 Chapter 1 Introduction to C
desired to execute the statement at least once whether the argument is
TRUE or not. In such a case, the argument should be tested at the end
of the loop rather than at the beginning as with the whi le. The do/
while construct accomplishes this operation. The construction of a
do-while loop is as follows
do
{
} while (expression) ;
•
The program will enter the do construct and execute the code that
follows up to the while statement. At that time, the expression is
evaluated. If it is TRUE, program control is returned to the statement
following the do. Otherwise, if the expression evaluates to FALSE,
control will pass to the statement following the while. Notice that
there is a semicolon following the whi 1 e ( exp r e s s i on ) . This semi-
colon is necessary for correct operation of the do -while loop.
The following function converts the integer number n into the
corresponding ASCII string. The function has two parts: the first
part converts the number into an ASCII string, but the result is back-
ward in the array; the second part reverses the data in the array so
that the result is correct.
/* convert a positive integer to an ASCII string;
valid for positive numbers only */
void itoa (unsigned int n, char s [] )
{
int i=0 , j =0 , temp;
/* convert the number to ASCII */
do
{
Program Flow and Control 41
s [i+ + ] = x 0' + n % 10;
n /=10;
} while ( n != 0) ;
s[i]=0; /* make the array a string */
/* but it is backwards in the array — reverse
t*/
i — ; /* don't swap the NULL */
while ( i > j )
{
temp = s [j ] ;
s [j+ + ] = s [i] ;
s [i--] = temp;
The function uses three integer variables. The variables i and j
are both initialized to zero, and the variable temp does not need to
be initialized. The first portion of the program contains a do - whi le
loop. Within this loop, the number is converted into a string. The
statement
s [i + + ] = x 0' + n % 10;
first calculates the value of the integer modulo 10. This value is the
number of Is in the number. Adding that value to the character x '
will create the character that corresponds to the number of Is. This
value is stored in the location s [i] with i = and then i is
incremented.
The second statement in the loop replaces n with n divided by
10. This code removes any Is that were in the number originally,
and now the original 10s are in the Is position. Since this division is
an integer division, if the result is between and 1 it will be rounded
to 0. Therefore, the test in the while argument allows the above two
statements to repeat until the original number n is exhausted by re-
peated divisions by 10.
When the do-while loop is completed, s [ i ] will be the charac-
ter immediately following the string of characters. A string is created
by placing a or a null in this location of the array.
42 Chapter 1 Introduction to C
To reverse the data, the program starts by decrementing i so that
s [ i ] , the last entry in the array, is the most significant character in
the number, and it must be placed in the first array location s [ ] : .
Likewise, the character in s [ ] must be placed in s [ i ] : and so
forth. The while loop that follows accomplishes this requirement.
EXERCISES
1. Write a program atoi (s [] ) that starts with a character string
and converts this string to an int. Assume that the string contains
no sign.
2. Write a program that reads a text file one character at a time and
counts the number of words in the file.
The If/Else Statement
The if/else statement has the general form
if (expression)
statementl ;
else
statement2 ;
If the logical evaluation of the expression that is the argument of the
if is TRUE, statementl will be executed. After statementl
is executed, program control will pass to the statement following
statement 2, and statement 2 will not be executed. If the evalu-
ation of statement is FALSE, statement 2 will be executed, and
statementl will be skipped. The else statement is not neces-
sary. If there is no else statement, the expression is evaluated. If it
is TRUE, statementl will be executed. Otherwise, statementl
will be skipped. The following program demonstrates the use of the
if /else flow control method.
/* count number of digits and other characters in
input */
# include <stdio .h>
int main (void)
Program Flow and Control 43
int c,nn,no;
no=0 ;
nn=0 ;
while ( (c=getchar ( ) ) ! =EOF)
if (c>= # ' &&c<=' 9 ' )
nn++ ;
else
no++ ;
printf ( "Digits=%d and other characters=%d\n // , nn, no) ;
return ;
}
The statement
int c,nn,no;
declares the three variables c , nn , and no to be integers. You may
declare as many variables as you wish with a single declaration state-
ment. The next statements
no=0 ;
nn=0 ;
initialize the values of no and nn to 0. Variables declared with the
above sequence of instructions are automatic variables. These vari-
ables are not initialized by the compiler, and the programmer must
initialize them to a required value. Otherwise the variables will con-
tain garbage.
The code sequence
while ( (c = getcharO) !=EOF)
if (c>=' 0' ScSc c<='9' )
nn++ ;
else
no++ ;
comprise the while and its following statement. The if portion of
the statement tests the value of c and determines if it is a digit. A
character constant is identified as a specific value by placing the char-
acter value in single quotes. Therefore, the expression c>= ' '
determines if the character in the location c is greater than or equal
44 Chapter 1 Introduction to C
to the character 0. If it is, the result of this expression is TRUE. Oth-
erwise, the result is FALSE. The expression c<= ' 9 ' determines if
the input character is less than or equal to the character 9. If both of
these logical expressions are TRUE, then the AND of the two will be
TRUE, and the statement nn++ will be executed to count the num-
bers found in the input stream. Program control will then skip to the
end of the if statement and continue to execute the while loop. If,
on the other hand, either of these expressions is FALSE, then the
AND of the two results will be FALSE and the statement no++ will
be executed. This statement keeps count of the number of characters
that are not digits found in the input stream.
At the conclusion of the program, the getchar ( ) will return
an EOF character and the program will fall out of the whi 1 e loop. It
will then execute the following statement:
printf ( "Digits=%d and other characters = %d\n" , nn, no) ;
The string contained within the double quotes in this argument causes
a combination of text plus calculated values of variables to be printed
out. Suppose that the program found 5 1 numbers and 488 other char-
acters. The printout from the program would then be:
Digits=51 and other characters=488
Each %d is associated with its corresponding argument and converted
to a numerical value before it is sent to the screen.
The If -Else If Statement
Sometimes it is necessary to select among several alternatives.
One of the methods that C offers is the if-else if sequence.
Examine the following program that counts the number of occur-
rences of each vowel in an input. The program also counts all other
characters found in the input.
/* Count the number of occurrences of each vowel
found in an input and also count all other charac-
ters. */
# include <stdio .h>
int main (void)
Program Flow and Control 45
int na=0 , ne=0 , ni=0 , no=0 , nu=0 ;
int nother = , c ;
while ( (c=getchar ( ) ) ! =EOF)
if (c=='A' | | c=='a' )
na=na+l ;
else if(c=='E'
ne=ne+l ;
else if (c== # I '
ni=ni+l ;
else if(c=='0'
no=no+l ;
else if(c=='U'
nu=nu+l ;
else
nother=nother+l ;
printf ( "As=%d, Es=%d, Is=%d, Os=%d, Us=ld and"
11 Others = %d\n" , na, ne, ni , no, nu, nother) ;
return ;
c=='e' )
c=='i' )
c=='o' )
c=='u' )
This program shows several new features of C. The first is found in
the program lines
int na=0 , ne=0 , ni=0 , no=0 , nu=0 ;
int nother= , c ;
When the variables na and so forth are defined, they are assigned
initial values of 0. Such an initialization is always possible when
variables are defined. The next statement of the program is
while ( (c=getchar ( ) ) ! =EOF)
if (c=='A'
na=na+l ;
else if(c=='E'
ne=ne+l ;
else if (c== # I '
ni=ni+l ;
else if(c=='0'
no=no+l ;
else if(c== / U /
c=='a' )
c=='e' )
c== , i / )
c== , o' )
c=='u' )
46 Chapter 1 Introduction to C
nu=nu+l ;
else
nother=nother+l ;
This single statement has quite a few lines of code associated with it,
and there are some new concepts here. First, the arguments of the if s
are combinations of two logical expressions. The expression
c=='A' | | c=='a'
says that if c is equal to uppercase a OR i f c is equal to lowercase
a the argument is TRUE. The vertical bars | | are the logical opera-
tor OR.
The first if statement is evaluated. If its argument is TRUE, the
statement following the i f is executed and program control moves
to the end of the if statements. Otherwise, the first else if state-
ment argument is evaluated. If this argument is TRUE, the following
statement is executed and program control moves to the end of the
if statements. This process is repeated until one of the arguments is
found to be TRUE, or all of the else if statements are evaluated.
At that time, the final statement following the else entry is evalu-
ated. The final else is not required.
In the above statement, please note that the while statement
itself and all that follows it form a single statement to the compiler.
Likewise, the combination of all of the if-if else constructs
also form a single statement. Furthermore, each of the statements
following either an i f or an i f else form single statements. The
formatting of this statement helps you understand what is going on,
but remember, the format of such a statement is completely up to the
programmer. The language is completely free format. The while
statement above and its statement following is indeed confusing to
observe, and probably the one thing that the programmer can do to
reduce the confusion is to block the statements following both the
while and the if and else key words. In this case the while
statement would look like
wh
ile
((c
=getchar
0) !
=EOF)
{
if (
c=='
A'
c=
==' a'
*
{
Program Flow and Control 47
na=na+l ;
else if (c=='E'
ne=ne+l ;
else if (c== # I '
ni=ni+l ;
else if (c=='0'
no=no+l ;
else if (c=='U #
nu=nu+l ;
else
nother=nother+l ;
c=='e' )
c=='i' )
c=='o' )
c=='u' )
This block of source code is longer than the original form, but it
is exactly the same to the compiler. This code is indeed longer than
the original form, but it is also much less open to misinterpretation
and misunderstanding. Writing code that cannot be misinterpreted is
as much the responsibility of the programmer as writing code that
works. Therefore, when writing production code, you should seri-
ously aim to generate code that is not open to any misinterpretation,
even though it will make your source code somewhat longer. Usu-
ally when you write code that is easy to maintain, it will not affect
the size of your object code.
The print f function call
printf ("As=%d, Es=%d, Is=%d, Os=%d, Us=%dand"
u Others=%d\n" , na, ne, ni, no, nu, nother) ;
48 Chapter 1 Introduction to C
has the normal print f arguments. Note however that the string is
not confined to one line. C compilers will not allow a string to be
split among several lines in a program. However, an ANSI C compli-
ant compiler will cause two adjacent strings to be concatenated into
a single string, so the above code will compile without error.
Break, continue, and goto
These commands will cause a C program to alter its program
flow. If a break statement is encountered, the program will exit the
loop in which it is executing. Break can be used to exit for, whi le,
do-while, and switch statements. An example of the use of the
break statement is shown in the next section.
The continue statement causes the next iteration of a for,
while, or a do while loop to be started. In the case of the for
statement, the last argument is executed, and control is passed to the
beginning of the for loop. For both the while and the do-while,
the argument of the while statement is tested immediately, and the
program proceeds according to the result of the test.
The break statement is seen frequently, and the continue
statement is rarely used. Another statement that is even more rarely
used is the goto. In C, the programmer can create a label at any
location by typing the label name followed by a colon. If it is neces-
sary, the goto <label> can be used to transfer control of the
program from one location to another. In general, C provides enough
structured language forms that the use of the goto <label> se-
quence will rarely be needed. One place where the goto can be
used effectively is when the program is nested deeply and an error is
detected. In such a case, the goto statement is an effective means of
unwinding the program from a deep loop to an outer loop to process
the error. In general, you should avoid goto statements whenever
possible.
That said, there is an excellent alternative to a goto. The reach
of a got o is limited to the function in which it is defined. In fact, the
reach should probably be confined to the block in which it is de-
fined. Since new variables can be defined at the beginning of any
block, undefined behavior can be introduced when a goto branches
into a block where new variables have been defined. Also, you can
introduce undefined behavior when you branch out of a block where
Program Flow and Control 49
variables have been defined. In both cases, the branch is around op-
erations of defining or deleting local variables. The alternative is to
make use of the set j mp ( ) and long j mp ( ) functions.
These handy library functions are declared in the setjmp.h
header file. Also declared in this header is a type called env. In your
program, you must define an external instance of env. This param-
eter is used as an argument to set jmp ( ) . set jmp ( ) saves the
status of the computer in the instance of env when it is called. When
called originally set jmp ( ) returns a zero or a FALSE. The func-
tion long j mp ( ) takes two arguments. The first is the env variable
corresponding to the return location in the program. The second is
an integer that is returned. Executation of the long j mp ( ) returns
control to within the set j mp ( ) function. When control is returned
to the function that called the set jmp ( ) the parameter that was
passed from the long j mp ( ) is returned. Therefore, when control
returns from set j mp ( ) a simple test determines whether the func-
tion set jmp ( ) or long jmp ( ) was called.
These functions restore the status of the computer to that which
existed when the set j mp ( ) was called. This restoration automati-
cally unrolls all function calls between the execution of the
set j mp ( ) and the long j mp ( ) . Also, there are no block or func-
tion limits on the use of these functions. As long as env is a globally
accessible variable, long j mp ( ) can pass control from any location
in a program to any other.
The Switch Statement
A second approach to selection between several alternates is the
switch statement. This approach is sometimes called the switch/case
statement. Following is a program that accomplishes exactly the same
as the above program. In this case, the switch statement is used.
/* Count the number of occurrences of each vowel
found in an input and also count all other charac-
ters. */
# include <stdio . h>
int main (void)
{
50 Chapter 1 Introduction to C
int na=0 , ne=0 , ni=0 , no=0 , nu=0 ;
int nother = , c ;
while ( (c=getchar ( ) ) ! =E0F)
switch ( c)
{
case 'A' :
case 'a': na=na+l;
break;
case X E' :
case x e': ne=ne+l;
break;
case x I ' :
case x i': ni=ni+l;
break;
case x 0' :
case x o' : no=no+l;
break;
case X U' :
case x u' : nu=nu+l;
breaks-
default :
nother=nother+l ;
}
printf ( xx As = %d, Es=%d, Is=%d, Os=%d, Us=%dand"
11 Others = %d\n // , na, ne, ni , no, nu, nother) ;
return ;
}
This program performs exactly the same function as the earlier one.
The data are read in a character at a time as before. Here, however,
the switch statement is used. The statement switch ( c ) causes the
argument of the switch to be compared with the constants follow-
ing each of the case statements that follows. When a match occurs,
the next set of statements to follow a colon will be executed. Once
the program starts to execute statements, all of the following state-
ments will be executed unless the programmer does something to
cause the program to be redirected. The break instruction does ex-
Functions
Functions 51
actly this operation for us. When a C program encounters a break,
it jumps to the end of the current block. Therefore, the breaks
following the executable statements above will cause the program to
jump out of the executing sequence and return to get the next charac-
ter from the input stream.
When all options have been exhausted without a match, the state-
ments following the default line will be executed. It is not necessary
to have a default line.
EXERCISES
1 . Write a program that counts the number of lines, words, and char-
acters in an input stream.
2. Extend the program from the exercise above to calculate the per-
centage usage of each character in the alphabet.
3. A prime number is a number that cannot be evenly divided by any
number. For example, the numbers 1, 2, and 3 are all prime num-
bers. Write a program that will calculate and print out the first 200
prime numbers.
Write this program without the use of either a modulo or a divide
operation.
The function is the heart of a C program. In fact, any C program
is merely a function named main. The purpose of a function is to
provide a mechanism to allow a single entry of a code sequence that
is to be repeated many times. A function is the most reusable element
in the C language. Properly written and debugged functions can be
collected into a program when needed. Therefore, the use of func-
tions will allow the programmer to write smaller programs and it is
not necessary to rewrite common functions that are used often.
A function can have many arguments or none whatsoever. Func-
tion arguments are contained in parentheses following the function
name. The values of the arguments are the parameters needed to
execute the function. A function can return a value, or perhaps it will
not have a return value. An example of a function that returns a value
is get char ( ) which returns a character from the input stream.
52 Chapter 1 Introduction to C
A function may not be nested inside another function. Therefore,
any function must be created outside of the boundaries of any other
function or program structure. Functions have only one entry point,
and they return only one return item. The return can be of any type
that C supports. Functions can have several arguments. The argu-
ments can be of any valid C type.
In ANSI C, the use of a function requires the use of a function
prototype. A function prototype is a statement of the following form:
type function_name (type, type, type, . . . ) ;
The first type preceding the function name is the type to be re-
turned from the function. It is also called the type of the function.
The several types found in the argument are the types of the corre-
sponding arguments that are sent to the function. Variable names
may or may not be used for the arguments of a function prototype.
The type list is the important item. The types in the argument list are
separated by commas.
Thus far, it might seem that we have been blindly using functions
like printf ( ) , getchar ( ) , and putchar ( ) without the ben-
efit of function prototypes. Not so! The header file stdio . h contains
the function prototypes of all input/output related functions, so it is
not necessary for you to put a function prototype in your code for
these functions. Other library functions have their prototypes in their
own header files.
Compilers will differ. If a programmer attempts to send the wrong
type of data to a function through its argument, the compiler might
consider it an error or it might well convert the argument to the cor-
rect type prior to calling the function. In either case, the compiler
will not let a program use the wrong type of data as an argument to a
function. The standard allows that the parameter type be corrected to
the correct type and proceed. Some embedded systems compilers
will require that the type of each parameter be correct before the
program can compile.
One item that is important. Copies of parameters are passed to
any function. Copies are placed on the system stack or in registers or
both before the function call is executed. Therefore, the program can
use these parameters in any way without altering the calling pro-
gram. In fact, after you have done what is needed with a parameter,
you may use the parameter as a storage location for your function.
Functions 53
Data returned from a function will always be converted to the
correct type before it is passed back to the program. If you wish to
have a different type returned, the cast operator can be used to change
the return data type to any type desired.
ANSI C defined the type void. This type is used in several dif-
ferent ways. If there is no function return, the prototype must identify
the function as type void. Also when there are no function argu-
ments, the argument list must contain the type void. This use of the
keyword void will prevent problems in function calls.
Note that the function prototype above is terminated with a semi-
colon. The semicolon is needed in the function prototype, but it is
not to be used after the name of the function in the code where the
function is defined. The function prototype is a declaration state-
ment that merely provides information to the compiler while the
function prologue, the first line of the function, is a function defini-
tion which opens the code for the function.
The philosophy in C is to use functions with little provocation.
Using many functions produces code that is easy to read and follow.
Often it is easier to debug many small functions rather than a larger
program. One must temper these ideas somewhat when writing code
for small microcontrollers. Calling a function requires some over-
head that is repeated each time the function is accessed. If the total
overhead is more than the length of the function, it is better to use
in-line code. In-line code implies that the function code is repeated
in-line every time that it is needed. If the function code is much
greater than the calling overhead, the function should be used. In
between these limits, it is difficult to determine a hard-and-fast rule.
In microcontroller applications, it is probably best to use function
calls to a single function if there is a net savings of memory as a
result. This savings is calculated by first determining the code needed
prior to calling the function, the code needed to clean up the process
after the function call, the number of times the function is called, and
the length of the function. The in-line code will be smaller than the
corresponding function code. Therefore, if the total code for the num-
ber of function calls listed first exceeds the total in-line code required
to accomplish the same operations, then use the in-line code. Other-
wise, use function calls.
54 Chapter 1 Introduction to C
The above argument is valid for microcontroller applications code.
It does not necessarily follow for code written for large computers.
When writing for a large computer, there are usually few memory
constraints. In those cases, it is probably best to use more function
calls and not be worried about the memory space taken up by func-
tion calls unless there is a serious speed constraint. When speed is a
problem, the programmer must go through an analysis similar to that
above with the dependent parameter being time rather than memory
space. In small computers where several registers can be saved and
restored when a function is entered and exited, single instructions
can require many clock cycles. When deciding whether to use a func-
tion or in-line code, the programmer must assess the total time lost to
entering and exiting a function each time it is entered, and weight
that time lost as a fraction of the total time the program resides in the
function. If this time is large, and the program requires too much
execution time, consider the use of in-line functions.
It is always good to write small functions and create simple call-
ing programs to exercise the small functions. These programs are
used to debug the functions, and they are discarded after the func-
tions are debugged. If later, the program constraints dictate that in-line
code should be used, the essential code of the function can be written
into the program wherever it is needed. Another approach that will
be discussed in the next chapter is to use a macro definition to specify
a small function. With a macro definition, the function code is writ-
ten in-line to the program whenever the function is invoked.
Let us revisit an example used earlier. Write a program to calcu-
late and display the square root of each integer less than 11:
/* Calculate and display the square roots of
numbers
1 <= x <=10 */
# include <stdio .h>
#define abs (t) (((t)>=0) ? (t) : - (t) )
#define square (t) (t)*(t)
double sqr ( double ) ;
int main (void)
Functions 55
int i ;
double c;
for (i=l;i<ll;i++)
{
c=sqr (i) ;
printf ("\t%d\t%f\t%f\n" , i, c, square (c) ) ;
}
return ;
}
/* the square root function */
double sqr ( double x )
{
double xl=l , x2=l , c ;
do
{
Xl=x2;
x2=(xl +
x/xl
)/2;
}
wh
C=xl-x2 ;
lie (abs (c
) > =
.00000001) ;
}
return x2 ;
The result of this calculation is shown below
1
1.000000
1.000000
2
1.414214
2.000000
3
1.732051
3.000000
4
2.000000
4.000000
5
2.236068
5.000000
6
2.449490
6.000000
7
2.645751
7.000000
8
2.828427
8.000000
9
3.000000
9.000000
10
3.162278
10.000000
56 Chapter 1 Introduction to C
The second line of code
#define abs (t) (((t)>=0) ? (t) : - (t) )
is called a macro definition. In this case, the macro definition has the
appearance of a simple function. This function will calculate the ab-
solute value of the argument t. The absolute value of the argument is
a positive value. If the argument is positive, it is returned unchanged.
If it is negative, it is multiplied by -1 before it is returned. A macro
definition is a type of character expansion. Whenever the function
abs (x) is found in the code, the character string ( ( (x) > = ) ?
(x) : - (x) ) is put in its place. The argument x can be any valid
C expression. This function returns the absolute value of its argu-
ment. The macro definition
#define square (t) (t)*(t)
returns the square of t . Since these arguments can be any valid C
expression, it is necessary to be cautious when writing the macro defi-
nitions. Suppose that the parentheses were left out of the above
expression, and the macro were written
#define square (t) t*t
Also suppose that the code using this function were as follows:
x=square (y+3) ;
The character expansion of this expression would be
x=y+3*y+3 ;
The result of this calculation is 4 *y+3 and not (y+3 ) * (y+3 ) as
expected. When writing macro definitions, surround all arguments
and functions created by the macro with parentheses so that all argu-
ments are evaluated prior to use in the macro definition function.
Another problem can sneak into your code through improperly
written macros. Suppose that you want a macro that doubles the value
of its argument. Such a macro could be written
#define times_two(x) (x) + (x)
This macro when expanded in the following expression
x = 7*times two (y) ;
Functions 57
will yield
x = 7*(y) +(y) ;
which is the wrong answer. The problem can be easily corrected by
wrapping parentheses around the whole macro as
#define times_two(x) ( (x) + (x) )
Remember, always place any argument of a macro within parenthe-
ses and always place the entire macro definition in parentheses.
Note that there is no semicolon at the end of the macro defini-
tion. There should not be. If a semicolon were placed at the end of a
macro definition, extra semicolons would be entered into expres-
sions containing macros with unpredictable results.
The function prototype
double sqr ( double );
notifies the compiler that the function returns a double and takes a
double argument.
Inside of the main function, the for loop
for (i=l;i<ll;i++)
{
c=sqr (i) ;
printf ("\t%d\t%f \t%f \n" , i, c, square (c) ) ;
}
is used to calculate the several results. The variable c is of the type
double, and i is an int. The expression c = sqr (i) will be ac-
cepted by the compiler. This function returns a double which can be
stored in c. The argument is an int, but the compiler recognizes
that sqr requires a double argument and converts i to a double
before it is sent to the function sqr ( ) .
The code in the function sqr ( ) is a restatement of a square root
operation that we saw earlier. In this case, the function processes
floating-point numbers rather than the integers used before. Three
double variables are needed. The variables xl and x2 are the current
and last values found in the Newton iteration. As the result converges
to the correct value for the square root, several things happen. Vari-
ables xl and x2 become equal. The square of x2, or xl for that
matter, becomes equal to x . The product of xl and x2 becomes
58 Chapter 1 Introduction to C
equal to x . Any of these tests can be used to determine if the esti-
mate has been through enough iterations to be accurate.
The macro definition abs (x) is used to test for the end of the
loop. You will note that the argument is evaluated three times for the
expansion of the macro. If we place a lot of calculation within the
argument of a macro definition, the expansion of the macro may
cause the code to calculate the argument to be repeated several times.
For this reason, the expression
C = xl - x2 ;
is placed inside of the while loop, and the test to determine loop
termination uses abs (c) .
At the end of a function, a return statement will cause the value
of the expression following the word return to be evaluated and
returned to the calling function. If this expression is not of the type
specified by the function prototype, it will be converted to the cor-
rect type prior to being returned to the calling function. The expression
following the return statement can be enclosed in parentheses or not.
Another example will show the use of static external variables.
/* Read in a string from the keyboard and print it
out in reverse order. */
# include <stdio .h>
#define null
/* some function prototypes */
void push(int) ;
int pull (void) ;
int main (void)
{
int c;
push (null) ;
while ( (c=getchar ( ) ) ! = ' \n' )
push (c) ;
printf ("\n") ;
while ( (c=pull () ) !=null)
Functions 59
putchar (c) ;
print f ( AX \n") ;
return ;
}
The following code is to be compiled in a separate file from the code
above:
/* the stack function */
#define MAX 100
static int buffer [MAX] ;
static int sp;
void push (int x)
{
if (sp < MAX)
buffer [sp++] =x;
else
{
printf ( "stack overf low\n" ) ;
exit (1) ;
}
}
/* the unstacking function */
int pull (void)
{
if(sp > 0)
return buffer [—sp] ;
else
{
printf ( "stack underf low\n" ) ;
exit (1) ;
}
}
A stack is a last in, first out (LIFO) structure. Therefore, a stack
can be used to reverse the order of data sent to it. The above program
uses a stack operation. The functions push and pull identified in the
60 Chapter 1 Introduction to C
function prototypes perform the stacking operations for the main
program. In the main program, a null is pushed onto the stack to
identify the end of the data as it is pulled off of the stack a character
at a time. Data are read in a character at a time, and as each character
is read in, it is pushed onto a stack. When a new line character is
detected, the input phase is stopped, and the data written to the stack
is pulled off and printed. When the null is detected, the data have
all been pulled off the stack, and the program is ended.
In the function above, the function exit ( ) is used. This func-
tion is similar to the return operation. Whenever a call to the
function exit is executed, the argument is evaluated, any files open
for write are flushed and closed, and the control of the computer is
returned to the operating system. The evaluation of the argument is
returned to the operating system. Whenever a return is encoun-
tered, the expression following the return call is evaluated and
returned to the calling function. If control is in main ( ) when the
return is encountered, the evaluation of the expression is returned
to the operating system. Also, from main ( ) all files open for write
are flushed and closed. The function exit ( ) and return work
the same in main ( ) , but exit ( ) exits a program and returns con-
trol to the operating system from anywhere in the program.
In a separate compilation, the stack functions are compiled. In
that function, the macro definition MAX is defined as 100. Macro
definitions can be used to define any character string that is needed
in a program. They are not limited to defining pseudo functions. Two
external variables are defined in this file: an array of MAX integers
named buffer and an int called sp. These variables are declared
to be static. As such, these variables can be accessed by any function
in the file, but they are not available to any function outside of the
file. The variable sp is used as an index into the array buffer. When
a push is executed, a test to determine if sp is less than MAX is com-
pleted. If sp is less than MAX, the data are stored at the sp location
in buffer and sp is then incremented. Otherwise, an error message
indicates that a stack overflow has occurred and the program is ex-
ited.
The pull ( ) function is the reverse of the push ( ) operation.
First a check is made to see if there are some data on the stack to be
pulled off. If there are data, the stack pointer is decremented, and the
Recursion 61
content of the buffer at that location is returned to the calling pro-
gram. In the event that sp is when the pull operation is executed, a
stack underflow message is sent to the screen prior to exiting the
program.
The advantage to our making the buffer and the stack pointer in
the stack functions static can be easily seen. Suppose that these vari-
ables could be accessed from anywhere in the program. In that case,
it would not be necessary for the programmer to call the functions
push or pull to stack and unstack data. If several different pro-
grammers were using the same stack for different tasks in one large
program, it would be possible for different programmers to access
the stack as expected, or from their own tasks. Suppose a program-
mer made the mistake of pre-decrementing the stack pointer on
stacking and post-incrementing the stack pointer on unstacking. The
whole program would suddenly be in chaos. Therefore, masking these
variables from the rest of the program can reduce serious potential
debugging problems.
Recursion
A recursive routine is one that calls itself. The C language is
supposed to produce recursive code. Compilers for large machines
usually support recursion, but recursion is often one of the first casu-
alties on small microcontrollers. Automatic variables are created when
a function is entered, and they are stored on the stack. Therefore,
each time a function is called, a new stack frame is created, and
variables in place from an earlier execution of the function are unal-
tered. Such a function is called re-entrant, and re-entrant functions
are also recursive. An example of a simple recursive function is the
factorial:
n! = n* (n-1) * (n-2) *. . . .*2*1
An interesting observation that can be made of factorial is that
n! = n* (n-1) !
or n factorial equals n times n-1 factorial. Also, the factorial of is
defined as 1. With these definitions, it is possible to write the follow-
ing recursive function to calculate the factorial of a number:
long factorial ( int n)
62 Chapter 1 Introduction to C
{
if (n==0)
return 1 ;
else
return n*f actorial (n-1) ;
}
This surprisingly simple function calculates the factorial. When-
ever you write a recursive routine, it is important to have means of
getting out of the routine. In the above case, when the argument reaches
zero, the function returns a result rather than calling itself again. At
that time the routine will work itself back a level at a time until it
reaches the initial factorial call, and the calculation will be done.
Recursion can create some elegant code in that the code is very
simple — often too simple. There is a cost in the use of recursive code,
and that is stack space. Each time a function call is made, the argu-
ment is placed on the stack and a subroutine call is executed. As a
minimum, the return address is two bytes, and the value of the argu-
ment is also two bytes. Thus, at least four bytes of stack space are
needed for each function call. That is no problem when the factorial
of a small number is calculated. (The factorial of 13 is larger than
can be held in a long, so only small numbers can be considered for
a factorial.) However, if a recursive function is written that calls it-
self many times, it is possible to get into stack overflow problems.
Another interesting recursive routine is the function to calculate
a Fibonacci number. A Fibonacci number sequence is described by
the following function:
long fib(int n)
{
if (n==l)
return 1 ;
else if (n==0)
return 1 ;
else
return fib(n-l) + fib(n-2);
}
This sneaky function calls itself twice. Some interesting characteris-
tics of this function are left to the exercises that follow.
Summary
Summary 63
EXERCISES
1. Write a function to calculate the Fibonacci number for 10, 20, 30,
and 40.
2. Devise a means for determining the number of times the fib func-
tion is called in the above program. What is this number for
fib(20)?
3. A separate problem from the number of times the function is called
is the number of times the function is called without exiting through
the bottom of the function. This term is called the depth of the
function. Determine the maximum depth of the f ib ( ) function
in calculating f ib ( 2 ) .
4. Repeat problem 1, but rewrite the Fibonacci number function so
that it does not employ recursion. How does the time to execute
this version of the fib ( 3 ) compared to that above?
The basics of writing programs in C have been discussed in this
chapter. Several important concepts have been skipped over in this
presentation and will be covered in Chapter 2.
If you have not done so, it is recommended that you enter and
compile each example shown. These programs will all compile and
run under the MIX PowerC Compiler, the Cosmic compiler for the
M68HC11, the M68HC16, and the M68300 series of chips. They
also compile on the DIAB MCORE compiler. With the exception of
the MIX PowerC compiler, all of the compilers listed are cross com-
pilers that run on a PC platform, but compile code for another
computer.
The ANSI version of the language is the current standard, and
none of the classical C constructs have been introduced in this text. It
is not to the programmer's advantage to use the classical version of
the language, even though programs that conform to classical C will
compile on an ANSI compliant compiler. Any version of a C++ com-
piler structured to compile C code will also compile ANSI C code.
The DIAB compiler listed above is a C/C++ compiler.
Chapter 2
Advanced C Topics
Pointers
The use of pointers sets the C language apart from most other
high-level languages. Pointers, under the name of indirect addressing,
are commonly used in assembly language. Most high-level languages
completely ignore this powerful programming technique. In C, point-
ers are simply variables that are the addresses of other variables. These
variables can be assigned, be used as operands, and treated much the
same as regular variables. Their use, however, can simplify greatly
many of the truly difficult problems that arise in day-to-day program-
ming. Let's discuss pointers with the view that they offer us a powerful
new tool to make our programming jobs easier.
How To Use Pointers
A pointer to a variable is the address of a variable. Thus far in our
discussion of variables, the standard variable types were found to
occupy 8-, 16-, or 32 bits depending on the type. Pointers are not
types in the sense of variables. Pointers might occupy some number
of bits, and their size is implementation dependent. In fact, there are
cases of pointers to like types having different sizes in the same pro-
gram depending on context.
If the variable px is a pointer to the type int and x is an int , px
can be assigned the address of x by
px = &x ;
The ampersand ( & ) notifies the compiler to use the address of x
rather than the value of x in the above assignment. The reverse op-
eration to that above is
65
66 Chapter 2 Advanced C Topics
- *
X = *px;
Here the asterisk ( * ) is a unary operator that applies to a pointer
and directs the compiler to use the integer pointed to by the pointer
px in the assignment. The unary * is referred to as the dereference
operator. With these two operators, it is possible to move from vari-
able to pointer and back again with ease.
A pointer is identified as a pointer to a specific type by state-
ments like
int *px,*pa;
long *pz;
float *pm;
In the above declarations, each of the variables preceded by the
unary asterisk identifies a pointer to the declared type. The pointers
px and pa are the addresses of integers, pz is the address of a long,
and pm is the address of a floating point number. Always remember:
if pk is a pointer to an integer, *pk is an integer. Therefore, the
declarations in the above examples are correct and do define ints,
longs and floats. However, when the compiler encounters the
statement
int *pi;
it provides memory space for a pointer to the type int, but it does
not provide any memory for the int itself. Suppose that a program
has the following declaration:
int m,n;
int *pm;
The statement
m = 10;
pm = &m;
will assign the value 1 to m and put its address into the pointer pm.
If we then make the assignment
n = *pm;
the variable n will have a value 10.
Another interesting fact about pointers is shown in the following
example:
Pointers 67
int able [10] ;
int *p;
After it has been properly declared, when the name ab 1 e is invoked
without the square brackets, the name is a pointer to the first location in
the array. So the following two statements are equivalent:
p = &able [0] ;
and
p = able;
Of course, the nth element of the array may be addressed by
p = &able [n] ;
The unary pointer operators have higher precedence than the arith-
metic operators. Therefore,
*pi = *pi + 10;
y = *pi + 1;
*pi += 1;
all result in the integers being altered rather than the pointers. In the
first case, the integer pointed to by pi will be increased by 1 . In the
second case, y will be replaced by one more than the integer pointed
to by pi. Finally, the integer pointed to by pi will be increased by
1 . The statement
++*pi;
causes the integer pointed to by pi to be increased by 1. Both + +
and the unary * associate from right to left, so in the following case
pi + +;
the pointer pi is incremented after the dereference operator is applied.
Therefore, the pointer is incremented in this case, and the integer *pi
remains unaltered. If you wish to post-increment the integer *pi, use
(*pi) ++;
At times, it is necessary to pre-increment the pointer before the
dereference. In these cases, use
+ +pi;
68 Chapter 2 Advanced C Topics
Pointers work with arrays. Suppose that you have
int *pa;
int a [20] ;
As we have already seen,
pa = a ;
assigns the address of the first entry in the array to the pointer pa
much the same as
pa = &a [0] ;
does. To increment the pointer to the next location in the array, you
may use
pa++ ;
or
pa = pa + 1 ;
In any case, the pointer that is 1 larger than pa points to the next
element in the array. This operation is true regardless of the type that
pa points to. If pa points at an integer, pa++ points to the next
integer. If pa points at a long, pa++ points to the next long. If
pa points to a long double, pa++ points to the next long double. In
fact, if pa points to an array of arrays, pa++ points to the next array
in the array of arrays. C automatically takes care of knowing the
number that must be added to a pointer to point to the next element
in an array.
In the above case, since a is an array of 2 integers, and pa
points to the first entry in the array, it follows that pa+1 points to
the second element in the array, and pa +2 points to the third ele-
ment. Similarly, * (pa+1) is the second element in the array and
* (pa+2 ) is the third element.
There is a set of arithmetic that can be applied to pointers. This
arithmetic is different from the normal arithmetic that can be applied
to numbers. In all cases, the arithmetic applies to pointers of like
types pointing to within the same array only. When these conditions
are met, the following arithmetic operations can be completed:
• Pointers can be compared. Two pointers to like types in the same
array can be compared in a C logical expression.
Pointers 69
• Pointers can be subtracted. Two pointers to like types in the same
array can be subtracted with a C arithmetic expression. The result
of the subtraction will be the number of elements between the two
pointers, not the difference in the values of the pointers.
• Pointers can be incremented and decremented. A pointer into an
array can be either incremented or decremented. The result will be
a pointer that points to an adjacent element in the array.
• Pointers can be assigned. A pointer can be assigned to another
pointer of the same type.
Pointers cannot be added, multiplied, divided, masked, shifted,
or assigned a pointer value of an unlike type. Pointers can be as-
signed to another pointer of the type void*.
The name of an array is a pointer to the first element in this array.
This type of variable occupies a special place in C. The name can
always be used as a pointer, and assigned to another pointer, but it
cannot be assigned to. Any attempt to assign a new value to the array
name would upset the beginning of the array to the program.
Therefore, an array name as a pointer can be used as a value on
the right side of an assignment equal sign, but it cannot be used on
the left side of the equal sign. C variables are broken into the types
rvalue and lvalue. All C variables, with the exception of the
names of functions and arrays, are both rvalues and lvalues.
They can be used on either side of an equal sign in an assignment
statement. Function names and array names are rvalues and can
be used only on the right side of the equal sign in an assignment
statement. Variables declared as constants and constants created by
the #def ine statement are also rvalues.
The type void* has a special meaning when applied to pointers.
A vo i d pointer (sometimes referred to as a generic pointer) is a pointer
that does not point at any specific type. Some functions will return
void pointers, and to use these pointers, they must be cast onto the
type that they represent. Therefore, a pointer of the type void* can
be assigned the value of a typed pointer. However, unless the void*
pointer is cast onto the specified type, the increment, decrement,
subtraction, and so forth will not work. A void pointer can be
used in expressions, but it is impossible to alter its value.
70 Chapter 2 Advanced C Topics
Pointers and Function Arguments
Values passed to functions as arguments are copies of the real
values. The data to be passed to a function are pushed on the stack or
saved in registers prior to the function call. Therefore, if the function
should modify any of the arguments, this modification would not
propagate to the calling function. Therefore, a function like
void swap(int x, int y)
{
int temp;
temp = x;
x = y;
y = temp;
}
does nothing but swap two passed values, and those values are never
returned to the calling program. This performance is good as well as
bad. The bad situation is shown above when the function simply
does not work as expected. The good side is that it is not easy to
inadvertently change variable values in the calling program. The use
of pointers permits this problem to be avoided. The technique is called
passing parameters by reference. Consider the following function:
void swap (int* px, int* py)
{
int temp;
temp :
= *px;
*px =
*py =
}
*py;
temp;
Here the integers pointed to by px and py are swapped. These
integers are the values in the calling program. The pointer values in
the calling program are unaltered.
C makes extensive use of passing parameters by reference. Re-
call the first program in Chapter 1. That program has the line
printf ( "Microcontrollers run the world!\n");
Pointers 71
We now know that the string ''Microcontrollers run the
world ! \n" is nothing more than a character array terminated by a
zero. The compiler creates that array in the memory of the program.
That array is not passed to print f. A pointer to that array is passed
to print f. Then print f prints the characters to the standard out-
put and increments through the array until it finds the terminator.
Then it quits. In other instances, arguments like s [ ] were used. In
these cases, C automatically knows to pass a pointer to the array. It is
interesting. If you have an array int s [nn] and pass that array to a
function as s, you can use an argument int * p in the function and
then in the function body deal with the array p [ ] . The C language
is extremely flexible in the use of pointers. An example of this type
of operation is as follows:
/* Count the characters in a string */
# include <stdio .h>
int strlen(char *) ;
int main (void)
{
int n, s [8 0] ;
fgets (s, 80 , stdin) ;
n=strlen (s) ;
printf ( "The string length = %d\n",n);
return ;
}
int strlen( char *p)
{
int i = 0;
while (p[i] !=0)
i + +;
return i ;
72 Chapter 2 Advanced C Topics
The above version of st rlen ( ) will return the number of char-
acters in the string not including the terminating 0. The function
gets ( ) is a standard function that retrieves a character string from
the keyboard. The prototype for this function is contained in s tdio.h.
It is interesting to note that the function strlen ( ) can be altered in
several ways to make the function simpler. The index i in the function
can be post-incremented in the argument so that it is not necessary to
have the statement following the while ( ) . This function will be
int strlen (char *p)
{
int i=0;
while (p [i++] !=0) ;
return i ;
}
Because the while ( ) argument is evaluated as a logical state-
ment, any value other than will be considered TRUE. Therefore,
int strlen (char *p)
{
int i=0;
while (p [i + + ] ) ;
return i ;
}
provides exactly the same performance. Yet another viewpoint is found
by
int strlen (char* p)
{
int i ;
f or (i = ; *p++; i + + ) ;
return i ;
}
Each of the above functions using s t rlen ( ) show a different
operation involving passing arguments by reference.
Let us examine another use of passing arguments by pointers.
Pointers 73
The following function compares two strings. It returns a negative
number if the string p is less than the string q, if the two strings
are equal, and a positive number if the string p is greater than the
string q .
int strcomp (char* p, char* q)
{
while ( *p++ == *q++)
if(*(p-D == x \0') /* The strings are equal */
return 0; /* zero */
return *(p-l) - *(q-l); /* The strings are
unequal , return the correct value*/
}
Here the whi 1 e ( ) statement will examine each character in the
string until the argument is not equal. Note, that the pointers p and q
are incremented after they are used. The test for equality will not
occur until after the pointers are incremented, so it is necessary to
decrement the pointer p to determine if the last equal value in the
two strings is a zero character. If the last value is a zero, the two
strings are equal, and a is returned. If the last tested character is not
a zero, the two strings are not equal and the difference between the
last character in p and the last character in q is returned. This choice
will give the correct sign needed to meet the function specification.
Another approach can be used that eliminates the increments and
decrements within the program and confines them to the argument
of a for construct. Consider
int strcomp (char* p, char* q)
{
for( ; *p==*q ; p++, q+ + )
if ( *p == *\0')
return ;
return *p - *q;
}
The pointers p and q are both incremented after the test in the
i f statement. Therefore, the pointer values are correct for the i f
statement as well as for calculation of the return value when the
strings are not equal.
74 Chapter 2 Advanced C Topics
C is completely unforgiving if you exceed the boundaries of the
array in your calculations. C does not have intrinsic boundary checks,
and it is possible to increment pointers right off the end of an array.
For that matter, the array index can be decremented to addresses
below the beginning of the array. When a program makes such a
mistake, it will destroy other data, perhaps destroy the program be-
ing executed, or in the worse case, the program can crash the system.
The simple single-tasking computers that run MS-DOS or similar
operating systems have no memory protection feature that protects
one task from another. In such cases, a program that runs wild and
overwrites memory not assigned to it can destroy other tasks that are
loaded in memory whether they are running or not.
EXERCISES
1 . Write several versions of a function that copies one string into
another.
2. Write a function that concatenates two strings. Concatenation of
two strings means that one string will be written at the end of the
other. Write this function with and without the use of pointers.
What is the advantage of the pointer version of the function?
Let us consider a problem that will be analyzed in more detail
later. Suppose that a function is needed that will sort the contents of
an array into ascending (or descending) order. There are several sort
functions, and each has its own set of advantages and disadvantages.
The simplest and probably most intuitive is called the bubble sort. In
a bubble sort, a swap flag is reset. The first entry in the array is
compared with the second, and if they are in the wrong order, they
are swapped. Then the second entry is compared with the third, and
they are swapped or not. If a swap ever occurs, the swap flag is set.
The elements of the array are successively compared and swapped
until all of the elements in the array have been compared. If a swap
has occurred during the scan of the array, the swap flag is reset, and
the whole process is repeated. This process repeats until the scan of
the array causes no swaps to occur. At that time, the array is in order.
If the array contains n elements, this approach requires on the
order of n squared compares and swaps. For large arrays, the time to
sort an array with a bubble sort is inordinate. Another approach was
Pointers 75
discovered by D. L. Shell. The Shell sort allows the array to be
sorted with n times the logarithm base two of n. This difference
can be substantial. For example, if the array contains 1000 ele-
ments, the bubble sort will require on the order of 1,000,000
compares and swaps while the Shell sort will need only 10,000.
Therefore, the bubble sort should not be used to sort large arrays.
Another sort technique that is used was discovered by C. A. R.
Hoare. This technique is called the quick sort. It uses a recursive
approach to accomplish the sort, and it also requires n log base 2 of
n compares and swaps. The shell sort and the quick sort usually
require about the same time to accomplish the sort. We will dem-
onstrate a shell sort now.
The code for a shell sort follows. Note that extensive use of
pointers is used in this version of the shell sort.
/* shell_sort ( ) : sort the contents of the array *v
into ascending order */
void shell_sort (int* v, int n)
{
int gap, i, j, temp;
f or ( gap = n/2; gap > 0; gap /= 2)
f or ( i=gap; i < n; i++)
for ( j =i-gap; j > = 0&&* (v+j ) >* (v+j +gap) ;
j -= gap)
{
}
temp = * (v+j ) ;
* (v+j ) = * (v+j+gap) ;
* (v+j+gap) = * (v+j ) ;
The shell sort receives as arguments a pointer to an array of inte-
gers and an integer n which is the length of the array.
This sort works by successively dividing the array into subarrays.
The first operation divides the whole array into two parts; the second
divides each of the two subarrays into two new subarrays, for a total
of four arrays; and so forth.
76 Chapter 2 Advanced C Topics
The corresponding elements in each subarray are compared, and
if they are out of order, they are swapped. At the close of this opera-
tion, a new set of subarrays are created, and the process is repeated
over these subarrays. Eventually, the gap between elements in the
subarrays will be reduced to one, and the array contents will be sorted.
The outer for loop above controls the splitting of the arrays into
the subarrays. The second loop steps along the array pairs. The in-
nermost loop successively compares the elements that are separated
by gaps in the subarrays. If elements are found that are out of order,
they are reversed or swapped in the array.
EXERCISES
1. Restate the shell sort above to use arrays rather than pointers to
arrays.
2. Write a program that reads in characters from the input stream and
record the number of occurrences of each character. Calculate the
percentage occurrence of each character, and print out the result in
ascending order of percentage of occurrence.
Functions can return pointers. For example, prototype to a func-
tion that returns a pointer is:
int *able(char* );
Here able returns a pointer to an integer.
In C, a NULL pointer is never used. A NULL pointer implies that
something is to be stored at the address zero. This address is never avail-
able for data storage, so no function can return a valid NULL pointer.
The NULL pointer can be used as a flag or an error return. The pro-
grammer should never allow a NULL pointer to be dereferenced, which
implies that data are read or stored at 0.
If C will support a pointer to a variable, it requires but little imagi-
nation to reason that C will support pointers to pointers. In fact, there
is no practical limit in the language to the depth of dereference C
will support. C will also allow arrays of pointers, and pointers to
functions. (We discussed pointers to arrays in the preceding section.)
An array of pointers can be very useful when needed. A most obvi-
ous use for an array of pointers is to read the contents of a command
line to a program. So far in our discussion of programs, there have
been no provisions for reading the content of a command line that is
Pointers 77
written to the screen when the program is executed. A command line
can be read by the program. The definition of the program name ma in
when extended to read in a command line is as follows
void main ( int argc, char *argv[])
The integer variable argc is the number of entries on the command line.
The array of pointers to the type char argv [] contains pointers to
strings. When entering arguments onto the command line, they must be
separated by spaces. The first string pointed to by argv [ ] is the name
of the program from the command line. The successive pointer values
point to additional character strings. These strings are each terminated,
and they point to the successive entries on the command line. The value of
argc is the total number of command line entries including the program
name. It must be remembered that each entry in argv [ ] is a pointer to a
string. Therefore, if a number is entered on the command line, it must be
converted from a string to an integer, or floating point number, prior to its
use in the program. Let us see how this concept can be used. Earlier, we
wrote a function to calculate a Fibonacci number. Let's use this function in
a program in which the argument for the Fibonacci calculation is read in
from the command line:
# include <stdio .h>
# include <stdlib . h>
long fib( int ); /* Fibonacci number function
prototype */
int main ( int argc, char* argv[] )
{
int i ;
i = atoi (argv [1] ) ;
printf ("The Fibonacci number of %d = %ld\n" , i,
fib(i)) ;
return ;
}
We will not repeat the code for f ib (i) . A new header file,
stdlib . h, is included with this program. The function prototype
for atoi ( ) is contained within this header file. The standard com-
mand line arguments are used in the call to main ( ) . The line
78 Chapter 2 Advanced C Topics
i = atoi (argv [1] ) ;
causes the function atoi — ASCII to integer conversion — to be ex-
ecuted with a pointer as an argument. This pointer points to the first
argument following the program name on the command line. In this
case, it will be pointing to an ASCII string that contains the number
to be used as an argument for the f ib ( ) call. This string must be
converted to an integer before f ib ( ) can operate on it, which is
exactly what the atoi ( ) function accomplishes. The final line in
this program prints out the result of the calculation.
Another example of use of the command line arguments is to print
out the command line. The following program will accomplish this task.
# include <stdio .h>
int main ( int argc, char* argv[] )
{
int i ;
for(i=0; argc--; i++)
printf ("%s w ,argv[i]);
printf ("\n") ;
return ;
}
The arguments to main ( ) are the same as before. This program
enters a for loop that initializes i to zero. It decrements argc each
time it tests its value, and executes until the loop in which argc is
decremented to 0. The printf call
printf ( "%s " , argv [i] ) ;
prints out the string to which argv [ i ] points. Notice the space in the
string xx % s w . This space will force a space between each argument as
it is printed. The program is written so that there are no new line charac-
ters printed. Arguments will all be on one line, and they will each be
separated by a space. The print f ( ) statement after execution of the
f or ( ) loop will print out a single new line so that the cursor will return
to the next line after the program is executed.
Command line entry is but a simple example of use of arrays of
pointers. Another area in which arrays to pointers are needed is in order-
ing strings of data. For example, it is possible to collect a large number
of words in memory, say from an input stream. Suppose that it is needed
to alphabetize these words. We saw earlier, that the shell sort will order
Pointers 79
the contents of an array, so it might be possible to simply modify the
shell sort to do this job.
First, a comparison is needed that will determine if the lexical value
of a one word is smaller, equal, or larger than that of another word. Such
a compare routine was outlined above. Second, a swap routine that will
swap the words that are in the wrong order. Here is a case where an array
of pointers can be quite useful. Assume that the program that reads in the
data will put each word into a separate memory location and keep an
array of pointers to the beginning of each word rather than just the array
of the words themselves. Then in the shell sort, when a swap is required,
rather than swapping the words, swap the pointers in the array. Swap
routines that swap pointers in the array are very easy to implement. On
the other hand, swap routines to swap two strings in memory are diffi-
cult and slow. Therefore, we can create a sort routine that is much more
efficient if we use an array of pointers rather than an array of strings.
/* shell_sort ( ) : sort the contents of the array
char* v[] into ascending order */
#include <string . h>
void shell_sort (char* v[], int n)
{
int gap, i, j ;
char* temp;
f or ( gap = n/2; gap > 0; gap /= 2)
for( i=gap; i < n; i++)
for( j=i-gap; j>=0 &&
strcmp (v[j] , v[j+gap] ) ; j -= gap)
{
temp = v [ j ] ;
v[j] = v[j+gap] ;
v[j+gap] = v[j] ;
}
}
Here the strcmp ( ) routine is used to determine if a swap is
needed, strcmp ( ) is identified in the header file string . h. When
needed, the contents of the array of pointers to the beginning of the
words is swapped rather than swapping the words themselves.
80 Chapter 2 Advanced C Topics
An important point of style: Recall that a few pages back a func-
tion strcomp ( ) was written as an example. It did the exact same
operations as strcmp ( ) above. Why should we choose one over
the other? Well, library functions have been written, rewritten, de-
bugged, and worked over for years. They work correctly, and you
can count on their robust construction. As a general rule, use a li-
brary function if you can find one to do the job that you are attempting.
Most of the time, programmers who write duplicates of library func-
tions do it to satisfy their own egos. The reward is poor when a bug is
discovered, especially in production code, that could have been
avoided by using a standard library function.
Multidimensional Arrays
C supports multidimensional arrays. Programmers often find that
much of the need for multidimensional arrays will go away with the
availability of pointers. Multidimensional arrays in C are thought of
as arrays of arrays. This idea can be extended to more than two di-
mensions. A two-dimensional array is identified as
array [x] [y] ; /* [row] [column] */
The first argument to the right can be thought of as the row dimen-
sion, and the second the column dimension. Elements specified by
the rightmost argument are stored in adjacent memory locations.
An array can be initialized at declaration time. For example:
int array [3] [4] = { {10,11,12,13},
{14,15,16,17},
{18,19,20,21} };
It is equally valid to initialize the array as follows:
int array [3] [4] ={10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21} ;
Either form of initialization will place the proper numbers in the
proper location in memory, and the two-dimensional indices will
work properly in either case.
Frequently, it is needed to know the size of a variable in C. This
variable can be a basic type, an array, a multiple dimensional array, or
even a structure that will be introduced later. C provides an operator
Multidimensional Arrays 81
that has much the appearance of a function called sizeof . To deter-
mine the size of any variable, use the sizeof operator as follows:
a = sizeof array;
which will return the number of bytes contained in array [ ] [ ] above.
There are several important different ways that you can use the
sizeof operator. First, the value of the return from sizeof is in
characters. The type of the return is called a type s i z e_t . This special
type is usually the largest unsigned type that the compiler supports.
For the MIX compiler, it is an unsigned long. If you should want
the size of a type in your program, you should enclose the parameter in
parentheses. If you want the size of any other item, do not use the paren-
theses. One other item. If the sizeof operator is used in a module
where an array is defined, it will give you the size of the array as above.
If you should pass an array to a function, the array name degenerates to
a pointer to the array, and in that case, the return from the sizeof
operator would give you the size of a pointer to the array.
A common example program using two-dimensional arrays is to
determine the Julian date. The Julian date is simply the day of the
year. The following function is one that allows counting the number
of days that have passed in a year.
int month_days [2] [13] = {
{0,31,28,31,30,31,30,31,31,30,31,30,31},
{0,31,29,31,30,31,30,31,31,30,31,30,31}};
int Julian_data (int month, int date, int year)
{
int i , leap;
leap = year%4==0 && year%100!=0 | | year%400==0;
for (i=l ; i<month; i++)
day += month_days [leap] [i] ;
return day;
}
The declaration
int month_days [2] [13] = {
{0,31,28,31,30,31,30,31,31,30,31,30,31},
{0,31,29,31,30,31,30,31,31,30,31,30,31}};
82 Chapter 2 Advanced C Topics
types month_days as an array of 26 integers. This array is a
two-dimensional array of two rows of 13 columns each. The values
assigned are shown. The extra entry at the beginning of each array
is to allow the conventional month designations 1 through 12 to be
used as indices and not have to worry about the fact that arrays in C
start with a index.
The introduction of the program is normal. The function returns
an int and expects to receive three int arguments; one for the
month, one for the day of the month, and one for the year. Note that
the year must be the full year, like 2013, rather than merely 13. The
first executable statement is the logic statement:
leap = year%4==0 && year%100!=0 | | year%400==0;
Leap years are usually every four years. However, a small discrepancy
still exists in the length of the year with the "once each four years"
correction. To further correct the error, the calendar makers have de-
cided that years divisible by 100 will not be a leap year unless the year
is divisible by 400. The above statement is a logical statement that
determines first if the year is divisible by 4. If it is divisible by 4, it is
then checked to determine if it is divisible by 100. The result of this
much of the analysis will be TRUE for any year divisible by 4, and not
divisible by 100. If this portion of the calculation is TRUE, leap will
be assigned a value TRUE, or 1, and the evaluation will terminate. If
the result of the first portion of the calculation is FALSE, it will be
necessary to evaluate the last term to determine if the whole statement
is TRUE or FALSE. The variable leap will be assigned the result of
year%400==0
in this case.
leap is assigned a value of 1 or according to the result of the
logic evaluation. This value can be used as an index into the two di-
mensional array to determine if the number of month days in a leap
year or a nonleap year will be used in the calculation of the Julian date.
Pointers and Multidimensional Arrays
Perhaps one of the most widely misunderstood and therefore mys-
terious aspects of pointers and C has to do with multidimensional
arrays. These problems are really not difficult. A multidimensional
Multidimensional Arrays 83
array must always be understood as being arrays of arrays of arrays
and so forth. For example, the declaration
int ar [3] [5] ;
defines three arrays of five elements each. We have already seen that
data stored in this array is column major. The second argument points
to a column in the two-dimensional array, and ar [n] [0] and
ar [n] [1] are stored in adjacent memory locations. Following the
logic of array names and pointers, the array name ar is a pointer to
the first element in the array. The value obtained when using ar as
an rvalue is &ar [ ] [ ] . The order of evaluation of the square
brackets is from left to right so that * (ar+1) is a pointer to the
element ar [ 1 ] [ ] in the array. Think of the two-dimensional ar-
ray as being * ( ar +n ) [ i ] where n has a range from to 2 and i
has a range from to 4. An increment in n here will increment the
absolute value of the pointer by 5* si zeof (int) .
These ideas can be carried to the next level. The element * ( ar +n )
is a pointer to the first element of a five-element array. Therefore, the
evaluation of*(*(ar+n)+i) is the value found in the location
ar [n] [ i ] . The important item is that the right- most argument in
multiple dimensional arrays point to adjacent memory locations, and
the increments of the left arguments step the corresponding pointer
value from array to array to array.
These ideas can be extended to arrays of more than two dimen-
sions. Had the array been
double br [3] [4] [5] ;
then* (* (* (br+1) +2) +3) would be the element br [1] [2] [3]
from the above array, and *(*(br+l)+2)+3isa pointer to this
element.
EXERCISES
1 . Write a function that will receive the year and the Julian date of
that year, and calculate the month and date.
2. Write a function that will calculate the product of a 4 by 4 matrix
and a scalar. The scalar product requires multiplication of each
element in the matrix by the scalar value.
84 Chapter 2 Advanced C Topics
3. Write a function that will calculate the vector product of two 1 by
4 matrices. The vector product of two arrays is the sum of the
products of the corresponding elements of the two arrays.
4. Write a function that will calculate the matrix product of two 4 by
4 matrices. The matrix product is a matrix whose i , j element is
the vector product of the ith row of the first matrix and the jth
column of the second matrix. As such, the product of two 4 by 4
matrices is a 4 by 4 matrix.
C has pointers to functions. A common use for pointers for func-
tions is in creating vector tables for microcontrollers. Most
microcontroller applications involve the use of interrupts. When an
interrupt occurs, the machine status is saved, and program control is
transferred to an interrupt service routine. At the close of the inter-
rupt service routine, the machine status is returned to the earlier
condition, and control is returned to the interrupted program. An
address table in memory called the vector table contains the addresses
of each interrupt service routine. It is the programmer's responsibil-
ity to fill the vector table with the proper addresses for the various
interrupt service routines. Pointers to functions allow the program-
mer access to the addresses of the interrupt service routines. We will
see several different methods to create the vector tables in the chap-
ters on the individual microcontrollers that follow.
A pointer to a function is identified by
int (*function_ptr) () ;
The above declaration says that f unct ion_pt r is a pointer to a
function that returns a type int. The arguments are not declared here.
The parentheses surrounding *f unct ion_ptr are required. If they
were not included, the declaration would declare f unct ion_pt r as
a function that returns a pointer to the type int. If f unct ion_ptr
is a valid pointer to a function, the function can be accessed by
(*function_ptr) (args) ;
The above declaration form can be used for arrays as well as
functions. The declaration
char (*array ptr) [] ;
Multidimensional Arrays 85
states that array_j?tr is a pointer to an array of char. The declaration
char* array [] ;
states that array is an array of pointers to the type char. Although
you will rarely find it used, these declarations can be combined to
create very complicated declarations.
One construct from the general area of complicated declarations is
so important to microcontroller code that it must be covered. C sup-
ports variable types called lvalues. As mentioned earlier, an lvalue
is a type of variable that can be the destination for an assignment. Most
variables in C are lvalues. Notable exceptions are function names
and array names. If a program deals with a number that can be a memory
address, it can be made accessible to the language by casting the ad-
dress to an appropriate type. For example, suppose that a special table
is located at the address 0x1000 in memory. Further, suppose that the
type to be stored at that address is an integer. Here, the code sequence
(int *) 0x1000
forces the number 0x1000 to be a pointer to a type int. Often this
idea must be carried further, and the programmer wants to put a value
into a specific address. The above representation is a pointer to the
type int. Therefore, a value can be assigned to that int by
Mint *) 0x1000 = integer_value;
which will place integer_value into the location 0x1000 in the
computer memory.
Frequently, control registers, data registers, and input/output port
registers are placed at specific locations in memory. These register
locations can be converted to tractable C names by use of the #de -
f ine macro capability of C. Suppose that an I/O port is located at
the address.
#define PORTA (*(char *) 0x1000)
allows the use of the name PORTA in the computer program. I define
PORTA as a pseudo- variable. It is created by a macro expansion and
such things are usually constants or function-like expansions. The
above is neither. PORTA can be assigned to, or its value read. You
can even do operations like
86 Chapter 2 Advanced C Topics
PORTA =0X8 0;
if (PORTA & 0x6==0)
do something;
You can perform any operations with PORTA that you would want
with any normal variable in the language. While PORTA has been
generated by a macro expansion it also can be used as a variable.
Thus, the title pseudo-variable. Unless there is a specific reason,
though, I will call these macros "variables."
The above pseudo- variable is of the type char, and its address is
0x1000. This capability is very useful in programming microcontrollers.
When programming microcontrollers, there are two reasons why
it is necessary to be able to manipulate direct addresses in memory.
Most high-level languages will not allow the programmer direct ac-
cess to specific memory locations. As seen above, C does allow the
programmer to bend these rules enough to be able to store data into
a specific memory address. Another feature that is highly desirable
is to be able to place the address of a function at a specific address.
This capability is necessary when implementing an interrupt service
routine. When an interrupt occurs, the computer will stop its current
operation, save at least the values contained in the status register and
the program counter, and begin execution at an address contained in
a vector location. Each interrupt will have a vector address, and each
interrupt will require its own interrupt service routine. Program ini-
tialization when interrupts are involved will require that the program
place the addresses of any interrupt service routines into the specific
vector addresses for each interrupt.
A continuation of the above approach can be used in this case. There
is a direct address in memory that is to receive the interrupt service
routine address. Let's think for a moment about what this address is. The
address is going to contain the address of a function. The address itself is
a pointer to a memory location. The contents of this location are a pointer
that points to a function that is the interrupt service routine. All interrupt
service routines are of the type void. Therefore, the vector address is a
pointer to a pointer to a type void. To be able to place the value of the
pointer to a type void into this location, we must assert one additional
level of indirection to access the content of the specific memory loca-
tion. Therefore, the following line of code
Structures 87
* (void **) Oxfffc = isr;
places the beginning address of the function isr into the memory
location Oxfffc.
A convenient method of executing this operation is to create a
macro. The following macro definition works:
#define vector (isr, address) (*(void**) (address) = (isr) )
Now the function call
vector (timer, OxffdO) ;
will place the address of the function named timer into the loca-
tion OxffdO in the computer memory map. It is important that
timer be defined as a function that returns a type void.
Structures
Another feature of C not found in many high-level languages is
the structure. A structure is similar to an array, but far more general.
A structure is a collection of one or more variables identified by a
single name. The variables can be of different types. Structures are
types in the sense that an int is a type. Therefore, if you have prop-
erly declared a structure, you may declare another structure of the
same type. Examine the following structure:
struct person
{
char *name;
char *address;
char *city;
char *state;
char *zip
int height;
int weight;
float salary;
};
This structure contains some of the features that describe a per-
son. The person's name and address are given as pointers to character
strings. The person's height and weight are integers, and the salary is
88 Chapter 2 Advanced C Topics
a floating-point number. The structure declaration must be followed
by a semicolon. This combination of variables is created every time
a structure of the type person is declared. The name person follow-
ing the struct declaration is called the structure tag or merely the
tag. Tags are optional. If a tag is used, it may be used to declare other
structures of the same type by
struct person ap,bp,cp;
Here ap , bp, and cp are structures of the type person. A single
instance of a structure can be declared by
struct {....} a;
In this case a is a structure with the elements defined between the
braces. The elements that make up a structure are called its mem-
bers. Members of a structure can be accessed by appending a period
followed by the member name to the structure name. For example,
the name of the person represented by ap is accessed by
ap . name
and that person's salary is
ap . salary
A pointer to a structure can be used. The pointer pper son is created by
struct person *pperson;
If a pointer to a structure is used, members of the structure can be
accessed by the use of a special operator - >. This operator is created
by use of the minus sign (-) followed by the right angle bracket
character (>). The height of a person identified by the pointer
pperson is accessed by
pperson- >height
Arrays of structures are used, and when dealing with pointers to ar-
rays of structures, to increment the pointer will move the pointer to
the next structure. If a program has
struct person people [20], *pp;
and pp is made to point at people [ ] by
pp=people
Structures 89
then
people [1] .name
is the same as
++pp->name
A structure can have another structure as a member element. Consider
struct point
{
int x;
int y;
};
where point contains two integer elements, x and y. A circle can
now be defined by
struct circle
{
struct point center;
int radius;
};
In this case access to the members of center is by
circle . center .x
or
circle . center .y
Of course the radius is accessed by
circle . radius
Functions can take structures as arguments and they can return
structures. For example, the function make_point ( ) that follows
returns a structure.
struct point make_point (int x, int y)
{
struct point hold;
hold.x=x;
90 Chapter 2 Advanced C Topics
hold.y=y;
return hold;
}
Observe that the struct point is treated as a type with no diffi-
culty in this function. The return type is struct point, and within
the body of the function hold is also a type struct point. The x
argument passed to the function is placed in the x member of hold as
is the y argument placed in the y member. Then the struct hold is
returned to the calling function. All of these operations are legal.
Since structures create types, structures can have members that
are structures. For example, suppose that the struct rect for a
rectangle is defined as
struct rect
{
struct point pi;
struct point p2 ;
};
Let's outline a program that will inscribe a circle within a rect-
angle. The circle is tangent to the sides that make up the narrowest
dimension of the rectangle.
/* Inscribe a circle in a rectangle */
/* first declare some useful structures */
struct point
{
int x;
int y;
};
struct circle
{
struct point center;
int radius;
};
struct rect
{
Structures 91
struct point pi;
struct point p2 ;
};
/* here is a function to make a circle */
struct circle make_circle ( struct point ct, int
rad )
{
struct circle temp;
temp. center = ct;
temp. radius = rad;
return temp;
}
/* some useful function like macros */
#define min(a,b) ( ( (a) < (b) ) ? (a) : (b) )
#define abs (a) ((a)<0 ? - (a) : (a))
/* function prototypes */
void draw_rectangle (struct rect);
void draw_circle (struct circle);
int main ( void )
{
struct circle cir;
struct point center;
struct rect window = { {80,80},{600,400} };
int radius, xc,yc;
center . x = (window .pi . x+window . p2 . x) /2 ;
center . y = (window .pi . y+window . p2 . y ) /2 ;
xc = abs (window.pl .x - window. p2 .x) /2 ;
yc = abs (window.pl .y - window. p2 .y) /2 ;
radius = min(xc,yc);
cir=make circle (center, radius) ;
92 Chapter 2 Advanced C Topics
draw_rectangle (window) ;
draw_circle (cir) ;
return ;
At the beginning of the program, several important struct types
are defined.These include a point, a rectangle, a circle, and a func-
tion to make circle given its center and its radius. Two macro
definitions are needed. The first is the calculation for the minimum
value of a and b and the second returns the absolute value of the
argument. Two function prototypes are included. These functions
will draw a rectangle and a circle to the screen, respectively.
Inside the main program cir is declared to be of the type
struct circle, center is struct point , and window is
of the type struct rect. When window is defined, it is initial-
ized to the values shown. This type of initialization is acceptable to
structures as well as arrays. The rectangle is defined by two points.
The point {80,80} is the lower lefthand corner of the rectangle, and
the point {600,400} is the upper right corner. These locations are
implementation dependent, and in some cases might represent the
upper left corner {80,80} and the lower right corner {600,400}.
The center of the window is calculated by determining the aver-
age value of the x members of each point along with the average
value of the y members. These values are the exact center of the
rectangle. The center of the inscribed circle will lie at this point. The
radius of the inscribed circle will be one-half the length of the short-
est dimension of the rectangle. The two potential value are calculated
as xc and yc. Here the absolute value is used, because in general, it
is impossible to know that the rectangle will be specified by the lower
left hand corner in p 1 and the upper right hand corner in p 2 . If these
points were interchanged, negative values would be calculated. The
selection of the positive result through the abs ( ) macro avoids
this problem. The final choice for radius is the minimum value of
xc or yc.
The above calculations provide enough information to specify the
circle, so c i r is calculated as the return value from make_c i r c 1 e ( ) .
Finally, two compiler specific functions, draw_rectangle ( ) and
draw_circle ( ) , are used to draw the calculated figures to the screen.
Structures 93
While it would not be difficult to execute these calculations without
the use of structures, it is obvious that the structure formulation of the
program makes a much simpler and easier to follow program. The vari-
ables and program elements are objects here rather than mere numbers.
C has a command called typedef . This command can rename
a specified type for the convenience of the programmer. It does not
create a new type, it merely renames an existing type. For example,
typedef int Miles;
typedef char Byte;
typedef int Word;
typedef long Dword;
are all valid typedef statements. After the above invocations, a
declaration
Miles m;
Byte a [2 0] ;
Word word;
Dword big;
would make m an int, a an array of 20 characters, word the type
int, and big a long. All that has happened is that these types are
a redefinition of the existing types. New types defined by typedef s
are usually written with an upper case first letter. This is a tradition,
not a requirement of the C language.
Structures used earlier could be modified by use of the typedef.
Consider
typedef struct
{
int x;
int y;
} Point;
This typedef redefines the earlier struct point as Point. The
judicious use of typedef s can make a program even easier to read
than the simple use of struct s. The program that follows is the
same as that above where all of the structures are typedef new names.
/* Inscribe a circle in a rectangle */
94 Chapter 2 Advanced C Topics
typedef struct
int x;
int y;
Point ;
typedef struct
Point center;
int radius;
Circle;
typedef struct
Point pi;
Point p2 ;
Rect ;
Circle make_circle ( Point ct, int rad )
Circle temp;
temp . center=ct ;
temp. radius = rad;
return temp;
/* some useful macros */
#define min(a,b) ( ( (a) < (b) ) ? (a) :
#define abs (a) ((a)<0 ? - (a) : (a))
(b))
/* function prototypes */
void draw_rectangle (Rect) ;
void draw circle (Circle) ;
int main ( void )
{
Circle cir;
Point center;
Rect window = { {80,80}, {600,400} };
Structures 95
int radius, xc,yc;
center . x = (window . pi . x+window . p2 . x) /2 ;
center . y = (window .pi . y+window . p2 . y ) /2 ;
xc = abs (window.pl .x - window. p2 .x) /2 ;
yc = abs (window.pl .y - window. p2 .y) /2 ;
radius=min (xc,yc) ;
cir=make_circle (center, radius) ;
draw_rectangle (window) ;
draw_circle (cir) ;
return ;
}
The function make_circle ( ) returns a type Circle and re-
quires arguments Point and int. Circle is used to declare the
variable temp in make_circle ( ) . Within the main program
Circle, Point, and Rect are used as types in the definition of
the several structure type variables used in the program. With the
typedef declarations, there is no basic change to the program, but
it is easier to read and follow.
The two functions draw_rectangle ( ) and draw_circle ( )
are not standard C functions. These functions are programmed and
the listing of the final program is shown in Appendix B.
Self Referential Structures
A structure cannot contain itself as a member. Structure defini-
tions are not recursive. However, a structure can contain a pointer to
a structure of the same type. This capability has proven quite useful
in dealing with complicated sort or listing problems. One sort prob-
lem that can be easily treated is the binary tree sort. A tree sort receives
data, such as a word. Within the tree there is a root node that contains
a word. The node also contains a count of the number of times the
word has been seen and two pointers to additional nodes. These nodes
are called child or descendent nodes. Traditionally, the node to the
left contains a word that is less than the root word, and the node to
the right contains a word that is greater than the root node. As data
are read into the tree, the new word is compared with the root word.
If it is equal to the root word, the count in the node is incremented,
96 Chapter 2 Advanced C Topics
and a new word is received. If the word is not the same and is less
than the root word, the node to the left is accessed and the compari-
son is repeated. This process is repeated until a match is found or a
node with no descendants is found. If a match is found, the count of
that node is incremented. If no match is found a new node is created
and placed at the bottom location, and the word is inserted into the
new node. This process is repeated with the smaller words always
going to the left and the larger words always going to the right.
Eventually a tree that contains all of the different words entered into
the program will have been created. The tree has several properties.
Since each node has two child nodes, the tree builds in a binary manner.
The root level has exactly one entry, the second level has two entries, the
third level has four entries, and so forth. If the tree is balanced, each level
will have a power of two entries. However, if word data are taken in
randomly, it is possible that some tree branches will terminate early and
others will extend to a depth or level that exceeds some.
Once the data are placed into the tree, it is possible to sort it or to
arrange the words in alphabetical order. If we traverse the tree from
the root node to the extreme left, we will find the word that is small-
est. Immediately above that word will be the next larger word. To the
right of the second word will be words larger than itself but smaller
than the word in the next node above. Therefore, the right path must
be traversed to the left to find the next larger words. All of this —
right and left, larger and smaller — sounds complicated. It is not.
Here is a case where a little thought and recursive code will help
things along easily. The code that follows is a complete function to
alphabetize and count the number of times that each word is used in
a document. Several new concepts will be shown in this program, so
it will be broken into short blocks and described in these small pieces
of code rather than trying to bite into the whole program at one time.
The structure tnode is listed below.
typedef struct tnode
{ /* the tree node */
char *word; /* points to text */
int count; /* occurrences */
struct tnode *left;/* pointer to left child */
struct tnode *right; /* pointer to right child*/
} Tnode ;
Structures 97
The first two elements to this structure are a pointer to the word
contained in the node, and the number of times that the word has
been seen. The last two elements are pointers to structures of the
type struct tnode. Note that in the typedef of the struct
tnode, the structure tag was used. It is necessary to use the tag in
this case because the self-referential pointers inside of the structure
needs the tag to find the correct type. For the remainder of the pro-
gram, the typedef Tnode is used.
Some new files are included in the include files. These files will
be discussed in a following section. The first is ctype . h. This pro-
gram uses several character tests that are identified in ctype . h.
There are string operations found in string . h , and there are stan-
dard functions defined in stdlib . h.
# include <stdio .h>
#include <ctype . h>
#include <string . h>
# include < stdlib . h>
The list of function prototypes for the functions written in this
program follow:
Tnode *addtree (TNODE *, char *) ;
Tnode *talloc (void) ;
void treeprint ( TNODE *);
int getword(char *, int) ;
char *strsave (char *) ;
The first function is the function that is used to add a tree to the
program. *t alloc () is a function that allocates memory for a
Tnode. The function treeprint ( ) prints out the tree, and
getword ( ) reads in a word from the input stream. There are two
function prototypes defined within the program getword ( ) . These
functions are used by getword ( ) only. The final function above
saves the string pointed to by the argument in a safe place and re-
turns a pointer to the word location in memory.
The main program is relatively simple. The constant MAXWORD
is the maximum number of characters that can be allowed in a word.
Within main ( ) a pointer to a structure Tnode named root is de-
clared along with a character array named word and an integer i .
98 Chapter 2 Advanced C Topics
/* word frequency count */
#define MAXWORD 10
int main (void)
{
Tnode *root;
char word [MAXWORD] ;
int i ;
root=NULL;
while (getword (word, MAXWORD) != EOF)
if (isalpha (word [0] ) )
root =addtree (root , word) ;
treeprint (root) ;
return ;
}
A NULL value is assigned to root, and the program enters a loop that
reads words from the input stream. It is assumed that if the first character
of the word is a letter, that a word has been read in. The function
i saplha ( ) returns a TRUE if its argument is a letter, and a FALSE if
it is not. If the input is a letter, the routine addt ree is executed with the
arguments root and word. The first time that this function is executed,
root is a NULL. This loop is repeatedly executed until getword ( )
receives an EOF character from the input stream. At that time the input
loop terminates, and the function treeprint ( ) is executed. When
treeprint ( ) is completed, main ( ) returns a to the calling pro-
gram or the operating system. This signal can be used to notify the
operating system that the program has executed correctly.
Here is a good point to introduce the concept of a NULL pointer.
Often people use the term NULL to mean zero or a character contain-
ing a zero. This idea is incorrect. In this text, the word NULL means
a NULL pointer to a type void. Such a pointer can be defined in a
header file and the definition will look like
#define NULL ( (* void) 0)
Any place you see the name NULL in this text, it has the above meaning.
The function addt ree ( ) is the most complicated function in
Structures 99
this program. This function receives a pointer to a Tnode and a pointer
to a character string as arguments. It returns a pointer to a Tnode. If
the Tnode pointer argument is a NULL on entry to the function, the
first i f loop is executed. Within that loop, t al loc ( ) returns a pointer
to a new Tnode. The function strsave ( ) copies the string into a
safe place and returns a pointer to this location. This pointer is put into
the word pointer location in the new Tnode. A value of 1 is put into
the count location, and the pointers to the left and right child Tnode s
are set to NULL. At this time, the word has been put into the Tnode ,
and the pointer to this Tnode is returned to the calling program.
/* addtree: add a node with w, at or below p */
Tnode *addtree (Tnode *p, char *w)
{
int cond;
if (p == NULL) /* new word has arrived */
{
p=talloc(); /* make a new node */
p->word=strsave (w) ;
p->count=l ;
p->lef t=p->right=NULL;
}
else if ((cond = strcmp (w,p->word) ) == )
p->count++; /* repeated word */
else if (cond <0) /* less than into left subtree*/
p->left = addtree (p->left, w) ;
else /* greater than into right subtree */
p->right = addtree (p->right, w) ;
return p;
}
Suppose now that at a later time addtree ( ) is called and this
time p is no longer a NULL. In this case, a string compare test will be
executed to determine if the word that has been passed is equal to that
in the Tnode pointed to by p. If it is equal, it is a repeated word for
that node, so the word count is incremented and control is returned to
the calling program. If it is not equal (say, it is lexically less than the
100 Chapter 2 Advanced C Topics
word of the node), it is necessary to either traverse to the node on the
left or add a new node on the left if there is none there. The code
p->left = addtree (p->left, w) ;
does exactly what is needed in this case. This recursive call to
addtree ( ) will descend to the left Tnode if one exists. If one does
not exist, the pointer to the left Tnode will be NULL, so addtree ( )
will create a new node for this location, addt ree ( ) returns a pointer
to the new node, so it will be placed in the left pointer of this node.
Had the lexical value of the word been greater than that of the
word stored in the node, the addtree ( ) call would work on the
pointer to the right child node. Therefore, the function addtree ( )
will start at the root node and traverse down the tree to the right or
left child nodes depending on the size of the word relative to the
sizes of the words in the tree. If the word is found in the tree, its
count is incremented. If control proceeds down the tree, and the word
is not found, eventually, a Tnode with a NULL pointer to the direc-
tion that the path must move. A that time a new Tnode is created
and the word is assigned to that Tnode.
When all of the data to be input into the tree are read in, control
is passed to the function treeprint () . The argument of
treeprint ( ) is a pointer to the root node and treeprint ( )
returns nothing to the calling program. Efficient printing out of the
data requires another recursive routine. The function treeprint ( )
shown below shows this routine, treeprint ( ) is called with the
root pointer as an argument. The root pointer will not be a NULL, so
the code following the i f statement will be executed. The first
/* treeprint: in-order print of tree p */
void treeprint (TNODE *p)
{
if(p !=NULL)
{
treeprint (p->left) ;
printf ( "%4d%15s\n" , p->count ,p->word) ;
treeprint (p->right) /
}
}
Structures 101
statement of this code is a recursive call to treeprint ( ) with the
pointer to the left child pointer as an argument. This recursive call will
cause control to propagate to the lowest and leftmost level of the tree. At
this time treeprint ( ) will return normally, and the word pointed to
in the Tnode will be printed out by the print f ( ) call. The program
will then start a recursive treeprint ( ) to the right of this node.
Control will immediately go to the left side of the right branch and will
descend the lowest level and print out the word "found." This routine
will repeat up and down until the whole tree content has been printed.
The function strsave ( ) copies the word passed to it as an
argument into a save place and returns a pointer to this memory loca-
tion to the calling program. C provides for dynamic allocation of
memory.
char *strsave (char *s) /* make a duplicate of s */
{
char *p;
p = (char *) malloc (strlen (s) +1) ;
if (p != NULL)
strcpy (p, s) ;
return p;
Up to this point, all memory access was to memory allocated by
declaration statements. With dynamic allocation, the program can
go to the operating system and request memory at any time. This
memory is from a memory area called the program heap. The first
call to allocate memory is malloc ( ) shown above. The function
prototype for this function is found in stdlib . h , and the func-
tion requires an argument that is the length of the memory space
needed. The program returns a pointer to the base type of the system,
chars, to the required block of memory. If there is not enough
memory available, the function returns a NULL pointer. C will never
return a valid NULL pointer. Therefore, if you have a C program call
that returns a pointer, the program can always test for a NULL pointer
to determine if the call succeeded. In the above code, it is assumed
that the calling program will check for the NULL pointer and deter-
mine what to do.
102 Chapter 2 Advanced C Topics
The function prototype of the function mal loc ( ) is contained
in stdlib . h . This function returns a pointer to a type void. If
you will recall the rules of arithmetic for pointers, a pointer to a type
void can be assigned to any other pointer type, and vice versa. There-
fore, it is not necessary to cast the return from the mal loc call to
the type char* as was done above. In fact, there are people that say
that you should not perform this cast. If you make a mistake and
do not include the header stdlib . h, then the compiler would as-
sume that the return from the mal loc function call is an int. The
cast operation would introduce a bug into your code if the header
were not included. The error of the missing header file would be
identified by the compiler if there is no cast because the program
would be loathe to assign an integer to a pointer. Therefore, the code
without the cast is more robust.
I personally object to this logic. The current trend is to insist that
all operands in expressions that are not of the correct type be cast
to the correct type prior to execution of any operators. This trend
largely ignores the automatic type promotion that takes place when
there are mixed types within a single expression. In fact, many pro-
gramming standards insist that all mixed types be cast explicitly. In
such an environment, it seems rather silly that one case be singled
out and done differently.
The function t alloc ( ) also makes use of the mal loc ( )
function. In this case, the argument of the mal loc ( ) call is the
sizeof operator. sizeofisaC operator that returns the size of
the argument, sizeof is an operator in C and requires no prototype.
/* talloc : make a tnode */
TNODE *talloc (void)
{
return (TNODE *) malloc (sizeof (TNODE) ) ;
}
The memory allocation function mal loc ( ) returns a pointer to
the basic memory size of the system. It is always necessary to cast
the return from mal loc ( ) onto the type of variable that the return
must point to. In this case, the cast is to the type pointer to TNODE.
In strsave ( ) , the cast was to the type pointer to char. There-
fore, the statement
Structures 103
return (TNODE *) malloc (sizeof (TNODE) ;
will return to the calling function a pointer of the type TNODE to a
free memory space the size of a TNODE. This function is called by
the function addtree ( ) . You will note that addtree ( ) does
not test the return from t alloc ( ) to determine if the return is a
NULL pointer. Good programming practice would dictate that the
return from t alloc ( ) should be tested to make certain that
malloc ( ) did not return a NULL pointer.
The next function that must be incorporated into the program is
getword ( ) . getword ( ) returns the first character of the word
or an EOF in the case that an EOF is detected. It requires two argu-
ments. The first is a pointer to a character array into which the input
data are to be stored. The second argument is the length of the array
and hence the maximum length of any word that can be read into the
program by getwordf) . Two functions are accessed by
getword ( ) . The first is get ch ( ) which returns a character from
the input stream. The second is ungetch ( ) which restores a char-
acter back onto the input stream. We will see later that for getword ( )
to work correctly, it must pull one more character than the length of
the word in some cases. When that happens, the extra character must
be put back onto the input stream so that it will be available for the
next getch ( ) call.
/* getword: get next word or character from input */
int c, getch (void);
void ungetch (int) ;
int getword (char *word, int lim)
{
char *w=word;
while (isspace (c=tolower (getch ())));
if (c!=EOF)
*W++=C ;
if ( ! isalpha (c) )
{
*w= , \0' ;
return c ;
}
f or ( ; —lim >0 ; w++)
104 Chapter 2 Advanced C Topics
if ( ! isalnum (*w=tolower (getch ( ) ) ) )
{
ungetch (*w) ;
break;
}
*w= , \0' ;
return word[0] ;
}
The first executable statement
while (isspace (c=tolower (getch ())));
includes two standard C functions. The function isspace ( ) has
its prototype in the header file ctype . h. This function returns a
TRUE if its argument is a space and FALSE otherwise. The second
function, tolower ( ) , is also prototyped in ctype . h . It tests
the argument and returns a lower case letter if the argument is upper
case. Therefore, this operation will loop until it receives a nonspace
input, and the lower case version of the letter input will be stored in c.
If c is not an EOF, it is put into the next open location of the word
array and the pointer into this array is incremented. If the return is an
EOF, the second if statement will execute. The if statement
if ( ! isalpha (c) )
{
*w= , \0' ;
return c ;
}
tests to determine if the character from the input stream is a letter.
isalpha ( ) returns a TRUE if its argument is a letter and a FALSE
otherwise. If the character taken from the input stream is an EOF,
isalpha ( ) will return a FALSE and the statement following the
i f will be executed. In this case, a zero character is written to the
word, and the EOF is returned to the calling function.
If the return is a letter, the following sequence will be executed:
f or ( ; — lim >0 ; w++)
if ( ! isalnum (*w=tolower (getch ( ) ) ) )
{
ungetch (*w) ;
Structures 105
break;
}
The central loop here is the i f statement. Its argument executes
getch ( ) to read in characters from the input stream. These inputs
are converted to lower case and stored into the array location pointed
to by w . The result is then checked to determine if it is a letter or a
number. If it is not, it is put back onto the input stream and the loop
is exited by the break instruction. If it is a letter or a number, the
pointer to the output array w is incremented and the maximum length
of the array is decremented. If this last result is greater than zero, the
next character is read in. Otherwise, the if statement is skipped.
The last two statements in the function are
*w= x \0' ;
return word[0] ;
The last entry of the character array is made a ' \ ' to satisfy the
C requirement that a string must terminate with a 0, and the first
character of the word is returned to the calling program. This value
is returned just to guarantee that an EOF is not returned.
There are two final functions required for this program. These func-
tions work together to form the getch ( ) /unget ch ( ) pair. A global
buffer with an argument BUFSIZE is created, and a global integer
buf p is declared, buf p is initialized to 0. Since these variables are
global, they are initialized at the beginning of the program, and they
will not be changed when control is returned to a calling function.
In getch ( ) , the value of buf p is tested. If it is greater than 0,
the character returned is taken from the buffer and the buf p is
decremented. Otherwise, a new character is read from the input stream
by getchar ( ) .
#define BUFSIZE 100
char buf [BUFSIZE] ; /* buffer for ungetch */
int bufp=0; /* next free position in buffer */
int getch (void) /* get the next character from the
buffer */
106 Chapter 2 Advanced C Topics
{
return (bufp>0) ? buf [— buf p] : getcharO;
}
void ungetch(int c) /* put character back onto
input */
{
if (bufp>=BUFSIZE)
printf ( "ungetch: too many characters \n" ) ;
else
buf [bufp+ + ] =c;
}
When ungetch ( ) is executed, a test is made to determine if
this store would exceed the buffer length. If buf p will be greater
that BUFSIZE after it is incremented, an error return is executed.
Otherwise, the return character is put in the buffer and it is there to
be read the next time getch ( ) is executed.
This program has introduced several C functions. In fact, C has a
ungetchar ( ) that does the same function as ungetch ( ) above. It
has given examples of structures containing pointers to like structures,
recursive functions to process data, and dynamic memory management.
EXERCISES
1 . With an editor, enter the tree program and compile it. To run this
program, use an input redirection such as c:>tree < (filename)
2. Modify the above program to determine the number of levels, and
record the level number with each word. Print out the levels when
the output is printed. Hint: the levels can be determined in the
addt ree ( ) function, and the printout of the levels require a modi-
fication of treeprint ( ) .
3. Add a histogram to the above program that shows the number of
entries in each level. Count the total number of different words in
the document tested.
More Structures 107
More Structures
There are two more important considerations that should be placed
under structures. The first of these is the union, and the second is bit
manipulations and bitfields.
Unions
A union is defined much the same as a struct . There is a signifi-
cant difference, though. A union can have several different arguments,
each of which is a different type. The compiler, when it sees a union
declared, provides enough memory to hold the largest argument of
the union. When different arguments are used, the different types
occupy the same memory location. Consider the following sequence:
struct bothints
{
int hi , lo;
};
union both
{
long 1 ;
struct bothints b;
} compound;
This sequence will cause the compiler to generate a structure that
contains two int s. The union both will provide space for which-
ever is larger, a longora struct bothints. Of course, a long
is the size of struct bothints , so enough memory will be
provided to store a long. In use, a sequence like
compound . b . hi = a ;
compound. b . lo = b;
will place the int a into the upper location of compound , and b
will go into the lower location of compound. After these opera-
tions, compound . 1 will contain a in its upper word, and b in its
lower word. If compound . 1 were used as a variable, it would be
this combination.
Unions are most often thought of as a method of saving memory.
If several variables are completely independent and never used at the
108 Chapter 2 Advanced C Topics
same time, a union that contains these several variables will allow
the programmer to store each in the same memory location.
Bitfields
The concepts of bitfields fall loosely under structures. The first
built-in operations that allow bit manipulations from C involve an
enum. Consider the following enum:
enum {PB1=1 , PB2=2 , OUTl=4 , OUT2=8 } ;
int PORTA;
Notice that the different elements of the enum are each powers of 2.
We can then use an expression like
PORTA | = PB1 | PB2 ;
to turn on bits corresponding to PB1 and PB2 in the integer PORTA.
Or, these corresponding bits might be turned off in PORTA by
PORTA &= ~ (PB1 | PB2) ;
Tests can be executed like
if (PORTA & (PB1 | PB2) == 0)
Here the argument of the if call will be TRUE if both bits corre-
sponding to PB1 and PB2 are turned off in PORTA.
These manipulations are not really special bit manipulations. What
is seen here is merely creation of bit-like operations using normal C.
C does support bit fields. Bit fields are created in the form of a
struct. The following struct defines several bit fields:
struct
{
unsigned int PB1
unsigned int PB2
unsigned int OUT1
unsigned int OUT2
unsigned int ALL
} FLAGS;
1;
1/
l;
l;
4;
This struct consists of several bit fields. The colon that fol-
lows the field name designates the bit field whose size is the number
More Structures 109
following the colon. The first four fields are each 1 bit wide, and the
final field ALL is 4-bits wide. These bits can be turned on by:
FLAGS. OUT1 = FLAGS. OUT2 = 1;
or off
FLAGS . OUT1 = FLAGS . 0UT2 = ;
and they can be tested
if (FLAGS. PB1 == && FLAGS . PB2 == 1)
Some of the compilers have special bit constructs. These con-
structs are usually struct s that have either 8- or 16-bits within the
field. These struct s are useful as Boolean variables.
When setting up a microcontroller program, the programmer will
frequently want to have bit fields at specific locations in memory.
These bit fields can be used as I/O ports, control registers and even
arrays of bits to be used internally as flags. An approach to this prob-
lem is found in the bit array.
typedef struct
{ "
bit_0
1,
bit_l
l t
bit_2
1,
bit_3
l t
bit_4
l t
bit_5
l t
bit_6
1,
bit_7
l t
} BITS:
A mac
:ro
definition is used to create a variable:
#define PORTA (*( BITS *) 0x1000)
With these definitions, instruction statements like
PORTA. bit_7 = 0;
if (PORTA. bit_3 == 1 && PORTA. bit_2 == 0)
etc. can be used in dealing with the bits within this memory location.
110 Chapter 2 Advanced C Topics
Input and Output
C has no provision for either input or output within the language.
Any such operations must be programmed as functions that are called
from the programs. There are several standard I/O functions that are
always included with a complete C compiler. However, in many in-
stances, compilers for very small microcontrollers will have no built-in
input/output capability.
File accesses are through a set of functions and a special struc-
ture. The struct FILE is a structure that contains all parameters
needed to access a file. The file pointer is given a value by
FILE *fp;
FILE *f open (char* name, char* mode);
Here name is a pointer to a character array that contains the name
of the file. This name can be simply the file name if the file resides in
the default directory, or it can be the complete path name-file name
combination for files elsewhere in the file system. The string pointed
to by mode is a one or two character string. The various modes are
Wt.t//
\\ -. //
r" read only
w" write only
a" append
and sometimes
"b" binary
w rw" read/write
To open a file, the program must contain a statement
fp=f open (name, mode) ;
where f p is declared as a pointer to the type FILE. Once the file is
opened, all file accesses will use f p as an argument in some way.
There are two single character file access functions:
int getc (FILE* fp) ;
int putc(int c, FILE* fp) ;
The first function returns a character from an open file f p and the
second puts a character c onto an open file f p. There are three special
file pointers created by the operating system when a program is loaded.
These three file pointers are stdin, stdout, and stderr. stdin
Input and Output 111
is the standard input, and defaults to the keyboard, stdout in the
standard output and defaults to the terminal screen, and stderr is
the standard error output which defaults to the terminal screen. These
defaults can be redirected by the operating system through the use of
redirection operators and pipe operators when the program is executed.
The functions get char ( ) and put char ( ) are macro defini-
tions:
#define getcharO getc(stdin)
#define putchar(c) put c (c, stdout)
Therefore, get char ( ) will read a single character from the key-
board, and put char ( ) will put a single character to the terminal screen.
There are string I/O with the file functions. A string can be put to
a file by
int fputs(char* string, FILE* fp) ;
This function returns an EOF if an error occurs. Otherwise, it
returns a zero when there is no error.
A string can be read from a file by
int fgets(char* string, int maxline, FILE* fp) ;
f gets returns the next input line from the file f p. These data are
written into string, and can contain at most maxl ine - 1 charac-
ters. The string is terminated, f gets will return a zero when an
error occurs. If there is no error, f gets returns the pointer string.
Another important file access function is
int f close (FILE* fp) ;
This function releases the file pointer f p and it causes any data
written to the file, but not written to the final destination, to be written.
For example, most disk file systems use a buffer to store up a signifi-
cant amount of data before it is written to the disk. If data are buffered
when the f close ( ) routine is executed, any buffered data is written
to the disk and the program connection to the file system is dissolved.
Different compilers have several file handling routines. To ob-
tain the maximum advantage of these routines, you should consult
the manual that comes with your specific compiler.
We have seen several instances of input/output functions. The
most common is the print f ( ) function. This function is but one
112 Chapter 2 Advanced C Topics
of several functions that can be grouped together under the category
of formatted input/output. The formatted output functions include
int print f (char* format, argl, arg2 , . . .) ;
int sprintf (char* string, char* format,
argl , arg2 ,...);
int fprintf (FILE* fp, char* format, argl,
arg2 ,...);
The function print f ( ) has been used throughout the text so far,
and there should be little question as to its use. This function prints data
to the terminal screen. The pointer format points to a character string
that contains the information to print out. Within the format, there can
be conversion commands identified by a percent sign %. These com-
mands will be discussed in detail later. The number or arguments, argl,
arg2 , etc. depend on the number of commands within the format string.
The second function above, sprint f ( ) , performs exactly the
same conversion as print f ( ) . Rather than printing the result to
the screen, it is written into the character array string in memory
designated by char* string.
Data can be printed to a file with the third function f print f ( ) .
Here f p is the file pointer of an open file, and the remainder of the
arguments are exactly the same as with the print f ( ) function.
The following is a list of all the conversion commands and their
meanings. If the character following the % in a format string is not
found in the table, function behaviors will be undefined.
Character Printed as
d, i int : decimal number
o int : unsigned octal number
x,X int: unsigned hexadecimal number
u int : unsigned decimal number
c int : single character
s char* : print characters from string until '\0' is
detected.
f double : floating point [-]m.dddd where the num-
ber of d's is given by precision. Default precision is 6.
e , E doub 1 e : floating point [-]m.ddddExx where the num-
ber of d's is given by the precision, and the power of 10
exponent can be plus or minus. Default precision is 6.
Input and Output 113
g , G Uses e or f format, whichever requires least space.
No argument is converted. Print as a
% No argument is converted. Print as a %
Additional formatting capability exists. The complete form of
the % command is as follows:
[flags] [width] [.precision] [modifier] type
where the various optional fields have the following meanings.
flags
Left-justify the value if there is a width field speci-
fied.
+ Print leading + character for positive outputs
width
Width Precision
Minimum width of The number of digits
print zone. Pad the to the right of the
field with if the decimal point. For
leading width strings, the maximum
specifier is 0. number of characters.
Modifier
Modifier Meaning
h Used with any integral type, data type is short.
1 Used with any integral type, data type is long.
L Used with any floating point type, the data type is
long double.
This formatting approach works with strings as well as nu-
merical outputs. If a string output contains a field width or a precision,
it works much the same as a numerical output. If the field width
specified is not long enough to hold the output string, the specified
field width is ignored. The complete string is printed out. With a
string, precision specifies the number of output characters. If the
precision is smaller than the field width, the width of the output field
will be the precision specification. A minus sign applied to field width
114 Chapter 2 Advanced C Topics
will cause the data to be printed to the left edge of the field. Other-
wise, the data will print to the extreme right edge of the field.
Formatted input is also provided in C. The three formatted
input functions are
int scanf (char* format , argl , arg2 ,...);
int sscanf (char* string, char*
format , argl , arg2 ,...);
int f scanf (FILE* fp,char* format, argl , arg2 ,...) ;
The first of these functions reads formatted data from the standard
input, the second reads formatted data from a string in memory, and
the third reads formatted data from an open file f p. There is a sig-
nificant difference between the formatted inputs and outputs. Recall
that the arguments in the formatted outputs are data values. In the
formatted inputs, all arguments must be pointers to locations in
memory that can hold the data types specified in the data string. If
the program should contain
int i ;
scanf ("%d",i) ; /* BAD */
the program will probably compile, the compilers will not note an
error, and the program will crash because the scanf ( ) argument is
not a pointer. It is generally recommended that you avoid use of the
scanf ( ) function.
Memory Management
There are two functions in C that allow dynamic memory man-
agement. The function
void* malloc (size_t n) ;
returns a pointer to a block of memory that is n bytes long. This
memory is uninitialized, malloc ( ) (and calloc ( ) below) re-
turns a pointer to a block of memory of the type void. Therefore,
before it can be used, the return pointer must be cast onto the data
type that the program requires. In this case, an assignment of the
return value to a pointer of the correct type will work. If this step is
Memory Management 115
not taken, none of the usual pointer arithmetic will work. If the
memory is not available to be allocated, the allocation program will
return a NULL.
A second dynamic memory allocation function is
void* calloc(int number, size_t n) ;
This function returns a pointer to a type void. It takes two arguments.
The first argument is the number of items and the second argument is
the size of the specific item. Therefore, if you wished to allocate space
for ten TNODEs from the above problem, you should use
(TNODE *) calloc(10, sizeof (TNODE) ) ;
calloc ( ) differs from malloc ( ) in that it returns a pointer to
initialized space. The calloc () function initializes all memory
assigned by the function to zero. When memory is allocated by ei-
ther malloc ( ) or calloc ( ) , this memory must be returned to
the system. If allocated memory is not returned to the system, even-
tually all of the available memory will be allocated. Further attempts
to allocate memory will fail. If the return pointer from the allocation
function is not tested and properly handled when a NULL is returned,
the program will exhibit undefined behavior. This problem can be
avoided by deallocating memory when it is no longer needed by the
program. The function
void free (void* ) ;
will release allocated memory. The argument in this case must be a
pointer that was returned from an allocate function, either mal loc ( )
or calloc ( ) .
An additional memory allocation function is
void *realloc (void *,size_t);
The first parameter here is a pointer to a previously allocated memory
block, and the second is the new size that you want for the allocated
memory. This function will change the size of the allocated memory
block. If you are reducing the size of the block, the pointer returned
will probably be the same as that passed to real loc. If you are
increasing the size of the block, and there is not enough contiguous
memory available, the function will search for a proper sized block.
116 Chapter 2 Advanced C Topics
If one is found, the data stored in the original block are copied to the
new block and the proper space is allocated. The pointer returned in
this case will not be the same as that passed.
Miscellaneous Functions
C provides several sets of miscellaneous functions that are very
useful. As mentioned earlier, the input/output functions that are
prototyped in stdio . h are not parts of the language, but instead
are separate functions. The miscellaneous functions are prototyped
is several different header files. Many of these functions are macros,
and if they are, the macro definitions are contained in the header file.
Following is a list of several header files specified by the ANSI
Standard. With each header file is a list of functions or function pro-
totypes contained within these files. You should look to the
documentation for your compiler to get the detailed descriptions of
each of these files.
As mentioned earlier, these files are there, tested, work and are
probably as robust and reliable as any code written. They do what
they are supposed to, and they are free. It makes little sense to re-
write these programs just to satisfy an ego trip. I have seen shop
coding standards that did not allow the use of standard library files
because the programmers could not examine the source code. Such a
standard is absolutely silly. If you do not trust the compiler to pro-
vide safe, satisfactory library code, you should not trust the compiler
to generate any code. Therefore, you are using the wrong compiler.
Get a compiler that meets your standard.
Most embedded programs that employ microcontrollers do not
have standard connections to input/output devices. For example, there
is rarely a terminal or a keyboard attached to an embedded system.
More likely you would expect to find a numeric, or special, keypad
and an LCD special purpose output display. None of the standard i/o
programs can connect with such devices. In such cases, it will be
your responsibility as the programmer to write drivers to interface
with these devices. Also, you will rarely see disk files in your em-
bedded product. Therefore, you will rarely need to include the header
file stdio.hina microcontroller program.
Miscellaneous Functions 117
STRING OPERATIONS— Defined in string . h
strcat ( s , t ) concatenate t to s
strncat ( s , t , n) concatenate n characters of t to s
strcmp ( s , t ) return negative, 0, or positive
for s< t, s ==t, ors>t respectively
strncmp ( s , t , n) compare n characters of t and s
strcpy ( s , t ) copy t to s
strncpy ( s , t , n) copy n characters of t to s
strchr ( s , c ) point to the first c in s or x \ '
s t rrchr ( s , c ) point to last c in s or x \ '
CHARACTER TESTS— in ctype . h
i s alpha ( c ) non-zero if c is alphabetic, if not
i supper ( c ) non-zero if c is uppercase, if not
i s lower ( c ) non-zero if c is lowercase, if not
isdigit ( c ) non-zero if c is a digit, if not
i sxdigi t ( c ) non-zero if c is a hexadecimal digit, if not
i s a 1 num ( c ) non-zero if c is a digit or alphabetic, zero if not
i s space ( c ) non-zero if c is a blank, tab, newline,
return, formfeed, vertical tab, if not
i spunc t ( c ) non-zero if c is a printing character except
space, letter, or digit
i sent r 1 ( c ) non-zero if c is a control character, if not
i sgraph ( c ) non-zero if printing character except space
i sprint ( c ) non-zero if printing character including space
toupper ( c ) return c converted to upper case
tolower ( c ) return c converted to lower case
MATH FUNCTIONS— in math . h
s in ( x ) sine of x, x in radians
cos (x) cosine of x, x in radians
tan (x) tangent of x, x in radians
asin (x) arcsine of x, -1<=x<=1
acos (x) arccosine of x, -1<=x<=1
atan2 (x , y ) four quadrant arctan of y/x, in radians
at an (x) two quadrant arctan of x, in radians
s i nh ( x ) hyperbolic sine of x
118 Chapter 2 Advanced C Topics
cosh (x) hyperbolic cosine of x
t anh ( x ) hyperbolic tangent of x
exp (x) exponential function of x
log (x) logarithm base e of x
1 og 1 ( x ) common logarithm of x
pow ( x , y ) raise x to the y power
sqrt (x) square root of x
f ab s ( x ) absolute value of x
ceil ( x ) smallest integer not less than x
f 1 oor ( x ) largest integer not greater than x
1 dexp ( x , n ) x*2 A n
f r exp ( x , i n t * exp ) split double into mantissa and exponent
modf (x, double *ip) Splits x into integral and fractional
parts. Puts integral in ip and returns
fractional part.
f mod ( x , y ) floating-point remainder of x/y
These functions all require double arguments and return double values.
Utility functions — in stdlib . h
double atof (const char *s);
integer atoi(const char *s);
long atol(const char *s);
double strtod(char *str,char **scanstop);
long strtol(char *str,char **scanstop,int base);
unsigned long strtoul(char *str,char **scanstop,int base);
ldiv_t ldiv(long numer,long denom);
div_t div(int numer,int denom);
long labs(long n);
int abs(int n);
void *bsearch(void *key, void *base,size_t number,size_t size,
int(*compare)(void*,void*));
void qsort(void *base,size_t number, size_t size,
int(*compare)(void*,void*));
int rand(void);
int srand(unsigned int seed);
void *calloc(size_t nobj, size_t size);
void *malloc(size_t size);
void *realloc(void *p, size_t size);
Miscellaneous Functions 119
void free(void *p);
void abort(void);
int atexit(void(*fcn)(void));
void exit(int status);
int system(const char *name);
Non-local Jumps — in set j mp . h
int setjmp(env);
int longjmp(env,int);
Diagnostics — in assert . h
The macro assert ( ) is defined in assert . h
Signals — in signal . h
void (*signal(int sig, void (*sighandler)(int)))(int);
int raise(int sig);
Date and Time Functions — in time . h
char *asctime(struct tm *timeptr);
clock_t clock(void);
char *ctime(time_t *timer);
double difftime(time_t time2, time_t timel);
struct tm *gmtime(time_t *timer);
struct tm *localtime(time_t *timer);
time_t mktime(struct tm *timeptr);
size_t strftime(char *buffer, size_t bufsize, char *format,
struct tm *timeptr);
time_t time(time_t *timer);
Standard input and output — in stdio . h
void clearerr(FILE *fp);
int fclose(FILE *fp);
int feof(FILE *fp);
int ferror(FILE *fp);
int fflush(FILE *fp);
int fgetc(FILE *fp);
int fgetpos(FILE *fp, fpos_t *pos);
char *fgets(char *buffer, int n, FILE *fp);
FILE *fopen(char ^filename, char *access);
120 Chapter 2 Advanced C Topics
int fprintf(FILE *fp, char *format, ...);
int fputc(int c, FILE *fp);
int fputs(char *string, FILE *fp);
s i z e_t fread(void *buffer, size_t size, size_t number, FILE
*fp);
FILE *freopen(char ^filename, char *mode, FILE *fp);
int fscanf(FILE *fp, char *fs, ...);
int fseek(FILE *fp, long offset, int origin);
int fsetpos(FILE *fp, fpos_t *pos);
long ftell(FILE *fp);
s i z e_t fwrite(void *buffer, size_t size, size_t number, FILE
*fp);
int getc(FILE *fp);
char *gets(char *buffer);
void perror(char * string);
int printf(char *format, ...);
int putc(int c, FILE *fp);
int puts(char ^string);
int remove(char ^filename);
int rename(char *oldname, char *newname);
void rewind(FILE *fp);
int scanf(char *format, ...);
void setbuf(FILE *fp, char *bufptr);
int setvbuf(FILE *fp, char *bufptr, int buftype, size_t
bufsize);
int sprintf(char *s, char *format, ...);
int sscanf(char *s, char *format, ...);
FILE *tmpfile(void);
char *tmpnam(char ^buffer);
int ungetc(int c, FILE *fp);
int vfprintf(FILE *fp, char *format, va_list arglist);
int vprintf(char *format, va_list arglist);
int vsprintf(char *s, char *format, va_list arglist);
As you scan through the above listings, you will see many types
that are not from the usual list of available types. Whenever you find
such a type, the type will be either a typedef of a structure or a
typede f of a standard type. These typede f s will be found in the
header file where they are used.
Summary
Summary 121
In this chapter, you have seen how pointers are used in C, learned
about structures, and viewed a summary of the contents of the stan-
dard C library. You have also been shown the essentials of creating
header files that can attach peripheral devices to your programs. These
areas will be explored in detail in later chapters.
Chapter 3
What Are Microcontrollers?
A microcontroller differs from a microprocessor in several im-
portant ways. The early name for a microcontroller was
microcomputer. The big difference between a microprocessor and a
microcomputer/microcontroller is the completeness of the machine
each represents. A microprocessor was simply the "heart" of a com-
puter. To put a microprocessor into use, the designer required memory,
peripheral chips, and serial and parallel ports to make a completely
functional computer. By contrast, the microcomputer was designed
to be a complete computer on a single chip. Necessary memory and
peripheral components were integrated onto the chip so that a com-
plete computer-based system could be built with a minimum of
external components. A basic microcontroller is shown in block dia-
gram form in Figure 3-1.
Program
Memory
Input/Output
Arithmetic
Logic Unit
Data Memory
Figure 3-1: A Typical Microcontroller Block Diagram
123
124 Chapter 3 What Are Microcontrollers?
The central control unit of the microcontroller is the arithmetic
logic unit (ALU). Figure 3-1 shows that the ALU is connected to
three different blocks. The first is the input/ output block (I/O), the
second is the program memory, and the third is the data memory.
Most Motorola microcontrollers combine the last three blocks into
one block. The architecture shown in the figure is known as a Harvard
architecture, as opposed to the more common Von Neumann
architecture. The Harvard architecture is a computer configuration
in which the memory area that contains the program instructions for
the computer is separated from the memory area in which data are
stored. By contrast, the Von Neumann architecture has just one
memory space where both program and data are stored.
The main functional difference between Harvard and Von
Neumann architectures is in their ultimate operating speeds. Both
architectures require that the ALU access memory once each
instruction to get the next instruction to execute. Often the instruction
being executed will also require an access to memory. Reading data
into a register, storing data in a memory address, and accessing a
location in memory that is in fact an input/output register are examples
of operations that require memory accesses in addition to the normal
memory fetches. As seen in Figure 3-1, the Harvard architecture has
two or more internal data busses over which these different accesses
can take place. There are usually two such internal busses: one for
instruction access, and one for other data access. The processor can
easily tell which data bus to use. If the access is to fetch an instruction,
it is relative to the program counter. These accesses will go to the
program memory area. All other memory accesses will go to the data
memory area. It is entirely possible to have two or more memory
accesses simultaneously with a Harvard architecture.
The Von Neumann architecture is somewhat simpler than the
Harvard architecture. A Von Neumann processor has only one memory
bus. All memory accesses must go through this single path on the
system. With such a system, the processor can never process more
than one memory access at a time and all memory accesses — instruction,
data, or input/output — must pass through a single data bus. This is the
origin of the term "Von Neumann bottleneck." The multiple accesses
to memory for each instruction ultimately limit the maximum speed of
a Von Neumann architecture processor. However, the speed of such
processors can be many millions of instructions per second, so there
What are Microcontrollers? 125
are numerous excellent, fast microcontrollers constructed with the Von
Neumann architecture. The Von Neumann architecture has been the
mainstay of microcontrollers and will be the only microcontroller
configuration available for the foreseeable future.
A microcontroller has its program stored internally, and the ALU
reads an instruction from memory. This instruction is decoded by the
ALU and executed. At the completion of the execution of the
instruction, the next instruction is fetched from memory and it is
executed. This procedure is repeated until the end of the program is
found, or the program gets into a loop where it is instructed to branch
back to a beginning point. In this case, the machine will stay in the
loop forever or until something happens to release it from the
never-ending loop.
There are three ways for a machine locked in a loop to be removed
from the loop so it can execute code outside of the loop. These
operations are called exceptions. The first is to reset the part with a
reset signal. A reset signal usually requires connecting the reset pin
of the part to a logic low signal. A logic low is usually ground. When
this condition is detected, several internal registers are set to
predetermined values, and the microcontroller fetches the address of
the reset routine from a specific memory location. This address is
placed in the program counter, and the program starts to execute.
There is a table in memory that contains the addresses of several
routines accessed when exceptions occur. These are the addresses of
the interrupt service routines, reset routines, etc. This table is called
the vector table, and the addresses are called vectors.
A second means of forcing the part out of the loop is for the part
to detect an external interrupt. An external interrupt occurs when the
interrupt request (IRQ) pin on the part is set low. This pin is tested at
the beginning of the execution of each instruction. Therefore, if an
instruction is being executed when an IRQ is asserted, the instruction
will complete before the IRQ signal is processed. Processing for the
IRQ consists of first determining if IRQs are enabled. If they are, the
status of the machine is saved. All interrupts are disabled by setting
the interrupt mask bit in the status register of the microcontroller.
Then the address stored in the IRQ vector location is fetched. This
address, the address of the interrupt service routine (ISR), is placed
in the program counter. The ISR then executes.
126 Chapter 3 What Are Microcontrollers?
The process of saving the status of the machine is to push the
contents of all machine registers onto the machine stack. Therefore,
the ISR can safely use any of the central machine resources without
disrupting the operation of the main line of code when control is
returned. When exiting an ISR, it is necessary to use a special
instruction called a return from interrupt or a return from exception.
This instruction restores the status of the machine from the stack and
picks up execution of the code from the instruction following the one
where the interrupt occurred.
The third means for exiting the main loop of the program is from
internal interrupts. The microcontroller peripherals can often cause
interrupts to occur. An internal interrupt causes exactly the same
sequence of operations to occur as an external interrupt. Different
interrupt vectors are used for each of the several internal peripheral
parts so the cause of the interrupt is generally known and control is
directed to the specific ISR for each of the several possible internal
interrupts.
Data are transferred, information is passed, or events are handled
either synchronously or asynchronously. The difference between these
two methods of data transfer has mainly to do with how the clocking
of the data is handled. The most common form of synchronous data
transfer is with a three- wire serial link. One of the wires is a clock,
and the other two are input data and output data, respectively. For a
synchronous transfer, the value of the input is usually sampled at one
edge of the clock signal (such as the fall of the clock) and the value
of the bit to be sent out is guaranteed to be correct at the fall of the
clock signal. Any synchronous system must set its output at such a
time that it will be stable while the clock is high, and hold it in that
condition until the clock signal falls. To receive a bit, the condition
of the input line must be latched into the system as the clock signal
falls from high to low.
Within the computer, there is another distinction for synchronous.
Often an input is allowed to set a bit when it occurs. If this happens,
the program will not expeditiously observe the fact that the bit is set.
In fact, the program will test the state of the bit according to the
program timing requirements. This type of operation is also called
synchronous because the test is synchronized with the program.
Asynchronous operation, on the other hand, usually depends on a
prearranged series of events to cause the data transfer. Serial data
Microcontroller Memory 127
communications is a common example of asynchronous data transfer.
Here, an input line can have two states: mark and space. A line is
held at the mark state whenever no data are being transferred. When
data are to be transferred, the data line is transitioned to the space
state and held there for a specified time. This period is called the
start bit. From that time onward, the data bits are placed on the line
a bit at a time so that at the specified time intervals the receiving
device can examine the data line and determine the bit sequence.
Asynchronous operation means that there is no computer clock-
related specification as to the time that events will occur. Another
example of asynchronous transfer occurs within the computer.
Generally, events and data transfers that are initiated by interrupts
are considered to be asynchronous. Most of the peripheral devices
that are found on Motorola microcontrollers will allow either
synchronous or asynchronous notification of the program that the
peripheral business is completed.
The following sections contain brief discussions of microcontroller
memory and several of the standard peripherals found on these devices.
These discussions are intended to be qualitative and provide a broad
overview of these parts of the microcontroller. Detailed descriptions
of how to access and use these several peripherals will be found in
later chapters.
Microcontroller Memory
In a microcontroller, the program instructions are usually stored
in a memory type called read-only memory (ROM). ROM is usually
programmed by a special mask during the manufacture of the
microcontroller and is called masked ROM. ROM is the least expensive
means of storing a program in a microcontroller, especially for high-
volume manufacturing.
There are at least two means for the end user of the microcontroller
to place the program memory into the chip. The first is called erasable
programmable read-only memory (EPROM). EPROM is a memory
technology that can be erased by exposing it to high-energy ultraviolet
light. The EPROM requires the application of a high voltage to be
programmed. The memory can be programmed with either a
development system or a special programming board designed
specifically to program the microcontroller.
128 Chapter 3 What Are Microcontrollers?
Packages that contain EPROM have a quartz glass window
through which the ultraviolet light can pass with minimum attenuation.
These packages are quite expensive, and therefore, microcontrollers
with EPROM are usually too expensive to use. EPROM was used for
development purposes in the past, but it is just too expensive in light
of more recent developments to be used for that purpose today.
For limited production purposes, a less expensive version of the
EPROM chip is available. This is the one time programmable (OTP)
chip. An OTP chip has the exact same silicon component as an
EPROM, but it is packaged in a standard plastic package. This package
is much less expensive than the windowed package discussed
previously. However, once the chip has been programmed, the
program contents cannot be changed.
There is yet another means of storing programs or, in some
instances, data in a microcontroller. This technique is called
electrically erasable programmable read-only memory (EEPROM).
EEPROM is programmable from instructions within the
microcontroller. EEPROM also requires a high programming voltage.
If there are large blocks of EEPROM on the chip, the programming
voltage is usually applied during the programming cycle through a
pin connected to an external voltage source. In cases where the amount
of EEPROM to be programmed is relatively small, a charge pump on
the microcontroller chip will allow the EEPROM to be programmed
with no externally applied voltage. The amount of EEPROM that
can be programmed with an on-board charge pump is usually so
small that it is not useful for storing program instructions. But on-
board EEPROM can be quite useful in the storage of data generated
by the program that must be saved through a power-down cycle.
Sometimes in the execution of the program, some data are generated
that must be saved for later use. These data are called volatile data or
variables, and are usually stored in random access memory (RAM).
Careful design of a program will usually result in the need for much
less RAM than ROM. In most microcontrollers, the amount of RAM
is usually 60 to at most a few hundred bytes. The amount of ROM,
EPROM or EEPROM usually runs from 1000 bytes upwards to a
few tens of thousands of bytes.
EEPROM is quite expensive, and has been replaced by a newer
technology called FLASH memory. FLASH programs in a manner
nput/Output 129
similar to EEPROM and it is inexpensive enough to allow rather
large amounts of programmable memory on a microcontroller chip.
You will find chips with 30,000 bytes and more of FLASH and the
intent is to use these chips for production runs. The FLASH is
programmed as part of the production cycle. We will see many details
on programming FLASH memory in later chapters.
The architecture of Motorola microcontrollers is strictly Von
Neumann. That is, within the microcontroller chip, there is only one
data bus over which all program, data, and input/output must pass. In
a Harvard architecture system, each of these different data types will
have a dedicated bus over which the information will pass. Therefore,
the Harvard architecture microcontroller is able to access data, program,
and I/O simultaneously. The simultaneous availability of these different
data paths can result in a significant increase in overall processor speed.
It also increases the area of the microcontroller die and, hence, the cost
of the microcontroller. In general, most of the applications to which
the microcontrollers are directed do not require extreme speed. Thus,
the Von Neumann architecture is completely satisfactory.
Input/Output
The Motorola microcontrollers use an architecture called memory-
mapped I/O. Each I/O device input and output registers, its control
registers, and status registers are mapped into memory locations. I/O
transactions require no special computer instructions. It is merely
necessary to know the memory locations of the pertinent registers and
the uses of the register bits to be able to handle any I/O function. Listed
below are brief descriptions of several microcontroller I/O peripherals
found on Motorola microcontrollers. Not all of these peripheral systems
are found on each microcontroller. It is possible to pick and choose
between needs for the several peripheral systems and select a
microcontroller that has exactly those peripherals required.
Timer Subsystems
There are four popular timer systems that you will find on different
microcontrollers. The first is a general-purpose timer. Motorola refers
to the general-purpose timers as either 8- or 15-bit timers. These
timers are different. The 8-bit system contains a prescaler that counts
down from system clock. The output from the prescaler is fed into a
130 Chapter 3 What Are Microcontrollers?
counter that counts down from it the value stored in it. When the
counter underflows, a flag is set, and an interrupt can be executed.
The 15-bit timer is a strictly Motorola name, and it is even
simpler than the 8-bit timer. This timer has a 15-bit minimally
programmable prescaler. An interrupt can be taken from two
locations in this ripple counter.
A second class of timer is the 16-bit timer. This timer is often
called a general purpose timer. These timers contain a 16-bit counter
that is clocked by the system clock. There are two associated
subsystems: the first is called an input capture system, and the second
is the output compare.
The input capture system simply captures the value of the system
timer counter when an input occurs. These inputs can set a flag or
request an interrupt so the input can be processed either synchronously
or asynchronously. The important fact is that the exact time of the
input relative to the 16-bit clock is saved when the input occurs.
Applications for input capture systems are interpulse period
measurements or frequency measurements.
The output compare system allows the programmer to specify a
time relative to the 16-bit counter when an output is to occur. This
time is calculated by adding the time offset value to the current value
of the 16-bit counter. This result is stored in the output compare
register. When the 16-bit counter counts to the value in the output
compare register, the output occurs, a bit is set, and an interrupt can
be processed if desired.
Input capture and output compare functions are sometimes called
high-speed inputs and outputs. The number of input captures and
output compare systems vary from as few as one each to as many as
16 programmable timers, each of which can be either input capture
or output compare.
There is another style of timer subsystem that is used on high-end
microcontrollers. This system is called the timer processor unit (TPU).
In most conventional computers, the contents of a memory location
are called an operand, and the processor has built-in operators that
operate on the operands. A TPU is also a computer, but rather than
using memory location contents as operands, time is the main operand
used by the TPU. Most TPUs contain many complex systems to
implement their operation. The TPU of the M68300 family and the
M68HC16 family contains sixteen registers, each of which can be
nput/Output 131
operated as either an input capture or an output compare. Each output
compare can have its events coupled to other registers to control
intricate timing events with fine time resolution. We will not see the
direct programming of a TPU in this text, but we will see some of the
types of events that are controlled by the TPU programmed with the
usual 16-bit timer.
On the newer computers, such as the MCORE architecture, a
time-of-day (TOD) clock has been introduced. This clock is based on
a 32768-Hz watch crystal. These crystals are readily available, small,
very accurate, and quite inexpensive. Their only problem is that they
are slow, and are not very good for fine time measurements unless
the crystal is used as a time base to a frequency synthesizer.
Another timer function found on most microcontrollers is the
computer operating properly (COP) or watchdog timer. Most
microcontrollers are placed in embedded controls. That is, the
microcontroller is a part of a larger system, and usually an operator
never deals directly with the microcontroller. Even though great care
has been taken in the design of the microcontroller, it is possible to
cause these devices to get lost from the program that they are
executing. The power might dip, or a large transient magnetic field
might cause the part to go into abnormal operation. In such a case,
the easiest way to restore normal operation is to send the part through
a reset sequence. Such a sequence will restore all of the initial internal
status of the microcontroller, execute the initialization code procedure
of the program, and restart the execution of the application loop. A
COP timer provides just this function. A COP timer is a timer with a
relatively long period. Once the COP timer is started, it is necessary
for the main program to reset the COP periodically prior to the
expiration of the COP period. The COP timer is never allowed to
time out. If the computer gets lost, the program no longer resets the
COP, so the timer will eventually overflow, and this operation causes
the microcontroller to reset. Therefore, if the part ever gets lost from
its normal program sequence, the COP will force a reset and restore
the normal operation of the system.
Digital Input/Output
Most microcontrollers have several digital I/O ports. Usually a port
consists of eight or fewer bits, and the bits in these ports can be outputs,
inputs, or often bit programmable as either input or output bits. If a
132 Chapter 3 What Are Microcontrollers?
port has programmable I/O, it will have an associated data direction
register — DDRA, DDRB, and so forth. The ports are usually named PORTA,
PORTB, and so forth. DDRA is associated with PORTA. Each bit in DDRA
has a corresponding bit in PORTA. If a bit in DDRA is set, the corresponding
bit in PORTA is an output. The same is true for PORTB, PORTC, PORTD,
PORTE, and so forth if these ports exist on the part.
A port pin can be made into an output. When this occurs, this pin
becomes a latched output. In other words, when this bit is set it will
remain set until it is reset by the program, and vice versa. Just because a
port pin is designated to be an output does not mean that its state cannot
be read by the computer. When a port is read in, the state of all of the
outputs as well as the state of the inputs will be shown in the result.
Some I/O pins are multiplexed and serve multiple functions. For
example, microcontrollers with analog-to-digital converters, ADC,
usually allow the ADC pins to serve as digital input pins as well. In
that case you need merely read the input port, and those pins that are
above the high threshold will indicate one, and those below the low
threshold will indicate zero. Reading the port does not affect the
ADC operation at all.
Analog-to-Digital Converters
The ADC subsystem on most microcontrollers consists of a single
successive approximation analog-to-digital converter preceded by an
analog multiplexer that can switch the converter to any of several input
pins. The program controls this switching. The electromagnetic
environment of the surface of a microcontroller die is about as bad as
can be found anywhere. Therefore, attempts to do fine resolution
measurements of analog voltages in these parts is fraught with problems.
Most ADCs use a resistive ladder to act as a digital-to-analog converter.
The inputs to this ladder are sequenced in a prescribed manner to build
a voltage that matches the voltage being measured. The input to the D-
to-A is then the digital equivalent to the voltage being measured.
Precision resistors are very difficult to manufacture on silicon,
and even precision matching between resistors is extremely difficult.
While making precision capacitors is very difficult on a silicon die,
it is possible to make several capacitors with highly accurate ratios
between the capacitor values. Therefore, the approach is to use a set
of matched capacitors and a charge balance technique to accomplish
nput/Output 133
the successive approximation of the analog voltage. This method
works well, and 8- and 10-bit systems are available on
microcontrollers with plus or minus one-half bit accuracy.
Serial Input/Output
Where would the computer be without serial input/output? The
serial system was probably the first direct human interface with any
computer system. It has expanded, and today, relatively low-speed
asynchronous serial interfaces are used for terminal and modem and
network interfaces. High-speed synchronous serial links are used for
all of the above plus inter-computer connections, hardware peripheral
communications, and other types of devices where high-speed, secure
communication is required.
Many microcontrollers have both asynchronous and synchronous
communications peripherals built in. Usually, an asynchronous
interface is called a serial communications interface (SCI) while the
synchronous interface is called a serial peripheral interface (SPI).
Typically SCI systems can communicate at any of the popular
asynchronous serial bit rates. These systems have built-in baud rate
generators, double buffered input and output registers, and all of the
error detection found on a universal asynchronous receiver-transmitter
(UART) chip. These I/O devices can be either polled or interrupt-
driven by the computer portion of the microcontroller.
The SPI is designed to communicate at high speeds with other
microcontrollers or perhaps with hardware devices with a synchronous
serial interface. These devices typically run at megabit per second
rates. Since synchronous systems require a system clock, each
microcontroller SPI can act as either a master or a slave. The main
difference between the master and the slave is which chip generates
the system clock. The master generates the system clock, and the
data are clocked into and out of the slave by the system clock.
Communications with the microcontroller and the SPI can be either
polled (synchronous) or via interrupt controller (asynchronous).
Different Controllers
Not all of these peripheral systems are found on each
microcontroller. It is possible to pick and choose between needs for
the several peripheral systems and select a microcontroller that has
134 Chapter 3 What Are Microcontrollers?
exactly those peripherals required. The smallest microcontroller has
only a 15-bit timer, and the most complete MC68HC05 part has
everything but a SPI system. All varieties in between these extremes
exist. In the larger chips, some of the basic requirements for the
microcontroller change. We will see these larger chips in later chapters.
Programming Microcontrollers
The preceding brief description of what microcontrollers are gives
a rather bleak picture of a potential programming environment from
a computer standpoint. Most programmers are used to having an
operating system that handles such mundane things as I/O, memory
management, time management, program loading, error processing,
interdevice or intertask communications, and so forth. Be prepared
for a giant step backwards when you address the microcontroller.
There is usually no operating system, no libraries of useful functions,
no I/O handling, nothing but a bare-bones computer with a bunch of
hard-to-tame peripheral components onboard the single-chip device.
C compilers for the microcontrollers have been available long
enough that they are thoroughly tested and do a good job of creating
proper code. Anyone who has programmed a microcontroller in
assembly language knows that the programs must be very direct and
have no fancy overhead. Memory is strictly limited, and the compiler
must generate assembly code that is as resourceful as can be created
by any thoroughly qualified assembly language programmer for the
machine.
The development environment, while quite sophisticated in terms
of how it works, does little for the programmer in terms of direct
help in debugging a program. There are two different types of
development systems that are in common use. Both of these systems
require a host computer to run the device. The simplest of these
systems goes by names like evaluation module, evaluation system,
or evaluation board. These devices are usually board-level products
that require a power supply in addition to a host computer.
The software to run the development boards is merely a good
terminal emulator. Assemblers and linkers for the different chips are
provided as part of the development board. The programmer writes
the code for the part in the host computer. This code is assembled,
compiled, and linked in the host computer. The code is then
Programming Microcontrollers 135
down-loaded to the development board through either a serial or a
parallel link depending upon the individual system.
The development board has the microcontroller that is to be
emulated on board. This microcontroller sometimes operates in a
nonuser mode that allows internal bus access. A second computer on
the development board controls the operation of the microcontroller.
Code delivered from the host is put into memory accessed by the
microcontroller, and the microcontroller can operate as if the code
were contained within its internal memory. All of the I/O lines associated
with the microcontroller are brought to a header on the development
board, and a cable can be attached to this header to a plug-in device
that plugs into a target board. This target system then operates as if it
had a programmed microcontroller plugged into its socket.
The microcomputer on the development board has a complete
monitor system in its firmware. This monitor provides
communications with the host, down-loading and up-loading
capability and, most important, complete debugging firmware for
the microcontroller.
There is a single line assembler and disassembler in the firmware.
This package allows the programmer to examine and change memory
in assembly mnemonics. The microcontroller program can be single
stepped, run, address breakpointed, and the memory can be displayed
in normal hexadecimal format. The microcontroller runs at full speed
when emulating operation in a target board.
An experienced programmer will be able to debug code in a
microcontroller with the help of such a development board. There is
additional software available that provides a nice display of all
pertinent information in a single screen on the host computer. In this
area, you will also find that the microcontroller can be controlled
from a display of C source code on the host computer. This technique
is called source level debugging.
On later chips, another feature is incorporated to help the
development environment. This feature is called Background Debug
Mode, or ONCE. Both of these similar operations allow debug to
take place in an external computer without any access to the
microcontroller resources such as interrupts or memory. When a chip
is put into BDM, certain pins become a special serial input/output
port. There are several commands that can be delivered to this port
from an external computer. These commands allow the computer to
136 Chapter 3 What Are Microcontrollers?
set memory, examine memory, examine registers, set and clear
registers, execute code, set and clear break points, and so forth. All
of the operations normally needed to debug a program can be executed
through this special serial port. There is no need for an on-board
monitor on the microcontroller, and it is not necessary to make use
of the chip interrupts by the debugger during the debug operation.
All of the programming needed for the debugger can reside in a host
computer. Most modern chips have this type of interface, which
greatly simplifies debugging of microcontrollers.
All of the above capabilities are available with the development
boards. Another level of capability is available. These devices are box
level, and usually have a built-in power supply. Most development
systems require a host computer, and usually they come with special
software to interface with the host computer. These systems have all of
the capabilities outlined above plus some significant improvements.
The breakpoint capability of these systems is much improved over the
simple address breakpoint above. Here a complicated breakpoint can
be employed that will break the program operation on read or write, at
any data or address location, on access of data or program, or access of
a range of data or address locations. Also, the breakpoint can occur
after a specified number of occurrences of the breakpoint conditions.
Another major difference in the development systems is the trace
buffer. A trace buffer is a memory that is as many as 48 or 64 bits wide.
Each clock cycle of the microcontroller, the condition of all address
bits, the data bus, the internal microcontroller control bus, and as many
as 16 external test point lines are captured in the trace buffer.
Usually, the trace buffer is 4 to 16 kilowords deep, so it can hold
a significant number of microcontroller clock cycles. Even if the
microcontroller is running slowly, one million clock cycles per second,
such a trace buffer represents an insignificant execution time. To help
make the data contained in the trace buffer, trace buffer capture can
be controlled by a system that is the same as the breakpoint operation.
Therefore, the portion of the program that is traced is under the
detailed control of the programmer.
The data in the trace buffer can be displayed in several different
manners. The simplest, of course, is to print to the computer screen
the I/O pattern of all the lines captured. This type of display is
extremely difficult to interpret, but it is useful in some cases. To help
the programmer determine where the microcontroller is operating, it
Coding Tips for Microcontrollers 137
is possible to read the data bits and display a disassembled version of
the code being read into the microcontroller. This display is also
quite useful in debugging the code.
Yet another display is called a logic analyzer. A logic analyzer is
an oscilloscope display that shows the logical status of the various
lines captured in the trace buffer. A logic analyzer is a separate device,
but it can have a built-in disassembler that displays the disassembled
code along with the condition of the designated lines.
The devices with logic analyzers and trace buffers are quite a bit
more expensive than the development boards discussed earlier. Some
of the development systems provide source level debugging capability
for high-level languages like C.
Another approach to development systems has been made
available in some of the newer microcontrollers. The microcontrollers
from the MC68HC16 family and those from the MC68300 family all
have a background mode of operation. When operating in the
background mode, these chips stop their normal computing and start
serial communications with an external computer. The background
mode can be entered as the result of an internal command or an
external signal. There are enough debug commands that can be
communicated over this port to allow complete debug of any program
that the microcontroller might be running. Minimum external circuitry
is needed to support the debug mode, so these high-powered chips
can operate as their own development environment. Here, the
development support is mostly software contained within the host
computer, and the deliverable system can contain all of the essential
components of a development system.
Coding Tips for Microcontrollers
One of the major tasks facing a programmer when writing code in
a high-level language for any microcontroller is to make the resulting
program as readable as possible. Other people who might later need to
read or modify your code must be able to understand what is going on
in your program. It is extremely important that mnemonics be used as
much as possible when dealing with various registers, their bit contents,
and special memory addresses throughout your programs; otherwise,
the resulting code will be a "quasi-C" program filled with many numbers
and funny-looking cast operations that will be largely incomprehensible
to others trying to maintain your program.
138 Chapter 3 What Are Microcontrollers?
Modern large 8-bit systems are very nearly as complete as
yesterday's mini-computers. Programs for microcontrollers are also
getting large, and they are increasingly being written by a team of
programmers instead of a single programmer. The management of
the programming task is much more important when a team is involved
because the person-to-person interface is the most "dangerous" in all
programming. It is easy for people to make mistakes; misspelled
words, interchanged order of parameters in a function argument,
passing incorrect data to a function, etc., are all problems that can
(and often will) arise when several people try to write a single
program. Each person on the team can probably keep a maximum of
six or seven function interfaces in mind, and even then mistakes will
be made. When the program is really large, there will be dozens of
function interfaces and sources of possible error will abound. For
example, most of the time programmers will try to use what is in
mind rather than look up a questionable function call.
How can these problems be minimized? Fortunately, there are
several steps you can take to keep errors down to a manageably low
level. A big one is to make certain that an ANSI-certified compiler can
be used for any program and that the computer will enforce strict type
checking in the program. This will cause most of the errors due to
carelessness or simple accidents to be caught at compile time and not
during the debugging phase. If such errors are not caught by the
compiler, they are among the most difficult to find when they show up
at run time. Programs can be improved by following these conventions:
1 . Use a consistent constant definition naming convention. Use all capital
letters for the names of constants defined in #def ine statements.
2. Make all function names thoroughly descriptive. Use several words
for each name if possible, and capitalize the first letter of each
word so that the words can be distinguished easily. For example,
a function that returns the time should have a name like
TimeOf Day ( ) or Time_Of _Day ( ) . The important thing is
that the name describe the function in some detail. Be consistent
with naming conventions. If you start with names having no un-
derscores, keep this convention throughout the entire program.
3 . Make use of t yp e de f and the fact that structures create new types .
Where possible, create a type that is descriptive to your program.
For example, in the C language the interface to all input/output and
Coding Tips for Microcontrollers 139
files is through the type FILE. FILE in this case is simply a type
that has been made available to the program through a typedef
statement of a structure that contains all of the essential elements
needed for file input/output. It is recommended that types created
by typedef statements involving a structure should have names
with the first letter capitalized. The names of these types should be
brief and should be one word only.
4. The C macro gives us the ability to create functions that will be
compiled in-line. This is very useful, but remember there is no
type checking for macro defined functions and you do not know
what will be returned from a macro function. This does not mean
to avoid macro functions, but it does mean that with no type check-
ing we have a part of the program that does not get the normal
scrutiny we expect with most function calls. Be careful when us-
ing macros — you, not the compiler, must make certain that the
argument types and the return type are all correct.
5. Use as few global variables as possible. Global variables seem to
be an easy way to avoid using function arguments, but they also
can make debugging extremely difficult. Parameters should be
passed to functions as arguments. This approach will give the
programmer some assurance that compile time checking will catch
any typing errors, and will eliminate the problems of "side ef-
fects" found with global variables.
6. Be consistent. When designing function calls, make the order of
the parameters in the argument list logical and be consistent
throughout the whole program.
7. Avoid complicated argument lists. If it becomes necessary to send
many variables to a function, create a structure that contains all of the
arguments and send a pointer to the argument structure as a param-
eter rather than the arguments themselves. Since the members of the
structure are all mnemonics with — hopefully! — meaningful names,
the filling of the structure should result in a more accurate creation of
the parameter list than would be found if the parameters are all listed
in the function argument.
8. Be courteous — document your code. There is no need for a com-
ment for every line of code, but it is unconscionable that a program
should go page after page with no comments. Every function
140 Chapter 3 What Are Microcontrollers?
should have a comment header that explains what the function
does in the program. The header should contain a list of the func-
tion arguments and a record of modifications to the function. If
there is some nonobvious code in the program, this code should
be documented to explain what is happening.
9. Kernighan and Ritchie, the developers of C, gave us a language
that is rich with shorthand notation. While often useful, this is a
tool that can be badly misused. Don't write code in a convoluted
manner using tricky shorthand notations so that nobody can un-
derstand what you're trying to do. You might be the one who has
to maintain the resulting code a decade later!
10. Always create mnemonics for addresses and bit variables used in
the program.
11. Use assembly language only when C cannot accomplish neces-
sary operations.
^.Microcontrollers usually work with unsigned variables. Often the
microcontroller can create more efficient code when doing compares
with unsigned variables. Therefore, use unsigned variables every-
where unless it is necessary to use signed variables. A few typede f
operations will help keep the use of unsigned variables in mind:
typedef WORD unsigned int;
typedef BYTE unsigned char;
In writing code for a microcontroller, you must be able to deal
with ports at some specific address location, the bits within these
ports, control registers, certain data registers, and other important
control functions. The program can be easily written if the names of
all registers, ports, bits, and so forth are taken from the component
specification and not made subject to the whim of the programmer.
Therefore, if all of the ports and bits have the name given in the
microcontroller reference manual, then any variable used will be
defined in one place and the programmer need only find the appro-
priate register along with the definition of its bits in the reference
manual to understand what is being done in the program. The refer-
ence manual becomes part of your documentation.
We saw back in Chapter 2 how to create mnemonics for address
locations. It is necessary for programmers to write a series of define
Coding Tips for Microcontrollers 141
statements which identify all of the register locations, port addresses,
and bit names for the microcontroller. It is best if all of these pre-
liminary data are placed in a header file specific to a device and then
it can be included at the beginning of programs for the device. This
approach is convenient because it gives the programmer the ability
to use mnemonics throughout the program. It also provides some
limited portability. If a second device in the same family has some-
what different register and port locations, it is possible to write a new
header file for this second device and code written for the original
device will probably work with the second device when the proper
header file is included. We will discuss header files further as we
examine the programming of specific microcontrollers.
In this book, you will see the C programming language used to
develop code for embedded systems products. C is a powerful
language that can be abused and is often the subject of such abuse.
Don't abuse this fine language. It is your responsibility as a
programmer to write code that is easy to read, easy to understand,
easy to debug, easy to maintain, and easy to use. Any fancy antics in
coding are uncalled for and not needed. Any group that is writing
project code should implement a shop coding standard that will direct
the way code is written. There are several organizations that have
prepared such standards. I cannot agree with all of the elements of
these coding standards that I have seen, but any coding standard is
better than none. Research the matter and find a coding standard that
comes close to suiting your needs and then modify it to meet your
needs exactly. And then use it.
I previously mentioned "group development." I have seen many
small projects in which the software development was an afterthought.
There are still many projects that can fit into a few hundred bytes or
even a few thousand bytes. Such projects might not seem to require
the careful oversight implied by a shop standard. But they do! In the
development of software, you will find that the initial cost of the
software is dwarfed by the cost of maintenance. Therefore, any effort
put forth to help the maintenance of the software into the future is
effort well spent regardless of the size of the project. You will often
find that newer chips have massive memory spaces and with these
memory spaces available, the code will expand to fill them. This
expansion of the code is not necessarily bad if the expansion
accompanies improved operation and features. However, one
142 Chapter 3 What Are Microcontrollers?
implication is that the one hardware engineer in the lab can no longer
be expected to develop the code for such projects. More and more
teams are coming together to develop embedded systems software
today. Such teams almost demand that a coding standard be in place
if the code thus generated is to ever be useful.
Testing
In future chapters we will see many programming examples de-
veloped. The code in such examples has been tested and runs on the
microcontroller it was designed for. The hardware interface used to
test these programs has been a series of evaluation boards built by
Motorola. The several different boards used each represent a differ-
ent approach to an inexpensive development environment. For the
MC68HC05 family, I used the MC68HC05EVM and
MC68HC05EVS series. The EVM is an evaluation module for many
of the older MC68HC05 family of microcontrollers. This family grew
so large and so many different devices were introduced into the prod-
uct line that it was impossible for a single board to provide the
development services needed for the whole family. A new series des-
ignated as the EVS family was developed to meet these expanding
needs. The EVS is comprised of a base board with many of the needed
interface components. A daughterboard containing all of the "com-
ponent unique" capability of the system is plugged into the base board.
These systems provide excellent development environments. Each
system contains a microcontroller of the type being developed. In the
case of the MC68HC05 family, the on-board components are operat-
ing in a special mode called the non-user mode. In the non-user mode,
the device reorganizes its pins to provide an expanded bus operation so
that memory and external control can be used on what is usually a
single-chip microcontroller. For these parts, a special chip is used called
a port replacement unit, or PRU, which is put onto the expanded bus,
and its outputs are exactly the same as what would be seen on the ports
if the chip were operating in the normal mode. With this capability,
RAM can be substituted for internal ROM on the microcontroller and
the software implications of this change are unlimited. For example,
programs can be loaded into the memory at will, memory access
breakpoint can be implemented, the program can be started at any
point, the contents of the memory can be changed, and so forth.
Coding Tips for Microcontrollers 143
A firmware monitor is placed on the evaluation board. A serial
port is added, and the board, under the control of the monitor,
communicates with a host computer. As a matter of fact, the host
need only be a terminal in the most basic case. The monitor provides
several important debug and development functions. Code can be
downloaded from the host computer into the memory of the evaluation
board. The monitor contains a single line assembler/disassembler
module. With this software available, the user can examine the code
in the microcontroller memory in either hexadecimal or mnemonic
form. Also, memory contents can be changed in either hexadecimal
or mnemonic form. Breakpoints can be inserted into the code, and
the operation of the program can be observed from the terminal of
the host computer.
In addition to the evaluation capability of these boards, they also
each have extension headers that allow the board to be plugged into
a target system. When operated in this manner, the target system
operates as if it has a programmed microcontroller in its socket. Of
course, the code in the microcontroller is that in the memory on the
evaluation module. The emulation of the microcontroller in this case
is excellent. The microcontroller on board the evaluation module is
executing the code for the target system. The microcontroller re-
sources used in the target system are those found on board the
microcontroller in the evaluation module. In most cases, the lines
connected to the target system are connected directly to either the
evaluation module microcontroller or PRU. The PRU is designed to
emulate the pin operation of the microcontroller as well as possible.
Therefore, the loads presented to the target system by the emulator
and the signal responses of these pins very nearly duplicate those of
the microprocessor being emulated.
Figures 3-2 and 3-3 are photographs of the MC68HC05EVM
and the MC68HC05EVS, respectively. Note that Figure 3-2 shows a
single-board system while Figure 3-3 shows a two-board device. All
of the development devices discussed here require an external power
supply. Any device suffixed "EVM" needs a +5 volt supply as well
as +12 and -12 volt sources. The higher voltage supplies are needed
only to drive the RS232 communications signals necessary to com-
municate with the host computer. On the EVS and the
MC68HC16EVB boards, only a +5 volt supply is needed. Most of
the components being emulated by these boards can have some on-
144 Chapter 3 What Are Microcontrollers?
board nonvolatile memory, such as EEPROM or EPROM. The evalu-
ation boards each provide a means of programming these memories.
Usually, a high voltage is needed. This voltage will vary from
part-to-part and it must be applied as a separate voltage on the board.
Figure 3-2: MC68HC05EVM Single Board Development System.
Figure 3-3: MC68HC05EVS Two-Board Development System.
Coding Tips for Microcontrollers 145
The previous discussion is aimed at the MC68HC05 development
boards. The MC68HC11 also has an EVM. This EVM operates ex-
actly the same as that discussed above. There are several EVMs for
the different parts in the MC68HC11 family. Figure 3-4 shows a
photograph of the MC68HC1 1EVM.
Figure 3-4:
MC68HC11EVM Development System.
In all of the devices we've discussed, communication with the
host computer is through an RS232 serial link from the board. The
development work can be done with a terminal emulator on the host
computer. Such a terminal emulator might be found in the software
programs PROCOMM or KERMIT. Certainly, any terminal emulator
can do the job, and those mentioned here are just two of many.
Development through a simple terminal emulator is usually not
the easiest approach. Motorola ships a software package with each
evaluation device. This software package is written by P & E Micro-
computer Systems, Inc. It provides a "windowed" environment that
shows the conditions of many internal features of the device being
evaluated. All of the internal microcontroller registers are displayed,
a listing of the breakpoints that have been set are shown, a block of
the code being debugged is displayed, and a control screen is also
available. The display will differ with various devices, but in general
the screen interface is easy to use and understand.
146 Chapter 3 What Are Microcontrollers?
In some cases, a complete symbolic debug environment can be es-
tablished. P & E Microcomputer Systems, Inc., has a file system that
allows transfer of source code to the program. This source code is
displayed in the code window. It helps during the debug procedure to
see the source code while stepping through it. This capability is avail-
able with the compiler used for the MC68HC05, but not for the
MC68HC11 and MC68HC16 families.
All of the software written by P & E runs under the DOS operating
system. Today, there are fewer computers that run under DOS than
in the past, so you might want to find a later development system that
runs under a more modern operating system. I have always used DOS
by itself or running under either Windows 3.1 or one of the later
Windows operating systems. During the availability of OS/2, it was
an ideal operating system to develop microcontrollers under these
DOS-based development systems. More recent systems require
Windows 95 or later.
Development is enhanced if the host computer has multitasking
capability. This capability can come from any of the popular operat-
ing systems, such as Microsoft Windows or X Windows. In such a
case, it is possible to keep an editor with the listing file of the pro-
gram being debugged in a window along with the P & E display in
another window. This approach provides both insight into the code
being developed along with the condition of the hardware as the ex-
ecution of the code proceeds. It is recommended that this approach
be used when debugging programs on evaluation boards.
Unfortunately, none of these evaluation boards allows access to
the operation of the component being debugged when the device is
executing the test code. To achieve this level of operation would in-
crease the complexity of the development board significantly. Also
note there is no provision for a trace buffer on these boards. The
newer boards, EVS and MC68HC16EVB, provide headers that can
be connected to a logic analyzer that can act as a trace buffer and
provide many of the functions usually found in the more compre-
hensive development devices.
One last development device used is the MC68HC16EVB, shown
in Figure 3-5. This board differs from the others in one significant
way. The MC68HC16 family of devices (as well as those found in
the MC68300 family) provide a capability not usually found in most
Coding Tips for Microcontrollers 147
microcontrollers. These devices can be placed into a background
debug mode that facilitates development of the code. In all of the
above development boards, the monitor software must execute on
the microcontroller being developed. This operation means there must
be either memory bank switching or the monitor must occupy some
of the normal memory space of the microcontroller. In either case,
the presence of the monitor software can be seen in the operation of
the microcontroller. Also, development with the above systems will
require that other resources be used for the development operation.
For example, if control of the development is through a serial port to
a terminal, the SPI of the microcontroller will probably be used. In
the background debug mode (BDM) of the larger devices, this memory
and resource sharing between the monitor and the code being de-
bugged is unnecessary. When a device is placed into BDM, its normal
operation ceases and all communications with the device occurs
through a special serial port. Through this serial port, the condition
of the device can be examined, memory can be accessed and changed,
breakpoints can be established, etc., just like the invasive technique.
However, when the device is returned to operation, there is no way
the presence of the monitor can be seen by the running program.
Figure 3-5: MC68HC16EVB Development System.
148 Chapter 3 What Are Microcontrollers?
With BDM, all debug software can be placed on a host computer.
Therefore, it is possible to write much better debugging systems that
connect through the BDM system. Most recent microcontroller chips
employ some type of background debug mode operation. It is not
always called BDM. It might be called ONCE or JTAG, but these
operations are basically similar and allow background debugging of
the microcontroller and a host to contain all of the necessary debug
software.
Another advantage to using the BDM is that all communication
between the host and the device is through an 8-wire bus. This bus
can be accessed in any device, so these devices become their own
development systems and no development system is really needed to
work on the final target system.
The MC68HC16EVB uses the BDM operation. Communications
with the host computer are through the parallel, or printer, port on the
computer. P & E has written an interface for "PC clone" computers that
uses the parallel port. Its operation is essentially as for the
MC68HC05EVM and MC68HC1 1EVM devices. P & E has also made
an additional device which can connect to the 8-wire BDM bus and
connect directly to the host computer parallel port. It requires a +5 volt
power source and takes its voltage through the lines to the target system.
Three other chips will be examined in the following chapters. The
M68HC08 family and the M68HC12 family are extensions of the
M68HC05 and the M68HC1 1 families, respectively. Yet another recent
chip family is the MCORE. These RISC chips are very fast and run at
extremely low power. The MMC2001 chip from this family will be
examined. The development system for the MCORE chips is called an
EBDI — Extended Background Debug Interface. This package is
interfaced to an evaluation board through an 8-wire serial port.
All of the programs in the chapters that follow were tested on the
appropriate development boards. These programs are relatively small
because each is designed to show some feature of either the
microcontroller or the language as applied to the microcontroller; larger
programs were not appropriate to the tutorial aims of this book. With
judicious use of the boards along with the development environment
on the host computer, these programs were easy to develop and debug.
Chapter 4
Small 8-Bit Systems
Not surprisingly, writing code for any microcontroller — whether
in assembly language or a high-level language like C — requires a
detailed knowledge of the microcontroller being programmed. Usu-
ally, a high-level programming language requires little knowledge
of the underlying computer on the part of the programmer. This ap-
proach allows the programmer to concentrate on the nature of the
problem being solved rather than how to squeeze the problem into a
specific computer. A C abstract computer has been designed and you
write code for this computer when you write C code. The abstract
computer has no registers, control registers, index or address regis-
ters, or any other of the normal resources found on a typical computer.
The language is sufficient to allow proper creation of code needed to
run the core computer. However, the essence of any microcontroller
is the special on-board peripherals that it provides. These peripherals
are not directly available from the C language either.
Programming techniques must allow use of these peripherals or
the high-level language is valueless. Three distinct levels of
microcontrollers will be covered in different sections of this text. The
simplest microcontroller is embodied in the M68HC05 family. These
8-bit devices are usually completely self-contained and do not support
an expanded bus. Another level of complexity is found in the M68HC08,
the M68HC1 1 and the M68HC12 families. These computers are also
8-bit machines, but they have more registers than the M68HC05 fam-
ily and support an expanded data bus. With the expanded data bus,
these families can have external memory and peripherals in addition to
those within the chip itself. (The peripherals on these chips are not
very different from those found on the M68HC05.) The step up in
computer power is the M68HC16 microcomputer. This computer is a
149
150 Chapter 4 Small 8-Bit Systems
16-bit machine and its peripheral components are nearly all different
from those found on the 8-bit machines. The M68HC16 is a superset
of the M68HC1 1; it will execute M68HC1 1 code, but the hardware
computer extensions and new peripheral components are significant.
To successfully program a microcontroller using a high-level lan-
guage, the programmer must be able to access various control and status
registers in the computer. The program must force the language to place
both program and data memory addresses in the proper locations in the
memory map. Vectors associated with interrupt service routines, and the
service routines themselves, must be handled directly by the program.
These tasks are difficult to accomplish with most high-level languages,
but C allows access to these things without extensions. However, most C
compilers for microcomputers have extensions that allow such special
features to be easily treated.
The compiler used in this chapter is called C6805 1 . It was written to
support the M68HC05 family of devices. Be forewarned: some M68HC05
microcontroller instructions have no counterpart in the standard C lan-
guage. Special directives identify unique microcontroller characteristics
to the compiler. Listed in Table 4-1 below are nine assembly instructions
available to the 68HC05. These instructions have no equivalent C call.
They can be accessed as either a single instruction (all uppercase) or as
a function call as shown. The function call requires a pair of closed
parentheses to follow the name of the instruction.
Function Operation
CLC or CLC ( ) clear carry bit
SEC or SEC ( ) set carry bit
CLI or CLI ( ) clear interrupt flag (interrupts on)
SEI or SEI ( ) set interrupt flag (interrupts off)
NOP or NOP ( ) no operation
RSP or RSP ( ) reset stack pointer
STOP or STOP ( ) STOP instruction
SWI or SWI ( ) software interrupt
WAI T or WAI T ( ) WAIT instruction
Table 4-1: Assembly Codes Directly Callable By C6805
1 Byte Craft Limited, 421 King Street North, Waterloo, Ontario, Canada N2J 4E4
Small 8-Bit Systems 151
A pragma is a C preprocessor command not defined by the lan-
guage. As such, the compiler writer can use the #pragma command
to satisfy a need not specifically identified by the language. C6805
uses pragmas to identify microcontroller- specific characteristics.
Table 4-2 contains a list of pragmas used by C6805. The format of a
pragma directive here is
#pragma portxx portname @ address
where portxx can be portr, portw, or portrw which shows
whether the port is read, write, or both, portname is the name used
in the program for the port. The at symbol (@) identifies a memory
address. #pragma mor identifies the contents of the masked option
register used on field programmable chips. There are some instruc-
tions that are not found across the whole M68HC05 family. In
particular, some devices may not have the MUL, the DIV, the STOP,
and the WAIT instruction. The #pragma has a preprocessor call
that identifies the instructions from this set in the particular
microcontroller.
pragma Function
#pragma portxy I/O port definition
#pragma memory RAM/ROM definition
#pragma mor mask option register
#pragma has instruction set options
#pragma options compiler directives
#pragma vector interrupt vector definitions
Table 4-2: C6805 pragma Directives
This compiler has certain options that can be inserted from the
command line or, if needed by the programmer, the #pragma op-
tions preprocessor command can also be used to set the appropriate
compiler options. Finally, the #pragma vector identifies a given
function name as an interrupt service routine. When the compiler com-
piles the name specified, it will place the address of the function into
the defined vector location. Another modification in the compiled code
will take place when #pragma vector is used. All returns from a
function identified by a vector pragma will use the return from in-
terrupt instruction rather than the usual return from subroutine.
152 Chapter 4 Small 8-Bit Systems
Another useful directive pair is the #asm and the #endasm.
The code enclosed in a block that starts with #asm and ends with
#endasm must be in standard assembly language. Variables defined
in the C program can be used safely.
C can accomplish almost everything that the assembly lan-
guage program can. You will find that the C6805 compiler will create
tight, efficient code that is probably as good as can be written by a
competent assembly programmer. There are, however, some items that
are absolutely foreign and inaccessible to a compiler. A compiler cre-
ates code for an abstract machine that does not exist in reality. The
usual registers found in the real machine are nonexistent in the abstract
machine. For example, it is not possible to access the status register of
the microcontroller with compiled code. Usually, status register con-
tents are not directly important to the conduct of the program. But
later we'll see an example where the ability to manipulate the carry bit
of the status register can save many bytes of code. Therefore, it is
important to be able to use some assembly code as well as C.
This chapter will concentrate on small 8-bit microcontrollers.
Subsystems such as timers, analog-to-digital converters, computer
operating properly (COP) timers, etc., found on the 8-bit systems
will be outlined and their programming discussed. While the main
details of the central processor in the microcontroller are important
to the assembly language programmer, they are of little interest to
the C programmer. This observation is true at least at the C level. If it
becomes necessary to enter an assembly language program for opti-
mization of code size or other considerations, then the programmer
is required to have detailed knowledge of the programming model
and the internal architecture of the computer.
Let's start by discussing important microcontroller peripheral
components that you can expect to find. We'll begin with what is
probably the most important single consideration in the selection of
a microcontroller to do a job — the device memory. This discussion
will be followed by sections on other important peripherals such as
timers, analog-to-digital and digital-to-analog converters, serial com-
munications devices, and simple digital input/output lines.
Microcontroller Memory 153
Microcontroller Memory
Most microcontrollers have memory on-board. The memory is
in the form of random access memory (RAM), read-only memory
(ROM), erasable programmable read-only memory (EPROM), elec-
trically erasable read-only memory (EEPROM), and a newer type of
EEPROM memory called FLASH. These memory types are discussed
in the following paragraphs. The discussion of FLASH memory will
be deferred until the chapter on the M68HC08 family.
Random Access Memory (RAM)
In a microcontroller, onboard RAM is static random access
memory. It is always volatile — when the power to the microcontroller
is removed, the contents of this memory disappear. Sometimes, spe-
cial provisions are made to deliver power to RAM when the processor
is in the "off state. This provision is called battery backed- up RAM,
and it is one of the alternative ways that a small amount of important
data can be saved when power is removed from the main system.
The requirement for RAM in typical microcontroller applica-
tions is modest to small. Available RAM is usually limited to a few
hundred bytes, and often there will be as little as a few tens of bytes
of RAM. In the design of the microcontroller, price is a major con-
sideration. The total silicon area of the computer die often drives the
final price of the component. In most computers, a base page is the
first 256 bytes of memory. This page is unique because it requires
only 8-bits of address to reach any location. Silicon area needed to
construct the address decoding for the upper address bits is not re-
quired to address base page memory. Therefore, onboard RAM is
usually located in the computer base page. There are some other
functions that are usually assigned to the base page. Generally, you
will find that the amount of RAM is limited to less than 256 bytes.
Read-Only Memory (ROM)
Programs and other data that can never be changed are stored in
ROM. ROM is programmed during the manufacture of the chip, and
its contents cannot be changed once the microcontroller is delivered
to the customer. The ROM program is installed as a mask layer and
is called masked ROM.
154 Chapter 4 Small 8-Bit Systems
Most microcontroller applications require more program memory
space than RAM space. The smallest microcontroller usually has about
512 bytes of ROM, while the largest can contain as much as 32,000
bytes (32 kilobytes, or 32k) or more. Sometimes, the programmer
will find it desirable to have a small amount of ROM that can be
accessed from the computer base page. To meet these requirements,
the microcontroller designers will place a few bytes of ROM in the
base page memory map.
Erasable Programmable Read-Only Memory (EPROM)
EPROM is a form of programmable memory that permits the
programmer to change the program contents and, if necessary, re-
turn and change it later after testing. As the name implies, it is possible
to reprogram EPROM. First, this memory must be erased. The eras-
ing procedure involves allowing ultraviolet light to fall upon the
memory area of the die. This high-energy light removes stored charge
that is placed on each memory gate during programming.
EPROM programming requires that a higher than normal volt-
age be applied to the chip, and the code be systematically placed in
each memory location. The procedure is slow because the code must
be left in place for several milliseconds for each memory location
stored. Often, a separate programmer board is used to transfer code
from an EPROM to the microcomputer EPROM. These program-
ming boards can program as many as one to eight parts at a time.
EPROM requires a larger silicon die area than the corresponding
amount of ROM. Therefore, it is somewhat more expensive. Also,
the window package that allows the EPROM to be erased is expen-
sive. This additional expense makes it impractical to use normal
EPROM for production volumes. The window package EPROM de-
vices are excellent for development purposes, though. The modestly
higher cost of these devices is not a serious impediment to their use
in development programs.
The economics of production sets the smallest production vol-
ume for a masked ROM microcontroller at about one to five thousand
units. An alternative to the use of masked ROM at smaller levels of
production is called the one-time programmable (OTP) chip. These
devices use the standard EPROM technology for their program memo-
ries. They are programmed in the same manner as EPROM chips.
Microcontroller Memory 155
Their packages, however, have no windows to allow erasure of the
program once it is put in place. These devices cost somewhat more
than masked ROM, but they are sufficiently less expensive than the
EPROM parts to allow economic production of rather small quanti-
ties. They do have the disadvantage that, once programmed, they can
never be used for a different program.
Electrically Erasable Programmable Read-Only Memory (EEPROM)
EEPROM is a technology that uses a memory cell similar to the
standard EPROM cell. These cells are somewhat larger than the stan-
dard EPROM, and are therefore more expensive. It is possible to
erase an EEPROM electrically without the high-energy ultraviolet
light. EEPROM requires a high voltage in programming and erasing
the memory. Some microcontrollers have EEPROM that can be pro-
grammed without an externally applied high voltage. This
programming is accomplished by the use of an onboard charge pump
to generate the programming voltage. Such charge pumps are not
capable of delivering much current, so the amount of EEPROM that
can be programmed from an onboard system is usually limited to a
maximum of 512 bytes. This EEPROM is used for the storage of
information gathered after the microcontroller has been placed into
a system. This memory is not often used for the storage of program.
The smaller block of EEPROM can be programmed with the use
of the onboard charge pump, and can be programmed "on the fly"
during the normal execution of program. Devices with EEPROM are
moderately expensive because EEPROM requires the largest silicon
area of any memory technology.
Other Memory Considerations
Not all microcontrollers have enough onboard memory to suf-
fice in some jobs. In these cases, an expanded bus part can be used.
Expanded bus parts allow the programmer to access memory that is
external to the microcontroller. None of the small microcontrollers
currently provide for expanded bus operation. The larger
microcontrollers — large 8-bit, 16-bit, or 32-bit — provide expanded
bus. In some instances, they provide no onboard memory at all. As
we will see later, pins on a microcontroller are at a premium. An
expanded bus operation means that some of the component pins must
156 Chapter 4 Small 8-Bit Systems
be used to access memory and will not be available for other
microcontroller features. (Pin usage, bus expansion, and pin multi-
plexing will be discussed in later sections.) The important
consideration at this point is that the limited program memory area
usually associated with a microcontroller should not cause serious
concern. If the program grows to exceed the available size of onboard
memory for a microcontroller family, it is always possible to get a
larger microcontroller that can handle any additional memory re-
quirements. The programming goal, though, is usually to confine the
program in the smallest possible program memory space so that the
least expensive microcontroller will do the job.
Using Microcontroller Memory
In our discussion on variables in Chapter 1, it was shown that C
treats all automatic variables as local to the block in which they are
declared. The scope of these variables is the block where they are
declared. Since these variables exist only in the block where they are
declared, the memory locations dedicated to the storage of these vari-
ables can be freed when the variables go out of scope. These rules
create an ideal situation for storage on the program stack. Memory
space is easily created on the stack at the beginning of a block, and it
is equally easily destroyed at the close of the block. This operation is
exactly what is needed, but it cannot be used in a typical small
microcontroller. Most microcontrollers have very limited RAM, and
the stack arrangement in them is completely different from that you
will find on a large computer. On the M68HC05 family of parts, for
example, the chip has a hardware stack and no stack pointer into
memory that the compiler writer can access. Therefore, it is imprac-
tical to even attempt to use the system stack to store local variables.
The hardware stack on these chips is used only for storage of the
processor status when an interrupt occurs or to store the return ad-
dress from a jump to a subroutine. The stack pointer is set to its
initial value on microcontroller reset, and the occurrence of an inter-
rupt or a jump to subroutine instruction are the only ways that the
stack pointer can be changed.
In the larger machines, the stack pointer is set to a value that
points to a memory location. This pointer will be automatically
incremented and decremented by the equivalent of stack push or pull
Microcontroller Memory 157
operations. The program can arbitrarily change the stack pointer value
so that room for automatic variables can be easily provided or elimi-
nated. In the small microcontrollers, automatic variables are stored
in RAM and their scope is not limited to the block in which they are
defined. Their access is limited to their block, however. Consider the
following code segment:
main ( )
{
int i;
}
void able (void)
{
int i ;
The two occurrences of the variable i in this case will cause no
trouble because each i will be given a unique location in RAM and
the scoping arrangement will insure that any reference to i in main ( )
will not be confused with the i in able ( ) and vice versa.
An important implication of this change in storage: recursion is
no longer available! Only one memory location is available for each
variable in the program. When a stack is used to store automatic
variables, a function can call itself and a new block is created each
time the function is entered. Thus, each time a function calls itself, a
new stack frame that contains space for all automatic storage in the
function is created. The function can call itself repeatedly as long as
there is space on the stack to create new stack frames for the succes-
sive calls. Without stack space for variable storage, recursion is
impossible.
A second limitation that occurs is in the available arguments for
function calls. The compiler C6805 for the M68HC05 family de-
fines an int as an 8-bit number and a long as a 16-bit number.
158 Chapter 4 Small 8-Bit Systems
This definition is not compliant with the ANSI Standard, which re-
quires that an int be at least 16 bits wide and a long be at least 32
bits. Since the stack cannot be used to pass arguments, they must be
passed in either registers or as global variables. If they are passed in
registers, only two bytes can be passed. The arguments can be either
two ints or one long. Function return values have the same limi-
tations. Of course, the program can use global variables to pass
information to or from a function. A global variable defined external
to any function can be accessed by any function in the program.
Most C compilers for the M68HC05 family provide automatic
placement of variables in the available RAM of the part. Specific
memory addresses are identified to the compiler by the #pragma
memory directives. The following code segment shows an example
of how the memory is defined within an M68HC05 program:
#pragma memory ROMPAGEO [48] @ 32;
#pragma memory ROMPROG [5888] @ 2048;
#pragma memory RAMPAGEO [176] @ 80;
#pragma memory RAMPROG [2 56] @ 2 56;
This sequence of code will be used to identify the memory map of
the M68HC05B6. This part has 48 bytes of ROM in page zero start-
ing at address 32. There are 5888 bytes of program ROM starting at
address 2048. The 176 bytes of page zero RAM starts at address 8 0.
There are 256 bytes of EEPROM in this part that begin at the address
2 56. Here we treat EEPROM as program RAM because it is pro-
grammable and is outside of the base page.
Inclusion of the above code lines will identify the necessary
memory locations for the compiler, and further concerns about
memory locations should be unnecessary. The compiler will auto-
matically place the code in the ROMPROG area and the RAM
requirements will fall at the starting address identified by RAMPAGEO.
Programmers who wish to make use of the ROMPAGEO memory can
do so by a command like
const int table []={—,—,—,... ,—} @ 32;
This instruction will place the specified array of data in the ROMPROG0
area and will start it at the address 32.
Microcontroller Memory 159
Programming EEPROM
EEPROM is read like any other memory in the microcontroller.
Two different types of EEPROM can be found on a microcontroller:
program memory and data storage memory. Program memory usu-
ally cannot be programmed without the aid of an externally applied
programming voltage. Data storage memory can be programmed from
within the program and requires no externally applied programming
voltage. In the case of the M68HC805B6, there are 5888 bytes of
program EEPROM and 255 bytes of data storage EEPROM. Other
than the reduced size of the data storage memory, this memory is no
different from the program memory. It is possible to write code to
the data storage EEPROM and execute this code.
One additional byte of data storage EEPROM exists and is called
the OPTION register. This register content is saved in EEPROM which
is read into a latched register during the initialization of the
microcontroller. The address of this register is 0x100. The bits in this
register control the security option of the part and control a block
protect region in the data storage EEPROM that will prevent acci-
dental writing of data into the protected memory area. A description
of these bits follows:
Options Reg
0x0100
Bit 7
Bit 6
Bit 5
Bit 4
Bit 3
Bit 2
Bit 1
BitO
EE1P
SEC
SEC
BitO
EE1P Bit 1
Security Bit. When the SEC bit is programmed to
zero, the contents of the EPROM are secured by
preventing access to the test mode. The only way
to erase the SEC bit to a one state is to enter the
self-check mode. In this event, the data on
EEPROM will all be erased. When the SEC bit is
changed, its new value will have no effect until
after the next chip reset.
EEPROM Block Protect Bit. The EEPROM is in
two parts: 0x101 to 0x1 If is part 1 and 0x120 to
Oxlff is part 2. The EE1P bit allows part 2 to be
protected. If this bit is in the erased state, (1), part
2 of the EEPROM will be protected. This memory
area can be read as usual, but any attempt to write
to this area will fail. The protection remains in
160 Chapter 4 Small 8-Bit Systems
effect after this bit is erased until after the next
chip reset.
Control of the EEPROM programming is through the EEPROM CTL
register found at address 0x07. The bits in this register are as follows:
EEPCTL/CLK
0x07
Bit 7
Bit 6
Bit 5
Bit 4
Bit 3
Bit 2
Bit 1
BitO
ECLK
E1ERA
E1LAT
E1PGM
E1LAT Bit 1
E 1 PGM Bit EEPROM Program Bit. This bit turns the internal
Vpp charge pump on and off. When this bit is 0,
the charge pump is turned off, and when it is at 1,
the charge pump is turned on. The charge pump
voltage can be measured on the pin Vppl . This bit
cannot be set until after the program data are latched
in place by asserting the E1LAT bit. Resetting the
E1LAT bit will also reset the El PGM bit.
EEPROM Data/Address Latch. When this bit is re-
set to zero, both the El PGM bit and the El ERA bit
are reset to zero. When the E1LAT bit is reset, data
can be read from the EEPROM. The first data write
to the EEPROM array after this bit is set is latched
until the E1LAT bit is reset. Data can be latched
only when the El PGM bit is reset to zero. This op-
eration allows programming of the EEPROM.
E1LAT is automatically reset when the chip is reset
or when the STOP instruction is executed.
EEPROM Erase Bit. If the bit El ERA is reset to
zero when E 1 LAT and E 1 PGM are set to one, data
are programmed into the EEPROM. Otherwise,
if El ERA is set to one and El LAT and El PGM
are set to one, the specified address in the
EEPROM will be erased. El ERA cannot be set
before El LAT, and resetting El LAT to zero will
cause El ERA to be reset.
Let us now examine a possible sequence of code that can be used
to program and erase locations in EEPROM. First, several macro
definitions should be used to define the various parameters used.
El ERA Bit 2
Microcontroller Memory 161
/* pragmas to identify EEPROM control registers */
#pragma portrw EEPROM_CTL @ 0x07;
#pragma portrw OPTIONS @ 0x100;
/* EEPROM programming specific defines */
#define E1PGM
#define E1LAT 1
#define E1ERA 2
#define PROG TIME 10
/* some function prototypes */
void delay (unsigned long);
void program (int , int) ;
void erase (int );
int EEPROM [Oxff] @ 0x101; /* Identify the EEPROM */
void program(int address, int value)
{
EEPR0M_CTL.E1LAT=1; /* set the E1LAT bit */
EEPROM [address] =value; /* put the data and address
in place */
EEPR0M_CTL.E1PGM=1; /* turn on the charge pump */
delay (PROG_TIME) ; /* delay programming time */
EEPROM_CTL.E1LAT=0; /* reset the E1LAT also
resets the E1PGM bit */
} /* return when done */
void erase (int x)
{
162 Chapter 4 Small 8-Bit Systems
EEPR0M_CTL.E1LAT=1; /* set the E1LAT bit */
EEPR0M_CTL.E1ERA=1; /* set the E1ERA erase bit */
EEPROM [x] =0; /* select the address */
EEPR0M_CTL.E1PGM=1; /* turn on the charge pump*/
delay (PROG_TIME) ; /* wait the appropriate time*/
EEPROM_CTL.E1LAT=0; /* reset the E1LAT bit turns
off both E1PGM and E1ERA
bits */
} /* return when done */
The above program sequences are compiled and listed below.
The function delay is not included or linked into this program.
To handle this type of problem, the registers are set up for the func-
tion call, and the instruction JSR $ * * * * is executed. A later linking
will replace the unknown function address with the correct value. An
appropriate delay ( ) function will be written in the timer section.
The instructions for this function call are found at addresses 0x8 c
to 0x8 Of in the following listing.
0020 0030 #pragma memory ROMPAGE0 [4 8] @ 32;
0800 1700 #pragma memory ROMPROG [5888] @ 2 048;
005 00B0 #pragma memory RAMPAGE [176] @ 80;
0100 0100 #pragma memory RAMPROG [256] @ 256;
/* pragmas to identify EEPROM control registers */
0007 #pragma portrw EEPROM_CTL @ 0x07;
0100 #pragma portrw OPTIONS @ 0x100;
/* EEPROM programming specific defines */
0000 #define E1PGM
0001 #define E1LAT 1
0002 #define E1ERA 2
000A #define PROG TIME 10
/* some function prototypes */
void delay(long);
Microcontroller Memory 163
void program (int , int );
void erase (int );
0101 0101 00FF int EEPROM [Oxf f ] @0xl01;
void program (int address, int value)
0050 0051 {
0801 BF 50 STX $50
0803 B7 51 STA $51
0805 12 07 BSET 1,$07 EEPROM_CTL . E1LAT=1 ;
0807 D7 01 01 STA $0101, X EEPROM [address] =value ;
080A 10 07 BSET 0,$07 EEPROM_CTL . E1PGM=1 ;
080C 5F CLRX delay (PROG_TIME) ;
8 0D A6 0A LDA #$0A
080F CD 00 00 JSR $****
0812 13 07 BCLR 1,$07 EEPROM_CTL . E1LAT=0 ;
814 81 RTS }
void erase (int x)
0052 {
0815 B7 52 STA $52
0817 12 07 BSET 1,$07 EEPROM_CTL . E1LAT=1 ;
819 14 07 BSET 2, $07 EEPROM_CTL . E1ERA=1 ;
081B 97 TAX EEPROM [x]=0;
081C 4F CLRA
8 ID D7 01 01 STA $0101, X
0820 10 07 BSET 0,$07 EEPROM_CTL . E1PGM=1 ;
0822 5F CLRX delay (PROG_TIME) ;
0823 A6 0A LDA #$0A
0825 CD 00 00 JSR $****
0828 13 07 BCLR 1,$07 EEPROM CTL.E1LAT=0;
082A 81 RTS }
The function program ( ) requires 20 bytes and the function
erase requires 22. This is a good point to explore some of the C
programming practices that can lead to poor M68HC05 family com-
164 Chapter 4 Small 8-Bit Systems
piled code. The M68HC05 family is a family of 8-bit machines. There
has been no discussion of the programmers' register model of these
devices. Programmer models of the larger microcontrollers will be
discussed because knowledge of the programmers' model might help
in crafting good C code. For these small machines, the watchword is
8-bit. The internal structure of the system is all 8-bit. The width of
the single index register is 8 bits, and the width of the accumulator is
also 8 bits. The program counter is more than 8 bits in most cases,
but it is wide enough to address only the range of the internal com-
puter memory. In fact, the width of the stack pointer in the
M68HC05Bx family is only 6 bits. There is no luxury of spare bits in
any register.
Therefore, when writing code for the M68HC05 family, keep fore-
most in your mind that you are dealing with an 8-bit device. If at all
possible, avoid 16-bit operations because they will always result in larger
memory and/or code usage. The following code demonstrates an ex-
ample of the careless use of 16-bit implied code in an 8-bit machine.
Consider the erase ( ) routine from above. This function could
have been written as follows:
void erase (int *x)
{
EEPR0M_CTL.E1LAT=1; /* set the E1LAT bit */
EEPR0M_CTL.E1ERA=1; /* set the E1ERA erase bit */
*x=0; /* select the address */
EEPR0M_CTL.E1PGM=1; /* turn on the charge pump*/
delay (PROG_TIME) ; /* wait the appropriate time*/
EEPROM_CTL.E1LAT=0; /* reset the E1LAT bit turns
off both E1PGM and E1ERA
bits */
} /* return when done */
The only change in this version is to pass the integer *x to the
function by reference. Remember, since all addresses in the M68HC05
family of parts are greater than 8 bits, the compiler must handle the
transfer of the pointer x as a 16-bit number. The statement *x=0 ;
compiles into an inline function at the address range 0x8 Id to 0x82c in
the compiled version of the code shown below. This function creates a
subroutine that does an indexed store with a 16-bit offset. First, the
value to be programmed is placed in the accumulator. Then the op
Microcontroller Memory 165
code, 0xd7, to do a store the accumulator indexed with a 16-bit offset
is created at the location 0x56 in memory. The 16-bit offset is the
address passed to the function in the combination of the x register and
the accumulator. This address is placed in the memory locations 0x58
and 0x57, completing the store instruction. At the address 0x59, a re-
turn from subroutine instruction, 0x81, is placed to complete the
function. The index register is cleared, and this two-instruction sub-
routine is executed to store the appropriate data prior to the program
setting the latch bit.
void erase (int* x)
0052 {
0815 BF 52 STX $52
0817 B7 53 STA $53
0819 12 07 BSET 1,$07 EEPROM_CTL . E1LAT=1 ;
8 IB 14 07 BSET 2, $07 EEPROM_CTL . E1ERA=1 ;
081D B7 58 STA $58 *x=0;
081F 9F TXA
0820 B7 57 STA $57
0822 4F CLRA
0823 AE D7 LDX #$D7
0825 BF 56 STX $56
0827 AE 81 LDX #$81
0829 BF 59 STX $59
082B 5F CLRX
082C BD 56 JSR $56
082E 10 07 BSET 0,$07 EEPROM_CTL . E1PGM=1 ;
0830 5F CLRX delay (PROG_TIME) ;
0831 A6 0A LDA #$0A
0833 CD 00 00 JSR $****
0836 13 07 BCLR 1,$07 EEPROM_CTL . E1LAT=0 ;
0838 81 RTS }
This code sequence requires 36 bytes, plus 4 bytes of uncommit-
ted RAM space, to accomplish what required 22 bytes in the earlier
example of the same operation.
You must not avoid the use of pointers because of this one ex-
ample. There are cases when proper pointer usage will provide the
best code that you can generate. When writing code for
microcontrollers, use many relatively small functions that you can
166 Chapter 4 Small 8-Bit Systems
compile individually and examine if they create outlandish code. Once
these small functions are all debugged, you can integrate them into
your program as either inline code or as function calls. Hence, the
fundamental rule of writing good high-level code for a
microcontroller: the production of good C code for a microcontroller
is a joint effort between the programmer and the compiler writer.
Erasure of EEPROM causes component wear, and most EEPROMs
will wear out after a large number of erasures. The nature of the deg-
radation is that the component refuses to erase after many erasures.
There has been no evidence that data retention is affected by repeated
erasures. The number of erasures that can cause problems is tempera-
ture sensitive. Most of these devices are rated for 10,000 write/erase
cycles at the maximum rated temperature for the part. At room tem-
perature, the number of write/erase cycles without damage can grow
to several hundred thousand. In light of these facts, it is important that
programs use care in rewriting the contents of EEPROM.
EXERCISES
l.The EEPROM in microcontrollers erase to the 1 state. Write a
function that checks to determine if an erasure cycle erases the
contents of a given location in memory.
2. Write a function that compares data to be programmed into
EEPROM and determine if it is necessary to erase a byte contain-
ing data before it is reprogrammed. This approach will extend the
life of the EEPROM.
Timers
The systems of timers placed on microcontrollers are among the
most creative engineering efforts most people will ever see. The func-
tions of these timers cover literally dozens of different operations.
This set of peripheral components mainly relieve the computer of
much work associated with execution of the peripheral function. We
will start with the simplest timer and outline different timer capabili-
ties in increasing complexity. The most basic timer in the M68HC05
family is called the 15-bit timer, and probably the most advanced
timer system is the 16-bit timer. These different timer systems are
literally unrelated in the features they offer. Another timer feature
Timers 167
offered in some microcontrollers is the computer operating prop-
erly (COP) timer. Each of the systems will be examined in some
detail in the following paragraphs.
Multifunction Timer— 15-Bit Timer
A timer of this nature can be found on the smallest of the
M68HC05 components, such as the M68HC05 Jl or the M68HC05P8,
and it is not found on the M68HC05Bx devices discussed previously.
This timer consists of an 8-bit ripple counter followed by an addi-
tional 7-bit counter. This counter chain is driven by a signal that is at
one-fourth the internal clock frequency of the microcontroller. The
internal clock frequency in turn is half the crystal frequency. This
portion of the counter is completely uncontrolled. The program can,
however, read the value of this counter at any time. A block diagram
of this type of timer is shown in Figure 4- 1 .
Internal
Processor
Clock
(XTAL -=-2)
Least Significant Eight Bits of 15 Stage Ripple Counter f
Most Significant Seven Bits of 15 Stage Ripple Counter
Service (Clear)
COP Watchdog
COP Timeout-Generate
Internal MCU Reset
Figure 4-1 15-bit Timer Block Diagram
168 Chapter 4 Small 8-Bit Systems
STATUS REG
0x08
Bit 7
Bit 6
Bit 5
Bit 4
Bit 3
Bit 2
Bit 1
BitO
TOF
RTIF
TOFE
RTIE
RT1
RTO
RTO
BitO
RT1
Bitl
RTIE
Bit 4
TOFE
Bit 5
Real Time Interrupt Select Rates. These bits con-
trol the rate at which the real-time interrupt and
the COP reset time will occur. Note that these two
bits are delivered to the RTI Rate Select register in
the diagram. Table 4-3 shows the various RTI and
COP rates that can be obtained for different values
of RTI and RTO. Reset sets both bits so that the
periodic rates will be the slowest possible when
the part comes out of reset. It is expected that these
bits will not be changed If these bits are altered,
the first cycle following the change will be wrong.
Real Time Interrupt Enable. When this bit is set, a
CPU interrupt request is generated whenever the
RTIF is set.
Timer Overflow Enable. When this bit is set, a
CPU interrupt request is generated whenever the
TOF is set.
Real Time Interrupt Flag. This bit is set whenever the
output from the selected divider stages is set. If the
RTIE is also set, setting of this bit will request a CPU
interrupt. This bit is cleared by reset or by writing a
zero to it. It is not possible to write a 1 to this bit.
Timer Overflow Flag. This bit is set whenever the
8-bit ripple counter overflows from a Oxff to a 0x00.
This bit is cleared by reset or by writing a zero to
it. It is not possible to write a 1 to this bit. The
timing that results from selection of values for RTI
and RTO are shown in Table 4-1. This table also
shows the real time interrupt rate for different val-
ues of the real time interrupt select rate bits.
The timer counter register at address 0x09 contains the value
found in the 8-bit ripple counter. The contents of this counter can be
read at any time, but it cannot be written to. When the part comes out
of reset, the timer counter register contains a zero. 4096 clock cycles
will follow, during which the TCR will count at the minimum rate.
RTIF
Bit 6
TOF
Bit 7
RTI Rate
Minimum
COP Reset
8.2 ms
57.3 ms
16.4 ms
114.7 ms
32.8 ms
229.4 ms
65.5 ms
458.8 ms
Timers 169
At the close of this period, the initialization of the part is complete,
and the TCR is again reset to zero prior to execution of the code
identified by the reset vector. Whenever the RESET line is asserted,
the TOF will be loaded with zeros.
Any program can read the TCR, so it is possible to generate asyn-
chronous time events faster than the TOF or the RTI F would indicate.
When the processor enters the WAIT mode after execution of a
WAIT instruction, the CPU clock halts, but the timer clock continues
to execute. If the interrupts are not masked, a timer interrupt, an exter-
nal interrupt or a reset will cause the device to exit the WAIT mode.
Table 4-3: RTI And COP Rates for F xta| = 4.0 MHz
RTI RTO
1
1
1 1
If a STOP instruction is executed, the timer clock is halted along
with the CPU clock. The STOP mode is exited when an external
interrupt occurs or the RESET line is asserted. In this case, the part
performs as described above.
Most microcontrollers are placed in operation with no operator to
intervene in the event of a problem. A COP timer will provide one
means of recovering if the operation of the microcontroller gets lost.
"Gets lost"? The situation that can cause a microcontroller to get lost
is usually some type of voltage spike or glitch in the power supply
operation. The program counter usually ends up with a value outside
of the program, and no one knows what will happen. The COP is sim-
ply a timer that counts for a specified amount of time. If the COP timer
has not been reset before the specified time elapse, the COP timer
overflow causes an internal reset of the microcontroller. If the cause of
the problem is a drop in power or other error, in most instances forcing
a reset will bring the microcontroller back into normal operation.
The COP control register is located at address 0x7f0 in the
M68HC05J1. To service the COP from the program, the program
must merely write a zero to bit of this address to reset the COP
portion of the timer system.
170 Chapter 4 Small 8-Bit Systems
Good programming practice dictates that microcontroller- specific
information be placed in a header file like that shown here:
#pragma portrw PORTA @ 0x0 0;
#pragma portrw PORTB @ 0x01;
#pragma portrw DDRA @ 0x04 ;
#pragma portrw DDRB @ 0x05;
#pragma portrw TCST @ 0x08;
#pragma portrw TCR @ 0x09;
#pragma portrw COPSVS @ Ox7fO;
#pragma vector TIMER @ 0x07f8;
#pragma vector IRQ @ 0x07fa;
#pragma vector SWI @ 0x07fc ;
#pragma vector RESET @ 0x07fe;
#pragma has STOP ;
#pragma has WAIT ;
#pragma has MUL ;
#pragma memory RAMPAGE0 [64] @ OxcO;
#pragma memory ROMPROG [1024] @ 0x3 00;
#define RT0 /* TSCR Bits */
#define RT1 1
#define RTIE 4
#define TOFE 5
#define RTIF 6
#define TOF 7
Listing 4-3: Header File For The M68HC05J1
The #pragma and several important #def ine commands are
microcontroller specific. Therefore, to change the program from one
microcontroller to another, the programmer need only change the
microcontroller header file. Listing 4-3 is a header file for the
M68HC05JJ1 controller. The first six entries identify the locations
of the I/O ports, the data direction registers, timer status/control reg-
ister, and the timer counter Register. The next five entries specify the
COP service address and the vector locations for this part. Note that
the names associated with the vector locations all start with a double
Timers 171
underscore. These names are also listed in all upper-case letters. These
entries are the names of the various interrupt service routines. Since
C is case sensitive, the names of the functions to be used as interrupt
service routines must have the same form.
The three #pragma entries identified as has notifies the com-
piler that the microcontroller has the STOP, WAIT, and MUL
instructions. The next pair of entries defines the memory map for
this microcontroller. Finally, the next six entries are #de fines that
identify the bits in the TCSR. Therefore, mnemonic representations
of all registers and bits can be used in the C program.
Several header files for the M68HC05 family are found on the
CD-ROM. The conventions in these files are to use bit names and
register names that are identical to those used in the technical data
books that describe the devices. Therefore, the programmer can safely
use register names and bit names found in the books without having to
look up the values in the header files. These files include commands to
prevent listing of these files in the compiler listing output files.
Listed below is a simple program that shows the use of the 15-bit
timer in the M68HC05J1. This program is not aimed at doing more
than showing the use of the timer operation. The system will create
an inaccurate clock in which the time in hours, minutes, and seconds
will be recorded in memory, but no provision to display these values
or even set the values will be considered at this time.
Most clocking operations should be interrupt driven. If a periodic
interrupt can be generated, the operation of the clock will be transpar-
ent to any other operations being conducted in the microcontroller.
#include u hc05jl.h"
enum { FALSE , TRUE ) ;
enum {OFF, ON};
#define FOREVER while (TRUE)
#define MAX_SECONDS 59
#define MAX_MINUTES MAX_SECONDS
#define MAX_HOURS 12
# define MAX_COUNT 121
/* define the global variables */
int hrs,mts,sec;
172 Chapter 4 Small 8-Bit Systems
int count ;
main (void)
{
count=0; /* start count at zero */
TCST.RT0=OFF; /* 57.3 ms cop timer */
/* 8.192 ms RTI */
/* Turn on the RTI */
TCST.RTIF=OFF; /* Reset interrupt */
TCST.TOF=ON; /* flags */
CLI(); /* turn on interrupt */
TCST.RT1=0FF
TCST.RTIE=ON
FOREVER
{
if (seoMAXjSECONDS) /* do clock things */
{
sec=0 ;
if (++mts>MAX_MINUTES)
{
mts=0 ;
if (++hrs>MAX_HOURS)
hrs=l ;
}
}
/* here is where any applications program should
be placed. */
}
}
void TIMER (void) /* routine executed every
RTI (8.192 ms) */
{
TCST.RTIF=OFF; /* reset interrupt flag */
if (++ count >MAX_COUNT)
{
sec++; /* increment seconds */
count=0;/* reset the count each second */
}
}
Listing 4-4: ATime-of-Day Program Based On The 15-bit Timer.
Timers 173
A few words about a good programming practice: numbers in a
program with no defined meaning are called "magic numbers." You
should avoid magic numbers, because a number with no meaning
makes life difficult for the program maintenance people. In the pro-
gram above, several numbers are needed. These numbers are given a
name by either enum statements or #def ine statements. Then, in
the program, you can see every instance of the use of the number
does have a meaning relative to the program. Another advantage to
avoiding magic numbers is not too evident in the above program, but
it is truly an important advantage. If the program is long and com-
plicated, these numbers might be used many times. Then if a
maintenance situation requires the change of the value of a number
in the program, it can be changed in one place and a recompilation
will correct every instance of the number in the program.
Several global variables are used in this program: hrs, mts,
sec, and count. These variables are all changed in the main pro-
gram, but they are available in any other part of the program if needed.
For example, the count variable is initialized to zero in the main
program and incremented and reset in the interrupt service routine.
One point should be noted in this program: the main program has all
of the time calculations based on the current contents of sec. The
variable sec is incremented each second in the interrupt service rou-
tine. Some programmers would put the complete time service within
the interrupt service routine. That is, they would reset sec when it
reaches 60, increment mts, and so forth within the interrupt service
routine. Either approach will provide the same result, and each takes
the same total computer time. It is, however, better to keep the time
that the program is controlled by the interrupt service routine at a
minimum. Interrupts are disabled when a program is in an interrupt
service routine. If there are several competing interrupts, execution
of an interrupt service routine prevents other interrupts from being
processed. Quickest response to all interrupts will be obtained if all
of the interrupt service routines are as short as possible.
Program Organization
A compiled version of this program is listed below. Note that the
compiler listing routine prints out the contents of the include file.
The memory map #pragmas puts the RAM in page beginning at
174 Chapter 4 Small 8-Bit Systems
OxcO and the program memory starts at 0x300. Note that the com-
piler places the global variables in OxcO through 0xc3, and the
executable program begins at 0x300 as one would expect.
The first several instructions clear the count location and set or
reset proper bits in the TSCR. The instruction CLI clears the inter-
rupt bit in the status register of the microcontroller. When this bit is
cleared, interrupts will detected and processed.
The beginning of the loop defined by the macro command FOR-
EVER is at address 0x30d. This macro causes no code at the beginning
of the loop. At the end of the loop, address 0x32b, there is an instruc-
tion BRA 0x3 Od that causes control of the program to start at the
beginning of the loop. That is the total code created by the macro
FOREVER. Within this loop, the code created by the compiler is
straightforward and not very different from code that would be cre-
ated by a competent assembly language programmer.
#include "hc05jl.h"
0000 #pragma portrw PORTA @ 0x00;
01 #pragma portrw PORTB @ 0x01;
03 #pragma portr PORTD @ 0x03
04 #pragma portrw DDRA @ 0x04
0005 #pragma portrw DDRB @ 0x05
08 #pragma portrw TCST @ 0x0 8
09 #pragma portrw TCNT @ 0x0 9
07F0 #pragma portrw COPSVS @ 0x7f0;
07F8 #pragma vector TIMER @ 0x07f8;
07FA #pragma vector IRQ @ 0x07fa;
07FC #pragma vector SWI @ 0x07fc ;
07FE #pragma vector RESET @ 0x07fe;
#pragma has STOP ;
#pragma has WAIT ;
#pragma has MUL ;
00C0 0040 #pragma memory RAMPAGE [64] @ OxcO;
0300 0400 #pragma memory ROMPROG [1024] @ 0x3 00;
Timers 175
0000 #define RTO
0001 #define RT1 1
0004 #define RTIE 4
0005 #define TOFE 5
0006 #define RTIF 6
0007 #define TOF 7
01 #define TRUE 1
0000 #define FALSE
0001 #define FOREVER while (TRUE)
00C0 00C1 00C2 int hrs,mts,sec;
0C3 int count;
main (void)
{
0300 3F C3 CLR $C3 count=0;
0302 11 08 BCLR 0,$08 TCST.RT0=0;
0304 13 08 BCLR 1,$08 TCST.RT1=0;
0306 18 08 BSET 4, $08 TCST.RTIE=1;
0308 ID 08 BCLR 6, $08 TCST.RTIF=0;
030A IF 08 BCLR 7, $08 TCST.TOF=0;
030C 9A CLI CLI () ;
FOREVER
{
030D B6 C2 LDA $C2 if (sec = = 60)
030F Al 3C CMP #$3C
0311 25 18 BCS $032B
{
0313 3F C2 CLR $C2 sec=0;
0315 3C CI INC $C1 if (++mts==60)
0317 B6 CI LDA $C1
0319 Al 3C CMP #$3C
031B 26 0E BNE $032B
{
031D 3F CI CLR $C1 mts=0;
031F 3C CO INC $C0 if (++hrs==13 )
0321 B6 CO LDA $C0
0323 Al 0D CMP #$0D
176 Chapter 4 Small 8-Bit Systems
0325 26 04 BNE $032B
0327 A6 01 LDA #$01 hrs=l;
0329 B7 CO STA $C0 }
}
032B 20 E0 BRA $030D }
032D 81 RTS }
void TIMER (void)
07F8 03 2E {
032E ID 08 BCLR 6, $08 TCST . RTIF=0 ;
0330 3C C3 INC $C3 if (++count==122 )
0332 B6 C3 LDA $C3
0334 Al 7A CMP #$7A
0336 26 04 BNE $033C
{
Timers
0338 3C C2 INC $C2 sec++;
033A 3F C3 CLR $C3 count=0;
}
033C 80 RTI }
07FE 03 00
At the beginning of the interrupt service routine there is an entry
7 f 8 , which has a value of 3 2 e, in the address column. This entry
places the address of the timer interrupt service routine 0x0 3 2e
into the timer vector 0x0 7 f 8. The code generated in the interrupt
service routine is straightforward and little different from what one
would expect an assembly language programmer to do. Note, how-
ever, that the return at the end of the interrupt service routine is an
RTI instruction. This is the instruction that causes the microcontroller
to restore the processor status to the state that existed when the inter-
rupt occurred. The normal return from subroutine RTS does not restore
the processor state. An RTI must be used to return from interrupt
service routines, and any function identified with a vector pragma
will be assumed to be an interrupt service routine by the compiler.
It was noted earlier that this timer routine is inaccurate. It is inac-
curate only because 122 periods of 8.192 milliseconds each total
0.999424 seconds. This seemingly small error will cause big problems
Timers 177
if one wants a real clock because the error amounts to 2. 1 seconds per
hour. One way this error could be corrected is to adjust the crystal
frequency of the microcontroller. Suppose we would use a frequency
of 3.996354 MHz instead of 4.0 MHz. This number is derived by
122*2 A 15/f=l
which yields the above value for f . The 122 periods of 8.196721 milli-
seconds, which is the real-time interrupt time for this frequency, is exactly
1 second. Another approach involves making small corrections to the
time periodically so that on the average the time is correct. An example
of an interrupt service routine that makes these corrections is as follows:
void TIMER (void) /^routine executed every RTI
(8.192 ms) */
{
static int corrl , corr2 , corr3 ;
TCST.RTIF=0; /* flags */
if (++count==122) /* increment seconds */
{ /* To correct for 8.192 */
sec++; /* ms per tick. Run 122*/
if (++corrl==14) /* ticks per second for */
{ /* 13 seconds, and 123 */
corrl=0; /* for the 14th second */
if (++corr2==80) /* With this algorithm */
{ /* there are 14.000128 */
corr2=0; /* actual seconds per */
if (++corr3==4) /* 14 indicated. Then */
{ /* run 79 of these */
count =1; /* cycles followed by */
corr3==0; /* one cycle of 14 */
} /* seconds with 122 ticks */
else /* per second. The */
count=0; /* elapsed time for this*/
} /* cycle is 1120.002048 */
else /* seconds for and */
count=(-l); /* the count is 1120 */
} /* seconds. Repeat this */
else /* cycle 4 times and on */
178 Chapter 4 Small 8-Bit Systems
count=0; /* the last cycle drop */
/* one tick makes the */
/* indicate and elapsed */
/* time exactly 4480 sec.*/
The three static variables corrl, corr2, and corr3 are used to
keep track of the number of times the several different loops in the
algorithm are executed. C will always initialize these variables to zero
and then their value will be retained from call to call of the function.
16-bit Timers
The multifunction timer discussed in the previous section pro-
vides for implementation of relatively simple timing functions. Let's
assume that the microcontroller clock frequency is 4.0 MHz. The
fastest interrupt time with this system is 0.512 milliseconds, and the
granularity of the interrupt times is in large, power-of-two blocks for
the RTI system. Also, the relation of the interrupt times to unity is
not "clean"; complicated algorithms or special frequency crystals
are needed to get the device to respond accurately in seconds.
Often microcontroller applications must provide more than one
time function. The 15-bit timer is set up to provide only one time
base. Of course, a programmer can program the timer to control many
different functions and at many different times. The limits on the
functions and times are difficult to determine. It is clear that the fast-
est practical time base in the 15-bit timer is 0.512 milliseconds. To
obtain any finer time resolution, the programmer would have to com-
pare the TCR bits to a specified value on a cycle-by-cycle basis. This
type of program completely consumes the microcontroller and leaves
no processing time for other functions during the execution of the
timing program. If the time base must be other than some multiple of
0.512 milliseconds and not one of the standard RTI times, the pro-
cessor can probably service only one time function. If the required
times can fall on the above values, the processor can execute several
time-based functions limited by the total time required to execute the
functions and the microcomputer interrupt latency time.
The 16-bit timer addresses these problems. A block diagram of this
type of timer is shown in Figure 4-2. This style of timer contains an
internal 16-bit counter that is clocked at some fraction of the
Timers 179
MC68HC05B6 Internal Bus
Timer
Status
Register
$13
TCMP1
Pin
Input Output Overflow
Capture Compare Interrupt
Interrupt Interrupt $1 FF4.5
S1FF8.9 $1FF6.7
Figure 4-2:
16-Bit Timer For The M68HC05B6
180 Chapter 4 Small 8-Bit Systems
microcontroller crystal frequency. An input capture operation de-
tects the occurrence of an input and transfers the contents of the
16-bit counter into the input capture register. This transfer will al-
ways set a flag, and it can cause a CPU interrupt if desired. With an
input capture system, precise measurement of time interval is pos-
sible. Details such as phase between two waveforms can be determined
or slight differences in frequencies between several signals can be
detected. The input capture provides far more accurate time mea-
surement than can be obtained with either synchronous polling or
asynchronous interrupt time measurements. There is a tiny inherent
delay between the occurrence of the input and the setting of the input
capture register. Such a measurement made by polling an input would
require that the computer have a free running counter available to
interrogate when the input is detected. Then, the computer would
have to be assigned totally to the job of watching the input for the
impending transition. When the transition is detected, the value of
the counter would have to be read to determine the time of the tran-
sition. Of course, this sequence of operations would require several
computer clock cycles per test, and also several cycles would be re-
quired to read the counter. Therefore, the accuracy of the time
measurement would be compromised by these necessary time delays.
An asynchronous interrupt method to determine the time inter-
val is better than a polled method, but even this method has built-in
errors that make it an impractical means to measure time intervals
accurately. The input capture register input system resolves most of
the problems associated with accurate measurement of time inter-
vals with a microcomputer.
Another type of timing problem exists. Suppose that the time
that an event is to occur has been calculated. If the time of occur-
rence is to be accurate, we have a situation like that discussed above.
The processor will have to spend all of its time watching the clock to
determine when the correct time has arrived. Any time spent on other
tasks during this measurement interval will be a latency during which
the processor cannot determine if the specified time has arrived. In
this case, the accuracy of the event time will be degraded by the time
spent on other tasks.
The 16-bit timer avoids this type of problem nicely. An output
compare system is used. The time of occurrence is calculated rela-
Timers 181
tive to the internal 16-bit counter. This value is placed into an output
compare register. The content of the counter is compared automati-
cally by the microcontroller to the value in the output compare register
at each count of the counter. When the two values are equal, a flag is
set, an output occurs, and if desired, the CPU is interrupted. The
output compare system can be used to generate waveforms, to con-
trol phases between different waveforms, to control events based on
calculated times.
Different microcontrollers will have differing numbers of input
capture and output compare registers. In the discussions that follow,
details of a single input capture and output compare register will be
discussed. It is assumed that these registers are part of an
M68HC05B6, so there are two input captures and two output com-
pares onthe microcontroller. For details on access to the second
register set, refer to the appropriate data manual. Later we will see
microcontrollers that have many more input capture and output com-
pare systems (up to 16 on one microcontroller).
Timer Control Register
The timer control register (TCR) is located at the address 0x12.
This read/write register controls the operation of the 16-bit timer
system. Shown below is a diagram of this register, and a listing of the
functions of the various register bits.
TCR
0x12
Bit 7
Bit 6
Bit 5
Bit 4
Bit 3
Bit 2
Bit 1
BitO
ICIE
OCIE
TOIE
FOLV1
FOLV2
CLVL2
IEDG1
OLVI1
OLVL1 Bit Output Level 1. The contents of this bit will be
copied to the output level latch the next time an
output compare occurs. This result will appear at
TCMP1. This bit and the output level latch are
cleared when the part is reset.
IEDG1 Bit 1 Input Edge 1. This bit determines the transition
direction that will cause an input to occur on In-
put Capture 1 :
IEDG1 = Falling Edge
IEDG1 = 1 Rising Edge
The contents of this bit are undetermined and un-
affected at reset.
182 Chapter 4 Small 8-Bit Systems
0LVL2 Bit 2
F0LV1 Bit 3
FOLV2 Bit 4
TOIE
Bit 5
OCIE
Bit 6
ICIE
Bit 7
Output Level 2. The contents of this bit will be
copied to the output level latch the next time an
output compare occurs. This result will appear at
TCMP2. This bit and the output level latch are
cleared when the part is reset.
Forced Output Compare 1. This bit always reads
zero. A one written to this position will force the
OLVL1 bit to be copied to the output level latch.
This result will appear at TCMP1. A forced out-
put compare does not affect the OCF1 bit in the
timer status register. This bit is cleared at reset.
Forced Output Compare 2. This bit always reads
zero. A one written to this position will force the
OLVL2 bit to be copied to the output level latch.
This result will appear at TCMP2. A forced out-
put compare does not affect the OCF2 bit in the
timer status register. This bit is cleared at reset.
Timer Overflow Interrupt Enable. If the TOIE is set,
the timer overflow interrupt is enabled and an inter-
rupt will occur when the TOF flag is set in the timer
status register. This bit is cleared at reset and the in-
terrupt is inhibited.
Output Compare Interrupt Enable. If the OCIE bit
is set, the output compare interrupt is enabled, and
an interrupt will occur whenever either the OCF1
or the OCF2 is set in the timer status register. This
bit is cleared at reset and the reset is inhibited.
Input Capture Interrupt Enable. If the ICIE bit is
set, the input compare interrupt is enabled and an
interrupt will occur whenever either the ICF1 or
the ICF2 is set in the timer status register. This
bit is cleared at reset and the reset is inhibited.
Timer Status Register
This register-
-TSR — is an 8-bit register. The most significant 5
bits of this register contain read only status information. These bits
describe the condition of the 16-bit timer system. Their functions are
outlined below:
Timers 183
TSR
0x13
Bit 7
Bit 6
Bit 5
Bit 4
Bit 3
Bit 2
Bit 1
BitO
ICF1
OCF1
TOV
ICF2
0CF2
0CF2 Bit 3 Output Compare Flag 2. This bit is set when the
content of the free-running counter matches the
contents of output compare register 2. OCF2 is
cleared by accessing the TSR (specifically the
OCF2) followed by an access to the low byte of
the output compare register 2, Oxlf. The output
compare flag 2 is undetermined at power on and
is unaffected by reset.
ICF2 Bit 4 Input Capture Flag. This bit is set when a nega-
tive edge is sensed at TCAP2. It is cleared by an
access of the timer status register followed by an
access of the low byte of the input capture regis-
ter, Ox Id. The input capture 2 flag is undetermined
at power on and is unaffected by reset.
TOF Bit 5 Timer Overflow Bit. This bit is set by a transition
of the free-running counter from a Oxffff to a
0x0000. It is cleared by accessing the TSR with the
TOF set followed by an access of the free-running
counter low byte, 0x19. The TOF bit is undeter-
mined at power on and is unaffected by reset.
OCF1 Bit 6 Output Compare Flag 1. This bit is set when the
content of the free-running counter matches the
contents of output compare register 1. OCF2 is
cleared by accessing the TSR (specifically the
OCF1) followed by an access to the low byte of the
output compare register 1, 0x17. The output com-
pare flag 1 is undetermined at power on and is
unaffected by reset.
ICF1 Bit 7 Input Capture Flag 1. This bit is set when the
proper edge is sensed at TCAP1. The edge is se-
lected by the IEDG1 bit in the TCR. It is cleared
by an access of the timer status register followed
by an access of the low byte of the input capture
register, 0x15. The input capture 1 flag is unde-
termined at power on and is unaffected by reset.
184 Chapter 4 Small 8-Bit Systems
To clear bits in the TSR, the program must first access the TSR
followed by an access of the LSB of the register associated with the bit
that must be reset in the TSR. This sequence can lead to problems in
dealing with the counter register. Suppose you are attempting to mea-
sure an elapsed time and are reading the counter register at random
times and you also will read the TSR to service timer requirements. It
is possible in these circumstances to accidentally reset the TOF bit
when it is undesired. To avoid this problem, an alternate counter regis-
ter has been designed into the M68HC05 devices. The alternate register
always contains the same values as the prime register, but the TOF bit
in the TSR is not reset when the alternate register is read.
Counter Register
The counter register is found in the memory locations 0x18 and
0x19. The least significant byte of the counter is in 0x19. An alter-
nate counter register is found in addresses Ox la and Ox lb with Ox lb
being the least significant byte of this register. These registers are
clocked at the same time and are incremented from low values to
higher values. The counters are clocked at one-fourth of the internal
processor clock, which in turn is one-half the oscillator frequency.
The clocking frequency is one-eighth the crystal frequency, and the
clocking period is 2 microseconds when the crystal frequency is 4
MHz. These ratios are not adjustable in the M68HC05B6.
The free-running counter values can be read at any time. A read
sequence that reads only the least significant byte will receive the
count value at the time of the read. If the most significant byte of
either counter is read, the count value will be received and the con-
tents of the least significant byte will be transferred to a buffer. This
value will remain in the buffer until the program reads the contents
of the least significant byte of the register. The value received for this
read is the buffered value saved when the most significant byte was
read. The most significant byte, MSB, can be read several times prior
to reading the least significant byte, LSB, and the contents of the
buffer will remain unchanged. After the MSB has been read and the
LSB has been buffered, the free-running counter continues to be
incremented at its normal rate. If the MSB/LSB read sequence is
started, it is necessary to read the LSB to complete the sequence.
The counter is 16 bits, and when the register overflows from
Timers 185
Oxffff to 0x0000, the timer overflow (TOF) bit is set. This event can
cause an interrupt if the TO IE bit is set. Since the register is clocked
at 2 microseconds, the interval between TOF is 0.131072 seconds.
Input Capture Registers
There are two input capture registers called ICR1 and ICR2.
ICR1 is found at addresses 0x14 and 0x15, and ICR2 is located at
addresses 0x1c and Ox Id. The lower address always contains the MSB
of a 16-bit number. With the exception of the edge detection system
discussed in the TCR section, these two registers operate the same.
ICR1 can be set to respond to either a rising edge or a falling edge
on the timer compare input pin TCAP1. If IEDG1 is 0, ICR1 re-
sponds to a falling edge on TCAP1. Otherwise, if IEDG1 is 1, ICR1
responds to a rising edge on TCAP1. ICR2 responds only to a fall-
ing edge on TCAP2 . An interrupt will also accompany an input capture
if the corresponding I CI E bit is set in the TCR.
The contents of the free-running counter are transferred to the
input capture registers each clock cycle. Therefore, the registers con-
tain a value that corresponds to the most recent input capture. After a
read of the most significant byte of the input capture register, the
transfer of new data to the least significant byte of the input capture
register is inhibited until this byte is read. At no time during this
sequence is the counter register inhibited.
Output Compare Registers
There are two output compare registers. OCR1 is found at ad-
dress locations 0x16 and 0x17 while OCR2 is located at Oxle and
Oxlf. Again the lower addresses contain the MSB of these 16-bit
numbers. These registers may be read or written at any time regard-
less of the timer hardware. If the output compare functions are not
utilized, these four bytes can be used for data storage. Their contents
are not altered at reset. There is only one output compare interrupt
bit that is used for both output compares in the system.
The contents of the output compare registers are compared with
the contents of the counter register each cycle of the counter register.
If a match is found with either output compare register, the corre-
sponding output compare flag — OCF1 or OCF2 — bit is set. Also,
the value of proper output level bit — OLVL1 or OLVL2 — is trans-
186 Chapter 4 Small 8-Bit Systems
ferred to the proper output pin, TCMP1 or TCMP2. If the OCIE bit is
set in the timer control register, an interrupt will accompany the out-
put compare.
There are times when it is desirable to force an output compare
from a program. The FOLV1 and FOLV2 bits can be used for this
purpose. These bits will always read 0, but writing a 1 to these bits in
the TCR will cause transfer of the corresponding OLVL1 or OLVL2 bit
to the specified output compare bit, either TCMP1 or TCMP2. This
output does not affect the compare flags, so no interrupt is generated.
Programming the 16-bit Timer
We will examine several different uses of the 16-bit timer system
in this section. The first is merely a repeat of the simple timer pro-
grammed in the section on the 15-bit system. Here we merely want
to keep track of time, hours, minutes, and seconds in memory. No
provisions are made yet for reading the time values or to change the
values; these problems will be discussed later.
A listing of this program is shown below. In this case, the header
file for the M68HC05B6 is used. A listing of this file is found on the
CD-ROM. This program will make use of an output compare to gen-
erate periodic interrupts to the microcontroller. We will use output
compare register 1. It will be set up so that when the first output
compare interrupt occurs, the contents of the output compare regis-
ter will be incremented by 500. Since the clocking time of the counter
register is 2 microseconds, 500 2-microsecond periods will allow an
output compare every 1 millisecond. This occurrence will be treated
in the interrupt service routine.
#include "hc05b6.h"
int hrs, mts, sec; /* global variables */
long count=1000;
struct bothbytes /* 16 bit int structure */
{
int hi ;
int lo;
};
Timers 187
union both /* and union */
{
long 1 ;
struct bothbytes b;
};
union both time_count;
registera ac;
main ( )
{
TCR.0CIE=1; /* enable output compare interrupt */
CLI(); /* enable all interrupts */
FOREVER
{
if (sec>59) /* do clock things each minute */
{
sec=0 ;
if ( + +mts>59)
{
mts=0 ;
if (++hrs>12)
hrs=l ;
}
}
WAIT ( ) ;
}
}
void TIMER_OC (void) /* time interrupt service
routine */
{
if (TSR.0CF2==1) /* is this interrupt due to 0C2?*/
{
ac=0CL02; /* Yes. read 0CL02 to disable */
return; /* the interrupt and exit */
} /* the routine */
188 Chapter 4 Small 8-Bit Systems
/* the program gets here every millisecond */
time_count . b .hi = 0CHI1;
ac = TSR; /* Arm 0CF1 bit clear */
time_count .b.lo = 0CL01; /* Clear 0CF1 bit */
time_count.l += 500; /* 500 counts per ms */
OCHI1 = time_count . b .hi ;
OCLOl = time_count .b . lo;
if ( --count = = 0)
return ;
else
{
sec++; /* here every second */
count=1000 ; /* reset count to 1 second */
Listing 4-4: Timer Using Output Compares
The listing shows that microcontroller executing programs are
broken into three distinct sections. The first section is referred to as
the initialization section. In this case, the initialization section is the
first two lines following the main ( ) invocation. In the initialization
section, the code executed sets up the operation of the microcontroller.
Generally, this code is executed only once. Therefore, initialization
of volatile memory values, setting up of interrupts, establishment of
I/O ports and data direction registers are all completed in the initial-
ization of the program. Unless there is a pressing reason, the main
system interrupts should not be enabled during initialization.
The second section is the applications section. The applications
section is usually a loop that contains all of the code to be handled
routinely by the microcontroller. All input or output operations should
take place within the applications section.
The third section of the program is the collection of interrupt service
routines (asynchronous service section). These routines are called when
appropriate interrupts are generated. In general, interrupt service rou-
tines should be short and do as little as possible to service the specified
interrupt. When an interrupt is serviced, the status register of the
microcontroller is saved and the system interrupt is disabled. Therefore,
unless the programmer takes special care to re-enable the interrupts, no
Timers 189
other interrupt can be serviced while the microcontroller is executing an
interrupt service routine. This operation does not lead to missed inter-
rupts, but it can cause an inordinate delay in service of an interrupt.
In some cases, it is possible that data might be lost if an interrupt
is not handled expeditiously. For example, a high-speed serial port
might notify the microcontroller that its receiving data buffer is full
by an interrupt when the interrupt is disabled. In such a case, if the
interrupt service routine being executed is not completed before the
next serial data are received, the data in the buffer will be lost.
Sometimes it is necessary to pass data between the applications
portion of the program and the interrupt service routine. These data
can be stored in global memory, and both routines can access them.
Here is a case where you must examine the assembly code generated
by the compiler to make certain that no problems will be generated
in passing of data between these routines. Problems can be created
by passing information this way, but they can be avoided if the pro-
gram rigorously avoids loading data that is changed in an interrupt
service routine into a register in the application section of the pro-
gram. We will see instances of this problem later.
If you go back to Listing 4-4, you'll see that the program organi-
zation discussed above is also used. This organization will be found
for every program in this book. We will refer to the initialization, the
applications, and the asynchronous service sections of the program.
This arrangement works quite well, and is reliable. There should be
a strong justification if alternate program forms are to be used.
When programming the 15-bit timer, we found that asking a com-
piler designed specifically for an 8-bit machine to work in 16-bit
quantities often created unwieldy code. A programmer, however, does
not always have the freedom to work with 8-bit quantities only. All of
the time registers in this system are 16 bits wide, and the time values
contained in these registers must be processed. These registers are al-
ways located in two adjacent 8-bit memory locations. One method for
handling the 8/16-bit dichotomy is to use a union. The code sequence
struct bothbytes
{
int hi ;
int lo;
};
190 Chapter 4 Small 8-Bit Systems
creates a structure that contains two bytes. This structure is com-
bined with a type long in the union below. Remember that a union
provides space to hold its largest member and the different members
of the union will occupy the same memory space. Therefore, the
memory space to store the long 1 is the exact same memory to
store the structure b . b . hi will occupy the most significant byte of
1, and b . lo will occupy the least significant byte of 1. It is now easy
to deal with the bytes of the 16-bit quantity when moving the data
around, and equally easy to invoke 16-bit arithmetic operations on
the long combination of the two bytes.
union both
{
long 1 ;
struct bothbytes b;
};
The statement
union both time__count;
declares that t ime_count is a union of the type both. In the in-
terrupt service routine, the members of time_count are handled
with little difficulty, as shown below.
The type registera defined as ac above is unique to the
M68HC05 compiler. This type specifies that the variable ac will be
stored in the accumulator. There is also a regis terx type for the
index register.
In the main program, the output compare interrupt is enabled
and the system interrupt is enabled with the CLI ( ) instruction in
the initialization section. The program then enters an endless loop in
which the clock is serviced every time sec becomes 60. The param-
eter sec will be changed by the interrupt service routine every second
so that the system should be a satisfactory clock. This loop is the
initialization section.
The last instruction in the FOREVER loop is a WAIT ( ) instruc-
tion. This instruction places the processor into the wait mode. In this
mode, processor operations are halted and the microcontroller op-
eration is configured to reduce energy consumption. The operation
of the internal timers proceed as usual. The part is removed from the
wait mode by either a reset or the occurrence of an interrupt. The
Timers 191
interrupt can be either an internal or an external interrupt. Since the
output compare timer is set up and its interrupt is enabled, when the
internal counter matches the contents of the output compare register,
an interrupt will occur and remove the processor from the wait mode.
Sometimes, it is desirable to get a measure of the fraction of the
time that the microcontroller is used to execute its program. The use
of the wait mode provides an excellent mechanism for measurement
of this usage. If there is an extra output port pin available, this pin
can be set just prior to entering the wait mode. The pin can then be
reset as the first instruction in the interrupt service routine. To make
this measurement more accurate, you can set another output pin to
the on condition all of the time. Measure these two outputs with an
averaging DC voltmeter. One hundred times the ratio of the cycling
output to the fixed output is the percentage of time that the
microcontroller is available to execute other code.
Examine the following code sequence:
time_count . b .hi = OCHI1;
ac = TSR; /* Arm OCF1 bit clear*/
time_count.b.lo = OCLOl; /* Clear OCF1 bit */
time_count.l += 500; /* 500 counts per millisecond */
OCHI1 = time_count . b .hi ;
OCLOl = time_count .b . lo;
The first instruction copies the high byte of the output compare
register into the high byte of the structure b in the union t ime_count .
When the TSR is copied into the a register, the system is set up to clear
the OCF1 bit. Then, the low byte of the output compare register is
moved into the low byte of the structure b. These operations leave the
16-bit contents of the output compare register in the union
t ime_count . 1. Note that the high byte is moved from OCHI 1, the
TSR is accessed, and then the low byte is moved from OCLOl. This
sequence, accessing OCLOl after the TSR has been accessed will clear
the output compare flag 1, OCF1, and remove the interrupt source.
500 is added to this 16-bit number, and the result is copied back into
the output compare register 1 a byte at a time.
While it is usually best to keep the interrupt service routines short,
sometimes other operations can be completed within these routines
that can be useful. Recall that in the EEPROM programming routine
192 Chapter 4 Small 8-Bit Systems
there was a function called delay. A delay function can be integrated
into a time interrupt service routine easily. The interrupt service rou-
tine above is entered every millisecond. We specified a function delay
that had an unsigned long argument that corresponds to the required
delay in milliseconds. Such a function could be implemented as:
void delay (unsigned long);
unsigned long time=0;
void delay (unsigned long del)
{
time = del; /* place the delay time in the global
memory */
while (time>0) ;
}
Now we must add one code sequence to the interrupt service routine
above:
if ( --count= = 0)
{
if (time>0)
--time;
return ;
}
The function delay ( ) places the value of the required delay in
the unsigned long location time. The program then hangs on the
while statement so long as time is greater than 0. Every time the
I SR is entered — which is every millisecond — the 1 ong uns igned
value time will be decremented. When the proper time has passed,
the delay ( ) function will return to the calling program.
The function delay ( ) does something that many programmers
do not like: the program hangs in a loop until a specified time has
passed. This operation may seem to waste the microcontroller re-
source. Note, however, that the program does not spend all of the
time in the loop; interrupts are being serviced during this time. We
will see that the timer interrupt is only one of many potential inter-
rupts that will be working on the part at all times. Placing the device
into a wait loop for a few milliseconds may stop the main program in
Timers 193
its tracks, but the background operations being serviced by the inter-
rupts will continue to be processed unhindered. In this case, a global
variable has been used to transfer data between an interrupt service
routine and the applications portion of the program. The variable
time is set in the applications program, and the completion of the
time delay is evaluated in the applications portion of the program,
and time itself is decremented in the interrupt service routine.
It is possible to create errors in operation with this type of proce-
dure. One place where a nasty bug can creep into your code is when
you are dealing with bit manipulations in both the application por-
tion of the program and the interrupt service routine. Suppose that
you want to toggle a bit when an event was detected in the applica-
tion, and simultaneously you need a periodic bit toggle that is
controlled by code in the interrupt service routine. The code sequence
might appear as follows:
PORTA. BITAPP = ! PORTA. BITAPP;
In the interrupt service routine, the code could be
PORTA. BITINT = ! PORTA. BITINT;
In each case, this code will compile into
Ida PORTA
eor 2^BITNUMBER
sta PORTA
The same code sequence will appear in both the application and
the interrupt service routine with the BITNUMBER appropriately cho-
sen. Suppose that we are in the application, and have just executed
the Ida PORTA instruction when the interrupt occurs. In the inter-
rupt service routine, the above code will be executed properly, and
when control is returned to the program main line, it will continue
with the eor instruction. However, the contents of PORTA will have
194 Chapter 4 Small 8-Bit Systems
been changed by the interrupt service routine, so the value that was
loaded by the Ida instruction prior to the interrupt is no longer valid.
The earlier loaded value of PORTA will be restored by the above
sequence in the application code, and the change made in the inter-
rupt service routine will be undone.
This bit of interplay between the applications program and the
interrupt service routine is a type of software race. It can be easily
avoided. Whenever a variable is changed in both the applications
code and in an ISR, the erroneous set/reset can be avoided if the
program disables all interrupts before the offending instruction in
the applications code. The interrupts must then be re-enabled after
the code execution. In this case, the code will be
SEI ();
PORTA . BITABB= ! PORTA . BITAPP ;
CLIO ;
in the applications code. In this case, there can be no interrupt to
interfere with the proper handling of PORTA in the application.
In the timer routine above, there is what some might call a glaring
oversight. The timer was not initialized in the initializing routine of
the program. The first lines of code merely enabled the timer interrupt
and then proceeded into the FOREVER loop, which is the applications
portion of the program. The initial state of the TCR and the OCR regis-
ters is not established at reset. There is no way of knowing when the
first output compare will take place. If the initial value of the OCR
happens to be one less than that of the TCR, there will be an output
compare about 0.13 seconds after the interrupts are enabled, and from
then on the interrupt period will be accurate. If it is imperative that the
first output compare occur at exactly the specified time, then the code
for initialization of the output compare registers must be included in
the initialization routine. Otherwise, if this error in the initial timing
can be tolerated, it will save bytes of code space that might be desper-
ately needed for another portion of the program.
Analog-to-Digital Converter Operation 195
EXERCISES
1 . Write a program that uses an output compare system to generate
an accurate waveform with a 1000.0 second period.
2. Devise a convenient method to test the performance of the pro-
gram in Exercise 1 above.
3. Write a program to generate two waveforms. One output is to be at
twice the frequency of the other. The duty factors of the two sig-
nals are to be equal to 50%. The frequency of the slowest wave is
to be 1000 Hz. The phase of the higher frequency signal is to be
such that its rising edge is to occur 260 microseconds following
the rising edge of the first signal.
4. Two DC motors are running. Each motor has an optical interrupter
on its shaft with 15 interrupts per revolution of the shaft. All but one
of the interrupts occupy one-sixteenth of the circumference of the
rotation. The fifteenth interrupt occupies one-eighth of the circum-
ference. Using input capture registers, measure the speed of the two
motors, and provide a slow down or speed up signal that can be used
on either motor to synchronize the rotation of the two motors with
the wide interrupter positions on the shafts being in lock-step.
5. What microcontroller characteristics will control the maximum speed
at which the motors in Exercise 4 can run? The minimum speed?
Analog-to-Digital Converter Operation
The analog-to-digital converter (ADC) found on the M68HC05
family is moderately simple in its operation. There are a few impor-
tant items that must be remembered when dealing with the ADC.
Most important is that the ADC must be turned on for at least 100
microseconds prior to reading a value. If 100 microseconds has not
elapsed, it is guaranteed that the value read will be in error. The ADC
is turned on by setting the ADON bit in the ADC control/status regis-
ter. This register is referred to as AD_CTST. The following code
sequence will turn the ADC on:
AD_CTST=0;
AD CTST.ADON=l;
196 Chapter 4 Small 8-Bit Systems
The following function will provide a reading of a single channel of
the ADC input:
unsigned int read_adc (int k)
{
AD_CTST &=~0X7;
AD_CTST |=k;
while (AD_CTST.COCO==0)
; /* wait here til COCO is set */
return AD_DATA;
}
The argument k is the channel that is to be read, and k can have
a value of to 7 to read the external channels.
The first two lines of code in the above function will place the
channel number to be read in the channel bits of AD_CTST. These
bits must be cleared by an instruction sequence that will not alter the
upper bits of AD_CTST because the ADON bit is in the upper portion
of AD_CTST. This bit cannot be reset while the ADC operation is
continuing. The first line of code clears the least significant three
bits, and the second line places the channel number in these bits.
Writing to AD_CTST will cause the ADC conversion to start. There-
fore, all that must be done is to wait until the conversion is completed
to read the data into the program. The code
while (AD_CTST.COCO==0)
; /* wait here til COCO is set */
will keep control of the microcontroller in that instruction sequence
until the COCO bit, which is the conversion completion bit, is set. At
that time the value found in AD_DATA will be the result of the latest
conversion.
Often, the ADC results must be subjected to some processing to
remove unwanted characteristics of the signal being measured. Here
is a case where careful use of assembly language procedures can
make a big difference in the execution speed as well as the amount of
code needed. An example that is often used is to average the past
values of the data. A reasonably simple approach is to allow the lat-
est ADC reading to have a 50% weight and all of the past readings to
have a 50% weight. The following example code will accomplish
this task in three different ways:
Analog-to-Digital Converter Operation 197
#include "hc05b6.h"
unsigned adc_data [8] ;
unsigned read_adc (int) ;
main ( )
{
unsigned int j ;
AD_CTST=0;
AD_CTST.AD0N=1;
for (j=0; j<8; j++)
{
adc_data [ j ] = (read_adc ( j ) +adc_data [ j ] ) /2 ;
adc_data[j] >>= 1;
adc_data[j] += read_adc ( j ) >>1 ;
#asm
ldx j
Ida j
jsr read_adc
add adc_data,x
rora
sta adc_data,x
#endasm
Listing 4-5: Three Different ADC Averaging Routines
The first attempt to read and average the data approaches the
problem as simple as practical. The data are read in from the ADC,
added to the corresponding stored data in the array adc_data, the
result is divided by two, and the final average is put back into the
proper location in the array. The following line of code is all that is
necessary to accomplish this task:
198 Chapter 4 Small 8-Bit Systems
adc_data [ j ] = (read_adc ( j ) +adc_data [ j ] ) /2 ;
Hidden in this code is the fact that both read_adc and
adc_data are unsigned results. When two unsigned numbers are
added together, the most significant bit of each can be 1 so there can
be a carry or overflow when the addition takes place. Problems from
this carry can be avoided in this case by merely using a long or
double precision add routine in adding the two numbers. Then,
when the result is divided by 2, if a bit is carried into the upper byte
of the result it will be shifted back into the lower byte. The result of
this operation is correct and will fit into a single unsigned int.
The compiled version of the above program with read_adc ( )
merged into it follows:
#include u hc05b6.h"
0050 0008 unsigned adc_data [8] ;
unsigned read_adc (int) ;
void main (void)
{
0058 unsigned int j ;
0100 3F 09 CLR $09 AD_CTST=0;
0102 1A 09 BSET 5, $09 AD_CTST . ADON=l ;
0104 3F 58 CLR $58 f or ( j =0 ; j <8 ; j ++)
0106 B6 58 LDA $58
0108 Al 08 CMP #$08
010A 24 36 BCC $0142
{
010C CD 01 43 JSR $0143 adc_data [ j ] = (read_adc ( j ) +
adc_data [ j ] ) /2 ;
010F BE 58 LDX $58
0111 EB 50 ADD $50, X
0113 AE 02 LDX #$02
0115 CD 01 57 JSR $0157
0118 9F TXA
0119 BE 58 LDX $58
011B E7 50 STA $50, X
Analog-to-Digital Converter Operation 199
011D BE 58 LDX $58 adc_data [ j ] >>= 1;
011F E6 50 LDA $50, X
0121 44 LSRA
153
0122 BE 58 LDX $58
0124 E7 50 STA $50, X
0126 B6 58 LDA $58 adc_data [ j ] +=
read_adc ( j ) > > 1 ;
0128 CD 01 43 JSR $0143
012B 44 LSRA
012C BE 58 LDX $58
012E EB 50 ADD $50, X
0130 E7 50 STA $50, X
#asm
0132 BE 58 ldx j
0134 B6 58 Ida j
0136 CD 01 43 jsr read_adc
013 9 EB 5 add adc_data,x
013B 46 rora
013C E7 50 sta adc_data,x
#endasm
}
013E 3C 58 INC $58
0140 20 C4 BRA $0106
0142 81 RTS }
unsigned int read_adc(int k)
0059 {
0143 B7 59 STA $59
0145 B6 09 LDA $09 AD_CTST&=~0X7 ;
0147 A4 F8 AND #$F8
0149 B7 09 STA $09
014B B6 09 LDA $09 AD_CTST |=k;
014D BA 59 ORA $59
014F B7 09 STA $09
200 Chapter 4 Small 8-Bit Systems
0151 OF 09 FD BRCLR 7, $09, $0151
while (AD_CTST.COCO==0) ;
0154 B6 8 LDA $08 return AD_DATA;
0156 81 RTS
}
0157 BF 5A STX $5A
0159 B7 5B STA $5B
015B 4F CLRA
015C 5F CLRX
015D 5C INCX
015E 38 5B LSL $5B
0160 49 ROLA
0161 BO 5A SUB $5A
0163 24 03 BCC $0168
0165 BB 5A ADD $5A
0167 99 SEC
0168 59 ROLX
0169 24 F3 BCC $015E
016B 53 COMX
016C 81 RTS
1FFE 01 00
The assembly code to execute this single line of C code is found in
the address range $10c to $llb, or 17 bytes of code. However,
there is a call to the doub 1 e precision add routine at address $115.
This routine occupies the address range $157 to $ 16c, or 22 addi-
tional bytes of code hidden from the main routine. Therefore, this
requires 37 bytes of code for its execution.
The next approach is to avoid the overflow problem by dividing
by 2 each of the terms to be added prior to the addition. Division by
2 is accomplished by shifting each of the terms to the right by one
bit. This approach guarantees that there will be no overflow into the
higher byte because the most significant bit of each number will be
after the shift, and no binary addition can cause more than a 1-bit
overflow. Therefore, the sum will at most have its most significant
bit turned on. This number is then stored in the location
adc data [ j ] .
Pulse Width Modulator System 201
The assembly code created by the compiler for this approach is
in the address range $lld to $130. This code uses 21 bytes of
memory. Careful examination of this code will show that the instruc-
tion at $ 1 2 2 is unneeded, so the code could have been completed in
17 bytes.
The assembly version of the same routine merely adds the two
unsigned numbers, and then rotates the result right 1 bit to accom-
plish the divide by 2. The 1-bit rotate accomplishes the same thing as
a right shift by 1 bit with the exception that the carry bit is shifted
into the most significant bit of the result. The only way the carry bit
could be set is by the most significant bits of both addends being 1.
The assembly version of the code resides in $132 to $13c and re-
quires 12 bytes of code.
Here is a case where judicious choice of assembly code will
provide a significant improvement in the amount of code needed to
execute a specific program. The main reason that the assembly ver-
sion is shorter is that the carry bit in the condition code register is
available to the programmer from assembly. This bit is completely
hidden from the programmer in any high-level language. Therefore,
tricks like rotating a bit from the carry into a register are not avail-
able in the high-level language.
EXERCISE
1. Write a routine to average readings from the ADC on a
microcontroller, but weight the current reading three times that of
the past average.
Pulse Width Modulator System
In this section on programming of the timers, two approaches to
the generation of a pulse width modulation (PWM) signal will be
discussed. These approaches both use the output compare system of
the 16-bit timer.
What is a PWM system? A PWM signal is a periodic signal where
the signal is set high for a calculated duty factor, and then the output
goes low for the remainder of the period. If you measure a PWM
signal with an averaging voltmeter it will have a value equal to the
peak voltage times the duty factor of the PWM. Here the duty factor
is defined as the ratio of the on time to repetition period of the signal.
202 Chapter 4 Small 8-Bit Systems
Thus, we have created a simple digital-to-analog converter. Many
control applications need analog control signals for parts of the sys-
tem. PWM systems on microcontrollers provide an excellent means
for providing these analog voltages, so long as the limitations we
will discuss below are acceptable to the total system design.
The built-in PWMs in the M68HC05Bx family of devices pro-
vide you with one of two pulse periods: the programmable timer
clock frequency divided by 256 or 4096. These frequencies are fixed,
and the interval is divided into 256 parts. Therefore, the analog fre-
quency ranges — and the accuracy of the analog reproduction — is
limited to one part in 256 over the range of the output.
When using the output compare system to create a PWM, it is
possible to achieve a finer analog resolution at slower frequencies,
or perhaps a faster signal with poorer accuracy. A PWM created from
an output compare system is somewhat more flexible than the usual
built-in PWM. With the PWM systems built around the output com-
pare system, the computer is responding to frequent interrupts. When
an interrupt occurs, the system status is pushed onto the stack and the
address of the interrupt service routine is placed into the program
counter. It takes approximately 10 clock cycles to respond to an in-
terrupt after the completion of the executing instruction. Return from
an interrupt requires 9 clock cycles. Therefore, it is not possible to
accomplish the full goal of a PWM with an output compare system.
The goal in this case is to achieve full on at the one extreme input
level and full off at the other. The times required to process an inter-
rupt will limit the performance at one extreme, the other or perhaps
both when a PWM is implemented with an output compare system.
A first example is shown in the following listing. This example
shows the interrupt service routine along with the necessary defines
only. It is assumed that the output compare interrupt bit along with the
interrupt bit are properly set to allow the output compare 2 to interrupt
the processor. Also shown in this example is code to control a second
output compare — 1 — to provide the system with a fixed time base.
In operation, the applications portion of the program must place
a pair of complementary numbers, pwm_number and of f _count,
in place. The sum of these two numbers must equal a constant that
establishes the period for the PWM signal. For the M68HC05B4, the
Pulse Width Modulator System 203
period will be 2 microseconds times the total count value. Prior to
starting use of the PWM, the bit f lag . ON should be reset and the
value for count, which is the main time base period count, should
be set.
/* bits in flag */
#define ON
union both time_cnt;
unsigned long of f_count , pwm_number , count ;
bits flag;
void TIMER_OC (void)
{
if ( TSR.OCF2==l) /* accesses the TSR */
{
if (flag.ON= = 0)
{
flag.ON=l;
TCR.OLVL2=0; /* Timer Compare 2 off*/
time_cnt .b.hi=OCHI2 ;/* get start time */
time_cnt .b.lo=OCL02; /* reset the OCF */
time_cnt . 1 +=pwm_number ;
OCHI2=time_cnt . b . hi ;
OCL02 = t ime_cnt . b . lo ;
return;
}
else
{
flag.ON=0;
TCR.OLVL2=l;/* Timer Compare 2 to on */
time_cnt .b.hi=OCHI2 ; /* get start time */
time_cnt .b.lo=OCL02; /* reset the OCF */
time_cnt . 1 +=of f _count ;
OCHI2=time_cnt . b . hi ;
OCL02 = t ime_cnt . b . lo ;
return;
204 Chapter 4 Small 8-Bit Systems
}
}
else /* must be that 0CF1==1 rather than OCF2
*
/
time_cnt .b . hi=OCHIl ; /* output compare */
time_cnt.b.lo=OCL01; /* get lo byte */
time_cnt.l +=count; /* time for interrupt */
OCHIl=time_cnt . b.hi;
OCL01=time_cnt .b.lo;/* reset the OCF */
ms_10++ ;
return;
}
}
Listing 4-6: ISR For PWM System
Since the interrupt bits are set to allow output compare interrupts,
an interrupt will occur. The interrupt service routine will be entered,
and the instruction sequence
if ( TSR.OCF2==l) /* accesses the TSR */
{
accomplishes two things. First, it determines if the output compare 2
caused the interrupt, and, secondly, it accesses the TSR and arms the
reset of the OCFX when the proper lower byte of the output compare
register is read later.
For the moment, assume that OCF2 bit is set: the interrupt was
caused by a compare in output compare register 2. In this case a test
is made to determine if flag. ON is reset. Since the initialization rou-
tine reset this bit, it will be 0. Therefore, the code sequence
flag.ON=l;
TCR.OLVL2=0; /* Timer Compare 2 to turn off*/
time_cnt .b.hi=OCHI2 ; /* get the start time */
time_cnt .b.lo=OCL02; /* reset the OCF */
time cnt . 1 +=pwm number;
Pulse Width Modulator System 205
OCHI2=time_cnt . b . hi ;
OCL02 = t ime_cnt . b . lo ;
return;
will be executed. This code is the "on" code for the PWM system.
The first instruction in this sequence will set the bit f lag . ON, so
that this code will not be executed in the next execution of the inter-
rupt service routine. The bit TCR . OLVL2 is the state that will be
transferred to TCMP2 pin when an output compare occurs.
TCR . OLVL2 is then set to so that TCMP2 will be set low when the
next output compare occurs. The code sequence that follows gets the
contents of the output compare register 2, increments this value by
pwm_number, and places this new value back into output compare
register 2. Thus, the time of the output compare is established and
the interrupt service routine is exited.
Eventually, the next output compare 2 interrupt will occur. Since
the bit f lag . ON has been set, the following code will be executed.
This code is the "off" code for the PWM system. The first business
to take care of is to reset the bit f lag . ON so that the on portion of
PWM code will be executed the next time into the I SR. TCR . OLV02
is set so that TCMP2 will go on when the next output compare occurs.
flag.ON=0;
TCR.OLVL2=l; /* Timer Compare 2 to turn on */
time_cnt .b .hi=OCHI2 ; /* get the start time */
time_cnt .b.lo=OCL02; /* reset the OCF */
time_cnt.l +=off_count; /^complete the cycle*/
OCHI2=time_cnt . b . hi ;
OCL02=time_cnt .b . lo;
return;
The contents of OCR2 are handled as above, but this time their value
is incremented by the contents of of f _count. The main line code
is then re-entered by the use of the return instruction.
There is another approach to generation of PWM signals with out-
put compare systems that will materially aid this problem. However,
this approach requires two output compare systems. One output com-
pare system is used to create the time base and the other is used to
generate the on or off time. Let's examine the following interrupt
service routine. Output compare 1 is used here to generate the PWM
206 Chapter 4 Small 8-Bit Systems
time_cnt . b . lo=TCLO
time cnt . 1 +=count
period. The way that this routine works, it is possible for an interrupt
to occur as a result of a compare on output compare 2. This interrupt
is to be ignored, and the first five lines of the code below tests for this
condition and clears interrupt when it occurs. This interrupt occurs
at the end of the on time. Control of the program must be out of this
portion of the ISR and in the main line code before the next interrupt
caused by OCF1 occurs. The time required for the first five lines of
code here is about 18 microseconds, which is the minimum on time.
void TIMER_OC (void)
{
if (TSR.OCF2==l)
{
ac=OCL02 ; /* if interrupt is caused by out-
put*/
return; /* compare 2 reset it and return */
}
time cnt .b.hi=TCHI ; /* get in the TCR */
/* get lo byte */
/* bump the time counter
for next*/
OCHIl=time_cnt . b.hi ; /* interrupt */
OCL01=time_cnt .b.lo;/* reset the OCF */
if (flag.PWM==l)
{
TCR.OLVL2=l;/* Timer Compare 2 to set output */
TCR.FOLV2=l;/* turn on Timer compare 2 output */
TCR.OLVL2=0;/* Timer Compare 2 to reset output*/
time_cnt .b .hi=TCHI ; /* get the start time */
t ime_cnt . b . lo=TCLO ;
t ime_cnt . 1 +=pwm_number ;
OCHI2=time_cnt . b . hi ;
OCL02 =t ime_cnt .b.lo;
}
}
Listing 4-7: Alternate PWM ISR
The next sequence is the code required to establish the time base.
This time base operates on output compare 1 . This routine is similar
to those discussed above. A bit in the bit field flag is tested to deter-
Other Program Items 207
mine if the PWM signal is to be turned on. If flag . PWM is set, the
PWM code will be executed. When this routine is entered, TCMP2
will be reset to zero. The first instruction sets TCR . OLVL2 so that
TCMP2 will be on at the next output compare. The next instruction,
TCR . FOVL2 = l , sets the force overflow bit for TCMP2 which will
cause this bit to be turned on since TCR . OLVL2 is set. This time is
the beginning of the on period of the PWM signal.
The next instruction
TCR.OLVO2=0;
is so that TCMP2 will go off or be reset at the next output compare 2.
Control is then passed to the main line of code. When TCMP2 oc-
curs, the output will return to and an interrupt will occur. This
interrupt is detected by the first lines of the interrupt service routine.
One could write a code sequence that would generate the mirror
image signal to that above. That is, the off time would be controlled
by TCMP2 and the on time would be the difference between the time
of TCMP2 and the main time base.
Other Program Items
Several other small programs come to mind that are very useful
in programming microcontrollers. With these small parts, it is usu-
ally desirable to avoid library functions and, if possible, use a bag of
tricks to arrive at the desired results. For example, dealing with num-
bers for either input or output offers a good place to exercise some
experience over expedience. One case where it is often important to
minimize code space is in generating binary coded decimal (BCD)
numbers from integer numbers to output from a computer. Perhaps
the most direct approach to accomplish this conversion is as follows:
/* convert a binary number less than 100 to BCD */
unsigned char convert_bcd (unsigned char n)
{
unsigned int result;
if (n>99) return Oxff;
result=n/10<<4 ;
result += n%10;
208 Chapter 4 Small 8-Bit Systems
return result;
}
Listing 4-8: First BCD Conversion
This small function takes an 8-bit character n that is less than 99
and converts the number into one 8-bit result. The upper nibble is the
number of tens in the number and the lower nibble is the number of
units. A hexadecimal number Oxff is returned if the number is greater
than 99. Note that the calculation requires an integer divide opera-
tion and a modulus operation which is equivalent to a divide operation.
The code to execute the divide and modulus operations must be asso-
ciated with this function to complete its task.
Another approach is shown below. This approach avoids any exter-
nal function calls and actually requires less total code than the version in
Listing 4-8, even though the C code is somewhat longer. In this case, the
while loop essentially divides the input number by 10 and places the
result in the most significant nibble of the result. After the tens have been
removed from the number, all that is left is the units. This value — the
number of units — is ORed on the result before it is returned.
/* convert a binary number less than 10 to BCD */
unsigned char convert_bcd (unsigned char n)
{
unsigned int result;
if (n>99) return Oxff;
result=0 ;
while ( (n-10) >=0)
result+=0xl0 ;
n+=10;
result += n;
return result;
}
Listing 4-9: Second BCD Conversion
The function shown below was originally written for use on a
M68HC05, but was later used on a M68HC11 as well. A two-digit
number must be sent to a seven- segment LED display. The number
to be shown is contained in the memory locations for tens and units.
Summary 209
The numeric display takes four inputs that are merely the BCD value
of the number to be shown. With this particular display, a 4-bit num-
ber between the values of and 9 will be displayed. If each of the
four input lines to the display is turned on, the display will be turned
off. The parameters passed to the function — high and low — are
flags to indicate whether the corresponding output is to be turned on.
void display (int high, int low)
/* Display the contents of units and tens on the
appropriate LED displays. High corresponds to
tens, and low corresponds to units. If the proper
argument is TRUE, the corresponding LED will be
turned on. If the argument is FALSE, the LED will
be turned off. */
{
unsigned int save;
save = tens<<4;
save |= units70x0f;
PORTA=save;
if (! high)
PORTA |= OxfO;
if ( !low)
PORTA = Oxf;
}
Summary
In this chapter, we have discussed programming techniques for a
few of the more important peripheral components found on
microcontrollers. Timers and ADC applications will be reconsidered
in later chapters. In later chapters, serial communications, attendant
programming of look-up tables, interpolation between data points in
look-up tables, and synchronous communications from standard digi-
tal I/O pins rather than an SPI will be covered. Some small 8-bit
microcontrollers have pulse width modulation (PWM) outputs that
can be used as a digital-to-analog converter (DAC). Often the ranges
available from these fixed PWM systems are not satisfactory for the
required application. Other methods of accomplishing the PWM
operation will be discussed in later chapters.
210 Chapter 4 Small 8-Bit Systems
Many of the peripheral components found on the small 8-bit de-
vices are found on larger microcontrollers. The next chapter introduces
a group of larger microcontrollers, the M68HC1 1 family. The com-
piler used for the development of the M68HC05 code does not extend
to the M68HC1 1 family. Therefore, the source code will look a little
different in the following chapters, but it will still be all C. To gener-
ate code for the M68HC05, the compiler has had to "bend" the
concepts of ANSI C to create code that would work with that family.
Larger microcontrollers accommodate more of the large computer
features so use of ANSI C is possible.
Chapter 5
Programming Large 8- Bit Systems
This chapter on the programming of large 8-bit systems will make
use of the MC68HC11 microcontroller. It is absolutely necessary
that any programmer understand the device when writing code for a
microcontroller application. If you are not familiar with the
MC68HC1 1 family, then read the M68HC1 1 Reference Manual and
the M68HC1 1 E Series Technical Data Manual on the accompanying
CD-ROM to get the needed background to be able to continue the
work in this chapter.
Header File
The CD-ROM contains the C header file HC1 IE 9 . H. This file should
be included with any program that is going to be used on the
MC68HC11E9 or the MC68HC711E9. An abbreviated version of
this header file is listed below. The header file has about 400 lines of
source code, and most of that code is repeats of the portions of code
shown in the following listing.
#ifndef HClle9
#define HClle9
unsigned int Register_Set = 0x1000;
typedef struct {
unsigned
char
bitO
1
unsigned
char
bitl
1
unsigned
char
bit2
1
unsigned
char
bit3
1
211
212 Chapter 5 Programming Large 8-Bit Systems
unsigned
char
bit4
1
unsigned
char
bit5
1
unsigned
char
bit6
1
unsigned
char
bit7
1
} Register;
#define PORTA (* (volatile Register*) (Register Set+0) )
typedef struct {
unsigned char
unsigned char
unsigned char
unsigned char
unsigned char
unsigned char
unsigned char
unsigned char
}Pioc;
STAF
1
STAI
1
CWOM
1
HNDS
1
OIN
1
PLS
1
EGA
1
INVB
1
#define PIOC (* (volatile Pioc*) (Register_Set+2) )
#define PORTC (* (volatile Register*) (Register_Set+3) )
#define PORTB (* (volatile Register*) (Register Set+4) )
#define TCNT (* (unsigned int *) (Register_Set + OxE) )
#define TIC1 (* (unsigned int *) (Register Set + OxlO) )
/* To clear bits in the flag registers use the form
TFLG1 = OC1
to clear 0C1F in TFLG1 . Use this form only in the
two flag registers TFLG1 and TFLG2 below.
/
Header File 213
#def ine
#def ine
#def ine
#def ine
#def ine
#def ine
#def ine
#def ine
0C1F 0x80
OC2F 0x4
OC3F 0x20
OC4F 0x10
I405F 0x08
IC1F 0x04
IC2F 0x02
IC3F 0x01
#define TFLG1 (* (unsigned char*) (Register Set+0x23))
typedef struct {
unsigned char TCLR
unsigned char SCP
unsigned char RCKB
unsigned char SCR
}Baud;
1
2
1
3
#define BAUD (* (volatile
Baud*) (Register Set+0x2B) )
/
Macros and function to permit interrupt service
routine programming from C.
To use the vector call, do vector (isr,
vector_address) where isr is a pointer to the
interrupt service routine, and vector_address
is the vector address where the isr pointer
must be stored. */
#define vector (isr, vector_address) (*(void**) (vector_address) = (isr) )
#define cli() _asm( u cli\n" )
#define sei() asm ( "sei\n" )
214 Chapter 5 Programming Large 8-Bit Systems
#ifndef NULL
#define NULL (void *)0
enum { FALSE , TRUE } ;
enum { STOP , START } ;
enum {OFF, ON};
#define FOREVER while (TRUE)
typedef unsigned int WORD;
typedef unsigned char BYTE;
#endif
#endif
The first instruction in the file is
#ifndef HC11E9
#define HC11E9
and the last entry in the file is
#endif
These lines of code are useful to prevent multiple definitions of the
items defined within the file. If the header file has not been previously
compiled as a part of the program when the above statement is seen,
HC 1 1 E 9 will not be defined. The first instruction determines if HC 1 1 E 9
is not defined, and if it is not, the second line defines it. Then all of the
code until the matching #endi f will be compiled. If HC1 IE 9 is already
defined, then the compiler will skip the code lines until the matching
# end if is found. Therefore, the programmer can put an
#include <hcllE9.h>
at the beginning of each program module, and it will be used in the
program only once even if several modules are combined into one
and the above statement is included several times in a single file.
This approach is convenient because it allows each module to be
compiled and tested and then the several modules can be merged and
compiled as a unit without worry about multiply defined variables
and values found in header file.
The file next entry is a simple typedef and declaration of a
structure called Register. This structure contains eight 1-bit entries.
Each of these bits corresponds to a specific bit in a register field, and
Header File 215
the bits are given the names bitO,bitl, . . . b i 1 7 . Therefore,
when dealing with specific bits within a type Register, the
programmer should use bi tx, where x is the number of the bit being
referenced.
In the second portion of the file that follows, all of the I/O
registers are declared.
The external variable Register_Set is given a value 0x1000.
This value is the initial location of the I/O register map in the system.
In the MC68HC 1 1 , bit manipulation assembly instructions have a one-
byte address that can be an offset from an index register. With the
indexed version of the instruction, any single byte or bit in the entire
memory map can be accessed or tested with a single instruction.
However, the address operation must be indexed. If you use a single
address with offsets to each of the registers as is done in the header
file, the compiler will automatically place the value of Regi s t er_Set
into an index register and use the offsets specified to allow indexed
access to the data in the registers from anywhere in the program.
The value of Regi ster_Set is not fixed by the microcontroller.
It can be changed within the first 64 clock cycles following reset. To
make this change, the programmer must assign the correct value to
the INIT register in the I/O memory space. This value should be
changed in the initialization routine for the program, which is usually
written in assembly language. After the INIT register is changed, a
new proper value assigned to Reg i s t e r_S e t will allow the desired
access to all registers and bits in the I/O memory map.
The definition of Register shows that any instance of this
variable type is a collection of eight bits. Any location that is defined
as a type Regi s t er is truly a collection of bits and each bit must be
processed individually. For example, PORTA is declared to be of the
type Register. Therefore, an expression like
PORTA = 0x3 f;
will result in an illegal assignment error because PORTA is of the
type Register, not char .
It is possible to make assignments to ports defined in the above
manner as either a byte- wide field or as bit fields. Return to the initial
declaration of a register in the header file:
typedef struct
216 Chapter 5 Programming Large 8-Bit Systems
{
signed char bit :1;
• • •
unsigned char bit 7 :1;
} Register;
We can now declare:
typedef union
{
char byte;
Register bits;
} Mix_Register ;
and thus
#define PORTA (^volatile
Mix_Register*) (Register_Set +0)
will allow the programmer to use
PORTA. byte = 0x2 E
to set the value of all bits in the port with one instruction and in the
same program to use
PORTA. bit s.bit3 = 1;
to set, reset, or test the individual bits inside of the port. In this book,
we will use the bit fields only as shown in header file.
Since PORTA is of the type Register, it is possible to deal
with the individual bits within this location by normal C constructs
such as
PORTA. bit 3 = 1;
if (PORTA. bit6 == 0)
• • •
Of course, it is much better to define practical names to the various
bits within the port to achieve even clearer code:
# define ON TRUE
#define MOTOR bit3
#define PUSH BUTTON bit6
Header File 217
PORTA. MOTOR = ON; /* turn the motor on */
if (PORTA. PUSH_BUTTON==ON)
{
do push button things
}
With this compiler, an int is a 16-bit value. Therefore, the
registers that are two bytes are cast onto the type int. Usually these
registers are accessed as ints only and there is no need to have the
individual bit access afforded by the use of the Register type.
The timer counter register (TCNT) is one such register that is accessed
as an int only. There are also a few one byte, or 8-bit, registers that
are accessed as bytes only. No bit accesses within these registers are
needed. In these cases, the register is cast onto the type char. The
several ADRx registers are examples of this type. The ADRx registers
contain the result of an analog-to-digital conversion that is usually
handled as an 8-bit unit only.
In most instances, register locations should be uns igned. The ADRx
registers each contain the result of analog-to-digital conversions. These
results are all unsigned. Therefore, these registers should be cast as
uns igned char. Also note that all of the timer count and input capture
or output compare registers are also declared as unsigned.
In the listing above, you will note that there are two parts to the
declaration of each register. The first identifies all of the bits in the
register through a structure typedef . Then a macro definition of the
port name causes each instance of the port name in the program to be
replaced by the dereferenced value of the register address cast onto a
pointer of the correct type. The port name is the name of the port
found in the data manual, and the bit names given the bits in the structure
are the names found in the data manual. Therefore, if you wish to set
the bit named HNDS found in the register PIOC, you need to use
PIOC.HDNS=ON;
There are two register locations that work differently from the
rest. These are the two flag registers whose bits are set by the
218 Chapter 5 Programming Large 8-Bit Systems
occurrence of an interrupt. The bits in these registers are turned off
when the code instructs the bit to be set. Since these two registers are
so different from the remainder of the registers in the system, I handle
them differently. Rather than declare a structure for these registers,
the individual bits are #defined as power-of-two values. Therefore,
the bit OC2F is defined as 0x40 and the bit IC2F is #defined as
0x02. The flagl register TFLG1 is forced to the address
Regis ter_Set+0x23. Now to clear the bit OC2F, you need to set
TFLGl=OC2F;
The final section of the file contains several macros that are helpful
in handling interrupt service routines. The first macro is
#define vector (a, b) ((*(void **)b) = (a))
This macro is used to place the address of an interrupt service
routine into a vector address. The argument a is a pointer to the interrupt
service routine, and b is the vector address. If one asks what b is, you
must say that b is a pointer to a location that contains the address of the
interrupt service routine. The interrupt service routine address is also a
pointer to function that has no (void) return. Therefore, the vector
address is a pointer to a pointer to the type void and must be cast as
such before it can be used. This value must be dereferenced to be able
to place the address of the interrupt service routine into it. You can use
the macro vector ( a , b ) to place the address of each interrupt service
routine used into the proper vector location.
This macro will create code that copies the address of an interrupt
service routine to the specified memory location. This operation is
needed whenever the vector table is stored in RAM, so that the vector
table must be rebuilt each time the microcontroller is powered up.
Another approach must be used to place interrupt service routine
addresses in the vector table when this table is contained in ROM. This
latter case is probably more common than the former. In this case, we
are trying to fill a memory array with the values of the addresses of the
several interrupt service routines that the program might use. One way
to do this operation is to build an array that contains these addresses,
compile this array, and then at link time force the array to be linked to
the memory location corresponding to the beginning of the vector table.
Consider the following code sequence.
Header File 219
extern void ICl_Isr(), OC3_Isr ( ) ,_stext ( ) ;
void (* const vector [] ) ( ) ={0 , 0, , 0, 0, 0, 0,OC3_Isr, 0,
0,0,0, ICl_Isr, 0,0,0,0,0,0, 0,_stext} ;
This two-line sequence identifies three functions, each of which
returns nothing. The first two are interrupt service routines that will
process interrupts from input capture 1 and output compare 3,
respectively. The third entry is the name of the entry point in the
start-up routine that is linked to the C program. The second line of
code indicates that vector is an array of const pointers to functions
of the type void . There is one entry in this array for each interrupt
vector and the reset vector for the micro-controller. This array is
initialized with either zeros or the addresses of the interrupt service
routines in the proper locations. The address of the start-up routine is
placed in the last location in the array. This little program will be
compiled and linked to the final program. The name of the file that
contains this file is interrup . c , and it must be modified for each
program in which it is used. At link time, the address of vector [ ]
will be forced to Oxf f d6 which is the beginning of the vector table
in the MC68HC 11 family.
Some programmers will initialize the vector table with a known
address rather than 0. In the event that a spurious interrupt occurs and
takes the processor to an unused vector location, the processor would
certainly get lost if the vector table were filled with zeros. Placing a
known program into all unused vector locations will prevent this problem.
Another problem can occur in the operation of unattended
microcontrollers. It is possible that control of the microcontroller
could be diverted to unused ROM locations by serious noise spikes.
Such a loss of control will not be devastating if the system uses a
computer operating properly (COP) system. However, another safety
back-up that the programmer can incorporate into the program is to
fill all unused memory with the one-byte instruction swi (software
interrupt). This instruction causes the program to save the machine
status and pass control to the function addressed in the SWI vector
location. If this value contains the address of the start-up program,
the system will be restarted immediately if control is accidentally
moved to unused ROM.
220 Chapter 5 Programming Large 8-Bit Systems
There are several small routines associated with the processing
of interrupts that are not in the C library. These routines are written
as functions to make it easier to access these important operations.
The cli ( ) and sei ( ) functions allow the program to clear or set
the interrupt bit in the condition code register (CCR) . The programmer
can with these two functions and the interrupt masks in the I/O
memory map control all interrupt operations of the part.
The compiler must be notified that a function is to be an interrupt
service routine. An interrupt service routine can have no arguments,
and it must return nothing. Therefore, the function prototype of an
interrupt service routine might look like
void isr_clock( void );
However, the compiler would still have no way of knowing that this
function is an interrupt service routine. The reason that the compiler
must know an i s r is that the MC68HC 1 1 family stacks the complete
machine status when an interrupt is accepted by the device. This
status must be restored when the program control is returned to the
interrupted program. The machine status is restored when an rti
instruction is executed. Therefore, any return from an interrupt must
be the assembly instruction rti rather than the instruction rts
usually used to return from a function. An interrupt service routine is
identified to the compiler by the sequence ©port ahead of the
definition of the function in the function prototype. The function
prototype of an interrupt service routine should be
©port void isr_clock( void );
This flag will cause all returns from the function to be rt i instructions.
There is no guarantee that there will be macro definitions for
useful numbers and functions. A series of enumerations that define
TRUE, FALSE, ON, OFF, START, STOP, etc., are included to pro-
vide mnemonics for these often-used values. The pointer constant
NULL is also defined by a macro here. These constants are often
defined in other header files, so the protection against including these
constants more than one time is also included. Also useful is the
macro FOREVER which is included.
Header File 221
EXERCISES
1 . Write a header file that contains definitions of all vectors shown on
page 3 of the HC11 E Series Programming Reference Guide,
M68HC1 1ERG/AD found on the CD-ROM. This file should be in-
cluded with any program that is intended to make use of interrupts.
2. Create a small program that uses the function vector (a, b) .
Compile the function and observe the assembly language code gen-
erated to accomplish this operation.
The Cosmic Compiler
The Cosmic compiler was originally known as the Whitesmiths
compiler. Cosmic retained the maintenance contract on this com-
piler through a couple of corporate owners, and now has the rights to
the compiler. This compiler used for the MC68HC1 1 family is some-
what more complicated to use than the ByteCraft compiler used with
the C68HC05. The compiler should be installed using the install pro-
gram that comes with it. You should allow the compiler to modify
your autoexec.bat and config.sys files unless you plan to do it your-
self. The system path should contain the path to the bin directory in
the directory that contains the compiler. There are also two set com-
mands that should be inserted into the autoexec.bat file. These
changes, once completed, will make compilation of programs rather
easy. This compiler creates an intermediate assembly language pro-
gram that must be assembled. The result of the assembly is a
relocatable object module. This module must be linked with other
modules of the program and basic library modules to comprise the
final program. The linking phase of the compilation places all of the
parts of the program in their proper memory location.
Fortunately, the compiler can make use of command files to
control the compilation sequence. These files relieve the programmer
of the need to remember the dozens of little details that must go into
each compilation. We will start with the highest level of the command
files and work down into the lower levels. The basic command file to
compile a program and create an S Record version of the program
is as follows:
222 Chapter 5 Programming Large 8-Bit Systems
c -dlistcs +o %l.c
lnkhll < %l.lnk
hexhll -s -o %l.hex %l.hll
This file is called comp . bat and it is invoked by
comp < filename >
The % 1 in the command file will be replaced by < f i 1 ename > when
the command file is run. The first line of the command file tells the
compiler, named c, to execute with the options -dlistcs and +o.
The name of the file will be the filename entered on the command
line with a . c extension. The -dlistcs option causes a listing file
to be generated and saved in a file of the same name filename but
with an extension . 1 s. The +o option informs the compiler to create
a relocatable object module of the program.
The basic invocation syntax of the compiler is
c [options] file, [c | s | o ] [filen. [ c | s | o ] ]
Any portion of the command line in the above sequence that is
enclosed in square brackets [ ] is optional. Therefore, the only required
command line entry following the c call is the file name. (Refer to
the compiler manual for the variety of options that can be used on
this command line.)
Several files can be included on the command line. Each of these
files can have one of three extensions, c , s , or o. If the extension
is . c, the compiler expects a C program. If it is . s, the compiler
will process an assembly language program. When the extension is
. o, the compiler invokes the linker.
The compiler creates a relocatable object module that is linked
by the next line in the command file.
lnkhll < %l.lnk
The direct call to the linker lnkhll is handed a command file to
control the linking. Each program must have its own link command
file, and the extension of this file is . Ink. An example linker
command file is shown below. This command file is for a program
developed later in this chapter.
Header File 223
#
# link command file for motorfr program
#
+h # multi- segment output
-o motorfr. hll # output file name
+text -b OxdOOO # program start address
+data -b 0x0000 # data start address
crtsmot.o # start-up routine
motorfr. o # application program
libi.hll # integer library
libm.hll # machine library
+text -b 0xffd6 # vectors start address
interrup.o # interrupt vectors
+def memory= bss # symbol used by library
Comment lines are preceded with a # in this command file. The
first executable line of code in the file contains a command +h which
notifies the linker that the program will be in multiple memory
segments rather than in a single block in memory. The second line
-o motorfr. hll
tells the linker to write the output file to motorfr. hll. This
particular linker file is for a program named motorfr. The +text
option places the beginning of the code portion of the next module to
be linked at the offset OxdOOO. The +data option places the data
memory for the next module at the offset 0x0000. Finally, the names
of the modules to be linked are next. The module crt smot . o is a
relocatable object module version of a start-up program. This routine
will be discussed below. The next module to be linked is motorfr . o
which is the output of the compiler. This line is followed by the
invocation of two library calls. The first is to the integer library, and
the second is to the machine library. These libraries will provide
necessary code for function calls that are not contained directly in
the program. Another library named libf .hll contains floating
point operations that might be needed in your program.
Another + text option will force the beginning of the code linked
next to the address Oxf f d6. This address is the beginning of the
MC68HC 1 1 vector table, and the code linked at this point is the vector
function discussed earlier. This entry will create the vector table at
224 Chapter 5 Programming Large 8-Bit Systems
the correct location in memory, and it will also place the addresses of
the designated vectors in the proper locations in the table.
The start-up routine mentioned earlier is shown below. This
routine is patterned after one provided with the compiler. This
assembly language program contains all of the code necessary to
begin the operation of the C program to which it is linked. But note
this point on syntax: within C, all function and memory names
generated by the code are modified by the addition of an underscore
_ at the beginning of the name. Therefore, if the function main ( ) in
the C program were to be referred to by an assembly program, the
assembly program would be required to use _main. You will notice
that there are many names beginning with both single and double
underscores. For example _main has a single underscore, and
memory has a double underscore in the external statement that
follows. Whenever an assembly language memory location or function
name has underscores preceding the name, one of the underscores
must be discarded when this same memory or function name is used
in a C program to be linked to the module.
Here you see an example of why the programmer should not use
an underscore for the first character of a name. The compiler writer
assumes complete freedom to use the single underscore to begin any
name needed by the operation of the compiler, and the programmer
should concede this freedom to the compiler writer by avoiding the
use of the underscore for the first character of a name anywhere in
his program.
An example of this linkage has already been seen. In a previous
section where the vector table was programmed, the entry point into
the program was the function _s t ext(). Note in the code that follows
that the function name in the assembly language part of the program is
stext with a double underscore. This start-up routine has been
modified for use with a program that we will use later in the text. The
most significant modification is the first two executable instructions
which set the y register to the beginning of the I/O register memory
area and then sets bits and 1 of the location offset 36 (0x24) from the
y register content. These instructions set the prescaler of the timer
counter to 4 so that the timer will increment at the slowest possible
rate. These two instructions modify the contents of TFLG2 and they
must be changed within 64 bus cycles after the microcontroller exits
reset. These changes and others needed in the INIT register, the
Header File 225
OPTION register, and the CONFIG register must be placed in this
location in the program. These registers become read only memory
locations after the first 64 bus cycles following reset.
The value memory is calculated in the command file
motorf r . Ink and it is the number of bytes of memory that the
program uses for volatile memory. The start of the base memory
section is _sbss and it is zero for this program. The instruction
sequence starting with ldx # sbss and ending with bne zbcl
will clear all of the volatile memory used by the program. Finally the
stack pointer is set to a value of Oxf f and the C program is executed
by the j sr _main call.
.processor m68hcll
; C START-UP FOR MC6 8HC11
.external _main, memory
.public _exit, stext, return
.psect _bss
sbss :
.psect _text
stext :
ldy #4096
bset l,36,y ; set the prescaler to /4
clra ; reset the bss
ldx # sbss ; start of bss
bra loop ; start loop
zbcl :
staa 0,x ; clear byte
inx ; next byte
loop :
cpx # memory ; up to the end
bne zbcl ; and loop
prog:
sts sdata ; save sp for monitor return
xgdx ; initialize stack pointer
ldd #00ffH ; put stack pointer at ff for HC11E9
xgdx
226 Chapter 5 Programming Large 8-Bit Systems
txs
jsr _main ; execute main
_exit :
Ids sdata ; restore stack pointer
rts ; and return to calling program
return:
rti ; used by default interrupt vector
•
.psect _data
sdata :
.word ; avoid any pointer to null
. end
The start-up program must be written to account for any special
problems that might be needed for your program. Through this
routine, memory can be initialized with data stored in ROM, the
special registers that must be processed within the first 64 bus cycles
can be set, the stack can be placed where the programmer desires,
and so forth. The code at the end of the routine restores the stack
pointer to the value it contained when the start-up routine was entered
and executes an rts, return to subroutine, to return control of the
processor to a monitor that started the program if one exists.
The location sdata is the first byte following the volatile
memory used by the program. This value will be used by the memory
allocation functions to provide memory on the system heap.
Finally, the last line of the compiler command file
hexhll -s -o %l.hex %l.hll
causes execution of the program hexhl 1 , which converts the object
module from machine code to an S record format that can be written
into an evaluation module or used to burn an EPROM version of the
microcontroller. This S record file can also be delivered to the factory
and used to define a mask for a mask ROM version of the
microcontroller. The file created by this line of code will have an
extension .hex.
EXERCISES
1 . Write and compile a small program that will show some bit manipu-
lations in PORTA. Make the program test, set, and clear bits in this
Header File 227
port. Does the assembly language generated by the compiler appear
to be efficient? If not, what can be done to improve the code?
2. Write and compile a program that uses Output Compare 3 to gener-
ate a periodic interrupt. This interrupt should occur each millisecond.
3. With the time tick generated by the results of Example 2 above,
create a clock that will keep track of the time of day accurate to the
nearest one-hundredth of a second.
The Compiler Optimizer and Volatile
The compiler optimizer that is a part of the Cosmic C compiler does
an excellent job of reducing code size. One of the steps involved is to
remove code required to change values that are not changed by the pro-
gram. Recall that the volatile qualifier identifies varibles that should not
be optimized. The following code sequence will show this problem:
volatile char able;
char baker
test (voi
-d)
{
char <
dog;
dog =
able;
dog =
able;
able
= 0;
able
= 0;
dog =
baker;
dog =
baker;
baker
= 0;
baker
= 0;
Note that this function makes multiple assignments of dog, able,
and baker. The assignments involving able show how the compiler
responds to a volatile variable, and baker to a nonvolatile variable.
The following listing is the compilation of the above program:
1 ; Compilateur C pour MC68HC11 (COSMIC-France)
32 . include"macro .till"
228 Chapter 5 Programming Large 8-Bit Systems
3 .list +
4 .psect _text
5 ; 1 volatile char able;
6 ; 2 char baker ;
7 ; 3
8 ; 4 test (void)
9 ; 5 {
10 _test:
11 0000 3C pshx
12 0001 34 des
13 0002 30 tsx
14 .set OFST=l
15 ; 6 char dog;
16 ; 7
17 ; 8 dog = able;
18 0003 F60000 ldab _able
19 LL4:
20 0006 E700 stab OFST-l,x
21 ; 9 dog = able;
22 0008 F60000 ldab _able
23 LL6:
24 000B E700 stab OFST-l,x
2 5 ; 10 able = 0;
26 000D 7F0000 clr _able
27 LL01:
2 8 ; 11 able = 0;
29 0010 7F0000 clr _able
3 LL21:
31 ; 12 dog = baker;
32 0013 F60001 ldab _baker
33 ; 13 dog = baker;
34 0016 E700 stab OFST-l,x
3 5 ; 14 baker = 0;
3 6 ; 15 baker = ;
37 0018 7F0001 clr _baker
38 ; 16 }
39 001B 31 ins
40 001C 38 pulx
41 001D 39 rts
Header File 229
42
/ 17
43
.public _test
44
.psect _bss
45
_baker :
46
0000 .byte [1]
47
.public _baker
48
.psect _data
49
_able :
50
.byte [1]
51
.public _able
52
. end
Lines 17 throu
dog
= able;
dog
= able;
The memory location of able in the program is _able, and the
memory location for dog is at OFST- 1 , x. Note that the value in
_able is read and placed into dog twice as the program stated.
Also, the instruction clr _able is executed twice to account for
the two lines
able = 0;
able = 0;
This code is what you should expect when the volatile keyword is
used when able is declared. On the other hand, lines 31 through 34
show the compilation of the two C lines
dog = baker;
dog = baker;
In this case, you will note that the value for baker, stored at _baker,
is placed into the address of dog only once even though the code line
is repeated. Similarly, the two lines
baker = ;
baker = ;
result in assembly code that clears the location _baker only once.
This type of optimization will save you much code space. It is certainly
a problem whenever there is a memory location that can be changed
from outside the program. The volatile keyword will cause the
230 Chapter 5 Programming Large 8-Bit Systems
optimization operation to skip over all variables that can be changed
by the hardware portion of the system without knowledge of the
program.
Sorting Programs
You might not expect that sorting routines would be important when
programming microcontrollers, but there have been several instances in
my experience where sorting of a list of data was needed in a
microcontroller application. Therefore, let's examine some sorting
routines that can be used with a microcontroller and determine which of
several popular approaches is best for use in a microcontroller program.
There are several sorting routines that are popular with program-
mers. The three most-used routines are named the entry sort, the
Shell sort, and the quick sort. The entry sort is a routine that is thor-
oughly discredited because it is extremely slow when compared with
the other routines. An entry sort orders the contents of an array in
place. The first entry in the array is compared with the remaining
entries and, if any array entry is smaller than the first, these values
are swapped. This operation is then repeated for the second entry in
the array followed by the third entry in the array until all entries in
the array are compared with all others. When this sequence is com-
pleted, the array is sorted. If there are n entries in the array, the first
scan involves n compares, the second involves n-\ compares, the
third n-2 and so forth. Therefore, the total number of compares is
n+(n-l)+(n-2)+. . .1
The value of this sum isn*(n-l)/2.In other words, the entry sort
requires on the order of n 2 compares.
The Shell sort was discovered in 1959 by D. L. Shell. Like the
entry sort, the Shell sort orders the contents of an array in place. The
Shell sort splits the ^-element array into two halves. The contents of
these two halves are compared entry -by-entry and, if the value in the
one half is larger than that in the other, the entries are swapped. These
arrays are then split again, and the contents of the four arrays are
scanned and ordered two at a time as above. Then the four arrays are
split into eight arrays and the scan and ordering operation is repeated
again. This operation of splitting the arrays and ordering the contents
is repeated until there are n/2 arrays of 2 elements each. After this
last scan and sort operation, the array is ordered.
Sorting Programs 231
There are n/2 compares completed each scan of the array.
However, the sort will be completed after the logarithm based-2 of n
times through the array, because \og 2 n is the number of times that an
array that contains n entries can be cut in half. Therefore, the time to
execute a Shell sort of an array of n items will be nearly proportional
to n*log (n) . For even medium-sized arrays, this time is
significantly shorter than for the entry sort. For example, an array of
1000 entries requires time on the order of 1000000 to execute an
entry sort and 10000 to execute a Shell sort.
Another sort is the quick sort developed by C. A. R. Hoare in
1 962. This sort is a recursive routine. It sorts an array in place. Assume
that the array runs from left to right and we want to sort the array in
ascending values from left to right. An array scan will place at least
one of the array values in the proper final array location. This entry
is called the pivot entry. As the scan is completed, all entries larger
than the pivot value are swapped to the right end of the array and
smaller values are placed in the left end of the array. When this
operation is completed, the two portions of the array — excluding the
pivot entry — are again quick sorted and the result of this recursive
operation is that the final result is sorted. This sorting operation also
requires time proportional to n*ln (n) /2.
The question now arises whether we should use a Shell sort or a
quick sort when writing code for a microcontroller. Let's examine
these functions. The code for a Shell sort is as follows:
/* shell_sort (int v[ ] , int i) : sort the array v
of i things into ascending order */
void swap ( int *, int *) ;
void shell_sort (int v[ ], int i)
{
int split, m,n;
for( split = i/2 ; split > 0; split 1=2 )
f or ( m = split ; m < i ; m++)
for ( n = m- split; n >=0 &&
v [n] > v[n+split]; n -= split)
swap (v+n, v+n+split) ;
}
232 Chapter 5 Programming Large 8-Bit Systems
The outer loop in the preceding routine controls the way that the
array is split. It first splits the array in half and then shrinks the arrays
by halves until the sub arrays become zero in width. The second loop
steps along the elements of the arrays, and the third loop compares the
elements that are separated by split and swaps them when necessary.
The quick sort is:
/* quick_sort ( int v[ ], int left, int right) :
sort the array v into ascending order */
void quick_sort (int v[ ], int left, int right)
{
int i, pivot;
if (right > left)
{
swap(v+left, v+ (right+lef t) /2) ; /* chose the
value at center to pivot on */
pivot = left;
for (i=lef t+1 ; i<=right; i++)
if (v[i] < v[left] )
swap (v+ (++pivot) , v+i) ;
swap(v+left, v+pivot) ; /* put the partition
element in place*/
quick_sort (v, lef t , pivot- 1) ;
quick_sort (v,pivot+l , right) ;
}
}
The swap routine for both of these sorts is the same:
/* swap (int *, int *) : swap two integers in an
array */
void swap (int *i, int *j)
{
int temp;
temp = *i;
*i=*j;
* j =temp;
}
One of the nice things about both of these programs is that the swap
operation is not built into the program. Therefore, you can swap values
Sorting Programs 233
as was done above, but also, you can swap pointers from an array of
pointers to order a set of data that would be difficult to swap. For
example, if you had a collection of words, you could use a srt cmp ( )
routine to determine whether one word was lexically larger than the
other and then write a swap routine that merely swapped the pointers
to these words rather than swapping the words themselves. This
approach to sorting a list of words is relatively simple. On the other
hand, it would be extremely difficult — nearly impossible, in fact —
to sort a list of words that are packed into memory.
The following program is used to test the performance of the
above sort routines. The program simply makes an array of 1 1 entries
and sorts the array by both the Shell sort and the quick sort. The
result of this program is printed out below.
# include <stdio .h>
main ( )
{
int v[]={81, 99, 23, 808, 3, 77, 18, 27, 12 8, 360,550};
int i,k[ll] ;
for (i=0;i<ll;i++)
k[i]=v[i] ;
shell_sort (k, 11) ;
f or ( i=0;i<ll;i++)
printf ("%d ",k[i] ) ;
print f ("\n") ;
quick_sort (v, 0,11 -1) ;
f or ( i=0; i<ll; i++)
printf ("%d ",v[i] ) ;
printf ("\n") ;
}
The original array is not printed out by the program, but the order of
the array is seen in the above listing. The two sort programs sort the
values correctly.
3 18 23 27 77 81 99 128 360 505 808
3 18 23 27 77 81 99 128 360 505 808
The quick sort routine is not the best example of recursive
programming. Usually, the use of recursion results in a program that
is significantly shorter than would be expected with conventional
234 Chapter 5 Programming Large 8-Bit Systems
linear programming. Here, the Shell sort requires fewer lines of C
code and, as we will see, the Shell sort has a smaller assembly language
program. You would not expect the linear program to be shorter in
either C or assembly language. These two programs were compiled
and the results analyzed.
The Shell sort requires 0x78 (120) bytes of code while the quick
sort needs Oxaa (170) bytes of code. This amount of extra code would
not be a serious cost if that were the end of the costs. However, each
time a recursive routine calls itself it must establish an argument set
that contains all of the variables to be passed to the function. When
the quick_sort routine calls itself, it must pass three parameters.
These parameters are usually passed in either the stack or in computer
registers. When quick_sort returns control to the calling program,
the stack pointer must be corrected. quick_sort will continue
calling itself repeatedly while the value of the parameter right is
greater than the value of the parameter left when the function is
entered. Each time the function is called, the program will create a
new stack frame which must be made available from RAM.
Unfortunately, RAM is usually a more precious commodity than ROM
to a microcontroller.
It is possible to get an idea about how much RAM is needed to do
a quick sort. The stack frame consisting of six bytes — four bytes for
parameters plus two bytes for the return address — must be created
each time quick sort is entered. You can count the maximum number
of times recursive routine calls itself. The maximum depth is the
number of times the function is called before a normal return to the
calling function is executed. The depth is found by incrementing a
count each time the function is entered. Whenever control is passed
back to the calling function through the normal return sequence at
the end of the function, the depth will be evaluated to determine if it
is the largest value seen so far, and then the depth will be returned to
zero. The two listings below incorporate these modifications.
/* quick_sort ( int v[ ], int left, int right) :
sort the array v into ascending order */
void quick_sort (int v[ ], int left, int right)
{
int i, value;
extern int calls, depth, max depth;
Sorting Programs 235
calls++; /* count the number of times called */
depth++ ;
if (right > left)
{
swap(v+left, v+ (right+lef t) /2) ; /* chose the
value at center to partition on */
value = left;
for (i=lef t+1 ; i<=right; i++)
if (v[i] < v[left] )
swap (v+ (++value) , v+i) ;
swap(v+left, v+value) ; /* put the partition
element in place*/
quick_sort (v, lef t , value-1) ;
quick_sort (v,value+l , right) ;
}
}
if (depth>max_depth)
max_depth=depth ;
depth=0 ;
void swap ( int *, int *) ;
void quick_sort (int v[ ], int left, int right) ;
# include <stdio .h>
int calls, depth, max_depth;
main ( )
{
int v[ll] ={81,99,23,808,3,77,18,27,128,360,550};
int i ;
quick_sort (v, 0,11 -1) ;
f or ( i=0; i<ll; i++)
printf ("%d u ,v[i] ) ;
printf ("\n") ;
printf ( "calls=%d\nmax_depth=%d\n" , calls , max_depth) ;
}
Note that the global variables calls and depth are incremented
each time quick_sort ( ) is entered. When quick_sort ( ) is
exited through its normal return procedure at the end of the routine,
depth is examined to determine if it is greater than max_depth .
236 Chapter 5 Programming Large 8-Bit Systems
If it is, max_depth is replaced by depth, depth is reset to zero
prior to returning through the end of the quick_sort ( ) routine.
The result of this program is as follows:
3 18 23 27 77 81 99 128 360 550 808
calls=15
max_depth=4
To sort the above 1 1 element array, the program called
quick_sort ( ) 15 times and the maximum number of stack frames
that were created at one time was four. Each stack frame consists of
four bytes for data and two bytes for the return address. Therefore, at
one time in the execution of this program, the sort routine required
24 bytes of stack space in addition to the memory area required to
hold the array being sorted. This is distressing when you remember
that the array is 22 bytes long!
Careful analysis of the program will show that the expected
maximum number of stack frames for the quick sort is proportional to
ln 2 n. Therefore, the stack frame performance will get better when the
size of the array is larger. The main problem with the use of any recursive
routine is that you do not know exactly the number of stack frames
needed and therefore you must always analyze the maximum stack
frame depth and allow that amount of stack for execution of the function.
The quick sort is faster than the Shell sort under most — but not
all — circumstances. Let's take the example of having a quick sort and
a Shell sort compiled to run under the same host computer. If larger
arrays are used to do the measurement and random arrays are used, we
will find that the sort time for both quick sort and Shell sort depends
on the array. The reason for this difference is the number of swap
routines that must be executed during the sort operation. Therefore,
there will be some arrays that the Shell sort will sort quicker that the
quick sort. However, the quick sort is usually faster than the Shell sort.
The Shell sort is every bit as flexible as the quick sort and can be
used to sort different types, character strings, and swap pointers rather
than swap the objects being sorted. Therefore, in a microcontroller
application where memory is always limited, you should use a Shell
sort rather than a quick sort because the Shell sort has no funny
memory requirements that will always occur with a recursive routine
like the quick sort.
Data Compression 237
The purpose of the above discussion is to demonstrate that the
programmer must exercise much more care in the details of program
design for a microcontroller than is necessary for a larger machine. The
above sorting routine would be of no concern for a typical desktop
machine because RAM is usually of no consideration with such machines.
It might not be a problem for some microcontroller applications, but in
a case where RAM is limited and needed, it might make the difference
between being able to get the program to run or not.
In most instances, the elegance of recursive code has a cost of
RAM. Usually a recursive program will be slower than the equivalent
linear program. The microcontroller programmer must examine each
instance of code to determine if the elegant recursive program has
hidden side effects that end up costing more in computer resources
than is gained in small code size.
EXERCISES
1. Write a program to run on a host computer that will test the time
required to execute both a quick sort and a Shell sort on random
large arrays. Make the array sizes selectable from the keyboard.
2. Test both the quick sort and Shell sort for various array sizes. Ex-
amine the time required to execute the sorts for several random
arrays. Explain what is happening when arrays of 4090, 4095, and
4096 entries are sorted.
Data Compression
Another program problem that can arise with microcontroller
applications is handling displays in which there are many phrases
and names each consisting of several letters. The question that must
be considered is the most efficient method of storing these phrases
and names, in memory so that they can be recalled for display. The
oldest, and yet very effective, method of compressing the data for
storage is to use a Huffman code. This code is developed based on
the statistical occurrence of each letter in the alphabet. A letter that
occurs frequently will be given a short code sequence. A letter that is
seen infrequently can be assigned a long code, which will not
appreciably affect the number of bits per letter in the message.
238 Chapter 5 Programming Large 8-Bit Systems
Huffman codes are self-synchronizing. A long string of characters
can be represented as a series of bits, and the decoding program can
find each character in the sequence without any special flags to designate
the end of each character. Messages stored as ASCII characters require
at least 7 bits per character, and because of the nature of storage in a
computer, 8 bits are usually assigned to each character. With a Huffman
code, it is possible to reduce the average number of bits per character
for the whole English language to about 4.5 bits per character.
Sometimes even fewer bits are needed with limited messages.
The programmer does all of the Huffman encoding and the computer
must decode the messages for display. Huffman decoding requires extensive
bit manipulation, so C is an ideal language to decode the messages.
A Huffman decoding scheme can be represented by a strictly binary
tree. A binary tree is comprised of a collection of nodes. Each node has
two descendents, often called the right and left descendents (thus the
term binary tree). The terminating node in a tree is called a leaf. In a
strictly binary tree, every nonleaf node has both a right and left descendent.
If a strictly binary tree has n leafs, it has a total of 2n - 1 nodes.
The Huffman code is decoded in this manner. The sequence always
starts at the root node. The code sequence to be decoded is examined a
bit at a time. If the bit is a 0, move to the left node. If the bit is a 1,
move to the right node. The tree is traversed until a leaf is found which
contains the desired character. The sequence is then restarted at the
root node. If the tree is not known, the sequence of bits is bewildering.
Usually it is found that the occurrence of ones and zeros is about equally
likely, so no statistical analysis can be applied to decode the sequence
easily. A disadvantage is that if a single bit in the sequence is lost, the
remainder of the code stream is undecipherable. Of course, the
advantage to the Huffman code is that it takes somewhere between
one-quarter and one-half the number of bits to represent a message as
is required by the ASCII code. A Huffman Code tree is shown in Figure
5-1. The bit sequence 111111100101111010100000 as decoded by
this tree is seen to spell the word "committee." This sequence requires
23 bits, and the ASCII code needed to represent committee is 72 bits
long. Of course, this tree was designed specifically for the word
"committee," but try to encode other words that can be made of these
same letters such as "tic," "me," "come," "torn," "mite," etc., and you
will find that every word that can be derived from this tree requires
fewer bits than the corresponding ASCII sequence would need.
Data Compression 239
If you look at Figure 5-1, you will note that the letters e, m, and
t require only 2 bits each to determine the value. These letters were
selected to be represented by 2-bit values because they are the most
frequently occurring letters in the message. The remainder of the
letters require more bits but they do not occur so often in the message.
Figure 5-1: Huffman Code Tree
The code must be designed specifically for the messages to be
represented. First, all of the characters in all of the messages are
collected into a histogram of the occurrence of each character.
Remember to include spaces and other such characters in the list.
Once you have determined the number of different characters in the
list, you can design a tree that will compress the code. If the list
contains n different characters, there will be 2n - 1 nodes in the tree.
Each level in the tree, starting from the root node at the zeroth level,
can contain 2 k nodes or leafs. This tree cannot be balanced like a
binary sort tree because we want some of the leafs to be determined
by as few as 2 bits, and we do not care about the number of bits
needed to determine a character that occurs infrequently. In Figure
5-1, levels and 1 contain only nodes with no leaves. Level contains
three leaves and one node. It is through this single node that all of the
remainder of the characters must be found.
A series of avionics-related items were put together and an analysis
of the frequency of characters in these messages was completed. A
Huffman code that will encode these data effectively was created. This
240 Chapter 5 Programming Large 8-Bit Systems
code is shown in Table 5-1. Frequently occurring letters like e, i, or a
are encoded with only two or three bits. On the other hand, letters that
occur very infrequently — like c or v — require 9 bits to encode. Even
though some letters require more than 8 bits to encode, the average
number of bits per character for this particular message set is only 4.032.
Decoding a Huffman code with a program is relatively easy. The
program must contain the Huffman tree with all of its nodes and leaves.
If we want the entire alphabet and a space, a period, a comma and an
end of message, there will be 59 entries in the tree. This or an equivalent
tree is an overhead that must be carried for every Huffman program.
Assume that the statistical analysis of the messages along with the
construction of the Huffman tree is complete. The approach taken to
build this tree in memory is to allow one byte for each node. The tree
is searched from the root node, which is the lowest address. The message
to be decoded is examined. If the bit in the message is a zero, the node
pointer is incremented by one and the next bit is examined. If the bit in
the message is a one, the node pointer is incremented by the content of
the node. All printable characters are entered into the tree as negative
numbers. The contents of the node are examined. If the content of the
node is positive, the next bit from the message is examined and so
forth. Whenever the content of a node is negative, its most significant
bit is removed, and the character is sent to the output. At that time, the
node pointer is returned to the root node, and the sequence is repeated
until an end of message is found.
Character Code Character Code
E
00
D
110000
I
01
i i
110001
A
100
G
111100
'\n'
101
u
111101
N
11001
L
1 1 1 1 100
R
11010
O
1111101
T
11011
F
11111100
M
11100
H
11111101
S
11101
P
11111110
C
111111110
V
111111111
Table 5-1: Huffman Code
Data Compression 241
The listing shown below is a simple Huffman-encoded routine to
print out to the screen several aircraft-oriented terms that might be
used in an on-board avionics system. The first part of the function is
a list of the several words or phrases.
#include <stdio.h>
/* The messages to send out */
static unsigned char Ml [] ={0x9f , 0x3 6, Oxef , Oxdc, 0x0a} ;
static unsigned char M2 [] ={0xfd, 0x2 6, OxOe, 0x7c, OxaO} ;
static unsigned char M3 [] ={0xcl, Oxee, 0xe6 , 0x7f , 0xc6 ,
0x3a, 0x39, Oxlc, 0xb9, Oxf 2 , 0x80 };
static unsigned char M4 [] ={0xfc, Oxf 4, Oxf 9, 0x8e, 0x8e,
0x4 7, 0x2e, 0x7c, 0xa0} ;
static unsigned char M5 [] ={ 0x8e, Oxbl, Oxef, Oxf , 0x61,
0x40};
static unsigned char M6 [] ={0x73, 0x8f , Oxef, Oxdf , 0x75,
Oxda} ;
static unsigned char M7 [] ={0xdb, 0xc2, 0x80} ;
static unsigned char M8 [] ={0x3b, 0xb7, 0x93, 0x66, 0x18,
Oxed, Oxel , 0x8f , Oxdf , Oxcc , 0x66 ,
0xb4 , Oxf f , 0xe7 , Oxca} ;
static unsigned char M9 [] ={0xf3, 0x5f , 0x7d, Oxce, 0x18,
Oxf 7, Oxf 8, 0x3 0, OxaO} ;
/* The Huffman tree */
const static char
Node [] ={4, 2, X E' | 0X80,
x \n' | 0X80, 10, 6, 4, 2
X N' I 0X80, 2, X R' 0X80,
% I' 0X80, 4
D' | 0X80, x
T' | 0X80, 4,
X S'J0X80, 4, 2, X G'|0X80, X U' |0X80,
X L'|0X80, x O'|0X80, 4, 2, X F'|0X80,
X P'|0X80, 2, X C'|0X80, % V |0X80};
2, X A' 0X80,
0X80,
2, X M' | 0X80,
4, 2,
*W 0X80, 2,
void decode (unsigned char *M)
{
unsigned char mask = 0x80;
char i=0 , j =0 , k=0 , 1=0 ; /* j is the node pointer,
i is the byte pointer, M
is the message pointer */
while(k != , \n')
242 Chapter 5 Programming Large 8-Bit Systems
{
if ( (mask & M[i] ) ==0)
j++; /* use next node entry if bit is zero */
else
j +=Node [ j ] ; /* jump to designated node when
one */
if (Node [j] <0)
{ /* if a printable, send it out */
putchar (k=Node [1] &0x7f ) ;
j=0; /* also go to the root node */
}
if ( (mask>>=l) ==0) /* if the mask is zero, turn
on MSB */
{
mask = 0x8 0;
i++; /* and get the next byte from message */
}
}
Listing 5-1: Huffman Decode Function
The next part of the program is the Huffman tree needed to decode
the data. This tree was constructed to decode data specifically for the
nine messages of this problem. You will note that each byte in the
table is a node. Intermixed in the table are numbers that dictate jumps
to the next node depending on whether the incoming bit is a zero or
a one. The leaves each contain a negative number that is generated
by the inclusive OR of the character to be printed and the number
0x80.
The messages are sent to the function decode as a pointer to a bit
array. The data must be examined one bit at a time to implement the
decoding. The bit selection is accomplished by ANDing the data in
the incoming message with the contents of the variable mask. Mask
is initially set to 0x80, so the most significant bit of the first byte of
the incoming data is selected. Later in the routine, the value of mask
will be shifted right to select other bits in the incoming sequence.
The variable k is loaded with the character to be output during the
program. The linefeed character An' was used to determine the end
of message, so the decoding sequence will be executed until the
Data Compression 243
character that was output from the program is a An'. Recall the way
that the tree was built. If an incoming bit is 0, the next node in the
tree will be taken. If the incoming bit is 1, the content of the current
node will be added to the node pointer to determine the next node in
the tree. If at any time a negative number is found in the tree, a leaf
has been found. The data portion of this byte, bits through 6, will
be sent out to the screen.
After each bit is processed, the next bit in the sequence must be
processed. The next bit is selected by shifting the mask byte to the
right one bit. If the result of this shift creates a zero mask, a new
mask with a value of 0x80 is created and the next byte from the
incoming message code is selected when i is incremented.
The simple program below causes the nine messages to be printed
on the screen:
main ( )
{
decode (Ml)
decode (M2)
decode (M3)
decode (M4)
decode (M5)
decode (M6)
decode (M7)
decode (M8)
decode (M9)
}
Listing 5-2: Message Printing Program
The result of execution of this program is shown below. The nine
messages contain 124 characters that would require 992 bits of
memory to store the messages in standard ASCII format. The encoded
sequence requires 500 bits or 63 bytes of storage.
ALTITUDE
HEADING
DISTANCE REMAINING
FUEL REMAINING
AIR SPEED
244 Chapter 5 Programming Large 8-Bit Systems
IN HOURS
TIME
ESTIMATED TIME OF ARRIVAL
GROUND SPEED
The function below was written to show the difference in memory
required for the Huffman code and conventional ASCII coding. This
program was compiled as a function to be run on the MC68HC1 1 . The
function decode given in Listing 5- 1 was compiled to MC68HC1 1 code.
The result of these two compilations showed that Listing 5- 1 created an
object module that was 255 bytes long, and Listing 5-2 created an object
module that was 277 bytes long. The Huffman code for even the small
message list in this case provided nearly 10% reduction in code size. A
larger message list would provide an even greater memory savings
because the code required to decode the messages would be allocated
over more message characters to be sent out.
#include <stdio . h>
char Ml [
char M2 [
char M3 [
char M4 [
char M5 [
char M6 [
char M7 [
char M8 [
char M9 [
=" ALTITUDE" ;
=" HEADING" ;
="DISTANCE REMAINING";
="FUEL REMAINING";
="AIR SPEED" ;
="IN HOURS" ;
="TIME" ;
="ESTIMATED TIME OF ARRIVAL";
=" GROUND SPEED" ;
void decode (char *M)
{
while(*M !=0)
putchar (*M++) ;
put char ( x \n' ) ;
}
Listing 5-3: Nonencoded Output Function
EXERCISES
1 . How could the tree in Listing 5- 1 be altered to reduce its size by 8
bytes? What effect would this change have on the routine decode?
Would there be a net savings in overall code?
Timer Operations 245
2. Do an analysis of a substantial piece of writing and create a Huffman
code that will encode the data efficiently. It is recommended that
the first three pages of a novel be used for this analysis. Compute
the average number of bits per character that this code generates.
Hint: you might want to write a program in C for the host com-
puter to calculate the histogram and help create the Huffman codes.
3. Write a program that will implement the above code so that an
operator can type the data into a computer and the Huffman code
sequences will be generated. You should break the message se-
quences into moderate size bit strings, 500 to 1000 bits, and restart.
4. Create a Huffman tree table as was used in Listing 5-1 to decode
the Huffman code developed in Exercise 2.
5. How can you double the number of entries in a Huffman tree by
adding only one bit to the code strings?
Timer Operations
The programs written in the earlier sections on sorting and data
compression were more computer programs than microcontroller
programs. The code written would work on a desktop system or a
mainframe computer if needed. In this section, we graduate to true
microcontroller programming. The set up of the MC68HC11 family
requires that the programmer have a detailed knowledge of the operation
of the device. Even though the program is written in a high-level
language, it is the responsibility of the programmer to properly set all
of the necessary control bits to make the device work as desired.
Unfortunately, there are few helpful tools that can guide you through
this portion of the program. You must first understand what you want
the device to do and then dig through its specifications to find the
necessary bits to be set to make it perform as desired. It is highly
recommended that prior to an attempt at programming this device that
you familiarize yourself with the technical data manual for it as well
as the reference manual for the family that is found on the CD-ROM.
The timer subsystem in the MC68HC11 family contains both
input capture operations and output compare functions. In this section,
we will explore these subsystems associated with the MC68HC1 lEx
series. The main difference between the Ex series and the other devices
lies in the number of output compare and input capture systems on
246 Chapter 5 Programming Large 8-Bit Systems
each device. The Ex series has four output compares, three input
captures, and one timer that can be programmed as either output
compare or input capture. The other devices have three input captures
and five output compares.
Output Compare Subsystems
What does an output compare do? The explanation of the timer
subsystem must always begin with the 16-bit timer counter called TCNT
that is always counting at some fraction of the system bus frequency.
The bus frequency is always one-fourth of the system crystal frequency.
The prescaler frequency is set to the bus frequency divided by either 1 ,
4, 8, or 16 depending on the bits PR1 and PRO in the register TMSK2.
The timer counter register can be read at any time but it cannot be
written to. The Output Compare 1 is special. All of the discussion that
follows is valid for all output compares, but — as will be shown later —
Output Compare 1 has capability beyond the others.
Associated with each output compare system there is a single
16-bit output compare register containing the time that the program
needs an event to occur. When TCNT contents matches that of OCx,
an OCx event occurs. Note that I say an event, not output. What
happens when the contents of the TCNT matches the contents of OCx
is up to the program. Associated with each output compare is a pin
that can be set, reset, or toggled when an output compare occurs.
When an output compare occurs, the corresponding OCxF flag bit is
set in the TFLG1 register. This bit can be examined asynchronously
by the program to determine whether an output compare has occurred.
If the corresponding bit in the TMSK1 register is set when the OCxF
flag bit is set, an interrupt will be requested by the part and the event
will be processed asynchronously. These are the operations of all
output compare subsystems in the part.
There are three input captures, four output compares, and one
timer that can be programmed as either an output or an input. This
timer is controlled by the 1405 bit in the PACTL register. When this
bit is a zero, the programmable timer is an output compare. Otherwise
when the bit is set, the programmable timer is an input capture. Often
it is desirable to have an output compare operation be initiated by the
completion of another output compare. Output Compare 1 is set up
in just this manner. The bits in the 0C1M and the 0C1D registers
control the coupling between the Output Compare 1 and the other
Timer Operations 247
output compare subsystems. When one of the mask bits, say OC1M5,
is set in the OC1M and a compare occurs on OC1, in addition to the
normal operation that usually occurs when OC1 happens, the contents
of the corresponding bit, OC1D5, in the OC1D register is sent to the
output compare pin, which in this case is OC3. Completely
independent of the operation of OC1, OC3 could be set to toggle at
some time. Output Compare 3 could be programmed to be a PWM
output where Output Compare 1 establishes the period of the output,
and Output Compare 3 establishes the on time.
The applications for use of the coupled output compares are
unlimited. An accurate PWM is but one of several. These outputs
could be set up to establish an output sequence to drive a stepper
motor. The control of acceleration or deceleration of the motor is
easily controlled by merely selecting the base time used with OC1.
If you recall, the output compare-based PWM system discussed
for the MC68HC05 had several limitations. For example, that system
required that the system operate from interrupt service routines. The
latency time of interrupt service was so long that the minimum pulse
width was considerably longer than one would expect. Also, the
interrupt service timing dictated the maximum on time for the pulse,
which again was not nearly 100%. Let us look at how we would do a
PWM system with the coupled output compare systems.
#include "hclle9.h"
/* This program will provide a PWM output to OC3 , or
PA5 . The period will be the integer value
found in period, and the on time will be the
integer value found in time_on. Keep time_on
less than period. */
#define PERIOD 0X1000
#define TIME_ON 0x0800
WORD period=PERIOD, time_on=TIME_ON;
main ( )
{
OClM.OClM7=ON; /* sent OC1 out to PA7 */
248 Chapter 5 Programming Large 8-Bit Systems
0C1M.0C1M5=0N; /* couple OC1 to 0C3 */
0C1D.0C1D5=0N; /* turn on 0C3 when 0C1 occurs */
TCTL1.0L3=0N; /* toggle 0C3 when 0C3 occurs */
PACTL.DDRA7=0N; /* make 0C1 an output to PA7 */
TOCl=TCNT+period; /* set 0C1 to the period */
T0C3=T0Cl+time_on; /* set 0C3 to the time on */
FOREVER
{
if (TFLG1&0C1F)
{
TFLG1=0C1F; /* reset 0C1 interrupt flag */
TOCl+=period;
0C1D.0C1D7 ^=0N; /* toggle the output
Compare 1 bit */
}
if (TFLG1&0C3F)
{
TFLGl=0C3F;/*reset 0C3 interrupt flag */
T0C3=T0Cl+time_on;
}
Listing 5-4: Pulse Width Modulation Routine PWM.C
This routine starts with the inclusion of the header file he 1 1 e 9 . h.
It is created as a main program to demonstrate how it will work, but
should be changed to a subroutine later. The period and t ime_on
interval are declared as global variables. They are declared to be the
type WORD. WORD is a typedef synonym for the type unsigned
int . The main program begins with a series of five initialization
instructions. The first two instructions
0C1M.0C1M7=0N;
0C1M.0C1M5=0N;
declare that the output of OC1 should be sent to the outside, which
will be to pin PA7, and that whenever OC1 occurs, OC3 through pin
PA5 should be activated.
Timer Operations 249
The instruction
0C1D.0C1D5=0N;
indicates that when OC1 occurs, OC3 should be turned on. The next
instruction
TCTL1.0L3=ON;
causes OC3 to toggle when its time expires, and the instruction
PSCTLl.DDRA7=ON;
sets the DDRA7 bit so that signals from OC1 will be sent to the output
pin PA7 . TOC1 and TOC3 are initialized by the next two instructions.
TOC1 is given a value of the contents of period greater than the
contents of the timer counter register TCNT. TOC3 is set equal to the
contents of TOC1 plus the time_on.
The routine then enters an endless loop. Within this loop, two
flags are examined and if they are set, they are reset. Also, new times
are calculated for when the next output compares should occur. The
OC1F flag in TFLG1 is set whenever an Output Compare 1 occurs.
This flag must then be reset. The instruction sequence
if (TFLG1 & OC1F)
{
TFLG1 = OC1F;
will accomplish the required assembly instructions to test the bit and
reset it if the bit is turned on.
The time of the next OC1 is calculated as TOCl+period. In
this particular instance, the bit OC1D . OC1D7 is complemented so
that the output observed on PA7 will toggle with the occurrence of
each OC1. TFLG1 . OC3 F is tested to determine if an Output Compare
3 has happened. If it has, this bit is reset, and TOC3 is set to a new
value of TOCl + time_on. With this setup, OC3— PA5— will go on
when each OC1 occurs, and will be reset an amount corresponding
to time_on after it goes on. Therefore, the period of this system
can be set and the on time can be any value less than the period.
Normally, the operation of a PWM is to generate an analog signal
that is present continuously. The above program does perform this
250 Chapter 5 Programming Large 8-Bit Systems
task, but it also assumes that the computer does not have much else to
do. Other tasks could be built into the above program, but it is absolutely
necessary that the program have a cycle time that is shorter than the
period of the PWM signal. If the loop time of the FOREVER loop is less
than the PWM period, then you can use the above synchronous approach.
It makes no difference when in the cycle that the two if statements in
the FOREVER loop are executed. It is necessary that they be executed
prior to the occurrence of the OC1 that designates the end of the period.
If it becomes necessary to include so much code in the FOREVER loop
that it is impossible to guarantee that the loop time will always be less
than the period, then an asynchronous approach should be used. This
program was compiled and executed on an evaluation module. The
value of t ime_on was adjusted to determine the range of outputs that
could be created with this program. The minimum time on must be
greater than the time required to execute the code
if (TFLG1&OC3F)
{
TFLGl=OC3F; /* reset OC3 interrupt flag */
TOC3=TOCl+time_on;
}
when OC3F is found on. The reason for this timing is that TOC3
must be updated after TOC1 is given a new value and the update
must be complete prior to the passing of time_on . Otherwise, the
period of TOC1 will pass before the OC3 will occur. This time was
measured, and it was found to be 6 clock cycles. Therefore, reliable
performance is obtained with a minimum time on of 6 clock cycles.
The maximum time on was found to be Oxf f e with the above
code. The output signal at Oxf f f was on all of the time with no
single cycle off period as the program would indicate.
An interrupt can be requested whenever an output compare occurs.
We are servicing two output compares in this case. One's initial
thought might be to have two interrupts, in this case one for OC1 and
one for OC3. Is this approach really necessary? If it is guaranteed
that the time_on parameter is always less than period, then an
approach that can be used is to delay the reset of the OC1 flag until
the OC3 has occurred. OC3 will always happen after OC1. The problem
with this approach is that OC3 occurs very near the end of the period,
and there might not be enough time to reset the OC1 interrupt flag
Timer Operations 251
prior to the expiration of the period. The following program can be
used to demonstrate this approach:
#include "hclle9.h"
/* This program will provide a PWM output to OC3 , or
PA5 . The period will be the integer value
found in period, and the on time will be the
integer value found in time_on. Keep time_on
less than period. This program uses asynchro-
nous service of output compare occurrences. */
WORD period=0xl000, time_on= 0x0800 ;
©port void OC3_Isr (void) ; /* need a prototype for the
ISR */
main ( )
{
0C1M.0C1M7=0N; /* sent OC1 tout to PA7 */
0C1M.0C1M5=0N; /* couple OC1 to OC3 */
TMSK1.0C3I=ON; /* enable the OC3 interrupt */
0C1D.0C1D5=0N; /* turn on OC3 when OC1 occurs */
TCTL1.0L3=ON; /* toggle OC3 when OC3 occurs */
PACTL.DDRA7=ON; /* make OC1 an output to PA7 */
TOCl=TCNT+period; /* set OC1 to the period */
TOC3=TOCl+time_on; /* set OC3 to the time on */
cli(); /* enable the system interrupts */
FOREVER
{ /* wait here forever */
}
}
©port void OC3_Isr( void)
{
TFLG1=0C1F; /* reset OC1 interrupt flag */
TOCl+=period;
OC1D.OC1D7 ^=ON; /* toggle the output */
TFLG1=0C3F; /* reset OC3 interrupt flag */
TOC3=TOCl+time_on;
}
Listing 5-5: System Using Asynchronous Time Service PWM1.C
252 Chapter 5 Programming Large 8-Bit Systems
Notice that this code is very similar to the earlier program shown
in Listing 5-4. The interrupt service handling is set up by use of the
©port construct and the two added instructions which first enable
the OC3 interrupt and then enable the system interrupts. The code
that was contained within the FOREVER loop in Listing 5-4 has been
moved into the interrupt service routine in this program.
This program was run, and it was experimentally determined that
the maximum value that can be allowed for time_on is Oxf f
when period is Oxl 0. This maximum value indicates that 16 clock
cycles or 8 microseconds at an 8-MHz crystal is needed to service
the interrupt prior to the occurrence of an OC1. The minimum on
time found here is slightly better than found with the code in Listing
5-4. In this case, the minimum on time is 1 clock cycle, which is the
expected minimum value.
We have already seen that a significant time is required to process
an interrupt, and it is usually impossible to have an output event occur
during the interrupt service routine. With the above code it is easy to
have a minimum on time of one clock cycle. This small time is
accomplished by having the setting of the output signal not be attended
by an interrupt. The interrupt will occur when the output signal is
reset. Therefore, when the coupled output signal with OC1 goes high,
the output compare on the coupled channel will have its event even if
the time is as short as one clock cycle beyond the occurrence of OC1.
Assume that the coupled channel is OC3. When the event occurs on
OC3, an interrupt will occur. In this interrupt service routine, both
OC1 and OC3 will have to be set up to operate in the correct manner.
This operation has a problem with long on times. If the event associated
with OC3 occurs a few clock cycles prior to the occurrence of the OC1
event, and the interrupt is caused by OC3, then the OC1 set-up might
not be completed when the next OC1 time arrives. In this case, the
whole base period would go out of kilter, and there would be at least
one cycle of the output that would be based on the timer overflow
cycle rather than the desired time base.
As is often found in engineering operations, there is a choice that
can be made. If the interrupt is based on the reset time of the PWM
cycle, then a minimum on period of one cycle can be achieved. With
this choice of operation, the maximum on time will be several clock
cycles — perhaps 20 to 30 — short of 100% on. On the other hand, if
the interrupt is based on the set time of the PWM cycle, the minimum
Timer Operations 253
on time is poor and the maximum on time can be up to one clock
cycle shy of the base period. If you wish to have good performance
at both ends, minimum on time at the same time as maximum on
time, you can examine the on time in your program and if it is less
than 50% of the base time period, control the PWM by an interrupt
on reset. Otherwise, control the PWM by an interrupt on set.
Implementation of the code for this approach is left to the exercises.
Output compare operations OC2 through OC4 and the
programmable timer subsystem 1405 are all identical. These versatile
timers can be used to generate complex timing waveforms that are
useful in keeping time, running stepper motors, etc. The examples
shown above demonstrate how these outputs can be converted into a
PWM digital to analog output. The coupling between OC1 and the
other outputs provides an excellent mechanism for synchronizing
two time events for the outside circuitry. Of course, these subsystems
can create events in time, but they are completely unable to measure
time. We will later examine the analysis of timed events that can be
measured with the help of the input capture subsystems.
EXERCISES
1 . Write the code that will determine from the on time for the PWM
the interrupt operation that will allow the minimum off and maxi-
mum on time simultaneously.
2. Modify the code in Listing 5.4 to eliminate the need for a condi-
tional test if (OC1D.OC1D7 = = 1).
Input Capture Subsystems
On the MC68HC1 1EX family, there are three input capture timers
and one timer that can be programmed as either an input capture or
an output compare. These sub-systems are used to measure time
interval. The same 16-bit timer used in the output compare systems
is used to support the input captures. When an input occurs on one of
the input capture pins, the value contained in the 16-bit timer is saved
in a register designated for that pin, a flag is set, and the processor
can request an interrupt. With input captures, we can measure period
and hence frequency (or speed). One of the leading applications where
the input capture is used is in automatic braking systems. We shall
254 Chapter 5 Programming Large 8-Bit Systems
choose a somewhat simpler system for an example, but the general
approach used here is much the same as would be used in the design
of an automatic braking system.
Suppose that we have a DC motor that is being controlled by an
MC68HC 1 1E9 and wish to be able to set the speed of the motor. The
motor has a magnetic sensor whose output will cycle once each
rotation of the motor. To determine the speed of the motor, we must
measure the cycle time of the output from the sensor. Let us examine
the minimum speed of the motor. The TCNT register is being clocked
at a frequency that is either one-fourth of the crystal frequency of the
system or it can be altered by a prescaler value of either 4, 8, or 16.
The 16-bit TCNT register will overflow every 65,536 clocks at its
input. Therefore, if we use an 8-MHz clock, the timer overflow period
will be 32.768 ms with no prescaler, 131.072 ms with a prescaler
value of 4, 262.144 ms with a prescaler value of 8, or 524.288 ms
with a prescaler value of 16. The minimum speed at which the motor
can move and still be detected unambiguously must be greater than
one revolution in the timer overflow time. If the motor rotates any
slower than this value, the differences in times between two inputs
will be such that the program cannot tell if the period is longer than
one TCNT overflow time or a very short time. It is possible to extend
the minimum unambiguous time that can be measured by the system
through the use of a timer overflow interrupt, but let's examine an
approach without these steps and then look at the time extended
approach later.
The maximum practical time to be measured with the divide-by- 16
prescaler is 500 milliseconds. The minimum rotation speed must be
greater than 1 revolution per 500 milliseconds or 120 revolutions per
minute. The motor that we will use in this example has a minimum
speed of 1000 rpm, so that the prescaler need not be as great as 16. We
will use a prescaler value of four, which will provide about a minimum
speed of 500 rpm so that there is a safety factor in our measurement.
With the divide-by-four prescaler, the minimum time that can be
measured is ( 4 [crystal to bus frequency division]* 4 [prescaler divide
ratio ]/ 8 MHz [clock frequency] ), which is 2 microseconds. At this
time interval, the maximum speed of the motor shaft that can be
measured is one revolution in 2 microseconds, or 500000 * 60
revolutions per minute. It is quite clear that such a system is better
suited to handle high-speed operations than low speed. If there is no
Timer Operations 255
clear reason otherwise, it is usually best to operate a control system
at the maximum possible speed. One reason that is important is the
operation of the PWM DAC that accompanies the operation of the
control system. It is usually best to have a PWM run at the highest
practical speed. If the PWM is slow, then conversion of the pulse
output from the system to a DC voltage is difficult and not very
accurate on an instantaneous basis.
The following code segment might be used as an interrupt service
routine to handle the input capture operation.
©port void ICl_Isr( void)
{
TFLG1=IC1F; /* reset IC1 interrupt flag */
measured_period=TICl-timel ;
timel=TICl;
}
Here it is assumed that the maximum time between input captures is
less than the time of a timer overflow. In this case, the time is merely
the difference between the current value and the preceding value
which is stored in time 1.
This approach has only one major problem. Most inputs such as
will be obtained from reed switches, push buttons, and even optical
interrupt type devices will be noisy when the contact is closed. This
noise is called switch bounce, and it will always be present with a
contact closure. Therefore, the switch must be debounced in some
manner before its data are reliable.
The most common way of debouncing a contact closure is to
observe the closure, and then wait a time and observe if the contact is
still closed. If closed, it is assumed that the contact is good; otherwise,
another wait period in allowed to elapse and the contact is observed
again. This procedure is repeated until the contact is closed on a pair
of successive observations. Only then is it assumed that the contact is
closed. The time between observations is the subject of much
engineering debate. Often the designer can place an oscilloscope on
the contact and repeatedly close and open it. With proper
synchronization of the instrument, it is possible to see the signal caused
by the bouncing contacts. If this time can be measured, then having
a debounce time of perhaps twice the bounce time will probably give
a safe time for the debounce. However, you should not assume that
256 Chapter 5 Programming Large 8-Bit Systems
this value will be correct over the lifetime of the contact. As the
mechanism ages, it is possible that the bounce time will change, and
it will always change in the worst possible way. Therefore, be safe,
and keep the debounce time at least twice the bounce time, and perhaps
longer for the sake of safety. The range of values used for debounce
times ranges from 2 to 30 milliseconds.
This system is measuring the time of an input. How can we stick
an arbitrary time for debounce into our measurement equation and be
reliable? It is possible to do the debounce and not have the time as part
of the measured interval. The contact closure will cause an observable
input. Our real concern with debounce is to guarantee that no additional
inputs occur after the initial input has been processed. Therefore, we
can implement a debounce by merely not re-enabling the input capture
interrupt until the debounce time has passed. This way, the time of the
first edge seen in the input sequence is processed, and all other inputs
caused by bounce will not be processed because the input capture
interrupt is disabled. The time that the interrupt is disabled can also be
adjusted to meet the particular needs of the program. For example, in
the system we will construct below, the speed is measured with a reed
switch and a magnet on the motor shaft. The circuit will be built such
that the closing of the switch will cause a voltage to fall from +12 volts
to volts. There is a resistor between the 12- volt supply and the switch.
The bottom side of the switch is grounded, and as the switch is closed
and opened, the voltage measured at the top of the switch will change
from 12 volts to ground. When the switch closes, it is expected that it
will bounce and the voltage will "chatter" between and 12 volts.
Surprisingly, the opening of the switch, while it will exhibit less bounce,
will also generate these unwanted signals. If we want to block out all
of the error signals that can be introduced by the bounce, it is necessary
that the time following the initial signal drop be protected, as well as a
time around the signal rise that occurs midway during the shaft rotation.
In the first example, we will provide a fixed time for the debounce.
Later when the program is measuring the speed of the motor, we will
provide a debounce time that is somewhat longer than one half the
measured period to block out all bounce signals that occur on both the
rising and falling edge of the input signal.
The input capture subsystem will capture any specified input when
enabled. These inputs are captured whether the interrupts are enabled
or not. In other words, if an input occurs after the one that was captured,
Timer Operations 257
it will be captured and the original saved value will be lost. Therefore,
the very first operation in the interrupt service routine for an input
capture should be to save the contents of the input capture register.
Then subsequent inputs from bounces will not affect the measured
time. Another problem can occur, however. Whenever an input occurs
on an input capture line, not only are the time data captured, but also,
the appropriate input capture interrupt flag is set. If this flag is set, an
interrupt will be requested whenever the input capture interrupt is
enabled. Therefore, in the debounce interrupt routine, the input capture
interrupt flag for the channel being used should also be reset.
We still must control the time that the input capture is disabled. How
this control is implemented depends on the system. If the system is not
busy, one might calculate a value that equals the contents of the TCR
plus the number of timer counter ticks in the debounce time. This value
could then be compared with the contents of the TCR, and when the
TCR equals the calculated value, the input capture interrupt would be
reenabled, and control returned to the interrupted program. This approach
provides an accurate debounce time, but the processor is devoted entirely
to the measurement of this time during the debounce time. It is not wise
to tie up a microcontroller for milliseconds at a time and lock out other
important actions that might take place during that time.
If all other events that are occurring within the microcontroller
are interrupt driven, the programmer could re-enable the system
interrupts prior to entering the delay time. This approach would at
least keep the processor available for other asynchronous events that
might occur during the debounce period. Yet another approach would
be to disable the input capture interrupt and within the applications
program, provide a time measurement that would have to expire before
the input capture interrupt is reenabled. Both of these methods can
be implemented without the use of an output compare. If an output
compare is available, it could be used to execute the debounce timeout.
This output compare would be set up in the input capture interrupt
service routine. Also within the input capture interrupt service routine,
the input capture interrupt would be disabled. The output compare
interrupt service routine would disable the output compare operation
and reenable the input capture. This approach is by far the best, and
will be used here. The code for this method of debounce is included
in the listing shown in Listing 5-6. In this case, a fixed debounce
time is used. We will see the variable debounce time in a later program.
258 Chapter 5 Programming Large 8-Bit Systems
A beginning program or framework from which to build this
application is shown in Listing 5-6. All this program contains is the
setup of the interrupts and the interrupt service routines. Input capture
1 will serve as input from the motor shaft encoder. The motor will be
controlled by a PWM signal. This signal will be filtered to provide a
DC signal that can drive the motor. As with the MC68HC05 programs,
this code is broken into three basic parts: 1) the initialization section,
2) the applications section, and 3) the asynchronous service section.
In this case, there is no applications section, so it is designated by a
FOREVER command followed by an empty block.
The program begins with the inclusion of the HC 1 1 E 9 . H header
file. Immediately following this entry are the prototype entries for
all of the interrupt service routines. The declarations of the global
variables follows. The first set of variables are all associated with the
input capture, and the second line of variables controls the output
compare portion of the program.
Notice that the bit set-up instructions are all grouped so that the
bits from each register are set in the same location. It is not necessary
to group these instructions; however, if they are grouped by register
as is done here, the compiler will use a single bit manipulation
instruction to set all of the bits in each register. The first 8-bit
manipulation instructions in the following code will require only six
assembly instructions as they are grouped.
#include u hclle9.h"
#define MS3_DEBOUNCE 1500
#define PERIOD 0X1000
#define TIME_ON 0x08
©port void ICl_Isr (void) ;
©port void OC2_Isr (void) ;
©port void OC3_Isr (void) ;
WORD measured_period / timel, delpc;
WORD PWM_period=PERIOD, time_on=TIME_ON;
main ( )
{
TCTL2 .EDGlB=ON;/* capture falling edge only */
Timer Operations 259
0C1M.0C1M7=0N; /* sent 0C1 tout to PA7 */
0C1M.0C1M5=0N; /* couple 0C1 to 0C3 */
TMSK1.0C3I=0N; /* enable the 0C3 interrupt */
TMSK1.IC1I=0N; /* enable the IC1 interrupt */
0C1D.0C1D5=0N; /* turn on 0C3 when 0C1 occurs */
TCTL1.0L3=0N; /* toggle 0C3 when 0C3 occurs */
PACTL.DDRA7=0N; /* make 0C1 an output to PA7 */
TOCl=TCNT+PWM_period; /* set 0C1 to the period */
T0C3=T0Cl+time_on; /* set 0C3 time on */
cli(); /* enable the system interrupts */
FOREVER
{
/* put application code here */
}
}
©port void ICl_Isr( void)
{
timel=TICl;
TFLG1=IC1F; /* reset IC1 interrupt flag */
measured_period=TICl-timel ;
TOC2=TCNT+MS3_DEBOUNCE; /* 3 ms debounce time */
TMSK1.IC1I=0FF; /* disable IC1 interrupt */
TMSK1.0C2I=0N; /* enable 0C2 interrupt */
©port void 0C2_Isr (void)
{
TFLG1=IC1F|0C2F; /*reset IC1 & 0C2 interrupt flag */
TMSK1.0C2I=0FF; /* disable 0C2 interrupt */
TMSK1.IC1I=0N; /* enable IC1 interrupt */
}
©port void 0C3_Isr( void)
{
TFLG1=0C1F; /* reset 0C1 interrupt flag */
TOCl+=PWM_period;
0C1D.0C1D7 ^=0N;
260 Chapter 5 Programming Large 8-Bit Systems
TFLGl=OC3F; /* reset OC3 interrupt flag */
TOC3=TOCl+time_on;
}
Listing 5-6: Motor Control Program Framework
The interrupt service routine for I CI resets the IC1F bit in the
TFLG1 register. Then the elapsed time from the last input capture time
is calculated. The result of this calculation is saved in periodl which
is the current elapsed time required for the motor to rotate one revolution.
At the end of this calculation, the contents of the input capture register is
saved in t imel to be used as the old time in the next calculation.
The PWM output from OC3 will be used to drive motor 1 whose
shaft sensor is fed into IC1. The value in speed will be the desired
speed of the motor in revolutions per minute. The range of this number
will be 1000<=RPM<= 10000, and it will have to be put in place through
a debugger for this test. This range of motor speed was chosen to sat-
isfy the speed of a small DC motor used in the final system. Let us plan
a servo type operation where the motor is driven by a feedback loop
that is controlled by the speed error. Therefore, within the applications
loop, there must be code to check and control the PWM signal to force
the speedl parameter to match the input speed parameter.
The input speed is measured in RPM, and the values measured by
the microcontroller are all times. A question that should be considered is
whether the number placed in speedl will actually be times or should
this number be RPM. To calculate the time, note that there is one interrupt
per revolution of the shaft and one minute is 60 seconds, so RPM/60 will
be revolutions per second. What we need is the number of bus cycles in
the time corresponding to this period. With a prescaler count of four, the
clocking speed of the TCNT register is one count every two microseconds,
or 500,000 counts per second. Therefore, the conversion from RPM to
time for a revolution of the shaft in bus cycle counts is
500000 * 60 30000000
Counts = =
RPM RPM
At 3000 RPM, the count value will be 10000 while at 16000
RPM the count value will be 1875. These numeric values can be
handled by the timer system in the MC68HC1 1.
The above expression can be used to calculate RPM from the
count value as
Timer Operations 261
„„ w 30000000
RPM =
Counts
Neither calculation is convenient in a microcontroller. The numerator
is larger than an int but smaller than a long. Both Counts and
RPM have a range that can be contained in an int . One would expect
that the conversion to time should be done on the input speed. That
way, the division would be done only once for each speed setting. If
the measured times were converted to RPM each time a time were
measured, the computer program would be loaded with complicated
divisions in its real time application portion. But let's examine the
nature of the error with the different types of measures. Suppose the
input RPM were converted to time and the measurements were based
on time. Then the error signal, which is the difference between the
measured value and the desired value, would have a wrong sense.
That is if the motor is moving too slowly, then the error signal would
be negative, which would cause the motor to go even slower. If the
calculation were done based on RPM and the motor were going too
slow, the error signal would be positive. In this case, the control would
drive the motor faster which is the needed correction.
The difficulty with the time-based system could be solved by
using the negative value of the error signal for the feedback control.
A simple analysis will show the potential problem with this approach.
Suppose that we work with two counts, C d and C . Let us use the
value K for the constant in the above equation. Therefore,
e = C d -C m =K
( 1 1 ^
RPM, RPM
\ a mi
The error signal is seen to be
e = K
r RPM m -RPM d ^
RPM d *RPM m
When the two speeds are nearly the same, this expression is very
nearly proporstional to the difference between the two speeds. However,
when one of the speeds deviates significantly from the other, it will
cause the resultant error signal to be less sensitive to difference than
would be expected. Also, there is the problem that occurs if the motor
is stopped and the error signal in that case is undefined.
262 Chapter 5 Programming Large 8-Bit Systems
If the counts are converted to RPM prior to calculation of the error
signal, the reduction of sensitivity for large errors would not be a
problem. The problem that occurs when the motor is stopped still exists.
In this case, no input capture would occur when the motor is not rotating,
and the count value would be undefined. There seems to be no clear-cut
reason to choose the time or the velocity measurement. Each has
advantages and each has drawbacks. The drawbacks are about the same
for each, so we will choose the case that has the simplest program.
Therefore, the time-based or count-based system will be used.
Now comes the interesting problem of the calculation of 3000000/
RPM. Each time the desired speed of the motor is input to the system,
this calculation must be completed. The straightforward calculation
of this value will yield code as follows:
#include "hclle9.h"
WORD count (WORD RPM)
{
unsigned long num = 30000000;
return num/RPM;
The listing file of the compiled version of this function is
1 ; Compilateur C pour MC68HC11 (COSMIC-France)
2 . include"macro . hll"
3 .list +
4 .psect _text
5 ; 1 #include "hclle9.h"
6 .psect _data
7 _Register_Set :
8 0000 1000 .word 4096
9
10
11
12
3 WORD count (WORD RPM)
4 {
.psect _text
13 _count :
14 0000 BD0000
jsr c kents
Timer Operations 263
15 0003 0C .byte 12
16 .set OFST=12
17 ; 5 unsigned long num = 12 0000000;
18 0004 CC0E00 ldd #3584
19 0007 ED0A std OFST-2,x
20 0009 CC0727 ldd #1831
21 000C ED08 std OFST-4,x
22 ; 6
23 ; 7 return num/RPM;
24 000E EC0C ldd OFST+0,x
25 0010 6F02 clr 2,x
26 0012 6F03 clr 3,x
27 0014 ED06 std OFST-6,x
28
0016
EC02
ldd 2,x
29
0018
ED04
std OFST-8,x
30
001A
EC08
ldd OFST-4,x
31
001C
ED02
std 2,x
32
001E
EC00
ldd 0,x
33
0020
C3FFF7
addd #-9
34
0023
188F
xgdy
35
0025
EC0A
ldd OFST-2,x
36
0027
BD0000
jsr c_ludv
37
002A
AE00
Ids 0,x
38
002C
3 8 pulx
39
002D
39 rts
40
; 8 ]
>
41
; 9
42
.public _count
43
.public _Register_Set
44
. external
c_kents
45
. external
c_ludv
46
.end
This function requires 0x2d bytes (45 bytes) of code and that
does not count the functions c_ludv and c_kents that must be
linked to this function. Our innocuous little one-line piece of code
creates a rather formidable piece of assembly code. If at all possible,
it would be desirable to shorten this function. A slight modification
of the above function code is
264 Chapter 5 Programming Large 8-Bit Systems
#include u hclle9.h"
WORD count (WORD RPM)
{
return 3 00000001u/RPM;
}
The compiled version of this program is
1 ; Compilateur C pour MC68HC11 (COSMIC- France)
2 . include"macro . hll "
3 .list +
4 .psect _text
5 ; 1 #include "hclle9.h"
6 .psect _data
7 _Register_Set :
8 0000 1000 .word 4096
9 ; 2
10 ; 3 WORD count (WORD RPM)
11 ; 4 {
12 .psect _text
13 _count :
14 0000 BD0000 jsr c_kents
15 0003 08 .byte 8
16 .set OFST=8
17 ; 5 return 1200000001u/RPM;
18 0004 EC08 ldd OFST+0,x
19 0006 6F02 clr 2,x
20 0008 6F03 clr 3,x
21 000A ED06 std OFST-2,x
22 000C EC02 ldd 2,x
23 000E ED04 std OFST-4,x
24 0010 CC0727 ldd #1831
25 0013 ED02 std 2,x
26 0015 EC00 ldd 0,x
27 0017 C3FFFB addd #-5
28 001A 188F xgdy
29 001C CC0E00 ldd #3584
30 001F BD0000 jsr c ludv
Timer Operations 265
31
0022 AE00
Ids 0,x
32
0024 38
pulx
33
0025 39
rts
34
; e }
35
; 7
36
.public
count
37
.public
Register_Set
38
. external
c_kents
39
. external
c_ludv
40
.end
Note that this version differs from the first only in that the number
30000000 is not created in a declaration, but rather is created in line
when it is needed. This version requires 37 bytes of code.
The purpose of this exercise is to demonstrate that there are usually
many different ways that any part of a program can be approached.
If the programmer grabs the first idea and plods through without any
careful examination of the code being generated by the compiler, the
program will almost always suffer. The assembly listings of programs
should be examined, and where it seems as if the compiler is creating
clumsy code, a new approach should be considered. Writing C code
for a microcontroller is a joint exercise by the programmer and the
compiler to create the most efficient overall program.
Let's return to the problem: we wish to set the speed of a motor and
the measurable control signal is the time of a revolution. Most servo type
devices work to position an output. Ours must set a speed which is a
different problem from most. The input will be an RPM which will be
converted to a time. Our system must compare the desired time with the
measured time and correct the speed to make the two times match. A
signal to drive the motor will be generated based on the desired time,
and this signal will be adjusted by the error signal calculated as the
difference between the measured time and the desired time. The following
pseudocode sequence will accomplish the desired operation.
FOREVER
{
do
convert to time;
calculate the required PWM output count;
apply calculated time error to PWM count;
266 Chapter 5 Programming Large 8-Bit Systems
read in the motor speed;
while ( motor speed does not change) ;
For the moment, let us defer the problem of getting the desired motor
speed. This value will be converted to time by use of the count ( )
routine and will be saved in motor_period. Next, we need to
calculate the required motor voltage. For this calculation, let us revert
to an experimental technique that can be used usefully. An open loop
system was set up to test the operation of the motor system. Several
input values were entered, and the performance of the motor was
recorded. Table 5-2 shows the result of these measurements.
PWM_Count
PWM_Count
icap period
period ms
voltage
RPM
Hex
decimal
counts
0x700
1792
15812
31.62
2.186
1897
0x780
1920
11405
22.81
2.341
2630
0x800
2048
8581
17.17
2.498
3494
0880
2176
6841
13.68
2.653
4385
0x900
2304
5616
11.23
2.809
5342
0x980
2432
4759
9.52
2.964
6304
OxaOO
2560
4059
8.12
3.118
7391
0xa80
2688
3530
7.06
3.273
8499
OxbOO
2816
3085
6.17
3.424
9724
0xb80
2944
2736
5.47
3.578
10965
Table 5-2: Motor Performance Measurements
The data for this table were gathered by the use of an
M68HC1 1EVM and a simple motor driver. This driver is a
demonstration board provided by Motorola to show the use of the
MC33033 pulse width motor driver and the MPM3002 FET motor
driver H bridge. A small DC motor with a speed range of 1000 to
1 1000 rpm was mounted on the board, and the appropriate circuitry
was added to interface the board to the computer. This interface
consisted of a simple RC integrator to receive the PWM signal from
the computer board and a circuit to measure the rotation of the motor.
This latter circuit was quite crude. A magnetic reed switch was
mounted near the motor shaft and a magnet cemented to the shaft.
For each rotation of the shaft, the reed switch would close and open
one time. A resistor was connected between the 12 volts of the motor
driver and one side of the switch; the other side of the switch was
Timer Operations 267
grounded. Therefore, during each rotation the top side of the switch
would jump from 12 volts to volts and back. This voltage is larger
than the maximum input for the microcontroller, so the 12 volt square
wave was clamped to a maximum high value with a resistor and a
diode connected to the +5 volt power supply of the EVM1 1 board.
The schematic diagram for this test setup is shown in Figure 5-2 and
a photograph of the complete system is shown in Figure 5-3.
M6811C11EVM
MC33033 - MPM3002 Motor Drive
33033
OC1
OC3
PWM Output
33k
Ping
7W
= 1u/
+5V 12V
A 33k^
f — '
IC1
Time-Input
\AA
i
.01 =
VVV
8.2k
^ ^>
e
f 7
Magnet
Switch
—
Figure 5-2: Schematic Diagram of Motor Driver Test Circuit
Figure 5-3: Photograph Of
Assembled Motor Driver Test Circuit.
268 Chapter 5 Programming Large 8-Bit Systems
The program used to obtain the data in the above table was a
slight modification of the motor control program listing given in
Listing 5-6. The only change in this program was that the function
©port void I Cl_Isr (void) from Listing 5-7 was used. This
routine stores in the location measured_period a value equal to
the average of the last eight measured periods.
For several reasons, a reed switch is probably the worst example
of an input sensor that you can find for a microcontroller. I chose this
input because it is the poorest, and the result has been satisfactory.
With reed switches, there are serious bounce problems on both the
rising and falling edges of the signals. The life expectancy for a reed
switch is relatively small, about ten million closures. During the course
of preparing this text, I broke one reed switch and had probably a
total of several hours of service out of the two used for these
experiments. In practice, it is recommended that rotational
measurements be made with optical interrupters. Do not, however,
think that an optical interrupter is free from bounce problems. An
optical interrupter will exhibit both bounce and also a relatively slow
rise time. In such a case, it is necessary to use a software debounce
technique and also an external Schmidt trigger circuit to create a
quick and stable rise time that can be captured by the microcontroller.
The data for the above table were measured on the circuit shown in
Figure 5-2. It was found that the PWM to voltage conversion was very
accurate and repeatable with many changes in the system. For example,
the prescaler value was changed from the value of 4 discussed above
to 2 and up to 16. No difference could be noted in the PWM to voltage
conversion over this range. Also, the input capture values were quite
stable. The program in Listing 5-6 was compiled and used to complete
the measurements. The PWM_count value was changed manually over
the range of values shown above. The value for icap period is the value
found in measured_j?er iod in the program. The voltage measured
was the integrated value that was sent into the input of the MC33033
motor driver. The RPM was calculated as
™w 60000
RPM =
measured_period
Data from this table are used to create an equation that relates RPM
to the PWM_count. This equation will be used in the program to
Timer Operations 269
close the loop and regulate the motor speed. The relation that can be
derived from the above data is
RPM + 12528
PWM count =
7.875
This expression is accurate to about 1% across the range of
interest. The measured data is the input capture count, which we will
call p. What we must do is to calculate a new value for the
PWM_count from a measured p. An error in p is called p and this
expression means a change in p. The PWM_count will be designated
as pc and also means a change in pc. We shall now attempt to arrive
at an equation that expresses the necessary change in pc to correct
for an error in p. The error in p is the difference between the desired
cycle time and the measured cycle time. RPM in the above expression
is converted to cycle time p by noting that the RPM is 60000 / shaft
cycle time in milliseconds, and that the clock is ticking 500 times per
millisecond. Therefore, we have
1
Pc =
7.875
( 12528 + 6000 0* 500 \
P
If there is a error in the cycle time say, Ap, then the change in
pc, Ape, is seen to be
7 ^ n7 3809500
p c +Ap c =1591 +
p + Ap
This expression, after some simplification and approximation
reduces to
Ap c = -3809500 + ^- (5 . 1}
This value calculated by the above expression will be used to change
the PWM_count that drives the motor. The data contained in
measuredjperiod are placed there by the input capture interrupt
service routine. It is assumed that the data there are always current.
The calculation to determine the PWM_count correction is
substantial. We shall use this approach here to determine the
correction, but recognize that there is another approach that probably
requires less total calculation. This approach is to use a lookup table.
270 Chapter 5 Programming Large 8-Bit Systems
We will see the look-up table in some detail in Chapter 6.
We will probably run into overflow problems if care is not exercised
in the calculation of the correction Ap . In Equation 5-1 above, p is the
time of a shaft rotation in milliseconds. This number can vary from 1000
to 20000 depending on the motor speed. The best approach we could
use is to divide the constant -3809500 by the value of p. Then multiply
the result by Ap and finally divide the result by p a second time. This
approach will minimize the overflow problems. In the expression be-
low, p is the measured_period, pc is the PWM_count, and Ap is
the calculated difference mo tor_period - measured_j?eriod.
FOREVER
{
if (old_motor_speed! =motor_speed)
{
motor_period=3 00000001u/motor_speed;
old_motor_speed=motor_speed;
PWM_count= ( (motor_speed+12528) /63) *8;
PWM_count= limit (PWM_count) ;
delpc= (3809500/motor_period) ;
delpc=delpc* (motor_period-measured_period) /p;
PWM_count -= delpc;
/* read in the motor speed; */
}
Recall that division by 7.875 is needed in the calculation of
PWM_Count. This floating-point operation is avoided by dividing
the expression by 63/8. Most often, floating point operations can be
approximated by integer operations with sufficient accuracy.
It was mentioned earlier that it is possible to extend the maximum
measurable time to something greater than the time required to clock
the timer counter register 65535 times. If the longest time exceeds
this value, the timer overflow interrupt can be used to an advantage.
Suppose that you want to measure a long time with input capture 1.
When this measurement is started, the value in the TCNT will be
saved, and the timer overflow interrupt will be enabled. Also a counter
will be reset to zero. In the timer overflow interrupt service routine,
the counter will be incremented. Eventually, an input capture will
occur, and the time between inputs is calculated as the time remaining
Timer Operations 271
prior to the first TO I, plus 65535 times the number of TO Is that
have occurred, plus the time indicated in the input capture register.
With a scheme of this nature, there is essentially no limit to the length
of time that can be measured accurately. The resolution of this
measurement is the time increment of the prescaler output.
A velocity servo is a particularly difficult application. Remember
that velocity or motor speed is the derivative of position. Any derivative
operation is prone to noise, and the successful servo must be carefully
put together. For example, the equation calculated above to determine
the feedback term is essential to the proper operation of the system. An
interesting challenge is to try to duplicate the operation of the system
described here with a normal gain type feedback. Even though the program
seems to be messy, this approach will work, and most other approaches
give you a wildly hunting or oscillating system. The calculation of the
proper feedback value will avoid many of these problems.
Another problem area will be found. If the above FOREVER loop is
allowed to run without any time control, the computer will be calculating
a new feedback value many times before the effect of an earlier
calculation will be seen in the system performance. This operation again
causes the system to become unstable, and usually the system never
finds the final value but hunts over some wide range around it. This
operation can be corrected by slowing the rate at which feedback
corrections are calculated. In particular, several rates have been tested,
and it was found that a stable, accurate system would be obtained with
a new feedback value calculated each quarter of a second. This time
control is shown in the listing below. The main application program is
broken into two parts: the first part is the code that will be executed
whenever the motor speed is changed and the second part is the code
that will alter the PWM_count which is the feedback portion of the
program. The first portion is executed under the control of an if statement
if (old_motor_speed! =motor_speed)
There is no means to change the motor speed in this program, and
this change will be introduced in the next section. Therefore, it is
expected that the portion of the code controlled by this i f statement
will execute the first time the program is executed, and it should never
be executed again until the system is reset. The second i f statement
if (tick)
272 Chapter 5 Programming Large 8-Bit Systems
will execute its control code each time tick is TRUE, tick is ini-
tialized to TRUE when it is created, and it is set to FALSE each time
the if (tick) routine is entered in the application program. If
you go to the end of Listing 5-7, you will find that tick is reset to
TRUE about each quarter of a second in the PWM timer routine.
This sequence will cause the feedback calculation to be executed
about each quarter of a second.
The initialization portion of the program shown in Listing 5-7 is
little changed from that of the one shown in Listing 5-6. As mentioned
above, we have added an application section that contains the
calculations to control the motor speed. Also, there is one simple
function that is used by the application section.
#include "hclle9.h"
#def ine
DIVIDE_8_SHIFT
3
#def ine
COUNT_8
8
#def ine
COUNT_MAX
3300
#def ine
COUNT_MIN
1600
#def ine
COUNT_ONE_QUARTER
32
#def ine
PERIOD
0X1000
#def ine
TIMEJON
0x0800
#def ine
IMPOSSIBLE
3500
#def ine
TOO LOW
100
©port void ICl_Isr (void)
©port void OC2_Isr (void)
©port void OC3 Isr(void)
long limit (long) ;
long measured_period, delpc;
WORD timel , time2 , motor_period,
motor_speed=IMPOSSIBLE ;
WORD old_motor_speed=TOO_LOW, rpm,mparray [COUNT_8] ;
long PWM_period=PERIOD, PWM_count=TIME_ON;
int tick=TRUE, count=0;
main ( )
{
Timer Operations 273
/* The initialization portion of the program */
TCTL2 .EDG1B=0N; /* capture falling edge only */
0C1M.0C1M7=0N
0C1M.0C1M5=0N
TMSK1.0C3I=0N
TMSK1.IC1I=0N
0C1D.0C1D5=0N
TCTL1.0L3=0N;
/*
/*
/*
/*
/*
sent 0C1 out to PA7 */
couple 0C1 to 0C3 */
enable the 0C3 interrupt */
enable the IC1 interrupt */
turn on 0C3 with 0C1 */
/* toggle 0C3 when 0C3 occurs */
PACTL.DDRA7=0N; /* make 0C1 an output to PA7 */
TOCl=TCNT+PWM_period; /* set 0C1 */
T0C3=T0Cl+PWM_count; /* set 0C3 time on */
cli(); /* enable the system interrupts */
/* the applications portion of the program */
FOREVER
{ /* All of the numbers used below are derived
in the text */
if (old_motor_speed! =motor_speed)
{
motor_period=3 0Lu/motor_speed ;
old_motor_speed=motor_speed ;
PWM_count= ( (motor_speed+12528) /63) *8;
PWM_count= limit (PWM_count) ;
}
if (tick)
{
tick=FALSE;
delpc= (3809500Lu/motor_period) ;
delpc=delpc*
(motor_period-measured_period) /
motor_period;
PWM_count - =delpc ;
PWM_count= limit (PWM_count) ;
rpm=3 0000000L/measured period;
}
/*
}
input the new motor speed */
/* functions of the main program */
274 Chapter 5 Programming Large 8-Bit Systems
/* range of acceptable PWM_count is 1600 to 3300 */
long limit (long x)
{
if (x<COUNT_MIN)
return COUNT_MIN;
else if (x>COUNT_MAX)
return COUNT_MAX ;
else
return x;
}
/* The asynchronous service portion of the program */
©port void ICl_Isr( void) /*the motor speed
measurement*/
{
static int i;
int j ;
time2=TICl;
TFLG1=IC1F; /* reset IC1 interrupt flag */
mparray [i] =time2 - timel ;
if (++i==COUNT_8)
{
i = 0;
measured_period=0 ;
for(j=0;j<COUNT_8;j++)
measured_period += mparray [j];
measured_period >>= DIVIDE_8_SHIFT;
}
timel=time2 ;
TOC2=time2+5*measured_period/8 ; /*debounce time
5/8 revolution */
TMSK1.IC1I=0FF; /* disable IC1 interrupt */
TMSK1.0C2I=ON; /* enable OC2 interrupt */
}
©port void OC2_Isr (void) /* the debounce isr */
{
TFLG1=IC1F OC2F; /* reset interrupt flags */
Timer Operations 275
TMSK1.0C2I=OFF; /* disable 0C2 interrupt */
TMSK1.IC1I=0N; /* enable IC1 interrupt */
}
©port void 0C3_Isr( void) /* the PWM isr */
{
TFLG1=0C1F; /* reset 0C1 interrupt flag */
TOCl+=PWM_period;
0C1D.0C1D7 ^=0N;
TFLG1=0C3F; /* reset 0C3 interrupt flag */
T0C3=T0Cl+PWM_count;
if (++count==COUNT_ONE_QUARTER)
{
count=0; /* enter the speed control */
tick = TRUE; /* about each quarter of
of a second */
Listing 5-7: Motor Control Program
In this program, dynamic debounce is used. In the function
ICl_Isr ( ) the debounce time is a calculated value rather than a
mere 3 ms as was done in Listing 5- 6. Here the debounce lock-out
time is five-eighths of the current measured period. This value goes
beyond the half-way point in the input waveform enough to avoid
any bounce that will occur during the rise time of the input signal.
The remainder of this program is quite similar to that in Listing 5-6.
We have completed most of the application. The remaining parts
of the program are to find a means to read into the microcontroller
the speed that is desired for the motor, and to integrate and debug the
whole program. Usually, the speed is established by some measure-
ment being done with the microcontroller. Let us approach the
problem a little differently. Instead of making the speed a result of a
measurement, let's enter the speed into the part through the asyn-
chronous serial port.
Serial Communications Interface (SCI)
The SCI is another of the very useful peripheral functions found
on the MC68HC11 family of microcontrollers. In fact, the SCI is
276 Chapter 5 Programming Large 8-Bit Systems
found on parts from all families of Motorola microcontrollers. The
SCI performs the operation of a full function universal asynchronous
receiver transmitter (UART). It can operate with most standard baud
rates, and several nonstandard rates. Control of this peripheral is
through the BAUD register, the SCSR (serial communications status
register), and both SCCR1 and SCCR2 (serial communications control
registers 1 and 2). There is one additional register called the serial
communications data register. This register when written to is the
transmit data register, and when read the receiver data register. Both
the transmit and receive data registers are double buffered.
The BAUD register contains two test bits and two divide control
registers that set the baud rate for the SCI system. The bits SCP control
the baud rate prescaler. This prescaler has the unlikely prescale value of
divide-by- 13 when both bits are set. This value sets the maximum baud
rate to 9600 when the system is driven by an 8 MHz clock. With this
prescaler value, all of the standard baud rates from 9600 down to 150 are
available to the SCI. The second divider is comprised of SCR. The bit
patterns in these bits will select any power of two divisors between 1 —
2° -and 128 — 2 7 . For our system, we will choose 9600 baud, which is
sufficiently slow that there should be few communications problems.
The code to set up this baud rate is as follows:
BAUD.SCP=3;
BAUD.SCR=0;
These lines of code must be added to the initialization portion of the
program to set up the baud rate to 9600 baud for the SCI system.
We will use the SCI system to read in the required motor speed
for the program started in the preceding section. Such a system must
be able to read in data through the SCI, but it must also echo the
same data back to the originating source to provide full duplex
operation. Therefore, both serial transmit and receive must be
implemented. With the above system, there is no serious need to
implement an interrupt driven system. The computer is clearly not
being used to its capacity, so we will add the SCI input and output
sequence to the applications portion of the code and feel safe that no
input data will ever be lost because of an overrun error. (An overrun
error occurs when a new input overwrites an old input before the old
input is processed.) Therefore, none of the bits in SCCR1 need be
changed from their reset value. In the SCCR2 register, there are two
Timer Operations 277
bits, transmit enable and receive enable, that must be set to enable
the SCI. The following two lines of code will enable these bits.
SCCR2 .TE=1;
SCCR2 .RE=1;
These two lines of code must also be added to the initialization portion
of the program.
The code to read the data in is as follows:
if (SCCR.RDRF= = 1) /* read in data if it is there */
{
new_character=SCDR; /*get new byte and reset RDRF*/
while (SCCR.TDRE==0) ; /* wait until transmit
buffer empty */
SCDR=new_character ; /* send out byte and reset
TDRE */
}
This sequence of code does quite a bit more than you might expect.
Before this code sequence can be executed, both the RDRF and TDRE
flags must be set and reset. The RDRF is set when a character has
been received by the SCI, and the TDRE is set when the SCDR is
empty and can receive a character to send. Both of these bits are reset
by the sequential read of SCSR with the bit set followed by a read for
the RDRF or a write for the TDRE to the SCDR register. The above
sequence accomplishes all of the proper bit resets, transmits the newly
received character back to the sender, and leaves the new character
in the location new_character . After receiving the data, the
following sequence converts it into a binary number to be processed
by the remainder of the program.
if (SCSR.RDRF==1) /* read in data if there */
{
new_character=SCDR; /*get new byte and reset RDRF*/
while (SCSR. TDRE==0) ; /* wait until transmit
buffer empty */
SCDR=new_character ; /* send out byte and reset
TDRE */
/* if a number, process it */
if (new character>=' ■ && new character <='9')
278 Chapter 5 Programming Large 8-Bit Systems
new_speed = 10*new_speed + new_character- ' ' ;
else if (new_character==' \r' )
{
/* reject any number out of range */
/* and start over again */
if (new_speed>=1000 && new_speed<=10000)
motor_speed=new_speed ;
new_speed=0 ;
}
else
new_speed=0; /* reject everything else */
}
The variable new_speed is initialized to outside of this portion
of the program. If the input character is a number (a number is any
character that lies between '0' and '9'), it is converted from a character
to a number when the character '0' is subtracted from it. This value
is added to ten times the value stored in new_speed , and the result
is saved in new_speed . Repeated executions of this code sequence
will convert a series of ASCII character numbers to an appropriate
unsigned integer value. The entry sequence is terminated when a
nonnumber character is received. If this character is a line terminator
(a carriage return escape character in this case), the new speed value
is put into motor_speed to cause the motor to change to the new
value, and new_speed is reset to zero to await for the next input.
If any other character is received, the data saved in new_speed is
lost, and the whole entry of a new speed into the system must be
repeated from the beginning.
In Listing 5-7 there was a lonely line of code
/* input the new motor speed */
That line has been replaced by the above and entered into Listing
5-8. The new motor speed input is placed in the main loop of the
applications program, so a test is made each time through the loop to
determine if there is a new input from the serial port. This routine
also sends out the current speed of the motor once each half second.
This output is controlled by the parameters tickl andin_process.
tickl is set to TRUE each half second, and if in_process is
FALSE the motor speed is calculated and sent to the serial port output.
Timer Operations 279
This sequence repeats each half second until in_proce s s is set to
TRUE. Whenever a character is read in through the serial port,
in_process is set to TRUE to disable the continual output from
the system while a new input speed value is being entered. If there is
a new character in the input buffer, this character is read in and the
RDRF flag is reset. The new character is immediately echoed back to
the sending device as part of full duplex operation. If the new character
is a digit it is processed, if it is a line feed it is also processed, and any
number string is converted to an integer so that it can be used by the
computer. The size of the number is tested to be certain that it is
within the acceptable speed limits for the motor, and if it passes all
of the tests, it is placed in the variable location motor_speed to
indicate that a new motor speed is here and should be processed.
#include u hclle9.h"
#def ine
DIVIDE_8_SHIFT
3
#def ine
COUNT_8
8
#def ine
COUNT_MAX
3300
#def ine
COUNT_MIN
1600
#def ine
COUNT_ONE_
_QUARTER
32
#def ine
COUNT_ONE_
_SECOND
128
#def ine
PERIOD
0X1000
#def ine
TIMEJDN
0x0800
#def ine
IMPOSSIBLE
3500
#def ine
TOO_LOW
100
#def ine
RPM_MIN
1000
#def ine
RPM_MAX
11000
#def ine
CR
OxOd
#def ine
LF
0x0a
/^function prototypes */
©port void ICl_Isr (void)
©port void OC2_Isr (void)
©port void OC3_Isr (void)
int putchar(char new_character) ;
void dprint (unsigned int c) ;
void do_crlf (void) ;
long limit (long );
280 Chapter 5 Programming Large 8-Bit Systems
/* external variable definitions */
long measured_period, delpc;
WORD timel, time2,
motor_period, motor_speed=IMPOSSIBLE;
WORD new_speed;
WORD old_motor_speed=TOO_LOW, rpm,mparray [COUNT_8] ;
long PWM_period=PERIOD, PWM_count=TIME_ON;
int new_character , tick=TRUE, count=0 , in_process ;
int tickl=TRUE, count 1=0;
WORD got_new=TRUE;
main ( )
{
/* The initialization portion of the program */
TCTL2 .EDG1B=0N;/* capture falling edge only */
0C1M.0C1M7=0N; /* sent OC1 out to PA7 */
0C1M.0C1M5=0N; /* couple OC1 to OC3 */
TMSK1.0C3I=ON; /* enable the OC3 interrupt */
TMSK1.IC1I=0N; /* enable the IC1 interrupt */
0C1D.0C1D5=0N; /* turn on OC3 when OC1 occurs */
TCTL1.0L3=ON; /* toggle OC3 when OC3 occurs */
PACTL.DDRA7=ON;/* make OC1 an output to PA7 */
TOCl=TCNT+PWM_period; /* set OC1 to the period*/
TOC3=TOCl+PWM_count; /* set OC3 time on */
BAUD.SCP=3; /* set up the SCI */
BAUD.SCR=0; /* 9600 baud */
SCCR2.TE=ON;
SCCR2.RE=ON;
cli(); /* enable the system interrupts */
/* the applications portion of the program */
FOREVER
{
if (old_motor_speed! =motor_speed)
{ /* All of the numbers used below are
derived in the text */
motor_period=3 00000001u/motor_speed;
old motor speed=motor speed;
Timer Operations 281
PWM_count= ( (motor_speed+12528) /63) *8;
PWM_count= limit (PWM_count) ;
}
if (tick)
{
tick=FALSE;
delpc= (38095001u/motor_period) ;
delpc=delpc* (motor_period-
measured_period) /motor_period;
PWM_count -=delpc;
PWM_count= limit (PWM_count) ;
rpm=3 OL/measured_period ;
}
/* input the new motor speed */
/* Send out the measured RPM to the terminal
periodically */
if (tickl&& ! in_process)
{
tickl=FALSE;
rpm=3 0000000L/measured_period;
dprint (rpm) ;
do crlf ( ) ;
}
if (SCSR.RDRF==ON) /* read in data if it is
there */
{
in_process=TRUE;
new_character=SCDR; /* get new byte and
save it */
while (SCSR.TDRE==OFF) ; /* wait until transmit
buffer is empty */
SCDR=new_character ; /* send out byte and
reset TDRE */
/* got a number, process it */
if (new_character>=' ■ && new_character < = ' 9 ' )
new_speed = 10*new_speed + new_character- x ' ;
else if (new character=='\r' )
282 Chapter 5 Programming Large 8-Bit Systems
{
else
}
}
}
do_crlf () ;
/* reject any number out of range */
/* and start over again */
if (new_speed>=RPM_MIN && new_speed <=RPM_MAX)
{
got_new=TRUE ;
in_process=FALSE;
motor_speed=new_speed ;
new_speed=0 ;
}
new speed=0; /* reject everything else */
/* functions of the main program */
/* the range of acceptable PWM_count is 1600 to
3300 */
long limit (long x)
{
if (x<COUNT_MIN)
return COUNT_MIN;
else if (x>COUNT_MAX)
return COUNT_MAX ;
else
return x;
}
/* send out a single carriage return-line feed
sequence */
void do_crlf (void)
{
Timer Operations 283
while (SCSR.TDRE==OFF) ;
SCDR=CR;
while (SCSR.TDRE==OFF) ;
SCDR=LF;
/* send a single character to the serial port */
int putchar(char new_character)
{
while (SCSR.TDRE==OFF) ;
/* wait until transmit buffer empty */
SCDR=new_character ;
/* send out byte and reset TDRE */
}
/* convert an integer and send it to the serial
port as a string */
void dprint (unsigned int c)
{
if ( c/10) /* recursively determines if */
dprint (c/10) ; /* a zero has been reached and then */
putchar (c%10+ / ' ) ; /* sends out the characters */
}
/* The asynchronous service portion of the program */
©port void ICl_Isr( void) /*the motor speed
measurement * /
{
static int i;
int j ;
time2=TICl;
TFLG1=IC1F; /* reset IC1 interrupt flag */
mparray [i] =time2-timel ;
if (++i==COUNT 8)
{
i = 0;
284 Chapter 5 Programming Large 8-Bit Systems
measured_period=0 ;
for(j=0;j<COUNT_8;j++)
measuredjperiod += mparraytj];
measured_period >>= DIVIDE_8_SHIFT;
}
timel=time2 ;
TOC2=time2+5*measured_period/8 ; /*debounce time
Is 5/8 revolution */
TMSK1.IC1I=0FF; /* disable IC1 interrupt */
TMSK1.0C2I=ON; /* enable 0C2 interrupt */
}
©port void 0C2_Isr (void) /* the debounce isr */
{
TFLG1=IC1F|0C2F; /* reset interrupt flags */
TMSK1.0C2I=0FF; /* disable 0C2 interrupt */
TMSK1.IC1I=0N; /* enable IC1 interrupt */
}
©port void 0C3_Isr( void) /* the PWM isr */
{
TFLG1=0C1F; /* reset 0C1 interrupt flag */
TOCl+=PWM_period;
0C1D.0C1D7 ^=0N;
TFLG1=0C3F; /* reset 0C3 interrupt flag */
T0C3=T0Cl+PWM_count;
if (++count>=COUNT_ONE_QUARTER)
{
count=0; /*enter speed control about */
tick=TRUE; /*each quarter of a second */
}
if (++countl==COUNT_ONE_SECOND)
{
count 1=0 ;
tickl=TRUE;
}
}
Listing 5-8: The Completed Application
Summary 285
There are three input/output routines that have been written for
this program. These routines, put char ( ) , dprint ( ) , and
do_crlf ( ) , can be used with other systems with a serial input/
output system. The Cosmic compiler does provide the usual I/O
routines like pr int f ( ) , ge t s ( ) , put s ( ) , etc. It does not provide
a basic put char ( ) andgetchar () which is used by all of these
library routines. The reason that these routines are not provided by
the compiler is the wide variety of what the programmer will want to
implement with the built-in SCI ports on the MC68HC11 family.
The put char ( ) shown above will work in most instances. The
dprint ( ) routine is a recursive routine that converts an integer
into an ASCII string and sends it to the SCI port.
There is one final modification to the program. In the last lines of
the PWM timer routine, count 1 and tickl are processed to set
t ickl to be TRUE each second. This flag is then used to control the
writing of the motor speeds to the terminal screen.
Summary
There has been no attempt to work all of the peripherals on the
MC68HC1 1 . The various peripherals are similar to those on the other
parts that we have discussed in other chapters or will discuss later.
We have seen several timer applications both in the MC68HC1 1 and
in the MC68HC05. We will see other timer applications in the
following chapters.
We have seen detailed use of the output compare timer subsystem
to make a pulse width modulation digital-to-analog converter system.
Depending on the program, the system allowed excellent performance
in either short on times or maximum on times, but not both without the
addition of a significant amount of code. We will see a system in the
next chapter that provides excellent performance for both minimum
and maximum on times. This performance is not a limitation of the
MC68HC11, merely a limitation of the programs presented so far.
The input capture subsystem has been used to measure motor
speed in a simple DC motor controller. This system used a primitive
reed switch to measure the rotation of the motor shaft, and the
performance of the switch was poor. A debouncing system was
developed that prevented input captures to occur for a specified time
after the first input was detected. This approach uses an output
286 Chapter 5 Programming Large 8-Bit Systems
compare channel, but it does not tie up the microcontroller to wait
out any delay times during the debounce period. This program is not
too removed from many of those encountered in the real world.
The organization of the program is similar to how most
applications can be programmed, and the way in which the program
was developed showed how most problems should be approached.
The problem was broken down into a set of small operations that
could each be handled easily. These different parts of the program
were developed, tested, and debugged separately. This approach keeps
the development of the individual parts of the program manageable,
and debugging is not too difficult. If the whole program were written
and then debugging started, it would have been nearly impossible to
separate out the effects of one part of the program on the others. The
main interrupt service routines were written first and tested as well
as possible by themselves. With these important functions behind us,
it was easy to attack the applications portion of the program in which
the closed loop system was implemented along with the management
of the input/output through the serial port of the device.
The MC68HC 1 1 is a powerful enough computer that it is possible
to make an ANSI compliant compiler. Parameters can be passed to
functions on the stack, and re-entrant or recursive functions can be
written for this part as was demonstrated with the dprint ( )
function. Remember, it is the microcontroller that limited the ANSI
compliance with the MC68HC05 — not the compiler.
Chapter 6
Large Microcontrollers
In this chapter we'll examine the programming of systems em-
ploying large microcontrollers. The realm of the large-microcontroller
system is not all that different from the 8-bit systems. One of the
main advantages of the use of a high-level language is that it keeps
the nasty details of the underlying computer hidden from the pro-
grammer. Usually, the programmer will not see much difference
between the code for different types of computers. The fallacy to this
idea is that when programming any microcontroller, the program-
mer must know about and use all of the on-board peripherals found
on the microcontroller. These peripherals will vary from machine to
machine, and how they are accessed will differ from device to de-
vice. In this chapter, however, we are going to see an application
where a substantial amount of assembly language is required. The
chip that we are going to use here has a Digital Signal Processor
section. This processor is accessed through special core chip regis-
ters and the core condition code register. The abstract machine that
the compiler creates code for contains no registers. Therefore, the
only access to these features is through assembly language. We will
show how to create assembly language functions that can be accessed
from your C program.
The part that we will use for the 16-bit discussions is the Motorola
MC68HC 1 6 family of components . These are similar to the MC68HC 1 1
in many ways, but there are important differences. First, there are some
new registers that must be programmed directly to make the
MC68HC16 work as desired. (The only register in the MC68HC11
that must receive special assembly instructions is the condition code
register.) Also, the MC68HC16 does not have automatic stacking of its
registers when an exception occurs, unlike the MC68HC11. There-
fore, all interrupt service routines must begin with code that saves the
287
288 Chapter 6 Large Microcontrollers
status of the machine before the normal interrupt operations can pro-
ceed. We will see a few other differences between these devices, but
the main difference is in the way peripherals are handled.
On the MC68HC16, the core processor is called the CPU16. This
central computer is interfaced to an internal bus called the
inter- modual bus (1MB). The 1MB is very much like the bus a hard-
ware designer would put onto an external circuit board. It has address
and data busses and all of the necessary control signals to control any
peripheral that the microcontroller might have applied. In the stan-
dard device, the MC68HC16Z1, there are five built-in peripherals:
the system integration module (SIM), the analog-to-digital converter
(ADC), the queued serial peripheral interface module (QSPI), the
general-purpose timer module (GPT), and the static random access
memory module (SRAM). These modules and others can be added or
deleted in future components as the customer needs dictate.
The several internal peripheral modules are each interfaced to the
1MB. Each module has a specific set of registers that are located at an
address relative to the base address of the module. These base ad-
dresses are set at design time. This is interesting because the same
modules are used on the MC68300 series of microcontrollers, which
are 32-bit microcontrollers. Therefore, the material presented in this
chapter is directly applicable to the MC68300 series of microcontrollers.
The only difference that the programmer will see is that the base
memory locations of the various peripheral modules are different, but
even these differences disappear because of the careful design of the
microcontrollers. We therefore can consider this chapter to be on large
microcontrollers rather than on the 16-bit systems alone.
The MC68HC16
The MC68HC16 is a truly complicated device. However, for our
purposes, it can be divided into its several components, and each
component is somewhat as one would expect from a programming
standpoint. Therefore, we will examine this family of parts as a col-
lection of modules. Each module is rather straightforward. The core
processor, which is known as the CPU 16, is indeed a complete and
competent microcomputer. The bulk of its complexity is hidden by
the fact that the program is written in C. This section contains a brief
description of the MC68HC16Z1 microcontroller that can be used to
The MC68HC16 289
aid in writing programs for the component in C. There are several
Motorola manuals that are useful adjuncts to this chapter. l23456
Copies of these manuals are all to be found on the attached CD-
ROM. It is recommended that these manuals be reviewed prior to
any attempt to write code for this family.
CPU16 Core Processor
The CPU 16 represents an attempt to bridge the difference be-
tween the large 8 -bit microcontrollers and the high-end components
embodied in the MC68300 family. This processor is a 16-bit proces-
sor. As such, its instructions are each 16 bits wide, instructions are
read from memory 16 bits at time, and the instructions are processed
16 bits at a time. On this particular part, a 20-bit address bus with
additional control allows the program to access two individual one-
megabyte address maps. The controls available distinguish between
program memory and data memory.
It has been stated several times that the programmer's model for
a microcontroller is not too important when writing code in C. To be
able to access all of the features of a CPU16, you will have to use
assembly language-based functions. There are certain features that
are simply outside the concept of a high-level language. In these
cases, it is recommended that functions which access and control
these features be written and then you can call these functions from
the C program. The main properties of the CPU 16 that are not avail-
able to the C programmer are the DSP type registers found in the
part. Programming of these registers is the subject of a later section
of this chapter.
The 20-bit address space of the CPU16 is a significant deviation
from the normal 1 6-bit address space of the MC68HC 1 1 . This change
has been handled by the addition of several 4-bit extension registers
for those registers that deal specifically with addresses. These regis-
ters are the program counter (PC), the stack pointer (SP), the three
1 M68HC16 Family MC68HC16Z1 Users Manual MC68HC16Z1UM/AD
2 M68HC16 Family CPU 16 Central Processor Unit Reference Manual CPU16RM/AD
3 Modular Microcontroller Family GPT General Purpose Timer Reference Manual GPTRM/AD
4 Modular Microcontroller Family ADC Analog-to-Digital Converter Reference Manual ADCRM/AD
5 Modular Microcontroller Family QSM Queued Serial Module Reference Manual QSMRM/AD
6 Modular Microcontroller Family SIM System Integration Module Reference Manual SIMRM/AD
290 Chapter 6 Large Microcontrollers
index registers (IX, I Y, and I Z), and the EK register. All these regis-
ters except for the EK are 16 bits wide. The EK register is an extension
register that is used with the extended addressing mode. Any ex-
tended address calculation will result in a 16-bit number. The result
will be concatenated with the EK register to create a 20-bit address.
(The EK register has nothing to do with the E accumulator found in
the device.) The extension registers are named PK, SK, XK, etc. The
extension registers are usually set during initialization of the device,
and there is no need to change them during program operation. The
content of an extension register is a page pointer. The pages in this
case are each 65536 bytes long. Transition from one page to another
is automatic. For example, if an address is calculated for a j mp which
will pass program control to code in another page, the proper ad-
dress will be calculated for both base and extension register
automatically. The calculation will alter the contents of both the base
and the extension register without programmer concern.
The MC68HC16 family currently has the several internal modules
mentioned above, and it is planned that future versions of the part will
have more modules. This complexity has suggested that the arrange-
ment of the header file for the part be broken into several different
files: One file for the main processor, and several different header files,
one for each of the peripheral modules in the individual part. This
approach is shown in the HEADER/HC16HEADERS directory on
the CD-ROM. There you will find a header file named he 1 6 . h along
with files named adc . h , gp t . h , s im . h , sram . h , and qsm . h.
When writing code for any specific module, you should include the
he 16 .h file along with the proper files for the peripheral portions
needed. The he 16 .h file must be included first because there are
items defined in this file needed by the other headers.
One major difference between the large and small devices is the
way the exception vector table is handled. Recall that, in the MC68HC 1 1
family, all interrupt vectors are placed at the top of memory. With the
MC68HC16 family, the vector table is contained within the first 512
bytes of memory. Upon reset, the CPU 16 core processor reads the first
four words of memory where it must find certain data for the opera-
tion of the program. The address must contain a word whose least
significant 12 bits are the contents of the ZK, the SK and the PK reg-
isters when the part comes out of reset. The address 2 contains the
initial program counter, and the address 4 must contain the initial stack
The MC68HC16 291
pointer. Finally the address 6 must contain the initial value of the IZ
register. Note that the word addresses in the MC68HC16 are always
even. In this machine you will find words on even boundaries, bytes
anywhere, and long words on even boundaries. These data are loaded
into this memory area by the use of a vector routine similar to that seen
in Chapter 5 for the MC68HC1 1. With this approach, all of the regis-
ters needed to begin operation of the basic computer are loaded from
memory at reset time. Of course, this operation does not eliminate the
need for program initialization; it merely provides a mechanism by
which the processor will start accessing memory at the correct ad-
dresses when the device comes out of reset.
Table 6-1 contains a listing of the uses of each entry in the
vector table. Note that vectors 0x0 through 0x3 7 have assigned
functions. The vectors 0x3 8 through Oxf f are available for user-
defined operations. Note the relationship between the vector number
and the vector address. The vector address is always twice the vec-
Table 6-1 : Exception Vector Table
Vector
Number
Vector Address
Type of Exception
0x0000
Reset— Initial ZK, SK, PK
1
0x0002
Reset— Initial PC
2
0X0004
Reset— Initial SP
3
0X0006
Reset — Initial IZ
4
0X0008
Breakpoint
5
0X000A
Bus Error
6
oxoooc
Software Interrupt
7
0X000E
Illegal Instruction
8
0X0010
Division by Zero
9-E
0X001 2-0X001 C
Unassigned, Reserved
F
0X001 E
Uninitialized Interrupt
10
0X0020
Level Autovector
11
0X0022
Level 1 Autovector
12
0X0024
Level 2 Autovector
13
0X0026
Level 3 Autovector
14
0X0028
Level 4 Autovector
15
0X002A
Level 5 Autovector
16
0X002C
Level 6 Autovector
17
0X002E
Level 7 Autovector
18
0X0030
Spurious Interrupt
19-37
0X0032-0X006E
Unassigned, Reserved
38-FF
0X0070-0X01 FE
User-defined Interrupts
292 Chapter 6 Large Microcontrollers
tor number. When programming the interrupt vectors in the several
peripheral modules of the MC68HC16, the programmer will select
the interrupt vector. When it comes time to place the interrupt ser-
vice routine address in the proper memory location, it is to the
vector address — NOT the vector number — that this value must be
assigned.
When setting up the vector table, a wise programmer will fill all
of the possible unused vector addresses with the address of a dummy
function that provides an orderly return to the program in the event
of an unexpected interrupt. Usually the first Oxl 8 or 24 vectors should
be filled with this address. An example function that can be used for
this type of operation is
static ©port void _init_vector (void)
{ }
This program will compile to a single RTI (return from interrupt)
that will return the program control to the location when the interrupt
occurred. A static function is not used often. When a function is
declared static, it can be seen only in the file in which it is defined.
Following is a listing of the routine vector . c . This program
is modeled closely after that provided with the Cosmic MC68HC16
C compiler.
extern @far ©port void_stext (void) ; /^startup routine*/
extern ©port void OC3_Isr (void) ; /* ISR address */
static ©port void _init_vector (void) ;
static const struct reset {
©far ©port void (*rst) (void) ; /* reset + code
extension */
unsigned short isp; /* initial stack pointer */
unsigned short dpp; /* direct page pointer */
©port void (*vector [252] ) (void) ; /* interrupt vectors */
}_reset = {
_stext, /* 1-start address */
0x03fe, /* 2-stack pointer */
0x0000, /* 3-page pointer */
_init_vector , /* 4 -Breakpoint */
init vector, /* 5-Bus Error */
The MC68HC16 293
_init_vector , * 6-Software Interrupt */
_init_vector , /* 7-Illegal Instruction */
_init_vector, /* 8-Divide by Zero */
_init_vector, /* 9-Reserved */
_init_vector, /* a-Reserved */
_init_vector, /* b-Reserved */
_init_vector, /* c-Reserved */
_init_vector, /* d-Reserved */
_init_vector, /* e-Reserved */
_init_vector , /* f -Uninitialized Interrupt */
_init_vector, /* 10-Reserved */
_init_vector, /* 11-Level 1 Interrupt Autovector */
_init_vector, /* 12 -Level 2 Interrupt Autovector */
_init_vector, /* 13 -Level 3 Interrupt Autovector */
_init_vector, /* 14 -Level 4 Interrupt Autovector */
_init_vector, /* 15 -Level 5 Interrupt Autovector */
_init_vector, /* 16 -Level 6 Interrupt Autovector */
_init_vector, /* 17-Level 7 Interrupt Autovector */
_init_vector, /* 18 -Spurious Interrupt */
/* vectors 0x19-0x37 unassigned, reserved */
0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
/* put timer at vector 0x46 0x40 from ICR and 6 for OC3 */
0,0,0,0,0,0, OC3_Isr /* OC3_Isr vector at 0x46 address at 0x8c */
};
static ©port void _init_vector (void)
{ }
Listing 6-1 : The Vector Initialization Routine vector. c
We have already seen the ©port command in Chapter 5. The
@f ar command is unique to the Cosmic compiler. This command
notifies the compiler that the pointer associated with the command is
not the usual 16-bit pointer. An @f ar pointer is an extended pointer.
This 20-bit pointer will be placed into the first two memory words
by the vector . c routine. The rightmost 4 bits in word is the
program counter extension PK, so that the placement of the 20-bit
294 Chapter 6 Large Microcontrollers
program counter in this location will provide proper initialization of
the program counter.
In the example above, the interrupt service routine OC3_Isr is
placed in the address 0x46. Unfortunately, there is no easy way to
accomplish this placement. The structure address must be counted
by hand and the pointer placed at the correct location. The structure
members are initialized only up to the vector with the highest ad-
dress. Note that the structure is global, so that its members will all be
initialized to zero unless otherwise assigned. Notice also that the
vectors up to vector number 0x18 are initialized. These vectors in-
clude all of the program-generated exceptions. These areas are where
one would expect most of the problems to occur in debugging a pro-
gram. The vector numbers 0x19 through Oxf f are left initialized to
0. These vectors are all accessed by either the internal modules or
from external interrupts. If there are no hardware problems with the
system, it is unlikely that external devices will cause uncalled-for
interrupts, and if one of the internal modules causes an interrupt with
an uninitialized vector, the CPU 16 will access the uninitialized inter-
rupt vector.
One other problem can arise when debugging programs that
involve user- specified interrupts. In the event that the isr address
is improperly placed in the vector table, the program will become
lost whenever the interrupt occurs. If such a program exhibits bizarre
behavior, a good trick is to place a break point at the address 0. If the
vector is wrong, an interrupt will take the proper vector which will
contain a and attempt to execute the code at the address 0. The
break point at this location will stop execution and give you a clue as
to the program error. A break point at _init_vector can also be
useful to determine where the program is when an unexplained
exception occurs.
If a compiler provides a mechanism that complies to the ANSI
standard and one that does not, it is better to choose the ANSI stan-
dard mechanism rather than the nonstandard approach. For example,
ANSI states nothing about how to establish a vector table. The above
approach is but one of several that can be used to handle the place-
ment of the vectors in the vector table. Another approach is to use the
vector macro that is found in he 16 . h . The disadvantage to the
vector macro is that it can place a vector into RAM only. Often with
The MC68HC16 295
embedded controls, it is necessary to place the vectors into ROM. In
that case, the use of the routine vector . c is the best approach.
The use of the ©port command is certainly not found in the
ANSI standard. It is possible to create a complete interrupt service
routine without the use of the ©port. However, if C is used to the
maximum, the isr will have some built-in inefficiencies. For ex-
ample, the language has no direct register commands so it is necessary
to create a function that saves the status of the computer on entry to
the routine, and another to restore the machine status prior to the
return from the interrupt. Also, a special function must be created to
execute the RTI instruction at the end of the interrupt service rou-
tine. These assembly routines can be created as function calls and
saved in the header file. When they are used, it is wise to study care-
fully the code generated by the compiler to make certain that there
are no errors in the code. For example, if a function should happen to
clear some space on the stack for local storage, the placement of an
RTI instruction in the C code sequence would cause the return op-
eration to be executed before the stack is restored. Such an error will
cause serious problems in system performance if not corrected.
Both the @f ar and the ©port commands are not in compliance
with the ANSI standard. It is recommended that you use these com-
mands sparingly because their use causes nonportable code to be
generated with the compiler. So far in this text, we have used two
significantly different compilers. Each compiler manufacturer claims
that their compiler complies to ANSI. In the case of the C6805 com-
piler from Byte Craft, it probably conforms as closely as can be
expected for such a primitive machine. Both the MC68HC 1 1 and the
MC68HC16 compilers comply more closely to the standard than the
C6805. Both of these machines are so much more computer that one
should expect very close compliance. Any extension to the basic lan-
guage should be used with care. The above two commands are
desirable and provide useful functions for the embedded control field.
The Cosmic compilers also have an extension that has not been
used in this text. The way in which they define the internal registers
to the machine is not standard, and it does not permit very efficient
use of bit manipulation by the compiler. The approach used here is
shown in the various header files written for the parts. The compiler
writer assumes that the use of a construct like
296 Chapter 6 Large Microcontrollers
#define ABLE (* (Register *) 0x2000)
is too complicated for most programmers to understand. The ap-
proach that they use is nonportable, and the latter approach is
completely portable among ANSI compliant compilers. It is recom-
mended that, even though the header files contain some code that
might be difficult to explain, you should use the approach presented
here to create code that is as portable as possible.
Some of the sections that follow will show how portable code can
be used. Examples from both the MC68HC11 and the MC68HC05
will be used on the MC68HC16, and you will see that much of the
code will be transferred with little change. Where would you expect
changes? Recall the recommendation that each program be broken
into three sections: the initialization section, the applications section,
and the asynchronous service section. Each section will be subject to
some change when moving from one machine to another, but the ap-
plications section will probably suffer little change and the other two
sections will see the most changes when the code is moved. For ex-
ample, you will see that the initialization of theMC68HC 1 6 is somewhat
different from that of the MC68HC1 1, but the interrupt service rou-
tines will be nearly the same. In fact, in some cases the isr for the
two parts is identical. On the other hand, the way that things are handled
on the MC68HC05 is so different that the initialization and isr will
probably have to be completely rewritten when moving code to one of
the larger machines. However, here the machine-independent portions
of the applications routine can be moved with little change.
System Integration Module (SIM)
A brief examination of the names of most of the modules will
reveal their use. There is one notable exception — what is a system
integration module (SIM)? The SIM is sort of the interface between
the 1MB and the outside world. It is very useful, and contains much
of the circuitry that a hardware designer would have to incorporate
to make a computer out of a microprocessor. The object of the chip
designer with the introduction of the SIM was to make it possible to
use the MC68HC16 in a system with a minimum of external cir-
cuitry. This section is not to provide you with a complete description
of the SIM. The SIM Reference Manual is a 200-page document,
and I'm not intending to duplicate that manual here. The following
System Integration Module (SIM) 297
paragraphs each contain a brief description of the several blocks found
within the SIM. The set up and control of these blocks are all con-
trolled by the registers described in the SIM book on the CD-ROM.
System Configuration and Protection
This module monitors many of the things that can go wrong with
the operation of the MC68HC16. Internal and external signals can
be generated that signal an error has occurred. Reset signals can be
originated from several sources. The reset status monitor keeps track
of the source of the latest reset to help with debug operations. The
halt monitor responds to a HALT signal on the internal bus. This
monitor, if properly enabled, can request a reset. The bus monitor
and the spurious interrupt monitor can each request a bus error. The
bus monitor responds primarily to an unanswered asynchronous bus
transfer request. Such a sequence is usually the result of a program
access to unimplemented memory. If properly enabled, the spurious
interrupt monitor can initiate a bus error.
There are two time-based functions in the system configuration
and protection section of the SIM. The first is a software watchdog
timer. This timer requires that the program access a memory location
with a code sequence. This access resets a timer. With a proper pro-
gram, the special location in memory is accessed routinely in the normal
execution of the program and the timer should never overflow. If it
does, there is probably a system error that is corrected by a system
reset. The periodic interrupt timer is used whenever a simple clocking
sequence is needed. We will see use of this timer in a later section.
System Clock
The system clock provides timing signals for the 1MB and all of the
internal modules of the microcontroller. The time base for the
microcontroller can be a 32768-kHz reference crystal , a 4.194-MHz
crystal, or an external clock signal. If either of the crystal oscillators are
used there is an internal phase-locked loop frequency multiplier that will
multiply the operating frequency to the final system clock frequency.
External Bus Interface
The external bus interface transfers information between the 1MB
and external devices. This interface supports a 16-bit data bus, up to a
298 Chapter 6 Large Microcontrollers
24-bit address bus, a three-line function control signal bus, control sig-
nals for dynamic bus sizing, and handshaking for external bus arbitration.
Interrupts
The CPU 16 contains a three- wire, seven-level interrupt system.
These interrupts are interfaced to the SIM through the 1MB. The
outside world is interfaced into the S IM as seven individual interrupt
lines which are multiplexed onto the three- wire line within the SIM.
There are also two sources of interrupt from within the SIM itself.
These interrupts are from the periodic interrupt timer and the soft-
ware watchdog timer.
Chip Selects
There are many signal lines on the microcontroller that might
not be needed with a typical system. For example, address lines 20
through 23 all follow the condition of address line 19. Perhaps not
all of the external interrupt lines are needed. Often the function con-
trol lines that can be used to decode the nature of a bus cycle — i.e.,
either data or program access — are not used. Altogether there are 12
signal lines that can be implemented as chip selects. This line will
assert when there is an access within a memory range specified.
Reset and System Initialization
The microcontroller has several sources of reset. The reset and
system initialization section directs the several resets to the proper
operation, and records the source of the resets. Also, when the sys-
tem is reset initially, the state of several pins on the system bus is
analyzed to determine the mode of the part when it exits the reset
sequence.
General Purpose I/O
There are 16 SIM pins that can be configured as general-purpose
input/output signals. These ports are ports E and F, and the pins are
all multiply assigned. Port E pins, for example, are bus control
pins, and Port F pins have a second use as the external interrupt
inputs to the system.
A Pulse Width Modulation Program 299
A Pulse Width Modulation Program
While the MC68HC16 has a built-in pulse width modulation
(PWM) system, it is sometimes desired to achieve more flexible reso-
lution than can be obtained with the built-in system. Therefore, it is
not unreasonable that one would want to use the general-purpose
timer to create a PWM. Prior to writing the code for a PWM in this
manner, we should look at the basic time period of the processor.
Unless otherwise directed, all of the timers in the GPT are driven by
the system clock. This signal is passed through a prescaler controlled
by the bits CPR2 through CPRO in the register TMSK2. The default
prescaler value will cause a clock rate to the GPT of the system clock
divided by four.
The question now is the clock rate. Recall that the clock control
is a portion of the SIM. Within the SIM there is a register named
SYNCR. The clock frequency is controlled by the three bit fields
named W, X, and Y in this register. When operating at a low crystal
frequency — between 25 and 50 kHz — the formula for the system
frequency is given by
F s =F r [4(y + l)(2 2w+x )]
where y has a value between and 63, and both w and x can have
values of or 1. With a frequency f of 32767 and the W, X, and Y
default values of 0, 0, and 63, respectively, the device will come out
of reset with a system frequency of 8.388 MHz. To be able to get the
finest resolution for our timing functions, let us plan to operate the
system clock frequency at its maximum value by changing the value
of X from its default value of to 1. This change will cause the
system frequency to be 16.776704 MHz. The frequency of the input
into the GPT is one-fourth this value or 4.194176 MHz. The time
period for this frequency is 238 nanoseconds.
The code for a PWM on the MC68HC 1 1 was shown in Chapter 5. A
program that implements a PWM on the MC68HC16 is shown below.
/* This program provides a pwm output to OC3 . The period
will be the integer value found in pwm_period, and the
on time will be the integer value found in pwm_count .
Keep pwm count less than pwm period. */
300 Chapter 6 Large Microcontrollers
#include "hcl6 . h"
#include "gpt.h"
#include "sim.h"
#define PERIOD 0x1000
#define ON_TIME 0x0800
#define GPT_IARB 5
#define GPT_IRL 6
#define GPT_VBA 4
/* function prototypes */
©port void OC3_Isr( void); /* the PWM isr */
WORD pwm period=PERIOD, pwm count =ON TIME;
ICR. IRL=GPT_IRL
ICR . VBA=GPT_VBA
OCONM . OCONM3 =ON
main ( )
{
/* The initialization portion of the program */
SYNCR.X=ON; /* set the clock freq to 16.78 MHz */
SYPCR.SWE=OFF; /* disable the watchdog */
GPT_MCR. IARB=GPT_IARB; /* pick an IARB for the Timers */
/* interrupt level 6 */
/* vectors start at 0x40 */
/* sent OC1 out to pin */
CONM.OCONM5=ON; /* couple OC1 to OC3 */
TMSKON.OC3I=ON; /* enable the OC3 interrupt */
OCOND.OCOND5=ON; /* turn on OC3 when OC1 occurs */
TCTLON.OL3=ON; /* toggle OC3 when OC3 occurs */
TOCl=TCNT+pwm_period;/* set OC1 to the period */
TOC3=TOCl+pwm_count; /* set OC3 time on */
cli() ; /* enable the system interrupts */
/* the applications portion of the program */
FOREVER
{
}
}
/* The asynchronous service portion of the program */
©port void OC3 Isr( void) /* the PWM isr */
A Pulse Width Modulation Program 301
TFLG1.0C1F=0FF; /* reset OC1 interrupt flag */
if (0C1D.0C1D3 ==0N) /* compliment OC1D3 */
0C1D.0C1D3=0FF;
else
0C1D.0C1D3=0N;
TFLG1.0C3F=OFF; /* reset OC3 interrupt flag */
TOCl+=pwm_period;
TOC3=TOCl+pwm_count ;
Listing 6-2: Elementary PWM Program For The MC68HC16
In keeping with the new usage of header files for writing code
fortheMC68HC16, the header files he 16 . h, gpt.h, and sim.h
are included in the above program. These files contain definitions of
all registers needed for the implementation of this program. You should
include he 16 . h with every program or function that you write for
this part. In this case, most of the program involves registers within
the general purpose timer, so gpt . h is included. There is one regis-
ter accessed from the system integration module. Therefore, the header
sim . h is also included.
The MC68HC16 contains a software watchdog. The part comes
out of reset with the watchdog enabled. Therefore, unless a program
periodically accesses the watchdog, the part will execute a watchdog
reset. This periodic reset will make debugging of timing operations
difficult. The watchdog is disabled in the first instruction of the ini-
tialization of the program. The next three instructions are
MC68HC16-specific instructions.
The MC68HC16 has a seven-level interrupt system similar to the
MC68000 family of parts. The seventh level is the highest priority
and is the only nonmaskable interrupt for the part. The remaining
levels are progressively lower priority until the level is found. At
level zero, no interrupt is being processed, and this level is where the
processor usually operates. When an interrupt occurs, the hardware
level of the interrupt is placed in the interrupt priority field of the
code register. Further interrupts of the designated level or lower will
remain pending until a RTI instruction restores the IP field to a lower
level. This approach provides for priority selection among several
internal or external interrupt sources.
302 Chapter 6 Large Microcontrollers
Another potential problem occurs when two internal modules
are assigned the same priority level. In this case, a second level of
arbitration is set up to choose among these several modules, in the
event that more than one module requests an interrupt at the same
time. The module control register of each module has a field called
IARB. This 4-bit field is assigned by the programmer and it is an
arbitration level that will be applied when the processor must select
between two equal priority interrupts that occur simultaneously. The
interrupting module with the largest IARB value will be given con-
trol of the processor. The IARB field can contain values from to
15. When the module is being used, its IARB field must be assigned
a non-zero value, and no two modules can contain the same IARB
value. The code line
GPT_MCR. IARB=GPT_IARB; /* pick an IARB for the Timers */
places a value 5 into the IARB of the general purpose timer.
The interrupt level 6 is assigned to the GPT by the code line
ICR.IRL=GPT_IRL; /* interrupt level 6 */
and the vector base address is assigned a value of 40 by the code line
ICR.VBA=GPT_VBA; /* vectors start at 0x40 */
The means by which the vector assignment is accomplished in
the GPT is different from that for the remaining modules in the
MC68HC16. For the GPT, a 4-bit vector base address field is found
in the interrupt configuration register. A vector is an 8-bit value.
The vector assignment is accomplished when the value placed in
the VBA field of the ICR is used as the high nibble of an 8-bit num-
ber. The lower four bits are specified by the contents of Table 6-2.
There you will note that the vector address of OC3 is at 0xV6 . When
the contents of the VBA field is 4, then the vector for OC3 is 0x4 6.
The vector address is twice the value of the vector or 0x8 c in this
case. That is the reason that the address OC3_Isr is placed in the
address 0x8 c in the vector initialization routine.
You will note that each interrupt in the timer will be assigned a
vector with the most significant nibble of the value placed in the
VBA field of the ICR. There is a prearranged priority among these
several interrupts. One interrupt can be moved to the highest priority
among the several timer interrupts if desired. The priority adjust bits
A Pulse Width Modulation Program 303
Table 6-2: GPT Interrupt Priorities And Vector Addresses
Name
Function
Priority
Level
Vector
Address
Adjusted Channel
(highest)
OxVO
IC1
Input Capture 1
1
0xV1
IC2
Input Capture 1
2
0xV2
IC3
Input Capture 3
3
0xV3
OC1
Output Capture 1
4
0xV4
OC2
Output Capture 2
5
0xV5
OC3
Output Capture 3
6
0xV6
IC4
Output Capture 4
7
0xV7
IC4/OC5
Input Capture 4/Output Capture 5
8
0xV8
TCF
Timer Overflow
9
0xV9
PAOVF
Pulse Accumulator Overflow
10
OxVA
PAIF
Pulse Accumulator Input
11 (lowest)
OxVB
PAB field in the ICR allows this shift. For example, if the number 6
were placed in the PAB field, then the priority of OC3 would be
shifted from 6 to 0, where is the highest priority of the 1 1 levels
with the GPT. In this case, the vector for OC3 would be located at
0x4 0, and the vector address would be 0x8 0. None of the other
interrupt vectors or priorities would be changed by this operation.
The remaining code of the initialization section of the above pro-
gram is almost the same as that found in Listing 5-5. The register and
bit naming conventions used with the MC68HC16 are such that the
code written for the MC68HC11 can be used directly on the
MC68HC16. There is one change. In the MC68HC1 1, it was neces-
sary to set the DDRA7 bit to allow the output from OC1 to show up on
the pin PA7. The GPT has a different output pin arrangement on the
MC68HC 16 and it does not require the use of the DDRA register at all.
One additional modification: A cli ( ) instruction was used in
Chapter 5 to enable the system interrupts. There is no single bit in
the MC68HC16 that can be used to enable the interrupts. The 3-bit
field in the condition code register named IP sets the level of inter-
rupt that can be acknowledged. Since there is no equivalent instruction,
a macro definition of an instruction
#define cli() ("andp $fflf \n" )
is included in the header file he 16 . h. There is also a macro
#define sei() ("orp $00e0\n" )
304 Chapter 6 Large Microcontrollers
These two macros accomplish the equivalent of the same instruc-
tions for the MC68HC16. The cli ( ) instruction clears the bits of
the IP to zero, so that any interrupt will be acknowledged by the
processor. The sei ( ) instruction set the IP bits so that only a
nonmaskable interrupt will be acknowledged. Additional macros can
be written that enable the programmer to set the interrupt level any-
where between 1 and 7 if needed.
Recall that with the MC68HC 1 1 the two registers TFLG1 and TFLG2
were different from the usual registers in the part. To reset bits in these
registers, it is necessary to write ones to the designated bits rather than
zeros. On the MC68HC16, this anomaly has been corrected. On the
MC68HC16, to reset bits in the TFLG1 and TFLG2 registers the pro-
gram must write zeros to the appropriate bits. That change shows up in
two locations in the OC3_Isr routine. The much more logical
TFLG1.0C1F=0FF;
TFLG1.0C3F=OFF;
instructions are used here. Otherwise, the remainder of the interrupt ser-
vice routine shown in Listing 6-2 is the same as that found in Listing 5-5.
This portability is what you should expect when changing be-
tween the MC68HC1 1 and the MC68HC16 family of parts. Care has
been used in the design to assure that register names and bitfield
names are common between the families. Therefore, code written
for the MC68HC 1 1 should move to the MC68HC 1 6 with little change.
The need for change at all is caused by the fact that architectures of
the basic machines are different.
EXERCISES
1 . Modify the program shown in Figure 6-2 to allow the PWM range
to vary from 1 to OXFFF. Compile this program and test the code.
2. Write a macro that will permit the program to put an arbitrary value
between and 7 into the IP field of the condition code register.
Cosmic MC68HC1 6 Compiler 305
Cosmic MC68HC16 Compiler
The Cosmic compiler for the MC68HC16 is quite similar to the
MC68HC11 compiler. Its operation is the same. Several command
files are used in the course of executing a compilation, and these
files will be shown here. The first file is the program that completely
compiles and links the program. This file is shown below:
c -dlistcs +s +o %l.c
lnkhl6 < %l.lnk
hexhl6 -s -o %l.hex %l.hl6
pause
This command file requires as an input a file with a . c extension.
For example, if you were to compile the program newpwm . c , you
would enter
c : \ >comp newpwm
The first line will invoke the compiler and create a listing file, a
source assembly listing, and an object file named newpwm.o . The
second line invokes the linker named lnkhl 6, and requires an input
file named newpwm . Ink . The .Ink file is similar to that one dis-
cussed in Chapter 5, and a listing of the one for this program is shown
below.
# Link command file for NEWPWM. c
+h # multi- segment output
-max Oxfffff # maximum size
-psl6 -pc. # set up banking options
-o newpwm. hi 6 # output file name
+text -b # reset vectors start
address
vector. o # vectors
+text -b 0x400 # program start address
+data -b 0x700 # data start address
crts.o # startup routine
newpwm.o # application program
c : /ccl6/lib/libi .hl6 # C library (if needed)
c : /ccl6/lib/libm.hl6 # machine library
+def memory= bss # symbol used by library
306 Chapter 6 Large Microcontrollers
Here the entries are fairly well explained by the comments. Note
that two additional input files are required by this link file. The vec-
tor table vec tor . o is a compiled version of the vector listing given
in Listing 6-1. The crts . o object module is an assembled version
of the start-up routine for this machine. Both of these routines must
be written and compiled or assembled for the specific program. Oth-
erwise, the link command file is as discussed in Chapter 5. A version
of crts . s is shown below:
C STARTUP FOR 68HC16
Copyright (c) 1991 by COSMIC (France)
.external _main,
.external ._main,
.public _exit, stext
.psect _bss
sbss :
.psect _text
stext :
memory
bss
ldk #. bss
tbek
tbxk
tbzk
ldab #0fh
• start of the i/o memory space
tbyk
• put it in y
ldx #sbss ,
• start of bss
clrd ,
• to be zeroed
bra mtest ,
• start loop
bcl:
std 0,x ,
• clear memory
aix #2
• next word
mtest :
cpx # memory ,
• end of memory ?
bio bcl
• no, continue
aix #1000h
• 4K stack
txs ,
• for instance
jsr main,#. main ,
• call application
_exit :
bra exit
• loop here if return
Cosmic MC68HC1 6 Compiler 307
. end
In the above code, the EK, XK, and ZK registers are initialized
to the value found in . bss . The initial value of YK is set to
Oxf . The Y register will be used by the compiler to contain an offset
to all of the data contained in the control registers found in the header
files. Therefore, the YK register must be set to the top memory block
in the computer memory space.
Using the SCI Portion of the Queued Serial Module
The queued serial module (QSM) contains two parts. The first is a
convenient serial communications interface (SCI) which provides
asynchronous serial communications that is used between comput-
ers and other devices. The remainder of the QSM is a queued serial
peripheral interface that is often used for high-speed communica-
tions between computers and peripheral devices. This interface is
strictly synchronous.
Let's examine an interface between the program and a terminal
much like that found in Chapter 5. Here, the interface will simply
read in a number from the screen and put that number into the
pwm_count value for the PWM program. With this operation in
place, the operator can type in a value and change the PWM on time
at will. Listing 6-3 contains a program that will accomplish this end.
#include w hcl6 . h"
#include "gpt.h"
#include "sim.h"
#include "qsm.h"
#include "defines. h"
#define PERIOD 0x1000
#define ON_TIME 0x0800
#define SIM_IARB 4
#define GPT_IARB 10
#define GPT_IRL 6
#define GPT_VBA 5
/* set the baud rate=f clock/32*baud_rate */
#define BAUD_SET (32768*512 )/ (32*38400 )
/* function prototypes */
©port void OC3 Isr(void);
308 Chapter 6 Large Microcontrollers
/* External variables */
WORD pwm_period=0xl0 , p wm_count= 0x0800 , new_input=0 ;
BYTE new_character ;
main ( )
{
/* The initialization portion of the program */
/* initialize the SIM registers */
SYNCR.X=ON; /* set the system freq to 16.78 mHz */
SYPCR.SWE=OFF; /* disable the watchdog */
/* initialize the GPT */
GPT_MCR.IARB=GPT_IARB;/* pick an IARB for the timers */
ICR.IRL=GPT_IRL; /* interrupt level 6 */
/* vectors start at 0x40 */
sent OC1 out to pin */
couple OC1 to OC3 */
enable the OC3 interrupt */
turn on OC3 when OC1 occurs */
toggle OC3 when OC3 occurs */
/* set OC1 to the period */
/* set OC3 time on */
I CR . VBA=GPT_VBA ;
0C1M.0C1M3=0N
0C1M.0C1M5=0N
TMSK1.0C3I=ON
0C1D.0C1D5=0N
TCTL1.0L3=ON;
TOCl=TCNT+pwm_period;
TOC3=TOCl+pwm count;
/*
/*
/*
/*
/*
/* initialize the SCI
SCCRO . SCBR=BAUD_SET;
SCCR2 .TE=ON;
SCCR2 .RE=ON;
*/
/* set baud rate to 9600 */
/* enable the transmit and */
/* receiver of the SCI */
cli() ; /* enable the system interrupts */
/* the applications portion of the program */
FOREVER
{
if (SCSR.RDRF==ON) /* read in data if it is there */
{
new_character=SCDR; /* get new byte, reset RDRF */
while (SCSR.TDRE==OFF) /* wait until transmit
buffer empty */
SCDR=new_character ; /* send out byte and reset TDRE */
/* got an input, process it */
if (new_character>= ' ' &&new_character<=' 9 ' )
new_input=10*new_input+new_character- ' ■ ;
else if (new_character== ' \r ' )
{
/* reject any number out of range */
Cosmic MC68HC1 6 Compiler 309
/* and start over again */
if (new_input>=l && new_input<=4 04 8)
pwm_count=new_input ;
new_input=0 ;
}
else
new input=0; /^reject everything else*/
/* The asynchronous service portion of the program */
©port void OC3_Isr( void) /* the PWM isr */
{
TFLG1.0C1F=0FF;
if (0C1D.0C1D3==0N)
0C1D.0C1D3=0FF;
else
0C1D.0C1D3=0N;
TFLG1.0C3F=OFF;
TOCl+=pwm_period;
TOC3=TOCl+pwm count;
/* reset OC1 interrupt flag */
/* toggle OC1D3 */
/* reset OC3 interrupt flag */
Listing 6-3: PWM System With Keyboard Input
The qsm . h header file is included to add all of the register and
defines needed for the operation of the SCI. We are going to place
the operation of the SCI into the applications portion of the pro-
gram. Implementation of the SCI requires that a baud rate be selected
and the transmit enable along with the receive enable bits be set in
the serial communications control register number 2. The baud rate
is set to 38400 by placing a properly calculated value into the SCBR
field of SCCRO. With these lines of code, the serial communications
interface is set up and ready to work.
The code inside the application portion of the program is essen-
tially identical to that found in Chapter 6. The only difference is that
the variable names are changed to be more compliant with this appli-
cation. Here is another interesting case where the code written for
the MC68HC1 1 will move directly to the MC68HC16 with minimal
change.
310 Chapter 6 Large Microcontrollers
This program is a minimum SCI system. The SCI portion of
this chip has comprehensive capabilities that are not exploited in the
program above. These capabilities and error-checking devices are all
available on the chip, and their access anduse is explained in detail in
the QSM Reference Manual. Access of the various capabilities is
exactly the same as was shown above in this program.
Periodic Interrupt
To make the problem a little more interesting, let's add another
load to the machine by making use of the periodic interrupt capabil-
ity provided in the SIM to create an interrupt. With this interrupt, we
will build a time-of-day clock similar to that developed in Chapter 4.
In this case you will see that the applications code developed for the
MC68HC05 will work fine for the MC68HC16. Of course, the ini-
tialization code and the interrupt service routine will be completely
different for the MC68HC16. This additional code will be put right
into the program listed in Listing 6-3. It is asserted that there will be
no adverse interactions between the various sections of the code.
#include u hcl6zl.h"
#include "gpt.h"
#include u sim.h"
#include "qsm.h"
#include xx def ines .h"
#define PERIOD 0x1000
#define ON_TIME 0x0800
#define SIM_IARB 4
#define PIC_PIRQL 6
#define PIC_PIV 0X38
#define PIT_PITM 16
#define GPT_IARB 10
#define GPT_IRL 6
#define GPT_VBA 5
/* set the baud rate=f clock/32*baud_rate */
#define BAUD_SET (32768*512 )/ (32*38400 )
#define TIME_COUNT 1000
#define MAX_MIN 5 9
# define MAX_HOURS 12
#define MIN_HOURS 1
#define NO_WAIT
#define OC2_OFFSET 5
#define MAX SEC 5 9
Cosmic MC68HC1 6 Compiler 31 1
#define MAX_MIN MAX_SEC
# define MAX_HOURS 12
#define MIN_HOURS 1
/* function like macros */
#define get_hi (x) ((xJ/lO+'O 1 )
#define get_lo (x) ((x)%10+'0')
/* function prototypes */
©port void 0C3_Isr (void) ;
©port void PIT_Isr (void) ;
void output_time (void) ;
void putch(int) ;
void send_out (WORD) ;
/* External variables */
WORD pwm_period=PERIOD, pwm_count=ON_TIME, new_input=0 ;
WORD hrs , mts , sec , been_here=0 ;
char new character;
main ( )
{
/* The initialization portion of the program */
/* initialize the SIM registers */
SYNCR.X=ON;
SYPCR.SWE=OFF;
SIM_MCR . IARB=SIM_IARB ;
different */
PICR.PIRQL=PIC_PIRQL;
PICR.PIV=PIC_PIV;
PITR.PTP=ON;
PITR.PITM=PIT_PITM;
second */
/* initialize the GPT *
GPT MCR.IARB=GPT IARB;
/*
/*
/*
/*
/*
/*
/*
set the system freq to 16 . 88 mHz */
disable the watchdog */
IARBs for each module is
put all timers at level 6 */
vector is 0x38, address is 0x70 */
512 prescaler */
divide by 16*4, 1 tic per
ICR.IRL=GPT_IRL;
I CR . VBA=GPT_VBA ;
0C1M.0C1M3=0N
0C1M.0C1M5=0N
TMSK1.0C3I=0N
0C1D.0C1D5=0N
TCTL1.0L3=0N;
TOCl=TCNT+pwm_period; /
T0C3=T0Cl+pwm count;/*
/*
/*
/*
/*
/*
/*
/*
/
/*pick an IARB for the timers '
interrupt level 6 */
vectors start at 0x40 */
sent 0C1 out to pin */
couple OC1 to OC3 */
enable the OC3 interrupt */
turn on OC3 when OC1 occurs */
toggle OC3 when OC3 occurs */
* set OC1 to the period */
set OC3 time on */
/
312 Chapter 6 Large Microcontrollers
/* initialize the SCI */
SCCRO.SCBR=BAUD_SET; /* set baud rate to 9600 */
SCCR2 .TE=ON; /* enable the transmit and */
SCCR2.RE=0N; /* receiver of the SCI */
cli(); /* enable the system interrupts */
/* the applications portion of the program */
FOREVER
{
if (SCSR.RDRF==ON) /* read in data if it is there */
{
new_character=SCDR; /* get new byte,
reset RDRF */
while (SCSR.TDRE==OFF)
; /* wait until transmit
buffer empty */
SCDR=new_character ; /* send out byte and
Reset TDRE */
/* got an input, process it */
if (new_character>=' ' &&new_character<= ' 9 ' )
new_input=10*new_input+new_character- ' ' ;
else if (new_character== ' \r ' )
{
/* reject any number out of range */
/* and start over again */
if (new_input>=l && new_input<=4 04 8)
pwm_count=new_input ;
new_input=0 ;
}
else
new_input=0; /^reject everything else*/
}
if (sec>MAX_SEC)
{
sec=0 ;
if (++mts>MAX_MIN)
{
mt s = ;
if (++hrs>MAX_HOURS)
hrs=MIN_HOURS;
}
}
if (been_here && !new_input)
{
Cosmic MC68HC16 Compiler 313
been_here=OFF ;
output time ( ) ;
}
void output_time (void)
{
int i ;
putch( x \r' )
putch( *\t' )
putch( x \t' )
putch( x \t' )
send_out (hrs)
putch( x : ' ) ;
send_out (mts)
putch( x : ' ) ;
send_out (sec)
putch( x \r' ) ;
}
void putch(int x)
{
while (SCSR.TDRE==OFF)
/* send out a carriage return */
/* tab over the pwm_count */
/* on the screen */
/* wait until data
register is empty*/
SCDR = (char) x;
}
void send_out (WORD data)
{
putch(get_hi (data) ) ;
putch (get_lo (data) ) ;
}
/* The asynchronous service portion of the program */
©port void 0C3_Isr( void) /* the PWM isr */
{
TFLG1 .0C1F=0FF; /* reset 0C1 interrupt flag */
if (0C1D.0C1D3 ==0N)
0C1D.0C1D3=0FF;
else
0C1D.0C1D3=0N;
TFLG1.0C3F=0FF; /* reset 0C3 interrupt flag */
TOCl+=pwm period;
314 Chapter 6 Large Microcontrollers
TOC3=TOCl+pwm count;
©port void PIT Isr( void) /* the PIT isr */
{
been_here++ ;
sec++ ;
Listing 6-4: Clock Routine Added to PWM
If you compare this listing with that shown in Listing 6-3, you will
find that there are few structural changes to the program. The code
used to initialize the SIM is changed by the addition of the initializa-
tion of the periodic timer interrupt. This code is shown below.
/*
SIM_MCR. IARB=SIM_IARB
PICR . PIRQL=PIC_PIRQL /
PICR.PIV=PIC_PIV;
PITR.PTP=ON;
PITR.PITM=PIT PITM;
IARBs for each module
is different */
/* put all timers at
level 6 */
/* vector is 0x38,
address is 0x70 */
/* 512 prescaler */
/* divide by 16*4, 1
tic per second */
The interrupt arbitration level field in the SIM module control
register is set to 4. Recall that the value here can be anywhere be-
tween 1 and 15, with 15 the highest priority. All active internal
modules that are to use an interrupt must have a unique I ARB value.
The I ARB value for the GPT was set to 5. Note that the interrupt
level for both the GPT and the PIT is set to the level 6. Therefore,
both sources of timing have the same interrupt priority; however,
since the I ARB of the GPT is higher than that of the PIT, in the
event of a simultaneous occurrence of the two interrupts, the GPT
service routine will be executed before the PIT.
The interrupt vector for the PIT is placed at 0x3 8. Because the
address of the vector is twice the value of the vector, the interrupt
vector address is 0x7 0. A pointer to the PIT interrupt service rou-
tine will be placed at this address in the vector . c routine. The
periodic timer itself is set up by the next two lines of code. This
clock is driven by the EXTAL signal. In our case, the frequency of
Cosmic MC68HC16 Compiler 315
the EXTAL signal is 32768 Hz, not some number around 16 MHz.
The formula to calculate the periodic interrupt time is
T pi[ =4PITM(511PTP + l)/F extal
Here PITM is the 8-bit field with the same name found in the
PITR. PTP is a single-bit field in the PITR that can have a value of
either or 1. As such, if PTP is 1, the prescaler value of 512 is used.
Otherwise, when there is no prescaler, the value in the parentheses
reduces to 1 . With the values placed in these fields in the above code,
i.e., PTP of 1, and PITM of 64, and with a 32768-Hz external crys-
tal, the periodic interrupt time should be one second.
Two additional blocks of code are added to the applications sec-
tion of the code. This code is shown below:
if (sec>MAX_SEC)
{
sec = ;
if (++mts>MAX_MIN)
{
mt s = ;
if (++hrs>MAX_HOURS)
hrs=MIN_HOURS;
}
}
if (been_here && !new_input)
{
been_here=OFF ;
output time ( ) ;
}
The first eight lines of code here are taken directly from similar
clocking code found in Chapter 4. This code merely counts the time
in seconds, minutes, and hours. The second block of code determines
if a PIT has been serviced, and if it has, it resets the been_here
flag that indicated that the PIT service routine has been entered and
then sends the time out the serial port when output_time ( ) is
executed.
Here is a case where several subroutines are used in the applica-
tions portion of the program. output_time ( ) calls functions
putch( ) and send out ( ) . In turn send out ( ) calls
316 Chapter 6 Large Microcontrollers
convert_bcd ( ) . putch( ) is straightforward. This routine
waits until the transmit data register is empty and then stores the
character to be transmitted into the serial communications data reg-
ister. The routine send_out ( ) takes the character parameter passed
to it and causes it to be converted from integer format to two binary-
coded decimal BCD characters. These two characters are then
converted to ASCII characters by the addition of the character '0' to
each and then sent to the output. (Recall that convert_bcd ( )
was shown in Chapter 4 in Listing 4-8.) Another version of this func-
tion is shown in Listing 4-7. This alternate version has been tried in
the program above and it works as well as the function used above.
With the functions putch ( ) and send_out ( ) available, it
is a simple matter to write the code that will output the time to the
center of the top line of the screen. It is assumed that the cursor is on
the top line when the program begins to run. The last remaining
modification is the interrupt service routine PIT_Isr ( ) . Within
this function, the been_here flag is set, and the value in sec is
incremented. Since the applications portion of the program will pro-
cess sec and reset it whenever it reaches a value of 60, there is no
need for other code in this isr. For interest, listed below is the out-
put from the compiler for both OC3_I sr ( ) and PIT_I sr ( ) .
; 150 ©port void OC3_Isr( void) /* the PWM isr */
; 151 {
. even
_OC3_Isr:
pshm k, z ,y,x, d, e
tskb
tbek
tbxk
tbyk
tbzk
; 152 TFLG1.0C1F=0FF; /* reset OC1 interrupt flag */
ldy #0
bclr -1758, y, #8
; 153 if (0C1D.0C1D3==0N)
brclr - 1783, y, #8, LI 02
; 154 OClD.OClD3=OFF;
bclr -1783, y, #8
Cosmic MC68HC16 Compiler 317
155 else
bra L112
L102: ; line 155, offset 33
156 0C1D.0C1D3=0N;
ldy #0
bset -1783, y, #8
L112: ; line 156, offset 41
157 TFLG1.0C3F=OFF; /* reset OC3 interrupt flag */
ldy #0
bclr -1758, y, #32
15 8 TOCl+=pwm_period;
ldd _pwm_period
addd -1772, y
std -1772, y
159 TOC3=TOCl+pwm_count;
addd _pwm_count
std -1768, y
160 }
pulm k, z ,y,x, d, e
rti
161
162 ©port void PIT_Isr( void) /* the PIT isr */
163 {
. even
PIT_Isr :
pshm k, z ,y,x, d, e
tskb
tbek
tbxk
tbyk
tbzk
164 been_here++;
incw _been_here
165 sec++;
incw _sec
166 }
pulm k, z ,y,x, d, e
rti
Listing 6-5: Interrupt Service Routines
318 Chapter 6 Large Microcontrollers
Lines 150 through 160 above are the interrupt service routine
OC3_Isr ( ) and lines 161 through 166 comprise the PIT_Isr ( ) .
The main item that is observed here is the quality of the optimizer for the
compiler. In general, interrupt service routines, ISR, must save the com-
plete status of the machine prior to executing any code. The CCR along
with the PC are both saved by the interrupt sequence. The remainder of
the registers must also be saved if the ISR can use any of the additional
register resources of the computer. Usually this case will be found. Note,
for example, in OC3_I sr ( ) the first instruction is
pshm k, z ,y,x, d, e
This instruction causes the contents of all significant registers to be
saved on the stack, so that the status of the machine at the time the
interrupt occurred can be restored before control is returned to the
portion of the program that was interrupted. This restoration is com-
pleted by the two instructions
pulm k, z ,y,x, d, e
rti
which refills all of the registers with the values they contained when
the ISR was entered, and the rti instruction restores the condition
code register to the value it had and the program counter is then
restored. Thus, the status of the machine is restored and the program
control is returned to the interrupted instruction. Note that the rou-
tine P I T_I s r ( ) compiles to a much simpler routine. This routine
has simply two increment instructions and requires no register re-
sources. The optimizer recognizes this simple operation and does
not save the status beyond that saved when the interrupt occurred.
This routine has probably been pushed further than it should be,
for demonstration purposes. This routine demonstrates multiple in-
terrupts working simultaneously, keyboard input and output, and
generates a simple pulse-width modulation signal whose on period
is determined by a number entered from the keyboard. All of these
operations are quite similar to those shown in Chapter 5; however,
some major modifications in approach are required to meet the sev-
eral requirements of the peripheral components on the MC68HC16.
Let us now look at some other applications often used with
microcontrollers.
Table Look-up 319
Table Look-Up
Often in the implementation of a practical problem it is necessary to
implement a conversion of data in a completely heuristic manner.
Observations are made and a curve of sorts is fit to the data. It is then
desired to put a few samples of this curve into a data table and then
calculate values between these samples with an accuracy that usually
requires interpolation between the points contained in the data table.
Such tables are usually sparse in that the number of measured points
across the range covered by the table are few. The curve in Figure
6-1 shows an example of such a conversion table, and the table itself
is shown as Table 6-3.
250 t
20 40 80
Figure 6-1: Data Conversion Curve
120
180
Table 6-3: Look-Up Table
X
Y
20
5
40
15
80
50
120
120
180
240
The object of the program is to deliver to the program a number
like 67 and get the proper result back from the table look-up routine.
The x value of 67 lies between the 40 and the 80 entry in the table.
Therefore, the interpolation calculation needed in this case is
320 Chapter 6 Large Microcontrollers
J 67 -40)
Result = 15 + (50 -15)± '- = 38.625
v J 80-40
Notice that there is a product and a division that is the same for
all input values between 40 and 80 in this case. This calculation is
the slope of the line between the two points being interpolated. Usu-
ally table look-up operations are used where time constraints on the
program are great so that these operations, multiply and divide (es-
pecially divide), are unwelcome in these programs. There is a way to
avoid the divide operation. Note that between each set of points the
slope is a constant. Therefore, if the slope of the line is built into the
table, the calculation would involve one subtraction, one multiply,
and one addition. Let us modify the table above to contain the slopes
of the lines between each point.
20
5
0.5
40
15
0.875
80
50
1.75
120
120
2
180
240
Table 6-4: Look-Up Table With Slopes
Note that the slope of the line runs from the lower point to the
next point. Therefore, there is no slope for the last point. When this
table is built in memory, it consists of a header followed by a four-
byte entry for each entry in the table. The header consists of a single
byte that indicates the length of the table. In this case, since the table
starts with an independent variable value of 20, there are expected to
be no input values less than 20. If there are, however, the value of 5
will be used for these output values. Also, if the value of the indepen-
dent variable is greater than the maximum of 1 80, a value of 240 will
be returned from the routine. With data from the above table, the
calculation above would reduce to
Result = 15 + 0.875(67 - 40) = 38.625
which requires a multiply, a subtract and one add. Of course, the
routine will truncate the fractional part of the result so that the value
returned will be 38.
Table Look-Up 321
Each table entry contains four bytes. The first byte is the inde-
pendent variable value, often designated as x. The second byte
contains the dependent variable value, usually called y, and the final
two bytes contain a floating point format of the slope. The slope
must be floating point. The third byte contains the integer position of
the slope, and the fourth byte contains the fractional value of the
slope. With the binary point placed between the third and fourth bytes
of the table entry, we can accomplish some interesting multiply op-
erations. Prior to looking at the coding of this problem, let us complete
the table and convert the slopes into hexadecimal format.
5
20
5
0x00
0x80
40
15
0x00
OxeO
80
50
0x01
Oxd
120
120
0x02
0x00
180
240
Table 6-5: Look-Up Table with Hexadecimal Slopes
How does one build a table of this nature in C? Of course, it is an
array of structures, or it could be a structure that contains an array of
structures. The latter approach would be
struct entry
{
char x;
char y;
int slope;
};
struct lut
{
char entries;
struct entry data [5] ;
} _lut = {
5,
20, 5, 0x0080,
40, 15, OxOOeO,
322 Chapter 6 Large Microcontrollers
80, 50, OxOlcO,
120, 120, 0x0200,
80, 240
};
Note that in the table above, the slopes are entered in the table as
two-byte integers. There is no binary point in the slope. The conver-
sion of these integers to floating-point numbers is accomplished easily.
We must merely recognize that these numbers are a factor of 128 too
large, so after the slope is used as a multiplier, the result must be
divided by 128 to get the correct answer. Of course, division by 128
can be accomplished by a shift right by 8, or more practically merely
choosing the left byte of the product.
A function that will make use of this table is
char table_look_up ( char x_in)
{
int i ;
for (i = ;x_in>_lut . data [i] .x && i<=_lut . entries ; i + +) ;
if (i>=_lut . entries)
return _lut . data [_lut . entries- 1] . y;
else if (i==0)
return _lut.data[0] . y;
else
return _lut . data [i-1] . y+ ( ( (x_in-_lut . data [i-1] . x) *
lut . data [i-1] . slope) >>8) ;
}
Listing 6-6: Table Look-Up Routine, Version 1
The compiler optimizer will recognize the shift right by 8 in the
above function and will merely select the upper byte of the result
rather than executing the shift operation indicated.
This function was checked on an evaluation system for the single
input value of 67 and the result was the expected value of 38. Of
course, that did not check the function over its full range of opera-
tion. The check over the full range was accomplished on a host
machine. It is a simple matter to include the above function and table
in the following program.
#include "tlu.h"
void main (void)
Table Look-Up 323
int i ;
for(i=0;i< 256; i++)
printf( w i = %d r = %d\n" , i , table look up(i));
where tlu . h contains the function and the table above. This code
compiles and runs on a MS-DOC PC, and the result is as expected.
Here, the program scans the entire input range and prints out the
resultant value at each point. A check of the outputs will show that
the function hits the break value at each break, and creates a linear
interpolation between all points. For values above or below the range,
the proper output is observed.
In a normal control system, there will often be need for several table
look-up operations. The above code is not too good in this case because
the code to do the look-up must be repeated with each table. This extra
code can be eliminated if the function t ab 1 e_l ook_up ( ) is passed
two parameters: the first parameter is the interpolation value, and the
second value is a pointer to the look-up table as is shown below:
char table_look_up ( char x_in, struct lut* table)
{
int i ;
f or (i=0 ;x_in>table->data [i] .x && i<=table->entries ; i++) ;
if (i> = table- >entries)
return table->data [table->entries-l] .y;
else if (i==0)
return table->data [0] .y;
else
return table->data [i-1] . y+
( ( (x in- table- >data [i-1] .x) * table- >data [i- 1] .slope) >>8) ;
}
Listing 6-7: Table Look-Up, Version 2
This form of the table look-up should probably be used in all
cases. Here is another example of where it is wise to examine the
code generated by a compiler. It will not be listed here, but the as-
sembly code required for Listing 6-6 is 182 bytes long, and that for
listing 6-7 is but 142 bytes. This greatly improved utility automati-
cally gives a substantial savings in code.
It was pointed out that the compiler optimizer will sometimes
change the basic code dictated by the programmer. Perhaps, it should
324 Chapter 6 Large Microcontrollers
be stated that the optimizer changes the code suggested by the pro-
grammer. Often when you examine the code generated by the compiler,
the operations that take place will not even resemble those that the
programmer had in mind. This alteration of the code will show up
with shifts, and divides or products of powers of two. A proper optimizer
should determine if a multiply or a shift is better and provide the best
code. Often you will find that the compiler optimizer will provide
either fastest execution or minimum code. Usually the two will be
different. Faster code will almost always take more memory.
It may seem that the lost time is small, but you will always find
that if you want fast code, you will not use any looping constructs. The
code that increments a counter and tests the counter must be executed
each loop. Without the looping construct, this code is completely elimi-
nated, and its execution each loop will be completely eliminated.
Therefore, if you want fast code, you should eliminate looping con-
structs and repeat the code within the loop the desired number of times.
The cost of this move is more code. We have also seen that recursive
code can require an inordinant amount of time. Again, recursive code
merely creates hidden looping constructs that require frequent genera-
tion of stack frames prior to new function calls. The construction of
these stack frames and return from functions require time which is
often masked by the elegant appearance of the code. Usually you will
produce faster code if you figure a way to accomplish the same end
operation without calls to the executing function.
Today, we are at the very beginning of a completely new set of
microcontrollers. These machines are based on RISC (reduced instruc-
tion set computer) techniques. Do not be misled. RISC does not really
mean reduced instruction set. The instruction sets are complete and
extensive. However, the basic architecture of a RISC machine is quite
different from the older machines like those we have been working
with. One of the main differences is that the design of the machine is
to provide for the execution of one or more instructions with each
clock cycle. A standard RISC will allow almost one instruction per
clock cycle, and a super scaler architecture will allow two or more
instructions per clock cycle. This operation is enabled by the use of
instruction pipelines and multiple arithmetic logic units (ALUs). Some-
times, an instruction must use more than one clock to complete an
Table Look-Up 325
instruction. For example, an integer multiply instruction might require
five clock cycles. In that case, an instruction pipe five instructions long
would be implemented within the integer ALU. Therefore, a multiply
instruction would be launched into the instruction pipe for execution.
In the meantime, other instructions could execute while the multiply is
progressing through the pipe.
Compiler optimization for these machines must take into account
all of the pipes, the separate ALUs, and other unique operations when
creating the required assembly code. Clearly, a straightforward cre-
ation of code in the order that might seem to be natural to a
programmer will not necessarily create the quickest code for a RISC
machine. Here, speed optimization consists of arranging the code so
that, as nearly as possible, the maximum number of instructions are
launched each clock cycle. This rearrangement of your code will
make it extremely difficult to examine the assembly code version of
the program and even make sense of it. So long as the results are not
altered, the optimizer for a RISC machine will move instructions
around in the code stream to accomplish this end. Therefore, from a
practical sense, any debugging on a RISC machine will probably be
done with a source level debugger, and not an assembly language
version of the program.
The chip used in Chapter 8 is of the MCORE family of RISC
microcontrollers. We will see more of the above comments in that
chapter.
EXERCISE
1 . Sometimes two input values are needed to specify a parameter.
For example, if you recall from Chapter 5, the change in pulse on
time to properly control the motor speed was given by
Ap = -3809500 ^
P 2
create a sparse two-dimensional look-up table whose inputs are p and
Ap and which provides Ap as the result of three interpolations. Is the
look-up table better than the calculation in any way — less code, quicker,
etc.? Why would you use a look- up table in a problem like this one?
326 Chapter 6 Large Microcontrollers
Digital Signal Processor Operations
Most microcontrollers, regardless of their basic speed, are not
really able to process signals in real time. The basic speed of the
processors is fast enough to accomplish most signal processing; how-
ever, the set of things needed to do digital signal processing is not
usually available in the regular microcontroller. The basic digital sig-
nal processor (DSP) function that is required is summarized by three
actions: 1) the processor must multiply two values, 2) the processor
must add the product into a value, and 3) it must prepare for the next
multiply. This set of operations must execute quickly enough that the
computer can keep ahead of the real-time input of data being pro-
cessed. Almost all signal processing operations are based on the
multiply-accumulate sequence. Filtering, correlation operations, sca-
lar products of vectors, Fourier and other transformations, and
convolutions are but a few of the operations that are built around the
DSP multiply and accumulate sequence above. The MC68HC16 fam-
ily has an extension to its core that can execute basic DSP operations
fast enough to support real-time filtering, correlation, and so forth.
Unfortunately, C compilers do not know of these added func-
tions, so the C programmer would seem to be unable to include DSP
operations in programs. We will see here that, while it is rather in-
convenient, it is possible to write assembly functions that will permit
the programmer access to the complete DSP capabilities found in
this family of devices.
One of the good features of C is that it is not necessary for the
programmer to have detailed knowledge of the nature of the basic
computer being programmed. So far, we have had little to say about
accumulators or index registers or the like. No more! We must now
get inside of the computer to create functions that will accomplish
our DSP needs. When these functions are complete, we should be
able to treat the DSP operations in much the same manner that we
would any other function call. The DSP contains four registers and
controls three bits in the condition code register (CCR). The first two
registers are called the MAC multiplier input registers H and I, re-
spectively. These registers must be loaded with the multiplier and
the multiplicand to be executed. Data stored in these registers are
signed fractional binary numbers with the radix point between bits
15 and 14. The product will be accumulated into the MAC accumu-
Digital Signal Processor Operations 327
lator M register. This register is 36 bits long. For convenience, the
register is broken into two portions, bits 35 through 16 and bits 15
through 0. We will see later that different portions of this register are
moved by different instructions, so that breaking this register into
the two parts is logical.
The last register in the DSP register model is the MAC XY mask
register. Automatic selection of the addresses for the next multiply
needs modulo arithmetic. We will see more modulo arithmetic later.
The mask register contains the modulo base for both the x and y
index registers which will allow fixed coefficient tables, that can be
manipulated as either single-or two-dimensional arrays, to be tra-
versed automatically during successive multiply and accumulate
instructions. The DSP register model is shown in Figure 6-2 below.
20
16
15
8
H R
I R
A M
A M
XMSK
YMSK
Bit Position
MAC Multiplier Register
MAC Multplican Register
MAC Accumulator MSB[35-16]
MAC Accumulator LSB[15-0]
MAC XY Mask Register
Figure 6-2: MC68HC16 DSP Register Model
The CCR contains three bits that are associated with the DSP.
Bits 14 and 12 are DSP overflow flags. These flags will be discussed
in detail later. Bit 4 is called the SM bit and is the DSP saturation
mode control bit.
15
3
S
MV
H
EV
N
Z
V
c
11 12 13
SM
PK
DSP Control Bits
CCR
Figure 6-3: DSP Control Bits In The CCR
The accumulator is 36 bits with the radix point between bits 31
and 30. When accumulating into the MAC accumulator AM, there are
328 Chapter 6 Large Microcontrollers
two types of overflow that can occur. The first is when an addition
operation, which should always add fractions, causes an overflow
from bit 30 to 31. This type of overflow is reversible because the
arithmetic that caused the overflow cannot cause more than a 1-bit
overflow error, and bits AM [34-31] are there to absorb this type of
overflow. The contents of this register are signed, so that bit 35 of AM
is the sign bit. The maximum value that can be placed in the 36-bit
AM register is 0x7 f f f f f f f f . This value has a decimal value of
15.999969482. The minimum value is 0x8 00000000 correspond-
ing to -1 6. Whenever arithmetic involves the bits AM [34-31] the
EV bit will be set, and the program will know that the bits in
AM [35-31] are the signed integer part of the number. If successive
operations cause the overflow to disappear, the EV bit will be reset.
Another situation is less tractable. Suppose an overflow occurs as
a result of an arithmetic operation into the AM that causes bit 34 to
overflow into bit 35. This type of error is not reversible as is the case
above. When this overflow occurs, the MV bit is set to notify the pro-
gram of the result. An internal latch known as the sign latch SL will
contain the value of A [3 5] after the overflow has occurred. There-
fore, SL is the complement of the sign-bit when the overflow occurred.
To communicate with the DSP portion of the MC68HC16, we
have to use assembly language. In keeping with the basic rule to use C
whenever possible, the approach to be taken will be to create C-call-
able functions that access these important capabilities. What features
should we include in these functions? Obviously, all possible func-
tions cannot be conceived. A set of functions that will embody the
most important features of the DSP capabilities will be written.
Let us examine how this compiler transfers parameters to a func-
tion. When more than one parameter is passed, the parameters are
pushed on the stack starting with the right-most parameter in the
function argument list. The leftmost parameter is put into the D reg-
ister. If a single parameter is passed, it is placed in the D register
prior to the function call. Within the function, the D and X register is
saved on the stack, and the stack pointer is decremented by an amount
needed to provide space for all of the function variables. At that point,
the stack pointer is transferred into the X register. An example of this
code is shown below. This little function receives three parameters
and merely puts these values into three local variable locations.
Digital Signal Processor Operations 329
void dot_product (char length, int* xdata, int* ydata)
{
int i,*xp, *yp;
xp=xdata
yp=ydata
i=length
}
Listing 6-8: Sample Parameter Handling Function
The compiled version of the above function is shown below. The
first important instruction is the . even assembly directive that causes
the code to follow to start on an even boundary. Code must be started
at an even address. Note in the program above that three parameters
are passed to the function. After the contents of the X and the D
registers are saved, the stack pointer is decremented by 6 to provide
space for the variables i , *xp, and *yp. The two pointer param-
eters require 16 bits each, and the character parameter needs only 8
bits. The program, in favor of greater speed, will store the variable i
in a 16-bit location even though i is only 8 bits wide. At this time,
the value contained in the stack pointer is transferred into the X reg-
ister. During this transfer, the value will be automatically incremented
by two so that the contents of the X register will be pointed to the last
value stored on the stack rather than to the next empty location on
the stack as is found in the stack pointer.
1 ; Compilateur C pour MC68HC16 (COSMIC-France)
2 .include "macro.hl6"
3 .list +
4 .psect _text
5 ; 1 void dot_product (char length, int* xdata, int* ydata)
6 ; 2 {
7 . even
8 _dot_product :
9 pshm x,d
10 ais #-6
11 tsx
12 .set OFST=6
13 ; 3 int i,*xp, *yp;
14 ; 4
15 ; 5 xp=xdata;
16 ldd OFST+8,x
17 std OFST-4,x
18 ; 6 yp=ydata;
19 ldd OFST+10,x
330 Chapter 6 Large Microcontrollers
20 std OFST-6,x
21 ; 7 i=length;
22 ldab OFST+3,x
23 clra
24 std OFST-2,x
25 ; 8
26 ; 9 }
2 7 ldx 6,x
28 ais #10
29 rts
3 .public _dot_product
31 .even
32 .psect _data
3 3 .even
34 .psect _bss
3 5 .even
3 6 .end
Listing 6-9: Compiled Version Of Parameter-Handling Function
The diagram shown in Figure 6-4 will help you visualize what is
happening here. On the right side of this diagram you will find the
location at which the stack pointer is pointed at various times during
the function call and its execution. On the left side of the diagram,
you will see the locations pointed to by the X register. An offset
named OF ST is established by the program. This offset will have a
value equal to the space emptied on the stack with the ais instruc-
tion: in other words, in this case OFST will be 6. In every case from
this point forward in the program, variable and parameter accesses
will be indexed relative to the X register, and the total offset from the
X register will be a value OFST+k where k is a positive or negative
value that corresponds to the address of the parameter being accessed.
For example, the instruction
ldd OFST+8,x
will load the value at X+OFST+ 8 which you can see in Figure 6.4 is
the value *xdata. This value is stored at the location x+OFST-4
by the instruction
std OFST-4,x
which is the location where xp is stored on the stack. Remember
that each word on the stack is two bytes, so that all of the offsets and
address will be even numbers.
Digital Signal Processor Operations 331
The code in lines 16 through 20 above saves the values of xdata
and ydata in xp and yp respectively. The operations shown in lines
21 through 24 save the 8-bit value found in the B register to the
16-bit location at x+OFST-2. To be certain that the value is not
corrupted by some garbage value in the A register, the c 1 r a instruc-
tion is inserted prior to the time that the value is saved.
Stack Contents
SP after ais #-6
X after tsx
*yp
t
i
*xp
length
SP after pushm x, d
X + OFST
X at rts
X
t
D
SP after function call
CCR
t
PC
SP before function call
*xdata
*ydata
Figure 6-4: Stack Contents During Function Operation
After the closing brace of the function in the above listing, two
important operations take place. First, the contents contained in the
X register on entry to the function are restored by the instruction
ldx 6,x
and the stack pointer content is restored to the value it contained
when the function was entered. At this point in the program, an rts
instruction will return the program control to the instruction follow-
ing the j sr or bsr instruction used to enter the function code
originally.
When the function return is executed, the registers D, E, Y, Z, and
CCR are all undefined. A 16-bit return from the function will be returned
in the D register, and a 32-bit return will be contained in the E and the D
register. The least significant portion of the return is in the D register.
332 Chapter 6 Large Microcontrollers
What is a dot_produc t ( ) ? In vector algebra, a dot product,
or a scalar product, operates on all of the members of two vectors
and returns a single scalar result. This value is the magnitude of the
projection of one vector on the other. The familiar arithmetic form
for a dot product is
w-l
c = X a k b k
Note that all corresponding members of the two vectors are mul-
tiplied and summed. The result is a single number. Another important
calculation needed to be accomplished by a DSP is called convolu-
tion. A convolution is the time domain operation of a filter. Most of
the time, a designer thinks of a filter as operating on the different
frequencies of the signal being processed. In the frequency domain,
at every frequency the filter has a gain which is complex. "Complex"
in this case means the gain has two dimensions that can be thought of
as magnitude and phase. The signal also has a similar two-dimensional
description in frequency. At each frequency, the magnitude of the
filter gain multiplies the magnitude portion of the signal, and thephase
of the filter gain adds to the corresponding phase of the signal. There
are easy ways to treat this operation in the frequency domain. In fact,
the design of most filters takes place in the frequency domain.
However, the frequency domain is an artifact that we can never
really get our hands on. In reality, the signals we must deal with are
varying voltages or currents. These varying signals can be continu-
ous, or when converted to a tractable form for operation in a computer,
they are a series of samples. Let us call them x . Here x is the value
of the signal at sample points k. Now k might be thought of as re-
lated to time, and in fact different values of k do correspond to samples
taken at different times. Usually, k corresponds to samples taken pe-
riodically at carefully spaced, equal intervals.
A filter in the time domain has what is called a weighting func-
tion. The weighting function is indeed the Fourier transform of the
complex frequency response of the filter. In the continuous domain,
there is a mathematical trick that allows the weighting function to be
shown. A function called a Dirac Delta function is defined as a func-
tion that is everywhere except at one point. The integral across this
point is one. Such a function really does not exist. However, if such
Digital Signal Processor Operations 333
a function were delivered into the input of a filter, the output of the
filter would be the filter weighting function.
As we move to the digital realm, the Dirac Delta function is re-
placed by the Kroniker Delta function. This simple function, 8 k , is
for all values of k except for k = where its value is 1 . If this func-
tion is sent through a digital filter, the output observed is the weighting
function of the filter.
In both cases, analog and digital, the frequency response of the
filter is the Fourier transform of the filter weighting function. This
duality between the frequency response and the time domain response
makes it possible to design filters to accomplish what is really de-
sired. Usually, the filter specification is best established in the
frequency domain. The designer knows what frequencies are to be
passed or rejected by the filter. There has been a long history in the
field of passive network synthesis devoted to the "approximation
problem." How does one specify a filter to meet accurately a desired
frequency response? This problem has led to many sophisticated
approaches to the specification by mathematics of a frequency re-
sponse to meet the system need. More important, these frequency
responses have a nature that can be realized by a finite collection of
passive electrical components — resistors, capacitors, and inductors.
In other words, these frequency responses are realizable.
A series of mathematical transformations exist that can be used
to transform frequency responses directly to filter weighting func-
tions. We will not go into these transformations here, but will refer
you to Elliott for practical means to specify the weighting functions
for digital filters. 7 A more general text on this subject is by Antoniou. 8
The time domain response calculation is called a convolution. If there is
a signal x k applied to a filter with a weighting function h k there will be an
output from the filter at each time sample, and this output will be called
y k . The relations between these parameters are given by the equation
n-l
y k = X x k-i h
i=0
7 Elliott, Douglas R, Handbook of Digital Signal Processing and Applications:
Academic Press Inc. 1987
8 Antoniou, Andreas, Digital Filter Analysis and Design: McGraw Hill, 1979
334 Chapter 6 Large Microcontrollers
The convolution is little more than a series of dot products, one dot
product for each output sample. Therefore, if you have a function that
will calculate a dot product, it can also calculate a convolution.
An item of consequence is the use of modular arithmetic in calcu-
lation of the data addresses. Often times, it is desirable to traverse an
array and return to its beginning automatically when the end is reached.
Modular arithmetic allows this type of operation. When working in
combination with an unusual step value, modular arithmetic will per-
mit the collection of coefficients from a rectangular array placed in
linear memory space. Here the step value refers to one of the numbers
associated with the mac or the rmac instruction. These two values are
called xo and yo. This value is used to increment the address of the
corresponding register whenever data is loaded into either the H or the
I register. The location of the array in memory should be placed at
special location in memory. This location is discussed below. The ef-
fective address for the next value to be placed in the X after the value
is incremented by the xo register is given by
IX = (IX) &-XMASK | ( (IX) +xo) &XMASK
Note that XMASK here is an 8-bit mask that defines the length of
the circular array. The array length must be a power of two less than
or equal to 256. The value placed in XMASK is one less than the array
length. When the contents of IX, or (IX), is anded with the comple-
ment of the sign extended value of XMASK, the value that is left is the
starting address of the array. The second term above causes the con-
tents in IX to be incremented. As the value in IX is replaced by
( ( IX) + xo) &XMASK after each multiply and accumulate opera-
tion. Since XMASK must contain a number that is one less than a
power of two raised to the n: its least significant n bits are 1. The
value placed in IX is the address with the n least significant bits
masked off. When these two values are ORed together, an address is
created that will range from the beginning to the end of the array and
then back to the beginning in steps of xo. The requirements to make
this scheme work are:
1. The array length must be a power of two less than or equal to
256.
2. The array must begin on a specific address. This address is any
value where the least n bits are zero. Here n is determined by
Digital Signal Processor Operations 335
n = log 2 (array length)
or
array length = 2 n
3. The value in xo must be equal to the step between adjacent
data samples in the array.
This scheme can be used to move, in a modular manner, around
multiple-dimensional arrays as well as one-dimensional arrays. Let
us now modify the earlier code to show how a circular buffer can be
used to advantage.
The above compilations use the default memory model for the
Cosmic C compiler for the MC68HC16. This computer has a memory
addressing space of 20 bits. In the design of the part, the basic address
registers were all made 16 bits wide, and the additional bits required to
address the total space were placed in extension registers. Therefore,
for each of the addressing registers, the stack pointer, the program
counter, and the index registers X, Y and Z, there is a 4-bit extension
register. These registers are called SK, PK, XK, YK, and ZK respec-
tively. As mentioned earlier, several of these extension registers are
initialized upon reset, and the remainder must be initialized prior to
the execution of the main program. Usually, when a calculation alters
a value in an extension register, this change takes place seamlessly. It
is typically not necessary to worry about the contents of the extension
registers. Recall in the code for the initialization routine listed above,
crts . s, that the values for XK and ZK were set to and the value
placed in YK was Oxf . The reason for this choice is that the compiler
automatically uses the Y register as an offset when calculating the ad-
dresses listed in the various header files. Since all of these registers are
in the highest memory page, the YK value of Oxf is appropriate.
The default memory model is called the compact model, and the
code is compiled in the compact model so that all code, data, and stack
memory space is contained within one 65 kilobyte (K) memory bank.
Therefore, the initialized values of the extension registers need never
change. On the other hand, it is sometimes necessary to change the
values in these registers, and it is desirable to have the compiler take
care of this bookkeeping when needed. All of the additional memory
models will provide this tracking of the extension registers. These
models are: small, one 65 K bank for code and one 65 K bank for data
336 Chapter 6 Large Microcontrollers
and stack; program, multiple 65 K banks for code and one 65 K bank
for data and stack; data, one 65 K bank for code and multiple 65K
banks for data and stack; and finally far, multiple 65 K banks for code,
data, and stack. With each of these memory models, it is necessary to
keep track and often change the contents of the extension registers. As
a result, with these models, it is necessary to alter slightly the stacking
sequence. Such a sequence is shown in Figure 6-5. The function call
that will cause the stack to be arranged as is shown in the figure below
has four arguments. From left to right these arguments are xlen,
*xdata, ylen, and *ydata. The lengths are simple character
values, and the pointers in this case must be 20-bit values. If a function
call to this routine is compiled with the small memory model, or any
other model with the exception of the compact model, the stacking
will include the extension registers as shown below.
X after tsx
t
SP at rts
Stack Contents
XK
CCR
PC
xdatak
xdata
ylen
ydatak
ydata
SP after XK is saved
SP after pushm x, d
SP after function call
I
SP before function call
Figure 6-5: Stacking Sequence That Passes Extension Registers
The B register, which is the righthand side of the D register, will
contain the left-most parameter when the function is called. There-
fore, the parameter xlen will be found in the least significant bits of
the location labeled D in the above stack outline. The routine shown
below will make use of both the X and the Y registers. The compiler
allows for all registers with the exception of the X register to be
Digital Signal Processor Operations 337
altered by any function call. Therefore, it is necessary to save the
contents of the X register prior to altering this register.
; int circular_conv (char xlen, int* xdata, charylen, int* ydata)
. even
.set xlen=5
.set xk=ll
.set xdata=12
.set ylen=15
.set yk=17
.set ydata=18
_circular_conv :
pshm x, d
txK ; save old xk
pshm d
tsx
clrm ; clear the m register
clra ; put ylen into e register
ldab ylen,x ; ylen
addb #-1 ; decrement count to get correct
tde ; number of iterations
ldaa xlen,x ; xlen is the circular part
asla
of the convolution. Create
coma ; a mask that is the compliment
clrb ; of twice the length.
tdmsk ; put xlen in XMASK
ldab yk,x
tbyk
ldy ydata, x ; ydata with yk extension
pshm x ; save x
ldab xk,x
tbxk
ldx xdata, x ; xdata with the xk extension
ldhi ; load H and I registers for mpy
rmac 2,2 ; do rmac ylen times
tmer ; send result to e and then
pulm x ; restore x
ldx 2,x ; restore it to its original value
338 Chapter 6 Large Microcontrollers
pulm d ; restore old xk
tbxk
ais #4 ; fix the stack
ted ; data to be returned
rts ; return
. public _circular_conv
. end
Listing 6-10: Circular Convolution Routine In Assembly Language
Variable names get lost from the program when executing an
assembly language program called from C. Therefore, when a func-
tion is entered, the names of the variables are replaced by offsets
from a table saved by the compiler. Programming functions without
the aid labels and variable names requires careful attention to the
details of stacking and unstacking these data. It is recommended that
when a program is to be prepared, an abbreviated version like the
function dot_produc t ( ) above — which will guide the program-
mer in setting up the variable locations in memory — be written. In
the above routine, the offset OF ST was not used, and the various
offsets were assigned names that correspond to the variables. This
approach makes it easier to understand what the program is doing
and easier to write the code correctly.
It is important that the contents of the XK register be restored to its
initial value when a function call is returned to the calling program.
The content of the X and D registers is saved on the stack and the value
or XK is then saved. The contents of the stack are placed in the X
register, and this register is incremented by two so that it will point to
the last data pushed on the stack. The M register is cleared, and the
parameter ylen is moved into the E register. This value is used with
the rmac instruction to tell how many times the mac instruction is to
be repeated. The value of xl en is the length of the buffer that contains
input data. This buffer is fed circularly so that when the calculation
reaches the last entry in the buffer the pointer into the buffer will be
returned to the top of the buffer to get its next entry. The length xlen
is the number of entries in the buffer, but the length for the calculation
must be the number of bytes in the buffer. Therefore, the value of
xlen must be doubled prior to the creation of a mask to be used in
XMASK. In this case, XMASK value is calculated, and YMASK which is
stored as the B register content of the D register is made zero. There-
Digital Signal Processor Operations 339
fore, there will be a circular buffer calculation on X, but there will be
no circular calculation on the Y register.
After the MASKs are set up, the two 20-bit pointer registers, X
and Y, are assigned the values passed as parameters, and then the
product data are put into place. The rmac instruction is then ex-
ecuted the number of times indicated by the contents of the E register.
The register contents are then restored and the result of the single-
point convolution is moved into the D register before control is
returned to the calling program.
A simple program was written to test the operation of this func-
tion. This function does not attempt to make a digital filter, but it rather
sets up to show how the circular convolution works. In this case, it was
intended that the program run on an EVB16 board that has a normal
serial interface to an RS232 port. Therefore, the data being tested can
be sent out of the serial port to a terminal. The purpose of the program
is really quite simple. It is to execute a convolution between a short set
of coefficients and a long set of data. The coefficient set is 32 integers
long, and the data set is 64 integers long. The coefficient data is a
descending array of numbers that are in the upper byte of the number.
These numbers start at 3 1 and reduce successively to zero. The data
that will be used here are merely the numbers through 63 in the
upper byte of the number. The system is set up, the system frequency
is set to 16.78 MHz, and the watchdog is disabled. The serial port is set
to 9600 baud and the SCI transmitter is enabled.
#include u hcl6.h"
#include "sim.h"
#include "qsm.h"
int putchar(char new_character) ;
void dprint (int c) ;
void main (void)
{
int circular_conv (char , int*, char, int*);
int data [64], coef [32] , point [64] , i , j , *ip;
/* initialize the SIM */
SYNCR.X=1; /* set the system freq to 16.78 MHz */
SYPCR.SWE=0; /* disable the watchdog */
/* initialize the SCI */
340 Chapter 6 Large Microcontrollers
SCCRO .SCBR=55; /* set baud rate to 9600 */
SCCR2.TE=1; /* enable the transmit of the SCI */
for (i=0 ; i<64 ; i++)
data [i] =i*0xl00;
for (i=0; i<32 ; i++)
coef [i] = (31- i) * 0x10 0;
for (i=0; i<64 ; i++)
{
ip=data+i ;
point [i] =circular_conv (64 , ip, 3 2 , coef) ;
}
for (i=0;i<8;i++)
{
for ( j=0; j<8; j++)
{
dprint (point [j+8*i] ) ;
putchar ( x x ) ;
}
putchar (Oxd) ;
putchar ( Oxa) ;
}
int putchar (char new_character)
{
while (SCSR.TDRE= = 0) ; /* wait until transmit buff er empty */
SCDR=new_character ; /* send out byte and reset TDRE */
}
void dprint (int c)
{
if (c<0)
{
putchar ( x - ' ) ;
c = - c ;
}
if ( c/10)
dprint (c/10) ;
putchar (c%10+' ■ ) ;
}
Listing 6-11: A Test Program for Circular Convolution
Digital Signal Processor Operations 341
Next, the circular convolution program is executed 64 times. These
results are stored in the array point [] . This array is then sent out
the terminal eight numbers at a time. A few lines of code are ex-
tracted from Figure 6-2 to make the put char ( ) function. Finally,
the function dprint ( ) sends the data out of the serial port to the
terminal. The output from this program is as follows :
64 64 64 64 64 64 64 64
64 64 64 64 64 64 64 64
64 64 64 64 64 64 64 64
64 64 64 64 64 64 64 64
64 64 64 64 64 64 64 64
64 64 64 64 64 64 64 64
64 64 64 64 64 64 64 64
64 64 64 64 64 64 64 64
This program is merely a test program to show that the circular
convolution does indeed work. The fact that the output data is always
the same value, decimal 64, shows that the addresses are handled
correctly inside of the circular convolution. Let us examine first to
determine why the answer should be 64. The coefficients and data
are each 0x10 0. Two of these values are multiplied and summed 32
or 0x2 times. Therefore, one would expect that the result would be
0x2 0000. However, you must remember that each of the above
products is in fact the product of two binary fractional numbers. The
binary point in each case is between bit numbers 14 and 15. The
product of these numbers will yield 0x100 0, but in that case, with
the binary point between bits 29 and 30. Actually, the binary point
dictated by the microcontroller is between bits 30 and 31. The an-
swer is corrected to this binary point location in the M register, and
the result of the product is then 0x2 0. This number is summed
32 times and the final result is 0x4 000 0. When the M register is
moved into the E register and then into the D register, the value that
is saved is 0x4 0, or 64, as the test program showed.
Remember Equation 6-1 for the convolution:
n-l
y k = X x k-i h i
i=0
342 Chapter 6 Large Microcontrollers
In this expression, it seems possible that the x subscript can have a
negative value. Actually, such a case is not possible because a nega-
tive subscript implies that data is used before it is available. Positive
subscripts correspond to time that has already passed. Values of x
will be zero for negative subscripts. If the kth sample corresponds to
"now," increasing values of i will get older samples of x. This op-
eration can lead to a little problem in creating the code for the
convolution. The looping construct within the assembly program
above selects the different values of i in the above equation. The
coefficients h. are placed in memory in successive order so that an
increase in the value of i will select the correct next coefficient.
However, if the data values x were placed in memory as one would
naturally expect, the newest value of data would be at the current
array index, and old values of the data would be at lesser index val-
ues. This arrangement will not work correctly. The data must be placed
in the array backwards in order to get the convolution to work. Older
data must be at higher indices than the current data sample. Also,
when filling the array initially, the program should start at the top of
the array rather than the bottom. The routine listed below will store
the data properly in the array.
int data [64] ;
int handle_data (char new_data)
{
static int i = 63;
if (i<0)
i = 63;
data [i— ] =new_data;
return i+1;
}
Listing 6-12: Convenient Data Storage For DSP Use
This routine is integrated into the code shown in Listing 6-11
and used to test the circular convolution. This resultant program is
shown in Listing 6-13. In this case, the variables data and coef
are moved outside of the function main ( ) to make them global.
Digital Signal Processor Operations 343
#include "hcl6 . h"
#include u sim.h"
#include "qsm.h"
int putchar(char new_character) ;
void dprint (int c) ;
int handle_data (int new_data) ;
int data [64] @0x2000;
int coef[32] @0x2100;
void main (void)
{
int circular_conv (char , int*, char, int*);
int point [128] , i , j , *ip;
/* initialize the SIM */
SYNCR.X=1; /* set the system freq to 16.78 MHz */
SYPCR.SWE=0; /* disable the watchdog */
/* initialize the SCI */
SCCRO .SCBR=55; /* set baud rate to 9600 */
SCCR2.TE=1; /* enable the transmit of the SCI */
for (i=0 ; i<64 ; i++)
data [i] =0x00;
for (i=0; i<32 ; i++)
coef [i] =0x100;
for (i=0; i<64 ; i++)
{
/* get new data— use 0x100 for this test */
ip=data+handle_data ( 0x10 ) ;
point [i] =circular_conv (64 , ip, 32 , coef) ;
}
for (i=0;i<8;i++)
{
for ( j=0; j<8; j++)
{
dprint (point [j+8*i] ) ;
putchar ( x x ) ;
}
344 Chapter 6 Large Microcontrollers
putchar(Oxd) ;
put char ( Oxa) ;
}
}
int handle_data (int new_data)
{
static int i = 63;
data [i— ] =new_data;
i &=63;
return (i==63) ?0 : i+1 ;
}
Listing 6-13: Test Program For The Circular Convolution
Then the code to send the data to the circular_conv ( ) is
modified slightly to that shown below.
for (i=0;i<64;i++)
{
/* get new data — use 0x100 for this test */
ip=data+handle_data (0x10 0) ;
point [i] =circular_conv (64 , ip, 32 , coef ) ;
}
The value 0x10 is sent into the function handle_data , and
the return is the index into the array where the data was stored in the
array data. When this integer is added to data, which is a pointer to a
type int, the pointer ip will point to the location into the array where
the latest value was stored in memory. Under normal circumstances,
this piece of code would be entered under control of a clock and the
data sent into the routine handl e_da t a ( ) would be new input from
an analog to digital converter. Also, the for statement is not expected
in a practical application. The output from this program is:
2 4 6 8 10 12 14 16
18 20 22 24 26 28 30 32
34 36 38 40 42 44 46 48
50 52 54 56 58 60 62 64
64 64 64 64 64 64 64 64
64 64 64 64 64 64 64 64
64 64 64 64 64 64 64 64
64 64 64 64 64 64 64 64
Other MC68HC16 Considerations 345
The data array starts out empty, and the coefficients are each Oxl .
The output is as you would expect — it starts and increases linearly
from to 64 in the first 32 samples. The output then remains at 64 until
the end of the test. This linear ramp is exactly what one would expect
when sending a step function into the constant weighting function.
There is one important point to be found in this program. It was
stated earlier that the programmer should attempt to keep the code in
C whenever possible. In the above case, we introduced a rather simple
function in assembly language. The fact that the program was a func-
tion, and it was necessary to use a large memory model, the code
required to handle the data as it was passed into the function is much
longer than you would expect. Also, the implied C code in preparing
the machine for the function call and the handling of the extension
registers in the main program makes the overall program larger than
expected. Unfortunately, it is not possible to access the DSP with C,
so if you wish to do DSP operations, assembly language access is all
that you can use.
Other MC68HC16 Considerations
The discussion in this chapter has been dedicated to the
MC68HC16 microcontroller. Of course, there are components of this
microcontroller that are not covered in this chapter. No attempt was
made to outline the access to the analog-to-digital converter, several
features of the general purpose timer, the serial peripheral interface,
or the static RAM. However, the programs shown here do cover
enough of the part to demonstrate that the on-board peripherals can
be accessed from the C language. In the approach used, the header
files contain all of the bit field definitions needed to access any bit or
bit field in any control register in the part.
Chapter 7
Advanced Topics in Programming
Embedded Systems (M68HC12)
During the past few years, we have seen an unbelievable
proliferation of embedded systems products. Devices that could only
be imagined ten years ago are commonplace today and their very
existence demonstrates the importance of C programming for small
microcontrollers. Let's take a look at one such application and see
how easily you can develop rather complicated applications on
microcontrollers.
Throughout this text, much emphasis has been placed on the
construction of small functions and then integrating these functions
into a working package. This approach is about the only way that
you can really hope to create a complicated piece of firmware in a
sensible time. My early projects would always start with careful design
of the whole project, partitioning of the project into sensible modules,
design of each module, writing the code for each module, integration
of the whole project and then, after cleaning up syntax errors, I'd
begin to test the whole program. What a disaster! These programs
would never work and there would be no hope of ever getting the
package to run as a unit.
As I gained experience, I found that the top-down approach I
used was probably satisfactory if I tempered the integration of the
system. Today, I start with a careful design for the whole system.
This design is partitioned into constituent components. At that point,
another look at the design is in order to see if the existing components
can be further reduced into sensible components. At that point, the
lowest-level components are coded. Often these functions are so
simple that they work when first tested. Whenever there is a required
347
348 Chapter 7 Advanced Topics
interface for a component, I take the time when it is being created to
write stub functions to provide any necessary interface to test the
function. Every small function is tested as it is written.
The hierarchy of these functions is built upward. Those functions
that make use of the lower-level functions are coded and tested, and
so forth until the whole project is completed. Every function is written
and tested as it is created so that, as the program builds upward, it is
always based on known working modules. And this approach is used
to complete the whole program.
At every phase in the construction of the project the key words
are test, test, test. Every function is tested at its creation and, therefore,
the whole project is built upon relatively simple, small functions that
have been tested and work. Does this approach guarantee that no
bugs will be built into the program? Does it guarantee that the final
integrated version of the program will work as desired? In both cases,
the answer is a resounding NO. However, you have at least some
chance of creating a program that can be debugged and will meet the
desired specification. Also, you will find that writing and testing a
series of small functions always requires less time than writing and
testing the aggregate, more complicated function.
In summary, make your functions small, test them until there is no
possibility of hidden errors, revise them and retest them always with
the intent of reducing the function size. Then, when you have completed
the function, parade it in front of your peers and see if they can suggest
ways to improve the functions. Build your project with such blocks
and you will have a reasonable chance of meeting your deadlines.
In this chapter on the M68HC12, we will discuss programming
into the chip a part of the features of a telephone, or perhaps of an
electronic phone book. Note that the problem discussed here is but a
small part of the control of a telephone. A telephone book function is
one complete module that is a part of the telephone control.
As it stands, the HC12 has a small amount of EEPROM into
which a phone book can be stored. The chip also has a large amount
of FLASH ROM in which the program is stored. The FLASH memory
has some disadvantages that discourage its use to store the telephone
book data. On these chips, the FLASH memory is broken into two
parts. The smaller portion of the FLASH is called BOOT FLASH
and the larger portion is specified for general program storage. If
you need to erase any memory, the whole block, either the BOOT or
Advanced Topics in Programming Embedded Systems (M68HC12) 349
the general program storage, must be completely erased. Therefore,
if you should want to change the contents of the phone book, the
whole block would have to be erased and rewritten. Another approach
that could be used is just to assume that you will have enough FLASH
to write over and discard the memory used and move the entry to
new memory whenever it is changed. This approach, however, is
wasteful of the memory.
The EEPROM does not suffer this problem. Any individual byte
in the EEPROM memory block can be written, read, or erased without
a problem. Also, the EEPROM can be erased and rewritten at least
10,000 times without deterioration. FLASH, on the other hand, is
specified to be able to withstand 100 erase/write cycles during its
lifetime. All in all, FLASH makes a good program storage memory
and EEPROM makes a better changeable nonvolatile memory.
The C compiler used in this chapter is that provided by Cosmic. This
compiler is essentially the same as that seen in Chapters 5 and 6 except,
of course, it creates code for the M68HC12 family of parts. One very
nice extension of the compiler is the ability to identify EEPROM in the
code. The approach followed here uses the following #pragma:
#pragma data [768] @eeprom
It identifies data as an array 768 bytes long that is stored in
EEPROM. At link time a command like
+seg . eeprom -b OxdOO -m768
indicates that the EEPROM will be found at the address OxdO and it
will be 768 bytes long. The nice feature derived from this extension to
the C language is that any assignment to the data array will first erase
the byte and then store the data into the specified location automatically.
The fact that the HC 12 component that is to be used for this project
contains a large amount of FLASH and little RAM leads to some
difficulty in writing code for use on this part. There are no development
environments that contain sufficient RAM to test a significant program.
Therefore, prior to executing the first byte of code on the target chip,
you must be more certain than normal that the code will work as
intended. I solve this problem by developing most of the code for the
final configuration to execute in a DOS environment and only after I
have a complete working program is there any attempt to move it into
the target system. Code developed in this manner was discussed in the
350 Chapter 7 Advanced Topics
previous paragraphs. The main difference is that the development
flow will be for a DOS-based system and the microcontroller-based
code will be very carefully designed and tested and integrated into
the program as it is developed. The final tests after the code has been
transferred to the target system will be limited to those items that are
specific to the target system only.
Let us look now at what we would like our phone book code to
do.
The purpose of the program is to allow storage of names and
telephone numbers in the EEPROM section of an M68HC912B32.
This chip has 768 bytes of EEPROM and 32K bytes of FLASH
EEPROM. It has an on-board UART through which all of the
communications with the chip are conducted. One of four single-
letter commands can be entered into the system:
Command Response
n Receive a NAME terminated by an <enter>
followed by a phone number also terminated by
an <enter>.
s Display the entire directory contents.
a Display the next directory entry.
r Delete the entire contents of the directory.
Nonvolatile storage is at a premium. Therefore, all data stored in
EEPROM will be encoded to compress the data as much as practical.
All numbers will be stored in BCD form. This approach requires 4
bits per stored number when, in fact, 3.32 bits per digit is required if
it is assumed that the use of each number is equally likely. Confusion
can result when an empty number is stored, so the value stored for
the number will be Oxa rather than 0x0.
Alpha, or letter, data will be compressed using a Huffman code
as was shown in Chapter 5. This code will be written specifically to
compress data from the names found in a telephone directory. Fre-
quency of letter usage here is different from that found with general
English text. The decode scheme to be used here will follow the
general approach given in Chapter 5.
As a first estimate, the following functions will be required in
putting this program together:
Advanced Topics in Programming Embedded Systems (M68HC12) 351
Monitor
Encode
Decode
Numdup
Putbcd
Getchar
Putchar
Get
Printout
Printafter
Saveit
Reset
This function executes all of the time and receives
data from the keyboard. It interprets the entries and
passes control to the appropriate function to execute.
Encodes the alpha data read from the keyboard
with a Huffman code.
Decodes the Huffman encoded data stored in
FLASH when needed.
Converts numeric data passed in an array to the
modified BCD format and saves these data in the
FLASH array, also a passed parameter.
Converts the encoded numeric data contained in
the passed array to ASCII form. Places the con-
verted data in an array that is passed to the function.
Reads in a character from the serial port. This func-
tion and putchar ( ) below work with the
standard library input/output functions that will
be used by the program.
Sends a character to the serial port.
Reads in a character string, either numeric or al-
pha from the serial input.
Prints the contents of the phone book stored in
EEPROM.
Prints the next entry in the phone book.
Saves the phone book entry in the proper
EEPROM location.
Erases the contents of the EEPROM.
Most of these functions have nothing to do with the underlying
computer. Therefore, we will write code that is completely indepen-
dent of the computer. If there is ever a potential modification in this
code when changing to the embedded microcontroller, standard com-
piler control commands will be used.
352 Chapter 7 Advanced Topics
Numeric Encoding
Data will be entered from a keyboard that is assumed to provide
an ASCII character stream. In operation, the first entry will be the
telephone number. As this number is received the coding will be
converted from ASCII to a modified BCD format. The modification
is needed to eliminate trouble with the occurrence of zeros in the
number. The conventional BCD encoding for a zero is a 0x0 that is 4
bits wide. If you should have a double zero, the encoded version
would be an 8-bit zero or '\0\ which is interpreted in C as an end of
a character string. That confuses the issue enough that it was decided
to encode the digit zero as Oxa. The literal interpretation of this
number is the value ten. But, with our BCD encoding, the number
ten will never occur, so it is safe to use this value for the value zero.
/* The ascii data in the constant array s [] con
tains a number. These data are converted to a
modified BCD form and stored in the array
array [] . The number zero is stored as Oxa. The
series is terminated with a null character
followed by an enter character. */
#include <ctype>
int numbdup (char * const s, unsigned *array, int len)
{
char *pq, *sp;
int i ;
sp=s; /* use local pointer */
pq= (char *) array; /* convert pointer to character */
f or (i=0 ; i<len; i++) /* empty the array */
pq[i] =0;
i = 0;
while (*sp I = ' \0 ' &&*sp ! = ' \n' ) /* read until termination char*/
{
*pq=0;
if (isdigit (*sp) ) /* convert the inputs two at a time*/
Numeric Encoding 353
if(*sp== / 0') /* handle a zero input */
*sp=' ' +0xa;
*pq| = (*sp-' ' ) <<4 ;
}
else
pq|=0xf0; /* non digit, mark it */
if (isdigit (* (sp+1) ) ) /* the next input */
{
if(*sp== / O l ) /* treat a zero */
*sp=' ' +0xa;
*pq|=(*(sp+l) -'0') ;
}
else
pq|=0xf; /* another non digit */
sp+=2; /* Increment the input data pointer, */
pq++; /* the output data pointer, */
i++; /* and the data count */
}
return i; /* length of the array */
Listing 7-1: Numeric Encoding Routine
All of the storage arrays in the EEPROM are of the type un-
signed int . Here the storage of both numeric data and alpha
data requires that every bit of every storage location be used so that
unsigned is the norm. In the coding routines, the data are passed in as
characters and the destination arrays are unsigned. Therefore, to aid
the local bookkeeping, the destination array pointer is immediately
assigned to a type char * and this pointer will be used to store the
encoded data.
The following program provides a simple test for the numeric
coding. This program has a serious problem though. All computers
configure memory in either a big endian or a little endian order. In
the big endian order, the most significant byte, 8 bits, of a 16-bit
address is given the smaller address and the least significant byte
goes to the larger address. The little endian order is just the reverse.
354 Chapter 7 Advanced Topics
Here the least significant byte of data is assigned to the smaller ad-
dress and the most significant byte goes to the larger address. Almost
all Motorola chips use big endian, and almost all Intel chips use little
endian. There can be some confusion when developing code to run
on one style of data storage on a machine with the opposite. This
problem is seen in the following program.
# include <stdio .h>
main ( )
{
unsigned array [25] ;
int i ;
numbdup( "123456789098765", array, 25) ;
for (i=0;i<8;i++)
printf ( u %x" , array [i] ) ;
putchar ( x \n' ) ;
}
Listing 7-2: Numeric Encode Test
If this program is compiled with a PC (Intel-based) compiler, the
result will not appear to be correct. However, if the program is com-
piled on an HC12, or 68HC16, or 683XX, or 68HC11, or 68HC05
compiler, it will seem to work correctly. In fact, both results are cor-
rect, only the numeric representation in memory is different.
Numeric Decoding
Once the numeric data are encoded and stored, they must be de-
coded to be used by other parts of the program. The decode routine
is called putbcd ( ) . This function is shown below.
void putbcd (char *s,char *number)
{
int c, i=0 ;
char *sa;
sa=s;
while(*sa!='\0' )
{
Numeric Decoding 355
}
if (isdigit (c= (*sa>>4) +' 0' ) )
number [i++] =c;
else if (c = =' ' +0xa)
number [i++] = ' ■ ;
if (isdigit (c= (*sa&0xf ) +' ' ) )
number [i++] =c;
else if (c = =' ' +0xa)
number [i + + ] =' ' ;
sa++ ;
}
number [i++] =' \n' ;
number [i + + ] =0 ;
Listing 7-3: Numeric Decoding Routine
In this function the output data is called number [ ] and the in-
put is s [ ] . The encoded data in s [ ] is converted one BCD 4-bit
field at a time to ASCII characters. In the event that a character re-
ceived has a value 'O'+Oxa, it is then a character zero or '0'. Each
byte is converted from two 4-bit BCD values to two ASCII charac-
ters that represent the proper digits.
The test program for this routine is a combination of the encode
test routine with one to decode the encoded data. As one would ex-
pect, when both the encode and the decode routine are used together,
the result is correct. This observation is true on either the Intel or the
Motorola style chip. The endian-ness of the chip is immaterial when
the entire encode/decode operation is completed.
# include <stdio .h>
#define ARRAY_SIZE 10
int decode (unsigned M[],char *s) ;
int encode(char *a, unsigned *array,int length);
main ( )
{
char a [ARRAY_SIZE] ;
int c, i=0;
unsigned array [ARRAY_SIZE] ;
char s [ARRAY SIZE] ;
356 Chapter 7 Advanced Topics
while ( (c=getchar ( ) ) ! =' \n' )
a [i + + ] =c;
a[i]='\n' ;
encode (a, array, ARRAY_S I ZE) ;
decode (array, s) ;
printf ("%s" ,s) ;
Listing 7-4: Numeric Decode Test
Coding the alpha data
For encoding and decoding the name data to be stored in our
phone book, we will use a Huffman code. We saw the decoding of a
Huffman code in Chapter 5 and the decoding approach used here
will be almost the same as was used there. In the discussion in Chap-
ter 5 there was no encoding and that feature must be added here. To
do justice to the encoding technique, it is necessary to try to build the
code to encode the type of text that a phone book represents. There is
no reason to suspect that a collection of English names will contain
the same character frequency as standard English text. It is necessary
to understand the frequency of occurrence of each letter in the text to
be encoded. With this understanding, you can write a code that as-
signs few bits to frequently occurring letters and more bits to letters
that occur less frequently.
The program below reads in data, counts the occurrences of let-
ters, both upper and lower case, in a document. The occurrence of
letters is sorted in order of decreasing occurrence. These data are
printed out. The program calculates the average theoretical entropy,
bits per character, of each character in the document and displays
this number. Also included in the calculations are the space, ' ' , char-
acter and the new line, An', character. These characters cause the
output to be distorted, so the character '>' is used to indicate a space
character and a '<' indicates a new line. These data will then be used
to create a Huffman code used to compress the data prior to storage
in the internal EEPROM.
This particular program, along with many variations, has been
an exercise used for years in classes. It does demonstrate some im-
portant considerations. The shell sort was used in Chapters 2 and 5 to
sort data, and here we will use it again. In this case, the data to be
Coding the Alpha Data 357
sorted is contained in an array of structures. The structure is
typedef ed and called a type Entry. Each instance of an Entry
contains an integer value count and a character named letter. The
letter is the actual letter being recorded and count is the number
of occurrences of the letter in a document.
Our alphabet consists of the normal 26 letters plus the space and
new line characters. Therefore the constant LETTERS is given a
value of 28.
In the main program, the necessary variables are defined. Note
that the array of Ent rys named letters [ ] contains LETTERS
entries. The variables used to count the input data are initialized. The
variable characters is initialized to zero. The character member
of the array letters is initialized to the actual character values in
order and the count value is initialized to zero. The character values
in the last two entries in the array are initialized to '>' and '<' respec-
tively to correspond to the space character and the new line character.
When reading the data in, each character is operated on by the
t o lower ( ) function. This operation converts any upper-case letter to
a lower-case letter, but it does not alter any other characters. If the
character returned is a letter, a space or a new line character, it will be
processed by the following block. Otherwise, the character is discarded
and a new character is read in by the argument of the whi le ( ) loop.
As the characters to be processed are detected, the corresponding
letters . count is incremented in the array. After all of the data are
entered, an EOF is detected, the data are sorted and then printed out.
The modifications to the earlier shell sort are minimal. First of
all, the array passed to the routine is identified as a type Entry
rather than an int. Also, the temp variable is a type Entry. Then
the comparison in the test argument of the innermost f or ( ) loop is
converted to compare the two v [] . count entries. The swap
operation that follows needs no modification.
# include <stdio .h>
#include <math.h>
#include <ctype . h>
#define LETTERS 28
typedef struct {
358 Chapter 7 Advanced Topics
int count ;
char letter;
} Entry;
int main()
{
int c , characters , i ;
Entry letters [LETTERS] ;
double a, sum;
characters=0 ;
for (i=0; i<LETTERS; i++)
{
letters [i] . count=0;
letters [i] . letter=i+' a' ;
}
letters [ x z' -'a'+l] . letter=' >' ;
letters [ x z' -'a' +2] . letter=' < x ;
while ( (c=getchar ( ) ) ! =EOF)
{
c=tolower (c) ;
if (isalpha(c) | |c==' ' ||c=='\n')
{
characters++ ;
if (c>=' a' &&c<=' z' ) /* count the letters */
letters [c- ' a' ] .count + +;
else if (c== # x )
letters [ x z ' - ' a' +1] . count + + ; /* count the spaces */
else if (c== # \n' )
letters [ ' z ' - ' a' +2] . count++ ; /* count the new lines */
}
}
/* got all of the data in and processes , print it out */
shellsort (letters, LETTERS) ;
printf ("\n\n") ;
printf ( u Char Frequency Char
Coding the Alpha Data 359
Frequency\n\n" ) ;
for (i=0 ; i<LETTERS/2 ; i++)
printf(" %c %7.4f %c %7.4f\n #/ #
letters [i] .letter,
100 . * letters [i] . count /characters,
letters [i+LETTERS/2] .letter,
100.*letters [i+LETTERS/2] . count /characters) ;
printf ( "There are %d characters\n" , characters) ;
sum=0 . ;
for (i=0; i<LETTERS; i++)
{
a=l . *letters [i] . count/characters ;
a= (a!=0) ?a: . 00001;
sum += -a*log(a);
}
sum / = log (2) ;
printf ("The theoretical average bits per char-
acter is %f \n" , sum) ;
}
/* shellsort: sort v[0] ... v[n-l] into increasing
order */
void shellsort (Entry v[], int n)
{
int gap, i, j ;
Entry temp;
f or (gap=n/2 ;gap>0 ;gap /= 2)
for (i=gap; i<n; i++)
f or ( j =i-gap; j > = && v [ j ] . count<v [ j +gap] . count ; j -=gap)
{
temp=v[j] ;
v[j] =v[j+gap] ;
v [ j+gap] =temp;
}
}
Listing 7-5: Letter Analysis Program
360 Chapter 7 Advanced Topics
The purpose of this code is to calculate frequency of occurrence
of letters in a document and provide some guidance as to how well
the compression approach developed works. This program was run
with a ten-page instruction manual and then with a telephone book
with 200 entries. The results of these two executions are shown below.
Char
Frequency
Char
Frequency
>
30.5977
1
2.
.1504
e
9.
.1109
P
2.
.0276
t
6.
.5040
f
1.
.8257
o
5.
.2664
u
1.
.2727
r
5.
.1523
y
1.
.2288
i
4.
.6959
9
1.
.1762
a
4.
.6169
b
0.
.8514
s
4.
.5730
w
0.
.5793
n
4.
.2746
k
0.
.5617
h
3.
.0194
V
0.
.2984
c
2.
.8263
X
0.
.2721
d
2.
.6946
q
0.
.0351
<
2.
.2031
j
0.
.0088
m
2.
.1768
z
0.
.0000
There are 11393 characters
The theoretical average bits per character is
3.797302
Output 7-1: Calculation of entropy for the document manual.doc
The outputs shown above follow very closely the expected
occurrence of letters found in the typical technical text. The bits per
character should be about 4.5, but this value is distorted because the
space character is included in the count, and its very frequent
occurrences distort the overall averages and hence the entropy per
character found in the document.
Shown below in Output 2 is a repeat of the same calculation on
the contents of a phone book. Note here that occurrences of the letters
and other characters are quite different from those found above. Even
though the phone book used to create the table below contained only
about 200 entries, these data will be used to create a Huffman code
to compress the data when storing names into the microcomputer
EEPROM.
Coding the Alpha Data 361
Char
Frequency
Char
Frequency
e
9.
.2042
d
2.
.7331
<
7.
.7572
k
2.
.3312
>
7.
.5563
b
2.
.2106
a
7.
.5161
y
1.
.9695
r
7.
.3151
P
1.
.8891
n
6.
.3505
u
1.
.7685
o
5.
.6270
g
1.
.7283
i
5.
.3859
w
1.
.2862
1
5.
.0241
f
1.
.2460
s
4.
.5418
j
1.
.2058
t
4.
.0595
V
0.
.8039
c
3.
.8585
X
0.
.1206
h
3.
.2958
z
0.
.1206
m
3.
.0547
q
0.
.0402
There are 2488 characters
The theoretical average bits per character is
4.392597
Output 7-2: Calculation of Entropy for Phone Book
Next, a Huffman code will be created to encode the data from the
phone book. A Huffman code is built into a complete binary tree.
Such a tree always has two descendents from every node unless the
node is a leaf node. As such, whenever a Huffman tree is created to
encode n characters, there will be 2n-l nodes in the tree. Figure 7.1
shows an instance of such a tree. This tree encodes the data shown in
Output 2 above. As with most trees, analysis, or encoding, starts at
the root node at the top of the page. Whenever you traverse to the
left, a code value of zero is recorded. When traversing to the right, a
code value of 1 is recorded. For example, the character R will be
encoded as 0010, and the character M will be 1 1 1 100. This table is
constructed and filled to keep the most frequently occurring letters
at the top of the tree and the least frequently occurring letters at the
bottom. Therefore, the number of bits for each character is inversely
proportional to its frequency of occurrence. This choice for the letter
codes requires less than the number of bits one would expect when
using the standard 8 bits per character.
362 Chapter 7 Advanced Topics
Figure 7-1: Huffman Tree for Encoding the Phone Book Data
We have seen above that the minimum number of bits per character
for the telephone book is 4.39. We cannot expect to reach this level,
but we should expect to be significantly fewer than 8 bits per character.
The code corresponding to the tree in Figure 7-1 is shown in the
following table:
Character Code
x \n'
a
b
c
d
e
f
9
100
101
0001
0000111
000010
111110
01
0000110110
000011000
Coding the Alpha Data 363
h 000001
i 1100
j 000011001
k 1111111
1 1110
m 111100
n 1101
o 0011
p 111111001
q 11111101111
r 0010
s 111101
t 000000
u 111111000
v 111111010
w 0000110111
x 11111101110
y 000011010
z 1111110110
Table 7-1 : Huffman Code for CompressingTelephone Book Names
The encoding routine is shown in Listing 7-6. Contained in the
listing of the encoding routine is a look-up table that contains all of the
codes. In this table, the first two entries correspond to a space character
and a new line character. The following entries correspond to the letters
in the alphabet. In other words, the third entry corresponds to the letter
A and the seventh entry corresponds to the letter E. Notice that this
table is defined as external, but it is labeled static so that there is no
linkage to the table outside of the file encode. c.
In operation, this function receives three parameters. The first is
a pointer to an array that contains the data to be encoded. This array
contains a zero terminated string. The second array of unsigned
integers is named array. Its length is the third passed parameter
length. Encoded data are all loaded into this array. All of the local
variables used by encode are straightforward. The variable
bitbase is an unsigned int with its most significant bit set to
one and the remainder of its bits zero. When the function is executed,
the array [] is first filled with zeros. The variable i is initialized
364 Chapter 7 Advanced Topics
to zero and bit is given the value bitbase. Then the code for
each character is retrieved successively. If the character read is a
space, the first entry in the table is used; if it is a new line character,
the second entry in the table is used; and if it is any other letter, the
letter is converted to an index into the alphabet and that particular
code, offset by two, is used as the code string for the input character.
static char *code[]={
"100" , "101"," 0001" ," 0000111 "," 000010 "," 111110",
"01", "0000110110" ,"000011000 "," 000001 "," 1100",
" 000011001 ", "1111111", "1110" ,"111100", "1101",
"0011 ","111111001 ","11111101111", "0010 ","111101",
"000000 ","111111000 ","111111010 ","0000110111",
"11111101110 ","000011010 ","1111110110"
};
#include <ctype . h>
int encode (char *a, unsigned *array,int length)
{
unsigned i , bit , bitbase=~ (~0u>>l) ;
int c=l;
char *ptr, *pa;
pa=a;
for (i=0 ; i< length; i++)
array [i]=0; /* initialize the array */
i = 0;
bit=bitbase;
while (c!= # \n' )
{
c=*pa++; /* assumes file is not empty */
if (isalpha (c=toupper (c))||c==' x ||c=='\n / )
{
if(c==' M
ptr=code [0] ;
else if (c== # \n' )
ptr=code [1] ;
else
ptr=code [c-'A # +2] ;
Coding the Alpha Data 365
while(*ptr!='\0' )
{
if (*ptr++==' 1 ' )
array [i] | =bit ;
bit>>=l ;
if (bit==0)
{
bit=bitbase;
i + +;
}
}
}
}
return + + i; /* the length of the coded array */
}
Listing 7-6: Encode Function
The program then enters a while loop that examines the contents
of the code received. If the leftmost entry is a character ' 1 ' a value of
bit is ORed into the location array [i] . In either case,
*ptr==' 1' or *ptr==' ' , the value of bit is replaced by bit
shifted right by 1. Whenever bit has been shifted until its value
becomes zero, it indicates that the uns igned int value pointed to
by ptr has been filled and bit is reinitialized to bi tbase. Also at
this time i, the index into array [ ] , is incremented to get the next
character to decode.
Decoding the alpha data
The above function encodes the alpha data entered in the array
s [ ] into a Huffman code of the same data and returns the encoded
data in the array array [] . Perhaps the easiest way to test the
encode routine is to execute it in conjunction with its corresponding
decode routine. The decode operation essentially recreates the tree
shown in Figure 7-1. Rather than a two-dimensional rendition, it
must be a single-dimension list. The list will have built-in mechanisms
for traversing the tree from its root node to the encoded character
based on the 1 and patterns in the encoded data.
366 Chapter 7 Advanced Topics
Recall in Chapter 5 the decode scheme involved intermixing jump
distances in with the decoded characters in a table. There, the decode
operation started at the zero entry in the table. The code being decoded
was examined a bit at a time. If the code bit was zero, the table index
was incremented by one. If the code bit was one, the value in that
table location would be added to the table index. Whenever the table
index fell to a location that contained a character, that character would
be output and the index would be returned to the value zero.
That approach is fine for relatively small alphabets, as used in
Chapter 5. Here, we are using the full alphabet, which makes the
creation of the table above extremely complicated. Another approach
was used this time. We still have a table that contains jump instructions
intermixed with characters to be output. The characters to be output
are each ORed with the hex value 0x8 0. The printable characters
here are all identified with the least significant 7 bits of the character.
Therefore, a test for a character is to determine if the value found in
the table has a value when ANDed with 0x8 0.
Each numeric entry in the node table is broken into two nibbles.
The left 4 bits correspond to the jump when a code is found and the
right 4 bits correspond to the jump when a code 1 is found. In other
words, the decode operation starts at the beginning of the node table.
If the first bit of the encoded data is a 0, the value found in the most
significant 4 bits of the data is added to the node table index and the
decoding is continued from that point in the node table. If the encoded
data is a 1 the contents of the least significant 4 bits is added to the
node table index. Whenever the node table index is changed, the
value of that location is tested to see if the most significant bit is
turned on. If so, that bit is turned off and the result is saved in output
array. Otherwise, the process is repeated from that location until an
output character is found. At that time, the node table index is reset
to zero and the process repeated until a new line character is detected.
Then the null character is put on the end of the output data and control
is returned to the calling program.
One little problem with this approach: The number that contains
the jump data can never have its most significant bit turned on.
Therefore, the maximum jump when the encoded bit is is seven.
This restriction did not cause any difficulty when writing this tree. In
fact, most of the time the jump caused by a bit was 1 or 2. This
Coding the Alpha Data 367
restriction did cause a few longer jumps corresponding to 1. Overall,
the table was quite easy to construct.
static const char node[]={
0xld,0x21, 'E' | 0x80,0x41,0x21, 'O' |0x80, 'R' | 0x80, 0x21,
X A' |0x80,0xle,0x21, 'H' |0x80, "I" | 0x80, 0x14, 0x21, '\n' |0x80,
"0x80,0x14,0x21, 'N' |0x80, 'I' |0x80,0xlf , 'L' | 0x80, 0x12,
C | 0x80, 0x21, 'B' | 0x80, 0x14, 0x12, 'G' |0x80, 'J' | 0x80, 0x12,
Y' | 0x80, 0x12, 'F' | 0x8 0, 'W | 0x80, 0x14, 0x12, 'M' | 0x80,
S' | 0x80, 0x12, 'D' | 0x80, 0x15, 0x15, 0x12, 'U' |0x80, 'P' |0x80,
K' I 0x80, 0x12, 'V 0x80, 0x12, 'Z' 0x80 , 0x12 , ' X' 0x80,
\ \
\p/
\ v/
\ n r
\17/
\ r\ f
Q
};
0x80
int decode (unsigned M[],char *s)
{
unsigned mask,maskdo = ~(~0u>>1);
char i=0,k=0,l=0; /* l is the node pointer,
i is the byte pointer,
M is the message pointer */
mask=maskdo ;
while(k != , \n / )
{
if ( (mask & M[i] ) = = 0)
l+=node [l] >>4;
else
l+=node [l] &0xf ;
if (node [l] &0x80)
{ /* if a printable, send it out */
*s++= (k=node [l] &0x7f ) ;
1=0; /* also go to the root node */
}
if ( (mask>>=l) ==0) /* if the mask is 0, turn
on MSB */
{
mask = maskdo;
i++; /* and get the next byte from message */
}
}
368 Chapter 7 Advanced Topics
*S = 0;
return i ;
}
Listing 7-7: Huffman Decoding Data
The above function is tested in conjunction with the encode routine
with the following relatively simple program. In this code, provision
is made to enter a line of text from the computer keyboard. This text
is terminated when a new line character is detected. These data are
then sent to the encode routine. The encode routine returns the encoded
data in the array array [ ] . This array is passed to the decode routine.
The return information from decode is contained in the array s [ ] .
This string is then printed out to the screen.
# include <stdio .h>
#define ARRAY_SIZE 10
int decode (unsigned M[] , char *s) ;
int encode(char *a, unsigned *array,int length);
main ( )
{
char a [ARRAY_SIZE] ;
int c, i=0 ;
unsigned array [ARRAY_SIZE] ;
char s [ARRAY_SIZE] ;
while ( (c=getchar ( ) ) ! = ' \n' )
a [i++] =c;
a[i]='\n' ;
encode (a, array, ARRAY_S I ZE) ;
decode (array, s) ;
printf ("%s",s) ;
}
Listing 7-8: Encode / Decode Test Routine
The above program echoes the input string to the computer screen.
All lower-case letters are converted to upper case in the process.
Coding the Alpha Data 369
Read data from the keyboard
A function get ( ) is used to read data from a keyboard into a
data buffer. This function is used in the monitor. A problem with
many such functions is that they do not provide proper protection
from a buffer overflow as the data are read in. The standard library
function f get s ( ) almost meets the needs of this function and more.
The "more" in this case is the reason that we should not use the
f gets ( ) in this case. This function is part of the standard library
and as such, it requires the definition of an input. The most often
used input file here is the one named stdin . When we construct
this system, we do not want to include all of the side effects of adding
the standard library to our system. Therefore, in this case, it is probably
best to write the function get ( ) from scratch.
The function get ( ) is shown below. This function takes two
parameters. The first is a pointer to a character string where the input
data are to be stored and the second is the length of this array. In the
event that the input data size exceeds the array size, the data array is
filled with zeros. Otherwise, the new line character is placed on the
end of the string and the string is terminated with a null character.
void get (char* a,int n)
{
/* read in field and terminate the read with an x \n' */
int i=0,c;
while ( (c=getchar() )! = ' \n' && i<(n-l))
a [i + + ] =c;
if (i<n-l)
{
a[i++]='\n' ;
a[i]='\0' ;
}
else /* input did not terminate soon enough */
for (i=0;i<n;i++)
a[i] =0;
}
Listing 7-9: get () Input Data Routine
370 Chapter 7 Advanced Topics
This function is tested with the following program:
# include <stdio .h>
void get (char *, int) ;
#define LENGTH 15
main ( )
{
char data [LENGTH] ;
get (data, LENGTH) ;
if (data[0] =='\0' )
printf ( xx Buf f er overf low\n" ) ;
else
puts (data) ;
}
Listing 7-10: get Test Routine
This program reads in a line of data and echoes the string to the
computer screen. If the length of the input data is longer than the
specified length, the Buffer overflow message is printed to the screen.
The Monitor Program
The next program to be written is the monitor routine. This
function executes all of the time and receives data from the keyboard.
It will interpret the entries and pass control to the appropriate function
to execute. In building all of the functions, monitor ( ),
printaf ter ( ) , printout ( ) , and reset ( ) , there are a large
number of constants and function prototypes that must be included
in each function. All of these items will be collected together into a
single header file to be included in each function. This header file is
shown below as Listing 7-11. This file starts with the usual multiple
inclusion protection. The code for this program will be tested
completely on a DOS-based system before it is compiled for use for
the final microcontroller. There are a couple of items needed for the
DOS-based system that are not needed for the microcontroller.
Therefore, the parameter DOS is defined at the beginning of the header
The Monitor Program 371
file and certain lines of the file will be included or excluded depending
on the definition of this parameter.
It is intended to store the data in a linked list in memory. The
linked list will have a node called an Entry. An Entry contains two
characters that will be indices into the data array. The first member is
the index to the data in the data array. The second member is an index
to the next En t ry for the next data entry. An array of 35 Ent rys will
be stored in EEPROM along with an array of 698 (=768-35*2) chars
to store the nonvolatile data. There are 768 bytes of EEPROM on the
particular M68HC912B32 chip that we are using here.
A structure type named Epro is created to hold the collections
of Ent rys and the remaining data for the data storage area. This
structure will be forced to the address OxdOO at link time. It will
also be identified as EEPROM so that assignments to this memory
area will compile to storage to EEPROM rather than writes to normal
data memory. The first three entries in the dat a [ ] array are devoted
to special uses, data [0] contains the next open index into the
dat a [ ] array, dat a [ 1 ] contains an index to the beginning of the
list and data [2] contains the number of entries in the list. To
simplify both the code writing and remembering of the uses of these
memory locations, I used macros to define useful names for these
locations. Also included here is an old favorite, the FOREVER loop.
#define DOS
#ifndef PHONE_H
# define PHONE_H
#ifdef DOS
# include <stdio .h>
typedef unsigned int WORD;
enum Bool { FALSE , TRUE } ;
#define FOREVER while (TRUE)
#endif
# include <stdlib . h>
#include <string.h>
typedef struct {
unsigned dataindex;
372 Chapter 7 Advanced Topics
unsigned next;
} Entry;
#define ALEN 3
#define NLEN 16
#define DLEN 35
#define EEPROMLEN 768
#define DATAPROM EEPROMLEN-DLEN*sizeof ( Entry)
#define END Oxff
typedef struct {
Entry header [DLEN] ;
unsigned data [DATAPROM] ;
}Epro;
#define NEXT_OPEN epro->data [0]
#define START_OF_LIST epro- >data [1]
#define LIST_ENTRIES epro- >data [2]
void saveit(char * , char * , Epro *) ;
void printout (Epro *) ;
void printaf ter (Epro *) ;
void reset (Epro *) ;
int encode(char *, unsigned *,int);
int decode (unsigned * , char *) ;
void get (char *,int);
int numbdup(char * const, unsigned *, int);
void putbcd(char * , char *) ;
#ifndef DOS
void inituart (void) ;
void putchar(int) ;
int getchar (void) ;
void puts (char *) ;
#endif
#endif
Listing 7-11: Phone Book Header File
The Monitor Program 373
The final portion of this header file is a collection of all of the
function prototypes needed for this program. When this program is
moved from the DOS-based system to the microcontroller-based
system, it is necessary to remove the first line of the above header
file. There are three functions found in the standard input/output
library that will be rewritten for this program. These functions are
inituart ( ) , putchar ( ) , getchar ( ) and puts ( ) . The
function prototypes for these functions are included and will be
discarded when the parameter DOS is not defined.
The monitor program is shown below in Listing 7-12. In the
header file above, a structure was typedef ed as an Epro. This
structure is the size of the EEPROM on board the chip. An external
instance of an Epro, named able, is created and it will be used as
a destination for all of the nonvolatile stored data in the program.
Inside the main ( ) program, two arrays are created: one array to
store the name entered from the keyboard and the other to store the
phone number entered from the keyboard. Also, the external structure
will be passed around from function to function via a pointer. This
pointer is created and initialized to the structure able. Also, if the
parameter DOS is not defined, the function inituart ( ) is executed
to enable the use of the UART on the microcontroller when it is needed.
After this initialization is completed, control is passed into a
FOREVER loop where it will remain so long as the computer
continues to run. Within this loop, an input is read from the keyboard.
It is assumed that the keyboard input will read in the data and return
ASCII characters. If the system is a part of a telephone or a PDA, the
input routine will have to read in the keyboard data and convert it to
the correct ASCII value prior to its use in the following program.
Therefore, getchar ( ) in the following program can be a function
that reads data from a serial port or some other program that will
input the data from whatever keyboard is used with the system. Inside
of the FOREVER loop, a character is read in and then it is tested in
a switch ( ) /case sequence. The current test values are 'n', 'a',
V, and 'r\ These inputs are commands that determine what the
program will do:
Command Action
n Read in a number/name sequence, encode these
data and save them in the nonvolatile array.
374 Chapter 7 Advanced Topics
r Reset the system and erase the EEPROM array
which destroys all stored information.
s Display all stored data in sequence,
a Display the next stored number and name.
The functions that perform these various operations are discussed in
the following sections. All of the case choices merely call the required
function with the exception of the 'n' command. The 'n' command
calls the function get ( ) twice to read in the number followed by
the name. These data are stored in the designated arrays and the two
arrays containing the data are passed to the function save it ( )
along with a pointer to the nonvolatile structure able. Otherwise, the
program remains in the FOREVER loop, exiting the loop only to
execute commands entered from the keyboard.
/* monitor . c is the initial program that is being
developed to use on the HC12.The end product
can be used as a part of a PDA or a telephone
that requires that you enter names and phone
numbers .These entries are to be saved in
nonvolatile RAM such as flash or EEPROM,
perhaps both. In order to save memory space,
encoding of the stored data will be used. All
numbers will be stored as BCD digits and
letters will be stored as a Huffman code. It
is assumed that letter fields and number
fields will not contain mixed data.
The code in this program is programmed
for the DOS based system. No printf is used,
but other i/o functions are used as needed. It
is assumed now that any input will be
received as serial data from a serial port
on the chip .
This particular program reads data in
from the keyboard. The numeric field is
written first and the alpha field is written
second. If it is a numeric field, the data
are converted to BCD and stored in an
allocated memory field. If it is alpha data,
it is written to an allocated memory field.
The Monitor Program 375
Any encoding or decoding is done in the stor-
age routines.
T. Van Sickle August 1, 2 000 */
# include "phone . h"
Epro able;
main ( )
{
char name [ALEN] ;
char number [NLEN] ;
Epro *epro;
int c;
#ifndef DOS
inituartO ; /* initialize the uart to 9600 b/s */
#endif
epro=&able;
reset (&epro) ;
FOREVER
{
c=getchar ( ) ;
f flush (stdin) ; /* needed for the PowerC compiler */
switch (c)
{
case x n'
case
x a'
case
x s'
/* new entry */
puts ("Enter new number and name\n" ) ;
get (number, NLEN) ;
get (name, ALEN) ;
saveit (name, number, &epro) ;
break;
/* print the next entry */
printaf ter (&epro) ;
break;
/* show all */
printout (&epro) ;
376 Chapter 7 Advanced Topics
break;
case x r' : /* reset the system */
reset (&epro) ;
break;
}
}
}
Listing 7-12: Monitor Program
The SAVEITQ Routine
The save it ( ) function receives the data entered from the
keyboard in monitor ( ) , encodes these data and saves the result in
EEPROM for later use. This program is set up to save the data in a
linked list. The linked list is an array DLEN, found in phone . h, long
and each member of the list is of the type Entry. These data are
stored in the main data array and access to these data is made through
the values stored in the corresponding Entry for each set of data.
In the header file phone . h, several macros are defined that make it
somewhat easier to code this function. The first three entries in the data
array are used for special purposes. Therefore, these values are renamed
as macros to make the code more understandable. These macros are:
#define NEXT_OPEN epro->data [0]
#define START_OF_LIST epro- >data [1]
#define LIST_ENTRIES epro- >data [2]
Useful names can now be used rather than the cryptic actual names
of these various memory locations.
On entry to this function, both the name and number parameters
are encoded and the result is saved in the array name [] and
number [ ] respectively. The next block of code determines if there
is enough room in the EEPROM array to store all of the data. In the
event that there is not enough room, the message u *** buffer
f ul 1 * * * " is sent to the output and control is returned to the monitor
program when return is executed.
This program works in conjunction with moni-
tor() . It receives the input found in monitor
The SAVEITQ Routine 377
which then sends the information to this function,
saveitO . The data that arrives is in the form of
two binary strings. The first string, the name, is
Huffman encoded and the second string, the phone
number, is bed encoded. These data are to be
stored in the EEPROM. A linked list that uses
indicies rather than pointers is used. The EEPROM
is broken into two fields. The first field is an
array of the type Entry. This array is DLEN long.
The next field is an array that contains the re-
mainder of the EEPROM. This array is an array of
type unsigned. It will mostly contain data that
has been encoded. The first three entries in the
array are special. The first field contains the
index to the next unused entry in the array. The
second field is the index to the start of the list
and the final field contains the number of entries
in the list.
******************************************
#include "phone . h"
void saveit(char *s,char *n,Epro *epro)
{
int i, j , sl,nl ;
unsigned name [ALEN] ;
unsigned number [NLEN] ;
sl=encode (s, name, ALEN) ; /* encode both the name and */
nl=numbdup (n, number , NLEN) ; /* the number */
/* store encoded data on the end of the array */
if (NEXT_OPEN+sl+nl>DATAPROM) /* do not overwrite the array */
{
puts("***buffer full***\n") ;
return;
}
for (i=NEXT_OPEN, j =0 ; j <sl ; i + + , j++)
epro- >data [i] =name [ j ] ; /* save the name then the number */
378 Chapter 7 Advanced Topics
for(j=0; j<nl;i++, j++) /* at NEXT_OPEN */
epro->data [i] =number [ j ] ;
epro-
>header [LIST_ENTRIES++] . dataindex=NEXT_OPEN;
NEXT_OPEN+=sl+nl ;
if (LIST_ENTRIES>=1) /* no entries in list never */
epro->header [LIST_ENTRIES-1] .next=LIST_ENTRIES;
}
Listing 7-13: The saveitO Function
If there is enough room in the EEPROM array to store the new
data, the encoded name and the encoded number are both written
into the array at the appropriate location. Recall that the encode
routines return the lengths of the encoded data. NEXT_OPEN is the
next unused entry in the array. After the data are written to the array,
the new value for NEXT_OPEN is calculated by adding the length
of the two encoded arrays to the old value.
Finally, if there is more than one entry in the array the index for
the new entry, LIST_ENTRIES, will be put in the next location of
the previous entry, epro->header[LIST_ENTRIES-l].next.
The printout/) and the printafterf) Functions
These two functions are almost the same. Therefore, they will be
discussed together. In both cases, control is returned to the calling
program if there are no data to be printed out. The printout ( )
routine starts at the beginning of the list and prints the entire contents
of the EEPROM. Prior to the printout, the contents are decoded.
Both decode routines return a new line character at the end of the
data stream along with a null character to indicate the end of the line.
Notice in the printout ( ) function, the data starts at the start of
the list and cycles through all of the contents of the list.
#include "phone . h"
void printout (Epro *epro)
The printoutQ and the printafterQ Functions 379
{
char na [ALEN] , nu [NLEN] ;
int i, j , k, count =0 ;
if (LIST_ENTRIES==0)
return; /* no entries */
k = S T ART_0 F_L I S T ;
do
{
i=epro->header [k] . dataindex;
j =decode (&epro->data [i] ,na) ;
puts (na) ;
putbcd (&epro->data [i+j +1] ,nu) ;
puts (nu) ;
k=epro->header [k] .next;
count ++ ;
}
while (count <LIST_ENTRIES && count<DLEN) ;
}
Listing 7-14: The printout () Function
Listing 7-15 contains the printafter () routine. Here, the
routine cycles through the data, decodes it and prints it out one Entry
at a time. In this case, the parameter k is static and it starts with the
value 0. This is the value of the index to the first Entry in the array.
After each output, k is incremented and so is count. Whenever
count attains the value LIST_ENTRIES, all of the data in the
memory has been printed out. Then count is restored to along
with the value of k being set to 0. This action causes the data in the
array to be printed out one field at a time and when all of the data are
sent out, it is restored to the beginning of the array and recycled.
# include "phone . h"
void printafter (Epro *epro)
{
char na [ALEN] , nu [NLEN] ;
int j , i ;
static k=0 , count=0 ;
380 Chapter 7 Advanced Topics
if (LIST_ENTRIES==0)
return; /* no entries to decode */
i=epro->header [k] . dataindex;
j =decode (&epro->data [i] ,na) ;
puts (na) ;
putbcd (&epro->data [i+j +1] ,nu) ;
puts (nu) ;
k++ ;
if (++count==LIST_ENTRIES)
{
count =0 ;
k=0;
}
}
#ifdef DOS
void puts (char *s)
{
char *sp;
sp=s;
while(*sp!='\n' && *sp!='\0')
putchar (*sp+ + ) ;
putchar ( x \n' ) ;
}
#endif
Listing 7-15: The printafterO Function
When developing and testing this program with the DOS system,
I found it necessary to include the new puts() function. When the
program is moved to operate on the HC12, it will be necessary to
include separate i/o functions to be discussed below. The i/o
functions are not included in the DOS version of the program.
Therefore, the put s ( ) function that is added to the end of the
print after ( ) function above is included. This little function will
be discarded whenever the parameter DOS is not defined. In that
case, the i/o functions should be included.
Reset 381
Reset
The reset function is dependent on the system being used. When
programming for the DOS-based system, the array that represents
the EEPROM is put into a state that simulates erased EEPROM. Then
the first three entries in the array data [ ] are initialized to the proper
values. Recall that data [0] will always contain the index to the
next open entry in the data [] array. This index is defined by a
macro in the phone . h file to be NEXT_OPEN. The next two
members dat a [ ] and data [ 1 ] contain the index to the starting
index into the header array, START_OF_LIST and the number of
entries in the list LIST_ENTRIES respectively. The first three
members of this array are used as described above. The first available
member for storage of data is at the index 3. Therefore, NEXT_OPEN
is assigned a value of 3 and both START_OF_LIST and
LIST_ENTRIES are initialized to 0. Remember, that this data array
is the second member of a structure of the type Epro. An external
instance of this structure named able is defined in the file
monitor . h, and a pointer to this structure epro is also defined
there.
The reset ( ) function is the first in this program that requires
code for the DOS implementation that differs from the HC12. When
the EEPROM is initialized, all bits in the memory are turned on: the
memory is filled with OXFFFF. Therefore when simulating this
memory, the initialization will fill the memory similarly. There is a
library function in the Cosmic library, eepera(), that erases the
memory. Therefore, when coding for the HC12, this function will be
used. The code for the reset ( ) function is shown in Listing 7-16.
#include "phone . h"
void reset (Epro *epro)
{
int i ;
#ifdef DOS
memset (epro, Oxf f , EEPROMLEN) ;
/*make arrays look like EEPROM*/
#else
eepera ( ) ; /* erase the EEPROM with library function */
382 Chapter 7 Advanced Topics
#endif
/* Start the linked list */
NEXT_OPEN = 3;/* next open entry in the list */
START_OF_LIST =0;/* start of the list */
LIST_ENTRIES=0;/* number of entries in the list */
epro->header [0] . dataindex=NEXT_OPEN; /* start
the list */
for (i=0;i<DLEN;i++)
epro->header [i] .next=END;
}
Listing 7-16: The Reset Function
Input/Output Functions
There are three input/output functions that are usually found in
the standard I/O library that we will replace with microcontroller
specific code here. These functions are put char ( ) , get char ( ) ,
and puts ( ) . In the first two instances, rather than sending and
receiving data from devices like stdout and stdin, all output
will go to the serial port on the HC12 and input will come likewise
from the serial port. Therefore, these routines will have to be written
from scratch. In addition to the direct input/output functions, an
initialization function that enables the serial port will be needed. This
function must set the bit rate for the serial port and enable both the
UART transmitter and receiver. This function is as follows:
#include "hcl2.h"
#define BAUD9600 52
#define BAUDREG * (BYTE *)&SC0BDL
void inituart (void)
{
BAUDREG=BAUD9600; /* Set the bit rate */
SC0CR2 .TE=ON; /* turn on transmitter */
SC0CR2 .RE=ON; /* and the receiver */
Input/Output Functions 383
The choice for bit rate for this system is 38.4 kbits per second. To
achieve this rate, the baud rate divisor value is given by
BR= E J 16 BaudRate
elk
The E clock for the system is 8 MHz. Therefore, the divisor, BR, is
52 when rounded to the nearest integer value.
The register SCOBDL is defined in the header file he 12 . h as a
type Register, or a collection of eight individual bits. In this case,
it is more understandable to put the data into this register as a char
rather than as a set of bits. The type of this location can be changed
to a type BYTE easily, using the line of code
#define BAUDREG * (BYTE *)&SC0BDL
Read this line of code from right to left. It says to cast a pointer to the
memory location SCOBDL onto a pointer to a type BYTE and then
dereference it. Therefore, whenever the defined name BAUDREG is
used, it accesses the BYTE contents found at the address SCOBDL.
This address is defined in the header file he 12 . h.
The next two lines of code turn the bits TE and RE in SC0CR2
on. When these bits are ON, both the UART transmitter and receiver
will work.
The next function is putchar () . This routine sends the
designated BYTE to the serial port. It is necessary to wait until any
data in the transmit data register has been completely processed before
sending new data to this register. The first line of code does not allow
the value of SCODRL to be altered until the transmit data ready bit,
TDRE, is set. Then the value x is stored in the location SCODRL,
which causes the data to be sent to the serial port.
void putchar (BYTE x)
{
while ( ! SC0SR1 . TDRE)
; /* wait until register is ready */
SC0DRL=x; /* send the data out */
}
The last of the I/O functions is getchar ( ) . The getchar ( )
function is a little longer than the putchar ( ) function. This difference
is caused by the fact that when a character is read in from the serial
port it should be immediately echoed back. Therefore, the entered
384 Chapter 7 Advanced Topics
data are stored in the memory location a and sent out with the
instruction put char ( a ) before it is returned to the calling program.
BYTE getchar (void)
{
BYTE a;
while ( 1SC0SR1.RDRF)
; /* Wait for data ready */
a=SC0DRL;
putchar(a); /* echo the data and */
return a; /* then return it*/
}
Finally, the puts ( ) function from the standard library does not
terminate transmission when it detects a new line character. For proper
operation in this program, it should, so the following function was
written. Notice that this function terminates whenever either a new
line character or a zero character is detected in the input string. Like
the standard library put s ( ) it outputs a new line character regardless
of the termination of the data.
void puts (char *s)
{
char *sp;
sp=s;
while(*sp!='\n' && *sp!='\0')
putchar (*sp++) ;
put char ( x \n' ) ;
}
An interesting observation: If you look in Chapters 4, 5, 6, and 8
you will find similar routines for other chips. In every case, the
functions, even though they are for a broad range of different chips,
are the same — a testimonial to the use of a high-level language like
C to program our microcontrollers. A listing of these three functions
is collected together in Listing 7-17 shown below.
#include "HC12.H"
#define BAUD9600 52
Input/Output Functions 385
#define BAUDREG * (BYTE *)&SC0BDL
void inituart (void)
{
BAUDREG=BAUD9600;/* Set the bit rate */
SC0CR2 .TE=ON; /* turn on transmitter */
SC0CR2 .RE=ON; /* and the receiver */
void putchar(BYTE x)
{
while ( ! SC0SR1 . TDRE)
; /* wait until register is ready */
SC0DRL=x; /* send the data out */
}
BYTE getchar (void)
{
BYTE a;
while ( 1SC0SR1.RDRF)
/ /* Wait for data ready */
a=SC0DRL;
putchar(a); /* echo the data and */
return a; /* then return it*/
}
void puts (char *s)
{
char *sp;
sp=s;
while(*sp!='\n' && *sp!='\0')
putchar (*sp++) ;
putchar ( v \n' ) ;
}
Listing 7-17: The Input/Output Routines Used with the
M68HC912B32 Chip
386 Chapter 7 Advanced Topics
You will notice that the file shown in Listing 7-17 is the only file
in which the header file he 1 2 .h is included. None of the other code
depends in any way on the bit field structures that control all of the
peripherals on the chip. There are a few places throughout the code
where there are some specific differences between the two programs.
The first line of code in the header phone . h is
#define DOS
This line is left intact whenever the program is compiled to run under
DOS. When the program is compiled to run on the HC12, this line is
removed. Then sequences like the following found in monitor . c
#ifndef DOS
inituart () ; /* initialize the uart to 9600 b/s */
#endif
will cause the execution of inituart ( ) to be ignored when
compiled for DOS but it will be included when the program is
compiled for the HC12. Such inclusions will be found throughout
the various functions that are linked to form the program.
Putting It All Together
The above programs were all compiled, linked and tested with
the DOS-based compiler provided by MIX Software 1 . This reliable
compiler created satisfactory test code to run on any PC-style
computer. When everything was working as desired, the code was
moved to the HC12 system. The compiler used in this case was the
COSMIC compiler 2 . This compiler was provided by the same
company that provided the compilers used in Chapters 5 and 6. Cosmic
provides two software packages that are very useful. The first is called
IDEA 12. This package is a so-called Integrated Development
Environment, IDE. Within the IDE, you can specify things like the
default directory, and provide a list of files for a make utility. The
make utility will compile all source files that are newer than the
corresponding object files. Therefore, as you debug various files in
1 Mix Software, 1132 Commerce Drive, Richardson, TX 75081, (972) 783-6001.
2 Cosmic Software, 400 W. Cummings Park, Ste. 6000, Woburn, MA 01801-6512, (781) 932-2556 xl5.
Putting It All Together 387
your program, you can automatically compile only those files
modified since they were last compiled.
There are a couple of additional files needed to complete the
program for the phone book. The first is the interrupt vector table.
The vector table is stored in nonvolatile memory on this system, and
as such, it is best to write a C program that creates this table. This
little program is compiled and linked just like any other module in
the program. It will be linked as a constant section at the address
OxFFCE. The interrupt vector table program is shown in Listing 7-
18. There is only one external module to be linked to this particular
table. It is _stext ( ) . The function prototype for this function is
included at the beginning of the program, and its name is placed in
the proper vector location. Recall from our earlier discussion of
complicated declarations that the line of code
void (* const _vectab[]) ()
tells us that _vectab is an array of constant pointers to functions
that return the type void. The addresses of the various entries in the
array of pointers to functions begin at the memory location OxFFCE.
The remainder of the vectors that follow each have a specific use,
and if there were an interrupt service routine needed for the program,
its address would be placed in the corresponding location.
/* INTERRUPT VECTORS TABLE 6 8HC912B32
* Copyright (c) 1997 by COSMIC Software
*/
void _stext(); /* startup routine */
void (* const _vectab[]) = { /* OxFFCE */
0, /* Reserved */
0, /* BDLC */
0, /* ATD */
0, /* SCI 1 */
0, /* SCI */
0, /* SPI */
0, /* Pulse ace input */
0, /* Pulse ace overf */
0, /* Timer overf */
0, /* Timer channel 7 */
388 Chapter 7 Advanced Topics
o,
/*
Timer channel
6
*/
o,
/*
Timer channel
5
*/
o,
/*
Timer channel
4
*/
o,
/*
Timer channel
3
*/
o,
/*
Timer channel
2
*/
o,
/*
Timer channel
1
*/
o,
/*
Timer channel
*/
o,
/*
Real time
*/
o,
/*
IRQ
*/
o,
/*
XIRQ
*/
o,
/*
SWI
*/
o,
/*
illegal
*/
o,
/*
cop fail
*/
o,
/*
cop clock fail
*/
stext
/*
RESET
*/
};
Listing 7-18: M68HC912B32 Vector Table
Listing 7-19 shows the next function that must be included for
operation on the M68HC12. This start-up program is named crts . s.
The . s extension indicates that the function is an assembly language
function. This is the sum total of all assembly code needed for the
program discussed in this chapter. Rather than give a line-by-line
description of the code, we will see that the initial portion of the
program initializes the section named bss to all zeros. This section
is where all static and external memory are stored. Then the stack
pointer is initialized to the value designated as stack and then
control is passed to the function main ( ) . If main ( ) should return,
which it should not, the instruction following main ( ) forms an
infinite loop that branches to itself and does nothing.
C STARTUP FOR MC6 8HC12
Copyright (c) 1996 by COSMIC Software
xdef
exit,
stext
xref
main,
sbss,
memory,
stext :
stack
Putting It All Together 389
clra
; reset the bss
clrb
ldx
# sbss
; start of bss
bra
loop
; start loop
zbcl :
std
2,x+
; clear word
loop :
cpx
# memory ,
• up to the end
bio
zbcl ,
• and loop
Ids
# stack ,
• initialize stack pointer
j sr
main ,
• execute main
_exit :
bra
_exit i
• stay here
end
Listing 7-19: Crts.s C Program Start-Up Function
Any linker for an embedded system requires some type of linker
command file. The command file for this application is as follows.
# link command file for test program
# Copyright (c) 1996 by COSMIC Software
#
+seg .text -b 0x8000 -n . text# program start address
+ seg .const -a .text # constants follow program
+seg .data -b 0x800 # data start address
+seg .eeprom -b OxdOO -m768 #identify EEPROM block
+def sbss=@.bss # start address of bss
crts.o # startup routine
monitor. o # applications programs
saveit . o
encode . o
decode . o
reset . o
numbdup . o
putbcd. o
priout . o
priaf ter . o
serial . o
390 Chapter 7 Advanced Topics
get . o
"C:\COSMIC\CX12\lib\libi.hl2"
"C:\COSMIC\CX12\lib\libm.hl2"
+seg .const -b 0xffce# vectors start address
vector. o # interrupt vectors
+def memory=@.bss # symbol used by library
+def stack=0xc00 # stack pointer initial value
Listing 7-20: Linker Command File
This file can be broken into three sections. The first section
specifies all of the important memory locations needed for the
program. The first instruction identifies the text section as beginning
at the hex address 0x8000. That is the beginning of the FLASH
EEPROM on this chip. The designation text identifies the executing
portion of the program. The next line says that all program constants
shall be placed in the text section. Since the text section is
placed in internal nonvolatile FLASH, nothing in this section can be
changed by the program.
The internal RAM on this part is the 1024 bytes beginning at the
hex address 0x800. The ending address of RAM is OxBFF. The next
instruction places data, variables and stack at the starting address 0x800.
Jump to the end of this command file. Note that the parameter stack
is given a value of OxCOO. This value is used because the M68HC12
stack pointer points at the top of the stack rather than the next open
location on the stack as was seen with the M68HC11 family of parts.
The final segment designation is that of the EEPROM location. This
segment has a beginning address of OxDOO and it contains 768 bytes.
The next section of the file contains the files to be linked. There
are twelve applications files to be linked. With the exception of the
crts . o file, these files are those created and tested earlier in this
chapter. After the application program files, two standard compiler
libraries are included. These libraries contain all functions needed to
complete the program.
The last section links the vector table discussed above to the correct
address in the program, establishes the value for the initial stack
pointer, and remembers the address of the end of the bss section for
use in the program.
The program was compiled, linked and an srecord of the code
created. This code was loaded into an M68EVB912B32 evaluation
Summary 391
board. Loading the code into this board requires the use of another
development board, an HC 1 2 FLASH PROGRAMMER. This board
contains an M68EVB912B32 board also. The code is downloaded
into the programmer board and from there it is transferred to the
FLASH memory on the second evaluation board. This combination
worked to program the FLASH, and it must be noted that the final
program worked as designed after one error was corrected.
Summary
In writing this chapter, I have attempted to show that modular
development of a program can yield very satisfying results. The program
here was reduced to several relatively small functions that could each
be developed and tested separately. Then these functions were integrated
one-by-one to build the whole program. This is not to say that this
approach is less work. Several of the modules listed above are very
complicated and require careful design to make certain that they work
as desired. Some of these modules require almost invention. For
example, the means used to express the Huffman table in the decode
routine needed several different starts before a satisfactory one was
found. Recall that the complete binary tree needed to express a Huffman
code requires 2n-l nodes where n is the number of items being encoded.
I felt that it was desirable to express this tree by an array with no more
than 2n-l members. The array shown in Listing 7-7 contains exactly
2/2—1 bytes. It took six different tries to arrive at this particular
representation, and hence the code to decode the data.
The reward was that the final code worked in the embedded
product with almost no error.
Chapter 8
MCORE, a RISC Machine
Reduced Instruction Set Computer (RISC) machines are the new
architecture rage. First, the R in RISC is an absolute misnomer. When
first learning the M68000 machine, a CISC (Complicated Instruction
Set Computer), I found a controller with about 70 instructions. The
MMC2001, a RISC, has about 110 instructions, and it has no
instructions for the complex addressing modes that make the CISC
machine so easy to program. The RISC has its advantages though.
Most of the RISC instructions require only one clock cycle per
instruction while the CISC typically requires six clocks per instruction
with a range of two clocks to twenty-four clocks per instruction.
Therefore, a RISC chip with a 33-MHz clock will execute more than
thirty-one million instructions per second, obviously much faster than
a similar speed CISC, which would probably execute less than six
million instructions per second. However, the RISC machine will
require more instructions to execute the same program. Overall, the
RISC machine is almost always faster than the corresponding CISC,
even though the RISC requires more memory to implement the same
code.
The RISC/CISC dichotomy will be the source of nearly religious
debate until the next big architecture change is introduced. This text
is not aimed at comparison of different architectures. The goal is to
help you write code for the chips in the C language. Now the problem
becomes one of finding an appropriate compiler for the chip.
The compiler/debugger combination used in preparing the code
found in this chapter is provided by DIAB and SDS. 1 2 These software
1 Diab Data, Inc., (650) 571-1700, fax (650) 571-9068, email info@ddi.com, www.ddi.com
2 Software Development Systems, Inc., (630) 368-0400, fax (630) 990-4641,
email sales@sdsi.com, www.sdsi.com
393
394 Chapter 8 MCORE, A RISC Machine
systems are available as demonstration systems from the respective
manufacturers. Contact them directly for information on
demonstration systems.
A photo of the development system is shown in Figure 8.1. The
host computer in this case is a laptop computer. The Extended
Background Debug Interface, EBDI, is connected to the computer
serial port. A flexible cable connects the EBDI to the AXIOM
demonstration board that contains the MCORE chip, the MMC2001 .
Additional peripheral devices such as a keypad or an LCD display
panel can be connected directly to the AXIOM board. There are two
serial ports on the Axiom board. These serial ports can be used by
any program that needs serial access.
Figure 8-1: Development System
Delay Routine 395
Delay Routine
In the course of this chapter, we are going to see several small
routines and programs that can be used to accomplish some useful
tasks. One routine that is often useful is a delay routine. There are
several ways to implement a delay. A most important feature of any
delay routine is that it must be accurate and it must not depend on
counting a number of instruction cycles to measure the delay. Almost
all microcontrollers have timer subsystems that can be used to control
these delays.
A delay program has to cause an executing program to suspend
execution for a specified time. What is the program to do during this
time? The simplest approach is to have the delay program merely
execute a loop until the time has past, and this is the first approach
that we will use. If your program consumes essentially all of the
computer resources, such an approach is very wasteful. The computer
will merely execute a tight loop during the entire delay and exit the
loop when the delay is completed. The computer is unable to do
anything else during the delay.
There is a second approach that returns control of the computer
to the calling program and allows other operations to execute during
the delay time. This approach uses a semaphore to control execution
of the calling program. We will see this approach shortly.
Listing 8.1 is the code for the function void delay (int
t ime ) . This function makes use of the programmable interval timer,
PIT, portion of the on-board timer found on the MMC2001. The
timer is driven by a 32768-Hz crystal oscillator. The frequency
generated by this oscillator is divided by four. Therefore, the time
period of clocks entering the PIT is 1/8192 seconds, or 122.070312
microseconds. This is the resolution of the PIT on this chip. There
are two registers, the PIT data register, ITDR, and the PIT alternate
data register, ITADR. The data written to the ITDR is retained and is
transferred to the ITADR the next time that the ITADR underflows,
indicating completion of the specified time sequence. At that time,
the contents of the ITDR are written to the ITADR, and the PIT
interrupt flag ITIF is set. If the interrupt sequence is enabled, a core
processor interrupt is requested. Otherwise, the system can be polled
to determine when the time has expired. This approach is used in the
following program.
396 Chapter 8 MCORE, A RISC Machine
#include "mmc2 01 .h"
#include "timer. h"
Delay by t milliseconds. The data here are passed
as an int because, this routing ties up the
computer the whole delay time. An alternate means
should be used if a long delay is needed. This
routine uses the PIT which counts at about 122 us
per ticks. It is not very accurate because, the
count is at 1/8192 seconds, more nearly 122.070312
us ticks. Not good enough to make a clock, but
good enough to control a few milliseconds.
This modification assumes that the pit is enabled
and shall remain enabled. TVS 6/2000
icicicicicicicicicieicicicicicicicificicicicicicicicicicicicicicicicic-kic-kic-kic-kic-k-k-k-k-k-k-kic-k/
void delay (WORD t)
{
UWORD now, next;
UWORD count;
count= (long) t*1000 ; /* make the delay in us */
count/=122; /* 122 us per tick */
ITCSR.EN=ON; /* enable the pit */
while (count>0 ) /* there are probably faster ways to do */
{ /* this, but who cares, you are killing time */
now=ITADR; /* and don't care for speed */
do
{
next = ITADR;
} while (now==next) ; /* wait until next tick */
count — ;
}
ITCSR.EN=OFF; /* delay is done don' t need the pit now */
Listing 8-1: Delay routine
The function parameter time is the delay time in milliseconds.
The clocking rate for the counter is each 122.0... microseconds.
Therefore the time is converted to counts when it is divided by 122.
The PIT is then enabled with the instruction
ITCSR.EN = ON; /* enable the pit */
Delay Routine 397
This rather clumsy loop reads the value contained in ITADR into the
variable now. This register is then read into the location next until
the value in ITADR is changed by the clock. That should occur 122
microseconds later. At this time, count is decremented. When count
becomes the time has expired.
The variables now and next are both declared to be volatile. This
declaration is necessary to avoid loss of the whole loop when the code is
optimized. The variable ITADR is declared to be volatile in the header
file. That declaration does not guarantee to the compiler that the variables
will ever change, so the optimizer could well remove the code
now = ITADR; /* and don't care how fast your test is */
do
{
next = ITADR;
} while (now == next) ; /* wait until next tick */
during the optimization phase. That would make the whole function
rather pointless.
Semaphore
This delay function is used in later code to implement debounce
routines. A more practical delay routine can be implemented with
the use of a semaphore. Here, we are going to develop a semaphore
that follows that developed in Reusable Software Components . 3 We
will not create a semaphore object as done in that text, but the program
will follow the items shown there.
A semaphore is merely a flag that is attached to a process like a
program or a function. The semaphore is set and the calling program is
not able to proceed until the called program resets the semaphore. This
reset is usually done in an interrupt service routine implemented in the
called program. Let's look at the elements of a semaphore first.
When a semaphore is set, it usually marks a resource as busy. It is
interesting, but you will find that many semaphores are used around
interrupt service routines and while interrupts are being used. Often a
strange race condition can occur that will cause a semaphore to be
improperly set. Suppose that I want to set a flag, semaphore, to indicate
that a resource is busy. The process of setting the semaphore is to first
Reusable Software Components, Ted Van Sickle, Prentice Hall, Upper Saddle River, NJ, 1997
398 Chapter 8 MCORE, A RISC Machine
read the contents of the semaphore and then, if it is in the proper
condition, it will be set. Interrupts can come asynchronously at any
time. Suppose that in the process of attaching a semaphore, the status
of the interrupt is read to be tested, and, prior to marking the semaphore
as used, an interrupt occurs. Within this interrupt routine, it also could
require an interrupt. In this case, the interrupt that was being processed
by the earlier routine will seem to be available, but it is not. In fact
when control is returned to the initial portion of the program, the
semaphore will be again marked as busy, and now two completely
different processes will both assume control over the same semaphore.
/* A call to attach_semaphore ( x a' ) will attempt to attach
safely a semaphore. If a semaphore can be attached, a
semaphore number will be returned. Otherwise, a -1 is
returned and no semaphore can now be attached. When
attach_semaphore (n) , where 0<=n<10, is executed, an
attempt is made to attach the semaphore specified by the
number. If it is not available, a -1 is returned. After
the semaphore is attached, use the semaphore number as a
parameter on all other semaphore function calls. Function
release_semaphore ( ) returns the semaphore to the avail-
able semaphore pool. The function semaphore_status ( )
returns TRUE when the semaphore is NOT available, and the
function wait_f or_semaphore ( ) waits in a tight loop until
the semaphore becomes available */
#include w mmc2 01 . h"
#define Number_of_semaphores 10
#define Minus_one -1
/* function prototypes */
int attach_semaphore (void) ;
void release_semaphore (int ) ;
int semaphore_status (int ) ;
void wait_f or_semaphore (int ) ;
static volatile int semaphore [Number_of_semaphores] ;
int save;
int attach_semaphore (int r)
{
int i ;
Delay Routine 399
/* save interrupt status */
asm(" mfcr Rl,PSR\n lrw R2 / save\n stw Rl , (R2,0)\n");
/* disable all interrupts */
Disable_Interrupts ( ) ;
Disable_Fast_Interrupts ( ) ;
/* request specific semaphore */
if (0<=r && r<Number_of_semaphores&&semaphore_status [r] &&r ! ='a' )
return Minus_one; /* it is not available */
else
{
semaphore [r] =TRUE;
/* reenable interrupts */
asm( u lrw r2,save\n ldw Rl,(r2,0)\n mtcr Rl,PSR\n");
return r; /* return semaphore number */
}
for(i=0; (i<Number_of_semaphores) && (semaphore [i] !=0) ;i++)
; /* find an unused semaphore */
if (i>=Number_of_semaphores)
{
/* reenable interrupts */
asm(" lrw r2,save\n ldw Rl / (r2 / 0)\n mtcr Rl,PSR\n");
return Minus_one; /* no semaphore available */
}
else
{
semaphore [i] =TRUE ; /* mark semaphore as used */
/* reenable interrupts */
asm( u lrw r2,save\n ldw Rl / (r2 / 0)\n mtcr Rl,PSR\n" );
return i; /* return semaphore number */
}
}
void release_semaphore (int i)
semaphore [i] = FALSE;
int semaphore_status (int i)
return semaphore [i] ;
void wait for semaphore (int i)
400 Chapter 8 MCORE, A RISC Machine
while (semaphore [i] ==TRUE)
}
Listing 8-2: Semaphore functions
This problem is avoided by disabling all interrupts prior to reading
the semaphore state during the attachment process. Interrupts are
disabled and enabled by setting and clearing bits in the PSR register
on the core processor. If you examine the mmc2 1 . h header file,
there are four macros, Enabl e_In t e r rup t s ( ) ,
Enable_Fast_Interrupts ( ) , Disable_Interrupts ( )
and Disable_Fast ^Interrupts ( ) defined. These macros
are all written in assembly language because the C language cannot
access core control registers directly. In the attach_semaphore ( )
routine above, there is one line of assembly code as follows
asm ( w mf cr Rl , PSR\n lrw R2 , save\n stw Rl , (R2 , ) \n" ) ;
These three assembly instructions save the contents of the PSR reg-
ister in the memory location named save. The code
asm ("lrw r2,save\nldw Rl,(r2,0)\n mtcr Rl,PSR\n");
moves the contents of the memory location save back into the PSR.
The bits that enable both the fast interrupts and the conventional
interrupts are contained in this register. Therefore, the code as shown
in the attach_semaphore ( ) routine saves the currently enabled
interrupts, disables all interrupts, and after the status of the semaphore
is established and set properly, restores the PSR to its original value.
When the semaphore is attached, it is marked true in the array of
available semaphores. The function release_semaphore ()
marks the semaphore as FALSE. The program can query the
semaphore to determine if it is being used. This function returns the
value saved in the specified location in the array of semaphores that
is TRUE when the semaphore is being used. The final function is to
wai t_f or_semaphore ( ) . When this function is entered, control
will remain in the function until the specified semaphore is released.
It is important that you do not attempt to wait for an unattached
semaphore. In that case, control will never be returned to the calling
program.
Delays Revisited 401
Delays Revisited
With semaphores available, we can improve on the delay function
by removing the wait loop from within the function to the calling
function, where its use can be dictated by the program. In this case,
the calling function will create a semaphore and send the semaphore
to the delay function. The proposed delay function is
include "mmc2 01 .h"
#include "timer. h"
#include "intctl .h"
/***********************************************************
Delay by t milliseconds. This delay controls a semaphore
that will be examined by the calling program. When the
semaphore is released, the delay is completed. It is
assumed that fast interrupts are enabled when this func-
tion is called and that the interrupt handler has been
appropriately set up.
*************************************************
static int sem; /* this variable needs to be file global */
void delay (WORD t, int semaphore)
{
long count;
sem=semaphore ; /* save semaphore so isr can see it */
ITCSR.EN = ON; /* enable the pit */
ITCSR.0VW=0N; /* enable write through */
/* an interrupt will occur in t milliseconds */
count = (long) t*1000 ; /* make the delay in us */
ITDR =count/122; /* 122 us per tick */
ITCSR.ITIE = ON; /* enable pit interrupt */
FIER.EF8=ON; /* enable the fast interrupts */
} /* return to the calling program, we are done here */
void pit_isr (void) /* interrupt occurs when delay time expires */
{
ITCSR.ITIF = ON; /* turn interrupt flag off */
ITCSR.ITIE = OFF; /* disable the PIT interrupt */
ITCSR.EN = OFF; /* disable the PIT */
FIER.EF8 = OFF; /* disable the pit fast interrupt */
release_semaphore (sem) ; /* release semaphore to calling program */
}
Listing 8-3: Alternate Delay Routine
402 Chapter 8 MCORE, A RISC Machine
The program that calls the delay ( ) function above has some
responsibilities in making this function work. This delay is using the
PIT interrupt. Most of the code necessary to implement the interrupt
is contained in the delay function. There are, however, a couple of
items that have to be taken care of outside the delay routine. The first
is to place the address of the interrupt handler into the fast interrupt
vector. Also, the system fast interrupts must be enabled. You will see
these matters are taken care of in the following test function.
In the previous routine, the semaphore number is saved externally
so that it can be accessed by the interrupt service routine. Next the
PIT is enabled and the ITDR to ITADR write through is enabled so
that the value written to the ITDR is the value to be counted down in
the timer. The count value is next calculated. Remember, the counter
is driven at 8192 Hz. This value is obtained by counting the output
from a crystal-controlled oscillator running at 32768 Hz by 4. The
time per count is approximately 122 microseconds. The delay time
passed to the function is in milliseconds, so to calculate the count
value to be placed into the ITDR/ITADR, the program first converts
the delay time to microseconds by multiplying it by 1000. Then, the
count value is calculated by dividing the microseconds by 122. This
value is written to the ITDR and it is automatically written through
to the ITADR where it is counted down by the hardware.
The remaining code in the delay ( ) routine enables the PIT interrupt
and also enables the fast interrupt, bit 8, that is connected to the output
from the PIT. Control is then passed back to the calling program.
#include u mmc2001 ,h"
#include "serial. h"
#define FAST_AUTOVECTOR 0x3 000002c
/* function prototypes */
void handler (void) ;
int attach_semaphore (void) ;
void wait_f or_semaphore (int ) ;
main ( )
{
UWORD count=0;
int semaphore;
Delays Revisited 403
inituart (3 8400) ;
Enable_Fast_Interrupts ( ) ;
vector (handler , FAST_AUTOVECTOR) ;
while (count++<300)
{
if ( (semaphore=attach_semaphore ( ) ) = = -1)
{
puts ( "semaphore attachment error\n" ) ;
exit (0) ;
}
}
delay (10 , semaphore) ;
wait_for_semaphore (semaphore) ;
printd (count ) ;
putchar ( x \r' ) ;
Listing 8-4: Delay Test Routine
The delay test routine first initializes the on-board UART so that
signals can be sent to a terminal and then enables the fast interrupts
and places the handler address in the FAST_AUTO VECTOR location
as needed. The main test attaches a semaphore, executes a delay of
1000 milliseconds, waits for the delay to expire and prints a value to
the screen. This sequence is placed in a loop that is executed 300 times.
Note that it is important that attach_semaphore ( ) be checked
for a -1 return. If not, you could attempt to attach too many semaphores,
and your code would not catch the error. In the above case, the program
was exited with the exit ( ) function. It is not usually necessary to
abort the program when there is no semaphore when one is needed.
You can merely wait for a semaphore with any value between and 9.
When that semaphore is released, you can proceed with a request to
attach a semaphore and it should then succeed.
The attach semaphore is in the calling program in this case. The
semaphore number is sent to the delay program where it is in turn
passed to the pit interrupt service routine. When the interrupt service
routine, after the proper delay, is executed, the specified semaphore
is released. Therefore, in the calling program, the wait for semaphore
routine is executed until the semaphore is released. If needed, the
semaphore status can be polled synchronously by the program to
determine when the delay time has expired. In other words, the
404 Chapter 8 MCORE, A RISC Machine
computer does not have to sit in a wait loop until the delay time is
over to implement the delay.
The two delay functions, those shown in Listings 8-1 and 8-3,
are examples of code that are very close to the microcontroller. The
application program shown in Listing 8-4 is the type of program that
could almost be viewed as divorced from the microcontroller. In 8-4
the two operations
Enable_Fast_Interrupts ( ) ;
vector (handler , FAST_AUTOVECTOR) ;
are clearly processor dependent, but these processor dependent
functions are written as functions, or function-like macros. Therefore,
if it is necessary to move to another processor, it is necessary only to
adjust the contents of these functions to make the program usable.
The two delay functions, on the other hand, are a mass collection of
processor-specific commands that would have no meaning if the
processor were to be changed. It is a good idea when writing code
for embedded microcontrollers to collect together all of the processor
specific code into functions by themselves and allow the general code
to be as free from processor specifics as possible. We will see this
idea in the following section where several general functions interface
with several processor- specific functions.
Serial Input/Output
Serial input/output is an important capability of almost all
microcontrollers that allows the programmer or user to communicate
with the processor. Most of these functions are rather simple to
implement. Some of the functions are quite processor- specific and
others are more generally applicable to many microcontrollers. As
mentioned above, it is important that you split these functions apart
so that as much of the code that you generate as possible is portable.
Let us look at the functions contained inseriall.c.
These routines implement a serial port on the MMC2001.
The UART1 is used and the default is set to 9600 b/s, 8 bit,
no parity, and 1 stop bit. The baud rate is passed as an
integer to the inituartO function. There are several i/o
functions included. These are:
Serial Input/Output 405
inituartO Initializes the uart to the passed baud
rate and sets 8 bit, no parity, and one
stop bit .
getchar() tests for receive ready and echo
getch() no echo
getceO no ready test or echo
kbhit() returns TRUE if a key has been hit,
FALSE otherwise
putchar() Tests for transmit ready
puts() sends out the indicated string
gets() reads in a string into the buffer
getse() reads in a string and echos
printd ( ) convert an unsigned long integer to an
ascii string representation of a decimal
integer and send it out the serial port
printx ( ) convert an unsigned long integer to an
ascii string representation of a
hexidecimal integer and send it out the
serial port
#include u mmc2001 .h"
#include "uart . h"
#include "serial. h"
#define UlSRint (* (volatile unsigned short *) (0xl000a086) )
enum {TRDYint=8192 } ;
#define UlRXint (* (volatile unsigned short *) (OxlOOOaOOO) )
enum {CHARRDYint=3 2 76 8 } ;
#define CLOCK_FREQ 32000000L
/* the functions */
/* Initialize the UART1 */
void inituart(int baud)
{
/* Parity is disabled at reset by default */
/* stop bits is set to 1 by default */
/* assumes a 32 mHz system clock */
U1CR1 .UARTEN=ON; /* Turn the uart on */
/
U1CR1.TXEN=0N
U1CR1.RXEN=0N
U1CR2 . IRTS=ON
/* enable transmitter and receiver */
/* ignore request to send */
U1CR2 .WS=ON; /* 8 bit word */
/* Baud rate=CLOCK_FREQ/16/baud= */
U1BRGR.CD= (UHWORD) (CLOCK FREQ/16/baud) ;
406 Chapter 8 MCORE, A RISC Machine
U1PCR.PC3=0N; /* connect all i/o pins to the uart */
U1PCR.PC2=0N
U1PCR.PC1=0N
U1PCR.PCO=ON
U1DDR. PDC1=0N; /* make output pin for the uart */
} /* probably not needed */
/* send a character out the serial port when it is ready */
static void put (BYTE x)
{
while ( (UlSRint & TRDYint)==0)
; /* wait until character is ready */
U1TX.DATA =x; /* send the data out */
}
/* read in and echo a character through the serial port */
BYTE getchar (void)
{
BYTE a;
while ( (UlRXint & CHARRDYint ) ==0 )
; /* wait till character is ready */
a=UlRX.DATA;
put char (a) ;
return a;
}
/* read in a character with no echo */
BYTE getch(void)
{
BYTE a;
while ( (UlRXint & CHARRDYint ) ==0 )
; /* wait till character is ready */
a=UlRX.DATA;
return a;
}
/* read in a character from the serial port. Do not
check for the character ready. This routine should be used
with kbhit () . */
BYTE getce (void)
{
return U1RX.DATA;
}
Serial Input/Output 407
/* return TRUE when a key has been hit and FALSE otherwise */
int kbhit (void)
{
return U1RX . CHARRDY;
}
/* convert a long to a series of ascii characters
and send it out the serial port */
void printd (unsigned long x)
{
if (x/10)
printd (x/10) ;
putchar (x%10+ # ■ ) ;
}
/* same as above but output hexidecimal */
void printx (unsigned long x)
{
if (x/16)
printx (x/16) ;
putchar (x%16+ ( (x%16>9) ? ( * a' ) : ( x ' ) ) ) ;
Listing 8-5: Serial Input/Output Functions
At the beginning of the program, the usual headers are included.
In this case, mmc2 01 . h is needed to identify the address of the
UART, the header uart . h contains all of the definitions needed for
the UART registers, and the serial .h header contains function
prototypes for all of the serial input/output functions. These functions
are for demonstration purposes only. They all work. I have used these
functions at 115200 bit rates and had no problems. None of them
have any built-in tests for errors on reception like parity tests, framing
errors, or overrun errors. These tests need to be included and the
errors handled properly if you want to have a production quality
input/output library.
In the text that follows, each of the several small functions
contained in the above listing will be described individually. The
first function is the inituart ( ) function.
#include "mmc2001 . h"
#include "uart . h"
#include "serial. h"
408 Chapter 8 MCORE, A RISC Machine
#define CLOCK_FREQ 32000000L
/* Initialize the UART1 */
void inituart(int baud)
{
/* Parity is disabled at reset by default */
/* stop bits is set to 1 by default */
/* assumes a 32 mHz system clock */
U1CR1 .UARTEN=0N; /* Turn the uart on */
U1CR1 .TXEN=ON; /* enable transmitter and receiver */
U1CR1.RXEN=0N;
U1CR2 . IRTS=ON; /* ignore request to send */
U1CR2.WS=0N; /* 8 bit word */
/* Baud rate=CLOCK_FREQ/16/baud= */
U1BRGR.CD= (UHWORD) (CLOCK_FREQ/l6/baud) ;
U1PCR.PC3=0N; /* connect all i/o pins to the uarts */
U1PCR.PC2=0N
U1PCR.PC1=0N
U1PCR.PCO=ON
U1DDR. PDC1=0N; /* make output pin for the uart */
}
Listing 8-6: The inituart() Function
Each of these functions will have the same three header functions
shown in Listing 8-6. In this case, the function requires the clock
speed of the chip. This speed can be anything, and in our particular
case, it is 32 MHz. The macro definition CLOCK_FREQ makes this
value 32000000. If you ever change the clock speed of the computer,
this value should be changed. The several instructions in the function
are all commented with their operations. This function should be
executed whenever the serial I/O system is to be used by a program.
Pass the desired baud rate to the function as an integer. The integer
size on the DIAB compiler used for this system is 32 bits; therefore,
there will be no overflow problem for any of the usual choices for
baud rates.
Following the inituart ( ) function above, there is the series
of five functions shown in Listing 8-7. These functions are all placed
together because they each access on-board features of the MCORE
chip. Almost always, it is best to collect operations that access on-
board registers that control peripheral devices into individual functions
that can be called from the applications portion of the program. This
way, the detailed features of the chip are tightly contained in functions
Serial Input/Output 409
that can be easily changed if the chip itself is changed. The de f i ne s
and enums set at the beginning of Listing 8-7 are from the top of the
serial 1 . c program. The values established by these devices are
used in the functions of Listing 8-7. These #de fines and enums
would not be necessary if the compiler worked as expected. Generally,
if you want to wait until it is safe to send a character out the serial
port, the code that you might use is
while (U1SR.TDRY ==0)
; /* wait here until TDRY is 1 */
This code will cause the computer to sit in a loop while the TDRY bit
found in U1SR is 0. Unfortunately, the code created by the compiler
failed to reload the value of the bit inside the loop, so the computer
went into an infinite loop when it executed this code.
There are always ways around such problems and with C it is not
usually necessary to jump to assembly language when such a problem
is found. Notice the code below. Rather than the argument shown
above, the argument was converted to a simple bit- wise AND, which
did compile correctly. Remember, the addition of a # define or an
enum in your code does not add or subtract from the executable
code in your program.
The first two functions shown below are put ( ) and
getchar ( ) . The function put ( ) sends a character to the serial
port number 1 on the board. It works with the function put char ( ) ,
which works exactly the same as the put char ( ) function that you
are used to using in your programs. Here put char ( ) sends a
character to the serial port rather than to the device stdout. The
function getchar ( ) receives a character from the serial port 1. In
C, when a x \n ' character is sent to the output the program executes
a carriage return and a line feed. Therefore, to make getchar ( )
here the same, you will see in the next group of functions that the
data received by get char ( ) is tested. If it is a x \n ' character, two
characters, x \n' and x \r ' , are sent to the serial port. Otherwise,
the character passed to the function is sent to the serial port. This
operation mimics the operation of the normal getchar ( ) , but it
also requires a special function to output the character to the serial
port. The function put (BYTE x) is that function. Since the function
put ( ) is never to be used outside of this file, it is designated as
static.
410 Chapter 8 MCORE, A RISC Machine
The function get char ( ) reads a single character from the serial
port. This function also mimics the getchar ( ) that you are used
to using, because it echoes the character received to the serial port
output. There might be an occasion in which you want to read in a
character without echoing it to the serial output. In that case, you can
use getch ( ) shown below. This function works exactly the same
as getchar ( ) but it does not echo the data received.
#define UlSRint (* (volatile unsigned short *) (0xl000a086) )
enum {TRDYint=8192 } ;
#define UlRXint (* (volatile unsigned short *) (OxlOOOaOOO) )
enum {CHARRDYint=32 76 8 } ;
/* send a character out the serial port when it is ready */
static void put (BYTE x)
{
while ( (UlSRint & TRDYint)==0)
; /* wait until register available */
U1TX.DATA =x; /* send the data out */
}
/* read in and echo a character through the serial port */
BYTE getchar (void)
{
BYTE a;
while ( (UlRXint & CHARRDYint ) ==0)
; /* wait till character is ready */
a=UlRX.DATA;
put char (a) ;
return a;
}
/* read in a character with no echo */
BYTE getch (void)
{
BYTE a;
while ( (UlRXint & CHARRDYint ) ==0)
; /* wait till character is ready */
a=UlRX.DATA;
return a;
Serial Input/Output 411
/* read in a character from the serial port. Do not
check for the character ready. This routine should
be used with kbhit() . */
BYTE getce (void)
{
return U1RX.DATA;
}
/* return TRUE when a key has been hit and FALSE otherwise */
int kbhit (void)
{
return U1RX . CHARRDY;
}
Listing 8-7: Direct I/O Functions
The last two functions shown in Listing 8-7 are useful when you
need to exit a function if there is an asynchronous keyboard hit. The
function kbhit ( ) returns a logical TRUE or FALSE. Its return should
be used as the argument to an i f ( ) or whi 1 e ( ) test. Whenever the
keyboard is touched, kbhit ( ) returns a TRUE. That means that a
character has been entered into the keyboard. If you need to read and
use the value entered, the test to determine if a character is ready is
not necessary. Therefore, the function get ce ( ) was written to read
in the value contained in the data register without involving a test to
show that there is a character ready to be read. These two functions,
kbhit ( ) and getce ( ) should be used together.
The next four functions shown below also access the serial input
and output, but they all use the functions above for the direct access
and have no computer specific code. The function putchar ( ) checks
to determine if the parameter x is a x \n' . If it is, the function
put ( ) is called twice with a x \n ' and a x \r ' argument. Otherwise,
the character passed to the function is sent to put ( ) where it is sent
to the serial port.
/* Send a character to the serial port when the port is
available. If a x \n' is received send a x \n' followed by
a *\r' sequence. */
void putchar (BYTE x)
{
if ( x ==' \n' )
412 Chapter 8 MCORE, A RISC Machine
}
{
putP\n' ) ;
put('\r' ) ;
}
else
put (x) ;
/* send a string to the serial port */
void puts (BYTE *a)
{
while (*a! = ' \0 ' )
putchar (*a++) ;
}
/* This function reads a string into the buffer a. The
length of the buffer is max. If the string is less than
max long, the function returns the number of characters
entered. If the string is longer than the buffer, the
buffer is filled and a -1 is returned. The input string
is terminated by either a x \n' or a x \r' . */
int gets (BYTE *a,int max) /* no echo */
{
int i=0,c;
do /* read in data a byte at a time */
{
*a++=c=getch ( ) ;
} while (c ! = ' \n' &&c ! = ' \r ' &&++i<max-l) ;
*a='\0'; /* make it a string */
return (i>=max) ?-l : i ;
}
/* same as gets() but data entered are echoed */
int getse (BYTE *a,int max) /* with echo */
{
int i=0,c;
do
{
*a++=c=getchar ( ) ;
} while (c!=' \n' &&c!=' \r' &&++i<max-l) ;
*a='\0' ;
return (i>=max) ?-l : i ;
Listing 8-8: General Serial Input/Output Functions
Handling Interrupts 413
The next three functions can be used to send and receive strings
through the serial port. To use puts ( ) , you use a string argument.
This function will send characters from the string until it finds a zero
value in the string. The get string functions perform similar to the
standard fgets function. The difference between gets () and
getse ( ) is that getse ( ) echoes the characters read in to the serial
output. Otherwise these two functions are identical. You pass gets ( )
two parameters. The first is a pointer to a character array and the second
is the dimension of this array. As characters are read into the program,
they are stored in the character array. The string input is terminated by
either a ' \n' or a x \r ' character. The input will also be terminated
when the input character string is one less than the size of the array size.
When termination is detected, the input is terminated with a character
zero, making the data a string. The return to the calling program will be
-1 in the event that the character array was completely filled. Otherwise,
the return is the number of characters entered into the character array.
The function gets ( ) reads data from the serial port with the
getch ( ) function. This input does not echo the data entered.
getse ( ) uses the getchar ( ) to read the data in. getchar ( )
always echoes the input data to the serial output.
The above routines were separated into a series of individual
functions, which were combined into an archive library file. This file
is found on the CDROM under the name libserio.a. This file
can be linked during the linking operation like any other library file.
Handling Interrupts
All of the onboard peripherals found on the MMC2001 that need
access to interrupts are set up to use the core processor Auto Vector
capability. Shown in Table 8-1 is a copy of the interrupt vector table.
The vector table is placed at a location called the VBA, Vector Base
Address, when the chip is initialized. You will note that the table is
0x200, two hundred hex bytes, long. The vector numbers are
consecutive. The vector addresses move in steps of 4 and are usually
dealt with as hexadecimal values. This table must be filled in some
way when the program is initialized. Each vector will contain the address
of the function executed when the corresponding exception occurs.
Most of these vectors are self-explanatory. Of import here is the Fast
Interrupt Auto vector location. Note that the offset of this vector is 0x2c.
414 Chapter 8 MCORE, A RISC Machine
The use of this table is similar to that shown with the MC68HC16
family. Here, though, we will concentrate on the use of the auto vector.
The autovector is accessed when an interrupt is called with the
autovector line to the core processor asserted. When this type of
interrupt is executed, control of the processor is automatically
transferred to the function addressed contained in the autovector
vector. Note in the table that there are two autovector locations, the
normal autovector and the fast autovector. The DIAB compiler
automatically encodes interrupts to use the fast autovector, so the
offset vector location is 0x2c from the Vector Base Address.
The MMC2001 has an internal peripheral called the Interrupt
Controller. This device handles all interrupt sources from the on-
board peripherals. There are several 32-bit registers in the Interrupt
Controller. You will notice in the MMC2001 Reference Manual,
Section 10, that these registers are each just collections of 32 single
bits. These registers control interrupts. The first register is called the
Interrupt Source Register, INTSCR. Whenever an interrupt is
requested by this controller, it is assigned a level from to 32 and the
corresponding bit in the INTSCR is set. There are two interrupt enable
registers: Normal Interrupt Enable, NIER and Fast Interrupt Enable,
FIER. The program must set the corresponding bit in one of these
registers to enable an interrupt to be detected.
When an interrupt is requested, the corresponding bit in the
INTSCR is set and this register is automatically ORed with the
Interrupt Enable registers. The results of these operations are stored
in the proper Interrupt Pending Register. These registers, NIPND
and FIPND, then contain a bit pattern that show all of the pending
interrupts for the system.
There is one very convenient instruction available in the MCORE
instruction set. This instruction, FF1, indicates "find the first 1 set" in
a memory location. This instruction uses the system barrel shifter and
requires only one clock cycle. The priority of the several interrupting
sources is established by their individual bit locations in the various
registers in the controller. In the FIPND registers, bit 31 contains the
status of the highest priority pending interrupt, etc. When the FF1
instruction is executed on the FIPND, the result identifies the bit number
of the highest priority pending fast interrupt. Table 8-2 shows the
interrupt assignments of all on-board peripherals on this chip.
Handling Interrupts 415
The FF1 instruction returns a if bit 31 is set and 31 if bit is
set. It also returns a 32 if no bit is set in the designated memory
location. Therefore, if the PIT is the highest requested interrupt, the
contents of the FIPND register are stored in R3, and the instruction
FFl R3
is executed, R3 will contain the numeric value 24. We can use this
sequence to vector to the correct interrupt service routine. Consider
the code shown in Listing 8-9. This function is a general-purpose
interrupt handler to control the autovector access. The program starts
with the normal file inclusions. In this case, the interrupt controller
is being used so the header intctl . h is included. The code that
follows must access the contents of the fast pending interrupt register
FPIND. The address of this register is placed into the location f Ipndl
for convenient access by the assembly program that is generated later.
The next entry defines a struct Table, which contains an array of
32 pointers to functions that require no parameters and return the type
void. An instance of this structure is created and named table. The
32 entries in the array are each filled with a pointer to the function
unused_vec tor ( ) . It is difficult to decide what to fill unused vectors
with. The choice of zero is not realistic. When an interrupt occurs that
needs one of these vectors, control is passed to the address contained
in the vector. If the vector contains zero, the computer goes to zero and
starts executing instructions found there. The program will surely run
amuck until who knows when if it is told to execute the code found at
zero ! I usually create a simple interrupt service routine that does nothing
to help here. The function in the listing below is called
unused_vec t or ( ) . In this case, if an uninitialized interrupt occurs,
it will be ignored and control will be passed back to the executing
program. This particular approach has the drawback that you are never
aware of the occurrence of uninitialized interrupts unless you put a
break of some sort in the unused_vector ( ) routine.
In practice, you will need to place the names of interrupt service
routines in the proper vector locations shown below. We will do this
in later code to show you how.
#include w mmc2 01 . h"
#include "intctl . h"
UWORD const f Ipndl=INTCTL +0X10;
416 Chapter 8 MCORE, A RISC Machine
void handler (void) ;
void unused_vector (void) ;
static struct Table {
void (*vector [32] ) (void);
};
struct Table table ={
unused_vector
unused_vector
unused_vector
unused_vector
unused_vector
unused_vector
unused_vector
unused_vector
unused_vector
unused_vector
unused_vector
unused_vector
unused_vector
unused_vector
unused_vector
unused_vector
unused_vector
unused_vector
unused_vector
unused_vector
unused_vector
unused_vector
unused_vector
unsued_vector
unused_vector
unused_vector
unused_vector
unused_vector
unused_vector
unused_vector
unused_vector
unused_vector
};
, /*
31
unused */
, I*
30
unused */
, /*
29
unused */
, /*
28
INT7 */
, /*
27
INT6 */
, /*
26
INT5 */
, /*
25
INT4 */
, /*
24
INT3 */
, /*
23
INT2 */
, /*
22
INT1 */
, /*
21
INTO */
, /*
20
ISPI */
, /*
19
UART1 receive */
, /*
18
UARTO receive */
, /*
17
UART1 transmit */
, /*
16
UARTO transmit */
, /*
15
PWM5 */
, /*
14
PWM4 * /
, /*
13
PWM3 */
, /*
12
PWM2 * /
, /*
11
PWM1 */
, /*
10
PWMO */
, /*
9 i
unused */
, /*
8
PIT */
, /*
7
rime-of-day alarm */
, /*
6
KPP control */
, /*
5 1
JARTO RTS_DELTA */
, /*
4 i
unused */
, /*
3 i
unused */
, /*
2
software3 */
, /*
1
software2 */
/*
softwarel */
#define Do_Interrupt ( ) asm( u subi R0,32\n subi R0,28\n \
stm R1-R15, (R0)\n lrw R2 , table\n \
lrw R3,flpndl\n ldw R3 , (R3,0)\n \
ldw R3, (R3,0)\n FF1 R3\n lsli R3,2\n \
Handling Interrupts 417
addu R2,R3\n ldw R2 , (R2,0)\n \
jsr R2\n 1dm R1-R15, (RO)\n \
addi RO ,32X11 addi R0,28\n rfi\n")
void unused_vector (void)
{}
void handler (void)
{
Do Interrupt ( ) ;
Listing 8-9: Autovector Interrupt Handler
Following the table initialization, there is an assembly language
sequence. This sequence is #defined as the function
Do_Interrupt ( ) , which is called in the interrupt service routine
handler ( ) . The first five assembly instructions clear 120 bytes on
the stack, enough to save the contents of registers 1 through 15, and
Table 8-1 Exception Vector Assignments
Vector
Number(s)
Vector
Offset (Hex)
Assignment
000
Reset
1
004
Misaligned access
2
008
Access error
3
00c
Divide by zero
4
010
Illegal instruction
5
014
Privilege violation
6
018
Trace exception
7
01C
Breakpoint exception
8
020
Unrecoverable error
9
024
Soft reset
10
028
INT autovector
11
02C
FINT autovector
12
030
Hardware accelerator
13
034
(Reserved)
14
038
15
03C
16-19
040-04C
Trap #0-3 Instruction Vectors
20-31
050-07C
Reserved
32-127
080-1 FC
Reserved for vectored
interrupt controller use
418 Chapter 8 MCORE, A RISC Machine
save these registers on the stack. The following two assembly
instructions load the address of table into R2 and the address of
f Ipndl into R3. We want the contents of the value found in the
FPIND register. Therefore, we must dereference the value in R3 twice,
once to get the address of FPIND and then to get the contents of
FPIND. This value has a bit set for every pending fast interrupt. When
this value is operated on by the FF1 instruction, the register R3 will
contain the 32 minus the bit number of the highest priority pending
interrupt. If you look at how table ( ) is constructed, you will find
that the highest priority interrupts are listed first down to the lowest
priority. Each entry in the table requires four bytes to hold a function
pointer with this system. Therefore, we must multiply the value found
in R3 by 4, shift it left 2, and then add the result to the value in R2 to
find the address of the desired interrupt service routine.
All of that is exactly what is done with the assembly language
insert shown above. The last instruction j sr R2 passes control of
the computer to the specified interrupt service routine. If you look at
the C code generated by handler ( ) you will find first that the
necessary computer status is saved and then the assembly code in the
macro Do_Interrupt ( ) is executed. The last instruction here is
the j sr R2 instruction mentioned above. After the code needed for
the interrupt service routine is completed, control is returned to
handler ( ) where the machine status is restored and control is
returned to the interrupted program with an rf i, return from fast
interrupt, instruction.
The compiler has a #pragma interrupt isr_f unction. This
#pragma causes the function specified by the isr_f unction to
be compiled as an interrupt service routine. These functions save a
portion of the machine status, execute the code specified in the
isr_f unction, and restore the machine status before returning
to the interrupted program with an rf i instruction. When writing a
general function such as the one above, it was found that the partial
status save was not enough. No functions could be called from within
the interrupt service routine. This limitation was too great for a
general-purpose handler like that above, so in this routine, the entire
status of the computer is saved and restored rather than the partial
save and restore generated by the #pragma. It is safe to execute
functions within the interrupt service routines in the handler above.
A Clock Program 419
Table 8-2 Interrupt Controller Assignments
Bit
Number
Use
0,1,2
Software
3
unused
4
unused
5
UARTO RTS_DELTA
6
KPP control
7
Time-of-day alarm
8
PIT
9
unused
10
PWMO
11
PWM1
12
PWM2
13
PWM3
14
PWM4
15
PWM5
16
UARTO transmit
17
UART1 transmit
18
UARTO receive
19
UART1 receive
20
ISPI
21
INTO
22
INT1
23
INT2
24
INT3
25
INT4
26
INT5
27
INT6
28
INT7
29
unused
30
unused
31
unused
A Clock Program
A clock program is an excellent program for demonstrating the
use of interrupts on a microcontroller. The MMC2001 has a rather
extensive timer subsystem. It is broken into three parts. There is a
time of day clock, TOD, with a built-in alarm, a watchdog timer and
a programmable interval timer, PIT. We will use the PIT in this
example. Briefly, the timers are driven by a separate clock. The clock
here is a 32768-Hz clock controlled by a watch crystal. The TOD
420 Chapter 8 MCORE, A RISC Machine
clock is driven by the low-frequency oscillator frequency divided by
128 or 256 Hz. The watchdog and the PIT are both driven by the
low-frequency oscillator frequency divided by 4 or 8196 Hz. The
TOD keeps track of the time as the number of seconds since a specified
time, perhaps midnight. There is no provision for automatic roll-
over of the registers at the end of the day, so if you want to work
from midnight each day, you must reset the TOD clock to zero each
midnight. This clock also has an 8-bit fractional second register in
addition to the regular 32-bit second count register.
The alarm system also contains an 8-bit fractional second register
and a 32-bit second register. When the time in the alarm registers
matches the time in the time of day register, a flag is set that can be
polled or an interrupt can be requested.
The PIT system contains a register that is counted down. When
this register underflows, it can be reloaded automatically, set a flag
to be polled, or request an interrupt. Loading this register is done
through a register called the ITDR, the Interval Timer Data Register.
If a bit is set, data written to the ITDR will automatically transfer to
the ITADR, the register that counts down. When this register
underflows, the contents of the ITDR is automatically transferred to
the ITADR. The contents of the ITDR can be changed at any time,
and it can be changed without altering the contents of the ITADR
until the next underflow.
The ITADR is clocked 8192 times per second, approximately 122
microseconds. Since it is clocked an exact number of times per second,
it is possible to count in exact second intervals. It makes no difference
how you break up the time intervals, the clock will decrement exactly
8 192 times in one second. Therefore, if you want a basic interval time
for other uses in the system, you can cause interrupts to occur at this
faster rate and then count the number of interrupts until you achieve
the magic number of 8192 ticks each second. For example, many
systems need a 1 or 2 millisecond time interval. The system can be set
to interrupt about every 2 milliseconds and then in the ISR, a counter
incremented. This counter counts to about 500 each second and can be
easily used in a clock controller. Of course, "about" is not allowed.
The interrupt time must be 0.001953125 seconds and there will be
exactly 512 interrupts per second.
The program starts out with the creation of a function that we
will call keep_t ime ( ) . This function is executed repeatedly inside
A Clock Program 421
of a loop. There are four external variables, hours, seconds, minutes,
and count accessed by keep_time ( ) . The variable count is
incremented in an interrupt service routine that is executed each
1 .953 125 milliseconds. The constant T I ME_COUNT will have a value
of 511. When count becomes greater than TIME_COUNT, count
is reset to zero and the parameter seconds are incremented. When
seconds exceeds the value 59, seconds is reset to zero and
minutes is incremented. When minutes exceeds 59, it is reset to
zero and hours is incremented. Finally, when hours becomes
greater than 12, it is reset to 1 which corresponds to one o'clock.
Once each second, the function output_time() is executed.
#include u mmc2001 .h"
#include "timer. h"
#define TIME_COUNT 511
#define MAX_SECONDS 5 9
#define MAX_MINUTES MAX_SECONDS
#define MAX_HOURS 12
#define MIN_HOURS 1
/* function prototypes */
void output_time (void) ;
void keep_time (void) ;
/* external— global variables */
WORD seconds , minutes , hours , count ;
/* the main applications program, count is incremented in
the isr 512 times each second. Therefore, this routine
must be executed at least once every two milliseconds. */
void keep_time (void)
{
if ( count >TIME_COUNT)
{
count =0 ;
if ( + + seconds>MAX_SECONDS)
{
seconds=0 ;
if ( ++minutes>MAX_MINUTES)
{
minutes=0 ;
if (++hours>MAX_HOURS)
hours=MIN HOURS;
422 Chapter 8 MCORE, A RISC Machine
}
}
output_time ( ) ;
}
Here we are starting to write a program. Therefore, it is smart to
do all things correctly. For example, we will attempt to avoid magic
numbers by defining mnemonics for the numbers used in the program.
Also, whenever a new function is written, its function prototype will
be immediately inserted into a function prototype list at the beginning
of the program. A series of external variables is used. It is usually
better to use local variables when working with parameters in several
different functions. In that case, the parameters can be passed as
arguments to the functions when they are called. This approach avoids
debug problems where it becomes difficult to determine where
variables are changed in different functions. In the case here, the
variables hours, minutes and seconds are changed in only one
function, keep_time(), and the variable count is changed only
in the interrupt service routine or the reset time function. Therefore,
there is no uncertainty as to where the variables are changed.
Note the structure of the i f ( ) statements in the function
keep_time(). The nesting of these statements
if
if
if
}
causes the first test to be executed every time the function is executed.
The argument of the first test must be TRUE when the second test is
executed, and the argument of the second test must be TRUE when
the third test is executed, and so forth. Most of the time when the
function is executed, the total effort required by the above nested if
A Clock Program 423
sequence is the outside test only. Such an arrangement will require a
minimum amount of computer time each time the function is executed.
Many programmers will not nest the above tests so that each test is
executed each time the function is called. It works, but it requires
more computer resources than the nesting shown above.
In the function keep_time ( ) above, note that the arguments
of the several if ( ) statements all involve a "greater than" test. This
particular approach is more robust than any test that involves an "is
equal to" test. In all cases but the hours, the tested parameter is reset
to zero. Therefore, the zero is counted in the sequence and the
maximum value is one less than the number that might be expected.
For example, the range of seconds is 0..59 not 1..60, the range of
count is 0.. 511 not 1..512. When the respective count exceeds the
specified maximum value, the parameter is reset to its minimum value.
If lightning should strike the chip and the value of seconds be set at
100, the "greater than" test would fix the error the next time that
second were tested. In the event that an "is equal to" test were used,
it would be a long time before the seconds would count through a
wrap-around and be equal to the MAX_SECONDS value again.
Also note the i f argument
if (++seconds>MAX_SECONDS)
{
The seconds are first incremented and then the test is completed.
This sequence could be completed in two statements,
seconds=seconds+l ;
if ( seconds >MAX_SECONDS)
{
Some people prefer the latter approach, but the two approaches
accomplish exactly the same thing. The important item is that seconds
must be incremented before the test is completed rather than after.
At the bottom of the seconds loop, a call to output_time ( )
is executed. This function will send the current time to the output
device. Here again, the call to output_time ( ) could be placed
anywhere in the loop, but the bottom, as is shown above, is best
because the output takes place after all of the parameters are updated
and the time displayed will be now rather than before all of the updates
are completed.
424 Chapter 8 MCORE, A RISC Machine A Clock Program
Let us now look at output_t ime(). For this program, we will
send the time to the serial port to be displayed on a terminal. All of
the parameters hours, minutes, and seconds each have a range of or
1 to some maximum two-digit value. Therefore, these numbers will
contain only tens and units. There will be no hundreds or thousands
or fractions, or minus signs for that matter. To convert these values to
characters to be sent to a function like put char ( ) , we have to do
two things. First count the number of tens, convert this number to an
ASCII digit and use it as an argument to put char ( ). Then calculate
the number of units in the number, convert it to an ASCII digit and
send it to put char ( ) . These two operations are relatively easy to
program. In fact, these little functions can be written as function-like
macros easily, as follows:
#define hi (x) ((x)/10+'0')
#define lo (x) ((x)%10+'0')
Remember, the value passed to these two macro functions must lie
between and 99. The first function hi (x) determines the number
of tens in the number and converts the result to an ASCII digit by
adding the character zero to the result. The second calculates the
number of units in the number by calculating the number modulo 10.
This value is converted to an ASCII digit when the character zero is
added.
With these two little macros in hand, the output_time ()
function is easy to write:
void output_time (void)
{
putchar ( x \r' ) ;
putchar (hi (hours) ) ;
putchar (lo (hours) ) ;
putchar (':');
putchar (hi (minutes) ) ;
putchar (lo (minutes) ) ;
putchar (':');
putchar (hi (seconds) ) ;
putchar (lo (seconds) ) ;
}
The output_time ( ) function consists of a series of
putchar ( ) function calls. The first has an argument % \r ' . This
command causes the cursor on the screen to be returned to its leftmost
A Clock Program 425
position. The next two calls send the hours to the screen. Then a
colon is printed. This sequence is repeated for the minutes and the
seconds except no colon is sent out after the seconds.
Our next problem is to set up the PIT. The PIT is to interrupt
once each 1.953125 milliseconds. The clock that drives the PIT runs
at 8192 Hz and we need a clock rate of 512 Hz. Therefore, we need
to count 16 clock ticks between interrupts. I will call this time TWO_MS
in the following code. The function below is an initialization function
to set up the PIT to operate as needed. Recall, data written to the
ITDR is transferred to the ITADR when the ITADR underflows. Data
written to the ITDR is automatically transferred to the ITADR if the
OVW bit is set when the ITDR is written. This bit will be set first and
then the value TWO_MS is written to the ITDR. Next, the reload mode
is enabled by setting the RLD bit in the ITCSR register. In this mode,
the value in the ITDR is automatically reloaded into the ITADR when
the ITADR underflows. This operation assures a periodic interrupt at
the period set by the value stored in the ITDR.
The interrupt handler shown in the section "Handling Interrupts"
will be used here. Recall that function contains a general-purpose
interrupt service routine that can be used for any of the autovector
interrupts in the MMC200 1 . For our purposes here, the interrupt vector
number 8 in Listing 8-9 will have to be changed from
unused_vector to the name of the PIT interrupt service routine.
We will call that routine pit_isr and it will be written in the next
section. The macro function vec t or ( ) in the mmc2 1 . h header
file will be used to put the address of the header into the Fast
Autovector vector location. Also in the Interrupt Handler section you
will find Table 8-1. This table shows the allocation of the interrupt
vectors in the base vector table. This table is placed in a specified
location by the linker program. With the set-up used for our programs
in this book, I placed the system RAM at the address 0x30000000.
The first 0x200 entries in this table are the vector table whose outline
is shown in Table 8- 1 and whose values are established by the interrupt
handler routine in Listing 8-9. There you will note that the Fast
Interrupt Autovector is offset 0x2c into the vector table. Therefore,
the address of the FAST_AUTOVECTOR in our program must be
0x3000002c. The next instruction in the code below will place the
address of the pit_isr interrupt service routine in this address.
426 Chapter 8 MCORE, A RISC Machine
#include "intctl . h"
#define TWO_MS 15
/* initialize the PIT */
void pit_init (void)
{
ITCSR.OVW=ON; /* set-up ITDR to ITADR overload */
ITDR=TWO_MS;/* interrupt each two milliseconds */
ITCSR.RLD=ON; /* enable reload mode */
ITCSR.ITIF=ON;/* clear the interrupt flag */
ITCSR.ITIE=ON;/* enable the pit interrupt */
FIER.EF8=0N; /* enable the AVEC interrupt */
ITCSR.EN=ON; /* enable the pit */
}
The final four instructions in the pit_init ( ) function get the
chip ready to receive interrupts from the PIT. The first
ITCSR. ITIF=ON; /* clear the interrupt flag */
clears the interrupt flag ITIF. It is a quirk of the chip that the interrupt
flag is cleared by writing a 1 to the chip rather than a 0. This flag
should be cleared prior to enabling the interrupts for both the
auto vector and the PIT. If this bit is set when the interrupts are enabled,
the system will immediately respond to the interrupt and that action
is not usually what is wanted. The next instruction turns on the
interrupt enable flag ITIE for the PIT. The fast interrupt bit 8 is set in
the FIER register of the interrupt controller and finally, the PIT is
turned on when the bit EN is set in the ITCSR. The FIER register is
identified in the intct 1 . h header file. After this code is executed,
the PIT is set up and ready to interrupt every 16 ticks of the 8 192-Hz
clock.
Of course, we will need an interrupt service routine. When the
Interrupt Handler is used, the interrupt service routine need not be
concerned with saving the system status. That is handled by the
interrupt handler. All that is needed in this case is to turn off the
interrupt flag in the ITCSR and increment the value of count. The
entire interrupt service routine is
A Clock Program 427
/* in this isr, all that must be done is to turn off the
pit interrupt request bit and increment the global
parameter count */
void pit_isr (void)
{
ITCSR. ITIF=ON; /* ON clears the interrupt bit in these
registers */
count ++ ;
The interrupt handler takes care of restoring the status prior to
returning to the interrupted program.
The next logical function that we will need is the function
main ( ) . Main is broken into two parts, the initialization portion of
the program and the applications portion of the program. The first
three instructions are the initialization portion of the program. Here,
the serial I/O system is initialized and the baud rate is set to the value
BAUD_RATE. The pit_init ( ) routine written above is executed
and then the system fast interrupts are enabled with the assembly
language macro function defined in the header file mmc2 1 . h.
#define BAUD_RATE 3 8400
#define FAST_AUTOVECTOR 0x3 000002c
main ( )
{
inituart (BAUD_RATE) ; /* initialize the UART */
pit_init(); /* initialize the PIT */
vector (handler, FAST_AUTOVECTOR) ; /* put handler address
in FAST_AUTOVECTOR. */
Enable_Fast_Interrupts ( ) ;
FOREVER
{
keep_time() ; /* keep track of the time */
if (kbhit () )
reset time ( ) ;
The application portion of the program is quite simple. It is simply
a FOREVER loop that repeatedly executes keep_t ime ( ) . Also in
the loop, a test is made to determine if a key has been touched on the
terminal attached to the serial port. If it has, the function
reset time ( ) is executed where the clock time can be set.
428 Chapter 8 MCORE, A RISC Machine
The function reset_time ( ) follows. Recall from earlier
discussions that the function getch ( ) was written specifically to
read in a character from the serial port after the function kbhit ( )
indicates that the keyboard has received an input. This function,
getch ( ) , is used here. The character read in is saved in the location
c and then a switch/case statement is set up to determine what to do
with the input. It is assumed that the letters m, M, h, and H are the
valid inputs. When either an m or an M is received, the minutes will
be incremented. Detection of either an h or an H will cause the hours
to be incremented. Any other input will merely be discarded, so there
is no need for a default statement in the switch/case sequence. If an
m, either upper or lower case, is detected, the minutes will be
incremented. If the incremented value exceeds MAX_MINUTES,
minutes will be reset to 0. Similarly if an h is detected, the hours will
be incremented and if the incremented value exceeds MAX_HOURS,
hours is reset to MIN_HOURS. After these transactions are completed,
the function output_time ( ) is executed. Without this final
function call, the setting of the time will be very jumpy, definitely
not smooth.
void reset_time (void)
{
int c ;
c=getch ( ) ;
switch (c)
{
case x m' :
case X M' :
if (++minutes>MAX_MINUTES)
minutes=0 ;
break;
case l h / :
case X H' :
if (++hours>MAX_HOURS)
hours =MIN_HOURS ;
}
output time ( ) ;
break;
/* send out a new time after each change
*/
That is the end of the program. The program seems quite simple,
and it has been tested thoroughly. Shown below is the whole program
A Clock Program 429
pulled together into a single listing. Several items have been moved
around in this listing to make the whole program easier to follow.
#include w mmc2 01 .h"
#include "timer. h"
#include "intctl . h"
#include "serial. h"
#define TIME_COUNT 511
#define MAX_SECONDS 5 9
#define MAX_MINUTES MAX_SECONDS
#define MAX_HOURS 12
#define MIN_HOURS 1
#define FAST_AUTOVECTOR 0x3 000002c
#define TWO_MS 15
#define BAUD_RATE 3 8400
#define hi (x) ((xJ/lO+'O 1 )
#define lo (x) ((x)%10+'0')
/* function prototypes */
void output_time (void) ;
void keep_time (void) ;
void reset_time (void) ;
void pit_init (void) ;
void pit_isr (void) ;
void handler (void) ;
/* external— global variables */
WORD seconds , minutes , hours , count ;
/* the main applications program, count is incremented in
the isr every two milliseconds. Therefore, this routine
must be executed at least once every two milliseconds to
keep the outputs to the serial port smooth. */
void keep_time (void)
{
if ( count >TIME_COUNT)
{
count =0 ;
if ( + + seconds>MAX_SECONDS)
{
seconds=0 ;
if (++minutes>MAX_MINUTES)
{
430 Chapter 8 MCORE, A RISC Machine
minutes=0 ;
if (++hours>MAX_HOURS)
hours=MIN HOURS;
}
output_time ( ) ;
}
void output_time (void)
{
putchar ( x \r ' ) ;
putchar (hi (hours) ) ;
putchar (lo (hours) ) ;
putchar (':');
putchar (hi (minutes) ) ;
putchar (lo (minutes) ) ;
putchar ( y : ' ) ;
putchar (hi (seconds) ) ;
putchar (lo (seconds) ) ;
}
/* initialize the PIT */
void pit_init (void)
{
ITCSR.OVW=ON;
ITDR=TWO_MS ;
ITCSR.RLD=ON;
ITCSR.ITIF=ON;
ITCSR.ITIE=ON;
FIER.EF8=ON;
ITCSR.EN=ON;
/*
/*
/*
/*
/*
/*
/*
}
set-up ITDR to ITADR overload */
interrupt each two milliseconds */
enable reload mode */
clear the interrupt flag */
enable the pit interrupt */
enable the AVEC interrupt */
enable the pit */
/* in this isr, all that must be done is to turn off the pit
interrupt request bit and increment the global parameter
count */
void pit_isr (void)
{
ITCSR. ITIF=ON; /* ON clears the interrupt bit in these
registers */
count ++ ;
}
main ( )
A Clock Program 431
{
inituart (BAUD_RATE) ; /* initialize the UART */
pit_init(); /* initialize the PIT */
vector (handler , FAST_AUTOVECTOR) ;
/* put handler address in FAST_AUTOVECTOR. */
Enable_Fast_Interrupts ( ) ;
FOREVER
{
keep_time() ; /* keep track of the time */
if (kbhit () )
reset_time ( ) ;
}
}
void reset_time (void)
{
int c ;
c=getch ( ) ;
switch (c)
{
case x m' :
case X M' : if (++minutes>MAX_MINUTES)
minutes=0 ;
break;
case x h' :
case X H' : if (++hours>MAX_HOURS)
hours =MIN_HOURS ;
break;
}
output_time () ; /* send out a new time after each change */
}
Listing 8-10: Complete Clock Routine
Note that in this program the only functions that have direct access
to the memory of the computer are those that involve the PIT. The
pit_init ( ) routine is truly chip specific as is the pit_isr ( ) .
All of the other routines could be used on just about any computer. It
is always good to place the chip-specific routines in functions by
themselves. Then when you need to change to another chip, all of the
chip-specific code is lumped together and the general-purpose code
is easy to lift and move to the new program.
All #define object- like and function-like macros are placed at
the head of the program. Also, a list of function prototypes is included.
432 Chapter 8 MCORE, A RISC Machine
Every function in the program with the exception of main ( ) has a
function prototype at the beginning of the program. In this case, the
functions are all void functions that require no arguments. It is
important, however, that every programmer get into the habit of
placing these prototypes at the beginning of their programs, either in
the form of a header file or a direct listing in the program.
Keyboard
The MMC2001 chip has a built-in keyboard interface. This
interface can be either synchronously polled by the program, or it
can be asynchronously controlled through the use of an interrupt.
We will examine the asynchronous control here. This program requires
three parts. The first is the initialization routine. This routine sets up
the operation of the keyboard to cause an interrupt whenever a key is
depressed. The interrupt service detects which key is depressed and
saves it in a buffer. The program also saves a semaphore passed to
the initialization routine. This semaphore is released when the end of
a line is detected in the input. Then the main program prints the
buffer to the terminal screen.
The initialization routine is shown below:
#include "mmc2 01 . h"
# include "keypad. h"
#include "intctl .h"
static int semaph;
static BYTE *data, *savedata;
static int leng;
/* key board initialization routine */
void kpinit(int sem, BYTE *s, int length)
{
KPCRN.LOROWS=0xf ; /* enable keypad rows */
KPDRB.COLUMNS=0x0; /* write to KPDR[15:8] */
/* make cols open drain */
/* make cols outputs */
/* and rows inputs */
KPCRN.LOCOLS=0xf
KDDRN.LOCOLS=0xf
KDDRN.LOROWS=0x0
KPSR.KPKD=ON
KPSR.KDSC=ON
KPSR.KDIE=ON
/* clear KPKD status flag */
/* clear key depress synchronizer */
/* set KDIE bit to enable depress
Interrupt */
Keyboard 433
KPSR.KRIE=OFF; /* disable release interrupt */
FIER.EF6=0N; /* enable Fast Interrupt number 6 */
semaph=sem; /* save the semaphore */
data=s ;
savedata=s; /* and array information for */
leng=length; /* use in the isr */
It is expected that the calling program will attach a semaphore
and provide a buffer into which the input data will be stored. The
semaphore is passed to the initialization routine as sem, and the array
by a pointer to its first element and its length. We plan to use a 4 by
4 matrix keyboard. This keyboard will be connected to the least
significant four rows and columns of the keyboard controller interface.
There are several registers that attach to these interfaces. The KPCR
is a control register, the KPDR is the data register, and the KDDR is
the data direction register. In creating the header file for the keyboard
controller, I added an extra letter, N or B, to these pseudovariable
names to indicate that they have been broken into groups of bits
rather than a register of single bits. A register name that has the letter
N appended is split into two 8-bit fields named COLUMNS and ROWS.
Those with a letter B appended are split into four fields named
HI COLS, LOCOLS, HIROWS and LOROWS. Therefore, the command
KPCRN.LOROWS=0xf ;
will cause the least significant bits of the ROWS field in KPCR to be
set to 1 .
The comments above explain the various actions that take place
in the initialization routine. The Key Pad Data Register, KPDR, is a
register that connects to the external keyboard interface. The most
significant 8 bits are to connect to columns and the least significant
bits connect to the rows. The plan is to detect cross connections
between a column and a row. The columns serve as outputs and the
rows inputs. Therefore, the columns will be set as open drain and as
outputs through the data direction register. In the KBSR, Key Board
Status Register, the Key Pad Key Depressed bit, KPKR, is set
whenever the system detects a depressed key. This bit is reset by
writing a 1 to it. On both key depress and key release, there is a
synchronizer that aims to hide any key bounce that might occur. The
key depress synchronizer is cleared by writing a 1 to KPSR . KDSC .
434 Chapter 8 MCORE, A RISC Machine
Next the key depress interrupt is enabled and the key release interrupt
is disabled. The parameters passed to the initialization routine are
now saved in static external variables. These parameters are used in
the interrupt service routine.
The column outputs on the four least significant bits are all set to
zero so that any key depression will cause the normally high inputs
sensed by the key inputs to be pulled low. This action will initiate the
key depress synchronizer operation. Approximately four milliseconds
later, if the input key has the same state, a key depressed interrupt
will be requested. The key release operates similarly. As long as a
key remains depressed, nothing happens. When the key is released,
the key release synchronizer initiates its action. If the key has the
same state after the synchronizer time-out, a key release interrupt
can be requested. For these operations to work, the column outputs
must all be held at zero.
It is most convenient to use both the key depress and the key
release interrupts for the task at hand. Each time a key is depressed,
it is detected and the position of the key in the keypad is saved in a
buffer. Conditions to cause a key release interrupt are then set up.
Then control is returned to the interrupted program. When the key is
released, a key release interrupt is requested, and in the interrupt
service routine, a key depress interrupt is set up so that the next key
press will be detected. Each time a key position is entered into the
buffer, the next position in the buffer is marked with a Oxff.
I speak of the key position. In the interrupt service routine below,
it is first tested to determine if the interrupt was caused by a key
depress or a key release. If it is a key depress interrupt, control is
immediately passed to a for loop. Within this loop, the columns are
first loaded with a Oxe, which will have the three highest bits in the
column high and the least significant bit low. A test checks to
determine if all of the column bits are high. If not the value of column
is impressed on KPDRB . COLUMNS . The next test checks the value
found on KPDRB . ROWS. If there is no button depressed that connects
column 1 with any of the rows, the result will be Oxff. Therefore,
control will go to the next column. If the value found on rows is not
Oxff, the least significant four bits will indicate the row in which the
switch is depressed. Then, the row and column data is combined into
a single value that is stored in the data array. The column contains 4
Keyboard 435
bits, as does the row. These bits are packed into a single 8-bit value
by the instruction
c=*data++= (~ (KPDRB. COLUMNS << 4 | (KPDRB . ROWS&Oxf ) ) ) &0xff;
The column value is shifted left by four bits and the result is bit wise
ANDed with the row value. These values are the ones-complement
of the values we want, so the ones-complement is taken. The final
result is ANDed with a mask of Oxff to delete any bits outside of the
eight bits needed for the final result. This result is stored in the array
back in the calling program. Also at this time, the result is stored in a
local variable for later use. The count of the number of characters
entered into the array are incremented. If this value exceeds the length
of the array, there is an error and the input is completely terminated
when the program is restarted at its very beginning.
#define COL_0 OxOe
/* key board interrupt service routine */
void kb_isr(void)
{
UHWORD column, input ;
static BYTE C;
if (KPSR.KPKD) /* if a key has been depressed */
{
for (column=COL_0 /column! =0xf;column= (column<<l |0xl) & Oxf )
{
KPDRB . COLUMNS=column;
/* kill some time */
if ( KPDRB. ROWS != Oxff )
{
c=*data++= (~ (KPDRB. COLUMNS <<4 j
( KPDRB. ROWD&oxf ) ) ) & Oxff;
if (++n>=leng) /* data exceed the array */
{
puts ("input data overf low\n" ) ;
_start() ; /* bad error, restart the
program */
}
}
}
*data=0xff; /* needed to terminate decode */
if (c==0xll) /* *=' reached the end of the input */
436 Chapter 8 MCORE, A RISC Machine
{
decode (savedat a) ;
release_semaphore (semaph) ;
}
KPDRB.COLUMNS=0X0; /* write to KPDR[15:8] */
KPSR.KDIE=OFF; /* disable key depress interrupt */
KPSR.KRIE=ON; /* enable key release interrupt */
}
else /* key release was detected */
{
KPSR.KPKR=ON
KPSR.KPKD=ON
KPSR.KRSS=ON
KPSR.KDSC=ON
/* clear key release and depress */
/* set key release synchronizer */
/* clear key depress synchronizer */
KPDRB. COLUMNS =0X0;/* write to KPDR[15:8] */
KPSR.KDIE=ON; /* enable key depress interrupt */
KPSR.KRIE=OFF; /* disable key release interrupt */
Notice in the code above that after the columns value is set and
before the row values are tested, there is a comment to kill some
time. It turns out that some time is required to allow propagation
from the command to the electrical connection on the external switch.
The delay ( ) routine with an argument of 1, a 1-ms delay, worked
well, but this routine also uses the PIT. It is intended to use this
routine with the clock shown in the earlier section, so it is very
inconvenient to use the clock. Therefore, a series of instructions
around the keyboard controller were put together that would use up
the necessary time. These instructions would have to be executed to
set up correct conditions prior to the return from interrupt, so it is not
too much a waste of computer resources. To get enough delay, I used
some of these instructions more than one time. The code for this
time delay is shown below. The last six lines of code are required to
set up for the exit from the isr, and the first four are duplicates needed
to round out the time.
KPSR.KRSS=ON;
KPSR.KPKD=ON; /* flip and flop some innocuous bits */
KPSR.KDIE=OFF; /* just to kill time to synchronize */
KPSR.KRIE=OFF; /* the command with data arrival at */
KPSR.KPKR=ON; /* the chip's edge */
KPSR.KPKD=ON
KPSR.KRSS=ON
Keyboard 437
KPSR.KDSC=ON;
KPSR.KDIE=OFF;
KPSR.KRIE=OFF;
After the key position is detected, and its value stored in the array,
the value Oxff is placed in the next location in the array. This value is
used in the decode routine that changes the key positions to ASCII
characters for display. A termination character is needed to show the
end of the input data has been reached. There is an '=' , equal sign on
the keyboard, and it was decided to use it as the termination character.
We will see later that the position value for the '=' symbol is Oxl 1.
Therefore, if the value c is 0x1 1 at this point in the code flow, it is
time to decode the buffer contents and proceed further. After the data
are decoded, the semaphore is released and control is returned to the
interrupted program. The COLUMNS bits are all set to zero, the key
depress interrupt is disabled and the key release interrupt is enabled
prior to the return.
It turns out that it is next to impossible to get your finger off a
key soon enough to allow this code to work without detecting the
key release. The key release interrupt is set up in the key depress
interrupt service routine. The main operation that takes place in the
key release isr is to disable the key release interrupt and set up for
a key depress interrupt. Without this approach, each time the key is
depressed, it will get through the isr several times before the key is
released and the buffer will be filled with duplicate values each time
the isr is executed. The last seven lines of code in the isr above
are executed in the key release isr.
The decode function converts a value that represents the position
of the depressed key on the keyboard to an ASCII representation of
the character. The position data is an 8-bit unit with the upper 4 bits
representing the column and the lower 4 representing the row. Only
one column or row bit should be enabled at a time. More than one
row or column is enabled if multiple keys have been depressed. The
acceptable row and column values are 1, 2, 4, and 8. The circuitry
fixes the row value of 1 corresponding to the bottom row with the
value 8 as the top row. Also, the right-most column corresponds to
column 1 and the left-most column corresponds to column 8. The
inner rows and columns follow the pattern established above. A look
at the keyboard has the top row, read from left to right, containing
438 Chapter 8 MCORE, A RISC Machine
'+', '7\ '8', and '9'. The next row contains '-', '4', '5', and '6\ The
third row from the top contains ' * ' , ' 1 ' , ' 2 ' , and ' 3 ' . The bottom row
7\ '0', 7, and '='. Remember that the right-most column is column
1 in the position and the bottom-most row is row 1. Therefore, one
can build a matrix as shown below that converts the position of the
key to its ASCII value. Now the positions must be indices into a two-
dimensional array rather than the position values. The decode routine
must convert the position values to indices.
The decode routine works on an array that contains several key
positions. This data array is terminated with a character Oxff . The string
is passed as a pointer to the beginning of the string. In the function, the
value of the pointer is copied to another pointer to a type BYTE that is
used in the program. A while loop is entered which will walk the
length of the array that contains all of the key position data. This loop
is terminated when the data Oxff is found in the input array. Each piece
of input data is now split into two parts, the rows and the columns, and
a switch/case statement is used to convert the 1, 2, 4, 8 values that the
input data contains into the 0, 1, 2, 3 values used for indices into the
two-dimensional array. In the event of a multiple key hit, a default
value of 4 is passed to either the row or column. These values are
tested for, prior to access to the keys [ ] [ ] array. If a 4 is found in
either case, the decode loop is skipped to the next character. This
approach does not prevent errors in decoding, but it does keep control
and allows the procedure to be restarted.
static const BYTE keys [] [4] = { { x = ' , ' . ' , ' ' , ' / ' } ,
{ *3' , '2' , '1' , '*' },
{*6', '5' ,'4« ,'-'},
{ * 9 ' , ' 8 ■ , ' 7 « , ' + ' } } ;
/* the key position decode routine */
static void decode (BYTE *s)
{
int r,c;
BYTE *p=s;
while (*p !=0xff)
{
switch(*p 8c Oxf)
{
case 8 : r=0 ;
Keyboard 439
break;
case 4 : r=l ;
break;
case 2 : r=2 ;
break;
case 1 : r=3 ;
break;
default : r=4 ;
}
switch(*p & OxfO)
{
case 0x10 : c=0 ;
break;
case 0x20 : c=l ;
break;
case 0x40 : c=2 ;
break;
case 0x80 : c=3 ;
break;
default : c=4 ;
}
if (r==4 c==4)
break; /* got a multiple key hit,
skip it*/
*p++=keys [r] [c] ;
}
*p='\0' ;
data=savedata ;
Lastly, after the character is read from the array and stored into
the array, the loop is continued. When the loop is terminated, a null
character is written to the next entry in the array. This character is a
string terminator and the data can be used as such with any of the
input/output functions. In the initialization routine, two values of
pointers to the array a [ ] were saved. The value saved in data has
been corrupted by now and it is necessary to restore it to its original
value by assigning savedata to data.
Next, we have to test the code above. The following program
tests the keyboard initialization and isr routines. To use these
routines, you must first adjust the code in the program handler ( )
and recompile it. This code must be linked to the following program.
The address of the routine kb_isr ( ) must be placed in the specified
location in the vector table found with the handler ( ) function.
440 Chapter 8 MCORE, A RISC Machine
This location is entry number 6 in the vector table. The main ( )
function is broken into two sections, the initialization, and the
applications section. In initialization, inituart () establishes
connection with the serial I/O functions. The function
attach_semaphore ( x a ' ) connects a semaphore to this process.
This semaphore is passed to the kpinit ( ) function along with a
pointer to the beginning of the array a [ ] and also its length. The
address of the interrupt handler is placed into the autovector location
by the vector ( ) function call, and finally the fast interrupts are
enabled when Enable_Fast_Interrupts ( ) is executed.
In the main ( ) application loop, the program waits for the release
of the semaphore sem that was attached earlier. At this time, the
keyboard handler has collected a buffer of data and has detected the
input of an '='. Also, the decode has been completed so that it is
possible to execute puts (a) to send the contents of the buffer to
the serial port. After the data are written to the output, to continue
operation, it is necessary to reattach the original semaphore if it is
available and proceed. This particular program will remain in the
inner loop forever reading inputs from the keyboard and outputting
them to the serial port whenever an '=' button is depressed.
Usually at this point, a single listing of the whole program is
included in the text. It is a duplicate of the various smaller segments
shown above. Here I will refer to the program listing that is contained
on the CD-ROM. It can be found under the directory chapter 8
and has the name kbinit . c.
Integrating Keyboard and Clock
The next piece of code will integrate the keyboard with the clock
developed earlier. The purpose of this program is to show the ease of
integrating these two programs and the use of more than one interrupt
simultaneously with the interrupt handler developed earlier. The main
change in the clock program will be to alter the reset_time ( )
function. This new function will receive inputs from the keypad. The
code developed in the previous section can be used, but it will have
to be altered significantly to be of use in this program. Here, we will
want to set up the keyboard input to interrupt when any key is
depressed and send a flag to the executing program when one or two
appropriate keys have been depressed. Let us increase the minutes
Integrating Keyboard and Clock 441
when the ' 1 ' button is depressed and the hours increase when the '2'
button is depressed. The program will need the interrupt service
routine, but the recording of the data into a buffer and all of the
attendant decoding is unnecessary here. We will use a single global
variable to set when a correct key is depressed. That is all required
for this application.
Let's start with the initialization routine. This initialization routine
needs to do even less than was needed earlier. Consider the code:
/* key board initialization routine */
void kpinit (void)
{
KPCRN.LOROWS=0xf ; /* enable keypad rows */
KPDRB.COLUMNS=0x0; /* write to KPDR[15:8] */
/* make cols open drain */
/* make cols outputs */
/* and rows inputs */
/* clear KPKD status flag */
/* clear key depress synchronizer */
/* set KDIE bit to enable depress
Interrupt */
KPSR.KRIE=OFF; /* disable release interrupt */
FIER.EF6=ON; /* enable Fast Interrupt number 6*/
KPCRN.LOCOLS=0xf
KDDRN. LOCOLS=0xf
KDDRN. LOROWS=0x0
KPSR.KPKD=ON
KPSR.KDSC=ON
KPSR.KDIE=ON
The main change observed is that no parameters are passed to the
initialization function and none of the variables saved earlier are
needed here. Otherwise, this function is identical to the earlier
kpinit () function.
The changes made in kb_isr ( ) are instructive. There is no
substantive change in the function until after the depressed key has
been detected. For this program, we are interested only in the detection
of a T or a '2'. These digits correspond to the position values 0x42
and 0x22 respectively. After the key has been detected, a simple case
statement converts these specific values to the proper digits. If the
position numbers are not one of the expected values, *data is
assigned a value of zero. This value is used by the main ( ) program
to determine when a key has been depressed.
/* key board interrupt service routine */
void kb_isr(void)
{
UHWORD column, input ;
442 Chapter 8 MCORE, A RISC Machine
static BYTE c;
if (KPSR.KPKD) /* if a key has been depressed */
{
for (column=COL_0 /column! =0xf;column= (column<<l | Oxl) &0xf)
{
KPDRB . COLUMNS=column;
/* kill some time here */
if (KPDRB.ROWS!=0xff )
{
c= (~ (KPDRB. COLUMNS <<4 j
( KPDRB. ROWS&Oxf ) ) ) & Oxff;
}
}
if (c==0x22)
*data=' 2 ' ;
else if(c==0x42)
*data=' 1 ' ;
else
*data=0;
c = 0;
KPDRB. COLUMNS = 0X0 ; /* write to KPDR[15:8] */
KPSR.KDIE=OFF; /* disable key depress interrupt */
KPSR.KRIE=ON; /* enable key release interrupt */
}
else /* key release was detected */
{
KPSR.KPKR=ON;
KPSR.KPKD=ON; /* clear key release and depress */
KPSR. KRSS=ON; /* set key release synchronizer */
KPSR. KDSC=ON; /* clear key depress synchronizer */
KPDRB. COLUMNS= 0X0; /* write to KPDR[15:8] */
KPSR.KDIE=ON; /* enable key depress interrupt */
KPSR.KRIE=OFF; /* disable key release interrupt */
The code executed by the i s r when a key is released is unchanged
from earlier.
Adding a Display
Access to the LCD port is through a memory-mapped register.
This register contains two byte-wide fields, one called COMMAND
Adding a Display 443
and the other DATA. This register is at address 0x2c3ffff0. The
following typedef and #def ine makes this location available to
the program.
typedef struct {
BYTE COMMAND
:8;
BYTE DATA
:8;
} Lcddrv;
#define LCDDRV (* (Lcddrv * ) (0X2C3FFFF0) )
Access to this memory area is through Chip Select number 3. This
Chip Select must be set up and enabled. The following initialization
function allows access to the above memory address. The chip select
control registers are not set to a specified value at reset. It is best to
put a specified value into this register, 0, and then set the necessary
bits. The first two lines of code in the following function put a zero
into the address CS3CR. The fields in the chip select control register
allows insertion of wait states from to 15. Fifteen was used here
because the memory location is rarely accessed; when accessed, it
deals with the outside world, and the long wait state will not
appreciably degrade the computer performance. An extra dead cycle
is added when there is a write to this address. The access is to a 16-
bit port. The last instruction enables the chip select number 3.
/* set up memory to access the LCD */
void initperip (void)
{
UWORD *CS3CRX= (UWORD *)&CS3CR;
zero the whole register */
3 wait states */
extra dead cycle on write */
enable byte write access only */
16 bit port */
chip select enabled */
}
There are two output functions that are useful. These functions are:
/* Routines for the LCD Display */
/* send a command to the LCD */
void LCDCommand (BYTE command)
*CS3CRX=0;
/*
CS3CR.WSC=3;
/*
CS3CR.EDC=ON;
/*
CS3CR.EBC=ON;
/*
CS3CR.DSZ=2;
/*
CS3CR.CSEN=ON;
/*
444 Chapter 8 MCORE, A RISC Machine
{
LCDDRV . COMMAND = command ;
delay (25) ;
}
/* send data to the LCD */
void LCDData(BYTE data)
{
if (data==' \r' )
LCDDRV. DATA= 0X01;
else
LCDDRV . DATA=data ;
delay (25) ;
}
These functions do essentially the same thing. The main difference
is that the Command function writes to the upper byte of the specified
address and the Data function writes to the lower byte of this address.
One other special operation has been put into the Data function. If
the data string contains a '\r' character, it is intended that the cursor
should be returned to the beginning of the line. The 0x01 command
does just that. Otherwise, the data received is sent to the screen for
display. It is recommended that a 25 -ms delay be executed after each
write to the LCD system. This delay is included in both of the above
functions.
A sequence of events is needed to bring the LCD display into
operation. The code below first executes the initialization routine
above so that the LCD display is connected to the computer. There
immediately follows a repeated execution of the LCD command 0x03
three times. These instructions prepare the LCD display for operation.
There follows a series of commands: set the display for 2 by 40
character display, turn the display off, clear the display and move the
cursor to the home position, automatically increment cursor position
after each write, turn the display and cursor on, and finally shift the
cursor right after each input. These commands are all executed by
the following code.
/* initialize the LCD */
InitLCDO
{
int i ;
initperipO ; /* initialize memory block */
for (i=0;i<3;i++)
Adding a Display 445
LCD Command (0x03 ) ; /* get things turned on */
LCDCommand(0x3c) ; /* 2x4 display */
LCDCommand(0x08) ; /* display off */
LCDCommand ( 0x01) ; /* clear display and home cursor */
LCDCommand ( 0x06 ) ; /* increment cursor position */
LCDCommand (OxOf) ; /* Display and cursor on */
LCDCommand ( 0x14 ) ; /* Shift cursor right */
The function LCDData ( ) shown above performs the essential
operation of the put char ( ) that we have used so often above. In
our program above, we used put char ( ) everywhere. It seems that
we should have a separate command for the LCD display, but at the
same time, it would be desirable that we could write to the LCD with
a put char ( ) and not have to worry about changing all of the
occurrences of put char ( ) in the earlier code. A way around this
problem is to use a simple macro
#define putchar(a) LCDData (a)
and now we can use put char () in place of LCDData () .Of course,
it is important that we do not link seriall.oto the program, and
the inclusion of serial . h is no longer needed.
One item that must be considered here is the use of the function
delay () . Recall that delay ( ) uses the pit to clock the delay time.
The function in Listing 8-1 turns the pit on at the beginning of the
operation and off at the end. We cannot allow this function to turn
the pit off because the pit is being used by the basic clock function.
Therefore, the two lines of code
ITCSR.EN=ON;
ITCSR.EN=OFF;
must be removed from the delay routine. The function now will work
correctly, and it will not interfere with the operation of the basic pit
operation used by the clock. This modified function is del ay 2 . c
on the CD-ROM.
Usually, a complete listing of the program is included in earlier
chapters of this text. In this case, the code is approximately 250 lines
long, and it is not so important to see this amount of code. It is all
446 Chapter 8 MCORE, A RISC Machine
shown above in bits and pieces. The complete listing is found in
newclock . c found on the CD-ROM in the Chapter 8 directory.
Summary
The developments in this chapter show an incremental approach
to producing a program. The whole project was understood at the
beginning of the chapter, but rather than try to develop the whole project,
a series of small tasks were developed and tested. The complexity of
each of these individual tasks was minimal. Often, the code required
for each portion was but a dozen or so lines long. Such an approach is
often the best approach to the development of complex code. This
approach is not mine — it has been around as long as I can remember.
With any new project, you should analyze the whole project and
start the development by breaking the project into tasks. Then, break
into subtasks and repeat the process until the tasks are so simple that
the code to implement them is trivial. Then step back a level and
repeat the process, making certain that the code implementation is as
simple as possible at all times. As this code is integrated, each module
should be compiled and tested. Therefore, your program will be built
of tested modules. Never attempt to write a big block of code without
first testing the various small blocks that comprise the whole program.
Index
!=, not equal operator, 17, 26
#, preprocessor command identifier, 2
#asm, 152
#define, 16,431
#endasm, 152
#pragma, 115, 349,418
%, modulo operator, 24
%d, integer format, 5, 7
&&, and operator, 26
&, address-of operator, 65
*, multiplication operator, 24, 66
-, subtraction operator, 24
/, division operator, 24
@far, 295
@port, 295
\?, escape sequence for question mark, 15
\\, escape sequence for backslash, 15
V, escape sequence for single quote, 15
\", escape sequence for double quote, 15
\a, escape sequence for bell character, 15
\b, escape sequence for backspace, 15
\f, escape sequence for form feed, 15
\n, escape sequence for new line, 2, 15, 409, 411
\ooo, escape sequence for octal number, 15
\r, escape sequence for carriage return, 15, 409,
411
\t, escape sequence for horizontal tab, 6, 15
\v, escape sequence for vertical tab, 15
\xxx, escape sequence for hexadecimal number,
15
{, block or compound statement beginning in-
dicator, 2
I I, or operator, 26
}, terminator of compound statement, 3
" ", file name delimiter, 2
+, addition operator, 24
<, less than operator, 26
<=, less than or equal to operator, 26
= =, equality operator, 26
>, greater than operator, 26
>=, greater than or equal to operator, 26
15-bit timer, 130, 166
16-bit timer, 130, 166, 178-181,
programming, 186-195
abs(x), 56
accessing files, 110
acos(x), 117
ADC, 132-133
analog-to-digital converter (ADC), 132-133,
195-201, 288
AND, 26
ANSI standard, 294
argument, function, 51
arithmetic logic unit (ALU), 124
arithmetic operators, 24-25
arithmetic shift, 29
array boundary checking, 19, 74
array index, 19
array initialization, 80
array, 18-19
array, multidimensional, 80-87
ASCII characters, 352
asin(x), 117
assembly codes callable by C6805, 150
assembly language for MC68HC12, 388
assembly language for MC68HC16, 328-332,
338, 345
assembly language, 152
assignment operators, 32
association, 7
447
448 Index
associativity of operators, 34-35
asynchronous time service, 251
asynchronous, 126
atan(x), 777
atan2(x,y), 777
auto, 9
automatic variables, 12-13
auto vector interrupt handler, 416-417
auto vector, 414
Axiom demonstration board, 394
B
background debug mode, 147
BAUD register, 276
BCD encoding, 207-209, 352
binary operator, 26
binary tree sort, 95
binary tree, 238
bit field, 107, 108-109
bit manipulations, 108
bitwise AND, 28
bitwise operator, 28
boot FLASH memory, 348
boundary checks, 74
break keyword, 9, 48
bubble sort, 74
C6805 compiler, 150, 295
calloc, 775
case keyword, 9
cast operator, 28, 114
char, 9, 75
character constants, 15-18
character tests, 117
chip-specific routines, 431
circular convolution program, 341
clear interrupt flag routine, 426
CLI instruction, 174
clock program for MMC2001, 419, 429-431
clock, 297
coding tips, 137-148
comments, 17
compilers, 52, 63, 150, 221-230, 305-318, 386,
393, 414
compound statement, 3
computer operating properly (COP), 131, 167,
219
concatenation, 74
conditional expression, 33
const keyword, 9-10
constant, 8-11
continue keyword, 9, 48
controlling DC motor, 254
conversion commands, 112
convolution, 332, 334, 341
cos(x), 777
Cosmic compiler, 221-230, 285, 293, 295, 305-
318,335,349,386
counter register, 1 84
CPU 16 core processor, 288, 289-296
D
data compression, 237-245
data storage memory, 159
date function, 81, 119
DC motor control, 255-275
debouncing, 255, 275
debugging programs with user-specified inter-
rupts, 294
declaration statement, 4
decrement operators, 30-32
default keyword, 9
deference operator, 66
definition statement, 4
delay routine for MMC2001, 395-397, 401, 436,
446
development boards, 136
development environment for microcontroller
programming, 134-137
Diab compiler, 393, 414
diagnostics, 119
digital input/output, 131
digital signal processor, 287
digital-to-analog converter (DAC), 209
Dirac Delta function, 335
display, LCD, 443-446
do keyword, 9
Index 449
do/while construct, 39-42
Do_interrupt( ), 417-418
dot product, 332
double keyword, 9, 28
DSP operations with microcontrollers, 326-345
M68HC16 and, 326-345
DSP register model, 327
dynamic debounce, 275
dynamic memory allocation, 101
EBDI, 394
EEPROM, 128, 153, 155, 159
Programming, 159
EK register, 290
else keyword, 9
enum, 9, 20-22
erasable programmable read-only memory
(EPROM), 127, 153-154
escape sequence, 15
exception vector table, 290-291
exceptions, 125
exclusive OR, 28
exit, 60
expanded bus, 155
expression, 24-34
Extended Background Debug Interface, 394
extern keyword, 9, 15
external static variable, 13-14, 58
factorial, 62
fast interrupt enable register (FIER), 414
FFI instruction for MMC2001, 414
fgets, 111, 413
Fibonacci number, 62, 77
FIER register, 426
filter, 332
FIPND interrupt pending register, 414
flags, 113
FLASH memory, 128, 153
float keyword, 9
floating point variable type, 4, 1 1
for keyword, 9
for loop, 38-39
FOREVER loop, 174, 427
formatting, 113
Fourier transform, 333
fp, 110
fputs, 111
fractions, handling, 25
free( ), 115
function argument, 51
function prototype, 52, 432
functions, 8, 51-61
G
general purpose timer module (GPT), 288, 314
general purpose timer, 129
generic pointer, 69
get string functions, 413
getce( ) function, 411
getch function, 413
getchar, 17, 382-383, 409, 413
gets function, 72, 413
getse function, 413
goto keyword, 9, 48
"greater than" test vs. "is equal to" test, 423
H
handling interrupts on MMC2001, 413-419
Harvard architecture, 124
HC11E9.H header file, 258
hcl6.h header file, 290
header files, 116, 171,211-221
hexadecimal numbers, 1 1
Hoare, C.A.R., 231
Huffman code, 237-244, 350, 356, 361-368
/
IARB field, 302
if keyword, 9
if/else, 42-44
if-else if sequence, 44-48
450 Index
include, 2
inclusive OR, 28
increment operators, 30-32
index registers, 290
index, of array, 19
initialization section of program, 188
inituart( ) function, 409
input capture, 130, 185
MC68HC11 family, 253
input/output, 110, 129-134, 382-386
memory-mapped, 129
int, 2,9, 11
integer variable type, 3
integrating keyboard and clock, 440-443
inter-modual bus (1MB), 288
interrupt controller, 414, 419
interrupt flag clear routine, 426
interrupt handler routine for MMC2001, 415-
417, 425
interrupt pending register, 414
interrupt request, 125
interrupt service routine, 125, 252, 294, 415,
425-426
interrupt source register (INTSCR), 414
interrupt vectors, 290
interrupts, processing, 220, 301-302
for MMC200 1,413-419
IRQ, 125
isalnum(c), 117
isalpha(c), 117
iscntrl(c), 777
isdigit(c), 777
isgraph(c), 777
islower(c), 777
isprint(c), 777
ispunct(c), 777
ISR, 125, 220
isr_f unction, 418
isspace(c), 777
isupper(c), 777
isxdigit(c), 777
ITADR, 395, 402, 420, 425
ITDR, 395, 402, 420, 425
ITIE flag, 426
ITIF flag, 426
jsr R2 instruction, 418
K
kbhit( ) function, 411
KBSR register, 432
KDDR register, 432
keep, 14
key depress synchronizer, 434
key release interrupt, 434
keyboard interface for MMC2001 chip, 432-440
keywords in C, 9
KPCR register, 432
KPDR register, 432
KPKR register, 432
Kroniker Delta function, 333
label, 48
last in, first out (LIFO), 59
LCD display routines, 445
left shift operator, 29
letter analysis program, 359
library functions, 80, 116, 223
libserio.a archive library file, 413
local static variables, 13
log(x), 118
loglO(x), 118
logic analyzer, 137
logical AND, 26
logical operator, 26
logical OR, 26
logical shift, 29
long, 9-10, 28
longjmp function, 49
look-up table with slopes, 320-322
looping construct, 6
eliminating, 325
lvalue, 69, 85
Index 451
M
M68HC08, 149
MAC multiplier input registers, 326
macro definition, 56, 60, 102
main( ), 1, 2, 3, 51, 427
malloc, 12, 101-102, 114
masked ROM, 127, 153
math functions, 117
MC68300, 288
MC68HC05 microcontroller, 142, 149
MC68HC05EVM, 152
MC68HC05EVS, 142
MC68HC11/12, 149, 211-286, 347-391
MC68HC16, 149, 287-296
MC68HC16EVB, 146
MCORE architecture, 131, 393-446
memory models, 336
memory allocation, 4, 10
memory management, 114-116, 156
memory types, 153
memory-mapped I/O, 129
microcomputer, 123
microprocessor, 123
MIX compiler, 386
MMC2001 microcontroller, 393
macros for, 400
mmc2001.h header, 400, 407
mnemonics, 140, 422
modular arithmetic, 334
modular program development, 347-349, 391
monitor routine, 370-376
motor control routines, 255-275
multidimensional arrays, 82-83
N
names, 8
nested functions, 52
nested if statements, 422
Newton loop, 38
NIPND interrupt pending register, 414
noise spikes, 219
noisy switches, 255
normal interrupt enable register (NIER), 414
null pointer, 76, 98
null, 19, 60
Number-to-character conversion, 424
numeric encoding/decoding, 352-356
o
one time programmable, 128
one's complement, 28
operators, 24-34
optimizing code, 325
OPTION register, 159
OR, 26
OTP chip, 128, 154
output compare, 130-131, 185, 246-253
output_time function, 428
P&E Microcomputer Systems, Inc., 146
page zero, 158
parameters, copies of, 52
periodic interrupt, 309-315
phone book program, 360
PIT (see also programmable interval timer), 314-
315,395,402,419-420,425
pointers, 65-121
and function arguments, 70
array name as, 69
assigning, 69
comparing, 68
incrementing/decrementing, 69
null, 98
subtracting, 69
to functions, 84
type void as applied to, 69
PORTA, 85, 109, 132
portable code, 296, 404
pragma directives (C6805 compiler), 151
pragma, 151
precedence, 7, 30, 33, 34-36
preprocessor command, 2
print formatting, 113
printf, 2, 3, 5, 7, 18, 112
printing routine, 378-380
program counter (PC), 289
452 Index
program flow and control, 36-51
program memory, 159
programmable interval timer (PIT), 314-315,
395, 402, 419-420
programmable timer, 246-251
programming hierarchy, 347-348
prototype, function, 52, 432
PSR register, 400
pull( ), 60
pulse width modulation program for
MC68HC11, 245-251
pulse width modulation program for
MC68HC16, 297-302
pulse width modulator (PWM), 201-207, 247-
253, 255, 268
push( ), 60
put( ) function, 409, 411
putchar, 111, 382-383, 411
puts function, 382, 384, 413
queued serial peripheral interface module
(QSPI), 288
queued serial module (QSM), 305-308
quick sort, 23 1
R
RAM, 153
random access memory, 153
reading data from the keyboard, 371
read-only memory, 127
real time interrupt (RTI), 168, 176
realloc, 775
recursion, 61-63, 96, 157
recursive code, 324
reed switch, 268
re-entrant function, 61
register, 9, 13
relational operator, 26-27
reset function, 381-382
reset signal, 125, 169
reset time function, 428
return from interrupt, 126
return, 9
rfi instruction, 418
right shift operator, 29
RISC microcontrollers, 324, 393-446
ROM, 127, 153
RTI, 168, 176
RTS, 176
rvalue, 69
saving data to EEPROM, 376-378
SCCR1/SCCR2, 276
semaphore, 397-400, 403, 433
semicolon, use of in C, 53
sequence point, 11
serial communications control registers, 276
serial communications data register, 276
serial communications interface (SCI), 133
MC68HC11 family, 275-285
MC68HC16 family, 308
serial communications status register (SCSR),
276
serial I/O, 133
with MMC200 1,404-413
serial peripheral interface (SPI), 133
serial port, 382
SET INCLUDE, 2
setjmp function, 49, 119
Shell, D.L., 230
short, 9-10
signals, 119
signed, 9-10
sin(x), 777
sinh(x), 777
sizeof, 9, 34, 81
Software Development Systems (SDS), 393
software watchdog, 301
sort, 74-76, 230-237
bubble, 74
entry, 230
quick, 75, 230-237
Shell, 75, 230-231, 234, 236, 356
square, 56
SRAM, 288
Index 453
stack pointer (SP), 289
stack, 59
standard I/O, 119
static variables, 13-15
static, 9, 58
stdio.h header, 2, 3, 17, 52, 116
stdlib.h header, 101
STOP instruction, 169
storage classes, 12-15
strcat(s,t), 117
strchr(s,c), 117
strcmp(s,t), 117
strcpy(s,t), 117
string operations, 117
string, 2, 19
strlen, 72
strncat(s,t,n), 777
strncmp(s,t,n), 777
strncpy(s,t,n), 777
strrchr(s,c), 777
struct FILE, 110
struct, 9, 20, 23-24
structure tag, 88
structures, 87-106
pointers to, 88
self-referential, 95
types, 92
switch bounce, 255-258, 268
switch, 9, 20, 49-51
synchronous, 126
SYNCR register, 299
system integration module (SIM), 288, 296-297
chip selects, 298
external bus interface, 297
general purpose I/O, 298
interrupts, 298
reset and initialization, 298
system clock, 297
system configuration and protection mod-
ule, 297
talloc, 97, 102
tan(x), 77
TCNT, 246, 254
TCR, 168-169
telephone book function, 348
terminal emulator, 145
testing philosophy, 348
testing using evaluation boards, 142
TFLG1/2 registers, 304
time of day (TOD) clock, 419
time-of-day (TOD) clock, 131
timer control register (TCR), 181
timer counter register, 246
timer processor unit (TPU), 130
timer status register (TSR), 182
timer subsystems, 129-131, 166-173, 245-288,
419
TOF, 168
tolower(c), 777
toupper(c), 777
TPU, 130
trace buffer, 136
type conversions, 27-28
type declaration, 9-12
type, 9, 52
typedef, 9, 93
u
UART, 276
unary operators, 25
uninitialized interrupts, 415
union, 9, 20, 22-23, 107-108
universal asynchronous receiver transmitter
(UART), 276
unsigned, 9
utility functions, 118
table look-up, 318-325
tag, 88
V
variable types, 4
variable, 8-10
vector assignment, 303
vector initialization routine, 293
vector table, 125, 387-388
454 Index
void, 3, 9, 53, 86
volatile, 9, 11, 227
Von Neumann architecture, 124, 129
w
wait routine, 409
WAIT, 169
watchdog timer, 131, 301, 419
weighting functions for digital filters, 333
while, 9, 17, 36-38, 72
Embedded Technology™ Series
Programming Microcontrollers
in C, Second Edition
by Ted Van Sickle
INCLUDES WINDOWS 95/98 CD-ROM.
Completely updated new edition of a classic
for embedded systems designers and
programmers. It covers C basics, advanced C
topics, microcontroller basics and usage, and
gives example code, using the Motorola family
of microcontrollers, including RISC machines.
The CD-ROM contains the code from the
book, a full set of Motorola's microcontroller
documentation in PDF format, and a fully
searchable electronic version of the text.
1-878707-57-4 $59.95
Embedded Controller
Hardware Design
by Ken Arnold
INCLUDES WINDOWS CD-ROM. This
practical tutorial introduces the reader to the
design of embedded microprocessor- and
microcontroller-based systems. General
topics covered in the book include device
architecture, interfacing, timing, memory, I/O,
as well as design and development tech-
niques. The book presents the latest
application-oriented information concerning
this rapidly changing area of technology.
1-878707-52-3 $49.95
Controlling the World with
Your PC
by Paul Bergsman
INCLUDES PC DISK. A wealth of circuits
and programs that you can use to control the
real world. Connect to the parallel printer port
of your PC and monitor fluid levels, control
stepper motors, turn appliances on and off,
and much more. The accompanying disk for
PCs contains all the software files in ready-to-
use form. All schematics have been fully
tested. Great for embedded systems
engineers, as well as students and scientists.
1-878707-15-9 $35.00
CIRCUIT REFERENCES
Our list of circuit references and design
"cookbooks" contains valuable aids for
embedded designers.
The Forrest Mims
Engineers Notebook
by Forrest Mims III
Revised edition of a classic by world's best-
sellling electronics author. Hundreds of useful
circuits built from ICs and other parts.
1-878707-03-5 $19.95
The Forrest Mims Circuit
Scrapbook, Volumes I and II
by Forrest Mims III
More "greatest hits" circuit designs from
Forrest Mims. Volume I contains digital PLLs,
interval timers, light wave communicators, and
much more. Volume II contains comparators,
data loggers, laser diode devices, fiber optic
sensors, power supplies, and much more.
Vol. I: 1-878707-48-5 $19.95
Vol. II: 1-878707-49-3 $24.95
The Integrated Circuit
Hobbyist's Handbook
by Thomas R. Powers
This comprehensive "cookbook" of circuit
applications is conveniently cross-indexed by
device and application. Contains amplifiers,
filters, bus transceivers and bus buffers for
digital interfacing, counters, comparators, FSK
modulators and decoders, oscillators, and
much more.
1-878707-12-4 $19.95
Simple, Low-Cost
Electronics Projects
by Fred Blechman
Contains a wealth of fully tested electronics
design projects using commonly available
parts, each with circuit theory, parts lists, and
design and testing guidelines.
1-878707-46-9 $19.95
Visit our web site for more great technical
books on all subjects!
www.LLH-Publishing.com