ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

UART异步通信协议详解:从帧结构、波形分析到串口实战避坑

UART异步通信协议详解:从帧结构、波形分析到串口实战避坑

"第一天"这三个字对我而言,一直是嵌入式学习里最有分量的一个起点。很多朋友问我从哪学起,我给的建议基本都是先把 UART 异步通信协议调通。开发板点灯只是看见一个高低电平,串口通信才真正让板子和外界交换数据——调试日志、AT指令、传感器数据、上位机交互,背后都是 UART。

这篇文章是我整理的一份从零开始的 UART 异步通信协议笔记。你会弄懂异步到底怎么同步、一帧数据的位结构、用逻辑分析仪怎么看波形、最小驱动代码怎么写,以及我实测中踩过的三个最常见的大坑。适合刚接触单片机、想做板卡与电脑之间通信、或者打算接下来学 SPI/I2C 的同学。按我的经验,花一天把 UART 吃透,后面看任何协议都会顺很多。

1. 为什么要从UART开始学通信协议

1.1 调试日志、传感器模块、上位机,串口是"万能口"

先说一个很直观的事实:几乎所有单片机都带 UART 外设,哪怕是没有 USB 口的芯片,只要把 TX、RX 两根线引出来,就能和电脑通信。日常开发里,你总得往电脑上打印点调试信息吧?打印日志靠的就是串口。很多外设模块——GPS、蓝牙、指纹识别、空气质量传感器、4G 模组——也都默认用 UART 输出数据。你拿一个 USB 转 TTL 小板接上 TX、RX、GND,打开串口助手选好波特率,就能开始收发指令和数据。

我还见过一种情况:有些开发板自带 USB 口,用户以为用的是 USB 协议,其实板子内部也只是把单片机的 UART 引脚通过一颗 USB 转串口芯片桥接出来。也就是说,串口在嵌入式系统里的角色,远比想象中普及。它同时承担"监控口"和"控制口"两种角色:一方面把系统内部状态吐给开发者看,另一方面接收上位机下发的配置指令。UART 不是最炫的协议,但它是成本最低、最不容易缺位的通信通道。

1.2 较早搞懂"位、帧、时序"意味着什么

为什么我强烈建议把 UART 作为通信协议的第一课?因为它的协议规则很少,但"麻雀虽小,五脏俱全"。学习任何通信协议,核心就三件事:数据怎么被切分成一帧一帧的,帧里的每个位按什么顺序排列,收发双方在哪个时间点去读这些位。UART 恰好把这三个要素全部覆盖。

对比一下后面的协议:SPI 有时钟线和片选线,I2C 有地址和起始停止条件,CAN 有仲裁和位填充。它们各自的规则更多,但底层的"位—帧—时序"思维和 UART 完全一致。你如果在第一天就能理解"一帧数据包含起始位、数据位、停止位"这个概念,后面学任何带时钟线的协议都会轻松很多。我见过一些同学,用库函数调串口收发已经跑得很溜,但问他"为什么发送 0x55 时示波器上会看到那样一串波形",他就答不上来了。这就是只学接口、没学协议。第一天,我更希望你先成为后者,至少心里有波形。

2. 异步通信的底层规则:没有时钟线,靠什么对齐

2.1 "异步"不是没有节奏,而是节奏藏在数据线里

UART 全称是 Universal Asynchronous Receiver/Transmitter,关键词就是这个 Asynchronous。同步通信比如 SPI,会单独拉一条时钟线 SCLK,主机在时钟边沿告诉从机"现在这个时刻数据有效"。UART 只有 TX、RX 两根数据线,没有时钟线。那收发双方怎么知道每一位什么时候结束、什么时候该采下一个位?

答案是:双方提前约定一个"节奏",也就是波特率。更准确地说,UART 的设计是让接收端通过数据线上的边沿变化来恢复时钟节拍。导线空闲时保持高电平,一旦发送端想发数据,先拉低一个位时间,这个下降沿就是"起跑令"——接收端看到它,就知道一帧开始了。之后双方按约定的速度,沿着每个位的中间位置去采样数据。

用一个生活例子辅助理解:一个合唱团在没有节拍器的情况下合唱,每个人必须用相同的演唱速度,但需要一个指挥在开头喊一声"预备"。UART 的起始位就是这个"预备",波特率就是大家说好的演唱速度。节奏一旦不同,后面必然唱到一块去。

2.2 一帧数据长什么样:起始位、数据位、校验位、停止位

明确了异步的大思路,接下来看帧结构。UART 的一帧数据由四个部分顺序组成:

  • 起始位:1 位,低电平。它标志着帧的开始。
  • 数据位:5 到 9 位不等,最常见是 8 位,顺序是先发最低位,也就是 LSB first。
  • 校验位:可选。奇校验或偶校验,让数据位加上校验位里"1"的总数为奇数或偶数。
  • 停止位:1 位或 2 位,高电平。它标志着帧的结束。

平时总听到的"8N1"表示 8 个数据位、无校验、1 个停止位。对应还有 8E1、8O1、7E1 这类说法。E 是偶校验,O 是奇校验。偶校验的规则是:数据位和校验位里的 1 加起来必须是偶数。举个例子,0x55 有 4 个 1,偶校验位就填 0;如果数据是 0x01,只有 1 个 1,偶校验位就要填 1。校验位只能检测单比特翻转,不能纠错,但对链路噪声排查还是有用的。

为什么不能没有起始位和停止位?因为它们定义了帧边界。接收端需要知道"从哪里开始算一帧、到哪里结束"。停止位的高电平还有个作用:给线路一个明确的空闲标志,也给了接收端整理状态的时间。如果两个字节连续发送,上一帧的停止位和下一帧的起始位会形成一个"高→低"的边沿,方便接收端重新同步。

2.3 波特率计算:每秒多少位,每位多长

波特率的字面意思是每秒传输的符号数。因为 UART 里每个符号就是 1 个比特,所以通常可以认为波特率就等于比特率。把这个定义倒过来,就能算出每一位持续多长时间:

每位时间 = 1 / 波特率

举个例子,115200 bps 时,每位时间约为 8.68 微秒。这个数字很有用,后面用逻辑分析仪量波形时,直接拿它跟实测位宽对比就能判断配置是否正确。

波特率每位时间
9600约 104.17 微秒
19200约 52.08 微秒
38400约 26.04 微秒
57600约 17.36 微秒
115200约 8.68 微秒

那么 UART 的波特率能不能有一点误差?实际是可以的。接收端通常用 16 倍波特率时钟对数据线采样,起始位下降沿出现后,它数到第 8 个采样时钟就落在数据位的中心附近,之后每隔 16 个采样时钟读一次。只要采样点没有偏离位中心太远,收发双方存在 2%~3% 的波特率误差通常还能正确通信。零界点大概在采样点滑出位中心的时候,误差一旦超过这个范围,就必然乱码或丢字节。

所以工程上要注意:单片机使用外部晶振时,波特率误差通常很小;使用内部 RC 振荡器且没有校准,误差可能接近 2%,在 115200 这样的高速率下就比较容易出现不定时乱发数据的情况。这也是为什么很多开发板调试时,默认用 9600 反而比 115200 稳定——低速时每位更长,同样的时钟误差对采样点的影响更小。

2.4 UART协议、TTL电平、RS-232、RS-485,别把四者混为一谈

很多新手把"UART"和"TTL 电平"当成一回事,其实它们不是同一层面的东西。UART 定义的是帧格式和时序逻辑:高电平代表 1,低电平代表 0。TTL 电平只是物理实现的一种:3.3V 或 5V 表示逻辑 1,0V 表示逻辑 0。

RS-232 是另一种物理层标准,它用负逻辑,逻辑 1 用 -3V 到 -15V,逻辑 0 用 +3V 到 +15V,传输距离比 TTL 远,适合几十米内的设备互联。RS-485 则使用差分信号,通过 A、B 两条线上的电压差判断电平,抗干扰能力强,支持几百米传输和多个设备挂同一条总线。它们传输的内容仍然是 UART 帧格式,物理层变了而已。

理解这一点对选型很重要。电脑没有原生 TTL 串口,所以我们常用 USB 转 TTL 小板来做电平转换。如果单片机是 3.3V 逻辑,而 USB 转 TTL 小板输出 5V,直接连接就可能遇到信号识别不了或者损坏引脚的问题。后面讲坑的时候还会专门提这个。

3. 用逻辑分析仪把一帧数据还原成看得见的波形

3.1 先发一个0x55,波形会非常规律

纸上谈兵的帧结构终究抽象,最直观的办法是把波形抓出来看。我强烈建议你准备一个逻辑分析仪,几十块钱的就行,不需要高档示波器。

测试时让单片机循环发送 0x55。0x55 的二进制是 0101 0101,按 LSB first 发送,数据位依次是 1、0、1、0、1、0、1、0。整帧波形会呈现出规律的高低交替:

阶段电平
空闲高
起始位低
数据位0高
数据位1低
数据位2高
数据位3低
数据位4高
数据位5低
数据位6高
数据位7低
停止位高

从表格能看出来,0x55 的波形几乎是完美的方波:起始位拉低之后,每个数据位都在翻转,最后停止位回到高电平。用逻辑分析仪的测量工具量相邻边沿之间的时间差,正好就是一个位宽度。反过来,如果你想确认自己理解没理解 LSB first,发 0x55 后数一数第一个数据位是高还是低——按 LSB first,最低位是 1,所以起始位之后第一个数据位应该是高。如果软件里配置成了 MSB first,波形第一个数据位就是低,对照下来马上露馅。

3.2 逻辑分析仪接线和参数设置的注意点

逻辑分析仪测量 UART 时,接线很简单:把探头的信号线接目标设备的 TX 引脚,GND 接设备的地。注意 GND 必须共地,否则信号参考点不同,抓到的波形可能完全是乱的。

采样率设置很关键。理论上最低需要几倍于波特率,实际建议至少 16 倍以上。比如分析 115200 的串口,采样率最好设到 2MHz 甚至更高。采样率太低,波形边沿会变得很糊,解码也可能出错。

解码参数里,协议选择 UART(有些工具叫 Serial 或 Async Serial),波特率必须和发送端一致,数据位、停止位、校验位也要对应。常见的坑是只改了波特率,忘了把停止位从 1 改成 2,或者忘了打开校验位。如果波形非常干净,但解码结果乱码,优先检查这些设置项,不要急着怀疑电路。

3.3 从波形反推波特率,比查代码快得多

逻辑分析仪最有价值的一点,是能直接从波形反推波特率,省去一大堆盲猜。量出一个位的时间 T,波特率就等于 1/T。比如你量到相邻边沿间隔大约是 104 微秒,那实际波特率就是 9600;如果量到约 8.68 微秒,就是 115200。

我遇到乱码时的标准操作是这样的:先抓发送端的 TX 波形,用测量工具拉出几个位间隔,算平均位宽,判断发送端实际波特率是多少。如果实测值和配置值差得不多,那问题多半在接收端的帧格式;如果差了一大截,比如配置 115200 但波形位宽明显是 9600 的,那多半是代码里波特率寄存器算错了,或者时钟分频选择出了问题。

波形不会骗人。学会这一步,你排查串口问题就基本脱离猜谜模式了。

4. 从零驱动一个串口:初始化、发送、接收的完整链路

4.1 初始化之前,先把五个参数定死

驱动 UART 的代码在不同平台上写法不同,但核心步骤完全一致。我建议你初始化之前,先把下面五件事想清楚,再动手写:

  • 使用哪个 UART 外设,以及对应的引脚。
  • 外设时钟是多少,这决定波特率分频怎么算。
  • 波特率是多少。
  • 帧格式是几数据位、什么校验、几位停止位。
  • 收发模式是轮询、中断还是 DMA。

通用初始化代码框架大致是这样:

void uart_init(uint32_t baud) { // 1. 使能UART外设和对应GPIO端口的时钟 // 2. 配置TX引脚为复用推挽输出,RX引脚为浮空输入或上拉输入 // 3. 设置数据位为8位,1个停止位,无校验(8N1) // 4. 计算分频系数:divider = peripheral_clock / (16 * baud) // 5. 使能发送器和接收器 // 6. 如果使用中断,配置中断优先级并使能接收中断 }

波特率分频系数怎么算?绝大多数 UART 的时钟模型是:目标波特率 = 外设时钟 / (16 × 分频系数)。所以分频系数 = 外设时钟 / (16 × 波特率)。举例:外设时钟 72MHz,目标 115200,算出来分频系数约 39.06。现在很多芯片的分频寄存器支持小数位,就是为了把 0.06 这部分尽量凑准,减小波特率误差。如果芯片没有小数分频,只能取整,那 39.06 取整成 39,再反推实际波特率是 72000000 / (16 × 39) ≈ 115384,误差只有约 0.16%,完全没问题。

有个容易忽略的点:GPIO 复用模式没配好,信号根本到不了引脚。有些初学者初始化了 UART 外设,也写了数据寄存器,但 TX 引脚实际不在复用模式,和普通 GPIO 一样悬空,结果上位机什么都收不到。所以引脚复用配置这一步不能省。

4.2 发送一字节:先看标志位,再写数据寄存器

发送侧有两个核心标志位。TXE 表示发送数据寄存器空,可以写入下一个要发的数据;TC 表示整个帧已经彻底发送完成,包括停止位都发出去了。第一天写代码,用 TXE 就够:

void uart_send_byte(uint8_t data) { while (!(uart->SR & UART_FLAG_TXE)); // 等待上一个字节写入完成 uart->DR = data; // 写入数据寄存器 }

注意区分:TXE 置位说明"可以往数据寄存器里填充下一个字节",但不代表引脚上已经把数据发完了;TC 置位才是整帧发送结束。如果程序在连续发送大量数据时,只等 TXE 不等 TC,数据仍然能发出去,因为发送移位寄存器会自动把数据搬出。但有少数场景需要电平完全稳定,比如切换 RS-485 方向时,就得查 TC,确保最后一帧彻底发完再拉低发送使能,否则最后一个字节会被截断。

为什么第一天先从轮询开始?因为轮询不依赖中断系统,代码路径非常直白,只要硬件接线没问题,它几乎一定能跑通。它能帮你排除"是硬件问题还是软件问题"这个大类别。等轮询调通之后,再升级成中断发送,思路会清晰很多。

4.3 接收一字节:RXNE标志和溢出错误

接收侧的核心标志是 RXNE,收到一整个字节后硬件自动置位,程序读取数据寄存器就会清除它。轮询接收的骨架如下:

uint8_t uart_receive_byte(void) { while (!(uart->SR & UART_FLAG_RXNE)); // 等待收到一字节 return (uint8_t)uart->DR; }

这里藏着一个新手容易踩的坑:如果数据来得太快,程序还没来得及读走上一字节,下一字节又接收完成,UART 硬件就会触发溢出错误 ORE。后续所有新数据都可能被丢弃,甚至影响 RXNE 标志的判断。标准清 ORE 的动线是:先读状态寄存器 SR,再读数据寄存器 DR,两步一起才能把错误标志彻底清掉。只读 DR 不清 SR,错误状态可能一直挂着,表现为"串口明明有数据进来,程序却收不到"。

我建议你在写完整接收逻辑时,先检查 SR 里的错误标志,再决定是读数据还是清错误。轮询代码里最简单的容错就是:发现 ORE 后把状态清掉继续跑,别让一个错误锁死整个循环。

4.4 轮询、中断、DMA,第一天的取舍

轮询适合第一天验证硬件链路,但如果程序要在等待串口数据的间隙去处理其他事情,轮询会占满 CPU。中断方式让你在收到数据时被"打断"去处理,平时该干嘛干嘛,这是工程中最常用的方案。DMA 则是让数据在外设和内存之间自动搬运,CPU 完全不参与,适合大数据量的日志输出和固件升级。

三种方式对比大致如下:

方式CPU占用实时性代码复杂度适用场景
轮询高中等低演示、简单收发
中断低高中交互式数据、按键/指令
DMA极低高(取决于配置)高批量日志、固件升级、大数据

第一天不要碰 DMA。DMA 会牵扯缓冲区管理、半传输中断、环形队列、错误处理,状态多,调试复杂度一下子拉满。先把轮询、中断各写一遍,理解"CPU 主动去读"和"硬件通知 CPU 来读"的区别,后面上 DMA 就是水到渠成的事。我自己也是做到日志系统时才认真研究 DMA,当时发现只要把缓冲区索引处理好,DMA 并没那么可怕,但前提是基本功已经打牢了。

5. 实测串口最容易翻车的三个坑:乱码、接反、电平绑架

5.1 乱码的本质:双方对"一帧"的理解不一致

乱码是串口调试里出现频率最高的问题,但大多数时候不是芯片坏了,而是通信双方对"一帧数据"的理解不一致。最常见的四个原因:波特率不同、数据位/停止位/校验位配置不同、GND 没共地、USB 转 TTL 小板的电平不对。

排查乱码时,我的顺序很固定:

  1. 用逻辑分析仪抓发送端 TX 波形。
  2. 测量位宽,算出实际波特率。
  3. 核对收发双方的帧格式是否都是 8N1 或对应的 8E1。
  4. 检查 GND 线是否牢固连接。
  5. 最后才改代码。

有一个容易迷惑人的现象:发送端明明配置 115200,代码里也写的 115200,但波形显示实际波特率是 9600。这种通常是分频寄存器被别处代码改掉了,或者时钟初始化顺序不对,导致 UART 用的时钟源和预期不一致。不看波形,光靠双眼瞪代码,很难发现这类问题。

另外提一句:如果发的是中文文本,上位机用不同的编码解码,看起来也会是乱码,但这属于编码问题,不是串口问题。排查阶段先发纯 ASCII 字符,比如字符"A"对应的十六进制 0x41,能显著减少干扰变量。

5.2 TX接TX是怎么发生的

UART 接线规则只有一句:发送方 TX 接接收方 RX,接收方 TX 接发送方 RX,GND 共地。看起来不难,但新手接线时经常把两块开发板的 TX 接 TX,RX 接 RX,结果机器静悄悄。原因也好理解:大家都觉得"串口就是那两根线,照着名字接总不会错",实际把两个输出口怼在一起,两边都在等对方先说话,自然没有数据。

如果只是想先验证自己的板子能不能自发自收,可以把 TX 引脚和 RX 引脚直接用杜邦线短接,然后代码里调用发送函数,看到接收寄存器里回收到同样的数据,链路就通了。和电脑通信时一定要记得交叉:开发板 TX 接 USB 转 TTL 板的 RX,开发板 RX 接 USB 转 TTL 板的 TX。

如果确认接线没问题但就是收不到,把两条信号线对调再试一次。这个操作听起来很基础,但我至少见过两次明明插反了却排查了半个小时的案例,这里面也包括我自己。

5.3 电平不匹配和"共地"问题,不能靠运气

TTL 电平分 5V 和 3.3V 两套。5V 单片机 TX 输出的高电平是 5V,如果对方是 3.3V 单片机且引脚不耐 5V,轻则逻辑判断出错,重则直接损坏 IO。反过来 3.3V 输出到 5V 输入,多数时候能识别成高电平,但也不是稳定可靠的设计。规范做法是加一个双向电平转换模块,几块钱的东西,别在这上面省钱。

还有共地问题。串口信号的"0V"参考点就是 GND,两个设备必须要用地线连起来。如果 GND 不接,两边参考点不一样,接收端看到的不再是完整的高低电平,可能是奇奇怪怪的中间电压,表现就是完全解不出数据或者收到满屏 0xFF。我调试串口的第一步永远是检查 GND,然后再看 TX、RX。

此外,插拔杜邦线时最好断电操作。带电插拔不仅容易接触不良,还可能因为瞬间电平抖动把引脚打坏。USB 转 TTL 小板插在电脑上,电脑侧的电一直存在,改接线时先把小板的 USB 端拔下来更稳妥。

6. 第一天的验收清单与往后的学习路线

6.1 今天能完成这几件事,就算真的入门

我给你一份比较现实的验收清单,不用贪多,只求每项都真正跑通:

  1. 开发板 TX 和 RX 短接,程序自发自收,能收到完整数据。
  2. 通过 USB 转 TTL 小板连接电脑串口助手,板子发字符串能被上位机正确显示。
  3. 电脑通过串口助手发一个 0x55,开发板能收回来并确认内容正确。
  4. 用逻辑分析仪抓 TX 波形,能指出哪一段是起始位、哪一段是数据位、哪一段是停止位。
  5. 从波形上量出位宽,反推出实际波特率。

第 2 条和第 4 条是最关键的。只要这两条通了,说明你对 UART 的理解已经不只是"调个库"的层面,而是真的看懂了这个协议。我自己判断一个初学者有没有入门,就看他能不能拿着逻辑分析仪的截图把整个帧讲明白。

6.2 下一步值得看的几个点

第一天的内容可以做几个方向的扩展。如果对帧格式还有兴趣,可以研究校验位和数据位 9 位模式,多机通信里常通过第 9 位来区分地址帧和数据帧。硬件流控 RTS/CTS 也值得了解,高速传输时它能避免接收端缓冲区满了还把数据往外面发。

如果接触工业现场,RS-485 是绕不开的。它的核心难点在于发送结束后要切换收发方向控制引脚,不能多拖也不能提前。还有 DMA 加环形缓冲区这套组合,做固件升级和日志系统时非常实用,等你把中断收发写熟练之后再上。

6.3 先看波形,再改代码

说实话,我最早学 UART 时也没一步到位。乱码、接反、电平不匹配,全都遇到过。后来养成一个习惯:先看波形,再改代码。这个习惯帮我解决了很多看似玄学的问题。第一天能自己画出 0x55 的波形,你就已经超过了只会用库调串口的同学。后面不管学 I2C、SPI 还是 CAN,只要记得"位、帧、时序"这三个关键词,都会轻松不少。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进