首页 / 资讯中心 / 文章详情

STM32L051低功耗串口唤醒实战:LPUART在STOP模式下保持监听

STM32L051低功耗串口唤醒实战:LPUART在STOP模式下保持监听 ★ FEATURED ARTICLE
低功耗项目里最难处理的就是“设备睡了还要能被叫醒”这件事。我手里这套电池供电的环境采集节点主控用的就是STM32L051整机待机电流需要压到微安级同时又要求外部主机能随时通过串口远程唤醒设备、修改采集参数。一开始我的方案很简单粗暴串口直接全关进STOP模式结果发现上位机那边怎么发数据都叫不醒它。后来翻参考手册才发现L051上自带的LPUART这套外设设计初衷就是干这个活的——它可以在STOP模式下依靠LSE这种超低功耗时钟保持接收状态并在收到唤醒帧时把内核从停止状态拉回来。这篇就把我实际用LPUART打通“低功耗-唤醒-收数-继续休眠”这套链路的过程完整拆开写一遍。这个项目里踩的坑不少最典型的就是波特率配置、唤醒标志清理和时钟恢复这三个环节任何一个没处理到位设备要么唤醒不了要么唤醒了但串口收的全是乱码。如果你也在做电池供电类设备或者手上正好有STM32L0系列芯片想让串口在低功耗模式下还能保持“在线接客”那这篇文章应该能帮你少走不少弯路。1. 项目概述电池供电设备如何在停摆状态下被叫醒先说说项目背景方便你对号入座。这套采集节点用在户外无电源环境内部一节锂电池供电要求整机平均功耗做到非常低才能保证一年以上的续航。硬件上主控是STM32L051C8T6外挂一个温湿度传感器和一个气压传感器正常情况下MCU每10秒醒来一次采集完数据就继续睡。但现场运维人员偶尔需要远程改一下采集间隔或者校准系数所以系统必须留一条“随时能叫醒MCU”的通信链路。最开始我想用普通USART来实现这个功能但STM32L0系列进入STOP模式以后普通外设的时钟基本都停了USART自然也就完全断连。于是我把目标转向了LPUART也就是低功耗通用异步收发器。这个东西和普通USART最大的区别在于它可以在内核和大部分外设都停止供电的情况下靠自身维持一小块低压域的逻辑电路运行同时配合LSE低速外部晶振继续监听串口总线上的数据。适合读这篇文章的人我觉得分三类第一类是正在用STM32L0系列做电池设备的嵌入式工程师第二类是虽然用的不是L0但想了解低功耗串口唤醒方案的人第三类是准备入门低功耗设计、想弄明白STOP模式和唤醒机制的新手。不管你是哪一类下面这套配置思路和代码流程应该都能给你提供一个完整的参考框架。2. STM32L051低功耗模式与LPUART特性拆解2.1 先搞清楚四种低功耗模式该怎么选STM32L051的低功耗模式比F1那种老架构要细得多从Sleep到LPRun到Stop再到Standby每一档对电流的压制力度不同代价是可用外设和唤醒源也逐级减少。我在项目里把这几种模式都实测过一遍电流数据差异非常大选错模式整个方案的功耗预算直接就崩了。模式典型电流可用外设唤醒源LPUART是否可用Sleep约10~30 uA大部分外设保持任意中断可用但功耗偏高Low-power Run约5~15 uA内核低频运行复位/中断可用Low-power Sleep约3~10 uA低功耗外设部分中断可用Stop约1 uA备份域/低功耗外设EXTI/RTC/LPUART等可用需配置UESMStandby约0.3 uA仅备份域复位/RTC唤醒引脚不可用项目里我的目标是把待机电流压到3 uA以内所以Sleep模式直接排除它虽然省事但电流太高。Low-power Run也能跑到十几个微安但对于10秒唤醒一次的应用来说还是浪费。Standby虽然电流最低但LPUART完全不工作唤醒后相当于重启一遍设备这在远程维护场景里很尴尬。最终选定的就是Stop模式这也是本文后面所有代码和配置的基础。Stop模式的本质是系统主时钟MSI/HSI和大部分外设时钟全部停摆但SRAM中的数据保留部分低功耗外设可以通过特定配置继续工作LPUART就是其中之一。这里有一个容易误会的点很多人以为Stop模式下所有外设都断了其实L0系列专门留了一小片“独立电源域”给LPUART、RTC、I2C这些低功耗外设使用。你只要把对应的使能位打开它们就能在核心睡觉的时候继续干活。2.2 LPUART与普通USART到底差在哪LPUART这个名字虽然看起来只是“低功耗版USART”但它在硬件设计上和普通USART有不少本质区别。最核心的一点是它的时钟设计不是随便套用PCLK而是可以独立选择时钟源包括LSE、HSI甚至SYSCLK分频这种灵活性让它在Stop模式下也能获得一个持续运行的时钟。第二个区别是过采样结构不同。普通USART为了支持高波特率内部有比较复杂的过采样和分频逻辑而LPUART的设计目标就是跑低波特率、低频率通信它的采样结构更精简但这也导致它在一些场合的寄存器配置算法和USART完全不一样。第三个也是最重要的区别就是LPUART支持在Stop模式下的唤醒功能。它内部有一个唤醒事件检测电路当事先设置的唤醒条件满足时比如收到起始位或地址匹配帧可以产生一个唤醒信号通过EXTI通道把MCU从Stop模式拉回运行模式。这就是“设备睡了但串口还醒着”的硬件基础。不过LPUART也有明显的短板。它不支持硬件流控RTS/CTS没有同步模式寄存器数量也比USART少。在项目里如果用到流控那还得额外用普通IO模拟或者换别的方式好在我这个应用场景是点对点短距离通信这些缺陷完全不构成阻碍。2.3 停止模式下LPUART保持运行的原理LPUART能在Stop模式下继续监听总线依赖三个条件第一LPUART的时钟源必须选一个在Stop模式下还继续运行的时钟最常用的是LSE 32.768 kHz外部晶振这个晶振本身就是给RTC用的低功耗特性极好第二LPUART的UESM位必须置1这个位全称是“UART Enable in Stop Mode”不置它LPUART在Stop模式下直接断电第三唤醒事件需要连接到EXTI中断线上这样LPUART检测到唤醒条件后才能一路把信号传到NVIC和内核。这里有个容易踩的坑LSE如果没起振LPUART在Stop模式下就是一具尸体。我之前调试时犯过这个错代码里把LPUART时钟源指向LSE但没检查LSE是否稳定就急着进Stop模式结果设备一睡不醒。正确做法是进入停止模式之前确认LSE已经完全稳定并且对应RCC寄存器配置正确。另外Stop模式下如果选了HSI作为LPUART时钟源虽然也能工作在低功耗模式但HSI本身是需要额外功耗维持的典型电流会比LSE高不少。我自己实测下来用LSE比用HSI整体电流能低2~3 uA左右别小看这几微安在电池寿命核算里可能就是几个月的差异。3. 核心配置把唤醒链路逐级打通3.1 时钟源选择决定低功耗的下限给LPUART选时钟源是整个配置的第一步也是决定功耗水平的关键。STM32L051的LPUART1时钟源选择寄存器在RCC的CCIPR里可以选PCLK、SYSCLK、HSI或者LSE。在Stop模式下PCLK和SYSCLK都已经停了实际可用的就是HSI和LSE两种。HSI的优势是频率高16 MHz下可以支持更高的波特率范围但代价是功耗大而且Stop模式下要继续给HSI供电那待机电流肯定是压不下去的。LSE的优势是功耗极低32.768 kHz的频率虽然低但正好能够覆盖1200~9600 bps这个最常用的远程维护波特率范围。我的建议是如果你的应用波特率在9600及以下无脑选LSE如果一定要跑115200这种高速率那LPUART基本就不太合适了得回头考虑普通USART加定时唤醒的折中方案。配置代码上很简单主要就是设置RCC的CCIPR寄存器/* 选择LSE作为LPUART1时钟源 */ RCC-CCIPR ~RCC_CCIPR_LPUART1SEL; RCC-CCIPR | RCC_CCIPR_LPUART1SEL_1; /* 0b11 - LSE */3.2 波特率寄存器BRR的计算32768 Hz怎么算9600LPUART的波特率计算方式和普通USART差异很大这也是很多从F1系列转过来的人最容易翻车的地方。普通USART是直接用PCLK除以16 × USARTDIV而LPUART因为内部简化了过采样逻辑计算方式变成了BRR 256 × fck / baud。以LSE 32.768 kHz、目标波特率9600为例BRR 256 × 32768 / 9600 873.81取整后写入BRR寄存器的值是874。实际出来的波特率是实际波特率 256 × 32768 / 874 ≈ 9598 bps计算误差大概在0.02%左右完全在UART通信的容错范围内收发双方只要波特率误差不超过2%基本不会出乱码。同理你可以算一下4800、2400、1200这几个常用频率都在LSE的支持范围内。这里要特别提醒LPUART的BRR寄存器位宽有限MANTISSA只有13位最大值是8191。如果按公式算出来的BRR超过8191说明这个波特率在当前时钟下配置不了。比如LSE下想跑9600是没问题的但如果想跑300 bpsBRR27962已经超了那就只能考虑用HSI或者放弃这个低波特率需求。3.3 引脚映射与复用功能不能配错LPUART1在STM32L051上默认的引脚映射是PC0RX和PC1TX复用功能编号是AF5。不同封装可能会有差异我用的LQFP48封装就是这两个脚。很多人把初始化和GPIO配置全弄完了结果数据收发不对最后排查半天发现是AF配置错了。GPIO这块要做的几件事开GPIO时钟、把PC0和PC1设为复用推挽/输入模式、指定AF5、同时注意RX引脚不能悬空。这里我建议把RX线上拉一个电阻到VDD原因是防止外部设备没接的时候引脚电平随机抖动造成误唤醒。GPIO_InitTypeDef GPIO_InitStruct; RCC-IOPENR | RCC_IOPENR_GPIOCEN; /* PC0 RX, PC1 TX */ GPIO_InitStruct.Pin GPIO_PIN_0 | GPIO_PIN_1; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_PULLUP; /* RX拉高, 防止悬空误唤醒 */ GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; GPIO_InitStruct.Alternate GPIO_AF5_LPUART1; HAL_GPIO_Init(GPIOC, GPIO_InitStruct);3.4 唤醒机制UESM位与EXTI28的联动LPUART在Stop模式下能保持活动关键就在于CR1寄存器里的UESM位置1。我把这个位理解为“低功耗保留开关”没打开它LPUART在Stop模式下就是一个普通外设直接断电罢工。同时还要在CR3寄存器中使能唤醒中断WUFIE这样当LPUART收到符合条件的唤醒帧时才能产生唤醒事件。唤醒事件最终要走EXTI通道进入NVIC。STM32L051的LPUART1对应的EXTI线是28需要在EXTI寄存器中把这条中断线使能并且配置成合适的触发边沿。这里要注意L0系列的EXTI配置和F1系列不太一样它是用寄存器配置单边沿或双边沿触发没有F1那种上升沿下降沿分开的写法那么直观。/* 使能LPUART在Stop模式下保持工作 */ LPUART1-CR1 | USART_CR1_UESM; /* 使能唤醒中断 */ LPUART1-CR3 | USART_CR3_WUFIE; /* 配置EXTI line 28 */ EXTI-IMR | (1U 28); EXTI-RTSR | (1U 28); /* 上升沿触发 */ NVIC_EnableIRQ(LPUART1_IRQn);4. 完整实操流程与代码示例4.1 初始化LPUART的完整代码下面这套初始化流程是我项目里实际用的寄存器版本为主HAL用户也能看懂。先开LPUART时钟再选择时钟源配置波特率使能接收和唤醒相关位最后配置GPIO和EXTI。void LPUART1_Init(void) { /* 1. 使能LPUART1时钟 */ RCC-APB1ENR | RCC_APB1ENR_LPUART1EN; /* 2. 选择LSE作为时钟源 */ RCC-CCIPR ~RCC_CCIPR_LPUART1SEL; RCC-CCIPR | RCC_CCIPR_LPUART1SEL_1; /* 3. 等待LSE稳定, 实际项目中LSE通常也是RTC时钟源, 这里确保其已就绪 */ while ((RCC-CSR RCC_CSR_LSERDY) 0); /* 4. 配置波特率 9600 */ LPUART1-BRR 874; /* 256 * 32768 / 9600 ≈ 874 */ /* 5. 配置数据格式 8N1 */ LPUART1-CR1 0; LPUART1-CR2 0; /* 6. 使能接收、接收中断、Stop模式使能 */ LPUART1-CR1 | USART_CR1_RE | USART_CR1_RXNEIE | USART_CR1_UESM; /* 7. 使能唤醒中断 */ LPUART1-CR3 | USART_CR3_WUFIE; /* 8. 最后使能LPUART */ LPUART1-CR1 | USART_CR1_UE; }注意这个顺序BRR要在UE使能之前配置好否则波特率没生效就上电了可能收到的是无效数据。另外UESM位我建议直接在CR1一起配置这样后续进Stop模式不用再额外设置。4.2 进入Stop模式前要做哪些准备动作进入Stop模式不是简单执行一条WFI指令就行。在调这条指令之前要保证外部通信方已经知道“设备要睡了接下来随时可能发唤醒帧”不然你刚睡下对方发数据有可能因为系统还没完全进入Stop模式而错过唤醒。我在项目里是这样做的收到上位机的“休眠”指令后先回一帧ACK让上位机知道“我要睡了”延时10 ms左右让对方把总线让出来然后再执行进Stop模式的流程。这10 ms的延时是为了确保TX的数据已经完整发送出去避免最后半帧数据卡在移位寄存器里。真正进入Stop模式的代码如下void EnterStopMode(void) { /* 清除唤醒标志 */ PWR-CR | PWR_CR_CWUF; /* 选择STOP模式, 不是Standby */ PWR-CR ~PWR_CR_PDDS; /* 低功耗停止模式 */ PWR-CR | PWR_CR_LPSDSR; /* 关中断再WFI, 防止进入时有中断抢占 */ __disable_irq(); __WFI(); __enable_irq(); /* 唤醒后执行系统时钟恢复 */ SystemClock_Config(); }4.3 唤醒后的系统恢复流程设备被LPUART唤醒后第一件要做的事不是立刻接收数据而是把系统时钟重新配置回正常运行频率。因为Stop模式下MSI或者外部晶振都停了唤醒后默认可能还是低频或者未就绪状态这时候如果外设还在跑很容易出现各种奇怪问题。我习惯的做法是在LPUART中断服务程序里先做两件事清EXTI标志、清WUF标志。这两个标志位不清干净会导致中断反复触发设备看起来就是“醒了但一直卡在中断里出不来”。void LPUART1_IRQHandler(void) { /* 1. 清除EXTI唤醒事件标志 */ if (EXTI-PR (1U 28)) { EXTI-PR (1U 28); } /* 2. 清除LPUART唤醒标志 */ if (LPUART1-ISR USART_ISR_WUF) { LPUART1-ICR | USART_ICR_WUCF; } /* 3. 接收数据 */ if (LPUART1-ISR USART_ISR_RXNE) { uint8_t data (uint8_t)(LPUART1-RDR 0xFF); /* 把数据存入环形缓冲区 */ RingBuffer_Push(data); } }主循环里醒过来之后我先把系统时钟恢复然后从环形缓冲区里解析数据。这里要特别强调一下唤醒帧和正式数据帧之间建议留一点时间间隔。原因是唤醒瞬间内核从Stop模式恢复到运行模式需要时间如果对方在唤醒帧之后紧跟一大包数据你的接收缓冲区可能来不及读走RXNE会堆积最终导致溢出。5. 实测数据与收发验证5.1 功耗测量记录我把这套方案在最小系统板上实测过测试条件是3.3 V供电LSE用32.768 kHz晶振LPUART持续接收使能不进RTC唤醒。测量仪器用的是六位半万用表采样率设为每秒1次取其稳定后的平均值。工作状态实测电流说明正常运行8 MHz MSI约2.8 mA打开ADC和传感器Sleep模式约12.6 uA内核停止外设时钟未关Stop模式仅LPUARTEXTI使能约2.3 uALPUART用LSE持续监听Stop模式LPUARTRTC使能约3.1 uA多一个RTC秒中断从数据来看LPUART在Stop模式下的功耗开销大约在2 uA左右配合RTC总共也就3 uA出头对于一节2000 mAh的锂电池来说理论上单纯停在这里就能放个几十年的当然实际还要算上定期唤醒采集和通信的功耗整体预算完全够用。5.2 收发功能实测结果为了验证唤醒和通信可靠性我做了两组测试。第一组是“边沿唤醒测试”上位机连发5个字节的0x55第一个字节的起始位负责唤醒MCU后面4个字节正常接收实测4个字节全部接收正确没有丢字节。第二组是“地址匹配唤醒测试”把LPUART配置为地址匹配模式只有收到预设地址字节时才唤醒MCU非地址帧直接忽略。这里提醒一个实测中发现的很微妙的点如果你用的是“起始位唤醒”那实际上第一个字节的起始位只是把系统叫醒这个字节本身能不能完整接收取决于时钟恢复够不够快。我实测发现唤醒后的第一个字节经常能收到但内容不对因为波特率恢复和端口状态还没稳定下来。所以实际应用里我规约设计了2个字节的引导头第一个字节专门用来唤醒第二个字节起才是有用数据。5.3 测量功耗时容易忽略的坑功耗测试这事儿看着简单实际上到处都是坑。首先板上如果有电源指示灯一个LED就能吃掉毫安级电流测试前必须摘掉或者用跳线断开。常见的调试串口芯片如CH340、CP2102即使不连接USB静态电流也不小做功耗测试时要考虑把整块调试电路隔离掉。还有LDO稳压器本身的静态功耗也要看是哪种普通AMS1117是几毫安的级别压根不能用在电池设备上要用低功耗LDO。测量工具方面如果只是看个大概用万用表电流档串联进电源回路就行但要注意万用表在微安档上的内阻可能导致你的设备输入电压偏低进不了正常工作状态。如果条件允许用示波器电流探头或者高精度电流钳会更直观能看到休眠和唤醒瞬间的电流变化波形不过价格比较贵一般实验室没这个条件。6. 常见问题与排查技巧6.1 设备进入Stop模式后完全无法唤醒这个现象我调试时遇到过查到最后是UESM位没置位。如果你的代码是先配置好CR1再进Stop模式但中途不小心改了CR1或重新初始化了LPUART这个位可能会被覆盖掉。还有个隐蔽的原因LPUART的唤醒信号需要经过EXTI通道如果你使用的是HAL库需要确认该EXTI线对应的是否正确L051上是线28但部分参考代码会写31或者别的一定要查对应型号的手册。现象可能原因排查方向唤不醒UESM位没置1查LPUART1-CR1唤不醒EXTI线未使能查EXTI-IMR bit28唤不醒LSE未稳定就进Stop查RCC-CSR中的LSERDY唤不醒外部发送设备TX呈高阻或浮空查对端TX引脚配置唤醒后又立即睡死中断标志没清干净查WUF和EXTI PR位清理6.2 唤醒后收到的数据是乱码数据乱码的首要嫌疑是波特率不对。很多人会把LPUART的BRR计算方式和普通USART搞混直接把USART那套公式套过来那结果基本必乱。前面算过根据取整的874理论误差不到0.02%但如果有人不小心写成了840或者900误差就大到完全无法通信了。其次要检查时钟源选择。如果你的LPUART时钟源实际用的是HSI但代码里按LSE计算BRR那实际波特率会差将近500倍收到的全是乱码。所以排查乱码问题时第一步就是把RCC-CCIPR里的实际值读出来确认是否和你预期一致。6.3 频繁误唤醒设备根本没睡多久误唤醒多半是RX引脚电平抖动造成的。LPUART的“起始位唤醒”说白了就是检测到一根下降沿如果外部线路上的噪声干扰比较大很可能隔几秒就抖出一个下降沿设备就会频繁醒来功耗直接炸掉。解决措施是三层硬件上RX引脚加10 kΩ上拉电阻软件上配置EXTI时考虑选择合适的触发边沿必要时用地址匹配唤醒功能只有收到特定地址字节才唤醒抗干扰能力强很多最后就是在进Stop之前把RX引脚设置成上拉输入模式别让它处于浮空状态。6.4 唤醒后的正常工作受影响设备唤醒后第一次发送数据失败这个现象我查过很久最终定位到系统时钟恢复顺序上。Stop模式唤醒后芯片的时钟树不会自动回到运行模式要在软件里重新配置MSI或者外部晶振等待时钟稳定后再收发数据。如果你用的是HAL库SystemClock_Config这个函数就是干这个的但不要在主循环里直接调用最好是做一个标志位主循环检测到唤醒标志后先恢复时钟再处理串口数据。另外唤醒后LPUART自身的接收中断优先级最好调到比较高的层级因为唤醒瞬间系统还有很多初始化工作要做如果低优先级中断被拖住接收缓冲区满了就会溢出丢数据。6.5 关于低功耗串口唤醒实践经验总结这套项目做下来我体会到低功耗串口通信的核心其实不是串口本身而是系统级的资源协调。时钟源选型、唤醒源配置、中断优先级、系统时钟恢复任何一个环节都牵一发动全身。LPUART的硬件设计已经把这个任务简化了很多只要把LSE、UESM、EXTI这条链路打通剩下的就是软件层面的时序规划。回到开头那个问题电池供电设备如何在停摆状态下被叫醒我的答案是不是靠定时器掐着表等而是靠LPUART这种能在睡眠状态下持续监听总线的外设。从硬件到固件这一条唤醒链路让设备真正做到了“平时装死喊一声就醒”。最后再分享一个小经验低功耗功能千万不要等到最后再集成最好项目第一天就把进Sleep和唤醒的demo跑通不然后期跟业务逻辑绑在一起排查问题的成本会成倍上涨。
阅读完成 · 觉得有帮助?
咨询建站