1. 先认清UART:三根线里藏着最基本通信逻辑
做了几年嵌入式开发,回头发现一件事:UART这个协议,越早吃透,后面越省心。上学时第一次用USB转串口线给板子打印调试日志,串口助手出来一屏乱码,我还以为是芯片坏了,折腾了半宿,最后只是把波特率从115200改成了9600。就是这么个不起眼的协议,用好了能陪你从入门一直用到产品量产阶段。
这篇是“通信协议”系列的第一篇,我把UART从物理链路到实际调试验证完整讲一遍。适合刚开始学单片机、对串口通信一知半解的读者,也适合做了几年项目但只停留在“会调库函数”层面的工程师——很多你以为懂了的细节,在实际调板子时才会暴露出来。
UART全称Universal Asynchronous Receiver/Transmitter,通用异步收发传输器。拆开这几个字就好理解了:通用,是说它几乎标配在每一颗MCU上;异步,指收发双方不需要额外的时钟线,各自按约定速率采样;收发器,则说明它一根线发、一根线收,天然支持全双工通信。和I2C、SPI相比,UART最大的优势就是结构足够简单,哪怕只是两根普通杜邦线,也能搭出一条可靠的数据通道。
1.1 三根线的分工:TX、RX与GND
一条最基本的UART链路只需要三根线——
- TX(发送线):当前设备向外发送数据的引脚。
- RX(接收线):当前设备接收外部数据的引脚。
- GND(地线):供收发双方共用的电平参考地。
发数据这件事,本质就是TX引脚在高低电平之间快速切换,接收端在RX引脚上采样这些电平并还原成字节。TX和RX在电路上完全独立,所以理论上同一时刻既能发又能收,这就是全双工。你可以把UART想象成一根电话线:两个人能同时说话,也能同时听对方说,不需要轮流等。
这里有个初学者最容易误解的地方:UART没有时钟线。I2C有SCL,SPI有SCK,发送方什么时候拉高、什么时候拉低,接收方怎么知道该在哪一拍采样?答案是双方事先约定好波特率,然后各自用自己的本地时钟去数每一位的时间。这也是“异步”两个字的含义。
1.2 交叉接线与误接电源:新手第一大坑
三根线里,TX和RX必须交叉连接:你的TX接对方的RX,你的RX接对方的TX。这个规则我见过太多人翻车——把两块板子的TX接TX、RX接RX,结果数据发过去对方根本收不到。原因很直白:发送端的数据要从自己的TX脚“流”进接收端的RX脚,如果两边都把信号放在TX脚上,就等于两个人在对讲机里同时按下发射键。
排查接线问题时,一个很好记的口诀是“同端不相连”。设备A的TX,永远对应设备B的RX;只有调试USB转TTL模块自发自收时才需要把模块自己的TX和RX短接。
更危险的操作是误把TX/RX接到电源上。有些开发板引脚排布紧密,杜邦线一插偏,把TX接到了5V或GND上,轻则数据全错,重则烧掉引脚。我见过不止一块板子因为这一步报废。接完线之后,上电前先拿万用表量一下引脚电压,这是成本最低的保护措施。
1.3 共地:那个看不见的第四根线
很多人在两块板子之间只接了TX和RX,收到的是全乱码或者毫无数据。网上一搜,答案往往是“波特率不对”,其实还有个大坑:没接公共地。
UART的电平判断依赖GND作为参考基准。接收端判断某个引脚电压是高还是低,测量的其实是引脚相对自己GND的电压差。如果两块板子各自用独立电源,又没有共地,A板输出的3.3V高电平在B板看来可能只剩1.5V,刚好落在逻辑阈值附近,结果就是随机误判。
处理方法是:除了TX和RX,一定把两边的GND也连在一起。这是物理层可靠通信的隐形第四根线,少它不可。尤其使用两个独立电源适配器供电的场景,千万别偷懒。也有人会把GND接得很长很细,在高波特率下容易引入地电位差和干扰,正规做法规矩是GND尽量短粗,必要时用双绞线把TX和GND绞在一起走。
2. 一帧数据的完整旅行:时序里的每个高低电平都有名字
理解了物理连接,接下来看一个字节到底是怎么从电信号变成可读数据的。这是UART协议最核心的部分,也是大家常说的“时序”。
2.1 从空闲电平到起始位:下降沿就是发令枪
UART线上空闲时保持高电平。这个设计是有讲究的:高电平代表“无事发生”,同时也让线路在空闲状态下有个确定电位,方便检测故障。发送方准备发数据时,首先把TX线从高拉低,持续一个位时间,这个低电平脉冲就是起始位。
接收方平时一直监视RX线的电平变化,一旦捕捉到一个下降沿,就知道发送方开始发数据了,随后按照约定好的位宽,依次在每一位时间的中点附近采样。这一套动作,和两个人约定好了语速,然后一方说“注意,我开始念了”很像——起始位就是那个“注意”。
起始位之后,发送方逐位送出数据位。数据位默认从最低位(LSB)开始发送。以ASCII字符“A”(十六进制0x41)举例:它的二进制是01000001,从最低位开始,发送顺序就是1、0、0、0、0、0、1、0。很多人一开始理解不了“从LSB先发”这个点,实际看波形就明白了:最先出现在线上的位,是数值最低的那一位,而不是最高位。
2.2 数据位、校验位与停止位:8-N-1的真实含义
一帧UART数据的完整结构是:
- 1位起始位(低电平)
- 5到9位数据位(最常见是8位)
- 可选的1位校验位
- 1到2位停止位(高电平)
日常配置里最常见的“8-N-1”,意思是8个数据位、无校验位(None)、1个停止位。理解帧结构的时候,把它当成一个字谜:起始位通知“开始”,数据位承载“内容”,停止位通知“结束”。停止位必须保持高电平至少一个位时间,接收方看到停止位之后,线回到空闲高电平状态,等待下一帧。
校验位是可选项。奇校验要求“数据位中1的个数 + 校验位”为奇数,偶校验要求为偶数,它只能发现单比特错误,不能纠正。实际项目中低速近距离通信经常不用校验位,但远距离或电磁环境差的场景,加上至少能帮你判断是不是收到了被干扰的坏数据。
2.3 用9600波特率算一笔账:一个字节到底要传多久
波特率的单位是bps,即每秒传输的比特数。9600波特率意味着每个比特的持续时间是1/9600秒,约104微秒。
以8-N-1帧为例,1帧共10位(1起始+8数据+1停止)。传输一个字节耗时约1.04毫秒,满速状态下每秒大约能传960个字节。把波特率提高到115200,每位时间压缩到约8.68微秒,一帧约86.8微秒,每秒约11520字节。
这张账最好心里有数:如果你要做日志输出、传感器数据上报这类应用,要先算清楚波特率够不够用。有人以为“波特率越高越快,直接拉到最高”,结果忽略了一点:数据位里真正有效载荷只有8/10,效率上限是80%。而且波特率越高,对时钟精度、线缆质量、抗干扰能力的要求越高。
3. 波特率匹配与误差估算:乱码问题的第一嫌疑人
前面说乱码是我刚入门时的噩梦,其实正确定位乱码,关键是搞懂波特率背后那笔数字账。
3.1 波特率为什么会有一堆“奇怪的”标准值
9600、19200、38400、57600、115200,这些“不整不齐”的数字来自历史传承。传统的UART控制器用系统时钟分频来产生波特率,而当时的晶振频率(比如14.7456MHz、11.0592MHz、18.432MHz)往往是一些特殊数值,对这些频率做整数分频,就能得到上述“标准波特率”。这也是为什么很多老练的工程师手里常备11.0592MHz晶振——它能精确分频出9600和115200,误差几乎为零。
现在的MCU内部振荡器已经能做到不错的精度,甚至还能自动校准,但基本原则没变:波特率必须是某个可精确获得的数值。工程上不要自创一个250000之类的非标值,除非你能确认两端芯片都支持、且时钟分频误差可控。省下的那一点点带宽,远不够填乱码的坑。
3.2 分频误差估算:为什么有些板子115200会偶尔错字
UART接收端为了减少误判,一般会在每个位时间的中段采样。理论上,如果收发双方的波特率完全一致,采样点不会漂移。但实际双方本地时钟总有偏差,误差是容许的——通常认为收发两端累积误差在±3%以内还能正常通信,超过这个值就开始频繁出错。
计算分频误差很简单:用单片机时钟频率除以目标波特率的16倍(多数UART外设内部还要再做16倍过采样),得到一个分频系数,取整后再反推实际波特率,两者之差就是误差率。举个例子:8MHz的系统时钟要出115200,8M/(16×115200)=4.34,只能取整数4,反推实际波特率是8M/(16×4)=125000,误差高达8.5%,这个值已经远超容忍范围,通信必然乱码。换成11.0592MHz时钟,11.0592M/(16×115200)=6,刚好整除,误差为零,就能稳定跑115200。
实际开发中,如果你发现某块板子在115200下“偶尔错一两个字节”,可以先用这个办法算一算分频误差。很多国产MCU数据手册里直接给了“波特率误差表”,用之前扫一眼,能省很多排查时间。
3.3 什么时候用校验位、停止位该选1还是2
当链路环境比较差、偶尔有干扰时,加一位校验位能帮你发现错误,从而触发重传或丢弃。代价是每帧多1位传输开销,有效吞吐下降约10%。一些工控协议会在应用层做更强的CRC校验,这时物理层的校验位意义就不大了。
停止位选1还是2,关键在于接收端恢复时钟的能力。老式UART芯片处理能力弱,如果双方波特率有偏差,多给1位停止位相当于多出一段高电平缓冲时间,能让接收端重新对齐帧边界。现在的主流MCU对时钟恢复能力很强,1位停止位足够。只有连接那些十几年前的传感器、工业仪表时,我才会特意把停止位配成2位“迁就”一下。
实际调参有个经验顺序:先确认波特率误差,再决定要不要加校验位,最后才考虑停止位位数。大多数嵌入式设备默认9600或115200、8-N-1就能稳定工作,不要为了“看起来更可靠”盲目加位,每多一个参数,就多一个出错点。
4. TTL、RS-232、RS-485:同样的UART数据,不同的物理载体
UART定义的只是“异步收发”的数据格式,至于电信号用多少伏特表示0和1,UART本身不管,要靠电平标准去定。三种最常见的就是TTL、RS-232、RS-485。
| 标准 | 逻辑1电平 | 逻辑0电平 | 传输方式 | 典型距离 |
|---|---|---|---|---|
| TTL | 2.4V~5V | 0~0.8V | 单端 | 1米以内 |
| RS-232 | -3V~-15V | +3V~+15V | 单端 | 15米左右 |
| RS-485 | 两线压差为正 | 两线压差为负 | 差分 | 1200米以上 |
4.1 板上直连的TTL电平与USB转串口模块
单片机引脚直接输出的UART信号就是TTL电平,3.3V或5V系统。两个板子之间通信,只要电压范围兼容,直接交叉连接即可。
和电脑调试时,需要USB转串口模块。市面上常见的CH340、CP2102、FT232就是干这个的。选择这类模块有几个细节:一是确认模块引脚支持的电平范围是3.3V还是5V;二是检测驱动现象。我的建议是手里常备一个支持跳线切换3.3V/5V的模块,能应付大多数调试场合。
TTL电平最大的弱点是抗干扰差,线路稍长就会变形。如果你把杜邦线拉到15厘米以上还要跑115200,就可以考虑换双绞线或者降波特率了。
4.2 RS-232的负逻辑:为什么老设备要“反着来”
RS-232是老一代PC串口的标准,它的电平值得特别留意:逻辑1是负电压,逻辑0是正电压,和TTL完全反过来。原因是当年为了抗干扰,RS-232把信号幅值拉大到了±12V左右,让接收端能更轻松区分噪声和有效信号。
那为什么取反?更多是电气设计上的综合考虑,并不存在“逻辑上的必然”。对使用者来说,需要记住的是TTL和RS-232不能直接互通,必须经过电平转换芯片,比如MAX3232。如果你在实验室翻出一台带DB9串口的老设备,别把TTL线直接怼上去,先确认它的针脚定义再决定要不要加转换板。
RS-232到今天已经逐步退出民用市场,但工控、医疗、测量仪器领域还在大量用。会看它的引脚定义和技术手册,仍然是硬件调试的基本功。
4.3 RS-485:UART走上差分总线后要处理的半双工方向问题
RS-485把UART的数据放到一对差分线上传输,抗干扰能力和传输距离远高于TTL。逻辑状态不再由单根线对地电压决定,而是由两根线A和B之间的电压差决定。RS-485支持总线上挂几十个节点,是工业现场最常见的UART物理层。
加了RS-485后,UART原本的全双工能力往往保不住了:大部分RS-485实现是半双工,同一时刻要么发要么收,使用前必须控制方向。常见的方案是用单片机的DE(发送使能)引脚控制收发芯片的方向,发送前拉高,发送完拉低,时间把握不好就会“收尾巴”“吞字节”。调试RS-485时,忘记切回接收方向是出现概率极高的问题。
如果只有一组UART但想实现多节点网络,RS-485外接120Ω匹配电阻也值得留意。匹配电阻放在总线两端,用来吸收信号反射。像我这种吃过反射亏的人,现在每做一条RS-485总线,都会先确认两端终端电阻有没有正确放置。
5. 手把手跑通一次真实UART通信:调试顺序与波形验证
讲完原理,说说实操。第一次调UART,我建议按“回环测试→波形确认→对接设备”的顺序走,每一步都能把问题限定在最小范围内。
5.1 回环测试:不依赖对方设备就能自检
回环测试的逻辑很简单:把设备的TX引脚直接短接到自己的RX引脚,然后在串口助手发送数据。如果发送什么收到什么,说明这个串口外设、引脚配置、以及你的软件收发链路都是通的。
用USB转TTL模块接电脑做回环测试时,把TXD和RXD两个引脚用一根短跳线连起来,打开串口助手,选择正确COM口,配置9600、8-N-1,发送一串“hello”。如果能原样收到,说明模块驱动没问题、串口参数没问题。这一步把“电脑到模块”这条链路验证干净了,再去接板子,出问题了就知道锅在板子那边。
5.2 示波器和逻辑分析仪怎么看UART波形
看到波形比猜参数可靠得多。用示波器测UART时,把探头夹在TX引脚和GND之间,触发方式设置为下降沿触发,因为在空闲高电平状态下,起始位的下降沿就是唯一明显的跳变。正常波形应该是:一长段高电平,然后一个低电平脉冲(起始位),中间8个高低不定的脉冲(数据位),最后回到高电平(停止位)。测量相邻两个下降沿之间的时间,如果正好约等于1/波特率,说明发送方的波特率是准的。比如配对9600,一位就该是104微秒上下。
逻辑分析仪比示波器更省事,尤其是需要同时看TX和RX两路信号时。采样率建议拉到波特率的10到20倍以上,太高会占内存,太低则边界不清晰。多数逻辑分析仪软件自带UART协议解析,直接帮你把波形翻译成十六进制数据,非常直观。有一次我在帮朋友调两块板子,看波形发现数据位宽度明显偏窄,一算果然是时钟分频误差导致波特率漂了5%,这类问题光靠肉眼对日志根本查不出来。
5.3 串口助手配参数:波特率、数据位、校验位、停止位一次配齐
串口助手的参数,必须和设备端UART配置完全一致,几个关键项目缺一不可:端口号、波特率、数据位、校验位、停止位、流控(通常关闭)。如果有任何一位对不上,输出的就可能是乱码或残缺数据。
打开串口助手时,还要确认几个容易忽略的设置:十六进制显示和ASCII显示模式,选错了看不出问题,选对了能一眼辨认数据包边界;发送换行符,很多传感器协议要求以回车换行结束,配错会导致设备“没反应”;DTR/RTS默认电平,少数模块会在打开串口时拉低DTR,引起目标板复位,必要时得在软件里关掉这两个信号。
调试过程中,冷启动或热拔插后端口号变了,重新在设备管理器里确认一下,别对着一个消失的COM口傻等。
6. 调板子时容易忽略的UART细节:缓冲、流控与接收方式
最后这部分,全是实战里踩过坑才反推出来的经验,尤其适合正在写UART驱动或应用层逻辑的人。
6.1 丢字节和环形缓冲区
UART接收数据是“来一个字节就产生一次中断”,如果每次中断都做大量处理,比如打印日志、解析协议、操作Flash,很容易在下次数据到来前没处理完。更麻烦的是,MCU内部的UART硬件FIFO一般很小,老经典的16550标准只有16字节深,满了之后后续字节直接被丢弃。
解决思路是在RAM里维护一个环形缓冲区。中断里只做一件事:把读到的字节塞进环形缓冲,清中断标志,立即返回。主循环或RTOS任务再从缓冲里取数据做协议解析。这种“生产者-消费者”模式看似绕了一圈,却是嵌入式接收UART数据最普遍也最稳妥的做法。缓冲区大小要结合波特率和主循环周期设计,比如9600波特率下每秒来960字节,给256字节缓冲区,主循环至少每266毫秒消费一次才不会溢出。
缓冲区代码看起来简单,但注意边界指针绕回、读空写满判断。我见过不少初学者把“头尾指针相等”同时用于判空和判满,结果缓冲区就废了一半,这类细节写驱动时值得多花点心思。
6.2 硬件流控RTS/CTS到底什么时候用得上
UART的流控有两种,一种是硬件流控,靠RTS、CTS两条线“商量”暂停发送;一种是软件流控XON/XOFF,直接在数据流里插入特殊字符。嵌入式设备之间通信,我通常建议关闭流控,因为两根线确实可以省,协议本来就够简单。
但当对方设备缓冲很小、又持续高速发数据时,流控的“反压”能力就非常重要了。典型例子是老式的蓝牙模块或4G模组,它还带串口缓冲,数据一多就丢。这时按模块手册接好RTS/CTS,接收端缓冲快满时主动拉高RTS,对方看到CTS无效就暂停发送,比任何软件重传机制都直接。
调试时如果发现“数据一多就丢失”,先别急着加协议重传,检查一下模块是否默认开启了硬件流控但你没接线,这是没有最脆弱的隐藏前提。
6.3 中断方式接收与DMA:高吞吐场景下的选择
接收方式的选择可以按数据量来分:
- 轮询接收:适合CPU很闲、数据极少的场景,缺点是每来一个字节CPU都要等。
- 中断接收:最主流,配合环形缓冲区,可覆盖绝大多数项目。
- DMA接收:适合高速、大批量、不能打断的场景,比如从4G模块下载固件。
DMA能把数据从UART硬件直接搬进内存,不占用CPU,配合串口空闲中断(IDLE)可以做到“一次接收完整一包不定长数据”。这也是很多HAL库教程里推荐的做法。但DMA接收有个特点:必须等总线空闲中断才能知道“一包数据结束了”,如果对方持续发送不停顿,缓冲可能溢出。所以做DMA方案时,要么约定每包结束留一个小间隙,要么用固定长度帧,二选一,人为给接收端一个“结算”时机。
说实话,UART协议本身只有那几样东西:物理链路、帧格式、波特率、电平标准。但真正把它用得稳,是在这些细节上积累起来的手感。我第一次用DMA接收时,还被“对方发太快导致IDLE永远不来”坑过一回,后来才明白,很多时候不是协议复杂,而是没把时序和缓冲当成一个整体来设计。
再分享一个小习惯:拿到陌生板子,我第一件事永远是先看它能不能用UART回环,能通再谈别的。这三根线连着的,是整个嵌入式世界最朴素也最可靠的信息通道,值得你花心思把每个细节都磨透。