1. 回调函数不执行,问题大概率不在中断本身
如果你在用STM32 HAL库做串口接收,代码里明明调用了HAL_UART_Receive_IT(),中断服务函数也写了,但回调函数HAL_UART_RxCpltCallback()就是死活不进——这个场景我见过太多次了。新手第一反应通常是怀疑中断没触发,于是反复检查NVIC配置、翻手册确认中断向量表,折腾半天发现中断其实进了,只是回调没执行。问题的根子往往藏在HAL库的状态机逻辑里,而不是中断硬件本身。
这篇文章面向的是正在用STM32 HAL库开发串口通信的嵌入式工程师和学生,尤其是那些已经能跑通阻塞式收发、但切换到中断接收模式后卡住的开发者。我会把HAL库UART中断接收的完整链路拆开,从状态机、中断标志清除、回调注册到常见误用场景,逐层分析回调不执行的根因,并给出可直接复现的验证方法。涉及的关键点包括HAL_UART_Receive_IT的调用时机、huart->RxState的状态流转、__HAL_UART_ENABLE_IT的使能顺序,以及HAL_UART_IRQHandler内部的错误处理分支。
先把结论摆出来:回调不执行,九成以上是以下四类原因之一——接收中断没真正使能、状态机卡在BUSY_RX、错误标志未清除导致中断被挂起、或者回调函数被重复定义/弱符号覆盖。下面逐个拆解,每个都配上排查方法和实测验证。
2. HAL_UART_Receive_IT到底做了什么:状态机与中断使能的完整链路
2.1 从调用到中断使能,HAL库内部走了哪几步
很多人把HAL_UART_Receive_IT()当成一个"启动接收"的开关,调用了就等着回调。实际上这个函数内部做了一串有严格顺序的操作,任何一步没满足条件,函数会直接返回HAL_BUSY或HAL_ERROR,中断根本不会使能。
函数的核心逻辑大致是这样的:首先检查huart->RxState是否等于HAL_UART_STATE_READY,如果不是,直接返回HAL_BUSY,后面的代码一行都不执行。这就是为什么连续调用两次HAL_UART_Receive_IT(),第二次一定失败——第一次已经把状态改成了HAL_UART_STATE_BUSY_RX。状态检查通过后,函数会设置接收缓冲区指针pRxBuffPtr、接收长度RxXferCount,然后把RxState置为HAL_UART_STATE_BUSY_RX,最后调用UART_Start_Receive_IT()去使能具体的中断位。
UART_Start_Receive_IT()里做的事情更关键:它会根据你传入的数据长度决定使能哪些中断。如果长度是1,只使能RXNE(接收数据寄存器非空)中断;如果长度大于1,还会使能PE(奇偶校验错误)和ERR(帧错误、噪声、溢出)中断。最后调用__HAL_UART_ENABLE_IT()写入CR1和CR3寄存器。整个链路是串行的,前一步不满足,后面全部跳过。
注意:
HAL_UART_Receive_IT()的返回值一定要检查。返回HAL_BUSY说明上一次接收还没完成,返回HAL_ERROR说明串口初始化有问题。忽略返回值是回调不执行的头号原因。
2.2 RxState状态机:为什么第二次调用直接返回BUSY
huart->RxState是HAL库UART接收的核心状态变量,它只有三个值:HAL_UART_STATE_READY、HAL_UART_STATE_BUSY_RX、HAL_UART_STATE_BUSY_TX_RX。每次调用HAL_UART_Receive_IT(),第一件事就是检查这个变量。
问题在于,这个状态变量只有在接收完成回调触发后,才会被UART_Receive_IT()重新置回READY。也就是说,如果你启动了一次接收,但数据一直没来,状态就永远停在BUSY_RX,此时任何再次调用HAL_UART_Receive_IT()的尝试都会失败。很多人的代码结构是在main循环里轮询调用HAL_UART_Receive_IT(),或者在回调里忘记重新启动接收,导致第二次接收根本没使能中断。
更隐蔽的一种情况是:接收过程中发生了错误(比如溢出错误ORE),HAL库会进入错误处理分支,调用HAL_UART_ErrorCallback(),但RxState可能没有被正确复位。这时候你看到的现象就是中断进了、错误回调也进了,但正常接收回调永远不触发。
2.3 中断使能位与NVIC:两层开关缺一不可
STM32的UART中断有"两层开关":外设级的中断使能位(CR1寄存器里的RXNEIE、PEIE、EIE等)和NVIC级的中断通道使能。HAL_UART_Receive_IT()只负责打开外设级的中断使能位,NVIC的配置是在HAL_UART_MspInit()里通过HAL_NVIC_EnableIRQ()完成的。
如果你用的是CubeMX生成的代码,MspInit里通常已经配好了NVIC。但如果你是手动移植代码,或者从标准库转过来,很容易漏掉NVIC使能。这时候外设中断标志置位了,但NVIC不响应,中断服务函数根本不会被调用,回调自然无从谈起。
验证方法很直接:在调试器里查看USARTx->CR1寄存器的RXNEIE位是否为1,再查看NVIC的ISER寄存器对应位是否使能。两个都为1,中断链路才是通的。
3. 回调不执行的四种典型场景与逐层排查方法
3.1 场景一:接收中断压根没使能,中断服务函数从未进入
这是最基础也最容易确认的一种。排查步骤:在stm32fxxx_it.c里的USARTx_IRQHandler()函数第一行打个断点,或者翻转一个GPIO用示波器看。如果中断服务函数从来没进过,说明问题在中断使能环节。
先确认HAL_UART_Receive_IT()的返回值。如果返回HAL_BUSY,说明状态机不是READY,需要找到上一次接收为什么没完成。如果返回HAL_OK但中断还是不进,检查HAL_UART_MspInit()里有没有调用__HAL_RCC_USARTx_CLK_ENABLE()和HAL_NVIC_EnableIRQ(USARTx_IRQn)。我遇到过有人把NVIC配置写在MX_USART1_UART_Init()之后,但MspInit是在Init内部调用的,顺序反了导致NVIC没配上。
还有一种情况是中断服务函数名字写错了。HAL库的启动文件里定义的是USART1_IRQHandler,如果你写成了USART1_IRQHandlerr或者大小写不一致,链接器不会报错,但你的函数永远不会被调用。用调试器在启动文件的向量表里确认一下函数地址是否指向你的实现。
3.2 场景二:状态机卡在BUSY_RX,后续接收全部失效
状态机卡死通常发生在接收过程中出现错误但没被正确处理的时候。比如波特率不匹配导致帧错误,或者接收溢出导致ORE标志置位。HAL库在HAL_UART_IRQHandler()里会检查这些错误标志,如果使能了错误中断,会进入错误处理分支。
关键点在于:HAL库处理ORE错误的方式是"读SR寄存器再读DR寄存器"来清除标志,然后调用HAL_UART_ErrorCallback()。但如果你没有重写这个错误回调,或者重写了但没有在里面重新调用HAL_UART_Receive_IT(),接收状态就不会恢复。更麻烦的是,有些HAL版本在错误处理后会把RxState置为READY,但不会自动重新使能接收中断,需要你在错误回调里手动重启。
排查方法:在调试器里观察huart1.RxState的值。如果它一直是HAL_UART_STATE_BUSY_RX(值为0x22),而你又确认没有正在进行的接收,那就是卡死了。解决办法是在错误回调里先调用HAL_UART_AbortReceive_IT()或直接手动复位状态,再重新启动接收。
3.3 场景三:错误标志未清除,中断被错误处理分支截胡
这个场景很隐蔽:中断进了,但每次都在错误处理分支里打转,正常接收回调永远轮不到。典型表现是HAL_UART_ErrorCallback()被反复调用,而HAL_UART_RxCpltCallback()一次都不进。
根因通常是ORE(溢出错误)标志没清干净。STM32的ORE标志清除有个特殊要求:必须先读SR寄存器,再读DR寄存器,顺序不能反。HAL库内部是按这个顺序做的,但如果你在中断服务函数里自己加了读DR的代码,或者在错误回调里又读了一次DR,就可能打乱清除时序,导致ORE标志一直置位。
另一个常见原因是RXNE标志和ORE标志同时置位。当接收溢出时,这两个标志会一起出现。HAL库的处理逻辑是先判断ORE,如果ORE置位就进错误分支,清除标志后直接返回,不会去读DR里的数据。如果清除不成功,下次中断又进错误分支,形成死循环。
验证方法:在HAL_UART_IRQHandler()里打断点,观察每次进中断时SR寄存器的值。如果ORE位(bit3)一直是1,说明清除失败。这时候可以尝试在错误回调里手动执行一次"读SR、读DR"的操作,或者降低波特率、增加接收缓冲区来避免溢出。
3.4 场景四:回调函数被弱符号覆盖或重复定义
HAL库里的HAL_UART_RxCpltCallback()是一个__weak函数,意思是如果你没有自己定义,链接器会用库里的空实现。如果你自己定义了同名函数,链接器应该用你的版本。但如果你在多个.c文件里都定义了这个函数,或者函数签名不一致(比如参数类型写错),链接器可能选择了库里的弱符号,你的代码就成了"死代码"。
排查方法:在HAL_UART_RxCpltCallback()函数体第一行打个断点,如果中断进了、UART_Receive_IT()也执行到了调用回调的那一行,但断点就是不命中,那基本就是符号被覆盖了。用nm命令或者IDE的符号浏览器查看这个符号的地址,确认它指向你的函数而不是库里的空函数。
还有一种情况是函数名拼写错误。HAL库的回调名是HAL_UART_RxCpltCallback,注意是RxCplt不是RxComplete,也不是ReceiveCplt。拼错了编译器不会报错,因为那只是一个普通函数定义,但永远不会被HAL库调用。
4. 用调试器把中断链路逐段点亮:一套可复现的排查流程
4.1 从寄存器层面确认中断是否真正触发
与其猜,不如直接看寄存器。在调试器里打开USARTx的外设视图,重点看三个寄存器:CR1、SR、DR。CR1的RXNEIE位(bit5)应该是1,表示接收中断使能。SR的RXNE位(bit5)在收到数据后会置1,ORE位(bit3)在溢出时置1。DR里是接收到的数据。
如果RXNEIE是0,说明HAL_UART_Receive_IT()没有成功使能中断,回到上一节检查状态机和返回值。如果RXNEIE是1但SR的RXNE一直是0,说明数据根本没收到,检查硬件连线、波特率、时钟配置。如果RXNE和ORE同时为1,说明发生了溢出,需要先处理错误。
我习惯在USARTx_IRQHandler()入口处加一个GPIO翻转,用示波器看中断频率。如果中断根本没触发,示波器上就是一条直线;如果频繁触发但回调不进,说明在错误分支里打转。这个方法比单步调试直观得多。
4.2 在HAL_UART_IRQHandler内部设置断点,观察分支走向
HAL_UART_IRQHandler()是HAL库处理UART中断的总入口,内部有多个分支:错误处理、接收处理、发送处理。在调试器里单步走一遍,能清楚看到每次中断走了哪条路。
重点关注两个判断:第一个是错误标志判断,如果(SR & (PE|FE|NE|ORE))不为0,会进错误分支;第二个是RXNE判断,如果RXNE置位且RXNEIE使能,会调用UART_Receive_IT()。如果每次都在错误分支里结束,说明错误标志没清掉;如果RXNE判断通过了但UART_Receive_IT()没被调用,检查RXNEIE位是否真的为1。
UART_Receive_IT()内部会递减RxXferCount,当它减到0时,会调用HAL_UART_RxCpltCallback()。如果你在调试器里看到RxXferCount在递减但回调没进,那问题就在回调注册环节。
4.3 用GPIO翻转和串口打印做双重验证
调试器不是万能的,有时候中断频率太高,单步调试会打乱时序。这时候可以用GPIO翻转做粗粒度验证:在中断服务函数入口翻转GPIO1,在回调函数入口翻转GPIO2。用示波器同时看两路信号,如果GPIO1有翻转但GPIO2没有,说明中断进了但回调没执行;如果两路都没有,说明中断根本没进。
串口打印是另一种验证手段,但要注意不能在中断里用阻塞式打印,否则会引入新的时序问题。可以在回调里置一个标志位,在主循环里检测标志位并打印。这样既能确认回调是否执行,又不会影响中断响应。
提示:用GPIO翻转验证时,翻转操作要放在中断服务函数的最前面,避免被后续代码阻塞。如果用的是HAL库默认的
HAL_UART_IRQHandler(),可以在它之前加一行GPIO翻转。
5. 那些手册上不会写的实操细节与避坑经验
5.1 接收长度设为1和大于1,中断行为完全不同
HAL_UART_Receive_IT()的第三个参数是接收长度。设成1和设成大于1,HAL库使能的中断位不一样。长度为1时只使能RXNE中断,每收到一个字节就触发一次回调。长度大于1时还会使能PE和ERR中断,并且只有在收满指定长度后才触发回调。
这个差异导致一个常见坑:如果你设了长度10,但实际只收到5个字节,回调永远不会触发,状态机一直卡在BUSY_RX。解决办法是要么用长度1逐字节接收,要么用空闲中断(IDLE)配合DMA来收不定长数据。HAL库本身没有提供"收满或超时"的机制,需要自己用定时器或空闲中断来实现。
我个人的习惯是:对于命令响应类的协议,用长度1逐字节接收,在回调里自己组包;对于大批量数据,用DMA加空闲中断。HAL_UART_Receive_IT()设成长度大于1的场景其实很少,除非你确切知道每次数据长度固定。
5.2 在回调里重新启动接收的正确姿势
很多教程会告诉你"在回调里再次调用HAL_UART_Receive_IT()",但没告诉你这样做的风险和正确写法。回调触发时,UART_Receive_IT()已经把RxState置回了READY,所以此时调用HAL_UART_Receive_IT()是安全的。但如果你在回调里做了耗时操作(比如打印调试信息),可能会错过下一个字节,导致溢出。
正确的做法是:在回调里尽快重新启动接收,把数据处理逻辑放到主循环里。如果必须在回调里处理数据,确保处理时间远小于一个字节的传输时间。以115200波特率为例,一个字节的传输时间约87微秒,你的回调处理必须在这个时间内完成。
另一个细节是:如果你在回调里调用了HAL_UART_Transmit()之类的阻塞函数,会严重影响接收时序。发送数据应该用HAL_UART_Transmit_IT()或DMA方式,避免在接收回调里阻塞。
5.3 错误回调不重写,等于埋了一颗定时炸弹
HAL库默认的HAL_UART_ErrorCallback()是空函数。如果你不重写它,发生错误时HAL库会清除错误标志、调用空回调、然后继续。但有些HAL版本在错误处理后不会自动重新使能接收中断,导致后续数据全部丢失。
我的建议是:永远重写HAL_UART_ErrorCallback(),在里面做三件事——记录错误类型(通过huart->ErrorCode判断)、复位接收状态、重新启动接收。复位状态可以用HAL_UART_AbortReceive_IT(),它会清除所有接收相关的中断使能位并把RxState置回READY。然后再调用HAL_UART_Receive_IT()重新开始。
错误类型可以通过huart->ErrorCode的位掩码判断:HAL_UART_ERROR_PE是奇偶校验错误,HAL_UART_ERROR_NE是噪声错误,HAL_UART_ERROR_FE是帧错误,HAL_UART_ERROR_ORE是溢出错误。其中ORE最常见,通常是因为接收处理太慢或者波特率不匹配。
5.4 用CubeMX生成代码时,这几个配置项要特别留意
CubeMX能自动生成UART初始化代码,但有几个配置项容易配错。第一个是NVIC标签页里的中断使能,必须勾选USARTx global interrupt,否则MspInit里不会生成NVIC配置。第二个是DMA配置,如果你同时用了DMA接收,注意DMA中断和UART中断的优先级配置,避免DMA中断抢占UART中断导致数据丢失。
第三个是时钟配置。UART的波特率依赖于APB时钟,如果时钟树配错了,波特率会偏差很大,导致通信失败或频繁出错。用CubeMX的时钟树视图确认USARTx的时钟源频率,再核对波特率寄存器(BRR)的计算值。以STM32F103为例,USART1挂在APB2上,默认72MHz,波特率115200对应的BRR值是0x271,可以用这个值做交叉验证。
还有一个隐藏坑:CubeMX生成的MX_USARTx_UART_Init()里会调用HAL_UART_Init(),而HAL_UART_Init()内部会调用HAL_UART_MspInit()。如果你在MX_USARTx_UART_Init()之后又手动调用了HAL_UART_MspInit(),会导致GPIO和NVIC被重复初始化,虽然通常不会出问题,但可能覆盖你手动修改的配置。
6. 从标准库迁移到HAL库时,中断接收的思维转换
6.1 标准库的"直接操作寄存器"思路在HAL里行不通
用惯了标准库的人,写中断接收通常是这样的:在USARTx_IRQHandler()里判断RXNE标志,读DR寄存器,处理数据,清标志。整个过程直接操作寄存器,逻辑清晰。但HAL库把这套流程封装进了HAL_UART_IRQHandler(),你只需要调用它,剩下的交给库处理。
问题在于,很多从标准库迁移过来的人会保留自己的中断服务函数逻辑,同时又调用了HAL_UART_IRQHandler(),导致中断被处理两次。第一次你的代码读了DR,清除了RXNE标志;第二次HAL_UART_IRQHandler()发现没有中断标志,直接返回。这种情况下回调可能偶尔触发,但行为不稳定。
正确的做法是:中断服务函数里只调用HAL_UART_IRQHandler(&huartx),所有数据处理逻辑放到回调函数里。不要在中断服务函数里直接操作寄存器,除非你明确知道自己在做什么。
6.2 HAL库的"状态机+回调"模型需要重新理解
标准库的中断接收是"事件驱动"的:中断来了就处理,处理完就结束。HAL库是"状态机+回调"模型:HAL_UART_Receive_IT()启动一次接收,状态机进入BUSY_RX,中断来了由HAL_UART_IRQHandler()处理,收满指定长度后状态机回到READY并触发回调。
这个模型要求你在每次接收完成后重新启动接收,否则状态机停在READY但中断没使能,后续数据收不到。很多人的代码只在初始化时调用一次HAL_UART_Receive_IT(),收完第一包数据后回调触发了,但忘记重新启动,第二包数据就收不到了。
理解这个模型的关键是:HAL_UART_Receive_IT()不是"打开接收开关",而是"启动一次接收任务"。每次任务完成后,需要重新启动下一次任务。这跟标准库的"使能中断后一直收"是两种不同的思路。
6.3 中断优先级配置的差异与注意事项
标准库通常直接在NVIC里配置优先级,HAL库通过HAL_NVIC_SetPriority()和HAL_NVIC_EnableIRQ()两个函数配合完成。CubeMX生成的代码里,优先级配置在HAL_UART_MspInit()里,但如果你手动修改了优先级,要注意HAL库的优先级分组设置。
HAL_Init()里会调用HAL_NVIC_SetPriorityGrouping()设置优先级分组,默认是NVIC_PRIORITYGROUP_4,即4位抢占优先级、0位子优先级。如果你在别的地方又调用了这个函数改了分组,可能导致UART中断的优先级不符合预期。
UART中断的优先级不宜设得太高,否则会抢占其他关键中断(比如定时器中断)。也不宜设得太低,否则可能被其他中断阻塞导致接收溢出。我通常把UART中断设为中等优先级,比如抢占优先级5左右,具体要看系统里其他中断的分布。
7. 几个真实案例的排查过程复盘
7.1 案例一:回调只进一次,之后再也不进
有个朋友的项目里,串口接收回调第一次能进,之后再也进不去了。他反复检查中断配置,都没问题。我让他把huart1.RxState的值打印出来,发现第一次回调后状态是READY,但第二次调用HAL_UART_Receive_IT()返回的是HAL_BUSY。
根因是他的回调函数里有一行HAL_UART_Receive_IT(&huart1, buf, 1),但他在回调外面又调用了一次。第一次回调触发时,回调内部的调用把状态置为BUSY_RX,然后回调返回,外层代码又调用了一次,返回BUSY。之后状态一直是BUSY_RX,因为第二次调用失败后没有数据来触发完成回调。
解决办法很简单:只在回调里重新启动接收,外层代码不要重复调用。或者用一个标志位控制,确保同一时间只有一个接收任务在运行。
7.2 案例二:中断进了但回调不进,错误标志在作怪
另一个案例是中断服务函数频繁进入,但HAL_UART_RxCpltCallback()一次都不进。在调试器里看SR寄存器,ORE位一直是1。进一步检查发现,他的接收回调里有一行printf,打印耗时太长,导致下一个字节来的时候上一个还没处理完,接收溢出。
ORE标志置位后,HAL库进入错误分支,清除标志后返回。但因为他没有重写错误回调,HAL库的默认空回调什么也不做,接收状态没有恢复。下一个字节来的时候又溢出,又进错误分支,形成死循环。
解决办法是去掉回调里的printf,改用标志位加主循环打印。同时在错误回调里重新启动接收,确保溢出后能恢复。
7.3 案例三:移植代码后回调不执行,符号被覆盖
有个从其他项目移植过来的代码,UART配置看起来完全正确,但回调就是不执行。用调试器在HAL_UART_RxCpltCallback()里打断点,发现中断进了、UART_Receive_IT()也执行到了调用回调的那一行,但断点不命中。
用nm命令查看符号表,发现HAL_UART_RxCpltCallback有两个定义:一个在他的.c文件里,一个在HAL库的stm32fxxx_hal_uart.c里。链接器选择了库里的弱符号,他的实现被忽略了。根因是他的函数签名写成了void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart, uint16_t len),多了一个参数,导致链接器认为这是另一个函数。
把参数改成和HAL库一致后,问题解决。这个坑的教训是:重写HAL库回调时,函数签名必须和库里的声明完全一致,包括参数类型和返回值。
8. 把接收逻辑写稳的几个工程习惯
8.1 用环形缓冲区解耦中断与数据处理
在中断回调里直接处理数据是很多问题的根源。更好的做法是在回调里只做一件事:把收到的字节写入环形缓冲区,然后重新启动接收。主循环从环形缓冲区里取数据并处理。这样中断处理时间极短,不会因为数据处理耗时导致溢出。
环形缓冲区的实现很简单:一个数组加两个指针(读指针和写指针)。写指针在中断里更新,读指针在主循环里更新。注意读写指针的更新要保证原子性,对于8位或16位指针,在32位MCU上单次读写是原子的,不需要额外保护。如果缓冲区大小超过255,指针用uint16_t,同样在32位MCU上单次访问是原子的。
缓冲区大小要根据数据速率和处理速度来定。以115200波特率、主循环1毫秒轮询一次为例,1毫秒最多收到约11个字节,缓冲区设64或128字节足够。如果主循环可能被其他任务阻塞,缓冲区要相应加大。
8.2 空闲中断配合DMA,收不定长数据的利器
对于不定长数据,HAL_UART_Receive_IT()设固定长度很不方便。更好的方案是用DMA接收加空闲中断(IDLE)。DMA负责把数据搬到缓冲区,空闲中断在总线空闲时触发,告诉你一帧数据收完了。
配置方法是:用HAL_UART_Receive_DMA()启动DMA接收,然后在HAL_UART_MspInit()里使能IDLE中断(__HAL_UART_ENABLE_IT(&huartx, UART_IT_IDLE))。在中断服务函数里判断IDLE标志,清除标志后计算收到的数据长度(用DMA的剩余计数反推),然后处理数据。
这个方案的优点是CPU占用极低,数据长度灵活。缺点是配置稍复杂,需要同时理解DMA和空闲中断。STM32的HAL库没有直接提供空闲中断的回调,需要自己在中断服务函数里处理。
8.3 超时机制:给接收任务加一个保险丝
无论用中断还是DMA,都建议加一个超时机制。如果接收任务启动后长时间没有完成,强制复位接收状态并重新启动。这样可以避免因为干扰、波特率偏差等原因导致的状态机卡死。
实现方式可以用一个定时器,在启动接收时开启,接收完成回调里关闭。如果定时器超时了接收还没完成,在定时器中断里调用HAL_UART_AbortReceive_IT()复位状态,然后重新启动接收。超时时间根据协议来定,一般设为预期帧间隔的2到3倍。
这个机制在工业现场特别有用,因为现场干扰大,偶发的帧错误或溢出很难完全避免。有了超时保险丝,即使出错也能自动恢复,不需要人工重启设备。
9. 写在最后:几个我踩过的坑和常用检查清单
调试UART中断接收这些年,我踩过的坑大致可以归成三类:状态机相关的、中断使能相关的、回调注册相关的。每次遇到回调不执行,我现在会按这个顺序检查:先看HAL_UART_Receive_IT()的返回值,再看huart->RxState的值,然后看CR1寄存器的RXNEIE位,最后看NVIC的使能位。这四步走完,基本能定位到问题所在。
还有一个习惯是:在项目初期就把错误回调重写好,不要等到出问题了再补。错误回调里至少要做状态复位和重新启动接收,最好再加一个错误计数器,方便后期统计通信质量。我见过太多项目因为没处理ORE错误,跑几天就死机一次,查半天查不出原因。
最后分享一个调试小技巧:如果怀疑是中断优先级或时序问题,可以把UART中断优先级临时调到最高,看问题是否消失。如果消失了,说明是优先级冲突;如果还在,说明是配置或逻辑问题。这个方法能快速缩小排查范围。