关于设备树的总结


设备树是嵌入式软件工程师绕不开的东西,无论是在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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值