LV16-22-FatFs-02-FatFs文件系统移植

本文主要是FatFs文件系统移植和基本使用相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
  • 通用
分类 网址说明
官方网站https://www.arm.com/ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.st.com/content/st_com/zh.htmlST官方网站,在这里我们可以找到STM32的相关文档
https://www.stmcu.com.cn/意法半导体ST中文官方网站,在这里我们可以找到STM32的相关中文参考文档
http://elm-chan.org/fsw/ff/00index_e.htmlFatFs文件系统官网
教程书籍《ARM Cortex-M3权威指南》ARM公司专家Joseph Yiu(姚文祥)的力作,中文翻译是NXP的宋岩
《ARM Cortex-M0权威指南》
《ARM Cortex-M3与Cortex-M4权威指南》
开发论坛http://47.111.11.73/forum.php开源电子网,正点原子的资料下载及问题讨论论坛
https://www.firebbs.cn/forum.php国内Kinetis开发板-野火/秉火(刘火良)主持的论坛,现也做STM32和i.MX RT
https://www.amobbs.com/index.php阿莫(莫进明)主持的论坛,号称国内最早最火的电子论坛,以交流Atmel AVR系列单片机起家,现已拓展到嵌入式全平台,其STM32系列帖子有70W+。
http://download.100ask.net/index.html韦东山嵌入式资料中心,有些STM32和linux的相关资料也可以来这里找。
博客参考http://www.openedv.com/开源网-原子哥个人博客
http://blog.chinaaet.com/jihceng0622博主是原Freescale现NXP的现场应用工程师
cortex-m-resources这其实并不算是一个博客,这是ARM公司专家Joseph Yiu收集整理的所有对开发者有用的官方Cortex-M资料链接(也包含极少数外部资源链接)
  • STM32
STM32STM32 HAL库开发实战指南——基于F103系列开发板野火STM32开发教程在线文档
STM32库开发实战指南——基于野火霸道开发板野火STM32开发教程在线文档
  • SD卡
SD Association提供了SD存储卡和SDIO卡系统规范
点击查看相关文件下载
STM32F103xx英文数据手册STM32F103xC/D/E系列的英文数据手册
STM32F103xx中文数据手册STM32F103xC/D/E系列的中文数据手册
STM32F10xxx英文参考手册(RM0008)STM32F10xxx系列的英文参考手册
STM32F10xxx中文参考手册(RM0008)STM32F10xxx系列的中文参考手册
Arm Cortex-M3 处理器技术参考手册-英文版Cortex-M3技术参考手册-英文版
STM32F10xxx Cortex-M3编程手册-英文版(PM0056)STM32F10xxx/20xxx/21xxx/L1xxxx系列Cortex-M3编程手册-英文版
SD卡相关资料——最新版本有关SD卡的一些资料可以从这里下载
SD卡相关资料——历史版本有关SD卡的一些历史版本资料可以从这里下载,比如后边看的SD卡2.0协议
SD 2.0 协议标准完整版这是一篇关于SD卡2.0协议的中文文档,还是比较有参考价值的,可以一看

注意这里的代码可能会用到动态管理内存的东西,可以看后边的笔记。

一、API与返回值

我们来看几个函数,以及结构体,方便后边测试和配置的时候使用。

1. 两个结构体

1.1 FATFS

关于该结构体,它定义在ff.h文件中,我们可以参考官方文档:FatFs - FATFS (elm-chan.org)

1
2
3
4
5
6
7
8
9
10
/* Filesystem object structure (FATFS) */

typedef struct {
BYTE fs_type; /* Filesystem type (0:not mounted) */
BYTE pdrv; /* Volume hosting physical drive */
BYTE ldrv; /* Logical drive number (used only when FF_FS_REENTRANT) */
BYTE n_fats; /* Number of FATs (1 or 2) */
// 中间的部分省略
BYTE win[FF_MAX_SS]; /* Disk access window for Directory, FAT (and file data at tiny cfg) */
} FATFS;

FATFS结构(文件系统对象)保存各个逻辑驱动器的动态工作区域。它由应用程序给出,并通过f_mount函数注册/取消注册到FatFs模块。结构的初始化在必要时由卷挂载进程完成。应用程序不得修改此结构中的任何成员,否则将导致FAT卷崩溃。

我们要注意一下这个结构体是非常大的,我们可以看到最后一个成员win,这是一个数组,大小为FF_MAX_SS,这个最大值我们后边可能会配置为4096,这样下来这个结构体就非常的大,用它定义的变量最好不要定义为局部变量,否则可能会导致栈的溢出,当然,要是我们的栈非常大的话吗,就可以不用考虑这个问题了。

1.2 FIL

关于该结构体,它定义在ff.h中,我们可以参考官方文档:FatFs - FIL (elm-chan.org)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* File object structure (FIL) */

typedef struct {
FFOBJID obj; /* Object identifier (must be the 1st member to detect invalid object pointer) */
BYTE flag; /* File status flags */
BYTE err; /* Abort flag (error code) */
FSIZE_t fptr; /* File read/write pointer (Zeroed on file open) */
DWORD clust; /* Current cluster of fpter (invalid when fptr is 0) */
LBA_t sect; /* Sector number appearing in buf[] (0:invalid) */
#if !FF_FS_READONLY
LBA_t dir_sect; /* Sector number containing the directory entry (not used at exFAT) */
BYTE* dir_ptr; /* Pointer to the directory entry in the win[] (not used at exFAT) */
#endif
#if FF_USE_FASTSEEK
DWORD* cltbl; /* Pointer to the cluster link map table (nulled on open, set by application) */
#endif
#if !FF_FS_TINY
BYTE buf[FF_MAX_SS]; /* File private data read/write window */
#endif
} FIL;

FIL结构(文件对象)保存打开文件的状态。它由f_open函数创建,并由f_close函数丢弃。应用程序不能修改该结构中除cltbl之外的任何成员,否则将导致FAT卷崩溃。请注意,扇区缓冲区是在非微小配置(FF_FS_TINY == 0)的结构中定义的,因此该配置中的FIL结构不应该被定义为自动变量。

我们要注意一下这个结构体是非常大的,我们可以看到最后一个成员buf,这是一个数组,大小为FF_MAX_SS,这个最大值我们后边可能会配置为4096,这样下来这个结构体跟FATFS一样,就非常的大,所以一般我们也不会将其定义为局部变量。

2. 函数返回值

2.1 返回值的枚举类型

我们首先来了解一下我们常用的一些函数的返回值的含义,我们可以通过这些返回值来判断是哪里出错了:

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
/* File function return code (FRESULT) */

typedef enum {
FR_OK = 0, /* (0) Succeeded */
FR_DISK_ERR, /* (1) A hard error occurred in the low level disk I/O layer */
FR_INT_ERR, /* (2) Assertion failed */
FR_NOT_READY, /* (3) The physical drive cannot work */
FR_NO_FILE, /* (4) Could not find the file */
FR_NO_PATH, /* (5) Could not find the path */
FR_INVALID_NAME, /* (6) The path name format is invalid */
FR_DENIED, /* (7) Access denied due to prohibited access or directory full */
FR_EXIST, /* (8) Access denied due to prohibited access */
FR_INVALID_OBJECT, /* (9) The file/directory object is invalid */
FR_WRITE_PROTECTED, /* (10) The physical drive is write protected */
FR_INVALID_DRIVE, /* (11) The logical drive number is invalid */
FR_NOT_ENABLED, /* (12) The volume has no work area */
FR_NO_FILESYSTEM, /* (13) There is no valid FAT volume */
FR_MKFS_ABORTED, /* (14) The f_mkfs() aborted due to any problem */
FR_TIMEOUT, /* (15) Could not get a grant to access the volume within defined period */
FR_LOCKED, /* (16) The operation is rejected according to the file sharing policy */
FR_NOT_ENOUGH_CORE, /* (17) LFN working buffer could not be allocated */
FR_TOO_MANY_OPEN_FILES, /* (18) Number of open files > FF_FS_LOCK */
FR_INVALID_PARAMETER /* (19) Given parameter is invalid */
} FRESULT;

这是一个枚举类型,它也定义在ff.h中,我们可以参考官方文档来详细了解这些值的含义:FatFs - API Return Code (elm-chan.org)

2.2 字符串数组?

上边返回值好多,我们知道值的话,还需要知道代表什么含义,我们其实可以定义一个二维数组,按顺序将上边的枚举所代表的含义依次写入,然后打印的时候就可以直接获取错误码的含义了,不过这样可能会比较占内存,慎用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static BYTE API_RET[][90] = {
"(0) Succeeded ",
"(1) A hard error occurred in the low level disk I/O layer ",
"(2) Assertion failed ",
"(3) The physical drive cannot work ",
"(4) Could not find the file ",
"(5) Could not find the path ",
"(6) The path name format is invalid ",
"(7) Access denied due to prohibited access or directory full ",
"(8) Access denied due to prohibited access ",
"(9) The file/directory object is invalid ",
"(10) The physical drive is write protected ",
"(11) The logical drive number is invalid ",
"(12) The volume has no work area ",
"(13) There is no valid FAT volume ",
"(14) The f_mkfs() aborted due to any problem ",
"(15) Could not get a grant to access the volume within defined period ",
"(16) The operation is rejected according to the file sharing policy ",
"(17) LFN working buffer could not be allocated ",
"(18) Number of open files > FF_FS_LOCK ",
"(19) Given parameter is invalid ",
};

我们打印的时候就可以这样:

1
printf("res:%s\n\r", API_RET[res]);

这样就可以直接将错误的原因打印出来啦。

3. 常用函数

3.1 f_mount()

我们可以参考FatFs - f_mount (elm-chan.org)

1
2
3
4
5
FRESULT f_mount (
FATFS* fs, /* [IN] Filesystem object */
const TCHAR* path, /* [IN] Logical drive number */
BYTE opt /* [IN] Initialization option */
);

【函数说明】f_mount函数为FatFs模块提供工作区域。也就是用于挂载我们存储设备上的文件系统,挂载之后我们才能正常的使用这个文件系统。

【函数参数】

  • fs :指向要注册和清除的文件系统对象的指针。若是NULL的话,则表示取消注册已注册的文件系统对象,每个存储介质只需要一个即可,用于存储这个存储设备的文件系统的信息。
  • path :指向指定逻辑驱动器的以空结尾的字符串的指针。不带驱动器号的字符串为默认驱动器。比如我们上边规定的SPI FLASH的drive number为1,那么这里就可以写 “1:” ,当挂载成功后,他就相当于我们win中的C、D等这些的盘符。
  • opt :0,现在不挂载(要在第一次访问卷时挂载),1,强制立即挂载卷,并检查卷是否可以工作。

3.2 f_mkfs()

我们可以参考FatFs - f_mkfs (elm-chan.org):

1
2
3
4
5
6
FRESULT f_mkfs (
const TCHAR* path, /* [IN] Logical drive number */
const MKFS_PARM* opt,/* [IN] Format options */
void* work, /* [-] Working buffer */
UINT len /* [IN] Size of working buffer */
);

【函数说明】f_mkfs函数在逻辑驱动器上创建一个FAT/exFAT卷。什么意思呢?就是我们的存储介质上原本是什么都没有的,我们需要将其格式化并创建一个文件系统,这样我们才能正常的去使用文件系统来管理其中的文件。

【函数参数】

  • path:指向以空结尾的字符串的指针指定要格式化的逻辑驱动器。如果其中没有驱动器号,则表示指定默认驱动器。在格式化过程中,逻辑驱动器可能已经挂载,也可能没有挂载。

  • opt :MKFS_PARM 类型结构体指针变量,表示格式选项。如果给出一个空指针,它将给函数提供默认值中的每个选项,详细情况我们可以看一下参考文档中的详细说明。

  • work :指向格式化过程使用的工作缓冲区的指针。如果 null 指针以 FF_USE_LFN == 3 给出,则该函数在此函数中使用 len 字节的堆内存。

  • len :工作缓冲区的大小,以字节为单位。它至少需要是FF_MAX_SS。大量的工作缓冲区减少了对驱动器的写事务数量,因此格式化过程将很快完成。

4. 时间函数get_fattime()

这个函数是所有存储介质移植的时候都需要的,这个函数在FatFs源码中没有定义,但是又用到了,所以还是需要定义一下的,不然会报错。我们可以看一下参考文档FatFs - get_fattime (elm-chan.org),函数声明如下:

1
DWORD get_fattime (void);

这个函数主要是获取当前的时间,这个函数在FatFs系统中没有实现,我们直接编译是会报这个错误的,我们可以通过STM32的RTC来获取时间,当然,若是暂时不需要,我们可以将其定义为如下的形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @brief 获取当前系统时间
* @note 用于文件时间属性的确定
* @param
* @retval
*/
DWORD get_fattime(void)
{
/* 返回当前时间戳 */
return ((DWORD)(2015 - 1980) << 25) /* Year 2015 */
| ((DWORD)1 << 21) /* Month 1 */
| ((DWORD)1 << 16) /* Mday 1 */
| ((DWORD)0 << 11) /* Hour 0 */
| ((DWORD)0 << 5) /* Min 0 */
| ((DWORD)0 >> 1); /* Sec 0 */
}

二、基于SPI FLASH的文件系统

我们先来看一下以SPI FLASH为存储介质的FatFs文件系统的移植。

1. 底层读写

1.1 初始化

首先我们需要初始化SPI FLASH,在这里我使用的是W25Q128,由于我使用的是STM32CubeMX来配置的SPI,所以初始化就如下函数所示:

1
2
/* SPI2 init function */
void MX_SPI2_Init(void);

1.2 获取SPI FLASH器件ID

这一步其实本不是必须,但是由于代码支持了其他型号的SPI FLASH,所以这一步也就成了必要的步骤,其实也有好处,那就是可以通过读取ID来判断我们的SPI FLASH是否已经初始化完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @brief 获取W25QXX型号
* @note 目前只支持W25Q128
* @param
* @retval 返回0表示W25Q128状态正常,返回-1表示W25Q128不可用
*/
int8_t Get_W25QXX_Type(void)
{
W25QXX_TYPE = W25QXX_ReadID();
//printf("SPI FLASH ID=%#x\r\n", W25QXX_TYPE);
if(W25QXX_TYPE == 0XEF17)
return 0;
else
{
PRTE("W25QXX_TYPE not is W25Q128!!!\r\n");
return -1;
}
}

1.3 读取数据

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
/**
* @brief 读取SPI FLASH
* @note 在指定地址开始读取指定长度的数据
* @param pBuffer 数据存储区
* @param ReadAddr 开始读取的地址(24bit)
* @param NumByteToRead 要读取的字节数(最大65535)
* @retval 成功返回0
*/
int8_t W25QXX_Read(uint8_t *pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead)
{
uint16_t i;
W25QXX_CS = 0; // 使能器件
SPI2_ReadWriteByte(W25X_ReadData); // 发送读取命令
if (W25QXX_TYPE == W25Q256) // 如果是W25Q256的话地址为4字节的,要发送最高8位
{
SPI2_ReadWriteByte((uint8_t)((ReadAddr) >> 24));
}
SPI2_ReadWriteByte((uint8_t)((ReadAddr) >> 16)); // 发送24bit地址
SPI2_ReadWriteByte((uint8_t)((ReadAddr) >> 8));
SPI2_ReadWriteByte((uint8_t)ReadAddr);
for (i = 0; i < NumByteToRead; i++)
{
pBuffer[i] = SPI2_ReadWriteByte(0XFF); // 循环读数
}
W25QXX_CS = 1;
return 0;
}

1.4 写入数据

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
// 动态管理内存
#ifndef SRAMIN
uint8_t W25QXX_BUFFER[4096];
#endif
/**
* @brief 向 SPI FLASH 写入指定字节数据
* @note 在指定地址开始写入指定长度的数据,该函数带擦除操作!
* @param pBuffer 数据存储区
* @param WriteAddr 开始写入的地址(24bit)
* @param NumByteToWrite 要写入的字节数(最大65535)
* @retval 成功返回0
*/
int8_t W25QXX_Write(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
uint32_t secpos;
uint16_t secoff;
uint16_t secremain;
uint16_t i;
uint8_t *W25QXX_BUF;
#ifdef SRAMIN
W25QXX_BUF = (uint8_t *)pub_malloc(SRAMIN, 4096); // 申请内存
if (W25QXX_BUF == NULL)
{
PRTE("pub_malloc failed!!!\r\n");
return -1; // 申请失败
}
#else
W25QXX_BUF = W25QXX_BUFFER;
#endif
secpos = WriteAddr / W25QXX_SECTOR_SIZE; // 扇区地址
secoff = WriteAddr % W25QXX_SECTOR_SIZE; // 在扇区内的偏移
secremain = W25QXX_SECTOR_SIZE - secoff; // 扇区剩余空间大小
// printf("ad:%X,nb:%X\r\n",WriteAddr,NumByteToWrite);//测试用
if (NumByteToWrite <= secremain)
secremain = NumByteToWrite; // 不大于4096个字节
while (1)
{
W25QXX_Read(W25QXX_BUF, secpos * W25QXX_SECTOR_SIZE, W25QXX_SECTOR_SIZE); // 读出整个扇区的内容
for (i = 0; i < secremain; i++) // 校验数据
{
if (W25QXX_BUF[secoff + i] != 0XFF)
break; // 需要擦除
}
if (i < secremain) // 需要擦除
{
W25QXX_Erase_Sector(secpos); // 擦除这个扇区
for (i = 0; i < secremain; i++) // 复制
{
W25QXX_BUF[i + secoff] = pBuffer[i];
}
W25QXX_Write_NoCheck(W25QXX_BUF, secpos * W25QXX_SECTOR_SIZE, W25QXX_SECTOR_SIZE); // 写入整个扇区
}
else
W25QXX_Write_NoCheck(pBuffer, WriteAddr, secremain); // 写已经擦除了的,直接写入扇区剩余区间.
if (NumByteToWrite == secremain)
break; // 写入结束了
else // 写入未结束
{
secpos++; // 扇区地址增1
secoff = 0; // 偏移位置为0

pBuffer += secremain; // 指针偏移
WriteAddr += secremain; // 写地址偏移
NumByteToWrite -= secremain; // 字节数递减
if (NumByteToWrite > W25QXX_SECTOR_SIZE)
secremain = W25QXX_SECTOR_SIZE; // 下一个扇区还是写不完
else
secremain = NumByteToWrite; // 下一个扇区可以写完了
}
}
#ifdef SRAMIN
pub_free(SRAMIN, W25QXX_BUF); // 释放内存
#endif
return 0;
}

2. diskio.c文件的移植

接下来,就来一步一步移植这个文件系统,然后看一看中间可能会有什么坑。这一部分主要都是在diskio.c文件中进行移植,这里先不关心配置文件,后边测试的时候会根据出错的情况一步一步修改配置文件。

2.1 drive number

首先,我们需要定义我们的盘符,在 diskio.c 文件的开头有以下几个宏:

1
2
3
4
/* Definitions of physical drive number for each drive */
#define DEV_RAM 0 /* Example: Map Ramdisk to physical drive 0 */
#define DEV_MMC 1 /* Example: Map MMC/SD card to physical drive 1 */
#define DEV_USB 2 /* Example: Map USB MSD to physical drive 2 */

这些宏就表示了不同的存储介质,后边会根据这些宏来调用不同的初始化及读写函数去操作对应的存储设备。我们这里定义两个,一个是SD卡(预留,后边会使用),一个是SPI FLASH:

1
2
#define DEV_SDCARD      0	/* Example: Map SD card to physical drive 0 */
#define DEV_SPI_FLASH 1 /* Example: Map SPI FLASH to physical drive 1 */

2.2 disk_initialize()

2.2.1 函数声明

首先是初始化函数disk_initialize(),我们可以参考文档FatFs - disk_initialize (elm-chan.org)

1
2
3
DSTATUS disk_initialize (
BYTE pdrv /* [IN] Physical drive number */
);

这个函数初始化存储设备,并使其准备好进行通用读/写。

2.2.2 对SPI FLASH初始化

我们来看一下对SPI FLASH的初始化操作:

1
2
3
4
case DEV_SPI_FLASH:
//MX_SPI2_Init(); // SPI初始化,在main.c中完成
result = Get_W25QXX_Type();// 这里最开始的时候写的是 result = 0; 但是这样是无法完整对SPI FLASH初始化,后边会分析问题所在。
break;

由于我使用的是STM32CubeMX来配置的SPI,所以当STM32CubeMX生成工程的时候,会直接在main函数中进行SPI初始化函数的调用,所以为了后边修改工程方便,这里就不再放对SPI的初始化了。

2.2.3 移植结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
DSTATUS disk_initialize(
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
int result = 0;

switch (pdrv)
{
case DEV_SPI_FLASH:
//MX_SPI2_Init(); // SPI初始化,在main.c中完成
result = Get_W25QXX_Type();
break;
default:
result = -1;
break;
}
if(result == 0)
return RES_OK;
else
return STA_NOINIT; //初始化失败
}

2.3 disk_status()

2.3.1 函数声明

这个函数主要是获取设备的状态,其实这个函数并不重要,我们甚至可以让它永远返回一个成功测标志。我们可以参考:FatFs - disk_status (elm-chan.org)

1
2
3
DSTATUS disk_status (
BYTE pdrv /* [IN] Physical drive number */
);

2.3.2 对SPI FLASH的状态检测

我们可以将获取W25Q128器件ID的操作放在状态读取的函数中,这样也可以确保我们的SPI FLASH可以正常运行:

1
2
3
case DEV_SPI_FLASH:
result = Get_W25QXX_Type();
break;

2.3.3 移植结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
DSTATUS disk_status(
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
int result = 0;

switch (pdrv)
{
case DEV_SPI_FLASH:
result = Get_W25QXX_Type();
break;
default:
result = -1;
break;
}
if(result == 0)
return RES_OK;
else
return STA_NOINIT;
}

2.4 disk_read()

2.4.1 函数声明

这个函数主要是读取数据,我们可以参考:FatFs - disk_read (elm-chan.org)

1
2
3
4
5
6
DRESULT disk_read (
BYTE pdrv, /* [IN] Physical drive number */
BYTE* buff, /* [OUT] Pointer to the read data buffer */
LBA_t sector, /* [IN] Start sector number */
UINT count /* [IN] Number of sectros to read */
);

【函数参数】

  • pdrv :用于标识目标设备的物理驱动器号。

  • buff :指向字节数组中用于存储读取数据的第一个数据的指针。读取数据的大小将是 扇区大小x字节数。

  • sector :启动扇区号。数据类型 LBA_t 是 DWORD 或 QWORD 的别名,具体取决于配置选项。

  • count :要读取的扇区数。

【注意事项】FatFs每次操作,都是以扇区为基本单位的。

2.4.2 从SPI FLASH读取

1
2
3
case DEV_SPI_FLASH:
result = W25QXX_Read((uint8_t *)buff, sector * 4096, count * 4096);
break;

前边我们知道,W25Q128将16MB的容量分为256个块(Block),每个块大小为64K字节,每个块又分为16个扇区(Sector),每个扇区4K个字节。而我们操作W25Q128的时候每次也是擦除一个扇区,也就是4KB,16M的空间,一共就有4096个扇区,而扇区号乘以扇区大小就可以得到整个扇区的起始地址。count 表示扇区的数量,每个扇区4KB,相乘就可以得到要读取的字节数量。

2.4.3 移植结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
DRESULT disk_read(
BYTE pdrv, /* Physical drive nmuber to identify the drive */
BYTE *buff, /* Data buffer to store read data */
LBA_t sector, /* Start sector in LBA */
UINT count /* Number of sectors to read */
)
{
int result = 0;

switch (pdrv)
{
case DEV_SPI_FLASH:
result = W25QXX_Read((uint8_t *)buff, sector * 4096, count * 4096);
break;
default:
result = -1;
break;
}
if(result == 0)
return RES_OK;
else
return RES_ERROR;
}

2.5 disk_write()

2.5.1 函数声明

这个函数主要是写入数据,我们可以参考:FatFs - disk_write (elm-chan.org)

1
2
3
4
5
6
DRESULT disk_write (
BYTE pdrv, /* [IN] Physical drive number */
const BYTE* buff, /* [IN] Pointer to the data to be written */
LBA_t sector, /* [IN] Sector number to write from */
UINT count /* [IN] Number of sectors to write */
);

【函数参数】

  • pdrv :用于标识目标设备的物理驱动器号。

  • buff :指向字节数组中用于存储写入数据的第一个数据的指针。写入数据的大小将是 扇区大小x字节数。

  • sector :启动扇区号。数据类型 LBA_t 是 DWORD 或 QWORD 的别名,具体取决于配置选项。

  • count :要读取的扇区数。

【注意事项】FatFs每次操作,都是以扇区为基本单位的。

2.5.2 向SPI FLASH写入

1
2
3
case DEV_SPI_FLASH:
result = W25QXX_Write((uint8_t *)buff, sector * 4096, count * 4096);
break;

这里与前边的读一样,都是以扇区为单位操作,将参数传入底层写入函数的时候需要转换为相应的地址。

2.5.3 移植结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
DRESULT disk_write(
BYTE pdrv, /* Physical drive nmuber to identify the drive */
const BYTE *buff, /* Data to be written */
LBA_t sector, /* Start sector in LBA */
UINT count /* Number of sectors to write */
)
{
int result = 0;

switch (pdrv)
{
case DEV_SPI_FLASH:
result = W25QXX_Write((uint8_t *)buff, sector * 4096, count * 4096);
break;
default:
result = -1;
break;
}

if(result == 0)
return RES_OK;
else
return RES_ERROR;
}

2.6 disk_ioctl()

2.6.1 函数声明

这个函数主要是用于控制设备,可根据不同命令执行不同功能,我们可以参考:FatFs - disk_ioctl (elm-chan.org)

1
2
3
4
5
DRESULT disk_ioctl (
BYTE pdrv, /* [IN] Drive number */
BYTE cmd, /* [IN] Control command code */
void* buff /* [I/O] Parameter and data buffer */
);

【函数参数】

  • pdrv :用于标识目标设备的物理驱动器号。

  • cmd :命令编号,比如GET_SECTOR_COUNT、GET_SECTOR_SIZE、GET_BLOCK_SIZE等,详细的命令及含义可以参考官方文档说明。

  • buff :参数的指针取决于命令代码。不要在意该命令是否没有要传递的参数。

2.6.2 获取SPI FLASH信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
case DEV_SPI_FLASH:
switch(cmd)
{
case GET_SECTOR_COUNT:// 扇区数量
*(DWORD *)buff = 256 * 16;//256块x16个扇区
break;
case GET_SECTOR_SIZE:// 扇区大小
*(WORD *)buff = 4096;// 每个扇区是4KB
break;
case GET_BLOCK_SIZE:// 每次擦除块的大小的个数
*(DWORD *)buff = 1;// 每次只擦除一个扇区
default:
break;
}
result = 0;
break;

2.6.3 移植结果

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
DRESULT disk_ioctl(
BYTE pdrv, /* Physical drive nmuber (0..) */
BYTE cmd, /* Control code */
void *buff /* Buffer to send/receive control data */
)
{
int result = 0;

switch (pdrv)
{
case DEV_SPI_FLASH:
switch(cmd)
{
case GET_SECTOR_COUNT:// 扇区数量
*(DWORD *)buff = 256 * 16;//256块x16个扇区
//*(DWORD *)buff = FLASH_SECTOR_COUNT;//12M/512B个扇区
break;
case GET_SECTOR_SIZE:// 扇区大小
*(WORD *)buff = 4096;// 每个扇区是4KB
//*(WORD *)buff = FLASH_SECTOR_SIZE;// 每个扇区是512B
break;
case GET_BLOCK_SIZE:// 每次擦除块的大小的个数
*(DWORD *)buff = 1;// 每次只擦除一个扇区
//*(DWORD *)buff = FLASH_BLOCK_SIZE;// 每次只擦除一个大扇区,512*8=4096
default:
break;
}
result = 0;
break;
default:
result = -1;
break;
}

if(result == 0)
return RES_OK;
else
return RES_ERROR;
}

3. ffconf.h文件的配置

这一节我们通过报错来看一下不同的配置项有什么影响。我们这部分以例子来说明。

3.1 FF_VOLUMES

测试函数我们用f_mount来尝试:

1
2
3
4
5
6
7
8
9
10
11
static FATFS fs = {0}; // FatFs文件系统对象
static BYTE work[FF_MAX_SS]; // 格式化设备工作区
void fatfs_Test(void)
{
FRESULT res = FR_OK;
res = f_mount(&fs, "1:", 1); // 挂载FLASH.
if(res != FR_OK)
{
printf("f_mount error:%s\r\n", API_RET[res]);
}
}

然后我们调用并打印,会有如下打印:

1
f_mount error:(11) The logical drive number is invalid

这是怎么回事?我们来看一下,报错写的是我们的逻辑驱动器编号不合法,我们看一下我们定义的drive number,发现SPI FLASH是1,这没问题啊,我们从f_mount()函数一级一级往下找,发现我们的驱动器编号收到这个宏的限制 FF_VOLUMES,它表示我们的文件系统最多支持多少个驱动器,也就是存储设备,我们这个宏默认是1,那么我们挂载的时候就只能挂载 “0:” ,我们修改宏为2:

1
2
#define FF_VOLUMES		2
/* Number of volumes (logical drives) to be used. (1-10) */

3.2 FF_MAX_SS

不出意外的话上边宏改完,就不会报11这个错了,但是我们会进入另一个错误:

1
[error]HardFault!!!

这是什么?这是我们之前添加的硬件错误中断中打印的,怎么会发生这样一个错误?一般来讲都是内存溢出,或者访问了空的指针才会出现。我们还记得上边的FATFS结构体吧,里边有一个成员 win[FF_MAX_SS],我们看一下这个宏默认是512,所以这是一个512字节的数组,由于我们定义的fs是个全局变量,所以栈空间应该不会溢出,那怎么回事呢?我们现在用的SPI FLASH一个扇区是4096字节,并且我们在disk_ioctl()函数中已经设置了一个扇区为4096,若是初始化的时候操作SPI FLASH用到这个扇区大小的话,可能就溢出了,这也很有可能就会导致上边的错误,我们来看一下函数调用关系(Source Insight):

image-20230602224918758

发现f_mount()函数挂载会使用到这个成员,那么基本就可以验证我们的猜想了,所以我们需要将此处改为4096:

1
2
3
4
5
6
7
8
#define FF_MIN_SS		512
#define FF_MAX_SS 4096
/* This set of options configures the range of sector size to be supported. (512,
/ 1024, 2048 or 4096) Always set both 512 for most systems, generic memory card and
/ harddisk, but a larger value may be required for on-board flash memory and some
/ type of optical media. When FF_MAX_SS is larger than FF_MIN_SS, FatFs is configured
/ for variable sector size mode and disk_ioctl() function needs to implement
/ GET_SECTOR_SIZE command. */

然后我们再编译下载,就会产生其他错误了,就不会再是这个硬件出错误了:

3.3 FF_USE_MKFS

新的bug:

1
f_mount error:(13) There is no valid FAT volume

这又是什么?There is no valid FAT volume翻译过来就是没有有效的FAT卷,也就是没有文件系统,这样就可以理解了,原本我们的SPI FLASH就什么都没有。那么现在我们就需要格式化SPI FLASH 来创建一个文件系统,这个时候就需要用到f_mkfs()函数啦,我们修改测试函数如下:

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
static FATFS fs = {0}; // FatFs文件系统对象
static BYTE work[FF_MAX_SS]; // 格式化设备工作区
void fatfs_Test(void)
{
FRESULT res = FR_OK;
res = f_mount(&fs, "1:", 1); // 挂载FLASH.
if(res != FR_OK)
{
printf("f_mount error:%s\r\n", API_RET[res]);
if (res == FR_NO_FILESYSTEM) // FLASH磁盘,FAT文件系统错误,重新格式化FLASH
{
printf("Flash Disk Formatting...\r\n"); // 格式化FLASH

res = f_mkfs("1:", 0, work, sizeof(work)); // 格式化FLASH,1,盘符;1,不需要引导区,8个扇区为1个簇
if (res == FR_OK)
{
printf("f_mkfs:Flash Disk Format Finish!!!\r\n"); // 格式化完成
res = f_mount(NULL, "1:", 1); // 格式化后,先取消挂载
res = f_mount(&fs, "1:", 1); // 重新挂载
if(res != FR_OK)
{
printf("f_mount error:%s\r\n", API_RET[res]);
return;
}
}
else
{
printf("f_mkfs error:%s\r\n", API_RET[res]); // 格式化失败
return;
}
}
}
}

然后我们发现编译直接报错:

1
Error: L6218E: Undefined symbol f_mkfs (referred from fatfs_ex.o).

发现没有定义这个符号,也就是没有这个函数,我们找一下这个函数,会发现,它受到这个宏的影响:

image-20230602230210319

我们要向使用这个宏,不能是只读的文件系统,并且还要开启FF_USE_MKFS,FF_FS_READONLY默认就是0,所以不用管。

1
2
3
#define FF_FS_READONLY  0
#define FF_USE_MKFS 1
/* This option switches f_mkfs() function. (0:Disable or 1:Enable) */

3.4 SPI FLASH的一个BUG

然后我们重新编译下载运行,发现我们重新挂载的时候又报错了:

1
2
3
4
f_mount error:(13) There is no valid FAT volume 
Flash Disk Formatting...
f_mkfs:Flash Disk Format Finish!!!
f_mount error:(13) There is no valid FAT volume

上边明明显示我们格式化完成了,但是依然没有成功创建文件系统,网上搜索了资料,配置文件到这里其实就可以了,我们再检查一下是不是写被禁止了,但是上边我们的文件系统根本就不是只读的,那么就有可能是我们底层接口问题了,还记得我们使用SPI FLASH的时候是要先擦除才能写入的,但是我们初始化函数中似乎并没有进行擦除,那么有又怎么把文件系统创建上去呢?所以我们在初始化的时候对扇区进行一次擦除,但是我们的写函数中已经内置了擦除功能了,那问题出在哪?

我后来直接在fatfs_Test()函数调用之前就读写一次SPI FLASH,发现之后就正常了,那么还是我们初始化的问题,问题就出现在我们的Get_W25QXX_Type()函数中,由于我们底层的SPI FLASH还可以支持其他类型的SPI FLASH,所以在使用之前是需要先获取ID的,但是我把这个函数放在了disk_status()中吗,就导致初始化有问题了,这个函数既可以用来确认SPI FLASH的状态,也是初始化所必要的所以我们需要修改一下 disk_initialize() 中对SPI FLASH的初始化,调用一次Get_W25QXX_Type()函数即可,然后再编译,下载运行就会发现,我们可以正常格式化以及挂载文件系统了:

1
2
SPI FLASH ID=0xef17
f_mount success!!!

三、基于SD卡的文件系统

我们先来看一下以SD卡为存储介质的FatFs文件系统的移植。另外就是,我后来移植的过程中,发现我们在使用STM32CubeMX配置SDIO的时候不要开SDIO和DMA中断,不然SD卡读写是没问题的,但是吧,移植好文件系统后,不知道为什么,怎么都跑不起来,具体是哪的影响还不清楚,后边知道了再补充。

1. 底层读写

1.1 初始化

首先我们需要初始化SD卡,这里还是使用STM32CubeMX来配置的SDIO,所以初始化就如下函数所示:

1
void MX_SDIO_SD_Init(void);

【函数说明】这个函数是完成SDIO的初始化,也就完成了对SD卡的初始化。注意这里有两个坑,一个就是初始化的时候的总线宽度设置为1位,还有就是时钟频率,若通信失败,可以降低试一试。这个里边要注意时钟

1.2 获取SD卡信息

这一步主要是为了看一下SD卡是否初始化成功,并且获取一些SD卡的信息,后边使用:

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
HAL_SD_CardInfoTypeDef  SDCardInfo;                 //SD卡信息结构体
void show_sdcard_info(void)
{
uint64_t CardCap; //SD卡容量
HAL_SD_CardCIDTypeDef SDCard_CID;

/* 检测SD卡是否正常(处于数据传输模式的传输状态) */
if(HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER)
{
printf("SD card init fail!\r\n" );
return;
}
printf("Initialize SD card successfully!\r\n");
HAL_SD_GetCardCID(&hsd, &SDCard_CID); //获取CID
HAL_SD_GetCardInfo(&hsd, &SDCardInfo); //获取SD卡信息
switch(SDCardInfo.CardType)
{
case CARD_SDSC:
{
if(SDCardInfo.CardVersion == CARD_V1_X)
printf("Card Type :SDSC V1 \r\n");
else if(SDCardInfo.CardVersion == CARD_V2_X)
printf("Card Type :SDSC V2 \r\n");
}
break;
case CARD_SDHC_SDXC:
printf("Card Type :SDHC \r\n");
break;
}
CardCap=(uint64_t)(SDCardInfo.LogBlockNbr)*(uint64_t)(SDCardInfo.LogBlockSize); //计算SD卡容量
printf("Card ManufacturerID:%d \r\n",SDCard_CID.ManufacturerID); //制造商ID
printf("Card RCA :%d \r\n",SDCardInfo.RelCardAdd); //卡相对地址
printf("LogBlockNbr :%d \r\n",(uint32_t)(SDCardInfo.LogBlockNbr)); //显示逻辑块数量
printf("LogBlockSize :%d \r\n",(uint32_t)(SDCardInfo.LogBlockSize)); //显示逻辑块大小
printf("Card Capacity :%d MB\r\n",(uint32_t)(CardCap>>20)); //显示容量
printf("Card BlockSize :%d \r\n\r\n",SDCardInfo.BlockSize); //显示块大小
}

1.3 读取数据

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
/**
* @brief 从SD卡读取数据
* @note
* @param buf 读数据缓存区
* @param sector 扇区地址
* @param cnt 扇区个数
* @retval 返回错误状态;0,正常;其他,错误代码;
*/
uint8_t SD_ReadDisk(uint8_t *buf, uint32_t sector, uint32_t cnt)
{
uint8_t sta = HAL_OK;
uint32_t timeout = SD_TIMEOUT;
long long lsector = sector;
INTX_DISABLE(); // 关闭总中断(POLLING模式,严禁中断打断SDIO读写操作!!!)
sta = HAL_SD_ReadBlocks(&hsd, (uint8_t *)buf, lsector, cnt, SD_TIMEOUT); // 多个sector的读操作

// 等待SD卡读完
while (SD_GetCardState() != SD_TRANSFER_OK)
{
if (timeout-- == 0)
{
sta = SD_TRANSFER_BUSY;
}
}
INTX_ENABLE(); // 开启总中断
return sta;
}

【函数说明】在指定地址开始读取指定长度的数据。

【参数说明】

  • buf :读数据缓存区
  • sector :扇区地址
  • cnt :扇区个数

1.4 写入数据

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
/**
* @brief 向SD卡写入数据
* @note
* @param buf 写数据缓存区
* @param sector 扇区地址
* @param cnt 扇区个数
* @retval 返回错误状态;0,正常;其他,错误代码;
*/
uint8_t SD_WriteDisk(uint8_t *buf, uint32_t sector, uint32_t cnt)
{
uint8_t sta = HAL_OK;
uint32_t timeout = SD_TIMEOUT;
long long lsector = sector;
INTX_DISABLE(); // 关闭总中断(POLLING模式,严禁中断打断SDIO读写操作!!!)
sta = HAL_SD_WriteBlocks(&hsd, (uint8_t *)buf, lsector, cnt, SD_TIMEOUT); // 多个sector的写操作

// 等待SD卡写完
while (HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER)
{
if (timeout-- == 0)
{
sta = SD_TRANSFER_BUSY;
}
}
INTX_ENABLE(); // 开启总中断
return sta;
}

【函数说明】在指定地址开始写入指定长度的数据。

【参数说明】

  • buf :写数据缓存区
  • sector :扇区地址
  • cnt :扇区个数

2. diskio.c文件的移植

2.1 drive number

前边移植SPI FLASH的时候已经预留了:

1
2
#define DEV_SDCARD      0	/* Example: Map SD card to physical drive 0 */
#define DEV_SPI_FLASH 1 /* Example: Map SPI FLASH to physical drive 1 */

2.2 disk_initialize()

2.2.1 函数声明

首先是初始化函数disk_initialize(),我们可以参考文档FatFs - disk_initialize (elm-chan.org)

1
2
3
DSTATUS disk_initialize (
BYTE pdrv /* [IN] Physical drive number */
);

这个函数初始化存储设备,并使其准备好进行通用读/写。

2.2.2 对SD卡初始化

我们来看一下对SD卡的初始化操作:

1
2
3
4
5
case DEV_SDCARD:
//MX_SDIO_SD_Init();// SDIO初始化
//show_sdcard_info();
result = 0;
break;

2.2.3 移植结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
DSTATUS disk_initialize(
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
int result = 0;

switch (pdrv)
{
case DEV_SDCARD:
//MX_SDIO_SD_Init();// SDIO初始化
//show_sdcard_info();
result = 0;
break;
default:
result = -1;
break;
}
if(result == 0)
return RES_OK;
else
return STA_NOINIT; //初始化失败
}

2.3 disk_status()

2.3.1 函数声明

这个函数主要是获取设备的状态,其实这个函数并不重要,我们甚至可以让它永远返回一个成功测标志。我们可以参考:FatFs - disk_status (elm-chan.org)

1
2
3
DSTATUS disk_status (
BYTE pdrv /* [IN] Physical drive number */
);

2.3.2 对SD卡的状态检测

这里就没检测了,直接返回OK:

1
2
3
case DEV_SDCARD:
result = 0;
break;

2.3.3 移植结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
DSTATUS disk_status(
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
int result = 0;

switch (pdrv)
{
case DEV_SDCARD:
result = 0;
break;
default:
result = -1;
break;
}
if(result == 0)
return RES_OK;
else
return STA_NOINIT;
}

2.4 disk_read()

2.4.1 函数声明

这个函数主要是读取数据,我们可以参考:FatFs - disk_read (elm-chan.org)

1
2
3
4
5
6
DRESULT disk_read (
BYTE pdrv, /* [IN] Physical drive number */
BYTE* buff, /* [OUT] Pointer to the read data buffer */
LBA_t sector, /* [IN] Start sector number */
UINT count /* [IN] Number of sectros to read */
);

【函数参数】

  • pdrv :用于标识目标设备的物理驱动器号。

  • buff :指向字节数组中用于存储读取数据的第一个数据的指针。读取数据的大小将是 扇区大小x字节数。

  • sector :启动扇区号。数据类型 LBA_t 是 DWORD 或 QWORD 的别名,具体取决于配置选项。

  • count :要读取的扇区数。

【注意事项】FatFs每次操作,都是以扇区为基本单位的。

2.4.2 从SD卡读取

1
2
3
case DEV_SDCARD:
result = SD_ReadDisk(buff, sector, count); // 成功时返回0;
break;

2.4.3 移植结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
DRESULT disk_read(
BYTE pdrv, /* Physical drive nmuber to identify the drive */
BYTE *buff, /* Data buffer to store read data */
LBA_t sector, /* Start sector in LBA */
UINT count /* Number of sectors to read */
)
{
int result = 0;

switch (pdrv)
{
case DEV_SDCARD:
result = SD_ReadDisk(buff, sector, count); // 成功时返回0;
break;
default:
result = -1;
break;
}
if(result == 0)
return RES_OK;
else
return RES_ERROR;
}

2.5 disk_write()

2.5.1 函数声明

这个函数主要是写入数据,我们可以参考:FatFs - disk_write (elm-chan.org)

1
2
3
4
5
6
DRESULT disk_write (
BYTE pdrv, /* [IN] Physical drive number */
const BYTE* buff, /* [IN] Pointer to the data to be written */
LBA_t sector, /* [IN] Sector number to write from */
UINT count /* [IN] Number of sectors to write */
);

【函数参数】

  • pdrv :用于标识目标设备的物理驱动器号。

  • buff :指向字节数组中用于存储写入数据的第一个数据的指针。写入数据的大小将是 扇区大小x字节数。

  • sector :启动扇区号。数据类型 LBA_t 是 DWORD 或 QWORD 的别名,具体取决于配置选项。

  • count :要读取的扇区数。

【注意事项】FatFs每次操作,都是以扇区为基本单位的。

2.5.2 向SD卡写入

1
2
3
case DEV_SDCARD:
result =SD_WriteDisk((uint8_t*)buff, sector, count);
break;

这里与前边的读一样,都是以扇区为单位操作,将参数传入底层写入函数的时候需要转换为相应的地址。

2.5.3 移植结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
DRESULT disk_write(
BYTE pdrv, /* Physical drive nmuber to identify the drive */
const BYTE *buff, /* Data to be written */
LBA_t sector, /* Start sector in LBA */
UINT count /* Number of sectors to write */
)
{
int result = 0;

switch (pdrv)
{
case DEV_SDCARD:
result =SD_WriteDisk((uint8_t*)buff, sector, count);
break;
default:
result = -1;
break;
}

if(result == 0)
return RES_OK;
else
return RES_ERROR;
}

2.6 disk_ioctl()

2.6.1 函数声明

这个函数主要是用于控制设备,可根据不同命令执行不同功能,我们可以参考:FatFs - disk_ioctl (elm-chan.org)

1
2
3
4
5
DRESULT disk_ioctl (
BYTE pdrv, /* [IN] Drive number */
BYTE cmd, /* [IN] Control command code */
void* buff /* [I/O] Parameter and data buffer */
);

【函数参数】

  • pdrv :用于标识目标设备的物理驱动器号。

  • cmd :命令编号,比如GET_SECTOR_COUNT、GET_SECTOR_SIZE、GET_BLOCK_SIZE等,详细的命令及含义可以参考官方文档说明。

  • buff :参数的指针取决于命令代码。不要在意该命令是否没有要传递的参数。

2.6.2 获取SD卡信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
case DEV_SDCARD:
switch(cmd)
{
case GET_SECTOR_COUNT:// 扇区数量
*(DWORD *)buff = SDCardInfo.LogBlockNbr;
break;
case GET_SECTOR_SIZE:// 扇区大小
*(WORD *)buff = SDCardInfo.BlockSize;// 每个扇区是512B
break;
case GET_BLOCK_SIZE:// 每次擦除块的大小的个数
*(DWORD *)buff = SDCardInfo.LogBlockSize;
default:
break;
}
result = 0;
break;

2.6.3 移植结果

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
DRESULT disk_ioctl(
BYTE pdrv, /* Physical drive nmuber (0..) */
BYTE cmd, /* Control code */
void *buff /* Buffer to send/receive control data */
)
{
int result = 0;

switch (pdrv)
{
case DEV_SDCARD:
switch(cmd)
{
case GET_SECTOR_COUNT:// 扇区数量
*(DWORD *)buff = SDCardInfo.LogBlockNbr;
break;
case GET_SECTOR_SIZE:// 扇区大小
*(WORD *)buff = SDCardInfo.BlockSize;// 每个扇区是512B
break;
case GET_BLOCK_SIZE:// 每次擦除块的大小的个数
*(DWORD *)buff = SDCardInfo.LogBlockSize;
default:
break;
}
result = 0;
break;
default:
result = -1;
break;
}

if(result == 0)
return RES_OK;
else
return RES_ERROR;
}

3. ffconf.h文件的配置

这里的配置基本不变。

3.1 FF_VOLUMES

1
2
#define FF_VOLUMES		2
/* Number of volumes (logical drives) to be used. (1-10) */

3.2 FF_MAX_SS

1
2
3
4
5
6
7
8
#define FF_MIN_SS		512
#define FF_MAX_SS 4096
/* This set of options configures the range of sector size to be supported. (512,
/ 1024, 2048 or 4096) Always set both 512 for most systems, generic memory card and
/ harddisk, but a larger value may be required for on-board flash memory and some
/ type of optical media. When FF_MAX_SS is larger than FF_MIN_SS, FatFs is configured
/ for variable sector size mode and disk_ioctl() function needs to implement
/ GET_SECTOR_SIZE command. */

3.3 FF_USE_MKFS

1
2
3
#define FF_FS_READONLY  0
#define FF_USE_MKFS 1
/* This option switches f_mkfs() function. (0:Disable or 1:Enable) */

四、一些测试函数

最终的代码可以看这里:STM32F103-Prj: STM32学习使用(STM32CubeMX+Makefile+VScode+J-Flash) (gitee.com)

1. 文件系统结构体定义

为了方便管理文件系统,定义一个结构体:

1
2
3
4
5
6
7
8
9
10
11
typedef struct __ex_fatfs
{
FATFS *fs[FF_VOLUMES]; // 逻辑磁盘工作区.
BYTE *work; // 挂载的时候用
FIL *file; // 文件1
FIL *ftemp; // 文件2
UINT br; // 读指针
UINT bw; // 写指针
FILINFO fileinfo; // 文件信息
DIR dir; // 目录
} EX_FATFS_PARAM;

2. 申请内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @brief 为文件系统所用变量申请内存
* @note
* @param
* @retval 0,申请成功,-1,申请失败
*/
int8_t exf_init(void)
{
uint8_t i;
for (i = 0; i < FF_VOLUMES; i++)
{
exFatfsParam.fs[i] = (FATFS *)pub_malloc(SRAMIN, sizeof(FATFS)); // 为磁盘i工作区申请内存
if (!exFatfsParam.fs[i])
break;
}
exFatfsParam.file = (FIL *)pub_malloc(SRAMEX, sizeof(FIL)); // 为file申请内存
exFatfsParam.ftemp = (FIL *)pub_malloc(SRAMEX, sizeof(FIL)); // 为ftemp申请内存
exFatfsParam.work = (uint8_t *)pub_malloc(SRAMEX, FF_MAX_SS); // 为work申请内存
if (i == FF_VOLUMES && exFatfsParam.file && exFatfsParam.ftemp && exFatfsParam.work)
return 0; // 申请有一个失败,即失败.
else
return -1;
}

3. 获取磁盘容量

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

/**
* @brief 得到磁盘剩余容量
* @note 好像是去掉了FatFs自身占据的空间
* @param drv 磁盘编号("0:"/"1:")
* @param total 总容量 (单位KB)
* @param free 剩余容量 (单位KB)
* @retval 0,正常.其他,错误代码
*/
uint8_t exf_getfree(uint8_t *drv, uint32_t *total, uint32_t *free)
{
FATFS *fs1;
uint8_t res;
uint32_t fre_clust = 0, fre_sect = 0, tot_sect = 0;
// 得到磁盘信息及空闲簇数量
res = (uint32_t)f_getfree((const TCHAR *)drv, (DWORD *)&fre_clust, &fs1);
if (res == 0)
{
tot_sect = (fs1->n_fatent - 2) * fs1->csize; // 得到总扇区数
fre_sect = fre_clust * fs1->csize; // 得到空闲扇区数
//printf("fs1->n_fatent %d, fs1->csize %d, tot_sect %d, fre_sect %d\r\n", fs1->n_fatent, fs1->csize, tot_sect, fre_sect);
#if FF_MAX_SS != 512 // 扇区大小不是512字节,则转换为512字节
tot_sect *= fs1->ssize / 512;
fre_sect *= fs1->ssize / 512;
#endif
*total = tot_sect >> 1; // 单位为KB
*free = fre_sect >> 1; // 单位为KB
}
return res;
}