2. MCU低功耗模式选型与切换策略

任务定义

设计并实现一个面向电池供电设备的MCU低功耗模式选型与切换策略框架。本案例以STM32L0系列(典型Cortex-M0+内核)为硬件参考,完成以下目标:

  1. 建立低功耗模式选型决策表,明确不同工作阶段应进入何种低功耗模式;
  2. 实现可复用的模式切换驱动,支持RUN、SLEEP、STOP、STANDBY四种状态间的受控迁移;
  3. 提供唤醒源配置与唤醒后恢复机制,确保外设状态一致;
  4. 给出功耗验证方法,通过实际测量判定切换策略是否有效。

低功耗模式选型决策

STM32L0提供多种低功耗模式,选型依据是:唤醒延迟、可保留RAM/寄存器、可用外设、唤醒源类型。本案例采用如下决策策略:

工作阶段空闲时长选用模式保留资源唤醒源唤醒延迟
传感器采样间隔< 1msSLEEP全部任意中断~2μs
等待RTC周期事件1ms ~ 10sSTOPRAM全部,RTC/GPIORTC闹钟、外部事件~4μs
长时间待机> 10sSTANDBY仅备份域+RTCRTC闹钟、WKUP引脚~60μs(复位启动)

切换策略核心原则:快速事件用SLEEP,周期事件用STOP,深度待机用STANDBY。每次切换前保存必要上下文,唤醒后根据唤醒源决定恢复路径。

RUN 主循环/任务执行 SLEEP WFI,全时钟关 STOP HSI/MSI关,LSE可开 STANDBY 仅备份域+RTC 空闲 <1ms 任意中断唤醒 空闲 1ms~10s,需RTC RTC闹钟/外部事件唤醒 空闲>10s,深度待机 WKUP/RTC唤醒 → 系统复位 迁移条件 正常切换 复位唤醒路径

模式切换驱动实现

以下代码实现低功耗模式切换的核心驱动,包含模式决策函数、唤醒源配置和恢复处理。代码基于STM32L0 HAL库,可直接编译运行于NUCLEO-L053R8开发板。

// lowpower_mgr.h
#ifndef LOWPOWER_MGR_H
#define LOWPOWER_MGR_H

#include "stm32l0xx_hal.h"

typedef enum {
    LP_MODE_RUN = 0,
    LP_MODE_SLEEP,
    LP_MODE_STOP,
    LP_MODE_STANDBY
} LP_Mode_t;

typedef enum {
    WAKEUP_SOURCE_NONE = 0,
    WAKEUP_SOURCE_RTC,
    WAKEUP_SOURCE_EXTI,
    WAKEUP_SOURCE_RESET
} LP_WakeupSource_t;

// 初始化RTC唤醒源(配置为周期闹钟)
void LP_Init_RTC_Wakeup(uint32_t seconds);

// 根据空闲时长决策目标模式
LP_Mode_t LP_DecideMode(uint32_t idle_ms);

// 执行模式切换(含上下文保存与恢复)
void LP_EnterMode(LP_Mode_t mode);

// 获取本次唤醒源
LP_WakeupSource_t LP_GetWakeupSource(void);

// 唤醒后恢复处理(时钟、外设重初始化)
void LP_ResumeAfterWakeup(LP_WakeupSource_t src);

#endif
// lowpower_mgr.c
#include "lowpower_mgr.h"
#include <string.h>

static LP_WakeupSource_t g_wakeup_src = WAKEUP_SOURCE_NONE;
static RTC_HandleTypeDef hrtc;

// 保存RTC闹钟配置(用于STOP模式周期唤醒)
static void RTC_AlarmConfig(uint32_t seconds) {
    hrtc.Instance = RTC;
    hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
    hrtc.Init.AsynchPrediv = 127;
    hrtc.Init.SynchPrediv = 255;
    hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
    hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
    hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;
    HAL_RTC_Init(&hrtc);

    RTC_AlarmTypeDef sAlarm = {0};
    sAlarm.AlarmTime.Seconds = seconds % 60;
    sAlarm.AlarmTime.Minutes = (seconds / 60) % 60;
    sAlarm.AlarmTime.Hours = (seconds / 3600) % 24;
    sAlarm.AlarmMask = RTC_ALARMMASK_DATE_WEEKDAY;
    sAlarm.AlarmSubSecondMask = RTC_ALARMSUBSECONDMASK_ALL;
    sAlarm.Alarm = RTC_ALARM_A;
    HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BIN);
}

void LP_Init_RTC_Wakeup(uint32_t seconds) {
    RTC_AlarmConfig(seconds);
    // 使能RTC闹钟中断(EXTI line 17)
    HAL_NVIC_SetPriority(RTC_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(RTC_IRQn);
}

LP_Mode_t LP_DecideMode(uint32_t idle_ms) {
    if (idle_ms < 1)      return LP_MODE_SLEEP;
    if (idle_ms < 10000)  return LP_MODE_STOP;
    return LP_MODE_STANDBY;
}

void LP_EnterMode(LP_Mode_t mode) {
    switch (mode) {
        case LP_MODE_SLEEP:
            // SLEEP模式:内核停止,外设时钟保持
            HAL_SuspendTick();
            __WFI();
            HAL_ResumeTick();
            g_wakeup_src = WAKEUP_SOURCE_EXTI; // 任意中断唤醒
            break;

        case LP_MODE_STOP: {
            // STOP模式:关闭HSI/MSI,保留RAM
            HAL_SuspendTick();
            // 进入STOP前关闭不需要的外设时钟以降低功耗
            __HAL_RCC_GPIOA_CLK_DISABLE();
            __HAL_RCC_GPIOB_CLK_DISABLE();
            // 配置RTC为唤醒源(已在LP_Init_RTC_Wakeup中完成)
            HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
            // 唤醒后恢复时钟
            SystemClock_Config();  // 重新配置系统时钟
            __HAL_RCC_GPIOA_CLK_ENABLE();
            __HAL_RCC_GPIOB_CLK_ENABLE();
            HAL_ResumeTick();
            g_wakeup_src = WAKEUP_SOURCE_RTC;
            break;
        }

        case LP_MODE_STANDBY:
            // STANDBY模式:仅备份域保留,唤醒后复位
            HAL_SuspendTick();
            // 使能WKUP引脚(PA0)作为备用唤醒源
            HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1);
            __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);
            HAL_PWR_EnterSTANDBYMode();
            // 不会执行到这里,唤醒后系统复位
            break;

        default:
            break;
    }
}

LP_WakeupSource_t LP_GetWakeupSource(void) {
    return g_wakeup_src;
}

void LP_ResumeAfterWakeup(LP_WakeupSource_t src) {
    if (src == WAKEUP_SOURCE_RTC) {
        // RTC唤醒:外设状态已由HAL恢复,重新初始化外设
        MX_GPIO_Init();      // 重新配置GPIO
        MX_USART2_UART_Init(); // 重新配置调试串口
    }
    // 其他唤醒源无需特殊处理
}

// RTC中断回调(在stm32l0xx_it.c中调用)
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc) {
    // 清除闹钟标志
    __HAL_RTC_ALARM_EXTI_CLEAR_FLAG();
}

应用层切换策略示例

// app_main.c 核心逻辑片段
#include "lowpower_mgr.h"

// 模拟传感器采样任务
void Sensor_Task(void) {
    // 读取传感器数据(略)
}

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART2_UART_Init();

    // 配置RTC每5秒唤醒一次(用于周期采样)
    LP_Init_RTC_Wakeup(5);

    while (1) {
        Sensor_Task();  // 执行采样

        // 计算距离下次采样的空闲时间(此处模拟为5000ms)
        uint32_t idle_ms = 5000;

        // 决策并进入低功耗模式
        LP_Mode_t mode = LP_DecideMode(idle_ms);
        LP_EnterMode(mode);

        // 唤醒后恢复
        LP_WakeupSource_t src = LP_GetWakeupSource();
        LP_ResumeAfterWakeup(src);
    }
}

验证方法与判定标准

硬件连接:将NUCLEO-L053R8的IDD测量跳线(JP3)断开,串入高精度万用表(如Keysight 34461A)或使用STM32CubeMonitor-Power工具测量VDD电流。

测试步骤:

  1. 编译烧录后,保持系统在RUN模式运行,记录电流I_RUN(预期约5~10mA,含调试器供电);
  2. 注释掉Sensor_Task()中的实际采样,仅保留空循环,使系统进入STOP模式等待RTC唤醒,记录电流I_STOP;
  3. 将idle_ms改为20000(大于10s),使系统进入STANDBY模式,记录电流I_STANDBY。

判定标准:

  • I_STOP应小于I_RUN的1/100(典型值< 10μA,若调试器仍供电则需断开调试器测量);
  • I_STANDBY应小于I_STOP(典型值< 1μA);
  • 系统每5秒从STOP唤醒执行一次采样,通过串口输出日志确认唤醒周期准确;
  • STANDBY唤醒后系统复位,串口输出启动日志,确认复位路径正常。

注意:测量低功耗电流时必须断开ST-Link调试器(拔掉CN1跳线或使用电池供电),否则调试器电流会淹没被测电流。