强烈建议阅读官方文档
在存储器容量不大,存储内容也不多的时候,我们通常就直接按照存储芯片的读写时序,进行读写操作,直接访问存储器的内存。
但是存储的数据量很大,或者需要分门别类的存储时,上面这种方式就很不方便了。这样就引入了文件系统,以文件的形式存储数据,就像用电脑,我们记录数据都是用的 word、txt、excel 等文件,十分方便管理。
FatFs 是用于小型嵌入式系统的通用 FAT/exFAT 文件系统模块。FatFs 模块是按照 ANSI C (C89) 编写的,它可以在 SD 卡、U 盘、Flash 芯片等存储介质上创建 FAT 或 exFAT 文件系统,它提供应用层接口函数,用户可以通过它直接对文件进行数据读写操作和管理。并且与磁盘 I/O 层完全分离。因此,它独立于平台。它可以集成到资源有限的小型微控制器中,如 8051、PIC、AVR、ARM、Z80、RX 等。(DOS/Windows 兼容 FAT/exFAT 文件系统、平台无关,容易移植)
- 用户应用程序:调用FATFS的通用接口API函数进行文件系统的操作,比如f_open()、f_write()等
- FATFS通用程序:与硬件无关的用于文件系统管理的一些API函数。比如文件系统的创建和挂载、目录操作等,这些操作需要调用底层的硬件访问操作
- FATFS底层设备控制:FATFS与底层存储设备通信的一些函数,包括读写存储介质的函数disk_read()、disk_write()、获取时间戳get_fattime()等。比如调用f_write(),到这层就是调用disk_wtire()。FATFS移植主要就是自己实现这几个函数的功能
文件系统的一些基本概念
- 文件系统
FAT和exFAT都是微软定义的标准文件系统。FAT现在有FAT12、FAT16、FAT32。
| FAT | |
|---|---|
| FAT12 | 过于古早、最大分区32MB 最大分区2G(有的4G) |
| FAT16 | |
| FAT32 | 最大分区32G(有的2T、8T) |
| exFAT | 几乎无限制 |
- FAT卷
一个FAT文件系统称为一个逻辑卷(logic volume)或者逻辑驱动器(logic driver),比如电脑上的C盘、D盘
每个FAT卷都包含如下的3或4个区域:
- 保留区域:用于存储卷的配置数据
- FAT区域:存储数据区的分配表
- 根目录区域:FAT32卷上无
- 数据区域:存储文件和目录的内容
- 扇区 sector
扇区是存储介质上读写数据的最小单元(物理层面上),一般扇区大小都是512byte,FATFS支持512、1024、2048、4096byte等几种大小的扇区。每个扇区都有自己的一个扇区编号。
- 簇 cluster
一个卷的数据分区分为很多簇,一个簇包含一个或多个扇区,数据区以簇为单位进行管理。一个卷的FAT类型就是由其包含的簇的数量决定的。
- 数据存储形式
小端字节 little endian。字(word)数据也不一定是边界对齐的,所以尽量使用字节数组的形式进行读写
FATFS文件组成
先略过,一些底层的东西和移植的东西先不看。先看应用
FATFS的应用程序接口函数
在ff.h中定义。可以分为:卷管理和系统配置、文件和目录管理、目录访问、文件访问。强烈建议阅读官方文档
CubeMX 生成代码中某些函数的定义 与 FatFS官网上的原型定义 并不一样。这里以cubemx生成代码进行介绍
卷管理和系统配置
电脑上我们有好多分区(好多卷、好多盘),比如CDEF盘等,但是在嵌入式设备中,一个存储设备一般只有一个卷(盘),且卷的编号一般用字符串“0:" "1:" "2:"这样表示的。所以一般不用f_fdisk()这个函数
f_mkfs
用于在一个卷上创建文件系统,也就是格式化(清除所有数据,建立文件系统)。其函数原型如下:
path:卷号(逻辑驱动器号),比如”0:“或者”1:“这样。如果是空字符串,则表示默认卷
opt:要创建的文件系统类型,一般32G以下的,填FM_FAT;32G及其以上的填FM_FAT32。也有exFAT
au:簇的大小,单位是byte。
work:缓冲区的指针。
len:缓冲区work的大小。缓冲区越大,格式化越快。缓冲区大小必须是簇的一倍以上大小
f_mount
进行各种文件系统操作的第一步
FATFS要求每个驱动器都要有个文件系统对象作为工作区域。
//文件系统对象
FATFS fs;
FRESULT res=f mount(&fs,"0:",1); //立刻挂载文件系统
if(res==FR NO FILESYSTEM) //不存在文件系统,未格式化
{
//工作缓冲区
BYTE workBuffer[4096];
res=f mkfs("0:",FM FAT,4096,workBuffer,4096); //簇大小=4096字节
//格式化成功
if(res ==FR OK)
{
//先卸载
res=f mount(NULL,"0:",1);
//再次挂载文件系统
res=f mount(&fs,"0:",1);
}
}f_getfree
返回一个卷上剩余的簇的个数
FATES *fs;
DWORD fre_clust;
FRESULT res =fgetfree("0:",&fre_clust,&fs);
if(res == FR OK)
{
DWORD tot_sect = (fs->nfatent - 2) * fs->csize; //总的扇区个数
DWORD fre_sect = fre_clust * fs->csize; //剩余的扇区个数 = 剩余簇个数*每个簇的扇区个数
DWORD freespace = (fre_sect * fs->ssize) / 1024: //剩余空间大小,单位:KB
}文件和目录管理
只记录几个项目上可能用到的
f_utime
改变一个文件或者目录的时间戳数据
// FILINFO结构体
typedef struct
{
FSIZE_t fsize; /* 文件大小,字节数 */
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]; /* 文件名 */
#endif
} FILINFO;
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);//修改文件的时间戳
目录访问
f_readdir
读取目录的下个项(文件或目录)
dp:已经open的目录对象
fno:存储着读取到的项的信息1
连续调用f_readdir可以依次读取目录下的项,不包括"."和".."项。当一个目录下所有项都被读取完之后,fno的成员变量fname1变成以NULL结尾的空字符串
官方有读取某目录下所有项的例程2
f_findfirst、f_findnext
建议阅读官方文档
文件访问
f_write、f_read
相对一个文件进行操作,首先要f_open。操作结束后一定要f_close,否则对文件的修改可能不会保存到存储介质上,导致文件损坏
文件对象结构体FIL里有个DWORD类型成员变量fptr,称为文件读写位置指针,用于表示文件内当前读写位置。文件open后,该指针指向文件最前面。f_write写入数据后,该指针会自动向后移动。
close后再open,指针回到最前面
fwriteO)和fread()是通用的数据读写函数,可以读写任何类型的数据,但不太适合于读写字符串数据。
f_puts、f_gets
这两个函数专门用于字符串的读写
字符串最后要有\n或者\r\n,取决于宏USE_STRFUNC的值
这两个函数,写入的时候不会把字符串的结束符'\0'写入。读取操作持续进行,直到存储'\n'、到达文件末尾、缓冲区已填充len - 1 个字符,自动添加字符串的结束符'\0'
文件内读写指针的移动
FTP断点续传的时候需要,先问服务器已经存了多少了,指针移动到相应的地方,继续发送。
f_sync
函数f_sync用于在写入文件时,将缓存的数据保存到物理文件里。与f_close类似,只是文件继续处于可以继续写入开状态。
就相当于我们写文档的时候按一下Ctrl+S保存文件,这样如果突然电脑死机了,已经写入的东西不会丢失。f_close相当于关闭文档时弹出的是否保存,我们点了保存。
Footnotes
-
↩ ↩2// FILINFO结构体 typedef struct { FSIZE_t fsize; /* 文件大小,字节数 */ 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]; /* 文件名 */ #endif } FILINFO; -
↩/* Recursive scan of all items in the directory */ FRESULT scan_files ( char* path /* Start node to be scanned (***also used as work area***) */ ) { FRESULT res; DIR dir; UINT i; static FILINFO fno; res = f_opendir(&dir, path); /* Open the directory */ if (res == FR_OK) { for (;;) { res = f_readdir(&dir, &fno); /* Read a directory item */ if (res != FR_OK || fno.fname[0] == 0) break; /* Break on error or end of dir */ if (fno.fattrib & AM_DIR) { /* It is a directory */ i = strlen(path); sprintf(&path[i], "/%s", fno.fname); res = scan_files(path); /* Enter the directory */ if (res != FR_OK) break; path[i] = 0; } else { /* It is a file. */ printf("%s/%s\n", path, fno.fname); } } f_closedir(&dir); } return res; } int main (void) { FATFS fs; FRESULT res; char buff[256]; res = f_mount(&fs, "", 1); if (res == FR_OK) { strcpy(buff, "/"); res = scan_files(buff); } return res; }