中断号映射:硬件中断号与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中,中断映射的典型流程是这样的:

  1. 设备树解析:内核解析interrupt-parent和interrupts属性,找到中断控制器节点
  2. irq_domain创建:中断控制器驱动调用irq_domain_add_linear()或irq_domain_add_tree()
  3. 映射建立:调用irq_create_mapping()或irq_of_parse_and_map()
  4. 中断处理:硬件中断触发,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严格匹配。

映射关系可视化

为了让你更直观地理解整个映射流程,我画了一张图:

中断号映射流程 中断号映射流程 硬件中断源 hwirq = 0x30 GIC中断控制器 硬件中断号映射 irq_domain 线性/树形映射 线性映射 数组索引 O(1) 连续中断号 树形映射 基数树 O(log n) 稀疏中断号 Linux IRQ号 (virq) 中断处理函数执行 关键点 硬件中断号 → irq_domain映射 → Linux IRQ号,三者缺一不可

映射操作函数

在实际开发中,你主要会用这几个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子中断全都不工作。