Chapter 1: Program Structure
What's in Chapter 1?
- A sample program introduces C
- C is a free field language
- Precedence of the operator determines the order of operation
- Comments are used to document the software
- Prepreocessor directives are special operations that occur first
- Global declarations provide modular building blocks
- Declarations are the basic operations
- Function declarations allow for one routine to call another
- Compound statements are the more complex operations
- Global variables are permanent and can be shared
- Local variables are temporary and are private
- Source files make it easier to maintain large projects
This chapter gives a basic overview of programming in C for an
embedded system. We will introduce some basic terms so that you
get a basic feel for the language. Since this is just the first
of many chapters it is not important yet that you understand fully
the example programs. The examples are included to illustrate
particular features of the language.
Case Study 1: Microcomputer-Based Lock
To illustrate the software development process, we will implement a simple digital lock. The lock system has 7 toggle switches and a solenoid as shown in the following figure. If the 7-bit binary pattern on Port A bits 6-0 becomes 0100011 for at least 10 ms, then the solenoid will activate. The 10 ms delay will compensate for the switch bounce. For information on switches and solenoids see Chapter 8 of Embedded Microcomputer Systems: Real Time Interfacing by Jonathan W. Valvano. For now what we need to understand is that Port A bits 6-0 are input signals to the computer and Port A bit 7 is an output signal.
To illustrate the software development process, we will implement a simple digital lock. The lock system has 7 toggle switches and a solenoid as shown in the following figure. If the 7-bit binary pattern on Port A bits 6-0 becomes 0100011 for at least 10 ms, then the solenoid will activate. The 10 ms delay will compensate for the switch bounce. For information on switches and solenoids see Chapter 8 of Embedded Microcomputer Systems: Real Time Interfacing by Jonathan W. Valvano. For now what we need to understand is that Port A bits 6-0 are input signals to the computer and Port A bit 7 is an output signal.
Before we write C code, we need to develop a software plan. Software
development is an iterative process. Even though we list steps
the development process in a 1,2,3... order, in reality we iterative
these steps over and over.
1) We begin with a list of the inputs and outputs. We specify the range of values and their significance. In this example we will use PORTA. Bits 6-0 will be inputs. The 7 input signals represent an unsigned integer from 0 to 127. Port A bit 7 will be an output. If PA7 is 1 then the solenoid will activate and the door will be unlocked. In assembly language, we use #define MACROS to assign a symbolic names, PORTA DDRA, to the corresponding addresses of the ports, $0000 $0002.
#define PORTA *(unsigned char volatile *)(0x0000)
#define DDRA *(unsigned char volatile *)(0x0002)
2) Next, we make a list of the required data structures. Data structures are used to save information. If the data needs to be permanent, then it is allocates in global space. If the software will change its value then it will be allocated in RAM. In this example we need a 16-bit unsigned counter.
unsigned int cnt;
If data structure can be defined at compile time and will remain
fixed, then it can be allocated in EEPROM. In this example we
will define an 8 bit fixed constant to hold the key code, which
the operator needs to set to unlock the door. The compiler will
place these lines with the program so that they will be defined
in ROM or EEPROM memory.
const unsigned char key=0x23; // key code
It is not real clear at this point exactly where in EEPROM this
constant will be, but luckily for us, the compiler will calculate
the exact address automatically. After the program is compiled,
we can look in the listing file or in the map file to see where
in memory each structure is allocated.
3) Next we develop the software algorithm, which is a sequence of operations we wish to execute. There are many approaches to describing the plan. Experienced programmers can develop the algorithm directly in C language. On the other hand, most of us need an abstractive method to document the desired sequence of actions. Flowcharts and pseudo code are two common descriptive formats. There are no formal rules regarding pseudo code, rather it is a shorthand for describing what to do and when to do it. We can place our pseudo code as documentation into the comment fields of our program. The following shows a flowchart on the left and pseudo code and C code on the right for our digital lock example.
3) Next we develop the software algorithm, which is a sequence of operations we wish to execute. There are many approaches to describing the plan. Experienced programmers can develop the algorithm directly in C language. On the other hand, most of us need an abstractive method to document the desired sequence of actions. Flowcharts and pseudo code are two common descriptive formats. There are no formal rules regarding pseudo code, rather it is a shorthand for describing what to do and when to do it. We can place our pseudo code as documentation into the comment fields of our program. The following shows a flowchart on the left and pseudo code and C code on the right for our digital lock example.
Normally we place the programs in ROM or EEPROM. Typically, the
compiler will initialize the stack pointer to the last location
of RAM. On the 6812, the stack is initialized to 0x0C00. Next
we write C code to implement the algorithm as illustrated in the
above flowchart and pseudo code.
4) The last stage is debugging. For information on debugging
see Chapter 2 of Embedded Microcomputer Systems: Real Time Interfacing by Jonathan W. Valvano.
Let's begin with a small program. This simple program is typical
of the operations we perform in an embedded system. This program
will read 8 bit data from parallel port C and transmit the information
in serial fashion using the SCI, serial communication interface.
The numbers in the first column are not part of the software,
but added to simplify our discussion.
1 /* Translates parallel input data to serial outputs */
2 #define PORTC *(unsigned char volatile *)(0x1003)
3 #define DDRC *(unsigned char volatile *)(0x1007)
4 #define BAUD *(unsigned char volatile *)(0x102B)
5 #define SCCR2 *(unsigned char volatile *)(0x102D)
6 #define SCSR *(unsigned char volatile *)(0x102E)
7 #define SCDR *(unsigned char volatile *)(0x102F)
8 void OpenSCI(void) {
9 BAUD = 0x30; /* 9600 baud */
10 SCCR2 = 0x0C;} /* enable SCI, no interrupts */
11 #define TDRE 0x80
12 /* Data is 8 bit value to send out serial port */
13 void OutSCI(unsigned char Data){
14 while ((SCSR & TDRE) == 0); /* Wait for TDRE to be set
*/
15 SCDR = Data; } /* then output */
16 void main(void){ unsigned char Info;
17 OpenSCI(); /* turn on SCI serial port */
18 DDRC = 0x00; /* specify Port C as input */
19 while(1){
20 Info = PORTC; /* input 8 bits from parallel port C */
21 OutSCI(Info);}} /* output 8 bits to serial port */
22
extern void _start(); /* entry point in crt11.s */
23
#pragma abs_address:0xfffe
24
void (*reset_vector[])() ={_start};
25
#pragma end_abs_address
Listing 1-1: Sample ICC11 Program
The first line of the program is a comment giving a brief description of its function. Lines 2 through 7
define macros that provide programming access to I/O ports of the 6811. These
macros specify the format (unsigned 8 bit) and address (the Freescale
microcomputers employ memory mapped I/O). The #define invokes the preprocessor that replaces each instance of PORTC with *(unsigned char volatile *)(0x1003). For more information see the section on macros in the preprocessor chapter.
Lines 8,9,10 define a function or procedure that when executed will initialize the SCI port.
The assignment statement is of the form
address=data;
In particular line 9 (BAUD=0x30;
) will output a hexadecimal $30 to I/O configuration register
at location $102B. Similarly line 10 will output a hexadecimal
$0C to I/O configuration register at location $102D. Notice that
comments can be added virtually anywhere in order to clarify the
software function. OpenSCI is an example of a function that is executed only once at the
beginning of the program. Another name for an initialization function
is ritual.
Line 11 is another #define that specifies the transmit data ready empty (TDRE) bit as bit
7. This #define illustrates the usage of macros that make the software more readable.
Line 12 is a comment Lines 13,14,15 define another function, OutSCI, having an 8 bit input parameter that when executed will output
the data to the SCI port. In particular line 14 will read the
SCI status register at $102E over and over again until bit 7 (TDRE)
is set. Once TDRE is set, it is safe to start another serial output
transmission. This is an example of Gadfly or I/O polling. Line
15 copies the input parameter, Data, to the serial port starting
a serial transition. Line 15 is an example of an I/O output operation.
Lines 16 through 21 define the main program. After some brief
initialization this is where the software will start after a reset
or after being powered up. The sequence unsigned char Info in line 16 will define a local variable. Notice that the size
(char means 8 bit), type (unsigned) and name (Info) are specified. Line 17 calls the ritual function OpenSCI. Line 8 writes a 0 to the I/O configuration register at $1007,
specifying all 8 bits of PORTC will be inputs (writing ones to
a direction register specifies the current bits as outputs.) The
sequence while(1){ } defines a control structure that executes forever and never finishes.
In particular lines 20 and 21 are repeated over and over without
end. Most software on embedded systems will run forever (or until
the power is removed.) Line 20 will read the input port C and
copy the voltage levels into the variable Info. This is an example
of an I/O input operation. Each of the 8 lines that compose PORTC
corresponds to one of the 8 bits of the variable Info. A digital
logic high, voltage above +2V, is translated into a 1. A digital
logic low, voltage less than 0.7V) is translated into a 0. Line
21 will execute the function OutSCI that will transmit the 8 bit data via the SCI serial port.
With ICC11/ICC12 lines 22 through 25 define the reset vector so
that execution begins at the _start location. With Metrowerks, we would delete lines 22-25, and specify
the reset vector in the linker file, *.prm. With both the Metrowerks
and ImageCraft compilers, the system will initialize then jump
to the main program.
Free field language
In most programming languages the column position and line number
affect the meaning. On the contrary, C is a free field language.
Except for preprocessor lines (that begin with #, see Chapter 11), spaces, tabs and line breaks have the same meaning. The other
situation where spaces, tabs and line breaks matter is string
constants. We can not type tabs or line breaks within a string
constant. For more information see the section on strings in the constants chapter. This means we can place more than one statement on a single
line, or place a single statement across multiple lines. For example
the function OpenSCI could have been written without any line breaks
void OpenSCI(void){BAUD=0x30;SCCR2=0x0C;}
"Since we rarely make hardcopy printouts of our software, it is
not necessary to minimize the number of line breaks."
Similarly we could have added extra line breaks
void OpenSCI(void)
{
BAUD=
0x30;
SCCR2=
0x0C;
}
At this point I will warn the reader, just because C allows such
syntax, it does not mean it is desirable. After much experience
you will develop a programming style that is easy to understand.
Although spaces, tabs, and line breaks are syntactically equivalent,
their proper usage will have a profound impact on the readability
of your software. For more information on programming style see
chapter 2 of Embedded Microcomputer Systems: Real Time Interfacing by Jonathan W. Valvano, Brooks/Cole Publishing Co.,
2006.
A token in C can be a user defined name (e.g., the variable
Info
and function OpenSCI
) or a predefined operation (e.g., * unsigned while
). Each token must be contained on a single line. We see in the
above example that tokens can be separated by white spaces (space,
tab, line break) or by the special characters, which we can subdivide
into punctuation marks (Table 1-1) and operations (Table 1-2).
Punctuation marks (semicolons, colons, commas, apostrophes, quotation marks, braces, brackets, and parentheses) are very important in C. It is one of the most frequent sources
of errors for both the beginning and experienced programmers.punctuation | Meaning |
; |
End of statement |
: |
Defines a label |
, |
Separates elements of a list |
( ) |
Start and end of a parameter list |
{ } |
Start and stop of a compound statement |
[ ] |
Start and stop of a array index |
" " |
Start and stop of a string |
' ' |
Start and stop of a character constant |
Table 1-1: Special characters can be punctuation marks
The next table shows the single character operators. For a description
of these operations, see Chapter 5.
operation | Meaning |
= |
assignment statement |
@ |
address of |
? |
selection |
< |
less than |
> |
greater than |
! |
logical not (true to false, false to true) |
~ |
1's complement |
+ |
addition |
- |
subtraction |
* |
multiply or pointer reference |
/ |
divide |
% |
modulo, division remainder |
| |
logical or |
& |
logical and, or address of |
^ |
logical exclusive or |
. |
used to access parts of a structure |
Table 1-2: Special characters can be operators
The next table shows the operators formed with multiple characters.
For a description of these operations, see Chapter 5.
operation | Meaning |
== |
equal to comparison |
<= |
less than or equal to |
>= |
greater than or equal to |
!= |
not equal to |
<< |
shift left |
>> |
shift right |
++ |
increment |
-- |
decrement |
&& |
Boolean and |
|| |
Boolean or |
+= |
add value to |
-= |
subtract value to |
*= |
multiply value to |
/= |
divide value to |
|= |
or value to |
&= |
and value to |
^= |
exclusive or value to |
<<= |
shift value left |
>>= |
shift value right |
%= |
modulo divide value to |
-> |
pointer to a structure |
Table 1-3: Multiple special characters also can be operators
Although the operators will be covered in detail in Chapter 9,
the following section illustrates some of the common operators.
We begin with the assignment operator. Notice that in the line
x=1;
x is on the left hand side of the = . This specifies the address
of x is the destination of assignment. On the other hand, in the
line z=x;
x is on the right hand side of the = . This specifies the value
of x will be assigned into the variable z. Also remember that
the line z=x;
creates two copies of the data. The original value remains in
x, while z also contains this value.short x,y,z; /* Three variables */
void Example(void){
x = 1; /* set the value of x to 1 */
y = 2; /* set the value of y to 2 */
z = x; /* set the value of z to the value of x (both are
1) */
x = y = z = 0; /* all all three to zero */
}
Listing 1-2: Simple program illustrating C arithmetic operators
Next we will introduce the arithmetic operations addition, subtraction,
multiplication and division. The standard arithmetic precedence
apply. For a detailed description of these operations, see Chapter 5.
short x,y,z; /* Three variables */
void Example(void){
x=1; y=2; /* set the values of x and y */
z = x+4*y; /* arithmetic operation */
x++; /* same as x=x+1; */
y--; /* same as y=y-1; */
x = y<<2; /* left shift same as x=4*y; */
z = y>>2; /* right shift same as x=y/4; */
y += 2; /* same as y=y+2; */
}
Listing 1-3: Simple program illustrating C arithmetic operators
Next we will introduce a simple conditional control structure.
PORTB is an output port, and PORTE is an input port on the 6811.
For more information on input/output ports see chapter 3 of Embedded Microcomputer Systems: Real Time Interfacing by Jonathan W. Valvano, Brooks/Cole Publishing Co., 1999. The
expression
PORTE&0x04
will return 0 if PORTE bit 2 is 0 and will return a 4 if PORTE
bit 2 is 1. The expression (PORTE&0x04)==0
will return TRUE if PORTE bit 2 is 0 and will return a FALSE
if PORTE bit 2 is 1. The statement immediately following the if
will be executed if the condition is TRUE. The else
statement is optional. #define PORTB *(unsigned char volatile *)(0x1004)
#define PORTE *(unsigned char volatile *)(0x100A)
void Example(void){
if((PORTE&0x04)==0){ /* test bit 2 of PORTE */
PORTB = 0;} /* if PORTE bit 2 is 0, then make PORTB=0 */
else{
PORTB = 100;} /* if PORTE bit 0 is not 0, then make PORTB=100
*/
}
Listing 1.4: Simple program illustrating the C if else control
structure
PORTA bit 3 is another output pin on the 6811. Like the
if
statement, the while
statement has a conditional test (i.e., returns a TRUE/FALSE).
The statement immediately following the while
will be executed over and over until the conditional test becomes
FALSE. #define PORTA *(unsigned char volatile *)(0x1000)
#define PORTB *(unsigned char volatile *)(0x1004)
void Example(void){ /* loop until PORTB equals 200 */
PORTB = 0;
while(PORTB!=200){
PORTA = PORTA^0x08;} /* toggle PORTA bit 3 output */
PORTB++;} /* increment PORTB output */
}
Listing 1.5: Simple program illustrating the C while control structure
The
for
control structure has three parts and a body. for(part1;part2;part3){body;}
The first part PORTB=0
is executed once at the beginning. Then the body PORTA = PORTA^0x08;
is executed, followed by the third part PORTB++
. The second part PORTB!=200
is a conditional. The body and third part are repeated until
the conditional is FALSE. For a more detailed description of the
control structures, see Chapter 6.#define PORTB *(unsigned char volatile *)(0x1004)
void Example(void){ /* loop until PORTB equals 200 */
for(PORTB=0;PORTB!=200;PORTB++){
PORTA = PORTA^0x08;} /* toggle PORTA bit 3 output */
}
}
Listing 1.6: Simple program illustrating the C for loop control
structure
As with all programming languages the order of the tokens is important.
There are two issues to consider when evaluating complex statements.
The precedence of the operator determines which operations are performed first.
In the following example, the 2*x is performed first because *
has higher precedence than + and =. The addition is performed
second because + has higher precedence than =. The assignment
= is performed last. Sometimes we use parentheses to clarify the
meaning of the expression, even when they are not needed. Therefore,
the line z=y+2*x; could also have been written z=2*x+y; or z=y+(2*x); or z=(2*x)+y;.
short example(short x, short y){ short z;
z = y+2*x;
return(z);
}
The second issue is the associativity. Associativity determines the left to right or right to left
order of evaluation when multiple operations of the precedence
are combined. For example + and - have the same precedence, so
how do we evaluate the following?
z = y-2+x;
We know that + and - associate the left to right, this function
is the same as z=(y-2)+x;. Meaning the subtraction is performed first because it is more
to the left than the addition. Most operations associate left
to right, but the following table illustrates that some operators
associate right to left.
Precedence | Operators | Associativity |
highest | () [] . -> ++(postfix) --(postfix) |
left to right |
++(prefix) --(prefix) !~ sizeof(type) +(unary) -(unary) &(address) *(dereference) |
right to left | |
* / % |
left to right | |
+ - |
left to right | |
<< >> |
left to right | |
< <= > >= |
left to right | |
== != |
left to right | |
& | left to right | |
^ | left to right | |
| | left to right | |
&& | left to right | |
|| | left to right | |
? : | right to left | |
= += -= *= /= %= <<= >>= |= &= ^= |
right to left | |
lowest | , | left to right |
Table 1-4: Precedence and associativity determine the order of
operation
"When confused about precedence (and aren't we all) add parentheses
to clarify the expression."
Comments
There are two types of comments. The first type explains how to
use the software. These comments are usually placed at the top
of the file, within the header file, or at the start of a function.
The reader of these comments will be writing software that uses
or calls these routines. Lines 1 and 12 in the above listing are examples of this type of comment. The second type of comments
assists a future programmer (ourselves included) in changing,
debugging or extending these routines. We usually place these
comments within the body of the functions. The comments on the
right of each line in the above listing are examples of the second type. For more information on writing
good comments see chapter 2 of Embedded Microcomputer Systems: Real Time Interfacing by Jonathan W. Valvano, Brooks/Cole Publishing Co.,
2006.
Comments begin with the
/*
sequence and end with the */
sequence. They may extend over multiple lines as well as exist
in the middle of statements. The following is the same as BAUD=0x30;
BAUD /*specifies transmission rate*/=0x30/*9600 bits/sec*/;
ICC11 and ICC12 do allow for the use of C++ style comments (see
compiler option dialog). The start comment sequence is
//
and the comment ends at the next line break or end of file. Thus,
the following two lines are equivalent:OpenSCI(); /* turn on SCI serial port */
OpenSCI(); // turn on SCI serial port
C does allow the comment start and stop sequences within character
constants and string constants. For example the following string
contains all 7 characters, not just the ac:
const char str[10]="a/*b*/c";
ICC11 and ICC12 unfortunately do not support comment nesting.
This makes it difficult to comment out sections of logic that
are themselves commented. For example, the following attempt to
comment-out the call to
OpenSCI
will result in a compiler error.void main(void){ unsigned char Info;
/*
OpenSCI(); /* turn on SCI serial port */
*/
DDRC = 0x00; /* specify Port C as input */
while(1){
Info =P ORTC; /* input 8 bits from parallel port C */
OutSCI(Info);}} /* output 8 bits to serial port */
The conditional compilation feature can be used to temporarily remove and restore blocks
of code.
Preprocessor Directives
Preprocessor directives begin with
#
in the first column. As the name implies preprocessor commands
are processed first. I.e., the compiler passes through the program
handling the preprocessor directives. Although there are many
possibilities (assembly language, conditional compilation, interrupt
service routines), I thought I'd mention the two most important
ones early in this document. We have already seen the macro definition
(#define) used to define I/O ports and bit fields. A second important
directive is the #include
, which allows you to include another entire file at that position
within the program. The following directive will define all the
6811 I/O port names. #include "HC11.h"
Examples of
#include
are shown below, and more in Chapter 11.
Global Declarations
An object may be a data structure or a function. Objects that
are not defined within functions are global. Objects that may
be declared in ICC11/ICC12/Metrowerks include:
character variables (8 bit signed or unsigned)
arrays of integers or characters
pointers to integers or characters
arrays of pointers
structure (grouping of other objects)
unions (redefinitions of storage)
functions
Both Metrowerks and ICC12 support 32 bit long integers and floating
point. In this document we will focus on 8 and 16 bit objects.
Oddly the object code generated with the these compilers is often
more efficient using 16 bit parameters rather than 8 bit ones.
Declarations and Definitions
It is important for the C programmer two distinguish the two terms
declaration and definition. A function declaration specifies its name, its input parameters
and its output parameter. Another name for a function declaration
is prototype. A data structure declaration specifies its type and format.
On the other hand, a function definition specifies the exact sequence
of operations to execute when it is called. A function definition
will generate object code (machine instructions to be loaded into
memory that perform the intended operations). A data structure
definition will reserve space in memory for it. The confusing
part is that the definition will repeat the declaration specifications.
We can declare something without defining it, but we cannot define
it without declaring it. For example the declaration for the function
OutSCI
could be written asvoid OutSCI(unsigned char);
We can see that the declaration shows us how to use the function,
not how the function works. Because the C compilation is a one-pass
process, an object must be declared or defined before it can be
used in a statement. (Actually the preprocess performs a pass
through the program that handles the preprocessor directives.)
Notice that the function
OutSCI
was defined before it was used in the above listing. The following alternative approach first declares the functions,
uses them, and lastly defines the functions:/* Translates parallel input data to serial outputs */
#define PORTC *(unsigned char volatile *)(0x1003)
#define DDRC *(unsigned char volatile *)(0x1007)
#define BAUD *(unsigned char volatile *)(0x102B)
#define SCCR2 *(unsigned char volatile *)(0x102D)
#define SCSR *(unsigned char volatile *)(0x102E)
#define SCDR *(unsigned char volatile *)(0x102F)
void OpenSCI(void);
void OutSCI(unsigned char);
void main(void){ unsigned char Info;
OpenSCI(); /* turn on SCI serial port */
DDRC = 0x00; /* specify Port C as input */
while(1){
Info = PORTC; /* input 8 bits from parallel port C */
OutSCI(Info);}} /* output 8 bits to serial port */
void OpenSCI(void) {
BAUD = 0x30; /* 9600 baud */
SCCR2 = 0x0C;} /* enable SCI, no interrupts */
/* Data is 8 bit value to send out serial port */
#define TDRE 0x80
void OutSCI(unsigned char Data){
while ((SCSR & TDRE) == 0); /* Wait for TDRE to be set */
SCDR = Data; } /* then output */
Listing 1-7: Alternate ICC11 Program
An object may be said to exist in the file in which it is defined,
since compiling the file yields a module containing the object.
On the other hand, an object may be declared within a file in
which it does not exist. Declarations of data structures are preceded
by the keyword extern. Thus,
short RunFlag;
defines a 16 bit signed integer called
RunFlag
; whereas,extern short RunFlag;
only declares the
RunFlag
to exist in another, separately compiled, module. We will use
external function declarations in the ICC11/ICC12 VECTOR.C file
when we create the reset/interrupt vector table. Thus the lineextern void TOFhandler();
declares the function name and type just like a regular function
declaration. The extern tells the compiler that the actual function exists in another
module and the linker will combine the modules so that the proper
action occurs at run time. The compiler knows everything about
extern objects except where they are. The linker is responsible
for resolving that discrepancy. The compiler simply tells the
assembler that the objects are in fact external. And the assembler,
in turn, makes this known to the linker.
Functions
A function is a sequence of operations that can be invoked from other places
within the software. We can pass 0 or more parameters into a function.
The code generated by the ICC11 and ICC12 compilers pass the first
input parameter in Register D and the remaining parameters are
passed on the stack. A function can have 0 or 1 output parameter.
The code generated by the ICC11 and ICC12 compilers pass the return
parameter in Register D (8 bit return parameters are promoted
to 16 bits.) The
add
function below has two 16 bit signed input parameters, and one
16 bit output parameter. Again the numbers in the first column
are not part of the software, but added to simplify our discussion.1 short add(short x, short y){ short z;
2 z = x+y;
3 if((x>0)&&(y>0)&&(z<0))z=32767;
4 if((x<0)&&(y<0)&&(z>0))z=-32768;
5 return(z);}
6 void main(void){ short a,b;
7 a = add(2000,2000)
8 b = 0
9 while(1){
10 b = add(b,1);
11 }
Listing 1-8: Example of a function call
The interesting part is that after the operations within the function
are performed control returns to the place right after where the
function was called. In C, execution begins with the main program. The execution sequence is shown below:
6 void main(void){ short a,b;
7 a = add(2000,2000); /* call to add*/
1 short add(short x, short y){ short z;
2 z = x+y; /* z=4000*/
3 if((x>0)&&(y>0)&&(z<0))z=32767;
4 if((x<0)&&(y<0)&&(z>0))z=-32768;
5 return(z);} /* return 4000 from call*/
8 b = 0
9 while(1){
10 b = add(b,1); } /* call to add*/
1 short add(short x, short y){ short z;
2 z = x+y; /* z=1*/
3 if((x>0)&&(y>0)&&(z<0))z=32767;
4 if((x<0)&&(y<0)&&(z>0))z=-32768;
5 return(z);} /* return 1 from call*/
11 }
9 while(1){
10 b = add(b,1); } /* call to add*/
1 short add(short x, short y){ short z;
2 z = x+y; /* z=2*/
3 if((x>0)&&(y>0)&&(z<0))z=32767;
4 if((x<0)&&(y<0)&&(z>0))z=-32768;
5 return(z);} /* return 2 from call*/
11 }
Notice that the return from the first call goes to line 8, while
all the other returns go to line 11. The execution sequence repeats
lines 9,10,1,2,3,4,5,11 indefinitely.
The programming language Pascal distinguishes between functions
and procedures. In Pascal a function returns a parameter while
a procedure does not. C eliminates the distinction by accepting
a bare or
void
expression as its return parameter.
C does not allow for the nesting of procedural declarations. In
other words you can not define a function within another function.
In particular all function declarations must occur at the global
level.
A function declaration consists of two parts: a declarator and a body. The declarator states the name of the function and the names
of arguments passed to it. The names of the argument are only
used inside the function. In the add function above, the declarator
is (short x, short y) meaning it has two 16 bit input parameters. ICC11 and ICC12 accept
both approaches for defining the input parameter list. The following
three statements are equivalent:
short add(short x, short y){ return (x+y);}
short add(x,y)short x; short y;{ return (x+y);}
short add(x,y)short x,y;{ return (x+y);}
The parentheses are required even when there are no arguments.
When there are no parameters a
void
or nothing can be specified. The following four statements are
equivalent:void OpenSCI(void){BAUD=0x30;SCCR2=0x0C;}
OpenSCI(void){BAUD=0x30;SCCR2=0x0C;}
void OpenSCI(){BAUD=0x30;SCCR2=0x0C;}
OpenSCI(){BAUD=0x30;SCCR2=0x0C;}
I prefer to include the
void
because it is a positive statement that there are no parameters.
For more information on functions see Chapter 10.
The body of a function consists of a statement that performs the
work. Normally the body is a compound statement between a {} pair.
If the function has a return parameter, then all exit points must
specify what to return. In the following median filter function
shown in Listing 1-4, there are six possible exit paths that all
specify a return parameter.
The programs created by ICC11 and ICC12 actually begin execution
at a place called _start. After a power on or hardware reset, the embedded system will
initialize the stack, initialize the heap, and clear all RAM-based
global variables. After this brief initialization sequence the
function named main() is called. Consequently, there must be a main() function somewhere in the program. If you are curious about what
really happens, look in the assembly file crt11.s or crt12.s.
For programs not in an embedded environment (e.g., running on
your PC) a return from main() transfers control back to the operating system. As we saw earlier,
software for an embedded system usually does not quit. Software
systems developed with Metrowerks also perform initialization before
calling main().
Compound Statements
A compound statement (or block) is a sequence of statements, enclosed by braces, that stands
in place of a single statement. Simple and compound statements
are completely interchangeable as far as the syntax of the C language
is concerned. Therefore, the statements that comprise a compound
statement may themselves be compound; that is, blocks can be nested.
Thus, it is legal to write
// 3 wide 16 bit signed median filter
short median(short n1,short n2,short n3){
if(n1>n2){
if(n2>n3)
return(n2); // n1>n2,n2>n3 n1>n2>n3
else{
if(n1>n3)
return(n3); // n1>n2,n3>n2,n1>n3 n1>n3>n2
else
return(n1); // n1>n2,n3>n2,n3>n1 n3>n1>n2
}
}
else{
if(n3>n2)
return(n2); // n2>n1,n3>n2 n3>n2>n1
else{
if(n1>n3)
return(n1); // n2>n1,n2>n3,n1>n3 n2>n1>n3
else
return(n3); // n2>n1,n2>n3,n3>n1 n2>n3>n1
}
}
}
Listing 1-9: Example of nested compound statements.
Although C is a free-field language, notice how the indenting
has been added to the above example. The purpose of this indenting
is to make the program easier to read. On the other hand since
C is a free-field language, the following two statements are quite
different
if(n1>100) n2=100; n3=0;
if(n1>100) {n2=100; n3=0;}
In both cases
n2=100;
is executed if n1>100
. In the first case the statement n3=0;
is always executed, while in the second case n3=0;
is executed only if n1>100
.
Global Variables
Variables declared outside of a function, like
Count
in the following example, are properly called external variables because they are defined outside of any function. While
this is the standard term for these variables, it is confusing
because there is another class of external variable, one that
exists in a separately compiled source file. In this document
we will refer to variables in the present source file as globals, and we will refer to variables defined in another file as externals.
There are two reasons to employ global variables. The first reason
is data permanence. The other reason is information sharing. Normally
we pass information from one module to another explicitly using
input and output parameters, but there are applications like interrupt
programming where this method is unavailable. For these situations,
one module can store data into a global while another module can
view it. For more information on accessing shared globals see
chapters 4 and 5 of Embedded Microcomputer Systems: Real Time Interfacing by Jonathan W. Valvano, Brooks/Cole Publishing Co., 1999.
In the following example, we wish to maintain a counter of the
number of times
OutSCI
is called. This data must exist for the entire life of the program.
This example also illustrates that with an embedded system it
is important to initialize RAM-based globals at run time. Most
C compilers (including ICC11 ICC12 and Metrowerks) will automatically initialize globals to zero at startup. unsigned short Count; /* number of characters transmitted*/
void OpenSCI(void) {
Count = 0; /* initialize global counter */
BAUD = 0x30; /* 9600 baud */
SCCR2 = 0x0C;} /* enable SCI, no interrupts */
#define TDRE 0x80
void OutSCI(unsigned char Data){
Count = Count+1; /* incremented each time */
while ((SCSR & TDRE) == 0); /* Wait for TDRE to be set */
SCDR=Data; } /* then output */
Listing 1-10: A global variable contains permanent information
Although the following two examples are equivalent, I like the
second case because its operation is more self-evident. In both
cases the global is allocated in RAM, and initialized at the start
of the program to 1.
short Flag = 1;
void main(void) {
/* main body goes here */
}
Listing 1-11: A global variable initialized at run time by the
compiler
short Flag;
void main(void) { Flag=1;
/* main body goes here */
}
Listing 1-12: A global variable initialized at run time by the
compiler
From a programmer's point of view, we usually treat the I/O ports
in the same category as global variables because they exist permanently
and support shared access.
Local Variables
Local variables are very important in C programming. They contain
temporary information that is accessible only within a narrow
scope. We can define local variables at the start of a compound
statement. We call these local variables since they are known only to the block in which they appear,
and to subordinate blocks. The following statement adjusts
x
and y
such that x
contains the smaller number and y
contains the larger one. If a swap is required then the local
variable z
is used.if(x>y){ short z; /* create a temporary variable */
z=x; x=y; y=z; /* swap x and y */
} /* then destroy z */
Notice that the local variable z is declared within the compound
statement. Unlike globals, which are said to be static, locals are created dynamically when their block is entered,
and they cease to exist when control leaves the block. Furthermore,
local names supersede the names of globals and other locals declared
at higher levels of nesting. Therefore, locals may be used freely
without regard to the names of other variables. Although two global
variables can not use the same name, a local variable of one block
can use the same name as a local variable in another block. Programming
errors and confusion can be avoided by understanding these conventions.
Source Files
Our programs may consist of source code located in more than one
file. The simplest method of combining the parts together is to
use the #include preprocessor directive. Another method is to
compile the source files separately, then combine the separate
object files as the program is being linked with library modules.
The linker/library method should be used when the programs are
large, and only small pieces are changed at a time. On the other
hand, most embedded system applications are small enough to use
the simple method. In this way we will compile the entire system
whenever changes are made. Remember that a function or variable
must be defined or declared before it can be used. The following
example is one method of dividing our simple example into multiple
files.
/* ****file HC11.H ************ */
#define PORTC *(unsigned char volatile *)(0x1003)
#define DDRC *(unsigned char volatile *)(0x1007)
#define BAUD *(unsigned char volatile *)(0x102B)
#define SCCR2 *(unsigned char volatile *)(0x102D)
#define SCSR *(unsigned char volatile *)(0x102E)
#define SCDR *(unsigned char volatile *)(0x102F)
Listing 1-13: Header file for 6811 I/O ports
/* ****file SCI11.H ************ */
void OpenSCI(void);
void OutSCI(unsigned char);
Listing 1-14: Header file for the SCI interface
/* ****file SCI11.C ************ */
void OpenSCI(void) {
BAUD=0x30; /* 9600 baud */
SCCR2=0x0C;} /* enable SCI, no interrupts */
/* Data is 8 bit value to send out serial port */
#define TDRE 0x80
void OutSCI(unsigned char Data){
while ((SCSR & TDRE) == 0); /* Wait for TDRE to be set */
SCDR=Data; } /* then output */
Listing 1-15: Implementation file for the SCI interface
/* ****file VECTOR.C ************ */
extern void _start(); /* entry point in crt11.s */
#pragma abs_address:0xfffe
void (*reset_vector[])() ={_start};
#pragma end_abs_address
Listing 1-16: Reset vector on the ICC11
/* ****file MY.C ************ */
/* Translates parallel input data to serial outputs */
#include "HC11.H"
#include "SCI11.H"
void main(void){ unsigned char Info;
OpenSCI(); /* turn on SCI serial port */
DDRC=0x00; /* specify Port C as input */
while(1){
Info=PORTC; /* input 8 bits from parallel port C */
OutSCI(Info);}} /* output 8 bits to serial port */
#include "SCI11.C"
#include "VECTOR.C"
Listing 1-17: Main program file for this system
With Metrowerks, we do not need the VECTOR.C file or the line
#include "VECTOR.C"
. This division is a clearly a matter of style. I make the following
general statement about good programming style.
"If the software is easy to understand, debug, and change, then
it is written with good style"
While the main focus of this document is on C syntax, it would
be improper to neglect all style issues. This system was divided
using the following principles:
For each module place the user-callable prototypes in a *.H header file
For each module place the implementations in a *.C program file
In the main program file, include the header files first
In the main program file, include the implementation files last
Breaking a software system into files has a lot of advantages.
The first reason is code reuse. Consider the code in this example.
If a SCI output function is needed in another application, then
it would be a simple matter to reuse the SCI11.H and SCI11.C files.
The next advantage is clarity. Compare the main program in Listing
1-11 with the entire software system in Listing 1-1. Because the
details have been removed, the overall approach is easier to understand.
The next reason to break software into files is parallel development.
As the software system grows it will be easier to divide up a
software project into subtasks, and to recombine the modules into
a complete system if the subtasks have separate files. The last
reason is upgrades. Consider an upgrade in our simple example
where the 9600 bits/sec serial port is replaced with a high-speed
Universal Serial Bus (USB). For this kind of upgrade we implement
the USB functions then replace the SCI11.C file with the new version.
If we plan appropriately, we should be able to make this upgrade
without changes to the files SCI11.H and MY.C.
No comments:
Post a Comment