大家好,我是王鸽,这篇文章主要是介绍一下中断管理的映射,中断号是怎么来的,以及中断的映射的过程等内容。
首先在Linux kernel 中,使用两个ID来标识一个来自外设的中断。
HW interrupt ID
对于interrupt controller而言,它收集了多个外设的interrupt request line并向上传递,因此,interrupt controller需要对外设中断进行编码。Interrupt controller用HW interrupt ID来标识外设的中断。在interrupt controller级联的情况下,仅仅用HW interrupt ID已经不能唯一标识一个外设中断,还需要知道该HW interrupt ID所属的interrupt controller(HW interrupt ID在不同的Interrupt controller上是会重复编码的)。
中断号
IRQ number,每个中断都有一个中断号,通过中断号即可区分不同的中断,中断号叫中断线。在 Linux 内核中使用一个 int 变量表示中断号,这个IRQ number是一个虚拟的interrupt ID,和硬件无关,仅仅是被CPU用来标识一个外设中断。
HW interrupt ID映射到IRQ number这个就是本篇文章重点要讲述的。
irq_domain引入
随着SOC硬件接口使用的中断越来越多,导致SOC芯片不能单单一个中断控制器,所以需要多个中断控制器,这些中断控制器可以级联,一个中断控制器作为中断源连接到另一个中断控制器,但只有一个中断控制器作为根控制器直接连接到处理器。为了把每个中断控制器本地的硬件中断号映射到全局唯一的 Linux 中断号(也称为虚拟中断号),内核定义了中断域 irq_domain,每个中断控制器有自己的中断域。
向系统注册irq_domain
中断控制器的驱动程序使用分配函数 irq_domain_add_*()创建和注册中断域。 每种映射方法提供不同的分配函数,调用者必须给分配函数提供 irq_domain_ops 结构体,分配函数在执行成功的时候返回 irq_domain 的指针,通用中断处理模块中有一个irq domain的子模块,该模块将这种射关系分成了三类:
1. 线性映射。
其实就是一个lookup table,HW interrupt ID作为index,通过查表可以获取对应的IRQ number。对于Linear map而言,interrupt controller对其HW interrupt ID进行编码的时候要满足一定的条件:hw ID不能过大,而且ID排列最好是紧密的。对于线性映射,其接口API如下:
static inline struct irq_domain *irq_domain_add_linear(struct device_node *of_node, unsigned int size,---------该interrupt domain支持多少IRQ const struct irq_domain_ops *ops,---callback函数 void *host_data)-----driver私有数据{ return __irq_domain_add(of_node, size, size, 0, ops, host_data);}
2.树映射Radix Tree map。
建立一个Radix Tree来维护HW interrupt ID到IRQ number映射关系。HW interrupt ID作为lookup key,在Radix Tree检索到IRQ number。如果的确不能满足线性映射的条件,可以考虑Radix Tree map。实际上,内核中使用Radix Tree map的只有powerPC和MIPS的硬件平台。对于Radix Tree map,其接口API如下:
static inline struct irq_domain *irq_domain_add_tree(struct device_node *of_node, const struct irq_domain_ops *ops, void *host_data){ return __irq_domain_add(of_node, 0, ~0, 0, ops, host_data);}
3.不映射( no map)。
有些中断控制器很强,硬件中断号是可以配置的,例如 PowerPC 架构使用的 MPIC( Multi-Processor Interrupt Controller)。我们直接把 Linux 中断号写到硬件,硬件中断号就是Linux 中断号,不需要映射。对于不映射,分配中断域的函数如下:
static inline struct irq_domain *irq_domain_add_nomap(struct device_node *of_node, unsigned int max_irq, const struct irq_domain_ops *ops, void *host_data){ return __irq_domain_add(of_node, 0, max_irq, max_irq, ops, host_data);}
这类接口的逻辑很简单,根据自己的映射类型,初始化struct irq_domain中的各个成员,调用__irq_domain_add将该irq_domain挂入irq_domain_list的全局列表。
所有的irq domain被挂入一个全局链表,链表头定义如下:
staticLIST_HEAD(irq_domain_list);
struct irq_domain中的link成员就是挂入这个队列的节点。通过irq_domain_list这个指针,可以获取整个系统中HW interrupt ID和IRQ number的mapping DB。host_data定义了底层interrupt controller使用的私有数据,和具体的interrupt controller相关(对于GIC,该指针指向一个struct gic_chip_data数据结构)。
将HW interrupt ID转成IRQ number函数
中断域是连接 hwirq 和 virq 的桥梁,所有转换操作都依赖中断域完成。向系统注册一个irq domain,具体HW interrupt ID和IRQ number的映射关系都是空的,因此,具体各个irq domain如何管理映射所需要的database还是需要建立的。例如:对于线性映射的irq domain,我们需要建立线性映射的lookup table,对于Radix Tree map,我们要把那个反映IRQ number和HW interrupt ID的Radix tree建立起来。创建映射有四个接口函数,代码位于kernel/kernel/irq/irqdomain.c。1. 调用irq_create_mapping函数
建立HW interrupt ID和IRQ number的映射关系。该接口函数以irq domain和HW interrupt ID为参数,返回IRQ number(这个IRQ number是动态分配的)。该函数的原型定义如下:
extern unsigned int irq_create_mapping(struct irq_domain *host,映射建立后,调用 irq_create_mapping() 建立映射,得到内核虚拟中断号virq,virq 与 domain、hwirq 的关系是一对一的,一个 virq 只会对应一个 domain 和一个 hwirq。unsignedinthwirq_to_virq_generic(struct irq_domain *domain, irq_hw_number_t hwirq){ unsigned int virq; // 1. 合法性检查(中断域和hwirq有效,此处简化hwirq范围检查) if (!domain) { pr_err("Invalid irq domain\n"); return 0; } // 2. 核心转换:创建映射并返回virq(完成hwirq -> virq转换) virq = irq_create_mapping(domain, hwirq); // 3. 结果判断 if (!virq) { pr_err("Failed to convert hwirq %lu to virq\n", hwirq); } return virq;}
2.irq_create_strict_mappings。
这个接口函数用来为一组HW interrupt ID建立映射。externintirq_create_strict_mappings(struct irq_domain *domain,unsigned int irq_base,irq_hw_number_t hwirq_base, int count);
3.irq_create_of_mapping。
看到函数名字中的of(open firmware)。这是设备树场景下的封装方法,会自动从设备树中解析 hwirq 和 irq_domain,然后完成转换,无需手动提取这两个核心参数,更贴合现代嵌入式 Linux 驱动开发。irq_create_of_mapping() 是设备树场景下的专用中断映射函数,核心功能是:从设备树中断描述信息中提取出「中断域(irq_domain)」和「硬件中断号(hwirq)」,自动调用irq_create_mapping()建立映射,最终返回内核虚拟中断号(virq)。通常,一个普通设备的device tree node已经描述了足够的中断信息,在这种情况下,该设备的驱动在初始化的时候可以调用irq_of_parse_and_map这个接口函数进行该device node中和中断相关的内容(interrupts和interrupt-parent属性)进行分析,并建立映射关系。unsignedintirq_of_parse_and_map(struct device_node *dev, int index){struct of_irq oirq;if (of_irq_map_one(dev, index, &oirq))//分析device node中的interrupt相关属性return 0;return irq_create_of_mapping(oirq.controller, oirq.specifier, oirq.size);//创建映射,并返回对应的IRQ number}EXPORT_SYMBOL_GPL(irq_of_parse_and_map);
另外现代设备树驱动推荐方法(of_irq_get()/devm_of_irq_get())这是设备树场景下的更高层封装,内部会自动完成「设备树中断信息解析 → hwirq 转 virq → 映射建立」全流程,返回值直接是可用的 virq,无需手动调用上述两个转换函数,是现代驱动开发的首选。好处是快速获取设备对应的 virq,无需关心底层 hwirq 和 irq_domain 细节的场景。#include<linux/of_irq.h>#include<linux/device.h>// 转换函数(现代设备树驱动首选,示意)unsignedinthwirq_to_virq_modern(struct device_node *dev_node, unsignedint irq_idx){ unsigned int virq; // 核心转换:一步完成设备树解析 + hwirq -> virq 转换 // irq_idx:中断索引(设备有多个中断时,指定第几个,从0开始) virq = of_irq_get(dev_node, irq_idx); // 结果判断(注意:of_irq_get() 失败返回负数错误码,与前两个函数不同!) if (virq <= 0) { pr_err("Failed to convert hwirq to virq, err: %d\n", (int)virq); return 0; } return virq;}
4.irq_create_direct_mapping。
这是给no map那种类型的interrupt controller使用的.数据结构描述
一个中断控制器用一个struct irq_domain数据结构来描述,struct irq_domain数据结构定义如下:- link:用于将 irq_domain 连接到全局链表 irq_domain_list 中
- fwnode:对应中断控制器的 device node
- parent:指向父级 irq_domain 的指针,用于支持级联 irq_domain
- hwirq_max:该 irq_domain 支持的中断最大数量
- linear_revmap[]:hwirq->virq 反向映射的线性表
其中struct irq_domain_ops 是 irq_domain 映射操作函数集struct irq_domain_ops {int (*match)(struct irq_domain *d, struct device_node *node);int (*map)(struct irq_domain *d, unsigned int virq, irq_hw_number_t hw);void (*unmap)(struct irq_domain *d, unsigned int virq);int (*xlate)(struct irq_domain *d, struct device_node *node, const u32 *intspec, unsigned int intsize, unsigned long *out_hwirq, unsigned int *out_type);};
- match:用于中断控制器设备与 irq_domain 的匹配;
- map:用于硬件中断号与 Linux 中断号的映射;
- xlate:通过 device_node,解析硬件中断号和触发方式。
中断号映射流程
最开始的DTB文件,到初始化DeviceTree的时候关于中断拓扑数据结构,然后在of_irq_init中调用合适的驱动进行初始化,最后在驱动初始化中创建映射关系。驱动入口注册 → 设备树匹配 → 探针函数执行(核心)→ 硬件初始化 → 创建irq_domain → 注册中断操作集 → 完成加载。
代码流程
start_kernel() -> init_IRQ() -> irqchip_init()->of_irq_init(const struct of_device_id*matches)->gic_of_init(struct device_node*node, struct device_node*parent)->函数__gic_init_basesvoid __init gic_init_bases(unsignedint gic_nr, int irq_start, void __iomem *dist_base, void __iomem *cpu_base, u32 percpu_offset, struct device_node *node){ irq_hw_number_t hwirq_base; struct gic_chip_data *gic; int gic_irqs, irq_base, i;/* * For primary GICs, skip over SGIs. * For secondary GICs, skip over PPIs, too. */if (gic_nr == 0 && (irq_start & 31) > 0) { hwirq_base = 16;if (irq_start != -1) irq_start = (irq_start & ~31) + 16; } else { hwirq_base = 32; }对于root GIC hwirq_base = 16; gic_irqs = 系统支持的所有的中断数目-16。之所以减去16主要是因为root GIC的0~15号HW interrupt 是for IPI的,因此要去掉。也正因为如此hwirq_base从16开始 irq_base = irq_alloc_descs(irq_start, 16, gic_irqs, numa_node_id());申请gic_irqs个IRQ资源,从16号开始搜索IRQ number。由于是root GIC,申请的IRQ基本上会从16号开始 gic->domain = irq_domain_add_legacy(node, gic_irqs, irq_base, hwirq_base, &gic_irq_domain_ops, gic);---向系统注册irq domain并创建映射}
其中irq_domain_add_legacy函数irq_domain_add_legacy()是 Linux 内核中断域子系统中,用于创建并注册一个「传统型中断域」的函数,核心特点是hwirq和virq预先建立了固定线性映射关系,无需调用任何函数,直接通过公式计算即可完成转换。virq = first_irq + (hwirq - first_hwirq)。struct irq_domain *irq_domain_add_legacy(struct device_node *of_node, unsigned int size, unsigned int first_irq, irq_hw_number_t first_hwirq, const struct irq_domain_ops *ops, void *host_data){ struct irq_domain *domain; domain = __irq_domain_add(of_node, first_hwirq + size,----注册irq domain first_hwirq + size, 0, ops, host_data); if (!domain) return NULL; irq_domain_associate_many(domain, first_irq, first_hwirq, size); ---创建映射 return domain;}
struct device_node *of_node:设备树节点指针
unsigned int size:中断域支持的中断数量(连续中断的个数)
unsigned int first_irq:起始内核虚拟中断号
irq_hw_number_t first_hwirq:起始硬件中断号,
指定该中断域管理的「连续硬件中断号区间」的起始值,后续该中断域的size个硬件中断号为[first_hwirq, first_hwirq + size - 1]。示例:若first_hwirq = 32,size = 16,则该中断域管理的hwirq区间是 32~47。补充:与first_irq对应,两者的区间长度都是size,形成「一一固定映射」:hwirq = first_hwirq + n对应virq = first_irq + n(其中n取值 0~size-1)。const struct irq_domain_ops *ops:中断域操作集。void *host_data:中断域私有数据指针,函数会将该指针存入创建的struct irq_domain结构体的host_data成员中,后续可通过domain->host_data获取,若无需私有数据,可传入NULL。如何将HW interrupt ID转成IRQ number
其中 irq = irq_of_parse_and_map(node, 0);--解析second GIC的interrupts属性,并进行mapping,返回IRQ numbergic_cascade_irq(gic_cnt, irq);---设置handlervoid __initgic_cascade_irq(unsigned int gic_nr, unsigned int irq){ if (irq_set_handler_data(irq, &gic_data[gic_nr]) != 0)---设置handler data BUG(); irq_set_chained_handler(irq, gic_handle_cascade_irq);---设置handler}
另外补充:反向转换(virq → hwirq)
如果需要从virq反向获取hwirq,可使用内核提供的irq_get_hwirq()函数。#include <linux/irq.h>irq_hw_number_t virq_to_hwirq(unsigned int virq){ struct irq_desc *desc; irq_hw_number_t hwirq; // 1. 获取irq描述符 desc = irq_to_desc(virq); if (!desc) { pr_err("Invalid virq %u\n", virq); return (irq_hw_number_t)-1; } // 2. 反向获取hwirq hwirq = irq_get_hwirq(virq); return hwirq;}
这篇文章核心总结是所有方法最终都依赖「中断域」建立hwirq与virq的映射,中断域是转换的核心桥梁。