设备树是嵌入式软件工程师绕不开的东西,无论是在Uboot中还是在linux内核中都经常需要和设备树打交道。以前我也只是了解个皮毛,基本上不对设备树做修改,只是按照指令对其进行编译并打包,工作以后接触公司自研的板子,需要对设备树进行修改,因此写下这篇文章作为学习设备树的总结。
设备树的起源
设备树最早是应用在PowerPC架构中,后来被推广到其他平台,来描述平台的硬件信息。所谓的硬件信息指的就是基地址,中断号,管脚连接,dma等信息。以串口uart为例,在arm架构下不同于x86平台通过端口与外设交互,核心和外设之间的交互通过内存映射IO(Memory Map IO)进行,也就是通过对某个内存地址的读写实现与外设之间的交互。例如,uart控制器在0xC000地址处,0xC000为串口配置寄存器,那么通过写入0xC000这个地址就可以实现串口波特率,数据位奇偶校验等信息的配置。uboot和linux串口相关的驱动程序需要了解0xC000这个基地址才能进行配置,一种简单的解决方法是直接将该地址写入到程序代码中,作为宏定义或者是预定义的结构体这。种方法的缺点就是会使得内核代码体积膨胀,含有了太多与内核代码不相关的硬件信息定义文件,其次就是需要更改这些硬件信息都需要重新编译内核,大大降低了开发速度。因此内核的维护者们引入了设备树实现代码与硬件信息分离。
设备树的语法
在讲解设备树语法之前首先需要明确以下几个名词
- disi 设备树头文件
- dts 设备树源文件
- dtc 设备树编译器
- dtb 设备树二进制文件
设备树的构建方式与C语言类似,共用的部分写在头文件也就是dtsi中,源代码写在dts中,通过编译器dtc将dts编译为二进制文件dtb,在编译过程中dts包含的头文件dtsi一并打包到dtb中。
设备树既然被称为设备树,所以其源码也是以树形结构展开。
/{
adult@30{
name = "zhangsan";
info = "180", "70kg";
code = [0x0, 0x1, 0x2, 0x3];
child@5{
student;
name = "zhangxiaoxiao";
grade = <70>;
teacher = <&ls>;
};
};
ls: adult@25{
name = "lisi";
work = <70 80 90 99>;
};
};
上面的设备树从“/”开始定义了两个节点,从"/"开始定义设备树是设备树的固定语法。adult被称为节点名,@后面跟着的地址被称为单元地址。第二个节点定义了一个标签(label)也就是ls,所以第二个节点可以通过&ls索引到。在每个节点下面定义的东西被称为属性(property),其中属性又分为直接定义的字符串(name),字符串列表(info),使用16进制定义的bytedate(code),布尔属性(student),cell属性(grade,work),phandel属性(teacher)等。
如何读懂一个板子的设备树
下面就以Uboot中RK3588的官方开发板evb7为例讲解如何读懂一个设备树。该开发板的设备树源代码文件为arch/arm/dts/rk3588-evb.dts
// rk3588-evb.dts
#include "rk3588.dtsi"
#include "rk3588-u-boot.dtsi"
#include <dt-bindings/input/input.h>
/ {
model = "Rockchip RK3588 Evaluation Board";
compatible = "rockchip,rk3588-evb", "rockchip,rk3588";
adc-keys {
compatible = "adc-keys";
io-channels = <&saradc 1>;
io-channel-names = "buttons";
keyup-threshold-microvolt = <1800000>;
u-boot,dm-pre-reloc;
status = "okay";
volumeup-key {
u-boot,dm-pre-reloc;
linux,code = <KEY_VOLUMEUP>;
label = "volume up";
press-threshold-microvolt = <1750>;
};
};
};
这个设备树包含了三个头文件,在dts中仍然在根节点下定义了两个属性model和compatible。而在rk3588s.dtsi中
//引用路径如下:
// rk3588.dtsi->rk3588s.dtsi
/ {
compatible = "rockchip,rk3588";
.....
};
可以看出在dtsi中和dts的根节点下都定义了compatible属性,这就引出了设备树的继承规则,如果不同文件中定义了同一个属性那么以最后一个文件定义的为准,同时会把所有文件定义的属性进行融合。也就是说最后的dtb中,compatible以dts为准,同时会补充
model属性到dtb中,也就是说最后编译出的dtb中:
compatible = "rockchip,rk3588-evb", "rockchip,rk3588";
与uboot相关的头文件为rk3588-u-boot.dtsi,在该文件中
// rk3588-u-boot.dtsi
/ {
aliases {
ethernet0 = &gmac0;
ethernet1 = &gmac1;
mmc0 = &sdhci;
mmc1 = &sdmmc0;
mmc2 = &sdmmc1;
};
chosen {
stdout-path = &uart2;
u-boot,spl-boot-order = &sdmmc0, &sdhci, &nandc0, &spi_nand, &spi_nor;
};
};
.....
&uart2 {
clock-frequency = <24000000>;
u-boot,dm-spl;
status = "okay";
};
其中chosen节点用来向内核传递参数,但因为rk3588的启动方式较为特殊,会在uboot后期使用kernel的设备树,所以chosen节点中没有包含bootargs这个属性。uboot设备树中的chosen节点定义了
stdout-path = &uart2;
也就是将输出重定向到uart2,在下面的节点中
// rk3588-u-boot.dtsi
&uart2 {
clock-frequency = <24000000>;
u-boot,dm-spl;
status = "okay";
};
将status属性设置为okay同时设置了clock-frequency,并定义了u-boot和dm-spl两个布尔属性。但是上述对uart2的定义仍然是不完整的,uart2节点下缺少基地址寄存器的定义,中断号irq的定义以及管脚定义。答案就是利用了设备树的继承规则,缺少的定义在rk3588s.dtsi中。在rk3588s.dtsi中
// rk3588s.dtsi
/{
......
uart2: serial@feb50000 {
compatible = "rockchip,rk3588-uart", "snps,dw-apb-uart";
reg = <0x0 0xfeb50000 0x0 0x100>;
interrupts = <GIC_SPI 333 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru SCLK_UART2>, <&cru PCLK_UART2>;
clock-names = "baudclk", "apb_pclk";
reg-shift = <2>;
reg-io-width = <4>;
dmas = <&dmac0 10>, <&dmac0 11>;
pinctrl-names = "default";
pinctrl-0 = <&uart2m0_xfer>;
status = "disabled";
};
......
};
在rk3588.dtsi中的uart2节点上补全了关于uart2的定义,例如reg属性定义了uart2的基地址,0x100则是寄存器偏移。interrupts定义了uart2的中断号,pinctrl定义的是与IO管脚复用相关的属性。值得注意的最后的属性
status = "disabled";
这条属性说明这个uart2没有被使用,而在rk3588-u-boot.dtsi中的uart2节点下将status设置为okay进行了使能。
在设备树中另一个需要关注的属性是compatible,这一条属性与驱动编写有密切关系。
// drivers/serial/ns16550.c
static const struct udevice_id ns16550_serial_ids[] = {
{ .compatible = "ns16550", .data = PORT_NS16550 },
{ .compatible = "ns16550a", .data = PORT_NS16550 },
{ .compatible = "ingenic,jz4780-uart", .data = PORT_JZ4780 },
{ .compatible = "nvidia,tegra20-uart", .data = PORT_NS16550 },
{ .compatible = "snps,dw-apb-uart", .data = PORT_NS16550 },
{ .compatible = "ti,omap2-uart", .data = PORT_NS16550 },
{ .compatible = "ti,omap3-uart", .data = PORT_NS16550 },
{ .compatible = "ti,omap4-uart", .data = PORT_NS16550 },
{ .compatible = "ti,am3352-uart", .data = PORT_NS16550 },
{ .compatible = "ti,am4372-uart", .data = PORT_NS16550 },
{ .compatible = "ti,dra742-uart", .data = PORT_NS16550 },
{}
};
#endif /* OF_CONTROL && !OF_PLATDATA */
/* TODO(sjg@chromium.org): Integrate this into a macro like CONFIG_IS_ENABLED */
#if !defined(CONFIG_TPL_BUILD) || defined(CONFIG_TPL_DM_SERIAL)
U_BOOT_DRIVER(ns16550_serial) = {
.name = "ns16550_serial",
.id = UCLASS_SERIAL,
#if CONFIG_IS_ENABLED(OF_CONTROL) && !CONFIG_IS_ENABLED(OF_PLATDATA)
.of_match = ns16550_serial_ids,
.ofdata_to_platdata = ns16550_serial_ofdata_to_platdata,
.platdata_auto_alloc_size = sizeof(struct ns16550_platdata),
#endif
.priv_auto_alloc_size = sizeof(struct NS16550),
.probe = ns16550_serial_probe,
.ops = &ns16550_serial_ops,
.flags = DM_FLAG_PRE_RELOC,
};
在上面的驱动代码中同样定义了与设备树中相同的compatible 字符串,通过这一条属性实现driver和device的相互匹配。
参考文章
[1] https://bootlin.com/pub/conferences/2021/webinar/petazzoni-device-tree-101/petazzoni-device-tree-101.pdf