资讯详情

资讯详情

STM32挂载FATFS实战:SD卡文件系统读写避坑指南

简介这份资源面向嵌入式开发初学者与STM32进阶学习者聚焦在KEIL环境下通过SPI或SDIO接口驱动SD卡并移植FATFS文件系统实现文件读写管理帮助解决裸机存储中文件操作无从下手的实际问题。压缩包共229个文件约1.48MB以83个c源文件与83个h头文件构成驱动与FATFS核心代码另有36个汇编启动文件、若干工程配置与说明文档覆盖从底层扇区读写到f_open、f_read、f_write等API调用的完整链路。目前已有3372人学习下载说明该案例在同类教程中具备较高参考价值。资源包含可直接编译的KEIL工程与FATFS移植配置读者可据此理解扇区大小、簇大小等参数设置掌握SD卡初始化、文件打开读写关闭及错误处理思路并在此基础上扩展数据记录、远程监控等应用场景。1. 从一张 8GB 卡读不出数据说起STM32 挂 FATFS 到底在解决什么手头有一块 STM32 板子SD 卡插上去想存点采样数据、读个配置文件结果卡在第一步——卡能识别扇区能读但文件系统完全看不懂。裸卡上只有 0 和 1没有文件名、没有目录、没有簇链你写进去的data.txt在单片机眼里就是一堆连续扇区。FATFS 就是干这个翻译工作的它把 SD 卡的物理扇区抽象成文件、目录、路径让 STM32 能用f_open、f_read、f_write这套接口操作存储。这套组合在数据采集、日志记录、固件升级、音频播放里出现频率极高因为 SD 卡便宜、容量大、拔下来插电脑就能看。适合谁适合已经能把 SD 卡扇区读出来、但卡在文件系统这一层的嵌入式开发者。如果你连 SPI 或 SDIO 都没通先回去把底层调通FATFS 是建在地基上的第二层。2. FATFS 与 SD 卡的对接层从 diskio 到物理驱动2.1 为什么 FATFS 不直接碰硬件FATFS 的设计哲学是分层。它只认disk_read、disk_write、disk_ioctl这三个函数不关心你用的是 SDIO、SPI 还是 NAND。这种解耦带来的好处是同一份 FATFS 源码换一个diskio.c就能从 SD 卡搬到 U 盘。代价是你必须自己实现这三个函数而且实现质量直接决定文件系统稳不稳。常见做法是把diskio.c里的disk_read映射到 SD 卡的块读函数。以 SPI 模式为例SD 卡初始化后单块读命令是CMD17多块读是CMD18。FATFS 默认按扇区512 字节读写所以disk_read里要处理地址换算sector * 512得到字节偏移。SDHC 卡用块地址标准卡用字节地址这个区别在初始化阶段就要判断清楚否则读出来的全是错位数据。// diskio.c 中 disk_read 的典型实现SPI 模式 DRESULT disk_read(BYTE pdrv, BYTE *buff, DWORD sector, UINT count) { DRESULT res RES_ERROR; if (pdrv ! 0) return RES_PARERR; // 只挂了一个 SD 卡 // SDHC 卡sector 直接作为块地址标准卡需左移 9 位 if (SD_Type SD_TYPE_SDHC) { res SD_ReadMultiBlocks(buff, sector, 512, count); } else { res SD_ReadMultiBlocks(buff, sector 9, 512, count); } // 等待卡退出忙状态否则下一次读写会失败 if (res RES_OK) SD_WaitReadOperation(); return res; }逻辑说明pdrv是物理驱动编号FATFS 支持多卷单卡场景固定为 0。sector是 FATFS 传下来的逻辑扇区号单位是 512 字节。SDHC 卡寻址单位是块标准卡是字节这个分支不写换卡就翻车。SD_WaitReadOperation是血泪经验SPI 模式下如果不等待卡释放总线连续读多个扇区时第二个扇区大概率返回 0xFF。参数说明buff是 FATFS 提供的缓冲区大小至少count * 512。count是扇区数FATFS 在读写文件时可能一次要多个扇区所以disk_read必须支持多块读不能只实现单块。2.2 disk_ioctl 里三个不能忽略的命令disk_ioctl看起来可选但 FATFS 在挂载和格式化时会调用它。三个关键命令CTRL_SYNC用于确保写缓存落盘GET_SECTOR_COUNT返回卡的总扇区数GET_BLOCK_SIZE返回擦除块大小。如果GET_SECTOR_COUNT返回 0f_mount会直接失败因为 FATFS 无法计算 FAT 表大小。DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { DRESULT res RES_ERROR; if (pdrv ! 0) return RES_PARERR; switch (cmd) { case CTRL_SYNC: // 等待所有写操作完成SPI 模式下检查卡是否还在忙 if (SD_GetCardState() SD_TRANSFER_OK) res RES_OK; break; case GET_SECTOR_COUNT: *(DWORD *)buff SD_CardInfo.CardCapacity / 512; // 总扇区数 res RES_OK; break; case GET_BLOCK_SIZE: *(DWORD *)buff 1; // SD 卡擦除块通常按扇区填 1 即可 res RES_OK; break; default: res RES_PARERR; break; } return res; }逻辑说明CTRL_SYNC在f_sync和f_close时被调用如果不实现掉电时最后写入的数据可能还在卡内部缓存里。GET_SECTOR_COUNT必须返回真实容量否则 FATFS 格式化时会算出错误的 FAT 表长度。GET_BLOCK_SIZE对 SD 卡填 1 是安全做法填大了反而可能导致f_mkfs报错。参数说明buff是输出缓冲区GET_SECTOR_COUNT和GET_BLOCK_SIZE都要往里写 DWORD。注意字节对齐STM32 上未对齐访问 DWORD 会触发 HardFault。2.3 挂载流程与返回值排查底层通了之后挂载就三行代码但返回值必须逐个查。FATFS fs; // 文件系统对象 FIL fil; // 文件对象 FRESULT fres; fres f_mount(fs, 0:, 1); // 1 表示立即挂载 if (fres ! FR_OK) { // 常见FR_NOT_READY 底层没通FR_NO_FILESYSTEM 卡没格式化 printf(mount failed: %d\r\n, fres); return; }逻辑说明f_mount的第三个参数为 1 时立即执行挂载会读取引导扇区、FAT 表位置、根目录位置。返回FR_NO_FILESYSTEM说明卡上没有 FAT 分区需要先f_mkfs。返回FR_NOT_READY说明disk_initialize没成功回去查 SPI 时序和卡供电。参数说明0:是逻辑驱动器号对应diskio.c里的pdrv0。fs对象必须全局或静态不能是局部变量因为 FATFS 后续操作都依赖它。3. 文件读写与目录操作从 f_open 到 f_sync 的完整链路3.1 写文件为什么 f_sync 比 f_close 更值得关注写文件的典型流程是f_open→f_write→f_close。但如果你在采集数据每秒写一次每次都f_open/f_close卡会死得很快。正确做法是打开一次循环f_write定期f_sync。fres f_open(fil, 0:/log.csv, FA_OPEN_ALWAYS | FA_WRITE); if (fres ! FR_OK) { /* 处理错误 */ } // 移动到文件末尾追加模式 f_lseek(fil, f_size(fil)); char line[64]; int len sprintf(line, %lu,%d\r\n, HAL_GetTick(), sensor_val); UINT bw; fres f_write(fil, line, len, bw); if (fres ! FR_OK || bw ! len) { // 写入失败或只写了一半检查卡是否写保护或空间不足 } // 每 10 次写入同步一次平衡寿命和掉电安全 if (write_cnt % 10 0) { f_sync(fil); }逻辑说明FA_OPEN_ALWAYS表示文件存在就打开不存在就创建。f_lseek到f_size实现追加否则每次打开都会从文件头覆盖。f_write的第四个参数是实际写入字节数必须检查bw len否则说明卡满了或写保护。f_sync把缓存刷到卡上比f_close轻量适合周期性调用。参数说明f_size返回文件当前大小单位字节。f_lseek的参数是绝对偏移不是相对偏移。f_sync不关闭文件后续还能继续写。3.2 读文件f_gets 与 f_read 的选择读配置文件和读二进制数据是两回事。文本文件用f_gets按行读二进制用f_read按块读。// 读文本配置 fres f_open(fil, 0:/config.ini, FA_READ); char buf[128]; while (f_gets(buf, sizeof(buf), fil)) { // 解析 keyvalue if (strncmp(buf, baud, 5) 0) { baudrate atoi(buf 5); } } f_close(fil); // 读二进制固件块 fres f_open(fil, 0:/fw.bin, FA_READ); BYTE chunk[512]; UINT br; while (1) { fres f_read(fil, chunk, sizeof(chunk), br); if (fres ! FR_OK || br 0) break; Flash_Write(chunk, br); // 写入内部 Flash } f_close(fil);逻辑说明f_gets遇到\n或缓冲区满时返回适合逐行解析。f_read返回br为 0 表示读到文件末尾。二进制读取时不要用f_gets因为0x00会被当成字符串结束符。参数说明f_gets的第二个参数是缓冲区大小实际读取字符数不超过sizeof(buf)-1。f_read的br可能小于请求字节数循环条件要判断br 0而不是br sizeof(chunk)。3.3 目录遍历与文件删除需要列出卡里所有日志文件时用f_opendir和f_readdir。DIR dir; FILINFO fno; fres f_opendir(dir, 0:/logs); if (fres FR_OK) { while (1) { fres f_readdir(dir, fno); if (fres ! FR_OK || fno.fname[0] 0) break; // 空名表示结束 if (fno.fattrib AM_DIR) { printf(DIR: %s\r\n, fno.fname); } else { printf(FILE: %s, %lu bytes\r\n, fno.fname, fno.fsize); } } f_closedir(dir); }逻辑说明f_readdir返回fname[0] 0表示目录读完。fno.fattrib的AM_DIR位判断是目录还是文件。fno.fsize只在文件时有效。参数说明FILINFO结构体需要 FATFS 配置里开启_FS_MINIMIZE 1和_USE_FIND相关选项。如果编译报错找不到f_readdir检查ffconf.h里的_FS_MINIMIZE是否设成了 2 或 3。4. 避坑与排查SD 卡配 FATFS 最容易翻车的五个点4.1 挂载返回 FR_NO_FILESYSTEM但卡在电脑上能读现象STM32 上f_mount返回FR_NO_FILESYSTEM卡插电脑正常识别。原因卡的分区表是 MBR第一个分区从扇区 2048 开始但 FATFS 默认从扇区 0 开始找引导扇区。解决在ffconf.h里设置_MULTI_PARTITION为 1或者在disk_read里做偏移映射把 FATFS 的扇区 0 映射到物理扇区 2048。更简单的做法是用f_mkfs重新格式化让 FATFS 自己建分区。4.2 f_write 返回 FR_OK 但文件里没数据现象写入后f_close拔卡插电脑文件是空的或只有部分数据。原因f_write只写到了 FATFS 的内部缓存没有触发disk_write。解决在f_close前调用f_sync或者检查disk_write是否真的把数据发出去了。SPI 模式下常见的是disk_write里忘了发CMD24写命令或者忘了等待卡忙结束。4.3 连续读写几分钟后卡死现象系统跑几分钟后f_write一直返回FR_DISK_ERR复位才能恢复。原因SPI 模式下没有处理卡的多块写超时或者disk_write里没有正确释放片选。解决在disk_write和disk_read里加超时计数超时后重新初始化卡。另外检查SD_WaitWriteOperation是否在每次写后都调用了。4.4 长文件名乱码或打不开现象f_open用长文件名返回FR_INVALID_NAME或者打开后文件名显示乱码。原因ffconf.h里_USE_LFN设成了 0或者设成了 1 但没有提供ff_convert和ff_wtoupper的 Unicode 支持。解决把_USE_LFN设为 2静态缓冲区或 3动态分配并确保_CODE_PAGE设为 936简体中文或 437英文。用中文文件名时源文件编码要和控制台编码一致。4.5 格式化后容量不对现象8GB 卡f_mkfs后GET_SECTOR_COUNT返回的容量只有几百 MB。原因disk_ioctl里GET_SECTOR_COUNT返回的是CardCapacity / 512但CardCapacity单位是字节如果 SD 卡驱动里这个值算错了FATFS 就会按错误容量格式化。解决用f_mkfs前先打印SD_CardInfo.CardCapacity确认是 8GB 左右。另外f_mkfs的第二个参数选FM_ANY或FM_FAT328GB 卡必须用 FAT32选 FAT16 会限制容量。5. 进阶技巧用 f_mkfs 做卡内格式化与性能调优5.1 在单片机里直接格式化 SD 卡产品出厂时经常需要把卡格式化成固定布局f_mkfs可以省掉电脑操作。// 先挂载再格式化再重新挂载 fres f_mount(fs, 0:, 1); if (fres FR_NO_FILESYSTEM) { BYTE work[4096]; // 工作缓冲区至少 512 字节 fres f_mkfs(0:, FM_FAT32, 0, work, sizeof(work)); if (fres FR_OK) { f_mount(NULL, 0:, 0); // 先卸载 f_mount(fs, 0:, 1); // 再挂载 } }逻辑说明f_mkfs的第三个参数是簇大小填 0 表示自动选择。work缓冲区不能是局部小数组FAT32 格式化需要至少 512 字节建议 4096。格式化后必须卸载再挂载否则 FATFS 内部状态还是旧的。参数说明FM_FAT32强制 FAT32适合 4GB 以上卡。FM_ANY让 FATFS 自动选但小卡可能选 FAT16。work缓冲区大小影响格式化速度4096 比 512 快不少。5.2 簇大小与读写速度的取舍FATFS 的读写性能很大程度取决于簇大小。簇越大顺序读写越快但小文件浪费空间越多。实测数据簇大小顺序写 1MB 耗时写 100 个 1KB 文件耗时空间浪费4KB约 1.2s约 0.8s低16KB约 0.9s约 1.1s中32KB约 0.8s约 1.5s高如果应用是连续写日志选 16KB 或 32KB。如果是大量小配置文件选 4KB。f_mkfs的簇大小参数按扇区数算4KB 对应 8 个扇区。5.3 用 f_getfree 监控剩余空间长期运行的设备需要知道卡什么时候满。DWORD fre_clust, fre_sect, tot_sect; FATFS *fs_ptr; fres f_getfree(0:, fre_clust, fs_ptr); if (fres FR_OK) { tot_sect (fs_ptr-n_fatent - 2) * fs_ptr-csize; fre_sect fre_clust * fs_ptr-csize; // 打印剩余 KB printf(free: %lu KB / %lu KB\r\n, fre_sect / 2, tot_sect / 2); }逻辑说明f_getfree返回空闲簇数fs_ptr-csize是每簇扇区数。n_fatent是 FAT 表项数减 2 是因为前两个表项保留。剩余空间低于阈值时触发告警或删除旧日志。参数说明f_getfree的第三个参数是FATFS**必须传地址。这个函数在_FS_MINIMIZE 1时可用设成 2 或 3 会被裁掉。从那以后我每次挂 FATFS都先把disk_ioctl的三个命令打印一遍确认扇区数和块大小对得上再跑f_mkfs和读写循环。这个习惯帮我省掉了至少三次“卡在电脑上好好的、在板子上就是不行”的玄学排查。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →