Memory Management in Embedded C/Arduino

by akshulg in Circuits > Microcontrollers

562 Views, 1 Favorites, 0 Comments

Memory Management in Embedded C/Arduino

Screenshot from 2021-07-12 20-32-43.png

Controllers are the chips which are designed to perform very specific tasks. They have very limited memory, like 32KB Flash and 2KB RAM in ATmega328p. A lot of the time you don’t know what other requirements will come in for your product. In that case you need to add some more functionality to your firmware. If you keep adding more functionalities and keep interfacing more sensor/peripheral , you will run out of memory pretty quickly. So it is very important to take care of the memory otherwise before you know it there will be memory leaks and it will be infuriating to pin point the possible culprits. To make your life easier here are a few tips.

Before We Start, Here Is a Typical SRAM Mapping.

Static is the memory slab in SRAM in which global variables, strings and structures are stored.

Stack in the portion where local variables and function calls are stored but the memory is also released as soon as the function terminates.

Heap is the portion where dynamic data is stored and used for dynamic memory allocation. But when allocated memory is freed then they left holes and can only be used for memory allocation of same or less size. Now Heap starts after the Static and Stack starts from the other end of SRAM. When used, heap and stack expands towards each other and the memory space between them, free memory, shrinks.

String Object

Let’s start with the most infuriating one. This one exists in the Arduino environment to make it easier for you to write code but it comes with a price. This is how one use it.

String greeting = "Hello World"void setup()
{
    Serial.begin(9600);
}
void loop()
{
    Serial.println(greeting);
}

/*
Memory usage - 
Sketch uses 2858 bytes (8%) of program storage space.
Global variables use 280 bytes (13%) of dynamic memory
*/

Instead if one use a character array, then it will be like this,

char greeting[] = "Hello World"void setup()
{
    Serial.begin(9600);
}
void loop()
{
    Serial.println(greeting);
}

/*
Memory usage - 
Sketch uses 1494 bytes (4%) of program storage space.
Global variables use 264 bytes (12%) of dynamic memory
*/

As you can see using String object is taking 1.36 KB extra space in program memory and 16 bytes more in RAM. Also, String uses dynamic memory allocation.

Dynamic Memory Allocation

This refers to assigning memory block to a variable on runtime and it uses heap to do so. Dynamic memory allocation is done using c functions, namely, malloc(), calloc(), realloc(), and free(). Though it is a nice way to allocate memory on the go when one is not sure how much space a variable might need, it creates holes in heap. This means when you assign a variable with a dynamic memory, you can free up that memory space/gaps after you are done using the variable. But that space is only limited to a variable of the exact same space. If you need to assign a variable with size more than the previous one you will not be able to use that space. This leads to unnecessary memory leaks and before you know it, your heap usage keeps increasing and you will be left with low RAM availability and your code might crash.

PROGMEM

When you are low on RAM, you should use Flash/Program memory to store constant values and arrays (which are defined as global variables) instead. You can use it like this,

//Code #1
#define F_CPU 16000000UL
#include 
const uint16_t constant_value = 2;
int variable = 1;int main()
{
 variable = variable * constant_value;
}
/*
Memory usage - 
Program:     184 bytes (0.6% Full)
(.text + .data + .bootloader)Data:          4 bytes (0.1% Full)
(.data + .bss + .noinit)text    data     bss     dec     hex filename
180       4      0     184      b8 *.elf
*/
//Code #2 #define F_CPU 16000000UL #include #include const PROGMEM uint16_t constant_value = 2; int variable = 1;int main() { variable = variable * constant_value; } /* Memory usage - Program: 184 bytes (0.6% Full) (.text + .data + .bootloader)Data: 2 bytes (0.2% Full) (.data + .bss + .noinit)text data bss dec hex filename 182 2 0 184 b8 *.elf */

In code #2 we use PROGMEM to store the constant, so 2 bytes that were previously used in .data(SRAM) in code #1 is now shifted to .text(Program memory). To understand memory sections, .text, .data, etc refer to nongnu’s article.

Another example is how we use Serial.print() function in arduino. Using F() will store the String variable inside the function in Program memory.

Serial.print(F(“String”));

Constants

When there is a value which will never be changed throughout it’s life while the code is running, then we should define it using const or #define

//Code #1
#define F_CPU 16000000UL
#include uint16_t constant_value = 2;
int variable = 1;int main()
{
 variable = variable * constant_value;
}
/*
Memory usage - 
Program:     202 bytes (0.6% Full)
(.text + .data + .bootloader)Data:          4 bytes (0.2% Full)
(.data + .bss + .noinit)text    data     bss     dec     hex filename
198       4       0     202      ca *.elf
*/
//Code #2 #define F_CPU 16000000UL #include const uint16_t constant_value = 2; int variable = 1;int main() { variable = variable * constant_value; } /* Memory usage - Program: 184 bytes (0.6% Full) (.text + .data + .bootloader)Data: 4 bytes (0.2% Full) (.data + .bss + .noinit)text data bss dec hex filename 180 4 0 184 b8 *.elf */

As you can see from memory usage of both codes, #2 have low .text(Program memory usage), no changes in SRAM usage though. This is because declaring it as const will decrease the number of instructions used in the Program memory.

Defining Variable Size

It’s very important to define variables according to your need. If you need a 8 bit variable and defining it as 16 bit then it’s a waste of space. uint8_t, uint16_t, uint32_t are variable assigning for 8, 16 and 32 bit integers respectively. If you have a variable flag, i.e., a variable that switches between 1 and 0 then use bool.

Assign your array size only to a maximum index that you might use. union is a data type where all the members share the same memory location. When you have a bunch of variables that are mutually exclusive, you can use union to save up space.

Pin Toggling

Take a look at the following code. The application is to switch LED using an external input, for example, using UART.

//Code #1
#define LED 5
bool led_state = 0;
void setup()
{
    pinMode(LED, OUTPUT);
    Serial.begin(9600);
}void loop()
{    
     led_state = Serial.read();
     if(led_state == 1){digitalWrite(LED, HIGH);}
     else{digitalWrite(LED, LOW);}
}
/*
Memory usage - 
Sketch uses 1742 bytes (5%) of program storage space.
Global variables use 249 bytes (12%) of dynamic memory
*/

//Code #2
#define LED 5
bool led_state = 0;
void setup()
{
    pinMode(LED, OUTPUT);
    Serial.begin(9600);
}void loop()
{    
     led_state = Serial.read();
     digitalWrite(LED, led_state);
}
/*
Memory usage - 
Sketch uses 1734 bytes (5%) of program storage space.
Global variables use 249 bytes (12%) of dynamic memory
*/

UART Buffer Size

You can change the UART transmit and receive buffer size according to your code’s requirement. By default it is defined as 64 bytes in Arduino environment but if you need less than 64 bytes (or more in some case) then you can change that and save up some space. You can find the buffer definition in,

/arduino-1.8.5-linux64/arduino-1.8.5/hardware/arduino/avr/cores/arduino/HardwareSerial.h

#define SERIAL_TX_BUFFER_SIZE 64
#define SERIAL_RX_BUFFER_SIZE 64

Third Party Libraries

Most of the times these libraries available on Github are pretty general. They have all the possible functions defined within so that you can find any function that fits your requirement. But a lot of times you will never use a majority of available functions. Striping the libraries to only the useful functions will help you save a lot of memory.

Monitoring Runtime Memory Usage

Sometimes it’s not possible to use all the above tips in your code. In that case monitoring the runtime available memory of your code is very beneficial to track possible leaks. You can use this github repo to do so.

Concluding

Hopefully these tips will help you make more optimized and cleaner codes.
All of the above codes are available on my github for you to tinker and understand better. Go check it out. Stay Tuned!