首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >设备树入门:简单介绍设备树

设备树入门:简单介绍设备树

原创
作者头像
武状元
发布于 2026-10-28 21:02:04
发布于 2026-10-28 21:02:04
520
举报

​

设备树在嵌入式 Linux 驱动开发里是一个绕不开的话题。很多人第一次打开 设备树的目录时,会被层层嵌套的 .dtsi 和 .dts 文件弄晕,而厂商已经做好了大量板级适配,我们只需要找到我们需要的设备树,然后编译。本文会把设备树的核心概念、迅为平台的典型文件结构、屏幕与 GPIO 的修改方法,以及编译验证的完整链路串起来,目标是让你看完就能在自己的迅为板子上动手改起来。

一、设备树的文件

设备树源码有两种文本文件:.dts 和 .dtsi。.dtsi 通常是 SoC 厂商提供的芯片级描述,包含 CPU、中断控制器、片上外设控制器等公共信息;.dts 是板级描述,描述具体开发板上的外设、电源、引脚复用等差异内容。.dts 通过 #include 把 .dtsi 包含进来,编译时合并成完整的设备树。使用dtc工具编译后的二进制文件叫 .dtb。Bootloader启动时把 DTB 的地址传给内核,内核读取 DTB 中的 magic 字段确认合法性,再按结构块和字符串块的偏移量解析硬件描述。

在实际开发中,我们需要在板子中找到实际使用的设备树文件,需要在/boot文件夹中使用命令ll -al来查看设备树实际链接的文件。找到后在内核文件中找到这个设备树文件的.dts源码,通过修改源码然后编译,就可以增加相应的设备并获取相应设备的属性。

二、设备树的语法

设备树由节点和属性两种基本元素构成。根节点 / 有且仅有一个,所有硬件信息都挂在根节点之下。在设备树开头可以include文件,include的文件的内容的根节点和这个文件的根节点是一样的。设备树开头需要有版本,一般是 /dts-v1/; 之后跟着根节点 /{ };根节点里面放入我们需要的内容。

节点的基本格式是 label: node-name@unit-address { ... }。label 是标签,方便在其他地方用 &label 引用;node-name 是节点名;unit-address 通常是寄存器基地址。一个 UART 节点可以写成:

代码语言:javascript
复制
uart1: serial@02020000 {
    compatible = "fsl,imx6ul-uart", "fsl,imx6q-uart";
    reg = <0x02020000 0x4000>;
    status = "okay";
};
UART 节点代码
UART 节点代码

属性里最重要的几个:

  • compatible:驱动匹配的依据。格式推荐 "manufacturer,model",内核按顺序匹配,直到找到对应驱动。
  • reg :寄存器地址空间,格式 <基地址 长度>,具体解析方式由父节点的 #address-cells 和 #size-cells 决定。
  • status :"okay" 表示启用,"disabled" 表示禁用。很多 SoC 外设在 .dtsi 里默认是 disabled,需要在板级 .dts 中改为 okay。
  • pinctrl-names 和 pinctrl-0 :引用引脚复用配置节点,是迅为平台上改 GPIO 时绕不开的一对属性。
  • phandle / 标签引用:&gpio1 这种写法在编译时会被 dtc 自动决议为一个唯一的 32 位数字 ID。

子节点的 reg 属性格式不是固定的,它由父节点的 #address-cells 和 #size-cells 决定:前者规定 reg 中地址部分占几个 32 位 cell,后者规定长度部分占几个 cell。

在典型的 i.MX6ULL 设备树中:

代码语言:javascript
复制
/ {
    #address-cells = <1>;
    #size-cells = <1>;

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

        cpu0: cpu@0 {
            reg = <0>;
        };
        cpu1: cpu@1 {
            reg = <1>;
        };
    };

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

        uart1: serial@02020000 {
            reg = <0x02020000 0x4000>;
        };
    };
};

cpus 节点设 #size-cells = <0>,所以两个 CPU 子节点的 reg 只有地址、没有长度,分别是 <0> 和 <1>。soc 节点设 #size-cells = <1>,uart1 的 reg 就变成 <基地址 长度> 两个 cell。

根节点的子节点使用的是 CPU 视角的地址域,所以直接子节点不需要 ranges。但更深层的总线节点(比如 I2C 控制器、外部总线)有自己的地址域,这时就需要 ranges 把子地址映射到父地址。

ranges 的格式由父节点的 #address-cells、子节点的 #address-cells 和父节点的 #size-cells 共同决定,每一项是 <子地址 父地址 长度>。

三、pinctrl子系统的使用方法

pinctrl 的设备树分为消费者(客户端) 和生产者(服务端) 两部分。消费者语法所有平台一致,生产者语法各芯片厂商不同。-1

3.1 消费者:pinctrl-names 与 pinctrl-x

任何需要使用引脚的设备节点,通过两个属性声明需求:

  • pinctrl-names :字符串列表,给每个“状态”起名字。
  • pinctrl-0、pinctrl-1……:每个状态对应的引脚配置 phandle 列表,pinctrl-0 对应 pinctrl-names 中的第一个名字,pinctrl-1 对应第二个,以此类推。

最常见的单状态写法:

代码语言:javascript
复制
&uart1 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_uart1>;
};

多状态用于运行中动态切换复用功能。例如一个引脚在“GPIO 模式”和“I2C 模式”之间切换:

代码语言:javascript
复制
my_device {
    pinctrl-names = "gpio_state", "i2c_state";
    pinctrl-0 = <&my_pin_as_gpio>;
    pinctrl-1 = <&my_pin_as_i2c_sda>;
};

驱动在运行时调用 pinctrl_select_state() 传入 "i2c_state",pinctrl 子系统就会把 pinctrl-1 对应的配置写进硬件。一个 pinctrl-x 也可以包含多个 phandle,表示该状态同时配置多组引脚。

3.2 生产者之一:Rockchip 平台(RK3568)的 rockchip,pins

RK3568 的引脚配置节点放在 pinctrl 节点下(通常是 rk3568-pinctrl.dtsi),每个配置是一个子节点,核心属性是 rockchip,pins,每个引脚用四个 cell 描述:

代码语言:javascript
复制
<bank号  bank内引脚号  复用功能号  电气配置phandle>

第一个参数是 GPIO 组(bank),第二个是该组内的引脚编号,第三个是复用功能号,第四个是电气配置(上下拉、驱动强度等)的 phandle。

一个 UART 的配置示例:

代码语言:javascript
复制
&pinctrl {
    uart1 {
        uart1_xfer: uart1-xfer {
            rockchip,pins =
                <2 RK_PB4 2 &pcfg_pull_up>,
                <2 RK_PB5 2 &pcfg_pull_up>;
        };
    };
};

RK_PB4、RK_PB5 是 Rockchip pinctrl 头文件里定义的宏,展开后就是 bank 内引脚号。第三个参数“复用功能号”是写 pinctrl 时最需要查手册的一个:同一个引脚在不同复用号下对应不同功能,比如复用号 1 可能是 PWM,2 可能是 UART。这个号必须查芯片数据手册或用户手册的引脚复用表才能确定,不能猜。第四个参数通常用 SoC dtsi 中预定义好的 pcfg_pull_up、pcfg_pull_none 等 phandle。

rockchip,pins 的父节点(如上面的 uart1 分组节点)只是组织用的容器,真正的 label 挂在包含 rockchip,pins 的那个子节点上(uart1_xfer),消费者的 pinctrl-0 引用的正是这个 label。

四、验证语法是否写对的方法

改完设备树后,可以在 /proc/device-tree/查看自己的设备是否在里面,如果没有在里面,需要查看自己的设备树是否写错了。

如果节点存在但驱动 probe 时报告 pinctrl 相关错误,先用 dmesg | grep pinctrl 看内核日志。常见的错误是 pinctrl-0 引用的 label 在最终 DTS 中不存在,或者 rockchip,pins 的复用号与硬件不匹配。

如果还是看不出来,可以反编译最终 DTB:

代码语言:javascript
复制
dtc -I dtb -O dts -o /tmp/final.dts arch/arm/boot/dts/your_board.dtb

在反编译输出中搜索你改的节点,看 reg、clocks、pinctrl-0 的最终 phandle 是否指向了预期的节点。设备树的 &label 在编译后全部变成数字 phandle,反编译能帮你确认这些数字到底指向谁。

五、总结

设备树语法看起来零散,但拆开看只有两条主线:“地址怎么解释”和“资源怎么引用”。

节点关系决定地址格式。 子节点的 reg 长什么样,不由子节点自己决定,而由父节点的 #address-cells 和 #size-cells 决定。这两个属性只对直接子节点生效,不向下继承,所以每个需要描述地址的总线节点都要自己声明。跨地址域时用 ranges 做映射;板级 .dts 用 &label 重新打开节点追加或覆盖属性,最终生效的是最后一次赋值。

pinctrl 在设备树中采用“生产者声明配置、消费者声明需求”的结构。引脚复用和电气配置内容写在pinctrl 控制器节点下,并给配置节点打上 label;消费者节点只负责通过 pinctrl-x 引用这个 label。

reg、pinctrl-0、clocks 在编译后全部变成数字 phandle,用 dtc -I dtb -O dts 反编译看实际指向,比在多个源文件之间猜要可靠得多。节点存在但驱动不 probe,优先查 compatible 匹配、pinctrl label 是否存在、引脚是否被重复申请、clock 是否拿到。

​

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、设备树的文件
  • 二、设备树的语法
  • 三、pinctrl子系统的使用方法
    • 3.1 消费者:pinctrl-names 与 pinctrl-x
    • 3.2 生产者之一:Rockchip 平台(RK3568)的 rockchip,pins
  • 四、验证语法是否写对的方法
  • 五、总结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档