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" 约束。嗯,这里要提醒一下:这是告诉编译器,这段汇编会修改内存,防止它乱优化。我在项目中见过有人漏掉这个,结果中断开关前后变量值对不上,查了两天才找到原因。
什么时候用全局开关?我个人经验是:保护极短的关键代码段。比如操作一个被中断和主循环共享的链表,用全局关中断包一下,几微秒完事。
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");
我在一个掌机项目里用过这招。当时有个问题:手柄扫描中断优先级设得偏高,结果每次按按键都会打断音频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通道的保安。三个配合好,你的游戏机中断系统就稳了。
下一节咱们聊聊中断优先级分组和抢占——那才是真正体现架构设计水平的地方。