How to use Flash strings for initialisation through a string constant

Here is the thing. I have a function that just creates an empty file on an SD card.

bool touch(char *pathName) {
File f;
bool result = f.open(pathName, FILE_WRITE);
f.close();
return result;
}

I also want to set up a predefines list of files. But, the file pathnames are const strings and so end up in RAM, taking valuable space. For example:

touch("config.txt");

eats up another 12 bytes of RAM (as well as warning me about C++ and string constants).

I know I could write a version of touch that accepts flash strings but this is an example of a wider problem where library functions expect a char * argument and the application wants to use a string constant in the same way I just illustrated.

I can work around the whole thing by using the String class like this:

touch(String(F("config.txt")).c_str());

but it is pretty ugly and drags in about 1.5k of unwanted String code .

So - what is a good way to store initialiser strings in flash so that they can be used where char * arguments are expected?

I neglected to record the post's URL for context but here's a sketch (not mine) that illustrates the concept.

struct pyClass_t {
  int16_t pyNumber;  // or if you need 32 bits declare as --> int32_t pyNumber; 
  const char* const pyName;
};

// you define first your cStrings in flash memory
const char v00[] PROGMEM = "420";
const char v01[] PROGMEM = "2000";
const char v02[] PROGMEM = "29ER";
const char v03[] PROGMEM = "505";
const char v04[] PROGMEM = "ALBACORE/KSTRL";
const char v05[] PROGMEM = "BLAZE";
//... do the same for all your strings

// Then you use the strings names (pointers) to add them into the structure
const pyClass_t pyClasses[] PROGMEM = {
  {1111, v00},
  {1112, v01},
  { 907, v02},
  { 903, v03},
  {1038, v04},
  {1027, v05}
  // ... all your data
};

// this is the number of entries
const size_t nbElements = sizeof(pyClasses) / sizeof(pyClasses[0]);

void setup() {
  // Max 14 Characters per pyName (otherwise won't fit on display)
  char buffer[20]; // Needs to be large enough! (at least 15 = your 14 + 1 char for the trailing null char)

  Serial.begin(115200);
  Serial.println();
  for (size_t i = 0; i < nbElements; i++)
  {
    Serial.print(i);
    Serial.print(F(")\t"));
    Serial.print( (int16_t) pgm_read_word_near(&(pyClasses[i].pyNumber)) );
  // Serial.print( (int32_t) pgm_read_dword_near(&(pyClasses[i].pyNumber)) ); // if you need 32 bits
    Serial.print(F("\t"));
    strcpy_P(buffer, (const char* const)pgm_read_word(&(pyClasses[i].pyName)));
    Serial.println( buffer );
  }
}

void loop() {}

see also: avr-libc: Data in Program Space

Unless the function you're calling accepts an argument of type '__FlashStringHelper *', you'll have to pull the string into a temporary RAM buffer using one of the strcpy_P() type of functions. BTW, even if your function does accept '__FlashStringHelper *' arguments, it will do the same thing internally.

Temporary allocation of RAM while the string is being used is fine.

Thanks for the suggestion dougp.

The most space efficient method then seems to be something like:

    const PROGMEM char path01[] = "dart.cfg";
    const PROGMEM char path02[] = "arrow.cfg";
    const PROGMEM char path03[] = "lightning.cfg";

    bool touch(const char *pathName) {
      File f;
      bool result = f.open(pathName, FILE_WRITE);
      f.close();
      return result;
    }

   void setup(){
      :
      :
      touch(path01);
      touch(path02);
      touch(path03);
      :
      :
    }

It would be more tidy still if I could declare the strings as an array in PROGMEM but I can't for the life of me work out a declaration that does that.

peterharrison:
The most space efficient method then seems to be something like:
...

This is not going to work.

-> To convince yourself just try this

const PROGMEM char path01[] = "dart.cfg";
const PROGMEM char path02[] = "arrow.cfg";
const PROGMEM char path03[] = "lightning.cfg";

void touch(const char* pathName) {
  Serial.println(pathName);
}

void setup() {
  Serial.begin(115200);
  touch(path01);
  touch(path02);
  touch(path03);
}

void loop() {}

You won't see your string in the Serial console but garbage

It would need to be something like this to work fine

const PROGMEM char path01[] = "dart.cfg";
const PROGMEM char path02[] = "arrow.cfg";
const PROGMEM char path03[] = "lightning.cfg";

void touch(const void* pathName) {
  Serial.println((const __FlashStringHelper*) pathName);
}

void setup() {
  Serial.begin(115200);
  touch(path01);
  touch(path02);
  touch(path03);
}

void loop() {}

and that's because the println() function knows how to handle a __FlashStringHelper pointer

In your case the open function does not know how to handle a __FlashStringHelper pointer so you would have to create a local cString (null terminated char buffer) to call open() and close(). Something like this (using an array which is also in flash memory to make your life easier) should work

const PROGMEM char path01[] = "dart.cfg";
const PROGMEM char path02[] = "arrow.cfg";
const PROGMEM char path03[] = "lightning.cfg";

const char* const pathList[] PROGMEM = {path01, path02, path03};
const uint8_t nbPaths = sizeof(pathList) / sizeof(pathList[0]);


void touch(const uint8_t index) {
  char buffer[51];
  strncpy_P(buffer, pgm_read_word(&(pathList[index])), 50); // make sure you don't overwflow the buffer
  buffer[50] = '\0'; // just in case

  Serial.print(F("pathList["));
  Serial.print(index);
  Serial.print(F("] = \t"));
  Serial.println( buffer );

  // you would do your open / close on buffer
}

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

  for (uint8_t index = 0; index < nbPaths; index++) {
    touch(index);
  }
}

void loop() {}

or if you want to keep a function accepting an entry in the array

const PROGMEM char path01[] = "dart.cfg";
const PROGMEM char path02[] = "arrow.cfg";
const PROGMEM char path03[] = "lightning.cfg";

const char* const pathList[] PROGMEM = {path01, path02, path03};
const uint8_t nbPaths = sizeof(pathList) / sizeof(pathList[0]);


void touch(const char* const& p) {
  char buffer[51];
  strncpy_P(buffer, pgm_read_word(&p), 50); // make sure you don't overwflow the buffer
  buffer[50] = '\0'; // just in case
  Serial.println( buffer );

  // you would do your open / close on buffer
}

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

  for (uint8_t index = 0; index < nbPaths; index++) {
    Serial.print(F("pathList["));
    Serial.print(index);
    Serial.print(F("] = \t"));
    touch(pathList[index]);
  }
}

void loop() {}

Thank you. Yes - I had just worked that out and was editing my earlier reply when visitors arrived before I could finish.

Thank you for taking the trouble to set it all out.

In my case, I wanted to preserve the signature of the touch() function because it is just an example of a library function that expects to see a const char * argument.

That means doing the PROGMEM copy on the caller side. The same solution I think but applied in a different place:

const PROGMEM char path01[] = "dart.cfg";
const PROGMEM char path02[] = "arrow.cfg";
const PROGMEM char path03[] = "lightning.cfg";
const PROGMEM char path04[] = "viper.cfg";

const char *const pathList[] PROGMEM = {path01, path02, path03, path04};
const int PathCount = sizeof(pathList) / sizeof(pathList[0]);

void setup() {
  :
  for (int i = 0; i < PathCount; i++) {
    char buf[32];
    strcpy_P(buf, (char*)pgm_read_word(&(pathList[i])));
    touch(buf);
  }
  :
}

Not the first or last time I have cursed the memory architecture of the AVR.

Thanks again.

peterharrison:
Not the first or last time I have cursed the memory architecture of the AVR.

:slight_smile: :smiling_imp:

peterharrison:
In my case, I wanted to preserve the signature of the touch() function because it is just an example of a library function that expects to see a const char * argument.

That means doing the PROGMEM copy on the caller side. The same solution I think but applied in a different place:

Why not overload the library function with a new one to take a 'const __FlashStringHelper *' argument? That overloaded version would pull the string from PROGMEM and call the original version of the function with the expected 'const char *' argument. That way you only do the copy from PROGMEM in one place.

Have you tried overloading the 'touch()' function?

void setup()
{
  Serial.begin(115200);
  touch(F("Character string in FLASH"));
}


void touch(const __FlashStringHelper *fsh)
{
  int len = strlen_P((const char *)fsh);
  // Serial.println(len);
  char ca[len+1];  // Allocate space on the stack
  strcpy_P(ca, (const char *)fsh);  // Move from FLASH to the stack
  touch(ca);  // Call the base function
}


void touch(const char *cp)
{
  Serial.println(cp);
}


void loop() {}