一:设备树概念
Linux内核发展早期arm架构关于SOC和开发板及其外设的板级文件极其庞大,随着越来越多的SOC和开发板的诞生,其板级文件以指数级增长,同时越来越多的“垃圾文件”被编译进了Linux内核中。对此linus有话要说:“This whole ARM thing is a fucking pain in the ass”,arm的东西糟糕透顶了,于是arm就引入了powerpc架构中的设备树技术,将这些描述板级信息的内容从Linux中分离出来,用一个专属的文件格式来描述,这个文件就是设备树,文件扩展名为“.dts”,对于相同SOC作出的不同开发板的共同信息则提取到“.dtsi”文件。Linux内核用到的是DTC工具对DTS源文件编译后的DTB二进制文件,DTC工具源码位于linux内核scripts/dtc目录下,设备树源文件DTS位于arch/arm/boot/dts,编译设备树命令为“make dtbs”。
二:设备树语法
1,设备节点
/ {
aliases {
can0 = &flexcan1;
can1 = &flexcan2;
... ...
};
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>;
... ..
}
}
}
设备树是采用树形结构来描述板子上的设备信息文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,aliases和cpus是根节点“/”下的两个子节点,而cpu0又是cpus下面的子节点。设备节点命名格式:“label:node-name@unit-address”
“label”:节点标签,可以直接通过&label来访问这个节点。不如对于节点“cpu0:cpu@0”来说通过&cpu0就可以访问“cpu@0”这个节点。
“node-name”:节点名字,为ASCII字符串,节点名字可以清洗的描述出该节点的功能,比如“spi1”就是表示这个节点是spi1外设。
“unit-address”:一般表示设备的地址或寄存器首地址,如果没有则可以省略。比如“cpu@0”
2,节点属性
节点是由一堆“属性”组成,节点都是具体的设备,不同的需要的属性不同,用户还可以自定义属性。
(1)compatible:“兼容性”属性,值为一个字符串列表,用于将设备和驱动绑定起来。字符串列表用于选择设备所要使用的驱动程序,设备会从这个字符串列表中依次进行匹配查找对应的驱动文件。
格式:“manufacturer,model”,manfacturer表示厂商,model表示模块对应的驱动名字。
示例:compatible = “fsl,imx6ul-evk-wm8960”,“fsl,imx-audio-wm8960”表示厂商飞思卡尔出品,采用vm8960音频芯片驱动模块。在驱动文件mx-wm8960.c中则会有以下内容与设备树文件完成适配。
sound {
compatible = "fsl,imx6ul-evk-wm8960",
"fsl,imx-audio-wm8960";
}
static const struct of_device_id imx_wm8960_dt_ids[] = {
{ .compatible = "fsl,imx-audio-wm8960", },
{ /* sentinel */ }
};
(2)model:属性值为一个字符串,一般情况描述设备模块信息。
sound {
model = "wm8960-audio";
}
(3)status:属性值为字符串,表示为设备状态
“okay”:表示设备可操作、
“disabled”:表示设备当前不可操作,未来可以变为可操作,例如热插拔设备。
“fail”:表示设备不可操作,设备检测到了一系列错误,未来不太可能变为可操作。
“fail-sss”:表示与“fail”相同,sss为检测到的错误内容。
(4)#address-cells和#size-cells:属性值为无符号32位整形,用于描述子节点的地址信息。
#address-cells属性值决定了子节点reg属性中地址信息所占的字长(32位)。
#size-cells属性值决定了子节点reg属性中长度信息所占的字长(32位)。
(5)reg:属性值和地址相关包括地址长度。
格式:reg = <address1 length1 address2 length2 address3 length3…………>
每个“address length”组合表示一个地址范围,address为起始地址,length为地址长度。#address-cells表示address这个数据所占用的字长,#size-cells表示length这个数据所占的字长。
spi4 {
#address-cells = <1>;
#size-cells = <0>;
gpio_spi: gpio_spi@0 {
reg = <0>;
};
};
aips1: aips-bus@02000000 {
#address-cells = <1>;
#size-cells = <1>;
pwm1: pwm@02080000 {
reg = <0x02080000 0x4000>;
}
}
(6)ranges:属性值可以为空或者按照(chile-bus-address,parent-bus-address,length)格式编写的数字矩阵。
chile-bus-address:子总线地址空间的物理地址,由父节点的#address-cells确定此物理地址所占用的字长
parent-bus-address:父总线地址空间的物理地址,同样由父节点的#address-cells确定此物理地址所占用的字长。
length:子地址空间的长度。
demo_level {
compatible = "simple-bus";
ranges = <0x0 0x30000000 0x3000>;
#address-cells = <1>;
#size-cells = <1>;
range@0 {
compatible = "range";
reg = <0x100 0x200>;
reg-names = "range0";
};
}
节点demo-level属性ranges值为<0x0, 0x30000000, 0x3000>,表示指定一个0x3000大小的地址范围,子地址空间的物理起始地址为0x0,父地址空间的物理起始地址为0x30000000。节点range@0中的reg属性定义寄存器起始地址为0x100,寄存器长度为0x100。经过地址转换可得到range0设备的起始地址为:0x30000100=0x30000000+0x100,结束地址为:0x30000300=0x30000000+0x100+0x200。
(7)name:属性值为字符串,记录了节点名称。
(8)device_type:属性值为字符串,此属性对于设备树来讲只能用于cpu节点或者memory节点。见(二,1插图)
三:设备树与设备匹配方法
每个节点都有“compatible”属性,根节点“/”下同样存在,Linux内核会通过根节点compatible属性查看是否支持该设备,如果支持就启动Linux内核。
/ {
model = "Freescale i.MX6 ULL 14x14 EVK Board";
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
}
1,在未引入设备树前
Linux内核会根据uboot传递过来的一个machine id值判断是否支持该设备。Linux内核中描述每个设备的数据格式如以下示例:
MACHINE_START(S3C2440, "SMDK2440")
/* Maintainer: Ben Dooks <[email protected]> */
.atag_offset = 0x100,
.init_irq = s3c2440_init_irq,
.map_io = smdk2440_map_io,
.init_machine = smdk2440_machine_init,
.init_time = smdk2440_init_time,
MACHINE_END
其中MACHINE_START()原型为:
#define MACHINE_START(_type,_name) \
static const struct machine_desc __mach_desc_##_type \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = MACH_TYPE_##_type, \
.name = _name,
#define MACHINE_END \
};
将参数“S3c2440”和“SMDK2440”带入可得到“.nr = MACH_TYPE_S3C2440; .name = SMDK2440”,其中MACH_TYPE_S3C2440就是SMDK2440这款开发板的machine id,定义在uboot的include/generated/mach-types.h文件,该数据存储在“.arch.info.init”段中。Linux内核就是根据这个machine id判断是否支持该设备。
2,引入设备树后
Linux内核描述设备的数据格式改为以下,通过观察可发现“.nr”的设置已经不一样了,证明引入设备树后已经不会再根据machine id来判断Linux内核是否支持某个设备。
#define DT_MACHINE_START(_name, _namestr) \
static const struct machine_desc __mach_desc_##_name \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = ~0, \
.name = _namestr,
static const char *imx6ul_dt_compat[] __initconst = {
"fsl,imx6ul",
"fsl,imx6ull",
NULL,
};
DT_MACHINE_START(IMX6UL, "Freescale i.MX6 Ultralite (Device Tree)")
.map_io = imx6ul_map_io,
.init_irq = imx6ul_init_irq,
.init_machine = imx6ul_init_machine,
.init_late = imx6ul_init_late,
.dt_compat = imx6ul_dt_compat,
MACHINE_END
machine_desc结构体中的“.dt_compat”成员变量保存着该设备兼容属性,其属性表imx6ul_dt_compat中含有“fsl,imx6ul”和“fsl,imx6ull”,只要某个设备根节点“/”的compatible属性值与imx6ul_dt_compat表中任何一个值匹配,那么就表示Linux内核支持此设备。
3,Linux内核与设备树匹配machine_desc从而启动Linux内核流程分析
在Linux内核启动函数start_kernel中有子函数“setup_arch(&command_line);”内核会用setup_arch函数来匹配machine_desc
asmlinkage __visible void __init start_kernel(void)
{
lockdep_init();
... ...
setup_arch(&command_line);
... ...
}
void __init setup_arch(char **cmdline_p)
{
const struct machine_desc *mdesc;
setup_processor();
mdesc = setup_machine_fdt(__atags_pointer); /* (1) */
if (!mdesc)
mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type);
machine_desc = mdesc;
machine_name = mdesc->name;
dump_stack_set_arch_desc("%s", mdesc->name);
... ...
}
调用(1)处的函数来获取匹配的machine_desc,参数就是atags的首地址,也就是uboot传递给Linux内核的dtb文件的首地址,而返回值mdesc就是找到的最匹配的machine_desc。
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;
mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach); /* (2) */
__machine_arch_type = mdesc->nr;
return mdesc;
}
调用(2)处的函数来获取匹配的machine_desc,参数mdesc_best是默认的machine_desc。找到匹配的machine_desc的过程就是用设备树根节点的compatible属性值和Linux内核中保存的machine_desc结构的“.dt_compat”中的值比较,如果相等的话就表示找到匹配的machine_desc。
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(); /* (3) */
while ((data = get_next_compat(&compat))) {
score = of_flat_dt_match(dt_root, compat); /* (4)*/
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;
}
通过(3)处的函数获得设备树根和节点,while循环就是查找匹配的machine_desc的过程,(4)处的函数会将根节点conpatible属性的值和每个machine_desc结构体中.dt_compat的值进行比较。
4、Linux内核解析DTB文件
asmlinkage __visible void __init start_kernel(void)
{
setup_arch(&command_line);
}
void __init setup_arch(char **cmdline_p)
{
unflatten_device_tree();
}
void __init unflatten_device_tree(void)
{
__unflatten_device_tree(initial_boot_params, &of_root,
early_init_dt_alloc_memory_arch);
}
static void __unflatten_device_tree(void *blob,
struct device_node **mynodes,
void * (*dt_alloc)(u64 size, u64 align))
{
size = (unsigned long)unflatten_dt_node(blob, NULL, &start, NULL, NULL, 0, true);
}
四:设备树常用操作函数
设备树描述了设备的详细信息,这些信息包括数字类型,字符串类型,数组类型,Linux内核提供了一系列的函数来获取这些信息。
设备都是以节点的方式存在于设备树文件中,为了获取这些设备的属性信息,Linux内核使用device_node结构体来描述一个节点。
1、查找节点的函数
struct device_node {
const char *name;
const char *type;
phandle phandle;
const char *full_name;
struct fwnode_handle fwnode;
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;
#if defined(CONFIG_SPARC)
const char *path_component_name;
unsigned int unique_id;
struct of_irq_controller *irq_trans;
#endif
};
(1)通过节点名字查找指定的节点
struct device_node *of_find_node_by_name(struct device_node *from,
const char *name)
(2)通过节点类型type查找指定的节点,也就是device_type属性值
struct device_node *of_find_node_by_type(struct device_node *from,
const char *type)
(3)通过device_type和compatible这两个属性查找指定的节点
struct device_node *of_find_compatible_node(struct device_node *from,
const char *type, const char *compatible)
(4)通过of_device_id匹配表来查找指定的节点
static inline struct device_node *of_find_matching_node_and_match(
struct device_node *from,
const struct of_device_id *matches,
const struct of_device_id **match)
(5)通过路径来查找指定的节点
static inline struct device_node *of_find_node_by_path(const char *path)
2、查找父/子节点的函数
(1)获取指定节点的父节点
struct device_node *of_get_parent(const struct device_node *node)
(2)获取子节点
struct device_node *of_get_next_parent(struct device_node *node)
3、获取属性值函数
struct property {
char *name;
int length;
void *value;
struct property *next;
unsigned long _flags;
unsigned int unique_id;
struct bin_attribute attr;
};
(1)查找指定属性
struct property *of_find_property(const struct device_node *np,
const char *name,
int *lenp)
(2)获取属性中元素的数量,比如获取reg数组属性的数组大小
int of_property_count_elems_of_size(const struct device_node *np,
const char *propname, int elem_size)
(3)获取属性中指定标号的u32类型数据值
static inline int of_property_read_u32_index(const struct device_node *np,
const char *propname, u32 index, u32 *out_value)
(4)获取属性中u8,u16,u32,u64类型的数组数据
static inline int of_property_read_u8_array(const struct device_node *np,
const char *propname, u8 *out_values, size_t sz)
static inline int of_property_read_u16_array(const struct device_node *np,
const char *propname, u16 *out_values, size_t sz)
static inline int of_property_read_u32_array(const struct device_node *np,
const char *propname,
u32 *out_values, size_t sz)
static inline int of_property_read_u64_array(const struct device_node *np,
const char *propname,
u64 *out_values, size_t sz)
(5)获取属性中的整形值
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)
(6)获取属性中字符串值
int of_property_read_string(struct device_node *np, const char *propname,
const char **out_string)
(7)获取#address_cells和#size_cells值
int of_n_addr_cells(struct device_node *np);
int of_n_size_cells(struct device_node *np);
4、其他常用函数
(1)查看节点compatible属性是否包含compat指定的字符串
int of_device_is_compatible(const struct device_node *device,
const char *compat)
(2)获取地址相关属性,“reg”或者“assigned-address”
const __be32 *of_get_address(struct device_node *dev, int index, u64 *size,
unsigned int *flags)
(3)将从设备树读取到的地址转换为物理地址
u64 of_translate_address(struct device_node *dev, const __be32 *in_addr)
(4)获取IIc,SPI等外设的资源“resource”
struct resource {
resource_size_t start;
resource_size_t end;
const char *name;
unsigned long flags;
struct resource *parent, *sibling, *child;
};
int of_address_to_resource(struct device_node *dev, int index,
struct resource *r)
(5)用户直接内存映射,取代ioremap的功能
void __iomem *of_iomap(struct device_node *np, int index)