设备树是是Linux中一种用于描述硬件配置的数据结构,它在系统启动时提供给内核,以便内核能够识别和配置硬件资源。设备树在嵌入式Linux系统中尤其重要,因为这些系统通常不具备标准的硬件配置,需要根据实际的硬件配置来动态配置内核。在Linux中,设备树源文件的扩展名为.dts
,其二进制编码文件为.dtb
,将.dts
编译成.dtb
需要使用DTC工具,位于Linux内核的scripts/dtc
文件夹下
基于 ARM 架构的 SOC 有很多种,一种 SOC 又可以制作出很多款板子,每个板子都有一个对应的 DTS 文件,那么如何确定编译哪一个 DTS 文件呢?我们就以 I.MX6ULL 这款芯片对应的板子为例来看一下,打开 arch/arm/boot/dts/Makefile
// 从381行开始
dtb-$(CONFIG_SOC_IMX6UL) += \
imx6ul-14x14-ddr3-arm2.dtb \
imx6ul-14x14-ddr3-arm2-emmc.dtb \
....
dtb-$(CONFIG_SOC_IMX6ULL) += \
imx6ull-14x14-ddr3-arm2-emmc.dtb \
imx6ull-14x14-ddr3-arm2-flexcan2.dtb \
imx6ull-14x14-ddr3-arm2-gpmi-weim.dtb \
imx6ull-14x14-ddr3-arm2-wm8958.dtb \
imx6ull-14x14-evk.dtb \
imx6ull-14x14-evk-btwifi.dtb \
imx6ull-14x14-evk-emmc.dtb \
imx6ull-14x14-evk-gpmi-weim.dtb \
当选中 I.MX6ULL 这个 SOC 以后(CONFIG_SOC_IMX6ULL=y)
,所有使用到IMX6ULL 这个 SOC 的板子对应的.dts
文件都会被编译为.dtb
。如果使用了这一款SOC搓了一块板子,那么我们只需要新建一个该板子对应的.dts
,然后将对应的.dtb
文件名添加到dtb-$(CONFIG_SOC_IMX6ULL)
下,这样在使用make编译设备树的时候就会将对应的.dts
编译为二进制的.dtb
一般dtb文件会和根文件目录以及uboot一同烧录进启动存储设备中。(待补充)
如何编写DTS
虽然我们基本上不会从头到尾重写一个.dts 文件,大多时候是直接在 SOC 厂商提供的.dts文件上进行修改。但是我们肯定需要修改.dts文件,因此DTS 文件语法我们还是要学习一遍
.dtsi头文件
和 C 语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi
。一般.dtsi 文件用于描述 SOC 的内部外设信息,比如 CPU 架构、主频、外设寄存器地址范围,比如 UART、IIC 等等。设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键—值对。比如我们使用的正点原子imx6ull使用的设备树是imx6ull-alientek-emmc.dts
,而imx6ull-alientek-emmc.dts
里又include了imx6ull.dtsi
,以下是从imx6ull.dtsi
文件中缩减出来的设备树文件内容:
/ {
aliases {
can0 = &flexcan1;
};
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>;
};
};
intc: interrupt-controller@00a01000 {
compatible = "arm,cortex-a7-gic";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0x00a01000 0x1000>,
<0x00a02000 0x100>;
};
}
第 1 行,“/”是根节点,每个设备树文件只有一个根节点。如果有多个dts或dsti,那么这几个设备树文件的根节点会合并为一个根节点。aliases、cpus 和 intc 是三个子节点。
在设备树中节点命名格式为:label:node-name@unit-address
,其中“label”是节点标签,主要是方便访问,“node-name”是节点名字,为 ASCII 字符串,“unit-address”一般表示设备的地址或寄存器首地址,如果没有则可以省去,nodename和unit-addr共同构成了节点名字。intc: interrupt-controller@00a01000
就是这种命名格式,而label的存在使得使用&cpu0
就可以访问cpu@0这个节点
每个节点都有不同属性,不同的属性又有不同的内容,属性都是键值对,值可以为空或任意的字节流。设备树源码中有如下几种数据形式:
1.字符串
compatible = “arm,cortex-a7”;
2.32位无符号整数,可以是一组值也可以是单个值
reg = <0 0x123456 100>;
3.字符串列表,使用‘,’分隔字符串
compatible = “fsl,imx6ull-gpmi-nand”, “fsl, imx6ul-gpmi-nand”;
标准属性
节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以自定义属性。除了用户自定义属性,有很多属性是标准属性,Linux 下的很多外设驱动都会使用这些标准属性
1. compatible 属性
compatible 属性也叫做“兼容性”属性,compatible 属性的值是一个字符串列表,这个属性的作用是告诉内核,当前的设备节点应该由哪个驱动来处理。内核会根据compatible属性中列出的字符串,去查找与之匹配的驱动程序。如果找到了匹配的驱动,内核就会尝试加载并初始化该驱动,以便操作相应的硬件设备,其格式如下:
compatible = “manufacturer,model”
其中 manufacturer 表示厂商,一般是芯片制造厂商,model 一般是模块对应的驱动名字,比如 imx6ull-alientek-emmc.dts 中 sound 节点是 I.MX6U-ALPHA 开发板的音频设备节点,I.MX6U-ALPHA 开发板上的音频芯片采用的欧胜出品的 WM8960,sound 节点的 compatible 属性值如下:
compatible = “fsl,imx6ul-evk-wm8960”,“fsl,imx-audio-wm8960”;
其中fsl表示厂商是飞思卡尔,“imx6ul-evk-wm8960”是驱动模块的名字,这个属性会遍历字符串列表中符合的驱动程序名称,然后再Linux内核中查找该驱动程序,如果第一个没有则第二个,第二个没有则第三个,直到找到位置。
在内核中,驱动程序通常会提供一个匹配表,这个表中列出了该驱动支持的所有compatible字符串。当内核解析设备树时,它会检查每个设备节点的compatible属性,并与驱动程序的匹配表进行对比,以确定是否应该由该驱动程序来处理该设备节点,比如在文件 imx-wm8960.c 中有如下内容
// 从632行开始
static const struct of_device_id imx_wm8960_dt_ids[] = {
{ .compatible = "fsl,imx-audio-wm8960", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, imx_wm8960_dt_ids);
static struct platform_driver imx_wm8960_driver = {
.driver = {
.name = "imx-wm8960",
.pm = &snd_soc_pm_ops,
.of_match_table = imx_wm8960_dt_ids,
},
.probe = imx_wm8960_probe,
.remove = imx_wm8960_remove,
};
imx_wm8960_dt_idss 就是 imx-wm8960.c 这个驱动文件的匹配表,此匹配表只有一个匹配值“fsl,imx-audio-wm8960”,可以和imx6ull-alientek的dts中sound节点中的compatible 属性的字符串相匹配,那么这个节点就会使用此驱动文件。
2. model属性
model 属性值也是一个字符串,一般 model 属性描述设备模块信息,比如名字什么的,比
如:
model = “wm8960-audio”;
3.status 属性
status 属性看名字就知道是和设备状态有关的,status 属性值也是字符串,字符串是设备的状态信息,可选的状态如表:
值 | 描述 |
---|---|
“okay” | 表明设备是可操作的。 |
“disabled” | 表明设备当前是不可操作的,但是在未来可以变为可操作的,比如热插拔设备插入以后。至于 disabled 的具体含义还要看设备的绑定文档。 |
“fail” | 表明设备不可操作,设备检测到了一系列的错误,而且设备也不大可能变得可操作。 |
“fail-sss” | 含义和“fail”相同,后面的 sss 部分是检测到的错误内容 |
4.#address-cells 和#size-cells 属性
这两个属性的值都是无符号 32 位整形,#address-cells
和#size-cells
这两个属性可以用在任何拥有子节点的设备中,用于描述子节点的地址信息。#address-cells
属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位),#size-cells
属性值决定了子节点 reg 属性中长度信息所占的字长(32 位)。#address-cells
和#size-cells
表明了子节点应该如何编写 reg 属性值,一般 reg 属性都是和地址有关的内容,格式如下:
reg = <address1 length1 address2 length2 address3 length3……>
每个“address length”组合表示一个子节点设备或其寄存器的地址范围,其中 address 是起始地址,length 是地址长度,addressx表示第x个设备的起始地址,lengthx表示第x个设备的长度,当#address-cells=<1>的时候表示reg中的每一个address的长度是1字长,同样,#size-cells=<1>
表示每个length的长度是1字长。
这部分看不懂没关系,后面会有详解
5.reg 属性
reg 属性的值一般是(address,length)对。reg 属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息
6.ranges属性
ranges 是一个地址映射/转换表,ranges 属性每个项目由子地址、父地址和地址空间长度
这三部分组成,目的是将:
- child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells 确定此物理地址所占用的字长。
- parent-bus-address:父总线地址空间的物理地址,同样由父节点的#address-cells 确定此物理地址所占用的字长。
- length:子地址空间的长度,由父节点的#size-cells 确定此地址长度所占用的字长
如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换。以ranges = <0x0 0xe0000000 0x00100000>;
为例,此属性值指定了一个 1024KB(0x00100000)的地址范围子,地址空间的物理起始地址为 0x0,父地址空间的物理起始地址为 0xe0000000,range会将子空间映射到父空间中,这样,我们在写程序的时候就直接以0x0为起点进行编程就好,地址映射会自动映射到父空间的对应位置
特殊属性
chosen属性
aliases属性
设备树的工作方式
在DTS文件中,每个节点都有 compatible 属性,但根节点 compatible 属性值得拿出来单独说一下,通过根节点的 compatible 属性可以知道我们所使用的设备,一般第一个值描述了所使用的硬件设备名字,第二个值描述了设备所使用的 SOC。我们一般会将设备树和uboot一起烧录到启动设备中,uboot在引导Linux内核启动时,会将设备树dtb文件的首地址传给Linux内核,Linux内核检查设备树的根节点compatible 就可以获知当前设备的信息
以IMX6ULL设备为例子,其设备树arch/arm/mach-imx/mach-imx6ul.c
的根节点信息如下:
/ {
model = "Freescale i.MX6 ULL 14x14 EVK Board";
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
Linux内核中存储着其支持的板子和芯片的信息,Linux内核都用MACHINE_START
和MACHINE_END
来定义一个machine_desc
结构体来描述这个设备,Linux内核会获取设备树文件dtb的根节点compatible 属性,然后遍历它支持的machine_desc
结构体,用于确认自己是否支持当前设备。该结构体位于arch/arm/include/asm/mach/arch.h
,源码如下
#define DT_MACHINE_START(_name, _namestr) \
static const struct machine_desc __mach_desc_##_name \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = ~0, \
.name = _namestr,
#endif
可以看到,要实现该架构体需要两个参数:以IMX6ULL设备为例子,这个设备的machine_desc
结构体实现位于文件 arch/arm/mach-imx/mach-imx6ul.c
中
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
machine_desc
结构体中有个.dt_compat 成员变量,此成员变量保存着本设备兼容属性。只要某个设备(板子)根节点“/”的 compatible 属性值与imx6ul_dt_compat 表中的任何一个值相等,那么就表示 Linux 内核支持此设备。而mach-imx6ul.c
的根节点的compatible属性中的"fsl,imx6ull"
显然与之匹配,那么Linux就可以确认该设备树dtb是自己支持的,从而将设备树加载进来。
Linux内核设备树与machine_desc匹配详解
挖个坑,后面补