PROGMEM and F() macro as parameters for function

Hello,

I would like to create a function that accepts:

  • Normal data in ram
  • PROGMEM data
  • "F()" macro data

That function will send the data to a Arduino I2C slave, which has its internal EEPROM accessible just like any other external I2C EEPROM.

The parameter for normal data in ram is "const void *pData".
I made a second function with "PROGMEM byte *pData". It uses memcpy_P to copy it to a local buffer and calls the normal function.
But I can't get the data with the "F()" macro.

This is a runnable sketch that shows the data on the serial monitor:

prog_uchar test2[] PROGMEM = { "Test 2" } ;

void setup()
{
  Serial.begin( 9600);
  Serial.println( "Start");
}

void loop()
{
  WriteSlave( "Test 1", 6);
  WriteSlave( test2, 6);
  // WriteSlave( PSTR("Test 3"));
  WriteSlave( F("Test 4"), 6);

  delay(5000);
}


void WriteSlave( const void *pData, int size)
{
  // write to serial monitor instead of I2C slave
  Serial.write( (const uint8_t *)pData, size);
  Serial.println("");          // new line
}

void WriteSlave( PROGMEM byte *pData, int size)
{
  byte buffer[20];

  Serial.println( "Going through PROGMEM function");

  memcpy_P( buffer, pData, size);
  WriteSlave( (const void *)buffer, size);
}

void WriteSlave( __FlashStringHelper* pData, int size)
{
  Serial.println( "Going through __FlashStringHelper function");
  // ?
}

The use of macro "F()" does not call the __FlashStringHelper function.
This is the output:

Test 1
Going through PROGMEM function
Test 2
[][][][][][]                <- unreadable characters in serial monitor

An answer should be here, What does the F() do exactly? - #3 by Coding_Badly - Programming Questions - Arduino Forum
but I still don't know how to solve this.
Or should I use normal data in ram and PROGMEM only ?

This thread: Arduino Forum may answer your question.

It is not enough to make my function work.

Hi Erdin,

I was trying to figure this out today, too. I did something like this:

void print(const char *string)
{
  Serial.println(string);
}

void print(const __FlashStringHelper *string)
{
  Serial.println(string);
}

Then I can pass RAM strings, or F("flash strings"):

print("Hello");
print(F("Hello");

Does this help? Today is the first day I have played with this.

Thank you. I noticed that you had 'const' and I forgot that.
Now I have it working, but I'm not happy with the result.

// Must be unsigned char (or byte) to use the PROGMEM function.
prog_uchar test2[] PROGMEM = { "Test 2" } ;

void setup()
{
  Serial.begin( 9600);
  Serial.println( "Start");
}

void loop()
{
  Serial.print( "1: ");
  WriteSlave( "Test 1", 6);
  Serial.print( "2: ");
  WriteSlave( test2, 6);
  // PSTR can't be used.
  // PSTR is (const PROGMEM char *)
  // The PROGMEM is an attribute.
  // The compiler can not see the difference between (const char *) and (const PROGMEM char *).
  // The PSTR would be directed to the normal (const char *) function.
  //  Serial.print( "3: ");
  //  WriteSlave( PSTR("Test 3"), 6);
  Serial.print( "4: ");
  WriteSlave( F("Test 4"), 6);
  Serial.println("");
  delay(5000);
}

void WriteSlave( const char *pData, int size)
{
  // write to serial monitor instead of I2C slave
  Serial.write( (const uint8_t *)pData, size);
  Serial.println("");          // new line
}

// The compiler can not see the difference between
// a (const char *) and a (const PROGMEM char *),
// that is why this function uses a byte pointer.
// The normal function uses a 'char' and the PROGMEM uses a 'byte'.
void WriteSlave( const PROGMEM byte *pData, int size)
{
  byte buffer[20];

  Serial.print( "-> PROGMEM -> ");

  memcpy_P( buffer, pData, size);
  WriteSlave( (const char *)buffer, size);
}

// The compiler does see a __FlashStringHelper,
// created with the "F()" macro.
// This function casts the pointer to a PROGMEM pointer.
void WriteSlave( const __FlashStringHelper* pData, int size)
{
  Serial.print( "-> __FlashStringHelper -> ");

  // Convert the __FlashStringHelper pointer to a PROGMEM pointer.
  // After that, call the function that handles te PROGMEM pointer.
  WriteSlave( (const PROGMEM byte *) pData, size);
}

It turns out, that the compiler can see the __FlashStringHelper, and directs it to that function.
However, the compiler can not distinguish between (const char *) and (const PROGMEM char *).
I had to use a trick to make it work. My PROGMEM string is a byte and the normal string in ram is a char. But if I would forget that, the wrong function is called. So I will not use that.

The next example uses the (void *) again, but uses a different function WriteSlave_P() for PROGMEM data.

prog_char test2[] PROGMEM = { "Test 2" } ;

void setup()
{
  Serial.begin( 9600);
  Serial.println( "Start");
}

void loop()
{
  Serial.print( "1 : ");
  WriteSlave( "Test 1", 6);
  Serial.print( "2 : ");
  WriteSlave_P( test2, 6);
  Serial.print( "3 : ");
  WriteSlave_P( PSTR("Test 3"), 6);

  // The "F()" macro is handled by a seperate function,
  // but it can also call WriteSlave_P direct.
  Serial.print( "4a: ");
  WriteSlave( F("Test4a"), 6);
  Serial.print( "4b: ");
  WriteSlave_P( F("Test4b"), 6);
  Serial.println("");
  delay(5000);
}

void WriteSlave( const void *pData, int size)
{
  // write to serial monitor instead of I2C slave
  Serial.write( (const uint8_t *)pData, size);
  Serial.println("");          // new line
}

// Use a seperate function "..._P" for a PROGMEM pointer.
// This way it is always clear how the pointer is handled.
void WriteSlave_P( const PROGMEM void *pData, int size)
{
  byte buffer[20];

  Serial.print( "-> PROGMEM -> ");

  memcpy_P( buffer, pData, size);
  WriteSlave( (const void *)buffer, size);
}

// The compiler can handle a __FlashStringHelper,
// created with the "F()" macro.
// This function casts the pointer to a PROGMEM pointer.
void WriteSlave( const __FlashStringHelper* pData, int size)
{
  Serial.print( "-> __FlashStringHelper -> ");

  WriteSlave_P( (const PROGMEM void *) pData, size);
}

That is still not ideal, since the compiler does not warn if the wrong function is used.

There are a couple of things you can do.

the macro F is defined as

#define F(string_literal) (reinterpret_cast<const __FlashStringHelper *>(PSTR(string_literal)))

So it already uses PSTR(), and was designed to prevent the ambiguities. The macro casts the progmem pointer to a type __FlashStringHelper* which allows functions to overload based on a type that is incompatible with normal char*.

So instead of PSTR(), just use F().

To solve the problem for global data, i.e. prog_uchar test2[] PROGMEM = { "Test 2" } ; you could use a macro as well:

#define PGMT( pgm_ptr ) ( reinterpret_cast< const __FlashStringHelper * >( pgm_ptr ) )

Here is a test case:

char test2[] PROGMEM = { "Test 2" } ;

#define PGMT( pgm_ptr ) ( reinterpret_cast< const __FlashStringHelper * >( pgm_ptr ) )

void setup()
{
  Serial.begin( 9600);
  Serial.println( "Start");
  
  Serial.print( "1: ");
  WriteSlave( "Test 1");
  Serial.print( "2: ");
  WriteSlave( PGMT( test2 ), 6);
  Serial.print( "3: ");
  WriteSlave( F("Test 3"), 6);
  Serial.println("");
  delay(5000);  
}

void loop(){}

void WriteSlave( const char *pData )
{
  Serial.write( pData );
  Serial.println("");
}

void WriteSlave( const __FlashStringHelper* pData, const int size)
{
  char buffer[ size + 1 ]; //Size array as needed.
  Serial.print( "-> __FlashStringHelper -> ");
  memcpy_P( buffer, pData, size);
  buffer[ size ] = '\0';  //protect from odd sized strings ( a short string will reveal old chars )
  WriteSlave( buffer, size);
}

As you can see, even more ambiguities are removed by having different counts of parameters ( "Test 1" is a null-terminated literal, so serial.print doesn't need a count.

But we can go one step further and remove the count from the progmem side of things:

char test2[] PROGMEM = { "Test 2" } ;

#define PGMT( pgm_ptr ) ( reinterpret_cast< const __FlashStringHelper * >( pgm_ptr ) )

void setup()
{
  Serial.begin( 9600);
  Serial.println( "Start");
  
  Serial.print( "1: ");
  WriteSlave( "Test 1");
  Serial.print( "2: ");
  WriteSlave( PGMT( test2 ));
  Serial.print( "3: ");
  WriteSlave( F("Test 3"));
  Serial.println("");
  delay(5000);  
}

void loop(){}

void WriteSlave( const char *pData )
{
  Serial.write( pData );
  Serial.println("");
}

void WriteSlave( const __FlashStringHelper* pData )
{
  char buffer[ 20 ]; //Size array as needed.
  int cursor = 0;
  prog_char *ptr = ( prog_char * ) pData;

  Serial.print( "-> __FlashStringHelper -> ");
  while( ( buffer[ cursor ] = pgm_read_byte_near( ptr + cursor ) ) != '\0' ) ++cursor;
  WriteSlave( buffer );
}

Thank you for the very good explantion !
I think I will keep the (void *), to be able to handle all sorts of data.
I will implement it and start to make a class with the functions.