上节学习了FatFs文件系统的相关知识,这节内容继续学习在STM32上如何移植FatFs文件系统,并且实现文件的创建、读、写与删除等功能。各位看官觉得还行的话点点赞,收藏一下呗。
移植还是在之前学习过程中一直之用的模板,一点点的在自建的工程上逐步完善整个STM32的板级支持包。
将ff11a版本的文件夹复制到工程文件夹的User目录下,里面的Doc文件夹是FatFs的说明文档,这里不需要,可以直接删除掉了。
将在工程文件中,新增FatFs文件组,并将ff.c、diskio.c、ccsbcs.c文件加入到目录下。(ccsbcs.c暂时可以不用添加,这个项目中暂时用不到,添加了编译也会报错)
以上就完成了向工程中添加FatFs源码的步骤,下来我们对FatFs文件系统进行裁剪和修改。
这个跟Windows上基本上是一致的,大家可以在Windows中进入磁盘管理看一下,比如我的电脑是双硬盘,两个磁盘的物理编号就分别是0和1
在FatFs文件系统的源码中,已经存在了3个设备编号,分别是ATA、MMC和USB,对应编号为0-2,这里我们新增FLASH的编号为3(注意,这里如果直接宏定义为FLASH的话,会和说stm32f4xx.h中重定义,所以这里宏定义为SPI_FLASH)
/* Definitions of physical drive number for each drive */
#define ATA 0 /* Example: Map ATA harddisk to physical drive 0 */
#define MMC 1 /* Example: Map MMC/SD card to physical drive 1 */
#define USB 2 /* Example: Map USB MSD to physical drive 2 */
#define SPI_FLASH 3 /* Example: Map FLASH to physical drive 3 */
2.1 获取驱动器状态函数DSTATUS disk_status (BYTE pdrv)
/**
* @brief 获取驱动器状态
* @param 驱动器编号
* @retval 驱动器状态
*/
DSTATUS disk_status (BYTE pdrv)
{
DSTATUS stat;
switch (pdrv) {
case ATA :
return stat;
case MMC :
return stat;
case USB :
return stat;
case SPI_FLASH : /* 这里获取FLASH的状态信息,我们之前在实现SPI读写FLASH的时候,可以用检测ID号判断设备是否能正常读取来判断 */
if(SPI_FLASH_ReadID() == sFLASH_ID)
{
return 0; /* 若设备能正常读取ID号,则返回0 */
}
else
{
return STA_NOINIT; /* 若读不到ID号,则返回驱动器未初始化,及为0x01 */
}
}
return STA_NOINIT;
}
2.2 设备初始化,这里实际上就是对SPI总线的配置,只需要调用我们之前实现的SPI配置函数即可。
/**
* @brief 初始化设备
* @param 驱动器编号
* @retval 驱动器状态
*/
DSTATUS disk_initialize (BYTE pdrv)
{
DSTATUS stat ;
u32 status = STA_NOINIT;
switch (pdrv) {
case ATA :
return stat;
case MMC :
return stat;
case USB :
return stat;
case SPI_FLASH :
SPI_GPIO_Config(); /* 这里直接调用我们对SPI总线的配置函数即可 */
SPI_Flash_WakeUp(); /* 为了防止在其他地方对SPI设置为掉电模式,这里直接增加一条强置唤醒 */
status = SPI_FLASH_ReadID(); /* 同样,这里以读取FLASH ID的方式判断设备是否上线 */
if(status == sFLASH_ID)
{
return 0;
}
else
{
return STA_NOINIT;
}
}
return STA_NOINIT;
}
2.3 扇区读写
/**
* @brief 读扇区
* @param
pdrv:驱动器编号
buff:读取到的数据存储缓冲区
sector:读取的目标扇区号
count:读取扇区数量
* @retval 驱动器状态
*/
DRESULT disk_read (BYTE pdrv,BYTE *buff,DWORD sector,UINT count)
{
DRESULT res;
switch (pdrv) {
case ATA :
return res;
case MMC :
return res;
case USB :
return res;
case SPI_FLASH :
/* 这里因为我用的是野火的F429开发板,厂家在出场的时候在FLASH中已经存储
了开机例程所需要的一些字库文件以及图片文件,因此根据厂家给出的扇区分布对
我们可以使用的扇区进行了偏移,具体偏移量看下面的表格。
*/
sector += 1536;
/* FatFs文件系统是以扇区读写内容,我们实现的Flash读写是以字节读取,因此这里将扇区转换为字节位置,数量转换为字节 */
SPI_Flash_ReadDate(buff, sector * 4096, count * 4096);
return RES_OK;
}
return RES_PARERR;
}
厂家FLASH预留的FLASH存储设置,前1536个扇区已被使用,如果后面需要厂家预留的文件,只使用后面10M的空间就行,不需要的话可以全部擦除。
序号 | 文件名/工程 | 功能 | 起始地址 | 长度 |
---|---|---|---|---|
1 | 外部flash读写例程 | 预留给裸机Flash测试 | 0 | 4096 (BYTE) |
2 | 预留 | 预留 | 1*4096 | 59*4096 (BYTE) |
3 | app.c | XBF字库文件(emWin使用,新宋体25.xbf) | 60*4096 | 649*4096(1.23MB) |
4 | app.c | XBF字库文件(emWin使用,新宋体19.xbf) | 710*4096 | 529*4096(172KB) |
4 | firecc936.c | 文件系统中文支持字库(emWin使用,UNIGBK.BIN) | 1240*4096 | 43*4096(172KB) |
5 | EMW1062模块 | WIFI模块固件(BCM43362-5.90.230.12.bin) | 1284*4096 | 62*4096(248KB) |
5.1 | EMW1062模块 | WIFI模块参数1(预留,不需要写文件) | 1347*4096 | 1*4096(4KB) |
5.2 | EMW1062模块 | WIFI模块参数2(预留,不需要写文件) | 1348*4096 | 1*4096(4KB) |
6 | 裸机中文显示例程 | 裸机中文字库(GB2312_H2424.FON) | 1360*4096 | 144*4096(576KB) |
7 | diskio.c | FATFS文件系统(emWin使用) | 1536*4096 | 2560*4096(10MB) |
2.4 数据写入,与读取基本相似,需要注意的是,该函数受到写保护限制,使用前需要先看一下_USE_WRITE
是否为1。
/**
* @brief 写扇区
* @param
pdrv:驱动器编号
buff:写入的数据存储缓冲区
sector:写入的目标扇区号
count:写入扇区数量
* @retval 驱动器状态
*/
#if _USE_WRITE
DRESULT disk_write (BYTE pdrv,const BYTE *buff, DWORD sector,UINT count)
{
DRESULT res;
switch (pdrv) {
case ATA :
return res;
case MMC :
return res;
case USB :
return res;
case SPI_FLASH :
/* 前6M空间存储出厂的一些数据文件,因此这里偏移一部分 */
sector += 1536;
SPI_Flash_Erase(sector * 4096); /* 写入之前需要擦除数据 */
SPI_FLASH_BufferWrite((u8 *)buff, sector * 4096, count * 4096);
return RES_OK;
}
return RES_PARERR;
}
#endif
2.5 控制函数
/**
* @brief 控制函数
* @param
pdrv:驱动器编号
cmd:控制命令
buff:指向缓冲区的指针,取决于命令代码,不使用时可以传个空指针进去
* @retval 控制状态
*/
#if _USE_IOCTL
DRESULT disk_ioctl (BYTE pdrv,BYTE cmd,void *buff)
{
DRESULT res;
switch (pdrv) {
case ATA :
return res;
case MMC :
return res;
case USB :
return res;
case SPI_FLASH :
switch(cmd)
{
case CTRL_SYNC: /* 是否使用缓存功能,这里我们不适用,所以留空 */
break;
case GET_SECTOR_COUNT: /* 获取磁盘的可用扇区数 */
/* 前6M空间存储出厂的一些数据文件,因此这里偏移一部分 */
*(DWORD *)buff = 4096 - 1536;
break;
case GET_SECTOR_SIZE: /* 返回磁盘的扇区大小,FLASH中一个扇区为4096个字节,所以这里直接返回4096即可 */
*(WORD *)buff = 4096;
break;
case GET_BLOCK_SIZE: /* 获取擦除块的大小,一般我们擦除的时候都是整个扇区擦除,此处直接返回1即可 */
*(DWORD *)buff = 1;
break;
}
return res;
}
return RES_PARERR;
}
#endif
FatFs文件系统默认支持设备为1个,我们在上面增加了SPI_FLASH后,一共为4个,所以这里需要将支持设备数量改为4.
#define _VOLUMES 4 /* 将默认设备数量更改为4个 */
FLASH的扇区大小为4096,这里直接改为4096即可。
#define _MIN_SS 4096 //这里默认为512,在学习野火的教程时,我看火哥对此项没有更改,但是后面测试的时候会有问题,更改为4096后正常了
#define _MAX_SS 4096
到此文件系统的一直基本完成,需要主义的是,我们在上面对diskio.c文件的修改,使用到了之前我们实现的SPI读写FLASH部分函数,因此需要在diskio.c中也包含#include "bsp_spi_flash.h"头文件,并且源文件中还有usbdisk.h
、atadrive.h
、sdcard.h
的文件我们用不到,这里需要删掉。
另外,在编译的过程中会出现一个错误:
Error: L6218E: Undefined symbol get_fattime (referred from ff.o).
我们暂时用不到获取文件时间的功能,所以这里就自己写一个get_fattime
函数来骗过编译器,等到后面深度学习文件系统的时候再考虑实现。
DWORD get_fattime(void)
{
return 0;
}
OK,到这里文件系统的移植和裁剪已经初步完成,我们来测试一下。
文件系统使用时,首先需要再系统中注册设备,这里就是用到了f_mount
,与Windows系统一样,我们再挂载设备的时候,需要知道设备的盘符,比如:C:\Windows\1.txt
,这里的C就是设备的盘符,我们在diskio.c中将SPI_FLASH宏定义为3,那这里对应的SPI_FLASH的盘符就是3。
FATFS flash_fs;
int main(void)
{
DEBUG_USART1_Config();
printf("\r\n这是一个文件系统移植例程实验\r\n");
res = f_mount(&flash_fs,"3:",1);
printf("\r\nf_mount res = %d\r\n",res);
if(FR_OK == res)
{
printf("\r\n文件系统挂载成功!\r\n");
}
else
{
printf("\r\n设备挂载失败,失败代码:error = %d\r\n",res);
}
while(1)
{
}
}
f_mount的返回值如下:
返回值 | 说明 |
---|---|
FR_OK | 0:成功 |
FR_DISK_ERR | 1:磁盘I/O层中发生硬错误 |
FR_INT_ERR | 2:断言失败 |
FR_NOT_READY | 3:物理驱动器无法工作 |
FR_NO_FILE, | 4:找不到文件 |
FR_NO_PATH | 5:找不到路径 |
FR_INVALID_NAME | 6:路径格式无效 |
FR_DENIED | 7:禁止访问或目录已满,访问被拒绝 |
FR_EXIST | 8:禁止访问,访问被拒绝 |
FR_INVALID_OBJECT | 9:文件/目录对象无效 |
FR_WRITE_PROTECTED | 10:物理驱动器写保护 |
FR_INVALID_DRIVE | 11:逻辑驱动器号无效 |
FR_NOT_ENABLED | 12:卷没有工作区域 |
FR_NO_FILESYSTEM | 13:没有有效的FAT卷 |
FR_MKFS_ABORTED | 14:f_mkfs()由于任何参数错误而中止 |
FR_TIMEOUT | 15:无法在定义的时间段内获得访问卷的授权 |
FR_LOCKED | 16:根据文件共享策略,该操作被拒绝 |
FR_NOT_ENOUGH_CORE | 17:无法分配LFN工作缓冲区 |
FR_TOO_MANY_OPEN_FILES | 18:打开的文件数>_FS_LOCK |
FR_INVALID_PARAMETER | 19:给定的参数无效 |
回到上面关于扇区大小的设置里面看一下,我刚开始在测试的时候,只修改了_MAX_SS
为4096,没有修改_MIN_SS
,导致返回值为1(FR_DISK_ERR),后来尝试修改_MIN_SS
为4096后正常了,到现在也没搞明白具体是因为什么,只有等到后面将FatFs文件系统全部搞清楚后,才能知道,这里先留做一个疑点吧。
设备挂载成功后,就打开文件了。这里使用到的函数主要有:
FRESULT f_open (
FIL* fp, /* [OUT] 指向文件对象的指针 */
const TCHAR* path, /* [IN] 文件名 */
BYTE mode /* [IN] 模式 */
);
关于文件指针的相关知识点,各位在学习C语言的时候应该有涉及到,这里就不详细讲了。
在FatFs文件系统中,文件名与DOS/Windows下基本是一致的,这里指的文件名不只是单一文件的名称,而是包含文件的绝对路径地址,其文件名格式如下:
[drive#:][/]directory/file
比如我们在这里Flash的卷标为3,在根目录下存在一个1.txt,那么文件名就应该为:3:1.txt,官方在这里给出了一些文件名的写法,大家可以参考一下:
Path name | FF_FS_RPATH == 0(绝对路径) | FF_FS_RPATH >= 1(相对路径) |
---|---|---|
file.txt | 驱动器0个目录中的file.txt文件 | 当前所在驱动器的当前所在目录下的file.txt文件 |
/file.txt | 驱动器0个目录中的file.txt文件 | 当前所在驱动器的根目录下的file.txt文件 |
驱动器0的根目录 | 当前所在驱动器的当前目录 | |
/ | 驱动器0的根目录 | 当前所在驱动器的根目录 |
2: | 驱动器2的根目录 | 驱动器2的当前所在目录 |
2:/ | 驱动器2的根目录 | 驱动器2的根目录 |
2:file.txt | 驱动器2根目录中的file.txt文件 | 驱动器2的当前所在目录下的file.txt文件 |
…/file.txt | 无效路径/文件名 | 父目录的file.txt文件,即上级目录中的file.txt文件 |
. | 无效路径/文件名 | 本目录 |
… | 无效路径/文件名 | 当前目录的父目录(上级目录) |
dir1/… | 无效路径/文件名 | 当前目录 |
/… | 无效路径/文件名 | 根目录 |
文件打开的模式,这里跟C语言的文件操作模式基本类似,这里也列出来各位学习一下。
Flags | Meaning |
---|---|
FA_READ | 指定对文件的读取访问权限。可以从文件中读取数据。 |
FA_WRITE | 指定对文件的写入访问权限。数据可以写入文件。与“FA_READ”组合用于读写访问。 |
FA_OPEN_EXISTING | 打开一个文件。如果文件不存在,函数将返回失败。(默认) |
FA_CREATE_NEW | 创建一个新文件。如果文件存在,则返回FR_EXIST 失败信息。 |
FA_CREATE_ALWAYS | 创建新的文件,如果文件已经存在,则会覆盖原文件 |
FA_OPEN_ALWAYS | 打开文件,若文件不存在,则创建该文件 |
FA_OPEN_APPEND | 与“FA_OPEN_ALWAYS”相同,文件打开后,读/写指针设置定位在文件末尾。 |
官方的说明文件中,还针对文件的打开方式仿照我们在PC上编程时做了个对照表。
POSIX | FatFs | 说明 |
---|---|---|
“r” | FA_READ | 只读 |
“r+” | FA_READ | FA_WRITE | 读写 |
“w” | FA_CREATE_ALWAYS | FA_WRITE | 覆盖创建,只写 |
“w+” | FA_CREATE_ALWAYS | FA_WRITE | FA_READ | 覆盖创建,读写 |
“a” | FA_OPEN_APPEND | FA_WRITE | 打开文件,将读写指针定位到末尾,只写 |
“a+” | FA_OPEN_APPEND | FA_WRITE | FA_READ | 打开文件,将读写指针定位到末尾,读写 |
“wx” | FA_CREATE_NEW | FA_WRITE | 创建新文件,只写 |
“w+x” | FA_CREATE_NEW | FA_WRITE | FA_READ | 创建新文件,读写 |
函数的返回值有:FR_OK, FR_DISK_ERR, FR_INT_ERR, FR_NOT_READY, FR_NO_FILE, FR_NO_PATH, FR_INVALID_NAME, FR_DENIED, FR_EXIST, FR_INVALID_OBJECT, FR_WRITE_PROTECTED, FR_INVALID_DRIVE, FR_NOT_ENABLED, FR_NO_FILESYSTEM, FR_TIMEOUT, FR_LOCKED, FR_NOT_ENOUGH_CORE, FR_TOO_MANY_OPEN_FILES
,具体的含义各位可以看一下f_mount那里的表格说明。
文件操作完成后,需要将文件关闭,这个就比较简单了。
FRESULT f_close (
FIL* fp /* [IN] 指向文件对象的指针 */
);
函数的返回值有:FR_OK, FR_DISK_ERR, FR_INT_ERR, FR_INVALID_OBJECT, FR_TIMEOUT
具体的含义各位可以看一下f_mount那里的表格说明。
文件的读写主要使用到两个函数:f_read
和f_write
,关于这两个函数的使用与PC上的用法基本是一致的,这里将函数大致说明一下,等会在程序中我们做测试即可。
先看一下f_read
函数:
FRESULT f_read (
FIL* fp, /* [IN] 指向文件对象的指针 */
void* buff, /* [OUT] 读取数据缓冲区 */
UINT btr, /* [IN] 需要读取的字节数 */
UINT* br /* [OUT] 实际读取到的字节数 */
);
函数的返回值有:FR_OK, FR_DISK_ERR, FR_INT_ERR, FR_DENIED, FR_INVALID_OBJECT, FR_TIMEOUT
。
f_write
函数
FRESULT f_write (
FIL* fp, /* [IN] 指向文件对象的指针 */
const void* buff, /* [IN] 指向写入数据缓冲区的地址 */
UINT btw, /* [IN] 写入的字节数 */
UINT* bw /* [OUT] 指向写入数据数量的指针 */
);
函数的返回值有:FR_OK, FR_DISK_ERR, FR_INT_ERR, FR_DENIED, FR_INVALID_OBJECT, FR_TIMEOUT
。
4、测试
#include "stm32f4xx.h"
#include "bsp_led.h"
#include "bsp_usart_dma.h"
#include "bsp_spi_flash.h"
#include "ff.h"
#include
#include
FATFS flash_fs; //声明文件系统对象
FIL fp; //创建文件
UINT fnum; //接收读/写返回的数量
FRESULT res; //获取返回值
BYTE writeBuffer[] = "\r\n瀛洲学士风流远,中叶唐惭贞观唐。\r灵武拾遗晚羁旅,开元供奉老佯狂。\r戏苕翡翠非伦拟,撼树蚍蜉不揣量。\r赖有元和韩十八,骑麟被发共翱翔。\r\n";
BYTE readBuffer[1024] = {0};
int main(void)
{
DEBUG_USART1_Config();
printf("\r\n这是一个文件系统移植例程实验\r\n");
res = f_mount(&flash_fs,"3:",1); //挂载FLASH,注意卷号应该和diskio.c中宏定义的一致
if(FR_OK == res) //若挂载成功
{
printf("\r\n文件系统挂载成功!\r\n");
f_unlink("3:1.txt"); //删除卷3下的1.txt文件
res = f_open(&fp,"3:1.txt",FA_CREATE_ALWAYS | FA_WRITE );//以只写方式打开卷3下的1.txt文件,这里如果改为FA_CREATE_NEW,就不需要删除了
if(res == FR_OK) //若打开成功
{
printf("\r\n文件打开成功,准备写入数据!\r\n");
res = f_write(&fp,writeBuffer,sizeof(writeBuffer),&fnum); //写入数据
if (res==FR_OK) //若写入成功
{
printf("》文件写入成功,写入字节数据: %d\n",fnum); //写入成功,输出写入的数据量
printf("》向文件写入的数据为: \r\n%s\r\n",writeBuffer);
}
else
{
printf("!!文件写入失败: (%d)\n",res);
}
f_close(&fp); //关闭文件
}
else
{
printf("\r\n文件打开失败,失败代码 = %d\r\n",res); //输出打开失败代码
}
res = f_open(&fp,"3:1.txt",FA_OPEN_EXISTING | FA_READ ); //以只读方式打开文件
if(res == FR_OK) //若文件打开成功
{
printf("\r\n文件打开成功,准备读取数据!\r\n");
res = f_read(&fp,readBuffer,sizeof(readBuffer),&fnum); //读取文件中的内容
if(res==FR_OK) //若读取成功
{
printf("》文件读取成功,读到字节数据:%d\r\n",fnum); //输出读取到的数据量
printf("》读取得的文件数据为:\r\n%s \r\n", readBuffer); //输出读取到的内容
}
else
{
printf("!!文件读取失败:(%d)\n",res);
}
f_close(&fp); //关闭文件
}
else
{
printf("\r\n文件打开失败,失败代码 = %d\r\n",res); //输出打开失败代码
}
}
else
{
printf("\r\n设备挂载失败,失败代码:error = %d\r\n",res); //设备挂载失败,输出失败代码
}
while(1)
{
}
}