打印

【转】FAT32文件操作系统

[复制链接]
1063|4
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
风萧寒|  楼主 | 2016-12-21 19:35 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

FAT32文件系统  

FAT32文件系统的存储机制及其在单片机上的实现 based on a SD card。

FAT32文件系统您一定不会陌生,最多看到它是在windows操作系统里,但在一些嵌入式产品(如手机、MP3、MP4等)中,也能看到它的身影。从某种意义上来讲,FAT32文件系统是非常成功的,使我们可以脱离底层储存设备驱动,更为方便高效地组织数据。给单片机系统中的大容量存储器(如SD卡、CF卡、硬盘等)配以FAT32文件系统,将是非常有意义的(如创建的数据文件可以在windows等操作系统中直接读取等)。

FAT32本身是比较复杂的,对其进行讲解的最好方法就是实际演练。笔者手里持有一张刚以FAT32格式化的SD卡,我们就围绕它来讲解FAT32的实现机理。

FAT32分为几个区域,这里将用实例的方法对它们的结构与在文件存储中的功能进行详细的剖析。

实例说明

此实例首先在一张空的SD卡(已被格式化为FAT32格式)上创建一个文本文件,并在其中输入20个字符。再将它插入到单片机系统中,实现对这个文件的读取,将文件内容输出在调试终端上。

实现过程

格式化与创建文件

Windows上的磁盘格式化与文件创建就不用多说了。如下图:

1)、主引导扇区的结构

要注意,硬盘的磁头和柱面都是从0开始数的,但是扇区是从1开始数的,通常我们硬盘上的这个0磁头、0柱面、1扇区称作第一个物理扇区,一般我们把这个主引导扇区分成三部分,第一部分叫MBR(Master Boot Rocord),主引导记录,这部分有446个字节,从0--445(0x00--0x1BD);另一部分叫做DPT(Disk Partition Table),磁盘分区表,占这个扇区中的其余64个字节,从446--509(0x1BE--0x1FD);第三部分是一个结束标志,占两个字节,从510--511(0x1FE--0x1FF),其正常内容应该是0xAA55(0x55在低地址字节)。

主引导记录中一般有启动代码和数据,使用不同的bootloader,启动代码可能是不同的,这一系列**将分析DOS下的启动代码和GRUB的启动代码,大家可以比较其中的差异。

不管用什么boot loader,其分区表的结构都是一样的,为了后面**叙述的方便,我们在这里做一个简单的介绍,分区表可以容纳4个分区的信息,每个分区信息占16个字节,其结构如下:

字节偏移
说明
0
引导标志。若值为80H表示活动分区,若值为00H表示非活动分区。
1-3
本分区的起始磁头号、扇区号、柱面号。其中:磁头号--第1字节;扇区号--第2字节的低6位;柱面号—为第2字节高2位+第3字节8位
4
分区类型符:
    00H——表示该分区未用(即没有指定);
    01h--FAT12基本分区
    04H--FAT16基本分区
    06H——big FAT16基本分区;
    0BH——FAT32基本分区;
    05H——扩展分区;
    07H——NTFS分区;
    0FH——(LBA模式)扩展分区(83H为Linux分区等)
5-7
本分区的结束磁头号、扇区号、柱面号。其中:
    磁头号——第1字节;
    扇区号——第2字节的低6位;
    柱面号——第2字节的高2位+第3字节
8-11
分区起始扇区数,指分区相对于记录该分区的分区表的扇区位置之差 (该分区表:LBA=0x0)
12-15
本分区的总扇区数

2)DBR(DOS BOOT RECORD 操作系统引导记录区)

DBR是我们进军FAT32的首道防线。其实DBR中的BPB部分才是这一区域的核心部分(第12~90字节为BPB),只有深入详实的理解了BPB的意义,才能够更好的实现和操控FAT32。关于DBR在FAT32中的地位就不多说了,以下面实际的DBR内  图所示:

上面的数据看起来杂乱不堪,无从下手,其实对我们有用的数据只不过90个字节(如图中彩色线标记的字节)。仅仅是这90个字节就可以告诉我们关于磁盘的很多信息,比如每扇区字节数、每簇扇区数、磁道扇区数等等。对于这些信息的读取,只要遵循DBR中的字段定义即可。(比如图中紫色字段的两个字节表示这张磁盘的每一个扇区有512个字节,具体的计算方法见下文)

字段定义如下表(BPB后面的422个字节对我们的意义不大,表中省略):

字段名称
长度
含义
偏移量
jmpBoot
3
跳转指令
0
OEMName
8
这是一个字符串,标识了格式化该分区的操作系统的名称和版本号
3
BytesPerSec
2
每扇区字节数
11
SecPerClus
1
每簇扇区数
13
RsvdSecCnt
2
保留扇区数目
14
NumFATs
1
此卷中FAT表数
16
RootEntCnt
2
FAT32为0
17
TotSec16
2
FAT32为0
19
Media
1
存储介质
21
FATSz16
2
FAT32为0
22
SecPerTrk
2
磁道扇区数
24
NumHeads
2
磁头数
26
HiddSec
4
FAT区前隐扇区数
28
TotSec32
4
该卷总扇区数
32
FATSz32
4
FAT表扇区数
36
ExtFlags
2
FAT32特有
40
FSVer
2
FAT32特有
42
RootClus
4
根目录簇号
44
FSInfo
2
文件系统信息
48
BkBootSec
2
通常为6
50
Reserved
12
扩展用
52
DrvNum
1
64
Reserved1
1
65
BootSig
1
66
VolID
4
67
FilSysType
11
71
FilSysType1
8
82

DBR的实现代码:

struct FAT32_DBR

{

unsigned char BS_jmpBoot[3];    //跳转指令            offset:0

unsigned char BS_OEMName[8]; //                   offset: 3

unsigned char BPB_BytesPerSec[2];//每扇区字节数        offset:11

unsigned char BPB_SecPerClus[1]; //每簇扇区数          offset:13

unsigned char BPB_RsvdSecCnt[2]; //保留扇区数目(从DBR到FAT的扇区数)       offset:14

unsigned char BPB_NumFATs[1];  //此卷中FAT表数     offset:16

unsigned char BPB_RootEntCnt[2]; //FAT32为0           offset:17 根目录最多容纳的目录相数,FAT12/16位512

unsigned char BPB_TotSec16[2];   //FAT32为0           offset:19 扇区总数小于32M时使用

unsigned char BPB_Media[1];    //存储介质            offset:21 0XF8本地硬盘固定0XF0移动存储介质

unsigned char BPB_FATSz16[2];   //FAT32为0         offset:22 每个FAT表的大小扇区数

unsigned char BPB_SecPerTrk[2];  //磁道扇区数          offset:24

unsigned char BPB_NumHeads[2];   //磁头数            offset:26

unsigned char BPB_HiddSec[4];    //隐扇区数(从MBR到DBR的扇区数)    offset:28 分区前已使用扇区数

unsigned char BPB_TotSec32[4];   //该卷总扇区数        offset:32

unsigned char BPB_FATSz32[4];    //一个FAT表扇区数   offset:36

unsigned char BPB_ExtFlags[2];   //FAT32特有           offset:40

unsigned charBPB_FSVer[2];      //FAT32特有          offset:42 版本号

unsigned char BPB_RootClus[4];   //根目录簇号          offset:44

unsigned charFSInfo[2];         //保留扇区FSINFO扇区数offset:48

unsigned char BPB_BkBootSec[2];  //通常为6           offset:50

unsigned char BPB_Reserved[12];  //扩展用             offset:52

unsigned char BS_DrvNum[1];     //                  offset:64

unsigned char BS_Reserved1[1];  //                   offset:65

unsigned charBS_BootSig[1];    //                   offset:66

unsigned charBS_VolID[4];      //                   offset:67

unsigned char BS_FilSysType[11]; //                offset:71

unsigned char BS_FilSysType1[8];//"FAT32    "        offset:82

};



相关帖子

沙发
风萧寒|  楼主 | 2016-12-21 19:36 | 只看该作者

前面我们在定位MBR和DBR时,都是用的绝对扇区号,那么现在可以说什么是逻辑扇区号了,我们看到,一个分区的第一个扇区是DBR分区引导记录,那么这个扇区就是这个分区里逻辑扇区号为0的扇区,我们可以推算出,DBR所在的扇区的绝对扇区号是隐藏扇区数(bpb_hiddenSec)这个字段,所有我们有如下公式:

    绝对扇区号 = 逻辑扇区号 + 隐藏扇区数

    下面我们再说DBR中的内容就不会再使用绝对扇区号,而是用逻辑扇区号了。

    FAT起始扇区号 = 保留扇区数(bpb_reservedSec)+隐扇区数(BPB_HiddSec从MBR到DBR的扇区数)
    根目录起始扇区号 =FAT起始扇区号 + FAT个数(bpb_numFAT) * 每个FAT的扇区数(bpb_secPerFAT)
    数据区起始扇区号 =根目录起始扇区号 + (32 X 根目录目录项数(bpb_rootEntry)) / 每扇区字节数(bpb_bytesPerSector)

在程序中我们采用以上的结构体指针对扇区数据指针进行转化,就可以直接读取数据中的某一字段,如要读取BPB_BytesPerSec,可以这样来作:((structFAT32_DBR *)pSector)-> BPB_BytesPerSec

用如上语句就可以得到这一字段的首地址。

心细的读者可能会发现读回来的字节拼在一起,与实际的数据并不吻合。例如BPB_BytesPerSec读出来的内容是“0002”,在程序中我们把00作为int型变量的高字节,把02作为其低字节,那么这个变量的值为2,而实际的SD卡里的扇区大小为512个字节,这512与2之间相去甚远。是什么造成这种现象的呢?

这就是大端模式与小端模式在作怪。上面我们合成int型变量的方法(00为高字节,02为低字节)为小端模式。而如果我们改用大端模式来进行合成的话,结果就会不同:将02作高字节,而把00作低字节,变量值就成了0x0200(十进制的512),这样就和实际数据吻合了。可见FAT32中字节的排布是采用小端模式的。在我们程序中需要将它转为大端模式的表达方式。在笔者的程序有这样一个函数lb2bb,专门垃圾将小端模式转为大端模式,程序如下:

unsigned long lb2bb(unsigned char *dat,unsigned char len)//小端转为大端

{

unsigned long temp=0;

unsigned long fact=1;

unsigned char i=0;

for(i=0;i<len;i++)

{

  temp+=dat*fact;

  fact*=256;

}

return temp;

}

这样就可以从BPB中读出关于磁盘的各种参数信息,为我们后面的工作做准备。而这个从BPB中读取参数装入到参数表中以备后用的过程就是FAT32的初始化了。具体的实现如下:

先定义用来装入从BPB中读取的参数的结构:

struct FAT32_Init_Arg

{

unsigned char BPB_Sector_No;   //BPB所在扇区号

unsigned longTotal_Size;         //磁盘的总容量

unsigned longFirstDirClust;       //根目录的开始簇

unsigned long FirstDataSector;     //文件数据开始扇区号

unsigned int BytesPerSector;      //每个扇区的字节数

unsigned int FATsectors;         //FAT表所占扇区数

unsigned int  SectorsPerClust;    //每簇的扇区数

unsigned long FirstFATSector;     //第一个FAT表所在扇区

unsigned longFirstDirSector;         //第一个目录所在扇区

unsigned long RootDirSectors;     //根目录所占扇区数

unsigned longRootDirCount;        //根目录下的目录与文件数

};

当然也可以用零散的变量来存储参数,但用结构体更方便管理,也会使程序更为整洁。FAT32的初始化将向结构中装入参数,实现如下:

void FAT32_Init(struct FAT32_Init_Arg *arg)

{

struct FAT32_BPB *bpb=(struct FAT32_BPB*)(FAT32_Buffer); //将数据缓冲区指针转为structFAT32_BPB 型指针

arg->BPB_Sector_No  =FAT32_FindBPB();        //FAT32_FindBPB()可以返回BPB所在的扇区号

arg->Total_Size     =FAT32_Get_Total_Size();     //FAT32_Get_Total_Size()可以返回磁盘的总容量,单位是兆

arg->FATsectors     =lb2bb((bpb->BPB_FATSz32)    ,4);   //装入FAT表占用的扇区数到FATsectors中

arg->FirstDirClust  =lb2bb((bpb->BPB_RootClus)   ,4);      //装入根目录簇号到FirstDirClust中

arg->BytesPerSector =lb2bb((bpb->BPB_BytesPerSec),2);    //装入每扇区字节数到BytesPerSector中

arg->SectorsPerClust=lb2bb((bpb->BPB_SecPerClus) ,1);      //装入每簇扇区数到SectorsPerClust 中

arg->FirstFATSector=lb2bb((bpb->BPB_RsvdSecCnt),2)+arg->BPB_Sector_No;

//装入第一个FAT表扇区号到FirstFATSector 中保留扇区数+BPB所在扇区

arg->RootDirCount   =lb2bb((bpb->BPB_RootEntCnt) ,2);  //装入根目录项数到RootDirCount中

arg->RootDirSectors  =(arg->RootDirCount)*32>>9;  //装入根目录占用的扇区数到RootDirSectors中除以512=29

arg->FirstDirSector=(arg->FirstFATSector)+(bpb->BPB_NumFATs[0])*(arg->FATsectors);

//装入第一个目录扇区到FirstDirSector中

arg->FirstDataSector=(arg->FirstDirSector)+(arg->RootDirSectors);

//装入第一个数据扇区到FirstDataSector中

}

3)FAT(文件分配表)

   FAT表是FAT32文件系统中用于磁盘数据(文件)索引和定位引进的一种链式结构。可以说FAT表是FAT32文件系统最有特色的一部分,它的链式存储机制也是FAT32的精华所在,也正因为有了它才使得数据的存储可以不连续,使磁盘的功能发挥得更为出色。

   FAT表到底在什么地方?它到底是什么样子的呢?

  从第一步从BPB中提取参数中的FirstFATSector就可以知道FAT表所在的扇区号。其实每一个FAT表都有另一个与它一模一样的FAT存在,并且这两个FAT表是同步的,也就是说对一个FAT表的操作,同样地,也应该在另一个FAT表进行相同的操作,时刻保证它们内容的一致。这样是为了安全起见,当一个FAT因为一些原因而遭到破坏的时候,可以从另一个FAT表进行恢复。

FAT表的内容如下图所示:

上图就是一个实际的FAT表。前8个字节“F8 FF FF 0F FF FF FF FF”为FAT32的FAT表头标记,用以表示此处是FAT表的开始。后面的数据每四个字节为一个簇项(从第2簇开始),用以标记此簇的下一个簇号。

拿我们创建的那个叫TEST.TXT(大小为20个字节)的文件来说,如果这个文件的开始簇为第2簇的话,那么就到FAT表里来查找,看文件是否有下一个簇(如果文件大小大于一个簇的容量,必须会有数据存储到下一个簇,但下一个簇与上一个簇不一定是连续的),可以看到“簇2”的内容为“FF FF FF 0F”,这样的标记就说明这个文件到第2簇就已经结束了,没有后继的簇,即此文件的大小是小于一个簇的容量的。

上面讲了很多,都是围绕簇这样一个词来讲的,簇又是什么?为什么要将它引入到FAT32里来呢?

磁盘上最小可寻址存储单元称为扇区,通常每个扇区为512个字节。由于多数文件比扇区大得多,因此如果对一个文件分配最小的存储空间,将使存储器能存储更多数据,这个最小存储空间即称为簇。根据存储设备(磁盘、闪卡和硬盘)的容量,簇的大小可以不同以使存储空间得到最有效的应用。在早期的360KB磁盘上,簇大小为2个扇区(1,024字节);第一批的10MB硬盘的簇大小增加到8个扇区(4,096字节);现在的小型闪存设备上的典型簇大小是8KB或16KB。2GB以上的硬盘驱动器有32KB的簇。如果对于容量大的存储定义了比较小的簇的话,就会使FAT表的体积很大,从而造成数据的冗余和效率的下降。

需要指出的是,簇作为FAT32进行数据存储的最小单位,内部扇区是不能进一步细分的,即使一个文件的数据写到一个簇中后,簇中还有容量的剩余(里部扇区没有写满),哪怕这个簇只写了一个字节,其它文件的数据也是不能接在后面继续数据的,而只能另外找没有被占用的簇。

我们按照初始化参数表中的SectorsPerClust可以知道一个簇中的扇区数,笔者的SD卡实测簇大小为4个扇区,按照上面的说法,TEST.TXT这样一个只有20个字节的文件,也会占用一个簇的容量,让我们在Windows里看看它的实际占用空间的情况。如左图:

从左图可以看到文件大小为20个字节,但占用空间却是2048个字节(一个簇的容量,4个扇区)。TEST.TXT容量只有20个字节,所以只占用了一个簇,可能FAT表中还看不出链式结构,现在我们再创建一个文件,使它占用26个簇,如下: 

可以看到图中红色标记的就是文件所占用的26个簇。从第4簇开始,簇项4的内容为“05 00 00 00”(小端模式),说明下一个簇为第5簇,而簇项5的内容为“06 00 00 00”,说明下一个簇为第6簇……依此类推,直到内容为“FF FF FF 0F”,说明无后继簇,文件数据到此结束。FAT表中的链式存储结构已经非常明显。把我们从FAT表中分析的结果与Windows的统计结束进行对比,说明我们的解理是正确的,如下图:

从上面可以看到,当数据结束于某一簇时,FAT32就用“FF FF FF 0F”来对其进行标记。其实还有其它的标记以表达其它的簇属性,如“00 00 00 00 ”表示未分配的簇,“FFFF FF F7”表示坏簇等。

给出一个簇号,计算出它的后继簇号,是实现FAT32的重点,实现如下:

unsigned long FAT32_GetNextCluster(unsigned longLastCluster)

{

unsigned long temp;

struct FAT32_FAT *pFAT;

struct FAT32_FAT_Item *pFAT_Item;

temp=((LastCluster/128)+Init_Arg.FirstFATSector);  

//计算给定簇号对应的簇项的扇区号

FAT32_ReadSector(temp,FAT32_Buffer);

pFAT=(struct FAT32_FAT *)FAT32_Buffer;

pFAT_Item=&((pFAT->Items)[LastCluster%128]);

//在算出的扇区中提取簇项

return lb2bb(pFAT_Item,4);//返回下一簇号

}

那么FAT表有多大呢?FAT表中每四个字节表示一个簇,所以FAT表的大小由实际的簇数来决定。从这里也可以看出,如果簇过大,就会则FAT表比较小,但会造成空间的浪费,而如果簇过小,可以减小空间的浪费,但会使FAT表变得臃肿。FAT表的大小也可以从BPB参数FATsectors读出。从上面的BPB图可以得知笔者的SD卡的FAT表大小为958个扇区(“BE030000”的大端表示)。如果这958个扇区每四个字节都表示一个簇项,则它可以表示(958*512/4)-2= 122622个簇(减去2是因为有8个字节的FAT表头标识。

看看我们计算的是否正确呢,下面是Winhex计算出来的簇数:

与Winhex计算的结果是吻合的,我们对FAT表与簇的理解是正确的。

看完上面对FAT表的讲解中,你可能会问:一个文件数据的首簇号怎样来确定呢?只有知道了一个文件数据的首簇号才能继续查找下一簇数据的位置,直到数据结束。下面将要讲到的“根目录区”就可以由一个文件的文件名来查到它的首簇。


使用特权

评论回复
板凳
风萧寒|  楼主 | 2016-12-21 19:37 | 只看该作者

4)根目录区

在FAT32中其实已经把文件的概念进行扩展,目录同样也是文件,从根目录的地位与其它目录是相同的,因此根目录也被看作是文件。既然是文件就会有文件名,根目录的名称就是磁盘的卷标。如笔者的SD卡在格式会时设置卷标为znmcu,则根目录的名称就为ZNMCU,如下图:

        每一个文件都对应一个描述它属性的结构,定义如下:

FAT32文件目录项32个字节的定义
字节偏移量
字数量
定义
0~7
8
文件名
8~10
3
扩展名



11



1

属性字节 
0x00   (读写)
0x01   (只读)
0x02   (隐藏)
0x04   (系统)
0x08   (卷标)
0x10   (子目录)
0x20   (归档)
12
1
系统保留
13
1
创建时间的10毫秒位
14~15
2
文件创建时间
16~17
2
文件创建日期
18~19
2
文件最后访问日期
20~21
2
文件起始簇号的高16 位
22~23
2
文件的最近修改时间
24~25
2
文件的最近修改日期
26~27
2
文件起始簇号的低16 位
28~31
4
表示文件的长度

根目录区所在扇区可从BPB参数FirstDirSector获取,从BPB图得FirstDirSector=FirstFATSector+BPB_NumFATs*FATsectors=2053。根目录区的初始大小为一个簇,实际的内容如下:

图中的记录1描述根目录,前八个字节为文件名“ZNMCU   ”(长度小于8的部分用空格符补齐),下面的三个字节为扩展名“   ”(长度小于3的部分用空格符补齐),08表示此文件为卷标,开始簇高字节为0000,低字节为0000,开始簇为0,文件长度为0。

记录2描述TEST.TXT文件,文件名为“TEST    ”,扩展名为“TXT”,20表示此文件为归档,开始簇为3(“00000003”),长度为20。 0X0014

记录3描述BIGTEST.TXT文件,文件名为“BIGTES~1”,扩展名为“TXT”,开始簇为4,长度为52000字节(0000CB20)。

可以看到FAT32中的文件名都以大写字母表示,长度不足的部分用空格符补齐,所以我们要读取的文件TEST.TXT就变成了“TEST   .TXT”,这将有助于文件名的匹配,我们不用去处理不等长文件名所带来的麻烦。另外,还会发现长度过长的部分会被~1所替换,如果替换后有文件与之重名,则~后面的数字将增加为2。

文件目录项结构的实现如下:

struct direntry

{

unsigned chardeName[8];       // 文件名

unsigned char deExtension[3];    // 扩展名

unsigned char deAttributes;     // 文件属性

unsigned char deLowerCase;         // 系统保留

unsigned char deCHundredth;         // 创建时间的10 毫秒位

unsigned char deCTime[2];           // 文件创建时间

unsigned char deCDate[2];    // 文件创建日期

unsigned char deADate[2];    // 文件最后访问日期

unsigned char deHighClust[2];  // 文件起始簇号的高16 位

unsigned char deMTime[2];          // 文件的最近修改时间

unsigned char deMDate[2];           // 文件的最近修改日期

unsigned char deLowCluster[2];// 文件起始簇号的低16 位

unsigned char deFileSize[4];   // 表示文件的长度

}

我们最终要实现的是对TEST.TXT文件的读取,须要作到给定文件名后,可以得到相应文件的首簇。主要的思想就是对根目录区中(本实例只针对根目录中的文件进行读取,至于多级子目录的实现,只须要进行多次首簇定位)的记录进行扫描,对记录中的文件名进行匹配。具体的实现如下:

struct FileInfoStruct * FAT32_OpenFile(char *filepath)

{

unsigned char depth=0;

unsigned char i,index=1;

unsigned long iFileSec,iCurFileSec,iFile;

struct direntry *pFile;

iCurFileSec=Init_Arg.FirstDirSector;

for(iFileSec=iCurFileSec;

iFileSec<iCurFileSec+(Init_Arg.SectorsPerClust);

iFileSec++)

{

  FAT32_ReadSector(iFileSec,FAT32_Buffer);

  for(iFile=0;

iFile<Init_Arg.BytesPerSector;

iFile+=sizeof(struct direntry))//对记录逐个扫描

  {

   pFile=((struct direntry*)(FAT32_Buffer+iFile));

  if(FAT32_CompareName(filepath+index,pFile->deName))//对文件名进行匹配

   {

   FileInfo.FileSize=lb2bb(pFile->deFileSize,4);

strcpy(FileInfo.FileName,filepath+index);

FileInfo.FileStartCluster=lb2bb(pFile->deLowCluster,2)+lb2bb(pFile->deHighClust,2)*65536;

FileInfo.FileCurCluster=FileInfo.FileStartCluster;

FileInfo.FileNextCluster=FAT32_GetNextCluster(FileInfo.FileCurCluster);

FileInfo.FileOffset=0;

return &FileInfo;

   }

  }

}

}

这个函数在找到目标文件后,会将此文件的一些参数信息装入到文件结构中,为以后的文件读取作好准备。文件结构如下:struct FileInfoStruct

{ unsigned char FileName[12];        //文件名

unsigned long  FileStartCluster;//文件首簇号

unsigned long  FileCurCluster;//文件当前簇号

unsigned long  FileNextCluster;//下一簇号

unsigned long  FileSize;     //文件大小 

unsigned char FileAttr;      //文件属性

unsigned short FileCreateTime;   //文件建立时间

unsigned short FileCreateDate;    //文件建立日期

unsigned short FileMTime;//文件修改时间

unsigned short FileMDate;//文件修改日期

unsigned long  FileSector;  //文件当前扇区

unsigned int   FileOffset;   //文件偏移量

};

通过对根目录区的扫描,可以得到TEST.TXT首簇为3,下面就可能以它为起点,来读取文件内容了。

5)文件读取

通过上面的讲解,我们已经得到了TEST.TXT的首簇。现在要做的就是到相应的簇及其后继簇去读取数据了。一直都在说簇,比如第2簇、第3簇等等。那这些簇在磁盘的什么位置呢?从FAT表中可以看到,簇号是从2开始的,而第2簇的位置就在第二个FAT表(一共有两个FAT表,它们即时同步)的后面,即根目录所在的簇就为第2簇。

下面就为本篇教程的最后部分,读TEST.TXT文件的内容。主要思想是这样的:在已各文件首簇的前提下,从首簇开始,对于文件满一簇的数据,就把整簇数据读出(其实还是按扇区来读,只是一次性读出所有扇区),对于文件结尾不足一簇的部分,计算它占用了簇内几个扇区,把占用整个扇区部分直接按扇区读出,而最后很有可能是零散的若干个字节,不足一个扇区,即占用了最后一个此文件最后一个扇区的一部分,对于这部分我们也要将整个扇区读出,截选中有效的数据部分。文件信息结构中的FileOffset参数将时刻记录文件读到的位置,它与文件大小的差就是还未读取的数据数量。

具体的实现如下:

void FAT32_ReadFile(struct FileInfoStruct *pstru,unsignedlong len)

{

unsigned longSub=pstru->FileSize-pstru->FileOffset;

unsigned long iSectorInCluster=0;

unsigned long i=0;

while(pstru->FileNextCluster!=0x0fffffff) //如果FAT中的簇项为0x0fffffff,说明无后继簇

{

for(iSectorInCluster=0;iSectorInCluster<Init_Arg.SectorsPerClust;iSectorInCluster++)  //读出整簇数据

  {

FAT32_ReadSector((((pstru->FileCurCluster)-2)*(Init_Arg.SectorsPerClust))+Init_Arg.FirstDataSector

+(iSectorInCluster),FAT32_Buffer);

  pstru->FileOffset+=Init_Arg.BytesPerSector;

   Sub=pstru->FileSize-pstru->FileOffset;

   for(i=0;i<Init_Arg.BytesPerSector;i++)

   {   send(FAT32_Buffer);   //将数据发送到终端上显示   }

  }

pstru->FileCurCluster=pstru->FileNextCluster;

pstru->FileNextCluster=FAT32_GetNextCluster(

pstru->FileCurCluster);   //这里是FAT簇链的传递

}

iSectorInCluster=0;

while(Sub>=Init_Arg.BytesPerSector)  //处理不足一簇,而足扇区的数据

{

FAT32_ReadSector((((pstru->FileCurCluster)-2)*(Init_Arg.SectorsPerClust))+Init_Arg.FirstDataSector

+(iSectorInCluster++),FAT32_Buffer);

  pstru->FileOffset+=Init_Arg.BytesPerSector;

  Sub=pstru->FileSize-pstru->FileOffset;

  for(i=0;i<Init_Arg.BytesPerSector;i++)

  {   send(FAT32_Buffer);  }

}

FAT32_ReadSector((((pstru->FileCurCluster)-2)*(Init_Arg.SectorsPerClust))+Init_Arg.FirstDataSector

+(iSectorInCluster),FAT32_Buffer); //读取最后一个扇区

for(i=0;i<Sub;i++)    //Sub为最后剩余的字节数

{  send(FAT32_Buffer);}     

}


使用特权

评论回复
地板
风萧寒|  楼主 | 2016-12-21 19:38 | 只看该作者

6)最终的实现

在主函数中对磁盘驱动及以上函数进行正确合理的调用,就可以达到我们要实现的效果了。主函数如下:

#include <reg51.h>

#include <string.h>

#include <stdio.h>

#include <math.h>

#include <uart.h>

#include <fat32.h>

void main()

{

delay(10000);

UART_Init();  //串口初始化,用以向调试终端发送数据

send_s("yahoo!!!"); //发送一个测试字符串

MMC_Init(); //SD卡初始化

delay(10000);

MMC_get_volume_info();   //获得SD卡相关信息,输出到终端

FAT32_Init(&Init_Arg);        //FAT32文件系统初始化,装入参数

Printf("BPB_Sector_No" ,Init_Arg.BPB_Sector_No);

Printf("Total_Size"    ,Init_Arg.Total_Size   );

Printf("FirstDirClust" ,Init_Arg.FirstDirClust);

Printf("FirstDataSector",Init_Arg.FirstDataSector);

Printf("BytesPerSector",Init_Arg.BytesPerSector);

Printf("FATsectors"    ,Init_Arg.FATsectors);

Printf("SectorsPerClust",Init_Arg.SectorsPerClust);

Printf("FirstFATSector",Init_Arg.FirstFATSector);

Printf("FirstDirSector",Init_Arg.FirstDirSector);   //以上几个语句用以输出参数值到终端

Printf("FAT32_OpenFile" ,(FAT32_OpenFile("\\TEST.TXT"))->FileSize);//打开根目录下的TEST.TXT文件并输出文件大小

FAT32_ReadFile(&FileInfo);  //读取文件数据,输出到终端

while(1);

}

最终实现的效果如上图所示:

    至此对于FAT32文件系统根目录下的文件读取就已经实现了,至于多级子目录结构可以像查找文件的首簇一样查找某一级目录名的首簇,然后再到此簇下去找下一级目录的首簇,直到最终的文件。

AAAAAAAA

FAT32系统一簇对应8个逻辑相邻的扇区,理论上,这种用法所能管理的逻辑盘容量上限为16TB(16384GB),容量大于16TB时,可以用一簇对应16个扇区,依此类推。FAT16系统在逻辑盘容量介于128MB到256MB时,一簇对应8个扇区,容量介于256MB到512MB时,一簇对应16个扇区,容量介于512MB到1GB时,一簇对应32个扇区,容量介于1GB到2GB时,一簇对应32个扇区,超出2GB的部分无法使用。显然,对于容量大于512MB的逻辑盘,采用FAT32的簇比采用FAT16的簇小很多,大大减少了空间的浪费。

  但是,对于容量小于512MB的盘,采用FAT32虽然一簇8个扇区,比使用FAT16一簇16个扇区,簇有所减小,但FAT32的FAT表较大,占用空间较多,总数据区被减少,两者相抵,实际并不能增加有效存储空间,所以微软建议对小于512M的逻辑盘不使用FAT32。

另外,对于使用FAT16文件系统的用户提一建议,硬盘分区时,不要将分区(逻辑盘)容量正好设为某一区间的下限,例:将一逻辑盘容量设为1100M(稍大于1024M),则使用时其有效存储容量比分区为950M的一般还少,因其簇大一倍,浪费的空间较多。还有,使用FDISK等对分区指定容量时,由于对1MB的定义不一样(标准的二进制的1MB为1048576B,有的系统将1MB理解为1000000B,1000KB等),及每个分区需从新磁道开始等因素,实际分配的容量可能稍大于指定的容量,亦需注意掌握。

BBBBB

FAT32中根目录区(ROOT区)不再是固定区域、固定大小,可看作是数据区的一部分。因为根目录已改为根目录文件,采用与子目录文件相同的管理方式,一般情况下从第二簇开始使用,大小视需要增加,因此根目录下的文件数目不再受最多512的限制。FAT16文件系统的根目录区(ROOT区)是固定区域、固定大小的,是从FAT区之后紧接着的32个扇区,最多保存512个目录项,作为系统区的一部分。

CCCCC

目录区中的目录项变化较多,一个目录项仍占32字节,可以是文件目录项、子目录项、卷标项(仅根目录有)、已删除目录项、

长文件名目录项等。目录项中原来在DOS下保留未用的10个字节都有了新的定义,全部32字节的定义如下:

(1) 0-- 7字节文件正名。

(2) 8--10字节文件扩展名。

(3) 11字节文件属性,按二进制位定义,最高两位保留未用,0至5位分别是只读位、隐藏位、系统位、卷标位、子目录位、归档位。

(4) 11--13字节仅长文件名目录项用,用来存储其对应的短文件名目录项的文件名字节校验和等。

(5) 13--15字节24位二进制的文件建立时间,其中的高5位为小时,次6位为分钟。

(6) 16--17字节16位二进制的文件建立日期,其中的高7位为相对于1980年的年份值,次4位为月份,后5位为月内日期。

(7) 18--19字节16位二进制的文件最新访问日期,定义同(6)。

(8) 20--21字节起始簇号的高16位。

(9) 22--23字节16位二进制的文件最新修改时间,其中的高5位为小时,次6位为分钟,后5位的二倍为秒数。

(10)24--25字节16位二进制的文件最新修改日期,定义同(6)。

(11)26--27字节起始簇号的低16位。

(12)28--31字节32位的文件字节长度。

其中第(4)至(8)项为以后陆续定义的。对于子目录项,其(12)为零;已删除目录项的首字节值为E5H。在可以使用长文件名的FAT32系统中,文件目录项保存该文件的短文件名,长文件名用若干个长文件名目录项保存,长文件名目录项倒序排在文件短目录项前面,全部是采用双字节内码保存的,每一项最多保存十三个字符内码,首字节指明是长文件名的第几项,11字节一般为0FH,12字节指明类型,13字节为校验和,26--27字节为零。

(三)磁盘文件分配表(FAT)

在介绍FAT之前,我们要先了解有关簇的概念,簇(Cluster)是文件数据区被划分成的具有大小相等的区域用于磁盘文件的计量分配单位。一个簇可能有1、2、4、8、16、32、64或128(必须是2的幂)个扇区构成,但对于一种磁盘系统是其值往往是一定的,比如FAT16中每簇一般为32K(占64个扇),FAT32中每簇一般为4K(占8个扇)。要理解操作系统为什么要用簇对磁盘进行管理,还是得学习FAT,FAT32磁盘中在系统引导区之后有一些保留未用的扇区,再后面有一个以F8HFFH FFH 0FH 开始的FAT表,(对于FAT16是以F8H FFH开始的),每个FAT项占32位(4个字节),FAT16的每个FAT项占16位(2个字节),不同的FAT值有不同的含义:

FAT12的表项值    FAT16的表项值    FAT32的表项值               值的含义

000H              0000H            00000000H                  未用的空簇

001H~FEFH        0001H~FFEFH      00000001H~0FFFFFEFH      文件已使用的簇

FF0H~FF6H        FFF0H~FFF6H      0FFFFFF0H~0FFFFFF6H     系统保留簇

FF7H              FFF7H            0FFFFFF7H                坏簇

FFF8H~FFFH       FFF8H~FFFFH     0FFFFFF8H~0FFFFFFFH     文件的最后一簇

注意:在FAT表项中的16进制码总是从高向底排列的。

我们再来看看系统是如何利用FAT表来管理和访问文件的。系统在创建一个新文件时,逐一扫描FAT,跳过已经分配的簇,将该簇分给文件,其簇号作为该文件的起始簇号被放在该文件的文件目录项中(关于文件目录项我们将在后面详谈),如果此文件的大小只需要一个簇就可以放下的话,在该簇对应的FAT项中将放文件最后一簇的标志(一般是FFHFFH FFH F0H,即其值为0FFFFFFFH),如果文件大小一个簇放不下,系统就会在继续寻找FAT表中未用的簇,找到后将该簇的簇号写到上一簇对应的FAT项中,如果此时已经可以存下该文件的数据,系统就会在此簇对应的FAT中记上最后簇的标志,否则就继续找下一空簇......

也就是说FAT和簇是一一对应的关系,对于FAT32的FAT来说每4个字节为1个FAT项(对于FAT16的FAT每2个字节为一个FAT项),从0~N个FAT项分别对应0~N个簇,在我们对文件进行访问时,总是先访问文件的目录项,找到首簇簇号,再找到该簇号对应的FAT项,在其中找到下一簇的簇号,再在下一簇对应的FAT项中找到再下一簇的簇号......一直到在FAT项中找到有文件最后一簇的标志,我们对该文件的查找才结束。(当然也可能在首簇对应的FAT中该文件就结束了)这样就形成了一个链,我们把它称为盘簇链。

一个问题:到底每簇占多少扇区合适?文件所占簇数为:文件占用的簇数=[文件长度/每簇所占空间]取整+1文件在最后一个簇存放的时候不可能刚好放满,没有放满的空间就浪费了(就好象我们打电话不可能每个电话都打到X分59.99秒,即使你通话时间为X分1秒你也要付出X+1分的电话费),我们可以来计算一下你的FAT系统磁盘空间浪费的大小:浪费的空间大小=文件个数/2*每簇字节数(平均每个文件浪费半个簇的空间)这样看来好象簇越小浪费的空间也越小,但是有个矛盾的地方是簇越小FAT所用的空间就会越大,同时簇小了,簇链就越长,访问文件的时间就会加长,这又是一种资源上的浪费,因此,簇的大小应该是以提高文件的访问时间和充分利用磁盘空间为原则的。

(三)目录项的结构

在第2个FAT表(系统一般有两个同样的FAT表)后我们可以找到该分区的根目录区,在上面有许多目录项(注意:目录项并非根目录才有),让我们先来复习一下FAT16的目录项各个字节的含义:

由32个字节构成:

00H~07H 文件的文件名,其中00H为以下值时有些特定含义:

00H 表项为空表项  E5H 文件已被删除

05H 实际该字节为的值为E5H

08H~0AH 文件的扩展名

0BH 文件属性8位文件属性字节含义如下:

B7~B6 未用

B5 归档位

B4 子目录(代表该文件是一个目录或叫文件夹)

B3 卷标(卷标也解释为一种特殊的文件)

B2 系统文件

B1 隐藏文件

B0 只读文件

0CH~15H FAT16系统保留未用

16H~17H 系统最后修改时间,其中:

16H字节的0~4位是以2秒为增量的秒

16H字节的5~7位和17H字节的0~2位是分钟

17H字节的3~7位是小时

18H~19H 文件最后修改的日期,其中:

18H字节0~4位是天号

18H字节5~7位和19H字节0位是月份

19H字节的1~7位为年号,0~119分别代表1980~2099

1AH~1BH 文件的起始簇号(我们在之前已经介绍了)

1CH~1FH 文件的长度(单位为字节)


使用特权

评论回复
5
风萧寒|  楼主 | 2016-12-21 19:40 | 只看该作者

我们知道用在FAT16系统下的文件名有一些弊端,如:文件名最多只能有8个字符(或4个汉字),扩展名最多有3个字符,不分大小写,不能用一些特殊字符等。在FAT32系统这些问题已经得到解决,我们来看看FAT32系统是如何解决长文件名问题的。假如在你的电脑中有一个文件名为abcdefghijklmnopqrstuvwxyz111111.txt的文件名,那么该文件在磁盘目录中就占用了4个以32字节为单位的目录项,其中有3个目录项是用来描述长文件名的,有1个目录项是用来兼容老的FAT系统的,我们来看看例子中前3个用于描述长文件名的目录项:

43H 31H 00H 31H 00H 31H 00H 31H 00H 31H 00H 0FH 00H 27H31H 00H

2EH 00H 74H 00H 78H 00H 74H 00H 00H 00H 00H 00H FFH FFHFFH FFH

02H 6EH 00H 6FH 00H 70H 00H 71H 00H 72H 00H 0FH 00H 27H73H 00H

74H 00H 75H 00H 76H 00H 77H 00H 78H 00H 00H 00H 79H 00H7AH 00H

01H 61H 00H 62H 00H 63H 00H 64H 00H 65H 00H 0FH 00H 27H66H 00H

67H 00H 68H 00H 69H 00H 6AH 00H 6BH 00H 00H 00H 6CH 00H6DH 00H

不难看出描述长文件名的目录项中的一些规则:

在每个目录项的32个字节中,(1)、偏移0H处:代表了长文件描述目录项的序号,其中高4位如果为0100则表示此项为最后一个目录项,低4位表示此长文件名的目录项的序号。如果此长文件名描述目录只用到了1个目录项,则此值为41H,如果此值为E5H代表此文件已被删除;

(2)、偏移0BH~0CH处:其值总为0FH 00H;(3)、偏移0D处:该长文件目录项的标号,同一个长文件目录的不同目录项该值总相同(比如本例中3个目录项该值都为27H);(4)、偏移1AH~1BH处:该值总为00H 00H;(5)从偏移01H~1FH跳过前4项提到的字节,总是一个文件名的ASCII码接一个00H排列的,如果文件名的ASCII码在一个目录项还未写完,则会接到下一个目录项(实际上这些目录项都是从高到低排列的)同样的位置继续写,如果已经写完,则系统会在最后一个ACSII码后写00H,最多写3个00H,如果3个00H写完后,目录项还有空余位置,则系统会把这些位置全部写上FFH。同时FAT32系统还有一个类似与FAT16的目录项(紧接着长文件名的目录的后面),同样占32个字节:

00H~07H 文件的文件名

08H~0AH 文件的扩展名

0BH 文件属性

0CH 保留未用

0EH~0FH 文件创建时间

10H~11H 文件最后访问日期

12H~13H 文件创建日期

14H~15H 文件起始簇号的高16位

16H~17H 系统最后修改时间,其中:

16H字节的0~4位是以2秒为增量的秒

16H字节的5~7位和17H字节的0~2位是分钟

17H字节的3~7位是小时

18H~19H 文件最后修改的日期,其中:

18H字节0~4位是天号

18H字节5~7位和19H字节0位是月份

19H字节的1~7位为年号,0~119分别代表1980~2099

1AH~1BH 文件的起始簇号的低16位

1CH~1FH 文件的长度(单位为字节)

注意:其中文件名为DOS兼容文件名,比如上例中在DOS下的文件名为ABCDEF~1.TXT;其首字节含义同FAT16系统;文件属性字节含义同FAT16系统;在FAT32系统中增加了最后访问日期和文件创建时间日期,其计算原理同FAT16中的最后修改的时间和日期。

  其中第(4)至(8)项为以后陆续定义的。对于子目录项,其(12)为零;已删除目录项的首字节值为E5H。在可以使用长文件名的FAT32系统中,文件目录项保存该文件的短文件名,长文件名用若干个长文件名目录项保存,长文件名目录项倒序排在文件短目录项前面,全部是采用双字节内码保存的,每一项最多保存十三个字符内码,首字节指明是长文件名的第几项,11字节一般为0FH,12字节指明类型,13字节为校验和,26--27字节为零

(四)扩展分区表

我们已经学习了在主引导扇区中的分区表的偏移04H,是分区系统标志,当该值为05H或0FH表示该分区为扩展分区,其实它并非一个真正意义上的分区项,此分区项只是指向一个扩展分区表,这样做是为了解决分区表中最多只能有4个分区项的问题,扩展分区表也是从扩展分区表所在的扇区偏移1BEH~偏移1FD,该扇区0H~1BDH一般为1BEH个00H,同样要以结束标志55H AAH结束。同样,在扩展分区表中也可能存在指向下一个扩展分区表的分区项。

4.2  关于保留扇区

       在上述FAT文件系统DBR的偏移0x0E处,用2个字节存储保留扇区的数目。所谓保留扇区(有时候会叫系统扇区,隐藏扇区),是指从分区DBR扇区开始的仅为系统所有的扇区,包括DBR扇区。在FAT16文件系统中,保留扇区的数据通常设置为1,即仅仅DBR扇区。而在FAT32中,保留扇区的数据通常取为32,有时候用Partition Magic分过的FAT32分区会设置36个保留扇区,有的工具可能会设置63个保留扇区。
       FAT32中的保留扇区除了磁盘总第0扇区用作DBR,总第2扇区(win98系统)或总第0xC扇区(win2000,winxp)用作OS引导代码扩展部分外,其余扇区都不参与操作系统管理与磁盘数据管理,通常情况下是没作用的。操作系统之所以在FAT32中设置保留扇区,是为了对DBR作备份或留待以后升级时用。FAT32中,DBR偏移0x34占2字节的数据指明了DBR备份扇区所在,一般为0x06,即第6扇区。当FAT32分区DBR扇区被破坏导致分区无法访问时。可以用第6扇区的原备份替换第0扇区来找回数据。

格式化FAT16分区时,格式化程序根据分区的大小确定簇的大小,然后根据保留扇区的数目、根目录的扇区数目、数据区可分的簇数与FAT表本身所占空间来确定FAT表所需的扇区数目,然后将计算后的结果写入DBR的相关位置。

FAT16文件系统根据根目录来寻址其他文件(包括文件夹),故而根目录的位置必须在磁盘存取数据之前得以确定。FAT文件系统就是根据分区的相关DBR参数与DBR中存放的已经计算好的FAT表(2份)的大小来确定的。格式化以后,跟目录的大小和位置其实都已经确定下来了:位置紧随FAT2之后,大小通常为32个扇区。根目录之后便是数据区第2簇。 (FOR FAT16)

FAT32在格式化的过程中就根据分区的特点构建好了它的DBR,其中BPB参数是很重要的,可以回过头来看一下表4和表5。首先FAT32保留扇区的数目默认为32个,而不是FAT16的仅仅一个。这样的好处是有助于磁盘DBR指令的长度扩展,而且可以为DBR扇区留有备份空间。上面我们已经提到,构建在FAT32上的win98或win2000、winXP,其操作系统引导代码并非只占一个扇区了。留有多余的保留扇区就可以很好的拓展OS引导代码。在BPB中也记录了DBR扇区的备份扇区编号。备份扇区可以让我们在磁盘遭到意外破坏时恢复DBR。
      FAT32的文件分配表的数据结构依然和FAT16相同,所不同的是,FAT32将记录簇链的二进制位数扩展到了32位,故而这种文件系统称为FAT32。32位二进制位的簇链决定了FAT表最大可以寻址2T个簇。这样即使簇的大小为1扇区,理论上仍然能够寻址1TB范围内的分区。但实际中FAT32是不能寻址这样大的空间的,随着分区空间大小的增加,FAT表的记录数会变得臃肿不堪,严重影响系统的性能。所以在实际中通常不格式化超过32GB的FAT32分区。WIN2000及之上的OS已经不直接支持对超过32GB的分区格式化成FAT32,但WIN98依然可以格式化大到127GB的FAT32分区,但这样没必要也不推荐。同时FAT32也有小的限制,FAT32卷必须至少有65527个簇,所以对于小的分区,仍然需要使用FAT16或FAT12。

FAT32的另一项重大改革是根目录的文件化,即将根目录等同于普通的文件。

而且,根目录的位置也不再硬性地固定了,可以存储在分区内可寻址的任意簇内,不过通常根目录是最早建立的(格式化就生成了)目录表。所以,我们看到的情况基本上都是根目录首簇占簇区顺序上的第1个簇。

长文件名的实现有赖于目录项偏移为0xB的属性字节,当此字节的属性为:只读、隐藏、系统、卷标,即其值为0FH时,DOS和WIN32会认为其不合法而忽略其存在。这正是长文件名存在的依据。将目录项的0xB置为0F,其他就任由系统定义了,Windows9x或Windows 2000、XP通常支持不超过255个字符的长文件名。系统将长文件名以13个字符为单位进行切割,每一组占据一个目录项。所以可能一个文件需要多个目录项,这时长文件名的各个目录项按倒序排列在目录表中,以防与其他文件名混淆。      长文件名中的字符采用unicode形式编码(一个巨大的进步哦),每个字符占据2字节的空间。


使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

68

主题

134

帖子

3

粉丝