2. 设备树基础

设备树(Device Tree,DT)是嵌入式 Linux 系统中用于描述硬件拓扑结构的一种数据结构。它采用树形节点层次结构,将 CPU、内存、外设、中断控制器、GPIO 控制器等硬件资源以节点和属性的形式呈现给内核。在摄像头驱动开发中,设备树是连接硬件寄存器映射、电源管理(Regulator)和 GPIO 控制的关键桥梁。

核心概念:设备树源文件(.dts)经过设备树编译器(dtc)编译为设备树二进制(.dtb),内核在启动时解析 .dtb 并动态创建 platform_device,驱动通过匹配 compatible 属性完成绑定。

2.1 设备树语法基础

设备树使用一种类似于 C 语言的结构化文本格式,以分号结尾,支持宏定义和包含文件(#include)。其基本单元是节点(node)和属性(property)。

/dts-v1/;

/ {
    model = "Embedded Camera Board v1.0";
    compatible = "vendor,camera-board";

    chosen {
        bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2 rw";
    };

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;
        cpu@0 {
            compatible = "arm,cortex-a7";
            reg = <0x0>;
        };
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x40000000>;  /* 1GB RAM */
    };

    soc {
        #address-cells = <1>;
        #size-cells = <1>;
        compatible = "simple-bus";
        ranges;

        i2c@ffd00000 {
            compatible = "vendor,i2c-controller";
            reg = <0xffd00000 0x1000>;
            #address-cells = <1>;
            #size-cells = <0>;
            status = "okay";

            camera_sensor: camera@3c {
                compatible = "vendor,camera-sensor-v1";
                reg = <0x3c>;
                clocks = <&clkc 15>;
                reset-gpios = <&gpio1 7 GPIO_ACTIVE_LOW>;
                power-gpios = <&gpio2 5 GPIO_ACTIVE_HIGH>;
                avdd-supply = <&reg_camera_avdd>;
                dvdd-supply = <&reg_camera_dvdd>;
            };
        };
    };
};
语法要点:
  • 根节点:/ 表示设备树的根,所有设备节点都挂载在根下。
  • 节点名称:格式为 node-name@unit-address,其中 unit-address 通常对应设备基地址或 I2C 从机地址。
  • 属性值:可以是字符串、32 位整数数组、64 位整数数组、二进制数据或字符串列表。
  • 标签(label):如 camera_sensor:,用于在其他节点中通过 &camera_sensor 引用。
设备树节点层次结构图 / (根节点) cpus memory@80000000 soc chosen i2c@ffd00000 camera@3c (camera_sensor) 节点属性示例 (camera@3c) compatible = "vendor,camera-sensor-v1"; reg = <0x3c>; reset-gpios = <&gpio1 7 GPIO_ACTIVE_LOW>; avdd-supply = <&reg_camera_avdd>; clocks = <&clkc 15>; 节点层次关系 根节点 → soc → i2c@ffd00000 → camera@3c 每个节点通过 compatible 属性与驱动匹配,reg 属性描述地址/从机地址。

2.2 节点与属性详解

设备树中的每个节点代表一个硬件设备或总线,属性则描述设备的配置参数。以下表格总结了摄像头驱动开发中最常用的属性及其用途。

属性名称 格式 用途说明 摄像头驱动示例
compatible 字符串列表 驱动匹配标识,内核通过该属性查找对应的 platform_driver "vendor,camera-sensor-v1"
reg 整数数组 描述设备地址(内存基地址、I2C 从机地址等) <0x3c> (I2C 地址)
interrupts 整数数组 描述中断号、触发类型 <GIC_SPI 55 IRQ_TYPE_EDGE_RISING>
gpios / *-gpios GPIO 描述符 描述 GPIO 引脚、标志(高/低有效) reset-gpios = <&gpio1 7 GPIO_ACTIVE_LOW>
clocks 时钟句柄数组 引用时钟控制器和时钟 ID <&clkc 15>
*-supply 电源句柄 引用 Regulator 节点,描述电源域 avdd-supply = <&reg_camera_avdd>
status 字符串 设备状态:"okay"、"disabled"、"fail" status = "okay";
#address-cells / #size-cells 整数 定义子节点中 reg 属性的地址和长度字段宽度 #address-cells = <1>; #size-cells = <0>;
常见陷阱:
  • compatible 顺序:内核优先匹配列表中的第一个字符串,建议将最具体的型号放在前面,通用型号放在后面。
  • reg 宽度:#address-cells 和 #size-cells 必须在父节点中定义,否则子节点的 reg 解析会出错。
  • GPIO 标志:GPIO_ACTIVE_HIGH (0) 和 GPIO_ACTIVE_LOW (1) 定义在 <dt-bindings/gpio/gpio.h> 中,务必包含头文件。

2.3 compatible 属性:驱动匹配的核心

compatible 属性是设备树与驱动之间建立联系的唯一标识。当内核遍历设备树节点时,会将该节点的 compatible 值与已注册的 platform_driver 中的 of_match_table 进行匹配。

// 驱动端匹配表示例
static const struct of_device_id camera_sensor_of_match[] = {
    { .compatible = "vendor,camera-sensor-v1", .data = &sensor_v1_config },
    { .compatible = "vendor,camera-sensor-v2", .data = &sensor_v2_config },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, camera_sensor_of_match);

static struct platform_driver camera_sensor_driver = {
    .probe  = camera_sensor_probe,
    .remove = camera_sensor_remove,
    .driver = {
        .name = "camera_sensor",
        .of_match_table = camera_sensor_of_match,
    },
};
module_platform_driver(camera_sensor_driver);
匹配流程:
  1. 内核解析 .dtb,为每个节点创建 struct device_node。
  2. 总线(platform_bus)注册驱动时,遍历所有节点,调用 of_match_node() 比较 compatible 字符串。
  3. 匹配成功后,内核调用驱动的 probe() 函数,并将 struct device_node 传递给驱动。
  4. 驱动通过 of_property_read_* 系列 API 读取节点中的其他属性(reg、gpios、supply 等)。

2.4 reg 属性:地址空间描述

reg 属性用于描述设备在总线上的地址范围。对于内存映射设备,它表示基地址和长度;对于 I2C/SPI 设备,它表示从机地址。其格式由父节点的 #address-cells 和 #size-cells 决定。

总线类型 #address-cells #size-cells reg 示例 含义
内存映射 (Memory-mapped) 1 1 <0xffd00000 0x1000> 基地址 0xffd00000,长度 0x1000 (4KB)
I2C 1 0 <0x3c> I2C 从机地址 0x3c
SPI 1 0 <0x0> SPI 片选编号 0
64 位地址 2 1 <0x0 0xffd00000 0x0 0x1000> 高 32 位 + 低 32 位地址,长度 64 位
// 驱动中读取 reg 属性的典型方式
struct resource *res;
void __iomem *base;

// 方式1:使用 platform_get_resource (推荐)
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(&pdev->dev, res);

// 方式2:使用 of 函数直接读取
u32 addr, size;
of_property_read_u32_index(np, "reg", 0, &addr);
of_property_read_u32_index(np, "reg", 1, &size);

2.5 中断描述

设备树使用 interrupts 属性描述设备的中断资源,同时需要 interrupt-parent 属性指定中断控制器。对于摄像头驱动,通常使用 interrupts-extended 来显式指定中断控制器,避免依赖父节点继承。

// 标准中断描述
camera_sensor: camera@3c {
    compatible = "vendor,camera-sensor-v1";
    reg = <0x3c>;
    interrupt-parent = <&gic>;          // 中断控制器
    interrupts = <GIC_SPI 55 IRQ_TYPE_EDGE_RISING>;
    // GIC_SPI: 共享外设中断, 55: 中断号, IRQ_TYPE_EDGE_RISING: 上升沿触发
};

// 使用 interrupts-extended 显式指定
camera_sensor: camera@3c {
    compatible = "vendor,camera-sensor-v1";
    reg = <0x3c>;
    interrupts-extended = <&gic GIC_SPI 55 IRQ_TYPE_EDGE_RISING>;
};
中断类型宏定义(位于 <dt-bindings/interrupt-controller/irq.h>):
  • IRQ_TYPE_EDGE_RISING (1) — 上升沿触发
  • IRQ_TYPE_EDGE_FALLING (2) — 下降沿触发
  • IRQ_TYPE_LEVEL_HIGH (4) — 高电平触发
  • IRQ_TYPE_LEVEL_LOW (8) — 低电平触发
摄像头传感器通常使用 IRQ_TYPE_EDGE_RISING 或 IRQ_TYPE_LEVEL_HIGH 来报告帧同步信号。

2.6 GPIO 描述

GPIO 在设备树中使用 *-gpios 属性描述,格式为 <&gpio_controller pin_number flags>。摄像头驱动中常见的 GPIO 包括复位引脚、电源使能引脚、帧同步中断引脚等。

// 摄像头传感器 GPIO 描述
camera_sensor: camera@3c {
    compatible = "vendor,camera-sensor-v1";
    reg = <0x3c>;

    // 复位引脚:GPIO1 第7脚,低电平有效
    reset-gpios = <&gpio1 7 GPIO_ACTIVE_LOW>;

    // 电源使能引脚:GPIO2 第5脚,高电平有效
    power-gpios = <&gpio2 5 GPIO_ACTIVE_HIGH>;

    // 帧同步中断引脚:GPIO3 第2脚,上升沿触发
    vsync-gpios = <&gpio3 2 GPIO_ACTIVE_HIGH>;
};
// 驱动中获取 GPIO 并操作
#include <linux/gpio/consumer.h>

struct gpio_desc *reset_gpio, *power_gpio;

// 获取 GPIO 描述符
reset_gpio = devm_gpiod_get(&pdev->dev, "reset", GPIOD_OUT_HIGH);
power_gpio = devm_gpiod_get(&pdev->dev, "power", GPIOD_OUT_LOW);

if (IS_ERR(reset_gpio) || IS_ERR(power_gpio)) {
    dev_err(&pdev->dev, "Failed to get GPIOs\n");
    return PTR_ERR(reset_gpio);
}

// 操作 GPIO:拉低复位引脚(低有效)
gpiod_set_value_cansleep(reset_gpio, 1);  // 输出低电平(复位)
msleep(10);
gpiod_set_value_cansleep(reset_gpio, 0);  // 输出高电平(释放复位)

// 操作 GPIO:使能电源
gpiod_set_value_cansleep(power_gpio, 1);  // 输出高电平(使能)
GPIO 标志注意事项:
  • GPIO_ACTIVE_HIGH (0):物理高电平表示“有效”状态。
  • GPIO_ACTIVE_LOW (1):物理低电平表示“有效”状态。
  • 驱动中使用 gpiod_set_value() 时,传入 1 表示“有效”状态,0 表示“无效”状态,GPIO 子系统会自动根据标志反转电平。
  • 例如:reset-gpios 为 GPIO_ACTIVE_LOW,调用 gpiod_set_value(reset_gpio, 1) 会输出物理低电平。
GPIO 与中断描述流程 camera@3c (设备树节点) GPIO 属性 reset-gpios power-gpios vsync-gpios standby-gpios 中断属性 interrupt-parent interrupts interrupts-extended 电源属性 avdd-supply dvdd-supply iovdd-supply 驱动解析流程 1. of_match_node() 匹配 compatible → 调用 probe() 2. devm_gpiod_get() 解析 *-gpios → 申请 GPIO 3. platform_get_irq() 解析 interrupts → 注册中断处理函数 4. regulator_get() 解析 *-supply → 控制电源 5. of_iomap() 解析 reg → 映射寄存器地址 关键 API 映射 GPIO: devm_gpiod_get(dev, "reset", GPIOD_OUT_HIGH) 中断: platform_get_irq(pdev, 0) → devm_request_threaded_irq() Regulator: devm_regulator_get(dev, "avdd") 寄存器: devm_platform_ioremap_resource(pdev, 0)

2.7 设备树包含与复用

实际项目中,设备树通常通过 #include 机制复用公共定义,避免重复编写。摄像头驱动开发中常用的包含文件包括 GPIO 标志、中断类型、时钟定义等。

// camera-sensor.dtsi (设备树包含文件)
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>
#include <dt-bindings/clock/sun8i-a33-ccu.h>

/ {
    reg_camera_avdd: regulator-camera-avdd {
        compatible = "regulator-fixed";
        regulator-name = "camera_avdd";
        regulator-min-microvolt = <2800000>;
        regulator-max-microvolt = <2800000>;
        regulator-always-on;
    };

    reg_camera_dvdd: regulator-camera-dvdd {
        compatible = "regulator-fixed";
        regulator-name = "camera_dvdd";
        regulator-min-microvolt = <1200000>;
        regulator-max-microvolt = <1200000>;
    };
};

// 主设备树文件 board.dts
#include "camera-sensor.dtsi"

&i2c0 {
    status = "okay";
    camera_sensor: camera@3c {
        compatible = "vendor,camera-sensor-v1";
        reg = <0x3c>;
        reset-gpios = <&pio 7 0 GPIO_ACTIVE_LOW>;  // PH7
        avdd-supply = <&reg_camera_avdd>;
        dvdd-supply = <&reg_camera_dvdd>;
    };
};
最佳实践:
  • 将通用的 Regulator 节点和传感器节点定义放在 .dtsi 文件中,便于多板复用。
  • 板级特定的 GPIO 引脚和电源配置放在主 .dts 文件中,通过 &label 引用覆盖。
  • 使用 status = "disabled" 默认禁用设备,在板级文件中通过 status = "okay" 启用。
  • 所有 GPIO 和中断相关的宏定义必须通过 #include 引入,不要使用魔数。