第3章 低功耗模式唤醒源:外部中断、RTC闹钟、定时器唤醒的原理与配置
低功耗模式不是睡死过去,而是随时准备醒来。我经常跟学员说,唤醒源才是低功耗设计的灵魂。你让单片机睡下去很容易,但怎么让它在该醒的时候准时醒来,这才是真功夫。
这一章,我们专门聊三种最常用的唤醒方式:外部中断、RTC闹钟、定时器唤醒。它们各有脾气,配置不对,要么醒不来,要么乱醒。
核心观点:低功耗设计的成败,90%取决于唤醒源的配置是否正确。寄存器写错一位,整个系统就废了。
3.1 外部中断唤醒:最直接的叫醒方式
外部中断唤醒,说白了就是用一根引脚的电平变化把MCU从睡梦中拉起来。这是最常用、最直观的唤醒方式。
我记得第一次做低功耗项目时,选的就是外部中断唤醒。按键按下,系统醒来,处理完再睡回去。听起来简单,但坑不少。
3.1.1 原理简述
当MCU进入Stop或Standby模式后,大部分时钟都停了。但外部中断控制器(EXTI)仍然在监听指定的GPIO引脚。一旦检测到设定的边沿(上升沿、下降沿或双边沿),就会产生一个唤醒事件,把MCU从低功耗状态拉回来。
为什么会这样?因为EXTI模块有自己的独立电源域,不依赖主时钟。你想想看,如果连EXTI都睡了,那谁去叫醒CPU?
3.1.2 寄存器配置要点
配置外部中断唤醒,核心就三步。我习惯按这个顺序来,不容易漏:
- 配置GPIO为中断模式——设置输入模式、上下拉、速度
- 配置EXTI寄存器——选择边沿触发方式、使能中断线
- 配置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闹钟)。这时候要注意几个问题:
- 中断优先级管理——不同唤醒源的中断优先级要合理分配,避免优先级反转
- 唤醒源识别——在唤醒后的代码中,要判断是哪个源唤醒的,执行对应的处理逻辑
- 标志位清除顺序——先清除标志位,再处理业务,防止重复进中断
我曾经在一个项目里同时用了外部中断和RTC闹钟,结果发现每次RTC闹钟唤醒后,外部中断标志位也被置位了。查了半天,原来是RTC闹钟事件和EXTI线路有共享的映射关系。嗯,这里要注意,不同MCU的唤醒源映射关系不一样,一定要看数据手册。
个人习惯:我一般在进入低功耗前,用一个全局变量记录当前允许的唤醒源。唤醒后先读这个变量,再判断具体是哪个源触发的。这样代码清晰,不容易出bug。