前言:这篇文章主要分析内核对设备树的处理,即怎么生成了最后的设备节点。
**Linux uses DT data for three major purposes:
- platform identification, 平台识别信息
- runtime configuration, and 运行时配置信息
- device population. 设备信息**
一、head.S对设备树文件的简单处理
bootloader启动内核时,会设置r0,r1,r2三个寄存器,
r0一般设置为0;
r1一般设置为machine id (在使用设备树时该参数没有被使用);(machine id会在内核中用来选择匹配的machine_desc,匹配原则是id与machine_desc.nb相等);
r2一般设置ATAGS或DTB的开始地址
内核第一个文件head.S/head-common.S会进行如下操作:
把bootloader传来的r1值, 赋给了C变量: __machine_arch_type
把bootloader传来的r2值, 赋给了C变量: __atags_pointer // dtb首地址
最后会调用C函数start_kernel
二、对设备树中平台信息的处理(选择machine_desc)
我们知道当不使用设备树的时候,uboot会使用r1寄存器传递machine id,然后内核会把该ID与内核中的所有的machine_desc.nb匹配,但是如果使用设备树,id就不会被使用,这时内核怎么选择machine_desc呢?
在设备树文件中,在根节点中有一个compatible属性,该属性的值是一系列的字符串,比如compatible = “samsung,smdk2440”“samsung,smdk2410,samsung,smdk24xx”;该属性就是告诉内核要选择什么样的machine_desc,因为machine_desc结构体中有一个dt_compat成员,该成员表示machine_desc支持哪些单板,所以内核会把compatible中的字符串与dt_compat进行比较,但是compatible中是一系列的字符串,所以匹配是有最优解的,即所有的machine_desc中的dt_compat与compatible进行比较后,匹配的compatible中的字符串越靠前的dt_compat所对应的machine_desc即为最优解。下面给出图示:
下面是代码的实现部分,主要是比较的过程,遍历所有的machine_desc,然后作比较,得到最优解:
start_kernel // init/main.c
setup_arch(&command_line); // arch/arm/kernel/setup.c
mdesc = setup_machine_fdt(__atags_pointer); // arch/arm/kernel/devtree.c
early_init_dt_verify(phys_to_virt(dt_phys) // 判断是否有效的dtb, drivers/of/ftd.c
initial_boot_params = params;
mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach); // 找到最匹配的machine_desc, drivers/of/ftd.c
while ((data = get_next_compat(&compat))) {
score = of_flat_dt_match(dt_root, compat);
if (score > 0 && score < best_score) {
best_data = data;
best_score = score;
}
}
machine_desc = mdesc;
arch_get_next_mach是每调用一次,返回下一个machine_desc,至于该函数为什么能实现,是因为所有的machine_desc有一个段属性值,在编译的时候,一样的段属性会放到一块。如下图,所以就能在arch_info_begin 到arch_info_end中找到所有的machine。如下图:
三、对设备树运行时配置信息的处理
设备树我们知道只是起一个设备信息的传输作用,所以内核会将设备树中的信息保存在变量中,储存起来。
a. /chosen节点中bootargs属性的值, 存入全局变量: boot_command_line
b. 确定根节点的这2个属性的值: #address-cells, #size-cells
存入全局变量: dt_root_addr_cells, dt_root_size_cells
c. 解析/memory中的reg属性, 提取出"base, size", 最终调用memblock_add(base, size);
四、dtb文件转化为device_node
dtb文件会在内存中一直存在着,不会被内核或者应用程序占用,我们需要使用的时候可以直接使用dtb文件。dtb文件的内容会被解析生成多个device_node,然后这些device_node构成一棵树, 根节点为: of_root
1.上一章我们学习了dtb文件的格式,dtb文件中有如下规则:
每一个节点都以TAG(FDT_BEGIN_NODE, 0x00000001)开始, 节点内部可以嵌套其他节点,
每一个属性都以TAG(FDT_PROP, 0x00000003)开始
所以可以根据这个规则解析dtb文件,生成device_node结构体。但是内核如何去解析文件,这里不是重点,我们需要知道的是dts文件中的所有的内容保存在device_node结构体中的什么成员中。
2.每一个节点都转换为一个device_node结构体
这里给出两个结构体,根据这两个结构体,就能大概看出来dts文件中的内容与结构体成员的对应关系了。
struct device_node {
const char *name; // 来自节点中的name属性, 如果没有该属性, 则设为"NULL"
const char *type; // 来自节点中的device_type属性, 如果没有该属性, 则设为"NULL"
phandle phandle;
const char *full_name; // 节点的名字, node-name[@unit-address]
struct fwnode_handle fwnode;
struct property *properties; // 节点的属性
struct property *deadprops; /* removed properties */
struct device_node *parent; // 节点的父亲
struct device_node *child; // 节点的孩子(子节点)
struct device_node *sibling; // 节点的兄弟(同级节点)
#if defined(CONFIG_OF_KOBJ)
struct kobject kobj;
#endif
unsigned long _flags;
void *data;
#if defined(CONFIG_SPARC)
const char *path_component_name;
unsigned int unique_id;
struct of_irq_controller *irq_trans;
#endif
};
device_node结构体中有properties, 用来表示该节点的属性, 每一个属性对应一个property结构体:
struct property {
char *name; // 属性名字, 指向dtb文件中的字符串
int length; // 属性值的长度
void *value; // 属性值, 指向dtb文件中value所在位置, 数据仍以big endian存储
struct property *next;
#if defined(CONFIG_OF_DYNAMIC) || defined(CONFIG_SPARC)
unsigned long _flags;
#endif
#if defined(CONFIG_OF_PROMTREE)
unsigned int unique_id;
#endif
#if defined(CONFIG_OF_KOBJ)
struct bin_attribute attr;
#endif
};
上面结构体已经能很清楚的知道每个成员代表着dts文件中的什么属性了,这里我们有一个现成的例子,dts文件如下,我们画出该dts文件解析后生成的device_node;
/ {
model = "SMDK24440";
compatible = "samsung,smdk2440";
#address-cells = <1>;
#size-cells = <1>;
memory { /* /memory */
device_type = "memory";
reg = <0x30000000 0x4000000 0 4096>;
};
/*
cpus {
cpu {
compatible = "arm,arm926ej-s";
};
};
*/
chosen {
bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200";
};
led {
compatible = "jz2440_led";
pin = <S3C2410_GPF(5)>;
};
};
图片太大,建议放大查看。
五、device_node转化为platform_device
首先,这里有两个问题:
1 .那么多的device_node,哪些会被转化为platform_device呢?
- 根节点下的子节点,且该子节点必须包含compatible属性;
- 如果一个结点的compatile属性含有这些特殊的值(“simple-bus”,“simple-mfd”,“isa”,“arm,amba-bus”)之一,
那么它的子结点(需含compatile属性)也可以转换为platform_device。
2.spi,i2c等节点怎么处理?
spi或者i2c控制器的节点会被转化为platform_device,但是这些节点下的子节点则由相应的总线驱动决定生成什么。
下图是一个简单的例子,来描述i2c,spi及根节点下的子节点及特殊节点的子节点会被怎么处理:
解释了上面两个问题之后,我们来说一下device_node最后转化为platform_device的什么部分。
我们知道设备树中的属性会有reg/irq等,就会转化为platform_device结构体中的resource【】数组,然后其他的部分就全部在platform_device.dev.of_node结构体成员中,想要得到文件中的任何信息,就从该结构体成员获得。
六、platform_device跟platform_driver的匹配
匹配其实之前我总结在了第一章中,但是当时只总结了三种办法,漏掉了优先级最高的,这里我们把它加上,优先级排序为 0.1.2.3
七、内核中设备树的操作函数
include/linux/目录下有很多of开头的头文件:
a. 处理DTB
of_fdt.h // dtb文件的相关操作函数, 我们一般用不到, 因为dtb文件在内核中已经被转换为device_node树(它更易于使用)
b. 处理device_node
of.h // 提供设备树的一般处理函数, 比如 of_property_read_u32(读取某个属性的u32值), of_get_child_count(获取某个device_node的子节点数)
of_address.h // 地址相关的函数, 比如 of_get_address(获得reg属性中的addr, size值)
of_match_device(从matches数组中取出与当前设备最匹配的一项)
of_dma.h // 设备树中DMA相关属性的函数
of_gpio.h // GPIO相关的函数
of_graph.h // GPU相关驱动中用到的函数, 从设备树中获得GPU信息
of_iommu.h // 很少用到
of_irq.h // 中断相关的函数
of_mdio.h // MDIO (Ethernet PHY) API
of_net.h // OF helpers for network devices.
of_pci.h // PCI相关函数
of_pdt.h // 很少用到
of_reserved_mem.h // reserved_mem的相关函数
c. 处理 platform_device
of_platform.h // 把device_node转换为platform_device时用到的函数,
// 比如of_device_alloc(根据device_node分配设置platform_device),
// of_find_device_by_node (根据device_node查找到platform_device),
// of_platform_bus_probe (处理device_node及它的子节点)
of_device.h // 设备相关的函数, 比如 of_match_device
八、在根文件系统中查看设备树(有助于调试)
a. /sys/firmware/fdt // 原始dtb文件
hexdump -C /sys/firmware/fdt
b. /sys/firmware/devicetree // 以目录结构程现的dtb文件, 根节点对应base目录, 每一个节点对应一个目录, 每一个属性对应一个文件
c. /sys/devices/platform // 系统中所有的platform_device, 有来自设备树的, 也有来有.c文件中注册的
对于来自设备树的platform_device,
可以进入 /sys/devices/platform/<设备名>/of_node 查看它的设备树属性
d. /proc/device-tree 是链接文件, 指向 /sys/firmware/devicetree/base