唤醒源与中断机制:外部中断、RTC闹钟、定时器唤醒、串口唤醒
各位做低功耗的朋友,咱们今天聊聊唤醒源。说白了,就是怎么把睡着的设备叫醒。
我刚开始做低功耗项目时,踩过一个坑——设备休眠后死活唤不醒。后来发现是中断配置错了。嗯,从那以后我对唤醒源这块就特别较真。
唤醒源的四种基本类型
低功耗设备常用的唤醒源,我归纳为四类。每类都有自己的脾气。
| 唤醒源类型 | 触发方式 | 典型功耗 | 响应速度 |
|---|---|---|---|
| 外部中断 | GPIO电平变化 | nA级 | μs级 |
| RTC闹钟 | 定时触发 | μA级 | ms级 |
| 定时器唤醒 | 内部计时 | μA级 | μs级 |
| 串口唤醒 | 数据接收 | μA级 | ms级 |
外部中断:最直接的唤醒方式
外部中断是唤醒的「急先锋」。按键按下、传感器触发、通信请求,都能用它。
我个人习惯把外部中断引脚配置为下降沿触发。为什么?因为上升沿容易受噪声干扰。我在项目中遇到过,用上升沿触发时,设备莫名其妙被唤醒,一查是电源纹波在作怪。
关键配置要点:
- 必须选择支持唤醒的中断引脚(不是所有GPIO都能唤醒)
- 使能GPIO中断,并设置为唤醒源
- 配置内部上拉/下拉,防止浮空误触发
// 外部中断唤醒配置示例(STM32L0系列)
void EXTI_WakeUp_Config(void)
{
// 使能GPIO时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
// 配置PA0为输入,上拉
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 使能EXTI中断
HAL_NVIC_SetPriority(EXTI0_1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI0_1_IRQn);
// 设置为唤醒源
HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1);
}
我曾经踩过的坑:外部中断唤醒后,一定要在中断服务函数里清除中断标志位。否则设备会陷入「唤醒→进中断→不清标志→再次唤醒」的死循环。电池半天就耗光了。
RTC闹钟:定时唤醒的王者
RTC闹钟适合做周期性任务。比如传感器每10分钟采集一次数据,或者设备每天凌晨上报一次状态。
你想想看,RTC在休眠时还在跑,功耗只有几百纳安。这性价比,没谁了。
我的经验:RTC闹钟可以设置多个匹配条件——年、月、日、时、分、秒。但别设太复杂,匹配条件越多,功耗越高。我一般只用「分+秒」匹配,够用了。
// RTC闹钟唤醒配置
void RTC_Alarm_Config(void)
{
RTC_AlarmTypeDef sAlarm = {0};
// 设置闹钟时间为 30秒后
sAlarm.AlarmTime.Hours = 0;
sAlarm.AlarmTime.Minutes = 0;
sAlarm.AlarmTime.Seconds = 30;
sAlarm.AlarmTime.SubSeconds = 0;
// 匹配模式:只匹配秒
sAlarm.AlarmMask = RTC_ALARMMASK_DATEWEEKDAY |
RTC_ALARMMASK_HOURS |
RTC_ALARMMASK_MINUTES;
// 使能闹钟
HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BIN);
// 配置中断优先级
HAL_NVIC_SetPriority(RTC_Alarm_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(RTC_Alarm_IRQn);
}
定时器唤醒:灵活但费电
定时器唤醒和RTC闹钟有点像,但有个关键区别——定时器在休眠时通常不工作。除非你用的是低功耗定时器(LPTIM)。
为什么我这么说?因为普通定时器在深度睡眠时会停摆。只有LPTIM这类专门为低功耗设计的定时器,才能在休眠时继续计数。
适用场景对比:
- RTC闹钟:长时间间隔(秒级到天级),功耗极低
- LPTIM:短时间间隔(微秒到秒级),精度更高
- 普通定时器:只能在浅睡眠模式下使用
串口唤醒:通信驱动的唤醒
串口唤醒是个好东西。设备休眠时,串口线上有数据过来,自动唤醒。
我在项目中做过一个无线传感器节点,平时深度睡眠,主机通过串口发一个字节就能唤醒它。功耗从mA级降到μA级,电池寿命从3个月延长到2年。
但要注意,串口唤醒有个前提——串口接收引脚必须支持唤醒功能。不是所有MCU的UART都能做到。
// 串口唤醒配置(以USART为例)
void USART_WakeUp_Config(void)
{
// 使能USART时钟
__HAL_RCC_USART1_CLK_ENABLE();
// 配置USART引脚
// ...(GPIO配置略)
// 使能USART唤醒功能
USART_WakeUpTypeDef WakeUpStruct;
WakeUpStruct.WakeUpEvent = USART_WAKEUP_ON_START_BIT;
HAL_USARTEx_WakeUpConfig(&husart1, &WakeUpStruct);
// 使能唤醒中断
HAL_NVIC_SetPriority(USART1_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
// 进入休眠前使能唤醒
HAL_USARTEx_EnableWakeUp(&husart1);
}
注意:串口唤醒的代价是,接收引脚需要保持供电。有些MCU在深度睡眠时会关掉IO电源,这时候串口唤醒就失效了。选型时一定要看数据手册的「Wake-up from Stop mode」章节。
中断优先级:别让优先级坑了你
中断优先级配置,看着简单,其实门道很多。
我见过一个案例:同事把RTC闹钟中断优先级设得比外部中断还高。结果外部中断来了,RTC闹钟正好也触发,高优先级的RTC把外部中断堵住了。设备该响应的没响应,用户投诉说按键失灵。
我的建议是:
- 紧急事件(外部中断):优先级最高(数值最小)
- 定时任务(RTC/LPTIM):中等优先级
- 通信唤醒(串口):较低优先级
为什么这么排?因为外部中断通常是用户交互或紧急告警,不能丢。定时任务晚几十毫秒没关系。串口唤醒嘛,数据来了先收着,处理可以稍后。
我个人习惯:在Cortex-M系列MCU上,我会用4位抢占优先级,0位子优先级。这样优先级分组清晰,不容易乱。配置一次,所有项目通用。
多唤醒源协同:别打架
实际项目中,很少只用一种唤醒源。往往是外部中断+RTC闹钟+串口唤醒一起上。
这时候就要注意了——多个唤醒源同时触发怎么办?
我的做法是:在中断服务函数里设置标志位,主循环里统一处理。不要在中断里做复杂操作。
// 多唤醒源标志位处理
volatile uint32_t wakeup_flags = 0;
void EXTI0_IRQHandler(void)
{
if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET)
{
wakeup_flags |= WAKEUP_EXTI;
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
}
}
void RTC_Alarm_IRQHandler(void)
{
wakeup_flags |= WAKEUP_RTC;
HAL_RTC_AlarmIRQHandler(&hrtc);
}
void USART1_IRQHandler(void)
{
wakeup_flags |= WAKEUP_UART;
HAL_USART_IRQHandler(&husart1);
}
// 主循环中处理
while(1)
{
if(wakeup_flags & WAKEUP_EXTI)
{
// 处理外部中断事件
wakeup_flags &= ~WAKEUP_EXTI;
}
if(wakeup_flags & WAKEUP_RTC)
{
// 处理定时任务
wakeup_flags &= ~WAKEUP_RTC;
}
// ... 进入休眠
}
核心原则:中断只做最轻量级的工作——清标志、设标志。真正的业务逻辑放到主循环里。这样既不会丢失中断,也不会让中断嵌套搞乱优先级。
知识体系总览
下面这张图,是我梳理的唤醒源与中断机制的整体框架。你可以把它当作设计时的检查清单。
这张图把唤醒源分成了四个分支,每个分支下面有它的核心特点。最下面是中断优先级配置原则。你设计的时候,对着这张图捋一遍,基本不会漏东西。
好了,唤醒源和中断机制就聊到这儿。记住一句话:唤醒源选对了,功耗就降了一半;中断优先级配好了,系统就稳了一半。剩下的,就是动手调了。