Problems working with strings from Flash memory (F() and PROGMEM)

Hello Community,

Hope that someone can help me with my problem moving all static text into Flash memory.

The board I use is a Mega 2560 with Ethernet shield, a real time clock DS3231 and a self made shield for all needed I/O ports, relays and LEDs.

My project is an alarm system based on a state machine. External triggers and events can change the state, and with it the output pins. Every state change is written into a log file. The text for each line is compiled from real time, date, IP address of the requester – if the trigger was an Ethernet request, and the actual message.

In my first attempt, I created the strings “hard coded” in the code – which quickly brought the SRAM down to its knees.
Example:

sprintf (tmpString, "%s %s %s", Zustand WechselToDisarmed: RFID ", sensorText, " hat System Armed");

The second attempt was to create the text from separate modules, where mostly every word was a separate array of chars. Now the text is easily compiled via sprintf, using the char * .
Example:

sprintf (tmpString, "%s %s: %s %s %s %s %s",s_Zustand, s_WechselToDisarmed, s_RFID, sensorText, s_hat,s_System, s_Armed);

Now I have most of the text only once in the memory, but with the number of different messages, the SRAM is still used too much. To remedy this, I tried to put the text into the Flash memory. The first attempt was to use F(). This works great on Serial.print and client.print, but apparently not with sprintf and strcat (See Option 1 in my code).

Option 2 and option 3 use PROGMEM. Option 2 would be my preferred solution. The problem is that it only seems to work with a separate buffer per parameter. My problem is that the number of parameters and their length vary with the text. That means creating many buffers would put a high load on the SRAM again – which I try to avoid.

Option 3 works, but it unnecessarily inflates the code and makes the code virtually unreadable.

Has anybody an idea how to solve this problem by using Option 2? For weeks now I’ve done extensive research on the Forum, but I wasn’t able to find a solution or an idea for how to tackle this problem.

Thanks in advance for your help.

Axel

#include <avr/pgmspace.h>

const byte d_Armed                      =  0;  const char s_Armed[]                      PROGMEM = "Armed";
const byte d_WechselToDisarmed          =  1;  const char s_WechselToDisarmed[]          PROGMEM = "WechselToDisarmed";
const byte d_RFID                       =  2;  const char s_RFID[]                       PROGMEM = "RFID";
const byte d_Zustand                    =  3;  const char s_Zustand[]                    PROGMEM = "Zustand";
const byte d_System                     =  4;  const char s_System[]                     PROGMEM = "System";
const byte d_hat                        =  5;  const char s_hat[]                        PROGMEM = "hat";

// Then set up a table to refer to your strings.
const char* const string_table[] PROGMEM = { s_Armed,
                                             s_WechselToDisarmed,
                                             s_Zustand,
                                             s_RFID,
                                             s_System,
                                             s_hat,
                                           };
                                           
char buffer[50];    // make sure this is large enough for the largest string it must hold

#define _Stringlaenge     255

char sensorText[]             = "Sensor 1"; // In the real program this text is assigned by the trigger detector
char tmpString[_Stringlaenge] = "";


// ######################## Function getString ################################################################################
char *get_string (int i_element)
{
  
  strcpy_P(buffer, (char*)pgm_read_word(&(string_table[i_element]))); // Necessary casts and dereferencing, just copy.

  return (buffer);
  
} // End Function get_string


// ######################## Function Setup ####################################################################################
void setup()
{
  
  Serial.begin(9600);
  while(!Serial);
  Serial.println("Setup done");
  
} // End Function Setup


// ######################## Loop Forever ######################################################################################
void loop()
{

  // ##########################################  Option 1 with F()  ###########################################################
  // The best solution, unfortunately F() doesn't work in this context. Sprintf deliveres garbage and for strcat, the compiler reports errors  
  // Expected Output: "Zustand WechselToDisarmed: RFID Sensor 1 hat System Armed"
  sprintf (tmpString, "%s", F("Zustand WechselToDisarmed: RFID ")); // This line delivers garbage
  strcat (tmpString,sensorText);                                    // This line works
//  strcat (tmpString, F(" hat System Armed"));                       // Compiler error
     
  Serial.println (tmpString);
  // Output: Sensor 1     
     
     
  // ##########################################  Option 2 with PROGMEM and sprintf  ###########################################
  // My hope was that sprintf processes the parameters one by one, running the function get_string and then copying the result. Apparently 
  // all the function calls are called before the copies take place. Therefore sprintf copies 6 times the last string "Armed".
  // Expected Output: "Zustand WechselToDisarmed: RFID Sensor 1 hat System Armed"
  sprintf (tmpString, "%s %s: %s %s %s %s %s", get_string(d_Zustand),get_string(d_WechselToDisarmed),get_string(d_RFID),
                                               sensorText,
                                               get_string(d_hat),get_string(d_System),get_string(d_Armed));

  Serial.println (tmpString);
  // Output: Armed Armed: Armed Sensor 1 Armed Armed Armed


  // ##########################################  Option 3 with PROGMEM and seperate strcat ####################################
  // This version works, however is way to much code for the result
  // Expected Output: "Zustand WechselToDisarmed: RFID Sensor 1 hat System Armed"
  tmpString[0] = '�'; // Delete old content of string
  strcat (tmpString,get_string(d_Zustand));
  strcat (tmpString," ");
  strcat (tmpString,get_string(d_WechselToDisarmed));
  strcat (tmpString,": ");
  strcat (tmpString,get_string(d_RFID));
  strcat (tmpString," ");
  
  strcat (tmpString,sensorText);
  
  strcat (tmpString," ");
  strcat (tmpString,get_string(d_hat));
  strcat (tmpString," ");
  strcat (tmpString,get_string(d_System));
  strcat (tmpString," ");
  strcat (tmpString,get_string(d_Armed));

  Serial.println (tmpString); // Output as expected

 
while (1); // Stop program here

} // End Loop

Did you mean sprintf_P?

Thank you for your lightning fast responses.

Yes, I meant sprintf, but maybe there is a better solution.

As for sprintf_P, if I understand the function right, it puts the result into PROGMEM. However, I need the result in SRAM to send it into the log file and to the serial output.

So my problem is that the sources for the sprintf are located in PROGMEM and the result is in SRAM.

Stupid me - of course. How could the PROGMEM be changed at runtime. Makes perfect sense.
I will do some tests with sprintf_P as you mentioned. Maybe this is the solution to my problem.

But now it's off to work.

TTFN

Axel

Delta_G - you rock - you have solved my problem. Thank you so much.

In case somebody else has a similar problem, here is the sample code of how it works now:

/*

Demo program for PROGMEM 

The intention is to have text modules stored in the program Flash Memory.
At runtime, these modules (words) are read and compiled into a sentence, stored in SRAM. 
This allows to create different messages from words stored only once.
Once the text is back in SRAM, it can be processed, printed annd manipulated in any way.

*/

#include <avr/pgmspace.h>

const char p_This[]         PROGMEM = "This";
const char p_text[]         PROGMEM = "text";
const char p_was[]          PROGMEM = "was";
const char p_located[]      PROGMEM = "located";
const char p_in[]           PROGMEM = "in";
const char p_the[]          PROGMEM = "the";
const char p_Flash_Memory[] PROGMEM = "Flash Memory";
// To make the text readable in the later sprintf_P, I call the variables like their content

                                           
#define _strLength     255


char textInSRAM[]          = "and this text came from the SRAM";

char tmpString[_strLength] = "";


// ######################## Function Setup ####################################################################################
void setup()
{
  
  Serial.begin(9600);
  while(!Serial); // Waiting for the serial port to initialize
  
  Serial.println(F("Setup done"));  // The macro F() puts the text into the Flash Memory

} // End Function Setup


// ######################## Loop Forever ######################################################################################
void loop()
{
  int intDemo = 25;

  // Expected Output: "This text was located in the Flash Memory, and this text came from the SRAM. And a number: 25"
  Serial.println("This text was located in the Flash Memory, and this text came from the SRAM. And a number: 25");
  
  // And now from sprintf_P:
  sprintf_P (tmpString, PSTR("%S %S %S %S %S %S %S, %s. And a number: %d"), p_This,p_text, p_was, p_located, p_in, p_the, p_Flash_Memory,
                                                                               textInSRAM, intDemo);
  // NOTE: For strings from PROGMEM: %S
  //       For strings from SRAM:    %s
  
  Serial.println (tmpString);


 
while (1); // Stop program here

} // End Loop