3. 唤醒源与唤醒机制:外部中断、定时器唤醒、RTC唤醒、比较器唤醒
低功耗设计里,休眠只是手段,唤醒才是目的。你让芯片睡过去,总得有个东西把它叫醒。这个「叫醒」的动作,就是唤醒源和唤醒机制。
我见过不少工程师,功耗降不下去,不是因为休眠没做好,而是唤醒配置出了岔子。要么唤醒不了,要么频繁误唤醒。今天咱们就把四种最常用的唤醒方式掰开揉碎讲清楚。
核心观点:唤醒源的选择,直接决定了系统的响应速度和平均功耗。没有「最好」的唤醒源,只有「最合适」的。
3.1 外部中断唤醒
这是最直接、最常用的唤醒方式。说白了,就是给芯片一根「门铃线」—— 外部电平变化,芯片立刻醒来。
我习惯把外部中断唤醒分成两类:
- 电平触发:高电平或低电平。优点是稳定,缺点是如果电平一直保持,会持续唤醒。
- 边沿触发:上升沿、下降沿或双边沿。适合按键、传感器脉冲这类短暂信号。
我的经验:做产品时,按键唤醒我必用下降沿触发。为什么?因为按键按下瞬间电平跳变,松开时也有跳变。用下降沿只响应按下动作,避免一次按键触发两次唤醒。
代码示例(STM32L0 系列,进入 Stop 模式并配置外部中断唤醒):
// 配置 PA0 为外部中断,下降沿触发
void EXTI_WakeUp_Config(void)
{
// 使能 GPIOA 时钟
RCC->IOPENR |= RCC_IOPENR_GPIOAEN;
// PA0 配置为输入
GPIOA->MODER &= ~(GPIO_MODER_MODE0);
// 选择 PA0 作为 EXTI0 输入
SYSCFG->EXTICR[0] &= ~SYSCFG_EXTICR1_EXTI0;
SYSCFG->EXTICR[0] |= SYSCFG_EXTICR1_EXTI0_PA;
// 下降沿触发
EXTI->FTSR1 |= EXTI_FTSR1_FT0;
// 不屏蔽中断线
EXTI->IMR1 |= EXTI_IMR1_IM0;
// 使能 NVIC 中断
NVIC_EnableIRQ(EXTI0_IRQn);
}
// 进入 Stop 模式
void Enter_StopMode(void)
{
// 设置 SLEEPDEEP 位
SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;
// 选择 Stop 模式(LPMSR 寄存器)
PWR->CR1 |= PWR_CR1_LPMS_STOP0;
// 执行 WFI 指令
__WFI();
}
// 中断服务函数
void EXTI0_IRQHandler(void)
{
if(EXTI->PR1 & EXTI_PR1_PIF0)
{
EXTI->PR1 = EXTI_PR1_PIF0; // 清除标志位
// 处理唤醒事件
}
}
⚠️ 注意:进入休眠前,一定要确保中断标志位已清除。我曾经踩过这个坑 —— 休眠前没清标志,一睡下去立刻被同一个中断唤醒,根本进不了低功耗模式。
3.2 定时器唤醒
需要周期性执行任务的场景,定时器唤醒是首选。比如传感器每隔 100ms 采集一次数据。
定时器唤醒的核心思路:让一个低速定时器在休眠期间继续跑,溢出时产生中断把芯片叫醒。
常用的定时器选择:
| 定时器类型 | 时钟源 | 典型功耗 | 适用场景 |
|---|---|---|---|
| LPTIM(低功耗定时器) | LSI / LSE | ~0.5 µA | 长周期唤醒(秒级) |
| RTC 闹钟 | LSE(32.768 kHz) | ~0.3 µA | 精确时间唤醒 |
| 基本定时器(TIM6/7) | HSI / MSI | ~2 µA | 短周期唤醒(毫秒级) |
我个人习惯:能用 LPTIM 就不用普通定时器。LPTIM 在休眠时依然可以依靠 LSI 时钟运行,功耗极低。普通定时器在 Stop 模式下时钟会被切断,根本跑不起来。
配置 LPTIM 唤醒的要点:
- 选择 LSI 或 LSE 作为时钟源(LSE 更准,LSI 更省)
- 设置合适的预分频和自动重装载值
- 使能 LPTIM 中断并配置为唤醒源
避坑指南:我曾经用 LSI 做 1 秒定时唤醒,结果实测变成 1.3 秒。后来才发现 LSI 的频率误差能达到 ±10%。如果你的应用对时间精度有要求,务必用 LSE 或者做软件校准。
3.3 RTC 唤醒
RTC(实时时钟)唤醒,本质上也是一种定时唤醒,但它更「聪明」。RTC 可以设置闹钟、周期性唤醒、甚至支持「按星期几」唤醒。
RTC 唤醒的三种模式:
- 闹钟匹配唤醒:设定一个具体时间点(比如 08:30:00),到了就唤醒。
- 周期性唤醒:每隔 N 秒/分钟/小时唤醒一次,不需要指定具体时间。
- 时间戳唤醒:检测到外部事件(如 tamper 检测)时记录时间并唤醒。
实际项目中,我用的最多的是周期性唤醒。比如一个环境监测节点,每 10 分钟采集一次数据,用 RTC 的周期性唤醒最合适。
// 配置 RTC 每 10 秒唤醒一次
void RTC_WakeUp_Config(void)
{
// 使能 RTC 时钟
RCC->BDCR |= RCC_BDCR_RTCEN;
// 等待 RTC 同步
while(!(RTC->ISR & RTC_ISR_RSF));
// 进入配置模式
RTC->ISR |= RTC_ISR_INIT;
while(!(RTC->ISR & RTC_ISR_INITF));
// 设置唤醒定时器:10 秒
// 时钟源 LSE (1 Hz),计数器值 = 10 - 1
RTC->CR &= ~RTC_CR_WUCKSEL;
RTC->CR |= RTC_CR_WUCKSEL_0; // 选择 1 Hz 时钟
RTC->WUTR = 9; // 10 - 1
// 退出配置模式
RTC->ISR &= ~RTC_ISR_INIT;
// 使能唤醒中断
RTC->CR |= RTC_CR_WUTIE;
NVIC_EnableIRQ(RTC_WKUP_IRQn);
}
小技巧:RTC 唤醒后,记得在中断里重新配置下一次唤醒时间。如果你用的是「一次性闹钟」,不重新配置的话,下次就不会唤醒了。
3.4 比较器唤醒
比较器唤醒,是模拟世界和数字世界的桥梁。当模拟电压超过或低于某个阈值时,比较器翻转,产生唤醒信号。
这玩意儿在电池供电设备里特别有用。比如:
- 电池电压监测:电压低于 3.0V 时唤醒,提示用户充电。
- 传感器阈值检测:光照强度超过设定值,唤醒系统处理数据。
- 过流保护:电流异常时立即唤醒,执行保护动作。
比较器唤醒的配置要点:
| 参数 | 说明 | 注意事项 |
|---|---|---|
| 输入源选择 | 选择正极和负极输入引脚 | 确保引脚在休眠时保持有效 |
| 阈值设置 | 内部参考电压或外部基准 | 内部参考精度有限,关键应用用外部基准 |
| 迟滞配置 | 防止噪声导致频繁翻转 | 迟滞太小会误唤醒,太大会延迟响应 |
| 输出极性 | 上升沿或下降沿触发唤醒 | 根据应用场景选择 |
⚠️ 重要提醒:比较器本身也会耗电!一个典型的片内比较器功耗在 1~5 µA 之间。如果你对功耗要求极其苛刻(比如 < 1 µA),可以考虑用外部比较器 + 中断引脚的方式,或者用 RTC 定时唤醒后轮询检测。
我记得有个项目,客户要求待机功耗 < 2 µA,但需要实时监测电池电压。用片内比较器的话,光比较器就吃掉 3 µA,直接超标。后来我改用 RTC 每 1 秒唤醒一次,用 ADC 快速采样电压,平均功耗降到了 1.2 µA。你看,有时候「实时」不一定非要「真实时」。
3.5 唤醒源的选择策略
讲了四种唤醒方式,到底怎么选?我总结了一个简单的决策流程:
- 需要立即响应外部事件? → 外部中断唤醒
- 需要周期性执行任务? → 定时器或 RTC 唤醒
- 需要监测模拟信号? → 比较器唤醒
- 需要精确时间点唤醒? → RTC 闹钟唤醒
但实际项目中,往往是多种唤醒源组合使用。比如一个智能门锁:
- 外部中断(按键/刷卡)→ 立即唤醒开锁
- RTC 周期性唤醒 → 每 30 秒检查一次网络心跳
- 比较器唤醒 → 电池电压过低时报警
核心原则:唤醒源的功耗 + 唤醒后的工作时间 × 频率 = 平均功耗。别只盯着唤醒源本身的功耗,要把整个唤醒-工作-休眠周期一起算。
嗯,唤醒源这部分就讲到这里。记住一句话:唤醒不是目的,低功耗地完成工作是目的。选对唤醒源,你的低功耗设计就成功了一半。