目录
前言
上一节我们已经学习了 SD 卡的使用,本节我们将学习使用文件管理系统来应用SD卡。
FATFS 是一个完全免费开源的 FAT 文件系统模块,专门为小型的嵌入式系统而设计。FATFS 完全用标准的C语言编写,所以具有良好的硬件平台独立性,FATFS 可以移植到 8051单片机、AVR、ARM等系列单片机上而只需做简单的修改即可。
1. 文件系统简介
使用文件系统时,它为了存储和管理数据,在存储介质建立了一些组织结构,这些结构包括操作系统引导区、目录和文件。
常见的 Windows 下的文件系统格式包括FAT32、NTFS、exFAT。在使用文件系统前,要先对存储介质进行格式化。格式化时会在存储介质上新建一个文件分配表和目录。这样,文件系统就可以记录数据存放的物理地址,剩余空间。
文件系统的结构和特性:
使用文件系统时,数据都是以文件的形式存储的。写入新文件时,先在目录中创建一个文件索引,文件索引指示了文件存放的物理地址,再把数据存储到该地址上。当需要读取数据时,可以从目录中找到该文件的索引,进而在相应的地址上读取出数据。具体还涉及到逻辑地址、簇大小、不连续存储等一系列辅助结构或处理过程。
文件系统的存在使存取数据时,不再是简单地向某物理地址直接读写,而是要遵循它的读写格式。如经过逻辑转换,一个完整的文件可能被分开成多段存储到不连续的物理地址,使用目录或者链表的方式来获知下一段的位置。
文件系统示意图:
文件系统中的簇可以认为是磁盘中的扇区,每个磁盘格式化以后(也就是我们刚买电脑是得到的磁盘信息)都会有一个文件分配表和目录。A.TXT就存储在第2~11簇,B.TXT就存储在第12~65簇,C.TXT就存储在第66~86簇。
目录记录了文件的开始簇位置、大小等信息。
其中文件名占50个字节,开始簇占4个字节(A.TXT文件在第2~11簇,所以文件从第2簇到第11簇,文件大小占10个字节),同时记录了文件的创建日期、时间(占10个字节),文件的修改日期和时间(占10个字节),文件的读写属性(占4个字节)。
文件分配表:
文件 A.TXT 我们根据目录项中指定的 A.TXT 的首簇为2,然后找到文件分配表的第2簇记录(簇号为2,对应的数据为3),上面登记的是3就能确定下一簇是3。找到文件分配表的第三簇记录,上面对应的数据是4,依次类推,……直到找到第11簇,上面对应的数据是FF,表示结束,文件读取完毕。(事实上,了解链表的应该很清楚,整个读取过程和链表读取数据的过程类似,链表由一个数据域和指针域构成,数据域中自然存放着数据,指针域是指向下一个链表的指针,就类似与对应数据是3,那么指针域就指向下一个簇号为3的链表)
之所以使用这种读取方式,而不是直接连续的读取第2簇到第11簇的数据是因为:
我们在进行文件操作时,经常会伴随着删除操作,假设我们存储了A.TXT、B.TXT、C.TXT之后,删除了B.TXT,在这操作之后,我们又希望加入D.TXT,这个时候B.TXT的空间已经被释放了,我们当然希望D.TXT存储在B.TXT的空间上,但是如果D.TXT比B.TXT所占的空间要大,那么原本B.TXT的空间就不够D.TXT存放,此时又需要在C.TXT之后加上部分的D.TXT的内容;
整个文件系统的存储格式就是A.TXT、D.TXT、C.TXT、D.TXT,这个时候如果通过文件索引连续的读取D.TXT,是无法连续读取D.TXT中的数据内容的,这个时候通过链表的读取方式,就可以在读完原本B.TXT空间上的D.TXT之后,紧接着读取C.TXT之后的D.TXT内容。
2. FATFS文件系统
上一部分我们已经学习到了通过文件系统可以更加高效的读取数据,进行数据的增加和删除操作。本部分我们具体来学习FATFS文件系统。
C语言的文件操作:
文件的打开操作:fopen 打开一个文件
文件的关闭操作:fclose 关闭一个文件
文件的读写操作:fgetc 从文件中读取一个字符
fputc 写一个字符到文件中
fgets 从文件中读取一个字符串
fputs 写一个字符串到文件中
fprintf 往文件中写格式化数据
fscanf 格式化读取文件中的数据
fread 以二进制形式读取文件中的数据
fwrite 以二进制形式写数据到文件中
getw 以二进制形式读取一个整数
putw 以二进制形式存储一个整数
文件状态检查函数:feof 文件结束
ferror 文件读/写出错
clearerr 清除文件错误标志
ftell 了解文件指针的当前位置
文件定位函数:rewind 反绕
fseek 随机定位
C语言_文件操作(上)_light_2025的博客-优快云博客
C语言_文件操作(下)_light_2025的博客-优快云博客
注意:单纯的使用上述C语言文件操作函数是不能访问开发板上的文件的,因为这个函数不知道开发板上是否有文件操作的接口,也可以说它们是不知道我们的数据是存储在EEPROM中还是FLASH中的。
FATFS简介:
随着信息技术的发展,当今社会的信息量越来越大,以往由单片机构成的系统简单地对存储媒介按地址、按字节的读/写已经不满足人们实际应用的需要,于是利用文件系统对存储媒介进行管理成了今后单片机系统的一个发展方向。目前常用的文件系统主要有微软的 FATl2、FATl6、FAT32、NTFS,以及 Linux 系统下的 EXT2、EXT3 等。由于微软 Windows 的广泛应用,在当前的消费类电子产品中,用得最多的还是 FAT 文件系统,如 U 盘、MP 3、MP4、数码相机等,所以找到一款容易移植和使用、占用硬件资源相对较小而功能又强大的 FAT 开源文件系统,对于单片机系统设计者来说是很重要的。
FATFS是面向小型嵌入式系统的一种通用的FAT文件系统。它完全是由AISIC语言编写并且完全独立于底层的I/O介质。因此它可以很容易的不加修改的移植到其他的处理器当中,如8051单片机、AVR、ARM等。FATFS支持FAT12、FAT16、FAT32等格式。
利用前面写好的SD卡驱动程序,把FATFS文件系统代码移植到工程当中,就可以利用文件系统的各种函数,对SD卡以 “文件” 格式进行读写函数了。
层次结构:
FATFS Module 一开始就是为了能在不同的单片机上使用而设计,所以具有良好的层次结构。如下图,最顶层是应用层,使用者无需理会 FatFs Module 的内部结构和复杂的 FAT 协议,只需要调用 FatFs Module 提供给用户的一系列应用接口函数,如 f_open、f_read、f_write、f_close 等,就可以像在电脑上读写文件那样简单的操作文件系统了。
中间层的FatFs Module实现了FAT文件读/写协议。FatFs Module的完全版提供的是ff.c ff.h,简化版提供的是tff.c tff.h。除非有必要,使用者一般不用修改,使用的时候将需要版本的头文件直接包含进去即可。
需要使用者编写移植代码的是FatFs Module提供的底层接口,它包括存储媒介读/写接口 DiskIO 和 供给文件创建修改时间的实时时钟。
存储媒介的接口代码就是利用SPI进行初始化、读和写。
FATFS 的移植主要分为 3 步:
1. 数据类型:在 integer.h 里面去定义好数据的类型。这里需要了解你用的编译器的数据类型,并根据编译器定义好数据类型。
2. 配置:通过 ffconf.h 配置 FATFS 的相关功能,以满足你的需要。
1. _FS_TINY。这个选项在 R0.07 版本中开始出现,之前的版本都是以独立的 C 文件出现 (FATFS 和 Tiny FATFS),有了这个选项之后,两者整合在一起了,使用起来更方便。我们使用 FATFS,所以把这个选项定义为 0 即可。
2. _FS_READONLY。这个用来配置是不是只读,本章我们需要读写都用,所以这里设置 为 0 即可。
3. _USE_STRFUNC。这个用来设置是否支持字符串类操作,比如 f_putc,f_puts 等,本章 我们需要用到,故设置这里为 1。
4. _USE_MKFS。这个用来定时是否使能格式化,本章需要用到,所以设置这里为 1。
5. _USE_FASTSEEK。这个用来使能快速定位,我们设置为 1,使能快速定位。
6. _USE_LABEL。这个用来设置是否支持磁盘盘符(磁盘名字)读取与设置。我们设置 为 1,使能,就可以通过相关函数读取或者设置磁盘的名字了。
7. _CODE_PAGE。这个用于设置语言类型,包括很多选项(见 FATFS 官网说明),我们 这里设置为 936,即简体中文(GBK 码,需要 c936.c 文件支持,该文件在 option 文件夹)。
8. _USE_LFN。该选项用于设置是否支持长文件名(还需要_CODE_PAGE 支持),取值范 围为 0~3。0,表示不支持长文件名,1~3 是支持长文件名,但是存储地方不一样,我们选择使 用 3,通过 ff_memalloc 函数来动态分配长文件名的存储区域。
9. _VOLUMES。用于设置 FATFS 支持的逻辑设备数目,我们设置为 2,即支持 2 个设备。
10. _MAX_SS。扇区缓冲的最大值,一般设置为 512。
3. 函数编写:打开 diskio.c,进行底层驱动编写,一般需要编写 6 个接口函数
因为 FATFS 模块完全与磁盘 I/O 层分开,因此需要下面的函数来实现底层物理磁 盘的读写与获取当前时间。底层磁盘 I/O 模块并不是 FATFS 的一部分,并且必须由用户提供。
编写DiskIO需要编写6个接口函数:
① DSTATUS disk_initialize(BYTE drv);
存储媒介初始化函数。由于存储的媒介是SD卡,所以事实上是对SD卡的初始化。drv是存储媒介号码,FatFs只支持一个存储媒介,所以drv应该恒为0。执行无误返回0,错误时返回非0.
② DSTATUS disk_status(BYTE drv);
状态检测函数。检测是否支持当前的存储媒介(SD卡),对于FatFs来说,只要drv为0,就认为支持,然后返回0.
③ DRESULT disk_read(BYTE drv,BYTE*buff,DWORD sector,BYTE.count);
读扇区函数。在SD卡读接口函数的基础上编写,*buff存储已经读取的数据,sector是开始读的起始扇区,count是需要读的扇区数。1个扇区512个字节,执行无误返回0,错误返回非0.
④ DRESULT disk_write(BYTE drv,const BYTE*buff,DWORD sector,BYTE count);
写扇区函数。在SD卡写接口函数的基础上编写,*buff存储要写入的数据,sector是开始写的起始扇区,count是需要写的扇区数。1个扇区512个字节,执行无误返回0,错误返回非0.
⑤ DRESULT disk_ioctl(BYTE drv,BYTE ctrl,VoiI*buff);
存储媒介(SD卡)控制函数。ctrl是控制代码,*buff存储或接收控制数据。可以在此函数里编写 自己需要的功能代码,比如获得存储媒介的大小、检测存储媒介的上电与否存储媒介的扇区 数等。如果是简单的应用,也可以不用编写,返回0即可。
⑥ DWORD get_fattime(Void);
实时时钟函数。返回一个32位无符号整数,时钟信息包含在这32位中,
- bit31:25 年(O..127)从 1980 年到现在的年数
- bit24:21 月(1…12)
- bit20:16 日(1..31)
- bitl5.1] 时(O..23)
- bitl0:5 分(O..59)
- bit4:0 秒/2(0..29)
2.1 实际演练
首先在一张已经被格式化为FAT32格式的SD卡上创建一个文本文件,并在其输入20个字符,再将它插入到单片机系统中, 实现对这个文件的读取,将文件内容输出在调试终端上。
DBR(DOS BOOT RECORD 操作系统引导记录区)---DBR是我们进军FAT32的首道防线。事实上,DBR中的BPB部分才是这一区域的核心部分(第12~90字节为BPB),下图就是DBR操作区,其中对我们最有用的是彩色线标记的90个字节,这90个字节告诉了我们每扇区的字节数、每簇扇区数、磁道扇区数等等。对于这些信息的读取,只要遵循DBR中的字段定义即可。
其中彩色部分(也就是BPB部分)的意义如下:
jmpBoot:长度3,表示跳转命令,偏移量0(偏移量表示距起始位置的字节数,jmpBoot是首字节,所以偏移量是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
Vo1ID:长度4,偏移量67
FilSysType:长度11,偏移量71
FilSysType1:长度8,偏移量82
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]; //保留扇区数目 offset:14
unsigned char BPB_NumFATs[1]; //此卷中 FAT 表数 offset:16
unsigned char BPB_RootEntCnt[2]; //FAT32 为 0 offset:17
unsigned char BPB_TotSec16[2]; //FAT32 为 0 offset:19
unsigned char BPB_Media[1]; //存储介质 offset:21
unsigned char BPB_FATSz16[2]; //FAT32 为 0 offset:22
unsigned char BPB_SecPerTrk[2]; //磁道扇区数 offset:24
unsigned char BPB_NumHeads[2]; //磁头数 offset:26
unsigned char BPB_HiddSec[4]; //FAT 区前隐扇区数 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 char BPB_FSVer[2]; //FAT32 特有 offset:42
unsigned char BPB_RootClus[4]; //根目录簇号 offset:44
unsigned char FSInfo[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 char BS_BootSig[1]; // offset:66
unsigned char BS_VolID[4]; // offset:67
unsigned char BS_FilSysType[11]; // offset:71
unsigned char BS_FilSysType1[8]; //"FAT32 " offset:82
};
在程序中我们采用以上的结构体指针对扇区数据指针进行转化,就可以指针读取数据中的某一段,比方说我们想要读取BPB_BytePerSec,
可以((struct FAT32_DBR*)pSector)->BPB_BytePerSec 用该语句就可以得到这一字段的首地址
心细的读者可以发现读回来的字节拼在一起与实际的数据并不吻合。例如BPB_BytePerSec读出来的内容是“00 02”,在程序中我们把00作为int型变量的高字节,把02作为其低字节,那么这个变量的值就是2;但是实际的SD卡中扇区的大小为512个字节,512个字节和2个字节显然是不符合的。
这其实是大端模式和小端模式在作怪!值为2时(00 为高字节,02为低字节)为小端模式;而如果我们改为大端模式,将02作为高字节,00作为其低字节的话,变量值就成了0x0200(十进制的512),这样就和实际数据吻合了。可见FAT32中的字节排布是采用的小端模式,因为SD卡的扇区大小是512个字节,所以在程序中要把它转为大端模式的表达方式。
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[i]*fact; fact*=256; } return temp; }
该程序可以实现小端模式转换为大端模式;
这样就可以从BPB中读出关于磁盘的各种参数信息,为我们后面的工作做准备。从BPB中读取参数装入到参数表中以备后用的过程就是FAT32的初始化。
//定义从BPB中读取的参数的结构(也可以说是我们要从BPB中知道哪些参数)
struct FAT32_Init_Arg
{
unsigned char BPB_Sector_No; //BPB 所在扇区号
unsigned long Total_Size; //磁盘的总容量
unsigned long FirstDirClust; //根目录的开始簇
unsigned long FirstDataSector; //文件数据开始扇区号
unsigned int BytesPerSector; //每个扇区的字节数
unsigned int FATsectors; //FAT 表所占扇区数
unsigned int SectorsPerClust; //每簇的扇区数
unsigned long FirstFATSector; //第一个 FAT 表所在扇区
unsigned long FirstDirSector; //第一个目录所在扇区
unsigned long RootDirSectors; //根目录所占扇区数
unsigned long RootDirCount; //根目录下的目录与文件数
};
FAT文件分配表:
FAT表是FAT32文件系统中用于磁盘数据(文件)索引和定位引进的一种链式结构。可以说FAT表是FAT32文件系统最有特色的一部分,它的链式存储机制也是FAT32的精华所在,链式的存储结构可以使得数据的存储不连续。
从第一步中从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簇就已经结束了,没有后续的簇。
簇:
磁盘上最小可寻址存储单元称为扇区,通常每个扇区为512个字节。由于多数文件比扇区大得多,因此如果对一个文件分配最小的存储空间,将使得存储器能存储更多的数据,这个最小的存储空间即称为簇。
(这些话有些官方,簇简单来说就是:
簇作为FAT32进行数据存储的最小单元,内部扇区是不能再进行划分的,也就是说,哪怕一个簇里面只写了一个字节,该簇剩下的空间也不能写其他数据的,只能找另外没有被占用的簇来写;
所以对一个文件分配最小的存储空间也是为了减少空间浪费;防止一个簇只写1个字节而导致其余空间浪费的现象发生)
我们按照初始化参数表中SectorsPerClust可以知道一个簇中的扇区数;TEST.TXT这样一个只有20个字节的文件,也会占用一个簇(磁盘上最小的可存储空间)的容量;如下图,文件大小为20个字节,占用空间为2048个字节(注意:这个2048个字节就是一个簇的容量,也就是磁盘分配的最小可存储空间,分配这个簇的目的是为了最大程度的减小空间的浪费,1个扇区512个字节,2048个字节就是4个扇区)
图中红色标记的就是文件所占的26个簇,从第4个簇开始,簇项的内容为 “05 00 00 00”,这显然是小端模式,根据链表的存储规则,指针域指向说明下一个簇为第5簇,而簇项5的内容为 “06 00 00 00”,说明紧接的簇是第6个簇,……依次类推,直到内容是 “FF FF FF 0F”,说明无后继簇,文件数据到此结束。
大端模式:数据的高字节保存在内存的低地址上,数据的低字节保存在内存的高地址上;
小端模式:数据的高字节保存在内存的高地址上,数据的低字节保存在内存的低地址上;
从中可以发现,当数据结束于某一簇时,FAT32就用 “FF FF FF 0F”来对其进行标记。还有其他的标记,例如 “00 00 00 00”表示未分配的簇,“FF FF FF F7”表示坏簇等。
//给出一个簇号,计算出它的后继簇号
unsigned long FAT32_GetNextCluster(unsigned long LastCluster)
{
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表变得臃肿;
假设SD卡的FAT表大小为958个扇区(BE 03 00 00的大端表示);如果这958个扇区每四个字节表示一个簇项,则它可以表示(958*512/4)-2=122622个簇(958个扇区乘以512:1个扇区512个字节,整体除以4:4个字节表示一个簇,这样可以计算出有多少个簇,1个簇4个字节,2个簇8个字节,减去两个簇表示减去8个字节的FAT表头标识)
链表查找下一个数据域是需要找到首簇的,只有找到链表的首簇号才能继续查找下一簇数据的位置,直到数据结束。
那么具体如何找到首簇号呢?
通过根目录区就可以由一个文件的文件名来查到它的首簇。
根目录区:
在文件系统中其实已经把文件的概念进行了扩展,目录同样也是文件;根目录的地位和其他目录是相同的,根目录也被看做是文件。是文件就会有文件名,根目录的名称就是磁盘的卷标。
根目录的名称就是ZNMCU。
下图中,记录1描述根目录,前8个字节为文件名 “ZNMCU”(长度小于8的部分用空格符补齐),下面的三个字节为扩展名(长度小于3的部分有空格符补齐),08表示此文件为卷标,开始簇高字节为00 00,低字节为00 00,开始簇为0,文件长度为0.
记录 2 描述 TEST.TXT 文件,文件名为“TEST ”,扩展名为“TXT”,20 表示此文件为归档,开始簇为 3(“00 00 00 03”),长度为 20。
记录 3 描述 BIGTEST.TXT 文件,文件名为“BIGTES~1”,扩展名为“TXT”,开始簇为 4,长度为 5200 字节(00 00 CB 20)。
//文件目录项结构的实现
struct direntry
{
unsigned char deName[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,得到首簇就可以以它为起点来读取文件内容了
文件读取:
通过根目录区我们已经得到了TEST.TXT的首簇。现在要做的就是到相应的簇及其后继簇去读取数据。
主要思想:在已知各文件首簇的前提下,从首簇开始,对于文件满一簇的数据,把整簇数据读出(按照扇区来读,一次性读出整个扇区),对于文件结尾不足一簇的部分,计算它占用簇内几个扇区,把占用整个扇区部分直接按照扇区读出,而最后很大可能是零散的若干个字节,不足一个扇区,也就是占用了最后一个文件的最后一个扇区的一部分(对于这部分来讲我们也要将整个扇区读出,截选中有效的数据部分),
2.2 FATFS读书笔记整理
这一部分内容是参考《数据重现》一书整理的读书笔记,更进一步的理解、学习文件系统。
硬盘结构:
硬盘由两大部分组成:控制电路板和盘体。
1. 控制电路板是由接口、DSP处理器、ROM、缓存、磁头驱动电路和盘片电机驱动电路组成的。
2. 盘体是由盘腔、上盖、盘片电机、盘头、磁头、音圈和其他辅助组件组成的。
磁道:
当磁盘在旋转时,磁头若保持在一个位置上,则每个磁头都会在磁盘表面划出一个圆形轨道,这些圆形轨迹就叫做磁道。(这有点类似于我们老一辈的唱片机)每张盘片上的磁道由外向内依次从0开始进行编号。虽然磁道上的编号由外向内是从0开始编号的,但这并不意味着 0 磁道位于磁盘片的最外沿。固件区的物理位置有的位于比 0 磁道更靠近磁盘片的外缘的磁道上。有的位于磁盘片的中部。
扇区:
磁盘上每个磁道被等分为若干个弧段,这些弧段便是磁盘的扇区。每个扇区的大小为512个字节。扇区从 1 开始编号 。
柱面:
磁盘通常由重叠的一组盘片组成。由于每个盘面都被划分为数目相等的磁道,从外圈的 0 开始编号,具有相同编号的磁道形成一个圆柱,这个圆柱我们称之为磁盘的柱面。磁盘上数据的存取是沿着柱面进行的,也就是在一个柱面内依次从低号盘片向高号盘片写入,写满一个柱面后再转到下一个柱面。
磁盘的柱面数和一个盘面上的磁道数相等。由于每一个盘面都有自己的磁头,因此,盘面数等于总的磁头数。所谓硬盘的CHS,就是Cylinder(柱面)、Head(磁头)、Sector(扇区)。
硬盘的启动过程:
1. 硬盘上电后,DSP首先运行ROM中的程序,部分硬盘会检查各部件的完整性。
2. 盘片电机启动,当转速达到预定的转速时,磁头开始运作,定位到盘片的固件区,读取硬盘的固件区和坏道表,部分硬盘会先将 ROM 中记忆的系列号与盘片上的进行比较,如果不一致,硬盘会终止初始化工作。
3. 当所有必须的固件正确读出时,磁盘进入就绪状态,等待接收指令进行数据的读写操作。
寻址方式:
1. &C/H/S寻址方式
使用Cyclinder(柱面)、Head(磁头)、Sector(扇区)三个参数来定位唯一的扇区地址。
2. &LBA寻址方式
LBA寻址方式即Logic Block Address(逻辑块地址),又称为 “线性寻址模式”。
数的存储格式:
大端模式:字节由最高位向最低位依次存放至内存中,高位在前,低位在后。
小端模式:字节由最低位向最高位依次存放至内存中,低位在前,高位在后。
比如说:一个十六进制数,00 23 0f 4a
大端模式:00 23 0f 4a 也就是原本十六进制中的高字节存放在内存中的高位上,低字节存放在内存中的低位上
小端模式:4a 0f 23 00
文件系统FATFS感官的认识:
文件系统就是对数据进行存储与管理的方式。
就比方说我们自己的电脑上,我们的硬盘都是几百个G的容量,我们天天的往里存放各种各样的文件,有文档文件,有音乐文件,有视频文件;试想一下,如果没有一个管理数据的家伙,那么我们的硬盘将会变的乱七八糟。最要命的是,我们无从读取我们之前存储的数据,我们根本不知道之前存储的数据放在哪里。但是一旦有了文件系统,我们就可以把任意文件存放到任意的位置,更有甚者有些文件我们不想让其他人知道,我们甚至可以隐藏起来。
文件系统会在你把某个文件存放到某个目录下时记录你文件的信息(文件存放的起始簇号,文件大小,文件创建、修改、访问、保存的时间等等)。当我们下一次去访问那个文件时,文件系统就会根据已知的信息去寻址它,进而完成文件的读写操作,文件系统也会同步更新,为下次寻址该文件做准备。
文件系统是为了长久的存储和访问数据而为用户提供的一种基于文件和目录的存储机制。
我们在使用硬盘进行数据存储之前,首先要对分区进行格式化。格式化的过程就是在分区内建立文件系统的过程。一个文件系统由系统结构和按一定规则存放的用户数据组成。(在windows下当我们要格式化一个分区或者存储介质时,windows都会弹出一个对话框,会让我们选择文件系统FAT32或者NTFS文件系统,当按下格式化以后,操作系统就会为这个分区建立我们所选择的文件系统)
数据单元:
数据在写入磁盘或者从磁盘读取数据时每次操作的数据量称为数据单元。它的大小在建立文件系统时确定。数据单元在不同的文件系统中有不同的称呼:例如在 FAT 和 NTFS 文件系统中称作“簇(Cluster)”, ExtX 中称作“块(Block)”等。一个数据单元由若干个连续的扇区组成,大小总是 2 的整数次幂个扇区。
2.3 FAT文件系统的神秘面纱
FAT(File Allocation Table,文件分配表)文件系统是 windows 操作系统所使用的一种文件系统,它的发展过程经历了FAT12,FAT16,FAT32三个阶段。
FAT文件系统用 “ 簇 ” 作为数据单元。一个 “簇” 由一组连续的扇区组成,簇所含的扇区数必须是 2 的整数次幂。簇的最大值为64个扇区,也就是32KB。所有簇从2开始编号,每个簇都一个自己的地址编号。用户文件和目录都存储在簇中。
FAT文件系统的数据结构中有两个重要的结构:文件分配表和目录项
文件分配表用来描述如何找到另外的簇。一个文件或者文件夹如果需要多余的簇,FAT文件用于指出文件的下一个簇,同时也说明簇的分配状态。
目录项记录着文件名、大小、文件内容起始地址以及其他一些元数据。
在FAT文件系统中,文件系统的数据记录在 “引导扇区DBR中”。引导扇区位于整个文件系统的 0 号扇区,是文件系统隐藏区域的一部分。DBR中记录着文件系统的起始位置、大小、FAT表个数以及大小等相关信息。
在FAT文件系统中,同时使用着 “扇区地址”和 “簇地址” 两种地址管理方式。存储用户数据的数据区使用簇进行管理,其他文件系统管理数据区域使用扇区地址进行管理。文件系统的起始扇区为 0 号扇区。
FAT文件系统的整体布局:
1. 保留区含有一个重要的数据结构——系统引导扇区(DBR)。
2. FAT区由各个大小相等的FAT表组成——FAT2紧跟FAT1之后。
3. FAT12、FAT16的根目录虽然也属于数据区,但他们并不是由簇进行管理的。也就是说 FAT12、 FAT16 的根目录是没有簇号的,他们的 2 号簇从根目录之后开始。而 FAT32 的根目录通常位于 2 号簇。
2.3.1 引导扇区
引导扇区是FAT32的第一个扇区,也被称为DBR扇区。
引导扇区记录着:
- 1. 每扇区的字节数
- 2. 每簇的扇区数
- 3. 保留扇区数
- 4. FAT表个数
- 5. 文件系统的大小(扇区数)
- 6. 每个FAT表大小(扇区数)
- 7. 根目录起始簇号
- 8. 其他一些附加信息
注:DBR中记录文件系统参数的部分也称为BPB(BIOS Parameter Block)
以上这些参数是至关重要的。
1. 通过每个FAT表的大小扇区数乘以FAT表的个数就可以得到FAT区域的大小。
2. 通过保留扇区数和FAT区域的大小就可以得知数据区的起始位置。也就得到了文件系统第一簇的位置。
保留扇区是指从硬盘第二个扇区开始,用于加载并转让处理器控制控制权给操作系统。简单来说就是FAT区域分区中的未用区域。
3. 由根目录的簇号和第一簇的位置就可以得到根目录的位置。
根目录的簇号就是根目录第一簇的簇号。
(数据的存储是以小端模式存储的,也就是低字节放在内存中的低位上,高字节放在内存中的高位上)
【1】0x00~0x02:3 个字节,跳转指令。
【2】0x03~0x0A:8 个字节,文件系统标志和版本号,这里为 MSDOC5.0。
【3】0x0B~0x0C:2 个字节,每扇区字节数,512(0X02 00)。
【4】0x0D~0x0D:1 个字节,每簇扇区数,8(0x08)。
【5】0x0E~0x0F:2 个字节,保留扇区数,704(0x02 C0)。
【6】0x10~0x10:1 个字节,FAT 表个数,2。
【7】0x11~0x12:2 个字节,根目录最多可容纳的目录项数,FAT12/16 通常为 512。FAT32 不使用此处值,
置 0。
【8】0x13~0x14:2 个字节,扇区总数,小于 32MB 时使用该处存放。超过 32MB 时使用偏移 0x20~0x23 字
节处的 4 字节存放。笔者的 SD 卡容量为 2GB,所以不使用该处,置 0.
【9】0x15~0x15:1 个字节,介质描述符,0xF8 表示本地硬盘。
【10】0x16~0x17:2 个字节,每个 FAT 表的大小扇区数(FAT12/16 使用,FAT32 不使用此处,置 0)。
【11】0x18~0x19:2 个字节,每磁道扇区数,63(0x00 3F)。
【12】0x1A~0x1B:2 个字节磁头数,255(0x00 FF)。
【13】0x1C~0x1F:4 个字节,分区前已使用扇区数,137(0x00 00 00 89)。
【14】0x20~0x23:4 个字节,文件系统大小扇区数,3841911(0x 00 3A 9F 77)。
【15】0x24~0x27:4 个字节,每个 FAT 表的大小扇区数,3744(0x 00 00 0E A0)。
【16】0x28~0x29:2 个字节,标记。
【17】0x2A~0x2B:2 个字节,版本号。
【18】0x2C~0x2F:4 个字节,根目录簇号,2。(虽然在 FAT32 文件系统下,根目录可以存放在数据区的任
何位置,但是通常情况下还是起始于 2 号簇)
【19】0x30~0x31:2 个字节,FSINFO(文件系统信息扇区)扇区号,1。(上图的标注即用黄色条纹的标注
有误,请读者注意)该扇区为操作系统提供关于空簇总数及下一可用簇的信息。
【20】0x32~0x33:2 个字节,备份引导扇区的位置,6。(上图的标注即用黄色条纹的标注有误,请读者注
意)备份引导扇区总是位于文件系统的 6 号扇区。
【21】0x34~0x3F:12 个字节,未使用。
【22】0x40~0x40:1 个字节,BIOS INT 13H 设备号,0x80。(这个我也不知道什么意思☺)
【23】0x41~0x41:1 个字节,未用。
【24】0x42~0x42:1 个字节,扩展引导标志。0x29。
【25】0x43~0x46:1 个字节,卷序列号。通常为一个随机值。
【26】0x47~0x51:11 个字节,卷标(ASCII 码),如果建立文件系统的时候指定了卷标,会保存在此。笔
者当时没有指定卷表,上图中的 YCY 是后来指定的。
【27】0x52~0x59:8 个字节,文件系统格式的 ASCII 码,FAT32。
【28】0x5A~0x1FD:410 个字节,未使用。该部分没有明确的用途。
【29】0x1FE~0x1FF:签名标志“55 AA”。
上图是SD卡的整体布局;
以上提供的参数至关重要,因为文件系统初始化的第一步就是要找这些参数。我们的SD卡没有分区,默认就是一个分区。上述的参数就是相对于MBR的地址偏移量(MBR如上图中的保留区),事实上,我们要明确MBR的扇区地址才是整个SD卡的扇区号为0的地址,引导扇区DBR并不是整个SD卡最开始的地方,引导扇区DBR处于MBR保留区之后。
2.3.2 引导代码
引导扇区的前三个字节为一个由机器代码构成的跳转命令,该跳转命令的作用是使得CPU越过跟在其后面的配置数据跳转到引导代码处。
FAT32文件系统引导扇区的512个字节中,90~509字节为引导代码。
2.3.3 FSINF0信息扇区
FAT32的保留区中增加了一个FSINF0扇区,用来记录文件系统中空闲簇的数量以及下一可用簇的簇号等信息。
大多数的FSINF0信息扇区一般位于文件系统的1号扇区
【1】0x00~0x03: 4 个字节,扩展引导标志“52526141”。
【2】0x04~0x1E3:480 个字节,未使用,全部置 0。
【3】0x1E4~0x1E7: 4 个字节,FSINFO 签名“72724161”。
【4】0x1E8~0x1EB: 4 个字节,文件系统的空簇数,4294967295(0xFF FF FF FF)。
【5】0x1EC~0x1EF: 4 个字节,下一可用簇号,2(0x 00 00 00 02)。
//这里4 5 两项的数据是原始数据,下一可用的簇号为2,也就是说文件系统的系统数据区的起始扇区号就是2号簇,因为0号和1号FAT表项被系统写入特定的值
【6】0x1F0~0x1FD: 14 个字节,未使用。
【7】0x1FE~0x1FF: 2 个字节,“55 AA”标志。
2.3.4 FAT表
位于保留区后的是FAT区,由两个完全相同的FAT(File Allocation Table,文件分配表)表组成。
1. 对于文件系统来说,FAT表有两个重要作用:描述簇的分配状态以及标明文件或目录的下一簇的簇号。
2. 通常情况下,一个FAT文件系统会有两个FAT表。
3. FAT区紧跟在文件保留区之后,所以FAT1在文件系统中的位置可以通过引导记录中的偏移0x0E~0x0F字节处的 “保留扇区数” 得到。(因为保留区都是未用的扇区,所以得到保留区数也就相当于得到了FAT1的位置)
4. FAT2 紧跟在 FAT1 之后,它的位置可以通过 FAT1 的位置加上 FAT 表的大小扇区数计算出来。
FAT特性:
FAT表由一系列大小相等的FAT表项组成。
1. FAT32 中每个簇的簇地址,是有 32bit(4 个字节)记录在 FAT 表中。FAT 表中的所有字节位置以 4 字节为单位进行划分,并对所有划分后的位置由 0 进行地址编号。0 号地址与 1 号地址被系统 保留并存储特殊标志内容。从 2 号地址开始,每个地址对应于数据区的簇号,FAT 表中的地址编 号与数据区中的簇号相同。我们称 FAT 表中的这些地址为 FAT 表项,FAT 表项中记录的值称为 FAT 表项值。
2. 当文件系统被创建,也就是进行格式化操作时(文件系统被创建等价于文件系统的格式化),分配给 FAT 区域的空间将会被清空,在 FAT1 与 FAT2 的 0 号表项与 1 号表项写入特定值。由于创建文件系统的同时也会创建根目录,也就是为根 目录分配了一个簇空间,通常为 2 号簇,所以 2 号簇所对应的 2 号 FAT 表项也会被写入一个结束 标记。
3. 如果某个簇未被分配使用,它所对应的 FAT 表项内的 FAT 表项值即用 0 进行填充,表示该 FAT 表 项所对应的簇未被分配。
4. 当某个簇已被分配使用时,则它对应的 FAT 表项内的 FAT 表项值也就是该文件的下一个存储位置 的簇号(类似于链表)。如果该文件结束于该簇,则在它的 FAT 表项中记录的是一个文件结束标记,对于 FAT32 而言,代表文件结束的 FAT 表项值为 0x0FFFFFFF。
5. 如果某个簇存在坏扇区,则整个簇会用 FAT 表项值 0xFFFFFF7 标记为坏簇,不再使用,这个坏簇 标记就记录在它所对应的 FAT 表项中。
6. 由于簇号起始于 2 号,所以 FAT 表项的 0 号表项与 1 号表项不与任何簇对应。FAT32 的 0 号表项 值总是“F8FFFF0F”。如上图所示。
7. 1 号表项可能被用于记录脏标志,以说明文件系统没有被正常卸载或者磁盘表面存在错误。不过 这个值并不重要。正常情况下 1 号表项的值为“FFFFFFFF”或“FFFFFF0F”。
8. 在文件系统中新建文件时,如果新建的文件只占用一个簇,为其分配的簇对应的 FAT 表项将会写 入结束标记。如果新建的文件不只占用一个簇,则在其所占用的每个簇对应的 FAT 表项中写入为 其分配的下一簇的簇号,在最后一个簇对应的 FAT 表象中写入结束标记。
9. 新建目录时,只为其分配一个簇的空间,对应的 FAT 表项中写入结束标记。当目录增大超出一个 簇的大小时,将会在空闲空间中继续为其分配一个簇,并在 FAT 表中为其建立 FAT 表链以描述它 所占用的簇情况。
10. 对文件或目录进行操作时,他们所对应的 FAT 表项将会被清空,设置为 0 以表示其所对应的簇处 于未分配状态。
一个文件的起始簇号记录在它的目录项中(2号簇),该文件的其他簇则用一个簇链结构记录在 FAT 表中。如果要寻找一个文件的下一簇,只需要查看该文件的目录项中描述的起始簇号所对应的 FAT 表项,如果该文件 只有一个簇,则此处的值为一个结束标记;如果该文件不止一个簇,则此处的值是它的下一个簇的簇号。
2.3.5 FAT32数据区
数据区才是真正用于存放用户数据的区域。数据区紧跟在 FAT2 之后,被划分成一个个的簇。所有的簇 从 2 开始进行编号。也就是说,2 号簇的起始位置就是数据区的起始位置。(这正好对应了FAT表的簇号分配,0号和1号簇用来存放特定数据,2号簇存放目录,和数据区的簇号一一对应)
原则上,FAT32允许根目录位于数据区的任何位置,但是通常情况下它都位于2号簇。
1. 定位根目录
在 FAT 文件系统中,寻找第一簇(即 2 号簇)的位置也就是寻找数据区的开始位置(数据簇的起始位置就是2号簇,和FAT表的2号簇是一一对应的),这并不是一件容易的事,因为它不是位于文件系统开始处,而是位于数据区。在数据区前面是保留区域和 FAT 区域,这两 个区域都不使用 FAT 表进行管理(直接写入特定信息的)。因此,数据区以前的区域只能使用扇区地址(逻辑卷地址),而无法使用簇地址。
要想定位一个 FAT32 文件系统的数据起始处,可以通过引导扇区的相关参数计算出来。1. 从引导扇区的偏移 0x0E~0x0F 字节处得到保留扇区个数。2. 从偏移 0x10 字节处得到 FAT 表的个数。3. 从偏移 0x24~0x27 字 节处得到每个 FAT 表的大小扇区数。4.利用如下公式计算:
保留扇区数 + 每个 FAT 表大小扇区数 × FAT 表个数 = 数据区起始扇区号
要想计算其他已知簇号的扇区号,还要由引导扇区的偏移 0x0D 字节处查找到每个簇大小扇区数,并 使用如下公式计算:
某簇起始扇区号 = 保留扇区数 + 每个 FAT 表大小扇区数 × FAT 表个数 + (该簇簇号 - 2) × 每簇扇区数
其中,之所以使用 该簇簇号-2 是因为在FAT表中第 0 号和第 1 号是写入特定信息的,第2号簇是根目录,和数据区起始位置一一对应的。所需要减去这两个簇。
2. 根目录分析
根目录在文件系统建立时(文件格式化时)即已被创建,其目的就是存储目录(也称文件夹)或文件的目录项。每个目录项的大小为 32 个字节。
文件系统刚被创建时,还没有存储任何数据时,根目录下没有任何内容,文件系统只是为根目录分配 了一个簇的空间(通常为 2 号簇),将结束标记写入该簇对应的 FAT 表项,表示该簇已经被分配使用。这时候,为根目录分配的空间没有任何内容。但如果在创建文件系统的时候是定了卷标,则会在根目录下为其建立一个卷标目录项,该目录项占用根目录中的第一个目录项位置。
下图显示了刚刚创建的 FAT32 文件系统的根目录,该文件系统的卷标为“YCY”。
卷标是一个磁盘的一个标识,不唯一。由格式化自动生成或人为设定。仅仅是一个区别于其他磁盘的标识而已。
不管是根目录还是子目录下的目录项,都具有以下的基本特性:
1. 为文件或子目录分配的第一个簇的簇号记录在它的目录项中,其他后续簇则由 FAT 表中的 FAT 表 链进行跟踪。
2. 目录项中除记录子目录或文件起始簇号外,还记录它的名字、大小(子目录没有大小)、时间值等信息。
3. 每个子目录或文件除具有一个短文件目录项外,还会有长文件名目录项。
4. 短文件名目录项固定占用 32 字节,长文件名目录项则根据需要占用 1 个或者若干个 32 字节。
5. 对于同一个子目录或文件,它的长文件名目录项存放在它的短文件名目录项之前,如果长文件名 目录项占用多个 32 字节,则按倒序存放于短文件名目录项之前。
拓展:
1. 何为短文件名?
短文件名是DOS+FAT12/FAT16时代的产物,命名规则为8.3
8是指文件名,3是指扩展名(完整文件=文件名.扩展名)
文件名不能超过8个字节,如果多于8个字节,在DOS里不会被识别
扩展名不能超过3个字节,如果多于3个字节,在DOS里不会被识别2. 何为长文件名?
文件名超过8个字节或扩展名超出3个字节都是长文件名
FAT32文件系统完全支持长文件名,长文件名记录在目录项中,可能一个文件名占据多个目录项3. 目录项
在 FAT32文件系统中,根据结构不同可以将目录项大致分为四种:卷标目录项、 “.”目录项和“..”目录项、短文件名目录项、长文件名目录项。短文件名目录项是最重要的数据结构,其中存放着有关子目录或文件的短文件名、属性、起始簇号、时间值以及内容大小等信息。在 FAT32 文件系统中,将子目录看作是一种特殊的文件
2.3.6 子目录
在 FAT32 文件系统中,除根目录在创建文件系统时即被建立并分配空间外,其他所有的子目录都是在使用过程中根据需要建立的。新建一个子目录时,在其父目录中为其建立目录项,在空闲空间中为其分配 一个簇并对该簇进行清零操作,同时将这个簇号记录在它的目录项中。如果在根目录下创建一个子目录, 我们就称这个子目录为根目录的子目录,称根目录为这个子目录的父目录。
创建子目录时,在为其父目录分配的簇中建立目录项,目录项中描述了这个目录的起始簇号。在为子目录建立目录项的同时,也在为子目录分配的簇中,使用前两个目录项描述它与父目录的关系。
创建子目录时,在为其父目录分配的簇中建立目录项,目录项中描述了这个目录的起始簇号。在为子目录建立目录项的同时,也在为子目录分配的簇中,使用前两个目录项描述它与父目录的关系。
这两句话的意思是:
我们建立的子目录下并没有再建立任何下级子目录和文件,但却在其占有的簇空间中建立了两个目录项,第一个是 “.” 目录项,它描述该子目录本身的一些信息;第二个是 “..” 目录项,它描述的是该子目录的父目录(也就是根目录)的相关信息。通过这种方式,就在子目录和父目录之间建立起了联系。 在子目录中建立下一级子目录或文件时,为下级子目录或文件建立的目录项将从第三个目录项开始写入。
注:
子目录和根目录不同之处只在于根目录是在创建文件系统时就建立了的,如果没有卷标和内容,分配给根目录的簇空间内没有任何内容。而子目录是在存储过程中根据需要由用户建立的,随着子目录建立的同时,会在为其分配的簇空间开始处建立两个目录项来描述子目录本身和其父目录的信息,以使字符目录间建立联系。
下图就表示我们在根目录下建立了一个子目录yatou。
2.3.7 目录项
在 FAT32 文件系统中,根据结构不同可以将目录项大致分为四种:卷标目录项、“.”目录项和“..” 目录项、短文件名目录项、长文件名目录项。短文件名目录项是最重要的数据结构,其中存放着有关子目 录或文件的短文件名、属性、起始簇号、时间值以及内容大小等信息。在 FAT32 文件系统中,将子目录看作是一种特殊的文件。
1. 短文件名目录项
短文件名目录项是 FAT 文件系统中非常重要的一种数据结构。之所以称其为短文件名目录项,是因为它所记录的文件名延续了 DOS 时代的 8.3 格式,即 8 个字符的名字加上 3 个字符的扩展名:
如果文件名不足 8 个字符,用 0x20 进行填充。
超过 8 个字符时则会被截短,因为短文件名目录项中没有足够的空间记录超出的部分。截短的 方法是取文件名的前 6 个字符加上“~1”(如果有同名文件,则会依次递增该数值),然后加上 其扩展名。
如果是子目录,则将扩展名部分用“0x20”进行填充。
为了解决长文件名的问题,FAT 文件系统又增加了一种“长文件名”目录项结构。其从 windows95 开 始,不管文件名的长度是否超过 8 个字符,都会同时为其创建短文件名目录项和长文件名目录项,因为短 文件名不区分大小写,而长文件名则是区分大小写的。
2. 短文件名目录项的特性
每个文件或子目录都分配有一个大小为 32 字节的目录项,用以描述文件或目录的属性。
1. 所有的目录项并不是具有相同的地址,要找到一个目录项的位置只能用分配给文件或子目录的全名进行是搜索(也就是用文件夹的重命名的名称进行搜索)。
2. 目录项结构中有一个属性区域,每个文件可以设置 7 种属性。
3. 每个文件或目录还有四个非关键属性:
- 只读属性
- 隐藏属性
- 系统属性
- 存档属性
4. 每个目录项(文件夹)包括三个时间值,即建立时间、最后访问时间、最后修改时间:
- 建立时间,精确到十分之一
- 最后访问时间,精确到日
- 最后修改时间,精确到2秒
5. 一个目录项是否被分配是用它的第一个字节来描述。对于已经分配使用的目录项,它的第一个字节是文件名的第一个字符,而文件或目录被删除后,它所对应的目录项的第一个字节将被置为 0xE5,这就是为什么有的 FAT 数据恢复工具需要用户自己输入文件名的第一个字符的原因。
3. 短文件名目录项的数据结构
每个短文件名目录项占32个字节。
【1】0x00~0x00:1 个字节,如果该目录项正在使用中,则为文件名或子目录名的第一个字符。
- 0x00:说明该目录项未被分配使用。
- 0xE5:说明该目录项曾经被使用过,但是现在已被删除。目前处于未分配状态
【2】0x01~0x0A:10 个字节,文件名的第 2 至第 11 个 ASCII 码,除扩展名外,如果文件的名字不足 8 个字符则用 0x20 进行填充。
【3】0x0B~0x0B:1 个字节,所描述文件的属性
- 0x01-只读
- 0x02-隐藏
- 0x04-系统文件
- 0x08-卷标
- 0x0F-为此值时表示该目录项为长文件名目录项
- 0x10-目录
- 0x20-存档
【4】0x0C~0x0C:1 字节,保留
【5】0x0D~0x0D:1 个字节,文件穿件的时间,精确到创建时刻的十分之一秒
【6】0x0E~0x0F:2 个字节,文件创建的时间——时分秒
两个字节的 16bit 被划分为 3 个部分:
- 0~4bit 为秒,以 2 秒为单位,有效值为 0~29,可以表示的时刻为 0~58
- 5~10bit 为分,有效值为 0~59
- 11~15bit 为时,有效值为 0~23
下面举例说明: 如上图所示,其子目录项偏移 0x0E~0x0F 字节处的内容为“A1A9”,我们来计算一下。由于 FAT 文件系统数据采用的小端存储方式,因此“A1A9”表示成 16进制为 0xA9A1,换算成 2进制就是 1010 1001 1010 0001, 我们将其分成三部分并计算它的值,如下图所示:
【7】0x10~0x11:2 个字节,文件创立的日期,16bit 也划分为三个部分:
- 0~4bit 为日,有效值为 1~31
- 5~10bit 为月,有效值为 1~12
- 11~15bit 为时,有效值为 0~127,这是一个相对于 1980 年的年数值,也就是说该值加上 1980 即为文件创建的日期值。该部分笔者就不再举例就计算了,原理和计算创建时间是一样的。请 读者自己去计算。
【8】0x12~0x13:2 个字节,最后访问日期。
【9】0x14~0x15:2 个字节,文件起始簇号的高两个字节。
【10】0x16~0x17:2 个字节,文件最后修改的时间。
【11】0x18~0x19:2 个字节,文件最后被修改时的日期。
【12】0x1A~0x1B:文件内容起始簇号的低两个字节,与 0x14~0x15 字节处的高两个字节组成文件内容起始簇号。
【13】0x1C~0x1F:文件内容大小字节数,只对文件有效,子目录的目录项此处全部设置为 0。
实例分析:
首先我们在卷标为 ycy 的 SD 卡中再建立一个 yatou.txt 空的文本文件和一个写有数据的 ycy.txt 文本文件。即当前 SD 卡下只有三个文件:yatou(目录)、yatou.txt(空的文本文件)、ycy.txt(写有数据的文本文件)。如 下图:
![]()
4. 长文件名目录项
FAT32 文件系统在为文件分配短文件名目录项的同时会为其分配长文件名目录项。文件系统在为文件 创建长文件名(Long File Name,LFN)类型的目录项时,并没有舍弃原有的短文件名目录项,具有 LFN 的 文件同时也有一个常规的 SFN(Short File Name,短文件名)类型目录项。之所以仍然需要 SFN,是因为 LFN 目录项只包含文件的名字,而不包括任何有关时间、大小及起始簇号等信息,这些信息仍然需要用 SFN 目录项来记录。
5. 长文件名目录项特征
如果一个文件的文件名超过了 8 个字符,则会为其名字截短后为其建立短文件名。将短文件名存储在短文件名目录项中。长文件名则存放在长文件名目录项中。长文件名目录项有以下的特性:
1. LFN 和 SFN 目录项结构在相同位置有一个属性标志字节,LFN 目录项使用一个特定的属性值,以 说明它是一个长文件名项。
2. 项中的其他字节,使用 UTF-16 编码(UTF-16 是 Unicode 的其中一个使用方式。 UTF 是 Unicode/UCS Transformation Format,即把 Unicode 转做某种格式的意思),存储 13 个 Unicode 字符的文件名,每个字符占用两个字节。
3. 如果文件名长于 13 个字符,则继续为其分配 LFN 项,直到够用为止。
4. 所有 LFN 都包含一个校验和,通过这个校验和将其与相应的 SFN 项关联起来。
5. 一个文件的所有 LFN 项按倒序排列在它的 SFN 项前面,即文件名的第一部分距离 SFN 是最近的。
6. 长文件名目录项数据结构
【1】0x00~0x00:1 个字节,长文件名目录项的序列号,一个文件的第一个长文件名序列号为 1,然 后依次递增。如果是该文件的最后一个长文件名目录项,则将该目录项的序号与 0x40 进行“或(OR)运算” 的结果写入该位置。如果该长文件名目录项对应的文件或子目录被删除,则将该字节设置成删除标志 0xE5。
【2】0x01~0x0A:5 个字节,长文件名的第 1~5 个字符。长文件名使用 Unicode 码,每个字符需要两 个字节的空间。如果文件名结束但还有未使用的字节,则会在文件名后先填充两个字节的“00”,然后开始 使用 0xFF 填充。
【3】0x0B~0x0B:1 个字节,长目录项的属性标志,一定是 0x0F。
【4】0x0C~0x0C:保留。
【5】0x0D~0x0D:1 个字节,校验和。如果一个文件的长文件名需要几个长文件名目录项进行存储, 则这些长文件名目录项具有相同的校验和。
【6】0x0E~0x19:12 个字节,文件名的第 6~11 个字符,未使用的字节用 0xFF 填充。
【7】0x1A~0x1B:2 个字节,保留。
【8】0x1C~0x1F:4 个字节,文件名的第 12~13 个字符,未使用的字节用 0xFF 填充。
实例分析:
首先我们在根目录下建立一个名字为“amp3foryatoumadebyfgd20090808summer.txt”的文件,然后 用 winhex 来看看它的长文件名目录项,如下图:
7. “.” 目录项和 “..” 目录项
前面曾经介绍过,一个子目录的起始簇,前两个目录为“.”目录项和“..”目录项,子目录通过这 两个目录项及它在父目录中的目录项建立起父子目录的联系。
- “.” 目录项位于子目录起始簇的第一个目录项位置,它用以表明该簇是一个子目录的起始簇。 另外,该目录项实际上是对目录自身的描述,它记录了该子目录时间信息、起始簇号等。需要注意的是,它所记录的起始簇号也就是该子目录目前所处的位置。
- “..”目录项位于子目录起始簇的第二个目录项位置,用于描述该子目录的父目录的相关信息。
- 第一个目录项也就是“.”目录项,记录的簇号为 4 号簇,也正是本子目录所在的簇。小端模式0x0004
- 第二个目录项记录的簇号为这个子目录的父目录的起始簇号,如果父目录是根目录,则簇号位置全部设置为 0。
8. 卷目录项
如果创建文件系统时制定了卷标,则会在根目录下第一个目录项项的位置建立一个卷标目录项:
1. 卷标名最多允许占用长度为 11 个字节,也就是为短文件名分配的 11 个文件名区域,如果卷标名不足 11 个字节,则用 0x20 填充。(由于每个汉字占用 2 个字节空间,而卷标最多允许 11 个 字节,所以用汉字命名卷标时,卷标的长度不能超过 5 个汉字)。
2. 卷标目录项结构与普通短文件名目录项结构完全相同,但没有创建时间和访问时间,只有一个最后修改时间。
3. 另外,卷标目录项也没有歧视簇号和大小值,这些字节位置全部这只为 0,0x0B 字节处的属性 值为 0x08.
9. 目录项中的时间值的更新
目录项中有三个时间值:最后访问时间、最后写入时间和建立时间。
1. 建立时间。当windows为一个 “新” 文件分配目录项时设置建立时间。
2. 最后写入时间。当windows向文件写入新的内容时,最后写入时间会被更新。
3. 最后访问日期。这个时间只精确到日期,会经常被更新。
总结:
1. 建立文件
假设现在有一个子目录,它的名字是“smart monkey”,我们要在其下建立一个文件“yatou.txt”。 使用的文件系统为 FAT32,簇大小为 4096 字节,我们要建立的文件大小为 5000 个字节。
步骤 1:读取位于卷 0 号扇区的引导扇区,根据引导扇区中的信息定位 FAT 表、数据区和根目录的位置。
步骤 2:遵照“smart monkey”的位置。查看根目录下的每个目录项,寻找名字为“smart monkey” 且具有目录属性的目录项。找到后,查看它的起始簇号为 3。
步骤 3:读取 smart monkey 的起始簇(3 号簇)的内容,查找每个目录项,直到找到一个为分配的目录项。
步骤 4:找到可用项后写入文件名“yatou.txt”,并将文件大小和当前时间写入相应的位置。
步骤 5:为文件内容分配簇空间。转到 FAT 表,寻找空闲的位置。发现 4 号 FAT 表项未使用,这就说 明 4 号簇是空闲的。将 4 号簇分配给文件,并在 4 号簇的 FAT 表项内写入结束标记。
步骤 6:将簇号 4 写入文件目录项的起始簇号区域。将文件的前 4096 字节写入到 4 号簇中,还剩下 904 字节,所以还需要再为其分配一个簇。
步骤 7:在 FAT 表中继续寻找为分配簇,找到 5 号簇为空闲未使用(因其 FAT 表项为 0)。
步骤 8:将文件第一簇(即 4 号簇)的 FAT 表项值改写为 5,将文件的最后 904 字节写入 5 号簇。
步骤 9:在 5 号簇的 FAT 表项内写入结束标记。
2. 删除文件
现在我们将前面例子中建立的“smart monkey\yatou.txt”文件删除。
步骤 1:从卷 0 号扇区读取引导扇区,根据引导扇区中的信息定位 FAT 表、数据区和根目录的位置。
步骤 2:在根目录下寻找名字为“smart monkey”且具有目录属性的目录项。
步骤 3:由“smart monkey”的目录项中获取它的起始簇号为 3,到 3 号簇查看“smart monkey”的 内容,从中找到文件“yatou.txt”的目录项,提取出它的起始簇号,为 4 号簇。
步骤 4:到 FAT 表中找到该文件的簇链,确定他存储位置为 4 号簇和 5 号簇。
步骤 5:将 4 号簇和 5 号簇的 FAT 项设置为 0.
步骤 6:将文件“yatou.txt”的目录项的第一个字节改为 0xE5。
3. 实验程序
实验功能:
开机的时候先初始化SD卡,初始化成功之后,注册两个工作区(一个给SD卡用,一个给SPI FALSH用),然后获取SD卡的容量和剩余空间,显示在LCD上。
3.1 main.c
#include "stm32f4xx.h"
#include "delay.h"
#include "usart.h"
#include "LED.h"
#include "lcd.h"
#include "Key.h"
#include "usmart.h"
#include "SRAM.h"
#include "Malloc.h"
#include "SDIO_Card.h"
#include "W25Q128.h"
#include "ff.h"
#include "exfuns.h"
//LCD状态设置函数
void led_set(u8 sta)//只要工程目录下有usmart调试函数,主函数就必须调用这两个函数
{
LED1=sta;
}
//函数参数调用测试函数
void test_fun(void(*ledset)(u8),u8 sta)
{
led_set(sta);
}
int main(void)
{
u32 Total,Free;
u8 t=0;
u8 res=0;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
delay_init(168);
uart_init(115200);
LED_Init();
usmart_dev.init(84); //初始化USMART
LCD_Init();
Key_Init();
W25Q128_Init();
my_mem_init(SRAMIN); //初始化内部内存池
my_mem_init(SRAMCCM); //初始化CCM内存池
POINT_COLOR=RED;
LCD_ShowString(30,50,200,16,16,"Explorer STM32F4");
LCD_ShowString(30,70,200,16,16,"FATFS Test");
LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");
LCD_ShowString(30,110,200,16,16,"2023/20/23");
LCD_ShowString(30,130,200,16,16,"Use USMART for test");
while(SD_Init())//检测不到SD卡
{
LCD_ShowString(30,150,200,16,16,"SD Card Error!");
delay_ms(500);
LCD_ShowString(30,150,200,16,16,"Please Check! ");
delay_ms(500);
LED0=!LED0;//DS0闪烁
}
exfuns_init(); //为fatfs相关变量申请内存
f_mount(fs[0],"0:",1); //挂载SD卡
res=f_mount(fs[1],"1:",1); //挂载FLASH.
if(res==0X0D)//FLASH磁盘,FAT文件系统错误,重新格式化FLASH
{
LCD_ShowString(30,150,200,16,16,"Flash Disk Formatting..."); //格式化FLASH
res=f_mkfs("1:",1,4096);//格式化FLASH,1,盘符;1,不需要引导区,8个扇区为1个簇
if(res==0)
{
f_setlabel((const TCHAR *)"1:ALIENTEK"); //设置Flash磁盘的名字为:ALIENTEK
LCD_ShowString(30,150,200,16,16,"Flash Disk Format Finish"); //格式化完成
}else LCD_ShowString(30,150,200,16,16,"Flash Disk Format Error "); //格式化失败
delay_ms(1000);
}
LCD_Fill(30,150,240,150+16,WHITE); //清除显示
while(exf_getfree("0",&Total,&Free)) //得到SD卡的总容量和剩余容量
{
LCD_ShowString(30,150,200,16,16,"SD Card Fatfs Error!");
delay_ms(200);
LCD_Fill(30,150,240,150+16,WHITE); //清除显示
delay_ms(200);
LED0=!LED0;//DS0闪烁
}
POINT_COLOR=BLUE;//设置字体为蓝色
LCD_ShowString(30,150,200,16,16,"FATFS OK!");
LCD_ShowString(30,170,200,16,16,"SD Total Size: MB");
LCD_ShowString(30,190,200,16,16,"SD Free Size: MB");
LCD_ShowNum(30+8*14,170,Total>>10,5,16); //显示SD卡总容量 MB
LCD_ShowNum(30+8*14,190,Free>>10,5,16); //显示SD卡剩余容量 MB
while(1)
{
t++;
delay_ms(200);
LED0=!LED0;
}
}
3.2 diskio.c
/*-----------------------------------------------------------------------*/
/* 低级别磁盘I/O模块框架FatFs (C) */
/*-----------------------------------------------------------------------*/
/* 如果有可用的存储控制模块,则应该通过胶水函数将其附加到fatfs上,而不是修改它。
这是一个使用定义的API将各种现有存储控制模块附加到FatFs模块的粘接函数的示例。*/
/*-----------------------------------------------------------------------*/
#include "diskio.h" /* FatFs下层API */
#include "SDIO_Card.h"
#include "W25Q128.h"
#include "Malloc.h"
//在此选择使用SD卡还是SPI FASLH
#define SD_CARD 0 //SD卡,卷标为0
#define EX_FLASH 1 //外部flash,卷标为1
#define FLASH_SECTOR_SIZE 512
//对于W25Q128
//前12M字节给fatfs用,12M字节后,用于存放字库,字库占用3.09M. 剩余部分,给客户自己用
u16 FLASH_SECTOR_COUNT=2048*12; //W25Q128,前12M字节给FATFS占用
#define FLASH_BLOCK_SIZE 8 //每个BLOCK有8个扇区
//初始化磁盘
DSTATUS disk_initialize (BYTE pdrv/* 物理驱动器号(0..) */)
{
u8 res=0;
switch(pdrv)
{
case SD_CARD://SD卡
res=SD_Init();//SD卡初始化 SD_Init函数返回错误代码,返回0代表没有错误,初始化成功
break;
case EX_FLASH://外部flash
W25Q128_Init();
FLASH_SECTOR_COUNT=2048*12;//W25Q1218,前12M字节给FATFS占用
break;
default:
res=1;
}
if(res) //只要res是1,那么一定不是SD卡和SPI FLASH,两个都没有进行初始化,这里不考虑U盘的情况
return STA_NOINIT;
//#define STA_NOINIT 0x01 /* 驱动器未初始化 */
else
return 0; //初始化成功
}
//获得磁盘状态
DSTATUS disk_status (BYTE pdrv/* 物理驱动器号(0..) */)
{
return 0;
}
//读扇区
//drv:磁盘编号0~9
//*buff:数据接收缓冲首地址
//sector:扇区地址
//count:需要读取的扇区数
DRESULT disk_read (
BYTE pdrv, /* 物理驱动器号(0..) */
BYTE *buff, /* 用于存储读数据的数据缓冲区 */
DWORD sector, /* 扇区地址(LBA) */
UINT count /* 需要读取的扇区数 */
)
{
u8 res=0;
if (!count)
return RES_PARERR;//count不能等于0,否则返回参数错误
//RES_PARERR /* 4: 无效的参数 */
switch(pdrv)
{
case SD_CARD://SD卡
res=SD_ReadDisk(buff,sector,count);
while(res)//res=1表示读出错
{
SD_Init(); //重新初始化SD卡
res=SD_ReadDisk(buff,sector,count); //SD_ReadDisk返回0表示正常,返回其他值表示错误
//printf("sd rd error:%d\r\n",res);
}
break;
case EX_FLASH://外部flash
for(;count>0;count--) //W25Q128读扇区
{
W25Q128_Read(buff,sector*FLASH_SECTOR_SIZE,FLASH_SECTOR_SIZE);
//第二个参数表示开始读取的地址,sector*FLASH_SECTOR_SIZE表示扇区地址乘以扇区数代表整个FLASH的大小,指针指向整个FLASH存储地址的首地址上
//第三个参数表示要读取的大小
sector++; //读一次扇区地址++
buff+=FLASH_SECTOR_SIZE; //用于存储读数据的数据缓冲区在上一次读取的基础上相加,为下次读取存储做准备
}
res=0;
break;
default:
res=1;
}
//处理返回值,将SPI_SD_driver.c的返回值转成ff.c的返回值
if(res==0x00)
return RES_OK;
//RES_OK = 0, /* 0: 成功 */
else
return RES_ERROR;
//RES_ERROR, /* 1: 读/写 错误 */
}
//写扇区
//drv:磁盘编号0~9
//*buff:发送数据首地址
//sector:扇区地址
//count:需要写入的扇区数
#if _USE_WRITE
DRESULT disk_write (
BYTE pdrv, /* 物理驱动器号(0..) */
const BYTE *buff, /* 待写入数据 */
DWORD sector, /* 扇区地址(LBA) */
UINT count /* 要写入的扇区数(1..128) */
)
{
u8 res=0;
if (!count)
return RES_PARERR;//count不能等于0,否则返回参数错误
//RES_PARERR /* 4: 无效的参数 */
switch(pdrv)
{
case SD_CARD://SD卡
res=SD_WriteDisk((u8*)buff,sector,count); //SD_WriteDisk函数返回0写正常,返回其他表示错误
while(res)//返回其他的都表示为真,写出错
{
SD_Init(); //重新初始化SD卡
res=SD_WriteDisk((u8*)buff,sector,count);
//printf("sd wr error:%d\r\n",res);
}
break;
case EX_FLASH://外部flash
for(;count>0;count--)
{
W25QXX_Write((u8*)buff,sector*FLASH_SECTOR_SIZE,FLASH_SECTOR_SIZE);//和读取时的意思是一样的
sector++;
buff+=FLASH_SECTOR_SIZE;
}
res=0;
break;
default:
res=1;
}
//处理返回值,将SPI_SD_driver.c的返回值转成ff.c的返回值
if(res == 0x00)//res等于0表示成功
return RES_OK;
//RES_OK = 0, /* 0: 成功 */
else
return RES_ERROR;
//RES_ERROR, /* 1: 读/写 错误 */
}
#endif
//其他表参数的获得
//drv:磁盘编号0~9
//ctrl:控制代码
//*buff:发送/接收缓冲区指针
#if _USE_IOCTL
DRESULT disk_ioctl (
BYTE pdrv, /* 物理驱动器号(0..) */
BYTE cmd, /* 控制代码 */
void *buff /* 缓冲区发送/接收控制数据 */
)
{
DRESULT res;
if(pdrv==SD_CARD)//SD卡
{
switch(cmd)
{
case CTRL_SYNC:
//#define CTRL_SYNC 0 /* 刷新磁盘缓存(用于写入功能) */
res = RES_OK;
//RES_OK = 0, /* 0: 成功 */
break;
case GET_SECTOR_SIZE:
//#define GET_SECTOR_SIZE 2 /* 获取扇区大小(对于多个扇区大小(_MAX_SS >= 1024)) */
*(DWORD*)buff = 512;
//typedef unsigned long DWORD; 强制类型转换为DWORD,
res = RES_OK;
//RES_OK = 0, /* 0: 成功 */
break;
case GET_BLOCK_SIZE:
//#define GET_BLOCK_SIZE 3 /* 获取擦除块大小(仅适用于f_mkfs()) */
*(WORD*)buff = SDCardInfo.CardBlockSize; // u32 CardBlockSize; SD卡块大小
//typedef unsigned short WORD; 强制类型转换为WORD
res = RES_OK;
break;
case GET_SECTOR_COUNT:
*(DWORD*)buff = SDCardInfo.CardCapacity/512;
res = RES_OK;
break;
default:
res = RES_PARERR;
break;
}
}else if(pdrv==EX_FLASH) //外部FLASH
{
switch(cmd)
{
case CTRL_SYNC:
res = RES_OK;
break;
case GET_SECTOR_SIZE:
*(WORD*)buff = FLASH_SECTOR_SIZE;
res = RES_OK;
break;
case GET_BLOCK_SIZE:
*(WORD*)buff = FLASH_BLOCK_SIZE;
res = RES_OK;
break;
case GET_SECTOR_COUNT:
*(DWORD*)buff = FLASH_SECTOR_COUNT;
res = RES_OK;
break;
default:
res = RES_PARERR;
break;
}
}else res=RES_ERROR;//其他的不支持
return res;
}
#endif
//获得时间
//用户定义的函数,为fatfs模块提供当前时间 */
//31-25: Year(0-127 org.1980), 24-21: Month(1-12), 20-16: Day(1-31) */
//15-11: Hour(0-23), 10-5: Minute(0-59), 4-0: Second(0-29 *2) */
DWORD get_fattime (void)
{
return 0;
}
//动态分配内存
void *ff_memalloc (UINT size)
{
return (void*)mymalloc(SRAMIN,size);
}
//释放内存
void ff_memfree (void* mf)
{
myfree(SRAMIN,mf);
}
3.3 diskio.h
/*-----------------------------------------------------------------------/
/ 低级别硬盘接口模块包含文件 /
/-----------------------------------------------------------------------*/
#ifndef _DISKIO_DEFINED
#define _DISKIO_DEFINED
#ifdef __cplusplus
extern "C" {
#endif
#define _USE_WRITE 1 /* 1: 开启disk_write功能 */
#define _USE_IOCTL 1 /* 1: 启用disk_ioctl功能 */
#include "integer.h"
/* 硬盘功能状态 */
typedef BYTE DSTATUS;
/* 硬盘功能结果 */
typedef enum {
RES_OK = 0, /* 0: 成功 */
RES_ERROR, /* 1: 读/写 错误 */
RES_WRPRT, /* 2: 写保护 */
RES_NOTRDY, /* 3: 没有准备 */
RES_PARERR /* 4: 无效的参数 */
} DRESULT;
/*---------------------------------------*/
/* 磁盘控制功能的原型 */
DSTATUS disk_initialize (BYTE pdrv);
DSTATUS disk_status (BYTE pdrv);
DRESULT disk_read (BYTE pdrv, BYTE* buff, DWORD sector, UINT count);
DRESULT disk_write (BYTE pdrv, const BYTE* buff, DWORD sector, UINT count);
DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void* buff);
/* 磁盘状态位 */
#define STA_NOINIT 0x01 /* 驱动器未初始化 */
#define STA_NODISK 0x02 /* 驱动器中没有介质 */
#define STA_PROTECT 0x04 /* 写保护 */
/* disk_ioctrl函数的命令代码 */
/* 通用命令 */
#define CTRL_SYNC 0 /* 刷新磁盘缓存(用于写入功能) */
#define GET_SECTOR_COUNT 1 /* 获取媒体大小(仅适用于f_mkfs()) */
#define GET_SECTOR_SIZE 2 /* 获取扇区大小(对于多个扇区大小(_MAX_SS >= 1024)) */
#define GET_BLOCK_SIZE 3 /* 获取擦除块大小(仅适用于f_mkfs()) */
#define CTRL_ERASE_SECTOR 4 /* 强制擦除扇区块(仅针对_USE_ERASE) */
/* 通用命令(fatf不使用) */
#define CTRL_POWER 5 /* 获取/设置电源状态 */
#define CTRL_LOCK 6 /* 锁定/解锁介质移除 */
#define CTRL_EJECT 7 /* Eject media */
#define CTRL_FORMAT 8 /* 在媒体上创建物理格式 */
/* MMC/SDC专用ioctl命令 */
#define MMC_GET_TYPE 10 /* 获取卡类型 */
#define MMC_GET_CSD 11 /* 得到CSD */
#define MMC_GET_CID 12 /* 得到CID */
#define MMC_GET_OCR 13 /* 得到OCR */
#define MMC_GET_SDSTAT 14 /* 获取SD状态 */
/* ATA/CF专用ioctl命令 */
#define ATA_GET_REV 20 /* 获得F/W修订 */
#define ATA_GET_MODEL 21 /* 获取模型名称 */
#define ATA_GET_SN 22 /* 获取序列号 */
#ifdef __cplusplus
}
#endif
#endif
本节篇幅比较长,本人整理了多个对文件系统的介绍总结在此,望耐心研读!如有错误,欢迎留言改正!!!