文件系统I/O FATFS RW 源码分析
0 参考
FatFs 是用于小型嵌入式系统的通用 FAT/exFAT 文件系统模块。FatFs 整个项目都按照 ANSI C (C89) 编写。与存储器 I/O 解耦良好,便于移植到 8051、PIC、AVR、ARM、Z80、RX 等小型微控制器中。
下面是关于 FAT 文件系统格式和 FATFS 项目的文档链接。
- FatFs - Generic FAT Filesystem Module
- The basics of FAT filesystem
- Microsoft Extensible Firmware Initiative FAT32 File System Specification
1 如何写入
FATFS 提供了一系列使用例程,通过 FATFS 接口把数据写入文件系统的流程如下:
- 挂载文件系统
f_mount(&FatFs, "", 0); /* Give a work area to the default drive */
- 根据文件路径打开文件
fr = f_open(&Fil, "newfile.txt", FA_WRITE | FA_CREATE_ALWAYS); /* Create a file */
- 把数据写入文件
f_write(&Fil, "It works!\r\n", 11, &bw); /* Write data to the file */
- 关闭文件
fr = f_close(&Fil); /* Close the file */
如上,FATFS 的使用和我们在桌面操作系统上读写文件大差不差,使用这个包可以让我们在在 MCU 上存取数据时获得操作系统级的体验。下面是一个完整的 DEMO.
example
/*----------------------------------------------------------------------*/
/* Foolproof FatFs sample project for AVR (C)ChaN, 2014 */
/*----------------------------------------------------------------------*/#include <avr/io.h> /* Device specific declarations */
#include "ff.h" /* Declarations of FatFs API */FATFS FatFs; /* FatFs work area needed for each volume */
FIL Fil; /* File object needed for each open file */int main (void)
{UINT bw;FRESULT fr;f_mount(&FatFs, "", 0); /* Give a work area to the default drive */fr = f_open(&Fil, "newfile.txt", FA_WRITE | FA_CREATE_ALWAYS); /* Create a file */if (fr == FR_OK) {f_write(&Fil, "It works!\r\n", 11, &bw); /* Write data to the file */fr = f_close(&Fil); /* Close the file */if (fr == FR_OK && bw == 11) { /* Lights green LED if data written well */DDRB |= 0x10; PORTB |= 0x10; /* Set PB4 high */}}for (;;) ;
}
2 f_write()里做了什么
2.1 逐行分析源码
为了搞清楚 FATFS 的写操作逻辑,我们需要逐行分析 f_write() 的实现。
查看源码,立即就能发现 f_write() 调用了一个名为 disk_write 的函数,这很可能就包含着操作 磁盘/SD 卡等存储介质的底层实现。
if (disk_write(fs->pdrv, wbuff, sect, cc) != RES_OK) ABORT(fs, FR_DISK_ERR);
disk_write() 的原型如下:
DRESULT disk_write (BYTE drv, /* Physical drive nmuber (0) */const BYTE *buff, /* Pointer to the data to be written */LBA_t sector, /* Start sector number (LBA) */UINT count /* Sector count (1..128) */
)
参数 buff
和 count
很好理解,就是数据缓存和数据大小,无需多言。
参数 sector
则由“簇数” fp->clust
计算得到:
sect = clst2sect(fs, fp->clust); /* Get current sector */
fp->clust
从 fp->obj
中读出, 这个变量在 f_open() 中被初始化。
clst = fp->obj.sclust; /* Follow from the origin */
...
fp->clust = clst; /* Update current cluster */
drv
的值即 disk_write() 的第一个参数 fs->pdrv
的值, fs
在 f_write() 开头的有效性检查 validate() 那里被初始化,初始化时刻如下所示:
static FRESULT validate ( /* Returns FR_OK or FR_INVALID_OBJECT */FFOBJID* obj, /* Pointer to the FFOBJID, the 1st member in the FIL/DIR structure, to check validity */FATFS** rfs /* Pointer to pointer to the owner filesystem object to return */
){
...*rfs = (res == FR_OK) ? obj->fs : 0; /* Return corresponding filesystem object if it is valid */return res;
}
2.2 总结一下 f_write()
做了什么:
- 解析在 f_open() 中初始化的
fp
- 获取给定文件路径对应的簇号(cluster number)并转换成扇区号(sector number)
- 获取文件系统的物理驱动号(即
fp
对应的区域在哪个物理设备/磁盘上) - 以驱动号、扇区号、数据和数据大小为参数调用
disk_write()
3 我们实际上关心的是什么
依据上面的分析,我们已经知道我们可以通过修改 disk_write() 的实现来适配不同的存储设备。
但我们真正关心的是,如何在磁盘上找到某个文件,所以我们还需要分析 f_open() 的实现。
3.2 f_open() 里做了什么
下面是 f_open 运行时的调用树,看起来整个 open 的过程像是在树状结构中做检索。我想那么,也许有必要看看 FAT 文件系统的原始定义,也即文件系统的镜像格式。
f_open()
follow_path()
create_name() // iterator of dir name in file path
dir_find()dir_sdi()
get_fat() // get fat32 entry, ret cluster number
clst2sect() // transfrom cluster number to section number
回到文件系统的镜像格式
FAT(File Allocation Table),最早在DOS v1.0 中被引入,是一种极简的文件系统,占用空间,是目前最常见的文件系统之一。FAT 文件系统有多种历史版本,比如 FAT12/FAT16/FAT32/exFAT/VFAT,这里只介绍 FAT32,
使用 FAT 管理的存储介质分为三个基本区域:
- 启动记录 (The boot record)
- 文件分配表 (FAT,The File Allocation Table)
- 目录和数据区(The directory and data area)
“The boot record”(引导记录)是指存储在磁盘的第一个扇区的特殊区域。它也被称为引导扇区(boot sector)或主引导记录(master boot record,MBR)。
FAT32 的主引导扇区可分为两部分,前 36 字节与其他版本的 FAT 一致,36 字节以后的区域划分如下图所示。
origin_url=.%2Fbehind36.png&pos_id=img-nzYYNo5c-1710653498658)
偏移 44 字节处为根目录所在的扇区(通常为2),打开文件系统镜像,转到对应扇区,可见目录中的文件列表如下。
额外提一句,FAT 文件系统中的时间记录以 1980 年 1 月 1 日为基准,如上图偏移0x10位置的两个字节为0X3C21,高 7 位表示年,数值为30,对应的年份为 1980+30=2010 年。