3、设备树常用属性详解:compatible、reg、interrupts、clocks、status 等核心属性的含义与用法

设备树(Device Tree)是嵌入式 Linux 驱动与硬件平台之间的“契约文档”。驱动通过解析设备树节点中的属性来获取硬件资源信息,从而避免硬编码。本章将深入解析五个最核心的设备树属性:compatible、reg、interrupts、clocks 和 status,并结合实际案例说明其用法与排查要点。

排查要点:设备树属性解析失败是驱动移植中最常见的故障源。属性名拼写错误、格式不匹配、索引越界等问题,往往导致驱动 probe 失败或设备无法正常工作。

3.1 compatible 属性:驱动匹配的“身份证”

compatible 属性是设备树节点与驱动之间建立关联的核心纽带。它定义了一个或多个字符串,用于标识设备的具体型号和兼容性。

3.1.1 属性格式与匹配规则

规则项 说明
格式 compatible = "厂商,设备型号", "通用型号";
匹配顺序 驱动优先匹配第一个字符串;若失败,则依次尝试后续字符串
命名规范 厂商名使用小写字母,设备型号使用小写字母和数字,用逗号分隔
通配符 不支持通配符,必须精确匹配
// 设备树节点示例
i2c@1c2a000 {
    compatible = "allwinner,sun8i-h3-i2c", "allwinner,sun6i-a31-i2c";
    reg = <0x01c2a000 0x400>;
    #address-cells = <1>;
    #size-cells = <0>;
    
    eeprom@50 {
        compatible = "atmel,24c02";
        reg = <0x50>;
    };
};

// 驱动端匹配表
static const struct of_device_id sun6i_i2c_of_match[] = {
    { .compatible = "allwinner,sun8i-h3-i2c", .data = &sun8i_h3_i2c_data },
    { .compatible = "allwinner,sun6i-a31-i2c", .data = &sun6i_a31_i2c_data },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, sun6i_i2c_of_match);
常见错误:驱动端 of_match_table 中的 compatible 字符串与设备树中的字符串必须完全一致(包括大小写和标点)。一个常见的陷阱是设备树中使用了下划线而驱动中使用了连字符,或反之。

3.1.2 多级兼容与 fallback 机制

当同一 IP 核被多个 SoC 使用时,通常将最具体的型号放在第一位,将通用型号放在后面作为 fallback。驱动可以针对具体型号做优化,同时保留对通用型号的兼容性。

// 设备树:具体型号 + 通用型号
compatible = "ti,am335x-uart", "ns16550a";

// 驱动匹配表:可以同时匹配具体型号和通用型号
static const struct of_device_id ns16550_of_match[] = {
    { .compatible = "ti,am335x-uart", .data = &am335x_uart_data },
    { .compatible = "ns16550a", .data = &ns16550a_data },
    { /* sentinel */ }
};

3.2 reg 属性:硬件地址空间的“地图”

reg 属性描述了设备在系统总线上的地址空间,包括起始地址和长度。它是驱动获取 MMIO(内存映射 I/O)基地址的主要来源。

3.2.1 地址与大小编码规则

reg 属性的解析依赖于父节点的 #address-cells 和 #size-cells 属性:

父节点属性 含义 典型值
#address-cells 地址字段占用的 32 位单元数 1(32位地址)或 2(64位地址)
#size-cells 大小字段占用的 32 位单元数 0(无大小)或 1(32位大小)
// 32位地址空间示例
soc {
    #address-cells = <1>;
    #size-cells = <1>;
    
    uart@1c28000 {
        reg = <0x01c28000 0x400>;  // 起始地址 0x01c28000,大小 0x400
    };
};

// 64位地址空间示例
pcie@40000000 {
    #address-cells = <3>;
    #size-cells = <2>;
    
    pcie-ep@0,0 {
        reg = <0x0000 0x0000 0x0000 0x0 0x100000>;  // 3个地址单元 + 2个大小单元
    };
};

3.2.2 多段 reg 与索引访问

一个设备可能拥有多个独立的地址空间段,例如控制寄存器区域和 DMA 描述符区域。驱动通过索引来访问不同的段。

// 设备树:两段 reg
dma@1c02000 {
    compatible = "allwinner,sun8i-h3-dma";
    reg = <0x01c02000 0x1000>,  // 控制寄存器段 (index 0)
          <0x01c03000 0x1000>;  // 描述符段 (index 1)
};

// 驱动端访问
struct resource *res;
void __iomem *base0, *base1;

// 获取第0段
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base0 = devm_ioremap_resource(&pdev->dev, res);

// 获取第1段
res = platform_get_resource(pdev, IORESOURCE_MEM, 1);
base1 = devm_ioremap_resource(&pdev->dev, res);
排查技巧:当驱动报“can't request region”错误时,检查设备树中 reg 的地址范围是否与其他设备重叠,或者地址值是否超出了 SoC 的物理地址空间。

3.3 interrupts 属性:中断资源的“信号线”

interrupts 属性描述了设备使用的中断资源,包括中断号、触发类型和优先级等信息。它依赖于中断控制器(interrupt-controller)的解析规则。

3.3.1 中断属性格式

属性 说明 示例
interrupts 中断描述列表,格式由父中断控制器定义 <0 29 4>
interrupt-parent 指定该设备连接的中断控制器 phandle <&gic>
interrupt-names 为每个中断指定名称,便于驱动按名获取 "rx", "tx", "error"
// GIC 中断控制器定义
gic: interrupt-controller@1c81000 {
    compatible = "arm,gic-400";
    reg = <0x01c81000 0x1000>,
          <0x01c82000 0x2000>;
    interrupt-controller;
    #interrupt-cells = <3>;  // 格式: <中断类型 中断号 触发类型>
};

// 设备使用中断
uart@1c28000 {
    compatible = "ns16550a";
    reg = <0x01c28000 0x400>;
    interrupts = <0 29 4>;  // SPI 中断,中断号 29,高电平触发
    interrupt-parent = <&gic>;
};

// 驱动端获取中断
int irq_num;
irq_num = platform_get_irq(pdev, 0);  // 获取第0个中断号
if (irq_num < 0) {
    dev_err(&pdev->dev, "failed to get IRQ\n");
    return irq_num;
}

// 使用中断名称获取
int rx_irq = platform_get_irq_byname(pdev, "rx");

3.3.2 中断触发类型编码

触发类型 编码值 说明
上升沿触发 1 IRQF_TRIGGER_RISING
下降沿触发 2 IRQF_TRIGGER_FALLING
高电平触发 4 IRQF_TRIGGER_HIGH
低电平触发 8 IRQF_TRIGGER_LOW

3.4 clocks 属性:时钟资源的“电源开关”

clocks 属性描述了设备所需的时钟源,包括时钟控制器 phandle 和时钟索引。驱动通过时钟框架(CCF)来使能、关闭或调整时钟频率。

3.4.1 时钟属性结构

// 时钟控制器定义
ccu: clock-controller@1c20000 {
    compatible = "allwinner,sun8i-h3-ccu";
    reg = <0x01c20000 0x400>;
    #clock-cells = <1>;  // 1个时钟索引单元
};

// 设备使用时钟
mmc@1c0f000 {
    compatible = "allwinner,sun8i-h3-mmc";
    reg = <0x01c0f000 0x1000>;
    clocks = <&ccu CLK_BUS_MMC0>, <&ccu CLK_MMC0>;
    clock-names = "ahb", "mmc";
};

// 驱动端获取时钟
struct clk *ahb_clk, *mmc_clk;

ahb_clk = devm_clk_get(&pdev->dev, "ahb");
if (IS_ERR(ahb_clk))
    return PTR_ERR(ahb_clk);

mmc_clk = devm_clk_get(&pdev->dev, "mmc");
if (IS_ERR(mmc_clk))
    return PTR_ERR(mmc_clk);

// 使能时钟
clk_prepare_enable(ahb_clk);
clk_prepare_enable(mmc_clk);
最佳实践:始终使用 clock-names 为每个时钟命名,而不是依赖索引。这使设备树更具可读性,且驱动代码更健壮。

3.5 status 属性:设备的“开关”

status 属性控制设备节点的启用状态,是设备树覆盖(overlay)和板级适配中最常用的属性之一。

3.5.1 状态值定义

状态值 含义 说明
"okay" 设备启用 驱动可以正常 probe
"disabled" 设备禁用 驱动不会 probe 该设备
"reserved" 保留状态 资源被保留,其他设备不可用
"fail" 设备故障 硬件检测到故障,驱动不应使用
// SoC dtsi 中默认禁用
uart1: serial@1c28400 {
    compatible = "ns16550a";
    reg = <0x01c28400 0x400>;
    interrupts = <0 30 4>;
    status = "disabled";
};

// 板级 dts 中启用
&uart1 {
    pinctrl-0 = <&uart1_pins>;
    pinctrl-names = "default";
    status = "okay";
};
排查要点:如果驱动 probe 函数没有被调用,首先检查设备树中该节点的 status 是否为 "okay"。许多 SoC 的 dtsi 文件默认将外设节点设为 "disabled",需要在板级 dts 中显式启用。

3.6 核心属性综合关系图

下图展示了设备树节点与驱动之间的核心属性交互关系:

设备树节点与驱动核心属性交互关系 设备树节点 compatible = "厂商,设备" reg = <地址 大小> interrupts = <类型 号 触发> clocks = <&ccu 索引> status = "okay" pinctrl-0 = <&pins> 驱动 probe 流程 of_match_table 匹配 platform_get_resource() platform_get_irq() devm_clk_get() of_device_is_available() pinctrl_bind_pins() compatible 匹配 reg → MMIO 基址 interrupts → IRQ 号 clocks → 时钟句柄 status → 可用性检查 驱动移植排查要点 1. 检查 compatible 字符串是否与驱动 of_match_table 完全一致 2. 检查 reg 地址范围是否与其他设备重叠,且 #address-cells/#size-cells 配置正确 3. 检查 interrupts 的触发类型是否与硬件实际连接一致 属性解析失败常见错误码 -EPROBE_DEFER: 依赖资源(时钟、中断控制器)未就绪,驱动需延迟重试 -EINVAL: 属性格式错误或缺少必要属性 -ENXIO: 地址资源冲突或中断号无效

3.7 属性排查速查表

属性 驱动获取 API 常见错误 排查方向
compatible of_match_device() 字符串不匹配 检查大小写、下划线/连字符、厂商前缀
reg platform_get_resource() 地址冲突、越界 检查 #address-cells/#size-cells、地址重叠
interrupts platform_get_irq() 中断号无效、触发类型不匹配 检查 interrupt-parent、#interrupt-cells
clocks devm_clk_get() 时钟未使能、索引错误 检查 clock-names、时钟控制器 #clock-cells
status of_device_is_available() 设备未启用 检查 status 是否为 "okay"

3.8 综合案例:UART 设备树适配

以下是一个完整的 UART 设备树节点及其驱动适配示例,展示了上述属性的综合应用:

// 设备树节点 (dts)
uart0: serial@1c28000 {
    compatible = "ns16550a";
    reg = <0x01c28000 0x400>;
    interrupts = <0 29 4>;
    interrupt-parent = <&gic>;
    clocks = <&ccu CLK_BUS_UART0>, <&ccu CLK_UART0>;
    clock-names = "apb", "uart";
    pinctrl-0 = <&uart0_pins>;
    pinctrl-names = "default";
    status = "disabled";
};

// 板级启用
&uart0 {
    status = "okay";
};

// 驱动 probe 函数片段
static int ns16550_probe(struct platform_device *pdev)
{
    struct resource *res;
    void __iomem *base;
    int irq;
    struct clk *apb_clk, *uart_clk;
    
    // 1. 获取 MMIO 基址
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(base))
        return PTR_ERR(base);
    
    // 2. 获取中断号
    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;
    
    // 3. 获取时钟
    apb_clk = devm_clk_get(&pdev->dev, "apb");
    if (IS_ERR(apb_clk))
        return PTR_ERR(apb_clk);
    
    uart_clk = devm_clk_get(&pdev->dev, "uart");
    if (IS_ERR(uart_clk))
        return PTR_ERR(uart_clk);
    
    // 4. 使能时钟
    clk_prepare_enable(apb_clk);
    clk_prepare_enable(uart_clk);
    
    // 5. 注册中断处理
    devm_request_irq(&pdev->dev, irq, uart_isr, 0, "ns16550", port);
    
    // 6. 注册串口驱动
    uart_add_one_port(&ns16550_driver, port);
    
    return 0;
}
工程建议:在驱动移植过程中,建议先在设备树中添加 status = "okay" 并确保所有依赖属性完整,然后通过 devicetree 文件系统(/proc/device-tree)或 dtc 工具验证设备树编译是否正确。使用 dev_dbg() 打印关键资源值,可以快速定位属性解析失败的位置。