原文链接
RT-Thread(后文简称RT)提供的DFS组件、Fatfs组件和SDIO驱动组合起来可用于操作SD卡,但RT的底层驱动目前对STM32H743(后文简称H743)适配不是很好,在stm32h743上移植RT时,包括SDIO在内的多个设备驱动都无法直接编译通过。且当前官方论坛中关于在H743上应用RT的相关的帖子也比较少,因此在本次使用SD卡挂载文件系统时,因为底层驱动不适配,遇到了很多问题,也尝试了很多办法,最后通过重写块设备部分的代码实现了文件系统的挂载。
开发环境
主控芯片:stm32h743iit6
硬件平台:自行制作的单板
RT-Thread版本:4.0.2
开发工具:RT-Thread Studio (Version: 2.0.0)(后文简称RT-Studio)
STM32CubeMX(Version: 6.0.1)
STM32CubeIDE(Version: 1.5.1)
存储设备:SD卡
RT虚拟文件系统简介
详细内容参看RT官方文档。
RT虚拟文件系统层次架构如下图所示。
![](https://raw.githubusercontent.com/hekuzhengfeng/pictures/master/img20200001.png)
DFS是RT提供的虚拟文件系统组件,该组件为应用程序提供方便的文件操作接口,并支持多种文件系统和存储设备。本文采用的文件系统是FatFS,主要是因为FatFS可以兼容微软的FAT格式,基于该文件系统将文件写入到SD卡后,SD内容可以直接被windows系统识别。
由于底层驱动不适配的问题,本文基于ST的HAL库重写了块设备相关代码,最终实现了文件系统的功能。
基于RT-Studio的配置
在RT-Studio中新建工程及其他功能的实现此处不再详述,首先介配置DFS组件和FatFS组件。如下图所示:
![](https://raw.githubusercontent.com/hekuzhengfeng/pictures/master/img20210225155935.png)
点亮DFS组件和Fatfs组件。可能需要libc组件的支持,这点没有做过验证。其他组件与文件系统本身无关,此处不详细介绍。
![](https://raw.githubusercontent.com/hekuzhengfeng/pictures/master/img20210225160534.png)
按上图所示进行配置,DFS可以加载多种文件系统,但此处只使用了FatFs文件系统。
重写块设备相关代码
重写块设备代码的原因
之所以要重新块设备相关代码,主要原因有二:一个是本文已经反复提到的底层驱动不适配的问题,另一个是RT原有的SD Card设备类mcsd_blk_device
较为复杂,包含了多个子类,需要研究多个类型及相关函数才能完成实例化,比较困难。
mcsd_blk_device
的定义如下:
struct mmcsd_blk_device
{
struct rt_mmcsd_card *card;
rt_list_t list;
struct rt_device dev;
struct dfs_partition part;
struct rt_device_blk_geometry geometry;
rt_size_t max_req_size;
};
生成SDMMC初始化代码
使用STM32CubeMX创建项目,配置SDMMC1为SD 4 bit Wide bus模式。配置如下图:
![](https://raw.githubusercontent.com/hekuzhengfeng/pictures/master/img20210225171354.png)
时钟配置此处不做展示,项目中时钟源选择PLL1Q,频率为400MHz。配置完成后生成代码备用。
在RT中初始化并注册块设备
struct rt_device
{
struct rt_object parent; /**< inherit from rt_object */
enum rt_device_class_type type; /**< device type */
rt_uint16_t flag; /**< device flag */
rt_uint16_t open_flag; /**< device open flag */
rt_uint8_t ref_count; /**< reference count */
rt_uint8_t device_id; /**< 0 - 255 */
/* device call back */
rt_err_t (*rx_indicate)(rt_device_t dev, rt_size_t size);
rt_err_t (*tx_complete)(rt_device_t dev, void *buffer);
#ifdef RT_USING_DEVICE_OPS
const struct rt_device_ops *ops;
#else
/* common device interface */
rt_err_t (*init) (rt_device_t dev);
rt_err_t (*open) (rt_device_t dev, rt_uint16_t oflag);
rt_err_t (*close) (rt_device_t dev);
rt_size_t (*read) (rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size);
rt_size_t (*write) (rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size);
rt_err_t (*control)(rt_device_t dev, int cmd, void *args);
#endif
#if defined(RT_USING_POSIX)
const struct dfs_file_ops *fops;
struct rt_wqueue wait_queue;
#endif
void *user_data; /**< device private data */
};
块设备的类型定义如上,其中RT_USING_DEVICE_OPS
和RT_USING_POSIX
未被定义。
下文是初始化和注册块设备的代码。首先创建块设备device_sd0
,然后初始device_sd0
的几个成员函数,初始化MCU的SDMMC1外设并将块设备注册到系统中。
int sdInit(void)
{
rt_device_t device_sd0;
SD_HandleTypeDef *hsd1;
//创建一个系统块设备对象
device_sd0 = rt_device_create(RT_Device_Class_Block, sizeof(SD_HandleTypeDef));
//手动设置块设备对象的函数指针
device_sd0->init = sdmmcInit;//初始化函数指针
device_sd0->open = sdmmcOpen;
device_sd0->close = sdmmcClose;
device_sd0->read = sdmmcRead;
device_sd0->write = sdmmcWrite;
device_sd0->control = sdmmcControl;
//将SDMMC1的控制块存定义到块设备的user data中
device_sd0->user_data = &(device_sd0->user_data) + 1;
hsd1 = (SD_HandleTypeDef *)device_sd0->user_data;
rt_memset(hsd1, 0, sizeof(SD_HandleTypeDef));
//将device_sd0注册到系统中,设备名为sd0
rt_device_register(device_sd0, "sd0", RT_DEVICE_FLAG_RDWR);
//初始化SDMMC1
hsd1->Instance = SDMMC1;
hsd1->Init.ClockEdge = SDMMC_CLOCK_EDGE_RISING;
hsd1->Init.ClockPowerSave = SDMMC_CLOCK_POWER_SAVE_DISABLE;
hsd1->Init.BusWide = SDMMC_BUS_WIDE_4B;
hsd1->Init.HardwareFlowControl = SDMMC_HARDWARE_FLOW_CONTROL_ENABLE;
hsd1->Init.ClockDiv = 8;
if (HAL_SD_Init(hsd1) != HAL_OK)
{
return RT_ERROR;
}
return RT_EOK;
}
//注册初始化函数,参考官方文档自动初始化相关内容
INIT_DEVICE_EXPORT(sdInit);
使用HAL库初始化外设需要一个控制块(即SD_HandleTypeDef
类型的一个实例),在创建块设备对象时,已预留了空间用于存储控制块。其原理如下:
- 函数
rt_device_create
中有为块设备申请内存的操作,计算所需内存大小时在块设备类型的基础上增加了作为参数传入的SDMMC1控制块所需的内存大小; - 将块设备最后一个成员
void * user_data
指向预留的空间,及user_data
的下一个地址; - 将指针
hsd1
也指向预留的空间(方便操作); - 此时已完成将SDMMC1控制块定义到块设备user data中的操作。
初始化外设SDMMC1的代码可直接从上文STM32CubeMX生成的代码中拷贝。此处有几个细节需要注意:
- 原来代码中是直接定义了一个SDMMC控制块对象,而在本项目中使用的是指向SDMMC控制块的指针,在调用控制块成员和函数传参时语法不同;
- 原来代码中初始化函数
HAL_SD_init
返回失败时进入一个错误处理函数(默认为死循环),而本项目中改为返回RT_ERROR
; - 生成的代码中还有一个函数
HAL_SD_MspInit
也需要拷贝过来,改函数不需要任何更改,此处不再贴出代码。
块设备的几个成员函数定义如下,这些函数是文件系统与SD卡之间的桥梁。定义这几个函数的过程比较曲折,画了很多功夫才搞明白每一个函数应该实现什么样的功能,以及函数入参、返回值的意义。但函数的内容并不复杂,此处直接列出源码,不再详细解释。
rt_err_t sdmmcInit(rt_device_t dev)
{
return RT_EOK;
}
rt_err_t sdmmcOpen(rt_device_t dev, rt_uint16_t oflag)
{
return RT_EOK;
}
rt_err_t sdmmcClose(rt_device_t dev)
{
return RT_EOK;
}
rt_size_t sdmmcRead(rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size)
{
if (HAL_SD_ReadBlocks((SD_HandleTypeDef *)dev->user_data, (uint8_t *)buffer, pos, size, 5000) != HAL_OK)
{
return 0;
}
return size;
}
rt_size_t sdmmcWrite(rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size)
{
if (HAL_SD_WriteBlocks((SD_HandleTypeDef *)dev->user_data, (uint8_t *)buffer, pos, size, 5000) != HAL_OK)
{
return 0;
}
return size;
}
rt_err_t sdmmcControl(rt_device_t dev, int cmd, void *args)
{
struct rt_device_blk_geometry info;
switch (cmd)
{
case RT_DEVICE_CTRL_BLK_GETGEOME:
info.sector_count = ((SD_HandleTypeDef *)dev->user_data)->SdCard.LogBlockNbr;
info.bytes_per_sector = ((SD_HandleTypeDef *)dev->user_data)->SdCard.LogBlockSize;
info.block_size = ((SD_HandleTypeDef *)dev->user_data)->SdCard.LogBlockSize;
rt_memcpy(args, &info, sizeof(struct rt_device_blk_geometry));
break;
default:
break;
}
return RT_EOK;
}
将块设备挂载到文件系统
经过一番努力,现在已经在系统中注册了块设备"sd0",DFS组件和FatFS组件会自动初始化,接下来需要将块设备挂载到文件系统中。函数如下:
int sd_mount(void)
{
if(rt_device_find("sd0") != RT_NULL)
{
if (dfs_mount("sd0", "/", "elm", 0, 0) == RT_EOK)
{
LOG_I("sd card mount to '/'");
return RT_EOK;
}
else
{
LOG_W("sd card mount to '/' failed!");
}
}
return RT_ERROR;
}
INIT_APP_EXPORT(sd_mount);
更好的挂载文件系统的操作可以参考帖子RT-Thread进阶笔记之虚拟文件系统中6.6.4小节的方法创建一个挂载文件系统的线程。本项目中实际上是偷了个懒,挂载文件系统的操作只会尝试一次,因此必须等到DFS、FatFS、块设备都初始化完成后调用该函数才能成功挂载文件系统,所以把该函数注册到自动初始化最晚的“application init functions”中,具体参考官方文档相关章节。
其他注意事项
需要注意的是,在初始化系统时钟时,一定要配置外设SDMMC1的时钟,具体方法这里也不再详细说明。
小结
本项目不仅完成了RT-Thread文件系统组件在stm32h743上的应用,实际上也为RT-Thread其他设备驱动不适配stm32h743的问题提供了一个解决问题的思路。
由于本人能力有限,代码中可能存在诸多问题或bug,欢迎大家批评指正。