资讯动态

USART异步通信实战:从原理到中断与DMA收发

发布时间:2026/10/18 2:12:32 来源:尧图企业网站定制
1. 从点灯到通信为什么USART是绕不开的第一道坎刚接触嵌入式那会儿我总觉得点个LED、读个按键就算入门了。直到某天导师扔过来一个需求让两块板子互相传数据一块采集传感器数值另一块接收后显示出来。当时第一反应是——这有什么难的把变量直接赋值过去不就行了结果当然是被现实狠狠教育了一顿。两块芯片之间没有共享内存想让它们交换信息必须通过物理线路按照约定好的规则来传输电平信号这套规则就是通信协议。在众多通信协议里USARTUniversal Synchronous/Asynchronous Receiver/Transmitter通用同步/异步收发器几乎是每个嵌入式开发者最先接触、也最常打交道的那个。它不像SPI那样需要时钟线严格同步也不像I2C那样有复杂的起始条件和地址帧两根线甚至一根线就能让两个设备聊起来。你手边随便一块开发板上面引出的串口排针背后跑的就是USART。调试信息打印、模块间数据交换、与上位机通信USART的身影无处不在。这篇文章适合谁看如果你刚开始学单片机被各种通信协议搞得头晕想先把最基础的那个吃透或者你已经用过串口但对其中的波特率计算、中断收发、DMA配合这些细节还模棱两可想系统梳理一遍再或者你在实际项目中遇到过数据丢失、乱码、通信不稳定等问题想找到排查思路——那这篇内容应该能帮到你。我会从USART的核心原理讲起把配置参数的计算过程一步步拆开再结合中断和DMA两种收发方式给出可直接参考的代码框架最后把那些年我踩过的坑和排查技巧一并整理出来。2. USART核心原理拆解异步通信到底在“约”什么2.1 同步与异步的本质区别通信协议按是否依赖时钟信号可以分成同步和异步两大类。同步通信像两个人面对面说话除了说话内容本身还有眼神、口型这些额外的同步信号来辅助对齐节奏。SPI和I2C就是典型的同步通信它们有一根专门的时钟线SCL或SCK数据在时钟的上升沿或下降沿被采样收发双方严格跟着时钟节拍走。异步通信则像两个人背对背发短信没有实时的节奏对齐信号全靠事先约定好发送速度。USART的异步模式就是如此它不需要时钟线收发双方各自用自己的时钟源产生波特率只要双方的波特率足够接近就能正确解析数据。这就引出了异步通信最核心的概念——波特率匹配。注意异步通信对波特率误差的容忍度通常在2%到3%之间超过这个范围就可能出现采样错误导致数据错位或乱码。2.2 一帧数据里到底藏了什么USART传输数据是以“帧”为单位的。一帧完整的数据包含以下几个部分按传输顺序排列起始位1个逻辑0电平告诉接收方“我要开始发数据了”。接收方检测到从空闲状态高电平到低电平的跳变就知道一帧开始了。数据位5到9位通常是8位。低位先发这是约定俗成的规则。校验位可选奇校验、偶校验、无校验。校验位的作用是检测单比特错误但只能检错不能纠错。停止位0.5、1、1.5或2个逻辑1电平表示一帧结束同时给接收方一个缓冲时间来准备接收下一帧。假设配置为8位数据位、无校验、1位停止位那么一帧总共占用10个位时间1起始8数据1停止。如果波特率是115200bps每一位的时间就是1/115200秒约8.68微秒一帧就是86.8微秒。这个计算在后面做超时判断和缓冲区设计时非常有用。2.3 波特率是怎么算出来的波特率的本质是每秒传输的码元数。在USART里每个码元携带1比特信息所以波特率和比特率数值相等。波特率的产生依赖于系统时钟和USART外设的分频系数。以常见的STM32为例USART的波特率计算公式是波特率 fCK / (16 * USARTDIV)其中fCK是挂载在USART上的总线时钟频率USARTDIV是一个分频系数包含整数部分和小数部分。比如fCK是72MHz想要115200的波特率那么USARTDIV 72000000 / (16 * 115200) 39.0625整数部分是39小数部分是0.0625。小数部分需要根据寄存器定义转换成对应的值。STM32的USART_BRR寄存器中低4位表示小数部分高12位表示整数部分。0.0625乘以16等于1所以小数部分寄存器值就是1。最终BRR寄存器的值就是(39 4) | 1 0x271。提示实际配置时直接用库函数或配置工具生成BRR值即可但理解这个计算过程有助于你在时钟配置变更时快速判断波特率是否还能准确产生。2.4 采样机制为什么接收方不会错位接收方在没有时钟线的情况下怎么知道每一位的中间位置在哪里答案是过采样。USART接收器通常以波特率的16倍频率对接收线进行采样。检测到起始位的下降沿后接收器会连续采样几次确认这确实是起始位而不是噪声然后在第7、8、9个采样点取中间值作为该位的逻辑电平。这样即使存在轻微的波特率误差或信号抖动也能正确采样到位中间的值。这种机制也解释了为什么波特率误差不能太大。如果发送方和接收方的波特率偏差累积到半个位时间以上采样点就会偏移到相邻位导致数据解析错误。3. 配置参数怎么选从场景反推参数组合3.1 波特率的选择逻辑波特率不是越高越好也不是越低越稳。选择波特率要考虑三个因素通信距离、信号质量和双方时钟精度。近距离板间通信几厘米到几十厘米115200甚至921600都可以稳定工作。我实测过在10厘米杜邦线上跑1M波特率误码率依然很低。中等距离1到3米115200是比较稳妥的选择。再高就需要考虑线材质量和是否有干扰源。长距离超过5米建议降到9600或4800。长线缆的分布电容会严重衰减信号边沿高波特率下接收方可能根本检测不到清晰的跳变。另外如果双方使用的是内部RC振荡器作为时钟源精度通常只有1%到2%这时候波特率不宜超过38400否则误差累积容易导致通信失败。外部晶振的精度通常在20ppm以内可以放心使用高波特率。3.2 数据位、校验位、停止位的取舍这三个参数没有绝对的优劣关键看通信双方的约定和场景需求。参数常见选择适用场景注意事项数据位8位绝大多数场景传输ASCII字符或字节数据数据位9位多机通信、地址帧第9位常作地址/数据标志校验位无校验短距离、干扰小协议层自己做CRC校验校验位偶校验工业环境、干扰较大只能检单比特错误停止位1位常规场景帧间隔最小效率最高停止位2位低速、长距离给接收方更多缓冲时间我个人的习惯是调试串口用8N18数据位、无校验、1停止位工业现场如果线缆较长且没有协议层校验会加上偶校验和2位停止位。但要注意校验位和停止位增加会降低有效数据吞吐率。比如8E2配置下一帧变成181212位相比8N1的10位同样波特率下有效数据率下降了约17%。3.3 硬件流控要不要开USART支持硬件流控通过RTS请求发送和CTS清除发送两根额外信号线来协调收发节奏。当接收方缓冲区快满时拉高RTS告诉对方暂停发送发送方检测到CTS无效时停止发送。什么时候需要流控当发送方速度远快于接收方处理速度且数据量较大时。比如用串口传文件接收方需要写Flash写操作耗时较长如果没有流控发送方持续发送会导致接收方缓冲区溢出丢数据。但大多数嵌入式场景下数据量不大接收方处理及时不需要流控。两根额外的线也会增加布线复杂度。我的建议是如果协议层有应答重传机制可以不开流控如果传的是连续数据流且接收方处理有不确定性建议开启。4. 中断收发与DMA收发两种实战方案详解4.1 中断收发模式的实现要点中断方式是USART最常用的收发模式。发送完成触发TXE中断接收非空触发RXNE中断。在中断服务函数里读取或写入数据寄存器。先看发送部分。假设要发送一个缓冲区里的数据volatile uint8_t tx_buffer[128]; volatile uint16_t tx_index 0; volatile uint16_t tx_length 0; void usart_send_data(uint8_t *data, uint16_t len) { while (tx_index tx_length); // 等待上一次发送完成 memcpy((void*)tx_buffer, data, len); tx_length len; tx_index 0; USART1-DR tx_buffer[tx_index]; // 写入第一个字节触发TXE中断 } void USART1_IRQHandler(void) { if (USART1-SR USART_SR_TXE) { if (tx_index tx_length) { USART1-DR tx_buffer[tx_index]; } else { USART1-CR1 ~USART_CR1_TXEIE; // 发送完毕关闭TXE中断 } } // 接收处理... }这里有个细节TXE标志置位表示数据寄存器空可以写入下一个字节但此时最后一个字节可能还在移位寄存器里没有完全发出去。如果需要确保数据完全发送完毕要等待TC传输完成标志。接收部分的中断处理volatile uint8_t rx_buffer[256]; 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; // 读取数据同时清除RXNE标志 uint16_t next (rx_head 1) % sizeof(rx_buffer); if (next ! rx_tail) { // 缓冲区未满 rx_buffer[rx_head] data; rx_head next; } // 如果缓冲区满了丢弃数据或置错误标志 } }这是一个典型的环形缓冲区实现。rx_head由中断服务函数更新rx_tail由主循环更新。判断缓冲区满的条件是(next rx_tail)判断缓冲区空的条件是(rx_head rx_tail)。注意在中断服务函数里读取DR寄存器时一定要先读SR再读DR或者直接读DR。某些芯片系列要求这个顺序来正确清除标志位否则可能反复进入中断。4.2 DMA收发模式的配置与优势当中断方式下数据量较大或波特率较高时频繁进出中断会消耗大量CPU时间。比如115200波特率下每86.8微秒就产生一次中断如果主循环还有其他任务中断开销就很可观了。这时候DMA直接存储器访问就派上用场了。DMA方式下USART的数据寄存器与内存之间直接搬运数据不需要CPU介入。配置步骤大致如下使能DMA时钟和USART时钟。配置DMA通道的源地址、目的地址、传输方向、数据宽度、传输数量。使能USART的DMA发送和接收请求。使能DMA通道。使能USART的收发功能。以发送为例配置DMA将内存缓冲区数据搬到USART的DR寄存器void usart_dma_send(uint8_t *data, uint16_t len) { DMA1_Channel4-CCR ~DMA_CCR_EN; // 关闭DMA通道 DMA1_Channel4-CMAR (uint32_t)data; // 内存地址 DMA1_Channel4-CNDTR len; // 传输数量 DMA1_Channel4-CCR | DMA_CCR_EN; // 使能DMA USART1-CR3 | USART_CR3_DMAT; // 使能USART DMA发送 }DMA传输完成会触发传输完成中断在中断里可以做一些收尾工作比如置标志位通知主循环、关闭DMA等。接收方面DMA可以配合空闲线检测来实现不定长数据接收。思路是DMA一直处于接收状态当总线上出现空闲超过一帧时间没有新数据时触发空闲中断在空闲中断里计算接收到的数据长度并处理。void USART1_IRQHandler(void) { if (USART1-SR USART_SR_IDLE) { USART1-SR; // 读SR USART1-DR; // 读DR清除空闲标志 uint16_t received_len BUFFER_SIZE - DMA1_Channel5-CNDTR; // 处理接收到的数据 process_data(rx_dma_buffer, received_len); // 重新配置DMA准备下一次接收 DMA1_Channel5-CCR ~DMA_CCR_EN; DMA1_Channel5-CNDTR BUFFER_SIZE; DMA1_Channel5-CCR | DMA_CCR_EN; } }这套组合拳在实际项目中非常实用接收不定长数据帧时不需要事先知道长度也不需要每个字节都进中断。4.3 两种方式的对比与选型建议对比项中断方式DMA方式CPU占用每字节一次中断几乎为零实现复杂度低中等适用波特率9600到115200任意尤其高速适用数据量小批量、间歇性大批量、连续缓冲区管理环形缓冲区DMA计数器空闲中断调试难度容易稍复杂我的经验是调试打印、命令交互这类低频小数据量场景中断方式足够且代码简单。如果是高速数据采集、图像传输、与上位机大批量通信果断上DMA省下来的CPU时间可以做更多事情。5. 常见问题与排查技巧实录5.1 数据乱码从波特率查起乱码是最常见的USART问题。现象是接收到的数据看起来像随机字节或者偶尔正确偶尔错误。排查顺序如下确认双方波特率一致。这是最常见的原因。发送方用115200接收方用9600必然乱码。检查时钟源配置。如果系统时钟配置改了但USART的时钟源没有相应调整实际波特率就会偏离。比如从外部晶振切换到内部RC时钟频率变了BRR寄存器的值需要重新计算。测量实际波特率。用示波器或逻辑分析仪抓一帧数据测量起始位到停止位的时间反推实际波特率。如果偏差超过3%就需要调整BRR值。检查信号质量。如果波形边沿很缓、过冲严重可能是线缆太长或驱动能力不足。降低波特率或增加驱动电路。5.2 数据丢失缓冲区溢出与中断优先级数据丢失的表现是接收到的数据不完整中间少了几个字节。原因通常有两个接收缓冲区太小或处理不及时。中断里把数据存入环形缓冲区但主循环处理速度跟不上缓冲区满了之后新数据被丢弃。解决办法是增大缓冲区或者用DMA接收或者提高主循环处理优先级。中断被更高优先级中断打断太久。如果USART接收中断优先级较低被其他高优先级中断阻塞可能导致RXNE标志未及时清除下一个字节覆盖上一个字节。解决办法是适当提高USART中断优先级或者缩短其他中断服务函数的执行时间。提示在调试阶段可以在环形缓冲区满时置一个溢出标志主循环检测到这个标志就知道发生了数据丢失便于定位问题。5.3 通信不稳定地线与干扰两块板子单独供电时如果地线没有连接通信会非常不稳定甚至完全无法通信。USART信号是相对于地线的电压没有共同的地参考接收方无法正确判断高低电平。另外如果通信线缆经过电机、继电器等干扰源附近可能引入噪声。解决办法包括使用双绞线、缩短线缆长度、增加磁珠或滤波电容、降低波特率。5.4 常见问题速查表现象可能原因排查方法解决措施完全无数据接线错误、未使能USART检查TX/RX是否交叉连接交换TX/RX使能外设乱码波特率不匹配示波器测量位时间统一波特率配置偶发错误时钟精度不足检查时钟源换外部晶振或降低波特率数据丢失缓冲区溢出检查溢出标志增大缓冲区或用DMA只能收不能发TX引脚配置错误检查GPIO模式配置为复用推挽输出只能发不能收RX引脚未使能检查GPIO和USART配置配置为浮空或上拉输入5.5 几个容易被忽略的细节GPIO配置USART的TX引脚要配置为复用推挽输出RX引脚配置为浮空输入或上拉输入。如果TX配成了普通推挽输出USART无法控制引脚电平如果RX配成了输出模式可能损坏引脚。中断标志清除顺序不同芯片系列对标志清除的顺序要求不同。有些要求先读SR再读DR有些直接读DR即可。一定要看参考手册不要想当然。DMA传输完成与USART传输完成的区别DMA传输完成中断触发时最后一个字节可能还在USART的移位寄存器里没有发出去。如果此时立刻关闭USART或进入低功耗模式最后一个字节会丢失。正确做法是等待USART的TC标志置位后再操作。空闲中断的清除空闲中断标志的清除通常需要先读SR再读DR。如果只读DR不读SR标志可能清除不掉导致反复进入中断。6. 从USART延伸出去还能怎么玩USART的玩法远不止简单的收发数据。把它的能力吃透之后可以延伸出很多实用方案。多机通信利用9位数据位模式第9位作为地址标志位。主机发送地址帧时第9位置1所有从机接收后比较地址匹配的从机准备接收后续数据帧。这样一条总线上可以挂多个从机主机通过地址来选择通信对象。与上位机协议设计在USART之上定义一套简单的应用层协议比如帧头长度命令数据校验和。这样即使底层偶尔出现误码应用层也能通过校验和发现并请求重传。我常用的格式是0xAA 0x55开头然后是长度字节、命令字节、数据区、CRC16校验。半双工单线模式某些场景下只需要一根信号线USART支持单线半双工模式TX和RX合并到一根引脚上。通过配置寄存器切换收发方向。这种模式适合布线受限的场合但要注意收发切换的时机避免总线冲突。红外通信USART配合红外收发器可以实现红外遥控信号的收发。配置时需要注意红外载波频率与USART波特率的匹配关系。与传感器模块对接很多传感器模块如GPS、蓝牙、WiFi模组都提供USART接口。对接时除了电气连接还要注意模块的默认波特率、数据格式、命令集。有些模块上电后会输出大量启动信息需要先清空缓冲区再发送配置命令。我在实际项目中遇到过模块启动信息干扰配置命令的情况后来养成了一个习惯模块上电后先延时一段时间然后清空接收缓冲区再发送配置命令。这个延时时间根据模块手册确定通常是几百毫秒到几秒不等。最后分享一个调试小技巧如果手边没有逻辑分析仪可以用另一块开发板做一个简单的串口监听器。把监听器的RX接到目标TX上监听器只接收不发送把收到的数据通过另一个串口转发到电脑上显示。这样就能在不干扰目标通信的情况下观察数据流。这个土办法在紧急排查时救过我好几次。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑