25. 串行FLASH文件系统FatFs

本章参考资料:《00index_e.html》,这是FatFs官方的编译好的HTML文档,里面有FatFs所有函数的介绍和函数的应用示例,学习FatFs看这个官方的文档即可。

FatFs参考资料

25.1. 文件系统

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

但是,这样直接存储数据会带来极大的不便,如难以记录有效数据的位置,难以确定存储介质的剩余空间,以及应以何种格式来解读数据。 就如同一个巨大的图书馆无人管理,杂乱无章地存放着各种书籍,难以查找所需的文档。想象一下图书馆的采购人员购书后,把书籍往馆内一扔,拍拍屁股走人, 当有人来借阅某本书的时候,就不得不一本本地查找。这样直接存储数据的方式对于小容量的存储介质如EEPROM还可以接受, 但对于SPI Flash芯片或者SD卡之类的大容量设备,我们需要一种高效的方式来管理它的存储内容。

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

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

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

上一章的SPI Flash芯片驱动只完成了向物理地址写入数据的工作,而根据文件系统格式的逻辑转换部分则需要额外的代码来完成。 实质上,这个逻辑转换部分可以理解为当我们需要写入一段数据时,由它来求解向什么物理地址写入数据、以什么格式写入及写入一些原始数据以外的信息(如目录)。 这个逻辑转换部分代码我们也习惯称之为文件系统。

25.2. FatFs文件系统简介

上面提到的逻辑转换部分代码(文件系统)即为本章的要点,文件系统庞大而复杂,它需要根据应用的文件系统格式而编写, 而且一般与驱动层分离开来,很方便移植,所以工程应用中一般是移植现成的文件系统源码。

FatFs是面向小型嵌入式系统的一种通用的FAT文件系统。它完全是由ANSIC语言编写并且完全独立于底层的I/O介质。 因此它可以很容易地不加修改地移植到其他的处理器当中,如8051、PIC、AVR、SH、Z80、H8、ARM等。FatFs支持FAT12、FAT16、FAT32等格式, 所以我们利用前面写好的SPI Flash芯片驱动,把FatFs文件系统代码移植到工程之中, 就可以利用文件系统的各种函数,对SPI Flash芯片以“文件”格式进行读写操作了。

FatFs文件系统的源码可以从fatfs官网下载:

http://elm-chan.org/fsw/ff/00index_e.html

25.2.1. FatFs的目录结构

在移植FatFs文件系统到开发板之前,我们先要到FatFs的官网获取源码,官网有对FatFs做详细的介绍,有兴趣可以了解。 解压之后可看到里面有 doc 和 src 这两个文件夹,见图 FatFs文件目录。 doc 文件夹里面是一些使用帮助文档; src 才是FatFs文件系统的源码。

FatFs文件目录

25.2.2. FatFs帮助文档

打开 doc 文件夹,可看到如图 doc文件夹的文件目录 的文件目录:

doc文件夹的文件目录

其中 en 和 ja 这两个文件夹里面是编译好的html文档,讲的是FATFS里面各个函数的使用方法,这些函数都是封装得非常好的函数, 利用这些函数我们就可以操作SPI Flash芯片。有关具体的函数我们在用到的时候再讲解。这两个文件夹的唯一区别就是 en 文件夹下的文档是英文的, ja文件夹下的是日文的。img文件夹包含en和ja文件夹下文件需要用到的图片,还有四个名为app.c文件,内容都是FatFs具体应用例程。 00index_e.html和00index_j.html是一些关于FATFS的简介,至于另外两个文件可以不看。

25.2.3. FATFS源码

打开 src 文件夹,可看到如图 src文件夹的文件目录 的文件目录:

src文件夹的文件目录

option 文件夹下是一些可选的外部c文件,包含了多语言支持需要用到的文件和转换函数。

diskio.c文件是FatFs移植最关键的文件,它为文件系统提供了最底层的访问SPI Flash芯片的方法, FatFs有且仅有它需要用到与SPI Flash芯片相关的函数。 diskio.h定义了FatFs用到的宏,以及diskio.c文件内与底层硬件接口相关的函数声明。

00history.txt介绍了FatFs的版本更新情况。

00readme.txt说明了当前目录下 diskio.c 、diskio.h、ff.c、ff.h、integer.h的功能。

src文件夹下的源码文件功能简介如下:

  • 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文件中的函数就可以了。 本章主要讲解如何把FATFS文件系统移植到开发板上,并编写一个简单读写操作范例。

25.3. FatFs文件系统移植实验

25.3.1. FatFs程序结构图

移植FatFs之前我们先通过FatFs的程序结构图了解FatFs在程序中的关系网络,见图 FatFs程序结构图

FatFs程序结构图

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

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

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

25.3.2. 硬件设计

FatFs属于软件组件,不需要附带其他硬件电路。我们使用SPI Flash芯片作为物理存储设备,其硬件电路在上一章已经做了分析,这里就直接使用。

25.3.3. FatFs移植步骤

上一章我们已经实现了SPI Flash芯片驱动程序,并实现了读写测试, 为移植FatFs方便,我们直接拷贝一份工程, 我们在工程基础上添加FatFs组件,并修改main函数的用户程序即可。

1) 先拷贝一份SPI Flash芯片测试的工程文件(整个文件夹),并修改文件夹名为“SPI—FatFs文件系统”。 将FatFs源码中的src文件夹整个文件夹拷贝一份至“SPI—FatFs文件系统USER”文件夹下并修改名为“FATFS”, 见图 拷贝FatFs源码到工程

拷贝FatFs源码到工程

2) 使用KEIL软件打开工程文件(..SPI—FatFs文件系统ProjectRVMDK(uv5)\ BH-F103.uvprojx), 并将FatFs组件文件添加到工程中, 需要添加有ff.c、diskio.c和cc936.c三个文件,见图 添加FatFS文件到工程

添加FatFS文件到工程

3) 添加FATFS文件夹到工程的include选项中。打开工程选项对话框,选择“C/C++”选项下的“Include Paths”项目, 在弹出路径设置对话框中选择添加“FATFS”文件夹,见图 添加FATFS路径到工程选项

添加FATFS路径到工程选项

4) 如果现在编译工程,可以发现有两个错误,一个是来自diskio.c文件,提示有一些头文件没找到, diskio.c文件内容是与底层设备输入输出接口函数文件,不同硬件设计驱动就不同,需要的文件也不同; 另外一个错误来自cc936.c文件,提示该文件不是工程所必需的,这是因为FatFs默认使用日语, 我们想要支持简体中文需要修改FatFs的配置,即修改ffconf.h文件。至此,将FatFs添加到工程的框架已经操作完成, 接下来要做的就是修改diskio.c文件和ffconf.h文件。

25.3.4. FatFs底层设备驱动函数

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

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

为支持简体中文长文件名称需要添加ff_convert和ff_wtoupper函数,实际这两个已经在cc936.c文件中实现,我们只要直接把cc936.c文件添加到工程中就可以。

后面六个函数一般都不用。如真有需要可以参考syscall.c文件(srcoption文件夹内)。

FatFs移植需要用户支持函数

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

接下来,我们对每个函数结合SPI Flash芯片驱动做详细讲解。

宏定义

代码清单:文件系统-1 物理编号宏定义
1
2
3
/* 为每个设备定义一个物理编号 */
#define ATA         0     // 预留SD卡使用
#define SPI_FLASH   1     // 外部SPI Flash

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

SD卡是预留接口,在讲解SDIO接口相关章节后会用到,可以实现使用读写SD卡内文件。

设备状态获取

代码清单:文件系统-2设备状态获取
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
DSTATUS disk_status (
    BYTE pdrv   /* 物理编号 */
)
{

    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函数实现对pdrv的分支判断。 对于SD卡只是预留接口,留空即可。对于SPI Flash芯片,我们直接调用在SPI_FLASH_ReadID()获取设备ID, 然后判断是否正确,如果正确,函数返回正常标准;如果错误,函数返回异常标志。SPI_FLASH_ReadID()是定义在bsp_spi_flash.c文件中,上一章节已做了分析。

设备初始化

代码清单:文件系统-3 设备初始化
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
DSTATUS disk_initialize (
    BYTE pdrv       /* 物理编号 */
)
{
    uint16_t i;
    DSTATUS status = STA_NOINIT;
    switch (pdrv) {
    case ATA:          /* SD CARD */
        break;

    case SPI_FLASH:    /* SPI Flash */
        /* 初始化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函数也是有一个参数pdrv,用来指定设备物理编号。 对于SPI Flash芯片我们调用SPI_FLASH_Init()函数实现对SPI Flash芯片引脚GPIO初始化配置以及SPI通信参数配置。 SPI_Flash_WAKEUP()函数唤醒SPI Flash芯片,当SPIFlash芯片处于睡眠模式时需要唤醒芯片才可以进行读写操作。

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

读取扇区

代码清单:文件系统-4 扇区读取
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
DRESULT disk_read (
    BYTE pdrv,    /* 设备物理编号(0..) */
    BYTE *buff,   /* 数据缓存区 */
    DWORD sector, /* 扇区首地址 */
    UINT count    /* 扇区个数(1..128) */
)
{
    DRESULT status = RES_PARERR;
    switch (pdrv) {
    case ATA: /* SD CARD */
        break;

    case SPI_FLASH:
        /* 扇区偏移2MB,外部Flash文件系统空间放在SPI Flash后面6MB空间 */
        sector+=512;
        SPI_FLASH_BufferRead(buff, sector <<12, count<<12);
        status = RES_OK;
        break;

    default:
        status = RES_PARERR;
    }
    return status;
}

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

BYTE类型实际是unsigned char类型,DWORD类型实际是unsigned long类型, UINT类型实际是 unsigned int类型,类型定义在integer.h文件中。

开发板使用的SPI Flash芯片型号为W25Q64FV,每个扇区大小为4096个字节(4KB),总共有8M字节空间, 为兼容后面实验程序,我们只将后部分约6MB空间(1534个扇区)分配给FatFs使用, 前部分约2MB空间(514个扇区)用于其他实验需要,即FatFs是从2MB空间开始, 为实现这个效果需要将所有的读写地址都偏移514个扇区空间。

对于SPI Flash芯片,主要是使用SPI_FLASH_BufferRead()实现在指定地址读取指定长度的数据,它接收三个参数, 第一个参数为指定数据存放地址指针。第二个参数为指定数据读取地址,这里使用左移运算符,左移12位实际是乘以4096, 这与每个扇区大小是息息相关的。第三个参数为读取数据个数,也是需要使用左移运算符。

扇区写入

代码清单:文件系统-5 扇区写入
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
DRESULT disk_write (
    BYTE pdrv,        /* 设备物理编号(0..) */
    const BYTE *buff, /* 欲写入数据的缓存区 */
    DWORD sector,     /* 扇区首地址 */
    UINT count        /* 扇区个数(1..128) */
)
{
    uint32_t write_addr;
    DRESULT status = RES_PARERR;
    if (!count) {
        return RES_PARERR;    /* Check parameter */
    }

    switch (pdrv) {
    case ATA: /* SD CARD */
        break;

    case SPI_FLASH:
        /* 扇区偏移2MB,外部Flash文件系统空间放在SPI Flash后面6MB空间 */
        sector+=512;
        write_addr = sector<<12;
        SPI_FLASH_SectorErase(write_addr);
        SPI_FLASH_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)把数据写入到指定位置内。

其他控制

代码清单:文件系统-6 其他控制
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
DRESULT disk_ioctl (
    BYTE pdrv,    /* 物理编号 */
    BYTE cmd,     /* 控制指令 */
    void *buff    /* 写入或者读取数据地址指针 */
)
{
    DRESULT status = RES_PARERR;
    switch (pdrv) {
    case ATA: /* SD CARD */
        break;

    case SPI_FLASH:
        switch (cmd) {
        /* 扇区数量:1536*4096/1024/1024=6(MB) */
        case GET_SECTOR_COUNT:
            *(DWORD * )buff = 1536;
            break;
        /* 扇区大小  */
        case GET_SECTOR_SIZE :
            *(WORD * )buff = 4096;
            break;
        /* 同时擦除扇区个数 */
        case GET_BLOCK_SIZE :
            *(DWORD * )buff = 1;
            break;
        }
        status = RES_OK;
        break;

    default:
        status = RES_PARERR;
    }
    return status;
}

disk_ioctl函数有三个形参,pdrv为设备物理编号,cmd为控制指令,包括发出同步信号、获取扇区数目、获取扇区大小、 获取擦除块数量等等指令,buff为指令对应的数据指针。

对于SPI Flash芯片,为支持FatFs格式化功能, 需要用到获取扇区数量(GET_SECTOR_COUNT)指令和获取擦除块数量(GET_BLOCK_SIZE)。 另外,SD卡扇区大小为512字节,SPI Flash芯片一般设置扇区大小为4096字节,所以需要用到获取扇区大小(GET_SECTOR_SIZE)指令。

时间戳获取

代码清单:文件系统-7 时间戳获取
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
__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 */
}

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)。

25.3.5. FatFs功能配置

ffconf.h文件是FatFs功能配置文件,我们可以对文件内容进行修改,使得FatFs更符合我们的要求。 ffconf.h对每个配置选项都做了详细的使用情况说明。下面只列出修改的配置,其他配置采用默认即可。

代码清单:文件系统-8 FatFs功能配置选项
1
2
3
4
5
6
#define _USE_MKFS   1
#define _CODE_PAGE  936
#define _USE_LFN    2
#define _VOLUMES    2
#define _MIN_SS     512
#define _MAX_SS     4096

1) _USE_MKFS: 格式化功能选择,为使用FatFs格式化功能,需要把它设置为1。

2) _CODE_PAGE: 语言功能选择,并要求把相关语言文件添加到工程宏。为支持简体中文文件名需要使用“936”, 正如在图 添加FatFS文件到工程 的操作,我们已经把cc936.c文件添加到工程中。

3) _USE_LFN: 长文件名支持,默认不支持长文件名,这里配置为2,支持长文件名,并指定使用栈空间为缓冲区。

4) _VOLUMES: 指定物理设备数量,这里设置为2,包括预留SD卡和SPI Flash芯片。

5) _MIN_SS 、_MAX_SS: 指定扇区大小的最小值和最大值。SD卡扇区大小一般都为512字节,SPI Flash芯片扇区大小一般设置为4096字节,所以需要把_MAX_SS改为4096。

25.3.6. FatFs功能测试

移植操作到此,就已经把FatFs全部添加到我们的工程了,这时我们编译功能,顺利编译通过,没有错误。接下来, 我们就可以使用编写图 FatFs程序结构图 中用户应用程序了。

主要的测试包括格式化测试、文件写入测试和文件读取测试三个部分,主要程序都在main.c文件中实现。

变量定义

代码清单:文件系统-9 变量定义
1
2
3
4
5
6
7
FATFS fs;                         /* FatFs文件系统对象 */
FIL fnew;                         /* 文件对象 */
FRESULT res_flash;                /* 文件操作结果 */
UINT fnum;                        /* 文件成功读写数量 */
BYTE buffer[1024]= {0};           /* 读缓冲区 */
BYTE textFileBuffer[] =           /* 写缓冲区*/
    "欢迎使用野火STM32开发板 今天是个好日子,新建文件系统测试文件\r\n";

FATFS是在ff.h文件定义的一个结构体类型,针对的对象是物理设备,包含了物理设备的物理编号、扇区大小等等信息, 一般我们都需要为每个物理设备定义一个FATFS变量。

FIL也是在ff.h文件定义的一个结构体类型,针对的对象是文件系统内具体的文件,包含了文件很多基本属性,比如文件大小、 路径、当前读写地址等等。如果需要在同一时间打开多个文件进行读写,才需要定义多个FIL变量,不然一般定义一个FIL变量即可。

FRESULT是也在ff.h文件定义的一个枚举类型,作为FatFs函数的返回值类型,主要管理FatFs运行中出现的错误。总共有19种错误类型, 包括物理设备读写错误、找不到文件、没有挂载工作空间等等错误。这在实际编程中非常重要,当有错误出现时我们要停止文件读写, 通过返回值我们可以快速定位到错误发生的可能地点。如果运行没有错误才返回FR_OK。

fnum是个32位无符号整形变量,用来记录实际读取或者写入数据的数组。

buffer和textFileBuffer分别对应读取和写入数据缓存区,都是8位无符号整形数组。

主函数

代码清单:文件系统-10 主函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
int main(void)
{
    /* 设定系统时钟为72MHz */
    SystemClock_Config();

    LED_GPIO_Config();

    /* 配置串口1为:115200 8-N-1 */
    DEBUG_USART_Config();

    printf("\r\n这是一个8M串行flash(W25Q64)实验(SPI驱动)\r\n");

    /* 8M串行flash W25Q64初始化 */
    SPI_FLASH_Init();
    /* 获取 Flash Device ID */
    DeviceID = SPI_FLASH_ReadDeviceID();

    Delay( 200 );

    /* 获取 SPI Flash ID */
    FlashID = SPI_FLASH_ReadID();

    printf("\r\nFlashID is 0x%X,  Manufacturer Device ID is 0x%X\r\n",
        FlashID, DeviceID);

    /* 检验 SPI Flash ID */
    if (FlashID == sFLASH_ID) {
        printf("\r\n检测到SPI FLASH W25Q64 !\r\n");

        /* 擦除将要写入的 SPI FLASH扇区,FLASH写入前要先擦除 */
        SPI_FLASH_SectorErase(FLASH_SectorToErase);

        /* 将发送缓冲区的数据写到flash中 */
        SPI_FLASH_BufferWrite(Tx_Buffer, FLASH_WriteAddress,BufferSize);
        printf("\r\n写入的数据为:\r\n%s", Tx_Buffer);

        /* 将刚刚写入的数据读出来放到接收缓冲区中*/
        SPI_FLASH_BufferRead(Rx_Buffer, FLASH_ReadAddress, BufferSize);
        printf("\r\n读出的数据为:\r\n%s", Rx_Buffer);

        /* 检查写入的数据与读出的数据是否相等 */
        TransferStatus1 = Buffercmp(Tx_Buffer, Rx_Buffer, BufferSize);

        if ( PASSED == TransferStatus1 ) {
            LED2_ON;
            printf("\r\n8M串行flash(W25Q64)测试成功!\n\r");
        } else {
            LED1_ON;
            printf("\r\n8M串行flash(W25Q64)测试失败!\n\r");
        }
    }// if (FlashID == sFLASH_ID)
    else {
        LED1_ON;
        printf("\r\n获取不到 W25Q64 ID!\n\r");
    }

    SPI_Flash_PowerDown();
    while (1);
}

首先,初始化LED灯和调试串口,用来指示程序进程。

FatFs的第一步工作就是使用f_mount函数挂载工作区。f_mount函数有三个形参,第一个参数是指向FATFS变量指针,如果赋值为NULL可以取消物理设备挂载。 第二个参数为逻辑设备编号,使用设备根路径表示,与物理设备编号挂钩,在 代码清单:文件系统-1 中我们定义SPI Flash芯片物理编号为1,所以这里使用“1:”。 第三个参数可选0或1,1表示立即挂载,0表示不立即挂载,延迟挂载。 f_mount函数会返回一个FRESULT类型值,指示运行情况。

如果f_mount函数返回值为FR_NO_FILESYSTEM,说明没有FAT文件系统,比如新出厂的SPI Flash芯片就没有FAT文件系统。 我们就必须对物理设备进行格式化处理。使用f_mkfs函数可以实现格式化操作。f_mkfs函数有三个形参, 第一个参数为逻辑设备编号;第二参数可选0或者1,0表示设备为一般硬盘,1表示设备为软盘。第三个参数指定扇区大小,如果为0, 表示通过 代码清单:文件系统-6 中disk_ioctl函数获取。格式化成功后需要先取消挂载原来设备,再重新挂载设备。

在设备正常挂载后,就可以进行文件读写操作了。使用文件之前,必须使用f_open函数打开文件,不再使用文件必须使用f_close函数关闭文件, 这个跟电脑端操作文件步骤类似。f_open函数有三个形参,第一个参数为文件对象指针。第二参数为目标文件,包含绝对路径的文件名称和后缀名。 第三个参数为访问文件模式选择,可以是打开已经存在的文件模式、读模式、写模式、新建模式、总是新建模式等的或运行结果。比如对于写测试, 使用FA_CREATE_ALWAYS和FA_WRITE组合模式,就是总是新建文件并进行写模式。

f_close函数用于不再对文件进行读写操作关闭文件,f_close函数只要一个形参,为文件对象指针。f_close函数运行可以确保缓冲区完全写入到文件内。

成功打开文件之后就可以使用f_write函数和f_read函数对文件进行写操作和读操作。这两个函数用到的参数是一致的,只不过一个是数据写入, 一个是数据读取。f_write函数第一个形参为文件对象指针,使用与f_open函数一致即可。第二个参数为待写入数据的首地址, 对于f_read函数就是用来存放读出数据的首地址。第三个参数为写入数据的字节数,对于f_read函数就是欲读取数据的字节数。 第四个参数为32位无符号整形指针,这里使用fnum变量地址赋值给它,在运行读写操作函数后,fnum变量指示成功读取或者写入的字节个数。

最后,不再使用文件系统时,使用f_mount函数取消挂载。

25.3.7. 下载验证

保证开发板相关硬件连接正确,用USB线连接开发板“USB TO UART”接口跟电脑,在电脑端打开串口调试助手,把编译好的程序下载到开发板。 程序开始运行后,LED熄灭,在串口调试助手可看到格式化测试、写文件检测和读文件检测三个过程;最后如果所有读写操作都正常,LED2灯点亮,如果在运行中FatFs出现错误LED1灯点亮。

虽然我们通过LED灯指示和串口调试助手信息打印方法来说明FatFs移植成功,并顺利通过测试,但心底总是很踏实, 所谓眼见为实,虽然我们创建了“FatFs读写测试文件.txt”这个文件,却完全看不到实体。这个确实是个问题, 因为我们这里使用SPI Flash芯片作为物理设备,并不像SD卡那么方便直接用读卡器就可以在电脑端打开验证。 另外一个问题,就目前来说,在SPI Flash芯片上挂载FatFs好像没有实际意义,无法发挥文件系统功能。

实际上,这里归根到底就是我们目前没办法在电脑端查看SPI Flash芯片内FatFs的内容,没办法非常方便拷贝、删除文件。我们当然不会做无用功, STM32控制器还有一个硬件资源可以解决上面的问题,就是USB!我们可以通过编程把整个开发板变成一个U盘,而U盘存储空间就是SPI Flash芯片的空间。 这样非常方便实现文件读写。至于USB内容将在USB相关章节讲解。

25.4. FatFs功能使用实验

上个实验我们实现了FatFs的格式化、读文件和写文件功能,这个已经满足很多部分的运用需要。有时,我们需要更多的文件操作功能, FatFs还是提供了不少的功能的,比如设备存储空间信息获取、读写文件指针定位、创建目录、文件移动和重命名、文件或目录信息获取等等功能。 我们接下来这个实验内容就是展示FatFs众多功能,提供一个很好了范例,以后有用到相关内容,参考使用非常方便。

25.4.1. 硬件设计

本实验主要使用FatFs软件功能,不需要其他硬件模块,使用与FatFs移植实验相同硬件配置即可。

25.4.2. 软件设计

上个实验我们已经移植好了FatFs,这个例程主要是应用,所以简单起见,直接拷贝上个实验的工程文件,保持FatFs底层驱动程序, 我们只改main.c文件内容,实现应用程序。

主函数

代码清单:文件系统-11 主函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
int main(void)
{
    /* 设定系统时钟为72MHz */
    SystemClock_Config();

    LED_GPIO_Config();

    /* 配置串口1为:115200 8-N-1 */
    DEBUG_USART_Config();

    printf("******这是一个基于串行FLASH的FatFS文件系统实验******\r\n");

    /* 注册一个FatFS设备:串行FLASH */
    if (FATFS_LinkDriver(&SPIFLASH_Driver, SPIFLASHPath) == 0) {
        //在串行FLASH挂载文件系统,文件系统挂载时会将串行FLASH初始化
        f_res = f_mount(&fs,(TCHAR const*)SPIFLASHPath,1);
        printf_fatfs_error(f_res);
        /*----------------------- 格式化测试 --------------------*/
        /* 如果没有文件系统就格式化创建创建文件系统 */
        if (f_res == FR_NO_FILESYSTEM) {
            printf("》串行FLASH还没有文件系统,即将进行格式化...\r\n");
            /* 格式化 */
            f_res=f_mkfs((TCHAR const*)SPIFLASHPath,0,0);

            if (f_res == FR_OK) {
                printf("》串行FLASH已成功格式化文件系统。\r\n");
                /* 格式化后,先取消挂载 */
                f_res = f_mount(NULL,(TCHAR const*)SPIFLASHPath,1);
                /* 重新挂载 */
                f_res = f_mount(&fs,(TCHAR const*)SPIFLASHPath,1);
            } else {
                printf("《《格式化失败。》》\r\n");
                while (1);
            }
        } else if (f_res!=FR_OK) {
            printf("!!串行FLASH挂载文件系统失败。(%d)\r\n",f_res);
            printf_fatfs_error(f_res);
            while (1);
        } else {
            printf("》文件系统挂载成功,可以进行读写测试\r\n");
        }

        /*---------------文件系统测试:写测试 --------------*/
        /* 打开文件,如果文件不存在则创建它 */
        printf("****** 即将进行文件写入测试... ******\r\n");
        f_res = f_open(&file, "FatFs读写测试文件.txt",
                FA_CREATE_ALWAYS | FA_WRITE );
        if ( f_res == FR_OK ) {
            printf("》打开/创建FatFs读写测试文件.txt文件成功,向文件写入数据。\r\n");
            /* 将指定存储区内容写入到文件内 */
            f_res=f_write(&file,WriteBuffer,sizeof(WriteBuffer),&fnum);
            if (f_res==FR_OK) {
                printf("》文件写入成功,写入字节数据:%d\r\n",fnum);
                printf("》向文件写入的数据为:\r\n%s\r\n",WriteBuffer);
            } else {
                printf("!!文件写入失败:(%d)\r\n",f_res);
            }
            /* 不再读写,关闭文件 */
            f_close(&file);
        } else {
            printf("!!打开/创建文件失败。\r\n");
        }

        /*--------- 文件系统测试:读测试 ----------------*/
        printf("****** 即将进行文件读取测试... ******\r\n");
        f_res = f_open(&file, "FatFs读写测试文件.txt",
                FA_OPEN_EXISTING | FA_READ);
        if (f_res == FR_OK) {
            printf("》打开文件成功。\r\n");
            f_res = f_read(&file, ReadBuffer, sizeof(ReadBuffer),&fnum);
            if (f_res==FR_OK) {
                printf("》文件读取成功,读到字节数据:%d\r\n",fnum);
                printf("》读取得的文件数据为:\r\n%s \r\n",ReadBuffer);
            } else {
                printf("!!文件读取失败:(%d)\r\n",f_res);
            }
        } else {
            printf("!!打开文件失败。\r\n");
        }
        /* 不再读写,关闭文件 */
        f_close(&file);

        /* 不再使用,取消挂载 */
        f_res = f_mount(NULL,(TCHAR const*)SPIFLASHPath,1);
    }

    /* 注销一个FatFS设备:串行FLASH */
    FATFS_UnLinkDriver(SPIFLASHPath);

    /* 无限循环 */
    while (1) {
    }
}

串口在程序调试中经常使用,可以把变量值直观打印到串口调试助手,这个信息非常重要,同样在使用之前需要调用Debug_USART_Config函数完成调试串口初始化。

25.4.3. 下载验证

保证开发板相关硬件连接正确,用USB线连接开发板“USB TO UART”接口跟电脑,在电脑端打开串口调试助手, 把编译好的程序下载到开发板。程序开始运行,在串口调试助手可看到每个阶段测试相关信息情况。