UART这四个字母,但凡摸过单片机、调过串口、刷过路由器的朋友都不会陌生。它可能是很多人接触的第一个通信协议,也是嵌入式开发里最"皮实"、最不容易被淘汰的接口。但恰恰因为它太基础,很多人对它的理解停留在"能打印log就行"的层面,一旦遇到乱码、丢包、电平不匹配、隔离方案选型这些实际问题,就开始抓瞎。这篇内容我打算把UART从物理层到协议层、从TTL到RS232再到RS485、从裸机寄存器到Linux串口编程,完整地捋一遍,顺带把波特率校准、光耦隔离选型、ESD防护这些工程里绕不开的坑一并说清楚。不管你是刚上手STM32的新人,还是已经在做工业通信的老手,应该都能从里面找到点有用的东西。
1. UART到底是个什么东西,别被"串口"这个词糊弄了
1.1 UART和"串口"不是一回事
很多人把UART和串口当成同义词,严格来说这是不准确的。UART全称Universal Asynchronous Receiver/Transmitter,它是一个异步收发器,负责把并行数据转成串行数据发出去,再把收到的串行数据转回并行。它定义的是数据格式和时序,不定义电气特性。而"串口"是一个更宽泛的物理接口概念,它可能跑UART协议,也可能跑别的。
真正决定"这根线能不能直接对接"的,是电气层面的东西。同样是UART协议,TTL电平、RS232电平、RS485差分,三者物理特性完全不同,直接对接轻则通信失败,重则烧芯片。所以理解UART,必须把"协议层"和"物理层"分开看,这是后面所有选型和排错的基础。
1.2 异步通信的核心:起始位、数据位、校验位、停止位
UART是异步的,意思是收发双方没有共享时钟线,全靠事先约定好的波特率来对齐。一帧数据的结构是这样的:
- 起始位:1位,逻辑0,告诉对方"我要开始发了"
- 数据位:5到9位,通常是8位,低位先发
- 校验位:可选,奇校验、偶校验、无校验
- 停止位:1位、1.5位或2位,逻辑1
为什么要有起始位?因为空闲时线路是高电平,突然拉低就代表一个新帧开始,接收方靠这个下降沿来同步自己的采样时钟。这也是为什么波特率误差不能太大——误差累积到采样点偏移超过半个位宽,就会采错。
1.3 波特率和比特率,别混着用
波特率(Baud Rate)指的是每秒传输的码元数,比特率(Bit Rate)是每秒传输的比特数。在UART这种每个码元只承载1比特的场景下,两者数值相等,所以工程上经常混用。但严格讲,如果将来用了多电平调制,两者就不一样了。
常见的波特率:9600、19200、38400、57600、115200、230400、460800、921600。9600是最经典的,很多工业设备和老设备默认就是它。115200在调试打印里最常见。选波特率的原则很简单:双方必须一致,且在链路质量允许的前提下尽量高。线长、干扰、时钟精度都会限制上限。
2. TTL、RS232、RS485:同一套协议,三种完全不同的脾气
2.1 TTL电平:板级通信的默认选择
TTL电平的UART就是我们平时在STM32、ESP32、树莓派上看到的TX/RX。它的逻辑是:
- 逻辑1:约3.3V或5V
- 逻辑0:约0V
注意这里有个容易搞混的点——TTL是正逻辑,高电平代表1。而RS232是负逻辑,这个后面说。
TTL的优点是简单、便宜、速度快,缺点是抗干扰差、传输距离短,一般板内或几十厘米内使用。跨板通信如果线稍微长一点,就容易受干扰。另外3.3V和5V系统对接时要注意电平匹配,5V的TX直接接3.3V的RX,长期可能损伤3.3V器件的输入级。
2.2 RS232:负逻辑、高电压、老而弥坚
RS232的逻辑电平很反直觉:
- 逻辑1:-3V到-15V
- 逻辑0:+3V到+15V
也就是说,它是负逻辑,而且电压摆幅大。这种设计在早期是为了抗干扰和长距离(相对TTL而言),典型传输距离15米左右。RS232是点对点的,一对一通信,不能挂总线。
因为电平和TTL完全不同,两者对接必须经过电平转换芯片,比如经典的MAX232、SP3232。很多人第一次用USB转串口模块接老设备,发现死活不通,八成就是TTL和RS232没转换。
2.3 RS485:差分、总线、工业现场的主力
RS485不是一种协议,而是物理层标准,它上面跑的往往还是UART格式的数据。它的核心特点是:
- 差分传输:用A、B两根线,靠两根线的电压差表示逻辑,共模干扰会被抵消
- 半双工:同一时刻只能收或发,需要方向控制引脚(DE/RE)
- 多点总线:一条总线上可以挂几十个节点
- 传输距离远:几十米到上千米(低速时)
工业上大量使用RS485跑Modbus RTU,就是这个原因。RS485和RS232的区别,本质上是"差分抗干扰+总线"对"单端点对点"的碾压,代价是需要方向控制和终端电阻匹配。
| 特性 | TTL | RS232 | RS485 |
|---|---|---|---|
| 逻辑 | 正逻辑 | 负逻辑 | 差分 |
| 电平 | 0/3.3V或5V | ±3~15V | 差分±1.5V以上 |
| 距离 | 几十厘米 | 约15米 | 可达1200米 |
| 拓扑 | 点对点 | 点对点 | 多点总线 |
| 典型场景 | 板内调试 | 老设备、PC串口 | 工业现场、Modbus |
3. 波特率校准:为什么你的串口总是乱码
3.1 乱码的第一嫌疑犯永远是波特率
串口调试最经典的场景:接上线,打开助手,满屏乱码。这时候90%的情况是波特率不匹配。接收方按自己的波特率去采样,和发送方的位宽对不上,采出来的就是垃圾。
排查顺序建议这样:先确认双方波特率设置一致,再确认数据位、校验位、停止位一致,最后才怀疑硬件。很多人一上来就怀疑线坏了、芯片坏了,其实往往就是助手里的波特率下拉框选错了。
3.2 时钟误差是怎么累积的
UART接收方通常在起始位下降沿开始计数,然后在每个位的中间点采样。理想情况下,采样点永远落在位中心。但如果发送方和接收方的波特率有误差,采样点会逐渐偏移。
假设波特率误差为e,一帧10位(1起始+8数据+1停止),最坏情况下到第10位时,累积偏移是10×e个位宽。要保证采样点不越界,偏移必须小于0.5个位宽,所以:
允许的最大波特率误差 ≈ 0.5 / 10 = 5%
这是理论极限。实际工程里,为了留余量,一般要求双方误差都在2%以内,最好1%以内。如果用内部RC振荡器做时钟源,温漂可能就有几个百分点,这就是为什么高波特率下内部RC容易翻车,而外部晶振稳得多。
3.3 用外部晶振还是内部RC
STM32这类MCU,UART时钟一般来自APB总线,而APB又来自系统时钟。如果系统时钟用内部HSI(精度通常±1%左右,全温区可能更差),跑115200可能勉强,跑921600就悬了。用外部晶振(精度±10~50ppm)就稳得多。
我的经验是:调试打印用内部RC无所谓,正式产品尤其高速通信,一定上外部晶振。另外STM32的UART有过采样机制(16倍或8倍过采样),8倍过采样能容忍更高的时钟误差,但抗噪能力下降,需要权衡。
3.4 一个实用的校准思路
如果实在只能用内部RC,可以在出厂时做一次校准:用一个已知准确的参考(比如GPS的PPS或者另一个晶振系统)测出实际波特率偏差,然后把偏差值写进配置,调整分频系数补偿。这在成本敏感的消费类产品里挺常见。
4. 隔离与防护:光耦、ESD、防静电这些事
4.1 什么时候需要隔离
只要通信双方存在地电位差,或者一端是高压/强干扰环境,就必须考虑隔离。典型场景:
- 工业现场,设备之间地线很长,地电位差可能几伏甚至几十伏
- 医疗设备,需要电气隔离保证安全
- 电机驱动、继电器附近的通信线
不隔离的后果:轻则通信误码,重则烧毁接口芯片,甚至通过地线把干扰串到整个系统。
4.2 9600波特率用什么光耦隔离合适
这是热词里出现的问题,很实际。光耦隔离的核心矛盾是速度和成本。普通光耦(如PC817)响应慢,上升下降沿都是微秒级,跑9600波特率(位宽约104微秒)是够的,但跑115200(位宽约8.7微秒)就吃力了。
选型思路:
- 9600及以下:PC817、EL817这类普通光耦完全够用,便宜
- 115200:需要高速光耦,如6N137、EL357(部分型号)、TLP2362
- 更高速:考虑数字隔离器,如ADuM系列、Si86xx系列,速度快、寿命长、一致性好
用光耦时要注意上下拉电阻和限流电阻的配合。光耦输出端是集电极开路,需要上拉电阻;输入端需要限流电阻控制LED电流(一般5~15mA)。电阻选不好,要么速度上不去,要么功耗大、发热。
4.3 RS232通信防静电选什么ESD管
RS232接口经常要插拔,暴露在外,静电是头号杀手。选ESD管主要看几个参数:
- 工作电压:要高于RS232的正常电平摆幅(±15V),一般选24V左右的
- 钳位电压:越低越好,保护后级
- 结电容:RS232速率不高,电容要求不严,几十pF可以接受
- 响应速度:要快,纳秒级
常见的选择是TVS二极管阵列,比如SM712就是专为RS485/RS232设计的,双向、低电容。单路的话可以用PESD系列。关键是ESD管要尽量靠近接口,走线短,否则保护效果大打折扣。
4.4 一个容易忽略的点:共模电感
在RS485这种差分总线上,除了TVS,还经常加共模电感来抑制共模干扰。选的时候注意差分阻抗要匹配,否则会影响信号质量。这个细节很多新手会漏掉。
5. 从裸机到Linux:UART编程的两种世界
5.1 STM32 HAL库下的UART初始化
用HAL库配置UART,核心是填UART_HandleTypeDef结构体,然后调HAL_UART_Init。关键参数:
huart1.Instance = USART1; huart1.Init.BaudRate = 115200; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart1.Init.OverSampling = UART_OVERSAMPLING_16; HAL_UART_Init(&huart1);发送用HAL_UART_Transmit(阻塞)或HAL_UART_Transmit_IT(中断)或HAL_UART_Transmit_DMA(DMA)。接收类似。新手最容易踩的坑:阻塞发送在高速率下会占用大量CPU,实际项目里接收一般用中断或DMA+空闲中断的方式,避免丢数据。
STM32的UART管脚定义要看具体型号的datasheet,一般USART1是PA9(TX)/PA10(RX),但复用功能要记得开GPIO时钟和AF配置,这一步漏了就是"初始化了但没反应"。
5.2 Linux下的串口编程
Linux把串口当文件处理,核心是termios结构体:
int fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY); struct termios opt; tcgetattr(fd, &opt); cfsetispeed(&opt, B115200); cfsetospeed(&opt, B115200); opt.c_cflag |= (CLOCAL | CREAD); opt.c_cflag &= ~CSIZE; opt.c_cflag |= CS8; opt.c_cflag &= ~PARENB; opt.c_cflag &= ~CSTOPB; tcsetattr(fd, TCSANOW, &opt);几个关键点:O_NOCTTY防止串口成为控制终端;CLOCAL忽略调制解调器控制线;CREAD使能接收。原始模式(raw mode)下还要关掉回显、行缓冲等,否则读到的数据会被加工。Linux下读串口建议用select或poll做超时控制,别傻等。
5.3 中断、DMA、空闲中断:接收的正确姿势
不管裸机还是Linux,UART接收的难点都是不知道对方什么时候发完。常见方案:
- 定长接收:协议规定固定长度,简单但不够灵活
- 超时判断:一段时间没收到新字节就认为一帧结束
- 空闲中断(IDLE):STM32特有的,总线空闲一个字节时间就触发中断,配合DMA非常好用
我个人的偏好是DMA+IDLE:DMA负责搬数据不占CPU,IDLE负责判断帧结束。这套组合在Modbus这类变长协议里特别顺手。
6. 那些年踩过的UART坑,以及怎么爬出来
6.1 TX接TX,RX接RX
这是最经典的新手错误。UART是交叉连接:A的TX接B的RX,A的RX接B的TX。接反了就是完全没数据。更隐蔽的是有些模块丝印标的是"对方视角",容易看错,接线前一定查清楚。
6.2 忘记共地
TTL电平通信,如果两块板子不共地,参考电平不一致,通信必然失败甚至损坏。TTL通信必须共地。RS485差分虽然抗共模,但也要有参考地,只是要求没那么严。
6.3 电平不匹配烧芯片
5V的TX接3.3V的RX,短时间可能没事,长期会损伤。反过来3.3V的TX接5V的RX,可能因为高电平不够而被识别错误。跨电压域要么用电平转换芯片,要么用电阻分压(仅限低速、单向)。
6.4 刷机场景下的TTL
热词里提到"k2p拆机ttl刷breed""hi3798 ttl线刷"这类,本质都是通过UART进入设备的bootloader或uboot。要点:
- 找到板子上的TX、RX、GND(有时还有VCC,但刷机时一般不要接VCC,设备自己供电)
- 波特率通常是115200,个别是57600
- 上电瞬间狂按快捷键(不同uboot不一样)进入命令行
- 注意TTL模块的电平,老设备可能是5V,新设备3.3V
刷机有风险,接错线、电压不对可能变砖,操作前一定确认好。
6.5 红外调制和波特率
红外通信(IrDA)在UART基础上加了38kHz左右的调制,波特率一般不高(9600、115200)。如果直接用UART接红外收发头,需要额外的调制电路,或者用带IrDA功能的UART。这个场景现在少了,但空调遥控、老式设备还在用。
7. 选型和设计时我会怎么想
做项目选通信方案,我的决策链大概是这样:
第一步,看距离和拓扑。板内、几十厘米,TTL直接上;点对点十几米,RS232;多点、几十米以上、工业环境,RS485。
第二步,看干扰和地电位。有强干扰或地电位差,隔离必须上。光耦还是数字隔离器,看速率和预算。
第三步,看速率需求。9600够用就别上115200,速率越高对时钟、线材、隔离器件要求越高,成本和风险都上升。
第四步,看防护。接口外露的,TVS、共模电感、限流电阻该加就加,别省这点钱,返修成本高得多。
第五步,看软件架构。裸机还是Linux,中断还是DMA,定长还是变长,这些在硬件设计阶段就该想清楚,别等板子回来了才发现接收方案不合适。
UART这东西,入门容易精通难。协议本身简单到几页纸就能讲完,但真正把它用稳、用可靠,涉及的是电平、时钟、隔离、防护、软件架构一整套工程能力。我见过太多项目在实验室跑得好好的,一到现场就各种通信异常,根子往往不在协议,而在这些"周边"细节上。把物理层吃透,把误差算清楚,把隔离和防护做到位,UART才能真正成为你手里那个"皮实"的通信接口,而不是三天两头出问题的麻烦制造者。