3、QNX中断模型:InterruptAttach()、InterruptDetach()、中断服务例程(ISR)编写

中断处理,是实时系统的命脉。说句实话,QNX的中断模型设计得非常精巧,但如果你没摸透它的脾气,写出来的ISR可能会把整个系统的实时性拖垮。今天我们就来聊聊中断的挂载、卸载,以及ISR到底该怎么写。

3.1 中断的挂载与卸载:InterruptAttach() 与 InterruptDetach()

在QNX里,中断不是你想用就能用的。你得先向内核注册,告诉它:「嘿,这个中断号归我管了」。这个注册动作,就是 InterruptAttach()

3.1.1 InterruptAttach() 的使用

这个函数的原型长这样:

int InterruptAttach(
    int intr,
    const struct sigevent *(*handler)(void *, int),
    const void *area,
    int size,
    unsigned flags
);

参数看着多,其实核心就几个:

  • intr:中断向量号。这个你得查硬件手册,别猜。
  • handler:你的ISR函数指针。注意返回值类型是 const struct sigevent *,这个后面细说。
  • areasize:共享内存区域。ISR和你的进程可以通过这块内存交换数据。
  • flags:标志位。常用的有 _NTO_INTR_FLAGS_TRK_MSK_NTO_INTR_FLAGS_END

我个人习惯,在嵌入式板子上调试时,flags 一般会带上 _NTO_INTR_FLAGS_TRK_MSK。为什么?因为它能帮你追踪中断屏蔽状态,调试时少掉头发。

小提示: 如果你不确定中断号,可以在启动时用 pci -vvv 或者 devinfo -v 查看。我在项目中遇到过,有人直接写死了一个中断号,结果换了块板子就崩了。嗯,硬件兼容性是个坑。

3.1.2 InterruptDetach() 的时机

有挂载就有卸载。当你不再需要某个中断时,调用 InterruptDetach() 把它释放掉。

int InterruptDetach(int id);

这里的 idInterruptAttach() 返回的ID。注意,这个ID不是中断号,是内核分配的一个句柄。

我曾经见过一个案例:某位同事在进程退出时忘了调用 InterruptDetach(),结果下次启动时中断挂载失败,整个系统直接卡死。所以,我建议你在进程的清理函数里,务必加上中断卸载的逻辑。

警告: 不要在ISR内部调用 InterruptDetach()!这会导致死锁。ISR里只做最轻量级的工作,卸载操作交给主线程。

3.2 中断服务例程(ISR)的编写

ISR是中断处理的核心。但很多人把它写成了「大杂烩」,什么逻辑都往里塞。这是大忌。

3.2.1 ISR 的基本原则

ISR 必须短小精悍。为什么?因为它在关中断状态下执行。你想想看,如果ISR里跑了一个耗时的循环,其他中断就进不来了,系统的实时性直接归零。

我总结了几条铁律:

  • 不做阻塞操作:不能调用 printf()malloc()sleep() 等。
  • 不做浮点运算:浮点上下文保存开销大,ISR里别碰。
  • 不做复杂逻辑:比如链表遍历、哈希查找,这些放到线程里做。
  • 只做一件事:清中断标志,读硬件寄存器,然后发事件通知线程。
核心思想: ISR 只负责「通知」,不负责「处理」。处理逻辑交给线程。

3.2.2 ISR 的返回值:struct sigevent

ISR 的返回值是一个 struct sigevent 指针。这个结构体告诉内核:「中断处理完了,接下来该唤醒哪个线程?」

常见的用法是:

const struct sigevent *my_isr(void *area, int id) {
    // 清硬件中断标志
    *(volatile uint32_t *)(HW_BASE + INT_CLR) = 1;

    // 返回事件,通知线程
    return &my_event;
}

这里的 my_event 需要在初始化时设置好。比如用 SIGEV_PULSE() 发脉冲,或者 SIGEV_SIGNAL() 发信号。

我个人偏好用脉冲(pulse),因为它在QNX里开销最小,而且不会丢失。信号嘛,有时候会合并,你懂的。

3.2.3 共享内存区域:area 参数

ISR 和你的进程怎么交换数据?靠 area 参数。这块内存是物理连续的,ISR 可以直接读写。

举个例子:

typedef struct {
    volatile uint32_t counter;
    volatile uint32_t status;
} shared_data_t;

shared_data_t *shared = (shared_data_t *)area;

const struct sigevent *my_isr(void *area, int id) {
    shared_data_t *data = (shared_data_t *)area;
    data->counter++;
    data->status = in32(HW_STATUS_REG);
    return &my_event;
}

注意,volatile 关键字不能少。编译器优化会坑你,我吃过这个亏。

避坑指南: 我曾经在ISR里忘记加 volatile,结果读到的状态寄存器值一直是0。查了两天才发现,编译器把读操作优化掉了。从那以后,我所有硬件寄存器都加 volatile

3.3 实战中的注意事项

理论说完了,聊聊实战中容易踩的坑。

3.3.1 中断优先级与嵌套

QNX 默认不支持中断嵌套。也就是说,一个ISR执行时,其他中断都会被屏蔽。这其实是为了简化设计。但如果你需要高优先级中断抢占低优先级中断,得手动设置中断控制器。

我建议:除非你非常清楚自己在做什么,否则别碰中断嵌套。它带来的复杂度远大于收益。

3.3.2 中断延迟的测量

中断延迟是实时性的关键指标。你可以用 ClockCycles() 在ISR入口和出口打时间戳,然后统计最大值。

uint64_t start, end;
start = ClockCycles();
// ISR 处理
end = ClockCycles();
// 记录到共享内存

嗯,这里要注意,ClockCycles() 本身也有开销,但作为相对测量是够用的。

3.3.3 多核环境下的中断亲和性

在多核系统里,中断可以绑定到特定CPU核心。用 InterruptAttach_r() 或者 ThreadCtl() 设置亲和性。我一般把中断绑定到非业务核心上,避免干扰主线程。

注意: 中断亲和性设置不当,会导致CPU负载不均。我曾经见过一个系统,所有中断都挤在CPU0上,其他核心闲得发慌,CPU0却跑满了。后来把中断分散到各个核心,系统吞吐量提升了30%。

3.4 总结

QNX的中断模型,说白了就是「ISR快进快出,线程慢慢处理」。记住三个关键点:

  • InterruptAttach() 挂载中断,InterruptDetach() 卸载中断,别忘了。
  • ISR 要短,只做清标志、读寄存器、发事件这三件事。
  • 共享内存加 volatile,别让编译器坑了你。

好了,中断这块就聊到这儿。下一章我们聊聊时钟和定时器,那又是另一个有意思的话题。