c++arraysarduinoprogmem

3D array of strings


I'm trying to make an LCD project (16x2) that cycles through groups of messages. The idea being that it will cycle through all the messages in the currently selected group and won't leave the group until manually changed. Originally I had a 3D array: Level 1 was the group of messages, level 2 was the message to display and level 3 was the line.

#define WIDTH 16
#define HEIGHT 2
#define NGRP 3 
#define MAXGRP 6
String mainMsgs[NGRP][MAXGRP][HEIGHT] = { 
                      {
                       {"Value 1","Value 2"},
                       {"Value 3","Value 4"},
                       {"Value 5","Value 6"},
                       {"Value 7","Value 8"},
                       {"Value 9","Value 10"},
                      },
                      {
                       {"Value 1","Value 2"},
                       {"Value 3","Value 4"},
                       {"Value 5","Value 6"},
                       {"Value 7","Value 8"},
                       {"Value 9","Value 10"},
                       {"Value 9","Value 10"},
                       {"Value 11","Value 12"},
                      },
                      {
                       {"Value 1","Value 2"},
                       {"Value 3","Value 4"},
                       {"Value 5","Value 6"},
                       {"Value 7","Value 8"},
                      }

};

I had this working more or less but I think the array got too large for memory because half way through group 3 it stops displaying the messages. When I tested it the array indices were always correct. I assumed the array had been truncated. In trying to troubleshoot this I came across PROGMEM. I tried converting the arduino string tutorial at: http://www.arduino.cc/en/Reference/PROGMEM from a 1D array into a 3D array but couldn't get it to work, it either failed to compile or returned garbage. Below are a couple of my attempts.

const char message1[][2] PROGMEM = { "Value 1", "Value 2" }; // Through to message15
const char* const group1[][5] PROGMEM = { message1, message2, message3, message4, message5 };
const char* const group2[][6] PROGMEM = { message6, message7, message8, message9, message10, message11 };
const char* const group3[][4] PROGMEM = { message12, message13, message14, message15 };
const char* const groups[] PROGMEM = { group1, group2, group3 }; // Attempt 1
const char* const groups[NUMGRP][6] PROGMEM = { 
                                        {message1, message2, message3, message4, message5}, 
                                        {message6, message7, message8, message9, message10, message11}, 
                                        {message12, message13, message14, message15}, 
                                      }; // Attempt 2

So I tried converting the original array directly into progmem using

const char* const mainMsgs[NUMGRP][6][HEIGHT] = { /* same content as before*/ };
strcpy_P(buff, (char*)pgm_read_word(&(mainMsgs[groupID][msgID][i])));

But it still returned garbage. So then I thought I'd try and convert the data into a 1D array and just access messages and lines using offsets.

EDIT: Edited the below code to reflect the code I'd used in my original sketch.

const char message1[] = "Value 1";
const char message2[] = "Value 2"; // Down to message30
const char* const messages[] PROGMEM = { message1, message2,
                    message3, message4,
                    // ... ... ...
                    message29, message30
                  };
int groupStarts[] = { 0, 10, 22 }; // The first index of each group
int numMsgs[] { 5, 6, 4 };
char buff[WIDTH];

This is the test loop I used:

int id = 0;
for( groupID = 0; groupID < NGRP; groupID++ ) {
   for( msgID = 0; msgID < numMsgs[groupID]*HEIGHT; msgID+=HEIGHT ) {
     for( lineID = 0; lineID < HEIGHT; lineID++ ) {
       id = groupStarts[groupID] + msgID + lineID;
       strcpy_P(buff, (char*)pgm_read_word(&(messages[id])));
       Serial.print(id);
       Serial.print(" ");
       Serial.print(buff);
     }
     Serial.println("");
     delay(500);
   }
}

This results in an almost working example but it's not perfect:

0 Value1 1 Value 2
2 Value3 3 Value 4
4 Value 5 5 Value 6
6 Value 7 7 Value 8
8 Value 9
10 Value 11 11 Value 12
10 Value 11 11 Value 12
12 Value 13 13 Value 14
14 Value 15 15 Value 16
16 Value 17 17 Value 18
18 Value 19 19 Value 20 19 Value 20 19 Value 20...

You might notice that there is no Value 10 displayed, value 11 and 12 repeat twice and when it gets to value 19 it just gets stuck in an infinite loop.

I can't quite think what the final loop should be.

Ideally I'd prefer to keep the 3D array structure as I find it easier to read and understand but I'd be happy with a solution to either version of the code.

Edit to reflect shuttle87's suggestion:

#include <avr/pgmspace.h>

#define WIDTH 16
#define HEIGHT 2

const char string1[] PROGMEM = "Message 1";
const char string2[] PROGMEM = "Message 2";
const char string3[] PROGMEM = "Message 3";
const char string4[] PROGMEM = "Message 4";
const char string5[] PROGMEM = "Message 5";
const char string6[] PROGMEM = "Message 6";

const int groupLen[] = { 2, 3, 1 };

const char* msgs[][3][2] = {
  {
    {string1, string2 },
    {string3, string4 }
  },
  {
    {string5, string6 },
    {string3, string4 },
    {string1, string2 }
  },
  {
    {string2, string3 }
  }
};

char buffer[WIDTH];

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
}

void loop() {
  // put your main code here, to run repeatedly:
  for( int g = 0; g < 3; g++ ) {
    Serial.print("Switching to group: ");
    Serial.println(g);
    for( int m = 0; m < groupLen[g]; m++ ) {
      Serial.print("Switching to message: ");
      Serial.println(m);
      for( int l = 0; l < HEIGHT; l++ ) {
        Serial.print("Switching to line: ");
        Serial.println(l);
         strcpy_P(buffer, (char*)pgm_read_word(&(msgs[g][m][l])));
         Serial.println(buffer);
      }
      delay(500);
    }
  }
}

The current output I'm getting is: "Switchi" and nothing else, does that mean my Arduino is hanging because of my code or have I somehow killed it? I also updated the single array version to reflect how I'd actually coded it. When copying it in I'd miscopied it and it was a bit of a mess. It works more like how shuttle87 suggested but it still returns the error shown above.

Edit: Just realised I missed:

const char* msgs[][3][2] = {
      {
        {string1, string2 },
        {string3, string4 }
      },
      {
        {string5, string6 },
        {string3, string4 },
        {string1, string2 }
      },
      {
        {string2, string3 }
      }
    };

Should have started:

const char* const messages[][3][2] PROGMEM= {
      {
        {string1, string2 },
        {string3, string4 }
      },
      {
        {string5, string6 },
        {string3, string4 },
        {string1, string2 }
      },
      {
        {string2, string3 }
      }
    };

Sorry about that. That does seem to have fixed it. Thanks so much for the help :)

Thanks.


Solution

  • Most of your attempts have the same issue, you have stored the pointer to the table in progmem but the actual table data itself (in this case the strings) are not stored in progmem. Unfortunately GCC attributes (as of gcc 4.7) only apply to the current declaration so you have to specify progmem on each variable.

    So when you have

     const char message1[][2] PROGMEM = { "Value 1", "Value 2" };
    

    message1 is stored in progmem but the strings "Value 1" are not. Further, if I recall correctly, avr-gcc compiler always stores string literals in SRAM. Even if you specify a place to put them in progmem it's still copied in SRAM (at one point I was trying to write a library to use the c++11 user defined string literals to put things in progmem but this was thwarting me). The 1 dimensional array solution also falls into this same problem.

    To fix this you store everything explicitly in progmem, for example your 1D solution looks something like this:

    const char string_msg0_0[] PROGMEM = "Value 1";
    const char string_msg0_1[] PROGMEM = "Value 2";
    
    PGM_P strings_pgm_table[] PROGMEM = {string_msg0_0, string_msg0_1};
    
    char buffer[MAX_STRING_SIZE];
    strcpy_P(buffer, (PGM_P)pgm_read_word(&(strings_pgm_table[i])));
    

    I would recommend you have a look at the AVR-GCC tutorial putting data in progmem.