1 devicetree 概述
在ARM Linux中,arch/arm/plat-xxx和arch/arm/mach-xxx中充斥着大量的hardcode,很多代码只是在描述板级细节。如板上的platform设备、resource、i2c_board_info、spi_board_info以及各种硬件的platform_data,为了改变这种局面,于是PowerPC等其他体系架构下已经使用的Flattened Device Tree(FDT)进入ARM社区的视野。
Device Tree是一种描述硬件的数据结构,它起源于OpenFirmware (OF)。采用Device Tree后,
许多硬件的细节可以直接透过它传递给Linux,而不再需要在kernel中进行大量的冗余编码。
Devicetree会去解析并扩展dts文件,并生成platform_device,resouce,i2c_board_info等信息,并基于Linux驱动模型,把设备注册到系统中.
2 语义解析
2.1 Device Treesource file语法介绍
Devictree的源文件后缀为*.dts,所有的*.dts经过DTC编译后生成DTB文件,内核负责解析DTB文件.
Device Tree由一系列被命名的结点(node)和属性(property)组成,而结点本身可包含子结点。所谓属性,其实就是成对出现的name和value,以M85项目的dts文件来分析:
/ {
model= "MT6795";
compatible= "mediatek,MT6795";
interrupt-parent= <&gic>;
#address-cells= <2>;
#size-cells= <2>;
/*chosen */
chosen{
bootargs= "console=tty0 console=ttyMT0,921600n1 root=/dev/raminitrd=0x44000000,0x300000 loglevel=8";
};
/* Do not put any bus before mtk-msdc,because it should be mtk-msdc.0 for partition device node usage */
mtk-msdc {
compatible= "simple-bus";
#address-cells= <1>;
#size-cells= <1>;
MSDC0@0x11230000{
compatible= "mediatek,MSDC0";
reg= <0x11230000 0x10000 /*MSDC0_BASE */
0x10001E84 0x2>; /* FPGA PWR_GPIO, PWR_GPIO_EO */
interrupts = <0 79 0x8>;
};
};
};
“/”表示根节点,chose,mtk-msdc为根节点的子节点
其中mtk-msdc节点有子节点MSDC0@0x11230000
model起表示作用
compatible : 设备名,用于和驱动进行匹配
interrupt-parent :标示中断控制器节点
#address-cell :
#size-cell
Reg
reg的组织形式为reg =<address1 length1 [address2 length2] [address3 length3] ... >,其中的每一组addresslength表明了设备使用的一个地址范围。
address为1个或多个32位的整型(即cell),而length则为cell的列表或者为空(若#size-cells= 0)。address 和 length 字段是可变长的,
父结点的#address-cells和#size-cells分别决定了子结点的reg属性的address和length字段的长度。
如一块内存的starting address为 0x100000000, 大小为 0x20000
不同的#address-cell和#size-cell表述如下
#address-cell =1 #size-cell =1
reg = <0x10000000 0x20000>
#address-cell =2 #size-cell =2
reg = <0x00000000 0x100000000 0x000000000x20000>
#address-cell =2 #size-cell =1
reg = <0x00000000 0x1000000000x20000>
interrupts和interrupts-cell
#interrupt-cells – 与#address-cells 和 #size-cells相似,它表明连接此中断控制器的设备的interrupts属性的cell大小
interrupts – 用到了中断的设备结点透过它指定中断号、触发方法等,具体这个属性含有多少个cell,由它依附的中断控制器结点的#interrupt-cells属性决定。而具体每个cell又是什么含义,一般由驱动的实现决定,而且也会在Device Tree的binding文档中说明。譬如,对于ARM GIC中断控制器而言,#interrupt-cells为3,它3个cell的具体含义Documentation/devicetree/bindings/arm/gic.txt就有如下文字说明:
[plain] view plaincopy
The 1st cell is the interrupt type; 0 for SPI interrupts, 1 for PPI
interrupts.
The 2nd cell contains the interrupt number for the interrupt type.
SPI interrupts are in the range [0-987]. PPI interrupts are in the
range [0-15].
The 3rd cell is the flags, encoded as follows:
bits[3:0] trigger type and level flags.
1 = low-to-high edge triggered
2 = high-to-low edge triggered
4 = active highlevel-sensitive
8 = active low level-sensitive
bits[15:8] PPI interrupt cpu mask. Each bit corresponds to each of
the 8 possible cpus attached to the GIC. A bit set to '1' indicated
the interrupt is wired to that CPU. Only valid for PPI interrupts.
另外,值得注意的是,一个设备还可能用到多个中断号。对于ARM GIC而言,若某设备使用了SPI的168、169号2个中断,而言都是高电平触发,则该设备结点的interrupts属性可定义为:interrupts = <0 168 4>, <0 169 4>;
除了中断以外,在ARM Linux中clock、GPIO、pinmux都可以透过.dts中的结点和属性进行描述。
2.2.2 DCT编译器
DTC (device tree compiler)将.dts编译为.dtb的工具。DTC的源代码位于内核的scripts/dtc目录,在Linux内核使能了DeviceTree的情况下,编译内核的时候主机工具dtc会被编译出来,对应scripts/dtc/Makefile中的“hostprogs-y := dtc”这一hostprogs编译target。
在Linux内核的arch/arm/boot/dts/Makefile中,描述了当某种SoC被选中后,哪些.dtb文件会被编译出来,如与VEXPRESS对应的.dtb包括:
[plain] view plaincopy
dtb-$(CONFIG_ARCH_VEXPRESS) += vexpress-v2p-ca5s.dtb\
vexpress-v2p-ca9.dtb \
vexpress-v2p-ca15-tc1.dtb \
vexpress-v2p-ca15_a7.dtb \
xenvm-4.2.dtb
在Linux下,可以单独编译Device Tree文件。当我们在Linux内核下运行make dtbs时,若我们之前选择了ARCH_VEXPRESS,上述.dtb都会由对应的.dts编译出来。因为arch/arm/Makefile中含有一个dtbs编译target项目。
2.2.3 DTB 文件格式
经过Device Tree Compiler编译,Device Tree source file变成了Device Tree Blob(又称作flatteneddevice tree)的格式。Device Tree Blob的数据组织如下图所示:
DTB header。
对于DTB header,其各个成员解释如下:
header field name description
magic 用来识别DTB的。通过这个magic,kernel可以确定bootloader传递的参数block是一个DTB还是tag list。
totalsize DTB的total size
off_dt_struct device tree structure block的offset
off_dt_strings device tree strings block的offset
off_mem_rsvmap offset to memory reserve map。有些系统,我们也许会保留一些memory有特殊用途(例如DTB或者initrdimage),或者在有些DSP+ARM的SOC platform上,有写memory被保留用于ARM和DSP进行信息交互。这些保留内存不会进入内存管理系统。
version 该DTB的版本。
last_comp_version 兼容版本信息
boot_cpuid_phys 我们在哪一个CPU(用ID标识)上booting
dt_strings_size device tree strings block的size。和off_dt_strings一起确定了stringsblock在内存中的位置
dt_struct_size device tree structure block的size。和和off_dt_struct一起确定了devicetree structure block在内存中的位置
3.源码分析
主要分析从DTB解析出node和注册node到系统
3.1 device node创建
设备树中的每个节点都由struect 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将所有的devicenode连接起来
struct device_node *child;---------指向第一个child节点
struct device_node *sibling; -------指向兄弟节点
struct device_node *next; -----通过该指针可以获取相同类型的下一个node
struct device_node *allnext;---通过该指针可以获取nodeglobal list下一个node
struct proc_dir_entry *pde;----开放到userspace的proc接口信息
struct kref kref; ----该node的referencecount
unsigned long _flags;
void *data;
};
__unflatten_device_tree函数中实现了转换:
void __init unflatten_device_tree(void)
{
unsigned long start, mem, size;
struct device_node **allnextp = &allnodes;//设备树的头节点
/* First pass, scan for size */
start = ((unsigned long)initial_boot_params) +
be32_to_cpu(initial_boot_params->off_dt_struct);
size = unflatten_dt_node(0, &start, NULL, NULL, 0);//第一次调用mem传0,allnextpp传NULL,实际上是为了计算整个设备树所要的空间
size = (size | 3) + 1;
/*分配所有Node需要的内存 */
/* Allocate memory for the expanded device tree */
mem = early_init_dt_alloc_memory_arch(size + 4,
__alignof__(struct device_node));
mem= (unsigned long) __va(mem);
((__be32 *)mem)[size / 4] = cpu_to_be32(0xdeadbeef);//越界检测
/* Second pass, do actual unflattening */
start = ((unsigned long)initial_boot_params) +
be32_to_cpu(initial_boot_params->off_dt_struct);
/*内存分配完毕,进行整个设备树解析 */
unflatten_dt_node(mem, &start, NULL, &allnextp, 0);//生成整个设备树
}
//drivers/of/fdt.c
unsigned long __initunflatten_dt_node(unsigned long mem,
unsigned long *p,
struct device_node *dad,
struct device_node***allnextpp,
unsigned long fpsize)
{
struct device_node *np;
struct property *pp, **prev_pp = NULL;
char *pathp;
u32 tag;
unsigned int l, allocl;
int has_name = 0;
int new_format = 0;
tag = be32_to_cpup((__be32 *)(*p));//每个有孩子的设备节点,其tag一定是OF_DT_BEGIN_NODE
if (tag != OF_DT_BEGIN_NODE) {//
pr_err("Weird tag at start of node: %x\n", tag);
return mem;
}
*p += 4;//地址+4,这样指向节点的名称
pathp = (char *)*p;
l= allocl = strlen(pathp) + 1;//该节点名称的长度
*p = _ALIGN(*p + l, 4);//*p指向带分析的属性
/* version 0x10 has a more compact unit name here instead of the full
* path. we accumulate the full path size using "fpsize", we'llrebuild
* it later. We detect this because the first character of the name is
* not '/'.
*/
/*计算fullpath的长度 */
if ((*pathp) != '/') { new_format = 1;
if (fpsize == 0) {/根跟节点
/* root node: special case. fpsize accounts for path
* plus terminating zero. root node only has '/', so
* fpsize should be 2, but we want to avoid the first
* level nodes to have two '/' so we use fpsize 1 here
*/
fpsize = 1;
allocl = 2;//代分配的长度
} else {
/* account for '/' and path size minus terminal 0
* already in 'l'
*/
fpsize += l;//代分配的长度=本节点名称长度+父亲节点绝对路径的长度
allocl = fpsize;
}
}
/*分配一个节点 */
np = unflatten_dt_alloc(&mem, sizeof(struct device_node) + allocl,
__alignof__(structdevice_node));//分配一个设备节点
if (allnextpp) {//allnextpp指向前一个设备链表的指针
memset(np, 0, sizeof(*np));
np->full_name = ((char *)np) + sizeof(struct device_node);
if (new_format) {
char *fn = np->full_name;
/* rebuild full path for new format */
if (dad && dad->parent) {
strcpy(fn,dad->full_name);//把父亲节点绝对路径先拷贝
fn += strlen(fn);
}
*(fn++) = '/';
memcpy(fn, pathp, l);//拷贝本节点的名称
} else
memcpy(np->full_name, pathp, l);
prev_pp = &np->properties;// prev_pp指向节点的属性
**allnextpp = np;//当前节点插入链表
*allnextpp = &np->allnext;
if (dad != NULL) {//父亲节点不为空
np->parent = dad;//指向父亲节点
/* we temporarily use the next field as `last_child'*/
if (dad->next == NULL)//第一个孩子
dad->child = np;//child指向第一个孩子
else
dad->next->sibling =np;//把np插入next,这样孩子节点形成链表
dad->next = np;/*指向最新挂接的child node */
}
kref_init(&np->kref);
}
/*分析设备的属性*/
while (1) {
u32 sz, noff;
char *pname;
tag = be32_to_cpup((__be32 *)(*p));
if (tag == OF_DT_NOP) {//空属性,则跳过
*p += 4;
continue;
}
if (tag != OF_DT_PROP)//tag不是属性则退出,对于有孩子节点退出时为OF_DT_BEGIN_NODE,对于叶子节点退出时为OF_DT_END_NODE
break;
*p += 4;//地址加4
sz = be32_to_cpup((__be32 *)(*p));//属性的大小,是以为占多少整形指针计算的。例如网卡interrupts = <35 2 36 2 40 2>;,其值为24
noff = be32_to_cpup((__be32 *)((*p) + 4));//属性名称的便宜
*p += 8;/*指向value */
if (be32_to_cpu(initial_boot_params->version) < 0x10)
*p = _ALIGN(*p, sz >= 8 ? 8 : 4);
pname = find_flat_dt_string(noff);//属性名称
if (pname == NULL) {
pr_info("Can't find property name in list !\n");
break;
}
if (strcmp(pname, "name") == 0)//如果有名称为name的属性
has_name = 1;
l = strlen(pname) + 1;
pp = unflatten_dt_alloc(&mem, sizeof(struct property),
__alignof__(structproperty));//分配属性结构
if (allnextpp) {
/* We accept flattened tree phandles either in
* ePAPR-style "phandle" properties, or the
* legacy "linux,phandle" properties. If both
* appear and have different values, things
* will get weird. Don't do that.*/
if ((strcmp(pname, "phandle") == 0) ||
(strcmp(pname,"linux,phandle") == 0)) {
if (np->phandle == 0)
np->phandle = *((u32*)*p);
}
/* And we process the "ibm,phandle" property
* used in pSeries dynamic device tree
* stuff */
if (strcmp(pname, "ibm,phandle") == 0)
np->phandle = *((u32 *)*p);
pp->name = pname;//属性名
pp->length = sz;//长度
pp->value = (void *)*p;//属性值
*prev_pp = pp;//属性插入属性链表
prev_pp = &pp->next;
}
*p = _ALIGN((*p) + sz, 4);//指向下一个属性
}
/* with version 0x10 we may not have the name property, recreate
* it here from the unit name if absent
*/
if (!has_name) {//如果没有name,则为该节点生成一个name属性
char *p1 = pathp, *ps = pathp, *pa = NULL;
int sz;
while (*p1) {
if ((*p1) == '@')
pa = p1;
if ((*p1) == '/')
ps = p1 + 1;
p1++;
}
if (pa < ps)
pa = p1;
sz = (pa - ps) + 1;
pp = unflatten_dt_alloc(&mem, sizeof(struct property) + sz,
__alignof__(structproperty));
if (allnextpp) {
pp->name = "name";
pp->length = sz;
pp->value = pp + 1;
*prev_pp = pp;
prev_pp = &pp->next;
memcpy(pp->value, ps, sz - 1);//名字的值只去其中的@钱的字符,例如pcie@ffe0a000-> pcie
((char *)pp->value)[sz - 1] = 0;
pr_debug("fixed up name for %s -> %s\n", pathp,
(char *)pp->value);
}
}
if (allnextpp) {//如果有属性链表
*prev_pp = NULL;
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>";
}
while (tag == OF_DT_BEGIN_NODE || tag == OF_DT_NOP) {//对于tag为这2个取值
if (tag == OF_DT_NOP)//空属性则指向下个属性
*p += 4;
else
mem = unflatten_dt_node(mem, p, np, allnextpp,fpsize);//OF_DT_BEGIN_NODE则表明其还有子节点,所以递归分析其子节点
tag = be32_to_cpup((__be32 *)(*p));
}
if (tag != OF_DT_END_NODE) {//对于叶子节点或者分析完成
pr_err("Weird tag at end of node: %x\n", tag);
return mem;
}
*p += 4;
return mem;
}
3.2 device node注册
解析完DTB以后,会调用of_platform_populate函数创建platform device,调用顺序如下:
of_platform_populate
of_platform_bus_create
of_platform_device_create_pdata
of_device_alloc
of_device_add
staticint 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)
{
conststruct of_dev_auxdata *auxdata;
structdevice_node *child;
structplatform_device *dev;
constchar *bus_id = NULL;
void*platform_data = NULL;
intrc = 0;
/*创建platformdevice结构 */
dev= of_platform_device_create_pdata(bus, bus_id, platform_data, parent);
if(!dev || !of_match_node(matches, bus))/*如果不匹配matchs,则不需要为子节点创建platform_device*/
return0;
/*遍历所有子节点,递归条用of_platform_bus_create*/
for_each_child_of_node(bus,child) {
pr_debug(" create child: %s\n",child->full_name);
rc= of_platform_bus_create(child, matches, lookup, &dev->dev, strict);
}
returnrc;
}
struct platform_device*of_platform_device_create_pdata(
structdevice_node *np,
constchar *bus_id,
void*platform_data,
structdevice *parent)
{
structplatform_device *dev;
/*分配platform_device结构 */
dev= of_device_alloc(np, bus_id, parent);
if(!dev)
returnNULL;
dev->dev.coherent_dma_mask= DMA_BIT_MASK(32);
dev->dev.bus= &platform_bus_type;
dev->dev.platform_data= platform_data;
/*把pltatform添加到系统中 */
if(of_device_add(dev) != 0) {
platform_device_put(dev);
returnNULL;
}
returndev;
}
4 常用API
1.根据compatible找到对应的devicenode结构
struct device_node*of_find_compatible_node(struct device_node *from,
constchar *type, const char *compatible)
更加full path 找到对应的device node
struct device_node*of_find_node_by_path(const char *path)
2. 在device node 中获取属性值
struct property *of_find_property(conststruct device_node *np,
const char *name,
int *lenp)
3. 检查device node的是否与compa匹配
int of_device_is_compatible(const structdevice_node *device,
constchar *compat)
4. 读取属性值
static inline intof_property_read_u32(const struct device_node *np,
const char *propname,
u32 *out_value)
5. 通过phandle引用node
struct device_node *of_parse_phandle(conststruct device_node *np,
const char *phandle_name, int index)
6.映射中断
unsigned int irq_of_
5. 使用案例
以触摸屏为来说明如何使用.
另外i2c设备必须是i2c控制器的子设备才能顺利添加到系统中.
Dts文件如下:
/{
bus{
I2C2@0x11009000{
status="okay";
#size-cells = <0>;
synaptics_touch@20{
compatible = "synaptics,touch" ;
reg= <0x20>;
touch_irq=<2>;
};
};
};
驱动中添加:
static struct i2c_driversynaptics_i2c_driver = {
.driver = {
.name = TPD_DEVICE,
.owner = THIS_MODULE,
#ifdef CONFIG_OF
.of_match_table = &of_match,
#endif
},
.probe = synaptics_rmi4_probe,
.remove = synaptics_rmi4_remove,
.id_table = tp_id,
};
static const struct of_device_id of_match[]= {
{.compatible = "synaptics,touch",},/* 要和dts中的compatible匹配*/
{/*sentinel */},
};