目录
FatFS(FAT File System)是一个适用于嵌入式系统的文件系统管理工具,可用于管理SD卡、U盘、Flash存储器等存储介质上的文件。FatFS已经作为中间件集成到了STM32 MCU固件库中,在CubeMX中,可启用FatFS管理SD卡、U盘等存储介质,生成的代码自动完成了针对具体存储介质的FatFS移植,使用起来非常方便。
一、FatFS概述
1、FatFS的作用
文件系统是管理存储介质上的文件的软件系统,例如,Windows系统使用的文件系统是FAT32或NTFS。在Windows、Linux等平台上,用C++等高级语言编写程序进行文件读写操作时,只需通过文件名和文件操作函数进行文件读写,而不用关心文件是如何写入存储介质或如何从存储介质读取的,也不用关心文件存储在哪个扇区等底层的问题。
在没有文件系统的嵌入式系统中,用户读写数据时就需要进行底层的操作。例如,在一个SPI接口的Flash(以下简称为SPI-Flash)存储芯片W25Q128中,它有16M字节存储空间,分为256个块、4096个扇区和65536个页。通过SPI接口的HAL驱动程序和W25Q128的指令读写数据时,用户必须指定起始绝对地址和读写数据的长度。
在存储介质容量不太大,数据存储结构比较简单时,用户还可以通过这种底层操作进行数据管理,但是当存储容量很大,存储的数据结构比较复杂时,这种底层操作的管理难度就非常大了。例如,在16GB的SD卡上读写数据时,用户就难以直接通过扇区地址来管理数据了,而应该在嵌入式系统中引入类似于计算机上的文件系统,以文件为对象管理存储介质上的数据。
FatFS是一个用于嵌入式系统的文件管理系统,它可以在SD卡、U盘、Flash芯片等存储介质上创建FAT或exFAT文件系统,它提供应用层接口函数,用户可以通过它直接对文件进行数据读写操作和管理。FatFS是用ANSI C语言编写的,文件操作的软件模块与底层硬件访问层完全分离,能方便地移植到各种处理器平台和存储介质。使用FatFS的嵌入式系统软硬件结构如图所示,其中各个部分的作用如下。
(1)用户应用程序
用户应用程序通过FatFS的通用接口API函数进行文件系统的操作,例如,用函数f_open()打开一个文件,用函数f_write()向文件写入数据,用函数f_close()关闭一个文件。这些操作与在计算机上用C语言编程读写文件类似,只需知道文件名,而这个文件在SD卡或Flash存储芯片上如何存储,如何写入和读取,则是FatFS底层的事情。
(2)FatFS通用程序
这些是与硬件无关的用于文件系统管理的一些通用操作API函数,包括文件系统的创建和挂载、目录操作和文件操作等函数。这些通用函数是面向应用程序的接口,实现这些函数的功能需要调用底层的硬件访问操作,所以这些通用函数是用户应用程序与底层硬件之间的桥梁。
(3)FatFS底层设备控制
这部分是FatFS与底层存储设备通信的一些功能函数,包括读写存储介质的函数disk_read()和disk_write()、获取时间戳的函数get_fattime()等。例如,一个系统使用了SPI接口的Flash芯片存储文件,当应用程序调用f_write()向一个文件写入数据时,到了设备控制层,就是执行disk_write(),通过SPI接口向Flash芯片写入数据。FatFS底层设备控制部分是与硬件密切相关的,FatFS的移植主要就是实现这部分的几个函数。
(4)存储介质和RTC
存储介质就是用于存储文件的各种硬件设备,如Flash存储芯片、SD卡、U盘、扩展的SRAM等。在一个嵌入式设备上,用户可以使用FatFS管理多个存储设备,例如,可以同时使用SPI-Flash存储芯片和SD卡。RTC用于获取当前时间,在创建文件、修改文件后,保存文件时需要一个当前时间作为文件的时间戳信息。时间信息可以通过读取RTC来获得,可以使用MCU片上的RTC,也可以使用外接的RTC,如果对文件的时间信息不敏感,不使用RTC时间也没问题。
2、文件系统的一些基本概念
(1)文件系统
FatFS可以在存储介质上创建和管理FAT(File Allocation Table)文件系统或exFAT (ExtendedFile Allocation Table)文件系统,这两种都是Microsoft公司定义的标准文件系统。
经过不断发展,FAT文件系统现在有3种:FAT12、FAT16和FAT32。其中,FAT16的单个分区容量不能超过2GB;FAT32的单个分区容量不能超过2TB,单个文件大小不能超过4GB。FAT文件系统是向后兼容的,即FAT32兼容FAT16和FAT12。
exFAT是Microsoft继FAT16/FAT32之后开发的一种文件系统,它更适用于基于闪存的存储器,如SD卡、U盘等,而不适用于机械硬盘。所以,exFAT广泛应用于嵌入式系统、消费电子产品和固态存储设备中。exFAT文件系统允许单个文件大小超过4GB,单个分区大小和单个文件大小几乎没有限制。
(2)FAT卷
一个FAT文件系统称为一个逻辑卷(logical volume)或逻辑驱动器(logical drive),如计算机上的C盘、D盘。一个FAT卷包括如下的3个或4个区域(Area),每个区域占用1个或多个扇区,并按如下的顺序排列。
- 保留区域,用于存储卷的配置数据。
- FAT区域,用于存储数据区的分配表。
- 根目录区域,在FAT32卷上没有这个区域。
- 数据区域,存储文件和目录的内容。
(3)扇区
扇区(sector)是存储介质上读写数据的最小单元。一般的扇区大小是512字节,FatFS支持512字节、1024字节、2048字节和4096字节等几种大小的扇区。存储设备上的每个扇区有一个扇区编号,从设备的起始位置开始编号的称为物理扇区号(physical sector number),也就是扇区的绝对编号。另外,从卷的起始位置开始相对编号也是可以的,这称为扇区号(sector number)。W25Q16的扇区是4096B,4KB。
(4)簇
一个卷的数据区分为多个簇(cluster),一个簇包含1个或多个扇区,数据区就是以簇为单位进行管理的。簇越小,文件占用空间越小,但速度越慢。反之亦然。一个卷的FAT类型就是由其包含的簇的个数决定的,由簇的个数就可以判断卷的FAT类型。FAT的具体类型如下。
- FAT12卷,簇的个数≤4085。
- FAT16卷,4086≤簇的个数≤65525。
- FAT32卷,簇的个数≥65526。
(5)数据存储形式
FAT文件系统使用的是小端字节序(little endian),所以,如果处理器使用的是大端字节序(big endian),那么读取FAT文件系统的文件,就需要进行字节序的转换。另外,字(word)数据也不一定是边界对齐的,所以在读写FAT文件系统的文件时,最好使用字节数组的形式,逐个字节进行读写。
3、FatFS的功能特点和参数
FatFS是在存储介质上创建FAT/exFAT文件系统,并管理文件的软件系统,它有以下特性和限制。
- 支持FAT文件系统和exFAT文件系统。
- 支持长文件名和Unicode。
- 对于RTOS是线程安全的。
- 最多可以管理10个卷,可以是多个存储介质或多个分区。
- 支持不同大小的扇区,扇区大小可以是512字节、1024字节、2048字节或4096字节。
- 同时打开的文件个数:无限制,只受限于内存大小。
- 最小卷大小:128个扇区。
- 最大卷大小:FAT中是4G个扇区,exFAT中几乎是无限制的。
- 最大单个文件大小:FAT卷中是4GB,exFAT中几乎是无限制的。
- 簇大小限制:FAT卷中一个簇最大128个扇区,exFAT卷中是16MB。
4、FatFS的文件组成
FatFS已经作为一个中间件集成到了STM32 MCU固件库中,可以在CubeMX中启用和配置FatFS。CubeMX中的FatFS支持外部SRAM、SD卡、U盘和用户定义存储器(如SPI-Flash存储器)。除了用户定义存储器,CubeMX生成的代码能自动完成对所选存储介质的FatFS移植,如SD卡和U盘,所以在STM32Cube开发方式中使用FatFS很方便。
一个应用系统使用FatFS管理某个具体的存储介质上的文件系统时,又可以具体划分为如下图所示的分层结构。图中以SPI-Flash存储芯片W25Q128为例。图中的箭头表示调用关系,系统各层的功能描述如下。
(1)用户应用程序
包括CubeMX自动生成的FatFS初始化程序和用户编写的文件系统访问程序。CubeMX自动生成的代码在进行FatFS的初始化时,会调用文件f_gen_drv.h中的一个函数FATFS_LinkDriver(),其功能就是将一个或多个逻辑驱动器链接到FatFS管理的驱动器列表里。
用户编写的文件系统访问程序就是使用文件ffh中提供的API函数进行文件系统操作,例如,用函数f_open()打开文件,用函数f_write()向文件写入数据等。
(2)FatFS应用接口
这是面向用户应用程序的编程接口,提供文件操作的API函数,这些API函数与具体的存储介质和处理器类型无关。文件ff.h和ff.c中定义和实现了这些API函数。文件ffconf.h是FatFS的配置文件,用于定义FatFS的一些参数,以便进行功能裁剪。
(3)FatFS通用硬件接口
这是实现存储介质访问(Disk IO)的通用接口。文件diskio.h/.c中定义和实现了Disk IO的几个基本函数,包括disk_initialize()、disk_read()、disk_ioctl()等。在CubeMX生成的代码中,文件diskio.c中的这些Disk IO函数实际上是调用具体硬件访问层实现的具体器件的Disk IO函数。文件diskio.h/.c中的代码是不允许修改的,以往直接手工编程移植FatFS时,都是直接在文件diskio.c中实现具体器件的Disk IO函数。
文件ff_gen_drv.h定义了驱动器和驱动器列表管理的一些类型和函数。这个文件里最主要的一个函数是FATS_LinkDriver()。主程序进行FatFS初始化时,会调用这个函数,用于将一个驱动器链接到FatFS管理的驱动器列表里。而驱动器是在器件的Disk IO实现文件里定义的对象,驱动器实现了针对具体存储介质的Disk IO函数。
另外,还有一个文件integer.h,这个文件定义了FatFS用到的基本数据类型。
(4)具体硬件访问层
假设一个存储介质只建立一个逻辑分区,那么一个存储介质就是一个驱动器,例如,一个SD卡或一个U盘。驱动器需要实现自己的Disk IO函数,驱动器会通过函数指针,将diskio.h中定义的通用Disk IO函数指向器件的Disk IO函数。器件的Disk IO函数是在一个特定的文件里实现的,而这些Disk IO函数又会调用器件的驱动程序或硬件接口的HAL驱动程序。
例如,使用SPI-Flash存储芯片W25Q128作为存储介质时,需要在一个文件中实现SPI-Flash器件访问的Disk IO函数,这些Disk IO函数要用到W25Q128的驱动程序,而W25Q128的驱动程序又要用到HAL库中SPI接口的驱动程序。
(5)具体硬件
具体硬件就是用于存储文件的存储介质。CubeMX的FatFS模式设置中支持的存储介质,包括外部SRAM、SD卡、U盘和用户定义(User-defined)器件。在嵌入式设备中,一个存储介质一般只有一个分区。
如果使用外部SRAM、SD卡或U盘作为存储介质,CubeMX生成的代码会自动生成完整的FatFS移植代码,除了一个获取时间戳的函数需要用户编程实现,用户无须自己再编程实现器件的Disk IO函数,直接使用FatFS的API函数进行文件管理即可,所以使用起来很方便。
如果选择用户定义器件作为存储介质,CubeMX会生成FatFS移植的代码框架,用户需要自己编写器件的Disk IO函数。例如,SPI-Flash存储芯片W25Q128就是用户定义器件。
5、FatFS的基本数据类型定义
FatFS的文件integer.h对基本数据类型重新定义了符号,这是为了便于在不同的处理器平台上移植。文件integer.h中针对嵌入式平台定义的基本数据类型符号如下:
/*-------------------------------------------*/
/* Integer type definitions for FatFs module */
/*-------------------------------------------*/
#ifndef _FF_INTEGER
#define _FF_INTEGER
#ifdef _WIN32 /* FatFs development platform */
#include <windows.h>
#include <tchar.h>
typedef unsigned __int64 QWORD;
#else /* Embedded platform */
/* These types MUST be 16-bit or 32-bit */
typedef int INT;
typedef unsigned int UINT;
/* This type MUST be 8-bit */
typedef unsigned char BYTE;
/* These types MUST be 16-bit */
typedef short SHORT;
typedef unsigned short WORD;
typedef unsigned short WCHAR;
/* These types MUST be 32-bit */
typedef long LONG;
typedef unsigned long DWORD;
/* This type MUST be 64-bit (Remove this for ANSI C (C89) compatibility) */
typedef unsigned long long QWORD;
#endif
#endif
在STM32 MCU上,INT和LONG是32位整数,UINT和DWORD是32位无符号整数,QWORD是64位无符号整数。
二、 FatFS的应用程序接口函数
FatFS的应用程序接口函数在文件ff.h中定义,用于FAT文件系统的操作,函数可以分为几个大类。
1、卷管理和系统配置相关函数
卷管理和系统配置相关函数见表。这些函数的返回值类型都是FRESULT。
函数名 | 函数功能 | 函数原型 |
f_mount | 注册或注销一个卷,使用一个 | f_mount(FATFS*fs,const TCHAR*path,BYTE opt) |
f_mkfs | 在一个逻辑驱动器上创建FAT | f_mkfs(const TCHAR*path,BYTE opt,DWORD au,void* |
f_fdisk | 在一个物理驱动器上创建分区 | f_fdisk(BYTE pdrv,const DWORD*szt,void*work) |
f_getfree | 返回一个卷上剩余簇的个数 | f_getfree(const TCHAR*path,DWORD*nclst,FATFS**fatfs) |
f_getlabel | 获取一个卷的标签 | f_getlabel(const TCHAR*path,TCHAR*label,DWORD*vsn) |
f_setlabel | 设置一个卷的标签 | f_setlabel(const TCHAR*label) |
FatFS对文件系统是以卷为单位进行管理的,一个卷就相当于计算机上的一个逻辑分区,如C盘和D盘。在嵌入式设备中,一般一个存储介质只有一个卷,不需要用函数f_fdisk()进行分区,例如,一个SD卡是一个卷,一个SPI-Flash存储芯片是一个卷。在FatFS中,卷的编号默认是用字符串“0:”“1:”“2:”等表示的,其中的数字表示卷的编号。
要管理一个卷上的文件系统,首先需要用函数f_mount()注册这个卷,如果这个函数的返回值为FR_NO_FILESYSTEM,就表示卷上还没有文件系统,需要用函数f_mkfs()对卷进行格式化。注册一个卷之后,用户才能进行创建文件、打开文件、读写文件数据等操作。
(1)函数f_mkfs()
函数f_mkfs()用于在一个卷上创建文件系统,也就是进行格式化。函数f_mkfs()的原型定义如下。通常,在各参数的注释中,[IN]表示输入参数,[OUT]表示输出参数,输出参数就是传递指针用于获取返回数据。
FRESULT f_mkfs (
const TCHAR* path, /*[IN]逻辑驱动器号*/
BYTE opt, /*[IN]格式化选项*/
DWORD au, /*[IN]分配单元(簇)大小,单位:字节*/
void* work, /*[IN]用于格式化的工作缓冲区指针*/
UINT len /*[IN]工作缓冲区大小,单位:字节*/
); /* Create a FAT volume */
参数path是一个字符串,就是逻辑驱动器号(也就是卷号),如“0:”表示第1个卷,“1:”表示第2个卷。如果是空字符串,则表示默认卷,如果系统中只有一个卷,则可以使用空字符串。
参数opt是格式化选项,也就是要创建的文件系统类型,可设置的选项宏定义如下:
/*Format options(2nd argument of f_mkfs)*/
#define FM_FAT 0x01 //FAT文件系统,自动设置为FAT12或FAT16
#define FM_FAT32 0x02 //FAT32文件系统
#define FM_EXFAT 0x04 //exFAT文件系统
#define FM_ANY 0x07 //任意文件系统,前面3种的按位或,自动根据卷大小和簇大小来决定
#define FM_SFD 0x08 //只有单个分区时可以指定此选项,SFD表示super-floppy disk
参数opt的默认值是FM_ANY,也就是根据卷的总大小和簇的大小自动设置文件系统格式,一个文件系统是FAT12、FAT16或FAT32,完全由其簇的个数决定。一般情况下,32GB以下的卷可以使用FM_FAT选项,32GB以上的一般使用FM_FAT32选项。
参数au是簇的大小,以字节为单位。一个卷的数据区是以簇为单位进行管理的,一个簇包含1个或多个扇区,扇区是存储介质上读写数据块的最小单元。
参数work是格式化操作时的一个工作缓冲区,这个缓冲区大小必须是簇的1倍以上大小。缓冲区越大,格式化操作越快。
参数len是缓冲区work的大小,以字节为单位。
函数的返回值类型是FRESULT,这是FatFS中定义的函数返回值枚举类型。函数操作正常时,返回值为FR_OK,其他返回值表示了各种信息,具体见注释。FatFS的API函数返回值一般都是FRESULT类型。
/* File function return code (FRESULT) */
typedef enum {
FR_OK = 0, /* (0) Succeeded */
FR_DISK_ERR, /* (1) A hard error occurred in the low level disk I/O layer */
FR_INT_ERR, /* (2) Assertion failed */
FR_NOT_READY, /* (3) The physical drive cannot work */
FR_NO_FILE, /* (4) Could not find the file */
FR_NO_PATH, /* (5) Could not find the path */
FR_INVALID_NAME, /* (6) The path name format is invalid */
FR_DENIED, /* (7) Access denied due to prohibited access or directory full */
FR_EXIST, /* (8) Access denied due to prohibited access */
FR_INVALID_OBJECT, /* (9) The file/directory object is invalid */
FR_WRITE_PROTECTED, /* (10) The physical drive is write protected */
FR_INVALID_DRIVE, /* (11) The logical drive number is invalid */
FR_NOT_ENABLED, /* (12) The volume has no work area */
FR_NO_FILESYSTEM, /* (13) There is no valid FAT volume */
FR_MKFS_ABORTED, /* (14) The f_mkfs() aborted due to any problem */
FR_TIMEOUT, /* (15) Could not get a grant to access the volume within defined period */
FR_LOCKED, /* (16) The operation is rejected according to the file sharing policy */
FR_NOT_ENOUGH_CORE, /* (17) LFN working buffer could not be allocated */
FR_TOO_MANY_OPEN_FILES,/* (18) Number of open files > _FS_LOCK */
FR_INVALID_PARAMETER /* (19) Given parameter is invalid */
} FRESULT;
例如,以开发板上的SPI-Flash芯片W25Q16为例,它的总容量为16Mbit(2Mbyte,256byte/page),一个扇区大小为1024字节,若设置簇大小为2个扇区,则可以用FM_FAT格式化选项。假设系统中只有这一个存储介质使用FatFS进行管理,则对其进行格式化的示意代码如下:
BYTE workBuffer[1024]; //工作缓冲区
res=f_mkfs("0:",FM_FAT,2*1024,workBuffer,1024);
(2)函数f_mount()
FatFS要求每个逻辑驱动器(FAT卷)有一个文件系统对象(file system object)作为工作区域,在对这个逻辑驱动器进行文件操作之前,需要用函数f_mount()对逻辑驱动器和文件系统对象进行注册。只有注册了逻辑驱动器,才可以使用FatFS的应用接口API函数进行操作。
函数f_mount()的原型定义如下:
/*-----------------------------------------------------------------------*/
/* Mount/Unmount a Logical Drive */
/*-----------------------------------------------------------------------*/
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 /* Mode option 0:Do not mount (delayed mount), 1:Mount immediately */
)
参数fs是一个FATFS结构体指针,表示文件系统对象,若这个参数为NULL,则表示卸载(unmount)这个逻辑驱动器。
参数path是逻辑驱动器号,例如“0:”,如果是空字符串,则表示默认的逻辑驱动器号。
参数opt是模式选项。设置为1时,表示强制立刻挂载;设置为0时,表示延后挂载,在后续进行文件操作时自动挂载。延后挂载时,此函数总是返回FR_OK。
调用函数f_mount()的示例代码如下。返回值为FR_NO_FILESYSTEM时,表示存储介质还没有文件系统,需要调用f_mkfs()函数进行格式化,格式化成功后,需要先卸载,然后再次挂载,示意代码如下:
FATFS fs; //文件系统对象
FRESULT res =f_mount(&fs,"0:",1); //立刻挂载文件系统
if(res == FR_NO_FILESYSTEM) //不存在文件系统,未格式化
{
BYTE workBuffer[1024]; //工作缓冲区
res=f_mkfs("0:",FM_FAT,1024,workBuffer,1024); //簇大小=4096字节
if(res == FR_OK) //格式化成功
{
res = f_mount(NULL,"0:",1) ; //先卸载
res = f_mount(&fs,"0:",1); //再次挂载文件系统
}
}
(3)函数f_getfree()
FAT文件系统的数据区是以簇为单位进行管理的,函数f_getfree()返回一个逻辑驱动器上剩余簇的个数,其原型定义如下:
/*-----------------------------------------------------------------------*/
/* Get Number of Free Clusters */
/*-----------------------------------------------------------------------*/
FRESULT f_getfree
(
const TCHAR* path, /* Path name of the logical drive number */
DWORD* nclst, /* Pointer to a variable to return number of free clusters */
FATFS** fatfs /* Pointer to return pointer to corresponding file system object */
)
参数path是驱动器号,如“0:”。参数nclst是输出参数,是剩余簇的个数,使用传递地址的方式获取返回值。参数fatfs是输出参数,指向返回的文件系统对象。
FatFS的API函数的返回值一般都是FRESULT类型,表示函数执行是否成功或错误类型。要返回的数据使用指针型输出参数,返回的结果写入指针指向的变量或对象。
FATFS结构体是表示文件系统的结构体,它的一些成员变量存储了一个逻辑驱动器上FAT文件系统的一些参数。结构体FATFS的完整定义见源代码,比较有用的几个参数如下。
- BYTE fs_type,分区类型,1=FAT12,2=FAT16,3=FAT32,4=exFAT。
- WORD csize,簇的大小,即一个簇的扇区个数,至少是1个扇区。
- WORD ssize,扇区大小,可为512字节、1024字节、2048字节或4096字节。
- DWORD free_clst,剩余的簇个数,必须执行函数f_getfree()后,才会更新这个值。
- DWORD n_fatent,卷的条目(item)个数,等于簇的个数加2。
调用函数f_getfree()获取逻辑驱动器相关信息的示意代码如下:
FATFS *fs;
//DWORD fre_clust;
FRESULT res = f_getfree("0:",&fre_clust,&fs);
if(res == FR_OK)
{
DWORD tot_sect = (fs->n_fatent -2)*fs->csize; //总的扇区个数
DWORD fre_sect = free_clst*fs->csize; //剩余的扇区个数=剩余簇个数*每个簇的扇区个数
DWORD freespace = (fre_sect*fs->ssize)/1024; //剩余空间大小,单位:KB
}
2、文件和目录管理相关函数
文件和目录管理相关函数见表,这些函数的主要功能包括创建目录、删除文件或目录、改变当前工作目录、检查文件或目录是否存在等。这些函数的返回值类型都是FRESULT,所以在函数原型表示中省略了返回值类型。
函数名 | 函数功能 | 函数原型 |
f_stat | 检查一个文件或目录是否存在 | f_stat(const TCHAR*path,FILINFO*fno) |
f_unlink | 删除一个文件或目录 | f_unlink(const TCHAR*path) |
f_rename | 重命名或移动一个文件或目录 | f_rename(const TCHAR*path_old,const TCHAR*path_new) |
f_chmod | 改变一个文件或目录的属性 | f_chmod(const TCHAR*path,BYTE attr,BYTE mask); |
f_utime | 改变一个文件或目录的时间戳 | f_utime(const TCHAR*path,const FILINFO*fno) |
f_mkdir | 创建一个新的目录 | f_mkdir(const TCHAR*path); |
f_chdir | 改变当前工作目录 | f_chdir(const TCHAR*path); |
f_chdrive | 改变当前驱动器 | f_chdrive(const TCHAR*path) |
f_getcwd | 获取当前驱动器的当前工作目录 | f_getcwd(TCHAR*buff,UINT len) |
- 删除文件时,不能删除具有只读属性的文件或目录,不能删除非空目录或当前目录,不能删除打开的文件或目录。
- 重命名文件时,不能操作打开的文件或目录。
- 参数_FS_RPATH≥1表示使用相对路径,此时才有函数f_chdir()。
- 参数_FS_RPATH≥1且_VOLUMES≥2时,也就是使用相对路径,且卷的个数大于1时,才有函数f_chdrive()。
- 参数_FS_RPATH≥2时,才有函数f_getcwd()。
(1)函数f_stat()
函数f_stat()用于检查一个文件或目录是否存在,并且返回其文件信息,其原型定义如下:
/*-----------------------------------------------------------------------*/
/* Get File Status */
/*-----------------------------------------------------------------------*/
FRESULT f_stat (
const TCHAR* path, /* Pointer to the file path */
FILINFO* fno /* Pointer to file information to return */
)
输出参数fno是FILINFO结构体类型的指针,用于保存返回的文件信息。结构体FILINFO的定义如下,可以返回文件大小、最后修改的时间和日期、文件属性等信息:
/* File information structure (FILINFO) */
typedef struct {
FSIZE_t fsize; /* File size */
WORD fdate; /* Modified date */
WORD ftime; /* Modified time */
BYTE fattrib; /* File attribute */
#if _USE_LFN != 0
TCHAR altname[13]; /* Alternative file name */
TCHAR fname[_MAX_LFN + 1]; /* Primary file name */
#else
TCHAR fname[13]; /* File name */
#endif
} FILINFO;
函数的返回值为FR_OK表示文件或目录存在;为FR_NO_FILE表示文件不存在;为FR_NO_PATH表示目录不存在;还可能有其他返回值,具体请查阅FRESULT的枚举值。
使用f_stat()检查一个文件,并获取其参数的示例代码如下:
FILINFO fno;
FRESULT fr = f_stat("0:/readme.txt",&fno);
if(fr == FR_OK)
{
printf("File size(bytes)= %d. \r\n",fno.fsize);
}
(2)函数f_chmod()
函数f_chmod()用于改变一个文件或目录的属性,其原型定义如下:
/*-----------------------------------------------------------------------*/
/* Change Attribute */
/*-----------------------------------------------------------------------*/
FRESULT f_chmod (
const TCHAR* path, /* Pointer to the file path */
BYTE attr, /* Attribute bits */
BYTE mask /* Attribute mask to change */
)
文件或目录的属性用一个字节数据表示,各个位代表不同的属性。属性位为1表示具有相应的属性,否则就表示没有这个属性。文件ff.h定义了属性位的宏定义。
#define AM_RDO 0x01 //只读,Read only
#define AM_HID 0x02 //隐藏,Hidden
#define AM_SYS 0x04 //系统,System
#define AM_DIR 0x10 //目录,Directory
#define AM_ARC 0x20 //归档,Archive
参数attr是要置位的属性位,是这些属性位的按位或运算,例如,要将一个文件的属性设置为只读和隐藏,需要将attr的值设置为AM_RDO|AM_HID。参数mask是要修改的属性位的掩码,mask中为1的位会被置0。例如,要将文件readme.txt设置为只读属性,清除文件的隐藏属性,其他属性不变,需要执行如下代码:
f_chmod("readme.txt",AM_RDO,AM_RDO|AM_HID);
(3)函数f_utime()
函数f_utime()用于改变一个文件或目录的时间戳数据,其原型定义如下:
函数f_utime()用于改变一个文件或目录的时间戳数据,其原型定义如下:
/*-----------------------------------------------------------------------*/
/* Change Timestamp */
/*-----------------------------------------------------------------------*/
FRESULT f_utime (
const TCHAR* path, /* Pointer to the file/directory name */
const FILINFO* fno /* Pointer to the time stamp to be set */
)
其中的结构体FILINFO,其成员变量fdate表示日期,ftime表示时间,这两个成员变量都是WORD类型变量,也就是uint16_t类型。fdate的日期数据格式如下表。其中,年份是与1980(年)的差值。
数据位 | 数据范围 | 表示意义 |
15:9位 | 共7位,数据范围为0~127 | 年份,实际年份是1980加上这个值,如39表示2019年 |
8:5位 | 共4位,有效范围为1~12 | 月份,表示1到12月 |
4:0位 | 共5位,有效范围为1~31 | 日期,表示1到31日 |
ftime的时间数据格式如下表。其中,最低5位的最大值也只有31,有效数据范围是0~29,不能逐一表示0~59s,将此数值乘以2之后得到的才是秒数据。
数据位 | 数据范围 | 表示意义 |
15:11位 | 共5位,数据范围为0~23 | 小时,表示0~23时 |
10:5位 | 共6位,有效范围为0~59 | 分钟,表示0~59分 |
4:0位 | 共5位,有效范围为0~29 | 秒/2,将这个值乘以2之后是秒,表示0~58s |
使用函数f_utime()修改一个文件的日期时间的示意代码如下:
FILINFO fno;
//日期2019-12-13
WORD date = (2019-1980)<<9;
WORD month = 12<<5;
fno.fdate = date | month | 13;
//时间14:32:15
WORD time = 14<<11;
WORD minute = 32<<5;
WORD sec = 15>>1; //除以2
fno.ftime = time | minute | sec;
f_utime("readme,txt",&fno); //修改文件的时间戳
3、目录访问相关函数
使用目录访问函数可以打开一个目录,然后在这个目录下查找匹配的文件或目录。这些函数的返回值类型都是FRESULT。
函数名 | 函数功能 | 函数原型 |
f_opendir | 打开一个已存在的目录 | f_opendir(DIR*dp,const TCHAR*path) |
f_closedir | 关闭一个打开的目录 | f_closedir(DIR*dp) |
f_readdir | 读取目录下的一个项 | f_readdir(DIR*dp,FILINFO*fno) |
f_findfirst | 在目录下查找第一个匹配项 | f_findfirst(DIR*dp,FILINFO*fno,const TCHAR*path, const TCHAR*pattern) |
f_findnext | 查找下一个匹配项 | f_findnext(DIR*dp,FILINFO*fno) |
使用这些函数,可以列出一个目录下的特定类型文件。例如,一个嵌入式的图像抓拍设备自动将抓拍的图片以“cap*.jpg”的形式命名并保存到SD卡上(其中,*表示数字编号),就可以使用目录访问相关函数列出这些文件。系统复位后,可以查找编号最大的文件,然后在新编号的基础上保存新的图片文件。
(1)打开和关闭目录
函数f_opendir()用于打开一个已存在的目录,其原型定义如下:
FRESULT f_opendir (
DIR* dp, /* Pointer to directory object to create */
const TCHAR* path /* Pointer to the directory path */
)
输入参数path是目录名称;输出参数dp是一个DIR结构体类型指针,是这个函数创建的一个目录对象,表示打开的目录。函数f_closedir()用于关闭一个打开的目录。两个函数的使用示例代码如下:
DIR dir;
FRESULT res =f_opendir(&dir,"0:"); //打开根目录
f_closedir(&dir); //关闭目录
结构体DIR包含了目录的一些信息,在文件ff.h中定义
(2)函数f_readdir()
函数f_readdir()用于读取目录下的一个项(文件或目录),其原型定义如下:
/*-----------------------------------------------------------------------*/
/* Read Directory Entries in Sequence */
/*-----------------------------------------------------------------------*/
FRESULT f_readdir (
DIR* dp, /* Pointer to the open directory object */
FILINFO* fno /* Pointer to file information to return */
)
输入参数dp是DIR类型的指针,是已经打开的目录对象;输出参数fno是FILINFO类型的指针,存储了读取的一个项(文件或目录)的信息。
连续调用函数f_readdir()可以依次读取目录下的项,包括文件和子目录,不包括“.”和“..”项。当一个目录下的所有项被读取完之后,参数fno的成员变量fname变成以null结尾的空字符串。
在调用f_readdir()时,如果参数fno设置为NULL,就从头开始读取。
(3)查找目录下匹配的项
函数f_findfirst()和f_findnext()可以用于读取目录下某些特定的文件或子目录,例如,找出目录下文件名与“*.txt”匹配的所有文件。函数f_findfirst()的原型定义如下:
/*-----------------------------------------------------------------------*/
/* Find First File */
/*-----------------------------------------------------------------------*/
FRESULT f_findfirst (
DIR* dp, /* Pointer to the blank directory object */
FILINFO* fno, /* Pointer to the file information structure */
const TCHAR* path, /* Pointer to the directory to open */
const TCHAR* pattern /* Pointer to the matching pattern */
)
/*-----------------------------------------------------------------------*/
/* Find Next File */
/*-----------------------------------------------------------------------*/
FRESULT f_findnext (
DIR* dp, /* Pointer to the open directory object */
FILINFO* fno /* Pointer to the file information structure */
)
其中,参数path是要打开的目录名称,参数pattern是匹配模式字符串。匹配模式字符串可以使用字符‘?’或‘*’,问号用于匹配一个字符,星号用于匹配任意长度的字符串。
输出参数dp是打开的目录对象指针,输出参数fno是找到的第一个匹配项的文件信息指针。如果没有找到匹配项,fno为NULL,函数返回值不为FR_OK。
函数f_findnext()用于在上次执行f_findfirst()或ffindnext()之后,继续查找下一个匹配文件。如果找到了匹配项,函数f_findnext()的返回值为FR_OK,匹配项的文件信息存储在变量fno中;如果没有找到匹配项,函数f_findnext()返回的fno->fname为空字符串。
例如,查找并显示根目录下所有txt文件的示例代码如下:
DIR dir; //搜索的目录对象
FILINFO fno; //文件信息
FRESULT fr = f_findfirst(&dir,&fno,"","*.txt"); //查找第一个匹配文件
while(fr == FR_OK && fno.fname[0])
{
printf("fno.fname: %s \r\n",fno.fname);
fr = f_findnext(&dir,&fno); //查找下一个匹配项
}
f_closedir(Gdir); //关闭目录
4、文件访问相关函数
文件访问包括打开文件、读取数据、写入数据、关闭文件等操作,这些函数与计算机上C语言访问文件的函数是类似的。在函数原型中若没有写返回值类型,函数返回值类型就是FRESULT。
函数名 | 函数功能 | 函数原型 |
f_open | 打开一个文件 | f_open(FIL*fp,const TCHAR*path,BYTE mode) |
f_close | 关闭一个打开的文件 | f_close(FIL*fp) |
f_read | 从文件读取数据 | f_read(FIL*fp,void*buff,UINT btr,UINT*br) |
f_write | 将数据写入文件 | f_write(FIL*fp,const void*buff,UINT btw,UINT*bw) |
f_lseek | 移动读写操作的指针 | f_lseek(FIL*fp,FSIZE_t ofs) |
f_truncate | 截断文件 | f_truncate(FIL*fp) |
f_sync | 将缓存的数据写入文件 | f_sync(FIL*fp) |
f_forward | 读取数据直接传给数据流设备 | f_forward(FIL*fp,UINT(*func)(const BYTE*,UINT), |
f_expand | 为文件分配连续的存储空间 | f_expand(FIL*fp,FSIZE_t szf,BYTE opt) |
f_gets | 从文件读取一个字符串 | TCHAR *f_gets(TCHAR *buff,int len,FIL*fp) |
f_putc | 向文件写入一个字符 | int f_putc(TCHAR c,FIL*fp); |
f_puts | 向文件写入一个字符串 | intf puts(const TCHAR *str,FIL*cp) |
f_printf | 用格式写入字符串 | int f_printf(FIL*fp,const TCHAR *str,..); |
f_tell | 获取当前的读写指针 | #define f_tell(fp) ((fp)->fptr) |
f_eof | 检测是否到文件尾端 | #define f_eof(fp) (int)(fp)->fptr == (fp)->obj.objsize)) |
f_size | 获取文件大小,单位:字节 | #define f_size(fp) (fp)->obj.objsize) |
f_error | 检测是否有错误,返回0表示无 | #define f_error(fp) (fp)->err) |
要访问一个文件,需要先用函数f_open()打开或新建一个文件,然后用f_read()、f_write()等函数进行文件读写操作,操作完成后,要用函数f_close()关闭文件。
(1)函数f_open()
函数f_open()用于打开或新建一个文件,其原型定义如下:
/*-----------------------------------------------------------------------*/
/* Open or Create a File */
/*-----------------------------------------------------------------------*/
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 */
)
其中,参数path是要打开或新建文件的文件名,如果成功打开或新建了文件,会返回一个FIL结构体类型指针fp,在后续的文件读写操作里,用这个文件对象表示所操作的文件。参数mode是文件访问模式,是一些宏定义的位运算组合,这些模式的宏定义如下:
/* Get logical drive */
mode &= _FS_READONLY ? FA_READ : FA_READ | FA_WRITE | FA_CREATE_ALWAYS | FA_CREATE_NEW | FA_OPEN_ALWAYS | FA_OPEN_APPEND | FA_SEEKEND;
res = find_volume(&path, &fs, mode);
#define FA_READ 0x01 //读取模式
#define FA_WRITE 0x02 //写入模式,FA_READ |FA_WRITE表示可读可写
#define FA_OPEN_EXISTING 0x00 //要打开的文件必须已存在,否则函数失败
#define FA_CREATE_NEW 0x04 //新建文件,如果文件已存在,函数失败并返回FR_EXIST
#define FA_CREATE_ALWAYS 0x08 //总是新建文件,如果文件已存在,会覆盖现有文件
#define FA_OPEN_ALWAYS 0x10 //打开一个已存在的文件,如果不存在就创建新文件
#define FA_OPEN_APPEND 0x30 //与FA_OPEN_ALWAYS相同,只是读写指针定位在文件尾端
在使用函数f_open()成功打开一个文件,并完成文件的数据读写操作后,需要用函数f_close()关闭文件,否则,修改的数据可能保存不到文件的存储介质上,或使文件损坏。
(2)函数f_write()
函数f_write()用于向一个以可写方式打开的文件写入数据,其原型定义如下:
/*-----------------------------------------------------------------------*/
/* Write File */
/*-----------------------------------------------------------------------*/
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 */
)
参数fp是用函数f_open()打开文件时返回的文件对象指针。文件对象结构体FIL有一个DWORD类型的成员变量fptr,称为文件读写位置指针,用于表示文件内当前读写位置。文件打开后,这个读写位置指针指向文件顶端。函数f_write()在当前的读写位置指针处向文件写入数据,写入数据后,读写位置指针会自动向后移动。
参数buff是待写入的数据缓冲区指针,参数btw是待写入数据的字节数。输出参数bw是完成操作后,实际写入文件的数据字节数,如果*bw<btw,表明缓冲区buff内的数据没有全部写入文件,可能是存储介质满了。
举个例子,用函数f_open()新建一个文件,然后向文件写入一些数据,写完数据后,用f_close()关闭文件,代码如下:
void TestWriteBinFile(TCHAR *filename,uint32_t pointCount,uint32_t sampFreq)
{
FIL file;
FRESULT res = f_open(&file,filename,FA_CREATE_ALWAYS | FA_WRITE);
if(res == FR_OK)
{
UINT bw=0; //实际写入字节数
f_write(&file,&pointCount,sizeof(uint32_t),&bw);//数据点个数
f_write(&file,&sampFreq,sizeof(uint32_t),&bw); //采样频率
uint32_t value = 1000;
for(uint16_t i=0;i<pointCount;i++,value++)
f_write(&file,&value,sizeof(uint32_t),&bw);
}
f_close(&file);
}
(3)函数f_read()
在使用函数f_open()打开文件时,如果模式参数中带有FA_READ,打开文件后就可以用函数f_read()读取数据。其原型定义如下:
/*-----------------------------------------------------------------------*/
/* Read File */
/*-----------------------------------------------------------------------*/
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_read()会从文件的当前读写位置处读取btr个字节的数据,然后将读出的数据保存到缓冲区buff里,将实际读取的字节数返回到变量*br里。读出数据后,文件的读写指针会自动向后移动。与前面的函数TestWriteBinFile()对应的,从一个文件读取数据的代码如下:
void TestReadBinFile(TCHAR*filename)
{
FIL file;
FRESULT res = f_open(&file,filename,FA_READ);
if(res == FR_OK)
{
UINT bw = 0; //实际读取字节数
uint32_t pointCount,sampFreq;
f_read(&file,&pointCount,sizeof(uint32_t),&bw); //数据点个数
f_read(&file,&sampFreq,sizeof(uint32_t),&bw); //采样频率
uint32_t value;
for(uint16_t i=0;i<pointCount;i++)
f_read(&file,&value,sizeof(uint32_t),&bw);
}
f_close(&file);
}
二进制文件按照设定的顺序写入各种数据,读取时,也必须按照规定的顺序读取,这就是二进制文件的存储格式定义。
f_write()和f_read()是通用的数据读写函数,可以读写任何类型的数据,但不太适合于读写字符串数据。例如,用下面的代码向文件写入一个字符串:
TCHAR str[]="ADC1"; //以'\0'结束的字符串
UINT bw;
f_write(&file,str,1+sizeof(str),&bw);
字符串str是以结束符10结束的字符串,sizeof(str)的值是st的字符个数,不包含结束符\0',所以在使用函数f_write()写入字符串时,写入的字节数是1+sizeof(str),也就是实际写入了5字节。
在这种情况下,用函数f_read()读取字符串时会遇到问题。例如,读取前面写入的字符串“ADC1”的代码如下:
TCHAR str[20]; //缓冲区,预留较大空间
UINT br,btr=5; //字符串实际有5个字符,包含结束符'\0'
f_read(&file,str,btr,&br);
使用函数f_read()时,需要指定读取数据的字节数,这里已知字符串长度为5字节,所以指定为5。如果字符串长度不是固定的,采用函数f_read()读取字符串时,就需要采用一些处理方法了。例如,将字符串长度作为一个uint16_t数据写在字符串前面,先读取这个字符串长度数据,然后作为函数f_read()的参数用于读取字符串。
(4)读写字符串的函数f_puts()和f_gets()
FatFS提供了两个函数,专门用于字符串数据的读写。函数f_puts()用于写入一个字符串,其原型定义如下:
/*-----------------------------------------------------------------------*/
/* Put a string to the file */
/*-----------------------------------------------------------------------*/
int f_puts (
const TCHAR* str, /* Pointer to the string to be output */
FIL* fp /* Pointer to the file object */
)
其中,str是以'0'作为结束符的字符串指针,fp是文件对象指针。如果写入成功,函数返回实际写入的字节数,否则返回-1。这个函数的使用示例代码如下:
TCHAR str[]="Line1:Hello,FatFS\n"; //字符串必须有换行符'\n'
f_puts(str,&file); //不会写入结束符'\0'
必须在字符串str末尾加上换行符'n¹,因为使用函数f_puts()将字符串写入文件时,不会将字符串的结束符'0写入文件。
函数f_gets()用于读取一个字符串,它通过换行符'n'判断一个字符串的结束,在读出的字符串末尾会自动添加结束符'0'。其原型定义如下:
/*-----------------------------------------------------------------------*/
/* Get a string from the file */
/*-----------------------------------------------------------------------*/
TCHAR* f_gets (
TCHAR* buff, /* Pointer to the string buffer to read */
int len, /* Size of string buffer (characters) */
FIL* fp /* Pointer to the file object */
)
参数buff是保存读出字符串的缓冲区,len是buff的长度,fp是文件对象指针。这个函数的使用示例代码如下:
TCHAR str[100];
f_gets(str,100,&file);
这里的第二个参数值100是指缓冲区str的长度,而不是实际读出的字符串的长度。
缓冲区str的长度要足够大,能容纳一次读取的一行字符串。
当FatFS的全局宏定义_USE_STRFUNC的值为2时,换行符为"Irln"。
(5)文件内读写指针的移动
文件对象结构体FIL的成员变量fptr称为文件读写指针,用于存储文件读写的当前位置,是一个DWORD类型的变量。在文件刚打开时,fptr的值为0,也就是指向文件顶端。在使用函数f_write()或f_read()读写数据时,文件读写指针会自动移动。
还有几个可以用于文件读写指针操作的函数,宏函数f_tell()返回文件读写指针的当前值,函数f_lseek()直接将文件读写指针移到文件内的某个绝对位置,函数f_eof()可以判断文件读写指针是否到文件末尾了。函数f_lseek()的原型定义如下:
/*-----------------------------------------------------------------------*/
/* Seek File R/W Pointer */
/*-----------------------------------------------------------------------*/
FRESULT f_lseek (
FIL* fp, /* Pointer to the file object */
FSIZE_t ofs /* File pointer from top of file */
)
参数fp是文件对象指针;参数ofs是相对于文件顶端的偏移量,也就是文件内的绝对位置,单位为字节。所以,函数f_lseek()只能将文件读写指针定位到一个绝对位置,但是可以联合函数f_tell()实现相对移动,示例代码如下:
res =f_lseek(fp,0); //移到文件开头位置
res =f_lseek(fp,f_size(fp)); //移到文件末尾
res =f_lseek(fp,f_tell(fp)+100);//从当前位置后移100字节
res =f_lseek(fp,f_tell(fp)-20); //从当前位置前移20字节
函数f_eof()用于判断文件读写指针是否到达了文件末尾,例如,读取一个全是字符串的文本文件的内容:
FIL file;
FRESULT res = f_open(&file,"readme.txt",FA_READ);
if(res == FR_OK)
{
TCHAR str[100];
while(!f_eof(&file))
{
f_gets(str,100,&file); //读取1个字符串
//
}
}
(6)函数f_sync()
函数f_sync()用于在写入文件时,将缓存的数据保存到物理文件里。函数f_sync()的功能与函数f_close()的类似,只是文件继续处于打开状态,可以继续写入。
函数f_sync()一般用于长时间打开一个文件进行写操作的场景,例如,操作一个数据记录文件,隔几分钟才写入一个数据点。如果在使用函数f_close()之前出现异常,缓存的数据没有写入实际文件,就可能导致数据丢失。使用函数f_sync()就可以将缓存的数据写入实际文件,降低数据丢失的风险。
三、FatFS的存储介质访问函数
将FatFS移植应用于SD卡、U盘、SRAM、Flash芯片等不同存储介质时,用户需要针对具体的存储介质提供底层硬件访问接口。这些硬件层的访问接口在文件diskio.h中定义,FatFS的移植主要就是针对存储介质的硬件层访问程序的移植。
FatFS已经尽量简化了硬件访问层函数的定义,并且使用了统一的函数接口,移植时只需重新实现这些函数即可。文件diskio.h中定义的需要移植的几个硬件层访问相关函数见下表。
函数名 | 函数功能 |
disk_status | 获取驱动器当前状态,例如,是否已完成初始化 |
disk_initialize | 设备初始化,就是硬件接口的初始化 |
disk_read | 读取一个或多个扇区的数据 |
disk_write | 将数据写入一个或多个扇区,只有_USE_WRITE==1,才需要实现这个函数 |
disk_ioctl | 设备IO控制,例如获取扇区个数、扇区大小等参数。只有_USE_IOCTL==1,才需要 |
get_fattime | 获取当前时间作为文件的修改时间,可以通过RTC获取当前时间,也可以不实现这个 |
表中的前3个函数是必须实现的。存储设备数据读写的最小单位是1个扇区,函数disk_read()用于从存储设备将一个或多个扇区的数据读取到缓冲区,函数disk_write()用于将一个缓冲区的数据写入一个或多个扇区。