<RESOLVED without hacks > How I used F() Flash Memory Strings in snprintf, etc for Arduino Nano

<EDIT - RESOLVED> (properly; no hacks )
The following tests work, using '%S' rather than '%s' (and where can people find the documentation about this?)

test(T1272_GoldStandard_Test_snprintf_9) {
  char buff128[128] = "?";
  auto str1 = "Resolved";
  auto str2 = F("Hello9");
  auto str3 = F("not this");
  str3 = F("World");
  snprintf(buff128, ARRAY_SIZE(buff128), "<%s><%S><%S>", str1, str2, str3);
  assertEqual("<Resolved><Hello9><World>", buff128);
}
test(T1271_GoldStandard_Test_snprintf_P_8) {
  char buff128[128] = "?";
  auto str1 = "Resolved";
  auto str2 = F("Hello8");
  auto str3 = F("not this");
  str3 = F("World");
  // CAUTION fails //snprintf_P(buff128, ARRAY_SIZE(buff128), "<%s><%S><%S>", str1, str2, str3); // CAUTION fails
  snprintf_P(buff128, ARRAY_SIZE(buff128), (const char*)F("<%s><%S><%S>"), str1, str2, str3);
  assertEqual("<Resolved><Hello8><World>", buff128);
}

Ignore my following hack ...
</EDIT - RESOLVED>
I know that you can print F() strings via Serial (etc), but I wanted to use them as parameters in snprintf. Ideally, I wanted something like the C#/dotNet 'yield', but it seemed too hard.
So I created a macro that did the hard work, copying the flash string to a temporary (stack) buffer, then used as an argument to snprintf.
The bad news is that the temporary buffer has to be the maximum size; this could be avoided by using heap memory, which I decided to avoid. The good news is that it prints a Serial message if the buffer would be overflowed (it doesn't; the string is truncated).

#define TEMP_BUFF_FOR_FSTRING(TempName, MaxStrLen, FString) \
 char TempName[MaxStrLen+1]; \
 { size_t ix = 0; const char* asPChar = (const char *)FString; size_t actLen = strlen_P(asPChar); \
   if(actLen > MaxStrLen) { Serial.print(F("!! " #TempName " needs ")); Serial.print(actLen, DEC); Serial.print(F(" but only ")); Serial.println(MaxStrLen, DEC) ; } \
   for(ix = 0; ix < MaxStrLen; ++ix){ TempName[ix] = pgm_read_byte(asPChar+ix); if(TempName[ix] == '\0') break; }\
   TempName[MaxStrLen] = '\0'; \**strong text**
 }

I'm sure that improvements could be made for other people's situations,, but IWFM.
Examples of usage are in some following unit tests (you will notice that the second usage displays a warning message at run time):

test(T1100_Test_TempVarOption) {
  {
    TEMP_BUFF_FOR_FSTRING(tmp1, 20, F("HelloWorld"));
    assertEqual("HelloWorld", tmp1);
  }
  {
    TEMP_BUFF_FOR_FSTRING(tmp2, 5, F("HelloWorld"));
    assertEqual("Hello", tmp2);
  }
}

Notice that putting curly braces over the shortest chunks of code will free stack space sooner.
BTW my actual usage is not simply inserting literal strings, but variables holding pointers to a range of possible values (it's opcodes for an Arduino AVR decompiler I've written).
Feel free to comment, but please refrain from ones like 'why didn't you do it another way?' I worked pragmatically with limited information and time.

#EDIT# It seems that I am getting replies that don't address the issue. To save time, when suggesting improvements or alternatives, please try and get the following test to pass, perhaps by changing the 'snprintf_P' line of code:

test(T1270_GoldStandard_Test_snprintf_P_7) {
  char buff128[128] = "?";
  auto str1 = "TheQuote";
  auto str2 = F("Hello7");
  auto str3 = F("not this");
  str3 = F("World");
  snprintf_P(buff128, ARRAY_SIZE(buff128), (const char*)F("<%s><%s><%s>"), str1, str2, str3); // change this to provide required answer
  assertEqual("<TheQuote><Hello7><World>", buff128);
}

What if someone is in a position to suggest an alternative way to do what you want that may be better in some way ?

Please go ahead and suggest away. I am not opposed to improvements or alternatives. It's just that I had difficulty finding out how to do stuff, and more information will be good. I didn't try every possible way I might have - just enough to do what I wanted in a general way (it that's not an oxymoron).
#EDIT# IOW, I welcome improvements, but not disparaging comments about why I didn't think of another way.

snprintf_P(buff, sizeof(buff), PSTR("%s %d:%02d"), label, h, m);

1 Like

Use "snprintf_P()".

  char buffer[100];
  snprintf_P(buffer, sizeof buffer, (const char *)F("Test format %d"), 10);
  Serial.println(buffer);

There are '_P' versions of the other C++ string functions that can take a 'const char *' argument. 'strcat_P()', 'strncat_P()', 'strcpy_P()'...

1 Like

A macro does not have to be ugly.
In my opinion it is better to show what the code is doing. This is your macro:

#define TEMP_BUFF_FOR_FSTRING(TempName, MaxStrLen, FString) \
  char TempName[MaxStrLen+1]; \
  { \
    size_t ix = 0; \
    const char* asPChar = (const char *)FString; \
    size_t actLen = strlen_P(asPChar); \
    if(actLen > MaxStrLen) \
    { \ 
      Serial.print(F("!! " #TempName " needs ")); \
      Serial.print(actLen, DEC); \
      Serial.print(F(" but only ")); \
      Serial.println(MaxStrLen, DEC) ; \
    } \
    for(ix = 0; ix < MaxStrLen; ++ix) \
    { \
      TempName[ix] = pgm_read_byte(asPChar+ix); \
      if(TempName[ix] == '\0') \
        break; \
    } \
    TempName[MaxStrLen] = '\0'; \
  }
1 Like

johnwasser Use "snprintf_P()".

This seems to ONLY work for the 'format' string, NOT the arguments to the format. The following were my test results:

test(T1250_Test_snprintf_P_5) {
  char buff128[128] = "?";
  snprintf_P(buff128, ARRAY_SIZE(buff128), (const char*)F("<%s><%s>"), (const char*)F("Hello5"), F("World"));
  assertEqual("<Hello5><World>", buff128); // fails; as though empty args
}
test(T1240_Test_snprintf_P_4) {
  char buff128[128] = "?";
  snprintf_P(buff128, ARRAY_SIZE(buff128), (const char*)F("<%s><%s>"), (const char*)F("Hello4"), (const char*)F("World"));
  assertEqual("<Hello4><World>", buff128); // fails; empty & random characters
}
test(T1230_Test_snprintf_P_3) {
  char buff128[128] = "?";
  snprintf_P(buff128, ARRAY_SIZE(buff128), (const char*)F("<%s><%s>"), F("Hello3"), F("World"));
  assertEqual("<Hello3><World>", buff128); // fails; unprintable characters
}
test(T1220_Test_snprintf_P_2) {
  char buff128[128] = "?";
  snprintf_P(buff128, ARRAY_SIZE(buff128), (const char*)F("<%s><%s>"), "Hello2", "World");
  assertEqual("<Hello2><World>", buff128); // works but not what I need
}
test(T1210_Test_snprintf_P_1) {
  char buff128[128] = "?";
  snprintf_P(buff128, ARRAY_SIZE(buff128), "<%s><%s>", "Hello1", "World");
  assertEqual("<Hello1><World>", buff128); // fails; random memory dump
}

My testing (see my reply to johnwasser) shows that snprintf_P does not allow F() strings to be arguments to the format string. Thus, if your 'label' variable points to an F() string, it does not print it, but apparently from random memory.

I agree in part. Indeed, that is how I first wrote it. Once I got it working, I then treated it as an 'implementation is hidden' function, and reduced the mount of screen space that it took - rather like just having the function definition on show in the header file but hiding its implementation in the cpp file, rather than having all the code in the header.

Offtopic:

This is a forum, with new and experienced users all together. So some extra explaining and showing what code does will help new users in my opinion.

I admit, it is me. I like the text of the code to be open and spacious.
When someone new shows a sketch, then I want to say: "Don't make such a terrible big mess with the text layout of the code :rage:". But instead I say: "Hello lovely new user :kiss: so glad you join Arduino :heart: would you be so kind to press Ctrl+T and make the source code look good ? :kissing: ".

Ontopic:

I had the same problem in the past. The format string can be in Flash, but parameters can not. So I had to combine strcpy_P() and strcat_P() and sprintf_P().

Could you make a full working sketch ? I'm confused by the properly/hack/fail/edit/resolved in your top post.

when do you need to format a constant string into a constant string?
you can include it into the format string.

Use %S instead of %s when using F() for the parameter.

1 Like

There is some documentation on this page: https://www.nongnu.org/avr-libc/user-manual/group__avr__stdio.html#gaa3b98c0d17b35642c0f3e4649092b9f1

Most documentation for sprintf will refer to printf for the details of the format argument.

Could someone show how to use the %S ? With all combinations of snprintf() and snprintf_P() with all combinations of a normal string, PROGMEM string, F() and PSTR() for both the format and parameters ?

I get warnings everywhere and only 1 in 5 works.

I don't mean to be rude, but that documentation is terrible. It's how I would write stuff that I didn't want people to find.

  • A scan for '%s' or '%S' finds nothing.
  • A scan for 'flash' mentions format strings but not argument strings. The name 'flash' changes for 'S' option (on its own; no percent prefix); it calls it 'program-memory (ROM)'.
  • Searching for 'argument' finds the '[%]s' option but not the '[%]S' one.

It's as though you have to know what it contains already before you can find what you're looking for.

I agree documentation is hard to find. Not easy to search on google because it likes to include all results for lower case. The flash memory option is specific to processors that have the flash and ram in separate address spaces, so does not appear in most implementations.

No, you can't. Well, I can't, and if you can show me how I would appreciate it.
This is one of my new actual snprintf statements with some added comments:

      snprintf_P(resBuff_96, ARRAY_SIZE(resBuff_96), PSTR("%c[%02X %02X] %S%s%s%S%s")
      , (lastUsed4Bytes ? '*' : ' ')
      , b1
      , b0
      , fszOpCode // this is an F() string, one of a hundred or so like 'BREAK' or 'ADD'
      , szSBOAO // This stands for *cough* Space Between Operator And Operands, which may be one space or empty
      , szOpands // this is a static RAM char buffer containing say 'R12,R23' or 'Z+0X43,R21' 
      , fszOpandsSuffix // This is one of several standard suffices as F() string like ",-X"
      , szCodeComments32 // sundry comments (static RAM) like ';-0xA0' after 'RJMP 0xF60' that goes backwards
      );
    }

An example of some lines produced:

3E00W 7C00[2]*[BD 3C] CPI R27,0xCD
3E01W 7C02[4] [0E 94 F3 3C] CALL 0x3CF3
3E02W 7C04[2]*[F3 3C] CPI R31,0xC3
3E03W 7C06[2] [66 CF] RJMP 0xF66 ;-0x9A
3E04W 7C08[2] [85 E0] LDI R24,0x5
3E05W 7C0A[4] [0E 94 BD 3C] CALL 0x3CBD
3E06W 7C0C[2]*[BD 3C] CPI R27,0xCD
3E07W 7C0E[4] [0E 94 F3 3C] CALL 0x3CF3
3E08W 7C10[2]*[F3 3C] CPI R31,0xC3
3E09W 7C12[2] [60 CF] RJMP 0xF60 ;-0xA0
3E0AW 7C14[4] [0E 94 76 3C] CALL 0x3C76
3E0BW 7C16[2]*[76 3C] CPI R23,0xC6
3E0CW 7C18[4] [80 93 06 01] STS 0x106,R24
3E0DW 7C1A[2]*[06 01] MOVW R1:R0,R13:R12
3E0EW 7C1C[4] [0E 94 76 3C] CALL 0x3C76
3E0FW 7C1E[2]*[76 3C] CPI R23,0xC6
3E10W 7C20[4] [80 93 07 01] STS 0x107,R24
3E11W 7C22[2]*[07 01] MOVW R1:R0,R15:R14
3E12W 7C24[4] [0E 94 F3 3C] CALL 0x3CF3
3E13W 7C26[2]*[F3 3C] CPI R31,0xC3
3E14W 7C28[2] [55 CF] RJMP 0xF55 ;-0xAB
3E15W 7C2A[4] [0E 94 76 3C] CALL 0x3C76
3E16W 7C2C[2]*[76 3C] CPI R23,0xC6
3E17W 7C2E[2] [80 33] CPI R24,0x30

My full .ino sketch is over 2.5K lines (no, I am not exaggerating) with a utility .cpp over 600 lines. My problem (well, not for me, but for others) is that I tend to think like an Assembler compiler. I was hoping that my test code in edited original post with unit tests would demonstrate my newly discovered usage of '%S' in the format string, to insert F() strings into the output.
I will try to condense my findings down to some decision steps.

  • If the format string is in RAM, use snprintf
  • If the format string is in FLASH ROM(i.e. F() string), use snprintf_P
  • If you are including a string from RAM, use '%s' in the format string
  • If you are including a string from FLASH ROM, use '%S' in the format string *
    * A warning; other C/C++ implementations use '%S' to include wchar_t strings - wide characters, being UniCode, and that is why you should NEVER use code like snprintf(buff, sizeof(buf), as it will insidiously fail and walk over random memory, when someone updates it to handle UniCode. Use snprintf(buff, ARRAY_SIZE(buff), instead, where you have something like #define ARRAY_SIZE(X) (sizeof(X) / sizeof(*X)).

There is probably a bit of confusion because F() is a macro used to tell the compiler to store a text literal in flash memory, something that would rarely be used for sprintf unless the format string were variable. Your code appears to be using a pointer to different texts stored in flash memory, not a literal, so F() is not being used.

My code uses const __FlashStringHelper* variables which point to various F() strings, such as

  typedef const __FlashStringHelper* FLSH_SZ_VAR;
  ...
  // FLSH_SZ_VAR fszOpCode = FLSH_SZ("");
  FLSH_SZ_VAR fszOpCode = F(""); // EDIT to make it easier; I actually switch FLASH stuff on & off
  ...
        else if(bits45 == 0b10) { fszOpCode = F("OR");  }
        else if(bits45 == 0b11) { fszOpCode = F("MOV"); }