FatFs嵌入式文件系统移植:从integer.h到diskio全流程解析
2026/9/12 12:37:23 网站建设 项目流程

简介:面向嵌入式单片机开发场景的 FatFs 文件系统核心实现包,用于解决 SD 卡、Flash 等外部存储设备在资源受限环境中的文件管理问题。压缩包共 3 个文件,包含 2 个头文件和 1 个 C 源文件,整体仅 29KB。ff.c 实现了 FAT12/FAT16/FAT32/exFAT 的底层读写、目录遍历、文件创建删除等核心操作;ff.h 则定义了 FIL、DIR 等关键数据结构,以及 f_open、f_read、f_write、f_close 等常用 API 原型;integer.h 通过 INT16、UINT32 等类型宏统一不同编译器下的整数长度,降低移植时的位宽不一致风险。这套代码既可直接加入 STM32、51 等单片机工程,按需配置扇区大小、簇大小和工作区,再实现 SPI、SDIO 等底层驱动后完成 FatFs 移植;也可以作为学习 FAT 文件系统内部机制的精简范本,帮助入门者掌握文件对象的生命周期、目录项组织与存储分配思路。已有 153 人学习下载,适合嵌入式开发者参考或二次开发。

1. 拿到 ff.rar 先别急着编译:ff.c 与 integer.h 是一对必须一起看的文件

从网上下载的嵌入式文件系统源码包解压出来通常就是这个结构:一个 ff.rar,里面躺着两万行左右的 ff.c、不到五十行的 integer.h,再加上 ff.h、ffconf.h、diskio.c 和 diskio.h。很多人的第一反应是把 ff.c 直接拖进工程,然后对着 f_mount 返回的 FR_NO_FILESYSTEM 或 FR_NOT_READY 发愣。实际上这套文件是 FatFs 类文件系统模块的标准结构:ff.c 负责 FAT 表解析、目录项读写、簇链管理和全部对外 API,integer.h 则先把 C 基本类型重新映射成 BYTE、WORD、DWORD 这一组固定宽度类型。ff.c 内部大量位运算和结构体偏移都建立在这些类型的宽度不变的前提上——integer.h 映射错了,ff.c 就算编译通过,读写出来的数据也是错的。把类型映射、配置裁剪、diskio 对接和验证方法一次理清,这个包就能真正变成能用的文件系统。适合 MCU 开发、需要给设备加本地存储的嵌入式工程师往下看。

2. integer.h:几十行类型映射决定 ff.c 能不能编译、会不会算错

2.1 为什么 FatFs 不直接用 stdint.h,要自己包一层类型

integer.h 存在的原因有三层。第一层是历史包袱:这套代码从 DOS 时代一路演化过来,BYTE、WORD、DWORD 是当年 Windows 头文件和编译器的通用命名,沿用这套自定义类型后,同一份 ff.c 能在 8 位、16 位、32 位工具链之间原样传递,不必每换一个编译器就改一遍类型名字。第二层是隔离平台差异:Windows 分支要包含 windows.h 才能拿到 DWORD、QWORD,而 windows.h 本身又定义了 UINT、WORD 这些名字,直接 typedef 会报重复定义,所以需要 FF_INTEGER 这个保护宏来区分平台。第三层是可控性:ff.c 内部默认 BYTE 必须 8 位、WORD 必须 16 位、DWORD 必须 32 位,FAT32 的 32 位簇号、目录项里的 16 位首簇高字、文件大小字段,任何一位宽变化都会让模块整体失准。包一层类型之后,移植时只需要盯住 integer.h 一个文件。

2.2 一份可移植的 integer.h 写法与逐类型说明

#ifndef FF_INTEGER #define FF_INTEGER #if defined(_WIN32) && defined(_MSC_VER) #include <windows.h> typedef unsigned __int64 QWORD; #else #include <stdint.h> typedef int16_t INT; typedef uint16_t UINT; typedef int32_t LONG; typedef uint32_t ULONG; typedef uint8_t BYTE; typedef uint16_t WORD; typedef uint32_t DWORD; typedef uint64_t QWORD; #endif #endif

这段代码的逻辑是:先判断是不是 Windows + MSVC 环境,是就借用 windows.h 里现成的类型定义,避免重复 typedef;嵌入式环境一律以 C99 的 stdint.h 为基准。INT、UINT、LONG、ULONG 这一组用于字节序转换和扇区计算的中间量,BYTE、WORD、DWORD、QWORD 这一组用于位掩码、簇号、文件大小等无符号场景。注意 WORD 必须对应 uint16_t 而不是 unsigned int,因为 ff.c 的目录项解析要把 0xFFFF 当 16 位全 1 处理,unsigned int 在多数平台是 32 位,语义完全不同。

类型宽度要求ff.c 中的主要用途
BYTE8 位无符号扇区缓存、目录项单字节访问
WORD16 位无符号FAT 表项、目录项首簇号低字
DWORD32 位无符号文件大小、扇区号、簇号
QWORD64 位无符号FF_LBA64 开启时的大容量卷 LBA
INT16 位有符号状态值、中间计算
LONG32 位有符号时间戳、偏移量运算

2.3 移植 integer.h 的三个典型坑与编译期自检

第一个坑是类型重名。部分编译器的库头文件里已经定义过 UINT、WORD,Windows 分支包含 windows.h 之后尤其容易冲突。处理方式不是给类型改名,而是检查编译器头文件,确认 FF_INTEGER 的包含顺序,保证 integer.h 在平台头文件之后被包含。第二个坑是 8 位机上 char 默认有符号,如果手滑把 BYTE 定义成 char,做if (buff[0] == 0xFF)这类判断时,0xFF 会被符号扩展成 0xFFFFFFFF,条件永远不成立,表现为文件名乱码、FAT 表校验不过。第三个坑是位宽无误但没自检,改错后要等到运行时才暴露。

建议在工程里加一段编译期断言,让类型错误在编译阶段就炸出来:

_Static_assert(sizeof(BYTE) == 1, "BYTE must be 8-bit"); _Static_assert(sizeof(WORD) == 2, "WORD must be 16-bit"); _Static_assert(sizeof(DWORD) == 4, "DWORD must be 32-bit");

工具链不支持 C11 时,用老的 typedef 数组技巧代替:typedef char check_word_size[(sizeof(WORD) == 2) ? 1 : -1];。编译错误列表中出现 negative array size 相关报错,就是类型宽度被改坏了。这段断言放在任何包含 ff.h 的 C 文件里都有效,因为它读到的就是 ff.c 实际使用的整数类型。

注意:integer.h 里的类型名属于模块内部约定,不要为了“规范”擅自把它们替换成 uint8_t 直接编译 ff.c,除非你连 ff.c 里所有函数签名和结构体定义一起改。

3. ff.c 与 ffconf.h:先弄清结构体,再谈裁剪配置

3.1 FATFS、FIL、DIR 三个对象在 ff.c 里分别承担什么

ff.c 的实现核心围绕三个对象展开,它们的定义都在 ff.h 里。FATFS 是卷对象,一个挂载的存储介质对应一个,f_mount 时把介质状态、FAT 表缓存窗口、当前目录信息都放进去,挂载后常驻内存,是整个模块中占用 RAM 的大头。FIL 是文件对象,每次 f_open 分配一个,里面记录文件指针位置、当前簇号、扇区缓存窗口,只有 f_close 之后才释放。DIR 是目录遍历的游标,f_opendir 和 f_readdir 配合使用。

裁减顺序要先看 RAM。实际工程里常见做法是把 FATFS 和 FIL 声明成全局或静态变量,而不是在堆上 malloc,理由是嵌入式 heap 在目录层级深、打开文件多时容易产生碎片,而 ff.c 只返回 FR_NOT_ENOUGH_CORE,不会告诉你哪一次分配失败、碎片有多少。一个 FATFS 对象大约几百字节,一个 FIL 对象在不开 LFN 时约 550 字节左右,开 LFN 后要再加缓冲。把这些对象放在静态区,内存占用在链接期就可确定,比运行时才知道失败可靠得多。

3.2 决定 ff.c 体积和行为的九个关键参数

ffconf.h 里的配置宏直接控制 ff.c 编译进哪些代码、结构体里带多大缓冲。下面九个是移植时必调的:

参数常见取值对行为与体积的影响
FF_USE_LFN0/1/2/30 关闭长文件名,FIL 最小;1 静态工作缓冲;2 栈上缓冲;3 堆上缓冲
FF_FS_MINIMIZE0/1/2/31 去掉状态删除改名类;2 再去掉目录遍历;3 连 f_lseek 都去掉
FF_USE_STRFUNC0/1/2是否编译 f_printf/f_puts,2 还会做 LF 到 CRLF 的转换
FF_USE_MKFS0/1是否编译 f_mkfs/f_fdisk,量产工具需要,产品固件一般关掉
FF_FS_READONLY0/11 时所有写路径函数被裁掉,代码和 RAM 都明显减小
FF_MIN_SS / FF_MAX_SS512/4096扇区范围,决定文件系统是否支持 4K 扇区介质
FF_CODE_PAGE936/437/850非 ASCII 文件名的代码页,简体中文工程填 936
FF_FS_NORTC0/1无 RTC 时填 1,时间戳用固定值,避免依赖 f_get_fattime
FF_FS_TINY0/11 时 FIL 复用 FATFS 的窗口做数据缓冲,省 RAM、增加一点 CPU 开销

只读场景下,典型的最小配置长这样:

#define FF_FS_READONLY 1 #define FF_FS_MINIMIZE 3 #define FF_USE_STRFUNC 0 #define FF_USE_LFN 1 #define FF_MAX_LFN 255 #define FF_MIN_SS 512 #define FF_MAX_SS 512 #define FF_CODE_PAGE 936 #define FF_USE_MKFS 0 #define FF_FS_NORTC 1

这个配置适合 bootloader 或资源下载器:只能读、不建目录、不格式化,FIL 对象尺寸被压到最小,只留长文件名支持。如果要落文件,把 FF_FS_READONLY 改回 0,FF_FS_MINIMIZE 按需放宽到 0 或 1 即可。每个宏在 ffconf.h 里都有默认值和注释,改完后注意 ff.c 是被 ff.h 间接包含 ffconf.h 的,必须全量重新编译,依赖旧配置的增量编译结果不作数。

3.3 LFN 的连锁反应:缓冲区、代码页和堆栈

打开 FF_USE_LFN 不是改一个数字那么简单。FF_USE_LFN 为 1 时,FIL 对象内部会多一个约 2*(FF_MAX_LFN+1)字节的 WCHAR 缓冲,默认 255 时就是 512 字节,这在 RAM 紧张的 MCU 上是不能忽略的开销。FF_USE_LFN 为 2 时这个缓冲放到调用栈上,栈小的平台容易溢出;为 3 时走堆分配,依赖 malloc 可用性。三者没有绝对好坏,要在 RAM 总量、栈深度和分配失败概率之间权衡。

还有一个容易被忽略的编译细节:ff.c 在文件末尾会根据 FF_CODE_PAGE 自动包含对应的转换表源文件,比如 936 对应 option 目录下的 cc936.c。如果工程的头文件搜索路径里没加 option 目录,链接阶段会出现 ff_convert、ff_wtoupper 未定义的错误。这个错误和 integer.h 无关,却经常被误当成类型问题排查半天。

注意:FF_CODE_PAGE=936 的表会占几 KB 到几十 KB 的 ROM,如果产品只处理 ASCII 文件名,老老实实填 437 并把长文件名关掉,省下的空间可能比整个应用层还多。

4. ff.c 到存储介质:把 diskio.c 五个接口补全就能跑起来

4.1 diskio.c 是 ff.c 唯一能看见的硬件视图

ff.c 不认识 SPI 总线,不认识 SD 协议,也不认识 NAND 的坏块管理。它只通过 diskio.h 里声明的五个函数访问介质:disk_initialize、disk_status、disk_read、disk_write、disk_ioctl。这五个函数由移植者实现,参数全部在 diskio.h 里固定。注意扇区号类型是 LBA_t,它在 ff.h 里根据 FF_LBA64 被定义为 DWORD 或 QWORD,32 位卷上不用动它。

disk_read 和 disk_write 的 count 参数单位是扇区数,不是字节数,这是移植时最常见的误解。一次 f_read 请求可能被 ff.c 拆成多次 disk_read 调用,每次的 sector 和 count 都由文件系统内部逻辑决定,移植层不要自作主张地合并或拆分。

4.2 以 SD 卡为例的 disk_read / disk_write / disk_ioctl 实现

#include "ff.h" #include "diskio.h" extern uint8_t sd_align_buf[512]; /* 全局 4 字节对齐缓冲,供 DMA 使用 */ DSTATUS disk_initialize(BYTE pdrv) { if (pdrv != 0) return STA_NOINIT; return (sd_init() == 0) ? 0 : STA_NOINIT; } DSTATUS disk_status(BYTE pdrv) { if (pdrv != 0) return STA_NOINIT; return (sd_ready() ? 0 : STA_NOINIT); } DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { UINT i; if (pdrv != 0) return RES_PARERR; for (i = 0; i < count; i++) { if (sd_read_block(sector + i, buff + i * 512) != 0) return RES_ERROR; } return RES_OK; } DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) { UINT i; if (pdrv != 0) return RES_PARERR; for (i = 0; i < count; i++) { if (sd_write_block(sector + i, buff + i * 512) != 0) return RES_ERROR; } return RES_OK; } DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { if (pdrv != 0) return RES_PARERR; switch (cmd) { case GET_SECTOR_COUNT: *(DWORD *)buff = sd_get_block_count(); /* 总扇区数,供 f_mkfs 使用 */ return RES_OK; case GET_SECTOR_SIZE: *(WORD *)buff = 512; return RES_OK; case GET_BLOCK_SIZE: *(DWORD *)buff = 1; /* 按扇区擦除的卡填 1 */ return RES_OK; case CTRL_SYNC: return sd_sync() ? RES_ERROR : RES_OK; default: return RES_PARERR; } }

pdrv 是卷号,多介质设备靠它区分 SD 卡和 U 盘,单介质直接判断不等于 0 就报错。sector + i 是绝对扇区号,从 0 开始,不是相对于某个分区的偏移,这个偏移由 ff.c 自己换算,移植层不要二次偏移。buff 的字节对齐要求由底层 SDIO/SPI 驱动决定,DMA 模式通常要求 4 字节对齐,而 ff.c 传入的 buff 只能保证基本对齐,必要时要先在 sd_align_buf 里中转一次。

disk_ioctl 是五个接口里最重要的一个,f_mkfs 之前必须先确认它能正确响应:

命令作用不实现的后果
CTRL_SYNC刷写缓存落盘掉电丢数据
GET_SECTOR_COUNT返回介质总扇区数f_mkfs 直接返回 FR_MKFS_ABORTED
GET_SECTOR_SIZE返回单扇区字节数非 512 介质挂载失败
GET_BLOCK_SIZE返回擦除块/分配单元大小f_mkfs 算出的簇大小可能不合理
CTRL_TRIM通知 NAND 无效区域功能不报错,但性能和磨损会劣化

4.3 上真硬件前先检查的四个环节

排查顺序按调用链从底层往上走。第一,disk_status 上电后手动调一次,若返回 STA_NOINIT,f_mount 必然返回 FR_NOT_READY,问题在介质初始化而不在文件系统。第二,确认 disk_read 能正确读取扇区 0,读回来前 512 字节用串口打印,SD 卡的 MBR 或引导区必定以 0x55 0xAA 结尾,读不到就说明扇区号或 SPI 模式有问题。第三,确认 f_mount 第一参数不为 NULL 且卷号字符串与路径一致,比如挂载用f_mount(&fs, "0:", 1),打开文件就要写f_open(&fp, "0:/test.txt", ...),两者对不上只会得到 FR_INVALID_DRIVE。第四,f_mkfs 失败时重点排查 GET_SECTOR_COUNT 的返回值,很多假卡返回的块数会超出实际容量,格式化到一半报 FR_MKFS_ABORTED。

5. 用 RAM 盘验证 ff.c:不碰硬件也能测出移植对不对

在接 SD 卡和 Flash 之前,先用一块内存把 diskio 五个接口填上,等于给 ff.c 搭一个完全可控的测试环境。这样能把“文件系统逻辑问题”和“硬件时序问题”彻底分开——RAM 盘上跑不通的,多半是 integer.h 或配置的问题;RAM 盘上跑通了真硬件还出错,才需要去查信号和驱动。

static BYTE ram[8 * 1024 * 1024]; /* 8MB RAM 盘 */ DSTATUS disk_status(BYTE pdrv) { return (pdrv == 0) ? 0 : STA_NOINIT; } DSTATUS disk_initialize(BYTE pdrv){ return disk_status(pdrv); } DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { memcpy(buff, &ram[(LBA_t)sector * 512], (size_t)count * 512); return RES_OK; } DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) { memcpy(&ram[(LBA_t)sector * 512], buff, (size_t)count * 512); return RES_OK; } DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { switch (cmd) { case GET_SECTOR_COUNT: *(DWORD *)buff = sizeof(ram) / 512; return RES_OK; case GET_SECTOR_SIZE: *(WORD *)buff = 512; return RES_OK; case GET_BLOCK_SIZE: *(DWORD *)buff = 1; return RES_OK; default: return RES_PARERR; } }

验证流程按挂载、格式化、写读回三步走:

FATFS fs; FIL fp; BYTE work[512]; BYTE buf[16]; UINT bw, br; f_mount(&fs, "0:", 0); /* 只注册卷,不真正挂载 */ f_mkfs("0:", FM_FAT32, 0, work, sizeof(work)); /* RAM 盘上先格式化 */ f_mount(&fs, "0:", 1); /* 再真正挂载 */ f_open(&fp, "0:/hello.txt", FA_CREATE_ALWAYS | FA_WRITE); f_write(&fp, "fatfs ok", 8, &bw); f_close(&fp); f_open(&fp, "0:/hello.txt", FA_READ); f_read(&fp, buf, 8, &br); f_close(&fp);

f_mount 的 opt 参数在这里很关键:0 只注册文件系统对象,供 f_mkfs 使用;1 挂载并读取卷信息,介质上没有合法 FAT 卷时返回 FR_NO_FILESYSTEM。所以在格式化之前用 opt=1 挂载报 FR_NO_FILESYSTEM 是预期行为,不是移植错误。f_write 的 bw 必须与请求长度相等,不相等先查 FF_FS_READONLY 是否误设为 1。

全部通过后,用调试器把 ram[0] 到 ram[511] 导出来做最后一层验证:第 510 和第 511 字节必须是 0x55、0xAA,FAT32 的文件系统类型串 "FAT32 " 出现在偏移 0x52 处。这两个特征对上,说明 ff.c、integer.h、diskio 这一整条链在类型宽度、扇区布局和数据写路径上都没有问题,这时再换真 SD 卡,大概率一次就能挂载成功。

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

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

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

立即咨询