Working with array of strings, PROGMEM question

Hi there,
following my previous topic (Max number of Strings in a char array - #3 by Elso - Project Guidance - Arduino Forum) regarding the maximum number of strings I can place in an array, I am trying to use the PROGMEM to store the string arrays I have in the Flash memory; the main problem is that I don't k ow if this is the right procedure to follow in my case.

What I am trying to achieve is:
to have two arrays containing strings, about 300 strings each, each containing between 3 to 8 letters. Each time the sketch run, two strings are randomly selected from the two arrays (one pulled from each one of them); then they are concatenated and visualised on an OLED display, using the Adafruit library (GitHub - ladyada/Adafruit_CharacterOLED: Adafruit 'Liquid Crystal' compatible libary for Character OLEDs).
As those are too many strings and I already had some memory issues with my script (I am using an Arduino Mega 2560) I would like to move those strings to the flash memory.
I went to the PROGMEM tutorial page (PROGMEM - Arduino Reference) but it seems to me that I need to copy the string table / array to the RAM buffer.

My questions are:

  • do I need to copy the entire array or simply the string randomly selected?
  • do I need to clear that buffer once I don't need the string anymore to make space for another one?

Many thanks for your help, I am attaching a shorter version of my code

// include the library code:
#include <Adafruit_CharacterOLED.h>

// initialize the library with the numbers of the interface pins
Adafruit_CharacterOLED lcd(6, 7, 8, 9, 10, 11, 12);

long randNames; // random word taken from the group male names
long randLastnames; // random last name
char finalWord[40];  // enough room for all strings together
int myLenght; // lenght of the final word

char* maleNames[]={"FRANK", "JOHN", "MARK", "LORIS", "MICHAEL", "....,
 };

char* maleLastnames[]={"STEIN", "OLLIS", "GRAND", "MERLIN", "KRANZ", "...


 };

char* mySpace = " ";

void setup(){
Serial.begin(9600);
}

void loop(){
  randNames = random (320); //picks a random name
  randLastnames = random (320); //picks a random last name
  
  finalWord[0] = 0;          // start with a null string:
  strcat(finalWord, maleLastnames[randLastnames]);   // add first string
  strcat(finalWord, mySpace);   // add second string, the empty space string
  strcat (finalWord, maleNames[randNames]); //add third string
  
  myLenght = strlen(finalWord);

  if (myLenght < 16) {// if the final word doesn't exceed 16 spaces 
  lcd.setCursor(0, 0); //positions the cursor on the first row
  lcd.print(finalWord); //prints the word picked from the group

  delay(10000); // wait a bit for ppl to read
  lcd.clear(); // clears the display
  delay(5000); // dark/empty display
  }
   
}

You forgot a link to that OLED display.
Is this the one with an SD card ? If so, you could also store the data in a file (or files) on the SD card.

The Adafruit library doesn't use the standard 'stream' class which is used by many Arduino functions.
That stream class can also handle strings in Flash.
The Adafruit library requires the data to be in RAM.

My suggestion is to use the PROGMEM tutorial. Build the two strings in a RAM buffer and write that to the display.
You can use strcpy_P() or sprintf_P() to copy a string from Flash to RAM.
http://www.nongnu.org/avr-libc/user-manual/group__avr__pgmspace.html

Declaring string arrays in Flash is not so easy.
You could concatenate the strings with a seperation character.
You could make them all 8 characters long and use it as one long string.

This would be easy to implement:

prog_char listOne[] PROGMEM = 
{ 
  "AA      " \
  "BBBBBBBB" \
  "CCC     " \
};

The normal way to do this, is to declare each name with each an own string variable in Flash. After that declare an array of pointers (in Flash) to those string names (in Flash).
As far as I know, no easy way exists for this.

Thanks for the help,
the OLED display doesn't have an SD card unfortunately, it is a Winstar OLED 16x2 like this model:

pins are grouped differently on my model, but that is a detail.
Do I need to make each string 8 characters long?
I am going through the tutorial you linked, thanks for the resource!
I will let you know how it goes!

Many thanks

Best

Elso

The PROGMEM tutorial is the one you need: PROGMEM - Arduino Reference

To make each name 8 characters long, it it possible to declare it as one long string.
The example is exactly the same as this (but written in a different way):

prog_char listOne[] PROGMEM = "AA      BBBBBBBBCCC     ";

It would be one long string, only to avoid declaring both the strings and an array of strings seperately.

Okay...
I still seem to have a problem understanding the strings being 8 characters long, sorry, just being thick probably XD
At the moment, looking at the tutorial I have created two lists and a buffer into which I copy one string at time that is being concatenated into a char, I have rewritten the previous code like this:

// include the library code:
#include <Adafruit_CharacterOLED.h>
#include <avr/pgmspace.h>

// initialize the library with the numbers of the interface pins
Adafruit_CharacterOLED lcd(6, 7, 8, 9, 10, 11, 12);

prog_char string_0[] PROGMEM = "MARK";   // "String 0" etc are strings to store - change to suit.
prog_char string_1[] PROGMEM = "FRANCIS";
prog_char string_2[] PROGMEM = "JOHN";
prog_char string_3[] PROGMEM = "ANDREW";
prog_char string_4[] PROGMEM = "ROBERT";
prog_char string_5[] PROGMEM = "SEAN";

prog_char string_6[] PROGMEM = "HAYES";   // "String 0" etc are strings to store - change to suit.
prog_char string_7[] PROGMEM = "LUDWIG";
prog_char string_8[] PROGMEM = "HARRISON";
prog_char string_9[] PROGMEM = "GRAND";
prog_char string_10[] PROGMEM = "SALTER";
prog_char string_11[] PROGMEM = "BOND";

// Then set up a table to refer to your strings.

PROGMEM const char *string_table1[] = 	   // change "string_table" name to suit
{   
  string_0,
  string_1,
  string_2,
  string_3,
  string_4,
  string_5 };

PROGMEM const char *string_table2[] = 	   // change "string_table" name to suit
{   
  string_6,
  string_7,
  string_8,
  string_9,
  string_10,
  string_11};
  
char buffer[30];    // make sure this is large enough for the largest string it must hold


long randNames; // random word taken from the group names
long randLastnames; // random last name
char finalWord[40];  // enough room for all strings together
int myLenght; // lenght of the final word
//int myCentre; // coordinates to place the final word in the centre

char* mySpace = " ";

void setup(){
Serial.begin(9600);
}

void loop(){
  randNames = random (320); //picks a synonym
  randLastnames = random (320); //picks an adjective
  
  finalWord[0] = 0;          // start with a null string:
  strcpy_P(buffer, (char*)pgm_read_word(&(string_table1[randNames])));
  strcat (finalWord, buffer); //add third string
  strcat(finalWord, mySpace);   // add second string, the empty space string
  strcpy_P(buffer, (char*)pgm_read_word(&(string_table2[randLastnames])));
  strcat(finalWord, buffer);   // add first string
  
  myLenght = strlen(finalWord);
  
  Serial.println(finalWord);
   
}

This should give me the final word as a result of Names + Lastnames, one at time, I guess.
Instead I cannot see anything printed on the serial monitor, for the moment I am not trying to write to the OLED.

Can you spot if there is anything wrong in it?

Many thanks!

Elso

Your code is running with a few changes.
I hope you don't mind that I place the code without checking myself what changes are actually needed.
You don't need to include the pgmspace.h and I use integers for random. The random needs to be 0...5, so random(6) will do that.

prog_char string_0[] PROGMEM = "MARK";   // "String 0" etc are strings to store - change to suit.
prog_char string_1[] PROGMEM = "FRANCIS";
prog_char string_2[] PROGMEM = "JOHN";
prog_char string_3[] PROGMEM = "ANDREW";
prog_char string_4[] PROGMEM = "ROBERT";
prog_char string_5[] PROGMEM = "SEAN";

prog_char string_6[] PROGMEM = "HAYES";   // "String 0" etc are strings to store - change to suit.
prog_char string_7[] PROGMEM = "LUDWIG";
prog_char string_8[] PROGMEM = "HARRISON";
prog_char string_9[] PROGMEM = "GRAND";
prog_char string_10[] PROGMEM = "SALTER";
prog_char string_11[] PROGMEM = "BOND";

// Then set up a table to refer to your strings.

PROGMEM const char *string_table1[] = 	   // change "string_table" name to suit
{   
  string_0,
  string_1,
  string_2,
  string_3,
  string_4,
  string_5 };

PROGMEM const char *string_table2[] = 	   // change "string_table" name to suit
{   
  string_6,
  string_7,
  string_8,
  string_9,
  string_10,
  string_11};
  
char buffer[30];    // make sure this is large enough for the largest string it must hold


int randNames; // random word taken from the group names
int randLastnames; // random last name
char finalWord[40];  // enough room for all strings together
int myLenght; // lenght of the final word
//int myCentre; // coordinates to place the final word in the centre

void setup(){
Serial.begin(9600);
}

void loop(){
  randNames = random (6); //picks a synonym
  randLastnames = random (6); //picks an adjective
  
  finalWord[0] = '\0';          // start with a null string:
  strcpy_P(buffer, (char*)pgm_read_word(&(string_table1[randNames])));
  strcat (finalWord, buffer); //add third string
  strcat(finalWord, " ");   // add second string, the empty space string
  strcpy_P(buffer, (char*)pgm_read_word(&(string_table2[randLastnames])));
  strcat(finalWord, buffer);   // add first string
  
  myLenght = strlen(finalWord);
 
  Serial.println(finalWord);
  
  delay(1000);
}

wow!
your code works like a charm!
8) 8) 8) 8) 8) 8) 8) 8) 8) 8) 8) 8)
Thanks!!!!!!!

Elso

Uses a bit less RAM but is also a bit slower:

#include <avr/io.h>
#include <avr/pgmspace.h>

const char PROGMEM namesTable[] = { // all this text stored in flash
  "MARK\0FRANCIS\0JOHN\0ANDREW\0ROBERT\0SEAN\0HAYES\0LUDWIG\0HARRISON\0GRAND\0SALTER\0BOND\0"
};

PGM_P nT; // will point into namesTable

#define  FIRSTNAMES    6
#define  LASTNAMES     6
PGM_P    firstName[ FIRSTNAMES ];
PGM_P    lastName[ LASTNAMES ]; 

void  printProgstring( PGM_P FM )
{
  byte fb;
  do 
  {
    fb = pgm_read_byte( FM++ );    
    if ( fb )  Serial.print( fb );
  }
  while ( fb );
}

void setup(void)
{
  Serial.begin( 9600 );

  nT = namesTable;
  firstName[ 0 ] = nT;
  
  byte i; // only counting to 6 so 1 byte is enough
  for ( i = 1; i < FIRSTNAMES; i++ )
  {
    while ( pgm_read_byte( nT++ ));
    firstName[ i ] = nT++;    
  }
  
  for ( i = 0; i < LASTNAMES; i++ )
  {
    while ( pgm_read_byte( nT++ )); // find the \0 at the end of name[ i-1 ]
    lastName[ i ] = nT++;    
  }

  for ( i = 0; i < FIRSTNAMES; i++ )
  {
    printProgstring( firstName[ i ] ); 
    Serial.println( );   
  }
  Serial.println( );   
  for ( i = 0; i < LASTNAMES; i++ )
  {
    printProgstring( lastName[ i ] ); 
    Serial.println( );   
  }

} 

void loop(void)
{
}

Elso,
I wrote before about "You could concatenate the strings with a seperation character".
The example by GoForSmoke uses a null '\0' as seperation character.

GoForSmoke,
I would make two concatenated strings. One for first names and one for last names.
It would make the code easier to read.
And perhaps the data could be written like this:

const char PROGMEM namesTable[] = { // all this text stored in flash
  "MARK\0" \
  "FRANCIS\0" \
  "JOHN\0" \
  "ANDREW\0" \
  ............  and so on
};

I delimited with \0 so that C string functions could easily use the parts. But my 1-byte-buffer print routine could look for anything. Consider though, using \0 means it can print commas.

Yeah, could go with 2 at storage but since it is the pointers that are used, I went with 1. It's only an example anyway. XD

Thank you very much indeed guys for sharing your knowledge and opinion.
I can see I am simply scratching the surface of the Progmem and strings, I will solder together an new mini OLED tonight and will see if everything works correctly.

So far I have been only compiling to check that everything works, strange enough Arduino will always give me the same randomly calculated names and last names, is that normal? I mean it puts together names and last names once, then if I unplug it and plug it back either some minutes later or the day after, it gives me back names and last names with the same order it did before, as if it can only ramdomly calculate names and last names once.

Any ideas about it?

Thanks!

Elso

The random function starts with a seed and will produce the same numbers every time.

To start with a different seed, use : http://arduino.cc/en/Reference/RandomSeed
You could read noise from an analog input, or the light from an ldr, or the time from a RTC, and use that for the seed.

Thanks Krodal!

Example to work with PROGMEM.

const char PROGMEM message[]=        "String in flash";
String getFlashStr(const char PROGMEM varMessage[]) {
	char c;
	String msg;  

	while( c= pgm_read_byte(varMessage++) ) {
		msg+= c;
	}

	return msg;
}
char buffer[16];
getFlashStr(message).toCharArray(buffer, 16);

Hi,
I came to this thread because of the same problem with OP, but my project is for ESP8266.

To make Krodal's code above to work on ESP8266, small change should be made that is

change

pgm_read_word

to

pgm_read_dword

Notice the change from using 'word' to 'dword'. I got this solution from ESP8266 forum here.
Otherwise the ESP will always reset.

Below is full revised Krodal's code that works on ESP8266 (tested on NodeMcu v3)

prog_char string_0[] PROGMEM = "MARK";   // "String 0" etc are strings to store - change to suit.
prog_char string_1[] PROGMEM = "FRANCIS";
prog_char string_2[] PROGMEM = "JOHN";
prog_char string_3[] PROGMEM = "ANDREW";
prog_char string_4[] PROGMEM = "ROBERT";
prog_char string_5[] PROGMEM = "SEAN";

prog_char string_6[] PROGMEM = "HAYES";   // "String 0" etc are strings to store - change to suit.
prog_char string_7[] PROGMEM = "LUDWIG";
prog_char string_8[] PROGMEM = "HARRISON";
prog_char string_9[] PROGMEM = "GRAND";
prog_char string_10[] PROGMEM = "SALTER";
prog_char string_11[] PROGMEM = "BOND";

// Then set up a table to refer to your strings.

PROGMEM const char *string_table1[] =   // change "string_table" name to suit
{   
string_0,
string_1,
string_2,
string_3,
string_4,
string_5 };

PROGMEM const char *string_table2[] =   // change "string_table" name to suit
{   
string_6,
string_7,
string_8,
string_9,
string_10,
string_11};

char buffer[30];    // make sure this is large enough for the largest string it must hold


int randNames; // random word taken from the group names
int randLastnames; // random last name
char finalWord[40];  // enough room for all strings together
int myLenght; // lenght of the final word
//int myCentre; // coordinates to place the final word in the centre

void setup(){
Serial.begin(9600);
}

void loop(){
randNames = random (6); //picks a synonym
randLastnames = random (6); //picks an adjective

finalWord[0] = '\0';          // start with a null string:
strcpy_P(buffer, (char*)pgm_read_dword(&(string_table1[randNames])));
strcat (finalWord, buffer); //add third string
strcat(finalWord, " ");   // add second string, the empty space string
strcpy_P(buffer, (char*)pgm_read_dword(&(string_table2[randLastnames])));
strcat(finalWord, buffer);   // add first string

myLenght = strlen(finalWord);

Serial.println(finalWord);

delay(1000);
}