A little help with 2D Array?

Using an Uno with PlatformIO.

I'm trying to store hex values in a 2D const byte array called blendEQ. There are 13 rows, each has 4 hex values.

const byte blendEQ[13][4] = {
  {0x01, 0x00, 0x00, 0x00},
  {0x00, 0xE3, 0xD7, 0x0A},
  {0x00, 0xCA, 0x3D, 0x71},
  {0x00, 0xB0, 0xA3, 0xD7},
  {0x00, 0x99, 0x99, 0x9A},
  {0x00, 0x82, 0x8F, 0x5C},
  {0x00, 0x6C, 0xCC, 0xCD},
  {0x00, 0x58, 0x51, 0xEC},
  {0x00, 0x45, 0x1E, 0xB8},
  {0x00, 0x31, 0xEB, 0x85},
  {0x00, 0x20, 0x00, 0x00},
  {0x00, 0x0F, 0x5C, 0x29},
  {0x00, 0x00, 0x00, 0x00},
};

Right now I'm just trying to get it to serial print the values to see if it's working. Later my actual intent is to send a row over I2C driven by an integer value.

 Serial.println (blendEQ[3][4], HEX);

Currently it only reports "0".

My question of course is why I don't see the hex values on serial monitor, but also I want to ask; if I call blendEQ[3][4], does this call only the value in row 3, column 4, or does it call all values in row 3, up to collumn 4?

Full code below:

#include <Arduino.h>
#include <Wire.h> // I2C library.
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <avr/pgmspace.h>

void sendVolume();
void sendDisplay();
void updateDisplay();
void sendTilt();

// OLED screen setup.
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET     4 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C /// Screen I2C address, 7-bit.
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Rotary encoder variables
const byte inputCLK = 3; // Arduino pin the rotary encoder CLK pin is connected to.
const byte inputDT = 2; // Arduino pin the rotary encoder DT pin is connected to.
int currentStateCLK; // Current state of rotary encoder pin CLK. Used to identify rotation.
int previousStateCLK; // Current state of rotary encoder pin CLK. Used to identify rotation.
boolean encCCW; // Rotary encoder flag to denote counter-clockwise rotatation by one pulse.
boolean encCW; // Rotary encoder flag to denote clockwise rotatation by one pulse.

// Push button variables
const byte buttonPin = 4; // Arduino pin the push button is connected to.
byte previousButtonState = HIGH;  // Assume switch open because of pull-up resistor.
byte currentButtonState; // Will be populated by reading button pin satae.
const unsigned long debounceTime = 5;  // Milliseconds of debounce.
unsigned long buttonPressTime;  // Time since button last changed state. Used to differentialte button bounce vs. intentional press.
boolean buttonPressed = 0; // A flag variable to identify button presses. Returned from button debounce code. 

// Menu variables
int mode = 1;  // Mode variable used to run switch case. 1 = volume, 2 = audio input, 3 = tone control.
int currentInputNumber = 0; // Audio input the system is currently set to.
int newInputNumber = 0; // Audio input user is in the process of choosing by rotary control.
int currentTiltValue = 6; // Current tone control setting.
int newTiltValue = 6; // Tone control setting the user is in the process of choosing by rotary control.

const char inputName[][10] PROGMEM  = {
  "ANALOG 1",
  "ANALOG 2",
  "ANALOG 3",
  "DIGITAL 1",
  "DIGITAL 2",
  "DIGITAL 3"
};

const char tiltName[][15] PROGMEM = {
  "-3.0dB  +3.0dB",
  "-2.5dB  +2.5dB",
  "-2.0dB  +2.0dB",
  "-1.5dB  +1.5dB",
  "-1.0dB  +1.0dB",
  "-0.5dB  +0.5dB",
  "0.0dB  0.0dB",
  "+0.5dB  -0.5dB",
  "+1.0dB  -1.0dB",
  "+1.5dB  -1.5dB",
  "+2.0dB  -2.0dB",
  "+2.5dB  -2.5dB",
  "+3.0dB  -3.0dB"
};

const byte blendEQ[13][4] = {
  {0x01, 0x00, 0x00, 0x00},
  {0x00, 0xE3, 0xD7, 0x0A},
  {0x00, 0xCA, 0x3D, 0x71},
  {0x00, 0xB0, 0xA3, 0xD7},
  {0x00, 0x99, 0x99, 0x9A},
  {0x00, 0x82, 0x8F, 0x5C},
  {0x00, 0x6C, 0xCC, 0xCD},
  {0x00, 0x58, 0x51, 0xEC},
  {0x00, 0x45, 0x1E, 0xB8},
  {0x00, 0x31, 0xEB, 0x85},
  {0x00, 0x20, 0x00, 0x00},
  {0x00, 0x0F, 0x5C, 0x29},
  {0x00, 0x00, 0x00, 0x00},
};


// Gain variables 
float volume; // Volume level in dB.
float linGain; // Volume level as a linear gain value between 0 (no level) and 1 (full level).
uint32_t vol32; // Linear gain as a 32 bit word to populate hex array.
byte volArray[4]; // 4 byte array to hold volume hex values ready to send over I2C to the audio processor.

// Audio processor safeload addresses.
byte safeDataAddr0[2]; // 2-byte address to send first data word during a safe load.
byte safeDataAddr1[2]; // 2-byte address to send second data word during a safe load.
byte safeDataAddr2[2]; // 2-byte address to send third data word during a safe load.
byte safeDataAddr3[2]; // 2-byte address to send fourth data word during a safe load.
byte safeDataAddr4[2]; // 2-byte address to send fith data word during a safe load.
byte safeTargetAddr[2]; // 2-byte address to send target address during a safe load.
byte safeTrigAddr[2]; // 2-byte address to send number of words to write and trigger the safe load.
byte safeTrig1[4]; // Trigger safe load of 1 data word.

int dspAdd = 59; // Address of audio processor.
byte volAddress[4]; // 4-byte address of volume control.
unsigned long lastSafeWrite = -100; // Time at which the last safeload write was completed.

void setup() { 



  //Below addresses are fixed for safe load operations, do not change.
  safeDataAddr0[0] = 0x60;
  safeDataAddr0[1] = 0x00;
        
  safeDataAddr1[0] = 0x60;
  safeDataAddr1[1] = 0x01; 
     
  safeDataAddr2[0] = 0x60;
  safeDataAddr2[1] = 0x02;
        
 
  safeDataAddr3[0] = 0x60;
  safeDataAddr3[1] = 0x03;
 
  safeDataAddr4[0] = 0x60;
  safeDataAddr4[1] = 0x04;
 
  safeTargetAddr[0] = 0x60;
  safeTargetAddr[1] = 0x05;

  safeTrigAddr[0] = 0x60;
  safeTrigAddr[1] = 0x06;

  // Below variables are user configurable.
  volAddress[0] = 0x00; // Address of the volume module. Check in Sigma Studio.
  volAddress[1] = 0x00;
  volAddress[2] = 0x00;
  volAddress[3] = 0x19;
       

  safeTrig1[0] = 0x00; // Safeload trigger for 1 data word.
  safeTrig1[1] = 0x00;
  safeTrig1[2] = 0x00;
  safeTrig1[3] = 0x01;

    
  // Set encoder and push button pins as inputs.  
  pinMode (inputCLK,INPUT);
  pinMode (inputDT,INPUT);
  pinMode (buttonPin, INPUT_PULLUP);
  encCCW = 0; // Initial state for rotation flag not rotated.
  encCW = 0; // Initial state for rotation flag not rotated.
  previousStateCLK = digitalRead(inputCLK); // Read initial state of rotary encoder CLK pin. Assign to previousStateCLK so we can check for state changes.
   
  // Setup Serial Monitor
  Serial.begin (9600);

  // Setup I2C.
  Wire.begin(); // join i2c bus
  Wire.setClock(400000); // Set 400KHz frequency

  
  // Begin display. SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
    if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
        Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Don't proceed, loop forever
    }

  volume = -50; // Set default volume level to -50dB.    
  sendVolume(); // Send volume to audio processor.

 
  display.clearDisplay(); // Clear the display.
  sendDisplay(); // Update the display with initial settings.

  Serial.println(blendEQ[3][4], HEX);

} 

void loop() {

  // Rotary encoder code.
  currentStateCLK = digitalRead(inputCLK); // Read the current state of inputCLK pin.

  if (currentStateCLK != previousStateCLK) { // If the previous and the current state of inputCLK are different, then the encoder and been rotated.

    if (currentStateCLK != digitalRead(inputDT)) { // If the inputCLK state is different than the inputDT state then the rotation is counterclockwise.
      encCCW = 1; // Set rotation flag counter-clockwise.
    }

    else { // If inputCLK and inputDT are the same then encoder is not rotating CCW.
      encCW = 1; // Set rotation flag clockwise.
    }

  }

  // Button press code with debounce.
  currentButtonState = digitalRead (buttonPin); 

  if (currentButtonState != previousButtonState){ // If the button has changed state since the last loop.

    if (millis () - buttonPressTime >= debounceTime){ // If the current time minus the time when the switch was pressed (i.e. how long since the switch chagned state) is more than the debounce period. (This statement acts on the leading edge of a switch press, but rejects any state changes that happen within 10ms after that).
      buttonPressTime = millis ();  // Set switch press time = now.
      previousButtonState = currentButtonState;  // Save switch state for next loop.

      if (currentButtonState == LOW){ // When debounce criteria are met and button is still pressed.
        buttonPressed = 1; // Set button flag to 1.
      }

      else { // When debounce criteria are not met because boutton is not pressed or it is bouncing.
        buttonPressed = 0;  // Set button flag to 0.
      }
      
    }

  }

  // Limit Mode (menu page) and loop back to main menu.
  if (mode >= 4){ // If mode is more than 3.
   mode = 1; // Loop back to 1.
  }
   

  switch (mode) { // Each menu page is a case operated by the 'mode' variable.
   
    case 1: // Main menu page, volume control.
     
      if (encCCW == 1) { // If the rotary encoder has been rotated counter-clockwise.
        volume = volume - 2; // Decrement volume level by one. 

        if (volume <= -60){ // Limit volume level. If volume is less than -60dB.
          volume = -60; // Make it -60dB.
        }  

        sendVolume(); // Send volume over i2C.
        updateDisplay(); // Print to serial monitor.
        sendDisplay();
      }

      if (encCW == 1) { // If the rotary encoder has been rotated clockwise.
        volume = volume + 2; // Increment volume level by one.
      
        if (volume >= 0){ // Limit volume level. If volume is greater than 0dB.
         volume = 0; // Make volume 0dB.
        }   
        sendVolume(); // Send volume over i2C.
        updateDisplay(); // Print to serial monitor.
        sendDisplay();
      }

      if (buttonPressed == 1) { // If button is pressed.
        mode = mode + 1 ; // Progresses to the next page (case 2).
        updateDisplay();  // Print to serial monitor.
                sendDisplay();
      }

      buttonPressed = 0; // Reset button press for next loop.
    break;  // End of case 1.
  
      
    case 2:
     
      if (encCCW == 1) { // If the rotary encoder has been rotated counter-clockwise.
        newInputNumber -= 1; // subtract 1 from input number.
        
        if (newInputNumber < 0){ // Limit input selection. If input number is less than 1.
          newInputNumber = 5; // Loop back to 5.
        }  
        
        updateDisplay();  // Print to serial monitor.
        sendDisplay();
      }

      if (encCW == 1) { // If the rotary encoder has been rotated clockwise.
        newInputNumber += 1; // Add 1 to the input number. 

        if (newInputNumber > 5){ // Limit input selection. If input number is more than 5.
          newInputNumber = 0; // Loop back to 1.
        }

        updateDisplay();  // Print to serial monitor.
        sendDisplay();
      }

      if (buttonPressed == 1) { // If button is pressed.

        if (newInputNumber == currentInputNumber){ // Without changing input selection.
         mode = mode + 1 ; // Increment mode by 1 to progress to the next menu page.
         updateDisplay();  // Print to serial monitor.
         sendDisplay();
        }

        else { // A new audio input is being selected.
         currentInputNumber = newInputNumber; // Set the current audio input to the new selection.
         mode = 1; // Go back to the main menu page (case 1)
         updateDisplay();  // Print to serial monitor.
         sendDisplay();
        }

      }

      buttonPressed = 0; // Reset button press for next loop.
    break;  // End of case 2.

    
    case 3:
     
      if (encCCW == 1) { // If the rotary encoder has been rotated counter-clockwise.
        newTiltValue -= 1; // Subtract 1 from tilt value.

        if (newTiltValue < 0){ // Limit tilt value. If tilt value is less than 1.
          newTiltValue = 0; // Make it 1.
        }   

        updateDisplay();  // Print to serial monitor.
        sendDisplay();
        
      }

      if (encCW == 1) { // If the rotary encoder has been rotated clockwise.
        newTiltValue += 1; // Add 1 to the tilt value. 

        if (newTiltValue > 12){ // Limit tilt value. If tilt value is more than 9.
          newTiltValue = 12; // Make it 9.
        }

        updateDisplay();  // Print to serial monitor.
        sendDisplay();
      }

        if (buttonPressed == 1) { // If button is pressed.

            if (newTiltValue == currentTiltValue){ // Without changing tilt control value.
              mode = 1 ; // Go back to the main menu page, because this is the last menu.
              updateDisplay();  // Print to serial monitor.
              sendDisplay();
            }

            else{ // A new tilt value has been selected.
                currentTiltValue = newTiltValue; // Set the current tilt value to the new tilt value.
                mode = 1; // Go back to the main menu page (case 1).
                updateDisplay();  // Print to serial monitor.
                sendDisplay();
            }

        }

      buttonPressed = 0; // Reset button press for next loop.
   
    break; // End of case 3.

  } // End of switch.

  // Rotary encoder reset values for next loop.  
  previousStateCLK = currentStateCLK; // Update previousStateCLK with the current state of inputCLK.
  encCCW = 0; // Reset encoder counter-clockwise flag.
  encCW = 0; // Reset encoder clockwise flag.

} // end of loop.

void updateDisplay(){ // Display the menu mode, volume level and current audio input.

  Serial.print("Mode:");
  Serial.println(mode);
  Serial.print("Volume: ");
  Serial.print(volume);
  Serial.println("dB");
  Serial.print("Current Input Number:");
  Serial.println(currentInputNumber);
  Serial.print("New Input Number:");
  Serial.println(newInputNumber);
  Serial.print("Current Tilt Control Value:");
  Serial.println(currentTiltValue);
  Serial.print("New Tilt Control Value:");
  Serial.println(newTiltValue);
  Serial.println();
  
}

void sendVolume(){  // Convert dB volume level to hex array and send by I2C to the audio processor.
 
 // Convery dB level to hex array.
  linGain = pow(10, volume/20); // Convert dB level to linear gain value.
  vol32 = linGain * 16777216; // Convert linear gain value stored as a float variable to a 32 bit integer.
  volArray[0] = (vol32 >> 24) & 0xFF; // Populate first byte of array with most significant 8 bits of the 32 bit volume word, by shifting word right by 24 bits. Adding 0xFF denotes it as a hex value.
  volArray[1] = (vol32 >> 16) & 0xFF; // Populate second byte of array with next most significant 8 bits of the 32 bit volume word, by shifting word right by 16 bits. Adding 0xFF denotes it as a hex value.
  volArray[2] = (vol32 >> 8) & 0xFF; // Populate third byte of array with next most significant 8 bits of the 32 bit volume word, by shifting word right by 8 bits. Adding 0xFF denotes it as a hex value.
  volArray[3] = vol32 & 0xFF; // Populate fourth byte of array with least significant 8 bits of the 32 bit volume word, naturally the first 8 bits of the word. Adding 0xFF denotes it as a hex value.

  // Send hex array by I2C safeload proceedure to audio processor.
    if (millis () - lastSafeWrite >= 1){ // Safeload should only occur once per audio frame (1ms is pleanty). 
      Serial.println("Sending Volume Now");  
      Wire.beginTransmission(dspAdd); // Begin I2C transmission to 7-bit address of audio processor.
      Wire.write(safeDataAddr0, 2); // Prepare to write safe load data bank 1.
      Wire.write(volArray, 4); // Write hex array holding linear volume level.
      Wire.endTransmission(); // Send data queue and end transmission with stop bit.
      Wire.beginTransmission(dspAdd); // Begin I2C transmission to 7-bit address (0x3B) adds R/W bit automatically. 
      Wire.write(safeTargetAddr, 2); // Prepare to write target address.
      Wire.write(volAddress, 4); // Write aadress of volume control module.
      Wire.endTransmission(); // Send data queue and end transmission with stop bit.
      Wire.beginTransmission(dspAdd); // Begin I2C transmission to 7-bit address (0x3B) adds R/W bit automatically.
      Wire.write(safeTrigAddr, 2); // Prepare to writing number of data banks used (1-5) and trigger safe load.
      Wire.write(safeTrig1, 4); // Trigger safe load writing 1 data bank.
      Wire.endTransmission(); // Send data queue and end transmission with stop bit.
      lastSafeWrite = millis (); // Store time when the latest write occured.
    }
}

void sendDisplay(){

    int16_t x1, y1;
    uint16_t width, height;

  
  if (mode == 1) {
    display.setTextSize(2); // Draw 2X-scale text
    display.getTextBounds((__FlashStringHelper*)inputName[newInputNumber], 0, 0, &x1, &y1, &width, &height);
    
    display.clearDisplay(); // Clear the display.
    display.setTextSize(1); // Draw 1X-scale text
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(2, 0); // Position.
    display.print(F("CLIP   ")); // Text.
    display.print(F("DRC   ")); // Text.
    display.println(F("AMP 34c")); // Text.
    display.print(F("---------------------")); // Text.
    display.setTextSize(4); // Draw 4X-scale text
    display.setCursor(5, 17); // Position.
    display.print(volume, 0); // Text.
    display.print(F("dB"));
    display.setTextSize(2); // Draw 4X-scale text
    display.setCursor((SCREEN_WIDTH - width) / 2, 50);
    display.print((__FlashStringHelper*)inputName[newInputNumber]); // Text.
    display.display();      // Send to display.
  }

 if (mode == 2) {
    display.setTextSize(2); // Draw 2X-scale text
    display.getTextBounds((__FlashStringHelper*)inputName[newInputNumber], 0, 0, &x1, &y1, &width, &height);
    
    display.clearDisplay(); // Clear the display.
    display.setTextSize(1); // Draw 1X-scale text
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(2, 0); // Position.
    display.print(F("CLIP   ")); // Text.
    display.print(F("DRC   ")); // Text.
    display.println(F("AMP 34c")); // Text.
    display.print(F("---------------------")); // Text.
    display.setTextSize(3); // Draw 3X-scale text
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(20, 20); // Position.
    display.println(F("INPUT")); // Text.
    display.setTextSize(2); // Draw 2X-scale text
    display.setCursor((SCREEN_WIDTH - width) / 2, 50);
    display.print((__FlashStringHelper*)inputName[newInputNumber]); // Text.
    display.display();      // Send to display.
  }

if (mode == 3) {
    display.setTextSize(1); // Draw 2X-scale text
    display.getTextBounds((__FlashStringHelper*)tiltName[newTiltValue], 0, 0, &x1, &y1, &width, &height);
   
    display.clearDisplay(); // Clear the display.
    display.setTextSize(1); // Draw 1X-scale text
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(2, 0); // Position.
    display.print(F("CLIP   ")); // Text.
    display.print(F("DRC   ")); // Text.
    display.println(F("AMP 34c")); // Text.
    display.print(F("---------------------")); // Text.
    display.setTextSize(2); // Draw 2X-scale text
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(10, 17); // Position.
    display.println(F("Tilt Tone")); // Text.
    display.setTextSize(1); // Draw 1X-scale text
    display.setCursor(20, 40);
    display.print(F("LOWS  --  HIGHS")); // Text.
    display.setTextSize(1); // Draw 2X-scale text
    display.setCursor((SCREEN_WIDTH - width) / 2, 55);
    display.print((__FlashStringHelper*)tiltName[newTiltValue]); // Text.
    display.display();      // Send to display.
  }
 
}

Hi
There is no element 4 in the array.
Serial.println (blendEQ[3][4], HEX);

The matrix is 13 rows by 4 columns.
Rows range from 0 to 12 and columns from 0 to 3.

Serial.println (blendEQ[3][3], HEX); correct form

Thanks, that was a silly mistake!

Is there an easy way to call all the data in one row?

Use a loop of some sort, but it depends on what you mean by "call".

I've seen people use a for loop like:

for (int i = 0; i < column; ++i)

But since I only have 4 columns it hardly seems worth it. I thought there might be something equivilant to

Serial.println (blendEQ[3][0-3], HEX);

Where 0-3 means 0 to 3, rather than 0 minus 3. I'm aware this does not work but I think you will get me.

So write your own function to do it. Pass a two dimensional array, the column number, and the dimensions of the array. Or be cheesy and hard code the dimensions.

No, not in the C/C++ language.

store hex values

You are storing binary values. Hex is a human readable representation of a binary value.

1 Like

The alternative to that loop is:

Serial.println (blendEQ[3][0], HEX);
Serial.println (blendEQ[3][1], HEX);
Serial.println (blendEQ[3][2], HEX);
Serial.println (blendEQ[3][3], HEX);

And if you want multiple rows, not just the fourth row, you need another loop:

  for (int row = 0; row < 13; row++)
    for (int col = 0; col < 4; col++)
      Serial.println (blendEQ[row][col], HEX);

if the loops don't seem 'worth it' you can always write out all 52 println statements instead of three lines.

1 Like

Thanks, what you wrote to start is the way I went. Row is driven by a user controlled variable, then I just wrote out a separate line for each of the 4 elements.

Side note: I tried to use PROGMEM for this array and then call with (__flashStringHelper*) to call the row and column but it complained about casting types (sorry on my phone don't remember exact error).

I guess the issue is in the name - string helper. Is there a similarly easy way to call my byte array values from progmem?

(__FlashStringHelper *) only works when printing strings in PROGMEM. To read a byte variable from a 2D array in PROGMEM, use:
Serial.println (pgm_read_byte(&blendEQ[row][col]), HEX);
The 'pgm_read_*()' functions read from PROGMEM and the '&' is the "address of" operator.

1 Like

Thanks again!

Does PROGMEM work the same when using an ESP8622 or ESP32 with Arduino framework?

The ESP32 and ESP8266 don't have a separate address space for FLASH. The Arduino cores for them are written to ignore "PROGMEM" and define the various 'pgm_read_...()' functions to just use the address directly. For example "pgm_read_byte(x)" probably converts to something like (*(byte *)x) which means "use the address as a byte pointer and fetch a byte".

1 Like

Great info thanks. I'm not sure I fully understand though. The ESP can of course write and read it's own FLASH (I've heard about SPIFFS for example) and the dynamic memory can presumably be utilised however a programmer wants. So what is stopping a space in dynamic memory being a pointer to a FLASH address?

Is it a matter of no existing function to do it in the ESP library, maybe since the dynamic memory is already generous?

I am building this on Uno as I feel more comfortable with it, but I plan to move to an ESP once it's all running (or maybe before, since I'm running out of dynamic memory!).

On the AVR, RAM, FLASH, and EEPROM are three separate address spaces. The "PROGMEM" keyword tells the compiler that the data goes into FLASH but the compiler doesn't keep track of which addresses are in RAM and which are in FLASH. That is why you have to keep track yourself and use the "pgm_read_...()" functions if the address is in FLASH.

On the ESP32 and ESP8266 the RAM and FLASH are in the same address space. The compiler decides where to put your data: variables in RAM and constants in FLASH. Your program doesn't need to know which is where.

1 Like

Awesome! I did wonder why the Uno compiler doesn't simply put all const variables in FLASH.

I actually thought FLASH is EEPROM - the place where the binary program is stored. When you mention EEPROM are you referring to an external EEPROM IC on the SPI or I2C lines?

No. The AVR chips have a small amount of EEPROM in its own address space. Use the built-in EEPROM library to read and write bytes or get and put variables.

I'm struggling to understand the need of two types of non-volatile memory.

I've read a few bits about EEPROM and FLASH so I understand the program bootloader and program binary are in flash and things like a previous sensor state would be saved to EEPROM to recover after power reset.

I don't however understand why we do not simply save previous the sensor state or similar in FLASH?

On an UNO, Nano, Mega, Leonardo... the FLASH memory can only be written by the bootloader partition. That helps to protect the sketch from corrupting itself.

Note: The FLASH is good for 10,000 write cycles but the EEPROM is good for 100,000 write cycles. Another reason not to use FLASH for non-volatile read/write memory.

Wait.. what does PROGMEM and FlashStringHelper do, then?

On the AVR processor the SRAM, FLASH, and EEPROM are in separate address spaces. Instructions are fetched from FLASH (a.k.a. Program Memory or PROGMEM). Data is read from and written to SRAM. You have to use special machine instructions for reading (or writing) FLASH or reading or writing EEPROM. Any initialized variables will be initialized by copying the data from FLASH to the variable's location in SRAM before the sketch starts. This is great for initialized variables that will be modified by the sketch but for constant (read-only) variables it is a waste of precious SRAM space. For larger initialized read-only variables, such as lookup tables, it is good to tell the compiler to keep the data in FLASH. . (Note: The special instructions for writing into FLASH can only be executed from the BOOTLOADER area of the FLASH memory. Your sketch can't write into FLASH.)

Unfortunately, the compiler will only keep track of the address of the variable, not which address space it is in. It won't remember that your variable is in FLASH and if you try to use it like you would any other variable it will use that FLASH address to fetch data from SRAM and get the wrong data. It is up to you to add the special function calls to fetch data from FLASH whenever you want to fetch data from that variable.

String literals are a special case of initialized variables. They are treated like initialized read-only variables and their space in SRAM is initialized by copying the data from FLASH. Because they are often used for text output to Serial, LCD, Ethernet, WiFi, etc. there is a special trick used to make keeping them out of SRAM easier.

There is a macro named "F" in the standard include file Wstring.h:

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

The F() macro uses the macro PSTR() to tell the compiler to keep the string in FLASH and then changes the value type from 'const char *' (pointer to const character) to a 'const __FlashStringHelper *'. The class __FlashStringHelper has no body, just a type. The "reinterpret_cast" tells the compiler that you know that the value being cast is not compatible with the destination type. Unlike a regular cast, no conversion is done. The ONLY safe operation is to cast the value BACK to what it was before.

In the Print class there are .print() and .println() methods similar to the 'const char *' methods that accept a 'const __FlashStringHelper *' instead. The compiler chooses those methods when you pass the '__FlashStringHelper *' created by the F() macro. The methods then cast the __FlashStringHelper pointer BACk to a character pointer and fetch each character of the string from FLASH. Serial, LCD, Ethernet, WiFi, etc are all objects that inherit behavior from the Print class so their .print() and .println() methods can all accept the 'const __FlashStringHelper *' type and fetch the string from the FLASH address space.

If you write your own function and one of the arguments is often a fairly large string literal you might want to make a version of the function that accepts the 'const __FlashStringHelper *' type. Then you can use the F() macro to keep those string literals in FLASH. See Print.cpp in the Arduino core sources for examples of how you would treat the argument differently.

Sadly, you can't use the F() macro to initialize a global pointer. The PSTR() macro it uses has to be inside a function to work.

1 Like