3、UART串口通信:异步串行原理、波特率与帧格式、硬件流控与实战配置
UART,全称是Universal Asynchronous Receiver/Transmitter。说白了,就是通用异步收发器。
这玩意儿,是嵌入式世界里最古老、最基础、也最常用的通信接口之一。我入行那会儿,调试板子的第一件事,就是把UART的TX和RX引出来,接个USB转串口工具,看打印信息。没有它,你就像瞎子摸象,根本不知道芯片跑没跑起来。
今天,我们就把它彻底聊透。
3.1 异步串行原理:没有时钟线,怎么同步?
UART是异步通信。什么叫异步?就是通信双方没有单独的时钟线。
你想想看,没有时钟线,接收方怎么知道什么时候该采样数据?
答案是:靠约定好的波特率,以及起始位和停止位。
UART的物理层只有两根线:TX(发送)和RX(接收)。当然,还有GND共地。没有GND,信号参考电平都不对,通信肯定失败。我在项目中遇到过好几次,新手把TX、RX接对了,但忘了共地,结果数据全是乱码。
UART的数据帧格式是这样的:
- 空闲状态:总线保持高电平。
- 起始位:一个低电平脉冲,告诉接收方“我要开始发数据了”。
- 数据位:5到9位,通常是8位。先发LSB(最低有效位)。
- 校验位:可选,奇校验或偶校验,用于简单的错误检测。
- 停止位:1位、1.5位或2位的高电平,表示一帧结束。
接收方呢?它一直在检测RX线上的电平变化。一旦检测到起始位(高电平变低电平),就开始按约定的波特率,在每个数据位的中间点采样。这样,即使没有时钟线,也能把数据正确读出来。
核心要点:异步通信的关键是“起始位同步”。双方必须提前约定好波特率、数据位、校验位和停止位。任何一个参数不匹配,通信就会失败。
3.2 波特率:通信的“节拍器”
波特率,单位是bps(bits per second),表示每秒传输的比特数。
常见的波特率有:9600、19200、38400、115200、921600等。
为什么是这些数字?因为它们都是整数分频出来的。比如,一个16MHz的晶振,要得到115200的波特率,需要分频到多少?
计算公式很简单:
波特率 = 系统时钟频率 / (16 * 分频系数)
这里的16,是因为UART模块内部通常会用16倍波特率的时钟来采样。每个数据位采样16次,取中间3次的值做多数表决,这样可以有效抗干扰。
我个人的习惯是:调试阶段用115200,因为速度快,打印信息不卡顿。产品量产时,如果线缆长、环境干扰大,我会降到9600或19200,保证通信的可靠性。
避坑指南:我曾经在一个项目中,用了921600的波特率,结果线缆稍微长一点(超过30厘米),数据就开始丢包。后来查了半天,发现是信号反射和衰减导致的。降速到115200后,问题解决。所以,波特率不是越高越好,要看你的物理链路质量。
3.3 帧格式详解:数据位、校验位、停止位
我们来看一个典型的UART帧:
| 起始位 | 数据位(LSB先) | 校验位 | 停止位 |
|---|---|---|---|
| 1位(低电平) | 8位(0或1) | 1位(可选) | 1位(高电平) |
举个例子,发送0x55(二进制01010101),LSB先发,所以实际发送的顺序是:1 0 1 0 1 0 1 0。
加上起始位(0)和停止位(1),一帧总共10位。如果波特率是9600,那么发送一个字节的时间就是:10 / 9600 ≈ 1.04毫秒。
校验位有两种:
- 奇校验:数据位和校验位中,1的总数为奇数。
- 偶校验:数据位和校验位中,1的总数为偶数。
校验位只能检测奇数个位的错误,如果两个位同时翻转,校验位是检测不出来的。所以,它只适用于对可靠性要求不高的场景。
3.4 硬件流控:RTS/CTS到底要不要用?
硬件流控,就是通过额外的两根线——RTS(请求发送)和CTS(清除发送)——来控制数据流。
它的作用很简单:当接收方的缓冲区快满时,拉低RTS,告诉发送方“先别发了,我消化一下”。发送方检测到CTS被拉低,就会暂停发送。
什么时候需要用硬件流控?
- 高速通信(比如1Mbps以上)
- 接收方处理速度慢,容易丢数据
- 通信双方没有软件协议来管理数据流
但说实话,在大多数嵌入式场景中,我们很少用硬件流控。为什么?
- 多两根线,增加成本和布线复杂度
- 很多MCU的UART模块不支持硬件流控
- 通过软件协议(比如XON/XOFF)或简单的应答机制,就能解决大部分问题
注意:如果你决定用硬件流控,一定要确保双方的RTS和CTS交叉连接。即:A的RTS接B的CTS,A的CTS接B的RTS。我见过有人把RTS和RTS直接连在一起,结果通信完全不通。
3.5 实战配置:以STM32为例
好了,理论说完了,我们来看实战。以STM32F103为例,配置UART1,波特率115200,8位数据位,1位停止位,无校验。
第一步:初始化GPIO
// PA9 - TX, PA10 - RX
GPIO_InitTypeDef GPIO_InitStructure = {0};
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
// TX: 推挽复用输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// RX: 浮空输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
第二步:配置UART参数
USART_InitTypeDef USART_InitStructure = {0};
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_Init(USART1, &USART_InitStructure);
USART_Cmd(USART1, ENABLE);
第三步:发送和接收
// 发送一个字节
void UART_SendByte(uint8_t data) {
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, data);
}
// 接收一个字节(阻塞)
uint8_t UART_ReceiveByte(void) {
while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
return USART_ReceiveData(USART1);
}
嗯,这里要注意:发送时检查TXE(发送数据寄存器空)标志,接收时检查RXNE(接收数据寄存器非空)标志。这是最基础的轮询方式。
实际项目中,我一般会用中断或DMA来收发,避免CPU空等。比如,用中断接收,每收到一个字节就触发一次中断,把数据放到环形缓冲区里。这样,主循环可以专心做其他事,不会因为等待串口数据而卡死。
个人经验:我曾经在一个多传感器数据采集的项目中,用了4个UART同时工作。如果每个都用轮询方式,CPU根本忙不过来。后来全部改成DMA+环形缓冲区,CPU占用率从80%降到了15%。所以,对于多串口或高速通信,DMA几乎是必须的。
3.6 常见问题与调试技巧
最后,分享几个我调试UART时常用的方法:
- 逻辑分析仪:这是最直观的工具。把TX和RX接到逻辑分析仪上,看波形。起始位、数据位、停止位一目了然。如果波形不对,先查波特率设置。
- 串口助手:PC端的串口调试工具,比如SSCOM、Putty。发送0x55或0xAA,看接收到的数据对不对。0x55是01010101,0xAA是10101010,这两个数据能帮你快速判断位序和波特率是否匹配。
- 回环测试:把TX和RX短接,然后发送数据。如果接收到的数据和发送的一致,说明UART硬件没问题。如果不一致,检查GPIO配置和时钟。
好了,UART串口通信就讲到这里。下一章,我们会聊SPI总线,那又是一个完全不同的世界。到时候见。