通信协议篇三:USART 串口通信原理详解从帧结构示波器逻辑分析仪波形分析到代码原理
- 电子技术
- 15小时前
- 65热度
- 0评论
异步串口是嵌入式里最常见的通信方式之一。它硬件简单、成本低、调试方便,既能做日志输出,也能连接上位机、蓝牙、WiFi、GPS、4G 等外设。本文把 USART / UART 的核心概念、帧结构、采样机制、调试方法和实战代码压缩到一篇里,尽量做到好查、好记、好用。
TX、RX 和 GND。
1. USART 是什么
USART(Universal Synchronous/Asynchronous Receiver/Transmitter)是 MCU 常见的串行通信外设,用来完成 并行数据与串行比特流之间的转换,并负责设备之间按位发送与接收数据。
1.1 USART 和 UART 的区别
| 项目 | UART | USART |
|---|---|---|
| 支持模式 | 只支持异步 | 支持同步 + 异步 |
| 日常开发常见性 | 很常见 | 很常见 |
| 实际常用场景 | 串口调试、模块通信 | 串口调试、模块通信 |
实际项目里如果只用了 TX / RX 两根信号线,通常就是在使用 异步串口。
1.2 基本连线规则
TX:发送线RX:接收线GND:共地
连接原则:TX 接对方 RX,RX 接对方 TX,共地
异步串口通常是 全双工,发送和接收可以同时进行。
1.3 常见物理层接口
| 类型 | 特点 | 常见场景 |
|---|---|---|
| TTL 串口 | MCU 常用逻辑电平,常见 3.3V / 5V | 单片机、开发板、模块直连 |
| RS232 | 电平可能有正负摆动,不能直接接 MCU | 老式电脑串口、工业设备 |
| RS485 | 差分传输,抗干扰强,适合远距离和多节点 | 工业现场、Modbus |
常见转换芯片:
- USB ↔ 串口:
CH340、CP2102 - TTL ↔ RS232:
MAX3232、SP3232
TX/RX 一般是 TTL 电平。电脑通常是 USB,老式串口通常是 RS232,因此电脑不能直接和 MCU 的 TTL 串口硬连,通常需要 USB 转 TTL 或 RS232 电平转换芯片。
1.4 为什么嵌入式里大量使用串口
- 硬件简单,成本低
- 外设支持广,几乎所有 MCU 都内置
- 适合调试输出和模块通信
- 使用门槛低,排查问题方便
常见用途:
- 调试日志输出
- 上位机通信
- 外设连接(蓝牙 / WiFi / GPS / 4G)
- BootLoader 下载
- 模块间通信
2. 异步串口的核心时序
2.1 一帧结构
异步串口的核心是:双方提前约定波特率,然后按固定节拍发送与采样。
一帧通常包含:
- 空闲位:高电平
- 起始位:1 bit,低电平
- 数据位:5~9 bit,常用 8 bit
- 校验位:可选
- 停止位:1 / 1.5 / 2 bit,高电平
空闲(1) → 起始位(0) → 数据位 → 校验位(可选) → 停止位(1)
2.2 为什么空闲是高电平
线路空闲时保持高电平,这样一旦出现 下降沿,接收端就能立即把它识别为起始位。
下降沿 = 一帧的起点
后续所有采样时刻,都是基于这个起点推导出来的。
2.3 校验位的作用
校验位本质上是 低成本检错手段,常见有:
- 奇校验
- 偶校验
例如偶校验要求:数据位中 1 的个数,加上校验位后必须是偶数。
N(无校验)。
2.4 停止位的作用
停止位有两个作用:
- 告诉接收端这一帧结束了
- 让线路回到高电平,为下一帧做缓冲
常见配置:
- 1 位停止位:默认、效率高
- 2 位停止位:更稳,但更慢
可以把停止位理解为 帧与帧之间的缓冲区。
2.5 最常见格式:8N1
8N1 是最常见的串口参数:
- 8 位数据位
- N:无校验
- 1 位停止位
空闲(1) → 起始位(0) → D0 → D1 → D2 → D3 → D4 → D5 → D6 → D7 → 停止位(1)
2.6 三个关键结论
- 数据位通常按 LSB 先发
- 接收采样点通常放在 位中心
- 两端波特率必须匹配,否则采样误差会逐位累积,最终出现乱码
常见波特率:
9600、19200、38400、57600、115200
在普通 UART/USART 二进制电平通信里,通常可近似认为:
波特率 ≈ 比特率
2.7 时间直觉
以 115200 波特率为例:
- 1 bit 时间约为:$1 / 115200 \approx 8.68\,\mu s$
- 1 帧(8N1,共 10 bit)时间约为:$86.8\,\mu s$
baud rate、data bits、parity、stop bits、电平标准 是否一致。
3. STM32 中使用 USART
3.1 三个最关键的状态标志
| 标志位 | 含义 | 作用 |
|---|---|---|
TXE |
发送数据寄存器空 | 可以继续写下一个字节 |
TC |
发送完成 | 当前帧连同停止位都发完了 |
RXNE |
接收数据寄存器非空 | 已收到新数据,需要及时读取 |
使用逻辑:
- 发送流程:优先看 TXE,最后确认 TC
- 接收流程:轮询或中断检测 RXNE,收到后及时读取
TXE 只表示“现在可以再写数据”,并不代表这一帧已经真正发到线上;要确认完全发完,应看 TC。
3.2 常见工作模式
| 模式 | 特点 | 适用场景 |
|---|---|---|
| 轮询 | HAL_UART_Transmit/Receive,阻塞等待 |
简单调试 |
| 中断 | HAL_UART_Transmit/Receive_IT,非阻塞 |
一般应用 |
| DMA | HAL_UART_Transmit/Receive_DMA,CPU 占用更低 |
大数据量 / 高效率 |
3.3 HAL 库轮询收发示例
MX_USART1_UART_Init();
uint8_t receive;
while (1) {
if (HAL_OK == HAL_UART_Receive(&huart1, &receive, 1, HAL_MAX_DELAY)) {
receive = receive + 1;
HAL_UART_Transmit(&huart1, &receive, 1, 100);
}
}
这段代码的逻辑很简单:
- 一直等待接收 1 个字符
- 收到后把字符值加 1
- 再发回去
适合用来验证:
- 串口初始化是否正确
- 收发链路是否正常
- 上位机与单片机是否真正连通
4. MicroPython:ESP32-C3 使用硬件 UART
from machine import UART
uart = UART(1, baudrate=115200, tx=4, rx=5)
while True:
if uart.any():
recv = uart.read(1).decode()
num = ord(recv)
num += 1
uart.write(chr(num))
print(f"接收到{recv},发送回{chr(num)}")
代码逻辑:
- 用
UART(1)初始化串口 - 若串口缓冲区有数据,就读取 1 字节
- 将字符转成 ASCII 数值后加 1
- 再把新字符发回去

5. 串口波形与协议分析
5.1 逻辑分析仪适合看什么
逻辑分析仪更适合看 数字协议和字节解码。
空闲(1) | 起始位(0) | 数据位(D0-D7) | 校验位(可选) | 停止位(1)

观察重点:
- 空闲时是否保持高电平
- 是否存在明确的起始位下降沿
- 每一位宽度是否基本一致
- 协议解码参数是否正确
建议重点核对:
Baud rate = 115200Data bits = 8Parity = noneStop bits = 1- 是否反相
- 是否按 LSB first 发送
5.2 示波器适合看什么
示波器更适合分析 波形质量与时序细节。


观察重点:
- 位宽是否准确
- 起始位是否正确拉低
- 停止位是否回到高电平
- 边沿是否干净
- 是否有毛刺、过冲、振铃、抖动
以 115200 波特率为例,常用观察思路:
- 先用
10 us/div左右的时基 - 使用 下降沿触发 抓起始位
- 再用游标测 1 bit 的宽度,反推实际波特率
5.3 逻辑分析仪 vs 示波器
| 工具 | 更擅长 |
|---|---|
| 逻辑分析仪 | 看协议、看字节、看解码结果 |
| 示波器 | 看电平、看边沿、看抖动和噪声 |
6. GPIO 模拟串口(Bit-Bang)
6.1 MicroPython:GPIO 模拟发送
from machine import Pin
import utime
TX = Pin(2, Pin.OUT, value=1)
BAUD = 9600
BIT_US = int(1_000_000 / BAUD)
def uart_send_byte(data):
TX.value(0)
utime.sleep_us(BIT_US)
for i in range(8):
TX.value((data >> i) & 0x01)
utime.sleep_us(BIT_US)
TX.value(1)
utime.sleep_us(BIT_US)
def uart_send_string(s):
for ch in s:
uart_send_byte(ord(ch))
uart_send_string("Hello\r\n")
发送逻辑:
- 先输出 1 bit 低电平作为起始位
- 再按 LSB first 发送 8 位数据
- 最后输出高电平作为停止位
6.2 MicroPython:GPIO 模拟接收
from machine import Pin
import utime
RX = Pin(3, Pin.IN, Pin.PULL_UP)
BAUD = 9600
BIT_US = int(1_000_000 / BAUD)
def uart_recv_byte():
while RX.value() == 1:
pass
utime.sleep_us(BIT_US + BIT_US // 2)
value = 0
for i in range(8):
bit = RX.value()
value |= (bit << i)
utime.sleep_us(BIT_US)
stop_bit = RX.value()
return value, stop_bit
while True:
data, stop = uart_recv_byte()
print("recv:", data, chr(data), "stop:", stop)
接收逻辑:
- 一直等
RX从高变低,识别起始位 - 延时到第 1 位数据的中心位置
- 每隔 1 bit 周期采样一次
- 最后可顺带读一下停止位
6.3 软件模拟串口的本质
软件模拟串口本质上就是:
- 手动检测起始位
- 手动延时对齐到位中心
- 再按固定节拍逐位采样
因此它对以下问题非常敏感:
- 延时误差
- 中断打断
- CPU 调度抖动
- 波特率偏高导致时序来不及
如果误差过大,就会出现:
- 丢位
- 错位
- 乱码
7. 串口采样机制与误差来源
7.1 为什么要在位中心采样
异步串口没有共享时钟,接收端只能在检测到起始位后,用自己的时钟估算后续每一位的采样时刻。之所以总强调“位中心采样”,是因为位中心离前后边沿最远,最不容易受到 边沿延迟、噪声抖动和双方时钟误差 的影响。
7.2 起始位后的采样流程
接收过程可以理解为:先对齐,再按节奏取样。
- 线路空闲时保持高电平
- 当
RX从高变低时,接收端认为可能出现了起始位 - 先延时约半个 bit 到一个 bit,把采样点对齐到位中心附近
- 之后每隔 1 个 bit 周期采样一次
检测到下降沿 → 对齐起始位 → 每隔 1 bit 在中心采样一次
7.3 过采样
很多硬件 UART 并不是只在理论采样点“看一眼”,而是使用 过采样,常见有:
- 8 倍过采样
- 16 倍过采样
这样做的好处:
- 更容易定位位中心
- 抗短时噪声能力更强
- 接收稳定性更高
可以粗略理解为:
- 16 倍过采样更稳
- 8 倍过采样更容易支持高波特率
7.4 为什么波特率误差会导致乱码
最麻烦的地方不是“偶尔采错一次”,而是 误差会持续累积。
如果发送端和接收端的实际 bit 宽度略有偏差,那么:
- 第 1 位时采样点可能只偏一点
- 第 2 位时继续偏
- 第 3 位再偏
- 到后面几位时,采样点可能已经接近位边界
这时只要再叠加一点噪声或抖动,就可能采到相邻位,于是出现乱码、错位或校验错误。
7.5 常见误差来源
| 类别 | 典型问题 |
|---|---|
| 时钟误差 | 晶振精度有限、RC 震荡器误差大、温漂 |
| 边沿抖动 | 上升沿/下降沿过慢、毛刺、驱动不足 |
| 线缆与干扰 | 线太长、未共地、电磁干扰强、阻抗不合适 |
| 电平标准不匹配 | 3.3V/5V 混接、TTL 误接 RS232、信号反相 |
这些问题最终都会反映到波形质量和采样稳定性上,比如:
- 位宽不稳
- 边沿发虚
- 噪声变大
- 解码失败
7.6 实际排查建议
推荐按下面顺序排查:
- 先确认两端参数是否一致:
baud rate / data bits / parity / stop bits - 再确认连线是否正确:TX 接 RX、RX 接 TX、GND 共地
- 再确认电平标准是否匹配:TTL、RS232 还是 RS485
- 若仍有问题,先降低波特率再试
- 用逻辑分析仪看协议解码结果
- 必要时用示波器看位宽、边沿、毛刺、噪声和过冲
- 若怀疑时钟问题,再检查晶振、时钟源或校准配置
- 串口本质:约定波特率 + 下降沿起帧 + 位中心采样
- 最常见格式:8N1
- 最常见问题:参数不匹配、未共地、电平不匹配、波形质量差
- 调协议先看 逻辑分析仪,查波形质量再上 示波器