赞
踩
SPI读写SD卡的系列文章链接:
【ESP-IDF】ESP32S3用SPI读写 MicroSD/TF卡(一)SD卡初始化_esp sd spi-CSDN博客 【ESP-IDF】ESP32S3用SPI读写 MicroSD/TF卡(二)读写正文数据_spi读写sd卡驱动需要自己写吗-CSDN博客
上两篇完成了SD卡物理驱动层的通讯了,这篇准备安装FATFS系统控制SD卡读写。这篇主要介绍移植FATFS过程和遇到的问题。由于我失误烧坏了一张256MB的sd卡,买了张新的32GB SDHC卡,花了些时间适应FAT32系统,其中的问题也值得分享。
FATFS系统是一套数据组织的协议,其实很多设备都能装FATFS系统,PC、FLASH、SD卡都行,只要将FATFS系统代码烧录在SD卡上并做好格式化,主机(host)就懂得根据FATFS的协议寻找数据。FATFS特点就是将文件系统内容像普通数据一样放在SD卡存储区,人为逻辑上划分为“引导区”(像电脑的C盘),但SD卡不会特殊对待这部分内容,仍是可读可写,一旦改写了些信息,系统可能会完蛋。
根据SD卡的容量大小FATFS细分为FAT16(SDSC卡)、FAT32(SDHC卡)、exFAT(SDXC卡)系统等,大同小异,位数越多才能映射更多存储地址,容量才能扩大。上一张卡256MB是SDSC卡用FAT16,这张卡是32GB SDHC卡用FAT32,因为一些小细节差异差点翻车,后面会分享翻车事故。
FATFS有什么作用?实际上可以不用文件系统,用上两篇文章的知识用CMD24将数据烧录进SD卡,需要的时候用CMD17从SD卡读出来,而且1 bit空间都不浪费,要知道所有的储存设备都不是满额利用的,标32GB的SD卡,实际能利用只有28.9GB,因为有一部分储存空间用来存放文件系统。但自己硬核物理控制SD卡的话,要思考这些问题:下次如何寻找存放的数据位置?如果数据内容长度需要增加,但后面的存储地址空间已放了别的文件数据,岂不是很尴尬?每次检索文件名是否要从头检索32GB内容,这也太慢了?CMD24写数据命令一次要写512byte,但很多时候我们只想修改txt文档的2个字,这该怎样做?
FATFS文件系统就是解决以上问题而生的。面向用户这一方面,会有一个名为FIL的handle记录每个文件的属性,比如文件名称hello.txt、文件大小12KB等,要找某文档的信息就去找handle的属性值即可实现文档操控;面向SD卡这一面,函数如initialize()会调用CMD8、ACMD41等SD卡命令对接SD卡物理驱动底层实现控制。
FATFS系统代码是开源的,在这个网址:
FatFs - Generic FAT Filesystem Module
可以到这个网址把最新的FATFS系统下载,然后创建组件的形式把它扔进组件中。这个网址有时国内打不开。我也想把整个文档传上来分享,但CSDN老是自己搞资源收费。虽然ESP官方组件库就包含了FATFS组件代码,但咱们主打一个独立自主[狗头],不依靠ESP库。
FATFS函数详细介绍,下面链接这个老哥的文章就很清晰了:
移植后的ESP-IDF是这样:
FATFS源码库我几乎全放进来了,差ffsystem.c我没放,可以不用放。ffunicode.c也不是必需,但我想文档能用中文。
fatfs2.h和fatfs2.c是我自己建立的,不是FATFS库文件,我把上两篇文章的物理驱动层函数和指令如ACMD41、CMD17放到这两个自建文件中,FATFS官方库需要借用这部分物理层程序。
component里的CMakelist.txt内容这么写:
- idf_component_register(SRCS "fatfs2.c" "ff.c" "diskio.c" "ffunicode.c"
- INCLUDE_DIRS "include"
- REQUIRES driver)
FATFS文档里有7个.c和.h文件,要实现移植操作,只需要修改以下两个:
(1)diskio.c。这个要修改。io顾名思义就是输入输出,这个文件作用是将FATFS系统与物理驱动层产生关联的。由于CMD0、CMD8、CMD24等SD卡指令我放在了fatfs2.c的函数里面,因此diskio.c里的函数如disk_read()内容中只要调用fatfs2.c里的自定义函数read_card()就可实现读SD卡。这个文件对接的是底层物理驱动层,用户在应用层不需要使用diskio.c这个文件里的函数。用户调用f_open()后,文件系统会自己操作调用diskio.c里各种函数完成f_open()任务。
(2)ffconf.h。这个也要修改。这个头文件没有对应的源文件,是用来设定文件系统的参数的。比如分多少个区、是否支持中文命名等等就在这儿设定,用户手动改里面#define的值即可。
完成了以上两个文件的修改,从文档上就算移植完成FATFS了,后面可以直接用FATFS面向用户的应用函数如f_open()进行文档操作。ff.h就是面向用户应用层的函数接口,这个不用修改。
我用FATFS的最新R0.15版本,这个版本竟然没有自带get_fattime()函数,编译时警告提示缺少这个函数,所以得自己手动补上,因此我摆到第一个位置来讲。下面这段来自FATFS官方。
- DWORD get_fattime(void)
- {
- time_t t = time(NULL);
- struct tm tmr;
- localtime_r(&t, &tmr);
- int year = tmr.tm_year < 80 ? 0 : tmr.tm_year - 80;
- return ((DWORD)(year) << 25)
- | ((DWORD)(tmr.tm_mon + 1) << 21)
- | ((DWORD)tmr.tm_mday << 16)
- | (WORD)(tmr.tm_hour << 11)
- | (WORD)(tmr.tm_min << 5)
- | (WORD)(tmr.tm_sec >> 1);
- }
get_fattime( )这个函数内容可以为空,直接写return RES_OK就行,RES_OK对应值0。时间不正确也不会影响文件系统正常发挥。但若不把函数列出来就编译不过。我的代码是调用了ESP时钟系统time.h。
用户调用f_mount( )或f_mkfs()时就会自动调用这个函数进行初始化。实际上就是完成CMD0、CMD8、CMD55、ACMD41这个SD卡物理初始化过程,我把实现过程放在自己编的card_init()函数里。反正你随心所欲去初始化,去到SD卡能开始读写数据那步就行,FATFS不关心你实现的过程,不用特意返回什么信息或值。pdrv这个输入参数忽略,不用它也没关系,我只挂载一个SD卡存储设备,所以pdrv不用选了,系统默认就是0号物理驱动盘。disk_initialize( )函数最后记得return RES_OK。
- DSTATUS disk_initialize (BYTE pdrv)
- /* Physical drive nmuber to identify the drive */
- {
- card_init();
- return RES_OK;
- }
大家记得区分清楚哪些故障是SD卡物理层故障,哪些是FATFS文件系统故障。这个函数里面出现的故障理应是SD卡物理层故障。SD卡某个数据储存区损坏不会影响初始化,但SD卡引脚损坏、控制寄存器损坏就会导致初始化失败。
如何分辨是哪儿失败?如果把SD卡插入PC电脑,电脑能识别这个磁盘插入,哪怕提示什么磁盘错误磁盘损坏,都是有救的,说明SD卡物理层初始化成功,因此电脑才能跟SD卡通讯,才能知道是磁道损坏。如果电脑根本不能显示SD卡插入,大概率就是烧坏了,我上一张卡就是这么坏。
官方输入参数有4个,实际上我只用了2个,其他两个忽略也没关系。除了pdrv,count这个参数也不用了,因为read_card()这个自编函数只读单个block 512bytes。
- 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 */
- )
- {
- sector=sector+64;//物理地址要加上64块MBR隐藏区
- read_card(sector,buff);
- return RES_OK;
- }
注意这儿的sector参数,我在这翻车了。官方注释不是很清晰,这儿指的是第几个sector,不是sector的地址值!而且LBA含义是,这个参数代表的是逻辑值,不是物理值!这个输入参数是个十进制数,需要计算出sector的物理地址值,才能传递给SD卡的CMD17命令。这次是翻车在FATFS系统上!
另外,在address这儿还翻了一次车,翻在了SD卡物理层上!上一张卡256MB容量,是SDSC卡,CMD17命令参数要求填物理地址,1 byte一个地址值。而本次的卡是32GB的SDHC卡,CMD17参数是填block号码的!所以我按原来代码读写很多次都找不出来数据,还把SD卡MBR区搞坏了,重新格式化才救回来。排查了很久原因,又回去仔细读SD卡数据手册,才发现这行刻骨铭心的小字(下图)......这么重要的信息却小得离谱。
一个block(也叫cluster簇)包含多少个sector扇区由你设定,我设为1。
至于为什么代码里面sector值要加上64个block,用读卡器插入电脑,打开winhex软件。打开逻辑驱动盘,看逻辑地址0x00000000所在的扇区,右边,逻辑扇区号0对应物理扇区号64,所以结论是:逻辑地址+64block=物理地址。为什么要做这个转换?因为disk_read()函数参数传来的值是逻辑扇区号,而SD卡物理层需要的参数是物理扇区号。
这个跟read很像了。
- 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 */
- )
- {
- sector=sector+64;
- write_card(sector,(uint8_t *)buff);
- return RES_OK;
- }
无论是读还是写过程,出现故障,还是要靠物理层驱动代码来提示的,也可以把故障代码传到disk_write()里面,需一一对应转换成FATFS库的故障提示码。
这个函数叫io control,可以不修改,若你要调用f_fdisk()、f_mkfs()进行分区和格式化的话,就必须修改这个函数。这个函数作用是读SD卡的容量大小等参数,将结果返回给FATFS系统。
看上图,如果要用格式化(MKFS==1)功能的话,必须要在disk_ioctl( )里面对CTRL_SYNC、GET_BLOCK_SIZE、GET_SECTOR_SIZE这三种命令返回给FATFS想要的数值,意思就是,若FATFS传来GET_BLOCK_SIZE命令的话,disk_ioctl( )函数要返回512这个结果。具体代码看下面:
- DRESULT disk_ioctl (
- BYTE pdrv, /* Physical drive nmuber (0..) */
- BYTE cmd, /* Control code */
- void *buff /* Buffer to send/receive control data */
- )
- {
- switch (cmd)
- {
- case GET_SECTOR_COUNT:
- *(DWORD*)buff=0x4000000;
- break;
- case GET_SECTOR_SIZE:
- *(WORD*)buff=512;
- break;
- case GET_BLOCK_SIZE:
- *(WORD*)buff=1;
- break;
- }
- return RES_OK;
- }

简单粗暴直接将buff赋值一个数字就行,FATFS系统会自己使用buff里的值。注意函数参数的*buff,既可以是输入参数,也可以是输出参数,很特别。
这个函数最不重要了,内容里面直接写return RES_OK就行。
至此,diskio.c的内容就写完,完成了所有的关联。下一步就是在ffconf.h配置一下文件系统的参数。
这个文件最好理解,官方注释也很丰富,一般来说不用做太多配置修改,我的话就以下几点:
- #define FF_FS_READONLY 0
- #define FF_USE_MKFS 1
-
- #define FF_VOLUMES 1
-
- #define FF_STR_VOLUME_ID 0
- #define FF_VOLUME_STRS "SD"
-
- #define FF_MULTI_PARTITION 0
-
- #define FF_MIN_SS 512
- #define FF_MAX_SS 512
我不分区,一个物理盘对应一个逻辑盘,可读可写,一个block512字节。若一个block想变成2kb,就将FF_MAX_SS改成2048,若SD卡储存大体积文件如音视频文件的话,是很有必要将block扩大的,增加读写效率。
至此,FATFS在ESP-IDF上的移植代码部分就完成。下一步,要将这个系统刻进SD卡才能运作,主程序里会完成这事。
这个是自己创建的文件,不是官方文件。我把上两篇文章的底层物理驱动层代码塞进来,#include fatfs2.h就行。注意spi_device_handle_t和spi_device_interface_config_t我用了全局静态变量,这是有必要的,无论是哪个文件调用这些物理驱动层函数,都会涉及这两个变量。这变量涉及SPI的配置,需要有记忆性。另外我还设置了tx_dummy[514],供每次spi transaction使用。
接下来就是应用的过程了。
格式化的操作包含了SD卡初始化、SD卡存储区域全部清除、将FATFS标准系统(MBR、FAT表、根目录等)刻写在SD卡特定区域。如果SD卡上已有正常使用的系统的话,就不需要格式化这步骤。调用f_mkfs( )我试过很多次失败。mkfs应该是make filesystem简写,按照英文翻译为创建系统可能更直接一些。
由于我们不分区,一个物理盘上只有一个volume,因此不用调用f_fdisk()来分区了,否则主程序第一句应该先分区,再格式化。我们现在直接格式化就可。
- FRESULT res;
- BYTE work[4*FF_MAX_SS]={0};
- // 格式化
- MKFS_PARM mkfscnf={
- .align=1,
- .au_size=512,
- .fmt=FM_FAT32,
- .n_fat=1,
- .n_root=0,
- };
- res=f_mkfs("0:",&mkfscnf,work,sizeof(work));
- printf("f_mkfs:%d\n",res);
- f_mount(NULL,MOUNT_POINT,1);
网上有人说格式化前若磁盘有别的内容可能导致格式化失败,所以要先用SD卡协议的CMD32 CMD33 CMD38命令擦除干净SD卡再来格式化。每家SD卡制造商都不同,有的是用0xFF来擦除,有的是0x00。
注意不要把MBR的0 block擦除了,MBR是厂家烧录进去的信息,64个sector那么大,保存了SD卡的物理参数,一般用电脑打开的话是看不到这个区域的。如果MBR没了,就要先做f_fdisk()建立分区,再格式化。推荐用disk_genius软件-->新建分区-->格式化。
我们用电脑查看的话只能看逻辑扇区,看不到MBR引导区,因为逻辑扇区才是存储数据的起点,而用winhex软件或disk_genius软件查看的话,MBR引导区也能看到。我们调用ACMD41、CMD17等SD命令操作底层物理驱动层的话,则是不分逻辑或物理的,都是操作物理扇区,因为SD卡是不懂文件系统的, 文件系统对他来说也是一般数据的读写。逻辑扇区是人为划分出来的。
格式化完要unmount,所以有句f_mount(NULL,MOUNT_POINT,1)。
f_mount()这个函数叫挂载。挂载是指完成这些操作:SD卡通讯初始化、通过read()读出SD卡内的文件系统的信息、文件系统属性值传递给handle。SD卡存储区域中的MBR区域和DBR区域存放了文件系统的所有信息,包括sector size、逻辑区起始位置、FAT表情况等,将这些文件系统的属性赋值给一个结构体handle(全局变量),以后操作这个handle就代表操作这张SD卡文件系统。挂载对于ESP32来讲就是内存中多了个handle实例。
- FATFS fs;
- #define MOUNT_POINT "0:"
-
- ***********以上是全局变量********************
- res=f_mount(&fs,MOUNT_POINT,1);
- printf("f_mount:%d\n",res);
挂载完文件系统后,就可以自由冲浪了,使用文件的各种操作。
f_open( )函数既可以创建新文件,也可以打开旧文件。创建新文件的话,涉及SD卡引导区域的FAT表、根目录。
- FIL mmtxt;
- res=f_open(&mmtxt,"/kae.txt",FA_READ);
- printf("f_open:%d\n",res);
- if(res!=FR_OK)
- {
- f_close(&mmtxt);
- return;
- }
open文件的本质与mount系统的本质很像。创建一个FIL类型的handle,名称为mmtxt。将该文件的各种属性赋予给这个handle,包括文件名称(kae.txt)、文件大小、储存簇地址等,以后操作mmtxt这个handle就等于操作名为kae.txt的文档了。
官方文档说,不能同时打开两个写操作的file,否则数据会崩溃,切记!
f_read()最后一个参数br代表byte read(已读的字节数量)。我这儿想读10个byte,但kae.txt文件里面只有7个字,最终*br的内容就是7,该block除了前面7个byte,后面的值都是0x00,文件系统认为0x00不是任何信息,所以返回7。
- UINT br;
- uint8_t read_buf[100];
- res=f_read(&mmtxt,read_buf,10,&br);
- printf("f_read:%d\n",res);
- f_close(&mmtxt);
- f_unmount(MOUNT_POINT);
到这儿为止,就能实现ESP32S3通过SPI模式控制SD卡,不依赖ESP官方库,靠自己写驱动程序和移植外部FATFS文件系统,成就达成。
第一部分先搞定SPI硬件,第二部分搞定SD卡物理初始化(在此过程学SD卡的寄存器操作方式),第三部分读和写SD卡数据和擦除数据,第四部分安装FATFS系统实现标准化文件操作数据流,下一篇还会有第五部分vfs。毕竟换一种文件系统比如SPIFFS就不是这种函数和参数了,vfs就是在各种文件系统之上建立大一统标准,不仅储存设备的差异,连文件系统的差异也不用管了。
FATFS系统不关心设备是SD卡还是U盘,不关心是SPI还是IIC协议通讯,只关心diskio.c里面的disk_read()里的*buff参数和disk_ioctl()里的*buff参数,而disk_ioctl()也是调用disk_read()的,整个FATFS系统从底层接收信息的入口就是这两个*buff。所以底层物理驱动层只要能正确喂给这两个*buff数据就可驱动FATFS系统正常运作。因为FATFS挂载或格式化过程需要读出SD卡MBR、DBR区域的数据,只要喂入正确的MBR数据给FATFS,后面的打开、写入、关闭文件等操作就顺理成章。所以我们写底层物理驱动函数read()代码时,检查是否能够准确读出block 0地址的512byte MBR数据,若能,基本没问题了。
FATFS系统运作过程与ESP32的交互:
(1)ESP32操作SD卡初始化指令流程使SD卡进入数据读写的准备阶段,称为SD卡初始化,SD卡插拔或断电后需要重新进行初始化;
(2)ESP32将内存中的FATFS文件系统烧录到SD卡的数据储存区域的MBR区(0簇)、DBR区,称为分区和格式化,电脑上也叫格式化;
(3)ESP32内存创建文件系统和文件这两个handle变量,称为f_mount和f_open。电脑的mount时会显示驱动器F,电脑打开文件时就能读出数据,稍稍有些不同;
(4)无论是写还是读,ESP32内存都要开辟缓冲区域起码512bytes大小(视文件系统格式化时选择的簇大小而定),将SD卡数据区域整个block读出来,临时存放在ESP32内存;
(5)如果是写操作,则修改ESP32临存区域中相应位置的信息,然后整个block烧录到SD卡;如果是读操作,ESP32从临存区域提取想要的部分信息到别的变量中;
(6)关闭文件,代表释放file handle内存,并在这一步将ESP32内存的1个block数据烧录到SD卡,跟电脑操作一样,关闭文件时才选择是否保存文件,保存相当于烧录到非易失性驱动器;
(7)关闭文件系统,unmount,代表释放ESP32中的file system handle内存,电脑上相当于弹出驱动器
本来下一篇文章打算讲VFS系统与FATFS系统如何对接,但操作下来内容不多,比较简单,而且VFS系统不是这个系列文章想要传达的主旨,实操项目上也不会像我一样自己硬核写驱动、移植FATFS,直接用ESP官方库就可。我这个系列文章主旨是深度剖析外部储存设备的控制原理、数据传递过程、文件系统帮助数据组织成具体文件的过程。VFS作用仅是统一各个文件系统接口,因此这儿就简单讲,不开一篇文章。
ESP官方库就有vfs(“esp_vfs.h”),但vfs库里的函数只有这个esp_vfs_register()需要使用。esp_vfs_register()之后就可以直接用C语言标准函数操控文档了。操控文档涉及open、write、close等文件操作就用C语言标准库里的函数,比如fopen()、fwrite(),不要用vfs库里的esp_vfs_write()、esp_vfs_read()。
- #include "esp_vfs.h"
- esp_vfs_t vfscnf={
- .flags=ESP_VFS_FLAG_DEFAULT,
- .write=&f_xwrite,
- .lseek=&f_xlseek,
- .read=&f_xread,
- .open=&f_xopen,
- .close=&f_xclose,
- .fstat=&f_xstat,
- };
- esp_vfs_register("/sdcard",&vfscnf,NULL);
esp_vfs_t 结构体作用是将vfs与FATFS的函数进行关联。但有个问题,FATFS函数接口与vfs不一致(肯定是不一致的,如果一致,就不需要vfs对各个文件系统大一统了)。因此这儿的.write不能直接关联FATFS的f_write()函数,而是要建立一个中间函数f_xwrite(),在f_xwrite()里面进行函数参数的转换,将转换后的参数填入FATFS的函数f_write(),调用f_write()进行写数据操作。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。