☰
USART串口通信从原理到实战:帧格式、波特率与调试技巧全解析
2026/10/18 1:54:15 网站建设 项目流程

如果你在嵌入式开发这行泡过一阵子,肯定绕不开USART这个名字。不管是做单片机入门、驱动传感器、对接蓝牙模块,还是跟工控设备打交道,USART几乎是所有串行通信的起点。可以说,搞懂了USART,你再去看SPI、I2C,会发现很多概念其实是相通的。这篇文章不打算泛泛地讲概念,我会从工程实操的角度,把USART的硬件底层、帧格式、波特率计算、配置流程、调试方法和那些年踩过的坑,一次讲透。

USART全称是Universal Synchronous/Asynchronous Receiver/Transmitter,通用同步异步收发传输器。名字里带了个“同步”,这也是它和UART最本质的区别——USART在异步通信的基础上,额外支持同步模式,可以对外提供时钟信号。不过在实际项目里,绝大多数人用的都是USART的异步模式,也就是把它当UART用。所以很多新手会有疑惑:明明芯片手册上写的是USART,为什么例程里配置的全是异步内容?别急,这个后面展开聊。

这个内容适合谁?刚接触嵌入式开发的学生、从Arduino过渡到STM32或其它国产MCU的开发者、以及已经在做项目但主要靠复制例程、对串口底层细节一知半解的工程师。看完这篇,你能自己写出稳定的收发代码,能在示波器或逻辑分析仪上判断帧格式对不对,还能在遇到乱码、丢帧、收发异常时快速定位问题方向。

1. 整体设计与方案选型:为什么USART至今仍是主力

1.1 USART到底解决了什么问题

通信的本质是传输数据,但数据不会自己从A点跑到B点,必须有一种双方都认可的“约定”。这个约定包括:电平标准、数据位的排列顺序、传输速率、校验方式。USART要解决的,就是在两根线(TX和RX)上,按照约定好的节奏,可靠地完成数据搬运。

它的核心价值在于“异步”。发送方和接收方不需要共享时钟信号,只要两边约定好波特率(也就是每秒传输多少个bit),就能各自按节奏收发数据。这就好比两个人打电话,不需要先对表,只要语速差不多,就能听懂对方说什么。这种设计极大地减少了布线数量,两线制就能实现全双工通信,在PCB面积紧张的板子上尤其友好。

1.2 USART与UART、SPI、I2C怎么选

确认协议选型之前,先搞清楚各个协议之间的分工。我见过很多项目,明明传输距离短、数据量也不大,非要用USART,结果调试起来因波特率不匹配老是出问题;也有明明要跟多个从机通信,图省事用了USART一对一连,结果后期扩展时只能改板子。

协议总线数量通信模式典型场景主要优势
USART异步2线(TX/RX)全双工、异步日志输出、蓝牙/WiFi模块、GPS、工业串口屏接线简单、支持远距离(配合电平转换)
UART2线(TX/RX)全双工、异步电路板内部模块通信实现简单、成熟稳定
SPI4线(SCLK/MOSI/MISO/CS)全双工、同步Flash读写、显示屏、ADC采样速率高、无波特率误差
I2C2线(SCL/SDA)半双工、同步传感器、EEPROM支持多从机、两线制

USART最典型的选型理由,是设备之间没有共同时钟、且距离相对较远(比如几米到几十米,配合RS-232或RS-485电平转换)。如果板子内部短距离高速传数据,SPI更合适;如果要多设备挂总线且速率要求不高,I2C更合适。USART的优势在于通用性,几乎所有MCU都原生支持,市面上绝大多数透传模块、蓝牙模块、4G模块都预留了串口接口,这也是为什么它至今仍是嵌入式通信的“第一协议”。

1.3 同步模式和异步模式怎么取舍

USART的同步模式(SmartCard、SPI-like)会输出时钟,这在某些特定场景下有用,比如智能卡通信、与特定外设对接。但日常项目里,同步模式用得极少,原因有二:一是需要额外布线接时钟,破坏了异步模式“两线走天下”的优势;二是MCU的USART同步模式和硬件SPI相比,灵活性不足,速率也受限。

这里我直接给结论:多数项目,把USART当UART用就行,完全没必要碰同步模式。芯片手册里提到USART支持同步模式,只是为了芯片功能完整性,真正把同步模式用出价值的场景屈指可数。你在配置时,只要关注异步模式参数就足够了。

2. 核心细节解析:从电平标准到帧格式

2.1 电平标准:TTL、RS-232、RS-485不能混用

新手最容易栽的第一个跟头,就是电平不匹配。MCU引出来的UART通常是TTL电平,高电平是3.3V或5V,低电平是0V。而电脑的串口或某些工业设备,用的是RS-232电平,高电平是-12V,低电平是+12V,逻辑刚好相反。如果你直接把MCU的TX接到RS-232设备上,轻则通信失败,重则烧毁IO口。

RS-485则是差分信号,靠A、B两线电压差表示0和1,抗干扰能力强,传输距离能到1200米以上,适合工业现场总线。RS-485是半双工的,收发不能同时进行,需要软件或硬件控制收发方向。选型时,要根据传输距离和现场干扰情况决定是否需要加RS-485收发芯片。

TTL电平直接连接只适合板内或极短距离(10厘米以内)的通信。凡是线长超过这个范围,建议用MAX3232做RS-232电平转换,或用MAX3485做RS-485转换。接口电平选型问题一旦在项目后期发现,改板成本极高,所以前期务必定下来。

2.2 帧格式:起始位、数据位、校验位、停止位

USART异步通信的每一次传输,都以一帧为单位。默认空闲时,TX线保持高电平。发送开始时,先拉低一个bit的时间(起始位),告诉接收方“准备接收”;接着发送数据位,通常是8个bit,低位在前;然后是校验位(可选);最后拉高至少1个bit的时间(停止位),表示这一帧结束。

数据位为什么是低位在前?这是历史遗留的设计,早期电传打字机就是这么干的,后来被USART协议继承。你在写代码时不需要关心这个,硬件会自动处理字节序,但理解这一点有助于你阅读协议文档时看懂时序图。

校验位的存在是为了检测传输过程中的单bit错误。奇校验要求数据位加校验位中“1”的个数为奇数,偶校验则要求为偶数。但校验位只能检错,不能纠错,而且对多位反转无能为力。实际项目里,简单的数据传输用无校验(None)即可,重要数据靠更高层的校验算法来保证完整性。

停止位有1bit、1.5bit、2bit之分。停止位越长,帧间隔越大,接收端越容易同步。但停止位过长会降低有效数据传输速率。绝大多数应用选1bit停止位就够了,除非对端设备明确要求2bit。

2.3 波特率选择逻辑与误差计算

波特率是USART通信的“语速”,单位是bps,即每秒钟传输的bit数。常见的有9600、115200、460800、921600。选波特率要考虑几个因素:对端设备支持范围、传输数据量、线路质量和MCU时钟精度。

9600是工业领域最保守的选择,抗干扰能力强,但速度慢,传输1K字节大约需要1秒。115200是最常用的“万金油”,很多蓝牙模块、GPS模块默认就是这个速率,测试和日志输出都够用。超过460800后,对线路质量和MCU时钟精度要求明显提高,普通杜邦线连接可能就不稳定了。

波特率误差的计算很重要。MCU内部通常从系统时钟分频得到波特率,如果不能整除,就会产生误差。比如系统时钟72MHz,要得到115200波特率,分频系数是72MHz/(16115200)=39.0625,取整到39,实际波特率是72MHz/(1639)=115384.6,误差约0.16%,完全在允许范围内。USART接收端允许的误差通常在±3%左右,发送端建议控制在±2%以内。误差超过这个范围,就会出现“刚开始通信正常,跑一段时间后丢帧”的诡异现象。

3. 实操过程与核心环节实现:从零配置一个USART

3.1 GPIO初始化:别小看AF复用配置

配置USART的第一步不是操作USART外设本身,而是配置GPIO。在全配置模式下,TX引脚通常配置为复用推挽输出,RX引脚配置为复用浮空输入或上拉输入。这里有个细节:RX引脚建议配置为上拉输入,避免线路空闲时电平不确定,误触发起始位。

很多芯片的每个USART引脚并不固定,需要查阅数据手册里的AF映射表。比如某款MCU的USART1_TX可能是PA9也可能是PB6,取决于配置。选引脚时,还要注意避开板上已有的JTAG、晶振引脚,否则会出现莫名其妙的冲突。这个坑我踩过不止一次,改板后才发现某引脚跟板上其它功能打架,只能飞线解决,狼狈至极。

配置代码虽然各家SDK不同,但核心逻辑一致:

// 以某常见MCU为例,使能时钟并配置GPIO复用 __HAL_RCC_USART1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef gpio = {0}; gpio.Pin = GPIO_PIN_9; // TX gpio.Mode = GPIO_MODE_AF_PP; // 复用推挽 gpio.Speed = GPIO_SPEED_FREQ_HIGH; gpio.Alternate = GPIO_AF7_USART1; // 查手册确认AF号 HAL_GPIO_Init(GPIOA, &gpio); gpio.Pin = GPIO_PIN_10; // RX gpio.Mode = GPIO_MODE_AF_INPUT; // 复用输入 gpio.Pull = GPIO_PULLUP; // 上拉,防止误触发 HAL_GPIO_Init(GPIOA, &gpio);

3.2 USART参数配置:先设波特率,还是先设字长

USART外设的初始化顺序有讲究。标准做法是:先把USART挂在时钟树上,解除复位状态;然后配置基础参数,包括波特率、字长、停止位、校验位、硬件流控;最后使能收发模式和中断。

这里要特别提醒一个细节:硬件流控(RTS/CTS)。绝大多数场景用不到,但如果开启而对方不支持,通信会一直卡在等待状态。所以不带硬件流控的模块,务必确认这个选项是关闭的。

字长的选择也容易忽略。USART支持7位、8位、9位数据。常用的是8位。但如果开了奇偶校验,8位模式下有效数据其实是7位,因为最高位被校验位占了。有些协议栈会要求8E1(8数据位+偶校验+1停止位),这时候如果你配置成8位数据加校验,硬件实际发送的是7位数据加1位校验,收发双方就会全部乱套。正确配置应该是:字长选9位,使能偶校验,停止位1位,这才是真正的8E1。

UART_HandleTypeDef huart1 = {0}; 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);

3.3 数据收发:轮询、中断、DMA三种模式怎么选

数据传输有轮询、中断、DMA三种方式,选择标准是CPU的占用率和数据量。

轮询模式最简单,发送就是死等数据发完,接收就是死等数据到来。适合主循环里短小控制指令的收发,比如每隔100ms发一次查询命令。缺点是接收端要一直开着,CPU占用率高,如果主程序有延时操作,可能丢数据。

中断模式是项目里最常用的。发送时,把数据写入数据寄存器,等发送完成中断标志置位再发下一个;接收时,每次收到一个字节就触发接收中断,把数据存进环形缓冲区。这个模式CPU占用率低,响应及时,适合大多数传感器数据采集和模块通信场景。我强烈建议所有做USART接收的人都引入环形缓冲区,这是避免中断里丢数据的关键。

DMA模式适合大数据量收发,比如刷屏、文件传输、日志批量输出。DMA不需要CPU干预,直接把数据从内存搬到USART外设,或者从外设搬到内存。但DMA的几个坑要提前避开:一是缓存一致性,如果CPU修改了发送缓冲区而DMA还没启动,可能出现发送的是旧数据;二是DMA中断和USART空闲中断配合要仔细设计,否则包尾检测会出问题。

3.4 环形缓冲区:接收处理的核心设施

环形缓冲区是一个固定大小的数组,配合读指针和写指针使用。中断服务函数里把收到的字节写入缓冲区并移动写指针,主循环里读取缓冲区并移动读指针。当读指针追上写指针时,缓冲区为空;当写指针追上读指针时,缓冲区溢出,需要丢弃数据或置溢出标志。

缓冲区大小的设计要看数据流量和主循环处理速度的匹配关系。如果平均每秒接收2K字节,主循环每10ms处理一次,那么缓冲区至少需要容纳2K*0.01=20字节,再加上突发余量,一般取128或256字节比较稳妥。缓冲区不是越大越好,过大会浪费RAM,尤其是小容量MCU。判断标准很简单:溢出标志如果经常置位,要么加大缓冲区,要么优化主循环处理速度。

#define RX_BUF_SIZE 256 volatile uint8_t rx_buf[RX_BUF_SIZE]; volatile uint16_t rx_head = 0; volatile uint16_t rx_tail = 0; void USART1_IRQHandler(void) { if (USART1->SR & USART_SR_RXNE) { uint8_t data = USART1->DR; uint16_t next = (rx_head + 1) % RX_BUF_SIZE; if (next != rx_tail) { rx_buf[rx_head] = data; rx_head = next; } else { // 溢出标记,主循环可查询 rx_overflow = 1; } } }

4. 常见问题与排查技巧实录

4.1 乱码:波特率不匹配和电平标准错误

乱码是最常见的问题,可能的原因很多,但九成是波特率不一致。用逻辑分析仪或示波器实测一下波形,计算实际波特率就能确认。还有一种隐蔽的情况:MCU外部晶振频率不对或内部RC振荡器精度不够。某些国产MCU默认使用内部RC,精度在常温下是±1%到±2%,当环境温度变化大时,误差可能逼近极限,造成低温或高温环境下通信不稳定。

电平标准错误也会导致乱码。如果接收端线路上出现负电压,说明可能是RS-232电平没做转换。还有一种乱码是接线顺序错了,TX接TX、RX接RX,收发对不上,数据当然也乱。

排查乱码问题,我建议按以下顺序:先用逻辑分析仪看波形,数一下起始位到停止位的位宽,计算实际波特率;再看空闲电平是高还是低,排除电平反相;最后查两端的数据位、校验位、停止位配置。顺序对了,问题基本能定位。

4.2 丢帧:中断没及时处理,还是缓冲区溢出

接收丢数据的核心原因就两个:中断处理不及时,或缓冲区溢出。中断不及时可能是中断优先级太低,被其它高频中断抢占;也可能是中断处理函数里耗时太长,比如在USART中断里调用HAL_UART_Transmit阻塞发送,导致后续数据来不及收。

缓冲区溢出通常是因为主循环处理速度跟不上,或者缓冲区太小。检查方式是:在缓冲区溢出分支里置一个标志,跑一段业务后查询该标志。如果经常置位,就能确认是处理速度问题。这里我的习惯做法是:中断里只做“移数据”这件事,任何协议解析、数据处理全部放到主循环执行。这样即使某个周期主循环被阻塞,缓冲区也能兜住足够的数据。

4.3 只能发不能收,或只能收不能发

这个现象多半是硬件问题。只发不收,可能是RX引脚没配置复用功能,或者引脚被其它外设占用了;只收不发,可能是TX引脚被锁死成了输入模式,或者外部被拉低。也有一种情况是代码问题:使能了发送但没有使能接收,或者接收中断没打开。

排查硬件问题,先用万用表量引脚电平。空闲时TX应为高电平,如果被拉低,检查外部电路有无短路或下拉电阻。再用示波器看发送数据时TX脚有无波形翻转,没有波形就是配置问题,有波形但对端收不到则是接线或电平转换问题。

4.4 通信一段时间后卡死

这也是USART调试里的“玄学”问题。我之前做某项目调试时,设备每运行几分钟就停止响应,重启后又正常。排查了很久,最后发现是USART接收中断里调用了printf,而printf内部又重定向到USART发送,发送时因为没有及时清标志,卡死在中断里,导致整个系统锁死。

USART中断处理函数里,务必遵守“不发送、不等待、不做复杂计算”的原则。如果你需要在接收到数据后主动发送应答,正确做法是:中断里只置一个标志,主循环检测到标志后再执行发送。另外,还要注意溢出错误标志(ORE)的处理。当数据寄存器数据没被及时读走,又来了新数据时,硬件会置溢出错误标志。这个标志如果不手动清除,可能会一直阻塞后续接收,表现就是通信卡死。

4.5 硬件上的常见坑位

接地问题是最隐蔽的硬件坑。两个设备之间的串口通信,除了TX和RX,还必须有共地线。如果两个设备各用各的电源且没有共地,电平参考点不一致,通信就会出现随机丢字节。解决方法是把两个设备的GND连起来,这比任何软件优化都有效。

热插拔问题也要注意。带电插拔串口线时,瞬间的电位差可能损坏IO口。如果项目需要频繁插拔,建议在串口线上串联一个几百欧姆的电阻,或者在设备端做好ESD保护。这个习惯成本很低,但能避免很多IO口损坏的悲剧。

5. 对我个人来说,使用USART的心得和思考

USART的很多设计思想,在理解透之后,对学习其它通信协议有很强的迁移价值。比如帧格式里的起始位/停止位,本质上是通信双方的时间同步机制;波特率误差分析,直接可以迁移到CAN总线的位时序配置上;环形缓冲区的设计思路,在TCP/IP的接收缓冲里也是一模一样的。

我见过不少开发者用USART只停留在“会调库”的阶段,遇到问题就网上搜例程,搜不到就卡壳。我的建议是,如果你要在嵌入式这条路上走远,一定要花时间把USART的协议细节吃透。不用学得多深,能把时序图看明白、能亲手配置寄存器、能通过逻辑分析仪分析波形,就已经超越了大部分人。

后续如果你还想继续深入,可以去看DMA模式和空闲中断配合实现“不定长数据接收”,这是很多项目里真正在用的方案。再往后就是基于USART的协议封层设计,比如用状态机解析数据帧,或者移植一个轻量级通信协议。这些内容我后续文章里再展开,第一篇把基础打扎实最重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询