概述
摘自官方文档:
相同的 CPU 架构在实际项目中,不同的板卡上可能使用相同的 CPU 架构,搭载不同的外设资源,完成不同的产品,所以我们也需要针对板卡做适配工作。RT-Thread 提供了 BSP 抽象层来适配常见的板卡。如果希望在一个板卡上使用 RT-Thread 内核,除了需要有相应的芯片架构的移植,还需要有针对板卡的移植,也就是实现一个基本的 BSP。
在之前整体架构篇提到过,在RT-Thread的架构中,BSP的概念更多的是通过下层components即组件与服务层所提供的API来移植板载的一些硬件外设,如最基本的GPIO和UART。
这篇就以GPIO和UART为例,简单记录一下一个开发板需要如何做BSP的适配。
IO设备模型
官方文档:
I/O设备模型
。
理论部分
RT-Thread 提供了一套简单的 I/O 设备模型框架,如下图所示,它位于硬件和应用程序之间,共分成三层,从上到下分别是 I/O 设备管理层、设备驱动框架层、设备驱动层。
I/O 设备管理层
用户在应用程序中,只需要通过IO设备管理层所提供的接口来对某一个IO设备进行操作,用户并不需要关注这些设备具体是怎么实现的,只需要专注于功能即可。
例如现在需要使用UART设备进行输出。用户只需要调用
rt_device_open()
来打开设备,然后使用
rt_device_write()
即可使用打开的设备进行输出。至于底层的UART初始化、波特率,停止位,校验位等参数的设备、中断配置等等操作,都通过设备驱动框架层所提供的接口,由设备驱动层实现。在用户调用诸如
rt_device_open()
和
rt_device_write()
等接口时,这些接口会自动调用硬件接口来驱动外设。
当然不同的开发板所具有的硬件外设接口都会不同,所以这正是设备驱动框架的意义所在。
简单来说,I/O设备管理层向上直接对接应用层,这一层专注于提供统一的接口给用户调用。理论上使用了RT-Thread的IO设备模型之后,那么在此基础上使用的接口开发的应用程序,在任何基于RT-Thread设备框架的开发板上都可以完全套用,无需修改。
设备驱动框架层
设备驱动框架层是对同类硬件设备驱动的抽象,将不同厂家的同类硬件设备驱动中相同的部分抽取出来,将不同部分留出接口,由驱动程序实现。
还是以UART为例,基本任何的开发板都会板载至少一个硬件UART。要使用一个UART外设,必不可少的操作一般有:
- 配置UART参数,波特率、校验位、停止位、流控等。
- UART的IO口配置。
- UART时钟与使能。
- UART字符接收与发送。
- UART中断配置。
- ……
那么这些操作根据不同的开发板厂商的不同,都会在SDK提供的硬件外设接口上体现出来。这些也就是上面说的
不同部分留出接口,由驱动程序实现
。
简单来说,设备驱动框架层向上对接I/O 设备管理层,I/O 设备管理层所需要的功能,需要在这一层都做出对应的接口实现,例如对某个设备的
open
、
write
、
read
、
control
等操作。设备驱动框架层向下对接设备驱动层,设备驱动框架层最终也只是一个框架,仅仅提供了一个模板和一些准备与善后的偏向逻辑上的工作(例如对内存的创建与调用和对数组长度作出限制等),实际上在每个为I/O 设备管理层接口的实现函数里,都是调用设备驱动层中的函数,来对具体的硬件外设做出操作。
设备驱动层
设备驱动层是一组驱使硬件设备工作的程序,实现访问硬件设备的功能。
这一层向下直接对接硬件, 这一层也是移植者需要做的最主要的工作。通过上层设备驱动框架层所提出的
不同部分留出接口,由驱动程序实现
这个特点,这些不同的部分就包括上文说的那些必不可少的操作,这些具体的对设备的配置,都需要移植者根据开发板厂商所提供的硬件驱动库来编写与实现。
简单来说,设备驱动层需要移植者根据开发板的硬件外设驱动,来实现设备驱动框架层提供的接口。
补充
官方在IO设备模型的文档下添加了补充说明,这个图对三层的概念描述的就很形象了。
对UART来说,任何平台的UART设备都可以通过设备驱动层来向上对接到设备驱动框架层中的
串口设备类
,对接的方式就是实现设备驱动框架层中的串口设备类框架所提供的接口。
代码部分
其实理论部分就是了解一下架构上的东西,对整体的流程有一个认知和把握,真正要对BSP进行移植的话,其实只需要对设备驱动层进行编写即可。
摘自官方文档:
新增 BSP 设备驱动到 I/O 设备模型框架上时,开发者只需开发驱动层即可,设备驱动框架层和 I/O 设备管理层 RT-Thread 已写好了,无需改动,除非发现BUG或增加新的类别。
I/O 设备管理层
在
src/device.c
文件中,提供了对设备的操作接口。这里就是RT-Thread的I/O 设备管理层提供给应用程序的I/O 设备管理接口,包括对设备的创建、删除、初始化、开启、关闭、读写等操作。这些操作除了一些逻辑方面的代码以外,都调用了文件头部的宏定义。
如下图:
那么如果说这些形如
rt_device_find()
、
rt_device_init()
、
rt_device_open()
等函数是RT-Thread提供给应用层的统一设备驱动接口,上图中的六个宏定义就是I/O 设备管理层提供给设备驱动框架层的具体实现接口。这也正如上文理论部分所说的。
这些接口调用,都需要使用到
dev
这个参数,这个参数是指向设备驱动结构体的指针。来看看这个结构体,可以看到,这六个公共设备接口,正是设备驱动框架层(也可能是设备驱动层)所要实现的。
RT-Thread将六个I/O 设备管理接口与这六个操作方法一一映射,实现通过统一的接口来访问底层硬件。
这一层完全不需要移植者修改和操作,只需要其使用接口即可。
设备驱动框架层
这一层其实就是
src/components/drivers
中的内容。在这个文件夹中,包括了RT-Thread所提供的目前支持的所有设备驱动框架。
以UART为例,在
src/components/drivers/serial/serial.c
中(这里就是串口设备驱动框架),通过
rt_hw_serial_register()
函数对上文中的六个接口进行了实际的定义,然后使用
rt_device_register ()
函数将这个设备注册进了设备驱动器中。
那么其实当我们在应用层调用UART设备的驱动接口时(例如
rt_deive_init()
),其实就是调用了这个文件中的接口(对应
rt_serial_init()
)。
当然这些设备驱动框架也是RT-Thread已经提供写好的,我们也并不需要进行修改。
那么这六个函数又是怎么实现的呢?以
rt_serial_init()
为例,首先将传入的设备驱动结构体指针强转成了串口设备驱动结构体指针,然后直接调用了串口设备驱动结构体中的接口。
我们在
src/components/drivers/serial/serial.h
中,可以看到这个串口设备驱动结构体的内容。
那么这个
rt_uart_ops
结构体中的五个接口,就是上文代码部分中设备驱动框架层所说的,
不同部分留出接口,由驱动程序实现
。
这就是移植者需要在设备驱动层所真正要自行实现的接口部分。
设备驱动层
以STM32的USART为例,在
drv_usart.c
中,根据上图中的接口定义,来编写对应的实现代码。部分实现如下。
可以看到其实就是使用RT-Thread的串口驱动设备结构体中所提供的接口,来编写对应的功能函数,函数内部的代码使用平台的硬件外设驱动库中对应功能的代码来实现。
然后定义一个
rt_uart_ops
结构体,即uart操作方法结构体,用于将这些驱动实现接口保存。
最后将这个保存下来的操作方法结构体赋值到一个串口设备驱动结构体中即可。
那么至此为止,整个流程基本就结束了。我们可以从应用层的一个设备驱动接口调用开始对整个流程分析一遍。
-
通过
rt_hw_usart_init()
函数将uart设备注册到设备驱动器中。 -
在应用层通过
rt_device_find()
函数找到上一步注册进设备驱动器中的uart设备。 -
在应用层通过
rt_device_open()
函数打开uart设备。参数就是上一步中返回的设备句柄(指针)。 -
在I/O 设备管理层
rt_device_open()
调用设备驱动框架层的
rt_serial_open()
函数。 -
在设备驱动框架层
rt_serial_open()
函数调用设备驱动层的
stm32_control()
函数。 -
在设备驱动层
stm32_control()
函数中调用底层库中的API对uart设备进行相应的操作。
补充
serial
设备的三层接口映射如下图:
小结
整个流程下来,IO设备管理层和设备驱动框架层是不需要移植者进行任何修改的。
对移植者来说,想要新增一个BSP外设的移植时,一般来说需要做的是:
-
在
src/components/drivers/
中找到相应的设备驱动框架,然后引入工程中。 -
查看
src/components/drivers/
中对应设备驱动框架的头文件,例如
serial.h
,找到该设备驱动结构体。观察除父类之外的其他参数。如下图中的红框部分。
-
找到RT-Thread针对该外设所抽象出的具体接口,即驱动操作方法
OPS
。举例如下:
serial.h
:
pin.h
:
-
在工程中新建
drv_xxx.c
和
drv_xxx.h
文件。例如
drv_uart.c
和
drv_uart.h
。 -
结合上方的
2
和
3
,在
drv_xxx.c
文件中,实现对应的设备驱动结构体中需要的参数部分。其中
rt_xxx_ops
结构体中的接口必须予以实现,可以结合应用需要不实现部分接口。特定设备例如uart的参数配置(上图中的serial_configure结构体,用于配置波特率、停止位、校验位等)也必须实现。实现代码需要根据接口功能和硬件驱动库编写。 -
将实现的设备驱动操作方法通过
rt_xxx_ops
结构体保存下来,根据不同设备驱动注册函数的参数类型,调用
rt_hw_xxx_register()
函数注册进设备管理器中。不同的设备类型有着不同的形参要求。举例如下:
serial.h
:
pin.h
:
-
在设备驱动框架中查看哪些接口可以被应用层调用。例如
serial.c
中6个接口都实现了接口。理论上应用层都可以进行调用。但由于内部逻辑的实现,实际上应用层只需要调用
rt_device_open()
即可完成uart的初始化和打开工作。
而
pin.c
中仅仅实现了三个操作。所以应用层是不能够调用诸如
rt_device_init()
、
rt_device_open()
、
rt_device_close()
操作的。
但是在
pin
设备中,额外对应用层提供了七个接口。这些接口也使用上文中的
rt_pin_ops
结构体实现。但是其不需要经过IO设备管理层即可直接被用户访问。这也是RT-Thread对不同设备驱动的不同实现。
这是大概的流程,其中还会涉及到一些数据结构的问题。设备驱动层整体的三个核心就是:回调、指针和结构体。所以需要对这些有一定的了解。