STM32CubeMX教程26 FatFs 文件系统 - W25Q128读写

1、准备材料

正点原子stm32f407探索者开发板V2.4

STM32CubeMX软件(Version 6.10.0

keil µVision5 IDE(MDK-Arm

ST-LINK/V2驱动

野火DAP仿真器

XCOM V2.6串口助手

2、实验目标

使用STM32CubeMX软件配置STM32F407开发板使用FatFs中间件通过SPI通信协议对W25Q128芯片进行读写等操作

3、实验流程

3.0、前提知识

关于STM32F407使用SPI通信协议对W25Q128 FLASH芯片读写等操作涉及的SPI通信协议及W25Q128芯片相关知识请读者阅读STM32CubeMX教程20 SPI - W25Q128驱动实验,本实验不再过多介绍

对于容量较小的存储设备可以使用底层库函数直接根据内存地址对设备来进行读写,但是一旦存储设备容量稍大,直接根据地址对设备来进行读写将变得比较困难

这个时候使用文件系统来对存储设备进行各种操作将比较方便,FatFs是适用于小型嵌入式系统的通用 FAT/exFAT 文件系统模块,它与磁盘I/O层完全分离,可以独立于硬件平台,因此非常方便移植

STM32CubeMX Version 6.10.0 中在中间件和软件包 Middleware and Software Packs 中集成了R0.12c 版本的FatFs文件系统模块,这个中间件支持Extemal SRAM、SD Card、USB Disk和User-defined四种模式

其中外部SRAM需要启用FSMC连接SRAM功能后才可以勾选,SD卡需要启用SDIO功能之后才可以勾选,USB Disk需要配置USB为大容量存储主机类功能后才可以勾选,User-defined则任何时候都可以勾选,将FatFs配置在User-defined模式下就可以利用FatFs使用除上述提到的三种存储之外的设备,比如SPI FLASH等

我们可以通过其官网FatFs历史版本记录找到 R0.12c 版本的FatFs源码,将其下载下来之后观察其源码目录结构如下图所示

其中ff.c/ff.h为FatF模块的源码;fconf.h文件为模块配置文件,可以通过宏定义来选择哪些功能开启,哪些功能关闭;integer.h文件为变量类型重命名文件,主要是为了兼容不同的变量类型命名;option文件夹中的文件为unicode编码文件和操作系统相关函数的文件;上面提到的这些文件用户一般无需修改

如果读者希望手动移植FatFs到自己的嵌入式系统上,则应重点关注源码中diskio.c/diskio.h两个文件,这两个文件中需要根据用户使用的RAM、MMC和USB这几个不同的内存类型来实现以下几个底层函数,函数如下列表所示,完成之后就可以直接通过FatFs提供的上层应用接口(eg:f_open())来对底层的存储设备进行操作

  1. 存储设备状态读取函数disk_status()
  2. 存储设备初始化函数disk_initialize()
  3. 存储设备读函数disk_read()
  4. 存储设备写函数disk_write()
  5. 存储设备IO控制操作函数disk_ioctl()

但是如果要使用 STM32CubeMX 配置的话就不需要自己下载和移植源码,通过配置 STM32CubeMX 的 FatFs ,在生成的工程代码中就已经将 FatFs 的框架准备好,用户只需在生成的 user_diskio.c 文件中添加底层驱动IO函数即可(仅仅对于 User-defined 模式需要自己添加,其他的模式底层代码会自动生成),具体请阅读本实验”3.2.3、添加其他必要代码“小节

在 FatFs 中,大多数的API都拥有一个名为 FRESULT 的结构体返回值,其包含了20个枚举对象,由于该返回值对于查找错误有很大帮助,因此笔者在这里列出来所有返回值并做了简单解释,具体如下源代码所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef enum {
FR_OK = 0, /* (0) 成功 */
FR_DISK_ERR, /* (1) 在Disk IO层发生硬错误,检查user_diskio.c中代码 */
FR_INT_ERR, /* (2) 参数检查错误 */
FR_NOT_READY, /* (3) 物理驱动器不工作 */
FR_NO_FILE, /* (4) 找不到文件 */
FR_NO_PATH, /* (5) 找不到路径 */
FR_INVALID_NAME, /* (6) 路径名称格式无效,检查是否8.3格式/是否支持长文件名 */
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) 不能分配长文件名工作缓存区 */
FR_TOO_MANY_OPEN_FILES, /* (18) 打开文件个数大于_FS_LOCK */
FR_INVALID_PARAMETER /* (19) 无效参数 */
} FRESULT;

下图所示为带有 FatFs 模块的嵌入式系统的典型但非特定配置的依赖关系图,其中用户只需重点关注和实现”Low level disk I/O layer”,实现之后在实际应用中只需使用 “User Application”中提供的上层应用接口即可 (注释1)

3.1、CubeMX相关配置

3.1.0、工程基本配置

打开STM32CubeMX软件,单击ACCESS TO MCU SELECTOR选择开发板MCU(选择你使用开发板的主控MCU型号),选中MCU型号后单击页面右上角Start Project开始工程,具体如下图所示

开始工程之后在配置主页面System Core/RCC中配置HSE/LSE晶振,在System Core/SYS中配置Debug模式,具体如下图所示

详细工程建立内容读者可以阅读“STM32CubeMX教程1 工程建立

3.1.1、时钟树配置

系统时钟使用8MHz外部高速时钟HSE,HCLK、PCLK1和PCLK2均设置为STM32F407能达到的最高时钟频率,具体如下图所示

3.1.2、外设参数配置

本实验需要初始化开发板上WK_UP、KEY2、KEY1和KEY0用户按键,具体配置步骤请阅读“STM32CubeMX教程3 GPIO输入 - 按键响应

本实验需要初始化USART1作为输出信息渠道,具体配置步骤请阅读“STM32CubeMX教程9 USART/UART 异步通信

本实验需要以STM32CubeMX教程20 SPI - W25Q128驱动实验为基础,需要读者能够通过CubeMX软件配置STM32F407的SPI实现正常读写W25Q128芯片的功能,然后接下来只需要增加本实验需要的中间件FatFs即可

在Pinout & Configuration页面左边的功能分类栏中单击 Middleware and SoftwarePacks/FATFS,然后在右边的Mode下勾选 User-defined (目前只有该参数可以勾选),在下方Configuration/Set Defines对FatFs的功能进行配置,这个页面所有参数对应FatFs源码ffconf.h中的宏定义

这里我们将 CODE_PAGE(Code page on target) 参数修改为 Simplified Chinese (DBCS) ,然后将 MAX_SS (Maximum Sector Size) 参数修改为W25Q128芯片的扇区大小4096字节,其他所有参数不做修改,具体如下图所示

笔者将Set Defines页面的所有参数列为了一个表格,方便做简单介绍,具体如下图所示

3.1.3、外设中断配置

本实验无需配置任何中断

3.2、生成代码

3.2.0、配置Project Manager页面

单击进入Project Manager页面,在左边Project分栏中修改工程名称、工程目录和工具链,接着在链接设置中将最小栈大小修改为0x2000(8KB),之前所有实验该参数都为默认的0x0400(1KB),这是因为其他实验不需要占用太多的栈空间,但是本实验需要比较大的栈空间,不增加可能会导致FatFs读写文件失败卡死或者导致MCU复位的情况发生,读者可根据自己的情况自行设置栈大小

然后在Code Generator中勾选“Gnerate peripheral initialization as a pair of ‘c/h’ files per peripheral”,最后单击页面右上角GENERATE CODE生成工程,具体如下图所示

详细Project Manager配置内容读者可以阅读“STM32CubeMX教程1 工程建立”实验3.4.3小节

3.2.1、外设初始化调用流程

关于SPI的初始化函数调用流程请读者阅读STM32CubeMX教程20 SPI - W25Q128驱动实验的”3.2.1、外设初始化调用流程“小节,在此不再赘述

重点来看看FatFs中间件是如何被初始化并与W25Q128芯片底层操作联系在一起的,首先在CubeMX中勾选启用FatFs中间件之后,会在生成的工程代码中增加MX_FATFS_Init()初始化函数,在该函数中只调用了FATFS_LinkDriver()一个函数

这个FATFS_LinkDriver()函数将一个名为 xxx_Driver(根据所选的存储设备不同,生成的该变量名称也会改变,比如User_Driver,SD_Driver等)的 Diskio_drvTypeDef 类结构体链接到了FatFs管理的驱动器列表中,并将卷路径赋值为”0:/“

那么 xxx_Driver 是什么,为什么要将这个Diskio_drvTypeDef 类结构体的变量链接给FatFs管理呢?

跳转到 xxx_Driver 的定义处,我们发现该结构体变量中保存了五个函数指针,刚刚好就是我们需要实现的对存储设备进行底层读写等操作的函数,具体xxx_Driver定义如下述代码所示

1
2
3
4
5
6
7
8
Diskio_drvTypeDef USER_Driver =
{
USER_initialize,
USER_status,
USER_read,
USER_write,
USER_ioctl,
};

至此我们知道了FatFs初始化就是将用户重新实现的与存储设备底层进行读写等操作的函数链接到FatFs管理的驱动器列表中,将这些底层函数交给FatFs管理,用户直接使用FatFs提供的上层API函数来操作即可,对于为什么可以这样需要分析FatFs源码,本文就不涉及了

3.2.2、外设中断调用流程

本实验无配置任何中断

3.2.3、添加其他必要代码

打开整个工程之后观察其文件结构目录,在CubeMX中启用FatFs之后在生成的工程代码目录中会增加FatFs源码文件夹(该文件夹中文件无需用户修改),同时增加App和Target两个文件夹,在App文件夹中的fatfs.c文件需要用户实现获取RTC时间的函数①get_fattime(),在App文件夹中的user_diskio.c中需要用户实现②USER_initialize()、③USER_status()、④USER_read()、⑤USER_write()和⑥USER_ioctl()共计六个函数,其中USER_initialize(),USER_status(),USER_read()三个函数必须实现,其他函数按需实现,其文件结构目录如下图所示

对于配置为User-defined模式的FatFs来说,上面App文件夹和Target文件夹中的内容均需要用户自己实现,因为CubeMX并不知道用户想要使用的存储设备,所以也无法自动生成底层读写的IO驱动函数,但是对于Extemal SRAM、SD Card和USB Disk这三种固定类型的存储,则无需用户在App文件夹和Target文件夹中重新实现上面提到的一共六个函数,CubeMX生成的工程代码中会自动实现

接下来我们来实现上面提到的六个函数,注意FatFs获取RTC时间需要开启STM32F407的RTC功能,关于RTC的具体使用方法,读者可以阅读STM32CubeMX教程10 RTC 实时时钟 - 周期唤醒、闹钟AB事件和备份寄存器实验,如果不需要可以直接将函数体内容注释

对于Flash_ReadID()、Flash_ReadBytes()和Flash_WriteSector()三个函数是在STM32CubeMX教程20 SPI - W25Q128驱动实验中实现的,请读者自行查阅,重新实现的六个函数源代码如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
/*fatfs.h文件中*/
/*添加RTC头文件*/
#include "rtc.h"

/*fatfs.c文件中*/
/*FatFs获取RTC时间*/
DWORD get_fattime(void)
{
/* USER CODE BEGIN get_fattime */
RTC_TimeTypeDef sTime;
RTC_DateTypeDef sDate;
//获取RTC时间
if(HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN) == HAL_OK)
{
//获取RTC日期
HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);

WORD date=(2000+sDate.Year-1980)<<9;
date = date |(sDate.Month<<5) |sDate.Date;

WORD time=sTime.Hours<<11;
time = time | (sTime.Minutes<<5) | (sTime.Seconds>1);
DWORD dt=(date<<16) | time;

return dt;
}
else
return 0;
/* USER CODE END get_fattime */
}

/*user_diskio.c文件中*/
/*存储设备初始化函数*/
DSTATUS USER_initialize (
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
/* USER CODE BEGIN INIT */
Stat = STA_NOINIT;
//获取驱动器状态
Stat = USER_status(pdrv);
return Stat;
/* USER CODE END INIT */
}

/*获取存储设备状态*/
DSTATUS USER_status (
BYTE pdrv /* Physical drive number to identify the drive */
)
{
/* USER CODE BEGIN STATUS */
Stat = STA_NOINIT; //驱动器未初始化,Stat=0x01
if(Flash_ReadID() != 0) //读取Flash芯片的ID,只要不是0就表示En25Q128已初始化
Stat &= ~STA_NOINIT; //Stat=0x00
return Stat;
/* USER CODE END STATUS */
}

/*底层读函数*/
DRESULT USER_read (
BYTE pdrv, /* Physical drive nmuber to identify the drive */
BYTE *buff, /* Data buffer to store read data */
DWORD sector, /* Sector address in LBA */
UINT count /* Number of sectors to read */
)
{
/* USER CODE BEGIN READ */
//扇区编号左移12位得绝对起始地址
uint32_t globalAddr = sector << 12;
//字节个数,左移12位就是乘4096,每个扇有4096字节
uint16_t byteCount = count << 12;
//读取数据
Flash_ReadBytes(globalAddr, (uint8_t *)buff, byteCount);

return RES_OK;
/* USER CODE END READ */
}

/*底层写函数*/
DRESULT USER_write (
BYTE pdrv, /* Physical drive nmuber to identify the drive */
const BYTE *buff, /* Data to be written */
DWORD sector, /* Sector address in LBA */
UINT count /* Number of sectors to write */
)
{
/* USER CODE BEGIN WRITE */
/* USER CODE HERE */
//绝对地址
uint32_t globalAddr = sector<<12;
//字节个数
uint16_t byteCount = count<<12;
Flash_WriteSector(globalAddr, (uint8_t*)buff, byteCount);

return RES_OK;
/* USER CODE END WRITE */
}

/*底层控制操作函数*/
DRESULT USER_ioctl (
BYTE pdrv, /* Physical drive nmuber (0..) */
BYTE cmd, /* Control code */
void *buff /* Buffer to send/receive control data */
)
{
/* USER CODE BEGIN IOCTL */
DRESULT res = RES_OK;

switch(cmd)
{
/*以下四个命令都是按照FatFs默认参数配置时必须需要的*/
//完成挂起的写入过程(在_FS_READONLY == 0时需要)
case CTRL_SYNC:
break;

//获取存储介质容量(在_USE_MKFS == 1时需要)
case GET_SECTOR_COUNT:
//W25Q128总的扇区个数,4096
*(DWORD *)buff = FLASH_SECTOR_COUNT;
break;

//获取扇区大小(_MAX_SS != _MIN_SS时需要)
case GET_SECTOR_SIZE:
//W25Q128每个扇区的大小,4096字节
*(DWORD *)buff = FLASH_SECTOR_SIZE;
break;

//获取擦除块的大小(_USE_MKFS == 1时需要)
case GET_BLOCK_SIZE:
//W25Q128每个块拥有16个扇区,按块擦除
*(DWORD *)buff = 16;
break;

default:
res = RES_ERROR;
}

return res;
/* USER CODE END IOCTL */
}

然后增加使用FatFs库中API进行文件操作的函数,包括挂载文件系统、显示SD卡信息、读/写TXT文件、获取文件信息、扫描文件列表和删除文件等函数,笔者将其封装在了file_operate.c/file_operate.h文件中,具体的源代码如下所示

file_operate.c文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
#include "file_operate.h"

//定义用于格式化的工作区缓存
BYTE workBuffer[4*User_Sector];

/*挂载FatFs文件系统*/
void Mount_FatFs(void)
{
//挂载文件系统
FRESULT retUSER = f_mount(&User_FatFs, User_SDPath, 1);
//发生错误
if(retUSER != FR_OK)
{
//没有文件系统,需要格式化
if(retUSER == FR_NO_FILESYSTEM)
{
printf("\r\n没有文件系统,开始格式化\r\n");
//创建文件系统
retUSER = f_mkfs(User_SDPath, FM_FAT32, 0, workBuffer, 4*User_Sector);
//格式化失败
if(retUSER != FR_OK)
{
printf("格式化失败,错误代码 = %d\r\n", retUSER);
}
//格式化成功
else
{
printf("格式化成功,开始重新挂载\r\n");
//有文件系统后重新挂载
retUSER = f_mount(&User_FatFs, User_SDPath, 1);
//挂载失败
if(retUSER != FR_OK)
{
printf("发生错误,错误代码 = %d\r\n", retUSER);
}
//挂载成功
else
{
printf("*** 文件系统挂载成功 ***\r\n");
}
}
}
//不是没有文件系统,而是发生其他错误
else
{
printf("发生其他错误,错误代码 = %d\r\n", retUSER);
}
}
//有文件系统直接挂在成功
else
{
printf("文件系统挂载成功\r\n");
}
}

/*获取磁盘信息并在LCD上显示*/
void FatFs_GetDiskInfo(void)
{
FATFS *fs;
//定义剩余簇个数变量
DWORD fre_clust;
//获取剩余簇个数
FRESULT res = f_getfree("0:", &fre_clust, &fs);
//获取失败
if(res != FR_OK)
{
printf("f_getfree() error\r\n");
return;
}
printf("\r\n*** FAT disk info ***\r\n");

//总的扇区个数
DWORD tot_sect = (fs->n_fatent - 2) * fs->csize;

//剩余的扇区个数 = 剩余簇个数 * 每个簇的扇区个数
DWORD fre_sect = fre_clust * fs->csize;

//对于SD卡和U盘, _MIN_SS=512字节
#if _MAX_SS == _MIN_SS
//SD卡的_MIN_SS固定为512,右移11位相当于除以2048
//剩余空间大小,单位:MB,用于SD卡,U盘
DWORD freespace= (fre_sect>>11);
//总空间大小,单位:MB,用于SD卡,U盘
DWORD totalSpace= (tot_sect>>11);
#else
//Flash存储器,小容量
//剩余空间大小,单位:KB
DWORD freespace= (fre_sect*fs->ssize)>>10;
//总空间大小,单位:KB
DWORD totalSpace= (tot_sect*fs->ssize)>>10;
#endif

//FAT类型
printf("FAT type = %d\r\n",fs->fs_type);
printf("[1=FAT12,2=FAT16,3=FAT32,4=exFAT]\r\n");

//扇区大小,单位字节
printf("Sector size(bytes) = ");
//SD卡固定512字节
#if _MAX_SS == _MIN_SS
printf("%d\r\n", _MIN_SS);
#else
//FLASH存储器
printf("%d\r\n", fs->ssize);
#endif

printf("Cluster size(sectors) = %d\r\n", fs->csize);
printf("Total cluster count = %ld\r\n", fs->n_fatent-2);
printf("Total sector count = %ld\r\n", tot_sect);

//总空间
#if _MAX_SS == _MIN_SS
printf("Total space(MB) = %ld\r\n", totalSpace);
#else
printf("Total space(KB) = %ld\r\n", totalSpace);
#endif

//空闲簇数量
printf("Free cluster count = %ld\r\n",fre_clust);
//空闲扇区数量
printf("Free sector count = %ld\r\n", fre_sect);

//空闲空间
#if _MAX_SS == _MIN_SS
printf("Free space(MB) = %ld\r\n", freespace);
#else
printf("Free space(KB) = %ld\r\n", freespace);
#endif

printf("Get FAT disk info OK\r\n");
}

/*创建文本文件*/
void FatFs_WriteTXTFile(TCHAR *filename,uint16_t year, uint8_t month, uint8_t day)
{
FIL file;
printf("\r\n*** Creating TXT file: %s ***\r\n", filename);

FRESULT res = f_open(&file, filename, FA_CREATE_ALWAYS | FA_WRITE);
//打开/创建文件成功
if(res == FR_OK)
{
//字符串必须有换行符"\n"
TCHAR str[]="Line1: Hello, FatFs***\n";
//不会写入结束符"\0"
f_puts(str, &file);

printf("Write file OK: %s\r\n", filename);
}
else
{
printf("Open file error,error code: %d\r\n", res);
}
//使用完毕关闭文件
f_close(&file);
}

/*读取一个文本文件的内容*/
void FatFs_ReadTXTFile(TCHAR *filename)
{
printf("\r\n*** Reading TXT file: %s ***\r\n", filename);

FIL file;
//以只读方式打开文件
FRESULT res = f_open(&file, filename, FA_READ);
//打开成功
if(res == FR_OK)
{
//读取缓存
TCHAR str[100];
//没有读到文件内容末尾
while(!f_eof(&file))
{
//读取1个字符串,自动加上结束符”\0”
f_gets(str,100, &file);
printf("%s", str);
}
printf("\r\n");
}
//如果没有该文件
else if(res == FR_NO_FILE)
printf("File does not exist\r\n");
//打开失败
else
printf("f_open() error,error code: %d\r\n", res);
//关闭文件
f_close(&file);
}

/*扫描和显示指定目录下的文件和目录*/
void FatFs_ScanDir(const TCHAR* PathName)
{
DIR dir; //目录对象
FILINFO fno; //文件信息
//打开目录
FRESULT res = f_opendir(&dir, PathName);
//打开失败
if(res != FR_OK)
{
//关闭目录,直接退出函数
f_closedir(&dir);
printf("\r\nf_opendir() error,error code: %d\r\n", res);
return;
}

printf("\r\n*** All entries in dir: %s ***\r\n", PathName);
//顺序读取目录中的文件
while(1)
{
//读取目录下的一个项
res = f_readdir(&dir, &fno);
//文件名为空表示没有多的项可读了
if(res != FR_OK || fno.fname[0] == 0)
break;
//如果是一个目录
if(fno.fattrib & AM_DIR)
{
printf("DIR: %s\r\n", fno.fname);
}
//如果是一个文件
else
{
printf("FILE: %s\r\n",fno.fname);
}
}
//扫描完毕,关闭目录
printf("Scan dir OK\r\n");
f_closedir(&dir);
}

/*获取一个文件的文件信息*/
void FatFs_GetFileInfo(TCHAR *filename)
{
printf("\r\n*** File info of: %s ***\r\n", filename);

FILINFO fno;
//检查文件或子目录是否存在
FRESULT fr = f_stat(filename, &fno);
//如果存在从fno中读取文件信息
if(fr == FR_OK)
{
printf("File size(bytes) = %ld\r\n", fno.fsize);
printf("File attribute = 0x%x\r\n", fno.fattrib);
printf("File Name = %s\r\n", fno.fname);
//输出创建/修改文件时的时间戳
FatFs_PrintfFileDate(fno.fdate, fno.ftime);
}
//如果没有该文件
else if (fr == FR_NO_FILE)
printf("File does not exist\r\n");
//发生其他错误
else
printf("f_stat() error,error code: %d\r\n", fr);
}

/*删除文件*/
void FatFs_DeleteFile(TCHAR *filename)
{
printf("\r\n*** Delete File: %s ***\r\n", filename);
FIL file;
//打开文件
FRESULT res = f_open(&file, filename, FA_OPEN_EXISTING);
if(res == FR_OK)
{
//关闭文件
f_close(&file);
printf("open successfully!\r\n");
}
//删除文件
res = f_unlink(filename);
//删除成功
if(res == FR_OK)
{
printf("The file was deleted successfully!\r\n");
}
//删除失败
else
{
printf("File deletion failed, error code:%d\r\n", res);
}
}

/*打印输出文件日期*/
void FatFs_PrintfFileDate(WORD date, WORD time)
{
printf("File data = %d/%d/%d\r\n", ((date>>9)&0x7F)+1980, (date>>5)&0xF, date&0x1F);
printf("File time = %d:%d:%d\r\n", (time>>11)&0x1F, (time>>5)&0x3F, time&0x1F);
}

file_operate.h文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#ifndef FILE_OPERATE_H
#define FILE_OPERATE_H

#include "main.h"
#include "FatFs.h"
#include "stdio.h"

/*定义自己的存储设备*/
/*用户存储设备扇区字节数*/
#define User_Sector 4096
/*用户存储设备FatFS对象*/
#define User_FatFs USERFatFS
/*用户存储设备卷路径*/
#define User_SDPath USERPath

/*函数声明*/
void Mount_FatFs(void);
void FatFs_GetDiskInfo(void);
void FatFs_ScanDir(const TCHAR* PathName);
void FatFs_ReadTXTFile(TCHAR *filename);
void FatFs_WriteTXTFile(TCHAR *filename,uint16_t year, uint8_t month, uint8_t day);
void FatFs_GetFileInfo(TCHAR *filename);
void FatFs_DeleteFile(TCHAR *filename);
void FatFs_PrintfFileDate(WORD date, WORD time);

#endif

向工程中添加.c/.h文件的步骤请阅读“STM32CubeMX教程19 I2C - MPU6050驱动”实验3.2.3小节

最后在main.c文件中添加 ”file_operate.h“ 头文件,然后在主函数 main() 中调用文件系统挂载函数,实现按键控制逻辑程序,具体源代码如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/*main.c中添加头文件*/
#include "file_operate.h"

/*外输初始化完进入主循环前*/
//检测SPI与W25Q128通信是否正常
printf("Reset,ID:0x%x\r\n", Flash_ReadID());
//挂载文件系统
Mount_FatFs();
//获取磁盘信息
FatFs_GetDiskInfo();

/*主循环中按键逻辑*/
/*按键WK_UP被按下*/
if(HAL_GPIO_ReadPin(WK_UP_GPIO_Port,WK_UP_Pin) == GPIO_PIN_SET)
{
HAL_Delay(50);
if(HAL_GPIO_ReadPin(WK_UP_GPIO_Port,WK_UP_Pin) == GPIO_PIN_SET)
{
FatFs_ScanDir("0:/");
while(HAL_GPIO_ReadPin(WK_UP_GPIO_Port,WK_UP_Pin));
}
}

/*按键KEY2被按下*/
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
{
HAL_Delay(50);
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
{
FatFs_WriteTXTFile("test.txt",2016,11,15);
while(!HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin));
}
}

/*按键KEY1被按下*/
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
{
HAL_Delay(50);
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
{
FatFs_ReadTXTFile("test.txt");
FatFs_GetFileInfo("test.txt");
while(!HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin));
}
}

/*按键KEY0被按下*/
if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
{
HAL_Delay(50);
if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
{
FatFs_DeleteFile("test.txt");
while(!HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin));
}
}

4、烧录验证

烧录程序,开发板上电后会尝试在W25Q128 FLASH芯片上挂载文件系统,挂载成功后会输出读取到的 FLASH芯片的信息,接下来按照下面几个步骤使用FatFs文件系统对 FLASH芯片进行读写等测试

  1. 按下开发板上的WK_UP按键,扫描FLASH芯片根目录下所有文件,并通过串口将文件列表输出
  2. 按下开发板上的KEY2按键,在 FLASH芯片根目录创建一个”test.txt“文件,将一个字符串 ”Hello,OSnotes“ 写入该文件中,该字符串大小为15个字节(该字符串中末尾包括了一个’\n‘和一个‘\0’)
  3. 按下开发板上的KEY1按键,读取FLASH芯片根目录下名为”test.txt“的文件,将其中的内容通过串口输出,然后读取该文件的信息(大小,属性,名称),并通过串口输出
  4. 按下开发板上的KEY0按键,删除FLASH芯片根目录下名为”test.txt“的文件

整个实验过程串口具体的输出情况如下图所示

5、常用函数

FatFs的所有API函数详细介绍请参看FatFs官网 FatFs - Generic FAT Filesystem Module,如下所示为笔者对其常用应用接口及其功能做简单介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*注册/取消注册卷的工作区域*/
FRESULT f_mount(FatFs* fs, const TCHAR* path, BYTE opt)
/*在逻辑驱动器上创建FAT卷*/
FRESULT f_mkfs(const TCHAR* path, BYTE opt, DWORD au, void* work, UINT len)
/*获取卷上的可用空间*/
FRESULT f_getfree(const TCHAR* path, DWORD* nclst, FatFs** FatFs)
/*打开/创建文件*/
FRESULT f_open(FIL* fp, const TCHAR* path, BYTE mode)
/*写入一个字符串*/
int f_puts(const TCHAR* str, FIL* fp)
/*写入格式化字符串*/
int f_printf(FIL* fp, const TCHAR* fmt, ...)
/*关闭打开的文件*/
FRESULT f_close(FIL* fp)
/*读取字符串*/
TCHAR* f_gets(TCHAR* buff, int len, FIL* fp)
/*打开目录*/
FRESULT f_opendir(DIR* dp, const TCHAR* path)
/*读取目录项*/
FRESULT f_readdir(DIR* dp, FILINFO* fno)
/*关闭打开的目录*/
FRESULT f_closedir(DIR *dp)
/*检查文件或子目录是否存在*/
FRESULT f_stat(const TCHAR* path, FILINFO* fno)
/*删除文件或子目录*/
FRESULT f_unlink(const TCHAR* path)
/*重命名/移动文件或子目录*/
FRESULT f_rename(const TCHAR* path_old, const TCHAR* path_new)
/*创建子目录*/
FRESULT f_mkdir(const TCHAR* path)

6、注释详解

注释1:图片来源 FatFs Module Application Note (elm-chan.org)

参考资料

STM32Cube高效开发教程(高级篇)

更多内容请浏览 STM32CubeMX+STM32F4系列教程文章汇总贴


STM32CubeMX教程26 FatFs 文件系统 - W25Q128读写
https://lc-guo.github.io/2024/01/26/STM32CubeMX教程26-FatFs-文件系统-W25Q128读写/
作者
OSnotes
发布于
2024年1月26日
许可协议