Submitted by mdiaconescu on
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).
- .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 usingfree
) 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 else 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
andrealloc
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 }