The ESP8266 has precious little RAM. You start with a little over 80KB, but the system takes up over 20K of that just for wifi and other required functionality. So best case you get under 60K of RAM for your use. In fact, if you use the Arduino toolchain you’ll be lucky to be left with 30K.

Lots of things you wouldn’t expect use up that RAM. If you’re using C++ and have virtual classes, each one will have a vtable, a little hidden piece of RAM that has pointers to virtual functions. That doesn’t mean you shouldn’t use C++ or even virtual functions. You just need to take care to minimize the number of virtual functions you use.

String constants are typically the biggest offenders. Since they’re constants, you’d think they would go into Flash. But on the Esp Flash memory has access restrictions. You have to access memory on 4 byte boundaries. Attempting to access the bytes of a string stored in Flash would cause a crash. Other data structures like constant arrays used for initialization go into RAM as well. If you’re not careful, you’ll use up all that precious RAM before executing a line of code.

It’s easy to specify that a string or other data structure go into Flash, but I’ve seen a lot of confusion about how to access this memory, so I’ll try to clarify.

Getting your data into Flash memory

The ESP compiler allows you to specify which section a data value should go into. For instance adding this global:

int8_t value __attribute__((section(".irom.text"))) = 'A';

will place that byte in Flash memory. If you then try to access this value you will quickly crash. So what good is it? That’s where accessors come in. Before you continue let me say that you actually need to worry about one more thing. The above attribute will put the value in Flash, but it won’t necessarily align it, so even with an accessor you might get the wrong byte. Another attribute:

__attribute__((aligned(4)))

needs to be added. Now you can access that byte by reading a 32 bit value at its address, which will not crash, and then pick out the first byte and discard the rest. This can get pretty messy pretty quickly. But if you have a good set of accessors and macros, you can save lots of RAM and have good looking code.

The Arduino toolchain makes it pretty easy to put strings in memory. But that’s all derived from its AVR origins, which makes it pretty restrictive. The ESP SDK itself has a very cool feature to help with this. If you turn it on with:

#define USE_OPTIMIZE_PRINTF

then every time you use:

os_printf("This is a nice string\n");

That string will go into Flash. It does this by creating a static variable with the attribute shown above. And os_printf in the SDK will first read the passed string into a temporary RAM buffer using 4 byte aligned reads and output that. It’s nice and simple, but how do you extend that to work elsewhere?

A set of Flash accessors

My m8rscript library has a full set of Flash accessors. I put them directly into my library because those in the SDK are a confusing hodgepodge. There are 2 macros which add the appropriate attributes:

#define RODATA_ATTR  __attribute__((section(".irom.text"))) __attribute__((aligned(4)))
#define ROMSTR_ATTR  __attribute__((section(".irom.text.romstr"))) __attribute__((aligned(4)))

You use the latter for all your strings and the former for everything else. Why two? I found that there are cases where trying to put strings in flash with other data types fails because the compiler can’t figure out how to align them. So I added a new section and added it to my own custom .ld file. This places all the strings together so they can be properly aligned.

Now you can go:

int8_t value RODATA_ATTR = 'A';

And the value will go into Flash and be properly aligned. There’s also an accessor function. You can do this:

int8_t v = readRomByte(&value);

And you’ll get your value without any crashes. You can extend this to byte arrays and have a convenient way to store and access large pieces of data without using up all your RAM.

There are other accessors specifically for strings. You can store a string like this:

const char* myString = ROMSTR("This is a string in Flash");

And then access it like this:

const char* s = new char[ROMstrlen(myString)];
ROMCopyString(s, myString);
<use the string>
delete [ ] s;

The m8rscript library has functions for ROMmemcpy and ROMstrcmp as well. And much of the m8rscript functions allow a const char* strings in Flash. For instance, when you turn a string into an Atom, that string can be in ROM. And the print functions in SystemInterface can take format strings in ROM. That makes it pretty easy to keep strings from eating up RAM.

There is a caveat here. Accessing Flash is much slower that RAM. If you have a function that uses a table and does expensive computations or iterating you need to be careful. Moving the table into Flash could drastically reduce performance. You might want to keep that table in RAM or at least cache parts of it during the expensive parts of your function. The m8rscript library has chosen space over performance and for the most part the perf impact is small.

But what about structures that aren’t bytes? You can access structures in Flash with no problem as long as they are 4 byte aligned. So you can define an array of structures like this:

struct Entry { uint32_t id; uint32_t value; };
Entry RODATA_ATTR myArray [ ] = {
    { 0, 100 },
    { 1, 200 },
};

And now you can access that data without any special accessors. That’s because the elements in the struct are both 4 bytes and the RODATA_ATTR aligns the whole array on 4 byte boundaries.

With aggressive use of this technique, m8rscript uses just a bit over 30K of RAM, leaving over 50K for your scripts. So use these techniques and go save some RAM!

 


Leave a Reply

Your email address will not be published. Required fields are marked *