IRQ Domain
概述
在 linux kernel中,我们使用下面两个ID来标识一个来自外设的中断:
1)IRQ number,即virq.CPU需要为每一个外设中断编号,我们称之IRQ Number。这个IRQ number是一个虚拟的interrupt ID,和硬件无关,仅仅是被CPU用来标识一个外设中断。
2)HW interrupt ID,hwirq.对于 interrupt controller 而言,它收集了多个外设的中断信号并向上传递,因此,interrupt controller 需要对外设中断进行编码。Interrupt controller用 HW interrupt ID来标识外设的中断。在interrupt controller级联的情况下,仅仅用HW interrupt ID已经不能唯一标识一个外设中断,还需要知道该HW interrupt ID所属的interrupt controller。
(hwirq – > root interrupt controller,hwirq’ – > sub interrupt controller……)
这样,CPU和interrupt controller在标识中断上就有了一些不同的概念,但是,对于驱动工程师而言,我们和CPU视角是一样的,我们只希望得到一个IRQ number(virq),而不关系具体是那个interrupt controller上的那个HW interrupt ID(hwirq,hwirq’)。这样一个好处是在中断相关的硬件发生变化的时候,驱动软件不需要修改。因此,linux kernel中的中断子系统需要提供一个将HW interrupt ID映射到IRQ number上来的机制,就是“域”,即 irq domain。
随着linux kernel的发展,将interrupt controller抽象成irqchip这个概念越来越流行,甚至GPIO controller也已经被看成一个interrupt controller chip,这样,系统中至少有两个中断控制器了(如mt6853中,pio and gic),一个传统意义的中断控制器,一个是GPIO controller type的中断控制器。随着系统复杂度加大,外设中断数据增加,实际上系统可以需要多个中断控制器进行级联,面对这样的趋势,linux kernel工程师如何应对?答案就是irq domain这个概念。
irq domain,顾名思义,就是领域,范围的意思,也就是说,任何的定义出了这个范围就没有意义了。系统中所有的interrupt controller会形成树状结构,对于每个 interrupt controller 都可以连接若干个外设的中断请求(我们称之interrupt source),interrupt controller会对连接其上的interrupt source(根据其在Interrupt controller中物理特性)进行编号(也就是HW interrupt ID了)。但这个编号仅仅限制在本interrupt controller范围内。
接口
3.2.1向系统注册irq domain
Radix Tree map。建立一个Radix Tree来维护HW interrupt ID到IRQ number映射关系。HW interrupt ID作为lookup key,在Radix Tree检索到IRQ number。如果的确不能满足线性映射的条件,可以考虑Radix Tree map。实际上,我们MT6853kernel-4.14就是使用基数树的方式。注册 irq_domain,其接口API如下:
IRQCHIP_DECLARE(gic_v3, "arm,gic-v3", gicv3_of_init); err=gic_init_bases(dist_base,rdist_regs,nr_redist_regions,redist_stride,&node->fwnode);
gic_data.domain = irq_domain_create_tree(handle, &gic_irq_domain_ops,&gic_data);
return __irq_domain_add(fwnode, 0, ~0, 0, ops, host_data);
3.2.2为irq domain创建映射
上节的内容主要是向系统注册一个irq domain,具体HW interrupt ID和IRQ number的映射关系都是空的,因此,具体各个irq domain如何管理映射所需要的database还是需要建立的。对于Radix Tree map,我们要把那个反应IRQ number和HW interrupt ID的Radix tree建立起来。创建映射接口函数:
通常,一个普通设备的device tree node已经描述了足够的中断信息,在这种情况下,该设备的驱动在初始化的时候可以调用irq_of_parse_and_map这个接口函数进行该device node中和中断相关的内容(interrupts和interrupt-parent属性)进行分析,并建立映射关系,具体代码如下:
/**
* irq_of_parse_and_map - Parse and map an interrupt into linux virq space
* @dev: Device node of the device whose interrupt is to be mapped
* @index: Index of the interrupt to map
*
* This function is a wrapper that chains of_irq_parse_one() and
* irq_create_of_mapping() to make things easier to callers
*/
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
{
struct of_phandle_args oirq;
/*分析device node中的interrupt相关属性*/
if (of_irq_parse_one(dev, index, &oirq))
return 0;
/* 创建映射,并返回对应的 IRQ number */
return irq_create_of_mapping(&oirq);
}
对于一个使用Device tree的普通驱动程序,基本上初始化需要调用irq_of_parse_and_map获取IRQ number,然后调用request_threaded_irq申请中断handler。
比如在我们FP code中;
node = of_find_compatible_node(NULL, NULL, FP_IRQ_OF);//关联dts节点
fp_dev->int_port = irq_of_parse_and_map(node, 0);//通过dts中irq的描述获取virq;
数据结构描述
3.1 irq domain 的callback接口
struct irq_domain_ops 抽象了一个irq domain 的 callback 函数,定义如下:
struct irq_domain_ops {
int (*match)(struct irq_domain *d, struct device_node *node,
enum irq_domain_bus_token bus_token);
int (*select)(struct irq_domain *d, struct irq_fwspec *fwspec,
enum irq_domain_bus_token bus_token);
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);
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
/* extended V2 interfaces to support hierarchy irq_domains */
int (*alloc)(struct irq_domain *d, unsigned int virq,
unsigned int nr_irqs, void *arg);
void (*free)(struct irq_domain *d, unsigned int virq,
unsigned int nr_irqs);
void (*activate)(struct irq_domain *d, struct irq_data *irq_data);
void (*deactivate)(struct irq_domain *d, struct irq_data *irq_data);
int (*translate)(struct irq_domain *d, struct irq_fwspec *fwspec,
unsigned long *out_hwirq, unsigned int *out_type);
#endif
};
我们先看 translate函数,语义是翻译(translate)的意思,那么到底翻译什么呢?在DTS文件中,各个使用中断的device node会通过一些属性(例如interrupts和interrupt-parent属性)来提供中断信息给kernel以便kernel可以正确的进行driver的初始化动作。这里,interrupts属性所表示的interrupt specifier只能由具体的interrupt controller(也就是irq domain)来解析。而translate函数就是将指定的设备(node参数)上若干个(intsize参数)中断属性(intspec参数)翻译成HW interrupt ID(out_hwirq参数)和trigger类型(out_type)。
比如,
if (is_fwnode_irqchip(fwspec->fwnode)) {
if(fwspec->param_count != 2)
return -EINVAL;
*hwirq = fwspec->param[0];
*type = fwspec->param[1];
return 0;
}
&silead_fp {
interrupt-parent = <&pio>;
interrupts = <5 IRQ_TYPE_EDGE_RISING>;
status = "okay";
};
可见,通过translate函数获取dts里面中断属性中的hwirq和触发类型;
3.2 irq domain
在内核中,irq domain 的概念由 struct irq_domain 表示
/* Hardware interrupt number translation object */
struct irq_domain {
struct list_head link;
const char *name;
const struct irq_domain_ops *ops;
void *host_data;
unsigned int flags;
unsigned int mapcount;
/* Optional data */
struct fwnode_handle *fwnode;
enum irq_domain_bus_token bus_token;
struct irq_domain_chip_generic *gc;
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
struct irq_domain *parent;
#endif
#ifdef CONFIG_GENERIC_IRQ_DEBUGFS
struct dentry *debugfs_file;
#endif
/* reverse map data. The linear map gets appended to the irq_domain */
irq_hw_number_t hwirq_max;
unsigned int revmap_direct_max_irq;
unsigned int revmap_size;
struct radix_tree_root revmap_tree;
unsigned int linear_revmap[];
};
linux内核中,所有的irq domain被挂入一个全局链表,链表头定义如下:
static LIST_HEAD(irq_domain_list);
list_add(&domain->link, &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 数据结构)。
对于Radix Tree map:
1)linear_revmap 没有用,revmap_size等于0。
2)hwirq_max 没有用,设定为一个最大值。
3)revmap_direct_max_irq 没有用,设定为0。
4)revmap_tree 指向Radix tree的root node。
中断在Device Tree 的描述
想要进行映射,首先要了解interrupt controller的拓扑结构。系统中的interrupt controller的拓扑结构以及其interrupt request line的分配情况(分配给哪一个具体的外设)都在Device Tree Source 文件中通过下面的属性给出了描述,这里简单总结一下:
用我们FP中的例子,
&silead_fp {
interrupt-parent = <&pio>;
interrupts = <5 IRQ_TYPE_EDGE_RISING>;/* hwirq:5;irq type:IRQ_TYPE_EDGE_RISING */
status = "okay";
};
对于那些产生中断的外设,我们需要定义interrupt-parent和interrupts属性:
1)interrupt-parent。表明该外设的interrupt request line物理的连接到了哪一个中断控制器上
2)interrupts。这个属性描述了具体该外设产生的interrupt的细节信息(也就是传说中的interrupt specifier)。例如:HW interrupt ID(由该外设的device node中的interrupt-parent指向的interrupt controller解析)、interrupt触发类型等。
对于Interrupt controller,我们需要定义interrupt-controller和#interrupt-cells的属性:
pio: pinctrl {
compatible = "mediatek,mt6853-pinctrl";
reg_bases = <&gpio>,
<&iocfg_rb>,
<&iocfg_rm>,
<&iocfg_bm>,
<&iocfg_bl>,
<&iocfg_br>,
<&iocfg_lm>,
<&iocfg_rt>,
<&iocfg_tl>;
reg_base_eint = <&eint>;
pins-are-numbered;
gpio-controller;
gpio-ranges = <&pio 0 0 203>;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;/* 用2个32位的数据描述中断 ,比如, interrupts = <5 IRQ_TYPE_EDGE_RISING>;*/
interrupts = <GIC_SPI 212 IRQ_TYPE_LEVEL_HIGH>;/* 从拓扑结构看,这个是有gic中#interrupt-cells 的属性值决定 */
interrupt-parent = <&gic>;/* root intc*/
};
1)interrupt-controller。表明该 device node 就是一个中断控制器,这里 pio node 既是 GPIO控制器,也是中断控制器;
2)#interrupt-cells。该中断控制器用多少个cell(一个cell就是一个32-bit的单元)描述一个外设的interrupt request line。?具体每个cell表示什么样的含义由interrupt controller自己定义。
3)interrupts 和 interrupt-parent。对于那些不是root 的interrupt controller,其本身也是作为一个产生中断的外设连接到其他的interrupt controller上,因此也需要定义interrupts和interrupt-parent的属性。
Mapping DB的建立
3.5.1 Hwirq 和 virq 映射创建流程
系统中HW interrupt ID(hwirq)和IRQ number(virq)的 mapping DB是在整个系统初始化的过程中建立起来的,过程如下:
1)DTS文件描述了系统中的interrupt controller以及外设IRQ的拓扑结构,在linux kernel启动的时候,由bootloader传递给kernel(实际传递的是DTB)。
2)在Device Tree初始化的时候,形成了系统内所有的device node的树状结构,当然其中包括所有和中断拓扑相关的数据结构
(所有的interrupt controller的node和使用中断的外设node)
3)在machine driver初始化的时候会调用 of_irq_init 函数,在该函数中会扫描所有interrupt controller的节点,并调用适合的
interrupt controller driver进行初始化。毫无疑问,初始化需要注意顺序,首先初始化root,然后first level,second level,最后是leaf node。
在初始化的过程中,一般会调用上节中的接口函数向系统增加 irq domain。有些interrupt controller会在其driver初始化的过程中创建映射
flow:kernel 初始化过程 mt6853_r_tee/kernel-4.14$ vim init/main.c +638
asmlinkage __visible void __init start_kernel(void)
init_IRQ();
irqchip_init();/* mt6853_r_tee/kernel-4.14$ vim arch/arm64/kernel/irq.c +96 */
of_irq_init(__irqchip_of_table);/* mt6853_r_tee/kernel-4.14$ vim drivers/irqchip/irqchip.c +29,dtb数据置于__irqchip_of_table中*/
void __init of_irq_init(const struct of_device_id *matches)/* mt6853_r_tee/kernel-4.14$ vim drivers/of/irq.c +485,Scan and init matching interrupt controllers in DT*/
4)在各个driver初始化的过程中,创建映射
3.5.2 interrupt controller 初始化的过程中,注册 irq domain
我们以 GIC-V3 的代码为例。具体代码在
mt6853_r_tee/kernel-4.14/drivers/irqchip$ vim irq-gic-v3.c +1317
IRQCHIP_DECLARE(gic_v3, "arm,gic-v3", gicv3_of_init);
static int __init gicv3_of_init(struct device_node *node, struct device_node *parent)
void __init gic_init_bases(unsigned int gic_nr, int irq_start,void __iomem *dist_base, void __iomem *cpu_base,u32 percpu_offset, struct device_node *node) gic_data.domain=irq_domain_create_tree(handle,&gic_irq_domain_ops,&gic_data);/* 注册 irq domain*/
return__irq_domain_add(fwnode,0, ~0, 0, ops, host_data);list_add(&domain->link, &irq_domain_list);/* 实则是把 irq domain 添加到链表 irq_domain_list 中 */
3.5.3 在各个硬件外设的驱动初始化过程中,创建HW interrupt ID和IRQ number的映射关系
我们上面的描述过程中,已经提及:设备的驱动在初始化的时候可以调用 irq_of_parse_and_map 这个接口函数进行该device node中和中断相关的内容(interrupts和interrupt-parent属性)进行分析,并建立映射关系,具体代码如下:
flow:
/*
IRQCHIP_DECLARE(gic_v3, "arm,gic-v3", gicv3_of_init);
gic_of_setup_kvm_info(node);
gic_v3_kvm_info.maint_irq = irq_of_parse_and_map(node, 0);
/**
* irq_of_parse_and_map - Parse and map an interrupt into linux virq space
* @dev: Device node of the device whose interrupt is to be mapped
* @index: Index of the interrupt to map
*
* This function is a wrapper that chains of_irq_parse_one() and
* irq_create_of_mapping() to make things easier to callers
*/
*/ ------ > 这一块 gic-v3 的暂时不用理会,在gic-v3初始化过程中这是属于 kvm部分的;
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
{
struct of_phandle_args oirq;
/* 分析device node中的interrupt相关属性 */
if (of_irq_parse_one(dev, index, &oirq))
return 0;
return irq_create_of_mapping(&oirq);/* 创建映射 */
}
我们再来看看 irq_create_of_mapping 函数如何创建映射:
unsigned int irq_create_of_mapping(struct of_phandle_args *irq_data)
{
struct irq_fwspec fwspec;
of_phandle_args_to_fwspec(irq_data, &fwspec);/* get dts 中 irq 数据,并置于 fwspec 中 */
return irq_create_fwspec_mapping(&fwspec);
}
接下来重点分析一下 irq_create_fwspec_mapping
if (fwspec->fwnode) {
domain = irq_find_matching_fwspec(fwspec, DOMAIN_BUS_WIRED);
if (!domain)
domain = irq_find_matching_fwspec(fwspec, DOMAIN_BUS_ANY);
} else {
domain = irq_default_domain;
}
irq_find_matching_fwspec
list_for_each_entry(h, &irq_domain_list, link) {
if (h->ops->select && fwspec->param_count)
rc = h->ops->select(h, fwspec, bus_token);
gic_irq_domain_select
return d == gic_data.domain;/即,调用 gic-v3 初始化过程中创建的域,gic_data.domain = irq_domain_create_tree(handle, &gic_irq_domain_ops, &gic_data);
else if (h->ops->match)
rc = h->ops->match(h, to_of_node(fwnode), bus_token);
else
rc = ((fwnode != NULL) && (h->fwnode == fwnode) &&
((bus_token == DOMAIN_BUS_ANY) ||
(h->bus_token == bus_token)));
if (rc) {
found = h;
break;
}
}
A:这里的代码主要是找到 irq domain。这是根据传递进来的参数 fwspec寻找的,本质上是通过遍寻链表的方式查找:
if (irq_domain_translate(domain, fwspec, &hwirq, &type))
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
if (d->ops->translate)/* 在 mt6853中是 通过 irq_domain->ops->translate的函数获取&hwirq, &type */
return d->ops->translate(d, fwspec, hwirq, type);
gic_irq_domain_select
return d == gic_data.domain;/*即,调用 gic-v3 初始化过程中创建的域,gic_data.domain = irq_domain_create_tree(handle, &gic_irq_domain_ops, &gic_data);*/
#endif
if (d->ops->xlate)
return d->ops->xlate(d, to_of_node(fwspec->fwnode),
fwspec->param, fwspec->param_count,
hwirq, type);
/* If domain has no translation, then we assume interrupt line */
*hwirq = fwspec->param[0];
return 0;
B:通过 dts 中中断描述获取 hwirq 和 irq type;流程如下:
Flow:
if (irq_domain_translate(domain, fwspec, &hwirq, &type))
return d->ops->translate(d, fwspec, hwirq, type);
gic_irq_domain_translate
*hwirq = fwspec->param[0];
*type = fwspec->param[1];
virq = irq_create_mapping(domain, hwirq);
virq = irq_find_mapping(domain, hwirq);/*先判断是否存在 virq ,在这里应该是能获取到的*/
virq = irq_domain_alloc_descs(-1, 1, hwirq, of_node_to_nid(of_node), NULL);/* 分配一个virq */
__irq_alloc_descs
start = bitmap_find_next_zero_area(allocated_irqs, IRQ_BITMAP_BITS,from, cnt, 0);/*从 allocated_irqs 数组中查找到一个为0的数对应的值作为virq */
ret = alloc_descs(start, cnt, node, affinity, owner);
desc = alloc_desc(start + i, node, flags, mask, owner);
irq_insert_desc(start + i, desc);
radix_tree_insert(&irq_desc_tree, irq, desc);/* 即用 radix tree 方式以hwirq 为索引,存储 irq_desc */
C:解析完了,最终还是要调用 irq_create_mapping 函数来创建HW interrupt ID和IRQ number的映射关系。
irq_data = irq_get_irq_data(virq);
irqd_set_trigger_type(irq_data, type);
`D:调用 irq_set_irq_type 函数设定 trigger type
irq_create_mapping 函数建立HW interrupt ID和IRQ number的映射关系。该接口函数以 irq domain 和 HW interrupt ID 为参数,返回IRQ number。具体的代码如下:
``
```c
unsigned int irq_create_mapping(struct irq_domain *domain,
irq_hw_number_t hwirq)
{
unsigned int hint;
int virq;
/* 判断virq,如果映射已经存在,那么不需要映射,直接返回 */
/* Check if mapping already exists */
virq = irq_find_mapping(domain, hwirq);
if (virq) {
pr_debug("-> existing mapping on virq %d\n", virq);
return virq;
}
/* Allocate a virtual interrupt number */
virq = irq_domain_alloc_descs(-1, 1, hwirq, of_node_to_nid(of_node), NULL);
if (virq <= 0) {
pr_debug("-> virq allocation failed\n");
return 0;
}
if (irq_domain_associate(domain, virq, hwirq)) {---建立mapping
ret = domain->ops->map(domain, virq, hwirq);/* 通过 irq_domain->ops->map建立映射关系 */
irq_domain_set_mapping(domain, hwirq, irq_data);
radix_tree_insert(&domain->revmap_tree, hwirq, irq_data);/* --向radix tree插入一个node */
return __radix_tree_insert(root, index, 0, entry);
irq_free_desc(virq);
return 0;
}
return virq;
}
通过以上流程,我们就可以获取一个virq以及其对应的中断描述符了(struct irq_desc *desc = irq_to_desc(irq);即以virq为irq_desc[virq]数组下标)。(这种是简单的获取方式)
E.获取对应的 irq_desc ;
Flow:
virq = irq_find_mapping(domain, hwirq);
data = radix_tree_lookup(&domain->revmap_tree, hwirq);
return data ? data->irq : 0;
可见,是通过 hwirq 为索引,radix tree的方式获取;
创建和获取流程如下所示: