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传递给内核即可。所以设备树就是用来描述硬件资源的文件。
设备树是描述硬件的文本文件,因为语法结构像树一样。所以叫设备树。
设备树的基本概念
基本名词解释
shell
复制代码
<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里面。(需要高版本的内核代码, 支持设备树)
编译设备树
shell
复制代码
dtc -I dts -O dtb -o xxx.dtb xxx.dts
反编译设备树
shell
复制代码
dtc -I dtb -O dts -o xxx.dts xxx.dtb
编译内核设备树
进入到内核的顶层路径,执行make dtbs
, 这种方法需要使能环境变量,暂时无法在我的rk3588内核上编译通过,会报错。
实验测试
编写一个简单的设备树文件,代码路径:/home/topeet/Linux/my-test/40_dtc/my_device_tree.dts, 代码如下所示:
shell
复制代码
/dts-v1/;
/ {
};
这个设备树很简单,只包含了根节点/,而根节点中没有任何子节点或属性。这个示例并 没有描述任何具体的硬件设备或连接关系,它只是一个最基本的设备树框架,在本小节只是为 了测试设备树的编译和反编译。
dts-v1
明确声明该文件使用设备树语法版本1 ,这是设备树源文件的强制要求 ,必须放在文件第一行,不能省略,否则编译报错。
编译my_device_tree.dts :
shell
复制代码
/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:
shell
复制代码
root@ubuntu:/home/topeet/Linux/my-test/40_dtc# ls
my_device_tree.dtb
反编译my_device_tree.dtb:
shell
复制代码
/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:
shell
复制代码
root@ubuntu:/home/topeet/Linux/my-test/40_dtc# ls
re_my_device_tree.dts
VSCode 安装设备树插件
搜索插件DeviceTree 并安装。
2.设备树语法
根结点
根结点是设备树必须包含的结点,根结点的名字 "/",如下所示:
shell
复制代码
/dts-v1/; // 第一行表示dts文件的版本
/{ // 根结点
};
子结点
格式 :
shell
复制代码
[label:] node-name[@unit-address] {
[properties definitions]
[child nodes]
};
properties definitions\] : 表示结点属性
\[child nodes\]:表示该结点的子结点
**举例**:
```shell
node1{//子节点,节点名称为node1
node1_child{//子子节点,节点名称为node1_child
};
};
```
注意:同级节点下节点名称不能相同。不同级节点名称可以相同
**范例代码**:
```shell
/dts-v1/; // 第一行表示dts文件的版本
/{ // 根结点
node1 { // 子结点1
node1-child {
};
};
node2 { // 子结点2
node1-child {
};
};
};
```
#### 结点名称
在对节点进行命名的时候,一般要体现设备的类型,比如网口一般命名成ethernet,串口一般命名成uart ,对于名称一般要遵循下面的命名格式。
格式:\[标签\]:\<名称\>\[@\<设备地址\>\] 其中,\[标签\]和\[@\<设备地址\>\]是可选项,\[名称\]是必选项。另 外,这里的设备地址也没有实际意义,只是让节点名称更人性化 ,更方便阅读。
**举例**: uart: serial@02288000 其中, uart就是这个节点标签,也叫别名, serial@02288000 就是节点名称。
**范例代码**:
```shell
/dts-v1/; // 第一行表示dts文件的版本
/{ // 根结点
node1 { // 子结点1
node1-child {
};
};
node2 { // 子结点2
node1-child {
};
};
led:gpio@02211000 {
node1-child {
};
};
};
```
**reg属性**
reg属性可以来描述地址信息。比如存储器的地址。
reg属性的格式如下: reg = \
**举例1** : reg = \<0x02200000 0x4000\>*;*
**举例2** : reg = \<0x02200000 0x4000 0x02205000 0x4000 \>*;*
#### #address-cell和#size-cells属性
#address-cell和#size-cells用来描述子结点中的reg属性中的地址和长度信息。
**举例1:**
```shell
node1 {
#address-cells = <1>; // 子结点中reg 属性有一个地址
#size-cells = <0>; // 子结点中reg 属性没有长度
node1-child {
reg = <0>;
};
};
```
**举例2:**
```shell
node1 {
#address-cells = <1>; // 子结点中reg 属性有一个地址
#size-cells = <1>; // 子结点中reg 属性有一个长度值
node1-child {
reg = <0x02200000 0x4000>;
};
};
```
**举例3:**
```shell
node1 {
#address-cells = <2>; // 子结点中reg 属性有二个地址
#size-cells = <0>; // 子结点中reg 属性没有长度值
node1-child {
reg = <0x00 0x01>;
};
};
```
*** ** * ** ***
#### model属性
model属性的值是一个字符串,一般用model描述一些信息.比如设备的名称,名字等。
**举例1:**
```shell
model = "wm8969-audio";
```
**举例2:**
```shell
model = "This is Linux board"
```
*** ** * ** ***
#### status属性
status属性和设备的状态有关系,status的属性是字符串,属性值有以下几个状态可选:
| 属性值 | 描述 |
|----------|-----------------------------|
| okay | 设备是可用状态 |
| disabled | 设备是不可用状态 |
| fail | 设备是不可用状态并且设备检测到了错误 |
| fail-sss | 设备是不可用状态并且设备检测到了错误,sss是错误内容 |
*** ** * ** ***
#### compatible属性
compatible属性是非常重要的一个属性。compatible是用来和驱动进行匹配的。匹配成功以后会执行驱动中的probe函数。
**举例:**
```shell
compatible = "xunwei", "xunwei-board"
//在匹配的时候会先使用第一个值"xunwei"进行匹配,如果没有就会使用第二个值"xunwei-board"进行匹配。
```
*** ** * ** ***
#### device_type属性
在某些设备树文件中,可以看到 device_type 属性,device_type 属性的值是字符串,只用于 cpu 节点或者 memory 节点进行描述。
**举例1:**
```shell
memory@30000000 {
device_type = "memory";
reg = <0x30000000 0x4000000>;
};
```
**举例2:**
```shell
cpu1: cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-a35", "arm,armv8";
reg = <0x0 0x1>;
};
```
*** ** * ** ***
#### **自定义属性**
设备树中规定的属性有时候并不能满足我们的需求,这时候我们可以自定义属性。
**举例:**
自定义一个管脚标号的属性 `pinnum`。
```shell
pinnum = <0 1 2 3 4>;
```
*** ** * ** ***
#### 设备树特殊结点
##### aliases
特殊节点 `aliases` 用来定义别名。定义别名的目的就是为了方便引用结点点。当然,除了使用 `aliases` 来命名别名,也可以在对结点命名的时候添加标签来命名别名。
**举例:**
```shell
aliases {
mmc0 = &sdmmc0;
mmc1 = &sdmmc1;
mmc2 = &sdhci;
serial0 = "/simple@fe000000/serial@llc500";
};
```
**chosen**
特殊节点 chosen 用来由 U-Boot 给内核传递参数。重点是 bootargs 参数。chosen 节点必须是根节点的子节点。
```shell
chosen {
bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
};
```
*** ** * ** ***
#### 官方设备树文档路径
https://app.readthedocs.org/projects/devicetree-specification/downloads/pdf/latest/
#### 综合示例:
```shell
/dts-v1/;
/{
model = "This is Linux board";
#address-cells = <1>;
#size-cells = <1>;
aliases{
led1=&led; //给led取别名led1
led2=&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处理器中断节点实例:
```shell
//RK原厂工程师编写
gpio0: gpio@fdd60000 {
compatible = "rockchip,gpio-bank";
reg = <0x0 0xfdd60000 0x0 0x100>;
interrupts = ;
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和中断
*** ** * ** ***
```shell
// 开发人员编写的设备树节点
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
```shell
// 主中断控制器(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>; // 级联到主GIC
interrupts = <0 99 IRQ_TYPE_LEVEL_HIGH>; // 使用GIC的99号中断
};
```
使用interrupt-extended 来表示多组中断控制器
```shell
// 主中断控制器
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 = ; // 连接到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匹配值相关的部分,如下所示:
```c
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 编号的宏定义,如下图所示:
```c
#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。有了以上信息后,我们就可以编写触摸屏中断的设备树,如下所示:
```shell
/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 = ;
};
};
```
第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:单路时钟输出**
```shell
osc24m: osc24m {
compatible = "fixed-clock";
clock-frequency = <24000000>; // 24MHz时钟
clock-output-names = "osc24m"; // 时钟输出名称
#clock-cells = <0>; // 表示只有1路时钟输出
};
```
**举例2:多路时钟输出**
```shell
clock: clock-controller {
#clock-cells = <1>; // 表示有多路时钟输出
clock-output-names = "clock1", "clock2"; // 两路时钟名称
};
```
###### clock-frequency
clock-frequency 属性可以指定时钟的大小。
举例1:
```shell
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一般成对使用。当输出多路时钟时,为每路时钟进行编号。
举例:
```shell
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:标准索引映射**
```c
scpi_dvfs: clocks@0 {
#clock-cells = <1>; // 需要1个参数标识时钟
clock-indices = <0>, <1>, <2>; // 显式定义索引号
clock-output-names = "atlclk", "aplclk", "gpuclk"; // 三个时钟输出
};
```
**举例2:非连续索引映射**
```c
scpi_clk: clocks@1 {
#clock-cells = <1>; // 需要1个参数标识时钟
clock-indices = <3>; // 定义索引号为3(非连续)
clock-output-names = "pxlclk"; // 单个时钟输出
};
```
###### assigned-clock-parents
assigned-clock-parents 属性可以用来设置时钟的父时钟。
举例:
```c
clock:clock {
assigned-clock = <&clkcon 0>, <&pll 2>;
assigned-clock-parents = <&pll 2>;
assigned-clock-rates = <115200>, <9600>;
};
```
*** ** * ** ***
##### 消费者属性
###### clock-name
clocks属性和clock-name属性用来指定使用的时钟源和消费者中时钟的名字。
举例:
```c
clock:clock {
clocks = <&cru CLK_VOP>;
clock-names = "clk_vop",;
};
```
注:cru是clock reset unit的缩写,pmu是power management unit的缩写。
*** ** * ** ***
**消费者时钟节点实例分析**:
```c
gpio1: gpio@fe740000 {
compatible = "rockchip.gpio-bank";
reg = <0x0 0xfe740000 0x0 0x100>;
interrupts = ;
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 = ;
#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"。
*** ** * ** ***
```c
usb2phy0: usb2-phy@fe8a0000 {
compatible = "rockchip,rk3568-usb2phy";
reg = <0x0 0xfe8a0000 0x0 0x10000>;
interrupts = ;
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**
```c
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
compatible = "arm, cortex-a7";
device_type = "cpu";
};
};
```
**cpus 节点**
```c
cpus {
#address-cells = <1>;
#size-cells = <0>;
...
};
```
* **作用**:系统CPU的父容器节点
* **属性** :
* `#address-cells = <1>`:子节点地址字段使用1个32位单元
* `#size-cells = <0>`:子节点大小字段不使用任何单元
* **位置**:必须是根节点(/)的直接子节点
##### **cpu0: cpu@0 节点**
```c
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**
```c
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**
```c
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里面有两个线程。**
```c
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
```c
gpio0: gpio@fdd60000 {
compatible = "rockchip,gpio-bank";
reg = <0x0 0xfdd60000 0x0 0x100>;
interrupts = ;
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 = ;
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属性可以为自定义属性。
**举例(简化):**
```c
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 是一个三元组或四元组列表:
```c
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:**
```c
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 用法**
```c
/* 引脚控制器 */
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:**
```c
// client端:
&i2c2 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c2>;
};
// service端
&iomuxc {
pinctrl_i2c2: i2c2grp {
fsl,pins = <
MX6UL_PAD_UART5_TX_DATA__I2C2_SCL 0x4001b8b0
MX6UL_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:**
```c
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_hog_1>;
```
**解析:**
使用`pinctrl-names`表示设备的状态。这里只有一个`default`状态,`default`为第0个状态。`pinctrl-0 = <&pinctrl_hog_1>`表示第0个状态`default`对应的引脚在`pinctrl_hog_1`节点中配置。
**举例3:**
```c
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:**
```c
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示例:**
```c
// 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,到根节点最后添加代码:
```c
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服 务端节点,仿写完成的节点内容如下所示:
```c
&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文件格式
```c
/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](https://binary-viewer.en.lo4d.com/windows))
small header(头部),memory reservation block(内存预留块),structure block(结构块),strings block(字符串块)。free space(自由空间)不一定存在。

##### 1.header
```c
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` 的使用方法:
```c
/memreserve/ ;
```
其中 `` 和 `` 是 64 位 C 风格整数,例如:
```c
/* Reserve memory region 0x10000000..0x10003fff */
/memreserve/ 0x10000000 0x4000;
```
在内存保留块的存储格式:
```c
struct fdt_reserve_entry {
uint64_t address;
uint64_t size;
};
```
##### 3.字符串块
字符串块用来存放属性的名字,比如 compatible、reg 等。通过分析 DTB 的头部,我们已经知道字符串块的位置,如 model 在 DTB 中的表示:

##### 4.结构块
结构块描述的是设备树的结构,也就是设备树的节点。那如何表示一个节点的开始和结束呢?使用 `0x00000001` 表示节点的开始,然后跟上节点名字(根节点的名字用 `0` 表示),然后使用 `0x00000003` 表示一个属性的开始(每表示一个属性,都要用 `0x00000003` 表示开始),使用 `0x00000002` 表示节点的结束,使用 `0x00000009` 表示根节点的结束(整个结构块的结束)
属性的名字和值用结构体表示:
```C
struct {
uint32_t len;
uint32_t nameoff;
}
```
* `len` 表示属性值的长度
* `nameoff` 表示属性名字在字符串块中的偏移
例子中以下节点在 DTB 中是如何表示的呢?
```c
{
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`,它是内核中表示设备树节点的核心数据结构, 结构体如下所示:
```c
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`,结构体如下所示:
```c
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内核的核心函数之一,负责完成内核的初始化和启动过程,具体内容如下所示:
```c
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(); //设置处理器ID
debug_objects_early_init(); //初始化调试对象
cgroup_init_early(); //初始化cgroup(控制组)
local_irq_disable(); //禁用本地中断
early_boot_irqs_disabled=true; //标记早期引导期间中断已禁用
/*
*中断仍然被禁用。进行必要的设置,然后启用它们。
*/
boot_cpu_init(); //初始化引导CPU
page_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(); //初始化热插拔的引导CPU
build_all_zonelists(NULL); //构建所有内存区域列表
page_alloc_init(); //初始化页面分配器
........
}
```
代码第17行`setup_arch(&command_line);`该函数定义在内核源码的 `/arch/arm64/kernel/setup.c`文件中,具体内容如下所示:
```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`文件中,具体内容如下所示:
```shell
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" 文件中,具体内容如下所示:
```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"目录下,具体内容如下所示:
```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。该函数的具体内容如下所示:
```c
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()函数。这个函数的作用是扫描设备树的节点并进行相应的处理, 该函数的具体内容如下所示:
```c
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"文件中,具体内容如下所示:
```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这一设备树的解析函数进行详细的介绍,该函数的具体内容 如下所示:
```c
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函数具体定 义如下所示:
```c
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; //根节点为@dad
nps[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]; //将子节点指针赋值给@nodepp
if (!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()`函数定义如下所示:
```c
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 = "";
}
*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 属性的子节点**
* 例如:
```c
/ {
mydevice {
compatible = "vendor,device"; // ✅ 会被转换
};
};
```
2. **节点中 compatible 属性包含特定标识的节点**
* 若节点的 compatible 属性包含以下值之一:
* `"simple-bus"`
* `"simple-mfd"`
* `"isa"`
* 则该节点下包含 compatible 属性的子节点会被转换
* 例如:
```c
bus {
compatible = "simple-bus"; // 标识符
#address-cells = <1>;
#size-cells = <1>;
child@0 {
compatible = "vendor,child"; // ✅ 会被转换
};
};
```
3. **特殊排除规则**
* 如果节点的 compatible 属性包含 `"arm,primecell"` 值
* 则该节点会被转换为 amba 设备(**不是** platform_device)
* 例如:
```c
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`**
```c
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资源
##### 资源转换示例:
```c
// 设备树节点
serial@4000 {
compatible = "ns16550a";
reg = <0x4000 0x100>;
interrupts = <10 1>;
};
```
转化如下:
```c
// 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 是顶层设备树,为了便于理解我们之后在该设备树下进行节 点的添加(当然这里也可以修改其他设备树),进入该设备树文件之后如下所示:
```c
/ {
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, 代码如下所示:
```c
#include
#include
#include
#include
#include
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");
```
###### 查看设备树节点是否成功被加载到系统
```shell
ls /sys/firmware/devicetree/base/topeet/
'#address-cells' '#size-cells' compatible myLed name
```
### 4.of操作函数
#### device_node结构体
Linux 内核使用device_node结构体描述一个和结点,这个结构体定义在文件include/linux/of.h中:
```c
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;
#endif
unsigned 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)
```c
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)
```c
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
```c
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`**
适用于单值属性:
```c
clock-frequency = <50000000>; // 单个值
```
```c
u32 clk_freq;
of_property_read_u32(np, "clock-frequency", &clk_freq);
```
* **`of_property_read_u32_index`**
适用于多值数组中的特定元素:
```c
reg = <0x40008000 0x1000>; // 两个值的数组
interrupts = <0 40 0x4>; // 三个值的数组
```
```c
u32 irq_num;
// 读取interrupts属性的第2个值(索引1)
of_property_read_u32_index(np, "interrupts", 1, &irq_num);
```
*** ** * ** ***
###### of_property_count_elems_of_size
该函数在设备树中的设备节点下查找指定名称的属性,并获取该属性中元素的数量。调用 该函数可以用于获取设备树属性中某个属性的元素数量,比如一个字符串列表的元素数量或一 个整数数组的元素数量等。
```c
#include
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)
```c
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)
```c
u64 of_translate_address(struct device_node *np, const __be32 *addr);
```
* 将逻辑地址转换为物理地址
###### of_iomap(node, index)
```c
void __iomem *of_iomap(struct device_node *np, int index);
```
* 直接映射设备内存到虚拟地址空间
*** ** * ** ***
##### 5.中断相关函数
###### of_irq_get(node, index)
```c
int of_irq_get(struct device_node *np, int index);
```
* 获取中断号
###### of_irq_to_resource_table()
* 解析中断资源表
###### gpio_to_irq()
```c
int gpio_to_irq(unsigned int gpio)
```
**函数作用**:
获取中断号。
**函数参数**:
gpio: gpio编号
**返回值**:
成功返回对应中断号。
###### irq_of_parse_and_map()
```c
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
```
**函数作用**:
从设备树节点的 `interrupts` 属性中解析并映射到对应的硬件中断号。
**函数参数**:
`*dev`: 目标设备节点
`index`:要获取的中断在属性中的索引位置
**返回值**:
成功:返回映射后的中断号
失败:返回0
###### irqd_get_trigger_type()
```c
u32 irqd_get_trigger_type(struct irq_data *d)
```
**函数作用**:
从 `irq_data` 结构中获取中断触发类型标志
**函数参数**:
\*d: 指向 irq_data 结构的指针
**返回值**:
成功 返回中断触发标志。
失败 返回0
###### irq_get_irq_data()
```c
struct irq_data *irq_get_irq_data(unsigned int irq)
```
**函数作用**:
通过中断号获取对应的 `irq_data` 结构体
**函数参数**:
irq : 中断号
**返回值**:
成功: 返回指向irq_data的指针。
失败:返回NULL。
**示例代码**:
```c
#include
// 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. **基本格式**
```c
ranges = ;
```
或
```c
ranges; // 空属性
```
##### 2. **字段说明**
| 字段 | 说明 | 长度决定属性 |
|:---------------------|:-----------|:---------------------------|
| `child-bus-address` | 子地址空间的起始地址 | 由当前节点的 `#address-cells` 决定 |
| `parent-bus-address` | 父地址空间的起始地址 | 由父节点的 `#address-cells` 决定 |
| `length` | 映射区域的大小 | 由父节点的 `#size-cells` 决定 |
##### 3. **示例解析**
```c
ranges = <0x0 0x20 0x100>;
```
* **含义** :
* 子地址空间:`0x0` 到 `0x0 + 0x100`(`0x0-0x100`)
* 父地址空间:`0x20` 到 `0x20 + 0x100`(`0x20-0x120`)
* **映射关系** :子空间的 `0x0-0x100` 映射到父空间的 `0x20-0x120`
##### 4. **特殊值含义**
| 属性值 | 含义 |
|:----------------|:-----------------|
| `ranges;` | 1:1 映射(内存区域直接映射) |
| `ranges = < >;` | 无映射(地址空间不转换) |
##### 5. **关键属性依赖**
```c
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:内存映射外设
```c
// 父节点定义
soc {
compatible = "simple-bus";
#address-cells = <2>;
#size-cells = <2>;
ranges; // 1:1映射
};
// 子节点(直接映射)
uart0: uart@ff000000 {
reg = <0x0 0xff000000 0x0 0x1000>;
};
```
###### 场景2:地址转换(PCIe设备)
```c
pcie_controller {
#address-cells = <3>;
#size-cells = <2>;
// 子地址 → 父地址转换
ranges = <0x02000000 0 0xe0000000 0xc 0x20000000 0 0x20000000>;
// 含义:
// 子空间: PCIe内存空间 (0x02000000)
// 父空间: 0xc20000000-0xc3fffffff
};
```
###### 场景3:多级转换
```c
// 一级转换
soc {
ranges = <0x0 0xf0000000 0x100000>;
// 二级转换
i2c@1000 {
ranges = <0x0 0x1000 0x100>;
// 实际映射:
// 子地址0x0 → soc地址0x1000 → 最终物理地址0xf0001000
};
};
```
##### 7. **字节序与数据格式**
* 所有值均为 **大端序 (Big-Endian)**
* 每个值占用32位(4字节)
* 示例解析:
```c
<0x00000000 0x20000000 0x00001000>
// 等同于
<0x0 0x20 0x1000> // 简写形式
```
##### 8. **常见错误处理**
```c
/* 错误示例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。