1)实验平台:正点原子MiniPro H750开发板
2)平台购买地址:https://detail.tmall.com/item.htm?id=677017430560
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-336836-1-1.html
4)对正点原子STM32感兴趣的同学可以加群讨论:879133275
本章我们将学习STM32H7的串口,教大家如何使用STM32H7的串口来发送和接收数据。本章将实现如下功能:STM32H7通过串口和上位机的对话,STM32H7在收到上位机发过来的字符串后,原原本本的返回给上位机。
本章分为如下几个小节:
17.1 串口简介
17.2 硬件设计
17.3 程序设计
17.4 下载验证
学习串口前,我们先来了解一下数据通信的一些基础概念。
17.1.1 数据通信的基础概念
在单片机的应用中,数据通信是必不可少的一部分,比如:单片机和上位机、单片机和外围器件之间,它们都有数据通信的需求。由于设备之间的电气特性、传输速率、可靠性要求各不相同,于是就有了各种通信类型、通信协议,我们常见的有:USART、IIC、SPI、CAN、USB等。下面,我们先来学习数据通信的一些基础概念。
图17.1.1.1 数据传输方式
串行通信的基本特征是数据逐位顺序依次传输,优点是传输线少成本低,抗干扰能力强可用于远距离传输,缺点就是传输速率低。
而并行通信是数据各位可以通过多条线同时传输,优点是传输速率高,缺点就是线多成本就高了,抗干扰能力差因而适用于短距离、高速率的通信。
2. 数据传输方向
根据数据传输方向,通信又可分为全双工、半双工和单工通信。全双工、半双工和单工通信的比较如下图所示:
图17.1.1.2 数据传输方式
单工是指数据传输仅能沿一个方向,不能实现反方向传输。
半双工是指数据传输可以沿着两个方向,但是需要分时进行。
全双工是指数据可以同时进行双向传输。
这里注意全双工和半双工通信的区别:半双工通信是共用一条线路实现双向通信,而全双工是利用两条线路,一条用于发送数据,另一条用于接收数据。
3. 数据同步方式
根据数据同步方式,通信又可分为同步通信和异步通信。同步通信和异步通信比较如下图所示:
图17.1.1.3 数据同步方式
同步通信要求通信双方共用同一时钟信号,在总线上保持统一的时序和周期完成信息传输。优点:可以实现高速率、大容量的数据传输,以及点对多点传输。缺点:要求发送时钟和接收时钟保持严格同步,收发双方时钟允许的误差较小,同时硬件复杂。
异步通信不需要时钟信号,而是在数据信号中加入开始位和停止位等一些同步信号,以便使接收端能够正确地将每一个字符接收下来,某些通信中还需要双方约定传输速率。优点:没有时钟信号硬件简单,双方时钟可允许一定误差。缺点:通信速率较低,只适用点对点传输。
4. 通信速率
在数字通信系统中,通信速率(传输速率)指数据在信道中传输的速度,它分为两种:传信率和传码率。
传信率:每秒钟传输的信息量,即每秒钟传输的二进制位数,单位为bit/s(即比特每秒),因而又称为比特率。
传码率:每秒钟传输的码元个数,单位为Baud(即波特每秒),因而又称为波特率。
比特率和波特率这两个概念又常常被人们混淆。比特率很好理解,我们来看看波特率,波特率被传输的是码元,码元是信号被调制后的概念,每个码元都可以表示一定bit的数据信息量。举个例子,在TTL电平标准的通信中,用0V表示逻辑0,5V表示逻辑1,这时候这个码元就可以表示两种状态。如果电平信号0V、2V、4V和6V分别表示二进制数00、01、10、11,这时候每一个码元就可以表示四种状态。
由上述可以看出,码元携带一定的比特信息,所以比特率和波特率也是有一定的关系的。
比特率和波特率的关系可以用以下式子表示:
比特率 = 波特率 * log2M
其中M表示码元承载的信息量。我们也可以理解M为码元的进制数。
举个例子:波特率为100 Baud,即每秒传输100个码元,如果码元采用十六进制编码(即M=2,代入上述式子),那么这时候的比特率就是400 bit/s。如果码元采用二进制编码(即M=2,代入上述式子),那么这时候的比特率就是100 bit/s。
可以看出采用二进制的时候,波特率和比特率数值上相等。但是这里要注意,它们的相等只是数值相等,其意义上不同,看波特率和波特率单位就知道。由于我们的所用的数字系统都是二进制的,所以有部分人久而久之就直接把波特率和比特率混淆了。
17.1.2 串口通信协议简介
串口通信是一种设备间常用的串行通信方式,串口按位(bit)发送和接收字节。尽管比特字节(byte)的串行通信慢,但是串口可以在使用一根线发送数据的同时用另一根线接收数据。串口通信协议是指规定了数据包的内容,内容包含了起始位、主体数据、校验位及停止位,双方需要约定一致的数据包格式才能正常收发数据的有关规范。在串口通信中,常用的协议包括RS-232、RS-422和RS-485等。
随着科技的发展,RS-232在工业上还有广泛的使用,但是在商业技术上,已经慢慢的使用USB转串口取代了RS-232串口。我们只需要在电路中添加一个USB转串口芯片,就可以实现USB通信协议和标准UART串行通信协议的转换,而我们开发板上的USB转串口芯片是CH340C这个芯片。关于USB转串口芯片的原理图请看17.2小节。
下面我们来学习串口通信协议,这里主要学习串口通信的协议层。
串口通信的数据包由发送设备的TXD接口传输到接收设备的RXD接口。在串口通信的协议层中,规定了数据包的内容,它由启始位、主体数据、校验位以及停止位组成,通讯双方的数据包格式要约定一致才能正常收发数据,其组成如图 17.1.2.1所示。
图17.1.2.1 串口通信协议数据帧格式
串口通信协议数据包组成可以分为波特率和数据帧格式两部分。
图17.1.3.1.1 USART框图
为了方便大家理解,我们把整个框图分成几个部分来介绍。
①时钟与波特率
这部分的主要功能就是为USART提供时钟以及配置波特率。
在USART框图中,可以看到有两个时钟域,usart_pclk 时钟域和usart_ker_ck 内核时钟域。usart_pclk是外设总线时钟,需要访问 USART 寄存器时,该信号必须有效。usart_ker_ck是USART时钟源,独立于 usart_pclk,由RCC提供。因此,即使usart_ker_ck 时钟停止,也可以连续对 USART 寄存器进行读/写操作。
波特率,即每秒钟传输的码元个数,在二进制系统中(串口的数据帧就是二进制的形式),波特率与波特率的数值相等,所以我们今后在把串口波特率理解为每秒钟传输的二进制位数。波特率的计算公式分为16倍过采样和8倍过采样:
在16倍过采样的情况下,波特率通过以下公式得出:
在8倍过采样的情况下,波特率通过以下公式得出:
usart_ker_ckpres为USART的工作时钟,在usart_ker_ck不分频的情况下,时钟频率最高为120MHz。USARTDIV 是一个无符号定点数,我们可以通过上述的式子得到,因为一般我们知道的是baud和usart_ker_ck的时钟。得到 USARTDIV 的值后,下一步就是要把该值设置到波特率寄存器USART1->BRR中,以完成波特率的设置。波特率的设置还得根据OVER8的值来确定格式。
OVER8是USART_CR1寄存器的位15,当OVER8 = 0时(即16 倍过采样),BRR = USARTDIV;当OVER8 = 1时,BRR[2:0] = USARTDIV[3:0],这里右移 1 位,BRR[3]必须保持清零,BRR[15:4] = USARTDIV[15:4]。无论是16倍采样还是8倍过采样,USARTDIV都必须大于或等于 0d16。
下面举个例子说明:
当在16倍过采样时需要得到 115200 的波特率,usart_ker_ckpre = 120MHZ,那么可得:
得到USARTDIV = 1042,可解得 BRR = USARTDIV = 1042d = 0412h,那么需要设置
USART_BRR 的值为 0x412。这里的USARTDIV是有余数的,我们用四舍五入进行取整,这样会导致波特率会有所偏差,而这样的小误差是可以被允许的。8倍过采样计算方法类似。
②收发数据
USART双向通信需要的两个引脚:
TX:发送数据输出引脚。
RX:接收数据输入引脚。
USART_TDR是USART发送数据寄存器,要发送什么数据,往这个寄存器里写即可,低9位有效。
USART_RDR是USART接收数据寄存器,要接收什么数据,读这个寄存器里写即可,低9位有效。
USART_TDR和USART_RDR的第9位是否有效,通过USART 控制寄存器 1(USART_CR1)的 M位(M0:位 12,M1:位 28)设置:
7 位字符长度:M[1:0] =“10”
8 位字符长度:M[1:0] =“00”
9 位字符长度:M[1:0] =“01”
我们基本都是使用 8位数据字长。
③控制寄存器
我们可以通过控制寄存器控制USART数据的发送、数据接收、各种通信模式的设置、中断、DMA 模式还有唤醒单元等。具体在后面讲解USART寄存器的时候细讲。
④DMA和中断功能
USART支持DMA传输,可以实现高速数据传输,具体我们会在DMA实验中为大家讲解。在 USART 通信过程中,中断可由不同事件生成,同时支持 USART 模块生成唤醒中断。常用的中断比如:发送数据寄存器为空、发送 FIFO 未满、发送完成、接收 FIFO 非空、接收 FIFO 已满等。
⑤USART信号引脚
在 RS232 硬件流控制模式下需要以下两个引脚:
CTS(清除以发送):发送器在发送下一帧数据之前会检测 CTS 引脚,如果为低电平,表示可以发送数据,如果为高电平则在发送完当前数据帧之后停止发送。
RTS(请求以发送):如果为低电平,则该信号用于指示 USART 已准备好接收数据。
在 RS485 硬件控制模式下需要下面这个引脚:
DE(驱动器使能):该信号用于激活外部收发器的发送模式。
在同步主/从模式和智能卡模式下需要以下引脚:
CK:该引脚在同步主模式和智能卡模式下用作时钟输出,在同步从模式下用作时钟输入。
NSS:该引脚在同步从模式下用作从器件选择输入。
这些引脚我们暂时都没有用到,就给大家简单提一下。
17.1.3.2 USART寄存器
STM32H750的串口使用起来还是蛮简单的,只要你开启了串口时钟,并设置相应IO口的模式,然后配置一下波特率,数据位长度,奇偶校验位等信息,就可以使用了。下面,我们就简单介绍下这几个与串口基本配置直接相关的寄存器。
(1)串口时钟使能
串口作为STM32H750的一个外设,其时钟由外设时钟使能寄存器控制,这里我们使用的串口1是在APB2ENR寄存器的第4位。注意:除了串口1和串口6的时钟使能在APB2ENR寄存器,其他串口的时钟使能位都在APB1LENR和APB4ENR寄存器。
(2)串口波特率设置
每个串口都有一个自己独立的波特率寄存器USART_BRR,通过设置该寄存器就可以达到配置不同波特率的目的。波特率设置介绍过,请回顾。
(3)串口控制
STM32H750的每个串口都有3个控制寄存器USART_CR1~3,串口的很多配置都是通过这3个寄存器来设置的。USART_CR1寄存器的描述如图17.1.3.2.1所示:
图17.1.3.2.1 USART_CR1寄存器
该寄存器我们只介绍本节需要用到的一些位:M[1:0]位(位28和12),用于设置字长,我们一般设置为:00表示1个起始位,8个数据位,n个停止位(n的个数,由USART_CR2的[13:12]位控制)。OVER8为过采样模式设置位,我们一般设置位0,即16倍过采样已获得更好的容错性;UE为串口使能位,通过该位置1,以使能串口;PCE为校验使能位,设置为0,则禁止校验,否则使能校验;PS为校验位选择位,设置为0则为偶校验,否则为奇校验;TXEIE为发送缓冲区空中断使能位,设置该位为1,当USART_ISR中的TXE位为1时,将产生串口中断;TCIE为发送完成中断使能位,设置该位为1,当USART_ISR中的TC位为1时,将产生串口中断;RXNEIE为接收缓冲区非空中断使能,设置该位为1,当USART_ISR中的ORE或者RXNE位为1时,将产生串口中断;TE为发送使能位,设置为1,将开启串口的发送功能;RE为接收使能位,用法同TE。
其他位的设置,大家可以参考《STM32H7xx参考手册_V7(英文版).pdf》 。
(4)数据发送与接收
与STM32F1和F4不同,STM32H7的串口发送和接收由两个不同的寄存器组成。发送数据是USART_TDR寄存器,接收数据是USART_RDR寄存器,USART_TDR寄存器描述如图17.1.3.2.2所示:
图17.1.3.2.2 USART_TDR寄存器
可以看出,USART_TDR虽然是一个32位寄存器,但是只用了低9位(DR[8:0]),其他都是保留,TDR[8:0]为串口数据,具体多少位,由前面介绍的M[1:0]决定(一般是8位数据)。
当我们需要发送数据的时候,往USART_TDR寄存器写入你想要发送的数据,就可以通过串口发送出去了。而当有串口数据接收到,需要读取出来的时候,我们则必须读取USART_RDR寄存器,USART_RDR寄存器各位描述同USART_TDR是完全一样的,只是一个用来接收,一个用来发送。
当使能校验位(USART_CR1中PCE位被置位)进行发送时,写到MSB的值(根据数据的长度不同,MSB是第7位或者第8位)会被后来的校验位取代。
当使能校验位进行接收时,读到的MSB位是接收到的校验位。
(5)串口状态
串口状态通过状态寄存器USART_ISR读取。USART_ISR的各位描述如图17.1.3.2.3所示:
图17.1.3.2.3 USART_ISR寄存器
这里我们关注一下两个位,第5、6位RXNE和TC。
RXNE(读数据寄存器非空),当该位被置1的时候,就是提示已经有数据被接收到了,并且可以读出来了。这时候我们要做的就是尽快去读取USART_RDR,通过读USART_RDR可以将该位清零,也可以向该位写0,直接清除。
TC(发送完成),当该位被置位的时候,表示USART_TDR内的数据已经被发送完成了。如果设置了这个位的中断,则会产生中断。该位也有两种清零方式:
1)读USART_ISR,写USART_TDR。
2)向ICR寄存器的TCCF位写1。
通过以上一些寄存器的操作再加上IO口的配置,我们就可以达到串口最基本的配置了,关于串口更详细的介绍,请参考《STM32H7xx参考手册_V7(英文版).pdf》通用同步异步收发器这一章。
17.1.4 GPIO引脚复用功能
我们知道芯片有许多外设,而引脚的资源是很有限的,为了解决这个问题,方法就是引脚复用,这样使得引脚除了作为普通的IO口之外,还会与一些外设关联起来,作为第二功能使用,而且一个引脚不单单只有一种复用功能,而是拥有多个第二功能,但是一次只允许一个外设的复用功能,以确保共用同一个 IO 引脚的外设之间不会产生冲突。
下面我们把之前没讲解的GPIO复用功能寄存器讲解一下。
GPIO复用功能寄存器(AFRH 和 AFRL)
复用功能寄存器有2个,都是32位有效的寄存器,分高位(AFRH)和低位(AFRL)。复用器采用16路复用功能输入AF0~AF15,通过GPIOx_AFRL(引脚 0~7)、GPIOx_AFRH(引脚 8~15)寄存器对复用功能输入进行配置,每四位控制1路复用。
图17.1.4.1 AFRL寄存器
AFRL寄存器配置引脚 0~7复用功能,AFRH寄存器配置引脚 8~15复用功能。
IO口并不是想复用什么功能都可以,是有规定的,每个 IO 引脚的复用可以通过查阅数据手册《STM32H750VBT6.pdf》,我们看看Table 8. Port A alternate functions,其他的端口请自行查阅。
表17.1.4.1 Port A引脚复用
我们圈出来PA9和PA10对应AF7这列,因为我们的串口1就是用到这两个IO口,配置复用功能,使得PA9用做串口1的发送引脚TX,PA10则用做串口1的接收引脚RX。
我们还需要学会在HAL库中寻找这些复用的宏定义,在stm32h7xx_hal_gpio_ex.h文件中可以找到。从表17.1.4.1我们知道PA9和PA10都是在复用器的AF7这一路中,所以我们在HAL库中就找AF7的宏定义,其定义如下:
/**
* @brief AF 7 selection
*/
#define GPIO_AF7_SPI2 ((uint8_t)0x07) /* SPI2 Alternate Function mapping */
#define GPIO_AF7_SPI3 ((uint8_t)0x07) /* SPI3 Alternate Function mapping */
#define GPIO_AF7_SPI6 ((uint8_t)0x07) /* SPI6 Alternate Function mapping */
#define GPIO_AF7_USART1 ((uint8_t)0x07) /* USART1 Alternate Function mapping */
#define GPIO_AF7_USART2 ((uint8_t)0x07) /* USART2 Alternate Function mapping */
#define GPIO_AF7_USART3 ((uint8_t)0x07) /* USART3 Alternate Function mapping */
#define GPIO_AF7_USART6 ((uint8_t)0x07) /* USART6 Alternate Function mapping */
#define GPIO_AF7_UART7 ((uint8_t)0x07) /* UART7 Alternate Function mapping */
#define GPIO_AF7_DFSDM1 ((uint8_t)0x07) /* DFSDM Alternate Function mapping */
#define GPIO_AF7_SDMMC1 ((uint8_t)0x07) /* SDMMC1 Alternate Function mapping */
细心的朋友可以看出,这些宏定义的值都是一样的,即都是(uint8_t)0x07,而宏名只是为了区分是哪个外设而已。因为我们的外设是串口1,所以就很容易选择到我们复用的功能要用到的宏定义,就是GPIO_AF7_USART1。具体的场景应用请看我们的串口1的初始化源码。
17.2 硬件设计
图17.2.1 USB转串口原理图
这里需要注意的是:上图中的红色的接线引脚和蓝色的接线引脚需要用短路帽连接起来,否则串口的信号线跟USB接口是没有连接的。这里我们要把P5的RXD和PA9用跳线帽连接,以及TXD和PA10也用跳线帽连接。如图17.2.2所示:
图17.2.2 短路帽连接
17.3 程序设计
17.3.1 USART的HAL库驱动
HAL库中关于串口的驱动程序比较多,我们主要先来学习本章需要用到的,其余的后续用到再讲解。因为我们现在只是用到异步收发器功能,所以我们现在只需要stm32h7xx_hal_uart.c文件(及其头文件)的驱动代码,stm32h7xx_hal_usart.c是通用同步异步收发器,暂时没有用到,可以暂时不看。用到一个外设第一个函数就应该是其初始化函数。
typedef struct
{
USART_TypeDef *Instance; /* UART寄存器基地址 */
UART_InitTypeDef Init; /* UART通信参数 */
UART_AdvFeatureInitTypeDef AdvancedInit; /* UART高级功能配置结构体 */
uint8_t *pTxBuffPtr; /* 指向 UART 发送缓冲区 */
uint16_t TxXferSize; /* UART发送数据的大小 */
__IO uint16_t TxXferCount; /* UART发送数据的个数 */
uint8_t *pRxBuffPtr; /* 指向UART接收缓冲区 */
uint16_t RxXferSize; /* UART接收数据大小 */
__IO uint16_t RxXferCount; /* UART接收数据的个数 */
uint16_t Mask; /* UART数据接收寄存器掩码 */
DMA_HandleTypeDef *hdmatx; /* UART 发送参数设置(DMA) */
DMA_HandleTypeDef *hdmarx; /* UART 接收参数设置(DMA) */
HAL_LockTypeDef Lock; /* 锁定对象 */
__IO HAL_UART_StateTypeDef gState; /* UART发送状态结构体 */
__IO HAL_UART_StateTypeDef RxState; /* UART接收状态结构体 */
__IO uint32_t ErrorCode; /* UART操作错误信息 */
}UART_HandleTypeDef;
1)Instance:指向UART 寄存器基地址。实际上这个基地址HAL库已经定义好了,可以选择范围:USART1~ USART3、USART6、UART4、UART5、UART7、UART8。
2)Init:UART初始化结构体,用于配置通讯参数,如波特率、数据位数、停止位等等。下面我们再详细讲解这个结构体。
3)AdvancedInit:用于配置高级功能,如自动波特率,MSB 先行等。
4)pTxBuffPtr,TxXferSize,TxXferCount:分别是指向发送数据缓冲区的指针,发送数据的大小,发送数据的个数。
5)pRxBuffPtr,RxXferSize,RxXferCount:分别是指向接收数据缓冲区的指针,接受数据的大小,接收数据的个数;
6)Mask:UART数据接收寄存器的掩码,用于存放数据的校验位。
7)hdmatx,hdmarx:配置串口发送接收数据的 DMA具体参数。
8)Lock:对资源操作增加操作锁保护,可选HAL_UNLOCKED或者HAL_LOCKED两个参数。如果gState的值等于HAL_UART_STATE_RESET,则认为串口未被初始化,此时,分配锁资源,并且调用HAL_UART_MspInit函数来对串口的GPIO和时钟进行初始化。
9)gState,RxState:分别是UART的发送状态、工作状态的结构体和UART接受状态的结构体。HAL_UART_StateTypeDef 是一个枚举类型,列出串口在工作过程中的状态值,有些值只适用于gState,如 HAL_UART_STATE_BUSY。
10)ErrorCode:串口错误操作信息。主要用于存放串口操作的错误信息。
下面,我们来了解UART_InitTypeDef 这个结构体类型,该结构体用于配置UART的各个通信参数,包括波特率,停止位等,具体说明如下:
typedef struct
{
uint32_t BaudRate; /* 波特率 */
uint32_t WordLength; /* 字长 */
uint32_t StopBits; /* 停止位 */
uint32_t Parity; /* 校验位 */
uint32_t Mode; /* UART模式 */
uint32_t HwFlowCtl; /* 硬件流设置 */
uint32_t OverSampling; /* 过采样设置 */
uint32_t OneBitSampling; /* 采样位方法选择 */
uint32_t Prescaler; /* 时钟分频 */
uint32_t FIFOMode; /* FIFO模式 */
uint32_t TXFIFOThreshold; /* 发送FIFO阈值 */
uint32_t RXFIFOThreshold; /* 接受FIFO阈值 */
}UART_InitTypeDef
1)BaudRate:波特率设置。一般设置为 2400、9600、19200、115200。
2)WordLength:数据帧字长,可选 8 位或 9 位。这里我们设置为8位字长数据格式。
3)StopBits:停止位设置,可选0.5个、1个、1.5个和2个停止位,一般我们选择1个停止位。
4)Parity:奇偶校验控制选择,我们设定为无奇偶校验位。
5)Mode:UART模式选择,可以设置为只收模式,只发模式,或者收发模式。这里我们设置为全双工收发模式。
6)HwFlowCtl:硬件流控制选择,我们设置为无硬件流控制。
7)OverSampling:过采样选择,选择8倍过采样或者16过采样,一般选择16过采样。
8)OneBitSampling:一个采样位方法使能,0:三个采样位方法;1:一个采样位方法。
9)Prescaler:时钟分频系数,默认选择不分频。
10)FIFOMode:FIFO模式的使能或失能。
11)TXFIFOThreshold:发送 FIFO 的阈值。当达到设定的阈值时,将数据发送给TX移位寄存器。阈值的值可以为容量 1/8,1/4,1/2,3/4,7/8,满。
12)RXFIFOThreshold:接收FIFO的阈值。当达到设定的阈值时,将数据给接收寄存器。阈值的值可以为容量 1/8,1/4,1/2,3/4,7/8,满。
函数返回值:
HAL_StatusTypeDef枚举类型的值,有4个,分别是HAL_OK表示成功,HAL_ERROR表示错误,HAL_BUSY表示忙碌,HAL_TIMEOUT超时。后续遇到该结构体也是一样的。
2. HAL_UART_Receive_IT函数
HAL_UART_Receive_IT函数是开启串口接收中断函数。其声明如下:
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart,
uint8_t *pData, uint16_t Size);
函数描述:
用于开启以中断的方式接收指定字节。数据接收在中断处理函数里面实现。
函数形参:
形参1是UART_HandleTypeDef 结构体指针类型的串口句柄。
形参2是要接收的数据地址。
形参3是要接收的数据大小,以字节为单位。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
3. HAL_UART_IRQHandler函数
HAL_UART_IRQHandler函数是HAL库中断处理公共函数。其声明如下:
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart);
函数描述:
该函数是HAL库中断处理公共函数,在串口中断服务函数中被调用。
函数形参:
形参1是UART_HandleTypeDef 结构体指针类型的串口句柄。
函数返回值:无
注意事项:
该函数是HAL库已经定义好,用户一般不能随意修改。,如果用户要在中断中实现自己的逻辑代码,可以直接在函数HAL_UART_IRQHandler 的前面或者后面添加新代码,也可以直接在 HAL_UART_IRQHandler 调用的各种回调函数里面执行,这些回调都是弱定义的,方便用户直接在其它文件里面重定义。串口回调函数主要有下面几个:
__weak void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
__weak void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart)
__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
__weak void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
__weak void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
__weak void HAL_UART_AbortCpltCallback(UART_HandleTypeDef *huart)
__weak void HAL_UART_AbortTransmitCpltCallback(UART_HandleTypeDef *huart)
__weak void HAL_UART_AbortReceiveCpltCallback(UART_HandleTypeDef *huart)
本实验我们用到的是接收回调函数HAL_UART_RxCpltCallback,就是在接收回调函数里面编写我们的接收逻辑代码,具体请参考实验源码。
串口通信配置步骤
图17.3.2.1 串口通信实验程序流程图
17.3.3 程序解析
#define USART_TX_GPIO_PORT GPIOA
#define USART_TX_GPIO_PIN GPIO_PIN_9
#define USART_TX_GPIO_AF GPIO_AF7_USART1
/* 发送引脚时钟使能 */
#define USART_TX_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)
#define USART_RX_GPIO_PORT GPIOA
#define USART_RX_GPIO_PIN GPIO_PIN_10
#define USART_RX_GPIO_AF GPIO_AF7_USART1
/* 接收引脚时钟使能 */
#define USART_RX_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)
#define USART_UX USART1
#define USART_UX_IRQn USART1_IRQn
#define USART_UX_IRQHandler USART1_IRQHandler
/* USART1 时钟使能 */
#define USART_UX_CLK_ENABLE() do{ __HAL_RCC_USART1_CLK_ENABLE(); }while(0)
PA9和PA10都在复用器的AF7这路信号线中,所以都复用为GPIO_AF7_USART1,前面有提过。USART1_IRQn也就是我们中断向量表的37号中断,USART1_IRQHandler是串口1的中断服务函数。每个串口都有自己的中断函数,但是我们最终都是通过回调函数去实现逻辑代码,当然我们亦可在中断函数里实现逻辑代码。
另外我们还定义了三个宏,具体如下:
#define USART_REC_LEN 200 /* 定义最大接收字节数 200 */
#define USART_EN_RX 1 /* 使能(1)/禁止(0)串口1接收 */
#define RXBUFFERSIZE 1 /* 缓存大小 */
可以看到USART_REC_LEN表示最大接收字节数,这里定义的是200个字节,后续如果有需求要发送更大的数据包,可以改大这个值,这里不改太大,是避免浪费太多内存。USART_EN_RX则是用于使能串口1的接收数据。RXBUFFERSIZE是缓冲大小。
下面我们再解析usart.c的程序,先看串口1的初始化函数,其定义如下:
/**
* @brief 串口X初始化函数
* @param baudrate: 波特率, 根据自己需要设置波特率值
* @note 注意: 必须设置正确的时钟源, 否则串口波特率就会设置异常.
* 这里的USART的时钟源在sys_stm32_clock_init()函数中已经设置过了.
* @retval 无
*/
void usart_init(uint32_t baudrate)
{
g_uart1_handle.Instance = USART_UX; /* USART1 */
g_uart1_handle.Init.BaudRate = baudrate; /* 波特率 */
g_uart1_handle.Init.WordLength = UART_WORDLENGTH_8B; /* 字长为8位数据格式 */
g_uart1_handle.Init.StopBits = UART_STOPBITS_1; /* 一个停止位 */
g_uart1_handle.Init.Parity = UART_PARITY_NONE; /* 无奇偶校验位 */
g_uart1_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; /* 无硬件流控 */
g_uart1_handle.Init.Mode = UART_MODE_TX_RX; /* 收发模式 */
HAL_UART_Init(&g_uart1_handle); /* HAL_UART_Init()会使能UART1 */
/*该函数会开启接收中断:标志位UART_IT_RXNE,并且设置接收缓冲以及接收缓冲接收最大数据量*/
HAL_UART_Receive_IT(&g_uart1_handle, (uint8_t *)g_rx_buffer, RXBUFFERSIZE);
}
g_uart1_handle 是结构体UART_HandleTypeDef类型的全局变量,UART_HandleTypeDef结构体成员的含义请回到前面回顾。波特率我们直接赋值给g_uart1_handle.Init.BaudRate这个成员,可以看出很方便。需要注意的是,最后一行代码调用函数HAL_UART_Receive_IT,作用是开启接收中断,同时设置接收的缓存区以及接收的数据量。
上面的初始化函数只是串口初始化的其中一部分,我们还有一部分初始化需要HAL_UART_MspInit函数去完成。HAL_UART_MspInit是HAL库定义的弱定义函数,这里我们做重定义以实现我们的初始化需求。HAL_UART_MspInit函数在HAL_UART_Init函数中会被调用,其定义如下:
/**
* @brief UART底层初始化函数
* @param huart: UART句柄类型指针
* @note 此函数会被HAL_UART_Init()调用
* 完成时钟使能,引脚配置,中断配置
* @retval 无
*/
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
GPIO_InitTypeDef gpio_init_struct;
if(huart->Instance == USART1) /* 如果是串口1,进行串口1 MSP初始化 */
{
USART_UX_CLK_ENABLE(); /* USART1 时钟使能 */
USART_TX_GPIO_CLK_ENABLE(); /* 发送引脚时钟使能 */
USART_RX_GPIO_CLK_ENABLE(); /* 接收引脚时钟使能 */
gpio_init_struct.Pin = USART_TX_GPIO_PIN; /* TX引脚 */
gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推挽输出 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
gpio_init_struct.Alternate = USART_TX_GPIO_AF; /* 复用为USART1 */
HAL_GPIO_Init(USART_TX_GPIO_PORT, &gpio_init_struct); /* 初始化发送引脚 */
gpio_init_struct.Pin = USART_RX_GPIO_PIN; /* RX引脚 */
gpio_init_struct.Alternate = USART_RX_GPIO_AF; /* 复用为USART1 */
HAL_GPIO_Init(USART_RX_GPIO_PORT, &gpio_init_struct); /* 初始化接收引脚 */
#if USART_EN_RX
HAL_NVIC_EnableIRQ(USART_UX_IRQn); /* 使能USART1中断通道 */
HAL_NVIC_SetPriority(USART_UX_IRQn, 3, 3); /* 抢占优先级3,子优先级3 */
#endif
}
}
怎么来理解这个函数呢?该函数主要实现底层的初始化,事实上这个函数的代码还可以直接放到usart_init函数里面,但是HAL库为了代码的功能分层初始化,定义这个函数方便用户使用。所以我们也按照HAL库的这个结构来初始化外设。这个函数首先是调用if(huart->Instance == USART1)判断是要初始化那个串口,因为每个串口初始化都会调用HAL_UART_MspInit这个函数,所以需要判断是哪个串口要初始化才做相应的处理。只能说HAL库这样的结构机制有好处,自然也有坏处。
首先就是使能串口以及PA9和PA10的时钟,PA9和PA10需要用做复用功能,复用功能模式有两个选择:GPIO_MODE_AF_PP推挽式复用和GPIO_MODE_AF_OD开漏式复用,我们选择推挽式复用。选择了推挽式复用,我们还需要指定复用到哪个外设,通过赋值给Alternate这个结构体成员。然后就是调用HAL_GPIO_Init函数进行IO口的初始化。
最后因为我们用到串口中断,所以还需要中断相关的配置。HAL_NVIC_EnableIRQ函数使能串口1复用通道。HAL_NVIC_SetPriority函数配置串口中断的抢占优先级以及响应优先级。
串口初始化由上述两个函数完成,下面就该讲到串口中断服务函数了,其定义如下:
/**
* @brief 串口X中断服务函数
* @param 无
* @retval 无
*/
void USART_UX_IRQHandler(void)
{
#if SYS_SUPPORT_OS /* 使用OS */
OSIntEnter();
#endif
HAL_UART_IRQHandler(&g_uart1_handle); /* 调用HAL库中断处理公用函数 */
while (HAL_UART_Receive_IT(&g_uart1_handle, (uint8_t *)g_rx_buffer, RXBUFFERSIZE) != HAL_OK) /* 重新开启中断并接收数据 */
{
/* 如果出错会卡死在这里 */
}
#if SYS_SUPPORT_OS /* 使用OS */
OSIntExit();
#endif
}
从代码逻辑可以看出,在中断服务函数内部通过调用HAL_UART_GetState函数获取串口状态,计数处理时间是否超时,然后完成一次传输后,调用UART_Receive_IT函数重新开启中断。UART_Receive_IT函数的作用就是把每次中断接收到的字符保存在串口句柄的缓存指针pRxBuffPtr中,同时每次接收一个字符,其计数器RxXferCount减1,直到接收完成RxXferSize个字符,RxXferCount设置为0,同时调用接收回调函数HAL_UART_RxCpltCallback进行处理。
下面列出串口接收中断的一般流程,如图17.3.3.1所示:
图17.3.3.1 串口接收中断执行流程图
串口接收回调函数定义如下:
/**
* @brief Rx传输回调函数
* @param huart: UART句柄类型指针
* @retval 无
*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART_UX) /* 如果是串口1 */
{
if((g_usart_rx_sta & 0x8000) == 0) /* 接收未完成 */
{
if(g_usart_rx_sta & 0x4000) /* 接收到了0x0d(即回车键)*/
{
if(aRxBuffer[0] != 0x0a) /* 接收到的不是0x0a(即不是换行键)*/
{
g_usart_rx_sta = 0; /*接收错误,重新开始 */
}
else /* 接收到的是0x0a(即换行键)*/
{
g_usart_rx_sta |= 0x8000; /* 接收完成了 */
}
}
else /* 还没收到0X0D(即回车键) */
{
if(g_rx_buffer[0] == 0x0d)
{
g_usart_rx_sta |= 0x4000;
}
else
{
g_usart_rx_buf[g_usart_rx_sta & 0X3FFF] = g_rx_buffer[0] ;
g_usart_rx_sta++;
if(g_usart_rx_sta > (USART_REC_LEN - 1))
{
g_usart_rx_sta = 0; /* 接收数据错误,重新开始接收 */
}
}
}
}
}
}
因为我们设置了串口句柄成员变量RxXferSize为1,那么每当串口1接收到一个字符后触发接收完成中断,便会在中断服务函数中引导执行该回调函数。当串口接受到一个字符后,它会保存在缓存g_rx_buffer中,由于我们设置了缓存大小为1,而且RxXferSize=1,所以每次接受一个字符,会直接保存到RxXferSize[0]中,我们直接通过读取RxXferSize[0]的值就是本次接收到的字符。这里我们设计了一个小小的接收协议:通过这个函数,配合一个数组g_usart_rx_buf,一个接收状态寄存器g_usart_rx_sta(此寄存器其实就是一个全局变量,由作者自行添加。由于它起到类似寄存器的功能,这里暂且称之为寄存器)实现对串口数据的接收管理。数组g_usart_rx_buf的大小由USART_REC_LEN定义,也就是一次接收的数据最大不能超过USART_REC_LEN个字节。g_usart_rx_sta是一个接收状态寄存器其各的定义如表17.3.3.1所示:
g_usart_rx_sta
bit15 bit14 bit13~0
接收完成标志 接收到0X0D标志 接收到的有效字节个数
表17.3.3.1 接收状态寄存器位定义表
设计思路如下:
当接收到从电脑发过来的数据,把接收到的数据保存在数组g_usart_rx_buf中,同时在接收状态寄存器(g_usart_rx_sta)中计数接收到的有效数据个数,当收到回车(回车的表示由2个字节组成:0X0D和0X0A)的第一个字节0X0D时,计数器将不再增加,等待0X0A的到来,而如果0X0A没有来到,则认为这次接收失败,重新开始下一次接收。如果顺利接收到0X0A,则标记g_usart_rx_sta的第15位,这样完成一次接收,并等待该位被其他程序清除,从而开始下一次的接收,而如果迟迟没有收到0X0D,那么在接收数据超过USART_REC_LEN的时候,则会丢弃前面的数据,重新接收。
学到这里大家会发现,HAL库定义的串口中断逻辑确实非常复杂,并且因为处理过程繁琐所以效率不高。这里我们需要说明的是,在中断服务函数中,大家也可以不用调用HAL_UART_IRQHandler函数,而是直接编写自己的中断服务函数。串口实验我们之所以遵循HAL库写法, 是为了让大家对HAL库有一个更清晰的理解。
2. main.c代码
在main.c里面编写如下代码:
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
int main(void)
{
uint8_t t;
uint8_t len;
uint16_t times = 0;
sys_cache_enable(); /* 打开L1-Cache */
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(240, 2, 2, 4); /* 设置时钟, 480Mhz */
delay_init(480); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
while (1)
{
if (g_usart_rx_sta & 0x8000) /* 接收到了数据 */
{
len = g_usart_rx_sta & 0x3fff; /* 得到此次接收到的数据长度 */
printf("\r\n您发送的消息为:\r\n");
/*发送接收到的数据*/
HAL_UART_Transmit(&uartx_handler,(uint8_t*)g_usart_rx_buf,len,1000);
/*等待发送结束*/
while(__HAL_UART_GET_FLAG(&uartx_handler,UART_FLAG_TC)!=SET);
printf("\r\n\r\n"); /* 插入换行 */
g_usart_rx_sta = 0;
}
else
{
times++;
if (times % 5000 == 0)
{
printf("\r\n正点原子 Mini PRO STM32H750开发板 串口实验\r\n");
printf("正点原子@ALIENTEK\r\n\r\n\r\n");
}
if (times % 200 == 0) printf("请输入数据,以回车键结束\r\n");
if (times % 30 == 0) LED0_TOGGLE(); /* 闪烁LED,提示系统正在运行. */
delay_ms(10);
}
}
}
我们主要看无限循环里面的逻辑:首先判断全局变量g_usart_rx_sta的最高位是否为1,如果为1的话,那么代表前一次数据接收已经完成,接下来就是把我们自定义接收缓冲的数据发送到串口,在上位机显示。这里比较重点的两条语句是:第一条是调用HAL串口发送函数HAL_UART_Transmit来发送一段字符到串口。第二条是我们发送一个字节之后之后,要检测这个数据是否已经被发送完成了。如果全局变量g_usart_rx_sta的最高位为0,则执行一段时间往上位机发送提示字符,以及让LED0每隔一段时间翻转,提示系统正在运行。
17.4 下载验证
在下载好程序后,可以看到板子上的LED0开始闪烁,说明程序已经在跑了。串口调试助手,我们用XCOM V2.7,该软件在光盘有提供,且无需安装,直接可以运行,但是需要你的电脑安装有.NET Framework 4.0(WIN自带了)或以上版本的环境才可以,该软件的详细介绍请看:http://www.openedv.com/posts/list/22994.htm 这个帖子。
接着我们打开XCOM V2.7,设置串口为开发板的USB转串口(CH340虚拟串口,得根据你自己的电脑选择,我的电脑是COM8,另外,请注意:波特率是115200)。因为我们在程序上面设置了必须输入回车,串口才认可接收到的数据,所以必须在发送数据后再发送一个回车符,这里XCOM提供的发送方法是通过勾选发送新行实现,只要勾选了这个选项,每次发送数据后,XCOM都会自动多发一个回车(0X0D+0X0A)。设置好了发送新行,我们再在发送区输入你想要发送的文字,然后单击发送,可以看到如图17.4.1所示信息:
可以看到,我们发送的消息被发送回来了。大家可以试试,如果不发送回车(取消发送新行),在输入内容之后,直接按发送是什么结果,大家测试一下吧。