In a typical 8-bit microcontroller application with real EEPROM, I used to have to read EEPROM byte by byte to obtain values I want and store them into variables in the world of PIC16/18s (how awful they are). Or in the case of AVRs, I had the nice helper functions provided by <avr/eeprom.h>
Now the SAMD with it’s emulated EEPROM ASF driver, things suddenly get more interesting. Now when you read the “EEPROM”, you get 60 bytes back. Wow, all that in one read and now I have to step through bytes to create my variables?
Hell no, now I can use the power of structs, memcpy and magic!
My new usage of emulated EEPROM is to store structs directly to/from the emulated EEPROM pages. The only risk is if we create a struct greater than the page size but there’s an fix for that from the Linux kernel 😀
Structs themselves define a physically grouped set of data, so all the bytes we want to store are roughly next to each other. (see Struct Padding below)
Now to start, say this is my EEPROM variable storage struct
How do we store this struct into the emulated EEPROM?
This copies the settings struct as raw bytes into the page_data byte buffer and then saves it into page 0.
How do we read it back from emulated EEPROM?
Because of compiler optimization, the variables will be padded to match the default size of the chip’s memory, i.e. 32-bits. Meaning there will be 2 “padding” bytes between device_name and serial_number to make 4 bytes. When your struct isn’t big and its being used internally for your firmware only then it isn’t a big deal to just leave.
Here’s an example of how it would turn out for the previous struct:
uint8_t device_name; //10 bytes means 2 + 1/2 dword
uint8_t padding1; //1/2 dword
uint16_t serial_number; //16bit means 1/2 dword
uint8_t padding2; //1/2 dword
uint16_t voltage_output; //16bit means 1/2 dword
uint8_t padding3; //1/2 dword
uint16_t date_of_manufacture; //16bit means 1/2 dword
uint8_t padding4; //1/2 dword
However, when you start to exceed the 60 bytes limit you may want to force packing. Packing simply tells the compiler to smash all the bytes next to each other. The downside of this is optimization. The compiler will now have to recreate shorts, words, etc when you access them from the struct and they are split memory words. i.e. Memory word 1 and memory 2 might have the LSB and MSB of the serial_number struct define and the compiler must insert assembly to convert it to a single memory word 3 before usage. Without packing, it would already be stored as a single word.
So how do you pack?
Simply apply the gcc attribute informing it to do so like above.
I personally have not noticed any real performance deficiencies by using packed structs but its all application dependent on how often you access struct members, how the compiler packs struct members by chance (i.e. device_name gets split between words but voltage_output doesn’t), etc.
Struct Size Validation
This reads the page into the byte buffer and then we dump the byte buffer into the settings struct.
Why do I copy to a buffer instead of the struct directly? Because the driver will always try and return 60 bytes and there struct is not necessarily that big.
Now you may wonder, what happens if our struct is bigger than 60 bytes? Well, bad things will happen because memcpy is blind to that problem and other variables could be affected.
The worst case is maybe your struct is initially fine but then you later add onto it and cause it to grow past the page size unknowingly. It may then keep working or fail horribly in different conditions, the worse of which isn’t noticeable until some edge case. This is definitely something you do not want.
But there’s a solution!
The sizeof function is a operator that’ll normally compute the size of data types but it only works at compile-time. Sadly the pre-processor won’t be able to evaluate it and stop with a #error or #warning.
But,the Linux kernel has this useful macro in “include/linux/kernel.h”
#define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2*!!(condition)]))
This beautiful macro is a bit of magic that will let us do a sizeof check that will stop compiling. Incidentally, the macro uses a sizeof check that causes the build error.
So the beautiful solution is to shove
BUILD_BUG_ON( sizeof(settings) > EEPROM_PAGE_SIZE); somewhere the compiler will come across, when the sizeof the settings struct is greater than EEPROM_PAGE_SIZE, the compiler will emit an unrelated error but will point to the line that BUILD_BUG_ON is on.
This is the protection to make this implementation safe to use in your firmware. You can then fix the error by trimming your struct by adjusting the data type sizes of variables or splitting it into a new struct. You can ideally store a different struct per page.
I used this trick because I had around 90 end user configurable settings, I easily split them off into four different structs that could be saved into four different pages. This reduced the work for me from having to explicitly assign data from EEPROM to RAM variables on start-up and doing the reverse when I needed to save them. Now I just blindly store and read the struct.