1. Тема дня

Разбираем хранение конфигурации:

text
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:

text
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:

c
typedef struct {
    uint32_t magic;
    uint16_t version;
    uint16_t size;
    uint32_t crc32;
    uint32_t generation;
} cfg_header_t;

Смысл полей:

text
magic:
  это точно наша структура, а не мусор
version:
  какой формат конфигурации
size:
  сколько байт ожидать
crc32:
  данные не повреждены
generation:
  какая копия новее, если храним A/B

Defaults обязательны

c
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

text
config:
  настройки, которые меняются редко и задают поведение
state:
  runtime-состояние, которое может переживать reboot
counters:
  диагностические счётчики, могут часто обновляться
calibration:
  заводские/сервисные коэффициенты
secrets:
  ключи, сертификаты, пароли

Частые события нельзя писать в Flash: каждый phase_event, MQTT reconnect, ADC sample или UART timeout. Частые counters держи в RAM; во Flash сохраняй только важные snapshots.

A/B config

Храни две копии:

text
cfg_a
cfg_b

При загрузке:

text
1. читаем cfg_a;
2. проверяем magic/version/size/crc;
3. читаем cfg_b;
4. проверяем magic/version/size/crc;
5. выбираем валидную с большим generation;
6. если обе невалидны - defaults.

При сохранении новую конфигурацию пишем в неактивную копию с большим generation, затем commit. Старая копия остаётся fallback.

Migration

Новая прошивка должна понимать старый формат:

c
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 говорит, что данные не повреждены, но не говорит, что значения разумные.

c
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.

markdown
# 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:

text
config show
config validate
config save
config defaults
config factory-reset
config status

6. Что попробовать дальше

  • Реализовать 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.