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 :
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 bit 6 to bit 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 Sections 4.2 and 8.6.3 of Embedded
Systems: Introduction to ARM Cortex M
Microcontrollers 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 C, we use
#define MACROS to assign a symbolic names to the corresponding
addresses of the ports.
#define
GPIO_PORTA_DATA_R
(*((volatile unsigned long *)0x400043FC))
#define
GPIO_PORTA_DIR_R
(*((volatile unsigned long *)0x40004400))
#define
GPIO_PORTA_DEN_R
(*((volatile unsigned long *)0x4000451C))
#define SYSCTL_PRGPIO
_R
(*((volatile unsigned long *)
0x400FEA08
))
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 32-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 ROM. 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 Flash ROM memory.
const
unsigned char
key=0x23; // The key code 0100011 (binary)
It is not real clear at this
point exactly where in ROM 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.
Normally we place the
programs in Flash ROM. Typically, the
compiler will initialize the stack pointer to the last location of RAM.
On the ARM Cortex M, the stack is initialized to the 32-bit value
located at ROM address 0. 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 Sections 2.6, 4.6, 5.8 and 6.9 of Embedded Systems: Introduction to ARM Cortex M Microcontrollers by Jonathan W. Valvano.
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 a function could have been written without any line breaks
void
Lock_Init(void){ volatile unsigned long
delay; SYSCTL_
PRGPIO
_R
|= 0x01; delay = SYSCTL_
PRGPIO
_R;
GPIO_PORTA_DIR_R = 0x80; GPIO_PORTA_DEN_R =
0xFF; }
"Since we rarely make hardcopy printouts of our software, it is not necessary to minimize the number of line breaks."
However, we could have added extra line breaks and comments to make it more readable.
void
Lock_Init(void){ volatile unsigned long delay;
SYSCTL_PRGPIO
_R
|= 0x01; // activate clock for
Port A
delay = SYSCTL_PRGPIO
_R; //
allow time for clock to start
GPIO_PORTA_DIR_R = 0x80; // set
PA7 to output
and PA6-0 to input
GPIO_PORTA_DEN_R =
0xFF; // enable
digital port
}
At this point I will warn the reader, just because C allows many different forms of syntax, proper syntax will have a profound impact on the quality of our code. 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 Section 5.6 of Embedded Systems: Introduction to ARM Cortex M Microcontrollers by Jonathan W. Valvano.
A token
in C can be a user defined name (e.g., the variable Info
and function Lock_Init
)
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.
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 */
}
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; */
}
Next we will introduce a simple
conditional control structure.
Assume PORTB is configured as an output port, and PORTE as an input
port. For more information on input/output ports see Chapter 4 of Embedded
Systems: Introduction to ARM Cortex M
Microcontrollers by
Jonathan W. Valvano. 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
(*((volatile unsigned long *)0x400053FC))
#define PORTE
(*((volatile unsigned long
*)0x400243FC))
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 */
}
}
In the next example lets assume
that PORTA bit 3 is configured as an
output pin on the TM4C123. 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
(*((volatile unsigned long *)0x400043FC))
#define PORTB
(*((volatile unsigned long
*)0x400053FC))
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 */
}
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
(*((volatile unsigned long *)0x400053FC))
void
Example(void){
/* loop until
PORTB equals 200 */
for(PORTB=0; PORTB != 200; PORTB++){
PORTA = PORTA^0x08;}
/* toggle PORTA bit
3 output */
}
}
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."
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. 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 are examples of the second type. For more information on writing good comments see Section 5.6 of Embedded Systems: Introduction to ARM Cortex M Microcontrollers by Jonathan W. Valvano.
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 PORTA=0x80;
PORTA
/*PA7 is output*/=0x80/*turn on relay*/;
Keil uVision does allow the use
of C++ style comments. 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:
UART_Init();
/* turn on UART serial port */
UART_Init
();
// turn on UART 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";
Most compilers 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 PORTA
= 0x00;
will result in a
compiler error.
int
main(void){ unsigned char data;
Lock_Init();
/* initialize
I/O ports */
/*
PORTA = 0x00;
/* output to port
A
*/
*/
while(1){
Info
= PORTE; /* input from
port E */
PORTB =
Info;}} /*
output to port
B
*/
The conditional compilation feature can be used to temporarily remove and restore blocks of code.
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 TM4C123 I/O port names.
#include
"tm4c123gh6pm.h"
Examples of #include
are shown below,
and more in Chapter 11.
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 Keil uVision include:
integer variables (8-bit 16-bit
or 32-bit signed or unsigned)
character
variables (8-bit)
arrays
of integers or characters
pointers
to integers or characters
arrays
of pointers
structure
(grouping of other objects)
unions
(redefinitions of storage)
functions
Keil uVision supports 64-bit long long integers and floating point. In this document we will focus on 8-bit 16-bit and 32-bit objects. Object code generated with most ARM compilers is often more efficient using 32-bit parameters rather than 8-bit or 16-bit parameters.
It is important for the C
programmer to 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 UART_OutChar
could
be written as
void
UART_OutChar(char);
or
void UART_OutChar(char
letter);
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.) One approach is to first declare a functions, use it, and lastly defines the functions:
long add (long, long);
int
main(void) {
long x,y,z;
x = 2; y = 3;
z = add(x,y);
return 1;
}
long
add(long a, long b){
return (a+b);
}
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,
long RunFlag;
defines 32-bit signed integer
called RunFlag
;
whereas,
extern
long
RunFlag;
only declares the RunFlag
to exist in another,
separately compiled, module. Thus the line
extern
void SysTick_Handler();
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.
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 Keil uVision C compiler
passes
the first four input parameters in Register R0, R1, R2 and R3 and the
remaining parameters
are passed on the stack. A function can have 0 or 1 output parameter.
The return parameter is placed in Register R0 (8-bit or 16-bit return
parameters
are promoted to 32 bits.) The add
function below (an
improvement that checks for overflow) 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 int 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
int 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.
The parentheses are required even when there are no arguments. When there are no input parameters avoid
or nothing can be
specified. The following two statements are equivalent:
void
TogglePA3(void){PORTA ^= 0x08;}
void
TogglePA3
(){
PORTA
^= 0x08;
}
I prefer to include the void
because it is a positive
statement that there are no input parameters. However, one must specify
the output parameter, even if there is none. 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.
On a reset or a power initialization, the 32-bit value in ROM location 0x000000004 is loaded into the program counter, PC. The programs created using Keil uVision actually begin execution at a place called Reset_Handler, which can be found in the start.s file. 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 start.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.
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
.
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.
In the following example, we wish to maintain a counter of the number of times PA3 is toggled. 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 uVision) will automatically initialize globals to zero at startup.
unsigned
long Count; /* number of toggles,
initialized to 0 */
void
TogglePA3(void){
Count =
Count+1; /*
incremented each time called */
PORTA ^= 0x08;}
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.
int
Flag = 1;
void main(void) {
/* main body goes here */
}
Listing 1-11: A global variable initialized at run time by the compiler
int
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 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.
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
tm4c123gh6pm
.h
(actually much bigger)************ */
#define
GPIO_PORTA_DATA_R
(*((volatile unsigned long *)0x400043FC))
#define GPIO_PORTA_DIR_R
(*((volatile unsigned long *)0x40004400))
#define
GPIO_PORTA_DEN_R (*((volatile
unsigned
long *)0x4000451C))
#define SYSCTL_PRGPIO
_R
(*((volatile unsigned long *)
0x400FEA08
))
Listing 1-13: Header file for Port A I/O ports
/*
****file LOCK.h ************ */
void Lock_Init(void);
void Lock_Set(int flag);
unsigned long Lock_Input(void);
Listing 1-14: Header file for the Port A functions
/*
****file Lock.C ************ */
#include
"tm4c123gh6pm.h"
void
Lock_Init(void){ volatile unsigned long delay;
SYSCTL_PRGPIO
_R
|= 0x01; // activate clock for
Port A
delay = SYSCTL_PRGPIO
_R; //
allow time for clock to start
GPIO_PORTA_DIR_R = 0x80; // set
PA7 to output
and PA6-0 to input
GPIO_PORTA_DEN_R =
0xFF; // enable
digital port
}
void Lock_Set(int
flag)
{
if(flag){
GPIO_PORTA_DATA_R
=
0x80;
}else{
GPIO_PORTA_DATA_R
= 0;
}
}
unsigned
long Lock_Input(void)
{
return GPIO_PORTA_DATA_R
&0x7F;
// 0 to 127
}
Listing 1-15: Implementation file for the Port A interface
/* ****file main.c ************ */const
unsigned char
key=0x23; // The key code 0100011 (binary)
#include
"Lock.h"
void main(void){ unsigned char input; unsigned long cnt;
Lock_Init(); // initialize lock
cnt = 4000;
while(1){
input
=
Lock_Input()
;
// input 8 bits from parallel
port A
if(key == input){
cnt--;
// debounce
switches
if(cnt == 0){ // done
bouncing
Lock_Set(1); // unlock door
}
}else{
Lock_Set(0); // lock the
door
cnt = 4000;
}
}
}
#include "Lock.c"
Listing 1-16: Main program file for this system
With Keil uVision, we do not
need the
#include "Lock.c"
because
Lock.c will be included in the project.
I make
the following general statement about good programming 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:
Define the I/O ports in a tm4c123gh6pm.h
header file
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 Lock output function is needed in another application, then it would be a simple matter to reuse the lock.h and lock.c files. The next advantage is clarity. 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 Port A is replaced with Port B. For this kind of upgrade we implement the Port B functions in the Lock.c file with the new version. If we plan appropriately, we should be able to make this upgrade without changes to the files lock.h and main.c.
Go to Chapter 2 on Tokens Return to Table of Contents