什么是文件系统

负责管理和存储文件信息的软件机构,在磁盘上组织文件的方法。

即使读者可能不了解文件系统,读者也一定对“文件”这个概念十分熟悉,数据在PC上是以文件的形式储存在磁盘中的,这些数据的形式一般为ASCII码或者二进制形式出现。在之前我已经写过了SPI Flash芯片W25Q64的驱动函数,我们可以非常方便的在SPI FLASH芯片上读写数据。如需要记录一系列字符串时,可以先把字符串转化为ASCII码,存储在数组中,然后调用SPI_FLASH_BufferWrite函数,把数组内容写入到SPI Flash芯片的指定地址上,在需要的时候从地址把数据读取出来,再对读出来的数据以ASCII码的格式进行解读。

但是,这样存储数据会带来极大的不便,如难以记录有效数据的位置,难以确定存储介质的剩余空间,以及应以何种格式来解读数据。就如同一个巨大的图书馆无人管理,杂乱无章的存放着各种书籍,难以查找所需的文档。这样直接存储数据的方式对于小容量的EEPROM还可以接受,但对于SPI FLASH芯片或者SD卡之类的大容量设备,我们需要一种高效的方式来管理它的存储内容。

这些管理方式即为文件系统,她是为了存储和管理数据,而在存储介质建立的一种组织结构,这些结构包括操作系统引导区,目录和文件。常见的windows下的文件系统格式包括FAT32、NTFS、exFAT.在使用文件系统之前,要先对存储介质进行格式化。格式化先擦除原来内容,在存储介质上新建立一个文件分配表和目录。这样,文件系统就可以记录数据存放的物理地址、剩余空间。

使用文件系统时,数据都以文件的形式存储。写入新文件时先在目录中创建一个文件索引,它指示了文件存放的物理地址,再把数据存储到该地址中。当需要读取数据时,可以从目录中找到该文件的索引,进而在相应的地址中读取出数据。具体还涉及到逻辑地址、簇大小、不连续存储等一系列辅助结构或处理过程。

文件系统的存在使我们在存储数据时,不再是简单的向某物理地址直接读写,而是要遵循它的读写格式。如经过逻辑转换,一个完整的文件可能被分开成多段存储到不连续的物理地址,使用目录或者链表的方式来获知下一段的位置。

常用的文件系统
FAT/FATFS 小型嵌入式系统

NTFS WINDOWS

CDFS 光盘

exFAT 更适合用于闪存

FATFS优点:免费开源,专门为小型嵌入式系统设计,完全用C语言编写;支持FAT12,FAT16与FAT32,支持多种存储媒介,有独立的缓冲区,可对多个文件进行读写,可裁剪的文件系统(极为重要)

FATFS的特点:
Windows兼容的FAT文件系统,(支持FAT12,FAT16,FAT32)
与平台无关,移植简单
代码量少,效率高
多种配置选项,可以裁剪
支持多卷(物理驱动器或分区,最多10个卷)
多个ANSI/OEM代码页包括DBCS
支持长文件名、ANSI/OEM或Unicode
支持RTOS
支持多种扇区大小
只读,最小化的API和I/O缓冲区等
由于它以上的特点,使得FATFS在嵌入式系统中被广泛应用

FATFS层次结构:



1.底层接口,包括存储媒介读/写接口(disk I/O)和供给文件创建修改时间的实时时钟,需要我们根据平台和存储介质编写移植代码
2.中间层FATFS模块,实现了FAT文件读/写协议。FATFS模块提供的是ff.c和ff.h。除非有必要,使用者一般不用修改,使用时将头文件直接包含进去即可。
3.最顶层是应用层,使用者无需理会FATFS的内部结构和复杂的FAT协议,只需要调用FATFS模块提供给用户的一系列应用接口函数,如f_open,f_read,f_write,f_close等,就可以像在PC上读写文件那样简单。


FATFS的整个系统可以在FATFS的官网下载:官网地址

同时在官网还可以查看每个函数的说明,同时大部分的函数都带有示例,是不错的学习资源。

系统包的结构:



diskio.c和diskio.h是硬件层,需要根据存储介质来修改

ff.c和ff.h是FATFS的文件系统层和文件系统的API层

移植步骤:
数据类型:在integer.h里面定义好数据的类型,这里需要了解你用的编译器的数据类型,并根据编译器定义好数据类型。
配置:通过ffconf.h配置FATFS的相关功能,以满足你的需要
函数编写:打开diskio.c,进行底层驱动编写,一般需要编写6个接口


相关配置宏:
_FS_TINY mini版本的FATFS

_FS_READONLY 设置只读,可以减少所占的空间

_FS_MINIMIZE 削减函数

_USE_STRFUNC 字符及字符串操作函数

_USE_MKFS 是否启用格式化

_USE_FASTSEEK 使能快速定位

_USE_LABEL 是否支持磁盘盘符的设置和读取

_CODE_PAGE 设置语言936——中文GBK编码

_USE_LFN 是否支持长文件名,值不同存储的位置不同

_MAX_LFN 文件名的最大长度

_VOLUMES 支持的逻辑设备数目

_MAX_SS 扇区缓冲最大值,一般为512

FATFS文件系统移植实验
FATFS程序结构图
移植FATFS文件系统之前,我们先通过FATFS的程序结构图了解FATFS在程序中的关系网络,见图FATFS程序结构图。



用户应用程序需要由用户编写,想实现什么功能就编写什么的程序,一般我们只用到 f_read() 就可以实现文件的读写操作。

FATFS组件是FATFS的主体,文件都在源码src中,其中ff.c和ff.h、integer.h以及diskio.h四个文件我们不需要改动,只需要修改ffconf.h和diskio.c两个文件。

底层设备输入输出要求实现存储设备的读写操作函数、存储设备信息获取函数等等。我们使用SPI FLASH芯片作为物理设备,在之前已经编写好的驱动程序,这里我们可以直接使用。

FATFS底层设备驱动函数
FATFS文件系统与底层介质的驱动分离开来,对底层介质的操作都要交给用户去实现,它仅仅是提供了一个函数接口而已。表FATFS移植需要用户支持函数为FATFS移植时用户必须支持的函数。通过表FATFS移植需要用户支持函数我们可以清晰知道很多函数是在一定条件下才需要添加的,只有前三个函数是必须添加的。我们完全可以根据实际需求选择实现用到的函数。

前三个函数是实现读文件的最基本需求。接下来三个函数是实现创建文件、修改文件需要的。为实现格式化功能,需要在disk_ioctl添加两个获取物理设备信息选项。我们一般只需要实现前面6个函数就可以了,已经满足大部分功能。



底层设备驱动函数是diskio.c文件,我们的目的就是把diskio.c中的函数接口与SPI Flash芯片驱动连接起来。总共有5个函数,分别为设备状态获取(disk_status)、设备初始化(disk_initialize)、扇区读取(disk_read)、扇区写入(disk_write)、其他控制(disk_ioctl)。

下面我们结合SPI Flash芯片驱动函数做详细讲解。

宏定义

/*为每个设备定义一个物理编号*/
#define     ATA    0    //预留SD卡用
#define     SPI_FLASH    1    //外部SPI FLASH

这个两个宏定义在FATFS文件系统中非常重要,FATFS是支持多物理设备的,必须为每个物理设备定义一个不同的编号。

SD卡是预留接口,是和SDIO有关的内容。

设备状态获取

DSTATUS disk_status(

    BYTE    prdv    /*物理编号*/
)
{
    DSTATUS    status = STA_NOINIT;

    switch(pdrv){

       case ATA :    /*SD CARD*/
            break;
        case SPI_FLASH :
        /*spi flash 状态检测:读取SPI_FLASH设备ID*/
        if(sFLASH_ID == SPI_FLASH_ReadID())
        /*设备ID读取结果正常*/
        {
                status &= ~STA_NOINIT;
        }
        else
        {
                /*设备ID读取结果失误*/
                status = STA_NOINIT;
        }
            break;
default:
        status = STA_NOINIT;

        }
    return status;

}

disk_status函数只有一个参数pdrv,表示物理编号。一般我们都是使用switch函数实现对prdv的分支判断。对于SD卡只是预留接口,留空即可。对于SPI FLASH芯片,我们直接调用在SPI_FLASH_ReadID()获取设备ID,然后判断是否正确,如果正确,函数返回正常标准;如果错误,函数返回异常标志。SPI_FLASHReadID()是定义在之前的SPI FLASH驱动文件中的函数。

设备初始化

DSTATUS disk_initialize(
    BYTE prdv    /*物理编号*/
)
{
    uint16_t i;
    DSTATUS stauts = STA_NOINIT;
    switch(pdrv){
        case ATA :
        /*sd card*/
        break;
        case SPI_FLASH :
        SPI_FLASH_Init();
        /*延时一小段时间*/
        i= 500;    
        while(i--);
        /*唤醒SPI FLASH*/
        SPI_FLASH_WAKEUP();
        /*获取SPI FLASH芯片状态*/
        status = disk_status(SPI_FLASH);
        break;
     default :
        status = STA_NOINIT;

    }
    return status;

}

disk_initialize函数也是有一个参数prdv,用来指定设备物理编号。对于SPI FLASH芯片我们调用SPI_FLASH_Init()函数实现对SPI FLASH芯片引脚GPIO初始化配置以及SPI通信参数配置。SPI_FLASH_WAKEUP()函数唤醒SPI FLASH芯片,当芯片处于睡眠模式时需要唤醒芯片才可以进行读写。

最后调用disk_status函数获取SPI FLASH芯片状态,并返回状态值。

读取扇区

DRESULT disk_read(
        BYTE pdrv,    //设备物理编号
        BYTE *buff,    //数据缓存区
        DWORD sector,    //扇区首地址
        UINT count        //扇区个数
)
{
    DRESULT status = RES_PARERR;
    switch(pdrv)
    {
        case ATA :
        break;
        case SPI_FLASH :
        /*扇区偏移2MB,外部FLASH文件系统空间放在SPI FLASH后面的6MB空间*/
        sector += 512;
        SPI_FLASH_BufferRead(buff,sector<<12,count<<12);
        stauts = RES_OK;
        break;
        default :
        status = RES_PARERR;

    }
    return status;

}

disk_read函数有4个形参,pdrv为设备物理编号,buff是一个BYTE型指针变量,buff指向用来存放读取到数据的存储首地址。sector是一个DWORD类型变量,指定要读取数据的扇区首地址,count是一个UINT类型变量,指定扇区数量。

扇区写入

DRESULT disk_write(
    BYTE prdv,    //设备物理编号
    const BYTE *buff, //欲写入数据的缓存区
    DWORD sector, //扇区首地址
    UINT count    //扇区个数

)
{
    uint32_t write_addr;
    DRESULT status = RES_PARERR;
     if(!count){

                return RES_PARERR;
                }
      switch(prdv){

        case ATA :
            break;

        case SPI_FALSH :
        /*扇区偏移2MB,外部FLASH文件系统空间放在SPI FLASH 后面的6MB空间*/
        sector += 512;
        write_addr = sector<<12;
        SPI_FLASH_SectorErase(write_addr);
        SPI_FALSH_BufferWrite((u8 *)buff,write_addr,count<<12);
        status = RES_OK;
        break;
    default:
        status = RES_PARERR;

        }

    return status;

}

disk_write函数有四个形参,pdrv为设备物理编号,buff指向待写入扇区数据的首地址。sector指定要写入数据的扇区首地址,count指定扇区数量。对于SPI FLASH芯片,在写入数据之前,需要先擦除,所以用到扇区擦除函数(SPI_FLASH_SectorErase).然后就是在调用数据写入函数(SPI_FLASH_BufferWrite)把数据写入到指定位置。

FATFS给用户提供了大量的API函数,可以满足我们对文件的各种操作。

f_mount 注册/注销一个工作区域

f_open 打开/创建一个文件

f_close 关闭一个文件

f_read 读文件

f_write 写文件

f_lseek 移动文件读写指针

f_sync 冲洗缓冲数据Flush Cached Data

f_forward 直接转移文件数据到一个数据流

f_stat 获取文件状态

f_opendir 打开一个目录

f_closedir 关闭一个已经打开的目录

诸如其他的还有很多的相关的文件操作,具体的和C语言的文件操作大同小异,可以去FATFS文件系统官网看看详细的文件操作函数。这里笔者不再赘述。