Linux DeviceTree学习(二)
3 Device Tree基本语法
3.1 基本数据格式
设备树是节点和属性的简单树型结构。属性是键-值对,节点可以包含属性和子节点。例如,以下是.dts格式的简单树:
/dts-v1/;
/ {
node1 {
a-string-property = "A string";
a-string-list-property = "first string", "secondstring";
//十六进制隐含在字节数组中。不需要'0x'前缀
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{
};
};
};
- 上面描述的树是没有任何可使用的价值,没有描述任何板级系统,这里只是描述个个节点的属性结构,下面对上面结构做出解释:
一个单一的根节点:“/”;
一对子节点“node1”和“node2”;
子节点node1还有一对子节点中的子节点:“child-node1”和“child-node2”;
一堆散落在树上的属性。 - 属性是简单的键值对,其中值可以为空或包含任意字节流。虽然数据类型未编码到数据结构中,但有一些基本数据表示可以在设备树源文件中表示。
文本字符串(以NULL结尾)用双引号表示:
string-property=”a string”;
“cells”代表的是32位无符号整型数据,赋值的时候用”< >”包括:
Cell-property=<0xbeef 123 0xabcd1234>;
被”[ ]”包括的是二进制数据:
Binary-property=[0x01 0x23 0x45 0x67];
混合各种数据类型,不同数据类型用逗号隔开:
Mixed-property=”a string”,[0x01 0x23 0x45 0x67], <0x12345678>;
逗号也可以当做多个字符串的分隔符:
String-list=”red fish”, “blue fish”;
3.2 样机
在此给出一个例子,从0开始描述如何写一个完整的设备树。现在有个arm平台的开发板,假设制造商为“WYU”,该板命名为“WYU’S Board”,假设该板子有如下的硬件资源:
一颗32bit ARM CPU
处理器本地总线上映射了串口SPI控制器、I2C控制器、中断控制器以及外部总线桥
基地址为0,大小为256MB的SDRAM
基地址分别为0x101F1000和0x101F2000的2个UART
基地址为0x101F3000 的GPIO控制器
基地址为0x10170000的SPI控制器上有:MMC卡槽的SS引脚连接了GPIO #1
外部总线桥上有如下设备:
SMC91111以太网设备连接到外部总线,基于0x10100000
I2C控制器基于0x10160000,具有以下设备:Maxim DS1338实时时钟,从站相应地址为0x58
基于0x30000000的64MB Norflash
3.2.1 初始化结构体
首先先给一个设备树写一个框架如下所示:
/dts-v1/;
/ {
compatible ="Amicro,Am780’S Board";
};
上面首先在根节点下写了一个属性compatible,该属性是系统用来识别不同板级设备的重要依据,其一般由厂商名和样板名两部分组成。
3.2.2 加入CPU
假如我们的SoC是一颗双核A9的CPU,我们添加一个叫“cpus”的子节点,具体如下:
/dts-v1/;
/ {
compatible ="acme,coyotes-revenge";
cpus {
cpu@0 {
compatible = "arm,cortex-a9";
};
cpu@1 {
compatible = "arm,cortex-a9";
};
};
};
CPU的compatible设置与顶层的类似,有两部分组成,Arm描述制造商,cortex-a9指明了型号。实际上为了完成描述一个cpu需要添加更多的信息,但是现在我们先连接下其他的基础概念。
3.2.3 节点名称
节点的命令方式如下:
<name>[@<uint-address>]
-
name是不超过31个字符的字符串,通常节点名是以设备的类型来命名的而不是具体的设备型号,比如有个网络设备,那么命名的时候节点是“ethernet”,而不是具体的型号如:“SMC91111”。
Uint-address描述一个设备是否需要设置访问地址,通常uint-address描述的访问该设备的基地址,同时该地址也会在reg属性列出。
同一层次下节点命名必须唯一的,但是只要基地址不同,同一层次也可以存在相同的命名节点,比如:serial@101f1000 & serial@101f2000
系统中每个设备都是依靠设备树节点来描述的,接下来就需要添加设备节点来描述每一个设备:
/dts-v1/;
/ {
compatible = "acme,coyotes-revenge";
cpus {
cpu@0 {
compatible = "arm,cortex-a9";
};
cpu@1 {
compatible = "arm,cortex-a9";
};
};
serial@101F0000{
compatible = "arm,pl011";
};
serial@101F2000 {
compatible = "arm,pl011";
};
gpio@101F3000 {
compatible = "arm,pl061";
};
interrupt-controller@10140000 {
compatible = "arm,pl190";
};
spi@10115000 {
compatible = "arm,pl022";
};
external-bus {
ethernet@0,0 {
compatible = "smc,smc91c111";
};
i2c@1,0 {
compatible = "acme,a1234-i2c-bus";
rtc@58 {
compatible ="maxim,ds1338";
};
};
flash@2,0 {
compatible = "samsung,k8f1315ebm","cfi-flash";
};
};
};
- 通过上面的列表可看出,通过dts的层次关系,可看到具体的板级的设备连接关系,比如外部总线上有ethernet、I2C和Flash三个设备。
但是上面的描述的这棵树还不是一个完整有效的设备树,因为其缺少必要的连接方式、地址信息等。
以下几点需要注意:
每一个设备节点都有一个compatible属性;
Flash节点的compatible属性赋值了两个字符串,具体原因稍后解释;
节点名称反映设备的类型,而不是特定的模型。
3.2.4 理解compatible Property
- 树中表示设备的每个节点都需要具有该compatible属性。Compatible是操作系统来决定绑定到设备的设备驱动程序的关键,设备和驱动之间的结合就依赖这个属性的匹配。
- Compatible是一个字符串列表,列表中的第一个字符串指定节点在表单中表示确切设备的信息,第二个字符串描述的与该节点描述符相兼容的设备。
- 例如,飞思卡尔MPC8349片上系统(SoC)具有串行器件,该器件实现了National Semiconductor ns16550寄存器接口。因此,MPC8349串行设备的兼容属性应为: compatible = “fsl,mpc8349-uart”, “ns16550” 。在这种情况下,fsl,mpc8349-uart 指定确切的器件,并ns16550 声明它与National Semiconductor 16550 UART寄存器级兼容。
- 注意: ns16550 由于历史原因,没有制造商前缀。所有新的兼容值都应使用制造商前缀。这种做法允许将现有设备驱动程序绑定到较新的设备,同时仍然唯一地标识确切的硬件。
警告:不要使用通配符兼容值,例如“fsl,mpc83xx-uart”或类似值。半导体供应商总是会做出改变,在改变它的时候为时已晚,打破了你的通配符假设。相反,选择特定的半导体实现并使所有后续芯片与之兼容。
3.2.5 设备如何寻址
可寻址的设备使用以下属性将地址信息编码到设备树中:
-reg
-#address-cells
-#size-cells
每一个可寻址设备都会有一个reg属性,该属性有一个或多个元素组成,其基本格式为:
reg = <address1 length1 [address2 length2] [address3 length3] …>
- 上面的每一个元素都代表设备的寻址地址及其寻址大小,每一个元素中的address值都是一个或者多个无符号32位整形数据类型cell来表示,元素中的length可以为空也可以是一个或多个无符号32位整形数据类型cell。
- 由于每个可寻址设备都会有reg属性可设置,而且reg属性元素也是灵活可选择的,那么谁来指定reg属性元素中每个元素也就是address和length的个数呢?
在此要关注到其父节点的两个属性,其中#address-cells表示reg中address元素的个数,#size-cells表示length元素的个数。
3.2.5.1 CPU寻址地址
对于寻址地址的编写,CPU节点是最简单的一个例子,之前介绍每个CPU节点都包含一个标记ID,但是没有其他的描述信息,这里填充一些其他的属性:
cpus {
#address-cells= <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a9";
reg= <0>;
};
cpu@1 {
compatible = "arm,cortex-a9";
reg= <1>;
};
};
在cpus节点#address-cells赋予1,#size-cells赋予0,。这意味着其子节点的reg只有一个地址元素,没有长度元素值。在此案例中,两个CPU核分别被配成0和1,因为每个CPU只分配了地址,所以节点#size-cells元素被设置为0。
3.2.5.2 内存映射设备
-
需要内存映射的设备不同于上面的CPU节点,这类的设备需要一段内存而不是单一的内存地址,因此不仅需要包含内存的基地址而且还需要映射地址的长度,因此需要使用#size-cells属性来表示reg属性元素中表示地址长度元素的个数。
-
在下面的例子中,每个节点的address值有一个32bit无符号整型数据而且length值也是用一个32bit无符号整型数据来表示。因此在32bit的系统中#address-cells和#size-cells都要设置为1,但是在64bit系统中#address-cells就要设置为2。具体设置如下:
/dts-v1/; / { #address-cells= <1>; #size-cells = <1>; ... serial@101f0000{ compatible= "arm,pl011"; reg =<0x101f0000 0x1000 >; }; serial@101f2000{ compatible= "arm,pl011"; reg =<0x101f2000 0x1000 >; }; gpio@101f3000 { compatible= "arm,pl061"; reg =<0x101f3000 0x1000 0x101f4000 0x0010>; }; interrupt-controller@10140000 { compatible= "arm,pl190"; reg =<0x10140000 0x1000 >; }; spi@10115000 { compatible= "arm,pl022"; reg =<0x10115000 0x1000 >; }; ... };
-
上面的例子中reg属性都有address元素和length元素,值得注意的是,例子中的GPIO被分配了两个地址范围,分别是0x101f3000…0x101f3fff和0x101f4000…0x101f400f。
-
有一些设备可能有不同的寻址方案,比如一个设备挂载到总线上连接一个片选信号线,可通过片选信号选择不同的设备。由于父节点可以定义了其子节点的地址映射域,所以可以选择最适合的一项来描述硬件设备。下面的代码就是把片选号码编入地址码挂载到外部总线上的一个设备。
external-bus { #address-cells= <2> #size-cells = <1>; ethernet@0,0 { compatible = "smc,smc91c111"; reg= <0 0 0x1000>; }; i2c@1,0 { compatible = "acme,a1234-i2c-bus"; reg = <1 0 0x1000>; rtc@58{ compatible = "maxim,ds1338"; }; }; flash@2,0 { compatible= "samsung,k8f1315ebm", "cfi-flash"; reg = <2 0 0x4000000>; }; };
- 上面的代码中,#address-cells属性为2,则表示reg属性的address有两个地址域,其中一个表示片选信号,另一个表示设备到片选基地址的偏移量,#size-cells为1,其地址范围的个数还是一个32bit的无符号整数。所以随后reg有三个属性值,分别表示片选号、偏移量、地址范围。
3.2.5.3 非内存映射设备
-
其他的一些设备,他们在处理器总线上没有内存映射,但他们拥有地址范围但是他们不被CPU直接访问,而是被父设备驱动代替CPU进行访问。
-
以I2C设备为例,每个设备都会有一个指定的访问地址,但是这些设备不会有相关联的范围或者地址长度,这有点类型CPU节点地址分配。如下是具体代码的例子:
i2c@1,0 { compatible = "acme,a1234-i2c-bus"; #address-cells= <1>; #size-cells = <0>; reg =<1 0 0x1000>; rtc@58{ compatible = "maxim,ds1338"; reg= <58>; }; };
3.2.5.4 Ranges(地址转换)
-
之前已经介绍过如何给设备分配地址了,但是这个地址都是设备的本地地址并不一定是CPU可以访问该设备的地址。
根节点是从CPU的角度来描述地址空间,根节点的子节点总是依赖CPU的地址域,所以不需要显示地址映射。比如:serial@101f0000设备描述的地址就是0x101f0000。 -
但是有些节点并不是根节点的直接子节点,因此其不使用CPU地址域。为了可以得到设备的映射地址,设备树必须说明一个域到另一个域的内存地址的转换关系,range属性就此诞生。
下面是一个简单的例子:/dts-v1/; / { compatible ="acme,coyotes-revenge"; #address-cells= <1>; #size-cells =<1>; ... external-bus { #address-cells = <2> #size-cells= <1>; ranges = <0 0 0x10100000 0x10000 // Chipselect 1, Ethernet 1 0 0x10160000 0x10000 // Chipselect 2, i2ccontroller 2 0 0x30000000 0x1000000>; // Chipselect 3, NOR Flash ethernet@0,0{ compatible = "smc,smc91c111"; reg =<0 0 0x1000>; }; i2c@1,0 { compatible = "acme,a1234-i2c-bus"; #address-cells = <1>; #size-cells = <0>; reg =<1 0 0x1000>; rtc@58 { compatible = "maxim,ds1338"; reg= <58>; }; }; flash@2,0 { compatible = "samsung,k8f1315ebm", "cfi-flash"; reg =<2 0 0x4000000>; }; }; };
-
Ranges属性试说明地址转换的一个列表,列表的每一个条目分别表示子节点的地址,父节点的地址,子节点的地址空间的长度(内存空间大小),每个条目的个数分别是用子节点的#address-cells值,父节点#address-cells值,以及子节点#size-cells的值来表示。
-
比如,就上面出显得设备树代码来讲,子节点的#address-cells为2,父节点#address-cells为1,子节点#size-cells 为1,则上面代码中程序先的ranges属性的含义就是:
片选号为0,偏移量为0的设备映射的地址为0x10100000…0x1010ffff
片选号为1,偏移量为0的设备映射的地址为0x10160000…0x1016ffff
片选号为2,偏移量为0的设备映射的地址为0x30000000…0x30ffffff -
此外,假如父节点的地址空间与子节点的地址空间是相同的,可以添加一个空的ranges属性,当你看到一个空的ranges的时候,这意味着子节点的地址与父节点的地址空间是1:1映射的。
-
或许,你会感到困惑,既然映射比例是1:1那么为什么还要地址转换。一些总线(比如PCI总线)用于完全不同的地址空间,然而这种总线需要把具体的地映射范围反应给操作系统。一些可DMA访问的总线设备,操作系统必须知道其真实的地址。有些时候需要把一些共享相同软件可编程物理映射地址的设备分成一个组。是否1:1映射内存地址取决于操作系统需要获取的信息以及对硬件本身的描述。
-
或许细心的你已经注意到,在外部总线里面的i2c@1,0的节点中,并没有添加ranges属性信息。不同于外部总线,i2c中的设备不需要在cpu地址域中进行地址映射,cpu通过访问i2c@1,0来间接的访问rtc@58设备。一个节点缺少ranges属性意味着该设备不需要直接被除了父节点意外的设备访问。
3.2.6 中断如何工作
-
与遵循树的自然结构的地址范围转换不同,中断信号可以源自并终止机器中的任何设备。与在设备树中自然表达的设备寻址不同,对于一般的设备,其地址信息在设备树中自然的被表示出来,中断信号被表现在独立在树的节点之间的连接。四个属性用于描述中断连接:
interrupt-controller:一个空的属性表示该节点描述的设备接受中断信号;
#interrupt-cells:这是一个中断控制器节点属性,该属性类似于#address-cells和#size-cells表示中断控制器包含多个中断描述符;
interrupt-parent:该属性表示该设备节点所连接的中断控制器的句柄,没有interrupt-parent属性的节点也可以从其父节点继承该属性。
interrupts:该属性描述了设备节点包含的一系列的中断描述符,对应于该设备的中断输出信号。 -
一个中断描述符就是一个或者多个无符号32位整形数据的个数(具体的个数在#interrupt-cells中定义),重点描述符表示该设备接受哪些中断输入信号。就像下面设备树代码中的例子,大多数的设备仅仅有一个输出中断,但是有时候一个设备也有可能含有多个输出中断。这意味着一个中断描述符完全依赖绑定在设备上的中断控制器。每个中断控制器可以决定需要多少个cells来描述一个独一无二的输入中断。
下面是一个简单的例子:/dts-v1/; / { 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{ compatible= "arm,pl011"; reg =<0x101f0000 0x1000 >; interrupts= < 1 0 >; }; serial@101f2000{ compatible= "arm,pl011"; reg =<0x101f2000 0x1000 >; interrupts= < 2 0 >; }; gpio@101f3000 { compatible= "arm,pl061"; reg =<0x101f3000 0x1000 0x101f4000 0x0010>; interrupts= < 3 0 >; }; intc:interrupt-controller@10140000 { compatible= "arm,pl190"; reg =<0x10140000 0x1000 >; interrupt-controller; #interrupt-cells = <2>; }; spi@10115000 { compatible= "arm,pl022"; reg =<0x10115000 0x1000 >; interrupts= < 4 0 >; }; external-bus { #address-cells = <2> #size-cells= <1>; ranges =<0 0 0x10100000 0x10000 // Chipselect 1, Ethernet 10 0x10160000 0x10000 // Chipselect 2, i2c controller 20 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>; }; }; };
-
下面几点需要格外的注意:
该评估板仅仅包含一个中断控制器,interrupt-controller@10140000;
标号为“intc:”添加到中断控制器节点中,这个标号主要是被用于根节点的interrupt-parent属性的赋值句柄,除非在根节点的子节点中明确的定义了interrupt-parent属性,否则所有根节点的子节点将继承根节点的interrupt-parent属性;
每个设备都会使用interrupt属性来描述一个独一无二的中断输入线;
#interrupt-cells属性设置为2表示每个中断描述符包含2个cells,一般第一个cells表示表示中断线号,第二个cells表示一个标记号,比如表示该中断是高电平触发、是低电平触发还是电平触发等等。对于所有给定的中断控制器,请参考控制器绑定文档以便获取对象中断编码。
3.2.7 设备特定数据
除了上面出现几个通用含义的属性,在子节点中可以添加任意属性信息。只要属性遵循下面的规矩,添加的任何属性都可被操作系统识别。
首选,新添加的自定义的属性名字需要加上制造商的名字作为前缀以免与标准通用的属性名发生冲突。
其次,每当定义了一个自定义属性都应该在相应的内核文档中加以说明,以便内核开发者可以明天该属性的具体含义。在相应的文档中需要有对该属性的说明,该属性值的含义,该属性可以赋予何值,以及该值的具体含
义。
3.2.8 特殊节点
3.2.8.1 别名节点
引用一个特定的节点通常是通过全路径的方式,比如:
/external-bus/ethernet@0,0,但是这样有缺陷,使用者根本不关系真实路径信息,他们关心的仅仅是哪个是eth0,aliase节点就是将一个全部路径心机简化为一个别名信息,比如:
aliases {
ethernet0 =ð0;
serial0 =&serial0;
};
这里你会发现一个新的语法property = &label;用字符串属性引用一个标号来替代一个完整的路径信息。但是这不同于上面介绍的phandle =< &label >,因为phandle = < &label >是把一个phandle = < &label >,因值插入到一个cell。
3.2.8.2 chosen节点
chosen并不是代表一个真实的设备,仅仅是作为操作系统与固件之间传递数据的一个地方。相当于启动参数、在chosen节点的数据并不代表固件。通常,在。Dts文件中chosen节点为空,在启动的是将会被填充,填充的内容来自于Uboot的bootm等。
在假定的评估板中,固件可填充如下的信息。
chosen {
bootargs ="root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
};
一般有三个地方可以放置内核启动命令:
内核配置的CONFIG_CMDLINE参数
由bootloader传递给内核
在设备树的chosen/bootargs下描述
使用哪一个取决于内核的配置,具体如下:
Kernel command line type (Extend bootloader kernel arguments)
( ) Use bootloader kernel arguments if available——>使用BootLoader启动参数
(X) Extend bootloader kernel arguments ——>使用扩展型的启动参数
( ) Always use the default kernel command string ——>使用内核默认的启动参数