前言
- 因为之前师傅让我编写了一个 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 寄存器的值,手动产生中断信号。
参考
- 韦东山驱动大全