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.
Ключевое решение принимается до написания кода: какой взять размер сектора.
У флеш‑памяти минимальная единица стирания — сектор 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 ч). Таблица размещения
меняется реже — при каждом новом кластере, — но тоже в одних и тех же секторах.
Отсюда практические выводы:
- для интенсивной записи на SPI Flash берите не FAT, а свой журнал: пишите последовательно по кругу, стирайте сектор перед заходом на него;
- если FAT нужен именно для совместимости с компьютером — пишите редко и большими порциями, накапливая данные в ОЗУ;
- на SD‑карте проблема снимается контроллером карты, но дешёвые карты выравнивают износ плохо — для ответственных задач берут промышленные.
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 Итоги
- FatFS не знает о железе: вы пишете пять функций доступа к секторам, она даёт файлы и каталоги.
- Размер сектора файловой системы стоит делать равным сектору стирания флеш‑памяти — иначе каждая запись стоит восьмикратного износа.
FF_FS_TINYэкономит по буферу сектора на каждый открытый файл; при секторе 4 КБ это решает, влезет ли всё в ОЗУ.- Кириллица требует включённых длинных имён, кодовой страницы 866 и, для
исходников в UTF-8,
FF_LFN_UNICODE = 2. FR_NO_FILESYSTEM— не приказ форматировать: так же выглядят exFAT и ошибка слоя диска.- Данные фиксируются на носителе только при
f_closeилиf_sync. Питание, пропавшее раньше, оставит файл нулевой длины. - FatFS не выравнивает износ. На голой SPI Flash таблица размещения изнашивает свои секторы; для интенсивной записи нужен свой журнал или SD‑карта с внутренним контроллером.
- Структура тома должна жить всё время работы, буферы секторов — статические.
- Под ядром —
FF_FS_REENTRANT = 1с мьютексом или одна задача на все файлы.
Дальше: раздел 42а «LittleFS на SPI NOR: файловая система, которая переживает отключение питания» — файлы на голой SPI NOR без порчи при обрыве и с выравниванием износа.