Understanding pointers....

Can anyone help me to understand the usage of the pointers in the following code for Saving and Loading EEPROM data.

I genuinely can't remember where I got it from, but I use it in many of my projects, so want to understand it...

// Save settings to EEPROM
void settingsSAVE() {
  byte EEPROMAddress = SETTINGS_ADDR;
  const byte* pointer = (const byte*)(const void*)&settings;
  for (unsigned int i = 0; i < sizeof(settings); i++) EEPROM.write(EEPROMAddress++, *pointer++);
}

// Load settings from EEPROM
void settingsLOAD() {
  byte EEPROMAddress = SETTINGS_ADDR;
  byte* pointer = (byte*)(void*)&settings;
  for (unsigned int i = 0; i < sizeof(settings); i++) *pointer++ = EEPROM.read(EEPROMAddress++);
}

My questions are straightforward, hopefully the answers will be too.

  1. Why does settingsSAVE() use const for the data pointer, but settingsLOAD does not ?

  2. I know the * means "de-reference operator", but what does that actually mean ?

  3. What does (byte*)(void*)&settings do, especially the use of (void*) ?

Any help to make me appreciate this would be awesome, TIA

  1. It does not matter wheter const is used or not. It is probably added to show the programmer that the contents is not being modified.

  2. A reference is a variable / pointer which is pointing at some data / type. When you de-reference, you are accessing the contents of that variable / pointer. Eg. "int a = 10" - "a" is the reference, "10" is the de-reference.

  3. You could probably remove the (void*) cast without any problems. Usually is is used to tell the compiler that we want an "ambiguous reference" or an un-typed reference to something.

Thanks for your reply Danois90, may I go a little further....

Danois90:

  1. It does not matter wheter const is used or not. It is probably added to show the programmer that the contents is not being modified.

But the value of pointer is being modified in the "for" statement...

Danois90:
2) A reference is a variable / pointer which is pointing at some data / type. When you de-reference, you are accessing the contents of that variable / pointer. Eg. "int a = 10" - "a" is the reference, "10" is the de-reference.

So, if pointer is, say, 25, then *pointer returns the value at address 25, rather than "25"

Danois90:
3) You could probably remove the (void*) cast without any problems. Usually is is used to tell the compiler that we want an "ambiguous reference" or an un-typed reference to something.

I'll read up on "casting" ....

  1. "const" is bound to whatever is to the right of it, with "const byte* data" it is "byte*" which is const (the data it points to) and with "byte* const data" it is "data" which is const (the pointer itself) - in both cases the variable is of type "byte*", though.

  2. A pointer is always an address in memory defined by a number or a descriptor indicating where the data it references is stored.

I would be interested in knowing how the settings variable is declared. It looks like it is an array of bytes and if so there is a better way to do what you want.

But the value of pointer is being modified in the "for" statement

Not the value, the contents.

So, if pointer is, say, 25, then *pointer returns the value at address 25, rather than "25"

Correct.

So when you combine those two thoughts...

Take a typical example which is an array. You define an array such as byte array[10];
The array name, in this case array, is by design a pointer to the base address of the array which might be 0x100. If you print array, you'll get 0x100. If the array is populated and say array[0] = 25 and you print the contents of array[0] you'll get 25. Also though, if you de-reference the base address and print *array you'll get 25 as well. If you increment *array+=1, array[0] is now = 26. If however, you increment array+1, you are now pointing to 0x101 and the memory address of array[1].

UKHeliBob:
I would be interested in knowing how the settings variable is declared. It looks like it is an array of bytes and if so there is a better way to do what you want.

Would make more sense if "settings" is a struct.

UKHeliBob:
I would be interested in knowing how the settings variable is declared. It looks like it is an array of bytes and if so there is a better way to do what you want.

EDIT : I should have said so before, I know, sorry.....

"settings" is always a "struct" when I use the Save and Load code above.

It is for storing stuff that I want to "restore" on power-on.

A typical structure is thus....

typedef struct udt_Settings {
  byte          currentMode;
  byte          lastMode;
  unsigned int  NumTracks;           // we will read this from the DFPlayer
  byte          bass;                // for the PT Module
  byte          treble;              // for the PT module
  byte          HBTY_Vol;            // HBTY Mode volume setting : 0-63
  byte          SKY_Vol;             // PT2314 channel 1
  byte          AUX_Vol;             // PT2314 channel 2
  byte          MP3_Vol;             // PT2314 channel 3
  byte          BT_Vol;              // PT2314 channel 4
  char          id[19];              // this is last to catch structure changes
};

If you don't mind a bit of a read, have a look through the attached.

pointers (2).pdf (184 KB)

It would be neat if the Arduino IDE, or some compiler directive, could automatically change the settings.id, instead of me having to do it manually. An incrementing number would do it....

That way I can guarantee that my code detects changes to the structure after uploading, and my sketch presets the EEPROM "variables".

Also I have read {somewhere} that EEPROM.write() uses the EEPROM.update() method to reduce the number of write cycles. Whether that is true or not I have changed it in my sketch to .update(), it can't hurt can it ?

Not sure if I understand you, but if you want to detect changes to the settings struct during runtime in order to prevent an unchanged struct to be written, this is how I would do it:

struct SETTINGS {
...
} settings;

byte prevSettings[sizeof(settings)];

// Save settings to EEPROM
void settingsSAVE() {
  //Check for changes, return is none is detected
  if (memcmp(prevSettings, &settings, sizeof(settings)) == 0) return;

  //Settings changed, save to eeprom and copy to prevSettings
  memcpy(prevSettings, &settings, sizeof(settings));
  

  byte EEPROMAddress = SETTINGS_ADDR;
  const byte* pointer = (const byte*)(const void*)&settings;
  for (unsigned int i = 0; i < sizeof(settings); i++) EEPROM.write(EEPROMAddress++, *pointer++);
}

// Load settings from EEPROM
void settingsLOAD() {
  byte EEPROMAddress = SETTINGS_ADDR;
  byte* pointer = (byte*)(void*)&settings;
  for (unsigned int i = 0; i < sizeof(settings); i++) *pointer++ = EEPROM.read(EEPROMAddress++);

  //Copy settings to prevSettings to enable change detection
  memcpy(prevSettings, &settings, sizeof(settings));
}

YMMV.

EDIT: Or simply declare a "bool settingsChanged" and set it to true whenever settings are modified, or use "setters" in the settings struct which automatically sets a bool to true.

"settings" is always a "struct" when I use the Save and Load code above.

Even more reason to use the EEPROM.put() and EEPROM.get() functions and save/load the whole struct with just one command

You could also further improve the save method by doing a byte-by-byte compare on the fly:

// Save settings to EEPROM
void settingsSAVE() {
  const byte* curSettings = (const byte*)(const void*)&settings;
  for (unsigned int i = 0; i < sizeof(settings); i++)
  {
    if (prevSettings[i] != curSettings[i])
    {
      EEPROM.write(SETTINGS_ADDR + i, curSettings[i]);
      prevSettings[i] = curSettings[i];
    }
  }
}

Danois90:
You could also further improve the save method by doing a byte-by-byte compare on the fly:

EEPROM.put() does that for you (through EEPROM.update()) :wink:

UKHeliBob:
Even more reason to use the EEPROM.put() and EEPROM.get() functions and save/load the whole struct with just one command

So instead of the above code (which I did steal from elsewhere), I could just...

// Save settings to EEPROM
void settingsSAVE() {
  EEPROM.put(SETTINGS_ADDR, settings);
}

doesn't need to be a void function then, does it, wherever I am calling settingsSAVE() I can just use EEPROM.put, as above.

The reason I want to detect a structure change (which I can force by changing the id at compile time) is if I have deliberately changed the members of the structure (by adding or deleting variables), or more critically, if I use a virgin chip.

In the first case the byte alignment will be incorrect, and in the second case all the variables in the structure will be un-initialised. I detect the structure change if the id is not the same in the EEPROM as the id in RAM, and run a routine to set initial starting values. The values will get overwritten as the sketch does its job, so I want those new values to be used next power-up, instead of initialising them every time.

I have a #define SETTINGS_ID "HBTY_nnn" at the head of my sketch, and I increment the nnn when I want to force an EEPROM initialisation, which I do with...

settingsLOAD();                         // retrieve settings from EEPROM

// initialise start-up settings if not present
if (strcmp(settings.id, SETTINGS_ID) != 0) {
  message = "Virgin Settings";
  strcpy(settings.id, SETTINGS_ID);
  settings.NumTracks = 20;              // we will read this from the DFPlayer
  settings.bass = 9;                    // mid bass
  settings.treble = 11;                 // mid treble
  settings.HBTY_Vol = 50;               // MP3 HBTY Mode volume setting : 0-63
  settings.SKY_Vol = 45;                // PT2314 channel 1
  settings.AUX_Vol = 45;                // PT2314 channel 2
  settings.MP3_Vol = 45;                // PT2314 channel 3
  settings.BT_Vol = 45;                 // PT2314 channel 4
  settings.currentMode = MODE_SKY;      // need this, otherwise we don't have a mode...

  EEPROM.put(SETTINGS_ADDR, settings);  // write the data to EEPROM
}

doesn't need to be a void function then, does it, wherever I am calling settingsSAVE() I can just use EEPROM.put, as above.

That's right, but the advantage of putting it in a function is that you can give the function a meaningful name and perhaps call it from more than one place in the program if that is helpful

UKHeliBob:
That's right, but the advantage of putting it in a function is that you can give the function a meaningful name and perhaps call it from more than one place in the program if that is helpful

That's true, but the one-liner "EEPROM.put(SETTINGS_ADDR, settings);" carries as much meaning as it needs to, and can also be put anywhere in the program.

I've converted all my "settingsSAVE();" calls to "EEPROM.put", and it has barely made any difference in compiled size. Not sure if it gone up, or down, to be honest, but insignificant anyway.