文章目录
- 一、串行通信基本原理
- 1.串行通信接口背景知识
- 2.异步串口通信UART知识
- 3.STM32串口数据格式和通信过程
- 4.STM32串口框图
- 5.波特率计算方法
- 二、STM32F429 串口简介
- 三、硬件设计
- 四、软件设计
- 五、实验现象
- 六、STM32CubeMX 配置串口
本章介绍如何使用
STM32F429
的串口来发送和接收数据。本章将实现如下功能:
STM32F429
通过串口和上位机的对话,
STM32F429
在收到上位机发过来的字符串后,原原本本的返回给上位机。
一、串行通信基本原理
1.串行通信接口背景知识
处理器与外部设备通信有两种方式:
- 并行通信
- 传输原理:数据各个位同时传输。
- 优点:速度快
- 缺点:占用引脚资源多
- 串行通信
- 传输原理:数据按位顺序传输。
- 优点:占用引脚资源少
- 缺点:速度相对较慢
串行通信按照数据传送方向,分为:
- 单工
数据传输只支持数据在一个方向上传输 - 半双工
允许数据在两个方向上传输,但是,在某一时刻,只允许数据在一个方向上传输,它实际上是一种切换方向的单工通信 - 全双工
允许数据同时在两个方向上传输,因此,全双工通信是两个单工通信方式的结合,它要求发送设备和接收设备都有独立的接收和发送能力。
串行通信的三种传送方式如下:
串行通信按照通信方式来分
- 同步通信:带时钟同步信号传输
SPI
,IIC
通信接口
- 异步通信:不带时钟同步信号
UART
(通用异步收发器),单总线
常见的串行通信接口为
2.异步串口通信UART知识
异步通信UART
包含三点知识:
- 物理层(电气层:接口决定):通信接口(
RS232
,RS485
,RS422
,TTL
) - 数据格式(数据层:芯片决定)
- 通信协议(协议层:程序决定)
UART
异步通信方式引脚连接方法:
RXD
:数据输入引脚。数据接受。TXD
:数据发送引脚。数据发送。
接口类型如下:
STM32 UART
异步通信方式引脚:
查看芯片数据手册引脚功能表可得:
PA9
与PA10
可做串口1的发送和接受引脚;PB10
和PB11
可做串口3的发送和接受引脚。
3.STM32串口数据格式和通信过程
STM32
串口异步通信需要定义的参数:
- 起始位:1个逻辑0数据位开始
- 数据位(8位或者9位)
- 奇偶校验位(第9位)
- 停止位(1,1.5,2位)
- 波特率设置
STM32
串口通信过程如下:
4.STM32串口框图
由图中可以看出:
- 发送端与接收端共用一个波特率,即确定串行通信的速度
USART_BRR
为分数波特率发生器
5.波特率计算方法
波特率计算在串口框图如下部分:
OVER8
由控制寄存器USART_CR1
中的位15来控制。
波特率的计算公式如下:
OVER8=0
时波特率计算公式:
T
x
/
R
x
波
特
率
=
f
P
C
L
K
x
16
∗
U
S
A
R
T
D
I
V
Tx/Rx波特率=\frac{f_{PCLKx}}{16*USARTDIV}
Tx/Rx波特率=16∗USARTDIVfPCLKx
- 根据波特率和串口时钟频率,计算出
USARTDIV
的值。 DIV_Fraction=USART的小数部分 X16所得的整数
DIV_Mantissa=USART的整数部分
那么,假如串口时钟为90M,需要得到115200的波特率;则
二、STM32F429 串口简介
STM32F429
的串口资源相当丰富的,功能也相当强劲。ALIENTEK
阿波罗 STM32F429
开发板所使用的 STM32F429IGT6
最多可提供 8 路串口,有分数波特率发生器、支持同步单线通信和半双工单线通讯、支持 LIN
、支持调制解调器操作、智能卡协议和 IrDA SIR ENDEC
规范、具有 DMA
等。
阿波罗 STM32F429
开发板板载了 1 个 USB
串口和 2 个 RS232
串口,我们本章介绍的是通过 USB
串口和电脑通信。
串口最基本的设置,就是波特率的设置。STM32F429
的串口使用起来还是蛮简单的,只要你开启了串口时钟,并设置相应 IO
口的模式,然后配置一下波特率,数据位长度,奇偶校验位等信息,就可以使用了。下面介绍几个与串口基本配置直接相关的寄存器。
-
串口时钟使能。串口作为
STM32F429
的一个外设,其时钟由外设时钟使能寄存器控制,这里我们使用的串口 1 是在APB2ENR
寄存器的第 4 位。APB2ENR
寄存器在之前已经介绍过了,这里不再介绍。只是说明一点,就是除了串口 1 和串口 6 的时钟使能在APB2ENR
寄存器,其他串口的时钟使能位都在APB1ENR
寄存器。 -
串口波特率设置。每个串口都有一个自己独立的波特率寄存器
USART_BRR
,通过设置该寄存器就可以达到配置不同波特率的目的。 -
串口控制。
STM32F429
的每个串口都有 3 个控制寄存器USART_CR1~3
,串口的很多配置都是通过这 3 个寄存器来设置的。这里我们只要用到USART_CR1
就可以实现我们的功能了,该寄存器的各位描述如图所示:
该寄存器的高16位没有用到,低16位用于串口的功能设置。OVER8
为过采样模式设置位,我们一般设置位 0,即 16 倍过采样已获得更好的容错性;UE
为串口使能位,通过该位置 1,以使能串口;M
为字长选择位,当该位为 0 的时候设置串口为 8 个字长外加 n 个停止位,停止位的个数(n
)是根据USART_CR2
的[13:12]
位设置来决定的,默认为 0;PCE
为校验使能位,设置为 0,则禁止校验,否则使能校验;PS
为校验位选择位,设置为 0 则为偶校验,否则为奇校验;TXEIE
为发送缓冲区空中断使能位,设置该位为 1,当USART_SR
中的TXE
位为 1 时,将产生串口中断;TCIE
为发送完成中断使能位,设置该位为 1,当USART_SR
中的TC
位为 1 时,将产生串口中断;RXNEIE
为接收缓冲区非空中断使能,设置该位为 1,当USART_SR
中的ORE
或者RXNE
位为 1 时,将产生串口中断;TE
为发送使能位,设置为 1,将开启串口的发送功能;RE
为接收使能位,用法同 TE。
-
数据发送与接收。
STM32F429
的发送与接收是通过数据寄存器USART_DR
来实现的,这是一个双寄存器,包含了TDR
和RDR
。当向DR
寄存器写数据的时候,实际是写入TDR
,串口就会自动发送数据;当收到数据,读DR
寄存器的时候,实际读取的是RDR
。TDR
和RDR
对外是不可见的,所以,我们操作的就只有DR
寄存器,该寄存器的各位描述如图所示:
可以看出,虽然是一个 32 位寄存器,但是只用了低 9 位(DR[8:0]
),其他都是保留。DR[8:0]
为串口数据,包含了发送或接收的数据。由于它是由两个寄存器(TDR
和RDR
)组成的,一个给发送用(TDR
),一个给接收用(RDR
),该寄存器兼具读和写的功能。TDR
寄存器提供了内部总线和输出移位寄存器之间的并行接口。RDR
寄存器提供了输入移位寄存器和内部总线之间的并行接口。当使能校验位(USART_CR1
中PCE
位被置位)进行发送时,写到MSB
的值(根据数据的长度不同,MSB
是第 7 位或者第 8 位)会被后来的校验位取代。当使能校验位进行接收时,读到的MSB
位是接收到的校验位。 -
串口状态。串口的状态可以通过状态寄存器
USART_SR
读取。USART_SR
的各位描述如图所示:
这里我们关注一下两个位,第 5、6 位RXNE
和TC
。RXNE
(读数据寄存器非空),当该位被置 1 的时候,就是提示已经有数据被接收到了,并且可以读出来了。这时候我们要做的就是尽快去读取USART_DR
,通过读USART_DR
可以将该位清零,也可以向该位写 0,直接清除。TC
(发送完成),当该位被置位的时候,表示USART_DR
内的数据已经被发送完成了。如果设置了这个位的中断,则会产生中断。该位也有两种清零方式:- 读
USART_SR
,写USART_DR
- 直接向该位写 0。
- 读
通过以上一些寄存器的操作外加
IO
口的配置,我们就可以达到串口最基本的配置了。
串口字节的发送流程如下:
- 编程
USARTx_CR1
的M
位来定义字长。 - 编程
USARTx_CR2
的STOP
位来定义停止位位数。 - 编程
USARTx_BRR
寄存器确定波特率。 - 使能
USARTx_CR1
的UE
位使能USARTX
。 - 如果进行多缓冲通信,配置
USARTx_CR3
的DMA
使能DMAT
。 - 使能
USARTx_CR1
的TE
位使能发送器。 - 向发送数据寄存器
TDR
写入要发送的数据(对于M3
,发送和接收共用DR
寄存器)。 - 向
TRD
寄存器写入最后一个数据后,等待状态寄存器USARTx_SR(ISR)
的TC
位置1,传输完成。
上面是基于寄存器进行描述的。接下来,使用HAL
库实现串口发送程序配置:
- 初始化串口相关参数使能串口:
HAL UART Init();
- 串口相关
IO
口配置,复用配置:在HAL_UART_MspInit
中调用HAL GPIO Init
函数。 - 发送数据并等待数据发送完成:
HAL_UART_Transmit()
函数;
串口接收流程如下:
- 编程
USARTx_CR1
的M
位来定义字长。 - 编程
USARTx_CR2
的STOP
位来定义停止位位数。 - 编程
USARTx_BRR
寄存器确定波特率。 - 使能
USARTx_CR1
的UE
位使能USARTX
。 - 如果进行多缓冲通信,配置
USARTx_CR3
的DMA
使能DMAT
。 - 使能
USARTx_CR1
的RE
位为1使能接收器。 - 如果要使能接收中断(接收到数据后产生中断),使能
USARTx_CR1
的RXNEIE
位为1。
当串口接收到数据时:
USARTx_SR(ISR)
的RXNE
位置1。表明移位寄存器内容已经传输到RDR(DR)
寄存器。已经接收到数据并且等待读取。- 如果开启了接收数据中断(
USARTx_CR1
寄存器的RXNEIE
位为1),则会产生中断。(程序上会执行中断服务函数) - 如果开启了其他中断(帧错误等),相应标志位会置1。
- 读取
USARTx_RDR(DR)
寄存器的值,该操作会自动将RXNE
位清零,等待下次接收后置位。
接收数据过程:
-
步骤1获取状态标志位通过标识符实现:
__HAL_UART_GET_FLAG //判断状态标志位 __HAL_UART_GET_IT_SOURCE //判断中断标志位
-
步骤2-3中断服务函数:
void USARTx_IRQHandler(void) ;//(x=1~3,6) void USARTx_IRQHandler(void) ;//(x=4,5,7,8)
-
步骤4读取接收数据
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
接下来,使用HAL
库实现串口接收中断程序配置:
-
初始化串口相关参数,使能串口:
HAL_UART_Init();
-
串口相关
IO
口配置,复用配置:在HAL_UART_MspInit
中调用HAL_GPIO_Init
函数。 -
串口接收中断优先级配置和使能:
HAL_NVIC_EnableIRQ(); HAL_NVIC_SetPriority();
-
使能串口接收中断:
HAL_UART_Receive_IT();
-
编写中断服务函数:
USARTx_IRQHandler
接下来使用 HAL
库实现串口配置和使用的方法。在 HAL
库中,串口相关的函数和定义主要在文件 stm32f4xx_hal_uart.c
和 stm32f4xx_hal_uart.h
中。接下来我们看看 HAL
库提供的串口相关操作函数。
-
串口参数初始化(波特率/停止位等),并使能串口。
串口作为STM32
的一个外设,HAL
库为其配置了串口初始化函数。串口初始化函数HAL_UART_Init
定义如下:HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart);
该函数只有一个入口参数
huart
,为UART_HandleTypeDef
结构体指针类型。一般情况下,我们会定义一个UART_HandleTypeDef
结构体类型全局变量,然后初始化各个成员变量。结构体UART_HandleTypeDef
的定义如下:typedef struct { USART_TypeDef *Instance; UART_InitTypeDef Init; uint8_t *pTxBuffPtr; uint16_t TxXferSize; uint16_t TxXferCount; uint8_t *pRxBuffPtr; uint16_t RxXferSize; uint16_t RxXferCount; DMA_HandleTypeDef *hdmatx; DMA_HandleTypeDef *hdmarx; HAL_LockTypeDef Lock; __IO HAL_UART_StateTypeDef State; __IO uint32_t ErrorCode; }UART_HandleTypeDef;
该结构体成员变量非常多,一般情况调用函数
HAL_UART_Init
对串口进行初始化的时候,我们只需要先设置Instance
和Init
两个成员变量的值。-
Instance
是USART_TypeDef
结构体指针类型变量,它是执行寄存器基地址,实际上这个基地址HAL
库已经定义好了,如果是串口 1,取值为USART1
即可。 -
Init
是UART_InitTypeDef
结构体类型变量,它是用来设置串口的各个参数,包括波特率,停止位等。UART_InitTypeDef
结构体定义如下:typedef struct { uint32_t BaudRate; //波特率 uint32_t WordLength; //字长 uint32_t StopBits; //停止位 uint32_t Parity; //奇偶校验 uint32_t Mode; //收/发模式设置 uint32_t HwFlowCtl; //硬件流设置 uint32_t OverSampling; //过采样设置 }UART_InitTypeDef
- 第一个参数
BaudRate
为串口波特率,波特率可以说是串口最重要的参数了,它用来确定串口通信的速率。 - 第二个参数
WordLength
为字长,可以设置为 8 位字长或者 9 位字长,这里我们设置为 8 位字长数据格式UART_WORDLENGTH_8B
。 - 第三个参数
StopBits
为停止位设置,可以设置为 1 个停止位或者 2 个停止位,这里我们设置为 1 位停止位UART_STOPBITS_1
。 - 第四个参数
Parity
设定是否需要奇偶校验,我们设定为无奇偶校验位。 - 第五个参数
Mode
为串口模式,可以设置为只收模式,只发模式,或者收发模式。这里我们设置为全双工收发模式。 - 第六个参数
HwFlowCtl
为是否支持硬件流控制,我们设置为无硬件流控制。 - 第七个参数
OverSampling
用来设置过采样为 16 倍还是 8 倍。
- 第一个参数
pTxBuffPtr
,TxXferSize
和TxXferCount
三个变量分别用来设置串口发送的数据缓存指针,发送的数据量和还剩余的要发送的数据量。而接下来的三个变量pRxBuffPtr
,RxXferSize
和RxXferCount
则是用来设置接收的数据缓存指针,接收的最大数据量以及还剩余的要接收的数据量。这六个变量是HAL
库处理中间变量。hdmatx
和hdmarx
是串口DMA
相关的变量,指向DMA
句柄。其他的三个变量就是一些HAL
库处理过程状态标志位和串口通信的错误码。
函数HAL_UART_Init
使用的一般格式为:UART_HandleTypeDef UART1_Handler; //UART 句柄 UART1_Handler.Instance=USART1; //USART1 UART1_Handler.Init.BaudRate=115200; //波特率 UART1_Handler.Init.WordLength=UART_WORDLENGTH_8B; //字长为 8 位格式 UART1_Handler.Init.StopBits=UART_STOPBITS_1; //一个停止位 UART1_Handler.Init.Parity=UART_PARITY_NONE; //无奇偶校验位 UART1_Handler.Init.HwFlowCtl=UART_HWCONTROL_NONE; //无硬件流控 UART1_Handler.Init.Mode=UART_MODE_TX_RX; //收发模式 HAL_UART_Init(&UART1_Handler); //HAL_UART_Init()会使能 UART1
这里我们需要说明的是,函数
HAL_UART_Init
内部会调用串口使能函数使能相应串口,所以调用了该函数之后我们就不需要重复使能串口了。当然,HAL
库也提供了具体的串口使能和关闭方法,具体使用方法如下:__HAL_UART_ENABLE(handler); //使能句柄 handler 指定的串口 __HAL_UART_DISABLE(handler); //关闭句柄 handler 指定的串口
串口作为一个重要外设,在调用的初始化函数
HAL_UART_Init
内部,会先调用MSP
初始化回调函数进行MCU
相关的初始化,函数为:void HAL_UART_MspInit(UART_HandleTypeDef *huart);
我们在程序中,只需要重写该函数(
__weak
弱函数,可重写)即可。一般情况下,该函数内部用来编写IO
口初始化,时钟使能以及NVIC
配置。重写后的函数如下://UART底层初始化,时钟使能,引脚配置,中断配置 //此函数会被HAL_UART_Init()调用 //huart:串口句柄 void HAL_UART_MspInit(UART_HandleTypeDef *huart) { //GPIO端口设置 GPIO_InitTypeDef GPIO_Initure; if(huart->Instance==USART1)//如果是串口1,进行串口1 MSP初始化 { __HAL_RCC_GPIOA_CLK_ENABLE(); //使能GPIOA时钟 __HAL_RCC_USART1_CLK_ENABLE(); //使能USART1时钟 GPIO_Initure.Pin=GPIO_PIN_9; //PA9 GPIO_Initure.Mode=GPIO_MODE_AF_PP; //复用推挽输出 GPIO_Initure.Pull=GPIO_PULLUP; //上拉 GPIO_Initure.Speed=GPIO_SPEED_FAST; //高速 GPIO_Initure.Alternate=GPIO_AF7_USART1; //复用为USART1 HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化PA9 GPIO_Initure.Pin=GPIO_PIN_10; //PA10 HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化PA10 #if EN_USART1_RX HAL_NVIC_EnableIRQ(USART1_IRQn); //使能USART1中断通道 HAL_NVIC_SetPriority(USART1_IRQn,3,3); //抢占优先级3,子优先级3 #endif } }
-
-
使能串口和 GPIO 口时钟
我们要使用串口,所以我们必须使能串口时钟和使用到的GPIO
口时钟。例如我们要使用串口 1,所以我们必须使能串口 1 时钟和GPIOA
时钟(串口 1 使用的是PA9
和PA10
)。具体方法如下:__HAL_RCC_USART1_CLK_ENABLE(); //使能 USART1 时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); //使能 GPIOA 时钟
-
GPIO 口初始化设置(速度,上下拉等)以及复用映射配置
我们要复用PA9
和PA10
为串口发送接收相关引脚,我们需要配置IO
口为复用,同时复用映射到串口 1。配置源码如下:GPIO_InitTypeDef GPIO_Initure; GPIO_Initure.Pin=GPIO_PIN_9|GPIO_PIN_10; //PA9/PA10 GPIO_Initure.Mode=GPIO_MODE_AF_PP; //复用推挽输出 GPIO_Initure.Pull=GPIO_PULLUP; //上拉 GPIO_Initure.Speed=GPIO_SPEED_FAST; //高速 GPIO_Initure.Alternate=GPIO_AF7_USART1; //复用为 USART1 HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 PA9/PA10
-
开启串口相关中断,配置串口中断优先级
HAL
库中定义了一个使能串口中断的标识符__HAL_UART_ENABLE_IT
,大家可以把它当一个函数来使用,具体定义请参考HAL
库文件stm32f4xx_hal_uart.h
中该标识符定义。例如我们要使能接收完成中断,方法如下:__HAL_UART_ENABLE_IT(huart,UART_IT_RXNE); //开启接收完成中断
- 第一个参数为串口句柄,类型为
UART_HandleTypeDef
结构体类型。 - 第二个参数为我们要开启的中断类型值,可选值在头文件
stm32f4xx_hal_uart.h
中有宏定义。
有开启中断就有关闭中断,操作方法为:
__HAL_UART_DISABLE_IT(huart,UART_IT_RXNE); //关闭接收完成中断
对于中断优先级配置,方法就非常简单。参考方法为:
HAL_NVIC_EnableIRQ(USART1_IRQn); //使能 USART1 中断通道 HAL_NVIC_SetPriority(USART1_IRQn,3,3); //抢占优先级 3,子优先级 3
- 第一个参数为串口句柄,类型为
-
编写中断函数
串口 1 中断服务函数为://串口1中断服务程序 void USART1_IRQHandler(void) { u32 timeout=0; u32 maxDelay=0x1FFFF; #if SYSTEM_SUPPORT_OS //使用OS OSIntEnter(); #endif HAL_UART_IRQHandler(&UART1_Handler); //调用HAL库中断处理公用函数 timeout=0; while (HAL_UART_GetState(&UART1_Handler) != HAL_UART_STATE_READY)//等待就绪 { timeout++;超时处理 if(timeout>maxDelay) break; } timeout=0; while(HAL_UART_Receive_IT(&UART1_Handler, (u8 *)aRxBuffer, RXBUFFERSIZE) != HAL_OK)//一次处理完成之后,重新开启中断并设置RxXferCount为1 { timeout++; //超时处理 if(timeout>maxDelay) break; } #if SYSTEM_SUPPORT_OS //使用OS OSIntExit(); #endif }
当发生中断的时候,程序就会执行中断服务函数。然后我们在中断服务函数中编写们相应的逻辑代码即可。
HAL
库实际上对中断处理过程进行了完整的封装。 -
串口数据接收和发送
STM32F4
的发送与接收是通过数据寄存器USART_DR
来实现的,这是一个双寄存器,包含了TDR
和RDR
。当向该寄存器写数据的时候,串口就会自动发送,当收到数据的时候,也是存在该寄存器内。HAL
库操作USART_DR
寄存器发送数据的函数是:HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
通过该函数向串口寄存器
USART_DR
写入一个数据。
HAL
库操作USART_DR
寄存器读取串口接收到的数据的函数是HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
通过该函数可以读取串口接受到的数据。
三、硬件设计
本实验需要用到的硬件资源有:
- 指示灯
DS0
- 串口 1
本实验用到的串口 1 与 USB
串口并没有在 PCB
上连接在一起,需要通过跳线帽来连接一下。这里我们把 P4
的 RXD
和 TXD
用跳线帽与 PA9
和 PA10
连接起来。如图所示:
连接上这里之后,我们在硬件上就设置完成了,可以开始软件设计了。
四、软件设计
ALIENTEK
编写的串口相关的源码在 SYSTEM
分组之下的 usart.c
和 usart.h
中。第二章讲解了 HAL
库中串口操作的一般步骤以及操作函数。在使用 HAL
库配置串口的时候,HAL
库为我们封装了串口配置步骤。接下来,我们以串口接收中断为例讲解 HAL
库串口程序执行流程。
和其他外设一样,HAL
库为串口的使用开放了 MSP
函数。在串口初始化函数HAL_UART_Init
内部,会调用串口 MSP
函数 HAL_UART_MspInit
来设置与 MCU
相关的配置。根据前面的讲解,函数 HAL_UART_Init
主要用来初始化与串口相关的参数(这些参数与 MCU
无关),包括波特率,停止位等。而串口 MSP
函数HAL_UART_MspInit
用来设置 GPIO
初始化,NVIC
配置等于 MCU
相关的配置。这里我们定义了一个函数 uart_init
用来调用 HAL_UART_Init
初始化串口参数配置,具体函数如下:
UART_HandleTypeDef UART1_Handler; //UART 句柄
//初始化 IO 串口 1 bound:波特率
void uart_init(u32 bound)
{
//UART 初始化设置
UART1_Handler.Instance=USART1; //USART1
UART1_Handler.Init.BaudRate=bound; //波特率
UART1_Handler.Init.WordLength=UART_WORDLENGTH_8B; //字长为 8 位格式
UART1_Handler.Init.StopBits=UART_STOPBITS_1; //一个停止位
UART1_Handler.Init.Parity=UART_PARITY_NONE; //无奇偶校验位
UART1_Handler.Init.HwFlowCtl=UART_HWCONTROL_NONE; //无硬件流控
UART1_Handler.Init.Mode=UART_MODE_TX_RX; //收发模式
HAL_UART_Init(&UART1_Handler); //HAL_UART_Init()会使能 UART1
HAL_UART_Receive_IT(&UART1_Handler, (u8 *)aRxBuffer, 1);
//该函数会开启接收中断并且设置接收缓冲以及接收缓冲接收最大数据量
}
该函数实现的是第二章讲解的步骤1的内容。步骤1:串口参数初始化(波特率/停止位等),并使能串口。
串口 MSP
函数 HAL_UART_MspInit
函数代码如下:
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
//GPIO 端口设置
GPIO_InitTypeDef GPIO_Initure;
if(huart->Instance==USART1) //如果是串口 1,进行串口 1 MSP 初始化
{
__HAL_RCC_GPIOA_CLK_ENABLE(); //使能 GPIOA 时钟
__HAL_RCC_USART1_CLK_ENABLE(); //使能 USART1 时钟
GPIO_Initure.Pin=GPIO_PIN_9; //PA9
GPIO_Initure.Mode=GPIO_MODE_AF_PP; //复用推挽输出
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_FAST; //高速
GPIO_Initure.Alternate=GPIO_AF7_USART1; //复用为 USART1
HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 PA9
GPIO_Initure.Pin=GPIO_PIN_10; //PA10
HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 PA10
#if EN_USART1_RX
HAL_NVIC_EnableIRQ(USART1_IRQn); //使能 USART1 中断通道
HAL_NVIC_SetPriority(USART1_IRQn,3,3); //抢占优先级 3,子优先级 3
#endif
}
}
该函数代码实现的是步骤2到4的内容。
步骤2:使能串口和 GPIO 口时钟
步骤3:GPIO 口初始化设置(速度,上下拉等)以及复用映射配置
步骤4:开启串口相关中断,配置串口中断优先级
在该段代码中,通过判断宏定义标识符EN_USART1_RX
的值来确定是否开启串口中断通道和设置串口 1 中断优先级。标识符 EN_USART1_RX
在头文件 usart.h
中有定义,默认情况下我们设置为1。
#define EN_USART1_RX 1 //使能(1)/禁止(0)串口 1 接收
通过上面两个函数,我们就配置了串口相关设置。接下来就是步骤5:编写中断服务函数USART1_IRQHandler
。
首先,HAL
库定义了一个串口中断处理通用函数 HAL_UART_IRQHandler
,该函数声明如下:
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart);
该函数只有一个入口参数就是 UART_HandleTypeDef
结构体指针类型的串口句柄 huart
,使用我们在调用 HAL_UART_Init
函数时设置的同一个变量即可。该函数一般在中断服务函数中调用,作为串口中断处理的通用入口。一般调用方法为:
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&UART1_Handler); //调用 HAL 库中断处理公用函数
…//中断处理完成后的结束工作
}
也就是说,真正的串口中断处理逻辑我们会最终在函数 HAL_UART_IRQHandler
内部执行。而该函数是 HAL
库已经定义好,而且用户一般不能随意修改。那么我们的中断控制逻辑编写在哪里呢?为了把这个问题理解清楚,我们要来看看函数HAL_UART_IRQHandler
内部具体实现过程。因为本章实验,我们主要实现的是串口中断接收,也就是每次接收到一个字符后进入中断服务函数来处理。所以我们就以中断接收为例来分析。这里为了篇幅考虑,我们仅仅列出串口中断执行流程中与接收相关的源码。
函数 HAL_UART_IRQHandler
关于串口接收相关源码如下:
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
{
uint32_t tmp1 = 0, tmp2 = 0;
…//此处省略部分代码
tmp1 = __HAL_UART_GET_FLAG(huart, UART_FLAG_RXNE);
tmp2 = __HAL_UART_GET_IT_SOURCE(huart, UART_IT_RXNE);
if((tmp1 != RESET) && (tmp2 != RESET))
{
UART_Receive_IT(huart);
}
…//此处省略部分代码
}
从代码逻辑可以看出,在函数HAL_UART_IRQHandler
内部通过判断中断类型是否为接收完成中断,确定是否调用 HAL
另外一个函数 UART_Receive_IT()
。函数 UART_Receive_IT()
的作用是把每次中断接收到的字符保存在串口句柄的缓存指针 pRxBuffPtr
中,同时每次接收一个字符,其计数器 RxXferCount
减 1,直到接收完成 RxXferSize
个字符之后 RxXferCount
设置为0,同时调用接收完成回调函数 HAL_UART_RxCpltCallback
进行处理。为了篇幅考虑,这里我们仅列出 UART_Receive_IT()
函数调用回调函数 HAL_UART_RxCpltCallback
的处理逻辑,代码如下:
static HAL_StatusTypeDef UART_Receive_IT(UART_HandleTypeDef *huart)
{
...//此处省略部分代码
if(--huart->RxXferCount == 0)
{
HAL_UART_RxCpltCallback(huart);
}
...//此处省略部分代码
}
最后,我们列出串口接收中断的一般流程,如图所示:
这里,我们把串口接收中断的一般流程进行概括:当接收到一个字符之后,在函数UART_Receive_IT
中会把数据保存在串口句柄的成员变量pRxBuffPtr
缓存中,同时RxXferCount
计数器减 1。如果我们设置RxXferSize=10
,那么当接收到 10
个字符之后,RxXferCount
会由 10 减到 0(RxXferCount
初始值等于RxXferSize
),这个时候再调用接收完成回调函数HAL_UART_RxCpltCallback
进行处理。接下来,我们看看我们的配置。
首先,我们回到用户函数 uart_init
定义可以看到,在 uart_init
函数中调用完 HAL_UART_Init
后我们还调用了 HAL_UART_Receive_IT
开启接收中断,并且初始化串口句柄的缓存相关参数。代码如下:
HAL_UART_Receive_IT(&UART1_Handler, (u8 *)aRxBuffer, RXBUFFERSIZE);
而 aRxBuffer
是我们定义的一个全局数组变量,RXBUFFERSIZE
是我们定义的一个标识符:
#define RXBUFFERSIZE 1
u8 aRxBuffer[RXBUFFERSIZE];
所以,调用 HAL_UART_Receive_IT
函数后,除了开启接收中断外还确定了每次接收RXBUFFERSIZE
个字符后标示接收结束从而进入回调函数HAL_UART_RxCpltCallback
进行相应处理。最后,我们看看 HAL_UART_RxCpltCallback
函数定义:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance==USART1)//如果是串口 1
if((USART_RX_STA&0x8000)==0)//接收未完成
{
if(USART_RX_STA&0x4000)//接收到了 0x0d
{
if(aRxBuffer[0]!=0x0a)USART_RX_STA=0;//接收错误,重新开始
else USART_RX_STA|=0x8000; //接收完成了
}
else //还没收到 0X0D
{
if(aRxBuffer[0]==0x0d)USART_RX_STA|=0x4000;
else
{
USART_RX_BUF[USART_RX_STA&0X3FFF]=aRxBuffer[0] ;
USART_RX_STA++;
if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;
//接收数据错误,重新开始接收
}
}
}
}
}
因为我们设置了串口句柄成员变量 RxXferSize
为 1,也就是每当串口 1 发生了接收完成中断后(接收到一个字符),就会跳到该函数执行。当串口接受到一个字符后,它会保存在缓存aRxBuffer
中,由于我们设置了缓存大小为 1,而且 RxXferSize=1
,所以每次接受一个字符,会直接保存到 aRxBuffer[0]
中,我们直接通过读取 aRxBuffer[0]
的值就是本次接收到的字符。这里我们设计了一个小小的接收协议:通过这个函数,配合一个数组 USART_RX_BUF[]
,一个接收状态寄存器 USART_RX_STA
实现对串口数据的接收管理。USART_RX_BUF
的大小由 USART_REC_LEN
定义,也就是一次接收的数据最大不能超过USART_REC_LEN
个字节。USART_RX_STA
是一个接收状态寄存器其定义如表所示:
设计思路:当接收到从电脑发过来的数据,把接收到的数据保存在 USART_RX_BUF
中,同时在接收状态寄存器(USART_RX_STA
)中计数接收到的有效数据个数,当收到回车(回车的表示由 2 个字节组成:0X0D
和 0X0A
)的第一个字节 0X0D
时,计数器将不再增加,等待 0X0A
的到来,而如果 0X0A
没有来到,则认为这次接收失败,重新开始下一次接收。如果顺利接收到 0X0A
,则标记 USART_RX_STA
的第 15 位,这样完成一次接收,并等待该位被其他程序清除,从而开始下一次的接收,而如果迟迟没有收到0X0D
,那么,在接收数据超过USART_REC_LEN
的时候,则会丢弃前面的数据,重新接收。
在函数 USART1_IRQHandler
的结尾还有几行行代码,其中部分代码是超时退出逻辑,关键逻辑代码如下:
while (HAL_UART_GetState(&UART1_Handler) != HAL_UART_STATE_READY);
while(HAL_UART_Receive_IT(&UART1_Handler, (u8 *)aRxBuffer, 1) != HAL_OK);
这两行代码作用非常简单。
- 第一行代码是判断串口是否就绪,如果没有就绪就等待就绪。
- 第二行代码是继续调用
HAL_UART_Receive_IT
函数来开启中断和重新设置RxXferSize
和RxXferCount
的初始值为 1,也就是开启新的接收中断。
这里我们需要说明的是,在中断服务函数中,大家也可以不用调用HAL_UART_IRQHandler
函数,而是直接编写自己的中断服务函数。
如果我们不用中断处理回调函数,那么就不用初始化串口句柄的中断接收缓存,所以,HAL_UART_Receive_IT
函数就不用出现在初始化函数 uart_init
中,而是直接在要开启中断的地方通过调用__HAL_UART_ENABLE_IT
单独开启中断即可。如果不用中断回调函数处理,中断服务函数内容为:
//串口1中断服务程序
void USART1_IRQHandler(void)
{
u8 Res;
#if SYSTEM_SUPPORT_OS //使用OS
OSIntEnter();
#endif
if((__HAL_UART_GET_FLAG(&UART1_Handler,UART_FLAG_RXNE)!=RESET)) //接收中断(接收到的数据必须是0x0d 0x0a结尾)
{
HAL_UART_Receive(&UART1_Handler,&Res,1,1000);
if((USART_RX_STA&0x8000)==0)//接收未完成
{
if(USART_RX_STA&0x4000)//接收到了0x0d
{
if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始
else USART_RX_STA|=0x8000; //接收完成了
}
else //还没收到0X0D
{
if(Res==0x0d)USART_RX_STA|=0x4000;
else
{
USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;
USART_RX_STA++;
if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收
}
}
}
}
HAL_UART_IRQHandler(&UART1_Handler);
#if SYSTEM_SUPPORT_OS //使用OS
OSIntExit();
#endif
}
这段代码逻辑跟上面的中断回调函数类似,只不过这里还需要通过 HAL
库串口接收函数HAL_UART_Receive
来获取接收到的字符进行相应的处理。
HAL 库一共提供了 5 个中断处理回调函数:
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);//发送完成回调函数
void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart);//发送完成过半
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);//接收完成回调函数
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);//接收完成过半
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart);//错误处理回调函数
主函数代码为:
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "key.h"
int main(void)
{
u8 len;
u16 times=0;
HAL_Init(); //初始化HAL库
Stm32_Clock_Init(360,25,2,8); //设置时钟,180Mhz
delay_init(180); //初始化延时函数
uart_init(115200); //初始化USART
LED_Init(); //初始化LED
KEY_Init(); //初始化按键
while(1)
{
if(USART_RX_STA&0x8000)
{
len=USART_RX_STA&0x3fff;//得到此次接收到的数据长度
printf("\r\n您发送的消息为:\r\n");
HAL_UART_Transmit(&UART1_Handler,(uint8_t*)USART_RX_BUF,len,1000); //发送接收到的数据
while(__HAL_UART_GET_FLAG(&UART1_Handler,UART_FLAG_TC)!=SET); //等待发送结束
printf("\r\n\r\n");//插入换行
USART_RX_STA=0;
}else
{
times++;
if(times%5000==0)
{
printf("\r\nALIENTEK 阿波罗STM32F429开发板 串口实验\r\n");
printf("正点原子@ALIENTEK\r\n\r\n\r\n");
}
if(times%200==0)printf("请输入数据,以回车键结束\r\n");
if(times%30==0)LED0=!LED0;//闪烁LED,提示系统正在运行.
delay_ms(10);
}
}
}
这段代码首先判断全局变量 USART_RX_STA
的最高位是否为 1,如果为 1 的话,那么代表前一次数据接收已经完成,接下来就是把我们自定义接收缓冲的数据发送到串口。
HAL_UART_Transmit(&UART1_Handler,(uint8_t*)USART_RX_BUF,len,1000);
这一行代码就是调用 HAL
串口发送函数HAL_UART_Transmit
来发送一个字符到串口。
while(__HAL_UART_GET_FLAG(&UART1_Handler,UART_FLAG_TC)!=SET);
这一行代码就是我们发送一个字节之后之后,要检测这个数据是否已经被发送完成了。
五、实验现象
我们把程序下载到阿波罗 STM32F429
开发板,可以看到板子上的 DS0
开始闪烁,说明程序已经在跑了。接着我们打开 XCOM V2.0
,设置串口为开发板的 USB
转串口(CH340
虚拟串口,得根据你自己的电脑选择,我的电脑是 COM3
,另外,请注意:波特率是 115200
),可以看到如图所示:
可以看出,STM32F429
的串口数据发送是没问题的了。但是,因为我们在程序上面设置了必须输入回车,串口才认可接收到的数据,所以必须在发送数据后再发送一个回车符,这里 XCOM
提供的发送方法是通过勾选发送新行实现,如上图,只要勾选了这个选项,每次发送数据后,XCOM
都会自动多发一个回车(0X0D+0X0A
)。设置好了发送新行,我们再在发送区输入你想要发送的文字,然后单击发送,可以得到下图所示结果:
可以看到,我们发送的消息被发送回来了(图中圈圈内)。
六、STM32CubeMX 配置串口
本小节将讲解使用 STM32CubeMX
配置串口方法。这里不再讲解RCC
相关配置,仅讲解串口相关配置方法。
这里我们要配置串口 1,所以首先我们要使能串口 1 然后设置相应通信模式。打开 Pinout
选项卡界面,进入USART1
配置栏,如图所示:
USART1
配置栏有 2 个选项。第一个选项 Mode
用来设置串口 1 的模式或者关闭串口 1。第二个选项 Hardware Flow Control(RS232)
用来开启/关闭串口 1 的硬件流控制,该选项只有在Mode
选项值为 Asynchronous
(异步通信)模式的前提下才有效。这里我们要开启串口 1 的异步模式,并且不使用硬件流控制,所以这里我们直接选择 Mode
值为 Asynchronous
即可。配置好的USART1
界面如下图所示:
配置好串口 1 为异步通信模式后,那么在硬件上会使用到 PA9
和 PA10
作为串口 1 的发送接收引脚。在 STM32CubeMX
中,当我们选择好外设的工作模式之后,软件会自动配置 GPIO
口的相关模式和参数。在 Pinout
界面我们看看芯片引脚图会发现,PA9
和 PA10
端口的模式会自动复用为发送和接收模式,如下图所示:
同时,进入 GPIO
配置详细界面会发现,IO
口的模式等参数都做了相应的修改。Pin Configuration
界面多了一个 USART1
选项卡,该选项卡界面便是用来配置和查看串口引脚 PA9
和 PA10
配置参数的。如下图所示:
对于外设的功能引脚,在我们使能相应的外设(比如 USART1
)之后,STM32CubeMX
会自动设置 GPIO
相关配置,一般情况下用户不再需要去修改。所以这里,对于PA9
和 PA10
的配置我们就保留软件配置即可。
接下来我们需要配置 USART1
外设相关的参数,包括波特率,停止位等。接下来我们点击USART1
配置按钮,进入 USART1
详细参数配置界面。在弹出的 USART1 Configuration
界面会出现 5 个配置选项卡。
-
Parameter Settings
选项卡用来配置USART1
的初始化参数,包括波特率停止位等等。这里我们将USART1
配置为:波特率 115200,8 位字长模式,无奇偶校验位,1 个停止位,发送/接收均开启。
-
User Constants
是用来配置用户常量。
-
NVIC
选项卡用来使能USART1
中断。这里我们勾上Enabled
选项。
-
DMA Setting
是在使用USART1 DMA
的情况才需要配置。 -
GPIO Setting
便是查看和配置USART1
相关的IO
口
配置完 USART1
相关 IO
口和 USART1
参数之后,如果我们使用到串口中断,那么我们还
需要设置中断优先级分组。接下来便是配置 NVIC
相关参数。点击 NVIC
按钮之后,弹出 NVIC
配置界面 NVIC Configuration
,如图所示:
在弹出的 NVIC Configuration
界面,我们首先设置中断优先级分组级别,我们系统初始化设置为分组 2,那么就是 2 位抢占优先级和 2 位响应优先级。所以这里的参数我们选择“2 bits for pre-emption priority”
,也就是 2 位抢占优先级。配置完中断优先级分组之后,接下来我们要配置的是 USART1
的抢占优先级和响应优先级值,这里我们设置抢占和响应优先级均为 3 即可。进行完上面的操作之后,接下来我们便是生成工程代码。
打开生成的工程可以看到,在 main.c
文件中生成了如下串口初始化关键代码:
/* USART1 init function */
void MX_USART1_UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
HAL_UART_Init(&huart1);
}
同时在 stm32f4xx_hal_msp.c
中,生成了串口 MSP
函数 HAL_UART_MspInit
内容如下:
void HAL_UART_MspInit(UART_HandleTypeDef* huart)
{
GPIO_InitTypeDef GPIO_InitStruct;
if(huart->Instance==USART1)
{
/* USER CODE BEGIN USART1_MspInit 0 */
/* USER CODE END USART1_MspInit 0 */
/* Peripheral clock enable */
__HAL_RCC_USART1_CLK_ENABLE();
/**USART1 GPIO Configuration
PA9 ------> USART1_TX
PA10 ------> USART1_RX
*/
GPIO_InitStruct.Pin = GPIO_PIN_9|GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* Peripheral interrupt init */
HAL_NVIC_SetPriority(USART1_IRQn, 3, 3);
HAL_NVIC_EnableIRQ(USART1_IRQn);
/* USER CODE BEGIN USART1_MspInit 1 */
/* USER CODE END USART1_MspInit 1 */
}
}
函数 MX_USART1_UART_Init
的内容和本章串口实验源码中函数 uart_init
中调用HAL_UART_Init
函数作用类似,只不过波特率是通过入口参数动态设置。而生成的 MSP
函数HAL_UART_MspInit
内容和实验中该函数的作用就几乎是一模一样了。