
简介一份面向嵌入式低功耗开发的STM32L431完整示例工程围绕Stop模式下的休眠与唤醒展开重点解决RTC定时闹钟与LPUART串口数据起始位两种唤醒源的配置、使能及唤醒后系统恢复问题适合正在规划IoT节点、传感器采集或电池供电设备的开发者参考。整个压缩包包含1377个文件大小约31.23MB以C源码文件692个.c、头文件306个.h和汇编文件142个.s为主同时提供IAR/Keil工程配置、链接脚本、文本说明以及编译生成的hex/axf等文件目录结构与真实项目工程接近便于直接导入IDE分析或烧录验证。据页面统计已有5097人浏览学习说明该示例在低功耗唤醒场景中的实用性获得了不少开发者认可。具体代码展示了RCC寄存器级电源模式切换、RTC闹钟中断唤醒、LPUART接收唤醒使能以及唤醒后时钟树与外设状态的恢复处理作者声明经测试休眠与唤醒流程正常读者可据此快速搭建自己的低功耗工程或对比排查唤醒失败问题。1. STM32L431 stop 模式低功耗LPUARTRTC 双唤醒为什么我先测休眠再谈省电做低功耗设备时最怕的不是功耗压不下去是代码一跑__WFI()直接睡死外部信号来了也叫不醒。STM32L431 的 stop 模式其实分 Stop0/Stop1/Stop2 三档RTC 和 LPUART 都能在 stop 下保持工作但两者唤醒后的复位状态、时钟恢复和外设重初始化顺序完全不同。这次拿到一份实测可复现的 STM32L431 stop 模式工程通过 LPUART 和 RTC 双通道唤醒实测休眠正常非常适合正在做电池供电表计、传感器节点或者需要串口待机唤醒的工程师参考。下面把配置思路、代码填空位置和几个容易翻车的坑拆开讲。2. 进入 stop 模式之前时钟树、电源域与唤醒源选型2.1 SLEEPDEEP 与 WFIstop 模式和 sleep 模式的分水岭STM32L431 的低功耗模式从浅到深依次是 sleep、stop、standby。stop 模式与 sleep 的本质区别在于sleep 只是 CPU 停止执行内核时钟可能还开着外设可以继续工作而 stop 模式会把系统主时钟关掉内核电压域进入低功耗状态但 SRAM 和寄存器内容全部保留。进入 stop 前必须先设置PWR_CR的低功耗模式配置位再执行WFI或WFE指令。很多人只用 HAL 库的HAL_PWR_EnterSTOPMode()却没搞清楚它内部实际做了两步先置位SLEEPDEEP再执行__WFI()。常见做法是先调HAL_RCC_EnableLSI()或者确认 LSE 是否稳定因为 RTC 唤醒需要独立时钟源。STOP 模式下可以选择 LSI、LSE 或者 HSI 作为 RTC 时钟但 LSE 功耗最低32.768kHz 晶振对 RTC 闹钟精度影响也最小。数据手册里给 LSE 的功耗一般在微安级别比 HSI 整条时钟链路的消耗低好几个量级。所以我在实际项目中几乎只用 LSE 给 RTC 做时钟源既保证时间精度又不拖累整体待机电流。关于 stop 模式档位STM32L431 支持 Stop0、Stop1、Stop2。Stop0 是恢复最快的一档唤醒延迟在微秒级但功耗最高Stop2 是功耗最低的一档典型待机电流能做到几微安但唤醒时间更长。LPUART 在 Stop0 和 Stop1 下都能唤醒Stop2 下 LPUART 不具备唤醒能力。RTC 在 Stop0/Stop1/Stop2 下都能唤醒。选型时先看唤醒源如果只靠 RTC 定时唤醒直接上 Stop2要是既要串口唤醒又要低功耗就得在 Stop1 上妥协因为 Stop2 不支持 LPUART 唤醒很多人第一次做就踩在这个档位上。2.2 LPUART 能够在 stop 模式下唤醒的条件LPUART 和普通 USART 最大的区别在于它在低功耗模式下依然能检测起始位。但这不是白来的需要同时满足三个条件第一LPUART 的时钟源必须配置为 LSE 或者 LSI因为 Stop 模式下系统时钟已经关闭LPUART 必须有自己的独立时钟第二LPUART 的LPUART_CR3寄存器里要开启WUFIE唤醒中断使能并且设置WUS唤醒源选择为起始位检测或者指定字符第三对应的 EXTI 线要配置为上升沿触发因为 LPUART 唤醒事件会映射到 EXTI 线 28。我一般会把 LPUART 的波特率控制在 9600 或者更低的 2400原因有两个一是低功耗时钟源通常是 LSE 的 32.768kHz分频后误差控制更稳定二是唤醒检测是在休眠状态下进行的串口速率太快时起始位宽度小于时钟分辨率容易漏检。这套参数在模块实测中很稳上位机发一帧数据板子能在 3 个 bit 时间内醒来接收整帧。注意一点LPUART 唤醒后接收到的第一个字节可能不完整。因为觉醒过程有一定延迟起始位之前的部分往往已经过去了。我的做法是在协议层做超时重传唤醒后的第一帧只作为敲门信号不承载业务数据。这样比纠结硬件上如何补收漏掉的字节要省事得多。2.3 RTC 闹钟和周期唤醒定时器最小粒度和中断入口RTC 唤醒有两种常用途径闹钟 A 和唤醒定时器。闹钟 A 可以精确到秒甚至亚秒级配置格式为时分秒和日期唤醒定时器则是一个递减计数器时钟源可选 RTC 时钟的 2 的 N 次方分频。对周期性采集场景唤醒定时器更直接不用每次都对闹钟寄存器做一遍写操作。它的值从RTC-WPR解除写保护后通过RTC_WRITE_REG(RTC-WUTR, value)装载。RTC 唤醒定时器的最小计数单位取决于分频配置如果选择 1Hz 的时钟单位就是秒如果选择 16Hz最小单位就是 62.5ms。实际做传感器数据采集时我用 16Hz 分频设定值对应几百毫秒的间隔比单纯用秒级的闹钟灵活得多。RTC 中断入口方面闹钟 A 对应RTC_Alarm_IRQHandler唤醒定时器对应RTC_WKUP_IRQHandler。两个中断里面首先要清除标志位否则会在唤醒后第一时间再次进入中断造成死循环或者频繁唤醒。常见的坑是有人直接在中断里调用 HAL 库函数但 HAL 库函数对寄存器写保护的处理顺序是固定的如果在调用HAL_RTC_AlarmIRQHandler之前没有解除写保护中断标志会一直挂住。3. 双唤醒代码实现初始化顺序与中断服务程序写法3.1 初始化顺序时钟、LPUART、RTC 的先后顺序先说结论先配置 LSE 并等待稳定再初始化 RTC 和 LPUART最后才能设置 EXTI 唤醒线。这个顺序颠倒的话LPUART 在没有 LSE 的情况下会用 MSI 作为默认时钟一旦进入 stop 模式串口直接失效。void SystemClock_Config_LowPower(void) { // 使能 LSE 振荡器用于 RTC 和 LPUART 的低功耗时钟源 RCC_OscInitTypeDef RCC_OscInitStruct {0}; RCC_OscInitStruct.OscillatorType RCC_OSCILLATORTYPE_LSE; RCC_OscInitStruct.LSEState RCC_LSE_ON; RCC_OscInitStruct.PLL.PLLState RCC_PLL_NONE; if (HAL_RCC_OscConfig(RCC_OscInitStruct) ! HAL_OK) { Error_Handler(); } // LSE 就绪后开始等待稳定一般需要几百毫秒实际以 LSERDY 标志为准 }这段代码里最容易忽略的是PLL.PLLState RCC_PLL_NONE如果不显式关闭 PLL系统时钟链路会带有 PLL 配置残留进入 stop 模式后一些域还在耗电。等待 LSE 稳定不能只靠HAL_RCC_OscConfig的返回值建议单独加一层循环读取RCC-CR RCC_CR_LSERDY确保晶振起振完成后再往下走。LSE 起振慢尤其是环境温度低的时候几十毫秒到几百毫秒都可能不加等待直接操作 RTC 会偶发出错这也是很多休眠后时间不对问题的根源。3.2 进入 stop 模式的完整代码块与参数说明void Enter_Stop_Mode(void) { // 关闭不必要的外设时钟减少 stop 模式下的漏电路径 __HAL_RCC_GPIOB_CLK_DISABLE(); __HAL_RCC_GPIOA_CLK_DISABLE(); // 保留 LPUART 对应的引脚时钟 // 配置 LPUART 唤醒中断在停止模式下使能 // 唤醒源选择 WUS_STARTBIT即任意起始位都能唤醒 LPUART_WakeUpConfig(hlpuart1, LPUART_WAKEUP_STARTBIT); // 使能 LPUART 唤醒中断和接收中断 LPUART_InterruptConfig(hlpuart1, LPUART_IT_WUF | LPUART_IT_RXNE, ENABLE); // 进入 Stop1 模式voltage range 1低功耗模式选择 Stop1 HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI, PWR_STOP_MODE_STOP1); // 系统唤醒后重新配置时钟因为 stop 模式退出后时钟树已被复位 SystemClock_Config_LowPower(); }参数说明中PWR_LOWPOWERREGULATOR_ON表示主稳压器在 stop 期间关闭或降功耗实际对应低功耗稳压器模式PWR_STOPENTRY_WFI表示使用 WFI 指令进入中断唤醒后立即从下一行继续执行PWR_STOP_MODE_STOP1选择 Stop1 档位。三个参数是配套的如果改成PWR_STOP_MODE_STOP2请先删掉 LPUART 唤醒依赖因为 Stop2 下 LPUART 是没有时钟的。唤醒后第一件事永远是重新配置系统时钟不要急着操作外设。时钟树在 stop 退出时被硬件复位到 MSI不及时恢复会导致串口波特率错乱和 RTC 读取失败。这个SystemClock_Config_LowPower()和上电初始化时的时钟配置可以共用同一个函数但要确保里面有HAL_RCC_OscConfig先于外设初始化的逻辑。针对 WFI 与 WFE 的选择这里有一个值得注意的细节WFI 会在中断挂起时立即唤醒WFE 则是通过事件机制唤醒。如果内核里还有别的中断源在挂起WFI 可能在进入后立刻退出导致 stop 模式白进。我习惯在进入 stop 前加一条__disable_irq()然后再 WFI醒来后再开中断把嵌套向量中断控制器的干扰降到最低。不过这条不是必须的裸机简单场景直接用 HAL 默认即可。3.3 唤醒后的恢复流程识别唤醒源并重新初始化void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc) { // RTC 闹钟唤醒处理这里只清除标志位不做重操作 __HAL_RTC_ALARM_EXTI_CLEAR_FLAG(); // 记录唤醒源供主循环判断 wakeup_source WAKEUP_SOURCE_RTC; // 如果启用了周期唤醒定时器读取并清除 WUTF 标志 __HAL_RTC_WAKEUPTIMER_EXTI_CLEAR_FLAG(); } void LPUART_WakeUpCallback(void) { // LPUART 起始位唤醒先关闭唤醒中断等待接收完成 LPUART_InterruptConfig(hlpuart1, LPUART_IT_WUF, DISABLE); // 记录唤醒源 wakeup_source WAKEUP_SOURCE_LPUART; }这段代码建议放在中断服务程序里调用HAL_RTC_AlarmAEventCallback是弱函数可以被用户覆盖。没必要在主循环里轮询唤醒源因为中断标记可能在轮询前已经被覆盖。RTC 的中断处理函数里同时清理闹钟和唤醒定时器的 EXTI 标志是为了避免两个唤醒源同时触发时只处理一个、另一个遗留导致系统反复中断。恢复流程中还有一个容易漏的点LPUART 被唤醒后想要继续正常收发需要把 LPUART 的接收使能重新打开。stop 模式唤醒后部分外设寄存器被复位到默认值LPUART 的RE位可能被清零。我的做法是在唤醒后的主流程里统一执行HAL_LPUART_Init()然后重新设置波特率分频值比逐位恢复寄存器要省心。4. 避坑stop 模式低功耗调试中的五个踩坑记录4.1 坑一电流测量值偏高比数据手册典型值大一个数量级现象进入 stop 模式后用万用表串联供电测量电流显示 200uA 到 1mA与手册标称的几微安差距巨大。原因最常见的是 GPIO 悬空。ST 芯片在 stop 模式下悬空引脚会通过内部上下拉电阻或者输入缓冲产生额外漏电。另一个高频原因是 PLL 没有真正关闭或者调试器 SWD 接口仍然占用时钟链路。解决进入 stop 前把用不到的 GPIO 全部配置为模拟输入模式或者输出低电平。__HAL_RCC_GPIOA_CLK_ENABLE()之后跑一遍GPIO_InitStruct.Mode GPIO_MODE_ANALOG把所有引脚洗一遍只保留 LPUART 的 RX/TX 和 RTC 对应的引脚。SWD 在调试完闭后务必备注禁用至少保证nSWD引脚没有被拉高。4.2 坑二LPUART 唤醒后收到的第一帧乱码或者丢字节现象发送字符串WAKEUP给休眠中的设备设备确实被唤醒了但收到的数据总是少前两个字符或者显示出乱码。原因LPUART 唤醒检测起始位时硬件需要几个时钟周期来完成时钟同步期间到达的数据位无法被正确采样。这不是波特率配置错误也不是电路噪声问题而是唤醒时间导致的固有丢码。解决协议设计上让第一帧固定为 0xAA 或者 0x55 之类的单字节设备唤醒后清空接收缓冲等待第二帧真实数据。我在工程中就是这样做的上位机先发一个字节 0xAA 作为敲门随后间隔 50ms 发业务帧。实测 9600 波特率下稳定没有丢字节。4.3 坑三RTC 闹钟中断反复触发设备永远睡不过去现象设备进入 stop 模式后立刻被唤醒看门狗没动作但电流波形上看到设备频繁醒来根本无法进入真正的休眠。原因RTC 闹钟中断标志没有清除唤醒来后中断服务程序读取标志但下一次__WFI()执行时残留的挂起中断信号再次触发唤醒形成一个空转循环。偶尔是闹钟比较值配成了已过去的时刻导致标志一直置位。解决在进入 stop 模式前增加一个防抖流程先把RTC-ISR里的ALRAF和WUTF全部读并清零再置位ALRAE。中断服务程序里只做最小操作读中断状态寄存器和清中断标志。不要在里面调用打印函数或者长时间循环避免中断返回前再次进入了不必要的中断抢占。寄存器写保护也是必须注意的RTC 的WPR键值不先写入 0xCA 和 0x53写任何其他寄存器都会被忽略清标志这个操作根本不会生效所以出现了清了没清掉的假象。4.4 坑四Stop2 模式下 LPUART 完全无法唤醒现象将模式换成 Stop2 后外部串口发送数据设备纹丝不动功耗倒是低到微安级别但功能不可用。原因硬件设计上 Stop2 模式关闭了大部分外设时钟域LPUART 在 Stop2 下不具备唤醒能力这是产品手册明确规定的边界条件。很多开发者习惯性以为哈尔库函数允许设置 Stop2 就能全功能保留实际它只负责配置电源控制寄存器不具备外设能力判断。解决需要 LPUART 唤醒必须选择 Stop1如果一定要使用 Stop2只能通过 RTC 定时唤醒后由主控主动打开 LPUART 接收外部数据这就把串口唤醒变成了轮询模式功耗和实时性需要重新取舍。这是我当时在选型对照表里用红笔标注的一条边界。4.5 坑五PCB 上电后 LSE 不起振RTC 闹钟被忽略现象板子烧录代码后程序能跑但 RTC 设置的闹钟永不触发电流高得离谱。通过调试器查看RCC-CR的LSERDY位一直是 0。原因LSE 晶振的负载电容与实际失配最常见的是晶振买的是 6pF 负载要求但 PCB 上焊的是 12pF 电容导致反馈信号衰减到起振阈值以下。晶振两端对地电容过大或者过小都会影响。另一个原因是 LSE 旁边有其他高频信号线穿过串扰导致持续停振。解决检查 LSE 晶振匹配电容32.768kHz 晶振一般是 6pF 到 12.5pF计算出实际等效负载后调整电容容值。PCB 布局上把 LSE 引脚周围留出禁止铺铜区域避免高频信号干扰。软件层面也可以给 LSE 起振失败加一个回退策略检测到LSERDY超时后切换到 LSI保证 RTC 功能可用时间精度会差一些但至少不会永久卡死。STM32L431 的 RTC 有一个RTC_CR的OSEL位控制时钟源选择但切换时钟源前依然要解除写保护具体做法是先写RTC_WPR键值。5. 实测验证从静态电流到动态唤醒时序5.1 电流怎么测万用表 vs 串联电阻法测量点选择不要直接用普通万用表的电流挡并联在电源上万用表电流挡的内阻会引入压降削弱后端供电电压导致芯片掉电后进入非预期复位形态。正确的做法是断开供电线串入一个 10 欧姆采样电阻用示波器测量采样电阻两端的压降。通过欧姆定律换算成电流。万用表只适合测稳态电流示波器适合测瞬态唤醒电流。工程上有条件的话用专用电流探头更好没有条件就用串联电阻法这个方案在绝大多数场景下足够用。采样电阻的取值需要权衡10 欧姆对应微安级电流时会得到微伏级压降可能需要示波器的低噪声模式才能分辨换成 1 千欧电阻又会影响休眠时的实际供电电压。我一般分两档测静态电流用 1 千欧串联电阻配合高精度万用表读电压唤醒瞬间的动态电流再换 10 欧姆电阻抓波形。两次测量互相印证不会因为单次测量方法引入误判。5.2 唤醒时序验证示波器抓 LPUART 与 RTC 引脚的信号验证唤醒是否正常不只看设备能不能醒要看唤醒之后到串口能正常收发的时间差。将示波器探头夹在 LPUART 的 TX 引脚和一个普通 GPIO 上。GPIO 在进入 stop 前拉低唤醒后第一句话前拉高这段时间就是实际唤醒耗时。测量结果中 Stop1 模式一般会在几微秒到几十微秒之间如果这个时间超过 200 微秒说明唤醒后的时钟重配流程有冗余例如没有先用 MSI 快速跑而是去等 PLL 锁定。RTC 的验证更加直接把闹钟时间设为当前时间后 5 秒示波器看WKUP引脚有没有对应的电平变化。RTC 唤醒定时器可以关联到 RTC 输出引脚通过复用功能配置将唤醒事件映射到某个 GPIO逻辑分析仪就能直接看到周期性脉冲。我在实际项目中会把 WKUP 引脚作为一个呼吸灯信号肉眼就能判断休眠周期是否正常不需要每次都接示波器。5.3 进阶技巧唤醒后先跑 MSI 挡住 PLL 启动延迟Stop 模式退出时芯片默认用 MSI 时钟启动。MSI 的起振速度远快于 PLL但频率低只有 4MHz 左右。如果你的应用唤醒后不要求立即跑到 80MHz就可以先不配置 PLL只用 MSI 跑最低需求的外设之后在空闲时再升频。这样可以把「唤醒后到可响应」的时间压缩到很短。具体做法是在唤醒后的时钟配置代码里加一个分支判断当前运行模式为低功耗唤醒时跳过 PLL 配置直接初始化所需的最小外设。等主循环里确认任务需要高速算力时再调用完整的时钟配置函数切换。实测效果是HAL_PWR_EnterSTOPMode返回后到串口能接收数据时间从 170 微秒降到 35 微秒左右对低功耗瞬态响应要求高的场景非常有用。要注意的是跳过 PLL 期间不能做浮点密集型运算或 ADC 高精度采样因为 MSI 对ADC12的时钟源支持度和 jitter 参数没有 PLL 好。这算是我自己摸索出来的经验第一次做低功耗唤醒的时候曾因为上来就调 PLL 导致唤醒延迟超标后来翻芯片手册里 MSI 的启动特性才想通。从那以后我每次做低功耗唤醒都会强制走一遍「先看唤醒源边界条件再量唤醒延迟最后看稳态电流」的流程设备反而很少再出幺蛾子希望帮到你。本文还有配套的精品资源点击获取