简介:本资源面向嵌入式开发初学者与进阶工程师,提供在STM32平台上通过SPI接口驱动SD卡、移植FATFS文件系统并生成CSV数据文件的完整工程。项目以STM32F429高性能型号为硬件基础,涵盖SPI主模式配置、FATFS工作区与扇区参数设定,以及f_open、f_printf、f_close等API的读写流程,同时涉及文件打开失败、内存不足、写保护等错误处理与资源优化策略。压缩包共1482个文件,以629个C源文件、382个头文件、144个工程描述文件及144个编译中间文件为主,另含库文件、启动代码、IOC配置与HEX固件等,整体约59.43MB,目录结构完整,便于直接编译与二次开发。目前已有1113人学习下载。读者可从中掌握STM32文件系统集成方法、CSV格式数据记录技巧及嵌入式存储方案设计思路,适合作为课程设计、数据采集项目或自学练手的参考工程。
1. STM32 挂 SD 卡跑 FatFs 并生成 .csv:一条被低估的数据落盘链路
做数据采集的嵌入式项目,绕不开一个朴素需求:把传感器读数按时间顺序存成一张表,插到电脑上双击就能用 Excel 打开。STM32 + SD 卡 + FatFs + .csv 这套组合,就是满足这个需求最稳、成本最低的方案。它解决的不是"能不能存"的问题,而是"存出来的文件能不能被 PC 直接识别、能不能边采边写不丢数据、断电后文件还能不能读"这三个真问题。适合谁?做环境监测、电机参数记录、电池充放电曲线、毕业设计里要交数据表的人。很多人卡在 f_write 返回 FR_OK 但文件是空的,或者写了几百行后卡死,本质是没搞懂 FatFs 的缓冲机制和 SD 卡的块写入特性。这篇按我实际调过的路径,从 CubeMX 配置讲到 csv 格式化写入,把参数和坑一次说清。
2. 先搞懂 FatFs 在 STM32 上到底怎么跑起来
2.1 三层结构:SD 卡驱动、磁盘 IO 层、FatFs 内核
FatFs 不是一个能独立运行的库,它是一套文件系统逻辑,底下必须挂一个"磁盘 IO 层"(diskio.c),再底下才是真正的 SD 卡物理驱动。这三层的关系决定了你排错时该看哪一层。
最底层是 SD 卡通信。STM32 接 SD 卡有两种模式:SPI 模式和 SDIO 模式。SPI 模式接线简单(CS、SCK、MISO、MOSI 四根线),速率一般能到几 Mbps 到十几 Mbps,适合中低速采集;SDIO 模式是 4 位并行,速率能到 20MB/s 以上,适合高速连续写。新手我建议先用 SPI 跑通,因为 SPI 的时序问题好抓,逻辑分析仪一挂就看得见;SDIO 一旦初始化失败,报错信息很含糊。
中间层是 diskio.c,FatFs 通过 disk_initialize、disk_read、disk_write、disk_ioctl 这四个函数跟硬件打交道。你要做的就是把 SD 卡的块读写函数填进去。注意一个关键点:FatFs 读写的最小单位是扇区(通常 512 字节),disk_write 一次可能要求写多个扇区,你的驱动必须支持连续多块写,不能只写一块。
最上层才是 f_open、f_write、f_close 这些 API。FatFs 内部有窗口缓冲(默认 512 字节),你调 f_write 写 20 字节,它不一定立刻落到卡上,而是先攒在缓冲里,等攒满一个扇区或者你调 f_sync 才真正写卡。这就是为什么"写完不关文件,拔卡一看是空的"——数据还在 RAM 缓冲里。
2.2 CubeMX 里把 SDIO/SPI 和 FatFs 勾上
以 SDIO 模式为例,CubeMX 里的操作路径是:Connectivity 里选 SDIO,模式选 SD 4-bit 或 1-bit;Middleware 里选 FATFS,勾上 SD Card。生成代码后,CubeMX 会自动帮你把 diskio.c 里的 SD 卡读写接到 HAL_SD 驱动上,省掉大量手写工作。
几个必须确认的参数:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| SDIO 时钟分频 | 初始化阶段用低速,跑通后再提 | 分频系数太小会导致初始化失败 |
| Bus Width | 4-bit(硬件支持时) | 1-bit 更稳但慢 |
| FATFS 扇区大小 | 512 | 与 SD 卡物理扇区一致 |
| _USE_LFN | 1 或 2 | 需要长文件名就开,占 Flash |
| _FS_TINY | 0 | 用独立窗口缓冲,性能更好 |
| _FS_REENTRANT | 0 | 单任务裸机不用开 |
生成后先别急着写 csv,先跑一个"挂载 + 列目录"的最小验证:
FATFS fs; FIL fil; FRESULT res; UINT bw; res = f_mount(&fs, "0:", 1); // 1 表示立即挂载 if (res != FR_OK) { printf("mount fail: %d\r\n", res); // 常见 3=FR_NOT_READY, 13=FR_NO_FILESYSTEM return; } printf("mount ok\r\n");这段代码的逻辑是:f_mount 的第三个参数为 1 时会立刻调用 disk_initialize 和读引导扇区。如果返回 FR_NOT_READY(值为 3),说明卡没初始化成功,去查 SDIO 时钟和供电;如果返回 FR_NO_FILESYSTEM(值为 13),说明卡上没有 FatFs 能识别的分区表,需要用电脑重新格式化成 FAT32。这一步过了,后面才有意义。
2.3 从 f_open 到 f_write:一次完整落盘的最小闭环
挂载成功后,写文件的标准流程是 open → write → close,中间可以穿插 f_sync。下面是一个能直接抄的最小例子:
res = f_open(&fil, "0:/data.csv", FA_CREATE_ALWAYS | FA_WRITE); if (res != FR_OK) { printf("open fail: %d\r\n", res); return; } char header[] = "timestamp,temp,voltage\r\n"; res = f_write(&fil, header, sizeof(header) - 1, &bw); if (res != FR_OK || bw != sizeof(header) - 1) { printf("write header fail\r\n"); } char line[64]; int n = snprintf(line, sizeof(line), "%lu,%.2f,%.3f\r\n", HAL_GetTick(), 25.6f, 3.72f); res = f_write(&fil, line, n, &bw); f_sync(&fil); // 强制把缓冲刷到卡上 f_close(&fil); // 关闭并更新目录项逻辑说明:FA_CREATE_ALWAYS 表示文件存在就覆盖,适合每次上电重新记录;如果你要追加,用 FA_OPEN_ALWAYS 并把读写指针移到末尾(f_lseek(&fil, f_size(&fil)))。f_write 的第四个参数 bw 是实际写入字节数,必须检查它等于你期望的长度,否则说明卡写满了或出错。f_sync 是关键,它把 FatFs 内部缓冲和目录项都刷到物理卡上,代价是每次调用都会触发实际写操作,频繁调用会拖慢速度。
参数上,snprintf 的缓冲区大小要留够,一行 csv 一般不超过 128 字节。浮点用 %.2f 格式化,注意有些精简版 newlib 默认不带浮点 printf,需要在工程里开 "Use float with printf" 选项,否则打出来是空的或者乱码,这个坑我踩过。
3. 把传感器数据写成标准 .csv 的几个关键动作
3.1 csv 格式规范:逗号、换行、转义
csv 看着简单,但要让 Excel 和 pandas 都能正确解析,有几条硬规则。字段之间用英文逗号分隔,行尾用 \r\n(Windows 风格)兼容性最好,纯 \n 在部分 Excel 版本里会全部挤在一行。如果某个字段本身含逗号或引号,要用双引号把整个字段包起来,内部的双引号写成两个。传感器数据一般是数字,不涉及转义,但时间戳如果格式化成 "2024-01-01 12:00:00" 这种带空格的,建议也加引号。
表头建议第一行就写清楚列名,别省。我见过有人为了省空间不写表头,结果数据交上去没人看得懂哪列是什么。表头用英文或拼音,避免中文编码问题——FatFs 默认不做 UTF-8 BOM 处理,中文表头在 Excel 里可能乱码。
3.2 用 snprintf 拼行,避免 f_printf 的坑
FatFs 提供了 f_printf,用起来像 printf,但它内部实现比较简陋,对浮点和长整型的支持依赖配置,而且每次调用都有函数开销。我一般用 snprintf 先拼到内存缓冲,再一次性 f_write,这样既可控又快。
typedef struct { uint32_t ts; float temp; float voltage; } Sample_t; void write_sample(FIL *fp, Sample_t *s) { char buf[80]; int len = snprintf(buf, sizeof(buf), "%lu,%.2f,%.3f\r\n", (unsigned long)s->ts, s->temp, s->voltage); if (len <= 0 || len >= sizeof(buf)) return; // 格式化失败或截断 UINT bw; FRESULT r = f_write(fp, buf, len, &bw); if (r != FR_OK || bw != len) { // 记录错误,考虑重试或停止采集 } }参数说明:buf 大小按最长行估算,80 字节够存时间戳加两个浮点。snprintf 返回的是"本该写入的长度",如果它大于等于 buf 大小,说明被截断了,这行数据不完整,应该丢弃或扩大缓冲。f_write 之后检查 bw 是必须的,SD 卡写失败(比如接触不良)时 bw 会小于 len。
3.3 采集频率和缓冲策略:多久 sync 一次
这是性能和可靠性的核心权衡。f_sync 太频繁,每次都要写卡,SD 卡单次写有毫秒级延迟,1kHz 采集根本扛不住;f_sync 太少,断电丢的数据多。
我的经验值:采集频率 10Hz 以下,每行写完就 sync,简单可靠;10Hz 到 100Hz,攒 10 到 50 行 sync 一次;100Hz 以上,用双缓冲加 DMA,采集和写卡分离,写卡任务按扇区批量写。裸机环境下可以用一个环形缓冲,主循环里判断缓冲里攒够 N 行就批量 f_write 一次,再定期 f_sync。
#define FLUSH_EVERY 20 static int cnt = 0; void maybe_flush(FIL *fp) { if (++cnt >= FLUSH_EVERY) { f_sync(fp); // 每 20 行刷一次 cnt = 0; } }注意 f_sync 返回 FR_OK 才代表真的落盘了,如果返回错误,说明卡可能已经掉了,这时候继续写只会一直失败,应该报警并停止。
4. 避坑与排查:那些让文件写不出来的真凶
4.1 现象:f_write 返回 FR_OK 但文件是 0 字节
原因:数据还在 FatFs 的窗口缓冲里,没调 f_sync 也没 f_close,或者调了 f_close 但没检查返回值。另一个可能是 f_open 用了 FA_READ 却去写。
解决:写完必须 f_close,或者中途 f_sync。检查 f_open 的模式标志里有没有 FA_WRITE。用 f_size 查一下当前文件大小确认。
4.2 现象:写几百行后卡死,f_write 一直返回 FR_DISK_ERR
原因:SD 卡写块时出现超时,常见于 SPI 模式下时钟太快、卡质量差、或者供电不足。也可能是 FatFs 的 _MAX_SS 配置和实际扇区不匹配。
解决:降低 SPI/SDIO 时钟,初始化阶段用 400kHz 以下,稳定后再提。给 SD 卡供电加 100uF 以上电容。检查 diskio.c 里 disk_write 的超时返回值,别让它死等。
4.3 现象:文件在电脑上打不开,提示文件系统损坏
原因:写卡过程中直接拔卡或断电,目录项(FAT 表)没更新完整。FatFs 的目录项是在 f_close 或 f_sync 时才写的。
解决:采集结束前一定走正常 f_close 流程。如果必须断电,至少保证定期 f_sync。可以在文件末尾写一个结束标记,上位机解析时校验。
4.4 现象:csv 里浮点数变成 0.00 或乱码
原因:工程没开浮点 printf 支持,snprintf 对 %f 处理不了。或者 float 在传参时被隐式转成 double,栈空间不够。
解决:在 IDE 的链接选项里勾选支持浮点的 printf(Keil 里是 Use MicroLIB 加 float 选项,或标准库加 --fpmode)。或者干脆把浮点乘 100 转成整数再格式化,避免依赖。
4.5 现象:f_mount 返回 FR_NO_FILESYSTEM
原因:卡是 exFAT 或 NTFS 格式,FatFs 默认只认 FAT12/16/32。或者卡根本没分区表。
解决:用电脑把卡格式化成 FAT32,分配单元大小选 4096 或 512。大容量卡(64G 以上)Windows 默认给 exFAT,需要用工具强制格成 FAT32。
5. 进阶:让 csv 落盘更快、更稳、更好用
跑通基础流程后,真正拉开差距的是吞吐和可靠性。我一般会做三件事。
第一,把写卡从主循环里挪出去。用一个环形缓冲,采集中断或定时器往里塞数据,主循环或 DMA 完成回调里批量取出来写卡。这样采集频率和写卡速度解耦,1kHz 采样配 100ms 批量写完全可行。环形缓冲的大小按"最坏情况下写卡阻塞时间 × 采集速率"估算,比如写一次卡最多 50ms,1kHz 采集,那缓冲至少要能存 50 个样本,留一倍余量取 128。
第二,文件名带时间戳或序号,避免覆盖。用 RTC 取日期拼成 "log_20240101_120000.csv",或者用序号递增。FatFs 支持 f_stat 检查文件是否存在,存在就换名。
第三,加一层数据校验。每行末尾可以加一个简单的累加和或者 CRC,上位机解析时校验,能发现传输或存储过程中的位翻转。对可靠性要求高的场景,还可以写两遍或者用日志式追加。
验证方法上,我习惯用两个手段:一是写完把卡插电脑,用 Excel 和 Python 的 pandas 各读一遍,确认行列对齐、数值范围合理;二是故意在写入过程中断电,重启后看文件能不能打开、已 sync 的数据在不在。这个"断电测试"最能暴露缓冲策略的问题。
最后说个我自己的习惯:每次新板子第一次跑 SD 卡,我一定先用示波器看 SDIO 的 CLK 和 CMD 线,确认初始化时序正常,再去看 FatFs 的返回值。因为 FatFs 的报错永远是"上层现象",真正的根因八成在物理层。这套链路调通一次,后面换传感器、换采样率都是改改格式化字符串的事。希望帮到你。
本文还有配套的精品资源,点击获取