25. 基于FLASH的FatFs文件系统移植

25.1. 文件系统

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

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

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

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

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

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

25.2. FatFs文件系统介绍

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

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

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

25.2.1. FatFs 特性

  • DOS / Windows兼容的FAT / exFAT文件系统。

  • 与平台无关,易于移植。

  • 程序代码和工作区的占用空间非常小。

  • 支持以下各种配置选项:

    • ANSI / OEM或Unicode中的长文件名。

    • exFAT文件系统,64位LBA和GPT可存储大量数据。

    • RTOS的线程。

    • 多个卷(物理驱动器和分区,最多10个卷)。

    • 可变扇区大小。

    • 多个代码页,包括DBCS。

    • 只读,可选API,I / O缓冲区等…

25.2.2. FatFs 层级结构

FatFs层级结构如下:

图

应用层调用FatFs模块接口,FatFs调用底层接口,实现对USB、SD卡等存储设备的操作。(需提供RTC时钟给FatFs模块。)

25.3. 源码下载

图

FatFs 和 Petit FatFs: Petit FatFs 是用于小型8位微控制器的 FatFs 模块的子集。它是按照ANSI C编写的,并且与磁盘I/O层完全分开。 即使RAM大小小于扇区大小,也可以将其合并到内存有限的微型微控制器中。 我们这里选择完整版的 FatFs。

版本选目前最新的2022年11月06日更新的版本,也就是 FatFs 0.15 版本。

25.3.1. 源码结构

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

FatFs 0.15 版本的源码结构如下:

FatFs文件目录

25.3.1.1. FatFs帮助文档

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

documents文件夹的文件目录

documents 这个文件夹下面存放的是 FatFs 模块文档:

  • 其中 doc 文件夹里面是编译好的html文档,讲的是FatFs里面各个函数的使用方法, 这些函数都是封装得非常好的函数,利用这些函数我们就可以操作串行Flash芯片。 有关具体的函数我们在用到的时候再讲解。

  • res文件夹包含doc文件夹下文件需要用到的图片,还有四个名为app.c文件,内容都是FatFs具体应用例程。

  • 00index_e.html 相当于FatFs的主页。

  • updates.txt 记录了各个版本的更新。

至于另外一个文件,可以不管。

25.3.1.2. FATFS源码

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

source文件夹的文件目录

source 这个文件夹下面存放的是 FatFs 源码:

  • diskio.c/.h: IO层的实现。

  • ff.c/.h: FatFs核心文件,文件管理的实现方法。该文件独立于底层介质操作文件的函数,利用这些函数实现文件的读写。

  • ffconf.h: 这个头文件包含了对FatFs功能配置的宏定义,通过修改这些宏定义就可以裁剪FatFs的功能。

  • ffsystem.c: 操作系统相关。

  • ffunicode.c: unicode编码相关。FF_USE_LFN != 0 时必须包含此文件。

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

  • 00readme.txt: 说明了当前目录下各个文件的功能。

25.3.2. 长文件名

FatFs模块支持FAT文件系统的长文件名(LFN)扩展名。 默认情况下,禁用LFN。 如果要启用LFN,要将 FF_USE_LFN 设置为1、2或3,并将 ffunicode.c 文件添加到项目中。 LFN功能需要一定的工作缓冲区。 缓冲区大小可由 FF_MAX_LFN 根据可用内存配置。 LFN的长度最多可以为255个字符,因此 FF_MAX_LFN 也应设置为255。

图

25.3.3. FatFs限制

  • 文件系统类型:FAT、FAT32(rev0.0) 和 exFAT(rev1.0)。

  • 打开的文件数量:无限制。(取决于可用内存)

  • 卷数:最多 10 个。

  • 扇区大小:512、1024、2048 和 4096 字节。

  • 最小卷大小:128 个扇区。

  • 最大卷大小:32 位 LBA 中的 2^32 - 1 扇区,在带有 exFAT 的 64 位 LBA 中几乎不受限制。

  • 最大文件大小:FAT 卷上为2^32 - 1 字节,exFAT 卷上几乎不受限制。

  • 群集大小:FAT 卷上最多 128 个扇区,exFAT 卷上最多 16 MB。

25.3.4. FatFs的已知问题

在网址:http://elm-chan.org/fsw/ff/patches.html 记录着有关 Fatfs 最新版本的已知问题以及问题的解决方法或补丁。

图

网址上会公布已发现并解决的BUG,以补丁形式发布。 以R0.14b版本为例,目前该版本有2个补丁。

有需要的话可以按照官方提供的方法对源码进行修改, 由于我们这里使用的都是一些比较基础的功能,这些功能是没有问题的,所以就不进行修改了。

25.4. FatFs文件系统移植实验

25.4.1. 硬件设计及FSP

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

25.4.2. FatFs 移植步骤概述

基本步骤:
  1. 实现底层驱动接口

  2. 修改配置文件

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

FatFs程序结构图

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

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

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

25.4.2.1. 实现底层驱动接口

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

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

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

FatFs移植需要用户支持函数

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

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

25.4.2.2. 修改配置文件

ffconf.h 文件是 FatFs 的配置文件。

下面是 ffconf.h 文件中,需要修改的部分,只把需要修改的部分放出来:

更加详细的部分参考:http://elm-chan.org/fsw/ff/doc/config.html#use_mkfs

代码清单 24‑1 FatFs 的配置文件:ffconf.h
/********************/
/* 下面是经过修改部分 */
/********************/

#define FF_USE_MKFS          1   //此选项切换是否启用 f_mkfs() 函数,用于格式化 Flash、SD卡等
/* This option switches f_mkfs() function. (0:Disable or 1:Enable) */


#define FF_CODE_PAGE 936     //此选项指定使用的OEM代码页
/* This option specifies the OEM code page to be used on the target system.
/  Incorrect code page setting can cause a file open failure.
/
/   437 - U.S.
/   720 - Arabic
/   737 - Greek
/   771 - KBL
/   775 - Baltic
/   850 - Latin 1
/   852 - Latin 2
/   855 - Cyrillic
/   857 - Turkish
/   860 - Portuguese
/   861 - Icelandic
/   862 - Hebrew
/   863 - Canadian French
/   864 - Arabic
/   865 - Nordic
/   866 - Russian
/   869 - Greek 2
/   932 - Japanese (DBCS)
/   936 - Simplified Chinese (DBCS)
/   949 - Korean (DBCS)
/   950 - Traditional Chinese (DBCS)
/     0 - Include all code pages above and configured by f_setcp()
*/


#define FF_USE_LFN           2       //此选项切换对长文件名的支持
#define FF_MAX_LFN           255     //设置长文件名的最长长度
/* The FF_USE_LFN switches the support for LFN (long file name).
/
/   0: Disable LFN. FF_MAX_LFN has no effect.
/   1: Enable LFN with static  working buffer on the BSS. Always NOT thread-safe.
/   2: Enable LFN with dynamic working buffer on the STACK.
/   3: Enable LFN with dynamic working buffer on the HEAP.


#define FF_LFN_UNICODE       2       //此选项设置是否启用 Unicode 字符编码
/* This option switches the character encoding on the API when LFN is enabled.
/
/   0: ANSI/OEM in current CP (TCHAR = char)
/   1: Unicode in UTF-16 (TCHAR = WCHAR)
/   2: Unicode in UTF-8 (TCHAR = char)
/   3: Unicode in UTF-32 (TCHAR = DWORD)
/
/  Also behavior of string I/O functions will be affected by this option.
/  When LFN is not enabled, this option has no effect. */


#define FF_VOLUMES           2       //要使用的卷(逻辑驱动器)的数量。范围(1-10)
/* Number of volumes (logical drives) to be used. (1-10) */


#define FF_MIN_SS            512
#define FF_MAX_SS            4096    //这组选项配置支持的扇区大小范围
/* This set of options configures the range of sector size to be supported. (512,
/  1024, 2048 or 4096) Always set both 512 for most systems, generic memory card and
/  harddisk, but a larger value may be required for on-board flash memory and some
/  type of optical media. When FF_MAX_SS is larger than FF_MIN_SS, FatFs is configured
/  for variable sector size mode and disk_ioctl() function needs to implement
/  GET_SECTOR_SIZE command. */
/* 若 FF_MIN_SS != FF_MAX_SS。则需要在 disk_ioctl() 中指定所需操作的设备的扇区大小
 * 在 case GET_SECTOR_SIZE 项中指定 */

#define FF_FS_NORTC          1   //设置为1关闭时间戳  启用时间戳功能需要RTC
#define FF_NORTC_MON 1
#define FF_NORTC_MDAY        1
#define FF_NORTC_YEAR        2022
/* The option FF_FS_NORTC switches timestamp feature. If the system does not have
/  an RTC or valid timestamp is not needed, set FF_FS_NORTC = 1 to disable the
/  timestamp feature. Every object modified by FatFs will have a fixed timestamp
/  defined by FF_NORTC_MON, FF_NORTC_MDAY and FF_NORTC_YEAR in local time.
/  To enable timestamp function (FF_FS_NORTC = 0), get_fattime() function need to be
/  added to the project to read current time form real-time clock. FF_NORTC_MON,
/  FF_NORTC_MDAY and FF_NORTC_YEAR have no effect.
/  These options have no effect in read-only configuration (FF_FS_READONLY = 1). */

25.4.3. 路径名格式说明

在 FatFs 的 API 函数中,经常能看到 “const TCHAR* path” 这样的函数参数,比如:

FRESULT f_mount (
    FATFS* fs,                      /* 指向要注册的文件系统对象的指针(NULL:卸载) */
    const TCHAR* path,      /* 要装载/卸载的逻辑驱动器号 */
    BYTE opt                        /* 装载选项:0=不装载(延迟装载),1=立即装载 */
)

FRESULT f_open (
    FIL* fp,                        /* 指向空白文件对象的指针 */
    const TCHAR* path,      /* 指向文件名的指针 */
    BYTE mode                       /* 访问模式和打开模式标志 */
)

这就要求我们要知道 path 这个参数的可输入值是什么,也就是我们要了解FatFs的文件命名规范。

25.4.3.1. 文件命名格式

FatFs模块上的路径名格式与DOS/Windows的文件名规范类似,如下所示:

[drive#:][/]"directory 目录"/"file 文件"

FatFs 模块支持长文件名(LFN) 和 8.3 格式的文件名 (SFN)。长文件名(LFN) 可在 FF_USE_LFN >= 1 时使用。 子目录用 \ 或 / 分隔,方式与DOS/Windows API相同。 重复的分隔符和终止分隔符将被忽略,例如:“//animal///cat/”。 唯一不同的是,FatFs中指定逻辑驱动器(FAT卷)的标题驱动器前缀是数字(0-9)+冒号,而在DOS/Windows中是字母(A-Z)+冒号。 逻辑驱动器编号是指定要访问的卷的标识符。 如果省略驱动器前缀,则假定逻辑驱动器编号为默认驱动器。

注解

LFN 和 SFN

  • 8.3 命名规则,又称短文件名(Short File Name,SFN)是一种限制文件名长度的方法,这在DOS和Windows 95及Windows NT 3.51以前的Microsoft Windows版本中,在FAT文件系统中的常用方法。

  • 长文件名,(Long file name,LFN)也指长文件名支持。在旧版本的DOS操作系统下,因为文件名称有8.3格式的限制,凡文件主档名超过8字节或扩展名超过3字节的文件名,都被称为“长文件名”,在Windows下正常的文件名置换于DOS(或“命令提示字符”)环境下则可能无法完整显示,如“Program files”资料夹可能会显示成其对应的8.3文件名“PROGRA~1”。

25.4.3.2. FF_FS_RPATH

在默认配置 (FF_FS_RPATH==0) 中, 是没有当前目录的概念的。 卷上的每个对象始终以从根目录开始的完整路径名指定。 不可以使用点目录名称(“.”,“..”)。标题分隔符被忽略, 它可以存在或省略。默认驱动器固定为驱动器0。

启用相对路径功能时(FF_FS_RPATH>=1),如果存在分隔符, 则从根目录跟随指定的路径。如果没有分隔符,则从默认驱动器的当前目录开始。 路径名也允许使用点目录名。 当前目录由f_chdir函数设置,默认驱动器为f_chdrive函数设置的当前驱动器。

图

此外,驱动器前缀可以采用预定义的任意字符串。 当选项FF_STR_VOLUME_ID == 1 时,也可以将任意字符串卷 ID 用作驱动器前缀。 例如 “flash:file1.txt”、“ram:temp.dat” 或 “sd:” 。 如果 srting 与任何卷 ID 都不匹配,则该函数将失败并返回 FR_INVALID_DRIVE。

当FF_STR_VOLUME_ID == 2 时,可以使用 Unix 样式的驱动器前缀。 例如 “/flash/file1.txt”、“/ram/temp.dat” 或 “/sd” 。

FF_STR_VOLUME_ID 和 FF_VOLUME_STRS 宏
#define FF_STR_VOLUME_ID    0  //FF_STR_VOLUME_ID开关支持任意字符串形式的卷ID。
#define FF_VOLUME_STRS              "RAM","NAND","CF","SD","SD2","USB","USB2","USB3"
/* FF_STR_VOLUME_ID switches support for volume ID in arbitrary strings.
/  When FF_STR_VOLUME_ID is set to 1 or 2, arbitrary strings can be used as drive
/  number in the path name. FF_VOLUME_STRS defines the volume ID strings for each
/  logical drives. Number of items must not be less than FF_VOLUMES. Valid
/  characters for the volume ID strings are A-Z, a-z and 0-9, however, they are
/  compared in case-insensitive. If FF_STR_VOLUME_ID >= 1 and FF_VOLUME_STRS is
/  not defined, a user defined volume string table needs to be defined as:
/
/  const char* VolumeStr[FF_VOLUMES] = {"ram","flash","sd","usb",...
*/

FF_STR_VOLUME_ID: 此选项切换对字符串卷 ID 的支持。

FF_STR_VOLUME_ID 为不同数值时,对应的可选项

数值

描述

示例

0

只能使用数字 ID 中的 DOS/Windows 样式驱动器前缀。

1:/filename

1

还可以使用字符串 ID 中的 DOS/Windows 样式驱动器前缀。

flash:/filename

2

也可以使用字符串ID中的Unix样式驱动器前缀。

/flash/filename

FF_VOLUME_STRS: 此选项定义每个逻辑驱动器的卷 ID 字符串。项目数量不得少于FF_VOLUMES。 卷 ID 字符串的有效字符是 A-Z、a-z 和 0-9,不区分大小写。 如果FF_STR_VOLUME_ID == 0,则此选项不起作用。 如果 FF_STR_VOLUME_ID >= 1 并且未定义此选项, 则需要定义用户定义的卷字符串表,如下所示。不应动态修改该表。

用户定义卷ID字符串
/* 0: ~ 3: 的用户定义卷ID字符串: */
const char* VolumeStr[FF_VOLUMES] = {"ram","flash","sd","usb"};

这里我们并没有用到任意字符串形式的卷ID,就没有开启 FF_STR_VOLUME_ID 宏。

25.4.4. 接口函数

主要的接口文件都在 diskio.c 文件中了,我们只需要根据函数所提供的输入参数和返回参数, 来实现函数的功能,提供给FatFs调用就好了。

FatFs文件系统与存储设备的连接函数在diskio.c文件中,主要有5个函数需要我们编写的。

宏定义和存储设备状态获取函数

代码清单: 24_2 disk_ioctl 函数
/*-----------------------------------------------------------------------*/
/* Get Drive Status                                                      */
/*-----------------------------------------------------------------------*/

DSTATUS disk_status (
   BYTE pdrv         /* Physical drive nmuber to identify the drive */
)
{
   DSTATUS stat;

   switch (pdrv) {
   case DEV_FLASH :
      QSPI_Flash_WaitForWriteEnd(); //等待Flash芯片内部操作完成

      stat = RES_OK;
      return stat;

   }
   return STA_NOINIT;
}

存储设备初始化函数

代码清单: 24_3 disk_ioctl 函数
/*-----------------------------------------------------------------------*/
/* Inidialize a Drive                                                    */
/*-----------------------------------------------------------------------*/

DSTATUS disk_initialize (
   BYTE pdrv                         /* Physical drive nmuber to identify the drive */
)
{
   DSTATUS stat;

   switch (pdrv) {
   case DEV_FLASH :
      QSPI_Flash_Init();

      stat = RES_OK;
      return stat;

   }
   return STA_NOINIT;
}

存储设备数据读取函数

代码清单: 24_4 disk_ioctl 函数
/*-----------------------------------------------------------------------*/
/* Read Sector(s)                                                        */
/*-----------------------------------------------------------------------*/

DRESULT disk_read (
   BYTE pdrv,                /* Physical drive nmuber to identify the drive */
   BYTE *buff,               /* Data buffer to store read data */
   LBA_t sector,     /* Start sector in LBA */
   UINT count                /* Number of sectors to read */
)
{
   DRESULT res;

   switch (pdrv) {
   case DEV_FLASH :
      // translate the arguments here

      QSPI_Flash_BufferRead(buff, sector<<12, count<<12); //1 sector == 4096 bytes

      res = RES_OK;
      return res;

   }

   return RES_PARERR;
}

存储设备数据写入函数

代码清单: 24_5 disk_ioctl 函数
DRESULT disk_write (
   BYTE pdrv,                        /* Physical drive nmuber to identify the drive */
   const BYTE *buff, /* Data to be written */
   LBA_t sector,             /* Start sector in LBA */
   UINT count                        /* Number of sectors to write */
)
{
   DRESULT res;
   uint32_t write_addr;

   switch (pdrv) {
   case DEV_FLASH :
      write_addr = sector << 12;
      QSPI_Flash_SectorErase(write_addr);
      QSPI_Flash_BufferWrite(buff, write_addr, count<<12);

      res = RES_OK;
      return res;

   }

   return RES_PARERR;
}

IO控制函数

代码清单: 24_6 disk_ioctl 函数
DRESULT disk_ioctl (
   BYTE pdrv,                /* Physical drive nmuber (0..) */
   BYTE cmd,         /* Control code */
   void *buff                /* Buffer to send/receive control data */
)
{
   DRESULT res;

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

      res = RES_OK;
      return res;

   }

   return RES_PARERR;
}

get_fattime函数

代码清单: 24_7 disk_ioctl 函数
DWORD get_fattime(void) {
   /* 返回当前时间戳 */

   return (DWORD)(2022 - 80) << 25 |  /* Year */
         (DWORD)(1 + 1) << 21 |      /* Month */
         (DWORD)1 << 16 |            /* Mday */
         (DWORD)1 << 11 |            /* Hour */
         (DWORD)1 << 5 |             /* Min */
         (DWORD)1 >> 1;              /* Sec */
}

由于之前在配置文件关闭了 RTC 和时间戳功能,因此其实不需要实现这个返回当前时间戳函数。

由 FF_FS_NORTC 决定是否启用,FF_FS_NORTC设置为1以禁用时间戳功能。

25.4.5. FatFs 基本 API 函数说明

更多关于 FatFs 接口函数的说明请看官方的说明:http://elm-chan.org/fsw/ff/00index_e.html

25.4.5.1. f_mount

f_mount 函数原型如下:

FRESULT f_mount (
FATFS*       fs,    /* [IN] Filesystem object */
const TCHAR* path,  /* [IN] Logical drive number */
BYTE         opt    /* [IN] Initialization option */
);

FatFs 需要每个逻辑驱动器(FAT 卷)的工作区域(文件系统对象)。 在执行任何文件/目录操作之前,需要使用逻辑驱动器的f_mount函数注册文件系统对象。 完成此过程后,文件/目录 API 函数已准备好工作。 某些卷管理功能(f_mkfs、f_fdisk和f_setcp)不需要文件系统对象。

f_mount函数将文件系统对象注册/注销到 FatFs 模块,如下所示:

  1. 确定由 path 指定的逻辑驱动器。

  2. 清除并注销卷的已注册工作区(如果存在)。

  3. 如果 fs 不为 NULL,则清除新工作区并将其注册到卷中。

  4. 如果指定了强制装入,则对卷执行卷装入过程。

opt:安装选项。0:现在不挂载(要在第一次访问卷时挂载),1:强制挂载卷以检查它是否准备好工作。

25.4.5.2. f_mkfs

f_mkfs函数原型如下:

FRESULT f_mkfs (
const TCHAR* path,   /* [IN] Logical drive number */
const MKFS_PARM* opt,/* [IN] Format options */
void* work,          /* [-]  Working buffer */
UINT len             /* [IN] Size of working buffer */
);

簇(cluster): 由于扇区的空间比较小且数目众多,在寻址时比较困难, 所以操作系统就将多个的扇区组合在一起,形成一个更大的单位, 再对这个单位进行整体的操作。也可以理解为文件的磁盘空间分配单位。 当簇的大小为 32768 字节时,大小为 100 字节的文件将占用 32768 字节的磁盘空间。 随着簇大小的增加,磁盘使用的空间效率会变得很低,但与此同时,读/写效率也会提高。 因此,簇的大小是空间效率和读/写效率之间的权衡。

opt:若为NULL,则启用默认参数

f_mkfs opt 默认参数
static const MKFS_PARM defopt = {FM_ANY, 0, 0, 0, 0};        /* Default parameter */

work 来指向用于格式化过程的工作缓冲区的指针。

len 来确定工作缓冲区的大小(以字节为单位)。至少需要FF_MAX_SS。

提示

当 FF_FS_READONLY == 0 且 FF_USE_MKFS == 1 时可用。

25.4.5.3. f_setlabel

f_setlabel 函数原型如下:

FRESULT f_setlabel (
const TCHAR* label  /* [IN] Volume label to be set */
);

当字符串具有驱动器前缀时,卷标将设置为驱动器前缀指定的卷。 Unix 样式的卷 ID 不能用于指定卷。 如果未指定驱动器号,则卷标将设置为默认驱动器。 如果给定卷标的长度为零,则将删除卷上的卷标。卷标的格式如下所示:

在 FAT 卷上转换 OEM 代码页时最多 11 个字节。 在 exFAT 卷中最多 11 个字符。 FAT 卷允许的字符数为:SFN 允许的字符不包括点。低写字符向上转换。 exFAT 卷允许的字符为:LFN 允许的字符包括点。将保留小写字符。 空格可以嵌入卷标中的任何位置。尾随空格在 FAT 音量处被截断。

提示

当FF_FS_READONLY == 0 且FF_USE_LABEL == 1 时可用。

25.4.5.4. f_open

f_open 函数原型如下:

FRESULT f_open (
FIL* fp,           /* [OUT] Pointer to the file object structure */
const TCHAR* path, /* [IN] File name */
BYTE mode          /* [IN] Mode flags */
);

打开一个文件并创建一个文件对象。 完成对文件的访问后应该使用f_close函数将其关闭。 如果在关机、移出介质或重新装入之前对文件进行了任何更改且未调用f_close函数将其关闭,则文件可能会 collapsed (损坏)。

可选 mode 选项:

图

提示

当FF_FS_READONLY == 1 时,只有FA_READ和FA_OPEN_EXISTING可用于模式标志。

25.4.5.5. f_close

f_close 函数原型如下:

FRESULT f_close (
FIL* fp     /* [IN] Pointer to the file object */
);

关闭打开的文件对象。如果文件已更改,则文件的缓存信息将写回卷。 函数成功后,文件对象不再有效,可以丢弃。

请注意,如果文件对象处于只读模式且未启用FF_FS_LOCK, 则也可以在不执行f_close函数的情况下丢弃该文件对象。 但是,不建议这样做。

25.4.5.6. f_write

f_write 函数原型如下:

FRESULT f_write (
FIL* fp,          /* [IN] Pointer to the file object structure */
const void* buff, /* [IN] Pointer to the data to be written */
UINT btw,         /* [IN] Number of bytes to write */
UINT* bw          /* [OUT] Pointer to the variable to return number of bytes written */
);

该函数开始在读/写指针指向的文件偏移处将数据写入文件。 读/写指针随着写入的字节数而前进。 功能成功后,应检查 *bw 以检测磁盘已满。 如果 *bw < btw,则表示卷在写入操作期间已满。 该函数在卷已满或接近满时可能需要一段时间。

提示

FF_FS_READONLY == 0 时可用。

25.4.5.7. f_read

f_read 函数原型如下:

FRESULT f_read (
FIL* fp,     /* [IN] File object */
void* buff,  /* [OUT] Buffer to store read data */
UINT btr,    /* [IN] Number of bytes to read */
UINT* br     /* [OUT] Number of bytes read */
);

该函数开始在读/写指针指向的文件偏移处从文件中读取数据。 读/写指针随着读取的字节数而前进。 函数成功后,应检查 *br 以检测文件的末尾。 如果 *br < btr,则表示在读取操作期间读/写指针命中文件。

25.4.6. 软件设计

与FatFs文件系统使用相关的变量定义如下:

代码清单: 24_8 hal_entry.c:全局变量
FATFS fs;           /* FatFs文件系统对象 */
FIL fnew;           /* 文件对象 */
UINT fnum;          /* 文件成功读写数量 */
FRESULT res_flash;  /* 文件操作结果 */
BYTE  fileReadBuffer[1024]; /* 读缓冲区 */
BYTE  fileWriteBuffer[] =   /* 写缓冲区 */
      "感谢您选用野火启明瑞萨RA开发板 [FatFs读写测试文件.txt]";

BYTE work[FF_MAX_SS]; /* Work area (larger is better for processing time) */

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

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

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

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

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

hal_entry 入口函数

代码清单: 24_9 hal_entry 入口函数
/* 用户头文件包含 */
#include "led/bsp_led.h"
#include "debug_uart/bsp_debug_uart.h"

//FatFs
#include "FatFs/ff15/ff.h"


FATFS fs;           /* FatFs文件系统对象 */
FIL fnew;           /* 文件对象 */
UINT fnum;          /* 文件成功读写数量 */
FRESULT res_flash;  /* 文件操作结果 */
BYTE  fileReadBuffer[1024]; /* 读缓冲区 */
BYTE  fileWriteBuffer[] =   /* 写缓冲区 */
      "感谢您选用野火启明瑞萨RA开发板 [FatFs读写测试文件.txt]";

BYTE work[FF_MAX_SS]; /* Work area (larger is better for processing time) */


void hal_entry(void)
{
   /* TODO: add your own code here */

   LED_Init();         // LED 初始化
   Debug_UART4_Init(); // SCI4 UART 调试串口初始化

   printf("这是一个串行FLASH的FatFs使用演示例程\r\n");
   printf("打开串口助手查看打印的信息\r\n\r\n");

   /* 尝试挂载外部FLASH FAT文件系统 */
   res_flash = f_mount(&fs, "0:", 1);
   if (res_flash == FR_NO_FILESYSTEM)
   {
      printf(">>> FLASH还没有文件系统,即将进行格式化...\r\n");
      /* 格式化 */
      res_flash = f_mkfs("0:", NULL, work, sizeof(work));
      if (res_flash == FR_OK)
      {
            printf(">>> FLASH已成功格式化文件系统。\r\n");
            /* 格式化后,先取消挂载 */
            res_flash = f_mount(NULL,"0:",1);
            /* 重新挂载 */
            res_flash = f_mount(&fs,"0:",1);
      }
      else
      {
            printf(">>> 格式化失败!!\r\n");
            while (1);
      }
   }
   else if (res_flash == FR_OK)
   {
      printf(">>> 文件系统挂载成功,可以进行读写测试。\r\n");
   }
   else
   {
      printf("!!外部Flash挂载文件系统失败。(%d)\r\n", res_flash);
      printf("!!可能原因:Flash初始化不成功。\r\n");
      while (1);
   }


   /*----------------------- 文件系统测试:写测试 -----------------------------*/
   printf("\r\n****** 即将进行文件写入测试 ******\r\n");

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


   /*----------------------- 文件系统测试:读测试 -----------------------------*/
   printf("****** 即将进行文件读取测试 ******\r\n");

   /* 打开文件,该文件前面已创建 */
   res_flash = f_open(&fnew, "0:FatFs读写测试文件.txt", FA_OPEN_EXISTING | FA_READ);
   if (res_flash == FR_OK)
   {
      printf(">>> 打开文件成功。\r\n");
      res_flash = f_read(&fnew, fileReadBuffer, sizeof(fileReadBuffer), &fnum);
      if (res_flash==FR_OK) {
            printf(">>> 文件读取成功,读到字节数据:%d\r\n", fnum);
            printf(">>> 读取得的文件数据为:%s\r\n", fileReadBuffer);
      } else {
            printf("!!文件读取失败:(%d)\n",res_flash);
      }
      /* 不再读写,关闭文件 */
      f_close(&fnew);
   }
   else
   {
      printf("!!打开文件失败。\r\n");
   }

   /* 不再使用文件系统,取消挂载文件系统 */
   f_mount(NULL,"0:",1);

   printf("****** 测试结束 ******\r\n");
   while(1);


#if BSP_TZ_SECURE_BUILD
   /* Enter non-secure code */
   R_BSP_NonSecureEnter();
#endif
}

程序的开头首先初始化调试串口,用来打印程序运行的一些调试信息。 此处的代码是以启明6M5板子的为例,另外两块板子的串口初始化函数名可能不同,读者需要注意。

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

如果 f_mount 函数返回值为 FR_NO_FILESYSTEM,说明外部FLASH存储器还没有FAT文件系统。 我们就必须对外部FLASH存储器进行格式化处理,将里面的存储空间以FAT文件系统的格式进行格式化。 使用 f_mkfs 函数可以实现格式化操作。 f_mkfs 函数有三个形参,第一个参数为逻辑设备编号; 第二个参数为格式化选项,选择格式化的类型,比如:FAT16/FAT32/exFAT等,NULL表示使用默认选项进行格式化; 第三个参数指定工作缓存区;第四个参数提供工作缓存区的大小。 格式化成功后需要先取消挂载原来设备,再重新挂载设备。

在设备正常挂载后,就可以进行文件读写操作了。使用文件之前,必须使用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变量指示成功读取或者写入的字节个数。之后 我们通过相应函数的返回值赋值到res_flash,后进行判断当前程序是否运行成功我们点亮绿灯和蓝灯,如果其中有一个失败我们点亮红灯。

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

25.4.7. 实验验证

用USB线连接开发板“USB TO UART”接口跟电脑,在电脑端打开串口调试助手, 把编译好的程序下载到开发板。在串口调试助手可看到FatFs测试的调试信息。

图