警告

This document is not updated for ESP32C5 yet, so some of the content may not be correct.

This warning was automatically inserted due to the source file being in the add_warnings_pages list.

在主机上生成和解析 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 秒。