1. Тема дня
Endurance — это сколько раз можно erase/program. Retention
- как долго уже записанные биты сохранятся. Сегодня ищем
скрытую деградацию данных, которая уже произошла, но ещё не проявилась. Главная мысль: резервная копия является резервной только если её исправность периодически проверяется.
2. Зачем это нужно
A/B config может выглядеть здоровой, пока active B валидна. Но backup A мог повредиться месяцы назад. Когда B станет недоступна, окажется, что fallback уже давно не существует. Это критично для:
phase thresholds
GPIO mapping
ADC calibration
APN
MQTT endpoint
certificates
identity metadata3. Теория
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
copy A dies -> не замечаем -> copy B dies -> ERRORScrubbing превращает latent fault в recoverable fault:
copy A dies -> periodic CRC detects -> copy B still healthy -> repair AVerification sweep vs repair scrub
Verification: read + CRC/ECC check. Repair: восстановить повреждённую копию из здоровой.
Storage health
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
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
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. Типичные ошибки
- Считать retention и endurance одним параметром.
- Считать успешно записанные данные вечными.
- Проверять backup только при аварии active copy.
- Полагаться только на range-check без CRC.
- Считать CRC заменой ECC.
- Считать ECC заменой CRC.
- Игнорировать corrected ECC events.
- Одинаково реагировать на single- и double-bit ECC.
- Переносить ECC ISR между STM32 families без RM.
- Сканировать всю Flash длинным циклом в realtime firmware.
- Scrubber с высоким приоритетом.
- Repair без проверки здоровой копии.
- Увеличивать config generation при physical repair.
- Бесконечно ремонтировать деградирующую область.
- Не передавать storage degradation supervisor’у.
- Считать read-only factory partition бессрочно безошибочной.
5. Практическое задание
Добавь health model для cfg_a/cfg_b:
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:
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:
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
Эти десять уроков образуют единую архитектурную линию:
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 проектов:
- Data path должен быть bounded: DMA buffers, handles, SPSC, без больших memcpy() и mutex в горячем пути.
- Configuration path должен быть immutable: validate candi- date, atomic publish, generation, no mixed configs.
- State machines должны быть deterministic: события вместо прямого hardware access, replay и fuzzing.
- Contracts должны формализовать невозможные состояния.
- Supervisor должен восстанавливать fault domain, а не сразу весь MCU.
- Storage должен быть crash-consistent, с endurance budget и periodic scrubbing.
Задание
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 вместо бесконечных перезаписей.