串口收发
# 什么是串口
串口是一种在单片机,传感器,执行模块等诸多设备上常用的通讯接口,在比赛中,可以通过串口读取遥控器发送来的数据,也可以通过串口读取超声波等传感器的数据,也可以使用串口在单片机和运行计算机视觉的电脑之间进行通讯
在此章节中,我们将只讨论异步通信 UART,不需要对外提供时钟输出
# 串口的工作
# 接收信号的形式
虽然串口通讯有很多协议,但是经过解码后最终物理传输形式还是 TTL 电平,也就是通过高低电位表示0与1,所以在单片机与 PC 通讯时,需要一个 USB to TTL 的转换器
串口的通讯协议由开始位,数据位,校验位,结束位构成。一般以一个低电平作为一帧数据的起始,接着跟随8位或者9位数据位,之后为校验位,分为奇校验,偶校验和无校验,最后以一个先高后低的脉冲表示结束位,长度可以设置为0.5,1,1.5或2位长度,只要双方约定一致即可
奇偶校验位的原理是统计发送数据中高电平即1
数量的奇偶,将结果记录在奇偶校验位中发送给接收方,接收方收到奇偶校验位后和自己收到的数据进行对比,如果奇偶性一致就接受这帧数据,否则认为这帧数据出错
数据帧内容 | 长度 | 功能 |
---|---|---|
起始位 | 1位 | 标志 帧 的开始 |
数据位 | 8或9位 | 传输的数据 |
奇偶校验位 | 1位奇校验/偶校验,或无校验位 | 校验本帧是否正确 |
停止位 | 0.5、1、1.5或2位 | 标志 帧 的结束 |
提示
一个传输的“帧”就是一次信号的传输,包括开始位,数据位,校验位,结束位,可能就是传输了一个字节 8 bit 的内容
# 波特率
一般进行串口通讯时,收发双方要保证遵守同样的协议才能正确的完成收发,除了协议要一致之外,还有一个非常重要的要素要保持一致,那就是通讯的速率,即波特率。波特率是指发送数据的速率,单位为波特每秒,一般串口常用的波特率有115200
,38400
,9600
等。串口的波特率和总线时钟周期(clock)成倒数关系,即总线时钟周期越短,单位时间内发送的码元数量越多,串口波特率就越高
波特率一般有双方约定,但是将程序写入设定的波特率后,储存在寄存器中表示速率的数字却不是这个数字。因为存储这个值的寄存器,只是有16 bit 空间留给存储速率。所以算法是这样:
首先需要明确串口所在的总线的外设时钟频率fPCLKx
。我们需要知道的是,在串口的 USART_BRR
寄存器上的一个16 bit 的值USARTDIV
前12 bit 存储着一个数值的整数部分,后4 bit 则是其小数部分
`USARTDIV`的16 bit分配
前12 bit 是这个数的整数部分,也就是3位16进制数的范围
而后4 bit的表示方法是:将数字的小数部分乘16,随后向下取整,由于这个数一定小于16,所以1位16进制数是完全够用的
所以公式就是:
由于采取了一些近似手段,所以通过寄存器产生的波特率和最终期望的波特率之间是存在误差的,一般较小的误差不会影响最终的串口通讯,但是如果通讯时出现问题的话,要记得检查是否与通讯速率的实际值与理想值相差较大有关
# 硬件层面
我们从 HAL 库的句柄结构体入手:
typedef struct {
USART_TypeDef *Instance; //UART 寄存器基地址
UART_InitTypeDef Init; //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 接收计数器
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_HandleTypeDef;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
只重点讲到第一个:*Instance
也就是基地址,进入这个结构体之后会有:
typedef struct
{
__IO uint32_t SR; // USART Status register
__IO uint32_t DR; // USART Data register
__IO uint32_t BRR; // USART Baud rate register
__IO uint32_t CR1; // USART Control register 1
__IO uint32_t CR2; // USART Control register 2
__IO uint32_t CR3; // USART Control register 3
__IO uint32_t GTPR; // USART Guard time and prescaler register
} USART_TypeDef;
2
3
4
5
6
7
8
9
10
USART 数据寄存器(USART_DR)只有低 9 位有效,并且第 9 位数据是否有效取决于 USART 控制寄存 1(USART_CR1)的 M 位设置,当 M 位为 0 时表示 8 位数据字长,当 M 位为 1 表示 9 位数据字长,我们一般使用 8 位数据字长。
USART_DR 包含了已发送的数据或者接收到的数据。USART_DR 实际是包含了两个寄存器,一个专门用于发送的可写 TDR,另一个专门用于接收的可读 RDR。 当需要发送数据时,内核或 DMA 外设会把数据从内存写入到发送数据寄存器 TDR。因为 TDR 和 RDR 都是介于系统总线和移位寄存器之间,发送时,TDR 的数据转移到发送移位寄存器,然后从移位寄存器一位一位地发送出去。 接收数据就是一个逆过程,数据一位一位地输入接收移位寄存器,然后转移到RDR,最后使用内核指令或 DMA 读取到内存中
随后,HAL 库还有一个外设的初始化结构体:
typedef struct {
uint32_t BaudRate; //波特率
uint32_t WordLength; //字长
uint32_t StopBits; //停止位
uint32_t Parity; //校验位
uint32_t Mode; //USART 模式
uint32_t HwFlowCtl; //硬件流控制
uint32_t OverSampling; //过采样
} UART_InitTypeDef;
2
3
4
5
6
7
8
9
详细内容请参考书籍
# 实践操作
# CubeMX
首先,配置 USART,打开此标签,将USART1
设为Asynchronous
异步通信:
随后,将此串口的配置改为:波特率为115200,数据帧设置为8位数据位,无校验位,1位停止位
需要注意的是,这些设置也都是初值,在程序中可以通过修改设置这些的结构体来改变这些值。
最后在 NVIC 中使能USART1
的中断:
最后配置你想要的其他以及将时钟改为最高速率即可
# 接收中断与空闲中断
这两种中断只是串口状态能引发的中断条件之二
接收中断 | 空闲中断 | |
---|---|---|
处理函数 | USARTx_IRQHandler | USARTx_IRQHandler |
回调函数 | HAL_UART_RxCpltCallback | HAL库没有提供 |
USART状态寄存器中的位 | UART_FLAG_RXNE | UART_FLAG_IDLE |
触发条件 | 完成一次数据的接收之后触发一次中断 | 串口接收完一个字符串的数据后又过了一个字节的时间没有接收到任何数据 |
关系是这样:每当串口完成一次接收(传输完HAL_UART_Receive_IT
约定长度的内容)之后触发一次中断,这就是串口中断。STM32中相应的中断处理函数为USARTx_IRQHandler
,在这个函数中会调用函数HAL_UART_IRQHandler
去完成一中断的系列配置工作以及判断中断类型(不包括空闲中断),随后转入回调函数HAL_UART_RxCpltCallback
完成中断内容
可以通过USART状态寄存器中的UART_FLAG_RXNE
位判断 USART 是否发生了接收中断;也可以通过USART状态寄存器中的UART_FLAG_IDLE
判断是否发生了空闲中断。接收中断是接受一个帧后引发的中断。
而空闲中断可以这样解释:在传输内容基本结束后,就不会再有数据传输了,所以在此后的一个字节时间内串口没有接收到信号,自动转入空闲中断。当然这是在开启空闲中断的基础上的,在有数据传输的时候,UART_FLAG_RXNE
为1,而UART_FLAG_IDLE
为0,在一个字节时间内串口没有接收到信号时,UART_FLAG_IDLE
会自动置1,进入空闲中断,进行操作可以在这里对刚刚接收到的字符串进行规整。所以这对不定长度的信息接受及其有用
注意
你可能会担心空闲中断会不会在串口“空闲”的时候一直发生,实际上是多虑的。因为当清除UART_FLAG_IDLE
标志位后,必须有接收到第一个数据后,才开始触发
# 用到的库函数
函数名(宏定义) | 作用 | 详情 |
---|---|---|
HAL_UART_Transmit | 从指定的串口发送一段数据 | 点击这里 |
HAL_UART_RxCpltCallback | 接收中断的回调函数 | 点击这里 |
HAL_UART_Receive_IT | 开启接收中断的函数 | 点击这里 |
__HAL_UART_ENABLE_IT | 使能某个串口的某种中断类型 | 点击这里 |
__HAL_UART_GET_FLAG | 读取某串口的某寄存器状态位,判断是触发哪个中断的宏定义 | 点击这里 |
__HAL_UART_CLEAR_IDLEFLAG | 清除IDLE 的中断标志位的宏定义 | 点击这里 |
# 接收中断程序
对于接收中断,HAL 库有一套完整的封装好的处理流程,也就是上面提到的过程,调用回调函数。他能做到的就是接收一个帧后触发中断,处理,回调。在这个回调中将会自动清楚中断标志位等。需要区分的一点是,上面有两个库函数(宏定义):
HAL_UART_Receive_IT
,这个函数一定要在主循环之前手动打开,或者写进 USART 的初始化函数中去。但是在发生了一次中断后,这个函数将会失效,所以你需要在中断回调函数中手动再次打开这个函数才能有下一次中断。这个函数有一个重要的参数,其最后一个参数
uint16_t Size
表示一次性接收数据的字节长度,只有当这个函数把这些个字节长度都接收到缓存区中之后才会触发中断、回调。它的机制就是在这个函数中有一个接收计数器,每当这个函数接收一个字节的数据就会减一,当它的值减为0后,就先清除中断标记,随后产生中断__HAL_UART_ENABLE_IT
,它被上面的那个函数调用开启了串口的接收中断,也就是利用它HAL_UART_Receive_IT
才能使能一次接收中断
详情请参考这篇文章 (opens new window)与这篇文章 (opens new window)
# 空闲中断程序
对于空闲中断,则 HAL 库没有提供中断的回调函数以及使能的函数,所以这些操作需要我们自己设置:
首先需要使能空闲中断:使用
__HAL_UART_ENABLE_IT
去使能空闲中断,你可以选择去stm32f4xx_hal_uart.c
文件的HAL_UART_Init()
这个初始化函数中添加这个内容,或者在while(1)
之前添加均可/*Enable the IDLE Interrupt*/ __HAL_UART_ENABLE_IT(huart,UART_IT_IDLE);
1
2然后在
stm32f4xx_it.c
中向对应的串口中断服务函数中添加判断是否为空闲中断,若是空闲中断则进入空闲中断处理函数,空闲中断处理函数是自己写的由于受串口诱发的中断都会由硬件转入
USARTx_IRQHandler
,这个函数调用HAL_UART_IRQHandler
判断中断类型(发送中断还是接收中断)来决定调用哪个函数,若是接收中断则调用UART_Receive_IT
(不是HAL_UART_Receive_IT
),这个函数再调用回调函数模仿这个逻辑,我们在
USARTx_IRQHandler
这里的时候就加一步判断:是否为空闲中断?是则转入我们自己写的回调函数,若不是则按其自己的程序运行:/* file `stm32f4xx_it.c` */ void USART1_IRQHandler(void) { /* USER CODE BEGIN USART1_IRQn 0 */ if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET) { UART_IDLECallBack(&huart1); } else { /* USER CODE END USART1_IRQn 0 */ HAL_UART_IRQHandler(&huart1); /* USER CODE BEGIN USART1_IRQn 1 */ } /* USER CODE END USART1_IRQn 1 */ }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16判断的位置除了这么用还可以使用
huart1.Instance->SR & UART_FLAG_IDLE
这样的直接访问寄存器读取状态后,使用与运算得到那在回调时,定义一个自己的回调函数即可:
void UART_IDLECallBack(UART_HandleTypeDef *huart) { /*uart1 idle processing function*/ if(huart == &huart1) { __HAL_UART_CLEAR_IDLEFLAG(&huart1); /*your own code*/ } }
1
2
3
4
5
6
7
8
9在自己写的回调函数中有很重要的一步就是清除空闲中断的标志位。由于没有针对的流程,所以这些操作都需要自己设置
详细内容参考这篇文章 (opens new window)
在实验的时候你可以在两种不同的中断状态时设置不同的LED亮起以显示程序是否起了作用
# 此外
在《Robomaster 开发板 C 型教程》中的第八章提供了另一种处理空闲中断的方法,也就是全部改写USARTx_IRQHandler
,不让它调用HAL_UART_IRQHandler
,而是由我们自己决定。所以配置项会多一点,但是在写出两个中断函数时将会更自由,示例代码地址点这里 (opens new window)
# 参考
[1] 《Robomaster 开发板 C 型教程》
[2] HAL库教程6:串口数据接收 (opens new window)
[3] STM32 利用Hal库实现UART中断处理 (opens new window)
[4] STM32 HAL库笔记(一)——串口的操作 (opens new window)