STM32 USART串口通信:从基础配置到DMA与协议解析实战

发布时间:2026/8/6 7:52:51
STM32 USART串口通信:从基础配置到DMA与协议解析实战
1. 从“Hello World”到工业通信为什么USART是STM32的必修课如果你刚开始接触STM32点亮一个LED灯或者让一个蜂鸣器响起来那种成就感是巨大的。这就像是编程世界的“Hello World”宣告着你已经成功踏入了单片机的大门。但很快你就会发现仅仅控制几个GPIO通用输入输出引脚是远远不够的。当你想让单片机“开口说话”与电脑、传感器、另一个单片机甚至是一个无线模块交换数据时你遇到的第一个、也是最核心的通信外设往往就是USART。USART全称通用同步异步收发器我们更常叫它“串口”。这个名字听起来可能有点老派毕竟在USB、以太网、Wi-Fi大行其道的今天串口似乎显得有些“复古”。但恰恰是这种“复古”让它成为了嵌入式开发中最基础、最可靠、也最不可或缺的调试与通信手段。几乎每一款STM32芯片从最入门的F0系列到高性能的H7系列都标配了至少一个USART模块。你可以没有复杂的网络协议栈但不能不会用串口。它就像是你和芯片世界沟通的“母语”是验证程序逻辑、输出调试信息、配置外围模块的“生命线”。我见过很多初学者在调通了GPIO和定时器后卡在了串口通信上。电脑端的串口助手一片空白或者收到一堆乱码那种挫败感非常强烈。其实串口的配置逻辑非常清晰一旦你理解了它的几个核心“开关”和“旋钮”就能轻松驾驭。这篇笔记我就结合自己从初学到项目实战中积累的经验把STM32的USART外设掰开揉碎了讲清楚。我们不止要“点亮”串口更要理解数据是如何一位一位地、稳定可靠地穿越芯片引脚抵达另一个世界的。这背后涉及的波特率、数据位、停止位、校验位等概念以及中断和DMA这两种高效的收发方式将是本文重点探讨的内容。2. USART与UART一字之差背后的同步时钟奥秘在开始配置之前我们必须先厘清一个基本概念USART和UART有什么区别在很多场合包括STM32的库函数和资料里这两个词经常混用但它们确实有技术上的区别。UART是通用异步收发器。关键词是“异步”。这意味着通信的双方比如你的STM32和电脑并没有共享一个统一的时钟信号。它们各自依靠自己内部的高精度时钟源并约定好一个通信速率波特率来进行数据收发。因为时钟独立为了确保接收方能准确地在正确的时间点采样数据线上的电平异步通信需要在每个数据帧的前后加上“起始位”和“停止位”来框定数据的边界。我们常用的“串口通信”绝大多数情况下指的就是这种UART异步模式。USART在UART前面加了一个“S”代表“同步”。这意味着它除了支持纯粹的UART异步模式还额外支持同步模式。在同步模式下通信双方需要共享一根时钟线通常称为SCLK或CK。发送方在输出数据位的同时会同步输出一个时钟脉冲接收方则根据这个时钟脉冲来采样数据。这种方式不需要起始位和停止位数据传输效率更高常用于需要高速、可靠数据流的场景比如连接某些特定的存储器、显示控制器或早期的智能卡。对于STM32开发者而言99%的应用场景下我们使用的都是USART的异步模式。所以你可以简单理解为STM32的USART模块是一个功能更强的UART我们通常只用它的异步功能。在配置时我们只需关注异步模式相关的参数即可。这个“S”的存在更多是芯片设计上的功能完备性体现但在初学阶段知道这个区别避免概念混淆就足够了。那么一次典型的UART异步通信数据帧到底长什么样我们可以把它想象成一列运送货物的火车。起始位火车头。它是一个持续一位时间的低电平逻辑0告诉接收方“注意后面有数据要来了准备好采样”数据位车厢。这就是你要传输的实际数据通常是8位一个字节也可以是7位或9位。这些位紧跟在起始位之后。校验位货物清单校验可选。这是一个简单的错误检测位可以是奇校验或偶校验。发送方会根据数据位中“1”的个数计算并附加这一位使得整个数据位校验位中“1”的个数为奇数奇校验或偶数偶校验。接收方会进行同样的计算如果不匹配则说明传输过程中可能发生了单比特错误。停止位火车尾。它是一个持续1位、1.5位或2位时间的高电平逻辑1标志着本帧数据的结束并让数据线恢复到空闲的高电平状态为下一帧的起始位做准备。理解这个帧结构至关重要因为无论是STM32还是电脑端的串口助手双方的帧格式配置数据位、停止位、校验位必须完全一致否则必然出现乱码。3. 核心配置六要素让串口开口说话的“密码”要让STM32的USART开始工作你需要配置好六个核心参数这就像设置一段对话的规则。任何一项不匹配通信就会失败。3.1 波特率对话的语速波特率决定了每秒传输的符号数。对于UART一个符号就是一个比特所以波特率直接等于比特率。常见的波特率有9600 115200 460800等。115200是调试时最常用的速率它在传输速度和稳定性之间取得了很好的平衡。波特率越高传输越快但对时钟精度和线路抗干扰能力的要求也越高。在STM32中波特率是通过一个名为USART_BRR的寄存器来设置的。这个寄存器的值由芯片的系统时钟如APB总线时钟和期望的波特率计算得出。公式是BRR f_PCLK / (波特率 * 16)这里的f_PCLK是USART模块所在的APB总线时钟频率。例如如果APB2时钟为72MHz要设置115200的波特率计算过程为BRR 72,000,000 / (115200 * 16) 39.0625USART_BRR寄存器分为整数部分高16位和小数部分低4位。所以39.0625对应的设置就是整数部分39小数部分1因为0.0625 * 16 1。在标准库中我们可以直接调用USART_InitStruct.USART_BaudRate 115200;库函数会帮我们完成这个计算和填充。但在HAL库中如果遇到波特率不准的问题可能需要手动检查时钟树配置确保f_PCLK计算正确。注意波特率误差是导致乱码的常见原因之一。通常要求发送和接收双方的波特率误差小于2.5%。STM32的时钟源如HSI本身有一定误差如果对精度要求高务必使用外部晶振HSE。3.2 字长每个“单词”的长度字长即数据位通常设置为8位因为一个字节是8比特这是计算机数据的基本单位。STM32也支持9位字长这在多机通信使用地址帧或者需要传输带奇偶校验的8位数据时有用。对于绝大多数应用8位数据位是标准选择。3.3 停止位一句话说完后的停顿停止位给接收方一点处理时间并确保线路恢复到空闲状态。通常选择1位停止位。只有在某些非常老的设备或特定协议中才可能需要1.5位或2位停止位。盲目增加停止位会降低有效数据的传输效率。3.4 奇偶校验简单的“复述”核对奇偶校验是一种最简单的检错方式。如果启用会在数据位后添加一个校验位。偶校验确保数据位校验位中“1”的个数为偶数。奇校验确保“1”的个数为奇数。无校验不添加校验位。对于调试输出通常选择无校验。在干扰较强的工业环境中可以考虑启用校验来捕获单比特错误。但请注意奇偶校验只能检测奇数个比特错误如果两个比特同时出错它就无能为力了。3.5 硬件流控制避免“嘴快耳慢”想象一下如果接收方缓冲区满了来不及处理而发送方还在不停发送数据就会丢失。硬件流控制RTS/CTS就是为了解决这个问题。RTS请求发送。由接收方控制告诉发送方“我准备好了你可以发。”CTS清除发送。由发送方监测如果为低电平表示接收方允许发送如果为高电平则暂停发送。在STM32与电脑通过USB转串口线连接进行调试时通常不需要也不连接硬件流控制我们直接将其禁用。只有在与某些高速Modem或其他严格要求流量控制的设备通信时才需要启用并连接对应的RTS和CTS引脚。3.6 收发模式单工、半双工与全双工USART默认工作在全双工模式即发送TX和接收RX可以同时独立进行。STM32也支持半双工模式单线模式此时发送和接收共用一根数据线不能同时进行可以节省一个IO引脚。在初始化时我们需要使能发送器和接收器。配置好这六个要素USART的静态参数就设好了。接下来我们要决定如何与它“互动”即如何把要发送的数据交给它又如何从它那里读取接收到的数据。这里就有三种不同层次的玩法轮询、中断和DMA。4. 三种驱动模式详解从“傻等”到“全自动”4.1 轮询模式最直接也最“阻塞”轮询模式是最简单粗暴的方式。它的逻辑是发送把数据写入发送数据寄存器USART_DR然后原地循环不断查询状态寄存器USART_SR中的“发送完成”标志位TC直到标志位被硬件置位表示数据已从移位寄存器完全发出。接收不断查询“接收寄存器非空”标志位RXNE一旦置位就立刻从USART_DR中读取数据。// 轮询发送一个字符的伪代码 void USART_SendChar(USART_TypeDef* USARTx, uint8_t ch) { while((USARTx-SR USART_SR_TXE) 0); // 等待发送数据寄存器空 USARTx-DR ch; // 写入数据 while((USARTx-SR USART_SR_TC) 0); // 等待发送完成 }优点代码简单直观易于理解。致命缺点while循环等待严重浪费CPU资源在等待期间CPU什么也干不了整个系统会被“阻塞”。这在任何实际项目中都是不可接受的因为单片机往往需要同时处理多个任务扫描按键、刷新显示、采集传感器等。实战心得轮询模式仅适用于最简单的功能验证或者在不关心CPU效率的极早期学习阶段。一旦程序复杂起来必须尽快抛弃这种方式。4.2 中断模式让CPU“分身有术”中断模式是实际项目中最常用的方式。核心思想是CPU只负责发起和收尾具体的搬运工作由USART硬件在后台完成完成时通过中断通知CPU。发送使能发送数据寄存器空中断TXE。当USART_DR为空硬件可以接收下一个数据时会触发中断。在中断服务函数中我们写入下一个要发送的数据。如果所有数据发完则关闭TXE中断并开启发送完成中断TC来做最后的清理如关闭串口或切换状态。接收使能接收寄存器非空中断RXNE。当收到一个完整数据并转移到USART_DR后硬件触发中断。在中断服务函数中我们读取USART_DR将数据存入自定义的缓冲区如环形队列。// 中断服务函数伪代码示例 void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { // 收到数据 uint8_t rx_data USART_ReceiveData(USART1); // 将rx_data放入环形缓冲区 ring_buffer_write(rx_buf, rx_data); USART_ClearITPendingBit(USART1, USART_IT_RXNE); } if(USART_GetITStatus(USART1, USART_IT_TXE) ! RESET) { // 发送寄存器空可以写下一个数据 if(有数据待发送) { uint8_t tx_data 从发送缓冲区取出数据(); USART_SendData(USART1, tx_data); } else { // 没有数据了关闭TXE中断避免持续进入空中断 USART_ITConfig(USART1, USART_IT_TXE, DISABLE); // 可选开启TC中断等待最后一帧数据真正发送完毕 USART_ITConfig(USART1, USART_IT_TC, ENABLE); } } if(USART_GetITStatus(USART1, USART_IT_TC) ! RESET) { // 最后一帧数据发送完毕可以做后续处理如关闭串口 USART_ITConfig(USART1, USART_IT_TC, DISABLE); // ... 设置完成标志等 } }优点CPU利用率高。在数据收发期间CPU可以处理其他任务只在数据就绪时被短暂打断。关键点缓冲区管理中断函数必须短小精悍。通常我们会配合一个环形缓冲区FIFO来缓存收发数据。中断里只做最简单的“存/取”操作主循环或其他任务从缓冲区里处理数据。中断优先级如果系统中有多个中断源需要合理设置USART中断的优先级。通常串口接收中断的优先级会设得较高以防数据丢失。清除中断标志必须在中断服务函数中清除对应的中断标志位否则会连续触发中断。4.3 DMA模式解放CPU的终极武器当需要传输大量、连续的数据时如通过串口发送一张图片的数据或持续接收GPS模块的NMEA语句即使使用中断频繁进入中断上下文切换也会消耗不少CPU资源。此时DMA直接存储器访问就是最佳选择。DMA就像一个“智能搬运工”它可以在外设如USART的DR寄存器和内存如一个数组之间直接搬运数据完全不需要CPU参与。你只需要告诉DMA源地址、目标地址、数据长度然后启动它。搬运完成后DMA会通过中断通知CPU。发送流程将待发送的数据放在一个数组缓冲区里。配置DMA通道源地址为内存数组地址目标地址为USART-DR寄存器地址传输方向为内存到外设。使能USART的DMA发送请求USART_DMACmd(USARTx, USART_DMAReq_Tx, ENABLE)。启动DMA传输。DMA自动将数组中的数据逐个搬运到USART_DRUSART硬件则自动将这些数据发送出去。传输完成后触发DMA传输完成中断在中断中做后续处理如释放缓冲区、通知任务等。接收流程准备一个足够大的数组作为接收缓冲区。配置DMA通道源地址为USART-DR寄存器地址目标地址为内存数组地址传输方向为外设到内存。使能USART的DMA接收请求USART_DMACmd(USARTx, USART_DMAReq_Rx, ENABLE)。启动DMA传输。USART每收到一个数据DMA就自动将其搬运到内存数组中。当收到指定长度的数据后触发DMA传输完成中断。或者可以配置DMA为循环模式让缓冲区首尾相连实现一个“永不溢出”的环形接收缓冲区这在处理不定长数据流时非常有用。优点CPU占用率极低适合高速、大数据量传输。难点配置复杂需要同时配置USART和DMA两个外设并处理好它们之间的关联。数据边界对于不定长数据如一帧以回车换行结尾的数据纯DMA模式处理起来稍麻烦。通常的解决方案是DMA循环接收空闲中断IDLE。使能USART的空闲中断当串口总线在一帧数据后出现空闲时触发中断在中断中根据DMA的当前指针位置计算出自上一帧结束到当前空闲时刻接收到的数据长度从而提取出一帧完整数据。这是处理串口通信协议的黄金组合。5. 实战避坑指南从乱码、丢数据到稳定通信理论懂了代码写了但串口调不通是常态。下面分享几个最常见的坑和解决方案。5.1 乱码问题排查四步法乱码是“首恶”其根源一定是发送方和接收方的参数或时序不匹配。检查波特率这是最常见的原因。确保STM32程序和电脑串口助手的波特率设置完全一致。检查STM32的系统时钟配置特别是给USART提供时钟的APB总线时钟HCLK或PCLK1/PCLK2是否正确。如果使用内部RC振荡器HSI其精度较差在高速波特率如115200以上下误差可能超标建议换用外部晶振HSE。检查帧格式数据位8位、停止位1位、校验位无必须三方一致代码配置、电脑串口助手、对端设备。检查硬件连接这是最容易被忽略的STM32的TX引脚应该连接USB转串口模块或对端设备的RX引脚RX连接TX。接反了肯定没数据。同时确保共地。检查电压电平STM32是3.3V电平。如果你的USB转串口模块是5V电平如某些PL2303模块直接连接可能无法可靠识别高电平甚至损坏STM32的IO口。务必使用3.3V电平的转换模块如CH340G、CP2102等或者在中间增加电平转换电路。5.2 中断模式下的数据丢失与缓冲区溢出在中断模式下如果数据来得太快而你的中断服务函数处理太慢或者主程序从缓冲区读取数据不够及时就会导致缓冲区溢出新数据覆盖旧数据。解决方案使用环形缓冲区这是必须的。确保缓冲区大小足够应付数据流的突发情况。例如如果每秒接收115200比特约合11520字节你的缓冲区至少应该有256字节或更大。优化中断服务函数中断里只做最必要的操作——读取USART_DR写入环形缓冲区清除标志。绝对避免在中断里进行复杂计算、调用可能阻塞的函数如printf或操作其他慢速外设。流控制如果条件允许启用硬件流控制RTS/CTS让接收方控制发送方的节奏。5.3 DMA接收不定长数据的“IDLE中断”技巧这是串口高级应用的精华。假设你要接收一串以\r\n结尾的GPS数据长度不固定。单纯用DMA你不知道何时算一帧结束。标准操作流程开启DMA循环接收模式指向一个大的环形缓冲区。开启USART的空闲中断IDLE Interrupt。当串口总线在收到一字节数据后超过一个完整字符传输时间具体时间取决于波特率没有新数据硬件就会产生空闲中断。在空闲中断服务函数中 a. 清除空闲中断标志。 b. 读取DMA通道当前剩余数据计数DMA_CNDTRx寄存器。 c. 用“缓冲区总大小 - 剩余计数”计算出从上次处理到本次空闲期间接收到的数据长度。 d. 根据这个长度从缓冲区中取出数据进行解析例如判断是否包含\r\n。 e. 更新缓冲区读指针注意处理环形缓冲区的回绕。这样无论一帧数据有多长只要帧与帧之间有明显的空闲间隙你就能完美地将其分割出来。注意IDLE中断在帧与帧之间无空闲时不会触发。对于连续数据流需要结合超时机制或特定的帧头帧尾来分包。5.4 多串口系统中的资源管理与调试一个项目可能用到多个USARTUART1用于调试打印UART2连接GPSUART3连接4G模块。这时要注意中断优先级为每个USART的RX中断分配合理的优先级。通常用于接收关键控制指令的串口优先级应高于仅用于打印调试信息的串口。调试串口专门用一个USART做调试输出是很好的实践。可以使用重定向printf到该串口方便打印变量和状态。但注意printf函数本身比较耗时在中断或时间敏感的代码段中慎用。引脚复用STM32的很多引脚功能是复用的。在初始化USART前务必通过GPIO的AFIO复用功能寄存器将对应的TX/RX引脚配置为复用推挽输出TX和浮空输入/上拉输入RX。6. 进阶应用从字节流到协议帧裸的串口通信传输的是原始的字节流。在实际项目中我们需要在这些字节流中定义有意义的“协议帧”才能实现复杂的控制或数据交换。6.1 设计一个简单的自定义协议一个健壮的协议帧通常包含以下部分帧头1-2个特殊的字节如0xAA0x55用于标识一帧的开始。接收方只有在检测到帧头后才开始正式接收和解析一帧数据。长度域指示本帧数据载荷的长度。可以是1字节或2字节。有了它接收方就知道该收多少字节才算一帧完整。命令/类型域指示这帧数据是干什么的如读取传感器、设置参数等。数据载荷实际要传输的数据。校验和对帧头之后、校验和之前的所有字节进行某种运算如累加和、CRC16用于验证数据在传输过程中是否出错。接收方重新计算校验和并与帧中的校验和对比不一致则丢弃该帧。帧尾可选的结束标志如\r\n。例如一个简单的协议帧可以设计为[帧头0xAA] [长度L] [命令CMD] [数据...] [校验和CK]。6.2 在STM32中实现协议解析解析协议的本质是一个状态机。我们可以在串口接收中断或DMAIDLE中断获取的数据基础上运行一个解析状态机。typedef enum { STATE_WAIT_HEADER1, STATE_WAIT_HEADER2, STATE_WAIT_LENGTH, STATE_WAIT_CMD, STATE_RECEIVING_DATA, STATE_WAIT_CHECKSUM } ParserState; ParserState state STATE_WAIT_HEADER1; uint8_t rx_buffer[256]; uint8_t data_index 0; uint8_t expected_length 0; uint8_t calculated_checksum 0; void parse_byte(uint8_t byte) { switch(state) { case STATE_WAIT_HEADER1: if(byte 0xAA) { state STATE_WAIT_HEADER2; calculated_checksum byte; // 开始计算校验和 } break; case STATE_WAIT_HEADER2: if(byte 0x55) { state STATE_WAIT_LENGTH; calculated_checksum byte; } else { state STATE_WAIT_HEADER1; // 同步失败重置 } break; case STATE_WAIT_LENGTH: expected_length byte; calculated_checksum byte; state STATE_WAIT_CMD; data_index 0; // 准备接收数据 break; case STATE_WAIT_CMD: current_cmd byte; calculated_checksum byte; if(expected_length 0) { state STATE_RECEIVING_DATA; } else { state STATE_WAIT_CHECKSUM; // 没有数据载荷 } break; case STATE_RECEIVING_DATA: rx_buffer[data_index] byte; calculated_checksum byte; if(data_index expected_length) { state STATE_WAIT_CHECKSUM; } break; case STATE_WAIT_CHECKSUM: if(calculated_checksum byte) { // 校验通过一帧有效数据就绪 // 调用处理函数 process_frame(current_cmd, rx_buffer, expected_length); } // 无论校验是否通过都回到初始状态准备接收下一帧 state STATE_WAIT_HEADER1; break; } } // 在串口接收中断或数据回调函数中对每个收到的字节调用 parse_byte(rx_byte);这种状态机解析器非常高效能够从容地处理粘包两帧数据连在一起、断帧一帧数据分多次收到等情况是工业级串口应用的基石。7. 调试技巧与工具链整合工欲善其事必先利其器。高效的调试能极大提升开发效率。7.1 利用printf重定向进行调试这是最常用的调试手段。通过重写C库的_write或fputc函数将printf的输出重定向到串口。// 针对GCC/ARM Compiler 6 #include stdio.h #ifdef __GNUC__ #define PUTCHAR_PROTOTYPE int __io_putchar(int ch) #else #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f) #endif PUTCHAR_PROTOTYPE { while((USART1-SR USART_SR_TXE) 0); // 等待发送就绪 USART1-DR (ch 0xFF); return ch; } // 然后在代码中就可以直接使用 printf(Value: %d\r\n, sensor_value);注意printf是阻塞式的且会显著增加代码体积。在正式产品中可以考虑使用更轻量级的日志函数或者通过宏定义在发布版本中关闭printf。7.2 逻辑分析仪与串口示波器当软件调试无法解决问题时比如怀疑是硬件时序问题硬件工具就派上用场了。逻辑分析仪可以同时捕捉TX、RX引脚上的数字波形。你可以清晰地看到起始位、数据位、停止位的电平变化精确测量波特率检查波形是否干净有无毛刺。这是排查硬件连接和信号完整性问题的最有力工具。串口示波器一些高级的串口助手如Vofa、SerialPlot可以将接收到的数据实时绘制成波形图。这对于观察传感器数据的动态变化如加速度、温度曲线非常直观。7.3 与RTOS如FreeRTOS结合在实时操作系统中串口驱动通常作为一个独立的线程或任务存在。接收任务创建一个高优先级的任务阻塞在一个信号量或队列上。串口接收中断或DMAIDLE中断每收到一帧完整数据就释放该信号量或发送消息到队列唤醒接收任务进行处理。这样实现了中断与任务间的解耦。发送任务可以创建一个发送任务或者提供一个线程安全的发送API。当其他任务需要发送数据时将数据拷贝到一个缓冲区然后通过队列或信号量通知发送任务由发送任务实际驱动USART通过中断或DMA发送出去。避免多个任务同时操作发送硬件资源。这种设计模式使得串口通信模块清晰、可维护并且能很好地融入多任务系统。串口是简单的简单到几个参数就能启动串口也是深邃的从轮询到DMA从字节流到协议栈其背后是嵌入式系统对效率、可靠性和实时性的不懈追求。理解并掌握USART不仅仅是学会使用一个外设更是掌握了嵌入式系统与外界对话的基本方法论。当你能够稳定、高效地通过串口收发数据时STM32的世界才真正向你敞开大门你可以轻松地连接各种传感器、模块、上位机去构建更复杂、更强大的应用。