Help with PROGMEM and char arrays

I currently have this Pro Mini code in a sketch:

//global:

char *menuText[] = {                    // pointers to Menu options
  (char*)"SET DATE/TIME",               // set RTC date and time
  (char*)"SET CHKIN HOURS",             // set one to three daily checkin hours
  (char*)"AUTO ADJUST DST",             // enable auto adjust for daylight saving
  (char*)"SET BUZZER TIME",             // set alarm buzzer time if check-in missed
  (char*)"SET QUICK TRIES",             // number of immediate tries to send PUSH
  (char*)"ONE-HOUR RETRIES",            // number of retries on successive hours if PUSH still fails
  (char*)"SET WIFI & KEYS",             // set WiFi and Pushover keys
  (char*)"TESTPUSH TO ME",              // send test PUSH notification to my phone
  (char*)"TESTPUSH TO ALL",             // send to group
  (char*)"TRIM RTC SPEED",              // adjust RTC oscillation speed
  (char*)"COINCELL VOLTAGE",            // check voltage of RTC backup coin cell
  (char*)"EXIT MENU"
};

// and then later:

lcd.print(menuText[menuIndex]);

I don't remember where I got that, but it works fine. But it puts all the strings in ram. I would like to keep them in flash, and print them from there. So far, I've concluded I need to make everything const, but I don't know how to set up the array as PROGMEM, or do the printing. Any suggestions would be appreciated.

const type array[] PROGMEM = {
  "one",
  "two",
  "tre"
};

I used "flat" arrays, so inside the braces, the commas go away, and it's just one long string, but my strings in one array were all the same length so I can calculate where I need to read.

Then, I read the stored data out in bytes using: pgm_read_byte()

void sequencer(char list[], int columns, int loops) {
  for (int lop = 0; lop < loops; lop++) { // count through loops
    loopTime(columnSpacing, columns, loops); // display Loop Time
    for (int col = 0; col < columns; col++) { // count through row length
      for (int row = 0; row < ledCount; row++) { // count through number of LEDs
        switch (pgm_read_byte(&list[row * columns + col])) { // locate element row and column location
          case '*': digitalWrite(ledPin[row], HIGH); break; // turn LED ON
          case '.': digitalWrite(ledPin[row],  LOW); break; // turn LED OFF
          default: break;
        }
      }
      delay(columnSpacing); // regulate the speed of the sequence
    }
  }
  firstTime = 1; // set first time flag for LoopTime display
}

There are a couple of ways to do it.

If all the char arrays are the same length, or nearly the same length, then a 2-dimensional array can be used:

const char menuText[][17] PROGMEM = {                    // pointers to Menu options
  "SET DATE/TIME",               // set RTC date and time
  "SET CHKIN HOURS",             // set one to three daily checkin hours
  "AUTO ADJUST DST",             // enable auto adjust for daylight saving
  "SET BUZZER TIME",             // set alarm buzzer time if check-in missed
  "SET QUICK TRIES",             // number of immediate tries to send PUSH
  "ONE-HOUR RETRIES",            // number of retries on successive hours if PUSH still fails
  "SET WIFI & KEYS",             // set WiFi and Pushover keys
  "TESTPUSH TO ME",              // send test PUSH notification to my phone
  "TESTPUSH TO ALL",             // send to group
  "TRIM RTC SPEED",              // adjust RTC oscillation speed
  "COINCELL VOLTAGE",            // check voltage of RTC backup coin cell
  "EXIT MENU"
};

lcd.print((__FlashStringHelper*)&menuText[menuIndex])

If the char arrays vary in length, it is more efficient to have an array of pointers to the char arrays:


const char text00[] PROGMEM = "SET DATE/TIME";               // set RTC date and time
const char text01[] PROGMEM = "SET CHKIN HOURS";             // set one to three daily checkin hours
const char text02[] PROGMEM = "AUTO ADJUST DST";             // enable auto adjust for daylight saving
const char text03[] PROGMEM = "SET BUZZER TIME";             // set alarm buzzer time if check-in missed
const char text04[] PROGMEM = "SET QUICK TRIES";             // number of immediate tries to send PUSH
const char text05[] PROGMEM = "ONE-HOUR RETRIES";            // number of retries on successive hours if PUSH still fails
const char text06[] PROGMEM = "SET WIFI & KEYS";             // set WiFi and Pushover keys
const char text07[] PROGMEM = "TESTPUSH TO ME";              // send test PUSH notification to my phone
const char text08[] PROGMEM = "TESTPUSH TO ALL";             // send to group
const char text09[] PROGMEM = "TRIM RTC SPEED";              // adjust RTC oscillation speed
const char text10[] PROGMEM = "COINCELL VOLTAGE";            // check voltage of RTC backup coin cell
const char text11[] PROGMEM = "EXIT MENU";

const char* const menuText[] PROGMEM = {                    // pointers to Menu options
  text00,
  text01,
  text02,
  text03,
  text04,
  text05,
  text06,
  text07,
  text08,
  text09,
  text10,
  text11,
};

lcd.print((__FlashStringHelper*)pgm_read_ptr(&menuText[menuIndex]));

Thanks very much. But each string has to have it's own PROGMEM? I couldn't just add PROGMEM to the array I had originally, with the (char*) things?

No, unfortunately that does not work. The array of char* would go into PROGMEM, but the char arrays would go into ram.

Ok, I understand. Thanks again.

Note that this does rely on the lcd library inheriting from print, which has an overload of the print() function that uses __FlashStringHelper* to signify that a char* points to PROGMEM instead of ram.

I don't know what that means. But if it compiles, does that mean the library does all that correctly?

Yes, if it compiles then the library has a function that takes the __FlashStringHelper*. All the lcd libraries I'm familiar with will work, but there is always the chance someone is using there own code or a more obscure library.

The problems of a non-Von Neumann architecture ...

Too many dimensions cause pointer/reference issues.

That is why I (in my link) removed all the commas after the quotes to make the data one-dimension. Only needed a little row/col math to get the right data.

Not as well that casting as char * a string literal is not a good practice - that should be const char * but it’s implicit .

As you usually are not short for flash, I find the option of using fixed size arrays (to the longest string thus)) the easy way forward to define and use the array.

You do have to cast the pointer to __FlashStringHelper* when using it though as the compiler won’t know.

Indeed. Before I got to Arduino, I did assembler on the MSP430. You could address anything, anywhere. But I guess at runtime Harvard is faster.

So then for strings that are shorter, they are just null-filled at the end? Printing would stop at the first null.

Yes and Yes.

Agreed. But if you really had your heart set on a jagged array in PROGMEM, the below technique works. It's kind of slick, but it does require the pointer array to be in RAM. So, it may not be worth the trouble.

#define PROGMEM_STRING(stringLiteral) [](){static const char s[] PROGMEM = stringLiteral; return reinterpret_cast<const __FlashStringHelper*>(&s[0]);} ()

const __FlashStringHelper* menuText[] = {
  PROGMEM_STRING("SET DATE/TIME"),
  PROGMEM_STRING("SET CHKIN HOURS"),
  PROGMEM_STRING("AUTO ADJUST DST"),
  PROGMEM_STRING("SET BUZZER TIME"),
  PROGMEM_STRING("SET QUICK TRIES"),
  PROGMEM_STRING("ONE-HOUR RETRIES"),
  PROGMEM_STRING("SET WIFI & KEYS"),
  PROGMEM_STRING("TESTPUSH TO ME"),
  PROGMEM_STRING("TESTPUSH TO ALL"),
  PROGMEM_STRING("TRIM RTC SPEED"),
  PROGMEM_STRING("COINCELL VOLTAGE"),
  PROGMEM_STRING("EXIT MENU")
};

void setup() {
  Serial.begin(115200);
  delay(1000);
  for (auto ptr : menuText) {
    Serial.println(ptr);
  }
}

void loop() {
}

Output:

SET DATE/TIME
SET CHKIN HOURS
AUTO ADJUST DST
SET BUZZER TIME
SET QUICK TRIES
ONE-HOUR RETRIES
SET WIFI & KEYS
TESTPUSH TO ME
TESTPUSH TO ALL
TRIM RTC SPEED
COINCELL VOLTAGE
EXIT MENU

That just rolls off the tongue, doesn't it. :slight_smile:

That's why it's hidden in a macro.

Yes, I guess the definition of F would look very similar.

Excatly. Actually the String literal (the thing between the double quotes) does have a hidden null terminator and if that does not fill in all the bytes of the array then the compiler (it's a global variable) will pad that with 0.