Saturday, March 10, 2012

Arduino, limited RAM and PROGMEM

And so I was happily adding more and more descent error handling code to a piece of code running on an atmega328p (the Atmel AVR micro-controller also typically used in Arduino). And then the thing started acting up. Now I've had that happen to me before so I immediately thought that it was running out of RAM.

The atmega328p has 32k flash, which is typically plenty, but it has only 2k RAM, which is not very much at all.

I was doing things like:

Serial.println("FAIL invalid argument");

Now, you might be thinking, why does it have to store the "FAIL invalid argument" in RAM?
It has all to do with how braindead C/C++ is/are.

"FAIL invalid argument" is a "const char *" which although it seems read-only it actually isn't (and no "const char const *" isn't either).
Thing is in C it is perfectly legal to cast away any types and just start changing characters in the string.
And the compiler can't know you're not doing that as you could be doing it via some very scary indirect pointer arithmetic to get to the characters in the string.

Bottom line is that every "constant" string in the code is copied to RAM because it could theoretically be used.

Now, on to PROGMEM.

It is a way to use strings directly as contained in the program code, but without the copying to RAM.

Okay, I'll first give the short answer which works fine on arduino 0.22 or newer:

Serial.println(F("FAIL invalid argument"));

This does some casting and other magic in the back and does the right thing.

Just for educational purposes lets take a look how to do it without arduino tricks.

the <avr/pgmspace.h> header contains a useful macro, called PSTR which looks like we could use it just like this:

Serial.println(PSTR("FAIL invalid argument"));

Unfortunately that doesn't work. PSTR returns a PROGMEM pointer and it turns out (even though the atmega has a flat address space) we can't use PROGMEM pointers just like regular pointers.

It will nicely compile and give garbage.

The only way is copying the data from PROGMEM to RAM and then using it.

This could be done byte-by-byte with pgm_read_byte (this is what arduino does internally, take a peek in arduino-1.0/hardware/arduino/cores/arduino/Print.cpp ) or to a buffer in RAM with strcpy_P.

All this is quite awkward and error-prone and makes me long for a nicer language for microcontrollers ;)


Zeph said...

It really isn't that "C/C++ are braindead". The problem is that the AVR chips are "Harvard architecture", where code and data are in different address spaces, not sharing one flat space (they are also not the same width). The AVR general instruction set just does not work on program memory, period. Program memory can only be accessed by special limited copy instructions, so in general you HAVE to copy to RAM before operating on data. The most one can do about this AVR limitation is to partially hide the necessary copy to RAM first operations from the programmer, like the F() macro. ANY language will need to do that on the AVR. A language specific to just the AVR might hide is more thoroughly (no F() needed), but C and the "Arduino" language, is more portable. When the Due based on ARM comes out, it will be handy that C is more portable (and that other people are maintaining the compiler versions), even if some architectures require F() type work araround.

If you want to characterize something as "braindead", the AVR microcontrollers might be a better target (tho to be fair, there are also advantages of the Harvard architecture, so it's all a matter of balancing tradeoffs).

Unknown said...

Hey Zeph,
Thanks for the correction. I was under the false impression that the Atmel AVR has a completely flat address space.
This explains why there is really not a way around the clumsiness.