2、中断注册与注销:InterruptAttach()、InterruptDetach()、中断处理函数原型
好,咱们接着聊中断。上一节我们把中断控制器和中断向量表理了一遍,算是把硬件底子打好了。这一节,咱们要真正上手写代码了——怎么把一个中断处理函数挂到系统里,又怎么把它摘下来。
说白了,就是两个核心API:InterruptAttach() 和 InterruptDetach()。再加上中断处理函数到底长什么样,参数怎么传,返回值怎么处理。这些搞清楚了,你写中断驱动才算入了门。
2.1 中断处理函数原型——先搞清楚你要写什么
在QNX里,中断处理函数的签名是固定的。你别想着自己随便定义参数,系统不认。原型长这样:
const struct sigevent *handler(void *area, int id);
两个参数:
void *area—— 你注册中断时传进来的那个上下文指针。我习惯叫它“cookie”,你想传什么就传什么,比如设备结构体指针、寄存器基地址。说白了,这就是你的私有数据通道。int id—— 系统分配的中断ID。这个ID在你注册成功后由内核返回,处理函数里一般用不到,但偶尔需要用它来注销中断时会有用。
返回值是一个 const struct sigevent *。这个有意思了——它告诉内核:中断处理完了,你要不要唤醒某个线程?
- 返回
NULL:表示不需要唤醒任何线程,中断处理完全在ISR里搞定。 - 返回一个
struct sigevent指针:内核会把这个事件发给对应的线程,通常是发一个脉冲信号,让线程知道“数据准备好了,你来处理吧”。
嗯,这里要注意:中断处理函数是在中断上下文里执行的,不能阻塞,不能调用任何可能引起阻塞的API。你想想看,中断上下文里如果调了 sleep(),整个系统就挂了。我刚开始做QNX开发时,就犯过这个错——在ISR里打印日志,结果日志驱动自己也要用锁,直接死锁。教训深刻啊。
2.2 InterruptAttach()——把中断挂上去
注册中断的API有两个版本,我重点讲 InterruptAttach(),因为它更常用,也更灵活。
int InterruptAttach(
int intr,
const struct sigevent *(*handler)(void *, int),
void *area,
int size,
unsigned flags
);
参数说明:
| 参数 | 含义 | 我的建议 |
|---|---|---|
intr |
中断向量号,就是上一节说的IRQ号 | 从硬件手册或BSP里查,别猜 |
handler |
中断处理函数指针 | 注意函数签名必须完全匹配 |
area |
传给handler的上下文指针 | 我习惯传设备结构体指针 |
size |
area指向的数据大小,通常传0 | 除非你要用内核帮你拷贝数据 |
flags |
标志位,常用 INTR_ATTACH_STANDARD |
共享中断时用 INTR_ATTACH_FLAG_SHARED |
返回值:成功返回一个非负的ID(就是前面说的 id),失败返回 -1,并设置errno。
我个人的习惯是,注册中断时一定检查返回值。别偷懒。有一次我在项目里没检查返回值,结果中断根本没挂上,设备轮询跑了一整天,我还纳闷为什么CPU占用率那么高。后来一查,是中断号写错了。
核心要点:注册中断时,area 参数是你的“救命稻草”。把设备相关的所有信息打包成一个结构体传进去,ISR里就能拿到一切你需要的东西。我通常这样定义:
typedef struct {
uintptr_t base_addr; // 寄存器基地址
int irq; // 中断号
int dev_id; // 设备ID
// ... 其他设备相关字段
} my_device_t;
// 注册时
my_device_t *dev = malloc(sizeof(my_device_t));
dev->base_addr = 0xFE200000;
dev->irq = 67;
int id = InterruptAttach(dev->irq, my_isr, dev, 0, INTR_ATTACH_STANDARD);
2.3 InterruptDetach()——用完记得摘下来
有注册就有注销。函数原型很简单:
int InterruptDetach(int id);
参数就是 InterruptAttach() 返回的那个ID。成功返回0,失败返回 -1。
什么时候需要注销?
- 驱动卸载时,必须注销中断。否则中断来了,处理函数已经被卸载了,系统直接panic。
- 设备热插拔时,拔掉设备前要注销。
- 重新配置中断时,先注销旧的,再注册新的。
警告:千万不要在中断处理函数里调用 InterruptDetach() 来注销自己!这会导致死锁。我曾经见过一个同事这么干,结果系统直接挂死,连panic信息都打印不出来。正确的做法是:在ISR里设置一个标志位,然后在线程上下文中调用 InterruptDetach()。
2.4 一个完整的中断注册示例
光说不练假把式。我给你写一个完整的例子,从注册到处理到注销,一条龙:
#include <sys/neutrino.h>
#include <hw/inout.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct {
uintptr_t base;
int irq;
volatile int irq_count;
} dev_ctx_t;
const struct sigevent *my_isr(void *area, int id) {
dev_ctx_t *ctx = (dev_ctx_t *)area;
uint32_t status;
// 读中断状态寄存器
status = in32(ctx->base + 0x10);
if (status & 0x01) {
ctx->irq_count++;
// 清除中断标志
out32(ctx->base + 0x10, 0x01);
}
// 返回NULL,表示不需要唤醒线程
return NULL;
}
int main(int argc, char *argv[]) {
dev_ctx_t *ctx = malloc(sizeof(dev_ctx_t));
ctx->base = 0xFE200000;
ctx->irq = 67;
ctx->irq_count = 0;
int id = InterruptAttach(ctx->irq, my_isr, ctx, 0, INTR_ATTACH_STANDARD);
if (id == -1) {
perror("InterruptAttach failed");
free(ctx);
return EXIT_FAILURE;
}
printf("Interrupt attached, ID = %d\n", id);
// 模拟运行一段时间
sleep(10);
// 注销中断
if (InterruptDetach(id) == -1) {
perror("InterruptDetach failed");
}
printf("Total IRQ count: %d\n", ctx->irq_count);
free(ctx);
return EXIT_SUCCESS;
}
这个例子虽然简单,但骨架是完整的。你想想看,实际项目里无非就是在这个基础上加更多逻辑——比如用 sigevent 唤醒线程、处理多个中断源、支持共享中断等等。
2.5 避坑指南
最后,我把自己踩过的坑总结一下,你记好了:
- 中断处理函数要快——别在里面做复杂计算,别调用printf,别申请内存。我见过有人在ISR里malloc,直接导致系统响应变慢,最后整个系统卡死。
- 共享中断要小心——如果你的设备支持共享中断,注册时加上
INTR_ATTACH_FLAG_SHARED标志。同时,ISR里一定要检查中断状态寄存器,确认是不是自己的设备触发的中断。不是的话,赶紧返回NULL,让下一个handler处理。 - 中断ID要保存好——注销时要用的。我习惯把它放在设备结构体里,和irq号放一起,这样不会丢。
- 多线程环境下注意同步——ISR和线程共享的数据,要用原子操作或者关中断的方式来保护。别用互斥锁,ISR里不能用。
好了,中断注册和注销就讲到这里。下一节我们聊聊中断处理函数里到底能做什么、不能做什么,以及怎么和用户态线程高效通信。到时候我会分享一个我实际项目中用过的中断分发框架,挺实用的。