|
发表于 2011-8-9 10:05:49
|
显示全部楼层
楼主 你就不能自己直接 发布吗,,,为何总需要别人 动手呢。。。。
xldFAT文件系统研发笔记(2011-08-05 23:27:22)转载标签: 杂谈
xldFAT文件系统研发笔记
——许乐达,2011年8月5日
CH375是南京沁恒公司开发的一个USB总线的通用接通用接口芯片,单片机可以通过它方便地读写U盘。为方便U盘的文件管理,沁恒公司开发了相应的文件系统,可惜代码不开源。在学习掌握FAT32文件系统基本原理之后,决定针对RAM有限的单片机设计一个文件系统。利用业余时间,历时近两个月(中间寒假回家暂停了一段时间),于2011年3月开发完成文件系统“xldFAT”。 “xldFAT”支持多级子目录,支持8.3格式的大写字母文件名,支持文件打开、新建、删除、读写等常用操作,完全满足单片机对数据存储的要求。目前已发布源代码:http://www.ourdev.cn/bbs/bbs_content.jsp?bbs_sn=4934683&bbs_page_no=1&search_mode=1&search_text=发布一个FAT32文件系统&bbs_id=9999
近期将研发笔记整理如下,详细地记录了整个文件系统的研发思路、代码编写与调试,欢迎交流(QQ:553049047,Email:xuleda@126.com,博客:http://blog.sina.com.cn/u/1744636480)。
1、读文件
为了简化系统设计,采用8.3短文件名格式。
Ø 读取文件目录:
数据结构:
struct file_inf
{
unsigned char first_char;//条目的第一个字符,用于判断条目是否有效、是否为空
unsigned char is_long_name;//是否是长文件名
char name[9];//文件名
char suffix[4];//文件后缀
unsigned char attributes;//文件属性
unsigned long first_clu;//文件首簇
unsigned long file_size;//文件大小
//考虑到简化系统,忽略了时间等次要信息
}
算法:
void get_file_inf(unsigned char file_no,struct file_inf *p)//读取缓冲区内第file_no项条//目,提取目录信息
void dis_dir_file(void)//显示当前路劲下面的所有文件或文件夹
在当前目录簇下面查找,利用get_file_inf函数获取各个条目的关键信息,通过对关键信息的分析,识别出有效短文件名文件及文件夹:
在目录所有扇区范围内检索目录项,如果目录项以0xE5开头,那么直接忽略该条目,否者查看该条目的0xB字段,如果是0x0F,那么忽略之(因为它是长文件名的条目),如果0x0F以外的字符,那么判别为短文件名条目。查找过程中如果遇到空白条目,直接结束本次查找。同时利用文件属性区分文件是卷标、子目录还是普通文件。
Ø 读文件内容:
打开文件
unsigned char open_file(unsigned char *file_name,struct xldFAT_file_point *fp,unsigned char operation_style);//查找当前目录下的文件,并返回文件指针,不成功返回NULL,为读文件做准备
在当前目录簇下面查找,利用get_file_inf函数获取各个条目的关键信息,通过对关键信息的分析,识别出有效短文件名,如果文件名和后缀及类型都匹配找到文件后:
1、如果发现首簇为零,那么进行如下处理
如果是读操作,报错
如果是追加操作,申请首簇并在当前目录下登记,同时更新FAT表,(必须在此处申请首簇,因为一会就得将此信息写入目录表项)
按零初始化文件指针
2、如果首簇不是零,那么处理如下
如果是读操作,将文件指针定位到文件头,同时读入首个扇区数据至数据缓冲区。
如果是追加操作,将文件指针恢复到本文件上次关闭时的状态,同时读入首个扇区数据到数据缓冲区,以便文件写函数直接进行写操作,避免写函数内部判断是否是刚开始的追加操作
3、上面的难点是如何恢复上次关闭的状态,初步想法如下
调用若干次find_next_cluster寻找到簇尾,通过(file_size/sector_size)对cluster_size进行取模运算算出簇内扇区序号,再通过对sector_size取模算出扇区内字节偏移。
如:某个文件大小为8*512+513字节,占据了5、6两个簇。通过簇链可以找到簇尾6,file_size/sector_size=9,9%8=1,即数据指针尾部在簇内扇区1,file_size%sector=1,即数据指针尾部在扇区内部偏移1个字节的地方。(正常工作)
再如:某个文件大小为8*512字节,占据了6号簇。通过簇链可以找到簇尾就是6簇,file_size/sector_size=8,8%8=0,即数据指针尾部在簇内扇区0,file_size%sector=0,即数据指针尾部在扇区内部最头上。(工作异常,因为指针被定为到了6簇起始位置,6簇会被覆盖,需要处理这种特例。方法是通过文件大小识别出数据指针指向的簇是否是已经使用过的簇,如果是,需要触发read_file函数的新建机制,由它开辟新的存储空间;若是一个空簇直接写入即可——这些要求read_file函数是先判断后写入的工作模式)
如果文件大小与簇的数目匹配,那么文件指针指向的是已有的簇,否者指向的是空簇,可以直接使用。
读数据
unsigned char read_file(struct xldFAT_file_point *fp,unsigned char *data,unsigned long data_num);//从fp指向的文件中读出data_num个数据,并写入数组data中,成功则返回0,失败返回1,这种办法读数据效率较低,可以考虑用块读写的方式进行,即先扇区对齐,然后每次读取512字节。
从file_buffer中读取数据,同时修改文件指针相关内容,同时在跨扇区及跨簇读取时要读取新的数据到数据缓冲区,同时修改文件指针。
2、写文件
unsigned char write_file(struct xldFAT_file_point *fp,unsigned char *data,unsigned long data_num);//向fp指向的文件中写入data_num个数据,成功则返回0,失败返回1,这种办法写数据效率较低,可以考虑用块读写的方式进行,即先扇区对齐,然后每次写入512字节。
3、关闭文件
unsigned char file_close(struct xldFAT_file_point *fp)//关闭文件,如果是读操作,直接将fp清零即可;如果是写操作,在讲fp清零之前,需要将数据缓冲区内的数据写入存储卡,同时将文件大小等信息写入目录表项中
4、新建文件
unsigned char creat_file(unsigned char *file_name);//在当前目录下创建文件file_name
检查文件名是否符合8.3命名规范,并改为大写
搜索当前路径下是否已经存在同名文件
是否有空白目录项?有的话用空白目录项创建新文件,没有的话检查是否有被删除的目录项,有的话用被删除的目录项创建新文件。否者申请新的簇,在新簇下创建文件,同时将新的簇链接到簇链中。
1、 删除文件
unsigned char del_file(unsigned char *file_name)//删除以file_name命名的文件
检查文件名是否符合8.3命名规范,并改为大写
查找文件file_name,找不到则报错
删除该文件的簇链
将目录项的首字节改为0xE5。
2、 为了实现脱离单片机的虚拟FAT32仿真技术,使用如下代码实现PC端的扇区读:
使用SetFilePointer时,偏移量必须是一个扇区大小的整数倍。0-511以内的偏移都将被视为偏移0。
#include <stdio.h>
#include <conio.h>
#include <process.h>
#include <dos.h>
#include <stdlib.h>
#include <Windows.h>
#include <assert.h>
int main(void)
{ unsigned char pBuffer[512]={0},i;
unsigned long dwLen;
OVERLAPPED over_lap;
HANDLE hFile;
hFile = CreateFile("\\\\.\\M:",
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
0,
NULL);
SetFilePointer(hFile,512,NULL,FILE_BEGIN);
for(i=0;i<10;i++)
ReadFile(hFile, pBuffer, 512, &dwLen, NULL);
CloseHandle(hFile);
}
3、 新建子目录
为新的子目录寻找空间
Ø 在当前目录下查找,避免子目录名重复;
Ø 在当前目录下查找空闲目录项,空白条目优先,否则动用0xE5开头的被删除的条目,没有可用条目的话为当前目录申请新的簇,清空新簇并链接簇链,动用空闲簇的第一个条目;
Ø 将可用目录项的位置信息登记到dir32_inf的free_sector、dir32字段;
初始化子目录的各项参数
Ø 为子目录申请空闲簇,将获得的空闲簇登记到dir32_inf的first_clu字段,清空该簇内容,在空簇中创建两个子目录,一个是“.”(指向本目录);另一个是“..”(指向上一级目录,如果上一级目录是根目录的话,首簇字段写00);
Ø 将子目录名登记到dir32_inf的name字段;
Ø dir32_inf的name字段改写为0x00,表示写入的是子目录信息;
Ø 调用modify_dir32函数向新条目写入相关信息;
Ø 返回;
数据结构:
struct dir32_inf
{
unsigned char is_file;//表示即将改写的是子目录还是文件 0:子目录 1:文件
char name[9];//即将填入目录项的名字信息
char suffix[4];//文件的后缀,仅对文件有效,对于子目录此字段无效
unsigned char attributes;//目录项属性
unsigned long first_clu;//将填入目录项的首簇信息
unsigned long file_size;//将填入目录项的文件大小信息,仅对文件有效,对于子目录此字段无效
unsigned long free_sector;//可用条目的位置信息——可用条目所在扇区
unsigned char dir32; //可用条目的位置信息——可用条目在扇区内部的序号
//时间等次要信息暂时忽略
}
算法:
unsigned char search_file_dir(char *file_name,unsigned char is_file,unsigned long *sector,unsigned char *dir32);//检查当前路径下面是否存在名为file_name的文件或文件夹(is_file=0表示搜索子目录,否者=1表示搜索文件),存在的话返回所在扇区,及扇区内目录项序号。
unsigned char search_free_dir(unsigned long *sector,unsigned char *dir32);//在当前目录下搜索可用目录项,空白目录优先,没有空白目录则搜寻以“0xE5”开头的目录项,成功则返回空闲目录所在扇区及扇区内目录项序号,否则产生错误码。
unsigned char modify_dir32(struct dir32_inf *p_dir32,unsigned long sector,unsigned char dir32);//向目录项中填入相关信息,方法是先清空dir32所有字段,再将结构体p_dir32内部的文件名等信息写入sector扇区第dir32个目录项,时间等次要信息全部写零(对于子目录,在写名字时11个字符空间剩余部分用空格“0x20”补充);
unsigned char creat_dir(char *dir_name);//在当前目录下创建子目录dir_name;
9.向文件中写了少量数据时,一切正常,但当数据量大时,出现破坏引导扇区、FAT表等致命错误,导致u盘不可识别、无法格式化。问题分析解决如下:
在程序关键部分添加告警代码,当出现异常时自动警告。
程序运行后获得警告“簇链接失败”,重点监视簇链接子程序。继续跟踪调试,发现FAT表除了第一项外,全部被清零,此时簇链欲链接的簇是128簇,刚好出现跨扇区建立簇链的情况。
错误定位在簇链子程序中,重点怀疑簇尾和新簇不在一个扇区时的处理代码,认真检查该代码,发现如下错误:
程序本意:write_sector(first_FAT_sector+new_cluster/(sector_size>>2),file_buffer);
错误代码:write_sector(first_FAT_sector+last_cluster/(sector_size>>2),file_buffer);
打算保存新簇的时候,读入了新簇所在扇区的值(全零),修改后,本该写入新簇所在扇区的结果却写入了簇尾所在扇区,导致簇尾所在扇区的值全部变成零,除了第一项为0x0fffffff
心得:
写好的子程序最好要经过测试,测试子程序所有流程。
碰到问题可以添加告警代码或验证代码,保证异常时能够及时发现并获得处理。
尽量使用子程序,上述错误代码可以使用专门的子程序替代,替代后就不容易出问题。
10、向u盘写入一个大型文件(10M),发现速度刚开始较快,随着时间的推移越来越慢。其中的一个原因就是执行get_free_cluster时,每次都从FAT表第一项开始搜索,而且文件越大,搜索范围也越来越大,导致写入速度越来越慢。(类似地,为文件创建簇链时,为了跟踪簇链也需要频繁地读FAT表,考虑将簇链尾部写入文件指针fp中)
改进:在初始化文件系统时,先搜索一遍FAT表,找到第一个空闲簇,记录该簇项所在扇区sector_of_free_cluster。以后每次调用get_free_cluster函数时,函数首先从sector_of_free_cluster处开始搜索空闲簇项,遇到FAT表末尾时从FAT表头开始搜索,直到扇区sector_of_free_cluster。退出时刷新sector_of_free_cluster。
(为了进一步加快速度,可以直接记录空闲簇的簇号,利用簇号推算FAT表扇区和簇项内部序号,对应全局变量取名为:last_free_cluster_in_FAT)
根据上述原则改进后,读写速度大幅提高,而且很稳定,没有速度明显变慢现象。
11、通过对系统函数与xldFAT中读写函数执行效果的比较,write_file函数与read_file函数测试通过。
12、删除文件夹
unsigned char del_dir(char *dir_name,unsigned long dir_first_cluster);//该函数用于删除以dir_first_cluster为首簇的目录下的某个文件夹
基本原理:
调用函数del_file删除子目录中的所有文件
递归调用del_dir函数删除子目录中的所有子目录
由于del_file涉及到了全局变量current_dir_cluster,故需要对del_file函数改进,原函数为unsigned char del_file(char *name);它使用了全局变量current_dir_cluster,而del_file调用的诸多函数也使用了current_dir_cluster。为了支持函数重入,有一大批函数都要改造成可重入函数。
受牵连的函数有:
search_file_dir
search_free_dir
del_file
search_file_dir、search_free_dir两个函数经过测试,结果表明全部改造成功!
del_dir实现方法:
Ø 查找当前目录,检查要删除的子目录是否存在,若存在则获取子目录的簇首,否者报错返回。
Ø 要删除的文件夹为空吗?
Ø 若为空,像删除文件一样回收簇链,删除目录项
Ø 如果不为空,进入该子目录,删除其下面的文件和文件夹,其中删除文件夹使用该函数的递归调用。
Ø 注意处理两个文件夹“.”、“..”
算法:
unsigned char is_dir_empty(unsigned long dir_first_cluster);//子目录是否为空
unsigned char del_empty_dir(unsigned long dir_first_cluster);//删除空目录
unsigned char del_dir(char *dir_name,unsigned long dir_first_cluster);//删除目录
注意:由于该函数采用了递归调用,如果文件夹包含深度很深,将消耗大量RAM,一般的MCU可能扛不住,慎用!!!
效果:由于递归调用时file_buffer也是全局变量,引起数据冲突(这个错误是由数据冲突警告代码提示发现的,要不死都不知道怎么死的),需要改进。必须在函数重入前,释放掉file_buffer。
有两种改进方案:
Ø 反复扫描当前目录,找到一项删除一项,这样可以及时释放掉file_buffer
Ø 先删除当前目录下的文件,再搜索到一个文件夹删除一个文件夹
第一种方法结构简单,原理明确,选用第一种方法。编写了版本xldFAT_SD_15
xldFAT_SD_15
del_dir实现:
Ø 查找当前目录,检查要删除的子目录是否存在,若存在则获取子目录的簇首,否者报错返回。
Ø 扫描当前目录,查找任何存在的文件或子目录
Ø 是文件直接删除掉
Ø 是子目录的话判断文件夹是否为空?
Ø 若为空,像删除文件一样回收簇链,删除目录项
Ø 如果不为空,进入该子目录,删除其下面的文件和文件夹,其中删除文件夹使用该函数的递归调用。
Ø 注意处理两个文件夹“.”、“..”
与xldFAT_SD_14版本中del_dir的区别是,本方法调用专门函数查找当前目录下任何存在的文件或子目录,然后删除之,及时释放掉了file_buffer。保证重入del_dir时file_buffer可用。xldFAT_SD_14版由于存在遍历,遍历时要求持续占用file_buffer,而遍历过程内部又存在del_dir函数的重入(重入也需要使用file_buffer),导致file_buffer冲突。
添加关键函数:unsigned char search_any_file_dir(unsigned long *sector,unsigned char *dir32,unsigned long dir_first_cluster);//用于搜索任何有效的文件或文件夹,自动忽略文件“.”“..”。
13、FAT32研究过程中频繁使用计算器,其实可以打开一个EXCEL文件,将采用公式都编辑好,用的时候直接输入数据即可,方便且直观。根据函数返回的错误代码进行判断时,切记考虑返回的所有情况。如某函数可能返回0、1、2三种错误码,对于
if(fun())
P_1();
Else
P_2();
此时,如果错误码是1、2,都将执行P_1();,将1、2处理为一种情况,有可能出错。
14、del_dir工作基本正常(成功地删除了一个73层嵌套深度的文件夹),但是由于对文件属性不够了解,导致属性为0x00的文件无法删除。另外,当文件删除后重新写入数据,总是从FAT表头开始申请扇区,同时数据擦写也是从对应的簇开始,导致FAT表头和数据区开始部分被频繁擦写,容易损坏,而靠后的簇却很少被使用,可以改进如下,文件系统初始化空闲簇搜索开始位置时,可以产生一个随机位置(该位置要在FAT表范围内),从而随机找一个簇开始写,避免每次擦写文件都局限于数据区开始部分,有效保护U盘。
15、写文件函数可以将数据准确写入U盘,但是发现U盘里的文件总比源文件多一个字节,跟踪调试发现,是系统函数feof(p)出现异常。
While(!feof(p))
;
本来p指向的文件只有3个字节,但却执行了4次,导致复制数据时每次都在尾部多一个,说明write_file函数本次工作正常。
16、格式化函数void SD_formatting(void)
//////////////////////////////////////////////////////////////////////////////////////////格式化SD卡
测试:成功对一张256M的SD卡进行了格式化。
xldFAT_SD_16
1、添加了格式化程序,但是很粗糙,仅仅是擦除并初始化FAT表和根目录。而且前提是这个盘曾经被初始化为FAT32,使用范围受限,需要进一步研究格式化原理。
2、为了扩大通用性,开始采用逻辑扇区访问方法。原先的绝对扇区访问都是以DBR首扇区为基准的,也即系统运行时所有的扇区都是逻辑扇区加DBR首扇区的(DBR首扇区就是BPB_sector)。现在采用逻辑扇区,那么DBR首扇区为零扇区,同时VC对扇区的访问刚好是采用逻辑扇区的,故系统要求读取的扇区直接送入系统函数即可,不需要像原先一样将绝对扇区转换成逻辑扇区后送入系统函数。
3、改动地方有二,一个是指定BPB_sector=0,另一个是VC读写扇区函数入口参数由“sector-BPB_sector”变为“sector”。
4、不要忽略系统给出的警告信息,这些小警告的后面往往隐藏着巨大的危险,一般是逻辑错误,例如:1+2<<3,作者的本意可能是将2左移3位再加1,可实际效果是1+2得3,3再左移3位,得出的结果完全不是作者的本意,VC对于这种情况会自动警告,注意检查表达式的算符的优先级,如果你利用了该信息,就可以避免该错误。
5、为了研究格式化原理,进行如下实验:
Ø 操作:修改MBR中DBR位置字段,将DBR扇区由原来正常的63扇区改为1扇区。
Ø 结果:打开U盘,系统提示U盘未格式化。
Ø 结论:系统打开U盘时会自动读取MBR,然后找到DBR。
Ø 操作:在上述实验的基础上,执行格式化。
Ø 结果:格式化后,DBR信息果然被写入到1扇区。
Ø 结论:磁盘格式化时,利用到了MBR中的磁盘大小、DBR位置等信息。
Ø 推测:如果MBR被破坏了,U盘将无法格式化。
Ø 操作:擦除MBR,为了安全起见,备份一个MBR。
Ø 结果:擦除MBR,U盘还是可以格式化,只是格式化后,0扇区直接放的是DBR,不再有MBR了。
Ø 分析:连U盘大小的信息都没了,却还能格式化,有两种可能,一是U盘容量信息在U盘接口电路模块中有,另一种是这个信息在某个扇区有备份。记得上次出现程序跑飞,FAT表被破坏,未知扇区被擦写事故时,U盘就无法正常格式化了,推测是那个MBR备份被无意破坏了,故后一种情况的可能性比较大。开始在整个U盘寻找该备份,搜索后未找到该备份。发点狠,将U盘全部清空(winhex有一个Att+1和ALT+2用于选择区块,类似于EXCEL里的shift键盘)。结果U盘还能被格式化(只是同样没有MBR),这说明U盘的大小信息应该是保存在U盘接口电路里,上次事故中U盘无法格式化可能是系统读取了错误的U盘信息,导致无法格式化,即系统优先从扇区内读U盘相关信息,读不到则从接口电路里读。
Ø 2G的U盘总扇区数:3911616,容量:3911616*0.5=1955808=kB=1.865203857421875GB
Ø 操作:为了恢复MBR,在winhex中将备份了MBR的文件打开,复制数据到0扇区。重新格式化(指定DBR在1号扇区)。
Ø 结果:出现了上次事故中一样的现象——计算机反应变慢,erplorer进程死掉。调用计算机-管理-磁盘管理-右键格式化,恢复到了原先状态——活动的、有MBR。而且指定的DBR位置信息生效了,DBR真的被分配到了1扇区。
Ø 操作:MBR可以指定4个分区,可以在正常分区后面捏造三个虚假的超大U盘。填写修改真实分区后面的分区信息(注意,活动分区只能有一个,记得取消后面分区的活动属性),同时填写对应分区的DBR。
Ø 结果:失败,没有实现。
DBR有两份第二份DBR在第一份DBR后面第六个位置,例如第一个在100扇区,第二个就在106扇区。
SetFilePointer(hFile,512,NULL,FILE_BEGIN);中的第三个参数是一个指针,不是一个直接的数据。 |
|