STM32+SD卡+FATFS+CSV嵌入式数据采集完整实战

发布时间:2026/9/7 17:02:20
STM32+SD卡+FATFS+CSV嵌入式数据采集完整实战 简介面向嵌入式开发者这套基于STM32F429的完整工程实现了SD卡FatFS文件系统读写、生成CSV格式数据文件并集成以太网驱动与TCP服务器适合需要将网络采集数据落盘存储的物联网、工控或数据记录类项目参考代码从SD卡底层驱动、FatFS移植配置到上层文件操作均有示例。资源内含1482个文件以C语言源码629个c与头文件382个h为主辅以Keil工程配置uvprojx、STM32CubeMX配置ioc、编译输出文件o、axf、hex及DSP库lib等压缩包约59.43MB工程目录结构完整便于直接打开工程浏览。代码覆盖FatFS集成与参数配置、SD卡SPI/SDIO驱动、CSV文件创建与写入、lwIP协议栈接入、TCP服务器收发处理等关键环节可直接导入工程对照学习也可作为毕业设计或产品原型的基础框架尤其适合学习文件系统与嵌入式网络通信结合开发。目前已有8121人学习/下载具备较高参考价值。 最近在做STM32数据采集设备核心需求是把传感器的温湿度、气压数据存进SD卡再通过FATFS文件系统管理文件最终生成CSV格式的记录插上电脑就能用Excel分析。这个组合在嵌入式项目里非常典型底层驱动读写SD卡上层用FATFS管理目录和文件数据本身按CSV一行行写。整套链路跑通之后我最大的感受是方案并不复杂但每一个环节都有值得展开的细节。这篇文章想做的事就是把“STM32 SD卡 FATFS CSV”这条完整链路拆开来讲。从方案选型、硬件接线、FATFS移植配置到实际写CSV文件、掉电保护和排错经验都会覆盖到。特别适合正在做数据采集、设备日志、参数记录这类项目的朋友参考哪怕你之前没接触过文件系统按着这个思路也能把SD卡用起来。1. 项目整体思路与方案选型1.1 为什么是FATFS而不是裸读写SD卡先把三个角色理清楚。SD卡本质上是一个块存储设备它只认扇区你告诉它“往第100个扇区写512字节”它就照做。如果直接在裸机层用LBA地址读写就要自己维护哪些扇区用了、哪些扇区空着、文件从哪里开始、文件多大。这套管理逻辑做起来非常繁琐而且SD卡本身没有目录概念存多了数据之后想找到一个特定文件纯靠人工记录偏移位置基本不可维护。FATFS的出现就是解决这个问题的。它把文件系统层做成了标准的FAT12/16/32实现你在MCU上调用f_open、f_write、f_close它内部帮你换算成N次扇区读写。对应用开发者来说操作SD卡就像操作PC上的U盘一样自然目录、文件、大小、时间戳都有了。用仓库来类比SD卡是仓库FATFS是仓库管理员CSV是货单格式。你只管把货物和货单交给管理员不用关心货架编号和摆放规则。1.2 CSV格式为什么值得选我见过不少项目用自定义二进制格式存数据结构体直接memcpy到文件里。这样做的好处是写入高效、体积小坏处也明显文件离开设备后没法直接看每次都要写上位机解析脚本一个字节错位整条数据就废了。CSV则把这些问题都规避了。CSV的核心就是一行一条记录字段用逗号隔开第一行可以是表头。比如“2026-05-12 08:30:00,25.3,60.2,1013.5”这样一条数据Excel可以直接打开成表格Python里用pandas.read_csv一行代码就能加载。在MCU上写CSV也不复杂本质上就是拼字符串再写入文件用snprintf格式化一下就行。缺点是体积比二进制大但对SD卡来说根本不是问题一张32G的卡能存下海量文本数据。所以对于绝大多数需要事后分析数据的应用CSV就是优先级最高的选择。2. 硬件连接与底层驱动准备2.1 SDIO还是SPI先解决通信方式。STM32读写SD卡主要有两条路SDIO接口和SPI接口。我做这个项目用的SPI原因很简单引脚占用少初始化代码少而且低速数据记录场景根本不需要SDIO的高带宽。对比项SPI模式SDIO模式引脚占用CS、SCK、MISO、MOSI约4~6个引脚D0-D3、CLK、CMD更多引脚最高速率一般几MHz到几十MHz可达几十MHz以上代码复杂度低SPI外设驱动简单高需处理CMD和响应状态机适用场景数据记录、配置文件、日志图片、音频、视频等高速流式写入如果你的项目是写图片、音频流或者需要高频批量存储选SDIO更合适。像我这种每秒写一两行CSV的采集设备SPI完全够用而且SPI模式下出问题的概率更小调试起来也更直观。2.2 底层驱动必须实现的几个函数不管你用CubeMX生成还是自己写底层都要准备好几个最基础的操作。FATFS在diskio.c里调用的就是一组磁盘接口函数但咱们先不急着看接口先把SPI底层弄对SPI初始化时钟、引脚复用、波特率分频、CPOL/CPHA。SD卡的SPI模式对时钟极性和相位有明确规定实际工程中常用Mode 0或Mode 3初始化后用SD卡复位命令实测确认即可。片选控制写SD卡命令前拉低CS结束后拉高注意空闲时CS必须为高否则卡会进入未知状态。字节收发SPI是全双工读的时候也要发0xFF。尤其是读SD卡数据块时要持续发时钟才能把数据移出来。等待卡忙写数据块后SD卡可能处于忙状态需要轮询读D0线的电平卡忙时输出低电平。实测下来有个细节容易踩坑SPI的波特率在SD卡上电初始化阶段不能太高。很多卡在IDLE识别流程中对SPI时钟上限是400kHz初始化完成后再把时钟切到几MHz甚至更高。如果一上来就用高速时钟发CMD0很可能拿不到0x01响应。我习惯的顺序是分频系数先拉大等ACMD41返回就绪后再调整SPI分频到能稳定读写的频率。2.3 硬件连接注意事项SD卡在硬件上其实有不少讲究。首先供电SD卡必须工作在2.7V到3.6V直接用3.3V即可但要注意卡在写入时的峰值电流可能到几十甚至上百mA所以卡座电源引脚旁边一定要放一个10µF左右的陶瓷电容并且尽量靠近卡座。不要从STM32的某个引脚直接取电要接在稳压输出的主电上。信号线方面SPI的CS、SCK、MOSI建议加上拉电阻到3.3VMISO可以不加上拉但多数电路也会加个10k。上拉电阻的作用是防止引脚悬空导致误触发尤其是CS悬空的话卡可能随机进入未知状态。另外如果STM32是5V容忍引脚也不能直接拿5V逻辑去怼SD卡因为SD卡本身不是5V容忍的一不留神就会把卡的保护电路烧掉电平不匹配时必须做电平转换。还有一个经常被忽略的是卡检测和热插拔。很多卡座有CD引脚接入到STM32的GPIO上可以在插入和拔出SD卡时做处理。我实际项目中要求不能热插拔所以只用了写保护开关检测。如果非要支持热插拔务必确保在插入和拔出瞬间挂载和读写状态都处理干净否则FAT表很容易损坏。3. FATFS文件系统移植与配置3.1 源码拿回来怎么放从官网下载FATFS源码一般会有ff.c、ff.h、ffconf.h、diskio.c、diskio.h这几个核心文件。我用的是常见的R0.14b版本老版本R0.12也还在不少项目里服役功能上没有本质区别。把ff.c加入编译ffconf.h放工程目录下或include路径里diskio.c里填自己的硬件驱动。有一点要说清楚FATFS本身不依赖任何厂商SDK它只通过diskio.c的一组函数和底层打交道。所以不管你是标准库还是HAL库甚至RTOS环境下移植路径都是一样填好disk_initialize、disk_read、disk_write、disk_status、disk_ioctl、get_fattime这6个函数文件系统就能干活了。编译时记得开C99或C11支持FATFS源码其实比较老用Keil的AC5和AC6都能正常编译。3.2 ffconf.h里真正影响项目的配置项ffconf.h是FATFS的配置总开关可配置项很多但对本项目影响最大的是这几个_USE_MKFS需要格式化成FATFS时打开。比如拿到的卡未格式化、或者被格式化成exFAT就需要在代码里调f_mkfs此时必须设为1。_USE_STRFUNC决定f_printf、f_puts等格式化写函数是否可用。如果打算用f_printf写CSV要设为1或2。_MAX_SS设为512还是4096取决于SD卡扇区大小。目前大多数SDHC/SDXC卡的物理扇区是512字节设512就好。_FS_TINY小内存优化项。STM32F103这种内部SRAM只有20K的设备建议开它会减少缓冲区占用代价是写性能略降。_LFN_UNICODE一般项目不建议开。长文件名支持会占用不少RAM中文文件名在默认编码下很容易乱码。实际项目里我的经验是文件名就用英文缩写加数字索引完全避开中文名问题。这些配置项都不是拍脑袋设的每一项都对应着RAM占用和功能取舍。我的做法是先把需求想清楚要不要格式化功能、会不会用到中文文件名、RAM有多少剩余、对写性能要求多高然后再决定开关。3.3 diskio.c接口函数实现要点disk_read和disk_write是核心参数里有sector地址也就是逻辑块地址LBA。FATFS以扇区为单位调用它们所以你必须保证这两个函数能正确读写512字节的一个扇区。SD卡在SDHC/SDXC模式下CMD17读单块、CMD24写单块LBA直接作为地址参数使用。disk_ioctl主要用来处理GET_SECTOR_COUNT、GET_SECTOR_SIZE、GET_BLOCK_SIZE这几个请求。特别是f_mkfs和f_mount检查容量时会用到GET_SECTOR_COUNT不实现的话格式化流程走不通。CTRL_SYNC请求一般就是把数据真正刷到卡里很多简单实现里直接返回RES_OK但在掉电可靠性要求高的项目里建议在这里做一次等待卡不忙再返回。get_fattime实现上不需要太精确因为MCU一般没有RTC。我是用一个全局变量从系统启动时间推算出来的虽然不准确但至少能区分文件创建先后。如果你有RTC模块直接从这里读取转换后填入即可。4. 数据采集与CSV文件生成实操4.1 挂载文件系统并创建CSV文件前面铺垫完了现在开始写具体流程。先挂载FATFS fs; FIL file; FRESULT res; res f_mount(fs, , 1); // 挂载到根目录 if (res FR_NO_FILESYSTEM) { // 没有文件系统先格式化 res f_mkfs(, 0, 0); if (res FR_OK) { res f_mount(NULL, , 1); // 重新挂载 } }注意f_mount的第三个参数是立即挂载标志。设1表示挂载时检查文件系统并初始化如果卡里没有FAT文件系统会返回FR_NO_FILESYSTEM。我第一次上电遇到一张之前被其他设备格式化过的卡返回的就是这个错误。处理方式上面写了格式化成FATFS后重新挂载。格式化会清空整张卡所以如果卡里有重要数据千万别用这个流程自动格式化。创建CSV文件并写表头res f_open(file, LOG0001.CSV, FA_CREATE_ALWAYS | FA_WRITE); if (res FR_OK) { f_printf(file, timestamp,temp,humidity,pressure\r\n); f_close(file); }FA_CREATE_ALWAYS每次都会覆盖已有文件适合开机新建如果要追加数据用FA_OPEN_ALWAYS加FA_WRITE文件不存在则创建。这里CSV行分隔符建议用\r\n因为Windows的Excel对\r\n兼容性最好只用\n的话有些版本会显示成一行。4.2 用snprintf拼CSV行写数据时最要紧的是避免频繁开关文件。我实际项目里每次采集后打开文件、写一行、立刻关闭在数据量小时完全没问题但采集频率一高比如每秒几次频繁开关文件不仅浪费CPU还容易在文件系统层产生额外开销。更合理做法是开机时打开文件采集周期内只写不关定期f_sync一次最后再f_close。拼行的方法用snprintf最方便char line[128]; snprintf(line, sizeof(line), %s,%d.%02d,%d.%02d,%d.%02d\r\n, timestamp, temp_int, temp_dec, humi_int, humi_dec, pres_int, pres_dec); res f_write(file, line, strlen(line), bw); if (res ! FR_OK || bw ! strlen(line)) { // 写失败处理 }这里有个很多新手会踩的坑在MCU上直接用%f格式化浮点数往往拿不到正确结果。原因在于部分编译环境的printf系列默认不链接浮点打印支持比如Keil的AC5下你snprintf里写%f会输出空或者乱码。解决办法有两个一是打开编译器的浮点打印库选项比如Keil里勾选Use MicroLIB或者链接armclang时打开浮点格式支持二是像我这样把浮点拆成整数和小数部分用整型格式化输出。第二个方案更保险也不依赖编译器选项实测在各种工程里都稳。4.3 数据落盘f_sync和f_close的时机文件写入后数据不会立刻到卡上而是先存在FATFS的缓冲区里。f_write只保证数据进了文件系统缓冲区并不保证已经写进SD卡。这时候如果断电或者直接拔卡最后几行数据很可能会丢极端情况下FAT表也会损坏导致整个文件打不开。想降低丢失风险关键就是f_sync。f_sync会把文件相关的缓冲数据、FAT表信息刷到卡里但不会关闭文件调用频率可以自己掌握。我的经验是数据量小的时候每写10行或每1秒f_sync一次既能保证大部分数据不丢又不会因为频繁刷盘把SD卡写入寿命和速度拖垮。如果某个数据特别重要比如报警事件那就在写那行之后立刻f_sync一次。最后关机或换卡前一定要f_close。f_close内部会做一次同步之后才能安全卸载。有些项目还会在检测到掉电时用外部中断保存现场但MCU级别的掉电往往只有几毫秒准备时间与其依赖掉电流程不如靠定期f_sync把损失控制住。5. 常见问题与排查经验5.1 错误码速查与处理思路FATFS的函数都返回FRESULT类型错误码实际调试时把这几个弄懂大部分问题都能定位错误码含义常见场景FR_OK成功无FR_DISK_ERR底层读写失败SPI波形不对、卡忙、电压不稳FR_NOT_READY卡未就绪卡没插好、CS上拉缺失、初始化未完成FR_NO_FILESYSTEM没有FAT文件系统卡被格式化成exFAT或未格式化FR_EXIST文件已存在用FA_CREATE_ALWAYS以外的模式且重复创建FR_DENIED被拒绝只读属性、写保护开关、FAT表满了FR_NOT_ENOUGH_CORE内存不足LFN缓冲分配失败检查配置和堆栈遇到FR_DISK_ERR不要急着改应用层先确认底层读一个扇区是否稳定。我最快的定位方法是把disk_read单独抽出来死循环读固定扇区用示波器看MISO线上有没有稳定的数据波形。如果波形时有时无基本就是SPI初始化或者卡电源问题。5.2 掉电丢数据与文件损坏的深层原因文件损坏的本质是FAT表和数据区写入顺序不一致。比如数据已经写到数据区但FAT表记录的簇信息没更新重启后文件系统看到的就是错误链接。FATFS内部对FAT表有镜像和校验机制能解决一部分问题但扛不住频繁的未同步断电。这里有个进阶技巧在写完数据并f_sync之后可以再用f_lseek把文件指针定位到文件末尾。原因在于f_write之后文件指针会自动指向新位置但如果中间有过f_lseek或异常分支指针可能不在正确位置。我实际遇到过因为指针偏移导致数据覆盖了前面内容的情况后来养成了每次打开后直接f_lseek到末尾再写的习惯。对CSV日志来说用FA_OPEN_ALWAYS加f_lseek(file, 0, SEEK_END)比FA_CREATE_ALWAYS更稳。5.3 文件名、容量与兼容性限制FATFS默认按8.3短文件名工作即主名8个字符以内扩展名3个字符。文件名超过或包含空格、中文就需要LFN支持但LFN会额外占用RAM。我项目里日志文件用LOG0001.CSV这种命名循环递增等编号到一定数量再手动清理完全避开LFN稳定性最好。另外一块是新卡兼容性。现在市场上很多64G以上的卡出厂格式是exFATFATFS默认配置并不认识exFAT。两个选择要么在代码里打开_USE_EXFAT配置要么用电脑把卡重新格式化成FAT32。我建议普通场景直接用电脑格式化FAT32FATFS对FAT32的支持最成熟也少很多麻烦。容量超过32G的卡Windows自带格式化工具不让选FAT32可以用第三方工具处理但要注意簇大小FATFS能正常识别但簇太大浪费空间。5.4 写入速度瓶颈与缓冲区策略CSV写文件卡顿先别怀疑FATFS。文件系统层只会把数据拆成扇区写入真正的瓶颈通常在SPI时钟频率和SD卡自身的擦写速度。比如SPI时钟跑18MHz理论上每秒能传2MB以上但SD卡小粒度随机写可能只有几百KB和PC上的NVMe完全是两个概念。想提升单次写入效率最直接的办法就是让f_write尽量按照512字节对齐的整块来写。FATFS在底层已经帮你做了缓冲对齐应用层如果一条数据只有10字节它会把10字节先收进文件系统缓冲区攒满一个扇区再写。这时候如果你反复f_open/f_close每次关闭都要强制写最后一个不完整的扇区性能自然下降。所以高频写入场景保持文件长时间打开、积累数据后f_sync比频繁开关要高效得多。我实测下来每秒写一行30字节的CSVSPI跑8MHz配合每10行f_sync一次一整块32G卡连续写一整天文件打开、追加、关闭都很稳定。真正拖速度的反而是偶尔调用f_mkfs所以格式化最好只在首次上电做一次运行时不要反复格式化。这个方案目前在我手上的采集设备里跑了好几个月CSV文件按天新建攒了几百MB后直接用Excel或者pandas读取数据完整时间戳、数值都能对得上。回顾整个调试过程最大的体会就是FATFS这套东西虽然代码量不大但每个环节对细节的要求都不低尤其底层SPI时序和卡电源稳定性稍微含糊一点后面文件系统层就会以各种诡异错误码的形式跟你“算总账”。建议拿一张闲置的TF卡配合串口打印从最底层的CMD命令开始验证确认卡能稳定读写扇区后再往FATFS上层走排查问题会轻松很多。另外CSV虽然简单但别忘了它也能做很多事情比如带表头、按字段分组、用逗号或分号做分隔符关键是要在一开始就把约定确定下来免得后面数据格式不一致还要写转换脚本。祝各位数据记录顺利卡不丢数据。本文还有配套的精品资源点击获取