Linux内核:设备树

1. 设备树API函数:利用of_函数读取设备树结点/属性信息

​ 设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的,我们在编写驱动的时候需要获取到这些信息。

​ Linux 内核给我们提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”,所以在很多资料里面也被叫做 OF 。这些 OF原型都定义在include/linux/of.h 文件中。

判断根节点兼容性OF函数

在Linux内核中,常常使用如下OF函数来判断根节点的兼容性:

int of_machine_is_compatible(const char *compat);

DT设备的.dt_compat可能包含多个电路板,通过该函数可以判断根节点compatible的属性,当该函数的compat参数与根据点的compatible匹配时,返回一个正整数。
用法,例如:

of_machine_is_compatible("samsung,exynos4");

查找节点的 OF

​ 设备都是以节点的形式“挂”到设备树上的,因此要想获取这个设备的其他属性信息,必须先获取到这个设备的节点。 Linux 内核使用 device_node 结构体来描述一个节点,此结构体定义在文件 include/linux/of.h 中,代码如下:

struct device_node {
	const char *name;  /*节点的名字*/
	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
};

与查找节点相关的,有如下函数。

of_find_node_by_name

of_find_node_by_name通过节点名字查找指定的节点,函数原型如下:

struct device_node *of_find_node_by_name(struct device_node *from,
										const char *name);

函数参数和返回值含义如下:
from: 开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
name: 要查找的节点名字。

of_find_node_by_type

of_find_node_by_type通过 device_type 属性(过时了,建议不用)查找指定的节点,函数原型如下:

struct device_node *of_find_node_by_type(struct device_node *from,
										const char *type);

函数参数和返回值含义如下:

from: 开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。

type: 要查找的节点对应的 type 字符串,也就是 device_type 属性值。

返回值: 找到的节点,如果为 NULL 表示查找失败。

of_find_compatible_node

of_find_compatible_node根据 device_type 和 compatible 这两个属性查找指定的节点,函数原型如下:

struct device_node *of_find_compatible_node(struct device_node *from,
											const char *type,
											const char *compatible);

函数参数和返回值含义如下:
from: 开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
type: 要查找的节点对应的 type 字符串,也就是 device_type 属性值,可以为 NULL,表示忽略掉 device_type 属性。
compatible: 要查找的节点所对应的 compatible 属性列表。
返回值: 找到的节点,如果为 NULL 表示查找失败

of_find_node_by_path

of_find_node_by_path通过路径来查找指定的节点,函数原型如下:

inline struct device_node *of_find_node_by_path(const char *path);

函数参数和返回值含义如下:

  • path: 带有全路径的节点名,可以使用节点的别名,比如“/backlight”就是 backlight 这个节点的全路径。
  • 返回值: 找到的节点,如果为 NULL 表示查找失败

查找父/子节点的 OF

of_get_parent

of_get_parent用于获取指定节点的父节点,原型如下:

struct device_node *of_get_parent(const struct device_node *node);

函数参数和返回值含义如下:

  • node: 需要查找父节点的子节点;
  • 返回值: 返回父节点;

of_get_next_child

of_get_next_child用迭代的查找子节点,函数原型如下:

struct device_node *of_get_next_child(const struct device_node *node,
									struct device_node *prev);

函数参数和返回值含义如下:

  • node: 父节点;
  • prev: 前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为NULL,表示从第一个子节点开始。
  • 返回值: 找到的下一个子节点。

of_get_next_child

节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要, Linux 内核中使用结构体 property 表示属性,此结构体同样定义在文件 include/linux/of.h 中,内容如下:

struct property {
	char *name; /* 属性名字 */
	int length; /* 属性长度 */
	void *value; /* 属性值 */
	struct property *next; /* 下一个属性 */
	unsigned long _flags;
	unsigned int unique_id;
	struct bin_attribute attr;
};

提取属性值

Linux 内核也提供了提取属性值的 OF。

of_find_property

of_find_property用于查找指定的属性,函数原型如下:

property *of_find_property(const struct device_node *np,
							const char *name,
							int *lenp);

函数参数和返回值含义如下:

  • np: 设备节点;
  • name: 属性名字;
  • lenp: 属性值的字节数;

返回值: 找到的属性。

of_property_count_elems_of_size

of_property_count_elems_of_size用于获取属性中元素的数量,比如 reg 属性值是一个数组,那么使用此函数可以获取到这个数组的大小,此函数原型如下:

int of_property_count_elems_of_size(const struct device_node *np,
									const char *propname,
									int elem_size);

函数参数和返回值含义如下:

  • np: 设备节点。
  • proname: 需要统计元素数量的属性名字。
  • elem_size: 元素长度。

返回值: 得到的属性元素数量。

of_property_read_u32_index

of_property_read_u32_index用于从属性中获取指定标号的 u32 类型数据值(无符号 32位),比如某个属性有多个 u32 类型的值,那么就可以使用此函数来获取指定标号的数据值,此函数原型如下:

int of_property_read_u32_index(const struct device_node *np,
								const char *propname,
								u32 index,
								u32 *out_value);

函数参数和返回值含义如下:

  • np: 设备节点;
  • proname: 要读取的属性名字;
  • index: 要读取的值标号;
  • out_value: 读取到的值;

返回值: 0 读取成功,负值,读取失败, -EINVAL 表示属性不存在, -ENODATA 表示没有要读取的数据, -EOVERFLOW 表示属性值列表太小。

of_property_read_u8_array

of_property_read_u16_array

of_property_read_u32_array

of_property_read_u64_array

这 4 个函数分别是读取属性中 u8、 u16、 u32 和 u64 类型的数组数据,比如大多数的 reg 属性都是数组数据,可以使用这 4 个函数一次读取出 reg 属性中的所有数据。这四个函数的原型如下:

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);
int of_property_read_u64_array(const struct device_node *np,
							const char *propname,
							u64 *out_values
							size_t sz);

函数参数和返回值含义如下:

  • np: 设备节点;
  • proname: 要读取的属性名字;
  • out_value: 读取到的数组值,分别为 u8、 u16、 u32 和 u64。
  • sz: 要读取的数组元素数量。

返回值: 0,读取成功,负值,读取失败, -EINVAL 表示属性不存在, -ENODATA 表示没有要读取的数据, -EOVERFLOW 表示属性值列表太小。

of_property_read_u8

of_property_read_u16

of_property_read_u32

of_property_read_u64

有些属性只有一个整形值,这四个函数就是用于读取这种只有一个整形值的属性,分别用
于读取 u8、 u16、 u32 和 u64 类型属性值,函数原型如下:

int of_property_read_u8(const struct device_node *np,
						const char *propname,
						u8 *out_value);
int of_property_read_u16(const struct device_node *np,
						const char *propname,
						u16 *out_value);
int of_property_read_u32(const struct device_node *np,
						const char *propname,
						u32 *out_value);
int of_property_read_u64(const struct device_node *np,
						const char *propname,
						u64 *out_value);

函数参数和返回值含义如下:

  • np: 设备节点。
  • proname: 要读取的属性名字。
  • out_value: 读取到的数组值。

返回值: 0,读取成功,负值,读取失败, -EINVAL 表示属性不存在, -ENODATA 表示没有要读取的数据, -EOVERFLOW 表示属性值列表太小。

of_property_read_string

of_property_read_string用于读取属性中字符串值,函数原型如下:

int of_property_read_string(struct device_node *np,
							const char *propname,
							const char `out_string);

函数参数和返回值含义如下:

  • np: 设备节点。
  • proname: 要读取的属性名字。
  • out_string: 读取到的字符串值。

返回值: 0,读取成功,负值,读取失败。

of_n_addr_cells

of_n_addr_cells用于获取 #address-cells 属性值,函数原型如下:

int of_n_addr_cells(struct device_node *np);

函数参数和返回值含义如下:

  • np: 设备节点。
  • 返回值: 获取到的#address-cells 属性值。

of_n_size_cells

of_size_cells用于获取 #size-cells 属性值,函数原型如下:

int of_n_size_cells(struct device_node *np);

函数参数和返回值含义如下:

  • np: 设备节点。

返回值: 获取到的#size-cells 属性值。

其他常用的 OF

of_get_address

of_get_address用于获取地址相关属性,主要是“reg”或者“assigned-addresses”属性
值,函数属性如下:

const __be32 *of_get_address(struct device_node *dev,
							int index,
							u64 *size,
							unsigned int *flags);

函数参数和返回值含义如下:

  • dev: 设备节点。
  • index: 要读取的地址标号。
  • size: 地址长度。
  • flags: 参数,比如 IORESOURCE_IO、 IORESOURCE_MEM 等

返回值: 读取到的地址数据首地址,为 NULL 的话表示读取失败。

of_translate_address

of_translate_address负责将从设备树读取到的地址转换为物理地址,函数原型如下:

u64 of_translate_address(struct device_node *dev,
						const __be32 *in_addr);

函数参数和返回值含义如下:

  • dev: 设备节点。
  • in_addr: 要转换的地址。

返回值: 得到的物理地址,如果为 OF_BAD_ADDR 的话表示转换失败。

of_address_to_resource

resource结构体描述的都是设备资源信息, resource 结构体定义在文件include/linux/ioport.h 中,定义如下:

struct resource {
	resource_size_t start; /*起始地址,对于32位soc,resource_size_t 的数据类型是u32*/
	resource_size_t end; /*结束地址*/
	const char *name;  /*资源的名字*/
	unsigned long flags; /*资源标志位,一般表示资源类型,可选的资源标志定义在文件 include/linux/ioport.h,如IORESOURCE_BITS、IORESOURCE_MEM、IORESOURCE_IRQ等 */
	struct resource *parent, *sibling, *child;
};

of_address_to_resource是从设备树里面提取资源值,但是本质上就是将 reg 属性值,然后将其转换为 resource 结构体类型,函数原型如下所示:

int of_address_to_resource(struct device_node *dev,
							int index,
							struct resource *r);

函数参数和返回值含义如下:

  • dev: 设备节点。
  • index: 地址资源标号。
  • r: 得到的 resource 类型的资源值。

返回值: 0,成功;负值,失败。

of_iomap

of_iomap用于直接内存映射,以前我们会通过 ioremap来完成物理地址到虚拟地址的映射,采用设备树以后就可以直接通过 of_iomap来获取内存地址所对应的虚拟地址,不需要使用 ioremap了。该函数的原型如下:

void __iomem *of_iomap(struct device_node *np,
						int index);

函数参数和返回值含义如下:

  • np: 设备节点。
  • index: reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0。

返回值: 经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。

例子

目的:期望通过linux/of.h中相关函数,在驱动中读取设备树相关结点信息和属性信息

要求:

  • ①读取设备树/backlight结点下的属性,以及属性信息,合理处理返回值和错误信息
  • ②设计一个能够读取u32类型属性的通用函数,并在init函数中输出信息

设备树

/ {
    backlight {
        compatible = "pwm-backlight";
        pwms = <&pwm1 0 5000000>;
        brightness-levels = <0 4 8 16 32 64 128 255>;
        default-brightness-level = <6>;
        status = "okay";
        wp-inverted ;
    };
};

驱动程序

/* 此文件为linux 内核 of函数测试文件
 *  实验目的:在init函数中使用of函数读取设备树中的 根节点下xxx设备节点信息
 *  其路径为: 
 */

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_platform.h>
#include <linux/of_device.h>
#include <linux/slab.h>

struct device_node* ofdts_node;
struct property*    blprop;
int ret = 0;
const char* out_string;
int result = 0;
int mem_count = 0;
u32* out_values;
static u32 show_members(int mem_count, char* prop_name)
{
    u8 i;

    if(mem_count < 0){
        printk("fail_property_count_elems_of_size!!!\n");
        return -ENODEV;
    }
    else if (mem_count == 0)
    {   
        printk("%s中有%d个元素\n",prop_name, mem_count);
    }else if(mem_count == 1){
        printk("%s中有%d个元素\n",prop_name, mem_count);
        out_values = kmalloc(sizeof(u32) * mem_count,GFP_KERNEL);
        if(!out_values){
            printk("fail_mem\n");
            return -ENODEV;
        }
        ret = of_property_read_u32(ofdts_node, prop_name, out_values);
        if(ret < 0){
            printk("fail_read_u32!!!\n");
            kfree(out_values);
            return -ENODEV;
        }
        printk("%s中的元素为:%d\n",prop_name, *out_values);  
    }
    else{
        printk("%s中有%d个元素\n",prop_name, mem_count);
        out_values = kmalloc(sizeof(u32) * mem_count,GFP_KERNEL);
        ret = of_property_read_u32_array(ofdts_node, prop_name, out_values,mem_count);
        if(ret < 0){
            printk("fail_read_u32_array!!!\n");
            kfree(out_values);
            return -ENODEV;
        }
        printk("%s中有元素为:",prop_name); 
        for(i = 0; i < mem_count; i++){
            printk("%d\t",out_values[i]); 
        }
    }
    return 0;
}

static int __init ofdts_init(void)
{
    printk("\nofdts_initing.........\n");
    /* 0. 提取 backlight 节点*/
    ofdts_node = of_find_node_by_name(NULL, "backlight");
    if(!ofdts_node){
        goto fail_node;
    }else{
        printk("0. 获取节点:%s 成功~\n", ofdts_node->name);
    }

    /* 1. 获取compatible = "pwm-backlight"  和 status = "okay"这俩都是字符串类型的,使用两个不同的函数分别测试*/
    // 1 compatible
    ret = of_property_read_string(ofdts_node, "compatible", &out_string);
    if(ret < 0){
        goto fail_property_read_string;
    }else{
        printk("1.0 获取compatible成功:%s\n", out_string);
    }
    // 2 status
    blprop = of_find_property(ofdts_node, "status", NULL);
    if(!blprop){
        goto fail_find_property;
    }else{
        printk("1.1 获取status成功:%s\n", (char*)blprop->value);
    }

    /* 2. 获取default-brightness-level = <6>; */
    mem_count = of_property_count_elems_of_size(ofdts_node, "default-brightness-level", sizeof(u32));
    result = show_members(mem_count, "default-brightness-level");
    if(result < 0){
        return result;
    }
    /* 3. brightness-levels = <0 4 8 16 32 64 128 255>; */
    mem_count = of_property_count_elems_of_size(ofdts_node, "brightness-levels", sizeof(u32));
    result = show_members(mem_count, "brightness-levels");

    printk("ofdts_init.........OK!!!\n");   
    return result;

    fail_find_property:
    fail_property_read_string:
    printk("fail_property_read_string!!!\n");
    return -ENODEV;
    fail_node:
    printk("fail_find_node_byname!!!\n");
    return -ENODEV;
}


static void __exit ofdts_exit(void)
{
    kfree(out_values);
    printk("ofdts_exit.........OK!!!\n");
}

/* 注册函数 */
module_init(ofdts_init);
module_exit(ofdts_exit);

/* license和作者*/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("QJY");

注意:程序能够测试通过,可能返回值处理以及错误处理有不合理之处,作者并未排查,本程序仅作为练习测试使用;

测试结果:

lib/modules/4.1.15 #modprobe ofdts
ofdts_initing. . . .. . . . .
0.获取节点: backlight成功~
1.0获取compatible成功:pwm-backlight
1.1 获取status成功:okay
default-brightness-level中有1个元素
default-brightness-level中的元素为:6
brightness-levels中有8个元素
brightness-levels中有元素为:0 4 8 16 32 64 128 255
ofdts_init....OK!!

2. dtb格式

​ dtb作为二进制文件被加载到内存中,然后由内核读取并进行解析,如果对dtb文件的格式不了解,那么在看设备树解析相关的内核代码时将会寸步难行,而阅读源代码才是了解设备树最好的方式。所以,如果需要更透彻的了解设备树解析的细节,第一步就是需要了解设备树的格式。

dtb的由来

​ 设备树的一般操作方式是:开发人员根据开发需求编写dts文件,然后使用dtc将dts编译成dtb文件。dts文件是文本格式的文件,而dtb是二进制文件,在linux启动时被加载到内存中,接下来我们需要来分析设备树dtb文件的格式。

dtb格式总览

dtb的格式是这样的:

startstruct boot_param_header
(alignment gap)
memory reserve map
(alignment gap)
device-tree structure
(alignment gap)
enddevice-tree strings

dtb header

​ 但凡涉及到数据的记录,就一定会有一个总的描述部分,就像磁盘的超级块,书的目录,dtb当然也不例外,这个描述头部就是dtb的header部分,通过这个header部分,用户可以快速地了解到整个dtb的大致信息。

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 */
};
.magic

设备树的魔数,魔数其实就是一个用于识别的数字,表示设备树的开始,linux dtb的魔数为 0xd00dfeed

.totalsize

这个设备树的size,也可以理解为所占用的实际内存空间。

.off_dt_struct

offset to dt_struct,表示整个dtb中structure部分所在内存相对头部的偏移地址

.off_dt_strings

offset to dt_string,表示整个dtb中string部分所在内存相对头部的偏移地址

.off_mem_rsvmap

offset to memory reserve map,dtb中memory reserve map所在内存相对头部的偏移地址

.version

设备树的版本,截至目前的最新版本为17.

.last_comp_version

最新的兼容版本

.boot_cpuid_phys

这部分仅在版本2中存在,后续版本不再使用。

.size_dt_strings

表示整个dtb中string部分的大小

.size_dt_struct

表示整个dtb中struct部分的大小

.alignment gap

中间的alignment gap部分表示对齐间隙,它并非是必须的,它是否被提供以及大小由具体的平台对数据对齐和的要求以及数据是否已经对齐来决定。

.memory reserve map

memory reserve map:描述保留的内存部分,这个map的数据结构是这样的:

{
	uint64_t physical_address;
	uint64_t size;
}

​ 这部分存储了此结构的列表,整个部分的结尾由一个数据为0的结构来表示(即physical_address和size都为0,总共16字节)。

​ 这一部分的数据并非是节点中的memory子节点,而是在设备开始之前(也就是第一个花括号之前)定义的,例如:

/dts-v1/
/memreserve/ 0x10000000  0x100000
/* 在结构提中的表示为 physical_address=0x10000000, size=0x100000 */
{
	...
}

​ 这一部分的作用是告诉内核哪一些内存空间需要被保留而不应该被系统覆盖使用,因为在内核启动时常常需要动态申请大量的内存空间,只有提前进行注册,用户需要使用的内存才不会被系统征用而造成数据覆盖。

​ 值得一提的是,对于设备树而言,即使不指定保留内存,系统也会默认为设备树保留相应的内存空间。同时,这一部分需要64位(8字节)对齐。

device-tree structure

device-tree structure:每个节点都会被描述为一个struct,节点之间可以嵌套,因此也会有嵌套的struct。

  • 一个node开始信号,OF_DT_BEGIN_NODE, 内容为:0x00000001

  • 对于版本1-3而言,这一部分是节点的全路径,以/开头,而对于版本16及以上,这部分只是unit name(root 除外,它没有unit name),unit name是以0结尾的字符串

  • 可选的对齐字节

  • 对于每个属性字段:

    • 由OF_DT_PROP标识,数据为0x00000003
    • 32位的数据,表示属性的size
    • 32位的数据,表示属性名在string block中的偏移地址
    • 属性中的value data.
  • 如果有子节点,递归地对子节点进行描述。

  • 节点结束信号,OF_DT_END_NODE ,数据为0x00000002.
    每个节点的信息都按照上述结构被描述,需要注意的是,所有用于描述一个特定节点的属性都必须在任何子节点之前定义,虽然设备树的层次结构不会因此产生二义性,但是linux kernel的解析程序要求这么做。

device-tree strings

​ device-tree strings:在dtb中有大量的重复字符串,比如"model","compatile"等等,为了节省空间,将这些字符串统一放在某个地址,需要使用的时候直接使用索引来查看。

​ 需要注意的是,属性部分格式为key = value,key部分被放置在strings部分,而value部分的字符串并不会放在这一部分,而是直接放在structure中。

dtb文件解析示例

光说不练假把式,下面我就使用一个简单的示例来剖析dtb的文件格式。

下述示例仅仅是一个演示demo,不针对任何平台,为了演示方便,编写了一个非常简单的dts文件。

/dts-v1/;
/ {
    compatible = "hd,test_dts", "hd,test_xxx";
    #address-cells = <0x1>;
    #size-cells = <0x1>;
    model = "HD test dts";

    chosen {
        stdout-path = "/ocp/serial@ffff";
    };
    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x10000000>;
    };
    led1:led@2000000 {
        compatible = "test_led";
        #address-cells = <0x1>;
        #size-cells = <0x1>;
        reg = <0x200 0x4>;
    };
};

​ 编译当前dts文件,获取对应的dtb文件。

鉴于dtb文件为二进制文件,普通编辑器打开显示乱码,我们使用ultraEdit查看,它将数据以16进制形式显示:

在这里插入图片描述

整个dtb文件还是比较简单的,图中的红色框出的部分为header部分的数据,可以看到:

第1个四字节对应magic,数据为 D00DFEED.
第2四字节对应totalsize,数据为 000001BC,可以由整张图片看出,这个dtb文件的大小由0x0~0x1bb,大小刚好是0x1bc
第3个四字节对应off_dt_struct,数据为00000038。
第4个四字节对应off_dt_strings,数据为00000174,可以由整张图片看到,从0x174开始刚好是字符串开始的地方
第5个四字节对应off_mem_rsvmap,数据为00000028
第6个四字节对应version,数据为00000011,十进制为17
第7个四字节对应last_comp_version,数据为00000010,十进制为16,表示兼容版本16
第8个四字节对应boot_cpuid_phys,数据为00000000,仅在版本2中使用,这里为0
第9个四字节对应size_dt_strings,数据为00000048,表示字符串总长。
第10个四字节对应size_dt_struct,数据为0000013c,表示struct部分总长度。

​ 整个头部为40字节,16进制为0x28,从头部信息中off_mem_rsvmap部分可以得到,reserve memory起始地址为0x28,上文中提到,这一部分使用一个16字节的struct来描述,以一个全为0的struct结尾。

后16字节全为0,可以看出,这里并没有设置reserve memory。

structure 部分

上文回顾:每一个属性都是以 key = value的形式来描述,value部分可选。

偏移地址来到0x00000038(0x28+0x10),接下来8个字节为00000003,根据上述structure中的描述,这是OF_DT_PROP,即标示属性的开始。

接下来4字节为00000018,表明该属性的value部分size为24字节。

接下来4字节是当前属性的key在string 部分的偏移地址,这里是00000000,由头部信息中off_dt_strings可以得到,string部分的开始为00000174,偏移地址为0,所以对应字符串为"compatible".

之后就是value部分,这部分的数据是字符串,可以直接从图片右侧栏看出,总共24字节的字符串"hd,test_dts", “hd,test_xxx”,因为字符串之间以0结尾,所以程序可以识别出这是两个字符串。

可以看出,到这里,compatible = “hd,test_dts”, “hd,test_xxx”;这个属性就被描述完了,对于属性的描述还是非常简单的。

按照固有的规律,接下来就是对#address-cells = <0x1>的解析,然后是#size-cells = <0x1>…

然后就是递归的子节点chosen,memory@80000000等等都是按照上文中提到的structure解析规则来进行解析,最后以00000002结尾。

与根节点不同的是,子节点有一个unit name,即chosen,memory@80000000这些名称,并非节点中的.name属性。

而整个结构的结束由00000009来描述。

一般而言,在32位系统中,dtc在编译dts文件时会自动考虑对齐问题,所以对于设备树的对齐字节,我们只需要有所了解即可,并不会常接触到。

3. dtb转换成device

​ 前面我们了解到dtb的内存分布以后(dtb格式),接下来就来看看内核是如何把设备树解析成所需的device_node

设备树的执行入口setup_arch

linux最底层的初始化部分在HEAD.s中,这是汇编代码,我们暂且不作过多讨论。

在head.s完成部分初始化之后,就开始调用C语言函数,而被调用的第一个C语言函数就是start_kernel

asmlinkage __visible void __init start_kernel(void)
{
    //...
    setup_arch(&command_line);
    //...
}

而对于设备树的处理,基本上就在setup_arch()这个函数中。

可以看到,在start_kernel()中调用了setup_arch(&command_line);

void __init setup_arch(char **cmdline_p)
{
    const struct machine_desc *mdesc;
    
    /* 根据传入的设备树dtb的首地址完成一些初始化操作 */
    mdesc = setup_machine_fdt(__atags_pointer);
    
    /* ... */
    
    /* 保证设备树dtb本身存在于内存中而不被覆盖 */
    arm_memblock_init(mdesc);
    
    /* ... */
    /* 对设备树具体的解析 */
    unflatten_device_tree();
    /* ... */
}

这三个被调用的函数就是主要的设备树处理函数:

  • setup_machine_fdt():根据传入的设备树dtb的首地址完成一些初始化操作。
  • arm_memblock_init():主要是内存相关函数,为设备树保留相应的内存空间,保证设备树dtb本身存在于内存中而不被覆盖。用户可以在设备树中设置保留内存,这一部分同时作了保留指定内存的工作。
  • unflatten_device_tree():对设备树具体的解析,事实上在这个函数中所做的工作就是将设备树各节点转换成相应的struct device_node结构体。

下面我们再来通过代码跟踪仔细分析。

setup_machine_fdt

    const struct machine_desc *mdesc;
    
    // 根据传入的设备树dtb的首地址完成一些初始化操作
    mdesc = setup_machine_fdt(__atags_pointer);

__atags_pointer这个全局变量存储的就是r2的寄存器值,是设备树在内存中的起始地址,将设备树起始地址传递给setup_machine_fdt,对设备树进行解析。

const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys)
{
    const struct machine_desc *mdesc, *mdesc_best = NULL;
    // 内存地址检查
    if (!dt_phys || !early_init_dt_verify(phys_to_virt(dt_phys)))
        return NULL;

    // 读取 compatible 属性
    mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);

    // 扫描各个子节点
    early_init_dt_scan_nodes();
    // ...
}

setup_machine_fdt主要是获取了一些设备树提供的总览信息。

内存地址检查

先将设备树在内存中的物理地址转换为虚拟地址

然后再检查该地址上是否有设备树的魔数(magic),魔数就是一串用于识别的字节码:

  • 如果没有或者魔数不匹配,表明该地址没有设备树文件,函数返回失败
  • 否则验证成功,将设备树地址赋值给全局变量initial_boot_params

读取compatible属性

逐一读取设备树根目录下的compatible属性。

/**
 * of_flat_dt_match_machine - Iterate match tables to find matching machine.
 *
 * @default_match: A machine specific ptr to return in case of no match.
 * @get_next_compat: callback function to return next compatible match table.
 *
 * Iterate through machine match tables to find the best match for the machine
 * compatible string in the FDT.
 */
const void * __init of_flat_dt_match_machine(const void *default_match,
        const void * (*get_next_compat)(const char * const**))
{
    const void *data = NULL;
    const void *best_data = default_match;
    const char *const *compat;
    unsigned long dt_root;
    unsigned int best_score = ~1, score = 0;

    // 获取首地址
    dt_root = of_get_flat_dt_root();
    // 遍历
    while ((data = get_next_compat(&compat))) {
        // 将compatible中的属性一一与内核中支持的硬件单板相对比,
        // 匹配成功后返回相应的machine_desc结构体指针。
        score = of_flat_dt_match(dt_root, compat);
        if (score > 0 && score < best_score) {
            best_data = data;
            best_score = score;
        }
    }

    // ...

    pr_info("Machine model: %s\n", of_flat_dt_get_machine_name());

    return best_data;
}

machine_desc结构体中描述了单板相关的一些硬件信息,这里不过多描述。

主要的的行为就是根据这个compatible属性选取相应的硬件单板描述信息;一般compatible属性名就是"厂商,芯片型号"。

扫描各子节点

第三部分就是扫描设备树中的各节点,主要分析这部分代码。

void __init early_init_dt_scan_nodes(void)
{
    of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
    of_scan_flat_dt(early_init_dt_scan_root, NULL);
    of_scan_flat_dt(early_init_dt_scan_memory, NULL);
}

出人意料的是,这个函数中只有一个函数的三个调用,每次调用时,参数不一样。

of_scan_flat_dt

首先of_scan_flat_dt()这个函数接收两个参数,一个是函数指针it,一个为相当于函数it执行时的参数。

/**
 * of_scan_flat_dt - scan flattened tree blob and call callback on each.
 * @it: callback function
 * @data: context data pointer
 *
 * This function is used to scan the flattened device-tree, it is
 * used to extract the memory information at boot before we can
 * unflatten the tree
 */
int __init of_scan_flat_dt(int (*it)(unsigned long node,
                                     const char *uname, int depth,
                                     void *data),
                           void *data)
{
    unsigned long p = ((unsigned long)initial_boot_params) +
        be32_to_cpu(initial_boot_params->off_dt_struct);
    int rc = 0;
    int depth = -1;

    do {
        u32 tag = be32_to_cpup((__be32 *)p);
        const char *pathp;

        p += 4;
        if (tag == OF_DT_END_NODE) {
            depth--;
            continue;
        }
        if (tag == OF_DT_NOP)
            continue;
        if (tag == OF_DT_END)
            break;
        if (tag == OF_DT_PROP) {
            u32 sz = be32_to_cpup((__be32 *)p);
            p += 8;
            if (be32_to_cpu(initial_boot_params->version) < 0x10)
                p = ALIGN(p, sz >= 8 ? 8 : 4);
            p += sz;
            p = ALIGN(p, 4);
            continue;
        }
        if (tag != OF_DT_BEGIN_NODE) {
            pr_err("Invalid tag %x in flat device tree!\n", tag);
            return -EINVAL;
        }
        depth++;
        pathp = (char *)p;
        p = ALIGN(p + strlen(pathp) + 1, 4);
        if (*pathp == '/')
            pathp = kbasename(pathp);
        rc = it(p, pathp, depth, data);
        if (rc != 0)
            break;
    } while (1);

    return rc;
}

结论:of_scan_flat_dt()函数的作用就是扫描设备树中的节点,然后对各节点分别调用传入的回调函数。

那么重点关注函数指针,在上述代码中,传入的参数分别为

  • early_init_dt_scan_chosen
  • early_init_dt_scan_root
  • early_init_dt_scan_memory

从名称可以猜测,这三个函数分别是处理chosen节点、root节点中除子节点外的属性信息、memory节点。

early_init_dt_scan_chosen
of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);

boot_command_lineboot_command_line是一个静态数组,存放着启动参数,

int __init early_init_dt_scan_chosen(unsigned long node, const char *uname,int depth, void *data){
    // ...
    p = of_get_flat_dt_prop(node, "bootargs", &l);
    if (p != NULL && l > 0)
	    strlcpy(data, p, min((int)l, COMMAND_LINE_SIZE));
    // ...
}

int __init early_init_dt_scan_chosen(unsigned long node, const char *uname,
                     int depth, void *data)
{
    unsigned long l;
    char *p;

    pr_debug("search \"chosen\", depth: %d, uname: %s\n", depth, uname);

    if (depth != 1 || !data ||
        (strcmp(uname, "chosen") != 0 && strcmp(uname, "chosen@0") != 0))
        return 0;

    early_init_dt_check_for_initrd(node);

    /* Retrieve command line */
    // 找到设备树中的的chosen节点中的bootargs,并作为cmd_line
    p = of_get_flat_dt_prop(node, "bootargs", &l);
    if (p != NULL && l > 0)
        strlcpy(data, p, min((int)l, COMMAND_LINE_SIZE));

   // ...

    pr_debug("Command line is: %s\n", (char*)data);

    /* break now */
    return 1;
}

经过代码分析,early_init_dt_scan_chosen的作用是获取从chosen节点中获取bootargs,然后将bootargs放入boot_command_line中,作为启动参数。

而非字面意思的处理整个chosen

以我之前调过的zynq平台为例:

/ {
    model = "ZynqMP ZCU104 RevA";
    compatible = "xlnx,zynqmp-zcu104-revA", "xlnx,zynqmp-zcu104", "xlnx,zynqmp";

    aliases {
        ethernet0 = &gem3;
        gpio0 = &gpio;
        i2c0 = &i2c1;
        mmc0 = &sdhci1;
        rtc0 = &rtc;
        serial0 = &uart0;
        serial1 = &uart1;
        serial2 = &dcc;
        spi0 = &qspi;
        usb0 = &usb0;
    };

    chosen {
        bootargs = "earlycon";
        stdout-path = "serial0:115200n8";
    };

    memory@0 {
        device_type = "memory";
        reg = <0x0 0x0 0x0 0x80000000>;
    };
};

在支持设备树的嵌入式系统中,实际上:

  • uboot基本上可以不通过显式的bootargs=xxx来传递给内核,而是在env拿出,并存放进设备树中的chosen节点中
  • Linux也开始在设备树中的chosen节点中获取出来,

这样子就可以做到针对uboot与Linux在bootargs传递上的统一。

early_init_dt_scan_root
int __init early_init_dt_scan_root(unsigned long node, const char *uname,int depth, void *data)
{
    dt_root_size_cells = OF_ROOT_NODE_SIZE_CELLS_DEFAULT;
    dt_root_addr_cells = OF_ROOT_NODE_ADDR_CELLS_DEFAULT;

    prop = of_get_flat_dt_prop(node, "#size-cells", NULL);
    if (prop)
        dt_root_size_cells = be32_to_cpup(prop);
    prop = of_get_flat_dt_prop(node, "#address-cells", NULL);
    if (prop)
        dt_root_addr_cells = be32_to_cpup(prop);
    // ...
}

通过进一步代码分析,early_init_dt_scan_root为了将root节点中的#size-cells#address-cells属性提取出来,并非获取root节点中所有的属性,放到全局变量dt_root_size_cellsdt_root_addr_cells中。

size-cells和address-cells表示对一个属性(通常是reg属性)的地址需要多少个四字节描述,而地址的长度需要多少个四字节描述,数据长度基本单位为4。

// 表示数据大小为一个4字节描述,32位
#size-cells = 1

// 表示地址由一个四字节描述
#address-cells = 1

// 而reg属性由四个四字节组成,所以存在两组地址描述,
// 第一组是起始地址为0x12345678,长度为0x100,
// 第二组起始地址为0x22,长度为0x4, 
// 因为在<>中,所有数据都是默认为32位。
reg = <0x12345678 0x100 0x22 0x4>  
early_init_dt_scan_memory
int __init early_init_dt_scan_memory(unsigned long node, const char *uname,int depth, void *data){
    // ...
    if (!IS_ENABLED(CONFIG_PPC32) || depth != 1 || strcmp(uname, "memory@0") != 0)
		return 0;
    reg = of_get_flat_dt_prop(node, "reg", &l);
    endp = reg + (l / sizeof(__be32));

    while ((endp - reg) >= (dt_root_addr_cells + dt_root_size_cells)) {
        base = dt_mem_next_cell(dt_root_addr_cells, &reg);
	    size = dt_mem_next_cell(dt_root_size_cells, &reg);
        early_init_dt_add_memory_arch(base, size);
    }
}

函数先判断节点的unit name是memory@0,如果不是,则返回。然后将所有memory相关的reg属性取出来,根据address-cell和size-cell的值进行解析,然后调用early_init_dt_add_memory_arch()来申请相应的内存空间。

    memory@0 {
        device_type = "memory";
        reg = <0x0 0x0 0x0 0x80000000>, <0x8 0x00000000 0x0 0x80000000>;
    };

到这里,setup_machine_fdt()函数对于设备树的第一次扫描解析就完成了,主要是获取了一些设备树提供的总览信息。

arm_memblock_init

// arch/arm/mm/init.c
void __init arm_memblock_init(const struct machine_desc *mdesc)
{
    // ...
    early_init_fdt_reserve_self();
    early_init_fdt_scan_reserved_mem();
    // ...
}

对于设备树的初始化而言,主要做了两件事:

  • 调用early_init_fdt_reserve_self,根据设备树的大小为设备树分配空间,设备树的totalsize在dtb头部中有指明,因此当系统启动之后,设备树就一直存在在系统中。
  • 扫描设备树节点中的"reserved-memory"节点,为其分配保留空间。

memblock_init对于设备树的部分解析就完成了,主要是为设备树指定保留内存空间。

unflatten_device_tree

这一部分就进入了设备树的解析部分:

注意of_root这个对象,我们后续文章中会提到它。实际上,解析以后的数据都是放在了这个对象里面。

void __init unflatten_device_tree(void)
{
    // 展开设备树
    __unflatten_device_tree(initial_boot_params, NULL, &of_root,early_init_dt_alloc_memory_arch, false);

    // 扫描设备树
    of_alias_scan(early_init_dt_alloc_memory_arch);
    // ...
}

展开设备树

property原型
struct property {
    char	*name;
    int	length;
    void	*value;
    struct property *next;
    // ...
};

在设备树中,对于属性的描述是key = value,这个结构体中的name和value分别对应key和value,而length表示value的长度;

next指针指向下一个struct property结构体(用于构成单链表)。

__unflatten_device_tree
__unflatten_device_tree(initial_boot_params, NULL, &of_root,early_init_dt_alloc_memory_arch, false);

我们再来看最主要的设备树解析函数:

void *__unflatten_device_tree(const void *blob,struct device_node *dad,
                              struct device_node **mynodes,void *(*dt_alloc)(u64 size, u64 align),bool detached)
{
    int size;
    // ...
    size = unflatten_dt_nodes(blob, NULL, dad, NULL);
    // ...
    mem = dt_alloc(size + 4, __alignof__(struct device_node));
    // ...
    unflatten_dt_nodes(blob, mem, dad, mynodes);
}

主要的解析函数为unflatten_dt_nodes(),在__unflatten_device_tree()函数中,unflatten_dt_nodes()被调用两次:

  • 第一次是扫描得出设备树转换成device node需要的空间,然后系统申请内存空间;
  • 第二次就进行真正的解析工作,我们继续看unflatten_dt_nodes()函数:

值得注意的是,在第二次调用unflatten_dt_nodes()时传入的参数为unflatten_dt_nodes(blob, mem, dad, mynodes);

unflatten_dt_nodes

第一个参数是设备树存放首地址,第二个参数是申请的内存空间,第三个参数为父节点,初始值为NULL,第四个参数为mynodes,初始值为of_node.

static int unflatten_dt_nodes(const void *blob,void *mem,struct device_node *dad,struct device_node **nodepp)
{
    // ...
    for (offset = 0;offset >= 0 && depth >= initial_depth;offset = fdt_next_node(blob, offset, &depth)) {
        populate_node(blob, offset, &mem,nps[depth],fpsizes[depth],&nps[depth+1], dryrun);
        // ...
    }
}

这个函数中主要的作用就是从根节点开始,对子节点依次调用populate_node(),从函数命名上来看,这个函数就是填充节点,为节点分配内存。

device_node原型
// include/linux/of.h
struct device_node {
    const char *name;
    const char *type;
    phandle phandle;
    const char *full_name;
    // ...
    struct	property *properties;
    struct	property *deadprops;	/* removed properties */
    struct	device_node *parent;
    struct	device_node *child;
    struct	device_node *sibling;
    struct	kobject kobj;
    unsigned long _flags;
    void	*data;
    // ...
};
  • name:设备节点中的name属性转换而来。
  • type:由设备节点中的device_type转换而来。
  • phandle:有设备节点中的"phandle"和"linux,phandle"属性转换而来,特殊的还可能由"ibm,phandle"属性转换而来。
  • full_name:这个指针指向整个结构体的结尾位置,在结尾位置存储着这个结构体对应设备树节点的unit_name,意味着一个struct device_node结构体占内存空间为sizeof(struct device_node)+strlen(unit_name)+字节对齐
  • properties:这是一个设备树节点的属性链表,属性可能有很多种,比如:“interrupts”,“timer”,"hwmods"等等。
  • parent,child,sibling:与当前属性链表节点相关节点,所以相关链表节点构成整个device_node的属性节点。
  • kobj:用于在/sys目录下生成相应用户文件。
populate_node
static unsigned int populate_node(const void *blob,int offset,void **mem,
			  struct device_node *dad,unsigned int fpsize,struct device_node **pnp,bool dryrun){
    struct device_node *np;
    // 申请内存
    // 注,allocl是节点的unit_name长度(类似于chosen、memory这类子节点描述开头时的名字,并非.name成员)
    np = unflatten_dt_alloc(mem, sizeof(struct device_node) + allocl,__alignof__(struct device_node));
    
    // 初始化node(设置kobj,接着设置node的fwnode.ops。)
    of_node_init(np);
    
    // 将device_node的full_name指向结构体结尾处,
    // 即,将一个节点的unit name放置在一个struct device_node的结尾处。
    np->full_name = fn = ((char *)np) + sizeof(*np);
    
    // 设置其 父节点 和 兄弟节点(如果有父节点)
    if (dad != NULL) {
		np->parent = dad;
		np->sibling = dad->child;
		dad->child = np;
	}
    
    // 为节点的各个属性分配空间
    populate_properties(blob, offset, mem, np, pathp, dryrun);
    
    // 获取,并设置device_node节点的name和type属性
    np->name = of_get_property(np, "name", NULL);
	np->type = of_get_property(np, "device_type", NULL);
    if (!np->name)
		np->name = "<NULL>";
	if (!np->type)
		np->type = "<NULL>";
    // ...
}  

一个设备树中节点转换成一个struct device_node结构的过程渐渐就清晰起来,现在我们接着看看populate_properties()这个函数,看看属性是怎么解析的,

populate_properties
static void populate_properties(const void *blob,int offset,void **mem,struct device_node *np,const char *nodename,bool dryrun){
    // ...
    for (cur = fdt_first_property_offset(blob, offset);
         cur >= 0;
         cur = fdt_next_property_offset(blob, cur)) 
    {
        fdt_getprop_by_offset(blob, cur, &pname, &sz);
        unflatten_dt_alloc(mem, sizeof(struct property),__alignof__(struct property));
        if (!strcmp(pname, "phandle") ||  !strcmp(pname, "linux,phandle")) {
            if (!np->phandle)
                np->phandle = be32_to_cpup(val);

            pp->name   = (char *)pname;
            pp->length = sz;
            pp->value  = (__be32 *)val;
            *pprev     = pp;
            pprev      = &pp->next;
            // ...
        }
    }
}

从属性转换部分的程序可以看出,对于大部分的属性,都是直接填充一个struct property属性;

而对于"phandle"属性和"linux,phandle"属性,直接填充struct device_node phandle字段,不放在属性链表中。

扫描节点:of_alias_scan

从名字来看,这个函数的作用是解析根目录下的alias

struct device_node *of_chosen;
struct device_node *of_aliases;

void of_alias_scan(void * (*dt_alloc)(u64 size, u64 align)){
    of_aliases = of_find_node_by_path("/aliases");
    of_chosen = of_find_node_by_path("/chosen");
    if (of_chosen) {
        if (of_property_read_string(of_chosen, "stdout-path", &name))
            of_property_read_string(of_chosen, "linux,stdout-path",
                                    &name);
        if (IS_ENABLED(CONFIG_PPC) && !name)
            of_property_read_string(of_aliases, "stdout", &name);
        if (name)
            of_stdout = of_find_node_opts_by_path(name, &of_stdout_options);
    }
    for_each_property_of_node(of_aliases, pp) {
        // ...
        ap = dt_alloc(sizeof(*ap) + len + 1, __alignof__(*ap));
        if (!ap)
            continue;
        memset(ap, 0, sizeof(*ap) + len + 1);
        ap->alias = start;
        of_alias_add(ap, np, id, start, len);
        // ...
    }
}

of_alias_scan()函数先是处理设备树chosen节点中的"stdout-path"或者"stdout"属性(两者最多存在其一),然后将stdout指定的path赋值给全局变量of_stdout_options,并将返回的全局struct device_node类型数据赋值给of_stdout,指定系统启动时的log输出。

接下来为aliases节点申请内存空间,如果一个节点中同时没有name/phandle/linux,phandle,则被定义为特殊节点,对于这些特殊节点将不会申请内存空间。

然后,使用of_alias_add()函数将所有的aliases内容放置在aliases_lookup链表中。

转换过程总结

img
在这里插入图片描述

此后,内核就可以根据device_node来创建设备。

4. 把device_node转换成platfrom_device

​ 在上一节中讲到设备树dtb文件中的各个节点转换成device_node的过程,每个设备树子节点都将转换成一个对应的device_node节点。

​ 设备树dts文件最终在linux内核中会转化成platform_device:dts -> dtb ->device_node-> platform_device那么,接下来,我们就来看看linux内核如何把device_node转换成platfrom_device。

设备树对于驱动

​ 设备树的产生就是为了替代driver中过多的platform_device部分的静态定义,将硬件资源抽象出来,由系统统一解析,这样就可以避免各驱动中对硬件资源大量的重复定义。

​ 这样一来,几乎可以肯定的是,设备树中的节点最终目标是转换成platform device结构,在驱动开发时就只需要添加相应的platform driver部分进行匹配即可。

device_node转换为platform_device是有条件的

首先,对于所有的device_node,如果要转换成platform_device,除了节点中必须有compatible属性以外,必须满足以下条件:

  • 一般情况下,只对设备树中根的第1级节点(/xx)注册成platform device,也就是对它们的子节点(/xx/*)并不处理。
  • 如果一个结点的compatile属性含有这些特殊的值(“simple-bus”,“simple-mfd”,“isa”,“arm,amba-bus”)之一,并且自己成功注册成了platform_device,那么它的子结点(需含compatile属性)也可以转换为platform_device(当成总线看待)。
  • 根节点(/)是例外的,生成platfrom_device时,即使有compatible属性也不会处理

设备树结点的什么属性会被转换?

如果是device_node转换成platform device,这个转换过程又是怎么样的呢?

在老版本的内核中,platform_device部分是静态定义的,其实最主要的部分就是resources部分,这一部分描述了当前驱动需要的硬件资源,一般是IO,中断等资源。

在设备树中,这一类资源通常通过reg属性来描述,中断则通过interrupts来描述;

所以,设备树中的reg和interrupts资源将会被转换成platform_device内的struct resources资源。

那么,设备树中其他属性是怎么转换的呢?

答案是:不需要转换,在platform_device中有一个成员struct device dev,这个dev中又有一个指针成员struct device_node *of_node。

linux的做法就是将这个of_node指针直接指向由设备树转换而来的device_node结构;留给驱动开发者自行处理。

例如,有这么一个struct platform_device* of_test.我们可以直接通过of_test->dev.of_node来访问设备树中的信息。

platform_device转换的开始

of_platform_default_populate_init

函数的执行入口是,在系统启动的早期进行的of_platform_default_populate_init

// drivers/of/platform.c
static int __init of_platform_default_populate_init(void)
{
    struct device_node *node;

    if (!of_have_populated_dt())
        return -ENODEV;

    /*
     * Handle ramoops explicitly, since it is inside /reserved-memory,
     * which lacks a "compatible" property.
     */
    node = of_find_node_by_path("/reserved-memory");
    if (node) {
        node = of_find_compatible_node(node, NULL, "ramoops");
        if (node)
            of_platform_device_create(node, NULL, NULL);
    }

    /* Populate everything else. */
    of_platform_default_populate(NULL, NULL, NULL);

    return 0;
}
arch_initcall_sync(of_platform_default_populate_init);

在函数of_platform_default_populate_init()中,调用了of_platform_default_populate(NULL, NULL, NULL);,传入三个空指针:

int of_platform_default_populate(struct device_node *root,const struct of_dev_auxdata *lookup,struct device *parent)
{
    return of_platform_populate(root, of_default_bus_match_table, lookup,
                    parent);
}

of_platform_default_populate()调用了of_platform_populate(),我们注意下of_default_bus_match_table

of_default_bus_match_table
const struct of_device_id of_default_bus_match_table[] = {
    { .compatible = "simple-bus", },
    { .compatible = "simple-mfd", },
    { .compatible = "isa", },
    #ifdef CONFIG_ARM_AMBA
        { .compatible = "arm,amba-bus", },
    #endif /* CONFIG_ARM_AMBA */
        {} /* Empty terminated list */
};

如果节点的属性值为 “simple-bus”,“simple-mfd”,“isa”,"arm,amba-bus "之一的话,那么它子节点就可以转化成platform_device。

of_platform_populate

int of_platform_populate(struct device_node *root,
            const struct of_device_id *matches,
            const struct of_dev_auxdata *lookup,
            struct device *parent)
{
    struct device_node *child;
    int rc = 0;

    // 从设备树中获取根节点的device_node结构体
    root = root ? of_node_get(root) : of_find_node_by_path("/");
    if (!root)
        return -EINVAL;

    pr_debug("%s()\n", __func__);
    pr_debug(" starting at: %pOF\n", root);

    //遍历所有的子节点
    for_each_child_of_node(root, child) {
        // 然后对每个根目录下的一级子节点 创建 bus
        // 例如, /r1 , /r2,而不是 /r1/s1
        rc = of_platform_bus_create(child, matches, lookup, parent, true);
        if (rc) {
            of_node_put(child);
            break;
        }
    }
    of_node_set_flag(root, OF_POPULATED_BUS);

    of_node_put(root);
    return rc;
}
EXPORT_SYMBOL_GPL(of_platform_populate);

在调用of_platform_populate()时传入了的matches参数是of_default_bus_match_table[]

这个table是一个静态数组,这个静态数组中定义了一系列的compatible属性:"simple-bus""simple-mfd""isa""arm,amba-bus"

按照我们上文中的描述,当某个根节点下的一级子节点的compatible属性为这些属性其中之一时,它的一级子节点也将由device_node转换成platform_device.

到底是不是这样呢?接着往下看。

of_platform_bus_create

/**
 * of_platform_bus_create() - Create a device for a node and its children.
 * @bus: device node of the bus to instantiate
 * @matches: match table for bus nodes
 * @lookup: auxdata table for matching id and platform_data with device nodes
 * @parent: parent for new device, or NULL for top level.
 * @strict: require compatible property
 *
 * Creates a platform_device for the provided device_node, and optionally
 * recursively create devices for all the child nodes.
 */
static int of_platform_bus_create(struct device_node *bus,
                  const struct of_device_id *matches,
                  const struct of_dev_auxdata *lookup,
                  struct device *parent, bool strict)
{
    const struct of_dev_auxdata *auxdata;
    struct device_node *child;
    struct platform_device *dev;
    const char *bus_id = NULL;
    void *platform_data = NULL;
    int rc = 0;

    // ...

    // 创建of_platform_device、赋予私有数据
    dev = of_platform_device_create_pdata(bus, bus_id, platform_data, parent);
    
    // 判断当前节点的compatible属性是否包含上文中compatible静态数组中的元素
    // 如果不包含,函数返回0,即,不处理子节点。
    if (!dev || !of_match_node(matches, bus))
        return 0;

    for_each_child_of_node(bus, child) {
        pr_debug("   create child: %pOF\n", child);
        // 创建 of_platform_bus
        /* 
         * 如果当前compatible属性中包含静态数组中的元素,
         * 即当前节点的compatible属性有"simple-bus"、"simple-mfd"、"isa"、"arm,amba-bus"其中一项,
         * 把子节点当作对应的总线来对待,递归地对当前节点调用`of_platform_bus_create()`
         * 即,将符合条件的子节点转换为platform_device结构。
         */
        rc = of_platform_bus_create(child, matches, lookup, &dev->dev, strict);
        if (rc) {
            of_node_put(child);
            break;
        }
    }
    of_node_set_flag(bus, OF_POPULATED_BUS);
    return rc;
}
of_platform_device_create_pdata

这个函数实现了:创建of_platform_device、赋予私有数据

此时的参数platform_data为NULL。

/**
 * of_platform_device_create_pdata - Alloc, initialize and register an of_device
 * @np: pointer to node to create device for
 * @bus_id: name to assign device
 * @platform_data: pointer to populate platform_data pointer with
 * @parent: Linux device model parent device.
 *
 * Returns pointer to created platform device, or NULL if a device was not
 * registered.  Unavailable devices will not get registered.
 */
static struct platform_device *of_platform_device_create_pdata(
                    struct device_node *np,
                    const char *bus_id,
                    void *platform_data,
                    struct device *parent)
{
    // 终于看到了平台设备
    struct platform_device *dev;

    // ...

    // 创建实例,将传入的device_node链接到成员:dev.of_node中
    dev = of_device_alloc(np, bus_id, parent);
    if (!dev)
        goto err_clear_flag;

    // 登记到 platform 中
    dev->dev.bus = &platform_bus_type;
    dev->dev.platform_data = platform_data;
    of_msi_configure(&dev->dev, dev->dev.of_node);

    // 添加当前生成的platform_device。
    if (of_device_add(dev) != 0) {
        platform_device_put(dev);
        goto err_clear_flag;
    }

    return dev;

err_clear_flag:
    of_node_clear_flag(np, OF_POPULATED);
    return NULL;
}
of_device_alloc
struct platform_device *of_device_alloc(struct device_node *np,const char *bus_id,struct device *parent)
{
    //统计reg属性的数量
    while (of_address_to_resource(np, num_reg, &temp_res) == 0)
	    num_reg++;
    //统计中断irq属性的数量
    num_irq = of_irq_count(np);
    //根据num_irq和num_reg的数量申请相应的struct resource内存空间。
    if (num_irq || num_reg) {
        res = kzalloc(sizeof(*res) * (num_irq + num_reg), GFP_KERNEL);
        if (!res) {
            platform_device_put(dev);
            return NULL;
        }
        //设置platform_device中的num_resources成员
        dev->num_resources = num_reg + num_irq;
        //设置platform_device中的resource成员
        dev->resource = res;

        //将device_node中的reg属性转换成platform_device中的struct resource成员。  
        for (i = 0; i < num_reg; i++, res++) {
            rc = of_address_to_resource(np, i, res);
            WARN_ON(rc);
        }
        //将device_node中的irq属性转换成platform_device中的struct resource成员。 
        if (of_irq_to_resource_table(np, res, num_irq) != num_irq)
            pr_debug("not all legacy IRQ resources mapped for %s\n",
                np->name);
    }
    //将platform_device的dev.of_node成员指针指向device_node。  
    dev->dev.of_node = of_node_get(np);
    //将platform_device的dev.fwnode成员指针指向device_node的fwnode成员。
    dev->dev.fwnode = &np->fwnode;
    //设备parent为platform_bus
    dev->dev.parent = parent ? : &platform_bus;
}

首先,函数先统计设备树中reg属性和中断irq属性的个数,然后分别为它们申请内存空间,链入到platform_device中的struct resources成员中。

除了设备树中"reg"和"interrupt"属性之外,还有可选的"reg-names"和"interrupt-names"这些io中断资源相关的设备树节点属性也在这里被转换。

将相应的设备树节点生成的device_node节点链入到platform_device的dev.of_node中。

最终,我们能够通过在自己的驱动中,使用struct device_node *node = pdev->dev.of_node;获取到设备树节点中的数据。

of_device_add
int of_device_add(struct platform_device *ofdev){
    // ...
    return device_add(&ofdev->dev);
}

将当前platform_device中的struct device成员注册到系统device中,并为其在用户空间创建相应的访问节点。

这一步会调用platform_match,因此最终也会执行设备树的match,以及probe。

总结

总的来说,将device_node转换为platform_device的过程还是比较简单的。

整个转换过程的函数调用关系是这样的:

                            of_platform_default_populate_init()
                                        |
                            of_platform_default_populate();
                                        |
                            of_platform_populate();
                                        |
                            of_platform_bus_create()
                _____________________|_________________
                |                                      |
        of_platform_device_create_pdata()       of_platform_bus_create()
        _________________|____________________
       |                                      |
 of_device_alloc()                        of_device_add()         

5. 设备树中各个节点是谁转换的

​ 之前,在第4节《把device_node转换成platfrom_device》中提到在设备树的device_node到platform_device转换中,必须满足以下条件:

  • 一般情况下,只对设备树中根的一级子节点进行转换,也就是多级子节点(子节点的子节点)并不处理。但是存在一种特殊情况,就是当某个根子节点的compatible属性为"simple-bus"、“simple-mfd”、“isa”、"arm,amba-bus"时,当前节点中的一级子节点将会被转换成platform_device节点。
  • 节点中必须有compatible属性。

​ 那么,设备树中的一级节点的处理已经完成了,但是现在,我们还有一些子节点的读取不太了解。实际上,没有被处理称为platform_device的设备树节点,都被各个platform_device作为子节点进行处理了。

我们通过解析一棵比较经典的设备树,展示设备树中i2c子系统以及gpio-leds中是如何使用的。

典型的设备树

事实上,在设备树中,通常会存在描述设备驱动的设备树节点被放置在多级子节点的情况:

// arch/arm/boot/dts/zynq-7000.dts
/ {
    // 无效属性
    compatible = "xx"; 
    
    // /amba 节点, 会被转换为`platform_device`,因为它兼容"simple-bus",
    // 同时,在内核中有对应的platform_driver来完成probe
    amba {
        compatible = "simple-bus";
        #address-cells = <0x1>;
        #size-cells = <0x1>;

        /*  /amba/i2c@e0004000 也会被转换为platform_device
         *  因为 父节点 /amba 具备 simple-bus;
         *  而 自己 /amba/i2c@e0004000 也有 compatible 属性
         */
        i2c@e0004000 {
            compatible = "cdns,i2c-r1p10";
            status = "okay";
            // ...
            #address-cells = <0x1>;
            #size-cells = <0x0>;
        };
    };

    // ...
    // 类似的也有 /usb_phy0 节点, 它一般也是用来表示USB控制器, 它会被转换为platform_device, 
    // 同时,在内核中有对应的platform_driver 来完成probe
    usb_phy@0 {
        compatible = "ulpi-phy";
        #phy-cells = <0x0>;
        // ...
    };
    
    aliases {
        // i2c0 代表了 /amba/i2c@e0004000
        i2c0 = "/amba/i2c@e0004000";
    };
    soc: soc { };
};
#######################################################################
// arch/arm/boot/dts/zynq-zc702.dts
#include "zynq-7000.dtsi"
/ {
    model = "Zynq ZC702 Development Board";
    compatible = "xlnx,zynq-zc702", "xlnx,zynq-7000";

    aliases {
        ethernet0 = &gem0;
        i2c0 = &i2c0;
        serial0 = &uart1;
        spi0 = &qspi;
        mmc0 = &sdhci0;
        usb0 = &usb0;
    };
};

&usb0 {
    status = "okay";
    dr_mode = "host";
    usb-phy = <&usb_phy0>;
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_usb0_default>;
};

&i2c0 {
    status = "okay";
    clock-frequency = <400000>;
    pinctrl-names = "default", "gpio";
    pinctrl-0 = <&pinctrl_i2c0_default>;
    pinctrl-1 = <&pinctrl_i2c0_gpio>;
    scl-gpios = <&gpio0 50 0>;
    sda-gpios = <&gpio0 51 0>;

    /* 
     *  /i2c@0/si570 节点不会被转换为platform_device, 
     *  si570被如何处理完全由父节点的platform_driver决定, 
     *  在这里,si570会被创建为 i2c_device。
     */
    si570: clock-generator@5d {
        #clock-cells = <0>;
        compatible = "silabs,si570";
        temperature-stability = <50>;
        reg = <0x5d>;
        factory-fout = <156250000>;
        clock-frequency = <148500000>;
    };
};

&soc {
    #address-cells = <1>;
    #size-cells = <1>;
    ranges = <0 0 0 0xffffffff>;
    compatible = "simple-bus";
    
    mygpio-leds {
        compatible = "gpio-leds";

        led-blue{
            label = "blue";
            default-state = "off";
            //gpios = <&msm_gpio 17 0x00>;
        };
        led-green{
            label = "green";
            default-state = "off";
            //gpios = <&msm_gpio 34 0x00>;
        };
    };
};

显然,i2c@e0004000会被转换成platform_device,而si570则不会,至少在设备树初始化阶段不会被转换,仍旧以device_node的形式存在在内存中。

显而易见,这些设备并非是无意义的设备,接下来我们来看看,驱动中是如何处理的。

/usb

​ 类似的也有 /usb_phy0 节点, 它一般也是用来表示USB控制器, 它会被转换为platform_device, 同时,在内核中有对应的platform_driver 来完成probe

/ {
    // ...
    usb_phy@0 {
        compatible = "ulpi-phy";
        #phy-cells = <0x0>;
        reg = <0xe0002000 0x1000>;
        view-port = <0x170>;
        drv-vbus;
        linux,phandle = <0x7>;
        phandle = <0x7>;
    };
};

由 内核解析的时候,将usb_phy@0转换为platform_device

// drivers/usb/phy/phy-ulpi.c
static const struct of_device_id ulpi_phy_table[] = {
    { .compatible = "ulpi-phy" },
    { },
};
MODULE_DEVICE_TABLE(of, ulpi_phy_table);

static struct platform_driver ulpi_phy_driver = {
    .probe      = ulpi_phy_probe,
    .remove     = ulpi_phy_remove,
    .driver     = {
        .name   = "ulpi-phy",
        .of_match_table = ulpi_phy_table,
    },
};
module_platform_driver(ulpi_phy_driver);

ulpi_phy_probe

​ 加载这个驱动(ulpi_phy_driver)以后,因为设备树中有上述的platform_device,所以会执行对应的probe。

static int ulpi_phy_probe(struct platform_device *pdev)
{
    struct device_node *np = pdev->dev.of_node;
    struct resource *res;
    struct ulpi_phy *uphy;
    bool flag;
    int ret;
    
    /* 申请struct ulpi_phy 结构体类型内存 */
    uphy = devm_kzalloc(&pdev->dev, sizeof(*uphy), GFP_KERNEL);
    if (!uphy)
        return -ENOMEM;
    
    /* 从platform_device节点获取资源 */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    uphy->regs = devm_ioremap(&pdev->dev, res->start, resource_size(res));
    if (IS_ERR(uphy->regs))
        return PTR_ERR(uphy->regs);

    ret = of_property_read_u32(np, "view-port", &uphy->vp_offset);
    if (IS_ERR(uphy->regs)) {
        dev_err(&pdev->dev, "view-port register not specified\n");
        return PTR_ERR(uphy->regs);
    }

    flag = of_property_read_bool(np, "drv-vbus");
    if (flag)
        uphy->flags |= ULPI_OTG_DRVVBUS | ULPI_OTG_DRVVBUS_EXT;
   
    /* 根据.flags创建otg可见接口设备 */
    uphy->usb_phy = otg_ulpi_create(&ulpi_viewport_access_ops, uphy->flags);

    uphy->usb_phy->dev = &pdev->dev;

    uphy->usb_phy->io_priv = uphy->regs + uphy->vp_offset;

    ret = usb_add_phy_dev(uphy->usb_phy);
    if (ret < 0)
        return ret;

    return 0;
}

/amba

​ 还记得我们之前说的,因为 /amba节点的compatible属性符合"simple-bus"规则,所以,把/amba/中的所有子节点当作对应的总线来对待。

即,将符合条件的子节点转换为platform_device结构。

​ 事实上,/amba/i2c@e0004000对应一个i2c硬件控制器,控制器的起始地址是0x44e04000,这个节点的作用就是生成一个i2c硬件控制器的platform_device,与同样被加载到内存中的platform_driver相匹配。此后,在内存中构建一个i2c硬件控制器的描述节点,负责对应i2c控制器的数据收发。

/ {
    // /amba 节点, 会被转换为`platform_device`,因为它兼容"simple-bus",
    // amba 作为 一个soc总线,同时,在内核中有对应的platform_driver;
    amba {
        compatible = "simple-bus";
        #address-cells = <0x1>;
        #size-cells = <0x1>;
        // ...

        // 子节点/amba/i2c@e0004000 也会被转换为platform_device, 因为其父节点/amba具有simple-bus属性
        i2c@e0004000 {
            compatible = "cdns,i2c-r1p10";
            // ...
        };
    };

因此,i2c@e0004000会被注册成为platform_device,那么我们来看看对应的platform_driver

/amba/i2c@e0004000

基本上,只有原厂商以及比较老旧的平台需要自行开发总线控制器。但是我们作为一种学习了解也是不错的。

​ 根据platform driver驱动的规则,需要填充一个struct platform_driver结构体,然后注册到platform总线中,这样才能完成platfrom bus的匹配,因此,我们也可以在同文件下找到相应的初始化部分:

// drivers/i2c/busses/i2c-cadence.c
static const struct of_device_id cdns_i2c_of_match[] = {
    { .compatible = "cdns,i2c-r1p10", // ... },
    { .compatible = "cdns,i2c-r1p14",},
    { /* end of table */ }
};
MODULE_DEVICE_TABLE(of, cdns_i2c_of_match);

static struct platform_driver cdns_i2c_drv = {
    .driver = {
        .name  = DRIVER_NAME,
        .of_match_table = cdns_i2c_of_match,
        .pm = &cdns_i2c_dev_pm_ops,
    },
    .probe  = cdns_i2c_probe,
    .remove = cdns_i2c_remove,
};

module_platform_driver(cdns_i2c_drv);

cdns_i2c_probe

​ 既然platform总线的driver和device匹配上,就会调用相应的probe函数,根据.probe = cdns_i2c_probe,,我们再查看cdns_i2c_probe函数:

/**
 * cdns_i2c_probe - Platform registration call
 * @pdev:   Handle to the platform device structure
 *
 * This function does all the memory allocation and registration for the i2c
 * device. User can modify the address mode to 10 bit address mode using the
 * ioctl call with option I2C_TENBIT.
 *
 * Return: 0 on success, negative error otherwise
 */
static int cdns_i2c_probe(struct platform_device *pdev)
{
    struct resource *r_mem;
    struct cdns_i2c *id;
    int ret;
    const struct of_device_id *match;

    id = devm_kzalloc(&pdev->dev, sizeof(*id), GFP_KERNEL);
    if (!id)
        return -ENOMEM;

    id->dev = &pdev->dev;
    platform_set_drvdata(pdev, id);

    match = of_match_node(cdns_i2c_of_match, pdev->dev.of_node);
    if (match && match->data) {
        const struct cdns_platform_data *data = match->data;
        id->quirks = data->quirks;
    }

    id->pinctrl = devm_pinctrl_get(&pdev->dev);
    if (!IS_ERR(id->pinctrl)) {
        ret = cdns_i2c_init_recovery_info(id, pdev);
        if (ret)
            return ret;
    }

    r_mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    id->membase = devm_ioremap_resource(&pdev->dev, r_mem);
    if (IS_ERR(id->membase))
        return PTR_ERR(id->membase);

    id->irq = platform_get_irq(pdev, 0);

    // 设置 adapter 一些属性
    id->adap.owner = THIS_MODULE;
    id->adap.dev.of_node = pdev->dev.of_node;
    id->adap.algo = &cdns_i2c_algo;
    id->adap.timeout = CDNS_I2C_TIMEOUT;
    id->adap.retries = 3;       /* Default retry value. */
    id->adap.algo_data = id;
    id->adap.dev.parent = &pdev->dev;
    init_completion(&id->xfer_done);
    snprintf(id->adap.name, sizeof(id->adap.name),
         "Cadence I2C at %08lx", (unsigned long)r_mem->start);

    id->clk = devm_clk_get(&pdev->dev, NULL);
    if (IS_ERR(id->clk)) {
        dev_err(&pdev->dev, "input clock not found.\n");
        return PTR_ERR(id->clk);
    }
    ret = clk_prepare_enable(id->clk);
    if (ret)
        dev_err(&pdev->dev, "Unable to enable clock.\n");

    pm_runtime_set_autosuspend_delay(id->dev, CNDS_I2C_PM_TIMEOUT);
    pm_runtime_use_autosuspend(id->dev);
    pm_runtime_set_active(id->dev);
    pm_runtime_enable(id->dev);

    id->clk_rate_change_nb.notifier_call = cdns_i2c_clk_notifier_cb;
    if (clk_notifier_register(id->clk, &id->clk_rate_change_nb))
        dev_warn(&pdev->dev, "Unable to register clock notifier.\n");
    id->input_clk = clk_get_rate(id->clk);

    ret = of_property_read_u32(pdev->dev.of_node, "clock-frequency",
            &id->i2c_clk);
    if (ret || (id->i2c_clk > CDNS_I2C_SPEED_MAX))
        id->i2c_clk = CDNS_I2C_SPEED_DEFAULT;

#if IS_ENABLED(CONFIG_I2C_SLAVE)
    /* Set initial mode to master */
    id->dev_mode = CDNS_I2C_MODE_MASTER;
    id->slave_state = CDNS_I2C_SLAVE_STATE_IDLE;
#endif
    id->ctrl_reg = CDNS_I2C_CR_ACK_EN | CDNS_I2C_CR_NEA | CDNS_I2C_CR_MS;

    ret = cdns_i2c_setclk(id->input_clk, id);
    if (ret) {
        dev_err(&pdev->dev, "invalid SCL clock: %u Hz\n", id->i2c_clk);
        ret = -EINVAL;
        goto err_clk_dis;
    }

    ret = devm_request_irq(&pdev->dev, id->irq, cdns_i2c_isr, 0,
                 DRIVER_NAME, id);
    if (ret) {
        dev_err(&pdev->dev, "cannot get irq %d\n", id->irq);
        goto err_clk_dis;
    }

    cdns_i2c_init(id);

    // 添加成为 adapter
    ret = i2c_add_adapter(&id->adap);
    if (ret < 0)
        goto err_clk_dis;

    dev_info(&pdev->dev, "%u kHz mmio %08lx irq %d\n",
         id->i2c_clk / 1000, (unsigned long)r_mem->start, id->irq);

    return 0;

err_clk_dis:
    clk_disable_unprepare(id->clk);
    pm_runtime_disable(&pdev->dev);
    pm_runtime_set_suspended(&pdev->dev);
    return ret;
}
i2c_add_adapter
// drivers/i2c/i2c-core-base.c
/**
 * i2c_add_adapter - declare i2c adapter, use dynamic bus number
 * @adapter: the adapter to add
 * Context: can sleep
 *
 * This routine is used to declare an I2C adapter when its bus number
 * doesn't matter or when its bus number is specified by an dt alias.
 * Examples of bases when the bus number doesn't matter: I2C adapters
 * dynamically added by USB links or PCI plugin cards.
 *
 * When this returns zero, a new bus number was allocated and stored
 * in adap->nr, and the specified adapter became available for clients.
 * Otherwise, a negative errno value is returned.
 */
int i2c_add_adapter(struct i2c_adapter *adapter)
{
    struct device *dev = &adapter->dev;
    int id;

    if (dev->of_node) {
        // 获取 i2c 设备树节点
        id = of_alias_get_id(dev->of_node, "i2c");
        if (id >= 0) {
            adapter->nr = id;
            return __i2c_add_numbered_adapter(adapter);
        }
    }

    // ...
}
EXPORT_SYMBOL(i2c_add_adapter);
of_alias_get_id
// drivers/of/base.c
/**
 * of_alias_get_id - Get alias id for the given device_node
 * @np:     Pointer to the given device_node
 * @stem:   Alias stem of the given device_node
 *
 * The function travels the lookup table to get alias id for the given
 * device_node and alias stem.  It returns the alias id if find it.
 */
int of_alias_get_id(struct device_node *np, const char *stem)
{
    struct alias_prop *app;
    int id = -ENODEV;

    mutex_lock(&of_aliases_mutex);
    list_for_each_entry(app, &aliases_lookup, link) {
        if (strcmp(app->stem, stem) != 0)
            continue;

        if (np == app->np) {
            id = app->id;
            break;
        }
    }
    mutex_unlock(&of_aliases_mutex);

    return id;
}
EXPORT_SYMBOL_GPL(of_alias_get_id);

之前解析过的aliases节点都会加入到aliases_lookup中。

因此,现在根据名字,获取到对应的i2c节点的id。

alias_prop原型

id实际上就是一个索引,有了id就可以找到这个项目。

// drivers/of/of_private.h
/**
 * struct alias_prop - Alias property in 'aliases' node
 * @link:   List node to link the structure in aliases_lookup list
 * @alias:  Alias property name
 * @np:     Pointer to device_node that the alias stands for
 * @id:     Index value from end of alias name
 * @stem:   Alias string without the index
 *
 * The structure represents one alias property of 'aliases' node as
 * an entry in aliases_lookup list.
 */
struct alias_prop {
    struct list_head link;
    const char *alias;
    struct device_node *np;
    int id;
    char stem[0];
};
__i2c_add_numbered_adapter
// drivers/i2c/i2c-core-base.c
/**
 * __i2c_add_numbered_adapter - i2c_add_numbered_adapter where nr is never -1
 * @adap: the adapter to register (with adap->nr initialized)
 * Context: can sleep
 *
 * See i2c_add_numbered_adapter() for details.
 */
static int __i2c_add_numbered_adapter(struct i2c_adapter *adap)
{
    // ...
    return i2c_register_adapter(adap);
}
i2c_register_adapter

​ 根据这个名称可以看出这是根据设备树描述的硬件i2c控制器而生成的一个i2c_adapter,并注册到系统中,这个i2c_adapter负责i2c底层数据收发。

// drivers/i2c/i2c-core-base.c
static int i2c_register_adapter(struct i2c_adapter *adap)
{
	// ...
	of_i2c_register_devices(adap);
	// ...
}

​ 注意到一个of前缀的函数,看到of就能想到这肯定是设备树相关的函数。

of代表open framework,设备树就是从of来的。

of_i2c_register_devices

为每一个i2c下的节点注册对应为对应的i2c device(即,i2c_client

// drivers/i2c/i2c-core-of.c
void of_i2c_register_devices(struct i2c_adapter *adap)
{
	// ...
    
    // 轮询每个子节点
	for_each_available_child_of_node(bus, node) {
		if (of_node_test_and_set_flag(node, OF_POPULATED))
			continue;

		client = of_i2c_register_device(adap, node);
		if (IS_ERR(client)) {
			dev_warn(&adap->dev,
					"Failed to create I2C device for %pOF\n",
					node);
			of_node_clear_flag(node, OF_POPULATED);
		}
	}
	// ...
}

​ 所以可以看出,of_i2c_register_device()这个函数的作用就是解析设备树中当前i2c中的子节点,并将其转换成相应的i2c_client描述结构。

of_i2c_register_device
// drivers/i2c/i2c-core-of.c
static struct i2c_client *of_i2c_register_device(struct i2c_adapter *adap,struct device_node *node)
{
	// ...
	struct i2c_board_info info = {};
	of_modalias_node(node, info.type, sizeof(info.type);
	of_get_property(node, "reg", &len);
	info.addr = addr;
	info.of_node = of_node_get(node);
	info.archdata = &dev_ad;

	if (of_property_read_bool(node, "host-notify"))
		info.flags |= I2C_CLIENT_HOST_NOTIFY;

	if (of_get_property(node, "wakeup-source", NULL))
		info.flags |= I2C_CLIENT_WAKE;

	result = i2c_new_device(adap, &info);
	// ...
}

i2c_new_device
// drivers/i2c/i2c-core-of.c
struct i2c_client * i2c_new_device(struct i2c_adapter *adap, struct i2c_board_info const *info)
{
	// ...
	struct i2c_client	*client;
	client = kzalloc(sizeof *client, GFP_KERNEL);
	client->adapter = adap;
	client->dev.platform_data = info->platform_data;

	if (info->archdata)
		client->dev.archdata = *info->archdata;
	client->flags = info->flags;
	client->addr = info->addr;
	client->irq = info->irq;
	client->dev.parent = &client->adapter->dev;
	client->dev.bus = &i2c_bus_type;
	client->dev.type = &i2c_client_type;
	client->dev.of_node = info->of_node;
	client->dev.fwnode = info->fwnode;

	if (info->properties) {
		status = device_add_properties(&client->dev, info->properties);
		if (status) {
			dev_err(&adap->dev,
				"Failed to add properties to client %s: %d\n",
				client->name, status);
			goto out_err;
		}
	}
	device_register(&client->dev);
	return client;
	// ...
}

确实如此,从device_node到i2c_client的转换主要是在这两个函数中了:

  • of_i2c_register_device()函数中,从device_node节点中获取各种属性的值记录在info结构体中
  • 然后将info传递给i2c_new_device(),生成一个对应的i2c_client结构并返回。

/amba/i2c@e0004000/si570

&i2c0 {
    status = "okay";
    clock-frequency = <400000>;
    // ...

    /* 
     *  /i2c@0/si570 节点不会被转换为platform_device, 
     *  si570被如何处理完全由父节点的platform_driver决定, 
     *  在这里,si570会被创建为 i2c_client(device)。
     */
    si570: clock-generator@5d {
        #clock-cells = <0>;
        compatible = "silabs,si570";
        temperature-stability = <50>;
        reg = <0x5d>;
        factory-fout = <156250000>;
        clock-frequency = <148500000>;
    };
};

根据刚刚的分析,父节点已经在probe的时候,就帮所有的子节点注册为i2c_client

在i2c子系统中,device称为client

我们看看对应驱动的写法;注意到,这里是 i2c_driver 类型的驱动:

// drivers/clk/clk-si570.c
static const struct of_device_id clk_si570_of_match[] = {
    { .compatible = "silabs,si570" },
    { .compatible = "silabs,si571" },
    { .compatible = "silabs,si598" },
    { .compatible = "silabs,si599" },
    { },
};
MODULE_DEVICE_TABLE(of, clk_si570_of_match);

// 注意到,这里是 `i2c_driver` 类型的驱动
static struct i2c_driver si570_driver = {
    .driver = {
        .name = "si570",
        .of_match_table = clk_si570_of_match,
    },
    .probe      = si570_probe,
    .remove     = si570_remove,
    .id_table   = si570_id,
};
module_i2c_driver(si570_driver);

因此,就会在si570_driver中的probe匹配对应的devrice_driver,我们具体就不再细看了。

/soc/mygpio-leds

/soc同样具备compatible = "simple-bus";,就不再解释了。

&soc {
    #address-cells = <1>;
    #size-cells = <1>;
    ranges = <0 0 0 0xffffffff>;
    compatible = "simple-bus";
    
    // /soc/mygpio-leds 会称为 platform device
    mygpio-leds {
        compatible = "gpio-leds";

        led-blue{
            label = "blue";
            default-state = "off";
            //gpios = <&msm_gpio 17 0x00>;
        };
        led-green{
            label = "green";
            default-state = "off";
            //gpios = <&msm_gpio 34 0x00>;
        };
    };
};

我们看看下面的/soc/mygpio-leds节点,以及下面的子节点/soc/mygpio-leds/{led-blue,led-green}

// drievers/leds/leds-gpio.c
static const struct of_device_id of_gpio_leds_match[] = {
    { .compatible = "gpio-leds", },
    {},
};

MODULE_DEVICE_TABLE(of, of_gpio_leds_match);
static struct platform_driver gpio_led_driver = {
    .probe      = gpio_led_probe,
    .shutdown   = gpio_led_shutdown,
    .driver     = {
        .name   = "leds-gpio",
        .of_match_table = of_gpio_leds_match,
    },
};
module_platform_driver(gpio_led_driver);

gpio_led_probe

实际上,类似刚刚提到的i2c,/soc/mygpio-leds下的子节点会被gpio_led_driver处理,实际上就是在gpio_led_probe中进行的。

// drivers/leds/leds-gpio.c
static int gpio_led_probe(struct platform_device *pdev)
{
    struct gpio_led_platform_data *pdata = dev_get_platdata(&pdev->dev);
    struct gpio_leds_priv *priv;
    int i, ret = 0;

    if (pdata && pdata->num_leds) {
        priv = devm_kzalloc(&pdev->dev,
                sizeof_gpio_leds_priv(pdata->num_leds),
                    GFP_KERNEL);
        if (!priv)
            return -ENOMEM;

        priv->num_leds = pdata->num_leds;
        for (i = 0; i < priv->num_leds; i++) {
            // 填充 为 gpio led 对应的属性
            ret = create_gpio_led(&pdata->leds[i], &priv->leds[i],
                          &pdev->dev, NULL,
                          pdata->gpio_blink_set);
            if (ret < 0)
                return ret;
        }
    } else {
        // 寻找子节点,并解析,创建设备
        priv = gpio_leds_create(pdev);
        if (IS_ERR(priv))
            return PTR_ERR(priv);
    }

    platform_set_drvdata(pdev, priv);

    return 0;
}
创建led
static int create_gpio_led(const struct gpio_led *template,
    struct gpio_led_data *led_dat, struct device *parent,
    struct device_node *np, gpio_blink_set_t blink_set)
{
    int ret, state;

    led_dat->gpiod = template->gpiod;
    if (!led_dat->gpiod) {
        /*
         * This is the legacy code path for platform code that
         * still uses GPIO numbers. Ultimately we would like to get
         * rid of this block completely.
         */
        unsigned long flags = GPIOF_OUT_INIT_LOW;

        /* skip leds that aren't available */
        if (!gpio_is_valid(template->gpio)) {
            dev_info(parent, "Skipping unavailable LED gpio %d (%s)\n",
                    template->gpio, template->name);
            return 0;
        }

        if (template->active_low)
            flags |= GPIOF_ACTIVE_LOW;

        ret = devm_gpio_request_one(parent, template->gpio, flags,
                        template->name);
        if (ret < 0)
            return ret;

        led_dat->gpiod = gpio_to_desc(template->gpio);
        if (!led_dat->gpiod)
            return -EINVAL;
    }

    led_dat->cdev.name = template->name;
    led_dat->cdev.default_trigger = template->default_trigger;
    led_dat->can_sleep = gpiod_cansleep(led_dat->gpiod);
    if (!led_dat->can_sleep)
        led_dat->cdev.brightness_set = gpio_led_set;
    else
        led_dat->cdev.brightness_set_blocking = gpio_led_set_blocking;
    led_dat->blinking = 0;
    if (blink_set) {
        led_dat->platform_gpio_blink_set = blink_set;
        led_dat->cdev.blink_set = gpio_blink_set;
    }
    if (template->default_state == LEDS_GPIO_DEFSTATE_KEEP) {
        state = gpiod_get_value_cansleep(led_dat->gpiod);
        if (state < 0)
            return state;
    } else {
        state = (template->default_state == LEDS_GPIO_DEFSTATE_ON);
    }
    led_dat->cdev.brightness = state ? LED_FULL : LED_OFF;
    if (!template->retain_state_suspended)
        led_dat->cdev.flags |= LED_CORE_SUSPENDRESUME;
    if (template->panic_indicator)
        led_dat->cdev.flags |= LED_PANIC_INDICATOR;
    if (template->retain_state_shutdown)
        led_dat->cdev.flags |= LED_RETAIN_AT_SHUTDOWN;

    ret = gpiod_direction_output(led_dat->gpiod, state);
    if (ret < 0)
        return ret;

    return devm_of_led_classdev_register(parent, np, &led_dat->cdev);
}
根据子节点创建的
static struct gpio_leds_priv *gpio_leds_create(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct fwnode_handle *child;
    struct gpio_leds_priv *priv;
    int count, ret;

    count = device_get_child_node_count(dev);
    if (!count)
        return ERR_PTR(-ENODEV);

    priv = devm_kzalloc(dev, sizeof_gpio_leds_priv(count), GFP_KERNEL);
    if (!priv)
        return ERR_PTR(-ENOMEM);

    device_for_each_child_node(dev, child) {
        struct gpio_led_data *led_dat = &priv->leds[priv->num_leds];
        struct gpio_led led = {};
        const char *state = NULL;
        struct device_node *np = to_of_node(child);

        ret = fwnode_property_read_string(child, "label", &led.name);
        if (ret && IS_ENABLED(CONFIG_OF) && np)
            led.name = np->name;
        if (!led.name) {
            fwnode_handle_put(child);
            return ERR_PTR(-EINVAL);
        }

        led.gpiod = devm_fwnode_get_gpiod_from_child(dev, NULL, child,
                                 GPIOD_ASIS,
                                 led.name);
        if (IS_ERR(led.gpiod)) {
            fwnode_handle_put(child);
            return ERR_CAST(led.gpiod);
        }

        fwnode_property_read_string(child, "linux,default-trigger",
                        &led.default_trigger);

        if (!fwnode_property_read_string(child, "default-state",
                         &state)) {
            if (!strcmp(state, "keep"))
                led.default_state = LEDS_GPIO_DEFSTATE_KEEP;
            else if (!strcmp(state, "on"))
                led.default_state = LEDS_GPIO_DEFSTATE_ON;
            else
                led.default_state = LEDS_GPIO_DEFSTATE_OFF;
        }

        if (fwnode_property_present(child, "retain-state-suspended"))
            led.retain_state_suspended = 1;
        if (fwnode_property_present(child, "retain-state-shutdown"))
            led.retain_state_shutdown = 1;
        if (fwnode_property_present(child, "panic-indicator"))
            led.panic_indicator = 1;

        ret = create_gpio_led(&led, led_dat, dev, np, NULL);
        if (ret < 0) {
            fwnode_handle_put(child);
            return ERR_PTR(ret);
        }
        led_dat->cdev.dev->of_node = np;
        priv->num_leds++;
    }

    return priv;
}

就这样子,用上了。

总结

可以看到,i2c下面的子节点并没有被处理为platform_device

为什么在内核初始化时只将一级子目录节点(compatible属性中含有"simple-bus"、“simple-mfd”、“isa”、"arm,amba-bus"的向下递归一级)转换成platform_device?

对于bus而言,有不同的总线处理方式和不同的driverdevice的命名,自然不能将所有节点全部转换成platform_device

在linux中,将一级子节点视为bus,而多级子节点则由具体的bus去处理。这样子其实也是从另外的角度体现出整个驱动框架的分层思想。

  • 2
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值