前言
- 因为之前师傅让我编写了一个 I2C 扩展 GPIO 控制器,但是这个 GPIO 控制器并没有要求编写中断的内容,所以就一直搁浅了。后面突发奇想,感觉还是要了解一下中断控制器的驱动程序如何编写,因此就有了该篇博客。
- 在韦东山老师的驱动大全中,其实已经介绍了中断控制器的驱动程序如何编写。但是我后面简单了解了一下,发现韦东山老师的中断驱动都是参考的 IMX6ULL 的中断控制器驱动。而在一些特殊的项目中,可能会要求外扩中断控制器,例如 ste-nomadik-nhk15 开发板就采用了两个 I2C 外扩的中断控制器。
i2c0 {stmpe0: stmpe2401@43 {compatible = "st,stmpe2401";reg = <0x43>;// ...stmpe_gpio43: stmpe_gpio {compatible = "st,stmpe-gpio";gpio-controller;#gpio-cells = <2>;interrupt-controller;// ...};};stmpe1: stmpe2401@44 {compatible = "st,stmpe2401";reg = <0x44>;// ...stmpe_gpio44: stmpe_gpio {compatible = "st,stmpe-gpio";gpio-controller;#gpio-cells = <2>;interrupt-controller;//...};};// ...
};
- 在 ARM Cortex-A和Cortex-M中断处理浅析 一文中,我简单介绍了 Cortex-A 和Cortex-M 中断处理机制。我们知道了对于 Cortex-A 核的中断处理,需要软件实现。
- Linux 做了一套中断处理框架,需要驱动工程师按照这套框架进行适配。这里,我将会简单介绍一下,如果你希望向 Linux 中注册一个 中断控制器,应该如何做。
- 个人邮箱:zhangyixu02@gmail.com
- 微信公众号:风正豪

中断控制器驱动程序
重要结构体
- 所谓的 Linux 驱动程序,本质上就是按照 Linux 内核开发者规定的框架进行填鸭。这里我将介绍比较重要的结构体,作为驱动开发者,如果要编写一个中断控制器要填充的结构体成员有哪些。
irq_chip
结构体重要成员:
irq_chip.irq_ack
: 当中断产生时,将会调用该成员函数清除中断源信号,标记该中断被处理,为新的相同中断触发做准备。无特殊需求,可以直接调用 Linux 提供的 irq_gc_ack_set_bit 函数。irq_chip.irq_mask
: 中断产生,需要调用该成员函数屏蔽相同的中断源,防止同一个中断执行过程中再次被自己中断导致重入问题。无特殊需求,可以直接调用 Linux 提供的 irq_gc_mask_set_bit 函数。irq_chip.irq_mask_ack
: 是 irq_chip.irq_ack
和 irq_chip.irq_mask
之和。如果定义了这个成员,那么 irq_chip.irq_ack
和 irq_chip.irq_mask
就没有必要再进行定义。irq_chip.irq_unmask
: 当中断执行完成之后,我们需要调用该函数解除对该中断的中断源屏蔽解除。无特殊需求,可以直接调用 Linux 提供的 irq_gc_mask_clr_bit
函数。irq_chip.irq_set_type
: 当注册中断时,设置中断的触发类型时将会调用该函数。这个必须提供,并且必须由芯片原厂提供,因为不同的中断控制器其中断触发类型不同。
- 需要支持
irq_gc_mask_clr_bit
、 irq_gc_mask_set_bit
和 irq_gc_ack_set_bit
函数就需要填充 irq_chip_type.regs
中的 ack
和 mask
成员。
struct irq_chip_regs {unsigned long enable; unsigned long disable; unsigned long mask; unsigned long ack; unsigned long eoi; unsigned long type; unsigned long polarity;
};
成员变量被调用时机
- 设备驱动调用
request_irq
将会导致 irq_chip.irq_unmask
被调用用以使能中断。 - 如果是电平触发的函数,在函数执行之前,会调用
irq_chip.irq_mask
和 irq_chip.irq_ack
来屏蔽中断,执行完成后调用 irq_chip.irq_unmask
重新使能中断。
if (chip->irq_mask_ack) {chip->irq_mask_ack(&desc->irq_data);
} else {chip->irq_mask(&desc->irq_data);if (chip->irq_ack)chip->irq_ack(&desc->irq_data);
}
- 屏蔽中断可以发送在外设、中断控制器、CPU 三个位置。
- 设备驱动层调用
local_irq_disable
和 local_irq_enable
针对的不是外部的中断控制器,而是直接让 CPU 自身不响应中断请求。 - 设备驱动层调用
disable_irq
和 enable_irq
针对的是中断控制器。当调用 disable_irq
,那么将会暂时屏蔽中断,在内核中的实现层面是做了延后屏蔽,直到 enable_irq
再执行 ISR。 - 外设端的中断信号产生高度依赖外设本身,因此 Linux 并不会提供标准的 API,而是由外设驱动自行设计。
GIC 中断控制器
- GIC 是中断管理控制器,是直接与 CPU 核连接的。该控制器驱动程序是由 ARM 官方提供,如果是作为芯片原厂的工程师,是无需关心的。对于绝大多数人也接触不到,因此我不做介绍。(笑,水字,狗头)
- 如果你还是希望学习阅读 GIC 控制器如何编写,请自行阅读
Linux-4.9.88\drivers\irqchip\irq-gic.c
。
GPIO 级联中断控制器
- 对于 GPIO 级联中断控制器就是挂载在 GPIO 控制器下的中断控制器,例如前言中所说的 I2C GPIO 外扩中断控制器就属于级联在 GPIO 控制器下的中断控制器。
- 这类中断控制器可以由芯片原厂工程师编写,也可以由方案商的驱动工程师根据项目需求进行编写。对于想要学习这类中断控制器驱动编写的,可以参考
Linux-4.9.88\drivers\gpio\gpio-stmpe.c
或 Linux-4.9.88\drivers\gpio\gpio-pca953x.c
。
设备树
i2c0 {stmpe0: stmpe2401@43 {compatible = "st,stmpe2401";reg = <0x43>;// ...stmpe_gpio43: stmpe_gpio {compatible = "st,stmpe-gpio";gpio-controller;#gpio-cells = <2>;interrupt-controller;// ...};};stmpe1: stmpe2401@44 {compatible = "st,stmpe2401";reg = <0x44>;// ...stmpe_gpio44: stmpe_gpio {compatible = "st,stmpe-gpio";gpio-controller;#gpio-cells = <2>;interrupt-controller;//...};};// ...
};
驱动程序
- 如下为一个简单的 GPIO 级联中断控制器的框架,请自行参考上述两个文件对照这个框架学习。
#define chip0 0
#define chip1 1
#define extend_irq_chip_id chip0
static struct irq_chip extend_irq_chip = {.name = "extend_irq_chip", .irq_mask = extend_irq_mask, .irq_unmask = extend_irq_unmask, .irq_bus_lock = extend_irq_bus_lock, .irq_bus_sync_unlock = extend_irq_bus_sync_unlock, .irq_set_type = extend_irq_set_type, .irq_shutdown = extend_irq_shutdown,
};
static irqreturn_t extend_irq_handler(int irq, void *devid)
{if (devid == chip0) {} else if (devid == chip1) {} else {return IRQ_NONE;}return IRQ_HANDLED;
}static int extend_irq_probe(struct i2c_client *client,const struct i2c_device_id *i2c_id)
{struct gpio_chip *gc;gc->parent = &client->dev;devm_request_threaded_irq(&client->dev,client->irq,NULL,extend_irq_handler,IRQF_TRIGGER_LOW | IRQF_ONESHOT |IRQF_SHARED,dev_name(&client->dev), extend_irq_chip_id);gpiochip_irqchip_add_nested(gc,&extend_irq_chip,0,handle_simple_irq,IRQ_TYPE_NONE);gpiochip_set_nested_irqchip(gc,&extend_irq_chip,client->irq);
}
链式中断控制器
- 这个名词是韦东山老师发明的,所以我这里也直接用他所创作的这个名词了。
- 下图为链式后中断控制器和层级中断控制器的定义,对于链式中断就是说,中断控制器只会给 GIC 控制器发送一个中断信号,但是链式中断控制器里面还有很多细分的中断信号需要判断。上面的 GPIO 级联中断控制器也就是链式中断控制器的一种,只不过这里的链式中断控制器是直接连接的 GIC,而 GPIO 级联中断控制器是连接的 GPIO 中断控制器。
- 层级式中断控制器就比较方便理解,级联中断控制器会像 GIC 中断控制器发送多个中断信号,每个中断信号对应一个中断事件,就不再需要进行细分。

设备树
/{virtual_intc: virtual_intc_100ask {compatible = "100ask,virtual_intc";reg = <0x0209c000 0x4000>;interrupt-controller;#interrupt-cells = <2>;interrupt-parent = <&intc>;interrupts = <GIC_SPI 122 IRQ_TYPE_LEVEL_HIGH>;};gpio_keys_100ask {compatible = "100ask,gpio_key";interrupt-parent = <&virtual_intc>;interrupts = <0 IRQ_TYPE_LEVEL_HIGH>,<1 IRQ_TYPE_LEVEL_HIGH>,<2 IRQ_TYPE_LEVEL_HIGH>,<3 IRQ_TYPE_LEVEL_HIGH>;};};
驱动程序
- 实话实说,感觉韦东山老师的链式中断控制器的示例代码有一点点小小的问题。也可能是我理解上存在偏差,这里是我参考 imx6ull 和 rk3568 的 GPIO 中断控制器写的一个简单的示例代码。各位可参考
Linux-4.9.88\drivers\gpio\gpio-mxc.c
配合如下示例代码学习。
static struct irq_domain *gpio_irq_domain;struct private_data {
};
static void gpio_irq_handler(struct irq_desc *desc)
{u32 irq_stat;struct private_data *port = irq_desc_get_handler_data(desc);struct irq_chip *chip = irq_desc_get_chip(desc);chained_irq_enter(chip, desc);gpio_hwirq = get_gpio_hwirq();generic_handle_irq(irq_find_mapping(port->domain, gpio_hwirq));chained_irq_exit(chip, desc);
}static int mxc_gpio_probe(struct platform_device *pdev)
{struct device_node *np = pdev->dev.of_node;struct resource *iores; void __iomem *virbase; int gic_hwirq,irq_base; struct irq_chip_generic *gc; gic_hwirq = platform_get_irq(pdev, 0);iores = platform_get_resource(pdev, IORESOURCE_MEM, 0);virbase = devm_ioremap_resource(&pdev->dev, iores);irq_set_chained_handler_and_data(gic_hwirq,gpio_irq_handler,NULL);irq_base = devm_irq_alloc_descs(&pdev->dev, -1, 0, 4, numa_node_id());gpio_irq_domain = irq_domain_add_legacy(np, 4, irq_base, 0,&irq_domain_simple_ops, NULL);gpio_irq_domain = irq_domain_add_linear(np, 4,&irq_generic_chip_ops, NULL);gc = devm_irq_alloc_generic_chip(&pdev->dev, "gpio-test", 1, irq_base,virbase, handle_level_irq);gc->chip_types[0].chip.irq_ack = irq_gc_ack_set_bit;gc->chip_types[0].chip.irq_mask = irq_gc_mask_clr_bit;gc->chip_types[0].chip.irq_unmask = irq_gc_mask_set_bit;gc->chip_types[0].chip.irq_set_type = ...; gc->chip_types[0].regs.ack = ...; gc->chip_types[0].regs.mask = ...; devm_irq_setup_generic_chip(&pdev->dev, gc, IRQ_MSK(4),IRQ_GC_INIT_NESTED_LOCK,IRQ_NOREQUEST, 0);
}
层级中断控制器
设备树
#define m 123/{virtual_intc: virtual_intc_100ask {compatible = "100ask,virtual_intc";interrupt-controller;#interrupt-cells = <2>;interrupt-parent = <&intc>;// GIC 中断号起始位置upper_hwirq_base = <122>; // imx6ull//upper_hwirq_base = <210>; // stm32mp157};gpio_keys_100ask {compatible = "100ask,gpio_key";interrupt-parent = <&virtual_intc>;interrupts = <0 IRQ_TYPE_LEVEL_HIGH>,<1 IRQ_TYPE_LEVEL_HIGH>,<2 IRQ_TYPE_LEVEL_HIGH>,<3 IRQ_TYPE_LEVEL_HIGH>;};};
驱动程序
- 层级式中断控制器就很简单了,因为他们不涉及到函数分发的问题。这里直接给韦东山老师的示例代码了。
- 各位可以配合
Linux-4.9.88\arch\arm\mach-imx\gpc.c
学习。
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/clk.h>
#include <linux/err.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/io.h>
#include <linux/random.h>
#include <linux/irq.h>
#include <linux/irqdomain.h>
#include <linux/irqchip/chained_irq.h>
#include <linux/platform_device.h>
#include <linux/pm_runtime.h>
#include <linux/slab.h>
#include <linux/gpio/driver.h>
#include <linux/gpio.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/of_device.h>
#include <linux/bug.h>static struct irq_domain *virtual_intc_domain;
static u32 upper_hwirq_base;static int virtual_intc_domain_translate(struct irq_domain *d,struct irq_fwspec *fwspec,unsigned long *hwirq,unsigned int *type)
{if (is_of_node(fwspec->fwnode)) {if (fwspec->param_count != 2)return -EINVAL;*hwirq = fwspec->param[0];*type = fwspec->param[1];return 0;}return -EINVAL;
}static void virtual_intc_irq_unmask(struct irq_data *d)
{printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);irq_chip_unmask_parent(d);
}static void virtual_intc_irq_mask(struct irq_data *d)
{printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);irq_chip_mask_parent(d);
}static void virtual_intc_irq_eoi(struct irq_data *d)
{printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);irq_chip_eoi_parent(d);
}static struct irq_chip virtual_intc_chip = {.name = "virtual_intc",.irq_mask = virtual_intc_irq_mask,.irq_unmask = virtual_intc_irq_unmask,.irq_eoi = virtual_intc_irq_eoi,
};static int virtual_intc_domain_alloc(struct irq_domain *domain,unsigned int irq,unsigned int nr_irqs, void *data)
{struct irq_fwspec *fwspec = data;struct irq_fwspec parent_fwspec;irq_hw_number_t hwirq;int i;hwirq = fwspec->param[0];for (i = 0; i < nr_irqs; i++)irq_domain_set_hwirq_and_chip(domain, irq + i, hwirq + i,&virtual_intc_chip, NULL);parent_fwspec.fwnode = domain->parent->fwnode;parent_fwspec.param_count = 3; parent_fwspec.param[0] = 0; parent_fwspec.param[1] = fwspec->param[0] + upper_hwirq_base; parent_fwspec.param[2] = fwspec->param[1]; return irq_domain_alloc_irqs_parent(domain, irq, nr_irqs,&parent_fwspec);
}static const struct irq_domain_ops virtual_intc_domain_ops = {.translate = virtual_intc_domain_translate, .alloc = virtual_intc_domain_alloc,
};static int virtual_intc_probe(struct platform_device *pdev)
{ struct irq_domain *parent_domain;struct device_node *parent;int err;printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);err = of_property_read_u32(pdev->dev.of_node, "upper_hwirq_base", &upper_hwirq_base);parent = of_irq_find_parent(pdev->dev.of_node);parent_domain = irq_find_host(parent);virtual_intc_domain = irq_domain_add_hierarchy(parent_domain, 0, 4,pdev->dev.of_node, &virtual_intc_domain_ops,NULL);return 0;
}
static int virtual_intc_remove(struct platform_device *pdev)
{printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);return 0;
}static const struct of_device_id virtual_intc_of_match[] = {{ .compatible = "100ask,virtual_intc", },{ },
};static struct platform_driver virtual_intc_driver = {.probe = virtual_intc_probe,.remove = virtual_intc_remove,.driver = {.name = "100ask_virtual_intc",.of_match_table = of_match_ptr(virtual_intc_of_match),}
};
static int __init virtual_intc_init(void)
{ printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);return platform_driver_register(&virtual_intc_driver);
}
static void __exit virtual_intc_exit(void)
{printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);platform_driver_unregister(&virtual_intc_driver);
}module_init(virtual_intc_init);
module_exit(virtual_intc_exit);MODULE_LICENSE("GPL");
调试
- 我们可以执行如下命令查看中断控制器注册情况。
$ cd /sys/kernel/irq/[虚拟中断号]
$ cat chip_name
$ cat hwirq
- 编写一个设备驱动程序,查看是否能够成功注册中断。
cat /proc/interrupts
- 使用 devmem 工具修改 GIC 寄存器的值,手动产生中断信号。
参考
- 韦东山驱动大全