1. Тема дня

Endurance — это сколько раз можно erase/program. Retention

  • как долго уже записанные биты сохранятся. Сегодня ищем

скрытую деградацию данных, которая уже произошла, но ещё не проявилась. Главная мысль: резервная копия является резервной только если её исправность периодически проверяется.

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

A/B config может выглядеть здоровой, пока active B валидна. Но backup A мог повредиться месяцы назад. Когда B станет недоступна, окажется, что fallback уже давно не существует. Это критично для:

text
phase thresholds
GPIO mapping
ADC calibration
APN
MQTT endpoint
certificates
identity metadata

3. Теория

Retention vs endurance

Flash хранит заряд, он деградирует со временем. Температура и история cycling влияют на retention. Для ESP32 reten- tion/endurance берём из datasheet конкретной внешней SPI NOR Flash.

CRC и ECC

CRC обнаруживает accidental corruption логического record. ECC может исправить одиночную и обнаружить двойную ошибку на физическом слове, если конкретный MCU/память это поддерживает. ECC не отменяет application CRC, потому что они работают на разных уровнях. Latent corruption

text
copy A dies -> не замечаем -> copy B dies -> ERROR

Scrubbing превращает latent fault в recoverable fault:

text
copy A dies -> periodic CRC detects -> copy B still healthy -> repair A

Verification sweep vs repair scrub

Verification: read + CRC/ECC check. Repair: восстановить повреждённую копию из здоровой.

Storage health

c
typedef enum {
    STORAGE_COPY_UNKNOWN = 0,
    STORAGE_COPY_VALID,
    STORAGE_COPY_CORRUPTED,
    STORAGE_COPY_MISSING,
} storage_copy_state_t;
typedef struct {
    storage_copy_state_t state;
    uint64_t generation;
    uint64_t last_verified_us;
    uint32_t verify_failures;
    uint32_t repair_count;
} storage_copy_health_t;

Health matrix

text
A valid + B valid     -> REDUNDANT
A valid + B invalid   -> DEGRADED
A invalid + B valid   -> DEGRADED
A invalid + B invalid -> FAILED

Если A и B имеют одинаковую generation, но разные payload — это split-brain invariant violation.

Repair inactive copy

text
1. Read healthy copy.
2. Validate healthy copy fully.
3. Write damaged copy from healthy data.
4. Commit.
5. Read back.
6. CRC + semantic validation.
7. Mark redundancy HEALTHY.

Repair не должен увеличивать logical config generation, потому что логическая config не изменилась.

Repair budget

Если одна и та же copy постоянно портится, не нужно бесконечно переписывать её. После нескольких failures переводим storage в MEDIA_DEGRADED.

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

  1. Считать retention и endurance одним параметром.
  2. Считать успешно записанные данные вечными.
  3. Проверять backup только при аварии active copy.
  4. Полагаться только на range-check без CRC.
  5. Считать CRC заменой ECC.
  6. Считать ECC заменой CRC.
  7. Игнорировать corrected ECC events.
  8. Одинаково реагировать на single- и double-bit ECC.
  9. Переносить ECC ISR между STM32 families без RM.
  10. Сканировать всю Flash длинным циклом в realtime firmware.
  11. Scrubber с высоким приоритетом.
  12. Repair без проверки здоровой копии.
  13. Увеличивать config generation при physical repair.
  14. Бесконечно ремонтировать деградирующую область.
  15. Не передавать storage degradation supervisor’у.
  16. Считать read-only factory partition бессрочно безошибочной.

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

Добавь health model для cfg_a/cfg_b:

c
typedef struct {
    storage_copy_state_t state;
    uint64_t generation;
    uint64_t last_verified_us;
    uint32_t verify_count;
    uint32_t crc_failures;
    uint32_t repairs;
} storage_copy_health_t;

Реализуй verification:

c
static storage_copy_state_t config_verify_slot(const char *key, config_record_t *out)
{
    size_t size = sizeof(*out);
    esp_err_t err = nvs_get_blob(s_nvs, key, out, &size);
    if (err == ESP_ERR_NVS_NOT_FOUND) return STORAGE_COPY_MISSING;
    if (err != ESP_OK || size != sizeof(*out)) return STORAGE_COPY_CORRUPTED;
    if (config_record_check(out) != CONFIG_RECORD_OK) return STORAGE_COPY_CORRUPTED;
    return STORAGE_COPY_VALID;
}

Добавь split-brain invariant: если A/B valid и generation equal, pay- loads должны совпадать. Реализуй repair:

text
A valid, B invalid -> repair B from A
B valid, A invalid -> repair A from B
both invalid -> factory recovery / safe mode

Добавь CLI storage health. Тест: искусственно повредить B, дождаться scrub cycle, убедиться, что corruption обнаружена до reboot, затем repair B и проверить, что она usable.

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

  • Для STM32 с ECC добавить counters: corrected ECC, uncor- rectable ECC, last failing address.
  • Разнести physical mirror и rollback history как разные функции.
  • Добавить storage health в supervisor: HEALTHY / DE- GRADED_REDUNDANCY / MEDIA_DEGRADED / FAILED.

Краткая связка уроков 51-60

Эти десять уроков образуют единую архитектурную линию:

text
DMA ownership
 -> SPSC transfer
 -> immutable config publication
 -> deterministic replay
 -> state-machine fuzzing
 -> contracts
 -> supervisor recovery
 -> crash-consistent storage
 -> endurance budget
 -> latent corruption detection

Практический итог для твоих ESP32/STM32 проектов:

  1. Data path должен быть bounded: DMA buffers, handles, SPSC, без больших memcpy() и mutex в горячем пути.
  2. Configuration path должен быть immutable: validate candi- date, atomic publish, generation, no mixed configs.
  3. State machines должны быть deterministic: события вместо прямого hardware access, replay и fuzzing.
  4. Contracts должны формализовать невозможные состояния.
  5. Supervisor должен восстанавливать fault domain, а не сразу весь MCU.
  6. Storage должен быть crash-consistent, с endurance budget и periodic scrubbing.
A и B проходят validation и имеют одинаковую generation, но разные payloads. Что должна сообщить модель урока?

Задание

A полностью valid на generation 42; B invalid. Опишите repair и условие healthy redundancy. Увеличивает ли physical repair logical generation? Что делать при повторном повреждении B?

Критерии самопроверки: Укажите validation, commit/read-back, неизменную logical generation и escalation при повторных media faults.

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

Полностью проверьте A, запишите B из исправных данных A, выполните commit и read-back, затем CRC и semantic checks до healthy redundancy. Logical generation не увеличивается, поскольку configuration не менялась. Повторные failures требуют ограниченного repair budget и сообщения MEDIA_DEGRADED supervisor вместо бесконечных перезаписей.