中断号映射:硬件中断号与Linux IRQ号的桥梁
中断号映射,说白了就是让硬件知道“谁在喊我”,让Linux内核知道“该找谁处理”。在Jacinto平台上,这个映射可不是简单的一一对应。我刚开始接触这个平台时,就被这层映射关系绕晕过——硬件上明明写着IRQ 48,到了Linux里却变成了IRQ 224,查了半天才发现是映射表没配对。
为什么需要映射?
硬件中断控制器(比如GIC)有自己的中断编号,从0到几百不等。Linux内核也有自己的IRQ编号空间。这两套编号体系,就像两套语言,需要一个翻译官。这个翻译官就是irq_domain。
你想想看,一个SoC里可能有多个中断源:GPIO、I2C、SPI、DMA……每个外设的中断号在硬件手册里都写死了。但Linux内核在运行时动态分配IRQ号,两者必须建立对应关系。没有映射,中断来了内核根本不知道是哪个设备触发的。
核心概念:硬件中断号(hwirq)→ irq_domain → Linux IRQ号(virq)
irq_domain机制详解
irq_domain是Linux内核提供的一套中断映射框架。在Jacinto平台的BSP开发中,我几乎每个外设驱动都要跟它打交道。它的核心作用就三个:
- 注册映射关系:告诉内核“这个硬件中断号对应哪个Linux IRQ号”
- 查找映射关系:中断发生时,根据硬件中断号找到对应的IRQ描述符
- 管理生命周期:设备树解析、中断控制器初始化、中断释放等
irq_domain的结构体定义在include/linux/irqdomain.h里,我挑几个关键字段说说:
struct irq_domain {
struct list_head link; /* 全局链表链接 */
const char *name; /* 域名,调试用 */
const struct irq_domain_ops *ops; /* 操作函数集 */
irq_hw_number_t hwirq_max; /* 最大硬件中断号 */
unsigned int revmap_size; /* 映射表大小 */
struct radix_tree_root revmap_tree; /* 树形映射根 */
enum irq_domain_bus_token bus_token; /* 总线类型 */
void *host_data; /* 私有数据 */
};
这里有个细节:revmap_size和revmap_tree是互斥的,一个用于线性映射,一个用于树形映射。我当年在调试一个GPIO中断问题时,就是因为没搞清楚这个互斥关系,导致映射表初始化失败,中断一直触发不了。
线性映射 vs 树形映射
这两种映射方式,是irq_domain的核心实现策略。选哪个,直接影响到中断响应性能和内存占用。
| 特性 | 线性映射 | 树形映射 |
|---|---|---|
| 数据结构 | 固定大小数组 | 基数树(radix tree) |
| 查找速度 | O(1),直接索引 | O(log n),树查找 |
| 内存占用 | 固定,可能浪费 | 按需分配,节省内存 |
| 适用场景 | 中断号连续且数量少 | 中断号稀疏或数量大 |
| Jacinto平台使用 | GIC主中断控制器 | GPIO、Mailbox等子控制器 |
线性映射:说白了就是一张大表。硬件中断号作为数组下标,直接拿到对应的Linux IRQ号。速度快,但浪费内存。比如GIC有256个中断,你就得开一个256个元素的数组。Jacinto的GIC-500就用的线性映射,因为它的中断号是连续的0~255。
树形映射:用基数树来存储映射关系。硬件中断号作为key,Linux IRQ号作为value。查找时走树路径,速度稍慢但内存省。GPIO控制器经常用这个,因为GPIO引脚的中断号可能只用了几个,开大数组太浪费。
我的经验:在Jacinto J721E上做GPIO中断时,我一开始用了线性映射,结果系统启动时分配了2MB的映射表,实际只用了几十个中断。后来改成树形映射,内存占用降到几十KB。所以,中断号稀疏的场景,千万别偷懒用线性映射。
映射流程实战
在Jacinto平台BSP中,中断映射的典型流程是这样的:
- 设备树解析:内核解析
interrupt-parent和interrupts属性,找到中断控制器节点 - irq_domain创建:中断控制器驱动调用
irq_domain_add_linear()或irq_domain_add_tree() - 映射建立:调用
irq_create_mapping()或irq_of_parse_and_map() - 中断处理:硬件中断触发,GIC通过
irq_find_mapping()找到对应的IRQ号
看一段实际代码,这是我之前在Jacinto平台上写的一个GPIO中断控制器的映射初始化:
static int gpio_irq_domain_init(struct gpio_chip *gc)
{
struct device_node *np = gc->parent->of_node;
struct irq_domain *domain;
/* 创建树形映射域,因为GPIO中断号可能不连续 */
domain = irq_domain_add_linear(np, gc->ngpio,
&gpio_irq_domain_ops, gc);
if (!domain) {
dev_err(gc->parent, "Failed to add irq domain\n");
return -ENOMEM;
}
gc->irq.domain = domain;
return 0;
}
这里我用了irq_domain_add_linear(),因为GPIO引脚数量固定(比如J721E的GPIO有84个),而且中断号是连续的0~83,线性映射更合适。嗯,这里要注意:irq_domain_ops里的.map和.unmap回调一定要实现,否则中断来了内核找不到处理函数。
避坑指南:我曾经在调试一个SPI中断时,发现中断号映射成功了,但中断就是不触发。查了两天才发现,设备树里interrupt-cells配错了,导致解析出来的硬件中断号偏移了4位。所以,设备树的中断属性一定要跟中断控制器的#interrupt-cells严格匹配。
映射关系可视化
为了让你更直观地理解整个映射流程,我画了一张图:
映射操作函数
在实际开发中,你主要会用这几个API:
| 函数 | 作用 | 使用场景 |
|---|---|---|
irq_domain_add_linear() |
创建线性映射域 | GIC、GPIO等连续中断号 |
irq_domain_add_tree() |
创建树形映射域 | Mailbox、PRU等稀疏中断 |
irq_create_mapping() |
建立单个映射 | 动态分配IRQ号 |
irq_find_mapping() |
根据hwirq查找virq | 中断处理流程 |
irq_dispose_mapping() |
释放映射 | 驱动卸载时 |
我个人习惯在设备树解析完成后,立即调用irq_create_mapping()建立映射,而不是等到中断注册时再做。这样可以在驱动初始化阶段就发现映射问题,而不是等到运行时才崩溃。
调试技巧:在/proc/interrupts里可以看到映射后的IRQ号。如果发现某个中断的计数一直为0,先检查映射是否成功——用cat /sys/kernel/debug/irq_domain_mapping查看当前所有映射关系。
Jacinto平台的特殊处理
Jacinto平台的多核架构让中断映射多了一层复杂性。GIC-500支持SPI(共享外设中断)和PPI(私有外设中断),它们的映射方式不同:
- SPI:全局中断号,所有核共享,用线性映射
- PPI:每个核私有,中断号重复,用树形映射+核ID区分
我记得在调试J721E的DMA中断时,发现同一个硬件中断号在不同核上映射到了不同的Linux IRQ号。查了GIC手册才知道,PPI中断的映射必须带上cpu_mask参数,否则映射会混乱。
嗯,这里还要注意:Jacinto的中断控制器级联也很常见。比如GPIO控制器挂在GIC下面,GPIO自己的中断号是0~83,但到了GIC层面,它只占一个SPI中断号。这种两级映射,需要分别在GPIO域和GIC域建立irq_domain。我见过不少新手在这里栽跟头——只配了GIC的映射,忘了配GPIO域的映射,结果GPIO子中断全都不工作。