简介:这份资源面向嵌入式开发学习者与STM32工程师,聚焦在STM32F429平台上集成FatFS文件系统、驱动SD卡写入数据并生成.csv文件,同时结合以太网驱动搭建TCP服务器接收网络数据。内容覆盖FatFS的移植与参数配置、SDIO或SPI方式下的SD卡初始化与读写、CSV格式数据的文件创建与写入,以及基于lwIP的TCP服务器连接建立、数据接收与处理流程,适合具备一定C语言与嵌入式基础、希望打通存储与网络通信链路的开发者参考。资源包共1482个文件,以629个c源文件、382个h头文件为主,另含144个d依赖文件、144个o目标文件、142个crf交叉引用文件及少量库文件、工程配置与说明文档,压缩包约59.43MB,工程结构完整便于直接编译与二次开发。目前已有8150人学习下载,可帮助读者快速理解SD卡文件系统与TCP数据落盘的完整实现思路。1. 从一块 SD 卡说起:为什么 fatfs 是 STM32 数据落盘绕不开的一环手上有个 STM32 的采集板,传感器数据在 RAM 里滚了几百个字节,一断电全没了。想把数据存下来,最直接的办法就是插一张 SD 卡,让它像 U 盘一样能被电脑读走。但 STM32 本身不认识 FAT32 文件系统,它只会按扇区读写原始数据。这时候 fatfs 就派上用场了——它是一个轻量级的 FAT 文件系统模块,专门给单片机用,能把裸扇区操作封装成f_open、f_write、f_close这种文件级接口。这个资源做的事很明确:在 STM32 上通过 SDIO 或 SPI 驱动 SD 卡,移植 fatfs,然后周期性地把采集数据写成.csv文件。csv 的好处是电脑端 Excel、Python pandas 直接能打开,不用自己写解析器。适合谁?做数据采集、工业记录仪、离线日志存储的嵌入式开发者。如果你正在纠结数据怎么落盘、csv 怎么生成、fatfs 怎么配,这篇可以跟着走一遍。2. fatfs 移植与 SD 卡底层驱动:从扇区到文件句柄2.1 先搞清楚 fatfs 的分层结构fatfs 的设计是分层解耦的。最上层是ff.c,提供f_open、f_read、f_write这些 API,跟具体硬件无关。中间是diskio.c,这是一层薄薄的适配层,把 fatfs 的disk_read、disk_write、disk_initialize映射到你自己的 SD 卡驱动。最底下才是 SDIO 或 SPI 的寄存器操作。常见做法是:SDIO 模式跑 4 位总线,理论速率能到几十 Mbps,适合高速采集;SPI 模式接线简单,但速率受限于 SPI 时钟,一般也就几 Mbps。我一般优先选 SDIO,除非引脚实在不够。移植的核心工作就三件事:实现diskio.c里的五个函数、配置ffconf.h里的宏、确保 SD 卡驱动能正常初始化并读写扇区。2.2 diskio.c 的五个函数怎么写diskio.c里必须实现disk_initialize、disk_status、disk_read、disk_write、disk_ioctl。前两个是状态查询,后三个是实际读写。下面是一个基于 SDIO 的典型实现骨架:#include "diskio.h" #include "sdio_driver.h" // 你自己的 SDIO 驱动头文件 #define DEV_SD 0 DSTATUS disk_initialize(BYTE pdrv) { if (pdrv != DEV_SD) return RES_PARERR; if (SDIO_Init() != 0) return STA_NOINIT; // 初始化失败 return 0; // 成功返回 0 } DSTATUS disk_status(BYTE pdrv) { if (pdrv != DEV_SD) return RES_PARERR; return 0; // 简化处理,实际可查卡是否插入 } DRESULT disk_read(BYTE pdrv, BYTE *buff, DWORD sector, UINT count) { if (pdrv != DEV_SD) return RES_PARERR; if (SDIO_ReadBlocks((uint8_t *)buff, sector, count) != 0) return RES_ERROR; return RES_OK; } DRESULT disk_write(BYTE pdrv, const BYTE *buff, DWORD sector, UINT count) { if (pdrv != DEV_SD) return RES_PARERR; if (SDIO_WriteBlocks((uint8_t *)buff, sector, count) != 0) return RES_ERROR; return RES_OK; } DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { if (pdrv != DEV_SD) return RES_PARERR; switch (cmd) { case CTRL_SYNC: return RES_OK; // 等待写完成,SDIO 阻塞模式下可直接返回 case GET_SECTOR_COUNT: *(DWORD *)buff = SDIO_GetSectorCount(); return RES_OK; case GET_SECTOR_SIZE: *(WORD *)buff = 512; return RES_OK; case GET_BLOCK_SIZE: *(DWORD *)buff = 1; // 擦除块大小,SD 卡填 1 即可 return RES_OK; default: return RES_PARERR; } }逻辑说明:disk_initialize里调用你的 SDIO 初始化函数,返回 0 表示成功。disk_read和disk_write直接转发给底层块读写函数,注意sector是扇区号,不是字节地址。disk_ioctl里GET_SECTOR_COUNT必须返回正确的扇区总数,否则 fatfs 格式化或挂载时会算错容量。参数说明:pdrv是物理驱动号,单卡场景固定为 0。buff是数据缓冲区,count是扇区数。CTRL_SYNC在阻塞写模式下可以直接返回 OK,但如果你用了 DMA 或非阻塞写,这里必须等写完成。2.3 ffconf.h 里必须改的几个宏ffconf.h是 fatfs 的配置文件,默认值不一定适合 STM32。下面几个宏我每次都会检查:宏名默认值建议值原因_FS_TINY00设为 1 会省 RAM 但性能下降,采集场景不差这点内存_FS_READONLY00要写 csv 必须为 0_USE_MKFS01卡没格式化时可以直接在板子上格式化_CODE_PAGE932936932 是日文,936 是简体中文,避免文件名乱码_USE_LFN01 或 2支持长文件名,csv 文件名可以写完整日期_MAX_SS512512扇区大小,SD 卡固定 512_VOLUMES11单卡场景一个卷够了_USE_LFN设为 1 时用静态缓冲区,设为 2 时用栈上动态分配,设为 3 时用堆。STM32 上我一般用 1,省得折腾内存管理。2.4 挂载与格式化:第一次用卡必须做的事新卡或者电脑上格式化过的卡,挂载前最好先确认文件系统类型。如果卡是 exFAT 或 NTFS,fatfs 默认不支持,需要f_mkfs重新格式化成 FAT32。下面是一段挂载加自动格式化的代码:FATFS fs; // 文件系统对象 FIL fil; // 文件对象 FRESULT fres; fres = f_mount(&fs, "0:", 1); // 1 表示立即挂载 if (fres != FR_OK) { // 挂载失败,尝试格式化 fres = f_mkfs("0:", FM_FAT32, 0, NULL, 0); if (fres != FR_OK) { printf("Format failed: %d\r\n", fres); return; } fres = f_mount(&fs, "0:", 1); if (fres != FR_OK) { printf("Mount after format failed: %d\r\n", fres); return; } }逻辑说明:f_mount的第三个参数为 1 时会立即执行挂载操作,为 0 时只注册不挂载。f_mkfs的第二个参数指定格式类型,FM_FAT32适合 32GB 以下的卡。第四个参数是工作缓冲区,传 NULL 时 fatfs 会用内部缓冲区,但有些版本要求外部提供,传 NULL 前先确认版本。参数说明:"0:"是逻辑驱动器号,对应diskio.c里的DEV_SD。f_mkfs的第三个参数是簇大小,传 0 表示自动选择。3. 生成 csv 文件:从 f_open 到 f_printf 的完整链路3.1 csv 文件的写入流程csv 本质是纯文本,每行一条记录,字段用逗号分隔。fatfs 提供f_printf函数,可以直接往文件里写格式化字符串,比手动拼接再f_write方便得多。但f_printf需要开启_FS_READONLY为 0 且_USE_STRFUNC不为 0。典型流程是:打开文件(追加模式)→ 写表头(如果是新文件)→ 写数据行 → 关闭文件。下面是一个完整的写入函数:FRESULT write_csv_record(FIL *fil, uint32_t timestamp, float temp, float humi) { FRESULT fres; UINT bw; // 以追加模式打开,如果文件不存在则创建 fres = f_open(fil, "0:data.csv", FA_OPEN_ALWAYS | FA_WRITE); if (fres != FR_OK) return fres; // 移动到文件末尾 fres = f_lseek(fil, f_size(fil)); if (fres != FR_OK) { f_close(fil); return fres; } // 如果文件为空,先写表头 if (f_size(fil) == 0) { f_printf(fil, "timestamp,temperature,humidity\r\n"); } // 写数据行 f_printf(fil, "%lu,%.2f,%.2f\r\n", timestamp, temp, humi); f_close(fil); // 必须关闭,否则数据可能还在缓存里 return FR_OK; }逻辑说明:FA_OPEN_ALWAYS表示文件存在就打开,不存在就创建。f_lseek移动到末尾是为了追加而不是覆盖。f_size返回当前文件大小,用来判断是否需要写表头。f_printf的格式化语法和标准 C 的printf基本一致,但浮点数支持取决于_USE_STRFUNC的配置。参数说明:fil是文件对象指针,调用前需要先f_mount。timestamp用%lu因为它是 32 位无符号数。%.2f保留两位小数,csv 里够用了。\r\n是 Windows 换行符,Excel 打开不会乱行。3.2 写入频率与缓冲策略每次采集都f_open+f_close一次,在低频场景(比如每秒一条)没问题。但如果每秒要写几十上百条,频繁开关文件会拖慢速度,因为每次f_close都会触发 FAT 表更新和扇区写入。我一般会这样做:文件保持打开状态,每隔 N 条或者每隔几秒调用一次f_sync,把缓存刷到卡里。f_sync比f_close轻量,只刷新数据不关闭文件。掉电风险高的场景,N 取小一点,比如 10 条同步一次;对掉电不敏感的场景,可以 100 条同步一次。// 保持文件打开,定期 sync static FIL csv_fil; static uint32_t record_count = 0; void csv_append(uint32_t ts, float t, float h) { f_printf(&csv_fil, "%lu,%.2f,%.2f\r\n", ts, t, h); record_count++; if (record_count % 10 == 0) { f_sync(&csv_fil); // 每 10 条刷一次 } }逻辑说明:f_sync会把文件缓存区的脏数据写回 SD 卡,但不关闭文件句柄。这样既减少了开关开销,又控制了掉电丢数据的窗口。参数说明:record_count是静态变量,记录自上次 sync 以来写了多少条。取 10 是经验值,具体看你的数据重要程度和 SD 卡写入速度。3.3 文件名带时间戳:避免覆盖和历史追溯固定叫data.csv有个问题:每次上电都往同一个文件追加,时间长了文件巨大,而且分不清哪次采集是哪次。更好的做法是用 RTC 的时间生成文件名,比如20250101_120000.csv。这样每次上电新建一个文件,电脑上按文件名就能排序。void create_csv_by_time(FIL *fil, RTC_TimeTypeDef *time, RTC_DateTypeDef *date) { char fname[32]; snprintf(fname, sizeof(fname), "0:%04d%02d%02d_%02d%02d%02d.csv", date->Year + 2000, date->Month, date->Date, time->Hours, time->Minutes, time->Seconds); f_open(fil, fname, FA_CREATE_ALWAYS | FA_WRITE); }逻辑说明:snprintf拼出文件名,FA_CREATE_ALWAYS表示每次都新建,如果同名文件存在就覆盖。RTC 的年份寄存器通常存的是 2000 年起的偏移,所以要加 2000。参数说明:fname缓冲区至少 32 字节,"0:"前缀不能少。snprintf比sprintf安全,能防止缓冲区溢出。4. 避坑与排查:SD 卡 fatfs 最容易翻车的五个地方4.1 挂载返回 FR_NOT_READY 或 FR_DISK_ERR现象:f_mount返回FR_NOT_READY,或者f_open返回FR_DISK_ERR。原因:九成是disk_initialize没成功,或者disk_read读不到扇区 0。SD 卡上电后需要一段初始化时间,如果 MCU 启动太快,卡还没准备好就发命令,就会失败。解决:在disk_initialize里加延时,上电后至少等 1ms 再发 CMD0。另外检查 SDIO 时钟分频,初始化阶段必须降到 400kHz 以下,初始化完成后再切到高速。SPI 模式同理,先低速再高速。4.2 写 csv 后电脑打开乱码或列错位现象:csv 文件在 Excel 里打开,中文乱码,或者所有数据挤在一列。原因:Excel 默认按系统区域设置解析 csv,如果文件里没有 BOM,中文可能乱码。列错位通常是分隔符问题,有些区域设置用分号而不是逗号。解决:表头和数据里不要用中文,字段名用英文。如果一定要中文,在文件开头写入 UTF-8 BOM(0xEF 0xBB 0xBF)。列错位的话,确认f_printf里用的是逗号,且字段值里没有多余的逗号。4.3 f_write 返回成功但数据没落盘现象:f_write和f_close都返回FR_OK,但拔卡插电脑上发现文件是空的或者少了最后几条。原因:fatfs 有缓存机制,f_write只是写到缓存,f_close才真正刷到卡。如果f_close之前掉电,缓存里的数据就丢了。另外有些 SD 卡内部也有缓存,f_close返回后卡可能还在写。解决:对掉电敏感的场景,每次写完调用f_sync,并且f_sync之后加一句disk_ioctl(0, CTRL_SYNC, NULL)确保卡内部也写完。如果还不行,换一张卡试试,有些杂牌卡缓存策略很激进。4.4 长时间写入后速度越来越慢现象:刚开始写 csv 很快,写了几千条之后越来越慢,甚至卡死。原因:FAT 表碎片化。频繁创建删除文件、或者文件反复增长,会导致 FAT 表里簇链变得很长,每次写都要遍历簇链。另外如果 SD 卡质量差,坏块重映射也会拖慢速度。解决:定期整理,比如每天新建一个文件,而不是往同一个文件无限追加。格式化时选大一点的簇,比如 32KB,减少簇链长度。如果卡已经用了很久,用f_mkfs重新格式化一次。4.5 多任务环境下 fatfs 重入问题现象:在 RTOS 里,一个任务写 csv,另一个任务读配置,偶尔会死机或者文件损坏。原因:fatfs 默认不是线程安全的,多个任务同时调用f_open、f_write会破坏内部状态。解决:给 fatfs 加互斥锁。简单做法是用一个全局标志位,访问前检查并置位,访问完清除。更规范的做法是用 RTOS 的互斥量,在f_mount之后创建,每次文件操作前 take,操作完 give。另外_FS_REENTRANT宏可以开启 fatfs 自带的重入保护,但需要你实现ff_req_grant和ff_rel_grant两个信号量函数。5. 进阶技巧:用 f_printf 直接输出带表头的结构化 csv前面写的 csv 是手动拼表头和数据行,其实 fatfs 的f_printf可以更灵活。比如你要记录多种传感器,字段数量不固定,可以用一个格式化函数统一处理。下面这个技巧我经常用:把表头和数据行都封装成函数,字段用数组传入,避免每次改字段都要改代码。// 写表头,fields 是字段名数组,count 是字段数 FRESULT csv_write_header(FIL *fil, const char **fields, uint8_t count) { for (uint8_t i = 0; i < count; i++) { f_printf(fil, "%s", fields[i]); if (i < count - 1) f_printf(fil, ","); } f_printf(fil, "\r\n"); return FR_OK; } // 写一行 float 数据 FRESULT csv_write_row(FIL *fil, const float *values, uint8_t count) { for (uint8_t i = 0; i < count; i++) { f_printf(fil, "%.3f", values[i]); if (i < count - 1) f_printf(fil, ","); } f_printf(fil, "\r\n"); return FR_OK; }逻辑说明:csv_write_header遍历字段名数组,用逗号分隔,最后换行。csv_write_row同理,但输出的是浮点数。这样字段增减只需要改数组,不用动写入逻辑。参数说明:fields是字符串指针数组,values是浮点数组,count是字段数量。%.3f保留三位小数,按需调整。验证方法:写完之后把卡拔下来插电脑,用 Excel 或文本编辑器打开,确认列对齐、行数正确、没有多余空行。更严格的验证是用 Python 的csv模块读一遍:import csv with open('data.csv', 'r') as f: reader = csv.reader(f) for row in reader: print(row)如果 Python 能正常解析,说明格式没问题。我一般还会检查文件大小和记录数是否匹配,比如每条记录约 30 字节,1000 条就是 30KB 左右,差太多说明有丢数据。从那以后我每次写完 csv 都强制走一遍「拔卡→电脑读→Python 解析」的流程,确认无误再批量部署。希望帮到你。本文还有配套的精品资源,点击获取
简介:这份资源面向嵌入式开发学习者与STM32工程师,聚焦在STM32F429平台上集成FatFS文件系统、驱动SD卡写入数据并生成.csv文件,同时结合以太网驱动搭建TCP服务器接收网络数据。内容覆盖FatFS的移植与参数配置、SDIO或SPI方式下的SD卡初始化与读写、CSV格式数据的文件创建与写入,以及基于lwIP的TCP服务器连接建立、数据接收与处理流程,适合具备一定C语言与嵌入式基础、希望打通存储与网络通信链路的开发者参考。资源包共1482个文件,以629个c源文件、382个h头文件为主,另含144个d依赖文件、144个o目标文件、142个crf交叉引用文件及少量库文件、工程配置与说明文档,压缩包约59.43MB,工程结构完整便于直接编译与二次开发。目前已有8150人学习下载,可帮助读者快速理解SD卡文件系统与TCP数据落盘的完整实现思路。1. 从一块 SD 卡说起:为什么 fatfs 是 STM32 数据落盘绕不开的一环手上有个 STM32 的采集板,传感器数据在 RAM 里滚了几百个字节,一断电全没了。想把数据存下来,最直接的办法就是插一张 SD 卡,让它像 U 盘一样能被电脑读走。但 STM32 本身不认识 FAT32 文件系统,它只会按扇区读写原始数据。这时候 fatfs 就派上用场了——它是一个轻量级的 FAT 文件系统模块,专门给单片机用,能把裸扇区操作封装成f_open、f_write、f_close这种文件级接口。这个资源做的事很明确:在 STM32 上通过 SDIO 或 SPI 驱动 SD 卡,移植 fatfs,然后周期性地把采集数据写成.csv文件。csv 的好处是电脑端 Excel、Python pandas 直接能打开,不用自己写解析器。适合谁?做数据采集、工业记录仪、离线日志存储的嵌入式开发者。如果你正在纠结数据怎么落盘、csv 怎么生成、fatfs 怎么配,这篇可以跟着走一遍。2. fatfs 移植与 SD 卡底层驱动:从扇区到文件句柄2.1 先搞清楚 fatfs 的分层结构fatfs 的设计是分层解耦的。最上层是ff.c,提供f_open、f_read、f_write这些 API,跟具体硬件无关。中间是diskio.c,这是一层薄薄的适配层,把 fatfs 的disk_read、disk_write、disk_initialize映射到你自己的 SD 卡驱动。最底下才是 SDIO 或 SPI 的寄存器操作。常见做法是:SDIO 模式跑 4 位总线,理论速率能到几十 Mbps,适合高速采集;SPI 模式接线简单,但速率受限于 SPI 时钟,一般也就几 Mbps。我一般优先选 SDIO,除非引脚实在不够。移植的核心工作就三件事:实现diskio.c里的五个函数、配置ffconf.h里的宏、确保 SD 卡驱动能正常初始化并读写扇区。2.2 diskio.c 的五个函数怎么写diskio.c里必须实现disk_initialize、disk_status、disk_read、disk_write、disk_ioctl。前两个是状态查询,后三个是实际读写。下面是一个基于 SDIO 的典型实现骨架:#include "diskio.h" #include "sdio_driver.h" // 你自己的 SDIO 驱动头文件 #define DEV_SD 0 DSTATUS disk_initialize(BYTE pdrv) { if (pdrv != DEV_SD) return RES_PARERR; if (SDIO_Init() != 0) return STA_NOINIT; // 初始化失败 return 0; // 成功返回 0 } DSTATUS disk_status(BYTE pdrv) { if (pdrv != DEV_SD) return RES_PARERR; return 0; // 简化处理,实际可查卡是否插入 } DRESULT disk_read(BYTE pdrv, BYTE *buff, DWORD sector, UINT count) { if (pdrv != DEV_SD) return RES_PARERR; if (SDIO_ReadBlocks((uint8_t *)buff, sector, count) != 0) return RES_ERROR; return RES_OK; } DRESULT disk_write(BYTE pdrv, const BYTE *buff, DWORD sector, UINT count) { if (pdrv != DEV_SD) return RES_PARERR; if (SDIO_WriteBlocks((uint8_t *)buff, sector, count) != 0) return RES_ERROR; return RES_OK; } DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { if (pdrv != DEV_SD) return RES_PARERR; switch (cmd) { case CTRL_SYNC: return RES_OK; // 等待写完成,SDIO 阻塞模式下可直接返回 case GET_SECTOR_COUNT: *(DWORD *)buff = SDIO_GetSectorCount(); return RES_OK; case GET_SECTOR_SIZE: *(WORD *)buff = 512; return RES_OK; case GET_BLOCK_SIZE: *(DWORD *)buff = 1; // 擦除块大小,SD 卡填 1 即可 return RES_OK; default: return RES_PARERR; } }逻辑说明:disk_initialize里调用你的 SDIO 初始化函数,返回 0 表示成功。disk_read和disk_write直接转发给底层块读写函数,注意sector是扇区号,不是字节地址。disk_ioctl里GET_SECTOR_COUNT必须返回正确的扇区总数,否则 fatfs 格式化或挂载时会算错容量。参数说明:pdrv是物理驱动号,单卡场景固定为 0。buff是数据缓冲区,count是扇区数。CTRL_SYNC在阻塞写模式下可以直接返回 OK,但如果你用了 DMA 或非阻塞写,这里必须等写完成。2.3 ffconf.h 里必须改的几个宏ffconf.h是 fatfs 的配置文件,默认值不一定适合 STM32。下面几个宏我每次都会检查:宏名默认值建议值原因_FS_TINY00设为 1 会省 RAM 但性能下降,采集场景不差这点内存_FS_READONLY00要写 csv 必须为 0_USE_MKFS01卡没格式化时可以直接在板子上格式化_CODE_PAGE932936932 是日文,936 是简体中文,避免文件名乱码_USE_LFN01 或 2支持长文件名,csv 文件名可以写完整日期_MAX_SS512512扇区大小,SD 卡固定 512_VOLUMES11单卡场景一个卷够了_USE_LFN设为 1 时用静态缓冲区,设为 2 时用栈上动态分配,设为 3 时用堆。STM32 上我一般用 1,省得折腾内存管理。2.4 挂载与格式化:第一次用卡必须做的事新卡或者电脑上格式化过的卡,挂载前最好先确认文件系统类型。如果卡是 exFAT 或 NTFS,fatfs 默认不支持,需要f_mkfs重新格式化成 FAT32。下面是一段挂载加自动格式化的代码:FATFS fs; // 文件系统对象 FIL fil; // 文件对象 FRESULT fres; fres = f_mount(&fs, "0:", 1); // 1 表示立即挂载 if (fres != FR_OK) { // 挂载失败,尝试格式化 fres = f_mkfs("0:", FM_FAT32, 0, NULL, 0); if (fres != FR_OK) { printf("Format failed: %d\r\n", fres); return; } fres = f_mount(&fs, "0:", 1); if (fres != FR_OK) { printf("Mount after format failed: %d\r\n", fres); return; } }逻辑说明:f_mount的第三个参数为 1 时会立即执行挂载操作,为 0 时只注册不挂载。f_mkfs的第二个参数指定格式类型,FM_FAT32适合 32GB 以下的卡。第四个参数是工作缓冲区,传 NULL 时 fatfs 会用内部缓冲区,但有些版本要求外部提供,传 NULL 前先确认版本。参数说明:"0:"是逻辑驱动器号,对应diskio.c里的DEV_SD。f_mkfs的第三个参数是簇大小,传 0 表示自动选择。3. 生成 csv 文件:从 f_open 到 f_printf 的完整链路3.1 csv 文件的写入流程csv 本质是纯文本,每行一条记录,字段用逗号分隔。fatfs 提供f_printf函数,可以直接往文件里写格式化字符串,比手动拼接再f_write方便得多。但f_printf需要开启_FS_READONLY为 0 且_USE_STRFUNC不为 0。典型流程是:打开文件(追加模式)→ 写表头(如果是新文件)→ 写数据行 → 关闭文件。下面是一个完整的写入函数:FRESULT write_csv_record(FIL *fil, uint32_t timestamp, float temp, float humi) { FRESULT fres; UINT bw; // 以追加模式打开,如果文件不存在则创建 fres = f_open(fil, "0:data.csv", FA_OPEN_ALWAYS | FA_WRITE); if (fres != FR_OK) return fres; // 移动到文件末尾 fres = f_lseek(fil, f_size(fil)); if (fres != FR_OK) { f_close(fil); return fres; } // 如果文件为空,先写表头 if (f_size(fil) == 0) { f_printf(fil, "timestamp,temperature,humidity\r\n"); } // 写数据行 f_printf(fil, "%lu,%.2f,%.2f\r\n", timestamp, temp, humi); f_close(fil); // 必须关闭,否则数据可能还在缓存里 return FR_OK; }逻辑说明:FA_OPEN_ALWAYS表示文件存在就打开,不存在就创建。f_lseek移动到末尾是为了追加而不是覆盖。f_size返回当前文件大小,用来判断是否需要写表头。f_printf的格式化语法和标准 C 的printf基本一致,但浮点数支持取决于_USE_STRFUNC的配置。参数说明:fil是文件对象指针,调用前需要先f_mount。timestamp用%lu因为它是 32 位无符号数。%.2f保留两位小数,csv 里够用了。\r\n是 Windows 换行符,Excel 打开不会乱行。3.2 写入频率与缓冲策略每次采集都f_open+f_close一次,在低频场景(比如每秒一条)没问题。但如果每秒要写几十上百条,频繁开关文件会拖慢速度,因为每次f_close都会触发 FAT 表更新和扇区写入。我一般会这样做:文件保持打开状态,每隔 N 条或者每隔几秒调用一次f_sync,把缓存刷到卡里。f_sync比f_close轻量,只刷新数据不关闭文件。掉电风险高的场景,N 取小一点,比如 10 条同步一次;对掉电不敏感的场景,可以 100 条同步一次。// 保持文件打开,定期 sync static FIL csv_fil; static uint32_t record_count = 0; void csv_append(uint32_t ts, float t, float h) { f_printf(&csv_fil, "%lu,%.2f,%.2f\r\n", ts, t, h); record_count++; if (record_count % 10 == 0) { f_sync(&csv_fil); // 每 10 条刷一次 } }逻辑说明:f_sync会把文件缓存区的脏数据写回 SD 卡,但不关闭文件句柄。这样既减少了开关开销,又控制了掉电丢数据的窗口。参数说明:record_count是静态变量,记录自上次 sync 以来写了多少条。取 10 是经验值,具体看你的数据重要程度和 SD 卡写入速度。3.3 文件名带时间戳:避免覆盖和历史追溯固定叫data.csv有个问题:每次上电都往同一个文件追加,时间长了文件巨大,而且分不清哪次采集是哪次。更好的做法是用 RTC 的时间生成文件名,比如20250101_120000.csv。这样每次上电新建一个文件,电脑上按文件名就能排序。void create_csv_by_time(FIL *fil, RTC_TimeTypeDef *time, RTC_DateTypeDef *date) { char fname[32]; snprintf(fname, sizeof(fname), "0:%04d%02d%02d_%02d%02d%02d.csv", date->Year + 2000, date->Month, date->Date, time->Hours, time->Minutes, time->Seconds); f_open(fil, fname, FA_CREATE_ALWAYS | FA_WRITE); }逻辑说明:snprintf拼出文件名,FA_CREATE_ALWAYS表示每次都新建,如果同名文件存在就覆盖。RTC 的年份寄存器通常存的是 2000 年起的偏移,所以要加 2000。参数说明:fname缓冲区至少 32 字节,"0:"前缀不能少。snprintf比sprintf安全,能防止缓冲区溢出。4. 避坑与排查:SD 卡 fatfs 最容易翻车的五个地方4.1 挂载返回 FR_NOT_READY 或 FR_DISK_ERR现象:f_mount返回FR_NOT_READY,或者f_open返回FR_DISK_ERR。原因:九成是disk_initialize没成功,或者disk_read读不到扇区 0。SD 卡上电后需要一段初始化时间,如果 MCU 启动太快,卡还没准备好就发命令,就会失败。解决:在disk_initialize里加延时,上电后至少等 1ms 再发 CMD0。另外检查 SDIO 时钟分频,初始化阶段必须降到 400kHz 以下,初始化完成后再切到高速。SPI 模式同理,先低速再高速。4.2 写 csv 后电脑打开乱码或列错位现象:csv 文件在 Excel 里打开,中文乱码,或者所有数据挤在一列。原因:Excel 默认按系统区域设置解析 csv,如果文件里没有 BOM,中文可能乱码。列错位通常是分隔符问题,有些区域设置用分号而不是逗号。解决:表头和数据里不要用中文,字段名用英文。如果一定要中文,在文件开头写入 UTF-8 BOM(0xEF 0xBB 0xBF)。列错位的话,确认f_printf里用的是逗号,且字段值里没有多余的逗号。4.3 f_write 返回成功但数据没落盘现象:f_write和f_close都返回FR_OK,但拔卡插电脑上发现文件是空的或者少了最后几条。原因:fatfs 有缓存机制,f_write只是写到缓存,f_close才真正刷到卡。如果f_close之前掉电,缓存里的数据就丢了。另外有些 SD 卡内部也有缓存,f_close返回后卡可能还在写。解决:对掉电敏感的场景,每次写完调用f_sync,并且f_sync之后加一句disk_ioctl(0, CTRL_SYNC, NULL)确保卡内部也写完。如果还不行,换一张卡试试,有些杂牌卡缓存策略很激进。4.4 长时间写入后速度越来越慢现象:刚开始写 csv 很快,写了几千条之后越来越慢,甚至卡死。原因:FAT 表碎片化。频繁创建删除文件、或者文件反复增长,会导致 FAT 表里簇链变得很长,每次写都要遍历簇链。另外如果 SD 卡质量差,坏块重映射也会拖慢速度。解决:定期整理,比如每天新建一个文件,而不是往同一个文件无限追加。格式化时选大一点的簇,比如 32KB,减少簇链长度。如果卡已经用了很久,用f_mkfs重新格式化一次。4.5 多任务环境下 fatfs 重入问题现象:在 RTOS 里,一个任务写 csv,另一个任务读配置,偶尔会死机或者文件损坏。原因:fatfs 默认不是线程安全的,多个任务同时调用f_open、f_write会破坏内部状态。解决:给 fatfs 加互斥锁。简单做法是用一个全局标志位,访问前检查并置位,访问完清除。更规范的做法是用 RTOS 的互斥量,在f_mount之后创建,每次文件操作前 take,操作完 give。另外_FS_REENTRANT宏可以开启 fatfs 自带的重入保护,但需要你实现ff_req_grant和ff_rel_grant两个信号量函数。5. 进阶技巧:用 f_printf 直接输出带表头的结构化 csv前面写的 csv 是手动拼表头和数据行,其实 fatfs 的f_printf可以更灵活。比如你要记录多种传感器,字段数量不固定,可以用一个格式化函数统一处理。下面这个技巧我经常用:把表头和数据行都封装成函数,字段用数组传入,避免每次改字段都要改代码。// 写表头,fields 是字段名数组,count 是字段数 FRESULT csv_write_header(FIL *fil, const char **fields, uint8_t count) { for (uint8_t i = 0; i < count; i++) { f_printf(fil, "%s", fields[i]); if (i < count - 1) f_printf(fil, ","); } f_printf(fil, "\r\n"); return FR_OK; } // 写一行 float 数据 FRESULT csv_write_row(FIL *fil, const float *values, uint8_t count) { for (uint8_t i = 0; i < count; i++) { f_printf(fil, "%.3f", values[i]); if (i < count - 1) f_printf(fil, ","); } f_printf(fil, "\r\n"); return FR_OK; }逻辑说明:csv_write_header遍历字段名数组,用逗号分隔,最后换行。csv_write_row同理,但输出的是浮点数。这样字段增减只需要改数组,不用动写入逻辑。参数说明:fields是字符串指针数组,values是浮点数组,count是字段数量。%.3f保留三位小数,按需调整。验证方法:写完之后把卡拔下来插电脑,用 Excel 或文本编辑器打开,确认列对齐、行数正确、没有多余空行。更严格的验证是用 Python 的csv模块读一遍:import csv with open('data.csv', 'r') as f: reader = csv.reader(f) for row in reader: print(row)如果 Python 能正常解析,说明格式没问题。我一般还会检查文件大小和记录数是否匹配,比如每条记录约 30 字节,1000 条就是 30KB 左右,差太多说明有丢数据。从那以后我每次写完 csv 都强制走一遍「拔卡→电脑读→Python 解析」的流程,确认无误再批量部署。希望帮到你。本文还有配套的精品资源,点击获取