Booting the Linux/ppc kernel without Open Firmware
- The DT block format
这一章定义了传递给内核的FDT(flattened device tree)的格式。关于它包含的内容以及内核需要的属性将在后续章节描述。
注:DT block应该被加载到主存储器中,内核需要在实模式和保护模式下都能够访问DT block,因此,要求DT block的首地址不能被重新映射。Boot loader负责将DT block加载到内存,然后将它的首地址传递给内核。
1.1 Header
Boot loader将Device Tree传递给内核的时候,实际上是传递的DT在内存中的物理地址,开始的部分是DT的头部信息,由inlude/linux/of_fdt.h中的struct boot_param_header结构体描述:
318struct boot_param_header {
319 u32 magic; /* magic word OF_DT_HEADER */
320 u32 totalsize; /* total size of DT block */
321 u32 off_dt_struct; /* offset to structure */
322 u32 off_dt_strings; /* offset to strings */
323 u32 off_mem_rsvmap; /* offset to memory reserve map
324 */
325 u32 version; /* format version */
326 u32 last_comp_version; /* last compatible version */
328 /* version 2 fields below */
329 u32 boot_cpuid_phys; /* Which physical CPU id we're
330 booting on */
331 /* version 3 fields below */
332 u32 size_dt_strings; /* size of the strings block */
334 /* version 17 fields below */
335 u32 size_dt_struct; /* size of the DT structure block */
336};
与此同时,有一些常量被定义,用来描述头部信息,包括:
341#define OF_DT_HEADER 0xd00dfeed /* 4: version,
342 4: total size */
343#define OF_DT_BEGIN_NODE 0x1 /* Start node: full name
344 */
345#define OF_DT_END_NODE 0x2 /* End node */
346#define OF_DT_PROP 0x3 /* Property: name off,
347 size, content */
348#define OF_DT_END 0x9
所有Header中的值都是采用大端模式存储在磁盘上的,以下的部分会详细介绍Header中各属性的意义。需要注意的是,所有的offset值都是从Header开始的偏移(以字节为单位),也就是DT Block的物理地址。
magic属性标识了DT Block头部的开始,它的值为0xd00dfeed,也就是宏定义OF_DT_HEADER表示的值。
totalsize表示包含DT Header的所有DT Block的大小,DT block包含了所有的数据结构,如设备树结构、字符串集以及内存保留区等等。
off_dt_struct表示从DT Header到DT Block中structure区域的偏移地址。Off_dt_strings表示从DT Header到DT Block中strings区域的偏移地址。
off_mem_rsvmap表示从DT Header到reserved memory map区域的偏移地址。内存保留区块图是一组64位的整数对,每一对整数包含一个物理地址和大小,这组数据构成的列表的最后一个节点是一个大小为0的整数对。内存保留区块图是用来告诉内核在进行内存分配的时候不要占用的内存区域,特别是在内核早期初始化过程中。这样我们就可以把类似于DT Block,ramdisk等不希望被内核其他模块覆盖的信息存放在内存中,供内核后续使用。
version表示DT的版本号。版本1已停用,版本2增加了boot_cpuid_phys区域,版本3增加了strings区块的大小并允许内核重定位它。当前正在使用的是版本17,它增加了size_dt_struct字段。
last_comp_version表示向前兼容的最低版本。
boot_cpuid_phys字段仅仅在版本2中出现,表示物理CPU ID。如果你使用多处理器系统,那么这个字段应该与CPU节点的reg属性匹配。
size_dt_strings字段在版本3及其以后的版本中出现,表示strings区块的大小。
size_dt_struct字段在版本17及其以后版本中出现,表示structure区块的大小。
因此,典型的DT Block的布局图应该如下:
其中,alignment gap区域并没有必要存在,他们是否存在和大小独立于各数据块的对齐需求。
1.2 Device Tree Generalities
设备树本身被区分为structure和strings两个区块,每个区块都是以4字节对齐的。设备树的布局遵循IEEE 1275设备树规范。从本质上讲,它是一个包含很多节点的树形结构,每个节点有两个或多个有名属性,每个属性可以有值或者不需要值。另外,因为它是一个树形结构,所以每一个除了root节点之外的节点都有且仅有一个父节点,root节点没有父节点。
一个节点有两个名称,真实的节点名通常由节点的name类型的属性描述,在版本1到版本3之间,该属性必须指定。从版本16开始,该属性变为可选的,但是如果不显式指定的话,在编译的过程中会使用unit name指定该属性。
Unit name用来区别在同一层级使用同样的名称的节点,它的通常形式是node_name@unit_address,unit address通常是指设备在被挂载的总线上的地址。Unit name并不以其自身的一个属性的形式存在,而是作为DT结构的一部分。它通常用来代表设备节点在DT中的路径。内核通用代码通常不会使用unit address(尽管一些板级支持代码会使用),所以unit address在这里的唯一需求是保证unit name在同一层级的唯一性。没有任何地址概念的节点,以及一些没有相邻节点的节点(例如/cpu或者/memory)通常省略掉unit address,或者使用@0来表示。unit name用来定义节点的完整路径,完整的路径是从根结点开始的所有父节点的串联,以/符号分隔,这一点有点类似于Linux文件系统中的路径。根结点没有unit name也没有unit address,其路径就是”/”。
每一个节点都代表一个真实存在的设备。每一个节点都可以被其他节点的属性引用,但是这需要我们为被引用的节点定义一个”phandle”或者”linux,phandle”类型的属性。”phandle”属性是一个32位的数值,用来唯一标识一个节点,唯一的要求是在DT中所有的phandle属性的数值不能重复。
接下来我们看一个简单的实例:
564 / o device-tree
565 |- name = "device-tree"
566 |- model = "MyBoardName"
567 |- compatible = "MyBoardFamilyName"
568 |- #address-cells = <2>
569 |- #size-cells = <2>
570 |- linux,phandle = <0>
571 |
572 o cpus
573 | | - name = "cpus"
574 | | - linux,phandle = <1>
575 | | - #address-cells = <1>
576 | | - #size-cells = <0>
577 | |
578 | o PowerPC,970@0
579 | |- name = "PowerPC,970"
580 | |- device_type = "cpu"
581 | |- reg = <0>
582 | |- clock-frequency = <0x5f5e1000>
583 | |- 64-bit
584 | |- linux,phandle = <2>
585 |
586 o memory@0
587 | |- name = "memory"
588 | |- device_type = "memory"
589 | |- reg = <0x00000000 0x00000000 0x00000000 0x20000000>
590 | |- linux,phandle = <3>
591 |
592 o chosen
593 |- name = "chosen"
594 |- bootargs = "root=/dev/sda2"
595 |- linux,phandle = <4>
其中,以双引号包含的代表结束符为’0’的字符串,中括号包含的代表数值,如果是十六进制数值则以0x开头。
1.3 Device Tree “structure” block
DT中的structure区块是一个线性的树形结构。它使用”OF_DT_BEGIN_NODE”标记一个节点的开始,”OF_DT_END_NODE”标记节点的结束。子结点的定义在父节点的OF_DT_END_NODE之前。OF_DT_BEING_NODE,OF_DT_END_NODE,OF_DT_END等标记都是32位的数值。OF_DT_END标记出现在structure区块的结尾。
一个节点的内容可以概括为起始标记,完整路径,属性列表,子结点列表,结束标记。每一个子结点的结构也如同上述描述。这个地方要注意的是,属性列表最好处在子结点列表之前。
1.4 Device Tree “strings” block
因为属性名称通常是冗长的字符串,为了节省空间,DT单独把它们存放在strings区块中,这个区块是所有属性名称的字符串(结束符为’0’)串联在一起构成的,定义在structure区块中的属性会包含属性名称字符串的偏移地址(从strings区块开始处的偏移地址)。
- Required content of the device tree
2.1 Note about cells and address representation
通常,设备地址的格式由父总线类型基于#address-cells和#size-cells属性定义。这里要注意的是,子结点不会继承父节点的父节点的#address-cells和#size-cells属性,所以拥有子结点的子结点必须自己指定这两个属性值。内核要求根结点为直接映射到处理器总线上的设备定义设备地址格式,也是通过指定这两个属性值。
一个cell表示一个32位数值,#address-cells表示设备地址的位长,#size-cells表示设备大小的位长。如果这两个属性值都为2,表示分别由两个64位数值组成(大端模式)。
“reg”属性通常是一个”address size”形式的元组,其中address和size的数据长度则由上述两个属性值确定。当总线支持不同的地址空间和对应地址空间的标志时,这些标志通常添加在物理地址的最高几位上。例如,一个PCI总线的物理地址由三个单元组成,底部两个单板包括真实的物理地址,顶部的一个单元包括地址空间标识符,标志位,总线号和设备号。
对于支持动态分配设备地址的总线,通常不通过reg属性指定设备地址,而是首先通过一个标志指明设备地址是动态分配的。接下来,提供一个单独的”assigned-addresses”属性指定分配的设备地址。这一点可以参考PCI bindings。
通常,一个没有地址空间位和动态地址分配机制的总线是内核希望看到的,因为这样内核就不需要为bus_type定义单独的地址转换函数。
“reg”属性仅仅是在一个给定的总线中的定义设备地址和设备寄存器组大小。为了将设备地址向更高一级的总线地址转换(进入总线地址空间),或者进入更高级别的CPU物理地址空间,所有的总线都必须包含一个”ranges”属性。如果ranges属性没有指定,那么就假设不存在地址转换机制。例如,设备的寄存器组对于总线而言就是不可见的。总线的ranges属性的格式如下:
bus address, parent bus address, size
“bus address”的格式在总线节点定义时就确定好了,对于一个PCI桥设备而言,它就是PCI地址空间地址。因此,(bus address, size)定义了一组子设备的地址簇。”parent bus address”的格式是由总线节点的父总线节点规定的。例如,对于一个PCI主控制器,它就是CPU物理地址空间地址;对于PCI-ISA桥设备,它就是一个PCI地址空间地址。”parent bus address”定义了设备地址簇在父总线地址空间中映射的基址。
“ranges”属性也可以为空,表示设备寄存器组对于父总线是可见的,但是使用一致的映射机制。换句话说,就是父总线地址空间和子总线地址空间一样。
2.2 Note about “compatible” propertites
这个属性是可选的,但是强烈建议在设备结点和根结点中添加这个属性。”compatible”属性的格式是一组串联的以’0’为结束符的字符串。它们表示设备兼容的一系列相似的设备,在一些情况下,可以让一个驱动匹配多个设备,不管它们的真实名称是不是一样。
2.3 Note about “name” propertites
早期的使用者使用设别的真实名称来填充”name”属性,但是现在,使用类似于设备类(通常是device_type)的名称来填充”name”属性被普遍接受并推崇。举例说明,网卡控制器命名为”ethernet”,然后利用一个额外的”model”属性定义精确的芯片类别/型号,并使用”compatible”属性定义兼容的一类设备,这样可以让一个驱动支持多种同类的设备。与此同时,内核通常不会严格限制”name”属性的值,这种尝试渐渐地变成了一种规范的操作。
另外,在版本16中,将name属性设置为可选属性。如果没有显示地指定name属性,那么unit name就被重构为name属性,也就是说unit name中在@符号之前的部分被指定为name属性值。
2.4 Note about node and property names and character set
节点和属性必须使用ASCII码a~z, 0~9,逗号,顿号,下划线,加号,#号,问号和连字符。另外,节点名还可以使用ASCII码A~Z。属性名必须为小写字母。节点名和属性名的最大字符串长度都是31。
2.5 Required nodes and propertites
接下来介绍的是当前需要的所有bindings。我们强烈建议用户将自定义的bindings以文档的形式输出到Open Firmware。
a) The root node
根结点需要定义以下属性:
-mode: 平台名/型号
-#address-cells:根结点子设备地址表示方式
-#size-cells:根结点子设备大小表示方式
-compatible:兼容的平台
在根结点下,你也可以自定义一些平台特殊属性,例如序列号等。如果你有些需要定制的属性,可以以vendor_name,custom_propertity_name的形式指定,这样就不会导致不同平台混淆。
b) The /cpus node
这个节点是所有独立CPU节点的父节点。它并没有任何特殊的属性,但是我们通常会指定#address-cells和#size-cells属性。通常为:
#address-cells = <00000001>
#size-cells = <00000000>
它表示使用32位数值表示CPU地址,但是没有表示大小(无意义)。内核在读取CPU节点的reg属性时,会假设按照这个格式来解析。
c) The /cpus/* nodes
在/cpus节点下,我们可以为机器上的每一个CPU定义一个节点。没有严格的规定name属性,但是通常命名为<architecture>,<core>。例如,苹果使用PowerPC, G5来命名,而IBM使用PowerPC,970FX来命名。从中我们看出,通常我们使用兼容的CPU架构名和指定的CPU型号来命名name属性。
必需的属性:
-device_type:只能为”cpu”
-reg:物理CPU数目,它通常是一个32位的数值,也用来和unit name一起构成cpu节点的完整路径。例如,一个有两个CPU的平台,CPU节点完整路径如下:
/cpus/PowerPC,970FX@0
/cpus/PowerPC,970FX@1
-d-cache-block-size: 32位数值,表示L1数据缓存区块大小(字节)
-i-cache-block-size: 32位数值,表示L1指令缓存区块大小(字节)
-d-cache-size:32位数值,表示L1数据缓存大小
-i-cache-size:32位数值,表示L1指令缓存大小
其中block大小指缓存管理的存储单元大小,即一次操作多少个字节的数据。
可选属性:
-timebase-frequency:频率值
-clock-frequency:CPU时钟频率值
-d-cache-line-size:跟d-cache-block-size一样,只是为了兼容以前的定义
-i-cache-line-size: 跟i-cache-block-size一样,只是为了兼容以前的定义
d) The /memory node(s)
为了描述物理内存布局,我们需要定义一个或者多个momory节点。你可以在一个memory节点里面用reg属性指定多个memory ranges。也可以定义多个memory节点分别描述。构成memory节点完整路径的unit addres是由第一个定义memory range的memory节点决定的。如果使用单个memory节点,那么通常为@0。
必需的属性:
-device_type:必须为”memory”
-reg:定义memory ranges, 是一系列address/sizes对串联在一起的,address和size的格式由根结点的#address-celles和#size-cells定义。例如:
00000000 00000000 00000000 80000000
00000001 00000000 00000001 00000000
第一个range表示从0地址开始的0x80000000个字节,第二个range表示从0x100000000开始的0x100000000个字节。(#address-cells和#size-cells都为2)可以看到,其中不包括2GB~4GB的内存空间,内核要把这一段内存地址空间做其他用途。
e) The /chosen node
这个节点通常用来存放一些可变的环境变量,例如内核参数,默认输入输出设备等。
必需的属性:
-bootargs:内核命令行参数
-linux,stdout-path:终端控制台设备的完整路径
f) The /soc<SoCname> node
这个节点用来表示一个SoC,如果使用一款SoC作为处理器,那么必须指定这个节点。Soc节点的顶层属性包含对于SoC上所有设备都有效的信息。Soc节点名应该包含unit address,是SoC的内存映射寄存器组的基址。Soc节点名要以”soc”开头,剩下的部分用soc型号来填充。例如,MPC8540的soc节点名可以定义为”soc8054”。
必需的属性:
-ranges:应该被指定为1,描述内存映射寄存器组的地址转换方式。
-bus-frequency:包含soc节点的总线工作频率,通常,这个属性值由boot loader填充。
-compatible: soc的准确型号
可选的属性:
-reg:soc寄存器组的地址和大小格式。
-#address-cells:指定soc子设备的地址格式。
-#size-cellsL指定soc子设备的大小格式。
-#interrupt-cells:定义表示中断的数据位宽。通常这个值为2,其中32位表示中断号,另外32位表示中断检测位和中断级别。(interrupt sense and level)
SOC节点可能包含每个SOC设备的子结点,存在在SOC上但是被特定平台使用的节点不能定义在soc节点下。后面章节会详细介绍如何准确地定义SoC上的设备节点。
MPC8540平台的soc节点描述如下:
soc8540@e0000000 {
#address-cells = <1>;
#size-cells = <1>;
#interrupt-cells = <2>;
device_type = "soc";
ranges = <0x00000000 0xe0000000 0x00100000>
reg = <0xe0000000 0x00003000>;
bus-frequency = <0>;
}
这篇文章是http://lxr.linux.no/linux+v3.19.1/Documentation/devicetree/booting-without-of.txt的翻译,但是关于DTC编译器的编译规则和一些SoC下子设备节点的定义规则没有翻译完,不过这部分的信息可以从内核Documents里面的bindings中找到。