linux内核机制之设备树
1. 设备树(Device Tree)基本概念及作用在内核源码中,存在大量对板级细节信息描述的代码。这些代码充斥在/arch/arm/plat-xxx和/arch/arm/mach-xxx目录,对内核而言这些platform设备、resource、i2c_board_info、spi_board_info以及各种硬件的platform_data绝大多数纯属垃圾冗余代码。为了解
1. 设备树(Device Tree)基本概念及作用
在内核源码中,存在大量对板级细节信息描述的代码。这些代码充斥在/arch/arm/plat-xxx和/arch/arm/mach-xxx目录,对内核而言这些platform设备、resource、i2c_board_info、spi_board_info以及各种硬件的platform_data绝大多数纯属垃圾冗余代码。为了解决这一问题,ARM内核版本3.x之后引入了原先在Power PC等其他体系架构已经使用的Flattened Device Tree。
“A data structure by which bootloaders pass hardware layout to Linux in a device-independent manner, simplifying hardware probing.”开源文档中对设备树的描述是,一种描述硬件资源的数据结构,它通过bootloader将硬件资源传给内核,使得内核和硬件资源描述相对独立(也就是说*.dtb文件由Bootloader读入内存,之后由内核来解析)。
本质上,Device Tree改变了原来用code方式将HW配置信息嵌入到内核代码的方法,改用bootloader传递一个DB的形式。对于嵌入式系统,在系统启动阶段,bootloader会加载内核并将控制权转交给内核,此外,还需要把上述的三个参数信息传递给kernel,以便kernel可以有较大的灵活性。在linux kernel中,Device Tree的设计目标就是如此。
在devie tree中,可描述的信息包括:
1、CPU的数量和类别
2、内存基地址和大小
3、总线和桥
4、外设连接
5、中断控制器和中断的使用情况
6、GPIO控制器和GPIO使用情况
7、clock控制器和clock使用情况
另外,设备树对于可热插拔的热备不进行具体描述,它只描述用于控制该热插拔设备的控制器。
设备树的主要优势:对于同一SOC的不同主板,只需更换设备树文件.dtb即可实现不同主板的无差异支持,而无需更换内核文件。
它基本就是一棵电路板上的CPU、总线、设备组成的树,Bootloader会将这棵树传递给内核,然后内核来识别这棵树,并根据它展开出Linux内核中的platform_device、i2c_client、spi_device等设备,而这些设备用到的内存、IRQ等资源,也被传递给内核,内核会将这些资源绑定给展开的相应设备
设备树包含DTC(device tree compiler),DTS(device tree source和DTB(device tree blob)。其对应关系如图1-1所示:
图1-1 DTS、DTC、DTB之间的关系
2.1. DTS和DTSI
.dts文件是一种ASCII文本对Device Tree的描述,放置在内核的/arch/arm/boot/dts目录。一般而言,一个.dts文件对应一个ARM的machine。
.dtsi文件作用:由于一个SOC可能有多个不同的电路板,而每个电路板拥有一个 .dts。这些dts势必会存在许多共同部分,为了减少代码的冗余,设备树将这些共同部分提炼保存在.dtsi文件中,供不同的dts共同使用。.dtsi的使用方法,类似于C语言的头文件,在dts文件中需要进行include *.dtsi文件。当然,dtsi本身也支持include 另一个dtsi文件。
2.2. DTC
DTC为编译工具,它可以将.dts文件编译成.dtb文件。DTC的源码位于内核的scripts/dtc目录,内核选中CONFIG_OF,编译内核的时候,主机可执行程序DTC就会被编译出来。 即scripts/dtc/Makefile中
hostprogs-y := dtc
always := $(hostprogs-y)
在内核的arch/arm/boot/dts/Makefile中,若选中某种SOC,则与其对应相关的所有dtb文件都将编译出来。在linux下,make dtbs可单独编译dtb。以下截取了TEGRA平台的一部分。
ifeq ($(CONFIG_OF),y)
dtb-$(CONFIG_ARCH_TEGRA) += tegra20-harmony.dtb \
tegra30-beaver.dtb \
tegra114-dalmore.dtb \
tegra124-ardbeg.dtb
2.3. DTB
DTC编译*.dts生成的二进制文件(.dtb),bootloader在引导内核时,会预先读取.dtb到内存,进而由内核解析。
2.4. Bootloader
Bootloader需要将设备树在内存中的地址传给内核。在ARM中通过bootm或bootz命令来进行传递。bootm [kernel_addr] [initrd_address] [dtb_address],其中kernel_addr为内核镜像的地址,initrd为initrd的地址,dtb_address为dtb所在的地址。若initrd_address为空,则用“-”来代替。
3. 设备树中dts、dtsi文件的基本语法
3.1 DTS基本语法
3.1.1DTS的基本结构
DTS的基本语法范例,如图3-1 所示。
它包括一系列节点,以及描述节点的属性。
“/”为root节点。在一个.dts文件中,有且仅有一个root节点;在root节点下有“node1”,“node2”子节点,称root为“node1”和“node2”的parent节点,除了root节点外,每个节点有且仅有一个parent;其中子节点node1下还存在子节点“child-nodel1”和“child-node2”。
注:如果看过内核/arch/arm/boot/dts目录的读者看到这可能有一个疑问。在每个.dsti和.dts中都会存在一个“/”根节点,那么如果在一个设备树文件中include一个.dtsi文件,那么岂不是存在多个“/”根节点了么。其实不然,编译器DTC在对.dts进行编译生成dtb时,会对node进行合并操作,最终生成的dtb只有一个root node。Dtc会进行合并操作这一点从属性上也可以得到验证。这个稍后做讲解。
在节点的{}里面是描述该节点的属性(property),即设备的特性。它的值是多样化的:
1.它可以是字符串string,如①;也可能是字符串数组string-list,如②
2.它也可以是32 bit unsigned integers,如cell⑧,整形用<>表示
3.它也可以是binary data,如③,十六进制用[]表示
4.它也可能是空,如⑦
图3-1 DTS的基本语法范例
在/arch/arm/boot/dts/目录中有一个文件skeleton.dtsi,该文件为各ARM vendor共用的一些硬件定义信息。以下为skeleton.dtsi的全部内容。
/ {
#address-cells = <1>;
#size-cells = <1>;
chosen { };
aliases { };
memory { device_type = “memory”; reg = <0 0>; };
};
如上,属性# address-cells的值为1,它代表以“/”根节点为parent的子节点中,reg属性中存在一个address值;#size-cells的值为1,它代表以“\” 根节点为parent的子节点中,reg属性中存在一个size值。即父节点的# address-cells和#size-cells决定了子节点的address和size的长度;Reg的组织形式为reg =<>
由于其他设备节点依据属性进行描述,具有类似的形式。接下来的部分主要分析各种属性的含义及作用,并结合相关的例子进行阐述。
在device node 中,reg是描述memory-mapped IO register的offset和length。子节点的reg属性address和length长度取决于父节点对应的#address-cells和#size-cells的值。例:
在上述的aips节点中,存在子节点spda。spda中的中reg为<0x70000000 0x40000 >,其0x700000000为address,0x40000为size。这一点在图3-1下有作介绍。
这里补充的一点是:设备节点的名称格式node-name@unit-address,节点名称用node-name唯一标识,为一个ASCII字符串。其中@unit-address为可选项,可以不作描述。unit-address的具体格式和设备挂载在哪个bus上相关。如:cpu的unit-address从0开始编址,以此加1;本例中,aips为0x70000000。
在①中,compatible属性为string list,用来将设备匹配对应的driver驱动,优先级为从左向右。本例中spba的驱动优先考虑“fsl,aips-bus”驱动;若没有“fsl,aips-bus”驱动,则用字符串“simple-bus”来继续寻找合适的驱动。即compatible实现了原先内核版本3.x之前,platform_device中.name的功能,至于具体的实现方法,本文后面会做讲解。
注:对于“/”root节点,它也存在compatible属性,用来匹配machine type。具体说明将在后面给出。
设备节点通过interrupt-parent来指定它所依附的中断控制器,当节点没有指定interrupt-parent时,则从parent节点中继承。上面例子中,root节点的interrupt-parent = <&mic>。这里使用了引用,即mic引用了②中的inrerrupt-controller @40008000;root节点的子节点并没有指定interrupt-controller,如ahb、fab,它们均使用从根节点继承过来的mic,即位于0x40008000的中断控制器。
若子节点使用到中断(中断号、触发方法等等),则需用interrupt属性来指定,该属性的数值长度受中断控制器中#inrerrupt-controller值③控制,即interrupt属性<>中数值的个数为#inrerrupt-controller的值;本例中#inrerrupt-controller=<2>,因而④中interrupts的值为<0x3d 0>形式,具体每个数值的含义由驱动实现决定。
3.4.4. ranges属性
ranges属性为地址转换表,这在pcie中使用较为常见,它表明了该设备在到parent节点中所对用的地址映射关系。ranges格式长度受当前节点#address-cell、parent节点#address-cells、当前节点#size-cell所控制。顺序为ranges=<前节点#address-cell, parent节点#address-cells , 当前节点#size-cell。在本例中,当前节点#address-cell=<1>,对应于⑤中的第一个0x20000000;parent节点#address-cells=<1>,对应于⑤中的第二个0x20000000;当前节点#size-cell=<1>,对应于⑤中的0x30000000。即ahb0节点所占空间从0x20000000地址开始,对应于父节点的0x20000000地址开始的0x30000000地址空间大小。
注:对于相同名称的节点,dtc会根据定义的先后顺序进行合并,其相同属性,取后定义的那个。
3.2 标准属性
3.2.1 compatible
每一个dts文件都是由一个root的根节点组成,内核通过根节点“/”的兼容性即可判断它启动的是什么设备,其代码结构如下
/ {
model = "Spreadtrum SC9830A-5 V1.0.0 Smartphone Board";
compatible = "sprd,sp9830a-5h10-ga1", "sprd,sc9830";
chosen {
bootargs = "earlycon=sprd_serial,0x70100000,115200n8 loglevel=8 console=ttyS1,115200n8 init=/init root=/dev/ram0 rw androidboot.hardware=sc9830";
linux,initrd-start = <0x85500000>;
linux,initrd-end = <0x855a3212>;
};
};
model属性值是,它指定制造商的设备型号。推荐的格式是:“manufacturer,model”,其中manufacturer是一个字符串描述
制造商的名称,而型号指定型号。
compatible属性值是,指定了系统的名称,是一个字符串列表,它包含了一个“<制造商>,<型号>”形式的字符串。重要的是要指定一个确切的设备,并且包括制造商的名字,以避免命名空间冲突。
chosen 节点不代表一个真正的设备,但功能与在固件和操作系统间传递数据的地点一样,如根参数,取代以前bootloader的启动参数,控制台的输入输出参数等
3.1.2 #address-cells和#size-cells
- #address-cells = <1>: 基地址、片选号等绝对起始地址所占字长,单位uint32
- #size-cells = <1>: 长度所占字长,单位uint32
soc {
#address-cells = <1>;
#size-cells = <1>;
serial {
compatible = "ns16550";
reg = <0x4600 0x100>;
clock-frequency = <0>;
interrupts = <0xA 0x8>;
interrupt-parent = <&ipic>;
};
};
3.1.3 CPU addressing
在讨论寻址时,CPU节点代表了最简单的情况。 每个CPU都分配有一个唯一的ID,并且没有与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值是单独的uint32,它用无大小字段表示地址。在此情况下,这两个cpu分配到的地址为0和1。Cpu节点的#size-cells是0因为每个cpu只分配到了一个单独的地址。
你仍然需要注意reg值班需要与节点名的值相匹配。按照惯例,如果一个节点有一个reg属性,那么这个节点名称必须包括unit-address,这是reg属性的第一个address值。
3.1.4 Memory Mapped Devices
与在cpu节点中单独的address值不同,内存映射设备被分配了一系列将要响应的地址,因此不仅需要包含内存的基地址而且还需要映射地址的长度,因此需要使用#size-cells用来表示在每个子reg元组中长度字段的大小。在以下示例中,每个address值为1 cell(32 bits),每个长度值也是1 cell,这在32 bit系统是比较典型的。64 bit设备也许会为#address-cells和#size-cells使用数值2,在device tree中获取64 bit addressing。
/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 >;
};
...
};
3.1.5 Non Memory Mapped Devices
处理器总线的其它设备为非内存映射设备。他们有地址范围,但不能被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.1.6 Ranges (Address Translation)
我们已经讨论过如何向设备分配地址,但此时这些地址只是本地设备节点,还没有说明如何从那些地址里映射到cpu可以使用的地址。根节点经常描述地址空间的CPU视图。根节点的子节点已经使用了CPU的address domain,所以不需要任何明确的映射。例如,serial@101f0000设备被直接分配了地址0x101f0000。
根节点的非直接子节点是无法使用CPU的address domain的。为了在deivce tree获取内存映射地址必须指定如何从一个域名将地址转换到另一个。Ranges属性就用于此目的。以下是添加了ranges属性的device tree示例。
/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, i2c controller
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是一个地址转换列表。每个输入ranges表格的是包含子地址的元组,母地址和子地址空间的范围大小。每个字段的大小都由获取的子地址的#address-cells值,母地址的#address-cell值和子地址的#size-cells值而定。以外部总线为例,子地址是2 cells,母地址是1 cell,大小也为1 cell。转换三个ranges:
- Offset 0 from chip select 0 is mapped to address range 0x10100000…0x1010ffff
- Offset 0 from chip select 1 is mapped to address range 0x10160000…0x1016ffff
- Offset 0 from chip select 2 is mapped to address range 0x30000000…0x30ffffff
例如上面的总线是有片选的,就需要描述片选及片选的偏移量,在说明地址时,还需要说明地址映射范围
3.1.7 status
device tree中的status标识了设备的状态,使用status可以去禁止设备或者启用设备,看下设备树规范中的status可选值
value | Description |
---|---|
“okay” | 表示设备正在运行 |
“disabled” | 表示该设备目前尚未运行,但将来可能会运行 |
“fail” | 表示设备无法运行。 在设备中检测到严重错误,确实如此没有修理就不可能投入运营 |
“fail-sss” | 表示设备无法运行。 在设备中检测到严重错误,它是没有修理就不可能投入运营。 值的sss部分特定于设备并指示检测到的错误情况 |
3.2.reserved-memory
reserved-memory:保留内存,顾名思义就是被保留下来的内存,它不被添加到系统的内存管理中,通常给驱动使用,由驱动开发人员自己进行划分和地址映射:如下:
/ {
#address-cells = <1>;
#size-cells = <1>;
memory {
reg = <0x40000000 0x40000000>;
};
reserved-memory {
#address-cells = <1>;
#size-cells = <1>;
ranges;
/* global autoconfigured region for contiguous allocations */
linux,cma {
compatible = "shared-dma-pool";
reusable;
size = <0x4000000>;
alignment = <0x2000>;
linux,cma-default;
};
display_reserved: framebuffer@78000000 {
reg = <0x78000000 0x800000>;
};
multimedia_reserved: multimedia@77000000 {
compatible = "acme,multimedia-memory";
reg = <0x77000000 0x4000000>;
};
};
/* ... */
fb0: video@12300000 {
memory-region = <&display_reserved>;
/* ... */
};
scaler: scaler@12500000 {
memory-region = <&multimedia_reserved>;
/* ... */
};
codec: codec@12600000 {
memory-region = <&multimedia_reserved>;
/* ... */
};
};
进行内存区域的保留,上面代码中展示了两种方式
-
static allocation
reg (required) - standard definition -
dynamic allocation
size (required) - length based on parent’s #size-cells
- Size in bytes of memory to reserve.
alignment (optional) - length based on parent’s #size-cells
- Address boundary for alignment of allocation.
alloc-ranges (optional) - prop-encoded-array (address, length pairs).
- Specifies regions of memory that are
acceptable to allocate from.
If both reg and size are present, then the reg property takes precedence
and size is ignored.
在《devicetree-specification-v0.3》提供了保留内存的另一只方案:
Memory reservations define an entry for the devicetree blob’s memory reservation table. They have the form: e.g.,
/memreserve/ <address> <length>; Where <address> and <length> are 64-bit C-style integers.
3.3. chosen node
chosen {
bootargs = “tegraid=40.0.0.00.00 vmalloc=256M video=tegrafb console=ttyS0,115200n8 earlyprintk”;
};
chosen node 主要用来描述由系统指定的runtime parameter,它并没有描述任何硬件设备节点信息。原先通过tag list传递的一些linux kernel运行的参数,可以通过chosen节点来传递。如command line可以通过bootargs这个property来传递。如果存在chosen node,它的parent节点必须为“/”根节点。
3.4 特殊节点
aliases节点为了解决节点路径名过长的问题,引入了节点别名的概念,可以引用到一个全路径的节点。如/external-bus/ethernet@0,0,但当用户想知道具体内容的时候显得太累赘,“哪个设备是eth0?”
aliases {
ethernet0 = ð0;
serial0 = &serial0;
};
当为设备分配一个标识符的时候,操作系统更倾向于使用aliases
3.5中断映射
与遵循树的自然结构而进行的地址转换不同,机器上的任何设备都可以发起和终止中断信号。另外地址的编址也不同于中断信号,前者是设备树的自然表示,而后者者表现为独立于设备树结构的节点之间的链接。 下图显示了设备的自然结构以及每个节点在逻辑中断树中的位置。
上图包括以下部分:
-
open-pic中断控制器是中断树的根
-
中断树根有三个子设备,它们将中断直接路由到open-pic
- device1
- PCI host controller
- GPIO Controller
-
存在三个中断域; 一个以开放式pic节点为根,一个在PCI主桥节点,一个在GPIO Controller节点上
-
有两个nexus节点; 一个位于PCI主桥,一个位于GPIO控制器。
属性 | 属性值 | 描述 |
---|---|---|
#interrupts | prop-encoded-array | 一个设备节点属性,该属性主要描述了中断的HW interrupt ID以及类型 |
#interrupt-parent | phandle | 该属性主要描述了该设备的interrupt request line连接到哪一个interrupt controller,那些没有 interrupt-parent 的节点则从它们的父节点中继承该属性 |
#interrupts-extended | phandle prop-encoded-array | 列出了设备生成的中断,当设备连接到多个中断控制器 |
#interrupt-cells | u32 | 这是中断控制器节点的属性,用来标识这个控制器需要几个单位做中断描述符(类似于 #address-cells 和 #size-cells),则子节点的interrupts一个cell三个32bits整型值: <中断域 中断 触发方式> |
#interrupt-controller | empty | 一个空属性用来声明这个node接收中断信号 |
下面显示了具有PCI总线控制器和采样中断的设备片段
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
open-pic {
clock-frequency = <0>;
interrupt-controller;
#address-cells = <0>;
#interrupt-cells = <2>;
};
pci {
#interrupt-cells = <1>;
#size-cells = <2>;
#address-cells = <3>;
interrupt-map-mask = <0xf800 0 0 7>;
interrupt-map = <
/ * IDSEL 0x11 - PCI slot 1* /
0x8800 0 0 1 &open-pic 2 1 / * INTA* /
0x8800 0 0 2 &open-pic 3 1 / * INTB* /
0x8800 0 0 3 &open-pic 4 1 / * INTC* /
0x8800 0 0 4 &open-pic 1 1 / * INTD* /
/ * IDSEL 0x12 - PCI slot 2* /
0x9000 0 0 1 &open-pic 3 1 / * INTA* /
0x9000 0 0 2 &open-pic 4 1 / * INTB* /
0x9000 0 0 3 &open-pic 1 1 / * INTC* /
0x9000 0 0 4 &open-pic 2 1 / * INTD* /
>;
};
};
4. DTB相关结构
本节讲下.dts编译生成的dtb文件,其布局结构。
DTB由三部分组成:头(Header)、结构块(device-tree structure)、字符串块(string block)。下面将详细介绍这三部分的内容。
4.1. Header
在\kernel\include\linux\of_fdt.h文件中有相关定义
4.2.device-tree structure
设备树结构块是一个线性化的结构体,是设备树的主体,以节点的形式保存了主板上的设备信息。
在结构块中,以宏OF_DT_BEGIN_NODE标志一个节点的开始,以宏OF_DT_END_NODE标识一个节点的结束,整个结构块以宏OF_DT_END (0x00000009)结束。在\kernel\include\linux\of_fdt.h中有相关定义,我们把这些宏称之为token。
(1)FDT_BEGIN_NODE (0x00000001)。该token描述了一个node的开始位置,紧挨着该token的就是node name(包括unit address)
(2)FDT_END_NODE (0x00000002)。该token描述了一个node的结束位置。
(3)FDT_PROP (0x00000003)。该token描述了一个property的开始位置,该token之后是两个u32的数据,分别是length和name offset。length表示该property value data的size。name offset表示该属性字符串在device tree strings block的偏移值。length和name offset之后就是长度为length具体的属性值数据。
(4)FDT_NOP (0x00000004)。
(5)FDT_END (0x00000009)。该token标识了一个DTB的结束位置。
一个节点的结构如下:
(1)节点开始标志:一般为OF_DT_BEGIN_NODE(0x00000001)。
(2)节点路径或者节点的单元名(version<3以节点路径表示,version>=0x10以节点单元名表示)
(3)填充字段(对齐到四字节)
(4)节点属性。每个属性以宏OF_DT_PROP(0x00000003)开始,后面依次为属性值的字节长度(4字节)、属性名称在字符串块中的偏移量(4字节)、属性值和填充(对齐到四字节)。
(5)如果存在子节点,则定义子节点。
(6)节点结束标志OF_DT_END_NODE(0x00000002)。
4.3. 字符串块
通过节点的定义知道节点都有若干属性,而不同的节点的属性又有大量相同的属性名称,因此将这些属性名称提取出一张表,当节点需要应用某个属性名称时,直接在属性名字段保存该属性名称在字符串块中的偏移量。
4.4. memory reserve map
这个区域包括了若干的reserve memory描述符。每个reserve memory描述符是由address和size组成。其中address和size都是用U64来描述。
有些系统,我们也许会保留一些memory有特殊用途(例如DTB或者initrd image),或者在有些DSP+ARM的SOC platform上,有些memory被保留用于ARM和DSP进行信息交互。这些保留内存不会进入内存管理系统。
5. 解析DTB的函数及相关数据结构
5.1. machine_desc结构
内核将机器信息记录为machine_desc结构体(该定义在/arch/arm/include/asm/mach/arch.h),并保存在_arch_info_begin到_arch_info_end之间(_arch_info_begin,_arch_info_end为虚拟地址,是编译内核时指定的,此时mmu还未进行初始化。它其实通过汇编完成地址偏移操作)
machine_desc结构体用宏MACHINE_START进行定义,一般在/arch/arm/子目录,与板级相关的文件中进行成员函数及变量的赋值。由linker将machine_desc聚集在.arch.info.init节区形成列表。
bootloader引导内核时,ARM寄存器r2会将.dtb的首地址传给内核,内核根据该地址,解析.dtb中根节点的compatible属性,将该属性与内核中预先定义machine_desc结构体的dt_compat成员做匹配,得到最匹配的一个machine_desc。
在代码中,内核通过在start_kernel->setup_arch中调用setup_machine_fdt来实现上述功能,该函数的具体实现可参见/arch/arm/kernel/devtree.c。
5.2. 设备节点结构体
1.
记录节点信息的结构体。.dtb经过解析之后将以device_node列表的形式存储节点信息。
5.3. 属性结构体
device_node结构体中的成员结构体,用于描述节点属性信息。
5.4. uboot下的相关结构体
首先我们看下uboot用于记录os、initrd、fdt信息的数据结构bootm_headers,其定义在/include/image.h中,这边截取了其中与dtb相关的一小部分。
fit_hdr_fdt指向DTB设备树镜像的头。
lmb为uboot下的一种内存管理机制,全称为logical memory blocks。用于管理镜像的内存。lmb所记录的内存信息最终会传递给kernel。这里对lmb不做展开描述。在/include/lmb.h和/lib/lmb.c中有对lmb的接口和定义的具体描述。有兴趣的读者可以看下,所包含的代码量不多。
6. DTB加载及解析过程
先从uboot里的do_bootm出发,根据之前描述,DTB在内存中的地址通过bootm命令进行传递。在bootm中,它会根据所传进来的DTB地址,对DTB所在内存做一系列操作,为内核解析DTB提供保证。上图为对应的函数调用关系图。
在do_bootm中,主要调用函数为do_bootm_states,第四个参数为bootm所要处理的阶段和状态。
在do_bootm_states中,bootm_start会对lmb进行初始化操作,lmb所管理的物理内存块有三种方式获取。起始地址,优先级从上往下:
1. 环境变量“bootm_low”
2. 宏CONFIG_SYS_SDRAM_BASE(在tegra124中为0x80000000)
3. gd->bd->bi_dram[0].start
大小:
1. 环境变量“bootm_size”
2. gd->bd->bi_dram[0].size
经过初始化之后,这块内存就归lmb所管辖。接着,调用bootm_find_os进行kernel镜像的相关操作,这里不具体阐述。
还记得之前讲过bootm的三个参数么,第一个参数内核地址已经被bootm_find_os处理,而接下来的两个参数会在bootm_find_other中执行操作。
首先,bootm_find_other根据第二个参数找到ramdisk的地址,得到ramdisk的镜像;然后根据第三个参数得到DTB镜像,同检查kernel和ramdisk镜像一样,检查DTB镜像也会进行一系列的校验工作,如果校验错误,将无法正常启动内核。另外,uboot在确认DTB镜像无误之后,会将该地址保存在环境变量“fdtaddr”中。
接着,uboot会把DTB镜像reload一次,使得DTB镜像所在的物理内存归lmb所管理:①boot_fdt_add_mem_rsv_regions会将原先的内存DTB镜像所在的内存置为reserve,保证该段内存不会被其他非法使用,保证接下来的reload数据是正确的;②boot_relocate_fdt会在bootmap区域中申请一块未被使用的内存,接着将DTB镜像内容复制到这块区域(即归lmb所管理的区域)
注:若环境变量中,指定“fdt_high”参数,则会根据该值,调用lmb_alloc_base函数来分配DTB镜像reload的地址空间。若分配失败,则会停止bootm操作。因而,不建议设置fdt_high参数。
接下来,do_bootm会根据内核的类型调用对应的启动函数。与linux对应的是do_bootm_linux。
① boot_prep_linux
为启动后的kernel准备参数
② boot_jump_linux
以上是boot_jump_linux的片段代码,可以看出:若使用DTB,则原先用来存储ATAG的寄存器R2,将会用来存储.dtb镜像地址。
boot_jump_linux最后将调用kernel_entry,将.dtb镜像地址传给内核。
下面我们来看下内核的处理部分:
在arch/arm/kernel/head.S中,有这样一段:
_vet_atags定义在/arch/arm/kernel/head-common.S中,它主要对DTB镜像做了一个简单的校验。
真正解析处理dbt的开始部分,是setup_arch->setup_machine_fdt。这部分的处理在第五部分的machine_mdesc中有提及。
如图,是setup_machine_fdt中的解析过程。
解析chosen节点将对boot_command_line进行初始化。
解析根节点的{size,address}将对dt_root_size_cells,dt_root_addr_cells进行初始化。为之后解析memory等其他节点提供依据。
解析memory节点,将会把节点中描述的内存,加入memory的bank。为之后的内存初始化提供条件。
解析设备树在函数unflatten_device_tree中完成,它将.dtb解析成device_node结构(第五部分有其定义),并构成单项链表,以供OF的API接口使用。
下面主要结合代码分析:/drivers/of/fdt.c
总的归纳为:
① kernel入口处获取到uboot传过来的.dtb镜像的基地址
② 通过early_init_dt_scan()函数来获取kernel初始化时需要的bootargs和cmd_line等系统引导参数。
③ 调用unflatten_device_tree函数来解析dtb文件,构建一个由device_node结构连接而成的单向链表,并使用全局变量of_allnodes保存这个链表的头指针。
④ 内核调用OF的API接口,获取of_allnodes链表信息来初始化内核其他子系统、设备等。
7. OF的API接口
OF的接口函数在/drivers/of/目录下,有of_i2c.c、of_mdio.c、of_mtd.c、Adress.c等等
这里将列出几个常用的API接口。
1. 用来查找在dtb中的根节点
unsigned long __init of_get_flat_dt_root(void)
2. 根据deice_node结构的full_name参数,在全局链表of_allnodes中,查找合适的device_node
struct device_node *of_find_node_by_path(const char *path)
例如:
struct device_node *cpus;
cpus=of_find_node_by_path("/cpus");
3. 若from=NULL,则在全局链表of_allnodes中根据name查找合适的device_node
struct device_node *of_find_node_by_name(struct device_node *from,const char *name)
例如:
struct device_node *np;
np = of_find_node_by_name(NULL,“firewire”);
4. 根据设备类型查找相应的device_node
struct device_node *of_find_node_by_type(struct device_node *from,const char *type)
例如:
struct device_node *tsi_pci;
tsi_pci= of_find_node_by_type(NULL,“pci”);
5. 根据compatible字符串查找device_node
struct device_node *of_find_compatible_node(struct device_node *from,const char *type, const char *compatible)
6. 根据节点属性的name查找device_node
struct device_node *of_find_node_with_property(struct device_node *from,const char *prop_name)
7. 根据phandle查找device_node
struct device_node *of_find_node_by_phandle(phandle handle)
8. 根据alias的name获得设备id号
int of_alias_get_id(struct device_node *np, const char *stem)
9. device node计数增加/减少
struct device_node *of_node_get(struct device_node *node)
void of_node_put(struct device_node *node)
10. 根据property结构的name参数,在指定的device node中查找合适的property
struct property *of_find_property(const struct device_node *np,const char *name,int *lenp)
11. 根据property结构的name参数,返回该属性的属性值
const void *of_get_property(const struct device_node *np, const char *name,int *lenp)
12. 根据compat参数与device node的compatible匹配,返回匹配度
int of_device_is_compatible(const struct device_node *device,const char *compat)
13. 获得父节点的device node
struct device_node *of_get_parent(const struct device_node *node)
14. 将matches数组中of_device_id结构的name和type与device node的compatible和type匹配,返回匹配度最高的of_device_id结构
const struct of_device_id *of_match_node(const struct of_device_id *matches,const struct device_node *node)
15. 根据属性名propname,读出属性值中的第index个u32数值给out_value
int of_property_read_u32_index(const struct device_node *np,const char *propname,u32 index, u32 *out_value)
16. 根据属性名propname,读出该属性的数组中sz个属性值给out_values
int of_property_read_u8_array(const struct device_node *np,const char *propname, u8 *out_values, size_t sz)
int of_property_read_u16_array(const struct device_node *np,const char *propname, u16 *out_values, size_t sz)
int of_property_read_u32_array(const struct device_node *np,const char *propname, u32 *out_values,size_t sz)
17. 根据属性名propname,读出该属性的u64属性值
int of_property_read_u64(const struct device_node *np, const char *propname,u64 *out_value)
18. 根据属性名propname,读出该属性的字符串属性值
int of_property_read_string(struct device_node *np, const char *propname,const char **out_string)
19. 根据属性名propname,读出该字符串属性值数组中的第index个字符串
int of_property_read_string_index(struct device_node *np, const char *propname,int index, const char **output)
20. 读取属性名propname中,字符串属性值的个数
int of_property_count_strings(struct device_node *np, const char *propname)
21. 读取该设备的第index个irq号
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
22. 读取该设备的第index个irq号,并填充一个irq资源结构体
int of_irq_to_resource(struct device_node *dev, int index, struct resource *r)
23. 获取该设备的irq个数
int of_irq_count(struct device_node *dev)
24. 获取设备寄存器地址,并填充寄存器资源结构体
int of_address_to_resource(struct device_node *dev, int index,struct resource *r)
const __be32 *of_get_address(struct device_node *dev, int index, u64 *size,unsigned int *flags)
25. 获取经过映射的寄存器虚拟地址
void __iomem *of_iomap(struct device_node *np, int index)
24. 根据device_node查找返回该设备对应的platform_device结构
struct platform_device *of_find_device_by_node(struct device_node *np)
25. 根据device node,bus id以及父节点创建该设备的platform_device结构
struct platform_device *of_device_alloc(struct device_node *np,const char *bus_id,struct device *parent)
static struct platform_device *of_platform_device_create_pdata(struct device_node *np,const char *bus_id,
void *platform_data,struct device *parent)
26. 遍历of_allnodes中的节点挂接到of_platform_bus_type总线上,由于此时of_platform_bus_type总线上还没有驱动,所以此时不进行匹配
int of_platform_bus_probe(struct device_node *root,const struct of_device_id *matches,struct device *parent)
参考
https://www.cnblogs.com/edver/p/9063526.html
https://blog.csdn.net/u012489236/article/details/97137007
更多推荐
所有评论(0)