STM32 SD卡 FATFS 写CSV文件完整教程与避坑指南
简介面向STM32F429开发者的嵌入式工程资源包实现了基于FatFS的SD卡文件系统可将采集数据写成CSV文件同时集成以太网驱动与TCP服务器用于接收网络数据并存储。其适用场景包括数据采集、工业监控、物联网网关等需要本地落盘与网络交互的场景适合有一定STM32基础、想快速上手文件系统和网络协议栈的工程师或学生。压缩包共1482个文件约59.43MB其中C语言源码与头文件占比最高含629个c文件和382个h文件便于阅读和修改同时包含Keil工程配置、链接脚本、编译生成的hex和axf文件可直接烧录验证。已有8121人学习下载。资源中FatFS已完成SDIO/SPI驱动适配提供f_open、f_write等API调用示例CSV生成部分详细展示了逗号分隔与换行记录写法TCP服务器基于lwIP实现涵盖连接建立、数据接收、缓冲区处理和文件写入流程。通过分析其工程结构和中断设计可快速搭建“网络数据→SD卡CSV日志”的完整链路也可迁移到其他STM32系列。 STM32的SD卡存储需求很多项目最终都会落到“数据记录”这个场景上。前期我用串口把传感器数据发到上位机几百个点还好等到连续采集几分钟、几千条数据的时候串口工具卡死、数据错位、还得手动复制粘贴到Excel里清洗整个人都不好了。后来把STM32、SD卡、FATFS这套组合跑通直接把数据写成CSV文件插电脑上就能用Excel或者Python处理效率完全不是一个量级。这篇东西就是记录整个落地过程包括FATFS怎么移植、CSV怎么写、踩过哪些坑给准备做数据记录仪或者小型采集器的朋友一份可以直接参考的实操笔记。1. 为什么用SD卡加FATFS存CSV而不是串口直接传1.1 从“能存”到“会存”关键在文件系统SD卡本身只是存储介质往里面写数据可以分两个层次。裸写扇区也就是自己计算柱面、磁道、扇区地址直接把数据怼到固定位置。这种方案能做但是每次想看看数据得自己写一个上位机按照你定义的格式去解析原始二进制或者特定协议数据。麻烦的地方在于换一台电脑、换一个读卡器解析工具还得跟着走数据量大了以后你很难判断哪个扇区写满了、哪个扇区数据是新的。我见过有人这么做维护成本非常高。另一种就是挂FATFS文件系统。FATFS是一个开源的FAT/exFAT文件系统模块专门为嵌入式小资源环境设计的占用的RAM和ROM都很少。它的核心价值在于让SD卡变成“U盘”插到电脑上直接被操作系统识别里面的文件直接双击打开。用户拿到你的记录仪不需要任何专用软件就能看数据光这一点就值得引入文件系统。FATFS做的事本质上就是把你在PC上熟悉的文件操作抽象成f_open、f_write、f_read、f_close这套API。底层怎么擦写扇区、怎么维护文件分配表全部帮你封装好了。文件系统这一层不只是方便它还对数据做了结构化管理。文件末尾、剩余空间、目录项都有统一规范不容易出现“写到某个地方覆盖了旧数据”这种低级问题。1.2 CSV文件是这个场景里的最优解CSV的全称是Comma-Separated Values逗号分隔值。它本质上是纯文本每一行是一条记录每个字段用逗号隔开。CSV最舒服的地方在于它没有复杂的二进制格式任何文本编辑器都能打开Excel直接双击就能识别Python的pandas.read_csv()一行代码就能读取分析。对于单片机来说CSV的生成成本极低。STM32只需要用f_printf往文件里格式化输出字符串然后加\r\n换行这就完事了。相比输出二进制数据CSV多占一点存储空间但换来的是跨平台可读性和调试便利性。在一个采集系统里调试成本往往比存储成本更值钱。我用FATFS生成CSV后实测一个包含时间戳、温度、湿度三个字段的数据文件每秒采一条跑24小时大概是7到8MB。一张8GB的Class10 TF卡按这个写入频率可以连续记录接近一年。所以对于绝大多数嵌入式数据记录场景FATFS加CSV的组合无论从开发效率还是用户使用体验上都是性价比最高的方案。2. 硬件准备与FATFS源码获取这两步决定后面顺不顺2.1 SD卡接口选SPI还是SDIOSTM32操作SD卡物理层接口通常有SPI和SDIO两种。SPI模式兼容性最好几乎所有的SD卡和MicroSD卡都支持SPI协议而且STM32的SPI外设是标配引脚分配灵活代码也简单顺手。缺点是传输速度上限不如SDIO大概是2到10Mbps的水平对于每秒几十个字节到几百个字节的传感器数据记录完全够用。如果只是数据记录我建议直接上SPI不要犹豫。SDIO模式支持4位并行传输速度可以跑到几十Mbps适合音频、视频、高速数据采集这类大吞吐量场景。代价是引脚占用多SDIO的时序要求更严格走线要留意信号完整性初始化流程比SPI复杂。还有一个实际痛点某些低端SD卡在SDIO模式下的兼容性反而不如SPI稳定容易在初始化阶段卡住。千万别只看峰值速度就选SDIO吞吐量需求上不去SPI就是最稳的选择。FatFs本身跟硬件接口是解耦的它在底层抽象了disk_initialize、disk_read、disk_write、disk_status这几个函数文件系统逻辑完全不关心你的硬件是SPI还是SDIO。所以哪怕你后面换SDIOFatFs代码主体不用动只需要改底层接口实现。2.2 电源、上拉电阻、电平匹配SD卡对供电比较敏感MicroSD卡正常工作电压是3.3V瞬间电流可以达到50到100mA甚至更高尤其写入扇区的时候。如果用STM32开发板上的3.3V稳压器要留意这个稳压器的输出能力。有些小板子的AMS1117-3.3输出电流上限是1A理论上够但实际接上电机、OLED、无线模块之后3.3V可能被拉低SD卡就表现为初始化失败或者读写中途返回错误。SPI模式下SD卡的CS、SCK、MOSI、MISO这几根信号线建议都接10kΩ左右的上拉电阻到3.3V。SD卡协议要求这些引脚在空闲状态是高电平上拉电阻能提升抗干扰能力。我自己第一次做的时候没接上拉板子在实验室环境跑得好好的拿到现场一上电就概率性初始化失败后来加上拉电阻问题消失。如果用的是现成开发板一般已经处理好了自制板子务必注意。电平匹配上STM32的IO基本都是3.3V逻辑直接跟SD卡连接没问题。如果用的是5V单片机的板子需要做电平转换。SD卡的信号引脚不推荐直接接5V长时间超过额定电压容易损坏卡。2.3 FatFs源码获取与版本选择去FatFs官网下载源码包就行当前主流的版本是R0.14系列、R0.15系列。源码包里核心文件就几个ff.c文件系统实现、ff.h头文件、ffconf.h配置头文件、diskio.c底层磁盘IO接口、diskio.h。另外还有integer.h这类辅助头文件。下载的时候建议直接拿完整源码包不要用别人工程里拷贝出来的精简版。精简版经常被改过宏定义出了问题你很难判断是官方逻辑还是被改动导致的。用官方原包自己在ffconf.h里改配置出问题也好查。官方源码包附带doc目录里面每个函数的说明非常详细比网上很多二手资料的描述准确得多建议常翻。3. FatFs配置里必须改的几个宏背后是成本和兼容性的权衡3.1 核心配置开关说明ffconf.h是FatFs的配置文件里面全是宏定义初学者容易忽略但决定文件系统能不能正常工作、功能是否符合需求的恰恰是这些宏。我把关键几个列出我实际用的配置宏定义我用的值理由FF_USE_STRFUNC2启用f_printf等字符串格式化函数这是在CSV里写格式化文本的核心功能FF_FS_EXFAT1开启exFAT支持新买的大容量SD卡出厂可能是exFAT格式不开启会挂载失败FF_USE_LFN2启用长文件名支持0表示只用短文件名实际用可能遇到“8.3格式不够用”FF_USE_MKFS1允许在设备上直接格式化SD卡避免第一次使用还要专门拿电脑格式化FF_PRINT_LLI1让f_printf支持%lld和%llu打印64位整数时间戳和计数器用得上FF_USE_STRFUNC这个宏是重点。它默认是0也就是f_printf和f_puts这些函数根本不会被编译进去。CSV文件写入字符串格式化完全依赖它设置为1时支持f_printf和f_puts设置为2时额外支持f_gets和f_putc。我直接设2功能全开。FF_USE_LFN配置为2时FatFs会把长文件名的缓冲区放在堆上动态分配这比静态分配省RAM但是要求你的编译器提供了malloc和free。如果工程没开堆就用FF_USE_LFN1把缓冲区做成静态数组。还有一种做法是配置FF_VOLUMES和FF_LFN_BUF适当调节缓冲区大小。文件名我建议尽量用英文和数字避免中文编码带来的乱码麻烦。3.2 工作区内存和栈空间要注意FatFs在挂载文件系统时需要一个FATFS对象结构体不小包含了文件分配表缓存等信息。如果用exFAT加长文件名FATFS结构体体积会明显增大有些配置下可能到几百字节甚至更多。加上FIL文件对象、DIR目录对象你自己算一下RAMSTM32F103C8T6只有20KB RAM全部塞进去够用但留给业务逻辑的内存就紧张了。如果做稍微复杂一点的采集系统建议上STM32F103RCT6或者F407系列RAM容量会更宽裕。另外FatFs内部操作会用到栈尤其是f_mount做挂载解析时需要的临时空间比较大。STM32工程里默认栈大小有1KB到2KB有时不够。我在移植过程中遇到过栈溢出导致的硬件错误后来把启动文件里的Stack_Size从0x400加到0x1000问题解决。具体来说0x400是1KB0x1000是4KB这个配置改一下不亏。4. 写CSV的完整代码实现初始化、文件名生成、格式化落卡4.1 挂载与初始化注意“首次使用要先格式化”下面这段是初始化和写文件的完整核心代码基于标准库或者HAL库都可以FATFS部分的操作是平台无关的。#include ff.h FATFS g_fs; // 文件系统对象 FIL g_file; // 文件对象 UINT g_br; // 实际写入字节数 void SD_Storage_Init(void) { FRESULT res; // 挂载SD卡文件系统第二个参数是逻辑驱动器号空字符串表示默认驱动器 res f_mount(g_fs, , 1); if (res FR_NO_FILESYSTEM) { // 没有文件系统需要格式化 // 先卸载再执行格式化最后重新挂载 f_mount(NULL, , 0); BYTE work[FF_MAX_SS * 2]; res f_mkfs(, FM_FAT32, 0, work, sizeof(work)); if (res ! FR_OK) { // 格式化失败打印错误码后停止 return; } f_mount(g_fs, , 1); } else if (res ! FR_OK) { // 挂载失败检查硬件和SPI/SDIO初始化 return; } }这段代码的意图很明确f_mount把文件系统对象跟物理驱动器绑定返回值FR_NO_FILESYSTEM表示SD卡里没有合法的文件系统。这时候就需要调用f_mkfs对SD卡做格式化。f_mkfs需要的work缓冲区至少要FF_MAX_SS * 2字节FF_MAX_SS默认是512所以这里用了BYTE work[1024]也可以直接声明大一点。注意一个细节f_mount成功挂载不等于SD卡一定能读写它只是把文件系统元数据加载到内存里。如果f_mount返回FR_NOT_READY基本可以确定问题出在底层硬件供电、SPI初始化、卡片没插好。返回FR_INVALID_DRIVE则说明逻辑驱动器号没对上需要检查FF_VOLUMES配置和f_mount的第一个参数。4.2 生成带时间戳的文件名CSV文件如果永远叫data.csv第二次运行就会覆盖掉第一次的数据。比较好用的方案是用时间戳或者日期来命名文件。前提是你的系统里有RTC并且正确设置时间。char filename[32]; RTC_DateTypeDef sdate; RTC_TimeTypeDef stime; HAL_RTC_GetDate(hrtc, sdate, RTC_FORMAT_BIN); HAL_RTC_GetTime(hrtc, stime, RTC_FORMAT_BIN); snprintf(filename, sizeof(filename), 0:/%04d%02d%02d_%02d%02d%02d.csv, sdate.Year 2000, sdate.Month, sdate.Date, stime.Hours, stime.Minutes, stime.Seconds);这样生成的文件名例如0:/20250614_153012.csv按时间排序也方便。文件名的前缀我建议加一个逻辑驱动器号0:这是FatFs里的逻辑卷编号如果你只挂载了一张SD卡只有0:不写也没有关系但写上是好习惯避免后续扩展多个存储设备时混乱。文件名一定要控制在合理长度内虽然开了FF_USE_LFN支持长文件名但FAT文件系统单个文件名最长255字节加上路径别超。还要避免使用/\:*?|这些非法字符。4.3 写入传感器数据核心是格式化字符串文件打开后写入CSV文件的核心就变成了一行一行地格式化字符串。CSV文件每一行对应一条记录行内字段用逗号分隔。void Write_CSV(TempHumidity_t *data, const char *filename) { FRESULT res; char line_buf[128]; // FA_OPEN_ALWAYS文件存在则打开不存在则创建 // FA_WRITE以写模式打开 res f_open(g_file, filename, FA_OPEN_ALWAYS | FA_WRITE); if (res ! FR_OK) { return; } // 把文件指针移动到文件末尾这样不会覆盖之前的数据 f_lseek(g_file, f_size(g_file)); snprintf(line_buf, sizeof(line_buf), %lu,%d.%02d,%d.%02d\r\n, (unsigned long)data-timestamp, >if (write_count % 20 0) { f_sync(g_file); }f_sync这个函数就是强制把所有缓存信息写回SD卡包括文件分配表。它比f_close轻量不需要重新打开文件所以适合周期性调用。代价是写入次数变多卡寿命会有所消耗但对于普通TF卡这点损耗完全在可接受范围内。我遇到的实际案例是程序跑了20分钟中途拔卡插电脑上看CSV只有前2分钟的数据。后来在任务循环里每50条记录调一次f_sync拔卡测试最多丢最后几条基本可以接受。5.3 坑三CSV里出现乱码或者中文文件名变成“____”FATFS默认的代码页是FF_CODE_PAGE如果配置为936GBK那么写入的中文字符串会被当作GBK编码存进去。问题是CSV文件里的“编码元数据”不明确Excel在不同语言环境下打开时可能用系统默认代码页解析导致乱码。处理办法有两个方向。一是CSV内容统一用ASCII字符集不要写字面中文字符表头用英文比如timestamp,temperature,humidity数据都是数字完全避开编码问题。二是如果业务必须写入中文建议文件表头用UTF-8的BOM开头即先写入0xEF 0xBB 0xBF三个字节这样现代版本Excel能自动识别UTF-8但老版本Excel依然可能乱码。文件名方面FatFs支持长文件名和Unicode但配置起来比较繁琐需要在ffconf.h里选择正确的FF_CODE_PAGE并处理UTF-8与Unicode双向转换。我图省事文件名全部用英文加日期数字再也没有乱码问题。5.4 坑四写入过程中返回FR_DISK_ERR怎么办FR_DISK_ERR是底层磁盘IO错误意思是disk_write或disk_read返回了失败而文件系统无法自己恢复。常见原因有三个。SD卡供电不稳。看是否在写入瞬间电压被拉低用示波器量一下3.3V波形如果掉到3.0V以下考虑加一个100μF或者更大容量的电容在卡座电源附近。SPI速率太快。有些卡在1MHz能读写上了18MHz就罢工。排查时先用低速模式比如HAL库的SPI_BAUDRATEPRESCALER_256确认硬件没问题再把速率逐步提高。卡本身虚焊或者损坏。换一张卡测试如果换卡后正常基本就是卡或卡座的问题。5.5 坑五SPI模式初始化卡的兼容性问题SPI模式初始化SD卡有一个特殊流程上电后至少给74个时钟周期让卡完成内部初始化。很多移植代码写得太随意上电后马上发CMD0卡片还没缓过劲来命令发出去石沉大海然后返回初始化失败。在做底层disk_initialize时可以在发送CMD0之前加一段延时和时钟脉冲for (uint16_t i 0; i 10; i) { SD_SPI_CS_HIGH(); for (int j 0; j 0x10; j) { SD_SPI_ReadWriteByte(0xFF); } } HAL_Delay(10);这段操作本质上是让SCK先输出一些空时钟帮助SD卡完成启动序列然后再开始CMD0握手。如果你用的是CubeMX自动生成的BSP驱动一般已经处理过但如果你从某些开源工程里粘贴的驱动没有这个预热过程就要小心了。6. 进阶优化批量写入、多文件管理和给数据加表头6.1 批量写入提高吞吐量如果采集频率高比如每秒采样100次每次写一行字符串频繁调用f_write会产生很多小写入操作。每次f_write都要检查文件系统状态、更新文件指针效率并不高。更好的做法是在SRAM里开一个环形缓冲区攒够一定字节数再一次性写入SD卡。uint8_t sdcard_buffer[1024]; uint16_t buffer_len 0; void Append_To_Buffer(const char *data) { uint16_t len strlen(data); if (buffer_len len sizeof(sdcard_buffer)) { memcpy(sdcard_buffer[buffer_len], data, len); buffer_len len; } if (buffer_len sizeof(sdcard_buffer) - 64) { Flush_Buffer_To_SD(); } } void Flush_Buffer_To_SD(void) { if (buffer_len 0) { f_write(g_file, sdcard_buffer, buffer_len, g_br); buffer_len 0; } }这个思路是把“采集”和“存储”解耦避免底层存储阻塞采集逻辑。需要注意的是缓冲区的刷新时机要在掉电保护前尽量主动比如主循环里每隔一段时间就检查一次一旦数据达到阈值就刷新别攒到快溢出才动。如果系统跑的是RTOS也可以把存储放到单独的任务用消息队列把数据传给存储任务。6.2 数据量大的时候如何切分文件一个CSV文件写到几百MB以后在Windows上打开会明显变慢Excel直接卡死而且单个文件过大一旦损坏损失太大。更好的做法是按时间或者按容量自动切换新文件。代码里维护一个当前文件写入字节数的计数器当达到设定上限比如10MB就把当前文件关闭生成一个新的时间戳文件名继续写。这样既能控制单文件大小也方便用户按时间段查看数据。还需要注意如果采集任务持续运行打开的文件一直不关闭会占用一个FIL对象而FatFs里每个文件对象都需要占用独立内存。如果同时只写一个文件一个FIL就够了但是如果说你要做“边写数据边读配置”的功能就要留意FF_FS_LOCK配置项开启文件锁功能才能支持多文件同时打开否则只能关掉一个再打开另一个。6.3 数据表头与CSV兼容性CSV文件的表头不是必需的但没有表头会让数据列含义不明确。建议在新建文件时写入一次表头void Create_CSV_With_Header(const char *filename) { f_open(g_file, filename, FA_CREATE_NEW | FA_WRITE); f_printf(g_file, timestamp,temperature,humidity\r\n); f_close(g_file); }FA_CREATE_NEW指定只有文件不存在时才创建如果文件已存在会返回FR_EXIST。这个标志在生成新文件、避免覆盖时特别好用比FA_OPEN_ALWAYS更安全。相比之下FA_OPEN_ALWAYS虽然简洁但在有些逻辑路径下可能会打开一个旧的、数据格式不对的文件继续往里追加结果生成一个表头和数据穿插的畸形CSV。如果数据字段里有逗号比如描述信息“Hello, world”CSV文件会解析错位。解决办法是用双引号把包含逗号的字段包起来比如Hello, world。需要注意的是字段内部如果自身包含双引号需要用两个双引号转义He saidhi这样。不过对于纯传感器数据字段都是数字和单位基本不会遇到。6.4 FatFs的disk_status函数别忽略disk_status是FatFs在每次读写前检查硬件状态的一个接口默认实现可能只是返回RES_OK。如果你在写数据过程中希望检测到SD卡被拔掉disk_status里最好加入对卡检测引脚的判断。很多SD卡座自带一个CDCard Detect引脚插卡时接地拔卡时悬空用这个引脚就可以在FatFs读写前快速判断卡是否在位。如果不做这个检测卡在写入过程中被拔出FatFs可能返回FR_DISK_ERR或者直接卡死在底层SPI读写里。小组里做数据记录仪时我们还在SD卡槽旁边加了一个LED指示状态读写时闪烁写满时常亮用户反馈体验很好。7. 最终落地时的几点体会这套SD卡加FATFS加CSV的方案我前前后后做了三轮迭代从最初的裸写扇区到后来FATFS加手动拼接CSV再到现在的格式化添加表头加定时同步加自动切分文件。最大的一点体会是不要把文件系统想象得很神秘它就是一个帮你管理数据的库你能用它把复杂的事情拆成简单API调用但前提是理解它底层的缓存机制和配置项的取舍。另一个经验是SD卡初始化失败的问题不要拿着代码反复看先拿逻辑分析仪去看SPI波形是否正常看CMD0之后有没有回应。很多时候排查半天最后发现是卡座虚焊或者SPI引脚被复用成了其他外设。写代码前把硬件通路先验证明白能省大量时间。做存储类项目提前多花一点时间设计好文件格式和分片策略后面数据分析会非常省心。CSV文件简单到了极点但正是这种简单让它在嵌入式系统里优势尽显没有解析负担不用维护二进制协议上位机拿来就能用。如果你刚开始做数据记录建议直接跳过裸扇区方案从FATFS开始这会是你最省力的一条路。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →