Skip to content

LV015-设备树中怎么写

参考文档可以看这里:interrupts.txt - Documentation/devicetree/bindings/interrupt-controller/interrupts.txt - Linux source code v4.19.7

一、设备树里的中断控制器

中断 硬件框图如下:

image-20250317154800714

在硬件上,“中断控制器”只有 GIC 这一个,但是我们在软件上也可以把上图中的“ GPIO”称为“中断控制器”。很多芯片有多个 GPIO 模块,比如 GPIO1、GPIO2 等等。所以软件上的“中断控制器”就有很多个: GIC、 GPIO1、 GPIO2 等等

GPIO1 连接到 GIC, GPIO2 连接到 GIC,所以 GPIO1 的父亲是 GIC, GPIO2 的父亲是 GIC。假设 GPIO1 有 32 个中断源,但是它把其中的 16 个汇聚起来向 GIC 发出一个中断,把另外 16 个汇聚起来向 GIC 发出另一个中断。这就意味着 GPIO1 会用到 GIC 的两个中断,会涉及 GIC 里的 2 个 hwirq。

这些层级关系、中断号(hwirq),都会在设备树中有所体现。

在设备树中,中断控制器节点中必须有一个属性: interrupt-controller,表明它是“中断控制器”。还必须有一个属性: #interrupt-cells,表明引用这个中断控制器的话需要多少个 cell。 #interrupt-cells 的值一般有如下取值:

(1)#interrupt-cells = <1> :别的节点要使用这个中断控制器时,只需要一个 cell 来表明使用“哪一个中断”。

(2)#interrupt-cells = <2> :别的节点要使用这个中断控制器时,需要一个 cell 来表明使用“哪一个中断”;还需要另一个 cell 来描述中断,一般是表明触发类型(trigger type and level flags):

c
// 第 2 个 cell 的 bits [3:0]
1 = low-to-high edge triggered,上升沿触发
2 = high-to-low edge triggered,下降沿触发
4 = active high level-sensitive,高电平触发
8 = active low level-sensitive,低电平触发

示例如下:

c
vic: intc@10140000 {
    compatible = "arm,versatile-vic";
    interrupt-controller;
    #interrupt-cells = <1>;
    reg = <0x10140000 0x1000>;
}

如果中断控制器有级联关系,下级的中断控制器还需要表明它的“ interrupt-parent ” 是谁,用了 interrupt-parent ” 中的哪一个“ interrupts”

二、设备树里使用中断

一个外设,它的中断信号接到哪个“中断控制器”的哪个“中断引脚”,这个中断的触发方式是怎样的?这 3 个问题,在设备树里使用中断时,都要有所体现。

1. 相关属性

  • 要用哪一个中断控制器里的中断?
c
interrupt-parent=<&XXXX>
  • 要用哪一个中断?
c
interrupts=<>;

Interrupts 里要用几个 cell?这个是由 interrupt-parent 对应的中断控制器决定。在中断控制器里有“ #interrupt-cells”属性,它指明了要用几个 cell 来描述中断。比如:

c
i2c@7000c000 {
    gpioext: gpio-adnp@41 {
        compatible = "ad,gpio-adnp";
        interrupt-parent = <&gpio>;
        interrupts = <160 1>;
        gpio-controller;
        #gpio-cells = <1>;
        interrupt-controller;
        #interrupt-cells = <2>;
    };
    //......
};
  • 新的写法:interrupts-extended

一个“ interrupts-extended”属性就可以既指定“ interrupt-parent”,也指定“ interrupts”,比如:

c
interrupts-extended = <&intc1 5 1>, <&intc2 1 0>;

2. 总结

简单总结一下与中断有关的设备树属性信息:

①、 #interrupt-cells,指定中断源的信息 cells 个数。

②、 interrupt-controller,表示当前节点为中断控制器。

③、 interrupts,指定中断号,触发方式等。

④、 interrupt-parent,指定父中断,也就是中断控制器。

三、设备树里中断节点的示例

1. 示例 1

我们看一下 imx6ul.dtsi - arch/arm/boot/dts/imx6ul.dtsi

c
	intc: interrupt-controller@a01000 {
		compatible = "arm,gic-400", "arm,cortex-a7-gic";
		interrupts = <GIC_PPI 9 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_HIGH)>;
		#interrupt-cells = <3>;
		interrupt-controller;
		interrupt-parent = <&intc>;
		reg = <0x00a01000 0x1000>,
		      <0x00a02000 0x2000>,
		      <0x00a04000 0x2000>,
		      <0x00a06000 0x2000>;
	};

第 2 行, compatible 属性值为“arm, cortex-a7-gic”在 Linux 内核源码中搜索“arm, cortex-a7- gic”即可找到 GIC 中断控制器驱动文件。

第 3 行, #interrupt-cells 和#address-cells、 #size-cells 一样。表示此中断控制器下设备的 cells 大小,对于设备而言,会使用 interrupts 属性描述中断信息, #interrupt-cells 描述了 interrupts 属性的 cells 大小,也就是一条信息有几个 cells。每个 cells 都是 32 位整形值,对于 ARM 处理的 GIC 来说,一共有 3 个 cells,这三个 cells 的含义如下:

第一个 cells:中断类型, 0 表示 SPI 中断, 1 表示 PPI 中断。第二个 cells:中断号,对于 SPI 中断来说中断号的范围为 0~987,对于 PPI 中断来说中断号的范围为 0~15。第三个 cells:标志, bit[3:0]表示中断触发类型,为 1 的时候表示上升沿触发,为 2 的时候表示下降沿触发,为 4 的时候表示高电平触发,为 8 的时候表示低电平触发。 bit[15:8]为 PPI 中断的 CPU 掩码。

第 4 行, interrupt-controller 节点为空,表示当前节点是中断控制器。

2. 示例 2

对于 gpio 来说, gpio 节点也可以作为中断控制器,比如 imx6ul.dtsi 文件中的 gpio5 节点内容如下所示:

c
			gpio5: gpio@20ac000 {
				compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
				reg = <0x020ac000 0x4000>;
				interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,
					     <GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;
				clocks = <&clks IMX6UL_CLK_GPIO5>;
				gpio-controller;
				#gpio-cells = <2>;
				interrupt-controller;
				#interrupt-cells = <2>;
				gpio-ranges = <&iomuxc 0 7 10>, <&iomuxc 10 5 2>;
			};

在这段代码的第 4 行, interrupts 描述中断源信息,对于 gpio5 来说一共有两条信息,中断类型都是 SPI,触发电平都是 IRQ_TYPE_LEVEL_HIGH。不同之处在于中断源,一个是 74,一个是 75,打开可以打开《IMX6ULL 参考手册》的“Chapter 3 Interrupts and DMA Events”章节,找到表 3-1,有

image-20260119200741605

可以看出, GPIO5 一共用了 2 个中断号,一个是 74,一个是 75。其中 74 对应 GPIO5_IO00~GPIO5_IO15 这低 16 个 IO, 75 对应 GPIO5_IO16~GPIOI5_IO31 这高 16 位 IO。

第 8 行, interrupt-controller 表明了 gpio5 节点也是个中断控制器,用于控制 gpio5 所有 IO 的中断。

第 9 行,将#interrupt-cells 修改为 2。

怎么引用这个 GPIO 节点的中断控制器?应该是在 NXP 维护的 4.1.15 版本内核中有 arch/arm/boot/dts/imx6ull-14x14-evk.dts 设备树文件,上面有这样一个节点:

c
    fxls8471@1e {
        compatible = "fsl,fxls8471";
        reg = <0x1e>;
        position = <0>;
        interrupt-parent = <&gpio5>;
        interrupts = <0 8>;
    };

fxls8471 是 NXP 官方的 6ULL 开发板上的一个磁力计芯片, fxls8471 有一个中断引脚链接到了 I.MX6ULL 的 SNVS_TAMPER0 因脚上,这个引脚可以复用为 GPIO5_IO00。

第 5 行, interrupt-parent 属性设置中断控制器,这里使用 gpio5 作为中断控制器。

第 6 行, interrupts 设置中断信息, 0 表示 GPIO5_IO00, 8 表示低电平触发。

3. 示例 3

这里参考的是伟东山的嵌入式教程,所以以 100ASK_IMX6ULL 开发板为例,在 arch/arm/boot/dts 目录下可以看到 2 个文件: imx6ull.dtsi、 100ask_imx6ull-14x14.dts 。我们把里面有关中断的部分内容抽取出来。

image-20250317210527344

从设备树反推 IMX6ULL 的中断体系,如下,比之前的框图多了一个“ GPC INTC”:

image-20250317210628880

GPC INTC 的 英 文 是 : General Power Controller, Interrupt Controller。它提供中断屏蔽、中断状态查询功能,实际上这些功能在 GIC 里也实现了,觉得有点多余。除此之外,它还提供唤醒功能,这才是保留它的原因。

4. alpha 开发板中的按键中断

4.1 硬件原理图

开发板上有一个按键,原理图如下:

image-20250318091821501

按键 KEY0 是连接到 I.MX6U 的 UART1_CTS 这个 IO 上的, KEY0 接了一个 10K 的上拉电阻,因此 KEY0 没有按下的时候 UART1_CTS 应该是高电平,当 KEY0 按下以后 UART1_CTS 就是低电平。搜一下参考手册就会发现,这个引脚是 GPIO1_IO18:

image-20260119200943901

4.2 中断号

可以看这个笔记《LV04-07-中断与异常-05-IMX6ULL 按键中断实例 | 苏木》这里再简单了解一下。我们前边知道了按键接在了 GPIO1_IO18 上边,我们可以查看《I.MX6UL 参考手册》的 3.2 Cortex A7 interrupts 一节,找到这个 GPIO 管脚对应的中断号:

image-20260119201045674

可以看到 GPIO1 的 0 -15 管脚使用的是 66,16 - 31 使用的是 67,这里只是 IRQ 的编号,对应到 GIC 的 SPI 中断号需要在此编号基础上加上 32,所以这里的按键中断号实际为 99(67+32)。但是其实在 linux 中开发的时候,会有函数(例如 gpio_to_irq())自动帮我们计算,我们只需要知道是哪个引脚就可以了。

4.3 触发方式

触发方式就可以看这个 irq.h - include/dt-bindings/interrupt-controller/irq.h

c
#define IRQ_TYPE_NONE 0 		// 无中断触发类型
#define IRQ_TYPE_EDGE_RISING 1  // 上升沿触发
#define IRQ_TYPE_EDGE_FALLING 2 // 下降沿触发
#define IRQ_TYPE_EDGE_BOTH (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)// 双边沿触发
#define IRQ_TYPE_LEVEL_HIGH 4 	// 高电平触发
#define IRQ_TYPE_LEVEL_LOW 8 	// 低电平触发

4.4 中断节点

按键 KEY0 使用中断模式,需要在“key”节点下添加中断相关属性,添加完成以后的“key”节点内容如下所示:

c
sdev_key {
    #address-cells = <1>;
    #size-cells = <1>;
    compatible = "sdev_key";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_key>;
    key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */
    interrupt-parent = <&gpio1>;
    interrupts = <18 IRQ_TYPE_EDGE_BOTH>; /* FALLING RISING */
    status = "okay";
};

第 8 行,设置 interrupt-parent 属性值为“gpio1”,因为 KEY0 所使用的 GPIO 为 GPIO1_IO18,也就是设置 KEY0 的 GPIO 中断控制器为 gpio1。

第 9 行,设置 interrupts 属性,也就是设置中断源,第一个 cells 的 18 表示 GPIO1 组的 18 号 IO。

其中这个 gpio1 是在 imx6ul.dtsi

c
gpio1: gpio@209c000 {
    compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
    reg = <0x0209c000 0x4000>;
    interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>,
    <GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clks IMX6UL_CLK_GPIO1>;
    gpio-controller;
    #gpio-cells = <2>;
    interrupt-controller;
    #interrupt-cells = <2>;
    gpio-ranges = <&iomuxc  0 23 10>, <&iomuxc 10 17 6>,
    <&iomuxc 16 33 16>;
};

四、在代码中获得中断

之前我们学习设备树的时候 , 知道设备树中的节点有些能被转换为内核里的 platform_device,有些不能:

(1)根节点下含有 compatile 属性的子节点,会转换为 platform_device

(2)含有特定 compatile 属性的节点的子节点,会转换为 platform_device 如果一个节点的 compatile 属性,它的值是这 4 者之一: "simplebus", "simple-mfd", "isa", "arm, amba-bus",那么它的子结点(需含 compatile 属性)也可以转换为 platform_device。

(3)总线 I2C、 SPI 节点下的子节点: 不转换为 platform_device 某个总线下到子节点, 应该交给对应的总线驱动程序来处理, 它们不应该被转换为 platform_device。

1. 对于 platform_device

一个节点能被转换为 platform_device,如果它的设备树里指定了中断属性,那么可以从 platform_device 中获得“中断资源”,函数如下,可以使用 platform_get_resource() 函数获得 IORESOURCE_IRQ 资源,即中断号:

c
/**
 * platform_get_resource - get a resource for a device
 * @dev: platform device
 * @type: resource type 取哪类资源? IORESOURCE_MEM、 IORESOURCE_REG、IORESOURCE_IRQ 等
 * @num: resource index 这类资源中的哪一个?
 */
struct resource *platform_get_resource(struct platform_device *dev,
				       unsigned int type, unsigned int num)
{
	//......
}

2. 对于 I2C 设备、 SPI 设备

对于 I2C 设备节点, I2C 总线驱动在处理设备树里的 I2C 子节点时,也会处理其中的中断信息。一个 I2C 设备会被转换为一个 i2c_client 结构体,中断号会保存在 i2c_client 的 irq 成员里,代码如下(i2c-core-base.c - drivers/i2c/i2c-core-base.c):

c
static int i2c_device_probe(struct device *dev)
{
	//......
	if (!client->irq && !driver->disable_i2c_core_irq_mapping) {
		int irq = -ENOENT;

		if (client->flags & I2C_CLIENT_HOST_NOTIFY) {
			//......
		} else if (dev->of_node) {
			irq = of_irq_get_byname(dev->of_node, "irq");
			if (irq == -EINVAL || irq == -ENODATA)
				irq = of_irq_get(dev->of_node, 0); //从设备树解析出中断号
		} else if (ACPI_COMPANION(dev)) {
			irq = acpi_dev_gpio_irq_get(ACPI_COMPANION(dev), 0);
		}
		//......
        client->irq = irq;
	}
    //......
}

对于 SPI 设备节点, SPI 总线驱动在处理设备树里的 SPI 子节点时,也会处理其中的中断信息。一个 SPI 设备会被转换为一个 spi_device 结构体,中断号会保存在 spi_device 的 irq 成员里 ,代码如下(spi.c - drivers/spi/spi.c):

c
static int spi_drv_probe(struct device *dev)
{
	//......
	if (dev->of_node) {
		spi->irq = of_irq_get(dev->of_node, 0);//从设备树解析出中断号
		//......
	}
	//......
}

3. 调用 of_irq_get() 获得中断号

如果设备节点既不能转换为 platform_device,它也不是 I2C 设备,不是 SPI 设备,那么在驱动程序中可以自行调用 of_irq_get() 函数去解析设备树,得到中断号。

4. 对于 GPIO

可以使用 gpio_to_irq() 或者 gpiod_to_irq() 获得中断号。例如 gpio_keys.c - drivers/input/keyboard/gpio_keys.c 中第 559 行:

image-20250318093808546

假设在设备树中有如下节点:

c
gpio-keys {
    compatible = "gpio-keys";
    pinctrl-names = "default";
    user {
        label = "User Button";
        gpios = <&gpio5 1 GPIO_ACTIVE_HIGH>;
        gpio-key,wakeup;
        linux,code = <KEY_1>;
    };
};

那么可以使用下面的函数获得引脚和 flag:

c
button->gpio = of_get_gpio_flags(pp, 0, &flags);
bdata->gpiod = gpio_to_desc(button->gpio);

再去使用 gpiod_to_irq 获得中断号:

c
irq = gpiod_to_irq(bdata->gpiod);

参考资料:

genirq: add threaded interrupt handler support - kernel/git/torvalds/linux.git - Linux kernel source tree

Linux 中断管理 (1)Linux 中断管理机制 - ArnoldLu - 博客园

Linux 卸载驱动时,提示:Trying to free already-free IRQ-CSDN 博客

嵌入式 Linux 驱动笔记(二十七)------中断子系统框架分析_irq: type mismatch-CSDN 博客