← На главную

Работа с FAT32 и exFAT с помощью библиотеки FatFs

В рамках статьи Учимся работать с SDHC/SDXC-картами по протоколу SPI мы освоили использование SD-карт подобно блочным устройствам в *nix системах, то есть, читая и записывая блоки размером 512 байт. Навык этот, бесспорно, полезен, но настоящая крутизна заключается в умении работать с файловыми системами на эти картах. Написать соответствующую библиотеку с нуля – это проект далеко не на один и не на два вечера, как в случае с какими-нибудь OLED-экранчиками. Поэтому даже такой изобретатель колес, как я, в данном случае был вынужден использовать что-то готовое. А именно, библиотеку FatFs.

Коротко о главном

FatFs реализует поддержку файловых систем FAT и exFAT. Библиотека написана на ANSI C и имеет BSD-подобную лицензию. Характерно, что FatFs все равно, где вы его используете – на микроконтроллерах AVR, STM32, или вообще на десктопе. Также ему все равно, что вы используете для хранения информации. Это может быть и SD-карта, и SPI flash, и файл на диске, и даже какое-то сетевое устройство. В рамках этой заметки будет продемонстрировано использование данной библиотеки на примере микроконтроллеров STM32 и SD-карт. Однако важно помнить, что область применения FatFs этим не ограничивается. Кстати, выше я неспроста отметил, что написать аналогичную библиотеку с нуля крайне непросто. FatFs развивается с 2006 года, и в ней до сих пор временами исправляют небольшие ошибки.

Если говорить конкретно про STM32, то есть по крайней мере два способа подключения библиотеки FatFs к проекту. Это можно сделать через свойства проекта в STM32CubeMX, или же вручную. Проблема с первым способом заключается в том, что он добавляет в проект далеко не самую свежую версию FatFs. А как было отмечено выше, в старых версиях есть неисправленные ошибки. Поэтому в рамках данной статьи речь пойдет исключительно о ручном способе. Интересующиеся тем, как это делается в STM32CubeMX, могут изучить данный вопрос самостоятельно, это не сложно. Кроме того, в полной версии исходников к сей статье вы найдете два проекта, один из которых был сделан автоматическим способом, а второй – ручным. Отличия в них небольшие.

Подключение FatFs

FatFs подключается к проекту следующим образом. Качаем исходники библиотеки и распаковываем их в отдельный каталог:

fatfs/ +-- 00history.txt +-- 00readme.txt +-- diskio.c +-- diskio.h +-- ff.c +-- ffconf.h +-- ff.h +-- ffsystem.c +-- ffunicode.c +-- integer.h

Makefile правим следующим образом. В C_SOURCES добавляем:

fatfs/ff.c \ fatfs/diskio.c \ fatfs/ffunicode.c \

Также дописываем в C_INCLUDES:

-Ifatfs \

Настройка библиотеки осуществляется через fatfs/ffconf.h. Настроек довольно много, но они хорошо документированы. Основные настройки, которые имеет смысл изменить, следующие.

Включаем поддержку длинных имен файлов (LFN, Long File Name). Эта опция необходима для exFAT, который в свою очередь нужен для работы с картами SDXC. Другими словами, если тип карты заранее неизвестен, или планируется использовать карты размером больше 32 Гб, включаем:

#define FF_USE_LFN 1

Чтобы не греть мозг с кодировками и локалями, включаем поддержку UTF-8:

#define FF_LFN_UNICODE 2

Поддержка exFAT:

#define FF_FS_EXFAT 1

В этом проекте я не использовал часы реального времени. Так как текущее время прошивке неизвестно, говорим FatFs использовать для всего захардкоженное время:

#define FF_FS_NORTC 1 #define FF_NORTC_MON 1 #define FF_NORTC_MDAY 1 #define FF_NORTC_YEAR 2018

Далее нужно отредактировать fatfs/diskio.c. Здесь объявляются процедуры, определяющие, где физически библиотека будет искать данные, и прочее в таком духе.

Процедура disk_status возвращает текущий статус диска. В нашем случае она всегда будет возвращать успех:

DSTATUS disk_status ( BYTE pdrv /* Physical drive nmuber to identify the drive */ ) { return 0; }

Процедура disk_initialize вызывается при инициализации диска. Как ни странно, в нашем случае она тоже будет просто возвращать успех. Связано это вот с чем. Как было объяснено в прошлой статье, SD-карта должна быть либо первым инициализируемым устройством на SPI-шине, либо использовать отдельную шину. К сожалению, FatFs производит отложенную инициализацию. Другими словами, мы не можем сказать точно, в какой момент она произойдет. Более того, момент этот будет меняться с изменением основного кода. Поэтому, чтобы гарантировать, что SD-карта инициализируется сразу после запуска прошиви, disk_initialize ничего не делает:

DSTATUS disk_initialize ( BYTE pdrv /* Physical drive nmuber to identify the drive */ ) { // already initialized in init() procedure return 0; }

… а сама инициализация выполняется вручную в процедуре init из Src/main.c:

int init() { // unselect all SPI devices first SDCARD_Unselect(); ST7735_Unselect(); // initialize SD-card as fast as possible, it glitches otherwise // (this is important if SPI bus is shared by multiple devices) int code = SDCARD_Init(); if(code < 0) { UART_Printf("SDCARD_Init() failed, code = %d\r\n", code); return -1; } /* ... */ }

Процедура disk_read читает с диска заданное количество блоков. FatFs позволяет использовать блоки размером от 512 до 4096 байт. По умолчанию используются блоки размером 512 байт, что как раз совпадает с размером блока у SD-карт. Реализация процедуры:

DRESULT disk_read ( BYTE pdrv, /* Physical drive nmuber to identify the drive */ BYTE *buff, /* Data buffer to store read data */ DWORD sector, /* Start sector in LBA */ UINT count /* Number of sectors to read */ ) { if(SDCARD_ReadBegin(sector) < 0) { return RES_ERROR; } while(count > 0) { if(SDCARD_ReadData(buff) < 0) { return RES_ERROR; } buff += 512; count--; } if(SDCARD_ReadEnd() < 0) { return RES_ERROR; } return RES_OK; }

Аналогично, всю запись на диск FatFs осуществляет через disk_write:

DRESULT disk_write ( BYTE pdrv, /* Physical drive nmuber to identify the drive */ const BYTE *buff, /* Data to be written */ DWORD sector, /* Start sector in LBA */ UINT count /* Number of sectors to write */ ) { if(SDCARD_WriteBegin(sector) < 0) { return RES_ERROR; } while(count > 0) { if(SDCARD_WriteData(buff) < 0) { return RES_ERROR; } buff += 512; count--; } if(SDCARD_WriteEnd() < 0) { return RES_ERROR; } return RES_OK; }

Процедура disk_ioctl позволяет выполнять синхронизацию данных с диском, если используется какое-то кэширование, узнать количество блоков, что требуется при выполнении форматирования (нужно включить FF_USE_MKFS), и делать некоторые другие вещи. В нашем случае достаточно подтверждать, что синхронизация данных была выполнена:

DRESULT disk_ioctl ( BYTE pdrv, /* Physical drive nmuber (0..) */ BYTE cmd, /* Control code */ void *buff /* Buffer to send/receive control data */ ) { if(cmd == CTRL_SYNC) { return RES_OK; } else { // should never be called return RES_ERROR; } }

Наконец, get_fattime возвращает текущее время. Поскольку мы определили FF_FS_NORTC, эта функция все равно не будет использована:

/* DWORD get_fattime(void) { return 0; } */

Ура, теперь можно ходить в файловую систему!

Первый пример: изучаем API

Следующий код демонстрирует монтирование файловой системы, получение информации о свободном месте, просмотр содержимого заданного каталога, а также чтение и запись текстовых файлов:

void UART_Printf(const char* fmt, ...) { char buff[256]; va_list args; va_start(args, fmt); vsnprintf(buff, sizeof(buff), fmt, args); HAL_UART_Transmit(&huart2, (uint8_t*)buff, strlen(buff), HAL_MAX_DELAY); va_end(args); } void init() { FATFS fs; FRESULT res; UART_Printf("Ready!\r\n"); // mount the default drive res = f_mount(&fs, "", 0); if(res != FR_OK) { UART_Printf("f_mount() failed, res = %d\r\n", res); return; } UART_Printf("f_mount() done!\r\n"); uint32_t freeClust; FATFS* fs_ptr = &fs; // Warning! This fills fs.n_fatent and fs.csize! res = f_getfree("", &freeClust, &fs_ptr); if(res != FR_OK) { UART_Printf("f_getfree() failed, res = %d\r\n", res); return; } UART_Printf("f_getfree() done!\r\n"); uint32_t totalBlocks = (fs.n_fatent - 2) * fs.csize; uint32_t freeBlocks = freeClust * fs.csize; UART_Printf("Total blocks: %lu (%lu Mb)\r\n", totalBlocks, totalBlocks / 2000); UART_Printf("Free blocks: %lu (%lu Mb)\r\n", freeBlocks, freeBlocks / 2000); DIR dir; res = f_opendir(&dir, "/"); if(res != FR_OK) { UART_Printf("f_opendir() failed, res = %d\r\n", res); return; } FILINFO fileInfo; uint32_t totalFiles = 0; uint32_t totalDirs = 0; UART_Printf("--------\r\nRoot directory:\r\n"); for(;;) { res = f_readdir(&dir, &fileInfo); if((res != FR_OK) || (fileInfo.fname[0] == '\0')) { break; } if(fileInfo.fattrib & AM_DIR) { UART_Printf(" DIR %s\r\n", fileInfo.fname); totalDirs++; } else { UART_Printf(" FILE %s\r\n", fileInfo.fname); totalFiles++; } } UART_Printf("(total: %lu dirs, %lu files)\r\n--------\r\n", totalDirs, totalFiles); res = f_closedir(&dir); if(res != FR_OK) { UART_Printf("f_closedir() failed, res = %d\r\n", res); return; } UART_Printf("Writing to log.txt...\r\n"); char writeBuff[128]; snprintf(writeBuff, sizeof(writeBuff), "Total blocks: %lu (%lu Mb); Free blocks: %lu (%lu Mb)\r\n", totalBlocks, totalBlocks / 2000, freeBlocks, freeBlocks / 2000); FIL logFile; res = f_open(&logFile, "log.txt", FA_OPEN_APPEND | FA_WRITE); if(res != FR_OK) { UART_Printf("f_open() failed, res = %d\r\n", res); return; } unsigned int bytesToWrite = strlen(writeBuff); unsigned int bytesWritten; res = f_write(&logFile, writeBuff, bytesToWrite, &bytesWritten); if(res != FR_OK) { UART_Printf("f_write() failed, res = %d\r\n", res); return; } if(bytesWritten < bytesToWrite) { UART_Printf("WARNING! Disk is full.\r\n"); } res = f_close(&logFile); if(res != FR_OK) { UART_Printf("f_close() failed, res = %d\r\n", res); return; } UART_Printf("Reading file...\r\n"); FIL msgFile; res = f_open(&msgFile, "log.txt", FA_READ); if(res != FR_OK) { UART_Printf("f_open() failed, res = %d\r\n", res); return; } char readBuff[128]; unsigned int bytesRead; res = f_read(&msgFile, readBuff, sizeof(readBuff)-1, &bytesRead); if(res != FR_OK) { UART_Printf("f_read() failed, res = %d\r\n", res); return; } readBuff[bytesRead] = '\0'; UART_Printf("```\r\n%s\r\n```\r\n", readBuff); res = f_close(&msgFile); if(res != FR_OK) { UART_Printf("f_close() failed, res = %d\r\n", res); return; } // Unmount res = f_mount(NULL, "", 0); if(res != FR_OK) { UART_Printf("Unmount failed, res = %d\r\n", res); return; } UART_Printf("Done!\r\n"); }

Как видите, интерфейс достаточно простой и отдаленно напоминает POSIX.

Второй пример: вывод картинки

Следующий пример парсит картинку в формате BMP (наглядное описание формата есть на Википедии) и отображает ее с помощью экранчика на базе ST7735:

int displayImage(const char* fname) { UART_Printf("Openning %s...\r\n", fname); FIL file; FRESULT res = f_open(&file, fname, FA_READ); if(res != FR_OK) { UART_Printf("f_open() failed, res = %d\r\n", res); return -1; } UART_Printf("File opened, reading...\r\n"); unsigned int bytesRead; uint8_t header[34]; res = f_read(&file, header, sizeof(header), &bytesRead); if(res != FR_OK) { UART_Printf("f_read() failed, res = %d\r\n", res); f_close(&file); return -2; } if((header[0] != 0x42) || (header[1] != 0x4D)) { UART_Printf("Wrong BMP signature: 0x%02X 0x%02X\r\n", header[0], header[1]); f_close(&file); return -3; } uint32_t imageOffset = header[10] | (header[11] << 8) | (header[12] << 16) | (header[13] << 24); uint32_t imageWidth = header[18] | (header[19] << 8) | (header[20] << 16) | (header[21] << 24); uint32_t imageHeight = header[22] | (header[23] << 8) | (header[24] << 16) | (header[25] << 24); uint16_t imagePlanes = header[26] | (header[27] << 8); uint16_t imageBitsPerPixel = header[28] | (header[29] << 8); uint32_t imageCompression = header[30] | (header[31] << 8) | (header[32] << 16) | (header[33] << 24); UART_Printf( "--- Image info ---\r\n" "Pixels offset: %lu\r\n" "WxH: %lux%lu\r\n" "Planes: %d\r\n" "Bits per pixel: %d\r\n" "Compression: %d\r\n" "------------------\r\n", imageOffset, imageWidth, imageHeight, imagePlanes, imageBitsPerPixel, imageCompression); if((imageWidth != ST7735_WIDTH) || (imageHeight != ST7735_HEIGHT)) { UART_Printf("Wrong BMP size, %dx%d expected\r\n", ST7735_WIDTH, ST7735_HEIGHT); f_close(&file); return -4; } if((imagePlanes != 1) || (imageBitsPerPixel != 24) || (imageCompression != 0)) { UART_Printf("Unsupported image format\r\n"); f_close(&file); return -5; } res = f_lseek(&file, imageOffset); if(res != FR_OK) { UART_Printf("f_lseek() failed, res = %d\r\n", res); f_close(&file); return -6; } // row size is aligned to 4 bytes uint8_t imageRow[(ST7735_WIDTH * 3 + 3) & ~3]; for(uint32_t y = 0; y < imageHeight; y++) { uint32_t rowIdx = 0; res = f_read(&file, imageRow, sizeof(imageRow), &bytesRead); if(res != FR_OK) { UART_Printf("f_read() failed, res = %d\r\n", res); f_close(&file); return -7; } for(uint32_t x = 0; x < imageWidth; x++) { uint8_t b = imageRow[rowIdx++]; uint8_t g = imageRow[rowIdx++]; uint8_t r = imageRow[rowIdx++]; uint16_t color565 = ST7735_COLOR565(r, g, b); ST7735_DrawPixel(x, imageHeight - y - 1, color565); } } res = f_close(&file); if(res != FR_OK) { UART_Printf("f_close() failed, res = %d\r\n", res); return -8; } return 0; }

Процедура в действии:

STM32: пример вывода картинки с SD-карты

Помимо прочего, приведенный код интересен тем, что он активно использует два SPI-устройства (карту и экранчик) на одной шине. Это позволило обнаружить все те интересные тонкости, о которых рассказывалось ранее. Кроме того, данный код использует процедуру f_lseek, которая оказалась нерабочей на exFAT в версии FatFs, предлагаемой STM32CubeMX. В последней версии FatFs с официального сайта этот баг уже исправлен.

Если что-то пошло не так

Все мы люди и делаем ошибки в коде. Поэтому я решил включить в статью команды для восстановления файловых систем в Linux:

# пересоздать FAT32 sudo mkfs.fat -F 32 /dev/mmcblk0p1 # пересоздать exFAT sudo mkfs.exfat /dev/mmcblk0p1

Проверить существующую файловую систему на ошибки:

# для FAT32 sudo fsck.fat -w -r -l -a -v -t /dev/mmcblk0p1 # для exFAT sudo fsck.exfat /dev/mmcblk0p1

Если вводить команды в консоли лень, обратите внимание на GUI-программу gparted. В этом контексте также хочу отметить, что FatFs понимает только таблицу разделов MBR. Так что, если будете чинить таблицу разделов, не создайте по привычке GPT.

Заключение

FatFs имеет свои странности. И речь не только о необычном форматировании кода или крайне неудобной отложенной инициализации устройства хранения данных. Возьмем для примера процедуру f_getfree. Она зачем-то принимает в качестве последнего аргумента указатель на указатель на структуру FATFS. Плюс к этому она имеет побочный эффект, который меняет поля структуры FATFS, которые не имеют прямого отношения к получению свободного места. Зачем было делать такой неудобный и очевидно чреватый ошибками интерфейс, мне решительно неясно. Еще более неясно, что он продолжает делать в библиотеке после стольких лет ее активной разработки. Давно можно было бы поправить!

Тем не менее, несмотря на названные странности, библиотека работает, и работает очень хорошо. С ее помощью вы успешно решите такие задачи, как запись на SD-карточку логов с показаниями датчиков, отображение картинок (скажем, каких-то элементов интерфейса), запись и воспроизведение звука в формате WAV, хранение файлов конфигурации, и так далее.

Полную версию исходников к посту, как обычно, вы найдете на GitHub.