Embedded HandbookПриложения
Все разделы › XVIII. ESP32: второй микроконтроллер
ESP32 · уровень средний · 13 мин чтения
Открыть в интерактивном справочнике

155. Хранение на ESP32: таблица разделов, NVS, SPIFFS, FAT с выравниванием износа

На STM32 настройки кладут в свободную страницу флеш-памяти вручную (раздел 11), а файловую систему заводят на SD-карте (раздел 42). У ESP32 всё это уже есть: флеш-память на модуле разбита таблицей разделов, настройки хранит NVS — хранилище «ключ-значение» с контролем целостности, а файлы можно держать в SPIFFS или в FAT с выравниванием износа, и то и другое видно через обычные fopen и fread. Раздел показывает, чем они отличаются и сколько на самом деле занимает файл. Всё проверено в QEMU, где флеш-память эмулируется честно (_verify/esp155/): три перезапуска подряд, форматирование и запись.

Как это называется в документации

Оригинал Перевод
partition table (CSV) таблица разделов флеш-памяти
NVS (non-volatile storage) хранилище настроек «ключ-значение»
namespace, key, entry пространство имён, ключ, запись (32 байта)
SPIFFS плоская файловая система для флеш-памяти
LittleFS файловая система для флеш-памяти, современная замена SPIFFS; в ESP-IDF — отдельный компонент
FATFS, wear levelling FAT и слой выравнивания износа
VFS (virtual file system) слой, дающий fopen/fread для всех
allocation unit (cluster) кластер: наименьший кусок, который занимает файл
LFN (long file name) длинные имена; без них — только 8.3
sector, page сектор флеш-памяти 4096 байт, страница SPIFFS 256
spiffsgen.py, nvs_partition_gen.py сборка образов разделов на компьютере

155.1 Своя таблица разделов

Таблица по умолчанию из раздела 150 — настройки, калибровка радио и приложение. Как только нужны файлы, пишут свою (partitions.csv и CONFIG_PARTITION_TABLE_CUSTOM):

nvs,      data, nvs,     0x9000,  24K
phy_init, data, phy,     0xf000,  4K
factory,  app,  factory, 0x10000, 1M
storage,  data, spiffs,  ,        512K
fat,      data, fat,     ,        1M

Пустое смещение означает «сразу за предыдущим», и сборка считает его сама:

nvs,data,nvs,0x9000,24K,
phy_init,data,phy,0xf000,4K,
factory,app,factory,0x10000,1M,
storage,data,spiffs,0x110000,512K,
fat,data,fat,0x190000,1M,

Программа видит ту же таблицу через esp_partition_find:

раздел nvs       subtype 0x02 offset 0x009000 size  24576
раздел phy_init  subtype 0x01 offset 0x00f000 size   4096
раздел storage   subtype 0x82 offset 0x110000 size 524288
раздел fat       subtype 0x81 offset 0x190000 size 1048576

Раздел приложения обязан начинаться с адреса, кратного 64 КБ, любой раздел — с сектора 4096 байт (SPI_FLASH_SEC_SIZE). Суммарно эта таблица занимает 2,6 МБ из 4 — остальное можно отдать под второй образ приложения для обновления по воздуху — о нём дальше в этой части.

Флеш-память ESP32 по таблице разделов: загрузчик, таблица, NVS, приложение, SPIFFS и FAT; у каждого хранилища своя цена за файл


155.2 NVS: настройки, которые переживают всё

NVS — это не «эмуляция EEPROM», а журнальное хранилище: записи по 32 байта, страницы по сектору, контрольные суммы, и при записи нового значения старое помечается устаревшим, а не стирается на месте. Работа с ним — открыть пространство имён, читать и писать по ключу, подтвердить:

    nvs_handle_t h;
    ESP_ERROR_CHECK(nvs_open("app", NVS_READWRITE, &h));
    uint32_t count = 0;
    e = nvs_get_u32(h, "boot", &count);              /* при первом запуске ключа нет */
    count++;
    ESP_ERROR_CHECK(nvs_set_u32(h, "boot", count));
    ESP_ERROR_CHECK(nvs_set_str(h, "name", "датчик-7"));
    ESP_ERROR_CHECK(nvs_commit(h));

Три загрузки подряд (программа сама вызывает esp_restart):

загрузка 1: nvs_get_u32 → ESP_ERR_NVS_NOT_FOUND, boot = 1; записей занято 4, свободно 752, доступно 626, всего 756, пространств имён 1
загрузка 2: nvs_get_u32 → ESP_OK, boot = 2; записей занято 4, свободно 752, доступно 626, всего 756, пространств имён 1
загрузка 3: nvs_get_u32 → ESP_OK, boot = 3; записей занято 4, свободно 752, доступно 626, всего 756, пространств имён 1

Первый запуск — ESP_ERR_NVS_NOT_FOUND: ключа ещё нет, и это не ошибка, а нормальный ответ, под который пишут значение по умолчанию. Дальше счётчик растёт и переживает перезапуск. Ёмкость: раздел 24 КБ = 6 страниц по 4096, на странице после заголовка и карты помещается 126 записей, итого 756. Но одну страницу NVS всегда держит пустой: при уплотнении на неё переписываются живые записи. Поэтому под данные доступно на 126 меньше — 626; это поле available_entries, а free_entries резервную страницу включает. Наши четыре записи — пространство имён, число, строка и её продолжение; число занимает одну запись, строка — столько, сколько нужно на 32-байтовые куски. Файл образа можно собрать и на компьютере из CSV:

Created NVS binary: ===> /tmp/nvs.bin
nvs.bin: 24576 байт
страниц по 4096: 6 ; с записями: 1 ; с одним заголовком: 4
записей по 32 байта на странице после заголовка и карты: 126

Так на завод уезжают серийные номера и калибровки: nvs_partition_gen.py из таблицы CSV, esptool — по адресу раздела.

Раздел NVS на 24 КБ из шести страниц по 4096 байт; на странице после заголовка и карты состояний 126 записей по 32 байта — всего 756; занятые четыре — пространство имён app, число boot, строка name и её продолжение; журнал, а не эмуляция EEPROM


155.3 SPIFFS: плоско и дёшево

SPIFFS монтируется одной структурой, и если раздел пуст — форматирует его сам:

W (6151) SPIFFS: mount failed, -10025. formatting...
SPIFFS: всего 524288, доступно 474641, занято 0 после монтирования

Из 512 КБ раздела под данные остаётся 474 641 байт — почти 50 КБ, то есть 9,5 %, уходит на служебные страницы. Но и их до конца не заполняют: SPIFFS надёжно использует лишь около 75 % раздела. Когда место кончается, сборка мусора раз за разом просматривает всю файловую систему, и одна запись может занять несколько секунд, а то и не пройти. Для 512 КБ разумный предел — около 384 КБ данных. Дальше двадцать файлов по 1000 байт:

SPIFFS: записано файлов по 1000 байт: 20, занято стало 25100 (+25100), то есть 1255 байт на файл
SPIFFS: чтение f000.txt: «xxxx», каталогов нет — имена плоские

Файл в 1000 байт занимает 1255: страница SPIFFS — 256 байт, у каждой свой заголовок, плюс индексная страница на файл. То есть накладные расходы около четверти для мелких файлов — чем крупнее файл, тем меньше доля. Каталогов в SPIFFS нет вовсе: /spiffs/log/a.txt — это имя файла целиком, а не файл в каталоге, и длина имени вместе с путём и завершающим нулём ограничена 32 байтами (CONFIG_SPIFFS_OBJ_NAME_LEN), то есть на само имя — до 31 байта (кириллица в UTF-8 — по два байта на букву). Образ тоже собирается на компьютере — spiffsgen.py из каталога:

spiffs.bin: 524288 байт из 100 файлов по 1000

Так в прошивку кладут веб-страницы и конфигурации. Оговорка: SPIFFS объявлен устаревшим, у него нет защиты от отключения питания посреди записи, и новые проекты берут LittleFS — он подключается компонентом из реестра и ведёт себя так же снаружи.


155.4 FAT: дорого, но по-настоящему

FAT нужен там, где файлы читает компьютер: карта памяти, журнал, который вынимают и смотрят. Во флеш-памяти FAT живёт поверх слоя выравнивания износа (wear_levelling), потому что сам FAT перезаписывал бы одни и те же секторы таблицы:

FAT: всего 1048576, том 995328, свободно 995328 после форматирования (сектор WL 4096)
FAT: записано файлов по 1000 байт в /fat/log: 20, свободно стало 909312 (−86016), то есть 4300 байт на файл

Из мегабайта раздела том получился 995 328 байт: 52 КБ забрал слой выравнивания. 86 016 байт — это ровно 21 кластер: по одному на каждый из 20 файлов и ещё один под сам каталог log. Так что каждый файл в 1000 байт стоит 4300 байт: целый кластер 4096 и доля кластера каталога — делить кластер между файлами FAT не умеет. Тысяча мелких файлов в FAT — это 4 МБ, которых нет; для журнала пишут один файл и дописывают в конец. Зато каталоги настоящие (mkdir работает), и карту можно вставить в компьютер.

Важная мелочь: по умолчанию в ESP-IDF длинные имена выключены (CONFIG_FATFS_LFN_NONE=y), то есть имена — только 8.3, как в разделе 42. LOG_2026.TXT пройдёт, лог-2026-09-14.txt — нет; длинные имена включают в menuconfig, они стоят памяти под буфер. Русские буквы не пройдут даже в коротком имени: по умолчанию кодовая страница 437 (CONFIG_FATFS_CODEPAGE_437). Нужна 866 или длинные имена вместе с CONFIG_FATFS_API_ENCODING_UTF_8.


155.5 Что выбрать

Что хранить Чем
настройки, калибровки, серийный номер NVS
счётчик наработки, последнее состояние NVS (журнальная запись, износ размазан)
веб-страницы, файлы прошивки, словари SPIFFS или LittleFS, образ из spiffsgen.py
журнал, который вынут и прочитают на компьютере FAT + выравнивание износа, ещё лучше — SD-карта
много мелких записей подряд один файл с дописыванием, не тысяча файлов
данные, которые нельзя потерять при отключении питания LittleFS или NVS; SPIFFS не обещает

Ресурс флеш-памяти модуля — порядка 100 000 стираний сектора, и именно поэтому в ESP-IDF всё пишущее идёт либо через журнал (NVS), либо через выравнивание износа (FAT), либо через файловую систему, которая размазывает запись сама (SPIFFS, LittleFS).


155.6 Грабли

Симптом Причина
ESP_ERR_NVS_NOT_FOUND при первом чтении ключа ещё нет — это норма, писать значение по умолчанию
nvs_flash_init вернул ESP_ERR_NVS_NO_FREE_PAGES раздел заполнен или формат старый: nvs_flash_erase и заново
настройки пропали после обновления прошивки стёрли раздел NVS вместе с приложением; idf.py app-flash
SPIFFS: файл 1000 байт занял 1255 страница 256 байт плюс индекс на файл
mkdir("/spiffs/log") возвращает ошибку каталогов нет: fopen("/spiffs/log/a.txt", "w") создаёт файл с плоским именем log/a.txt, а mkdir не поддерживается; имя с путём — до 31 байта
FAT: 20 файлов съели 86 КБ кластер 4096 байт на файл
fopen("/fat/лог.txt") возвращает NULL длинные имена выключены, только 8.3
том FAT меньше раздела на 52 КБ служебная область выравнивания износа
данные пропали после отключения питания SPIFFS не переживает обрыв записи — LittleFS или NVS
раздел не влез в 4 МБ смещения кратны сектору, приложение — 64 КБ; считать таблицу заранее

155.7 Итоги

Полные исходники

main.c — NVS через перезапуски, SPIFFS и FAT с выравниванием износа в своих разделах.

/* Раздел 155: хранение на ESP32 — NVS через перезапуски, SPIFFS и FAT с выравниванием износа
 * в своих разделах: сколько влезает на самом деле. Три загрузки подряд через esp_restart(). */
#include <dirent.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include "esp_attr.h"
#include "esp_log.h"
#include "esp_partition.h"
#include "esp_spiffs.h"
#include "esp_system.h"
#include "esp_vfs_fat.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "wear_levelling.h"

static const char *TAG = "store155";
RTC_NOINIT_ATTR static int boots;                   /* номер загрузки переживает esp_restart */

static int fill(const char *dir, int chunk, int limit)   /* пишет до limit файлов по chunk байт */
{
    static char buf[1024];
    memset(buf, 'x', sizeof buf);
    int n = 0;
    for (; n < limit; n++) {
        char name[64];
        snprintf(name, sizeof name, "%s/f%03d.txt", dir, n);
        FILE *f = fopen(name, "wb");
        if (!f) break;
        int left = chunk, bad = 0;
        while (left > 0) {
            int part = left > (int)sizeof buf ? (int)sizeof buf : left;
            if (fwrite(buf, 1, part, f) != (size_t)part) { bad = 1; break; }
            left -= part;
        }
        if (fclose(f) != 0 || bad) { unlink(name); break; }
    }
    return n;
}

void app_main(void)
{
    if (esp_reset_reason() == ESP_RST_POWERON) boots = 0;
    boots++;

    /* NVS: счётчик загрузок в настройках */
    esp_err_t e = nvs_flash_init();
    if (e == ESP_ERR_NVS_NO_FREE_PAGES || e == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        e = nvs_flash_init();
    }
    ESP_ERROR_CHECK(e);
    nvs_handle_t h;
    ESP_ERROR_CHECK(nvs_open("app", NVS_READWRITE, &h));
    uint32_t count = 0;
    e = nvs_get_u32(h, "boot", &count);              /* при первом запуске ключа нет */
    count++;
    ESP_ERROR_CHECK(nvs_set_u32(h, "boot", count));
    ESP_ERROR_CHECK(nvs_set_str(h, "name", "датчик-7"));
    ESP_ERROR_CHECK(nvs_commit(h));
    nvs_stats_t st;
    nvs_get_stats(NULL, &st);
    printf("загрузка %d: nvs_get_u32 → %s, boot = %lu; записей занято %u, свободно %u, доступно %u, всего %u, пространств имён %u\n",
           boots, esp_err_to_name(e), (unsigned long)count, (unsigned)st.used_entries, (unsigned)st.free_entries,
           (unsigned)st.available_entries, (unsigned)st.total_entries, (unsigned)st.namespace_count);
    nvs_close(h);

    if (boots < 3) {
        vTaskDelay(pdMS_TO_TICKS(100));
        esp_restart();
    }

    /* таблица разделов, как её видит программа */
    esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, NULL);
    for (; it; it = esp_partition_next(it)) {
        const esp_partition_t *p = esp_partition_get(it);
        printf("раздел %-9s subtype 0x%02x offset 0x%06lx size %6lu\n", p->label, p->subtype, (unsigned long)p->address, (unsigned long)p->size);
    }

    /* SPIFFS: плоская файловая система в разделе storage */
    esp_vfs_spiffs_conf_t sc = { .base_path = "/spiffs", .partition_label = "storage", .max_files = 5, .format_if_mount_failed = true };
    ESP_ERROR_CHECK(esp_vfs_spiffs_register(&sc));
    size_t total = 0, used = 0;
    esp_spiffs_info("storage", &total, &used);
    printf("SPIFFS: всего 524288, доступно %u, занято %u после монтирования\n", (unsigned)total, (unsigned)used);
    size_t used0 = used;
    int nf = fill("/spiffs", 1000, 20);
    esp_spiffs_info("storage", &total, &used);
    printf("SPIFFS: записано файлов по 1000 байт: %d, занято стало %u (+%u), то есть %u байт на файл\n",
           nf, (unsigned)used, (unsigned)(used - used0), (unsigned)((used - used0) / (nf ? nf : 1)));
    FILE *f = fopen("/spiffs/f000.txt", "rb");
    char probe[8] = { 0 };
    fread(probe, 1, 4, f);
    fclose(f);
    printf("SPIFFS: чтение f000.txt: «%s», каталогов нет — имена плоские\n", probe);
    esp_vfs_spiffs_unregister("storage");

    /* FAT с выравниванием износа в разделе fat */
    wl_handle_t wl = WL_INVALID_HANDLE;
    esp_vfs_fat_mount_config_t fc = { .max_files = 5, .format_if_mount_failed = true, .allocation_unit_size = 4096 };
    ESP_ERROR_CHECK(esp_vfs_fat_spiflash_mount_rw_wl("/fat", "fat", &fc, &wl));
    uint64_t ft = 0, ff = 0;
    esp_vfs_fat_info("/fat", &ft, &ff);
    printf("FAT: всего 1048576, том %llu, свободно %llu после форматирования (сектор WL %u)\n", (unsigned long long)ft, (unsigned long long)ff, (unsigned)wl_sector_size(wl));
    ESP_ERROR_CHECK(mkdir("/fat/log", 0777) == 0 ? ESP_OK : ESP_FAIL);
    uint64_t ff0 = ff;
    nf = fill("/fat/log", 1000, 20);
    esp_vfs_fat_info("/fat", &ft, &ff);
    printf("FAT: записано файлов по 1000 байт в /fat/log: %d, свободно стало %llu (−%llu), то есть %llu байт на файл\n",
           nf, (unsigned long long)ff, (unsigned long long)(ff0 - ff), (unsigned long long)((ff0 - ff) / (nf ? nf : 1)));
    esp_vfs_fat_spiflash_unmount_rw_wl("/fat", wl);
    ESP_LOGI(TAG, "done");
}

Дальше: раздел 156 — сеть: Ethernet и Wi-Fi, TCP/IP, HTTP-сервер, MQTT, SNTP, syslog.

Открыть в интерактивном справочнике