在主机上生成和解析 FATFS

[English]

本章主要面向 Python 工具 fatfsgen.py fatfsparse.py 的开发人员、对这些工具感兴趣的用户、和对在 ESP-IDF 中实现 FAT 文件系统感兴趣的用户。如果你对这些工具感兴趣,请参考 FatFs 分区生成器

FAT 文件系统由多个逻辑单元组成。这些单元用于存储有关文件系统、分配、文件内容、目录以及文件元数据的一般信息。 fatfsgen.pyfatfsparse.py 工具用于实现 FAT 文件系统,该工具会考虑上述所有逻辑单元,并且支持磨损均衡。

FAT 文件系统生成器及解析器的设计

本节介绍了 FAT 文件系统生成器和解析器设计的特定单元,设计这些单元是为了创建一个专注于宏操作的有效 FAT 结构模型,用于生成和解析整个分区,而无需在运行(挂载)过程中进行修改。

图表

FAT 文件系统生成器及解析器设计架构

FATFS 类

FATFS 类是建立 FAT 文件系统模型最常用的实体,由 FATFSState (保存元数据和引导扇区)、 FAT (保存文件分配表)和 Directory (表示 FAT12 和 FAT16 所需的根目录)组成。该类可以处理分区的所有要求,分析用于将其转换为二进制映像的本地文件夹,并生成本地文件夹的内部表示。然后,该类就能从内部 FAT 文件系统模型中生成二进制映像。

WLFATFS 类

WLFATFS 类扩展了 FATFS 类的功能,可在磨损均衡中添加虚拟扇区(用于平衡负载的冗余扇区,参见 磨损均衡)、配置扇区和状态扇区,从而对文件系统进行封装。WLFATFS 类还能生成带有初始化的磨损均衡层的二进制 FATFS 分区,并提供了完全删除磨损均衡的选项,用于进一步分析。该类可由 wl_fatfsgen.py 脚本进行实例化并调用。

BootSectorState 类

BootSectorState 实例包含了构建引导扇区和 BPB(BIOS 参数块)所需的元数据。构件引导扇区主要是为了实现跨平台兼容,即当 ESP 芯片组连接到其他平台时,会始终遵守所有的 FAT 文件系统标准。但是,在分区生成期间,芯片并不消耗此引导扇区的数据和其他所需数据,因为这些数据是不变的常量。换句话说,一般无需更改前缀为 "BS" 的字段,且更改通常没有作用。如需添加新功能,应关注前缀为 "BPB" 的字段。 BootSectorState 类还有一个重要作用,即在整个类系统中共享对元数据和二进制镜像的访问。因此,系统中的每个类都可以访问这一单例。

FATFSState 类

FATFSState 类未来可能会被弃用,开发人员可以将其功能转移到 BootSectorState 中。该类包含对 BootSectorState 的引用,并且在创建引导扇区时,会用一些未知或不必要的信息(例如在文件系统支持长文件名时生成的信息)扩展引导扇区数据。

FAT 类

FAT (File Allocation Table) 代表文件分配表,是分布在一个或多个扇区上的字节序列。扇区数由簇数决定,并由 utils.py 中的函数 get_fat_sectors_count 计算得出,因为在引用文件系统中的物理簇时,每个 FAT 的扇区数应越少越好。FAT 的工作方式如下:对于每个位于 i * some_constant 地址的物理簇,FAT 在第 i 个位置有一个条目,表示文件链中下一个簇的地址。每个 FAT 文件系统版本使用的 FAT 条目大小不同。FAT12 每个条目大小为 12 位,因此每 2 个条目占据 3 个字节。FAT16 的每个条目为 16 位,因此每 1 个条目占据 2 个字节。FAT32 的 每个 FAT 条目为 32 位,因此每 1 个条目占据 4 个字节。所有条目都按小字节顺序排列。

i 个条目处的所有 0 表示相应的簇为空闲状态,第 i 个条目处的所有 1 表示相应簇已占用,并且是文件链中的最后一个簇。 第 i 个条目处的其他数字表示下一个簇在文件链中的地址。这些簇并不一定存储在内存中的相邻位置,而通常会分散在整个数据区域中。

在生成分区时,文件会被分为几个部分以适应簇的大小。注意,文件的结构分配是一个链表。文件分配链中的每个簇在 FAT 中都有一个条目指向下一个簇,或文件链中最后一个簇的信息。如前所述,FAT12 的每个 FAT 条目有 12 位,因此最多可以枚举 4096 个簇,因为在 12 位(一个半字节)条件下最多可以列出 4096 个簇。但实际上由于其他开销,FAT12 最多可以有 4085 个簇。同样,FAT16 最多可以有 65525 个簇,而 FAT32 最多可以有 268435445 个簇,因为实际上每个条目只用了 32 位中的 28 位。即便文档中声称可行,但实际上目前的实现方式不允许将簇数少于 4085 的 FAT 文件系统强制重新定义为 FAT16。反之亦然,将具有 4085 个以上簇的 FAT 文件系统强制重新定义为 FAT12 也是没有意义的,否则将无法访问某些地址超出范围的簇。

Cluster 类

Cluster 类用于访问 FAT 条目和相应的物理簇。 FAT 类是特定数量 Cluster 实例的集合。每个簇具有一个唯一 ID,用于确定其在 FAT 中的位置和在数据区中的相应扇区。分配簇时,分配链中的第一个簇会指向一个文件或目录。每个簇中包含该簇是否为空的信息,以及该簇是否为文件分配链中的最后一个簇。如果不是最后一个簇,则指向链表中下一个簇。在实际应用中,簇不需要访问其中的文件,而是反过来由 文件目录 访问对应的簇,以检索链上可能的全部内容。

图表

Directory 类

Directory 类表示文件系统目录。 Directory 的实例包含对相应 Cluster 实例的引用,该实例中有给定目录的分配链中的第一个簇。根目录较为特殊,其扇区数量不同,实例化过程也有所不同。不过,根目录仍然是此类的实例,也是分别与 FATFS 类和 WLFATFS 类相关联的唯一 Directory 实例。 Directory 类(除根目录外)与在父目录中定义其条目的 Entry 类一对一的关联。此外,由于每个目录都包含由实际目录内容组成(如别名、文件和目录)的多个条目,因此,它还有一个与 Entry 类关联的聚合。

File 类

Directory 类似, File 代表文件系统中的单个文件。此类与其分配链中的第一个簇一对一关联。通过这个簇, File 类可以访问相应的物理地址,从而修改其内容。每个文件还与属于其父目录的 Entry 实例具有一对一关联。

Entry 类

Entry 类封装了在相应父目录数据区中的文件名或目录名信息。每个文件系统实体(文件/目录)都有一个条目。如果使用符号进行连接,可以让实体具有多个条目。目录使用条目来访问其后代文件和子目录,并实现对树状结构的遍历。此外, Entry 还保存了所用文件名(长文件名或 8.3 文件名)相关的名称、扩展名、大小等信息。

树状图

fatfsgen.py

组件 fatfsgen.py 在主机上生成 FAT 文件系统。

fatfsgen.py 递归式地遍历给定文件夹的目录结构,将文件和(或)目录添加到二进制分区中。用户可以设置脚本生成的分区是否支持磨损均衡和长文件名,以及是否保留原始文件夹在主机上的修改日期和时间。

./fatfsgen.py Espressif 命令默认生成一个简单的二进制分区。这里 Espressif 是生成二进制映像的本地文件夹(包含文件和/或子目录)。

fatfsgen.py wl_fatfsgen.py 脚本都可以用于此目的,二者的区别在于, wl_fatfsgen.py 首先用 fatfsgen.py 生成分区,然后再初始化磨损均衡。

脚本命令行参数如下:

fatfsgen.py [-h] [--output_file OUTPUT_FILE] [--partition_size PARTITION_SIZE] [--sector_size {4096}] [--long_name_support] [--use_default_datetime] input_directory

--output_file:生成的二进制分区的路径
--partition_size:定义二进制分区大小(十进制、十六进制或二进制)
--sector_size:扇区大小
--long_name_support:flag,表示支持长文件名
--use_default_datetime:flag,强制使用默认的日期和时间 (date == 0x2100, time == 0x0000),不使用参数保留原始文件系统元数据
input_directory:必填参数,编码到二进制分区 fat-compatibile 的目录名称

fatfsparse.py

fatfsparse.py 将二进制映像转换成内部表示,并在主机上生成具有等效内容的文件夹。如果要求解析分区具有初始化磨损均衡, fatfsparse.py 会使用 wl_fatfsgen.py 提供的 remove_wl 函数删除磨损均衡扇区。删除扇区后,对分区的解析和没有初始磨损均衡的情况相同。

./fatfsparse.py fatfs_image.img 命令会生成与二进制数据映像 fatfs_image.img 具有等效内容的目录。

脚本命令行参数如下:

fatfsparse.py [-h] [--wl-layer {detect,enabled,disabled}] input_image

--wl-layer:表示是否启用、禁用或应检测磨损均衡(模糊检测)
input_image:二进制映像的路径

长文件名可以自动检测,但无法 100% 检测出磨损均衡,因为根据用户的上下文,一个分区在有或没有磨损均衡的情况下都是有效的。脚本找到磨损均衡扇区(cfg 和 state)时,会假设磨损均衡已启用,但实际不一定启用。

支持功能

FAT12/FAT16

支持 FAT12 和 FAT16。对于较小的分区,使用 FAT12 即可。具体选择根据检测簇数决定,用户无法进行更改。如果分区簇数小于 4085,会选择 FAT12(FAT 的条目为 12 位)。如果分区簇数在 4085 到 65526 之间(不包括 4085 和 65526),会选择 FAT16。目前 fatfsgen.pyfatfsparse.py 不能处理簇数超过 65525 的文件系统。

磨损均衡

与磨损均衡层相关的操作有两个,即初始化磨损均衡记录,和在生成及解析 FAT 文件系统映像时删除磨损均衡记录。

  1. 初始化磨损均衡

生成支持磨损均衡的新映像时,脚本会初始化磨损均衡功能所需的几个额外扇区。

  • 虚拟扇区:位于分区起始位置的空扇区,文件系统挂载时会被忽略。虚拟扇区复制下一个扇区的内容,在特定数量的擦除周期后,与下一个扇区交换位置(如果虚拟扇区已是最后一个扇区,则与第一个扇区交换位置)。这样,每个 FAT 文件系统扇区会遍历整个 flash 分区,而与此扇区对应的擦除周期也会分布在整个 flash 上。

  • 状态扇区:状态扇区存储了 64 字节的数据。
    • pos:虚拟扇区的位置

    • max_pos:分区中的扇区数(不包括配置扇区和状态扇区)

    • move_count:表示虚拟扇区遍历整个 flash 的次数

    • access_count:虚拟扇区交换位置前的扇区擦除周期数

    • max_count:等于 wl_config_t::updaterate

    • block_size:等于 wl_config_t::page_size

    • version:等于 wl_config_t::version

    • device_id:在状态扇区次初始化时随机生成

    • reserved:7 x 32 位,设置为 0

    • crc32:前面所有字段的 crc32,包括保留字段

    此外,状态扇区会对每个 pos 值增加 16 字节的 pos update record。该记录会帮助确定虚拟扇区的位置。

    由于状态扇区的 erase + write 不是原子操作,在 “erase” 和 “write” 之间断电可能会导致数据丢失。不过状态扇区保留了两份副本,可以在断电后帮助复原。每次更新时两份副本都会更新,因此,断电后可以恢复原来的有效状态扇区。

  • 配置扇区:此扇区包含磨损均衡层使用的分区信息。
    • start_addr:分区的起始地址(始终为 0)

    • full_mem_size:分区大小,包括数据、虚拟、状态 x 2 和配置扇区,单位为字节

    • page_size:等于扇区大小(通常为 4096)

    • sector_size:对于 ESP-IDF 支持的 NOR flash 类型,始终为 4096

    • updaterate:ESP-IDF 始终将此值设置为 16。需要时可将其用作配置选项

    • wr_size:始终设置为 16

    • version:当前版本为 2

    • temp_buff_size:始终设置为 32(实际不应该存储在 flash 中)

    • crc:之前所有值的 crc32

2. 删除磨损均衡 删除磨损均衡记录时,须找到虚拟扇区的位置以及分区的原始有效顺序(因为遍历虚拟扇区会打乱分区)。脚本可以从分区中删除其他磨损均衡扇区。删除磨损均衡记录的步骤如下:

  • 找到虚拟扇区位置 pos。该位置由状态扇区中 pos update records 的数量决定。

  • 删除虚拟扇区并合并虚拟扇区前后的剩余扇区,从而创建新映像。

  • 删除分区末尾的磨损均衡状态扇区和配置扇区。

  • 对新映像重新排序以获得其原始顺序。 move_count 可以找到分区的起点。分区会从 end_of_partition - move_count 位置开始,因此删除磨损均衡扇区后,分区的起始位置是 partition[end_of_partition - (move_count*page_size)]

文件名编码

FAT 协议支持两种类型的文件名。

短文件名 (SFN)

文件名必须遵循 SFN 规范。SFN 指 8.3 文件名规范,即文件名为 8 个字符,扩展名为 3 个字符。这种模式不区分大小写,但在生成器的内部表示中,所有文件名都会改为大写。描述短文件名的条目长 32 字节,其结构如下:

Offset:   00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
0x000000: 46 49 4C 45 4E 41 4D 45 45 58 54 20 18 00 00 00    FILENAMEEXT.....
0x000010: 21 00 21 00 00 00 00 00 21 00 02 00 1E 00 00 00    !.!.....!.......

该条目表示当前文件名遵循 8.3 文件名规范 ("FILENAME.EXT") __(0x00/00-0A)__,文件名大小为 0x1E = 30 字节 __(0x10/0x0C)__,默认修改和创建时间为 (0x0021) __(0x10/00,02 和 08)__。文件相关的簇位于 __0x02 (0x10/0A)__。注意,每个字符用 1 个字节编码(例如,__0x46 == 'F'__)。

长文件名 (LFN)

长文件名 LFN 支持 255 个字符,不包括末尾的 NULL。LFN 支持短文件名中的任何字符,以及句点 . 和特殊字符 + , ; = [ ]。LFN 使用 UNICODE,因此每个字符用 2 个字节编码。

使用 LFN 编码的文件名称结构如下:

00003000: 42 65 00 2E 00 74 00 78 00 74 00 0F 00 43 FF FF    Be...t.x.t...C..
00003010: FF FF FF FF FF FF FF FF FF FF 00 00 FF FF FF FF    ................
00003020: 01 74 00 68 00 69 00 73 00 69 00 0F 00 43 73 00    .t.h.i.s.i...Cs.
00003030: 6C 00 6F 00 6E 00 67 00 66 00 00 00 69 00 6C 00    l.o.n.g.f...i.l.
00003040: 54 48 49 53 49 53 7E 31 54 58 54 20 00 00 D6 45    THISIS~1TXT...VE
00003050: 26 55 26 55 00 00 D6 45 26 55 02 00 1C 00 00 00    &U&U..VE&U......

上述示例展示了文件名 thisislongfile.txt 的编码。该记录由多个条目组成,第一个条目包含元数据,相当于 SFN 条目。如果文件名符合 8.3 文件名规范,该条目可能就是最后的条目,使用 SFN 文件名编码结构。否则,生成器会在 SFN 条目上方添加具有上述 LFN 结构的多个条目,其中包含有关文件名及其一致性校验和的信息。每个 LFN 可以容纳 13 个字符(26 字节)。文件名首先会被切分成一定数量的 13 个字符的子串,这些子串会被添加到 SFN 条目上方。

LFN 条目以逆序添加,因此,目录中的第一个条目是文件名的最后一部分,即 SFN 条目。在上述示例中,第一个条目包含文本 e.txt,而另外的条目包含文件名开头部分 thisislongfil。LFN 条目的第一个字节表示顺序或序列号(从 1 开始编号)。要确定 LFN 的第一个条目,第一个字节会被掩码为 0x40 (first_byte =| 0x40)。最后一个条目的值会与 0x40 进行 OR 运算,作为最后一个条目的标记。例如,当记录是 LFN 条目中的第二条也是最后一条时,其第一个字节是 0x42

LFN 条目在 DIR_Attr 字段的值为 ATTR_READ_ONLY | ATTR_HIDDEN | ATTR_SYSTEM | ATTR_VOLUME_ID (参见文件 long_filename_utils.py )。SFN 条目在此字段中包含 ATTR_DIRECTORYATTR_ARCHIVE (LFN 中可能也包含这两个值),分别表示目录或文件。

LFN 条目在 DIR_NTRes 字段上标记为 0x00。这是 SFN 条目在 LFN 记录中的标志,如果条目是一个完整 SFN 记录,值为 0x18。在第一个示例中,该字段中此值为 0x18,因为名称 "FILENAME.EXT" 同样符合 SFN 规范。然而,上一个示例 "thisislongfile.txt" 在最后一个条目的 DIR_NTRes 字段中值为 0x00,这是因为它仅符合 LFN 规范。SFN 须唯一。为此, fatfsgen.py 使用文件名的前 6 个字符,将其与 ~ 和一个 ID 相连接。这一 ID 表示该文件名在相同前缀的文件名中的顺序,范围 在 0 到 127 之间,127 是具有相同前缀的文件的最大数量。

校验和的计算在 utils.py 中由函数 lfn_checksum 描述并实现。 fatfsparse.py 假设 LFN 条目可能不会紧挨彼此,但保留了彼此的相对顺序。这一脚本首先用 DIR_NTRes 字段找到属于某个 LFN 记录的 SFN,然后开始在相应扇区自下而上进行搜索,直至找到 LFN 记录中的最后一个条目(前半字节等于 4 的条目)。脚本通过校验和来区分条目。这一过程结束后,即可组成文件名。

FAT 文件系统中的日期和时间

ESP-IDF 使用的 FAT 文件系统协议不保留芯片介质上的日期或时间,因此,从设备中提取的所有映像都具有相同的默认时间戳,这个时间戳会应用到所有 FAT 相关的日期和时间字段上,包括创建、最后修改时间戳,以及创建、最后修改和最后访问日期。

SFN 条目中有几个描述时间的字段,如 DIR_CrtTimeDIR_WrtTime。ESP-IDF 的 FAT 实现过程会忽略一些字段(参见文件 entry.py),然而 DIR_WrtTimeDIR_WrtDate 字段的更改会保留在芯片中。时间和数据条目都是 16 位的,其中时间粒度为 2 秒。