linux驱动之设备树
一、前言
要学习嵌入式linux设备驱动,就一定逃不过设备树。结果过嵌入式linux的读者都应该知道,设备树是描述硬件的一种方法,能够让设备在不需要改动驱动代码的情况下快速适配不同的方案。那么本文就简述一下设备树的相关描述属性和一些转换过程。当然,本文是笔者的一些理解,如果有错漏,还望指出。
二、设备树描述
设备树编译命令: make dtbs ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
在本问中 带 符号< > 代表必选项, 带 符号[ ]的 代表可选项
- 在 include/dt-bindings目录 下可以查看不同设备下支持的宏定义,这些宏定义可以直接在dts跟代码中使用
- 在 documentation/devicetree/bindings/arm/目录下可以查看各个设备的cell定义
在本节中,主要讲述设备树种常用的几种属性,其余元素读者都可以从文档中获知
2.1 节点命名
节点命名格式一般为 <node_name>[@<unit-address>]
label 命名一般格式为 lable_name: <node_name>[@<unit-address>],label 命名 一般与 节点命名 一起使用,其作用是为了可以在其他的dst文件引用节点,表示硬件之间的关联性。其中 lable_name 的默认命名规则为 <设备类型><index>
2.2 兼容性
兼容性命名格式一般为 <manufacturer>,<model>
其原则为: 从左到右是从具体到抽象,由具体的板子和设备到其他抽象的设备。在添加完设备后需要在驱动程序中写入 of匹配表 结构体,从而使 probe函数 执行。
compatible = "my-test";
2.3 地址
地址 用于获取设备在芯片中使用的地址信息,需要注意的是 不同的设备节点代表的信息并不相同。那么对于 各个设备的地址信息分别代表什么
这个问题本文暂不描述,这需要一定的经验积累,比如i2c设备的子节点的address字段代表的是i2c地址
其命名格式为有以下几种:
- reg = <addrss1 length1 [addrss2 length2] ...> 表示一个设备的地址资源
- #address-cells = <n> 表示用n个cell来描述地址, 每一个cell长度为32bit, 每个cell的声明意义有可能不同,需要查看手册
- #size-cells = <n> 表示用n个cell来描述该地址的大小, 每一个cell长度为32bit
其中 #address-cells 和 #size-cells* 主要用来描述 子节点 里面 reg 的信息,
比如:#address-cells = <1> 表示用一个32位的数来描述地址,#size-cells = <0> 表示用0个32位的数来描述该地址的大小
另外需要注意下面 2 点:
- reg 中的地址个数应该是 address-cells 加 size-cells 的整数倍
- 当 address-cell = <2> 时, 子节点 中 reg 的第一个 address 表示 片选, 第二个 address 表示相对片选的偏移基地址
2.4 中断
设备树种描述中断一般有以下几个元素:
- interrupt-controller: 该属性一般为空,只有中断控制器gic使用,以声明节点自身的身份
- interrupt-parent: 该属性指定本节点所连接到的中断控制器, 在不使用的情况是默认继承父节点所连接的中断控制器, 一般情况下不使用(连接到根节点的中断控制器)
- #interrupt-cells: 表明设备本身中断属性的cell个数
- interrupt : 该属性的 cell个数 由 #interrupt-cells 决定。
-
interrupt-name:为 interrupt 的每个成员命名,在代码中可以通过命名直接获取属性,用于一个设备多个中断的情况
在 arm体系 中, 每个cell的定义可在 Documentation/devicetree/bindings/interrupt-controller/gic.txt 中查看, 其中 SPI 表示 共享中断 ,可以由不同的CPU处理,PPI表示 私有中断 ,由指定的CPU处理
需要注意,在设备树中共享中断要减32,私有中断要减16!!
接下去还有类似gpio属性, clock属性跟pinmux等属性, 在设备驱动需要使用到时可以通过查看相关文档来明白各个属性的用处
咱可以使用下面的例子简单地看一下,每一项可以与上面所讲述的每个设备树成员一 一对应
test_node@0x23336666{
compatible = "my-test";
#address-cells = <1>
#size-cells = <1>
reg = <0x23336666, 0x100>
interrupts = <0 168 4>, <0 169 4>;
interrupt-name = "test1", "test2";
}
test_nodes{
compatible = "test_nodes_compatible"; /* 设备节点的属性 */
reg = <0x12345678 0x2333>;
interrupts = <GIC_SPI 65 IRQ_TYPE_LEVEL_HIGH>;
child_node1{ /* 设备子节点 */
label = "child_node1_label"; /* 设备子节点的属性 */
code = <0x11>;
};
child_node2{
label = "child_node2_label";
code = <0x22>;
};
};
三、简述设备树转换为platform_device
3.2、设备树到 device_node
设备树 dtb文件 到 结构体device_node的转换过程如下
->setup_arch(init/main.c)
->unflatten_device_tree
->__unflatten_device_tree
->unflatten_dt_nodes(扫描大小)
->unflatten_dt_nodes(解析设备树节点)
->populate_node(遍历设备树节点)
->unflatten_dt_alloc
->populate_properties
最主要的是在 函数populate_properties 中,这里可以将每一个属性键值对拷贝到 device_node中,设备树中的节点都是由属性组成,每一项属性都可以在结构体 device_node 中找到对应的成员。
在最终所有的设备树节点串成一个链表,其头部是一个全局变量 of_root
3.2、device_node 到 platform_device
platform_device 数据结构关系如下:
platform_device
{
...
device
device_node
...
}
从device_node到platform_device的转换流程为
of_platform_default_populate_init(drivers/of/platform.c)
->of_platform_device_create
->of_platform_device_create_pdata
->of_device_alloc
->of_platform_default_populate
->of_platform_populate(遍历of_root变量)
->of_platform_bus_create()
->of_platform_device_create_pdata
->of_device_alloc
->_address_to_resource
step1: of_device_alloc 转换函数先统计设备树中 reg属性 和 中断irq属性 的个数,然后分别为它们申请内存空间并链入到 platform_device 中的 struct resources 成员中。除了设备树中 reg 和 interrupt 属性之外,还有可选的 reg-names 和 interrupt-names 这些io中断资源相关的设备树节点属性也在这里被转换。
step2: of_platform_bus_create函数 会检测每一个 设备树节点(device_node) 是有 compatible 属性,如果没有将不对其进行转换,如果该设备树节点有 compatible 属性,并且 compatible属性 是属于以下of_default_bus_match_table 的,那么将被展开为 platform_device
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 */
};
step3: of_platform_device_create_pdata函数 最终在该函数为每一个 device_node 创建 platform_device
step4: of_device_alloc函数完成了对 platform_device 的资源成员的填充,从以前的代码可知,常见资源有寄存器、中断及内存。按笔者的理解,这里主要拷贝的就是 寄存器地址 和 中断
step5: _address_to_resource代码如下,主要就是在之前创建的 platform_device 中的资源拷贝到目标地址,即新开辟的 res结构体。但内核在此之前已经对每一个 device_node 创建了 platform_device,为什么不将之前的这些 platform_device 中的 res成员 的地址赋给新的 res成员 呢。笔者目前不理解这里面的缘由,有待研究
of_device_alloc(...)
{
res = kzalloc(sizeof(*res) * (num_irq + num_reg), GFP_KERNEL);
if (!res) {
platform_device_put(dev);
return NULL;
}
dev->num_resources = num_reg + num_irq;
dev->resource = res;
for (i = 0; i < num_reg; i++, res++) {
rc = of_address_to_resource(np, i, res);
WARN_ON(rc);
}
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);
}
int of_address_to_resource(struct device_node *node,
int index,
struct resource *r)
{
struct platform_device *op = of_find_device_by_node(node);
if (!op || index >= op->num_resources)
return -EINVAL;
memcpy(r, &op->archdata.resource[index], sizeof(*r));
return 0;
}
如果读者们对于转换过程或者设备树的其他接口有兴趣,可以直接阅读源码,设备树接口大部分在include/linux/of.h
四、参考链接
linux内核中的IS_ENABLED():https://www.cnblogs.com/dakewei/p/11326453.html
设备树实现dtb转换platform_device:https://blog.csdn.net/Guet_Kite/article/details/87943165
dtb转换成device_node:https://www.cnblogs.com/downey-blog/p/10485596.html
device_node转换成platform_device:https://www.cnblogs.com/downey-blog/p/10486568.html