STM32串口通信深度解析:从原理到实战,掌握USART高效稳定开发
1. 项目概述为什么STM32的串口值得深挖如果你玩过STM32或者任何一款单片机串口USART几乎是你第一个打交道、也最离不开的外设。它就像单片机的“嘴巴”和“耳朵”负责和电脑、传感器、蓝牙模块、甚至另一块单片机“说话”。我刚开始学的时候觉得串口不就是配置一下波特率、收收发发数据嘛能有多复杂后来在实际项目里踩了无数坑数据收不全、莫名其妙丢包、一上中断系统就卡死……才明白这看似简单的串口里面门道深得很。STM32的串口官方叫USART通用同步异步收发器它比基础的UART多了同步时钟支持功能更强大。但不管是USART还是UART核心思想都是把数据一位一位地按顺序发送出去。为什么它如此重要因为它是嵌入式开发中最基础、最可靠的调试和通信手段。没有串口打印日志调试就像蒙着眼睛走路没有串口与上位机通信很多智能设备就失去了“大脑”。从简单的按键状态回传到复杂的Modbus协议通信底层都离不开它。这篇文章我会结合我这些年做项目的实际经验把STM32的USART从里到外拆解一遍。我们不只讲标准库或HAL库怎么配置更要讲清楚寄存器背后每个比特位的意义讲明白查询、中断、DMA这三种方式到底该怎么选以及最让人头疼的“不定长数据接收”到底有哪些靠谱的解决方案。目标是让你看完后不仅能配置出能用的串口更能设计出稳定、高效的串口通信方案彻底告别玄学般的通信故障。2. USART核心原理与STM32实现机制2.1 串行通信的基础从比特流到字节在深入STM32之前我们必须统一语言理解几个核心概念。串口通信是异步的意味着发送和接收双方没有统一的时钟线来同步每一位数据。那它们怎么知道一位数据什么时候开始、什么时候结束呢这就靠波特率Baud Rate和帧格式来约定。你可以把串口通信想象成两个人用摩斯电码发电报。波特率就是双方约定的敲击电键的基本时间单位比如每秒敲10下波特率9600大致对应每秒传输9600个比特位。帧格式就是电报的格式每次发电报先发一个“开始”信号起始位通常是逻辑0然后发8个电码数据位代表一个字节可能再加一个“校验”信号校验位用于检错最后发一个“结束”信号停止位通常是逻辑1可以是1、1.5或2个时间单位。这一整套就是一帧数据。STM32的USART硬件完美地封装了这个过程。当你把数据写入发送数据寄存器TDR后USART外设会自动帮你加上起始位、校验位如果使能和停止位然后按照设定的波特率在TX引脚上一位一位地移出。接收端则在RX引脚上持续采样当检测到起始位的下降沿时就启动一个内部计数器在每位数据的中间时刻进行采样将采样的比特流组合起来去掉帧头帧尾把最终的数据字节放入接收数据寄存器RDR。这一切都是硬件自动完成的CPU只需要在合适的时间去取数据或者放数据即可极大地解放了CPU。2.2 STM32 USART的独到之处与关键寄存器STM32的USART不仅仅是实现基础UART功能。以STM32F1系列为例其USART提供了许多高级特性多缓冲器通信支持单字节、多缓冲和DMA通信这是实现高效数据吞吐的关键。分数波特率发生器通过一个16位整数USART_BRR寄存器的高12位和一个4位小数低4位来设置波特率使得即使在非标准晶振频率下也能产生非常精确的波特率减少误差。多种校验控制支持偶校验、奇校验和无校验。多处理器通信可以通过地址标记实现唤醒特定从机在总线式通信中很有用。LIN、智能卡、IrDA等模式拓展了应用场景。对于开发者而言我们主要跟几个关键寄存器打交道以标准库或直接寄存器操作为视角USART_SR状态寄存器这是最重要的寄存器之一。你需要时刻关注其中的标志位TXE发送寄存器空为1时表示TDR寄存器为空可以写入下一个要发送的数据。注意很多新手会误判TC发送完成标志。TC要等到一帧数据完全从移位寄存器发送出去后才置位而TXE在数据从TDR转移到移位寄存器后就置位了。在连续发送时查询TXE效率更高。RXNE接收寄存器非空为1时表示RDR寄存器收到了新数据可以读取。ORE溢出错误、NE噪声错误、FE帧错误、PE校验错误这些错误标志位一旦置位通常需要软件读取USART_DR寄存器才能清除错误标志的清除比较特殊需要先读USART_SR再读USART_DR。如果不处理可能会导致后续数据无法接收。USART_DR数据寄存器这是一个双向寄存器。写操作会写入TDR启动发送读操作会读取RDR获取接收到的数据。读这个寄存器会清除RXNE标志写这个寄存器在TXE1时会清除TXE标志。USART_BRR波特率寄存器设置波特率的核心。计算公式为波特率 fCK / (16 * USARTDIV)。其中fCK是给USART的时钟频率PCLK1或PCLK2USARTDIV就是你要写入BRR寄存器的那个值包含整数和小数部分。STM32CubeMX或者库函数已经帮我们算好了但理解原理有助于排查波特率不匹配的问题。USART_CR1控制寄存器1功能开关的大本营。使能USARTUE位、使能发送TE和接收RE、设置数据位长度M位、使能校验控制PCE,PS、使能中断TXEIE,TCIE,RXNEIE,PEIE等都在这里配置。注意关于时钟。USART1通常挂在APB2总线其他USART挂在APB1总线。它们的时钟源不同频率也可能不同APB2通常比APB1快。在计算波特率和使用高级功能如DMA时务必先确认你使用的USART的时钟fCK是多少这是所有配置的基石。我曾在项目中将USART2的时钟误认为是系统主频导致波特率实际偏差巨大通信完全乱码排查了很久。3. 三种通信方式深度解析与选型指南搞清楚了原理和寄存器我们面临第一个工程选择用哪种方式让CPU和USART硬件协作主要有三种查询、中断和DMA。它们没有绝对的好坏只有适合的场景。3.1 查询方式简单场景的利器查询就是CPU不断地去“问”状态寄存器数据发完了吗查TXE或TC有数据来了吗查RXNE。这是最简单、最直观的方式。发送流程void USART_SendByte(USART_TypeDef* USARTx, uint8_t data) { while((USARTx-SR USART_SR_TXE) 0); // 等待发送寄存器空 USARTx-DR data; // 写入数据启动发送 // 如果希望确保数据完全发出可以再加一句等待TC // while((USARTx-SR USART_SR_TC) 0); }接收流程uint8_t USART_ReceiveByte(USART_TypeDef* USARTx) { while((USARTx-SR USART_SR_RXNE) 0); // 等待接收到数据 return (uint8_t)(USARTx-DR 0xFF); // 读取数据 }优点代码简单流程清晰没有中断开销对简单任务如单次发送一个命令或接收一个字节响应非常合适。致命缺点CPU被完全阻塞。在while循环等待期间CPU什么都做不了。如果对方设备迟迟不发送数据或者发送速度很慢你的整个程序就“卡死”在那里了。这在任何需要实时响应或多任务处理的系统中都是不可接受的。适用场景仅用于上电初始化时发送少量固定信息如版本号。在绝对没有其他任务需要执行的简单循环中。作为调试代码的临时手段。实操心得永远不要在产品代码的主循环中用查询方式做接收。我见过一个车载设备因为用查询等待GPS模块的$GPRMC语句导致整个UI界面刷新卡顿最后发现是GPS信号丢失时查询陷入了死等。这个坑让项目延期了一周。3.2 中断方式平衡性能与复杂度的首选中断是解放CPU的关键。我们不再轮询而是告诉USART硬件“数据准备好发送空或收到数据时你打断我一下我来处理”。核心配置在USART_CR1中使能对应的中断TXEIE和/或RXNEIE。在NVIC嵌套向量中断控制器中配置USART中断通道的优先级并使能。实现USART的中断服务函数IRQ Handler。中断服务函数编写要点void USART1_IRQHandler(void) { // 1. 首先判断是哪种中断源 if(USART1-SR USART_SR_RXNE) { // 接收中断 uint8_t received_data (uint8_t)(USART1-DR 0xFF); // 读取数据会自动清除RXNE标志 // 将数据存入缓冲区例如环形队列Ring Buffer ring_buffer_write(rx_buf, received_data); // 可以在这里进行协议解析但不宜做复杂耗时操作 } if((USART1-SR USART_SR_TXE) (USART1-CR1 USART_CR1_TXEIE)) { // 发送寄存器空中断且中断使能 if(发送缓冲区还有数据) { uint8_t data_to_send get_next_byte_from_send_buffer(); USART1-DR data_to_send; } else { // 发送缓冲区空了关闭发送空中断防止持续进入中断 USART1-CR1 ~USART_CR1_TXEIE; // 可以在这里置位一个“发送完成”软件标志通知主程序 } } // 错误中断处理非常重要 if(USART1-SR (USART_SR_ORE | USART_SR_NE | USART_SR_FE | USART_SR_PE)) { uint32_t error_flags USART1-SR; // 先读取SR (void)USART1-DR; // 再读取DR以清除某些错误标志根据手册要求 // 记录错误日志或进行错误恢复 // 如果不处理ORE溢出错误会导致后续数据无法接收 } }优点CPU利用率高。大部分时间CPU可以执行其他任务只有数据到来或需要发送下一字节时才被短暂打断。缺点每收发一个字节都要进一次中断。如果波特率很高如115200每秒可能产生上万次中断中断上下文保存/恢复的开销会变得显著可能影响系统实时性。对于大量连续数据收发中断频率过高。适用场景绝大多数中低速、非连续爆发的通信场景。例如接收GPS模块每秒一次的NMEA语句接收用户串口命令与蓝牙模块进行AT指令交互等。这是最常用、最平衡的方案。3.3 DMA方式高速大数据流的终极武器DMA直接存储器访问是“硬件搬运工”。你可以把它配置好数据从哪里来内存地址到哪里去USART-DR寄存器搬多少字节。配置完成后DMA控制器会在USART硬件发出请求如TDR空或RDR满时自动在总线上搬运数据完全不需要CPU干预。搬完后再给CPU发一个“完成中断”通知一下。发送配置内存到外设配置DMA通道设置源地址你的发送数组地址、目标地址USARTx-DR、数据宽度、传输数量、循环模式等。使能USART的DMA发送请求USART_CR3中的DMAT位。启动DMA传输。在DMA传输完成中断中处理后续事宜如关闭DMA通知主程序。接收配置外设到内存配置DMA通道设置源地址USARTx-DR、目标地址你的接收缓冲区地址、数据宽度、传输数量。接收通常使用循环模式让DMA在缓冲区首尾相接循环写入配合空闲中断来判定一帧数据结束下文详解。使能USART的DMA接收请求USART_CR3中的DMAR位。启动DMA传输。优点CPU零开销。在数据传输过程中CPU可以完全处理其他任务系统效率最高。特别适合高速如921600波特率或大数据量如图像、音频数据传输场景。缺点配置相对复杂需要理解DMA控制器的工作机制。对于不定长数据的接收处理需要结合其他机制如空闲中断来判断一帧数据的边界这是难点。适用场景高速ADC采样数据通过串口实时上传。向串口屏发送大量的图片数据。与其他设备进行文件传输。任何需要持续、高速、稳定传输数据的场合。选型决策表特性查询方式中断方式DMA方式CPU占用极高阻塞中频繁中断极低仅配置和完成中断实现复杂度极低中等高数据吞吐量极低中高极高实时性影响极差阻塞其他任务中等中断延迟好仅DMA完成中断典型应用单次调试输出命令响应、中等速率数据高速流数据、文件传输我的经验法则是默认首选中断方式它平衡了性能和复杂度。只有当明确遇到中断频率过高导致系统性能瓶颈或者有明确的、连续的大数据块传输需求时才升级到DMA方案。4. 不定长数据接收的工程解决方案这是串口应用中最经典的难题。对方发送的数据长度不固定比如一条指令“SET LED ON”和“SET LED1 BRIGHTNESS 200”长度不同。如何可靠地知道一帧数据已经接收完毕4.1 方案一超时判定法适用于低速、简单协议思路在中断中每收到一个字节就刷新一个计时器。如果超过一定时间例如5ms或10ms没有收到新字节就认为一帧数据结束。// 在中断中 void USART1_IRQHandler(void) { if(USART1-SR USART_SR_RXNE) { uint8_t data USART1-DR; ring_buffer_write(rx_buf, data); // 刷新超时计时器 g_uart_rx_timeout_timer UART_FRAME_TIMEOUT_MS; } } // 在主循环或SysTick中断中检查超时 void check_uart_frame(void) { if(g_uart_rx_timeout_timer 0) { g_uart_rx_timeout_timer--; if(g_uart_rx_timeout_timer 0) { // 超时认为一帧数据接收完成 process_received_frame(); } } }优点实现简单不依赖特定帧尾字符。缺点超时时间难以确定。设得太短容易把一帧数据拆成两帧设得太长帧尾响应延迟大。在波特率变化或数据流不均匀时不稳定。4.2 方案二特定帧尾字符法如\r\n思路在中断中检查收到的字节是否为约定的帧尾字符例如换行符\n。如果是则认为一帧数据结束。void USART1_IRQHandler(void) { if(USART1-SR USART_SR_RXNE) { uint8_t data USART1-DR; if(data \n) { // 检测到帧尾 // 将之前缓存的数据作为一帧处理 process_received_frame(); clear_rx_buffer(); } else { // 存储数据 store_to_rx_buffer(data); } } }优点判断精确帧结束立刻可知延迟小。缺点要求数据内容中不能包含与帧尾相同的字符否则会提前断帧。通常需要转义机制增加了协议复杂性。很多标准协议如NMEA、Modbus ASCII使用此方法。4.3 方案三空闲中断IDLE DMA / 中断强烈推荐这是STM32 USART提供的一个硬件特性也是目前处理不定长数据最优雅、最高效的方案。当RX线上持续一个字节传输时间的高电平即没有数据时硬件会检测到“线路空闲”状态并产生空闲中断IDLE。核心原理我们开启DMA循环接收模式目标是一个足够大的环形缓冲区。DMA会默默地把所有收到的字节按顺序存到缓冲区并自动管理写入指针。我们同时开启USART的空闲中断。当一帧数据发送完毕RX线空闲触发空闲中断。在空闲中断服务函数中我们通过计算DMA当前剩余传输数量CNDTR寄存器反推出从开始到空闲这段时间一共收到了多少字节的数据从而确定一帧的边界。配置步骤以HAL库为例但原理通用初始化USART开启接收DMA请求huart-hdmarx配置为循环模式。开启USART的空闲中断__HAL_UART_ENABLE_IT(huart, UART_IT_IDLE)。启动DMA接收HAL_UART_Receive_DMA(huart, rx_buffer, BUFFER_SIZE)。实现USART空闲中断回调在HAL_UART_IRQHandler中会调用UART_IDLECallback。// 用户重写的空闲中断回调函数 void HAL_UART_IdleCallback(UART_HandleTypeDef *huart) { if(huart-Instance USART1) { // 1. 清除空闲中断标志通过读SR和DR寄存器 __HAL_UART_CLEAR_IDLEFLAG(huart); // 2. 计算本次接收到的数据长度 // DMA接收缓冲区总大小 - DMA当前剩余未传输数据量 已接收数据长度 uint16_t recv_len BUFFER_SIZE - __HAL_DMA_GET_COUNTER(huart-hdmarx); if(recv_len 0) { // 3. 处理数据例如将rx_buffer[0]到rx_buffer[recv_len-1]的数据拷贝出来解析 process_received_data(rx_buffer, recv_len); // 4. 重置DMA接收可选如果缓冲区是循环的且处理速度够快可以不重置 // 但为了避免处理过程中新数据覆盖未处理完的数据通常建议重新启动DMA // 或者使用双缓冲区交替机制 __HAL_DMA_DISABLE(huart-hdmarx); huart-hdmarx-Instance-CNDTR BUFFER_SIZE; // 重新设置传输数量 huart-hdmarx-Instance-CMAR (uint32_t)rx_buffer; // 重新设置内存地址 __HAL_DMA_ENABLE(huart-hdmarx); } } }优点高效DMA搬运数据CPU几乎不参与。精准硬件自动检测帧结束无超时误差。灵活不依赖特定字符能接收任意数据。稳定避免了因中断处理延迟导致的数据溢出DMA有硬件缓冲。缺点配置和理解稍复杂需要处理好DMA指针和缓冲区管理防止数据覆盖。避坑指南使用“空闲中断DMA”时最大的坑是数据覆盖。如果一帧数据还没处理完下一帧数据又来了DMA会继续往缓冲区里写覆盖掉未处理的数据。解决方案有两种一是使用双缓冲区Ping-Pong Buffer一个用于DMA接收另一个用于应用程序处理接收完成后交换二是确保你的处理速度远快于数据接收速度。对于高速数据流双缓冲区是更稳妥的选择。我曾在一个工业传感器数据采集项目中因为处理JSON解析较慢又没用双缓冲导致数据错乱后来改用双缓冲后问题彻底解决。5. 稳定性实战错误处理与抗干扰设计一个健壮的串口通信模块必须能应对各种异常情况。否则在复杂的电磁环境或长线传输中通信会变得极其脆弱。5.1 必须处理的硬件错误标志在USART_SR寄存器中除了RXNE和TXE以下几个错误标志位必须被妥善处理尤其是在中断服务程序中ORE溢出错误这是最常见也最致命的一个错误。当RXNE标志还未被清除即CPU还没来及读取RDR中的数据下一个数据又来了就会发生溢出新数据丢失ORE位置1。关键点一旦ORE被置1除非你按特定顺序先读SR再读DR清除它否则RXNE将不会再被置起即后续所有数据都无法接收处理方法是在中断里如果检测到ORE先读取SR寄存器将错误标志加载然后读取DR寄存器清除ORE和RXNE最后最好再做一些错误计数或日志记录。FE帧错误当检测到停止位不是预期的“1”而是“0”时置位。通常由波特率不匹配、线路噪声或硬件故障引起。处理方式同ORE需要清除标志并考虑上报或调整通信参数。NE噪声错误PE校验错误分别在采样时检测到噪声和校验失败时置位。处理方式也是读取SR和DR来清除标志。一个健壮的中断服务函数开头应该这样void USARTx_IRQHandler(void) { uint32_t tmp_flag USARTx-SR; uint32_t tmp_err 0; // 先处理错误顺序很重要。 if(tmp_flag (USART_SR_ORE | USART_SR_NE | USART_SR_FE | USART_SR_PE)) { tmp_err USARTx-DR; // 读取DR以清除错误标志 // 记录错误到日志或变量例如 g_uart_error_count; // 如果错误频繁可能需要复位通信或报警 // return; // 注意发生错误时RXNE也可能被置位是否return取决于业务逻辑 } // 再处理正常数据接收 if((tmp_flag USART_SR_RXNE) (USARTx-CR1 USART_CR1_RXNEIE)) { uint8_t data (uint8_t)(USARTx-DR 0xFF); // ... 处理数据 } // 处理发送 if((tmp_flag USART_SR_TXE) (USARTx-CR1 USART_CR1_TXEIE)) { // ... 发送处理 } }5.2 软件层面的抗干扰与可靠性设计数据校验硬件校验位奇偶校验只能检测一位错误。对于重要数据必须在应用层增加软件校验如校验和Checksum、循环冗余校验CRC。在帧尾附加一个字节的校验和接收方重新计算并比对不匹配则丢弃或请求重发。协议设计帧头帧尾使用固定的、不常见的字节序列作为帧起始和结束标志如0xAA 0x55。长度字段在帧头后包含一个“数据长度”字段这样接收方可以预知该收多少字节结合帧尾校验双重保险。转义字符如果数据内容可能包含帧头帧尾字符需定义转义字符如0x7D并在其后的字符进行异或等操作实现透明传输。超时与重发机制对于需要应答的指令发送后启动一个定时器。如果在规定时间内未收到应答则认为帧丢失进行重发。重发次数应有上限避免死循环。缓冲区管理使用环形缓冲区Ring Buffer是必须的。无论是中断接收还是DMA接收都应该将数据先存入环形缓冲区再由后台任务取出处理。这能有效解耦“接收”和“处理”两个时延不确定的环节防止数据丢失。务必注意缓冲区的读写指针操作需要是原子操作在中断中写在主循环中读或者关中断保护。流控制在高速或处理能力不匹配的场合使用硬件流控制RTS/CTS或软件流控制XON/XOFF来防止数据丢失。当接收方缓冲区快满时通过拉低CTS硬件或发送XOFF字符软件通知发送方暂停发送。6. 高级应用与性能优化技巧6.1 多串口协同与资源管理在一个复杂系统中可能同时使用多个USART如UART1接调试终端UART2接GPSUART3接4G模块。管理好几个串口的关键是解耦和统一接口。为每个串口封装独立的驱动模块每个模块管理自己的缓冲区、状态机和回调函数。避免全局变量乱飞。使用RTOS如FreeRTOS这是最优雅的方案。为每个串口的接收任务创建一个信号量或队列。当串口中断收到一帧完整数据后释放一个信号量或发送一个消息到队列对应的处理任务被唤醒进行处理。这样各个串口的通信和处理完全并行互不阻塞。中断优先级管理如果多个串口都使用中断且通信都很关键需要合理设置NVIC中的中断优先级。通常接收中断的优先级应高于发送中断高波特率或关键数据通道的优先级应高于低波特率调试口。6.2 低功耗模式下的串口唤醒对于电池供电设备STM32可以进入低功耗模式如Stop模式以节省电量。此时USART可以配置为在收到数据时唤醒MCU。配置USART的唤醒中断USART_CR3中的WUFIE位并选择唤醒方式如地址匹配或线路空闲。在进入低功耗模式前确保USART和其时钟保持运行。当RX引脚检测到起始位或空闲帧时USART会产生唤醒事件将MCU从低功耗模式唤醒然后MCU再正常接收数据。这是一个高级功能需要仔细阅读参考手册中关于低功耗模式下外设行为的部分并配合正确的时钟配置。6.3 使用STM32CubeMX与HAL库的注意事项HAL库极大提高了开发效率但也隐藏了细节容易让人忽略原理。阻塞式APIHAL_UART_Transmit/Receive其内部就是查询TXE/RXNE标志的while循环。绝对不要在中断回调函数或高优先级任务中使用它们否则会导致长时间阻塞。中断和DMA APIHAL_UART_Transmit_IT/DMA和HAL_UART_Receive_IT/DMA是更常用的。但要注意它们通常不是“一次调用永久有效”。例如调用HAL_UART_Receive_IT(huart1, buffer, 10)是期望接收10个字节收满后回调HAL_UART_RxCpltCallback。如果你想持续接收需要在回调函数中再次调用HAL_UART_Receive_IT来重启接收。自定义回调函数HAL库提供了很多弱定义的Callback函数如HAL_UART_RxCpltCallback,HAL_UART_ErrorCallback。在自己的代码中重写它们以加入业务逻辑。超时参数很多HAL函数有Timeout参数。对于中断和DMA函数这个超时是指等待操作启动如DMA配置完成的超时而不是数据传输的超时不要理解错了。6.4 调试技巧与常见问题排查收不到数据/数据全为0检查时钟树配置确认USART所在APB总线的时钟已使能且频率正确。检查GPIO配置TX/RX引脚是否复用正确是否上下拉电阻配置有误通常配置为浮空输入和复用推挽输出。用示波器或逻辑分析仪测量TX/RX引脚看是否有波形。这是最直接的证据。检查波特率、数据位、停止位、校验位是否与对方设备严格一致。一个常见的坑是电脑端串口助手显示“115200 8N1”但实际可能默认带了奇偶校验。数据错乱/乱码首要怀疑对象是波特率误差。计算一下你的系统时钟和USARTDIV配置产生的实际波特率与目标波特率的误差是否在可接受范围通常要求2%。STM32的分数波特率发生器精度很高但前提是输入时钟fCK要算对。检查电源是否稳定尤其是使用外部有源晶振时。长距离传输时考虑线路干扰增加RS-485收发器而非直接使用TTL电平。通信一段时间后死机极有可能是中断服务函数处理时间过长或发生了中断嵌套导致栈溢出。检查中断函数中是否调用了耗时的函数如printf、浮点运算。检查是否未及时清除中断标志导致反复进入中断。使用DMA时检查缓冲区是否溢出或者DMA传输完成中断中是否进行了不当的操作。使用printf重定向到串口 这是一个非常方便的调试手段。通常重写_write或fputc函数。但请注意默认的printf是阻塞式的且线程不安全。在产品代码中建议使用自己实现的、基于环形缓冲区的非阻塞打印函数或者使用RTOS下的线程安全printf。最后串口通信的稳定性是“磨”出来的。理论懂了代码写了一定要实际上电测试用各种边界情况去冲击它高速发送、随机断电、插拔线缆、注入噪声观察系统的表现。所有的错误处理机制都是为了应对这些真实世界中必然发生的异常。当你设计的串口模块能在这种折腾下依然稳定工作时你对STM32 USART的理解才算真正过关了。