Structs, arrays and EEPROM. Oh my.

Programming my first large project, a RC transmitter.

Each channel has a host of settings that need to survive a reboot so need to be saved to EEPROM.

On researching this I found EEPROM.get() and .put() and their ability to save from and read to data structures. My program already had a couple of structures for the transmission and receive packets so I made some more for all the channel settings.

As my program has expanded there has been a couple of times where being able to call a function whilst passing an index for the exact channel I am referencing would save some code.

Everything I know about coding has just been picked up from trial and error so I have no idea what is considered the best or proper method.

Should I just keep the struct or should I look into storing the settings in an array? I'm thinking a 2D array where each row is a different setting and each column is a different channel?

If the array is the better option could somebody point me in the direction of the best way to write and read an array to and from the EEPROM?

Or is there another option that is more suited? I have copied the structure below so you can see what setting and data types I am using.

struct AnalogCHConfig {
  int LowerCali;
  int NeturalCali;
  int HighCali;
  byte LowEP;
  byte HighEP;
  int Trim;
  boolean Reversed;
};

Why not make an array of struct where each struct is for one channel?

#define NUM_CHANNELS;

ChannelConfig chcfg[NUM_CHANNELS];

void config_eeprom(bool load)
{
  unsigned int addres = 0;
  for (i = 0; i < NUM_CHANELS; i+)
  {
    if (load) EEPROM.get(address, chcfg[i]);
    else EEPROM.put(address, chcfg[i]);
    address += sizeof(ChannelConfig);
  }
}

Bugs are for free today! :slight_smile:

How about an array of structs which allows data of different types to be kept together whilst having the convenience of an array ?

Saving each to EEPROM is easy. Saving the array of them goes like this :

for (int x = 0; x < NUMBER_OF_STRUCTS; x++)
  {
    EEPROM.put(x * STRUCT_SIZE_IN_BYTES, structName[x]);
  }

and loading them back is just as easy

for (int x = 0; x < NUMBER_OF_STRUCTS; x++)
  {
    EEPROM.get(x * STRUCT_SIZE_IN_BYTES, structName[x]);
  }

Keep the struct - having the names of the fields will be clearer. You can have an array of struct, one per channel.

I don't think you can write an array to EEPROM in one go although you can write a struct. So you can either use EEPROM.put on each element of the array in a loop or you can just treat the array as an array of bytes and do it with EEPROM.update. The latter is probably (marginally) simpler.

Edit: Too slow!

Learn something new everyday, didn't realize I could have an array of structs.

Another related question, when the end user changes a setting what would be the best way to handle saying the new setting to EEPROM?

The way I see it I could have some code to work out the correct address and just save the updated setting/entire channel struct or I could just have a function to update the entire contents of the EEPROM.

I know the EEPROM.put() uses the update method which means it wouldn't overwrite a memory byte is the data hasn't changed so theoretically I could save all data every time without using up any of my finite write's but writing to the EEPROM must take an amount of time and once every thing is in place i will be saving 200-300 bytes.

Would the shotgun approach of updating every thing be OK or should I be more precise and just save the single updated struct?

Thanks.

Use the shotgun initially. If it turns out to be too slow for your needs, go for precision.

GForce2010:
I know the EEPROM.put() uses the update method which means it wouldn't overwrite a memory byte is the data hasn't changed so theoretically I could save all data every time without using up any of my finite write's but writing to the EEPROM must take an amount of time and once every thing is in place i will be saving 200-300 bytes.

The update method would involve a read and compare with data to be written. Since it doesn't perform a write if they are the same, there will be no write overhead for that item. Consequently, if you write an array that has only one difference, it will happen much, much faster than if you had to write all of them. An EEPROM read is not very slow.

Apologies, I should have said in my first post that I am using two types are structs to store my channel setting. I have two analog channels that use pots but the rest are digital only so have less settings.

Can I still make a single array using the two different structs? If so how would i initialize this as all the examples I find are for a single struct type?

If not would I be better off by having two arrays, one for analog and one for digital, or creating a single struct type that could be used for both and just zero out the settings I don't need for a certain channel. Though thinking about it now that last option would mean I would have a fair bit of wasted space in RAM and EEPROM.

Thanks.

//Data structure for Analog channels
struct AnalogCHConfig {
  int LowerCali;
  int NeturalCali;
  int HighCali;
  byte LowEP;
  byte HighEP;
  int Trim;
  boolean Reversed;
};

//Data structure for Digital channels
struct DigitalCHConfig {
  byte LowEP;
  byte HighEP;
  int Trim;
  boolean Reversed;
};

This is an old code i wrote when i was trying to figure out how to save structs to the EEPROM, i never worked out how to save arrays without listing every cell in the put() and get() but it will at least help you understand the basics of structs and EEPROM. And sure you can have an arrays of structs, and even an array of structs which point to other structures like i do with font engines.

#include <EEPROM.h>

byte EEPROM_key = 101; // change this key if you adjust the structure of the EEPROM ! i.e 102
// simple variables to store
bool test_1 = true;
char test_2 = '!';
byte test_3 = 101;
int test_4 = 1234;
long test_5 = 1234567;
float test_6 = 21.43;
// arrays to store
bool test_array_1[5] = {true, false, true, false, true};
char test_array_2[5] = {'T', 'e', 's', 't', '1'};
byte test_array_3[5] = {101, 102, 103, 104, 105};
int test_array_4[5] = {1234, 1235, 1236, 1237, 1238};
long test_array_5[5] = {1234567, 1234568, 1234569, 1234570, 1234571};
float test_array_6[5] = {21.43, 21.44, 21.45, 21.46, 21.47};


struct EEPROMstructure {
  bool EEPROM_test_1;
  char EEPROM_test_2;
  byte EEPROM_test_3;
  int EEPROM_test_4;
  long EEPROM_test_5;
  float EEPROM_test_6;
  // arrays
  bool EEPROM_test_array_1[5];
  char EEPROM_test_array_2[5];
  byte EEPROM_test_array_3[5];
  int EEPROM_test_array_4[5];
  long EEPROM_test_array_5[5];
  float EEPROM_test_array_6[5];
};

void setup() {
  Serial.begin(115200);
  Serial.println("EEPROM structure example v1");
  readEEPROM();
  printvalues(); // show retrived EEPROM data
}

void loop() {
  // nothing going on here
}

void readEEPROM(void) {
  if (EEPROM_key == EEPROM.read(0)) {
    retriveEEPROM();
  }
  else {
    Serial.println(F("EEPROM was blank or the structure changed, but is now corrected"));
    EEPROM.put(0, EEPROM_key);
    writeEEPROM();
  }
}

void writeEEPROM(void) {
  Serial.println(F("wrote variables to eeprom"));
  EEPROMstructure writeEEPROM = {
    test_1,
    test_2,
    test_3,
    test_4,
    test_5,
    test_6,
    test_array_1[0], // array 1
    test_array_1[1],
    test_array_1[2],
    test_array_1[3],
    test_array_1[4],
    test_array_2[0], // array 2
    test_array_2[1],
    test_array_2[2],
    test_array_2[3],
    test_array_2[4],
    test_array_3[0], // array 3
    test_array_3[1],
    test_array_3[2],
    test_array_3[3],
    test_array_3[4],
    test_array_4[0], // array 4
    test_array_4[1],
    test_array_4[2],
    test_array_4[3],
    test_array_4[4],
    test_array_5[0], // array 5
    test_array_5[1],
    test_array_5[2],
    test_array_5[3],
    test_array_5[4],
    test_array_6[0], // array 6
    test_array_6[1],
    test_array_6[2],
    test_array_6[3],
    test_array_6[4],
  };
  EEPROM.put(1, writeEEPROM);
  delay(250);
}

void retriveEEPROM(void) {
  Serial.println(F("retriving EEPROM variables"));
  EEPROMstructure retriveEEPROM;
  EEPROM.get(1, retriveEEPROM);
  delay(250);
  test_1 = retriveEEPROM.EEPROM_test_1;
  test_2 = retriveEEPROM.EEPROM_test_2;
  test_3 = retriveEEPROM.EEPROM_test_3;
  test_4 = retriveEEPROM.EEPROM_test_4;
  test_5 = retriveEEPROM.EEPROM_test_5;
  test_6 = retriveEEPROM.EEPROM_test_6;
  test_array_1[0] = retriveEEPROM.EEPROM_test_array_1[0]; // array 1
  test_array_1[1] = retriveEEPROM.EEPROM_test_array_1[1];
  test_array_1[2] = retriveEEPROM.EEPROM_test_array_1[2];
  test_array_1[3] = retriveEEPROM.EEPROM_test_array_1[3];
  test_array_1[4] = retriveEEPROM.EEPROM_test_array_1[4];
  test_array_2[0] = retriveEEPROM.EEPROM_test_array_2[0]; // array 2
  test_array_2[1] = retriveEEPROM.EEPROM_test_array_2[1];
  test_array_2[2] = retriveEEPROM.EEPROM_test_array_2[2];
  test_array_2[3] = retriveEEPROM.EEPROM_test_array_2[3];
  test_array_2[4] = retriveEEPROM.EEPROM_test_array_2[4];
  test_array_3[0] = retriveEEPROM.EEPROM_test_array_3[0]; // array 3
  test_array_3[1] = retriveEEPROM.EEPROM_test_array_3[1];
  test_array_3[2] = retriveEEPROM.EEPROM_test_array_3[2];
  test_array_3[3] = retriveEEPROM.EEPROM_test_array_3[3];
  test_array_3[4] = retriveEEPROM.EEPROM_test_array_3[4];
  test_array_4[0] = retriveEEPROM.EEPROM_test_array_4[0]; // array 4
  test_array_4[1] = retriveEEPROM.EEPROM_test_array_4[1];
  test_array_4[2] = retriveEEPROM.EEPROM_test_array_4[2];
  test_array_4[3] = retriveEEPROM.EEPROM_test_array_4[3];
  test_array_4[4] = retriveEEPROM.EEPROM_test_array_4[4];
  test_array_5[0] = retriveEEPROM.EEPROM_test_array_5[0]; // array 5
  test_array_5[1] = retriveEEPROM.EEPROM_test_array_5[1];
  test_array_5[2] = retriveEEPROM.EEPROM_test_array_5[2];
  test_array_5[3] = retriveEEPROM.EEPROM_test_array_5[3];
  test_array_5[4] = retriveEEPROM.EEPROM_test_array_5[4];
  test_array_6[0] = retriveEEPROM.EEPROM_test_array_6[0]; // array 6
  test_array_6[1] = retriveEEPROM.EEPROM_test_array_6[1];
  test_array_6[2] = retriveEEPROM.EEPROM_test_array_6[2];
  test_array_6[3] = retriveEEPROM.EEPROM_test_array_6[3];
  test_array_6[4] = retriveEEPROM.EEPROM_test_array_6[4];
}

void printvalues() {
  Serial.print(F("test_1 = ")); Serial.println(test_1);
  Serial.print(F("test_2 = ")); Serial.println(test_2);
  Serial.print(F("test_3 = ")); Serial.println(test_3);
  Serial.print(F("test_4 = ")); Serial.println(test_4);
  Serial.print(F("test_5 = ")); Serial.println(test_5);
  Serial.print(F("test_6 = ")); Serial.println(test_6);
  for (bool val : test_array_1) {val == true ? Serial.print(F("true, ")) : Serial.print(F("false, "));} Serial.println();
  for (char val : test_array_2) {Serial.print(val); Serial.print(F(", "));} Serial.println();
  for (byte val : test_array_3) {Serial.print(val); Serial.print(F(", "));} Serial.println();
  for (int val : test_array_4) {Serial.print(val); Serial.print(F(", "));} Serial.println();
  for (long val : test_array_5) {Serial.print(val); Serial.print(F(", "));} Serial.println();
  for (float val : test_array_6) {Serial.print(val); Serial.print(F(", "));} Serial.println();
}

All the elements of any type of array must be the same data type.

Use the same struct with all of the possible values in it for both types of channel. Either ignore the values that don't matter. Add a channel type data item to the struct and use that when processing the data to determine which type it is

GForce2010:
Can I still make a single array using the two different structs?

Kind of, although each array element will still be (wastefully) occupying the size of the larger struct: look at C unions.

The easiest solution is as UKHeliBob suggests. Whether you can afford the wasted EEPROM is for you to decide and of course depends on how many channels you anticipate needing. The two array solution is obviously slightly more complex and you would probably want to store the digital and analog channel counts in EEPROM too, so you can figure out where the arrays have been saved.

As a matter of interest, which Arduino board are you using ?

As this is an RC transmitter you will have a fair amount of space to accommodate the Arduino. Have you considered storing the data on an SD card ? It would give you almost limitless storage space, the ability to check and revise the settings on a PC and to back it up easily.

UKHeliBob:
As a matter of interest, which Arduino board are you using ?

As this is an RC transmitter you will have a fair amount of space to accommodate the Arduino. Have you considered storing the data on an SD card ? It would give you almost limitless storage space, the ability to check and revise the settings on a PC and to back it up easily.

I'm using an Pro Mini.

I did look at using an SD card during the planning stage but it seems a bot overkill.

If I were to go with a single struct I would have, in this current version, 98 Bytes of memory assigned but not actually being used.

Hmm. My TX has some buttons on the grip that I want to be able to configure through the UI so they would also need to be saved. I could reorganize my struct in a way where i use the unused space in channel 3 to store the mapping data for button 1 etc.

It all seems to come down to how many channels there will be. You only have 2K of RAM, and some of it will be used for other purposes so it may well be that 1K of EEPROM will hold all the channels you can actually handle.

I'd be inclined to figure out the likely feasible vs. desired channel count before trying to pack the data in tighter.

As to packing the button settings into unused struct fields, I would avoid that until I was forced to do it. It would make your code more cryptic and might well confuse you when you come back to it in six months.

I could reorganize my struct in a way where i use the unused space in channel 3 to store the mapping data for button 1 etc.

Probably not a good idea as it will make programming more difficult

Don't forget that 2 position switch/button states can be saved in 1 bit and even 3 position switch states can be save using 2 bits. This means that you could save 8 2 position or 4 3 position switch states in 1 byte

How many switches and of what type (2 or 3 position) do you have on the transmitter ?

wildbill:
I'd be inclined to figure out the likely feasible vs. desired channel count before trying to pack the data in tighter.

It is 16 channels, my RX is using a 16 channel PWM driver. I cannot envision a situation where I would need anymore channels from this system.

UKHeliBob:
How many switches and of what type (2 or 3 position) do you have on the transmitter ?

There are 5 tactile buttons. The settings I would be saving would be what channel each one triggers, is it going to be a momentary or latching signal and what range of motion I want.

So assuming I deal with the button mapping data separately I seem to have a couple of options.

  1. Do nothing and stay with separate struts that I have now.
  2. Create a single struct to deal with all 16 channels. This would greatly simplify a some of my functions at the cost of having approx. 100 bytes of RAM and EEPROM being wasted.
  3. Have one array for the 2 analog structs and 1 array for the 14 digital channels. Gets rid of that chunk of wasted memory and still allows me to simplify some functions to a point.

3 seems to be a good compromise as I would just need to add a single if statement to check to see which array I need to be accessing.

With that channel count, you have plenty of EEPROM, so I wouldn't worry about wasting it. It's obviously important to be cognizant of your RAM usage on a Pro mini, given that there's so little of it.

However, there's a lot to be said for code simplicity too. I suggest that you try option two to start with. You can always change course later if that 100 bytes becomes important.

To help ensure that it doesn't, make use of the F macro when you're printing text anywhere.

wildbill:
With that channel count, you have plenty of EEPROM, so I wouldn't worry about wasting it. It's obviously important to be cognizant of your RAM usage on a Pro mini, given that there's so little of it.

However, there's a lot to be said for code simplicity too. I suggest that you try option two to start with. You can always change course later if that 100 bytes becomes important.

To help ensure that it doesn't, make use of the F macro when you're printing text anywhere.

I do have a habit of getting fixated on small issues like this so you're probably right.

Ironically I only just learnt about the F macro yesterday.

Adding the F to all my debug code just saved over 20% of RAM. Not that any of my debug code will be left active when I'm finished but it does put into perspective how small the 100 bytes I was worrying about is.

Thanks for all your helo.

You do not need to have two types of struct since they share vital parts:

struct ChannelConfig {
  bool IsAnalog; //true if analog, false if digital
  int LowerCali; //Used only for analog
  int NeturalCali; //Used only for analog
  int HighCali; //Used only for analog
  byte LowEP; //Used for both analog and digital
  byte HighEP; //Used for both analog and digital
  int Trim; //Used for both analog and digital
  boolean Reversed; //Used for both analog and digital
};

Doing it this way may consume slightly more memory for the data wasted by digital channels, but it would simplify the code and I/O from EEPROM. Depending on the values stored in each data type, you may be able to optimize the struct with bitfields.