STM32F4-USART

STM32F429 串口简介
串口作为 MCU 的重要外部接口,同时也是软件开发重要的调试手段,其重要性不言而喻。现在基本上所有的 MCU 都会带有串口,STM32 自然也不例外。
STM32F429 的串口资源相当丰富的,功能也相当强劲。ALIENTEK 阿波罗 STM32F429 开发板所使用的 STM32F429IGT6 最多可提供 8 路串口,有分数波特率发生器、支持同步单线通信和半双工单线通讯、支持 LIN、支持调制解调器操作、智能卡协议和 IrDA SIR ENDEC 规范、具有 DMA 等。
接下来我们先从寄存器层面,告诉你如何设置串口,以达到我们最基本的通信功能。本章,我们将实现利用串口 1 不停的打印信息到电脑上,同时接收从串口发过来的数据,把发送过来的数据直接送回给电脑。阿波罗 STM32F429 开发板板载了 1 个 USB 串口和 2 个 RS232 串口,我们本章介绍的是通过 USB 串口和电脑通信。
串口最基本的设置,就是波特率的设置。STM32F429 的串口使用起来还是蛮简单的,只要你开启了串口时钟,并设置相应 IO 口的模式,然后配置一下波特率,数据位长度,奇偶校验位等信息,就可以使用了,详见 5.3.2 节。下面,我们就简单介绍下这几个与串口基本配置直接相关的寄存器。
1,串口时钟使能。串口作为 STM32F429 的一个外设,其时钟由外设时钟使能寄存器控制,这里我们使用的串口 1 是在 APB2ENR 寄存器的第 4 位。APB2ENR 寄存器在之前已经介绍过了,这里不再介绍。只是说明一点,就是除了串口 1 和串口 6 的时钟使能在 APB2ENR 寄存器,其他串口的时钟使能位都在 APB1ENR 寄存器。
2,串口波特率设置。在 5.3.2 节,我们已经介绍过了,每个串口都有一个自己独立的波特率寄存器USART_BRR,通过设置该寄存器就可以达到配置不同波特率的目的。具体实现方法,请参考 5.3.2 节。
3,串口控制。STM32F429 的每个串口都有 3 个控制寄存器 USART_CR1~3,串口的很多配置都是通过这 3 个寄存器来设置的。这里我们只要用到 USART_CR1 就可以实现我们的功能了,该寄存器的各位描述如图 8.1.1 所示:
在这里插入图片描述
该寄存器的高 16 位没有用到,低 16 位用于串口的功能设置。OVER8 为过采样模式设置位,我们一般设置位 0,即 16 倍过采样已获得更好的容错性;UE 为串口使能位,通过该位置 1,以使能串口;M 为字长选择位,当该位为 0 的时候设置串口为 8 个字长外加 n 个停止位,停止位的个数(n)是根据 USART_CR2 的[13:12]位设置来决定的,默认为 0;PCE 为校验使能位,设置为 0,则禁止校验,否则使能校验;PS 为校验位选择位,设置为 0 则为偶校验,否则为奇校验;TXIE 为发送缓冲区空中断使能位,设置该位为 1,当 USART_SR 中的 TXE 位为 1 时,将产生串口中断;TCIE 为发送完成中断使能位,设置该位为 1,当 USART_SR 中的 TC 位为 1时,将产生串口中断;RXNEIE 为接收缓冲区非空中断使能,设置该位为 1,当 USART_SR 中的 ORE 或者 RXNE 位为 1 时,将产生串口中断;TE 为发送使能位,设置为 1,将开启串口的发送功能;RE 为接收使能位,用法同 TE。
其他位的设置,这里就不一一列出来了,大家可以参考《STM32F4xx 中文参考手册》第714 页有详细介绍,在这里我们就不列出来了。
4,数据发送与接收。STM32F429 的发送与接收是通过数据寄存器 USART_DR 来实现的,是一个双寄存器,包含了 TDR 和 RDR。当向 DR 寄存器写数据的时候,实际是写入 TDR,串口就会自动发送数据;当收到数据,读 DR 寄存器的时候,实际读取的是 RDR。TDR 和 RDR对外是不可见的,所以我们操作的就只有 DR 寄存器,该寄存器的各位描述如图 8.1.2 所示:
STM32F4-USART_第1张图片
可以看出,虽然是一个 32 位寄存器,但是只用了低 9 位(DR[8:0]),其他都是保留。
DR[8:0]为串口数据,包含了发送或接收的数据。由于它是由两个寄存器(TDR 和 RDR)组成的,一个给发送用(TDR),一个给接收用(RDR),该寄存器兼具读和写的功能。TDR 寄存器提供了内部总线和输出移位寄存器之间的并行接口。 RDR 寄存器提供了输入移位寄存器和内部总线之间的并行接口。
当使能校验位(USART_CR1 中 PCE 位被置位)进行发送时,写到 MSB 的值(根据数据的长度不同,MSB 是第 7 位或者第 8 位)会被后来的校验位取代。
当使能校验位进行接收时,读到的 MSB 位是接收到的校验位。
5,串口状态。串口的状态可以通过状态寄存器 USART_SR 读取。USART_SR 的各位描述如图 8.1.3 所示:
在这里插入图片描述
这里我们关注一下两个位,第 5、6 位 RXNE 和 TC。
RXNE(读数据寄存器非空),当该位被置 1 的时候,就是提示已经有数据被接收到了,并且可以读出来了。这时候我们要做的就是尽快去读取 USART_DR,通过读 USART_DR 可以将该位清零,也可以向该位写 0,直接清除。
**TC(发送完成),当该位被置位的时候,表示 USART_DR 内的数据已经被发送完成了。如果设置了这个位的中断,则会产生中断。**该位也有两种清零方式:1)读 USART_SR,写USART_DR。2)直接向该位写 0。
通过以上一些寄存器的操作外加一下 IO 口的配置,我们就可以达到串口最基本的配置了,关于串口更详细的介绍,请参考《STM32F4xx 中文参考手册》第 676 页至 720 页,通用同步异步收发器这一章节。
对于怎么直接使用寄存器配置串口收发,请参考我们寄存器版本教程和源码。接下来我们将着重讲解使用 HAL 库实现串口配置和使用的方法。在 HAL 库中,串口相关的函数和定义主要在文件stm32f4xx_hal_uart.c 和 stm32f4xx_hal_uart.h 中。接下来我们看看 HAL 库提供的串口相关操作函数。
1) 串口参数初始化(波特率/停止位等),并使能串口。
串口作为 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);

我们在程序中,只需要重写该函数即可。一般情况下,该函数内部用来编写 IO 口初始化,时钟使能以及 NVIC 配置。
2)使能串口和 GPIO 口时钟
我们要使用串口,所以我们必须使能串口时钟和使用到的 GPIO 口时钟。例如我们要使用串口 1,所以我们必须使能串口 1 时钟和 GPIOA 时钟(串口 1 使用的是 PA9 和 PA10)。具体方法如下:

__HAL_RCC_USART1_CLK_ENABLE();//使能 USART1 时钟
__HAL_RCC_GPIOA_CLK_ENABLE();//使能 GPIOA 时钟

使能使能相关方法我们在时钟系统相关章节有讲解,操作方法也非常简单,这里我们就不重复讲解。
3)GPIO 口初始化设置(速度,上下拉等)以及复用映射配置
我们在跑马灯实验中讲解过,在 HAL 库中 IO 口初始化参数设置和复用映射配置是在函数HAL_GPIO_Init 中一次性完成的。这里大家只需要注意,我们要复用 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

3) 开启串口相关中断,配置串口中断优先级
HAL 库中定义了一个使能串口中断的标识符__HAL_UART_ENABLE_IT,大家可以把它当一个函数来使用,具体定义请参考 HAL 库文件 stm32f4xx_hal_uart.h 中该标识符定义。例如我们要使能接收完成中断,方法如下:

__HAL_UART_ENABLE_IT(huart,UART_IT_RXNE);//开启接收完成中断

第一个参数为我们步骤 1 讲解的串口句柄,类型为 UART_HandleTypeDef 结构体类型。第二个参数为我们要开启的中断类型值,可选值在头文件 stm32f4xx_hal_uart.h 中有宏定义。有开启中断就有关闭中断,操作方法为:

__HAL_UART_DISABLE_IT(huart,UART_IT_RXNE);//关闭接收完成中断

对于中断优先级配置,方法就非常简单,详细知识情参考 4.5 小节相关知识。参考方法为:

HAL_NVIC_EnableIRQ(USART1_IRQn);//使能 USART1 中断通道
HAL_NVIC_SetPriority(USART1_IRQn,3,3);//抢占优先级 3,子优先级 3

4) 编写中断服务函数
串口 1 中断服务函数为:

void USART1_IRQHandler(void) ;

当发生中断的时候,程序就会执行中断服务函数。然后我们在中断服务函数中编写们相应的逻辑代码即可。HAL 库实际上对中断处理过程进行了完整的封装,具体内容我们在 8.3 小节通过结合实验源码给大家详细讲解。
5) 串口数据接收和发送
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);

通过该函数可以读取串口接受到的数据。
软件设计
本小节,我们首先会讲解使用 HAL 库配置串口的一般步骤。然后我们会具体讲解我们串口实验程序实现。ALIENTEK 编写的串口相关的源码再 SYSTEM 分组之下的 usart.c 和 usart.h中。
8.1 小节我们讲解了 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 串口 1bound:波特率
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);//该函数会开启接收中断并且设置接收缓冲以及接收缓冲接收最大数据量
}

该函数实现的是我们 8.1 小节讲解的步骤 1 的内容。同时这里大家需要注意,最后一行代码调用函数 HAL_UART_Receive_IT,作用是开启接收中断,同时设置接收的缓存区以及接收的数据量,对于这个缓冲我们在后面会给大家讲解它的作用。
串口 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;
		GPIO_Initure.Mode=GPIO_MODE_AF_PP;
		GPIO_Initure.Pull=GPIO_PULLUP;
		GPIO_Initure.Speed=GPIO_SPEED_FAST;
		GPIO_Initure.Alternate=GPIO_AF7_USART1;
		HAL_GPIO_Init(GPIOA,&GPIO_Initure);
		//PA9
		//复用推挽输出
		//上拉
		//高速
		//复用为 USART1
		//初始化 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
	}
}

该函数代码实现的是我们 8.1 小节讲解的步骤 2 到 4 的内容。这里大家需要注意,在该段代码中,通过判断宏定义标识符 EN_USART1_RX 的值来确定是否开启串口中断通道和设置串口 1 中断优先级。标识符 EN_USART1_RX 在头文件 usart.h 中有定义,默认情况下我们设置为1.

#define EN_USART1_RX1//使能(1)/禁止(0)串口 1 接收

通过上面两个函数,我们就配置了串口相关设置。接下来就是编写中断服务函数USART1_IRQHandler。而 HAL 库中,对中断服务函数的编写有非常严格的讲究。
首先 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);
	}
	...//此处省略部分代码
}

最后我们列出串口接收中断的一般流程,如图 8.3.1 所示:
STM32F4-USART_第2张图片
这里,我们再把串口接收中断的一般流程进行概括:当接收到一个字符之后,在函数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,所以每次接受一个字符,回直接保存到 RxXferSize[0]中,我们直接通过读取 RxXferSize[0]的值就是本次接收到的字符。这里我们设计了一个小小的接收协议:通过这个函数,配合一个数组 USART_RX_BUF[],一个接收状态寄存器 USART_RX_STA(此寄存器其实就是一个全局变量,由作者自行添加。由于它起到类似寄存器的功能,这里暂且称之为寄存器)实现对串口数据的接收管理。USART_RX_BUF 的大小由 USART_REC_LEN 定义,也就是一次接收的数据最大不能超过USART_REC_LEN 个字节。USART_RX_STA 是一个接收状态寄存器其各的定义如表 8.3.2 所
示:

					USART_RX_STA
bit15 				bit14 				bit13~0
接收完成标志 			接收到 0X0D 标志 		接收到的有效数据个数

设计思路如下:
当接收到从电脑发过来的数据,把接收到的数据保存在 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 库定义的串口中断逻辑确实非常复杂,并且因为处理过程繁琐所以效率不高。这里我们需要说明的是,在中断服务函数中,大家也可以不用调用HAL_UART_IRQHandler 函数,而是直接编写自己的中断服务函数。串口实验我们之所以遵循 HAL 库写法, 是为了让大家对 HAL 库有一个更清晰的理解。

如果我们不用中断处理回调函数,那么就不用初始化串口句柄的中断接收缓存,所以我们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);//错误处理回调函数

有兴趣的同学可以自行测试每个回调函数的使用方法,这里我们就不做过多讲解。最后我
们来看看主函数:

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);
while(__HAL_UART_GET_FLAG(&UART1_Handler,UART_FLAG_TC)!=SET);

第一句,其实就是调用 HAL 串口发送函数 HAL_UART_Transmit 来发送一个字符到串口。
第二句呢,就是我们发送一个字节之后之后,要检测这个数据是否已经被发送完成了。

你可能感兴趣的:(嵌入式)