4. 设备树实战:编写一个简单的I2C设备节点、SPI设备节点、MMIO设备节点

设备树(Device Tree)是嵌入式Linux系统中描述硬件拓扑结构的核心机制。本章通过三个实战案例,深入讲解I2C、SPI和MMIO设备节点的编写规范、属性含义及与驱动程序的绑定关系。掌握这些基础节点类型,是构建复杂设备树和实现驱动兼容设计的起点。

实战目标:独立编写一个包含I2C温度传感器、SPI Flash存储器、MMIO GPIO控制器的设备树源文件(.dts),并理解每个节点如何与对应的内核驱动匹配。

4.1 I2C设备节点实战

I2C(Inter-Integrated Circuit)是一种同步、多主从、串行通信总线,广泛用于连接低速外设。在设备树中,I2C设备节点作为其控制器节点的子节点存在。

4.1.1 节点结构与关键属性

一个标准的I2C设备节点必须包含以下属性:

<>否
属性 含义 示例值 是否必须
compatible 设备兼容字符串,用于驱动匹配 "ti,tmp102" 是
reg I2C从设备地址(7位或10位) <0x48> 是
interrupt-parent 中断控制器父节点phandle &gpio1 否
interrupts 中断号与触发类型 <5 IRQ_TYPE_EDGE_FALLING> 否
status 设备状态(okay/disabled) "okay"

4.1.2 实战案例:TMP102温度传感器

以下是一个完整的I2C温度传感器节点定义,挂载在I2C控制器i2c0下:

/* I2C控制器节点 */
&i2c0 {
    status = "okay";
    clock-frequency = <100000>;  /* 100kHz标准模式 */
    pinctrl-names = "default";
    pinctrl-0 = <&i2c0_pins>;

    /* TMP102温度传感器子节点 */
    tmp102: temperature-sensor@48 {
        compatible = "ti,tmp102";
        reg = <0x48>;
        interrupt-parent = <&gpio1>;
        interrupts = <15 IRQ_TYPE_EDGE_FALLING>;
        #thermal-sensor-cells = <0>;
        status = "okay";
    };
};
关键点:I2C地址0x48是7位地址,在设备树中直接写入。驱动通过of_i2c_register_devices()解析子节点,并调用i2c_new_client_device()创建设备。compatible字符串必须与驱动中of_match_table完全匹配。

4.1.3 I2C节点与驱动匹配流程

下图展示了设备树I2C节点与内核驱动之间的匹配过程:

I2C设备节点与驱动匹配流程 设备树节点 compatible = "ti,tmp102" reg = <0x48> 解析 OF核心层 of_i2c_register_devices() 创建i2c_client 匹配 I2C驱动 of_match_table probe()回调 匹配规则: 1. 驱动注册时,将of_match_table中的compatible字符串与设备树节点比较。 2. 匹配成功后,内核调用驱动的probe()函数,传入i2c_client结构体。 3. 驱动通过client->dev.of_node获取设备树中的自定义属性。

4.2 SPI设备节点实战

SPI(Serial Peripheral Interface)是一种高速、全双工、同步串行总线,通常用于Flash、ADC、显示控制器等设备。SPI设备节点同样作为控制器节点的子节点存在,但属性与I2C有所不同。

4.2.1 SPI节点特有属性

属性 含义 示例值 说明
reg 片选(CS)线编号 <0> 对应硬件CS0
spi-max-frequency 设备支持的最大SPI时钟频率 <50000000> 单位Hz,50MHz
spi-cpol 时钟极性(无值属性) 存在即表示CPOL=1 可选
spi-cpha 时钟相位(无值属性) 存在即表示CPHA=1 可选
spi-cs-high 片选高电平有效 存在即表示高有效 默认低有效

4.2.2 实战案例:W25Q128 SPI Flash

以下是一个SPI NOR Flash节点定义,挂载在SPI控制器spi0下:

/* SPI控制器节点 */
&spi0 {
    status = "okay";
    num-cs = <2>;               /* 支持2个片选 */
    cs-gpios = <&gpio0 10 GPIO_ACTIVE_LOW>,
               <&gpio0 11 GPIO_ACTIVE_LOW>;

    /* W25Q128 SPI Flash子节点 */
    flash: flash@0 {
        compatible = "winbond,w25q128", "jedec,spi-nor";
        reg = <0>;               /* 使用CS0 */
        spi-max-frequency = <50000000>;
        spi-cpol;                /* 时钟极性=1 */
        spi-cpha;                /* 时钟相位=1 */
        #address-cells = <1>;
        #size-cells = <1>;

        /* Flash分区定义 */
        partition@0 {
            label = "bootloader";
            reg = <0x00000000 0x00100000>;  /* 1MB */
        };
        partition@1 {
            label = "kernel";
            reg = <0x00100000 0x00500000>;  /* 5MB */
        };
        partition@2 {
            label = "rootfs";
            reg = <0x00600000 0x00A00000>;  /* 10MB */
        };
    };
};
注意事项:SPI设备的compatible通常包含两个字符串:第一个是具体型号(如"winbond,w25q128"),第二个是通用兼容类型(如"jedec,spi-nor")。内核驱动匹配时优先使用第一个,失败后尝试第二个。这种设计提高了设备树的兼容性和复用性。

4.2.3 SPI节点与驱动匹配流程

SPI设备节点与驱动匹配流程 SPI设备树节点 compatible = "winbond,w25q128" reg = <0>; spi-max-frequency SPI核心层 of_register_spi_devices() 创建spi_device SPI驱动 of_match_table probe()回调 SPI特有匹配机制: 1. SPI控制器驱动解析cs-gpios属性,为每个片选分配GPIO。 2. spi-max-frequency用于配置控制器时钟分频器。 3. spi-cpol/spi-cpha决定SPI模式(0/1/2/3),驱动通过spi->mode读取。

4.3 MMIO设备节点实战

MMIO(Memory-Mapped I/O)设备通过物理地址空间直接映射到CPU的内存访问域,是最常见的设备类型。设备树中MMIO节点通常包含reg属性描述地址范围,以及interrupts属性描述中断资源。

4.3.1 MMIO节点核心属性

属性 含义 格式 示例
reg 寄存器地址与大小 <地址 大小> <0x02000000 0x1000>
interrupts 中断号与触发类型 <中断号 触发类型> <31 IRQ_TYPE_LEVEL_HIGH>
clocks 时钟源phandle <&clk_gate> <&clk_uart>
resets 复位控制器phandle <&rst_ctrl 0> 可选

4.3.2 实战案例:GPIO控制器

以下是一个简单的GPIO控制器节点,基于MMIO方式访问寄存器:

/* GPIO控制器节点 */
gpio0: gpio@02000000 {
    compatible = "vendor,gpio-controller";
    reg = <0x02000000 0x1000>;          /* 4KB寄存器空间 */
    interrupts = <31 IRQ_TYPE_LEVEL_HIGH>;
    interrupt-controller;
    #interrupt-cells = <2>;             /* 中断类型+触发方式 */
    gpio-controller;
    #gpio-cells = <2>;                  /* GPIO号+标志 */
    ngpios = <32>;                      /* 32个GPIO引脚 */
    gpio-ranges = <&pinctrl 0 0 32>;   /* 引脚复用映射 */
    clocks = <&clk_gpio>;
    status = "okay";
};

4.3.3 MMIO节点地址映射与驱动访问

驱动通过devm_platform_ioremap_resource()将设备树中的reg地址映射到内核虚拟地址空间:

static int gpio_probe(struct platform_device *pdev)
{
    struct resource *res;
    void __iomem *base;

    /* 获取并映射reg属性中的第一个地址范围 */
    base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(base))
        return PTR_ERR(base);

    /* 获取中断号 */
    int irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;

    /* 读取GPIO方向寄存器(假设偏移0x00) */
    u32 dir = readl(base + 0x00);

    /* 设置GPIO输出值(假设偏移0x04) */
    writel(0x1, base + 0x04);

    return 0;
}

static const struct of_device_id gpio_of_match[] = {
    { .compatible = "vendor,gpio-controller" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, gpio_of_match);

static struct platform_driver gpio_driver = {
    .probe = gpio_probe,
    .driver = {
        .name = "vendor-gpio",
        .of_match_table = gpio_of_match,
    },
};
module_platform_driver(gpio_driver);
MMIO驱动要点:使用devm_platform_ioremap_resource()自动处理资源管理和内存释放。中断号通过platform_get_irq()获取,无需手动解析设备树。驱动通过of_match_table与设备树节点的compatible属性匹配。

4.3.4 MMIO设备节点与驱动匹配流程

MMIO设备节点与驱动匹配流程 MMIO设备树节点 compatible = "vendor,gpio" reg = <0x02000000 0x1000> 平台总线(platform bus) of_platform_populate() 创建platform_device 平台驱动 of_match_table匹配 probe()回调 驱动probe()内部资源获取: 1. devm_platform_ioremap_resource(pdev, 0) → 映射reg[0]地址范围到虚拟地址 2. platform_get_irq(pdev, 0) → 获取interrupts[0]中断号 3. devm_clk_get(&pdev->dev, NULL) → 获取clocks属性指定的时钟 4. device_property_read_u32() → 读取自定义属性(如ngpios)

4.4 三种设备节点对比总结

对比维度 I2C设备节点 SPI设备节点 MMIO设备节点
总线类型 I2C总线(i2c_bus_type) SPI总线(spi_bus_type) 平台总线(platform_bus_type)
reg属性含义 I2C从设备地址 片选线编号 物理地址与大小
设备创建函数 of_i2c_register_devices() of_register_spi_devices() of_platform_populate()
驱动资源获取 i2c_client结构体 spi_device结构体 platform_get_resource()
典型设备 传感器、RTC、EEPROM Flash、ADC、显示屏 GPIO、UART、DMA控制器
地址空间 逻辑地址(7/10位) 逻辑编号(CS0/CS1) 物理地址(MMIO)

4.5 实战避坑指南

常见错误与解决方案:
  • compatible字符串不匹配:驱动中的of_match_table必须与设备树中的compatible完全一致,包括大小写和厂商前缀。
  • I2C地址错误:7位地址左对齐写入,不要包含读写位(最低位)。例如设备地址0x90(8位)对应设备树中的0x48(7位)。
  • SPI模式配置遗漏:未设置spi-cpol/spi-cpha时,内核默认使用SPI模式0(CPOL=0, CPHA=0),可能导致通信失败。
  • MMIO地址范围不足:reg中的size必须覆盖设备所有寄存器,通常为4KB对齐。过小会导致ioremap失败或访问越界。
  • 中断号配置错误:中断号是硬件中断号,不是IRQ号。需要参考SoC数据手册,并确认interrupt-parent指向正确的中断控制器。

4.6 本章小结

本章通过三个实战案例,详细讲解了I2C、SPI和MMIO设备节点的编写方法。核心要点包括:

  • I2C节点以reg表示从设备地址,驱动通过i2c_client访问设备。
  • SPI节点以reg表示片选编号,通过spi-max-frequency和模式属性配置通信参数。
  • MMIO节点以reg表示物理地址范围,驱动通过devm_platform_ioremap_resource()映射寄存器。
  • 所有节点都依赖compatible属性与驱动of_match_table匹配。

掌握这三种基础节点类型,为后续学习复杂设备树(如时钟树、中断控制器、DMA控制器)和ACPI驱动兼容设计打下坚实基础。