Chapter 4: Variables and Constants
What's in Chapter 4?
A static variable exists permanently
A static global can be accessed
only from within the same file
A static local can be accessed
only in the function
We specify volatile variables when
using interrupts and I/O ports
Automatic variables are allocated
on the stack
We can understand automatics
by looking at the assembly code
A constant local can not be
changed
External variables are defined
elsewhere
The scope of a variable defines where
it can be accessed
Variables declarations
8-bit variables are defined with
char
Discussion of when to use static versus
automatic variables
Initialization of variables and
constants
We can understand
initialization by looking at the assembly code
The purpose of this chapter is to explain how to create and access variables and constants. The storage and retrieval of information are critical operations of any computer system. This chapter will also present the C syntax and resulting assembly code generated by the Keil uVision compiler.
A variable is a named object that resides in RAM memory and is capable of being examined and modified. A variable is used to hold information critical to the operation of the embedded system. A constant is a named object that resides in memory (usually in ROM) and is only capable of being examined. As we saw in the last chapter a literal is the direct specification of a number character or string. The difference between a literal and a constant is that constants are given names so that they can be accessed more than once. For example
short
MyVariable; /*
variable allows read/write access */
const short MyConstant=50; /* constant allows only read access
*/
#define fifty 50
void main(void){
MyVariable=50; /*
write access to the variable */
OutSDec(MyVariable); /*
read access to the variable */
OutSDec(MyConstant); /*
read access to the constant */
OutSDec(50); /*
"50" is a literal */
OutSDec(fifty); /*
fifty is also a literal */
}
The options on many compilers can be used to select the precision of each of the data formats (int, short etc.)
The concepts of precision and type (unsigned vs. signed) developed for numbers in the last chapter apply to variables and constants as well. In this chapter we will begin the discussion of variables that contain integers and characters. Even though pointers are similar in many ways to 32-bit unsigned integers, pointers will be treated in detail in Chapter 7. Although arrays and structures fit also the definition of a variable, they are regarded as collections of variables and will be discussed in Chapter 8 and Chapter 9.
The term storage class refers to the method by which an object is assigned space in memory. The Metrowerks compiler recognizes three storage classes--static, automatic, and external. In this document we will use the term global variable to mean a regular static variable that can be accessed by all other functions. Similarly we will use the term local variable to mean an automatic variable that can be accessed only by the function that created it. As we will see in the following sections there are other possibilities like a static global and static local.
Static variables are given space in memory at some fixed location within the program. They exist when the program starts to execute and continue to exist throughout the program's entire lifetime. The value of a static variable is faithfully maintained until we change it deliberately (or remove power from the memory). A constant, which we define by adding the modifier const, can be read but not changed.
In an embedded system we normally wish to place all variables in RAM and constants in ROM. The following example sets a global, called TheGlobal, to the value 1000. This global can be referenced by any function from any file in the software system. It is truly global.
long
TheGlobal; /* a regular global variable*/
void main(void){
TheGlobal =
1000;
}
The ARM code generated by the uVision compiler is as follows (register use will vary)
LDR R0,=1000
LDR R1,=TheGlobal
STR R0,[R1]
The fact that these types of variables exist in permanently reserved memory means that static variables exist for the entire life of the program. When the power is first applied to an embedded computer, the values in its RAM are usually undefined. Therefore, initializing global variables requires special run-time software consideration. See the section on initialization for more about initializing variables and constants.
A static global is very similar to a regular global. In both cases, the variable is defined in RAM permanently. The assembly language access is identical. The only difference is the scope. The static global can only be accessed within the file where it is defined. The following example also sets a global, called TheGlobal, to the value 1000. This global can not be referenced by modules in other files.
static
short TheGlobal; /* a static global
variable*/
void main(void){
TheGlobal =
1000;
}
The code generated by the compiler is the same as a regular global. The compiler does properly limit the access only to the static global to functions defined in this file.
A static local is similar to the static global. Just as with the other statics, the variable is defined in RAM permanently. The assembly language code generated by the compiler that accesses the variable is identical. The only difference is the scope. The static local can only be accessed within the function where it is defined. The following example sets a static local, called TheLocal, to the value 1000. The compiler limits the access to the static local, so that this variable can not be accessed by other functions in this file or in other files. Notice that the assembly language name of the static local is a unique compiler-generated name (L2 in this example.) This naming method allows other functions to also define a static local or automatic local with the same name.
void
main(void){
static stort
TheLocal; /* a static local variable*/
TheLocal =
1000;
}
Again the code generated by the compiler is the same as a regular global. The compiler does properly limit the access only to the static local to the function in which it is defined.
A static local can be used to save information from one instance of the function call to the next. Assume each function wished to know how many times it has been called. Remember upon reset, the compiler will initialize all statics to zero (including static locals). The following functions maintain such a count, and these counts can not be accessed by other functions. Even though the names are the same, the two static locals are in fact distinct.
void
function1(void){
static short TheCount;
TheCount = TheCount+1;
}
void function2(void){
static short TheCount;
TheCount = TheCount+1;
}
We add the volatile modifier to a variable that can change value outside the scope of the function. Usually the value of a global variable changes only as a result of explicit statements in the C function that is currently executing. The paradigm results when a single program executes from start to finish, and everything that happens is an explicit result of actions taken by the program. There are two situations that break this simple paradigm in which the value of a memory location might change outside the scope of a particular function currently executing:
1) interrupts and
2) input/output ports.
An interrupt is a hardware-requested software action. Consider the following multithreaded interrupt example. There is a foreground thread called main(), which we setup as the usual main program that all C programs have. Then, there is a background thread called SysTick_Handler(), which we setup to be executed on a periodic basis (e.g., every 16 ms). Both threads access the global variable, Time. The interrupt thread increments the global variable, and the foreground thread waits for time to reach 100. Notice that Time changes value outside the influence of the main() program.
volatile
unsigned long Time;
void SysTick_Handler(void){ /* every 16ms
*/
Time = Time+1;
}
void main(void){
SysTick_Init();
Time = 0;
while(Time<100){}; /* wait for 100
counts of the 16 ms timer*/
}
Without the volatile modifier the compiler might look at the two statements:
Time
= 0;
while(Time<100){};
and conclude that since the while loop does not modify Time, it could never reach 100. Some compilers might attempt to move the read Time operation, performing it once before the while loop is executed. The volatile modifier disables the optimization, forcing the program to fetch a new value from the variable each time the variable is accessed.
In the next example, assume PORTA is an input port containing the current status of some important external signals. The program wishes to collect status versus time data of these external signals.
unsigned
char data[100];
#define
GPIO_PORTA_DATA_R
(*((volatile unsigned long *)0x400043FC))
void Collect(void){ short i;
for(i=0;i<100;i++){ /*
collect 100 measurements */
data[i]
= GPIO_PORTA_DATA_R
; /* collect
ith measurement */
}
}
Without the volatile modifier in the PORTA definition, the compiler might optimize the for loop, reading PORTA once, then storing 100 identical copies into the data array. I/O ports will be handled in more detail in Chapter 7 on pointers.
Automatic variables, on the other hand, do not have fixed memory locations. They are dynamically allocated when the block in which they are defined is entered, and they are discarded upon leaving that block. When there are a few variables, they are allocated in registers. However, when there are a lot of local variables, they are allocated on the stack by subtracting 4 from the SP for each 32-bit variable. Since automatic objects exist only within blocks, they can only be declared locally. Automatic variables can only be referenced (read or write) by the function that created it. In this way, the information is protected or local to the function.
When a local variable is created it has no dependable initial value. It must be set to an initial value by means of an assignment operation. C provides for automatic variables to be initialized in their declarations, like globals. It does this by generating "hidden" code that assigns values automatically after variables are allocated space.
It is tempting to forget that automatic variables go away when the block in which they are defined exits. This sometimes leads new C programmers to fall into the "dangling reference" trap in which a function returns a pointer to a local variable, as illustrated by
int
*BadFunction(void) {
int z;
z = 1000;
return (&z);
}
When callers use the returned address of z they will find themselves messing around with the stack space that z used to occupy. This type of error is NOT flagged as a syntax error, but rather will cause unexpected behavior during execution.
Implementation of automatic variables
For information on how local variables are implemented on the ARM Cortex M see Chapter 7 of Embedded Systems: Introduction to ARM Cortex M Microcontrollers by Jonathan W. Valvano. If locals are dynamically allocated at unspecified memory (stack) locations, then how does the program find them? This is done by using the stack pointer (SP) to designate a stack frame for the currently active function.
void fun(void){ long y1,y2,y3; /* 3 local
variables*/
y1 = 1000;
y2 = 2000;
y3 = y1+y2;
}
fun SUB SP,#12 ; allocate3 local
variables
; y1 = 1000
LDR R0,=1000
STR R0,[SP,#0]
;
y2 = 2000
LDR R0,=2000
STR R0,[SP,#4]
; y3 = y1+y2
LDR R0,[SP,#0] ; y1
LDR R1,[SP,#4] ; y2
ADD R2,R0,R1
STR R2,[SP,#8] ;set y3
ADD SP,#12 ;deallocate
BX LR
A constant local is similar to the regular local. Just as with the other locals, the constant is defined temporarily on the stack. The difference is that the constant local can not be changed. The assembly language code generated by the compiler that accesses the constant local is identical to the regular local.
short
TheGlobal; /* a regular global variable*/
void main(void){
const short
TheConstant=1000; /* a constant local*/
TheGlobal=TheConstant;
}
Objects that are defined outside of the present source module have the external storage class. This means that, although the compiler knows what they are (signed/unsigned, 8-bit 16-bit 32-bit etc.), it has no idea where they are. It simply refers to them by name without reserving space for them. Then when the linker brings together the object modules, it resolves these "pending" references by finding the external objects and inserting their addresses into the instructions that refer to them. The compiler knows an external variable by the keyword extern that must precede its declaration.
Only global declarations can be designated extern and only globals in other modules can be referenced as external.
The following example sets an external global, called ExtGlobal, to the value 1000. This global can be referenced by any function from any file in the software system. It is truly global.
extern
short ExtGlobal; /* an external global
variable*/
void main(void){
ExtGlobal=1000;
}
The scope of a variable is the portion of the program from which it can be referenced. We might say that a variable's scope is the part of the program that "knows" or "sees" the variable. As we shall see, different rules determine the scopes of global and local objects.
When a variable is declared globally (outside of a function) its scope is the part of the source file that follows the declaration--any function following the declaration can refer to it. Functions that precede the declaration cannot refer to it. Most C compilers would issue an error message in that case.
The scope of local variables is the block in which they are declared. Local declarations must be grouped together before the first executable statement in the block--at the head of the block. This is different from C++ that allows local variables to be declared anywhere in the function. It follows that the scope of a local variable effectively includes all of the block in which it is declared. Since blocks can be nested, it also follows that local variables are seen in all blocks that are contained in the one that declares the variables.
If we declare a local variable with the same name as a global object or another local in a superior block, the new variable temporarily supersedes the higher level declarations. Consider the following program.
unsigned
char x; /* a regular global variable*/
void sub(void){
x=1;
{ unsigned
char x; /* a local variable*/
x=2;
{ unsigned
char x; /* a local variable*/
x=3;
PORTA=x;}
PORTA=x;}
PORTA=x;}
}
This program declares variables with the name x, assigns values to them, and outputs them to PORTA with in such a way that, when we consider its output, the scope of its declarations becomes clear. When this program runs, it outputs 321. This only makes sense if the x declared in the inner most block masks the higher level declarations so that it receives the value '3' without destroying the higher level variables. Likewise the second x is assigned '2' which it retains throughout the execution of the inner most block. Finally, the global x, which is assigned '1', is not affected by the execution of the two inner blocks. Notice, too, that the placement of the last two PORTA=x; statements demonstrates that leaving a block effectively unmasks objects that were hidden by declarations in the block. The second PORTA=x; sees the middle x and the last PORTA=x; sees the global x.
This masking of higher level declarations is an advantage, since it allows the programmer to declare local variables for temporary use without regard for other uses of the same names.
One of the mistakes a C++ programmer makes when writing C code is trying to define local variables in the middle of a block. In C local variables must be defined at the beginning of a block. The following example is proper C++ code, but results in a syntax error in C.
void
sub(void){ int x; /* a valid local variable
declaration */
x=1;
int
y; /* This declaration is improper */
y=2;
}
Unlike BASIC and FORTRAN, which will automatically declare variables when they are first used, every variable in C must be declared first. This may seem unnecessary, but when we consider how much time is spent debugging BASIC and FORTRAN programs simply because misspelled variable names are not caught for us, it becomes obvious that the time spent declaring variables beforehand is time well spent. Declarations also force us to consider the precision (8-bit, 16-bit etc.) and format (unsigned vs. signed) of each variable.
As we saw in Chapter 1, describing a variable involves two actions. The first action is declaring its type and the second action is defining it in memory (reserving a place for it). Although both of these may be involved, we refer to the C construct that accomplishes them as a declaration. As we saw above, if the declaration is preceded by extern it only declares the type of the variables, without reserving space for them. In such cases, the definition must exist in another source file. Failure to do so, will result in an unresolved reference error at link time.
Table 4-1 contains examples of legitimate variable declarations. Notice that the declarations are introduced by one or type keywords that states the data type of the variables listed. The keyword char declares 8-bit values, int declares 16-bit values, short declares 16-bit values and long declares 32-bit values. Unless the modifier unsigned is present, the variables declared by these statements are assumed by the compiler to contain signed values. You could add the keyword signed before the data type to clarify its type.
When more than one variable is being declared, they are written as a list with the individual names separated by commas. Each declaration is terminated with a semicolon as are all simple C statements.
Declaration
Comment
Range
unsigned
char uc;
8-bit unsigned number
0 to +255
char
c1,c2,c3;
three 8-bit signed
numbers
-128 to +127
unsigned
int ui;
32-bit unsigned number
0 to
+4294967296
int
i1,i2;
two 32-bit signed numbers
-2147483648L
to
2147483647L
unsigned
short us;
16-bit unsigned number
0 to +65535
short
s1,s2;
two 16-bit signed numbers
-32768 to
+32767
long
l1,l2,l3,l4;
four signed 32 bit
integers
-2147483648L
to
2147483647L
unsigned long ui;
32-bit unsigned number
0 to +4294967296
float
f1,f2;
two 32-bit floating
numbers
not
recommended
double
d1,d2;
two 64-bit floating
numbers
not
recommended
The compiler allows the register modifier for automatic variables, but it may still defines register locals on the stack. The keywords char int short long specify the precision of the variable. The following tables shows the available modifiers for variables.
Modifier | Comment |
auto |
automatic, allocated on the stack |
extern |
defined in some other program file |
static |
permanently allocated |
register |
attempt to implement an automatic using a register instead of on the stack |
Modifier | Comment |
volatile |
can change value by means other than the current program |
const |
fixed value, defined in the source code and can not be changed during execution |
unsigned |
range starts with 0 includes only positive values |
signed |
range includes both negative and positive values |
Refer to the Appendix 1: C Declarations - A Short Primer for a brief Primer on how to read and write declarations in C.
In all cases const
means the variable has a fixed value and
cannot be changed. When defining a constant global on an embedded
system like the Cortex M, the parameter will be allocated in
ROM.
In the following example, Ret
is allocated in ROM. When const
is added to a parameter or a local variable, it means that parameter
can not be modified by the function. It does not change where the
parameter is allocated. For example, this example is legal.
unsigned
char const Ret=13;
void
LegalFuntion(short in){
while(in){
UART_OutChar(Ret);
in--;
}
}
On
the other hand, this example is not legal because the function attempts
to modify the input parameter.
in
in this example would have been allocated on the stack or in a register.
void
NotLegalFuntion(const short in){
while(in){
UART_OutChar(13);
in--; // this operation is
illegal
}
}
Similarly,
this example is not legal because the function attempts to modify the
local variable. count
in this example would have been allocated on the stack or in a register.
void
NotLegalFuntion2(void){ const short count=5;
while(count){
UART_OutChar(13);
count--; // this operation is
illegal
}
}
As we shall see, a similar syntax is used to declare pointers, arrays, and functions (Chapters 7, 8, and 10).
Character variables are stored as 8-bit quantities. When they are fetched from memory, they are always promoted automatically to 32-bit integers. Unsigned 8-bit values are promoted by adding 24 zeros into the most significant bits. Signed values are promoted by coping the sign bit (bit7) into the 24 most significant bits.
There is a confusion when signed and unsigned variables are mixed into the same expression. It is good programming practice to avoid such confusions. As with integers, when a signed character enters into an operation with an unsigned quantity, the character is interpreted as though it was unsigned. The result of such operations is also unsigned. When a signed character joins with another signed quantity, the result is also signed.
char
x; /* signed 8 bit global */
unsigned short y; /* unsigned signed 16
bit global */
void sub(void){
y=y+x;
/* x treated as unsigned even though defined as signed */
}
There is also a need to change the size of characters when they are stored, since they are represented in the CPU as 32-bit values. In this case, however, it matters not whether they are signed or unsigned. Obviously there is only one reasonable way to put a 32-bit quantity into an 8-bit location. When the high-order 24 bits are chopped off, an error might occur. It is the programmer's responsibility to ensure that significant bits are not lost when characters are stored.
When Do We Use Automatics Versus Statics?
Because their contents are allowed to change, all variables must be allocated in RAM and not ROM. An automatic variable contains temporary information used only by one software module. As we saw, automatic variables are typically allocated, used, then deallocated from the stack. Since an interrupt will save registers and create its own stack frame, the use of automatic variables is important for creating reentrant software. Automatic variables provide protection limiting the scope of access in such a way that only the program that created the local variable can access it. The information stored in an automatic variable is not permanent. This means if we store a value into an automatic variable during one execution of the module, the next time that module is executed the previous value is not available. Typically we use automatics for loop counters, temporary sums. We use an automatic variable to store data that is temporary in nature. In summary, reasons why we place automatic variables on the stack include
• dynamic
allocation release allows for reuse of memory
• limited scope of access provides for data protection
• can be made reentrant.
• limited scope of access provides for data protection
• since absolute addressing is not used, the code is
relocatable
• the number of variables is only limited by the size of the
stack allocation.
A static variable is information shared by more than one program module. E.g., we use globals to pass data between the main (or foreground) process and an interrupt (or background) process. Static variables are not deallocated. The information they store is permanent. We can use static variables for the time of day, date, user name, temperature, pointers to shared data. The Metrowerks compiler uses absolute addressing (direct or extended) to access the static variables.
Initialization of variables and constants
Most programming languages provide ways of specifying initial values; that is, the values that variables have when program execution begins. We saw earlier that the Metrowerks compiler will initially set all static variables to zero. Constants must be initialized at the time they are declared, and we have the option of initializing the variables.
Specifying initial values is simple. In its declaration, we follow a variable's name with an equal sign and a constant expression for the desired value. Thus
short
Temperature = -55;
declares Temperature
to be a 16-bit signed
integer, and gives it an initial value of -55. Character constants with
backslash-escape sequences are permitted. Thus
char
Letter = '\t';
declares Letter
to be a character, and
gives it the value of the tab character.
If
array elements are being initialized, a list of constant expressions,
separated by commas and enclosed in braces, is written. For example,
const
unsigned short Steps[4] = {10, 9, 6, 5};
declares Steps
to be an unsigned 16-bit
constant integer array, and gives its elements the values 10, 9, 6, and
5 respectively. If the size of the array is not specified, it is
determined by the number of initializers. Thus
char
Waveform[] = {28,27,60,30,40,50,60};
declares Waveform
to be a signed 8-bit
array of 7 elements which are initialized to the 28,27,60,30,40,50,60
. On the other hand, if
the size of the array is given and if it exceeds the number of
initializers, the leading elements are initialized and the trailing
elements default to zero. Therefore,
char
Waveform[100] = {28,27,60,30,40,50,60};
declares Waveform
to be an integer array
of 100 elements, the first 7 elements of which are initialized to the 28,27,60,30,40,50,60
and the others to zero.
Finally, if the size of an array is given and there are too many
initializers, the compiler generates an error message. In that case,
the programmer must be confused.
Character arrays and character pointers may be initialized with a character string. In these cases, a terminating zero is automatically generated. For example,
char
Name[4] = "Jon";
declares Name
to be a character array
of four elements with the first three initialized to 'J', 'o', and 'n'
respectively. The fourth element contains zero. If the size of the
array is not given, it will be set to the size of the string plus one.
Thus ca in
char
Name[] = "Jon";
also contains the same four elements. If the size is given and the string is shorter, trailing elements default to zero. For example, the array declared by
char
Name[6] = "Jon";
contains zeroes in its last three elements. If the string is longer than the specified size of the array, the array size is increased to match. If we write
char
*NamePt = "Jon";
the effect is quite
different from initializing an array. First a word (16 bits) is set
aside for the pointer itself. This pointer is then given the address of
the string. Then, beginning with that byte, the string and its zero
terminator are assembled. The result is that NamePt
contains the address of
the string "Jon". The
Imagecraft and
Metrowerks compilers accept initializers for character variables,
pointers, and arrays, and for integer variables and arrays. The
initializers themselves may be either constant expressions, lists of
constant expressions, or strings.
Implementation of the initialization
The compiler initializes static constants simply by defining its value in ROM. In the following example, J is a static constant (actually K is a literal)
short
I; /*
16 bit global */
const short J=96; /* 16 bit constant */
#define K 97;
void main(void){
I=J;
I=K;}
Even though the following two applications of global variable are technically proper, the explicit initialization of global variables in my opinion is a better style.
/*
poor style
*/ /* good
style */
int
I=95; int
I;
void main(void){ void
main(void){
I=95;
} }
Go to Chapter 5 on Expressions Return to Table of Contents