简介:面向嵌入式学习者与需在STM32上实现存储读写的开发者,这份资料展示KEIL环境下通过SPI或SDIO驱动SD卡并挂载FATFS的完整思路,涉及引脚配置、扇区读写、FATFS裁剪及f_open、f_read等文件操作,也包含未检测到卡、文件不存在、读写异常的处理参考,可用于数据记录与日志存储等项目入门。压缩包共229个文件,主体为83个C源文件、83个头文件和36个汇编/启动文件,另配工程配置、说明文档及网页资料,整体约1.48MB,便于对照KEIL工程梳理源码结构。已有3377人学习下载,适合作为可编译的SD卡读写样例,并在此基础上扩展文件管理与实际应用。
1. 一张卡插上去能挂载成功,文件却打不开:问题从来不在 SPI 时序
用 STM32 读 SD 卡上的 FATFS 文件系统,绝大多数人遇到的第一个坎不是 SPI 波形不对,而是f_mount返回FR_OK、f_open却返回FR_NO_FILESYSTEM。波形是对的,卡也是好的,程序却读不出来东西。这说明 SD 卡读取这条链路上,真正的难点不是“把电平拉高拉低”,而是把一块只能按扇区读写的裸存储,翻译成一套有目录、有簇链、有文件长度的语义。FATFS 就是这层翻译器,diskio.c是你和它之间的唯一接缝。搞懂这条链上每一层各自的职责边界,比调通某个寄存器重要得多。
2. SD 卡的块设备约定:为什么初始化序列比读写本身更容易翻车
SD 卡在 SPI 模式下不是一个“插上就能读”的器件。它从上电到进入可读写状态,中间要走完一套状态机迁移:idle → ready → transfer。每一步迁移都由特定命令触发,发错一条、少等一拍时钟,卡就会安静地停在某个中间态,后续所有读写都返回错误,而你的代码看起来毫无问题。这就是为什么很多人第一次接 SD 卡时,逻辑分析仪上的波形干干净净,读出来却全是 0xFF。
2.1 上电那 74 个时钟和三条命令在协商什么
SPI 模式下的 SD 卡有一个硬性要求:主机必须先给它至少 74 个时钟周期,此时 CS 拉高、MOSI 保持高电平。这 74 个时钟不是“随便发几个 0xFF 凑数”,它是卡内部上电复位和电荷泵稳定的时间窗口。少发或者频率太高,卡可能根本没完成内部初始化,后面发 CMD0 也不会有响应。
初始化阶段的时钟频率必须压到 400kHz 以下。这个限制来自卡内部的逻辑在低压下的建立时间,不是保守估计。很多“初始化偶发失败、重新上电又好了”的现场问题,根源就是初始化阶段跑在了 8MHz 甚至更高。
三条关键命令的作用要分清楚:
- CMD0(GO_IDLE_STATE):把卡强制拉回 idle 态,是所有初始化的起点,只有这条命令的 CRC 是硬性的
0x95。 - CMD8(SEND_IF_COND):探测卡是否支持 2.0 协议。参数里带上电压范围和校验图样,支持的话卡会原样回显这 8 位。不支持的卡会返回非法命令标志,这时要按 v1 卡的老流程走。
- ACMD41(SD_SEND_OP_COND):真正让卡退出 idle 的命令。它不是直接发的,必须先发 CMD55 告诉卡“下一条是应用命令”,再发 ACMD41。参数里的 HCS 位表示主机是否支持大容量卡。
下面这段是初始化骨架,重点看循环和返回值判断,而不是命令表本身:
/* 返回值:0 成功,1 超时,2 卡类型异常 */ uint8_t sd_init(void) { uint8_t r1, buf[4]; uint32_t retry = 0; CS_HIGH(); for (uint8_t i = 0; i < 10; i++) spi_xfer(0xFF); /* 至少 74 个时钟 */ CS_LOW(); r1 = sd_cmd(0, 0, 0x95); /* CMD0 必须带正确 CRC */ CS_HIGH(); spi_xfer(0xFF); if (r1 != 0x01) return 2; /* 没进 idle 态,卡没起来 */ CS_LOW(); r1 = sd_cmd(8, 0x1AA, 0x87); /* CMD8 探测 2.0 协议 */ if (r1 == 0x01) { spi_read(buf, 4); /* R7 响应,后 4 字节是回显 */ if ((buf[2] & 0x0F) != 0x01 || buf[3] != 0xAA) { CS_HIGH(); return 2; } } else if (r1 & 0x04) { /* 非法命令位,说明是 v1 老卡,走无 HCS 的流程 */ } CS_HIGH(); spi_xfer(0xFF); do { /* ACMD41 可能要多轮 */ CS_LOW(); sd_cmd(55, 0, 0x01); CS_HIGH(); spi_xfer(0xFF); CS_LOW(); r1 = sd_cmd(41, 0x40000000, 0x01); /* HCS=1,支持大容量 */ CS_HIGH(); spi_xfer(0xFF); } while (r1 != 0x00 && ++retry < 0xFFFF); if (r1 != 0x00) return 1; /* 超时,卡没退出 idle */ CS_LOW(); /* CMD58 读 OCR,拿 CCS 位 */ r1 = sd_cmd(58, 0, 0x01); spi_read(buf, 4); CS_HIGH(); spi_xfer(0xFF); card_high_capacity = (buf[0] & 0x40) ? 1 : 0; /* bit30 = CCS */ CS_LOW(); /* 块长固定 512 */ sd_cmd(16, 512, 0x01); CS_HIGH(); spi_xfer(0xFF); spi_set_speed(FAST); /* 到这里才允许提频 */ return 0; }这里有个容易被忽略的细节:card_high_capacity这个标志必须存成全局状态,后面每一次 CMD17、CMD18、CMD24 都要看它决定参数怎么算。它不是一个初始化完成就没用的临时变量,而是贯穿整个生命周期的地址解释方式。
2.2 字节地址与块地址:一个参数差异引发的全部乱码
SD 卡分两代寻址规则。老卡(SDSC,容量通常 2GB 以下)的读写命令参数是字节地址,读第 100 个扇区要传100 * 512 = 51200;新卡(SDHC/SDXC)的参数直接就是块号,读第 100 个扇区传100。CMD58 回读的 OCR 寄存器第 30 位(CCS)就是这个规则的分水岭。
这个差异的隐蔽之处在于:f_mount大概率能成功。因为挂载时只读第 0 个扇区,字节地址 0 和块地址 0 在数值上完全一样。等到f_open去读 FAT 表、读目录项,地址大了才开始错位。表现出来就是挂载返回FR_OK,读文件得到一堆乱码,或者f_read读回来的内容和十六进制编辑器里看到的不一致。
常见的实现思路是在disk_read里做一次统一换算,把上层传下来的“逻辑扇区号”翻译成卡认识的参数:
/* 上层只认扇区号,这里负责翻译成卡能懂的地址 */ DRESULT disk_read(BYTE pdrv, BYTE *buff, DWORD sector, UINT count) { if (pdrv != 0 || count == 0) return RES_PARERR; for (UINT i = 0; i < count; i++) { uint32_t arg = card_high_capacity ? (sector + i) /* 块地址,直接给 */ : (sector + i) << 9;/* 字节地址,乘 512 */ if (sd_read_block(buff + i * 512, arg) != 0) return RES_ERROR; } return RES_OK; }sector是 FATFS 眼里的逻辑扇区号,512 字节为一单位,从卡的第 0 字节开始编号。count多次读时用的是逐块读而不是 CMD18 多块读,这是有意的取舍:单块读逻辑简单,出错重试容易定位;代价是多块连续读时每条命令都有额外开销,大文件吞吐会明显下降。真要提升读速度,就换成 CMD18 并在结尾发 CMD12 停止,但错误处理分支会复杂不少。
提示:SDHC 卡在 SPI 模式下必须用块地址,这一点没有兼容模式可切。判断依据只能是 CMD58 的 CCS 位,不能靠容量猜,2GB 边缘的卡两种都可能出现。
2.3 单块、多块与忙等待:写完为什么必须等 DAT0
读操作的时序相对宽松:发完 CMD17,等 R1 为 0,再等起始令牌0xFE,然后连续收 512 字节数据加 2 字节 CRC。CRC 在 SPI 模式下可以丢弃,但必须读走,否则后续字节全部错位一格。这个“错位一格”的症状特别有欺骗性——数据看起来像数据,只是每个字段都偏了。
写操作才是容易出问题的地方。发 CMD24 之后,要先发一个0xFE起始令牌,再发 512 字节数据,最后两个字节 CRC。数据发完并不代表写完,卡内部还在往闪存里编程,这段时间 DAT0 会被拉低表示忙。如果这时候主机又发下一条命令,卡会直接忽略,结果就是“写进去的数据莫名其妙少了一段”。
/* 写单块:发完数据必须等忙结束,否则下一条命令会被吞掉 */ int sd_write_block(const uint8_t *buf, uint32_t arg) { if (sd_cmd(24, arg, 0x01) != 0x00) return -1; spi_xfer(0xFE); /* 数据起始令牌 */ spi_write(buf, 512); spi_xfer(0xFF); spi_xfer(0xFF); /* 两个字节 CRC,SPI 模式下卡不校验 */ /* 等 DAT0 释放,最多等 250ms 量级 */ uint32_t t = 0; while (spi_xfer(0xFF) == 0x00) { if (++t > 0xFFFFFF) return -2; } return 0; }这段忙等待是“血泪经验”级别的坑:不写它在桌面调试时经常看不出问题,因为卡往往很快就写完;一旦换一张慢卡或者容量大的卡,写入就间歇性失败,而且失败位置不固定,非常像玄学。disk_ioctl里的CTRL_SYNC就该在这里落实——它的语义是“把还没落盘的数据全部刷下去”,实现上就是等最后一次写的忙信号结束。
3. FATFS 的分层接缝:diskio 五个函数凭什么能屏蔽所有卡
FATFS 的移植工作量小得有点反直觉:不管底下是 SD 卡、NOR Flash、还是 U 盘,要写的永远只有diskio.c里那五个函数。这种设计不是偷懒,而是把“块设备”这个抽象压到了最小——只要能按扇区读、按扇区写、能报告容量,上层就能在其上构建出完整的文件语义。
3.1 diskio 五个函数的约定边界
五个函数的职责必须分清楚,混淆了就会出现“能挂载但不能写”这类半残状态:
| 函数 | 职责 | 常见实现错误 |
|---|---|---|
disk_status | 报告卡是否在位、是否已初始化 | 永远返回 0,导致拔卡后上层不知道 |
disk_initialize | 走完 2.1 的初始化序列 | 失败后不重置状态,下次调用直接返回成功 |
disk_read | 按扇区读,可多扇区 | 忽略count,只读一块 |
disk_write | 按扇区写,可多扇区 | 写完不等忙,下一次操作被吞 |
disk_ioctl | 容量、扇区大小、同步 | GET_SECTOR_COUNT随便填个大数 |
disk_ioctl的GET_SECTOR_COUNT尤其要老实。它返回的是卡的总扇区数,FATFS 用它做边界检查。有人图省事直接返回0xFFFFFFFF,结果f_lseek定位到文件末尾之外时不会被拦截,最后写坏 FAT 表——这类问题排查起来非常痛苦,因为破坏是静默的。
GET_BLOCK_SIZE的语义是擦除块大小,SD 卡没有这个概念,返回 1 就行。它的存在是为了 NOR/NAND 这类有擦除粒度限制的介质,SD 卡上填错会影响到f_mkfs的簇对齐计算。
3.2 扇区窗口 win[]:一个指针决定读几次卡
FATFS 内部没有动态内存,所有缓存都是FATFS结构体里的一个定长数组win[_MAX_SS],默认 512 字节。整个文件系统在同一时刻只看得到这一个扇区,这个扇区是谁,由winsect记录。每次要访问某个扇区,先比较winsect和目标扇区号,相同就直接用,不同才真正调disk_read。
/* FATFS 结构体里的关键字段(精简,非完整定义) */ typedef struct { BYTE fs_type; /* 0=未挂载,12/16/32 表示 FAT 类型 */ BYTE pdrv; /* 物理驱动器号 */ BYTE wflag; /* win[] 是否脏,需要写回 */ WORD id; /* 挂载 ID,用于校验 FIL 是否属于当前卷 */ WORD csize; /* 每簇多少个扇区 */ DWORD fatbase; /* FAT 表起始扇区 */ DWORD dirbase; /* 根目录起始扇区(FAT12/16) */ DWORD database; /* 数据区起始扇区 */ DWORD winsect; /* 当前 win[] 里装的是哪个扇区 */ BYTE win[_MAX_SS]; /* 唯一的数据窗口 */ } FATFS;wflag和id这两个字段值得单独说。wflag是脏标志,写目录项或 FAT 项时只改win[]并置脏,真正落盘发生在窗口切换或者f_sync的时候。这个延迟写是性能的关键——创建一个文件要改目录项、改 FAT 表、可能还要扩簇,如果每次都同步落盘,速度会慢一个数量级。id是挂载计数,每次f_mount都递增,FIL结构体里存着自己打开时看到的id,一旦不匹配就返回FR_INVALID_OBJECT,防止你把上一张卡的句柄用到新卡上。
_FS_TINY这个配置项切换的是窗口归属:关闭时(默认)每个打开的文件有自己的 512 字节缓冲,读写不同文件互不干扰;打开时所有文件共享fs->win,RAM 占用从“每文件 512 字节”降到“全局 512 字节”,代价是每次切换文件都要重新读扇区,而且一次f_read只能读一个扇区。RAM 紧张的 MCU 上这个开关很实用,但如果同时开好几个文件交替读写,性能下降会非常明显。
3.3 f_mount 到 f_open 的真实调用链
f_mount并不是“把卡初始化一遍”这么简单,它的核心工作是解析 BPB 并把卷参数填进FATFS结构体。调用链大致是:f_mount→find_volume→disk_initialize→disk_read(扇区 0)→check_fs。
check_fs会先看第 0 扇区的第 510、511 字节是不是0x55 0xAA,再看文件系统类型字符串是不是FAT。如果不是,它把第 0 扇区当成 MBR 分区表来解析,找到第一个有效分区的起始扇区,再去那里读引导扇区。所以“卡是超级软盘格式(无分区表)还是有 MBR”这两种情况它都能处理,真正的失败原因通常是 exFAT 或者压根没格式化。
挂载成功之后f_open走的是另一条路:f_open→follow_path→ 逐级dir_find。每一级目录都是一次“在目录项数组里线性查找”,找一个 32 字节项的前 11 字节和请求的名字匹配。目录项本身就在数据区里,可能是根目录(固定位置),也可能是某个簇(要顺着簇链找)。这就是为什么打开一个深层路径的文件,背后可能有十几次disk_read。
f_mount的签名在不同版本间变过:老版本是f_mount(&fs, "", 0)两个参数,新版本多了一个“是否立即挂载”的参数。移植时如果照抄旧代码,编译报参数数量不匹配,别急着改函数原型,先确认手上这版ff.h的声明。
4. 从文件名到扇区号:FAT 表和目录项到底怎么把路径翻译成地址
FATFS 最核心的换算只有一条公式:扇区号 = database + (簇号 - 2) * csize。整条链路的所有复杂度,都是为了求出这个“簇号”。文件名变成簇号靠目录项,簇号变成下一个簇号靠 FAT 表,两件事凑齐了,一个文件的物理布局就确定了。
4.1 BPB 里的四个数决定整个地址空间
引导扇区里的 BPB(BIOS Parameter Block)是整个卷的坐标系。至少有四个字段必须理解:
BPB_BytsPerSec:每扇区字节数,FATFS 要求 512,其他值直接判为不合法。BPB_SecPerClus:每簇扇区数,必须是 2 的幂。这就是fs->csize。BPB_RsvdSecCnt:保留扇区数,引导扇区本身算在里面,通常为 1 或 32。BPB_NumFATs:FAT 表份数,通常是 2,两份互为备份。
FAT 表的位置是fatbase = RsvdSecCnt,数据区位置是database = RsvdSecCnt + NumFATs * FATSz + RootDirSectors。FAT12/16 的根目录是固定长度区域,紧跟在 FAT 表后面,长度由BPB_RootEntCnt决定;FAT32 的根目录本身就是一个簇链,dirbase存的是它的首簇号。
这里有个容易踩的地方:BPB_FATSz16在 FAT32 里是 0,真正的大小在BPB_FATSz32里。如果解析时只看 16 位字段,FAT32 卷算出来的database会完全错位,症状是挂载成功但任何目录操作都返回FR_NO_FILE。判断依据是BPB_RootEntCnt == 0 && BPB_FATSz16 == 0则为 FAT32。
4.2 目录项 32 字节:短名、长名和首簇号的两半
每个文件在目录区占一个 32 字节目录项。前 11 字节是 8.3 格式的短名,属性字节在偏移 11,首簇号被拆成高低两半分别存在偏移 20 和 26,文件大小在偏移 28。这个拆法是为了兼容:FAT12/16 时代首簇号只有 16 位,偏移 20 那两字节当时是保留的,FAT32 才启用。
| 偏移 | 长度 | 含义 |
|---|---|---|
| 0 | 11 | 8.3 短名,不足补空格 |
| 11 | 1 | 属性,0x0F 表示长名项,0x10 表示目录 |
| 13 | 1 | 创建时间十分之一秒 |
| 14 | 2 | 创建时间 |
| 16 | 2 | 创建日期 |
| 20 | 2 | 首簇号高 16 位(FAT32 才用) |
| 26 | 2 | 首簇号低 16 位 |
| 28 | 4 | 文件大小(字节) |
时间日期的编码是位域压缩的:日期 =((年-1980) << 9) | (月 << 5) | 日,时间 =(时 << 11) | (分 << 5) | (秒/2)。get_fattime的实现必须严格按这个格式拼,拼错了会出现“文件日期是 2107 年”或者“所有文件都是 1980-01-01”。后者是返回 0 的表现,FATFS 对 0 的处理是保留原值不变。
长文件名(LFN)的实现方式很朴素:一个长名要占用若干个目录项,每个存 13 个 UTF-16 字符,属性都是0x0F,并且这些项在物理上是逆序排列的——第一个项存的是名字的最后 13 个字符,紧跟在后面的短名项才是真正的文件入口。所以遍历目录时看到0x0F不能直接跳过,要往前收集,直到遇到短名项。
4.3 簇链遍历:一次 f_read 背后到底读了几次卡
f_read的路径是:从FIL里取出当前簇号,算出扇区号,读出来,把数据拷给用户缓冲,簇内偏移走完就查 FAT 表拿下一个簇。查 FAT 表这一步又要读扇区,而 FAT 表项的大小随类型变化:FAT16 每项 2 字节,一个 512 字节扇区装 256 项;FAT32 每项 4 字节,一扇区装 128 项。
/* 顺着簇链走一步:读 FAT 表,取出下一个簇号 */ static DWORD fat_next(FATFS *fs, DWORD clust) { DWORD sector, offset, next; BYTE *p; if (fs->fs_type == 32) { sector = fs->fatbase + (clust * 4) / 512; /* FAT32 每项 4 字节 */ offset = (clust * 4) % 512; if (disk_read(0, fs->win, sector, 1) != RES_OK) return 0xFFFFFFFF; p = &fs->win[offset]; next = (p[0] | (p[1] << 8) | (p[2] << 16) | ((DWORD)p[3] << 24)) & 0x0FFFFFFF; } else { sector = fs->fatbase + (clust * 2) / 512; /* FAT16 每项 2 字节 */ offset = (clust * 2) % 512; if (disk_read(0, fs->win, sector, 1) != RES_OK) return 0xFFFFFFFF; p = &fs->win[offset]; next = p[0] | (p[1] << 8); if (next >= 0xFFF8) next = 0xFFFFFFFF; /* 链尾标志 */ } return next; }0x0FFFFFFF这个掩码不能省:FAT32 表项的高 4 位是保留位,不清掉会算出巨大的簇号。0xFFF8及以上是 FAT16 的链尾标志,0xFFFFFFFF是 FATFS 内部约定的“链结束”。
一段连续读 4KB 的f_read,如果簇大小是 4KB(8 个扇区),理想情况只读一次 FAT 项加 8 次数据扇区;如果簇大小是 512 字节,就要读 8 次数据加 8 次 FAT——慢一倍不止。这就是格式化时簇大小选择的实际影响,不只是“浪费空间”的问题。
注意:
f_lseek跳到文件末尾附近再写,会触发簇分配,f_write的返回值要检查。返回写入字节数小于请求字节数,通常是卡满了或者 FAT 表写失败。
5. 避坑与排查:五个在真机上反复出现的现象
5.1 挂载成功但 f_open 报 FR_NO_FILESYSTEM
现象是f_mount返回FR_OK,紧接着f_open返回FR_NO_FILESYSTEM,同一个卡在电脑上读写正常。
原因通常是卡被格式化成了 exFAT。FATFS 原生不支持 exFAT,check_fs在引导扇区里找不到FAT字样,就会一路走到失败分支。另一种可能是卡的第一个分区不是 FAT 类型,比如有隐藏的恢复分区排在前面。
解决办法是重新格式化为 FAT32,分配单元大小选 4KB 或更大;如果卡容量超过 32GB 且必须用 exFAT,那就不是换配置能解决的,得换文件系统方案。排查时可以直接读第 0 扇区,看偏移 82 附近的类型字符串,FAT32和EXFAT一眼就能分辨。
5.2 挂载成功但读出来全是乱码
现象是f_mount正常,f_open也能拿到文件大小,但f_read读出来的内容和电脑上看到的不一样,或者只有开头一小段是对的。
这是 2.2 说的块地址 / 字节地址问题。挂载只碰第 0 扇区,两种寻址方式下地址都是 0,所以能成功;一旦读到后面就错位。典型特征是读出的内容呈周期性偏移——因为起始地址错了,后面的数据整体平移。
解决办法是确保初始化时正确读取 CMD58 的 CCS 位,并让disk_read、disk_write都按这个标志换算地址。检查方法很直接:在disk_read里把sector和实际传给卡的arg打印出来,如果读第 1 个扇区传的是 512,那就是把 SDHC 卡当老卡处理了。
5.3 跑一会儿就 FR_DISK_ERR,重新上电又好了
现象是系统运行几分钟到几十分钟后,所有文件操作开始返回FR_DISK_ERR,断电重启恢复正常,位置不固定。
三个常见原因叠在一起:SPI 时钟在初始化完成后提得太高,超过了布线能承受的频率;disk_write写完没等 DAT0 释放忙信号;电源在写入瞬间跌落,卡内部复位了但主机不知道。
解决办法按顺序排查:先把 SPI 时钟降到 4MHz 以下观察是否还复现,能把问题推迟就说明是时序余量不够;然后检查写函数里有没有忙等待;最后看电源,SD 卡写入瞬间的电流峰值可以到 100mA 量级,去耦电容不够会导致电压跌落。软件上还可以在disk_status里加一次轻量探测,失败时重新走初始化。
5.4 写入的数据拔卡后消失,或文件长度是 0
现象是f_write返回成功,程序里读回来也对,拔卡插到电脑上文件是空的或者长度不对。
原因是没调f_close或f_sync。FATFS 的写是延迟的,目录项里的文件大小和首簇号、FAT 表里的链信息,都还停在win[]里没落盘。程序直接复位或者断电,这些元数据就丢了。数据区可能已经写进去了,但没人知道它属于哪个文件。
解决办法是每次写完必须f_close,它内部会做f_sync加释放句柄。对可靠性要求高的场景,在关键写入后主动调一次f_sync,并确保disk_ioctl的CTRL_SYNC真的等到了忙结束。中断里不要做文件写入,宁愿置个标志让主循环处理。
5.5 加了个文件句柄就进 HardFault
现象是原来一个文件正常,增加到两个FIL变量后程序跑飞,栈检查发现栈指针跑到数据段里去了。
原因是FIL和FATFS结构体都不小:FATFS光win[]就 512 字节,FIL在非_FS_TINY模式下也有 512 字节缓冲。把它们定义在函数局部变量里,栈瞬间被吃掉 1KB 以上,小容量 MCU 的默认栈根本扛不住。
解决办法是把FATFS和FIL都定义成全局或静态变量,不要放栈上。启动文件里的栈大小也要按实际情况调,开了长文件名支持的话,_USE_LFN还需要额外的 LFN 工作缓冲区,同样不能放栈上。改完之后用调试器看一眼栈的最高水位,留出至少 30% 余量。
6. 设计取舍:为什么 FATFS 宁愿慢一点也不肯多占一字节 RAM
FATFS 的所有取舍都指向同一个约束:它要能跑在 RAM 只有几 KB 的器件上。这个约束解释了它的绝大部分设计决策——不用malloc,所有缓冲静态分配;不用递归,目录遍历用循环加显式状态;不维护内存中的 FAT 缓存,只有一个 512 字节窗口。代价是性能,收益是可预测性。
这个取舍在几个配置项上体现得最直接:
| 配置项 | 打开后 | 代价 |
|---|---|---|
_FS_TINY | 所有文件共享一个扇区窗口 | 多文件交替读写时反复读扇区 |
_USE_LFN | 支持长文件名 | 额外 LFN 缓冲区,且目录扫描变慢 |
_FS_READONLY | 裁掉所有写路径 | 无法创建、修改文件 |
_MAX_SS大于 512 | 支持大扇区介质 | 窗口变大,RAM 占用线性上升 |
最值得琢磨的是它不做日志。现代文件系统靠日志或者写时复制来保证掉电一致性,FATFS 没有这一层,它把责任交给了调用方:f_sync就是你能拿到的最强保证。这个设计放在消费级 SD 卡场景里是合理的——卡本身也没有掉电保护,日志写到一半断电一样会坏。但对工业场景就不够用了,真要抗掉电,得在应用层做双备份加校验,或者换带日志的文件系统。
另一个隐含取舍是簇链的遍历方式。FAT 表的链式结构决定了随机访问是 O(n) 的:要跳到文件第 1000 个簇,得从头顺着走 1000 步,每一步都可能是一次disk_read。这就是为什么大的连续文件读取速度尚可,而频繁f_lseek的小块随机读会非常慢。缓解办法无非是应用层自己缓存簇位置,或者把文件设计成顺序读写。
我自己的习惯是:任何带文件写入的项目,第一版就把f_sync的调用点标出来,并且用断电模拟验证一遍——在写到一半的时候直接拉电,看重新上电后能不能识别出损坏的文件。这个测试做过一次之后,你会对所有“写完就完事”的代码产生本能的怀疑。希望帮到你。
本文还有配套的精品资源,点击获取