Embedded HandbookПриложения
Все разделы › VI. Практикум
STM32F103, STM32F407 · уровень средний · 12 мин чтения
Открыть в интерактивном справочнике

42. Файловая система FatFS

Сырая флеш‑память — это просто пронумерованные секторы. Файловая система превращает их в файлы и каталоги, а главное — делает носитель понятным для компьютера: вынули карту, вставили в ноутбук, увидели логи.

FatFS — небольшая библиотека, реализующая FAT для микроконтроллеров. Она не знает ничего о вашем железе: вы даёте ей пять функций доступа к секторам, она даёт вам файлы.

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

Оригинал Перевод
volume / drive том / диск
to mount / unmount смонтировать / размонтировать
cluster кластер, единица размещения файла
allocation table таблица размещения
long file name (LFN) длинное имя файла
code page кодовая страница
sector size размер сектора
flush / sync сброс кэша на носитель
file allocation table (FAT) таблица размещения файлов

42.1 Какой FAT выбрать

FAT12 FAT16 FAT32 exFAT
максимум тома 32 МБ 2 ГБ 2 ТБ огромный
максимум файла — 2 ГБ 4 ГБ без практического предела
накладные расходы малые малые средние большие
поддержка в FatFS всегда всегда всегда надо включать

Для внешней SPI Flash на 2–16 МБ подходит FAT12 или FAT16 — они экономнее по служебным структурам. Для SD‑карт от гигабайта — FAT32. exFAT нужен только для карт больше 32 ГБ, и включать его стоит осознанно: он заметно увеличивает объём кода.

Форматирует том сама FatFS вызовом f_mkfs, тип можно не выбирать — она подберёт по размеру. Рабочий буфер ей нужен не меньше сектора — при 4096 это 4 КБ, на стеке задачи он не поместится, его делают статическим. В новых версиях вызов такой: f_mkfs("", NULL, work, sizeof work), где NULL — параметры по умолчанию.


42.2 Из чего состоит FatFS

Библиотека разделена на два уровня, и это разделение — главное, что нужно понять:

Слой Файлы Кто пишет
файловая система ff.c, ff.h автор библиотеки, не трогаем
доступ к носителю diskio.c вы
настройки ffconf.h вы, один раз

Всё, что требуется для запуска на своём железе, — реализовать пять функций в diskio.c. Библиотека вызывает их и больше ни о чём не спрашивает.


42.3 Пять обязательных функций

DSTATUS disk_initialize(BYTE pdrv);
DSTATUS disk_status(BYTE pdrv);
DRESULT disk_read (BYTE pdrv, BYTE *buff, LBA_t sector, UINT count);
DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count);
DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff);

Параметр pdrv — номер физического диска: если носитель один, он всегда 0.

Слои FatFS: программа с f_open и f_write, файловая система ff.c, которую не трогают, слой diskio.c с пятью функциями disk_initialize, disk_status, disk_read, disk_write и disk_ioctl, который пишете вы, и драйвер носителя; справа выбор сектора — 512 байт дают восьмикратный износ флеш с сектором стирания 4096, равный сектор даёт прямую запись

Ключевое решение принимается до написания кода: какой взять размер сектора.

У флеш‑памяти минимальная единица стирания — сектор 4 КБ. У файловой системы традиционный сектор — 512 байт. Если оставить 512, то каждая запись превращается в: прочитать 4 КБ в ОЗУ → стереть 4 КБ → изменить 512 байт → записать 4 КБ обратно. Восьмикратный расход ресурса и в разы более медленная запись. По памяти выигрыша нет: слою чтения-изменения-записи нужен буфер на 4 КБ, а при секторе 4096 такой же буфер держит сама FatFS (42.8). Выбор решает износ.

Поэтому размер сектора файловой системы делают равным сектору стирания — 4096 байт. FatFS это поддерживает (допустимы 512…4096), и запись становится прямой: стереть один сектор, записать один сектор.

Тогда запись выглядит так:

DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count)
{
    if (pdrv != 0 || (disk_state & STA_NOINIT)) {
        return RES_NOTRDY;
    }
    if (sector + count > FLASH_SECTOR_COUNT) {
        return RES_PARERR;
    }

    for (UINT s = 0; s < count; s++) {
        uint32_t base = (sector + s) * FS_SECTOR_SIZE;

        /* Стереть сектор целиком — иначе запись даст побитовое «И»
           со старым содержимым. */
        w25_sector_erase(base);

        /* Записать постранично: за одну команду больше страницы нельзя. */
        for (uint32_t off = 0; off < FS_SECTOR_SIZE; off += FLASH_PAGE_SIZE) {
            w25_page_program(base + off,
                             buff + s * FS_SECTOR_SIZE + off,
                             FLASH_PAGE_SIZE);
        }
    }
    return RES_OK;
}

А disk_ioctl отвечает на четыре вопроса о геометрии:

DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff)
{
    switch (cmd) {
    case CTRL_SYNC:                  /* всё ли записано на носитель */
        return RES_OK;               /* у нас запись синхронная */

    case GET_SECTOR_COUNT:
        *(LBA_t *)buff = FLASH_SECTOR_COUNT;
        return RES_OK;

    case GET_SECTOR_SIZE:
        *(uint16_t *)buff = FS_SECTOR_SIZE;
        return RES_OK;

    case GET_BLOCK_SIZE:             /* в единицах секторов */
        *(DWORD *)buff = 1;
        return RES_OK;

    default:
        return RES_PARERR;
    }
}

Для SD‑карты всё проще: у неё блок и так 512 байт, стирания не требуется, и disk_write сводится к отправке команды записи блока.


42.4 Настройка в ffconf.h

Файл конфигурации решает, сколько библиотека займёт места. Значимые параметры:

Параметр Что делает Что ставить на МК
FF_FS_READONLY убрать всю запись 1, если только читаете — экономит килобайты
FF_USE_LFN длинные имена 0 — нет; 1 — буфер имени статический (одна задача); 2 — на стеке, +0,5 КБ стека; 3 — в куче
FF_USE_STRFUNC f_gets, f_puts, f_printf 1, если нужен f_printf
FF_CODE_PAGE кодировка имён 866 для кириллицы
FF_LFN_UNICODE кодировка строк в вызовах 2 (UTF-8), если исходники в UTF-8
FF_MAX_SS максимальный сектор 4096 для SPI Flash
FF_FS_TINY общий буфер вместо буфера на файл 1 при нехватке ОЗУ
FF_USE_MKFS форматирование 1, если хотите f_mkfs
FF_FS_EXFAT exFAT 0, если не нужен

⚠️ FF_FS_TINY экономит ощутимо. При нуле каждый открытый файл держит собственный буфер сектора: при секторе 4 КБ это 4 КБ на файл — больше четверти всей памяти BluePill. При единице буфер один и лежит в структуре тома. Расплата — чуть медленнее работа при нескольких открытых файлах.

⚠️ Кириллица в именах требует трёх настроек: FF_USE_LFN не ноль; FF_LFN_UNICODE = 2, если исходники в UTF-8, — иначе FatFS считает байты строки символами кодовой страницы, и «лог.txt» ляжет на карту как «╨╗╨╛╨│.txt»; FF_CODE_PAGE = 866 — для коротких имён 8.3, которые записываются рядом с длинными. Без длинных имён имя длиннее 8.3 не создаётся вовсе (FR_INVALID_NAME), а файлы с длинными именами видны под короткими: «Отчёт за май.txt» — как ОТЧЁТЗ~1.TXT. В старых FatFS из пакетов STM32Cube (R0.11, R0.12c) параметры называются с подчёркиванием (_USE_LFN, _CODE_PAGE), а UTF-8 в вызовах нет — он появился в R0.13a.


42.5 🔧 Работа с файлами

FATFS fs;                        /* структура тома, живёт всё время работы */
FIL   file;
FRESULT res;
UINT   written;

/* Монтирование. Третий аргумент 1 — смонтировать немедленно,
   0 — отложить до первого обращения. */
res = f_mount(&fs, "", 1);
if (res != FR_OK) {
    /* тома нет или он не отформатирован */
}

/* Запись. FA_OPEN_APPEND дописывает в конец, не затирая. */
res = f_open(&file, "log.txt", FA_WRITE | FA_OPEN_APPEND);
if (res == FR_OK) {
    f_write(&file, "hello\r\n", 7, &written);
    f_close(&file);              /* именно закрытие фиксирует данные */
}

/* Чтение. */
char buf[64];
UINT got;
res = f_open(&file, "log.txt", FA_READ);
if (res == FR_OK) {
    f_read(&file, buf, sizeof(buf), &got);
    f_close(&file);
}

Форматированный вывод удобнее делать через f_printf — он есть в библиотеке и не тянет за собой тяжёлый printf из стандартной библиотеки.

Перебор каталога:

DIR dir;
FILINFO fno;

if (f_opendir(&dir, "/") == FR_OK) {
    while (f_readdir(&dir, &fno) == FR_OK && fno.fname[0] != 0) {
        if (fno.fattrib & AM_DIR) {
            /* это каталог */
        } else {
            /* файл, размер в fno.fsize */
        }
    }
    f_closedir(&dir);
}

Пустое имя в fno.fname — признак конца перечисления, а не ошибка.

Под ядром. Если к файлам обращаются несколько задач, без защиты они портят том: две задачи разом правят таблицу размещения и буфер окна в FATFS. Включают FF_FS_REENTRANT = 1 и дают FatFS мьютекс ядра — функции ff_mutex_create, ff_mutex_take, ff_mutex_give и ff_mutex_delete в ffsystem.c (в старых версиях из STM32Cube — ff_cre_syncobj, ff_req_grant и соседи в syscall.c). Проще — отдать все файлы одной задаче-привратнику (30.8), а остальные шлют ей запросы очередью. Стек задачи с FatFS считают: сама библиотека под Cortex-M3 берёт около 0,4 КБ, а при FF_USE_LFN = 2 буфер имени (512 байт) ложится на стек, и выходит около 0,9 КБ — плюс слой диска и драйвер.


42.6 ⚠️ Что происходит при отключении питания

Самая болезненная особенность FAT, и о ней надо знать заранее.

Когда вы вызываете f_write, данные не оказываются на носителе целиком. Меняются три вещи: содержимое кластеров, таблица размещения и запись в каталоге с длиной файла. Между этими шагами есть моменты, когда файловая система несогласованна.

Отключение питания в такой момент даёт один из вариантов:

Что с этим делать:

f_write(&file, data, len, &written);
f_sync(&file);        /* принудительно записать метаданные, не закрывая файл */

f_sync делает всё то же, что f_close, но оставляет файл открытым. Вызов после каждой важной записи резко снижает окно уязвимости — ценой лишних обращений к носителю.

Полностью проблему FAT не решает: она не журналируемая. Если данные критичны, применяют либо резервный носитель, либо собственный формат журнала — последовательная запись с контрольной суммой на каждую запись, чтобы битые хвосты можно было отбросить при чтении.


42.7 ⚠️ Износ: FatFS его не выравнивает

Важное, о чём редко пишут. FAT постоянно пишет в одно и то же место — таблица размещения и корневой каталог обновляются при каждом изменении файла. На SD‑карте это неважно: внутри карты собственный контроллер, который раскидывает записи по кристаллу. А на голой SPI Flash таблица окажется в конкретных секторах, и они износятся первыми.

При ресурсе 100 000 циклов и записи раз в секунду с f_sync первым выработается сектор каталога, где хранится длина файла: он стирается каждую секунду — примерно за сутки непрерывной работы (100 000 с ≈ 28 ч). Таблица размещения меняется реже — при каждом новом кластере, — но тоже в одних и тех же секторах.

Карта износа за сутки записи раз в секунду: FAT на голой SPI Flash и журнал по кругу

Отсюда практические выводы:


42.8 Сколько это займёт

Порядок величин, чтобы прикинуть, влезет ли:

Flash ОЗУ
FatFS без длинных имён, только чтение ~6 КБ ~1 КБ
FatFS с записью, без длинных имён ~10 КБ 1 КБ + сектор
с длинными именами и кодовой страницей 866 ~14 КБ + буфер имени
структура тома FATFS — ~50 байт + FF_MAX_SS: буфер окна есть всегда
структура файла FIL — ~40 байт, и ещё FF_MAX_SS при FF_FS_TINY = 0

При секторе 4 КБ структура FATFS занимает 4 КБ с небольшим всегда, а каждый открытый файл при FF_FS_TINY = 0 — ещё 4 КБ. На BluePill с её 20 КБ ОЗУ это очень много. Поэтому там ставят FF_FS_TINY = 1 или берут SD‑карту вместо SPI Flash. Сектор 512 на SPI Flash память не экономит: слою диска тогда нужен свой буфер на 4 КБ для перезаписи сектора стирания (42.3).

Таблица кодовой страницы 866 сама по себе занимает несколько сотен байт Flash — при жёстком дефиците от кириллицы в именах отказываются.


42.9 Грабли

Файл нулевой длины. Забыт f_close или пропало питание до него. Лечится f_sync после записи.

f_mount возвращает FR_NO_FILESYSTEM. Причин три: носитель не отформатирован; том в exFAT, а FF_FS_EXFAT = 0 — так с завода приходят карты больше 32 ГБ; слой диска читает не тот сектор — disk_ioctl отдаёт не тот размер сектора или перепутана адресация карт SDSC и SDHC (41а.4). Поэтому сама прошивка по этому коду f_mkfs не вызывает — так она сотрёт карту пользователя. Форматируют только свой носитель (SPI Flash) или по явной команде.

Структура FATFS объявлена локально. Она должна жить всё время работы с томом. Объявили внутри функции — при выходе память освободится, и обращения к тому пойдут по мусору.

Не выровненный буфер. Если обмен с носителем идёт через DMA, буфер сектора должен быть выровнен, иначе передача даёт ошибку (раздел 9).

Русские имена превращаются в мусор. Не хватает FF_USE_LFN, неверная кодовая страница или строки в UTF-8 при FF_LFN_UNICODE = 0 (42.4).

Медленная запись. При секторе 512 на флеш‑памяти каждая запись читает, стирает и переписывает 4 КБ. Совпадение размеров сектора со стиранием ускоряет всё в разы.

Переполнение стека. Буфер сектора на 4 КБ, объявленный локальной переменной, не влезет в стек микроконтроллера. Делайте такие буферы статическими.


42.10 Итоги

Дальше: раздел 42а «LittleFS на SPI NOR: файловая система, которая переживает отключение питания» — файлы на голой SPI NOR без порчи при обрыве и с выравниванием износа.

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