UART(Universal Asynchronous Receiver/Transmitter,通用异步收发传输器)也常被称为串口。UART作为异步串口通信协议的一种,工作原理是将传输数据的每个字符一位接一位地传输。UART是在应用程序开发过程中使用频率最高的数据总线。在嵌入式设计中,UART常用于主机与辅助设备通信,如嵌入式设备与外接模块(Wi-Fi、蓝牙模块等)的通信,嵌入式设备与PC监视器的通信,或用于两个嵌入式设备之间的通信。
UART串口属于字符设备的一种,它的硬件连接也比较简单,只要两根传输线就可以实现双向通信:一根线(TX)发送数据,另一根线(RX)接收数据。
UART串口通信有几个重要的参数,分别是波特率、起始位、数据位、停止位和奇偶检验位,对于两个使用UART串口通信的端口,这些参数必须匹配,否则通信将无法正常完成。UART串口传输的数据格式如图2-1所示。
图2-1 UART串口传输的数据格式
从图2-1中可以看出,数据格式包含起始位、数据位、奇偶校验位、停止位。
对UART串口通信参数的解释如下。
❏起始位:表示数据传输的开始,电平逻辑为“0”。
❏数据位:数据位通常为8bit的数据(一个字节),但也可以是其他大小,例如5bit、6bit、7bit,表示传输数据的位数。
❏奇偶校验位:用于接收方对接收到的数据进行校验,校验一个二进制数中“1”的个数为偶数(偶校验)或奇数(奇校验),以此来校验数据传送的正确性,使用时也可以不需要此位。
❏停止位:表示一帧数据的结束,电平逻辑为“1”。
❏波特率:串口通信时的速率,它用单位时间内传输的二进制代码的有效位数来表示,其单位为bit/s。常见的波特率值有4800、9600、14400、38400、115200等,数值越大数据传输越快,波特率为115200表示每秒传输115200位数据。
本章内容基于UART v2.0版本的UART框架和驱动进行讲解,涵盖UART层级结构和驱动开发步骤、创建并操作UART设备、处理串口中断、增加串口DMA模式,以及驱动配置与验证。
UART层级结构如图2-2所示。
图2-2 UART层级结构图
1)I/O设备管理层向应用层提供rt_device_read/write等标准接口,应用层可以通过这些标准接口访问UART设备。
2)UART设备驱动框架源码文件为serial_v2.c,位于RT-Thread源码的components\drivers\serial文件夹中。抽象出的UART设备驱动框架和平台无关,是一层通用的软件层。UART设备驱动框架提供以下功能。
①对接上层的I/O设备管理层,以让应用层调用I/O设备管理层提供的统一接口对UART进行操作。
②UART设备驱动框架向UART设备驱动层提供UART设备操作方法接口struct rt_uart_ops(如configure、control、putc、getc、transmit),驱动开发者需要实现这些接口。
③提供设备注册管理接口rt_hw_serial_register和中断处理接口rt_hw_serial_isr。
3)UART设备驱动源码文件为drv_usartv2.c,放在具体bsp目录下,v2表示对接在串口v2版本的设备驱动框架上。UART设备驱动的实现与平台相关,它操作具体的MCU UART控制器。UART设备驱动需要实现UART设备的操作方法struct rt_uart_ops,以提供访问和控制UART硬件的能力。这一层也负责调用rt_hw_serial_register函数将UART设备注册到操作系统。最后还需调用中断处理接口rt_hw_serial_isr,通知UART设备驱动框架层处理数据。
4)最下面一层是MCU外接的UART模块,如UART通信模块、RS-232芯片或者RS-485芯片电路模块等,这样MCU就可以与外接模块进行数据通信了。
UART设备驱动开发的主要任务就是实现串口设备操作方法接口struct rt_uart_ops,然后注册串口设备。本章将会以STM32的UART驱动为例讲解UART驱动的具体实现。接下来先按照步骤创建一个UART设备。
本节介绍如何创建UART设备。对UART设备来说,在驱动开发时需要先从struct rt_serial_device结构中派生出新的串口设备模型,然后根据自己的设备类型定义私有数据域。特别是在可能有多个类似设备的情况下(例如串口1、串口2),设备接口可以共用同一套接口,不同的只是各自的数据域(例如寄存器基地址)。
例如,STM32的UART设备模型从struct rt_serial_device派生,并增加了STM32 UART的特有数据结构,如STM32串口句柄、串口配置信息、DMA结构信息等,代码如下所示。
串口驱动根据此类型定义串口设备对象并初始化相关变量,MCU一般都支持多个串口,所以串口驱动也可以支持多个串口设备。以下是在驱动文件中定义多个串口设备的代码片段,其中定义了每个串口的配置信息,如名称、句柄、中断入口等,同时定义了串口配置信息表和串口对象表,包含多个串口对象信息。
UART设备驱动框架层为UART设备驱动层提供的操作方法原型如下所示。在开发驱动时,需要为设备定义并实现这些操作方法。
这些操作方法会完成串口的基本操作,例如:configure方法用于配置串口(波特率等);control方法用于控制串口;putc方法用于串口向外发送字符数据;getc方法用于串口获取字符数据;transmit方法用于数据发送,主要是进行多字节数据的发送。下面继续讲解如何实现这些操作方法。
操作方法configure的作用是根据配置参数对UART设备进行配置,配置参数如波特率、接收缓冲区大小、数据位、停止位、奇偶校验等,UART设备在初始化时会调用此方法,其原型如下所示。
configure方法的参数及返回值如表2-1所示。
表2-1 configure方法的参数及返回值
参数cfg是串口设备的配置参数,结构原型为struct serial_configure,如下所示。configure方法根据这些成员的值进行配置。
成员可取值以及参考的默认取值如下所示,读者可以根据实际用途选择合适的值。
使用STM32串口驱动configure方法的实现示例如下。其内容主要是初始化STM32 UART的句柄,将cfg配置参数赋值给STM32 UART的句柄的成员,然后对串口进行初始化操作。
操作方法control用于控制UART设备行为,会根据传入的参数cmd(控制命令)对串口的行为进行相应的控制,例如配置设备、关闭设备、清除中断等操作,其原型如下所示。
control方法的参数及返回值如表2-2所示,该方法根据控制命令cmd和控制参数arg控制串口设备,如开关中断及DMA的配置。
表2-2 control方法的参数及返回值
在驱动实现时,需要完成的cmd取值情况如下所示。如果注释中标明“驱动中不用实现”,表示在实现驱动代码时不用考虑cmd的取值,因为这些取值无关底层设备,而系统对这些取值的处理也已经在设备驱动框架中实现了。
以下是STM32串口驱动的control方法的代码,该方法实现了6种cmd命令对应的操作。
操作方法putc用于发送一个字符的数据,其原型如下所示。
putc方法的参数及返回值如表2-3所示。
表2-3 putc方法的参数及返回值
我们看一个具体的putc方法的示例代码,STM32串口驱动中putc方法实现的部分代码如下所示。
在示例代码中,先利用接口rt_container_of获取到STM32的UART设备模型,然后等待上一次数据发送完成后再向硬件寄存器发送一个字符数据。这里需要注意,在UART设备驱动drv_usart_v2.h中,已经定义了向寄存器写入数据的宏UART_SET_TDR,供驱动开发者使用:
操作方法getc用于从硬件寄存器中接收一个字符数据,其原型如下所示。
getc方法的参数及返回值如表2-4所示。
表2-4 getc方法的参数及返回值
我们看一个具体的getc方法的示例代码,STM32串口驱动中getc方法实现的部分代码如下所示。
在示例代码中,同样先获取STM32的UART设备模型uart,然后利用UART_GET_RDR宏从硬件寄存器中读取一个字符的数据。其中,UART设备驱动drv_usart_v2.h定义了获取寄存器数据的宏UART_GET_RDR,供驱动开发者使用:
操作方法transmit一般用于中断和DMA的数据发送,其原型如下所示。
transmit方法的参数及返回值如表2-5所示。
表2-5 transmit方法的参数及返回值
其中参数tx_flag可取值如下,驱动开发者可以根据以下两种情况完成驱动:
我们来看一个在STM32上实现串口transmit方法的示例代码:
在示例代码中,首先检测是否使用DMA发送数据,然后直接调用了STM32 HAL库提供的DMA传输接口,完成了数据的发送。
UART设备的操作方法实现后需要注册设备到操作系统,注册UART设备的rt_hw_serial_register接口如下所示:
rt_hw_serial_register接口的参数及返回值如表2-6所示。
表2-6 rt_hw_serial_register接口的参数及返回值
其中,flag参数支持下列取值(可以采用“按位或”的方式支持多种操作):
注意:RT_DEVICE_FLAG_STREAM流模式主要是当串口外设作为控制台时才会使用,该模式用来解决用户回车换行的问题,在正常的串口外设通信场景中一般不会使用该模式。
在注册UART设备之前,需要根据struct rt_uart_ops的定义创建一个全局的ops结构体变量stm32_uart_ops。stm32_uart_ops将在注册UART设备时赋值给UART设备的ops参数。在STM32中注册设备的代码如下所示。
在示例代码中,因为STM32串口驱动只实现了中断接收、DMA接收及轮询发送的模式,所以注册设备时flag参数取值为RT_DEVICE_FLAG_RDWR、RT_DEVICE_FLAG_INT_RX、RT_DEVICE_FLAG_DMA_RX,表示串口设置支持读写、中断接收及DMA接收模式,轮询发送模式不需要置标志位。
stm32_uart_ops中的stm32_configure是操作方法对应的函数名,即函数指针,函数需要按照rt_uart_ops结构中的configure原型实现,并赋值给各个相应的成员,剩余其他操作方法的函数也一样。操作方法的名称可以自定义,但不要脱离实际意义,并且需要遵循代码规范。所有的操作方法的函数都属于内部函数,在函数实现时,需要使用static进行修饰。
UART设备驱动需要将对应的中断事件通知给UART设备驱动框架,让驱动框架完成后续的数据收发处理等事情。UART设备中断处理需要使用UART设备驱动库的中断处理函数调用RT-Thread UART设备驱动框架提供的rt_hw_serial_isr函数,从而通知UART设备驱动框架对应中断的发生。rt_hw_serial_isr()中断处理函数的原型如下所示:
rt_hw_serial_isr中断处理函数的参数如表2-7所示。
表2-7 rt_hw_serial_isr中断处理函数的参数
根据不同的中断事件,event可取以下值:
来看一个STM32 UART设备中断处理示例。在如下所示的代码中,使用STM32 UART驱动库的中断处理函数USARTx_IRQHandler调用RT-Thread UART设备驱动框架提供的rt_hw_serial_isr中断处理函数,以完成中断的对接,且在进入与退出中断时需要调用中断进入和中断退出函数。
DMA(Direct Memory Access,直接存储器访问)是现代处理器的特色功能,用于提供外设和存储器或者存储器和存储器之间的高速数据传输。DMA模式的数据传输,在CPU初始化完成这个传输动作之后,由DMA控制器直接将数据从一个地址空间复制到另一个地址空间,而不用CPU参与传输过程,这大大提高了CPU的运行效率。如果硬件MCU UART支持DMA模式的数据收发,则可实现该功能。每个串口设备都有自己的DMA配置参数,比如使用的硬件DMA控制器、DMA通道等。
增加UART设备DMA模式,需要首先对每个UART的DMA进行配置,接着进行DMA初始化和中断处理,最后完成DMA发送。以下是DMA配置代码。
DMA基础配置完成之后,可以开始实现DMA的初始化、DMA中断处理以及DMA发送相关的代码。
1.DMA初始化
增加串口DMA模式需对串口DMA进行初始化。stm32_control接口会调用stm32_dma_config初始化DMA,主要是完成串口DMA句柄的初始化及对应中断的配置,DMA初始化的部分代码如下所示。
2.DMA中断处理
为UART设备增加DMA模式需要进行DMA中断处理,DMA中断处理包含DMA中断接收处理与发送处理。STM32串口DMA中断接收与发送的代码如下所示,该代码实现了DMA相应中断以及回调函数。进入与退出中断时,需要调用中断进入和中断退出函数。
STM32的DMA中断回调函数如下所示:
3.DMA发送
最后完成DMA发送,DMA发送是基于transmit方法实现的,以下是在STM32中的实现,即在transmit操作方法中增加对DMA标志的判断,从而进行DMA发送。
RT-Thread使用SCons构建工程,使用基于Kconfig机制的menuconfig工具配置工程。因此不仅要实现驱动,还要实现驱动相关的配置选项:一是Kconfig配置,配置好的配置文件将会在menuconfig工具中形成对应的配置界面;二是进行SConscript配置,配置好后,相应的驱动文件将会被添加到工程中。后面各章的驱动相关配置选项与此类似,如无特殊配置将不再赘述。
1.Kconfig配置
下面参考bsp/stm32/stm32f407-atk-explorer/board/Kconfig文件配置串口驱动的相关选项,如下所示:
代码段中相关宏的说明如下所示。
❏BSP_USING_UART:串口驱动代码对应的宏定义,这个宏控制串口驱动相关代码是否会添加到工程中。
❏RT_USING_SERIAL:串口驱动框架代码对应的宏定义,这个宏控制串口驱动框架的相关代码是否会添加到工程中。
❏BSP_USING_UART1:串口设备1对应的宏定义,这个宏控制串口设备1是否会注册到系统中。
❏BSP_UART1_RX_USING_DMA:串口设备1使用DMA接收数据。
2.SConscript配置
在HAL_Drivers/SConscript文件中为串口驱动添加判断选项,代码如下所示。这是一段Python代码,表示如果定义了宏BSP_USING_UART,则drv_uart.c会被添加到工程的源文件中。
注册设备之后,UART设备将以字符设备的形式在I/O设备管理器中存在。系统启动并开始运行后,可以在终端使用list_device命令看到注册的设备包含了UART设备,之后则可以使用UART设备驱动框架提供的统一API对UART设备进行操作。
串口收发的验证方法是:可以使用TTL转串口工具将开发板上UART对应的TX、RX引脚连接到PC电脑上,然后通过调用下面的示例代码查看串口终端有没有输出。
注意:一般情况下,在打开串口时,我们会选择发送阻塞模式以及接收非阻塞模式来进行开发,即:
还有其他的可配置模式,驱动开发者可以根据需要选用,具体如下所示。
注意,RT_DEVICE_FLAG_STREAM流模式在串口外设作为控制台时才会使用,该模式用来解决用户回车换行的问题,在正常的串口外设通信场景中一般不会使用该模式。
本章讲解了如何开发UART设备驱动、如何将UART设备对接到设备驱动框架、如何验证UART设备驱动是否可用。
在RT-Thread中,将UART外设抽象为UART设备,并结合UART设备的通用操作方法与驱动框架思想设计出UART设备驱动框架,这为开发者提供了更便利的设备控制方式。同时,这使基于UART设备编写出来的应用代码更具兼容性与通用性。
开发者还需要注意以下两点。
1)操作方法的名称可以自定义,但不要脱离实际意义,并且需要遵守代码规范。所有的操作方法/函数都属于内部函数,在函数实现时,需要使用static进行修饰。 本条注意事项对每种驱动都适用,后面章节将不再赘述。
2)在进入与退出中断时,需要调用中断进入和中断退出函数,如下所示。 本条注意事项对每种驱动都适用,后面章节将不再赘述。