2.5 UART 通信
理论知识
物理信号
硬件电路引脚
- 实现模块点对点通信
- 在两模块均已供电的情况下,只需 3 条线即可(Rx,Tx,GND)
- Rx,Receiver,接收端
- Tx,Transmitter,发送端
- GND,Ground,接地端:两模块需共地,以区分 Rx & Tx 线上的电平高低
- 部分具有流控策略的设备还需 RTS & CTS 两根线
接地方式
设备一 | 设备二 |
---|---|
RXD | TXD |
TXD | RXD |
GND | GND |
信号与电平格式
- 传输 0/1 码元
- 高电平为 1(3.3V
低电平为 0(0V)) ,
比特率与波特率
- 比特率(bit/s,bps
每秒钟传输二进制码元数量) : - 波特率(symbol/s ):每秒钟传输有效信息码元的数量
- UART 通信中,比特率 = 波特率
- 例 十六进制字符编码中,如果一秒传输 1000 个字符,每个字符对应一种唯一的电平信号,那么波特率就是 1000symbol/s;由于一个十六进制字符要用四位二进制编码表示,那么它的比特率就是 4000bit/s
异步通信(Async)
不需等待某一方时钟信号同步后再发送,但通信内容较长时可能会因时钟不统一而错位。
如何解决错位问题呢?
可通过多次采样发现甚至解决错位问题(常见采样次数为 8 和 16)
* 对于 USART(即 UART & SART)通信,这种 SART 具有时钟线同步策略。
收发模式
仅接受、仅发送、收发双向。
链路传输
UART 协议
- 点到点通信
- 仅有校验与帧格式
- 没有地址或 ID 的概念
- 没有冲突检测、避免和仲裁的说法
- 没有地址便没有滤波器与掩码
误码校验
Why?
减少硬件偶然错误的信息编码被解析利用
但仍可能存在强干扰化境导致信道电压波动而改变电平
如何实现?
奇偶校验算法
- 对已发送的一段比特流进行计算,统计 1 的个数决定 1bit 的校验位
- 偶数个 1 为 0,奇数个 1 为 1,则是偶校验
- 奇数个 1 为 0,偶数个 1 为 1,则是奇校验
- 等效于异或操作,对前面的比特流进行异或
- 异或结果直接作为校验位,是偶校验
- 异或结果取反作为校验位,是奇校验
- 例 01010101,偶校验位为 0,奇校验位为 1
UART 帧数据格式
分段 | 长度 bit | 备注 |
---|---|---|
起始位 | 1 | 标志帧起始,必须为低电平 0 |
数据位 | 5~8 | 先发低位后发高位,一般常见的都是 8 位,也就是一个字节。也有 7 位的,相当于发 ASCII 码 |
校验位 | 1 | 将数据位通过奇偶校验算法得到的值放入该位 |
停止位 | 1 | 标志帧结束,必须为高电平 1 |
空闲位 | % | 链路上不传输任何信息,必须为高电平 1 |
通信方式
阻塞式通信
发送一段数据,在没有发送完所有数据之前,一直停留在此发送函数(可设定阻塞时间
中断式通信
发送数据的过程在中断中进行,这个中断实际上是发送数据寄存器空的中断。
比如说你要发送 10 个字节的数据,那么这 10 个字节肯定不会一次性发送完,只能是一个一个字节地发送。每发送一个字节,发送数据寄存器就会有一个为空的标志,以提示可以发送下一个字节了,这样在发送数据的过程中 CPU 不会一直在等待,而是用中断来提示需要进行发送的时候再发送。不影响主程序进行。
DMA 中断通信
DMA(Direct Memory Access,直接内存访问)是一种计算机系统中用于数据传输的机制。它允许数据在外设和内存之间直接传输,而不需要 CPU 的介入,从而减轻了 CPU 的负担,提高了数据传输的效率。
举个栗子:
想象一下我们搬家的场景:你要把家里的一些东西从旧房子搬到新房子。在传统的情况下,你可能要亲自搬每一箱东西,把它们从旧房子搬到新房子。这就相当于 CPU 传统地处理数据传输的方式。 现在,有一支搬家队,他们专门负责搬家。你只需要告诉他们从哪里搬,搬到哪里,然后他们就会自己完成这项任务。而你可以利用这段时间去做其他事情,不需要亲自动手。这就有点类似于 DMA 的工作原理。 在计算机中,CPU 通常会处理数据的传输工作,就像你亲自搬家一样。但有了 DMA,就好比有了一支专门负责数据传输的队伍。CPU 只需要告诉 DMA 从哪里搬,搬到哪里,然后就可以去处理其他任务了。DMA 负责在外设和内存之间直接传输数据,而不需要 CPU 一直参与。
简而言之,DMA 就像是一支搬家队伍,负责在不需要 CPU 亲自操劳的情况下完成数据传输任务,从而提高了系统的效率。
该内容摘抄自 CSDNTM32CubeMx+HAL 库:USART 串口收发数据的三种方式
空闲中断
当单片机的 UART 歇下来的时候,会把之前连续接收的内容自动拼装成一段有效的信息存到缓冲区中
DMA 空闲中断
代码示例
阻塞式
- 配置 UART 通信模块 USART1
- 设置 USART1 为 asynchronous 模式
- 设置波特率为 115200
- 设置数据长度为 8bit
- 不需要奇偶校验
- 一位停止位
- 收发双向通信
// 声明变量以存储UART的收发数据
uint8_t RxData[10];
uint8_t TxData[10] = "test";
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5);
HAL_Delay(500);
HAL_UART_Transmit(&huart1, TxData, 3, 1000);
HAL_UART_Receive(&huart1, RxData, 3, 1000);
if (RxData[0] == '1') {
HAL_GPIO_TogglePin(GPIOE, GPIO_PIN_5);
RxData[0] = 0;
}
中断式
- 注意要配置 使能 USART1 global interrupt
- 主函数负责发送,中断回调函数负责就收
- 随意收发长数据
// 声明变量以存储UART的收发数据
uint8_t RxData[10];
uint8_t TxData[10] = "test";
while (1) {
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5);
HAL_UART_Transmit_IT(&huart1, TxData, 5);
HAL_Delay(1000);
}
// 定义中断实函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
uint8_t ReceiveData[10] = "okkk";
if (huart == &huart1) {
HAL_UART_Transmit_IT(&huart1, ReceiveData, 5);
HAL_UART_Receive_IT(&huart1, RxData, 5);
if (RxData[0] == '1') {
HAL_GPIO_TogglePin(GPIOE, GPIO_PIN_5);
RxData[0] = 0;
}
}
}
空闲中断
- 实现 UART 随意发送定长数据,可接收一定长度以内的数据
- 主函数只负责发送,中断回调函数负责接收
代码在 中断式 的程序基础上修改
HAL_UART_Receive_IT(&huart1,ExData,5);
换成HAL_UARTEx_ReceiveToIdle_IT(&huart1, RxData, 5);
- 中断回调函数名称改为
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
DMA 中断
配置 USART 的 DMA 中断
- Cube 使能 UART 中断
- 添加发送 DMA
- 添加接收 DMA
- DMA 选项
- 普通模式:支持定时搬运或代码主动调用搬运
- 循环模式:一直搬运
- 搬运时,内存需要自增地址,否则一段话会搬运到同一个地方去造成覆盖
- FIFO(First In First Out)
- 当接收信息太多时,DMA 可能搬不过来。FIFO 队列可以先把来不及搬运的内容缓存下来排队,一个一个让 DMA 搬运,但代价是让信息的时效性降低
- 代价比较:信息丢失 vs 信息时效性差,根据这个来决定是否使用 FIFO 以及 FIFO 的长度
实现 UART 随意收发定长数据
主函数只负责发送,中断回调函数负责接收
代码在 中断式 的基础上进行修改(即 将 IT 换为 DMA)
- 将
HAL_UART_Transmit_IT(&huart1, TxData, 5);
换为HAL_UART_ Transmit_DMA(&huart1, TxData, 5);
- 将
HAL_UART_Receive_IT(&huart1, RxData, 5);
换成HAL_UART_Receive_DMA(&huart1, RxData, 5);
DMA 空闲中断
- UART 随意收发定长数据,可接收一定长度内的数据
- 主函数只负责发送,中断回调函数负责接收
代码在 DMA 中断 的基础上进行修改
- 将
HAL_UART_Receive_DMA(&huart1, TxData, 5);
换为HAL_UARTEx_ReceiveToIdle_DMA(&huart1, TxData, 5);
- 将中断回调函数名改为
HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
串口查询函数
void HAL_UART_GetState(); //判断UART的接收是否结束,或者发送数据是否忙碌
食用方法:
while (HAL_UART_GetState(&huart1) ==
HAL_UART_STATE_BUSY_TX) // 检测UART1发送结束
我们可以查看一下 void HAL_UART_GetState();
的定义:
HAL_UART_StateTypeDef HAL_UART_GetState(UART_HandleTypeDef *huart) {
uint32_t temp1 = 0x00U, temp2 = 0x00U;
temp1 = huart->gState;
temp2 = huart->RxState;
return (HAL_UART_StateTypeDef)(temp1 | temp2);
}
让我们深入查看一下 HAL_UART_StateTypeDef
的定义,包含如下状态:
HAL_UART_STATE_RESET:UART外设已重置但未初始化。
HAL_UART_STATE_READY:UART外设已初始化并且可以进行数据传输。
HAL_UART_STATE_BUSY:UART外设正在进行数据传输中。
HAL_UART_STATE_BUSY_TX:UART外设正在进行数据发送中。
HAL_UART_STATE_BUSY_RX:UART外设正在进行数据接收中。
HAL_UART_STATE_BUSY_TX_RX:UART外设同时正在进行数据发送和接收中。
HAL_UART_STATE_TIMEOUT:UART外设传输超时。
HAL_UART_STATE_ERROR:UART外设传输错误。
总结
- 阻塞式
HAL_UART_Transmit(&huart1, TxData, 5, 1000);
HAL_UART_Receive(&huart1, RxData, 5, 1000);
- 中断式(使能中断)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){}
HAL_UART_Transmit_IT(&huart1, TxData, 5);
HAL_UART_Receive_IT(&huart1, RxData, 5);
- 空闲中断(使能中断)
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size){}
HAL_UART_Transmit_IT(&huart1, TxData, 5);
HAL_UARTEx_ReceiveToIdle_IT(&huart1, RxData, 5);
- DMA 中断(配置 DMA 且使能中断)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){}
HAL_UART_Transmit_DMA(&huart1, TxData, 5);
HAL_UART_Receive_DMA(&huart1, RxData, 5);
- DMA 空闲中断(配置 DMA 且使能中断)
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size){}
HAL_UART_Transmit_DMA(&huart1, TxData, 5);
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, RxData, 5);