4. 中断系统与低功耗事件唤醒
在 MCX 系列 MCU 的低功耗设计中,中断系统不只是“响应外设事件”的机制,更是把芯片从 Sleep、Deep Sleep、Power Down 等低功耗状态拉回运行态的唯一常规通道。理解 NVIC 的优先级仲裁、唤醒中断控制器(WIC)与低功耗模式之间的关系,以及 RTC 闹钟唤醒中断服务程序(ISR)的编写方式,是构建“平时极低功耗、事件到来立即响应”终端的关键。
NVIC 中断优先级:抢占与子优先级
MCX 基于 Arm Cortex-M 内核,中断由 NVIC(Nested Vectored Interrupt Controller)统一管理。每个可屏蔽中断都有 8 位优先级寄存器,但芯片通常只实现高若干位。以 4 位优先级为例,可配置 16 个优先级,数值越小优先级越高。这 4 位又按“抢占优先级 + 子优先级”分组,常见分组为 2/2:高 2 位决定抢占优先级,低 2 位决定子优先级。
抢占优先级决定“能否打断正在执行的中断”。若中断 A 的抢占优先级高于 B,A 到来时可以打断 B 的 ISR,形成嵌套;若两者抢占优先级相同,则 B 不会被 A 打断,A 只能等 B 执行完再按子优先级顺序排队。子优先级只在“同时挂起”时决定谁先执行,不影响嵌套。因此,把实时性要求最高的外设(如故障保护、通信接收)放在高抢占优先级,把 RTC 闹钟、按键等对延迟不敏感的事件放在低抢占优先级,是低功耗终端的常见做法。
在 MCX 的 SDK 中,通常通过 NVIC_SetPriority(IRQn, priority) 设置优先级,通过 NVIC_EnableIRQ(IRQn) 使能中断。注意优先级数值与“优先级高低”方向相反,配置时容易写反,建议用宏或注释明确标注。
唤醒中断控制器 WIC 的作用
进入 Deep Sleep 或 Power Down 后,内核时钟可能被关闭,NVIC 本身无法继续工作。此时由 WIC 接管:它把能够在低功耗模式下唤醒芯片的中断源筛选出来,保持对这些中断的监听能力。当唤醒事件到来,WIC 先让芯片恢复时钟与供电,再把中断请求交回 NVIC,由 NVIC 按优先级完成仲裁并跳转到对应 ISR。
因此,一个中断能否唤醒芯片,取决于两点:该中断源是否被配置为唤醒源(在低功耗外设或 WIC 中使能),以及该外设是否在低功耗模式下仍有时钟或异步检测能力。例如 RTC、LPTMR、部分 GPIO 引脚、低功耗 UART 等通常具备唤醒能力,而普通 SPI 在深度低功耗下往往已断电,无法唤醒。
低功耗模式下的中断响应流程
以 Deep Sleep 为例,一次完整的唤醒响应大致经历以下阶段:主循环调用 __WFI() 或进入低功耗 API,内核停止执行;外设(如 RTC)在低功耗域继续计数,到达闹钟时刻产生中断请求;WIC 检测到该请求,触发电源与时钟恢复;内核复位唤醒后,NVIC 读取挂起的中断,按优先级决定是否立即进入 ISR;ISR 执行完毕后返回主循环,主循环根据标志位处理业务,再决定是否重新进入低功耗。
这里有两个容易出错的点。第一,唤醒后必须清除外设的中断标志,否则会反复进入 ISR,导致无法再次休眠。第二,ISR 应尽量短,只做“清标志、置事件标志、必要时记录时间戳”,把耗时处理放到主循环,避免长时间占用高优先级中断、影响其他实时任务。
RTC 闹钟唤醒与 ISR 编写
RTC 是低功耗终端最常用的定时唤醒源。典型配置流程是:初始化 RTC 时钟源(如 32.768 kHz 晶振或内部低功耗振荡器),设置秒/分/时或闹钟寄存器,使能闹钟中断,配置 NVIC 优先级并使能对应 IRQ,最后把 RTC 闹钟设为唤醒源。下面是一段示例代码,用于演示结构,参数为教学假设值,实际应以 MCX SDK 头文件和参考手册为准。
/* 示例:RTC 闹钟每秒唤醒一次(教学假设参数) */
void RTC_Alarm_Init(void)
{
/* 1. 配置 RTC 时钟源为 32.768 kHz */
RTC_EnableClockSource(kRTC_Osc32k);
/* 2. 设置闹钟:当前时间 + 1 秒 */
rtc_datetime_t alarm;
RTC_GetDatetime(&alarm);
alarm.second += 1;
if (alarm.second >= 60) { alarm.second = 0; alarm.minute++; }
RTC_SetAlarm(&alarm);
/* 3. 使能闹钟中断 */
RTC_EnableInterrupts(kRTC_AlarmInterruptEnable);
/* 4. 配置 NVIC:抢占优先级 3,子优先级 0 */
NVIC_SetPriority(RTC_IRQn, NVIC_EncodePriority(2, 3, 0));
NVIC_EnableIRQ(RTC_IRQn);
/* 5. 将 RTC 闹钟设为低功耗唤醒源 */
EnableDeepSleepWakeupSource(kWakeupRTC);
}
/* RTC 中断服务程序:只做最短处理 */
void RTC_IRQHandler(void)
{
if (RTC_GetStatusFlags() & kRTC_AlarmFlag) {
RTC_ClearStatusFlags(kRTC_AlarmFlag); /* 必须清标志 */
g_rtcWakeupFlag = 1; /* 通知主循环 */
}
}
/* 主循环:处理事件后重新休眠 */
int main(void)
{
Board_Init();
RTC_Alarm_Init();
while (1) {
if (g_rtcWakeupFlag) {
g_rtcWakeupFlag = 0;
ProcessPeriodicTask(); /* 采集、上报等业务 */
}
__WFI(); /* 进入低功耗,等待下一次唤醒 */
}
}
这段代码体现了低功耗中断设计的三个要点:ISR 只清标志并置位全局变量;主循环负责真正的业务处理;处理完成后再次执行 __WFI() 回到低功耗。若使用 FreeRTOS 等 RTOS,可在 ISR 中调用 xTaskNotifyFromISR 或 vTaskNotifyGiveFromISR,由任务完成后续处理,避免在中断上下文做复杂操作。
优先级与唤醒源的协同设计
最后需要把优先级和唤醒源放在一起考虑。唤醒源决定“哪些事件能把芯片叫醒”,优先级决定“醒来后谁先执行”。对于同时挂起的多个唤醒事件,NVIC 会按抢占优先级和子优先级排序。如果 RTC 闹钟与通信接收同时到来,而通信接收的抢占优先级更高,则通信 ISR 先执行,RTC 处理稍后完成,这通常是可以接受的;但如果 RTC 承担系统时基,就应给它较高的抢占优先级,避免时间戳被长时间推迟。
实际项目中,建议为每个唤醒源建立一张小表,记录中断源、IRQ 编号、抢占优先级、子优先级、是否使能为唤醒源、ISR 中需要清除的标志。这样在排查“唤不醒”“反复唤醒”“唤醒后卡死”等问题时,可以快速定位是配置遗漏还是优先级冲突。