3、设备树进阶:中断控制器、GPIO控制器、时钟控制器、DMA控制器绑定

在掌握了设备树基础语法和标准属性之后,本章深入探讨嵌入式系统中最关键的四个控制器类设备绑定:中断控制器(Interrupt Controller)、GPIO控制器(GPIO Controller)、时钟控制器(Clock Controller)和DMA控制器(DMA Controller)。这些控制器是SoC内部基础设施的核心,其设备树绑定的正确性直接影响整个系统的中断响应、引脚控制、时钟管理和数据搬运能力。

学习提示: 本章的四个控制器绑定具有高度相似性——每个控制器都定义了自己的“单元”资源(中断号、GPIO编号、时钟ID、DMA通道),并通过*-cells属性描述消费者设备如何引用这些资源。理解这种“提供者-消费者”模式是掌握设备树进阶绑定的关键。

3.1 中断控制器绑定

中断控制器是系统中负责接收外设中断请求并转发给CPU的硬件模块。在设备树中,中断控制器作为中断资源的提供者,通过标准属性描述其能力,并允许消费者设备通过interrupts属性引用中断线。

3.1.1 中断控制器节点属性

属性 类型 描述
interrupt-controller 空属性 标识该节点是一个中断控制器
#interrupt-cells u32 指定消费者设备interrupts属性中每个中断描述符所需的cell数
interrupt-parent phandle 指向该中断控制器自身的中断父节点(级联场景)

以ARM通用中断控制器(GIC)为例,其典型的设备树绑定如下:

gic: interrupt-controller@f0001000 {
    compatible = "arm,cortex-a9-gic";
    #interrupt-cells = <3>;
    interrupt-controller;
    reg = <0xf0001000 0x1000>,
          <0xf0000100 0x100>;
};

对于#interrupt-cells = <3>的GIC,每个中断描述符的三个cell分别表示:中断类型(0=SPI共享外设中断,1=PPI私有外设中断)、中断号、触发类型和极性标志。

3.1.2 消费者设备中断引用

消费者设备通过interrupts属性引用中断控制器提供的中断资源:

uart0: serial@f0002000 {
    compatible = "ns16550a";
    reg = <0xf0002000 0x100>;
    interrupts = <0 33 4>;  /* SPI, 中断号33, 高电平触发 */
    interrupt-parent = <&gic>;
};
关键点: 当系统中存在多个中断控制器时,每个消费者设备必须通过interrupt-parent明确指定其中断父节点。如果省略,内核会使用根节点interrupt-parent的默认值。

3.1.3 中断控制器级联

在复杂SoC中,多个中断控制器可能以级联方式连接。下图展示了一个典型的两级中断控制器拓扑:

中断控制器级联拓扑 CPU 核心 GIC (根中断控制器) GPIO 中断控制器 I2C 中断控制器 外设A 外设B 外设C 级联关系 子控制器通过 interrupt-parent 连接到父控制器

级联场景下的设备树描述:

gpio_controller: gpio@f0003000 {
    compatible = "vendor,gpio-controller";
    reg = <0xf0003000 0x100>;
    interrupt-controller;
    #interrupt-cells = <2>;
    interrupt-parent = <&gic>;
    interrupts = <0 50 4>;  /* 连接到GIC的中断线 */
};

i2c_controller: i2c@f0004000 {
    compatible = "vendor,i2c-controller";
    reg = <0xf0004000 0x100>;
    interrupt-controller;
    #interrupt-cells = <1>;
    interrupt-parent = <&gic>;
    interrupts = <0 51 4>;
};

3.2 GPIO控制器绑定

GPIO控制器管理SoC的通用输入输出引脚,提供引脚复用、方向控制和电平读写功能。在设备树中,GPIO控制器既是GPIO资源的提供者,也可能作为中断控制器提供引脚中断能力。

3.2.1 GPIO控制器节点属性

属性 类型 描述
gpio-controller 空属性 标识该节点是一个GPIO控制器
#gpio-cells u32 指定消费者设备gpios属性中每个GPIO描述符所需的cell数
gpio-ranges prop-encoded-array 描述GPIO控制器与引脚控制器之间的映射关系

典型的GPIO控制器节点:

gpio0: gpio@f0005000 {
    compatible = "vendor,generic-gpio";
    reg = <0xf0005000 0x200>;
    gpio-controller;
    #gpio-cells = <2>;
    interrupt-controller;
    #interrupt-cells = <2>;
    gpio-ranges = <&pinctrl 0 0 32>;
};

对于#gpio-cells = <2>的GPIO控制器,两个cell分别表示:GPIO编号(相对于该控制器的偏移)和GPIO标志(如高有效/低有效、开漏等)。

3.2.2 消费者设备GPIO引用

消费者设备通过gpios或cs-gpios等属性引用GPIO资源:

led_red {
    compatible = "gpio-leds";
    gpios = <&gpio0 15 0>;  /* GPIO0_15, 高有效 */
    label = "red-led";
};

spi0: spi@f0006000 {
    compatible = "vendor,spi-controller";
    reg = <0xf0006000 0x100>;
    cs-gpios = <&gpio0 10 0>,
               <&gpio0 11 0>;
};
常见错误: GPIO编号是相对于控制器的本地偏移,而非全局引脚号。例如gpio0 15表示GPIO控制器0的第15号引脚,而不是SoC的第15号引脚。全局引脚映射由gpio-ranges属性在GPIO控制器和pinctrl子系统之间建立。

3.2.3 GPIO与中断的关联

许多GPIO控制器同时提供引脚中断功能。消费者设备可以通过interrupts属性引用GPIO控制器的中断能力:

key_button {
    compatible = "gpio-keys";
    gpios = <&gpio0 20 1>;  /* GPIO0_20, 低有效 */
    interrupts = <20 2>;     /* GPIO0_20, 下降沿触发 */
    interrupt-parent = <&gpio0>;
    label = "power-key";
};

3.3 时钟控制器绑定

时钟控制器负责生成和分配SoC内部各模块所需的时钟信号。设备树通过时钟绑定描述时钟的提供者、消费者以及时钟频率、相位等属性。

3.3.1 时钟控制器节点属性

属性 类型 描述
#clock-cells u32 指定消费者设备clocks属性中每个时钟描述符所需的cell数
clock-output-names stringlist 可选的时钟输出名称列表
clock-frequency u32 固定频率时钟的输出频率(单位Hz)

时钟控制器节点示例:

clkc: clock-controller@f0007000 {
    compatible = "vendor,clock-controller";
    reg = <0xf0007000 0x1000>;
    #clock-cells = <1>;
    clock-output-names = "cpu_clk", "bus_clk", "uart_clk";
};

3.3.2 消费者设备时钟引用

消费者设备通过clocks属性引用时钟资源,并通过clock-names属性为每个时钟指定名称:

uart1: serial@f0008000 {
    compatible = "ns16550a";
    reg = <0xf0008000 0x100>;
    clocks = <&clkc 2>;       /* 引用clkc的第2个输出(uart_clk) */
    clock-names = "baud_clk";
};
时钟索引约定: 当#clock-cells = <1>时,消费者使用一个整数索引来指定所需的时钟输出。索引0通常对应第一个时钟输出,索引1对应第二个,以此类推。驱动开发者需要查阅SoC手册确认每个索引对应的实际时钟。

3.3.3 时钟树结构

下图展示了一个典型的SoC时钟树结构,包含多个时钟控制器和消费者设备:

SoC时钟树结构 外部晶振 (24MHz) PLL (锁相环) 时钟控制器 (clkc) #clock-cells = <1> cpu_clk (idx 0) bus_clk (idx 1) uart_clk (idx 2) CPU 核心 总线桥 UART 控制器 时钟索引映射 消费者通过 clocks = <&clkc idx> 选择时钟源

3.4 DMA控制器绑定

DMA(Direct Memory Access)控制器允许外设直接访问系统内存,无需CPU干预。设备树中的DMA绑定描述了DMA控制器的通道资源、传输能力和消费者设备的DMA请求映射。

3.4.1 DMA控制器节点属性

属性 类型 描述
dma-controller 空属性 标识该节点是一个DMA控制器
#dma-cells u32 指定消费者设备dmas属性中每个DMA描述符所需的cell数
dma-channels u32 DMA控制器支持的通道数量
dma-requests u32 DMA控制器支持的请求线数量

DMA控制器节点示例:

dma0: dma-controller@f0009000 {
    compatible = "vendor,dma-controller";
    reg = <0xf0009000 0x1000>;
    dma-controller;
    #dma-cells = <2>;
    dma-channels = <8>;
    dma-requests = <16>;
    interrupt-parent = <&gic>;
    interrupts = <0 60 4>,
                 <0 61 4>,
                 <0 62 4>,
                 <0 63 4>;  /* 4个中断线对应不同通道组 */
};

对于#dma-cells = <2>的DMA控制器,两个cell通常表示:DMA通道编号和DMA传输类型或优先级标志。

3.4.2 消费者设备DMA引用

消费者设备通过dmas属性引用DMA通道,并通过dma-names属性为每个DMA通道指定名称:

spi1: spi@f000a000 {
    compatible = "vendor,spi-controller";
    reg = <0xf000a000 0x100>;
    dmas = <&dma0 2 0>,   /* 使用dma0的通道2,类型0 */
           <&dma0 3 0>;   /* 使用dma0的通道3,类型0 */
    dma-names = "tx", "rx";
};
设计模式: 许多高性能外设(如SPI、I2S、UART)同时支持发送和接收DMA通道。使用dma-names属性可以明确区分每个DMA通道的用途,使驱动代码更具可读性。内核DMA引擎API通过dma_request_slave_channel(dev, "tx")这样的名称匹配来获取通道。

3.4.3 DMA请求映射

在复杂SoC中,多个外设共享DMA控制器的请求线。设备树通过dma-requests属性和消费者设备的dmas属性共同建立外设到DMA请求线的映射关系:

/* DMA控制器定义16条请求线 */
dma1: dma-controller@f000b000 {
    compatible = "vendor,dma-controller";
    reg = <0xf000b000 0x1000>;
    dma-controller;
    #dma-cells = <3>;
    dma-channels = <4>;
    dma-requests = <16>;
};

/* 外设A使用请求线5 */
uart2: serial@f000c000 {
    compatible = "ns16550a";
    reg = <0xf000c000 0x100>;
    dmas = <&dma1 0 5 0>;  /* 通道0, 请求线5, 标志0 */
    dma-names = "rx";
};

/* 外设B使用请求线7 */
i2c1: i2c@f000d000 {
    compatible = "vendor,i2c-controller";
    reg = <0xf000d000 0x100>;
    dmas = <&dma1 1 7 0>;  /* 通道1, 请求线7, 标志0 */
    dma-names = "tx";
};

3.5 四种控制器绑定对比

下表总结了中断控制器、GPIO控制器、时钟控制器和DMA控制器在设备树绑定中的核心异同:

控制器类型 提供者标识属性 cells属性 消费者引用属性 典型cell数 cell含义
中断控制器 interrupt-controller #interrupt-cells interrupts 1~3 中断类型、中断号、触发标志
GPIO控制器 gpio-controller #gpio-cells gpios 2 引脚编号、有效电平标志
时钟控制器 (无专用标识) #clock-cells clocks 0~1 时钟输出索引(0表示固定时钟)
DMA控制器 dma-controller #dma-cells dmas 2~3 通道编号、请求线编号、传输标志

3.6 综合示例:多控制器协同

以下是一个完整的设备树片段,展示了中断控制器、GPIO控制器、时钟控制器和DMA控制器在同一个SoC中的协同工作:

/ {
    #address-cells = <1>;
    #size-cells = <1>;
    interrupt-parent = <&gic>;

    gic: interrupt-controller@f0001000 {
        compatible = "arm,cortex-a9-gic";
        #interrupt-cells = <3>;
        interrupt-controller;
        reg = <0xf0001000 0x1000>,
              <0xf0000100 0x100>;
    };

    clkc: clock-controller@f0007000 {
        compatible = "vendor,clock-controller";
        #clock-cells = <1>;
        reg = <0xf0007000 0x1000>;
    };

    gpio0: gpio@f0005000 {
        compatible = "vendor,generic-gpio";
        #gpio-cells = <2>;
        gpio-controller;
        #interrupt-cells = <2>;
        interrupt-controller;
        reg = <0xf0005000 0x200>;
        gpio-ranges = <&pinctrl 0 0 32>;
    };

    dma0: dma-controller@f0009000 {
        compatible = "vendor,dma-controller";
        #dma-cells = <2>;
        dma-controller;
        dma-channels = <8>;
        reg = <0xf0009000 0x1000>;
        interrupts = <0 60 4>;
    };

    uart0: serial@f0002000 {
        compatible = "ns16550a";
        reg = <0xf0002000 0x100>;
        interrupts = <0 33 4>;
        clocks = <&clkc 2>;
        clock-names = "baud_clk";
        dmas = <&dma0 0 0>,
               <&dma0 1 0>;
        dma-names = "tx", "rx";
    };

    led_red {
        compatible = "gpio-leds";
        gpios = <&gpio0 15 0>;
        label = "red-led";
    };
};
综合要点: 上述示例中,UART控制器同时使用了三种控制器资源——中断控制器(interrupts)、时钟控制器(clocks)和DMA控制器(dmas)。每个资源引用都遵循“提供者-消费者”模式,通过phandle指向对应的控制器节点,并通过cell值指定具体的资源索引。这种分层解耦的设计使得硬件描述高度模块化,便于移植和复用。

3.7 常见绑定错误与调试

在实际开发中,控制器绑定错误是设备树调试中最常见的问题。以下是几种典型错误及其排查方法:

错误类型 现象 排查方法
缺少interrupt-parent 中断无法触发,驱动probe失败 检查消费者节点是否显式指定interrupt-parent,或根节点是否有默认值
#*-cells数量不匹配 内核解析设备树时报of_parse_phandle_with_args错误 核对提供者节点的#*-cells值与消费者节点中提供的cell数量是否一致
GPIO编号越界 GPIO操作无效或影响其他引脚 确认GPIO编号小于控制器 ← 上一章 📖 返回目录 下一章 →