Linux驱动-设备树概念及实战

发布于:2025-02-25 ⋅ 阅读:(11) ⋅ 点赞:(0)
一、设备树的核心作用
  1. 硬件抽象描述
    用树状结构描述硬件拓扑,替代传统硬编码的board-*.c文件。

  2. 驱动与硬件解耦
    同一内核可适配不同硬件,通过更换.dtb文件实现兼容。

  3. 资源分配与冲突避免
    明确指定外设的寄存器地址、中断号、时钟等资源。

  4. 动态配置支持
    运行时加载不同设备树,无需重新编译内核。


二、实际案例解析
以下代码包含嵌入式系统中常见的硬件配置,包含了 CPU、内存、串口、GPIO 控制器等设备
/dts-v1/;
/ {
    model = "Example Board";
    compatible = "example,board", "generic-armv7";

    #address-cells = <1>;
    #size-cells = <1>;

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

    memory@0 {
        device_type = "memory";
        reg = <0x0 0x40000000>; /* 1GB memory starting at address 0x0 */
    };

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;

        cpu@0 {
            device_type = "cpu";
            reg = <0>;
            compatible = "arm,cortex-a9";
            clock-frequency = <800000000>; /* 800 MHz */
        };
    };

    serial@10000000 {
        compatible = "arm,pl011";
        reg = <0x10000000 0x1000>;
        interrupts = <1 0>;
        clocks = <&clocks 1>;
        clock-names = "uartclk";
        status = "okay";
    };

    gpio@20000000 {
        compatible = "arm,pl061";
        reg = <0x20000000 0x1000>;
        interrupts = <2 0>;
        gpio-controller;
        #gpio-cells = <2>;
        status = "okay";
    };

    clocks {
        #address-cells = <1>;
        #size-cells = <0>;
        compatible = "simple-bus";

        uartclk: clock@1 {
            compatible = "fixed-clock";
            #clock-cells = <0>;
            clock-frequency = <1843200>;
        };
    };
};

代码解释

1. 文件头和根节点
/dts-v1/;
/ {
    model = "Example Board";
    compatible = "example,board", "generic-armv7";
    #address-cells = <1>;
    #size-cells = <1>;
  • /dts-v1/:表示使用设备树版本 1。
  • model:指定设备的型号名称。
  • compatible:是一个字符串列表,用于告诉内核该设备与哪些设备兼容,内核可以根据这个属性来匹配合适的驱动程序。
  • #address-cells 和 #size-cells:用于指定在 reg 属性中地址和大小所占用的单元格数量。
2. chosen 节点
chosen {
    bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait";
};
  • chosen 节点用于传递内核启动参数,bootargs 属性指定了内核启动时的命令行参数。

 3. memory 节点

memory@0 {
    device_type = "memory";
    reg = <0x0 0x40000000>; /* 1GB memory starting at address 0x0 */
};
  • device_type:指定设备类型为内存。
  • reg:指定内存的起始地址和大小,这里表示从地址 0x0 开始的 1GB 内存。

 4. cpus 节点和 cpu 子节点

cpus {
    #address-cells = <1>;
    #size-cells = <0>;

    cpu@0 {
        device_type = "cpu";
        reg = <0>;
        compatible = "arm,cortex-a9";
        clock-frequency = <800000000>; /* 800 MHz */
    };
};
  • cpus 节点是 CPU 设备的容器节点。
  • cpu@0 表示第一个 CPU,compatible 属性指定了 CPU 的型号,clock-frequency 指定了 CPU 的时钟频率。
5. serial 节点
serial@10000000 {
    compatible = "arm,pl011";
    reg = <0x10000000 0x1000>;
    interrupts = <1 0>;
    clocks = <&clocks 1>;
    clock-names = "uartclk";
    status = "okay";
};
  • compatible:指定串口设备的兼容型号为 arm,pl011
  • reg:指定串口设备的寄存器基地址和大小。
  • interrupts:指定串口设备的中断号。
  • clocks:指定串口设备使用的时钟源。
  • status:表示设备的状态,"okay" 表示设备可用。
6. gpio 节点
gpio@20000000 {
    compatible = "arm,pl061";
    reg = <0x20000000 0x1000>;
    interrupts = <2 0>;
    gpio-controller;
    #gpio-cells = <2>;
    status = "okay";
};
  • gpio-controller:表示该设备是一个 GPIO 控制器。
  • #gpio-cells:指定在使用 GPIO 时需要的参数数量。
7. clocks 节点和 clock 子节点
clocks {
    #address-cells = <1>;
    #size-cells = <0>;
    compatible = "simple-bus";

    uartclk: clock@1 {
        compatible = "fixed-clock";
        #clock-cells = <0>;
        clock-frequency = <1843200>;
    };
};
  • clocks 节点是时钟设备的容器节点。
  • uartclk 是一个固定频率的时钟源,clock-frequency 指定了时钟频率。

编译设备树文件

要将 .dts 文件编译成 .dtb 文件(设备树二进制文件),可以使用 dtc(设备树编译器)工具,命令如下:

dtc -I dts -O dtb -o example.dtb example.dts

其中,-I dts 表示输入文件格式为 .dts-O dtb 表示输出文件格式为 .dtb-o example.dtb 指定输出文件名,example.dts 是输入的设备树源文件。

案例1:GPIO按键驱动
1.1 设备树定义
// 按键节点定义
gpio-keys {
    compatible = "gpio-keys";  // 匹配内核驱动
    #address-cells = <1>;
    #size-cells = <0>;

    button@1 {
        label = "USER Button";
        linux,code = <KEY_ENTER>;  // 对应键盘的ENTER键
        gpios = <&gpio1 18 GPIO_ACTIVE_LOW>; // GPIO1_IO18,低电平触发
        debounce-interval = <50>;  // 消抖时间50ms
    };
};

1.2 驱动中解析设备树

// 驱动代码片段
static int gpio_keys_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct gpio_keys_button *buttons;
    struct gpio_keys_platform_data *pdata;

    // 解析设备树中的按键数量
    int nbuttons = of_get_child_count(dev->of_node);
    
    // 分配内存存储按键配置
    buttons = devm_kcalloc(dev, nbuttons, sizeof(*buttons), GFP_KERNEL);
    
    // 遍历子节点
    struct device_node *child;
    int i = 0;
    for_each_child_of_node(dev->of_node, child) {
        u32 code;
        of_property_read_u32(child, "linux,code", &code); // 读取键值
        buttons[i].code = code;
        
        // 获取GPIO编号
        buttons[i].gpio = of_get_gpio(child, 0);
        // 注册中断
        irq = gpio_to_irq(buttons[i].gpio);
        request_irq(irq, button_isr, IRQF_TRIGGER_FALLING, "gpio-key", NULL);
        i++;
    }
    
    // 注册输入设备
    input_register_device(input_dev);
}
案例2:I2C温度传感器(LM75)
2.1 设备树定义
&i2c1 {
    clock-frequency = <100000>;  // I2C速率100kHz
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_i2c1>;
    status = "okay";

    lm75@48 {
        compatible = "nxp,lm75";  // 匹配内核驱动drivers/hwmon/lm75.c
        reg = <0x48>;             // I2C设备地址
        vsupply = <&reg_3v3>;     // 电源依赖
    };
};

2.2 驱动中获取设备树参数

// 驱动代码片段
static int lm75_probe(struct i2c_client *client)
{
    struct device *dev = &client->dev;
    struct device_node *np = dev->of_node;
    
    // 读取设备树中的电源配置
    struct regulator *vcc;
    vcc = devm_regulator_get(dev, "vsupply");
    regulator_enable(vcc);  // 开启传感器电源
    
    // 获取I2C地址
    u8 i2c_addr = client->addr;  // 0x48
    
    // 初始化传感器
    i2c_smbus_write_byte_data(client, LM75_REG_CONF, 0x00);
}
案例3:内存映射型外设(UART)
3.1 设备树定义
uart1: serial@02020000 {
    compatible = "fsl,imx6ul-uart";  // 匹配驱动drivers/tty/serial/imx.c
    reg = <0x02020000 0x4000>;       // 寄存器物理地址范围
    interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;  // 中断号
    clocks = <&clks IMX6UL_CLK_UART1_IPG>,  // 时钟依赖
             <&clks IMX6UL_CLK_UART1_SERIAL>;
    clock-names = "ipg", "per";
    status = "okay";
};

3.2 驱动中映射寄存器

// 驱动代码片段
static int imx_uart_probe(struct platform_device *pdev)
{
    struct resource *res;
    void __iomem *base;
    
    // 获取寄存器物理地址
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    
    // 映射到内核虚拟地址
    base = devm_ioremap_resource(&pdev->dev, res);
    
    // 配置波特率(从设备树或默认值)
    u32 baudrate;
    if (of_property_read_u32(pdev->dev.of_node, "current-speed", &baudrate))
        baudrate = 115200;
    
    // 写入寄存器
    writel(baud_reg, base + UART_UBIR); 
}
三、设备树调试技巧
  1. 查看设备树节点

    # 树形结构查看
    dtc -I fs /proc/device-tree
    
    # 查看特定属性值
    hexdump -C /proc/device-tree/soc/i2c@021a0000/clock-frequency
    

    2. 内核调试日志

    dmesg | grep -i "of_"  # 查看设备树解析过程
    

    3. 验证驱动匹配

    # 检查设备与驱动是否匹配成功
    cat /sys/kernel/debug/devices_deferred
    


网站公告

今日签到

点亮在社区的每一天
去签到