简介FATFS R0.14b是轻量级嵌入式文件系统模块的官方源代码包由Mizuki Chinen维护发布于2021年4月。该版本面向STM32等各类微控制器平台提供标准FAT文件系统读写能力适合需要在资源受限设备上实现文件存储功能的嵌入式开发者。资源包共89个文件压缩后约1.84MB其中主要包含ff.c、ff.h等核心源码diskio.c等驱动适配层接口以及配套的HTML说明文档和PNG图示便于开发者快速查阅API和移植步骤。已有610人学习下载。包内除完整源代码外还附带LICENSE、历史更新记录和配置示例可帮助读者理解FAT12/16/32的选择、长文件名支持等关键参数快速完成驱动适配与系统集成。1. 从FATFS R0.14b说起一个稳定分支凭什么能直接进量产“FATFS文件系统源代码 版本R0.14b (April 17, 2021)”这行字在嵌入式圈子里几乎就是“量产级文件系统”的代名词。它不是拿来演示的示例工程而是做SD卡数据记录、设备日志、OTA升级包时默认能带上的那套底层代码稳定、接口清晰、出错能查。R0.14b这个版本号值得你注意的是它在2021年4月17日之后对外接口就再没大动过新项目拿它当底座后面换版本的成本很低。这篇文章不打算复述文档而是把源码包里每个文件干什么、ffconf.h里哪些开关真正要改、从解压到上板怎么走一遍、最容易翻车的几个点在哪一次说清楚。适合正在裸机或RTOS上做文件存储的开发者也适合还在犹豫要不要采用这套方案的人。2. 源码包布局ff.c、diskio.c和ffconf.h之间的分工2.1 包内文件分工真正的移植面比你想象的窄R0.14b的压缩包解压后根目录文件并不多先给每个文件定个位你才知道哪些要改、哪些碰都不要碰。文件角色移植时是否改动ff.cFAT文件系统核心实现FAT12/16/32和exFAT都在里面不改ff.h数据类型、API声明、FRESULT错误码只读ffconf.h功能裁剪与行为配置主要改动项ffunicode.cUnicode与代码页转换表按需参与编译diskio.c磁盘I/O封装包内是模板全部重写diskio.h底层接口声明按需修改integer.h基础整数类型定义很少动option目录各种代码页转换实现按需添加diskio.c真正要你实现的其实只有四个基础函数加一个时间回调disk_initialize、disk_status、disk_read、disk_write、disk_ioctl外加get_fattime。很多工程师第一次移植时以为要进ff.c里改逻辑才能适配新盘这是最大的误解。ff.c是完整且自洽的状态机你的硬件差异全在diskio这一层被隔离掉了。我一般会建议先把integer.h和ff.h里的类型定义扫一遍重点看LBA_t它决定扇区地址是32位还是64位这直接影响你写disk_read时形参该用多宽的类型。2.2 ff.c内部的对象与卷一次f_open调用的完整旅程往下写代码前至少要明白f_mount和f_open到底做了什么。f_mount做的事是读引导扇区DBR解析BPB参数每扇区字节数、每簇扇区数、保留扇区数、FAT表个数、根目录起始位置。这些参数存在FATFS对象里之后的读写全依赖它。f_open会从根目录开始按路径逐级读取目录项每个目录项32字节里面存文件名、属性、起始簇号、文件大小和时间戳。找到目标后ff.c把目录项内容填进FIL结构体文件才算打开。如果要新建文件还得在目录区找空位再从FAT表分配第一个簇。这里最值得记住的换算公式是文件数据区的某个簇号换算成扇区号等于数据区起始扇区加上簇号减2乘以每簇扇区数。这个公式解释了为什么碎片化会拖慢读写同一文件的扇区散开后底层寻址时间变长FAT表也要频繁读取更新。FATFS为了省内存FAT表是按需加载到缓存里的每跨一次簇就可能要重新读表所以连续分配对性能的意义很大。理解了这条链路后面看FF_USE_FASTSEEK、FF_USE_EXPAND这类开关你才明白它们到底是补哪块短板的。2.3 接口约定改配置、加文件、编进工程的三件事把源码放进工程时有三个容易出错的地方提前处理能省半天。第一R0.14b开始所有配置宏都加了FF_前缀如果老项目里还留着_USE_LFN、_FS_EXFAT这种旧写法新旧宏同时存在会直接编译报错。拿到新包后搜一遍你的工程代码把旧宏全清掉别让它们混战。第二路径分隔符用的是斜杠而不是Windows习惯的反斜杠f_open(0:/sys/log.bin)是合法写法反斜杠在部分配置下会被当成非法字符。第三FF_USE_LFN开到1时ff.c内部工作缓冲在栈上分配小栈MCU容易无征兆跑飞。遇到莫名死机先看栈用量这是血泪经验后面章节还会详说。3. 移植前必调的八个开关ffconf.h从哪个宏下手3.1 FF_USE_LFN与FF_LFN_UNICODE长文件名和中文路径的稳定搭法ffconf.h里有几十个宏但真正决定项目命运的第一个就是长文件名开关。FF_USE_LFN取三个值0表示只用短文件名8.3格式1表示启用长文件名但工作缓冲在栈上2表示启用长文件名且缓冲由malloc分配。栈小的MCU慎用1因为FF_MAX_LFN默认255配套的工作缓冲会吃掉不少栈空间。RTOS里任务栈只有1到2KB时建议用2配合堆但前提是你的堆稳定、分配函数线程安全。与它成对的是FF_LFN_UNICODE这个决定你调用API时文件名用什么编码。0是ANSI/OEM单字节1是UTF-162是UTF-8。对中文路径来说0几乎必乱码1适合协议层直接用宽字符的场景2适合日志系统里到处是char字符串的工程。我见到的量产项目里最多见的组合是FF_USE_LFN2配FF_LFN_UNICODE2既能用中文长文件名又不破坏现有代码的字符串处理习惯。开这两项后ffunicode.c必须加入编译否则链接阶段会报缺转换函数。提示FF_LFN_UNICODE2时源文件里的中文字符串常量要保证编译器按UTF-8输出。有些老旧IDE默认按GBK保存源码生成的字符序列和UTF-8不一致文件系统里存出来的名字就会和预期不同。3.2 FF_FS_EXFAT与FF_LBA64大容量卡和4K逻辑扇区一起处理如果你的存储介质是SDXC卡或者你打算用f_mkfs直接格式化成exFAT卷FF_FS_EXFAT必须设为1。它有几个连锁前提FF_LFN_UNICODE不能是0编译器要支持64位整数类型如果开了FF_FS_REENTRANT还需要配好同步层。这些条件缺一个编译期就会报错这是R0.14b有意做的保护不算坑。FF_LBA64决定LBA_t的宽度0是32位1是64位。exFAT卷以及超过2TB容量的分区都可能超出32位扇区编号这种场合必须开。但别把它和exFAT强绑定普通512字节扇区、几十GB的卡FF_LBA64保持0反而省一点计算开销。更常见的坑在扇区大小上——FF_MIN_SS和FF_MAX_SS控制扇区适配范围。大量NVMe模拟U盘、部分eMMC设备逻辑扇区其实是4096字节而不是512。两个宏都锁死在512时f_mount返回的可能不是成功而是FR_NO_FILESYSTEM这类玄学错误。正确做法是把FF_MAX_SS设为4096并在disk_ioctl里实现GET_SECTOR_SIZE让ff.c启动时主动探测实际扇区尺寸。3.3 FF_USE_FASTSEEK、FF_FS_LOCK与重入开关性能和RAM的交换FF_USE_FASTSEEK1时f_lseek可以用一张簇映射表完成跳转适合音频播放、数据检索这类随机访问多的应用。代价是你要自己提供一个DWORD数组数组大小和文件占用的簇数相当文件越大越费RAM。纯顺序写的日志项目开它意义不大。FF_FS_LOCK是数值型开关表示系统最多同时打开多少个文件对象默认0表示不启用文件锁。开了它以后每个打开的文件对象都占一个队列项同一卷下的打开操作会严格互斥。单任务裸机项目保持0更省内存但RTOS里多任务同时写日志和读配置时建议开到2到4同时把FF_FS_REENTRANT设为1让ff.c内部操作也走互斥。这几个开关单独看都简单组合起来就影响内存布局和代码段大小。我的习惯是先按需求开最小集合跑通基础读写后再逐个加功能不要一开始全开否则出了问题根本不知道是哪部分引入的。4. 从压缩包到上板运行最小移植步骤与自测顺序4.1 先写四个底层函数diskio.c的骨架和参数说明移植的核心工作在diskio.c。下面这份骨架按单卡、普通扇区读写的场景组织实际驱动函数名换成你自己SD驱动里的实现即可。/* diskio.c 移植骨架 */ #include ff.h #include diskio.h #include sd_driver.h /* 假设你已经写好的SD卡驱动 */ DSTATUS disk_initialize(BYTE pdrv) { if (pdrv ! 0) return STA_NOINIT; /* 本工程只有盘0 */ if (sd_init() 0) return 0; /* 成功则清状态位 */ return STA_NOINIT; } DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { if (pdrv ! 0) return RES_PARERR; if (sd_read_sectors(sector, buff, count) 0) return RES_OK; return RES_ERROR; } DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) { if (pdrv ! 0) return RES_PARERR; if (sd_write_sectors(sector, buff, count) 0) return RES_OK; return RES_ERROR; }这段代码的要点在接口对齐pdrv是盘号ff.c在配置了多卷时会传入0到FF_VOLUMES-1buff是读写缓冲它可能是512字节也可能是4096字节取决于你的FF_MAX_SS配置。这里还必须留意对齐问题SDIO控制器做DMA时通常要求4字节或更高对齐如果buff是普通局部数组必要时得声明成对齐的全局数组或用malloc分配。disk_ioctl里最常被忽略的是GET_BLOCK_SIZE它返回底层擦除块大小FATFS做分配对齐时会用到。如果你拿不准先返回1也能跑但性能不是最优。get_fattime必须实现除非在ffconf.h里开了FF_FS_NORTC否则ff.c在创建文件或更新目录项时会调用它生成时间戳。DWORD get_fattime(void) { /* bit31:25 年bit24:21 月bit20:16 日 */ /* bit15:11 时bit10:5 分bit4:0 秒/2 */ return ((DWORD)(2024 - 1980) 25) | ((DWORD)4 21) | ((DWORD)17 16) | ((DWORD)9 11) | ((DWORD)30 5); }很多板子没接RTC又不愿意开FF_FS_NORTC结果时间戳全变成1980年。开FF_FS_NORTC1后ffconf.h里的FF_NORTC_YEAR、FF_NORTC_MONTH、FF_NORTC_DAY会自动填充连这个函数都可以不用认真写。我建议手头时间源不稳的先走FF_FS_NORTC这条路避免文件创建时间不可控。4.2 挂载与写入的最小流程f_mount到f_sync的一段代码底层函数就位后应用侧的挂载逻辑可以写成下面这样这是几乎所有日志类项目的最小雏形。FATFS fs; FIL fil; FRESULT res; UINT bw; BYTE buf[512]; res f_mount(fs, , 1); /* 挂载默认盘最常用 */ if (res ! FR_OK) { /* 打印错误码正常情况下FR_OK0 */ } res f_open(fil, log2024.bin, FA_WRITE | FA_CREATE_ALWAYS); if (res ! FR_OK) { /* 创建失败查目录是否满、文件名是否合法 */ } res f_write(fil, buf, sizeof(buf), bw); if (res ! FR_OK || bw ! sizeof(buf)) { /* 写不完整或返回错误查底层IO */ } res f_sync(fil); /* 把缓冲刷到卡里 */ f_close(fil);f_mount的第三个参数opt为1时表示挂载的同时立即读取卷信息为0时只是注册FATFS对象之后第一次访问文件时才真正读取。如果你的卡上电初始化比较慢opt1可能返回FR_NOT_READY这不是卡坏了而是调用时卡还没准备好。量产代码里一般会写成轮询失败后延时几百毫秒再调一次最多尝试三次。f_sync是关键操作它把用户缓冲里的数据写回存储介质并同步更新FAT表和目录项。写日志类应用如果每写一帧就f_sync一次卡会被频繁FAT更新拖慢如果一直不sync掉电就会丢数据。正确节奏是把f_sync放在检查点比如每积累16KB数据或者在写完一个完整文件分块后调用。4.3 上板后的自测顺序四次验证把移植钉死代码能编译通过只是开始上板验证要按顺序做跳过任何一步都可能把隐患留到现场。测试操作期望结果底层IO只调disk_read读扇区0打印前512字节最后两字节是0x55 0xAA挂载调用f_mount打印返回值返回FR_OK格式化调用f_mkfs用FAT32格式重建卷返回FR_OK电脑能识别写读回写10KB随机数据关闭后重新打开读出逐字节比对一致格式化这一步用R0.14b的新签名和旧版本差别很大MKFS_PARM mopt; BYTE work[4096]; mopt.fmt FM_FAT32 | FM_SFD; /* 强制FAT32单分区 */ mopt.n_fat 0; /* FAT表个数0表示自动 */ mopt.align 0; mopt.n_root 0; mopt.au_size 0; res f_mkfs(, mopt, work, sizeof(work));MKFS_PARM里的fmt是格式类型FM_FAT32表示强制FAT32FM_SFD表示不用分区表直接建文件系统。n_fat、align、n_root、au_size都填0时由ff.c按卷大小自动选合理值绝大多数SD卡场景够用。work缓冲建议给4096字节太小时格式化可能返回FR_MKFS_ABORTED。5. FATFS使用中的四个经典坑现象、原因和排查套路5.1 f_mount永远返回FR_NO_FILESYSTEM现象卡在电脑上读得好好的格式也是FAT32板子上f_mount就是返回FR_NO_FILESYSTEM换两张卡都一样。原因大概率不是文件系统坏了而是扇区尺寸配置和卡的实际逻辑扇区不匹配。卡出厂逻辑扇区不一定都是512字节部分大容量SDXC和U盘出厂就是4KB逻辑扇区。另一种常见原因是底层disk_read读出来的数据字节错位SPI模式下时序不对会整体偏移DBR签名自然对不上。解决先把FF_MAX_SS改成4096保证ff.c能探测真实扇区大小然后在disk_read里写一个针对扇区0的调试函数上电就把前512字节通过串口dump出来人工确认0x55AA出现在最后两字节同时观察F8 FF FF FF这类FAT镜像标记是否在正确偏移。这两步做完FR_NO_FILESYSTEM能解决掉八成。5.2 写文件速度忽快忽慢FAT表更新和簇对齐现象写入64KB数据前面很快后面突然卡一下或者写几百个文件后写速度明显下降。原因文件碎片多了以后每写一个簇都要读FAT表找下一个空闲簇FAT表本身还要回写如果文件分布在不相邻的簇SD卡底层寻址时间也会拉长。另一个隐藏问题是写缓冲不满就落盘例如512字节缓冲只写满342字节就触发写操作底层Flash或SD卡要做读-改-写速度立刻下降。解决写日志时尽量按簇大小的整数倍来组织缓冲区比如每簇16KB就把缓冲凑满16KB再写写完一个完整数据块后再考虑f_sync。对提前知道大小的文件可以在创建后立即用f_expand预分配连续空间避免写的过程中反复找簇。这两个改动立竿见影比盲目调FF_USE_FASTSEEK有效得多。5.3 文件名乱码FF_USE_LFN和FF_LFN_UNICODE错配现象在电脑上创建的“测试.bin”文件板子上f_open怎么也打不开或者f_readdir读出来的文件名显示成乱码。原因电脑上文件名编码和板子上配置的编码不一致。FF_USE_LFN0时底层只有短文件名中文名根本没法表示FF_LFN_UNICODE0时API层按单字节处理UTF-8的中文按字节拆开后自然对不上目录项里的UCS-2内容。解决统一到UTF-8链路。ffconf.h里设FF_USE_LFN2、FF_LFN_UNICODE2ffunicode.c加入工程。代码里的文件名数组用UTF-8编码的字符串字面量编译器也按UTF-8输出。还要注意FF_MAX_LFN默认255字符中文按UTF-8算每个字占3字节理论上最长85个汉字实际命名别贴边。5.4 掉电后文件大小对但内容有洞现象设备突然断电重新上电后文件还在大小看起来也对但读到中间有一段是旧数据或全零。原因文件大小字段和实际数据落盘不是原子的。掉电瞬间目录项里的文件大小可能已经更新但对应的FAT表和用户数据还没来得及写回另外f_write的缓冲还停留在RAM里没有刷到介质上。FATFS本身没有事务日志机制这种不一致靠代码习惯来规避。解决把f_sync放在每一个可恢复的检查点之后比如写完一帧完整数据就sync一次对特别关键的记录用双份文件交替写写坏一份还有另一份兜底启动时做一次目录扫描发现异常文件先改名隔离不让它参与后续写入。这个习惯能让你在客户现场少跑很多趟。6. 量产前最后一步掉电恢复、RTOS共享与收尾习惯6.1 掉电自恢复状态标记文件比想象中好用掉电保护的经典做法是在固定路径放一个状态标记文件。开始写数据前先创建或更新标记文件内容写入本次记录的状态机编号数据全部写完并f_sync后再清除标记。设备上电启动时扫描这个文件如果标记存在说明上次记录没正常结束这时做两件事把不完整的日志文件改名加后缀_bad然后重新创建新文件继续记录。这样用户拿回来的卡始终能看到完整的文件列表不会出现一个打开就报错或读到一半崩溃的“洞文件”。6.2 多任务共享FF_FS_REENTRANT下的同步层写法RTOS里多个任务同时访问FATFS必须开FF_FS_REENTRANT1。这时ff.h要求你实现四个同步函数ff_cre_syncobj创建信号量ff_del_syncobj删除信号量ff_req_grant加锁ff_rel_grant解锁。常见做法是直接用RTOS的互斥量包一层int ff_req_grant(FF_SYNC_t sob) { /* sob是对应卷的互斥量句柄 */ return mutex_lock(sob, 1000) 0; } void ff_rel_grant(FF_SYNC_t sob) { mutex_unlock(sob); }加锁的时间片要设置合理。ff_cre_syncobj会在f_mount时被调用每个卷对应一把锁多卷项目记得给FF_VOLUMES配够数组。这里最容易翻车的是在中断上下文里调f_write——FATFS不是中断安全的中断里读写文件轻则死锁重则堆栈损坏任务里用队列把写请求转出去才是正路。我现在的习惯是每个用FATFS的项目固定留一个调试入口通过串口命令手动触发“挂载、建文件、写满、回读、卸载”五个动作新硬件回来先跑这一套跑通了再往上叠业务逻辑。这个习惯帮我挡掉过不少底层时序问题。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?