STM32F103+FatFs文件系统管理:配置、日志与排错
简介面向STM32F103单片机开发者的FATFS文件系统管理实验例程基于HAL库和KEIL环境编写适合正在学习嵌入式文件系统应用、或需要快速搭建存储管理功能的读者。压缩包内含226个文件以C源文件与H头文件为主覆盖FATFS核心驱动和STM32F1xx系列HAL库外设驱动同时附有PNG接线示意图、TXT说明文档、工程配置文件及实用脚本整体大小仅2.43MB下载后即可对照使用。目前已有121人学习下载例程经过精心编写关键代码带有详细注释并明确给出了单片机与所接模块的引脚定义和接线方式方便在硬件上快速验证。除此之外工程中还提供烧录文件与清理脚本可帮助提升调试效率适合物联网实验开发中直接复用或在此基础上做功能扩展对于理解文件系统读写、文件管理等底层逻辑也有较强的参考价值。1. 把 FATFS 从“例程能跑”改成“自己会改”很多拿到 STM32F103 FATFS 实验例程的人第一步往往是把 .rar 解压出一个工程烧进去能读卡就认为完成了。但真正接手项目时卡住你的通常不是 FatFs 本身而是 HAL 库生成代码里的底层回调没接对、SDIO 和 SPI 选错了引脚、或者 ffconf.h 里一个宏把 RAM 吃掉大半。这个实验讲的是用 STM32 HAL 库把 FATFS 文件系统管理工作做完整建目录、写日志、列文件、删旧文件并在 CubeMX 配置和 FRESULT 排错层面留出可复现的步骤。适合刚学完 HAL 库中断和 DMA、想往数据记录方向走的开发者也适合做量产固件前想把这套文件栈摸透的工程师。2. 先用 STM32CubeMX 把 F103 的 FATFS 底子生成出来解压例程包以后不急着看 main.c。先明确一个原则文件系统代码只有很小一部分是 FatFs 中间件真正和板子强相关的是diskio.c里的disk_initialize、disk_read、disk_write、disk_ioctl四个函数。HAL 库工程里这四个函数通常由 CubeMX 自动生成但接口模式选错后面写的所有文件操作都会在莫名其妙的地方失败。2.1 为什么老例程值得重生成而不是直接改网上流传的 STM32F103 FATFS 例程很多是用标准外设库写的底层是SD_Init()、SD_ReadBlock()这类函数拿过来和自己的 HAL 工程混用会出现两套外设状态机互相覆盖的问题。常见做法是直接在 STM32CubeMX 里新建工程只保留 FatFs 中间件和底层生成代码再把自己的应用逻辑加进去。这样做的另一个好处是CubeMX 会把 FATFS 的磁盘接口和 STM32 的 SDIO 外设绑在一起生成FATFS_Target和User文件夹。后续升级 HAL 库版本时重新生成不会覆盖手写的业务函数排错的边界也很清楚应用层只调用 FatFs 的 API底层只面对扇区。2.2 SDIO 还是 SPI先看板子再动 CubeMXF103 上跑 FATFS最常见的介质是 SD 卡而 STM32 访问 SD 卡有两条路SDIO 四线模式和 SPI 模式。这里没有绝对的好坏只有适不适合你手上的板子。对比项SDIO 四线模式SPI 模式典型引脚PC8-PC12、PD2SPI 的 MOSI/MISO/SCK另加一根 CS理论速度高适合连续数据记录低但日志频率不高时够用接线难度线多杜邦线容易松动线少面包板也能跑HAL 生成复杂度CubeMX 直接配置 SDIO需要在 diskio.c 里自己桥接 SPI卡兼容性对卡时序要求稍高许多老卡反而更听话对于 STM32F103C8T6 最小系统板和普通 SD 卡模块我一般建议先用 SPI 模式把 FATFS 跑通因为四线 SDIO 一旦遇到接触不良排查起来比写代码还费时间。等代码逻辑完整了再改成 SDIO 模式去追求吞吐量这时候排错的变量就少了。CubeMX 里的配置步骤分为四步第一时钟树先把 HCLK 配到 72MHz第二如果有 SDIO 外设在 Connectivity 里勾选 SDIO并根据硬件选择 1-bit 或 4-bit 总线第三在 Middleware 里启用 FATFS接口选择 SDIO第四生成工程到 Keil 或 Makefile 工程。2.3 生成后最先改的 ffconf.h 参数FATFS 的配置集中在ffconf.h这个文件决定了代码体积、RAM 占用和中文文件名的处理方式。实验里至少要把下面几个宏确认一遍#define FF_USE_LFN 2 // 2 表示长文件名工作缓冲在栈上分配 #define FF_MAX_LFN 64 // 最大文件名长度按实际需要调整 #define FF_CODE_PAGE 936 // 936 对应 GBK方便中文文件名 #define FF_FS_READONLY 0 // 0 表示可写 #define FF_USE_STRFUNC 2 // 允许 f_printf 等格式化输出 #define FF_VOLUMES 1 // 卷数量实验通常只用 0 号卷这段配置里最容易踩坑的是FF_USE_LFN。如果设为 1工作缓冲是全局静态数组多个函数共用时要注意重入如果设为 2FatFs 在解析路径时会在栈上分配一块大小为FF_MAX_LFN 2的缓冲F103 的栈如果只给了 0x400 字节长文件名一出现就会 HardFault。遇到这个现象先看 Startup 文件里的 Stack_Size再决定要不要把FF_MAX_LFN调小。提示FF_CODE_PAGE改成 936 以后FatFs 内部会带一个较大的 Unicode 转换表F103 的 Flash 占用会明显增加。如果只是记录 ASCII 日志可以保持 437避免 Flash 容量不够。3. 写一个按目录管理的 FATFS 日志读写例程把文件系统跑通和把文件系统用起来是两回事。在实验里建议不要把所有代码堆在 main.c 里而是把 FatFs 的调用封装成几个带有错误返回的接口这样后面加 DMA、加掉电保护时才不会改一处崩三处。3.1 挂载卷的封装要覆盖“新卡未格式化”的情况挂载是第一个容易产生错觉的 API。f_mount的第一个参数是 FATFS 结构体指针第二次挂载同一张卡时传入 NULL 可以只注销第二个参数是路径前缀F103 上通常传或0:第三个参数 1 表示立即挂载0 表示只登记工作区。static FATFS g_fs; FRESULT file_system_init(void) { FRESULT res f_mount(g_fs, , 1); if (res FR_NO_FILESYSTEM) { BYTE work[FF_MAX_SS]; // f_mkfs 需要的工作缓冲区 res f_mkfs(, NULL, work, sizeof(work)); if (res FR_OK) { res f_mount(g_fs, , 1); // 格式化完成后重新挂载 } } return res; }这段代码里FF_MAX_SS是扇区大小宏常取 512 或 4096。f_mkfs的第三个参数必须是 4 字节对齐的缓冲区否则部分底层接口会报对齐错误。格式化是破坏性操作代码里加上FR_NO_FILESYSTEM条件能避免每次上电都把正常卡格式化一遍。3.2 创建目录并按天追加日志文件系统管理实验里日志一般按日期分目录或分文件。先写一个确保目录存在的函数FRESULT ensure_dir(const char *path) { FILINFO fno; if (f_stat(path, fno) FR_OK) { if (fno.fattrib AM_DIR) { return FR_OK; } return FR_EXIST; } return f_mkdir(path); }这里用f_stat先判断路径是否存在避免了f_mkdir返回FR_EXIST被误当成错误处理。FATFS 的f_mkdir一次只能建一层目录要建多级路径时得逐层调用。追加日志的典型写法是FRESULT log_append(const char *fmt, ...) { FRESULT res; va_list ap; res f_open(g_file, 0:/LOG/RUNTIME.LOG, FA_WRITE | FA_OPEN_APPEND); if (res ! FR_OK) return res; f_printf(g_file, %lu,, (unsigned long)HAL_GetTick()); va_start(ap, fmt); f_vprintf(g_file, fmt, ap); va_end(ap); f_puts(\r\n, g_file); res f_close(g_file); return res; }FA_OPEN_APPEND每次打开文件都会把读写指针移到文件末尾适合追加记录。每次写一行就f_close会损失一部分性能但换来了掉电时丢数据概率更低的保证。如果日志写入频繁可以改成保留文件句柄攒满 512 字节再f_sync一次但这个优化会放大掉电窗口项目中要衡量。3.3 列目录时注意 FILINFO 的长文件名缓冲遍历目录是实验里展示“管理”能力的重要部分。FATFS 的f_readdir会把结果填进FILINFO其中fname是短文件名长文件名需要调用方自己准备缓冲。FILINFO 字段用途注意事项fname短文件名固定 13 字节始终有效lfname长文件名缓冲指针调用前必须自己赋值lfsize上面缓冲的大小填 0 会禁用长文件名fattrib文件属性用 AM_DIR 判断目录fsize文件大小单位字节目录项此值为 0fdate文件修改日期位域格式可直接比较void list_dir(const char *path) { DIR dj; FILINFO fno; char lfn[FF_MAX_LFN 1]; fno.lfname lfn; fno.lfsize sizeof(lfn); if (f_opendir(dj, path) ! FR_OK) return; for (;;) { if (f_readdir(dj, fno) ! FR_OK) break; if (fno.fname[0] 0) break; // 读到目录末尾 if (fno.fname[0] .) continue; // 跳过 . 和 .. if (lfn[0]) { printf(%s, %lu\r\n, lfn, fno.fsize); } else { printf(%s, %lu\r\n, fno.fname, fno.fsize); } } f_closedir(dj); }这段代码里最容易错的是忘记给lfname赋值。如果fno.lfname保持 NULLf_readdir就只填短文件名显示出来的日志名会被截断成 8.3 格式。另一个坑是lfn缓冲每次循环都会被复用所以要先打印再用不要积累引用。如果要做“删除三天前的日志”不要在这个循环里直接调f_unlink。删除文件会改变目录项虽然 FATFS 在多数情况下允许但不安全的写法容易漏项。正确做法是先把要删的文件名存到数组里循环结束后再统一删除。4. 从 FRESULT 到 HAL 底层读函数一级一级定位问题文件系统实验报告里最难写的是“排错”部分因为 FATFS 返回的错误码覆盖了从物理层到逻辑层的所有问题。遇到FR_NOT_READY时先怀疑文件系统代码是最常见的误区。4.1 先用底层读函数确认扇区是否可读FATFS 的f_open走的是disk_read所以最直接的排查方式是绕过 FATFS直接调用 HAL 的 SDIO 读接口读第 0 个扇区。SDIO 模式下CubeMX 会生成SD_HandleTypeDef hsd实验代码可以这样验证uint8_t buf[512]; HAL_StatusTypeDef st; __ALIGN_BEGIN uint8_t aligned_buf[512] __ALIGN_END; st HAL_SD_ReadBlocks(hsd, aligned_buf, 0, 1, 1000); if (st ! HAL_OK) { printf(HAL_SD_ReadBlocks fail: %d\r\n, st); }参数说明第一个参数是 SDIO 句柄第二个参数要求 4 字节对齐普通局部数组在编译器默认对齐下通常可以但开启 DMA 后不对齐会直接卡死第三个参数 0 表示逻辑扇区号第四个参数 1 表示读 1 个扇区最后一个参数是超时毫秒数。读回来的 512 字节里偏移 510 和 511 必须是0x55和0xAA这是 MBR 或 FAT 卷的结束标志。如果这个都读不对问题在硬件不在 FATFS。4.2 把 FRESULT 返回值和硬件表现对应起来FRESULT典型硬件/配置原因排查动作FR_NOT_READY卡未插好、供电不足、SDIO 无时钟先读 CID/CSD确认卡进入传输态FR_DISK_ERR扇区读写超时响应 CRC 错误降低 SDIO 时钟分频换短杜邦线FR_NO_FILESYSTEM新卡没有格式化或分区表格式不支持用 f_mkfs 或电脑格式化为 FAT32FR_NOT_ENABLEDf_mount没执行或 FATFS 工作区被清空检查初始化调用顺序FR_EXIST文件已存在而打开模式要求新建改用 FA_OPEN_ALWAYS 或先删除FR_DENIED卡写保护或文件属性只读查 disk_ioctl 的写保护返回这些错误里FR_DISK_ERR 的迷惑性最强。它在写入大量数据后出现多半不是文件系统逻辑错误而是 F103 的 SDIO 在高速下采样不稳定。先把卡时钟降下来用HAL_SD_Init里的 Init.ClockDiv 参数调整确认能连续写 100 个文件后再逐步提频。4.3 加一个统一的返回值检查宏工程里的每次f_open、f_read、f_write都不能只看一眼返回值。推荐在调试阶段做一个快速断言static FRESULT f_chk(FRESULT res) { if (res ! FR_OK) { printf(FRESULT%d at %s:%d\r\n, res, __FILE__, __LINE__); while (1) { // 停在错误现场方便在调试器里看调用栈 } } return res; }调用处改成f_chk(f_open(...))一旦出现异常程序会停在出错那一行。这个写法不是给最终交付固件用的而是帮你在实验初期把问题暴露在第一时间。4.4 新卡格式化时的工作缓冲和分区选项如果读到了FR_NO_FILESYSTEM不要急着用电脑格式化。FATFS 自己可以完成格式化MKFS_PARM opt; BYTE work[FF_MAX_SS]; opt.fmt FM_FAT32 | FM_SFD; opt.au_size 0; opt.n_fat 1; opt.align 0; res f_mkfs(, opt, work, sizeof(work));FM_SFD表示不创建 MBR 分区表整个卡当作一个超级软盘。实验用的 SD 卡容量不大这种方式更简单也减少了分区表偏移带来的对齐问题。n_fat设为 1 能减少 FAT 表写入次数但对容错不利如果实验里经常直接拔电建议保留默认值 2。5. 交付前我会做掉的三个验证动作实验做到能读写文件还不够最后要留几个可以反复执行的验证手段让“文件系统管理”这件事可被测量。5.1 用 f_getfree 回读容量防止容量算错格式化完成后不要相信卡面上写的 8GB。用 FATFS 的卷信息回读能同时验证 FAT 表是否被正确解析。DWORD fre_clust 0; FATFS *pfs NULL; DWORD total_kb; if (f_getfree(, fre_clust, pfs) FR_OK) { total_kb (pfs-n_fatent - 2) * pfs-csize * pfs-ssize / 1024; printf(total%lu KB, free%lu KB\r\n, total_kb, fre_clust * pfs-csize * pfs-ssize / 1024); }这里的n_fatent是 FAT 表的表项数减 2 是因为前两个表项保留。不同 FatFs 分支的 FATFS 结构体字段略有差异如果是老版本移植过来的编译报错时先回ff.h里确认一下结构体名。5.2 用串口脚本做连续写测试建议在实验板上写一个测试命令循环追加 1000 条记录每条记录带序号。上位机用脚本把日志拉回来检查序号是否连续。#!/bin/bash expected1 while IFS, read -r seq rest; do if (( seq ! expected )); then echo hole at $expected, got $seq exit 1 fi ((expected)) done log.txt echo all sequence ok这个验证能同时暴露两个问题写多了缓冲区溢出导致丢数据以及f_sync位置不对导致重启后日志断裂。如果测试中发现序号空洞优先看任务栈大小再看写文件时是不是跨了扇区边界。5.3 掉电恢复时用临时文件再加 f_rename对数据记录类设备直接写正式文件名在掉电时容易留下半截文件。一个稳妥技巧是先写入同目录下的临时文件写完后用f_rename改成正式名字。虽然 FATFS 的 rename 不是严格原子操作但它比在正式文件中间覆盖写入更容易恢复。具体的做法是文件名带序号例如REC_0001.TMP和REC_0001.DAT每次启动先扫描.DAT文件如果最后一个.DAT的文件头校验不对再检查同序号的.TMP把能用的数据补齐。这比单纯依赖 FATFS 的掉电保护更可控也是实验中体现“管理”二字的最佳落点。本文还有配套的精品资源点击获取