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 */
}

Listing 4-1: Example showing a variable, a constant and some literals

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.

Statics

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;    
}

Listing 4-2: Example showing a regular global variable

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;    
}

Listing 4-3: Example showing a static global variable

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;    
}

Listing 4-4: Example showing a static local variable

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;
}

Listing 4-5: Example showing two static local variables with the same name

Volatile

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*/
}

Listing 4-6: An example showing shared access to a common global variable

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 */
  }
}

Listing 4-7: Example showed shared access to a common global variable

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.

Automatics

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);
}

Listing 4-8: Example showing an illegal reference to a local variable

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

Listing 4-9: Example showing three local variables

 

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;
}

Listing 4-10: Example showing a constant local 

Externals

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;    
}

Listing 4-11: Example showing an external global
 

Scope

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;}
}

Listing 4-12: An example showing the scope of local variables

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;
}

Listing 4-13: Example showing an illegal local variable declaration

 

Declarations

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

Table 4-1: Variable Declarations
 

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

Table 4-2: Variable storage classes
 

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

Table 4-3 Variable modifiers

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

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 */
}

Listing 4-13: An example showing the mixture of signed and unsigned variables

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;}

Listing 4-14: An example showing the initialization of a static constant

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;
}                     }

Opinion: I firmly believe a good understanding of the assembly code generated by our compiler makes us better programmers.

Go to Chapter 5 on Expressions Return to Table of Contents