榨干最后一微安:STM32 的低功耗设计与中断唤醒机制深度剖析

1. 背景

在进行低功耗架构设计时,系统理论上在 Stop 2 模式下的静态电流应控制在 50uA 以下。然而,实测时电流竟然高达1.1MA左右。

1.1 硬件排查

怀疑是外围电路存在漏电。结合原理图,将矛头指向了两个按键检测电路。

  • 实验 1(断开测试): 拆除按键、滤波电容及上拉/限流电阻。功耗暴跌1.1MA ,回落到了理想状态。

  • 实验 2( 交叉验证 ): 重新焊回电容,功耗未变;但焊回电阻后,功耗再次飙升

  • 实验 3(极限飞线): 尝试用飞线直接短路电阻,功耗依然未降。

难道是按键本身存在绝缘电阻过低导致的漏电?查阅元器件 Datasheet 确认其绝缘内阻在兆欧级别,绝不可能贡献毫安级的电流。此时硬件排查陷入僵局------拆电阻功耗就跌,装电阻功耗就涨,但电阻和按键本身又没有质量问题。

由以上推论,并非硬件漏电的问题,开始将目光转向软硬件 耦合 处的控制状态

1.2 软件追凶

既然硬件通路无误,那么唯一的可能就是:GPIO 引脚上的电平状态,触发了板卡某种持续耗电机制,甚至阻止了 MCU 真正进入 Stop 2 模式。

顺着这个思路,逐步复盘软件行为:

  1. 按键在进入stop2模式前,均配置为 EXTI 外部中断输入。联系到硬件排查中"拆除按键器件后功耗下降"的现象,推测根因为:EXTI 中断未被完全屏蔽,导致 MCU 无法完全进入低功耗状态,持续维持在较高功耗的唤醒待机状态。

  2. 查阅 STM32 参考手册(RM),重点核查 Stop2 模式的进入条件与中断屏蔽要求。手册明确说明:若 EXTI 中断屏蔽寄存器(IMR1)中对应位未清除,相关中断线仍处于使能状态,MCU 将无法完全进入 Stop2 模式。

  3. 审查进入低功耗前的初始化代码,发现中断屏蔽寄存器 EXTI→IMR1 中,按键对应的 bit0 与 bit1 未被清零,EXTI 通道始终处于开放状态。

1.3 代码修复

在进入 Stop2 模式前,显式清除对应 EXTI 中断屏蔽位:

cpp 复制代码
/* 进入 Stop2 前,屏蔽按键 EXTI 中断线(PX0 / PX1),防止阻塞低功耗入口 */
CLEAR_BIT(EXTI->IMR1, (1U << 0) | (1U << 1));    /* K1=line0, K2=line1 */

2. STM32的中断唤醒机制

  • NVIC(Cortex-M 标准):管的是"RUN 模式下 ISR 要不要跳"。

  • EXTI IMR1(STM32 私有):管的是"边沿要不要送往 PWR、用于把 MCU 从 STOP 抬醒"。

这两条路径是并联的,互相不影响。

3. 陷阱记录

MX_GPIO_Init 把 K1/K2 配成 GPIO_MODE_IT_RISING_FALLING 的时候,HAL 一次性设了:

  • EXTI->IMR1 |= (1<<0) | (1<<1)

  • EXTI->RTSR1 |= (1<<0) | (1<<1)

  • EXTI->FTSR1 |= (1<<0) | (1<<1)

  • NVIC_ISER 使能 EXTI0_IRQn / EXTI1_IRQn

进 STOP2 前的清理只关了 NVIC:

HAL_NVIC_DisableIRQ(EXTI0_IRQn);

HAL_NVIC_DisableIRQ(EXTI1_IRQn);

IMR1、RTSR1、FTSR1 三个寄存器原样保留。EXTI → PWR/WIC 那条唤醒通路还是开着的。

进 STOP2 前,把 K1/K2 从 input 切到 analog。在硬件层面:

cpp 复制代码
 切换前(input):  EXTI line 输入 = pin 实际电平 = HIGH(上拉电路)
                                  │
                                  │ ← MODER 写 0b11,input buffer 关
                                  │
 切换后(analog): EXTI line 输入 = 强制 0                ← 这是 HIGH→LOW

这一瞬间,EXTI line 0/1 输入从 1 跳到 0,是一次下降沿FTSR1 开着,EXTI 把它当真实下降沿锁进 PR1。再走 IMR1 那条路(还开着)送到 PWR/WIC,于是:

cpp 复制代码
 进 STOP2 的 WFI → 立刻被 pending wake-up 弹出 → 回到 RUN
                ↑                                  │
                │                                  ▼
                │             Power_Mode_OnStopWakeup(重配时钟、重启 LPTIM)
                │                                  │
                │                                  ▼
                └──── 主循环跑一轮 → 再进 STOP2 ────┘

MCU 进入 "假 STOP2" 循环:每次 STOP2 只待几十微秒就被自己产生的伪边沿弹出。

4. 进入 STOP 模式中断配置必做清单

cpp 复制代码
/**
 * @brief 注销除 K3 外的 EXTI 与 NVIC
 */
static void EXTI_Suspend_NonK3_WakeSources(void)
{
    /* 关掉 NVIC */
    HAL_NVIC_DisableIRQ(EXTI0_IRQn);
    HAL_NVIC_DisableIRQ(EXTI1_IRQn);

    /* 关闭 K1/K2 的 EXTI 唤醒掩码:进 STOP2 不允许 PB0/PB1 边沿唤醒。 
    * 单纯 NVIC_DisableIRQ 不够------STOP 模式下唤醒由 IMR1 决定,不是 NVIC。 */
    CLEAR_BIT(EXTI->IMR1, (1U << 0) | (1U << 1));   /* K1=line0, K2=line1 */

    /* 清除中断标志 */
    __HAL_GPIO_EXTI_CLEAR_IT(K1_Pin | K2_Pin | K3_Pin);

    /* 保留需要的中断 */
    NVIC_ClearPendingIRQ(EXTI2_IRQn);
    HAL_NVIC_EnableIRQ(EXTI2_IRQn);
}

5. 名词解释

5.1 EXTI(External Interrupt /Event Controller ,外部中断/事件 控制器

监测单片机 GPIO 引脚上的电平变化,并在检测到特定变化时,立刻通知 CPU 做出响应,或者直接触发某个硬件联动。

5.2 ISR Interrupt Service Routine 中断服务程序 或中断服务 函数

如果说 EXTI(外部中断) 是单片机的"门铃",那么 ISR 就是你去开门并拿外卖的"具体动作"。它是单片机内核在接收到中断请求时,强制暂停当前正在执行的主程序,转而执行的一段特定的、具有最高优先级的短代码。

5.3 PWR(Power Control,电源控制/电源管理器)

监控供电质量("防低血糖"与"安全带")

芯片的供电电压必须稳定,忽高忽低会导致内部逻辑混乱。PWR 内部集成了几个非常重要的电压监控器:

  • POR/PDR(上电/掉电复位): 当你刚插上电池,电压从 0V 往上涨,或者拔掉电池电压往下跌时,硬件会自动触发复位,防止芯片在半通电、电压不稳的状态下"跑飞"写坏内部 Flash。

  • BOR(欠压复位): 保证系统必须在安全的电压阈值以上才能运行,低于这个值直接强制芯片复位。

  • PVD(可编程电压检测器): 这是一个非常有用的软件预警机制。比如你的电池快没电了,电压跌到了 2.2V,PVD 会立刻触发一个中断通知 CPU:"主人,电池快枯竭了!" CPU 可以在 ISR 里赶紧把核心数据保存到 EEPROM 闪存中,并做最后的优雅关机,避免数据丢失。

控制"电源域"(Power Domains)的分断

单片机内部被 PWR 划分成了不同的物理电源区域。在需要省电时,PWR 可以像拉闸断电一样,把不需要的区域完全断电:

  • 备份域(Backup Domain): 通常由纽扣电池(VBAT)供电,里面包含 RTC(硬件时钟) 和一些备份寄存器。即使单片机主电池没电了,只要备份域有电,时钟就永远在走,数据就不会丢。

  • 核心域(VCORE Domain): 包含 CPU 内核、SRAM 内存和大部分数字外设。

  • 闪存域(Flash Memory): 存储代码的地方。

  1. 管理"低功耗模式"(Low-Power Modes)

这是工程师最常跟 PWR 打交道的功能。PWR 通过切断时钟或关闭电源域,为单片机配置了不同的低功耗模式:

  • Sleep(睡眠模式): 仅仅关闭 CPU 时钟。外设还能用。功耗从 mA 级降到几百 uA,任何中断都能瞬间唤醒。

  • Stop(停止模式): 关闭所有总线时钟,1.2V 核心电源域不关但断开时钟。此时 CPU 和外设全部定格。功耗直奔 uA 级别。只能通过外部中断(EXTI)或低功耗定时器(LPTIM)唤醒。

  • Standby / Shutdown(待机/关机模式): 把除了备份域(RTC)以外的所有核心电源全断掉,SRAM 里的内存数据彻底丢失。功耗压榨到 nA(纳安)级。唤醒后相当于单片机重新彻底重启(Reset)。

5.4 WIC(Wakeup Interrupt Controller,唤醒中断控制器)

WIC 的核心存在意义

在没有 WIC 之前,单片机如果想被中断唤醒,NVIC必须一直保持通电和时钟开启。因为 NVIC 属于内核结构,这意味着内核的一部分无法彻底进入最省电的停机状态。

有了 WIC 之后:

  1. 深度断电成为可能: 当单片机通过执行 WFI(Wait For Interrupt)指令进入Stop 模式时,内核(包含 CPU、NVIC、SysTick 等)的时钟可以全部关闭,甚至把 VCORE 电压降到最低。

  2. 硬件自动交接: 在内核闭眼睡觉的前一指令周期,NVIC 会把当前使能的中断信号控制权,自动同步/镜像给 WIC。随后内核彻底断电停摆。

  3. 极低功耗监听: WIC 是一个纯硬件的组合逻辑电路,它不需要任何时钟驱动(0Hz 运行),只消耗微弱的静态电流(纳安 nA 级别)。

WIC 的工作流程

当单片机处于 Stop 模式,外部发生了一个中断:

  1. 信号捕获: 引脚的电平跳变穿过外部电路,直接触发了 WIC 的监测线。

  2. 硬件唤醒(Wakeup Signal): WIC 识别到这是一个被允许的唤醒源,立刻向单片机的 PWR(电源控制器) 发出硬件信号:"快送电,有紧急情况!"

  3. 时钟与电源恢复: PWR 立刻拉高内核电压(VCORE),重新启动时钟振荡器(如 MSI),恢复内核总线时钟。

  4. 权力交还: 内核通电复活后,WIC 将控制权重新移交给 NVIC。

  5. 处理中断: 此时醒来的 CPU 并在物理上感知到 NVIC 里的挂起标志,开始保护现场,进入对应的 ISR(中断服务程序) 执行代码。

5.5 WIC、NVIC 与 EXTI 的物理关系

这三个概念经常同时出现,它们在单片机内部各司其职,构成了一条完整的唤醒生物链:

  • EXTI(外部中断): 属于内核 之外的外设。负责把物理引脚上的"电平跳变"变成一个"中断请求信号"。

  • WIC(唤醒 控制器 ): 属于内核 外围的常驻硬件 。负责在内核断电期间 盯着 EXTI 传过来的信号,并负责拉闸送电、唤醒内核

  • NVIC( 向量 中断 控制器 ): 属于内核 内部的核心外设 。负责在内核通电期间,管理成百上千个中断的优先级、排队、并指引 CPU 跳转到哪个 ISR 代码去运行。

cpp 复制代码
/**
* @brief 外部中断/事件控制器
*/

typedef struct
{
  __IO uint32_t IMR1;        /*!< EXTI 中断屏蔽寄存器 1,            Address offset: 0x00 */
  __IO uint32_t EMR1;        /*!< EXTI 事件屏蔽寄存器 1,            Address offset: 0x04 */
  __IO uint32_t RTSR1;       /*!< EXTI 上升沿触发选择寄存器 1,       Address offset: 0x08 */
  __IO uint32_t FTSR1;       /*!< EXTI 下降沿触发选择寄存器 1,      Address offset: 0x0C */
  __IO uint32_t SWIER1;      /*!< EXTI 软件触发中断/事件寄存器 1,       Address offset: 0x10 */
  __IO uint32_t PR1;         /*!< EXTI 挂起寄存器 1,                Address offset: 0x14 */
  uint32_t      RESERVED1;   /*!< 保留, 0x18                                                   */
  uint32_t      RESERVED2;   /*!< 保留, 0x1C                                                   */
  __IO uint32_t IMR2;        /*!< EXTI 中断屏蔽寄存器 2             Address offset: 0x20 */
  __IO uint32_t EMR2;        /*!< EXTI 事件屏蔽寄存器 2             Address offset: 0x24 */
  __IO uint32_t RTSR2;       /*!< EXTI 上升沿触发选择寄存器 2,       Address offset: 0x28 */
  __IO uint32_t FTSR2;       /*!< EXTI 下降沿触发选择寄存器 2,      Address offset: 0x2C */
  __IO uint32_t SWIER2;      /*!< EXTI 软件触发中断/事件寄存器 2,       Address offset: 0x30 */
  __IO uint32_t PR2;         /*!< EXTI 挂起寄存器 2,                Address offset: 0x34 */
} EXTI_TypeDef;
相关推荐
梁朝辉3 小时前
STM32H750VBT6中ADCINP与INN什么区别
stm32·单片机·嵌入式硬件
省四收割者4 小时前
一文详解信号完整性(1)
python·嵌入式硬件·数学建模·信息与通信·信号处理·智能硬件
崇山峻岭之间5 小时前
单片机无感FOC驱动之ADC
单片机·嵌入式硬件
Ligocious5 小时前
1.点亮一颗小小的LED
单片机·嵌入式硬件
KaifuZeng9 小时前
通信与接口协议面试八、CAN通信
单片机·嵌入式硬件·面试·通信与接口协议
智源单片机设计10 小时前
基于单片机的直流电机双闭环调速系统设计
单片机·嵌入式硬件
KaifuZeng1 天前
通信与接口协议面试一、基本概念
嵌入式硬件·通信与接口协议
wuyk5551 天前
24. C 语言模块化:不是拆几个.c 文件那么简单
c语言·开发语言·stm32·单片机
☆cwlulu1 天前
调试排查工具介绍(gdb、strace、Valgrind等)
开发语言·c++·嵌入式硬件·ubuntu
lzqrzpt1 天前
LED驱动电源选型标准与工程应用技术要点解析
python·单片机·嵌入式硬件·物联网