Работа с FAT32 и exFAT с помощью библиотеки FatFs
13 июня 2018
В рамках статьи Учимся работать с 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 подключается к проекту следующим образом. Качаем исходники библиотеки и распаковываем их в отдельный каталог:
+-- 00history.txt
+-- 00readme.txt
+-- diskio.c
+-- diskio.h
+-- ff.c
+-- ffconf.h
+-- ff.h
+-- ffsystem.c
+-- ffunicode.c
+-- integer.h
Makefile правим следующим образом. В C_SOURCES добавляем:
fatfs/diskio.c \
fatfs/ffunicode.c \
Также дописываем в C_INCLUDES:
Настройка библиотеки осуществляется через fatfs/ffconf.h. Настроек довольно много, но они хорошо документированы. Основные настройки, которые имеет смысл изменить, следующие.
Включаем поддержку длинных имен файлов (LFN, Long File Name). Эта опция необходима для exFAT, который в свою очередь нужен для работы с картами SDXC. Другими словами, если тип карты заранее неизвестен, или планируется использовать карты размером больше 32 Гб, включаем:
Чтобы не греть мозг с кодировками и локалями, включаем поддержку UTF-8:
Поддержка exFAT:
В этом проекте я не использовал часы реального времени. Так как текущее время прошивке неизвестно, говорим FatFs использовать для всего захардкоженное время:
#define FF_NORTC_MON 1
#define FF_NORTC_MDAY 1
#define FF_NORTC_YEAR 2018
Далее нужно отредактировать fatfs/diskio.c. Здесь объявляются процедуры, определяющие, где физически библиотека будет искать данные, и прочее в таком духе.
Процедура disk_status возвращает текущий статус диска. В нашем случае она всегда будет возвращать успех:
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
return 0;
}
Процедура disk_initialize вызывается при инициализации диска. Как ни странно, в нашем случае она тоже будет просто возвращать успех. Связано это вот с чем. Как было объяснено в прошлой статье, SD-карта должна быть либо первым инициализируемым устройством на SPI-шине, либо использовать отдельную шину. К сожалению, FatFs производит отложенную инициализацию. Другими словами, мы не можем сказать точно, в какой момент она произойдет. Более того, момент этот будет меняться с изменением основного кода. Поэтому, чтобы гарантировать, что SD-карта инициализируется сразу после запуска прошиви, disk_initialize ничего не делает:
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
// already initialized in init() procedure
return 0;
}
… а сама инициализация выполняется вручную в процедуре init из Src/main.c:
// 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-карт. Реализация процедуры:
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:
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), и делать некоторые другие вещи. В нашем случае достаточно подтверждать, что синхронизация данных была выполнена:
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
Следующий код демонстрирует монтирование файловой системы, получение информации о свободном месте, просмотр содержимого заданного каталога, а также чтение и запись текстовых файлов:
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:
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;
}
Процедура в действии:
Помимо прочего, приведенный код интересен тем, что он активно использует два SPI-устройства (карту и экранчик) на одной шине. Это позволило обнаружить все те интересные тонкости, о которых рассказывалось ранее. Кроме того, данный код использует процедуру f_lseek, которая оказалась нерабочей на exFAT в версии FatFs, предлагаемой STM32CubeMX. В последней версии FatFs с официального сайта этот баг уже исправлен.
Если что-то пошло не так
Все мы люди и делаем ошибки в коде. Поэтому я решил включить в статью команды для восстановления файловых систем в Linux:
sudo mkfs.fat -F 32 /dev/mmcblk0p1
# пересоздать exFAT
sudo mkfs.exfat /dev/mmcblk0p1
Проверить существующую файловую систему на ошибки:
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.
Метки: STM32, Электроника.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.