第3章 低功耗模式唤醒源:外部中断、RTC闹钟、定时器唤醒的原理与配置

低功耗模式不是睡死过去,而是随时准备醒来。我经常跟学员说,唤醒源才是低功耗设计的灵魂。你让单片机睡下去很容易,但怎么让它在该醒的时候准时醒来,这才是真功夫。

这一章,我们专门聊三种最常用的唤醒方式:外部中断、RTC闹钟、定时器唤醒。它们各有脾气,配置不对,要么醒不来,要么乱醒。

核心观点:低功耗设计的成败,90%取决于唤醒源的配置是否正确。寄存器写错一位,整个系统就废了。

低功耗模式唤醒源体系 唤醒源 外部中断唤醒 RTC闹钟唤醒 定时器唤醒 GPIO边沿触发 RTC计数器匹配 LPTIM/LSE时钟 Stop / Standby Stop / Standby Stop (仅限) 注:不同MCU系列支持的唤醒源组合略有差异,请参考具体数据手册

3.1 外部中断唤醒:最直接的叫醒方式

外部中断唤醒,说白了就是用一根引脚的电平变化把MCU从睡梦中拉起来。这是最常用、最直观的唤醒方式。

我记得第一次做低功耗项目时,选的就是外部中断唤醒。按键按下,系统醒来,处理完再睡回去。听起来简单,但坑不少。

3.1.1 原理简述

当MCU进入Stop或Standby模式后,大部分时钟都停了。但外部中断控制器(EXTI)仍然在监听指定的GPIO引脚。一旦检测到设定的边沿(上升沿、下降沿或双边沿),就会产生一个唤醒事件,把MCU从低功耗状态拉回来。

为什么会这样?因为EXTI模块有自己的独立电源域,不依赖主时钟。你想想看,如果连EXTI都睡了,那谁去叫醒CPU?

3.1.2 寄存器配置要点

配置外部中断唤醒,核心就三步。我习惯按这个顺序来,不容易漏:

  1. 配置GPIO为中断模式——设置输入模式、上下拉、速度
  2. 配置EXTI寄存器——选择边沿触发方式、使能中断线
  3. 配置NVIC——使能对应的中断通道、设置优先级

下面是一个典型的配置代码,以STM32为例:

// 外部中断唤醒配置示例(PA0引脚,下降沿触发)
void EXTI_WakeUp_Init(void)
{
    // 1. 使能GPIO时钟
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
    
    // 2. 配置PA0为输入模式
    GPIOA->MODER &= ~GPIO_MODER_MODER0;  // 00: 输入
    
    // 3. 使能内部上拉(防止浮空误触发)
    GPIOA->PUPDR |= GPIO_PUPDR_PUPDR0_0;  // 01: 上拉
    
    // 4. 将PA0连接到EXTI0
    SYSCFG->EXTICR[0] &= ~SYSCFG_EXTICR1_EXTI0;
    SYSCFG->EXTICR[0] |= SYSCFG_EXTICR1_EXTI0_PA;
    
    // 5. 配置下降沿触发
    EXTI->FTSR |= EXTI_FTSR_TR0;  // 下降沿触发寄存器
    
    // 6. 取消屏蔽中断线0
    EXTI->IMR |= EXTI_IMR_MR0;
    
    // 7. 配置NVIC
    NVIC_SetPriority(EXTI0_IRQn, 0x0F);
    NVIC_EnableIRQ(EXTI0_IRQn);
}

个人经验:我建议在进入低功耗前,先清除一次EXTI挂起标志位。否则刚睡下去就被旧事件唤醒,系统根本进不了低功耗。这个坑我踩过,查了两天才发现。

3.1.3 中断服务函数中的关键操作

唤醒后,中断服务函数里必须做两件事:

  • 清除EXTI挂起标志位——否则会一直进中断
  • 判断唤醒源——如果有多个中断源,要区分是哪个引脚唤醒的
void EXTI0_IRQHandler(void)
{
    // 检查是否是EXTI0触发的
    if(EXTI->PR & EXTI_PR_PR0)
    {
        // 清除挂起标志位(写1清除)
        EXTI->PR = EXTI_PR_PR0;
        
        // 处理唤醒后的业务逻辑
        // ...
    }
}

警告:在Standby模式下,大部分RAM内容会丢失。唤醒后需要重新初始化外设和变量。如果你有重要数据要保留,必须放在备份域(Backup Domain)中。

3.2 RTC闹钟唤醒:定时定点叫醒

RTC闹钟唤醒,说白了就是给MCU设一个闹钟,到点了自动醒。适合那些需要定时采集数据、定时上报的场景。

我曾经做一个环境监测节点,要求每15分钟采集一次温湿度。用RTC闹钟唤醒,MCU睡14分55秒,醒来5秒采集完再睡回去。一节电池撑了两年多。

3.2.1 原理简述

RTC模块有自己的独立时钟源(通常是32.768kHz的LSE晶振),即使主系统时钟停了,RTC仍然在走。RTC闹钟寄存器里存了一个预设的时间值,当RTC计数器与闹钟值匹配时,就会产生一个唤醒事件。

这个事件可以配置为:

  • 直接唤醒MCU(不经过中断)
  • 触发RTC全局中断(经过中断服务函数)

我个人习惯用直接唤醒模式,少一层中断处理,功耗更低。

3.2.2 寄存器配置要点

RTC闹钟配置比外部中断复杂一些,因为涉及写保护和同步问题。RTC寄存器通常有写保护,必须先解锁才能配置。

步骤 操作 寄存器 说明
1 使能RTC时钟 RCC->BDCR LSE振荡器使能,RTC时钟选择
2 退出RTC写保护 RTC->WPR 写入0xCA, 0x53解锁
3 进入初始化模式 RTC->ISR 置位INIT位
4 配置闹钟值 RTC->ALRMAR 设置时、分、秒、日期
5 使能闹钟 RTC->CR 置位ALRAE位
6 退出初始化模式 RTC->ISR 清除INIT位
// RTC闹钟唤醒配置示例(每15分钟唤醒一次)
void RTC_Alarm_WakeUp_Init(void)
{
    // 1. 使能PWR和RTC时钟
    RCC->APB1ENR |= RCC_APB1ENR_PWREN;
    RCC->APB1ENR |= RCC_APB1ENR_RTCEN;
    
    // 2. 允许访问备份域
    PWR->CR |= PWR_CR_DBP;
    
    // 3. 解锁RTC写保护
    RTC->WPR = 0xCA;
    RTC->WPR = 0x53;
    
    // 4. 进入初始化模式
    RTC->ISR |= RTC_ISR_INIT;
    while(!(RTC->ISR & RTC_ISR_INITF));  // 等待初始化完成
    
    // 5. 配置闹钟A:每15分钟触发一次
    // 掩码设置:MSK4=1(忽略日期),MSK3=1(忽略小时)
    RTC->ALRMAR = (0x1 << 12) |  // MSK4: 日期掩码
                  (0x1 << 20) |  // MSK3: 小时掩码
                  (0x0 << 24);  // MSK1: 秒不掩码
    
    // 6. 使能闹钟A
    RTC->CR |= RTC_CR_ALRAE;
    
    // 7. 退出初始化模式
    RTC->ISR &= ~RTC_ISR_INIT;
    
    // 8. 重新使能写保护(可选)
    RTC->WPR = 0xFF;
}

避坑指南:我曾经在配置RTC闹钟时,忘了等待INITF标志位就往下写寄存器,结果闹钟值根本没写进去。RTC的时序要求很严格,每一步都要检查标志位,别偷懒。

3.3 定时器唤醒:灵活的周期性唤醒

定时器唤醒,说白了就是用低功耗定时器(LPTIM)来产生周期性的唤醒信号。相比RTC闹钟,定时器唤醒的灵活性更高,可以设置更短的唤醒周期。

3.3.1 原理简述

大多数MCU都有一个专门的低功耗定时器(LPTIM),它可以在Stop模式下继续运行。LPTIM使用LSE或LSI作为时钟源,计数到预设值后产生比较匹配事件,从而唤醒MCU。

为什么不用普通定时器?因为普通定时器的时钟在Stop模式下就停了。你想想看,定时器都停了,谁去计数?

3.3.2 寄存器配置要点

LPTIM的配置相对简单,但要注意时钟源选择和预分频器设置:

// LPTIM定时器唤醒配置示例(每100ms唤醒一次)
void LPTIM_WakeUp_Init(void)
{
    // 1. 使能LPTIM时钟
    RCC->APB1ENR |= RCC_APB1ENR_LPTIM1EN;
    
    // 2. 选择时钟源为LSE(32.768kHz)
    RCC->BDCR |= RCC_BDCR_LSEON;
    while(!(RCC->BDCR & RCC_BDCR_LSERDY));  // 等待LSE稳定
    
    // 3. 配置LPTIM预分频器(128分频)
    // 32.768kHz / 128 = 256Hz,每个计数周期约3.9ms
    LPTIM1->CFGR = (0x7 << LPTIM_CFGR_PRESC_Pos);  // 128分频
    
    // 4. 设置自动重装载值(256 * 25 = 6400,约100ms)
    LPTIM1->ARR = 25;
    
    // 5. 使能比较匹配中断
    LPTIM1->IER |= LPTIM_IER_ARRMIE;
    
    // 6. 使能LPTIM
    LPTIM1->CR |= LPTIM_CR_ENABLE;
    
    // 7. 配置NVIC
    NVIC_SetPriority(LPTIM1_IRQn, 0x0F);
    NVIC_EnableIRQ(LPTIM1_IRQn);
}

注意:LPTIM的唤醒只适用于Stop模式,不支持Standby模式。如果你需要从Standby模式唤醒,只能用外部中断或RTC闹钟。这个区别很重要,选错了模式,系统就醒不来了。

3.4 三种唤醒源的对比与选型建议

三种唤醒方式各有适用场景。我整理了一个对比表,方便你选型:

特性 外部中断 RTC闹钟 定时器(LPTIM)
触发方式 外部引脚电平变化 时间匹配 定时器计数溢出
适用模式 Stop / Standby Stop / Standby Stop(仅限)
最小唤醒周期 无限制(外部触发) 1秒 几十微秒
功耗 极低(仅监听引脚) 低(RTC持续运行) 低(LPTIM运行)
灵活性 高(任意引脚) 中(固定时间点) 高(可调周期)
典型应用 按键唤醒、传感器触发 定时上报、闹钟 周期性采样、心跳

选型建议:

  • 需要即时响应外部事件(如按键、传感器中断)→ 选外部中断
  • 需要长周期定时唤醒(秒级到天级)→ 选RTC闹钟
  • 需要短周期快速唤醒(毫秒级)→ 选LPTIM定时器

3.5 多唤醒源共存时的注意事项

实际项目中,往往不止一个唤醒源。比如一个智能门锁,既要有按键唤醒(外部中断),又要有定时心跳(RTC闹钟)。这时候要注意几个问题:

  1. 中断优先级管理——不同唤醒源的中断优先级要合理分配,避免优先级反转
  2. 唤醒源识别——在唤醒后的代码中,要判断是哪个源唤醒的,执行对应的处理逻辑
  3. 标志位清除顺序——先清除标志位,再处理业务,防止重复进中断

我曾经在一个项目里同时用了外部中断和RTC闹钟,结果发现每次RTC闹钟唤醒后,外部中断标志位也被置位了。查了半天,原来是RTC闹钟事件和EXTI线路有共享的映射关系。嗯,这里要注意,不同MCU的唤醒源映射关系不一样,一定要看数据手册。

个人习惯:我一般在进入低功耗前,用一个全局变量记录当前允许的唤醒源。唤醒后先读这个变量,再判断具体是哪个源触发的。这样代码清晰,不容易出bug。