news 2026/8/23 8:52:33

STM32 SD 卡 + FatFS 实战:掉电丢数据?f_sync 和簇对齐写救你

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32 SD 卡 + FatFS 实战:掉电丢数据?f_sync 和簇对齐写救你

给温控器加数据记录功能那次,客户要求"断电前至少保留最近 1000 条记录"。我一开始用片上 Flash 轮流擦两页存,每条 16 字节,两页一共只能存 128 条。后来换了 SD 卡,FatFS 一挂,f_open 一个 CSV 文件一行行 f_write,感觉空间无限。

结果第一次现场测试就翻车:客户拔电源去搬设备,插回来一看日志文件大小是 0KB。一千多条温度记录全没了。原因很简单:我只在程序正常退出时调了f_close(),突然断电的时候文件系统的缓存还在 RAM 里,根本没落盘。后来加了f_sync()每 10 条刷一次,再也没丢过数据。

原理人话版

SD 卡就是一块带控制器的 NAND Flash。它自己内部管磨损均衡和坏块屏蔽,对外只暴露标准的块设备接口(以 512 字节扇区为单位读写)。你不用操心它里面怎么擦怎么搬,只管按扇区读写。

STM32 接 SD 卡有两条路:

  • SDIO(推荐):四根数据线 D0-D3 加一根命令线 CMD,最高 48MHz 时钟下理论带宽 24MB/s。速度快、CPU 占用低(支持 DMA),但占用的 GPIO 多。
  • SPI 兼容模式:只用 MOSI/MISO/SCK/CS 四根线,速度慢很多(SPI 一般跑 25MHz 以下),但省引脚。适合 GPIO 紧张或者芯片没有 SDIO 外设的情况。

FatFS 是一个开源的嵌入式 FAT 文件系统实现。它把底层的"按扇区读写"翻译成你熟悉的f_open/f_read/f_write/f_close这套文件操作。你只需要提供两个函数:一个读扇区、一个写扇区,FatFS 剩下的目录管理、簇链追踪、FAT 表维护全帮你搞定。

CubeMX 怎么配(以 SDIO 4-bit + FatFS 为例)

第一步:开 SDIO 外设

在 Connectivity 里找到SDIO,模式选SD 4 bits Wide bus

  • Clock Divider (CLKDIV):初始化阶段 SD 卡要求时钟在 400kHz 以内,CubeMX 生成的代码会自动在初始化流程里先设低速、握手完成后再切高速。这里填个初始分频值(比如 118 对应 400kHz 左右)。
  • Flow Control:勾上。DMA 传输满或空时自动插入等待周期,防止数据溢出。
  • DMA Settings:开启 SDIO 的 TX 和 RX DMA 通道,选 Circular 或 Normal 模式都行(单次传输用 Normal 就够了)。

引脚分配确认:

  • CMD:PC12
  • CK:PC12(注意 CK 和 CMD 在某些封装上是复用的)
  • D0-D3:PC8-PC11
  • CD(卡检测):可选 GPIO,检测卡是否插入
  • WP(写保护):可选 GPIO

第二步:开 FatFS 中间件

Middleware 里找到FATFS,模式选SD Card

  • Use DMA Template:勾上,让 FatFS 底层走 DMA 读写扇区。
  • Use Long Filename:看你需要。开了之后每个文件对象多占几十字节堆内存,但文件名可以超过 8.3 格式。日志文件建议开(LOG_20260805.csv这种名字方便检索)。
  • Code Pages:填 936(简体中文 GBK),否则中文文件名乱码。
  • Max Sector Size:保持默认 512(SD 卡标准扇区大小)。
  • Min Allocation Unit Size:也就是簇大小。512 字节适合小文件频繁写的场景;4096 或 8192 适合大文件顺序写。日志场景建议 4096。

代码:挂卡、写日志、安全落盘

CubeMX 生成的MX_FATFS_Init()会调用f_mount()把 SD 卡挂载到指定逻辑盘(默认/就是 “0:”)。你直接用:

#include"fatfs.h"#include"sd_diskio.h"FATFS fs;// 文件系统对象FIL fil;// 文件对象charlog_buf[256];// 上电挂载 SD 卡intsd_card_init(void){FRESULT fr=f_mount(&fs,"0:",1);// 1 = 立即挂载, 不延迟if(fr!=FR_OK)return-1;// 可选: 检查剩余空间DWORD free_clusters,free_sectors;FATFS*pfs;fr=f_getfree("0:",&free_clusters,&pfs);if(fr==FR_OK){free_sectors=free_clusters*fs.csize;printf("SD free: %lu KB\r\n",(uint32_t)free_sectors/2);}return0;}

写日志的核心循环:打开文件(追加模式)、格式化一行、写入、定期同步。

// 写一条温度记录, 追加到 LOG_YYYYMMDD.csvintlog_temperature(floattemp,floatsetpoint){staticuint16_tline_count=0;// 打开文件: FA_OPEN_APPEND = 不覆盖已有内容, 从末尾追加FRESULT fr=f_open(&fil,"0:/LOG_20260816.csv",FA_WRITE|FA_OPEN_ALWAYS|FA_OPEN_APPEND);if(fr!=FR_OK)return-1;intlen=snprintf(log_buf,sizeof(log_buf),"%.3f,%.3f,%lu\r\n",temp,setpoint,(unsignedlong)(HAL_GetTick()/1000));UINT written;fr=f_write(&fil,log_buf,len,&written);if(fr!=FR_OK||written!=(UINT)len){f_close(&fil);return-2;}line_count++;// ★ 每 10 条 sync 一次, 断电最多丢 9 条if(line_count%10==0){f_sync(&fil);// 强制把文件缓存刷到 SD 卡物理介质}f_close(&fil);// 正常关闭也触发一次 flushreturn0;}

f_sync()f_close()的区别:f_sync()刷完缓存后文件仍然处于打开状态,下次f_write继续往后面追;f_close()刷缓存并释放文件句柄。高频写日志的场景下反复f_open/f_close开销大且容易耗尽文件描述符,保持文件打开、定期f_sync()是正确做法。

簇对齐写入提速

FatFS 默认的f_write内部会处理跨簇边界的情况:当前簇空间不够了就分配新簇、更新 FAT 表、可能还要读旧簇内容做合并。如果你每次写的长度恰好对齐到簇边界(比如簇大小 4096 字节,每次写 4096 的整数倍),这些额外开销全部消失,写入速度能快两到三倍。

// 高速批量写: 先攒够一簇再一次性 f_write#defineCLUSTER_SIZE4096staticuint8_tcluster_buf[CLUSTER_SIZE];staticuint32_tcluster_offset=0;voidlog_batch_add(constchar*line,uint16_tlen){if(cluster_offset+len>CLUSTER_SIZE){// 当前簇满了, 刷出去flush_cluster();}memcpy(cluster_buf+cluster_offset,line,len);cluster_offset+=len;}voidflush_cluster(void){if(cluster_offset==0)return;FRESULT fr=f_open(&fil,"0:/LOG_BATCH.csv",FA_WRITE|FA_OPEN_ALWAYS|FA_OPEN_APPEND);if(fr!=FR_OK){cluster_offset=0;return;}UINT written;f_write(&fil,cluster_buf,cluster_offset,&written);f_sync(&fil);f_close(&fil);cluster_offset=0;}

10 个真实翻车点

1. 断电没调 f_sync,日志全丢。我踩过的坑,也是新手第一坑。f_write只是把数据写进了 FatFS 在 RAM 里的文件缓存,不碰物理介质。突然断电,缓存里的东西跟着电压一起消失。高频写数据的场景必须定期f_sync(),频率取决于你能接受的最大丢失量(每条都 sync 最安全但最慢,每 10 到 50 条一次是常见折中)。

2. f_open 没检查返回值,后续操作全是废动作。SD 卡没插好、卡坏了、文件系统损坏、路径不存在,f_open会返回错误码。你不检查返回值直接f_write,写到一个无效的文件对象上,要么静默失败要么 HardFault。每个f_open后面跟一个if (fr != FR_OK)分支。

3. SPI 模式初始化时钟太快,卡不响应。SD 卡规范要求初始化阶段(CMD0 到 ACMD41 握手完成之前)时钟不得超过 400kHz。你一上来就给 SPI 配 18MHz,卡根本不认你。正确做法:先低速发 CMD0 复位,收到正确的 R1 响应后再提频。CubeMX 的 SDIO 模板一般自动处理了这个流程,但 SPI 模式你得自己管。

4. 写入没对齐簇边界,速度慢三倍。你每次f_write写 30 字节的一行 CSV,FatFS 内部要做"读当前簇尾部 -> 合并新数据 -> 写回整簇"的操作,频繁跨簇时还要更新 FAT 表。攒够一整簇(比如 4096 字节)再一次性写,这些开销全消掉。实测 30 字节逐行写大约 50KB/s,4KB 对齐写能到 150KB/s 以上。

5. 开了长文件名但堆不够,f_open 直接返回 FR_NOT_ENOUGH_CORE。FF_USE_LFN > 0 时每个文件对象需要额外的堆空间来存长文件名转换缓冲区。你堆才给了 2KB,开几个文件就爆了。要么关掉长文件名用 8.3 格式,要么加大_HEAP_SIZE(链接脚本或启动文件里改)。

6. 反复 f_open 不 f_close,文件句柄耗尽。FatFS 默认的_FS_LOCK定义了同时打开的文件数上限(通常是 8 左右)。你在循环里每次f_open都不f_close,跑到第 N 次返回FR_TOO_MANY_OPEN_FILES。日志场景保持一个文件常开更好(见上面的log_temperature做法)。

7. 写的过程中拔卡,文件系统损坏。f_sync()正在往 SD 卡写 FAT 表的时候用户把卡拔了,轻则丢几条记录,重则整个分区变成 RAW 格式(FAT 表损坏)。硬件上加卡检测引脚(CD),软件里在检测到卡移除后立刻停止一切文件操作并f_mount(NULL, ...)卸载。

8. FatFS 默认不是线程安全的。你在主循环写日志,中断回调里也想读配置文件,两个地方同时调 FatFS API,文件系统状态会被搞乱。开启_FS_REENTRANT并提供一个互斥锁(osMutex 或简单的开关中断),或者在架构上约定"所有文件操作只在主循环执行"。

9. 扇区缓冲区定义在栈上,栈溢出。FatFS 的底层 disk I/O 函数需要一个扇区大小的缓冲区(通常 512 字节)。你把它定义为局部变量放在 ISR 或者深层函数调用里,栈帧叠加起来超了。把它定义成全局静态数组或放到外部 SRAM 里。

10. SD 卡本身寿命到了,突然变砖。SD 卡内部的 NAND Flash 有擦写寿命(MLC 大约 3000 到 10000 次 P/E 循环)。你每秒写一次日志(每次修改 FAT 表 + 数据簇),一天就是 86400 次擦写操作,高强度使用下一两年就可能坏。做法:增大写入批次减少 FAT 表更新频率、选用工业级高 endurance 卡、或者考虑换 eMMC/NOR Flash 方案。

完整例程骨架

intmain(void){HAL_Init();SystemClock_Config();MX_GPIO_Init();MX_SDIO_SD_Init();MX_FATFS_Init();MX_USART1_UART_Init();if(sd_card_init()!=0){printf("SD card mount failed\r\n");// 降级运行: 只显示, 不记录}while(1){floatt=read_temperature();floatsp=get_setpoint();display_temp(t,sp);if(sd_mounted&&tick_1s){log_temperature(t,sp);// 内部含 f_synctick_1s=0;}HAL_Delay(100);}}

硬件清单:普通 microSD 卡一张(Class 10 以上保证写入速度)、SD 卡槽带 CD 引脚(最好还带 WP 引脚)、去耦电容 100nF 靠近卡槽供电引脚。走线尽量短,SDIO 高频信号线等长匹配更好(48MHz 下要求没那么严,但别绕大弯)。

适合:需要持久化存储任意容量数据的产品(温度/压力/振动记录仪、事件日志、固件升级包暂存、多媒体文件播放);也适合需要通过 PC 直接读取数据(拔卡插电脑就能看到 CSV/TXT 文件)的场景。不适合:只需存几十字节参数(片上 Flash 更可靠,没有机械接触风险);也不适合极端振动环境(SD 卡触点可能瞬断,那种该用焊接式 eMMC 或 NOR Flash)。铁律四条:写数据后必须定期 f_sync 防断电丢数据、f_open 必须查返回值、SPI 模式初始化必须先低速后提速、高频小数据写入必须攒批对齐簇边界。这四条做到,SD 卡 + FatFS 就是一块插拔自如的大号硬盘。

文件系统这层抽象最骗人的地方在于它让你以为"写了就是存了"。其实f_write只是写进了 RAM 缓存,f_sync才是真正落盘的那一刻。记住这个区别,断电就不会再吓你了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/23 8:48:21

C++模板编程:从泛型思维到智能指针实现

1. 从“硬编码”到“泛型思维”:为什么我们需要C模板? 如果你写过一些C代码,尤其是处理过不同类型数据但逻辑几乎相同的函数,你大概率经历过这种痛苦:为了处理 int 和 double 两种类型的数据,你不得不写…

作者头像 李华
网站建设 2026/8/23 8:46:10

Commun. Biol.:新生儿大尺度脑网络中的动态结构-功能耦合

本篇文献发表在Communications Biology杂志。所发布内容旨在与大家分享学术新知,促进交流学习,版权归原作者或原出处所有,感谢各位学者的辛勤付出与研究成果。1.引言新生儿期是大脑发育的关键阶段,其特点是解剖结构的快速成熟和功…

作者头像 李华
网站建设 2026/8/23 8:44:10

2023美赛D题实战:基于DEA与随机规划的SDGs效率评估与资源分配模型

1. 项目概述:从赛题到实战的思维跃迁每年二月的那个周末,对于全球数以万计的数学建模爱好者而言,都是一场头脑风暴的盛宴——美国大学生数学建模竞赛(MCM/ICM)。2023年的D题,将聚光灯投向了联合国2030年可持…

作者头像 李华
网站建设 2026/8/23 8:43:54

求职材料降AI率工具实测与优化策略

1. 项目背景与核心价值最近在辅导2025届应届生求职时,发现一个有趣现象:超过80%的同学都在使用各种"降AI率"工具来优化简历和求职材料。这引发了我的好奇——这些号称能降低AI识别率、提升人工筛选通过率的网站,实际效果究竟如何&a…

作者头像 李华
网站建设 2026/8/23 8:43:05

基于TVA的具身智能因果推理与反事实想象研究

前沿技术探索:TVA智能体(简称TVA)TVA智能体(亦称“AI智能体视觉”或“TVA视觉智能体”)是依托Transformer架构与“因式智能体”理论构建的系统级视觉技术框架。它融合深度强化学习(DRL)、卷积神…

作者头像 李华
网站建设 2026/8/23 8:39:12

从零转型AI大模型工程师:四阶段学习法与面试攻略

1. 从传统行业到AI大模型的转型之路 去年这个时候,我还在某传统行业做着重复性工作,每天面对Excel表格和PPT报告。直到偶然接触到ChatGPT,那种震撼感至今记忆犹新——原来机器已经能如此自然地理解人类语言。这个发现彻底改变了我的职业轨迹&…

作者头像 李华