第4章 设备树(Device Tree)详解

设备树这东西,刚接触时觉得挺玄乎。说白了,它就是一套描述硬件的数据结构。Linux内核通过它才知道「哦,我这块板子上接了哪些外设」。我在早期做嵌入式时,还用过古老的板级文件(board-xxx.c),每次换硬件都要重新编译内核,那叫一个痛苦。设备树解决了这个问题——硬件描述和内核代码分离,改个.dts文件就行。

4.1 设备树语法基础

设备树的源文件是.dts,编译后变成.dtb。内核启动时,bootloader把.dtb传给内核。内核解析这个二进制文件,构建出设备树。

一个最简单的节点长这样:

/dts-v1/;

/ {
    compatible = "acme,coyote";
    #address-cells = <1>;
    #size-cells = <1>;

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

几个关键点:

  • 根节点:用 / 表示,整棵树从这里开始
  • 节点名:格式是 node-name@unit-address,比如 uart@10000000
  • 属性:键值对,值可以是字符串、数字、二进制数据
  • reg:描述地址空间,由 #address-cells 和 #size-cells 决定长度
我的习惯:写.dts时,先把根节点的 #address-cells 和 #size-cells 定好。这两个值一旦写错,后面所有reg解析都会乱套。我一般用1和1,32位地址空间够用了。

4.2 GPIO节点配置

GPIO在设备树里怎么配?我拿一个实际项目中的例子来说。当时做一款工业数据采集器,需要控制8路继电器输出。

&gpio1 {
    gpio-controller;
    #gpio-cells = <2>;
};

/ {
    relay-control {
        compatible = "mycompany,relay-ctrl";
        relay-gpios = <&gpio1 10 GPIO_ACTIVE_HIGH>,
                      <&gpio1 11 GPIO_ACTIVE_HIGH>,
                      <&gpio1 12 GPIO_ACTIVE_HIGH>,
                      <&gpio1 13 GPIO_ACTIVE_HIGH>;
    };
};

这里要注意:

  • gpio-controller 声明这个节点是GPIO控制器
  • #gpio-cells = <2> 表示引用GPIO时需要两个参数:引脚号和电平标志
  • GPIO_ACTIVE_HIGH 定义有效电平,这个宏在 dt-bindings/gpio/gpio.h 里
我曾经踩过的坑:GPIO引脚号搞混了。芯片手册上写的是GPIO1_IO10,但设备树里用的是绝对编号。不同厂商的编号规则不一样,有的从0开始,有的从32开始。一定要看芯片的参考手册确认。

4.3 UART节点配置

UART配置相对标准。大部分SoC的UART都兼容16550。我调试过不少串口设备,配置模板基本一致:

&uart2 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_uart2>;
    current-speed = <115200>;
    uart-has-rtscts;
};

几个关键属性:

属性 说明 常见值
status 使能状态 "okay" 或 "disabled"
pinctrl-0 引脚复用配置 引用pinctrl节点
current-speed 波特率 9600, 115200, 921600
uart-has-rtscts 硬件流控 有则启用

实战经验:工业现场经常遇到串口通信不稳定。我建议把 current-speed 设低一点,比如9600。虽然慢,但抗干扰能力强。另外,uart-has-rtscts 这个属性,如果你的设备不支持硬件流控,千万别加,否则驱动会一直等CTS信号,导致通信卡死。

4.4 I2C节点配置

I2C总线上挂多个从设备,这是工业网关的常见场景。比如挂一个温湿度传感器和一个RTC时钟芯片:

&i2c1 {
    status = "okay";
    clock-frequency = <100000>;
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_i2c1>;

    temp-humidity@44 {
        compatible = "sensirion,sht20";
        reg = <0x44>;
    };

    rtc@68 {
        compatible = "dallas,ds3231";
        reg = <0x68>;
    };
};

配置要点:

  • clock-frequency:I2C时钟频率,标准模式100kHz,快速模式400kHz
  • 每个从设备用 reg 指定7位地址
  • compatible 必须与驱动匹配,格式是 "厂商,型号"
排查技巧:I2C设备不工作?先用 i2cdetect -y 1 扫描总线,看设备地址对不对。我遇到过好几次,芯片手册写的地址是8位格式,但设备树里要用7位,差一位就找不到设备。

4.5 SPI节点配置

SPI在工业网关里常用于连接ADC、DAC或无线模块。配置示例:

&spi2 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_spi2>;
    cs-gpios = <&gpio1 15 GPIO_ACTIVE_LOW>,
               <&gpio1 16 GPIO_ACTIVE_LOW>;

    adc@0 {
        compatible = "ti,ads1256";
        reg = <0>;
        spi-max-frequency = <2000000>;
        spi-cpol;
        spi-cpha;
    };

    rf-module@1 {
        compatible = "semtech,sx1278";
        reg = <1>;
        spi-max-frequency = <10000000>;
    };
};

SPI配置的几个关键点:

  • cs-gpios:片选引脚,可以用GPIO模拟,也可以用硬件片选
  • spi-max-frequency:每个从设备可以单独设置最大频率
  • spi-cpol 和 spi-cpha:定义SPI模式(0/1/2/3)
  • reg:片选编号,从0开始
注意:SPI模式一定要和从设备匹配。我吃过一次亏,把CPOL和CPHA搞反了,数据读出来全是0xFF。用示波器一看,时钟极性和相位都不对。后来老老实实查了芯片手册才搞定。

4.6 设备树覆盖(Overlay)技术

Overlay是什么?简单说就是「打补丁」。你有一个基础设备树,然后动态加载一个overlay文件,修改或添加节点。这在工业网关里特别有用——比如现场需要临时接一个传感器,不用重新编译整个设备树,加载一个overlay就行。

Overlay的写法:

/dts-v1/;
/plugin/;

/ {
    fragment@0 {
        target = &i2c1;
        __overlay__ {
            status = "okay";
            co2-sensor@76 {
                compatible = "sensirion,scd30";
                reg = <0x76>;
            };
        };
    };
};

关键点:

  • 文件头加 /plugin/; 声明这是overlay
  • 用 fragment@N 定义每个修改片段
  • target 指定要修改的节点
  • __overlay__ 里写要添加或覆盖的内容

加载overlay的方法:

# 编译overlay
dtc -@ -I dts -O dtb -o co2-overlay.dtbo co2-overlay.dts

# 加载到内核
mkdir /sys/kernel/config/device-tree/overlays/co2
cat co2-overlay.dtbo > /sys/kernel/config/device-tree/overlays/co2/dtbo

我的建议:Overlay技术非常适合工业现场的设备扩展。但要注意,不是所有内核都开启了CONFIG_OF_OVERLAY。编译内核时一定要确认这个选项打开了。另外,overlay加载后不能随意卸载,搞不好会内核崩溃。我一般在测试环境先验证好,再到现场部署。

4.7 设备树知识体系

下面这张图总结了设备树的核心知识结构:

设备树知识体系 设备树 (DT) 语法基础 外设节点配置 Overlay技术 节点/属性/reg compatible匹配 pinctrl复用 GPIO配置 UART配置 I2C配置 SPI配置 fragment片段 target定位 动态加载/卸载 核心思想:硬件描述与内核代码分离 修改硬件配置只需改.dts文件,无需重新编译内核

4.8 调试与排错

设备树出问题,最常见的现象是驱动不加载。我的排查步骤:

  1. 检查dtb是否加载成功:看内核启动日志,搜索 "Device Tree" 或 "OF:"
  2. 查看设备树内容:ls /proc/device-tree/ 看看节点有没有生成
  3. 检查compatible匹配:cat /proc/device-tree/model 确认板子型号
  4. 验证引脚复用:cat /sys/kernel/debug/gpio 看GPIO状态
  5. 检查地址冲突:I2C和SPI设备地址不能重复
重要提醒:设备树编译错误很隐蔽。少个分号、多个大括号,编译时可能只报个warning,但生成的dtb就是不对。我习惯用 dtc -I dts -O dtb -o test.dtb test.dts 2>&1 | grep -i error 严格检查。另外,make dtbs 时加上 DTC_FLAGS=-@ 才能生成overlay支持。

嗯,设备树这部分内容不少,但掌握了核心语法和配置方法,后面做工业网关集成调试就会顺手很多。记住一点:设备树是硬件和驱动之间的桥梁,桥搭好了,数据才能跑得通。