|
UART 串口通信完全指南 2026:波特率、流控与多机通信

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)理论吞吐量
9600104.17 μs1.04 ms~960 bytes/s
3840026.04 μs260 μs~3.8 KB/s
1152008.68 μs86.8 μs~11.5 KB/s
9216001.09 μs10.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默认引脚典型用途
UART0TX=GPIO1, RX=GPIO3USB 串口调试(Serial)
UART1TX=GPIO17, RX=GPIO16Flash 缓存(系统保留,可复用)
UART2TX=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 电平,但长距离通信需要转换为更抗干扰的电平标准。

三种电平对比

标准电压范围传输模式最大距离节点数典型场景
TTL0-3.3V/5V单端< 1m2板级通信、调试
RS232±3V ~ ±15V单端~15m2PC 串口、老设备
RS422±1.5V ~ ±6V差分~1200m10工业控制
RS485±1.5V ~ ±6V差分,半双工~1200m32/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=写多寄存器
数据寄存器地址、数量、写入值等
CRC16 位循环冗余校验(低位在前)

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 库(如 ModbusMasterArduinoModbus),而不是手写帧构造和 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 快速对比

特性UARTSPII2C
线数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 可能是嵌入式世界最”朴素”的协议——没有时钟线、没有地址、没有复杂的仲裁机制。但正是这种简单,让它在调试、工业通信、设备对接场景中不可替代。

记住几个关键点:

  1. 波特率必须一致,误差控制在 2% 以内
  2. TTL 电平只适合板级短距离通信,长距离用 RS485
  3. RS485 半双工需要手动控制 DE/RE 引脚方向
  4. Modbus RTU 是 UART 上最成熟的工业协议
  5. ESP32 有 3 个 UART 口,可以同时接多个设备

本文是嵌入式通信协议系列第三篇。如果你对传感器连接、工业现场总线感兴趣,这个系列值得持续关注。