Memory Map¶
Три типа энергонезависимой памяти: внутренний Flash ESP32, NVS-раздел внутри Flash, и внешний I2C-EEPROM M24C16.
1. Flash (ESP32, 4 МБ)¶
Раскладка определяется partitions.csv в корне проекта:
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x09000, 0x05000, # 20 КБ — Preferences
otadata, data, ota, 0x0E000, 0x02000, # 8 КБ — OTA selector
app0, app, ota_0, 0x10000, 0x1C0000, # 1.75 МБ — slot A
app1, app, ota_1, —, 0x1C0000, # 1.75 МБ — slot B
spiffs, data, spiffs, —, 0x02000, # 8 КБ — резерв
coredump, data, coredump, —, 0x10000, # 64 КБ — дамп при panic
| Раздел | Назначение |
|---|---|
nvs |
Все настройки и состояние (см. nvs-reference.md) |
otadata |
Селектор активного app-раздела для bootloader |
app0 |
OTA slot A. Используется на чётных обновлениях |
app1 |
OTA slot B. Используется на нечётных обновлениях |
spiffs |
Не используется на запись. Web-UI зашит в PROGMEM (app) |
coredump |
Core dump при panic/TWDT — backtrace, стеки задач. Читается через /api/diag/coredump |
Изменение партиционной таблицы (добавление coredump) применяется
только при прошивке по кабелю — OTA таблицу не переписывает. Offset'ы
app0/app1/nvs не сдвинуты — данные при этом не теряются.
Web-UI (HTML/CSS/JS) встроен напрямую в текст прошивки через
PROGMEM-заголовки include/*_gz.h, а не лежит в spiffs. Это упрощает OTA:
один бинарь обновляет и код, и UI атомарно.
ОТА-pattern:
1. Загружен app0 → app1 свободен.
2. POST /api/ota/upload пишет байты в app1.
3. POST /api/ota/apply ставит otadata на app1 и esp_restart().
4. Если новый образ не загрузился (boot-validation) — rollback на app0.
См. admin/ota-update.md.
2. NVS (20 КБ внутри Flash)¶
NVS — key-value store ESP-IDF, разбит на namespaces (≈ "таблицы").
Размер раздела 0x5000 = 20480 байт включает оверхеды.
Использование на устройстве можно прочитать через:
Все ключи каждого namespace и дефолты — в nvs-reference.md.
Размер записи (грубо): - Ключ ≤ 15 символов - Запись = 32 байта overhead + значение (uint = 4 байта, string округляется до 32 байт) - 20 КБ ≈ 500 записей — с большим запасом для текущей схемы (~60 ключей)
Износ: NVS использует wear-leveling внутри своего раздела. Частые записи
(например, OTA-прогресс в namespace ota) распределяются по флешу.
Расчётный ресурс — ≥10⁵ циклов на ячейку, реально >10⁶.
3. EEPROM M24C16 (внешний, I2C)¶
| Параметр | Значение |
|---|---|
| Чип | M24C16 / AT24C16 |
| Объём | 2048 байт (2 КБ) |
| Размер страницы | 16 байт |
| Количество страниц | 128 |
| I2C-адрес | 0x50 |
| ID-страница (ST) | 0x58 (только оригиналы; клоны не отвечают) |
| Шина | SDA/SCL (см. include/config.h) |
| Ресурс на страницу | ≥1 млн циклов записи |
См. ADR-007 для деталей определения чипа и self-test'а.
Раскладка наработки дозаторов (схема v3)¶
Чип делится на две зоны. EXPECTED_EEPROM_SCHEMA = 3.
0x000 ┌──────────────────────────────┐
│ Zone A — кольцо абсолютной │ 60 слотов × 32 байта
│ наработки (1920 байт) │ адрес слота i = i × 32
0x780 ├──────────────────────────────┤
│ Zone B — baseline + дата ТО │ 2 слота × 64 байта (пинг-понг)
0x800 └──────────────────────────────┘ (end)
Абсолютная наработка хранится в Zone A, относительная (с последнего ТО)
вычисляется как absolute − baseline, где baseline — снимок из Zone B.
Zone A — кольцо абсолютных счётчиков (32 байта/слот):
struct __attribute__((packed)) EepromSlotA {
uint8_t seq; // [0] монотонный счётчик 0→255→0 (RFC1982)
uint8_t version; // [1] = EXPECTED_EEPROM_SCHEMA (=3)
uint32_t abs_runtime[3]; // [2..13] RUN-наработка каналов 0..2, сек, LE
uint32_t abs_pump[3]; // [14..25] время насоса каналов 0..2, сек, LE
uint8_t reserved[4]; // [26..29] резерв
uint16_t crc16; // [30..31] CRC16-IBM байт [0..29], LE
};
Zone B — baseline последнего ТО (64 байта/слот):
struct __attribute__((packed)) EepromSlotB {
uint8_t seq; // [0] монотонный счётчик
uint8_t version; // [1] = EXPECTED_EEPROM_SCHEMA (=3)
uint32_t base_runtime[3]; // [2..13] снимок abs_runtime на момент ТО
uint32_t base_pump[3]; // [14..25] снимок abs_pump на момент ТО
uint32_t reset_ts[3]; // [26..37] local epoch последнего ТО, 0=неизв.
uint8_t reserved[24]; // [38..61] резерв
uint16_t crc16; // [62..63] CRC16-IBM байт [0..61], LE
};
Boot scan: eepromCountersInit() сканирует Zone A (head = валидный CRC
и максимальный seq по RFC 1982) и Zone B (пинг-понг из 2 слотов). Если
в Zone A нет слотов version==3, но есть version==2 — выполняется
миграция: старый счётчик становится абсолютным, baseline инициализируется
нулём.
Запись Zone A: новый слот = (head + 1) % 60, flush раз в 60 с при
активной работе и при DOSING→IDLE. При ресурсе 1 млн циклов и записи раз
в минуту каждая страница перезаписывается раз в час → ресурс — десятилетия.
Запись Zone B: только по команде «Сброс на ТО» (POST /api/statistics/
reset). base ← текущий абсолют, reset_ts ← local epoch. Запись редкая,
износ неактуален; 2 слота дают защиту от рваной записи при сбое питания.
Сброс абсолютной наработки возможен только полным форматированием чипа
(POST /api/diag/eeprom/format, защищён паролём администратора).
Текущая сессия живёт в RAM (DosatorCounters.session_s / session_pump_s)
и прибавляется к абсолюту при DOSING→IDLE.
См. src/eeprom_counters.cpp и include/eeprom_counters.h.
4. RAM (внутренняя SRAM ESP32, ≈ 320 КБ)¶
Использование статической RAM известно после сборки:
pio run -e esp32dev показывает RAM: 19.6% (~62 КБ).
Динамическая heap'a в норме 100…200 КБ свободно. Тонкие места:
- mbedTLS handshake (telegram/IMAP) — пик до ~40 КБ на коннект.
- AsyncTCP очередь — буферы по 1.5 КБ на соединение.
- AsyncWebServer response для больших JSON (например,
/api/diag/system) выделяет копию строки целиком в heap.
Текущее свободное heap можно посмотреть в GET /api/diag/system: