RAM is precious on a microcontroller, PROGMEM (program memory) not quite as much so. On the Arduino Uno (using an Atmega328P) you have:
- 1 kB of EEPROM
- 2 kB of RAM
- 32 kB of PROGMEM
Clearly, there is a lot more program memory than RAM.
Variables are copied into RAM at program startup
The problem with constants in general (including string constants like "hello") is that the compiler generates code to copy them into RAM at program startup, because most C functions do not expect to find data in PROGMEM. (It has to generate different instructions to access PROGMEM).
Benchmark memory use
To test memory use, I'll start off with a sketch that simply returns how much memory is free, without doing anything else. Tested on IDE 1.0.6, Arduino Uno, on Ubuntu.
#include "memdebug.h"
void setup ()
{
Serial.begin (115200);
Serial.println ();
Serial.print (F("Free memory = "));
Serial.println (getFreeMemory ());
} // end of setup
void loop () { }
Output:
Since we started with 2048 bytes of RAM, already we have used 2048 - 1702 = 346 bytes. These are used as follows:
- 34 bytes for the HardwareSerial instance (Serial)
- 64 bytes for the Serial transmit buffer
- 64 bytes for the Serial receive buffer
- 4 bytes for the Serial transmit buffer head and tail pointers
- 4 bytes for the Serial receive buffer head and tail pointers
- 9 bytes for keeping track of millis / micros
- 4 bytes for memory allocation (__malloc_heap_start, __malloc_margin)
- 128 bytes for the heap safety margin
- 6 bytes for a few nested function calls (main -> setup -> getFreeMemory)
- 16 bytes for the compiler vtable for HardwareSerial
- 4 bytes for variables __brkval and __flp (used in memdebug)
- 2 bytes pushed onto the stack in main (to save registers)
- 2 bytes pushed onto the stack in setup (to save registers)
- 4 bytes pushed onto the stack in getFreeMemory (to save registers)
- 1 byte because the stack pointer starts at 0x8FF rather than 0x900
(That's 346 bytes accounted for)
Tips:
- The stack starts at the highest possible address and grows downwards. Each function call would add 2 bytes to the stack (the return address) plus any local (auto) variables would be allocated there.
- The heap safety margin is a buffer between the top of the heap and the bottom of the stack. Without it, if you did a "malloc" and got all available memory, there would be no memory left for function calls and local variables in functions.
- The vtable is used by the compiler to generate late bindings for virtual functions used by classes.
- The compiler generates code to push registers onto the stack when you call a function (the registers used inside that function) so that if the same register was used by the calling function, it would not be corrupted.
Printing constants
Let's print a constant by adding this line:
Serial.println ("Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmo");
Output is now:
Free memory = 1630
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmo
So the free RAM has gone down by 72 bytes, which happens to be be how long that string is (allowing one byte for the 0x00 terminator at the end).
Why? Because the string was copied from PROGMEM, where it must be when the Arduino is turned off, into RAM.
The first thing we can do is use the F() macro, which cunningly expands out to print directly from PROGMEM, thus saving RAM. So we change the line above to read:
Serial.println (F("Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmo"));
Output:
Free memory = 1702
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmo
Back to 1702 bytes of free memory. So, lesson #1 is to use the F() macro as illustrated above whenever printing constants.
Tip:
You can actually do that to any form of outputting, not just Serial, by deriving a class from Print (if it isn't already so derived). For example:
#include <SPI.h>
class myOutputtingClass : public Print
{
public:
virtual size_t write (uint8_t c);
}; // end of myOutputtingClass
// output to whatever medium you want here
size_t myOutputtingClass::write (uint8_t c)
{
SPI.transfer (c);
return 1; // one byte output
} // end of myOutputtingClass::write
myOutputtingClass foo;
void setup ()
{
digitalWrite(SS, HIGH); // ensure SS stays high
SPI.begin ();
} // end of setup
void loop ()
{
digitalWrite(SS, LOW); // enable slave select
foo.print (F("Hello, world")); // sends to SPI
digitalWrite(SS, HIGH); // disable slave select
delay (1000);
} // end of loop
Getting data from an array of constants
Things get tricky if you want a whole lot of words in an array, but not necessarily to be used with Serial.print. For example:
char * messages[] = {
"Test0000",
"Test0001",
"Test0002",
"Test0003",
"Test0004",
"Test0005",
"Test0006",
"Test0007",
"Test0008",
"Test0009",
"Test0010",
};
...
Serial.print (messages [5]);
The above code wastes RAM in two ways:
- The strings (eg. "Test0000")
- The pointers to the strings (which is what is stored in the array)
Because of this, a seemingly-simple approach to saving memory won't work very well:
char * const messages[] PROGMEM = {
"Test0000",
"Test0001",
"Test0002",
"Test0003",
"Test0004",
"Test0005",
"Test0006",
"Test0007",
"Test0008",
"Test0009",
"Test0010",
};
...
char * ptr = (char *) pgm_read_word (&messages [5]);
Serial.print (ptr);
Whilst this works, and prints messages [5], the only saving we have made is putting the pointers into PROGMEM and not the strings themselves. I can prove that with a more elaborate example:
#include "memdebug.h"
#define USE_PROGMEM true
const int NUMBER_OF_ELEMENTS = 10;
char * const messages [NUMBER_OF_ELEMENTS]
#if USE_PROGMEM
PROGMEM
#endif
= {
"Twas bryllyg, and ye slythy toves",
"Did gyre and gymble",
"in ye wabe:",
"All mimsy were ye borogoves; And ye mome raths outgrabe.",
"\"Beware the Jabberwock, my son! \n The jaws that bite, the claws that catch!",
"Beware the Jubjub bird, and shun\n The frumious Bandersnatch!\"",
"He took his ",
"vorpal sword in hand:",
"Long time the manxome foe he sought - ",
"So rested he by the Tumtum tree, \n And stood awhile in thought.",
};
void setup ()
{
Serial.begin (115200);
Serial.println ();
Serial.print (F("Free memory = "));
Serial.println (getFreeMemory ());
unsigned int count = 0;
for (int i = 0; i < NUMBER_OF_ELEMENTS; i++)
{
#if USE_PROGMEM
char * ptr = (char *) pgm_read_word (&messages [i]);
#else
char * ptr = (char *) messages [i];
#endif
Serial.println (ptr);
count += strlen (ptr) + 1;
} // end of for loop
Serial.print (F("String memory used = "));
Serial.println (count);
} // end of setup
void loop () { }
Output:
Free memory = 1298
Twas bryllyg, and ye slythy toves
Did gyre and gymble
in ye wabe:
All mimsy were ye borogoves; And ye mome raths outgrabe.
"Beware the Jabberwock, my son!
The jaws that bite, the claws that catch!
Beware the Jubjub bird, and shun
The frumious Bandersnatch!"
He took his
vorpal sword in hand:
Long time the manxome foe he sought -
So rested he by the Tumtum tree,
And stood awhile in thought.
String memory used = 399
Now make it not use PROGMEM for the array:
#define USE_PROGMEM false
Output:
Free memory = 1278
Twas bryllyg, and ye slythy toves
Did gyre and gymble
in ye wabe:
All mimsy were ye borogoves; And ye mome raths outgrabe.
"Beware the Jabberwock, my son!
The jaws that bite, the claws that catch!
Beware the Jubjub bird, and shun
The frumious Bandersnatch!"
He took his
vorpal sword in hand:
Long time the manxome foe he sought -
So rested he by the Tumtum tree,
And stood awhile in thought.
String memory used = 399
The saving from PROGMEM was 1298 - 1278 = 20. Since there were 10 messages in the table, and as a pointer is 2 bytes, clearly we have only saved the memory for the 10 pointers and not the 399 bytes that the strings themselves used.
Putting the strings themselves into PROGMEM
The challenge now is to put the actual strings, and not just the pointers to them, into PROGMEM.
There is a method suggested here: Arduino reference - PROGMEM
The method is to make a variable per string (in PROGMEM) and put those variables in the array, like this:
#include "memdebug.h"
const int NUMBER_OF_ELEMENTS = 10;
const char Message0000 [] PROGMEM = "Twas bryllyg, and ye slythy toves";
const char Message0001 [] PROGMEM = "Did gyre and gymble";
const char Message0002 [] PROGMEM = "in ye wabe:";
const char Message0003 [] PROGMEM = "All mimsy were ye borogoves; And ye mome raths outgrabe.";
const char Message0004 [] PROGMEM = "\"Beware the Jabberwock, my son! \n The jaws that bite, the claws that catch!";
const char Message0005 [] PROGMEM = "Beware the Jubjub bird, and shun\n The frumious Bandersnatch!\"";
const char Message0006 [] PROGMEM = "He took his ";
const char Message0007 [] PROGMEM = "vorpal sword in hand:";
const char Message0008 [] PROGMEM = "Long time the manxome foe he sought - ";
const char Message0009 [] PROGMEM = "So rested he by the Tumtum tree, \n And stood awhile in thought.";
const char * const messages[NUMBER_OF_ELEMENTS] PROGMEM =
{
Message0000,
Message0001,
Message0002,
Message0003,
Message0004,
Message0005,
Message0006,
Message0007,
Message0008,
Message0009,
};
void setup ()
{
Serial.begin (115200);
Serial.println ();
Serial.print (F("Free memory = "));
Serial.println (getFreeMemory ());
unsigned int count = 0;
for (int i = 0; i < NUMBER_OF_ELEMENTS; i++)
{
char * ptr = (char *) pgm_read_word (&messages [i]);
char buffer [80]; // must be large enough!
strcpy_P (buffer, ptr);
Serial.println (buffer);
count += strlen (buffer) + 1;
} // end of for loop
Serial.print (F("String memory used = "));
Serial.println (count);
} // end of setup
void loop () { }
Output:
Free memory = 1616
Twas bryllyg, and ye slythy toves
Did gyre and gymble
in ye wabe:
All mimsy were ye borogoves; And ye mome raths outgrabe.
"Beware the Jabberwock, my son!
The jaws that bite, the claws that catch!
Beware the Jubjub bird, and shun
The frumious Bandersnatch!"
He took his
vorpal sword in hand:
Long time the manxome foe he sought -
So rested he by the Tumtum tree,
And stood awhile in thought.
String memory used = 399
The amount of free RAM is back to 1616 which is only 86 bytes less than the benchmark (1702 bytes). Since there is an extra buffer of 80 bytes which we copy the strings into, we have only really used an extra 6 bytes so clearly the strings are now not being copied into RAM.
A drawback, however, is the need to have an adequate size buffer for copying the string into RAM, prior to printing. If you get that wrong, the program will probably crash. One way is to add another function:
// Print a string from Program Memory directly to save RAM
void printProgStr (const char * str)
{
char c;
if (!str)
return;
while ((c = pgm_read_byte(str++)))
Serial.print (c);
} // end of printProgStr
Then change the printing part of the loop to use that instead of the temporary buffer:
for (int i = 0; i < NUMBER_OF_ELEMENTS; i++)
{
printProgStr ((const char *) pgm_read_word (&messages [i]));
Serial.println (); // finish line off
} // end of for loop
Doing that saves the 80-byte buffer (in this case) which we used to hold the string when copied from PROGMEM.
However, the whole thing is tedious. You have to invent names for all those strings, and then put those names into the array. Perhaps there is a simpler way?
Put the strings into the array
If all of the strings are a similar length (which would probably be the case if they are messages to go onto an LCD screen or similar) then you can put them directly into the array like this:
#include "memdebug.h"
const int NUMBER_OF_ELEMENTS = 10;
typedef struct {
char description [12];
} descriptionType;
const descriptionType descriptions [NUMBER_OF_ELEMENTS] PROGMEM = {
{ "Furnace on" },
{ "Furnace off" },
{ "Set clock" },
{ "Pump on" },
{ "Pump off" },
{ "Password:" },
{ "Accepted" },
{ "Rejected" },
{ "Fault" },
{ "Service rqd" },
};
void setup ()
{
Serial.begin (115200);
Serial.println ();
Serial.print (F("Free memory = "));
Serial.println (getFreeMemory ());
for (int i = 0; i < NUMBER_OF_ELEMENTS; i++)
{
// make a copy of the current one
descriptionType oneItem;
memcpy_P (&oneItem, &descriptions [i], sizeof oneItem);
Serial.println (oneItem.description);
} // end of for loop
} // end of setup
void loop () { }
This code defines a descriptionType, which in this case can hold up to 12 characters (so that would be 11 plus the null terminator). Assuming all your strings fit (and you can always make the number 12 larger) then this is much simpler.
Output:
Free memory = 1686
Furnace on
Furnace off
Set clock
Pump on
Pump off
Password:
Accepted
Rejected
Fault
Service rqd
This has only used 1702 - 1686 = 16 bytes of RAM compared to the benchmark sketch. So now we could easily hold a much larger array of messages without worrying about RAM usage.
You also have scope for adding more information per element, like this:
typedef struct {
char description [12];
char meaning [20];
int value;
} descriptionType;
So you could store multiple strings (of fixed length) plus numbers as well.
An alternative away of achieving an array of strings, without using a struct, is like this:
#include "memdebug.h"
const int NUMBER_OF_ELEMENTS = 10;
const int MAX_SIZE = 12;
const char descriptions [NUMBER_OF_ELEMENTS] [MAX_SIZE] PROGMEM = {
{ "Furnace on" },
{ "Furnace off" },
{ "Set clock" },
{ "Pump on" },
{ "Pump off" },
{ "Password:" },
{ "Accepted" },
{ "Rejected" },
{ "Fault" },
{ "Service rqd" },
};
void setup ()
{
Serial.begin (115200);
Serial.println ();
Serial.print (F("Free memory = "));
Serial.println (getFreeMemory ());
for (int i = 0; i < NUMBER_OF_ELEMENTS; i++)
{
// make a copy of the current one
char oneItem [MAX_SIZE];
memcpy_P (&oneItem, &descriptions [i], sizeof oneItem);
Serial.println (oneItem);
} // end of for loop
} // end of setup
void loop () { }
PROGMEM functions
These examples and the Arduino support for PROGMEM in general are built using features from the AVR Libc (AVR standard library) that you may also encounter in developing Arduino sketches and looking at ones developed by others.
AVR Libc, and therefore Arduino provides several standard string and memory manipulation functions in versions that address program memory rather than RAM. The AVR Libc convention is to append "_P" to the name of the standard function, thus we have strcpy_P() and memcpy_P() which are functions used in the above examples to copy strings or memory from PROGMEM to RAM. The "source" argument is assumed to be in PROGMEM and the destination argument in RAM. Otherwise they function like the standard library functions strcpy() and memcpy(). Some useful functions of this kind are:
strcpy_P (dest, PROGMEM src)
strcmp_P (s1, PROGMEM s2)
strncmp_P (s1, PROGMEM s2, len)
memcpy_P (dest, PROGMEM src, len)
sprintf_P (dest, PROGMEM format, item, ... )
There are many others documented in the AVR Libc manual.
The AVR Libc provides a macro PSTR() that is similar to the F() macro mentioned above. PSTR() works with the AVR Libc "_P" functions, for example:
if (strcmp_P (inputBuffer, PSTR("SUCCESS")) == 0)
{
}
Here we are using a strcmp() to check that the input buffer contains a certain string. By using strcmp_P() and PSTR() we are arranging that the comparison string doesn't have to occupy RAM.
char buf [20];
sprintf_P (buf, PSTR ("Temperature %i\n"), temperature);
The above formats a string for output without wasting RAM on the format string.
F() and PSTR() although similar in concept, are not interchangeable, and you may have to think carefully about which is appropriate in various situations. In particular, the F() macro generates code which the Print class (and derived classes) can understand for outputting text. The PSTR() macro generates a temporary variable that can be used inside a function call - however that function has to be one of the "_P" functions that expects PROGMEM data.
Below shows how we can avoid having to use the temporary buffer, thus saving more RAM:
#include "memdebug.h"
const int NUMBER_OF_ELEMENTS = 10;
const int MAX_SIZE = 12;
const char descriptions [NUMBER_OF_ELEMENTS] [MAX_SIZE] PROGMEM = {
{ "Furnace on" },
{ "Furnace off" },
{ "Set clock" },
{ "Pump on" },
{ "Pump off" },
{ "Password:" },
{ "Accepted" },
{ "Rejected" },
{ "Fault" },
{ "Service rqd" },
};
// Print a string from Program Memory directly to save RAM
void printProgStr (const char * str)
{
char c;
if (!str)
return;
while ((c = pgm_read_byte(str++)))
Serial.print (c);
} // end of printProgStr
void setup ()
{
Serial.begin (115200);
Serial.println ();
Serial.print (F("Free memory = "));
Serial.println (getFreeMemory ());
for (int i = 0; i < NUMBER_OF_ELEMENTS; i++)
{
printProgStr ((const char *) &descriptions [i]);
Serial.println (); // finish line off
} // end of for loop
} // end of setup
void loop () { }
|