作为嵌入式开发者,你肯定在各种项目中接触过串口通信。当我们需要让微控制器与传感器、GPS 模块甚至 PC 机进行对话时,UART(Universal Asynchronous Receiver/Transmitter,通用异步收发传输器)往往是首选方案。尽管现代通信技术日新月异,但 UART 凭借其简洁、可靠和通用的特性,依然牢牢占据着嵌入式系统接口的核心地位。
在这篇文章中,我们将深入探讨 UART 协议的方方面面。我们将不仅仅停留在“是什么”,更会通过实际的代码示例和硬件连接逻辑,去理解“怎么做”以及“为什么这么做”。我们会剖析异步通信背后的时钟同步奥秘,对比串行与并行的优劣,并提供一些实用的代码片段和调试技巧,帮助你在实际项目中避开常见的坑。
UART 是什么?不仅仅是名字的缩写
让我们先从名字本身来拆解 UART 的核心功能,这有助于我们理解其本质:
- U (Universal,通用):意味着这是一个被广泛接受的标准。几乎所有的微控制器(无论是 Arduino、STM32 还是 ESP32)和计算机都支持这种协议,它就像是数字世界的“普通话”。
- A (Asynchronous,异步):这是 UART 与 I2C、SPI 等同步协议最大的不同点。在 UART 通信中,不需要一根专门的时钟线来“打拍子”。发送方和接收方各自独立工作,依靠事先约定的节奏来沟通。
- R (Receiver,接收器) & T (Transmitter,发送器):这是通信的物理通道。
因此,简单来说,UART 就是一种不需要时钟信号的串行数据通信协议。正如我们之前提到的,“异步”是它最显著的特征,这也意味着我们在使用它时必须更加细心地处理时序问题。
硬件连接:简单的两根线,背后的交叉逻辑
UART 通信的基础非常简单,物理层上最少只需要两根线(地线 GND 是必须的,但通常不计入信号线):
- TX (Transmit):发送端
- RX (Receive):接收端
这里有一个新手常犯的错误:直接将两个设备的 TX 连 TX,RX 连 RX。千万不要这样做! UART 的通信逻辑是交叉连接的。
- 设备 A 的 TX 必须连接到 设备 B 的 RX。
- 设备 B 的 TX 必须连接到 设备 A 的 RX。
想象一下两个人对话,一个人的嘴巴(TX)必须对着另一个人的耳朵(RX)。
#### 为什么选择串行而不是并行?
你可能会问,既然并行通信(如 Parallel Port)一次能传 8 位甚至更多数据,为什么我们还要用一次只传 1 位的 UART 呢?
- 并行通信:速度快,但需要多条数据线(比如 8 位、16 位、32 位总线)。这不仅增加了 PCB 布局的复杂度和成本,而且随着频率提高,多条线之间的信号干扰(串扰)和时序偏移会成为噩梦。长距离传输时,并行通信的线缆既昂贵又笨重。
- 串行通信 (UART):总线复杂度极低,只需两根线。虽然单次速度受限,但对于大多数传感器数据、调试信息或控制指令来说,其带宽(通常配置为 9600 到 115200 bps 甚至更高)已经绰绰有余。
最佳实践:在现代高速应用中,我们更倾向于使用串行通信(如 USB、Ethernet 甚至更快的 SerDes 接口)来替代并行总线,因为它们在物理实现上更具可扩展性。UART 虽然速度不是最快的,但在低速、长距离和低成本的场景下,它是无可替代的王者。
核心概念:没有时钟信号,如何保持步调一致?
既然没有时钟线,接收方怎么知道什么时候读取数据是正确的呢?这就需要双方在通信开始前达成严格的“口头约定”,我们将这称为 UART 配置。如果配置不一致,数据就会变成乱码(通常称为“码砖”)。
我们必须在代码中明确以下四个参数:
#### 1. 波特率
波特率定义了数据传输的速度,即每秒传输的符号数。对于 UART 来说,通常也就是每秒传输的位数。常见的波特率有 9600、19200、38400、57600 和 115200。
关键点:通信双方必须设置为完全相同的波特率。如果设备 A 发送时是 9600 bps,而设备 B 试图以 115200 bps 读取,设备 B 读取数据的频率就会过快,导致数据错位。
#### 2. 数据长度
这是指一个数据包中有效载荷的位数。标准的配置是 8 位,这正好能容纳一个标准的字节。虽然也有 5 位、6 位或 7 位的配置(主要用于老旧的电传打字机),但在现代嵌入式开发中,我们几乎总是使用 8 位。
#### 3. 奇偶校验位
这是一种简单的错误检测机制。它可以配置为:
- 无:最常用,不进行校验。
- 偶校验:确保“1”的总数为偶数。
- 奇校验:确保“1”的总数为奇数。
#### 4. 停止位
为了表示一个数据包的结束,我们需要发送停止位。通常是 1 位,但在低速通信中,有时也配置为 1.5 位或 2 位,这能给接收方更多的时间来处理接收到的数据。
深入数据帧:数据的封装艺术
当一切配置妥当,数据是如何在线路上流动的呢?UART 使用 NRZ (Non-Return-to-Zero) 编码。简单来说,就是“高电平代表 1,低电平代表 0”,而且在一个位周期内,电平保持不变。
一个完整的 UART 数据帧结构如下:
- 空闲状态:线路保持高电平。
- 起始位:发送方将线路拉低,并持续 1 个位周期。当接收方检测到这个从高到低的跳变时,它就知道:“嘿,数据要来了!”并启动其内部定时器。
- 数据位:紧接着起始位,发送方发送 5 到 8 位数据。注意:通常是 LSB(最低有效位)先传输。例如发送 INLINECODE63d67703 (0000 1111),线路上出现的顺序是 INLINECODEf5f399c5。
- 奇偶校验位:如果启用,发送方会根据数据位计算校验位并发送。
- 停止位:数据位发送完毕后,线路被拉回高电平,持续 1 到 2 位时间。这标志着这一帧的结束,并让线路回到空闲状态,准备接收下一个起始位。
奇偶校验实战:如何发现错误?
让我们通过一个具体的例子来看看奇偶校验是如何工作的。假设我们使用 偶校验。
- 发送端:我们要发送数据字节
11100010。
* 数一数有多少个 1?这里有 4 个 1。
* 4 是偶数,所以奇偶校验位应该设为 0,以保持总数为偶数。
* 实际发送序列:0 (起始) + 11100010 (数据) + 0 (校验) + 1 (停止)。
- 接收端:收到了数据。
* 它数了数收到的 1 的个数。
* 如果传输过程中没有错误,1 的个数应该是偶数。如果是,校验通过。
* 如果在传输过程中干扰导致某一位翻转(比如变成了 INLINECODE7b439b97 变成了 INLINECODE9c6e0bd3,1 的个数变成了 5 个),接收方计算后发现 1 的个数是奇数,这与偶校验规则不符。此时,接收方就会设置一个奇偶错误标志,并丢弃该数据包。
代码示例与配置
在实际开发中,我们很少需要自己去翻转 GPIO 引脚来实现 UART,而是使用微控制器内置的硬件外设。以下是常见的配置场景。
#### 示例 1:Arduino 串口初始化
在 Arduino 中,使用 UART 非常简单,这得益于其封装良好的库。我们通常使用 Serial.begin() 来启动 UART。
// Arduino UART 初始化示例
// 我们将在 setup() 中配置波特率为 9600
// 配置数据格式为:8位数据位,无校验位,1位停止位 (8N1)
void setup() {
// 启动串口通信,波特率设为 9600
// 9600 是一个非常通用的“安全”速率,大多数调试工具都默认支持
Serial.begin(9600);
Serial.println("UART 已启动。等待数据...");
}
void loop() {
// 检查是否有数据传入
// 这是一个阻塞与非阻塞结合的例子
if (Serial.available() > 0) {
// 读取传入的字节
char incomingByte = Serial.read();
// 将接收到的字节回显给发送方
// 这是一个经典的“回显测试”方法,用于验证链路是否通畅
Serial.print("收到: ");
Serial.println(incomingByte);
}
}
#### 示例 2:C 语言下的位操作模拟 (Bit-banging)
理解底层原理最好的方式就是尝试实现它。虽然不推荐在产品代码中使用这种方式(因为会占用 CPU 资源),但在没有硬件 UART 的引脚上,或者在理解协议时,这非常有用。下面的代码展示了如何通过“软件翻转引脚”来模拟 UART 发送字节 0x41 (‘A‘)。
假设配置为:波特率 9600,8N1。
#include
#include // 假设使用 AVR 环境,或者其他提供延时函数的环境
// 定义引脚操作宏(伪代码,具体取决于硬件)
#define TX_PIN_HIGH() // ... 设置输出引脚为高电平
#define TX_PIN_LOW() // ... 设置输出引脚为低电平
// 9600 波特率对应每一位的时间是 104 微秒
const uint16_t bit_delay = 104;
void uart_send_byte(uint8_t data) {
// 1. 发送起始位 (逻辑 0)
TX_PIN_LOW();
_delay_us(bit_delay);
// 2. 发送 8 位数据位 (LSB 先发,即低位在前)
for (int i = 0; i >= 1;
}
// 3. 发送停止位 (逻辑 1)
TX_PIN_HIGH();
_delay_us(bit_delay);
// 此时线路回到空闲状态(高电平)
}
int main() {
// 初始化 TX 引脚为输出...
while(1) {
uart_send_byte(‘A‘); // 发送字符 ‘A‘
_delay_ms(1000); // 暂停 1 秒
}
}
常见问题与解决方案
在调试 UART 时,你会遇到各种各样的问题。让我们来看看如何诊断它们。
#### 1. 收到乱码
这是最常见的问题。如果在串口监视器中看到奇怪的符号(如 INLINECODE3ca626b3、INLINECODEbd07651c 或乱码):
- 原因:99% 的情况是波特率不匹配。请检查发送方和接收方的波特率设置是否完全一致。
- 解决:尝试在接收端调整波特率(9600, 115200 等),直到看到清晰的文本。
#### 2. 完全没有数据
- 硬件连接检查:使用万用表或逻辑分析仪。确认 TX 是否连接到了 RX,且共地(GND)是否连接。没有共地,信号电平就没有参考基准,通信无法进行。
- 电平标准:确认双方的电平标准是否兼容。例如,Arduino (5V) 连接 ESP32 (3.3V) 时,虽然 ESP32 有时能承受 5V,但最好使用电平转换模块或分压电路,否则可能损坏 ESP32。
#### 3. 数据丢失
- 原因:接收缓冲区溢出。如果你的中断处理函数太慢,或者主循环读取数据的速度慢于发送速度,新的数据到达时会覆盖旧的数据。
- 解决:优化代码效率,提高
Serial.read()的频率,或者使用硬件流控制(RTS/CTS)——虽然这在简单的点对点通信中较少使用,但在高速数据传输中非常有效。
总结与展望
通过这篇文章,我们从协议的名称出发,深入到了 UART 的硬件连接、数据帧格式以及奇偶校验的底层逻辑。我们还亲手尝试了配置硬件串口甚至用软件模拟串口发送。
UART 的主要优点在于它的简洁性。仅仅依靠两根线(TX/RX),我们就能够建立起设备间的可靠通信。然而,它的异步特性也要求我们必须精确匹配通信参数(特别是波特率),并且在处理高实时性需求时要小心缓冲区溢出的问题。
#### 接下来你可以做什么?
- 动手实验:拿出两块 Arduino 开发板,将 A 的 TX 连到 B 的 RX,反之亦然。编写代码让 A 每秒发送一个“Hello”,并在 B 的串口监视器上观察。
- 探索高级功能:研究一下你的微控制器是否支持带有 FIFO(先进先出缓冲区)的 UART,这能大大减少 CPU 的中断负载。
- 升级到 RS-232 或 RS-485:UART 是芯片间的接口标准。如果你想进行长距离传输(几十米到上千米),可以研究如何将 UART 信号转换为 RS-232 或 RS-485 差分信号,这是工业控制领域的基石。
希望这篇文章能帮助你建立对 UART 协议的深刻理解。下次当你面对着一片漆黑的串口监视器发愁时,别忘了回过头来看看这些基础知识,答案往往就藏在起始位和停止位之间。