4、硬件层面优化(二):中断控制器设计(NVIC、GIC、PLIC)与优先级抢占策略
好,我们接着聊硬件层面的优化。上一讲我们聊了中断源和信号路径,今天重点落在中断控制器上。说白了,中断控制器就是CPU的“秘书”,它决定哪个中断能进、哪个得排队、哪个能插队。
我个人习惯把中断控制器比作一个会议室的门口。NVIC、GIC、PLIC就是三种不同风格的“门卫”。它们的设计哲学完全不同,直接决定了你的系统延迟能做到多低。
4.1 三种主流中断控制器架构
先看个总览表格,心里有个底:
| 特性 | NVIC (Cortex-M) | GIC (Cortex-A) | PLIC (RISC-V) |
|---|---|---|---|
| 中断源数量 | 最多240个 | 最多1020个 | 可配置,通常1024个 |
| 优先级位数 | 8位可配置(实际使用4位常见) | 8位(GICv2)或16位(GICv3) | 可配置,通常7位 |
| 抢占模型 | 硬件原生支持嵌套抢占 | 硬件支持,但需软件配合 | 纯软件管理,硬件只做仲裁 |
| 中断分组 | 抢占优先级+子优先级 | Group优先级+子优先级 | 无分组,单一优先级 |
| 典型延迟 | 12个时钟周期左右 | 几十到上百周期 | 取决于软件处理,通常较慢 |
4.2 NVIC:Cortex-M的硬实时利器
NVIC是我最喜欢的中断控制器,没有之一。为什么?因为它把“确定性”做到了极致。
核心设计:每个中断都有独立的优先级寄存器。硬件自动比较当前中断优先级和待处理中断的优先级。如果新来的中断优先级更高,硬件直接触发抢占——注意,是硬件自动完成的,不需要软件干预。
我在项目中遇到过一个问题:一个电机控制应用,要求PWM中断必须在5微秒内响应。当时用的Cortex-M4,NVIC的抢占优先级设了4位,子优先级设了4位。我把PWM中断的抢占优先级设为0(最高),其他所有中断设为1或更低。结果延迟稳定在1.2微秒以内,非常漂亮。
// 设置PWM中断为最高抢占优先级
NVIC_SetPriority(TIM1_UP_IRQn, 0); // 抢占优先级0,子优先级0
// 设置UART中断为较低优先级
NVIC_SetPriority(USART1_IRQn, 0x30); // 抢占优先级3,子优先级0
// 使能中断
NVIC_EnableIRQ(TIM1_UP_IRQn);
NVIC_EnableIRQ(USART1_IRQn);
嗯,这里要注意:NVIC的优先级分组寄存器(SCB->AIRCR)决定了你使用多少位做抢占优先级、多少位做子优先级。我个人习惯用4位抢占+4位子优先级,这样有16个抢占等级,够用了。
4.3 GIC:多核系统的中断交响乐
GIC是ARM为Cortex-A系列设计的中断控制器。它比NVIC复杂得多,因为要处理多核、虚拟化、安全世界等问题。
GICv2 vs GICv3:
- GICv2:最多8个PE(处理单元),中断ID范围0-1019
- GICv3:最多256个PE,中断ID范围0-65535,支持直接注入中断(LPI)
GIC的抢占策略和NVIC不太一样。GIC支持两种中断模型:
- 1-N模型:一个中断只发给一个CPU。适合确定性要求高的场景。
- N-N模型:一个中断可以发给多个CPU。适合负载均衡场景。
我个人在调试一个4核A72系统时,发现GIC的优先级抢占有个坑:GIC的抢占是在中断确认(IAR寄存器读取)之后才生效的。也就是说,如果两个中断同时到达,CPU先确认一个,然后才会检查另一个的优先级。这会导致一个微小的窗口期——大约几个总线周期。
4.4 PLIC:RISC-V的软件化中断管理
PLIC是RISC-V架构的中断控制器。说实话,我第一次接触PLIC时有点不适应——它把很多硬件该做的事交给了软件。
PLIC的核心设计:
- 每个中断源有一个优先级寄存器(通常0-7,0表示禁用)
- 每个HART(硬件线程)有一个中断使能位图
- 每个HART有一个优先级阈值寄存器
- 中断仲裁:硬件只做简单的优先级比较,不处理抢占
你想想看,这意味着什么?意味着抢占逻辑完全由软件实现。硬件只告诉你“有中断来了”,至于要不要抢占当前中断,那是软件的事。
我在一个RISC-V项目中,需要实现类似NVIC的硬件抢占效果。当时我写了一个中断分发器,在中断入口处检查当前中断优先级是否高于正在执行的中断。如果是,就保存上下文并执行新中断;否则,把新中断挂起。
void plic_handler(void) {
uint32_t irq = PLIC_CLAIM; // 读取当前最高优先级中断
// 检查是否允许抢占
if (irq_priority[irq] > current_priority) {
// 保存当前上下文
save_context();
current_priority = irq_priority[irq];
// 执行中断服务
handle_irq(irq);
// 恢复上下文
current_priority = previous_priority;
restore_context();
} else {
// 挂起中断,稍后处理
pend_irq(irq);
}
PLIC_COMPLETE = irq; // 通知PLIC处理完成
}
这种软件化设计的优点是灵活,缺点是延迟不可控。我曾经测过一个PLIC系统,在最坏情况下,中断响应延迟能达到几百微秒——因为软件要处理优先级比较、上下文切换等操作。
4.5 优先级抢占策略的实战选择
讲完三种控制器,我们来聊聊怎么选策略。我个人总结了三条原则:
- 硬实时任务用固定优先级抢占:比如电机控制、PWM生成。把最高优先级给它们,其他任务靠边站。
- 软实时任务用轮转或时间片:比如网络协议栈、文件系统。用子优先级或软件调度来实现公平。
- 非实时任务用后台处理:比如日志输出、统计计算。放在最低优先级,有空才做。
我曾经在一个项目中,把中断优先级分成了三层:
| 层级 | 抢占优先级 | 典型中断 | 响应时间要求 |
|---|---|---|---|
| 紧急 | 0-3 | PWM、ADC过采样、故障检测 | < 5 μs |
| 普通 | 4-7 | UART、SPI、I2C | < 100 μs |
| 后台 | 8-15 | 定时器、RTC、DMA完成 | < 1 ms |
这个分层策略帮我解决了一个大问题:之前UART中断偶尔会打断PWM中断,导致电机抖动。把PWM放到最高优先级后,问题彻底消失。
4.6 总结与避坑
好了,这一讲的核心内容就这些。我最后再啰嗦几句:
- NVIC适合单核硬实时系统,延迟低、确定性好。
- GIC适合多核复杂系统,功能丰富但配置复杂。
- PLIC适合追求灵活性的RISC-V系统,但需要软件配合。
我曾经踩过一个坑:在一个GICv2系统上,我忘了配置中断路由寄存器(GICD_ITARGETSR),结果中断全发到了CPU0,其他三个核闲着。后来加上路由配置,系统吞吐量提升了3倍。嗯,细节决定成败。
下一讲我们会聊中断延迟的软件优化,包括中断服务函数怎么写才能最快、中断嵌套怎么管理。到时候见。