4. 中断使能与屏蔽:全局中断使能(CPSIE/CPSID)、外设中断使能寄存器、中断屏蔽寄存器(BASEPRI)

好,咱们接着聊中断系统的核心操作——使能与屏蔽。

很多人刚开始学嵌入式时,觉得中断控制就是“开一下、关一下”的事。其实没那么简单。你想想看,一个游戏机里同时跑着音频、视频、手柄输入、定时器……每个外设都可能产生中断。如果控制不好,系统随时会崩。

我个人习惯把中断控制分成三个层次:全局开关、外设开关、优先级屏蔽。咱们一层层拆开讲。

4.1 全局中断使能:CPSIE 与 CPSID

这是最粗暴的一层控制。说白了,就是整个CPU的“总电闸”。

在ARM Cortex-M系列里,我们用两条汇编指令搞定:

  • CPSIE I —— 开全局中断(Clear-Processor-State Interrupt Enable)
  • CPSID I —— 关全局中断(Clear-Processor-State Interrupt Disable)

代码长这样:

// 开全局中断
__asm volatile("cpsie i" : : : "memory");

// 关全局中断
__asm volatile("cpsid i" : : : "memory");

注意那个 "memory" 约束。嗯,这里要提醒一下:这是告诉编译器,这段汇编会修改内存,防止它乱优化。我在项目中见过有人漏掉这个,结果中断开关前后变量值对不上,查了两天才找到原因。

警告:全局关中断要快进快出。我曾经在一个音频驱动里关了全局中断做DMA配置,结果关了200微秒没开,音频输出直接卡顿。游戏机里画面掉帧、声音爆音,往往就是这么来的。

什么时候用全局开关?我个人经验是:保护极短的关键代码段。比如操作一个被中断和主循环共享的链表,用全局关中断包一下,几微秒完事。

4.2 外设中断使能寄存器

全局开关太粗暴了。很多时候我们只想让某个外设闭嘴,而不是把整个系统的中断都关了。

每个外设都有自己的中断使能寄存器。比如STM32的定时器,有个 TIMx_DIER 寄存器,里面每个位控制一种中断源:

名称 功能
0 UIE 更新中断使能(定时器溢出)
1 CC1IE 捕获/比较通道1中断使能
2 CC2IE 捕获/比较通道2中断使能
6 TIE 触发中断使能

代码示例:

// 使能定时器2的更新中断
TIM2->DIER |= TIM_DIER_UIE;

// 关闭定时器2的更新中断
TIM2->DIER &= ~TIM_DIER_UIE;

这里有个坑:外设中断使能只是“第一道门”。你开了外设的中断,还得在NVIC(嵌套向量中断控制器)里使能对应的中断通道。两道门都开了,中断才能进来。

NVIC使能代码:

NVIC_EnableIRQ(TIM2_IRQn);   // 使能TIM2的中断通道
NVIC_DisableIRQ(TIM2_IRQn);  // 禁用TIM2的中断通道

我刚开始做游戏机项目时,就犯过这个错:外设中断使能位设好了,NVIC没开,折腾了半天中断就是不触发。后来用调试器看寄存器才发现NVIC里对应位是0。你说冤不冤?

4.3 中断屏蔽寄存器:BASEPRI

这是Cortex-M系列里一个非常实用的寄存器。它不像全局开关那样一刀切,也不像外设使能那样逐个控制。它的作用是:屏蔽优先级低于某个值的中断

说白了,就是“我只让高优先级的中断进来,低优先级的先等着”。

BASEPRI寄存器的工作原理:

  • 写入一个优先级数值N
  • 所有优先级数值 大于等于 N 的中断都被屏蔽
  • 优先级数值 小于 N 的中断照常响应

注意:Cortex-M的优先级数值越小,优先级越高。所以BASEPRI设得越大,屏蔽的中断越多。

代码示例:

// 屏蔽优先级大于等于2的所有中断(保留优先级0和1的中断)
__asm volatile("msr basepri, %0" : : "r" (2) : "memory");

// 取消BASEPRI屏蔽(允许所有中断)
__asm volatile("msr basepri, %0" : : "r" (0) : "memory");
核心技巧:BASEPRI是实时系统的利器。比如在游戏机的垂直消隐期(VBlank),你只想让视频同步中断进来,其他音频、输入的中断统统靠边站。设个BASEPRI值,干净利落。

我在一个掌机项目里用过这招。当时有个问题:手柄扫描中断优先级设得偏高,结果每次按按键都会打断音频DMA的传输,导致声音有杂音。解决方案不是改优先级(因为其他模块依赖这个优先级顺序),而是在音频DMA传输的关键时刻,用BASEPRI临时屏蔽掉手柄中断。传输完再恢复。完美解决。

4.4 三个层次的配合使用

实际项目中,这三个层次要灵活搭配。我总结了一个经验表格:

场景 推荐做法 原因
保护极短的关键代码(<1μs) CPSID/CPSIE 开销最小,但影响全局
临时关闭某个外设中断 外设中断使能寄存器 精准控制,不影响其他中断
保留高优先级中断,屏蔽低优先级 BASEPRI 灵活,适合实时系统
初始化阶段 先全局关中断,配好外设和NVIC,再全局开 防止配置过程中产生意外中断
个人习惯:我写中断控制代码时,一定会加注释说明“为什么用这个层次”。比如:
// 用BASEPRI而不是CPSID,因为要保留定时器中断响应
这样三个月后回来看代码,自己还能看懂。

4.5 避坑指南

最后分享几个我踩过的坑:

  • 中断嵌套死锁:我曾经在低优先级中断里用CPSID关全局中断,结果高优先级中断来了,CPU不响应。等低优先级中断退出时,全局中断开了,但高优先级中断已经错过了。解决方案:用BASEPRI代替CPSID。
  • 外设中断使能位未清除:有个项目,外设产生了中断,但ISR里只清除了NVIC的中断挂起位,没清除外设自己的中断标志。结果中断反复触发,系统卡死。记住:外设中断标志位一定要在ISR里清除。
  • BASEPRI值写错:Cortex-M的优先级位数因芯片而异。有的芯片只用高4位,低4位忽略。如果你写了个0x10,实际效果可能和0x01一样。查芯片手册的优先级分组配置。

嗯,中断使能与屏蔽这块,说白了就是“什么时候让谁进来”的问题。全局开关是铁闸门,外设使能是每个房间的门禁,BASEPRI是VIP通道的保安。三个配合好,你的游戏机中断系统就稳了。

下一节咱们聊聊中断优先级分组和抢占——那才是真正体现架构设计水平的地方。