中断处理基础:中断概念、中断向量表、中断控制器(INTC)介绍

各位同学,咱们今天聊聊中断。说实话,搞嵌入式实时系统,中断就是命根子。你想想看,CPU 本来在乖乖跑主循环,突然外设说「我有急事」,CPU 就得放下手头活去处理。这个机制,就是中断。

我在 QNX 项目里见过太多因为中断没处理好导致系统崩溃的案例。有一次,一个电机控制板,中断响应慢了 50 微秒,电机直接抖成筛子。嗯,从那以后我对中断的敬畏心就上来了。

1. 中断到底是什么?

中断,说白了就是硬件给 CPU 发的一个信号。这个信号告诉 CPU:「别忙了,先看看我这边的事。」CPU 收到信号后,会保存当前执行状态,跳到一个特定地址去执行中断服务程序(ISR)。处理完再回来继续干活。

这里有个关键点:中断是异步的。CPU 不知道它什么时候来。所以你的 ISR 必须写得又快又稳。我个人习惯,ISR 里只做最必要的事,比如读个寄存器、设个标志位,复杂逻辑全扔给线程去做。

核心要点:中断的实时性取决于三个因素——中断延迟、中断响应时间、中断处理时间。这三个指标,做实时系统时必须心里有数。

2. 中断向量表——CPU 的「紧急联系人」

中断向量表,你可以把它想象成 CPU 的通讯录。每个中断源都有一个编号,叫中断向量号。CPU 收到中断后,拿着这个号去查表,找到对应的 ISR 入口地址,然后跳过去执行。

在 ARM Cortex-A 系列上,中断向量表通常放在 0x00000000 或者 0xFFFF0000 地址。QNX 启动时会帮你设置好,但如果你自己写裸机代码,就得手动配。

我记得有一次调试一个网络丢包问题,查了半天发现是中断向量表被意外修改了。嗯,从那以后我都在代码里加了个向量表校验机制。

// 伪代码:中断向量表结构
typedef void (*isr_func_t)(void);

isr_func_t vector_table[256] = {
    [0] = reset_handler,      // 复位
    [1] = undefined_handler,  // 未定义指令
    [2] = svc_handler,        // 系统调用
    [3] = prefetch_handler,   // 预取中止
    [4] = data_handler,       // 数据中止
    [5] = irq_handler,        // 普通中断
    [6] = fiq_handler,        // 快速中断
    // ... 其他中断向量
};

避坑指南:我曾经在移植 QNX 到新板子时,忘了把中断向量表重定位到 DDR 里。结果中断一来,CPU 去 ROM 里找 ISR,直接跑飞。记住:向量表地址必须和 MMU 配置一致。

3. 中断控制器(INTC)——中断的「交通警察」

一个 SoC 里可能有几十上百个中断源。CPU 只有一个 IRQ 引脚,怎么管理这么多中断?这就需要中断控制器(INTC)了。

INTC 的作用就是:接收所有中断请求,按优先级排队,然后选一个最紧急的发给 CPU。常见的 INTC 有 GIC(Generic Interrupt Controller,ARM 通用中断控制器)、NVIC(Nested Vectored Interrupt Controller,Cortex-M 用)等。

在 QNX 下,INTC 的初始化通常由 Startup 代码完成。你只需要在系统启动后,通过 InterruptAttach()InterruptAttachEvent() 注册 ISR 就行。

特性 GIC(ARM) NVIC(Cortex-M) 本地 INTC(x86)
中断源数量 最多 1020 个 最多 240 个 最多 256 个
优先级层级 16-256 级 8-256 级 16 级
中断分组 支持(Group 0/1) 不支持 不支持
典型应用 多核应用处理器 单片机/实时控制器 PC/服务器

你想想看,如果没有 INTC,CPU 得自己轮询所有外设的中断状态,那实时性就全完了。INTC 的存在,让 CPU 可以专心干活,只在必要时才被中断打断。

4. QNX 下的中断处理流程

在 QNX 里,中断处理分两层:

  1. 内核层:CPU 收到中断后,进入内核态。内核保存上下文,调用你注册的 ISR。
  2. 用户层:ISR 里可以通过 InterruptSend()InterruptWait() 通知用户态线程。

我个人建议:ISR 里绝对不要做阻塞操作。比如 printf()malloc()mutex_lock() 这些,统统不要放进去。为什么?因为 ISR 运行在中断上下文中,没有进程调度,一旦阻塞,整个系统就卡死了。

// QNX 中断注册示例
#include <sys/neutrino.h>

const struct sigevent *my_isr(void *arg, int id) {
    // 读硬件寄存器,清除中断标志
    uint32_t status = in32(REG_STATUS);
    out32(REG_CLEAR, status);

    // 通知用户态线程
    struct sigevent *event = (struct sigevent *)arg;
    InterruptSend(event);

    // 返回 NULL 表示中断已处理
    return NULL;
}

int main() {
    int irq_id = 42;  // 假设中断号是 42
    struct sigevent event;

    SIGEV_SIGNAL_INIT(&event, SIGUSR1);

    // 注册中断
    int id = InterruptAttach(irq_id, my_isr, &event, sizeof(event), 0);
    if (id == -1) {
        perror("InterruptAttach failed");
        return 1;
    }

    // 主循环
    while (1) {
        // 等待中断信号
        int sig;
        SignalWait(&sig);
        // 处理数据
    }

    InterruptDetach(id);
    return 0;
}

警告:千万不要在 ISR 里调用 InterruptAttach()InterruptDetach()。这些函数会修改内核的中断管理数据结构,而 ISR 运行时内核已经锁住了调度器。我曾经见过有人这么干,结果系统直接死锁,连串口都打不开了。

5. 中断优先级与嵌套

中断优先级,说白了就是给中断排个队。高优先级的中断可以打断低优先级的中断,这叫中断嵌套。

在 QNX 下,中断优先级由 INTC 硬件决定。你可以在 Startup 代码里配置,也可以在运行时通过 InterruptSetPriority() 修改。但我建议:优先级分配要慎重。把时间敏感的中断(比如定时器、电机控制)设高优先级,把非关键的中断(比如按键、LED)设低优先级。

我记得有个项目,工程师把所有中断都设成同一优先级。结果一个慢速的串口中断把高速的 SPI 中断堵住了,导致数据丢包。嗯,这就是优先级没规划好的典型问题。

经验之谈:中断嵌套层级不要超过 3 层。嵌套太深会导致栈溢出,而且调试起来极其痛苦。我一般把中断优先级分成三级:紧急(定时器、故障检测)、普通(数据收发)、低优先级(状态查询)。

6. 中断延迟——实时性的核心指标

中断延迟,就是从硬件发出中断信号,到 CPU 开始执行 ISR 第一条指令的时间。这个时间越短,实时性越好。

影响中断延迟的因素有:

  • CPU 当前状态:如果 CPU 正在执行关中断的代码,中断会被延迟。
  • 中断屏蔽:高优先级中断会屏蔽低优先级中断。
  • 缓存命中率:ISR 代码在缓存里,延迟就小;不在缓存里,延迟就大。
  • 总线仲裁:如果 CPU 正在访问慢速外设,中断响应会变慢。

在 QNX 下,你可以用 ClockCycles() 测量中断延迟。我一般会在系统启动时跑一个基准测试,记录最坏情况下的延迟。如果超过 10 微秒,就得优化了。

// 测量中断延迟的简单方法
uint64_t start, end;
start = ClockCycles();

// 触发一个软件中断
InterruptTrigger(SW_IRQ);

// 在 ISR 里记录结束时间
// end = ClockCycles();
// 延迟 = (end - start) / cycles_per_us

好了,中断处理的基础就讲到这里。下一章咱们聊聊中断亲和性设置和负载均衡,那才是多核系统下的重头戏。记住:中断处理做得好,系统实时性就稳了一半。