C Variables

Overview

Variables are used in the C programming language to store data.  The type of variable you use and where you declare the variable influence where the variable is allocated (eg: global memory pool, stack, or heap) and how many bytes are allocated.  Let’s examine some of the basic types of variables.

Primitive Types

The table below lists the amount of data allocated for each type in C for the Keil ARM compiler.

char and unsigned char 1 byte
short int and unsigned short int 2 bytes
int and unsigned int 4 bytes
long and unsigned long 4 bytes
long long and unsigned long long 8 bytes
float 4bytes (32 bits)
double 8 bytes (64 bits)
long double 8 bytes (64 bits)
enum 4 bytes
bit fields up to 32 bits

One potential problem when using the primitive types in C code is that some of the primitive types can allocate a different number of bytes depending on what architecture you are using.  On a ARM Cortex-M, an int allocates 4 bytes of data.  On a 16-bit architecture, such as the MSP430, and int allocates 2 bytes of data.  This is because the Cortex-M architecture is a 32-bit architecture and the MSP430 is a 16-bit architecture.

stdint.h

Instead of relying on the primitive types available in C you can use the stdint.h library.  Using the stdint variables types takes away all ambiguity when it comes to allocating a specific amount of data.

#include <stdint.h>

void main (void)
{
    char              myChar ;           // Signed 8-bit Number
    unsigned char     myUnsignedChar;    // Unsigned 8-bit number
    short             myInt;             // Signed 16-bit number
    unsigned short    myUnsignedInt;     // Unsigned 16-bit Number
    long              myLong;            // Singed 32-bit Number
    unsigned long     myUnsignedLong;    // Unsigned 32-bit number

    int8_t            signed8Bits;       // Signed 8-bit Number
    uint8_t           unsigned8Bits;     // Unsigned 8-bit number
    int16_t           signed16Bits;      // Signed 16-bit number
    uint16_t          unsigned16Bits;    // Unsigned 16-bit Number
    int32_t           signed32Bits;      // Singed 32-bit Number
    uint32_t          unsigned32Bits;    // Unsigned 32-bit number

}

Local vs Global Variables

Local variables are instantiated within a function.  Global variables are variables that are instantiated outside of a function.  Lets examine how these two categories of variables differ from one another.

  • Scope

The scope of a variable describes where a variable can be accessed within the application.    Access to a local variable is limited to the function where the local variable is instantiated .  Only code written within the same function is allowed to access the local variable.    This allows us to declare two local variables with the same name in two different functions.  

A global variable is instantiated outside of a function and can be accessed from anywhere within an application.  For this reason, a global variable must have have a unique name.

  • Data Allocation

Global variables are allocated in SRAM at compile time.  We will refer to this area of SRAM as the Global Memory Pool.  The final pass of the assembler/linker will determine the absolute address of all global variables and reserves space in SRAM for them.  Each SRAM location is identified by a label.   You can view the location of all global variables by examining the .map file.

When an application enters a function, the function allocates enough memory for all the local variables declared in that function.  This allocation process is accomplished by reserving space on the stack.  The function is then free to read/write data to specific locations in the stack that are reserved for these local variables.  When the function exits, the memory reserved for the local variables is returned to the stack.

  • Lifetime

Global variables have a lifetime that extends from when the application starts until the application ends.  Since global variables are allocated at compile time, we can access them at any point in the execution of an application.

Local variables exist only while the function is actively running.  When a function ends, the value contained in a local variable is lost.  This is related to how memory is allocated for local variables.  Local variables are allocated on the stack.  When the function exits, the memory location for the local variable is returned to the stack and the value of the local variable is lost.

Static Variables

In the C programming language, the static keyword can be used to do one of two things: make a local variable stateful and to limit the access of a global variable to the file in which it is contained.

If a variable is declared to be static in a function, this indicates that the variable’s location in memory is determined statically and placed in SRAM instead of the stack.  This results in a variable that is stateful, meaning that the value stored in that variable is maintained throughout the lifetime of the program.  As a result, the value of that variable is saved for the next time the function is called.

  #include <stdint.h>

uint32_t f1()
{
   static uint32_t myInt = 0;
   myInt++;
   return myInt;
}

void main (void)
{
   uint32_t localInt;
   localInt = f1();   // returns 1
   localInt = f1();   // returns 2
}

If a global variable is declared as being static , it indicates that the global variable is only accessible from functions within the current file.  The mapper/linker will not allow other files access to static global variables.  This is sometimes used as a method to encapsulate the inner workings of a function/library.

Extern

The keyword extern indicates to the compiler that a variable is defined in some other file.  The compiler will compile the current file and insert a dummy address for the location of all extern variables.  During the linking stage, all of the dummy addresses are replaced with the final calculated address of the appropriate variable.

If you declare a global variable ( int var1)  in file1.c, and want to access it in file2.c, you must add the following line of code to file2.c

extern int var1;

 Volatile

The volatile keyword indicates to the compiler that the data at a particular memory location may change asynchronously.  The volatile keyword forces the compiler to re-read the memory location prior to any assignments using that memory location.  This prevents stale values of the variable from being cached in our general purpose registers.  Compilers try to optimize code for faster execution by keeping data in the general purpose registers as long as possible.  This reduces the number of load and store instructions.  In some situations though, the data at a memory address will be updated by something other than the CPU.  As a result, the data in the general purpose register no longer matches what is in memory.

An example of this would be reading one of the GPIO ports.  Every time the port is examined, we want to re-read the GPIO port to get the current state of the GPIO pins.  Without the volatile keyword, the compiler might try to use the value currently stored in a general purpose register.  Since the GPIO port changes asynchronously, the value in the general purpose register may not contain the current value on the pins.  By using the volatile keyword, we are indicating to the compiler that the data may change asynchronously, so we want to re-read the memory location every time that variable is used in our program.

volatile int var1;

 Const

The const keyword indicates to the compiler to make data read-only.  This is equivalent to placing data in the CODE section of the program in assembly.  A variable that is declared as being a const, must be initialized when it is declared.

// Assigments to var1 will create an error in the compiler 
const int var1=123; 

// constPtr will always point to var1
int * const constPtr = &var1;

 

Leave a Reply