1. Today’s topic
We examine configuration storage:
NVS / key-value storage
Flash settings
factory defaults
versioned config schema
migration
CRC / validation
atomic commit
rollback settings
OTA compatibility
STM32 EEPROM emulationThe central idea: configuration is part of firmware. If Flash parameters are corrupt, outdated, or incompatible with a new firmware version, the device should load safe defaults and clearly explain the reason rather than hang.
2. Why this matters
Projects contain many parameters that cannot live only in #define:
EC25 / MQTT:
APN, broker, port, client_id, TLS flags, reconnect policy
Traffic-light inputs:
active level, filter window, debounce time, conflict policy
ADS1115 / GPIO-expander:
I2C addresses, enabled devices, diagnostic thresholds
CAN / RS-485:
bitrate, node_id, Modbus address, timeout
watchdog / health:
warn/fault timeouts, degraded mode policyProblems arise when new firmware expects a field missing from old configuration; a user saves an invalid broker; power is lost during a write; or the new version cannot read old settings after OTA.
3. Theory
Good configuration
A minimal header:
typedef struct {
uint32_t magic;
uint16_t version;
uint16_t size;
uint32_t crc32;
uint32_t generation;
} cfg_header_t;Meaning of the fields:
magic:
this really is our structure, not garbage
version:
the configuration format
size:
the expected byte count
crc32:
the data is not corrupt
generation:
which copy is newer when storing A/BDefaults are mandatory
static const app_config_t APP_CONFIG_DEFAULT = {
.mqtt = {
.enabled = true,
.broker = "test.mosquitto.org",
.port = 1883,
.keepalive_s = 60,
},
.input = {
.poll_period_ms = 2,
.ac_window_ms = 80,
.debounce_ms = 30,
.conflict_hold_ms = 100,
},
};The device must start without saved configuration: after first power-on, NVS erasure, board replacement, OTA rollback, corrupt settings, or factory reset.
Separate config, state, and counters
config:
settings that change rarely and determine behavior
state:
runtime state that may survive reboot
counters:
diagnostic counters that may change frequently
calibration:
factory/service coefficients
secrets:
keys, certificates, passwordsDo not write frequent events to Flash: every phase_event, MQTT reconnect, ADC sample, or UART timeout. Keep frequent counters in RAM; save only important snapshots to Flash.
A/B config
Store two copies:
cfg_a
cfg_bAt startup:
1. read cfg_a;
2. check magic/version/size/crc;
3. read cfg_b;
4. check magic/version/size/crc;
5. choose the valid copy with the higher generation;
6. if both are invalid - defaults.When saving, write the new configuration to the inactive copy with a higher generation, then commit. The old copy remains the fallback.
Migration
New firmware must understand the old format:
static bool config_migrate_to_current(app_config_t *cfg)
{
if (cfg->version == 1) {
cfg->mqtt.keepalive_s = 60;
cfg->mqtt.tls_enabled = false;
cfg->version = 2;
}
if (cfg->version == 2) {
cfg->transport.mode = TRANSPORT_MQTT;
cfg->version = 3;
}
return cfg->version == APP_CONFIG_VERSION;
}Validation
CRC indicates that the data is not corrupt; it does not indicate that the values are reasonable.
if (cfg->mqtt.port == 0 || cfg->mqtt.port > 65535) {
r.ok = false;
}
if (cfg->input.poll_period_ms < 1 || cfg->input.poll_period_ms > 100) {
r.ok = false;
}If validation fails: do not apply the invalid configuration; load defaults or last known good; record a fault event; show config status.
4. Common mistakes
- Storing settings without a version.
- Trusting CRC without validation.
- Writing counters frequently to NVS/Flash.
- Not calling nvs_commit() after nvs_set_*().
- Erasing all NVS on every error.
- Writing STM32 Flash directly as if it were EEPROM.
- Confirming OTA before checking and migrating configuration.
5. Practical assignment
Create CONFIG_STORAGE_POLICY.md.
# Config storage policy
## Main rule
Device must boot safely with missing, old, corrupted or invalid configuration.
## Rules
1. Every persistent config has magic, version, size and CRC.
2. Every config field has a default.
3. Config, state, counters, calibration and secrets are separate.
4. Fast path never writes Flash/NVS.
5. Config writes are handled by config_manager_task.
6. Saved config is validated before apply and after read-back.
7. OTA is confirmed only after config migration and critical self-test.
8. STM32 internal Flash is not treated as byte-addressable EEPROM.Add CLI commands:
config show
config validate
config save
config defaults
config factory-reset
config status6. What to try next
- Implement config_manager_task.
- Create an A/B blob with generation.
- Add HIL tests: empty NVS, corrupted cfg_a, both copies corrupted, old version, invalid value, power loss during save, and factory reset.
Exercise
An A/B save loses power after writing the inactive copy but before commit. Describe startup selection and how you would test that the last valid settings survive.
Self-check criteria: Distinguish data written from data committed, preserve the old fallback, and verify observable behavior after reboot.
Show the supplied answer
Validate both copies using the defined header, CRC, and semantic checks. Select the valid committed copy with the appropriate generation; if neither is valid, use safe defaults. Inject the interruption and verify both the chosen settings and config status.
Exercise
A configuration has a correct CRC but an invalid broker or timing value. Explain why CRC alone is insufficient and define the fallback and diagnostic evidence.
Self-check criteria: Name a value-level validation rule, a safe fallback, and evidence that invalid settings were not applied.
Show the supplied answer
CRC detects byte corruption, not whether settings are usable. Reject values that fail semantic validation, load safe defaults or last known good, record the fault, and expose the reason through config status.