文章节选自:《ARM Cortex-M0从这里开始》 作者:zhaojun_xf https://bbs.eeworld.com.cn/thread-324656-1-1.html
文件系统 在学习SPI总线时已经介绍了读写SD卡的方法。那么我们知道在对SD卡读写时,都是以扇区为基本单位的进行的。但是,如果需要读取文件呢?例如,要显示的图片文件。该怎么读取呢?如果采用扇区的方式,怎么知道文件存放哪些扇区呢?为了实现数码相框功能,我们不得不了解和学习文件系统。这不仅是本章的难点,也是本章的重点。 1 概述 文件系统是操作系统用于明确磁盘或分区上的文件的方法和数据结构;即在磁盘上组织文件的方法。从系统角度来看,文件系统是对文件存储器空间进行组织和分配,负责文件存储并对存入的文件进行保护和检索的系统。具体地说,它负责为用户建立文件,存入、读出、修改、转储以及控制文件的存取,当用户不再使用时撤销文件等。 在打开电脑或U盘等存储设备的时候,我们可以浏览其中的文件,知道文件大小、文件类型、读写时间等这都应归功于文件系统。如图7-13所示,是电脑硬盘C盘和SD卡属性。由这两幅图我们可以看出,它们的文件系统分别是NTFS和FAT32。
文件系统的类型比较多,常见的有: FAT:以前是PC机和现在大部分PC机都使用FAT文件系统,其中根据容量不同,又有三种类型FAT12(最大支持8M)、FAT16(最大支持2G)、FAT32(最大支持32GB); NTFS:是一个基于安全性的文件系统,是Windows NT所采用的独特的文件系统结构,它是建立在保护文件和目录数据基础上,同时照顾节省存储资源、减少磁盘占用量的一种先进的文件系统。新购买的PC机都采用这种文件系统。 CDFS:是大部分的光盘文件系统,只有小部分光盘使用其他文件系统。这些文件系统只能在CD-R或CD-RW上读取。 exFAT:Windows Embeded 5.0以上系统(包括Windows CE 5.0、6.0、Windows Mobile5、6、6.1)中引入的一种适合于闪存的文件系统。对于闪存,NTFS文件系统不适合使用,exFAT更为适用。 RAW: 是一种磁盘未经处理或者未经格式化产生的的文件系统。 Ext2:GNU/Linux 系统中标准的文件系统。 Ext3:是一种日志式文件系统,是对ext2系统的扩展,它兼容ext2。 由上面的介绍,我们知道,文件系统的种类还是比较多的。但是,对于SD卡使用比较多的还是FAT文件系统,所以,下面将对FAT文件系统进行详细的介绍。至于其他文件系统,读者可以通过其他书籍进行学习和了解。
2 FAT文件系统 一种由微软发明并拥有部分专利的文件系统,供MS-DOS使用,也是所有非NT内核的微软窗口使用的文件系统。FAT文档系统考虑当时计算机性能有限,所以未被复杂化,因此几乎所有个人计算机的操作系统都支持。 FAT12:是IBM第一台个人电脑中的MS-DOS 1.0使用的文件系统,主要用于软盘。这种系统限制分区的容量最大为16MB——但这根本算不上问题,因为软盘容量从来没有达到16MB。 FAT16:它被很多操作系统支持,兼容性最好,但分区最大只能到2GB,并且空间浪费现象比较严重。并且由于FAT16文件系统是单用户文件系统,不支持任何安全性及长文件名。 FAT32:是在FAT16基础上发展而来,随着Windows 95 OSR2一起发布,可以被大多数操作系统支持,FAT32比FAT16更有效地利用了硬盘空间,并且最大分区的上限已经达到了32GB,适合一般家庭使用。 通过上面介绍可知,FAT12和FAT16已经不再适应。所以,我们将对FAT32进行全面系统的学习。为了能直观的学习 FAT文件系统,下面就以一张1G的SD卡,存放一个test.txt文件作为示例学习。 1. 文件系统结构 FAT结构是所有按照FAT文件系统来组织存储单元的介质,都必须遵守的一种文件系统格式,而对于不同的介质,结构又有差异。如图7-14为FAT文件系统的基本组成。
每个FAT文件系统都是由5部分组成,这些基本区域按照如下顺序排列: MBR(master boot record)区:也就是硬盘的第一个扇区, 它由MBR (Master Boot Record), DPT (Disk Partition Table) 和 Boot Record ID 三部分组成; 保留区(Reserved Sectors):包含文件系统引导记录(DBR)扇区开始的仅为系统所有的扇区,包括DBR扇区,位于最开始的位置。 FAT(File Allocation Table)区:包含有两份文件分配表,操作系统分配磁盘空间是按簇来分配的。它是分区信息的映射表,指示簇是如何存储的。因此,文件占用磁盘空间时,基本单位不是字节而是簇,即使某一个文件只有一个字节,操作系统也会给他分配一个最小单元---即一个簇。 根目录区:它是在根目录中存储文件和目录信息的目录表。在FAT32下它可以存在分区中的任何位置,但是在早期的版本中它永远紧随FAT区域之后。 数据区:这是实际的文件和目录数据存储的区域,它占据了分区的绝大部分。通过简单地在FAT中添加文件链接的个数可以任意增加文件大小和子目录个数(只要有空簇存在)。 2. MBR区 MBR包含了磁盘的一些引导和分区信息,结构如图7-15所示。不过很多SD卡都没有这一部分信息。怎样知道一张SD卡有无MBR?可以参见第6章第3节的内容。
2. 保留区 (1)保留区结构 保留区包含了对文件系统进行识别的关键信息,此区域如果被破坏将无法进行数据读写等操作,因此十分重要。一般都在逻辑分区的第1扇区(即扇区0),所以又叫启动扇区或0扇区。如果磁盘没有MBR时,此区域将会在磁盘的第1扇区;如果有MBR,则可以通过区分表中的数据计算出此区域的地址。
(2)实例分析 格式一张1G的SD卡为FAT32,分配单元大小4096字节,如图7-16所示。
使用WinHex软件,打开SD卡。看看逻辑分区的启动区内容,如图7-17所示。
为了方便阅读,这里只取了此扇区的部分内容,并把有用部分用横线标识出来: ① BS_jmpBoot: EB 58 90 跳转指令。 ② BS_OEMName: 4D 53 44 4F 53 35 2E 30 为厂商标志和OS版本号,这里是MSDOS5.0。 ③ BPB_BytePerSec:00 02表示为0200(小端格式:高地址放高字节,低地址放低字节),就是512。表示每个扇区有512个字节。 ④ BPB_SecPerClus:08 表示每个簇有8个扇区,以就是说 1簇 = 8*512 字节。 ⑤ BPB_RsvdSecCnt:E4 10转换一下就是0x10E4,说明保留扇区数为4324个。那么就可以计数出FAT1的开始地址:0x10E4 *0x0200 = 0x21C800。 ⑥ BPB_NumFATs:02 表示此卷中FAT表的份数为2,另外一份为FAT备份表。 ⑦ BPB_TotSec32:00 54 1E 00转换一下就是0x001E5400,表示SD卡的总扇区数。那么SD卡的容量为0x001E5400*0x0200 = 0x3CA80000 = 970MB。 ⑧ BPB_FATSz32:8E 07 00 00转换一下就是0x0000078E,每个FAT表占用的1934个扇区。那么每个FAT表占用的字节数就是0x078E*0x0200 = 0xF1C00。 ⑨ 根据启动区、FAT1、FA2、根目录、数据区的顺序和上面的数据可以依次计算出它们的地址了。 由于本卡没有MBR,所以启动区地址为0x00。 FAT1 :0x21C800; FAT2 :0x21C800 + 0xF1C00 = 0x30E400; 根目录区 :0x30E400 + 0xF1C00 = 0x400000。 再通过WinHex软件可以查看计算是否正确,如图7-18是利用WinHex软件查看的数据。是不是和实际计算一样的呢?
3. 根目录区 (1)根目录区描述 在FAT32中已经把文件的概念进行扩展,没有专门的目录区,目录同样也是文件。根目录的地位与其它目录是相同的。既然是文件,就应该有文件名,根目录的名称就是磁盘的卷标。下面将对这一区进行详细介绍,如图7-3所示为文件属性描述表。
(2)示例说明 如图7-19所示,新建一个名字为test.txt的文本文件,并写入一定的内容。
如图7-20所示为SD卡根目录区的内容。这里只使用FAT32的短文件目录项,每32个字节表示一个文件或文件夹。至于长目录读者可以参考其他书籍。图中文件分别用黑色线画出。
前面32字节为卷标属性,根目录为SD_CARD。这个是格式化时,笔者添加的名称。如果在格式化时没有添加,则没有前面的32字节数据。对于这32字节数据的含义读者可以对照表7-3来了解具体含义,这里不再说明,下面具体说明下一个文件的属性及含义: 1) 文件名称:54 45 53 54 20 20 20 20表示文件名为TEST(空缺部分补空格)。 2) 文件类型:54 58 54表示TXT,为ASCII字符表示。 3) 文件属性:20表示存档文件。 4) 文件创建时间10毫秒位:33。 5) 文件创建时间和日期:56 A5为时间,转换一下0xA556 = 1010 0101 0101 0110,按照格式表示:时10100B分101010B秒10110B*2,即20时42分44秒;AC 3E为日期,转换一下0x3EAC = 0011 1110 1010 1100B,按照格式表示0011 111B +1980年0101B月01100B日,即2011年5月12日。 6) 最后访问的日期:AC 3E计算方法和文件创建日期相同,计算出来得2011年5月12日。 7) 最后修改时间和日期:62 A5为时间和AC 3E日期,计算出来为2011年5月12,20点43分04秒。 8) 文件开始簇号:高位字节为 00 00,2字节;低位字节为03 00,2字节。转换后为0x00000003,根据这个数据就可以找到文件下一簇号在FAT1中的位置了。0x21C800+0x03*0x04(FAT32用4字节表示一个簇号)= 0x21C80C。 9) 文件长度:BE 1A 00 00,4字节表示文件大小,转换后为 0x00001ABE = 6846字节。 通过上面的计算,再对照图7-17所示,发现计算的结果和实际是相同的。 4. FAT区 (1)FAT区描述 FAT即File Allocation Table(文件分配表),它是一一对应于数据区簇号的列表。文件系统分配磁盘空间时,最基本的单位并不是字节而是簇。即使某一个文件只有一个字节,操作系统也会分配给他一个最小单元—-即一个簇。为了可以将磁盘空间有序地分配给相应的文件,而读取文件的时候又可以从相应地址读取出文件,我们把数据区空间分成BPB_BytsPerSec*BPB_SecPerClus字节长的簇来管理。FAT表项的大小与FAT类型有关,FAT12的表项为12Bit,FAT16为16Bit,而FAT32则为32Bit。 对于大文件,需要分配多个簇。同一个文件的数据并不一定完整地存放在磁盘的一个连续区域内,而往往会分成若干段,像一条链子一样存放。这种存储方式称为文件的链式存放。为了实现文件的链式存储,文件系统必须准确地记录哪些簇已经被文件占用,为每个已经占用的簇指明存储后继内容的下一簇的簇号,对文件的最后一簇,则要指明本簇无后继簇。这些都是有FAT表来保存的,FAT表的对应表项中记录着它所代表的簇的有关信息;诸如是否空,是否是坏簇,是否已经是某个文件的尾簇等。表7-4为FAT区结构表:
(2)示例说明 从上面的学习知道,SD卡每个簇占用8个扇区,每个扇区512个字节,那么一个簇的空间就是4096字节,即4KB。那么SD卡中的一个文件test.txt文件的大小为6846字节,需要两个簇。这两个簇的开始地址也可以计算出来。由于FAT1和FAT2是完全一样的,FAT2只是个备份。所以,下面以FAT1为例,说明FAT中数据的存储格式。用WinHex打开FAT1,截图如图7-21所示。
从文件描述中知道,test.txt的开始簇地址为0x03,那么开始地址为0x400000(根目录地址)+(0x03-0x02)*0x08*0x200 = 0x401000(第一簇开始地址);由于簇号总是从2开始的,因为前面8字节为FAT标识符,所以这里要减2。 第二个簇的地址是存放在0x21C800+0x03*0x04 = 0x21C80C中,查看0x21C80C的内容为04 00 00 00,转换后为0x04,不是0x0FFFFFFF(文件结束标志),表示此地址中还有文件存放。那么久可以计算出第二簇数据的地址了:0x400000(根目录地址)+(0x04-0x02)*0x08*0x200 = 0x402000。 0x21C80C中的内容为0x04,指出的下一个簇号地址位置,0x21C800+0x04*0x04 = 0x21C810。查看0x21C810的内容为0x0FFFFFFF,表示文件放置已经结束了。 由此可以得出,文件test.txt一个有两个簇,地址分别为: 第一簇地址:0x401000; 第二簇地址:0x402000。 5. 数据区 先是通过文件中的描述信息知道了文件的起始簇数据的存放地址,并从这个信息知道下一簇地址存放在FAT区的位置,通过FAT区的数据计算出下一簇数据的具体位置。上面已经计算出数据地址,下面通过软件打开数据区查看一下是否正确,如图7-22所示。由于空间有限,每一簇只截了部分数据来验证。
自此文件系统基本就学习完成,读者完全可以按照上面的理解编写自己的文件系统了。至于长文件名,是在此基础上发展起来的,读者可以自行了解学习。笔者以前也编写过一个简易文件系统,在一个AVR单片机的项目上使用。但是由于在移植到其他单片机平台时,兼容性比较差,最后放弃了自己编写的文件系统,转而移植其他文件系统。下面将学习怎样来移植一个文件系统。 3 文件系统FatFs的移植 应用SD卡操作文件,文件系统是必不可少的。在网络上适用于微处理器的文件系统有很多,但是常见而且口碑不错的有四种:uC/FS、ZLG/FS、efsl、FatFs。 uC/FS:比较常用于嵌入式系统,是Micrium(uC/OS-II)公司开发的,稳定性、兼容性应该都不差。支持CF卡、SD/MMC卡、Nand Flash和硬盘等,不过是商用的,需要支付版权费; ZLG/FS:周立功的很多的开发板上面都送了这个文件系统的源代码的,在网上也能找到源代码。主要用于教学,效率并不太高; Efsl:是一个开源的项目,免费,移植也很简单。移植的时候只要提供读写扇区等几个函数即可。但是不支持多扇区读写,导致读写速度受到限制; FatFs:也是开源项目,作者一直在更新,而且使用的人比较多,很多网友DIY都是采用此系统,便于交流。 通过综合考虑,最终决定移植FatFs文件系统。 1. FatFs的特点: Windows兼容的FAT文件系统; 与平台无关,容易移植; 代码非常小,占用资源少,非常适应微处理器使用; 支持在ANSI/OEM或Unicode的长文件名; 支持RTOS; 支持多扇区读写; 大小可以裁减等。 2. FatFs的结构: 如图7-23所示为FatFs文件系统的结构图,整个系统分为三个层:磁盘I/O操作、FatFs模块和应用层。
磁盘I/O操作:此层为移植文件系统的需要编写的驱动函数层,也就是说,移植文件系统的主要工作就是编写此层中的6个函数。 disk_initialize() - 初始化磁盘 disk_status() - 获取磁盘状态 disk_read() - 读扇区操作 disk_write() - 写扇区操作 disk_ioctl() - 磁盘相关功能控制 get_fattime() - 获取当前时间 FatFs模块层:此层为文件系统操作层,读者不用修改,完全可以看成是一个黑匣子。当然,读者如果想了解FatFs是怎样实现的可以花一定的时间去参阅以下FatFs源代码。 应用层:当文件系统移植完成后,就可以根据自己的需要去编写应用程序,操作磁盘了。对于FatFs文件系统,提供了大量的API函数,读者只需要调用这些API函数就可以完成自己需要的功能。 f_mount() - 注册/注销工作区 f_open() - 打开/创建文件 f_close() - 关闭文件 f_read () - 读文件 f_write() - 写文件 f_lseek() - 移动读/写指针 f_truncate() - 文件截取 f_sync() - 清除缓冲数据 f_opendir() - 打开目录 f_readdir() - 读目录 f_getfree() - 获取闲置簇的数量 f_stat() - 获取文件状态 f_mkdir() - 创建目录 f_unlink() - 移动文件或者目录 f_chmod() - 改变属性 f_utime () - 修改文件时间 f_rename () - 重命名/移动文件或者目录 f_mkfs() - 在驱动上创建文件系统 f_forward() - 文件数据直接转发到数据流 f_chdir() - 改变当前目录 f_chdrive() - 改变当前驱动 f_getcwd() - 恢复当前目录 f_gets() - 读字符串 f_putc() - 写字符 f_puts() - 写字符串 f_printf() - 写格式化字符串 f_tell() - 获取当前读写指针 f_eof() - 测试结束文件 f_size() - 获取文件大小 f_error() - 错误测试 3. FatFs移植 要移植FatFs文件系统,首先到FatFs文件系统的官方网站下载文件系统源代码。目前比较新的版本是FatFs R0.08b (官方网站:http://elm-chan.org/fsw/ff/00index_e.html)。 (1)解压下载的文件系统,如图7-23所示,可以看到一共有两个文件夹:一个是doc,包含FatFs的描述、特性说明等;另一个是src,存放所有源代码信息,一共有8个文件。diskio.c和diskio.h是更硬件相关层,ff.c和ff.h是FatFs的文件系统层,提供文件系统的实现和操作文件系统的API函数,integer.h是文件系统所用到的数据类型的定义,ffconf.h问系统配置文件,00readme.txt为文件系统版本说明,option文件夹存放于长文件相关的文件。
(2)在工程下新建一个文件夹,存放文件系统源代码,如图7-25所示。
(3)把源代码都添加到相应的工程中,首先根据需要修改数据类型,在integer.h中修改适合的数据类型,具体代码如程序清单7-13所示。 /****************************** 程序清单7-13 **********************************/ /* 这些数据类型必须是16位,32位或较大的整数 */ typedef int INT; typedef unsigned int UINT;
/* 这些数据类型必须是8位整数 */ typedef char CHAR; typedef unsigned char UCHAR; typedef unsigned char BYTE;
/* 这些数据类型必须是16位整数 */ typedef short SHORT; typedef unsigned short USHORT; typedef unsigned short WORD; typedef unsigned short WCHAR;
/* 这些数据类型必须是32位整数 */ typedef long LONG; typedef unsigned long ULONG; typedef unsigned long DWORD; (4)配置文件系统,可以根据字节的实际需要进行配置,从而可以减小代码和内存空间。具体代码如程序清单7-14所示。 /****************************** 程序清单7-14 **********************************/ #define _FS_TINY 0 /* 0普通;1:小型 */ /* 配置为小型时,可以减少使用512字节内存空间 */ #define _FS_READONLY 0 /* 0:读/写;1:只读 */ /* 配置为只读时,与写相关的函数均不能使用 */ #define _FS_MINIMIZE 0 /* 0, 1, 2, 3 */ /* 数值越大,裁减的函数就越多 */ #define _USE_STRFUNC 0 /* 0:禁用;1/2:使能 */ /* 启用字符串功能, 设置 _USE_STRFUNC为1或 2. */ #define _USE_MKFS 0 /* 0:禁用; 1:使能 */ /* 设置_USE_MKFS 为 1时启用f_mkfs功能 */
#define _USE_FORWARD 0 /* 0:禁用; 1:使能 */ /* 和_FS_TINY同时设置为1,启用f_forward 功能 */ #define _USE_FASTSEEK 0 /* 0:禁用; 1:使能 */ /* _USE_FASTSEEK 设置为1,启用快速定位 */ #define _CODE_PAGE 936 /* _CODE_PAGE指定目标文件的OEM代码类别 */ #define _USE_LFN 0 /* 0 至3 */ #define _MAX_LFN 255 /* 长文件名的最大长 (12~255) */ #define _LFN_UNICODE 0 /* 0:ANSI/OEM;1:Unicode */ /* FatFs API上的中文代码设置切换到 Unicode */ #define _FS_RPATH 0 /* 0:禁用; 1:使能 */ /* _FS_RPATH 设置为1, 启用相对路径和f_chdir */ #define _DRIVES 1 /* 被使用的通用卷数量(逻辑驱动) */ #define _MAX_SS 512 /* 512, 1024, 2048, 4096 */ /* 可处理的最大扇区大小 */ #define _MULTI_PARTITION 0 /* 0:Single parition or 1:Multiple partition */ /* 设置每个卷对应同样大小的物理空间 */ #define _WORD_ACCESS 0 /* 0 or 1 */ /* 优先设置为0,它适用于所有平台. The WORD_ACCESS*/ #define _FS_REENTRANT 0 /* 0:禁用; 1:使能 */ #define _FS_TIMEOUT 1000 /* 超时时间 */ #define _SYNC_t HANDLE /* O/S 取决于同步对象类型 */ #define _FS_SHARE 0 /* 0:禁用; 1:使能 */ (5)打开diskio.c文件,编写相应的函数。在前面章节中,已经编写好了SPI方式下读写SD卡的函数,在这里只需要把这些函数填写在相应代码中即可,如程序清单7-15所示。 /****************************** 程序清单7-15 **********************************/ /******************************************************************************** * FunctionName : disk_initialize() * Description : 初始化磁盘 * EntryParameter : drv - 物理驱动器号 * ReturnValue : None ********************************************************************************/ DSTATUS disk_initialize(BYTE drv) { DSTATUS stat; if (drv) { return STA_NOINIT; // 只支持单个驱动器 } stat = MMCInit(); // 初始化SD卡 if (stat == STA_NODISK) { return STA_NODISK; } else { if (stat != 0) { return STA_NOINIT; // 其他错误:初始化失败 } } return stat; // 成功返回0 }
/******************************************************************************** * FunctionName : disk_status() * Description : 磁盘状态 * EntryParameter : None * ReturnValue : None ********************************************************************************/ DSTATUS disk_status (BYTE drv) { DSTATUS stat;
if (drv) { return STA_NOINIT; } else { stat = 0; }
return stat; }
/******************************************************************************** * FunctionName : disk_read() * Description : 读扇区 * EntryParameter : None * ReturnValue : None ********************************************************************************/ DRESULT disk_read ( BYTE drv, /* Physical drive nmuber (0..) */ BYTE *buff, /* Data buffer to store read data */ DWORD sector, /* Sector address (LBA) */ BYTE count /* Number of sectors to read (1..255) */ ) { if (drv || (!count)) { return RES_PARERR; }
if (count == 1) // 单扇区读 { if (!MMCReadSingleBolck(sector,buff)) { count = 0; } } else // 多扇区读 { if(!MMCReadMultipleBolck(sector,buff,count)) { count = 0; } }
return (count ? RES_ERROR : RES_OK); }
/******************************************************************************** * FunctionName : disk_write() * Description : 写扇区 * EntryParameter : None * ReturnValue : None ********************************************************************************/ #if _READONLY == 0 DRESULT disk_write ( BYTE drv, /* Physical drive nmuber (0..) */ const BYTE *buff, /* Data to be written */ DWORD sector, /* Sector address (LBA) */ BYTE count /* Number of sectors to write (1..255) */ ) { if (drv || (!count)) { return RES_PARERR; }
if (count == 1) // 写单扇区 { if (!MMCWriteSingleBlock(sector,buff)) { count = 0; } } else // 写多扇区 { if(!MMCWriteMultipleBlock(sector,buff,count)) { count = 0; } }
return (count ? RES_ERROR : RES_OK); } #endif /* _READONLY */
/******************************************************************************** * FunctionName : disk_ioctl() * Description : 磁盘相关功能控制 * EntryParameter : None * ReturnValue : 只有在格式时才使用,只是读写可以直接返回OK ********************************************************************************/ DRESULT disk_ioctl ( BYTE drv, /* Physical drive nmuber (0..) */ BYTE ctrl, /* Control code */ void *buff /* Buffer to send/receive control data */ ) { if (drv) { return RES_PARERR; }
return RES_OK; } (6)最后需要在ff.c中添加函数get_fattime。此函数是获取操作时间,便于在文件属性中填写相应的创建、修改、访问时间。由于本系统没有提供时间代码,所以在这里直接返回0即可。如程序清单7-16所示。 /****************************** 程序清单7-16 **********************************/ /******************************************************************************** * FunctionName : get_fattime() * Description : 获取当前时间 * EntryParameter : None * ReturnValue : 如果要返回时间,按照FatFs的格式要求返回,此处返回0 ********************************************************************************/ DWORD get_fattime (void) { return 0; } 到此,整个文件系统的移植就已经完成,在应用程序中就可以直接编写应用代码调用相应的API函数了。需要注意的事,不同配置,API函数是不同的。在这里就不单独对API的应用进行说了,在下面内容中使用到API时,加以说明。
|