ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32 FatFs文件系统移植实战:从配置到SD卡驱动优化

2026/9/11 17:22:51 拓冰建站 浏览量
STM32 FatFs文件系统移植实战:从配置到SD卡驱动优化 简介面向STM32嵌入式开发者的FatFs文件系统参考资料内容覆盖FATFS模块原理、移植集成与典型应用。文件分配表FAT、目录表FDT与数据区的结构关系FAT12/16/32格式差异以及FatFs库在不依赖OS的裸机或RTOS环境下的挂载方式均做了系统整理。针对STM32平台重点说明ffconf.h中的裁剪配置、f_mount/f_open/f_write/f_read/f_close等常用API的调用逻辑并结合错误码机制、日志调试、减少FAT查找次数、选择合适扇区大小及SPI/SDMMC接口参数设置给出性能优化与数据记录、固件更新等实际场景的示例思路。ZIP压缩包大小约8.71MB对于需要在资源受限MCU上实现SD卡读写与文件管理的开发者是一份清晰的学习路径参考。已有203人学习下载适合作为快速上手FatFs的辅助材料。1. 从一张 SD 卡说起FatFs 在 STM32 里的角色很多人做嵌入式存储时第一版是直接读卡器、按扇区写 512 字节然后为找一块连续区域放配置和日志写一堆地址换算。FatFs 把这块逻辑抽象成 FAT 文件系统让 STM32 可以像操作 U 盘一样读写 SD 卡。真正决定项目稳不稳定的不是 f_open 这些 API而是 ffconf.h 的裁剪和 diskio.c 的底层质量。如果你已经遇到 f_mount 返回 FR_DISK_ERR或从标准库迁到 HAL 库后读卡偶发失败下面按「内核配置 - 磁盘驱动 - 文件读写 - 性能验证」的顺序拆关键参数和边界条件中间直接给可复制代码。2. FAT 结构到 ffconf.h 裁剪先搞清 FatFs 在做什么2.1 FAT/FDT/数据区与 FatFs 的模块划分FAT 文件系统把存储介质分成三部分FAT 表记录每个簇的分配链FDT 才是真正存着文件名、大小、起始簇号的地方数据区放实际内容。FatFs 读写时把 FAT 链换算成扇区地址再通过 disk_read/disk_write 访问硬件。所以 f_open 只是初始化 FIL 对象f_read 第一次读时可能还没有走 FAT 表连续读反而很快而 f_lseek、f_unlink 这类操作会多次访问 FAT优化重点是减少 FAT 表回写次数。这个模块的源码文件不多但耦合很紧文件职责是否要改ff.c / ff.h文件系统逻辑和 API通常不改ffconf.h编译期配置必须逐项核对diskio.c / diskio.h把扇区读写接到 MCU 外设必须移植integer.h定义整数类型只在非标准编译器下动ffconf.h 的控制项以FF_或_开头旧版资料里经常混用认准你那份 FatFs 版本的实际前缀。FAT12/16/32 的核心逻辑都在 ff.c 里exFAT 则被拆到可选区块不开FF_FS_EXFAT时这部分代码不参与编译省掉约 2KB 到 6KB 的 Flash。2.2 ffconf.h 关键开关下面这份配置是我在 STM32 裸机上常用的起点#define FF_USE_LFN 1 // 长文件名支持 #define FF_MAX_LFN 128 // LFN 缓冲区大小单位字节 #define FF_FS_EXFAT 0 // 不开 exFAT省 RAM #define FF_VOLUMES 1 // 只挂一个盘 #define FF_USE_STRFUNC 1 // 允许 f_printf 格式化 #define FF_USE_MKFS 1 // 允许 f_mkfs 格式化 #define FF_USE_FASTSEEK 1 // 大文件随机访问 #define FF_USE_FIND 0 // 不需要 f_findfirst #define FF_FS_REENTRANT 0 // RTOS 下改成 1FF_USE_LFN开了之后长文件名会被解析到 FIL 结构里每次 f_open 会多分配FF_MAX_LFN字节的缓冲区。STM32 裸机全局内存本来就紧FF_MAX_LFN设成 255 和 128 在界面上没区别但每个同时打开的文件对象会多占一倍空间。FF_USE_STRFUNC看起来诱人它带来了 f_printf 和 f_gets但底层会依赖一个字符输出回调我一般只在调试固件时打开正式版改回 0。如果你想用 f_mkfs 对空卡直接格式化必须把FF_USE_MKFS打开否则调用时返回 FR_NOT_ENABLED。宏定义典型值影响选型理由FF_USE_LFN0/1/21 为堆内缓冲2 为栈内缓冲裸机用 1内存紧张用 0FF_MAX_LFN128-255LFN 缓存占用只存短文件名可以 64FF_FS_EXFAT0/18KB 左右额外空间大于 4GB 文件才需要FF_USE_FASTSEEK0/1占用 FAT 缓存日志追写用不到关掉注意FF_USE_LFN设为 2 时LFN 缓冲区使用栈空间在 RTOS 任务栈里很容易溢栈STM32 Cortex-M 上我很少用 2。2.3 裸机与 RTOS 下的配置差异如果只跑裸机FF_FS_REENTRANT保持 0 最省心FatFs 不要求可重入所有共享状态依赖调用方串行化。但有 RTOS 后多个任务同时读文件就不安全了需要把FF_FS_REENTRANT改成 1并定义FF_SYNC_t类型实现ff_cre_syncobj、ff_req_grant、ff_rel_grant、ff_del_syncobj四个回调一般用 FreeRTOS 的SemaphoreHandle_t包一层。GD32/APM32 这类国产 MCU 迁移时标准库和 HAL 库资源很大程度兼容 STM32FatFs 配置可以直接沿用但底层 diskio 里的外设寄存器地址和 DMA 通道号要重新核对。f_mount 返回 FR_OK但 f_read 偶尔读到全 0xFF多半是 DMA 时钟没开或用错了中断线。3. 底层磁盘接口把 SD 卡挂到 FatFs 的 diskio 上3.1 diskio 接口函数清单FatFs 通过 diskio.c 把文件系统的扇区请求转发给硬件。裸机最少要实现 disk_initialize、disk_status、disk_read、disk_write、disk_ioctl 五个函数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);pdrv是物理盘号buff必须是 4 字节对齐的内存SDMMC 外设 DMA 对 buffer 对齐有硬性要求。sector是 512 字节逻辑扇区偏移count一般一次为 1但如果 FatFs 在写大文件时聚成多扇区count 可能到 8 或 16。3.2 SPI 还是 SDMMC两种底层实现怎么选维度SPISDMMC/SDIO引脚占用4-Rx/Tx/SCK/CS6-D0-D3/CLK/CMD速度上限10-20Mbit/s可达 48MHzDMA 绑定SPI 的 DMA 传输专用 DMA 请求卡检测GPIO 轮询支持卡插拔中断适合 MCUF103 常用F407/F4x7/H7STM32F103 系列没有 SDMMC 外设最稳的是用 SPI 模式FatFs 配置不变只需把 disk_read 里的 SPI 传输换成HAL_SPI_TransmitReceive。F407 或 H750 上有 SDMMC建议直接走 4-bit 模式每秒写速率能差一到两个数量级。注意 SD 卡初始化阶段必须用 400kHz 左右时钟等卡返回 ready 后再把时钟切到 25MHz 或更高很多人直接拉到最大频率导致 Card init failed。3.3 代码示例HAL 库的 SDMMC 读函数模板HAL 库里 SDMMC 的读写和 SPI 不一样SDMMC 底层走命令/应答不是简单读写寄存器。下面是我通常会写的示例DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { if (pdrv ! DEV_SD) return RES_PARERR; if (HAL_SD_ReadBlocks(hsd, (uint32_t *)buff, sector, count, SDMMC_TIMEOUT) ! HAL_OK) return RES_ERROR; while (HAL_SD_GetCardState(hsd) ! HAL_SD_CARD_TRANSFER) ; return RES_OK; }HAL_SD_ReadBlocks的入参里没有 DMA buffer 长度它靠count * BLOCKSIZE计算所以buff必须提前分配至少count * 512字节并且按 4 字节对齐声明。调用后的 while 循环卡住说明 DMA 传输没有完成中断要重点检查 SDMMC 的 DMA 中断是否在 NVIC 里使能以及是否开启SDMMC_DMA_MODE。写函数disk_write对应HAL_SD_WriteBlocks返回参数和错误处理逻辑一致。3.4 disk_ioctl 与时钟配置的坑f_mount 成功的前提是 disk_ioctl 能正确响应GET_SECTOR_SIZE和GET_BLOCK_SIZE。GET_SECTOR_SIZE返回 512 是标准答案GET_BLOCK_SIZE返回 1 或扇区数如果不实现FatFs 默认按 512 字节扇区处理但写性能会变差。DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { switch (cmd) { case GET_SECTOR_COUNT: *(LBA_t *)buff (LBA_t)(hd_sector_count); return RES_OK; case GET_SECTOR_SIZE: *(WORD *)buff 512; return RES_OK; case GET_BLOCK_SIZE: *(DWORD *)buff 1; return RES_OK; case CTRL_SYNC: return RES_OK; default: return RES_PARERR; } }GET_SECTOR_COUNT返回卡容量FatFs 算不到正确的文件系统边界时会出各种奇怪错误比如 f_mkfs 能建但 f_mount 找不到卷。HAL 库可以在 SD 初始化后用HAL_SD_GetCardInfo拿到卡容量把它缓存到全局变量。时钟方面在 STM32CubeMX 里把 SDMMC Kernel Clock 配成 48MHz内部再分频SPI 模式则要保证 SCK 在卡初始化时低于 400kHz否则卡直接不应答。4. 从 f_mount 到 f_close数据落盘的完整代码与掉电安全4.1 工作区如何分配FATFS 结构和文件缓冲在裸机 STM32 上FATFS 对象、FIL 对象和读写缓冲区一般用静态方式分配。放在任务栈里容易因为 FS 内部数组太大导致栈溢出放在 malloc 堆里则要确认链式分配不会碎片化。FatFs 的 FATFS 结构体里有一个卷工作区大小由FF_FS_EXFAT等配置决定。我用过很多把 FATFS 对象定义成全局变量的项目在 RTOS 里只初始化一次效果稳定。static FATFS g_fs; static FIL g_file; static BYTE g_io_buf[512] __attribute__((aligned(4)));__attribute__((aligned(4)))不是 C 标准但对 SDMMC DMA 是必须的如果你用 SPI 模式4 字节对齐也不是坏事至少避免部分编译器分配未对齐缓冲区。文件系统的挂载路径建议保持为或0:这取决于你把 SD 卡挂到哪个逻辑驱动器。FatFs 的逻辑驱动器编号由FF_VOLUMES和 f_mount 的 path 参数决定0:对应第一个物理盘。4.2 标准读写流程与参数选择挂载、打开、写入、同步、关闭这条链路是所有日志功能、配置存储的骨架。挂载只做一次放在系统初始化阶段if (f_mount(g_fs, 0:, 0) ! FR_OK) { // 处理 SD 卡未插入或初始化失败 }后面的写入操作尽量复用全局 FIL避免反复 open/closeFRESULT fr f_open(g_file, 0:/datalog.bin, FA_OPEN_ALWAYS | FA_WRITE); if (fr FR_OK) { f_lseek(g_file, f_size(g_file)); // 移动偏移量到文件末尾实现追写 UINT bw; fr f_write(g_file, log_buf, len, bw); f_sync(g_file); f_close(g_file); }FA_OPEN_ALWAYS | FA_WRITE会打开文件如果不存在则创建这里故意不用FA_CREATE_ALWAYS否则每次启动都会清空旧日志。f_lseek 配合 f_size 是为了追加。f_write 的返回值 bw 必须检查如果 bw 小于 len说明存储介质写满或底层发生错误继续写下一块可能反复触发 FR_INT_ERR。f_sync 保证文件和 FAT 表都刷到介质代价是额外读几次 FAT。如果日志对连续性要求不高我通常在写完 4KB 或 1 秒数据后只调一次 f_sync。打开模式的选择用下面这个小表记模式组合行为适用场景FA_CREATE_ALWAYS每次都建新文件覆盖旧文件临时文件FA_OPEN_ALWAYS存在即打开不存在创建日志追写FA_OPEN_APPEND打开并把偏移移到末尾不需要 f_lseek简单追加FA_READFA_WRITE同时读写FA_OPEN_APPEND很方便但注意它是将第一个可用写入位置设为文件尾期间没有调用 f_lseek 的灵活性如果需要在末尾前插入或维护头部还是用显式 f_lseek 更可靠。4.3 掉电安全f_sync 的成本与时机FAT 文件系统本身不是日志型文件系统突然掉电可能丢失最近写入的簇链。FatFs 把数据写进扇区缓存后需要 f_sync 或 f_close 才会把缓存和 FAT 表回写。f_sync 做的是一次「文件缓存 FAT 表同步」操作频繁调用会让 SD 卡卡在刷盘状态写入速度可能掉一半。对应到工程上如果是数据记录建议攒满一个扇区再写并定期 f_sync如果是配置文件每次写完立即 f_sync再考虑是否 f_close。对于电池供电的设备最好在掉电检测中断里关闭全局中断然后直接调 f_sync保证最后一段数据不丢。这个中断优先级要高于外设中断但执行时间不能长。4.4 数据记录器示例定时器ADC 采样写 SD下面是一个典型的数据记录场景定时器 10ms 触发一次 ADC 采样攒满 512 字节后写入 datalog.bin。static uint8_t databuf[512]; static uint16_t data_idx 0; void TIM_IRQHandler(void) { // 触发 ADC 转换保存结果到 last_adc_raw if (data_idx sizeof(last_adc_raw) sizeof(databuf)) { memcpy(databuf[data_idx], last_adc_raw, sizeof(last_adc_raw)); data_idx sizeof(last_adc_raw); } if (data_idx sizeof(databuf)) { sd_append_block(databuf, data_idx); // 内部 f_open/f_write/f_sync data_idx 0; } }让 ADC 采样与 SD 写入分离是关键。直接在中断里调用 FatFs 的 f_open、f_lseek 很容易打断当前文件操作产生不可预期状态所以我的做法是在主循环或低优先级任务中处理实际写入中断只负责填充缓冲。sd_append_block里的 f_sync 不必每 512 字节都做可以计数到 4 个 block 才 sync 一次速度和安全性之间有个平衡点。5. 性能优化与错误码把卡上数据拿回来检查5.1 性能优化FatFs 的写性能瓶颈通常在 FAT 回写和缓存命中率。把 f_write 里的 buffer 大小对齐到扇区的整数倍一次写 512 的 4 倍次数能降低四分之三。FF_USE_FASTSEEK打开后文件对象里要分配一个 FAT 映射数组数据量越大越消耗 RAM适合大文件随机读而不是顺序写。另一个常见的优化是调整簇大小。格式化成 SD 卡时如果文件系统簇是 32KB写一个 1KB 的小文件也要占用一整簇FAT 表项反而少。日志多文件写的话簇保持默认就可以但用 f_getfree 检查剩余空间比在 Windows 上格式化更贴近实际。没有特殊需求在 STM32 上让 FatFs 按默认 512 字节扇区定时写入即可。5.2 错误码速查与常见卡死场景错误码含义排查方向FR_DISK_ERR底层磁盘错误disk_read/write 返回值非 RES_OKFR_NOT_READY存储设备未正确初始化disk_initialize 未调用成功FR_NO_FILE文件不存在f_open 路径写错或卡在其它 FATFR_NOT_ENABLED逻辑盘未挂载参数或路径前缀不对检查 f_mountFR_EXIST同名文件已存在FA_CREATE_NEW 下文件已出现FR_DENIED权限拒绝只读介质或打开模式冲突卡死场景大多是 DMA 中断优先级设置不当或 disk_read 里的 while 等待状态死循环。有效验证手段是在 SD 卡拔插事件里重新调用 disk_initializeSTM32CubeMX 生成的 SDMMC 库在卡移除后会返回卡状态错误但 FatFs 不会主动感知这时要把 disk_status 里的 STA_NODISK 和 STA_PROTECT 位正确置位否则 f_mount 会一直认为卡还在。5.3 验证文件内容从卡上读出来或直接挂到 PC把 SD 卡插到读卡器上用 hexdump 直接看前 512 字节是最快的定位方式hexdump -C /dev/sdc1 | head如果看到文件名和目录项说明 FAT 表结构正确。日志文件可以用strings提取可打印字符串看写入是否按预期换行再用fsck.vfat -n /dev/sdc1检查 FAT 链是否有错误。注意 hexdump 前几百字节就能判断扇区字节序是否被 MCU 反转这能解释为什么 FatFs 里读出的 f_size 一致但 PC 端打不开文件。看着卡上的数据回退到 FatFs 侧定位问题比反复读寄存器效率高得多。本文还有配套的精品资源点击获取