【猪圈丶嗨情歌的开发分享】
今天要给大家分享的是使用FatFs这个库来读写SD卡上面的文件。工程的初始化函数和FatFs都是通过STM32CubeMX配置生成的,不需要我们手动添加库。
今天分享的内容和我的上一篇帖子 The specified item was not found. 有关,最好掌握了SDIO读取SD卡扇区的基础之后再来看这一篇帖子。
写在前面的话
上一次发表了关于SDIO读取SD卡的一篇开发分享的帖子,今天呢就更进一步使用了FatFs文件系统。同样我们完全使用ST公司提供的STM32CubeMX软件和HAL库来进行开发。如果喜欢我的帖子请多多回复我会努力更新的。
我使用的工具
开发平台:正点原子探索者STM32F407开发板
硬件:使用了NUCLEO-F446RE开发板的ST-Link作为调试器、SD卡、数据线、开发板的电源适配器、DELL一体机
软件:STM32CubeMX、Keil V5、串口助手
学习的知识点
1、使用STM32CubeMX配置SDIO
2、在Keil中初始化SDIO
3、修改HEAP内存块的大小
4、malloc函数的使用
5、如何使用FatFs提供的一些库函数
共享的资源
完整的工程文件 下方附件FatFs.zip
STM32Cube中FatFs的中文帮助文档 帖子下方附件UM1721_DM00105259_CN.pdf
准备工作
参考 The specified item was not found. 这篇帖子,这里不再赘述。
目录
一、在STM32CubeMX中配置好我们的工程
1、配置FatFs
2、配置SDIO
3、配置SYS
4、配置USART
二、在Keil中做初始化
1、调用BSP_SD_Init()函数
2、修改MX_FATFS_Init(void)函数
三、修改启动文件
1、malloc函数的介绍
2、FatFs对malloc函数的调用
3、改变heap的内存大小
四、使用FatFs
1、定义一些变量
2、使用f_open
3、使用f_read
4、使用f_lseek
5、使用f_write
6、使用f_close
7、再次读取文件内容
五、上电测试
在STM32CubeMX引脚配置中,找到Configuration->MiddleWares->FatFs,勾选下面的SD Card。
STM32CubeMX引脚配置中Peripherals->SDIO,下拉框里面选择SD 4 bit Wide bus。
STM32CubeMX引脚配置中Peripherals->SYS,下拉框里面选择SWD and Asynchronous Trace。
STM32CubeMX引脚配置中Peripherals->USART,下拉框里面选择Asynchronous。
下面是我的工程配置界面的截图。
在main函数开头的部分调用了各种初始化函数,这些函数完成了硬件的初始化工作。在我的前一篇帖子 The specified item was not found. 中,SDIO的初始化也是要添加一点代码才可以正常的工作。这里也是一样,不过更加的简单。我们只需在MX_FATFS_Init();之前调用BSP_SD_Init();函数就可以了,我在下面贴出了这段初始化的代码。
这里介绍一下BSP_SD_Init()函数。BSP的意思是板级支持包的意思,也就是这个函数是专门针对某一类芯片支持的。这个函数在文件bsp_driver_sd.c文件中,在这个文件里面还有很多的函数,这些函数完成了对读写SD卡、查询SD卡状态、初始化SD卡等一系列操作。这些函数原先都是要用户来编写,提供给FatFs调用作为驱动支持的。但是这里帮我们都定义好了只需要调用即可,如果你要用FatFs来连接自己的设备的话,那你就需要自己定义这些底层的IO驱动函数了。
关于FatFs底层驱动支持和驱动架构的关系请查看附件UM1721_DM00105259_CN.pdf,这里面有ST官方对这个的详细说明。
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* Configure the system clock */ SystemClock_Config(); /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_SDIO_SD_Init(); MX_USART1_UART_Init(); BSP_SD_Init(); MX_FATFS_Init();
除了上面对初始化函数的调用以外,我们还需要一些别的修改。在这里,我希望调用BSP_SD_Init()初始化好了SD卡之后,在调用MX_FATFS_Init()初始化的时候就挂载SD卡。所以我们就在FatFs的初始化函数里面用户代码的部分加入了我们的f_mount函数。下面贴出MX_FATFS_Init()函数的代码,这个函数位于文件Application/User->fatfs.c中。
我还设置了一个简单的条件语句,如果挂载SD卡成功的话就发送一个成功的消息。
void MX_FATFS_Init(void) { /*## FatFS: Link the SD driver ###########################*/ retSD = FATFS_LinkDriver(&SD_Driver, SD_Path); /* USER CODE BEGIN Init */ /* additional user code for init */ if(f_mount(&SDCard, SD_Path,0) == 0) { HAL_UART_Transmit(&huart1, (uint8_t *)"Success!\n", 9, 500); } /* USER CODE END Init */ }
OK,到了这里我们对文件的修改就基本上完成了,但是仅仅如此只能让SD卡正常驱动和挂载,并不能够打开文件和读写操作。接下来的内容就是来解决这个问题的。
原型
extern void *malloc(unsigned int num_bytes);
头文件
#include <stdlib.h>
函数声明
void *malloc(size_t size);
备注:void* 表示未确定类型的指针,void *可以指向任何类型的数据,更明确的说是指申请内存空间时还不知道用户是用这段空间来存储什么类型的数据(比如是char还是int或者其他数据类型)。
功能
分配长度为num_bytes字节的内存块
返回值
如果分配成功则返回指向被分配内存的指针(此存储区中的初始值不确定),否则返回空指针NULL。当内存不再使用时,应使用free()函数将内存块释放。函数返回的指针一定要适当对齐,使其可以用于任何数据对象。
以上是百度百科对malloc函数的一些简介,在Keil中要使用这个函数需要包含stdlib.h头文件。同时在Keil的编译器设置中要选中microlib选项,不过使用STM32CubeMX生成的工程默认都选中了这一选项。这个函数是放在标准库里面的,所以不能找到这个的定义代码在哪里,编译的时候链接所属的库就可以了。
为了讨论FatFs和malloc函数的关系,我们要先打开MiddleWares/FatFs->syscall.c文件。在这个文件里面,我们可以看到有一些函数是分配内存的函数。滚动页面到最下面,可以看到最后两个函数都和分配内存有关。这些函数都调用了malloc函数来动态分配内存,在ST的手册中没有提到这个malloc。但是如果没有这个系统提供的malloc函数我们就需要自己来定义一个动态内存分配函数,同时把原来的malloc调用替换为我们提供的函数接口。
这样我们就知道了FatFs对malloc函数有调用。但是仅仅如此吗?不是这么简单。FatFs默认是开启了对长文件名支持的,这样就需要很多的内存来存储文件名这些信息。如果malloc内存初始化的时候内存设置的比较小的话,就不能成功分配内存了不是吗?这样一来调用FatFs的f_open函数的时候就会返回17,查询这个枚举值的定义就知道是内存分配不足的错误。感兴趣的话可以不进行下一步修改heap的大小来调用f_open同时把返回值发送到串口来看一看。
malloc函数的内存是从堆(heap)里面分配的,如果使用了对长文件名的支持我们就需要malloc为FatFs提供内存分配的支持。在使用malloc之前会对malloc的内存池进行初始化,而这个初始化的操作是由我们的库提供的启动代码完成的。
下面贴出汇编语言的启动代码的一部分。
Stack_Size EQU 0x00000400 AREA STACK, NOINIT, READWRITE, ALIGN=3 Stack_Mem SPACE Stack_Size __initial_sp ; <h> Heap Configuration ; <o> Heap Size (in Bytes) <0x0-0xFFFFFFFF:8> ; </h> Heap_Size EQU 0x00000800 AREA HEAP, NOINIT, READWRITE, ALIGN=3 __heap_base Heap_Mem SPACE Heap_Size __heap_limit
这一段代码是初始化内存池的汇编语言代码,在这里我把Heap_Size设置为0x00000800,也就是2KB。默认的数值是512Bytes,这样的话分配给我们的FatFs使用会不足,分配内存会失败。只有这样子修改了之后,后面的f_open这些函数才可以正常使用。
进行了以上的步骤之后,我们就可以轻松愉快地使用FatFs提供的各种函数了。
在我们代码开始的部分,先定义一些变量供我们使用。这里选择几个来解析一下。
第一个FIL file;这个变量是文件的结构体变量,记录了我们打开的文件的信息。使用f_open等函数的时候都要用到。
第二个Words变量是一个字符串指针,我用这个指针来存储读取的字符信息。这里我们就使用了malloc函数来分配内存,我们通过修改启动代码提供了更多的堆内存所以这里就可以分配一些给我们使用。
第三个是Path这个字符串,这里保存的是文件所在的路径。关于文件路径的写法,请参考FatFs的官方文档 网站。
FIL file; uint8_t Status = 0; char* Words = malloc(256); char* String = "\nWrite some words to this file by STM32F407 MCU.\n"; UINT Number = 0; char Path[16] = "/File/test.txt";
关于f_open函数的具体用法和详细说明,参见FatFs官方文档 网站。这里可以看看我在下方贴出的代码作为参考来使用。
关于f_read没有特别的说明,详细的用法去看看FatFs官方文档 网站。这里只想说一下第三个和第四个参数。这里的第三个参数是给定的要读取的字节数,而第四个参数是最后读取了的字节数的存储指针。这里要说明的是,不一定你要读取128个字节就一定会读取这么多,如果遇到了文件尾符号就会停止读取。停止读取的时候就会把读取的字节数写入到第四个参数指定的存储空间里。所以我们可以通过返回的读取字节数来发送到串口,不需要自己数读取了多少个字节。
if(!f_open(&file, Path, FA_READ | FA_OPEN_EXISTING | FA_WRITE)) { f_read(&file, Words, 256, &Number); HAL_UART_Transmit(&huart1, (uint8_t *)Words, Number, 500); }
我在调用f_write函数之前,调用了一下f_lseek函数来移动文件指针。不过在这里并不需要,你们参考我的完整工程文件就知道。在这之前我已经进行了文件读取的操作,文件指针已经定位在了文件的最后一个字节这里。不过我还是调用了一下,因为有时候需要用到这个函数。这个函数的详细用法请参考FatFs的官方文档 网站。
f_write的用法和f_read非常相似,这里就不在赘述。第三个参数和第四个参数的含义也是大同小异。想了解这个函数的详细说明,请参考FatFs的官方文档 网站。
f_lseek(&file, Number); Status = f_write(&file, String, 49, &Number);
f_close这个函数的用法就非常的简单了,这里在写入操作之后要调用一次这个函数。调用了这个函数之后,写入的信息才可以保存在SD卡的上面,下一次才可以读取出来。如果没有使用这个函数的话,文件就会存储在缓冲区中,只有这一次读取可以成功。到了下一次复位的时候,读取的还是原先的文件内容。
f_close(&file);
现在我们有写入了一些信息到了我们的文件里面,再次读取一下看看写入成功了吗。下面的代码实现了这一功能,大家参考一下。
f_open(&file, Path, FA_READ | FA_OPEN_EXISTING); if(!Status) { f_read(&file, Words, 256, &Number); HAL_UART_Transmit(&huart1, (uint8_t *)Words, Number, 500); } f_close(&file);
代码我们都准备好了,只要编译通过下载到我们的开发板上就行了。如果你的硬件连接都非常的正确,而且按照前面的步骤来的话就可以开始测试读取SD卡了。
我在SD卡的根目录下建立了/File/test.txt
初始的文件内容如下:
This is a test file to confirm the library of FatFs.
Don't support Chinese in this file.
Wish you a good luck!
测试结果
观察两次的输出结果可以看得出来,第二次复位的时候文件的末尾又多了一行文字。这一现象符合我们的程序逻辑。大家可以对比一下这里的结果,自己做一个参考。
写在后面的话
注意!注意!注意!本人不是什么工程师,只不过是爱好嵌入式开发的学生一枚,如果你发现在这个帖子中的错误请及时提醒我。如果对本帖的内容有什么疑问请在下方留言,我会经常过来逛论坛的。
这次开发FatFs读写确实走了很多的弯路啊,结果最后发现不过是堆内存空间不够,不能分配的错误而已。为了发现这个错误我也是找了很多的资料什么的,不过这就是学习的过程吧。我觉得只有这样才能够锻炼我们发现问题和解决问题的能力。相信在这一次又一次的坎坷和曲折之中我一定会进步越来越大的。大家一起加油!
下一次给大家带来的是什么呢?我现在贴出一张照片,大家一起猜一猜。
不过我获得了上次活动的开发板,可能下一次更新就是这块新的开发板的相关内容了。下一次要更新的东西会比较复杂的吧,所以时间可能会隔得比较久,希望期待下一篇帖子的社区成员多多关注。