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.html | ST官方网站,在这里我们可以找到STM32的相关文档 | |
https://www.stmcu.com.cn/ | 意法半导体ST中文官方网站,在这里我们可以找到STM32的相关中文参考文档 | |
http://elm-chan.org/fsw/ff/00index_e.html | FatFs文件系统官网 | |
教程书籍 | 《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
STM32 | STM32 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 | /* Filesystem object structure (FATFS) */ |
FATFS结构(文件系统对象)保存各个逻辑驱动器的动态工作区域。它由应用程序给出,并通过f_mount函数注册/取消注册到FatFs模块。结构的初始化在必要时由卷挂载进程完成。应用程序不得修改此结构中的任何成员,否则将导致FAT卷崩溃。
我们要注意一下这个结构体是非常大的,我们可以看到最后一个成员win,这是一个数组,大小为FF_MAX_SS,这个最大值我们后边可能会配置为4096,这样下来这个结构体就非常的大,用它定义的变量最好不要定义为局部变量,否则可能会导致栈的溢出,当然,要是我们的栈非常大的话吗,就可以不用考虑这个问题了。
1.2 FIL
关于该结构体,它定义在ff.h中,我们可以参考官方文档:FatFs - FIL (elm-chan.org)
1 | /* File object structure (FIL) */ |
FIL结构(文件对象)保存打开文件的状态。它由f_open函数创建,并由f_close函数丢弃。应用程序不能修改该结构中除cltbl之外的任何成员,否则将导致FAT卷崩溃。请注意,扇区缓冲区是在非微小配置(FF_FS_TINY == 0)的结构中定义的,因此该配置中的FIL结构不应该被定义为自动变量。
我们要注意一下这个结构体是非常大的,我们可以看到最后一个成员buf,这是一个数组,大小为FF_MAX_SS,这个最大值我们后边可能会配置为4096,这样下来这个结构体跟FATFS一样,就非常的大,所以一般我们也不会将其定义为局部变量。
2. 函数返回值
2.1 返回值的枚举类型
我们首先来了解一下我们常用的一些函数的返回值的含义,我们可以通过这些返回值来判断是哪里出错了:
1 | /* File function return code (FRESULT) */ |
这是一个枚举类型,它也定义在ff.h中,我们可以参考官方文档来详细了解这些值的含义:FatFs - API Return Code (elm-chan.org)
2.2 字符串数组?
上边返回值好多,我们知道值的话,还需要知道代表什么含义,我们其实可以定义一个二维数组,按顺序将上边的枚举所代表的含义依次写入,然后打印的时候就可以直接获取错误码的含义了,不过这样可能会比较占内存,慎用。
1 | static BYTE API_RET[][90] = { |
我们打印的时候就可以这样:
1 | printf("res:%s\n\r", API_RET[res]); |
这样就可以直接将错误的原因打印出来啦。
3. 常用函数
3.1 f_mount()
我们可以参考FatFs - f_mount (elm-chan.org):
1 | FRESULT f_mount ( |
【函数说明】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 | FRESULT f_mkfs ( |
【函数说明】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 | /** |
二、基于SPI FLASH的文件系统
我们先来看一下以SPI FLASH为存储介质的FatFs文件系统的移植。
1. 底层读写
1.1 初始化
首先我们需要初始化SPI FLASH,在这里我使用的是W25Q128,由于我使用的是STM32CubeMX来配置的SPI,所以初始化就如下函数所示:
1 | /* SPI2 init function */ |
1.2 获取SPI FLASH器件ID
这一步其实本不是必须,但是由于代码支持了其他型号的SPI FLASH,所以这一步也就成了必要的步骤,其实也有好处,那就是可以通过读取ID来判断我们的SPI FLASH是否已经初始化完成:
1 | /** |
1.3 读取数据
1 | /** |
1.4 写入数据
1 | // 动态管理内存 |
2. diskio.c文件的移植
接下来,就来一步一步移植这个文件系统,然后看一看中间可能会有什么坑。这一部分主要都是在diskio.c文件中进行移植,这里先不关心配置文件,后边测试的时候会根据出错的情况一步一步修改配置文件。
2.1 drive number
首先,我们需要定义我们的盘符,在 diskio.c 文件的开头有以下几个宏:
1 | /* Definitions of physical drive number for each drive */ |
这些宏就表示了不同的存储介质,后边会根据这些宏来调用不同的初始化及读写函数去操作对应的存储设备。我们这里定义两个,一个是SD卡(预留,后边会使用),一个是SPI FLASH:
1 |
2.2 disk_initialize()
2.2.1 函数声明
首先是初始化函数disk_initialize(),我们可以参考文档FatFs - disk_initialize (elm-chan.org):
1 | DSTATUS disk_initialize ( |
这个函数初始化存储设备,并使其准备好进行通用读/写。
2.2.2 对SPI FLASH初始化
我们来看一下对SPI FLASH的初始化操作:
1 | case DEV_SPI_FLASH: |
由于我使用的是STM32CubeMX来配置的SPI,所以当STM32CubeMX生成工程的时候,会直接在main函数中进行SPI初始化函数的调用,所以为了后边修改工程方便,这里就不再放对SPI的初始化了。
2.2.3 移植结果
1 | DSTATUS disk_initialize( |
2.3 disk_status()
2.3.1 函数声明
这个函数主要是获取设备的状态,其实这个函数并不重要,我们甚至可以让它永远返回一个成功测标志。我们可以参考:FatFs - disk_status (elm-chan.org)
1 | DSTATUS disk_status ( |
2.3.2 对SPI FLASH的状态检测
我们可以将获取W25Q128器件ID的操作放在状态读取的函数中,这样也可以确保我们的SPI FLASH可以正常运行:
1 | case DEV_SPI_FLASH: |
2.3.3 移植结果
1 | DSTATUS disk_status( |
2.4 disk_read()
2.4.1 函数声明
这个函数主要是读取数据,我们可以参考:FatFs - disk_read (elm-chan.org)
1 | DRESULT disk_read ( |
【函数参数】
pdrv :用于标识目标设备的物理驱动器号。
buff :指向字节数组中用于存储读取数据的第一个数据的指针。读取数据的大小将是 扇区大小x字节数。
sector :启动扇区号。数据类型 LBA_t 是 DWORD 或 QWORD 的别名,具体取决于配置选项。
count :要读取的扇区数。
【注意事项】FatFs每次操作,都是以扇区为基本单位的。
2.4.2 从SPI FLASH读取
1 | case DEV_SPI_FLASH: |
前边我们知道,W25Q128将16MB的容量分为256个块(Block),每个块大小为64K字节,每个块又分为16个扇区(Sector),每个扇区4K个字节。而我们操作W25Q128的时候每次也是擦除一个扇区,也就是4KB,16M的空间,一共就有4096个扇区,而扇区号乘以扇区大小就可以得到整个扇区的起始地址。count 表示扇区的数量,每个扇区4KB,相乘就可以得到要读取的字节数量。
2.4.3 移植结果
1 | DRESULT disk_read( |
2.5 disk_write()
2.5.1 函数声明
这个函数主要是写入数据,我们可以参考:FatFs - disk_write (elm-chan.org)
1 | DRESULT disk_write ( |
【函数参数】
pdrv :用于标识目标设备的物理驱动器号。
buff :指向字节数组中用于存储写入数据的第一个数据的指针。写入数据的大小将是 扇区大小x字节数。
sector :启动扇区号。数据类型 LBA_t 是 DWORD 或 QWORD 的别名,具体取决于配置选项。
count :要读取的扇区数。
【注意事项】FatFs每次操作,都是以扇区为基本单位的。
2.5.2 向SPI FLASH写入
1 | case DEV_SPI_FLASH: |
这里与前边的读一样,都是以扇区为单位操作,将参数传入底层写入函数的时候需要转换为相应的地址。
2.5.3 移植结果
1 | DRESULT disk_write( |
2.6 disk_ioctl()
2.6.1 函数声明
这个函数主要是用于控制设备,可根据不同命令执行不同功能,我们可以参考:FatFs - disk_ioctl (elm-chan.org)
1 | DRESULT disk_ioctl ( |
【函数参数】
pdrv :用于标识目标设备的物理驱动器号。
cmd :命令编号,比如GET_SECTOR_COUNT、GET_SECTOR_SIZE、GET_BLOCK_SIZE等,详细的命令及含义可以参考官方文档说明。
buff :参数的指针取决于命令代码。不要在意该命令是否没有要传递的参数。
2.6.2 获取SPI FLASH信息
1 | case DEV_SPI_FLASH: |
2.6.3 移植结果
1 | DRESULT disk_ioctl( |
3. ffconf.h文件的配置
这一节我们通过报错来看一下不同的配置项有什么影响。我们这部分以例子来说明。
3.1 FF_VOLUMES
测试函数我们用f_mount来尝试:
1 | static FATFS fs = {0}; // FatFs文件系统对象 |
然后我们调用并打印,会有如下打印:
1 | f_mount error:(11) The logical drive number is invalid |
这是怎么回事?我们来看一下,报错写的是我们的逻辑驱动器编号不合法,我们看一下我们定义的drive number,发现SPI FLASH是1,这没问题啊,我们从f_mount()函数一级一级往下找,发现我们的驱动器编号收到这个宏的限制 FF_VOLUMES,它表示我们的文件系统最多支持多少个驱动器,也就是存储设备,我们这个宏默认是1,那么我们挂载的时候就只能挂载 “0:” ,我们修改宏为2:
1 |
|
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):
发现f_mount()函数挂载会使用到这个成员,那么基本就可以验证我们的猜想了,所以我们需要将此处改为4096:
1 |
|
然后我们再编译下载,就会产生其他错误了,就不会再是这个硬件出错误了:
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 | static FATFS fs = {0}; // FatFs文件系统对象 |
然后我们发现编译直接报错:
1 | Error: L6218E: Undefined symbol f_mkfs (referred from fatfs_ex.o). |
发现没有定义这个符号,也就是没有这个函数,我们找一下这个函数,会发现,它受到这个宏的影响:
我们要向使用这个宏,不能是只读的文件系统,并且还要开启FF_USE_MKFS,FF_FS_READONLY默认就是0,所以不用管。
1 |
|
3.4 SPI FLASH的一个BUG
然后我们重新编译下载运行,发现我们重新挂载的时候又报错了:
1 | 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 | SPI FLASH ID=0xef17 |
三、基于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 | HAL_SD_CardInfoTypeDef SDCardInfo; //SD卡信息结构体 |
1.3 读取数据
1 | /** |
【函数说明】在指定地址开始读取指定长度的数据。
【参数说明】
- buf :读数据缓存区
- sector :扇区地址
- cnt :扇区个数
1.4 写入数据
1 | /** |
【函数说明】在指定地址开始写入指定长度的数据。
【参数说明】
- buf :写数据缓存区
- sector :扇区地址
- cnt :扇区个数
2. diskio.c文件的移植
2.1 drive number
前边移植SPI FLASH的时候已经预留了:
1 |
2.2 disk_initialize()
2.2.1 函数声明
首先是初始化函数disk_initialize(),我们可以参考文档FatFs - disk_initialize (elm-chan.org):
1 | DSTATUS disk_initialize ( |
这个函数初始化存储设备,并使其准备好进行通用读/写。
2.2.2 对SD卡初始化
我们来看一下对SD卡的初始化操作:
1 | case DEV_SDCARD: |
2.2.3 移植结果
1 | DSTATUS disk_initialize( |
2.3 disk_status()
2.3.1 函数声明
这个函数主要是获取设备的状态,其实这个函数并不重要,我们甚至可以让它永远返回一个成功测标志。我们可以参考:FatFs - disk_status (elm-chan.org)
1 | DSTATUS disk_status ( |
2.3.2 对SD卡的状态检测
这里就没检测了,直接返回OK:
1 | case DEV_SDCARD: |
2.3.3 移植结果
1 | DSTATUS disk_status( |
2.4 disk_read()
2.4.1 函数声明
这个函数主要是读取数据,我们可以参考:FatFs - disk_read (elm-chan.org)
1 | DRESULT disk_read ( |
【函数参数】
pdrv :用于标识目标设备的物理驱动器号。
buff :指向字节数组中用于存储读取数据的第一个数据的指针。读取数据的大小将是 扇区大小x字节数。
sector :启动扇区号。数据类型 LBA_t 是 DWORD 或 QWORD 的别名,具体取决于配置选项。
count :要读取的扇区数。
【注意事项】FatFs每次操作,都是以扇区为基本单位的。
2.4.2 从SD卡读取
1 | case DEV_SDCARD: |
2.4.3 移植结果
1 | DRESULT disk_read( |
2.5 disk_write()
2.5.1 函数声明
这个函数主要是写入数据,我们可以参考:FatFs - disk_write (elm-chan.org)
1 | DRESULT disk_write ( |
【函数参数】
pdrv :用于标识目标设备的物理驱动器号。
buff :指向字节数组中用于存储写入数据的第一个数据的指针。写入数据的大小将是 扇区大小x字节数。
sector :启动扇区号。数据类型 LBA_t 是 DWORD 或 QWORD 的别名,具体取决于配置选项。
count :要读取的扇区数。
【注意事项】FatFs每次操作,都是以扇区为基本单位的。
2.5.2 向SD卡写入
1 | case DEV_SDCARD: |
这里与前边的读一样,都是以扇区为单位操作,将参数传入底层写入函数的时候需要转换为相应的地址。
2.5.3 移植结果
1 | DRESULT disk_write( |
2.6 disk_ioctl()
2.6.1 函数声明
这个函数主要是用于控制设备,可根据不同命令执行不同功能,我们可以参考:FatFs - disk_ioctl (elm-chan.org)
1 | DRESULT disk_ioctl ( |
【函数参数】
pdrv :用于标识目标设备的物理驱动器号。
cmd :命令编号,比如GET_SECTOR_COUNT、GET_SECTOR_SIZE、GET_BLOCK_SIZE等,详细的命令及含义可以参考官方文档说明。
buff :参数的指针取决于命令代码。不要在意该命令是否没有要传递的参数。
2.6.2 获取SD卡信息
1 | case DEV_SDCARD: |
2.6.3 移植结果
1 | DRESULT disk_ioctl( |
3. ffconf.h文件的配置
这里的配置基本不变。
3.1 FF_VOLUMES
1 |
|
3.2 FF_MAX_SS
1 |
|
3.3 FF_USE_MKFS
1 |
|
四、一些测试函数
最终的代码可以看这里:STM32F103-Prj: STM32学习使用(STM32CubeMX+Makefile+VScode+J-Flash) (gitee.com)
1. 文件系统结构体定义
为了方便管理文件系统,定义一个结构体:
1 | typedef struct __ex_fatfs |
2. 申请内存
1 | /** |
3. 获取磁盘容量
1 |
|