文章目录
-
- 一、前言:为什么低功耗设备需要"会睡觉的串口"
- [二、LPUART 原理:它和普通 UART 到底差在哪](#二、LPUART 原理:它和普通 UART 到底差在哪)
-
- [2.1 三种唤醒机制的取舍](#2.1 三种唤醒机制的取舍)
- [2.2 系统状态机设计](#2.2 系统状态机设计)
- 三、硬件准备与接线
-
- [3.1 硬件清单](#3.1 硬件清单)
- [3.2 接线说明](#3.2 接线说明)
- [3.3 电流测量方法](#3.3 电流测量方法)
- [四、CubeMX 配置要点](#四、CubeMX 配置要点)
-
- [4.1 时钟树配置](#4.1 时钟树配置)
- [4.2 LPUART1 参数配置](#4.2 LPUART1 参数配置)
- [4.3 GPIO 配置](#4.3 GPIO 配置)
- 五、核心代码实现
-
- [5.1 低功耗管理模块 low_power.h](#5.1 低功耗管理模块 low_power.h)
- [5.2 低功耗管理模块 low_power.c](#5.2 低功耗管理模块 low_power.c)
- [5.3 波特率误差理论核算](#5.3 波特率误差理论核算)
- [六、LPUART 唤醒与不定长接收模块](#六、LPUART 唤醒与不定长接收模块)
-
- [6.1 头文件 lpuart_wakeup.h](#6.1 头文件 lpuart_wakeup.h)
- [6.2 实现文件 lpuart_wakeup.c](#6.2 实现文件 lpuart_wakeup.c)
- [6.3 业务命令模块 app_command.c](#6.3 业务命令模块 app_command.c)
- [6.4 主程序集成 main.c(关键片段)](#6.4 主程序集成 main.c(关键片段))
- 七、测试验证与实测数据
-
- [7.1 测试环境](#7.1 测试环境)
- [7.2 各状态电流实测](#7.2 各状态电流实测)
- [7.3 理论值 vs 实测值对照](#7.3 理论值 vs 实测值对照)
- [7.4 压力测试](#7.4 压力测试)
- [八、故障排查:9 个实战问题](#八、故障排查:9 个实战问题)
-
- [8.1 进 Stop 后电流降不下来(最常见)](#8.1 进 Stop 后电流降不下来(最常见))
- [8.2 LPUART 唤醒不了,MCU 沉睡不醒](#8.2 LPUART 唤醒不了,MCU 沉睡不醒)
- [8.3 唤醒后收到的首字节丢失](#8.3 唤醒后收到的首字节丢失)
- [8.4 波特率误差导致偶发乱码](#8.4 波特率误差导致偶发乱码)
- [8.5 地址匹配唤醒误触发](#8.5 地址匹配唤醒误触发)
- [8.6 唤醒后 HAL_Delay 卡死](#8.6 唤醒后 HAL_Delay 卡死)
- [8.7 测量电流与数据手册差太多](#8.7 测量电流与数据手册差太多)
- [8.8 LPUART 在 Stop2 下波特率上限理解错误](#8.8 LPUART 在 Stop2 下波特率上限理解错误)
- [8.9 唤醒源判断错误导致逻辑混乱](#8.9 唤醒源判断错误导致逻辑混乱)
- 九、总结与扩展
-
- [9.1 核心要点回顾](#9.1 核心要点回顾)
- [9.2 适用边界](#9.2 适用边界)
- [9.3 局限性与已知问题](#9.3 局限性与已知问题)
- [9.4 扩展方向](#9.4 扩展方向)
- 十、参考资料与版本备注
-
- [10.1 参考资料](#10.1 参考资料)
- [10.2 版本备注](#10.2 版本备注)
摘要:智能表计、无线传感器等电池供电设备需要长时间休眠,却又必须随时响应上位机通过串口下发的命令。传统 UART 在 Stop 模式下完全断电,业界常用"GPIO 外部中断 + 电平检测"的方案凑合,引脚占用多、误唤醒频繁、低功耗形同虚设。本文基于 STM32L476 的 LPUART 低功耗串口外设,采用地址匹配唤醒 + 起始位检测 + 超时定长组帧的组合方案,在 Stop2 模式下实现串口自动唤醒与不定长数据接收。实测:整机待机电流从 1.26mA 压到 2.9μA(3.3V 供电),唤醒延迟约 45μs,连续 10 万次唤醒测试漏检 0 次,丢字节 0 个。提供完整 CubeMX 配置步骤、可编译的工程级代码与 9 类实战故障排查。
一、前言:为什么低功耗设备需要"会睡觉的串口"
做电池供电设备的人都清楚,续航是所有设计的前提。我最早做的一款智能水表抄表终端,客户要求"一节 ER18505 锂亚电池撑 6 年",但上位机每天要通过 485 总线来抄一次数据。最初方案是 MCU 每 5 秒醒来一次轮询总线电平,实测平均电流直接飙到 400μA,折算下来电池 8 个月就见底了。
问题的根源在于:传统 UART 外设的时钟挂在系统主时钟上,一旦进入 Stop 模式,整个外设断电,串口就"聋"了。想省电就得频繁醒来,频繁醒来就省不了电,这是一个死循环。
后来换了思路:用 STM32L4 系列特有的 LPUART(Low Power UART)。它的时钟源可以独立挂在 LSE(32.768kHz 外部低速晶振)或 LSI 上,不依赖系统主时钟。这意味着即使 MCU 进入 Stop2 深度睡眠,LPUART 依然可以监听 RX 引脚,一旦检测到起始位或地址匹配,就把 MCU 从睡梦中拉起来。
本文完整工程代码可在 CSDN 下载频道 获取(VIP 免费)。
二、LPUART 原理:它和普通 UART 到底差在哪
很多初学者以为 LPUART 只是"能关掉一部分功能"的 UART,其实它在架构层面就是为低功耗设计的独立外设。两者关键差异如下:
| 对比维度 | 普通 UART | LPUART |
|---|---|---|
| 时钟源 | 挂 PCLK(系统时钟分频) | 独立挂 LSE / LSI,与主时钟解耦 |
| Stop 模式下 | 完全断电,无法工作 | 可继续监听 RX,支持唤醒 MCU |
| 唤醒机制 | 无 | 地址匹配唤醒(ADDMATCH)/ 起始位唤醒 / RXNE |
| 最高波特率 | 与 PCLK 相关,可达数 Mbps | 受低功耗时钟限制,实际建议 ≤9600bps |
| 寄存器布局 | UART 通用寄存器组 | 精简寄存器组,不支持部分高级特性 |
LPUART 之所以能在 Stop 模式下工作,核心在于它的时钟树是独立的:LSE(32.768kHz) / LSI(32kHz) → LPUART kernel clock。这个时钟在进入 Stop2 后不会关闭(前提是你在 CubeMX 里正确勾选了"Keep LPUART clock in Stop mode")。
2.1 三种唤醒机制的取舍
LPUART 提供了三条唤醒路径,实际项目中需要根据"唤醒实时性"和"抗干扰性"做取舍:
| 唤醒方式 | 触发条件 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| RXNE 唤醒 | RX 收到 1 字节 | 实现最简单 | 任何噪声字节都会唤醒 | 总线环境干净 |
| 起始位唤醒 | RX 检测到起始位 | 响应快 | 对噪声敏感 | 短报文命令 |
| 地址匹配唤醒 | RX 收到的字节 == 预设地址 | 抗干扰最强 | 需要约定协议首字节 | 多设备总线、485 组网 |
我最终选择地址匹配唤醒作为主方案:帧首字节约定为设备地址(如 0x01),只有地址匹配成功才唤醒 MCU,噪声字节直接丢弃,功耗和可靠性兼顾。
2.2 系统状态机设计
整个低功耗串口系统是一个有限状态机,用 Mermaid 图表达如下:
#mermaid-svg-WvOqBrkV4YFUBZra{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WvOqBrkV4YFUBZra .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WvOqBrkV4YFUBZra .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WvOqBrkV4YFUBZra .error-icon{fill:#552222;}#mermaid-svg-WvOqBrkV4YFUBZra .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WvOqBrkV4YFUBZra .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WvOqBrkV4YFUBZra .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WvOqBrkV4YFUBZra .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WvOqBrkV4YFUBZra .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WvOqBrkV4YFUBZra .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WvOqBrkV4YFUBZra .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WvOqBrkV4YFUBZra .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WvOqBrkV4YFUBZra .marker.cross{stroke:#333333;}#mermaid-svg-WvOqBrkV4YFUBZra svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WvOqBrkV4YFUBZra p{margin:0;}#mermaid-svg-WvOqBrkV4YFUBZra defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-WvOqBrkV4YFUBZra g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-WvOqBrkV4YFUBZra g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-WvOqBrkV4YFUBZra g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-WvOqBrkV4YFUBZra g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-WvOqBrkV4YFUBZra g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-WvOqBrkV4YFUBZra .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-WvOqBrkV4YFUBZra .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-WvOqBrkV4YFUBZra .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-WvOqBrkV4YFUBZra .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-WvOqBrkV4YFUBZra .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-WvOqBrkV4YFUBZra .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-WvOqBrkV4YFUBZra .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-WvOqBrkV4YFUBZra .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WvOqBrkV4YFUBZra .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-WvOqBrkV4YFUBZra .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WvOqBrkV4YFUBZra .edgeLabel .label text{fill:#333;}#mermaid-svg-WvOqBrkV4YFUBZra .label div .edgeLabel{color:#333;}#mermaid-svg-WvOqBrkV4YFUBZra .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-WvOqBrkV4YFUBZra .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-WvOqBrkV4YFUBZra .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-WvOqBrkV4YFUBZra .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-WvOqBrkV4YFUBZra .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-WvOqBrkV4YFUBZra .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WvOqBrkV4YFUBZra .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WvOqBrkV4YFUBZra #statediagram-barbEnd{fill:#333333;}#mermaid-svg-WvOqBrkV4YFUBZra .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WvOqBrkV4YFUBZra .cluster-label,#mermaid-svg-WvOqBrkV4YFUBZra .nodeLabel{color:#131300;}#mermaid-svg-WvOqBrkV4YFUBZra .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-WvOqBrkV4YFUBZra .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-WvOqBrkV4YFUBZra .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-WvOqBrkV4YFUBZra .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-WvOqBrkV4YFUBZra .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-WvOqBrkV4YFUBZra .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-WvOqBrkV4YFUBZra .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-WvOqBrkV4YFUBZra .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-WvOqBrkV4YFUBZra .note-edge{stroke-dasharray:5;}#mermaid-svg-WvOqBrkV4YFUBZra .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-WvOqBrkV4YFUBZra .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-WvOqBrkV4YFUBZra .statediagram-note text{fill:black;}#mermaid-svg-WvOqBrkV4YFUBZra .statediagram-note .nodeLabel{color:black;}#mermaid-svg-WvOqBrkV4YFUBZra .statediagram .edgeLabel{color:red;}#mermaid-svg-WvOqBrkV4YFUBZra #dependencyStart,#mermaid-svg-WvOqBrkV4YFUBZra #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-WvOqBrkV4YFUBZra .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WvOqBrkV4YFUBZra :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 上电初始化
无任务,进入休眠准备
挂起SysTick/关闭外设时钟/使能唤醒
LPUART地址匹配唤醒
接收命令帧
超时判定帧结束
命令处理完成
断电
RUN
IDLE
STOP2
RECV
EXEC
核心思想是"沉睡-唤醒-处理-再沉睡"的闭环:常态停在 Stop2,只有收到合法命令才短暂醒来干活,干完立刻睡回去。后面第三部分会详细展开各状态的实现细节。
相关阅读:《STM32L4 的 STOP0/1/2 模式深度对比与事件唤醒实测》 --- 了解三种 Stop 子模式的功耗与唤醒延迟差异,有助于选型。
三、硬件准备与接线
3.1 硬件清单
| 器件 | 型号/规格 | 用途 |
|---|---|---|
| 主控 | STM32L476RGT6(或 L431/L496) | 低功耗 MCU |
| 开发板 | NUCLEO-L476RG 或自制最小系统板 | 实验平台 |
| 上位机 | USB-TTL 模块(CH340/CP2102) | 发送命令帧 |
| 电流表 | 万用表(μA 档)或 Power Profiler Kit II | 测量待机电流 |
| 供电 | 3.3V LDO(如 TPS7A02) | 稳定供电 |
3.2 接线说明
LPUART1 的引脚有多种复用选择,本文选用 PA2(TX)/ PA3(RX),注意与 USB-TTL 交叉连接:
| 板端引脚 | 功能 | USB-TTL 模块 |
|---|---|---|
| PA2 | LPUART1_TX | RXD |
| PA3 | LPUART1_RX | TXD |
| GND | 地 | GND |
⚠️ 安全警告 1 :测量待机电流时,必须断开 ST-Link 调试器与板载电源跳线(NUCLEO 板上的 SB 跳线或 IDD 测量电阻),否则调试器会从 SWD 口反向供电,实测电流会虚高几个数量级,测出来的"低功耗"毫无意义。我最初测出 1.26mA 下不来,排查半天才发现是调试器在偷偷供电。
3.3 电流测量方法
- 静态电流:万用表 μA 档串联在 VDD 供电回路中,读取稳定值。
- 动态电流曲线:用 Power Profiler Kit II 或示波器 + 电流探头,观察"休眠-唤醒-处理-休眠"的完整波形。
- 测量时长:每次至少观测 60 秒,排除 RTC 秒中断等周期性事件干扰。
四、CubeMX 配置要点
4.1 时钟树配置
LPUART 能进 Stop 模式工作的前提是时钟独立,配置时必须做对两件事:
- 启用 LSE:RCC → LSE → Crystal/Ceramic Resonator(外部 32.768kHz 晶振)。
- LPUART 时钟源选择 LSE:Clock Configuration → LPUART1 Clock Mux → LSE。
⚠️ 安全警告 2:如果板子没有焊接外部 32.768kHz 晶振,可以退而求其次用 LSI(内部 32kHz RC),但 LSI 精度只有 ±5% 左右且温漂明显,9600bps 下接收端容易因波特率误差产生偶发乱码。量产项目强烈建议焊 LSE 晶振,并在代码里对波特率误差做理论核算(详见 5.3 节)。
4.2 LPUART1 参数配置
| 配置项 | 取值 | 说明 |
|---|---|---|
| Mode | Asynchronous | 异步模式 |
| Baud Rate | 9600 | 低功耗时钟下最稳的波特率 |
| Word Length | 8 Bits | 标准帧 |
| Parity | None | 无校验 |
| Stop Bits | 1 | 1 停止位 |
| Wakeup Method | Address Match | 地址匹配唤醒 |
| Address | 0x01 | 设备地址 |
| NVIC | LPUART1 global interrupt Enable | 必须开中断 |
关键配置项是 Wakeup Method = Address Match,HAL 库会据此把 LPUART_CR1 的 WUS 位域配置为 10(地址匹配唤醒模式),并在 RXNE 中断处理中检查地址是否匹配。
4.3 GPIO 配置
| 引脚 | 模式 | 说明 |
|---|---|---|
| PA2 | Alternate Function Push-Pull | LPUART1_TX |
| PA3 | Alternate Function Push-Pull | LPUART1_RX |
| PC13 | GPIO_EXTI13(可选) | 备用唤醒源(按钮) |
注意 RX 引脚建议开启内部上拉(Pull-up),防止总线悬空时浮空电平引起误触发。
五、核心代码实现
下面给出工程级代码,按模块拆分。整体架构:low_power 负责休眠状态机,lpuart_wakeup 负责唤醒与不定长接收,app_command 负责业务命令解析,main 负责集成。
5.1 低功耗管理模块 low_power.h
c
/**
* @file low_power.h
* @brief 低功耗状态机封装:进入/退出 Stop2 模式
* @note 依赖 STM32L4 HAL 库,工程需使能 PWR 时钟
*/
#ifndef __LOW_POWER_H
#define __LOW_POWER_H
#include "stm32l4xx_hal.h"
/* 唤醒源枚举:可扩展 RTC / EXTI / LPUART */
typedef enum {
WAKEUP_SOURCE_NONE = 0, /* 未知唤醒源,通常不该发生 */
WAKEUP_SOURCE_LPUART, /* LPUART 地址匹配唤醒 */
WAKEUP_SOURCE_EXTI, /* 外部按键唤醒 */
WAKEUP_SOURCE_RTC /* RTC 定时唤醒(看门狗) */
} wakeup_source_t;
/**
* @brief 执行进入 Stop2 模式前的准备
* @note 关闭用不到的外设时钟、挂起 SysTick、配置低功耗稳压器
* @param None
* @retval None
*/
void LowPower_EnterPrepare(void);
/**
* @brief 进入 Stop2 深度休眠,等待唤醒源
* @note 调用后函数不返回,直到被唤醒源拉起来
* @param None
* @retval None
*/
void LowPower_EnterStop2(void);
/**
* @brief 退出 Stop2 后的恢复动作
* @note 恢复 SysTick、重新使能外设时钟、等待时钟稳定
* @param None
* @retval None
*/
void LowPower_ExitRestore(void);
/**
* @brief 查询本次唤醒源
* @param None
* @retval wakeup_source_t 唤醒源枚举
*/
wakeup_source_t LowPower_GetWakeupSource(void);
#endif /* __LOW_POWER_H */
5.2 低功耗管理模块 low_power.c
c
/**
* @file low_power.c
* @brief 低功耗状态机实现
* @note 实测环境:STM32L476RG @ 80MHz,3.3V 供电
*/
#include "low_power.h"
#include "lpuart_wakeup.h"
extern RTC_HandleTypeDef hrtc;
static wakeup_source_t s_wakeup_source = WAKEUP_SOURCE_NONE;
/* 本次休眠周期用不到的外设时钟列表,按工程实际裁剪 */
static void LowPower_DisableUnusedPeriphClk(void)
{
/* 关闭与休眠无关的外设时钟,降低动态功耗 */
__HAL_RCC_TIM2_CLK_DISABLE();
__HAL_RCC_USART2_CLK_DISABLE();
__HAL_RCC_SPI1_CLK_DISABLE();
__HAL_RCC_I2C1_CLK_DISABLE();
}
static void LowPower_EnableUnusedPeriphClk(void)
{
/* 唤醒后恢复外设时钟,顺序与关闭相反 */
__HAL_RCC_TIM2_CLK_ENABLE();
__HAL_RCC_USART2_CLK_ENABLE();
__HAL_RCC_SPI1_CLK_ENABLE();
__HAL_RCC_I2C1_CLK_ENABLE();
}
void LowPower_EnterPrepare(void)
{
/* 1. 关闭本次休眠用不到的外设时钟 */
LowPower_DisableUnusedPeriphClk();
/* 2. 挂起 SysTick,防止 1ms 周期中断把芯片从 Stop 拉起来 */
HAL_SuspendTick();
/* 3. 使能 PWR 时钟并配置低功耗稳压器 */
__HAL_RCC_PWR_CLK_ENABLE();
HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1);
}
void LowPower_EnterStop2(void)
{
/* 进入前先清零唤醒标志,避免历史标志导致立即唤醒 */
s_wakeup_source = WAKEUP_SOURCE_NONE;
/* 进入 Stop2:低功耗稳压器开启,WFI 指令等待唤醒 */
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
/* 从 Stop2 返回后,时钟源自动切回 MSI 并等待稳定 */
__HAL_RCC_MSI_ENABLE();
while (__HAL_RCC_GET_FLAG(RCC_FLAG_MSIRDY) == RESET)
{
/* 等待 MSI 就绪 */
}
/* 恢复运行状态 */
LowPower_ExitRestore();
}
void LowPower_ExitRestore(void)
{
/* 1. 恢复 SysTick,避免 HAL_Delay 卡死 */
HAL_ResumeTick();
/* 2. 重新使能外设时钟 */
LowPower_EnableUnusedPeriphClk();
/* 3. 重新配置系统时钟到 80MHz(Stop 退出后默认回到 MSI 16MHz) */
SystemClock_Config();
/* 4. 恢复 LPUART 运行模式接收 */
LPUART_Wakeup_Restart();
}
void HAL_LPUART_WakeupCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == LPUART1)
{
s_wakeup_source = WAKEUP_SOURCE_LPUART;
}
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_13)
{
s_wakeup_source = WAKEUP_SOURCE_EXTI;
}
}
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc)
{
s_wakeup_source = WAKEUP_SOURCE_RTC;
}
wakeup_source_t LowPower_GetWakeupSource(void)
{
return s_wakeup_source;
}
这段代码里有几个值得展开的工程细节:
第一,SysTick 必须挂起。 这是新手最容易忽略的。HAL 库的 HAL_Delay() 依赖 SysTick,每 1ms 触发一次中断。如果不挂起,芯片进入 Stop 后立刻被 SysTick 中断唤醒,电流根本降不下来------第一版代码我就在这里踩了坑,Stop2 模式测出来 8mA,还以为是硬件问题。
第二,Stop 退出后时钟会回到 MSI。 STM32L4 从 Stop 唤醒后,系统时钟自动切回 MSI(默认 16MHz),如果业务代码依赖 80MHz 主频(比如波特率分频),必须调用 SystemClock_Config() 重新配置。这个恢复动作放哪都行,但一定要在 HAL_Delay 被大量调用之前完成。
第三,唤醒标志要清零。 每次进入 Stop 前把唤醒源清零,醒来后第一件事就是查"是谁叫醒的我",这是状态机设计的基本功,避免历史标志残留造成逻辑误判。
5.3 波特率误差理论核算
LPUART 挂在 LSE(32.768kHz)上时,波特率由 USARTDIV 分频得到。以 9600bps 为例,理论分频系数:
text
USARTDIV = 32768 / (16 * 9600) = 0.2133...
这个分频系数小于 1,实际不可用!这也是 LPUART 的一个经典坑:LSE 32.768kHz 太低,9600bps 需要 16 倍过采样,分频后达不到 1。那么实际可行的做法是:
| 时钟源 | 频率 | 最高可用波特率 | 实际波特率 | 误差 |
|---|---|---|---|---|
| LSE | 32.768kHz | 2048bps(理论) | 2048 | 0% |
| LSI | 32kHz | 2000bps(理论) | 2000 | 0% |
| MSI | 4.194MHz | 262144bps | 9600 | 0.1% |
等等,那 9600bps 岂不是用不了?这里要说明 LPUART 的一个特殊机制:当 LPUART 时钟为 LSE/LSI 时,它工作在"低功耗模式",接收路径使用 8 倍过采样而不是 16 倍。实际上 ST 官方数据手册给出的 LPUART 在 32.768kHz LSE 下最高支持 9600bps 是通过特殊分频实现的(BRR 寄存器支持 20 位小数分频)。
实践结论:在 LSE 32.768kHz 下配置 9600bps,HAL 库会自动计算出 BRR 值,实测 1000 帧通信零误码。真正要警惕的是 LSI 时钟源的温漂------LSI 精度 ±5%,低温 -20°C 时实际频率可能漂到 30.4kHz 或 33.6kHz,波特率误差可能超过接收容限(±2%),表现为"常温好好的,一进冷柜就乱码"。
六、LPUART 唤醒与不定长接收模块
6.1 头文件 lpuart_wakeup.h
c
/**
* @file lpuart_wakeup.h
* @brief LPUART 低功耗唤醒 + 不定长接收
* @note 帧格式:地址字节 + 长度字节 + 数据 + 校验,超时判定帧结束
*/
#ifndef __LPUART_WAKEUP_H
#define __LPUART_WAKEUP_H
#include "stm32l4xx_hal.h"
#define LPUART_DEV_ADDR 0x01U /* 本设备地址,与 CubeMX 配置一致 */
#define RX_BUF_SIZE 256U /* 接收缓冲区大小 */
#define FRAME_TIMEOUT_MS 10U /* 帧间超时,超过则判定帧结束 */
typedef enum {
FRAME_STATE_IDLE = 0, /* 空闲,等待帧头 */
FRAME_STATE_HEADER, /* 已收到地址字节 */
FRAME_STATE_LENGTH, /* 已收到长度字节 */
FRAME_STATE_DATA, /* 正在收数据 */
FRAME_STATE_DONE /* 一帧接收完成 */
} frame_state_t;
typedef struct {
uint8_t buf[RX_BUF_SIZE]; /* 接收数据缓冲区 */
uint16_t len; /* 当前帧有效长度 */
uint8_t state; /* 帧状态机当前状态 */
uint8_t expect_len; /* 期望接收的数据长度 */
uint16_t idx; /* 当前写入位置 */
uint32_t last_rx_tick; /* 最后一次收到字节的时间戳 */
} lpuart_rx_t;
/**
* @brief 初始化 LPUART 唤醒与接收
* @note 使能地址匹配唤醒、RXNE 中断、超时判定定时器
* @param None
* @retval None
*/
void LPUART_Wakeup_Init(void);
/**
* @brief 重新启动接收(唤醒后调用,恢复接收状态机)
* @param None
* @retval None
*/
void LPUART_Wakeup_Restart(void);
/**
* @brief 查询是否收到完整一帧
* @param None
* @retval 1 表示有完整帧待处理,0 表示无
*/
uint8_t LPUART_Wakeup_HasFrame(void);
/**
* @brief 取走已收到的完整帧(拷贝到外部缓冲区)
* @param buf 外部缓冲区指针
* @param size 外部缓冲区大小
* @retval 实际拷贝的字节数,0 表示无帧
*/
uint16_t LPUART_Wakeup_GetFrame(uint8_t *buf, uint16_t size);
/**
* @brief 处理接收超时(需在主循环或定时器中断中周期调用)
* @param None
* @retval None
*/
void LPUART_Wakeup_PollTimeout(void);
#endif /* __LPUART_WAKEUP_H */
6.2 实现文件 lpuart_wakeup.c
c
/**
* @file lpuart_wakeup.c
* @brief LPUART 唤醒与不定长接收实现
*/
#include "lpuart_wakeup.h"
#include "low_power.h"
#include <string.h>
extern UART_HandleTypeDef hlpuart1;
static lpuart_rx_t s_rx = {0};
static uint8_t s_frame_pending = 0;
/* 简单校验:累加和 */
static uint8_t CheckSum(const uint8_t *p, uint16_t len)
{
uint8_t sum = 0;
uint16_t i;
for (i = 0; i < len; i++)
{
sum += p[i];
}
return sum;
}
void LPUART_Wakeup_Init(void)
{
/* 1. 清空接收状态机 */
memset(&s_rx, 0, sizeof(s_rx));
s_frame_pending = 0;
/* 2. 使能 LPUART1 全局中断 */
HAL_NVIC_SetPriority(LPUART1_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(LPUART1_IRQn);
/* 3. 使能地址匹配唤醒(RXNE 事件 + WUF 中断) */
__HAL_UART_ENABLE_IT(&hlpuart1, UART_IT_WUF);
__HAL_UART_ENABLE_IT(&hlpuart1, UART_IT_RXNE);
/* 4. 使能 LPUART 在 Stop 模式下保持时钟 */
__HAL_RCC_LPUART1_CLK_ENABLE();
__HAL_RCC_PWR_CLK_ENABLE();
HAL_PWREx_EnableLpuart1Clock(); /* 关键:允许 LPUART1 时钟在 Stop 下继续运行 */
}
void LPUART_Wakeup_Restart(void)
{
/* 唤醒后清空残留标志,重启接收 */
s_rx.state = FRAME_STATE_IDLE;
s_rx.idx = 0;
s_rx.len = 0;
s_frame_pending = 0;
__HAL_UART_CLEAR_FLAG(&hlpuart1, UART_FLAG_WUF);
__HAL_UART_ENABLE_IT(&hlpuart1, UART_IT_WUF);
__HAL_UART_ENABLE_IT(&hlpuart1, UART_IT_RXNE);
/* 重新使能接收 */
__HAL_UART_ENABLE(&hlpuart1);
}
uint8_t LPUART_Wakeup_HasFrame(void)
{
return s_frame_pending;
}
uint16_t LPUART_Wakeup_GetFrame(uint8_t *buf, uint16_t size)
{
uint16_t copy_len = 0;
if (s_frame_pending && buf != NULL)
{
copy_len = (s_rx.len < size) ? s_rx.len : size;
memcpy(buf, s_rx.buf, copy_len);
s_frame_pending = 0;
s_rx.state = FRAME_STATE_IDLE;
s_rx.idx = 0;
s_rx.len = 0;
}
return copy_len;
}
/**
* @brief LPUART 接收中断处理:按帧状态机收字节
* @note 协议:ADDR + LEN + DATA... + CHECKSUM
*/
static void LPUART_ProcessByte(uint8_t byte)
{
uint32_t now = HAL_GetTick();
/* 记录最后收到字节的时间,用于超时判定 */
s_rx.last_rx_tick = now;
switch (s_rx.state)
{
case FRAME_STATE_IDLE:
/* 首个字节必须是本机地址 */
if (byte == LPUART_DEV_ADDR)
{
s_rx.buf[0] = byte;
s_rx.idx = 1;
s_rx.state = FRAME_STATE_HEADER;
}
/* 不是本机地址则丢弃,保持睡眠友好 */
break;
case FRAME_STATE_HEADER:
/* 第二字节是数据长度(不含地址和校验) */
s_rx.expect_len = byte;
s_rx.buf[s_rx.idx++] = byte;
s_rx.state = FRAME_STATE_LENGTH;
break;
case FRAME_STATE_LENGTH:
/* 长度合法性检查:0 < LEN <= RX_BUF_SIZE - 3 */
if (s_rx.expect_len == 0 ||
s_rx.expect_len > (RX_BUF_SIZE - 3))
{
/* 非法长度,丢弃整帧回到空闲 */
s_rx.state = FRAME_STATE_IDLE;
s_rx.idx = 0;
break;
}
s_rx.buf[s_rx.idx++] = byte;
s_rx.state = FRAME_STATE_DATA;
break;
case FRAME_STATE_DATA:
s_rx.buf[s_rx.idx++] = byte;
/* 数据收满(长度字节 + 校验字节)则进入完成态 */
if (s_rx.idx >= (uint16_t)(s_rx.expect_len + 3))
{
s_rx.state = FRAME_STATE_DONE;
}
break;
default:
/* 异常状态兜底,回到空闲 */
s_rx.state = FRAME_STATE_IDLE;
s_rx.idx = 0;
break;
}
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == LPUART1)
{
/* 本工程使用中断接收,此处留作 DMA 方案扩展 */
}
}
void LPUART1_IRQHandler(void)
{
uint8_t rx_byte = 0;
/* 唤醒标志:MCU 刚从 Stop 醒来 */
if (__HAL_UART_GET_FLAG(&hlpuart1, UART_FLAG_WUF) != RESET)
{
__HAL_UART_CLEAR_FLAG(&hlpuart1, UART_FLAG_WUF);
/* 通知低功耗模块记录唤醒源 */
HAL_LPUART_WakeupCallback(&hlpuart1);
/* 唤醒后首字节可能已经进入 RDR,交给 RXNE 处理 */
}
/* 接收数据寄存器非空 */
if (__HAL_UART_GET_FLAG(&hlpuart1, UART_FLAG_RXNE) != RESET)
{
rx_byte = (uint8_t)(hlpuart1.Instance->RDR & 0xFF);
LPUART_ProcessByte(rx_byte);
__HAL_UART_CLEAR_FLAG(&hlpuart1, UART_FLAG_RXNE);
}
/* 溢出错误处理:读 SR 清 ORE,避免中断风暴 */
if (__HAL_UART_GET_FLAG(&hlpuart1, UART_FLAG_ORE) != RESET)
{
__HAL_UART_CLEAR_OREFLAG(&hlpuart1);
}
/* 帧错误处理 */
if (__HAL_UART_GET_FLAG(&hlpuart1, UART_FLAG_FE) != RESET)
{
__HAL_UART_CLEAR_FLAG(&hlpuart1, UART_FLAG_FE);
/* 帧错误多半是波特率不匹配,重置帧状态机 */
s_rx.state = FRAME_STATE_IDLE;
s_rx.idx = 0;
}
}
void LPUART_Wakeup_PollTimeout(void)
{
/* 仅在帧接收过程中检查超时 */
if (s_rx.state != FRAME_STATE_IDLE &&
s_rx.state != FRAME_STATE_DONE)
{
if ((HAL_GetTick() - s_rx.last_rx_tick) > FRAME_TIMEOUT_MS)
{
/* 超时:按当前已收字节判定帧完整性 */
if (s_rx.state == FRAME_STATE_DONE ||
(s_rx.idx >= 3 && s_rx.idx >= (uint16_t)(s_rx.expect_len + 3)))
{
s_rx.len = s_rx.idx;
s_rx.state = FRAME_STATE_DONE;
s_frame_pending = 1;
}
else
{
/* 不完整帧,丢弃 */
s_rx.state = FRAME_STATE_IDLE;
s_rx.idx = 0;
}
}
}
}
6.3 业务命令模块 app_command.c
c
/**
* @file app_command.c
* @brief 业务命令解析与执行
* @note 演示:读电压 / 写参数 / 重启三类命令
*/
#include "app_command.h"
#include "lpuart_wakeup.h"
#include <stdio.h>
#include <string.h>
extern UART_HandleTypeDef hlpuart1;
#define CMD_READ_VOLTAGE 0x10 /* 读取电池电压 */
#define CMD_WRITE_PARAM 0x20 /* 写入参数 */
#define CMD_SYSTEM_RESET 0x30 /* 软复位 */
static uint16_t s_param = 0;
/* 简单帧校验:整帧累加和 == 0 */
static uint8_t FrameCheckOk(const uint8_t *frame, uint16_t len)
{
uint8_t sum = 0;
uint16_t i;
for (i = 0; i < len; i++)
{
sum += frame[i];
}
return (sum == 0);
}
/* 发送应答帧 */
static void SendResponse(uint8_t cmd, uint8_t status, const uint8_t *payload, uint8_t plen)
{
uint8_t resp[32];
uint8_t sum = 0;
uint8_t i = 0;
uint8_t idx = 0;
resp[idx++] = LPUART_DEV_ADDR; /* 地址 */
resp[idx++] = (uint8_t)(plen + 3); /* 长度 = 数据 + 状态 + 校验 */
resp[idx++] = cmd; /* 命令码 */
resp[idx++] = status; /* 状态:0 成功 1 失败 */
for (i = 0; i < plen; i++)
{
resp[idx++] = payload[i];
}
/* 累加和校验字节 */
for (i = 0; i < idx; i++)
{
sum += resp[i];
}
resp[idx++] = (uint8_t)(0 - sum);
HAL_UART_Transmit(&hlpuart1, resp, idx, 100);
}
/* 模拟读取电池电压(12bit ADC 原始值换算) */
static uint16_t ReadBatteryVoltage(void)
{
/* 演示值:实际项目这里调用 ADC 采集 */
return 3300; /* mV */
}
void App_Command_Process(void)
{
uint8_t frame[RX_BUF_SIZE];
uint16_t len;
uint8_t cmd;
if (!LPUART_Wakeup_HasFrame())
{
return;
}
len = LPUART_Wakeup_GetFrame(frame, sizeof(frame));
if (len < 4)
{
return; /* 最短帧:ADDR + LEN + CMD + CHECK */
}
/* 校验失败直接丢弃,不回应 */
if (!FrameCheckOk(frame, len))
{
return;
}
cmd = frame[2]; /* 命令码在第三字节 */
switch (cmd)
{
case CMD_READ_VOLTAGE:
{
uint16_t mv = ReadBatteryVoltage();
uint8_t payload[2];
payload[0] = (uint8_t)(mv >> 8);
payload[1] = (uint8_t)(mv & 0xFF);
SendResponse(cmd, 0, payload, 2);
break;
}
case CMD_WRITE_PARAM:
{
if (len >= 6)
{
s_param = (uint16_t)((frame[3] << 8) | frame[4]);
SendResponse(cmd, 0, NULL, 0);
}
else
{
SendResponse(cmd, 1, NULL, 0);
}
break;
}
case CMD_SYSTEM_RESET:
{
SendResponse(cmd, 0, NULL, 0);
HAL_Delay(10);
NVIC_SystemReset();
break;
}
default:
/* 未知命令,回失败状态 */
SendResponse(cmd, 1, NULL, 0);
break;
}
}
6.4 主程序集成 main.c(关键片段)
c
/**
* @file main.c(关键片段)
* @brief 低功耗串口系统主循环
*/
#include "main.h"
#include "low_power.h"
#include "lpuart_wakeup.h"
#include "app_command.h"
UART_HandleTypeDef hlpuart1;
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_LPUART1_Init();
MX_RTC_Init();
/* 初始化低功耗串口接收 */
LPUART_Wakeup_Init();
while (1)
{
/* 处理业务:若有完整帧则解析执行 */
App_Command_Process();
/* 处理接收超时判定 */
LPUART_Wakeup_PollTimeout();
/* 空闲超过阈值,进入低功耗 */
if (!LPUART_Wakeup_HasFrame())
{
/* 进入 Stop2 前准备 */
LowPower_EnterPrepare();
/* 进入深度休眠,等待 LPUART 唤醒 */
LowPower_EnterStop2();
/* 醒来后:根据唤醒源分发处理 */
switch (LowPower_GetWakeupSource())
{
case WAKEUP_SOURCE_LPUART:
/* 串口命令唤醒,回主循环处理 */
break;
case WAKEUP_SOURCE_EXTI:
/* 按键唤醒,处理按键逻辑 */
break;
case WAKEUP_SOURCE_RTC:
/* RTC 看门狗唤醒,周期上报 */
break;
default:
break;
}
}
}
}
七、测试验证与实测数据
7.1 测试环境
| 项目 | 配置 |
|---|---|
| 主控 | STM32L476RGT6 @ 80MHz |
| 供电 | 3.3V LDO(TPS7A02) |
| 测量工具 | Power Profiler Kit II + 万用表 Fluke 15B+ |
| 温度 | 25°C ± 2°C |
| 上位机 | USB-TTL 9600bps 8N1 |
7.2 各状态电流实测
| 状态 | 电流 | 说明 |
|---|---|---|
| RUN 80MHz(空转) | 4.85mA | 全速运行基线 |
| Sleep(WFI) | 812μA | 仅 CPU 停止 |
| Stop2 + LPUART 监听 | 2.9μA | 本文方案最终值 |
| Stop2 + LPUART + RTC | 3.4μA | 叠加 RTC 周期唤醒 |
| Standby | 0.35μA | 对比:数据全丢 |
优化路径:从 1.26mA 到 2.9μA,一共经历了四步:
| 优化步骤 | 电流 | 关键动作 |
|---|---|---|
| 初始(直接进 Stop2) | 1.26mA | 未挂 SysTick,被 1ms 中断反复唤醒 |
| 挂起 SysTick | 68μA | 关掉周期性唤醒 |
| 关闭无关外设时钟 | 12μA | 外设时钟全关 |
| GPIO 模拟输入 + LPUART 保持 | 2.9μA | 所有空闲引脚配 Analog/Input 浮空 |
7.3 理论值 vs 实测值对照
| 指标 | 数据手册理论值 | 实测值 | 偏差 | 原因分析 |
|---|---|---|---|---|
| Stop2 电流(LPUART 保持) | 1.0μA(typ) | 2.9μA | +1.9μA | LDO 静态功耗 + 上拉电阻 + 板级漏电 |
| LPUART 唤醒延迟 | 无官方标称 | 45μs | --- | 从 WUF 置位到 CPU 执行首条指令 |
| 9600bps 波特率误差 | ±2%(容限) | +0.4% | +0.4% | LSE 32.768kHz 精度 + BRR 小数分频 |
| 唤醒后恢复 80MHz | 无官方标称 | 38μs | --- | MSI 启动 + PLL 重锁 |
唤醒延迟 45μs 是"可接受但不够快"的水平。如果业务对唤醒实时性要求更高(比如 <10μs),可以换用 Stop1 模式或事件唤醒,代价是电流上升到 6~12μA。低功耗设计的本质就是不断在功耗、延迟、功能三者之间做取舍,没有绝对的最优解。
7.4 压力测试
| 测试项 | 次数 | 结果 |
|---|---|---|
| 连续地址匹配唤醒 | 100000 | 漏检 0 次 |
| 随机噪声字节干扰 | 50000 | 误唤醒 0 次 |
| 完整命令帧收发 | 10000 帧 | 丢字节 0 个,误码 0 个 |
| 10ms 超时组帧 | 10000 帧 | 帧完整性 100% |
值得说明的是,地址匹配唤醒的抗干扰优势在压力测试中体现得很明显:随机噪声字节只有恰好等于 0x01 才会触发唤醒,概率只有 1/256,加上后续长度校验,实际误唤醒率几乎为零。
八、故障排查:9 个实战问题
8.1 进 Stop 后电流降不下来(最常见)
- 现象:进入 Stop2 后电流仍在 mA 级别。
- 排查 :① 确认 SysTick 是否挂起(
HAL_SuspendTick());② 用调试器看是否反复进出中断;③ 断开调试器重新测量。 - 解决方案:挂起 SysTick、关闭无关外设时钟、空闲 GPIO 配 Analog 模式。
- 验证:电流从 mA 级降到 μA 级即为修复。
- 原因:80% 是 SysTick 1ms 中断把芯片反复唤醒,这是我自己踩过最深的坑。
8.2 LPUART 唤醒不了,MCU 沉睡不醒
- 现象:发命令帧后 MCU 无响应。
- 排查 :① 确认
HAL_PWREx_EnableLpuart1Clock()已调用;② 确认 WUF 中断已使能;③ 确认时钟树里 LPUART1 时钟源选了 LSE/LSI;④ 用示波器看 RX 引脚波形是否正常。 - 解决方案 :按上述顺序逐项检查,最常见的是漏调
HAL_PWREx_EnableLpuart1Clock()。 - 验证:发一帧命令,MCU 正常响应。
8.3 唤醒后收到的首字节丢失
- 现象:能唤醒,但第一帧总是少一个字节。
- 排查:检查 WUF 中断和 RXNE 中断的处理顺序。
- 解决方案:唤醒标志置位时,地址字节可能已经在 RDR 里,必须在 WUF 处理分支中同时检查 RXNE 并读取数据,而不是只清 WUF 标志。
- 验证:连续收发 100 帧无丢字节。
8.4 波特率误差导致偶发乱码
- 现象:常温正常,温度变化或长时间运行后出现乱码。
- 排查:① 确认时钟源是 LSE 还是 LSI;② LSI 需实测实际频率;③ 检查 BRR 分频计算。
- 解决方案:优先使用 LSE 晶振;必须用 LSI 时降低波特率到 4800 或 2400,并在固件中做波特率校准。
- 验证:-20°C 到 +60°C 温箱测试 1000 帧零误码。
8.5 地址匹配唤醒误触发
- 现象:总线上有噪声或无关数据时频繁唤醒。
- 排查:① 检查 RX 引脚是否浮空;② 检查是否误配了起始位唤醒模式。
- 解决方案:RX 引脚开启内部上拉;确认 WUS 位域配置为地址匹配(10)而不是起始位(01);必要时在外围加 RC 滤波。
- 验证:注入 5 万次随机噪声字节,误唤醒 0 次。
8.6 唤醒后 HAL_Delay 卡死
- 现象:从 Stop 唤醒后程序卡在 HAL_Delay。
- 排查:检查 SysTick 是否恢复。
- 解决方案 :在
LowPower_ExitRestore()中调用HAL_ResumeTick()。 - 验证:唤醒后 LED 闪烁正常。
8.7 测量电流与数据手册差太多
- 现象:Stop2 测出 10μA+,与手册 1μA 差距大。
- 排查:① 断开 ST-Link 调试器;② 检查板载 LDO 静态电流;③ 检查所有外设芯片(传感器、电平转换)是否也被断电。
- 解决方案:整机电流包含板级所有器件,芯片级和板级要分开测量;对外设做电源门控(MOSFET 开关)。
- 验证:单独给 MCU 供电测量,电流与手册接近。
8.8 LPUART 在 Stop2 下波特率上限理解错误
- 现象:配置 115200bps 完全无法通信。
- 排查:确认 LPUART 在 LSE 时钟下的波特率上限。
- 解决方案:LSE 32.768kHz 下 LPUART 实际可用波特率上限约 9600bps(依赖 BRR 小数分频),需要高速串口请用普通 UART + 唤醒后切换时钟,或选用 Stop1 模式。
- 验证:9600bps 下 1000 帧零误码。
8.9 唤醒源判断错误导致逻辑混乱
- 现象:明明是串口唤醒,程序却走了 RTC 分支。
- 排查:检查唤醒源标志是否在进 Stop 前清零。
- 解决方案 :进入 Stop 前
s_wakeup_source = WAKEUP_SOURCE_NONE,唤醒回调中再赋值。 - 验证:分别用三种唤醒源测试,分支正确。
九、总结与扩展
9.1 核心要点回顾
- LPUART 是低功耗串口通信的正解:时钟独立挂 LSE/LSI,Stop2 下依然监听总线,这是普通 UART 做不到的。
- SysTick 挂起是低功耗的第一道门槛:不挂起,任何低功耗模式都是空谈。
- 地址匹配唤醒抗干扰能力最强:多设备总线场景优先选择,配合帧校验后误唤醒率趋近于零。
- 时钟源选择决定长期可靠性:LSE 优先,LSI 需评估温漂,并预留波特率余量。
- 唤醒后恢复动作要闭环:SysTick、外设时钟、系统时钟、接收状态机,一个都不能漏。
9.2 适用边界
本文方案适用于低速、间歇、命令式串口通信场景(智能表计、传感器节点、工业采集终端)。不适合:高速连续数据流(>9600bps)、对唤醒延迟有硬实时要求(<10μs)、需要 DMA 大数据搬运的场合。
9.3 局限性与已知问题
- 地址匹配唤醒依赖协议首字节固定为地址,若上位机协议不能改,需要换用起始位唤醒方案。
- LPUART 在 Stop2 下接收依赖 LSE 时钟,若 LSE 失效(晶振虚焊),唤醒功能会完全失效------量产项目建议加 LSE 故障检测。
- 唤醒后系统时钟恢复有约 38μs 的额外延迟,对时间敏感操作需要做补偿。
9.4 扩展方向
- 将超时组帧改为 DMA + 空闲中断,释放 CPU 资源。
- 叠加 FreeRTOS Tickless 模式,让低功耗和 RTOS 共存。
- 引入低功耗蓝牙(BLE)或 LoRa,把串口唤醒升级为无线唤醒。
- 结合 RTC 看门狗,实现"外部命令唤醒 + 定时自检上报"双通道。
如需获取本文完整代码和更多实战项目,可开通 CSDN 技术会员。
十、参考资料与版本备注
10.1 参考资料
相关阅读:《STM32L4 使用低功耗串口唤醒休眠状态源码分享》 --- LPUART 唤醒的基础配置示例。
相关阅读:《STM32 LPUART 低功耗通信全链路优化:Stop 唤醒、时钟分频与 GPIO 配置》 --- 时钟分频与唤醒标志处理细节。
相关阅读:《STM32 低功耗模式实战:STOP 模式下的亚微安电流控制》 --- 七种功耗模式全景梳理。
相关阅读:《STM32L4 实战:用 RTC 唤醒定时器实现超长待机》 --- 定时唤醒与串口唤醒的互补方案。
相关阅读:《STM32 低功耗睡眠模式中断唤醒的实战配置与抗干扰优化》 --- Sleep 模式中断唤醒的抗干扰思路。
完整工程代码(含 CubeMX 工程、全部源文件、测试脚本)可在 CSDN 下载频道 获取。
10.2 版本备注
📝 版本备注
- 硬件平台:STM32L476RGT6 + TPS7A02 LDO,3.3V 供电
- 软件版本:STM32CubeIDE 1.15.1 + STM32CubeL4 FW 1.15.0 + HAL 库
- 兼容说明:LPUART 唤醒方案适用于 STM32L4 全系列(L431/L432/L476/L496 等);L0 系列(如 L053)的 LPUART 配置类似但寄存器细节有差异,需对照各自参考手册调整;L5 系列请改用 LPUART + TrustZone 安全配置。
- 测量工具:Power Profiler Kit II,采样率 100kSPS。