在主机上生成和解析 FATFS ===================================== :link_to_translation:`en:[English]` 本章主要面向 Python 工具 :component_file:`fatfsgen.py ` 和 :component_file:`fatfsparse.py ` 的开发人员、对这些工具感兴趣的用户、和对在 ESP-IDF 中实现 FAT 文件系统感兴趣的用户。如果你对这些工具感兴趣,请参考 :ref:`fatfs-partition-generator`。 FAT 文件系统由多个逻辑单元组成。这些单元用于存储有关文件系统、分配、文件内容、目录以及文件元数据的一般信息。 ``fatfsgen.py`` 和 ``fatfsparse.py`` 工具用于实现 FAT 文件系统,该工具会考虑上述所有逻辑单元,并且支持磨损均衡。 FAT 文件系统生成器及解析器的设计 ---------------------------------------- 本节介绍了 FAT 文件系统生成器和解析器设计的特定单元,设计这些单元是为了创建一个专注于宏操作的有效 FAT 结构模型,用于生成和解析整个分区,而无需在运行(挂载)过程中进行修改。 .. figure:: ../../../_static/classes_fatfsgen.svg :align: center :alt: 图表 FAT 文件系统生成器及解析器设计架构 FATFS 类 ^^^^^^^^^^^^^ FATFS 类是建立 FAT 文件系统模型最常用的实体,由 **FATFSState** (保存元数据和引导扇区)、 **FAT** (保存文件分配表)和 **Directory** (表示 FAT12 和 FAT16 所需的根目录)组成。该类可以处理分区的所有要求,分析用于将其转换为二进制映像的本地文件夹,并生成本地文件夹的内部表示。然后,该类就能从内部 FAT 文件系统模型中生成二进制映像。 WLFATFS 类 ^^^^^^^^^^^^^ WLFATFS 类扩展了 **FATFS** 类的功能,可在磨损均衡中添加虚拟扇区(用于平衡负载的冗余扇区,参见 :ref:`fafsgen-wear-levelling`)、配置扇区和状态扇区,从而对文件系统进行封装。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 中的位置和在数据区中的相应扇区。分配簇时,分配链中的第一个簇会指向一个文件或目录。每个簇中包含该簇是否为空的信息,以及该簇是否为文件分配链中的最后一个簇。如果不是最后一个簇,则指向链表中下一个簇。在实际应用中,簇不需要访问其中的文件,而是反过来由 **文件** 或 **目录** 访问对应的簇,以检索链上可能的全部内容。 .. figure:: ../../../_static/fat_table.svg :align: center :alt: 图表 Directory 类 ^^^^^^^^^^^^^^^ **Directory** 类表示文件系统目录。 **Directory** 的实例包含对相应 **Cluster** 实例的引用,该实例中有给定目录的分配链中的第一个簇。根目录较为特殊,其扇区数量不同,实例化过程也有所不同。不过,根目录仍然是此类的实例,也是分别与 **FATFS** 类和 **WLFATFS** 类相关联的唯一 **Directory** 实例。 **Directory** 类(除根目录外)与在父目录中定义其条目的 **Entry** 类一对一的关联。此外,由于每个目录都包含由实际目录内容组成(如别名、文件和目录)的多个条目,因此,它还有一个与 **Entry** 类关联的聚合。 File 类 ^^^^^^^^^^ 与 **Directory** 类似, **File** 代表文件系统中的单个文件。此类与其分配链中的第一个簇一对一关联。通过这个簇, **File** 类可以访问相应的物理地址,从而修改其内容。每个文件还与属于其父目录的 **Entry** 实例具有一对一关联。 Entry 类 ^^^^^^^^^^^ **Entry** 类封装了在相应父目录数据区中的文件名或目录名信息。每个文件系统实体(文件/目录)都有一个条目。如果使用符号进行连接,可以让实体具有多个条目。目录使用条目来访问其后代文件和子目录,并实现对树状结构的遍历。此外, **Entry** 还保存了所用文件名(长文件名或 8.3 文件名)相关的名称、扩展名、大小等信息。 .. figure:: ../../../_static/tree_fatfs.svg :align: center :alt: 树状图 ``fatfsgen.py`` --------------- 组件 :component_file:`fatfsgen.py ` 在主机上生成 FAT 文件系统。 ``fatfsgen.py`` 递归式地遍历给定文件夹的目录结构,将文件和(或)目录添加到二进制分区中。用户可以设置脚本生成的分区是否支持磨损均衡和长文件名,以及是否保留原始文件夹在主机上的修改日期和时间。 ``./fatfsgen.py Espressif`` 命令默认生成一个简单的二进制分区。这里 ``Espressif`` 是生成二进制映像的本地文件夹(包含文件和/或子目录)。 :component_file:`fatfsgen.py ` 和 :component_file:`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`` ----------------- :component_file:`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.py`` 或 ``fatfsparse.py`` 不能处理簇数超过 65525 的文件系统。 .. _fafsgen-wear-levelling: 磨损均衡 ^^^^^^^^^^^^^^ 与磨损均衡层相关的操作有两个,即初始化磨损均衡记录,和在生成及解析 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_DIRECTORY`` 或 ``ATTR_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_CrtTime** 和 **DIR_WrtTime**。ESP-IDF 的 FAT 实现过程会忽略一些字段(参见文件 ``entry.py``),然而 **DIR_WrtTime** 和 **DIR_WrtDate** 字段的更改会保留在芯片中。时间和数据条目都是 16 位的,其中时间粒度为 2 秒。