1. Тема дня

Строим storage, который переживает внезапное отключение питания в любой точке update transaction. Главная мысль: после power loss допустим OLD или NEW, но никогда HALF-NEW.

2. Зачем это нужно

Конфигурация связана: ADC thresholds, debounce, APN, MQTT host, retry policy. Если хранить их разрозненными keys, после power loss можно получить смесь generations.

3. Теория

Brownout detector не graceful shutdown

Brownout detector уже срабатывает, когда напряжение стало опасным. Он не означает, что осталось время спокойно записать NVS. Early warning

Нужен отдельный POWER_FAIL_WARNING выше reset threshold: PVD/comparator/ADC upstream supply.

Crash consistency

Storage contract:

text
after any power loss: OLD or NEW, never HALF-NEW

NVS semantics

nvs_set_*() ещё не durable. Durable boundary — успешный nvs_commit().

Application transaction поверх NVS

Храним целый record:

c
typedef struct {
    uint32_t magic;
    uint16_t schema;
    uint16_t reserved;
    uint64_t generation;
    config_payload_t payload;
    uint32_t crc32;
} config_record_t;

A/B records

Храним cfg_a, cfg_b. При boot читаем обе копии, валидируем CRC/semantic, выбираем максимальную valid generation. Отдельный persistent active_slot не обязателен.

Update transaction

text
ACTIVE generation 41
 -> build generation 42
 -> validate
 -> write inactive slot
 -> nvs_commit
 -> read-back verify
 -> publish runtime snapshot
 -> ACK backend

Raw Flash commit marker

Если не NVS, commit marker пишется последним:

text
erase -> header -> payload -> CRC -> verify -> VALID marker LAST

4. Типичные ошибки

  1. Считать brownout graceful interrupt.
  2. Начинать Flash write после low-voltage warning.
  3. Связанную config хранить независимыми keys.
  4. ACK backend до nvs_commit().
  5. Считать nvs_set_*() durable.
  6. Одна копия критичной config.
  7. A/B без CRC.
  8. CRC без generation.
  9. VALID marker до payload.
  10. Сразу стирать старую generation.
  11. Не делать semantic validation после CRC.
  12. nvs_commit() каждую секунду для counters.
  13. Не тестировать power cut в середине transaction.

5. Практическое задание

Сделай A/B config поверх NVS:

text
cfg_a
cfg_b

При boot:

text
read A/B -> validate -> choose highest valid generation -> publish runtime

Добавь fault points:

text
AFTER_SET
AFTER_COMMIT
AFTER_VERIFY
AFTER_RUNTIME_PUBLISH

В test build делай esp_restart() в каждой точке. После boot должно быть только generation 41 или 42, но не mixed config. CLI:

text
config storage
slot A: valid=yes generation=41 crc=OK
slot B: valid=yes generation=42 crc=OK
active=B

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

  • Настоящий HIL power-cut с Raspberry Pi и USB/MOSFET power switch.
  • Для STM32 изучить PVD + EEPROM emulation.
  • Разделить factory read-only NVS и runtime writable NVS.
Какой шаг является durability boundary обновления NVS в этом уроке?

Задание

Active A — generation 41. Update строит generation 42 в B, но питание пропадает во время операции. Опишите выбор при boot и допустимые outcomes. Доказывает ли один test-build esp_restart() поведение при электрическом power cut?

Критерии самопроверки: Требуйте целую OLD или NEW, проверяйте обе copies и отличайте software restart от power loss.

Показать ответ автора

При boot читаются обе slots, проверяются CRC и semantics, выбирается максимальная valid generation. Итог должен быть целой generation 41 или 42 без смеси полей. Software restart проверяет пути восстановления после reboot, но не все электрические прерывания; урок отдельно предлагает настоящий HIL power cut.