设备树

新版本的 Linux 中,ARM 相关的驱动全部采用了设备树。uboot 启动时,加载了 zImage 和 dtb 到 DDR 里。用 bootz 80800000 - 83000000 启动。其中 83000000 存放的就是设备树。

在现在的 linux 源码中,arch/arm/mach-xxxx 或者 arch/arm/plat-xxxx 下的 .c 文件都是用来描述板级的区别的。用 C 语言来描述整个板上的设备信息。

然而,一款 CPU 可以设计出来的开发板太多了,如果每个开发板都用 .c 写死,带来的问题是,如果硬件上的改变,必须要编译整个内核。linux 本身提供的应该是功能上的东西,对于这种板级的描述信息,硬编码进内核是不合适的。这也是 dts 引入 arm 的原因。

设备树是嵌入式 linux 驱动工程师掌握的必备技能。

描述设备树的文件叫做 DTS(Device Tree Source),这个 DTS 文件采用树形结构描述板级设备。设备数的存在也是为了把硬件相关的东西剥离出内核本身。

设备树源文件为 *.dts,编译出来的设备树二进制文件为 *.dtb,编译工具为 dtc 工具。

dtc 工具在 scripts 中,由 gcc 编译器编译出 dtc 工具。

编译设备树的命令为

make dtbs

或者编译指定的设备树

make imx6ull_mini.dtb

arch/arm/boot/dts 里的 makefile 根据架构型号,确定编译对应架构的开发板的设备树。因此,新增开发板对应的设备树,要记得在此文件中新增。

基本语法

/* 注释 */

node_label: node_name@@address{
    string-propty = "a string";
    string-list = "string1", "string2";
    int-property = <202>;
    int-list-property = <0xbeef 123 0xabcd4>;   /* uint32_t */
    mixed-list-property = "string", <0xdeadbeef>, <35>;
    byte-array-property = [0x01 0x55 0xaa];     
};

node_name 是必须的,@address 可选,如果节点是可寻址设备。

i2c1: i2c@021a0000 {
    compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
    reg = <0x021a0000 0x4000>;
};

标签node_label可以看做一个节点的指针,使用 &node_label 即引用此节点。(C语言中取地址)

表示和寻址设备

每个设备在设备树里至少有一个节点。

每个可寻址的设备都有 reg 属性

标准属性

  • compatible 属性,兼容性属性,字符串列表,用于将设备和驱动匹配

  • model 属性,字符串,用于描述设备模块信息

  • status 属性,字符串,设备状态信息

    • "okay" 设备是可操作的

    • "disabled" 表明设备当前是不可操作的,但是在未来可以变为可操作的,比如热插拔设备插入以后。

    • "fail" 表明设备不可操作,设备检测到了一系列的错误,而且设备也不大可能变得可操作

  • #address-cells#size-cells 都是uint32_t 类型的

    • 可以用在任何有子节点的设备中,用于描述子节点的地址信息

    • address-cells 决定了 reg 属性中地址的字长(32bit)

    • size-cells 决定了 reg 属性中长度占用的字长

    • 表明直接点改如何设置 reg 属性值

  • reg 属性,一般为<address length> 格式,描述设备地址空间信息

  • ranges 地址转换表,由子地址、父地址和地址空间长度组成

一些特殊的属性

根节点属性 compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";,匹配驱动,根节点的此属性一般第一个为设备(板子)名字,第二个为SOC名字。内核启动后会查根节点的 compatible 来看是否支持此设备。

arch/arm/mach-imx/mach-imx6ulc. 中有

static const char *imx6ul_dt_compat[] __initconst = {
	"fsl,imx6ul",
	"fsl,imx6ull",
	NULL,
};

DT_MACHINE_START(IMX6UL, "Freescale i.MX6 Ultralite (Device Tree)")
	.map_io		= imx6ul_map_io,
	.init_irq	= imx6ul_init_irq,
	.init_machine	= imx6ul_init_machine,
	.init_late	= imx6ul_init_late,
	.dt_compat	= imx6ul_dt_compat,
MACHINE_END

这样一个结构体 dt_compat,设备树根节点下 compatible 中 "fsl,imx6ull" 值和 .dt_compat 中 "fsl,imx6ull" 值匹配,因此linux内核支持此设备。

如果修改这两边的字符串不相同,那么内核就卡柱无法启动了。


设备树的头文件为 .dtsi,设备树也可以引用 .h 头文件,也可以引用 .dts 文件。在行为上,只是相当于把整个文件的内容复制过去,本质上都是文本文件,后缀只是给人看的。

这个设计也很好理解,设备树不仅仅描述了芯片外面挂的设备,也描述了 CPU 里的信息,因此,这部分是公共的,这些只写一次就好了。imx6ull.dtsi 就描述了 imx6ull 芯片的所有信息。

设备树语法很简单,几乎没啥学习成本,对着 imx6ull 看看就能明白的个大概。

源码在 arch/arm/boot/dts/imx6ull.dtsi 中,

开发板的 dts 中,先去包含一些东西

/dts-v1/;

#include <*.h>
#include "*.dtsi"

/ {
    属性1 = "字符串";
    属性2 = "字符串";

    结点1 {
        属性 = ;
    };

    结点2 {
        属性 = ;
    };
};

&cpu0 {
    arm-supply = <&reg_arm>;
    soc-supply = <&reg_soc>;
    dc-supply = <&reg_gpio_dvfs>;
};

设备树也是由 / 根节点开始,用树的方式描述设备信息。根节点之外,& 来引用前面的结点,修改属性。

在 dtsi 里也有根结点,在这个文件引用的 skeleton.dtsi 里也有根节点,这几个根节点是同一个根节点,里面的内容都会一同拼接起来。设备树里的内容可以在另外一个文件中对一个东西赋值,就修改了设备树里的内容。

三个文件拼接起来的设备树:

/ {
    #address-cells = <1>;       /* skeleton.dtsi */
    #size-cells = <1>;          /* skeleton.dtsi */
    model = "Freescale i.MX6 ULL 14x14 EVK Board";          /* board.dts */
    compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";    /* board.dts */

    chosen {                    /* skeleton.dtsi 定义 */
        stdout-path = &uart1;   /* board.dts新增属性 */
    };

    aliases {                   /* skeleton.dtsi */
        gpio0 = &gpio1;         /* imx6ull.dtsi 新增属性*/
        i2c0 = &i2c1;           /* imx6ull.dtsi 新增属性*/
        mmc0 = &usdhc1;         /* imx6ull.dtsi 新增属性*/
        serial0 = &uart1;       /* imx6ull.dtsi 新增属性*/
        spi0 = &ecspi1;         /* imx6ull.dtsi 新增属性*/
        usbphy0 = &usbphy1;     /* imx6ull.dtsi 新增属性*/
    };

    memory {                    /* skeleton.dtsi */
        device_type = "memory"; /* skeleton.dtsi */
        reg = <0 0>;            /* skeleton.dtsi */
        reg = <0x80000000 0x20000000>;  /* board.dts 修改属性 */
    };

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

	intc: interrupt-controller@00a01000 {

	};

    clocks {

    };

    soc {

    };
};

这里只给出了一级结点。结点的命名也是有要求的,参考 PAPR 文档,完整的命名为 label:node-name@address

后面的地址对于外设来说,一般是起始地址。对于具体的挂载总线上的设备,一般就是设备地址。

一个追加内容的例子,imx6ull 中 soc 节点下的 aips2 结点下的 i2c1

i2c1: i2c@021a0000 {
    #address-cells = <1>;
    #size-cells = <0>;
    compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
    reg = <0x021a0000 0x4000>;
    interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clks IMX6UL_CLK_I2C1>;
    status = "disabled";
}

在不同的开发板上,如果挂了外设,就需要新增子节点,使用引用的方式

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

	mag3110@0e {
		compatible = "fsl,mag3110";
		reg = <0x0e>;
		position = <2>;
	};

	fxls8471@1e {
		compatible = "fsl,fxls8471";
		reg = <0x1e>;
		position = <0>;
		interrupt-parent = <&gpio5>;
		interrupts = <0 8>;
	};
};

可以看出,这块板的 i2c1 上挂了两个芯片。

linux 中的设备树

proc/device-tree 中,能看到设备树的所有东西。结点以文件夹的形式给出来,属性以文件的形式给出。

内核启动时,是知道设备树的地址的,内核解析设备树,然后在 /proc/device-tree 里呈现出来。

可以在根结点下面随便新增一个设备树做测试。

在设备树中添加一个硬件对应的结点时,可以参考文档。

特殊设备结点

/ {
    chosen {
        stdout-path = &uart1;
    };

    aliases {

    };
}

单词 aliases 的意思是“别名”,因此 aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。不过我们一般会在节点命名的时候会加上 label,然后通过&label来访问节点,这样也很方便,而且设备树里面大量的使用&label 的形式来访问节点。

chosen 并不是一个真实的设备,chosen 节点主要是为了 uboot 向 Linux 内核传递数据,重点是 bootargs 参数。一般 .dts 文件中 chosen 节点通常为空或者内容很少,imx6ull-alientek-emmc.dts 中 chosen 节点内容如下所示:

但是进入 /proc/device-tree/chosen 会看到有 bootargs 这个属性。

uboot 将 bootargs 参数传递给 linux 内核,设备树中我们也未设置此参数,显然 uboot 增加了这一属性。

uboot 是知道 dtb 在 DDR 中的位置的,在 uboot 源码的 common/fdt_support.c 中有 fdt_chosen() 函数,此函数调用了 fdt_find_or_add_subnode() 从设备树中找 chosen 结点,没有的话就自己创建一个。然后把 bootargs 用 fdt_setprop() 函数写入设备树。

kernel api

在驱动开发中,需要获得设备树里的属性值。内核提供了许多 api 方便驱动开发,这些函数都放在 include/linux/of.h 中,并且都用 of_ 开头。

内核中,使用一个结构体来描述结点,定义为

struct device_node {
	const char *name;                   /* 节点名字 */
	const char *type;
	phandle phandle;
	const char *full_name;
	struct fwnode_handle fwnode;

	struct	property *properties;
	struct	property *deadprops;	/* removed properties */
	struct	device_node *parent;
	struct	device_node *child;
	struct	device_node *sibling;
	struct	kobject kobj;
	unsigned long _flags;
	void	*data;
#if defined(CONFIG_SPARC)
	const char *path_component_name;
	unsigned int unique_id;
	struct of_irq_controller *irq_trans;
#endif
};

属性也用要给结构体来描述

struct property {
	char	                *name;      /* 属性名字 */
	int	                    length;     /* 属性长度 */
	void	                *value;     /* 属性值 */
	struct property         *next;      /* 下一个属性 */
	unsigned long           _flags;
	unsigned int            unique_id;
	struct bin_attribute    attr;
};

查找结点

/* 通过名字查找 */
struct device_node *of_find_node_by_name(struct device_node *from, const char *name);

struct device_node *of_find_node_by_type(struct device_node *from, const char *type)

struct device_node *of_find_compatible_node(struct device_node *from,
                                            const char *type,
                                            const char *compatible)

struct device_node *of_find_matching_node_and_match(struct device_node  *from,
                                                    const struct of_device_id *matches,
                                                    const struct of_device_id **match)


inline struct device_node *of_find_node_by_path(const char *path)

查找父子结点

struct device_node *of_get_parent(const struct device_node *node)

struct device_node *of_get_next_child(const struct device_node *node,
                                      struct device_node *prev)

提取属性

property *of_find_property( const struct device_node *np,
                            const char *name,
                            int *lenp)

int of_property_count_elems_of_size(const struct device_node *np,
                                    const char *propname,
                                    int elem_size)

参考资料

最后更新于