简介本资源面向嵌入式开发工程师与STM32学习者提供一套基于STM32F429的完整工程实践方案解决SD卡文件系统读写与网络数据落盘的实际问题。项目将FatFS轻量级文件系统移植到STM32平台通过SPI或SDIO协议驱动SD卡实现文件创建与CSV格式数据写入并借助以太网MAC与lwIP协议栈搭建TCP服务器把网络接收到的数据解析后可靠存储到SD卡中覆盖文件系统配置、SD卡初始化、CSV记录格式、TCP连接与数据收发等关键环节。压缩包共1482个文件以629个c源文件、382个h头文件为主另含144个d依赖文件、144个o目标文件及crf、lib、uvprojx等工程与库文件整体约59.43MB工程结构完整可直接编译参考。目前已有8150人学习下载适合希望打通存储与网络通信链路的开发者对照实践、快速复用与排错。1. 从一张“存不下来”的波形图说起STM32 上跑 FatFs 到底解决了什么做过数据采集的同行大概都遇到过这种场景板子上的传感器跑得好好的串口也能看到实时数值可一旦想把这些数据攒下来做后续分析就发现 RAM 根本不够用掉电之后更是什么都不剩。这时候 SD 卡加 FatFs 文件系统就成了一个很自然的选择——它让 STM32 这种资源有限的单片机也能像电脑一样往一张普通 SD 卡里写文件而且写出来的还是 PC 能直接打开、Excel 能直接读的 .csv 格式。这个项目要干的事就是把 SD 卡的底层驱动、FatFs 文件系统移植、以及 .csv 文件的生成逻辑串成一条完整的链路让采集到的数据以逗号分隔的形式落盘。它适合正在做数据记录仪、工业现场采集终端、或者任何需要“离线存数据、事后用表格分析”的嵌入式开发者。以太网驱动和 TCP 服务器这两个关键词也出现在检索里说明不少人还想把存下来的数据通过网络传出去这个思路后面在改造章节会展开。2. 整体架构与目录结构从 SD 卡引脚到 FatFs 的调用链2.1 硬件层与软件层的分工这个项目的代码结构可以分成四层来看。最底下是 STM32 的 SDIO 外设或者 SPI 外设负责和 SD 卡进行物理层面的时序交互往上一层是 SD 卡的底层驱动处理卡的初始化、读写命令、以及块数据的搬运再往上是 FatFs 的磁盘 I/O 接口层也就是diskio.c里那几个disk_initialize、disk_read、disk_write函数它们把 SD 卡的块读写操作翻译成 FatFs 能理解的扇区操作最顶上才是应用层调用f_open、f_write、f_close这些 FatFs 的 API 来创建和写入 .csv 文件。常见做法是 SD 卡用 SDIO 四线模式因为速度比 SPI 快不少尤其是写大块数据的时候差距明显。但如果引脚紧张或者对速度要求不高SPI 模式也能跑只是要接受写入速率打个折扣。我一般会在diskio.c里把SD_ReadBlocks和SD_WriteBlocks这两个函数实现好FatFs 的移植就完成了一大半。2.2 目录结构长什么样一个典型的工程目录大概是这样组织的Project/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── fatfs.h │ │ └── sdio.h │ └── Src/ │ ├── main.c │ ├── fatfs.c │ └── sdio.c ├── FatFs/ │ ├── Inc/ │ │ ├── ff.h │ │ ├── ffconf.h │ │ └── diskio.h │ └── Src/ │ ├── ff.c │ ├── diskio.c │ └── ffsystem.c └── Drivers/ └── STM32F4xx_HAL_Driver/FatFs/Src/diskio.c是移植的关键文件ffconf.h里则决定了 FatFs 的功能裁剪比如是否支持长文件名、是否启用格式化、扇区大小是 512 还是 4096。这些配置直接影响到最终固件的大小和运行时的内存占用。2.3 数据从传感器到 .csv 的完整路径假设采集到的是三轴加速度数据每个采样点有三个 int16 的值。应用层的逻辑是先挂载文件系统然后以追加模式打开一个 .csv 文件把三个数值格式化成字符串中间用逗号隔开末尾加换行符再通过f_write写进去。写够一定数量或者定时器触发时调用f_sync把缓存刷到卡里防止突然断电导致数据丢失。这个链路里任何一个环节出问题最终表现都是“文件创建了但里面是空的”或者“数据写了一半就卡死”。3. 核心模块拆解SD 卡驱动、FatFs 移植与 .csv 写入3.1 SD 卡底层驱动的初始化与读写SD 卡的初始化流程比较讲究尤其是上电时序和时钟频率的切换。下面这段代码是基于 SDIO 外设的初始化骨架实际项目中会根据具体的 STM32 型号调整时钟分频和 GPIO 配置。// sdio.c 中的初始化函数 uint8_t SD_Init(void) { uint8_t status; // 上电后先以低速时钟发送至少 74 个时钟脉冲 SDIO_ClockCmd(DISABLE); for (uint8_t i 0; i 10; i) { SDIO_SendCommand(CMD0, 0); // 发送 CMD0 进入 idle 状态 } // 发送 CMD8 检查电压范围只有 SDHC 卡才支持 status SD_SendCmd(CMD8, 0x1AA, 0x87); if (status SD_OK) { // 卡支持 2.7-3.6V 电压继续后续流程 SD_SendCmd(CMD55, 0, 0); SD_SendCmd(ACMD41, 0x40000000, 0); // 发送 HCS 位表示支持高容量卡 while (SD_SendCmd(CMD55, 0, 0) SD_OK) { if (SD_SendCmd(ACMD41, 0x40000000, 0) SD_OK) break; HAL_Delay(1); } // 读取 OCR 寄存器确认卡的类型 SD_SendCmd(CMD58, 0, 0x87); // 根据 CCS 位判断是 SDSC 还是 SDHC/SDXC } // 提高时钟频率到 24MHz 或更高 SDIO_ClockCmd(ENABLE); return SD_OK; }这段代码里几个关键点CMD0 之前必须给足 74 个时钟脉冲否则卡可能不响应CMD8 用来区分是老卡还是新卡ACMD41 循环等待卡退出 busy 状态这个循环必须有超时不然卡坏了程序就死在这里。参数0x40000000是 HCS 位告诉卡主机支持高容量。最后通过 CMD58 读 OCR 的 CCS 位来决定后续寻址是用字节地址还是块地址这一步搞错了读写就会全部错位。3.2 diskio.c 里的四个关键函数FatFs 不关心底层是 SD 卡还是 NAND Flash它只要求你提供几个标准的磁盘操作函数。diskio.c就是干这个翻译工作的。// diskio.c 中与 SD 卡对接的部分 DSTATUS disk_initialize(BYTE pdrv) { if (pdrv ! 0) return RES_PARERR; if (SD_Init() ! SD_OK) { return STA_NOINIT; // 初始化失败FatFs 会认为磁盘不可用 } return 0; // 初始化成功 } DRESULT disk_read(BYTE pdrv, BYTE *buff, DWORD sector, UINT count) { if (pdrv ! 0) return RES_PARERR; if (SD_ReadBlocks(buff, sector, count) ! SD_OK) { return RES_ERROR; } return RES_OK; } DRESULT disk_write(BYTE pdrv, const BYTE *buff, DWORD sector, UINT count) { if (pdrv ! 0) return RES_PARERR; if (SD_WriteBlocks((BYTE *)buff, sector, count) ! SD_OK) { return RES_ERROR; } return RES_OK; } DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { switch (cmd) { case CTRL_SYNC: // 等待所有写操作完成确保数据落盘 return (SD_WaitWriteComplete() SD_OK) ? RES_OK : RES_ERROR; case GET_SECTOR_SIZE: *(WORD *)buff 512; return RES_OK; case GET_BLOCK_SIZE: *(DWORD *)buff 1; return RES_OK; default: return RES_PARERR; } }disk_initialize返回STA_NOINIT时FatFs 的f_mount会失败应用层就能知道卡没插好或者初始化有问题。disk_read和disk_write里的sector参数是扇区号不是字节地址SDHC 卡本身就是按块寻址的所以直接透传即可。disk_ioctl里的CTRL_SYNC很重要它保证f_sync调用时数据真正写到了卡上而不是停在 SDIO 的 FIFO 里。3.3 生成 .csv 文件的写入逻辑应用层的代码看起来简单但细节决定成败。下面是一个典型的写入函数// main.c 中生成 csv 文件的逻辑 FATFS fs; // 文件系统对象 FIL csvFile; // 文件对象 FRESULT fres; char buffer[64]; void CSV_WriteSample(int16_t x, int16_t y, int16_t z) { UINT bytesWritten; // 格式化一行数据逗号分隔末尾换行 int len sprintf(buffer, %d,%d,%d\r\n, x, y, z); fres f_write(csvFile, buffer, len, bytesWritten); if (fres ! FR_OK || bytesWritten ! len) { // 写入失败可能是卡满了或者接触不良 // 实际项目中这里应该点亮错误指示灯或者记录错误计数 } } void CSV_StartNewFile(void) { // 挂载文件系统0 表示立即挂载 fres f_mount(fs, 0:, 1); if (fres ! FR_OK) return; // 以创建追加的方式打开文件如果文件不存在就新建 fres f_open(csvFile, 0:data.csv, FA_OPEN_ALWAYS | FA_WRITE); if (fres ! FR_OK) return; // 移动文件指针到末尾实现追加写入 f_lseek(csvFile, f_size(csvFile)); // 写入表头方便 Excel 识别列名 f_write(csvFile, X,Y,Z\r\n, 7, NULL); }sprintf把三个整数拼成一行 CSV\r\n是 Windows 和 Excel 都认的换行符只用\n在某些版本的 Excel 里会显示成一行。FA_OPEN_ALWAYS表示文件存在就打开、不存在就创建配合f_lseek到文件末尾就能实现追加。表头写一次就行但如果每次上电都追加表头会重复出现所以实际项目中通常会检查文件是否为空再决定要不要写表头。f_write之后不一定要马上f_syncFatFs 有缓存机制但为了数据安全我一般会在每写入 100 行或者每秒调用一次f_sync代价是写入速度会慢一些。4. 避坑与排查SD 卡写不进去、文件乱码、速度慢的常见原因4.1 现象f_mount 返回 FR_NOT_READY原因通常是 SD 卡初始化没通过可能是卡没插好、供电不足、或者上拉电阻没接。SD 卡在初始化阶段电流需求比较大如果板子的 3.3V 电源带载能力弱卡会反复复位。解决方法是先用示波器看卡座上的电压在初始化时有没有跌落如果有换一个 LDO 或者加大滤波电容。另外检查 CMD 和 DAT 线上的上拉电阻SDIO 模式一般需要 10K 到 50K 的上拉。4.2 现象文件创建成功但内容为空这种情况多半是f_write之后没有f_sync或者f_close数据还在 FatFs 的缓存里而程序已经复位或者断电了。FatFs 的写缓存默认是 512 字节的扇区缓冲只有写满一个扇区或者主动 sync 才会真正调用disk_write。解决方法是养成习惯在每次批量写入结束后调用f_sync在程序正常结束前调用f_close。如果对数据完整性要求极高可以把ffconf.h里的_FS_TINY设为 0 并减小缓存但这样会牺牲一些性能。4.3 现象写入速度只有几十 KB/sSD 卡的写入速度受几个因素影响时钟频率、总线宽度、以及 FatFs 的缓存策略。如果用的是 SPI 模式时钟拉到 20MHz 以上速度也很难超过 1MB/s。换成 SDIO 四线模式时钟 48MHz 下写入速度可以到 5MB/s 以上。另外f_write的调用频率也有影响每次写几个字节和每次写几百个字节开销差别很大。我一般会攒够 512 字节再调用一次f_write减少函数调用和扇区操作的次数。4.4 现象CSV 文件在 Excel 里打开乱码这通常是编码问题。STM32 里sprintf生成的是 ASCII 字符串如果表头或者数据里混入了中文而文件没有 BOMExcel 可能会用本地编码去解析导致乱码。解决办法是纯英文表头或者手动在文件开头写入 UTF-8 BOM0xEF 0xBB 0xBF。另外注意换行符用\r\n不要只用\n。4.5 现象写一会儿就卡死如果f_write或者disk_write进入死循环最常见的原因是 SD 卡进入了 busy 状态但程序没有超时机制。SD 卡在内部擦除或者编程时会把 DAT0 线拉低主机必须等待它释放。如果卡的质量不好或者供电不稳这个等待可能永远不结束。解决方法是在SD_WaitWriteComplete里加超时计数超过一定时间就返回错误让上层决定是重试还是报错。5. 二次开发与改造从本地 CSV 到以太网传输的扩展思路5.1 把 CSV 数据通过 TCP 服务器推出去检索里出现了“以太网驱动”和“tcp服务器”说明不少人的最终目标不是把数据存在卡里而是实时传到上位机。一个可行的改造方案是保留 FatFs 的本地存储作为断线缓存同时用 LwIP 协议栈起一个 TCP 服务器。采集任务把数据同时写入 CSV 文件和发送缓冲区网络线程负责把缓冲区里的数据通过 socket 发出去。如果网络断了数据继续存卡等网络恢复后再把卡里没发完的数据补传。这样既保证了实时性又不会因为网络抖动丢数据。5.2 文件系统的性能调优参数ffconf.h里有几个参数值得根据场景调整参数默认值作用调整建议_FS_TINY0是否使用微小模式设为 1 可省 RAM但只能单文件操作_MAX_SS512扇区大小SD 卡保持 512 不要改_FS_REENTRANT0是否支持重入多线程访问时设为 1 并实现锁_USE_FASTSEEK0快速定位频繁 lseek 时开启可提速如果项目里同时有多个任务要读写文件_FS_REENTRANT必须打开并且要在ffsystem.c里实现ff_cre_syncobj、ff_req_grant这些同步函数否则会出现文件系统内部状态错乱。5.3 一个容易忽略的细节卡的热插拔SD 卡是可插拔的但 FatFs 默认不会检测卡的拔出。如果卡在写入过程中被拔掉f_write会返回错误但文件系统内部可能已经处于不一致状态。稳妥的做法是加一个卡检测引脚的中断在中断里调用f_mount(NULL, 0:, 0)卸载文件系统并停止所有写入任务。等卡重新插入后再重新挂载。这个逻辑不复杂但很多人在第一版代码里都会漏掉直到现场把卡拔坏了才后悔。5.4 验证数据完整性的一个笨办法写完 CSV 之后我习惯把卡拔下来插到电脑上用 Excel 打开看看行数对不对、有没有断行、数值范围是否合理。更严格一点的做法是在固件里加一个校验和每写入一行就累加一个 CRC文件末尾再写一行总校验值。上位机解析时先算一遍 CRC 对比不一致就说明写入过程中出过错。这个办法虽然土但在现场调试时能省下大量排查时间。从那以后我每次做 SD 卡存储的项目都会在f_write后面强制走一遍f_sync再继续采集哪怕牺牲一点速度也比事后发现数据缺了一块强。希望帮到你。本文还有配套的精品资源点击获取