ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32L0串口不进中断?UART与LPUART1配置及HAL库排查指南

STM32L0串口不进中断?UART与LPUART1配置及HAL库排查指南 做低功耗产品的朋友对STM32L0肯定不陌生这颗芯片的省电能力在ST全系里都排得上号但正因为低功耗它的串口外设也做得和普通MCU不太一样。我最近在STM32CubeMX里同时用UART和LPUART1做了一版网关板卡生成代码后一跑前面收到的几包数据是好的再往后串口中断就彻底不进了把波特率调低、换引脚、重新生成工程都试过折腾了半天最后才发现问题不在外设配置而是出在中断接收的“重新启动”这个动作上。这篇文章把我这次踩坑的过程和排查方法完整写出来写给正在用STM32CubeMX HAL库开发STM32L0、又遇到串口不进中断问题的朋友。这个标题拆开来看其实是三件事UART在CubeMX里怎么配、LPUART1和普通UART到底差在哪、串口不进中断究竟怎么查。我会把原理、配置、代码、排查一条线讲清楚尽量不废话直接上干货。1. 选型与原理STM32L0的UART家族为什么LPUART1要单独拎出来讲1.1 普通UART与LPUART1的硬件定位差异STM32L0系列通常带USART1、USART2两个普通串口外加一个LPUART1。USART是全功能串口支持同步模式、智能卡、LIN、IrDA这些而LPUART1本质上是一个精简版串口名字里的“LP”就是Low Power的意思。它牺牲掉那些不常用的功能换来了非常低的运行功耗并且可以在MCU进入Stop模式时继续工作用接收数据来唤醒系统。这一点是LPUART1和普通UART最大的区别。普通USART的时钟来自APB总线系统进Stop之后APB时钟被关闭串口就停工了LPUART1不一样它有独立时钟源可以选择即使系统主时钟停了只要它的时钟源还在跑就能维持接收逻辑。所以那些用纽扣电池供电、靠串口命令唤醒的设备基本都会把LPUART1作为唯一选择。但代价也很明显。LPUART1的功能寄存器比USART少很多不支持同步模式、智能卡、红外这些扩展功能连波特率范围都受限。也就是说你不能把它当成一个普通串口去随手配个115200完事它的时钟源和波特率有着严格的搭配关系。1.2 波特率不是想配多少就配多少先算一笔时钟账LPUART1的时钟源在STM32L0上可以选择LSE外部32.768kHz晶振、HSI内部16MHz或者系统时钟。CubeMX允许你在RCC配置里选但很多人根本没注意这个选项直接用默认配置结果通信就是不稳定。LPUART的波特率发生器为了降低功耗分频寄存器能做到的最小值是有限制的。在16倍过采样模式下BRR分频值最小值是3。什么意思就是波特率的上限等于时钟频率除以3。如果选LSE作为时钟源32.768kHz除以3得到大约10.9kbps。也就是说LSE下最高只能跑到9600bps配115200必然失败。不是调整一些误差就能救回来的是硬件上限。如果你在CubeMX里把LPUART时钟源选了LSE然后波特率填115200CubeMX一般不会报错但烧进去之后收到的全是乱码。如果选HSI16MHz作为LPUART时钟源就可以支持115200甚至更高代价是LPUART在低功耗模式下的优势会打折扣——因为HSI需要保持开启而HSI的功耗远大于LSE。如果你的场景不需要在Stop模式下跑串口直接选HSI或系统时钟省心很多如果一定要低功耗唤醒那就老老实实9600bps配LSE。顺便说一句普通UART一样要注意波特率误差。STM32L0的系统时钟如果用的是内部MSI精度在室温下还行但温度变化后误差会变大。串口通信两侧的波特率误差超过2%基本就废了所以长距离或者高低温环境最好外接晶体。2. STM32CubeMX配置UART和LPUART1的正确打开方式2.1 时钟树与波特率误差配置前先把这笔账算清楚用STM32CubeMX配置时Clock Configuration页面里要先看USART1和LPUART1挂在哪个时钟树上。USART1在L0系列上一般挂在APB2总线USART2挂在APB1LPUART1挂在APB1。APB1最高能跑到32MHz如果你把系统时钟设成32MHz、APB1不分频那USART2和LPUART1的时钟基准就是32MHz。此时普通UART配115200是完全没问题的误差也极小。但LPUART1多了一个时钟源选择这个选项不在Clock Configuration主界面而是在LPUART1的外设参数页面里或者在RCC的Configuration里。选LSE还是HSI还是系统时钟直接影响你能用的最大波特率。我之前做项目时遇到过这种情况CubeMX生成代码后LPUART1的接收在9600波特率下完全正常改成115200就乱码而且不是偶发乱码是非常规律的字节错误。后来用调试器看BRR寄存器的值才发现LSE时钟下要求BRR小于3HAL库已经做了保护实际波特率根本不是115200。这个坑如果只看配置界面根本发现不了。所以配置之前先算一笔账时钟源频率 / 目标波特率得到的结果必须大于等于3否则LPUART1就是摆设。对应关系大概是LSE 32.768kHz最大9600bpsHSI 16MHz最大1Mbps以上常规波特率都没问题系统时钟比如PLL到32MHz常规波特率随便配2.2 三步配置引脚、NVIC、时钟源一个都不能漏打开STM32CubeMX选好具体型号比如STM32L053R8或者STM32L072进入Pinout Configuration页面第一步是引脚复用配置。在Peripherals里找到USART1Mode选Asynchronous异步收发CubeMX会自动给你分配TX/RX引脚。LPUART1也一样会在单独的LPUART1外设里Mode选Asynchronous就行。如果自动分配的引脚和你硬件板子对不上可以手动点击引脚切换。第二步是参数配置。波特率、数据位8、停止位1、无校验这几个常规项根据需求填。HwFlowCtl如果板子没有硬件流控一定选None。LPUART1要多看一眼Clock Source。第三步是NVIC设置。这个太重要了很多串口不进中断就是卡在这。在USART1的Configuration里找到NVIC Settings选项卡必须勾选USART1 global interrupt优先级按需设置。LPUART1同样要勾选LPUART1 global interrupt。如果不勾选CubeMX生成的代码里不会有HAL_NVIC_EnableIRQ中断自然进不去。有的朋友会问我不在CubeMX里勾NVIC自己手动在代码里写HAL_NVIC_EnableIRQ行不行行但没必要。手动加代码虽然能工作但会破坏CubeMX代码的可维护性下次重新生成代码就丢了。老老实实在图形界面里勾选这才是ST设计好的工作流。2.3 生成代码后别急着写业务逻辑先检查初始化顺序CubeMX生成的是main.c文件里面初始化顺序大致是int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); MX_LPUART1_UART_Init(); }GPIO初始化放在外设初始化之前是有道理的。STM32的引脚复用需要GPIO控制器先准备好如果先初始化外设再初始化GPIO外设的TX/RX引脚悬空输出电平不确定通信会出现奇怪问题。所以不要轻易调整这个顺序。我见过有人把HAL_UART_Receive_IT这个接收启动函数放在while(1)之前这个没问题。但如果他放在MX_USART1_UART_Init之前那就坏事了HAL_UART_Receive_IT先运行往CR1寄存器写了RXNEIE使能位然后紧接着的HAL_UART_Init会重新配置CR1直接把这位置零。也就是说你启动了一次接收但初始化函数把它冲掉了后面永远进不了中断。正确的做法是所有MX_xxx_Init完成之后再调用HAL_UART_Receive_IT启动接收。放在while(1)大循环之前是标准姿势。3. HAL库UART中断机制进不进中断的底层逻辑3.1 HAL_UART_Receive_IT到底做了什么事很多人会用HAL_UART_Receive_IT但不知道它内部做了什么。简单说这个函数启动了“一次”中断接收不是持续接收。看HAL库源码HAL_UART_Receive_IT主要做了三件事保存传进来的接收缓冲区指针和接收长度到huart结构体里设计好传输状态把huart-gState和huart-RxState切换到就绪状态使能CR1寄存器里的RXNEIE接收寄存器非空中断、PEIE奇偶校验错误中断和ERRIE错误中断换句话说调用这个函数之后只要串口收到一个字节硬件就会把RXNE标志位置1然后触发中断。但注意“一次”这个关键词。如果传进去的接收长度是1那么收到一个字节后整个接收流程就完成了。HAL库在接收完成时会自动清掉RXNEIE位。这意味着什么呢如果你只调用了一次HAL_UART_Receive_IT后面就再也不管了那串口只会进入一次中断之后来的数据全都不再触发中断。这是我见过最多的“串口不进中断”的原因不是配置问题是没有重新启动接收。3.2 从中断入口到回调函数的完整链路STM32硬件触发中断后会跳转到中断向量表里对应的中断服务函数。以USART1为例中断服务函数名是USART1_IRQHandlerLPUART1是LPUART1_IRQHandler。CubeMX生成的中断服务函数一般是这样的void USART1_IRQHandler(void) { HAL_UART_IRQHandler(huart1); }HAL_UART_IRQHandler会根据中断标志位分别处理如果是接收中断RXNE置位就读数据寄存器DR里的值存入缓冲区然后递减接收计数。当计数减到0说明这一轮接收完成HAL库会调用回调函数void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { // 你在这里处理接收到的数据 }关键点来了这个回调函数是弱函数weak如果你不在自己的代码里重新实现它那么它什么都不做你的数据就静默丢弃了。很多人以为CubeMX生成了回调函数模板但其实它只生成了一个空壳你需要自己copy一份出来写上自己的逻辑。另一个常见问题是函数实现了但是回调里没有判断是哪个串口的句柄。一个工程里同时用了USART1和LPUART1回调函数是共用的必须通过huart参数判断来源void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { // 处理USART1的数据 } else if (huart-Instance LPUART1) { // 处理LPUART1的数据 } }3.3 为什么中断只进一次就不再进以及ORE错误的干扰先说不进中断的典型代码问题最常见的就是回调里没有再调用一次HAL_UART_Receive_IT。假设你的接收长度是1第一次调用HAL_UART_Receive_IT收到字节A进中断触发回调接收流程完成RXNEIE被清零。如果回调里不重新调用HAL_UART_Receive_IT那么字节B到达时RXNEIE是0中断不触发数据卡在DR寄存器里。看起来就像“串口不进中断”。正确写法是在回调末尾重新调用void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { HAL_UART_Receive_IT(huart1, rx_data, 1); } }还有一个隐蔽的坑就是OREOverrun Error溢出错误。当你接收速度太快MCU还没来得及把DR寄存器里的数据读走下一帧数据就到了硬件就会置上ORE标志。HAL库检测到ORE后会进HAL_UART_ErrorCallback这个回调而不是HAL_UART_RxCpltCallback。如果你两边的串口电平、地线接触不良或者上位机发送间隔极短很容易一直触发ORE导致RxCpltCallback永远不执行。所以排查时不光要看接收回调还要看错误回调void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) { if (huart-ErrorCode HAL_UART_ERROR_ORE) { // 清错误标志尝试恢复接收 __HAL_UART_CLEAR_OREFLAG(huart); HAL_UART_Receive_IT(huart1, rx_data, 1); } }4. 串口不进中断一步步排查的完整清单4.1 先分清是MCU没进中断还是上位机就没发过来排查串口问题最忌讳一上来就怀疑MCU配置。你先得确认数据是不是真的从电脑发出去了并且到了单片机的RX引脚。第一步看USB转串口。现在很多USB转串口芯片是FT232R、CP2104、FT231X这些芯片都需要安装驱动。如果你在电脑设备管理器里看不到COM口或者COM口旁边有个黄色感叹号那数据根本发不到MCU那边和单片机一点关系都没有。第二步做回环测试。把MCU的TX引脚和RX引脚用杜邦线短接在串口助手发一个字符。如果MCU的程序里有发送回显逻辑能收到自己发出去的数据说明串口硬件链路是通的。如果没有回显你用示波器或逻辑分析仪量一下MCU的RX引脚看有没有波形。这一步能帮你砍掉一半排查时间。很多“串口不进中断”的情况其实是上位机串口助手选错了COM口号或者TXD和RXD没交叉连接。普通串口线是TXD接TXD、RXD接RXD不对必须交叉。这是最原始也最容易犯的错误。4.2 在ISR打断点用调试器验证中断到底有没有触发如果确认数据到了MCU引脚下一步就是用调试器确认中断是否触发。在STM32CubeIDE里直接在USART1_IRQHandler函数第一行打断点。运行程序上位机发一个字节看程序是否停在断点上。如果停在了USART1_IRQHandler说明中断链路是通的问题在HAL_UART_IRQHandler后面的处理或者回调里。如果没有停在断点说明中断根本没有被触发问题在NVIC配置或者中断标志位没有置位。配合调试器的寄存器窗口检查两个点第一个是NVIC-ISER寄存器确认对应串口的中断使能位是1。第二个是USART的CR1寄存器确认RXNEIE位是1同时看ISR寄存器里的RXNE标志是否置位。如果RXNE已经置位但中断没触发那基本可以断定是NVIC层面的问题。检查CubeMX的NVIC设置或者看看是不是自己手动改了中断优先级分组。FreeRTOS环境下还有一个常见坑configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY设得太高把串口中断优先级挡在外面了。4.3 最常见的三类原因和修复代码我把实际项目中遇到的“串口不进中断”原因归成三类每类都附上对应的修复方式。第一类回调函数没实现或回调里没有重新启动接收。这种情况在单字节接收模式下最典型。修法就是在回调函数里再次调用HAL_UART_Receive_IT形成持续接收。第二类HAL_UART_Receive_IT的调用位置不对。比如在MX_USART1_UART_Init之前调用了或者放在while(1)循环里反复调用。放在while(1)里反复调用的缺点很隐蔽接收已经完成后你又启动一次新的接收看起来没问题但如果在中断回调处理完之前主循环又跑了一次HAL_UART_Receive_IT可能把正在接收的状态打乱。正确做法在主循环前启动一次后续在回调里续传不要在while(1)里轮询启动。第三类中断服务函数里没有调用HAL_UART_IRQHandler或者你自己用了别的串口驱动方式比如把中断向量表改了。之前有个项目集成了别人写的自定义Bootloader启动文件里的向量表偏移做了手脚结果跳不到USART1_IRQHandler。这个排查起来最费时间建议先检查是不是用了默认启动文件。修复代码示意uint8_t rx_byte 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { process_byte(rx_byte); HAL_UART_Receive_IT(huart1, rx_byte, 1); } else if (huart-Instance LPUART1) { process_lpuart_byte(rx_lpuart_byte); HAL_UART_Receive_IT(hlpuart1, rx_lpuart_byte, 1); } }4.4 LPUART1特有检查LSE、唤醒、时钟源一个都不能漏如果你用的是LPUART1除了上面的通用检查还有三个特有的坑。第一个是LSE是否真的起振。很多低功耗板子为了省电LSE外部晶振电路做得很极端匹配电容太小导致晶振起振困难。LPUART1选LSE做时钟源后如果LSE没起振HAL_UART_Init虽然可能返回正常但时钟树根本没跑起来。用调试器看RCC相关寄存器的LSERDY位或者做一个简单的GPIO翻转测试判断LSE有没有输出。第二个是唤醒源配置。LPUART1在Stop模式下要能唤醒MCU必须在CubeMX里配置Wake-up source。可以选择Start bit起始位唤醒或者Character match字符匹配唤醒。如果配置了Character match还要设置匹配的字符数据。这个配置如果没做你在正常Run模式下串口通信没问题但一旦MCU进了Stop模式LPUART1就完全“睡着了”发多少数据都醒不过来。第三个是时钟源选型导致的波特率错误。前面已经讲过LSE下最高9600HSI下可以115200。实际项目中如果你用LPUART1做高速通信建议选HSI或系统时钟同时接受一点功耗增加。如果既想要低功耗又想要高速率那在STM32L0这颗芯片上做不到只能换更高端的系列。5. 常见问题速查表与避坑心得5.1 问题速查表现象可能原因解决方法串口完全收不到数据回调不触发CubeMX里NVIC没勾选串口中断在NVIC Settings里勾上对应中断重新生成代码第一次能收到数据之后全部失效回调里没有再次调用HAL_UART_Receive_IT在回调末尾重新启动接收形成持续接收数据能收到但是乱码波特率误差过大或LPUART时钟源选错校准时钟确认LSE/HSI选择与波特率匹配进的是错误回调而不是接收回调ORE溢出或帧错误检查串口电平、共地在错误回调里清标志并恢复接收LPUART1最多只能跑到9600时钟源选了LSE硬件上限约10.9kbps改用HSI或系统时钟作为LPUART时钟源低功耗模式下LPUART不工作没有配置唤醒源CubeMX里配置Start bit或Character match唤醒调试器单步执行时正常脱机后不稳定断点掩盖了时序问题或初始化顺序有误去掉断点验证检查MX_Init代码顺序5.2 踩过坑之后的四个心得第一先确认“进没进中断”再改代码。很多人一遇到串口没反应就怀疑波特率、怀疑GPIO复用、怀疑时钟把CubeMX工程翻个底朝天但始终没确认中断服务函数有没有被执行。直接在ISR第一行打断点5秒钟就能确定问题方向。第二回调里不要做耗时操作。我见过有人直接在HAL_UART_RxCpltCallback里做字符串拼接、格式化输出、甚至写Flash这会导致回调函数执行时间过长下一帧数据到达时ORE溢出然后整个接收就崩了。正确的做法是回调里只存一个标志或把数据丢进环形缓冲区真正处理逻辑放到主循环里。第三LPUART1的时钟源要自己盯紧。CubeMX的默认配置不一定适合你的项目尤其是LPUART1这种有独立时钟选择的外设。生成代码后打开main.c找到MX_LPUART1_UART_Init里面的时钟配置确认是不是你想要的时钟源。第四一个工程里同时用UART和LPUART1回调函数要注意区分句柄。共用HAL_UART_RxCpltCallback时必须先判断huart-Instance再分别处理。我在这个坑里栽过一次两个串口的接收都启动了但回调里只处理了USART1LPUART1的数据全丢。最后再分享一个我个人的调试习惯每次调试串口我都会先打开调试器的外设寄存器窗口或者内存表达式窗口盯着CR1的RXNEIE位和NVIC的ISER寄存器。这个习惯帮我快速发现了很多“看似玄学”的串口问题其实九成都是使能位或者分频配置的事。STM32L0的串口本身不难难的是把每个环节的因果关系理清楚。希望这篇东西能帮你少走弯路。
返回列表