Optimize Arduino Memory Usage

mdiaconescu's picture

Did your Arduino gone "crazy", without obvious reasons, and restarts or resets by itself? Did your device started to misbehave but you are 100% sure that your code is correct? In such cases, one of the possible cause is the lack of free RAM (random access memory). In other words, your MCU does not have sufficient free RAM to perform the required task(s).

Random Access Memory: types and differences

There are two main types of RAM available in embedded devices: SRAM (static random access memory) and DRAM (dynamic random access memory). While SRAM is faster in read/write/access operation, it is also more expensive and usually takes more physical space. On the other hand, DRAM is generally slower in read/write/access operations (this improves with each generation), but cheaper to produce and usually smaller with respect to its physical size.

No matter which type of RAM (SRAM or DRAM) uses an embedded device, the following discussion stands. Many of the MCUs used by Arduino boards (e.g., ATmega328p in Arduino UNO v3 and ATmega2560 in Arduino MEGA2560) use SRAM memory, but unfortunately only in small quantities (e.g., 2KB for ATmega328P and 8KB for ATmega2560), thus special care is required in writing the code. For the rest of the discussion, we only use the RAM term for both, SRAM and DRAM.

RAM Diagnose: when heap meets stack

At first, we need to check if the problem is caused by insufficient free RAM, and not by various other possible reasons, such as a defective MCU, problem with peripherals or even non-obvious code bugs. Debugging an Arduino is not really easy since it does not "beeps" on error, does not show blue screens and also does not trigger popup windows telling you which is the possible problem. The RAM available in an Arduino MCU is organized as shown in the picture below (picture linked from: avr-libc).

8 bit Atmel MCUs SRAM structure

  • .data variables is the first RAM section and it is used to store program static data, such as strings, initialized structures and global variables.
  • .bss variables is the memory allocated for uninitialized global and static variables.
  • heap is the dynamic memory area, and this is the playground area for malloc (and alike). The heap can grow (when new allocation is made) or "possibly" decrease in size (when memory is released, as for example when using free) based on the requirements.
  • stack is the memory area located at the end of the RAM and it grows towards the heap area. The stack area is used for function calls, storing values for local variables. Memory occupied by local variables is reclaimed when the function call finished.
  • external RAM is only available to some of the MCUs and it means that it is possible to add RAM in a kind of similar way that we do for a PC. Usually this is expensive (a few KB of external RAM costs in general more than the MCU) and requires also advanced hardware and software skills.
  • free available memory is the area between heap and stack and this is what we need to measure in order to detect problems caused by not enough RAM resources.When this area is either too small for the required tasks, or is missing at all (heap meets stack), our MCU starts to missbehave or to restart itself.

The following C/C++ method definition allows to compute the free memory (in bytes) for an Arduino MCU. It works with both, Arduino IDE and also with other tools such as AvrStudio:

extern unsigned int __bss_end;
extern unsigned int __heap_start;
extern void *__brkval;

uint16_t getFreeSram() {
  uint8_t newVariable;
  // heap is empty, use bss as start memory address
  if ((uint16_t)__brkval == 0)
    return (((uint16_t)&newVariable) - ((uint16_t)&__bss_end));
  // use heap end as the start of the memory address
    return (((uint16_t)&newVariable) - ((uint16_t)__brkval));

The getFreeRam function defines a new variable (named newVariable), which being a local variable of a function will be stored in the stack. Because the stack memory area grows towards the heap, the memory address of this new variable is the last memory address used by the stack at the moment of calling this method. The __brkval is a pointer to the last memory address (towards the stack) used by the heap. We don't have to worry about the management of __brkval since this is done internally. We also have to be sure that the heap is not empty, because then __brkval can't be used (it is a NULL pointer). If the heap is empty, then we use __bss_end which is a variable internally defined, and it is stored in the last part of the .bss variables RAM area

The free amount of RAM represents the differences between the address used by our newVariable variable and the __brkval referenced address ( or the address of __bss_end if the heap is empty). This give us the number of unused bytes on 8bits MCUs, such as the ones used by the Arduino (with the exception of Arduino DUE, which uses an ARM 32 bits MCU).

The above code works with most of the Arduino MCUs (up to 64KB RAM), and in case you find one which does not, please report.

NOTE: the above discussion represents a simplified story of the RAM division and its management. Our intention was to provide an explanation for everyone (the beginner and also the advanced programmer) without going in "black hole" details.

Ram Usage Optimization: stack or heap?

Knowing that the problem comes from the lack of RAM resources, what can we do to fix it? There are at least two ways: use an MCU with more RAM resources, or optimize your code for a better management of existing RAM resources. While in some cases the first method is acceptable (the actual prices of the MCUs are quite low), there are many other cases when this is not a real solution, e.g., if the hardware already exists and new options need to be added to it. We discuss further how to optimize the RAM usage, which in many cases is the way to go for your Arduino.

Avoid using dynamic memory allocation

While using dynamic memory allocation is a good solution when programming a normal PC with multiple hundreds of megabytes, gigabytes or even terabytes of ram, it is in general a bad idea for embedded devices (such as the Arduino family). The problem with dynamic memory allocation is that may easily produce memory (heap area) fragmentation. Memory fragmentation can be seen as small "holes" in the RAM which can't be reused in many cases.

Lets take an example. Say that 8 bytes of memory are allocated with a malloc call,then another 16 bytes are allocated with another malloc call. As a result, we have 24 bytes of continuous allocated heap memory. Later, since the first 8 bytes of memory are no longer used, we decide to reclaim it, with a free call, hopping to gain that memory for later usage. Indeed, the memory is freed up, but additionally, we have also created a "hole" in the heap. Why this is bad? Well, if now we need to allocate 10 more bytes (or any number greater than 8) of memory, the heap is increased because the 8 bytes free memory (the heap hole) are not sufficient. Memory allocation with malloc calls (also when using calloc or realloc) works with continuous memory areas. If any time later 6 bytes of memory needs to be allocated, these can use a part of the "hole", but the two remaining bytes (was a 8 bytes area) are now isolated and have a big probability to never be used. Repetitions of what we described above can and will result in a big heap size with small unusable (in most of the cases) memory holes. Thus, sooner or later, the heap and stack collision becomes hard to avoid, (remember, the stack grows towards the heap and the heap grows toward the stack). When these two areas meet (or collide), strange things start to happen, such as auto-resets.

A few simple rules may help to avoid RAM fragmentation:

  • use stack instead of heap whenever possible - stack memory is preferred because the memory is complete freed up when the function returns, and also the stack memory is fragmentation free. In general, this means using local variables and avoid using dynamic memory allocation( i.e., malloc, calloc and realloc calls).
  • avoid using global and static data whenever possible - the memory area (.data variables and .bss variables) occupied by these variables is never freed up for the live time of the same program.
  • when using strings is a must, then it is important to keep them as short as possible - remember, each single char takes one byte of RAM (the entire 2KB RAM memory of an ATmega328p can be occupied by a string with a length of 2048 chars). 
  • when using arrays, try to keep their length at minimum - if later you really need a different length, just increase/decrease it the and reprogram your MCU.

Use appropriate types for variables/fields

In general, the programmers are tempted to use datatypes with a larger range than actually needed, in many cases, the reason being "who knows, maybe later I need a greater value". For example, one may define an integer (using int or short types) variable when actually the values of the variable are only positive numbers lower than 100. This is a bad idea no matter if we program a low resource device, such an MCU, or a normal PC application. Remember, we can change the variable type later, if a larger range is really required for that variable.

The following table provides the most used C/C++ types to be used when programming low resource devices (but not only):

Datatypes Size in Bytes Values
boolean, bool 1 true(1) or false(0)
char 1 ASCII character or signed value in the range [-128, 127]
unsigned char, byte, uint8_t 1 ASCII character or unsigned value in the range [0, 255]
int, short 2 signed value in the range [-32768, 32767]
unsigned int, word, uint16_t 2 unsigned value in the range [0, 65535]
long 4 signed value in the range [2147483648, 2147483647]
unsigned long, uint32_t 4 unsigned value in the range [0, 4294967295]
float, double 4

floating point value in the range [-3.4028235e+38, 3.4028235e+38]

NOTE: float and double are the same in this (Arduino) platform

Be responsible and try to use the type which both, fits with the requirements for your data but also is the one with the lowest number of bytes used for memory storage. Just another example to convince you: an array with 128 elements of type uin16_t instead of uint8_t uses 128 bytes more RAm. That is 6.25% of the total memory for an Arduino UNO v3, and it is occupied just because we have used a wrong type for an array variable!

Use PROGMEM for "constant" data

In many cases, a large amount of RAM is taken by the static memory (.data variable RAM area), as a result of using global variables (such as strings or numbers). Whenever this data is not likely to change, it can easily be stored in the so called PROGMEM (program memory). This represents a piece of the flash memory, and it is good to know that in general the flash memory is many times larger as the RAM (e.g., ATmega2560 has 8KB RAM and 256KB flash). The disadvantage of using PROGMEM is the reading speed, which is slower compared with reading the same data from RAM.

The general way to define a PROGMEM variable is:

#include <avr/pgmspace.h>

const PROGMEM datatype varName[] = {v0, v1, v2...};

For example, we define a string and the set of first seven prime numbers, and require to store them in the PROGMEM area, as follows:

#include <avr/pgmspace.h>

const PROGMEM char errorMsg[] = {"Invalid code!"};
const PROGMEM uint8_t primes[] = { 1, 3, 5, 7, 11, 13, 17};

We need to include pgmspace.h for being able to use PROGMEM. Later, reading back the set of first seven prime numbers, can be done as follows:

for ( uint8_t k = 0; k < 7; k++) {
  uint8_t prime = pgm_read_byte_near( primes + k);

  // now, do something with the prime number stored in the "prime" variable