Arduino Static Strings

UPDATE: 2014 for Arduino V1+ - Michael Cooper reminded me today that there is now a better way.

 Serial.println(F("Here is my new long string that is only in program memory"));
See also:

In C Strings use Memory

Skipping why this is the case, you can imagine on an Arduino 168 with 1K of RAM, this could easily become a big problem. Especially if you are writing an HTTP server.

The solution is ugly, and this is my attempt at making it prettier.

  // This takes up 21 bytes of 1024 available (based on a 168)
  const char exampleConstChar = "This is a const char";
  Serial.println(exampleConstChar);
  Serial.println(exampleConstChar);

  // #define version takes up 17 * 2 = 34 (yes, I used it twice, it is in memory twice)
  #define STR "This is a define"
  Serial.println(STR);
  Serial.println(STR);

  // Direct - 15 bytes
  Serial.println("Direct printed");

The above examples show that we are using a heap of RAM for each, of what should appear to be a static string.

You will find heaps of references on why it is using RAM. For now, trust me, it is using real RAM. Every string in your project is taking up real RAM. And when you only have 1K or perhaps 2K this becomes a problem.

What is ram used for - generally the amount of ram you use in your project is fairly fixed. If you have an int it is 2 bytes all the time. Each time you call a subroutine or method, your stack uses more memory, and programs can take unexpected routes - so keep your memory small.

Solution 1 - External

There are some solutions:

  • Stick your static content into the EEPROM (although this is only 512 bytes, while program ram is about 14K, or 30K on a 328).
  • Store it on an external device - useful for really large things, like web resources. A async_labs WiShield has space.

Solution 2 - PROGMEM as per the Examples

The solution that most people suggest is as follows:

const char exampleString[] PROGMEM = "Here is a big hello string";
void printString(const prog_char str[]) {
  char c;
  if(!str) return;
  while((c = pgm_read_byte(str++)))
    Serial.print(c,BYTE);
}

printString(exampleString);

An alternative to this example is to first copy the string.

const char exampleString[] PROGMEM = "Here is a big hello string";
char buffer[60];
void printString(const prog_char str[]) {
  strcpy_P(buffer, (char*)pgm_read_word(&(prog_char_str)));
  Serial.print(buffer);
}

printString(exampleString);

There are some problems with these examples:

  • They require you to know what you are doing with the string. What if you wanted to put it in a log, or send it over Ethernet and Serial.
  • In the second example you have to know how big your biggest strings are.

So here is my example which pulls it together:

Solution 3 - PROGMEM copied to a buffer, using normal print

// Define as many of these as you like !
const char exampleString[] PROGMEM = "Here is a big hello string";

#define MAX_STRING 60
char stringBuffer[MAX_STRING];
char* getString(const char* str) {
	strcpy_P(stringBuffer, (char*)str);
	return stringBuffer;
}

Serial.println(getString(exampleString));

The above example allows us to use any PROGMEM strings for any purpose. But remember you can only deal with one at a time. This is because there is only a single reused buffer. Technically we could also make the buffer outside of the routine, to allow it to be used directly. or for other things.

For most Arduino code this won't matter much, but you don't want to use this function inside an interrupt, or using some form of scheduler.

Solution 4 - Simpler is better

This solution uses preprocessor functions to do the work.

The adafruite wave shield uses the following code:

#define putstring(x) SerialPrint_P(PSTR(x))
void SerialPrint_P(PGM_P str) {
  for (uint8_t c; (c = pgm_read_byte(str)); str++) Serial.write(c);
}
putstring("This is a nice long string that takes no memory");

You can't use these strings for anything, as you need a new define and function for each thing you want to write to, but it does make your code much simpler... I am working on improving the usability.

And now my own version... really just some renames:

// SERIAL PRINT
// replace Serial.print("string") with SerialPrint("string")
#define SerialPrint(x) SerialPrint_P(PSTR(x))
void SerialPrint_P(PGM_P str, void (*f)(uint8_t) = SerialWrite ) {
  for (uint8_t c; (c = pgm_read_byte(str)); str++) (*f)(c);
}

// ETHERNET CLIENT
// replace client.print("string") with ClientPrint("string",client)
#define ClientPrint(x, y) SerialPrint_P(PSTR(x), y)
void ClientPrint_P(PGM_P str, Client client) {
  for (uint8_t c; (c = pgm_read_byte(str)); str++) client.write(c);
}

After about an hours work that is about as close as I can come to being simple. The problem... you need to create a method for each thing, and sometimes they need other parameters. There doesn't seem to be a generic way.

Worth it ?

Here are some numbers:

  • Used 446 bytes of text to print to the serial port
  • NO Data (base line) compile 2236 bytes, free memory 1836
  • Serial.print("strings") compile 2752 bytes, free memory 1390
  • SerialPrint("strings") compile 2778 bytes, free memory 1836

Now write a HTTP server, lots of client debugging etc, and yes it was worth it. The end result is also very easy to read:

SerialPrint("My debugging line goes here, and can be really quite long");
ClientPrint("<html><head><title>Hello</title></head><body><h1>Hello</h1>...", client);

TODO: Add Github project files for test code.

References