One of the biggest challenges of microcontrollers is the small quantity of RAM that they offer.

As a reminder, here are the RAM sizes of the most common microcontrollers:

Microcontroller RAM capacity Boards
ATmega328 2 KB
  • Arduino UNO
  • Arduino UNO WiFi
  • Arduino Nano
  • Adafruit Metro Mini 328
  • SparkFun RedBoard
ATmega32u4 2.5 KB
  • Arduino Leonardo
  • Arduino Micro
  • Adafruit Feather 32u4 Bluefruit LE
  • Arduino Yún
  • Teensy 2.0
ATMega4809 6 KB
  • Arduino Nano Every
  • Arduino UNO WiFi Rev 2
ATmega2560 8 KB
  • Arduino Mega 2560
AT90USB1286 8 KB
  • Teensy++ 2.0
MKL26Z64VFT4 8 KB
  • Teensy LC
MK20DX256VLH7 64 KB
  • Teensy 3.2
ESP8266 96 KB
  • Adafruit HUZZAH
  • SparkFun ESP8266 Thing
  • WeMos D1 Mini
MK64FX512 192 KB
  • Teensy 3.5
MK66FX1M0 256 KB
  • Teensy 3.6
ESP32 520 KB
  • Adafruit HUZZAH32
  • LoLin D32 Pro
i.MX RT1066 1 MB
  • Teensy 4.0

Some ESP32s come with a large external PSRAM that you can use with ArduinoJson.

Memories on microcontrollers are several orders of magnitude smaller than what we have in computers; that’s why it’s imperative to use special techniques to consume less memory in our program. Some of these techniques are counter-intuitive, and most are radically different from what you usually think is right. If you are used to writing applications for computers, phones, or tablets, you need to adopt a different mindset and challenge your assumptions.

On this page, we’ll see how we can write an Arduino program that consumes less memory. Of course, the focus is on JSON serialization and deserialization, but most tips are applicable even when you don’t use ArduinoJson.

Tip 1: Avoid duplication if the input is in memory

Remember that ArduinoJson’s deserializer supports two modes:

  1. the zero-copy mode, used when the input is writeable (char*)
  2. the normal mode, used when the input is read-only (const char*, String)

In the second mode, the deserializer duplicates the strings from the input into the JsonDocument. So, when the input is already in memory (i.e., not in a stream), you should use the zero-copy mode.

Good: zero-copy
char[] json = "{\"hello\":\"world\"}";
deserializeJson(doc, json);
Bad: duplication
const char* json = "{\"hello\":\"world\"}";
deserializeJson(doc, json);
Bad: duplication
String json = "{\"hello\":\"world\"}";
deserializeJson(doc, json);
See also:

Tip 2: Pass Stream directly to the deserializer

A stream (Stream or std::istream) is a volatile bytes source, so its content needs to be copied in RAM. The best way to deal with streams is to let ArduinoJson copy the content because it ignores everything it can: punctuation, spaces, comments, and duplicated strings.

To do that, simply pass the stream to deserializeJson() or deserializeMsgPack().

Good: pass the Stream directly
File file = SD.open(filename);
deserializeJson(doc, file);
Bad: pass a copy of the input
char buffer[256];
File file = SD.open(filename);
file.read(buffer, 256);
DynamicJsonDocument doc;
deserializeJson(doc, buffer);
See also:

Tip 3: Move longs strings literals to Flash memory

Flash (or PROGMEM) strings are excellent to reduce the RAM usage of a program: contrary to regular string literals, they only use RAM when the program pulls them from the Flash.

This advice is only relevant on Harvard architectures (mainly AVR and ESP8266).
It’s not applicable to von Neumann architectures (such as ESP32, megaAVR, and ARM).
See the first chapter of Mastering ArduinoJson for an explanation.

Good: long strings literals in Flash
doc[F("description")] = F("Lorem ipsum dolor sit amet, consectetur adipiscing elit.");
Bad: long strings literals in RAM
doc["description"] = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
Good: log strings in Flash
Serial.print(F("deserializeJson() failed with error code "));
Serial.println(err.f_str());
Bad: log strings in RAM
Serial.print("deserializeJson() failed with error code ");
Serial.println(err.c_str());

Caution: the compiler doesn’t deduplicate strings created by the F() macro. Don’t repeat the same string multiple times; instead, save the pointer in a variable of type const __FlashStringHelper*.

Tip 4: Filter input

Since ArduinoJson 6.15, you can tell deserializeJson() to filter the input to keep only the relevant parts.

This technique only makes sense if the input contains a lot of information that you are not interested in. A typical example is a web service like OpenWeatherMap, which returns many fields, but only a few are relevant to your project.

For the implementation details, see:

Tip 5: Deserialize in chunks

One neat feature of ArduinoJson is that, when it parses an object from a Stream, it stops reading when it encounters the closing }, and the same is true for arrays. Using this feature, you don’t have to deserialize the whole JSON document at once. Instead, you can parse only a part of it and repeat the operation.

This technique works great when your input contains an array of nested objects. For example, if you want to parse the huge response of a 10-day forecast of Weather Underground, you can skip the beginning until you see "forecastday": [ in the stream (use Stream::find()), and then parse the objects for each day one after the other.

As usual, don’t reuse the JsonDocument: declare it inside the loop.

For the implementation details, see:

Tip 6: Prefer stack to heap memory

Allocating and deallocating in the heap cause overhead and fragmentation, so the program can use much less RAM that there is actually on the device. Heap allocation happens anytime you use malloc(), new, and String.

ArduinoJson uses the stack with StaticJsonDocument and the heap for DynamicJsonDocument. For small JsonDocument (let’s say under 1KB), prefer a StaticJsonDocument.

If you’re using a microcontroller with very limited RAM (for example, the ATmega328 of an Arduino UNO ), you should not use the heap at all.

Good: only stack memory
char[] json = "{\"hello\":\"world\"}";
StaticJsonDocument<200> doc;
deserializeJson(doc, json);
Bad: only heap memory
String json = "{\"hello\":\"world\"}";
DynamicJsonDocument doc;
deserializeJson(doc, json);

Tip 7: Avoid global variables

Global variables are harmful on many levels; we’ll only look at the memory usage.

The problem with global variables is that, by definition, they live during the whole execution of the program. In other words, they always consume RAM, whether the program uses them or not. Contrast that with local variables that have a short lifespan; they only consume memory when the program needs them.

My advice is: reduce the number and the size of global variables to the strict minimum.

Tip 8: Avoid duplication of String

Remember that the String class always makes a copy of the string passed to the constructor.

For example, the following line makes a copy of “hello world”:

String s = "hello world"; // one copy

Yes, it means that “hello world” is present twice in RAM: in the global section and in the heap. If you just need to give a name to this variable, it’s better to use the actual type of the string literal, which is const char*:

const char* s = "hello world"; // no copy

Another common mistake is to declare a function that takes a parameter of type String by value:

void f(String s) {
  // ...
}

Because the function parameter is a value (not a pointer, nor a reference), each invocation of the function creates a new String, thereby creating another copy of the string. To avoid this useless duplication, use a const-reference:

void f(const String& s) {
  // ...
}
See also:

Tip 9: Reduce the size of variables

In situations where every byte counts, you need to make sure that every variable is as small as possible.

For example, suppose you have:

int value = 42;

If you’re sure that value will never exceed 127, you can use a char instead of an int:

char value = 42;

Depending on the platform, this can divide the size of this variable by four.

Of course, you save even more memory if it’s an array or a struct. Consider:

int values[32]; // 128 bytes on 32-bit architecture

You can reduce significantly by using char instead:

char values[32]; // 32 bytes on any architecture

This technique works for local variables, but you can also use it for function parameters. Indeed, when passed by value, function arguments are copied to the stack, so you can save some stack memory by reducing the parameters’ size.

For example, consider changing:

void f(int value);

to

void f(char value);

I know it seems to be a small gain, but remember that most functions call other functions, which call other functions… At some point, the stack becomes a big sandwich with the arguments to many functions. If you reduce the size of the arguments, you can save a lot of stack memory.

Keep learning

Mastering ArduinoJson

If you need more tips or if there are some notions you don’t understand, check out Mastering ArduinoJson.

In particular, the second chapter might interest you, it covers:

  • stack and heap
  • pointers and references
  • strings (including Flash strings and String)

But that’s not all. The last chapter contains several case studies to illustrate how to write clean and efficient code with ArduinoJson. For example, it shows how to parse the huge response from OpenWeatherMap with limited memory.

Global warming stripes by Professor Ed Hawkins (University of Reading)