资讯动态

STM32数据记录实战:SD卡+FATFS+CSV方案移植与调试

发布时间:2026/9/7 10:00:50 来源:尧图企业网站定制
简介一套基于STM32F429的嵌入式综合工程面向需要将SD卡存储、FatFS文件系统与以太网TCP通信结合起来的开发者解决网络下发的数据如何落盘为CSV文件的实际问题。包体内共1482个文件以C源码和头文件为主体包含Keil MDK工程配置.uvprojx、编译链接产物.axf/.map/.hex及lwIP协议栈相关文件压缩包约59.43MB适合直接导入工程查看或二次开发。已有8121人学习下载。从源码结构可以提取出SD卡SPI/SDIO驱动、FatFS移植配置、CSV格式写入封装、以太网MAC与PHY初始化以及基于lwIP的TCP服务器回调处理等完整实现文件类型中大量.c/.h按模块组织方便对照学习FATFS挂载、文件创建与写入、TCP数据接收后转存CSV这一链路。资源还附带编译中间文件与调试配置有助于理解工程构建流程适合正在做STM32数据采集、远程监测或网络存储项目的开发者参考。 STM32的板子上要记录采集数据我第一个想到的方案就是SD卡FATFSCSV这套组合。不管你是做环境监测、电池管理、仪器仪表还是单纯想给毕设加个数据记录功能这套方案都能用得上。核心思路其实很简单让STM32把传感器或业务数据定期写入SD卡SD卡采用FAT文件系统也就是FatFs管理文件最后把数据组织成CSV格式插到电脑上直接能打开。这篇文章我会把我实际移植FatFs、调试SD卡底层、最终稳定生成CSV文件的完整过程拆开讲包括哪些地方容易翻车、为什么要这么设计适合正在做同类项目的朋友参考。1. 方案选型为什么是“SD卡 FATFS CSV”这个组合1.1 存储体量决定方案上限很多刚开始做数据记录的人会优先考虑内部Flash或者外部SPI Flash它们的好处是接口简单、代码量小但实际用下来会发现问题内部Flash容量通常只有几十KB到几百KB还要同时放程序和掉电保存参数能分给日志数据的空间非常有限外部SPI Flash虽然有2MB、4MB甚至16MB可选但W25Q这类芯片的擦写寿命和文件管理都限制着它的使用方式数据稍微一多就得自己搞环形覆盖逻辑。SD卡最大的优势是容量和通用性。一张普通Class10 TF卡就有32GB哪怕每秒记录几十个浮点数连续跑几个月也不怕空间不够。更关键的是SD卡的物理接口和SPI Flash有天然的继承关系——很多MCU都能直接用硬件SPI甚至GPIO模拟SPI来驱动SD卡底层驱动写起来比很多人想象中简单。我在选型时还考虑过SDIO高速模式但对大多数数据采集场景来说SPI模式已经完全够用了下面会细说。1.2 FATFS为什么是嵌入式文件系统的事实标准SD卡出厂时也可能自带私有格式但如果你只是想写数据、断电重启后还能继续写那给SD卡上文件系统几乎是必须的。FatFs全称FatFs Generic FAT File System Module是一个专为小型嵌入式系统设计的FAT文件系统组件由ChaN开发后开放使用特点是源码清晰、移植简单、占用资源少。选FatFs而不是自己实现FAT格式核心原因有三点它把目录管理、FAT表维护、文件名解析、簇分配这些繁琐逻辑全部封装好你只需要提供底层的disk_initialize、disk_read、disk_write、disk_ioctl等函数。文件格式兼容性好PC端、MAC、Linux都能直接识别SD卡插到读卡器上就是普通U盘。源码是纯C写成不存在平台绑定STM32标准库、HAL库、LL库都可以用甚至你换成GD32、GD32F103这种国产芯片也能基本无缝移植。1.3 CSV格式让数据从“设备端”到“PC端”零障碍之前有朋友问过我“既然都要写文件了为什么不直接写二进制或者自定义格式”这个问题其实要看数据的最终消费端是谁。如果是给程序解析且追求极致效率和容量二进制确实好但绝大多数场景下你需要把数据交给Excel、Python、MATLAB或者NotePad去打开查看这时候CSV就是最佳解。CSVComma-Separated Values逗号分隔值本质上是纯文本表格每行一条记录字段用逗号分隔。设备端生成CSV只需要做一件事把数据按“时间戳, 数据1, 数据2, 数据3”的格式拼成字符串写入文件。不需要任何编码转换、不需要二进制序列化官方也不存在“版本不兼容”的问题。Excel双击就能打开Python里pandas三行代码就能分析import pandas as pd df pd.read_csv(DATA_20250101.CSV) print(df.head())这也是我在多条技术路线上最终坚持CSV的原因降低下游使用的门槛比省那几KB存储空间重要得多。1.4 SPI还是SDIO接口选择的实际权衡这个项目里我选了SPI模式驱动SD卡。很多人纠结STM32的SDIO接口更快为什么不用我的经验是SDIO的驱动复杂度和调试难度比SPI高一个量级。SDIO有其特定的命令响应时序、CRC校验、FIFO和DMA配合问题稍微没配好就卡在初始化或者读取超时上。而SPI模式只需要占用4根线CS、SCK、MOSI、MISO大部分STM32型号的硬件SPI都能直接跑逻辑极其直观调试时拿逻辑分析仪或者示波器一看就明白。当然SPI模式的读写速率不如SDIO但实测下来在8MHz SPI时钟下顺序写入一块4KB数据块大概需要几毫秒对绝大多数传感器采集场景而言已经不是瓶颈。更重要的是SPI总线可以和其他外设共享比如常见的“ESP32屏幕与SD卡共享SPI”就是利用片选信号隔离设备STM32也可以这样做。如果你用SDIO这个共享方案基本没法实现。2. 移植FATFS前先把底层diskio写稳2.1 SD卡SPI模式初始化唤醒流程不能省直接调用f_mount之前必须先把SD卡的底层驱动跑通。SD卡上电后默认处于SD模式也就是SDIO用的那种要切换到SPI模式需要执行一个标准的唤醒序列。我在第一次移植时漏掉了这部分结果卡在CMD0没响应排查了半天才发现问题。完整流程是这样的上电或复位后先延时至少1ms确保卡内电源稳定。把片选CS拉高释放卡然后SPI主机连续发送至少74个时钟周期的0xFF。这一步是让SD卡完成内部初始化并识别SPI模式。拉低CS发送CMD00x40 0x00 0x00 0x00 0x00 0x95等待响应0x01进入IDLE状态。循环发送CMD80x48 0x00 0x00 0x01 0xAA 正确CRC确认卡是否支持SDHC/SDXC规范同时校验接口电压范围。发送ACMD41先发CMD55再发CMD41等待响应0x00表示卡完成上电初始化。注意这一步通常需要轮询很多次。发送CMD58读取OCR寄存器判断卡是SDHC还是SDSC后续读取/写入扇区时地址长度逻辑会有所区别。设置块长度CMD16一般设为512字节。这串流程里最容易被忽视的是第2步的74个时钟周期。SD卡的SPI初始化时序非常严格时钟数少了卡就完全不理你。我习惯在初始化阶段把SPI时钟压到400kHz以下等卡完全跑起来后再把时钟提高到8MHz这也是官方资料里明确建议的做法。2.2 disk_write与disk_read命令、数据块、忙等待FatFs调用底层读写接口时最核心的两个函数就是disk_read和disk_write。SPI模式下读取一个扇区的流程是发送CMD170x51地址是扇区号左移9位后的字节地址如果卡是SDSC地址需按字节寻址块长度512。等待响应0x00表示卡已准备好。继续读取到0xFE数据起始令牌然后连续读取512字节数据再读2字节CRC。读完这一扇区后通常还要多读几个0xFF保证卡处理完。写入一个扇区的流程则是发送CMD240x58地址同样是扇区字节地址。等待响应0x00。发送0xFE数据起始令牌随后发送512字节数据再发2字节CRC。然后持续读取状态字节0xE5表示写入成功之后再等待卡不再输出0x00即忙状态结束。这段逻辑如果自己从头写很容易漏掉CRC字节或者忙等待。虽然SPI模式下卡不校验CRC通常可填0xFFFF但你必须在数据末尾补上否则卡会一直不回应。忙等待也很关键写完后直接发下一个命令大概率会超时因为卡内部的NAND擦写是需要时间的实测Class 10卡通常忙几毫秒到几十毫秒不等。2.3 一个可靠的SPI底层封装长什么样我习惯把SPI收发封装成一个简单的函数所有SD卡命令都走同一套收发逻辑uint8_t SPI_ReadWriteByte(uint8_t byte) { uint8_t rx; // 以HAL库为例 HAL_SPI_TransmitReceive(hspi1, byte, rx, 1, 100); return rx; } static uint8_t sd_send_cmd(uint8_t cmd, uint32_t arg, uint8_t crc) { uint8_t frame[6]; frame[0] 0x40 | cmd; frame[1] (arg 24) 0xFF; frame[2] (arg 16) 0xFF; frame[3] (arg 8) 0xFF; frame[4] arg 0xFF; frame[5] crc; uint8_t resp; uint8_t i; // 发送命令帧 for (i 0; i 6; i) { SPI_ReadWriteByte(frame[i]); } // 持续读取直到得到非0xFF的字节 resp 0xFF; for (i 0; i 10; i) { resp SPI_ReadWriteByte(0xFF); if ((resp 0x80) 0) { break; } HAL_Delay(1); } return resp; }这段代码的关键点是必须丢弃命令前导的高电平垃圾字节。SD卡在执行命令时前几个字节通常都是0xFF你需要循环读取直到响应字节的最高位为0才算真正收到响应。如果只读一个字节就当作响应极大概率读到0xFF导致误判超时。2.4 get_fattime少踩一个时间戳的大坑FatFs里的f_getfree、文件修改时间等字段需要用到当前时间而这个时间来自用户实现的get_fattime()函数。很多人移植时故意留空不实现导致文件日期变成1980年或者随机值这在写CSV文件名时尤其伤——因为很多人喜欢用日期作为文件名。如果你的板子上有RTC直接从RTC读出时间换算成FAT时间格式即可没有RTC也可以用编译日期做保底。标准FAT时间格式是32位年占7位从1980起、月占4位、日占5位、时占5位、分占6位、秒占5位除以2。实测最省事的写法是DWORD get_fattime(void) { return ((DWORD)(2025 - 1980) 25) | ((DWORD)1 21) | ((DWORD)1 16) | ((DWORD)0 11) | ((DWORD)0 5) | ((DWORD)0); }没有RTC时我会返回编译日期虽然文件时间不准确但至少不会出现非法时间导致文件系统操作异常。3. 生成CSV文件从挂载到落盘的关键细节3.1 f_mount的调用方式和常见误解FatFs里f_mount不是“格式化SD卡”而是注册一个工作区。很多人第一次用以为调用f_mount后卡就会被格式化结果数据全没了这就是理解偏差。f_mount只是把FATFS结构体和一个卷标绑定真正的格式化需要调用f_mkfs接口。示例代码如下FATFS fs; FIL file; FRESULT res; res f_mount(fs, , 1); // 注册SD卡为默认卷 if (res ! FR_OK) { // 处理错误 }注意这个函数的第二个参数是路径前缀这里传空字符串表示默认卷。第三个参数如果为1表示立即挂载如果为0表示延迟挂载后续第一次访问时自动挂载。我遇到过一种情况f_mount返回FR_OK但紧接着f_open返回FR_NOT_READY就是因为卡初始化不完整或者SD卡与MCU间的SPI电平时序不稳定。需要确认disk_initialize返回了0成功且SPI时钟在初始化阶段足够慢。另外FATFS结构体变量必须保持全局存活。如果你在某个函数里定义了局部FATFS fs函数返回后内存失效再次访问SD卡会出现各种奇怪问题。正确做法是定义成全局变量。3.2 创建CSV文件注意打开模式CSV文件本质就是txt文本打开模式建议用FA_OPEN_ALWAYS | FA_WRITE这样文件不存在时会新建存在时会从尾部继续追加。如果直接使用FA_CREATE_ALWAYS每次上电都会把旧文件覆盖掉数据就丢了。// 生成文件名例如DATA_20250101_103000.CSV char filename[32]; sprintf(filename, DATA_%04d%02d%02d_%02d%02d%02d.CSV, year, month, day, hour, minute, second); res f_open(file, filename, FA_OPEN_ALWAYS | FA_WRITE); if (res ! FR_OK) { return; } // 写入CSV表头仅当文件刚创建时写入 if (file.fsize 0) { f_printf(file, timestamp,temp,humidity,voltage\r\n); }这里有个细节file.fsize是FATFS的FIL结构体成员表示当前文件大小。文件刚创建时大小是0此时写入表头如果文件已经存在且之前已写过表头则跳过。用\r\n作为行结束符这是CSV标准约定Excel打开后换行才正常。如果只写\n在Windows上用记事本打开可能会显示成一行。3.3 写入频率与f_sync的平衡点SD卡写入速度不是主要瓶颈真正考验系统的是“持续写入时的掉电安全性”。很多人写完数据后不调用f_sync直接断电结果最后一条甚至最后一批数据丢失。这是因为FatFs内部有缓冲区f_write写入的数据不一定立刻落盘。f_sync的作用是把文件缓冲区的数据刷到物理扇区确保文件目录项和FAT表也同步更新。但也不能每个采样周期都调用f_sync那样会明显拉低写入效率因为每次sync至少涉及一次物理扇区写入频繁触发反而让卡一直处于忙状态。我的经验是对数据记录类应用建议每写入N条记录调用一次f_sync。N的具体值取决于你可以接受丢失多少条数据。比如采集频率1Hz每分钟同步一次断电最多丢1分钟数据对大部分应用完全可以接受。实际代码里可以这样控制uint16_t sync_count 0; while (1) { // 采集数据 float temp read_temp(); float humi read_humi(); f_printf(file, %lu,%.2f,%.2f,%.2f\r\n, timestamp, temp, humi, voltage); if (sync_count 60) { f_sync(file); sync_count 0; } HAL_Delay(1000); }3.4 文件名时间戳与长文件名SD卡里会存多个CSV文件如果都叫DATA.CSV过几天就乱了。我习惯用时间戳做后缀比如DATA_20250101_103000.CSV表示2025年1月1日10点30分创建的文件。好处是即使卡里塞了很多文件按名称排序也一目了然。但这里必须注意一个坑FatFs默认配置文件ffconf.h里有一个_USE_LFN选项默认值可能是0即只支持8.3短文件名格式。如果不开LFNLong File Name长文件名会被截断成类似DATA_202~1.CSV的样子虽然功能不受影响但很不直观尤其在Windows下查看时感觉特别乱。打开LFN的方式是修改ffconf.h#define _USE_LFN 2 #define _LFN_UNICODE 0其中_USE_LFN设为1或2都可以区别在于缓冲区分配方式。设为2表示由FatFs内部动态分配内存占用更小但需要额外的malloc支持如果你不想引入堆管理就设为1并在挂载后提供足够的静态缓冲区。注意LFN打开后文件操作需要额外的工作区内存对于STM32F103这类芯片RAM需求可以接受但如果你的MCU RAM极小且文件名很长要评估清楚。4. 常见问题排查从“卡死在延时”到“no stm32 target found”4.1 “no stm32 target found”是调试连接问题不是SD卡问题这个报错信息在STM32开发中出现的频率极高尤其是你刚接入SD卡模块后。表面意思是“找不到STM32目标设备”实际往往和SD卡无关而是调试器连接问题。常见原因ST-Link的SWDIO、SWCLK接线松动或被杜邦线的信号干扰。目标板供电不稳特别是SD卡模块从STM32的3.3V取电时电流需求骤增导致MCU电压跌落调试器连接失败。STM32芯片内部Flash的读保护RDP处于开启状态此时SWD接口被禁止连接。我遇到过最典型的场景SD卡模块直接插在面包板上用杜邦线连接到STM32的SPI引脚电机或者SD卡写入瞬间电流波动导致复位然后ST-Link就掉线了。排查时先断开SD卡模块确认STM32能正常下载程序再逐步接回SD卡。如果确实开了读保护用STM32 ST-LINK Utility执行全片擦除Mass Erase可以解除。4.2 SD卡初始化到CMD0就返回超时这个现象几乎每个移植FatFs的人都遇到过。我总结的原因按出现频率排序初始化阶段SPI时钟太快。上电初期必须控制在400kHz以下很多教程里没有强调这一点卡直接不理人。片选线没有正确拉低。SD卡在SPI模式下每个命令的发送都要求CS为低电平命令之间的间隔则要求CS为高。CS引脚如果被其他外设占用或者悬空卡永远不会响应。供电不足。SD卡数据写入时瞬时电流较大如果开发板的3.3V输出能力弱可能导致卡反复复位。使用了不兼容的劣质卡。一些杂牌卡对SPI模式支持较差换一张品牌Class10卡往往立刻解决。针对第2点我补充一个细节在发送CMD0之前CS要先拉高发送至少74个时钟周期的0xFF后再将CS拉低再发CMD0。顺序反了也会失败。4.3 f_open正常f_write之后系统卡死写数据时系统卡死大概率不是FatFs的问题而是SD卡写入流程中的忙等待没有处理好。很多新手写disk_write时发送完数据后立刻返回没有等待卡完成内部编程下一次写入或者f_sync时就会因为卡还在忙而导致超时甚至进入死循环。如果在磁盘写入的超时循环里加了错误的判断条件比如“只要读到0xFF就退出等待”可能卡永远不返回0xFF程序就卡在那里。排查方法是在SPI底层收发函数中加入超时计数器比如连续读取65535次仍得不到有效状态则返回超时错误uint16_t timeout 0xFFFF; while ((SPI_ReadWriteByte(0xFF) ! 0xFF) (--timeout)); if (timeout 0) { return RES_ERROR; }这样至少不会让MCU死锁同时还能把错误上报给上层方便定位是硬件问题还是时序问题。4.4 CSV文件打开后乱码、汉字显示异常数据是英文和数字时基本不会乱码但如果你在CSV表头里写中文比如“温度,湿度,电压”用Excel打开后可能出现乱码。原因不是CSV格式错了而是文件编码问题。STM32默认是UTF-8编码但Windows Excel打开CSV时默认按ANSIGBK解析两者不一致导致乱码。我通常的解决办法有两个表头直接用英文简单无歧义推荐。如果必须用中文就在文件开头写入UTF-8 BOM0xEF 0xBB 0xBF这样Excel也能识别uint8_t utf8_bom[3] {0xEF, 0xBB, 0xBF}; f_write(file, utf8_bom, 3, bw);但要注意只有新文件才需要写BOM已存在的文件追加时不要再写入不然文件头会出现干扰字符。4.5 为什么我建议每个CSV文件不要超过数百MB连续记录好几天后单个CSV文件会变得很大。CSV是纯文本格式几百万行数据在Excel里是打不开的Excel最多支持约104万行而且文本解析速度会变慢。更实际的风险是FAT32单个文件大小上限是4GB如果程序一直写同一个文件不轮换长期运行迟早顶到上限后续写入会失败。我的做法是按日期或按小时轮换文件每次开机创建一个新文件或者在检测到当前文件大小超过设定阈值时关闭旧文件并创建新文件。这个逻辑写起来很简单但能有效避免大数据量下的各种边界问题。5. 实测数据与性能体验5.1 用8MHz SPI写CSV实际能跑多快我在STM32F103C8T6上用硬件SPI1跑8MHz时钟Class 10闪迪32GB TF卡实测顺序追加写入每512字节一个扇区的吞吐率大约在30-50KB/s左右。这个速度包含了FatFs的目录管理开销和SPI单字节收发的时间。对多数传感器场景1秒写一条几十字节的记录实际占用带宽连1%都不到完全不是瓶颈。要注意的是FatFs每一次f_write如果写入长度不是扇区对齐的内部会先做读-改-写操作性能会下降。所以我在写CSV时虽然一行只有几十字节但会尽量攒够一定长度再写入实测效果显著。5.2 掉电测试的血泪经验我在做掉电可靠性测试时发现不调f_sync直接断电最后写入的数据大概率丢失。但调用了f_sync也不一定保证绝对不丢因为SD卡内部可能还有缓存。最稳妥的做法是在关键数据的写入逻辑里每N条记录f_sync一次同时确保断电时文件系统没有正在进行的跨扇区写操作。由于CSV是追加式文本写入每次f_sync后文件大小都是明确的重新上电后从文件尾部继续追加不会破坏文件系统结构。5.3 一点优化空间如果未来数据量更大可以考虑在写数据前先写入RAM缓冲区把多条记录拼成一个大字符串再调用一次f_write整块写入减少FatFs的调用次数和扇区读写次数。也可以把不同传感器的数据拆成不同CSV文件比如TEMP.CSV、ADC.CSV逻辑会更清楚。另外一个常见的优化思路是文件名固定为DATA.CSV数据写够一定量后自动改名备份再创建新的DATA.CSV这样可以避免长文件名频繁创建销毁带来的FAT表碎片。最后再分享一个小经验SD卡模块的3.3V供电不要直接从STM32的3.3V引脚取最好加一个独立的LDO或者从5V经过稳压模块供电尤其是使用TF卡座加转接板时接触电阻和供电裕量都会影响稳定性。很多疑难杂症排查到最后其实都是供电问题。这套组合我已经在多个项目里稳定运行过核心思路就是“底层驱动求稳文件操作按部就班CSV格式做通用接口”。你拿过去之后照着步骤移植、调试遇到问题按上文排查大概率能在半天内跑通。如果过程中有任何卡点欢迎带着具体现象来交流。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价