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控制器,具有:
- 地址空间:0xfdd60000 - 0xfdd60100
- 支持32个GPIO引脚(0-31)
- 既是GPIO控制器又是中断控制器
- 依赖两个时钟源
- 映射到pinctrl子系统
- 使用双参数格式引用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>
可能是设备特定配置(如协议版本) - 需要在驱动程序中解析此属性
总结:
- 在中断控制器中,必须有一个属性#interrupt-cells,表示其他节点如果使用这个中断控制器需要几个cell来表示使用哪一个中断。
- 在中断控制前中,必须有一个属性interrupt-controller,表示他是中断控制器。
- 在设备中使用中断,需要使用属性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节点介绍
-
cpus 节点
cpus 节点里面包含物理CPU的布局。也就是CPU的布局全部在此节点下描述。 -
cpu-map 节点
描述单核处理器不需要使用cpu-map节点,cpu-map节点主要用在描述大小核架构处理器中。cpu-map的节点名称必须是cpu-map,cpu-map节点的父节点必须是cpus节点。子节点必须是一个或者多个的cluster和socket节点。 -
socket 节点
socket 节点描述的是主板上的CPU插槽。主板上有几个CPU插槽,就有几个socket节点。socket节点的子节点必须是一个或者多个cluster节点。当有多个CPU插槽时,socket节点的命名方式必须是socketN,N=0,1,2… -
cluster节点
cluster节点用来描述CPU的集群。比如RK3399的架构是双核A72+四核A53,双核A72是一个集群,用一个cluster节点来描述,四核A53也是一个集群,用一个cluster节点来描述。cluster节点的命名方式必须是clusterN,N=0,1,2…,cluster节点的子节点必须是一个或者多个的cluster节点或者一个或者多个的core节点。
-
core节点
core节点用来描述一个cpu,如果是单核cpu,则core节点就是cpus节点的子节点。core节点的命名方式必须是coreN,N=0,1,2…,core节点的子节点必须是一个或者多个thread节点。
-
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-A7device_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行代码)。
总结:
- 在GPIO控制器中,必须有一个属性#gpio-cells,表示其他节点如果使用这个GPIO控制器需要几个cell来表示使用哪一个GPIO。
- 在GPIO控制器中,必须有一个属性gpio-controller,表示他是GPIO控制器。
- 在设备树中使用GPIO,需要使用属性data-gpios=<&gpio1 12 0>来指定具体的GPIO引脚。data-gpios属性可以为自定义属性。
举例(简化):
gpio1: gpio1 {gpio-controller;#gpio-cells = <2>;};[...]data-gpios = <&gpio1 12 0>, <&gpio1 15 0>;
其他属性
ngpios = <18>
- 作用:指定GPIO控制器管理的GPIO引脚总数
- 值说明:
18
表示该GPIO控制器有18个可用引脚(编号0-17) - 必要性:必需属性,驱动程序需要此信息初始化GPIO芯片
gpio-reserved-ranges = <0 4>, <12 2>
- 作用:指定保留/不可用的GPIO范围
- 格式:
<起始引脚 数量>
对 - 值说明:
<0 4>
:保留0-3号引脚(共4个)<12 2>
:保留12-13号引脚(共2个)
- 应用场景:
- 硬件设计上某些GPIO有特殊用途
- 防止驱动误用关键系统引脚
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>;};
};
- client端 (I2C2设备):
pinctrl-names = "default"
:定义引脚控制状态名称pinctrl-0 = <&pinctrl_i2c2>
:引用具体的引脚配置组
- 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
表示设备的状态。这里有default
和wake_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_1
和pinctrl_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.dtsi | rk3588-linux.dtsi | topeet_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。为大端模式。
成员介绍
字段 | 十六进制数值 | 代表含义 |
---|---|---|
magic | D00DFEED | 固定值 |
totalsize | 000002A4 | 转换为十进制为676,表示文件大小为676字节 |
off_dt_struct | 00000038 | 结构块从00000038地址开始,结合size_dt_struct确定结构块存储范围 |
off_dt_strings | 0000024C | 字符串块从0000024C地址开始,结合size_dt_strings确定字符串块存储范围 |
off_mem_rsvmap | 00000028 | 内存保留块偏移地址为00000028,位于header之后、结构块之前 |
version | 00000011 | 11(十六进制) = 17(十进制),表示当前设备树结构版本为17 |
last_comp_version | 00000010 | 10 转换为十进制之后为16,表示向前兼容的设备树结构 版本为16 |
boot_cpuid_phys | 00000000 | 表示设备树的teg属性为0 |
size_dt_strings | 00000058 | 表示字符串块的大小为 00000058 ,和前面的 off_dt_strings 字符串块偏移值一起可以确定字符串块的 范围 |
size_dt_struct | 00000214 | 表示结构块的大小为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
设备树是如何传递给内核的
- 编译阶段:DTC将.dts编译为.dtb二进制格式
- 加载阶段:U-Boot将内核和.dtb加载到内存
- 展开阶段:内核解析.dtb,构建设备树数据结构
- 使用阶段:驱动程序通过设备树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; // 指向该节点的下一个属性// ... 其他成员
};
- 每个字段的作用:
name
:属性名称字符串(如"compatible", "reg"等)length
:属性值的字节长度value
:指向属性值的指针next
:指向同一节点的下一个属性,形成链表
- 设备树节点的所有属性通过
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 呢?
转会规则
-
根节点下包含 compatible 属性的子节点
-
例如:
/ {mydevice {compatible = "vendor,device"; // ✅ 会被转换}; };
-
-
节点中 compatible 属性包含特定标识的节点
-
若节点的 compatible 属性包含以下值之一:
"simple-bus"
"simple-mfd"
"isa"
-
则该节点下包含 compatible 属性的子节点会被转换
-
例如:
bus {compatible = "simple-bus"; // 标识符#address-cells = <1>;#size-cells = <1>;child@0 {compatible = "vendor,child"; // ✅ 会被转换}; };
-
-
特殊排除规则
-
如果节点的 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
- 内核初始化时调用的入口函数
- 使用
arch_initcall_sync
修饰,在内核启动早期执行
-
of_platform_default_populate
- 参数全为NULL表示使用默认值
- 实际调用
of_platform_populate
-
of_platform_populate
- 核心转换函数
- 参数:
root
:设备树根节点matches
:总线匹配表(of_default_bus_match_table
)parent
:父设备(此处为NULL)
-
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{} /* 空值终止列表 */ };
- 定义了哪些总线类型下的节点需要转换
-
of_platform_device_create_pdata
- 为匹配的节点创建platform_device
- 调用
of_device_alloc
分配设备资源
-
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.dtsi | rk3588-linux.dtsi | topeet_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_u32 | of_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>;
- 含义:
- 子地址空间:
0x0
到0x0 + 0x100
(0x0-0x100
) - 父地址空间:
0x20
到0x20 + 0x100
(0x20-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。