SRAM optimisation
Arduinos based on the AVR chip sets have three types of memory. The different types are used differently and have varying characteristics.
Flash memory is used to store programs. The stored program is preserved when power is removed from the board. Flash memory is the type you would find in SD cards or “thumb drives”. It should be good for some 100,000 write cycles and so should last an effective lifetime even if you upload many different programs a day.
Static Random Access Memory (SRAM) is used to store data created and manipulated by the program. SRAM is a limited resource and the one an Arduino programmer is most likely to spend some time on managing effectively. Running out of SRAM memory space during program execution can cause a “crash” but would always have unexpected results. The allocation of space within SRAM is organised into three main areas. There is an area for static and global data, an area called the “stack” and an area known as the “heap”. The stack and heap can grow while a program is running to utilise any available free memory. Trouble arises if those growths result in a collision.
Electrically Erasable Programmable Read-Only Memory (EEPROM) can be used to store long term data. Data stored in the EEPROM is preserved when the board is not powered. EEPROM memory is relatively slow but can be re-written again perhaps 100,000 times without error. It can be read just about indefinitely so it is an effective storage for constant variables as long as their content is not required in a particularly critical time frame. Boards like the Arduino Due do not have EEPROM memory.
The ubiquitous Arduino Uno has 32K bytes of Flash memory, of which some 27K bytes is available for programs (the rest is required for the bootloader). The Uno only has 2K bytes of SRAM and 1K bytes of EEPROM memory available. The availability of memory may prove to be the key element when you select an Arduino model to implement a completed project.
When a program is compiled by the Arduino IDE then the Flash (program) memory usage will be clearly displayed. The IDE also provides a measure of SRAM usage by global variables which can be a useful start point when investigating runtime memory problems but the IDE can give no guidance on how additional memory may be consumed by the heap and stack.
The programmer has control over the use of EEPROM memory and if that is being used then he or she is fully responsible for managing that resource.
The use of SRAM is dynamic during program execution and some measures can be taken to optimise the usage of this limited resource. It is, of course, important to be able to measure what you are trying to optimise. The freeRam() function introduced here is a useful ally as you can call it from different points in a program to help diagnose where SRAM memory is most impacted.
If you are experiencing difficulties that you suspect can be attributed to running out of memory then there are a number of things you can do to minimise SRAM usage in particular. If all fails, you can always check to see if there is an Arduino model available with a greater capacity but it is always best to tackle these issues in programmer mode first.
Function to measure available SRAM
This function for AVR chip based Arduinos measures the “space” between the heap and the stack. It can’t report any free space within the heap which may or may not be re-usable by the next heap allocation. Detecting a fragmented heap (one with unused or unusable “holes” is tricky and probably something you would conclude after some experimentation
int freeRam () { extern int __heap_start, *__brkval; int f; return (int) &f - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); }
[Those are double underline characters by the way.]
Arm based boards while less likely to run out of memory, need an alternate function.
// three lines needed by freeRam()
#include <malloc.h>
extern char _end;
extern "C" char *sbrk(int i);
int freeRam() {
char *ramstart=(char *)0x20070000;
char *ramend=(char *)0x20088000;
char *heapend=sbrk(0);
register char * stack_ptr asm ("sp");
struct mallinfo mi=mallinfo();
return stack_ptr - heapend + mi.fordblks;
// mi.uordblks would return ram used
// ramend - stack_ptr would return stack size
}
Global variables (including static variables)
Work to minimise global variables as they permanently occupy memory in the Static Data Area. Don’t forget that variables declared within functions as static are also global albeit with only local scope.
Variables declared within a function or (even better) within a code block inside a function are placed on the stack and the memory recovered when the code block or function comes to an end.
If a C++ class that contains a destructor is created using the new keyword that returns a pointer then the destructor will not be called when the pointer goes out of scope. This can result in a memory leak.
Myclass* P = new MyClass(params);
Variable choice
When reviewing your code, it is worth spending just a few moments considering each integer variable in particular to ensure that is the correct one for a given range of values. If, in hindsight, you now know that a set of values stored in (say) an array are always going to be in the range 0 to 255 then they can be stored as bytes rather than maybe int types. Clearly, adjustments in variable choice will have more impact where arrays are in play but the odd byte saved here and there might just solve a problem.
Check any union structures to ensure that you are actually going to use the largest data type(s) declared for a given union.
Declare constants in C++ classes as static where multiple instances of a class are going to be used (or might be used).
The String class
There is a case to be made that the String class only exists to help out beginner Arduino programmers who find the manipulation of char arrays difficult. Fair enough, string manipulation in C (or any programming language) can be demanding. Languages like Visual Basic and JavaScript that abstract away string handling complexity are just hiding the problems away. Extensive use of the String class is very likely to lead to heap growth and fragmentation.
If a program is running out of memory and it is using one or more instance of the String class then it is probably time to dig out the detail on some of the available char array manipulation functions found in the string.h library. You will find notes on these in the chapter titled “Additional Library Functions”.
Having said that, the String class can be useful when a string is being constructed from multiple components; particularly if some of those components are numeric. The memory overhead of Strings can be contained if they are initialised and used within a short function as the class destructor will be called when the instance goes out of scope.
PROGMEM
You can move char arrays holding string constants to the Flash memory area used to store compiled programs. More properly, stop some or all of any char arrays holding constant (literal) strings from being copied to the SRAM memory area when the program first initialises. You can use the F() macro (and PROGMEM) to access them when needed. This is a little slower that SRAM access but could be a very useful trade off (marginal performance loss for perhaps a big memory gain). PROGMEM can also be used to store bytes, words, longs and floats but perhaps with diminishing gain.
The F() Macro
The F() macro is the simplest way to use PROGMEM for string constants (literals).
Replacing a program snippet like:
Serial.println(“Hello and welcome to the world or Arduino robotics brought to you today by an Arduino Uno”);
with
Serial.println(F(“Hello and welcome to the world or Arduino robotics brought to you today by an Arduino Uno”));
would save 90 bytes of SRAM which, while that sounds small, is not to be sniffed at when your Arduino may only have 2K available (a 4% saving). In fact, a quick trial confirmed a saving of 90 bytes of SRAM with an increase in overall Flash memory usage of 36 bytes (which was negligible in that context).
It may well be a good idea to routinely make use of the F() macro whenever a print style method is used with a string constant. Print or write methods are normally relatively slow so any additional overhead is unlikely to be noticed. The Serial.print methods are asynchronous in any case and do not unduly delay the execution of other (maybe time critical) code blocks. Worth noting that writing to any small display attached to an Arduino will almost certainly make use of a print method.
The PROGMEM variable modifier
The PROGMEM variable modifier instructs the compiler to store the relevant variable in Flash memory and not SRAM. However, variables stored in flash memory space need to be addressed using library functions as they are not accessible in the ordinary way. Given that overhead, using PROGMEM only generally makes sense if there is a good block of data that you want to locate in Flash memory. Consider the trade-off.
As a straightforward demonstration, the code below directs the compiler to store a char array and an int array in Flash memory and then retrieves the array elements in turn to output then to the Serial Monitor.
These variables need to be defined as if they were global. You can’t use the PROGMEM modifier inside a function without also applying the static modifier. There is no strict gain from defining a PROGMEM static variable within a function but you might consider doing so to assist in code readability as the intended use may then be clearer.
To retrieve the data, the code will make use of the methods included in the pgmspace.h library. This example uses strlen_P() to measure the char array as well as two functions to read the variable values.
const char welcome[] PROGMEM = {"Hello world of Arduino"}; const int someData[] PROGMEM = {234, 543, 765, 987, 295}; void setup() { Serial.begin(115200); for(int i = 0, j = strlen_P(welcome); i < j; i++) { char nChar = pgm_read_byte_near(welcome + i); Serial.print(nChar); } Serial.println(); for(int I = 0; i < 5; i++) { int nInt = (int)pgm_read_word_near(someData + i); Serial.println(nInt); } }
Your prior experience with pointer arithmetic will have explained how the two loops “index” the int and char values stored in PROGMEM.
The pgmspace.h library contains a range of methods, many of which can be used to manipulate stored strings. It is worth mentioning some additional functions used to read data stored in Flash memory. These are pgm_read_float_near() for float arrays and pgm_read_byte, pgm_read_word and pgm_read_float. An Internet search for the library name will provide a full list if you have the need to invest heavily in PROGMEM. The next code snippet uses the strcpy_P function to read a string from an array of strings (strictly a pointer array for a set of char arrays) into a prepared char array acting as a buffer.
const char jan[] PROGMEM = "January"; const char feb[] PROGMEM = "February"; const char mar[] PROGMEM = "March"; const char apr[] PROGMEM = "April"; const char may[] PROGMEM = "May"; const char jun[] PROGMEM = "June"; const char jul[] PROGMEM = "July"; const char aug[] PROGMEM = "August"; const char sep[] PROGMEM = "September"; const char oct[] PROGMEM = "October"; const char nov[] PROGMEM = "November"; const char dec[] PROGMEM = "December"; const char* const monthTable[] PROGMEM = {jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec};
void setup() { char buffer[10]; // minimum 10 as longest string is 10 byte array Serial.begin(115200); Serial.println(F("Months of the year")); for(int i = 0; i < 12; i++) { strcpy_P(buffer, (char*)pgm_read_word(&(monthTable[i]))); Serial.println(buffer); } }
PROGMEM and the F() macro are not required when writing code for ARM processor based boards. On boards such as the Arduino Due string constants remain stored in the program area where they are directly accessible to the processor at run time.
Data Structures and Organisation
The chapter titled “Data Structures” introduced a FiFo queue structure to store a variable length data set and then to subsequently release the memory used once it was no longer required. This sort of technique is an effective approach to managing peak data requirements. The same chapter also introduced the management of “look-up” data where different techniques were used to minimise the memory requirements of a data set that needs to be permanently available to the program code. There are some great opportunities for creative design to minimise memory usage and implementing them is always going to be a satisfying experience.