1、FAT32文件系统的存储机制及其在单片机上的实现FAT32文件系统的存储机制及其在单片机上的实现FAT32 文件系统您一定不会陌生,最多看到它是在windows 操作系统里,但在一些嵌入式产品(如手机、MP3、MP4 等)中,也能看到它的身影。从某种意义上来讲,FAT32文件系统是非常成功的,使我们可以脱离底层储存设备驱动,更为方便高效地组织数据。给单片机系统中的大容量存储器(如SD 卡、CF 卡、硬盘等)配以FAT32 文件系统,将是非常有意义的(如创建的数据文件可以在windows 等操作系统中直接读取等)。FAT32 本身是比较复杂的,对其进行讲解的最好方法就是实际演练。笔者手里持有一张
2、刚以FAT32 格式化的SD 卡,我们就围绕它来讲解FAT32 的实现机理。FAT32 分为几个区域,这里将用实例的方法对它们的结构与在文件存储中的功能进行详细的剖析。1、实例说明此实例首先在一张空的SD 卡(已被格式化为FAT32 格式)上创建一个文本文件,并在其中输入20 个字符。再将它插入到单片机系统中,实现对这个文件的读取,将文件内容输出在调试终端上。2、实现过程1) 格式化与创建文件Windows 上的磁盘格式化与文件创建就不用多说了。如下图:2)DBR(DOS BOOT RECORD 操作系统引导记录区)DBR 是我们进军FAT32 的首道防线。其实DBR 中的BPB 部分才是这一
3、区域的核心部分(第1290 字节为BPB),只有深入详实的理解了BPB 的意义,才能够更好的实现和操控FAT32。关于DBR 在FAT32 中的地位就不多说了,以下面实际的DBR 内图所示:上面的数据看起来杂乱不堪,无从下手,其实对我们有用的数据只不过90个字节(如图中彩色线标记的字节)。仅仅是这90 个字节就可以告诉我们关于磁盘的很多信息,比如每扇区字节数、每簇扇区数、磁道扇区数等等。对于这些信息的读取,只要遵循DBR 中的字段定义即可。(比如图中紫色字段的两个字节表示这张磁盘的每一个扇区有512 个字节,具体的计算方法见下文)字段定义如下表(BPB 后面的422 个字节对我们的意义不大,表
4、中省略):字段名称长度含义偏移量jmpBoot3跳转指令0OEMName8这是字符串,标识了格式化该分区的操作系统的名称和版本号3BytesPerSec2每扇区字节数11SecPerClus1每簇扇区数13RsvdSecCnt2保留扇区数目14NumFATs1此卷中FAT 表数16RootEntCnt2FAT32 为017TotSec162FAT32 为019Media1存储介质21FATSz162FAT32 为022SecPerTrk2磁道扇区数24NumHeads2磁头数26HiddSec4FAT 区前隐扇区数28TotSec324该卷总扇区数32FATSz324FAT 表扇区数36Ext
5、Flags2FAT32 特有40FSVer2FAT32 特有 42RootClus4根目录簇号 44FSInfo2文件系统信息48BkBootSec2通常为650Reserved12扩展用52DrvNum164Reserved1165BootSig166VolID467FilSysType1171FilSysType1882DBR 的实现代码:struct FAT32_DBR unsigned char BS_jmpBoot3 /跳转指令offset: 0unsigned char BS_OEMName8 / offset: 3unsigned char BPB_BytesPerSec2 /每
6、扇区字节数offset:11unsigned char BPB_SecPerClus1 /每簇扇区数offset:13unsigned char BPB_RsvdSecCnt2 /保留扇区数目offset:14unsigned char BPB_NumFATs1 /此卷中FAT 表数offset:16unsigned char BPB_RootEntCnt2 /FAT32 为0 offset:17unsigned char BPB_TotSec162 /FAT32 为0 offset:19unsigned char BPB_Media1 /存储介质offset:21unsigned char
7、BPB_FATSz162 /FAT32 为0 offset:22unsigned char BPB_SecPerTrk2 /磁道扇区数offset:24unsigned char BPB_NumHeads2 /磁头数offset:26unsigned char BPB_HiddSec4 /FAT 区前隐扇区数offset:28unsigned char BPB_TotSec324 /该卷总扇区数offset:32unsigned char BPB_FATSz324 /一个FAT 表扇区数offset:36unsigned char BPB_ExtFlags2 /FAT32 特有offset:4
8、0unsigned char BPB_FSVer2 /FAT32 特有offset:42unsigned char BPB_RootClus4 /根目录簇号offset:44unsigned char FSInfo2 /保留扇区FSINFO 扇区数offset:48unsigned char BPB_BkBootSec2 /通常为6 offset:50unsigned char BPB_Reserved12 /扩展用offset:52unsigned char BS_DrvNum1 / offset:64unsigned char BS_Reserved11 / offset:65unsign
9、ed char BS_BootSig1 / offset:66unsigned char BS_VolID4 / offset:67unsigned char BS_FilSysType11 / offset:71unsigned char BS_FilSysType18 /FAT32 offset:82在程序中我们采用以上的结构体指针对扇区数据指针进行转化,就可以直接读取数据中的某一字段,如要读取BPB_BytesPerSec,可以这样来作:(struct FAT32_DBR *)pSector)BPB_BytesPerSec用如上语句就可以得到这一字段的首地址。心细的读者可能会发现读回来的
10、字节拼在一起,与实际的数据并不吻合。例如BPB_BytesPerSec 读出来的内容是“00 02”,在程序中我们把00 作为int 型变量的高字节,把02 作为其低字节,那么这个变量的值为2,而实际的SD 卡里的扇区大小为512 个字节,这512 与2 之间相去甚远。是什么造成这种现象的呢?这就是大端模式与小端模式在作怪。上面我们合成int 型变量的方法(00 为高字节,02 为低字节)为小端模式。而如果我们改用大端模式来进行合成的话,结果就会不同:将02 作高字节,而把00 作低字节,变量值就成了0x0200(十进制的512),这样就和实际数据吻合了。可见FAT32 中字节的排布是采用小端
11、模式的。在我们程序中需要将它转为大端模式的表达方式。在笔者的程序有这样一个函数lb2bb,专门将小端模式转为大端模式,程序如下:unsigned long lb2bb(unsigned char *dat,unsigned char len) /小端转为大端unsigned long temp=0unsigned long fact=1unsigned char i=0for(i=0iBPB_Sector_No =FAT32_FindBPB()/FAT32_FindBPB()可以返回BPB 所在的扇区号argTotal_Size =FAT32_Get_Total_Size()/FAT32_Ge
12、t_Total_Size()可以返回磁盘的总容量,单位是兆argFATsectors =lb2bb(bpbBPB_FATSz32) ,4)/装入FAT 表占用的扇区数到FATsectors 中argFirstDirClust =lb2bb(bpbBPB_RootClus) ,4)/装入根目录簇号到FirstDirClust 中argBytesPerSector =lb2bb(bpbBPB_BytesPerSec),2)/装入每扇区字节数到BytesPerSector 中argSectorsPerClust =lb2bb(bpbBPB_SecPerClus) ,1)/装入每簇扇区数到Sector
13、sPerClust 中argFirstFATSector=lb2bb(bpbBPB_RsvdSecCnt) ,2)+argBPB_Sector_No/装入第一个FAT 表扇区号到FirstFATSector 中argRootDirCount =lb2bb(bpbBPB_RootEntCnt) ,2)/装入根目录项数到RootDirCount 中argRootDirSectors =(argRootDirCount)*329/装入根目录占用的扇区数到RootDirSectors 中argFirstDirSector=(argFirstFATSector)+(bpbBPB_NumFATs0)*(a
14、rgFATsectors)/装入第一个目录扇区到FirstDirSector 中argFirstDataSector =(argFirstDirSector)+(argRootDirSectors)/装入第一个数据扇区到FirstDataSector 中3)FAT(文件分配表)FAT 表是FAT32 文件系统中用于磁盘数据(文件)索引和定位引进的一种链式结构。可以说FAT 表是FAT32 文件系统最有特色的一部分,它的链式存储机制也是FAT32 的精华所在,也正因为有了它才使得数据的存储可以不连续,使磁盘的功能发挥得更为出色。FAT 表到底在什么地方?它到底是什么样子的呢?从第一步从BPB 中
15、提取参数中的FirstFATSector 就可以知道FAT 表所在的扇区号。其实每一个FAT 表都有另一个与它一模一样的FAT 存在,并且这两个FAT 表是同步的,也就是说对一个FAT 表的操作,同样地,也应该在另一个FAT表进行相同的操作,时刻保证它们内容的一致。这样是为了安全起见,当一个FAT 因为一些原因而遭到破坏的时候,可以从另一个FAT 表进行恢复。FAT 表的内容如下图所示:上图就是一个实际的FAT 表。前8 个字节“F8 FF FF 0F FF FF FF FF”为FAT32 的FAT 表头标记,用以表示此处是FAT 表的开始。后面的数据每四个字节为一个簇项(从第2 簇开始),用
16、以标记此簇的下一个簇号。拿我们创建的那个叫TEST.TXT(大小为20 个字节)的文件来说,如果这个文件的开始簇为第2 簇的话,那么就到FAT 表里来查找,看文件是否有下一个簇(如果文件大小大于一个簇的容量,必须会有数据存储到下一个簇,但下一个簇与上一个簇不一定是连续的),可以看到“簇2”的内容为“FF FF FF0F”,这样的标记就说明这个文件到第2 簇就已经结束了,没有后继的簇,即此文件的大小是小于一个簇的容量的。上面讲了很多,都是围绕簇这样一个词来讲的,簇又是什么?为什么要将它引入到FAT32 里来呢?磁盘上最小可寻址存储单元称为扇区,通常每个扇区为512 个字节。由于多数文件比扇区大得
17、多,因此如果对一个文件分配最小的存储空间,将使存储器能存储更多数据,这个最小存储空间即称为簇。根据存储设备(磁盘、闪卡和硬盘)的容量,簇的大小可以不同以使存储空间得到最有效的应用。在早期的360KB 磁盘上,簇大小为2 个扇区(1,024 字节);第一批的10MB 硬盘的簇大小增加到8 个扇区(4,096 字节);现在的小型闪存设备上的典型簇大小是8KB或16KB。2GB 以上的硬盘驱动器有32KB 的簇。如果对于容量大的存储定义了比较小的簇的话,就会使FAT 表的体积很大,从而造成数据的冗余和效率的下降。需要指出的是,簇作为FAT32 进行数据存储的最小单位,内部扇区是不能进一步细分的,即使
18、一个文件的数据写到一个簇中后,簇中还有容量的剩余(里部扇区没有写满),哪怕这个簇只写了一个字节,其它文件的数据也是不能接在后面继续数据的,而只能另外找没有被占用的簇。我们按照初始化参数表中的SectorsPerClust 可以知道一个簇中的扇区数,笔者的SD 卡实测簇大小为4 个扇区,按照上面的说法,TEST.TXT 这样一个只有20 个字节的文件,也会占用一个簇的容量,让我们在Windows 里看看它的实际占用空间的情况。如下图:从上图可以看到文件大小为20 个字节,但占用空间却是2048 个字节(一个簇的容量,4 个扇区)。TEST.TXT 容量只有20 个字节,所以只占用了一个簇,可能F
19、AT 表中还看不出链式结构,现在我们再创建一个文件,使它占用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”来对其进行
20、标记。其实还有其实的标记以表达其它的簇属性,如“00 00 00 00 ”表示未分配的簇,“FF FF FF F7”表示坏簇等。给出一个簇号,计算出它的后继簇号,是实现FAT32 的重点,实现如下:unsigned long FAT32_GetNextCluster(unsigned long LastCluster)unsigned long tempstruct FAT32_FAT *pFATstruct FAT32_FAT_Item *pFAT_Itemtemp=(LastCluster/128)+Init_Arg.FirstFATSector)/计算给定簇号对应的簇项的扇区号FAT32
21、_ReadSector(temp,FAT32_Buffer)pFAT=(struct FAT32_FAT *)FAT32_BufferpFAT_Item=&(pFATItems)LastCluster%128)/在算出的扇区中提取簇项return lb2bb(pFAT_Item,4) /返回下一簇号那么FAT 表有多大呢?FAT 表中每四个字节表示一个簇,所以FAT 表的大小由实际的簇数来决定。从这里也可以看出,如果簇过大,就会则FAT 表比较小,但会造成空间的浪费,而如果簇过小,可以减小空间的浪费,但会使FAT 表变得臃肿。FAT 表的大小也可以从BPB 参数FATsectors 读出。从上
22、面的BPB 图可以得知笔者的SD 卡的FAT 表大小为958 个扇区(“BE 03 0000”的大端表示)。如果这958 个扇区每四个字节都表示一个簇项,则它可以表示(958*512/4)- 2=122622 个簇(减去2 是因为有8 个字节的FAT 表头标识。看看我们计算的是否正确呢,下面是Winhex 计算出来的簇数:与Winhex 计算的结果是吻合的,我们对FAT 表与簇的理解是正确的。看完上面对FAT 表的讲解中,你可能会问:一个文件数据的首簇号怎样来确定呢?只有知道了一个文件数据的首簇号才能继续查找下一簇数据的位置,直到数据结束。下面将要讲到的“根目录区”就可以由一个文件的文件名来查
23、到它的首簇。4)根目录区在FAT32 中其实已经把文件的概念进行扩展,目录同样也是文件,从根目录的地位与其它目录是相同的,因此根目录也被看作是文件。既然是文件就会有文件名,根目录的名称就是磁盘的卷标。如笔者的SD 卡在格式会时设置卷标为znmcu,则根目录的名称就为ZNMCU,如下图:每一个文件都对应一个描述它属性的结构,定义如下:FAT32 文件目录项32 个字节的定义字节偏移量字数量定义0 78文件名8 103扩展名111属性字节0x00 (读写)0x01 (只读)0x02 (隐藏)0x04 (系统)0x08 (卷标)0x10 (子目录)0x20 (归档)121系统保留131创建时间的10
24、 毫秒位14152文件创建时间16172文件创建日期18192文件最后访问日期20212文件起始簇号的高16 位22232文件的最近修改时间24252文件的最近修改日期26272文件起始簇号的低16 位28314表示文件的长度根目录区所在扇区可从BPB 参数FirstDirSector 获取,从BPB 图得FirstDirSector=FirstFATSector+BPB_NumFATs*FATsectors=2053。根目录区的初始大小为一个簇,实际的内容如下:图中的记录1 描述根目录,前八个字节为文件名“ZNMCU ”(长度小于8 的部分用空格符补齐),下面的三个字节为扩展名“ ”( 长度
25、小于3的部分用空格符补齐),08 表示此文件为卷标,开始簇高字节为00 00,低字节为00 00,开始簇为0,文件长度为0。记录2 描述TEST.TXT 文件,文件名为“TEST ”,扩展名为“TXT”,20 表示此文件为归档,开始簇为3(“00 00 00 03”),长度为20。记录3 描述BIGTEST.TXT 文件,文件名为“BIGTES1”,扩展名为“TXT”,开始簇为4,长度为5200 字节(00 00 CB 20)。可以看到FAT32 中的文件名都以大写字母表示,长度不足的部分用空格符补齐,所以我们要读取的文件TEST.TXT 就变成了“TEST .TXT”,这将有助于文件名的匹配
26、,我们不用去处理不等长文件名所带来的麻烦。另外,还会发现长度过长的部分会被1 所替换,如果替换后有文件与之重名,则后面的数字将增加为2。文件目录项结构的实现如下:struct direntryunsigned char deName8 / 文件名unsigned char deExtension3 / 扩展名unsigned char deAttributes / 文件属性unsigned char deLowerCase / 系统保留unsigned char deCHundredth / 创建时间的10 毫秒位unsigned char deCTime2 / 文件创建时间unsigned
27、char deCDate2 / 文件创建日期unsigned char deADate2 / 文件最后访问日期unsigned char deHighClust2 / 文件起始簇号的高16 位unsigned char deMTime2 / 文件的最近修改时间unsigned char deMDate2 / 文件的最近修改日期unsigned char deLowCluster2/ 文件起始簇号的低16 位unsigned char deFileSize4 / 表示文件的长度我们最终要实现的是对TEST.TXT 文件的读取,须要作到给定文件名后,可以得到相应文件的首簇。主要的思想就是对根目录区中(本实例只针对根目录中的文件进行读取,至于多级子目录的实现,只须要进行多次首簇定位)的记录进
copyright@ 2008-2022 冰豆网网站版权所有
经营许可证编号:鄂ICP备2022015515号-1