Embedded Controllers
Embedded Controllers
Embedded Controllers
James M. Fiore
Embedded Controllers
Embedded Controllers
Using C and Arduino
by
James M. Fiore
Embedded Controllers
This Embedded Controllers Using C and Arduino, by James M. Fiore is copyrighted under the terms
of a Creative Commons license:
This work is freely redistributable for non-commercial use, share-alike with attribution
Embedded Controllers
Introduction
This text is designed to introduce and expand upon material related to the C programming language and
embedded controllers, and specifically, the Arduino development system and associated Atmel ATmega
microcontrollers. It is intended to fit the time constraints of a typical 3 to 4 credit hour course for
electrical engineering technology and computer engineering technology programs, although it could also
fit the needs of a hardware-oriented course in computer science. As such, the text does not attempt to
cover every aspect of the C language, the Arduino system or Atmel AVR microcontrollers. The first
section deals with the C language itself. It is assumed that the student is a relative newcomer to the C
language but has some experience with another high level language, for example, Python. This means
concepts such as conditionals and iteration are already familiar and the student can get up and running
fairly quickly. From there, the Arduino development environment is examined.
Unlike the myriad Arduino books now available, this text does not simply rely on the Arduino libraries.
As convenient as the libraries may be, there are other, sometimes far more efficient, ways of
programming the boards. Many of the chapters examine library source code to see whats under the
hood. This more generic approach means it will be easier for the student to use other processors and
development systems instead of being tightly tied to one platform.
All Atmel schematics and data tables are derived from the latest version (October, 2014) of the Atmel
328P documentation which may be found at http://www.atmel.com/devices/ATMEGA328P.aspx This
serves as the final word on the operation and performance of the 328P and all interested parties should
become familiar with it.
There is a companion lab manual to accompany this text. Other OER (Open Educational Resource) lab
manuals in this series include DC and AC Electrical Circuits, Computer Programming with Python and
Semiconductor Devices. An OER is available for Operational Amplifiers and Linear Integrated Circuits,
and a Semiconductor Devices text is due in early 2017. Please check my web sites for the latest versions.
Embedded Controllers
Embedded Controllers
Table of Contents
1. Course Introduction
2. C Memory Organization
10
3. C Language Basics .
14
4. C Language Basics II
24
32
36
40
8. C Pointers
48
9. C Look-Up Tables
52
10. C Structures .
56
60
12. C Memory*
64
68
72
74
78
84
90
98
102
106
112
116
124
132
136
142
146
154
Appendices
160
Index
165
* Included for more complete language coverage but seldom used for small to medium scale embedded work.
** Including modest comic relief for film noir buffs.
Embedded Controllers
1. Course Introduction
1.1 Overview
This course introduces the C programming language and specifically addresses the issue of embedded
programming. It is assumed that you have worked with some other high level language before, such as
Python, BASIC, FORTRAN or Pascal. Due to the complexities of embedded systems, we begin with a
typical desktop system and examine the structure of the language along with basic examples. Once we
have a decent grounding in syntax, structure, and the development cycle, we switch over to an embedded
system, namely an Arduino based development system.
This course is designed so that you can do considerable work at home with minimal cost, if you choose
(entirely optional, but programming these little beasties can be addicting so be fore warned). Along with
this course text you will need an Arduino Uno board (about $25) and a USB host cable. A small wall
wart power adapter for it may also be useful. Theres a lot of free C programming info on the net but if
you prefer print books and want more detail, you may also wish to purchase one of the many C
programming texts available. Two good titles are Kochans book Programming in C and the one by
Deitel & Deitel C-How to Program. Whichever book you choose, make sure that its focus is C, not C++.
You will also need a desktop C compiler. Just about any will do, including Visual C/C++, Borland, Code
Warrior, or even GCC. A couple of decent freeware compilers available on the net include Pelles C and
Miracle C.
Embedded Controllers
desktop work is done in C++ as well as C, most embedded work is done in C. Desktop development
systems are usually referred to as C/C++ systems meaning that theyll do both. Embedded
development systems may be strictly C (as is ours).
Where can I buy an Arduino development board?
The Arduino Uno board is available from a variety of sources including Digi-Key, Mouser, Parts
Express and others. Shop around!
Whats the difference between desktop PC development and embedded
programming?
Desktop development focuses on applications for desktop computers. These include things like word
processors, graphing utilities, games, CAD programs, etc. These are the things most people think of
when they hear the word computer. Embedded programming focuses on the myriad nearly invisible
applications that surround us every day. Examples include the code that runs your microwave oven,
automobile engine management system, cell phone, and many others. In terms of total units,
embedded applications far outnumber desktop applications. You may have one or even a few PCs in
your house, but you probably use dozens of embedded applications every day. Embedded
microcontrollers tend to be much less powerful but also much less expensive than their PC
counterparts. The differing programming techniques are an integral part of this course and we shall
spend considerable time examining them.
How does C compare with Python?
If, like many students taking this course, your background is with the Python language, you may find
certain aspects of C a little odd at first. Some of it may seem overly complicated. Do not be alarmed
though. The core of the language is actually simple. Python tends to hide things from the programmer
while C doesnt. Initially, this seems to make things more complicated, and it does for the most
simple of programs. For more complicated tasks C tends to cut to the heart of the matter. Many kinds
of data manipulation are much easier and more efficient in C than in other languages. One practical
consideration is that C is a compiled language while most versions of Python are essentially
interpreted. This means that there is an extra step in the development cycle, but the resulting compiled
program is much more efficient. We will examine why this is so a little later.
How does C compare with assembly language?
Assembly has traditionally been used when code space and speed are of utmost importance. Years
ago, virtually all embedded work was done in assembly. As microcontrollers have increased in power
and the C compilers have improved, the tables have turned. The downside of assembly now weighs
against it. Assembly is processor-specific, unstructured, not standardized, nor particularly easy to read
or write. C now offers similar performance characteristics to assembly but with all the advantages of a
modern structured language.
Embedded Controllers
2. C Memory Organization
2.1 Introduction
When programming in C, it helps if you know at least a little about the internal workings of simple
computer systems. As C tends to be close to the metal, the way in which certain things are performed as
well preferred coding techniques will be more apparent.
First off, lets narrow the field a bit by declaring that we will only investigate a fairly simple system, the
sort of thing one might see in an embedded application. That means a basic processor and solid state
memory. We wont worry about disk drives, monitors, and so forth. Specific details concerning controller
architecture, memory hardware and internal IO circuitry are covered in later chapters.
10
Embedded Controllers
address 0
address 1
address 2
address 3
address 4
address 5
Each address or slot represents a place we can store one byte. If we had to remember specific addresses
we would be doing a lot of work. Instead, the C compiler will keep track of this for us. For example, if we
declare a char named X, it might be at address 2. If we need to print that value, we dont have to say
fetch the value at address 2. Instead we say; fetch the value of X and the compiler generates code to
make this work out to the proper address (2). This abstraction eases our mental burden considerably. As
many variables require more than one byte, we may need to combine addresses to store a single value. For
example, if we chose a short int, that needs two bytes. Suppose this variable starts at address 4. It will
also require the use of address 5. When we access this variable the compiler automatically generates the
code to utilize both addresses because it knows were using a short int. Our little six byte memory
map could hold 6 char, 3 short int, 1 long int with 1 short int, 1 long int with 2 char, or
some other similar combination. It cannot hold a double as that requires 8 bytes. Similarly, it could not
hold an array of 4 or more short int.
Arrays are of special interest as they must be contiguous in memory. For example, suppose a system has
1000 bytes of memory and a 200 element char array was declared. If this array starts at address 500 then
all of the slots from 500 through 699 are allocated for the array. It cannot be created in scattered fashion
with a few bytes here and a few bytes there. This requirement is due to the manner in which arrays are
indexed (accessed), as we shall see later.
2.4 Stacks
Many programs need only temporary storage for certain variables. That is, a variable may only be used
for a limited time and then thrown away. It would be inefficient to allocate permanent space for this
sort of variable. In its place, many systems use a stack. Ordinarily, an application is split into two parts, a
code section and a data section. The data section contains the permanent (global) data. As these two
will not consume the entire memory map, the remainder of the memory is often used for temporary
storage via a stack. The stack starts at the opposite end of the memory map and grows toward the code
and data sections. It is called a First-In-Last-Out stack or FILO stack. It works like a stack of trays in a
Embedded Controllers
11
cafeteria. The first try placed on the stack will be the last one pulled off and vice versa. When temporary
variables are needed, this memory area is used. As more items are needed, more memory is taken up. As
our code exits from a function, the temporary (auto) variables declared there are no longer needed, and
the stack shrinks. If we make many, many function calls with many, many declared variables, it is
possible for the stack to overrun the code and data sections of our program. The system is now corrupt,
and proper execution and functioning of the program are unlikely.
address 0
area used by code and data
A bus typically refers to a collection of wires or connections upon which multiple data bits (or address bits) are
sent as a group.
12
Embedded Controllers
Embedded Controllers
13
3. C Language Basics
3.1 Introduction
C is a terse language. It is designed for professional programmers who need to do a lot with a little code
quickly. Unlike BASIC or Python, C is a compiled language. This means that once you have written a
program, it needs to be fed into a compiler that turns your C language instructions into machine code that
the microprocessor or microcontroller can execute. This is an extra step, but it results in a more efficient
program than an interpreter. An interpreter turns your code into machine language while its running,
essentially a line at a time. This results in slower execution. Also, in order to run your program on another
machine, that machine must also have an interpreter on it. You can think of a compiler as doing the
translation all at once instead of a line at a time.
Unlike many languages, C is not line oriented, but is instead free-flow. A program can be thought of as
consisting of three major components: Variables, statements and functions. Variables are just places to
hold things, as they are in any other language. They might be integers, floating point (real) numbers, or
some other type. Statements include things such as variable operations and assignments (i.e., set x to 5
times y), tests (i.e., is x more than 10?), and so forth. Functions contain statements and may also call other
functions.
14
Embedded Controllers
Bytes Used
1
1
2
2
4
4
Minimum
-128
0
-32768
0
-2 billion
0
Maximum
127
255
32767
65535
2 billion
4 billion
1.2 E -38
3.4 E +38
2.3 E -308
1.7 E +308
C also supports arrays and compound data types. We shall examine these in a later segment.
Variables must be declared before they are used. They cannot be created on a whim, so to speak, as they
are in Python. A declaration consists of the variable type followed by the variable name, and optionally,
an initial value. Multiple declarations are allowed. Here are some examples:
char x;
unsigned char y;
short z, a;
float b =1.0;
Note that each of these declarations is followed with a semi-colon. The semi-colon is the C language way
of saying This statement ends here. This means that you can be a little sloppy (or unique) in your way
of dealing with spaces. The following are all equivalent and legal:
float b = 1.0;
float b=1.0;
float
b
=
Embedded Controllers
1.0 ;
15
3.3 Functions
Functions use the same naming rules as variables. All functions use the same template that looks
something like this:
return_value function_name( function argument list )
{
statement(s)
}
You might think of the function in the mathematical sense. That is, you give it some value(s) and it gives
you back a value. For example, your calculator has a sine function. You send it an angle and it gives you
back a value. In C, functions may have several arguments, not just one. They might not even have an
argument. Also, C functions may return a value, but they dont have to. The guts of the function are
defined within the opening and closing brace pair {}. So, a function which takes two integers, x and y, as
arguments, and returns a floating point value will look something like this:
float my_function( int x, int y )
{
//...appropriate statements here...
}
If the function doesnt take or return values, the word void is used. If a function neither requires values
nor returns a value, it would look like:
void other_function( void )
{
//...appropriate statements here...
}
This may appear to be extra fussy work at first, but the listing of data types makes a lot of sense because
C has something called type checking. This means that if you try to send a function the wrong kind of
variable, or even the wrong number of variables, the compiler will warn you that youve made a mistake!
Thus if you try to send my_function() above two floats or three integers, the compiler will complain
and save you a big headache during testing.
All programs must have a place to start, and in C, program execution begins with a function called main.
This does not have to be the first function written or listed, but all programs must have a function called
main. Heres our first program, found in Figure 3.2, following:
16
Embedded Controllers
C also allows // to denote a single line comment without the backend pairing.
Embedded Controllers
17
The new math function takes two floats as arguments and returns a float to the caller. The compiler
sees the new function before it is used in main(), thus, it already knows that it should be sent two
floats and that the return value must be assigned to a float. It is very important to note that the new
math function uses different variable names (a and b) from the caller (x and y). The variables in the new
math function are really just place-holders. The values from the original call (x and y) are copied to these
new variables (a and b) and used within the new function. As they are copies, they can be altered without
changing the original values of x and y. In this case, x and y are said to be local to the main() function
while a and b are local to the add_mult_div() function. In other words, a isnt visible from main()
so you cant accidentally alter it! Similarly, x isnt visible from add_mult_div(), so you cant
accidentally alter it either. This is a positive boon when dealing with large programs using many variable
names. While its not usually preferred, there are times when you want a variable to be known
everywhere. These are called global items. You can make variables global by simply declaring them at
the beginning of the program outside of the functions (i.e., right after that initial comment in our
example).
3.4 Libraries
The examples above are rather limited because, although they perform a calculation, we have no way of
seeing the result! We need some way to print the answer to the computer screen. To do this, we rely on
system functions and libraries. There are a series of libraries included with most C development systems
to cover a variety of needs. Essentially, someone has already coded, tested and compiled a bunch of
functions for you. You add these functions to your program through a process called linking. Linking
simply combines your compiled code along with any required library code into a final executable
program. For basic printouts, data input, and the like, we use the standard IO (Input/Output) library, or
stdio for short. There is a function in this library named printf() for print formatted. So that the
18
Embedded Controllers
compiler can do type checking, it must know something about this new function. We tell the compiler to
look into a special file called a header file to find this information. Every library will have an associated
header file (usually of the same name) and it will normally end with a .h file extension. The compiler
directive is called an include statement.
// Our third program, this is an example of a single line comment
#include <stdio.h>
void main( void )
{
printf(Hello world.\n);
}
This program simply prints the message Hello world. to the screen. The backslash-n combo is a special
formatting token that means add a new line (i.e., bring the cursor to the line below). If we did not add the
#include directive, the compiler wouldnt know anything about printf(), and would complain when
we tried to use it. So, whats in a header file? Well, among other things they contain function prototypes.
The prototypes are nothing more than a template. You can create your own by cutting and pasting your
function name with argument list and adding a semicolon to it. Here is the function prototype for our
earlier math function:
float add_mult_div( float a, float b );
You could make your own library of functions if you want. To use them, all youd need is an appropriate
include statement in your code, and remember to add in your library code with the linker. This will
allow you to reuse code and save time. We will look at multiple file projects and more on libraries in a
later segment.
Consequently, if we want to print out the answer to the first program, wed wind up with something like
Figure 3.6 on the following page:
Embedded Controllers
19
The %f in the printf() function serves as a place holder for the variable z. If you need to print several
values you can do something like this:
printf(The answer from %f and %f is %f\n, x, y, z);
In this case, the first %f is used for x, the second %f for y, and the final one for z. The result will look
like:
The answer from 2.0 and 3.0 is 1.2
20
Embedded Controllers
A final caution: The examples above are meant to be clear, but not necessarily the most efficient way of
doing things. As we shall see, sometimes the way you code something can have a huge impact on its
performance. Given the power of C, expert programmers can sometimes create code that is nearly
indecipherable for ordinary people. There is a method behind the apparent madness.
3.7 Summary
Here are some things to keep in the back of your mind when learning C:
Embedded Controllers
21
3.8 Exercises
1. Write a C code comment that includes your name and the date. Use both the single line and the multiline styles.
2. Write a function that will take three floating point values as arguments. The function should return the
average value of the three arguments.
3. Write a program that will print out your name.
22
Embedded Controllers
Embedded Controllers
23
4. C Basics II
4.1 Input and Output
Weve seen the use of printf() to send information to the computer screen. printf() is a very large
and complicated function with many possible variants of format specifiers. Format specifiers are the %
things used as placeholders for values. Some examples are:
%f
%lf
%e
%g
%d
%ld
%x
%o
%u
%c
%s
float
double (long float)
float using exponent notation
float using shorter of e or f style
decimal integer
decimal long integer
hexadecimal (hex or base 16) integer
octal (base 8) integer
unsigned integer
single character
character string
Suppose that you wanted to print out the value of the variable ans in decimal, hex, and octal. The
following instruction would do it all:
printf(The answer is %d, or hex %x, or octal %o.\n, ans, ans, ans );
Note how the three variables are labeled. This is important. If you printed something in hex without some
form of label, you might not know if it was hex or decimal. For example, if you just saw the number 23,
how would you know its 23 decimal or 23 hex (35 decimal)? For that matter, how would you set a hex
constant in your C code? The compiler would have no way of knowing either. To get around this, hex
values are prefixed with 0x. Thus, we have 0x23 for hex 23. The printf() function does not
automatically add the 0x on output. The reason is because it may prove distracting if you have a table
filled only with hex values. Its easy enough to use 0x%d instead of just %d for the output format.
You can also add a field width specifier. For example, %5d means print the integer in decimal with 5
spaces minimum. Similarly, %6.2f means print the floating point value using 6 spaces minimum. The
.2 is a precision specifier, and in this case indicates 2 digits after the decimal point are to be used. As
you can see, this is a very powerful and flexible function!
The mirror input function is scanf(). This is similar to Pythons input statement. Although you can
ask for several values at once, it is generally best to ask for a single value when using this function. It uses
the same sort of format specifiers as printf(). There is one important point to note. The scanf()
function needs to know where to place the entered value in computer memory. Simply informing it of the
24
Embedded Controllers
name of the variable is insufficient. You must tell it where in memory the variable is, in other words, you
must specify the address of the variable. C uses the & operator to signify address of. For example, if you
wish to obtain an integer from the user and place it in a variable called voltage, you might see a
program fragment like so
printf(Please enter the voltage:);
scanf(%d, &voltage);
In some systems, 80 bit doubles and 64 bit integers are also available.
Embedded Controllers
25
Note how you are directing the compiler to turn the division into a short int. Otherwise, the result is in
fact a long int due to the promotion of y to the level of z. Whats the value of x? Why its 6 of course!
Remember, the fractional part is meaningless, and thus lost, on integer divides.
Casting is also useful when using math functions. If you have to use float, you can cast them to/from
double to make use of functions defined with double. For example, suppose a, b, and c are declared as
float but you wish to use the pow() function to raise a to the b power. pow() is defined as taking two
double arguments and returning a double answer.
c = (float)pow( (double)a, (double)b );
This is a very explicit example. Many times you can rely on a silent cast promotion to do your work for
you as in the integer example above. Sometimes being explicit is a good practice just as a form of
documentation.
26
Embedded Controllers
AND
OR
XOR
Ones Complement
Shift Right
Shift Left
Lets look at a few examples. Suppose the variables X, Y and Z are unsigned chars. X and Y are set to
13 and 134, respectively. In hex, thats 0x0d and 0x86 for bit patterns of 00001101 and 10000110.
Z
Z
Z
Z
Z
Z
=
=
=
=
=
=
X<<3;
X>>1;
~X;
X|Y;
X&Y;
X^Y;
//
//
//
//
//
//
Z
Z
Z
Z
Z
Z
is
is
is
is
is
is
01101000
00000110
11110010
10001111
00000100
10001011
or
or
or
or
or
or
0x68
0x06
0xf2
0x8f
0x04
0x8b
In C, bit position counting, like most sequences, starts from position 0 not position 1.
Embedded Controllers
27
If sometime later you wanted to also set the 1st and 2nd bits to output mode while keeping everything else
intact, the easy way to do it is simply to OR the bits you want:
DDR = DDR | 0x06;
The prior operation may be performed using the more common shorthand:
DDR |= 0x06;
Note that the preceding code does not affect any of the other bits so they stay in whatever mode they were
originally. By the way, a set of specific bits (such as the 0x06 above) is often referred to as a bit pattern or
bitmask.
To see if a specific bit is set, simply AND instead of OR. So, to see if the 1st bit of DDR is set for output
mode, you could use something like:
if ( DDR & 0x02 )
// true if set
Clearing bits requires ANDing with a bitmask that has been complemented. In other words, all 1s and 0s
have been reversed in the bit pattern. If, for example, we want to clear the 0th and 4th bits, wed first
complement the bit pattern 0x11 yielding 0xee. Then we AND:
DDR &= 0xee;
Often, its easier to just use the logical complement operator on the original bit pattern and then AND it:
DDR &= (~0x11);
If youre dealing with a single bit, you can use the left shift operator so you dont even have to bother
figuring out the bit pattern in hex. To set the 3rd bit and then clear the 4th bit of DDR, you could use the
following:
DDR |= (0x01<<3);
DDR &= ~(0x01<<4);
These operations are so common that they are often invoked using an in-line expansion via a #define.
4.6 #define
Very often it is desirable to use symbolic constants in place of actual values. For example, youd probably
prefer to use a symbol such as PI instead of the number 3.14159. You can do this with the #define
preprocessor directive. These are normally found in header files (such as stdio.h or math.h) or at the top
of a module of C source code. You might see something like:
#define PI 3.14159
Once the compiler sees this, every time it comes across the token PI it will replace it with the value
3.14159. This directive uses a simple substitution but you can do many more complicated things than
this. For example, you can also create something that looks like a function:
28
Embedded Controllers
#define parallel((x),(y))
((x)*(y))/((x)+(y))
Why do this? Because its an in-line expansion or macro. That means that theres no function call
overhead and the operation runs faster. At the same time, it reads like a function, so its easier for a
programmer to follow. OK, but why all the extra parentheses? The reason is because x and y are
placeholders, and those items might be expressions, not simple variables. If you did it this way you might
get in trouble:
#define parallel(x,y)
x*y/(x+y)
As multiplication is executed before addition, you wind up with 2 being added to the product of b times c
after the division, which is not the same as the sum of 2 and b being multiplied by c, and that quantity
then being divided. By using the extra parentheses, the order of execution is maintained.
Referring back to the bit field operations, here are some useful definitions for what appear to be functions
but which are, in fact, bitwise operations expanded in-line:
#define bitRead(value, bit) (((value) >> (bit)) & 0x01)
#define bitSet(value, bit)
((value) |= (1UL << (bit)))
#define bitClear(value, bit) ((value) &= ~(1UL << (bit)))
The 1UL simply means 1 expressed as an unsigned long. Finally, bit could also be defined as a symbol
which leads to some nice looking self-documenting code:
#define LEDBIT 7
// more code here...
bitSet( DDR, LEDBIT );
#define expansions can get quite tricky because they can have nested references. This means that one
#define may contain within it a symbol which is itself a #define. Following these can be a little
tedious at times but ultimately are worth the effort. We shall look at a few down the road. Remember,
these are done to make day-to-day programming easier, not to obfuscate the code. For now, start with
simple math constant substitutions. They are extremely useful and easy to use. Just keep in the back of
your mind that, with microcontrollers, specific registers and ports are often given symbolic names such as
Embedded Controllers
29
PORTB so that you don't have to remember the precise numeric addresses of them. The norm is to place
these symbolic constants in ALL CAPS.
4.7 Keywords
Here is a list of keywords in the C language:
auto
continue
entry
if
sizeof
typedef
break
do
extern
int
short
union
case
default
float
long
static
unsigned
char
double
for
register
struct
volatile
const
else
goto
return
switch
while
4.8 Exercises
1. Write a line of code that will print the statement The result is x volts where x is the value given by the
floating point variable output_voltage.
2. Write a line of code to define a constant called RECIP2PI that is equal to 1/(2).
3. Write the code to determine the number of bytes required for a variable called POWER_SUPPLY.
4. Assume the 8 bit variable X exists. Write the code to set the MSB (most significant bit), leaving all
other bits unchanged.
5. Assume the 8 bit variable Y exists. Write the code to set the LSB (least significant bit), leaving all other
bits unchanged.
6. Assume the 8 bit variable Z exists. Write the code to clear the MSB and LSB, leaving all other bits
unchanged.
7. Assume the 8 bit variable W exists. Write the code to complement each bit (flip 0 to 1 and 1 to 0).
8. If X is 0x04 and Y is 0x09, what are a) X|Y, b) X&Y, c) ~X, d) 0xf1&Y?
9. If X is 0xf0 and Y is 0x11, what are a) X|Y, b) X&Y, c) ~X, d) 0xf1&Y?
30
Embedded Controllers
Embedded Controllers
31
Static variables are used when you need a variable that maintains its value between function calls. So, if
we need a variable that will appear the way we left it from the last call, we might use something like
this:
static char y;
There is one important difference between auto and static types concerning initialization. If an auto
variable is initialized in a function as so:
char a=1;
Then a is set to 1 each time the function is entered. If you do the same initialization with a static, as in:
static char b=1;
Then b is set to 1 only on the first call. Subsequent entries into the function would not incur the
initialization. If it did reinitialize, what would be the sense of having a static type? This is explained by
the fact that a static does not usually use the stack method of storage, but rather is placed at a fixed
memory location. Again, C does not require the use of a stack, rather, it is a typical implementation.
32
Embedded Controllers
Two useful but not very common modifiers are volatile and const. A volatile variable is one that
can be accessed or modified by another process or task. This has some very special uses (typically, to
prevent an optimizing compiler from being too aggressive with optimizations-more on this later). The
const modifier is used for declaring constants, that is, variables that should not change value. In some
instances this is preferred over using #define as type checking is now available (but you cant use the
two interchangeably).
5.2 Scope
Scope has to do with where variables are seen. We have already mentioned the idea of global and local
in previous work but it is time to delve a little deeper. Generally, variables only exist within the block
they are declared. While it is legal to declare variables inside of a conditional or loop block, we normally
declare variables at the very beginning of a function. Consequently, these variables are known within the
function. That is, their scope of reference is within the function. Nothing outside of the function knows
anything about them. Thus, we say that they are local, or perhaps localized, to the function. For example,
consider the two function fragments below:
void func1( void )
{
int x;
int y;
//...some code here...
}
void func2( void )
{
int y;
int z;
//...some other code here...
}
There is no direct way to access the z variable of func2() from func1(). Likewise, there is no direct
way to access the x variable of func1() from func2(). More interestingly, the y variables of func1()
and func2() are entirely different! They do not refer to the same variable. This sometimes can be
confusing for new programmers but it is essential for large programs. Imagine that you were working
with a team of people on a very large program, perhaps tens of thousands of lines long. If the idea of local
scope did not exist, youd have to make sure that every single variable in the program had a unique name!
This would create a nightmare of confusion. By using local scope, youre saying: I only need this
variable for a job within this function. As it only needs to exist within this function, its name is only
meaningful within this function.
If some form of universally known data item is needed, we can resort to the global. Globals act pretty
much like statics and are usually stored the same way. If you have a program that consists of a single file,
you can declare your globals by listing them at the beginning of the program before (and outside of) any
functions. In this way they will be read by the compiler first and will therefore be known to all
functions that follow. Do not get in the habit of declaring all variables as global. This is considered a bad
and inefficient coding method. Get into the habit of using locals as the norm and resort to globals only
when called for.
Embedded Controllers
33
If you have a multiple file project, how do you get the functions in the second file to recognize the globals
declared in the first file? In this case, youll create a header file and use the #include directive. For
example, suppose your project consists of two C source files named foo.c and bar.c.
In foo.c you declare the following global:
int m;
In order for the functions in bar.c to see m, youll create a header file, perhaps called foo.h. foo.h will
contain the following:
extern int m;
Meaning that an integer named m has been declared externally (i.e., in another file). At the top of bar.c
youll add the line:
#include <foo.h>
So, when bar.c is compiled, the compiler will first open up foo.h. It will see that the variable m has been
declared elsewhere and puts it in a known variables list. It then continues to compile the remainder of
you code. When it comes across m in some function, the compiler understands that this is a variable that
was declared in another file. No problem!
So, you can now see that header files are largely composed of definitions and declarations from other
places, namely external data and function prototypes.
5.4 Exercises
1. Assume a function declares a variable like so: static int x=0; The function increments the
variable and then prints its value. What does the function print out on the tenth call to the function? How
would this change if the static keyword was not used?
2. Consider the following snippet of code:
void doug( void )
{
int x=0;
x=x+1;
printf( %d\n, x );
}
void dinsdale( void )
{
int x=20;
x=x+1;
printf( %d\n, x );
}
Suppose you call doug() five times in a row and then call dinsdale() five times in a row. What would
the resulting output look like?
34
Embedded Controllers
Embedded Controllers
35
An array of 10 floats
An array of 20 longs, or 80 bytes
A two-dimension array, 10 by 15 chars each, 150 bytes total
Note the use of square brackets and the use of multiple sets of square brackets for higher dimension
arrays. Also, C arrays are counted from index 0 rather than 1.5 For example, the first item of results[]
is results[0]. The last item is results[9]. There is no such item here as results[10]. That
would constitute an illegal access. You can pre-initialize arrays by listing values within braces, each
separated by a comma:
double a[5] = {1.0, 2.0, 4.7, -177.0, 6.3e4};
If you leave the index out, you get as many elements as you initialize:
short int b[] = {1, 5, 6, 7}; /* four elements */
If you declare more than you initialize, the remainder are set to zero if the array is global or static (but not
if auto).
short int c[10] = {1, 3, 20}; /* remaining 7 are set to 0 */
If you are dealing with character strings you could enter the ASCII codes for the characters, but this
would be tedious in most cases. C lets you specify the character in single quotes and it will do the
translation:
char m[20] = {m, y, , d, o, g, 0};
(The reason for the trailing 0 will be evident in a moment.) Even easier is to specify the string within
double quotes:
char n[20]={Bill the cat};
Consider the string n[] above. It contains 12 characters but was declared to hold 20. Why do this? Well,
you may need to copy some other string into this variable at a future time. By declaring a larger value, the
variable can hold a larger string later. At this point you might be wondering how the C library functions
know to use just a portion of the allocated space at any given time (in this case, just the first 12
5
The reason for this will be apparent when we cover addresses and pointers.
36
Embedded Controllers
characters). This is possible through a simple rule: All strings must be null terminated. That is, the
character after the final character in use must be null, numerically 0. In the case of the direct string
initialization of n[], the null is automatically added after the final character, t. In the case of the
character-by-character initialization of m[], we had to do it manually. The null is extremely important.
Functions that manipulate or print strings look for the trailing null in order to determine when their work
is done. Without a null termination, the functions will just churn through memory until they eventually hit
a null, which may cause system violations and even application or operating system crashes. Note that
char my_pet[] = {fido};
actually declares five characters, not four (four letters plus the terminating null). As C counts from index
0, this declaration is equivalent to:
my_pet[0]=
my_pet[1]=
my_pet[2]=
my_pet[3]=
my_pet[4]=
f;
i;
d;
o;
0;
The trailing null may also be written as \0. It is important to note that without the backslash, this has
an entirely different meaning! \0 means null, but 0means the numeral 0.
So, if you wanted to copy the contents of my_pet[] into n[], you could write:
strcpy( &n[0], &my_pet[0] );
If youre awake at this point, you might ask Whats with the ampersand? Good question! What the
string copy function needs are the starting addresses of the two arrays. In essence, all it does is copy a
character at a time from the source to the destination. When it hits the trailing null its done. Weve
already seen the address of (&) operator earlier when we looked at scanf(). So, all were saying here
is For the source, start at the address of the first character of my_pet[], and for the destination, start at
the first character of n[]. This can be a little cumbersome, so C offers a shortcut. You can think of & and
[] as sort of canceling each other out. Wed normally write:
strcpy( n, my_pet );
Note that it is perfectly acceptable to use an index other than zero if you need to copy over just a chunk of
the string. You could start copying from index 2 if youd like, and just get do instead of fido:
Embedded Controllers
37
strcpy( n, &my_pet[2] );
that is, dont start at the address of the first element of my_pet[], start 2 characters later. Well look at
this sort of manipulation much closer when we examine addresses and pointers.
What happens if the source string has more characters than the destination string was allocated to? For
example, what if you did this?
strcpy( my_pet, n );
This results in a memory overwrite that can accidentally destroy other variables or functions. Very bad!
Your program may crash, and in some cases, your operating system my crash. To protect against this, you
can use strncpy(). This places a limit on the number of characters copied by adding a third argument.
As the destination only has space for 5 characters, youd use:
strncpy( my_pet, n, 5 );
This function will stop at 5 characters. Unfortunately, it wont automatically null terminate the string if
the limit is reached. To be safe, youd need to add the following line:
my_pet[4] = 0; /* force null termination */
Remember, as C counts from 0, index 4 is the fifth (and final) element. There are many functions
available to manipulate strings as well as individual characters. Here is a short list:
strcmp()
strcmpi()
strncmp()
strncat()
strlen()
isupper()
isalpha()
tolower()
38
Embedded Controllers
If you dont have library documentation, it can be very instructive to simply open various header files and
look at the function prototypes to see whats available. Whatever you do though, dont edit these files!
Finally, if you need to convert numeric strings into integer or floating point values, use the functions
atoi(), atol() and atof(). (ASCII to int or long int in stdlib.h, ASCII to float in math.h).
6.3 Exercises
1. Write the code to declare an array of 12 single precision real numbers.
2. Write the code to declare an array of 15 eight bit signed integers.
3. Assume that an array of 100 double precision real numbers has been declared and is named points.
Write the code to print out the first item of points. Also, write the code to set the last item of points to
0.0.
4. Declare a string called mammal and initialize it to the word woodchuck.
5. Do you see any potential problems with this snippet of initialization code? If so, explain the issues and
how they might be corrected.
char bird;
bird[0]=s
bird[1]=w
bird[2]=a
bird[3]=l
bird[4]=l
bird[5]=o
bird[6]=w
Embedded Controllers
39
If there is only a single statement in the block (i.e., between the braces), the braces may be removed if
desired:
if( test condition(s).. )
//...single statement...
else
//...do other statement...
The test condition may check for numerous possibilities. The operators are:
==
!=
>
<
>=
<=
equality
inequality
greater than
less than
greater than or equal to
less than or equal to
It is very important to note that equality uses a double equal sign. A single equal sign is an assignment
operation. Dont think equals, think same as, with one symbol for each word. You may also use
Boolean (logic) operators, as shown in Figure 7.2.
40
Embedded Controllers
||
&&
!
OR
AND
NOT
Note that the logical operators do not behave the same as the similarly named bitwise operators. For
example, a logical AND returns TRUE if its two arguments are non-zero, not necessarily the same bits.
That is 1 & 2 yields 0, but 1 && 2 yields TRUE. TRUE is taken to be any non-zero value. Any
variable or expression that evaluates to a value other than zero is logically TRUE. If the result is zero,
then it is logically FALSE. Time for some examples. The conditional is written as a fragment with an
explanation following:
if( a==6 )
/* taken only if the variable a is a 6 */
if( b!=7 )
/* taken as long as the variable b isnt 7 */
if( (a==6) && (b!=7) )
/* taken as long as a is 6 and b is something other than 7 */
if( (a==6) || (b!=7) )
/* taken as long as a is 6 or b is something other than 7 */
if( a==0 )
/* taken if a is zero */
if( !a )
/* another way of saying taken if a is zero */
if( a!=0 )
/* taken if a is not zero */
if( a )
/* another way of saying taken if a is not zero */
How you word something is up to you. The following two code fragments are equivalent:
if( a==b )
do_x();
else
do_y();
if( a!=b )
do_y();
else
do_x();
It is very common for new programmers to use = when they want ==. This can have disastrous results.
Consider the following code fragment:
Embedded Controllers
41
if( a=b )
What does this do? At first glance, you might think it tests to see if a and b are the same. It does nothing
of the kind! Instead, it assigns the value of b to a and then checks to see if that value is non-zero. In other
words, it does this:
a=b;
if( a )
A trick to help you with this, at least with constants, is to reverse the normal order. Instead of writing
if( a==6 ), use if( 6==a ). This way, if you accidentally use a single equal sign, the compiler will
cough up a syntax error.
7.2 Nesting
If a multiple condition wont cut it, you can nest if/else tests. Nesting conditionals is easy:
if( test condition(s).. )
{
if( other tests.. )
{
}
else
{
}
}
else
{
if( still other tests.. )
{
}
else
{
}
}
You can go many levels deep if you desire. Note that C, unlike Python, doesnt require the indenting
shown, but it is expected formatting. For selection of a single value out of a list, you can use the
switch/case construct. The template looks like:
switch( test_variable )
{
case value_1:
//...do stuff...
break;
case value_2:
//...do other stuff...
break;
default:
//...do stuff for a value not in the list...
break;
}
42
Embedded Controllers
The default section is optional. Also, it does not have to be the final item in the list. If a break is left
out, the code will simply fall through to the next case, otherwise code execution jumps to the closing
brace. Also, cases can be stacked together. The action statements for each case may include any legal
statements including assignments, function calls, if/else, other switch/case, and so on. Note that
you cannot check for ranges, nor can the cases be expressions. The cases must be discrete values. The
following example shows all of these. The action statements are replaced with simple comments.
switch( x )
{
case 1:
/* This code performed only if x is 1, then jump to closing
brace */
break;
case 2:
/* This code performed only if x is 2, then jump to closing
brace */
break;
case 3:
/* This code performed only if x is 3, but continue to next
case (no break statement) */
case 4:
case 5:
/* This code performed only if x is 3, 4, or 5, */
break;
default:
/* this code performed only if x is not any of 1,2,3,4, or
5, then jump to closing brace (redundant here) */
break;
}
Sometimes it is very handy to replace the numeric constants with #define values. For example, you
might be choosing from a menu of different circuits. You would create some #define values for each at
the start of the file (or in a header file) as so:
#define VOLTAGE_DIVIDER
#define EMITTER_FEEDBACK
#define COLLECTOR_FEEDBACK
/* etc... */
1
2
3
You would then write a much more legible switch/case like so:
switch( bias_choice )
{
case VOLTAGE_DIVIDER:
/* do volt div stuff */
break;
case EMITTER_FEEDBACK:
/* do emit fdbk stuff */
break;
/* and so on... */
}
Embedded Controllers
43
7.3 Looping
There are three looping constructs in C. They are while(), do-while(), and for(). do-while() is
really just a while() with the loop continuation test at the end instead of the beginning. Therefore, you
always get at least one iteration. The continuation test follows the same rules as the if() construct. Here
are the while() and do-while() templates:
while( test condition(s)... )
{
//...statements to iterate...
}
do {
//..statements to iterate...
} while( test condition(s)... )
Usually, loops use some form of counter. The obvious way to implement a counter is with a statement
like:
a=a+1;
C has increment and decrement operators, ++ and --, so you can say things like:
a++;
a--;
C also has a shortcut mode for most operations. Here are two examples:
a+=1; /* equivalent to a=a+1; or a++; */
a*=2; /* equivalent to a=a*2; */
44
Embedded Controllers
You will see all three forms of increment in example and production code, although the increment and
decrement operators are generally preferred.
The for() construct is generally preferred over the while() if a specific number of iterations are
known. The template is:
for( initialization(s); termination test(s); increment(s) )
{
..statements to iterate..
}
Here is an example using the variable a as a counter that starts at 0 and proceeds to 9 by adding one each
time. The loop iterates 10 times.
for( a=0; a<10; a++ )
{
/* stuff to do ten times */
}
The following example is similar, but adds 2 at each loop, thus iterating 5 times.
for( a=0; a<10; a+=2 )
{
/* stuff to do five times */
}
In this case two variables are initialized. Also, at each loop completion, a is incremented by 1 and b is
multiplied by 3. Note that b is not used in the termination section, although it could be.
If the iterated block within the braces consists of only a single statement, the braces may be left out (just
like in the if/else construct). Loops may be nested and contain any legal C statements including
assignments, conditionals, function calls and the like. They may also be terminated early through the use
of the break statement. As in the switch/case construct, the break command redirects program flow to
the closing brace. Here is an example:
for( a=0, b=2; a<7; a++ )
{
while( b<a*10 )
b*=2;
if( b > 50 )
break;
}
Embedded Controllers
45
Note that the if() is not part of the while(). This is visually reinforced by the use of indents and
spacing, but thats not what makes it so. The code would behave the same even if it was entered like so:
for( a=0, b=2; a<7; a++ ){ while( b<a*10 ) b*=2; if( b>50 ) break;}
Obviously, the former style is much easier to read than the later. It is strongly recommended that you
follow the first style when you write code.
OK, what does the code fragment do? First, it sets a to 0 and b to 2. Then, the while() checks to see if b
is less than 10 times a. 2 is not less than 0, so the while() doesnt iterate. Next, the if() checks to see
if b is more than 50. Its not, so the break isnt executed. That concludes the first iteration. For the
second iteration, a is incremented to 1 and checked to see if its still less than 7. It is, so the loop
continues and enters the while(). b is smaller than 10 times a (2<10), so b is doubled to 4. This is still
smaller so its doubled again to 4, and again to 8. Finally, it is doubled to 16. It is now larger than 10
times a so the while() loop exits. The if() isnt true as 16 is not larger than 50 so the break isnt
taken. We wind up finishing iteration two by incrementing a to 2. The while() loop starts because b
(16) is less than 10 times a (now 20). The loop will only execute once, leaving b at 32. This is still less
than 50, so the break is ignored. The for() closes by incrementing a to 3. On the next iteration both
the while() and if() are ignored as b is less than 10 times a as well less than 50. All that happens as
that a is incremented to 4. Now that a is 4, the while() starts again (32<40). b is doubled to 64. Thats
greater than 10 times a, so the while() exits. b is now greater than 50 so the if() is taken. This results
in executing the break statement that directs program flow to the closing brace of the for() loop.
Execution picks up at the line following the closing brace and we are all done with the for() loop (no, a
never gets to 7). This example is admittedly a little tricky to follow and not necessarily the best coding
practice, but it does illustrate how the various parts operate.
This code fragment doesnt print the word hello ten times, it prints hello forever (or better to say until you
forcibly terminate the program)! Although a was initialized and tested, it was never incremented. You
need an a++; (or similar) within that loop.
46
Embedded Controllers
7.5 Exercises
1. Write the code to examine the value of the variable X. If its less than zero, the message negative value
should be printed.
2. Write the code to examine the value of the variable X. If its equal to zero, the message zero value
should be printed.
3. Write the code to compare the values of the variables X and Y. The greater value should be printed.
4. Write the code to examine the values of the variables X and Y. If both X and Y are greater than zero,
increment X by one.
5. Write the code necessary to print the message Error! six times but without using six sequential
printf() calls.
6. Write the code required to control a loop so that it continues so long as the variable K is less than 50.
7. Write the code needed to cycle the variable R from 100 to 200 in steps of 5 (i.e., 100, 105, 110, etc.).
8. Explain the practical difference between a while loop and a do-while loop.
Embedded Controllers
47
then referencing a will get us the value stored in a, as in the code b=a;. Using the address of operator,
as in &a, will obtain the memory location of a, not as value or contents. This is the sort of thing we used
with scanf() and strcpy(). It is possible to declare variables that are designed to hold these addresses.
They are called pointers. To declare a pointer, you preface the variable with an asterisk like so:
char *pc;
The variable pc is not a char, it is a pointer to a char. That is, its contents are the address of a char
variable. The content of any pointer is an address. This is a very important point. Consider the following
code fragments based on the declarations above:
pc = a;
pc = &a;
/* unhappy */
/* just fine */
The first line makes no sense as we are trying to assign apples to oranges, so to speak. The second line
makes perfect sense as both pc and &a are the same sort of thing, namely the address of a variable that
holds a char. What if we want pointers to other kinds of things? No problem, just follow the examples
below:
float *pf;
long int *pl;
double *pd, *p2;
short int *ps, i;
/*
/*
/*
/*
/*
pointer to a float */
pointer to a long int */
two pointers to doubles */
ps is a pointer to a short int */
i is just a short int */
As mentioned, all pointers contain addresses. Therefore, no matter what the pointer points to, all
pointers are the same size (same number of bytes). In most modern systems, pointers are either 32 bits
(4 bytes) or 64 bits (8 bytes) although some small controllers use 16 bit addressing. When in doubt, you
can check your code with sizeof(). If all pointers are the same size, then why do we declare different
48
Embedded Controllers
types of pointers? There are two reasons. First, this helps with type checking. Functions that take pointers
as arguments or that return pointers will be using certain forms of data. You wouldnt want to accidentally
send off a pointer to a float when the function expects the address of a short int for example.
Second, by specifying the type of thing the pointer points to, we can rely on the compiler to generate
proper pointer math (more on this in a moment).
We have declared two variables, a char and a pointer to a char. We then set the contents of the char to
1, and set the contents of the pointer to the address of the char. We dont really need to know what this
address is, but for the sake of argument, lets say that c is located at memory address 2000 while pc is
located at memory address 3000. If we were to search the computers memory, at address 2000 we would
find the number 1. At address 3000, we would find the number 2000, that is, the address of the char
variable. In a typical system, this value could span 32 bits or 4 bytes. In other words, the memory used by
pc is addresses 3000, 3001, 3002, and 3003. Conversely, in a 16 bit system, pc would only span 3000
and 3001 (half as many bytes, but far few possible addresses).
As the contents of (i.e., value of) pc tell us where a char resides, we can get to that location, and thus the
value of the char variable c. To do this, we dereference the pointer with an asterisk. We could say:
b = *pc;
Read this as b gets the value at the address given by pc. That is, pc doesnt give us the value, rather, it
tells us where to go to get the value. Its as if you went to your professors office and asked for your
grade, and instead he hands you a piece of paper that reads I will e-mail it to you. The paper doesnt
indicate your grade, but it shows you where to find it. This might sound unduly complicated at first but it
turns out to have myriad uses. By the way, in this example the value of b will be 1 because pc points to a,
which was assigned the value 1 at the start. That is, b gets the value at the address pc points to, which is
simply a.
Embedded Controllers
49
somestring
Lets look at an example of how you might use pointers instead of normal array indexing. We shall write
our own version of strcpy() to copy one string to another. The function takes two arguments, the
address of a source string and the address of a destination string. We will copy over a character at a time
until we come to the null termination. First, the normal array index way:
void strcpy1( char dest[], char source[] )
{
int i=0;
/* index variable, init to first char */
while( source[i] != 0 )
{
dest[i] = source[i];
i++;
}
dest[i] = 0;
Looks pretty straight forward, right? There are some minor improvements you can make such as changing
the loop to while( source[i] ), but thats not a big deal. Now in contrast, lets write the same thing
using pointers.
void strcpy2( char *dest, char *source )
{
while( *dest++ = *source++ );
}
Thats it. Heres how it works. dest and source are the starting addresses of the strings. If you say:
*dest = *source;
then what happens is that the value that source points to gets copied to the address referred to by dest.
That copies one character. Now, to this we add the post increment operator ++:
*dest++ = *source++;
This line copies the first character as above and then increments each pointer. Thus, each pointer contains
the address of the next character in the array (youve got to love that pointer math, this will work with any
sized datum). By placing this code within the while() loop, if the content (i.e., the character copied) is
non-zero, the loop will continue. The loop wont stop until the terminating null has been copied. As you
can imagine, the underlying machine code for strcpy2() will be much simpler, more compact, and
faster to execute than that of strcpy1(). As was said at the outset of the course, you can do a lot in C
with just a little code!
50
Embedded Controllers
8.6 Exercises
1. Declare a pointer to a floating point variable, naming it fptr.
2. Declare a pointer to a signed character variable, naming it cptr.
3. Consider the following snippet of code:
unsigned char c, *p;
Assume that the value of p is currently 1000 and the value of p2 is 2000. What are their values after the
following piece of code is executed?
p++;
p2++;
5. Explain the difference between the * and & operators in relation to pointers.
6. Consider the line of code below.
a = b*c;
What do you think this line does? How might you alter this line to mark the intent more clearly and less
prone to error or misinterpretation?
8. Explain the difference between the two lines of code below.
a*=b;
a=*b;
Embedded Controllers
51
9. C Look-up Tables
9.1 Introduction
Sometimes we use tools to make things without thinking about how the tools themselves are made. In the
world of software, sometimes how things are done (the implementation) can have a huge impact on
performance. It turns out that sometimes you can trade performance in one area for another. For example,
a certain technique might be very memory efficient but rather slow, or vice versa. Were going to take a
look at a common programming technique that is very fast. Sometimes it can require a lot of memory,
sometimes not. Its called a look-up table.
0.05234,
You can now compute the sine like so, given the argument angle:
answer = sine_table[ angle ];
Because of the duality of arrays and pointers, you can also write this as:
answer = *(sine_table + angle);
Without an optimizing compiler, the second form will probably generate more efficient machine code. In
either case, this is very fast because its nothing more than reading from a memory location with an offset.
The result is just one add plus the access instead of a dozen multiply/adds. It does come at the cost of 360
double floats, which, at eight bytes each, is nearly 3k of memory. By recognizing that the sine function
has quarter wave symmetry, you can add a little code to check for the quadrant and reduce the table to just
90 entries. Also, floats might have sufficient precision for the application which will cut the memory
requirements in half again when compared to doubles.
Outside those bounds you can always perform some minor integer math to get within the bounds (e.g., if the angle
is 370, just mod by 360 to get the remainder of 10 degrees, effectively the wrap around).
52
Embedded Controllers
To make the above just a little spiffier, you can always make it look like a function via a #define as
follows:
#define fast_sin(a)
(*(sine_table+(a)))
Of course, a down side to this operation is that it only deals with an integer argument and is only accurate
to a single degree. You can alter the definition to allow a floating point argument and round it to the
nearest whole degree as follows:
#define fast_sin(a)
(*(sine_table+(int)((a)+0.5)))
You could also turn this into a true function and add code to interpolate between adjacent whole degree
entries for a more accurate result. At some point the extra refinements will slow the function to the point
where a more direct computation becomes competitive.
These values could then be sent sequentially to a digital-to-analog converter (DAC) to create the desired
waveform. Once we get to the end of the table, we simply loop back to the start to make the next cycle.
With a 256 entry table, we can use an unsigned char as the table index and once it reaches 255,
incrementing it will cause it to roll over back to 0, automatically. The post increment operator is ideal for
this. For the code below, assume PORT is the memory location of the DAC we are writing to.
unsigned char i = 0;
// loop forever
while ( 1 )
{
PORT = ramp_table[i++];
// now wait between each value, dependent on sample rate
delay();
}
Embedded Controllers
53
This is a very fast process and as accurate as your calibration measurements. As long as the sensor data is
repeatable (e.g., it always reads 145C in a 150C oven), youll get good results.
54
Embedded Controllers
Embedded Controllers
55
10. C Structures
10.1 Introduction
C allows compound data called structures, or struct for short. The idea is to use a variety of the basic
data types such as float or int to describe some sort of object. Structures may contain several of each
type along with pointers, arrays, and even other structures. There are many uses for such a construct and
structures are very common in production C code.
As an example, we may wish to describe an electronic component such as a transistor. What sort of things
do we need? There are several performance parameters that may be used such as current gain, breakdown
voltage and maximum power dissipation. All of these items may be represented as double variables.
There will be a model number. This will probably be a string as it may contain letters (such as
2N3904). There will need to be a manufacturers code. This could be an int. A real world device will
have many more parameters than these five, but these will suffice for our purposes. If you only have one
transistor to deal with, five separate variables is not a big deal to keep track of. On the other hand, what if
you have a great number of parts as in a database? Perhaps there are 1000 devices. Creating 5000 separate
variables and keeping them straight presents a bit of a challenge. It would be nice if we could combine the
five items together into a super variable. Then, all we have to worry about is creating 1000 of them for
the database (perhaps with an array, although there are other techniques). There shouldnt be a problem of
getting the current gain of one device confused with that of another. This is where structures come in.
Below is an example of how we would define this transistor structure and associated instances.
struct transistor
double
double
double
short int
char
};
{
currentgain;
breakdown;
maxpower;
manufacturer;
model[20];
We have defined a structure of type transistor. We have also declared an instance of a struct
transistor called my_transistor, along with a pointer to a struct transistor called
ptransistor. The five elements are referred to as the fields of the structure (e.g., the currentgain
field). Note that this structure contains an array of characters for the model name/number. The model
cannot exceed 19 characters (19 plus terminating null yields 20 declared). It is unlikely that well ever
have model name/number this long, but if by chance we do, we will have to truncate it.
To set or retrieve values from an instance, we use a period to separate the structure name from the field of
interest. Here are some examples:
my_transistor.currentgain = 200.0;
my_transistor.maxpower = 50.0;
my_transistor.manufacturer = 23;
56
Embedded Controllers
In the last assignment, it may be better to use a #define rather than a hard number. For example, place
the following definition in a header file and then use the assignment below:
#define MOTOROLA 23
my_transistor.manufacturer = MOTOROLA;
Remember, strcpy() needs addresses. The double quote string literal produces this automatically. For
the model field, we are using the shortcut described in earlier work. The line above is equivalent to:
strcpy( &(my_transistor.model[0]), 2N3904 );
A good question at this point is whether of not the declared order of the fields makes any difference. This
depends on the compiler and target hardware. In some processors, multiple-byte variables such as long
and short integers, floats and pointers must be word aligned. For example, a short int may be required to
start on an even address or a float might be required to start on an address divisible by four. In such a
system, a structure declared with the order of char, float, char, int will need pad bytes between some fields
to ensure alignment and will take up more memory space than if the structure was organized as float, int,
char, char. This is of particular importance if large arrays of structures are to be used.
To access the various fields, we can longer use the period because we no longer have a struct
transistor; we have a pointer to one. For pointers, we access the fields via the pointer token, which is
made up of a dash followed by a greater than sign: -> Thus, we might say:
ptransistor->currentgain = 200.0;
strcpy( ptransistor->model, 2N3904 );
Embedded Controllers
57
Below is a function that simply prints out the values of the various fields.
void print_transistor( struct transistor *pt )
{
printf(For model: %s\n, pt->model );
printf(Current gain is %lf\n, pt->currentgain );
printf(Breakdown voltage is %lf\n, pt->breakdown );
printf(Maximum power is %lf\n, pt->maxpower );
}
/* note use of %s for string and %lf for long float i.e., double */
variable declarations */
foo my_foo;
bar my_bar;
bar *pbar = &my_bar;
z=1.0;
The bar structure contains a pointer to a double, a pointer to struct foo, and a struct foo. We
would access them as follows:
my_bar.pd = &z; /* pd isnt a double but the address of one, hence & */
my_bar.littlefoo.x = 2.2;
pbar->littlefoo.y = 3.3;
pbar->pf = &my_foo;
pbar->pf->x = 4.4;
Note that if you didnt say pbar->pf = &my_foo; first, then pbar->pf->x = 4.4; would be very
evil! Without assigning my_foo to pf, this pointer would contain some random number. The second
statement would then use that number as the starting address of struct foo, and write the number 4.4
58
Embedded Controllers
where the x field should be. As its highly unlikely that this random number is the starting address of a
struct foo, the number 4.4 overwrites something else. That might mean other data or even code gets
destroyed. Your program behaves erratically or crashes.
Pointer Rule Number One: Never dereference7 an uninitialized pointer!
Only bad, evil things will happen and you will become a very sad programmer.
At the beginning of the transistor example we noted that we might want to create a bunch of transistors.
One possibility is to use an array. There are other ways, as we shall see. Heres how youd declare an
array of 1000 transistor structures, given the definition above:
struct transistor transistors[1000];
Note that we do not have 1000 transistor structures, but rather 1000 pointers. Each of these would need to
point to an appropriate transistor structure. Assuming you had declared one named my_transistor as
we did earlier, you could write:
ptarray[0] = &my_transistor;
Although this may look a little odd at first, this sort of construct does have some good uses in more
advanced applications. To stretch your mind just a teensy bit further, C makes it possible to create
something like an array of pointers to structures which contain a structure which in turn contains an array
of pointers to still other structures. Read that again, imagine what that might look like as a memory map,
and then write some possible definitions/declarations. If you can do that, youve pretty well mastered the
idea.
10.4 Exercises
1. Declare a structure of type Quest called Grail that contains a float called X, a long integer called Y
and an unsigned character called Z.
2. Given the structure of problem one, will the order of the three fields have any effect or importance?
How might we determine if it does?
Embedded Controllers
59
ptr=&B
ptr=&C
ptr=&D
ptr=0
Imagine that each structure is 100 bytes in size. Further, assume that A is located at memory address 1000
(spanning 1000 to 1099), B is a located at address 2000, C at 3000, and D at 4000. Each structure contains
a pointer to the next one in the list. A contains a pointer to the address of B, B to C, and so forth. For
example, the value of As pointer is 2000 (the memory address of B), while the value of Cs pointer is
4000 (the address of D).
Note that &A is the top of the list, and that the last item signifies that no items remain by having its pointer
set to 0. Further, these four items could be linked in any manner by simply changing the pointer values.
Finally, items A, B, C, and D may reside anywhere in the memory map; they need not be contiguous. In
contrast, if we were to make an array out of these structures, they would need to be packed into memory
in sequence in order to be indexed properly. That is, if A were located at address 1000, B would have to be
at 1100, C at 1200, and D at 1300, as they are each 100 bytes in size8. If you wanted to rearrange the
structures (e.g., to sort them), add new ones or delete existing ones, this would require quite a bit of work
Array indexing works by simply multiplying the index value by the size of the arrayed item, and then using this
value as an offset from the starting address. Thus, in this example, item[2] (i.e., C) is found by multiplying the index
of 2 by the size of 100 bytes to achieve a 200 offset from the starting location of address 1000, or 1200. Remember,
item[0] is A, while item[1] is B, and item[2] is C.
60
Embedded Controllers
with an array. These tasks are much easier with a linked list because the structures themselves arent
manipulated, only the associated pointers.
We could declare three instances of Marmots and link them together as follows:
struct Marmot Larry = { 0, 3.4, 19.7 };
struct Marmot Jane = { &Larry, 2.5, 13.1 };
struct Marmot Felix = { &Jane, 2.9, 15.6 };
Felix is at the top of the list, while Larry is at the bottom. Note that the items must be declared in
inverse order since the pointer reference must exist prior to assignment (i.e., Jane must exist in order for
Felix to use Jane's address). It is common to also declare a pointer to use as the head of the list. Thus,
The final line of pointers to pointers is not very practical. To get around this, we can use a temporary
pointer. Below is an example function that takes the head of a Marmot list as its argument, and then prints
out the ages of all of the Marmots in the list.
void PrintMarmotAges( struct Marmot *top )
{
struct Marmot *temp;
temp = top; /* initialize pointer to top of list */
while( temp ) /* true only if marmot exists */
{
printf( "%f\n", temp->Age );
temp = temp->NextMarmot );
}
}
Embedded Controllers
61
Note that we could've reused top rather than use the local temp in this case. If the head of the list will be
needed for something else in the function though, then the local variable will be required (i.e., since temp
= temp->NextMarmot effectively erases the prior value of temp, we lose the head of the list as we
walk down the list).
11.3 Exercise
A bipolar transistor can be described (partially) with the following information: A part number (such as
"2N3904"), a typical beta, and maximum ratings for Pd, Ic, and BVceo. Using the data below, create a
program that would allow the user to search for devices that meet a minimum specified requirement for
beta, Pd, Ic, or BVceo. Devices that meet the performance spec would be printed out in a table (all data
fields shown). If no devices meet the spec, an appropriate message should be printed instead. For
example, a user could search for devices that have a Pd of at least 25 watts. All devices with Pd >= 25.0
would be printed out.
Device
Beta
Pd(W)
Ic(A)
BVceo(V)
2N3904
2N2202
2N3055
2N1013
MPE106
MC1301
ECG1201
150
120
60
95
140
80
130
.35
.5
120
50
15
10
1.3
.2
.3
10
4
1.5
.9
1.1
40
35
90
110
35
200
55
62
Embedded Controllers
Embedded Controllers
63
12. C Memory
12.1 Introduction
Up until now, whenever we have needed variables we simply declared them, either globally or locally.
There are times, however, when this approach is not practical. Consider a program that must deal with a
large amount of data from external files such as a word processor, or graphics or sound editor. In all
instances the application may need to open very large files, sometimes many megabytes in size. The issue
is not merely the size. After all, you could declare a very large array, or perhaps several of them. The
problem is that the data is both large and variable in size. For example, you might edit a sound file thats
100k bytes in size, but you might also need to edit one thats 100 times larger. It would not be wise to
declare a 10 megabyte array when you only need 100k. Further, you can guarantee that if you do declare
10 megabytes, the day will come when youll need 11 megabytes. What is needed is some way of
dynamically allocating memory of the size needed, when needed.
pointer can be thought of as a generic, one-size-fits-all pointer. It prevents possible type size clashes. You
can assign a void pointer to another type of pointer and not get a type mismatch. If the memory request
cannot be made (not enough memory) then the functions will return NULL. Always check for the NULL
return! Never assume that the allocation will work!
64
Embedded Controllers
If you want to obtain space for 100 bytes, youd do something like this:
char *cp;
cp = malloc( 100 );
if( cp )
{
/* memory allocated, do stuff... */
}
else
{
/* not allocated, warn user and fail gracefully... */
}
If you need space for 200 doubles, youd do something like this:
double *dp;
if( dp = calloc( 200, sizeof(double) ) ) /* assign and if test in 1 */
{
/* memory allocated, do stuff... */
}
else
{
/* not allocated, warn user and fail gracefully... */
}
Note the use of the sizeof() operator above. If you had a structure and needed to create one (for
example, to add to a linked list), you might do this:
struct foo *fp;
if( fp = calloc( 1, sizeof(struct foo) ) )
{
/* remainder as above ... */
Embedded Controllers
65
Note the freedom that we have with the pointer. It can be used as a normal pointer or thought of as the
base of an array and indexed accordingly. Similarly, we might need to allocate a structure and initialize its
fields. Here is a function that we can call to allocate a struct foobar, initialize some fields, and return
a pointer to it.
struct foobar {
double d;
int i;
char name[20];
};
/* other code... */
struct foobar * alloc_foobar( void )
{
struct foobar *fp;
if( fp = malloc( sizeof(struct foobar) ) )
{
fp->d = 12.0;
/* just some stuff to show how... */
fp->i = 17;
strcpy( fp->name, Timmy );
}
return( fp );
}
p is the pointer that you initially received from either malloc() or calloc(). The return value of the
free() function is 0 for success or -1 on error. Normally this function never fails if it is given a valid
pointer. If it does fail, there is little that you can do about it (at least not at this level). Remember: Every
block that you allocate eventually must be freed! You might wonder why the free() function does
not need to know the size of the block to free. This is because along with the memory they pass to you,
malloc() and calloc() actually allocate a little bit more for use by the operating system. In this extra
memory that you dont see are stored items such as the size of the block. This saves you a little house
keeping work.
66
Embedded Controllers
12.7 Exercises
1. Write the code to allocate 1000 bytes of memory.
2. Write the code to allocate space for an array of 500 single precision floating point values.
3. Write the code to free the memory allocated in problems one and two.
Embedded Controllers
67
13. C File IO
13.1 Introduction
High level fileio in C uses functions such as fopen(), fclose(), fread(), fwrite, fprintf(),
fgetc(), and so on. These utilize a variable of type FILE to access disk files. They allow you to read
and write data from/to files on disk in a manner very similar to sending data to the computer screen via
printf() or retrieving data from the keyboard via scanf().
Closer to home, we have low level fileio. These use a file descriptor, which is basically just an integer.
High level functions actually call the low level functions anyway. There are five things you need to do
with files. First, you open them to obtain access to an existing file or to create a new one. Next, you read
or write data from/to the file. You may also need to move around in a file, perhaps to reread data or to
skip over data. Finally, when you are finished using the file, you must close it. To open a file use:
fh = open( name, mode );
where
char *name: /* disk name of file */
int fh:
/* file descriptor */
int mode;
/* a define */
fh is the file descriptor that is needed for subsequent read/write calls. It will be >= O if all is OK, -1 on
error.
Example modes:
O_RDONLY read only
O_WRONLY write only
O_CREAT create if not exists
68
Embedded Controllers
where
long apos:
long rpos:
mode:
C allows you to have multiple files open simultaneously for reading and writing. You will need one file
descriptor for each file open at the same time (they can be reused later, if desired).
Below is a program that can be used to investigate the contents of almost any kind of file. It uses a variety
of techniques that we have examined through the course. Some lines have wrapped.
/*
headdump.c
This program spits out the first 128 bytes of a file in hex, decimal, and
string form (non printable chars are printed out as periods). */
#include
#include
#include
#include
<stdio.h>
<stdlib.h>
<string.h>
<ctype.h>
Embedded Controllers
69
70
Embedded Controllers
Embedded Controllers
71
The program is called archive (archive.exe), and you're telling it to compress the file foo.txt
and create a new file called foo.arc. This is much faster than using scanf() type input from within
the program (i.e., having the user run the program, at which point the program then prompts for the two
file names). C allows a very simple method of obtaining these command line arguments. This requires a
modification to the declaration of main():
void main( int argc, char *argv[] )
The first parameter is called the argument count and tells you how many items (strings) where entered on
the command line. In the example above, argc would be 3. The second parameter is called the argument
vector. It is an array of pointers to strings. These strings are the items entered on the command line. Thus
argv[0] points to "archive", argv[l] points to "foo.txt", and argv[2] points to foo.arc".
If argc is 1, then no arguments were added after the executable name. Below is a simple echo
example. This will echo whatever was typed in on the command line.
void main( int argc, char *argv[] )
{
int x;
for( x=0; x<argc; x++ )
printf( "Argument %d is %s\n", x, argv[x] );
}
Note that since argv is an array of pointers, then argv[x] is also a pointer, and is treated just like any
other pointer to characters. Thus, you can use functions like strcpy() or strcmp() on it. For numeric
arguments (such as "archive blah 23"), you may convert the ASCII string ("23") into either an integer,
long integer, or float via the atoi(), atol(), and atof() functions, respectively.
Other possibilities include printing out directions if argc is 1 or if the only added argument is ?. It is
possible to take this further by adding switches or flags to the argument list so that arguments can be
presented in any order. This is precisely what is done if the compiler or linker is run directly rather than
via the IDE.
In summation, command line arguments are a very handy and quick way of getting values into a program.
As an exercise, alter one of your previous programs to utilize command line arguments rather than the
scanf() approach.
72
Embedded Controllers
The #if/#else/#endif directives act similarly to the if/else commands. Note that parentheses are
not used to block multi-line sections (hence the need for the #endif directive). In the example above, the
char array title is initialized to "This is version A". If we commented out the #define VERSION
A line, then title would be initialized to "This is some other version". In some IDEs it is
possible to create variations on projects. Within each project you can define certain symbols (such as
VERSION_A). This way you wouldnt even have to comment/uncomment the #define, but just select the
desired project variation. Note that it is possible to nest #if/else directives. Further, you are not
limited to simply altering global declarations. This technique can be used on code as well as data. In fact,
entire functions may be added or excluded in this way. Here is another typical use:
void some func( void )
{
//...code stuff...
#ifdef DEBUG
printf("Error in some_func, x=%d\n", x );
#endif
//...rest of function...
}
If DEBUG is defined, the printf() call will be included in the executable. If it is not defined, it is as if
the printf() call never existed.
14.3 Exercise
Add DEBUG printf()statements to any of your existing programs. Compile with and without the
DEBUG directive.
Embedded Controllers
73
15.2 Input/Output
Consider a typical embedded application such as a programmable or intelligent thermostat. Unlike a
normal electro-mechanical thermostat, these devices allow the home owner to automatically change
temperature at different times of the day in order to save energy. After all, why have the heat or air
conditioner running when no ones home? Certainly, these devices do not come with a monitor or
keyboard. In their place may be a small LCD display with a few fixed messages and a two digit numeric
display for the temperature. For input there may be as few as two or three buttons for programming (set
item plus up and down). By comparison, a microwave oven will probably have a complete numeric
keypad with a series of special function buttons along with multiple seven-segment displays, or possibly
several alpha-numeric displays for short messages. In any case, these devices are far different from the
standard desktop computer. Consequently, a programmers approach to input and output processing will
be much different in the embedded case.
To start with, it is unlikely that there will be printf() and scanf() style functions. They are largely
worthless in this world. What use would printf() be if all you have for output hardware is a bunch of
LEDs? For input, you often need to read the state of switches, pushbuttons, and possibly some form of
level control such as a rotary knob. For output, you often need to simply light an LED or set a value on a
seven-segment display. For fixed style messages, these also need only a single signal to turn them on or
off, such as an LED. In more advanced applications, a multi-line alphanumeric display may be available
so setting individual letters is a possibility. In almost all cases these chores are handled by setting or
clearing bits on specific output or inputs ports on the microcontroller. Some ports may be set up as a byte
or word. Further, some ports may be bi-directional, meaning that they can behave as either input or output
depending on some other register setting. Ports are little more than pins on the microcontroller that are
hooked up to external circuitry. Thus, if a port is connected to an LED circuit, setting the output of the
port HIGH could light the LED while setting the port LOW could turn off the LED. The obvious question
then is How do you read from or write to a port? In many cases ports will be memory mapped. That is, a
specific address in the memory map is allocated to a given port. You read and write from/to it just like
any other variable. Further, development systems sometimes disguise these addresses as pre-defined
global variables. They might also include a library of specific routines to utilize the ports. Thus setting a
certain port (lets call it the A port) to a high might be as simple as PORT_A = 1; or set_portA(1);.
Reading from a port might be something like a = PORT_A; or a = get_portA();. Consequently,
embedded code is often all about reading and writing to/from ports and then branching to the requested
chores.
74
Embedded Controllers
There are some tricks to this. For example, how do you know if a key has been pressed? Calling
get_portA() tells you the state of the switch connected to port A at the instant you call it. There is no
history here if this is a simple momentary pushbutton instead of a toggle switch. In a case like this you
might poll the port waiting for something to happen. This involves using a simple loop and repeatedly
reading the port state in the loop. The code breaks out when the state changes:
while( get_portA() );
This will keep looping until get_portA() returns 0. Of course if you need to monitor several ports as is
typical, youll need to read a value from each for the test. This form of monitoring while waiting for
something to happen is called an event loop. It may not be evident, but your house and car are probably
filled with devices running event loops, just waiting for you to do something! These loops execute fairly
fast so a time lag between your push and the resulting action is not noticed. On the output end, a port
normally stays at the value you set it, so there is no need for a loop to keep it set.
For more complicated displays such as a seven segment or alpha-numeric device, you may need to create
a table of values indicating bit patterns for each numeral or letter to be displayed. These patterns, or
words, would then be sent out various ports that are in turn connected to the displays.
For variable input devices such as a volume control, the external device (knob, slider, sensor, etc.) will be
connected to an analog to digital converter or ADC. This might be a separate circuit or the controller may
have these built-in. The converter turns the analog voltage into a numeric value that you can read from a
port. For example, a volume control may be just a potentiometer connected to a fixed voltage. As the
knob is moved, the voltage at the wiper arm will change. The A/D converter may encode this into a single
byte. A byte ranges from 0 to 255 in value. Thus, if the volume is at maximum, the port will read 255. If
the volume is at halfway the port will read about 127, and finally 0 when the volume is off.
15.3 Math
Usually, embedded code is not math intensive. There are some exceptions to this rule, but generally code
for a microwave oven doesnt need something like a cosine function. Many embedded systems do not
have or need floating point math. All math operations are performed using integers. Look-up tables may
be used to speed processing in some cases. You will sometimes hear of fixed point math versus floating
point. This is a fairly simple idea. Suppose you need to work with variables to the precision of tenths but
you only have integers. Simply treat your variables as having an unseen decimal point after the first digit
and think of all values as being in tenths. Thus, the number 17.3 would be stored and manipulated as 173.
If the result of some calculation is say, 2546, then you know the real answer is 254.6.
Embedded Controllers
75
synchronization to some external hardware. You can do this with a simple loop that iterates a specific
number of times:
for( c=0; c<1000; c++ );
This loop does nothing but count, but each count will require a certain number of clock cycles from the
microcontroller, and thus a specific time. These are usually determined experimentally. You could sit
down with the processor manuals and figure out how long a loop will take, but its usually easier to just
write the thing and try a few values. The result will depend on the specific microcontroller used as well as
its clock frequency.
76
Embedded Controllers
Embedded Controllers
77
78
Embedded Controllers
from program memory. RISC processors are used in everything from simple embedded applications to
cell phones to supercomputers. Two examples of embedded RISC processors are the Atmel AVR and the
ARM. These can be found in applications such as tablet devices, smart phones, game consoles, game
controllers and automotive controllers. The AVR is the core of the ATmega processors used in most
Arduino controller boards. We shall examine the Arduino system in the following chapters.
79
The first thing to notice is that most of the blocks are interconnected via an eight bit data bus (i.e., a
collection of parallel connections, in this case, eight of them). The data bus is bidirectional meaning that
data can be placed onto it (written) by one block and pulled off (read) by another. Obviously, some form
of traffic control needs to be applied and that is usually handled by appropriate timing signals and tristate buffers9.
The ALU, or Arithmetic Logic Unit, is responsible for basic computational functions such as integer
addition and subtraction, bit operations, comparisons and the like. In association with this are 32 eight bit
general purpose registers. The ALU performs operations on the values in the registers, not directly on
values in general memory. For example, suppose you want to add two variables and place the result in a
third variable. The ALU has to transfer values from memory to the registers where the computation is
performed and then transfer the result back to the final location. A large number of registers is a common
feature of RISC processors. Early CISC processors had very few registers (indeed, many had a single
accumulator for these sorts of operations). There are also many specialized registers not shown here as
they are associated with IO modules, timers and so forth (more on this in a moment). Three important
specialized registers are the status register, program counter and stack pointer. The program counter (PC)
keeps track of the currently executing instruction (i.e., the current location of the code flow). The stack
pointer (SP) records the current memory address of the top of the stack (i.e., the location in memory used
for temporary variables and such). The status register (SR) contains a series of bits reflecting the most
recent results of an ALU operation and the general state of the processor. The AVR status register
contains eight bits, all of which are read/write and initialized at 0:
Bit
Function
7
I
6
T
5
H
4
S
3
V
2
N
1
Z
0
C
A tri-state buffer simply repeats its input signal at its output like an ordinary buffer but also offers a third high Z
state. When the high Z state is enabled, no output signal is present and the output impedance of the buffer goes to a
very high value, effectively disconnecting the buffer from the bus.
9
80
Embedded Controllers
It is important to remember that the state of the status register is not saved when entering an interrupt
routine or restored when returning from an interrupt. This must be handled in software. We shall see how
this is handled in future chapters.
16.3 Memory
Another important aspect is memory. Note that the AVR contains several different kinds of memory
including flash program memory, static data memory or SRAM (Static Random Access Memory) and
EEPROM (Electrically Erasable Programmable Read-Only Memory). Typical embedded applications run
a single program repeatedly. This program must run from the moment the device is turned on until it is
turned off. In many applications, this program is never updated (a microwave oven, perhaps). In other
applications, it may be updated but only by qualified technicians, not by the consumer (for example, an
automotive engine management system). Consequently, the program memory needs to be non-volatile,
that is, it must be able to survive without power. Historically, this was done via ROM (Read-Only
Memory) or PROM (Programmable Read-Only Memory). In both of these, individual bits can be set or
cleared through the use of fuse/anti-fuse links. The difference is that ROMs are programmed at the time
of manufacture while PROMs are programmed after manufacture. Both are permanent and cannot be
reprogrammed. ROM is less expensive for large production runs but more expensive in small quantities.
EEPROM has the advantage of being erasable and thus is a form of non-volatile read/write storage. Flash
RAM is similar but is much less expensive. It has the downside that it must be programmed in blocks
whereas EEPROM is byte-programmable. Therefore, Flash RAM (also called NVRAM) is ideal as a
storage medium for the main program and EEPROM is useful as a medium for storing data that must
survive a power cycle. A possible example use of EEPROM would involve saving user preference
settings for a digital camera. When power is turned off and back on, the user expects the device to be as
they left it, not reverting back to a default state. Typically, special instructions or procedures are needed
to write to or read from EEPROM.
In contrast to Flash RAM and EEPROM, SRAM is volatile, just like the common DRAM (Dynamic
RAM) variants found in personal computers. Static RAM typically is made of six transistors arranged as a
flip-flop for each bit of storage. DRAM, by comparison, typically consists of a single transistor-capacitor
combination per bit. SRAM is less dense and more expensive per bit than DRAM but it is very fast and
does not need to refreshed (the charge in a DRAM cell will leak over time requiring the cell to be
refreshed at regular intervals). As a result, SRAM is used for general purpose registers and special
purpose/IO registers. The ATmega 328P used in the Arduino Uno development board features 32k bytes
Embedded Controllers
81
of Flash RAM, 1k of EEPROM and 2k of SRAM. The Uno is detailed in the following chapter. By
comparison, the ATmega 48A consists of 4k Flash, 256 bytes of EEPROM and 512 bytes of SRAM.
Clearly, these are very modest memory footprints when compared to personal computers yet they are
sufficient to power a wide range of applications. The 328P is by no means at the top of the heap, so to
speak. Other controllers boast megabytes of addressable memory. There is, however, no need to overspecify a controller for a given embedded application. Extra memory that goes unused will not make the
application faster; it will merely lead to more expensive hardware.
Continuing on, all of the registers in the AVR core are memory mapped, that is, they are tied to specific
addresses in the SRAM. For example, the high (ADCH) and low (ADCL) bytes of the 328Ps analog to
digital converter output are found at addresses 0x79 and 0x78. Similarly, the output of port B (PORTB) is
found at address 0x25 while the data direction register of port B (DDRB) is found at 0x24. A detailed
table of all registers is found in the Atmel 2014 documentation under Register Summary. A portion of
these are available in the Appendix.
Finally, a series of other blocks such as the interrupt unit and the IO modules complete the design. We
shall take a much closer look at these blocks in the upcoming chapters.
82
Embedded Controllers
Embedded Controllers
83
84
Embedded Controllers
and deadly. Arduino clasped his hands together and smiled. Trouble? No trouble! Youve come to the
right place. Tell me more about this trouble. Well, Ive got to control a bunch of devices; LEDs,
motors, actuators, the usual crew, she started, and Ive got to obtain some information from the person
running the system, you know the kind, a real operator. Lots of settings; pushbuttons, switches,
potentiometers, the whole megillah. I tell ya, Im in over my head.
Arduino walked across the office and nibbled at some biscotti. He turned back to her, the twice-baked
biscuit still at his fingertips, held softly the way an orchestra conductor might balance a baton. This
application he said, his head slightly raised and cocked to one side revealing the beginning of a
knowing smile, it needs to be flexible, expandable and inexpensive, too? He had piqued the dames
interest but she couldnt let it show. Yes. Yes it does, she responded coolly. In that case, said
Arduino, allow me to introduce Uno, the One. He tossed the remnants of the biscotti to Rick and picked
up a small blue printed circuit board containing a few ICs, a bunch of headers with numbered pins and
what looked like USB and power connectors.
Pretty spiffy, she said. What is it? Arduinos eyes lit up. Its an open source microcontroller
development board, and when I say open source I mean both software and hardware. The software
distribution includes extensive examples, complete library source code, and all the data you could want
on the AVR. Whats an AVR? came the dames reply. AVR is a line of microcontrollers from Atmel.
The Uno uses an ATmega 328P with 32k of on-chip programmable flash memory, 2k of SRAM data
memory and 1k of EEPROM. 32 general purpose registers. Pipelined Reduced Instruction Set Computer
with Harvard architecture. Most instructions require only one tick of the 16 MHz clock. Arduinos
concentration was broken by a hissing sound coming from behind a large stack of books. Ahhh! Can I
interest either of you in a cup of espresso? he asked. Never after lunch, said Rick. He knew Arduinos
hyper-caffeinated concoction would have him bouncing off the walls all afternoon. And you, Miss
Seesi? Arduino queried. Maybe just this once, she relied, with milk. Arduino smiled. Cappuccino it
is!
Embedded Controllers
85
Sipping the steaming beverage, she looked over the board intently. The labeled port pins on the headers
are convenient- seems pretty small though. Are you sure it can handle the job? she asked. No problem!
said Arduino. He pulled out a couple of slightly crumpled and dog-eared pieces of paper from under a
stack of CD-ROMs and placed them on the desk. First was a block diagram of the 328P as used on the
Uno, the second a more detailed schematic of the board:
86
Embedded Controllers
Embedded Controllers
87
Youve got access to three IO ports, he began, B, C and D. Digital IO with programmable pull-down
resistor. All IO is memory mapped so you can read from and write to external devices as if they were just
ordinary variables using the C programming language. A Universal Synchronous-Asynchronous
Receiver-Transmitter, or USART, already programmed to communicate with a host computer via USB.
She interrupted him. So I can send text back and forth between the two? That would make debugging
pretty easy. You bet! said Arduino, obviously getting excited. Not only that, but youve got six 10 bit
analog to digital converter channels and three timer-counters; two eight bit and one 16 bit. Plus, six
outputs can generate pulse width modulation signals. What about interrupts? she asked. Ill need
interrupts. Multiple levels, said Arduino, internal and external. Theres a reset button already on the
board.
What about power? she asked. Multiple options, said Arduino. You can power it from the USB
cable. You know, USB will supply up to 100 mA to an un-enumerated device and up to 500 mA to an
enumerated device. If thats not enough, you can also plug in an external supply, the wall-wart kind, or
even hook in a regulated five volt source so you can supply power to relays, motors, whatever. Youd
power the big wire items off-board, of course. No need to run those currents through the board traces.
Whatever you choose, though, the Uno will intelligently figure out where to get its power from.
The dame was getting more interested and needed further detail. Great, but what will the chip deliver,
you know, fan out or drive. Generally speaking, Arduino began, the IO pins can sink or source up to
40 mA each. The entire microcontroller should be limited to 200 mA total draw to stay within thermal
limits so you obviously cant drive a whole bunch of pins to maximum current capability at the same
time. No big deal, thats why they make drive circuits, right? Rick and the dame both nodded knowingly.
The office was silent as the three of them peered at the little board. The dame looked up. What about the
programming interface? Command line or IDE? Arduino waved his hand and responded, The IDE runs
under Windows, Mac OS and Linux. You can bypass that if you want and go command line, but
Fine, fine, she interrupted, and this library, its a straight jacket, right? No, no! insisted Arduino.
Its a nice library and you can use as much or as little of it as you want. Even insert assembly op codes.
Assembly op codes. The thought sent a shiver down Ricks spine. He had spent a month one day trying to
debug a device driver written in 8086 assembly. Never again.
It seemed that Arduino was holding something back. Sounds good but youre not giving me the whole
story are you? she asked him. Whats the catch? No catch, replied Arduino, but theres one thing.
One small thing that sometimes bites the beginners. The dame raised her eyebrows and demanded,
And?
Like most controllers, Arduino began, the AVR uses a memory mapped IO address for writing to a
port. For example, you might write to PORTB to light an LED. So? the dame responded, Thats not
odd. Very true, said Arduino, but when it comes to reading from those same physical pins, they use a
different address, in this case PINB, instead of reusing PORTB.
Wait, stammered the dame, its the same physical pin and if I write to it, I use PORTB but if I read
from it I use PINB?
Dont confuse pins and ports, said Arduino as a look of melancholy crept across his face. You must
remember this, a port is just a port, a pin is just a pin. The fundamental things apply, as the clock ticks
by. The dame was shaken. But I she started. Look, you gotta get the IO straight, understand? said
Arduino. His expression grew serious and he looked at her squarely. You gotta get it plain or youll
regret it. Maybe not today, maybe not tomorrow, but soon and for the rest of your life! Her hand brushed
lightly across the Uno as she turned, and wiping a tear from her eye, she headed out the office door. Rick
88
Embedded Controllers
watched as she walked briskly down the hall. Some distance past the lab her silhouette disappeared into a
fog that had mysteriously formed out of nowhere accompanied by the dull roar of a DC3s idling engines.
Shes got the Uno, said Rick. Arduino nodded, Yes, I know. Ive got plenty more where that one came
from. All I need is a good lab tech to help me build more prototypes for Project Falcon using them. You
interested? A wry smile grew on Ricks face. This Falcon, is it Maltese? Why yes, yes it is came the
response. Arduino, Rick said, I think this is the beginning of a beautiful friendship.11
11
Embedded Controllers
89
<avr/pgmspace.h>
<avr/io.h>
<avr/interrupt.h>
"binary.h"
12
Complete details on the library and lots of other goodies including example code can be found at www.arduino.cc.
The Reference page in particular will prove useful.
90
Embedded Controllers
#define
#define
#define
#define
HIGH
LOW
true
false
0x1
0x0
0x1
0x0
#define INPUT
0x0
#define OUTPUT
0x1
#define INPUT_PULLUP 0x2
#define
#define
#define
#define
#define
PI
3.1415926535897932384626433832795
HALF_PI
1.5707963267948966192313216916398
TWO_PI
6.283185307179586476925286766559
DEG_TO_RAD 0.017453292519943295769236907684886
RAD_TO_DEG 57.295779513082320876798154814105
The first two lines prevent accidental re-entry. That is, if the file has already been included it wont be
included again (if it was, youd get a whole bunch of redefinition errors). After this we see a selection of
other commonly used library header files and then a series of constant definitions. Theres nothing too
crazy yet. Following this are a series of what look like functions but which are inline expansions. That is,
the preprocessor replaces your function call with a different bit of code. This is done because inline
expansions do not incur the overhead of function calls and thus run faster and use less memory. Notice
how some of these make use of the ternary if/else construct, such as min(), while others make use of
recently defined constants (radians()). Some, such as noInterruprs(), perform a substitution which
itself will perform a further substitution (in this case, the cli() function will turn into a single
assembly language instruction that turns off interrupts globally).
#define min(a,b) ((a)<(b)?(a):(b))
#define max(a,b) ((a)>(b)?(a):(b))
#define abs(x)
((x)>0?(x):-(x))
#define constrain(amt,low,high) ((amt)<(low)?(low):((amt)>(high)?(high):(amt)))
#define
#define
#define
#define
round(x)
radians(deg)
degrees(rad)
sq(x)
((x)>=0?(long)((x)+0.5):(long)((x)-0.5))
((deg)*DEG_TO_RAD)
((rad)*RAD_TO_DEG)
((x)*(x))
#define interrupts()
sei()
#define noInterrupts() cli()
#define clockCyclesPerMicrosecond() ( F_CPU / 1000000L )
Further down we find some typedefs, namely uint8_t, which is shorthand for an unsigned 8 bit
integer, i.e., an unsigned char. This typedef was written in another header file but notice that we
now have new typedefs based on that original typedef in the third and fourth lines! Thus, an
unsigned char may now be declared merely as boolean or byte, and finally, an unsigned int
may be declared as a word.
#define
#define
typedef
typedef
typedef
Embedded Controllers
91
Common procedures in IO programming include checking, setting and clearing specific bits in special
registers. Typically this is done through bitwise math operators. For example, if you want to set the 0 th bit
of a register called DDRB while leaving all other bits intact, youd bitwise OR it with 0x01 as in:
DDRB = DDRB | 0x01;
So if you want to set the bit for the motor, you would write:
DDRB = DDRB | (0x01<<MOTORBIT);
In other words, left shift a one in the zero bit location one place (resulting in the 1st vs. 0th bit being high)
and OR the register contents with it. This is a little cumbersome unless you turn it into a function call:
bitSet( DDRB, MOTORBIT );
Now thats pretty easy and obvious, so check out the lines below:
#define bitRead(value, bit) (((value) >> (bit)) & 0x01)
#define bitSet(value, bit) ((value) |= (1UL << (bit)))
#define bitClear(value, bit) ((value) &= ~(1UL << (bit)))
#define bitWrite(value, bit, bitvalue) (bitvalue ? bitSet(value, bit) :
bitClear(value, bit))
#define bit(b) (1UL << (b))
Further along in the file we come across a bunch of function prototypes for items we will be using:
void
void
int
int
void
void
pinMode(uint8_t, uint8_t);
digitalWrite(uint8_t, uint8_t);
digitalRead(uint8_t);
analogRead(uint8_t);
analogReference(uint8_t mode);
analogWrite(uint8_t, int);
The last two are particularly important. The Arduino development system has a pre-written main()
function. It makes calls to setup() and loop(), so well be writing these as our primary entry points.
92
Embedded Controllers
We find a (huge) series of conditional includes, each looking for the one pre-defined processor symbol set
by the Arduino IDE:
#if defined (__AVR_AT94K__)
# include <avr/ioat94k.h>
#elif defined (__AVR_AT43USB320__)
# include <avr/io43u32x.h>
#elif defined (__AVR_AT43USB355__)
# include <avr/io43u35x.h>
and so on until we get to the ATmega 328P for the Arduino Uno:
#elif defined (__AVR_ATmega328P__)
# include <avr/iom328p.h>
So whats in avr/iom328p.h you ask? This includes a bunch of things that will make our programming
lives much easier such as definitions for ports, registers and bits. Were going to be seeing these over and
over:
#ifndef _AVR_IOM328P_H_
#define _AVR_IOM328P_H_ 1
Embedded Controllers
93
PINB _SFR_IO8(0x03)
PINB0 0
PINB1 1
PINB2 2
PINB3 3
PINB4 4
PINB5 5
PINB6 6
PINB7 7
DDRB
DDB0
DDB1
DDB2
DDB3
DDB4
DDB5
DDB6
DDB7
_SFR_IO8(0x04)
0
1
2
3
4
5
6
7
PORTB _SFR_IO8(0x05)
PORTB0 0
PORTB1 1
PORTB2 2
PORTB3 3
PORTB4 4
PORTB5 5
PORTB6 6
PORTB7 7
and so on for ports C and D. Now for analog to digital converter (ADC) goodies:
#ifndef __ASSEMBLER__
#define ADC
_SFR_MEM16(0x78)
#endif
#define ADCW
_SFR_MEM16(0x78)
#define
#define
#define
#define
#define
#define
#define
#define
#define
94
ADCL _SFR_MEM8(0x78)
ADCL0 0
ADCL1 1
ADCL2 2
ADCL3 3
ADCL4 4
ADCL5 5
ADCL6 6
ADCL7 7
Embedded Controllers
#define
#define
#define
#define
#define
#define
#define
#define
#define
ADCH _SFR_MEM8(0x79)
ADCH0 0
ADCH1 1
ADCH2 2
ADCH3 3
ADCH4 4
ADCH5 5
ADCH6 6
ADCH7 7
#define
#define
#define
#define
#define
#define
#define
#define
#define
ADCSRA _SFR_MEM8(0x7A)
ADPS0 0
ADPS1 1
ADPS2 2
ADIE 3
ADIF 4
ADATE 5
ADSC 6
ADEN 7
#define
#define
#define
#define
#define
ADCSRB _SFR_MEM8(0x7B)
ADTS0 0
ADTS1 1
ADTS2 2
ACME 6
#define
#define
#define
#define
#define
#define
#define
#define
ADMUX _SFR_MEM8(0x7C)
MUX0 0
MUX1 1
MUX2 2
MUX3 3
ADLAR 5
REFS0 6
REFS1 7
and so on for the remainder of the header file. OK, so just what is this nugget below?
#define PORTB _SFR_IO8(0x05)
Most controllers communicate via memory-mapped IO. That is, external pins are written to and read from
as if they are ordinary memory locations. So if you want to write to a port, you can simply declare a
pointer variable, set its value to the appropriate address, and then manipulate it as needed. For the
ATmega 328P, the address of IO Port B is 0x25. You could write the following if you wanted to set bit 5
high:
unsigned char *portB;
portB = (unsigned char *)0x25; // cast required to keep compiler quiet
*portB = *portB | 0x20;
You could also use the bit setting function seen earlier which is a little clearer:
bitSet( *portB, 5 );
Embedded Controllers
95
The problem here is that you have to declare and use the pointer variable which is a little clunky. Theres
also the error-prone business of pointer de-referencing in the assignment statement (the * in front of the
variable which beginning programmers tend to forget). Besides, it requires that you look up the specific
address of the port (and change it if you use another processor). So to make things more generic we place
the addresses (or more typically, offsets from a base address) in the processor-specific header file.
The header file declares an item, PORTB, for our convenience. It is defined as _SFR_IO8(0x05) but
whats that _SFR_IO8 function? In sfr_defs.h its defined as follows:
#define _SFR_IO8(io_addr) _MMIO_BYTE((io_addr) + __SFR_OFFSET)
and __SFR_OFFSET is defined as 0x20. In other words, this item is 0x05 above the base offset address of
0x20, meaning its the address 0x25 as we saw earlier. But what the heck is _MMIO_BYTE()? That looks
a little weird at first glance:
#define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr))
This is just a cast with a pointer de-reference. It says this item is a pointer to an unsigned char which is
being de-referenced (de-referenced by the first * and dont forget the earlier typedef for the uint8_t).
By placing all of these items in header files weve managed to make the IO programming generic while at
the same time removing the need for pointer variable declarations and the need for pointer dereferencing13.
Therefore, if we want to set bit 5 high, we can now just say
PORTB = PORTB | 0x20;
bitSet( PORTB, 5 );
or
All the business about exactly where PORTB is located in the memory map is hidden from us and there
will be no accidental errors due to leaving out the *. The same code will work with several different
processors, and to top it off, its operationally more efficient than dealing with pointers as well.
Consequently, we will normally use these symbolic register and port names rather than hard addresses in
future work. Simply lovely. Time for a snack; something fruity perhaps, something that reminds us of
tropical islands
13
If you're wondering about volatile, recall that it's a modifier that indicates that the variable can be changed by
another process (possibly an interrupt). This will prevent overly aggressive assembly code optimizations by the
compiler.
96
Embedded Controllers
Embedded Controllers
97
98
Embedded Controllers
The circuit represents a single bit. An IO port consists of eight bits, typically. Therefore this circuitry
would be replicated seven more times to accommodate a single byte-wide port, of which there may be
several. Also, in order to reduce the external pin count of the IC, pins may be multiplexed with other
functions. The multiplexers are not shown in this schematic. Pxn, at the far left of the schematic,
represents the physical output pin. The data bus shown along the right edge is the source for data being
written to the physical pin and also the destination for data being read from the pin. For now we shall
focus on writing to the output and examine input functionality in a later chapter. While this circuit is
specific to the ATmega series, it is representative of GPIO circuitry found in many microcontrollers.
Embedded Controllers
99
First off, the PUD (Pull Up Disable), SLEEP and CLK (CLocK) control lines are common to all bits and
ports. We may ignore them for our purposes. The lowest section is removed because it is involved with
input functionality. The same goes for the upper section surrounding the MOSFET. Finally, a series of
two gates, an inverter and a multiplexer clustered around the middle section have been removed. These
serve a particular (and perhaps somewhat esoteric) function, namely the ability to rapidly toggle a bit.
Again, this section is not needed for typical functioning.
Upon simplification we are left with a circuit that centers around two D flip-flops and a few tri-state
buffers. The primary signals of interest are WDx and WRx which feed the DDxn and PORTxn flip-flops.
DDxn is the Data Direction bit. It determines whether the physical pin is configured for output (writing)
or input (reading). PORTxn presents the data that needs to be written. Note that the x refers to the port
letter, as in Port B, while the n refers to the bit number within that port. Thus, the physical pin for bit
number two in Port B would be denoted here as PB2 (or alternately, PORTB.2). The collection of all
eight bits of DDxn is referred to as the Data Direction Register, or DDR. The Data Direction Register for
Port B would be referred to as DDRB. Each port will have these registers mapped in memory. That is, for
Port B there will be a DDRB for direction control and a PORTB for writing data (there is also a PINB for
reading, more on that later). Similarly there will a DDRC and PORTC (and PINC) for Port C, and so on
for as many ports as the controller has (B, C and D for the ATmega 328P).
To understand how the circuit works, recall that a D flip-flops Q output simply echoes the logic level
present at the D input when the control signal transitions from low to high (i.e., positive edge trigger).
Note that the output of PORTxn feeds a tri-state buffer which in turn feeds the physical pin. In order to
write data to the pin, a logic high is first placed on the data bus. The WDx signal is pulsed which transfers
the logic high to Q of DDxn. This high level enables the tri-state buffer connected to Pxn. The port bit is
now configured for output and it will stay in this mode until WDx is re-asserted. The desired data (high or
low bit) is now placed on the data bus. The WRx signal is pulsed which transfers the logic level to Q of
PORTxn. This feeds the afore-mentioned tri-state which transfers the data bit to the output. This level will
remain until WRx is re-asserted. If WDx and WRx are never re-asserted, the output pin level will never
change. In order to write new data to the output pin, the desired data bit is placed on the bus and WRx is
pulsed. It is not necessary to re-load DDxn and re-assert WDx each time.
For example, suppose we wish to flash an LED several times. We could do this by attaching an LED to
the pin and then toggling the pin from high to low repeatedly. First, we would write a high to DDxn to
establish output (write) mode. Then we would write a high to PORTxn, turning on the LED. After a short
wait we would write a low to PORTxn, turning off the LED. After a further short wait we would write
another high to PORTxn (turning the LED back on) and continue the process in like manner for as many
flashes as we need.
The final two tri-state buffers associated with the RDx and RRx signals allow us to read the current states
of the direction and port bits.
100
Embedded Controllers
Embedded Controllers
101
102
Embedded Controllers
Once again we have removed the gates surrounding PORTxn that create the bit toggle function. Further,
we have removed the PUD, SLEEP and CLK signals and simplified the lower section leaving just the
Schmitt Trigger. The read process will be similar to the write process examined in the previous chapter.
To read a signal on an external pin, we will need to write a logic low to the data direction bit DDxn. This
will disconnect PORTxn from the physical pin, Pxn, because the tri-state buffer will go to high-Z state. If
Embedded Controllers
103
PORTxn was left connected, the external device would wind up trying to drive the active output of
PORTxn. This would produce unpredictable results at best.
Now that PORTxn is disconnected from Pxn, the signal existing at Pxn drives the Schmitt Trigger located
along the bottom edge of the diagram. This signal will be passed on to the data bus once the RPx (Read
Pin x) control signal is asserted on the lower tri-state buffer. A Schmitt Trigger is used as an intermediary
here as we cannot be certain of clean, noise-free input signals. Also, note that the input signals are read
from a PIN register (e.g., PINB or PINC). This is in contrast to the PORT registers used when writing
data. Thus, if you want to write data to the outside world, you write to a PORT register. If you want to
read data from the outside world, you read from a PIN register. Not all microcontrollers use this naming
convention. Some use a single PORT register for both writing and reading. A good mnemonic for
remembering whether PIN or PORT is used for input or output is to associate the in in PIN and the o
(for output) in PORT. Typically, the PORT, PIN and DDR registers are adjacent in the memory map. For
example, using port B we find PINB at 0x23, DDRB at 0x24 and PORTB at 0x25. Check the register map
in the Appendix for more.
We are still left with the MOSFET section. This is used to generate an optional pull-up resistor. There are
two basic ways to generate input signals. The first is with an active circuit. For example, the output of a
logic gate could be connected to the external pin. The voltage produced by the gate would be fed to the
Schmitt Trigger as described previously and the signal would make its way to the data bus. It is not
always convenient to generate an external voltage with an active circuit, though. If you wanted to read the
state of a simple push button, you couldnt just connect it between the pin and ground as there would be
no signal to sense. Instead youd have to connect it to an external power supply through a limiting
resistor, requiring more components and space. The pull-up takes care of this for you. When activated, the
MOSFET connects its associated resistor to the internal power line. If a switch is now connected between
the external pin and ground, and is closed, the pin is pulled to ground (i.e., logic low). If the switch is
opened, the resistor pulls up the pin voltage to the supply rail (i.e., logic high). Thus, we can determine
the state of the switch with no other external circuitry.
To activate the pull-up, first the pin must be in input mode (DDxn at logic low which feeds the AND
gate). Also, the output of PORTxn must be logic high. These two signals will drive the AND gates
output high, turning on the MOSFET and engaging the pull-up resistor. It might seem a little odd to be
writing to the PORT bit for read mode but this is just a neat way of making the most of the available
hardware. After all, in read mode, the PORT register is otherwise unused. Why let it go to waste and
require yet another register to control the pull-up?
In summary, to read from the external pin, a low is written to the appropriate DDR bit, placing this circuit
in read (input) mode. If a pull-up is desired, a high is written to the appropriate PORT bit. If a pull-up is
not desired, a low is written to the PORT bit. Once this is done, the external value can be read from the
PIN register. As with the output circuitry, the DDR and PORT do not have to be rewritten prior to each
subsequent read from the PIN. The D flip-flops will hold their values until they are rewritten, thus
maintaining the current data direction and pull-up status.
104
Embedded Controllers
Embedded Controllers
105
pinMode()
Description
Configures the specified pin to behave either as an input or an output. See the description of digital pins
for details on the functionality of the pins.
As of Arduino 1.0.1, it is possible to enable the internal pullup resistors with the mode INPUT_PULLUP.
Additionally, the INPUT mode explicitly disables the internal pullups.
Syntax
pinMode(pin, mode)
Parameters
pin: the number of the pin whose mode you wish to set
mode: INPUT, OUTPUT, or INPUT_PULLUP. (see the digital pins page for a more complete description
of the functionality.)
Returns
None
14
http://www.arduino.cc/en/Reference/PinMode
106
Embedded Controllers
So wed first have to think in terms of an Arduino pin number instead of a port bit number. Below is a
table of Arduino pin designations versus ATmega 328P port and pin naming.
Arduino
Designator
A0
General Purpose IO
Designator
PORTC bit 0
ADC input 0
A1
PORTC bit 1
ADC input 1
A2
PORTC bit 2
ADC input 2
A3
PORTC bit 3
ADC input 3
A4
PORTC bit 4
ADC input 4
A5
PORTC bit 5
ADC input 5
PORTD bit 0
RX
PORTD bit 1
TX
PORTD bit 2
PORTD bit 3
PORTD bit 4
PORTD bit 5
PWM
PORTD bit 6
PWM
PORTD bit 7
PORTB bit 0
PORTB bit 1
PWM
10
PORTB bit 2
PWM
11
PORTB bit 3
PWM
12
PORTB bit 4
13
PORTB bit 5
Comment
PWM
Built-in LED
Note that theA0 through A5 designators are for the analog inputs and the remaining are for digital IO. The
naming convention is reasonably logical but less than perfect. It should also be noted that the analog
channels are input-only. The controller cannot produce simple continuously variable analog voltages on
its own. This is not to say that its impossible to get analog control; just that its going to take a little more
work (as we shall see soon enough).
Practically speaking the pin naming convention isnt all that bad as the pins are labeled right on the board,
see Figure 21.3.
Embedded Controllers
107
OK, so what if were using this controller on a non-Arduino system, and is there a faster way to
accomplish this (code execution-wise)? Lets take a look at the code for this function. Its found in a file
called wiring_digital.c. Here are the relevant bits and pieces (slightly altered to make some portions a
little more clear):
void pinMode(uint8_t pin, uint8_t mode)
{
uint8_t bit, port, oldSREG;
volatile uint8_t *reg, *out;
bit = digitalPinToBitMask( pin );
port = digitalPinToPort( pin );
if (port == NOT_A_PIN) return;
108
Embedded Controllers
if (mode == INPUT)
{
oldSREG = SREG;
cli();
*reg &= ~bit;
*out &= ~bit;
SREG = oldSREG;
}
else
{
if (mode == INPUT_PULLUP)
{
oldSREG = SREG;
cli();
*reg &= ~bit;
*out |= bit;
SREG = oldSREG;
}
else // must be OUTPUT mode
{
oldSREG = SREG;
cli();
*reg |= bit;
SREG = oldSREG;
}
}
}
After the declaration of a few local variables, the specified Arduino pin number is decoded into both a
port and its associated bit mask by the digitalPinTo() functions. If a pin is specified that does not
exist on this particular processor, port will be set to an error value so we check for this and bail out if its
so. Whats a port bit mask? Thats just an 8 bit value with all 0s except for a 1 in the desired bit position
(sometimes its the complement of this, that is, all 1s with a 0- it depends on whether you intend to set,
clear, AND or OR). The portRegister() functions perform similar decoding operations. For
example, the so-called port mode register is better known as the data direction register, or DDR. The
ATmega 328P has three of these, DDRB through DDRD. Setting a bit sets the mode to output (write) while
clearing puts it in input (read) mode.
These four functions are really look-up tables disguised as functions. Its merely an access to an array
filled with appropriate values. Accessing an array is much faster than calling a function. Consider the
portModeRegister() function, which given a port designator, will return the actual data direction port
which we can manipulate (e.g., DDRC). This is defined as:
#define portModeRegister(P)
( (volatile uint8_t *)( pgm_read_word( port_to_mode_PGM + (P))) )
Embedded Controllers
109
port_to_mode_PGM turns out to be an array15 filled with the pre-defined error symbol and the addresses
space (remember, this controller uses a Harvard architecture with split memory).
The end result is that well get back the address of the required data direction register and thats what
well need to access in order to set input or output mode.
The next chunk of code is just three if/else possibilities, one for each of the modes we could request. Lets
take a look at OUTPUT mode first.
else
{
oldSREG = SREG;
cli();
*reg |= bit;
SREG = oldSREG;
}
Probably the single most important register on a microcontroller is the status register, here called SREG. It
contains a bunch of flag bits that signify a series of states. For example, there is a bit to indicate that an
arithmetic operation overflowed (i.e., there were too few bits to hold the result). Theres a bit to indicate
the result is negative, and so forth. One of the bits controls whether or not the system will respond to
interrupts. An interrupt is a high priority event that can halt (interrupt) the execution of your code while
the interrupt code (ISR, or interrupt service routine) runs instead. An interrupt can occur at any time, even
when your code is in the middle of something important. To prevent this from happening, we first save
the current contents of the status register and then clear the status bit that enables interrupts from
occurring. Thats the cli() call (which, as it turns out, is an in-line expansion to a single assembly
instruction called, you guessed it, CLI). Once we know we wont be interrupted, we can fiddle with the
DDR. reg is the DDR for this particular port. We OR it with the bit mask of interest, in other words, we
set that bit. This selects the direction as output. Finally, we restore the original contents of the status
register, enabling interrupts (assuming it had been set to begin with).
So, when originally we wrote
pinMode( 8, OUTPUT );
The function decoded Arduino pin 8 as being bit 0 of port B (i.e., PORTB). It also determined that the
corresponding DDR is DDRB. The bit 0 mask is 0x01 and it was ORed with the contents of DDRB, thus
selecting the direction for that bit as output mode.
15
110
Embedded Controllers
Note that here the DDR is ANDed with the complement of the bit mask to clear the bit, thus placing it in
input mode. This code also clears the same bit in the output register which disables the pull-up resistor at
the external pin. In contrast, note that INPUT_PULLUP mode sets this bit.
Great stuff for sure, but what if youre not using an Arduino or you need to get this done quicker? If you
dont have any interrupt code running you can safely twiddle directly with the port and DDR. Remember,
we wanted to make bit 0 of port B ready for output. That means we need to set bit 0 of DDRB.
DDRB |= 0x01;
Either would do the trick, the latter probably a little more clear and less error prone. And heres
something very useful to remember: What if you need to set a bunch of pins or even an entire port to
input or output mode? Using pinMode() youd have to make individual calls for each pin. In contrast, if
you needed to set the bottom six bits of port B to output mode16 you could just do this:
DDRB |= 0x3f;
Does this mean that we should never use pinMode()? No! We have seen that this function ties in
perfectly with the Arduino Uno board and is both more robust and more flexible. Its just that sometimes
a bicycle will serve us better than a motor vehicle and its good that we have the option.
16
You might do this in order to write values out of the port in parallel fashion, several bits at a time, for example to
feed a parallel input DAC.
Embedded Controllers
111
digitalWrite()
Description
Write a HIGH or a LOW value to a digital pin.
If the pin has been configured as an OUTPUT with pinMode(), its voltage will be set to the
corresponding value: 5V (or 3.3V on 3.3V boards) for HIGH, 0V (ground) for LOW.
If the pin is configured as an INPUT, writing a HIGH value with digitalWrite() will enable an internal
20K pullup resistor (see the tutorial on digital pins). Writing LOW will disable the pullup. The pullup
resistor is enough to light an LED dimly, so if LEDs appear to work, but very dimly, this is a likely
cause. The remedy is to set the pin to an output with the pinMode() function.
17
http://arduino.cc/en/Main/ArduinoBoardDue
112
Embedded Controllers
NOTE: Digital pin 13 is harder to use as a digital input than the other digital pins because it has an
LED and resistor attached to it that's soldered to the board on most boards. If you enable its internal 20k
pull-up resistor, it will hang at around 1.7 V instead of the expected 5V because the onboard LED and
series resistor pull the voltage level down, meaning it always returns LOW. If you must use pin 13 as a
digital input, use an external pull down resistor.
Syntax
digitalWrite(pin, value)
Parameters
pin: the pin number
value: HIGH or LOW
Returns
none
Example
int ledPin = 13;
void setup()
{
pinMode(ledPin, OUTPUT);
}
void loop()
{
digitalWrite(ledPin, HIGH);
delay(1000);
digitalWrite(ledPin, LOW);
delay(1000);
}
//
//
//
//
From the example code, this is pretty easy to use. Simply set the direction first, then write to the
appropriate Arduino pin designator. Remember, the pin designators are the numbers written next to the
headers on the Uno board, theyre NOT the port bit numbers on the ATmega 328P! The reference note
regarding Arduino pin 13 is worth re-reading. Pin 13 is hardwired to an on-board surface mount signaling
LED located right next to said pin. This also means that the total source current available for pin 13 is
somewhat reduced as LED current will also always be applied. Arduino pin 13 is PORTB bit 5
(sometimes written shorthand as PORTB.5).
So, just what does the digitalWrite()function do? Here is the code for digitalWrite(), slightly
cleaned up for your viewing pleasure18:
18
Embedded Controllers
113
turnOffPWM( timer );
SREG = oldSREG;
}
The AVR series of controllers, like most controllers, contain internal timers/counters. These allow the
controller to precisely time events or produce pulse signals (specifically, pulse width modulation). The
Arduino system pre-configures six of the available outputs with timers for use with the analogWrite()
function. As not all pins have this ability, we need to translate the Arduino pin to an associated timer with
the digitalPinToTimer() function. We will take a closer look at timers later, but for now it is only
important to understand that any associated timer needs to be turned off before we can use our basic
digital write function.
if (timer != NOT_ON_TIMER)
turnOffPWM( timer );
The code to turn off the PWM functionality is little more than a switch/case statement. The cbi() call
is used to clear a specific bit in a port, in this case the associated timer-counter control register (TCCRx).
More info can be found at http://playground.arduino.cc/Main/AVR.
114
Embedded Controllers
COM0A1);
COM0B1);
COM1A1);
COM1B1);
COM2A1);
COM2B1);
break;
break;
break;
break;
break;
break;
After this, the specified Arduino port is translated to an output register, that is, PORTx.
out = portOutputRegister( port );
As was seen with pinMode(), the status register is saved, the global interrupt bit is cleared to disable all
interrupts via the cli() call, the desired port (PORTx) is ANDed with the complement of the bit mask to
clear it (i.e., set it low) or ORed with the bit mask to set it (i.e., set it high). The status register is then
restored to its original state:
oldSREG = SREG;
cli();
if (val == LOW)
else
SREG = oldSREG;
Thats pretty much it. Now, it you want to write a bunch of bits to a group of output port connections, you
can simply look up the corresponding port for those Arduino pins (i.e., PORTx) and set or clear them
directly. For example, if you want to clear bits 0, 1, and 4 of port B (i.e., 00010011 or 0x13), you could
do the following (assuming no timer is active):
PORTB &= ~0x13;
// clear bits
PORTB |= 0x13;
// set bits
or to set them:
This would leave the other bits completely untouched. In contrast, suppose you want to set the bottom
four bits (i.e., 0 through 3) to the binary pattern 0101. Thats equivalent to 0x05. You could do the
following:
PORTB = (PORTB & 0xf0)| 0x05;
The first chunk clears the bottom four bits and the trailing part sets the binary pattern. This would be
preferable to clearing bits 1 and 3 and then setting bits 0 and 2 as that would cause two distinct sets of
output voltage patterns at the external pins. Granted, the first set will exist for only a very short time but
this can create problems in some applications.
Embedded Controllers
115
Note that we specify the number of milliseconds wed like to waste. Since each iteration of the loop takes
one microsecond, we multiply by 1000 to achieve milliseconds. The volatile modifier is important
here. This tells the compiler not to aggressively optimize the code for us because I could be changed by
code running elsewhere (for example, in an interrupt). Otherwise, the compiler might figure out that it can
achieve the same end result by ignoring the loop and doing a simple addition. The problem with this
function is that the resulting delay is highly dependent on the microcontroller used and its clock
frequency. If you just need a quick and dirty delay this will work fine, but a far more accurate delay is
available with the delay() function and its sibling delayMicroseconds(), whose reference material
is repeated below.
delay()19
Description
Pauses the program for the amount of time (in miliseconds) specified as parameter. (There are 1000
milliseconds in a second.)
Syntax
delay( ms )
Parameters
ms: the number of milliseconds to pause (unsigned long)
Returns
nothing
19
http://arduino.cc/en/Reference/Delay
116
Embedded Controllers
Example
int ledPin = 13;
void setup()
{
pinMode(ledPin, OUTPUT);
}
void loop()
{
digitalWrite(ledPin, HIGH);
delay(1000);
digitalWrite(ledPin, LOW);
delay(1000);
}
//
//
//
//
Caveat
While it is easy to create a blinking LED with the delay() function, and many sketches use short delays
for such tasks as switch debouncing, the use of delay() in a sketch has significant drawbacks. No other
reading of sensors, mathematical calculations, or pin manipulation can go on during the delay function,
so in effect, it brings most other activity to a halt. For alternative approaches to controlling timing see
the millis() function and the sketch sited below. More knowledgeable programmers usually avoid the use
of delay() for timing of events longer than 10s of milliseconds unless the Arduino sketch is very simple.
Certain things do go on while the delay() function is controlling the Atmega chip however, because the
delay function does not disable interrupts. Serial communication that appears at the RX pin is recorded,
PWM (analogWrite) values and pin states are maintained, and interrupts will work as they should.
See also
millis()
micros()
delayMicroseconds()
delayMicroseconds()
Description
Pauses the program for the amount of time (in microseconds) specified as parameter. There are a
thousand microseconds in a millisecond, and a million microseconds in a second.
Currently, the largest value that will produce an accurate delay is 16383. This could change in future
Arduino releases. For delays longer than a few thousand microseconds, you should use delay() instead.
Embedded Controllers
117
Syntax
delayMicroseconds(us)
Parameters
us: the number of microseconds to pause (unsigned int)
Returns
None
These functions are also tied in with two other functions, micros() and millis(), which are repeated below:
millis()
Description
Returns the number of milliseconds since the Arduino board began running the current program. This
number will overflow (go back to zero), after approximately 50 days.
Parameters
None
Returns
Number of milliseconds since the program started (unsigned long)
Tip:
Note that the parameter for millis is an unsigned long, errors may be generated if a programmer tries to
do math with other datatypes such as ints
118
Embedded Controllers
micros()
Description
Returns the number of microseconds since the Arduino board began running the current program. This
number will overflow (go back to zero), after approximately 70 minutes. On 16 MHz Arduino boards
(e.g. Duemilanove and Nano), this function has a resolution of four microseconds (i.e. the value returned
is always a multiple of four). On 8 MHz Arduino boards (e.g. the LilyPad), this function has a resolution
of eight microseconds.
Parameters
None
Returns
Number of microseconds since the program started (unsigned long)
All of these functions rely on the Arduino system configuring the timers the moment the board is reset.
One of these will be used to generate an interrupt when the counter overflows. The time to overflow will
take a predetermined amount of time based on the clock speed. The interrupt will in turn update three
global variables that will keep track of how long the program has been running.
First lets consider the initialization code along with some definitions and global variable declarations.
Besides this timer, the init code also sets up the other timers for pulse width modulation duties (via the
analogWrite() function). The code is reasonably well commented and is presented without further
explanation, save for a reminder that sbi() is a macro that will reduce to a single assembly language
instruction that sets a specific register bit.
#include "wiring_private.h"
// the prescaler is set so that timer0 ticks every 64 clock cycles, and the
// the overflow handler is called every 256 ticks.
#define MICROSECONDS_PER_TIMER0_OVERFLOW (clockCyclesToMicroseconds(64 * 256))
// the whole number of milliseconds per timer0 overflow
#define MILLIS_INC (MICROSECONDS_PER_TIMER0_OVERFLOW / 1000)
// the fractional number of milliseconds per timer0 overflow. we shift right
// by three to fit these numbers into a byte. (for the clock speeds we care
// about - 8 and 16 MHz - this doesn't lose precision.)
#define FRACT_INC ((MICROSECONDS_PER_TIMER0_OVERFLOW % 1000) >> 3)
#define FRACT_MAX (1000 >> 3)
Embedded Controllers
119
void init()
{
// this needs to be called before setup() or some functions won't
// work there
sei();
// set timer 0 prescale factor to 64
sbi(TCCR0B, CS01);
sbi(TCCR0B, CS00);
// enable timer 0 overflow interrupt
sbi(TIMSK0, TOIE0);
//
//
//
//
TCCR1B = 0;
// set timer 1 prescale factor to 64
sbi(TCCR1B, CS11);
sbi(TCCR1B, CS10);
// put timer 1 in 8-bit phase correct pwm mode
sbi(TCCR1A, WGM10);
// set timer 2 prescale factor to 64
sbi(TCCR2B, CS22);
// configure timer 2 for phase correct pwm (8-bit)
sbi(TCCR2A, WGM20);
// set a2d prescale factor to 128
// 16 MHz / 128 = 125 KHz, inside the desired 50-200 KHz range.
// XXX: this will not work properly for other clock speeds, and
// this code should use F_CPU to determine the prescale factor.
sbi(ADCSRA, ADPS2);
sbi(ADCSRA, ADPS1);
sbi(ADCSRA, ADPS0);
// enable a2d conversions
sbi(ADCSRA, ADEN);
// the bootloader connects pins 0 and 1 to the USART; disconnect
// them here so they can be used as normal digital i/o;
// they will be reconnected in Serial.begin()
UCSR0B = 0;
}
120
Embedded Controllers
Now lets take a look at the interrupt service routine. Each time the counter overflows (i.e. the 8 bit
counter tries to increment 255 and wraps back to 0) it generates an interrupt which calls this function.
Basically, all it does is increment the global variables declared earlier.
SIGNAL( TIMER0_OVF_vect )
{
// copy these to local variables so they can be stored in
// registers (volatile vars are read from memory on every access)
unsigned long m = timer0_millis;
unsigned char f = timer0_fract;
m += MILLIS_INC;
f += FRACT_INC;
if (f >= FRACT_MAX)
{
f -= FRACT_MAX;
m += 1;
}
timer0_fract = f;
timer0_millis = m;
timer0_overflow_count++;
}
As you might now guess, all the millis() and micros() functions do is access these global variables
and return their values. Because an interrupt can occur during this process, the value of the status register
(SREG) is copied, the status registers global interrupt enable bit is cleared with the cli() call, the access
performed (plus a little extra calculation for micros()) and the status register returned to its prior state.
The retrieved value is then returned to the caller.
unsigned long millis()
{
unsigned long m;
uint8_t oldSREG = SREG;
// disable interrupts while we read timer0_millis or we might get
// an inconsistent value (e.g. in the middle of a write to
// timer0_millis)
cli();
m = timer0_millis;
SREG = oldSREG;
return m;
}
Embedded Controllers
121
m++;
In a way, this is just a slightly more sophisticated version of our initial cheesy delay function. It is more
precise because it uses the accurate internal counters which are operating from a known clock frequency.
The microseconds version of the delay is a little trickier, especially for short delays. This also does a busy
wait but does so using in-line assembly code. Even with this, the delays are not particularly accurate for
periods of only a few microseconds. In the in-line comments are instructive:
122
Embedded Controllers
The major problem with using delay() is noted in its on-line documentation, namely, that during a busy
wait loop no other work can be done. The controller is effectively spinning its wheels. A more effective
way to delay is to make direct use of the millis() function. The basic idea is to check the time using
millis() and then do what you need to do inside a loop, checking the elapsed time on each iteration.
Here is a snippet of example code.
unsigned long currentMillis, previousMillis, intervalToWait;
// intervalToWait could be a passed variable, global or define
// initialize to current time
previousMillis = millis();
currentMillis = millis();
while ( currentMillis - previousMillis < intervalToWait )
{
// do whatever you need to do here
currentMillis = millis();
}
In essence youve built your own kind of busy wait loop but with requisite code inside.
Embedded Controllers
123
digitalRead()20
Description
Reads the value from a specified digital pin, either HIGH or LOW.
Syntax
digitalRead( pin )
Parameters
pin: the number of the digital pin you want to read (int)
Returns
HIGH or LOW
Example
int ledPin = 13; // LED connected to digital pin 13
int inPin = 7;
// pushbutton connected to digital pin 7
int val = 0;
// variable to store the read value
void setup()
{
pinMode(ledPin, OUTPUT);
pinMode(inPin, INPUT);
}
void loop()
{
val = digitalRead(inPin);
digitalWrite(ledPin, val);
}
20
http://arduino.cc/en/Reference/DigitalRead
124
Embedded Controllers
Note
If the pin isn't connected to anything, digitalRead() can return either HIGH or LOW (and this can change
randomly).
The analog input pins can be used as digital pins, referred to as A0, A1, etc.
See also
pinMode()
digitalWrite()
A slightly cleaned-up version of the source code follows (found in the file wiring_digital.c):
int digitalRead( uint8_t pin )
{
uint8_t timer, bit, port;
timer = digitalPinToTimer( pin );
bit =
digitalPinToBitMask( pin );
port = digitalPinToPort( pin );
if (port == NOT_A_PIN)
return LOW;
if (timer != NOT_ON_TIMER)
turnOffPWM(timer);
break;
break;
Embedded Controllers
125
The first three lines convert the Arduino pin designator to the appropriate ATmega 328P port, bit number
and timer. If the port is invalid, the function exits.
timer = digitalPinToTimer( pin );
bit =
digitalPinToBitMask( pin );
port = digitalPinToPort( pin );
if (port == NOT_A_PIN)
return LOW;
The timer is important because the Arduino system preconfigures the Unos three on-board timers for use
with the analogWrite() function through a pulse width modulation scheme. This affects six of the
possible pins. For proper operation of the digital read, these timers need to be turned off. We saw this
same bit of code inside the digitalWrite() function.
if (timer != NOT_ON_TIMER)
turnOffPWM(timer);
At this point the contents of the input register are read (the direct name of the input register is PINx) and
then ANDed with the requested bit. This removes all of the other bits so we can return either a simple
high or low.
if (*portInputRegister(port) & bit) return HIGH;
return LOW;
The function used to turn off the pulse width modulation timers is little more than a switch/case
statement. If the specified Arduino pin is hooked up to a timer internally, that timer is found in the switch
statement and a cbi() call is executed on the appropriate timer-counter control register. The cbi()
function translates to a single assembly language instruction to clear the specified bit in the control
register, thus turning off that timer.
static void turnOffPWM( uint8_t timer )
{
switch (timer)
{
case TIMER0A:
cbi(TCCR0A,
case TIMER0B:
cbi(TCCR0A,
case TIMER1A:
cbi(TCCR1A,
case TIMER1B:
cbi(TCCR1A,
case TIMER2A:
cbi(TCCR2A,
case TIMER2B:
cbi(TCCR2A,
}
}
COM0A1);
COM0B1);
COM1A1);
COM1B1);
COM2A1);
COM2B1);
break;
break;
break;
break;
break;
break;
In some applications, several bits need to be read at once, for example when reading parallel data. This
can be performed through a direct access of the appropriate PINx register. PINx is rather like the fraternal
twin of the PORTx register. While PORTx is used to write digital data to an external connection, PINx is
where you read digital data from an external connection. Just as there are four output port registers, A
through D, there are four input pin registers, A through D. Not all microcontrollers are configured this
way. Some of them use the same register for both reading and writing (the function being controlled by
the associated data direction register).
Here is how to directly access a single bit on a non-timer connected pin. First, clear the desired data
direction register bit to activate input mode. Second, if desired, set the same bit in the associated port
126
Embedded Controllers
register to enable the optional pull-up resister. If you dont want the pull-up, leave that bit clear. Finally,
read the desired bit in the pin register and AND it with the bit mask to remove the other bits. For example,
to read bit 4 (0x10 or 00010000 in binary) on port B:
DDRB &= (~0x10);
PORTB |= 0x10;
//
//
//
value = PINB & (~0x10); //
It is only minor work to alter this for multiple bits. To read both bits 0 and 4 but without the pull-up
resistors (bit pattern 00010001 or 0x11):
DDRB &= (~0x11);
PORTB &= (~0x11);
value = PINB & (~0x11);
127
finally releases the button the current state will be low with a prior state of high. This is the negative
going edge and, again, we have no need to process it. The next loop iteration will produce current and
prior states of low with no need to process. Eventually, when the user presses the button again well see a
current state of high with a prior state of low and well process this edge.
Well need three variables for the states, namely the current and prior pushbutton states and another for
the LEDs (which indicate fan speed). The pushbuttons can be thought of as Boolean variables (0/1) while
the LED variable will range from 0 through 3, 0 being off and 3 being high. For simplicity, well use
globals for these, unsigned chars will do nicely. The four LEDs will be positioned off of Arduino Uno
pins 8 through 11 (PORTB.0:3) and the pushbutton will be connected to pin 12 (PORTB.4). The setup()
function will have to set the proper directions for these port bits. This would require five separate calls to
pinMode() but only two simple operations if we operate directly. Here is the beginning portion:
// declare the state variables and initialize at 0 for off
unsigned char currentstate=0;
unsigned char priorstate=0;
unsigned char ledstate=0;
// define some values for the LED/fan state. These are conveniently
// chosen to be the FAN/LED bit positions in PORTB
#define FAN_OFF
0
#define FAN_LO
1
#define FAN_MED
2
#define FAN_HIGH 3
// declare bit masks for the four LED bits and pushbutton
#define LED_OFF 0x01
#define LED_LOW 0x02
#define LED_MED 0x04
#define LED_HIGH 0x08
#define PBUTTON 0x10
// the LED_XXX masks could also be defined like so:
// #define LED_OFF (1<<FAN_OFF)
// a convenience
#define LED_ALL (LED_OFF|LED_LOW|LED_MED|LED_HIGH)
setup()
{
// set for output
DDRB |= LED_ALL;
// set for input
DDRB &= ~PBUTTON;
// light up the off LED
PORTB |= LED_OFF;
// by default, outputs are 0 but if for some reason the other
// LEDs could be on, be sure to set them to off before continuing
// PORTB &= ~(LED_LOW|LED_MED|LED_HIGH);
}
128
Embedded Controllers
The looping code needs to check for the proper current state/prior state pair. If it is not found, there is
nothing to do. If it is found, we need to turn off the existing LED and then check the current LED/fan
state, increment to the next state and light that LED. There are a couple ways to do this. The first is
perhaps an obvious solution using a switch/case construct. The second method is a bit more compact (pun
intended).
loop()
{
// get current button state
currentstate = PINB & PBUTTON;
// do we have a positive going edge?
if( currentstate && !priorstate )
{
switch( ledstate )
{
case FAN_OFF:
PORTB &= ~LED_OFF; // turn off old LED
PORTB |= LED_LOW;
// turn on new LED
ledstate = FAN_LOW; // increment speed
break;
case FAN_LOW:
PORTB &= ~LED_LOW;
PORTB |= LED_MED;
ledstate = FAN_MED;
break;
case FAN_MED:
PORTB &= ~LED_MED;
PORTB |= LED_HIGH;
ledstate = FAN_HIGH;
break;
case FAN_HIGH:
PORTB &= ~LED_HIGH;
PORTB |= LED_OFF;
ledstate = FAN_OFF;
break;
}
}
// update state for next loop iteration
priorstate = currentstate;
}
The alternate version can be found on the next page. Note the code savings. Instead of checking for each
fan state, we turn off all LEDs and increment the fan state. If the state would increment past the maximum
then we make sure to set it to the off state. This new state tells us which LED to light. This version works
with little code because of the way in which we chose our bit positions. This is not always possible (or
even desirable) but can be a handy technique from time to time.
Embedded Controllers
129
loop()
{
// get current button state
currentstate = PINB & PBUTTON;
// do we have a positive going edge?
if( currentstate && !priorstate )
{
// increment to new state
if( ledstate < FAN_HIGH )
ledstate++;
else
ledstate = FAN_OFF;
// lazy: turn off all LEDs
PORTB &= ~LED_ALL;
// turn on new LED
PORTB |= (1<<ledstate);
}
// update state for next loop iteration
priorstate = currentstate;
}
24.3 Exercise
In closing, heres an interesting question. If the round-robin switch had numerous states, say a dozen or
so, a single increment button might prove a little frustrating to the user. For example, if they accidentally
go past the desired setting then theyre forced to go all the way around again to effectively back up. To
alleviate this we could add a decrement pushbutton alongside our existing increment pushbutton. How
would the code example presented above need to be altered to respond to a two button configuration? If at
first this appears to be too daunting of a challenge, then break it into two parts. Initially, consider how the
preceding code would need to be altered in order to change the operation of the fan speed increment
button into a fan speed decrement button. That is, instead of button pushes producing the sequence offlow-med-high-off etc., implement the button to produce off-high-med-low-off etc. Once this is completed,
determine how to combine this new implementation with the existing example code.
130
Embedded Controllers
Embedded Controllers
131
132
Embedded Controllers
Embedded Controllers
133
The heart of the system is a single successive approximation DAC with 10 bit resolution. Maximum
conversion speed at full resolution is about 15 k samples per second. Clearly this is insufficient for
applications such as CD quality audio or video but is more than fast enough for most sensor and user
interface monitoring. The converter performance specifications include .5 LSB integral non-linearity error
and 2 LSB absolute accuracy. An on-board reference is available although an external reference may
also be used. The converter includes a sample-and-hold system to stabilize the measurement for rapidly
changing input signals. There is an eight channel mux although only six of these channels are used on
some packages (the package used on the standard configuration of the Arduino Uno board uses six). The
input circuitry is optimized for sources that have an output impedance of 10 K or less. It is also very
important to recognize that the system is unipolar and will only respond to positive voltages. If the signal
to be measured is bipolar, some manner of DC level shifting will be required.
There are several registers associated with the ADC system. These are shown in Figure 25.2.
Register
Bit 7
Bit 6
Bit 5
Bit 4
Bit 3
Bit 2
Bit 1
Bit 0
ADCSRA
ADEN
ADSC
ADATE
ADIF
ADIE
ADPS2
ADPS1
ADPS0
ADCSRB
ACME
ADTS2
ADTS1
ADTS0
ADMUX
REFS1
REFS0
ADLAR
MUX3
MUX2
MUX1
MUX0
ADCH
ADC9/-
ADC8/-
ADC7/-
ADC6/-
ADC5/-
ADC4/-
ADC3/9
ADC2/8
ADCL
ADC1/7
ADC0/6
-/ADC5
-/ADC4
-/ADC3
-/ADC2
-/ADC1
-/ADC0
ADCSRA and ADCSRB are the Analog-Digital Control and Status Registers. Here are their bit descriptions:
134
hardware will reset this bit to zero. Writing zero to this bit will have no effect.
ADATE: Auto Trigger Enable. Set to one for auto trigger mode. A rising edge on the selected
trigger signal will start a conversion. The trigger source is selected via the ADTS bits in the
ADCSRB register (below).
ADIF: Interrupt Flag. This is used with interrupt mode. This bit is set when a conversion is
complete and the data registers are updated. This bit is cleared by the hardware when executing
the interrupt handler routine. Use with ADIE.
ADIE: Interrupt Enabled. Set to one to enable the ADC conversion interrupt. Use with ADIF.
ADPS2-0: Prescaler bits. These bits are used to divide down the system clock to the ADC clock.
000 and 001 yield division by two and increasing values double the clock divide with 111
yielding divide by 128.
ACME: Analog Comparator Multiplexer Enabled. Setting these bits allows any of the eight
multiplexed ADC pins to replace the negative input to the analog comparator.
ADTS2-0: Trigger Source. If ADATE is set to one, these bits determine the trigger source. 000 is
free running mode, 001 triggers from the analog comparator, 010 uses External Interrupt Request
0, and the remaining values set various timer/counter sources.
Embedded Controllers
ADMUX is the Analog-Digital MUltipleXer selection register. It is also used for reference selection and
REFS1-0: REFerence voltages. 00 yields the AREF pin, 01 yields AVCC, 11 yields internal 1.1 volt
Finally we have ADCH and ADCL, the ADC High and Low output registers. There are two registers
because this is a 10 bit converter but the registers only hold eight bits each. There are two modes of
operation in conjunction with the ADLAR bit in the ADMUX register. If ADLAR is set then the top eight bits
will be placed into ADCH and the bottom two bits will be placed at the top of ADCL. If ADLAR is cleared
then the bottom eight bits will be found in ADCL and the top two bits will be found at the bottom of ADCH.
Just think of the two registers as a single 16 bit register with the bits pushed to the left or right as shown
in Figure 26.3.
ADCH
ADLAR
1
0
ADCL
Bit
15
ADC
9
Bit
14
ADC
8
Bit
13
ADC
7
Bit
12
ADC
6
Bit
11
ADC
5
Bit
10
ADC
4
Bit
9
ADC
3
ADC
9
Bit
8
ADC
2
ADC
8
Bit
7
ADC
1
ADC
7
Bit
6
ADC
0
ADC
6
Bit
5
Bit
4
Bit
3
Bit
2
Bit
1
Bit
0
ADC
5
ADC
4
ADC
3
ADC
2
ADC
1
ADC
0
If only eight bits of resolution are needed, it is possible to left justify and read just ADCH, ignoring ADCL.
Single conversion operation is fairly straight forward. First, set up your initialization section. This begins
with setting ADEN to enable the system. Then decide on the proper prescale value. The chip specifications
indicate that the successive approximation circuitry will perform best with an ADC clock in the range of
50 kHz to 200 kHz. A typical conversion will take 13 ADC clock cycles (the first conversion will take 25
due to initialization overhead). Once this is determined, set ADLAR for data justification and REFS for the
reference voltage. Then set the ADMUX bits to determine the input channel. Finally, set ADSC to start a
conversion. You can wait on this bit or come back to it later. When the conversion is complete the
hardware will reset it to zero. At that point read the value from ADCL and combine (shift and OR) with the
value read from ADCH into a 16 bit integer. Note that you must read ADCL before reading ADCH. Reading
ADCL locks the ADC registers preventing the hardware from overwriting the registers on a subsequent
conversion. Reading ADCH unlocks them.
We will take a closer look at the software process in the next chapter.
Embedded Controllers
135
analogRead()21
Description
Reads the value from the specified analog pin. The Arduino board contains a 6 channel (8 channels on
the Mini and Nano, 16 on the Mega), 10-bit analog to digital converter. This means that it will map input
voltages between 0 and 5 volts into integer values between 0 and 1023. This yields a resolution between
readings of: 5 volts / 1024 units or, .0049 volts (4.9 mV) per unit. The input range and resolution can be
changed using analogReference().
It takes about 100 microseconds (0.0001 s) to read an analog input, so the maximum reading rate is
about 10,000 times a second.
Syntax
analogRead( pin )
Parameters
pin: the number of the analog input pin to read from (0 to 5 on most boards, 0 to 7 on the Mini and
Nano, 0 to 15 on the Mega)
Returns
int (0 to 1023)
Note
If the analog input pin is not connected to anything, the value returned by analogRead() will fluctuate
based on a number of factors (e.g. the values of the other analog inputs, how close your hand is to the
board, etc.).
21
http://arduino.cc/en/Reference/AnalogRead
136
Embedded Controllers
See also
analogReference()
analogReference()22
Description
Configures the reference voltage used for analog input (i.e. the value used as the top of the input range).
The options are:
DEFAULT: the default analog reference of 5 volts (on 5V Arduino boards) or 3.3 volts (on 3.3V Arduino
boards)
INTERNAL: an built-in reference, equal to 1.1 volts on the ATmega168 or ATmega328 and 2.56 volts on
the ATmega8 (not available on the Arduino Mega)
EXTERNAL: the voltage applied to the AREF pin (0 to 5V only) is used as the reference.
Syntax
analogReference( type )
Parameters
type: which type of reference to use (DEFAULT, INTERNAL, INTERNAL1V1, INTERNAL2V56, or
EXTERNAL).
Returns
None.
Note
After changing the analog reference, the first few readings from analogRead() may not be accurate.
Warning
Don't use anything less than 0V or more than 5V for external reference voltage on the AREF pin!
If you're using an external reference on the AREF pin, you must set the analog reference to
EXTERNAL before calling analogRead(). Otherwise, you will short together the active reference
voltage (internally generated) and the AREF pin, possibly damaging the microcontroller on your
Arduino board.
22
http://arduino.cc/en/Reference/AnalogReference
Embedded Controllers
137
Alternatively, you can connect the external reference voltage to the AREF pin through a 5K resistor,
allowing you to switch between external and internal reference voltages. Note that the resistor will alter
the voltage that gets used as the reference because there is an internal 32K resistor on the AREF pin. The
two act as a voltage divider, so, for example, 2.5V applied through the resistor will yield 2.5 * 32 /
(32+5) = ~2.2V at the AREF pin.
See also
analogRead()
The analogRead() function is best used as a single conversion snapshot. This is good for simple user
interface and basic sensor applications. Generally speaking, analogReference() is called once during
the setup and initialization phase of the program. Also, unless there is a compelling reason to do
otherwise, the default mode is the best place to start. This will yield a 5 volt peak to peak input range with
a bit resolution of just under 4.9 millivolts (5/1024). It is important to note that this range runs from 0
volts to 5 volts, not -2.5 volts to +2.5 volts. Depending on the amplitude and frequency range of the
sensor signal, some input processing circuitry may be required to apply a DC offset, amplify or reduce
signal strength, filter frequency extremes and so forth.
The code for analogReference() is about as simple as it gets. It just sets (and hides) a global variable
which will be accessed by the analogRead() function:
uint8_t analog_reference = DEFAULT;
void analogReference(uint8_t mode)
{
analog_reference = mode;
}
The analogRead() function is a bit more interesting (actually, 10 bits more interesting). From the
preceding chapter we saw that there several registers associated with the ADC system. For single
conversion operation the important registers include ADCSRA (ADC control and status register A) and
ADMUX (ADC multiplexer selection register). The register bits are repeated below for convenience:
Register
Bit 7
Bit 6
Bit 5
Bit 4
Bit 3
Bit 2
Bit 1
Bit 0
ADCSRA
ADEN
ADSC
ADATE
ADIF
ADIE
ADPS2
ADPS1
ADPS0
ADMUX
REFS1
REFS0
ADLAR
MUX3
MUX2
MUX1
MUX0
138
Embedded Controllers
Also recall that the ADCH and ADCL registers contain the high and low bytes of the result, respectively. For
ADCSRA, setting ADEN enables the ADC. Setting ADSC starts a conversion. It will stay at one until the
conversion is complete, the hardware resetting it to zero. ADIF and ADIE are used with an interrupt-based
mode not discussed here. ADATE stands for AD Auto Trigger Enable which allows triggering of the ADC
from an external signal (again, not discussed here). ADPSx are pre-scaler bits which set the conversion
speed. See the previous chapter for details.
For ADMUX, REFSx sets the reference voltage source where 00 = use AREF pin, 01 = use VCC, 10 =
reserved, 11 = use internal 1.1 volt reference. Setting ADLAR left-adjusts the 10 bit word within the ADCH
and ADCL result registers (i.e. it left-justifies, leaving the bottom six bits unused). Clearing this bit leaves
the result right justified (i.e. top six bits are unused). The four MUXx bits select which input channel is
used for the conversion. For example, to read from channel 5, set these bits to the pattern 0101.
Other registers are available such as ADSRB and ACSR which are useful for other modes of operation.
They are not necessary for the current purpose, though.
The code follows, cleaned up for ease of reading. The original code contains a considerable number of
#ifdefs so it works with different microcontrollers.
Embedded Controllers
139
Lets take a closer look. First, two unsigned bytes are declared to hold the contents of the high and low
result registers; then the pin argument is translated. Note the undocumented usage of either channel or pin
numbers. Always be cautious about using undocumented features.
uint8_t low, high;
if (pin >= 14) pin -= 14; // allow for channel or pin numbers
At this point the reference is set (note how the analog_reference global is shifted up to the REFSx
bits). The pin number is ANDed with 0x07 for safety and ORed into ADMUX. Note that the result will be
right-justified as ADLAR is not set.
ADMUX = (analog_reference << 6) | (pin & 0x07);
The conversion is initiated by setting the AD Start Conversion bit. sbi() is a macro that reduces to a
single set bit instruction in assembly. We then wait until the ADSC bit is cleared which signifies the
conversion is complete. The while loop is referred to properly as a busy-wait loop; the code simply
sits on that bit, checking it over and over until it is finally cleared. This causes the loop to exit. Again,
bit_is_set() is a macro that returns true if the bit under test is set to one and false if its clear.
// start the conversion
sbi(ADCSRA, ADSC);
// ADSC is cleared when the conversion finishes
while ( bit_is_set(ADCSRA, ADSC) );
The conversion is now complete. ADCL must be read first as doing so locks both the ADCL and ADCH
registers until ADCH is read. Doing the reverse could result in spurious values. The two bytes are then
combined into a single 16 bit integer result by ORing the low byte with the left-shifted high byte. As the
data in the result registers were right-justified, the top six bits will be zeros in the returned integer,
resulting in an output range of 0 through 1023.
low = ADCL;
high = ADCH;
// combine the two bytes
return (high << 8) | low;
Given the default prescaler values and function call overhead, the maximum conversion rate is
approximately 10 kHz which is perfectly acceptable for a wide variety of uses. The function is fairly barebones and straight forward to use as is. Use this function as a guide if you wish to produce left-justified
data (set ADLAR bit) or other simple modifications. If time is of the essence and machine cycles cannot be
wasted with the busy-wait loop seen above, an interrupt-based version or free-running version may be a
better choice.
140
Embedded Controllers
need to be mapped onto a range of desired numeric output values. Sometimes this can be done with
hardware but other times a simple mathematical process will suffice.
For example, suppose we have a sensor that well use for measuring the temperature of liquid water at
standard pressure. The specifications indicate that the unit will produce one volt at freezing (32 degrees
Fahrenheit) and three volts at boiling (212 degrees Fahrenheit). Assuming that were using the default
reference which yields an input range of zero to five volts, well only be using 40% of the available
dynamic range (i.e., two volts out of five). To make full use of the ten bit ADC, wed need to run the
sensor through a signal conditioning circuit to map the two volt output range onto the five volt input
range. This would require an amplifier with a voltage gain of 2.5 and an output level shift of one volt.
That is, we subtract one volt from the sensor output and then multiply the result by 2.5. By doing so, the
32 degree output of one volt will produce zero volts and the 212 degree output of three volts will produce
five volts. A modest op amp or discrete transistor stage can be designed to achieve this. At the ADC input
the total range of 180 degrees will be divided up into 1024 levels, yielding a resolution of better than .2
degrees per step. Note that the numeric values from the ADC are not the temperature values. A
temperature of 32 degrees yields an ADC output of zero while 212 degrees produces 1023 from the ADC.
If we need to display the Fahrenheit temperatures, perhaps on a multiplexed seven segment or LCD
display, well need to map these. This can be done with a simple formula that well look at in a moment
or with the Arduino map() function.
If a more modest resolution can be tolerated, a simple software mapping can be used. If we take the
sensor output as is, we have to accept that well only be getting 40% of the available resolution in this
example. That will still give us around one-half degree resolution. Assuming this is sufficient, for the
numeric readout just mentioned, all we need to do is map an input voltage of one volt to a numeric output
of 32 and an input of three volts to a numeric output of 212. The one volt input represents one-fifth of the
ADCs maximum input signal and would produce a value of approximately 205. The three volt input
would produce approximately 614. 205 represents the offset of 32 degrees and the range of 614-205 or
409 needs to represent the temperature range of 180 degrees. We can do this with one small formula:
// av is the value returned from analogRead() and
// dv is the value to be displayed
dv = 32+180*(av-205)/409;
Note that this computation is done entirely with integer math rather than floating point for best possible
speed and minimal compiled code. For very large values of the variable av the product could overflow
an unsigned int so a long computation would be needed. Note that the multiply must be done
before the integer divide or a horrendous loss of precision will occur due to truncation. dv itself could still
be a short (or even an unsigned char), so a little creative casting might yield something like this:
dv = (short)(32+180*(long)(av-205)/409);
As we are using integers, the displayed temperature resolution will be one degree (truncating).
26.3 Exercise
Using a sensor that produces two volts at freezing and four volts at boiling, describe a circuit that will
scale the sensor output to a zero-to-five volt input range along with the code required to display the
associated values in Centigrade (i.e., two volts displays 0 and four volts displays 100).
Embedded Controllers
141
analogWrite()23
Description
Writes an analog value (PWM wave) to a pin. Can be used to light a LED at varying brightnesses or
drive a motor at various speeds. After a call to analogWrite(), the pin will generate a steady square
wave of the specified duty cycle until the next call to analogWrite() (or a call to digitalRead() or
digitalWrite() on the same pin). The frequency of the PWM signal is approximately 490 Hz.
On most Arduino boards (those with the ATmega168 or ATmega328), this function works on pins 3, 5,
6, 9, 10, and 11. On the Arduino Mega, it works on pins 2 through 13. Older Arduino boards with an
ATmega8 only support analogWrite() on pins 9, 10, and 11.
The Arduino Due supports analogWrite() on pins 2 through 13, plus pins DAC0 and DAC1. Unlike the
PWM pins, DAC0 and DAC1 are Digital to Analog converters, and act as true analog outputs.
23
http://arduino.cc/en/Reference/AnalogWrite
142
Embedded Controllers
You do not need to call pinMode() to set the pin as an output before calling analogWrite().
The analogWrite function has nothing to do with the analog pins or the analogRead function.
Syntax
analogWrite( pin, value )
Parameters
pin: the pin to write to.
value: the duty cycle: between 0 (always off) and 255 (always on).
Returns
nothing
See also
analogRead()
analogWriteResolution()
Tutorial: PWM
The bottom line is that you call analogWrite() with just the pin of interest and a duty cycle value that
ranges from 0 (off) to 255 (fully on). Thats all there is to it. But why is the range 0 to 255 instead of the
more obvious 0 to 100 percent? Lets take a look at the code, as usual slightly cleaned up for your
convenience (the original code may be found in the file wiring_analog.c):
void analogWrite(uint8_t pin, int val)
{
pinMode(pin, OUTPUT);
if (val == 0)
{
digitalWrite(pin, LOW);
}
else
{
if (val == 255)
{
digitalWrite(pin, HIGH);
}
Embedded Controllers
143
else
{
switch( digitalPinToTimer(pin) )
{
case TIMER0A:
// connect pwm to pin on timer 0, channel A
sbi(TCCR0A, COM0A1);
OCR0A = val; // set pwm duty
break;
case TIMER0B:
// connect pwm to pin on timer 0, channel B
sbi(TCCR0A, COM0B1);
OCR0B = val; // set pwm duty
break;
// and so on for TIMER1A through TIMER2B
case NOT_ON_TIMER:
default:
if (val < 128)
digitalWrite(pin, LOW);
else
digitalWrite(pin, HIGH);
}
}
}
}
The first thing we see is a call to pinMode() to guarantee that the pin is set up for output. While its
possible to simply require programmers to make this function call themselves before each usage, it is
safer to place it inside the function. Note that if you were only using this pin in this mode, you could
make this call yourself just once as part of your setup routine and make your own version of
analogWrite() having removed this part (and perhaps some other sections) to shave the execution
time.
pinMode(pin, OUTPUT);
The code then does a quick check to see if we want fully on or fully off, in which case it dispenses with
the timer and just calls digitalWrite().
if (val == 0)
{
digitalWrite(pin, LOW);
}
else
{
144
Embedded Controllers
if (val == 255)
{
digitalWrite(pin, HIGH);
}
At this point, a value from 1 to 254 must have been entered. The pin is translated to a timer and, through a
switch/case statement, the appropriate timers control register is activated and the duty cycle value is
loaded into the associated count register. These timers are eight bit units. Therefore they can hold values
between 0 and 255. This is why the duty cycle value runs from 0 to 255 instead of 0 to 100 percent.
(Technically, timer one is a 16 bit unit but is used here with eight.)
switch( digitalPinToTimer(pin) )
{
case TIMER0A:
// connect pwm to pin on timer 0, channel A
sbi(TCCR0A, COM0A1);
OCR0A = val; // set pwm duty
break;
case TIMER0B:
// connect pwm to pin on timer 0, channel B
sbi(TCCR0A, COM0B1);
OCR0B = val; // set pwm duty
break;
// and so on for TIMER1A through TIMER2B
Finally, if analogWrite() is used on a pin that is not configured with a timer, the function does the best
it can, namely calling digitalWrite(). Any value below half (128) will produce a low and any value at
half or above will produce a high:
case NOT_ON_TIMER:
default:
if (val < 128)
digitalWrite(pin, LOW);
else
digitalWrite(pin, HIGH);
It is worth repeating that on Arduino boards that contain an internal DAC such as the Due, this function
will produce true analog output signals (the code for that is not shown).
Embedded Controllers
145
146
Embedded Controllers
First, there are a couple of registers used to program the operation of the block. These are TCCRnA and
TCCRnB (Timer Counter Control Registers A and B) where n is the timer/counter number (0, 1 or 2 here,
although other microcontrollers in the series may have more). These bits are usually set and forget, that
is, they are set/cleared for a given use and not touched again unless the unit needs to be reprogrammed for
a different use. More detail on these will be presented momentarily. There are also two registers that are
used with software interrupts, TIFRn and TIMSKn (Timer Interrupt Flag Register and Timer Interrupt
MaSK register) that arent shown here. We will examine these in the section on software interrupts. The
other key registers are TCNTx (Timer CouNT) along with OCRnA and OCRnB (Output Compare Registers
A and B).
In its most basic operation the unit increments the TCNTn register with each tick of the system clock.
Eventually, this register will reach its maximum value and overflow, resulting in zero, effectively
resetting the register and the count continues up from there. This maximum value is 255 for an eight bit
unit (i.e., 11111111 binary) and 65535 for a 16 bit unit. Optionally, the unit may be programmed to
inspect the values contained in the OCRn registers and compare them to the current contents of TCNTn to
see if they match. Both the overflow and the compare match can be used trigger some action, e.g., a
waveform level change or software interrupt. The compare match section feeds a pulse waveform
generator that in turn feeds an output pin. This section is shown in Figure 28.2.
Embedded Controllers
147
The box marked OCnx Pin is a physical pin on the microcontroller. As it would be impractical to have
single purpose pins for every function in the microcontroller, the waveform function is mapped onto the
general purpose IO pins. Compare this section to the GPIO block diagram discussed in chapter 19. Note
the PORT and DDR flip-flops in particular. The waveform generator feeds a flip flop which then feeds a
multiplexer along with the PORT flip flop. This signal is then fed to a tri-state buffer controlled by the DDR
which leads to the physical output pin. As there are three timer/counters and each has two compare match
registers, there are a total of six physical output pins available for waveform generation. The mapping
assignments are shown in Figure 28.3.
OCnx
PORT.bit
Arduino Pin
OC0A
D.6
OC0B
D.5
OC1A
B.1
OC1B
B.2
10
OC2A
B.3
11
OC2B
D.3
The following discussion will focus on timer/counter number two. This is because timer/counter units
zero and one are already used for common Arduino system calls such as delay(). Reprogramming the
timer/counters could alter the operation of those functions and procedures. Timer/counter number two is a
little safer to experiment with in that regard although be aware that all three units are used to cover the six
PWM capable output pins on the Uno (i.e., via analogWrite()).
The two control registers (TCCRnA/B) indicate the mode of operation. The registers are shown in Figures
28.4 and 28.5 for unit number two. The control registers for the other timer/counter units are similar and
details may be found in the Appendix.
Bit
WGM21
WGM20
Bit
Function
FOC2A
FOC2B
WGM22
CS22
CS21
CS20
148
Embedded Controllers
The first items of interest are the CS bits. These are the Clock preScale bits and they allow for longer
timer periods by dividing down the system clock before it feeds the counter. For example, if the controller
is running at 16 MHz then each clock tick represents 1/16 MHz or 62.5 nanoseconds. Counting up from 0
to 255 would yield a maximum delay of only 62.5 nanoseconds times 256, or 16 microseconds. If the
prescaler is set to eight, each tick is stretched by a factor of eight yielding a maximum delay of 128
microseconds in this example. The prescaler settings for TC2 are shown in Figure 28.6.
CS22
CS21
CS20
Divide By
TC Stopped
1 (no prescale)
32
64
128
256
1024
Turning to the Waveform Generation Mode (WGM) bits, there are normal mode, a compare mode (CTC or
Clear Timer on Compare) and two forms of pulse width modulation. We will examine Normal and Fast
PWM modes here and CTC mode in the chapter covering interrupts. Note that the WGM bits are spread
across both control registers rather than residing in a single register. The WGM bits are detailed in Figure
28.7.
WGM22
WGM21
WGM20
Mode
Normal
CTC
Reserved
Reserved
Embedded Controllers
149
Finally lets consider the Compare Output Mode (COM) bits found in TCCR2A. There are two bits for OC2A
and two for OC2B, and although the settings are similar they are not identical (for details, see the Atmel
documentation). The settings for OC2A are found in Figures 28.8 through 28.10 for three waveform
generation modes and describe how the output pin signal responds. Note that in all three of these modes if
both COM bits are clear then the output pin, OC2A, is disconnected from the timer/counter and operates in
ordinary IO form. With other settings of the COM bits, OC2A is controlled by the waveform generator. It is
important to note that the associated DDR must still be set for output mode in order for the waveform to
reach the output pin. Examine the block diagram of Figure 28.2 for verification.
COM2A1
COM2A0
Description
COM2A1
COM2A0
Description
COM2A1
COM2A0
Description
Figure 28.10, Phase Correct PWM, Compare Output Mode for TC2
150
Embedded Controllers
The setup() function sets the direction for the LED driver pin to output, places TC2 into normal mode
and sets the clock prescaler to 1024. This means that each count will occur at a rate just slower than 16
kHz given the 16 MHz clock of the Uno. The mydelay() function initializes the counters main register
at a predefine value (COUNTOFFSET). It will count from there to 255 after which point it will overflow
and set the TOV2 flag. We ensure that this bit is clear before continuing using the seemingly backward
command TIFR2=1<<TOV2; It first appears that this command should set the flag bit but it does indeed
clear it (as verified in the Atmel documentation). At this point we busy-wait on this bit waiting for it to be
set. In this example it will take 241 counts to overflow or about 15 milliseconds. This process is then
Embedded Controllers
151
iterated x times to achieve a longer and variable delay. Note that larger values of COUNTOFFSET will
yield shorter blink times.
// prescale by 64x
An interesting new twist here is that there is nothing in the main loop() function! Once the
timer/counter is set up it runs without further attention, truly set and forget operation. In this case, the
output frequency will be the main clock of 16 MHz divided by the 64x prescaler and then divided by the
total count of 256. This yields approximately 977 Hz. The duty cycle for output A is (128+1)/256 or
about 50.4%. For output B the result is (100+1)/256 or approximately 39.5%.
152
Embedded Controllers
The preceding code was downloaded to an Arduino Uno and an oscilloscope was attached to the two
output pins. The resulting waveforms are shown in Figure 28.11. Note the relative accuracy of the timing
computations compared to those measured by the oscilloscope.
It is worthwhile to note that the values of the output compare registers could be changed
programmatically, for example via a potentiometer connected to an analog input pin. This would
effectively recreate an alternate form of the analogWrite() function.
Embedded Controllers
153
154
Embedded Controllers
Embedded Controllers
155
After setting the IO pins, the code sets the INT0 bit in the EIMSK register to enable the interrupt. The
External Interrupt Control Register (EICRA) is programmed to trigger on a falling (negative going) edge.
When the switch is activated, the falling edge triggers the ISR to run. The ISR simply toggles the bit that
the LED is tied to. Note that there is no port setting and pin examination in the main code. In fact,
loop() is empty and does nothing.
Among other things, an external interrupt such as this is very useful for high importance items such as a
panic switch.
156
Embedded Controllers
#define ISR_COUNT 50
void setup()
{
DDRB |= LEDMASK;
TCCR2A = 0;
// normal mode, OC2x pin disconnected
TCCR2B = 0x07; // 1024x prescale
TIMSK2 = (1<<TOIE2);
}
ISR(TIMER2_OVF_vect)
{
if(!g_time)
PORTB ^= LEDMASK;
g_time++;
if( g_time >= ISR_COUNT )
g_time = 0;
}
void loop()
{
}
Timer/counter 2 is used here in normal mode meaning that it will count from 0 to 255 and then overflow
and repeat. The prescaler is set for 1024x, producing ticks at about 16 kHz. The overflow interrupt for this
counter is then enabled. This triggers the TIMER2_OVF_vect.
A full count from 0 to 255 will create an overflow at a rate of slightly over 60 Hz. This means that the
LED will have a single on/off cycle at a rate of about 30 per second which is a little too fast for the naked
eye. Therefore we cant simply toggle the LED as it will always appear on. Instead, we make use of a
global variable, g_time, that will keep track of how many times the interrupt has occurred. Once it hits
our predetermined maximum, ISR_COUNT, it will be reset and the LED toggled. We now have a blinking
LED that is completely hands off as far as the main code flow is concerned. Again, note that loop() is
empty. This will continue to be the case in the remaining examples.
Embedded Controllers
157
The code is very similar to that of the prior example with the addition of the alteration of the TCNT2 count
register. Each interrupt causes the pin to toggle, however, unlike the previous example the count register
is set to a new starting value which shortens the count time (i.e., the time to overflow). Further, the reset
value is dependent on whether the current output level is high or low.
158
Embedded Controllers
OCR2A = COMPARE_MATCH;
TIMSK2 = (1<<OCIE2A); // enable compare match interrupt
}
ISR(TIMER2_COMPA_vect)
{
PORTB ^= ARBPINMASK;
}
void loop()
{
}
Note that a new bit must be set in the timer/counter interrupt mask register, namely OCIE2A (Output
Compare Interrupt Enable for unit 2A). The Output Compare Register (OCR2A) is set with a fixed value
but again, this could be set by a variable. A COMPARE_MATCH value of 128 yields a square wave
frequency of slightly more than 60 Hz. Halving the value to 64 will roughly double the frequency to about
120 Hz.
Embedded Controllers
159
Appendix A
ATmega 328P Register Map
Derived from the October, 2014 version of the Atmel 328P documentation which may
be found at http://www.atmel.com/devices/ATMEGA328P.aspx
Address
Name
Bit 7
Bit 6
Bit 5
Bit 4
Bit 3
Bit 2
Bit 1
Bit 0
0x23
PINB
PINB7
PINB6
PINB5
PINB4
PINB3
PINB2
PINB1
PINB0
0x24
DDRB
DDB7
DDB6
DDB5
DDB4
DDB3
DDB2
DDB1
DDB0
0x25
PORTB
PORTB7
PORTB6
PORTB5
PORTB4
PORTB3
PORTB2
PORTB1
PORTB0
0x26
PINC
PINC6
PINC5
PINC4
PINC3
PINC2
PINC1
PINC0
0x27
DDRC
DDC6
DDC5
DDC4
DDC3
DDC2
DDC1
DDC0
0x28
PORTC
PORTC6
PORTC5
PORTC4
PORTC3
PORTC2
PORTC1
PORTC0
0x29
PIND
PIND7
PIND6
PIND5
PIND4
PIND3
PIND2
PIND1
PIND0
0x2A
DDRD
DDD7
DDD6
DDD5
DDD4
DDD3
DDD2
DDD1
DDD0
0x2B
PORTD
PORT7
PORT6
PORTD5
PORTD4
PORTD3
PORTD2
PORTD1
PORTD0
0x2C-34
Reserved
0x35
TIFR0
OCF0B
OCF0A
TOV0
0x36
TIFR1
ICF1
OCF1B
OCF1A
TOV1
0x37
TIFR2
OCF2B
OCF2A
TOV2
0x38-3A
Reserved
0x3B
PCIFR
PCIF2
PCIF1
PCIF0
0x3C
EIFR
INT1
INT0
0x3D
EIMSK
INTF1
INTF0
0x3E
GPIOR0
0x3F
EECR
EEMPE
EEPE
EERE
EEPM1
EEPM0
EERIE
0x40
EEDR
0x41
EEARL
0x42
EEARH
0x43
GTCCR
TSM
PSRASY
PSRSYNC
0x44
TCCR0A
COM0A1
COM0A0
COM0B1
COM0B0
WGM01
WGM00
0x45
TCCR0B
FOC0A
FOC0B
WGM02
CS02
CS01
CS00
0x46
TCNT0
Timer/Counter0 (8-bit)
0x47
OCR0A
0x48
OCR0B
0x49
Reserved
0x4A
GPIOR1
0x4B
GPIOR2
0x4C
SPCR
SPIE
SPE
DORD
MSTR
CPOL
CPHA
SPR1
SPR0
0x4D
SPSR
SPIF
WCOL
SPI2X
160
Embedded Controllers
0x4E
SPDR
0x4F
Reserved
0x50
ACSR
ACD
ACBG
ACO
ACI
ACIE
ACIC
ACIS1
ACIS0
0x51-52
Reserved
0x53
SMCR
SM2
SM1
SM0
SE
0x54
MCUSR
WDRF
BORF
EXTRF
PORF
0x55
MCUCR
BODS
BODSE
PUD
IVCEL
IVCE
0x56
Reserved
0x57
SPMCSR
SPMIE
RWWSB
SIGRD
RWWSRE
BLBSET
PGWRT
PGERS
SPMEN
0x58-5C
Reserved
0x5D
SPL
SP7
SP6
SP5
SP4
SP3
SP2
SP1
SP0
0x5E
SPH
SP10
SP9
SP8
0x5F
SREG
0x60
WDTCSR
WDIF
WDIE
WDP3
WDCE
WDE
WDP2
WDP1
WDP0
0x61
CLKPR
CLKPCE
CLKPS3
CLKPS2
CLKPS1
CLKPS0
0x62-63
Reserved
0x64
PRR
PRTWI
PRTM2
PRTM0
PRTM1
PRSPI
PRUSART0
PRADC
0x65
Reserved
0x66
OSCCAL
0x67
Reserved
0x68
PCICR
PCIE2
PCIE1
PCIE0
0x69
EICRA
ISC11
ISC10
ISC01
ISC00
0x6A
Reserved
0x6B
PCMSK0
PCINT7
PCINT6
PCINT5
PCINT4
PCINT3
PCINT2
PCINT1
PCINT0
0x6C
PCMSK1
PCINT14
PCINT13
PCINT12
PCINT11
PCINT10
PCINT9
PCINT8
0x6D
PCMSK2
PCINT23
PCINT22
PCINT21
PCINT20
PCINT19
PCINT18
PCINT17
PCINT16
0x6E
TIMSK0
OCIE0B
OCIE0A
TOIE0
0x6F
TIMSK1
ICIE1
OCIE1B
OCIE1A
TOIE1
0x70
TIMSK2
OCIE2B
OCIE2A
TOIE2
0x71-77
Reserved
0x78
ADCL
0x79
ADCH
0x7A
ADCSRA
ADEN
ADSC
ADATE
ADIE
ADPS2
ADPS1
ADPS0
0x7B
ADCSRB
ACME
ADPT2
ADPT1
ADPT0
0x7C
ADMUX
REFS1
REFS0
ADLAR
MUX3
MUX2
MUX1
MUX0
0x7D
Reserved
0x7E
DIDR0
ADC5D
ADC4D
ADC3D
ADC2D
ADC1D
ADC0D
0x7F
DIDR1
AIN1D
AIN0D
0x80
TCCR1A
COM1A1
COM1A0
COM1B1
COM1B0
WGM11
WGM10
0x81
TCCR1B
ICNC1
ICES1
WGM13
WGM12
CS12
CS11
CS10
0x82
TCCR1C
FOC1A
FOC1B
0x83
Reserved
0x84
TCNT1L
Embedded Controllers
161
0x85
TCNT1H
0x86
ICR1L
0x87
ICR1H
0x88
OCR1AL
0x89
OCR1AH
0x8A
OCR1BL
0x8B
OCR1BH
8C-AF
Reserved
0xB0
TCCR2A
COM2A1
COM2A0
COM2B1
COM2B0
WGM21
WGM20
0xB1
TCCR2B
FOC2A
FOC2B
WGM22
CS22
CS21
CS20
0xB2
TCNT2
Timer/Counter2 (8-bit)
0xB3
OCR2A
0xB4
OCR2B
0xB5
Reserved
0xB6
ASSR
EXCLK
AS2
TCN2UB
OCR2AUB
OCR2BUB
TCR2AUB
TCR2BUB
0xB7
Reserved
0xB8
TWBR
0xB9
TWSR
TWS7
TWS6
TWS5
TWS4
TWS3
TWPS1
TWPS0
0xBA
TWAR
TWA6
TWA5
TWA4
TWA3
TWA2
TWA1
TWA0
TWGCE
0xBB
TWDR
0xBC
TWCR
TWINT
TWEA
TWSTA
TWSTO
TWWC
TWEN
TWIE
0xBD
TWAMR
TWAM6
TWAM5
TWAM4
TWAM3
TWAM2
TWAM1
TWAM0
0xBE
Reserved
0xBF
Reserved
0xC0
UCSR0A
RXC0
TXC0
UDRE0
FE0
DOR0
UPE0
U2X0
MPCM0
0xC1
UCSR0B
RXCIE0
TXCIE0
UDRIE0
RXEN0
TXEN0
UCSZ02
RXB80
TXB80
UPM00
USBS0
UCSZ01 /
UDORD0
UCSZ00 /
UCPHA0
UCPOL0
0xC2
UCSR0C
UMSEL01
UMSEL00
UPM01
0xC3
Reserved
0xC4
UBRR0L
0xC5
UBBR0H
0xC6
UDR0
0xC7-FF
Reserved
162
Embedded Controllers
Appendix B
Answers to Selected Problems
Chapter 3
1.
/* Ralph Johnson, June 31, 2112 */
// Ralph Johnson, June 31, 2112
3.
#include <stdio.h>
int main(void)
{
printf("Ralph Johnson");
}
Chapter 4
1.
printf("The result is %f volts", output_voltage);
3.
int x;
x = sizeof(POWER_SUPPLY);
5.
Y = Y|0x01;
7.
W = ~W;
Chapter 7
1.
if(X<0)
printf("negative value");
3.
// This assumes the variables are integers
if(X>Y)
printf("%d",X);
else
printf("%d",Y);
5.
for(i=0;i<6;i++)
printf("Error!");
Embedded Controllers
163
7.
for(R=100;R<=200;R+=5)
Chapter 8
1.
float *fptr;
3. c is an unsigned character, an eight bit (one byte) variable. p is a pointer to an unsigned character
meaning that it contains the address of a variable which is an unsigned character (such as c). p could be
two, four or eight bytes in size, depending on the operating system.
5. & is the address of operator. It returns the address of the associated variable. * is the dereferencing
operator. It returns the value which is at the memory address referenced by the associated variable.
7. This multiplies b by the value referenced by c (i.e., c is a pointer). It may be more clear to show the
pointer dereference explicitly:
a = b * (*c);
Chapter 10
1.
// structure definition
struct Quest {
float X;
long Y;
unsigned char Z;
};
// declaration of an instance
struct Quest Grail;
Chapter 12
1.
char *x;
x = malloc(1000);
3.
free(x);
free(f);
164
Embedded Controllers
Index
74HC14 127
ADC, 54, 75, 78, 94, 107, 132-140
Arduino home, 4, 8
ARM Coretex, 112
ASCII, 14, 36, 39, 72, 112
auto class variable, 12, 32
Biscotti, 85
Bitwise operators, 27, 29, 41, 92
Boolean, 41, 91
Bus, 12, 80, 100, 104
Byte, 10, 15
C Compiler (free), 8
Cast, 26, 95, 96
char keyword, 11, 14, 15, 32, 36
CISC, 78, 80
const keyword, 30, 33, 110
CTC mode, 158
DAC, 53, 112, 132, 134, 142, 145
DDR (Data Direction Register), 27-29, 92, 100, 104, 104-112
debounce, 127
double (float) keyword, 11, 14, 15, 25, 26, 48, 49, 52
DRAM (Dynamic RAM), 81
EEPROM, 81, 82, 85
extern keyword, 30, 34
Flip-Flop, 81, 100, 104
float keyword, 15, 25, 26, 30
Global declaration, 11, 18, 21, 33, 34
Global Interrupt Enable, 80, 115, 121
GPIO, 78, 98, 99, 102
Harvard architecture, 12, 78, 79, 85, 110
Header files, 19, 20, 28, 34, 39, 43, 57, 64, 90-96
Hex (hexadecimal), 24, 28, 69, 70
Include files, see Header files
int keyword (long vs. short), 14, 25, 26
ISR (interrupt service routine), 154-159
LED, 29, 74, 100, 112, 113, 117, 124, 142, 156
Embedded Controllers
165
166
Embedded Controllers