1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. 分析背景
本文分析基于 linux-4.14.132
内核、ARM32
架构代码进行分析。
3. 实现
本篇以 ARM 架构的 GIC 芯片
为例进行分析。
3.1 硬件基础
使用 GIC
芯片的 SoC,典型的中断硬件拓扑结构如下:
其中:
SGI(Software-generated interrupt):用于处理期之间的通信,编号区间为 0~15 ,对于每个处理器,都是相同的编号区间;
PPI(Private peripheral interrupt):处理器私有中断,编号区间为 16~31 ,处理器私有编号,每个处理器都是相同的编号区间;
SPI(Shared peripheral interrupt):全局中断,可发送给任一处理器,编号区间为 32~1019 ,且编号全局唯一。
本文不讲述更多中断相关的硬件知识,读者可自行请参考手册
《ARM Architecture Reference Manual.pdf》
《DDI0406C_d_armv7ar_arm.pdf》
《IHI0048A_gic_architecture_spec_v1_0.pdf》
的相关章节。
3.2 中断初始化
3.2.1 早期初始化
start_kernel()
local_irq_disable(); /* 禁用 BOOT CPU 上的中断 */
early_boot_irqs_disabled = true;
setup_arch()
/* 中断向量表设置 */
paging_init(mdesc)
devicemaps_init(mdesc)
void *vectors;
vectors = early_alloc(PAGE_SIZE * 2);
early_trap_init(vectors)
vectors_page = vectors_base;
for (i = 0; i < PAGE_SIZE / sizeof(u32); i++)
((u32 *)vectors_base)[i] = 0xe7fddef1;
memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);
memcpy((void *)vectors + 0x1000, __stubs_start, __stubs_end - __stubs_start);
map.pfn = __phys_to_pfn(virt_to_phys(vectors));
map.virtual = 0xffff0000;
map.length = PAGE_SIZE;
#ifdef CONFIG_KUSER_HELPERS
map.type = MT_HIGH_VECTORS;
#else
map.type = MT_LOW_VECTORS;
#endif
create_mapping(&map);
if (!vectors_high()) {
map.virtual = 0;
map.length = PAGE_SIZE * 2;
map.type = MT_LOW_VECTORS;
create_mapping(&map);
}
/* Now create a kernel read-only mapping */
map.pfn += 1;
map.virtual = 0xffff0000 + PAGE_SIZE;
map.length = PAGE_SIZE;
map.type = MT_LOW_VECTORS;
create_mapping(&map);
early_abt_enable() @ arch/arm/mm/fault.c
fsr_info[FSR_FS_AEA].fn = early_abort_handler;
local_abt_enable();
fsr_info[FSR_FS_AEA].fn = do_bad;
/* 初始中断处理接口为 machine 定义的函数 */
handle_arch_irq = mdesc->handle_irq;
trap_init() // ARM32 为空操作
early_irq_init()
/*
* 初始化所有中断的默认 CPU 亲和性:
* 如果没有通过内核命令行参数 irqaffinity= 设置默认的中断 CPU 亲和性,
* 则将中断默认的 CPU 亲和性设置为系统中所有 CPU 。
*/
init_irq_default_affinity()
if (!cpumask_available(irq_default_affinity))
zalloc_cpumask_var(&irq_default_affinity, GFP_NOWAIT);
if (cpumask_empty(irq_default_affinity))
cpumask_setall(irq_default_affinity);
/* 探测架构预分配占用的 IRQ 中断数目: ARM32 下配置为 16 个 */
initcnt = arch_probe_nr_irqs();
/* 为探测到的、架构预分配占用的 IRQ 中断,分配中断描述符 */
for (i = 0; i < initcnt; i++) {
desc = alloc_desc(i, node, 0, NULL, NULL);
set_bit(i, allocated_irqs);
irq_insert_desc(i, desc);
radix_tree_insert(&irq_desc_tree, irq, desc);
}
arch_early_irq_init() // ARM32 为空操作
3.2.2 GIC芯片初始化
3.2.2.1 BOOT CPU 对 GIC 的初始化
上接 3.2.1
章节的分析:
start_kernel()
...
init_IRQ()
void __init init_IRQ(void)
{
if (IS_ENABLED(CONFIG_OF) && !machine_desc->init_irq)
irqchip_init();
else
machine_desc->init_irq();
...
}
void __init irqchip_init(void)
{
/*
* 表 __irqchip_of_table[] 内包含由 IRQCHIP_DECLARE() 声明的
* 中断控制器驱动列表(由 vmlinux.lds.h 中的 IRQCHIP_OF_MATCH_TABLE() 组织到一起),
* 而 DTS 声明中断控制器驱动实例。
* of_irq_init() 匹配中断控制器驱动到对应的DTS中的实例,并调用中断控制器
* 驱动的初始化接口,初始化中断控制器。
*/
of_irq_init(__irqchip_of_table);
...
}
/*
* 扫描 DT 匹配 @matches 的中断控制器,并初始化它们。
*
* DTS 声明的中断控制器:
* gic: interrupt-controller@01c81000 {
* compatible = "arm,gic-400";
* reg = <0x01c81000 0x1000>,
* <0x01c82000 0x2000>,
* <0x01c84000 0x2000>,
* <0x01c86000 0x2000>;
* interrupt-controller;
* #interrupt-cells = <3>;
* interrupts = <GIC_PPI 9 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_HIGH)>;
* };
*
* 匹配的中断控制器驱动声明:@drivers/irqchip/irq-gic.c
* IRQCHIP_DECLARE(gic_400, "arm,gic-400", gic_of_init); // AllWinner H3
* ==>
* static const struct of_device_id __of_table_gic_400
* __used __section(__irqchip_of_table)
* = { .compatible = "arm,gic-400",
* .data = (gic_of_init == (of_init_fn_2)NULL) ? gic_of_init : gic_of_init };
*/
void __init of_irq_init(const struct of_device_id *matches)
{
const struct of_device_id *match;
struct device_node *np, *parent = NULL;
struct of_intc_desc *desc, *temp_desc;
struct list_head intc_desc_list/*中断控制器列表*/, intc_parent_list;
INIT_LIST_HEAD(&intc_desc_list);
INIT_LIST_HEAD(&intc_parent_list);
/* 扫描 DTS 中匹配 @matches 的中断控制器,放入列表 @intc_desc_list */
for_each_matching_node_and_match(np, matches, &match) {
/* 属性 "interrupt-controller" 标记节点为中断控制器 */
if (!of_property_read_bool(np, "interrupt-controller") ||
!of_device_is_available(np))
continue;
...
/* 为中断控制器分配描述对象 */
desc = kzalloc(sizeof(*desc), GFP_KERNEL);
desc->irq_init_cb = match->data; /* gic_of_init(),... */
...
desc->interrupt_parent = of_irq_find_parent(np);
if (desc->interrupt_parent == np) /* 指定的父中断控制器和 @np 是相同节点,则置父中断控制器节点为 NULL */
desc->interrupt_parent = NULL;
list_add_tail(&desc->list, &intc_desc_list); /* 添加到中断控制器列表 */
}
/* 初始化中断控制器 */
while (!list_empty(&intc_desc_list)) {
/* 处理所有 interrupt_parent 为 @parent 的中断控制器 */
list_for_each_entry_safe(desc, temp_desc, &intc_desc_list, list) {
if (desc->interrupt_parent != parent) /* 当前只处理以 @parent 为父的中断控制器 */
continue;
list_del(&desc->list); /* 从中断控制器列表 @intc_desc_list 中移除 */
...
/* 初始化中断控制器 @desc */
ret = desc->irq_init_cb(desc->dev,
desc->interrupt_parent); /* gic_of_init(),... */
/*
* 当前已经初始化的中断控制器,可能作为其它中断控制器的父。
* 将它添加父中断控制器列表 @intc_parent_list ,后续在此循
* 环内以父 @parent 的形式出现,作为初始化的时的第2个参数
* 传递给它的子中断控制器。
*/
list_add_tail(&desc->list, &intc_parent_list);
}
desc = list_first_entry_or_null(&intc_parent_list,
typeof(*desc), list);
/* @desc 将作为新的、可能的父中断控制器,先将其从父中断控制器列表 @intc_parent_list 中移除 */
list_del(&desc->list);
parent = desc->dev; /* 已经成功初始化过的中断控制器 @desc ,以新父中断控制器的身份重新出场 */
kfree(desc);
}
/* 不是每个中断控制器都是别人的父 */
list_for_each_entry_safe(desc, temp_desc, &intc_parent_list, list) {
list_del(&desc->list);
kfree(desc);
}
err:
/* 出现错误,释放剩余没处理的节点 */
list_for_each_entry_safe(desc, temp_desc, &intc_desc_list, list) {
list_del(&desc->list);
of_node_put(desc->dev);
kfree(desc);
}
}
/*
* GIC 芯片初始化。
*
* GIC 的 DTS 配置如下
* gic: interrupt-controller@01c81000 {
* compatible = "arm,gic-400";
* reg = <0x01c81000 0x1000>,
* <0x01c82000 0x2000>,
* <0x01c84000 0x2000>,
* <0x01c86000 0x2000>;
* interrupt-controller;
* #interrupt-cells = <3>;
* interrupts = <GIC_PPI 9 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_HIGH)>;
* };
*/
int __init
gic_of_init(struct device_node *node, struct device_node *parent)
{
gic = &gic_data[gic_cnt];
ret = gic_of_setup(gic, node);
...
ret = __gic_init_bases(gic, -1, &node->fwnode);
...
if (parent) { /* 级联情形下的父中断控制器 */
irq = irq_of_parse_and_map(node, 0);
gic_cascade_irq(gic_cnt, irq);
}
gic_cnt++; /* 系统中注册的 ARM GIC 芯片 */
return 0;
}
/* 读取并映射 Distributor 和 CPU interface 寄存器基地址 */
static int gic_of_setup(struct gic_chip_data *gic, struct device_node *node)
{
/* 读取并映射 Distributor 寄存器基地址: reg[0] => <0x01c81000 0x1000> */
gic->raw_dist_base = of_iomap(node, 0);
/* 读取并映射 CPU interface 寄存器基地址: reg[1] => <0x01c82000 0x2000> */
gic->raw_cpu_base = of_iomap(node, 1);
/* 如果寄存器是各接口独立一份的,可以通过 "cpu-offset" 属性来指定个接口寄存器地址偏移 */
if (of_property_read_u32(node, "cpu-offset", &gic->percpu_offset))
gic->percpu_offset = 0;
return 0;
}
static int __init __gic_init_bases(struct gic_chip_data *gic,
int irq_start,
struct fwnode_handle *handle)
{
if (gic == &gic_data[0]) { /* 顶层 ROOT 中断控制器 */
for (i = 0; i < NR_GIC_CPU_IF; i++)
gic_cpu_map[i] = 0xff; /* 初始将中断派发给所有 CPU interface 上的 CPU */
#ifdef CONFIG_SMP
set_smp_cross_call(gic_raise_softirq); /* 用于处理器之间的通信:发送 SGI 中断到其它处理器 */
#endif
/*
* CPU 热插拔状态 CPUHP_AP_IRQ_GIC_STARTING 处理接口,在
* 多核处理系统下,非 BOOT CPU 热插拔启动上线最终达到
* CPUHP_AP_ONLINE 状态,在此之前会经过
* CPUHP_AP_IRQ_GIC_STARTING 态,在该状态下会调用这里注
* 册的回调 gic_starting_cpu() 来为对应 CPU 初始化中断。
*/
cpuhp_setup_state_nocalls(CPUHP_AP_IRQ_GIC_STARTING,
"irqchip/arm/gic:starting",
gic_starting_cpu, NULL);
/* 设置中断 GIC 中断处理入口,也即 ARM IRQ 中断处理入口 */
set_handle_irq(gic_handle_irq);
...
handle_arch_irq = handle_irq;
}
/* 这是中断芯片名,这个名字会输出到 /proc/interrupts */
name = kasprintf(GFP_KERNEL, "GICv2");
gic_init_chip(gic, NULL, name, true); /* 设定中断芯片接口 */
ret = gic_init_bases(gic, irq_start, handle);
...
return ret;
}
/* 初始化 GIC 中断芯片 irq_chip */
static void gic_init_chip(struct gic_chip_data *gic, struct device *dev,
const char *name, bool use_eoimode1)
{
/* Initialize irq_chip */
gic->chip = gic_chip;
gic->chip.name = name;
...
if (use_eoimode1) {
gic->chip.irq_mask = gic_eoimode1_mask_irq;
gic->chip.irq_eoi = gic_eoimode1_eoi_irq;
gic->chip.irq_set_vcpu_affinity = gic_irq_set_vcpu_affinity;
}
#ifdef CONFIG_SMP
if (gic == &gic_data[0])
gic->chip.irq_set_affinity = gic_set_affinity;
#endif
}
static int gic_init_bases(struct gic_chip_data *gic, int irq_start,
struct fwnode_handle *handle)
{
if (IS_ENABLED(CONFIG_GIC_NON_BANKED) && gic->percpu_offset) { /* 每 {Distributor, CPU interface} 的寄存器基址情形(非 Banked) */
...
} else { /* 非每 {Distributor, CPU interface} 的寄存器基址情形(Banked) */
/* gic_chip_data::percpu_offset 为 0 的情形,
* 应该开启 CONFIG_GIC_NON_BANKED 配置 */
gic->dist_base.common_base = gic->raw_dist_base;
gic->cpu_base.common_base = gic->raw_cpu_base;
gic_set_base_accessor(gic, gic_get_common_base);
}
/* 读取 GIC 实现支持的中断数目 */
gic_irqs = readl_relaxed(gic_data_dist_base(gic) + GIC_DIST_CTR) & 0x1f;
gic_irqs = (gic_irqs + 1) * 32; /* 计算 GIC 芯片支持的中断数目 */
if (gic_irqs > 1020) /* 1020~1023 被保留 */
gic_irqs = 1020;
gic->gic_irqs = gic_irqs; /* 记录 GIC 支持的中断数目 */
if (handle) { /* DT/ACPI */ /* 使用 DTS 的情形 */
/* 为中断芯片注册中断域,细节见 3.3 小节 */
gic->domain = irq_domain_create_linear(handle, gic_irqs,
&gic_irq_domain_hierarchy_ops,
gic);
} else { /* Legacy support */
...
}
/*
* 初始化 GIC Distributor:
* . 设置所有 SPI 中断转发到哪些 CPU 中断接口
* . 配置所有 SPI 中断的电平触发方式,且低电平触发
* . 配置所有 SPI 中断为相同的优先级
* . 禁用所有 SPI 中断
*/
gic_dist_init(gic);
ret = gic_cpu_init(gic);
...
return 0;
}
static void gic_dist_init(struct gic_chip_data *gic)
{
unsigned int i;
u32 cpumask;
unsigned int gic_irqs = gic->gic_irqs;
void __iomem *base = gic_data_dist_base(gic);
writel_relaxed(GICD_DISABLE, base + GIC_DIST_CTRL);
/*
* 读取【当前CPU】某个 {SGI, PPI} 的非0派发配置,然后立即返回该配置值。
* 这些派发配置只会将中断派发给某个CPU (这里是 CPU 0),因为我们通常读
* 到的是当前CPU的PPI派发配置:它们是只读的,只会派发给对应的当前CPU
* (也即 BOOT CPU ,只有 BOOT CPU 的中断初始化流程才走这里。BOOT CPU 通常是 CPU 0)。
*/
cpumask = gic_get_cpumask(gic);
/* 将 gic_get_cpumask() 读回的 8-bit 派发配置复制到 32-bit @cpumask 的所有 4个 8-bit 域 */
cpumask |= cpumask << 8;
cpumask |= cpumask << 16;
/*
* 寄存器 GIC_DIST_TARGET[0 ~ 7] 是 (SGI + PPI) 中断的固有只读配置,
* GIC_DIST_TARGET[8~...] 设置所有 SPI 中断转发到哪些 CPU 中断接口。
*
* @cpumask 通常是 PPI 的配置,所以这里的配置是将所有的 SPI 派发给
* 当前 CPU (也即 BOOT CPU ,只有 BOOT CPU 的中断初始化流程才走这
* 里。BOOT CPU 通常是 CPU 0)。这也就是说,默认情况下,所有的 SPI
* 中断都会派发给当前的 BOOT CPU (BOOT CPU 通常是 CPU 0),到这里还
* 是没问题的,毕竟其它CPU核还没有跑起来。
*/
for (i = 32; i < gic_irqs; i += 4)
writel_relaxed(cpumask, base + GIC_DIST_TARGET + i * 4 / 4);
/*
* 配置 GIC 芯片 distributor:
* . 配置所有 SPI 中断的电平触发方式,且低电平触发
* . 配置所有 SPI 中断为相同的优先级
* . 禁用所有 SPI 中断
*/
gic_dist_config(base, gic_irqs, NULL);
writel_relaxed(GICD_ENABLE, base + GIC_DIST_CTRL);
}
static int gic_cpu_init(struct gic_chip_data *gic)
{
void __iomem *dist_base = gic_data_dist_base(gic);
void __iomem *base = gic_data_cpu_base(gic);
unsigned int cpu_mask, cpu = smp_processor_id();
int i;
if (gic == &gic_data[0]) {
gic_check_cpu_features();
cpu_mask = gic_get_cpumask(gic);
gic_cpu_map[cpu] = cpu_mask; /* 当前 CPU @cpu 中断的默认转发配置:只转发给自身 */
/*
* Clear our mask from the other map entries in case they're
* still undefined.
*/
for (i = 0; i < NR_GIC_CPU_IF; i++)
if (i != cpu)
gic_cpu_map[i] &= ~cpu_mask;
}
gic_cpu_config(dist_base, NULL);
/*
* 只有优先级高于 GICC_INT_PRI_THRESHOLD 的中断才能传递给 GIC CPU 接口,
* 即 优先级值 < GICC_INT_PRI_THRESHOLD 的中断。
*/
writel_relaxed(GICC_INT_PRI_THRESHOLD, base + GIC_CPU_PRIMASK);
gic_cpu_if_up(gic); /* 启用转发到 GIC CPU 接口的中断 */
return 0;
}
void gic_cpu_config(void __iomem *base, void (*sync_access)(void))
{
int i;
/*
* Deal with the banked PPI and SGI interrupts - disable all
* PPI interrupts, ensure all SGI interrupts are enabled.
* Make sure everything is deactivated.
*/
writel_relaxed(GICD_INT_EN_CLR_X32, base + GIC_DIST_ACTIVE_CLEAR);
writel_relaxed(GICD_INT_EN_CLR_PPI, base + GIC_DIST_ENABLE_CLEAR);
writel_relaxed(GICD_INT_EN_SET_SGI, base + GIC_DIST_ENABLE_SET);
/*
* Set priority on PPI and SGI interrupts
*/
/* 设置 SGI, PPI 中断的优先级为缺省优先级 */
for (i = 0; i < 32; i += 4)
writel_relaxed(GICD_INT_DEF_PRI_X4,
base + GIC_DIST_PRI + i * 4 / 4);
if (sync_access)
sync_access();
}
static void gic_cpu_if_up(struct gic_chip_data *gic)
{
void __iomem *cpu_base = gic_data_cpu_base(gic);
u32 bypass = 0;
u32 mode = 0;
if (gic == &gic_data[0] && static_key_true(&supports_deactivate))
mode = GIC_CPU_CTRL_EOImodeNS;
/*
* Preserve bypass disable bits to be written back later
*/
bypass = readl(cpu_base + GIC_CPU_CTRL);
bypass &= GICC_DIS_BYPASS_MASK;
/* 启用转发到 GIC CPU 接口的中断 */
writel_relaxed(bypass | mode | GICC_ENABLE, cpu_base + GIC_CPU_CTRL);
}
3.2.2.2 非 BOOT CPU 对 GIC 的初始化
secondary_start_kernel()
notify_cpu_starting(cpu)
...
while (st->state < target) {
st->state++;
/* 当 st->state == CPUHP_AP_IRQ_GIC_STARTING,调用 gic_starting_cpu() */
ret = cpuhp_invoke_callback(cpu, st->state, true, NULL, NULL);
gic_starting_cpu()
}
static int gic_starting_cpu(unsigned int cpu)
{
gic_cpu_init(&gic_data[0]); /* 见章节 3.2.2.1 分析 */
return 0;
}
3.3 注册中断域
要为每个使用的中断芯片,注册一个中断域 irq_domain
,GIC
芯片也是一样。通过接口 irq_domain_create_linear()
注册中断域:
/*
* @fwnode : irq domain (中断控制器) 的 dts 配置节点指针
* @size : irq domain (中断控制器) 支持的中断数量
* @ops : irq domain (中断控制器) 接口
* @host_data: 特定 irq domain (中断控制器) 的数据???
*/
static inline struct irq_domain *irq_domain_create_linear(struct fwnode_handle *fwnode,
unsigned int size,
const struct irq_domain_ops *ops,
void *host_data)
{
return __irq_domain_add(fwnode, size, size, 0, ops, host_data);
}
struct irq_domain *__irq_domain_add(struct fwnode_handle *fwnode, int size,
irq_hw_number_t hwirq_max, int direct_max,
const struct irq_domain_ops *ops,
void *host_data)
{
struct device_node *of_node = to_of_node(fwnode);
struct irqchip_fwid *fwid;
struct irq_domain *domain;
/*
* 为 irq domain 以及其管理的 hwirq -> virq 的逆向映射表 分配空间。
* 其中 (sizeof(unsigned int) * size) 是逆向映射表的空间大小。
*/
domain = kzalloc_node(sizeof(*domain) + (sizeof(unsigned int) * size),
GFP_KERNEL, of_node_to_nid(of_node));
if (fwnode && is_fwnode_irqchip(fwnode)) {
...
} else if (of_node) { // Yes, we focus on this.
char *name;
name = kstrdup(of_node_full_name(of_node), GFP_KERNEL);
strreplace(name, '/', ':');
domain->name = name; /* 设定 irq domain 名称: 中断控制器 DTS 节点全路径 */
domain->fwnode = fwnode; /* 设定 irq domain 关联的 DTS 节点 */
domain->flags |= IRQ_DOMAIN_NAME_ALLOCATED;
}
of_node_get(of_node);
INIT_RADIX_TREE(&domain->revmap_tree, GFP_KERNEL);
/*
* 设定 irq domain 的操作接口:
* (如 gic_irq_domain_hierarchy_ops, sunxi_pinctrl_irq_domain_ops, ...)
*/
domain->ops = ops;
domain->host_data = host_data; /* 设定 irq domain 的私有数据 (&gic_data[0], ...) */
domain->hwirq_max = hwirq_max; /* 设定 irq domain 管理的中断目数 */
domain->revmap_size = size; /* 设定 irq domain 管理的映射条目数 */
domain->revmap_direct_max_irq = direct_max;
irq_domain_check_hierarchy(domain);
utex_lock(&irq_domain_mutex);
debugfs_add_domain_dir(domain); /* /sys/kernel/debug/domains/XXX */
list_add(&domain->link, &irq_domain_list); /* 添加到系统全局 irq domain 列表 */
mutex_unlock(&irq_domain_mutex);
return domain;
}
static void irq_domain_check_hierarchy(struct irq_domain *domain)
{
/* Hierarchy irq_domains must implement callback alloc() */
/*
* irq_domain_ops::alloc 接口由配置 CONFIG_IRQ_DOMAIN_HIERARCHY 使能;
* 同时,级联的中断域(中断控制器)必须实现 alloc 接口。
*/
if (domain->ops->alloc)
domain->flags |= IRQ_DOMAIN_FLAG_HIERARCHY;
}
3.4 中断配置解析和映射
会发送中断信号到 GIC
的硬件模块,其 DTS
节点都会配置 interrupts
属性,我们这里以一个 STMMicro
网卡芯片为例,来说明中断配置的解析和映射建立过程。本章节讨论的是中断信号发送到 ROOT 中断控制器
GIC
的情形,对于级联
的情形,将在后面的章节中讨论。
内核提供如下接口,来解析中断配置,以及建立 硬件中断号
到 Linux虚拟中断号
映射。
/* include/linux/of_irq.h */
extern int of_irq_get(struct device_node *dev, int index);
extern int of_irq_get_byname(struct device_node *dev, const char *name);
extern unsigned int irq_of_parse_and_map(struct device_node *node, int index);
/* include/linux/platform_device.h */
extern int platform_get_irq(struct platform_device *, unsigned int);
extern int platform_get_irq_byname(struct platform_device *, const char *);
先来看例子网卡的 DTS
配置:
emac: ethernet@1c30000 {
compatible = "allwinner,sun8i-h3-emac";
...
interrupts = <GIC_SPI 82 IRQ_TYPE_LEVEL_HIGH>;
interrupt-names = "macirq";
...
};
来看中断解析和映射建立的过程:
int irq = platform_get_irq_byname(pdev, "macirq");
int platform_get_irq_byname(struct platform_device *dev, const char *name)
{
struct resource *r;
if (IS_ENABLED(CONFIG_OF_IRQ) && dev->dev.of_node) {
int ret;
ret = of_irq_get_byname(dev->dev.of_node, name);
if (ret > 0 || ret == -EPROBE_DEFER)
return ret;
}
r = platform_get_resource_byname(dev, IORESOURCE_IRQ, name);
return r ? r->start : -ENXIO;
}
int of_irq_get_byname(struct device_node *dev, const char *name)
{
int index;
if (unlikely(!name))
return -EINVAL;
index = of_property_match_string(dev, "interrupt-names", name);
if (index < 0)
return index;
return of_irq_get(dev, index);
}
int of_irq_get(struct device_node *dev, int index)
{
int rc;
struct of_phandle_args oirq;
struct irq_domain *domain;
rc = of_irq_parse_one(dev, index, &oirq);
if (rc)
return rc;
/* 找到 @dev 挂接的中断控制器对应的中断域对象 */
domain = irq_find_host(oirq.np);
if (!domain)
return -EPROBE_DEFER;
return irq_create_of_mapping(&oirq);
}
int of_irq_parse_one(struct device_node *device, int index, struct of_phandle_args *out_irq)
{
...
/* Look for the interrupt parent. */
p = of_irq_find_parent(device); /* 找到设备的中断控制器父节点:中断信号发给谁 */
if (p == NULL)
return -EINVAL;
/* Get size of interrupt specifier */
if (of_property_read_u32(p, "#interrupt-cells", &intsize)) {
res = -EINVAL;
goto out;
}
/* 读取中断配置项 */
out_irq->np = p;
out_irq->args_count = intsize;
for (i = 0; i < intsize; i++) {
res = of_property_read_u32_index(device, "interrupts",
(index * intsize) + i,
out_irq->args + i);
if (res)
goto out;
}
...
out:
of_node_put(p);
return res;
}
unsigned int irq_create_of_mapping(struct of_phandle_args *irq_data)
{
struct irq_fwspec fwspec;
of_phandle_args_to_fwspec(irq_data, &fwspec);
return irq_create_fwspec_mapping(&fwspec);
}
unsigned int irq_create_fwspec_mapping(struct irq_fwspec *fwspec)
{
/*
* 例子中, @fwspec 的数据如下:
* fwspec->fwnode = &gic;
* fwspec->param_count = 3;
* fwspec->param[0..2] = {GIC_SPI, 82, IRQ_TYPE_LEVEL_HIGH};
*/
if (fwspec->fwnode) {
/* 找到中断挂接中断控制器的 irq_domain */
domain = irq_find_matching_fwspec(fwspec, DOMAIN_BUS_WIRED);
} else {
...
}
/*
* 调用 irq_domain (中断控制器)的中断配置项解析接口,
* 提取 【硬件中断号】 和 【中断触发类型】 信息:
* @fwspec ==> {@hwirq, @type}
*/
if (irq_domain_translate(domain, fwspec, &hwirq, &type))
return 0;
/*
* 分配一个空闲的 Linux 虚拟 IRQ 中断号,映射到 @hwirq ,
* 同时分配一个 IRQ 中断描述符 irq_desc 。
*/
if (irq_domain_is_hierarchy(domain)) {
virq = irq_domain_alloc_irqs(domain, 1, NUMA_NO_NODE, fwspec);
} else {
...
}
...
/* Store trigger type */
irqd_set_trigger_type(irq_data, type); /* 设置中断触发类型: RISING, FALLING, HIGH, LOW */
return virq; /* 返回分配的 Linux IRQ 虚拟中断号 */
}
static int irq_domain_translate(struct irq_domain *d,
struct irq_fwspec *fwspec,
irq_hw_number_t *hwirq, unsigned int *type)
{
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
/* 调用 irq_domain (中断控制器)的中断配置解析接口: @fwspec ==> {@hwirq, @type} */
if (d->ops->translate)
return d->ops->translate(d, fwspec, hwirq, type); /* gic_irq_domain_translate() */
#endif
}
/* GIC 中断配置项解析:@fwspec ==> {@hwirq, @type} */
static int gic_irq_domain_translate(struct irq_domain *d,
struct irq_fwspec *fwspec,
unsigned long *hwirq,
unsigned int *type)
{
if (is_of_node(fwspec->fwnode)) { /* 普通中断信号源设备 DTS 节点 */
*hwirq = fwspec->param[1] + 16; /* 跳过 SGIs 中断编号区间 */
if (!fwspec->param[0]) /* GIC_SPI */
*hwirq += 16;
*type = fwspec->param[2] & IRQ_TYPE_SENSE_MASK; /* 中断触发类型,如 IRQ_TYPE_LEVEL_HIGH */
return 0;
}
...
}
/*
* 1. 中断描述符分配;
* 2. 映射 硬件中断号 到 Linux虚拟中断号;
* 3. 中断处理接口设置。
*/
static inline int irq_domain_alloc_irqs(struct irq_domain *domain,
unsigned int nr_irqs, int node, void *arg/*struct irq_fwspec*/)
{
return __irq_domain_alloc_irqs(domain, -1, nr_irqs, node, arg, false,
NULL);
}
int __irq_domain_alloc_irqs(struct irq_domain *domain, int irq_base,
unsigned int nr_irqs, int node, void *arg,
bool realloc, const struct cpumask *affinity)
{
int i, ret, virq;
...
/* 分配中断描述符,以及 Linux 虚拟中断号 */
virq = irq_domain_alloc_descs(irq_base, nr_irqs, 0, node,
affinity);
/* 设置中断描述符的中断域(中断控制器) */
if (irq_domain_alloc_irq_data(domain, virq, nr_irqs)) {
...
ret = -ENOMEM;
goto out_free_desc;
}
...
/* 设置中断的 处理接口 和 flag 等 */
ret = irq_domain_alloc_irqs_hierarchy(domain, virq, nr_irqs, arg);
/* 建立中断域内 硬件中断号 到 Linux 虚拟中断号 的 映射关系 */
for (i = 0; i < nr_irqs; i++)
irq_domain_insert_irq(virq + i);
return virq; /* 返回分配的起始 Linux 虚拟中断号 */
}
int irq_domain_alloc_irqs_hierarchy(struct irq_domain *domain,
unsigned int irq_base,
unsigned int nr_irqs, void *arg)
{
return domain->ops->alloc(domain, irq_base, nr_irqs, arg); /* gic_irq_domain_alloc(), ... */
}
static int gic_irq_domain_alloc(struct irq_domain *domain, unsigned int virq,
unsigned int nr_irqs, void *arg)
{
int i, ret;
irq_hw_number_t hwirq;
unsigned int type = IRQ_TYPE_NONE;
struct irq_fwspec *fwspec = arg;
ret = gic_irq_domain_translate(domain, fwspec, &hwirq, &type);
for (i = 0; i < nr_irqs; i++) {
ret = gic_irq_domain_map(domain, virq + i, hwirq + i);
if (ret)
return ret;
}
return 0;
}
static int gic_irq_domain_map(struct irq_domain *d, unsigned int irq,
irq_hw_number_t hw)
{
struct gic_chip_data *gic = d->host_data;
if (hw < 32) { /* 每 CPU 的私有中断: SGI + PPI */
irq_set_percpu_devid(irq);
/* 设置 SGI, PPI 中断处理接口,由它再调用irq_desc::action 接口 */
irq_domain_set_info(d, irq, hw, &gic->chip, d->host_data,
handle_percpu_devid_irq, NULL, NULL);
irq_set_status_flags(irq, IRQ_NOAUTOEN);
} else { /* 全局于所有 CPU 的 SPI 中断 */
/* 设置 SPI 中断处理接口,由它再调用 irq_desc::action 接口 */
irq_domain_set_info(d, irq, hw, &gic->chip, d->host_data,
handle_fasteoi_irq, NULL, NULL);
irq_set_probe(irq);
irqd_set_single_target(irq_desc_get_irq_data(irq_to_desc(irq)));
}
return 0;
}
static void irq_domain_insert_irq(int virq)
{
struct irq_data *data;
for (data = irq_get_irq_data(virq); data; data = data->parent_data) {
struct irq_domain *domain = data->domain;
domain->mapcount++;
irq_domain_set_mapping(domain, data->hwirq, data);
...
}
irq_clear_status_flags(virq, IRQ_NOREQUEST);
}
static void irq_domain_set_mapping(struct irq_domain *domain,
irq_hw_number_t hwirq,
struct irq_data *irq_data)
{
if (hwirq < domain->revmap_size) {
domain->linear_revmap[hwirq] = irq_data->irq; /* 硬件中断号 到 Linux 虚拟中断号 的映射 */
} else { /* 超过 irq_domain 注册时设定的中断数目,用基树建立映射关系 */
mutex_lock(&revmap_trees_mutex);
radix_tree_insert(&domain->revmap_tree, hwirq, irq_data);
mutex_unlock(&revmap_trees_mutex);
}
}
到此,完成 硬件中断号
到 Linux虚拟中断号
映射的建立,同时分配了中段描述符 irq_desc
,函数的最终返回值为分配的 Linux虚拟中断号
。
另外,从代码分析,我们看到了 GIC 硬件中断号 DTS 配置值的一个规律:如果是 PPI 中断,DTS 配置值应该按实际硬件中断号减去 16;如果是 SPI 中断,DTS 配置值应该按实际硬件中断号减去 32
。
3.5 中断处理接口注册
在章节 3.4
中完成中断配置解析和映射后 ,剩下的就是注册中断处理接口。可以通过 request_irq()
或 request_threaded_irq()
来注册中断处理接口,这里只分析线程化方式的 request_threaded_irq()
,传递给 request_threaded_irq()
的 irq
参数,就是章节 3.4
申请的 Linux虚拟中断号
:
request_threaded_irq(irq, handler, thread_fn, irqflags, devname, dev_id)
desc = irq_to_desc(irq);
if (!handler) { /* 线程化中断的 @handler 通常设为 NULL */
if (!thread_fn)
return -EINVAL;
handler = irq_default_primary_handler;
}
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
action->handler = handler; /* 设置中断处理接口 */
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;
retval = __setup_irq(irq, desc, action);
new->irq = irq; /* new = action */
if (new->thread_fn && !nested) { /* 中断线程化 */
ret = setup_irq_thread(new, irq, false);
struct task_struct *t;
struct sched_param param = {
.sched_priority = MAX_USER_RT_PRIO/2,
};
t = kthread_create(irq_thread, new, "irq/%d-%s", irq, new->name);
sched_setscheduler_nocheck(t, SCHED_FIFO, ¶m);
get_task_struct(t);
new->thread = t;
set_bit(IRQTF_AFFINITY, &new->thread_flags);
return 0;
...
}
old_ptr = &desc->action;
old = *old_ptr;
...
irqd_get_trigger_type(&desc->irq_data);
...
*old_ptr = new;
irq_pm_install_action(desc, new);
desc->irq_count = 0;
desc->irqs_unhandled = 0;
...
irq_setup_timings(desc, new);
if (new->thread)
wake_up_process(new->thread); /* 唤醒中断处理线程 */
register_irq_proc(irq, desc); /* /proc/irq/N/* */
irq_add_debugfs_entry(irq, desc);
new->dir = NULL;
register_handler_proc(irq, new);
return 0;
...
return retval;
3.6 中断处理流程
先来看看中断发生时,ARM32
硬件架构会做哪些动作:
其中
<exception_mode>
是指 CPU 的异常模式,如 IRQ 。简单总结一下,进入中断异常模式时,在 ARM32
架构做了如下动作:
. R14_<exception_mode> = 被中断的指令的地址(即处理中断后应返回的地址)
. SPSR_<exception_mode> = CPSR (被中断 CPU 模式的 CPSR,如 CPSR_svc, CPSR_usr)
. CPSR[4:0] = exception mode nuber(切换到异常模式)
. 如果时进入到 Reset 或 FIQ 异常模式,则禁用 FIQ (CPSR[6] = 1)
. 禁用 IRQ 中断 (CPSR[7] = 1)
...
. PC = 异常向量地址(即跳转到异常向量地址处执行)
这些动作都是硬件自动完成的,不需要软件的干预。另外,需要知道的是,在 CPU 各异常模式下,部分寄存器有独立的副本,详情如下:
可以看到,寄存器 r0 ~ r7
在所有 CPU 模式共用同一副本;r8 ~ r12
除了 FIQ 模式有自己独立副本外,其它模式下共用一个副本;r13 ~ r14
除 User/System
模式共用一个副本外,其它模式下都有自己独立的副本;PC
各模式共用。
了解上述内容,对于理解中断的处理过程是必不可少的。接下来,看 ARM32
架构下中断处理的具体流程。本文只讨论 IRQ
中断的处理流程,对其它中断异常类型处理过程感兴趣的读者,可自行阅读相关源码。
3.6.1 ARM32 中断向量表
/* arch/arm/kernel/entry-armv.S */
.section .vectors, "ax", %progbits
.L__vectors_start:
W(b) vector_rst /* 复位 */
W(b) vector_und /* 未定义指令异常向量表指针 */
/*
* @ arch/arm/kernel/vmlinux.lds.S
*
* __stubs_start = .;
* .stubs ADDR(.vectors) + 0x1000 : AT(__stubs_start) {
* *(.stubs)
* }
* . = __stubs_start + SIZEOF(.stubs);
* __stubs_end = .;
*/
W(ldr) pc, .L__vectors_start + 0x1000 /* 软中断向量(swi) */
W(b) vector_pabt
W(b) vector_dabt
W(b) vector_addrexcptn
W(b) vector_irq /* IRQ */
W(b) vector_fiq /* FIQ */
.data
.align 2
发生中断后,先切换到 SVC 模式,然后保存被中断的上下文到 SVC 模式栈(即进程内核栈)
,然后进一步跳转到 IRQ
中断处理入口 handle_arch_irq
。在进一步讨论之前,先解释下为什么要切换 SVC 模式,这主要是为了借用 SVC 模式栈(进程内核栈)进行中断处理
,因为各异常模式配置的栈空间只有 3 个 u32 大小。更多关于中断异常模式栈空间
的细节,可参考博文 Linux: ARM32 各 CPU 模式下栈配置 的章节 4.2.2 中断异常发生时各异常模式 CPU 栈配置
。
/* arch/arm/kernel/entry-armv.S */
.macro irq_handler
#ifdef CONFIG_MULTI_IRQ_HANDLER
ldr r1, =handle_arch_irq /* r1 = gic_handle_irq() */
mov r0, sp
badr lr, 9997f
ldr pc, [r1]
#else
arch_irq_handler_default
#endif
9997:
.endm
...
.data
.align 2
vector_stub irq, IRQ_MODE, 4
.long __irq_usr @ 0 (USR_26 / USR_32)
.long __irq_invalid @ 1 (FIQ_26 / FIQ_32)
.long __irq_invalid @ 2 (IRQ_26 / IRQ_32)
.long __irq_svc @ 3 (SVC_26 / SVC_32)
.long __irq_invalid @ 4
.long __irq_invalid @ 5
.long __irq_invalid @ 6
.long __irq_invalid @ 7
.long __irq_invalid @ 8
.long __irq_invalid @ 9
.long __irq_invalid @ a
.long __irq_invalid @ b
.long __irq_invalid @ c
.long __irq_invalid @ d
.long __irq_invalid @ e
.long __irq_invalid @ f
.align 5
__irq_svc: /* 内核态(ARM CPU SVC模式)中断入口 */
svc_entry /* 保存被中断模式上下文到 SVC 模式栈(即进程内核栈)上 */
irq_handler /* 中断处理入口 */
#ifdef CONFIG_PREEMPT
ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
ldr r0, [tsk, #TI_FLAGS] @ get flags
teq r8, #0 @ if preempt count != 0
movne r0, #0 @ force flags to 0
tst r0, #_TIF_NEED_RESCHED
blne svc_preempt
#endif
/* 恢复被中断模式上下文,退出中断 */
svc_exit r5, irq = 1 @ return from exception
UNWIND(.fnend )
ENDPROC(__irq_svc)
...
.align 5
__irq_usr: /* 用户态(ARM CPU User模式)中断入口 */
usr_entry
kuser_cmpxchg_check
irq_handler /* 中断处理入口 */
get_thread_info tsk
mov why, #0
b ret_to_user_from_irq
UNWIND(.fnend )
ENDPROC(__irq_usr)
3.6.2 进入中断处理主入口
现在 handle_arch_irq
指针的值,正是在 GIC
初始化阶段设置的 gic_handle_irq()
:
static void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
u32 irqstat, irqnr;
struct gic_chip_data *gic = &gic_data[0];
void __iomem *cpu_base = gic_data_cpu_base(gic);
do {
/* 读取硬件中断号 */
irqstat = readl_relaxed(cpu_base + GIC_CPU_INTACK);
irqnr = irqstat & GICC_IAR_INT_ID_MASK;
if (likely(irqnr > 15 && irqnr < 1020)) { /* 处理 PPI, SPI */
...
handle_domain_irq(gic->domain, irqnr, regs);
continue;
}
if (irqnr < 16) { /* 处理 SGI */
writel_relaxed(irqstat, cpu_base + GIC_CPU_EOI);
...
#ifdef CONFIG_SMP
handle_IPI(irqnr, regs);
#endif
continue;
}
} while (1);
}
static inline int handle_domain_irq(struct irq_domain *domain,
unsigned int hwirq, struct pt_regs *regs)
{
return __handle_domain_irq(domain, hwirq, true, regs);
}
int __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq,
bool lookup, struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);
unsigned int irq = hwirq;
int ret = 0;
irq_enter();
#ifdef CONFIG_IRQ_DOMAIN
if (lookup)
irq = irq_find_mapping(domain, hwirq); /* 查找 硬件中断号 @hwirq 对应的 Linux 虚拟中断号 */
#endif
...
generic_handle_irq(irq); /* 处理中断 */
...
irq_exit(); /* 软中断, RCU 等等处理 */
set_irq_regs(old_regs);
return ret;
}
int generic_handle_irq(unsigned int irq)
{
struct irq_desc *desc = irq_to_desc(irq);
if (!desc)
return -EINVAL;
generic_handle_irq_desc(desc);
return 0;
}
static inline void generic_handle_irq_desc(struct irq_desc *desc)
{
/*
* GIC v1 芯片:
* . PPI: handle_percpu_devid_irq()
* . SPI: handle_fasteoi_irq()
*
* 级联 GPIO 中断控制器:
* handle_fasteoi_irq(): 处理 LEVEL 触发中断(高低电平)
* handle_edge_irq(): 处理 EDGE 触发中断(上升、下降沿)
*/
desc->handle_irq(desc);
}
/* 处理 PPI 中断 */
void handle_percpu_devid_irq(struct irq_desc *desc)
{
struct irq_chip *chip = irq_desc_get_chip(desc);
struct irqaction *action = desc->action;
unsigned int irq = irq_desc_get_irq(desc);
irqreturn_t res;
__kstat_incr_irqs_this_cpu(desc); /* 更新中断 @irq 次数统计 */
if (chip->irq_ack) /* 中断芯片 ACK 处理 */
chip->irq_ack(&desc->irq_data);
/* 调用 request_irq(), request_thread_irq() 注册的接口 */
res = action->handler(irq, raw_cpu_ptr(action->percpu_dev_id));
if (chip->irq_eoi) /* 中断芯片 EOI 处理 */
chip->irq_eoi(&desc->irq_data);
}
/* 处理 SPI 中断 */
void handle_fasteoi_irq(struct irq_desc *desc)
{
struct irq_chip *chip = desc->irq_data.chip;
...
kstat_incr_irqs_this_cpu(desc); /* 更新中断当前 CPU 的统计数据:SPI 中断可能派发到任一 CPU 核上 */
if (desc->istate & IRQS_ONESHOT) /* 中断标记为只触发一次,触发一次中断后,屏蔽它 */
mask_irq(desc);
preflow_handler(desc);
handle_irq_event(desc);
...
}
irqreturn_t handle_irq_event(struct irq_desc *desc)
{
rqreturn_t ret;
desc->istate &= ~IRQS_PENDING;
irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS); /* 标记一个中断正在处理中 */
raw_spin_unlock(&desc->lock);
ret = handle_irq_event_percpu(desc);
raw_spin_lock(&desc->lock);
irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS); /* 中断处理完成,清除 "中断正在处理中" 标记 */
return ret;
}
irqreturn_t handle_irq_event_percpu(struct irq_desc *desc)
{
irqreturn_t retval;
unsigned int flags = 0;
retval = __handle_irq_event_percpu(desc, &flags);
add_interrupt_randomness(desc->irq_data.irq, flags); /* 中断处理为 CRND 随机数做贡献 */
...
return retval;
}
irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
{
record_irq_time(desc);
/* 调用注册到中端 irq 的所有 action handler */
for_each_action_of_desc(desc, action) {
irqreturn_t res;
res = action->handler(irq, action->dev_id); /* 调用 (SGI) 中断处理接口 */
switch (res) {
/* 线程化中断,主接口应该返回 IRQ_WAKE_THREAD ,
* 譬如线程化中断的默认主接口 irq_default_primary_handler()
*/
case IRQ_WAKE_THREAD:
__irq_wake_thread(desc, action); /* 唤醒中断线程处理中断 */
/* Fall through to add to randomness */
case IRQ_HANDLED: /* 中断处理完毕 */
*flags |= action->flags;
break;
default:
break;
}
retval |= res;
}
return retval;
}
4. 中断控制器级联
中断控制器级联
最常见的情形,是将 GPIO 中断控制器
,级联到 GIC ,拓扑结构如下:
GPIO 中断信号 -> GPIO中断控制器 -> GIC -> CPU
我们还是以一个 GPIO pinctrl 驱动为例,来分析级联中断的初始化、中断接口注册、中断处理
的3个过程。
4.1 级联中断控制器的初始化
先看级联 GPIO 中断控制器的 DTS 配置:
pio: pinctrl@01c20800 {
...
interrupts = <GIC_SPI 11 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 17 IRQ_TYPE_LEVEL_HIGH>;
...
gpio-controller;
#gpio-cells = <3>;
interrupt-controller;
#interrupt-cells = <3>;
...
};
从 DTS 看到,这是一个 GPIO 中断控制器
,它输出2个
中断信号11,17
给 GIC
。看一下 级联的GPIO中断控制器
初始化细节:
/*
* 节选自全志 H3 的 gpiochip/pinctrl 复合驱动,代码均为开源代码。
* 为方便理解,对逻辑上进行些改动,以突出中断相关部分。
*/
struct sunxi_pinctrl {
...
struct gpio_chip *chip;
...
struct irq_domain *domain;
int virq[2]; /* 记录 DTS 的配置的中断 11,17 的 Linux 虚拟中断号 */
...
};
/* GPIO 中断控制器 中断域接口 */
static const struct irq_domain_ops sunxi_pinctrl_irq_domain_ops = {
.xlate = sunxi_pinctrl_irq_of_xlate,
};
int sunxi_pinctrl_init_with_variant(struct platform_device *pdev,
const struct sunxi_pinctrl_desc *desc,
unsigned long variant)
{
struct sunxi_pinctrl *pctl;
int i;
pctl = devm_kzalloc(&pdev->dev, sizeof(*pctl), GFP_KERNEL);
...
/* 设置将 GPIO PIN ID 映射为其对应 Linux 虚拟中断号 的接口 */
pctl->chip->to_irq = sunxi_pinctrl_gpio_to_irq,
...
/*
* DTS 配置的 GPIO 中断控制器输出到 GIC 的2个中断信号的解析
* 和映射:
* interrupts = <GIC_SPI 11 IRQ_TYPE_LEVEL_HIGH>,
* <GIC_SPI 17 IRQ_TYPE_LEVEL_HIGH>;
*/
pctl->virq[0] = platform_get_irq(pdev, 0);
pctl->virq[1] = platform_get_irq(pdev, 1);
/* 创建 GPIO 中断控制器 的中断域 irq_domain */
pctl->domain = irq_domain_add_linear(node, 64/*64个GPIO,64个中断*/,
&sunxi_pinctrl_irq_domain_ops, pctl);
/* 为 GPIO 中断控制器的所有64个GPIO硬件中断,映射分配 Linux虚拟中断号 */
for (i = 0; i < 64; i++) {
int virq = irq_create_mapping(pctl->domain, i);
/* 设置 GPIO 中断 的 【中断芯片底层操作接口】 和 【沿触发模式 中断处理接口】 */
irq_set_chip_and_handler(virq, &sunxi_pinctrl_edge_irq_chip,
handle_edge_irq);
/* 绑定 GPIO 中断 的 中断芯片底层数据 @pctl */
irq_set_chip_data(virq, pctl);
}
/* 我们还要设置 GPIO 中断控制器输出到 GIC 信号的中断处理接口,默认的接口不是我们想要的。 */
irq_set_chained_handler_and_data(pctl->virq[0],
sunxi_pinctrl_irq_handler, pctl);
irq_set_chained_handler_and_data(pctl->virq[1],
sunxi_pinctrl_irq_handler, pctl);
}
/* 硬件中断号 到 Linux虚拟中断号的 映射过程 */
unsigned int irq_create_mapping(struct irq_domain *domain,
irq_hw_number_t hwirq)
{
struct device_node *of_node;
int virq;
...
of_node = irq_domain_get_of_node(domain); /* 获取中断域的 DTS 节点 */
virq = irq_find_mapping(domain, hwirq); /* 查找硬件中断号 @hwirq 在中断域 @domain 中的映射 */
if (virq) { /* 硬件中断号 @hwirq 在中断域 @domain 中已经映射过了 */
return virq; /* 已经映射过了,返回映射的 Linux 虚拟中断号 */
}
/*
* 为硬件中断号 @hwirq 映射一个虚拟中断号,
* 并分配并初始化一个中断描述符,插入全局中断描述符基树。
*/
virq = irq_domain_alloc_descs(-1, 1, hwirq, of_node_to_nid(of_node), NULL);
/*
* . 建立中断域 @domain 内 【硬件中断号 @hwirq】 到 【Linux
* 虚拟中断号 @virq】 的映射 ;
* . 设定中断 @virq 的中断域为 @domain 。
*/
if (irq_domain_associate(domain, virq, hwirq)) {
irq_free_desc(virq);
return 0;
}
return virq; /* 返回 @hwirq 映射的 Linux虚拟中断号 @virq */
}
到此,我们已经初始化好了级联的 GPIO 中断控制器,接下来看 GPIO 中断的使用过程。
4.2 级联中断使用范例
4.2.1 例1:USB PHY 芯片 GPIO 中断使用
先看 DTS 配置:
usbphy: phy@01c19400 {
compatible = "allwinner,sun8i-h3-usb-phy";
...
usb0_id_det-gpios = <&pio 6 12 GPIO_ACTIVE_HIGH>; /* PG12 */
...
};
PG12
中断信号处理接口注册流程:
data->id_det_gpio = devm_gpiod_get_optional(dev, "usb0_id_det",
GPIOD_IN);
...
data->id_det_irq = gpiod_to_irq(data->id_det_gpio); /* 将 GPIO PIN 映射到 Linux 虚拟中断号 */
/* 注册 PG12 GPIO 中断处理接口 */
ret = devm_request_irq(dev, data->id_det_irq,
sun4i_usb_phy0_id_vbus_det_irq,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"usb0-id-det", data);
...
gpiod_to_irq()
有点新鲜,但很常见,看一下它的具体细节:
int gpiod_to_irq(const struct gpio_desc *desc)
{
struct gpio_chip *chip;
int offset;
chip = desc->gdev->chip;
offset = gpio_chip_hwgpio(desc);
if (chip->to_irq) {
int retirq = chip->to_irq(chip, offset); /* sunxi_pinctrl_gpio_to_irq() */
...
return retirq;
}
}
static int sunxi_pinctrl_gpio_to_irq(struct gpio_chip *chip, unsigned offset)
{
struct sunxi_pinctrl *pctl = gpiochip_get_data(chip);
struct sunxi_desc_function *desc;
unsigned pinnum = pctl->desc->pin_base + offset;
unsigned irqnum;
...
desc = sunxi_pinctrl_desc_find_function_by_pin(pctl, pinnum, "irq");
if (!desc)
return -EINVAL;
irqnum = desc->irqbank * IRQ_PER_BANK + desc->irqnum;
return irq_find_mapping(pctl->domain, irqnum); /* 返回 硬件中断号 @irqnum 映射的 Linux 软件中断号 */
}
4.2.2 例2:无线网卡唤醒 GPIO 中断使用
先看 DTS 配置:
brcmf: bcrmf@1 {
reg = <1>;
compatible = "brcm,bcm4329-fmac";
interrupt-parent = <&pio>;
interrupts = <6 10 IRQ_TYPE_LEVEL_LOW>; /* PG10 / EINT10 */
interrupt-names = "host-wake";
};
注意,interrupt-parent = <&pio>;
表明该设备的中断信号是传递给 GPIO 中断控制器 pio
的。看看中断配置解析映射的过程:
if (!of_find_property(np, "interrupts", NULL))
return;
irq = irq_of_parse_and_map(np, 0); /* hwirq ==> irq */
irqf = irqd_get_trigger_type(irq_get_irq_data(irq));
sdio->oob_irq_supported = true;
sdio->oob_irq_nr = irq;
sdio->oob_irq_flags = irqf;
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
{
struct of_phandle_args oirq;
/*
* oirq.np = &pio;
* oirq.args_count = 3;
* oirq.args[0..2] = {6, 10, IRQ_TYPE_LEVEL_LOW};
*/
if (of_irq_parse_one(dev, index, &oirq))
return 0;
return irq_create_of_mapping(&oirq);
}
unsigned int irq_create_of_mapping(struct of_phandle_args *irq_data)
{
struct irq_fwspec fwspec;
of_phandle_args_to_fwspec(irq_data, &fwspec);
return irq_create_fwspec_mapping(&fwspec);
}
unsigned int irq_create_fwspec_mapping(struct irq_fwspec *fwspec)
{
struct irq_domain *domain;
if (fwspec->fwnode) {
/* 找到中断挂接中断控制器的 irq_domain */
domain = irq_find_matching_fwspec(fwspec, DOMAIN_BUS_WIRED);
} else {
...
}
/*
* 调用 irq_domain (中断控制器)的中断配置项解析接口,
* 提取 硬件中断号 和 中断触发类型 信息:
* @fwspec ==> {@hwirq, @type}
*/
if (irq_domain_translate(domain, fwspec, &hwirq, &type))
return 0;
if (irq_domain_is_hierarchy(domain)) {
...
} else {
/* Create mapping */
/* 建立 硬件中断号 到 Linux虚拟中断号 的映射 */
virq = irq_create_mapping(domain, hwirq);
if (!virq)
return virq;
}
irqd_set_trigger_type(irq_data, type); /* 设置中断触发类型: RISING, FALLING, HIGH, LOW */
return virq; /* 返回分配的 Linux IRQ 虚拟中断号 */
}
static int irq_domain_translate(struct irq_domain *d,
struct irq_fwspec *fwspec,
irq_hw_number_t *hwirq, unsigned int *type)
{
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
...
#endif
if (d->ops->xlate) /* 级联的 GPIO 中断控制器中断域的接口 sunxi_pinctrl_irq_of_xlate() */
return d->ops->xlate(d, to_of_node(fwspec->fwnode),
fwspec->param, fwspec->param_count,
hwirq, type);
...
}
static int sunxi_pinctrl_irq_of_xlate(struct irq_domain *d,
struct device_node *node,
const u32 *intspec,
unsigned int intsize,
unsigned long *out_hwirq,
unsigned int *out_type)
{
struct sunxi_pinctrl *pctl = d->host_data;
struct sunxi_desc_function *desc;
int pin, base;
...
base = PINS_PER_BANK * intspec[0]; /* intspec[0]: GPIO bank */
pin = pctl->desc->pin_base + base + intspec[1]; /* intspec[1]: GPIO pin */
/* 查询 GPIO PIN 的中断属性信息 */
desc = sunxi_pinctrl_desc_find_function_by_pin(pctl, pin, "irq");
if (!desc)
return -EINVAL;
*out_hwirq = desc->irqbank * PINS_PER_BANK + desc->irqnum; /* 返回硬件中断号 */
*out_type = intspec[2]; /* 解析中断触发类型 */
return 0;
}
中断信号处理接口注册:
ret = request_irq(pdata->oob_irq_nr, brcmf_sdiod_oob_irqhandler,
pdata->oob_irq_flags, "brcmf_oob_intr",
&sdiodev->func1->dev);
4.3 级联中断的处理过程
前一部分和章节 3.6
的流程一样:
gic_handle_irq()
handle_domain_irq(gic->domain, irqnr, regs)
__handle_domain_irq(domain, hwirq, true, regs)
struct pt_regs *old_regs = set_irq_regs(regs);
unsigned int irq = hwirq;
int ret = 0;
irq_enter();
irq = irq_find_mapping(domain, hwirq); /* 查找 硬件中断号 @hwirq 对应的 Linux 虚拟中断号 */
generic_handle_irq(irq);
struct irq_desc *desc = irq_to_desc(irq);
generic_handle_irq_desc(desc);
desc->handle_irq(desc) = sunxi_pinctrl_irq_handler()
irq_exit(); /* 软中断, RCU 等等处理 */
set_irq_regs(old_regs);
return ret;
从 sunxi_pinctrl_irq_handler()
处开始不同,继续看 sunxi_pinctrl_irq_handler()
细节:
static void sunxi_pinctrl_irq_handler(struct irq_desc *desc)
{
unsigned int irq = irq_desc_get_irq(desc);
...
val = readl(pctl->membase + reg); /* 读取触发的 GPIO 中断 */
if (val) {
int irqoffset;
chained_irq_enter(chip, desc);
for_each_set_bit(irqoffset, &val, IRQ_PER_BANK) { /* 处理所有触发的 GPIO 中断 */
int pin_irq = irq_find_mapping(pctl->domain,
bank * IRQ_PER_BANK + irqoffset); /* 找到硬件中断的 Linux 虚拟中断号 */
/* okay, 这里没有无穷递归形成。 */
generic_handle_irq(pin_irq); /* 处理一个 GPIO 中断 */
}
chained_irq_exit(chip, desc);
}
}
int generic_handle_irq(unsigned int irq)
{
struct irq_desc *desc = irq_to_desc(irq);
if (!desc)
return -EINVAL;
generic_handle_irq_desc(desc);
return 0;
}
static inline void generic_handle_irq_desc(struct irq_desc *desc)
{
/*
* GIC v1 芯片:
* . PPI: handle_percpu_devid_irq()
* . SPI: handle_fasteoi_irq()
* . 级联中断信号: sunxi_pinctrl_irq_handler()
*
* 级联 GPIO 中断控制器:
* handle_fasteoi_irq(): 处理 LEVEL 触发中断(高低电平)
* handle_edge_irq(): 处理 EDGE 触发中断(上升、下降沿)
*/
desc->handle_irq(desc);
}
/* 处理 GPIO 沿触发方式中断 */
void handle_edge_irq(struct irq_desc *desc)
{
...
kstat_incr_irqs_this_cpu(desc); /* 中断数据统计 */
...
desc->irq_data.chip->irq_ack(&desc->irq_data); /* sunxi_pinctrl_irq_ack() */
do {
...
handle_irq_event(desc);
} while ((desc->istate & IRQS_PENDING) &&
!irqd_irq_disabled(&desc->irq_data));
...
}
irqreturn_t handle_irq_event(struct irq_desc *desc)
{
irqreturn_t ret;
desc->istate &= ~IRQS_PENDING;
irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS); /* 标记一个中断正在处理中 */
raw_spin_unlock(&desc->lock);
ret = handle_irq_event_percpu(desc);
raw_spin_lock(&desc->lock);
irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS); /* 中断处理完成,清除 "中断正在处理中" 标记 */
return ret;
}
irqreturn_t handle_irq_event_percpu(struct irq_desc *desc)
{
irqreturn_t retval;
unsigned int flags = 0;
retval = __handle_irq_event_percpu(desc, &flags);
add_interrupt_randomness(desc->irq_data.irq, flags); /* 中断处理为 CRND 随机数做贡献 */
...
return retval;
}
irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
{
/* 调用注册到中端 irq 的所有 action handler */
for_each_action_of_desc(desc, action) {
irqreturn_t res;
/* sun4i_usb_phy0_id_vbus_det_irq() / brcmf_sdiod_oob_irqhandler() */
res = action->handler(irq, action->dev_id);
switch (res) {
/* 线程化中断,主接口应该返回 IRQ_WAKE_THREAD ,
* 譬如线程化中断的默认主接口 irq_default_primary_handler()
*/
case IRQ_WAKE_THREAD:
__irq_wake_thread(desc, action); /* 唤醒中断线程处理中断 */
/* Fall through to add to randomness */
case IRQ_HANDLED: /* 中断处理完毕 */
*flags |= action->flags;
break;
default:
break;
}
retval |= res;
}
return retval;
}
4.3.1 GPIO 中断电平触发模式切换
前面看到的GPIO中断控制器处理流程
,是关于沿触发方式
的中断处理,那 GPIO的 电平触发方式
的处理是怎样的?首先,要将 GPIO 的中断触发方式进行切换,用户空间通过向 /sys/class/gpio/gpioN/edge
写入 rising
或 falling
,我们例子中的最终会调用接口 sunxi_pinctrl_irq_set_type()
:
static int sunxi_pinctrl_irq_set_type(struct irq_data *d, unsigned int type)
{
struct sunxi_pinctrl *pctl = irq_data_get_irq_chip_data(d);
u32 reg = sunxi_irq_cfg_reg(d->hwirq, pctl->desc->irq_bank_base);
u8 index = sunxi_irq_cfg_offset(d->hwirq);
unsigned long flags;
u32 regval;
u8 mode;
switch (type) {
case IRQ_TYPE_EDGE_RISING:
mode = IRQ_EDGE_RISING;
break;
case IRQ_TYPE_EDGE_FALLING:
mode = IRQ_EDGE_FALLING;
break;
case IRQ_TYPE_EDGE_BOTH:
mode = IRQ_EDGE_BOTH;
break;
case IRQ_TYPE_LEVEL_HIGH:
mode = IRQ_LEVEL_HIGH;
break;
case IRQ_TYPE_LEVEL_LOW:
mode = IRQ_LEVEL_LOW;
break;
default:
return -EINVAL;
}
raw_spin_lock_irqsave(&pctl->lock, flags);
if (type & IRQ_TYPE_LEVEL_MASK)
irq_set_chip_handler_name_locked(d, &sunxi_pinctrl_level_irq_chip,
handle_fasteoi_irq, NULL);
else
irq_set_chip_handler_name_locked(d, &sunxi_pinctrl_edge_irq_chip,
handle_edge_irq, NULL);
...
raw_spin_unlock_irqrestore(&pctl->lock, flags);
return 0;
}
从上面的逻辑可以看出,处理电平模式 IRQ_TYPE_LEVEL_HIGH
或 IRQ_TYPE_LEVEL_LOW
,最终会调用 handle_fasteoi_irq()
进行中断处理。
从前面的分析,我们了解到一个事实:
Linux 虚拟中断号 是全局唯一的,它不特定于某个中断域(中断芯片);
而 硬件中断号 是特定于某个中断域(中断芯片)的,中断域之间的 硬件中断号 可以是重复的。
5. SGI 中断处理
SGI 中断
用于处理器核之前的通信,前面我们没有分析 SGI 中断
的处理,现在来看一下:
static void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
u32 irqstat, irqnr;
struct gic_chip_data *gic = &gic_data[0];
void __iomem *cpu_base = gic_data_cpu_base(gic);
do {
/* 读取硬件中断号 */
irqstat = readl_relaxed(cpu_base + GIC_CPU_INTACK);
irqnr = irqstat & GICC_IAR_INT_ID_MASK;
if (likely(irqnr > 15 && irqnr < 1020)) { /* 处理 PPI, SPI */
...
}
if (irqnr < 16) { /* 处理 SGI */
writel_relaxed(irqstat, cpu_base + GIC_CPU_EOI);
...
#ifdef CONFIG_SMP
/* SGI 中断用于处理器间的通信,所以只有多核处理器架构下才有 */
smp_rmb();
handle_IPI(irqnr, regs);
#endif
continue;
}
break;
} while (1);
}
void handle_IPI(int ipinr, struct pt_regs *regs)
{
unsigned int cpu = smp_processor_id();
struct pt_regs *old_regs = set_irq_regs(regs);
if ((unsigned)ipinr < NR_IPI) {
trace_ipi_entry_rcuidle(ipi_types[ipinr]);
__inc_irq_stat(cpu, ipi_irqs[ipinr]);
}
switch (ipinr) {
case IPI_WAKEUP:
break;
#ifdef CONFIG_GENERIC_CLOCKEVENTS_BROADCAST
case IPI_TIMER:
irq_enter();
tick_receive_broadcast();
irq_exit();
break;
#endif
case IPI_RESCHEDULE:
scheduler_ipi();
break;
case IPI_CALL_FUNC:
irq_enter();
generic_smp_call_function_interrupt();
irq_exit();
break;
case IPI_CPU_STOP:
irq_enter();
ipi_cpu_stop(cpu);
irq_exit();
break;
#ifdef CONFIG_IRQ_WORK
case IPI_IRQ_WORK:
irq_enter();
irq_work_run();
irq_exit();
break;
#endif
case IPI_COMPLETION:
irq_enter();
ipi_complete(cpu);
irq_exit();
break;
case IPI_CPU_BACKTRACE:
printk_nmi_enter();
irq_enter();
nmi_cpu_backtrace(regs);
irq_exit();
printk_nmi_exit();
break;
default:
pr_crit("CPU%u: Unknown IPI message 0x%x\n",
cpu, ipinr);
break;
}
if ((unsigned)ipinr < NR_IPI)
trace_ipi_exit_rcuidle(ipi_types[ipinr]);
set_irq_regs(old_regs);
}
6. 中断共享
在调用 request_irq()
传递 IRQF_SHARED
标记,和自身独特的数据指针标记
。 这里不讨论中断共享的细节,感兴趣的读者可自行阅读代码分析。
7. 中断开关操作
先看一个简单中断相关硬件拓扑结构:
GIC CPU
----------------------------------------- --------------
| ------------- ---------------- | | core0 core2 |
外设中断信号 ---|-->| Distributors |-->| CPU Interfaces |--|--->| |
| -------------- ---------------- | | core1 core3 |
----------------------------------------- --------------
7.1 中断禁用和使能
local_irq_disable() /* 禁用 CPU 本地中断:某个 core 不接收中断输入信号 */
local_irq_enable() /* 禁用 CPU 本地中断:某个 core 接收中断输入信号 */
7.2 中断源屏蔽和使能
操作接口如下:
/* 仅列举两个典型接口,其它的读者可自行阅读源码 */
extern void disable_irq(unsigned int irq); /* 屏蔽中断信号源 @irq */
extern void enable_irq(unsigned int irq); /* 使能中断信号源 @irq */
中断源屏蔽
是指屏蔽外设中断信号源的输入,这需要中断控制芯片的支持,具体来讲,就是中断芯片的 struct irq_chip
对象,需要实现 irq_disable/irq_mask
中的某个接口。如章节 4
例子中,级联 GPIO 中断控制器实现的 sunxi_pinctrl_irq_mask()
接口。
中断源使能
是指开启外设中断信号源的输入,这需要中断控制芯片的支持,具体来讲,就是中断芯片的 struct irq_chip
对象,需要实现 irq_enable/irq_unmask
中的某个接口。如章节 4
例子中,级联 GPIO 中断控制器实现的 sunxi_pinctrl_irq_unmask()
接口。
8. 中断的 CPU 亲和性
中断的 CPU 亲和性
是指系统将某个中断发送給哪些 CPU 核处理。中断亲和性配置,同样需要中断芯片的支持,具体来讲,就是中断芯片的 struct irq_chip
对象,需实现接口 irq_set_affinity
接口,如 GIC 驱动实现的 gic_set_affinity()
接口。
用户空间可通过如下接口之一,对中断的 CPU 亲和性进行配置:
/proc/irq/<N>/smp_affinity # 十六进制掩码形式配置接口,如 f
/proc/irq/<N>/smp_affinity_list # 列表形式配置接口,如 0-3
更多关于中断的 CPU 亲和性
配置细节,可参考内核文档 Documentation/IRQ-affinity.txt
。
irqbalance,是一个有名的操作中断 CPU 亲和性的程序,该程序通过周期性读取中断当前统计数据,按中断在各 CPU 核上分布情况,配置各中断的 CPU 亲和性,力图将它们均匀的分布到各个 CPU 核上。
9. 中断信息用户空间接口
/proc/irq/default_smp_affinity
/proc/irq/<N>/affinity_hint
/proc/irq/<N>/effective_affinity
/proc/irq/<N>/effective_affinity_list
/proc/irq/<N>/node
/proc/irq/<N>/smp_affinity
/proc/irq/<N>/smp_affinity_list
/proc/irq/<N>/spurious
/sys/kernel/irq/<N>/actions
/sys/kernel/irq/<N>/chip_name
/sys/kernel/irq/<N>/hwirq
/sys/kernel/irq/<N>/name
/sys/kernel/irq/<N>/per_cpu_count
/sys/kernel/irq/<N>/type
/proc/interrupts
...
看一下 cat /proc/interrupts
输出,该文件输出了当前的中断统计信息:
上图中:
第1列,显示中断的 Linux虚拟中断号;
第2~5列,显示各中断在每个CPU 核上发生的次数;
第6列,显示中断信号发往的中断芯片名;
第7列,显示中断在信号发往的中断芯片内的硬件中断号;
第8列,显示中断的触发方式:电平触发、沿触发;
第9列,显示中断名称。
另外,第1列不是数字的行,显示的 SGI 中断的发生情况。