
1. 项目缘起与整体设计思路1.1 为什么要在STM32上折腾CSV文件做过数据采集类项目的朋友大概都有这个体会传感器数据攒了一堆串口打印出来刷屏刷得眼花想存下来做后续分析要么得上位机实时接收要么就得在板子本地找个地方落盘。SD卡几乎是嵌入式场景里成本最低、容量最灵活的选择而CSV这种纯文本表格格式又恰好是PC端Excel、Python、MATLAB都能直接打开的通吃格式。把这两者结合起来STM32加FatFs读写SD卡里的CSV文件就成了一个非常实用的技能组合。这个项目要解决的问题很具体让STM32能够周期性地把采集到的数据以CSV格式写入SD卡同时也能反过来读取SD卡里已有的CSV文件做解析。适合谁看正在做毕业设计需要存数据的同学、做工业记录仪需要导出报表的工程师、以及任何想把单片机数据落到可读文件里的开发者。哪怕你之前只用过串口打印跟着走一遍也能把这条链路打通。1.2 整体方案选型与架构拆解整个系统的分层其实很清晰从下往上依次是物理层SD卡、通信层SPI或SDIO、驱动层、文件系统层FatFs、应用层CSV读写。这里每一个环节的选型都会影响最终的性能和稳定性我逐个说一下取舍逻辑。物理层没什么好选的标准SD卡或者microSD卡加卡座就行。通信层是第一个分水岭SPI模式接线简单四根线搞定几乎任何STM32都能用代价是速度慢理论最高也就几MB/s实际写CSV这种小数据量场景够用SDIO模式速度快4位总线能跑到十几MB/s甚至更高但引脚固定、配置复杂而且不是所有型号都带SDIO外设。我的建议是如果你的数据量不大、写入频率不高SPI模式完全够用别为了追求速度给自己找麻烦如果是高速连续采集比如音频或者振动信号那必须上SDIO。驱动层这块SPI模式下需要自己实现SD卡的初始化流程和扇区读写命令SDIO模式下STM32的HAL库已经提供了完整的SD卡驱动直接调用就行。文件系统层选FatFs几乎是默认答案它轻量、可裁剪、支持FAT16/FAT32/exFAT而且和STM32的适配资料极其丰富。应用层就是CSV的格式化与解析这部分没有现成库得自己写但逻辑不复杂。提示FatFs的作者ChaN一直在维护这个项目建议去官网下载最新版本不要用某些IDE里自带的远古版本老版本对exFAT和大容量卡的支持有问题。1.3 关键参数与性能预期在动手之前心里要有个性能预期不然调通了发现速度不达标会白折腾。SPI模式下假设SPI时钟跑到18MHz单次扇区读写512字节理论传输时间大约28微秒但加上命令开销、等待响应、FatFs的簇管理开销实际写入速度大概在100KB/s到300KB/s之间。SDIO 4位模式下时钟25MHz理论能到12.5MB/s实际受限于卡本身和FatFs的缓冲策略通常能跑到1MB/s到4MB/s。对于CSV写入来说假设每行数据50字节每秒采集100次那就是5KB/s的写入需求SPI模式绰绰有余。但如果每秒要写1000行那就是50KB/sSPI模式开始吃力建议上SDIO。这个估算方法你可以直接套用到自己的项目里先算数据速率再选通信方式。2. 核心细节解析与实操要点2.1 FatFs的移植与配置要点FatFs移植的核心是提供两个底层函数disk_read和disk_write以及disk_initialize、disk_status、disk_ioctl这几个辅助函数。SPI模式下disk_read和disk_write内部就是调用SD卡的扇区读写命令SDIO模式下直接调用HAL_SD_ReadBlocks和HAL_SD_WriteBlocks即可。配置方面ffconf.h里有几个关键宏需要根据实际情况调整。FF_USE_LFN建议开启设成1或2这样支持长文件名不然你的文件名只能8.3格式叫个data_log.csv都费劲。FF_FS_EXFAT如果要用64GB以上的卡就开启否则关掉省空间。FF_FS_TINY建议关掉虽然省一点RAM但会牺牲性能。FF_VOLUMES设成1就行除非你有多张卡。注意FF_USE_LFN开启后需要提供一个Unicode转换表或者使用FF_CODE_PAGE对应的编码页否则中文文件名会乱码。如果只用英文文件名设FF_CODE_PAGE为437即可。还有一个容易忽略的点是FF_FS_REENTRANT如果你在RTOS环境下多任务访问SD卡必须开启这个宏并实现互斥锁否则会出现文件系统崩溃。裸机环境下关掉就行。2.2 SD卡初始化的坑与排查SD卡初始化是新手最容易卡住的地方。SPI模式下初始化流程大致是上电后至少给74个时钟周期、发送CMD0进入Idle状态、发送CMD8检查电压范围、发送ACMD41等待卡就绪、发送CMD58读取OCR寄存器确认电压、最后发送CMD16设置扇区大小。这里有几个高频踩坑点。第一上电后的74个时钟必须保证CS片选拉高否则卡可能进入未知状态。第二CMD8的响应如果返回0x05说明是V2.0以上的卡如果返回0x01说明是老卡后续流程要区别对待。第三ACMD41要循环发送直到返回0x00超时时间建议设长一点有些卡上电慢给个1秒不过分。第四CMD16设置扇区大小为512字节虽然大部分卡默认就是512但显式设置一下更稳妥。SDIO模式下初始化简单很多HAL库的HAL_SD_Init会帮你处理大部分流程但要注意时钟分频配置。初始化阶段要用低速时钟通常不超过400KHz初始化完成后再切换到高速。这个切换如果忘了做要么初始化失败要么速度上不去。2.3 CSV格式的生成与解析细节CSV看起来简单但真写起来有几个细节要注意。首先是分隔符标准CSV用逗号但有些地区的Excel默认用分号这个在生成文件时要确认目标环境。其次是换行符Windows用\r\nLinux用\n为了兼容性建议用\r\n。第三是字段中如果包含逗号或引号需要用双引号包裹并对内部引号做转义不过嵌入式场景下一般不会遇到这么复杂的数据。写入的时候用f_printf或者自己拼字符串再f_write都行。f_printf方便但速度慢因为它内部做了格式化解析。如果追求速度建议用sprintf拼到缓冲区再一次性f_write或者更极致一点手动拼接字符串。我实测下来手动拼接比f_printf快三倍以上。读取解析的时候用f_gets按行读取然后用strtok按逗号分割字段。注意strtok会修改原字符串所以如果后续还要用原始行数据得先拷贝一份。另外f_gets读到的行末尾会带\r\n解析前记得去掉。3. 实操过程与核心环节实现3.1 硬件连接与工程搭建先说一下SPI模式的接线。以STM32F103为例SPI1的引脚是PA5SCK、PA6MISO、PA7MOSI片选CS可以随便选一个GPIO比如PA4。SD卡座的CD卡检测引脚可以接一个GPIO做插拔检测不接也行。供电是3.3V注意SD卡座有的带电平转换芯片有的不带不带的话直接接3.3V就行STM32的IO也是3.3V电平匹配。工程搭建方面用STM32CubeMX生成初始化代码最省事。配置SPI为全双工主模式时钟分频先设大一点比如256分频初始化完成后再改小。CS引脚配置为GPIO输出初始拉高。然后开启一个串口用于调试打印。FatFs的源码直接添加到工程里把diskio.c里的底层函数实现好。// diskio.c 中 SPI 模式的 disk_read 实现示例 DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { if (pdrv ! 0) return RES_PARERR; if (SD_ReadBlocks(buff, sector, count) ! 0) return RES_ERROR; return RES_OK; }SD_ReadBlocks内部就是拉低CS、发送CMD17、等待响应、读取512字节数据、拉高CS这一套流程。写操作类似用CMD24。3.2 文件系统挂载与CSV写入实现挂载文件系统很简单调用f_mount就行。注意f_mount只是注册工作区真正的挂载动作在第一次访问文件时才会执行。如果要确认挂载是否成功可以调用f_getfree获取剩余空间返回FR_OK就说明挂载成功。FATFS fs; FIL fil; FRESULT res; UINT bw; res f_mount(fs, 0:, 1); if (res ! FR_OK) { printf(Mount failed: %d\r\n, res); return; } res f_open(fil, 0:/data.csv, FA_CREATE_ALWAYS | FA_WRITE); if (res ! FR_OK) { printf(Open failed: %d\r\n, res); return; } // 写入表头 f_write(fil, timestamp,temp,humi\r\n, 21, bw); // 写入数据行 char buf[64]; int len sprintf(buf, %lu,%.2f,%.2f\r\n, tick, temp, humi); f_write(fil, buf, len, bw); f_close(fil);这里有个性能优化的点不要每写一行就f_open和f_close一次那样每次都要更新FAT表速度极慢。正确的做法是打开文件后持续写入定期调用f_sync刷新缓冲区采集结束后再f_close。f_sync的频率可以根据数据重要程度决定比如每100行同步一次这样即使断电最多丢100行数据。3.3 读取CSV并解析的完整流程读取的流程和写入对称。打开文件用FA_READ模式然后循环调用f_gets读取每一行。f_gets的第二个参数是缓冲区大小第三个参数是文件对象。读到NULL说明到文件末尾或者出错。res f_open(fil, 0:/data.csv, FA_READ); if (res ! FR_OK) return; char line[128]; while (f_gets(line, sizeof(line), fil)) { // 去掉行尾的 \r\n char *p strpbrk(line, \r\n); if (p) *p \0; // 跳过表头 if (strncmp(line, timestamp, 9) 0) continue; // 解析字段 char *token strtok(line, ,); int field 0; while (token) { switch (field) { case 0: timestamp strtoul(token, NULL, 10); break; case 1: temp atof(token); break; case 2: humi atof(token); break; } token strtok(NULL, ,); field; } // 处理解析后的数据 } f_close(fil);解析的时候要注意数值转换的边界情况。atof遇到非数字字符串会返回0如果CSV里有空字段或者非法字符需要额外判断。strtoul类似最好检查一下转换后的指针是否指向了预期位置。3.4 性能优化与缓冲策略前面提到SPI模式下写入速度大概100到300KB/s如果这个速度不够用有几个优化方向。第一增大SPI时钟F103的SPI1最高能到18MHzF4系列能到更高在信号质量允许的前提下尽量拉高。第二减少f_sync的调用频率但要在数据安全和速度之间权衡。第三使用FatFs的多扇区写入功能f_write内部如果检测到连续写入多个扇区会调用disk_write的count参数大于1的情况所以底层disk_write要支持多扇区写入不要每次只写一个扇区。还有一个容易被忽略的点是SD卡本身的写入特性。SD卡内部有擦除块的概念通常是128KB或者更大如果写入的数据跨越了擦除块边界卡内部会做读-改-写操作速度会骤降。所以如果可能的话尽量让文件大小是擦除块的整数倍或者至少让写入操作对齐到扇区边界。实操心得我在一个振动采集项目里SPI时钟18MHz每秒钟写200行、每行80字节也就是16KB/s的数据量实测下来CPU占用率不到5%完全不影响主循环的其他任务。所以大部分场景下SPI模式真的够用不用盲目上SDIO。4. 常见问题与排查技巧实录4.1 挂载失败与初始化异常排查挂载失败是最常见的问题f_mount返回FR_NOT_READY或者FR_NO_FILESYSTEM。前者说明底层驱动有问题卡没初始化成功后者说明卡初始化成功了但文件系统不识别可能是卡没格式化或者格式不对。排查步骤我一般是这样先用一个简单的测试程序不跑FatFs直接调用disk_initialize看返回值。如果返回0说明初始化成功返回非0就要查SPI接线、时钟配置、上电时序。初始化成功后用disk_read读第0个扇区打印出来看有没有MBR或者DBR签名。如果第0扇区全是0或者全是0xFF说明读操作有问题。如果第0扇区有数据但FatFs不认那可能是卡的文件系统是exFAT而FatFs没开启exFAT支持或者卡是GPT分区表而FatFs只认MBR。还有一个隐蔽的坑是卡的质量。有些便宜的卡在SPI模式下兼容性很差初始化偶尔成功偶尔失败。遇到这种情况换一张品牌卡试试往往问题就消失了。4.2 写入数据丢失与文件损坏分析写入数据丢失通常有几个原因。第一没有调用f_sync或f_close就断电了数据还在FatFs的缓冲区里没落盘。第二f_write返回的bw小于请求写入的字节数说明磁盘满了或者出错了但代码没检查。第三SD卡在写入过程中被拔出导致文件系统损坏。文件损坏的表现是PC上打不开或者内容乱码。这种情况一般是FAT表或者目录项被写坏了。预防措施是定期f_sync以及在检测到卡拔出时立即停止写入并关闭文件。如果已经损坏了可以用PC上的chkdsk或者专门的SD卡修复工具尝试恢复但成功率看运气。注意f_write之后一定要检查返回值FR_OK不代表全部字节都写成功了还要看bw是否等于请求的字节数。这个细节很多人会忽略。4.3 读写速度不达标的调优思路速度不达标先定位瓶颈在哪一层。用GPIO翻转加示波器测量disk_write的执行时间如果单次扇区写入超过1毫秒说明SPI时钟太低或者卡响应慢。如果disk_write很快但整体写入慢那瓶颈在FatFs的簇管理或者f_sync的频率。SPI时钟的调整要注意信号完整性。时钟拉高后如果出现写入错误可能是杜邦线太长或者没有加合适的上拉电阻。SD卡规范要求SPI模式下MISO和MOSI要有上拉很多模块自带但如果自己搭电路别忘了加。FatFs这边FF_FS_TINY关掉、FF_USE_FASTSEEK开启、FF_FS_EXFAT按需开启这几个配置对速度有影响。另外文件不要频繁打开关闭保持打开状态持续写入是最快的。4.4 常见问题速查表现象可能原因排查方法解决措施f_mount返回FR_NOT_READYSD卡初始化失败单独测试disk_initialize检查接线、时钟、上电时序f_mount返回FR_NO_FILESYSTEM文件系统不识别读第0扇区看签名格式化卡或开启exFAT支持f_open返回FR_NO_PATH路径不对打印路径字符串确认盘符和文件名格式写入后PC打不开文件文件系统损坏用chkdsk检查定期f_sync拔出前关闭文件写入速度慢SPI时钟低或sync频繁示波器测disk_write耗时提高时钟降低sync频率中文文件名乱码编码页不匹配检查FF_CODE_PAGE设成936或开启LFN多任务访问崩溃没有互斥保护检查FF_FS_REENTRANT开启并实现互斥锁大容量卡不识别分区表或格式问题查看卡的分区类型格式化为FAT32或开启exFAT这张表里的问题我基本都遇到过尤其是文件系统损坏那个早期没经验的时候经常掉数据后来养成了定期f_sync的习惯就再也没出过事。4.5 几个容易被忽略的实操细节第一个细节是文件名的长度。FatFs在FF_USE_LFN为0时只支持8.3格式也就是文件名最多8个字符、扩展名最多3个字符。如果你写了个data_log_2024.csv在LFN关闭的情况下会被截断成DATA_LO.CSV之类的可能导致打开失败。所以要么开启LFN要么老老实实用短文件名。第二个细节是f_printf的浮点数支持。默认情况下FatFs的f_printf不支持%f需要开启FF_PRINT_FLOAT宏。但即使开启了浮点格式化也会显著增加代码体积和运行时间。如果只是记录数据建议在应用层用sprintf格式化好再f_write这样更可控。第三个细节是SD卡的寿命。SD卡有写入次数限制虽然现代卡的磨损均衡做得不错但如果是高频写入场景比如每秒写几十次卡的寿命会受影响。一个缓解办法是攒一批数据再写比如攒够4KB再写一次这样减少写入次数。另一个办法是用工业级SD卡贵是贵点但耐用。第四个细节是掉电保护。如果项目对数据完整性要求高建议加一个超级电容或者备用电池检测到主电源掉电后立即f_sync并f_close这样能最大限度保住数据。我在一个电力监测项目里就是这么做的效果很好。5. 进阶扩展与项目实战建议5.1 从单文件到多文件管理实际项目里往往不是只写一个文件可能需要按日期分文件或者按数据类型分文件。FatFs支持同时打开多个文件但要注意每个打开的文件都会占用一个FIL结构体和一定的缓冲区。STM32的RAM有限同时打开的文件数量不要太多一般三五个就够了。按日期分文件的逻辑是在写入前检查当前日期如果和上次写入的日期不同就关闭旧文件、打开新文件。文件名可以用sprintf拼成20240501.csv这种格式。这里要注意日期来源如果没有RTC可以用一个计数器模拟或者从上位机同步时间。5.2 数据加密与完整性校验有些场景下CSV里的数据需要加密比如涉及商业机密或者个人隐私。嵌入式端的加密方案要权衡安全性和性能。简单的异或加密速度快但安全性低AES加密安全性高但计算量大。如果STM32带硬件加密外设比如F4系列的CRYP那用AES-128比较合适。如果没有硬件加速软件AES在72MHz的F103上大概能跑到几百KB/s对于CSV这种低速数据流够用。完整性校验可以用CRC32在文件末尾追加一行校验值读取的时候重新计算并比对。FatFs本身不提供校验功能需要自己实现。CRC32的查表法在STM32上跑起来很快对性能影响可以忽略。5.3 与上位机的数据交互CSV文件写好后怎么传到PC上也是个问题。最简单的办法是把SD卡拔下来插到读卡器上但频繁插拔对卡座和卡都不好。更好的办法是通过串口或者USB把文件内容传出去。串口的话用YMODEM协议传文件比较方便有很多现成的实现。USB的话可以把STM32模拟成U盘PC直接访问SD卡里的文件这个在STM32的USB库里有例程可以参考。如果项目有无线模块比如蓝牙或者WiFi也可以把CSV内容通过无线传出去。但无线传输的稳定性不如有线大文件传输容易出错建议加上重传机制。5.4 基于STM32的毕业设计选题建议如果你是在做毕业设计这个项目可以往几个方向扩展。一是加传感器做成环境监测记录仪采集温湿度、空气质量等数据存成CSV。二是加显示屏用LCD或者OLED实时显示当前数据和文件状态。三是加上位机软件用Python或者C#写一个解析CSV并画曲线的工具。四是加无线模块做成远程数据采集终端。选题的时候要注意工作量适中不要贪多。一个能稳定运行、有完整数据链路的系统比一堆花哨功能但跑不起来的东西强得多。答辩的时候老师更看重你对底层原理的理解和问题解决能力而不是功能列表的长度。实操心得我在带学生做毕设的时候发现最容易出问题的环节不是代码本身而是硬件连接和电源。SD卡在写入瞬间电流会突然增大如果电源滤波没做好会导致STM32复位或者SD卡写入失败。建议在SD卡座的电源引脚旁边并一个100uF的电解电容加一个0.1uF的陶瓷电容能解决大部分莫名其妙的写入错误。5.5 长期运行稳定性的一些经验如果设备需要7x24小时运行稳定性就是第一位的。除了前面说的定期f_sync和掉电保护还有几个点要注意。第一看门狗必须开而且要在主循环里定期喂狗防止程序跑飞。第二SD卡如果长时间不操作有些卡会进入休眠状态再次访问时需要重新初始化这个在驱动层要做好处理。第三文件大小要监控如果超过一定大小就新建文件避免单个文件过大导致FAT表操作变慢。第四定期检查卡的剩余空间快满了就报警或者删除旧文件。我在一个气象站项目里设备连续运行了半年多每天写一个CSV文件每个文件大概几MB从来没出过问题。关键就是做好了上面这几点尤其是看门狗和定期sync这两个是保命的。最后再分享一个小技巧在CSV文件的第一行表头里可以加上一个版本号或者设备ID比如#v1.0,device01这样后续解析的时候能知道数据格式版本方便做兼容处理。这个习惯我在多个项目里都保留了后期维护的时候省了不少事。