4. 中断使能与屏蔽:全局中断使能(DAIF)、局部中断使能、中断优先级与抢占

好,咱们接着聊中断。前面几章我们把中断的硬件架构和向量表理清楚了,现在要进入一个非常实操的环节——怎么控制中断的开关,以及多个中断同时来了怎么办。

说白了,中断使能与屏蔽,就是给中断系统装上一道道门。有些门是总闸,一拉全灭;有些门是房间门,只控制某一个中断源。而优先级和抢占,就是决定这些门打开的顺序。

4.1 全局中断使能:DAIF 寄存器

在 ARMv8-A 架构里,全局中断使能是通过 DAIF 寄存器来控制的。这个名字你可能不熟,但它的四个位你一定见过:D、A、I、F。

  • D:Debug 异常掩码
  • A:SError(系统错误)掩码
  • I:IRQ 掩码
  • F:FIQ 掩码

我们最常用的是 IF 位。写 1 就是屏蔽,写 0 就是使能。嗯,这里要注意,ARM 的设计是「屏蔽使能」——你写 1 反而把中断关了。

核心操作指令:

// 关闭全局 IRQ
asm volatile("msr daifset, #2");  // 设置 I 位

// 开启全局 IRQ
asm volatile("msr daifclr, #2");  // 清除 I 位

// 关闭全局 FIQ
asm volatile("msr daifset, #1");  // 设置 F 位

// 同时关闭 IRQ 和 FIQ
asm volatile("msr daifset, #3");  // 设置 I 和 F 位

我个人习惯在进入临界区之前,先保存当前的 DAIF 状态,退出时再恢复。这样不会破坏调用者的中断状态。你想想看,如果一个函数进去把中断关了,出来也不开,那整个系统就卡死了。

小技巧:联发科的 BootROM 里,启动阶段默认是关中断的。我记得有一次调试,发现外设中断一直不响应,查了半天,原来是 DAIF 的 I 位被某个驱动意外置位了。从那以后,我每次在中断初始化完成后,都会加一句打印确认 DAIF 的值。

4.2 局部中断使能:GIC 的逐中断控制

全局中断使能是总闸,但每个具体的中断源还有自己的开关。在 GIC(通用中断控制器)里,每个中断都有独立的使能位。

联发科平台上,GIC 的寄存器映射在内存地址空间里。操作起来很简单:

// 使能中断号 42(假设是 UART 中断)
uint32_t reg = *(volatile uint32_t*)(GICD_BASE + GICD_ISENABLER(42));
*(volatile uint32_t*)(GICD_BASE + GICD_ISENABLER(42)) = (1 << (42 % 32));

// 屏蔽中断号 42
*(volatile uint32_t*)(GICD_BASE + GICD_ICENABLER(42)) = (1 << (42 % 32));

这里有个坑——使能和屏蔽是两个不同的寄存器组。ISENABLER 是 Set-enable,写 1 使能;ICENABLER 是 Clear-enable,写 1 屏蔽。你不能直接往同一个寄存器写 0 来屏蔽,那样是无效的。

我曾经踩过的坑:刚接触 GIC 时,我习惯性地以为使能位是 1 表示使能,0 表示屏蔽。于是直接往 ISENABLER 写 0 来关中断。结果中断死活关不掉,外设一直在狂刷中断,CPU 负载飙到 100%。后来翻手册才发现,GIC 的设计是「写 1 有效」,写 0 是没反应的。要屏蔽,必须去操作 ICENABLER。

4.3 中断优先级:谁更重要?

多个中断同时来了,CPU 先处理谁?这就看优先级了。

在 GICv2/v3 里,每个中断都有一个 8 位的优先级寄存器,但实际使用的位数取决于硬件实现。联发科平台上,通常使用高 5 位,也就是优先级范围是 0~31。

优先级值 含义 典型用途
0 最高优先级 系统定时器、看门狗
1~15 高优先级 DMA、高速外设
16~31 低优先级 UART、GPIO、I2C

设置优先级也很直接:

// 设置中断号 42 的优先级为 16
uint32_t reg_addr = GICD_BASE + GICD_IPRIORITYR(42);
uint8_t *prio_ptr = (uint8_t*)reg_addr;
prio_ptr[42 % 4] = 16;  // 每个中断占 1 字节

为什么优先级重要?因为它决定了中断的响应延迟。高优先级的中断可以抢占低优先级的中断,这就是我们接下来要说的抢占。

4.4 中断抢占:谁可以打断谁?

抢占,说白了就是一个中断正在处理时,另一个更高优先级的中断来了,CPU 要不要停下来去处理新的?

在联发科平台上,GIC 支持两种模式:

  • 非抢占模式:中断处理过程中,所有中断都被屏蔽。等当前处理完了,再处理新的。
  • 抢占模式:高优先级中断可以打断低优先级中断的处理。

默认情况下,ARM 的异常处理是自动屏蔽同类型中断的。比如你在处理 IRQ 时,CPU 会自动设置 DAIF 的 I 位,防止新的 IRQ 进来。但如果你想让高优先级中断抢占,就需要在中断处理函数里手动清除 I 位。

抢占的实现要点:

void irq_handler(void) {
    // 保存当前中断优先级
    uint32_t current_prio = read_current_irq_priority();

    // 手动开启全局中断,允许更高优先级中断抢占
    asm volatile("msr daifclr, #2");

    // 处理当前中断
    handle_irq();

    // 关闭全局中断,准备退出
    asm volatile("msr daifset, #2");

    // 写 EOI,通知 GIC 中断处理完成
    write_gic_eoi(current_prio);
}

这里有个关键点:抢占是有代价的。每次抢占都会带来额外的上下文切换开销。如果抢占太频繁,反而会降低系统效率。

我的建议:在联发科平台上做实时系统时,我一般把抢占留给那些真正紧急的中断——比如系统心跳定时器、紧急错误处理。普通的外设中断,我建议用非抢占模式,或者只允许一级抢占。你想想看,如果每个中断都互相抢占,那光保存恢复上下文就够 CPU 忙的了。

4.5 实战中的优先级配置策略

说了这么多理论,咱们来点实际的。在联发科平台上,我一般这样配置中断优先级:

  1. 系统级中断(优先级 0~3):看门狗、系统定时器、PMC(电源管理控制器)。这些中断必须最快响应,否则系统可能崩溃。
  2. 实时性要求高的外设(优先级 4~15):DMA、高速 SPI、I2S 音频。这些外设有严格的时序要求,不能被长时间阻塞。
  3. 普通外设(优先级 16~31):UART、GPIO、低速 I2C。这些中断可以容忍一定的延迟。

为什么要这样分?因为我在项目里吃过亏。有一次,我把 UART 中断优先级设得比 DMA 还高。结果 DMA 传输过程中,UART 频繁打断,导致 DMA 缓冲区溢出,音频数据出现了爆音。后来把优先级调回来,问题就解决了。

注意:优先级不是越高越好。高优先级中断会抢占低优先级中断,如果高优先级中断处理时间过长,低优先级中断可能会饿死。我曾经见过一个系统,定时器中断优先级设得极高,处理函数里又做了大量计算,结果 UART 中断一直得不到响应,数据丢得一塌糊涂。

4.6 总结一下

中断使能与屏蔽,说白了就是三件事:

  • 全局开关:DAIF 寄存器控制所有中断的总闸。
  • 局部开关:GIC 的 ISENABLER/ICENABLER 控制每个中断的独立开关。
  • 优先级与抢占:决定中断的响应顺序和嵌套深度。

嗯,这一章的内容就到这里。下一章我们会讲中断延迟的测量与优化,到时候会结合联发科平台的实际案例,看看怎么把中断响应时间压到最低。