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 *,这个后面细说。 - area 和 size:共享内存区域。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);
这里的 id 是 InterruptAttach() 返回的ID。注意,这个ID不是中断号,是内核分配的一个句柄。
我曾经见过一个案例:某位同事在进程退出时忘了调用 InterruptDetach(),结果下次启动时中断挂载失败,整个系统直接卡死。所以,我建议你在进程的清理函数里,务必加上中断卸载的逻辑。
InterruptDetach()!这会导致死锁。ISR里只做最轻量级的工作,卸载操作交给主线程。
3.2 中断服务例程(ISR)的编写
ISR是中断处理的核心。但很多人把它写成了「大杂烩」,什么逻辑都往里塞。这是大忌。
3.2.1 ISR 的基本原则
ISR 必须短小精悍。为什么?因为它在关中断状态下执行。你想想看,如果ISR里跑了一个耗时的循环,其他中断就进不来了,系统的实时性直接归零。
我总结了几条铁律:
- 不做阻塞操作:不能调用
printf()、malloc()、sleep()等。 - 不做浮点运算:浮点上下文保存开销大,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 关键字不能少。编译器优化会坑你,我吃过这个亏。
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() 设置亲和性。我一般把中断绑定到非业务核心上,避免干扰主线程。
3.4 总结
QNX的中断模型,说白了就是「ISR快进快出,线程慢慢处理」。记住三个关键点:
- InterruptAttach() 挂载中断,InterruptDetach() 卸载中断,别忘了。
- ISR 要短,只做清标志、读寄存器、发事件这三件事。
- 共享内存加 volatile,别让编译器坑了你。
好了,中断这块就聊到这儿。下一章我们聊聊时钟和定时器,那又是另一个有意思的话题。