FatFs是一个通用的文件系统(FAT/exFAT)模块,用于在小型嵌入式系统中实现FAT文件系统。 FatFs 组件的编写遵循ANSI C(C89),完全分离于磁盘 I/O 层,因此不依赖于硬件平台。它可以嵌入到便宜的微控制器中,如 8051, PIC, AVR, ARM, Z80, RX等等,不需要做任何修改。
简单地说,FatFS将数据文件状态进行了处理封装,开发者只要调用对应api即可对文件进行便捷操作。
整个文件系统包含doc和src两个文件夹,顾名思义,doc为开源系统文档,src为源码。这里我们重点分析src文件夹,如图。
如上图,为src文件夹的内容。其中option文件夹包含支持各语言所需的文件,如cc936.c文件可用于支持简体中文。00history.txt和00readme.txt文件用于说明版本更迭历史及系统介绍,这里不多赘述。其他还有6个源文件,下面详细说明。
integer.h: FatFS的数值类型定义,包括对常用的变量类型的重定义;
diskio.c: 包含底层存储介质的操作函数,为半完成状态,需要用户自行补足实现。主要是在各函数里调用用户实现的底层驱动函数,如SPI读写flash的操作函数;
diskio.h: diskio.c文件的函数原型声明及各个宏定义;
ff.c: FatFS核心文件,通过调用diskio.c的底层操作函数实现文件的读写等操作。该文件独立于底层介质操作文件,移植系统时不需要修改;
ff.h: 相关文件/文件夹的结构体定义,宏定义,及ff.c文件的函数原型声明等;
ffconf.h: 用于FatFS的功能配置,通过宏实现条件编译,修改相应宏定义的值即可实现功能的裁剪。如,需要支持简体中文,则将宏_CODE_PAGE的值修改为“936”,并把cc936.c文件添加到工程中即可。
FatFs 提供下面的函数:
(用户函数,顶层)
f_mount - 注册/注销一个工作区域
f_open - 打开/创建一个文件
f_close - 关闭一个文件
f_read - 读文件
f_write - 写文件
f_lseek - 移动文件读/写指针
f_truncate - 截断文件
f_sync - 冲洗缓冲数据
f_opendir - 打开一个目录
f_readdir - 读取目录条目
f_getfree - 获取空闲簇
f_stat - 获取文件状态
f_mkdir - 创建一个目录
f_unlink - 删除一个文件或目录
f_chmod - 改变属性
f_utime - 改变时间戳
f_rename - 重命名/移动一个文件或文件夹
f_mkfs - 在驱动器上创建一个文件系统
f_forward - 直接转移文件数据到一个数据流
f_gets - 读一个字符串
f_putc - 写一个字符
f_puts - 写一个字符串
f_printf - 写一个格式化的字符磁盘I/O接口
因为FatFs模块完全与磁盘I/O层分开,因此需要下面的函数来实现底层物理磁盘的读写与获取当前时间。底层磁盘I/O模块并不是FatFs的一部分,并且必须由用户提供。资源文件中也包含有范例驱动。
(驱动函数,底层,控制存储器)
disk_initialize - 初始化磁盘驱动器
disk_status - 获取磁盘状态
disk_read - 读扇区
disk_write - 写扇区
disk_ioctl - 设备相关的控制特性
get_fattime - 获取当前时间
1、需准备的文件:
FATFS文件:
将SD卡驱动封装成两个函数
通过修改diskio.c,使FATFS文件调用这两个函数,完成与内存驱动的连接,随后在其他地方调用ff.c的API函数,即可使用FATFS。
2、添加工程文件
将diskio.c\ff.c\cc936.c\sdio_sdcard.c添加入工程即可
3、注意
正点原子的STM32例程里添加了malloc.c\exfuns.c\fattester.c文件,这几个是辅助用的文件,其实可以不加,只是加了可以比较方便的实现一些功能,它其实是对ff.c文件的进一步封装。
在SPI读写flash的实验基础上移植FatFS。
(例程不是我的,我用的是SD卡,排版很乱所以不放了)
将FatFS的“src”文件夹拷贝到项目USER文件夹下,并重命名;
在MDK里配置,将FatFS组件添加到工程中,需要添加的文件是ff.c、diskio.c及cc936.c三个文件;
添加FatFS组件的头文件包含路径。
接下来需要对diskio.c和ffconf.h文件进行修改及配置了。
diskio.c
diskio.c文件里的函数会被ff.c的函数调用,用于控制底层存储介质flash的读写操作。文件里有5个半完成状态的函数,其调用用户实现的底层驱动函数。在补足5个函数之前,需要先定义好各个物理设备的编号。
#define SPI_FLASH 1 // SPI FLASH的编号
通过传入不同物理设备的宏,分别对相应设备进行不同的操作。
#define sFLASH_ID 0XEF4017 // w25q64的ID
接下来逐个分析函数。
/*
* @brief 获取设备状态
* @param pdrv:物理设备编号
* @retval 0:设备ID读取成功 STA_NOINIT:设备ID读取失败
*/
DSTATUS disk_status(BYTE pdrv)
{
DSTATUS status = STA_NOINIT; // 设备状态
switch(pdrv)
{
case SPI_FLASH:
if(SPI_FLASH_ReadID() == sFLASH_ID)
{
status = 0; // 设备ID读取成功
}
else
{
status = STA_NOINIT;// 设备ID读取失败
}
break;
default:
status = STA_NOINIT;
}
return status;
}
disk_status ()函数很简单,传入需要操作的物理设备的编号pdrv,在switch里根据编号的不同执行对应设备的代码。这里通过获取设备ID的方式返回设备状态。
/**
* @brief 设备初始化
* @param pdrv:物理设备编号
* @retval 0:设备初始化成功 STA_NOINIT:设备初始化失败
*/
DSTATUS disk_initialize(BYTE pdrv)
{
DSTATUS status = STA_NOINIT;
uint16_t i = 500;
switch(pdrv)
{
case SPI_FLASH:
SPI_FLASH_Init();
while(i--);
SPI_Flash_WAKEUP(); // 唤醒SPI FLASH
status = disk_status(SPI_FLASH);
break;
default:
status = STA_NOINIT;
}
return status;
}
通过调用SPI_FLASH_Init()函数进行设备初始化,之后调用disk_status()函数获取设备ID,进而判断是否初始化完成。
/**
* @brief 扇区读取
* @param
* @arg pdrv:物理设备编号
* @arg buff:数据缓冲区指针
* @arg sector:扇区首地址
* @arg count:即将读取的扇区数量
* @retval RES_OK:读取成功 RES_PARERR:无效值
*/
DRESULT disk_read(BYTE pdrv, BYTE *buff, DWORD sector, UINT count)
{
DRESULT status = RES_PARERR;
switch(pdrv)
{
case SPI_FLASH:
sector += 512; // 偏离512个扇区
SPI_FLASH_BufferRead(buff, sector<<12, count<<12); // 左移12位,即乘以2^12
status = RES_OK;
break;
default:
status = RES_PARERR;
}
return status;
}
这里用的flash芯片有8M字节,每个扇区4096字节,我们把前面2M字节空出用于其他,后面6M字节给FatFS使用。那么,需要将所有的地址都偏移2M,即512个扇区的空间。
/**
* @brief 扇区读取
* @param
* @arg pdrv:物理设备编号
* @arg buff:待写入数据的缓冲区指针
* @arg sector:扇区首地址
* @arg count:写入的扇区数量
* @retval RES_OK:写入成功 RES_PARERR:无效值
*/
DRESULT disk_write(BYTE pdrv, const BYTE *buff, DWORD sector, UINT count)
{
DRESULT status = RES_PARERR;
uint32_t write_addr;
switch(pdrv)
{
case SPI_FLASH:
sector += 512;
write_addr = sector<<12;
SPI_FLASH_SectorErase(write_addr); //写入前先擦除扇区
SPI_FLASH_BufferWrite((u8 *)buff, sector<<12, count<<12);
status = RES_OK;
break;
default:
status = RES_PARERR;
}
return status;
}
和读取函数类似,需要偏移512扇区再进行擦除扇区及写入。
/**
* @brief 其他控制
* @param
* @arg pdrv:物理设备编号
* @arg cmd:控制指令
* @arg buff:写入或读取的数据地址指针
* @retval RES_OK:操作成功 RES_PARERR:无效值
*/
DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff)
{
DRESULT status = RES_PARERR;
switch (pdrv)
{
case SPI_FLASH:
switch(cmd)
{
/* 扇区数量:6MB*1024*1024/4096=1536 */
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;
}
ffconf.h
这个文件主要是FatFS的功能配置的宏定义,下面几个宏是需要修改的。
#define _USE_MKFS 1 // 格式化选择
#define _CODE_PAGE 936 // 语言选择
#define _USE_LFN 2 // 长文件名支持
#define _VOLUMES 2 // 物理设备数量
#define _MIN_SS 512 // 指定扇区的最小值
#define _MAX_SS 4096 // 指定扇区的最大值
相关的宏定义配置在每个宏之后都有做注释,可以阅读源文件。
移植至此,基本上就完成了,可以执行编译,不会有错误了。接下来可以尝试对其进行测试。在main.c文件里测试。
需要先格式化设备并申请文件信息存储区,然后挂载FatFS文件系统,最后分别进行读写测试。
FATFS fs;// FATFS文件系统结构体对象
FIL fnew;// FIL文件结构体对象
FRESULT res_flash;// 文件操作结果结构体对象
UINT fnum;// 文件读写数量
BYTE ReadBuffer[1024] = {0}; // 读缓冲区
BYTE WriteBuffer[] = "stm32专栏-FatFS测试专用";
// 写缓冲区
挂载文件系统
挂载之前需要先对设备进行格式化操作,用到了f_mount()和f_mkfs()两个函数。
res_flash = f_mount(&fs, "1:", 1);// 挂载时会对SPI设备初始化
if(res_flash == FR_NO_FILESYSTEM) // 判断是否有文件系统,没有则格式化并创建
{
printf("没有检测到文件系统...开始进行格式化...\r\n");
res_flash = f_mkfs("1:", 0, 0); // 格式化
if(res_flash == FR_OK) // 格式化成功
{
printf("已成功格式化\r\n");
res_flash = f_mount(NULL, "1:", 1);// 格式化后先取消挂载
res_flash = f_mount(&fs, "1:", 1); // 重新挂载
}
else
{
printf("格式化失败\r\n");
while(1);
}
}
else if(res_flash != FR_OK)
{
printf("FatFS文件系统挂载失败(%d)\r\n", res_flash);
while(1);
}
else
{
printf("FatFS文件系统挂载成功,可以开始读写测试。\r\n");
}
f_mount()和f_mkfs()函数都在ff.c文件里定义的,用于挂载系统及格式化。
写测试
需要用到f_open()、f_write()、f_close()函数。
/* 打开文件,每次都以新的形式打开,属性为可写 */
res_flash = f_open(&fnew, "1:FatFS读写测试文件.txt", FA_CREATE_ALWAYS | FA_WRITE);
if(res_flash == FR_OK)
{
printf("打开/创建 FatFS读写测试文件.txt 成功。\r\n\r\n");
res_flash = f_write(&fnew, WriteBuffer, sizeof(WriteBuffer), &fnum); // 将写缓冲区内容写入到文件内
if(res_flash == FR_OK)
{
printf("写入的字节数据大小:%d\n", fnum);
printf("写入的内容为:%s\r\n\r\n", WriteBuffer);
}
else
{
printf("写入失败(%d)\n", res_flash);
}
}
else
{
printf("打开/创建文件失败!!!\r\n");
}
f_close(&fnew); // 关闭文件
这三个函数也都在ff.c文件中,至于参数如何也都有注释,这里不再赘述。
读测试
读测试除了用到f_open()、f_close()函数,还用到了f_read()函数。
res_flash = f_open(&fnew, "1:FatFS读写测试文件.txt", FA_OPEN_EXISTING | FA_READ);
if(res_flash == FR_OK)
{
printf("打开 FatFS读写测试文件.txt 成功。开始读取数据。\\r\n\r\n");
res_flash = f_read(&fnew, ReadBuffer, sizeof(ReadBuffer), &fnum);
if(res_flash == FR_OK)
{
printf("读到的字节数据大小:%d\n", fnum);
printf("读到的内容为:%s\r\n\r\n\r\n", ReadBuffer);
}
else
{
printf("文件读取失败:(%d)", res_flash);
}
}
else
{
printf("打开文件失败!!!\r\n");
}
f_close(&fnew);
f_mount(NULL, "1:", 1);
printf("文件系统已被卸载");
读文件的测试和写文件大体相似,参考代码很容易读懂。至此,FatFS文件系统移植并测试完成了。