写在前面
在入门学习了RT-Thread之后,我撰写了本文剖析了在RTT中设备应用层与底层时如何映射和关联起来的,以及解释了串口DMA回调的原理机制(可触类旁通),有助于深入了解了RTT的逻辑架构,最后是我的相关学习心得。
有关RT-Thread入门学习可以参考我的这篇文章:
1 串口设备树映射
1.1 I/O设备树模型
首先HAL库中封装了直接对硬件寄存器进行相关操作的方法,然后设备驱动层drv_usart.c中会调用HAL库中的方法实现对寄存器的操作
1.2 映射机制
1.2.1 RT-Thread启动rtthread_startup
RT-Thread启动时在调用rtthread_startup(void)
进行了设备初始化
1.2.2 板级设备初始化rt_hw_board_init
1.2.3 设备初始化hw_board_init
1.2.4 串口设备初始化rt_hw_usart_init
实现**设备驱动层(drv_usart.c)到设备驱动框架层(serial.c)**的映射
即,stm32串口设备类(stm32_uart)→rtt串口设备类(rt_serial_device)
第1363行中进入uart_config
中
在board.h
中有宏定义串口设备的话,以上对应配置文件就会被启用
进入串口1配置UART1_CONFIG
里面包含串口1相关配置,将相关配置注册后,这也是为什么当在main函数中调用dev1 = rt_device_find("uart1");
能找到设备的原因
1.2.5 注册串口设备类rt_hw_serial_register
实现**设备驱动框架层(serial.c)到I/O设备管理层(device.c)**的映射
即,rtt串口设备类(rt_serial_device)→rtt设备基类(rt_device)
1.2.6 注册串口设备基类rt_device_register
**I/O设备管理层(device.c)**中
实现**rtt设备基类(rt_device)到rtt基类(rt_object)**的映射
1.2.7 rtt基类初始化rt_object_init
所有内核对象的信息(对象类型、大小)都存放在对象容器中,每类对象都被连接在一个链表上:
1.3 应用实例分析
分析完映射机制之后,让我们在应用实例中进行分析
1.3.1 查找设备
先找到基类(rt_object)
这里将rt_object返回有强制类型转换成rt_device_t的原因是,在device.c中注册rt_object基类时,是将rt_device_c结构体中的第一个元素也就是rt_object添加到内核对象容器中,因此对应的这个设备(比如串口1),所定义的内存应该是连续的即rt_device_t = rt_object + flag…,因此强转成rt_device_c只是恢复了对rt_object之后的元素的寻址,也就是通过原来的rt_object的地址还原了完整的rt_device_t 类(首地址一样),最终获取到串口设备基类的所有配置信息。
1.3.2 打开设备
上面有提到,这里的初始化device_init
函数其实是映射到了serial.c中的rt_serial_init
回到rt_device_open
中
同样的这里的device_open
也是映射到serial.c中的rt_serial_open
打开rt_serial_open
这里面就根据传入的参数进行相应配置
2 串口DMA接收回调原理机制
2.1 前言
当你在通过rtt的接口设置回调函数的时候,其实看到的是非常浅显的:
这里只是将dev->rx_indicate
这个函数指针指向了用户自定义的回调函数就没了,因为想要摸清回调的机制,就要找到这里的回调是从哪里映射过来的。
2.2 分析
**< 3 应用实例分析 3.2 打开设备>**说到打开设备,本章我们刚好从这里接着分析。
由于传入的参数是RT_DEVICE_FLAG_DMA_RX
(DMA接收模式),在serial.c中我们就只看这一部分:
这里对应DMA的两种接收模式:
至于这里选择的是哪种模式,我们可以在drv.usart.c文件中初始化的时候找到,就看serial->config.bufsz
(缓冲区)是否初始化为0就可以判断了,在rt_hw_usart_init
中:
在.\rt-thread\components\drivers\include\drivers下的serial.h中:
查到缓冲区大小并不为0,而是64,因此可以判断这里选择FIFO模式。
前面是缓冲区开辟和初始化,那箭头所指serial->ops->control
是哪里映射过来的?看到是串口设备serial相关的,让我们回到drv_usart.c最初的配置中找ops
:
可以发现ops
初始配置成了stm32_uart_ops
,打开stm32_uart_ops
:
找到了serial->ops->control
映射的函数stm32_control
,打开,重点关注参数cmd
,在serial.c的rt_serial_open
中·serial->ops->control
的参数传入的是:
在stm32_control
中:
找到switch
中对应参数的case
调用的函数stm32_dma_config
,打开,找到HAL_UART_Receive_DMA
:
打开HAL_UART_Receive_DMA
,在.\libraries\STM32F4xx_HAL_Driver\Src路径下的stm32f4xx_hal_uart.c中:
打开HAL_DMA_Start_IT
,在.\libraries\STM32F4xx_HAL_Driver\Src路径下的stm32f4xx_hal_dma.c中:
可以发现,这里将DMA接收中断使能位置1了,打开DMA_IT_TC
,在.\libraries\STM32F4xx_HAL_Driver\Src路径下的stm32f4xx_hal_dma.h中:
产生中断,调用HAL_UART_TxCpltCallback
,它在UART_DMAReceiveCplt
中:
在.\libraries\STM32F4xx_HAL_Driver\Src路径下的stm32f4xx_hal_uart.c中:
打开,在相同文件中:
可以发现他是一个弱函数,即如果用户如果定义了一个跟它同名的函数,就能将它重写,那我们只要找到哪里重写了该中断回调函数,就能找到回调的过程了。
最终在drv_usart.c中找到:
可以看到这里调用了DMA中断,进入dma_isr
:
可以发现调用了串口设备中断rt_hw_serial_isr
,打开:
功夫不负有心人,终于找到了DMA回调的源头:
至此,串口DMA回调机制全部摸清!至于其他的中断回调机制与这个大同小异。
3 心得
构建了I/O设备树,实现从底层bsp到应用层的逐级映射,类的“继承”实现面向对象的思想,对应接口的调用后封装实现了面向接口的思想,因此,当用户在应用层操作硬件设备时,只用创建对象,调用接口就能实现相应操作,屏蔽了底层复杂的操作和差异性,即用户只用定义相关的对象、方法,通过对应接口就能实现底层到它的映射,同时,由于封装,当底层驱动升级或者更替时不会影响上层应用,降低了代码的耦合性、复杂性,极大地提高了实际开发的便利性和系统的可靠性。
例如**<2 串口DMA接收回调原理机制>**中讲的到的中断回调,虽然DMA接收完毕后会触发中断回调函数,那把上层的操作逐级封装在这个callback中,再通过函数指针映射到应用层,因此在表面上看用户单纯只是实现了这个函数指针指向的函数,起到了屏蔽底层的作用。
此外,也由于封装,当想自定义设备时道理也是一样的,只需要创建设备对象,定义好相关的设备配置,实现设备操作方法的映射(init、open、control…),再将设备注册(将rt_object对象添加到内核对象容器),那当用户在调用rt_device_find时就能通过名字找到设备,找到基类rt_object,返回还原设备基类rt_device_t,就能够调用自定义的设备操作方法。详见**<设备和驱动 1 I/O设备模型 1.2 创建和注册I/O设备 1.2.3 注册I/O设备>**中有实例。
最后再补充一下为什么在上文说到类的“继承”时要加双引号,因为C中没有C++类的继承(class Student:public Person)的概念,所以这里说的C的继承的实现方式时,先创建一个大类,大类中包含一个中类,中类中包含一个小类,在配置好大类后,将中类注册(相关初始化),中类注册后将中类中的小类注册(将小类节点添加到链表中),那么就能通过链表索引找到小类节点,又由于大中小类的首地址相同,因此就能根据小类倒着找到中类、大类了。
在进阶学习了RT-Thread,了解了它的架构之后,彷佛打开了新世界的大门,十分震撼,正如RTT官方文档中描绘的:
比如从驱动层到驱动框架层,由不同厂商的相同硬件模块创建了很多子类对象,然后对接到同一个父类接口上,多对一,体现面向对象的抽象的威力。以串口设备为例,不管下层是 STM32、GD32 还是别的平台的,只要都是串口设备,都对接到 RT-Thread 的串口设备类——如图所绘,多个硬件对象对接同一个父类对象接口。同理,从设备驱动框架层到IO设备管理接口层,又是多对一,又是再一次的屏蔽差异,再一次的抽象。——面向对象的思想贯穿其中。