设备树在嵌入式 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 节点可以写成:
uart1: serial@02020000 {
compatible = "fsl,imx6ul-uart", "fsl,imx6q-uart";
reg = <0x02020000 0x4000>;
status = "okay";
};
属性里最重要的几个:
子节点的 reg 属性格式不是固定的,它由父节点的 #address-cells 和 #size-cells 决定:前者规定 reg 中地址部分占几个 32 位 cell,后者规定长度部分占几个 cell。
在典型的 i.MX6ULL 设备树中:
/ {
#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 的设备树分为消费者(客户端) 和生产者(服务端) 两部分。消费者语法所有平台一致,生产者语法各芯片厂商不同。-1
任何需要使用引脚的设备节点,通过两个属性声明需求:
最常见的单状态写法:
&uart1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart1>;
};多状态用于运行中动态切换复用功能。例如一个引脚在“GPIO 模式”和“I2C 模式”之间切换:
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,表示该状态同时配置多组引脚。
RK3568 的引脚配置节点放在 pinctrl 节点下(通常是 rk3568-pinctrl.dtsi),每个配置是一个子节点,核心属性是 rockchip,pins,每个引脚用四个 cell 描述:
<bank号 bank内引脚号 复用功能号 电气配置phandle>第一个参数是 GPIO 组(bank),第二个是该组内的引脚编号,第三个是复用功能号,第四个是电气配置(上下拉、驱动强度等)的 phandle。
一个 UART 的配置示例:
&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:
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 删除。