资讯动态

FatFs嵌入式文件系统移植:从integer.h到diskio全流程解析

发布时间:2026/9/12 12:37:32 来源:尧图企业网站定制
简介面向嵌入式单片机开发场景的 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 表解析、目录项读写、簇链管理和全部对外 APIinteger.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 开启时的大容量卷 LBAINT16 位有符号状态值、中间计算LONG32 位有符号时间戳、偏移量运算2.3 移植 integer.h 的三个典型坑与编译期自检第一个坑是类型重名。部分编译器的库头文件里已经定义过 UINT、WORDWindows 分支包含 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_puts2 还会做 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 文件名的代码页简体中文工程填 936FF_FS_NORTC0/1无 RTC 时填 1时间戳用固定值避免依赖 f_get_fattimeFF_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 改回 0FF_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_LFN1字节的 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_PAGE936 的表会占几 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 或 QWORD32 位卷上不用动它。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_ABORTEDGET_SECTOR_SIZE返回单扇区字节数非 512 介质挂载失败GET_BLOCK_SIZE返回擦除块/分配单元大小f_mkfs 算出的簇大小可能不合理CTRL_TRIM通知 NAND 无效区域功能不报错但性能和磨损会劣化4.3 上真硬件前先检查的四个环节排查顺序按调用链从底层往上走。第一disk_status 上电后手动调一次若返回 STA_NOINITf_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。所以在格式化之前用 opt1 挂载报 FR_NO_FILESYSTEM 是预期行为不是移植错误。f_write 的 bw 必须与请求长度相等不相等先查 FF_FS_READONLY 是否误设为 1。全部通过后用调试器把 ram[0] 到 ram[511] 导出来做最后一层验证第 510 和第 511 字节必须是 0x55、0xAAFAT32 的文件系统类型串 FAT32 出现在偏移 0x52 处。这两个特征对上说明 ff.c、integer.h、diskio 这一整条链在类型宽度、扇区布局和数据写路径上都没有问题这时再换真 SD 卡大概率一次就能挂载成功。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价