A Few years ago I posed an encyclopaedic set of unit tests (here) that showed how to do anything in progmem. It went through structs, strings, arrays and all the library functions that could use progmem. However as of arduino V1.55 a it was broken as things on the Arduino platform had changed, v1.6 compounded the issues. As I regularly get visitors to this page, I thought it was in need of an update.
So I present the code, updated to work on the latest platform. As per the last article:
- This is not an intro to progmem article. The readership level is set at those who are already familiar with the arduino PROGMEM documentation here and the use of the F() macro and __FlashStringHelper*.
- Its’ designed as a reference on how to accomplish many different common tasks in PROGMEM.
- It’s here to save you the “lets see how to make this work” time and communicate common pitfalls.
- The code is well documented, so you should be able to find what you need quickly.
- There is data declarations out front, assorted unit tests in setup() and a simple unit testing framework at the end of the file.
- Some of the examples have //JBYCDMUS after the line. This stands for “just because you can, doesn’t mean you should” implying It works but is not recommended.
- To use this search for the method you need to use, and see it running in working tested code.
//----------------------------------------------------------------------------------// // BUSYDUCKS.COM // // _ _ _ // // ( o)> ( o)< __ ( o)< // // ____\\ ____\\ __ (.. ) \\\_\\ // // ~~~~\_///__)~~\_///__)~~\_\\V__)~~\_____)~~~~~ // // // // Making you pro-duck-tive // // // // Author: Duckman Date: 08/02/16 Ver: 1.6 Licence: Creative Commons (by-sa) // // // // Demonstrates the use of PROGMEM. // // Compile with Arduino environment 1.6 or later // // // // Permision given to freely copy/paste "code snippets" into your own code. For // // other uses (e.g. derivative works) the Creative Commons Attribution Share- // // alike license applies (cite busyducks.com). This means comerical use is ok. // //----------------------------------------------------------------------------------// #if !(defined(ARDUINO) && ARDUINO >= 140) #include <environment.h> #endif void setTestName(const __FlashStringHelper* name); void setTestName(const char *name); bool assert(bool isTrue, Print *outStream=NULL); void pass(); void fail(Print *outStream=NULL); void printTestSummary(Print *outStream); /* char, (1 byte) -127 to 128 also letters i.e. 'a' pgm_read_byte unsigned char, byte (1 byte) 0 to 255 pgm_read_byte int, short (2 bytes) -32,767 to 32,768 pgm_read_word unsigned int, unsigned short (2 bytes) 0 to 65,535 pgm_read_word long (4 bytes) -2,147,483,648 to 2,147,483,647 pgm_read_dword unsigned long (4 bytes) 0 to 4,294,967,295 pgm_read_dword For ATMEGA based arduino's double is the same as float float, double (4 bytes) -3.4028235E+38 to 3.4028235E+38 pgm_read_float */ //-------------------------------------------------------------------------------------------------------------- // Data structures for test //-------------------------------------------------------------------------------------------------------------- struct Person { int height; char *name; int age; Person *spouse; }; //-------------------------------------------------------------------------------------------------------------- // Test Data Sets //-------------------------------------------------------------------------------------------------------------- #define BYTE_TEST_PATTERN_DATA {0, 1, 2, 3, 4, 9, 15, 16, 21, 31, 32, 64, 100, 201, 212, 255, 7} #define BYTE_TEST_PATTERN_SIZE 17 #define MAX_STRING_SIZE 100 //-------------------------------------------------------------------------------------------------------------- // Test Data //-------------------------------------------------------------------------------------------------------------- // regular data const byte progMem_ucharArray[] PROGMEM = BYTE_TEST_PATTERN_DATA; const byte progMem_uchar PROGMEM = 130; const int progMem_int PROGMEM = -3021; const long progMem_long PROGMEM = 2000000001; const float progMem_float PROGMEM = 3.14159265359; const char progMem_string[] PROGMEM = "Snollygoster"; const char progMem_string2[] PROGMEM = "Problem Alchemist"; const char progMem_string3[] PROGMEM = "Lead Paradigm Architect"; const char progMem_emptyString[] PROGMEM = ""; const float progMem_floatArray[2][5] PROGMEM = {{0.2, 0.1, 0.5, 0.7, -3.2}, {0.4, 0.2, 1.0, 1.4, -6.4}}; // NB this used to be in the form "PROGMEM const char *progMem_stringArray[] =", // now the extra const is required. PROGMEM const char *const progMem_stringArray[] = { progMem_string, progMem_string2, progMem_string3 }; // STRUCTS // this line is a foward decleration, it allows person1 to create a valid pointer // to person2 even though person2 is not defined yet. extern const Person progMem_person2; const char _name[] PROGMEM = "Papa Smurf"; const Person progMem_person PROGMEM = {189, (char *)_name, 32, (Person*)&progMem_person2}; const char _name2[] PROGMEM = "Muma Smurf"; const Person progMem_person2 PROGMEM = {160, (char *)_name2, 41, (Person*)&progMem_person}; //-------------------------------------------------------------------------------------------------------------- // Stats //-------------------------------------------------------------------------------------------------------------- long passCount = 0; long failCount = 0; long countAtLastNameChange = 0; char currentTestName[64]; //-------------------------------------------------------------------------------------------------------------- // setup and loop //-------------------------------------------------------------------------------------------------------------- void setup() { byte localData[] = BYTE_TEST_PATTERN_DATA; char buffer[MAX_STRING_SIZE]; Serial.begin(9600); //JBYCDMUS = just because you can, doesn't mean you should Serial.println(F("---------------- single variable tests")); setTestName(F("single variable tests")); assert( progMem_uchar == 130 ); //JBYCDMUS assert( pgm_read_byte(&progMem_uchar) == 130 ); assert( progMem_int == -3021 ); //JBYCDMUS assert( progMem_long == 2000000001 ); //JBYCDMUS assert( (int)(pgm_read_float(&progMem_float)*10000) == 31415 ); Serial.println(F("---------------- array tests")); setTestName(F("array tests")); for(int i=0; i< BYTE_TEST_PATTERN_SIZE; i++) { assert( pgm_read_byte(&progMem_ucharArray[i]) == localData[i] ); } for(int i=0; i<5; i++) { // the easy way of accessing the array float a = pgm_read_float(&progMem_floatArray[0][i]); // another way of accessing the array same as [1][i] float b = pgm_read_float(((unsigned int)progMem_floatArray)+(5+i)*sizeof(float)); a = a * 2; // to check two floating points are "equal" assert (abs(a - b) < 0.0001 ); } Serial.println(F("---------------- struct tests")); setTestName(F("struct tests")); assert( progMem_person.height == 189 ); //JBYCDMUS assert( pgm_read_byte(&progMem_person.height) == 189 ); // NB: this used to be (prog_char *)pgm_read_word(&progMem_person.name) but prog_char is depreciated assert( strcmp_P("Papa Smurf", (char PROGMEM *)pgm_read_word(&progMem_person.name)) == 0 ); assert( pgm_read_byte(&progMem_person.age) == 32 ); assert( progMem_person.age == 32 ); //JBYCDMUS // get person1's spouse Person *p= (Person *)pgm_read_word(&progMem_person.spouse); assert( p == &progMem_person2 ); assert( pgm_read_byte(&p->age) == 41 ); //get person2's spouse p = (Person *)pgm_read_word(&progMem_person2.spouse); assert( p == &progMem_person ); assert( pgm_read_byte(&p->age) == 32 ); Serial.println(F("---------------- memory operation tests")); setTestName(F("memcmp [memory compare]")); // compares len bytes of the memory s1 and flash s2 byte data[] = BYTE_TEST_PATTERN_DATA; assert( memcmp_P(data, progMem_ucharArray, BYTE_TEST_PATTERN_SIZE) == 0 ); data[BYTE_TEST_PATTERN_SIZE-1] = 0; assert( memcmp_P(data, progMem_ucharArray, BYTE_TEST_PATTERN_SIZE) < 0 ); data[BYTE_TEST_PATTERN_SIZE-1] = 255; assert( memcmp_P(data, progMem_ucharArray, BYTE_TEST_PATTERN_SIZE) > 0 ); // copy len bytes from flash to SRAM memset (data, 0, BYTE_TEST_PATTERN_SIZE); assert( memcpy_P(data, progMem_ucharArray, BYTE_TEST_PATTERN_SIZE) > 0 ); assert( memcmp(data, localData, BYTE_TEST_PATTERN_SIZE) == 0 ); Serial.println(F("---------------- string function tests")); // ---- strcmp & strncmp Compare two strings setTestName(F("strcmp [string compare]")); assert( strcmp_P("Snollygoster", progMem_string) == 0 ); assert( strcmp_P("sNollyGostEr", progMem_string) != 0 ); assert( strcmp_P("Bug", progMem_string) < 0 ); assert( strcmp_P("Zoo", progMem_string) > 0 ); setTestName(F("strncmp [string compare, first n chars]")); assert( strncmp_P("Snollyfoobar", progMem_string, 6) == 0 ); assert( strncmp_P("sNollyGostEr", progMem_string, 6) != 0 ); assert( strncmp_P("Bug", progMem_string, 3) < 0 ); assert( strncmp_P("Zoo", progMem_string, 3) > 0 ); // ---- strcasecmp & strncasecmp, Compare two strings, ignoring case assert( strcasecmp_P("Snollygoster", progMem_string) == 0 ); assert( strcasecmp_P("sNollyGostEr", progMem_string) == 0 ); assert( strcasecmp_P("Bug", progMem_string) < 0 ); assert( strcasecmp_P("Zoo", progMem_string) > 0 ); setTestName(F("strncasecmp [string compare ignoring case]")); assert( strncasecmp_P("Snollyfoobar", progMem_string, 6) == 0 ); assert( strncasecmp_P("sNollYGostEr", progMem_string, 8) == 0 ); assert( strncasecmp_P("Bug", progMem_string, 3) < 0 ); assert( strncasecmp_P("Zoo", progMem_string, 3) > 0 ); // ---- strcpy, strlcpy & strncpy: makes a copy of a string setTestName(F("strcpy [copy a string]")); clear(buffer, MAX_STRING_SIZE); //empty string buffer assert( strcpy_P(buffer, progMem_string) == buffer ); assert( strncmp(buffer, "Snollygoster", MAX_STRING_SIZE) == 0 ); setTestName(F("strlcpy [copy a string of maximim size, ensure null terminated result]")); char smallBuffer[12]; // not long enough to hold "Snollygoster" AND the final null terminator clear(buffer, MAX_STRING_SIZE); // empty string buffer clear(smallBuffer, 12); //empty string buffer assert( strlcpy_P(buffer, progMem_string, MAX_STRING_SIZE) == 12 ); assert( strlcpy_P(smallBuffer, progMem_string, 12) == 12 ); assert( strcmp(buffer, "Snollygoster") == 0 ); // This is the difference between strlcpy and strncpy, the whole string was not copied // to ensure a null terminator was present assert( strcmp(smallBuffer, "Snollygoste") == 0 ); setTestName(F("strncpy [copy a string of maximim size, null terminate iff space allows]")); clear(buffer, MAX_STRING_SIZE); // empty string buffer assert( strncpy_P(buffer, progMem_string, MAX_STRING_SIZE) == buffer ); assert( strcmp(buffer, "Snollygoster") == 0 ); // ---- strcat, strlcat & strncat: Concatenate two strings setTestName(F("strcat [join two strings]")); strcpy(buffer, "Chief "); assert( strcat_P(buffer, progMem_string) == buffer ); assert( strcmp(buffer, "Chief Snollygoster") == 0 ); setTestName(F("strlcat [join two strings, limiting final size, ensure termination]")); strcpy(smallBuffer, "Chief "); //smallBuffer is only 12 characters // NOTE strlcat_P n = sizeof destination; while strncat_P n = number of chars to copy assert( strlcat_P(smallBuffer, progMem_string, 12) == 18 ); // 12th character is the nul terminator assert( strcmp(smallBuffer, "Chief Snoll") == 0 ); strcpy(buffer, "Chief "); //smallBuffer is only 12 characters assert( strlcat_P(buffer, progMem_string, MAX_STRING_SIZE) == 18 ); assert( strcmp(buffer, "Chief Snollygoster") == 0 ); setTestName(F("strncat [join two strings, limiting final size]")); strcpy(buffer, "Chief "); // NOTE strncat_P n = number of chars to copy; while strlcat_P n = sizeof destination assert( strncat_P(buffer, progMem_string, 6) == buffer ); assert( strcmp(buffer, "Chief Snolly") == 0 ); // ---- strlen & strnlen: Finds the length of the string (searches for the null terminator) setTestName(F("strlen [find the length of a string]")); assert( strlen_P( progMem_string) == 12 ); setTestName(F("strnlen [find the length of a string, limited to n]")); assert( strnlen_P( progMem_string, 6) == 6 ); assert( strnlen_P( progMem_string, MAX_STRING_SIZE) == 12 ); // ---- strstr: searches s1 for the first occurrence of (the substring) s2 setTestName(F("strstr [find a string inside another string]")); // one item to find strcpy(buffer, "A Snollygoster sounds like something you would find in a handkerchief."); assert( strstr_P(buffer, progMem_string) == &buffer[2] ); assert( strstr_P("foobar", progMem_string) == NULL ); // what is defined to happen when searching an empty/null strings strcpy(buffer, "foobar"); assert( strstr_P(buffer, NULL) == NULL ); // this is why "if(strstr_P(s1, s2) != null){...}" is a bad idea!" assert( strstr_P(buffer, progMem_emptyString) == buffer ); Serial.println(F("---------------- done")); if(failCount > 0) { Serial.print(passCount); Serial.println(F(" test(s) passed")); Serial.print(failCount); Serial.println(F(" test(s) FAILED")); } else { Serial.print(F("All tests passed (")); Serial.print(passCount); Serial.println(F(" tests rum)")); } } void loop() { } void clear(char *buffer, int len) { memset(buffer, '\0', len); } //---------------------------------------------------------------------------------------------------------- // Testing framework data //---------------------------------------------------------------------------------------------------------- #define TEST_NAME_BUFFER_LEN (64) extern long passCount; extern long failCount; extern long countAtLastNameChange; extern char currentTestName[TEST_NAME_BUFFER_LEN]; //---------------------------------------------------------------------------------------------------------- // Testing framework //---------------------------------------------------------------------------------------------------------- template<typename T> __attribute__ ((noinline)) bool assert_EQ(T a, T b, Print *outStream=NULL) { bool ok = assert(a == b, outStream); if((!ok) && (outStream != NULL)) { outStream->print(F(" error: 0x")); outStream->print(a, HEX); outStream->print(F(" != 0x")); outStream->println(b, HEX); } } template<typename T> __attribute__ ((noinline)) bool assert_NEQ(T a, T b, Print *outStream=NULL) { bool ok = assert(a != b, outStream); if((!ok) && (outStream != NULL)) { outStream->print(F(" error: 0x")); outStream->print(a, HEX); outStream->print(F(" != 0x")); outStream->println(b, HEX); } } template<typename T> __attribute__ ((noinline)) bool assert_GT(T a, T b, Print *outStream=NULL) { bool ok = assert(a > b, outStream); if((!ok) && (outStream != NULL)) { outStream->print(F(" error: 0x")); outStream->print(a, HEX); outStream->print(F(" <= 0x")); outStream->println(b, HEX); } } template<typename T> __attribute__ ((noinline)) bool assert_LT(T a, T b, Print *outStream=NULL) { bool ok = assert(a < b, outStream); if((!ok) && (outStream != NULL)) { outStream->print(F(" error: 0x")); outStream->print(a, HEX); outStream->print(F(" >= 0x")); outStream->println(b, HEX); } } void setTestName(const __FlashStringHelper* name) { currentTestName[0]= 0; if(name != NULL) { strlcpy_P(currentTestName, (const char PROGMEM *)name, TEST_NAME_BUFFER_LEN); } countAtLastNameChange = passCount+failCount; } void setTestName(const char *name) { currentTestName[0]= 0; if(name != NULL) { strlcpy(currentTestName, name, TEST_NAME_BUFFER_LEN); } countAtLastNameChange = passCount+failCount; } bool assert(bool isTrue, Print *outStream) { if(isTrue) { pass(); return true; } else { fail(outStream); return false; } } void pass() { passCount++; } void fail(Print *outStream) { if(outStream != NULL) { outStream->print(F("TEST FAILED: ")); if(currentTestName != NULL) { int localTestNum = (passCount + failCount) - countAtLastNameChange + 1; outStream->print(F("(")); outStream->print(currentTestName); outStream->print(F(" [test ")); outStream->print(localTestNum); outStream->print(F("])")); } outStream->println(); } failCount++; } void printTestSummary(Print *outStream) { if(outStream != NULL) { if(failCount > 0) { outStream->print(passCount); Serial.println(F(" test passed")); outStream->print(failCount); Serial.println(F(" test FAILED")); } else { outStream->print(F("All tests passed (")); Serial.print(passCount); Serial.println(F(" tests run)")); } } }