В рамках статьи Учимся работать с 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 n Root directory: r n » ) ;
for ( ;; ) {
res = f_readdir ( & dir , & fileInfo ) ;
if ( ( res != FR_OK ) || ( fileInfo. fname [ 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 ] = ‘ ‘ ;
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 .