TF卡的存储数据结构—fat32格式
引言:为何要了解FAT32?
在嵌入式开发与存储设备领域,FAT32文件系统无处不在。无论是SD卡、TF卡还是U盘,FAT32因其极高的兼容性和简洁性成为事实上的标准。本文将带你深入FAT32的底层世界,解析其数据存储的奥秘,并探讨如何在单片机环境中进行实际操作。
一、FAT32物理存储结构:卷的整体布局
一张格式化后的FAT32存储卡,其空间被划分为几个连续且结构清晰的区域:
偏移量 (Sector) | 名称 | 大小 | 描述 |
---|---|---|---|
0 | DBR (引导扇区) | 通常 1 扇区 | 包含分区信息、引导代码和FAT32的关键参数,是所有计算的起点。 |
1 - (Reserved - 1) | 保留扇区 | DBR定义 (通常为32扇区) | 保留供系统使用。 |
Reserved | FAT1 (文件分配表1) | DBR定义 | 第一份FAT表,是整个文件系统的核心地图,记录簇的分配状态和链式关系。 |
Reserved + FAT_Size | FAT2 (文件分配表2) | 同FAT1 | FAT1的完整备份,用于冗余,提高数据安全性。 |
Reserved + 2*FAT_Size | 数据区 (Data Region) | 剩余所有空间 | 存放文件和目录实际内容的区域,空间在此被划分为簇(Cluster)。 |
关键概念:簇 (Cluster)
-
簇是FAT32分配空间的基本单位,由一个或多个连续的扇区组成(如64扇区=32KB)。
-
所有文件和目录都占用一个或多个簇。
1. 引导扇区(DOS Boot Record, DBR)
-
位置:分区的第一个扇区(通常是512字节)。
-
作用:
-
存储关键参数:包含让操作系统挂载(识别)这个文件系统所必需的全部信息。这些参数在格式化时就确定了。
-
包含引导代码(可选):如果该分区是活动分区,这里的代码可以用于启动操作系统。
-
-
关键参数举例(这些都是写在DBR里的):
-
每扇区字节数(Bytes Per Sector):通常是512。
-
每簇扇区数(Sectors Per Cluster):这是关键!FAT32不再操作单个扇区,而是将连续的扇区组合成一个“簇(Cluster)”作为基本存储单位。这个值可以是1, 2, 4, 8, ... 64(即簇大小可以是512B, 1KB, 2KB, ... 32KB)。大容量磁盘会用更大的簇来减少FAT表的大小。
-
保留扇区数(Reserved Sectors):通常为32。指从分区开始到FAT表之前的所有扇区,包括DBR本身。
-
FAT表份数(Number of FATs):通常是2。为了冗余备份,有两份一模一样的FAT表(FAT1和FAT2)。
-
每个FAT表的大小(Sectors Per FAT):告诉系统FAT表占用了多少扇区。
-
根目录起始簇(Root Directory Cluster):这是FAT32与FAT16的关键区别之一。FAT32的根目录不再固定位置和大小,它和普通文件一样,可以位于数据区的任何位置,并且大小可以动态增长。DBR里记录了根目录的第一个簇是哪个。
-
2. 文件分配表(File Allocation Table, FAT)
-
位置:紧跟在保留扇区(DBR等)之后。
-
作用:这是FAT文件系统的心脏。它是一个数字数组,数组的每个下标对应数据区的一个簇号。这个数组元素的值告诉我们下一个簇在哪里。
-
工作方式:
-
数据区的所有簇都在FAT表中有一个对应的“表项”。
-
每个表项的长度是 32位(4字节),这也是“FAT32”名字中“32”的由来。
-
FAT表项的值定义了对应簇的状态:
-
0
: 表示该簇是空闲的,可以使用。 -
2
-0x0FFFFFEF
: 表示该簇已被文件占用,并且这个值代表该文件的下一个簇的簇号。这就形成了一个簇链(Cluster Chain)。 -
0x0FFFFFF7
: 表示这是一个坏簇,不再使用。 -
0x0FFFFFF8
-0x0FFFFFFF
: 表示这是文件使用的最后一个簇(End Of File)。
-
-
举个例子理解簇链:
假设一个文件被存储在簇号 5
, 8
, 9
这三个簇中。
那么FAT表的内容看起来是这样的:
簇号 | FAT表项的值 (下一个簇) |
---|---|
5 | 8 |
8 | 9 |
9 | 0x0FFFFFFF (EOF) |
操作系统要读取这个文件,会先从目录项中找到它的起始簇是 5
,然后:
-
读簇
5
的内容。 -
查FAT[5],得到下一个簇是
8
。 -
读簇
8
的内容。 -
查FAT[8],得到下一个簇是
9
。 -
读簇
9
的内容。 -
查FAT[9],发现是EOF,文件读取结束。
3. 数据和目录区(Data Region)
-
位置:在FAT表(通常有两份)之后。
-
作用:存放文件和目录的实际数据。
-
目录(Directory)的实现:
-
目录在FAT32中本质上是一种特殊文件,它的内容不是文本或图片,而是一个由 32字节 的“目录项(Directory Entry)”组成的表格。
-
每个文件或子目录在它所属的目录文件中都对应一个或多个目录项。
-
目录项的结构(32字节):
-
文件名(8字节)+ 扩展名(3字节):经典的“8.3”格式。例如
README.TXT
。 -
属性(1字节):标识这是普通文件、目录、隐藏文件还是系统文件等。
-
创建/修改/访问时间戳。
-
起始簇号高16位/低16位(2+2字节):这是最关键的信息! 它指明了这个文件/目录的第一个簇的簇号。操作系统通过这个簇号,去FAT表中查找,就能得到整个文件的簇链。
-
文件大小(4字节):这限制了FAT32单个文件最大不能超过4GB(2^32字节)。
-
-
-
长文件名(Long File Name, LFN)的实现:
-
为了兼容DOS,“8.3”格式的目录项是必须存在的。
-
长文件名(如
This is a long file name.txt
)是通过在主目录项之前放置多个额外的、属性标记为“长名”的目录项来实现的。 -
每个长文件名目录项可以存储13个字符(UTF-16编码)。长文件名被倒序存放在这些额外的目录项中。
-
操作系统读取时,会先看到一系列长文件名项,组装出长名,最后看到传统的“8.3”短名项,其中包含了起始簇号等关键信息。
-
1. DBR (DOS Boot Record) 详解
DBR位于第一个扇区(512字节),其结构如下表所示。所有值均为小端字节序 (Little-Endian)。
偏移量 (字节) | 长度 (字节) | 名称 | 示例值 | 描述与计算公式 |
---|---|---|---|---|
0x00 | 3 | 跳转指令 (Jmp Boot) | 0xEB5890 | 跳过引导代码的指令。 |
0x03 | 8 | OEM ID | "MSWIN4.1" | 格式化此卷的操作系统标识。 |
0x0B | 2 | 每扇区字节数 (Bytes Per Sector) | 0x0200 | 512。几乎所有硬盘和SD卡都是512。 |
0x0D | 1 | 每簇扇区数 (Sectors Per Cluster) | 0x20 | 32。这意味着簇大小 = 32 * 512 = 16 KiB。 |
0x0E | 2 | 保留扇区数 (Reserved Sectors) | 0x0020 | 32。从分区开始到FAT表之前的扇区总数。 |
0x10 | 1 | FAT表份数 (Number of FATs) | 0x02 | 2。通常为2(FAT1和FAT2)。 |
0x11 | 2 | 根目录项数 (Root Entries) | 0x0000 | FAT32中必须为0。根目录现在是簇链的一部分。 |
0x13 | 2 | 总扇区数 (16位) | 0x0000 | 如果为0,则使用在0x20处的32位值。 |
0x15 | 1 | 介质描述符 (Media Descriptor) | 0xF8 | 0xF8 表示固定磁盘(硬盘、SD卡)。 |
0x16 | 2 | 每个FAT的大小 (16位) (Sectors Per FAT16) | 0x0000 | FAT32中必须为0。使用在0x24处的32位值。 |
... | ... | ... | ... | ... |
0x20 | 4 | 总扇区数 (32位) (Total Sectors 32) | 0x01D29600 | **** 分区的总扇区数 。 |
0x24 | 4 | 每个FAT的大小 (32位) (Sectors Per FAT32) | 0x00001CE3 | **** 每个FAT表占用的扇区数 。 |
... | ... | ... | ... | ... |
0x2C | 4 | 根目录起始簇号 (Root Cluster) | 0x00000002 | 根目录的簇号! 通常是2。这是FAT32与FAT16的关键区别。 |
... | ... | ... | ... | ... |
0x1FE | 2 | 结束标志 (Boot Signature) | 0xAA55 | 标识这是一个有效的引导扇区。 |
关键计算(基于DBR中的参数):
-
FAT1的起始扇区 =
保留扇区数
= 32 -
FAT2的起始扇区 =
保留扇区数 + 每个FAT的大小
= 32 +0x1CE3
-
数据区的起始扇区 =
保留扇区数 + (FAT表份数 * 每个FAT的大小)
= 32 + (2 *0x1CE3
) -
簇N对应的第一个扇区号 =
数据区起始扇区 + ((N - 2) * 每簇扇区数)
-
*为什么是N-2?因为数据区的簇号从2开始编号。*
-
2. 文件分配表 (FAT) 详解
FAT表是一个大数组,数组的每个索引号对应数据区的一个簇号。每个FAT项占4字节(32位)。
FAT项索引 (簇号) | FAT项值 (32位) | 描述 |
---|---|---|
0 | 0xFFFFFFF8 | 介质类型。通常与DBR中的介质描述符相同。 |
1 | 0xFFFFFFFF | 脏卷标志。如果文件系统未正常卸载,此值可能被设置。 |
2 | 0x0000000F | 根目录的簇链。值0x0F 表示根目录的下一个簇是15。 |
... | ... | ... |
10 | 0x00000011 | 一个文件的一部分。表示簇10的下一个簇是簇17。 |
11 | 0x00000012 | 表示簇11的下一个簇是簇18。 |
12 | 0x00000013 | 表示簇12的下一个簇是簇19。 |
13 | 0x0FFFFFFF | 簇13是文件结束 (EOF)。这是文件占用的最后一个簇。 |
14 | 0x00000000 | 簇14是空闲的 (Free),可以被分配。 |
15 | 0x0FFFFFF7 | 簇15是坏簇 (Bad),不会被使用。 |
... | ... | ... |
簇链示例: 如果一个文件的簇链是 10 -> 11 -> 12 -> 13 (EOF)
,那么它在FAT表中的记录就如上表所示。
3. 目录项 (Directory Entry) 详解
无论是根目录还是子目录,其内容都是由32字节的目录项组成的数组。
标准 8.3 格式目录项 (32字节)
偏移 | 长度 | 字段名 | 示例值 (Hex) | 说明 |
---|---|---|---|---|
0x0 | 8 | 文件名 (Name) | ```'F', 'I', 'L', 'E', ' ', ' ', ' ', ' '```` | 主文件名,不足用空格(0x20)填充。首字节为0xE5 表示已删除,0x00 表示未使用。 |
0x8 | 3 | 扩展名 (Ext) | 'T', 'X', 'T' | 文件扩展名,不足用空格填充。 |
0xB | 1 | 属性 (Attr) | 0x20 | 位掩码: 0x01 (只读), 0x02 (隐藏), 0x04 (系统), 0x08 (卷标), 0x10 (目录), 0x20 (存档) |
0xC | 1 | 保留 | 0x18 | |
0xD | 1 | 创建时间(10ms) | 0x7B | 以10毫秒为单位的创建时间(0-199)。 |
0xE | 2 | 创建时间 | 0x6C66 | 格式: HH(5)MM(6)SS/2(5) -> HH:MM:SS |
0x10 | 2 | 创建日期 | 0x4A4D | 格式: (Y-1980)(7)M(4)D(5) -> YYYY-MM-DD |
0x12 | 2 | 最后访问日期 | 0x4A4D | 格式同创建日期。 |
0x14 | 2 | 起始簇号高16位 | 0x0000 | 文件/目录起始簇号的高位字。 |
0x16 | 2 | 最后修改时间 | 0x6C66 | 格式同创建时间。 |
0x18 | 2 | 最后修改日期 | 0x4A4D | 格式同创建日期。 |
0x1A | 2 | 起始簇号低16位 | 0x000A | 文件/目录起始簇号的低位字。 |
0x1C | 4 | 文件大小 | 0x00004000 | 文件的实际字节数(字节)。对于目录,此项为0。 |
-
重要: 起始簇号 = (起始簇号高16位 << 16) + 起始簇号低16位 =
0x0000000A
(簇10)。
长文件名 (LFN) 目录项 (32字节)
当文件名不符合“8.3”格式时(如Long File Name.txt
),系统会创建附加目录项放在标准项之前。
偏移 | 长度 | 字段名 | 示例值 (Hex) | 说明 |
---|---|---|---|---|
0x0 | 1 | 序列号 (Ordinal) | 0x43 | 第4项,且是最后一项(0x40 )。 |
0x1 | 10 | 名字部分1 (Name1) | 0x004C... | UTF-16编码的Unicode字符(5个)。 |
0xB | 1 | 属性 (Attr) | 0x0F | 总是0x0F ,表示这是一个LFN项。 |
0xC | 1 | 类型 | 0x00 | 总是0。 |
0xD | 1 | 校验和 | 0x4A | 根据短文件名计算出的校验和。 |
0xE | 12 | 名字部分2 (Name2) | 0x0066... | UTF-16编码的Unicode字符(6个)。 |
0x1A | 2 | 总是零 | 0x0000 | |
0x1C | 4 | 名字部分3 (Name3) | 0x0065... | UTF-16编码的Unicode字符(2个)。 |
LFN工作方式: LFN项是倒序存放的。操作系统会收集所有序列号最高位为0的LFN项,按顺序拼接出完整的长文件名,最后找到对应的短名标准项来获取起始簇号和大小。
4. 数据区:簇的内容
情况A:簇存储文件数据 (例如簇10, 11, 12, 13)
簇10的偏移 | 数据 (Hex) | 说明 (对应文件内容) |
---|---|---|
0x0000 | 48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 0D 0A | "Hello, World!\r\n" (文本开头) |
... | ... (更多数据) ... | 文件的后续内容 |
0x1FFF | ... | 簇10的最后一个字节 |
-
注意: 文件结束由目录项中的文件大小决定,而不是簇内的特殊标记。簇13可能只有部分数据有效,后面是垃圾数据。
情况B:簇存储目录内容 (例如簇2,根目录)
目录项偏移 | 数据内容 (简化表示) | 类型 | 解析 |
---|---|---|---|
0x0000 | . ... clust=2... | 自引用项 | 当前目录是簇2。 |
0x0020 | .. ... clust=0... | 父目录项 | 父目录是簇0(根目录的父目录是它自己)。 |
0x0040 | LONGFI~1TXT ... clust=10 size=16384 | 标准短名项 | 文件LONGFI~1.TXT ,起始簇10,大小16KB。 |
0x0060 | [LFN Entry for Part 2] | LFN项 | 长文件名的一部分。 |
0x0080 | [LFN Entry for Part 1] | LFN项 | 长文件名Long File Name.txt 的第一部分。 |
0x00A0 | SUBDIR ... clust=15 size=0 | 标准目录项 | 一个名为SUBDIR 的子目录,起始簇15。 |
... | ... (剩余可能是空白或更多项) ... |
二.如何判断一个簇里面存取的是数据还是目录
你无法单独通过查看一个簇本身或它在FAT表中的条目来确认它是“项目簇”(目录簇)还是“数据簇”(文件数据簇)。
它们的物理存储格式完全相同,都是512字节 * 每簇扇区数的数据块。区分一个簇类型的唯一方法,是查看指向它的那个“目录项(Directory Entry)”中的“属性(Attribute)”字段。
一个目录里面包含的所有东西(无论是子目录还是文件),都统一用一种方式记录:即32字节的“目录项(Directory Entry)”。
这些目录项就像是一个表格里的一行行记录,紧密地排列在这个目录所占据的数据簇里。区分一条记录是“子目录”还是“文件”,完全取决于该目录项中那个至关重要的“属性(Attribute)”字节。
FAT表的表项和簇中存储的数据在物理上是完全分开的,它们不是紧挨着存放的。
你可以把FAT表想象成一本目录,而把数据区想象成图书馆的藏书库。目录(FAT表)里只写着“这本书的下本书的编号是XXX”,它本身并不包含书的内容。
所以,一个簇的“后面”并不是直接跟着FAT表的32位项。正确的逻辑关系是:
-
FAT表:存储在磁盘上一个固定的、集中的区域(在保留扇区之后)。它只是一个巨大的“簇号-下一簇号”的映射表。
-
簇:存储在磁盘上另一个巨大的区域,叫做数据区(在FAT表之后)。这里存放的才是文件和目录的实际内容。
当一个簇被分配用于存储时,它后面跟着的数据取决于这个簇的用途:
情况一:簇用于存储文件数据
这是最常见的情况。簇的内容就是文件本身的原始二进制数据,没有任何额外的头或尾。
-
示例:假设一个文本文件
hello.txt
的内容是:"Hello, World!"
(13字节),并且它被存储在簇号200中,簇大小为4KB(4096字节)。 -
那么簇200的内容将是:
偏移 (在簇内) 数据 (十六进制) 数据 (ASCII/说明) 0x0000 48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21
H e l l o , W o r l d !
0x000D 00 00 00 ... (直到4096字节)
未使用的空间,内容是随机的、无效的旧数据 -
关键点:
-
文件系统不会在数据簇的开头或结尾添加文件结束符(EOF)或文件名等信息。
-
文件的有效长度不是由簇内的任何标记决定的,而是由该文件目录项中那个4字节的“文件大小”字段精确定义的。系统读取时,读够这个字节数就会停止,不管簇里还剩多少空间。
-
情况二:簇用于存储目录(文件夹)
当一个簇被分配给一个目录时,它的内容就有了一个非常严格的结构。它不再是无结构的二进制流,而是一个由32字节的“目录项”(Directory Entry) 组成的数组。
-
示例:假设簇号300被分配给了
MyFolder
这个目录。 -
那么簇300的内容将是:
偏移 (在簇内) 目录项内容 (简化表示) 类型与说明 0x0000 .
(Attr=目录, 起始簇=300)特殊项: 指向当前目录自身。 0x0020 ..
(Attr=目录, 起始簇=100)特殊项: 指向父目录(这里是簇100)。这是实现嵌套的关键。 0x0040 FILE1 TXT
(Attr=存档, 起始簇=401, 大小=1500)标准项: 描述一个名为 FILE1.TXT
的文件。0x0060 SUBDIR
(Attr=目录, 起始簇=500, 大小=0)标准项: 描述一个名为 SUBDIR
的子目录。0x0080 LFN Entry
(Attr=长文件名)长名项: A Long File Name.txt
的一部分。0x00A0 ALONG~1TXT
(Attr=存档, 起始簇=402, 大小=2000)标准项: 对应上面长名的短名项。 ... ...
更多文件或子目录项... 0x1FE0 00 00 00 ...
未使用的目录项(首字节为0x00)。
fat32格式查找数据流程图
下面是读取写入tf卡通用的开源模块
https://elm-chan.org/fsw/ff/