
简介这是一份面向STM32嵌入式开发者的串口双通道中断接收示例工程解决USART1与USART2同时工作时的数据实时处理问题适用于多串口通信、复杂协议解析与多数据源采集等场景。压缩包共122个文件包含标准库工程所需的h源码、c程序、uvproj工程文件、hex烧录文件以及map、axf等构建产物并附带串口打印相关的printf与USER_printf示例整体体积仅1.11MB便于直接打开对照学习。资源基于STM32F103平台完整展示了GPIO复用配置、USART初始化、中断服务函数编写及NVIC中断优先级设置等关键步骤可帮助初学者快速理解双串口中断机制的搭建流程。已有1431人学习下载适合正在调试多串口通信或希望掌握中断方式接收数据的开发者参考复用。 前阵子一个项目里遇到个很实际的需求主控板要同时跟两个外设通信一个负责跟触摸屏通过串口对话另一个要实时接收传感器采集板的数据。单串口跑得挺顺一上两个串口同时中断输入怪事就来了——插上串口1的数据线串口2经常假死或者两个串口都在收发收着收着其中一个彻底不响应。折腾了大半天最后定位到的问题五花八门优先级配错、回调函数互相干扰、接收中断没有重新使能。这篇就把stm32串口12同时中断输入从原理到实操完整梳理一遍包括NVIC优先级配置、HAL库回调拆分、实测中遇到的坑以及RS485场景下的并发适配。刚接触双串口的朋友可以直接跟着做已经踩过坑的也可以对照排查。1. 为什么两个串口同时接收会“打架”先搞清楚硬件层和软件层的关系很多人看到“两个串口同时中断”第一反应是这能行吗MCU同一时间只能执行一条指令两个串口同时来数据中断不会互相挤掉吗好消息是STM32的每个USART外设完全是独立的硬件模块各自有独立的接收移位寄存器、数据寄存器、中断标志位和中断向量。串口1的RX引脚收到起始位后硬件自动把数据移进USART1-DR串口2的RX引脚同时收到数据硬件自动移进USART2-DR。这俩过程互不干扰也不需要CPU参与字节移位。CPU真正介入的时机是硬件把某个数据寄存器填满后触发中断的那一刻。所以从硬件层面讲两个串口同时接收在物理上没有任何冲突。真正的“打架”发生在软件层常见的就是三种情况中断优先级没配好一个串口的中断长时间占用CPU另一个串口的数据没来得及处理被后续新数据覆盖表现为“丢字节”或“卡死”。HAL库的中断回调函数是共用的两个串口都走到同一个HAL_UART_RxCpltCallback里代码里没区分串口数据互相串。接收中断是单次生效的收到一字节后没有重新调用接收函数中断被关闭表现就是“只收一次就再也不进了”。这三个问题前两个靠配置和代码规范解决第三个是HAL库机制问题是新手最容易忽略的。下面逐一拆。2. NVIC中断优先级配置双串口稳定的地基2.1 抢占优先级和子优先级到底谁说了算NVIC嵌套向量中断控制器是决定两个串口中断谁先执行的关键。STM32中断优先级分两组字段抢占优先级preemption priority和子优先级subpriority。抢占优先级决定是否能够打断正在执行的中断。两个中断同时挂起时抢占优先级高的先执行如果一个高抢占优先级中断来了正在执行的低抢占优先级中断会被打断CPU跳过去处理高优先级中断处理完再回来。子优先级只在抢占优先级相同的情况下起作用。抢占优先级相同的中断之间不分先后嵌套谁的子优先级高数值小谁先排队执行但不会互相打断。实际操作中最稳妥的做法是受实时性要求极高的串口设更高的抢占优先级另一个设相同或略低的抢占优先级。但如果你两个串口都在跑实时收发我个人的习惯是让两个串口的抢占优先级相同比如都设3子优先级分别配0和1。这样两个串口的数据谁先到谁先处理不存在互相打断导致一方长期饿死的问题。2.2 CubeMX里的配置要点打开CubeMX在Pinout Configuration里启用USART1和USART2后切到NVIC Settings选项卡你会看到这两个外设的中断使能选项USART1 global interrupt - 勾选 Enabled抢占优先级设为3子优先级设为0 USART2 global interrupt - 勾选 Enabled抢占优先级设为3子优先级设为1同时要注意左上角的Priority Group设置。用过标准库的人都知道NVIC_PriorityGroup_4这种分组方式HAL库对应的是HAL_NVIC_SetPriorityGrouping。CubeMX默认一般是NVIC_PRIORITY_GROUP_2也就是2位抢占优先级0~3加2位子优先级0~3。保持这个默认值即可但务必确认两个串口用的是同一优先级分组——如果代码里某个地方把分组改了整个工程的优先级配置就全乱套了表现极其诡异比如串口2本来优先级更高结果反而一直被串口1按在地上摩擦。2.3 一个容易想当然的地方发送和接收中断的关系USART1的所有中断接收、发送、错误在STM32里都映射到同一个中断向量USART1_IRQn进到IRQHandler之后HAL库再通过判断标志位分发到不同的回调。也就是说串口1的发送完成中断不会跟串口1的接收中断互为“抢占优先”关系它们压根不是两个独立的中断源。这带来的一个实际影响是在USART1_IRQHandler这个入口里不管进来的是发送完成事件还是接收事件你都不能做耗时操作否则另一个串口的中断就被堵住了。很多双串口“一收发就卡”的问题其实是中断服务函数里塞了printf、延时这类操作。printf重定向到串口1再在中断里调用串口1自己就把自己卡死了串口2的排队还得看串口1脸色。3. 初始化与中断服务函数拆分两条独立的接收链路3.1 初始化阶段要做什么CubeMX里配置好两个串口的参数后生成的初始化函数分别是MX_USART1_UART_Init和MX_USART2_UART_Init。两个串口的波特率、字长、停止位是各自独立的比如我这边串口1跑115200连触摸屏串口2跑9600连传感器板完全没问题。需要注意的一点是GPIO复用配置。很多人初始化完串口发现引脚电平不对多半是AF复用没设对。串口1的TX/RX要复用成AF7串口2的TX/RX也要复用成对应引脚的AF7这在CubeMX里会自动生成但如果手动写代码或者移植工程就容易漏掉。初始化完成后在主循环之前调用接收使能函数启动两个串口的接收HAL_UART_Receive_IT(huart1, rx1_data, 1); HAL_UART_Receive_IT(huart2, rx2_data, 1);注意这个函数的作用是“使能接收中断并准备好接收1个字节”不是连续不断地接管所有数据。每收满指定的字节数这里指定1中断就会关闭必须在回调里再次调用才能继续接收下一字节。这正是“只收一次”故障的根源。3.2 回调函数如何正确区分两个串口HAL库把同一个串口的所有接收完成事件都汇到HAL_UART_RxCpltCallback这一个弱函数里。两个串口都用这个回调就必须自己判断是哪个串口触发的中断否则数据会互相污染。我见过最典型的错误写法是这样的uint8_t rx_data; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { // 没有判断huart串口1和串口2的数据都往同一个变量里塞 rx_data 0; // ... }正确的做法是在回调入口处判断外设实例uint8_t rx1_data, rx2_data; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { process_rx1_frame(rx1_data); HAL_UART_Receive_IT(huart1, rx1_data, 1); } else if (huart-Instance USART2) { process_rx2_frame(rx2_data); HAL_UART_Receive_IT(huart2, rx2_data, 1); } }判断条件用huart-Instance USART1还是huart-Instance USART2都可以推荐用Instance判断可读性好且不容易出错。还有一种写法是把两个串口的句柄地址拿来比较比如if (huart huart1)效果一样但C语言里结构体比较不能直接写只能比地址别搞混了。3.3 接收缓冲区千万别混用两个串口的接收缓冲区务必分开。哪怕处理逻辑一模一样缓冲区也要独立这是我在实际项目中用血的教训换来的。之前图省事两个串口共用一个全局数组接收数据结果串口1刚收完一帧串口2的中断进来把同一个数组冲了串口1还没来得及解析的数据就全毁了。正确做法是给每个串口单独分配接收缓冲区和对应的解析状态机变量#define RX1_BUF_SIZE 64 #define RX2_BUF_SIZE 128 uint8_t rx1_buf[RX1_BUF_SIZE]; uint8_t rx2_buf[RX2_BUF_SIZE]; volatile uint8_t rx1_index 0; volatile uint8_t rx2_index 0;之所以加volatile是因为这些变量在中断上下文和主循环上下文都会被访问不加volatile的话编译器优化后主循环可能读到缓存的值导致数据明明到了却判断不出来。这是嵌入式里经典的共享变量坑。4. 实测中的两个经典故障丢数据与只收一次4.1 故障一丢字节排查链路现象两个串口同时通信串口1发一个16字节的报文串口2在连续接收传感器数据。串口1的报文偶尔会丢最后一两个字节。排查路径是这样的第一步先用逻辑分析仪抓串口1的RX引脚波形确认是MCU没收到还是MCU收了没处理。抓波形发现数据确实到达引脚了排除硬件连接问题。第二步在HAL_UART_RxCpltCallback入口处打断点或置一个IO口翻转信号发现最后一个字节根本没进回调。这说明接收中断没有触发。第三步查中断是否被屏蔽。排查发现串口1设置抢占优先级1串口2设置抢占优先级2。当串口2高频连续进入中断处理数据时串口1的中断被串口2的同优先级中断“排挤”又因为接收中断是边沿触发的如果硬件已经把数据收进寄存器但CPU没来得及读下一个字节到来时数据寄存器被覆盖先前的字节就丢了。这个问题的解决方式有两种。最简单的是把两个串口的抢占优先级调成相同这样不会形成长期压榨。更严谨的做法是给串口1配置DMA接收让DMA直接把数据搬到内存即使CPU在处理串口2的中断串口1的数据也不会丢。DMA的方式后面细说。4.2 故障二只收一次后面的数据全部没反应这个故障典型到几乎每个用HAL库的人都踩过。现象就是上电后第一个字节能收到之后就再也不进中断了。根因很简单HAL_UART_Receive_IT每接收指定数量的字节后会自动关闭接收中断你必须在回调里再次调用它才能让接收中断保持使能状态。void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { handle_byte(rx1_data); HAL_UART_Receive_IT(huart1, rx1_data, 1); // 重新使能关键 } }有人会在主循环的while(1)里轮询调用HAL_UART_Receive_IT但这有个坏处如果某次调用发生在接收中断正在进行中可能导致返回HAL_BUSY恰好没使能上然后就再也没人管接收了。在回调函数里重新调用是最稳妥的方式因为回调执行完中断函数马上退出不会有竞争。4.3 数据错乱一个容易被忽略的定时器坑还有一类故障不是中断配置问题而是和定时器有关。热词里也出现了“stm32 tim定时器”相关搜索这里顺带提一下。如果工程里某个定时器配置了很高的优先级中断并且中断服务函数执行时间较长它会抢占串口中断。特别是用定时器做软件延时或者按键扫描的工程定时器中断优先级如果配得比串口还高双串口接收就会异常。排查思路是先把定时器中断的抢占优先级降下来比如设到3和串口同级或者更低确认问题是否消失。如果消失说明就是优先级挤压的问题调整完再重新验证双串口通信的实时性是否满足需求。5. RS485半双工场景下的串口并发适配如果你做的是工控项目两个串口中有一个跑RS485那情况比普通TTL串口要复杂一点。RS485是半双工总线同一时刻只能收或者只能发方向控制通常用一个GPIO控制收发芯片的DE/RE引脚。这意味着即使MCU的串口1和串口2都能同时开中断接收硬件层面总线上仍然存在方向切换的问题。一个典型场景串口1接RS485总线连多个仪表串口2接TTL电平的调试屏。串口1以9600波特率周期轮询仪表数据串口2以115200波特率实时接收屏的点击指令。双串口中断输入在这个场景下的关键点不是“同时收”而是“RS485方向切换时要保证串口1的接收中断不被自己打乱”。RS485方向控制的标准动线是void rs485_send(uint8_t *data, uint16_t len) { RS485_DE_HIGH(); // 拉高DE/RE方向脚进入发送模式 HAL_UART_Transmit(huart1, data, len, 100); RS485_DE_DEALY(); // 等待最后一个字节完全移出 RS485_DE_LOW(); // 拉低回到接收模式 }这里容易忽略的是RS485_DE_DEALY这一步。串口发送函数返回时数据可能还留在发送移位寄存器里没有全部移出如果立刻拉低DE引脚最后一个字节的停止位可能被截断对端仪表就收不到完整帧。延时多少取决于波特率9600波特率下发送一个字节约1.04ms保守起见延时2ms以上。双串口同时接收时如果串口2在处理数据的过程中串口1的RS485发送使能两者不会冲突因为RS485的方向切换只影响串口1对应的那个总线。但如果项目里两个串口都接了RS485总线那就要小心两个收发方向引脚的操作顺序了。此时发送两个总线的指令要用状态机串行处理不能两个串口同时往各自总线发数据——虽然MCU能同时跑但应用层的总线控制逻辑会让整个系统状态难以追踪。另一个RS485相关的坑在热词里也很常见接收空闲中断判断接收线束。很多人用空闲中断IDLE来判定一帧数据接收完毕这在半双工RS485总线场景下确实比“固定字节数”或者“超时轮询”可靠。但要注意HAL库的标准外设库对新旧版本API命名有差异老版本用的是UART_Receive_IDLE新版本才统一进HAL_UARTEx_RxEventCallback等回调。如果你从网上复制代码版本不匹配会出现编译错误或者行为异常这个要特别注意。6. 从调通到稳定几个值得养成的工程习惯6.1 中断服务函数里坚决不做重活这是老生常谈但值得再用双串口场景强调一次。中断服务函数里最典型的重活就是HAL_UART_Transmit阻塞发送和printf。printf一旦重定向到串口里面隐含了阻塞发送逻辑数据多时可能卡在串口1的发送上几毫秒甚至几十毫秒这段时间里串口2的中断全部拥堵。把数据从中断里“拿出来”解析放主循环做这是双串口稳定运行的基本功// 中断里只做两件事搬数据 重新使能接收 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { rx1_frame[rx1_index] rx1_data; if (rx1_index RX1_BUF_SIZE) { rx1_index 0; } HAL_UART_Receive_IT(huart1, rx1_data, 1); } else if (huart-Instance USART2) { // 同样的逻辑 } }主循环里再用状态机解析rx1_frame和rx2_frame里的数据。这样做的好处是即使两个串口同时来数据中断函数也只需要几微秒就退出绝不会互相饿死。6.2 调试双串口的工具组合双串口联调时我常用的方案是两个USB转TTL模块分别接到电脑的两个USB口在串口调试助手里开两个窗口一个串口一个颜色标记进行区分。这样两个串口的数据都能独立监视问题出在哪个链路一眼就能看出来。热词里反复出现的ch340和ch341驱动问题其实大多是USB转TTL模块的驱动没装好或者被系统识别成未知设备。Windows下装好官方驱动后在设备管理器里能看到对应COM口号如果插上模块后设备管理器里出现黄色感叹号优先重装驱动别急着怀疑板子。如果手头有逻辑分析仪调试优先级问题会更直观。把两个串口的TX/RX引脚都接到逻辑分析仪上用触发模式抓取两个串口的波形。之前排查“丢字节”问题的时候我就是靠逻辑分析仪确认数据到了引脚却没进回调直接锁定了中断优先级挤压的方向。6.3 低速优先验证一个再加一个双串口并发调试有个很实用的推进策略先只打开串口1以112500波特率测试确认单串口通信稳定再把串口1零散发送时串口2接收测试最后才是两个串口同时高频收发压测。每一步都跑10分钟以上再推进到下一步。这样做的好处是一旦压测出问题你能明确判断是哪一个环节引入的。直接一上来就两个串口全速跑出了问题排查范围太广很难定位。最后分享一个小技巧两个外设串口的波特率通常不同比如一个9600一个115200。判断“卡死”问题是逻辑问题还是波特率问题时可以先把两个串口都配成一样的波特率跑一遍。如果问题消失说明跟波特率差异没有关系问题仍然出在中断处理或数据竞争层面如果问题依旧那排查优先级和共享资源的优先级更高。这个手段在排查双串口问题时非常省时间。本文还有配套的精品资源点击获取