嵌入式低功耗设计实战:从 12mA 到 40μA,电池设备续航翻十倍的踩坑记

2026年9月14日 0 By admin

去年接了一个水表远传项目,客户要求一节 ER18505 锂亚电池撑 8 年。第一版固件跑下来,实验室测算只有 2 年半。这篇文章把整个降功耗的过程拆开讲——最后整机待机电流从 12mA 降到了 40μA,实测续航估算超过 10 年。这里面大部分不是”用了什么黑科技”,而是一个个坑填出来的。

一、先搞清楚电都耗在哪了

低功耗设计第一步不是改代码,是测量。我在电源入口串了一个 1Ω 采样电阻,用示波器测压降,再配合万用表,把整机的电流画像画出来:

  • 待机时 MCU 平均电流:约 8mA(预期应该是 μA 级!)
  • 无线模块(NB-IoT)发射瞬间:峰值 350mA,持续 2~5s
  • 传感器偏置电路:1.5mA 常开
  • 电源静态电流:LDO 本身 2μA,这部分没问题

8mA 的待机电流明显不对劲。这就是第一个坑:别相信”芯片手册说待机 2μA”,要量整机

二、MCU 低功耗模式选错了,等于白睡

主控是 STM32L4 系列。第一个问题出在低功耗模式的选择上:

| 模式 | 典型电流 | 唤醒时间 | RAM 保持 |

|——|———|———|———|

| Sleep | ~300μA | 极快 | 是 |

| Stop 2 | ~1.5μA | ~5μs | 是 |

| Standby | ~300nA | 复位重启 | 否 |

最初固件用的是 Sleep 模式加 WFI,靠 SysTick 每 10ms 醒一次。问题就在这:Sleep 模式下主时钟还在跑,8mA 里大部分是它贡献的。

改成 Stop 2 后,又踩了三个坑:

坑 1:Stop 模式下唤醒源没配对。 休眠之后 RTC 闹钟和外部中断都要显式使能,特别是 EXTI 线的配置在进入 Stop 前要重新检查一遍。有一次按键中断死活唤不醒,最后发现是 HAL 库里某个外设的句柄把 EXTI 配置改掉了。

坑 2:唤醒后时钟没恢复。 Stop 模式退出后系统时钟默认切回 MSI(内部 4MHz),如果代码里直接跑了一段依赖 80MHz 的逻辑(比如 SPI 传输),时序全乱。唤醒后第一件事必须是重新配 PLL:


/* 从 Stop2 唤醒后恢复系统时钟 */
static void system_clock_restore(void)
{
    /* 唤醒后默认运行在 MSI,需重新启动 PLL */
    SystemClock_Config_from_MSIs();
    SystemCoreClockUpdate();
}

坑 3:没睡踏实。 测出来 Stop 2 下还有 300μA,逐个排查后发现是几个 GPIO 配置悬空了。休眠前要把所有 IO 统一收敛为模拟输入(AIN),浮空脚配上拉或下拉,具体按电路板上实际连接来:


/* 进入低功耗前,将所有未用 IO 配置为模拟模式,消除漏电流 */
static void gpio_enter_lowpower(void)
{
    GPIO_InitTypeDef init = {0};
    init.Mode = GPIO_MODE_ANALOG;
    init.Pull = GPIO_NOPULL;
    /* 未使用的引脚逐一置为模拟输入 */
    init.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2;
    HAL_GPIO_Init(GPIOA, &init);
    /* ...其余端口同理... */
}

仅这一项,待机电流从 300μA 降到 6μA。悬空引脚的漏电是低功耗设计的头号隐形杀手。

三、软件架构:别让定时器”碎碎念”

MCU 能睡住只是基础,真正的功夫在软件架构上。核心原则就一句话:减少唤醒次数,每次醒来把活干完再睡

原固件里每个模块各起一个软定时器:传感器采样 500ms 一次、RTC 校时 1s 一次、心跳上报 60s 一次……结果 MCU 根本睡不长。重构后的做法是统一到一个调度器里,按”下一事件时刻”设置 RTC 闹钟:


/* 统一低功耗调度:计算最近的事件时刻,设置 RTC 闹钟后进入 Stop2 */
void lowpower_scheduler_run(void)
{
    uint32_t next_event = scheduler_next_due_tick();

    if (next_event == SCHED_IDLE_NO_EVENT) {
        rtc_set_alarm_ms(DEEP_SLEEP_MAX_MS);  /* 封顶唤醒,防漏看门狗 */
    } else {
        rtc_set_alarm_ms(next_event - now_tick());
    }

    system_clock_restore_prepare();
    enter_stop2_with_wakeup_sources();

    system_clock_restore();
    scheduler_dispatch_due_tasks();
}

另外两个实用技巧:

  1. 把零散任务合并到同一次唤醒里做。比如 RTC 校时顺便读一下电池电压,多读一次 ADC 几乎不增加唤醒开销。
  1. UART 打印只在调试态开。一条 115200 的日志输出 100 字节要占用 CPU 近 10ms,量产版务必裁掉,或者改成进入低功耗前把日志缓存在 RAM、攒够一批再一次性输出。

四、外围电路:MCU 睡了,外设也得睡

软件抠到极致后,整机还有 30μA 左右。继续往下挖,发现两处硬伤:

传感器偏置常开。 某气体传感器需要 1.5mA 的加热偏置,原设计直接常供。改成一个 PMOS 开关 + 10kΩ 下拉,MCU 用一个 GPIO 控制,采样前 200ms 预热、采完断电:


#define SENSOR_PWR_ON()   HAL_GPIO_WritePin(SENSOR_PWR_GPIO, SENSOR_PWR_PIN, GPIO_PIN_SET)
#define SENSOR_PWR_OFF()  HAL_GPIO_WritePin(SENSOR_PWR_GPIO, SENSOR_PWR_PIN, GPIO_PIN_RESET)

注意 PMOS 选型要关注栅极阈值电压,3.3V 系统选 Vth 在 1V 左右的型号保证完全导通,否则导通电阻大,传感器读数会漂。

电源树要有”睡眠开关”。 NB-IoT 模块睡眠后仍有 20~50μA 的 PSM 电流,这个没办法(协议要求保持网络注册),但如果模块支持 PSM/eDRX,要确认它真的进去了——我们一度发现模块 AT 命令发了 PSM 配置但没生效,模块一直处于浅睡,白白多耗 1mA。抓模块日志、查 +CEREG 状态才定位到是网络侧没下发 PSM 参数,最终联系运营商解决。

五、测量方法:静态电流好测,动态电流难测

最后说说测量,这一步做不好,前面全白搭:

  • 静态/待机电流:高精度万用表(μA 档)直接串入即可。
  • 动态电流:示波器 + 小阻值采样电阻(0.5~1Ω)看波形,重点抓发射峰值和持续时间。
  • 长期能耗:最靠谱的是用 Joulwatt/Passive 专用的功耗分析仪(如 Nordic PPK2、Joulescope),能以 μA 级分辨率长时间记录电流曲线,算出真实的平均电流——水表这类”一天醒几次”的负载,平均值才是续航的关键。

一个常用估算:电池容量 2000mAh,平均电流 40μA,理论上 50000 小时 ≈ 5.7 年,再打上自放电(锂亚电池每年约 1~2%)和低温折扣,8 年目标是够的,但要留出 30% 余量做设计。

六、 checklist:低功耗设计的 12 条军规

  1. 先测量再优化,量整机不量芯片;
  1. 优先选 Stop/Standby 级别低功耗模式,Sleep 只用于极短空闲;
  1. 休眠前所有未用 GPIO 置模拟输入;
  1. 唤醒后第一时间恢复系统时钟;
  1. 统一调度器 + RTC 闹钟,消灭碎碎念定时器;
  1. 调试打印量产必裁;
  1. 大电流外设加电源开关,用完即断;
  1. 悬空 IO、上拉电阻网络逐个核算漏电;
  1. LDO/DCDC 选低静态电流型号(<1μA),重负载场景 DCDC 效率更高;
  1. 无线模块确认 PSM/eDRX 真正生效;
  1. 电池容量估算留 30% 以上余量;
  1. 全温区复测,低温下电池内阻上升会显著影响大电流脉冲。

低功耗没有一招制敌的银弹,它是一个”测量 → 定位 → 修改 → 复测”循环往复的过程。把每个 μA 的来龙去脉搞清楚,续航指标才真正攥在自己手里。