嵌入式设备的引导过程中,通常使用设备树来传递无法被自动探测的板级硬件信息,其作用和PC的ACPI类似。在虚拟化场景下,特定开发板的架构模拟,其硬件信息固定,因此设备树必须固定;对于virt架构的虚拟机,用户可以任务配置硬件信息,因此其设备树是动态生成的。 本文通过qemu自动生成的一份dtb,摘出其中gpio-key和串口芯片pl011硬件信息,分析它在系统引导各个阶段的格式。dtb文件可以通过指定dumpdtb选项生成:/path/to/$qemu_bin -machine virt,dumpdtb=qemu-virt.dtb
,通过fdtdump
工具可以将dtb文件对应的dts源码打印出来:fdtdump qemu-virt.dtb >> qemu-virt.dts
。摘出其中的根结点信息,gpio-key和pl011串口芯片硬件信息如下:
{ /* 根节点信息 */
interrupt-parent = <0x00008001>;
#size-cells = <0x00000002>;
#address-cells = <0x00000002>;
compatible = "linux,dummy-virt";
/* gpio-keys信息 */
gpio-keys {
#address-cells = <0x00000001>;
#size-cells = <0x00000000>;
compatible = "gpio-keys";
poweroff {
gpios = <0x00008003 0x00000003 0x00000000>;
linux,code = <0x00000074>;
label = "GPIO Key Poweroff";
};
};
/* pl011串口芯片信息 */
pl011@9000000 {
clock-names = "uartclk", "apb_pclk";
clocks = <0x00008000 0x00008000>;
interrupts = <0x00000000 0x00000001 0x00000004>;
reg = <0x00000000 0x09000000 0x00000000 0x00001000>;
compatible = "arm,pl011", "arm,primecell";
};
};
DTS
DTS(Device Tree Source),设备树源码,用于表示设备树逻辑信息,DTS的编写规范在设备树规范中定义。设备树由许多节点(node)组成,其基本单位是节点,每个节点有多个键值对用于描述节点设备的属性,除了根节点,每个节点都有一个parent,一个节点下面可以由多个子节点,设备树就是由许多节点组成的节点树。下图是一个设备树的逻辑示例图: 以文章开头的DTS源码为例:
{ /* 1 */
interrupt-parent = <0x00008001>; /* 2 */
#size-cells = <0x00000002>;
#address-cells = <0x00000002>;
compatible = "linux,dummy-virt";
gpio-keys { /* 3 */
#address-cells = <0x00000001>;
#size-cells = <0x00000000>;
compatible = "gpio-keys";
poweroff { /* 4 */
gpios = <0x00008003 0x00000003 0x00000000>;
linux,code = <0x00000074>;
label = "GPIO Key Poweroff";
};
};
pl011@9000000 { /* 5 */
clock-names = "uartclk", "apb_pclk";
clocks = <0x00008000 0x00008000>;
interrupts = <0x00000000 0x00000001 0x00000004>;
reg = <0x00000000 0x09000000 0x00000000 0x00001000>;
compatible = "arm,pl011", "arm,primecell";
};
};
1. root节点,逻辑图中可以用 / 表示
2. root节点的属性,键值对
3. root节点的子节点,逻辑图中可以用/gpio-keys表示
4. gpio-keys节点的子节点,逻辑图中可以用/gpio-keys/poweroff表示
5. root节点的子节点,逻辑图中用/pl011@9000000表示
其逻辑视图如下:
规范
节点名
节点名的格式分成name和address两部分,如下: node-name@unit-address
node-name:节点名 unit-address:节点设备地址,可选,如果节点描述的设备占用了父节点总线的地址空间,可以用这个地址表示空间的起始地址。unit-address的值必须和reg属性的第一个地址相同。
节点路径
设备树是层级结构,不同路径下的节点名可能相同,只有节点路径可以唯一标识一个节点,其格式如下: /node-name-1/node-name-2/node-name-N 比如/gpio-keys/poweroff这个节点路径,表示根节点/下的gpio-keys节点下,有一个子节点poweroff。
属性
属性是描述节点硬件信息的主要部分,它的格式为键值对,由属性名和属性值组成一对键值。
属性名,可以是一个单词,可以用,
隔开的单词组合。用于表示某个厂商定义的的属性,比如linux,code;linux,initrd-start 等等。 属性值,可以有不同类型的属性值,可以是地址,字符串,数组,节点引用等。
节点引用用于引用设备树的其它节点,当一个节点想要被引用时,它创建一个属性名为phandle的键值对声明自己在节点树中的ID,即phandle=<ID>
,其它节点的属性值可以时ID,用来指向被引用节点。 设备树的节点属性分为通用属性和设备相关属性,通用属性主要描述节点的基本信息,设备属性描述设备相关信息。
通用属性
节点的通用属性,其属性名都是约定好的,设备树的发送方和接受方可以通过这些属性确认节点的一些基本信息,下面简单介绍几个常见的通用属性。
compatible
兼容性,其值是字符串,字符串可以由,
隔开,其格式遵循specific到general的原则。兼容性属性提供了一种表达方式,用于表示同一设备驱动兼容多个设备。比如前面提到的串口芯片硬件信息,compatible = "arm,pl011", "arm,primecell"
,它表示这个串口芯片可以兼容arm厂商的pl011芯片和primecell芯片。这个属性向内核传递的信息是,如果没有pl011的设备驱动,那么可以尝试更通用的primecell设备驱动来加载这个芯片。
phandle
引用ID,它的值是一个ID,设备树的其它节点可以使用这个ID来引用使用phandle定义此ID的的节点。比如下面的GIC中断控制器,它被根节点下的interrupt-parent
属性引用。
{
interrupt-parent = <0x00008001>;
intc@8000000 {
phandle = <0x00008001>;
compatible = "arm,gic-v3";
...
}
}
#address-cells,#size-cells
地址宽度和大小宽度。用来表示reg属性中每个地址和大小所占用的宽度。以根节点为例:
{
#size-cells = <0x00000002>;
#address-cells = <0x00000002>;
pl011@9000000 {
compatible = "arm,pl011", "arm,primecell";
reg = <0x00000000 0x09000000 0x00000000 0x00001000>;
...
};
}
表示根节点下面的子节点中,reg属性值的地址宽度是2,长度宽度是2,pl011@9000000作为根节点下面的子节点,因此它的reg属性值中,前两个数值0x00000000 0x09000000
就是地址,后两个数值0x00000000 0x00001000
是长度。
reg
reg属性用来描述设备的地址资源,就是一段地址空间,它的格式是一个数组,数组的每个元素由(address length)
元组构成。
设备属性
DTB
当需要内核引导一块新的目标板时,开发人员首先编写描述目标板硬件信息的DTS文件,然后使用编译工具dtc将的dts转化成DTB。DTB(Device Tree Blob)是设备树编码后的二进制数据,它是bootloader和OS kernel之间传递设备树真正使用的格式。内存中也是该格式传递设备树信息。虚拟化场景下qemu向Linux kernel传递设备树也通过DTB。因此这一章主要分析DTB的格式。
Format
设备树二进制数据格式如下:
header: 用于描述整个dtb二进制文件的布局,并存放其它元数据在dtb文件中的偏移。 memory_reservation_block: 预留内存块,当dtb被加载到内存时,这段区间对应内存被保留用作特殊目的,不可被访问。 struct_block: 设备树结构块,存放设备树的具体内容,结构块由多个特殊token开头的数据组成,是设备树存放数据主要区间。
Header
header的长度为40字节,通过hexdump工具查看如下: 根据设备树规范,其header格式定义如下:
struct fdt_header {
fdt32_t magic; /* magic word FDT_MAGIC */
fdt32_t totalsize; /* total size of DT block */
fdt32_t off_dt_struct; /* offset to structure */
fdt32_t off_dt_strings; /* offset to strings */
fdt32_t off_mem_rsvmap; /* offset to memory reserve map */
fdt32_t version; /* format version */
fdt32_t last_comp_version; /* last compatible version */
/* version 2 fields below */
fdt32_t boot_cpuid_phys; /* Which physical CPU id we're booting on */
/* version 3 fields below */
fdt32_t size_dt_strings; /* size of the strings block */
/* version 17 fields below */
fdt32_t size_dt_struct; /* size of the structure block */
};
1. magic: 魔数
2. totalsize: 总长度,1MB
3. off_dt_struct: 结构块在dtb文件的偏移,0x40
4. off_dt_strings: TODO
5. off_mem_rsvmap: 内存预留块在文件的偏移,0x30
6. version: dtb版本号,17
7. last_comp_version: TODO
8. boot_cpuid_phys: TODO
9. size_dt_strings: TODO
10. size_dt_struct;: 结构块大小,0x1c54(7252bytes)
Memory Reservation Block
内存预留区域用于保留一段内存区间,引导程序在运行过程中不能对这段区间进行读写,这个字段在一些特殊的系统中下被用作存放关键的数据结构。我们不关注这个字段。
Structure Block
Header和Memory Reservation Block可以看做是设备树信息的元数据,Structure Block才是设备树存放信息的主要字段。Structure Block结构简单,由许多段数据组成,每段数据以特殊的token开头,token定义如下:
FDT_BEGIN_NODE(0X00000001):设备树节点信息开始标记。 FDT_END_NODE(0X00000002):设备树节点信息结束标记。FDT_BEGIN_NODE
和FDT_END_NODE
之间可以有多个属性信息。 FDT_PROP(0X00000003):节点属性开始标记。它位于FDT_BEGIN_NODE
和FDT_END_NODE
之间。 FDT_NOP(0X00000004):占位标记。 FDT_END(0X00000009):Structure Block结束标记。
节点属性由键值对表示,每个属性的格式都一样,如下:
struct {
uint32_t len; /* 属性值的长度 */
uint32_t nameoff; /* 属性名在Strings Block中的偏移 */
}
节点的属性名比较特殊,它没有在Structure Block中直接给出,而是放到了String Block中,所有属性名都通过nameoff在String Block中查找。 我们以之前的DTS举例,验证下面DTS信息在DTB中的数据存放规范,这里我们只验证两个属性:
{ /* 根节点信息 */
interrupt-parent = <0x00008001>;
#size-cells = <0x00000002>;
...
}
对应的DTB数据如下:
Strings Block
Strings Block字段主要存放字符串,为Structure Block中的属性提供引用的数据,比如上一小节提到的两个属性的属性名,分别是interrupt-parent
和#size-cells
,它们的值就来自Strings Block。