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
    • 不需要奇偶校验
    • 一位停止位
    • 收发双向通信
1
2
3
//声明变量以存储UART的收发数据
uint8_t RxData[10];
uint8_t TxData[10]="test";
1
2
3
4
5
6
7
8
9
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
  • 主函数负责发送,中断回调函数负责就收
  • 随意收发长数据
1
2
3
//声明变量以存储UART的收发数据
uint8_t RxData[10];
uint8_t TxData[10]="test";
1
2
3
4
5
while(1) {
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_5);
HAL_UART_Transmit_IT(&huart1, TxData, 5);
HAL_Delay(1000);
}
1
2
3
4
5
6
7
8
9
10
11
12
//定义中断实函数
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的接收是否结束,或者发送数据是否忙碌

食用方法:

1
while(HAL_UART_GetState(&huart1) == HAL_UART_STATE_BUSY_TX) //检测UART1发送结束

我们可以查看一下 void HAL_UART_GetState(); 的定义:

1
2
3
4
5
6
7
8
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 的定义,包含如下状态:

1
2
3
4
5
6
7
8
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);