1. Тема дня
Разбираем хранение конфигурации:
NVS / key-value storage
Flash settings
factory defaults
versioned config schema
migration
CRC / validation
atomic commit
rollback settings
OTA compatibility
STM32 EEPROM emulationГлавная мысль: конфигурация - это часть прошивки. Если параметры в Flash испортились, устарели или не подходят новой версии firmware, устройство должно не зависнуть, а загрузиться с безопасными defaults и понятно сообщить причину.
2. Зачем это нужно
В проектах есть много параметров, которые нельзя держать только в #define:
EC25 / MQTT:
APN, broker, port, client_id, TLS flags, reconnect policy
Светофорные входы:
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 policyПроблемы появляются, когда новая прошивка ждёт поле, которого нет в старой конфигурации; пользователь записал плохой broker; питание пропало во время записи; после OTA новая версия не может прочитать старые настройки.
3. Теория
Хорошая конфигурация
Минимальный header:
typedef struct {
uint32_t magic;
uint16_t version;
uint16_t size;
uint32_t crc32;
uint32_t generation;
} cfg_header_t;Смысл полей:
magic:
это точно наша структура, а не мусор
version:
какой формат конфигурации
size:
сколько байт ожидать
crc32:
данные не повреждены
generation:
какая копия новее, если храним A/BDefaults обязательны
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,
},
};Устройство должно запускаться без сохранённой конфигурации: после первого включения, очистки NVS, замены платы, отката OTA, повреждения настроек или factory reset.
Разделяй config, state и counters
config:
настройки, которые меняются редко и задают поведение
state:
runtime-состояние, которое может переживать reboot
counters:
диагностические счётчики, могут часто обновляться
calibration:
заводские/сервисные коэффициенты
secrets:
ключи, сертификаты, паролиЧастые события нельзя писать в Flash: каждый phase_event, MQTT reconnect, ADC sample или UART timeout. Частые counters держи в RAM; во Flash сохраняй только важные snapshots.
A/B config
Храни две копии:
cfg_a
cfg_bПри загрузке:
1. читаем cfg_a;
2. проверяем magic/version/size/crc;
3. читаем cfg_b;
4. проверяем magic/version/size/crc;
5. выбираем валидную с большим generation;
6. если обе невалидны - defaults.При сохранении новую конфигурацию пишем в неактивную копию с большим generation, затем commit. Старая копия остаётся fallback.
Migration
Новая прошивка должна понимать старый формат:
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 говорит, что данные не повреждены, но не говорит, что значения разумные.
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;
}Если validation fail: не применять плохую конфигурацию; загрузить defaults или last known good; сохранить fault event; показать config status.
4. Типичные ошибки
- Хранить настройки без version.
- Верить только CRC без validation.
- Часто писать counters в NVS/Flash.
- Не вызывать nvs_commit() после nvs_set_*().
- Очищать весь NVS при любой ошибке.
- Писать STM32 Flash напрямую как EEPROM.
- Подтверждать OTA до проверки и migration конфигурации.
5. Практическое задание
Создай 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.Добавь CLI:
config show
config validate
config save
config defaults
config factory-reset
config status6. Что попробовать дальше
- Реализовать config_manager_task.
- Сделать A/B blob с generation.
- Добавить HIL-тесты: empty NVS, corrupted cfg_a, corrupted both, old version, invalid value, power loss during save, factory reset.
Задание
Во время A/B save питание исчезло после записи неактивной копии, но до commit. Опишите выбор при загрузке и тест сохранности последних валидных настроек.
Критерии самопроверки: Отделить запись данных от commit, сохранить старый fallback и проверить наблюдаемое поведение после reboot.
Показать ответ автора
Проверить обе копии по header, CRC и смысловым ограничениям. Выбрать валидную подтверждённую копию с подходящим generation; если обе невалидны, использовать безопасные defaults. Внедрить прерывание и проверить выбранные настройки и config status.
Задание
У конфигурации правильный CRC, но недопустимый broker или параметр времени. Объясните недостаточность CRC и задайте fallback и диагностическое доказательство.
Критерии самопроверки: Назвать правило проверки значения, безопасный fallback и доказательство, что плохие настройки не применены.
Показать ответ автора
CRC обнаруживает повреждение байтов, а не пригодность настроек. Отклонить значения, не прошедшие смысловую validation, загрузить defaults или last known good, записать отказ и показать причину через config status.