项目目标:
STM32F413平台实现FATFS文件系统在SPI flash的移植和应用。
FATFS 概述
1、FAT概念
FATFS 是一个完全免费开源的 FAT 文件系统模块,专门为小型的嵌入式系统而设计。它完全用标准 C 语言编写,所以具有良好的硬件平台独立性,可以移植到 8051、PIC、AVR、SH、Z80、H8、ARM 等系列单片机上而只需做简单的修改。它支持 FATl2、FATl6 和 FAT32,支持多个存储媒介;有独立的缓冲区,可以对多个文件进行读/写,并特别对 8 位单片机和 16 位 单片机做了优化。
2、项目目标功能
- 本项目使用的SPI flash型号是GD25LQ128,GD25LQ128将16MB的容量分为 256 个块(Block),每个块大小为 64K 字节,每个块又分为 16个扇区(Sector),每个扇区 4K 个字节,每个扇区又由16个可编程页(Page)组成。 W25Q128 的最少擦除单位为一个扇区,也就是每次必须擦除 4K 个字节。
- 记录设备运行过程中发生的错误并生成错误代码,以自定义的log文件的形式写入SPI flash中存储起来,可用串口发送AT命令用以读取log文件。
- 本项目具体的记录流程:首先是生成一个记录当前最新文件编号的索引文件并默认为1,该文件只记录一个无符号的整型变量,当log文件的大小超出设定值的时候文件编号会递增并新建与编号相对应的log文件;然后根据最新的文件标记写入或读取log文件的数据,当log的文件数量超出设定值的时候,新的log文件将重新指向最开始记录的log文件并覆盖原有的数据。
例程实现和解析:
1、参考例程的结构
- 底层接口:包括存储媒介读/写接口(disk I/O)和供给文件创建修改时间的实时时钟,包含diskio.c的6个底层驱动函数,需要我们根据平台和存储介质编写移植代码。
- 中间层FATFS模块:实现了FAT文件读/写协议,FATFS模块提供的是ff.c和ff.h。除非有必要,使用者一般不用修改,使用时将头文件直接包含进去即可。
- 最顶层是应用层,使用者无需理会FATFS的内部结构和复杂的FAT协议,只需要调用FATFS模块提供给用户的一系列应用接口函数,如f_open,f_read,f_write和f_close等,就可以像在PC上读/写文件那样简单。
2、文件操作接口层描述
- 数据类型定义:在integer.h里面根据编译器去定义数据的类型。
- 选项配置:通过ffconf.h配置需要的FATFS的相关功能
- 用户程序编写:主要是调用ff.c和ff.h中间层的API函数
ffconf.h:FATFS关键配置
- _FS_READONLY:用来配置是不是只读。
- _USE_STRFUNC:用来设置是否支持字符串类操作。
- _USE_MKFS:用来定时是否使能f_mkfs()函数 。
- _USE_FASTSEEK:用来使能快速定位。
- _USE_LABEL:用来设置是否支持磁盘盘符(磁盘名字)读取与设置。
- _CODE_PAGE:用于设置语言类型,设置为936可支持简体中文(GBK码,需要c936.c文件支持)。
- _USE_LFN:该选项用于设置是否支持长文件名(需要_CODE_PAGE支持)。
- _VOLUMES:用于设置FATFS支持的逻辑设备数目。
- _MAX_SS:扇区缓冲的最大值,一般设置为512。
if.h:变量类型定义
类型 | 含义 |
---|---|
FATFS | 文件系统对象的结构体 |
FIL | 文件对象的结构体 |
DIR | 目录对象的结构体 |
FILINFO | 文件信息的结构体 |
FRESULT | 文件函数返回码 |
if.c:常用的API函数
函数 | 功能 |
---|---|
f_mount | 注册工作区域 |
f_open | 打开/创建文件 |
f_close | 关闭文件 |
f_write | 写入文件 |
f_read | 读取文件 |
f_mkfs | 在逻辑驱动器上创建文件系统 |
f_unlink | 删除文件/目录 |
f_getfree | 获取可用簇的数量 |
f_stat | 获取文件信息 |
f_lseek | 文件读写指针偏移 |
f_tell | 读取文件当前读写指针的位置 |
f_size | 读取文件大小 |
3、构建上层应用接口flog.c和flog.h文件
设备初始化:扫描和识别flash为其注册工作区域
int InitFileDisk(void)
{
//在外部SPI Flash挂载文件系统,文件系统挂载时会对SPI设备初始化
res_flash = f_mount(&fs,"1:",1);
/*----------------------- 格式化测试 ---------------------------*/
/* 如果没有文件系统就格式化创建创建文件系统 */
// if(1)//强制格式化
if(res_flash == FR_NO_FILESYSTEM)
{
format_file_system();
}
else if(res_flash!=FR_OK)
{
printf("!!外部Flash挂载文件系统失败。(%d)\r\n",res_flash);
printf("!!可能原因:SPI Flash初始化不成功。\r\n");
file_system_ok = 0;
//while(1);
}
else
{
printf("》文件系统挂载成功,可以进行读写测试\r\n\r\n");
file_system_ok = 1;
}
//清除文件log
//Clear_log();
return file_system_ok;
}
格式化文件系统
int format_file_system(void)
{
if(f_mkfs("1:",0,4096) == FR_OK)
{
printf("》FLASH已成功格式化文件系统。\r\n");
/* 格式化后,先取消挂载 */
res_flash = f_mount(NULL,"1:",1);
/* 重新挂载 */
res_flash = f_mount(&fs,"1:",1);
file_system_ok = 1;
}
else
{
printf("《《格式化失败。》》\r\n");
file_system_ok = 0;
return 1;
}
return 0;
}
根据索引文件判断写入log文件的位置并写入数据
u8 flog_write(UINT err)
{
char log_name[10]={0};
char code_str[20]={0};
UINT last=0;
if((file_system_ok != 1)||(!flog_enable))
return 1;
//拼接log数据字符串,格式为xxxx+xx+xx+xx+xx+xx+'\t'+err+'\n'
sprintf(code_str,"%s\t%03d\n",GetRtcFormatTime(),err);
LOG(code_str);
printf("写log文件开始... \r\n");
//读取编号文件
res_flash = file_read(_COUNT_FILE_NAME_,&frnum,sizeof(frnum));
if(res_flash)
{
if(res_flash == 4) //编号文件不存在
{
printf("无索引文件\r\n");
//没有索引文件默认0,存在的log数据为无效数据
frnum = 0;
//建立文件索引并写入内容1
if(file_write(_COUNT_FILE_NAME_,&frnum,0,sizeof(frnum)))
{
printf("创建并写入索引文件失败\r\n");
return 1;
}
printf("向编号文件写入的编号为:%d\r\n",frnum);
//合并log0.txt的文件名
sprintf((char*)log_name,"%s%d.txt",_LOG_FILE_NAME_,frnum);
//删除可能存在的编号1文件
f_unlink(log_name);
}
else //操作失败
{
return 2;
}
}
else
{
last =frnum%FILE_NUM_MAX;
sprintf((char*)log_name,"%s%d.txt",_LOG_FILE_NAME_,last);
}
printf("合并的log文件名:%s [%d]\n",log_name,last); //打印合并的log文件名
//读log文件大小
res_flash = file_read_size(log_name,&log_write_frnum);
printf("获取到log文件大小:%d res_flash=%d \n",log_write_frnum,res_flash);
//判断log文件存在
if(res_flash == 0 )
{
if(log_write_frnum >= FILE_SIZE_MAX )//超出大小
{
//新建编号加1的log文件
frnum++;
if(frnum == 0xffffffff)
{
//防止溢出
frnum=0xffffffff%FILE_NUM_MAX;
}
//更新索引文件编号
if(file_write(_COUNT_FILE_NAME_,&frnum,0,sizeof(frnum)))
{
printf("更新索引文件编号失败\r\n");
return 1;
}
printf("更新索引文件编号为:%d\r\n",frnum);
last = frnum%FILE_NUM_MAX;
//拼接新的log文件名
sprintf((char*)log_name,"%s%d.txt",_LOG_FILE_NAME_,last);
//删除新编号文件
f_unlink(log_name);
//文件写指针偏移量清0
log_write_frnum= 0;
}
}
else if(res_flash == 4) //文件不存在
{
//文件写指针偏移量清0
log_write_frnum= 0;
}
else //异常
{
return 3;
}
//写入log文件,文件不存在则新建并写入
if(file_write(log_name,code_str,log_write_frnum,sizeof(code_str)-1))//去掉结尾的'\0'字符
{
printf("写log文件失败\r\n");
return 4;
}
return 0;
}
根据索引文件判断按时间顺序读取全部的log文件数据
//读指定编号的log文件
static u8 flog_read_select(UINT f_num,char *buff)
{
UINT log_size=0; //log文件大小
char log_name[10]={0};
//printf("读指定编号的log文件... \r\n");
//合并log的文件名
sprintf((char*)log_name,"%s%d.txt",_LOG_FILE_NAME_,f_num);
//printf("合并的log文件名:%s\n",log_name); //打印合并的log文件名
//读log文件大小,根据文件大小读取全部数据
res_flash = file_read_size(log_name,&log_size);
//printf("获取到log文件大小:%d res_flash=%d \n",log_size,res_flash);
if(res_flash || file_read(log_name,buff,log_size))
{
return 3;
}
return 0;
}
//读取所有的log文件
u8 flog_read_all(char *buff)
{
UINT last=0,i,n=0;
char log_name[10]={0};
UINT f_rnum; //索引文件编号
char* pbuf = buff;
if((file_system_ok != 1)&&(sizeof(buff) <= FILE_READ_SIZE))
return 1;
//读取编号文件
if(file_read(_COUNT_FILE_NAME_,&f_rnum,sizeof(f_rnum)))
{
return 2;
}
flog_enable=0;
last = f_rnum%FILE_NUM_MAX;
//合并log的文件名
sprintf((char*)log_name,"%s%d.txt",_LOG_FILE_NAME_,last);
printf("合并的log文件名:%s\n",log_name); //打印合并的log文件名
if(f_rnum < FILE_NUM_MAX) //文件后面没有有效的数据
{
for(i=0;i <= last;i++)
{
flog_read_select(i,pbuf);
pbuf += FILE_SIZE_MAX;
}
}
else //从最早的log开始读
{
for(i=last+1;n < FILE_NUM_MAX;i++,n++)
{
if(i == FILE_NUM_MAX)
i=0;
flog_read_select(i,pbuf);
pbuf += FILE_SIZE_MAX;
}
}
flog_enable=1;
//printf("log%d.txt文件数据为\r\n%s\r\n",i,buff);
return 0;
}
小结:
- 调试过程中更改了disk.c的硬件配置后,重新下载测试文件系统时会出错,原因是没有格式化之前的文件系统数据导致调用API函数的时候出现异常。
- 调用close()函数关闭文件之后再open()打开文件时读写指针会指向数据开始的位置,直接读写会覆盖原有的数据,在文件尾添加数据时需要偏移文件读写指针。
- 文件系统支持中文编码需添加c936.c文件并配置 _CODE_PAGE定义,支持中文编码会占用大量的空间和延长下载程序的时间。
- 文件占用空间以簇为单位,文件大小不足1簇的其占用空间也为1簇。