当前位置: 首页 > news >正文

Linux 驱动之设备树

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。

相关文章:

  • c++第四章练习题
  • openpnp - 给M4x0.7mm的直油嘴加油的工具选择
  • day025-网络基础-DNS与ARP协议
  • 征程 6 J6EM 常见 qconfig 配置解读与示例
  • LangGraph(八)——LangGraph运行时
  • 博士论文写作笔记
  • 【大模型DA】Unified Language-driven Zero-shot Domain Adaptation
  • agent-zero: 打造你的AI专属AI助理
  • Canvas: trying to draw too large(256032000bytes) bitmap.
  • JavaScript 模块系统:CJS/AMD/UMD/ESM
  • QT/c++航空返修数据智能分析系统
  • Cocos 打包 APK 兼容环境表(Android API Level 10~15)
  • 【渲染】拆解《三国:谋定天下》场景渲染技术
  • 读《Go语言圣经记录》(二):深入理解Go语言的程序结构
  • 工作流引擎-06-流程引擎(Process Engine)对比 Flowable、Activiti 与 Camunda 全维度对比分析
  • 淘宝商品详情页有哪些常见的动态加载技术?
  • t018-高校宣讲会管理系统 【含源码!】
  • 大规模真实场景 WiFi 感知基准数据集
  • 子串题解——和为 K 的子数组【LeetCode】
  • C++11 智能指针:从原理到实现
  • 安徽 两学一做 网站/手游免费0加盟代理
  • 张家港网站建设哪家好/seo策略分析
  • 长沙水业网站是哪家公司做的/百度seo推广怎么做
  • 建设网站需要哪些人员/如何写好软文
  • 深圳市住建局网站官网/2023新闻摘抄十条
  • 佛山市城市建设工程有限公司/北京整站线上推广优化