嵌入式开发 UART 串口通信完全指南 2026:波特率、流控与多机通信
本文是嵌入式通信协议系列的第三篇。上一篇:SPI 协议完全指南,更早:I2C 总线完全指南
UART(Universal Asynchronous Receiver-Transmitter)大概是嵌入式世界里最”古老但至今仍在用”的协议了。
它诞生于 1960 年代,比 SPI 和 I2C 都早,但直到今天——你依然能在 ESP32 的串口调试输出、Arduino 的 Serial.println()、工业设备的 Modbus 通信里看到它的身影。
原因很简单:UART 不需要时钟线,不需要地址分配,接线最少,调试最方便。
但 UART 也有明显的短板:速度慢、距离受限、只能点对点通信(不像 I2C 和 SPI 能挂多个设备)。这篇文章把 UART 的协议细节、电平转换、工业协议实战全部讲清楚,让你知道什么时候 UART 是最佳选择,什么时候该换协议。
UART 协议原理
什么是”异步”?
UART 的”A”代表 Asynchronous——异步。与 SPI/I2C 的”同步”通信不同,UART 没有时钟线。发送方和接收方各自运行独立的时钟,通信的同步靠双方提前约定好的参数:
| 参数 | 典型值 | 说明 |
|---|---|---|
| 波特率 | 9600, 115200 | 每秒传输的符号数(bits per second) |
| 数据位 | 8 bits | 每帧有效数据的位数 |
| 停止位 | 1 bit | 帧结束的标记 |
| 校验位 | None/Even/Odd | 奇偶校验(可选) |
如果双方波特率不一致——比如发送方用 115200,接收方用 9600——收到的就是乱码。这是 UART 最常见的故障之一。
数据帧结构
UART 每次传输一个”帧”(frame),结构如下:
空闲(高) ┤
├─ 起始位(0) ┤ D0 ┤ D1 ┤ D2 ┤ D3 ┤ D4 ┤ D5 ┤ D6 ┤ D7 ┤ 校验(可选) ┤ 停止位(1) ┤─ 空闲(高)
└── 1 bit ───└────────── 8 data bits ──────────┘└─ 0/1 bit ─┘└── 1/2 bits ──┘
- 起始位:总是低电平,告诉接收方”数据来了”
- 数据位:低位先发(LSB first),通常 8 bit
- 校验位:可选,用于检测传输错误
- 停止位:总是高电平,标志一帧结束
波特率与误差容忍
UART 接收方在每一位的中间位置采样。如果双方波特率偏差超过一定范围,采样点就会漂移到错误位置。
误差公式:
最大允许误差 ≈ 1 / (2 × 数据位数) × 100%
以 8 数据位为例:最大允许误差约 6.25%。但实际工程中建议控制在 2% 以内,否则在高波特率下容易丢包。
常见波特率及对应位时间:
| 波特率 | 位时间 | 一帧时间(8N1) | 理论吞吐量 |
|---|---|---|---|
| 9600 | 104.17 μs | 1.04 ms | ~960 bytes/s |
| 38400 | 26.04 μs | 260 μs | ~3.8 KB/s |
| 115200 | 8.68 μs | 86.8 μs | ~11.5 KB/s |
| 921600 | 1.09 μs | 10.9 μs | ~92 KB/s |
115200 是最常用的”高速”波特率——足够快,又不至于太容易出错。
硬件流控 vs 软件流控
UART 传输没有时钟信号,也就没有”暂停”机制。当接收方处理不过来时,数据就会丢失。流控(Flow Control)就是为了解决这个问题。
RTS/CTS 硬件流控
RTS(Request to Send)和 CTS(Clear to Send)是两根额外的信号线:
发送方 ────TX────→ 接收方
发送方 ←───RX──── 接收方
发送方 ←───CTS─── 接收方 (接收方告诉发送方"我准备好了")
发送方 ────RTS───→ 接收方 (发送方告诉接收方"我要发了")
- 接收方处理缓冲区满时,拉低 CTS → 发送方暂停
- 缓冲区清空后,拉高 CTS → 发送方继续
优点:响应快,不占用数据带宽 缺点:多两根线,布线成本增加
XON/XOFF 软件流控
不需要额外引脚,通过在数据流中插入特殊字符来实现:
- XON (0x11, Ctrl-Q):告诉对方”可以继续发送”
- XOFF (0x13, Ctrl-S):告诉对方”暂停发送”
优点:只需 TX/RX 两根线 缺点:特殊字符可能与有效数据冲突,需要转义处理
ESP32 流控代码示例
// Arduino/ESP-IDF 风格
HardwareSerial Serial1(1); // 使用 UART1
void setup() {
// 115200 波特率,8 数据位,无校验,1 停止位
// 启用硬件流控(RTS=GPIO18, CTS=GPIO19)
Serial1.begin(115200, SERIAL_8N1,
/* RX */ GPIO_RX,
/* TX */ GPIO_TX,
/* RTS */ GPIO18,
/* CTS */ GPIO19);
}
void loop() {
if (Serial1.available()) {
String data = Serial1.readStringUntil('\n');
Serial1.println("Received: " + data);
}
}
ESP32 的三个 UART 口(UART0/1/2)都支持硬件流控,但默认只启用 TX/RX。需要流控时,手动指定 RTS/CTS 引脚即可。
ESP32 多 UART 口使用
ESP32 芯片内置 3 个 UART 控制器,这是它相比其他 MCU 的一大优势:
| UART | 默认引脚 | 典型用途 |
|---|---|---|
| UART0 | TX=GPIO1, RX=GPIO3 | USB 串口调试(Serial) |
| UART1 | TX=GPIO17, RX=GPIO16 | Flash 缓存(系统保留,可复用) |
| UART2 | TX=GPIO17, RX=GPIO16 | 自由使用 |
⚠️ 注意:ESP32 的 UART1 默认被 SPI Flash 缓存占用,但可以通过配置释放给普通串口使用。ESP32-S3/C3 的引脚分配略有不同。
实战:同时使用三个 UART 口
// 场景:UART0 调试输出,UART1 接 GPS 模块,UART2 接 Modbus 传感器
HardwareSerial gpsSerial(1); // UART1 → GPS
HardwareSerial modbusSerial(2); // UART2 → Modbus 传感器
void setup() {
Serial.begin(115200); // UART0:调试输出
gpsSerial.begin(9600, SERIAL_8N1, 16, 17); // GPS 通常 9600
modbusSerial.begin(9600, SERIAL_8N1, 22, 23); // Modbus 传感器
}
void loop() {
// 读取 GPS NMEA 数据
if (gpsSerial.available()) {
String nmea = gpsSerial.readStringUntil('\n');
Serial.println("GPS: " + nmea);
}
// 读取 Modbus 传感器数据
if (modbusSerial.available()) {
// Modbus 帧处理
}
}
这在工业场景中非常实用——一个 ESP32 同时连接 GPS、传感器和调试终端,各司其职。
RS232 / RS485 / RS422 电平转换
UART 是协议层概念,而 RS232/RS485/RS422 是物理层电气标准。MCU 的 UART 输出是 3.3V/5V TTL 电平,但长距离通信需要转换为更抗干扰的电平标准。
三种电平对比
| 标准 | 电压范围 | 传输模式 | 最大距离 | 节点数 | 典型场景 |
|---|---|---|---|---|---|
| TTL | 0-3.3V/5V | 单端 | < 1m | 2 | 板级通信、调试 |
| RS232 | ±3V ~ ±15V | 单端 | ~15m | 2 | PC 串口、老设备 |
| RS422 | ±1.5V ~ ±6V | 差分 | ~1200m | 10 | 工业控制 |
| RS485 | ±1.5V ~ ±6V | 差分,半双工 | ~1200m | 32/128 | 工业总线 |
RS485 半双工通信
RS485 是最常用的工业串行标准:
- 差分信号:用 A/B 两根线的电压差表示数据,抗共模干扰能力强
- 半双工:同一时刻只能发送或接收(需要 DE/RE 引脚控制方向)
- 多点总线:一条总线上可以挂 32 个(甚至 128 个)节点
典型接线:
ESP32 ──[MAX485]── A ───┬──[MAX485]── 节点1
├──[MAX485]── 节点2
└──[MAX485]── 节点3
B ───┘
ESP32 RS485 发送控制代码:
// MAX485 的 DE(发送使能) 和 RE(接收使能) 接在同一 GPIO
#define RS485_DE_RE 4
void setup() {
pinMode(RS485_DE_RE, OUTPUT);
digitalWrite(RS485_DE_RE, LOW); // 默认接收模式
Serial2.begin(9600, SERIAL_8N1, 16, 17);
}
void rs485_send(const char* data) {
digitalWrite(RS485_DE_RE, HIGH); // 切换到发送模式
delay(1); // 等待 MAX485 稳定
Serial2.print(data);
Serial2.flush(); // 等待发送完成
delay(1); // 确保最后一位发完
digitalWrite(RS485_DE_RE, LOW); // 切回接收模式
}
⚠️ 关键细节:DE/RE 切换必须有延迟。MAX485 的使能引脚响应时间约 50ns,但 MCU 的 GPIO 切换和 UART FIFO 排空需要额外缓冲,一般 1ms 足够。
Modbus RTU 协议实战
Modbus RTU 是运行在 UART/RS485 上的应用层协议,广泛用于工业传感器、PLC、变频器。
Modbus 帧结构
| 地址(1B) | 功能码(1B) | 数据(NB) | CRC(2B) |
| 字段 | 说明 |
|---|---|
| 地址 | 从站地址(0=广播,1-247=从站) |
| 功能码 | 03=读保持寄存器,06=写单寄存器,10=写多寄存器 |
| 数据 | 寄存器地址、数量、写入值等 |
| CRC | 16 位循环冗余校验(低位在前) |
Modbus RTU 读取温度传感器
#include <HardwareSerial.h>
HardwareSerial modbus(2);
// 功能码 03:读取保持寄存器
// 从站地址 1,起始寄存器 0x0000,读取 2 个寄存器(温度+湿度)
uint8_t readHoldingRegs[] = {
0x01, // 从站地址
0x03, // 功能码:读保持寄存器
0x00, 0x00, // 起始地址 0x0000
0x00, 0x02, // 读取 2 个寄存器
// CRC 稍后计算
};
uint16_t calculateCRC(uint8_t* buf, uint8_t len) {
uint16_t crc = 0xFFFF;
for (uint8_t i = 0; i < len; i++) {
crc ^= buf[i];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc >>= 1;
crc ^= 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
void sendModbusRequest() {
digitalWrite(RS485_DE_RE, HIGH);
delay(1);
// 计算 CRC 并追加
uint16_t crc = calculateCRC(readHoldingRegs, 6);
modbus.write(readHoldingRegs, 6);
modbus.write(crc & 0xFF); // CRC 低位
modbus.write((crc >> 8) & 0xFF); // CRC 高位
modbus.flush();
delay(1);
digitalWrite(RS485_DE_RE, LOW);
// 读取响应(等待从站回复)
delay(50);
if (modbus.available() >= 7) { // 地址(1) + 功能(1) + 字节数(1) + 数据(4)
uint8_t response[9];
modbus.readBytes(response, 9);
// 解析温度值(假设温度在寄存器 0,16 bit)
uint16_t temperature = (response[3] << 8) | response[4];
float tempC = temperature / 10.0; // 假设精度 0.1°C
Serial.printf("温度: %.1f°C\n", tempC);
}
}
💡 实用建议:实际项目中建议使用成熟的 Modbus 库(如
ModbusMaster或ArduinoModbus),而不是手写帧构造和 CRC 计算。上面的代码主要用于理解协议底层逻辑。
故障排查:UART 常见问题
1. 乱码
症状:收到奇怪的字符,如 ÿÿÿ 或随机符号。
可能原因及排查:
| 原因 | 排查方法 | 解决方案 |
|---|---|---|
| 波特率不匹配 | 确认两端波特率一致 | 改为相同值,误差 < 2% |
| 地线未连接 | 用万用表测 GND 连通性 | 确保共地 |
| TX/RX 接反 | 检查接线 | 交叉连接(TX→RX, RX→TX) |
| 电平不匹配 | 测量信号电压 | 加电平转换芯片(如 TXB0108) |
2. 数据丢失
症状:发送 10 字节,只收到 8 字节。
排查:
- 检查是否启用流控(RTS/CTS 或 XON/XOFF)
- 检查接收方缓冲区是否溢出
- 降低波特率测试
- 确认没有中断延迟影响 UART ISR
3. 通信距离受限
症状:TTL 电平超过 1 米就开始出错。
方案:
- 短距离(< 1m):TTL 直连即可
- 中距离(1-15m):RS232(MAX232 芯片)
- 长距离(15-1200m):RS485(MAX485 芯片)
4. RS485 总线冲突
症状:多个节点同时发送,数据混乱。
解决方案:
- 使用主从架构:只有一个主站负责发起请求
- 设置发送超时:从站回复超时后主站重试
- 总线两端加 120Ω 终端电阻(消除信号反射)
UART vs SPI vs I2C 快速对比
| 特性 | UART | SPI | I2C |
|---|---|---|---|
| 线数 | 2(TX/RX)+ 可选流控 | 4(MOSI/MISO/SCLK/CS) | 2(SDA/SCL) |
| 速度 | 最高 ~4 Mbps | 最高 ~80 MHz | 最高 3.4 MHz (Hs) |
| 拓扑 | 点对点 | 一主多从 | 多主多从 |
| 地址 | 无(点对点) | 片选线选择 | 7/10 位地址 |
| 全双工 | ✅ | ✅ | ❌ |
| 适合场景 | 调试、长距离通信、工业总线 | 高速外设(屏幕、Flash) | 低速传感器 |
| 调试方便度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
简单选型:
- 需要调试输出或接传感器读文本数据 → UART
- 需要高速数据传输(屏幕、SD 卡) → SPI
- 需要挂多个低速传感器且省引脚 → I2C
总结
UART 可能是嵌入式世界最”朴素”的协议——没有时钟线、没有地址、没有复杂的仲裁机制。但正是这种简单,让它在调试、工业通信、设备对接场景中不可替代。
记住几个关键点:
- 波特率必须一致,误差控制在 2% 以内
- TTL 电平只适合板级短距离通信,长距离用 RS485
- RS485 半双工需要手动控制 DE/RE 引脚方向
- Modbus RTU 是 UART 上最成熟的工业协议
- ESP32 有 3 个 UART 口,可以同时接多个设备
本文是嵌入式通信协议系列第三篇。如果你对传感器连接、工业现场总线感兴趣,这个系列值得持续关注。