1、文件系统
文件系统负责管理和存储文件信息的软件机构,在磁盘上组织文件的方法。
对于 SPI Flash 芯片或者 SD 卡之类的大容量设备,我们需要一种高效的方式来管理它的存储内容。这些管理方式即为文件系统,它是为了存储和管理数据,而在存储介质建立的一种组织结构,这些结构包括操作系统引导区、目录和文件。 常见的 windows 下的文件系统格式包括 FAT32、 NTFS、 exFAT。 在使用文件系统前,要先对存储介质进行格式化。格式化先擦除原来内容,在存储介质上新建一个文件分配表和目录。这样,文件系统就可以记录数据存放的物理地址,剩余空间。
使用文件系统时, 数据都以文件的形式存储。写入新文件时,先在目录中创建一个文件索引,它指示了文件存放的物理地址,再把数据存储到该地址中。当需要读取数据时,可以从目录中找到该文件的索引,进而在相应的地址中读取出数据。具体还涉及到逻辑地址、簇大小、不连续存储等一系列辅助结构或处理过程。
文件系统的存在使我们在存取数据时,不再是简单地向某物理地址直接读写,而是要遵循它的读写格式。如经过逻辑转换,一个完整的文件可能被分开成多段存储到不连续的物理地址,使用目录或链表的方式来获知下一段的位置。SPI Flash 芯片驱动只完成了向物理地址写入数据的工作,而根据文件系统格式的逻辑转换部分则需要额外的代码来完成。实质上,这个逻辑转换部分可以理解为当我们需要写入一段数据时,由它来求解向什么物理地址写入数据、以什么格式写入及写入一些原始数据以外的信息(如目录)。这个逻辑转换部分代码我们也习惯称之为文件系统。
2、FatFs 文件系统简介
上面提到的逻辑转换部分代码(文件系统)即为FatFs 文件系统要点,文件系统庞大而复杂,它需要根据应用的文件系统格式而编写,而且一般与驱动层分离开来,很方便移植,所以工程应用中一般是移植现成的文件系统源码。
常用的文件系统:FAT/FATFS(小型嵌入式系统)、 NTFS (WINDOWS)、CDFS(光盘)、exFAT(内存)。
FatFs 是面向小型嵌入式系统的一种通用的 FAT 文件系统。它完全是由 AISI C 语言编写并且完全独立于底层的 I/O 介质。因此它可以很容易地不加修改地移植到其他的处理器当中,如 8051、 PIC、 AVR、 SH、 Z80、 H8、 ARM 等。 FatFs 支持 FAT12、 FAT16、FAT32 等格式利用文件系统的各种函数, 所以可以利用FatFs对 SPI Flash 芯片以“文件”格式进行读写操作。
FATFS优点:免费开源,专门为小型嵌入式系统设计,c编写,支持FAT12, FAT16 与 FAT32,支持多种存储媒介,有独立的缓冲区,可对多个文件进行读写,可裁剪的文件系统(极为重要)。
FATFS的特点:
- Windows兼容的FAT文件系统(支持FAT12/FAT16/FAT32)与平台无关,移植简单
- 代码量少、效率高
- 多种配置选项
- 支持多卷(物理驱动器或分区, 最多10个卷)
- 多个ANSI/OEM代码页包括DBCS
- 支持长文件名、 ANSI/OEM 或Unicode
- 支持RTOS
- 支持多种扇区 大小
- 只读、最小化的API和I/O缓冲区等
FatFs 文件系统的源码可以从 fatfs 官网下载:http://elm-chan.org/fsw/ff/00index_e.html
3、FatFs 的目录结构
在移植 FatFs 文件系统到开发板之前,我们先要到 FatFs 的官网获取源码, 最新版本为R0.11a,官网有对 FatFs 做详细的介绍,有兴趣可以了解。
解压之后可看到里面有 doc 和src 这两个文件夹。 doc 文件夹里面是一些使用帮助文档; src 才是 FatFs 文件系统的源码。
3.1、doc文件夹
打开 doc 文件夹,可看到如下图的文件目录:
其中 en 和 ja 这两个文件夹里面是编译好的 html 文档,讲的是 FATFS 里面各个函数的使用方法,这些函数都是封装得非常好的函数,利用这些函数我们就可以操作 SPI Flash 芯片。这两个文件夹的唯一区别就是 en 文件夹下的文档是英文的, ja 文件夹下的是日文的。 img 文件夹包含 en 和 ja 文件夹下文件需要用到的图片,还有四个名为 app.c 文件,内容都是 FatFs 具体应用例程。 00index_e.html 和00index_j.html 是一些关于 FATFS 的简介,至于另外两个文件可以不看。
3.2、src文件夹
打开 src 文件夹,可看到如下图文件目录:
option 文件夹下是一些可选的外部 c 文件,包含了多语言支持需要用到的文件和转换函数。
00history.txt 介绍了 FatFs 的版本更新情况。
00readme.txt 说明了当前目录下 diskio.c 、 diskio.h、 ff.c、 ff.h、 integer.h 的功能。
diskio.c 文件是 FatFs 移植最关键的文件,它为文件系统提供了最底层的访问 SPI Flash芯片的方法, FatFs 有且仅有它需要用到与 SPI Flash 芯片相关的函数。
diskio.h 定义了FatFs 用到的宏,以及 diskio.c 文件内与底层硬件接口相关的函数声明。
源码文件功能简介如下:
integer.h:文件中包含了一些数值类型定义。
diskio.c:包含底层存储介质的操作函数,这些函数需要用户自己实现,主要添加底层驱动函数。
ff.c:FatFs 核心文件,文件管理的实现方法。该文件独立于底层介质操作文件的函数,利用这些函数实现文件的读写。
cc936.c:本文件在 option 目录下,是简体中文支持所需要添加的文件,包含了简体中文的 GBK 和 Unicode 相互转换功能函数。
ffconf.h:这个头文件包含了对 FatFs 功能配置的宏定义,通过修改这些宏定义就可以裁剪 FatFs 的功能。如需要支持简体中文,需要把 ffconf.h 中的_CODE_PAGE的宏改成 936 并把上面的 cc936.c 文件加入到工程之中。
建议阅读这些源码的顺序为: integer.h --> diskio.c --> ff.c 。
阅读文件系统源码 ff.c 文件需要一定的功底,建议读者先阅读 FAT32 的文件格式,再去分析 ff.c 文件。若仅为使用文件系统,则只需要理解 integer.h 及 diskio.c 文件并会调用ff.c 文件中的函数就可以了。
4、FatFs 程序结构图
用户应用程序需要由用户编写,想实现什么功能就编写什么的程序,一般我们只用到f_mount()、 f_open()、 f_write()、 f_read()就可以实现文件的读写操作。
FatFs 组件是 FatFs 的主体,文件都在源码 src 文件夹中,其中 ff.c、 ff.h、 integer.h 以及diskio.h 四个文件我们不需要改动,只需要修改 ffconf.h 和 diskio.c 两个文件。
底层设备输入输出要求实现存储设备的读写操作函数、存储设备信息获取函数等等。
5、 FatFs 底层设备驱动函数
FatFs 文件系统与底层介质的驱动分离开来,对底层介质的操作都要交给用户去实现,它仅仅是提供了一个函数接口而已。下表 为 FatFs 移植时用户必须支持的函数。通过表25-1 我们可以清晰知道很多函数是在一定条件下才需要添加的,只有前三个函数是必须添加的。我们完全可以根据实际需求选择实现用到的函数。
前三个函数是实现读文件最基本需求。接下来三个函数是实现创建文件、修改文件需要的。为实现格式化功能,需要在 disk_ioctl 添加两个获取物理设备信息选项。我们一般只有实现前面六个函数就可以了,已经足够满足大部分功能。
为支持简体中文长文件名称需要添加 ff_convert 和 ff_wtoupper 函数,实际这两个已经在 cc936.c 文件中实现了,我们只要直接把 cc936.c 文件添加到工程中就可以了。
后面六个函数一般都不用。如真有需要可以参考 syscall.c 文件(src\option 文件夹内)。
底层设备驱动函数是存放在 diskio.c 文件,我们的目的就是把 diskio.c 中的函数接口与SPI Flash 芯片驱动连接起来。总共有五个函数,分别为设备状态获取(disk_status)、设备初始化(disk_initialize)、扇区读取(disk_read)、扇区写入(disk_write)、其他控制(disk_ioctl)。
6、 FatFs测试
6.1、Flash
FatFs 属于软件组件,不需要附带其他硬件电路。我们使用 SPI Flash 芯片作为物理存储设备,其硬件电路在上一章已经做了分析,这里就直接使用。
FatFs 移植步骤
将 FatFs 组件文件添加到工程中,需要添加 ff.c、 diskio.c 和cc936.c 三个文件,FatFs_test.c为用户测试程序。
如果现在编译工程,可以发现有两个错误,一个是来自 diskio.c 文件,提示有一些头文件没找, diskio.c 文件内容是与底层设备输入输出接口函数文件,不同硬件设计驱动就不同,需要的文件也不同;另外一个错误来自 cc936.c 文件,提示该文件不是工程所必需的,这是因为 FatFs 默认使用日语,我们想要支持简体中文需要修改 FatFs 的配置,即修改 ffconf.h 文件。至此,将 FatFs 添加到工程的框架已经操作完成,接下来要做的就是修改文件和 ffconf.h 文件。
(1)、ffconf.h文件配置
ffconf.h 文件是 FatFs 功能配置文件,我们可以对文件内容进行修改,使得 FatFs 更符合我们的要求。 ffconf.h 对每个配置选项都做了详细的使用情况说明。
下面只列出修改的配置,其他配置采用默认即可:
//ffconf.h #define _USE_MKFS 1 #define _CODE_PAGE 936 #define _USE_LFN 2 #define _VOLUMES 2 #define _MIN_SS 512 #define _MAX_SS 4096 _USE_MKFS:格式化功能选择,为使用 FatFs 格式化功能,需要把它设置为 1。 _CODE_PAGE:语言功能选择,并要求把相关语言文件添加到工程宏。为支持简体中文文件名需要使用“936”。 _USE_LFN:长文件名支持,默认不支持长文件名,这里配置为 2,支持长文件名,并指定使用栈空间为缓冲区。 _VOLUMES:指定物理设备数量,这里设置为 2,包括预留 SD 卡和 SPI Flash 芯片。 _MIN_SS、_MAX_SS:指定扇区大小的最小值和最大值。 SD 卡扇区大小一般都为 512 字节, SPI Flash 芯片扇区大小一般设置为 4096 字节,所以需要把_MAX_SS 改为 4096。
(2)、底层设备驱动函数
/*-----------------------------------------------------------------------*/ /* Low level disk I/O module skeleton for FatFs (C)ChaN, 2014 */ /*-----------------------------------------------------------------------*/ /* If a working storage control module is available, it should be */ /* attached to the FatFs via a glue function rather than modifying it. */ /* This is an example of glue functions to attach various exsisting */ /* storage control modules to the FatFs module with a defined API. */ /*-----------------------------------------------------------------------*/ #include "diskio.h" /* FatFs lower layer API */ #include "ff.h" #include "main.h" //#include "usbdisk.h" /* Example: Header file of existing USB MSD control module */ //#include "atadrive.h" /* Example: Header file of existing ATA harddisk control module */ //#include "sdcard.h" /* Example: Header file of existing MMC/SDC contorl module */ /*为每个设备定义一个物理编号*/ /* Definitions of physical drive number for each drive 每个驱动器的物理驱动器号的定义*/ #define ATA 0 /* Example: Map ATA harddisk to physical drive 0 将一个硬盘映射到物理驱动器0*/ #define MMC 1 /* Example: Map MMC card to physical drive 1 将MMC卡映射到物理驱动器1*/ #define USB 2 /* Example: Map USB to physical drive 2 将USB映射到物理驱动器2*/ #define SD 3 /* Example: Map SD to physical drive 3 将SD卡映射到物理驱动器3*/ #define Flash 4 /* Example: Map FLASH to physical drive 4 将Flash映射到物理驱动器4*/ /*-----------------------------------------------------------------------*/ /* Get Drive Status 获取设备状态 */ /*-----------------------------------------------------------------------*/ /*BYTE pdrv :Physical drive nmuber to identify the drive 设备物理编号,通过物理驱动器编号来识别驱动器*/ DSTATUS disk_status (BYTE pdrv) { DSTATUS status = STA_NOINIT & 0x00; //int result; switch(pdrv) { case ATA: //result = ATA_disk_status();//获取设备状态 break; case MMC: //result = MMC_disk_status(); break; case USB: //result = USB_disk_status(); break; case SD: //result = SD_disk_status(); break; case Flash : //result = FLASH_disk_status(); //SPI Flash状态检测:读取SPI Flash 设备ID if(FLASH_ID == FLASH_Read_Jedec_ID()) //设备ID读取结果正确 { status = STA_NOINIT & 0x00; } else //设备ID读取结果错误 { status = STA_NOINIT; } break; default: status = STA_NOINIT; break; } return status; } /*-----------------------------------------------------------------------*/ /* Inidialize a Drive 设备初始化 */ /*-----------------------------------------------------------------------*/ /*BYTE pdrv :Physical drive nmuber to identify the drive 设备物理编号,通过物理驱动器编号来识别驱动器*/ DSTATUS disk_initialize(BYTE pdrv) { DSTATUS status = STA_NOINIT & 0x00; //int result; switch(pdrv) { case ATA: //result = ATA_disk_initialize();//设备初始化 break; case MMC: //result = MMC_disk_initialize(); break; case USB: //result = USB_disk_initialize(); break; case SD: //result = SD_disk_initialize(); break; case Flash: //result = FLASH_disk_initialize(); SPI_1_Config_Init(); int i=500; while(--i); Flash_PowerOn_Mode(); //唤醒Flash status = disk_status(Flash); //获取SPI Flash芯片状态 break; default: status = STA_NOINIT; break; } return status; } /*-----------------------------------------------------------------------*/ /* Read Sector(s) 读扇区:读取扇区内容到指定存储区 */ /*-----------------------------------------------------------------------*/ /*BYTE pdrv:Physical drive nmuber to identify the drive 设备物理编号(0...)*/ /*BYTE *buff:Data buffer to store read data 数据缓存区*/ /*DWORD sector:Sector address in LBA 扇区首地址*/ /*UINT count:Number of sectors to read 扇区个数(1...128)*/ DRESULT disk_read(BYTE pdrv,BYTE *buff, DWORD sector,UINT count) { DRESULT status = STA_NOINIT & 0x00; //int result; switch (pdrv) { case ATA: //result = ATA_disk_read(buff, sector, count);//读取扇区内容到指定存储区 break; case MMC: //result = MMC_disk_read(buff, sector, count); break; case USB: //result = USB_disk_read(buff, sector, count); break; case SD: //result = DS_disk_read(buff, sector, count); break; case Flash: //result = FLASH_disk_read(buff, sector, count); /*开发板使用的 SPI Flash 芯片型号为 W25Q128FV,每个扇区大小为 4096 个字节(4KB),总共有 16M 字节空间,为兼 容后面实验程序,我们只将后部分 10MB 空间分配给 FatFs 使用,前部分 6MB 空间用于其他实验需要,即 FatFs 是从 6MB 空间开始,为实现这个效果需要将所有的读写地址都偏移 1536 个扇区空间。 对于 SPI Flash 芯片,主要是使用 SPI_FLASH_BufferRead()实现在指定地址读取指定长度的数据,它接收三个参数, 第一个参数为指定数据存放地址指针。第二个参数为指定数据读取地址,这里使用左移运算符,左移12位实际是乘以4096, 这与每个扇区大小是息息相关的。第三个参数为读取数据个数,也是需要使用左移运算符。*/ sector = sector + 1536; //扇区偏移6MB,外部Flash文件系统空间放在SPI Flash后面10MB空间 FLASH_Read_Buffer(sector <<12, count<<12,buff); status = RES_OK; break; default: status = RES_PARERR; break; } return status; } /*-----------------------------------------------------------------------*/ /* Write Sector(s) 写扇区:将数据写入指定扇区空间上 */ /*-----------------------------------------------------------------------*/ /*BYTE pdrv: Physical drive nmuber to identify the drive 设备物理编号(0...)*/ /*const BYTE *buff: Data to be written 写入数据的缓存区*/ /*DWORD sector: Sector address in LBA 扇区首地址*/ /*UINT count: Number of sectors to write 扇区个数(1...128)*/ #if _USE_WRITE DRESULT disk_write(BYTE pdrv,const BYTE *buff,DWORD sector,UINT count) { DRESULT status = STA_NOINIT & 0x00; uint32_t write_addr; //int result; if(!count) //扇区个数为0 { status = STA_NOINIT; return status; } switch (pdrv) { case ATA: //result = ATA_disk_write(buff, sector, count);//将数据写入指定扇区空间上 break; case MMC: //result = MMC_disk_write(buff, sector, count); break; case USB: //result = USB_disk_write(buff, sector, count); break; case SD: //result = SD_disk_write(buff, sector, count); break; case Flash: //result = FLASH_disk_write(buff, sector, count); /*扇区偏移6MB,外部Flash文件系统空间放在Flash后面10MB空间 */ sector = sector + 1536; write_addr = sector<<12; FLASH_Erase_Sector(write_addr); //写入数据前先擦除 FLASH_Write_Buffer(write_addr,count<<12,(uint8_t *)buff); status = RES_OK; break; default: status = STA_NOINIT; break; } return status; } #endif /*-----------------------------------------------------------------------*/ /* Miscellaneous Functions 其他控制 */ /*-----------------------------------------------------------------------*/ /*BYTE pdrv:Physical drive nmuber (0..) 设备物理编号 */ /*BYTE cmd:Control code 控制指令,包括发出同步信号、获取扇区数目、获取扇区大小、获取擦除块数量等等指令*/ /*void *buff:Buffer to send/receive control data 写入或者读取数据地址指针*/ #if _USE_IOCTL DRESULT disk_ioctl (BYTE pdrv,BYTE cmd,void *buff) { DRESULT status = STA_NOINIT & 0x00; int result; switch (pdrv) { case ATA: break; case MMC: break; case USB: break; case SD: break; case Flash: switch (cmd) { case GET_SECTOR_COUNT: //扇区数量:2560*4096/1024/1024=10(MB) *(DWORD * )buff = 2560; break; case GET_SECTOR_SIZE : //扇区大小 *(WORD * )buff = 4096; break; case GET_BLOCK_SIZE : //同时擦除扇区个数 *(DWORD * )buff = 1; break; default: status = STA_NOINIT; break; } break; default: status = STA_NOINIT; break; } return status; } #endif /* get_fattime 函数用于获取当前时间戳,在 ff.c 文件中被调用。 FatFs 在文件创建、被修改时会记录时间,这里我们直接使用赋值方法 设定时间戳。为更好的记录时间,可以使用控制器 RTC 功能,具体要求返回值格式为: bit31:25 ——从 1980 至今是多少年,范围是 (0..127) ; bit24:21 ——月份,范围为 (1..12) ; bit20:16 ——该月份中的第几日,范围为(1..31) ; bit15:11——时,范围为 (0..23); bit10:5 ——分,范围为 (0..59); bit4:0 ——秒/ 2,范围为 (0..29) */ __weak DWORD get_fattime(void) { //返回当前时间戳 return ( (DWORD)(2015 - 1980) << 25) //Year 2015 | ((DWORD)1 << 21) //Month 1 | ((DWORD)1 << 16) //Mday 1 | ((DWORD)0 << 11) //Hour 0 | ((DWORD)0 << 5) //Min 0 | ((DWORD)0 >> 1); //Sec 0 }
FatFs 的第一步工作就是使用 f_mount 函数挂载工作区。 f_mount 函数有三个形参,第一个参数是指向 FATFS 变量指针,如果赋值为 NULL 可以取消物理设备挂载。第二个参 数为逻辑设备编号,使用设备根路径表示,与物理设备编号挂钩,在代码中我们定义 SPI Flash 芯片物理编号为 1,所以这里使用“1:”。第三个参数可选 0 或 1, 1 表示立即挂载, 0 表示不立即挂载,延迟挂载。 f_mount 函数会返回一个 FRESULT 类型值,指示运行情况。
FRESULT f_mount ( FATFS* fs, /* Pointer to the file system object (NULL:unmount)*/ const TCHAR* path, /* Logical drive number to be mounted/unmounted */ BYTE opt /* 0:Do not mount (delayed mount), 1:Mount immediately */ )
如果 f_mount 函数返回值为 FR_NO_FILESYSTEM,说明没有 FAT 文件系统,比如新出厂的 SPI Flash 芯片就没有 FAT 文件系统。我们就必须对物理设备进行格式化处理。使用 f_mkfs 函数可以实现格式化操作。 f_mkfs 函数有三个形参,第一个参数为逻辑设备编号;第二参数可选 0 或者 1, 0 表示设备为一般硬盘, 1 表示设备为软盘。第三个参数指定扇区大小,如果为 0,表示通过代码中 disk_ioctl 函数获取。格式化成功后需要先取消挂载原来设备,再重新挂载设备。
FRESULT f_mkfs ( const TCHAR* path, /* Logical drive number */ BYTE sfd, /* Partitioning rule 0:FDISK, 1:SFD */ UINT au /* Size of allocation unit in unit of byte or sector */ )
在设备正常挂载后,就可以进行文件读写操作了。使用文件之前,必须使用 f_open 函数打开文件,不再使用文件必须使用 f_close 函数关闭文件,这个跟电脑端操作文件步骤类似。 f_open 函数有三个形参,第一个参数为文件对象指针。第二参数为目标文件,包含绝对路径的文件名称和后缀名。第三个参数为访问文件模式选择,可以是打开已经存在的文件模式、读模式、写模式、新建模式、总是新建模式等的或运行结果。比如对于写测试,使用 FA_CREATE_ALWAYS 和 FA_WRITE 组合模式,就是总是新建文件并进行写模式。f_close 函数用于不再对文件进行读写操作关闭文件, f_close 函数只要一个形参,为文件对象指针。 f_close 函数运行可以确保缓冲区完全写入到文件内。
FRESULT f_open ( FIL* fp, /* Pointer to the blank file object */ const TCHAR* path, /* Pointer to the file name */ BYTE mode /* Access mode and file open mode flags */ )
FRESULT f_close ( FIL *fp /* Pointer to the file object to be closed */ )
成功打开文件之后就可以使用 f_write 函数和 f_read 函数对文件进行写操作和读操作。这两个函数用到的参数是一致的,只不过一个是数据写入,一个是数据读取。 f_write 函数第一个形参为文件对象指针,使用与 f_open 函数一致即可。第二个参数为待写入数据的首地址,对于 f_read 函数就是用来存放读出数据的首地址。第三个参数为写入数据的字节数,对于 f_read 函数就是欲读取数据的字节数。第四个参数为 32 位无符号整形指针,这里使用fnum 变量地址赋值给它,在运行读写操作函数后, fnum 变量指示成功读取或者写入的字节个数。
FRESULT f_write ( FIL* fp, /* Pointer to the file object */ const void *buff, /* Pointer to the data to be written */ UINT btw, /* Number of bytes to write */ UINT* bw /* Pointer to number of bytes written */ )
FRESULT f_read ( FIL* fp, /* Pointer to the file object */ void* buff, /* Pointer to data buffer */ UINT btr, /* Number of bytes to read */ UINT* br /* Pointer to number of bytes read */ )
最后,不再使用文件系统时,使用 f_mount 函数取消挂载。
注意:使用文件系统时,引脚初始化需要放在系统中