Linux设备树(5)—实现
一、DeviceTree简介
1. 将内核当做一个黑盒,那么其输入参数应该包括:
(1) 识别platform的信息
(2) runtime的配置参数
(3) 设备的拓扑结构以及特性
对于嵌入式系统,在系统启动阶段,bootloader会加载内核并将控制权转交给内核,此外,还需要把上述的三个参数信息传递给kernel,以便kernel可以有较大的灵活性。在linux kernel中,Device Tree的设计目标就是如此。
2. 是否Device Tree要描述系统中的所有硬件信息
答案是否定的。基本上,那些可以动态探测到的设备是不需要描述的,例如USB device、PCI device。不过对于SOC上的usb host controller,它是无法动态识别的,需要在device tree中描述。
二、DeviceTree基础
1. device tree的基本单元是node。这些node被组织成树状结构,除了root node,每个node都只有一个parent。一个device tree文件中只能有一个root node。每个node中包含了若干的property/value来描述该node的一些特性。每个node用节点名字(node name)标识,节点名字的格式是node-name@unit-address。如果该node没有reg属性(后面会描述这个property),那么该节点名字中必须不能包括@和unit-address。unit-address的具体格式是和设备挂在那个bus上相关。例如对于cpu,其unit-address就是从0开始编址,以此加一。而具体的设备,例如以太网控制器,其unit-address就是寄存器地址。root node的node name是确定的,必须是“/”。
2. 在一个树状结构的device tree中,如何引用一个node呢?要想唯一指定一个node必须使用full path,例如/node-name-1/node-name-2/node-name-N。
3. 属性(property)值标识了设备的特性,它的值(value)是多种多样的:
(1) 可能是空,也就是没有值的定义。例如上图中的64-bit ,这个属性没有赋值。
(2) 可能是一个u32、u64的数值(值得一提的是cell这个术语,在Device Tree表示32bit的信息单位)。例如 #address-cells = <1> 。当然,可能是一个数组。例如<0x00000000 0x00000000 0x00000000 0x20000000>
(3) 可能是一个字符串。例如device_type = "memory" ,当然也可能是一个string list。例如"PowerPC,970"
4. 一个node被定义成:
[label:] node-name[@unit-address] {
[properties definitions]
[child nodes]
}
“[]”表示可选的,因此可以定义一个只有node-name的空节点。label方便在dts文件中引用,具体后面会描述。child node的格式和node是完全一样的,因此,一个dts文件中就是若干嵌套组成的node,property以及child note、child note property描述。
5. 属性的value有三种情况:
(1) 属性值是text string或者string list,用双引号表示。例如device_type = "memory"
(2) 属性值是32bit unsigned integers,用尖括号表示。例如#size-cells = <1>
(3) 属性值是binary data,用方括号表示。例如binary-property = [0x01 0x23 0x45 0x67]
6. 如果一个device node中包含了有寻址需求(要定义reg property)的sub node(后文也许会用child node,和sub node是一样的意思),那么就必须要定义这两个属性。“#”是number的意思,#address-cells这个属性是用来描述sub node中的reg属性的地址域特性的,也就是说需要用多少个u32的cell来描述该地址域。同理可以推断#size-cells的含义,下面对reg的描述中会给出更详细的信息。
7. chosen node主要用来描述由系统firmware指定的runtime parameter。如果存在chosen这个node,其parent node必须是名字是“/”的根节点。
8. device tree用于HW platform识别,runtime parameter传递以及硬件设备描述。chosen节点并没有描述任何硬件设备节点的信息,它只是传递了runtime parameter。
9. aliases 节点定义了一些别名。需要别名的原因是因为Device tree是树状结构,当要引用一个node的时候要指明相对于root node的full path,例如/node-name-1/node-name-2/node-name-N。如果多次引用,每次都要写这么复杂的字符串多少是有些麻烦,因此可以在aliases 节点定义一些设备节点full path的缩写。
10. memory device node是所有设备树文件的必备节点,它定义了系统物理内存的layout。device_type 属性定义了该node的设备类型,例如cpu、serial等。对于memory node,其device_type必须等于memory。#####
11. reg属性定义了访问该device node的地址信息,该属性的值被解析成任意长度的(address,size)数组,具体用多长的数据来表示address和size是在其parent node中通过 #address-cells和#size-cells 来定义。对于device node,reg描述了memory-mapped IO register的offset和length。对于memory node,定义了该memory的起始地址和长度。
12. 本例中的物理内存的布局并没有通过memory node传递,其实我们可以使用command line传递,如下,我们command line中的参数“mem=64M@0x30000000”已经给出了具体的信息。######
"root=/dev/nfs nfsroot=1.1.1.1:/nfsboot ip=1.1.1.2:1.1.1.1:1.1.1.1:255.255.255.0::usbd0:off console=ttyS0,115200 mem=64M@0x30000000"
13. 用另外一个例子来加深对本节描述的各个属性以及memory node的理解。假设我们的系统是64bit的,physical memory分成两段,定义如下:
RAM: starting address 0x0, length 0x80000000 (2GB)
RAM: starting address 0x100000000, length 0x100000000 (4GB)
对于这样的系统,我们可以将root node中的 #address-cells 和 #size-cells 这两个属性值设定为2,可以用下面两种方法来描述物理内存:
方法1: memory@0 { device_type = "memory"; reg = <0x000000000 0x00000000 0x00000000 0x80000000 0x000000001 0x00000000 0x00000001 0x00000000>; }; 方法2: memory@0 { device_type = "memory"; reg = <0x000000000 0x00000000 0x00000000 0x80000000>; }; memory@100000000 { device_type = "memory"; reg = <0x000000001 0x00000000 0x00000001 0x00000000>; };
14. 假设定义该属性:compatible = “aaaaaa”, “bbbbb"。那么操作操作系统可能首先使用aaaaaa来匹配适合的driver,如果没有匹配到,那么使用字符串bbbbb来继续寻找适合的driver。#######
15. 具体各个HW block的interrupt source是如何物理的连接到interruptcontroller的呢?在dts文件中是用 interrupt-parent 这个属性来标识的。且慢,这里定义interrupt-parent属性的是root node,难道root node会产生中断到 interrupt controller 吗?当然不会,只不过如果一个能够产生中断的device node没有定义 interrupt-parent 的话,其 interrupt-parent 属性就是跟随parent node。##### 因此,与其在所有的下游设备中定义interrupt-parent,不如统一在root node中定义了。######
16. 对于根节点,必须有一个cpus的child node来描述系统中的CPU信息。对于CPU的编址我们用一个u32整数就可以描述了,因此,对于cpus node,#address-cells 是1,而#size-cells是0。
三、代码分析
1. 如何通过DeviceTree完成运行时参数传递以及platform的识别功能
在 arm 上,/kernel/msm-4.14/arch/arm/kernel/head.S (注arm64中没找到)中:
/* * Kernel startup entry point. * --------------------------- * * This is normally called from the decompressor code. The requirements * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0, * r1 = machine nr, r2 = atags or dtb pointer. */ .arm __HEAD ENTRY(stext) //入口 ....
2. device tree相关的setup_arch代码分析
start_kernel() //main.c setup_arch(cmdline_p) //setup.c setup_machine_fdt(__fdt_pointer) //setup.c early_init_dt_scan(dt_virt) //setup.c early_init_dt_scan_nodes() //fdt.c of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line); //fdt.c 从/chosen节点获取各类信息 of_scan_flat_dt(early_init_dt_scan_memory, NULL); //fdt.c 获取memory信息
setup_machine_fdt() 函数的功能就是根据Device Tree的信息,找到最适合的machine描述符。
early_init_dt_scan() 函数有两个功能,一个是为后续的DTB scan进行准备工作,另外一个是运行时参数传递。
of_flat_dt_match_machine() 是在machine描述符的列表中scan,找到最合适的那个machine描述符。
early_init_dt_scan_chosen() 运行时参数######是在扫描DTB的chosen node时候完成的,具体的动作就是获取chosen node的bootargs、initrd等属性的value,并将其保存在全局变量(boot_command_line,initrd_start、initrd_end)中。
early_init_dt_scan_memory() 扫描内存节点。
3. 初始化流程
在系统初始化的过程中,我们需要将DTB转换成节点是device_node的树状结构,以便后续方便操作。具体的代码位于 setup_arch() //setup.c --> unflatten_device_tree() //setup.c 中。此函数的主要功能就是扫描DTB,将device node被组织成:
(1) global list。全局变量struct device_node *of_allnodes 就是指向设备树的global list
(2) tree。
这些功能主要是在 __unflatten_device_tree 函数中实现.
4. 用 struct device_node 来抽象设备树中的一个节点,具体解释如下:
struct device_node { const char *name;-----------device node name const char *type;-----------对应device_type的属性 phandle phandle;-----------对应该节点的phandle属性 const char *full_name; --------从“/”开始的,表示该node的full path struct property *properties;----该节点的属性列表 struct property *deadprops; ----如果需要删除某些属性,kernel并非真的删除,而是挂入到deadprops的列表 struct device_node *parent;-----parent、child以及sibling将所有的device node连接起来 struct device_node *child; struct device_node *sibling; struct device_node *next; -----通过该指针可以获取相同类型的下一个node struct device_node *allnext;----通过该指针可以获取node global list下一个node struct proc_dir_entry *pde;----开放到userspace的proc接口信息 struct kref kref;---------该node的reference count unsigned long _flags; void *data; };
5. 并入linux kernel的设备驱动模型
系统会根据Device tree来动态的增加系统中的 platform_device。
6. cpus node的处理
这部分的处理可以参考 setup_arch --> arm_dt_init_cpu_maps 中的代码。注:Arm64中已经没有这块代码了。
7. memory的处理
memory 定义有的节点有两种,一种是node name是 memory@XXX 形态的,另外一种是node中定义了 device_type 属性并且其值是memory。#####这部分的处理可以参考: start_kernel --> setup_arch(cmdline_p) --> setup_machine_fdt(phys_addr_t dt_phys)--> early_init_dt_scan(params) --> early_init_dt_scan_nodes --> early_init_dt_scan_memory.
8. interrupt controller的处理
初始化是通过 start_kernel->init_IRQ->machine_desc->init_irq() 实现的。
参考:
Device Tree(一):背景介绍:http://www.wowotech.net/linux_kenrel/why-dt.html
Device Tree(二):基本概念:http://www.wowotech.net/linux_kenrel/dt_basic_concept.html
Device Tree(三):代码分析:http://www.wowotech.net/device_model/dt-code-analysis.html/comment-page-2
TODO: 下面配置是如何解析出地址值和size值的?
memory { device_type = "memory"; reg = <0x0 0x0 0x0 0x0>; };
posted on 2024-06-24 18:39 Hello-World3 阅读(59) 评论(0) 编辑 收藏 举报