By dave | September 14, 2018

Evaluating static memory (SRAM) usage in an Arduino Sketch - Part 1

While writing io-abstraction library and tc-menu library I noticed that SRAM memory usage seemed to increase at a rate greater than what seemed right by static evaluation of all the objects I had created. This will become a series of articles on the subject of efficiency in microcontroller environments. In this part, we’ll look at how to evaluate memory on your device, and see how to use underlying avr tools to examine the memory requirements.


A general introduction to code analysis

When things don’t work out as expected in code, the easiest way to work out what’s gone wrong it to go back to the most basic program possible, putting back a bit of code at a time until you re-create the problem. In this case we have a suspicion that more SRAM than would be expected is being statically allocated during compilation; so the easiest way to start is to go back to absolute basics.

An important note here is that we are not interested in running any of these sketches, we are using them purely to see the effect on SRAM sketch memory.

Before reading this I recommend reading how the Arduino AVR memory model works, unless you are already very familiar with the AVR memory model.

Code analysis of an Arduino sketch

In this article we are going to evaluate the memory usage of io-abstraction library, therefore let us start with the simplest program possible. I’ve create a new Arduino sketch and called it memTesting.

void setup() {
    pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

Before pressing verify, go to Arduino preferences and enable “Show verbose output” for both compilation and upload. This is absolutely needed before proceeding..

We see that verifying this sketch, the memory usage is as follows:

Sketch uses 812 bytes (2%) of program storage space. Maximum is 32256 bytes.
Global variables use 9 bytes (0%) of dynamic memory, leaving 2039 bytes for local variables. Maximum is 2048 bytes.

Add some components back in and evaluate the difference

First we re-introduce IoAbstraction’s task manager, to see what the memory implications are are we immediately notice an up-shift in memory.

#include <TaskManager.h>

void onBlink() {
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

void setup() {
    pinMode(LED_BUILTIN, OUTPUT);
    taskManager.scheduleFixedRate(1000, onBlink);
}

void loop() {
    taskManager.runLoop();
}

When we verify the sketch, the memory usage shows as:

Sketch uses 2868 bytes (8%) of program storage space. Maximum is 32256 bytes.
Global variables use 251 bytes (12%) of dynamic memory, leaving 1797 bytes for local variables. Maximum is 2048 bytes.

Now we need to investigate all the global variables we’ve brought in. Task manager creates 6 timer tasks on AVR and 1 TaskManager.

Each TimerTask holds:

uint16_t executionInfo; // 2 bytes
uint32_t scheduledAt;   // 4 bytes
TimerFn callback;       // 2 bytes
TimerTask* next;        // 2 bytes - total 10 bytes per task.

The global taskManager instance holds:

TimerTask *first;              // 2 bytes
uint8_t numberOfSlots;         // 1 bytes
InterruptFn interruptCallback; // 2 bytes
uint8_t lastInterruptTrigger;  // 1 byte
bool interrupted;              // 1 byte  - total of 7 bytes

We can see that task manager on AVR architecture needs (10 * 6) + 7 or 67 bytes minimum. We’d certainly not expect more than 100 bytes unless something else is inadvertently being included. We need to see where the other 100 bytes are going.

Looking in the output log highlights where the extra 100 bytes are going, IoAbstraction is bringing in the Wire library. Wire is needed by some components in the library, and even though these components are not always used it incurs about a 150 byte penalty. In nearly all cases we would want Wire with IoAbstraction so this is not an issue.

Linking everything together...
"~/Library/Arduino15/packages/arduino/tools/avr-gcc/4.9.2-atmel3.5.4-arduino2/bin/avr-gcc" ...
"~/Library/Arduino15/packages/arduino/tools/avr-gcc/4.9.2-atmel3.5.4-arduino2/bin/avr-objcopy" ...
"~/Library/Arduino15/packages/arduino/tools/avr-gcc/4.9.2-atmel3.5.4-arduino2/bin/avr-objcopy" ...
Using library IoAbstraction at version 1.0.3 in folder: ~/Documents/Arduino/libraries/IoAbstraction 
Using library Wire at version 1.0 in folder: ~/Library/Arduino15/packages/arduino/hardware/avr/1.6.21/libraries/Wire 

Just by static analysis of the Wire class, we can immediately see more than 70 bytes are needed for class TwoWire. But this does not come close to the actual used memory. We will now need to use the avr-objdump command in order to find out a bit more about memory usage. On all platforms the command is shipped with the Arduino IDE.

  • Windows locate the Arduino install and it should be in Java/hardware/tools/avr/bin/avr-objdump
  • MacOS /Applications/Arduino.app/Contents/Java/hardware/tools/avr/bin/avr-objdump

Finding where the output cache is stored can be a little tricky, I found it by searching for .ino.elf in the verbose output. Once you’ve got the output cache directory, change into it and you’ll see sketchName.ino.elf file in there. It’s this file we’ll be using to list the data and bss (SRAM) sections. To extract a detailed report of these sections:

avr-objdump -x memTesting.ino.elf

Within this map there are two memory areas we are immediately interested in .bss and .data, as these two seem to roughly equate to the SRAM storage. We are going to look at the bss and data sections.

                        RAM use  Which function
00800190 l     O .bss	00000004 timer0_millis
00800165 l     O .bss	00000001 twi_state
0080013f l     O .bss	00000001 twi_txBufferLength
0080011d l     O .bss	00000020 twi_txBuffer
0080011c l     O .bss	00000001 _ZN7TwoWire13rxBufferIndexE
008001a0 l     O .bss	00000043 taskManager                     <-- task manager 67 bytes
00800194 l     O .bss	0000000c Wire
008001a9 l     O .bss	0000001e switches                        <-- switches 30 bytes
0080018f l     O .bss	00000001 timer0_fract
0080018b l     O .bss	00000004 timer0_overflow_count
00800164 l     O .bss	00000001 twi_error
0080018a l     O .bss	00000001 twi_slarw
00800189 l     O .bss	00000001 twi_masterBufferIndex
00800188 l     O .bss	00000001 twi_masterBufferLength
00800168 l     O .bss	00000020 twi_masterBuffer
00800167 l     O .bss	00000001 twi_sendStop
00800166 l     O .bss	00000001 twi_inRepStart
00800163 l     O .bss	00000001 twi_rxBufferIndex
00800143 l     O .bss	00000020 twi_rxBuffer
00800141 l     O .bss	00000002 twi_onSlaveReceive
00800140 l     O .bss	00000001 twi_txBufferIndex
0080013d l     O .bss	00000002 twi_onSlaveTransmit
0080010a l     O .data	00000012 _ZTV7TwoWire
00800100 l     O .data	00000004 intFunc    

First off, we see that Wire needs twi, and the twi utilities on their own are about 100 bytes.

We see immediately that just including IoAbstraction brings in switches too, at a minimum cost of about 32 bytes. This is still not that bad, we can use IoAbstraction including switches and Wire library for about 200 bytes.

Switches is made up by default of 4 KeyboardItem classes which holds:

KeyPressState state;        // 1 byte
uint8_t pin;                // 1 byte
uint8_t counter;            // 1 byte
uint8_t repeatInterval;     // 1 byte
KeyCallbackFn callback;     // 2 bytes - total 6 bytes

and a SwitchInput which has 4 of the above included:

RotaryEncoder* encoder;     // 2 bytes
IoAbstractionRef ioDevice;  // 2 bytes
KeyboardItem keys[MAX_KEYS];// 6 * MAX_KEYS(4) is 24
uint8_t numberOfKeys;       // 1 byte
uint8_t swFlags;            // 1 byte - total 30 bytes

Using an extra global from the same library has little effect

So now we will add to our sketch, we’ll add in support for switches and a rotary encoder firstly on Arduino pins.

#include <IoAbstraction.h>

void onBlink() {
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

void onSwitch(uint8_t pin, bool heldDown) {
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

void setup() {
    switches.init(ioUsingArduino(), SWITCHES_POLL_EVERYTHING, true);
    switches.addSwitch(10, onSwitch);
    taskManager.scheduleFixedRate(1000, onBlink);
}

void loop() {
    taskManager.runLoop();
}

At this point when we compile we see a mild increase in memory usage

Sketch uses 4950 bytes (15%) of program storage space. Maximum is 32256 bytes.
Global variables use 283 bytes (13%) of dynamic memory, leaving 1765 bytes for local variables. Maximum is 2048 bytes.

We now analyse the heap again using the tool chain and in addition to before we’re paying the cost of a virtual table which is more significant in RAM that I had expected, there’s lots of discussions about this including [https://www.avrfreaks.net/forum/unhappy-c-virtual-tables-sram]

                        RAM use  Which function
0080010a l     O .data	00000016 _ZTV18BasicIoAbstraction
00800120 l     O .data	00000012 _ZTV7TwoWire

In summary for this section, the memory increase occurred when we included IoAbstraction, no further increase occurred once it was in place.

Now we are going to add an item that has no static memory allocated to it - it’s allocated at runtime, these are more dangerous as we can’t see them in our sketch. But the encoder object will add about 20 extra bytes at runtime.

#include <IoAbstraction.h>

void onBlink() {
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

void onSwitch(uint8_t pin, bool heldDown) {
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

void encoderHasChanged(int newVal) {
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

void setup() {
    switches.init(ioUsingArduino(), SWITCHES_POLL_EVERYTHING, true);
    switches.addSwitch(10, onSwitch);
    setupRotaryEncoderWithInterrupt(2, 3, encoderHasChanged);
    taskManager.scheduleFixedRate(1000, onBlink);
}

void loop() {
    taskManager.runLoop();
}

And upon verifying we see the memory usage increase slightly:

Sketch uses 4950 bytes (15%) of program storage space. Maximum is 32256 bytes.
Global variables use 283 bytes (13%) of dynamic memory, leaving 1765 bytes for local variables. Maximum is 2048 bytes.

It also has several virtual functions, so it also has a virtual table overhead again of 20 bytes, along with the runtime overhead:

                        RAM use  Which function
0080012a g     O .data	0000000a .hidden _ZTV13RotaryEncoder
00800120 g     O .data	0000000a .hidden _ZTV21HardwareRotaryEncoder

Lastly we add something that actually needs Wire

We know that further up, compilation of the library brought in Wire, but what happens when we use it, in this example we switch from ioUsingArduino to ioFrom8574 which actually requires the serial Wire library.

#include <IoAbstraction.h>
#include <IoAbstractionWire.h>

void onBlink() {
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

void onSwitch(uint8_t pin, bool heldDown) {
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

void encoderHasChanged(int newVal) {
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

void setup() {
    switches.init(ioFrom8574(0x20), SWITCHES_POLL_EVERYTHING, true);
    switches.addSwitch(10, onSwitch);
    setupRotaryEncoderWithInterrupt(2, 3, encoderHasChanged);
    taskManager.scheduleFixedRate(1000, onBlink);
}

void loop() {
    taskManager.runLoop();
}

We see now that the sketch uses significantly more memory:

Sketch uses 6162 bytes (19%) of program storage space. Maximum is 32256 bytes.
Global variables use 394 bytes (19%) of dynamic memory, leaving 1654 bytes for local variables. Maximum is 2048 bytes.

There are two additional overheads here, firstly we need an extra virtual function table for the ioFrom8574 abstraction but that is dwarfed by the TwoWire object at over 100 bytes.

0080010a g     O .data	00000016 .hidden _ZTV20PCF8574IoAbstraction
008001a1 l     O .bss	00000001 _ZN7TwoWire13rxBufferIndexE
008001a0 l     O .bss	00000001 _ZN7TwoWire14rxBufferLengthE
00800180 l     O .bss	00000020 _ZN7TwoWire8rxBufferE
0080017f l     O .bss	00000001 _ZN7TwoWire12transmittingE
0080017e l     O .bss	00000001 _ZN7TwoWire14txBufferLengthE
0080017d l     O .bss	00000001 _ZN7TwoWire13txBufferIndexE
0080015d l     O .bss	00000020 _ZN7TwoWire8txBufferE
0080015c l     O .bss	00000001 _ZN7TwoWire9txAddressE

In part 2 we cover memory implications of Arduino compilation, the overhead of virtual functions and a deeper dive into memory usage for Wire.

Part 2: Library compilation, virtual function analysis and Arduino Wire memory usage

Other pages within this category

comments powered by Disqus

This site uses cookies to analyse traffic, and to record consent. We also embed Twitter, Youtube and Disqus content on some pages, these companies have their own privacy policies.

Our privacy policy applies to all pages on our site

Should you need further guidance on how to proceed: External link for information about cookie management.

Send a message
X

Please use the forum for help with UI & libraries.

This message will be securely transmitted to our servers.