FATFS R0.14b源码详解:STM32上SD卡与SPI Flash文件系统移植实践
2026/9/11 8:34:36 网站建设 项目流程

简介:面向嵌入式开发者的FATFS文件系统R0.14b完整源代码包,适合STM32及其他ARM平台项目中需要为存储介质提供FAT读写能力的场景。该版本在可移植性与内存占用上做了优化,通过diskio驱动适配层屏蔽硬件差异,开发者只需编写底层扇区读写函数即可完成集成,并可根据需要裁剪FAT12/16/32、长文件名等功能选项。包体共89个文件,压缩后仅1.84MB,其中以10个C源文件与3个头文件组成的源码主体为核心,附带53个HTML帮助文档、16个PNG示意图、配置与更新说明等,便于对照学习与快速查阅。源码包内含完整目录结构,区分文档、资源与源代码模块,并保留LICENSE等授权信息,有助于理解驱动层、文件系统层和配置接口之间的关系,缩短移植与调试周期。当前已有603人学习下载,适合希望深入掌握嵌入式文件系统原理或正在实施存储相关功能的开发者。 我在一个量产的项目里连续写了几年 STM32 的存储模块,凡是涉及 SD 卡、SPI Flash 数据记录的设备,基本都绕不开 FATFS 这套文件系统。上一批设备的固件里用的就是 FATFS R0.14b,发布于 2021 年 4 月 17 日。这个版本到今天也不算新,但依旧是很多嵌入式工程师的默认选择——原因很简单:它足够稳定,代码结构干净,资源占用可控,踩过的坑大多有现成答案。如果你手里正有一颗 STM32 或者类似的 MCU,想接一张 SD 卡或者 SPI Flash 做点数据记录,迟早要跟这套源码打交道。这篇我就从源码角度把 R0.14b 掰开揉碎讲一遍,包括版本定位、各层代码怎么协作、移植到实际硬件的完整流程,以及我在这过程中总结出来的几个典型问题和排查思路。

1. 为什么 R0.14b 这颗老版本依然值得细读

先聊版本。FATFS 是 ChaN 开发的开源 FAT 文件系统模块,面向小型嵌入式系统,主要支持 FAT32、FAT16、FAT12,从某个版本开始支持 exFAT。R0.14b 是 2021 年 4 月的维护版本,这个版本本身的定位很明确:在 R0.14a 的基础上修复边界问题,尤其是 exFAT 相关的文件命名、时间处理和长分区处理。也就是说,它不是那种大改架构的版本,而是把上一版引入的功能打磨稳定的版本。

对做产品的工程师来说,这种"刚稳定下来的版本"往往比最新版本更有吸引力。最新版本可能功能更多,但也会带来新的配置项和行为变化;R0.14b 则处于一个相当不错的平衡点:exFAT 和 64 位 LBA 支持已经可用,同时整个模块的代码量和内存占用仍然维持在很低的水平。官方文档里给的资源占用参考是 ROM 大约 30KB 左右,RAM 取决于缓冲区和长文件名配置,最小可以压到几百字节。对于大多数 MCU 工程来说,这个开销完全在可接受范围内。

还有一个原因值得强调:R0.14b 的配置宏体系已经非常成熟。你打开ffconf.h就会看到从FF_USE_MKFSFF_FS_LOCK一整套完整的裁剪选项,几乎每个模块都能单独开关。这意味着你可以只保留自己需要的功能,把剩下的代码全部排除在编译之外,既省 Flash 又降低出问题的概率。

1.1 从 R0.14a 到 R0.14b,变化集中在哪

如果只从使用角度感受,R0.14a 和 R0.14b 之间的差异不算大。但如果你去翻源码目录下的history.txt,会发现 R0.14b 的修复点都在细节上:exFAT 卷上删除文件后的目录项清理、长文件名的边界匹配逻辑、以及某些系统下f_mkfs创建分区表时的对齐问题。这些场景在日常调试里不一定能碰到,但一旦碰到就是诡异问题,比如文件删掉了磁盘空间却不释放,或者长文件名的文件在某些播放器里显示乱码。R0.14b 就是在替这些边缘情况收尾。

另一个值得注意的点是FF_LBA64这个宏。exFAT 卷的容量可以做得很大,当扇区数超过 32 位 LBA 能表达的范围时,就需要 64 位扇区寻址。R0.14b 对这部分的支持已经稳定,如果你未来要接大容量 eMMC 或者高速 SDXC 卡,这一步是绕不过去的。

1.2 开源包打开之后,先认识这几个文件

FATFS 不像 Linux VFS 那样庞大,它整个源码包很小。解压后主要文件就这几个:

文件作用
ff.cFAT/exFAT 核心逻辑,所有文件操作 API 的实现
ff.h公共头文件,定义 API、数据结构和错误码
ffconf.h配置头文件,所有裁剪选项都在这里
diskio.h/diskio.c底层设备接口层,由用户编写或移植
ffunicode.cUnicode 和 OEM 代码页转换表,用于长文件名支持
option/目录可选的代码页和额外功能实现

我建议第一次接触时不要急着看ff.c里的实现细节,而是先读ff.h里的函数声明和结构体定义,再打开ffconf.h逐行看配置项。理解这层之后,整个系统的轮廓就清晰了:应用层调用 API,核心层处理 FAT 表和目录项,设备层最终把扇区读写映射到 SD 卡、Flash 或者其他存储介质上。

2. 源码层级拆解:应用层、核心层、设备层如何协作

FATFS 的设计思路可以这样理解:它把文件系统分成了三个层次,层与层之间用明确的接口隔开。上层是一个面向用户的文件操作 API,类似你熟悉的fopenfreadfwrite;中间是ff.c里的核心实现,负责解析 FAT 表、维护目录项、管理簇链;底层是设备驱动接口,处理真正和硬件打交道的扇区读写。

2.1 应用层 API:从 f_open 到 f_forward

应用层的入口是f_open,它的逻辑很像 C 语言标准库的fopen:指定一个路径和访问模式,返回一个FIL文件对象。之后就可以通过f_readf_writef_lseek来读写文件,最后用f_close关闭并释放资源。R0.14b 里还提供了一些实用函数,比如f_mkdir创建目录、f_unlink删除文件、f_rename重命名、f_stat获取文件信息。

比较容易被忽略的是f_mount。它负责注册一个逻辑驱动器号对应的卷,比如f_mount(&fs, "0:", 1)表示把fs对象挂载到"0:"上,最后一个参数为 1 时表示立即挂载,为 0 时则延迟到第一次访问文件时再挂载。很多人第一次使用 FATFS 时会忘记调用f_mount,直接f_open,结果返回FR_INVALID_DRIVE,就是这个原因。

这里有一个典型的理解偏差:f_mount并不是把存储介质重新格式化,它只是把一个文件系统对象和逻辑驱动器号绑定起来,后续所有写着"0:/"的文件操作都会走这个卷。如果你更换了 SD 卡,需要先f_mount卸载旧卷,再重新挂载新卷。

2.2 设备层 diskio:把控制权真正交给你的代码

设备层一共六个函数,这是移植 FATFS 时唯一需要自己动手的地方:

DSTATUS disk_initialize(BYTE pdrv); DSTATUS disk_status(BYTE pdrv); DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count); DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count); DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff); DWORD get_fattime(void);

disk_initialize初始化硬件,disk_readdisk_write按扇区读写数据,disk_ioctl处理一些控制命令,比如GET_SECTOR_COUNT获取总扇区数、GET_SECTOR_SIZE获取扇区大小、CTRL_SYNC确保写入落盘。get_fattime返回当前时间,FATFS 会把它写入文件的日期时间字段,通常由 MCU 的 RTC 驱动提供。

为什么这套设计被广泛应用?因为接口足够简单。你不需要理解文件系统的具体实现,只要能把扇区读出来、写进去,文件系统的事情就全交给ff.c。这就像你不需要懂图书馆怎么整理图书,只需要能从书架上取出指定编号的书,再放回去,图书管理系统自然会维护好索引。

2.3 配置层 ffconf.h:裁剪才是精髓所在

ffconf.h直接决定 FATFS 编译出来有多大、功能有多少。我每次移植新工程,第一件事就是打开这个文件逐项确认,而不是用默认配置直接编译。最常用的几个选项:

  • FF_USE_LFN:长文件名支持,0 表示关闭,1 或 2 表示启用,区别在于文件名缓冲区在内部还是外部。
  • FF_MAX_LFN:最大长文件名长度,默认 255。
  • FF_FS_EXFAT:是否支持 exFAT,开启后代码体积会增大。
  • FF_USE_MKFS:是否启用格式化功能,格式化功能在量产写卡时很实用。
  • FF_FS_MINIMIZE:裁剪 API 函数数量,追求最小资源时可以关掉一部分高级函数。
  • FF_FS_NORTC:当没有 RTC 时,用它代替get_fattime,文件时间戳会固定成某个值。

这里我最后的建议是:不用的功能坚决关掉。比如产品只读数据不写入,就把FF_FS_READONLY打开,ff.c里所有写文件相关代码都不会编译进去,Flash 占用能省很多。

3. 在 STM32 上把 FATFS R0.14b 完整跑起来

这一节讲真刀真枪的移植。以 STM32F103 + SD 卡为例,我尽量把每个环节说清楚,因为实际调试时最容易出问题的往往不是 FATFS 本身的逻辑,而是底层驱动的对接。

3.1 前置准备和最容易忽视的电平问题

硬件上先把 SD 卡接好。如果用 SPI 方式驱动,至少需要 MISO、MOSI、SCK、CS 四根信号线;如果使用 SPI 模式的 SD 卡,控制器的 SPI 速率最好在初始化阶段设置得低一些,很多卡在低速下才能稳定握手。有些卡在较高 SPI 时钟下初始化经常返回超时,这就是为什么初始化代码里通常会先把波特率降到 400kHz 左右。

电平问题很关键。SD 卡工作电压是 2.7V 到 3.6V,很多 STM32 开发板有板载电平转换,可以直接插卡。如果你是自己飞线连接,一定要确认 MCU 的 GPIO 电平匹配,否则会间歇性读写失败甚至烧坏卡。

3.2 五步完成移植流程

第一步,把官方源码里的source目录拷贝到工程里,注意加入编译路径。第二步,根据你的存储介质,实现diskio.c里的六个函数。第三步,打开ffconf.h,根据项目需要配置宏。第四步,编写挂载和文件操作代码。第五步,烧录后运行测试。

以 SD 卡 SPI 模式为例,disk_initialize里的动作大致是:拉高 CS 延时若干毫秒、发送至少 74 个时钟周期的空指令、发送 CMD0 进入 SPI 模式、发送 CMD1 或 CMD8+ACMD41 完成初始化、读取 CID/CSD 寄存器确认扇区数。disk_read发送 CMD17 读单扇区或者 CMD18 读多扇区,disk_write发送 CMD24 写单扇区或 CMD25 写多扇区。这些属于 SD 卡协议层的骨架,搞过 SD 卡驱动的人应该很熟。

3.3 第一个读写测试这样写

挂载成功之后,先做个最简单的写入测试:

FATFS fs; FIL fil; FRESULT res; UINT bw; res = f_mount(&fs, "0:", 1); // 挂载,立即挂载 if (res == FR_OK) { res = f_open(&fil, "0:/test.txt", FA_CREATE_ALWAYS | FA_WRITE); if (res == FR_OK) { f_write(&fil, "hello fatfs\r\n", 13, &bw); f_close(&fil); } }

如果这一步返回FR_OK,说明 FATFS 的整个工作链路已经通了。接下来可以调用f_mkfs做一次格式化,再创建目录、写多个文件、读回校验,基本就能确认文件系统稳定。

4. 使用 FATFS R0.14b 时最典型的几类坑

网上讨论 FATFS 的帖子很多,但真正有价值的往往不是"怎么用",而是"为什么这样用就出问题了"。我把实际项目里遇到的高频问题整理一下,按排查链路来讲。

4.1 挂载失败 FR_NO_FILESYSTEM

f_mount后返回FR_NO_FILESYSTEM,意思是"这块介质上没有有效的 FAT 引导扇区"。排查顺序是这样:

第一步,确认硬件有没有问题。用示波器或逻辑分析仪看 SD 卡 SPI 总线有没有数据,如果disk_read读出来的数据全是 0xFF,说明通信根本没有建立。第二步,确认已经调用了格式化。新买的 SD 卡虽然自带 FAT32 格式,但有些卡出厂是 raw 状态,插到设备上自然无法识别。第三步,确认disk_ioctl里的GET_SECTOR_SIZE返回了正确值。很多 SPI Flash 的扇区大小是 4096 字节,如果你直接按 512 字节处理,FATFS 会把整个数据结构读错。

如果是 SPI Flash 这一类非 SD 卡设备,没有现成的 FAT 文件系统,必须先f_mkfs格式化。注意f_mkfs也有自己的参数,包括扇区大小、分配单元和分区类型,需要跟介质特性匹配。

4.2 长文件名乱码和中文名问题

FATFS 默认情况下长文件名是关掉的,因为需要额外的内存缓冲。如果你打开FF_USE_LFN后仍然出现乱码,先检查代码页和编码方式。FATFS 的文件名在内部默认是 ASCII 和 OEM 代码页,如果你想支持 UTF-8 中文,需要设置FF_CODE_PAGE为对应代码页,并在读写文件名时使用匹配的编码。很多人的问题是:PC 端用 Windows 创建了中文文件名的文件,嵌入式设备却以 UTF-8 去读,结果当然是乱码。

另外,FF_MAX_LFN如果设置过小,超出长度的文件名会被截断或返回FR_INVALID_NAME。不要以为 255 是默认值就一定够用,有些工程的FF_MAX_LFN被裁剪成 64,这时候放入长文件名就很容易被卡住。

4.3 文件时间全部变成 1980 年

如果创建的文件在 PC 上显示时间是 1980-01-01,说明get_fattime返回了 0,或者你启用了FF_FS_NORTC。FATFS 用这个时间戳更新目录项里的日期时间字段,如果你没有实现 RTC,或者 RTC 刚上电还没初始化好,它就会把时间写成 FAT 约定的最小值。对大多数记录型设备来说这个不影响功能,但如果产品需要上报文件创建时间,就必须解决。

4.4 写入之后数据没保存,拔电后发现文件不对

这是最坑的一类问题。现象是:程序里f_write返回成功,断电后重新上电,文件内容缺失或者文件损坏。原因通常是写入的数据还在 FATFS 的缓冲区里,没有真正写到 SD 卡。f_write只是把数据交给文件系统的内部缓冲,只有缓冲区满了或者调用f_syncf_close时才会把数据刷到底层设备。

所以,关键数据写入后一定要调f_sync,或者干脆写完就f_closef_close内部会做两件事:刷新缓冲、更新目录项。如果你在写日志时希望每条数据都及时落盘,就每写几条调用一次f_sync,代价是写入速度会明显下降。

5. 性能与可靠性:怎么把 FATFS 调得更顺手

很多人以为 FATFS 慢是文件系统本身效率低,其实多数情况下是底层驱动没发挥好。文件系统层面能优化的点也不少。

5.1 多扇区读写比单扇区快得多

FATFS 的disk_readdisk_write参数里带了一个count,表示扇区数。底层驱动完全可以把这些扇区一次性读上来或写下去,而不是在文件系统里循环调用单扇区读写。对 SD 卡 SPI 模式来说,多块读用 CMD18,多块写用 CMD25,配合CTRL_SYNC保证写完成,整体吞吐量可以比单扇区翻好几倍。

还有一点容易被忽略:缓冲区必须对齐。在 Cortex-M 平台或者开启了 DMA 的场景里,如果缓冲区的地址没有按底层要求对齐,DMA 传输可能直接 HardFault,或者数据错位。FATFS 文件对象里自带一个扇区缓冲区,如果你用的是FF_FS_TINY模式,这个缓冲区会跟文件系统共享,内存节省但缓存命中率降低,需要根据项目取舍。

5.2 启用 FASTSEEK 处理大文件随机访问

FATFS 默认的f_lseek是线性查找簇链,对顺序写入没问题,但如果你频繁定位大文件的不同位置,性能会很难看。解决办法是开启FF_USE_FASTSEEK,在打开文件后分配一个clmtbl[]数组,通过f_lseek预建映射表。这样之后的随机定位会快很多。代价是每个文件对象要多分配一块内存,数组大小跟文件的总簇数有关。

5.3 掉电安全和设备寿命要自己想清楚

FATFS 本身没有日志功能,不像有些文件系统有 journal,掉电时如果正好在写 FAT 表,就有可能造成目录项损坏、文件系统丢失。这是 FAT 格式的先天限制,不是版本 bug。要缓解掉电风险,一是重要参数写入后立刻f_sync,二是尽量把日志写入到一个固定文件里,减少频繁的目录项更新。

对于 SPI Flash,还要额外考虑磨损均衡。FATFS 不管 Flash 的擦写寿命,如果你的数据变化频繁,同一个扇区反复擦写可能很快耗尽寿命。这种情况下最好在底层加一层 Flash 转换层,或者用CTRL_TRIM配合存储介质特性。SD 卡一般自带控制器做磨损均衡,SPI Flash 没有这个待遇。

6. 看到新版本,要不要升级

R0.14b 之后,FatFs 后续版本在不断迭代。新版本确实修了很多边角问题,也补充了一些功能,整体 API 变化不大,老工程升级不算困难。但我的观点是:如果产品已经稳定跑在 R0.14b 上,没必要为了"新"去动底层存储代码。文件系统这种模块,升级引发的回归风险往往大于功能收益。

如果是新项目,直接选用当时最新的稳定版当然没问题,毕竟官方修复的 bug 和兼容性问题确实有价值。R0.14b 的意义在于它被大量项目验证过,网上关于它的问题答案几乎都能找到,遇到问题是最好排查的状态。

我个人在实际项目里的做法是,把 FATFS 源码固定在一个版本里,锁死作为基础版本,同时把底层驱动和配置文件单独管理。这样以后不管是升级主控型号还是更换存储介质,文件系统层的改动都被限制在可控范围内。存储这一块,求稳永远比求新重要。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询