Linux设备树
设备树的产生是为了解决内核源码的arch/arm目录下代码混乱和臃肿的问题(过去每个厂商出个板子就要提供外设硬件和平台硬件信息,这些信息以.c和.h文件的形式呈现)。在使用设备树之后,就使得每个硬件平台的硬件资源仅需要一个设备树文件来描述了,而不用在内核源码的arch/arm下以.c 或 .h 文件来定义。Linux内核则在启动过程中,通过解析设备树中的硬件资源来初始化某个具体的平台。
DTS、DTB、DTC三者的关系
DTS是设备树源码文件;
DTB是DTS编译后得到的二进制文件;
DTC则是将 .dts 文件编译成 .dtb 文件的编译工具。
编译DTS文件
在内核源码的 arch/arm/boot/dts/ 目录下的 Makefile 文件中添加 DTS 文件的信息,即将文件名添加进去,添加位置如下图。(先确定SOC的类型,我的是IMX6ULL,再写入板子的名字,名字是自定义的)
然后回到内核源码根目录,执行如下命令进行编译
make dtbs
或者
make all
设备树的头文件
设备树可以用 .h 文件作为头文件,并且它也有另一种头文件,即 .dtsi 文件,并且都是用 #include 来引用的。
一般 .dtsi 文件用于描述 SOC 的内部外设信息。
示例:在 imx6ull-alientek-emmc.dts 文件中就引用了如下两个头文件。
在 imx6ull.dtsi 文件中的描述内容(简化后只留下树节点)如下:
#include <dt-bindings/clock/imx6ul-clock.h>
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/arm-gic.h>
#include "imx6ull-pinfunc.h"
#include "imx6ull-pinfunc-snvs.h"
#include "skeleton.dtsi"
/ {
aliases {};
cpus
{
cpu0: cpu@0 {};
};
intc: interrupt-controller@00a01000 {};
clocks
{
ckil: clock@0 {};
osc: clock@1 {};
ipp_di0: clock@2 {};
ipp_di1: clock@3 {};
};
soc
{
busfreq {};
pmu {};
ocrams: sram@00900000 {};
ocrams_ddr: sram@00904000 {};
ocram: sram@00905000 {};
dma_apbh: dma-apbh@01804000 {};
gpmi: gpmi-nand@01806000{};
aips1: aips-bus@02000000
{
spba-bus@02000000
{
spdif: spdif@02004000 {};
ecspi1: ecspi@02008000 {};
ecspi2: ecspi@0200c000 {};
ecspi3: ecspi@02010000 {};
ecspi4: ecspi@02014000 {};
uart7: serial@02018000 {};
uart1: serial@02020000 {};
esai: esai@02024000 {};
sai1: sai@02028000 {};
sai2: sai@0202c000 {};
sai3: sai@02030000 {};
asrc: asrc@02034000 {};
};
tsc: tsc@02040000 {};
pwm1: pwm@02080000 {};
pwm2: pwm@02084000 {};
pwm3: pwm@02088000 {};
pwm4: pwm@0208c000 {};
flexcan1: can@02090000 {};
flexcan2: can@02094000 {};
gpt1: gpt@02098000 {};
gpio1: gpio@0209c000 {};
gpio2: gpio@020a0000 {};
gpio3: gpio@020a4000 {};
gpio4: gpio@020a8000 {};
gpio5: gpio@020ac000 {};
snvslp: snvs@020b0000 {};
fec2: ethernet@020b4000 {};
kpp: kpp@020b8000 {};
wdog1: wdog@020bc000 {};
wdog2: wdog@020c0000 {};
clks: ccm@020c4000 {};
anatop: anatop@020c8000
{
reg_3p0: regulator-3p0@120 {};
reg_arm: regulator-vddcore@140 {};
reg_soc: regulator-vddsoc@140 {};
};
usbphy1: usbphy@020c9000 {};
usbphy2: usbphy@020ca000 {};
tempmon: tempmon {};
snvs: snvs@020cc000 {};
snvs_poweroff: snvs-poweroff {};
snvs_pwrkey: snvs-powerkey {};
};
epit1: epit@020d0000 {};
epit2: epit@020d4000 {};
src: src@020d8000 {};
gpc: gpc@020dc000 {};
iomuxc: iomuxc@020e0000 {};
gpr: iomuxc-gpr@020e4000 {};
mqs: mqs {};
gpt2: gpt@020e8000 {};
sdma: sdma@020ec000 {};
pwm5: pwm@020f0000 {};
pwm6: pwm@020f4000 {};
pwm7: pwm@020f8000 {};
pwm8: pwm@020fc000 {};
aips2: aips-bus@02100000
{
usbotg1: usb@02184000 {};
usbotg2: usb@02184200 {};
usbmisc: usbmisc@02184800 {};
fec1: ethernet@02188000 {};
usdhc1: usdhc@02190000 {};
usdhc2: usdhc@02194000 {};
adc1: adc@02198000 {};
i2c1: i2c@021a0000 {};
i2c2: i2c@021a4000 {};
i2c3: i2c@021a8000 {};
romcp@021ac000 {};
mmdc: mmdc@021b0000 {};
weim: weim@021b8000 {};
ocotp: ocotp-ctrl@021bc000 {};
csu: csu@021c0000 {};
csi: csi@021c4000 {};
lcdif: lcdif@021c8000 {};
pxp: pxp@021cc000 {};
qspi: qspi@021e0000 {};
uart2: serial@021e8000 {};
uart3: serial@021ec000 {};
uart4: serial@021f0000 {};
uart5: serial@021f4000 {};
i2c4: i2c@021f8000 {};
uart6: serial@021fc000 {};
};
aips3: aips-bus@02200000
{
dcp: dcp@02280000 {};
rngb: rngb@02284000 {};
uart8: serial@02288000 {};
epdc: epdc@0228c000 {};
iomuxc_snvs: iomuxc-snvs@02290000 {};
snvs_gpr: snvs-gpr@0x02294000 {};
};
};
};
-
aliases 里存放的是设备树给片上外设取的别名;
-
cpus 里存放cpu相关信息,主要是 CPU 的运行频率信息,SOC的运行频率信息,低功耗运行频率信息,时钟信息,时钟名字。
operating-points fsl,soc-operating-points fsl,low-power-run clocks clock-names -
intc 则是存放和中断相关的信息,例如:中断控制器。
-
clocks 存放与时钟相关的描述信息。
-
soc 里存放了各种片上外设的信息,比如 ecspi14、uart18、usbphy12、i2c14 等都在这里面。
-
chosen 子节点:其作用是为了让 uboot 向 Linux 内核传递数据,重点是 bootargs 参数。
在imx6ull-alientek/emmc.dts 中 chosen 节点内容如下所示:(设置输出路径为串口1)chosen { stdout-path = &uart1; };
但是,chosen节点并没有配置 bootargs 参数,这个参数是由 uboot 来完成配置的。
设备树的节点
头文件描述的设备树结构大致如下所示:
设备树从根节点开始向上生长,生长到某一个具体的片上外设为止。每个节点记录了该节点共有的属性,组成一个层次分明的层次网络模型。
节点的属性
-
compatible ,也叫”兼容性“属性,用于绑定设备和驱动,compatible 属性的值格式如下所示:
"manufacturer,model" /*其中 manufacturer 表示厂商,model 一般是模块对应的驱动名字。*/
-
model,该属性值是一个字符串,一般用于描述设备模块信息。
-
status,该属性与设备状态有关。4种状态:okay,disabled,fail,fail-sss。
-
#address-cells 和 #size-cells,这两个属性用于描述子节点的地址信息。#address-cells 属性值决定子节点 reg 属性中地址信息所占的字长(32 位),#size-cells 属性值决定子节点 reg 属性中长度信息所占的字长(32 位)
-
reg,reg 属性的值是(address,length)对,用于描述设备地址空间资源信息,一般是某个外设的寄存器地址范围信息。
-
ranges:ranges 是一个地址映射/转换表,ranges 属性每个项目由子地址、父地址和地址空间长度这三部分组成,该属性也可以为空。
child-bus-address 子总线地址空间的物理地址,由父节点的#address-cells
确定此物理地址所占用的字长。parent-bus-address 父总线地址空间的物理地址,同样由父节点的#address-cells
确定此物理地址所占用的字长。length 子地址空间的长度,由父节点的#size-cells 确定此地址长度所占用的字长。 -
name:用于记录节点名字,但已被弃用,有的老设备树文件可能有
-
device_type:用于描述设备的 FCode,但是设备树没有 FCode。此属性只能用于 cpu 节点或者 memory 节点。
利用设备树进行设备匹配
在文件 arch/arm/include/asm/mach/arch.h 内有如下一个结构体和启动方法
/* machine_desc结构体 */
struct machine_desc {
unsigned int nr; /* architecture number */
const char *name; /* architecture name */
unsigned long atag_offset; /* tagged list (relative) */
const char *const *dt_compat; /* array of device tree
* 'compatible' strings */
unsigned int nr_irqs; /* number of IRQs */
#ifdef CONFIG_ZONE_DMA
phys_addr_t dma_zone_size; /* size of DMA-able area */
#endif
unsigned int video_start; /* start of video RAM */
unsigned int video_end; /* end of video RAM */
unsigned char reserve_lp0 :1; /* never has lp0 */
unsigned char reserve_lp1 :1; /* never has lp1 */
unsigned char reserve_lp2 :1; /* never has lp2 */
enum reboot_mode reboot_mode; /* default restart mode */
unsigned l2c_aux_val; /* L2 cache aux value */
unsigned l2c_aux_mask; /* L2 cache aux mask */
void (*l2c_write_sec)(unsigned long, unsigned);
struct smp_operations *smp; /* SMP operations */
bool (*smp_init)(void);
void (*fixup)(struct tag *, char **);
void (*dt_fixup)(void);
void (*init_meminfo)(void);
void (*reserve)(void);/* reserve mem blocks */
void (*map_io)(void);/* IO mapping function */
void (*init_early)(void);
void (*init_irq)(void);
void (*init_time)(void);
void (*init_machine)(void);
void (*init_late)(void);
#ifdef CONFIG_MULTI_IRQ_HANDLER
void (*handle_irq)(struct pt_regs *);
#endif
void (*restart)(enum reboot_mode, const char *);
};
/* 利用设备树启动Linux内核方法 */
#define DT_MACHINE_START(_name, _namestr) \
static const struct machine_desc __mach_desc_##_name \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = ~0, \
.name = _namestr,
在文件 arch/arm/mach-imx/mach-imx6ul.c 内有如下代码:
大致意思是利用设备树启动的方式启动Linux内核,然后匹配设备的属性,这些属性在该文件中被规定,并且在描述设备树的头文件中也被定义,只要这两者信息相同,则说明设备匹配成功。
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
举例:
在 imx6ull.dtsi 文件中根节点的compatible属性如下:
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
在文件 arch/arm/mach-imx/mach-imx6ul.c 中有如下代码
static const char *imx6ul_dt_compat[] __initconst = {
"fsl,imx6ul",
"fsl,imx6ull",
NULL,
};
系统会对这两者的内容进行比较,只要某个设备(板子)根节点“/”的 compatible 属性值与imx6ul_dt_compat 表中的任何一个值相等,那么就表示 Linux 内核支持此设备。就可以正常启动Linux内核。
设备匹配的过程
这个匹配的过程要用到另外几个文件内的函数:
函数 setup_arch 定义在文件 arch/arm/kernel/setup.c 中
函数 setup_machine_fdt 定义在文件 arch/arm/kernel/devtree.c 中
函数 of_flat_dt_match_machine 定义在文件 drivers/of/fdt.c 中
大概过程如下图所示:(该图来自正点原子)
向节点追加或修改内容
向设备树的某个节点中追加内容需要在 .dts 文件中追加。
举例:如下图,该图内容在 arch/arm/boot/dts/imx6ull-alientek-emmc.dts 文件中,内容是 I2C节点的相关属性,在该节点下有两个子节点:mag3110@0e 和 fxls8471@1e ,分别是飞思卡尔的磁力计和一个线性加速度传感器,它们的通讯协议都是I2C协议,所以追加在I2C节点上。
设备树在系统中的体现
Linux 内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/device/tree 目录下根据节点名字创建不同文件夹。
Linux内核解析DTB文件
流程图来自正点原子
总结:
浅显的说,uboot引导linux内核启动,内核启动后就会加载设备树,让内核与各种硬件外设对接,然后我们编写驱动程序,驱动程序与设备树上的节点对接,从而控制某个开发板上的外设并让其运行起来。