# 在主机上生成和解析 FATFS

[English]

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

FAT 文件系统由多个逻辑单元组成。这些单元用于存储有关文件系统、分配、文件内容、目录以及文件元数据的一般信息。 `fatfsgen.py` 和 `fatfsparse.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.py` 或 `fatfsparse.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_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 秒。

提供有关此文档的反馈