在内核源码中,存在大量对板级细节信息描述的代码。这些代码充斥在/arch/arm/plat-xxx和/arch/arm/mach-xxx目录,对内核而言这些platform设备、resource、i2c_board_info、spi_board_info以及各种硬件的platform_data绝大多数纯属垃圾冗余代码。为了解决这一问题,ARM内核版本3.x之后引入了原先在Power PC等其他体系架构已经使用的Flattened Device Tree(设备树)。
设备树的定义:一种描述硬件资源的数据结构,它通过bootloader将硬件资源传给内核,使得内核和硬件资源描述相对独立。
设备树由一系列被命名的节点(Node)和属性(Property)组成,而节点本身可包含子节点。所谓属性,其实就是成对出现的名称和值。设备树可以描述的信息:
注意:设备树对于可热插拔的设备不进行具体描述,它只描述用于控制该热插拔设备的控制器。
设备树的主要优势:对于同一SOC的不同主板,只需更换设备树文件.dtb即可实现不同主板的无差异支持,而无需更换内核文件。
(注意:要使得3.x之后的内核支持使用设备树,除了内核编译时需要打开相对应的选项外,bootloader也需要支持将设备树的数据结构传给内核。)
设备树主要包含DTC(device tree compiler),DTS(device tree source)和DTB(device tree blob)。其对应关系如下图所示:
.dts文件是一种ASCII文本对Device Tree的描述,放置在内核的/arch/arm/boot/dts目录。一般而言,一个.dts文件对应一个ARM的machine。
由于一个SOC可能有多个不同的电路板( .dts文件为板级定义, .dtsi文件为SoC级定义),而每个电路板拥有一个 .dts。这些dts势必会存在许多共同部分,为了减少代码的冗余,设备树将这些共同部分提炼保存在.dtsi文件中,供不同的dts共同使用。.dtsi的使用方法,类似于C语言的头文件,在dts文件中需要进行include .dtsi文件。当然,dtsi本身也支持include 另一个dtsi文件。
.dts的结构:
/{
node1{
a - string - property = "A string";
a - string - list - property = "first string",
"second string";
a - byte - data - property =[0x01 0x23 0x34 0x56];
child - node1
{
first - child - property;
second - child - property = < 1 >;
a - string - property = "Hello, world";
};
child - node2
{
};
};
node2{
an - empty - property;
a - cell - property = < 1 2 3 4 >; /* each number (cell) is a uint32 */
child - node1
{
};
};
};
各节点都有一系列属性。这些属性可能为空,如an-empty-property;可能为字符串如a-string-property;可能为字符串数组,如a-string-list-property;可能为Cells(由u32整数组成),如second-child-property;可能为二进制数,如a-byte-data-property。
DTC为编译工具,dtc编译器可以把dts文件编译成为dtb,也可把dtb编译成为dts文件。在3.x内核版本中,DTC的源码位于内核的scripts/dtc目录,内核选中CONFIG_OF,编译内核的时候,主机可执行程序DTC就会被编译出来。 即scripts/dtc/Makefile中hostprogs-y := dtc always := $(hostprogs-y) 这一hostprogs的编译目标。
在Linux内核的arch/arm/boot/dts/Makefile中,描述了当某种SoC被选中后,哪些.dtb文件会被编译出来,如与VEXPRESS对应的.dtb包括:
dtb-$(CONfiG_ARCH_VEXPRESS) += vexpress-v2p-ca5s.dtb \
vexpress-v2p-ca9.dtb \
vexpress-v2p-ca15-tc1.dtb \
vexpress-v2p-ca15_a7.dtb \
xenvm-4.2.dtb
在Linux下,我们可以单独编译设备树文件。当我们在Linux内核下运行make dtbs时,若我们之前选择了ARCH_VEXPRESS,上述.dtb都会由对应的.dts编译出来,因为arch/arm/Makefile中含有一个.dtbs编译目标项目。
DTC除了可以编译.dts文件以外,其实也可以“反汇编”.dtb文件为.dts文件,其指令格式为:
./scripts/dtc/dtc -I dtb -O dts -o xxx.dts arch/arm/boot/dts/xxx.dtb
DTC编译.dts生成的二进制文件(.dtb),bootloader在引导内核时,会预先读取.dtb到内存,进而由内核解析。
Dts编译生成dtb
./dtc -I dts -O dtb -o B_dtb.dtb A_dts.dts \\把A_dts.dts编译生成B_dtb.dtb
Dtb编译生成dts
./dtc -I dtb -O dts -o A_dts.dts A_dtb.dtb \\把A_dtb.dtb反编译生成为A_dts.dts
对于设备树中的节点和属性具体是如何来描述设备的硬件细节的,一般需要文档来进行讲解,文档的后缀名一般为.txt。在这个.txt文件中,需要描述对应节点的兼容性、必需的属性和可选的属性。
这些文档位于内核的Documentation/devicetree/bindings目录下,其下又分为很多子目录。譬如,Documentation/devicetree/bindings/i2c/i2c-xiic.txt描述了Xilinx的I2C控制器,其内容如下:
Xilinx IIC controller:
Required properties:
- compatible : Must be "xlnx,xps-iic-2.00.a"
- reg : IIC register location and length
- interrupts : IIC controller unterrupt
- #address-cells = <1>
- #size-cells = <0>
Optional properties:
- Child nodes conforming to i2c bus binding
Example:
axi_iic_0: i2c@40800000 {
compatible = "xlnx,xps-iic-2.00.a";
interrupts = < 1 2 >;
reg = < 0x40800000 0x10000 >;
#size-cells = <0>;
#address-cells = <1>;
};
设备树绑定文档的主要内容包括:
Uboot设备从v1.1.3开始支持设备树,其对ARM的支持则是和ARM内核支持设备树同期完成。
为了使能设备树,需要在编译Uboot的时候在config文件中加入:
·#define CONfiG_OF_LIBFDT·
在Uboot中,可以从NAND、SD或者TFTP等任意介质中将.dtb读入内存,假设.dtb放入的内存地址为0x71000000,之后可在Uboot中运行fdt addr命令设置.dtb的地址,如:
·UBoot> fdt addr 0x71000000·
fdt的其他命令就变得可以使用,如fdt resize、fdt print等。
对于ARM来讲,可以通过bootz kernel_addr initrd_address dtb_address的命令来启动内核,即dtb_address作为bootz或者bootm的最后一个参数,第一个参数为内核映像的地址,第二个参数为initrd的地址,若不存在initrd,可以用“-”符号代替。
在.dts文件中,根节点"/"的兼容属性compatible=“acme,coyotes-revenge”;定义了整个系统(设备级别)的名称,它的组织形式为:
Linux内核通过根节点"/"的兼容属性即可判断它启动的是什么设备。这个顶层设备兼容属性一般包括两个或者两个以上的兼容性字符串,首个兼容性字符串是板子级别的名字,后面一个兼容性是芯片级别(或者芯片系列级别)的名字。比如:
compatible = "insignal,origen","samsung,exynos4210","samsung,exynos4";
在Linux 2.6内核中,ARM Linux针对不同的电路板会建立由MACHINE_START和MACHINE_END包围起来的针对这个设备的一系列回调函数:
MACHINE_START(VEXPRESS, "ARM-Versatile Express")
.atag_offset = 0x100,
.smp = smp_ops(vexpress_smp_ops),
.map_io = v2m_map_io,
.init_early = v2m_init_early,
.init_irq = v2m_init_irq,
.timer = &v2m_timer,
.handle_irq = gic_handle_irq,
.init_machine = v2m_init,
.restart = vexpress_restart,
MACHINE_END
这些不同的设备会有不同的MACHINE ID,Uboot在启动Linux内核时会将MACHINE ID存放在r1寄存器,Linux启动时会匹配Bootloader传递的MACHINE ID和MACHINE_START声明的MACHINE ID,然后执行相应设备的一系列初始化函数。
ARM Linux 3.x在引入设备树之后,MACHINE_START变更为DT_MACHINE_START,其中含有一个.dt_compat成员,用于表明相关的设备与.dts中根节点的兼容属性兼容关系。如果Bootloader传递给内核的设备树中根节点的兼容属性出现在某设备的.dt_compat表中,相关的设备就与对应的兼容匹配,从而引发这一设备的一系列初始化函数被执行。
一个典型的DT_MACHINE:
static const char * const v2m_dt_match[] __initconst = {
"arm,vexpress", "xen,xenvm",NULL };
DT_MACHINE_START(VEXPRESS_DT,"ARM-Versatile Express")
.dt_compat = v2m_dt_match,
.smp = smp_ops(vexpress_smp_ops),
.map_io = v2m_dt_map_io,
.init_early = v2m_dt_init_early,
.init_irq = v2m_dt_init_irq,
.timer = &v2m_dt_timer,
.init_machine = v2m_dt_init,
.handle_irq = gic_handle_irq,
.restart = vexpress_restart,
MACHINE_END
Linux倡导针对多个SoC、多个电路板的通用DT设备,即一个DT设备的.dt_compat包含多个电路板.dts文件的根节点兼容属性字符串。之后,如果这多个电路板的初始化序列不一样,可以通过Int of_machine_is_compatible(const char*compat) API判断具体的电路板是什么。在Linux内核中,常常使用如下API来判断根节点的兼容性:
int of_machine_is_compatible(const char *compat);
此API判断目前运行的板子或者SoC的兼容性,它匹配的是设备树根节点下的兼容属性。
在.dts文件的每个设备节点中,都有一个兼容属性,兼容属性用于驱动和设备的绑定。兼容属性是一个字符串的列表,列表中的第一个字符串表征了节点代表的确切设备,形式为"
设备节点的兼容性和根节点的兼容性是类似的,都是“从具体到抽象”。
使用设备树后,驱动需要与.dts中描述的设备节点进行匹配,从而使驱动的probe()函数执行。对于platform_driver而言,需要添加一个OF匹配表,如前文的.dts文件的"acme,a1234-i2c-bus"兼容I2C控制器节点的OF匹配表:
platform设备驱动中的of_match_table:
static const struct of_device_id a1234_i2c_of_match[] =
{
{
.compatible = "acme,a1234-i2c-bus",
},
{
},
};
MODULE_DEVICE_TABLE(of, a1234_i2c_of_match);
static struct platform_driver i2c_a1234_driver =
{
.driver =
{
.name = "a1234-i2c-bus",
.owner = THIS_MODULE,
.of_match_table = a1234_i2c_of_match,
},
.probe = i2c_a1234_probe,
.remove = i2c_a1234_remove,
};
module_platform_driver(i2c_a1234_driver);
一个驱动可以在of_match_table中兼容多个设备,在Linux内核中常常使用如下API来判断具体的设备是什么:
int of_device_is_compatible(const struct device_node *device,const char *compat);
此函数用于判断设备节点的兼容属性是否包含compat指定的字符串。这个API多用于一个驱动支持两个以上设备的时候。
设备树的.dts文件中对于设备节点的命名,遵循的组织形式为
name是一个ASCII字符串,用于描述节点对应的设备类型;
如果一个节点描述的设备有地址,则应该给出@unit-address。多个相同类型设备节点的name可以一样,只要unit-address不同即可。
对于挂在内存空间的设备而言,@字符后跟的一般就是该设备在内存空间的基地址。
还可以给一个设备节点添加label,之后可以通过&label的形式访问这个label,这种引用是通过phandle(pointer handle)进行的。
在以前的内核版本中,内核包含了对硬件的全部描述:
现今的内核版本使用了Device Tree:
可寻址的设备使用如下信息在设备树中编码地址信息:
reg
#address-cells // 子节点reg的address为几个32bit的整型数据
#size-cells // 长度为几个32bit整型数据,如果为0,则没有lenth字段
//其中,reg的组织形式为reg=,其中的每一组address length表明了设备使用的一个地址范围。address为1个或多个32位的整型(即cell),而length的意义则意味着从address到address+length–1的地址范围都属于该节点。若#size-cells=0,则length字段为空。
address和length字段是可变长的,父节点的#address-cells和#size-cells分别决定了子节点reg属性的address和length字段的长度。
设备树中还可以包含中断连接信息,对于中断控制器而言,它提供如下属性:
在整个设备树中,与中断相关的属性还包括:
除了中断以外,在ARM Linux中时钟、GPIO、pinmux都可以通过.dts中的节点和属性进行描述。
pinctrl @ 80018000
{
compatible = "fsl,imx28-pinctrl", "simple-bus";
reg = < 0x80018000 2000 >;
gpio0:gpio @ 0
{
compatible = "fsl,imx28-gpio";
interrupts = < 127 >;
gpio - controller;
#gpio -cells = <2>;
interrupt - controller;
#interrupt -cells = <2>;
};
gpio1:gpio @ 1
{
compatible = "fsl,imx28-gpio";
interrupts = < 126 >;
gpio - controller;
#gpio - cells = <2>;
interrupt - controller;
#interrupt -cells = <2>;
};
...
};
其中,#gpio- cells为2,第1个cell为GPIO号,第2个为GPIO的极性。为0的时候是高电平有效,为1的时候则是低电平有效。
使用GPIO的设备则通过定义命名xxx-gpios属性来引用GPIO控制器的设备节点,如:
sdhci@c8000400 {
status = "okay";
cd-gpios = <&gpio01 0>;
wp-gpios = <&gpio02 0>;
power-gpios = <&gpio03 0>;
bus-width = <4>;
};
而具体的设备驱动则通过类似如下的方法来获取GPIO:
cd_gpio = of_get_named_gpio(np,"cd-gpios", 0);
wp_gpio = of_get_named_gpio(np,"wp-gpios", 0);
power_gpio = of_get_named_gpio(np,"power-gpios", 0);
of_get_named_gpio()这个API的原型如下:
static inline int of_get_named_gpio(struct device_node *np,
const char *propname, int index);
在.dts和设备驱动不关心GPIO名字的情况下,也可以直接通过of_get_gpio()获取GPIO,此函数原型为:
static inline int of_get_gpio(struct device_node *np, int index);
clocks = <&clks 138>, <&clks 140>, <&clks 141>;
clock-names = "uart","general","noc";
而驱动中则使用上述的clock-names属性作为clk_get()或devm_clk_get()的第二个参数来申请时钟,譬如获取第2个时钟:
devm_clk_get(&pdev->dev,"general");
<&clks 138>里的138这个index是与相应时钟驱动中clk的表的顺序对应的,很多开发者也认为这种数字出现在设备树中不太好,因此他们把clk的index作为宏定义到了arch/arm/boot/dts/include/dt-bindings/clock中。譬如include/dt-bindings/clock/imx6qdl-clock.h中存在这样的宏:
#define IMX6QDL_CLK_STEP 16
#define IMX6QDL_CLK_PLL1_SW 17…
#define IMX6QDL_CLK_ARM 104…
而arch/arm/boot/dts/imx6q.dtsi则是这样引用它们的:
clocks = <&clks IMX6QDL_CLK_ARM>, //相当于<&clks 104>
<&clks IMX6QDL_CLK_STEP>, //相当于<&clks 16>
<&clks IMX6QDL_CLK_PLL1_SW>; //相当于<&clks17>
gpio:pinctrl @b0120000
{
#gpio - cells = <2>;
#interrupt -cells = <2>;
compatible= "sirf,atlas6-pinctrl";
…
lcd_16pins_a: lcd0 @ 0
{
lcd
{
sirf, pins = "lcd_16bitsgrp";
sirf, function = "lcd_16bits";
};
};
…
spi0_pins_a: spi0 @ 0
{
spi
{
sirf, pins = "spi0grp";
sirf, function = "spi0";
};
};
spi1_pins_a:spi1 @ 0
{
spi
{
sirf, pins = "spi1grp";
sirf, function = "spi1";
};
};
…
};
而SPI0这个硬件实际上需要用到spi0_pins_a对应的spi0grp这一组引脚,因此在atlas6-evb.dts中通过pinctrl-0引用了它:
spi@b00d0000 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&spi0_pins_a>;
…
};
这个硬件的.dts文件的内容为:
/{
compatible = "acme,coyotes-revenge";
#address -cells = <1>;
#size - cells = <1>;
interrupt - parent = < &intc >;
cpus //双核处理器
{
#address -cells = <1>;
#size - cells = <0>;
cpu @ 0
{
compatible= "arm,cortex-a9";
reg = < 0 >;
};
cpu @ 1
{
compatible= "arm,cortex-a9";
reg = < 1 >;
};
};
serial @ 101f0000 //位于0x101F1000的串口
{
compatible = "arm,pl011";
reg = < 0x101f0000 0x1000 >;
interrupts= < 1 0 >;
};
serial @ 101f2000 //位于0x101F2000的串口
{
compatible= "arm,pl011";
reg = < 0x101f2000 0x1000 >;
interrupts= < 2 0 >;
};
gpio @ 101f3000 //位于0x101F3000的GPIO
{
compatible= "arm,pl061";
reg = < 0x101f3000 0x1000
0x101f4000 0x0010 >;
interrupts= < 3 0 >;
};
intc:interrupt - controller @ 10140000 //位于0x10140000中断控制器
{
compatible= "arm,pl190";
reg = < 0x10140000 0x1000 >;
interrupt - controller;
#interrupt -cells = <2>;
};
spi @ 10115000 //位于0x10170000的SPI
{
compatible= "arm,pl022";
reg = < 0x10115000 0x1000 >;
interrupts= < 4 0 >;
};
external - bus //外部总线桥
{
#address -cells = <2>
#size - cells = <1>;
ranges= < 0 0 0x10100000 0x10000 // Chipselect 1, 以太网地址
1 0 0x10160000 0x10000 // Chipselect 2, i2c 控制器地址
2 0 0x30000000 0x1000000 >; // Chipselect 3, NOR Flash地址
ethernet @ 0, 0
{
compatible= "smc,smc91c111";
reg = < 0 0 0x1000 >;
interrupts= < 5 2 >;
};
i2c @ 1, 0
{
compatible= "acme,a1234-i2c-bus";
#address -cells = <1>;
#size - cells = <0>;
reg = < 1 0 0x1000 >;
interrupts= < 6 2 >;
rtc @ 58
{
compatible= "maxim,ds1338";
reg = < 58 >;
interrupts= < 7 3 >;
};
};
flash @ 2, 0
{
compatible= "samsung,k8f1315ebm", "cfi-flash";
reg = < 2 0 0x4000000 >;
};
};
};
有了设备树后,不再需要大量的板级信息,譬如过去经常在arch/arm/plat-xxx和arch/arm/mach-xxx中实施的一些事情:
static struct resource xxx_resources[] = {
[0] = {
.start = …,
.end = …,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = …,
.end = …,
.flags = IORESOURCE_IRQ,
},
};
static struct platform_device xxx_device = {
.name = "xxx",
.id = -1,
.dev = {
.platform_data = &xxx_data,
},
.resource = xxx_resources,
.num_resources = ARRAY_SIZE(xxx_resources),
};
之类的platform_device代码都不再需要,其中platform_device会由内核自动展开。而这些resource实际来源于.dts中设备节点的reg、interrupts属性。
典型的,大多数总线都与“simple_bus”兼容,而在与SoC对应的设备的.init_machine成员函数中,调用of_platform_bus_probe(NULL,xxx_of_bus_ids,NULL);即可自动展开所有的platform_device。
static struct i2c_board_info __initdata afeb9260_i2c_devices[] = {
{
I2C_BOARD_INFO("tlv320aic23", 0x1a),
}, {
I2C_BOARD_INFO("fm3130", 0x68),
}, {
I2C_BOARD_INFO("24c64", 0x50),
},
};
之类的i2c_board_info代码目前不再需要出现,现在只需要把tlv320aic23、fm3130、24c64这些设备节点填充作为相应的I2C控制器节点的子节点:
i2c@0x1a{
compatible = "acme,a1234-i2c-bus";
…
rtc@1a {
compatible = "maxim,tlv320aic23";
reg = <1a>;
interrupts = < 7 3 >;
};
};
设备树中的I2C客户端会通过在I2C host驱动的probe()函数中调用的of_i2c_register_devices(&i2c_dev->adapter);被自动展开。
而spi_board_info与i2c的变化类似。
多个针对不同电路板的设备,以及相关的回调函数
在过去,ARM Linux针对不同的电路板会建立由MACHINE_START和MACHINE_END包围的设备,引入设备树之后,MACHINE_START变更为DT_MACHINE_START,其中含有一个.dt_compat成员,用于表明相关的设备与.dts中根节点的兼容属性的兼容关系。
采用设备树后,我们可以对多个SoC和板子使用同一个DT_MACHINE和板文件,板子和板子之间的差异更多只是通过不同的.dts文件来体现。
设备与驱动的匹配方式
使用设备树后,驱动需要与在.dts中描述的设备节点进行匹配,从而使驱动的prob()函数执行。新的驱动、设备的匹配变成了设备树节点的兼容属性和设备驱动中的OF匹配表的匹配。
设备的平台数据属性化
在Linux 2.6下,驱动习惯自定义platform_data,在arch/arm/mach-xxx注册platform_device、i2c_board_info、spi_board_info等的时候绑定platform_data,而后驱动通过标准API获取平台数据。
在转移到设备树后,platform_data便不再喜欢放在arch/arm/mach-xxx中了,它需要从设备树的属性中获取。
除了前面接触到的of_machine_is_compatible()、of_device_is_compatible()等常用函数以外,在Linux的BSP和驱动代码中,经常会使用到一些Linux中其他设备树的API,这些API通常被冠以of_前缀,它们的实现代码位于内核的drivers/of目录下:
` struct device_node *of_find_compatible_node(struct device_node *from,const char *type, const char *compatible);`
根据兼容属性,获得设备节点。遍历设备树中的设备节点,看看哪个节点的类型、兼容属性与本函数的输入参数匹配,在大多数情况下,from、type为NULL,则表示遍历了所有节点。
int of_property_read_u8_array(const struct device_node *np,
const char *propname, u8 *out_values, size_t sz);
int of_property_read_u16_array(const struct device_node *np,
const char *propname, u16 *out_values, size_t sz);
int of_property_read_u32_array(const struct device_node *np,
const char *propname, u32 *out_values, size_t sz);
int of_property_read_u64(const struct device_node *np,
const char*propname, u64 *out_value);
读取设备节点np的属性名,为propname,属性类型为8、16、32、64位整型数组。对于32位处理器来讲,最常用的是of_property_read_u32_array()。
除了整型属性外,字符串属性也比较常用,其对应的API包括:
int of_property_read_string(struct device_node *np,
const char*propname,const char **out_string);
int of_property_read_string_index(struct device_node *np,
const char*propname,int index, const char **output);
前者读取字符串属性,后者读取字符串数组属性中的第index个字符串。
除整型、字符串以外的最常用属性类型就是布尔型,其对应的API很简单:
static inline bool of_property_read_bool(const struct device_node *np, const char *propname);
如果设备节点np含有propname属性,则返回true,否则返回false。一般用于检查空属性是否存在。
void __iomem *of_iomap(struct device_node *node, int index);
上述API可以直接通过设备节点进行设备内存区间的ioremap(),index是内存段的索引。若设备节点的reg属性有多段,可通过index标示要ioremap()的是哪一段,在只有1段的情况,index为0。采用设备树后,一些设备驱动通过of_iomap()而不再通过传统的ioremap()进行映射,当然,传统的ioremap()的用户也不少。
int of_address_to_resource(struct device_node *dev, int index,struct resource *r);
上述API通过设备节点获取与它对应的内存资源的resource结构体。其本质是分析reg属性以获取内存基地址、大小等信息并填充到struct resource*r参数指向的结构体中。
unsigned int irq_of_parse_and_map(struct device_node *dev, int index);
通过设备树获得设备的中断号,实际上是从.dts中的interrupts属性里解析出中断号。若设备使用了多个中断,index指定中断的索引号。
struct platform_device *of_find_device_by_node(struct device_node *np);
在可以拿到device_node的情况下,如果想反向获取对应的platform_device,可使用上述API。当然,在已知platform_device的情况下,想获取device_node则易如反掌,例如:
static int sirfsoc_dma_probe(struct platform_device *op)
{
struct device_node *dn = op->dev.of_node;
…
}