3、中断控制器驱动框架:Linux irqchip 子系统、struct irq_chip 结构体、irq_domain 映射机制
好,咱们接着聊中断。上一章我们把 ARM GIC 的硬件行为摸了个透,但硬件是硬件,软件是软件。Linux 内核怎么管理这些中断控制器?怎么把硬件中断号和软件中断号对应起来?这就是今天要讲的核心——irqchip 子系统。
说实话,我早年刚接触这部分时,也被绕得晕头转向。什么 irq_chip、irq_domain,一堆抽象概念。但后来我悟了:你只要搞懂三个东西,整个框架就通了——谁来操作硬件寄存器(irq_chip)、谁来管理映射关系(irq_domain)、谁来描述中断源(struct irq_desc)。
核心三要素:
- irq_chip:操作中断控制器硬件寄存器的函数集合
- irq_domain:硬件中断号 → Linux 软件中断号的映射表
- struct irq_desc:每个软件中断号对应的描述符,包含处理函数、标志位等
3.1 irqchip 子系统:谁在管这些中断控制器?
Linux 内核里,所有中断控制器驱动都注册到 irqchip 子系统。你可以把它理解成一个「中断控制器驱动管理中心」。每个中断控制器(GIC、GPIO 中断控制器、IOAPIC 等)都对应一个 struct irq_chip 实例。
我习惯把 irqchip 子系统比作一个「硬件操作工具箱」。每个中断控制器就是一把工具,而 irq_chip 就是这把工具的使用说明书——告诉你如何打开、关闭、屏蔽、唤醒这个中断。
内核启动时,中断控制器驱动会调用 irq_chip_register() 或直接通过设备树匹配来注册。注册完成后,这个中断控制器就纳入了内核的统一管理。
我的经验:调试新板子时,我经常先检查 /proc/interrupts 和 /sys/kernel/debug/irq/。如果某个中断控制器没注册成功,这里会直接暴露出来。我曾经花了两天查一个 GPIO 中断不触发的问题,最后发现是 irq_chip 的 irq_ack 回调没实现——硬件中断来了没人应答,系统直接卡死。
3.2 struct irq_chip:中断控制器的操作手册
这个结构体定义了中断控制器的所有操作接口。说白了,它就是一份「硬件操作函数表」。每个中断控制器驱动都要实现它。
struct irq_chip {
const char *name; // 控制器名字,/proc/interrupts 里能看到
unsigned int (*irq_mask)(struct irq_data *data); // 屏蔽中断
void (*irq_unmask)(struct irq_data *data); // 取消屏蔽
void (*irq_ack)(struct irq_data *data); // 应答中断
void (*irq_eoi)(struct irq_data *data); // 中断结束(EOI)
int (*irq_set_type)(struct irq_data *data, unsigned int flow_type); // 设置触发方式
int (*irq_set_wake)(struct irq_data *data, unsigned int on); // 设置唤醒源
// ... 还有其他回调,但上面这些是最常用的
};
每个回调函数接收一个 struct irq_data 参数。这个结构体包含了中断号、硬件寄存器地址等关键信息。我刚开始写驱动时,总搞不清 irq_data 和 irq_desc 的区别——irq_data 是给芯片操作函数用的,irq_desc 是给中断处理流程用的,分工明确。
| 回调函数 | 作用 | 我踩过的坑 |
|---|---|---|
| irq_mask | 屏蔽某个中断源 | 忘记实现这个,中断风暴直接打崩系统 |
| irq_unmask | 取消屏蔽 | 和 mask 要成对实现,否则中断永远打不开 |
| irq_ack | 应答中断,告诉硬件我收到了 | 电平触发的中断,ack 必须在处理完之前调用,否则会丢失 |
| irq_eoi | 中断处理结束,硬件可以发下一个中断了 | GIC 的 EOI 写错寄存器地址,导致中断无法再触发 |
| irq_set_type | 设置上升沿/下降沿/高电平/低电平触发 | 设备树里配了上升沿,驱动里却设成电平,中断永远不触发 |
注意:irq_chip 的回调函数都是在中断上下文或关中断的情况下执行的。千万别在里面做耗时操作,比如打印、申请内存、拿锁等。我曾经在 irq_ack 里加了个 printk,结果系统直接死锁——中断上下文里调用 printk 会触发控制台锁,而控制台锁又被中断打断...死循环了。
3.3 irq_domain:硬件中断号到软件中断号的桥梁
这是整个中断子系统里最巧妙的设计。为什么需要它?
你想想看,硬件中断控制器上的中断号是固定的——比如 GIC 的 SPI 中断号从 32 到 1019。但 Linux 内核里,每个中断用一个 irq 整数表示,这个数字是内核动态分配的。硬件中断号和软件中断号之间,必须有一个映射关系。
irq_domain 就是这张映射表。
内核支持三种映射方式:
- 线性映射(Linear):硬件中断号连续,直接做偏移计算。最简单,性能最好。适合 GIC 这种中断号连续的控制器。
- 树形映射(Tree):硬件中断号不连续,用红黑树查找。适合 GPIO 中断控制器,中断号可能很稀疏。
- 不映射(No Map):硬件中断号和软件中断号一一对应,不做转换。适合一些简单的嵌入式控制器。
// 创建设备树的 irq_domain 示例(GIC 驱动中常见)
struct irq_domain *domain;
domain = irq_domain_add_linear(node, nr_irqs, &gic_irq_domain_ops, NULL);
if (!domain) {
pr_err("Failed to add irq domain\n");
return -ENOMEM;
}
这里 gic_irq_domain_ops 是关键,它定义了如何从硬件中断号翻译到软件中断号:
static const struct irq_domain_ops gic_irq_domain_ops = {
.map = gic_irq_domain_map, // 建立映射
.unmap = gic_irq_domain_unmap, // 解除映射
.xlate = gic_irq_domain_xlate, // 从设备树解析中断号
};
设备树里写 interrupts = <0 42 4>,内核怎么知道 42 对应哪个软件中断号?就是通过 xlate 回调来解析的。解析完后,map 回调负责建立真正的映射关系。
一句话总结映射流程:
- 设备树解析中断属性 → 得到硬件中断号(如 42)
- irq_domain 的 xlate 回调解析出硬件中断号和触发类型
- irq_domain 分配一个空闲的软件中断号(如 168)
- map 回调建立 42 → 168 的映射,并初始化 irq_desc
- 以后硬件触发中断 42,内核自动找到软件中断号 168,执行对应的处理函数
3.4 实战:一个简单的 irqchip 驱动骨架
光说不练假把式。我给你看一个我实际项目中用过的 irqchip 驱动骨架。这个控制器是一个 FPGA 里实现的中断控制器,支持 32 个中断源。
static void myic_irq_mask(struct irq_data *d)
{
struct myic_chip *chip = irq_data_get_irq_chip_data(d);
u32 mask = readl(chip->base + MYIC_MASK);
mask &= ~BIT(d->hwirq); // 屏蔽对应位
writel(mask, chip->base + MYIC_MASK);
}
static void myic_irq_unmask(struct irq_data *d)
{
struct myic_chip *chip = irq_data_get_irq_chip_data(d);
u32 mask = readl(chip->base + MYIC_MASK);
mask |= BIT(d->hwirq); // 取消屏蔽
writel(mask, chip->base + MYIC_MASK);
}
static struct irq_chip myic_chip = {
.name = "my_irq_controller",
.irq_mask = myic_irq_mask,
.irq_unmask = myic_irq_unmask,
.irq_ack = myic_irq_mask, // 电平触发时,ack 就是 mask
.irq_set_type = myic_irq_set_type,
};
static int myic_domain_map(struct irq_domain *d, unsigned int irq,
irq_hw_number_t hw)
{
irq_set_chip_and_handler(irq, &myic_chip, handle_level_irq);
irq_set_chip_data(irq, d->host_data);
irq_set_noprobe(irq);
return 0;
}
static const struct irq_domain_ops myic_domain_ops = {
.map = myic_domain_map,
.xlate = irq_domain_xlate_twocell, // 使用标准的两单元解析
};
static int myic_probe(struct platform_device *pdev)
{
struct myic_chip *chip;
struct irq_domain *domain;
chip = devm_kzalloc(&pdev->dev, sizeof(*chip), GFP_KERNEL);
chip->base = devm_platform_ioremap_resource(pdev, 0);
domain = irq_domain_add_linear(pdev->dev.of_node, 32,
&myic_domain_ops, chip);
if (!domain)
return -ENOMEM;
chip->domain = domain;
platform_set_drvdata(pdev, chip);
return 0;
}
这段代码虽然简单,但麻雀虽小五脏俱全。它展示了 irqchip 驱动的核心流程:
- 实现 irq_chip 的操作函数(mask/unmask/ack/set_type)
- 创建 irq_domain,建立硬件中断号到软件中断号的映射
- 在 map 回调中,为每个中断设置对应的芯片操作函数和中断处理类型
我的建议:写 irqchip 驱动时,先别急着写全部回调。先把 mask/unmask 实现好,然后用 cat /proc/interrupts 看看中断号有没有注册成功。确认映射没问题后,再逐步添加 ack、set_type 等回调。这样调试起来轻松很多。
3.5 知识体系图
下面这张图展示了 irqchip 子系统的核心架构。我画这张图时,特意把数据流和控制流分开了,方便你理解。
从这张图你能看到,整个 irqchip 子系统是分层设计的。硬件中断从底层往上走,经过 irq_domain 映射,找到对应的 irq_chip 和 irq_desc,最终到达应用层的处理函数。每一层各司其职,耦合度很低。
再强调一次:irq_domain 的 xlate 回调一定要和设备树里的 interrupts 属性格式匹配。设备树用 <0 42 4> 三单元格式,你的 xlate 就得解析三个参数。我曾经接手一个项目,设备树是两单元格式,驱动里却用了三单元的 xlate,结果所有中断号都偏移了 32——查了整整一天才找到原因。
好了,irqchip 子系统的核心内容就这些。你掌握了 irq_chip 的操作函数、irq_domain 的映射机制,再配合设备树的中断解析,基本上就能搞定大部分中断控制器驱动的移植工作了。下一章我们聊聊中断处理流程的完整链路——从硬件触发到软件执行,中间到底发生了什么。
公众号:蓝海资料掘金营,微信deep3321