简介:面向使用STM32H7系列单片机的嵌入式开发者,这份源码包以STM32H750为例,采用寄存器库直接驱动硬件的方案,实现了FATFS文件管理系统,适用于需要将运行数据、配置参数或日志记录到SD卡等存储介质的工业控制、数据采集与物联网设备。压缩包合计有208个文件,主要组成是C语言源文件和H头文件,另包含PNG格式原理图或效果图、TXT说明文档、MDK工程配置文件和HEX固件,整体大小为3.24MB。资源中不仅提供FATFS核心源码模块,还整合了SD卡驱动、LCD显示驱动、NAND存储驱动、数据打包模块以及IMU传感器驱动,并附带了数学运算库支持,可以从中学习到寄存器级别下的外设初始化与读写时序控制。结合源码中的DMA与中断服务程序,可以梳理出高效数据传输和异常处理的设计框架,方便将整个方案快速迁移到同系列其他具体型号。目前已有247人学习下载,适合正在做H7平台底层驱动开发或希望加深对FATFS文件系统移植理解的工程师作为参考。
1. 从 SD 卡到文件系统:STM32H750 为什么绕不开 FATFS
把 STM32H750 当裸机跑的人,迟早会遇到一个尴尬场景:程序里采集的数据量一大,内部 Flash 装不下,调试日志也没地方落。加一颗 SD 卡、接上 SPI/SDMMC 接口之后,下一个问题就是——数据怎么组织?如果只是往扇区里裸写,断电一次、文件错位一次,整个存储就成了乱码堆。FATFS 的价值就在这里:它把扇区读写抽象成文件操作,让单片机上的数据能以 PC 可读的方式落盘,拷出来直接能在电脑上打开。
STM32H750 这颗芯片有点特殊,内部 Flash 只有 128KB,但主频拉到 480MHz,外设资源非常充足。很多人拿它做 UI、做采集、做协议网关,数据量一大就必然要挂存储。网上能搜到的方案里,最常见的组合就是 STM32H750 + 寄存器库驱动 + FATFS,原因很简单:HAL 库生成的工程体积大、底层封得厚,而寄存器库驱动的代码可以直接看到寄存器操作,出问题好排查,Flash 占用也更可控。这个压缩包之所以叫「支持 STM32H7系列_寄存器库驱动」,本质上是把 FATFS 的底层接口落到 H7 的 SDMMC 和 SPI 外设上,让文件系统跑起来不依赖 HAL 那套抽象。
这篇文章要解决的四个问题很具体:FATFS 在 H7 上挂载 SD 卡怎么选物理接口、寄存器库驱动怎么把 disk_initialize 和 disk_read/write 填上、路径和簇大小怎么配才不卡死、掉电和长文件名这些坑怎么绕。下面从接口选型开始讲。
2. 前置选型和 FATFS 移植的关键:接口怎么接、配置项怎么开
2.1 SDMMC 与 SPI:两条路的取舍,直接决定驱动怎么写
STM32H750 的 SD 卡接口有两条路:一是片上 SDMMC 控制器(SDMMC1/SDMMC2,支持 1 位和 4 位总线),二是把 SD 卡当 SPI 设备挂在 SPI 外设上。两者的差异非常明显。
SDMMC 走 4 位总线时,理论带宽能做到几十 MB/s,适合连续写日志、批量存采集数据;代价是引脚占用多(CLK、CMD、D0-D3 共 6 根),而且 SDMMC 的时序对 PCB 布线有一定要求,飞线场景下高速模式容易不稳定。SPI 方式只有 4 根线(CS、SCLK、MOSI、MISO),接线简单,任何空闲的 SPI 外设都能用,但速度上限大概在 2MB/s 左右,而且 SD 卡在 SPI 模式下有些命令行为不标准,初始化时序要额外兼容。
从实践角度看,如果只是存参数、存配置、偶尔读写小文件,SPI 完全够用,驱动也更好调试——毕竟 SPI 的收发是同步的,逻辑简单。如果要连续存传感器数据或者做音频播放,就老老实实上 SDMMC 4 位模式。很多网上的工程默认走 SDMMC,但寄存器版本的驱动在 SPI 下其实更好理解,因为每次命令交互你都能看到完整的字节流。
FATFS 对这两种接口的差异是感知不到的,它只关心底层提供的 disk_status、disk_initialize、disk_read、disk_write、disk_ioctl 这几个函数能不能正常工作。所以接口选型的本质,是决定你在 disk_read 里面是去读 SDMMC 的 FIFO,还是去操作 SPI 收发。
2.2 ffconf.h 的必调开关:把不需要的功能全部关掉
FATFS 源码拿到手,第一步不是去改 ff.c,而是把 ffconf.h 从头到尾过一遍。这个文件里的宏直接决定代码体积和内存占用,对于 STM32H750 这种内部 Flash 只有 128KB 的芯片来说尤其关键。
常见的必调项有这么几个。_FS_READONLY必须设为 0,文件系统要写不能只读。_USE_MKFS建议设为 1,这样目标板可以不依赖电脑,直接在 SD 卡上格式化出 FAT 文件系统。_USE_LFN是长文件名开关,设为 2 表示启用 LFN 且缓冲区在堆栈上分配,但注意这个选项会显著增加 RAM 占用;如果只是存日志文件,文件名的 ASCII 短名够用,可以直接设为 0。_MAX_SS决定扇区缓冲大小,SD 卡基本都是 512 字节扇区,设 512 即可,不要设成 4096,否则 FATFS 内部缓冲区多占 3.5KB RAM。_VOLUMES只挂一张卡就设 1。
还有一个很容易忽略的宏是_FS_MINIMIZE。它控制 FATFS 暴露哪些 API,如果只用到 f_open、f_read、f_write、f_close、f_mount、f_mkfs,把这个宏设为 2 或 3,可以把 f_opendir、f_stat 这些不用的目录操作裁掉,Flash 能省下几 KB。对于寄存器库驱动的工程来说,省下的空间可以多放几个中文字库或者多几层 UI 缓存。
2.3 寄存器库驱动的挂载点:diskio.c 里要填的函数签名
FATFS 和底层存储之间的桥梁是 diskio.c 文件。很多移植失败的原因,不是文件系统本身的问题,而是底层这几个函数没有按 FATFS 期望的语义实现。先看接口原型:
DSTATUS disk_initialize(BYTE pdrv); DSTATUS disk_status(BYTE pdrv); DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count); DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count); DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff);pdrv是卷号,单卡工程始终为 0,但函数签名不能省。disk_initialize负责初始化底层外设并发送 SD 卡的初始化命令序列;disk_read的语义是「从 sector 开始的连续 count 个扇区读到 buff」,注意 count 可以大于 1,驱动里必须用循环或者支持多块读的命令来处理;disk_write同理。disk_ioctl里至少要处理CTRL_SYNC和GET_SECTOR_SIZE两个命令,前者在 f_sync 和 f_close 时被调用,后者返回 512。
寄存器库驱动写这几个函数时,最常见的做法是直接操作 SDMMC 的寄存器组。H7 的 SDMMC 外设寄存器包括SDMMC_POWER、SDMMC_CLKCR、SDMMC_ARG、SDMMC_CMD、SDMMC_RESPx、SDMMC_DTIMER、SDMMC_DLEN、SDMMC_DCTRL、SDMMC_DCNTR、SDMMC_STATUS等,发送一条命令的基本流程是:等待上一条命令完成、写参数寄存器、写命令寄存器、查状态寄存器确认响应。数据收发则靠 DMA 或轮询 FIFO。
2.4 挂载失败时的定位手段:从返回值反推问题层
f_mount 返回错误码时,不要急着改 ffconf,先按层来定位。常见的返回值有三个:FR_DISK_ERR、FR_NOT_READY、FR_NO_FILESYSTEM。
FR_NOT_READY说明 disk_initialize 返回了非 0,SD 卡没有初始化成功。这时候直接调 disk_initialize 看返回码,再逐条检查 CMD0、CMD8、CMD55、ACMD41 的响应是否符合规范。FR_NO_FILESYSTEM说明卡初始化成功了,但 FATFS 在 0 扇区没找到合法的 BPB 结构,用 f_mkfs 格式化一遍就能解决。FR_DISK_ERR比较麻烦,往往是读多块或写多块时底层报错,重点检查 disk_read 的 count 参数是不是大于 1、驱动是否支持 multi-block 命令。
一个很实用的调试技巧,是把 disk_initialize 里每条命令的响应打印出来。CMD0 在 SDMMC 模式下应该返回 0x01(idle),CMD8 返回 0x01 加 4 字节的 check pattern,ACMD41 最终返回 0x00。哪一步响应不对,就说明对应时序有问题。下面给出一个标准的 SDMMC 初始化命令序列对照表:
| 命令 | 参数 | 预期响应 | 说明 |
|---|---|---|---|
| CMD0 | 0x00000000 | 0x01 | 进入 Idle 状态 |
| CMD8 | 0x000001AA | 0x01 0x00 0x00 0x01 0xAA | 确认 SDV2 支持,0x1AA 是电压范围 |
| CMD55 | 0x00000000 | 0x01 | 告诉卡下一条是应用命令 |
| ACMD41 | 0x40000000 | 0x00 | 上电完成,HCS 位置 1 表示支持高容量卡 |
| CMD2 | 0x00000000 | 0x00 + 16 字节 CID | 获取卡片标识 |
| CMD3 | 0x00000000 | 0x00 + 4 字节 RCA | 获取相对地址 |
| CMD7 | RCA 值 | 0x00 | 选中卡 |
3. diskio 层完整实现:把寄存器驱动落到 F 系列 API 的最后一个环节
3.1 disk_status 和 disk_initialize:寄存器位怎么映射成状态码
disk_status 的作用是告诉 FATFS 底层是否就绪。寄存器库实现时,通常把「卡是否插入、是否初始化过」用一个静态变量记录,同时可以读 SDMMC 的电源状态寄存器做辅助判断。一个简单可靠的实现是:
static volatile uint8_t sd_initialized = 0; DSTATUS disk_status(BYTE pdrv) { if (pdrv) return STA_NOINIT; if (!sd_initialized) return STA_NOINIT; return 0; } DSTATUS disk_initialize(BYTE pdrv) { if (pdrv) return STA_NOINIT; if (sd_initialized) return 0; sdmmc_power_on(); // 打开 SDMMC 时钟并拉高 POWER 寄存器 if (sdmmc_card_init() != 0) { return STA_NOINIT; // 卡初始化失败,保持未初始化状态 } sd_initialized = 1; return 0; }这段代码的逻辑很直白:sdmmc_power_on配置 SDMMC 外设的时钟分频和电源位,sdmmc_card_init按 2.4 节的命令序列完成卡初始化。寄存器库里的sdmmc_card_init是核心函数,逐条命令构造SDMMC_CMD寄存器的CMDINDEX字段,然后轮询SDMMC_STATUS的CPSMACT位等待命令完成,读SDMMC_RESPCMD确认响应命令号,再读SDMMC_RESP1拿响应内容。
需要注意的是,H7 的 SDMMC 在发送命令前必须配置SDMMC_CLKCR的分频值。初始化阶段时钟要低,建议 400kHz 左右;初始化完成后可以切换到 25MHz 甚至 50MHz。寄存器操作用一个SDMMC->CLKCR = (SDMMC->CLKCR & ~0xFF) | clk_div就能改分频。
3.2 disk_read:单块和多块读取的寄存器流程
disk_read 是实现中最容易出问题的地方,因为 FATFS 会以不同长度调用它。读取一个文件可能只读 1 个扇区,也可能一次读 64 个扇区。如果驱动只支持单块读,FATFS 内部会自己拆成多次调用,性能会下降,但 correctness 没问题。如果想要多块读,必须正确设置DCTRL寄存器的RWMOD和DMAEN位。
单块读的寄存器流程分成五步:
DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { if (pdrv || !sd_initialized) return RES_NOTRDY; if (!count) return RES_PARERR; sdmmc_set_clock(SDMMC_CLK_25MHz); // 高速读取前切换时钟 if (count == 1) { sdmmc_read_single_block(sector, buff); } else { sdmmc_read_multi_block(sector, buff, count); } return RES_OK; }sdmmc_read_single_block的寄存器序列是:向SDMMC_DTIMER写超时值,向SDMMC_DLEN写 512,向SDMMC_DCTRL配置数据方向为读、单块传输,向SDMMC_ARG写扇区地址,向SDMMC_CMD写 CMD17 的命令号。之后轮询SDMMC_STATUS的RXOVERR(接收溢出)和DCRCFAIL(CRC 错误)位,再从SDMMC_FIFO读 128 次 32 位数据(512 字节正好 128 个字)。
多块读使用 CMD18,逻辑上只是把DCTRL的传输模式改为多块,DLEN写count * 512,其余流程一致。这里的实时性风险在于:DMA 模式下如果 FIFO 溢出,RXOVERR位会置 1,必须重置数据路径并重试。
3.3 disk_write:从 cache 同步到真实扇区的细节
写操作比读操作多一个坑:SD 卡的写是先写内部缓存再刷入 NAND,所以发出写命令后不能立刻认为完成。CMD24(单块写)或 CMD25(多块写)返回响应后,还要轮询卡的状态,直到卡不再 busy。
DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) { if (pdrv || !sd_initialized) return RES_NOTRDY; if (!count) return RES_PARERR; if (count == 1) { sdmmc_write_single_block(sector, buff); } else { sdmmc_write_multi_block(sector, buff, count); } // 等待卡内部完成写入 while (sdmmc_card_busy()) ; return RES_OK; }sdmmc_card_busy的实现有两种方式:读 SDMMC 的STATUS寄存器的DPSMACT位判断数据通路是否空闲,或者发 CMD13 读卡状态寄存器,检查 bit 8 的状态位。后者更可靠,但会增加一次命令交互的时间。寄存器库方案里一般用前者,因为一次寄存器读比一条 SD 命令快得多。
很多人在 disk_write 里漏了while (sdmmc_card_busy())这个循环,结果写完文件后马上断电,文件显示存在但内容为空。原因就是卡的内部缓存还没有真正落到 NAND,FATFS 的 f_write 返回了,但物理写入还没结束。
3.4 disk_ioctl:让 f_mkfs 和 f_sync 跑通的最小实现
disk_ioctl不需要实现很多命令,但有两个必须有。CTRL_SYNC在 f_sync、f_close 时被调用,作用是确保所有未完成的写操作已经物理完成;GET_SECTOR_SIZE在 f_mkfs 时被调用,返回 512。还有一个GET_BLOCK_SIZE是擦除块大小,SD 卡通常返回 1 表示没有特殊的擦除对齐要求。
DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { if (pdrv || !sd_initialized) return RES_NOTRDY; switch (cmd) { case CTRL_SYNC: if (!sdmmc_card_busy()) return RES_OK; break; case GET_SECTOR_SIZE: *(WORD *)buff = 512; return RES_OK; case GET_BLOCK_SIZE: *(DWORD *)buff = 1; return RES_OK; case GET_SD_CARD_INFO: sdmmc_get_card_info((SD_CARD_INFO *)buff); return RES_OK; } return RES_PARERR; }GET_SD_CARD_INFO是扩展命令,不是 FATFS 标准的一部分,但项目里经常用。它可以返回卡的容量、是否 SDHC、RCA 等信息,调试时打印出来能看到卡被识别成了什么类型。这里有个容易忽略的问题:GET_SECTOR_SIZE的 buff 类型是WORD*,不是DWORD*,写错类型会导致高字节未初始化,f_mkfs 计算出错误的扇区数。
提示:diskio.c 里不要对 FATFS 传入的 buff 做局部缓存再拷贝。直接使用传入指针,因为 FATFS 层已经做了内存对齐。额外的拷贝会把原本就紧张的 RAM 吃得更紧。
4. 挂载与文件操作实战:从 f_mount 到日志落盘
4.1 f_mount 和 f_mkfs 的最小可运行代码
底层四个函数填完之后,应用层的挂载和格式化就变得很简单。f_mount 的工作是注册一个 FATFS 对象并挂载卷,f_mkfs 是格式化。注意 H7 的 RAM 其中一部分用于 FatFs 的工作区,FatFS 对象本身约 550 字节,再算上文件对象的FIL结构体约 550 字节,这些都要在启动时分配好。
FATFS fs; // FatFS 文件系统对象 FIL file; // 文件对象 UINT bw; // 实际写入字节数 void storage_init(void) { FRESULT res; res = f_mount(&fs, "", 1); // 立即挂载,根路径为空字符串 if (res == FR_NO_FILESYSTEM) { // 卡上还没有有效文件系统,先格式化 res = f_mkfs("", FM_FAT | FM_SFD, 0, work_buf, sizeof(work_buf)); if (res != FR_OK) { // 格式化失败,检查 disk_ioctl 的 GET_SECTOR_SIZE return; } res = f_mount(&fs, "", 1); // 格式化后重新挂载 } if (res != FR_OK) { // 挂载失败,打印 res 值定位 return; } }f_mkfs的第三个参数传 0 表示由 FATFS 自动决定分区方案,work_buf是一个至少 512 字节的临时缓冲区,格式化的过程中 FATFS 会反复使用它。这个缓冲区建议定义成static BYTE work_buf[512],不要放栈上,因为有些优化选项下栈深度不够。
挂载成功后的第一步操作,建议不要急着写文件,先做个连通性验证:用f_getfree获取剩余扇区数并打印容量,确认文件系统的元数据能正常读到。这一步能区分两种问题——挂载失败是底层驱动问题还是卡的问题。
4.2 文件写入的缓存机制:f_write 后不要马上断电
FATFS 的 f_write 是把数据写到内部扇区缓冲里,等缓冲区满了或者收到 f_sync/f_close 时才真正调 disk_write。因此,关键数据必须显式调用 f_sync,否则断电就是数据丢失。
res = f_open(&file, "0:/data/log.txt", FA_CREATE_ALWAYS | FA_WRITE); if (res != FR_OK) return; for (int i = 0; i < 100; i++) { char buf[64]; int len = snprintf(buf, sizeof(buf), "sample:%d,temp:%d\r\n", i, i * 3); res = f_write(&file, buf, len, &bw); if (res != FR_OK || bw != len) break; } res = f_sync(&file); // 手动 flush 到 SD 卡 f_close(&file);这里的路径"0:/data/log.txt"中,0:是卷号。f_mount 挂载时卷号字符串传了空,FATFS 默认逻辑盘号从 0 开始,所以写文件时用0:前缀是安全的。如果 SD 卡里没有 data 目录,这个 open 会失败,需要先用 f_mkdir 创建。
另一个实际项目里常踩的坑是:f_write 返回值是FR_OK,但bw(写入字节数)小于len。这种情况在 SD 卡写满或底层写错误时出现,必须检查 bw 和 len 是否一致,不能只靠返回值判断成败。日志场景还有一个注意点:不要在循环里频繁 open/close 文件,每次 open 都会更新目录项和 FAT 表,增加写入放大。正确做法是 long-running 写文件,定期 f_sync。
4.3 日志轮转:按大小拆分文件,避免单文件过大
FATFS 对单个文件的大小没有严格限制(FAT32 上限 4GB),但实际应用里日志文件超过几 MB 后,打开和追加的效率会明显下降。原因是需要遍历 FAT 链找到末尾簇。所以常见做法是文件达到设定大小后,关闭当前文件,以新名字重新创建。
uint32_t file_size = f_size(&file); // 当前文件大小 if (file_size >= MAX_LOG_SIZE) { f_close(&file); // 生成新的文件名 static uint32_t seq = 0; char name[32]; snprintf(name, sizeof(name), "0:/data/log_%03d.txt", seq++); res = f_open(&file, name, FA_CREATE_ALWAYS | FA_WRITE); if (res != FR_OK) { // 打开失败时回落到旧文件继续写 f_open(&file, "0:/data/log.txt", FA_OPEN_APPEND | FA_WRITE); } }这里的 seq 如果不想每次开机都从 0 开始,可以把序列号存到一个小的配置文件中,open 时读出来,写入时加一。FATFS 没有内置的「追加模式」标志,FA_OPEN_APPEND表示打开后文件指针移到末尾,注意它和FA_OPEN_ALWAYS的区别——前者每次 open 都定位到末尾,后者保留原指针。
提示:
f_size返回的是文件的实际大小,单位为字节。如果要基于簇大小判断轮转,可以在 f_mkfs 时记录每簇扇区数,然后用f_size / (bytes_per_sector * sectors_per_cluster)得到簇数。
5. 寄存器库驱动的性能与可靠性:时钟分频、DMA 和掉电保护
5.1 时钟树配置对 SDMMC 时钟的影响:分频不对导致卡初始化失败
H7 的 SDMMC 时钟源是PLL1Q或PLL2R,在做寄存器库驱动时,最容易忽略的就是RCC里 SDMMC 时钟的分频配置。如果 SDMMC 时钟跑到 200MHz 而 CLKCR 的分频系数没跟上,卡直接不响应。
系统时钟 480MHz 时,常见配置是 SDMMC 时钟源 200MHz,CLKCR分频系数设为 8,得到 25MHz 的卡时钟。初始化阶段需要 400kHz,分频系数要设 250 左右。所以sdmmc_set_clock函数不能只改 CLKCR,还要确认 RCC 的D2CCIP2R寄存器里 SDMMC 的时钟源选择是否正确。
void sdmmc_set_clock(uint32_t clk) { uint32_t sdmmc_clk = 200000000; // PLL 输出 200MHz uint32_t clkcr = SDMMC->CLKCR; clkcr &= ~(0xFF << 8); // 清掉保留位影响 clkcr &= ~0xFF; // 清分频值 clkcr |= (sdmmc_clk / clk / 2) - 1; // H7 的分频是 2 * (N+1) SDMMC->CLKCR = clkcr; // 等待时钟稳定 while (SDMMC->CLKCR != clkcr) ; }H7 的 SDMMC 分频计算方式和 F1/F4 不一样,F4 是直接分频,H7 是2 * (N+1),很多从 F4 移植过来的寄存器代码在这里算出错误的时钟。调试时可以用逻辑分析仪抓 CLK 引脚的频率,或者用示波器量一下 SDMMC_CK 的实际频率。
5.2 DMA 与轮询模式的选择:FIFO 溢出的典型场景
disk_read/disk_write 有两种数据搬运方式:CPU 轮询 FIFO 或者 DMA。寄存器库驱动里,DMA 配置稍显繁琐但收益很大。SDMMC 的 DMA 支持双缓冲区模式,可以在一个缓冲区传输完成时自动切换到另一个,切换中间 CPU 不会介入。
但 DMA 模式有一个典型的坑:如果 DMA 的描述符配置错误,DTErr位置位,数据路径停摆。排查方法是检查SDMMC_STATUS寄存器的DTAEND位和DBCKEND位。另一个坑是 DMA 的地址对齐要求——SDMMC 要求 DMA 地址按 4 字节对齐,而 FATFS 传入的 buff 通常已经对齐,但如果应用层的文件缓冲定义成结构体且没有做对齐声明,就可能踩到。
以下是一个判断当前工作模式的思路拿来做对照:如果项目是裸机单线程、数据量不大,轮询模式完全够用且逻辑简单;如果上了 RTOS 且写数据有实时性要求,DMA 加信号量同步更合适。轮询模式下,CPU 全程占用,480MHz 的主频读一个 512 字节扇区大约需要几十微秒,100 个扇区就是几毫秒,这个时间在 RTOS 里已经可能触发任务调度延迟。
5.3 掉电与数据一致性:FATFS 的 f_sync 到底保证了什么
SD 卡掉电损坏文件系统,是 FATFS 项目被诟病最多的一个问题。根源在于 FATFS 为了性能,把目录项和 FAT 表的更新延迟到了 f_sync 或 f_close。如果在 f_write 之后、f_sync 之前的窗口断电,FAT 表里记录的簇链和实际写入的数据不一致,轻则文件内容残缺,重则整个目录项损坏。
可行的保护策略分三层。最底层的是硬件方案:在 SD 卡电源端加一个大电容,掉电时让单片机有几毫秒时间把最后一笔数据 flush 出去。中间层是软件方案:关键数据每次写入后立即 f_sync,非关键数据批量后再 sync。最上层是容错方案:定期用 f_mkfs 重做文件系统不现实,更实用的做法是日志文件只存固定大小、固定文件名,每次开机时先删除文件再重新创建,这样即使上一次断电留下脏数据,本次开机也会重新开始。
另外要注意 FATFS 的_FS_LOCK配置。这个宏设为 0 时(默认),FATFS 不做任何文件锁保护,多个任务同时 open 同一个文件写,逻辑后果自行承担。如果项目用了 RTOS 且多个任务都会写日志,建议通过互斥量保护 f_open/f_write/f_sync 的整段流程。
6. 文件系统跑通的验证清单与最后的实用技巧
最后给一份可以直接对照的验证流程。不是所有步骤都要做,但做到位的项目,后续出问题的概率会小很多。
- 先用
disk_initialize单步调试,打印 CMD0、CMD8、CMD55、ACMD41 的响应值。响应不对就先调时钟和电压配置,不要碰 FATFS。 - 用
f_mount+f_getfree确认容量读取正确。如果 f_getfree 返回 0 或错误,优先检查disk_ioctl里的GET_SECTOR_SIZE。 - 连续写入 1000 个 512 字节数据块,写入后用
f_read读回校验。读回不一致说明多块读写的寄存器配置有问题。 - 写入完成后调用
f_mkfs重新格式化,验证格式化流程能正常走通。如果 f_mkfs 卡死,检查disk_write里的 busy 等待循环。 - 拔卡-插卡测试:插入新卡(未格式化),确认可以 f_mount 后自动 f_mkfs。这一条验证了板子独立工作的能力。
调试过程中最有效的辅助手段,是在 f_open、f_write、f_mount 的返回值处各放一个断点或串口打印。FATFS 的错误码是枚举值,FR_OK=0,FR_DISK_ERR=1,FR_NOT_READY=3,FR_NO_FILESYSTEM=13,看到数值就能直接对应到层。寄存器的 SDMMC 部分,常用调试寄存器是SDMMC_STATUS和SDMMC_RESP1,把这两个值打印出来,和 2.4 节的预期响应表对比,就能缩小到命令级问题。
最后分享一个实用技巧,针对 STM32H750 的 128KB Flash。如果项目里 FATFS 和 SDMMC 驱动编出来超过 40KB,需要检查是否开了所有 FATFS 功能。长文件名支持、f_printf、f_mkfs 这些功能其实都能按需裁剪。一个只支持 FAT32 短文件名读写、含 SDMMC 驱动的工程,Flash 占用控制在 20KB 以内是正常的。压缩包里带的工程如果编译后偏大,重点看_USE_LFN和_FS_MINIMIZE这两个宏,调完代码体积立竿见影。
本文还有配套的精品资源,点击获取