ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

UART协议彻底搞懂:从帧结构到串口调试实战

UART协议彻底搞懂:从帧结构到串口调试实战 1. 为什么要彻底搞懂UART协议异步串行通信依旧是嵌入式世界的“通用语”做嵌入式开发这些年我越来越觉得UART协议是门槛最低、却最容易被忽视的一个通信外设。很多新手上来就盯着SPI、I2C、CAN这些“听起来高级”的总线反而把串口当作一个“调printf用的”附属品。但实际上UART协议是整个异步串行通信体系中最经典的实现它解决了设备与设备之间最朴素的信息交换问题一根线发一根线收双方约定好速度就能稳定通信。我们日常调试中看到的串口打印日志、GPS模块输出的NMEA语句、蓝牙模块的AT指令交互、老式工控设备的RS232接口底层全都是UART协议在支撑。这篇内容我打算彻底展开聊一次UART从帧结构、时序、波特率误差、电平标准到MCU上的USART外设配置、环形缓冲区设计、常见故障排查尽量把整个链路讲透。不管你是刚接触单片机的学生还是被串口乱码、丢数据折腾过的在职工程师这篇文章应该都能给你一些参考。我会尽可能讲清楚背后的为什么而不只是贴配置代码。2. UART协议基本盘异步、全双工、串行这三个词意味着什么2.1 异步串行通信与“各自带钟”的同步机制UART全称是Universal Asynchronous Receiver/Transmitter通用异步收发器。它的“异步”两个字是理解整个协议的关键。所谓异步指的是通信双方不需要共享一个时钟信号。发送方按照自己的节奏把数据一位一位放到TX线上接收方按照预先约定的速度在同一根RX线上采样。双方之所以能对上拍子不是因为有根时钟线在协调而是因为每个设备都有自己的“钟表”——也就是波特率时钟。我用一个生活化的例子来类比你和朋友约好在某个时间点同时看表然后每秒钟说出一个数字。只要两块表走时误差足够小、校准过你不需要喊“现在开始”也能保持同步。UART就是类似的逻辑它通过一个起始位的下降沿来“对表”之后的每一位都在约定的时间间隔上采样。正因如此UART通信双方必须提前约定波特率Baud Rate也就是每秒传输多少个码元。默认情况下码元就是比特所以大家常把“波特率”和“比特率”混用。这种设计带来的好处非常直接硬件简单。只需要两根数据线TX和RX再加一根共地线就能实现双向通信。相比SPI需要SCK时钟线、I2C需要SCL时钟线UART在远距离和简单场景下的接线成本最低。代价是它的效率不算高每个字节要额外附带起始位、停止位甚至校验位而且对收发双方时钟精度有一定要求如果两边波特率偏差过大数据就会出错。2.2 全双工能力与UART的硬件连接方式UART是支持全双工的也就是可以同时发送和接收数据。因为发送和接收是两条独立的物理线路设备A的TX连接设备B的RX设备A的RX连接设备B的TXGND与GND相连。这种交叉连接的方式很容易理解但我见过太多人在这里栽跟头——直接把TX接TX、RX接RX结果什么也收不到。记住一个口诀发送对接收接收对发送地线必须通。这里额外多说一句地线。UART本质上是以地线为参考电平的电压信号传输如果没有共地收发双方的电压参考点不一致接收方读到的电平就是不确定的。在短距离、同电源供电的场景下共地问题不明显一旦两个设备各自用不同的电源适配器供电不接GND就很容易出现乱码或完全收不到数据的情况。从硬件结构上看一个典型的UART外设内部包含波特率发生器、发送移位寄存器、接收移位寄存器、发送缓冲寄存器TX Buffer、接收缓冲寄存器RX Buffer以及控制逻辑。CPU只需要把要发送的字节写入发送缓冲寄存器硬件就会自动按帧格式一位一位地挪出去接收方向硬件检测到起始位后自动把后续数据位收进来存到接收缓冲寄存器并触发接收中断通知CPU取走。这个“硬件自动打包拆包”的过程极大解放了CPU也正是UART能成为最通用的串行通信接口的原因之一。2.3 为什么UART协议到今天还不过时很多人会问现在USB、以太网这么普及UART这种几十年前的老协议为什么还在每一块MCU上存在原因很简单它够简单、够可靠、成本趋近于零。对于一个8引脚的小单片机来说片上集成一个UART外设几乎不占什么资源却能让它跟PC、Wi-Fi模块、蓝牙模块、传感器模块完成最基本的通信。几乎所有无线通信模组都保留了UART作为AT指令控制口所有带调试功能的高端芯片都会把日志输出到UART上。所以学习UART不是学一个“过时的古董”而是在掌握嵌入式系统调试和通信的底层通用语。理解了UART你再看RS485总线、MODBUS协议、LIN总线会发现它们都是在UART物理层基础上的扩展或应用层封装很多知识是相通的。3. UART数据帧结构深度拆解起始位、数据位、校验位、停止位是怎么协同工作的3.1 一帧数据的完整旅程UART的数据传输单位叫“帧”Frame。一个典型的UART帧由以下部分组成空闲态Idle、起始位Start Bit、数据位Data Bits、校验位Parity Bit可选、停止位Stop Bit。线路空闲时TX线保持在高电平。当发送方准备发送一个字节时先把TX线拉低一个位时间这就是起始位。接收方检测到这个高到低的跳变后就知道“数据要来了”开始按波特率在每一位的中间点采样。数据位从最低有效位LSB开始发送最高有效位MSB最后发送。数据位长度可以是5到9位最常用的是8位。数据位之后是可选校验位它用于简单的错误检测。最后是停止位线路被拉回高电平并保持至少1位时间代表这一帧结束。停止位可以是1位、1.5位或2位。为了直观说明我把最常用的8N1帧格式整理成表格。所谓8N1就是8个数据位、无校验、1个停止位。组成位长度电平作用空闲态任意高电平线路空闲状态无数据传输起始位1 bit低电平标志一帧数据的开始用于同步数据位8 bit高或低实际载荷数据LSB先发送停止位1 bit高电平标志一帧结束给接收方处理时间如果选择偶校验发送方会统计数据位中“1”的个数让“数据位校验位”中1的总数为偶数。比如数据是0b01000001其中有两个1已经是偶数校验位就填0。如果是奇校验则让1的总数为奇数。接收方收到数据后会重新计算校验如果与收到的校验位不符就认为这一帧出了错。要注意的是校验只能发现单个比特错误无法纠错也无助于检测偶数位翻转的错误。所以在实际工程中校验位用的并不多很多场景宁可去掉校验位通过更高层协议比如CRC来保证完整性。3.2 每个字节的采样点与位时序计算方法UART接收端是怎么知道每一位在哪采样的呢核心是靠“检测起始位下降沿 本地波特率时钟计数”。假设波特率是9600则每位的持续时间约为 1 / 9600 ≈ 104.17微秒。接收方在检测到起始位下降沿后通常会等约半个位时间52微秒左右再采样确保采到的是起始位的中间位置而不是边沿的毛刺。然后每隔一个位时间采样一次依次采数据位、校验位、停止位。这里的关键是采样点必须落在每一位的中间。如果收发双方的波特率有偏差偏差会随着每一帧的位数累积。一个字节有10位起始位8数据位停止位假设发送方实际比约定快2%那么第10位时累积误差约为20%个位时间采样点则从位中心偏移到约0.7位宽处。虽然还在位区间内但已经非常危险。所以一般工程上要求双方波特率误差在±2%以内最好在±1%以内。这也是为什么配置MCU串口时经常遇到“分频数取整导致波特率有误差”的问题下面我会专门算一次。3.3 波特率与分频器为什么单片机生成的波特率永远有误差UART的波特率不是凭空产生的它由外设时钟经过分频得到。以STM32的USART为例波特率计算公式大致是BRR值 外设时钟频率 / 目标波特率。但BRR是一个整数寄存器分频结果必须取整这就难免产生误差。举个例子外设时钟为72MHz目标波特率11520072MHz / 115200 625。这个数刚好是整数所以能精确产生115200。但如果外设时钟是16MHz16MHz / 115200 138.888...只能取138或139对应的实际波特率分别是115942和115108误差分别约为0.64%和0.09%。单独看都在容限内但如果你两边的晶振本身还有误差累计起来就可能出问题。所以配置串口时建议在初始化后把实际波特率算一遍确认误差在合理范围。这里分享一个快速检查方法直接读取波特率寄存器BRR反推实际波特率再和目标值比对。很多芯片的参考手册里还有误差速查表像STM32的DS文档里会列出常用主频下各波特率的误差值选一个误差最小的组合即可。4. UART电平标准与接口选型TTL、RS232、RS485到底有什么区别4.1 电平标准的本质差异谁用正逻辑谁用负逻辑谁用差分UART协议描述的是“帧结构”和“时序”但它没有规定信号必须用多少伏的电压来表示。这就引出了电平标准的概念。不同的电平标准决定了信号传输的距离、抗干扰能力和接线方式。最基础的是TTL电平常见于单片机系统内部3.3V或5V供电。TTL电平用高电平表示逻辑1低电平表示逻辑0属于单端信号。它的优点是电路简单、直接和MCU引脚兼容缺点是抗干扰能力弱、传输距离短一般不超过一米。如果你拿杜邦线在试验板上短距离连接两个模块直接用TTL就行。RS232是传统的串口标准用在PC和工控设备上。RS232使用负逻辑-3V到-15V表示逻辑13V到15V表示逻辑0。它比TTL多了电压摆幅所以传输距离可以更远理论上十几米但它不能直接和MCU引脚相连必须经过电平转换芯片比如MAX232把TTL信号转成RS232信号。还有一个容易踩的坑RS232的发送和接收电平都是负逻辑不能想当然地认为“高电平就是1”。RS485则是另一个方向它使用差分信号A、B两根线之间的电压差来决定逻辑状态抗共模干扰能力很强传输距离可达1200米。RS485通常是半双工的一发一收共用一对线需要方向控制。它也不是直接连MCU的需要RS485收发器芯片如MAX485来完成TTL电平到差分电平的转换。标准逻辑1电平逻辑0电平传输方式典型距离典型场景TTL2V~5V0V~0.8V单端0.5m~1m板级通信、调试口RS232-3V~-15V3V~15V单端15m左右PC串口、老工控设备RS485差分正电平差分负电平差分1200m工业现场总线、MODBUS4.2 调试场景下如何选择USB转串口工具现在笔记本基本没有DB9串口了调试UART最常用的就是USB转TTL工具。市面上的方案主要分两种基于CH340/CP2102这类芯片的普通转接板以及基于FTDI芯片的转接板。CH340便宜对大多数调试场景足够FTDI稳定但贵主要用在需要量产下载或工业级调试的场景。选的时候注意两点一是看模块是否支持目标设备的IO电平。很多USB转TTL模块默认输出5V TTL如果你的MCU是3.3V供电最好选带跳线切换3.3V/5V的版本否则5V电平可能通过RX引脚倒灌进MCU的IO长期使用有风险。二是看驱动兼容性。主流的CH340和CP2102在Windows、Linux、macOS下都有驱动但有些系统需要手动安装购买时先确认。从经验上说调试串口日志这种事我强烈建议在信号链路上预留一个可测试的节点。比如在PCB上引出TX、RX、GND三根测试点或者直接用手头的排针把TX、RX、GND引出来。等到设备出问题时这就是救命通道。4.3 长距离和工业场景下的选型建议如果你的项目需要在嘈杂的工厂环境里传输几十米以上别犹豫直接上RS485。RS485是差分信号两条线上感应到的噪声基本是共模的差分接收器会把它抵消掉。而且RS485支持多点挂接最多可以并联32个节点标准负载条件下配合MODBUS协议就能搭出一套完整的工业总线网络。转换方案上MCU侧一般通过UART连接MAX485这类收发器再由收发器驱动双绞线。方向切换由MCU控制DE/RE引脚发送前拉高DE发送完成后拉低DE再切回接收模式。这里有个关键细节半双工模式下如果在发送完成后立刻切换方向可能会把发送数据最后一个停止位截断导致对端报错。所以一般需要在发送完最后一位后延时一小段时间半位到一个位时间再切换方向。5. 实操在MCU上配置UART/USART外设并实现可靠收发5.1 初始化配置的关键参数与寄存器逻辑接下来我以最常见的STM32F1系列为例带大家走一遍完整的UART实战配置流程。虽然不同芯片的寄存器名略有不同但思路完全一样。第一步是开启GPIO和USART外设时钟把TX引脚配置为复用推挽输出RX引脚配置为浮空输入或复用开漏输入。第二步是配置USART的关键参数波特率、字长、停止位、校验位、硬件流控、收发模式。第三步是使能接收中断并配置NVIC优先级。第四步是在主循环里编写发送函数和中断接收回调。标准库和HAL库的写法有差异但底层最终都是在写USART_BRR、USART_CR1这些寄存器。关于硬件流控多说一句。USART的硬件流控使用RTS和CTS引脚发送方在发送前检查CTS引脚电平如果对端拉低表示“暂停发送”接收方在接收缓冲区快满时拉低RTS通知对方暂停。硬件流控在低速MCU之间通信时可能有用但在多数调试场景下用不到。如果误开了硬件流控而实际没有接线串口就完全收不到数据。这是一个非常隐蔽的问题。遇到串口完全不通的情况务必先检查USART_CR3里的RTSE和CTSE位是否被意外置1。5.2 发送函数与变长数据缓冲区的处理直接写一个阻塞式发送函数void uart_send_byte(UART_TypeDef *USARTx, uint8_t data) { while (!(USARTx-SR USART_FLAG_TXE)); USARTx-DR data; }这段代码的逻辑就是等TXE发送数据寄存器空标志位置1然后把数据写入DR寄存器。硬件会自动把DR里的数据挪进移位寄存器一位一位发送出去。如果发送多个字节就循环调用。这种方式在低速日志打印中够用但如果在中断里频繁做长字符串打印会阻塞整个系统所以复杂场景下建议使用发送缓冲区中断发送的机制。接收方向最容易出问题的是“接收不定长数据”。串口数据不是一次来一包而是一个字节一个字节连续到来的而且接收方事先不知道对端会发多少字节。处理思路有两种一是用固定帧长协议比如约定每条指令都是8字节收满8字节就处理二是用帧头帧尾超时判断比如以0xAA开头、0x55结尾或者串口空闲一段固定时间如3.5个字符时间认为一帧结束。第二种方式在MODBUS等协议里很常见在STM32上可以借助空闲中断IDLE来实现。所谓空闲中断就是检测到总线上一个字节传输完成后连续一段时间没有新数据到来硬件就触发一次中断。这个中断非常适合用于判断“一帧数据收完了”。5.3 环形缓冲区设计解决中断接收与主循环处理的并发冲突中断接收方式最常见的问题是主循环在处理缓冲区时中断又往缓冲区里写入新数据导致数据覆盖或逻辑错乱。所以工程上普遍采用环形缓冲区。环形缓冲区的基本思想是用一块连续内存作为队列写指针Write Index由中断服务函数更新读指针Read Index由主循环更新。当写指针追上读指针时认为缓冲区满当读指针追上写指针时认为缓冲区空。环形缓冲的优势在于不需要把数据搬来搬去也不需要频繁关闭中断。下面是一个简单的环形缓冲区实现#define UART_RX_BUFFER_SIZE 256 static volatile uint8_t rx_buffer[UART_RX_BUFFER_SIZE]; static volatile uint16_t rx_write_index 0; static volatile uint16_t rx_read_index 0; void uart_rx_isr(void) { uint8_t data USART_ReceiveData(USART1); uint16_t next (rx_write_index 1) % UART_RX_BUFFER_SIZE; if (next ! rx_read_index) // 如果缓冲未满则存储 { rx_buffer[rx_write_index] data; rx_write_index next; } } uint16_t uart_buffer_count(void) { return (uint16_t)((rx_write_index - rx_read_index UART_RX_BUFFER_SIZE) % UART_RX_BUFFER_SIZE); } uint8_t uart_buffer_read(void) { uint8_t data rx_buffer[rx_read_index]; rx_read_index (rx_read_index 1) % UART_RX_BUFFER_SIZE; return data; }这里有一个工程细节值得注意读索引和写索引使用的是volatile关键字因为它们在中断和主循环之间共享。对于单字节的读和写操作在大多数单片机上只要保证读写顺序正确不会出现撕裂问题。如果是在多核或带DMA的场景还需要考虑内存屏障和更复杂的同步机制。如果你在STM32上使用HAL库也完全可以在串口接收中断回调里做同样的环形缓冲逻辑原理完全一致。5.4 接收不定长数据的完整实现框架有了环形缓冲区接收不定长数据的框架就清晰了。启用USART的RXNE中断每收一个字节就进入ISR存入环形缓冲区。另外启用空闲中断当总线空闲时在空闲中断里把一个“帧完成标志”置1。主循环检测到这个标志后把环形缓冲区里从上次处理位置到当前位置的所有数据当作一帧进行处理然后复位标志。void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) SET) { uint8_t data USART_ReceiveData(USART1); uint16_t next (rx_write_index 1) % UART_RX_BUFFER_SIZE; if (next ! rx_read_index) { rx_buffer[rx_write_index] data; rx_write_index next; } USART_ClearITPendingBit(USART1, USART_IT_RXNE); } if (USART_GetITStatus(USART1, USART_IT_IDLE) SET) { frame_ready 1; frame_length (rx_write_index - rx_read_index UART_RX_BUFFER_SIZE) % UART_RX_BUFFER_SIZE; USART_ReceiveData(USART1); // 清除IDLE标志 USART_ClearITPendingBit(USART1, USART_IT_IDLE); } }这种框架的一个变体是在CAN和MODBUS从机中非常常见。注意一点清除IDLE标志的标准动作是“先读SR寄存器再读DR寄存器”不同芯片的清除方式可能不同。在HAL库中处理IDLE中断也容易踩坑务必严谨对照参考手册。6. 串口调试中的常见问题与排查实战乱码、丢数据、通信不稳定6.1 收到乱码波特率不匹配以外还有地线和电平因素乱码是串口调试里最典型的症状。我遇到的案例中80%是因为波特率配置不一致15%是因为地线没接好剩下5%是电平不匹配。排查思路从最可能的入手先确认收发双方的波特率、数据位、停止位、校验位是否完全一致。最简单的方法是先用逻辑分析仪或示波器抓一下TX引脚的波形数一数一帧的位宽用位宽直接反推波特率。如果波形显示的各位宽度不均匀大概率是发送端的时钟源有问题比如外部晶振没起振、MCU跑在了内部RC振荡器上导致频率偏了。地线导致的乱码比较隐蔽。两个设备各自独立供电时如果GND没接接收端采样到的电压是浮动的信号会畸变。这时数字万用表量一下两个设备的GND之间有没有压差如果有几伏的电压说明地没共好。补上GND线基本就能解决。还有一种情况是TTL电平信号线过长比如超过20cm的杜邦线在高速率下就容易引入串扰导致数据位翻转。遇到这种场景要么降低波特率要么换成双绞线或屏蔽线要么改用RS485。6.2 完全收不到数据从接线、引脚复用和中断三方面排查完全收不到数据时先不要急着怀疑代码按下面这个顺序排查。第一确认RX/TX有没有交叉连接。用万用表测一下设备A的TX到设备B的RX之间的通断确认虚焊和杜邦线松脱。第二确认USART外设的引脚有没有被其他功能复用。很多STM32的引脚是AFIO复用的一旦GPIO模式配置错误或者调试器占用了串口引脚比如PA9、PA10经常复用为USART1数据就进不来。第三确认中断是否真的触发了。在ISR入口加一个翻转LED的测试语句如果LED闪烁说明中断触发正常问题在缓冲逻辑如果不闪问题在中断配置或NVIC。第四确认接收使能位和串口时钟是否打开这个低级错误其实发生频率不低。还有一个容易忽略的地方是RX引脚的空闲电平。如果RX引脚被外部电路拉死在低电平UART会一直认为收到起始位不断尝试接收导致正常数据永远无法被识别。用示波器量RX引脚正常空闲状态应该稳定在高电平。6.3 数据偶发丢失和帧错位环形缓冲与帧同步的工程细节数据偶发丢失第一嫌疑是接收缓冲区溢出。如果ISR处理太快、主循环处理太慢写入指针追上读取指针新数据就会被丢弃。解决办法是把缓冲区调大或者改用DMA接收或者降低应用层的处理耗时。第二嫌疑是开启了硬件流控但没接RTS/CTS导致发送端认为对端一直忙数据积压。第三嫌疑是中断优先级冲突比如USB中断或DMA中断把UART中断抢占了在极端情况下导致UART的RXNE标志没来得及清除新数据无法进入。排查时可以临时把所有其他中断优先级调低观察丢数据是否消失。帧错位则是另一个维度的问题。当数据流中出现干扰或者上电瞬间的毛刺接收方可能错误地检测到一个“假起始位”导致后续数据全部错位。这时候仅仅靠UART的帧结构无法恢复同步因为UART没有完整的帧同步机制错位之后只能等下一个起始位。工程上常见的解决办法是应用层加帧头帧尾比如用0xAA 0x55作为包头接收方扫描缓冲区找到合法的帧头后再解析后续数据。如果连续多帧校验失败说明可能需要重新同步或触发对端复位。6.4 调试工具与现场排障速查表这里放一张我平时常用的串口问题速查表照着顺序排查能省很多时间故障现象可能原因排查方法解决措施完全无数据TX/RX交叉错误万用表量通路交叉连接TX/RX完全无数据共用GND缺失量两设备GND压差补GND线完全无数据硬件流控误开检查CTSE/RTSE位关闭硬件流控乱码波特率不一致或偏差大示波器测位宽重配波特率并进行误差计算乱码地电位不等量GND间压差接共地偶发丢数据环形缓冲区溢出观察buffer_count扩容或DMA接收帧错位干扰导致假起始位逻辑分析仪抓帧应用层加帧头帧尾单字节长时间卡死ISR处理过长检查ISR内部循环减少ISR操作只做入队逻辑分析仪是我调试UART的必备工具。它不需要很贵二三十块钱的即可但能把UART波形解码成直观的ASCII字符和帧格式排查乱码、错位问题非常高效。用法很简单把逻辑分析仪的通道0接到TX引脚GND接好在软件里选择UART协议解码填上波特率、数据位、停止位和校验位设置就能实时看到每一帧的数据。如果逻辑分析仪解码正常但单片机接收不正常问题一定在单片机侧如果逻辑分析仪解码也不正常问题就在发送端。7. 一些我坚持了很多年的UART使用习惯做嵌入式越久越觉得UART虽然简单但用好的细节非常多。最后分享几个我自己长期保持的习惯希望能给刚开始接触UART协议的朋友一点参考。第一所有MCU程序里串口打印日志一定要带上帧定界和可读性标识。比如用[INFO]、[ERROR]这种前缀再配合时间戳排查问题时会省很多精力。第二量产设备里如果条件允许尽量保留一个UART调试口哪怕不上排针也要在PCB上留测试点。第三调试时遇到问题不要急着换波特率先用示波器看一下真实波形很多时候问题不在软件而在电平、地线和接线。第四用HAL库开发时串口初始化后建议主动读一次SR和DR寄存器把残留标志清干净避免上电瞬间的垃圾数据触发一次接收中断。UART协议已经陪伴嵌入式工程师走了几十年未来也还会继续存在下去。真正吃透它不只是会调一个printf而是理解串行通信在“时序、电平、同步、缓冲”这些维度上的设计取舍。这份理解才是你做任何高性能通信协议时最扎实的地基。
返回列表