背景
通过上一讲了解完设备树DTS有关概念,我们这一讲就来基于设备树例程,学习设备树的语法规则。
设备树框架
1
个dts文件 + n
个dtsi文件,它们编译而成的dtb文件就是真正的设备树。
基于同样的软件分层设计的思想,由于一个SoC可能对应多个machine,如果每个machine的设备树都写成一个完全独立的.dts文件,那么势必相当一些.dts文件有重复的部分。
为了解决这个问题,Linux设备树目录把一个SoC公用的部分或者多个machine共同的部分提炼为相应的.dtsi文件。
这样每个.dts就只有自己差异的部分,公有的部分只需要"include"相应的.dtsi文件, 以保证整个设备树的管理更加有序。
以solidrun公司的hummingboard为例,其组成为imx6dl-hummingboard.dts |_imx6dl.dtsi | |_imx6qdl.dtsi |_imx6qdl-microsom.dtsi |_imx6qdl-microsom-ar8035.dtsi
此外,dts/dtsi兼容c语言的一些语法,能使用宏定义,也能包含.h文件
设备树用树状结构描述设备信息,它有以下几种特性:
- 每个设备树文件都有一个根节点,每个设备都是一个节点。
- 节点由 节点名 + 属性 组成。
- 节点间可以嵌套,形成父子关系,这样就可以方便的描述设备间的关系。
- 每个设备的属性都用一组key-value对(键值对)来描述。
- 每个属性的描述用
;
结束
所以,一个设备树的基本框架可以写成下面这个样子,一般来说,/表示板子,它的子节点node1表示SoC上的某个控制器,控制器中的子节点node2表示挂接在这个控制器上的设备(们)。
/ { //根节点
node1{ //node1是节点名,是/的子节点
key=value; //node1的属性
...
node2{ //node2是node1的子节点
key=value; //node2的属性
...
}
} //node1的描述到此为止
node3{
key=value;
...
}
}
以下是一颗最简单的设备树:
注意/dts-v1/;
是必须的,有时候正是因为忽略了它而引起了syntax error
且没有其他提示。
/dts-v1/;
/ {
};
节点node
{}包围起来的结构称之为节点,dts中最开头的/ {},称为根节点。
在节点中,以key = value
代表节点属性。
树中每个表示一个设备的节点都需要一个 compatible 属性。
节点名 name
- 节点名称:每个节点名格式为:
<name>[@<unit_address>]
,其中::设备名,就是一个不超过31位的简单 ascii 字符串,节点的命名应该根据它所体现的是什么样的设备。 - <unit_address> :设备地址,用来唯一标识一共节点。没有指定<unit_address>时,同级节点命名必须是唯一的;但只要<unit_address>不同,多个节点也可以使用一样的通用名称。
下面是典型节点名的写法:
/ {
model = "Freescale i.MX23 Evaluation Kit";
compatible = "fsl,imx23-evk", "fsl,imx23";
memory {
reg = <0x40000000 0x08000000>;
};
// 注意这里
apb@80000000 {
...
};
}
上面的节点名是apb,节点路径是/apb@80000000 ,这点要注意,因为根据节点名查找节点的API的参数是不能有"@xxx"这部分的。
Linux中的设备树还包括几个特殊的节点:比如chosen,chosen节点不描述一个真实设备,而是用于firmware传递一些数据给OS,比如bootloader传递内核启动参数给内核
/include/ "zynq-7000.dtsi"
/ {
model = "Zynq ZC702 Development Board";
compatible = "xlnx,zynq-zc702", "xlnx,zynq-7000";
...
chosen {
bootargs = "console=ttyPS1,115200 earlyprintk";
};
};
引用
当我们找一个节点的时候,我们必须书写完整的节点路径,这样当一个节点嵌套比较深的时候就不是很方便。所以,设备树允许我们用下面的形式为节点标注引用(起别名),借以省去冗长的路径。
标号引用常常还作为节点的重写方式,用于修改节点属性。
- 格式:
- 声明别名:
别名 : 节点名
- 访问 : &
别名
- 声明别名:
编译设备树的时候,相同的节点的不同属性信息都会被合并,相同节点的相同的属性会被重写(覆盖前值),使用引用可以避免移植者四处找节点,直接在板级.dts增改即可。
/include/ "imx53.dtsi"
/ {
model = "Freescale i.MX53 Automotive Reference Design Board";
compatible = "fsl,imx53-ard", "fsl,imx53";
memory {
reg = <0x70000000 0x40000000>;
};
eim-cs1@f4000000 {
#address-cells = <1>;
#size-cells = <1>;
compatible = "fsl,eim-bus", "simple-bus";
reg = <0xf4000000 0x3ff0000>;
lan9220@f4000000 {
compatible = "smsc,lan9220", "smsc,lan9115";
reg = <0xf4000000 0x2000000>;
phy-mode = "mii";
interrupt-parent = <&gpio2>; // 直接使用引用
vdd33a-supply = <®_3p3v>;
};
};
regulators {
compatible = "simple-bus";
reg_3p3v: 3p3v { // 定义一个引用
compatible = "regulator-fixed";
regulator-name = "3P3V";
};
};
...
// 引用一个节点,新增/修改其属性。
®_3p3v {
regulator-always-on;
}
节点属性 property
属性一般由
key = value;
键值对构成。
Linux设备树语法中定义了一些具有规范意义的属性,包括:compatible, address, interrupt等,这些信息能够在内核初始化找到节点的时候,自动解析生成相应的设备信息。
此外,还有一些Linux内核定义好的,一类设备通用的有默认意义的属性,这些属性一般不能被内核自动解析生成相应的设备信息,但是内核已经编写的相应的解析提取函数,常见的有 "mac_addr","gpio","clock","power"。"regulator" 等等。
- 简单的键-值对,它的值可以为空或者包含一个任意字节流。虽然数据类型并没有编码进数据结构,但在设备树源文件中任有几个基本的数据表示形式:
- 文本字符串(无结束符)可以用双引号表示:
string-property = "a string"
- Cells是 32 位无符号整数,用尖括号限定:
cell-property = <0xbeef 123 0xabcd1234>
- 二进制数据用方括号限定:
binary-property = [01 23 45 67];
- 不同表示形式的数据可以使用逗号连在一起:
mixed-property = "a string", [01 23 45 67], <0x12345678>;
- 逗号也可用于创建字符串列表:
string-list = "red fish", "blue fish";
- 混合形式:上述几种的混合形式
- 文本字符串(无结束符)可以用双引号表示:
compatible 兼容性
如果一个节点是设备节点,那么它一定要有compatible(兼容性),因为这将作为驱动和设备(设备节点)的匹配依据,compatible(兼容性)的值可以有不止一个字符串以满足不同的需求。(设备节点中对应的节点信息已经被内核构造成struct platform_device。驱动可以通过相应的函数从中提取信息。)
compatible属性是用来查找节点的方法之一,另外还可以通过节点名或节点路径查找指定节点。
而根节点的compatible也是非常重要的,一般在系统启动以后,用于识别对应系统一些东西,并由此进行对应的初始化。
- 格式:
compatible = "<manufacturer>,<model>" [, "<manufacturer>,<model>"]
- manufacturer指定厂家名,model指定特定设备型号;后续的<manufacturer,model>指定兼容的设备型号(其中,后续的
<manufacturer>
可空,第二个model也可空)。
我们来看 compatible 是如何与 驱动捆绑在一起的:
可以看出,驱动中用于匹配的结构使用的compatible和设备树中一模一样,否则就可能无法匹配,这里另外的一点是struct of_device_id数组的最后一个成员一定是空,因为相关的操作API会读取这个数组直到遇到一个空。
- manufacturer指定厂家名,model指定特定设备型号;后续的<manufacturer,model>指定兼容的设备型号(其中,后续的
1)先随便在设备树中出一个网卡设备,关键是找到 compatible
属性中的 <model>
值。
// 文件节选于:arch/arm/boot/dts/vexpress-v2m-rs1.dtsi
ethernet@2,02000000 {
compatible = "smsc,lan9118", "smsc,lan9115";
reg = <2 0x02000000 0x10000>;
interrupts = <15>;
phy-mode = "mii";
reg-io-width = <4>;
smsc,irq-active-high;
smsc,irq-push-pull;
vdd33a-supply = <&v2m_fixed_3v3>;
vddvario-supply = <&v2m_fixed_3v3>;
};
2)在驱动中(为了方便读者理解,这里在内核源码根目录下查找,实际上就是在driver目录中),找到对应的.compatible
关键字所在的文件以及行数。
$ find . 2>/dev/null | grep lan9115
arch/arm/boot/dts/vexpress-v2m-rs1.dtsi:50: compatible = "smsc,lan9118", "smsc,lan9115";
arch/arm/boot/dts/vexpress-v2m.dtsi:49: compatible = "smsc,lan9118", "smsc,lan9115";
drivers/net/ethernet/smsc/smsc911x.c:2578: { .compatible = "smsc,lan9115", },
3)顺藤摸瓜,找到所在行,也就找到了用来描述设备信息的结构体of_device_id
。
// 节选于 drivers/net/ethernet/smsc/smsc911x.c
#ifdef CONFIG_OF
static const struct of_device_id smsc911x_dt_ids[] = {
{ .compatible = "smsc,lan9115", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, smsc911x_dt_ids);
#endif
可以看出,驱动中用于匹配的结构使用的compatible和设备树中一模一样,且,字符串需要严格匹配。
注:这里另外的一点是
struct of_device_id
数组的最后一个成员一定是空,因为相关的操作API会读取这个数组直到遇到一个空。
i2c和spi驱动还支持一种“别名匹配”的机制,就以pcf8523为例,假设某程序员在设备树中的pcf8523设备节点中写了compatible = "pcf8523";,显然相对于驱动id_table中的"nxp,pcf8523",他遗漏了nxp字段,但是驱动却仍然可以匹配上,因为别名匹配对compatible中字符串里第二个字段敏感。
驱动程序将直接和设备树里的设备节点进行配对,是通过设备节点中的compatible(兼容性)来与设备节点进行配对的,具体的应用详见 基于i2c子系统的驱动分析、 基于platform总线的驱动分析
address 地址属性
有关节点的地址,比如i2c@021a0000,虽然它在名字后面跟了地址,但是正式的设置是在reg属性中设置。
(几乎)所有的设备都需要与CPU的IO口相连,所以其IO端口信息就需要在设备节点节点中说明。常用的属性有:
#address-cells = <CNT>
,用来描述子节点"reg"属性的地址表中用来描述首地址的cell的数量#size-cells = <CNT>
, 用来描述子节点"reg"属性的地址表中用来描述地址长度的cell的数量。reg = <address ... length>
: address 代表基地址, length 代表长度。基址和长度的格式是可变的,addr由父节点的#address-cells
个uint32值组成,len由父节点的#size-cells
个uint32值组成。表明了设备使用的一个地址范围。
例如:
aips-bus@02000000 { /* AIPS1 */
compatible = "fsl,aips-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02000000 0x100000>;
i2c1: i2c@021a0000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6q-i2c", "fsl,imx21-i2c";
reg = <0x021a0000 0x4000>;
rtc: rtc@68 {
compatible = "stm,mt41t62";
reg = <0x68>;
};
};
};
/*
我们知道,aips-bus@02000000 是 i2c@021a0000 的父节点;i2c@021a0000 是 rtc@68 的父节点。
aips-bus@02000000的 #address-cells 和#size-cells均为1,所以 i2c@021a0000 中的 `reg` 格式为: `<address length>`
i2c@021a0000的 #address-cells 和#size-cells分别为1和0,所以 rtc@68 中的 `reg` 格式为: `<address>`
通俗来讲,如果现在有 一个节点A的 #address-cells 和#size-cells分别为2和1;那么A的子节点B 的 `reg`格式为 `<address address length>`
*/
interrupts 中断属性
中断产生设备用interrupts属性描述中断源(interrupt specifier),因为不同的硬件描述中断源需要的数据量不同,所以interrupts属性的类型也是
。为了明确表示一个中断由几个u32表示,又引入了#interrupt-cells属性,#interrupt-cells属性的类型是u32,假如一个中断源需要2个u32表示(一个表示中断号,另一个表示中断类型),那么#interrupt-cells就设置成2。
有些情况下,设备树的父节点不是中断的父节点(主要是中断控制器一般不是父节点),为此引入了interrupt-parent属性,该属性的类型是,用来引用中断父节点(我们前边说过,一般用父节点的标签,这个地方说中断父节点而不是中断控制器是有原因的)。如果设备树的父节点就是中断父节点,那么可以不用设置interrupt-parent属性。interrupts属性和interrupt-parent属性都是中断产生设备节点的属性,但是#interrupt-cells属性不是,#interrupt-cells属性是中断控制器节点以及interrupt nexus节点的属性,这两类节点都可能是中断父节点。
一个计算机系统中大量设备都是通过中断请求CPU服务的,所以设备节点中就需要在指定中断号。常用的属性有:
interrupt-controller
: 一个空属性用来声明这个node接收中断信号,即这个node是一个中断控制器。#interrupt-cells
:是中断控制器节点的属性,用来标识这个控制器需要几个单位做中断描述符,用来描述子节点中interrupts
属性使用了父节点中的interrupts
属性的具体的哪个值。一般,如果父节点的该属性的值是3,则子节点的interrupts一个cell的三个32bits整数值分别为:<中断域 中断 触发方式>
,如果父节点的该属性是2,则是<中断 触发方式>
interrupt-parent
:标识此设备节点属于哪一个中断控制器,如果没有设置这个属性,会自动依附父节点的interrupts
:一个中断标识符列表,表示每一个中断输出信号。reg
: 在schips todo
这里重点说明一下,interrupts
属性,在ARM GIC(Generic Interrupt Controller)中:
备注:ARM GIC 说明文档位于:
Documentation/devicetree/bindings/arm/gic.txt
;此外,本人并没有找到#interrupt-cells
为1个时的文档说明。
当interrupt-cells
为3时,interrupts包含三个cells,如interrupts = <0 168 4>
:
第一个cell代表中断类型:0 表示SPI中断,1 表示PPI中断。
第二个cell代表具体的中断类型:、
- PPI中断:私有外设中断(Private Peripheral Interrupt),是每个CPU私有的中断。最多支持16个PPI中断,范围【0 - 15】。
- SPI中断类型:公用外设中断(Shared Peripheral Interrupt),最多可以支持988个外设中断,范围【0 - 987】。
第三个cell代表中断触发标志:
- bits [ 3 :0 ] 触发类型和级别标志:
1 = 低- 至- 高边沿触发
2 = 高- 到- 低边沿触发
4 = 活跃的高水平 - 敏感
8 = 低电平有效 - 敏感- bits [ 15 :8 ] PPI中断cpu掩码。每个位对应于每个位附加到GIC的8个可能的cpu。指示设置为"1"的位中断被连接到该CPU 。只有有效的PPI中断。
当interrupt-cells
为2时,interrupts包含2个cells,如interrupts = <2 4>
:
第一个cell代表具体的中断类型:
- SGI中断:软件触发中断(Software Generated Interrupt),通常用于多核间通讯,最多支持16个SGI中断,硬件中断号从ID0~ID15。
- PPI中断:私有外设中断(Private Peripheral Interrupt),是每个CPU私有的中断。最多支持16个PPI中断,硬件中断号从ID16~ID31。
- SPI中断类型:公用外设中断(Shared Peripheral Interrupt),最多可以支持988个外设中断,硬件中断号从ID32~ID1019。
第二个cell代表中断触发标志:
bits [ 3 :0 ] 触发类型和级别标志:
1 = 低- 至- 高边沿触发
2 = 高- 到- 低边沿触发
4 = 活跃的高水平- 敏感
8 = 低电平有效- 敏感
bits [ 15 :8 ] PPI中断cpu掩码。每个位对应于每个位附加到GIC的8个可能的cpu。指示设置为"1"的位中断被连接到该CPU 。只有有效的PPI中断。
/ {
compatible = "acme,coyotes-revenge";
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>;//指定依附的中断控制器是intc
serial@101f0000 { //子节点:串口设备
compatible = "arm,pl011";
reg = <0x101f0000 0x1000 >;
interrupts = < 1 0 >;
};
intc: interrupt-controller@10140000 { //intc中断控制器
compatible = "arm,pl190";
reg = <0x10140000 0x1000 >;
interrupt-controller;//定义为中断控制器设备
#interrupt-cells = <2>;
};
}
GPIO 属性
gpio也是最常见的IO口,常用的属性有:
- "gpio-controller",用来说明该节点描述的是一个gpio控制器
- "#gpio-cells",用来描述gpio使用节点的属性一个cell的内容,即 `属性 = <&引用GPIO节点别名 GPIO标号 工作模式>
通过上面的属性定义以后,就可以使用它,例如:
2 &spi_1 {
1 status = "okay";
388 cs-gpios = <&gpa2 5 GPIO_ACTIVE_HIGH>; // 使用 GPIO A2 第5个引脚,
1
2 w25q80bw@0 {
3 #address-cells = <1>;
4 #size-cells = <1>;
5 compatible = "w25x80";
6 reg = <0>;
7 spi-max-frequency = <1000000>;
8
9 controller-data {
10 samsung,spi-feedback-delay = <0>;
11 };
12
驱动自定义key属性
针对具体的设备,有部分属性很难做到通用,需要驱动自己定义好。
可以在设备树中自定义key属性,再在驱动中通过内核的属性提取解析函数进行值的获取。
比如:
/* 有关的 设备树写法 */
6 ethernet@2,02000000 {
5 compatible = "smsc,lan9118", "smsc,lan9115";
4 reg = <2 0x02000000 0x10000>;
3 interrupts = <15>;
2 phy-mode = "mii";
1 reg-io-width = <4>;
55 smsc,irq-active-high; // 自定义key
1 smsc,irq-push-pull; // 自定义key
2 vdd33a-supply = <&v2m_fixed_3v3>;
3 vddvario-supply = <&v2m_fixed_3v3>;
4 };
5
6 usb@2,03000000 {
7 compatible = "nxp,usb-isp1761";
8 reg = <2 0x03000000 0x20000>;
9 interrupts = <16>;
arch/arm/boot/dts/vexpress-v2m-rs1.dtsi
/* 有关的驱动写法 */
2389 if (of_get_property(np, "smsc,irq-active-high", NULL))
1 config->irq_polarity = SMSC911X_IRQ_POLARITY_ACTIVE_HIGH;
2
3 if (of_get_property(np, "smsc,irq-push-pull", NULL))
4 config->irq_type = SMSC911X_IRQ_TYPE_PUSH_PULL;
5
6 if (of_get_property(np, "smsc,force-internal-phy", NULL))
7 config->flags |= SMSC911X_FORCE_INTERNAL_PHY;
8
9 if (of_get_property(np, "smsc,force-external-phy", NULL))
10 config->flags |= SMSC911X_FORCE_EXTERNAL_PHY;
11
12 if (of_get_property(np, "smsc,save-mac-address", NULL))
13 config->flags |= SMSC911X_SAVE_MAC_ADDRESS;
14
15 return 0;
drivers/net/ethernet/smsc/smsc911x.c
附录:补充对于interrupt-parent的一些知识点
为什么会有interrupt-parent?
首先讲讲Linux设备管理中对中断的设计思路演变。随着linux kernel的发展,在内核中将interrupt controller抽象成irqchip这个概念越来越流行,甚至GPIO controller也可以被看出一个interrupt controller chip,这样,系统中至少有两个中断控制器了。另外,在硬件上,随着系统复杂度加大,外设中断数据增加,在这种趋势下,内核中原本的中断源直接到中断号的方式已经很难继续发展了,为了解决这些问题,linux kernel的大牛们就创造了irq domain(中断域)这个概念。domain在内核中有很多,除了irqdomain,还有power domain,clock 这些domain等等;所谓domain,就是领域,范围的意思(即:任何的定义出了这个范围就没有意义了)。
实际上系统可以需要多个中断控制器进行级联,形成事实上的硬件中断处理结构:
如上所述,系统中所有的interrupt controller会形成树状结构,对于每个interrupt controller都可以连接若干个外设的中断请求(interrupt source,中断源),interrupt controller会对连接其上的interrupt source(根据其在Interrupt controller中物理特性)进行编号(也就是HW interrupt ID了)。有了irq domain这个概念之后,这个编号仅仅限制在本interrupt controller范围内。
有了这样的设计,CPU(Linux 内核)就可以根据级联的规则一级一级的找到想要访问的中断。当然,通常我们关心的只是内核中的中断号,具体这个中断号是怎么找到相应的中断源的,我们作为程序员往往不需要关心。
在写设备树的时候,设备树就是要描述嵌入式软件开发中涉及的所有硬件信息。所以,设备树就需要准确描述硬件上处理中断的这种树状结构,如此,就有了我们的interrupt-parant这样的概念:用来连接这样的树状结构的上下级,用于表示这个中断归属于哪个interrupt controller,比如,一个接在GPIO上的按键,它的组织形式就是:
中断源--interrupt parent-->GPIO--interrupt parent-->GIC1--interrupt parent-->GIC2--...-->CPU
有了parant,我们就可以使用一级一级的偏移量来最终获得当前中断的绝对编号。
可以看出,在我板子上的dm9000的的设备节点中,它的"interrupt-parent"引用了"exynos4x12-pinctrl.dtsi"(被板级设备树的exynos4412.dtsi包含)中的gpx0节点:
而在gpx0节点中,指定了#interrupt-cells = <2>;
,所以在dm9000中的属性interrupts = <6 4>;
表示dm9000的的中断在作为irq parant的gpx0中的中断偏移量,即gpx0中的属性interrupts
中的<0 22 0>
,通过查阅exynos4412的手册知道,对应的中断号是EINT[6]。