11. 串口通信
一、串口通信简介
串口通信是一种设备间常用的串行通信方式,串口按位(bit)发送和接收字节。串口通信的数据包由 发送设备的 TXD 接口传输到接收设备的 RXD 接口。在串口通信的协议层中,规定了数据包的内容,它由 起始位、主体数据、校验位 以及 停止位 组成,通讯双方的数据包格式要约定一致才能正常收发数据。在串口通信中,常用的协议包括 RS-232、RS-422 和 RS-485 等。
随着科技的发展,RS-232 在工业上还有广泛的使用,但是在商业技术上,已经慢慢的使用USB 转串口取代了 RS-232 串口。我们只需要在电路中添加一个 USB 转串口芯片,就可以实现 USB 通信协议和标准 UART 串行通信协议的转换,而开发板上的 USB 转串口芯片是 CH340C 这个芯片。
二、串口通信协议
串口通信协议数据包组成可以分为 波特率 和 数据帧格式 两部分。
【1】、波特率
异步通信是不需要时钟信号的,但是这里需要我们约定好两个设备的波特率。波特率表示每秒钟传送的码元符号的个数,所以它决定了数据帧里面每一个位的时间长度。两个要通信的设备的波特率一定要设置相同,我们常见的波特率是 4800、9600、115200 等。
【2】、起始位和停止位
串口通信的一个数据帧是从起始位开始,直到停止位。数据帧中的起始位是由一个逻辑 0 的数据位表示,而数据帧的停止位可以是 0.5、1、1.5 或 2 个逻辑 1 的数据位表示,只要双方约定一致即可。
【3】、有效数据位
数据帧的起始位之后,就接着是数据位,也称有效数据位,这就是我们真正需要的数据,有效数据位通常会被约定为 5、6、7 或者 8 个位长。有效数据位是低位(LSB)在前,高位(MSB)在后。
【4】、校验位
校验位可以认为是一个特殊的数据位。校验位一般用来判断接收的数据位有无错误,检验方法有:奇检验、偶检验、0 检验、1 检验 以及 无检验。
- 奇校验 是指有效数据为和校验位中 “1” 的个数为奇数。
- 偶校验 与奇校验要求刚好相反,要求帧数据和校验位中 “1” 的个数为偶数。
- 0 校验 是指不管有效数据中的内容是什么,校验位总为 “0”.
- 1 校验 是指不管有效数据中的内容是什么,校验位总为 “1”
- 无校验 是指数据帧中不包含校验位。
三、串口简介
STM32407ZGT6 最多可提供 6 路串口,有分数波特率发生器、支持同步单线通信和半双工单线通讯、支持 LIN、支持调制解调器操作、智能卡协议和 IrDA SIR ENDEC 规范、具有 DMA 等。
STM32F4 的串口分为两种:USART(即通用同步异步收发器)和 UART(即通用异步收发器)。UART 是在 USART 基础上裁剪掉了同步通信功能,只剩下异步通信功能。简单区分同步和异步就是看通信时需不需要对外提供时钟输出,我们平时用串口通信基本都是异步通信。
STM32F4 有 4 个 USART 和 2 个 UART,其中 USART1 和 USART6 的时钟源来于 APB2 时钟,其最大频率为 84MHz,其他 4个串口的时钟源可以来于 APB1时钟,其最大频率为 42MHz。
四、USART框图
①、USART 信号引脚
- TX:发送数据输出引脚
- RX:接收数据输入引脚
- SCLK:发送器时钟输出,适用于同步传输
- SW_RX:数据接收引脚,属于内部引脚,用于智能卡模式
- IrDA_RDI:IrDA 模式下的数据输入
- IrDA_TDO:IrDA 模式下的数据输出
- nRTS:发送请求,若是低电平,表示 USART 准备好接收数据
- nCTS:清除发送,若是高电平,在当前数据传输结束时阻断下一次的数据发送
②、数据寄存器
USART_DR 包含了已发送或接收到的数据。由于它本身就是两个寄存器组成的,一个专门给发送用的(TDR),一个专门给接收用的(RDR),该寄存器具备读和写的功能。TDR 寄存器提供了内部总线和输出移位寄存器之间的并行接口。RDR 寄存器提供了输入移位寄存器和内部总线之间的并行接口。当进行数据发送操作时,往 USART_DR 中写入数据会自动存储在 TDR内;当进行读取操作时,向 USART_DR 读取数据会自动提去 RDR 数据。
USART 数据寄存器(USART_DR)低 9 位数据有效,其他数据位保留。USART_DR 的第 9 位数据是否有效跟 USART_CR1 的 M 位设置有关,当 M 位为 0 表示 8 位数据字长;当 M 位为 1 时表示 9 位数据字长,一般使用 8 位数据字长。
当使能校验位(USART_CR1 中 PCE 位被置位)进行发送时,写到 MSB 的值(根据数据的长度不同,MSB 是第 7 位或者第 8 位)会被后来的校验位取代。
③、控制器
USART 有专门控制发送的发送器,控制接收的接收器,还有唤醒单元、中断控制等等。
④、时钟与波特率
波特率,即每秒钟传输的码元个数,在二进制系统中(串口的数据帧就是二进制的形式),波特率与波特率的数值相等。波特率通过以下公式得出:
其中,fck 是给串口的时钟(USART2\3 和 UART4\5 的时钟源为 PCLK1,USART1\6 的时钟源为PCLK2),过采样设置为 16 倍过采样,即 OVER8 = 0,USARTDIV 是一个无符号的定点数,存放在波特率寄存器(USART_BRR)的低 16位,DIV_Mantissa[11:0] 存放的是 USARTDIV 的整数部分,DIV_Fractionp[3:0] 存放的是 USARTDIV 的小数部分。
当串口 1 设置需要得到 115200 的波特率,fck = 84MHz,那么可得:
得到 USARTDIV ≈ 45.5729,分离 USARTDIV 的整数部分与小数部分,整数部分为 45,即 0x2D,那么 DIV_Mantissa = 0x2D;小数部分为 0.5729,转化为十六进制即 0.5729 * 16 ≈ 9,所以 DIV_Fractionp = 0x9,USART_BRR 寄存器应该赋值为 0x2D9,成功设置波特率为 115200。值得注意 USARTDIV 是允许有余数的,我们用四舍五入进行取整,这样会导致波特率会有所偏差,而这样的小误差是可以被允许的。
五、UASRT常用寄存器
5.1、状态寄存器
这里我们关注一下两个位,第 5、6 位 RXNE 和 TC。
位 5 RXNE(读数据寄存器非空),当该位被置 1 的时候,就是提示已经有数据被接收到了,并且可以读出来了。这时候我们要做的就是尽快去读取 USART_DR,通过读 USART_DR 可以将该位清零,也可以向该位写 0,直接清除。
位 6 TC(发送完成),当该位被置位的时候,表示 USART_DR 内的数据已经被发送完成了。如果设置了这个位的中断,则会产生中断。该位也有两种清零方式:①、读 USART_SR,写 USART_DR;②、直接向该位写 0;
5.2、数据寄存器
STM32 的发送与接收是通过数据寄存器 USART_DR 来实现的,这是一个双寄存器,包含了 TDR 和 RDR。当向该寄存器写数据的时候,串口就会自动发送,当收到数据的时候,也是存在该寄存器内。
5.3、波特率寄存器
每个串口都有一个自己独立的波特率寄存器 USART_BRR,通过设置该寄存器就可以达到配置不同波特率的目的。
5.4、控制寄存器
STM32F407 每个串口都有 3 个控制寄存器 USART_CR1~3,串口的很多配置都是通过这 3个寄存器来设置的。USART_CR1 寄存器的描述如下图所示:
UASRT_CR1 寄存器的高 16 位没有用到,低 16 位用于串口的功能设置。
位 13 UE 为串口使能位,通过该位置 1,使能串口。
位 12 M 为字长,当该位为 0 的时候设置串口为 8 个字长外加 n 个停止位,停止位的个数(n)是根据 USART_CR2 的 [13:12] 位设置来决定的,默认为 0。
位 10 PCE 为校验使能位,设置为 0,即禁止校验,否则使能校验。位 9 PS 为校验位选择,设置为 0 为偶校验,否则奇校验。
位 7 TXEIE 为发送缓冲区空中断使能位,设置该位为 1,当 USART_SR 中的 TXE 位为 1 时,将产生串口中断。位 6 TCIE 为发送完成中断使能位,设置该位为 1,当 USART_SR 中的 TC 位为 1时,将产生串口中断。位 5 RXNEIE 为接收缓冲区非空中断使能,设置该位为 1,当 USART_SR 中的 ORE 或者 RXNE 位为 1 时,将产生串口中断。位 3 TE 为发送使能位,设置为 1,将开启串口的发送功能。位 2 RE 为接收使能位,用法同 TE。
这里,我们使用 USART_CR2 寄存器的位 [13:12] STOP 设置停止位个数。
这里,我们使用 USART_CR3 寄存器的位 3 HDSEL 设置工作模式。
六、IO引脚复用功能
【1】、USART1 IO 引脚复用及其重映射功能
功能引脚 | 复用引脚 | 重映射引脚 |
---|---|---|
TXD | PA9 | PB6 |
RXD | PA10 | PB7 |
【2】、USART2 IO 引脚复用及其重映射功能
功能引脚 | 复用引脚 | 重映射引脚 |
---|---|---|
TXD | PA2 | PD5 |
RXD | PA3 | PD6 |
【3】、USART3 IO 引脚复用及其重映射功能
功能引脚 | 复用引脚 | 重映射引脚 |
---|---|---|
TXD | PB10 | PD8/PC10 |
RXD | PB11 | PD9/PC11 |
【4】、UART4 IO 引脚复用及其重映射功能
功能引脚 | 复用引脚 | 重映射引脚 |
---|---|---|
TXD | PA0 | PC10 |
RXD | PA1 | PC11 |
【5】、UART5 IO 引脚复用及其重映射功能
功能引脚 | 复用引脚 | 重映射引脚 |
---|---|---|
TXD | PC12 | |
RXD | PD2 |
【6】、USART6 IO 引脚复用及其重映射功能
功能引脚 | 复用引脚 | 重映射引脚 |
---|---|---|
TXD | PC6 | PG9 |
RXD | PAC7 | PG14 |
七、串口通信配置步骤
7.1、使能对应的时钟
使能对应的串口时钟。
#define __HAL_RCC_USART1_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_USART1EN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->APB2ENR, RCC_APB2ENR_USART1EN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_USART2_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->APB1ENR, RCC_APB1ENR_USART2EN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->APB1ENR, RCC_APB1ENR_USART2EN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_USART6_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_USART6EN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->APB2ENR, RCC_APB2ENR_USART6EN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_USART3_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->APB1ENR, RCC_APB1ENR_USART3EN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->APB1ENR, RCC_APB1ENR_USART3EN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_UART4_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->APB1ENR, RCC_APB1ENR_UART4EN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->APB1ENR, RCC_APB1ENR_UART4EN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_UART5_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->APB1ENR, RCC_APB1ENR_UART5EN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->APB1ENR, RCC_APB1ENR_UART5EN);\
UNUSED(tmpreg); \
} while(0U)
使能对应的 GPIO 的时钟。
#define __HAL_RCC_GPIOA_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOB_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOBEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOBEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOC_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOCEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOCEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOD_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIODEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIODEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOG_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOGEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOGEN);\
UNUSED(tmpreg); \
} while(0U)
7.2、配置串口工作参数
要使用一个外设首先要对它进行初始化,串口的初始化函数,其声明如下:
HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart);
形参 huart 是串口的句柄,UART_HandleTypeDef 结构体类型,其定义如下:
typedef struct __UART_HandleTypeDef
{
USART_TypeDef *Instance; // UART寄存器基地址
UART_InitTypeDe Init; // UART初始化结构体
const 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接收端计数器
__IO HAL_UART_RxTypeTypeDef ReceptionType;
DMA_HandleTypeDef *hdmatx; // UART发送参数设置(DMA)
DMA_HandleTypeDef *hdmarx; // UART接收参数设置(DMA)
HAL_LockTypeDef Lock; // 锁对象
__IO HAL_UART_StateTypeDef gState; // UART发送状态结构体
__IO HAL_UART_StateTypeDef xState; // UART接收状态结构体
__IO uint32_t ErrorCode; // UART操作错误信息
} UART_HandleTypeDef;
Instance:指向 UART 寄存器基地址。实际上这个基地址 HAL 库已经定义好了,可以选择范围:USART1~ USART3、USART6、UART4、UART5。
#define USART1 ((USART_TypeDef *) USART1_BASE)
#define USART2 ((USART_TypeDef *) USART2_BASE)
#define USART3 ((USART_TypeDef *) USART3_BASE)
#define UART4 ((USART_TypeDef *) UART4_BASE)
#define UART5 ((USART_TypeDef *) UART5_BASE)
#define USART6 ((USART_TypeDef *) USART6_BASE)
Init:UART 初始化结构体,用于配置通讯参数,如波特率、数据位数、停止位等等。
Lock:对资源操作增加操作 锁保护,可选 HAL_UNLOCKED 或者 HAL_LOCKED 两个参数。如果 gState 的值等于 HAL_UART_STATE_RESET,则认为串口未被初始化,此时,分配锁资源,并且调用 HAL_UART_MspInit() 函数来对串口的 GPIO 和时钟进行初始化。
gState,RxState:分别是 UART 的发送状态、工作状态的结构体和 UART 接受状态的结构体。HAL_UART_StateTypeDef 是一个枚举类型,列出串口在工作过程中的状态值,有些值只适用于 gState,如 HAL_UART_STATE_BUSY。
ErrorCode:串口错误操作信息。主要用于存放串口操作的错误信息。
UART_InitTypeDef 这个结构体类型,该结构体用于配置 UART 的各个通信参数,包括波特率,停止位等,具体说明如下:
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:波特率设置。一般设置为 2400、9600、19200、115200。
WordLength:数据帧字长,可选 8 位或 9 位。这里我们设置为 8 位字长数据格式。
#define UART_WORDLENGTH_8B 0x00000000U
#define UART_WORDLENGTH_9B ((uint32_t)USART_CR1_M)
StopBits:停止位设置,可选 1 个或 2 个停止位,一般我们选择 1 个停止位。
#define UART_STOPBITS_1 0x00000000U
#define UART_STOPBITS_2 ((uint32_t)USART_CR2_STOP_1)
Parity:奇偶校验控制选择,我们可以设定为 无奇偶校验位、偶校验 或 奇校验。
#define UART_PARITY_NONE 0x00000000U
#define UART_PARITY_EVEN ((uint32_t)USART_CR1_PCE)
#define UART_PARITY_ODD ((uint32_t)(USART_CR1_PCE | USART_CR1_PS))
Mode:UART 模式选择,可以设置为 只收模式,只发模式,或者 收发模式。这里我们设置为全双工收发模式。
#define UART_MODE_RX ((uint32_t)USART_CR1_RE)
#define UART_MODE_TX ((uint32_t)USART_CR1_TE)
#define UART_MODE_TX_RX ((uint32_t)(USART_CR1_TE | USART_CR1_RE))
HwFlowCtl:硬件流控制选择,一般我们设置为无硬件流控制。
#define UART_HWCONTROL_NONE 0x00000000U
#define UART_HWCONTROL_RTS ((uint32_t)USART_CR3_RTSE)
#define UART_HWCONTROL_CTS ((uint32_t)USART_CR3_CTSE)
#define UART_HWCONTROL_RTS_CTS ((uint32_t)(USART_CR3_RTSE | USART_CR3_CTSE))
OverSampling:过采样选择,选择 8 倍过采样或者 16 过采样,一般选择 16 过采样。
#define UART_OVERSAMPLING_16 0x00000000U
#define UART_OVERSAMPLING_8 ((uint32_t)USART_CR1_OVER8)
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
typedef enum
{
HAL_OK = 0x00U, // 成功
HAL_ERROR = 0x01U, // 错误
HAL_BUSY = 0x02U, // 忙碌
HAL_TIMEOUT = 0x03U // 超时
} HAL_StatusTypeDef;
7.3、串口底层初始化
HAL 库中,提供 HAL_GPIO_Init() 函数用于配置 GPIO 功能模式,初始化 GPIO。该函数的声明如下:
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);
该函数的第一个形参 GPIOx 用来指定端口号,可选值如下:
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)
第二个参数是 GPIO_InitTypeDef 类型的结构体变量,用来设置 GPIO 的工作模式,其定义如下:
typedef struct
{
uint32_t Pin; // 引脚号
uint32_t Mode; // 模式设置
uint32_t Pull; // 上下拉设置
uint32_t Speed; // 速度设置
uint32_t Alternate; // 复用功能设置
}GPIO_InitTypeDef;
成员 Pin 表示 引脚号,范围:GPIO_PIN_0 到 GPIO_PIN_15。
#define GPIO_PIN_0 ((uint16_t)0x0001) /* Pin 0 selected */
#define GPIO_PIN_1 ((uint16_t)0x0002) /* Pin 1 selected */
#define GPIO_PIN_2 ((uint16_t)0x0004) /* Pin 2 selected */
#define GPIO_PIN_3 ((uint16_t)0x0008) /* Pin 3 selected */
#define GPIO_PIN_4 ((uint16_t)0x0010) /* Pin 4 selected */
#define GPIO_PIN_5 ((uint16_t)0x0020) /* Pin 5 selected */
#define GPIO_PIN_6 ((uint16_t)0x0040) /* Pin 6 selected */
#define GPIO_PIN_7 ((uint16_t)0x0080) /* Pin 7 selected */
#define GPIO_PIN_8 ((uint16_t)0x0100) /* Pin 8 selected */
#define GPIO_PIN_9 ((uint16_t)0x0200) /* Pin 9 selected */
#define GPIO_PIN_10 ((uint16_t)0x0400) /* Pin 10 selected */
#define GPIO_PIN_11 ((uint16_t)0x0800) /* Pin 11 selected */
#define GPIO_PIN_12 ((uint16_t)0x1000) /* Pin 12 selected */
#define GPIO_PIN_13 ((uint16_t)0x2000) /* Pin 13 selected */
#define GPIO_PIN_14 ((uint16_t)0x4000) /* Pin 14 selected */
#define GPIO_PIN_15 ((uint16_t)0x8000) /* Pin 15 selected */
成员 Mode 是 GPIO 的 模式选择,有以下选择项:
#define GPIO_MODE_AF_PP 0x00000002U // 推挽式复用
成员 Pull 用于 配置上下拉电阻,有以下选择项:
#define GPIO_NOPULL 0x00000000U // 无上下拉
#define GPIO_PULLUP 0x00000001U // 上拉
#define GPIO_PULLDOWN 0x00000002U // 下拉
成员 Speed 用于 配置 GPIO 的速度,有以下选择项:
#define GPIO_SPEED_FREQ_LOW 0x00000000U // 低速
#define GPIO_SPEED_FREQ_MEDIUM 0x00000001U // 中速
#define GPIO_SPEED_FREQ_HIGH 0x00000002U // 高速
#define GPIO_SPEED_FREQ_VERY_HIGH 0x00000003U // 极速
成员 Alternate 用于 配置具体的复用功能,不同的 GPIO 口可以复用的功能不同,具体可参考数据手册。
#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_AF8_UART4 ((uint8_t)0x08) /* UART4 Alternate Function mapping */
#define GPIO_AF8_UART5 ((uint8_t)0x08) /* UART5 Alternate Function mapping */
#define GPIO_AF8_USART6 ((uint8_t)0x08) /* USART6 Alternate Function mapping */
7.4、使能中断
7.4.1、设置中断优先级分组
HAL_NVIC_SetPriorityGrouping() 函数是设置中断优先级分组函数。其声明如下:
void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup);
其中,参数 PriorityGroup 是 中断优先级分组号,可以选择范围如下:
#define NVIC_PRIORITYGROUP_0 0x00000007U /*!< 0 bits for pre-emption priority
4 bits for subpriority */
#define NVIC_PRIORITYGROUP_1 0x00000006U /*!< 1 bits for pre-emption priority
3 bits for subpriority */
#define NVIC_PRIORITYGROUP_2 0x00000005U /*!< 2 bits for pre-emption priority
2 bits for subpriority */
#define NVIC_PRIORITYGROUP_3 0x00000004U /*!< 3 bits for pre-emption priority
1 bits for subpriority */
#define NVIC_PRIORITYGROUP_4 0x00000003U /*!< 4 bits for pre-emption priority
0 bits for subpriority */
这个函数在一个工程里基本只调用一次,而且是在程序 HAL 库初始化函数里面已经被调用,后续就不会再调用了。因为当后续调用设置成不同的中断优先级分组时,有可能造成前面设置好的抢占优先级和响应优先级不匹配。如果调用了多次,则以最后一次为准。
7.4.2、设置中断优先级
HAL_NVIC_SetPriority() 函数是设置中断优先级函数。其声明如下:
void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority);
其中,参数 IRQn 是 中断号,可以选择范围:IRQn_Type 定义的枚举类型,定义在 stm32f407xx.h。
typedef enum
{
USART1_IRQn = 37, /*!< USART1 global Interrupt */
USART2_IRQn = 38, /*!< USART2 global Interrupt */
USART3_IRQn = 39, /*!< USART3 global Interrupt */
UART4_IRQn = 52, /*!< UART4 global Interrupt */
UART5_IRQn = 53, /*!< UART5 global Interrupt */
USART6_IRQn = 71, /*!< USART6 global interrupt */
} IRQn_Type;
参数 PreemptPriority 是 抢占优先级,可以选择范围:0 到 15,具体根据中断优先级分组决定。
参数 SubPriority 是 响应优先级,可以选择范围:0 到 15,具体根据中断优先级分组决定。
7.4.3、使能中断
HAL_NVIC_EnableIRQ() 函数是中断使能函数。其声明如下:
void HAL_NVIC_EnableIRQ(IRQn_Type IRQn);
其中,参数 IRQn 是 中断号,可以选择范围:IRQn_Type 定义的枚举类型,定义在 stm32f407xx.h。
7.5、编写中断服务函数
每开启一个中断,就必须编写其对应的中断服务函数,否则将导致死机(CPU 将找不到中断服务函数)。中断服务函数接口厂家已经在 startup_stm32f407xx.s 中写好了。
void USART1_IRQHandler(void);
void USART2_IRQHandler(void);
void USART3_IRQHandler(void);
void UART4_IRQHandler(void);
void UART5_IRQHandler(void);
void USART6_IRQHandler(void);
HAL 库为了用户使用方便,提供了一个中断通用入口函数 HAL_UART_IRQHandler()
。
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
{
// 如果没有错误发生
errorflags = (isrflags & (uint32_t)(USART_SR_PE | USART_SR_FE | USART_SR_ORE | USART_SR_NE));
if (errorflags == RESET)
{
// UART接收模式。RXNE:读数据寄存器非空 RXNEIE:接收缓冲区非空中断使能
if (((isrflags & USART_SR_RXNE) != RESET) && ((cr1its & USART_CR1_RXNEIE) != RESET))
{
// 在该函数里清除相关中断标志位并调用HAL_UART_RxCpltCallback()
UART_Receive_IT(huart);
return;
}
}
// 如果发生了错误
if ((errorflags != RESET) && (((cr3its & USART_CR3_EIE) != RESET) || ((cr1its & (USART_CR1_RXNEIE | USART_CR1_PEIE)) != RESET)))
{
// pass
}
// UART发送模式。TXE:发送数据寄存器空 TXEIE:发送缓冲区空中断使能
if (((isrflags & USART_SR_TXE) != RESET) && ((cr1its & USART_CR1_TXEIE) != RESET))
{
UART_Transmit_IT(huart);
return;
}
// UART发送模式结束。TC:发送完成 TCIE:发送完成中断使能
if (((isrflags & USART_SR_TC) != RESET) && ((cr1its & USART_CR1_TCIE) != RESET))
{
// 在该函数里清除相关中断标志位并调用HAL_UART_TxCpltCallback()
UART_EndTransmit_IT(huart);
return;
}
}
HAL 库常用的回调函数如下:
__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); // 接收中止回调函数
7.6、串口接收数据
HAL 库提供 HAL_UART_Receive_IT() 用于开启以中断的方式接收指定字节。数据接收在中断处理函数里面实现。其声明如下:
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
形参 huart 是 UART_HandleTypeDef 结构体指针类型的串口句柄。形参 pData 是要接收的数据地址。形参 Size 是要接收的数据大小,以字节为单位。当接收到 Size 个字节之后,会执行对应的中断接收完成回调函数。
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
7.7、串口发送数据
HAL 库提供了以阻塞方式发送指定字节数据的函数。该函数的声明如下:
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout);
形参 huart 是 UART_HandleTypeDef 结构体指针类型的串口句柄。形参 pData 是要发送的数据地址。形参 Size 是要发送的数据大小,以字节为单位。形参 Timeout 设置超时时间,以毫秒为单位。
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
八、原理图
通过原理图,我们看出 USART1 的 TXD 连接到 PA9 引脚,RXD 连接到 PA10 引脚,工作模式为复用推挽输出。
九、程序源码
USART1 初始化函数内容如下:
UART_HandleTypeDef g_usart1_handle; // USART1句柄
uint8_t g_uart_rx_buffer[1]; // HAL库使用的串口接收数据缓冲区
/**
* @brief 串口初始化函数
*
* @param huart 串口句柄
* @param UARTx 串口寄存器基地址
* @param band 波特率
*/
void UART_Init(UART_HandleTypeDef *huart, USART_TypeDef *UARTx, uint32_t band)
{
huart->Instance = UARTx; // 寄存器基地址
huart->Init.BaudRate = band; // 波特率
huart->Init.WordLength = UART_WORDLENGTH_8B; // 数据位
huart->Init.StopBits = UART_STOPBITS_1; // 停止位
huart->Init.Parity = UART_PARITY_NONE; // 奇偶校验位
huart->Init.Mode = UART_MODE_TX_RX; // 收发模式
huart->Init.HwFlowCtl = UART_HWCONTROL_NONE; // 硬件流控制
huart->Init.OverSampling = UART_OVERSAMPLING_16; // 过采样
HAL_UART_Init(huart);
HAL_UART_Receive_IT(huart, (uint8_t *)g_uart_rx_buffer, 1); // 开启接收中断
}
USART1 底层初始化函数如下:
/**
* @brief 串口底层初始化函数
*
* @param huart 串口句柄
*/
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if (huart->Instance == USART1) // 初始化的串口是否是USART1
{
__HAL_RCC_USART1_CLK_ENABLE(); // 使能USART1时钟
__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能对应GPIO的时钟
// PA9 -> USART TXD
GPIO_InitStruct.Pin = GPIO_PIN_9; // USART1 TXD的引脚
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 推挽式复用
GPIO_InitStruct.Pull = GPIO_NOPULL; // 不使用上下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 输出速度
GPIO_InitStruct.Alternate = GPIO_AF7_USART1; // 复用功能
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// PA10 -> USART RXD
GPIO_InitStruct.Pin = GPIO_PIN_10; // USART1 RXD的引脚
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_NVIC_EnableIRQ(USART1_IRQn); // 使能USART1中断
HAL_NVIC_SetPriority(USART1_IRQn, 2, 0); // 设置中断优先级
}
}
该函数主要实现底层的初始化,事实上这个函数的代码还可以直接放到 USART1_Init()
函数里面,但是 HAL 库为了代码的功能分层初始化,定义这个函数方便用户使用。所以我们也按照 HAL 库的这个结构来初始化外设。这个函数首先是调用 if(huart->Instance == USART1)
判断是要初始化那个串口是否是 USART1,因为每个串口初始化都会调用 HAL_UART_MspInit()
这个函数,所以需要判断是哪个串口要初始化才做相应的处理。
首先就是使能串口以及 PA9 和 PA10 的时钟,PA9 和 PA10 需要用做复用功能,复用功能模式有两个选择:GPIO_MODE_AF_PP
推挽式复用和 GPIO_MODE_AF_OD
开漏式复用,我们选择的是推挽式复用。然后,我们需要将 PA9 和 PA10 的复用功能配置为 ,GPIO_AF7_USART1
。然后就是调用 HAL_GPIO_Init()
函数进行 IO 口的初始化。
串口中断服务函数内容如下:
/**
* @brief USART1中断服务函数
*
*/
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&g_usart1_handle); // 调用HAL库公共处理函数
HAL_UART_Receive_IT(&g_usart1_handle, (uint8_t *)g_uart_rx_buffer, 1); // 再次开启接收中断
}
从代码逻辑可以看出,在中断服务函数内部通过调用接收回调函数 HAL_UART_RxCpltCallback()
进行处理。然后,再调用 UART_Receive_IT()
函数重新开启中断。UART_Receive_IT()
函数的作用就是把每次中断接收到的字符保存在串口句柄的缓存指针 g_usart_rx_buffer
中,同时每次接收一个字符,其计数器 RxXferCount
减 1,直到接收完成 RxXferSize
个字符之后 RxXferCount
设置为 0。这里,我们直接设置了串口句柄成员变量 RxXferSize
为 1。
重写串口接收回调函数:
#define UART_RECEIVE_LENGTH 200
uint8_t g_usart1_rx_buffer[UART_RECEIVE_LENGTH]; // 接收数据缓冲区
uint16_t g_usart1_rx_status = 0; // 接收状态标记
/**
* @brief USART接收中断回调函数
*
* @param huart 串口句柄
*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
if ((g_usart1_rx_status & 0x8000) == 0) // 接收未完成
{
if (g_usart1_rx_status & 0x4000) // 接收到了0x0D,即回车键
{
if (g_uart_rx_buffer[0] != 0x0A) // 接收到的不是0x0A,即不是换行符
{
g_usart1_rx_status = 0; // 接收错误,重新开始
}
else // 接收到的是0x0A,即换行符
{
g_usart1_rx_status |= 0x8000; // 接收完成
}
}
else // 还没接收到0x0D,即还没接收到回车键
{
if (g_uart_rx_buffer[0] == 0x0D) // 接收到的是0x0D,即回车键
{
g_usart1_rx_status |= 0x4000;
}
else // 如果没有接收到回车
{
// 接收到的数据存入接收缓冲区
g_usart1_rx_buffer[g_usart1_rx_status & 0x3FFF] = g_uart_rx_buffer[0];
g_usart1_rx_status++;
if (g_usart1_rx_status > (UART_RECEIVE_LENGTH - 1)) // 接收到数据大于接收缓冲区大小
{
g_usart1_rx_status = 0; // 接收数据错误,重新开始接收
}
}
}
}
}
}
因为我们设置了串口句柄成员变量 RxXferSize
为 1,那么每当 USART1 接收到一个字符后触发接收完成中断,便会在中断服务函数中引导执行该回调函数。当串口接受到一个字符后,它会保存在缓存 g_usart_rx_buffer
中,由于我们设置了缓存大小为 1,而且 RxXferSize=1
,所以每次接受一个字符,会直接保存到 g_usart_rx_buffer[0]
中,我们直接通过读取 g_usart_rx_buffer[0]
的值就是本次接收到的字符。
这里我们设计了一个简单的接收协议:通过这个函数,配合一个数组 g_usart1_rx_buffer
,一个接收状态标志位 g_usart1_rx_status
实现对串口数据的接收管理。数组 g_usart1_rx_buffer
的大小由 USART_RECEIVE_LENGTH
定义,也就是一次接收的数据最大不能超过 USART_RECEIVE_LENGTH
个字节。其中,g_usart1_rx_status
的位 15 用来表示接收完成标志,位 14 用来表示接收到 0x0D 标志,位 0 ~ 13 用来表示接收到的有效字节个数。
当接收到从电脑发过来的数据,把接收到的数据保存在数组 g_usart_rx_buffer
中,同时在接收状态标志位(g_usart1_rx_status
)中计数接收到的有效数据个数,当收到回车(回车的表示由 2 个字节组成:0x0D 和 0x0A)的第一个字节 0x0D 时,计数器将不再增加,等待 0x0A 的到来,而如果 0x0A 没有来到,则认为这次接收失败,重新开始下一次接收。如果顺利接收到 0x0A,则标记 g_usart1_rx_status
的第 15 位,这样完成一次接收,并等待该位被其他程序清除,从而开始下一次的接收,而如果迟迟没有收到 0x0D,那么在接收数据超过 USART_RECEIVE_LENGTH
的时候,则会丢弃前面的数据,重新接收。
如果想要使用 printf() 函数,我们需要重写 _write()
函数实现 printf() 函数。
/**
* @brief 重写_write使用printf()函数
*
* @param fd 一个非负整数,代表要写入数据的文件或设备的标识
* @param ptr 一个指向字符数据的指针,即要写入的数据的起始位置
* @param length 一个整数,表示要写入的数据的字节数
* @return int 数据的字节数
*/
int _write(int fd, char *ptr, int length)
{
HAL_UART_Transmit(&g_usart1_handle, (uint8_t *)ptr, length, 0xFFFF); // g_usart1_handle是对应串口
return length;
}
注意:不同的编译器实现 printf() 函数要重写的函数不同;
默认情况下,只有一个串口可以使用 priintf() 函数。如果,我们想要多个串口使用 printf() 函数,可以通过如下方法:
/**
* @brief 多串口使用printf()函数
*
* @param huart 串口句柄
* @param fmt 格式化字符串
* @param ... 格式化参数
*/
void UART_Printf(UART_HandleTypeDef *huart, char *fmt, ...)
{
char buffer[UART_RECEIVE_LENGTH + 1]; // 用来存放转换后的数据
uint16_t i = 0;
va_list args;
va_start(args, fmt);
vsnprintf(buffer, UART_RECEIVE_LENGTH + 1, fmt, args); // 将格式化字符串转换为字符数组
i = (strlen(buffer) > UART_RECEIVE_LENGTH) ? UART_RECEIVE_LENGTH : strlen(buffer);
HAL_UART_Transmit(huart, (uint8_t *)buffer, i, 1000); // 串口发送数据
va_end(args);
}
main() 函数内容如下:
int main(void)
{
uint32_t length = 0;
HAL_Init();
System_Clock_Init(8, 336, 2, 7);
Delay_Init(168);
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
UART_Init(&g_usart1_handle, USART1, 115200);
printf("这是一个串口测试程序!\r\n");
while (1)
{
if (g_usart1_rx_status & 0x8000) // 接收到了数据
{
printf("接收到数据:\r\n");
length = g_usart1_rx_status & 0x3FFF;
HAL_UART_Transmit(&g_usart1_handle, (uint8_t *)g_usart1_rx_buffer, length, 1000); // 发送数据
while (__HAL_USART_GET_FLAG(&g_usart1_handle, USART_FLAG_TC) != SET); // 等待发送完成
printf("\r\n");
g_usart1_rx_status = 0;
}
}
return 0;
}