3. 设备树实例:GPIO控制器

本章通过一个完整的GPIO控制器设备树节点实例,深入讲解地址资源、中断资源、GPIO范围等核心属性的编写方法与驱动匹配机制。GPIO控制器是嵌入式系统中最基础、最常用的外设之一,掌握其设备树节点的编写规范,对于理解整个设备树体系具有举足轻重的作用。

学习目标: 掌握GPIO控制器设备树节点的标准属性结构;理解#gpio-cells、gpio-controller、interrupt-controller等关键标志位的作用;学会在驱动中通过of_match_table匹配自定义节点。

3.1 GPIO控制器节点结构总览

一个完整的GPIO控制器设备树节点通常包含以下五类属性:兼容性、地址资源、中断资源、GPIO控制标志、其他辅助属性。下面是一个基于ARM架构、挂载在AXI总线上的GPIO控制器节点示例:

gpio0: gpio@ff240000 {
    compatible = "vendor,gpio-controller", "generic-gpio";
    reg = <0x0 0xff240000 0x0 0x1000>;
    interrupts = <0 45 4>;
    interrupt-parent = <&gic>;
    #interrupt-cells = <2>;
    interrupt-controller;
    gpio-controller;
    #gpio-cells = <2>;
    gpio-ranges = <&pinctrl1 0 32 16>;
    ngpios = <16>;
    clocks = <&clkc 15>;
    status = "okay";
};
节点命名规范: 节点名称采用 gpio@基地址 格式,gpio0 是标签(label),用于在其它节点中通过 &gpio0 引用。标签名应具有唯一性,建议按序号递增。

3.2 核心属性详解

下表列出了GPIO控制器节点中每个核心属性的含义、格式与典型值:

属性名 格式 含义 典型值
compatible 字符串列表 驱动匹配标识,优先使用厂商特定字符串 "vendor,gpio-controller"
reg <地址 长度> 寄存器物理地址范围,支持多段 <0x0 0xff240000 0x0 0x1000>
interrupts <中断号 触发类型> 控制器本身的中断线描述 <0 45 4>
gpio-controller 空标志 标记该节点为GPIO控制器 无值
#gpio-cells 整数 GPIO specifier 中单元格数量 2
gpio-ranges <pin控制器 本地偏移 全局偏移 数量> GPIO与引脚控制器的映射关系 <&pinctrl1 0 32 16>
ngpios 整数 该控制器管理的GPIO引脚总数 16
常见错误: 忘记添加 gpio-controller; 标志会导致其它节点无法通过 &gpio0 引用该控制器;#gpio-cells 的值必须与驱动解析逻辑一致,通常为2(引脚号 + 标志位)。

3.3 GPIO specifier 与客户节点引用

当一个外设节点需要使用GPIO时,通过 gpios 或 xxx-gpios 属性引用控制器。每个GPIO引用称为一个 GPIO specifier,其单元格数量由控制器的 #gpio-cells 决定。

/* 客户节点:使用 gpio0 的第5号引脚,低电平有效 */
led-red {
    compatible = "gpio-leds";
    gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;
};

/* 客户节点:使用 gpio0 的第3号引脚,高电平有效,用于按键中断 */
button-reset {
    compatible = "gpio-keys";
    gpios = <&gpio0 3 GPIO_ACTIVE_HIGH>;
    interrupts = <3 IRQ_TYPE_EDGE_RISING>;
};

在驱动代码中,通过 of_get_named_gpio_flags() 或 gpiod_get() 系列API解析这些引用,无需手动计算寄存器偏移。

3.4 中断子节点与层级结构

当GPIO控制器同时作为中断控制器时(即同时声明 interrupt-controller; 和 #interrupt-cells),其内部可以包含中断子节点来描述每个引脚的触发方式。这种层级结构在GPIO扩展芯片(如PCA9535、MCP23017)中尤为常见。

gpio_expander: gpio@20 {
    compatible = "nxp,pca9535";
    reg = <0x20>;
    gpio-controller;
    #gpio-cells = <2>;
    interrupt-parent = <&gpio0>;
    interrupts = <10 IRQ_TYPE_EDGE_FALLING>;
    interrupt-controller;
    #interrupt-cells = <2>;

    /* 中断子节点:定义每个引脚的默认中断配置 */
    irq_pins {
        interrupt-parent = <&gpio_expander>;
        interrupts = <0 IRQ_TYPE_LEVEL_LOW>,
                     <1 IRQ_TYPE_LEVEL_LOW>;
    };
};
设计建议: 对于I2C/SPI接口的GPIO扩展芯片,务必在节点中同时声明 gpio-controller 和 interrupt-controller,否则内核无法建立从GPIO到中断控制器的完整映射,导致 gpio_to_irq() 调用失败。

3.5 GPIO控制器驱动匹配流程

下面这张SVG流程图展示了从设备树节点到驱动 probe 函数的完整匹配链路:

GPIO控制器设备树节点 → 驱动匹配流程 DTS 编译为 DTB Bootloader 加载 DTB 内核 OF 解析节点 compatible 匹配驱动 驱动 probe() 执行 关键匹配条件 • compatible 字符串完全匹配 • reg 地址范围不冲突 • 中断资源可正确映射 • gpio-controller 标志存在 • 时钟/电源依赖已就绪 驱动端匹配代码 static const struct of_device_id gpio_of_match[] = { { .compatible = "vendor,gpio-controller" }, { /* end */ } };

3.6 驱动端解析设备树资源的典型代码

在GPIO控制器的驱动 probe 函数中,通常需要解析以下四类资源。下面给出一个经过简化的、可直接参考的代码框架:

static int my_gpio_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct device_node *np = dev->of_node;
    struct resource *res;
    void __iomem *base;
    int irq, ngpios, ret;

    /* 1. 解析 reg 地址资源 */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    base = devm_ioremap_resource(dev, res);
    if (IS_ERR(base))
        return PTR_ERR(base);

    /* 2. 解析中断资源 */
    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;

    /* 3. 解析 ngpios 属性(可选,默认16) */
    ret = of_property_read_u32(np, "ngpios", &ngpios);
    if (ret)
        ngpios = 16;

    /* 4. 注册 gpio_chip 结构体 */
    struct gpio_chip *chip = devm_kzalloc(dev, sizeof(*chip), GFP_KERNEL);
    chip->label = dev_name(dev);
    chip->parent = dev;
    chip->owner = THIS_MODULE;
    chip->base = -1;               /* 动态分配基号 */
    chip->ngpio = ngpios;
    chip->of_node = np;
    chip->request = my_gpio_request;
    chip->free = my_gpio_free;
    chip->direction_input = my_gpio_direction_input;
    chip->direction_output = my_gpio_direction_output;
    chip->get = my_gpio_get;
    chip->set = my_gpio_set;

    ret = devm_gpiochip_add_data(dev, chip, NULL);
    if (ret)
        return ret;

    dev_info(dev, "GPIO controller probed: %d pins, irq %d\n", ngpios, irq);
    return 0;
}
关键API说明: devm_gpiochip_add_data() 是推荐使用的注册接口,它自动管理生命周期;of_property_read_u32() 用于读取设备树中的整数属性;platform_get_irq() 自动解析 interrupts 属性并返回Linux中断号。

3.7 多bank GPIO控制器的设备树设计

对于包含多个bank(每组32个引脚)的复杂GPIO控制器,设备树节点需要为每个bank独立描述地址范围和中断线。下面是一个双bank控制器的节点示例:

gpio1: gpio@ff250000 {
    compatible = "vendor,dual-bank-gpio";
    reg = <0x0 0xff250000 0x0 0x1000>,  /* Bank0 寄存器 */
          <0x0 0xff251000 0x0 0x1000>;  /* Bank1 寄存器 */
    reg-names = "bank0", "bank1";

    interrupt-controller;
    #interrupt-cells = <2>;
    interrupt-parent = <&gic>;
    interrupts = <0 46 4>,   /* Bank0 中断 */
                 <0 47 4>;   /* Bank1 中断 */
    interrupt-names = "bank0_irq", "bank1_irq";

    gpio-controller;
    #gpio-cells = <2>;
    ngpios = <64>;           /* 两个bank共64个引脚 */
    gpio-ranges = <&pinctrl1 0 0 32>,
                  <&pinctrl1 32 64 32>;
};

驱动端通过 platform_get_resource_byname(pdev, IORESOURCE_MEM, "bank0") 和 platform_get_irq_byname(pdev, "bank0_irq") 分别获取每个bank的资源,避免硬编码索引。

3.8 调试与验证方法

设备树节点编写完成后,建议按以下步骤进行验证:

  1. 编译检查: 使用 dtc -I dts -O dtb -o test.dtb test.dts 确认语法正确。
  2. 内核启动日志: 检查 dmesg 中是否出现 gpiochip_add 相关打印,确认引脚数量与中断号正确。
  3. 用户空间验证: 通过 /sys/kernel/debug/gpio 查看已注册的GPIO控制器列表。
  4. 中断测试: 使用 cat /proc/interrupts 观察GPIO中断号是否注册成功。
  5. 回读测试: 通过 libgpiod 工具(gpioinfo、gpioget、gpioset)验证引脚读写功能。
常见调试陷阱: 如果驱动 probe 未被调用,首先检查 compatible 字符串是否与驱动中的 of_match_table 完全一致(包括大小写和逗号);其次确认节点 status 属性不是 "disabled"。

3.9 本章小结

本章通过一个完整的GPIO控制器设备树节点实例,系统讲解了地址资源、中断资源、GPIO范围、控制器标志等核心属性的编写规范。重点强调了 gpio-controller 与 #gpio-cells 的配对使用、中断子节点的层级设计、以及驱动端解析设备树资源的标准代码框架。掌握这些内容后,读者可以独立为任意GPIO控制器编写正确的设备树节点,并快速定位驱动匹配失败的问题。