设备树语法及绑定
概述
Device Tree是一种用来描述硬件的数据结构,类似板级描述语言,起源于OpenFirmware(OF)。
就ARM平台来说,设备树文件存放在arch/arm/boot/dts下,绑定文档存在Documentation/devicetree/bindings下。
设备树由一系列被命名的节点(node)和属性(property)组成,而节点本身可包含子节点。所谓属性,就是成对出现的名称和值。
在设备树中可描述的信息包括(原来这些信息大多被编码在内核中):
》CPU数量和类别
》内存基地址和大小
》总线和桥
》外设连接
》中断控制器和中断使用情况
》GPIO控制器和GPIO使用情况
》时钟控制器和时钟使用情况
它基本上就是画一棵电路板上CPU、总线、设备组成的树,Bootloader会将这棵树传递给内核,然后内核可以识别这棵树,并根据它展开出Linux内核中的platform_device、i2c_client、spi_device等设备,而这些设备用到的内存、IRQ等资源,也被传递给了内核,内核会将这些资源绑定给展开的相应的设备。
对于设备树上的节点和属性具体是如何来描述设备的硬件细节的,一般需要绑定文件(.txt)说明:
rtc/rtc-cmos.txt
Motorola mc146818 compatible RTC
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Required properties:
- compatible : "motorola,mc146818"
- reg : should contain registers location and length. Optional properties:
- interrupts : should contain interrupt.
- interrupt-parent : interrupt source phandle.
- ctrl-reg : Contains the initial value of the control register also
called "Register B".
- freq-reg : Contains the initial value of the frequency register also
called "Regsiter A". "Register A" and "B" are usually initialized by the firmware (BIOS for
instance). If this is not done, it can be performed by the driver. ISA Example: rtc@ {
compatible = "motorola,mc146818";
interrupts = < >;
interrupt-parent = <&ioapic1>;
ctrl-reg = <>;
freq-reg = <0x26>;
reg = < 0x70 >;
};
基本可以看出,设备树绑定文档的主要内容包括:
》关于该模块的最基本的描述。
》必要属性的描述
》可选属性的描述
》一个示例
又如常见的GPIO控制LED,属性绑定示例,gpio/led.txt
LEDs connected to GPIO lines Required properties:
- compatible : should be "gpio-leds". Each LED is represented as a sub-node of the gpio-leds device. Each
node's name represents the name of the corresponding LED. LED sub-node properties:
- gpios : Should specify the LED's GPIO, see "Specifying GPIO information
for devices" in Documentation/devicetree/booting-without-of.txt. Active
low LEDs should be indicated using flags in the GPIO specifier.
- label : (optional) The label for this LED. If omitted, the label is
taken from the node name (excluding the unit address).
- linux,default-trigger : (optional) This parameter, if present, is a
string defining the trigger assigned to the LED. Current triggers are:
"backlight" - LED will act as a back-light, controlled by the framebuffer
system
"default-on" - LED will turn on, but see "default-state" below
"heartbeat" - LED "double" flashes at a load average based rate
"ide-disk" - LED indicates disk activity
"timer" - LED flashes at a fixed, configurable rate
- default-state: (optional) The initial state of the LED. Valid
values are "on", "off", and "keep". If the LED is already on or off
and the default-state property is set the to same value, then no
glitch should be produced where the LED momentarily turns off (or
on). The "keep" setting will keep the LED at whatever its current
state is, without producing a glitch. The default is off if this
property is not present. Examples: leds {
compatible = "gpio-leds";
hdd {
label = "IDE Activity";
gpios = <&mcu_pio >; /* Active low */
linux,default-trigger = "ide-disk";
}; fault {
gpios = <&mcu_pio >;
/* Keep LED on if BIOS detected hardware fault */
default-state = "keep";
};
}; run-control {
compatible = "gpio-leds";
red {
gpios = <&mpc8572 >;
default-state = "off";
};
green {
gpios = <&mpc8572 >;
default-state = "on";
};
}
linux内核下scripts/checkpatch.pl会运行一个检查,如果有人在设备树中新添加了compatible字符串,而没有添加相应的文档进行解释,checkpatch程序会报出警告:UNDOCUMENTED_DT_STRINGDT compatible string xxx appears un-documented。
设备树语法
设备树文件包括两种:dts和dtsi。dtsi相当于头文件,其可能包含一些板卡设备的共用部分(被提炼出来,如CPU信息等,多种类板卡共用同一CPU),dts可通过include包含dtsi。
#include "xxx.dtsi"
/include/ "xxx.dtsi"
下面看个示例
/*
* Allwinner Technology CO., Ltd. sun50iw1p1 platform
* modify base on juno.dts
*/ #include <dt-bindings/interrupt-controller/arm-gic.h>
#include <dt-bindings/gpio/gpio.h>
#include "sun50iw1p1-clk.dtsi"
#include "sun50iw1p1-pinctrl.dtsi"
/ {
model = "sun50iw1p1";
compatible = "arm,sun50iw1p1", "arm,sun50iw1p1";
interrupt-parent = <&gic>;
#address-cells = <>;
#size-cells = <>; aliases {
serial0 = &uart0;
......
......
......
boot_disp = &boot_disp;
}; chosen {
bootargs = "earlyprintk=sunxi-uart,0x01c28000 loglevel=8 initcall_debug=1 console=ttyS0 init=/init";
linux,initrd-start = <0x0 0x0>;
linux,initrd-end = <0x0 0x0>;
}; cpus {
#address-cells = <>;
#size-cells = <>; cpu@ {
device_type = "cpu";
compatible = "arm,cortex-a53","arm,armv8";
reg = <0x0 0x0>;
enable-method = "psci";
cpufreq_tbl = <
>;
clock-latency = <>;
clock-frequency = <>;
cpu-idle-states = <&CPU_SLEEP_0 &CLUSTER_SLEEP_0 &SYS_SLEEP_0>;
};
cpu@ {
device_type = "cpu";
compatible = "arm,cortex-a53","arm,armv8";
reg = <0x0 0x1>;
enable-method = "psci";
clock-frequency = <>;
cpu-idle-states = <&CPU_SLEEP_0 &CLUSTER_SLEEP_0 &SYS_SLEEP_0>;
};
cpu@ {
......
......
......
};
cpu@ {
......
......
......
};
}; memory@ {
device_type = "memory";
reg = <0x00000000 0x40000000 0x00000000 0x40000000>;
}; gic: interrupt-controller@1c81000 {
compatible = "arm,cortex-a15-gic", "arm,cortex-a9-gic";
#interrupt-cells = <>;
#address-cells = <>;
device_type = "gic";
interrupt-controller;
reg = <0x0 0x01c81000 0x1000>, /* GIC Dist */
<0x0 0x01c82000 0x2000>, /* GIC CPU */
<0x0 0x01c84000 0x2000>, /* GIC VCPU Control */
<0x0 0x01c86000 0x2000>; /* GIC VCPU */
interrupts = <GIC_PPI 0xf04>; /* GIC Maintenence IRQ */
}; soc: soc@01c00000 {
compatible = "simple-bus";
#address-cells = <>;
#size-cells = <>;
ranges;
device_type = "soc"; uart0: uart@01c28000 {
compatible = "allwinner,sun50i-uart";
device_type = "uart0";
reg = <0x0 0x01c28000 0x0 0x400>;
interrupts = <GIC_SPI IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clk_uart0>;
pinctrl-names = "default", "sleep";
pinctrl- = <&uart0_pins_a>;
pinctrl- = <&uart0_pins_b>;
uart0_port = <>;
uart0_type = <>;
status = "disabled";
......
......
......
};
......
......
......
};
};
*note:<name>[@<unit-address>]是节点的格式,其中unit-address是单位偏移地址,[]可省略。
“/"代表根节点;
“model”是板的ID;
"compatible"是平台兼容,一般格式是"manufacturer,model"。内核或者uboot依靠这个属性找到相对应driver,若"compatible"出现多个属性,按序匹配driver;
“#address-cells”是address的单位(32bit);
“#size-cells”是length的单位(32bit);
"reg"是寄存器,格式是"<address,length>",作为平台内存资源;
"aliase" 是别名,必须节点全称,可以通过地址引用获取;
”chosen“是板级启动参数;
"cpus"是SOC的CPU信息,可以改变运行频率或者开关CPU;
"memory"是板级内存的信息。
"interrupts"是中断控制器,根据SOC自定义格式,这里是<输入类型 中断号 触发方式>,作为平台中断资源;
“interrupt-controller”指示这个节点是中断控制节点;
"[label:]"如gic: interrupt-controller@1c81000,这个标签可以作为地址赋值到其他节点的属性;
“device_type":设备类型,寻找节点可以依据这个属性;
"status"是开关节点设备的状态,取值"okay"或者"ok"表示使能,"disabled"表示失能。
根节点兼容性
一个最简单的设备树必须包含根节点,cpus节点,memory节点。根节点的名字及全路径都是“/”,至少需要包含model和compatible两个属性。
model属性我是用来描述产品型号的,类型为字符串,推荐的格式为“manufacturer,model-number”(非强制的)。根节点的model属性描述的是板子的型号或者芯片平台的型号,如:
model = "Atmel AT91SAM9G20 family SoC"
model = "Samsung SMDK5420 board based on EXYNOS5420"
从软件的层面讲model属性仅仅表示一个名字而已,没有更多的作用。
compatible属性则不同,该属性决定软件如何匹配硬件对硬件进行初始化。compatible属性的类型是字符串数组,按照范围从小到大的顺序排列,每个字符串表示一种匹配类型。根节点的compatible属性表示平台如何匹配,比如
compatible = "samsung,smdk5420", "samsung,exynos5420", "samsung,exynos5" 表示软件应该首先匹配'samsung,smdk5420',这个是一款开发板。 如果无法匹配,再试着匹配"samsung,exynos5420",这个是一款芯片平台。 如果还是无法匹配,还可以试着匹配 "samsung,exynos5",这是一个系列的芯片平台。
这里说的匹配是指软件根据该信息找到对应的代码,如对应的初始化函数。
根节点表示的是整个板子或者芯片平台,所以在系统初始化比较早的时候就需要确认是什么平台,怎样初始化。对于Linux,是通过在start_kernel函数调用setup_arch函数实现的。不同的架构,setup_arch函数的实现不同,对于arm架构,setup_arch函数源代码位于arch/arm/kernel/setup.c中。
void __init setup_arch(char **cmdline_p)
{
struct machine_desc *mdesc; setup_processor();
mdesc = setup_machine_fdt(__atags_pointer);
if (!mdesc)
mdesc = setup_machine_tags(machine_arch_type);
machine_desc = mdesc;
machine_name = mdesc->name;
...
}
过去,ARM Linux针对不同的电路板会建立由MACHINE_START和MACHINE_END包围起来的针对这个machine的一系列callback,譬如:
MACHINE_START(AM335XEVM, "am335xevm")
/* Maintainer: Texas Instruments */
.atag_offset = 0x100,
.map_io = am335x_evm_map_io,
.init_early = am33xx_init_early,
.init_irq = ti81xx_init_irq,
.handle_irq = omap3_intc_handle_irq,
.timer = &omap3_am33xx_timer,
.init_machine = am335x_evm_init,
MACHINE_END
这些不同的machine会有不同的MACHINE ID,Uboot在启动Linux内核时会将MACHINE ID存放在r1寄存器,Linux启动时会匹配Bootloader传递的MACHINE ID和MACHINE_START声明的MACHINE ID,然后执行相应machine的一系列初始化函数。
引入Device Tree之后,MACHINE_START变更为DT_MACHINE_START,其中含有一个.dt_compat成员,用于表明相关的machine与.dts中root结点的compatible属性兼容关系。如果Bootloader传递给内核的Device Tree中root结点的compatible属性出现在某machine的.dt_compat表中,相关的machine就与对应的Device Tree匹配,从而引发这一machine的一系列初始化函数被执行。
static const char *omap3_boards_compat[] __initdata = {
"ti,omap3",
NULL,
}; DT_MACHINE_START(OMAP3_DT, "Generic OMAP3 (Flattened Device Tree)")
.atag_offset = 0x100,
.reserve = omap_reserve,
.map_io = omap3_map_io,
.init_early = omap3430_init_early,
.init_irq = omap3_init_irq,
.handle_irq = omap3_intc_handle_irq,
.init_machine = omap3_init,
.timer = &omap3_timer,
.dt_compat = omap3_boards_compat,
MACHINE_END
Linux倡导针对多个SoC、多个电路板的通用DT machine,即一个DT machine的.dt_compat表含多个电路板.dts文件的root结点compatible属性字符串。之后,如果的电路板的初始化序列不一样,可以透过
int of_machine_is_compatible(const char *compat)
API判断具体的电路板是什么。
static char const *exynos5_dt_compat[] __initdata = {
"samsung,exynos5250",
"samsung,exynos5440",
NULL
}; DT_MACHINE_START(EXYNOS5_DT, "SAMSUNG EXYNOS5 (Flattened Device Tree)")
/* Maintainer: Kukjin Kim <kgene.kim@samsung.com> */
.init_irq = exynos5_init_irq,
.smp = smp_ops(exynos_smp_ops),
.map_io = exynos5_dt_map_io,
.handle_irq = gic_handle_irq,
.init_machine = exynos5_dt_machine_init,
.init_late = exynos_init_late,
.timer = &exynos4_timer,
.dt_compat = exynos5_dt_compat,
.restart = exynos5_restart,
.reserve = exynos5_reserve,
MACHINE_END
它的.init_machine成员函数就针对不同的machine进行了不同的分支处理:
static void __init exynos5_dt_machine_init(void)
{
… if (of_machine_is_compatible("samsung,exynos5250"))
of_platform_populate(NULL, of_default_bus_match_table,
exynos5250_auxdata_lookup, NULL);
else if (of_machine_is_compatible("samsung,exynos5440"))
of_platform_populate(NULL, of_default_bus_match_table,
exynos5440_auxdata_lookup, NULL);
}
根节点还可能包含的属性为#address-cells和#size-cells,规范中说明这两个属性是必须的,实际应用时是可选的,还记得属性那一节说这两个属性如果没有都是有默认值的,#address-cells默认值为2,#size-cells默认值为1。根节点下必须包含的子节点为cpus和memory,后边会说明cpus下边还有每个cpu的子节点,memory节点下边定义的就是memory的起始地址及大小,所以根节点的#address-cells和#size-cells属性实际上说明的就是从cpu角度看系统总线的地址长度和大小。
设备节点兼容性
如何查找节点属性
每个节点都有一个兼容属性(compatible),兼容属性用于驱动和设备的绑定。
兼容属性值都应在绑定文档中描述,即Documentation/devicetree/bindings下文档,通过搜索兼容属性值可查找到该节点应包含的属性,如上述绑定文档。
如需了解通过GPIO连接的LED的属性,可这样查询:
#cd Documentation/devicetree/bindings
#grep 'gpio' ./* -rn
#grep 'led' ./* -rn
主要文件为gpio/led.txt,compatible = “gpio-leds”,每个子节点的属性包括gpios,label,default-status等。
驱动中绑定
使用设备树后,驱动需要与dts中描述的设备节点进行匹配,从而使驱动的probe()函数执行。新的驱动、设备的匹配变成了设备树节点的兼容属性和设备驱动中of_match_table的匹配。
对于platform_driver而言,需要添加一个OF匹配表,比如gpio-leds的驱动程序位于drivers/leds/leds-gpio.c。
static const struct of_device_id of_gpio_leds_match[] = {
{ .compatible = "gpio-leds", },
{},
};
static struct platform_driver gpio_led_driver = {
.probe = gpio_led_probe,
.remove = gpio_led_remove,
.driver = {
.name = "leds-gpio",
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(of_gpio_leds_match),
},
}; module_platform_driver(gpio_led_driver);
of_match_table添加匹配的dts中相关节点的兼容属性。
驱动中设备树解析
设备树的解析一般放在probe()中,当兼容性匹配时驱动执行probe()函数,此时解析设备树。为了兼容,若设备采用以前方式注册则不必解析设备树。
通过宏定义CONFIG_OF_GPIO实现编译选择。
还以drivers/leds/leds-gpio.c为例示意:
/* Code to create from OpenFirmware platform devices */
#ifdef CONFIG_OF_GPIO
static struct gpio_leds_priv *gpio_leds_create_of(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node, *child;
struct gpio_leds_priv *priv;
int count, ret; /* count LEDs in this device, so we know how much to allocate */
count = of_get_available_child_count(np);
if (!count)
return ERR_PTR(-ENODEV); for_each_available_child_of_node(np, child)
if (of_get_gpio(child, ) == -EPROBE_DEFER)
return ERR_PTR(-EPROBE_DEFER); priv = devm_kzalloc(&pdev->dev, sizeof_gpio_leds_priv(count),
GFP_KERNEL);
if (!priv)
return ERR_PTR(-ENOMEM);
for_each_available_child_of_node(np, child) {
struct gpio_led led = {};
enum of_gpio_flags flags;
const char *state; led.gpio = of_get_gpio_flags(child, , &flags);
led.active_low = flags & OF_GPIO_ACTIVE_LOW;
led.name = of_get_property(child, "label", NULL) ? : child->name;
led.default_trigger =
of_get_property(child, "linux,default-trigger", NULL);
state = of_get_property(child, "default-state", NULL);
if (state) {
if (!strcmp(state, "keep"))
led.default_state = LEDS_GPIO_DEFSTATE_KEEP;
else if (!strcmp(state, "on"))
led.default_state = LEDS_GPIO_DEFSTATE_ON;
else
led.default_state = LEDS_GPIO_DEFSTATE_OFF;
}
ret = create_gpio_led(&led, &priv->leds[priv->num_leds++],
&pdev->dev, NULL);
if (ret < ) {
of_node_put(child);
goto err;
}
} return priv; err:
for (count = priv->num_leds - ; count >= ; count--)
delete_gpio_led(&priv->leds[count]);
return ERR_PTR(-ENODEV);
} static const struct of_device_id of_gpio_leds_match[] = {
{ .compatible = "gpio-leds", },
{},
};
#else /* CONFIG_OF_GPIO */
static struct gpio_leds_priv *gpio_leds_create_of(struct platform_device *pdev)
{
return ERR_PTR(-ENODEV);
}
#endif /* CONFIG_OF_GPIO */
再看下probe()函数的实现:
static int gpio_led_probe(struct platform_device *pdev)
{
struct gpio_led_platform_data *pdata = dev_get_platdata(&pdev->dev);
struct gpio_leds_priv *priv;
int i, ret = ; if (pdata && pdata->num_leds) {
priv = devm_kzalloc(&pdev->dev,
sizeof_gpio_leds_priv(pdata->num_leds),
GFP_KERNEL);
if (!priv)
return -ENOMEM; priv->num_leds = pdata->num_leds;
for (i = ; i < priv->num_leds; i++) {
ret = create_gpio_led(&pdata->leds[i],
&priv->leds[i],
&pdev->dev, pdata->gpio_blink_set); if (ret < ) {
/* On failure: unwind the led creations */
for (i = i - ; i >= ; i--)
delete_gpio_led(&priv->leds[i]);
return ret;
}
}
} else {
priv = gpio_leds_create_of(pdev);
if (IS_ERR(priv))
return PTR_ERR(priv);
}
platform_set_drvdata(pdev, priv); return ;
}
设备节点语法
1. 如果一个节点描述的设备有地址,则应该给出@unit-address。多个相同类型设备节点的name可以一样,只要unit-address不同即可。
2. 对于挂在内存空间的设备而言,@字符后面跟的一般就是该设备在内存空间的基地址。
3. 可以给一个设备节点添加label,之后可以通过&label的形式访问这个label,这种引用是通过phandle(pointer handle)进行的。
4. 可寻址的设备使用如下信息在设备树中编码地址信息:
reg
#address-cells
#size-cells
其中reg的组织形式为reg = <address1 length1 [address2 length2] [address3 length3] ... >,其中的每一组address length表明了设备使用的一个地址范围。address为1个或多个32位的整型(即cell),而length的意义则意味着从address到address+length-1的地址范围都是属于该节点。若#size-cells = 0,则length字段为空。
address和length字段是可变长的,父节点的#address-cells和#size-cells分别决定了子节点reg属性的address和length字段的长度。
#address-cells决定了该reg的前address-cells个cells(整型数)为address,代表地址;
#size-cells决定了该reg的代表长度的cells个数,#size-cells代表没有字段代表地址的length。
举例如下:
external-bus结点的#address-cells = <2>和#size-cells = <1>;决定了其下的ethernet、i2c、flash的reg字段形如reg = <0 0 0x1000>;、reg = <1 0 0x1000>;和reg = <2 0 0x4000000>;。其中,address字段长度为0,开始的第一个cell(0、1、2)是对应的片选,第2个cell(0,0,0)是相对该片选的基地址,即reg的前两个cells代表地址;第3个cell(0x1000、0x1000、0x4000000)为length,为一个cells,即reg的第3个cells代表长度。
ranges = < 0x10100000 0x10000 // Chipselect 1, Ethernet
0x10160000 0x10000 // Chipselect 2, i2c controller
0x30000000 0x1000000>; // Chipselect 3, NOR Flash
ranges 是地址转换表,其中的每个项目是一个子地址、父地址以及在子地址空间的大小的映射。映射表中的子地址、父地址分别采用子地址空间的#address- cells和父地址空间的#address-cells大小。对于本例而言,子地址空间的#address-cells为2,父地址空间 的#address-cells值为1,因此0 0 0x10100000 0x10000的前2个cell为external-bus后片选0上偏移0,即子地址#adress-cells(为2),第3个cell表示external-bus后片选0上偏移0的地址空间被映射到CPU的0x10100000位置,即父地址#address-cells(为1),第4个cell表示映射的大小为0x10000(为1)。
特别要留意的是i2c结点中定义的 #address-cells = <1>;和#size-cells = <0>;作用到了I2C总线上连接的RTC,它的address字段为0x58,是设备的I2C地址,长度字段空。
5. 中断连接。终端相关绑定信息定义在Documentation/devicetree/bindings/interrupt-controller/interrupts.txt。
interrupt-parent: 设备节点通过它来指定它所依附的中断控制器的phandle,当节点没有interrupt-parent时,则从父节点继承。
interrupts: 用到了中断的设备节点,通过它指定中断号/触发方法等,这个属性具体有多少个cell,由它依附的中断控制器节点的#interrupt-cells属性指定。而每个cell具体含义一般由驱动的实现决定,而且也会在设备树的绑定文档中说明。
i2c@7000c000 {
gpioext: gpio-adnp@ {
compatible = "ad,gpio-adnp";
reg = <0x41>; interrupt-parent = <&gpio>;
interrupts = < >; gpio-controller;
#gpio-cells = <>; interrupt-controller;
#interrupt-cells = <>; nr-gpios = <>;
}; sx8634@2b {
compatible = "smtc,sx8634";
reg = <0x2b>; interrupt-parent = <&gpioext>;
interrupts = < 0x8>; #address-cells = <>;
#size-cells = <>; threshold = <0x40>;
sensitivity = <>;
};
};
参考:
http://www.eefocus.com/marianna/blog/cate_18142_0.html?p=2
http://www.eefocus.com/marianna/blog/14-10/306247_821be.html