1.设备树简介

传统上,操作系统内核会直接编译进所有支持的硬件设备的驱动程序。但是,随着硬件设备的不断增多和变化,这种方法变得不够灵活。设备树的引入解决了这个问题。设备树将硬件的描述信息以一种结构化的方式存储在单独的文件中,然后在引导过程中由操作系统内核加载和解析。

设备树文件使用一种称为”Device Tree Source”(DTS)的语言编写,它是一种人类可读的文本格式。该文件描述了硬件设备的层次结构、寄存器地址、中断线路、DMA通道和其他相关属性。这些信息对于内核来说非常重要,因为它们允许内核正确地初始化和配置硬件设备。

设备树文件经过DTC编译后会生成一种称为”Device Tree Blob”(DTB)的二进制格式。DTB文件在引导过程中由引导加载程序(Bootloader)提供给内核。内核会解析DTB文件,根据其中的描述信息初始化硬件设备,并加载相应的驱动程序。

设备树的作用在于用来描述一个具体的硬件平台的硬件资源,如果没有设备树,当我有一个新的硬件平台时,在移植操作系统时需要去修改源码去适配这个具体的硬件平台。有了设备树之后,bootloader就能直接从设备树中获取硬件信息,而不需要去修改源码,使得Linux内核的兼容性和可移植性大大增强。

  • DTS:Device Tree Source,.dts格式的文件,是一种ASII 文本格式的设备树描述,也是我们要编写的设备树源码,一般一个.dts文件对应一个硬件平台,位于Linux源码的/arch/***/boot/dts”目录下。

  • DTC:Device Tree Compiler,是指编译设备树源码的工具,一般情况下我们需要手动安装这个编译工具。

  • DTB :是设备树源码编译生成的文件,类似于我们C语言中“.C”文件编译生成“.bin”文件

Linux驱动之设备树的基础知识 - 山无言 - 博客园

参考链接:doc.embedfire.com/linux/imx6/base/zh/latest/linux_driver/driver_tree.html

2.设备树基本语法架构分析

2.1 dts基本框架

1.头文件:设备树是可以像C语言那样使用“#include”引用“.h”后缀的头文件,也可以引用设备树“.dtsi”后缀的头文件。因此这里k210.dts引用了k210.dtsi中的文件

2.设备树节点:每一个{ }都是一个节点,/ {…}表示“根节点”, 在根节点内部的“aliases {…}”、“chosen {…}”、“memory {…}”等字符,都是根节点的子节点。

3.设备树节点追加内容:向已经存在的子节点追加数据,这些已经存在的节点可能定义在.dts文件里,也可能定义在.dtsi文件里,这些节点比根节点下的子节点多了一个&

设备树由一个根节点和众多子节点组成,子节点也可以继续包含其他节点,也就是子节点的子节点。

2.1.1节点基本格式

设备树中的每个节点都按照以下约定命名:

node-name@unit-address{
属性1 = …
属性2 = …
属性3 = …
子节点…
}

节点格式中的 node-name 用于指定节点的名称。 它的长度为1至31个字符,只能由如下字符组成:

字符 描述
0-9 数字
a-z 小写字母
A-Z 大写字母
, 英文逗号
. 英文句号
_ 下划线
+ 加号
- 减号

注意:根节点没有节点名,它直接使用“/”指代这是一个根节点。

@unit-address :其中的符号“@”可以理解为是一个分割符,“unit-address”用于指定“单元地址”, 它的值要和节点“reg”属性的第一个地址一致。如果节点没有“reg”属性值,可以直接省略“@unit-address”, 不过要注意这时要求同级别的设备树下(相同级别的子节点)节点名唯一,从这个侧面也可以了解到, 同级别的子节点的节点名可以相同,但是要求“单元地址”不同,node-name@unit-address 的整体要求同级唯一。

2.1.2 节点标签

例如:

sysctl: sysctl@50440000 {
        compatible = "kendryte,k210-sysctl", "simple-mfd";
        reg = <0x50440000 0x1000>;
        #clock-cells = <1>;
};

sysctl就代表了sysctl@50440000这个节点的标签,用:来指明。通常节点标签是节点名的简写,所以它的作用是当其它位置需要引用时可以使用节点标签来向该节点中追加内容。

2.1.3 节点路径

通过指定从根节点到所需节点的完整路径,可以唯一地标识设备树中的节点,不同层次的设备树节点名字可以相同,同层次的设备树节点要唯一。 这有点类似于我们Windows上的文件,一个路径唯一标识一个文件或文件夹,不同目录下的文件文件名可以相同。

2.1.4 节点属性

  • compatible属性:属性值类型:字符串

    compatible属性值由一个或多个字符串组成,有多个字符串时使用“,”分隔开。设备树中的每一个代表了一个设备的节点都要有一个compatible属性。 compatible是系统用来决定绑定到设备的设备驱动的关键。 compatible属性是用来查找节点的方法之一,另外还可以通过节点名或节点路径查找指定节点。

    例如:

    compatible = "kendryte,k210";
  • model属性:属性值类型:字符串

    model属性用于指定设备的制造商和型号,推荐使用“制造商, 型号”的格式,当然也可以自定义。

    例如:

    model = "Kendryte K210 generic";
  • status属性

    状态属性用于指示设备的“操作状态”,通过status可以去禁止设备或者启用设备,可用的操作状态如下表。默认情况下不设置status属性设备是使能的。

    例如:

    /* External sound card */
    sound: sound {
    status = "disabled";
    };
  • reg属性

    reg属性描述设备资源在其父总线定义的地址空间内的地址。通常情况下用于表示一块内存的起始地址(偏移地址)和长度, 在特定情况下也有不同的含义。 ret属性的书写格式为reg = < cells cells cells cells cells cells…>,长度根据实际情况而定, 这些数据分为地址数据(地址字段),长度数据(大小字段)

    例如:

    reg = <0x80000000 0x400000>,
    <0x80400000 0x200000>,
    <0x80600000 0x200000>;

    这里描述了三段内存:起始地址为0x80000000,大小为0x400000;起始地址为:0x80400000 ,0x200000;起始地址为:0x80600000,大小为:0x200000。每一个cells都是32位的,如果想要描述一个64位的地址,需要先设置#address-cells 和 #size-cells属性的值。

  • #address-cells 和 #size-cells

    #address-cells,用于指定子节点reg属性“地址字段”所占的长度(单元格cells的个数)。

    #size-cells,用于指定子节点reg属性“大小字段”所占的长度(单元格cells的个数)。

    例如:

    soc {
    #address-cells = <1>;
    #size-cells = <1>;
    ocrams: sram@900000 {
    compatible = "fsl,lpm-sram";
    reg = <0x900000 0x4000>;
    };
    };

    例如当#address-cells=2,#address-cells=1,则reg内的数据含义为reg = <address address size address address size>,这样就可以来描述一个64位的地址了。

  • device_type

    cpus {
    #address-cells = <1>;
    #size-cells = <0>;

    cpu0: cpu@0 {
    compatible = "arm,cortex-a7";
    device_type = "cpu";
    reg = <0>;
    }
    }

    device_type属性也是一个很少用的属性,只用在CPU和内存的节点上。 如上例中所示,device_type用在了CPU节点。

  • ranges

2.1.5 特殊节点

  • aliases子节点:aliases子节点的作用就是为其他节点起一个别名,如下所示。
aliases {
can0 = &flexcan1;
can1 = &flexcan2;
ethernet0 = &fec1;
ethernet1 = &fec2;
gpio0 = &gpio1;
gpio1 = &gpio2;
gpio2 = &gpio3;
gpio3 = &gpio4;
gpio4 = &gpio5;
i2c0 = &i2c1;
i2c1 = &i2c2;
/*----------- 以下省略------------*/
}

以“can0 = &flexcan1;”为例。“flexcan1”是一个节点的名字, 设置别名后我们可以使用“can0”来指代flexcan1节点,与节点标签类似。 在设备树中更多的是为节点添加标签,没有使用节点别名,别名的作用是“快速找到设备树节点”。 在驱动中如果要查找一个节点,通常情况下我们可以使用“节点路径”一步步找到节点。 也可以使用别名“一步到位”找到节点。

  • chosen子节点:chosen子节点位于根节点下,如下所示
chosen {
stdout-path = &uart1;
};

chosen子节点不代表实际硬件,它主要用于给内核传递参数。 这里只设置了“stdout-path =&uart1;”一条属性,表示系统标准输出stdout使用串口uart1。 此外这个节点还用作uboot向linux内核传递配置参数的“通道”, 我们在Uboot中设置的参数就是通过这个节点传递到内核的, 这部分内容是uboot和内核自动完成的。

3.Linux内核中K210设备树分析

从Linux 5.7 开始,Linux内核开始支持国产 RISC-V 芯片 K210,在本地的linux5.10版本中的/arch/riscv/boot/dts文件夹中可以找到kendryte的K210设备树文件,我们以此为例子,来分析设备树的语法与语义

image-20230611222950267

3.1 k210.dts分析

其中k210.dts的文件如下:

#include "k210.dtsi"

/ {
//设置节点基本属性
model = "Kendryte K210 generic"; //指定为嘉楠的K210
compatible = "kendryte,k210"; //设置compatible属性
// 设置chosen特殊节点,
chosen {
bootargs = "earlycon console=ttySIF0";
stdout-path = "serial0";
};
};

&uarths0 {
status = "okay";
};
  • 第一行:引用头文件,k210.dts引用了k210.dtsi中的文件
  • 3~12行:k210的根节点,每一个设备树只有一个根节点。 如果打开k210.dtsi文件可以发现它也有一个根节点,虽然k210.dts引用了k210.dtsi文件, 但这并不代表设备树有两个根节点,因为不同文件的根节点最终会合并为一个。
    • bootargs = "earlycon console=ttySIF0"; 是 “chosen” 节点的属性之一。它用于指定系统引导过程中传递给内核的启动参数(boot arguments)。在这里,设置的启动参数是 “earlycon console=ttySIF0”,表示使用早期控制台(early console)并将其输出重定向到名为 “ttySIF0” 的串口设备。
    • stdout-path = "serial0"; 是 “chosen” 节点的另一个属性。它用于指定标准输出(stdout)的路径。在这里,标准输出被设置为名为 “serial0” 的设备。
  • 13~15行:设备树增加内容。向uarths0子节点添加了一个属性,status = "okay"; 在这里,状态被设置为 “okay”,表示uarths0设备节点处于可用状态,可以正常使用。

3.2 k210.dtsi分析

3.2.1 根节点

k210.dtsi的文件如下:

#include <dt-bindings/clock/k210-clk.h>
/ {
/*
* Although the K210 is a 64-bit CPU, the address bus is only 32-bits
* wide, and the upper half of all addresses is ignored.
*/
#address-cells = <1>;
#size-cells = <1>;
compatible = "kendryte,k210";

aliases {
serial0 = &uarths0;
};

/*
* The K210 has an sv39 MMU following the priviledge specification v1.9.
* Since this is a non-ratified draft specification, the kernel does not
* support it and the K210 support enabled only for the !MMU case.
* Be consistent with this by setting the CPUs MMU type to "none".
*/
cpus {
#address-cells = <1>;
#size-cells = <0>;
timebase-frequency = <7800000>;
cpu0: cpu@0 {
device_type = "cpu";
reg = <0>;
compatible = "kendryte,k210", "sifive,rocket0", "riscv";
riscv,isa = "rv64imafdc";
mmu-type = "none";
i-cache-size = <0x8000>;
i-cache-block-size = <64>;
d-cache-size = <0x8000>;
d-cache-block-size = <64>;
clocks = <&sysctl K210_CLK_CPU>;
clock-frequency = <390000000>;
cpu0_intc: interrupt-controller {
#interrupt-cells = <1>;
interrupt-controller;
compatible = "riscv,cpu-intc";
};
};
cpu1: cpu@1 {
device_type = "cpu";
reg = <1>;
compatible = "kendryte,k210", "sifive,rocket0", "riscv";
riscv,isa = "rv64imafdc";
mmu-type = "none";
i-cache-size = <0x8000>;
i-cache-block-size = <64>;
d-cache-size = <0x8000>;
d-cache-block-size = <64>;
clocks = <&sysctl K210_CLK_CPU>;
clock-frequency = <390000000>;
cpu1_intc: interrupt-controller {
#interrupt-cells = <1>;
interrupt-controller;
compatible = "riscv,cpu-intc";
};
};
};

sram: memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x400000>,
<0x80400000 0x200000>,
<0x80600000 0x200000>;
reg-names = "sram0", "sram1", "aisram";
};

clocks {
in0: oscillator {
compatible = "fixed-clock";
#clock-cells = <0>;
clock-frequency = <26000000>;
};
};

soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "kendryte,k210-soc", "simple-bus";
ranges;
interrupt-parent = <&plic0>;

sysctl: sysctl@50440000 {
compatible = "kendryte,k210-sysctl", "simple-mfd";
reg = <0x50440000 0x1000>;
#clock-cells = <1>;
};

clint0: clint@2000000 {
#interrupt-cells = <1>;
compatible = "riscv,clint0";
reg = <0x2000000 0xC000>;
interrupts-extended = <&cpu0_intc 3 &cpu0_intc 7
&cpu1_intc 3 &cpu1_intc 7>;
clocks = <&sysctl K210_CLK_ACLK>;
};

plic0: interrupt-controller@c000000 {
#interrupt-cells = <1>;
interrupt-controller;
compatible = "kendryte,k210-plic0", "riscv,plic0";
reg = <0xC000000 0x4000000>;
interrupts-extended = <&cpu0_intc 11>, <&cpu0_intc 0xffffffff>,
<&cpu1_intc 11>, <&cpu1_intc 0xffffffff>;
riscv,ndev = <65>;
riscv,max-priority = <7>;
};

uarths0: serial@38000000 {
compatible = "kendryte,k210-uarths", "sifive,uart0";
reg = <0x38000000 0x1000>;
interrupts = <33>;
clocks = <&sysctl K210_CLK_CPU>;
};
};
};

在第一行中包含了一个c语言中的头文件#include <dt-bindings/clock/k210-clk.h>,这个文件位于linux-5.10.99/include/dt-bindings/clock下,打开此文件如下:

#ifndef K210_CLK_H
#define K210_CLK_H

/*
* Arbitrary identifiers for clocks.
* The structure is: in0 -> pll0 -> aclk -> cpu
*
* Since we use the hardware defaults for now, set all these to the same clock.
*/
#define K210_CLK_PLL0 0
#define K210_CLK_PLL1 0
#define K210_CLK_ACLK 0
#define K210_CLK_CPU 0

#endif /* K210_CLK_H */

可以看到,只是定义了一些宏定义,后续可能用到

7~13

#address-cells = <1>;
#size-cells = <1>;
compatible = "kendryte,k210";

aliases {
serial0 = &uarths0;
};
  • #address-cells = <1>; 指定了设备树中地址单元的数量,这里设置为 1。
  • #size-cells = <1>; 指定了设备树中大小单元的数量,这里也设置为 1。
  • compatible = "kendryte,k210"; 表示设备树描述的硬件与 Kendryte K210 SoC 兼容。这个属性用于标识设备树所描述的硬件平台或设备的兼容性。
  • aliases:定义了一个别名 serial0,它指向名为 uarths0 的设备。

3.2.2 cpu节点

cpus {
#address-cells = <1>; // 地址单元为1
#size-cells = <0>; // 大小单元为0
timebase-frequency = <7800000>; //指定cpu时钟基准频率7800000hz
cpu0: cpu@0 {
device_type = "cpu";
reg = <0>;
compatible = "kendryte,k210", "sifive,rocket0", "riscv";
riscv,isa = "rv64imafdc";
mmu-type = "none";
i-cache-size = <0x8000>;
i-cache-block-size = <64>;
d-cache-size = <0x8000>;
d-cache-block-size = <64>;
clocks = <&sysctl K210_CLK_CPU>;
clock-frequency = <390000000>;
cpu0_intc: interrupt-controller {
#interrupt-cells = <1>;
interrupt-controller;
compatible = "riscv,cpu-intc";
};
};
cpu1: cpu@1 {
device_type = "cpu";
reg = <1>;
compatible = "kendryte,k210", "sifive,rocket0", "riscv";
riscv,isa = "rv64imafdc";
mmu-type = "none";
i-cache-size = <0x8000>;
i-cache-block-size = <64>;
d-cache-size = <0x8000>;
d-cache-block-size = <64>;
clocks = <&sysctl K210_CLK_CPU>;
clock-frequency = <390000000>;
cpu1_intc: interrupt-controller {
#interrupt-cells = <1>;
interrupt-controller;
compatible = "riscv,cpu-intc";
};
};
};

cpu单元定义了两个核心分别为cpu0和cpu1,两个cpu核心的配置差不都,下面依次描述一下具体描述了哪些信息,这里以cpu0为例子:

  • device_type = “cpu”:表示此节点为cpu。

  • reg = <0>:标明这是0号处理器。

  • compatible = “kendryte,k210”, “sifive,rocket0”, “riscv”:指定cpu的标识。

  • riscv,isa = “rv64imafdc”:表明该cpu为rv64imafdc架构。

  • mmu-type = “none”:cpu不启用mmu,未开启虚拟内存功能。

  • i-cache-size = <0x8000>:指令缓存的大小为 0x8000,即 32768 字节(或 32 KB)。

  • i-cache-block-size = <64>:指令缓存的块大小为 64 字节。

  • d-cache-size = <0x8000>:数据缓存(Data Cache)的大小为 0x8000,即 32768 字节(或 32 KB)。

  • d-cache-block-size = <64>:数据缓存的块大小为 64 字节。

  • clocks = <&sysctl K210_CLK_CPU>:指向设备树中名为 sysctl 的节点, sysctl 定义在下面的soc节点中,并使用 K210_CLK_CPU 作为其子节点,K210_CLK_CPU这个宏定义在k210-clk.h中,值为:0。表明cpu0的时钟是 sysctl 节点中的0号子时钟

  • clock-frequency = <390000000>:指定时钟的频率为390MHZ

  • cpu0_intc:中断控制器节点,用于处理与 CPU 0 相关的中断。

    • #interrupt-cells = <1>:指定了中断单元的数量,即中断号码的位数。在这种情况下,每个中断使用一个单元(一个整数值)来表示。
    • interrupt-controller:表示该节点是中断控制器。
    • compatible = "riscv,cpu-intc":指定了该中断控制器节点与 RISC-V 架构的 CPU 中断控制器兼容。

3.2.3 SRAM节点

sram: memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x400000>,
<0x80400000 0x200000>,
<0x80600000 0x200000>;
reg-names = "sram0", "sram1", "aisram";
};
  • memory@80000000:指定了 SRAM 的基地址为 0x80000000
  • device_type = "memory":指定了设备类型为内存。
  • reg:指定了 SRAM 的物理地址范围。在这种情况下,SRAM 被划分为三个连续的地址范围:
    • <0x80000000 0x400000>sram0 的地址范围为从 0x800000000x803FFFFF,大小为 4 MB。
    • <0x80400000 0x200000>sram1 的地址范围为从 0x804000000x805FFFFF,大小为 2 MB。
    • <0x80600000 0x200000>aisram 的地址范围为从 0x806000000x807FFFFF,大小为 2 MB。
  • reg-names = "sram0", "sram1", "aisram":指定了对应于每个地址范围的名称

3.2.4 clocks节点

clocks {
in0: oscillator {
compatible = "fixed-clock";
#clock-cells = <0>;
clock-frequency = <26000000>;
};
};

该设备树中的 clocks 节点定义了一个名为 in0 的时钟,具体如下:

  • in0:时钟的名称为 in0
  • oscillator:指定了该时钟源为一个振荡器。
  • compatible = "fixed-clock":指定了时钟的类型为固定频率时钟。
  • #clock-cells = <0>:表示该时钟节点不需要附加的时钟单元属性。
  • clock-frequency = <26000000>:指定了时钟的频率为 26 MHz。

3.2.5 soc节点

soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "kendryte,k210-soc", "simple-bus";
ranges; //省略ranges属性,不存在地址映射
interrupt-parent = <&plic0>;

sysctl: sysctl@50440000 {
compatible = "kendryte,k210-sysctl", "simple-mfd";
reg = <0x50440000 0x1000>;
#clock-cells = <1>;
};

clint0: clint@2000000 {
#interrupt-cells = <1>;
compatible = "riscv,clint0";
reg = <0x2000000 0xC000>;
interrupts-extended = <&cpu0_intc 3 &cpu0_intc 7
&cpu1_intc 3 &cpu1_intc 7>;
clocks = <&sysctl K210_CLK_ACLK>;
};

plic0: interrupt-controller@c000000 {
#interrupt-cells = <1>;
interrupt-controller;
compatible = "kendryte,k210-plic0", "riscv,plic0";
reg = <0xC000000 0x4000000>;
interrupts-extended = <&cpu0_intc 11>, <&cpu0_intc 0xffffffff>,
<&cpu1_intc 11>, <&cpu1_intc 0xffffffff>;
riscv,ndev = <65>;
riscv,max-priority = <7>;
};

uarths0: serial@38000000 {
compatible = "kendryte,k210-uarths", "sifive,uart0";
reg = <0x38000000 0x1000>;
interrupts = <33>;
clocks = <&sysctl K210_CLK_CPU>;
};
};
  • interrupt-parent = <&plic0>:指定了中断控制器的父节点,这里使用的是 plic0 中断控制器的引用。

  • sysctl: sysctl@50440000:这是一个子节点,描述了系统控制器 (sysctl)。它有以下属性:

    • compatible = "kendryte,k210-sysctl", "simple-mfd":指定了系统控制器的兼容性标识符,表示该节点描述的是 Kendryte K210 SoC 的系统控制器,并且它是一个多功能设备。
    • reg = <0x50440000 0x1000>:指定了系统控制器在内存中的地址范围。
    • #clock-cells = <1>:指定了该节点使用的时钟单元数。
  • clint0: clint@2000000:这是另一个子节点,描述了 CLINT (Core Local Interruptor)。它有以下属性:

    • compatible = "riscv,clint0":指定了 CLINT 的兼容性标识符。
    • reg = <0x2000000 0xC000>:指定了 CLINT 在内存中的地址范围。
    • interrupts-extended:指定了 CLINT 支持的中断引脚,这里使用的是 cpu0_intccpu1_intc 的引用。
    • clocks = <&sysctl K210_CLK_ACLK>:指定了 CLINT 使用的时钟源,这里使用的是 sysctl 节点中的 K210_CLK_ACLK 时钟。
  • plic0: interrupt-controller@c000000:这是另一个子节点,描述了 PLIC (Platform-Level Interrupt Controller)。它有以下属性:

    • compatible = "kendryte,k210-plic0", "riscv,plic0":指定了 PLIC 的兼容性标识符。
    • reg = <0xC000000 0x4000000>:指定了 PLIC 在内存中的地址范围。
    • interrupts-extended:指定了 PLIC 支持的中断引脚,这里使用的是 cpu0_intccpu1_intc 的引用。
    • riscv,ndev = <65>:指定了 PLIC 支持的设备数量。
    • riscv,max-priority = <7>:指定了 PLIC 支持的最大优先级
  • uarths0: serial@38000000:这是串口设备的节点定义,名称为 uarths0,描述了串口在内存中的地址范围。

    • compatible = "kendryte,k210-uarths", "sifive,uart0":指定了串口设备的兼容性标识符,表示该节点描述的是 Kendryte K210 SoC 的 uarths0 串口,并且它兼容 SiFive 的 UART0 设备。
    • reg = <0x38000000 0x1000>:指定了串口设备在内存中的地址范围。
    • interrupts = <33>:指定了串口设备的中断引脚,这里使用的是中断号 33。
    • clocks = <&sysctl K210_CLK_CPU>:指定了串口设备使用的时钟源,这里使用的是 sysctl 节点中的 K210_CLK_CPU 时钟。