中断处理基础:中断概念、中断向量表、中断控制器(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 里,中断处理分两层:
- 内核层:CPU 收到中断后,进入内核态。内核保存上下文,调用你注册的 ISR。
- 用户层: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
好了,中断处理的基础就讲到这里。下一章咱们聊聊中断亲和性设置和负载均衡,那才是多核系统下的重头戏。记住:中断处理做得好,系统实时性就稳了一半。