本文由RT-Thread论坛用户123原创发布:https://club.rt-thread.org/as...
RTT串口V1版本的使用分析及问题排查指南(一)
简述
无论是刚接触 RT-Thread 的新手,还是经验老道的大牛们,他们使用 RT-Thread 的时候,使用最频繁最广泛的外设,想必也非串口设备莫属。
回想大家在移植一个新的BSP或者芯片时,如何验证是否移植成功呢?是的,msh控制台窗口走一波 RTT 的 logo 信息,输出成功了就基本代表移植成功了。如下所示:
\ | /
- RT - Thread Operating System
/ | \ 4.0.4 build Dec 32 2021
2006 - 2021 Copyright by rt-thread team
msh >
例如 finsh 组件,以命令行的方式实现人机交互的功能,在项目开发调试中有着举足轻重的作用,也是开发者们使用最为频繁的组件。
又例如 ULOG组件,AT组件,ymodem组件,RT_Link组件等,其底层数据流都有串口的踪迹。因此,作为使用最频繁,覆盖面最广泛的串口设备,如果把它搞懂,那将会在你的项目开发中如虎添翼,或对嵌入式系统也会有更深刻的理解。
既然串口设备如此重要,受众人群又如此之多,使用范围如此之广,那么有必要去理一理串口框架,汇总一下问题所在,为广大开发者们指一下解决问题的方向。这也是这篇文章的主要工作。
本文会先结合STM32为平台,以串口V1版本做分析说明 (第一部分),并总结遇到串口方面的问题该如何解决的方法(第二部分)。
(在此需要解释一下,串口V1版本
这个名字非官方冠名,是本人为了区分串口V2版本
而临时起的名字,当然后续会有串口V2版本的介绍说明,马不停蹄更新中)
由于串口V1版本已经历经多年的开发与迭代,也被多数开发者整理成文,广为流传,本文若重新对其做细致的分析显然是在浪费各位看官的宝贵时间。因此,本文旨在总结串口使用过程中遇到的问题,弱化分析串口的执行流程,如需详细的串口流程分析,可移步文档中心串口章节,或者自行上网搜索,相信你一定能找到合适的答案。那么废话不多说,开整。
串口设备使用说明
串口流程分析分为两部分,一部分是串口驱动,另一部分是串口框架,用户(应用层)使用串口时,是按照下图的模型进行操作的:
首先需要使用 Device 框架(源码位置在 src/device.c
中),什么是Device 框架,比如用到的 rt_device_open/close/read/write/control()
等API操作接口,就是使用的 Device 框架的接口。本文中不做深入探讨,了解即可。
其次是 UART 设备驱动框架 (源码位置在 components\drivers\serial\serial.c
),串口设备驱动框架实现了Device框架的操作方法的接口rt_serial_open/close/read/write/control()
。举个例子,例如Device框架的rt_device_open()
接口是打开一个设备对象,而对应到串口框架上,就是对接到了 rt_serial_open()
。
然后是串口设备驱动(源码位置在 bsp/xxx/drivers/drv_usart.c
),串口设备驱动负责实例化串口设备 。这一层调用了 rt_hw_serial_register()
函数注册串口设备到操作系统中,也是与串口硬件直接打交道的媒介,这一层将会看到串口硬件的配置、读写寄存器、中断的操作等。
下面这张图将介绍串口设备的使用序列:
串口各个模式的流程分析
串口框架目前适配了三种硬件工作模式,即串口轮询(发送/接收)、串口中断(发送/接收)、串口DMA(发送/接收)。下面分别对三种模式进行介绍:
一:轮询模式
轮询模式即调用串口 接收/发送 的 API 后将一直占用CPU资源直到数据收发完成才返回,使用时需要用户主动调用 接收/发送 的API接口才会执行相应的操作。
轮询接收
应用程序调用 rt_device_read()
接收数据,轮询接收模式下会调用到底层串口驱动提供的 getc
接口,每次接收一个字节,如果接收不到数据将一直占用CPU资源。
轮询发送
应用程序调用 rt_device_write()
发送数据,轮询发送模式下会最终会调用到底层串口驱动提供的 putc
接口,每次发送一个字节,循环发送直到待发送的数据发送完成。
轮询接收和发送调用关系如下图所示:
二:中断模式
中断模式下收发数据时,将数据的收发过程放在中断中进行,不再持续占用CPU资源,应用程序只用负责将数据写入串口数据寄存器,然后线程就会让出资源,空出了程序等待串口硬件收发数据的时间。注意一点:中断接收和发送时,每次在中断中操作的还是单个字节。
中断接收
串口硬件接收到一个字节的数据后会触发中断并调用串口驱动框架的 rt_hw_serial_isr()
函数,此函数会将这一字节数据写入环形缓冲区,用户设置的回调函数也会被调用。应用程序读取数据实际是从环形缓冲区读取。
中断接收调用关系如下图所示:
中断发送
串口框架负责调用底层 putc
接口向串口寄存器写入待发送的数据,然后等待此数据发送完成的信号,此时当前线程会被挂起。上一个数据发送完成后会调用串口驱动框架的 rt_hw_serial_isr()
函数,此函数会发送完成信号唤醒发送数据线程,并重置完成信号状态为未完成。线程运行后会向串口寄存器写入下一个数据,然后等待完成信号,重复上一个流程,直到缓冲区数据发送完成。
该过程与中断接收流程很相像,再次不再贴图赘叙。
三:DMA模式
DMA模式下收发数据与中断模式很相像,可以粗略的认为,两种模式唯一不同的就是,中断模式每次收发数据为单字节,而DMA模式则是多个字节进行收发的。
DMA接收
使用 DMA 接收模式时,首先应用程序打开串口设备会指定打开标志为 RT_DEVICE_FLAG_DMA_RX,此时串口驱动框架会创建环形缓冲区并调用串口驱动层 control
接口使能 DMA 接收完成中断。
DMA接收调用关系如下图所示:
这里再啰嗦一句,在DMA中断处理流程中,有三种DMA接收中断,分别是DMA空闲中断(IDLE)、DMA半满中断(HFIT)和 DMA满中断(TCIT),这三个中断相辅相成,最后统一交由串口框架中断处理函数 rt_hw_serial_isr(RT_SERIAL_EVENT_RX_DMA_DONE)
中做处理。其中DMA空闲中断是在uart_isr中触发的,另外两个是在DMA接收回调中触发的,这两个接收回调是在DMA接收使能启动的时候,由HAL库注册的,用户无需关心这两个DMA接收回调的注册逻辑,只需知道,如果用户不想使用这两个DMA接收回调的时候,只要把函数里面的执行代码注释掉即可。类似下图这样 :
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
struct stm32_uart *uart;
RT_ASSERT(huart != NULL);
// uart = (struct stm32_uart *)huart;
// dma_isr(&uart->serial);
}
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
struct stm32_uart *uart;
RT_ASSERT(huart != NULL);
// uart = (struct stm32_uart *)huart;
// dma_isr(&uart->serial);
}
(至于为什么会有用户无需使用该回调函数的应用场景,这里先按下不表,后边会具体详细分析,而且这部分会很重要,该机制在串口V2上照样使用)
DMA发送
应用程序发送数据流程如下图所示,应用程序调用 rt_device_write()
发送数据,最终待发送数据的地址和大小会被放入数据队列,如果此时 DMA 空闲,就会开始发送数据。当 DMA 发送完成触发中断时,rt_hw_serial_isr()
函数会判断数据队列是否有待传输的数据并开始下次 DMA 传输,用户设置的回调函数也会被调用。
由上图我们可以看出,在使用DMA发送时,串口框架并未使用环形缓冲区,然而我们又知道,DMA发送时,肯定是需要缓冲区来存放数据的,那么DMA发送时候的数据块,存放在哪里呢?这里就需要注意了,DMA发送时候,数据缓冲区是由用户应用层定义的,在传给串口框架的时候,是将应用层定义的数据缓冲区的指针传递过来。如果在发送过程中,该缓冲区的内容被意外修改了,那么将会导致DMA发送的数据出现错误。我们可以再结合下面的图继续说明:
串口相关问题解析
篇幅有限,本节内容见 RTT串口V1版本的使用分析及问题排查指南(二)