浅析Linux DeviceTree

时间:2023-12-17 23:07:32

文本将介绍Linux DeviceTree的相关知识,包括DeviceTree源文件、结构、语法、编写规则等。

DeviceTree基础

DeviceTree(以下简称DT)用于描述设备信息以及设备于总线之间的层级关系,DT可用于描述绝大多数板级设备的细节,包括CPU、内存、中断、总线以及外设等,与DT相关的Object有dts、dtsi、dtc、dtb、dt.img。

  • dts:DT源文件称为dts文件,Ascii文本文件,一般一个dts文件对应一个Machine,ARM架构下dts文件存放于arch/arm/boot/dts/目录下
  • dtsi:多个Machine/SoC公用的dt文件,i代表include
  • dtc:DeviceTree Compile,用于将dts文件编译成二进制dtb文件
  • dtb:DeviceTree Bolb,由dtc编译dts文件生成的二进制目标文件
  • dt.img:多个dtb文件打包形成dt.img,以适配多个Machine,dts/dtb的结构是标准化的,dt.img有头信息和多个dtb组成,因为没有统一的标准,不同的厂商头信息可能是不同的

目前Android厂商大都使用kernel + ramdisk.img + dt.img的方式打包成boot.img。

本章将详细介绍如下内容:

  • devicetree文件结构
  • devicetree语法基础
  • devicetree文件结构实例解析
  • device tree compile用法介绍

最新内容请参考:lonzoc's gitbook

DTS文件结构


DTS文件主要由:root-node、child-node、property、include组成

  • root-node: 由'/'表示,DT的Entry Point,所有设备均以子节点的形式处于根节点下
  • child-node: node的形式为 node-name {};{}中是该node的实际内容,根节点下一般是Platform设备和总线,外设以子节点形式存在于总线类的节点中。如下的示例中,cpus 这个节点位于根节点下,代表着所有cpu,cpu0~x以子节点形式处于cpus下,代表着SoC上所有的cpu
  • property: 属性,以key-value的形式表示,位于节点中
  • include file: 用于包含其他源文件到dts中,dtsi一般中多个Machine公用的文件(i代表include),h文件在dts中一般用于宏定义
 /include/ "skeleton.dtsi"
/include/ <dt-bindings/clock/msm-clocks-a7.h>
/ {
model = "Qualcomm Technologies, Inc. MSM 8226";
compatible = "qcom,msm8226";
interrupt-parent = <&intc>; cpus {
#size-cells = <0>;
#address-cells = <1>; CPU0: cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a7";
reg = <0x0>;
};

注:

  1. 并非所有node都代表实际的设备,例如上例中的cpus。
  2. node通常还可以用来表示Runtime Configuration,例如bootargs,boot address

DT语法基础

这一节将介绍两个概念:Property,Label and Reference。

Property

属性(Property)是DT中描述设备的原语,其形式为:

          Key = Value;

Key的种类有很多种,但通常可以大致分为:dt保留的key,例如compatible/#address-cell/reg等;以及厂商自定义的key,例如goodix,irq-gpio等。需要明确的是,并非IEEE或者Linux定义了key的种类,key种类由解析devicetree的代码决定,之所以说compatible/reg这些属性是保留的是因为内核中这些key已经约定俗成了。

按key可以表述的数据类型,可以分为:

  • 字符串:string-prop = "a string";
  • 字符串列表:string-list = "hi","str","list";
  • 整型数据:cell-prop = <0xaa>;
  • 二进制数据:binary-prop = [aa bb f8];
  • 混合数据:mixed-prop = "str",<0x123>,[ff dd];
  • 布尔类型:bool-porp;
  • 引用类型:ref-prop = <&other-node>

可以看到,devicetree中使用不同的符号包裹数据来表示不同的数据类型;上述数据类型中的key只是为了表达意思,并非linux devicetree中使用的key;布尔类型的Property只有key没有value,例如定义了bool-prop这代表这个属性为true,若没定义则代表该属性为false

常见的Key及意义

Key 类型 释义
compatible string 用于device与driver匹配
reg integer 表示设备的地址空间
#address-cells integer 设置子节点reg属性中地址数据的cell数
#size-cells integer 设置子节点reg属性中地址长度的cell数
interrupt-parent reference 指定该设备的中断连接到的int controller

上表中知识简单罗列了最常用的Key,实际Machine的dts中包含众多的Key,并且绝大多数Key是由SoC厂商或者外设厂商自定义的。有关这些Key的意义以及写法介绍,内核要求厂商在Documents/devicetree/bindings/目录下提供文档介绍,如果大家在阅读或编写dts文件时遇到问题,应该首先到bindings目录下查阅对应的介绍文档。

有关属性中Key的命名原则有一个约定俗成的做法:SoC厂商或其他外设厂商应使用 vendor,prop的形式。属性Compatible也应遵守"vendor,model"的形式。 例如qcom,qcom,clk-rates;goodix,irq-gpio

Labels and Reference

标签和引用是为了方便编写dts文件,当标签用于节点时开发人员可在任意地方引用该标签而不用关注该标签的全路径;当标签应用于属性时,开发人员可以在其他属性中引用该标签,避免重复的工作。例如Label一个字符串,避免在每个需要的地方重复写同一个字符串。

全路径在dts中即节点的在树形结构中的位置,即从根节点索引到该节点所经过的所有父节点组合

Lable的方法很简单,请看如下例子

Label
i2c0: i2c@ff121288 {};
msmgpio: gpio@fd510000 {
msmgpio-comp: compatible = "msm-gpio";
}; Reference
/*goodix-ts is a i2c touch device */
&i2c0 {
goodix-ts@5d {
goodix,rst-gpio = <&msmgpio 16 0x00>;
}
} Without Reference
/ {
i2c@ff121288 {
goodix-ts@5d {
goodix,rst-gpio = <&msmgpio 16 0x00>;
}
};
};

上例中i2c0msmgpiomsmgpio-comp均是Label,我们在描述goodix-ts设备的时候引用了标签。 假设goodix-ts挂载在i2c0上,不必像Without Reference下的示例一样完整书写i2c0的全路径,而只需引用前面定义的i2c0这个标签即可。属性goodix,rst-gpio中引用到了msmgpio,这是比较常见的设定gpio pin的方法。对比使用标签引用和不使用标签引用的写法我们就能感受到标签与引用的妙处了。

另外,也可以使用alias为标签定义别名,如:alias { i2c_0 = &i2c0};,这样在引用的地方就不用再写&号了,alias+label+reference是非常常用的做法。

Linux DTS文件结构解析

本节将以Qcom的dts文件为例,介绍dts文件的结构、dts如何与一个Machine对应。

内核版本:MSM Kernel3.10

Repo:https://android.googlesource.com/kernel/msm

Branch: android-msm-angler-3.10-marshmallow-dr

Qcom的dts文件位于arch/arm/boot/dts/qcom/,arm64平台的dts软链接到前面的目录下,因此arm/arm64平台的dts均存放于前面的目录下。以Qcom MSM8974平台为例,其对应的dts文件有

msm8974-v1-fluid.dts
msm8974-v1-liquid.dts
msm8974-v1-mtp.dts
msm8974-xx-xxx.dts

这里仅罗列几个dts文件,由于平台版本的不同,存在不同的dts。那Build内核的时候是如何决定哪些dts需要被编译呢? 答案在arch/arm/boot/dts/qcom/Makefile中,

ifeq ($(CONFIG_OF),y)
#include $(srctree)/arch/arm/boot/dts/qcom/Makefile.board
dtb-$(CONFIG_ARCH_MSM8974) += msm8974-v1-cdp.dtb \
msm8974-v1-fluid.dtb \
msm8974-v1-liquid.dtb \
msm8974-v1-mtp.dtb \
msm8974-v1-rumi.dtb \
msm8974-v1-sim.dtb \

以上截取了Makefile的一部分,Build Kernel时将根据实际Platform类型决定哪些dts文件将被包含到dtb target中,本例中平台为MSM8974,包含了数个dts文件。最终多个dtb文件打包成dt.img,dt.img与Kernel,Ramdisk一起打包生成boot.img。Bootloader启动时从SoC寄存器中读取id信息, 从dt.img中找到对应的dtb并装载到RAM中然后传递地址给Kernel,匹配的属性即:dtb根节点下的compatible、qcom,msm-id属性。

一个具体的dts文件通常包含多个dtsi文件,大致分为以下几类

  1. 骨架类dtis - skeleton.dtsi,skeleton64.dtsi
  2. pinctrl dtsi - msm8974-pinctrl.dtsi,定义Pin Mux
  3. regulator dtsi - msm8974-regulator.dtsi,定义电源
  4. clock dtsi - msm8974-clock.dtsi,定义时钟信号
  5. panel/camera/sensor dtsi - msm8974-mdss-panel.dtsi,msm8974-camera-sensor-xxx.dtsi
  6. gpu/ion/leds/xxx

dtsi类型比较多,这里进列出部分。

Skeleton DTSI

Skeleton的作用是定义设备启动所需要的最小的组件,它定义了root-node下的最基本且必要的child-node类型,通常对应SoC上的基础设施如CPU,Memory等。

/*
* Skeleton device tree in the 64 bits version; the bare minimum
* needed to boot; just include and add a compatible value. The
* bootloader will typically populate the memory node.
*/
/ {
#address-cells = <2>;
#size-cells = <2>;
cpus { };
soc { };
chosen { };
aliases { };
memory { device_type = "memory"; reg = <0 0 0 0>; };
};

如上Code来源于skeleton64.dtsi,在root-node下一共定义了5个基本的child-node,cpus代表SoC上所有的cpu,具体cpu的个数以及参数定义在cpus的child-node下;soc代表平台上的所有片上外设以及片外外设,例如uart/clock/spi/i2c/display/...具体这些外设的定义在其他dts/dtsi文件中;chosen用于定义runtime configuration;aliases用于定义node的别名;memory用于定义设备上的物理内存。

address-cells定义了根节点的所有子节点的地址空间属性(reg属性)中使用2个cell来表示启示地址

size-cells定义了根节点的所有子节点的地址空间属性(reg属性)中使用2个cell来表示地址空间的长度

Main-DTS File

Main dts file将各种功能的dtsi文件组合,形成一个功能完善的Machine(SoC+Periph),以msm8974-v1-fluid.dts为例

/dts-v1/;      //定义了dts的版本
#include "msm8974-v1.dtsi" // 各种设备的定义分散到独立的dtsi中
#include "msm8974-fluid.dtsi"
/ {
model = "Qualcomm Technologies, Inc. MSM 8974 FLUID";
compatible = "qcom,msm8974-fluid", "qcom,msm8974", "qcom,fluid";
qcom,msm-id = <126 3 0>,
<185 3 0>,
<186 3 0>;
};
&pm8941_chg {
qcom,charging-disabled;
};

细心的同学可能看到了有些属性直接定义在root-node下,这部分属性用于定义Platform的ID信息,compatible属性定义匹配的平台类型,qcom,msm-id是SoC厂商定义的Key,这个属性定义匹配的SoC,Kernel启动时会从SoC的寄存器中读取id与之比对。

DTS中&用于引用已经定义的node,假设要在8974-v1-fluid.dts中定义个i2c adpater,则应该这样写

&soc {
// i2c adapter属于SoC上的设备,因此定义在soc节点下
i2c@0xff188888 {
// node content
}
i2c@0xff399999 {
// node content
}
};

这里又出现了一个符号@,@的作用是描述一个设备的地址信息,准确的说是该设备在IO地址空间的起始地址。

IO地址空间是读写和控制所有SoC上的外设的地址空间,可以理解为将SoC上的外设的寄存器地址映射成CPU可以理解的地址空间,例如上述代码中的0xff188888,CPU可以通过访问以0xff188888开始的地址来读写和控制i2c(当然这个地址是我假设的)

在ARM架构中,Memory和IO被映射到一个同一个地址空间内,CPU访问Memory和访问IO使用相同的指令,但这一点在x86平台中是不同的。

@符号还用与区分相同类型的外设,例如SoC上拥有4个i2c apapter,他们可以都叫i2c,但用@+地址来区分;当然你可以可以用aliase来定别名,这样就使用加@了。

不同的dtsi定义了不同类型的参数,实际开发过程中可以根据文件名或者include关系找到你需要关注的设备,当然我更喜欢用find/grep命令来帮助我定位设备。

DeviceTree Compiler(DTC)


DTC 是编译device tree源文件的工具链,根据官方的介绍,DTC工具链将一种文件格式作为输入转换成另一种文件格式。典型的输入文件为可读的dts文本文件,输出文件是二进制形式的dtb文件。当然DTC同样可以以二进制的dtb文件作为输入,输出dts文件。

这意味着,使用DTC可以使dts文件与dtb文件相互转换。

目前DTC支持输入格式为:dts,dtb,fs;支持的输出格式有:dtb,dts,asm。

CommandLine

DTC的命令格式为:

       dtc [options] [<input_filename>]

编译dts,生成dtb

      dtc -I dts -O dtb -o output.dtb  file-a.dts file-b.dts

反编译dtb,生成dts

      dtc -I dtb -O dts -o output.dts  file-z.dtb

DeviceTree Bolb and dt.img


有关dtb以及dt.img的知识,我将会简单介绍一下dtb、dtimg文件的结构,这部分属于进阶的内容,将放到其他章节讲解。

END


有关DeviceTree的基础知识就介绍到这里,后续会介绍DeviceTree的API以及常用设备的dts开发方法。

参考资料