Pointers

Overview

Embedded software designers are required to access specific addresses within the microprocessor all the time.   We use addresses to access peripheral devices, configure the processor, and a variety of other operations.  In the C language, a pointer is used to access data at a specific address.   Here we will examine the basic syntax of how to allocate and use pointers.

Declaring a pointer

A pointer is declared using the asterix character and indicating the type of data the pointer points to.  This allows the compiler to determine the appropriate  assembly instructions required for accessing the address stored in the pointer.  If you are using a uint16_t pointer to write to a memory location, the compiler knows to use STRH commands. If you use a uint32_t pointer, the compiler knows to use STR commands.

    uint32_t *unsignedWordPtr;
    uint16_t *unsignedHalfWordPtr;
    Node *myNode;

In the example above, how many bytes are required to allocate for each pointer type?  The answer is 4 bytes for all of the pointers.  All pointers in the Cortex-M architecture are 4-bytes in length because all addresses are 32-bits.

Initializing a Pointer

When instantiated, a pointer points to NULL or address 0x00000000. NULL means that the pointer does not point to anything.  In order to initialize a pointer, we need to set the pointer to a valid memory address.  We can do this either by assigning the pointer to the address of a local/global variable or my dynamically allocating memory and setting the pointer to the address returned by malloc.

    //Assigning a pointer to a static variable
    uint32_t  var1;
    uint32_t *myPtr = &var1;

    //Assigning a pointer to a dynamic memory address
    Node *myNode;

    // Allocates memory from the heap 
    // for on Node Struct
    myNode = malloc(sizeof(Node));

If you try to access the memory of an uninitialized pointer, bad things happen.  You are essentially trying to read an invalid memory location.  The result is that the microprocessor will issue a fault, your program terminates, and the processor is stuck in the fault handler until the processor is restarted.

De-referencing a Pointer

When you want to change the value stored at a memory address, we de-reference the pointer.  This does not change the address the pointer contains, rather we change the value found at that address.  De-referencing is indicated with a *

    uint32_t  var1;
    uint32_t *myPtr = &var1;

    // the same as var1=1; 
    *myPtr = 1;

When dealing with pointers to structs, we will access the fields within the struct using “->” to de-reference the field.

    Node *myNode = malloc(sizeof(Node));
    myNode->size = 0;
    myNode->next = NULL;

 

Pointer Arithmetic

Pointer arithmetic is the process of adding an offset to the pointer to access a different memory location.

When we add a constant to a pointer, the resulting memory address is dependent on the size of the data being pointed to.  When we add positive 1 to a 32-bit variable, we actually increment the memory address by 4 bytes.  If we add +1 to a 16-bit variable, we increment the address by 2 bytes.

uint32_t *ptr1;
uint8_t  *ptr2;

// Allocate space for 4 32-bit variables
ptr1 = malloc(4 * sizeof(uint32_t) );  

// if ptr1 = 0x20000000, ptr <= 0x20000004 after
// this instruction
ptr1 = ptr1 + 1; 

// Allocate space for 16 8-bit variables
ptr2 = malloc(16 * sizeof(uint8_t) );  

// if ptr2 = 0x30000000, ptr <= 0x30000001
// after this instruction
ptr2 = ptr2 + 1;

 

If we are adding constants to a structure, adding positive 1 to the pointer will advance the pointer the total number of bytes for all the fields contained in the struct.

typedef struct 
{
     uint32_t v1;
     uint32_t v2;
     uint32_t v3;
     uint8_t *ptr;
} genStruct;

genStruct *ptr1;
genStruct structArray[10];

ptr1 = structArray;

// if ptr1 = 0x20000000,  after this instruction
// ptr1 = 0x20000000 + sizeof(genStruct)
ptr1 = ptr1 + 1;

 

Static Memory Allocation

Static Memory allocation is the process that the compiler uses to allocate memory locations from SRAM at compile time.  Global variables are placed in SRAM using static memory allocation.  The compiler reserves space in the SRAM for each global variable and places it into a global memory pool.

Static memory allocation is perhaps the simplest memory allocation scheme but it has its limitations.  The most glaring limitation is that we must know the memory size requirements of data at compile time.  In some situations we may not know exactly how much data our system will require.  One work around is that you can simply allocate more data at compile time than is necessary for a given variable and hope that will suffice.  There are two potential issues with this approach.  Our guess at “more” data may still be too small.  On the other hand we may over compensate and allocate a block of SRAM that is much too large.  As a result we now have a large section of SRAM that is allocated but only a fraction of it is actually used by the application.  The unused portion of that large block is effectively a lost resource.

Dynamic Memory Allocation

Dynamic Memory Allocation solves some of the problems encountered by static memory allocation. In many situations, we want to have more flexibility in how memory is allocated .  One example is to allocate memory on demand.  As the application runs and requires additional memory, we want to be able to grab portions of SRAM that have not been previously been allocated.

Lets examine where the unused section of SRAM resides in our memory map.  SRAM is generically broken down into three sections.: the global memory pool, the stack, and the heap.

Heap

The global memory pool consists  of all global variables declared in the application.  These global variables are statically allocated by the compiler.  The stack is allocated at compile time as well.  A board initialization routine (often found in the “startup file”) will set the size of the stack and adjust the stack pointer accordingly.  The remainder of the SRAM becomes the heap.  The heap is the area of SRAM that has not been allocated at compile time and is available to the application to dynamically allocate while the application is running.

Applications make memory requests from the heap using the malloc command.

// Create a pointer to unsigned 32-bit data
uint32_t *data;

// Allocate space for 16 bytes, or four uint32_ts
// malloc always takes the number of bytes as an
// argument.
data = malloc(4*sizeof(uint32_t));

// When done with the data, it must be returned to the 
// heap.  If you do not return memory to the heap, the
// the memory is lost to the application (i.e. a memory
// leak).
free(data);

malloc is a library that manages the free space in  heap.  The heap is not simply unused SRAM.  The heap is a collection of data structures that keeps track of unused sections of SRAM.  In very simple terms, the heap is a linked list of free memory blocks.  When you call malloc, it searches a list of free blocks until it finds a free block of memory that will satisfy your request.  malloc removes this block from the free list and returns it to the application.  When the application is done with the block of memory it MUST return it to the heap using the free command.  Failing to do so results in that block of memory being lost to the application since it will never re-enter the free list and cannot be allocated again.  This is called a memory leak.

Dynamic memory allocation has its drawbacks as well.  A portion of SRAM is used as overhead in managing the data structures that keep track of the free list ( next pointers, size information, etc).  The overhead decreases the available SRAM for data.  Allocating memory from the heap can also be a lengthy operation.  Traversing the free list for the most optimal block size is a very computationally intensive task and can consume many CPU cycles.   Dynamic memory allocation also results in the fragmentation of the available SRAM.  As a system runs for extended periods of time, large unused memory blocks become scarce and the heap may not be able to satisfy a request for a large block of data.

Managing the heap is a reasonably complex topic that we will not cover here.  There is first fit, best fit, and next fit algorithms that can be used to determine which free block to return to the application.  There are also algorithms that dictate when to break a larger free block into smaller blocks and how to combine adjacent blocks of memory in the free list.   For our purposes, we are simply going to use malloc the the compiler has already implemented heap management for us.

Examples

// Create a pointer to a character
char *string;

// Allocate 27 characters
string = malloc(27);

// Initialize the string from a-z
for(i=0; i<26; i++)
{
  string[i] = 'a' + i;
}

// Set the last character in the string to 0 to end the string
string[26] = 0;

// Return the memory to the heap.
free(string);

This example is functionally the same as above, but illustrates a different way to access data using pointers.

  int i;

  // Create a pointer to a character
  char *string;
  char *currentChar;

  // Allocate 27 characters
  string = malloc(27);

  currentChar = string;
  // Initialize the string from a-z
  for(i=0; i<26; i++)
  {
    *currentChar = 'a' + i;
    currentChar++;
  }

  // Set the last character in the string to 0 to end the string
  currentChar = 0;

  printf("** %snr",string);

  // Return the memory to the heap.
  free(string);

 

Leave a Reply