09 - Reducing SRAM requirements

Submitted by Webbot on November 30, 2008 - 10:37am.

As we have seen previously: a microcontroller normally has a lot more Flash (read only memory) than it has SRAM (read write memory). So if our program has a lot of global variables, lookup tables or text strings then we can quickly use up all the SRAM or, at least, leave so little that there is no space for the stack.

 

In this section we will look at some different methods of reducing the amount of SRAM our program requires at runtime.

 

Use local variables

Global variables are the variables you normally define at the start of your source code - ie they are declared outside of any method. These variables are global beause they are accessible by all code. The downside is that they take up an amount of space for the duration of the whole program. But a local variable is one that is defined inside a method. For example:

void doSomething(){

   int counter;

   for( counter=0; counter<10; counter++){

      ...

   }

}

The variable called 'counter' is local because it is only visible to its containing method called 'doSomething'. Other methods may declare their own variables called 'counter' which are distinct from this one. The amount of space required this variable need only be allocated for the duration of the 'doSomething' method. Once the method has finished the space can be reclaimed so that it can be re-used for something else. This is normally done by moving the current stack pointer down by the required number of bytes so that the variable is stored within the stack. Once the mthod terminates: the stack pointer is restored to its previous position so that the space is 'popped' back off the stack. So make good use of local variables and only use global variables for variables that live for the duration of the program.

 

Passing arguments by reference

When passing some data to a method we have two ways of doing it: by value or by reference.

Here is an example of passing by value:-

void doSomething(int value){

    value = value + 1;

    printf("The value is %d",value);

}

void main(){

   int value = 0;

   doSomething(value);

    printf("The final value is %d",value);

}

This will print the following:

   The value is 1

   The final value is 0

Is this what you expected?

 

The reason is that the main routine creates the variable with a value of 0. But when it calls 'doSomething' it passes the variable by value. This is the equivalent of the doSomething method defining its own local variable called 'value' and initialising it with the passed value. So the 'doSomething' method is working with its own instance of the variable so that when it returns to the calling method (main) then its variable is left unchanged and so will still be 0.

 

Compare this with passing by reference:-

void doSomething(int* value){

    *value = *value + 1;

    printf("The value is %d",*value);

}

void main(){

   int value = 0;

   doSomething(&value);

    printf("The final value is %d",value);

}

This will print the following:

   The value is 1

   The final value is 1

The difference is that the doSomething method has an asterisk before the name of the variable and in the main method we prefix the variable name with an ampersand. This means that rather than being passed a copy of the variable it will instead be passed the address of the existing variable from main. When it adds one to the variable then its is actually changing the original variable so, on return to main, then the variable has been changed.

Why will that make a difference to the space requirements? Well in this simple case it wont make much difference at all. However: don't forget that you can use structures (discussed earlier) to create much more complex (and much larger) variables. Let's assume that we have used 'typedef' to define a structure to store all the relevant data we need about a servo (ie port, pin, current speed etc). Let's also assume that we have a method called setServoSpeed that we can pass one of these structure to in order to set the current speed for the servo. Without worrying about how these methods communicate with the servo, or how the servo structures are initialised, then our code may look like this if we are passing by value:-

SERVO left; // global variable for left servo

SERVO right; // global variable for right servo

SERVO setServoSpeed(SERVO servo, int speed){

   servo->speed = speed;

   ....

   return servo;

 

}

void main(){

    while(1){

        left = setServoSpeed(left,100);

        right = setServoSpeed(right,100);

}

Since we are passing by value then when we call the 'setServoSpeed' method then a new copy of the SERVO data will be created on the stack. Hence we make 'setServoSpeed' return this copy so that the caller, main, can then store it back into the left or right variable. This is messy for a number of reasons.

1. When we call the 'setServoSpeed' method we must remember to assign the result back into the correct variable otherwise the results of the method will get lost.

2. When we call the 'setServoSpeed' method the compiler will add in some extra code for us so that it can copy the contents of the servo variable into the working copy that 'setServoSpeed' uses and also to copy the result back into the global variable on return. This means that the code is bigger (as it has extra code to perform the copying) and will also run slower as it is constantly copying all this data backwards and forwards.

3. But here is the consideration as far as space is concerned..... Since the 'setServoSpeed' method has its own copy of the servo data then, whilst 'setServoSpeed' is being executed our SRAM will two copies of the servo data: one in the global variable and another on the stack. So if the servo structure is quite big then this could cause the stack to overflow and start zapping some of our data segment.

4. Another consideration to watch out for is if any of the data in the servo structure is modified under interrupts. You may, for example, be using an encoder which is firing interrupts to update a 'current position' in the global variable. When 'setServoSpeed' returns and its copy of the servo data is written back over the top of the global variable then these sorts of variables could rewind back to their value at the time that 'setServoSpeed' was called!!

 

So with large data structures that are passed as parameters it is much better to pass them by reference. This means that the only extra info on the stack is the pointer to the original servo data and so only requires two bytes (for a 16 bit processor) irrespective of how big the servo data actually is. It will also mean that the compiler doesn't need all the extra code to copy the data backwards and forwards since 'setServoSpeed' will be working directly on the data in the global variable. Last, and not least, the 'setServoSpeed' method doesn't need to return the new data so that it can be reassigned to the global variable.

So here is how we would do the same thing by reference:

SERVO left; // global variable for left servo

SERVO right; // global variable for right servo

void setServoSpeed(SERVO* servo, int speed){

   servo->speed = speed;

   ....

 

}

void main(){

    while(1){

        setServoSpeed(&left,100);

        setServoSpeed(&right,100);

}

 

Data that never changes

It is quite common for a program to contain data that never changes. For example:-

  • A lookup table to convert an angle into its sin or cosin
  • A lookup table to convert a number from 0 to 15 into its hexadecimal character '0' to 'F'
  • String constants used in printf statements for logging values back to the PC or to an LCD display

By default the compiler will always put code into the Code Segment and everything else into the Data Segment. So all of the above would be placed into the Data Segment and hence take up valuable SRAM at runtime. But hold on - this data is different to other variables in that it will never change - it is constant. So it would be great if we could keep it in Flash memory instead as we have a lot more of that. We can achieve this by doing two things:-

  1. When we define the table/string/etc we tell the compiler that it should be stored in the Code Segment instead of the Data Segment
  2. When we access the table/string/etc we remind the compiler that it is stored in the Code Segment instead of the Data Segment

Failure to do both of these things will cause unexpected outcomes.

 

Let's take an example of a method that converts a number to its hexadecimal character - note that these examples use commands defined within AVRlib and require you to #include <avr/pgmspace.h>  :-

 

const char toHexTbl[] = { '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F' };

char numToHex(int value){

     return toHexTbl[value & 15];

}

This will use 16 bytes of SRAM for the table. But since it never changes we could re-write it as:

const char toHexTbl[] PROGMEM = { '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F' };

char numToHex(int value){

     return (char)(pgm_read_byte(&toHexTbl[value & 15]));

}

Note we have added PROGMEM to the declaration and used the 'pgm_read_byte' function when it is accessed. If your table held 16 bit values then you would use 'pgm_read_word' instead.

The size of your HEX file will be unchanged as the 16 bytes have been taken out of the Data Segment and placed in the Code Segment instead but it will now require ZERO bytes of SRAM at runtime.

 

Here is another example that uses the AVRlib rprintf routines to output logging info. This demonstrates a common example whereby a method (in this case 'rprintf') has a matching method which works with data from program memory (in this case 'rprintfProgStr').  If our original code looked like this:-

void doSomething(int value){

    ...

    rprintf("Value = %d ", value);

    ...

}

The we could re-write this so that the string "Value = %d " is stored in program memory as follows:-

void doSomething(int value){

    ...

    rprintfProgStr( PSTR("Value = %d "), value);

    ...

}

Here we have used the PSTR() macro when the string is declared so that the compiler knows that it should be stored in program memory, and we have used the rprintfProgStr method rather than the rprintf method since rprintfProgStr expects the format string to be held in program memory.

 

Using EEPROM memory

This memory is used to hold the contents of variables that should retain their values when the power is discontinued. Think of it as battery back up SRAM. You could use it to hold some user preference such as, say, the current volume control setting for a sound output. Although they are useful for this purpose they don't really help with saving runtime memory as the number of variables that make sense to store in this way are normally limited. Admittedly if your robot performed an element of 'learning' then you may want it to remember what it has learned between reboots (for example: remember the map it has currently built up of the room it has explored). However: this kind of data is generally quite large and there is normally a lot less EEPROM memory than SRAM so it would be more practical to interface to something like an SD card to save this kind of information.

Check out AVRlib for details and examples of using EEPROM memory.