Linux 驱动之设备树

article/2025/8/11 11:01:23

Linux 驱动之设备树

参考视频地址

【北京迅为】嵌入式学习之Linux驱动(第七期_设备树_全新升级)_基于RK3568_哔哩哔哩_bilibili

本章总领

在这里插入图片描述


1.设备树基本知识

什么是设备树?

​ Linux之父Linus Torvalds在2011年3月17日的ARM Linux邮件列表中说道:This whole ARM thing is a f*cking pain in the ass。之后ARMLinux社区引入了设备树。为什么Linus Torvalds会爆粗口呢?

​ 在讲平台总线模型的时候,平台总线模型是把驱动分成了两个部分,一部分是device,一部分是driver,设备信息和驱动分离这个设计非常的好。device部分是描述硬件的。一般device部分的代码会放在内核源码中arch/arm/plat-xxx和arch/arm/mach-xxx下面。但是随着Linux支持的硬件越来越多,在内核源码下关于硬件描述的代码也越来越多。并且每修改一下就要编译一次内核。

​ 长此以往Linux内核里面就存在了大量"垃圾代码",而且非常多,这里说的"垃圾代码"是关于对硬件描述的代码。从长远看,这些代码对Linux内核本身并没有帮助,所以相当于Linux内核是"垃圾代码"。但是并不是说平台总线这种方法不好。

为了解决这个问题,设备树就被引入到了Linux上。使用设备树来剔除相对内核来说的“垃圾代码”,既用设备树来描述硬件信息,用来替代原来的device部分的代码。虽然用设备树替换了原来的device部分,但是平台总线模型的匹配和使用基本不变。并且对硬件修改以后不必重新编译内核。直接需要将设备树文件编译成二进制文件,在通过bootloader传递给内核即可。所以设备树就是用来描述硬件资源的文件。

​ 设备树是描述硬件的文本文件,因为语法结构像树一样。所以叫设备树。

设备树的基本概念

基本名词解释

<1>DT:Device Tree //设备树
<2>FDT: Flattened Device Tree //开放设备树,起源于OpenFirmware (OF)
<3>dts: device tree source的缩写 //设备树源码
<4>dtsi: device tree source include的缩写 //通用的设备树源码
<5>dtb: device tree blob的缩写//编译设备树源码得到的文件
<6>dtc: device tree compiler的缩写 //设备树编译器

DTS, DTSI, DTB, DTC 之间的关系:

在这里插入图片描述

​ DTS和DTSI相当于源码文件,通过DTC这个编译器,编译生成DTB文件。

​ 以RK3588为例,设备树文件路径为:kernel/arch/arm64/boot/dts/rockchip

DTC编译器的使用

​ 以RK3588为例, DTC编译器源码路径:kernel/scripts/dtc; 如果正常编译完内核后,会在这个路径生成编译器dtc。

在这里插入图片描述

​ 如果你编译完内核代码后,进入到kernel/scripts/dtc路径,发现没有生成dtc编译器,那么检查kernel路径下.config配置文件,是否包含:CONFIG_DTC=y

如果没有,请将其加入到.config里面。(需要高版本的内核代码, 支持设备树)

在这里插入图片描述

编译设备树
dtc -I dts -O dtb -o xxx.dtb xxx.dts
反编译设备树
dtc -I dtb -O dts -o xxx.dts xxx.dtb
编译内核设备树

​ 进入到内核的顶层路径,执行make dtbs, 这种方法需要使能环境变量,暂时无法在我的rk3588内核上编译通过,会报错。

实验测试

​ 编写一个简单的设备树文件,代码路径:/home/topeet/Linux/my-test/40_dtc/my_device_tree.dts, 代码如下所示:

/dts-v1/;
/ {};

​ 这个设备树很简单,只包含了根节点/,而根节点中没有任何子节点或属性。这个示例并 没有描述任何具体的硬件设备或连接关系,它只是一个最基本的设备树框架,在本小节只是为 了测试设备树的编译和反编译。

dts-v1 明确声明该文件使用设备树语法版本1,这是设备树源文件的强制要求,必须放在文件第一行,不能省略,否则编译报错。

编译my_device_tree.dts

/home/topeet/Linux/rk3588-linux/kernel/scripts/dtc/dtc -I dts -O dtb -o my_device_tree.dtb my_device_tree.dts

编译完成后,生成my_device_tree.dtb:

root@ubuntu:/home/topeet/Linux/my-test/40_dtc# ls
my_device_tree.dtb

反编译my_device_tree.dtb:

/home/topeet/Linux/rk3588-linux/kernel/scripts/dtc/dtc -I dtb -O dts -o re_my_device_tree.dts my_device_tree.dtb

反编译完成后,生成re_my_device_tree.dts:

root@ubuntu:/home/topeet/Linux/my-test/40_dtc# ls
re_my_device_tree.dts
VSCode 安装设备树插件

在这里插入图片描述

​ 搜索插件DeviceTree 并安装。


2.设备树语法

根结点

​ 根结点是设备树必须包含的结点,根结点的名字 ”/“,如下所示:

/dts-v1/;     // 第一行表示dts文件的版本
/{            // 根结点};

子结点

格式

[label:] node-name[@unit-address] {[properties definitions][child nodes]
};

[properties definitions] : 表示结点属性

[child nodes]:表示该结点的子结点

举例

node1{//子节点,节点名称为node1node1_child{//子子节点,节点名称为node1_child};
};

​ 注意:同级节点下节点名称不能相同。不同级节点名称可以相同

范例代码

/dts-v1/;     // 第一行表示dts文件的版本
/{            // 根结点node1 {   // 子结点1node1-child {};};node2 {   // 子结点2node1-child {};};};

结点名称

​ 在对节点进行命名的时候,一般要体现设备的类型,比如网口一般命名成ethernet,串口一般命名成uart ,对于名称一般要遵循下面的命名格式。

​ 格式:[标签]:<名称>[@<设备地址>] 其中,[标签]和[@<设备地址>]是可选项,[名称]是必选项。另 外,这里的设备地址也没有实际意义,只是让节点名称更人性化 ,更方便阅读。

举例: uart: serial@02288000 其中, uart就是这个节点标签,也叫别名, serial@02288000 就是节点名称。

范例代码

/dts-v1/;     // 第一行表示dts文件的版本
/{            // 根结点node1 {   // 子结点1node1-child {};};node2 {   // 子结点2node1-child {};};led:gpio@02211000 {node1-child {};};};

reg属性

​ reg属性可以来描述地址信息。比如存储器的地址。

​ reg属性的格式如下: reg = <address1 length1 address2 length2 address3 length3……>

举例1: reg = <0x02200000 0x4000>;

举例2: reg = <0x02200000 0x4000 0x02205000 0x4000 >;

#address-cell和#size-cells属性

​ #address-cell和#size-cells用来描述子结点中的reg属性中的地址和长度信息。

举例1:

node1 {#address-cells = <1>;  // 子结点中reg 属性有一个地址#size-cells = <0>;     // 子结点中reg 属性没有长度node1-child {reg = <0>;};
};

举例2:

node1 {#address-cells = <1>;    // 子结点中reg 属性有一个地址#size-cells = <1>;       //  子结点中reg 属性有一个长度值node1-child {reg = <0x02200000 0x4000>;};
};

举例3:

node1 {#address-cells = <2>;    // 子结点中reg 属性有二个地址#size-cells = <0>;       //  子结点中reg 属性没有长度值node1-child {reg = <0x00 0x01>;};
};

model属性

​ model属性的值是一个字符串,一般用model描述一些信息.比如设备的名称,名字等。

举例1:

model = "wm8969-audio";

举例2:

model = "This is Linux board"

status属性

​ status属性和设备的状态有关系,status的属性是字符串,属性值有以下几个状态可选:

属性值描述
okay设备是可用状态
disabled设备是不可用状态
fail设备是不可用状态并且设备检测到了错误
fail-sss设备是不可用状态并且设备检测到了错误,sss是错误内容

compatible属性

​ compatible属性是非常重要的一个属性。compatible是用来和驱动进行匹配的。匹配成功以后会执行驱动中的probe函数。

举例:

compatible = "xunwei", "xunwei-board"
//在匹配的时候会先使用第一个值“xunwei”进行匹配,如果没有就会使用第二个值“xunwei-board”进行匹配。

device_type属性

​ 在某些设备树文件中,可以看到 device_type 属性,device_type 属性的值是字符串,只用于 cpu 节点或者 memory 节点进行描述。

举例1:

memory@30000000 {device_type = "memory";reg = <0x30000000 0x4000000>;
};

举例2:

cpu1: cpu@1 {device_type = "cpu";compatible = "arm,cortex-a35", "arm,armv8";reg = <0x0 0x1>;
};

自定义属性

​ 设备树中规定的属性有时候并不能满足我们的需求,这时候我们可以自定义属性。

举例:
自定义一个管脚标号的属性 pinnum

pinnum = <0 1 2 3 4>;

设备树特殊结点

aliases

​ 特殊节点 aliases 用来定义别名。定义别名的目的就是为了方便引用结点点。当然,除了使用 aliases 来命名别名,也可以在对结点命名的时候添加标签来命名别名。

举例:

aliases {mmc0 = &sdmmc0;mmc1 = &sdmmc1;mmc2 = &sdhci;serial0 = "/simple@fe000000/serial@llc500";
};

chosen

​ 特殊节点 chosen 用来由 U-Boot 给内核传递参数。重点是 bootargs 参数。chosen 节点必须是根节点的子节点。

chosen {bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
};

官方设备树文档路径

https://app.readthedocs.org/projects/devicetree-specification/downloads/pdf/latest/

综合示例:

/dts-v1/;     
/{            model = "This is Linux board";#address-cells = <1>;    	#size-cells = <1>; aliases{led1=&led;                                             //给led取别名led1led2=&ledB;                                           //给ledB取别名led2                            led3="/gpio@2211002";                     //给"gpio@2211002"取别名led3    };chosen {bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";};cpu1: cpu@1 {device_type = "cpu";compatible = "arm,cortex-a35", "arm,armv8";reg = <0x0 0x1>;};node1 {   #address-cells = <1>;    	#size-cells = <0>;  gpio@2211001{reg = <0x2211001>;};};node2 {   node1-child {pinnum = <0 1 2 3 4>;};};led:gpio@2211000 {compatible = "led";reg = <0x2211000 0x40>;status="okay";};ledB:gpio@2211001 {compatible = "led";reg = <0x2211001 0x40>;status="okay";};ledC:gpio@2211002 {compatible = "led";reg = <0x2211001 0x40>;status="okay";};};

实例分析–中断

RK处理器中断节点实例:
//RK原厂工程师编写
gpio0: gpio@fdd60000 {compatible = "rockchip,gpio-bank";reg = <0x0 0xfdd60000 0x0 0x100>;interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;clocks = <&pmucru PCLK_GPIO0>, <&pmucru DBCLK_GPIO0>;gpio-controller;#gpio-cells = <2>;gpio-ranges = <&pinctrl 0 0 32>;interrupt-controller;#interrupt-cells = <2>;
};

第2行代码:节点申明

  • gpio0: - 节点标签,允许其他节点通过 &gpio0 引用此节点
  • gpio@fdd60000 - 节点名称格式:设备类型@基地址
  • 表示这是一个 GPIO 控制器,位于物理地址 0xfdd60000

第3行代码指定设备驱动为 rockchip,gpio-bank

第4行代码:寄存器定义

  • 地址格式:<高32位 低32位 长度高32位 长度低32位>
  • 基地址:0x00000000fdd60000 (64位地址)
  • 地址范围长度:0x100 (256字节)
  • 表示该GPIO控制器占用256字节的物理地址空间

第5行代码:中断定义

  • GIC_SPI - 中断类型:共享外设中断(SPI)
  • 33 - 硬件中断号
  • IRQ_TYPE_LEVEL_HIGH - 触发类型:高电平触发
  • 表示该GPIO控制器本身会产生中断(如端口状态变化)

第6行代码:时钟依赖

  • 引用两个时钟源:
    • &pmucru PCLK_GPIO0 - GPIO0的外设时钟
    • &pmucru DBCLK_GPIO0 - GPIO0的调试时钟
  • pmucru是时钟控制器的节点标签

第8行代码:GPIO控制器申明

  • 表明此节点是一个GPIO控制器
  • 允许其他节点通过phandle引用其GPIO引脚

第9行代码:GPIO单元格式

  • 定义引用GPIO引脚时需要提供的参数数量
  • <2> 表示需要两个参数:
    • 参数1:GPIO引脚号
    • 参数2:GPIO标志(如激活状态)

第10行代码:GPIO范围映射

  • 映射到pinctrl控制器 &pinctrl
  • 0 - GPIO控制器的起始引脚号
  • 0 - pinctrl的起始引脚号
  • 32 - 映射的引脚数量
  • 表示此GPIO控制器的0-31引脚对应pinctrl的0-31引脚

第11行代码:中断控制器声明

  • 表明此节点也是一个中断控制器
  • 可以处理其GPIO引脚产生的中断

第12行代码:中断单元格式

  • 定义引用中断时需要提供的参数数量
  • <2> 表示需要两个参数:
    • 参数1:GPIO引脚号
    • 参数2:中断触发标志

此设备树节点功能总结:此节点定义了一个Rockchip平台的GPIO控制器,具有:

  1. 地址空间:0xfdd60000 - 0xfdd60100
  2. 支持32个GPIO引脚(0-31)
  3. 既是GPIO控制器又是中断控制器
  4. 依赖两个时钟源
  5. 映射到pinctrl子系统
  6. 使用双参数格式引用GPIO和中断

// 开发人员编写的设备树节点
ft5x06:ft5x06@38 {status = "disabled";compatible = "edt,edt-fts";reg = <0x38>;touch_gpio = <&gpio0 5 IRQ_TYPE_EDGE_RISING>;interrupt-parent = <&gpio0>;interrupts = <5 IRQ_TYPE_LEVEL_LOW>;reset-gpios = <&gpio0 6 GPIO_ACTIVE_LOW>;touchscreen-size-x = <800>;touchscreen-size-y = <1280>;touch_type = <1>;
};

接下来逐行分析下上面设备树节点:

ft5x06: ft5x06@38 {

节点声明

  • ft5x06: - 节点标签,允许其他部分通过 &ft5x06 引用此节点
  • ft5x06@38 - 节点名称格式:设备类型@I2C地址
  • 表示这是一个FT5x06系列触摸控制器,位于I2C总线地址0x38
    status = "disabled";

设备状态

  • "disabled" 表示此设备默认不启用
  • 可在系统启动时通过覆盖设备树或用户空间启用(改为"okay"
    compatible = "edt,edt-fts";

兼容性属性

  • 指定设备驱动为edt,edt-fts
  • 内核通过此字符串匹配触摸屏驱动程序
  • 注意:虽然节点名为ft5x06,但兼容性指定为edt-fts系列

    reg = <0x38>;

I2C地址

  • 指定设备在I2C总线上的7位地址为0x38
  • I2C驱动将使用此地址与设备通信

    touch_gpio = <&gpio0 5 IRQ_TYPE_EDGE_RISING>;

自定义触摸信号属性

  • 自定义属性(非标准)
  • 引用GPIO控制器gpio0的5号引脚
  • 配置为上升沿触发(IRQ_TYPE_EDGE_RISING

    interrupt-parent = <&gpio0>;

中断父控制器

  • 指定中断控制器为gpio0(之前定义的GPIO控制器)
  • 表示此设备的中断信号连接到GPIO0控制器

    interrupts = <5 IRQ_TYPE_LEVEL_LOW>;

中断定义

  • 使用双参数格式(匹配gpio0#interrupt-cells = <2>
  • 5 - GPIO引脚号(GPIO0的第5号引脚)
  • IRQ_TYPE_LEVEL_LOW - 中断触发类型:低电平触发

    reset-gpios = <&gpio0 6 GPIO_ACTIVE_LOW>;

复位GPIO定义

  • 标准GPIO引用属性
  • 引用GPIO控制器gpio0的6号引脚
  • GPIO_ACTIVE_LOW - 低电平有效(复位时拉低)
  • 驱动将使用此引脚控制设备复位

    touchscreen-size-x = <800>;touchscreen-size-y = <1280>;

触摸屏尺寸

  • 标准触摸屏属性
  • X方向分辨率:800像素
  • Y方向分辨率:1280像素
  • 驱动使用此信息校准坐标

    touch_type = <1>;

自定义触摸类型属性

  • 自定义属性(非标准)
  • <1>可能是设备特定配置(如协议版本)
  • 需要在驱动程序中解析此属性

总结:
  1. 在中断控制器中,必须有一个属性#interrupt-cells,表示其他节点如果使用这个中断控制器需要几个cell来表示使用哪一个中断。
  2. 在中断控制前中,必须有一个属性interrupt-controller,表示他是中断控制器。
  3. 在设备中使用中断,需要使用属性interrupt-parent=<&XXXX>,表示中断信号链接的是哪个中断控制器,接着使用interrupts属性来表示中断引脚和触发方式。

​ 注意:interrupt里有几个cell,是由interrupt-parent对应的中断控制器里面的#interrupt-cells属性决定。

其他写法:

级联中断控制器,gpio_intc 级联到gic

// 主中断控制器(SoC级)
gic: interrupt-controller@fee00000 {compatible = "arm,gic-v3";#interrupt-cells = <3>;interrupt-controller;
};// 二级中断控制器(外设级)
gpio_intc: interrupt-controller@fdd60000 {compatible = "arm,gic-v2m";#interrupt-cells = <2>;interrupt-controller;interrupt-parent = <&gic>;  // 级联到主GICinterrupts = <0 99 IRQ_TYPE_LEVEL_HIGH>;  // 使用GIC的99号中断
};

使用interrupt-extended 来表示多组中断控制器

// 主中断控制器
gic1: interrupt-controller@fee00000 {compatible = "arm,gic-v3";#interrupt-cells = <3>;interrupt-controller;
};// 级联中断控制器
gic2: interrupt-controller@f0800000 {compatible = "arm,gic-v2m";#interrupt-cells = <2>;interrupt-controller;interrupt-parent = <&gic1>;interrupts = <GIC_SPI 99 IRQ_TYPE_LEVEL_HIGH>; // 连接到GIC1的99号SPI中断
};// 中断设备
interrupt@38 {compatible = "edt,edt-ft5206";reg = <0x38>;interrupt-extended = <&gic1 0 9 IRQ_TYPE_EDGE_RISING>, // SPI中断9<&gic2 10 IRQ_TYPE_EDGE_FALLING>; // 级联中断10
};
实践—使用设备树描述中断

​ 本小节将会编写一个在 RK3588 上的ft5x06 触摸中断设备树。首先确定ft5x06的中断引脚号,底板原理图如下:

在这里插入图片描述

​ 由上图可知,触摸引脚网络标号为TP_INT_L,对应的SOC管脚为GPIO3_C0。

​ 然后来查看内核源码目录下的“drivers/input/touchscreen/edt-ft5x06.c”文件,这是 ft5x06 的驱动文件,找到compatible匹配值相关的部分,如下所示:

static const struct of_device_id edt_ft5x06_of_match[] = {{ .compatible = "edt,edt-ft5206", .data = &edt_ft5x06_data },{ .compatible = "edt,edt-ft5306", .data = &edt_ft5x06_data },{ .compatible = "edt,edt-ft5406", .data = &edt_ft5x06_data },{ .compatible = "edt,edt-ft5506", .data = &edt_ft5506_data },{ .compatible = "evervision,ev-ft5726", .data = &edt_ft5506_data },/* Note focaltech vendor prefix for compatibility with ft6236.c */{ .compatible = "focaltech,ft6236", .data = &edt_ft6236_data },{ /* sentinel */ }
};

​ 这里随便选择一个.compatible标签,我这里选择”edt,edt-ft5206“。

​ 在内核源码目录下的“include/dt-bindings/pinctrl/rockchip.h”头文件中,定义了 RK 引脚名 和gpio 编号的宏定义,如下图所示:

#define RK_PA0		0
#define RK_PA1		1
#define RK_PA2		2
#define RK_PA3		3
#define RK_PA4		4
#define RK_PA5		5
#define RK_PA6		6
#define RK_PA7		7
#define RK_PB0		8
#define RK_PB1		9
#define RK_PB2		10
#define RK_PB3		11
#define RK_PB4		12
#define RK_PB5		13
#define RK_PB6		14
#define RK_PB7		15
#define RK_PC0		16
#define RK_PC1		17
#define RK_PC2		18
#define RK_PC3		19
#define RK_PC4		20
#define RK_PC5		21
#define RK_PC6		22
#define RK_PC7		23
#define RK_PD0		24
#define RK_PD1		25
#define RK_PD2		26
#define RK_PD3		27
#define RK_PD4		28
#define RK_PD5		29
#define RK_PD6		30
#define RK_PD7		31

​ 可以看到RK已经将GPIO组和引脚编号写成了宏定义的形式, GPIO3_C0 对应的宏为:RK_PC0。有了以上信息后,我们就可以编写触摸屏中断的设备树,如下所示:

/dts-v1/;#include "dt-bindings/pinctrl/rockchip.h"#include "dt-bindings/interrupt-controller/irq.h"/{model = "This is my devicetree!";ft5x06@38 {compatible = "edt,edt-ft5206";interrupt-parent = <&gpio3>;interrupts = <RK_PC5 IRQ_TYPE_EDGE_RISING>;};};

​ 第1行代码: 设备树文件的头部,指定了使用的设备树语法版本。

​ 第3行代码:用于定义 Rockchip 平台的引脚控制器相关的绑定。

​ 第4行代码:用于定义中断控制器相关的绑定。

​ 第5行代码:表示设备树的根节点开始。

​ 第6行代码:指定了设备树的模型名称,描述为 “This is my device tree!”。

​ 第8行代码:指定了设备节点的兼容性字符串,表示该设备与 “edt,edt-ft5206” 兼容。

​ 第9行代码:指定了中断的父节点,即中断控制器所在的节点。这里使用了一个引用(&gpio3) 来表示父节点。

​ 第10行代码:指定了中断信号的配置。RK_PC0表示中断信号的引脚编号,IRQ_TYPE_EDGE_RISING 表示中断类型为上升沿触发。

实例分析–时钟

​ 绝大部分的外设工作都需要时钟,时钟一般以时钟树的形式呈现。在ARM平台中可以使用设备树来描述时钟树,如时钟的结构、时钟的属性等。再由驱动来解析设备树中时钟树的信息,从而完成时钟的初始化和使用。

​ 在设备树中,时钟分为生产者(providers)消费者(consumers)

生产者属性
**#clock-cells **

#clock-cells 属性代表时钟输出的路数:

  • #clock-cells 值为 0 时,代表仅有 1 路时钟输出
  • #clock-cells 值大于等于 1 时,代表输出 多路 时钟

举例1:单路时钟输出

osc24m: osc24m {compatible = "fixed-clock";clock-frequency = <24000000>;  // 24MHz时钟clock-output-names = "osc24m";  // 时钟输出名称#clock-cells = <0>;            // 表示只有1路时钟输出
};

举例2:多路时钟输出

clock: clock-controller {#clock-cells = <1>;             // 表示有多路时钟输出clock-output-names = "clock1", "clock2";  // 两路时钟名称
};

clock-output-names

​ clock-output-names 属性定义了输出时钟的名字。

举例1:单路时钟输出

osc24m: osc24m {compatible = "fixed-clock";clock-frequency = <24000000>;  // 24MHz时钟clock-output-names = "osc24m";  // 时钟输出名称#clock-cells = <0>;            // 表示只有1路时钟输出
};

举例2:多路时钟输出

clock: clock-controller {#clock-cells = <1>;             // 表示有多路时钟输出clock-output-names = "clock1", "clock2";  // 两路时钟名称
};
clock-frequency

​ clock-frequency 属性可以指定时钟的大小。

举例1:

osc24m: osc24m {compatible = "fixed-clock";clock-frequency = <24000000>;  // 24MHz时钟clock-output-names = "osc24m";  // 时钟输出名称#clock-cells = <0>;            // 表示只有1路时钟输出
};
assigned-clocks和assigned-clock-rates

​ assigned-clocks和assigned-clock-rates一般成对使用。当输出多路时钟时,为每路时钟进行编号。

举例:

cru: clock-controller@fdd20000 {#clock-cells = <1>;assigned-clocks = <&pmucru CLK_RTC_32K>, <&cru ACLK_RKDEV_PRE>;assigned-clock-rates = <32768>, <300000000>;
};
clock-indices

clock-indices 属性用于指定时钟输出的索引号(index)。如果不提供这个属性,那么 clock-output-names 和索引的对应关系默认是 0, 1, 2…(线性递增)。如果这种对应关系不是线性的,可以通过 clock-indices 属性来定义自定义的索引映射。

举例1:标准索引映射

scpi_dvfs: clocks@0 {#clock-cells = <1>;              // 需要1个参数标识时钟clock-indices = <0>, <1>, <2>;   // 显式定义索引号clock-output-names = "atlclk", "aplclk", "gpuclk";  // 三个时钟输出
};

举例2:非连续索引映射

scpi_clk: clocks@1 {#clock-cells = <1>;              // 需要1个参数标识时钟clock-indices = <3>;             // 定义索引号为3(非连续)clock-output-names = "pxlclk";   // 单个时钟输出
};
assigned-clock-parents

​ assigned-clock-parents 属性可以用来设置时钟的父时钟。

举例:

clock:clock {assigned-clock = <&clkcon 0>, <&pll 2>;assigned-clock-parents = <&pll 2>;assigned-clock-rates = <115200>, <9600>;
};

消费者属性
clock-name

​ clocks属性和clock-name属性用来指定使用的时钟源和消费者中时钟的名字。

举例:

clock:clock {clocks = <&cru CLK_VOP>;clock-names = "clk_vop",;
};

​ 注:cru是clock reset unit的缩写,pmu是power management unit的缩写。


消费者时钟节点实例分析

gpio1: gpio@fe740000 {compatible = "rockchip.gpio-bank";reg = <0x0 0xfe740000 0x0 0x100>;interrupts = <GIC_SPI 34 IRQ_TYPE_LEVEL_HIGH>;clocks = <&cru PCLK_GPIO1>, <&cru DBCLK_GPIO1>;gpio-controller;#gpio-cells = <2>;gpio-ranges = <&pinctrl 0 32 32>;interrupt-controller;#interrupt-cells = <2>;
};spi0: spi@fe610000 {compatible = "rockchip,rk3066-spi";reg = <0x0 0xfe610000 0x0 0x1000>;interrupts = <GIC_SPI 103 IRQ_TYPE_LEVEL_HIGH>;#address-cells = <1>;#size-cells = <0>;clocks = <&cru CLK_SPI0>, <&cru PCLK_SPI0>;clock-names = "spick", "app_pclk";dmas = <&dmac0 20>, <&dmac0 21>;dma-names = "tx", "rx";pinctrl-names = "default", "high_speed";pinctrl-0 = <&spi0m0_cs0 &spi0m0_cs1 &spi0m0_pins>;pinctrl-1 = <&spi0m0_cs0 &spi0m0_cs1 &spi0m0_pins_hs>;status = "disabled";
};

​ 第1行和第5行代码:gpio1中有clocks 属性,配置2个时钟,模块cru提供的时钟PCLK_GPIO1。模块cru提供的时钟DBCLK_GPIO1。

​ 第14行代码和第20行代码:spi0使用2个时钟源,分别是<&cru CLK_SPI0>和<&cru PCLK_SPI0>,并且给他们起了一个名字(第21行代码),分别为”spick“和”app_pclk“。


usb2phy0: usb2-phy@fe8a0000 {compatible = "rockchip,rk3568-usb2phy";reg = <0x0 0xfe8a0000 0x0 0x10000>;interrupts = <GIC_SPI 135 IRQ_TYPE_LEVEL_HIGH>;clocks = <&pmucru CLK_USBPHY0_REF>;clock-names = "phyclk";#clock-cells = <0>;assigned-clocks = <&cru USB480M>;assigned-clock-parents = <&usb2phy0>;clock-output-names = "usb480m_phy";rockchip,usbgrf = <&usb2phy0_grf>;status = "disabled";u2phy0_host: host-port {#phy-cells = <0>;status = "disabled";};u2phy0_otg: otg-port {#phy-cells = <0>;status = "disabled";};
};

​ 第1行,第8,第9行代码:usb2phy0时钟<&cru USB480M>挂载在时钟<&usb2phy0>下面, 并且输出的时钟名为:“usb480m_phy”(第10行代码)。

​ 第5行,第6行代码,usb2phy0也使用时钟<&pmucru CLK_USBPHY0_REF>,时钟名为”phyclk“。


实例分析–CPU

设备树中CPU节点介绍

  1. cpus 节点
    cpus 节点里面包含物理CPU的布局。也就是CPU的布局全部在此节点下描述。

  2. cpu-map 节点
    描述单核处理器不需要使用cpu-map节点,cpu-map节点主要用在描述大小核架构处理器中。cpu-map的节点名称必须是cpu-map,cpu-map节点的父节点必须是cpus节点。子节点必须是一个或者多个的cluster和socket节点。

  3. socket 节点
    socket 节点描述的是主板上的CPU插槽。主板上有几个CPU插槽,就有几个socket节点。socket节点的子节点必须是一个或者多个cluster节点。当有多个CPU插槽时,socket节点的命名方式必须是socketN,N=0,1,2…

  4. cluster节点

    cluster节点用来描述CPU的集群。比如RK3399的架构是双核A72+四核A53,双核A72是一个集群,用一个cluster节点来描述,四核A53也是一个集群,用一个cluster节点来描述。cluster节点的命名方式必须是clusterN,N=0,1,2…,cluster节点的子节点必须是一个或者多个的cluster节点或者一个或者多个的core节点。

  5. core节点

    core节点用来描述一个cpu,如果是单核cpu,则core节点就是cpus节点的子节点。core节点的命名方式必须是coreN,N=0,1,2…,core节点的子节点必须是一个或者多个thread节点。

  6. thread节点

    thread节点用来描述处理的线程。thread节点的命名方式必须是threadN,N=0,1,2…

举例1:单核CPU

cpus {#address-cells = <1>;#size-cells = <0>;cpu0: cpu@0 {compatible = "arm, cortex-a7";device_type = "cpu";};
};

cpus 节点

cpus {#address-cells = <1>;#size-cells = <0>;...
};
  • 作用:系统CPU的父容器节点
  • 属性
    • #address-cells = <1>:子节点地址字段使用1个32位单元
    • #size-cells = <0>:子节点大小字段不使用任何单元
  • 位置:必须是根节点(/)的直接子节点
cpu0: cpu@0 节点
cpu0: cpu@0 {compatible = "arm, cortex-a7";device_type = "cpu";
};
  • 节点名称cpu@0 表示第0个CPU
  • 标签cpu0(可通过&cpu0引用)
  • 关键属性
    • compatible = "arm, cortex-a7":指定CPU架构为ARM Cortex-A7
    • device_type = "cpu":声明设备类型为CPU(必需属性)

举例2:四核CPU

cpus {#address-cells = <0x1>;#size-cells = <0x0>;cpu0: cpu@0 {device_type = "cpu";compatible = "arm, cortex-a9";};cpu1: cpu@1 {device_type = "cpu";compatible = "arm, cortex-a9";};cpu2: cpu@2 {device_type = "cpu";compatible = "arm, cortex-a9";};cpu3: cpu@3 {device_type = "cpu";compatible = "arm, cortex-a9";};
};

举例3:四核A53+双核A72

cpus {#address-cells = <2>;#size-cells = <0>;cpu-map {cluster0 {core0 {cpu = <&cpu_10>;};core1 {cpu = <&cpu_11>;};core2 {cpu = <&cpu_12>;};core3 {cpu = <&cpu_13>;};};cluster1 {core0 {cpu = <&cpu_b0>;};core1 {cpu = <&cpu_b1>;};};};cpu_10: cpu@0 {device_type = "cpu";compatible = "arm.context-a53", "arm.armv8";};cpu_11: cpu@1 {device_type = "cpu";compatible = "arm.context-a53", "arm.armv8";};cpu_12: cpu@2 {device_type = "cpu";compatible = "arm.context-a53", "arm.armv8";};cpu_13: cpu@3 {device_type = "cpu";compatible = "arm.context-a53", "arm.armv8";};cpu_b0: cpu@100 {device_type = "cpu";compatible = "arm.context-a72", "arm.armv8";};cpu_b1: cpu@101 {device_type = "cpu";compatible = "arm.context-a72", "arm.armv8";};
};

举例4:描述一个16核CPU,一个物理插槽,每个插槽中有2个集群,每个CPU里面有两个线程。

cpus {#size-cells = <0>;#address-cells = <2>;cpu-map {socket0 {cluster0 {cluster0 {core0 {thread0 {cpu = <&&PU0>;};thread1 {cpu = <&&PU1>;};};core1 {thread0 {cpu = <&&PU2>;};thread1 {cpu = <&&PU3>;};};};cluster1 {core0 {thread0 {cpu = <&&PU4>;};thread1 {cpu = <&&PU5>;};};core1 {thread0 {cpu = <&&PU6>;};thread1 {cpu = <&&PU7>;};};};};cluster1 {cluster0 {core0 {thread0 {cpu = <&&PU8>;};thread1 {cpu = <&&PU9>;};};core1 {thread0 {cpu = <&&PU10>;};thread1 {cpu = <&&PU11>;};};};cluster1 {core0 {thread0 {cpu = <&&PU12>;};thread1 {cpu = <&&PU13>;};};core1 {thread0 {cpu = <&&PU14>;};thread1 {cpu = <&&PU15>;};};};};};};
};

实例分析–GPIO

gpio0: gpio@fdd60000 {compatible = "rockchip,gpio-bank";reg = <0x0 0xfdd60000 0x0 0x100>;interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;clocks = <&pmucru PCLK_GPIO0>, <&pmucru DBCLK_GPIO0>;gpio-controller;#gpio-cells = <2>;gpio-ranges = <&pinctrl 0 0 32>;interrupt-controller;#interrupt-cells = <2>;
};ft5x06: ft5x06@38 {status = "disabled";compatible = "edt,edt-ft5306";reg = <0x38>;touch-gpio = <&gpio0 RK_PB5 IRQ_TYPE_EDGE_RISING>;interrupt-parent = <&gpio0>;interrupts = <RK_PB5 IRQ_TYPE_LEVEL_LOW>;reset-gpios = <&gpio0 RK_PB6 GPIO_ACTIVE_LOW>;touchscreen-size-x = <800>;touchscreen-size-y = <1280>;touch_type = <1>;
};

代码第1–12行是RK原厂工程师编写的。代码14–25 是驱动开发工程师编写的。

第7行代码,gpio0是一个GPIO控制器,第8行,后面引用这个GPIO管脚的,需要2个参数描述这个GPIO(对应第21行代码)。


总结:

  1. 在GPIO控制器中,必须有一个属性#gpio-cells,表示其他节点如果使用这个GPIO控制器需要几个cell来表示使用哪一个GPIO。
  2. 在GPIO控制器中,必须有一个属性gpio-controller,表示他是GPIO控制器。
  3. 在设备树中使用GPIO,需要使用属性data-gpios=<&gpio1 12 0>来指定具体的GPIO引脚。data-gpios属性可以为自定义属性。

举例(简化):

    gpio1: gpio1 {gpio-controller;#gpio-cells = <2>;};[...]data-gpios = <&gpio1 12 0>, <&gpio1 15 0>;

其他属性

  1. ngpios = <18>
    • 作用:指定GPIO控制器管理的GPIO引脚总数
    • 值说明18 表示该GPIO控制器有18个可用引脚(编号0-17)
    • 必要性:必需属性,驱动程序需要此信息初始化GPIO芯片
  2. gpio-reserved-ranges = <0 4>, <12 2>
    • 作用:指定保留/不可用的GPIO范围
    • 格式<起始引脚 数量>
    • 值说明
      • <0 4>:保留0-3号引脚(共4个)
      • <12 2>:保留12-13号引脚(共2个)
    • 应用场景
      • 硬件设计上某些GPIO有特殊用途
      • 防止驱动误用关键系统引脚
  3. gpio-line-names
    • 作用:为每个GPIO引脚指定用户友好的名称
    • 格式:字符串列表,按引脚顺序排列
    • 值说明:18个名称对应18个GPIO引脚:
      • 0: “MMC-CD”(SD卡检测)
      • 1: “MMC-WP”(SD卡写保护)
      • 2: “VDD eth”(以太网电源)
      • …直到17: “reset”(复位引脚)

4.gpio-ranges

gpio-ranges 主要用于定义 GPIO 控制器管理的 GPIO 引脚与物理 SoC 引脚之间的映射关系。

为什么需要 gpio-ranges?
在复杂的 SoC 系统中:

  • 一个物理引脚可能被配置为 GPIO 或外设功能(如 UART、I2C)
  • GPIO 控制器看到的 GPIO 编号是"虚拟"的
  • 需要将 GPIO 控制器的虚拟编号映射到物理引脚的实际位置

gpio-ranges 是一个三元组或四元组列表:

gpio-ranges = <&pinctrl_phandle gpio_offset pin_offset count>;或者
gpio-ranges = <&pinctrl_phandle gpio_offset pin_offset count&pinctrl_phandle gpio_offset2 pin_offset2 count2>;

参数说明:

  • &pinctrl_phandle:指向引脚控制器节点的引用
  • pin_offset:在引脚控制器中的起始物理引脚号
  • gpio_offset:在 GPIO 控制器中的起始 GPIO 号
  • count:要映射的连续引脚数量

举例1:

gpio-controller@00000000 {compatible = "foo";reg = <0x00000000 0x1000>;gpio-controller;#gpio-cells = <2>;ngpios = <18>;gpio-reserved-ranges = <0 4>, <12 2>;gpio-line-names = "MMC-CD", "MMC-WP", "VDD eth", "RST eth", "LED R","LED G", "LED B", "Col A", "Col B", "Col C", "Col D","Row A", "Row B", "Row C", "Row D", "NMI button","poweroff", "reset";
};

​ 第6行,ngpios = <18>表示一共18个GPIO引脚。

​ 第7行,<0 4>表示保留引脚:0,1,2,3;<12 2> 表示保留GPIO引脚12,13

​ 第18行,表示18个GPIO对应的名字。

举例2:gpio-ranges 用法

/* 引脚控制器 */
pinctrl: pinctrl@1000000 {compatible = "vendor,pinctrl";reg = <0x1000000 0x1000>;
};/* GPIO 控制器 */
gpio0: gpio@2000000 {compatible = "vendor,gpio-controller";reg = <0x2000000 0x1000>;gpio-controller;#gpio-cells = <2>;/* 映射关系 */gpio-ranges = <&pinctrl 0 0 32>;  // 将0~31 pin 映射到GPIO 控制器0~31
};

引入pinmux概念

在这里插入图片描述

​ AE24这根GPIO管脚,有GPIO得功能GPIO0_A6_d,PCIE30X2_CLKREQn_M0, SATA_CP_POD,GPU_PWREN复用功能。

​ AE24表示芯片上的物理坐标:

在这里插入图片描述

在这里插入图片描述

pinmux工作方式

在这里插入图片描述

pinctrl简介

​ Linux内核提供了pinctrl子系统,pinctrl是pin controller的缩写,目的是为了统一各芯片原厂的pin脚管理。所以一般pinctrl子系统的驱动是由芯片原厂的BSP工程师实现。有了pinctrl子系统以后,驱动工程师就可以通过配置设备树使用pinctrl子系统去设置管脚的复用以及管脚的电气属性。

pinctrl语法

​ pinctrl的语法我们可以看作是由两个部分组成,以部分是客户端,一部分是服务器段。

举例1:

// client端:
&i2c2 {pinctrl-names = "default";pinctrl-0 = <&pinctrl_i2c2>;
};// service端
&iomuxc {pinctrl_i2c2: i2c2grp {fsl,pins = <MX6UL_PAD_UART5_TX_DATA__I2C2_SCL 0x4001b8b0MX6UL_PAD_UART5_RX_DATA__I2C2_SDA 0x4001b8b0>;};
};
  1. client端 (I2C2设备)
    • pinctrl-names = "default":定义引脚控制状态名称
    • pinctrl-0 = <&pinctrl_i2c2>:引用具体的引脚配置组
  2. service端 (iomuxc引脚控制器)
    • pinctrl_i2c2: i2c2grp:定义引脚配置组(标签为pinctrl_i2c2
    • fsl,pins:指定具体的引脚配置(NXP i.MX平台特有属性)
      • MX6UL_PAD_UART5_TX_DATA__I2C2_SCL:将UART5_TX引脚复用为I2C2_SCL功能
      • MX6UL_PAD_UART5_RX_DATA__I2C2_SDA:将UART5_RX引脚复用为I2C2_SDA功能
      • 0x4001b8b0:引脚电气属性配置值(包括上下拉、驱动强度等)

此配置实现了I2C2控制器的引脚复用:将原本用于UART5的引脚重新配置为I2C2功能,并设置电气特性。

举例2:

pinctrl-names = "default";
pinctrl-0 = <&pinctrl_hog_1>;

解析:
使用pinctrl-names表示设备的状态。这里只有一个default状态,default为第0个状态。pinctrl-0 = <&pinctrl_hog_1>表示第0个状态default对应的引脚在pinctrl_hog_1节点中配置。

举例3:

pinctrl-names = "default", "wake_up";
pinctrl-0 = <&pinctrl_hog_1>;
pinctrl-1 = <&pinctrl_hog_2>;

解析:
使用pinctrl-names表示设备的状态。这里有defaultwake_up两个状态,default为第0个状态,wake_up为第1个状态。pinctrl-0 = <&pinctrl_hog_1>表示第0个状态default对应的引脚在pinctrl_hog_1节点中配置。pinctrl-1同理。

举例4:

pinctrl-names = "default";
pinctrl-0 = <&pinctrl_hog_1 &pinctrl_hog_2>;

解析:
使用pinctrl-names表示设备的状态。这里只有一个default状态,default为第0个状态。pinctrl-0 = <&pinctrl_hog_1 &pinctrl_hog_2>表示第0个状态default对应的引脚在pinctrl_hog_1pinctrl_hog_2两个节点中配置。

瑞芯微pinctrl示例:

// RK3399 示例
led {pinctrl-names = "default";pinctrl-0 = <&led1_cli>;
};led1_cli: led1-cli {rockchip,pins = <0 12 RK_FUNC_GPIO &pcfg_pull_up>;
};// RK3568 示例
&uart7 {status = "okay";pinctrl-names = "default";pinctrl-0 = <&uart7m1_xfer>;
};uart7m1_xfer: uart7m1-xfer {rockchip,pins =/* uart7_rxm7l */<3 RK_PC5 4 &pcfg_pull_up>,/* uart7_txm7l */<3 RK_PC4 4 &pcfg_pull_up>;
};// 功能宏定义
#define RK_FUNC_GPIO    0
#define RK_FUNC_1       1
#define RK_FUNC_2       2
#define RK_FUNC_3       3
#define RK_FUNC_4       4
#define RK_FUNC_5       5
#define RK_FUNC_6       6
#define RK_FUNC_7       7
#define RK_FUNC_8       8
#define RK_FUNC_9       9
#define RK_FUNC_10      10
#define RK_FUNC_11      11
#define RK_FUNC_12      12
#define RK_FUNC_13      13
#define RK_FUNC_14      14
#define RK_FUNC_15      15

代码2~5行,RK3399 pinctrl客户端的代码。

第8行代码,<0 12 RK_FUNC_GPIO &pcfg_pull_up>, 第1个参数0,表示GPIO0组,第2个参数表示GPIO0_12, 第三个参数RK_FUNC_GPIO表示这个管脚复用为GPIO功能,第4个参数表示电器特性,有以下几种可以选择:

  • &pcfg_pull_up:上拉电阻使能
  • &pcfg_pull_down:下拉电阻使能
  • &pcfg_pull_none:无上/下拉
  • &pcfg_output_high:输出高电平
  • &pcfg_output_low:输出低电平

第21行代码,<3 RK_PC5 4 &pcfg_pull_up>, 将GPIO3里面的C5 设置为功能 4(UART_RX),电器属性为上拉电阻使能。

第22行代码,<3 RK_PC4 4 &pcfg_pull_up>, 将GPIO3里面的C4 设置为功能 4(UART_TX),电器属性为上拉电阻使能。

功能4:根据RK3568手册,ALT4对应UART功能。


实践–pinctrl设置管脚复用关系

​ 本小节将通过上面学到的 pinctrl 相关知识,将外接 led 灯的控制引脚复用为 GPIO 模式。首先来对 rk3588 的设备树结构进行以下介绍,根据 sdk 源码目录下的 “device/rockchip/rk3588/BoardConfig-rk3588-evb7-lp4-v10.mk” 默认配置文件可以了解到编译的设备树为 rk3588-evb7-lp4-v10-linux.dts,整理好的设备树之间包含关系列表如下所示:

顶层设备树rk3588-evb7-lp4-v10-linux.dts
第二级设备树rk3588-evb7-lp4.dtsirk3588-linux.dtsitopeet_rk3588_config.dtsi
第三级设备树rk3588.dtsi
rk3588-evb.dtsi
rk3588-rk806-single.dtsi
topeet_screen_lcds.dts
topeet_camera_config.dtsi

​ 打开rk3588-evb7-lp4.dtsi,到根节点最后添加代码:

vbus5v0_typec: vbus5v0-typec {...;
};
//大概是在vbus5v0_typec设备树节点附近,添加如下代码
my_led:led {compatible = "topeet,led";gpios = <&gpio2 RK_PC4 GPIO_ACTIVE_HIGH>;pinctrl-names = "default";pinctrl-0 = <&rk_led_gpio>;
};

​ 第1行:节点名称为led,标签名为my_led。

​ 第2行:compatible属性指定了设备的兼容性标识,即设备与驱动程序之间的匹配规则。 在这里,设备标识为"topeet,led",表示该 LED 设备与名为 “topeet,led” 的驱动程序兼容。

​ 第3行:gpios属性指定了与LED相关的GPIO(通用输入/输出)引脚配置。

​ 第4行:pinctrl-names 属性指定了与引脚控制相关的命名。default表示状态 0 。

​ 第5行:pinctrl-0属性指定了与pinctrl-names属性中命名的引脚控制相关联的实际引脚控 制器配置。<&rk_led_gpio>表示引用了名为rk_led_gpio的引脚控制器配置。

​ 然后继续找到在同一设备树文件的pinctrl服务端节点在该节点添加led控制引脚pinctrl服 务端节点,仿写完成的节点内容如下所示:

&pinctrl {rk_led {rk_led_gpio:rk-led-gpio {rockchip,pins = <2 RK_PC4 RK_FUNC_GPIO &pcfg_pull_none>;};};...;
}

​ 接下来编译内核,如果没有报错,则说明我们添加的led设备树节点没有问题。


无设备树参考节点?

没有参考节点概率不大,如果真没有, 参考文档:kernel/Documentation/devicetree/bindings


3.分析DTB格式

DTB文件格式

/dts-v1/;/ {model = "This is my devicetree!";#address-cells = <1>;#size-cells = <1>;chosen {bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0, 115200";};cpu1: cpu@1 {device_type = "cpu";compatible = "arm,cortex-a35", "arm,armv8";reg = <0x0 0x1>;};aliases {led1 = "/gpio@22020101";};node1 {#address-cells = <1>;#size-cells = <1>;gpio@22020102 {reg = <0x20220102 0x40>;};};node2 {node1-child {pinnum = <01234>;};};gpio@22020101 {compatible = "led";reg = <0x20220101 0x40>;status = "okay";};
};

​ 上述设备树源码编译以后得到dtb文件,使用二进制查看软件打开得到内容。(二进制软件为Binary Viewer, 下载地址:Binary Viewer - Download)

​ small header(头部),memory reservation block(内存预留块),structure block(结构块),strings block(字符串块)。free space(自由空间)不一定存在。

在这里插入图片描述

1.header
struct fdt_header {uint32_t magic;uint32_t totalsize;uint32_t off_dt_struct;uint32_t off_dt_strings;uint32_t off_mem_rsvmap;uint32_t version;uint32_t last_comp_version;uint32_t boot_cpuid_phys;uint32_t size_dt_strings;uint32_t size_dt_struct;
};注意,所有成员类型均为 u32。为大端模式。
成员介绍
字段十六进制数值代表含义
magicD00DFEED固定值
totalsize000002A4转换为十进制为676,表示文件大小为676字节
off_dt_struct00000038结构块从00000038地址开始,结合size_dt_struct确定结构块存储范围
off_dt_strings0000024C字符串块从0000024C地址开始,结合size_dt_strings确定字符串块存储范围
off_mem_rsvmap00000028内存保留块偏移地址为00000028,位于header之后、结构块之前
version0000001111(十六进制) = 17(十进制),表示当前设备树结构版本为17
last_comp_version0000001010 转换为十进制之后为16,表示向前兼容的设备树结构 版本为16
boot_cpuid_phys00000000表示设备树的teg属性为0
size_dt_strings00000058表示字符串块的大小为 00000058 ,和前面的 off_dt_strings 字符串块偏移值一起可以确定字符串块的 范围
size_dt_struct00000214表示结构块的大小为00000214,和前面的off_dt_struct 结构块偏移值一起可以确定结构块的范围
2.内存保留块

​ 如果在 dts 文件中使用 memreserve 描述保留的内存,保留内存的大小就会在这部分保存。
memreserve 的使用方法:

/memreserve/ <address> <length>;

​ 其中 <address><length> 是 64 位 C 风格整数,例如:

/* Reserve memory region 0x10000000..0x10003fff */
/memreserve/ 0x10000000 0x4000;

​ 在内存保留块的存储格式:

struct fdt_reserve_entry {uint64_t address;uint64_t size;
};
3.字符串块

字符串块用来存放属性的名字,比如 compatible、reg 等。通过分析 DTB 的头部,我们已经知道字符串块的位置,如 model 在 DTB 中的表示:

在这里插入图片描述

4.结构块

​ 结构块描述的是设备树的结构,也就是设备树的节点。那如何表示一个节点的开始和结束呢?使用 0x00000001 表示节点的开始,然后跟上节点名字(根节点的名字用 0 表示),然后使用 0x00000003 表示一个属性的开始(每表示一个属性,都要用 0x00000003 表示开始),使用 0x00000002 表示节点的结束,使用 0x00000009 表示根节点的结束(整个结构块的结束)
属性的名字和值用结构体表示:

struct {uint32_t len;uint32_t nameoff;
}
  • len 表示属性值的长度
  • nameoff 表示属性名字在字符串块中的偏移

​ 例子中以下节点在 DTB 中是如何表示的呢?

{model = "This is my devicetree!";#address-cells = <1>;#size-cells = <1>;chosen {bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";};

在这里插入图片描述


dtb展开成device_node

设备树是如何传递给内核的

在这里插入图片描述

  1. 编译阶段:DTC将.dts编译为.dtb二进制格式
  2. 加载阶段:U-Boot将内核和.dtb加载到内存
  3. 展开阶段:内核解析.dtb,构建设备树数据结构
  4. 使用阶段:驱动程序通过设备树API获取硬件信息

​ 由于内核并不认识dtb文件,需要将dtb文件展开为device_node结构体后,方可识别。

struct device_node此结构体定义在Linux内核头文件:include/linux/of.h,它是内核中表示设备树节点的核心数据结构, 结构体如下所示:

struct device_node {const char *name;       // 节点中的 name 属性const char *type;       // 节点中的 device_type 属性phandle phandle;const char *full_name;  // 节点的名字struct fwnode_handle fwnode;struct property *properties;  // 指向该设备节点下的第一个属性,其他属性与该属性链表相连struct property *deadprops;struct device_node *parent;   // 节点的父节点struct device_node *child;    // 节点的子节点struct device_node *sibling;  // 节点的同级节点,也可以叫兄弟节点// ... 其他成员
};

struct property此结构体定义在Linux内核头文件:include/linux/of.h,结构体如下所示:

struct property {char *name;       // 属性名字int length;       // 属性值的长度void *value;      // 属性值struct property *next;  // 指向该节点的下一个属性// ... 其他成员
};
  1. 每个字段的作用:
    • name:属性名称字符串(如"compatible", "reg"等)
    • length:属性值的字节长度
    • value:指向属性值的指针
    • next:指向同一节点的下一个属性,形成链表
  2. 设备树节点的所有属性通过next指针连接成单向链表:

在这里插入图片描述

DTB展开为device_node,链表逻辑结构图:

在这里插入图片描述

实例:dtb展开成device_node

​ 首先来到源码目录下的“/init/main.c”文件,找到其中的start_kernel函数,start_kernel函 数是 Linux 内核启动的入口点,它是Linux内核的核心函数之一,负责完成内核的初始化和启动过程,具体内容如下所示:

asmlinkage __visible void __init __no_sanitize_address start_kernel(void){char*command_line;char*after_dashes;set_task_stack_end_magic(&init_task); //设置任务栈的魔数smp_setup_processor_id(); //设置处理器IDdebug_objects_early_init(); //初始化调试对象cgroup_init_early(); //初始化cgroup(控制组)local_irq_disable(); //禁用本地中断early_boot_irqs_disabled=true; //标记早期引导期间中断已禁用/**中断仍然被禁用。进行必要的设置,然后启用它们。*/boot_cpu_init(); //初始化引导CPUpage_address_init(); //设置页地址pr_notice("%s",linux_banner); //打印Linux内核版本信息setup_arch(&command_line); //架构相关的初始化mm_init_cpumask(&init_mm); //初始化内存管理的cpumask(CPU掩码)setup_command_line(command_line); //设置命令行参数setup_nr_cpu_ids(); //设置CPU个数setup_per_cpu_areas(); //设置每个CPU的区域smp_prepare_boot_cpu(); //准备启动CPU(架构特定的启动CPU钩子)boot_cpu_hotplug_init(); //初始化热插拔的引导CPUbuild_all_zonelists(NULL); //构建所有内存区域列表page_alloc_init(); //初始化页面分配器........}

​ 代码第17行setup_arch(&command_line);该函数定义在内核源码的 /arch/arm64/kernel/setup.c文件中,具体内容如下所示:

void __init __no_sanitize_address setup_arch(char **cmdline_p)
{...;setup_machine_fdt(__fdt_pointer);  // 设置机器的FDT(平台设备树)...;if (acpi_disabled)unflatten_device_tree();    // 展开设备树
}

​ 在setup_arch函数中与设备树相关的函数分别为第4行的setup_machine_fdt(__fdt_pointer)和第8行的unflatten_device_tree(),接下来将对上述两个函数进行详细的介绍。

setup_machine_fdt(__fdt_pointer)

​ setup_machine_fdt(fdt_pointer)中的fdt_pointer是dtb二进制文件加载到内存的地址, 该地址由bootloader启动kernel时通过x0寄存器传递过来的,具体的汇编代码在内核源码目 录下的/arch/arm64/kernel/head.S文件中,具体内容如下所示:

preserve_boot_args:mov x21, x0 //x21=FDT__primary_switched:str_l x21, __fdt_pointer, x5 //Save FDT pointer

​ 第2行:将寄存器x0的值复制到寄存器x21。x0寄存器中保存了一个指针,该指针指向设 备树(Device Tree)。

​ 第4行:将寄存器x21的值存储到内存地址__fdt_pointer中。 然后来看setup_machine_fdt函数,该函数定义在内核源码的“/arch/arm64/kernel/setup.c” 文件中,具体内容如下所示:

static void __init setup_machine_fdt(phys_addr_t dt_phys)
{int size;//将设备树物理地址映射到内核虚拟地址空间void *dt_virt = fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL);const char *name;if (dt_virt)//保留设备树占用的内存区域memblock_reserve(dt_phys, size);if (!dt_virt || !early_init_dt_scan(dt_virt)) {pr_crit("\n""Error: invalid device tree blob at physical address %pa (virtual address 0x%p)\n""The dtb must be 8-byte aligned and must not exceed 2 MB in size\n""\nPlease check your bootloader.",&dt_phys, dt_virt);while (true)cpu_relax();}/* Early fixups are done, map the FDT as read-only now */fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL_RO);//获取设备树的机器名name = of_flat_dt_get_machine_name();if (!name)return;pr_info("Machine model: %s\n", name);dump_stack_set_arch_desc("%s (DT)", name);
}

​ 第5行代码:fixmap_remap_fdt()将设备树映射到内核虚拟地址空间中的fixmap区域。

​ 第10行代码:如果映射成功,则使用memblock_reserve()保留设备树占用的物理内存区域。

​ 第12行代码:调用函数early_init_dt_scan(dt_virt),该函数功能是检查设备树的有效性和完整性,如果设备 树无效或扫描失败,则会输出错误信息并进入死循环。,该函数定义在内核源 码的“drivers/of/fdt.c”目录下,具体内容如下所示:

bool __init early_init_dt_scan(void *params)
{bool status;//验证设备树的兼容性和完整性status = early_init_dt_verify(params);if (!status)return false;//扫描设备树节点early_init_dt_scan_nodes();return true;
}

​ 第5行代码:首先,调用early_init_dt_verify()函数对设备树进行兼容性和完整性验证。该函数可能会检 查设备树中的一致性标记、版本信息以及必需的节点和属性是否存在。如果验证失败,函数会 返回false。该函数的具体内容如下所示:

bool __init early_init_dt_verify(void *params)
{if (!params)return false;/* 检查设备树头部的有效性 */if (fdt_check_header(params))return false;/* 设置指向设备树的指针为传入的参数 */initial_boot_params = params;/* 计算设备树的CRC32校验值, 将结果保存到of_fdt_crc32中 */of_fdt_crc32 = crc32_be(~0, initial_boot_params,fdt_totalsize(initial_boot_params));return true;
}

​ 第7行代码,检测设备树DTB的header是否合法,检查设备树头部的有效性。fdt_check_header是一个用于检查设备树头部的函数, 如果设备树头部无效,则返回false,表示设备树不合法。

​ 第11行代码,保存设备树指针。

​ 第14行代码,计算设备树CRC32校验值。

​ 然后继续回到early_init_dt_scan()函数中,如果设备树验证成功(即status为真),则调 用early_init_dt_scan_nodes()函数。这个函数的作用是扫描设备树的节点并进行相应的处理, 该函数的具体内容如下所示:

void __init early_init_dt_scan_nodes(void)
{int rc = 0;/*从/chosen节点中检索各种信息 */rc = of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);if (!rc)pr_warn("No chosen node found, continuing without\n");/* 初始化{size,address}-cells信息 */of_scan_flat_dt(early_init_dt_scan_root, NULL);/* 设置内存信息,调用early_init_dt_add_memory_arch函数 */of_scan_flat_dt(early_init_dt_scan_memory, NULL);
}

​ 函数early_init_dt_scan_nodes 被声明为__init,这表示它是在内核初始化阶段被调用,并 且在初始化完成后不再需要。该函数的目的是在早期阶段扫描设备树节点,并执行一些初始化 操作。

​ 函数中主要调用了of_scan_flat_dt函数,该函数用于扫描平面设备树(flatdevicetree)。 平面设备树是一种将设备树以紧凑形式表示的数据结构,它不使用树状结构,而是使用线性结构,以节省内存空间。 具体来看,early_init_dt_scan_nodes 函数的执行步骤如下:

​ (1)of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line):从设备树的/chosen 节点中检索各种信息。/chosen节点通常包含了一些系统的全局配置参数,比如命令行参数。 early_init_dt_scan_chosen 是一个回调函数,用于处理/chosen 节点的信息。boot_command_line 是一个参数,表示内核启动时的命令行参数。

​ (2)of_scan_flat_dt(early_init_dt_scan_root, NULL):初始化{size,address}-cells 信息。 {size,address}-cells 描述了设备节点中地址和大小的编码方式。early_init_dt_scan_root 是一个回 调函数,用于处理设备树的根节点。

​ (3)of_scan_flat_dt(early_init_dt_scan_memory, NULL) : 设 置 内 存 信 息 , 并 调 用 early_init_dt_add_memory_arch 函数。这个步骤主要用于在设备树中获取内存的相关信息,并 将其传递给内核的内存管理模块。early_init_dt_scan_memory是一个回调函数,用于处理内存 信息。

unflatten_device_tree()

​ 该函数用于解析设备树,将紧凑的设备树数据结构转换为树状结构的设备树,该函数定义 在内核源码目录下的“/drivers/of/fdt.c”文件中,具体内容如下所示:

void __init unflatten_device_tree(void)
{/* 解析设备树 */__unflatten_device_tree(initial_boot_params, NULL, &of_root,early_init_dt_alloc_memory_arch, false);/* 获取指向 "/chosen" 和 "/aliases" 节点的指针,以供全局使用 */of_alias_scan(early_init_dt_alloc_memory_arch);/* 运行设备树的单元测试 */unittest_unflatten_overlay_base();
}

​ 该函数主要用于解析设备树,并将解析后的设备树存储在全局变量of_root中。 函数首先调用__unflatten_device_tree函数来执行设备树的解析操作。解析后的设备树将 使用of_root指针进行存储。 接下来,函数调用of_alias_scan函数。这个函数用于扫描设备树中的/chosen和/aliases节 点,并为它们分配内存。这样,其他部分的代码可以通过全局变量访问这些节点。 最后,函数调用unittest_unflatten_overlay_base函数,用于运行设备树的单元测试。

​ 然后对__unflatten_device_tree这一设备树的解析函数进行详细的介绍,该函数的具体内容 如下所示:

void *__unflatten_device_tree(const void *blob,struct device_node *dad,struct device_node **mynodes,void *(*dt_alloc)(u64 size, u64 align),bool detached)
{int size;void *mem;pr_debug(" -> unflatten_device_tree()\n");if (!blob) {pr_debug("No device tree pointer\n");return NULL;}pr_debug("Unflattening device tree:\n");pr_debug("magic: %08x\n", fdt_magic(blob));pr_debug("size: %08x\n", fdt_totalsize(blob));pr_debug("version: %08x\n", fdt_version(blob));if (fdt_check_header(blob)) {pr_err("Invalid device tree blob header\n");return NULL;}/* 第一遍扫描,计算大小 */size = unflatten_dt_nodes(blob, NULL, dad, NULL);if (size < 0)return NULL;size = ALIGN(size, 4);pr_debug("  size is %d, allocating...\n", size);/* 为展开的设备树分配内存 */mem = dt_alloc(size + 4, __alignof__(struct device_node));if (!mem)return NULL;memset(mem, 0, size);*(__be32 *)(mem + size) = cpu_to_be32(0xdeadbeef);pr_debug("  unflattening %p...\n", mem);/* 第二遍扫描,实际展开设备树 */unflatten_dt_nodes(blob, mem, dad, mynodes);if (be32_to_cpup(mem + size) != 0xdeadbeef)pr_warn("End of tree marker overwritten: %08x\n",be32_to_cpup(mem + size));if (detached && mynodes) {of_node_set_flag(*mynodes, OF_DETACHED);pr_debug("unflattened tree is detached\n");}pr_debug(" <- unflatten_device_tree()\n");return mem;
}

​ 该函数的重点在两次设备树的扫描上,第一遍扫描的目的是计算展开设备树所需的内存大 小。

​ 第28行:unflatten_dt_nodes函数的作用是递归地遍历设备树数据块,并计算展开设备树 所需的内存大小。它接受四个参数:blob(设备树数据块指针)、start(当前节点的起始地址, 初始为NULL)、dad(父节点指针)和mynodes(用于存储节点指针数组的指针,初始为NULL)。 第一遍扫描完成后,unflatten_dt_nodes函数会返回展开设备树所需的内存大小,然后在对大 小进行对齐操作,并为展开的设备树分配内存。

​ 第二遍扫描的目的是实际展开设备树,并填充设备节点的名称、类型和属性等信息。

​ 第47行:再次调用了unflatten_dt_nodes函数进行第二遍扫描。通过这样的过程,第二遍扫描会将设备树数据块中的节点展开为真正的设备节点,并填充节点的名称、类型和属性等信 息。这样就完成了设备树的展开过程。 最后我们来对unflatten_dt_nodes函数内容进行一下深究,unflatten_dt_nodes函数具体定 义如下所示:

static int unflatten_dt_nodes(const void *blob,void *mem,struct device_node *dad,struct device_node **nodepp)
{struct device_node *root;   //根节点int offset = 0, depth = 0, initial_depth = 0;   //偏移量、深度和初始深度
#define FDT_MAX_DEPTH	64      //最大深度struct device_node *nps[FDT_MAX_DEPTH];   //设备节点数组void *base = mem;       //基地址,用于计算偏移量bool dryrun = !base;     //是否只是模拟运行,不实际处理if (nodepp)*nodepp = NULL;  //如果指针不为空,将其置为空指针/** 如果@dad有效,则表示正在展开设备子树。* 在第一层深度可能有多个节点。* 将@depth设置为1,以使fdt_next_node()正常工作。* 当发现负的@depth时,该函数会立即退出。* 否则,除第一个节点外的设备节点将无法成功展开。*/if (dad)depth = initial_depth = 1;root = dad;          //根节点为@dadnps[depth] = dad;   //将根节点放入设备节点数组for (offset = 0;offset >= 0 && depth >= initial_depth;offset = fdt_next_node(blob, offset, &depth)) {if (WARN_ON_ONCE(depth >= FDT_MAX_DEPTH))continue;// 如果未启用CONFIG_OF_KOBJ并且节点不可用,则跳过该节点if (!IS_ENABLED(CONFIG_OF_KOBJ) &&!of_fdt_device_is_available(blob, offset))continue;//填充节点信息,并将子节点添加到设备节点数组if (!populate_node(blob, offset, &mem, nps[depth],&nps[depth+1], dryrun))return mem - base;if (!dryrun && nodepp && !*nodepp)*nodepp = nps[depth+1];   //将子节点指针赋值给@nodeppif (!dryrun && !root)root = nps[depth+1];       //如果根节点为空,则将子节点设置为根节点}if (offset < 0 && offset != -FDT_ERR_NOTFOUND) {pr_err("Error %d processing FDT\n", offset);return -EINVAL;}//反转子节点列表。一些驱动程序假设节点顺序与.dts文件中的节点顺序一致if (!dryrun)reverse_nodes(root);return mem - base;  //返回处理的字节数
}

unflatten_dt_nodes函数的作用我们在上面已经讲解过了,这里重点介绍第31行的fdt_next_node()函数和第41行的populate_node函数。

fdt_next_node()函数用来遍历设备树的节点。从偏移量为0开始,只要偏移量大于等于0 且深度大于等于初始深度,就执行循环。循环中的每次迭代都会处理一个设备树节点。 在每次迭代中,首先检查深度是否超过了最大深度FDT_MAX_DEPTH,如果超过了,则跳 过该节点。

​ 如果未启用CONFIG_OF_KOBJ并且节点不可用(通过of_fdt_device_is_available()函数判 断),则跳过该节点。

​ 随后调用populate_node()函数填充节点信息,并将子节点添加到设备节点数 组nps中。populate_node()函数定义如下所示:

static bool populate_node(const void *blob,int offset,void **mem,struct device_node *dad,struct device_node **pnp,bool dryrun)
{struct device_node *np;   //设备节点指针const char *pathp;        //节点路径字符串指针unsigned int l, allocl;   //路径字符串长度和分配的内存大小pathp = fdt_get_name(blob, offset, &l);   //获取节点路径和长度if (!pathp) {*pnp = NULL;return false;}allocl = ++l;   //分配内存大小为路径长度加一,用于存储节点路径字符串np = unflatten_dt_alloc(mem, sizeof(struct device_node) + allocl,__alignof__(struct device_node));    //分配设备节点内存if (!dryrun) {char *fn;of_node_init(np);    //初始化设备节点np->full_name = fn = ((char *)np) + sizeof(*np);  //设置设备节点的完整路径名memcpy(fn, pathp, l);   //将节点路径字符串复制到设备节点的完整路径名中if (dad != NULL) {np->parent = dad;     //设置设备节点的父节点np->sibling = dad->child;   //设置设备节点的兄弟节点dad->child = np;      //将设备节点添加为父节点的子节点}}populate_properties(blob, offset, mem, np, pathp, dryrun);    //填充设备节点的属性信息if (!dryrun) {np->name = of_get_property(np, "name", NULL);    //获取设备节点的名称属性if (!np->name)np->name = "<NULL>";}*pnp = np;return true;
}

​ 在populate_node函数中首先会调用第18行的unflatten_dt_alloc函数分配设备节点内存。分配的内存大小为 sizeof(struct device_node) + allocl 字节,并使用 alignof(struct device_node) 对齐。然后调用populate_properties函数填充设备节点的属性信息。该函数会解 析设备节点的属性,并根据需要分配内存来存储属性值。 至此,关于dtb二进制文件的解析过程就讲解完成了,完整的源码分析流程图如下所示:

在这里插入图片描述

device_node转化为platform_device

​ 设备树替换了平台总线模型当中对硬件资源描述的 device 部分。所以设备树也是对硬件资源进行描述的文件。在平台总线模型中,device 部分是用 platform_device 结构体来描述硬件资源的。所以内核最终会将内核认识的 device_node 树转换为 platform_device。但是并不是所有的节点都会被转换成 platform_device,只有满足要求的才会转换成 platform_device,转换成 platform_device 的节点可以在 /sys/bus/platform/devices 下查看。

​ 节点要满足什么要求才会被转换成 platform_device 呢?

转会规则
  1. 根节点下包含 compatible 属性的子节点

    • 例如:

      / {mydevice {compatible = "vendor,device"; // ✅ 会被转换};
      };
      
  2. 节点中 compatible 属性包含特定标识的节点

    • 若节点的 compatible 属性包含以下值之一:

      • "simple-bus"
      • "simple-mfd"
      • "isa"
    • 则该节点下包含 compatible 属性的子节点会被转换

    • 例如:

      bus {compatible = "simple-bus"; // 标识符#address-cells = <1>;#size-cells = <1>;child@0 {compatible = "vendor,child"; // ✅ 会被转换};
      };
      
  3. 特殊排除规则

    • 如果节点的 compatible 属性包含 "arm,primecell"

    • 则该节点会被转换为 amba 设备(不是 platform_device)

    • 例如:

      uart0: serial@fe001000 {compatible = "arm,primecell", "arm,pl011"; // ❌ 转换为amba设备reg = <0xfe001000 0x1000>;
      };
      

​ 内核是如何将 device_node 转换为 platform_device 和上节课的转换规则是怎么来的。在内核启动的时候会执行 of_platform_default_populate_init 函数,这个函数是用 arch_initcall_sync 来修饰的。

arch_initcall_sync(of_platform_default_populate_init);

​ 所以系统启动的时候会调用 of_platform_default_populate_init 函数。

调用
参数
参数
参数
内部调用
参数
参数
参数
遍历节点
根据
设置
of_platform_default_populate_init
of_platform_default_populate
NULL
NULL
NULL
of_platform_populate
root (根节点)
of_default_bus_match_table
lookup.parent
对每个匹配节点调用
of_platform_bus_create
of_platform_device_create_pdata
of_device_alloc
设置platform_device资源
device_node属性
platform_device.resource
of_default_bus_match_table
{.compatible = 'simple-bus'}
{.compatible = 'simple-mfd'}
{.compatible = 'isa'}
{.compatible = 'arm,amba-bus'}
{} /* NULL terminated list */

关键函数说明:

  1. of_platform_default_populate_init

    • 内核初始化时调用的入口函数
    • 使用arch_initcall_sync修饰,在内核启动早期执行
  2. of_platform_default_populate

    • 参数全为NULL表示使用默认值
    • 实际调用of_platform_populate
  3. of_platform_populate

    • 核心转换函数
    • 参数:
      • root:设备树根节点
      • matches:总线匹配表(of_default_bus_match_table
      • parent:父设备(此处为NULL)
  4. of_default_bus_match_table

    static 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{} /* 空值终止列表 */
    };
    
    • 定义了哪些总线类型下的节点需要转换
  5. of_platform_device_create_pdata

    • 为匹配的节点创建platform_device
    • 调用of_device_alloc分配设备资源
  6. of_device_alloc

    • 从device_node提取资源信息
    • 设置platform_device的resource数组
    • 关键转换:
      • reg属性 → I/O内存资源
      • interrupts属性 → IRQ资源
      • dma属性 → DMA资源
资源转换示例:
// 设备树节点
serial@4000 {compatible = "ns16550a";reg = <0x4000 0x100>;interrupts = <10 1>;
};

转化如下:

// platform_device资源
static struct resource serial_resources[] = {[0] = {.start = 0x4000,    // 寄存器起始地址.end = 0x40FF,      // 结束地址 (0x4000 + 0x100 - 1).flags = IORESOURCE_MEM,},[1] = {.start = 10,        // 中断号.end = 10,.flags = IORESOURCE_IRQ | IRQ_TYPE_EDGE_RISING,}
};

设备树下platform_device和platform_driver匹配

​ 首先来对rk3588的设备树结构进行以下介绍,根据sdk源码目录下的“device/rockchip/r k3588/BoardConfig-rk3588-evb7-lp4-v10.mk”默认配置文件可以了解到编译的设备树为 rk3588 evb7-lp4-v10-linux.dts,整理好的设备树之间包含关系列表如下所示:

顶层设备树rk3588-evb7-lp4-v10-linux.dts
第二级设备树rk3588-evb7-lp4.dtsirk3588-linux.dtsitopeet_rk3588_config.dtsi
第三级设备树rk3588.dtsi
rk3588-evb.dtsi
rk3588-rk806-single.dtsi
topeet_screen_lcds.dts
topeet_camera_config.dtsi

​ rk3588-evb7-lp4-v10-linux.dts 是顶层设备树,为了便于理解我们之后在该设备树下进行节 点的添加(当然这里也可以修改其他设备树),进入该设备树文件之后如下所示:

/ {topeet {#address-cells = <1>;#size-cells = <1>;compatible = "simple-bus";myLed{compatible = "my_devicetree";reg = <0xFEC30004 0x00000004>;};};
};

​ 保存退出,重新编译内核文件。

​ 修改设备驱动文件,代码路径:/home/topeet/Linux/my-test/44_devicetree_probe/platform_drv.c, 代码如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <linux/ioport.h>
#include <linux/mod_devicetable.h>int my_platform_probe(struct platform_device *pdev)
{printk(KERN_INFO "my_platform_probe: Probing platform device.\n");return 0;
}int my_platform_remove(struct platform_device *pdev)
{printk("my_platform_driver: Removing platform device.\n");return 0;
}const struct platform_device_id mydriver_id_table = {.name = "my_platform_device",
};const struct of_device_id od_match_table_id[] = {{.compatible="my_devicetree"},{}
};static struct platform_driver my_platform_driver = {.probe = my_platform_probe,.remove = my_platform_remove,.driver = {.name = "my_platform_device",.owner = THIS_MODULE,.of_match_table = od_match_table_id,},.id_table = &mydriver_id_table,};static int __init my_platform_driver_init(void)
{int ret;ret = platform_driver_register(&my_platform_driver);if( ret ){printk(KERN_ERR "Failed to register platform driver.\n");return ret;}printk(KERN_INFO "my_platform_driver: Platform driver initialized.\n");return 0;
}static void __exit my_platform_driver_exit(void)
{platform_driver_unregister(&my_platform_driver);printk(KERN_INFO "my_platform_driver: Platform driver exited.\n");
}module_init(my_platform_driver_init);
module_exit(my_platform_driver_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("YAN");
MODULE_VERSION("v1.0");
查看设备树节点是否成功被加载到系统
 ls /sys/firmware/devicetree/base/topeet/
'#address-cells'  '#size-cells'   compatible   myLed   name

4.of操作函数

device_node结构体

​ Linux 内核使用device_node结构体描述一个和结点,这个结构体定义在文件include/linux/of.h中:

struct device_node {const char *name;phandle phandle;const char *full_name;struct fwnode_handle fwnode;struct	property *properties;struct	property *deadprops;	/* removed properties */struct	device_node *parent;struct	device_node *child;struct	device_node *sibling;
#if defined(CONFIG_OF_KOBJ)struct	kobject kobj;
#endifunsigned long _flags;void	*data;
#if defined(CONFIG_SPARC)unsigned int unique_id;struct of_irq_controller *irq_trans;
#endif
};

of_ 函数操作集

1.节点查找函数
of_find_node_by_name(from, name)
struct device_node *of_find_node_by_name(struct device_node *from, const char *name);
  • 作用:通过节点名称查找设备树节点
  • 参数:
    • from:起始节点(NULL 表示从根节点开始)
    • name:目标节点名称
  • 返回值:成功返回节点指针,失败返回 NULL
of_find_node_by_path(path)
struct device_node *of_find_node_by_path(const char *path);
  • 通过完整路径查找节点(如 /soc/usb@fe800000
of_find_compatible_node(from, type, compatible)
struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compatible);
  • 通过 compatible 属性查找节点

2.属性操作函数

Linux内核使用property结构体来描述一个属性,这个结构体定义在文件:include/linux/of.h

struct property {char *name;int length;void *value;struct property *next;
};
of_find_property(node, name, lenp)
struct property *of_find_property(const struct device_node *np, const char *name, int *lenp);
  • 获取节点属性值
  • lenp:返回属性长度
of_property_read_xxx()系列
int of_property_read_u32(const struct device_node *np, const char *propname, u32 *out_value);
int of_property_read_string(struct device_node *np, const char *propname, const char **out_string);
// 按索引的值index读取
int of_property_read_u32_index(const struct device_node *np,const char *propname,u32 index,u32 *out_value); 
  • 读取不同类型的属性值(u8/u16/u32/u64/string/array)

of_property_read_u32和of_property_read_u32_index的区别:

特性of_property_read_u32of_property_read_u32_index
读取目标属性中的第一个值属性中指定索引位置的值
参数差异不需要索引参数需要明确指定索引位置
适用场景单值属性多值数组属性中的特定元素
返回值处理只读取第一个值可读取任意位置的指定值
错误条件属性不存在或长度不足4字节索引越界或长度不足

使用场景区别

  • of_property_read_u32
    适用于单值属性:

    clock-frequency = <50000000>;  // 单个值
    
    u32 clk_freq;
    of_property_read_u32(np, "clock-frequency", &clk_freq);
    
  • of_property_read_u32_index
    适用于多值数组中的特定元素:

    reg = <0x40008000 0x1000>;  // 两个值的数组
    interrupts = <0 40 0x4>;    // 三个值的数组
    
    u32 irq_num;
    // 读取interrupts属性的第2个值(索引1)
    of_property_read_u32_index(np, "interrupts", 1, &irq_num);
    

of_property_count_elems_of_size

​ 该函数在设备树中的设备节点下查找指定名称的属性,并获取该属性中元素的数量。调用 该函数可以用于获取设备树属性中某个属性的元素数量,比如一个字符串列表的元素数量或一 个整数数组的元素数量等。

#include<linux/of.h>
int of_property_count_elems_of_size(const struct device_node *np, const char *propname, int elem_size);

函数参数:

​ np:设备节点。

​ propname:需要获取元素数量的属性名。

​ elem_size:单个元素的尺寸。

返回值:

​ 如果成功获取了指定属性中元素的数量,则返回该数量;如果未找到属性或属性中没有元 素,则返回0。

3.节点遍历函数
of_get_parent(node)
struct device_node *of_get_parent(const struct device_node *node);
  • 获取父节点
of_get_next_child(parent, prev)
struct device_node *of_get_next_child(const struct device_node *parent, struct device_node *prev);
  • 遍历子节点(prev = NULL 开始)
of_get_next_available_child()
  • 获取下一个可用的子节点

4.地址转化函数
of_translate_address(node, in_addr)
u64 of_translate_address(struct device_node *np, const __be32 *addr);
  • 将逻辑地址转换为物理地址
of_iomap(node, index)
void __iomem *of_iomap(struct device_node *np, int index);
  • 直接映射设备内存到虚拟地址空间

5.中断相关函数
of_irq_get(node, index)
int of_irq_get(struct device_node *np, int index);
  • 获取中断号
of_irq_to_resource_table()
  • 解析中断资源表
gpio_to_irq()
int gpio_to_irq(unsigned int gpio)

函数作用

​ 获取中断号。

函数参数

​ gpio: gpio编号

返回值

​ 成功返回对应中断号。

irq_of_parse_and_map()
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)

函数作用

​ 从设备树节点的 interrupts 属性中解析并映射到对应的硬件中断号。

函数参数

*dev: 目标设备节点

index:要获取的中断在属性中的索引位置

返回值

​ 成功:返回映射后的中断号

​ 失败:返回0

irqd_get_trigger_type()
u32 irqd_get_trigger_type(struct irq_data *d)

函数作用

​ 从 irq_data 结构中获取中断触发类型标志

函数参数

​ *d: 指向 irq_data 结构的指针

返回值

​ 成功 返回中断触发标志。

​ 失败 返回0

irq_get_irq_data()
struct irq_data *irq_get_irq_data(unsigned int irq)

函数作用

​ 通过中断号获取对应的 irq_data 结构体

函数参数

​ irq : 中断号

返回值

​ 成功: 返回指向irq_data的指针。

​ 失败:返回NULL。

示例代码

#include <linux/interrupt.h>// 1. 获取中断号
unsigned int irq = irq_of_parse_and_map(dev_node, 0);// 2. 获取irq_data结构
struct irq_data *irq_data = irq_get_irq_data(irq);
if (!irq_data) {pr_err("无法获取irq_data\n");return -ENODEV;
}// 3. 获取中断触发类型
u32 trigger_type = irqd_get_trigger_type(irq_data);
pr_info("中断触发类型: 0x%x\n", trigger_type);// 4. 根据类型处理中断
switch (trigger_type) {case IRQF_TRIGGER_RISING:pr_info("上升沿触发\n");break;case IRQF_TRIGGER_FALLING:pr_info("下降沿触发\n");break;case IRQF_TRIGGER_HIGH:pr_info("高电平触发\n");break;case IRQF_TRIGGER_LOW:pr_info("低电平触发\n");break;default:pr_warn("未知触发类型\n");
}

ranges属性

1. 基本格式
ranges = <child-bus-address parent-bus-address length>;

ranges;  // 空属性
2. 字段说明
字段说明长度决定属性
child-bus-address子地址空间的起始地址由当前节点的 #address-cells 决定
parent-bus-address父地址空间的起始地址由父节点的 #address-cells 决定
length映射区域的大小由父节点的 #size-cells 决定
3. 示例解析
ranges = <0x0 0x20 0x100>;
  • 含义
    • 子地址空间:0x00x0 + 0x1000x0-0x100
    • 父地址空间:0x200x20 + 0x1000x20-0x120
  • 映射关系:子空间的 0x0-0x100 映射到父空间的 0x20-0x120
4. 特殊值含义
属性值含义
ranges;1:1 映射(内存区域直接映射)
ranges = < >;无映射(地址空间不转换)
5. 关键属性依赖
soc {#address-cells = <1>;  // 父地址用1个32位数表示#size-cells = <1>;     // 长度用1个32位数表示serial@4000 {#address-cells = <1>;  // 子地址用1个32位数表示#size-cells = <1>;     // 子长度用1个32位数表示ranges = <0x0 0x4000 0x1000>; // 含义: 子地址0x0-0x1000 → 父地址0x4000-0x5000};
};
6. 典型应用场景
场景1:内存映射外设
// 父节点定义
soc {compatible = "simple-bus";#address-cells = <2>;#size-cells = <2>;ranges;  // 1:1映射
};// 子节点(直接映射)
uart0: uart@ff000000 {reg = <0x0 0xff000000 0x0 0x1000>;
};
场景2:地址转换(PCIe设备)
pcie_controller {#address-cells = <3>;#size-cells = <2>;// 子地址 → 父地址转换ranges = <0x02000000 0 0xe0000000  0xc 0x20000000 0 0x20000000>;// 含义:// 子空间: PCIe内存空间 (0x02000000)// 父空间: 0xc20000000-0xc3fffffff
};
场景3:多级转换
// 一级转换
soc {ranges = <0x0 0xf0000000 0x100000>;// 二级转换i2c@1000 {ranges = <0x0 0x1000 0x100>;// 实际映射: // 子地址0x0 → soc地址0x1000 → 最终物理地址0xf0001000};
};
7. 字节序与数据格式
  • 所有值均为 大端序 (Big-Endian)

  • 每个值占用32位(4字节)

  • 示例解析:

    <0x00000000 0x20000000 0x00001000>
    // 等同于
    <0x0 0x20 0x1000>  // 简写形式
    
8. 常见错误处理
/* 错误示例1:长度不匹配 */
soc {#address-cells = <2>;  // 需要2个地址值ranges = <0x0 0x4000>; // 缺少长度值 → 编译错误
};/* 错误示例2:未定义大小 */
serial@4000 {ranges = <0x0 0x4000 0x1000>;// 必须定义 #address-cells 和 #size-cells
};

参考资料 — 设备树bindings文档

​ 参考文档路径:kernel/Documentation/devicetree/bindings

bindings文档

​ 设备节点里面除了一些标准的属性(课程中讲解的属性都是标准属性),但是当我们在接触一个新的节点的时候,有的属性不是标准属性,是芯片原厂自定义的属性,我们很难去看懂他是什么意思。这时候我们就可以去源码中查询bindings文档。一般在bindings中可以找到说明。

​ bindings文档路径:内核源码下:Documentation/devicetree/bindings

​ 但是有的时候有些芯片在bindings中找不到文档,这时候可以去芯片原厂提供的资料中找下,如果也没有,可以咨询芯片供应商和FAE。


http://www.hkcw.cn/article/bySYaHWeBk.shtml

相关文章

Unity Mono与IL2CPP比较

Unity提供了两种主要的脚本后端(Scripting Backend)选项&#xff1a;Mono和IL2CPP。它们在性能、平台支持和功能特性上有显著差异。 Edit>Project Settings>Player>Other Settings Mono后端 特点&#xff1a; 基于开源的Mono项目(.NET运行时实现) 使用即时编译(JIT…

配置Ollama环境变量,实现远程访问

在安装 Ollama 时配置环境变量 OLLAMA_HOST0.0.0.0:11434的主要目的是允许 Ollama 服务被局域网或远程设备访问&#xff0c;而不仅仅是本地主机&#xff08;localhost&#xff09;。 以下是详细原因&#xff1a; 1. Ollama默认行为的限制 默认情况下&#xff0c;Ollama 的 API…

仓颉鸿蒙开发:制作底部标签栏

今天制作标签栏&#xff0c;标签栏里面的有4个区域&#xff1a;首页、社区、消息、我的&#xff0c;以及对应的图标。点击的区域显示为高亮&#xff0c;未点击的区域显示为灰色 简单的将视图上面区域做一下 一、制作顶部公共视图部分 internal import ohos.base.* internal …

AWS之数据分析

目录 数据分析产品对比 1. Amazon Athena 3. AWS Lake Formation 4. AWS Glue 5. Amazon OpenSearch Service 6. Amazon Kinesis Data Analytics 7. Amazon Redshift 8.Amazon Redshift Spectrum 搜索服务对比 核心功能与定位对比 适用场景 关键差异总结 注意事项 …

Linux进程间通信----简易进程池实现

进程池的模拟实现 1.进程池的原理&#xff1a; 是什么 进程池是一种多进程编程模式&#xff0c;核心思想是先创建好一定数量的子进程用作当作资源&#xff0c;这些进程可以帮助完成任务并且重复利用&#xff0c;避免频繁的进程的创建和销毁的开销。 下面我们举例子来帮助理…

【Oracle】安装单实例

个人主页&#xff1a;Guiat 归属专栏&#xff1a;Oracle 文章目录 1. 安装前的准备工作1.1 硬件和系统要求1.2 检查系统环境1.3 下载Oracle软件 2. 系统配置2.1 创建Oracle用户和组2.2 配置内核参数2.3 配置用户资源限制2.4 安装必要的软件包 3. 目录结构和环境变量3.1 创建Ora…

Pyecharts 库的概念与函数

基本概念 Pyecharts 是一个基于 ECharts 的 Python 数据可视化库&#xff0c;具有以下特点&#xff1a; 基于 ECharts&#xff1a;底层使用百度开源的 ECharts 图表库 多种图表类型&#xff1a;支持折线图、柱状图、饼图、散点图、地图等多种图表 交互式&#xff1a;生成的图…

【深入详解】C语言内存函数:memcpy、memmove的使用和模拟实现,memset、memcmp函数的使用

目录 一、memcpy、memmove使用和模拟实现 &#xff08;一&#xff09;memcpy的使用和模拟实现 1、代码演示&#xff1a; &#xff08;1&#xff09;memcpy拷贝整型 &#xff08;2&#xff09;memcpy拷贝浮点型 2、模拟实现 &#xff08;二&#xff09;memmove的使用和模…

设计模式——责任链设计模式(行为型)

摘要 责任链设计模式是一种行为型设计模式&#xff0c;旨在将请求的发送者与接收者解耦&#xff0c;通过多个处理器对象按链式结构依次处理请求&#xff0c;直到某个处理器处理为止。它包含抽象处理者、具体处理者和客户端等核心角色。该模式适用于多个对象可能处理请求的场景…

软件的兼容性如何思考与分析?

软件功能的兼容性是指软件在实现功能的时候&#xff0c;能够与其他软件、硬件、系统环境以及数据格式等相互协作、互不冲突&#xff0c;并且能够正确处理不同来源或不同版本的数据、接口和功能模块的能力。它确保软件在多种环境下能够正常运行&#xff0c;同时与其他系统和用户…

C++ —— STL容器——string类

1. 前言 本篇博客将会介绍 string 中的一些常用的函数&#xff0c;在使用 string 中的函数时&#xff0c;需要加上头文件 string。 2. string 中的常见成员函数 2.1 初始化函数 string 类中的常用的初始化函数有以下几种&#xff1a; 1. string() …

DFS每日刷题

目录 P1605 迷宫 P1451 求细胞数量 P1219 [USACO1.5] 八皇后 Checker Challenge P1605 迷宫 #include <iostream> using namespace std; int n, m, t; int a[20][20]; int startx, starty, endx, endy; bool vis[20][20]; int res; int dx[] {0, 1, 0, -1}; int dy[]…

USART 串口通信全解析:原理、结构与代码实战

文章目录 USARTUSART简介USART框图USART基本结构数据帧起始位侦测数据采样波特率发生器串口发送数据 主要代码串口接收数据与发送数据主要代码 USART USART简介 一、USART 的全称与基本定义 英文全称 USART&#xff1a;Universal Synchronous Asynchronous Receiver Transmi…

C# winform 教程(一)

一、安装方法 官网下载社区免费版&#xff0c;在线下载安装 VS2022官网下载地址 下载后双击启动&#xff0c;选择需要模块&#xff08;net桌面开发&#xff0c;通用window平台开发&#xff0c;或者其他自己想使用的模块&#xff0c;后期可以修改&#xff09;&#xff0c;选择…

ZLG ZCANPro,ECU刷新,bug分享

文章目录 摘要 📋问题的起因bug分享 ✨思考&反思 🤔摘要 📋 ZCANPro想必大家都不陌生,买ZLG的CAN卡,必须要用的上位机软件。在汽车行业中,有ECU软件升级的需求,通常都通过UDS协议实现程序的更新,满足UDS升级的上位机要么自己开发,要么用CANoe或者VFlash,最近…

Matlab作图之 subplot

1. subplot(m, n, p) 将当前图形划分为m*n的网格&#xff0c;在 p 指定的位置创建坐标轴 matlab 按照行号对子图的位置进行编号 第一个子图是第一行第一列&#xff0c;第二个子图是第二行第二列......... 如果指定 p 位置存在坐标轴&#xff0c; 此命令会将已存在的坐标轴设…

【STM32F1标准库】理论——外部中断

目录 一、中断介绍 二、外部引脚EXTI申请的中断 三、外部中断的适用场景 四、其他注意事项 一、中断介绍 STM32可以触发中断的外设有外部引脚(EXTI)、定时器、ADC、DMA、串口、I2C、SPI等 中断同一由NVIC管理 n表示一个外设可能同时占用多个中断通道 优先级的值越小优先…

SAP学习笔记 - 开发18 - 前端Fiori开发 应用描述符(manifest.json)的用途

上一章讲了 Component配置&#xff08;组件化&#xff09;。 本章继续讲Fiori的知识。 目录 1&#xff0c;应用描述符(Descriptor for Applications) 1&#xff09;&#xff0c; manifest.json 2&#xff09;&#xff0c;index.html 3&#xff09;&#xff0c;Component.…

定时任务:springboot集成xxl-job-core(一)

springboot:2.7.2 xxl-job-core: 2.3.0 一、集成xxl-job 1. 在gitee上下载xxl-job项目 git clone https://gitee.com/xuxueli0323/xxl-job.git 2. 执行以下目录下的sql /xxl-job-2.3.0/doc/db/tables_xxl_job.sql 3. 在xxl-job-admin的项目中配置数据库信息 ### xxl-job, data…

【STM32开发板】接口部分

一、USB接口 可以看到USBP和USBN与PA12,PA11引脚相接,根据协议&#xff0c;需要添加上拉电阻 二、ADC和DAC 根据原理图找到可以作为ADC和DAC的引脚 ADC和DAC属于模拟部分的&#xff0c;所以要接模拟地 三、指示灯电路 找几个通用的引脚&#xff0c;因为单片机的灌电流比拉电流…