自制工业控制系统:exFAT文件系统实战——引导扇区、目录项与掉电一致性
Contents
起因
块设备之上要有文件系统——G 代码程序、加工参数、日志、长时间数据记录都要落在盘上,还要和 U 盘交换。这一族是 1977 年写成、随 DOS 发扬光大的:FAT 的簇链表结构简单到软盘都跑得动,1996 年的 FAT32 把分区撑到 2TB,单文件却从此卡死在 4GB——长录像、大数据记录面前的硬墙。微软 2006 年为闪存和 SD 卡设计的 exFAT 就是来拆这堵墙的,簇链的骨架留着,其余按大容量闪存重画。选 exFAT:U 盘出厂普遍格式、规范公开(微软 2019 年公开完整规范并加入 OIN 向 Linux 生态开放相关专利,自实现的授权风险显著降低;商业产品如有顾虑可让法务复核一遍专利边界)。这篇从引导扇区讲到读写路径,能给表的全给表。
一、磁盘布局与引导扇区
exFAT 卷四段式:主引导区(含引导扇区+扩展引导)→ FAT 区 → 簇堆(数据)→ 末尾备份引导区。目录树就住在簇堆里,根目录是一条簇链(不是固定区域,这点跟 FAT32 根目录不同)。
引导扇区(第 0 扇区)关键字段:
| 偏移 | 字段 | 说明 |
|---|---|---|
| 0x00 | 跳转指令 | EB 76 90 |
| 0x03 | 卷标志 | "EXFAT " 8 字节 |
| 0x40 | 分区偏移 / 0x48 卷长度 | 8 字节 |
| 0x50 | FatOffset / 0x54 FatLength | 4 字节:FAT 起始扇区和扇区数 |
| 0x58 | ClusterHeapOffset | 簇堆起始扇区 |
| 0x5C | ClusterCount | 簇总数 |
| 0x60 | FirstClusterOfRootDirectory | 根目录首簇 |
| 0x68 | 卷序列号 | 4 字节 |
| 0x6C | BytesPerSectorShift | 每扇区字节数 = 512 « 此值 |
| 0x6D | SectorsPerClusterShift | 每簇扇区数 = 1 « 此值 |
| 0x6E | NumberOfFats | 通常 1 |
| 0x1FE | 签名 | 55 AA |
挂载三步:读第 0 扇区验签名和 "EXFAT " → 按移位字段算出簇大小(如 512«9 × 1«3 = 256KB 的盘也常见,工控盘格 32KB 顺手)→ 读根目录首簇开走。簇号从 2 开始编号(0/1 保留),簇号转 LBA:ClusterHeapOffset + (簇号-2) × 每簇扇区数。
二、FAT:簇链的规则
每簇一个 32 位 FAT 项(小端):
| 值 | 含义 |
|---|---|
| 0x00000000 | 空闲簇 |
| 2 … 0xFFFFFFF7 | 下一簇号 |
| ≥ 0xFFFFFFF8 | 链尾(EOC,惯例写 0xFFFFFFFF) |
| bit31 置位 | 坏簇 |
文件的真实长度存在目录项里(FAT 只管链不断不管长度),读文件 = 从首簇出发沿 FAT 走到 EOC,按目录项的 DataLength 截断最后一簇。
三、目录项:文件的长相
目录就是一组 32 字节目录项的线性数组,文件以"目录项集合"出现——一个主项带 N 个二级项:
| 类型码 | 名称 | 内容要点 |
|---|---|---|
| 0x81 | 分配位图 | 指出哪个 bitmap 结构、覆盖哪些簇——找空闲簇查它,比扫 FAT 快 |
| 0x82 | 大写表 | 文件名大小写折叠表(查找时用) |
| 0x83 | 卷标 | |
| 0x85 | 文件主项 | byte1 = SecondaryCount(后面跟着几个二级项);bytes4-5 = 属性(bit0 只读、bit1 隐藏);其后时间戳 |
| 0xC0 | 流扩展项 | byte3 = 名字长度(UTF-16 码元数);bytes4-5 = 名字哈希;bytes8-15 = 有效数据长度;bytes20-23 = 首簇号;bytes24-31 = 数据长度 |
| 0xC1 | 文件名项 | 每项装 15 个 UTF-16LE 码元(bytes2-31),一个名字最多 255 码元 |
解析一个文件的完整画像:读到 0x85 主项 → 数着 SecondaryCount 收二级项 → 流扩展项给首簇和长度 → 文件名项拼接出 UTF-16 名字 → 转成内部 UTF-8 比较/显示(中文文件名天然支持,名字本来就是 UTF-16 存的)。写新文件时这套要反过来拼,且主项 bytes2-3 是集合校验和(对集合内所有项按规范算法算,写错其它工具认不出)。
流扩展项里那个名字哈希(bytes4-5)只做加速用的预筛:先比哈希再比全名,哈希不同直接跳过。别拿哈希相等当名字相等——哈希是 16 位,几千个文件的目录里碰撞是必然事件,全名比对一步都不能省。
四、读写路径
读路径(打开 PART001.NC):
|
|
写路径(追加/新建)纪律是落盘顺序:先写数据簇 → 再更新 FAT(链尾接上)→ 再更新分配位图 → 最后回写目录项(DataLength/首簇)。中途断电,坏的方向是"数据写了目录项没更新"(文件看起来还是旧的,可容忍);反过来则是"目录项指着一堆垃圾"(灾难)。掉电一致性不追求"永不损坏",追求"损坏可检测、可恢复到上一个一致点"。
开机自检-lite:FAT 空闲标记 vs 位图互验、每文件簇链走一遍校验(无环、EOC 收尾)、DataLength 与簇数匹配——异常文件标记隔离并报警,不静默"修复"。exFAT 的事务扩展(TexFAT)没有通用生态,我们也不造日志文件系统这个轮子。
五、扇区缓存与挂载点
块设备之上一层小缓存:64 扇区 LRU 写穿。工控负载是小文件为主(几 KB 的程序、几十字节的参数),命中率极高;写穿策略让"掉电窗口"只有缓存到 Flush 之间那一小段,配合 NVMe 篇的关键节点 Flush 纪律,风险收敛到可接受。
缓存之下就是 BlockDevice trait(NVMe 篇定义):本篇代码一行不改就能同时服务 NVMe 盘和 U 盘(U 盘那篇的 MSC+SCSI 实现的是同一个 trait,见U盘驱动实战)。
六、时间戳:接上系统时间
文件项的时间戳格式跟 DOS 一脉相承:4 字节时间戳 = DOS 日期(高 16 位)+ DOS 时间(低 16 位),粒度 2 秒,纪元 1980;旁边跟一个 10ms 增量字节补精度,再加一个时区偏移字节(15 分钟一格)。写入时从系统时间转换(APIC 篇的 RTC+单调钟在这里派上用场:加工记录的时间戳要经得起对账);时区字节工控场景统一写 0(UTC)或本地值,别一台机器一个写法。
总结
实战清单:引导扇区偏移表挂载验卷、移位字段算簇大小、簇号-2 转 LBA、FAT 项语义表、目录项集合(0x85+0xC0+0xC1×n)解析出首簇/长度/UTF-16 名字、位图找空簇、写路径四步落盘顺序、开机自检-lite、64 扇区 LRU 写穿、时间戳接系统钟。跑通标志:QEMU 里 mkfs.exfat 一个镜像,我们的内核列目录、读出文件内容与宿主机一致;写入后挂回 Linux 能读出。到这里存储这条线闭环了:块设备、文件系统、U 盘交换都是自己的代码。下一篇把网络拉进来:e1000 有线网卡驱动和自研 TCP/IP 协议栈。
参考链接
- 微软 exFAT 规范(MS Learn)——目录项/FAT/位图的权威定义
- OSDev Wiki: ExFAT
- 本系列 NVMe 篇:《NVMe固态硬盘驱动实战》;APIC 篇:《APIC定时器与系统时间实战》
- 本系列第一篇:自制工业控制系统:从UEFI到内核
Author 软件开发大郭
LastMod 2026-09-11