龙空技术网

ALIENTEK 阿波罗 STM32F767 开发板资料连载 第八章 串口通信实验

正点原子日常 114

前言:

眼前同学们对“通过串口数据容错算法”大约比较重视,咱们都想要学习一些“通过串口数据容错算法”的相关内容。那么小编同时在网摘上收集了一些关于“通过串口数据容错算法””的相关文章,希望朋友们能喜欢,朋友们快快来学习一下吧!

1)实验平台:alientek 阿波罗 STM32F767 开发板

2)摘自《STM32F7 开发指南(HAL 库版)》关注官方微信号公众号,获取更多资料:正点原子

第八章 串口通信实验

前面两章介绍了 STM32F767 的 IO 口操作。这一章我们将学习 STM32F767 的串口,教大

家如何使用 STM32F767 的串口来发送和接收数据。本章将实现如下功能:STM32F767 通过串

口和上位机的对话,STM32F767 在收到上位机发过来的字符串后,原原本本的返回给上位机。

本章分为如下几个小节:

8.1 STM32F7 串口简介

8.2 硬件设计

8.3 软件设计

8.4 下载验证

8.5 STM32CubeMX 配置串口

8.1 STM32F7 串口简介

串口作为 MCU 的重要外部接口,同时也是软件开发重要的调试手段,其重要性不言而喻。

现在基本上所有的 MCU 都会带有串口,STM32 自然也不例外。

STM32F767 的串口资源相当丰富的,功能也相当强劲。ALIENTEK 阿波罗 STM32F767 开

发板所使用的 STM32F767IGT6 最多可提供 8 路串口,支持 8/16 倍过采样、支持自动波特率检

测、支持 Modbus 通信、支持同步单线通信和半双工单线通讯、支持 LIN、支持调制解调器操

作、智能卡协议和 IrDA SIR ENDEC 规范、具有 DMA 等。

5.3 节对串口有过简单的介绍,接下来我们将从寄存器层面,告诉你如何设置串口,以达到

我们最基本的通信功能。本章,我们将实现利用串口 1 不停的打印信息到电脑上,同时接收从

串口发过来的数据,把发送过来的数据直接送回给电脑。阿波罗 STM32F767 开发板板载了 1

个 USB 串口和 2 个 RS232 串口,我们本章介绍的是通过 USB 串口和电脑通信。

串口最基本的设置,就是波特率的设置。STM32F767 的串口使用起来还是蛮简单的,只要

你开启了串口时钟,并设置相应 IO 口的模式,然后配置一下波特率,数据位长度,奇偶校验

位等信息,就可以使用了,详见 5.3.2 节。下面,我们就简单介绍下这几个与串口基本配置直接

相关的寄存器。

1,串口时钟使能。串口作为 STM32F767 的一个外设,其时钟由外设时钟使能寄存器控制,

这里我们使用的串口 1 是在 APB2ENR 寄存器的第 4 位。APB2ENR 寄存器在之前已经介绍过

了,这里不再介绍。只是说明一点,就是除了串口 1 和串口 6 的时钟使能在 APB2ENR 寄存器,

其他串口的时钟使能位都在 APB1ENR 寄存器。

2,串口波特率设置。在 5.3.2 节,我们已经介绍过了,每个串口都有一个自己独立的波特

率寄存器 USART_BRR,通过设置该寄存器就可以达到配置不同波特率的目的。具体实现方法,

请参考 5.3.2 节。

3,串口控制。STM32F767 的每个串口都有 3 个控制寄存器 USART_CR1~3,串口的很多

配置都是通过这 3 个寄存器来设置的。这里我们只要用到 USART_CR1 就可以实现我们的功能

了,该寄存器的各位描述如图 8.1.1 所示:

图 8.1.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。

其他位的设置,这里就不一一列出来了,大家可以参考《STM32F7 中文参考手册》第 945

页有详细介绍,在这里我们就不列出来了。

4,数据发送与接收。与 STM32F1 和 F4 不同,STM32F7 的串口发送和接收由两个不同的

寄存器组成。发送数据是 USART_TDR 寄存器,接收数据是 USART_RDR 寄存器,USART_TDR

寄存器各位描述如图 8.1.2 所示:

图 8.1.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 的各位描

述如图 8.1.3 所示:

图 8.1.3 USART_ISR 寄存器各位描述

这里我们关注一下两个位,第 5、6 位 RXNE 和 TC。

RXNE(读数据寄存器非空),当该位被置 1 的时候,就是提示已经有数据被接收到了,并

且可以读出来了。这时候我们要做的就是尽快去读取 USART_RDR,通过读 USART_RDR 可以

将该位清零,也可以向该位写 0,直接清除。

TC(发送完成),当该位被置位的时候,表示 USART_TDR 内的数据已经被发送完成了。

如果设置了这个位的中断,则会产生中断。该位也有两种清零方式:1)读 USART_ISR,写

USART_TDR。2)直接向该位写 0。

通过以上一些寄存器的操作外加一下 IO 口的配置,我们就可以达到串口最基本的配置了,

关于串口更详细的介绍,请参考《STM32F7 中文参考手册》第 907 页至 964 页,通用同步异步

收发器这一章节。

对于怎么直接使用寄存器配置串口收发,请参考我们寄存器版本教程和源码。接下来我们

将着重讲解使用 HAL 库实现串口配置和使用的方法。在 HAL 库中,串口相关的函数和定义主

要在文件 stm32f7xx_hal_uart.c 和 stm32f7xx_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;

UART_AdvFeatureInitTypeDef AdvancedInit;

uint8_t

*pTxBuffPtr;

uint16_t

TxXferSize;

uint16_t

TxXferCount;

uint8_t

*pRxBuffPtr;

uint16_t

RxXferSize;

uint16_t

RxXferCount;

uint16_t

Mask;

DMA_HandleTypeDef *hdmatx;

DMA_HandleTypeDef *hdmarx;

HAL_LockTypeDef Lock;

__IO HAL_UART_StateTypeDef gState;

__IO HAL_UART_StateTypeDef RxState;

__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 句柄,这里我们先不讲解。

AdvancedInit 是用来配置串口的高级功能,有兴趣的同学可以对照中文参考手册了解一下。

其他的三个变量就是一些 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 库文件 stm32f7xx_hal_uart.h 中该标识符定义。例如我

们要使能接收完成中断,方法如下:

__HAL_UART_ENABLE_IT(huart,UART_IT_RXNE);

//开启接收完成中断

第一个参数为我们步骤 1 讲解的串口句柄,类型为 UART_HandleTypeDef 结构体类型。第

二个参数为我们要开启的中断类型值,可选值在头文件 stm32f7xx_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) 串口数据接收和发送

STM32F7 的发送与接收是通过数据寄存器 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);

通过该函数可以读取串口接受到的数据。

STM32F7 相关基础知识我们就给大家讲解到这里,接下来我们看看本实验的软硬件设计。

8.2 硬件设计

本实验需要用到的硬件资源有:

1) 指示灯 DS0

2) 串口 1

串口 1 之前还没有介绍过,本实验用到的串口 1 与 USB 串口并没有在 PCB 上连接在一起,

需要通过跳线帽来连接一下。这里我们把 P4 的 RXD 和 TXD 用跳线帽与 PA9 和 PA10 连接起

来。如图 8.2.1 所示:

图 8.2.1 硬件连接图示意图

连接上这里之后,我们在硬件上就设置完成了,可以开始软件设计了。

8.3 软件设计

本小节,我们首先会讲解使用 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 串口 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);

//该函数会开启接收中断并且设置接收缓冲以及接收缓冲接收最大数据量

}

该函数实现的是我们 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;

//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

}

}

该函数代码实现的是我们 8.1 小节讲解的步骤 2 到 4 的内容。这里大家需要注意,在该段

代码中,通过判断宏定义标识符 EN_USART1_RX 的值来确定是否开启串口中断通道和设置串

口 1 中断优先级。标识符 EN_USART1_RX 在头文件 usart.h 中有定义,默认情况下我们设置为

1。

#define EN_USART1_RX

1

//使能(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 所示:

图 8.3.1 串口接收中断执行流程图

这里,我们再把串口接收中断的一般流程进行概括:当接收到一个字符之后,在函数

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 所

示:

表 8.3.2 接收状态寄存器位定义表

设计思路如下:

当接收到从电脑发过来的数据,把接收到的数据保存在 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;

Cache_Enable();

//打开 L1-Cache

HAL_Init();

//初始化 HAL 库

Stm32_Clock_Init(432,25,2,9); //设置时钟,216Mhz

delay_init(216); //延时初始化

uart_init(115200);

//串口初始化

LED_Init();

//初始化 LED

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 STM32F7 开发板 串口实验\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);

}

}

}

这段代码逻辑比较简单,首先判断全局变量 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 来发送一个字符到串口。

第二句呢,就是我们发送一个字节之后之后,要检测这个数据是否已经被发送完成了。

8.4 下载验证

本实验需要用到的硬件资源有:

3) 指示灯 DS0

4) 串口 1

串口 1 之前还没有介绍过,本实验用到的串口 1 与 USB 串口并没有在 PCB 上连接在一起,

需要通过跳线帽来连接一下。这里我们把 P4 的 RXD 和 TXD 用跳线帽与 PA9 和 PA10 连接起

来。如图 8.2.1 所示:

图 8.2.1 硬件连接图示意图

连接上这里之后,我们在硬件上就设置完成了,可以开始软件设计了。

8.5 STM32CubeMX 配置串口

前面章节我们详细讲解了使用 STM32CubeMX 配置 IO 口输入输出,本小节我们将讲解使

用 STM32CubeMX 配置串口方法。同样,大家直接复制光盘的 STM32CubeMX 配置的工程模

板,目录为:“4,程序源码\标准例程-库函数版本\实验 0-3 Template 工程模板-使用

STM32CubeMX 配置”。然后使用 STM32CubeMX 打开该工程(点击工程目录的 Template.ioc)。

这里我们同样不再讲解 RCC 相关配置,我们仅仅讲解串口相关配置方法。

这里我们要配置串口 1,所以首先我们要使能串口 1 然后设置相应通信模式。打开 Pinout

选项卡界面,左侧依次进入 Configuration->Peripherals->USART1 配置栏,如下图 8.5.1 所示:

图 8.5.1 进入 Configuration->Peripherals->USART1 配置栏

USART1 配置栏有 2 个选项。第一个选项 Mode 用来设置串口 1 的模式或者关闭串口 1。

第二个选项 Hardware Flow Control(RS232)用来开启/关闭串口 1 的硬件流控制,该选项只有在

Mode 选项值为 Asynchronous(异步通信)模式的前提下才有效。这里我们要开启串口 1 的异步模

式,并且不使用硬件流控制,所以这里我们直接选择 Mode 值为 Asynchronous 即可。配置好的

USART1 界面如下图 8.5.2 所示:

图 8.5.2 USART1 配置

配置好串口1为异步通信模式后,那么在硬件上会默认开启PB14和PB15作为串口1引脚。

这时候我们进入引脚配置图可以发现,PB14 和 PB15 变为绿色,同时显示为 USART1_TX 和

USART1_RX 功能引脚,如下图 8.5.3 所示:

图 8.5.3 PB14/PB15 引脚模式

而这里我们需要使用 PA9 和 PA10 作为串口 1 的发送接收引脚,所以我们需要重新修改引脚模

式(这里实际上涉及到 F7 的引脚复用映射方面的知识,如有不理解的地方请参考 4.4 小节)。

这里我们分别选中 PA9 和 PA10,然后修改引脚模式为 USART1_TX 和 USART1_RX,那么 PB14

和 PB15 的模式就会自动设置为复位后的模式,修改后引脚图如下图 8.5.4 所示:

图 8.5.4PA9/PA10 引脚模式

同时,进入 GPIO 配置详细界面会发现,IO 口的模式等参数都做了相应的修改。参考 6.5

小节方法,依次进入 Configuration->GPIO 界面会发现,Pin Configuration 界面多了一个 USART1

选项卡,该选项卡界面便是用来配置和查看串口引脚 PA9 和 PA10 配置参数的。如下图 8.5.5 所

示:

图 8.5.5 USART1 引脚详细配置界面

对于外设的功能引脚,在我们使能相应的外设(比如 USART1)之后,STM32CubeMX 会

自动设置 GPIO 相关配置,一般情况下用户不再需要去修改。所以这里,对于 PA9 和 PA10 的

配置我们就保留软件配置即可。

接下来我们需要配置 USART1 外设相关的参数,包括波特率,停止位等。我们直接进入

Configuration 选项卡,如果我们之前使能了 USART1,那么在 Connectivity 栏会出现 USART1 配

置按钮。如下图 8.5.6 所示:

图 8.5.6Configuration 选项卡

接下来我们点击 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 口,这和图 8.5.4 作用一致。

配置完 USART1 相关 IO 口和 USART1 参数之后,如果我们使用到串口中断,那么我们还

需要设置中断优先级分组。接下来便是配置 NVIC 相关参数。同样的方法,进入 Conguration

选项卡,点击 NVIC 按钮,如下图 8.5.7 所示:

图 8.5.7Configuration->NVIC 按钮

点击 NVIC 按钮之后,弹出 NVIC 配置界面 NVIC Configuration,如下图 8.5.8 所示:

图 8.5.8 NVIC Configuration 配置界面

在弹出的 NVIC Configuration 界面,我们首先设置中断优先级分组级别,我们系统初始化

设置为分组 2,那么就是 2 为抢占优先级和 2 位响应优先级。所以这里的参数我们选择“2 bits for

pre-emption priority”,也就是 2 位抢占优先级。

配置完中断优先级分组之后,接下来我们要配置的是 USART1 的抢占优先级和响应优先级

值,这里我们设置抢占和响应优先级均为 3 即可。

进行完上面的操作之后,接下来我们便是生成工程代码。

打开生成的工程可以看到,在 main.c 文件中生成了如下串口初始化关键代码:

static void MX_USART1_UART_Init(void)

{

huart1.Instance = USART1;

huart1.Init.BaudRate = 115200;

huart1.Init.WordLength = UART_WORDLENGTH_7B;

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;

huart1.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;

huart1.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;

if (HAL_UART_Init(&huart1) != HAL_OK)

{

Error_Handler();

}

}

同时在 stm32f7xx_hal_msp.c 中,生成了串口 MSP 函数 HAL_UART_MspInit 内容如下:

void HAL_UART_MspInit(UART_HandleTypeDef* huart)

{

GPIO_InitTypeDef GPIO_InitStruct;

if(huart->Instance==USART1)

{

__HAL_RCC_USART1_CLK_ENABLE();

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);

HAL_NVIC_SetPriority(USART1_IRQn, 3, 3);

HAL_NVIC_EnableIRQ(USART1_IRQn);

}

}

函数 MX_USART1_UART_Init 的内容和本章串口实验源码中函数 uart_init 中调用

HAL_UART_Init 函数作用类似,只不过波特率是通过入口参数动态设置。而生成的 MSP 函数

HAL_UART_MspInit 内容和实验中该函数的作用就几乎是一模一样了。

关于使用 STM32CubeMX 配置串口的方法就给大家介绍到这里。

标签: #通过串口数据容错算法