dts
DTS即Device Tree Source 设备树源码, Device Tree是一种描述硬件的数据结构,它起源于 OpenFirmware (OF)。
[dts]Device Tree机制 - aaronGao - 博客园
Device Tree 详解 - 魅族内核团队
DTS入门知识(转载) - 大燎原 - CSDN博客
Linux设备树语法详解 - Abnor - 博客园
本系列导航:
Linux DTS(Device Tree Source) 设备树详解之一 (背景基础知识篇)
Linux DTS(Device Tree Source) 设备树详解之二 (dts 匹配及发挥作用的流程篇)
Linux DTS(Device Tree Source) 设备树详解之三 (高通 MSM8953 实例分析篇)
有上一篇文章,我们了解了 dts 的背景知识和相关基础,这次我们对应实际设备进行一下相关分析。
DTS 设备树的匹配过程
一个 dts 文件确定一个项目,多个项目可以包含同一个 dtsi 文件。找到该项目对应的 dts 文件即找到了该设备树的根节点。
kernel\arch\arm\boot\dts\qcom\sdm630-mtp.dts
/dts-v1/;
#include "sdm630.dtsi"
#include "sdm630-mtp.dtsi"
//#include "sdm660-external-codec.dtsi"
#include "sdm660-internal-codec.dtsi"
#include "synaptics-dsx-i2c.dtsi"
/ {
model = "Qualcomm Technologies, Inc. SDM 630 PM660 + PM660L MTP";
compatible = "qcom,sdm630-mtp", "qcom,sdm630", "qcom,mtp";
qcom,board-id = <8 0>;
qcom,pmic-id = <0x0001001b 0x0101011a 0x0 0x0>, <0x0001001b 0x0201011a 0x0 0x0>;
};
&tavil_snd { qcom,msm-mbhc-moist-cfg = <0>, <0>, <3>;};
当然 devicetree 的根节点也是需要和板子进行匹配的,这个匹配信息存放在 sbl(second boot loader)中,对应 dts 文件中描述的 board-id(上面代码中的 qcom,board-id 属性),通过共享内存传递给 bootloader,由 bootloader 将此 board-id 匹配 dts 文件(devicetree 的根节点文件),将由 dtc 编译后的 dts 文件(dtb 文件)加载到内存,然后在 kernel 中展开 dts 树,并且挂载 dts 树上的所有设备。
(ps:cat /proc/cmdline 查看 cmdline)
1. Dts 中相关符号的含义
/
根节点@
如果设备有地址,则由此符号指定&
引用节点:
冒号前的 label 是为了方便引用给节点起的别名,此 label 一般使用为 & label,
属性名称中可以包含逗号。如 compatible 属性的名字 组成方式为 "[manufacturer], [model]",加入厂商名是为了避免重名。自定义属性名中通常也要有厂商名,并以逗号分隔。#
#并不表示注释。如 #address-cells ,#size-cells 用来决定 reg 属性的格式。
空属性并不一定表示没有赋值。如 interrupt-controller 一个空属性用来声明这个 node 接收中断信号
2. Dts 中数据类型
"" 引号中的为字符串,字符串数组:”strint1”,”string2”,”string3”
<> 尖括号中的为 32 位整形数字,整形数组 < 12 3 4>
[] 方括号中的为 32 位十六进制数,十六机制数据 [0x11 0x12 0x13] 其中 0x 可省略
构成节点名的有效字符:0-9a-zA-Z,._+-
构成属性名的有效字符:0-9a-zA-Z,._+?#
DTS 中几个难理解的属性的解释
/ {
#address-cells = <1>;
#size-cells = <1>;
chosen { };
aliases { };
memory { device_type = "memory"; reg = <0 0>; };
};
如上,属性#address-cells
的值为1,它代表以“/”根节点为parent的子节点中,reg属性中存在一个address值;#size-cells
的值为1,它代表以“/” 根节点为parent的子节点中,reg属性中存在一个size值。
即父节点的# address-cells和#size-cells决定了子节点的address和size的长度;Reg的组织形式为reg =
a. 地址
设备的地址特性根据一下几个属性来控制:
reg
#address-cells
#size-cells
reg 意为 region,区域。格式为:reg = <address1 length1 [address2 length2] [address3 length3]>;
父类的 address-cells 和 size-cells 决定了子类的相关属性要包含多少个 cell,
如果子节点有特殊需求的话,可以自己再定义,这样就可以摆脱父节点的控制。
address-cells 决定了 address1/2/3 包含几个 cell,
size-cells 决定了 length1/2/3 包含了几个 cell。
本地SPI模块例如:
spi@10115000{
compatible = "arm,pl022";
reg = <0x10115000 0x1000 >;};
位于 0x10115000 的 SPI 设备申请地址空间,起始地址为 0x10115000,长度为 0x1000,即属于这个 SPI 设备的地址范围是 0x10115000~0x10116000。
实际应用中,有另外一种情况,就是通过外部芯片片选激活模块。例如,挂载在外部总线上,需要通过片选线工作的一些模块:
external-bus{
#address-cells = <2>
#size-cells = <1>;
ethernet@0,0 {
compatible = "smc,smc91c111";
reg = <0 0 0x1000>;
};
i2c@1,0 {
compatible ="acme,a1234-i2c-bus";
#address-cells = <1>;
#size-cells = <0>;
reg = <1 0 0x1000>;
rtc@58 {
compatible ="maxim,ds1338";
reg = <58>;
};
};
flash@2,0 {
compatible ="samsung,k8f1315ebm", "cfi-flash";
reg = <2 0 0x4000000>;
};
};
external-bus 使用两个 cell 来描述地址,一个是片选序号,另一个是片选序号上的偏移量。
而地址空间长度依然用一个 cell 来描述。所以以上的子设备们都需要 3 个 cell 来描述地址空间属性——片选、偏移量、地址长度。
在上个例子中,有一个例外,就是 i2c 控制器模块下的 rtc 模块。因为 I2C 设备只是被分配在一个地址上,不需要其他任何空间,所以只需要一个 address 的 cell 就可以描述完整,不需要 size-cells。
当需要描述的设备不是本地设备时,就需要描述一个从设备地址空间到 CPU 地址空间的映射关系,这里就需要用到 ranges 属性。还是以上边的 external-bus 举例:
#address-cells= <1>;
#size-cells= <1>;
...
external-bus{
#address-cells = <2>
#size-cells = <1>;
ranges = <0 0 0x10100000 0x10000 // Chipselect 1,Ethernet
1 0 0x10160000 0x10000 // Chipselect 2, i2c controller
2 0 0x30000000 0x1000000>; // Chipselect 3, NOR Flash
};
ranges 属性为一个地址转换表。表中的每一行都包含了<子地址、父地址、在子地址空间内的区域大小>。他们的大小(包含的 cell)分别由子节点的 address-cells 的值、父节点的 address-cells 的值和子节点的 size-cells 来决定。以第一行为例:
· 0 0 两个 cell,由子节点 external-bus 的 address-cells=<2> 决定;
· 0x10100000 一个 cell,由父节点的 address-cells=<1> 决定;
· 0x10000 一个 cell,由子节点 external-bus 的 size-cells=<1> 决定。
最终第一行说明的意思就是:片选 0,偏移 0(选中了网卡),被映射到 CPU 地址空间的 0x10100000~0x10110000 中,地址长度为 0x10000。
b. 中断
描述中断连接需要四个属性:
- interrupt-controller 一个空属性用来声明这个 node 接收中断信号;
- #interrupt-cells 这是中断控制器节点的属性,用来标识这个控制器需要几个单位做中断描述符;
- interrupt-parent 标识此设备节点属于哪一个中断控制器,如果没有设置这个属性,会自动依附父节点的;
- interrupts 一个中断标识符列表,表示每一个中断输出信号。
如果有两个,第一个是中断号,第二个是中断类型,如高电平、低电平、边缘触发等触发特性。对于给定的中断控制器,应该仔细阅读相关文档来确定其中断标识该如何解析。一般如下:
二个 cell 的情况:
第一个值: 该中断位于他的中断控制器的索引;
第二个值: 触发的类型 type 固定的取值如下:
1 = low-to-high edge triggered
2 = high-to-low edge triggered
4 = active high level-sensitive
8 = active low level-sensitive
三个 cell 的情况
第一个值:中断号
第二个值:触发的类型
第三个值:优先级,0 级是最高的,7 级是最低的;其中 0 级的中断系统当做 FIQ 处理。
c. 其他
除了以上规则外,也可以自己加一些自定义的属性和子节点,但是一定要符合以下的几个规则:
1. 新的设备属性一定要以厂家名字做前缀,这样就可以避免他们会和当前的标准属性存在命名冲突问题;
2. 新加的属性具体含义以及子节点必须加以文档描述,这样设备驱动开发者就知道怎么解释这些数据了。描述文档中必须特别说明 compatible 的 value 的意义,应该有什么属性,可以有哪个(些)子节点,以及这代表了什么设备。每个独立的 compatible 都应该由单独的解释。
新添加的这些要发送到 devicetree-discuss@lists.ozlabs.org 邮件列表中进行 review,并且检查是否会在将来引发其他的问题。
DTS 设备树描述文件中什么代表总线,什么代表设备
一个含有 compatible 属性的节点就是一个设备。包含一组设备节点的父节点即为总线。
由 DTS 到 device_register 的过程
dts 描述的设备树是如何通过 register_device 进行设备挂载的呢?我们来进行一下代码分析
在 arch/arm/mach-***/***.c 找到 DT_MACHINE_START 和 MACHINE_END 宏, 如下:
DT_MACHINE_START(******_DT, "************* SoC (Flattened DeviceTree)")
.atag_offset = 0x100,
.dt_compat =******_dt_compat, // 匹配dts
.map_io =******_map_io, // 板级地址内存映射, linux mmu
.init_irq =irqchip_init, // 板级中断初始化.
.init_time =******_timer_and_clk_init, // 板级时钟初始化,如ahb,apb等
.init_machine = ******_dt_init, // 这里是解析dts文件入口.
.restart =******_restart, // 重启, 看门狗寄存器相关可以在这里设置
MACHINE_EN
其中. dt_compat = ******_dt_compat 这个结构体是匹配是哪个 dts 文件, 如:
static const char * const ******_dt_compat[] = {
"******,******-soc",
NULL
};
这个 "***,***-soc" 字符串可以在我们的 dts 的根节点下可以找到.
好了, 我们来看看 init_machine = ******_dt_init 这个回调函数.
1. arch/arm/mach-***/***.c : void __init ******_dt_init(void)
******_dt_init(void) --> of_platform_populate(NULL,of_default_bus_match_table, NULL, NULL);
of_default_bus_match_table 这个是 structof_device_id 的全局变量.
const struct of_device_id of_default_bus_match_table[] = { { .compatible = "simple-bus",}, #ifdef CONFIG_ARM_AMBA { .compatible = "arm,amba-bus",}, #endif /* CONFIG_ARM_AMBA */ {} /* Empty terminated list */ };
我们设计 dts 时, 把一些需要指定寄存器基地址的设备放到以 compatible = "simple-bus" 为匹配项的设备节点下. 下面会有介绍为什么.
2. drivers/of/platform.c : int of_platform_populate(...)
of_platform_populate(...) --> of_platform_bus_create(...)
// 在这之前, 会有 of_get_property(bus,"compatible", NULL)
// 检查是否有 compatible, 如果没有, 返回, 继续下一个, 也就是说没有 compatible, 这个设备不会被注册
for_each_child_of_node(root, child) { printk("[%s %s %d]child->name = %s, child->full_name = %s\n", __FILE__, __func__,__LINE__, child->name, child->full_name); rc = of_platform_bus_create(child,matches, lookup, parent, true); if (rc) break; }
论询 dts 根节点下的子设备, 每个子设备都要 of_platform_bus_create(...);
全部完成后, 通过 of_node_put(root); 释放根节点, 因为已经处理完毕;
3. drivers/of/platform.c : of_platform_bus_create(bus, ...)
dev = of_platform_device_create_pdata(bus, bus_id,platform_data, parent); // 我们跳到 3-1步去运行 if (!dev || !of_match_node(matches, bus)) // 就是匹配 // dt_compat = ******_dt_compat, 也就是 compatible = "simple-bus", // 如果匹配成功, 以本节点为父节点, 继续轮询本节点下的所有子节点 return 0; 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); // dev->dev以本节点为父节点, 我们跳到 3-2-1步去运行 if (rc) { of_node_put(child); break; } }
3-1. drivers/of/platform.c : of_platform_device_create_pdata(...)
if (!of_device_is_available(np)) // 查看节点是否有效, 如果节点有'status'属性, 必须是okay或者是ok, 才是有效, 没有'status'属性, 也有效 return NULL; dev = of_device_alloc(np, bus_id, parent); // alloc设备, 设备初始化. 返回dev, 所有的设备都可认为是platform_device, 跳到3-1-1看看函数做了什么事情 if (!dev) return NULL; #if defined(CONFIG_MICROBLAZE) dev->archdata.dma_mask = 0xffffffffUL; #endif dev->dev.coherent_dma_mask =DMA_BIT_MASK(32); // dev->dev 是 struct device. 继续初始化 dev->dev.bus =&platform_bus_type; // dev->dev.platform_data =platform_data; printk("[%s %s %d] of_device_add(device register)np->name = %s\n", __FILE__, __func__, __LINE__, np->name); if (of_device_add(dev) != 0){ // 注册device,of_device_add(...) --> device_add(...) // This is part 2 ofdevice_register() platform_device_put(dev); return NULL; }
3-1-1. drivers/of/platform.c : of_device_alloc(...)
1) alloc platform_device *dev
2) 如果有 reg 和 interrupts 的相关属性, 运行 of_address_to_resource 和 of_irq_to_resource_table, 加入到 dev->resource
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); /*printk("[%s %s %d] res->name = %s, res->start = 0x%X, res->end =0x%X\n", __FILE__, __func__, __LINE__, res->name, res->start,res->end); */ WARN_ON(rc); } WARN_ON(of_irq_to_resource_table(np, res,num_irq) != num_irq);
3) dev->dev.of_node = of_node_get(np);
// 这个 node 属性里有 compatible 属性, 这个属性从 dts 来, 后续 driver 匹配 device 时, 就是通过这一属性进匹配
// 我们可以通过添加下面一句话来查看 compatible.
// printk("[%s %s %d]bus->name = %s, of_get_property(...) = %s\n", FILE, func,LINE, np->name, (char*)of_get_property(np, "compatible",NULL));
// node 再给 dev, 后续给驱动注册使用.
4) 运行 of_device_make_bus_id 设定 device 的名字, 如: soc.2 或 ac000000.serial 等
3-2. drivers/of/platform.c :
以 compatible = "simple-bus" 的节点的子节点都会以这个节点作为父节点在这步注册设备.
至此从 dts 文件的解析到最终调用 of_device_add 进行设备注册的过程就比较清晰了。
查看挂载上的所有设备
cd /sys/devices/ 查看注册成功的设备 对应 devicetree 中的设备描述节点 ^-^
声明:本文中部分内容参考
http://elinux.org/Device_Tree_Usage
https://www.devicetree.org/specifications/
http://blog.csdn[.NET](http://lib.csdn.net/base/dotnet ".NET知识库")/eastonwoo/article/details/51498647