☰
STM32 FatFs移植实战:SD卡CSV数据存储与以太网传输扩展
2026/10/19 17:36:32 网站建设 项目流程

简介:本资源面向嵌入式开发工程师与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_sync,FatFs 有缓存机制,但为了数据安全,我一般会在每写入 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/s

SD 卡的写入速度受几个因素影响:时钟频率、总线宽度、以及 FatFs 的缓存策略。如果用的是 SPI 模式,时钟拉到 20MHz 以上速度也很难超过 1MB/s。换成 SDIO 四线模式,时钟 48MHz 下写入速度可以到 5MB/s 以上。另外f_write的调用频率也有影响,每次写几个字节和每次写几百个字节,开销差别很大。我一般会攒够 512 字节再调用一次f_write,减少函数调用和扇区操作的次数。

4.4 现象:CSV 文件在 Excel 里打开乱码

这通常是编码问题。STM32 里sprintf生成的是 ASCII 字符串,如果表头或者数据里混入了中文,而文件没有 BOM,Excel 可能会用本地编码去解析,导致乱码。解决办法是纯英文表头,或者手动在文件开头写入 UTF-8 BOM(0xEF 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再继续采集,哪怕牺牲一点速度,也比事后发现数据缺了一块强。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询