As firmware evolves, stored settings formats change. If EEPROM layout is not versioned, upgrades can silently load garbage and produce hard-to-diagnose behavior.
My pattern is a small config header with magic bytes, schema version, payload length, and CRC. On boot, the firmware validates the header first. If validation fails, it loads defaults and writes a clean config block.
For each schema change, I add a migration function from version N to N+1. This makes upgrades explicit and testable.
The result is predictable: old devices can be flashed in the field without manual resets, and new firmware can still recover safely from corrupted storage.
The config header and struct
I keep the layout as a packed struct so its size and field offsets are predictable across builds. A magic value catches uninitialized or foreign EEPROM contents, the version byte drives migration, and the CRC catches corruption.
#include <EEPROM.h>
#include <util/crc16.h> // AVR: _crc16_update
static const uint16_t CONFIG_MAGIC = 0xC0DE;
static const uint8_t CONFIG_VERSION = 2;
static const int CONFIG_ADDR = 0;
struct __attribute__((packed)) Config {
uint16_t magic; // must equal CONFIG_MAGIC
uint8_t version; // schema version
uint8_t reserved; // keep the payload word-aligned
// --- payload ---
uint16_t sampleIntervalMs;
int16_t tempOffsetC10; // added in v2
// --- integrity ---
uint16_t crc; // CRC over every byte before this field
};
// CRC16 over the struct excluding its own crc field.
uint16_t configCrc(const Config &c) {
const uint8_t *p = reinterpret_cast<const uint8_t *>(&c);
size_t len = sizeof(Config) - sizeof(c.crc);
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; i++) {
crc = _crc16_update(crc, p[i]);
}
return crc;
}
On a non-AVR core without util/crc16.h, a small library such as FastCRC gives you the same helper without hand-rolling the polynomial.
Read, validate, or default
Boot reads the block, then checks magic and CRC before trusting a single field. Anything unexpected falls back to known-good defaults and rewrites a clean block.
void loadDefaults(Config &c) {
c.magic = CONFIG_MAGIC;
c.version = CONFIG_VERSION;
c.reserved = 0;
c.sampleIntervalMs = 1000;
c.tempOffsetC10 = 0;
}
void saveConfig(Config &c) {
c.magic = CONFIG_MAGIC;
c.version = CONFIG_VERSION;
c.crc = configCrc(c);
EEPROM.put(CONFIG_ADDR, c); // put() only writes changed bytes
}
Config loadConfig() {
Config c;
EEPROM.get(CONFIG_ADDR, c);
if (c.magic != CONFIG_MAGIC || c.crc != configCrc(c)) {
loadDefaults(c);
saveConfig(c);
return c;
}
if (c.version != CONFIG_VERSION) {
migrate(c); // bring an older layout up to the current one
saveConfig(c);
}
return c;
}
Migration skeleton
Each migration step is explicit and one version at a time, so upgrades from any old version chain forward through the same tested code paths.
void migrate(Config &c) {
switch (c.version) {
case 1:
// v1 had no tempOffsetC10 field; choose a safe default.
c.tempOffsetC10 = 0;
c.version = 2;
// fall through so a v1 device lands on the latest version
case 2:
// current version: nothing to do
break;
default:
// unknown/newer than firmware expects: refuse to guess
loadDefaults(c);
break;
}
}
The fall-through is deliberate: a device sitting on v1 walks through every intermediate step until it reaches the current version, and each step stays small enough to unit-test on the host.
A note on write endurance
Classic AVR EEPROM is rated around 100k write cycles per cell, so I never rewrite blindly. EEPROM.put() (and EEPROM.update() under it) only writes bytes that actually changed, which is why I prefer it over EEPROM.write() for whole-struct saves. The one trap to avoid is a rewrite-on-every-failed-boot loop: if validation keeps failing and you keep writing defaults, you can wear a cell in a way that is hard to diagnose in the field. On ESP32 the "EEPROM" is emulated over flash, so I use Preferences (NVS) instead, which does its own wear levelling and key-value storage rather than raw byte offsets.
Sources
- Imported from the previous version of the site (gaborl.hu). Original publication around 2025-01-12.
- Arduino EEPROM library reference
- ESP32 Preferences (NVS) documentation
- AVR-libc
util/crc16.h—_crc16_update
Update history
- — Adopted from previous gaborl.hu (Hugo) site via legacy import.