STM32移植FatFs ff16:SD卡文件系统驱动实现与优化
简介面向STM32嵌入式开发者与学生压缩包提供基于最新版FatFsff16文件系统在SD卡存储场景下的完整移植方案核心涉及SDIO接口和DMA数据传输适合需要为设备增加文件读写、日志记录或数据管理功能的项目参考。包内共88个文件整体约1.25MB以C源文件与头文件为主体各35个另含启动汇编文件、Keil工程配置、调试配置及Hex等覆盖FatFs源码、STM32标准外设库、SD卡底层驱动和构建脚本目录结构清晰。已有90人下载学习。通过该工程读者可以梳理从SDIO初始化和DMA读写到FatFs挂载及文件操作的完整代码路径附带的版本历史文件可了解ff16升级点配合可直接编译的Keil工程快速验证并扩展至自身项目是一份兼具学习与工程参考价值的资源。1. 项目概述与方案选型为什么是FatFs ff16做嵌入式开发的朋友应该都有这种体会SD卡这东西硬件上无非四根线或者六根线但一旦牵扯到文件系统水就深了。我之前在一款基于STM32的数据采集设备上做本地存储最开始是用裸的SD读写驱动往固定扇区地址写数据。前期调试没问题等到要导出数据的时候电脑上读不了客户又要求按天生成日志文件最后老老实实上文件系统。我选的就是FatFs版本直接用最新的ff16。先说结论FatFs是小型嵌入式设备上最主流的FAT/exFAT文件系统方案专门为MCU这类资源受限环境设计ANSI C编写理论上任何超过20KB RAM的单片机都能跑。ff16这个版本在2024年初发布主要修了旧版本在exFAT卷上的一些边界问题同时优化了长文件名LFN缓冲区的使用策略。它不是什么革命性升级但作为新项目选型直接上ff16不会有坑社区维护也活跃。这个项目适合谁参考如果你手里有STM32、GD32、ESP32这类MCU想把日志、配置参数、采集数据存到SD卡里而且希望文件能直接在电脑上打开那这次移植过程完全可以直接抄作业。2. fatfs核心机制拆解文件系统层与底层驱动如何协作在动手改代码之前先把FatFs的架构搞清楚后面排查问题会快得多。FatFs分为三层这套分层思想值得记一下。2.1 三层架构应用层、文件系统层、底层驱动层应用层就是你自己调用的APIf_open、f_write、f_read、f_close这些。文件系统层是FatFs自身实现的FAT16/FAT32/exFAT协议负责维护目录项、FAT表、簇链这部分你不需要动直接用官方代码。底层驱动层是你需要自己实现的针对特定存储介质SD卡、Flash、U盘提供最基础的读写接口。打个比方文件系统层像图书馆的编目系统你只需要告诉管理员“我要借《xxx》”不用管这本书具体放在哪个书架哪一层。底层驱动层就是管理员找书的过程你的SD卡驱动负责找到数据所在的物理扇区然后通过SPI或SDIO把数据搬出来。2.2 ff16版本变化哪些新特性值得关注ff16相比ff15改动不算大但有几点实际影响移植策略。第一个是disk_ioctl的命令集补充了CTRL_TRIM配合FF_USE_TRIM配置项文件删除时可以向SD卡发送TRIM命令类似SSD的Trim机制。这对于使用eMMC或者带磨损均衡的SD卡来说能改善长期使用的写入性能。第二个改动在exFAT的目录项处理上修复了f_rename在跨目录移动时可能导致的文件名损坏问题。第三个是内部缓冲区的管理逻辑重构FF_FS_HEAP_BUF这个选项默认启用允许把部分缓冲区动态分配到堆上而不是全用静态数组。这意味着如果你的RAM紧张可以通过配置项把缓冲区从静态区挪到堆区腾出连续RAM空间。我实际测下来ff16在Cortex-M4上编译只开启基本FAT32支持、关闭LFN时代码体积在8KB左右RAM占用主要取决于你配置的扇区缓冲数。对于主流MCU来说这个开销完全在可接受范围内。3. SD卡底层驱动移植从SDIO到FatFs接口的桥接现在进入正题开始移植。我的目标平台是STM32F407 SDIO四线模式RTOS用的FreeRTOS。先说一个总原则FatFs本身是平台无关的移植工作90%都在实现disk.c里的六个函数。这六个函数就是文件系统层和硬件之间的桥。3.1 六个底层接口每个函数的功能与实现要点disk_status用来获取磁盘状态最简实现直接返回STA_NOINIT让系统认为设备未初始化每次调用f_mount都会强制执行disk_initialize。如果你的SD卡支持热插拔这里有检测引脚的话可以在这里把写保护状态返回给上层。DSTATUS disk_status(BYTE pdrv) { if (pdrv ! DEV_SD) return STA_NOINIT; return res; }disk_initialize是初始化入口这里要完成SD卡的完整上电流程。以SDIO模式为例从发送CMD0进入空闲态开始依次发送CMD8验证电压范围、ACMD41协商容量类型、CMD2获取CID、CMD3获取RCA然后到CMD7选中卡最后设置4位总线宽度和高速模式。这个过程建议用状态机的思路写因为SD卡初始化时对时序有要求不能一上来就高速访问。3.2 disk_read和disk_write多扇区传输的正确姿势这两个函数是数据通路的核心。FatFs在读写文件时会尽量按连续扇区批量操作所以你看到disk_read函数的参数是BYTE *buff, LBA_t sector, UINT count其中count可能一次就是128或者256。如果你的底层驱动一次只能读一个扇区那就用一个循环把多个单扇区操作串起来但性能会打折扣。注意如果buff是4字节对齐的可以直接把DMA的目标地址指向buff如果不对齐就得先读到一个中间缓冲区再memcpy过去。我在F407上踩过这个坑DMA访问非对齐地址时数据会错乱排查了大半天。多扇区写入时SD卡规范要求每写一个扇区要等卡内部完成编程操作SDIO模式下可以通过等待SDIO_STA_MSD这个状态位实现。这里有个经验值写入性能的瓶颈往往不在SPI/SDIO总线速率而在于卡的内部Flash编程时间和FAT表的更新频率。FatFs的f_sync每次都会更新FAT表所以频繁调用f_sync会显著拖慢整体写入速度。每写几个扇区调一次或者干脆在写完一批数据后再调性能差距非常大。3.3 disk_ioctl和get_fattime两个容易被忽略的接口disk_ioctl处理各种控制命令最核心的是三个GET_SECTOR_COUNT返回SD卡总扇区数GET_SECTOR_SIZE返回扇区大小SD卡固定512字节CTRL_SYNC在写操作完成后调用用来确保数据真正落盘。如果你的代码跑在RTOS上还要实现MMC_GET_CSD用于某些优先判断卡容量类型的场景。DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { switch (cmd) { case GET_SECTOR_COUNT: *(LBA_t *)buff sd_get_sector_count(); return RES_OK; case GET_SECTOR_SIZE: *(WORD *)buff 512; return RES_OK; case CTRL_SYNC: sd_wait_ready(); return RES_OK; default: return RES_PARERR; } }get_fattime返回当前时间戳文件创建和修改时间都会用到。这个函数返回的是一个32位整数格式是固定的bit31~25存年份从1980起算bit24~21存月份bit20~16存日期以此类推。如果你的系统没接RTC直接返回一个固定时间比如2024年1月1日文件也能正常读写只是时间戳不准。我习惯把RTC的时间读出来在这个函数里拼装一下。3.4 RTOS环境下加锁多线程访问SD卡的正确姿势如果你的系统用了FreeRTOS强烈建议在所有底层接口里加互斥锁。原因很简单文件系统的API调用不是原子操作比如f_write可能被拆分多次底层写如果两个任务同时操作文件底层驱动没有锁保护的话FAT表和文件数据会互相踩踏用不了多久卡上的目录结构就乱了。FatFs其实内置了这个机制ffconf.h里有FF_FS_REENTRANT选项打开后系统会调用你提供的ff_mutex_create/take/give接口。我是在ffconf.h里定义FF_FS_REENTRANT 1然后实现这几个互斥操作函数底层驱动里只需要在每次操作SDIO前take锁操作完释放就行。实测两个任务分别写不同文件运行三天没有出现文件系统损坏。4. ffconf.h配置详解影响性能与功能的20个关键选项ffconf.h是FatFs的全局配置头文件它决定了文件系统的功能边界和资源占用。这个文件如果你不去动它默认配置可以跑通FAT12/16/32的读写但exFAT、长文件名、中文编码这些统统没有。我给你列几个必调的项。FF_USE_LFN长文件名支持。默认是0关必须改成2。改成2的意思是允许在栈上动态分配LFN缓冲区不需要额外的静态buffer。如果你的工程栈比较小改成1用静态缓冲区也行多占几百字节RAM。FF_USE_MKFS格式化支持。默认0是关闭的如果你希望程序启动时自动把空白SD卡格式化成FAT32需要打开这个选项同时把FF_USE_STRFUNC也打开才能在格式化时指定卷标。FF_FS_EXFATexFAT支持。SD卡容量超过32GB时出厂默认格式就是exFAT。如果你的设备要插这种卡必须打开否则f_mount会返回FR_NO_FILESYSTEM。FF_VOLUMES卷数量默认1。如果你的板子上既有SD卡又有SPI Flash都挂FatFs这里改成2。FF_MIN_SS和FF_MAX_SS扇区大小范围。SD卡是512字节这俩都填512。如果你可能用4KB扇区的Flash模拟U盘可以让FF_MIN_SS512, FF_MAX_SS4096让FatFs在挂载时自动探测。FF_USE_TRIM删除优化前面提到过配合ff16的CTRL_TRIM命令。建议打开对eMMC这种有FTL的介质有实际正面效果。经验值FF_USE_LFN和FF_FS_EXFAT一打开代码体积会膨胀不少。F407这种256KB Flash无所谓但如果你在STM32F103C8T664KB Flash上做就要考虑裁剪。实测F103 FatFs LFN代码在20KB左右还能接受。5. 完整移植实操流程从源码下载到跑通Demo下面按顺序过一遍整个移植流程这部分的顺序特别重要很多人挂载失败就是因为初始化顺序错了。5.1 源码准备与工程目录结构从FatFs官网下载ff16源码包解压后拿到source目录里的几个文件ff.c、ff.h、ffconf.h、diskio.c、diskio.h。其中ff.c和diskio.c放到工程里编译ffconf.h是配置diskio.h是接口头文件。我习惯的工程结构是app/fs/放ff.c、ff.h、ffconf.hbsp/sd/放sdio驱动、sd卡协议层代码bsp/sd/diskio.c放FatFs底层适配5.2 修改ffconf.h做一个最小可用配置直接把官方默认的ffconf.h打开逐项改我上面提的几个开源。我的一份最小配置长这样#define FF_USE_LFN 2 #define FF_USE_MKFS 1 #define FF_USE_STRFUNC 1 #define FF_FS_EXFAT 1 #define FF_VOLUMES 1 #define FF_MIN_SS 512 #define FF_MAX_SS 512 #define FF_USE_TRIM 1其他保持默认。注意FF_USE_STRFUNC打开以后f_printf函数可用调试日志写文件会非常方便。5.3 实现diskio函数以SDIO驱动对接最佳实践把diskio.c里的六个函数逐个填上。这一部分就是前面第3节讲的内容按那六个函数依次实现。重点说一下初始化时序调用disk_initialize的时候如果返回STA_NOINITFatFs会在f_mount时退出并返回FR_NOT_READY。很多时候排错发现mount失败问题不在代码逻辑而是SD卡上电后没等电源稳定就急着发CMD0直接导致卡不响应。硬件上SD卡供电建议用独立的LDO初始化前加个10ms延时成功率会高很多。5.4 主程序调用mount、格式化、读写测试挂载文件系统是第一步这一步做对了后续就顺畅了。FATFS fs; FRESULT res; res f_mount(fs, 0:, 1); if (res FR_NO_FILESYSTEM) { // 新卡没有文件系统格式化 res f_mkfs(0:, NULL, 0, work_buf, sizeof(work_buf)); if (res ! FR_OK) { printf(mkfs failed: %d\n, res); return -1; } res f_mount(fs, 0:, 1); }这里有一个容易被忽略的细节f_mkfs需要的work_buf大小官方建议是FF_MAX_SS的两倍即1KB。如果你的栈不够声明成全局数组。格式化完成后直接测试写文件FIL fil; UINT bw; res f_open(fil, 0:test.txt, FA_CREATE_ALWAYS | FA_WRITE); if (res FR_OK) { f_write(fil, Hello FatFs ff16!\r\n, 19, bw); f_sync(fil); f_close(fil); }然后插到电脑上看test.txt内容是否正确。这一步验证通过整个移植工作就基本完成了。6. 常见问题排查与性能调优移植过程中的坑主要集中在下面几个点我直接按现象、原因、解决方案列成表格方便你对照排查。现象可能原因解决方案挂载失败FR_NOT_READYSD卡未初始化成功或供电不稳检查CMD0响应上电延时10ms以上确认卡供电正常挂载失败FR_NO_FILESYSTEM卡上没有有效FAT分区或格式不受支持运行f_mkfs格式化确认exFAT配置已打开打开文件失败FR_NOT_ENABLED驱动崩溃或disk_status一直返回STA_NOINIT检查disk_status返回值在f_mount后调用f_getfree验证文件系统句柄读写正常但断电后文件损坏未调用f_sync或频繁突然断电在关键写入点调用f_sync或增加掉电检测并及时落盘数据出现错乱DMA缓冲区未对齐确保读写缓冲区4字节对齐检查DMA传输方向写入性能特别低FAT表频繁更新增大单次写入块大小减少f_sync频率6.1 关于性能提升的几个实测建议开启FF_FS_TINY可以在小RAM环境下减少缓冲占用但会牺牲一点性能。尽量使用f_write一次写大块数据而不是多次小数据写入。使用f_lseek配合FA_WRITE时可以按固定大小追加避免FAT表碎片化。如果数据量很大建议写满一个文件就关闭再开新文件比在一个文件里不断追加要稳定。6.2 最后提一个经常被忽略的点文件读写没问题的前提下再确认一下卡的热插拔逻辑。没有热插拔检测的板子用户在外电正常工作时拔卡很容易出现数据写入一半的情况。我后来在处理这个需求时加了一个电压检测引脚检测到掉电后立即把当前没写完的缓冲通过f_sync落盘然后把disk_status置为STA_NOINIT禁止后续写入。这个逻辑虽然简单但对数据完整性帮助很大。如果你做的也是需要长时间稳定记录数据的设备这个细节值得抄下来。本文还有配套的精品资源点击获取