一、Linux驱动-设备树基础
背景
问题
Linux 之父 Linus Torvalds 在 2011 年 3 月 17 日的 ARM Linux 邮件列表中曾提到:This whole ARM thing is a fucking pain in the ass。之后,ARM Linux 社区引入了设备树(Device Tree)。
那么,为何 Linus Torvalds 会说出这样的话呢?在前面分析的时候,平台总线模型将驱动程序拆分为了两个部分:设备信息和驱动程序本身。这一设计无疑是优雅的,设备部分用于描述硬件,通常会放在内核源码的 arch/arm/plat-xxx 和 arch/arm/mach-xxx 目录下。
然而,随着 Linux 支持的硬件不断增加,内核源码中描述硬件的代码也在不断膨胀。每次修改这些代码都需重新编译内核,长此以往,内核中累积了大量的"垃圾代码"。
例子
假设有三家不同的公司都使用了三星的S3C2410这款ARM SoC芯片来设计自己的产品板卡:
-
• 公司A:生产了产品 FooBoard,使用了UART0和UART1。
-
• 公司B:生产了产品 BarBoard,只使用了UART0。
-
• 公司C:生产了产品 BazBoard,使用了UART0和UART2。
在设备树出现之前,这些设备信息需要以C代码的形式硬编码在内核源码的 arch/arm/mach-s3c2410/ 目录下的板级文件中。内核开发者需要为每一块不同的板卡编写一个板级文件(Board File),例如 mach-foobar.c。在这个文件里,他们会用C语言数据结构来描述这块板子上的设备。
在三家公司的板级文件中,对于UART0设备,你可能会看到类似这样的代码:
static struct plat_serial8250_port serial_ports[] __initdata = {
{
.map_base = S3C2410_U0_BASE,
.membase = (char __iomem *)S3C2410抽出U0_BASE,
.irq = IRQ_U0,
.uartclk = S3C2410 clock frequency,
.regshift = 2,
.flags = UPF_BOOT_AUTOCONF | UPF_NO_RX_FIFO,
},
};
具体实现会根据公司的实际需求有所不同。例如:
-
• 对于 公司A(使用了UART0和UART1):
static struct plat_serial8250_port serial_ports[] __initdata = {
{
.map_base = S3C2410_U0_BASE,
.membase = (char __iomem *)S3C2410_U0_BASE,
.irq = IRQ_U0,
.uartclk = S3C2410 clock frequency,
.regshift = 2,
.flags = UPF_BOOT_AUTOCONF | UPF_NO_RX_FIFO,
},
{
.map_base = S3C2410_U1_BASE,
.membase = (char __iomem *)S3C2410_U1_BASE,
.irq = IRQ_U1,
.uartclk = S3C2410 clock frequency,
.regshift = 2,
.flags = UPF_BOOT_AUTOCONF | UPF_NO_RX_FIFO,
},
}; -
• 对于 公司B(只使用了UART0):
static struct plat_serial8250_port serial_ports[] __initdata = {
{
.map_base = S3C2410_U0_BASE,
.membase = (char __iomem *)S3C2410_U0_BASE,
.irq = IRQ_U0,
.uartclk = S3C2410 clock frequency,
.regshift = 2,
.flags = UPF_BOOT_AUTOCONF | UPF_NO_RX_FIFO,
},
}; -
• 对于 公司C(使用了UART0和UART2):
static struct plat_serial8250_port serial_ports[] __initdata = {
{
.map_base = S3C2410_U0_BASE,
.membase = (char __iomem *)S3C2410_U0_BASE,
.irq = IRQ_U0,
.uartclk = S3C2410 clock frequency,
.regshift = 2,
.flags = UPF_BOOT_AUTOCONF | UPF_NO_RX_FIFO,
},
{
.map_base = S3C2410_U2_BASE,
.membase = (char __iomem *)S3C2410_U2_BASE,
.irq = IRQ_U2,
.uartclk = S3C2410 clock frequency,
.regshift = 2,
.flags = UPF_BOOT_AUTOCONF | UPF_NO_RX_FIFO,
},
};
这种硬编码的方式存在下面问题
代码重复(Code Duplication):三块板子的描述代码中,关于UART0的定义是完全重复的。它们描述了同一个芯片(S3C2410)内部的同一个硬件模块。这些冗余代码对内核主线毫无价值,纯粹是"垃圾代码"。
内核膨胀(Kernel Bloat):每支持一款新的板卡,就需要向内核源码树中添加一个新的板级文件(mach-xxx.c)。成千上万的板卡意味着成千上万个文件,导致内核源代码急剧膨胀,其中绝大部分是描述硬件的"垃圾"。
维护噩梦(Maintenance Nightmare):如果发现S3C2410的UART0驱动有一个通用BUG,修复后需要为所有使用了S3C2410的板级文件重新编译内核。这毫无必要,因为硬件本身并没变。
合并冲突(Merge Hell):ARM社区的开发者不断向Linus的主线内核提交这些大量、重复、板级相关的代码。这导致 arch/arm/目录变得无比臃肿,每次合并分支都可能产生冲突,让维护工作变得极其痛苦。
解决
为了解决这个问题,设备树被引入到 Linux 中。设备树通过描述硬件信息来替代内核源码中原本的"垃圾代码",从而简化了系统维护。
设备树是一种用于描述硬件资源的文件,可以替代原本需要硬编码在 arch/arm/mach-s3c2410/ 目录下的板级文件中描述硬件的代码。虽然设备树替换了原来的设备描述代码,但平台总线模型的基本匹配和使用方式基本保持不变。这样,即使对硬件进行修改,也不必重新编译内核,只需要通过 bootloader 将设备树文件传递给内核即可。
因此,设备树的主要作用是描述硬件资源,简化了硬件描述代码的管理和维护。
设备树文件编译
基本名词解释
| 缩写 | 全称 | 说明 |
|---|---|---|
| DT | Device Tree | 设备树,一种用于描述硬件拓扑和数据的数据结构。 |
| FDT | Flattened Device Tree | 二进制形式的扁平设备树,起源于 OpenFirmware (OF)。 |
| dts | Device Tree Source | 人类可读的文本文件,用于描述硬件配置。 |
| dtsi | Device Tree Source Include | 设备树源包含文件,通常包含 SoC 级或公共的硬件描述,可被多个 dts 文件引用。 |
| dtb | Device Tree Blob | 由 dts 源码编译后得到的二进制文件,用于引导内核时传递硬件信息。 |
| dtc | Device Tree Compiler | 设备树编译器,负责将 dts 源码编译成 dtb 二进制文件。 |
关键点
-
• 其中 DT是LInux 的设备树模型其模型是源自于FDT(开放设备树)在后续的设备树调用的函数都会有很多以
XXXof命名就是因为DT是源自于FDT的原因 -
• dtsi是芯片的通用说明书(有什么),dts是板卡的具体接线图(用什么、怎么用)。dts通过 #include继承 dtsi的所有内容,然后修改和添加自己需要的内容。 这种设计完美清除了内核中冗余的板级"垃圾代码"。
-
• dtb的关系图如下

场景描述

芯片厂商:Awesome-Semi
Awesome-Semi 生产了一款 AS100 芯片,集成了一些固定资源:
-
• 1 个串口
uart0,地址在0x10000000 -
• 1 个 I2C 控制器
i2c0,地址在0x20000000
设备厂商
公司A 使用 AS100 芯片制作了一块叫"简单板"的开发板,仅使用了串口。
公司B 使用 AS100 芯片制作了一块叫"扩展板"的开发板,使用了串口和 I2C,并且在 I2C 总线上挂了一个温度传感器。
文件细节
-
- 芯片厂商提供:as100.dtsi (公共菜单)
这个文件描述了 AS100 芯片的所有硬件资源,并默认将它们置于"禁用"状态。
cpp// File: as100.dtsi / { soc { compatible = "awesome-semi,as100"; // 串口0的定义 uart0: serial@10000000 { compatible = "awesome-semi,as100-uart"; reg = <0x10000000 0x1000>; status = "disabled"; // 【关键】默认禁用,等待板卡来启用 }; // I2C控制器0的定义 i2c0: i2c@20000000 { compatible = "awesome-semi,as100-i2c"; reg = <0x20000000 0x1000>; #address-cells = <1>; #size-cells = <0>; status = "disabled"; // 【关键】默认禁用,等待板卡来启用 }; }; };作用 :这份公共菜单列出了芯片能提供的所有"菜"(硬件外设),但每道菜默认都不上(
status = "disabled")。 -
- 设备厂商A提供:company-a-simple-board.dts (点菜单A)
公司A根据自己板卡的设计,来"点菜"。
// File: company-a-simple-board.dts /dts-v1/; #include "as100.dtsi" // 【核心关系】包含公共菜单 / { model = "Company A Simple Board"; compatible = "company-a,simple-board", "awesome-semi,as100"; // 选中uart0这道"菜",将其状态从"禁用"改为"可用" &uart0 { status = "okay"; }; // i2c0没有被引用,所以它保持"disabled",不会被启用 };最终配置 :只有
uart0被启用。 -
- 设备厂商B提供:company-b-advanced-board.dts (点菜单B)
公司B的板卡更复杂,它点了更多的菜,甚至还"加了料"(在I2C总线上添加了子设备)。
// File: company-b-advanced-board.dts /dts-v1/; #include "as100.dtsi" // 【核心关系】包含同一个公共菜单 / { model = "Company B Advanced Board"; compatible = "company-b,advanced-board", "awesome-semi,as100"; // 1. 启用uart0 &uart0 { status = "okay"; }; // 2. 启用i2c0总线 &i2c0 { status = "okay"; // 3. 【补充】在已启用的i2c0总线上"加料",添加一个温度传感器设备 // 这个信息是板卡特有的,不可能出现在芯片通用的dtsi中 temperature-sensor@48 { compatible = "ti,tmp175"; reg = <0x48>; }; }; };最终配置 :
uart0和i2c0被启用,并且在i2c0上挂载了一个温度传感器。
编译语法
编译设备树:dtc -I dts -O dtb -o xxx.dtb xxx.dts
| 参数 | 全称 | 含义 | 解释 |
|---|---|---|---|
| dtc | Device Tree Compiler | 设备树编译器 | 调用的工具名称 |
| -I dts | --in-format dts | 输入文件格式为 dts | 告诉 dtc,你提供的源文件是一个文本格式的设备树源码文件 |
| -O dtb | --out-format dtb | 输出文件格式为 dtb | 告诉 dtc,你希望生成一个二进制格式的设备树 blob 文件 |
| -o xxx.dtb | --output xxx.dtb | 输出文件名 | 指定编译后生成的二进制文件的名字,这里是 xxx.dtb |
| xxx.dts | (无) | 输入文件名 | 指定源代码文件的名字,这里是 xxx.dts。这是命令的最后一个参数 |
反编译设备树:dtc-I dtb -O dts -o xxx.dts xxx.dtb
| 参数 | 全称 | 含义 | 解释 |
|---|---|---|---|
| dtc | Device Tree Compiler | 设备树编译器 | 同时具备编译和反编译功能的工具 |
| -I dtb | --in-format dtb | 输入文件格式为 dtb | 告诉 dtc,你提供的源文件是一个二进制格式的设备树blob文件 |
| -O dts | --out-format dts | 输出文件格式为 dts | 告诉 dtc,你希望生成一个文本格式的设备树源码文件 |
| -o xxx.dts | --output xxx.dts | 输出文件名 | 指定反编译后生成的文本文件的名字,这里是 xxx.dts |
| xxx.dtb | (无) | 输入文件名 | 指定二进制文件的名字,这里是 xxx.dtb |
设备树常用语法
节点(Nodes)
节点是设备树的基本构建块,代表一个设备或一个总线连接点。
定义语法:
node-name@unit-address {
property1 = value;
property2 = value;
child-node@address {
...
};
};
-
•
node-name:节点名称,应使用设备类型(如uart,i2c,gpio)而非芯片型号。 -
•
@unit-address:单元地址。用于区分同一层级同名的多个节点。通常是该设备在其父总线上的地址(如内存映射地址0x10000000)或芯片选择号(如spi0总线上的cs0)。如果节点无地址,可省略@unit-address部分。 -
•
child-node:子节点,代表一个连接到父节点所代表总线上的设备。
根节点 :
整个设备树的起点,用 / 表示。
/ {
compatible = "acme,coyotes-revenge";
...
};
属性(Properties)
属性是附着在节点上的键值对(name-value pairs),用于描述节点的具体特征。属性值类型有
-
• 空值(empty) :仅有关键字,没有值。用于表示一个布尔值
true。dma-coherent; // 表示该设备是 DMA 一致的 -
• 字符串(string) :
compatible = "arm,cortex-a9"; device_type = "memory"; status = "okay"; -
• 字符串数组(list of strings) :用逗号分隔的多个字符串。
compatible = "fsl,imx6ull-gpio", "fsl,imx6q-gpio"; // 驱动会按顺序尝试匹配 -
• 单元格(cell) :32 位无符号整数,用
<>括起来。cppreg = <0x10000000 0x1000>; // 表示一个起始地址为 `0x10000000`,长度为 `0x1000` 的区域 #size-cells = <1>; -
• 字节序列(bytestring) :用
[]括起来的十六进制字节流。local-mac-address = [00 11 22 33 44 55]; // 表示一个 MAC 地址
标签(Labels)与引用(References)
为了方便节点间的引用和覆盖,设备树引入了标签机制。
标签(Label) :
在节点名前加上 &label,为该节点创建一个标签。
uart0: serial@10000000 {
...
};
这里 uart0 就是节点 serial@10000000 的标签。
引用(Reference) :
使用 &label 来引用一个已被定义的节点。
&uart0 {
status = "disabled"; // 通过引用来覆盖原节点中的属性
};
compatible
-
• 作用: 用于将设备与驱动程序绑定。内核通过检查此属性来找到最适合该设备的驱动程序。
-
• 值: 一个或多个字符串。格式通常为 "manufacturer,model"。
-
• 示例 :
compatible = "nvidia,tegra20-uart", "ns16550a"; // 内核会优先寻找匹配 "nvidia,tegra20-uart" 的驱动程序,若未找到,则使用更通用的 "ns16550a" 驱动。
reg
-
• 作用: 描述设备在父总线地址空间内的内存映射区域(寄存器块)。
-
• 值 : 一个或多个 (address, length) 对。
#address-cells和#size-cells属性决定了address和length分别由几个单元格(cell)表示。 -
• 示例 :
cpp#address-cells = <1>; // 表示 address 用 1 个 cell (32位) #size-cells = <1>; // 表示 size 用 1 个 cell (32位) serial@10000000 { compatible = "ns16550a"; reg = <0x10000000 0x1000>; // 起始地址 0x10000000,长度 0x1000 字节 };
#address-cells 和 #size-cells
-
• 作用 : 并非设备属性,而是 节点属性 。它们用于说明其 子节点 的
reg属性中address和length字段分别占用多少个单元格(cell)。 -
• 示例 :
cppsoc { #address-cells = <1>; #size-cells = <1>; // 子节点的 reg 格式为 (addr, size) uart@10000000 { reg = <0x10000000 0x1000>; }; }; i2c@20000000 { #address-cells = <1>; #size-cells = <0>; // I2C 设备没有地址范围概念,只有设备地址,所以 size 为 0 eeprom@50 { compatible = "atmel,24c02"; reg = <0x50>; // 设备地址是 0x50,没有长度信息 }; };
status
-
• 作用: 指示设备的操作状态。
-
• 常用值:
-
• "okay": 设备已启用。
-
• "disabled": 设备存在但未被启用。
-
• "fail" 或 "fail-sss": 设备存在但运行失败。
-
interrupts 与 interrupt-parent
-
• 作用: 描述设备的中断连接。
-
•
interrupts指定中断号和触发方式等信息。 -
•
interrupt-parent指向该设备所连接的中断控制器节点,若未指定,则从父节点继承。
-
-
• 示例:
cpp
// 定义一个中断控制器
intc: interrupt-controller@40000000 {
compatible = "arm,pl190";
reg = <0x40000000 0x1000>;
interrupt-controller; // 这是一个空属性,标识本节点为中断控制器
#interrupt-cells = <2>; // 说明 interrupts 属性需要 2 个 cell,通常第一个是中断号,第二个是触发标志
};
// 一个使用中断的设备
uart@10000000 {
compatible = "ns16550a";
reg = <0x10000000 0x1000>;
interrupts = <5 1>; // 中断号 5,触发方式 1 (高电平触发)
interrupt-parent = <&intc>; // 引用上面定义的中断控制器
};
aliases
-
• 作用:它是一个包含一系列属性的特殊节点,这些属性的名称是你想要的别名,值是你要指向的节点的完整路径。
-
• 基本语法
/ {
aliases {
alias-name = &node-label;
// 或者使用完整路径
alias-name = "/full/path/to/node";
};
}; -
• alias-name: 别名的名称,通常是一个不带地址的通用设备名,如 serial0, serial1, ethernet0, mmc0, mmc1等。
-
• &node-label: 这是最常用和推荐的方式。通过标签来引用目标节点,编译器(dtc)会自动将其解析为节点的完整路径。
-
• "/full/path/to/node": 直接使用节点的字符串路径。这种方式非常脆弱,一旦节点路径发生变化,别名就会失效,因此不推荐使用
-
• 例子
/dts-v1/;
/ {
compatible = "my-company,my-board";
aliases {
serial0 = &uart0; // 为uart0创建别名serial0
serial1 = &uart1; // 为uart1创建别名serial1
ethernet0 = &fec0; // 为以太网控制器创建别名ethernet0
};soc { serial0: uart@10000000 { compatible = "ns16550a"; reg = <0x10000000 0x1000>; status = "okay"; }; serial1: uart@10001000 { compatible = "ns16550a"; reg = <0x10001000 0x1000>; status = "okay"; }; fec0: ethernet@10002000 { compatible = "fsl,imx6q-fec"; reg = <0x10002000 0x1000>; status = "okay"; }; };};
使用 dtc编译器编译上述 .dts文件后,生成的 .dtb文件中,aliases节点内的属性值会被展开为完整的节点路径。你可以用 fdtdump工具查看:
aliases {
serial0 = "/soc/uart@10000000";
serial1 = "/soc/uart@10001000";
ethernet0 = "/soc/ethernet@10002000";
};
chosen
作用
chosen 节点用于 U-Boot 向内核传递参数,特别是 bootargs 参数。chosen 节点必须是根节点的子节点。
-
• stdout-path:指定内核默认的标准输出设备。其值通常是一个别名或路径,指向一个串口或其他显示设备。
-
• stdin-path:指定内核默认的标准输入设备。
-
• bootargs :最常用的属性,用于传递内核启动参数。Bootloader(如 U-Boot)会将自己环境变量中的
bootargs写入到这个属性中,然后传递给内核。/ {
chosen {
stdout-path = &serial0; // 指定标准输出到别名serial0所指向的串口
bootargs = "console=ttyS0,115200 earlycon root=/dev/mmcblk1p2 ro"; // 内核启动参数
};
};
device_type
-
• 作用: 在某些设备树文件中,可以看到device_type属性,dedevice_type 属性的值是字符串,只用于cpu节点或者memory节点进行描述。
// 这是 device_type 的使用场景:描述 CPU 和内存
/ {
cpus {
cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a53";
...
};
};memory@80000000 { device_type = "memory"; reg = <0x80000000 0x40000000>; };};
例子
cpp
/dts-v1/;
/ {
// 1. 根节点和基本属性
compatible = "acme,example-board", "acme,example-soc"; // 板级和SoC兼容性
model = "Acme Example Board rev 1.0"; // 板卡型号
#address-cells = <1>; // 子节点地址用1个单元格表示
#size-cells = <1>; // 子节点大小用1个单元格表示
// 2. chosen节点 - 用于传递固件/内核参数
chosen {
bootargs = "console=ttyS0,115200n8 earlycon root=/dev/mmcblk0p2 rootwait"; // 内核启动参数
stdout-path = &uart0; // 标准输出设备
// 自定义属性示例
my-custom-prop = "custom-value";
};
// 3. aliases节点 - 为设备提供短名称
aliases {
serial0 = &uart0; // 串口0别名
serial1 = &uart1; // 串口1别名
ethernet0 = ð0; // 以太网0别名
mmc0 = &sdhci0; // MMC/SD控制器0别名
};
// 4. CPU节点 - 使用device_type属性
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
device_type = "cpu"; // 指定设备类型为CPU
compatible = "arm,cortex-a53";
reg = <0>;
clock-frequency = <1200000000>; // 1.2GHz
// 自定义属性示例
cpu-supply = <&vdd_cpu>;
};
cpu1: cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-a53";
reg = <1>;
clock-frequency = <1200000000>;
cpu-supply = <&vdd_cpu>;
};
};
// 5. 内存节点 - 使用device_type属性
memory@80000000 {
device_type = "memory"; // 指定设备类型为内存
reg = <0x80000000 0x40000000>; // 1GB RAM
};
// 6. 片上系统(SoC)节点
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0x40000000 0x10000000>; // 将子地址0x0映射到物理地址0x40000000
// 7. 系统控制器
syscon: system-controller@1000 {
compatible = "acme,example-syscon", "syscon";
reg = <0x1000 0x100>;
#clock-cells = <1>;
#reset-cells = <1>;
};
// 8. 中断控制器
intc: interrupt-controller@2000 {
compatible = "arm,pl190";
reg = <0x2000 0x1000>;
interrupt-controller; // 标识此为中断控制器
#interrupt-cells = <2>; // 中断说明符需要2个单元格
};
// 9. 串口0 - 使用标签和引用
uart0: serial@3000 {
compatible = "ns16550a";
reg = <0x3000 0x100>;
interrupts = <0 4>; // 中断号0,高电平触发
interrupt-parent = <&intc>; // 引用中断控制器
clocks = <&clkdiv 0>;
status = "okay";
// 自定义属性示例
fifo-size = <16>;
auto-flow-control;
};
// 10. 串口1 - 禁用状态示例
uart1: serial@3100 {
compatible = "ns16550a";
reg = <0x3100 0x100>;
interrupts = <1 4>;
interrupt-parent = <&intc>;
clocks = <&clkdiv 1>;
status = "disabled"; // 设备存在但被禁用
};
// 11. 以太网控制器
eth0: ethernet@4000 {
compatible = "acme,example-eth";
reg = <0x4000 0x1000>;
interrupts = <2 4>, <3 4>; // 多个中断
interrupt-parent = <&intc>;
clocks = <&clkdiv 2>;
status = "okay";
// 自定义属性示例
mac-address = [00 11 22 33 44 55];
phy-handle = <&phy0>;
phy-mode = "rgmii";
mdio {
#address-cells = <1>;
#size-cells = <0>;
phy0: ethernet-phy@0 {
reg = <0>;
// 自定义属性示例
max-speed = <1000>;
};
};
};
// 12. SDHCI控制器
sdhci0: mmc@5000 {
compatible = "acme,example-sdhci";
reg = <0x5000 0x100>;
interrupts = <4 4>;
interrupt-parent = <&intc>;
clocks = <&clkdiv 3>;
status = "okay";
// 自定义属性示例
bus-width = <4>;
cap-sd-highspeed;
cap-mmc-highspeed;
};
// 13. I2C控制器
i2c0: i2c@6000 {
compatible = "acme,example-i2c";
#address-cells = <1>;
#size-cells = <0>;
reg = <0x6000 0x100>;
interrupts = <5 4>;
interrupt-parent = <&intc>;
clocks = <&clkdiv 4>;
status = "okay";
clock-frequency = <100000>;
// 14. I2C设备 - EEPROM
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
// 自定义属性示例
pagesize = <8>;
};
// 15. I2C设备 - PMIC
pmic: pmic@60 {
compatible = "acme,example-pmic";
reg = <0x60>;
interrupts = <6 4>;
interrupt-parent = <&intc>;
// 16. 电压调节器
regulators {
vdd_cpu: cpu-reg {
regulator-name = "vdd_cpu";
regulator-min-microvolt = <800000>;
regulator-max-microvolt = <1350000>;
};
vdd_io: io-reg {
regulator-name = "vdd_io";
regulator-min-microvolt = <1800000>;
regulator-max-microvolt = <3300000>;
};
};
};
};
// 17. SPI控制器
spi0: spi@7000 {
compatible = "acme,example-spi";
#address-cells = <1>;
#size-cells = <0>;
reg = <0x7000 0x100>;
interrupts = <7 4>;
interrupt-parent = <&intc>;
clocks = <&clkdiv 5>;
status = "okay";
// 18. SPI设备 - Flash
flash@0 {
compatible = "jedec,spi-nor";
reg = <0>;
spi-max-frequency = <10000000>;
// 自定义属性示例
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
boot@0 {
label = "boot";
reg = <0x0 0x100000>;
};
rootfs@100000 {
label = "rootfs";
reg = <0x100000 0x700000>;
};
};
};
};
};
// 19. 外部总线上的设备
external-bus {
compatible = "simple-bus";
#address-cells = <2>; // 使用2个单元格表示地址(CS + Offset)
#size-cells = <1>;
ranges = <0 0 0x60000000 0x10000000 // CS0: 映射到0x60000000
1 0 0x70000000 0x10000000>; // CS1: 映射到0x70000000
// 20. FPGA设备在片选0上
fpga@0,0 {
compatible = "acme,example-fpga";
reg = <0 0 0x1000>; // CS0, 偏移0, 大小4KB
// 自定义属性示例
firmware-name = "fpga_image.bin";
};
};
// 21. GPIO LED示例
leds {
compatible = "gpio-leds";
led0 {
label = "heartbeat";
gpios = <&gpio 1 0>; // GPIO控制器1, 引脚0
linux,default-trigger = "heartbeat";
};
led1 {
label = "mmc0";
gpios = <&gpio 1 1>;
linux,default-trigger = "mmc0";
};
};
// 22. 保留内存区域
reserved-memory {
#address-cells = <1>;
#size-cells = <1>;
ranges;
// 为DMA引擎保留的内存
dma_reserved: dma@90000000 {
reg = <0x90000000 0x100000>; // 1MB保留区域
no-map; // 操作系统不应映射此区域
};
// 为DSP保留的内存
dsp_reserved: dsp@90100000 {
reg = <0x90100000 0x200000>; // 2MB保留区域
reusable; // 操作系统可重新使用此内存,但需先与DSP协商
};
};
};
// 23. 通过引用覆盖节点属性 - 通常在板级DTS文件中使用
&uart1 {
status = "okay"; // 启用第二个串口
};
&i2c0 {
// 24. 添加另一个I2C设备
temp-sensor@48 {
compatible = "ti,tmp75";
reg = <0x48>;
};
};
// 25. 自定义属性节点示例
&{/} {
my_custom_node {
compatible = "custom,node";
custom-string = "Hello Device Tree";
custom-number = <42>;
custom-array = <1 2 3 4>;
custom-byte-array = [01 02 03 04 DE AD BE EF];
custom-boolean; // 布尔属性,无值
custom-reference = <&uart0>;
custom-string-list = "first", "second", "third";
};
// 26. 板级特定配置
board-config {
serial-number = "EXMPL123456";
manufacturing-date = "2023-12-01";
hardware-revision = <2>;
};
};
二、Linux驱动设备树-中断-时钟-CPU
Linux驱动设备树-中断-时钟-CPU
设备树描述中断
在深入设备树描述中断之前,我们快速回顾几个关键概念:
-
- 中断源:产生中断信号的硬件设备,例如 GPIO 控制器、以太网控制器、定时器等。
-
- 中断控制器:负责管理和优先处理多个中断源的中断信号,并向 CPU 核心提交最终的中断请求。常见的有 GIC、NVIC 等。
-
- 中断信号类型:
-
•
边沿触发:在信号电平变化(上升沿或下降沿)时触发。 -
•
电平触发:在信号保持特定电平(高电平或低电平)期间持续触发。
设备树中的中断描述结构
设备树通过一系列属性来描述中断的连接关系。核心属性包括:
-
• interrupt-controller :这是一个空属性(无值),用于声明一个设备节点是一个中断控制器。任何能够向 CPU 传递中断的设备节点都必须包含此属性。
-
• #interrupt-cells :此属性位于中断控制器节点 内部。它定义了下游设备(即该控制器的中断使用者)需要使用多少个整数(cell) 来唯一指定一个中断。 例如,一个简单的中断控制器可能只需要一个 cell 来指定中断号,而一个复杂的控制器(如 GIC)可能需要2个或3个 cell,分别指定中断类型、中断号、触发标志等。
-
• interrupt-parent :此属性位于设备节点 (中断源)内部。它的值是一个phandle (指向另一个节点的句柄),用于指定该设备所连接的中断控制器是哪一个。如果一个节点没有定义
interrupt-parent,它会默认继承其父节点的interrupt-parent。 -
• interrupts :此属性位于设备节点 内部。它是一个由整数组成的列表,其格式和长度由该设备节点的
interrupt-parent所指向的中断控制器的#interrupt-cells属性决定。它精确地描述了设备连接到中断控制器的哪个输入以及中断的触发方式等。
示例
让我们通过一个假设的 SoC 平台来构建一个完整的例子。
SoC 硬件概况:
-
• CPU 核心:ARM Cortex-A53
-
• 主中断控制器:GIC-400
-
• 一个 GPIO 控制器,其本身会产生中断(例如,当 GPIO 引脚状态变化时),并连接到 GIC。
-
• 一个外部以太网控制器,通过 SPI 总线连接,其中断线连接到一个 GPIO 引脚。
第 1 步:描述主中断控制器(GIC)
首先,我们需要描述最顶层的中断控制器------GIC。
cpp
gic: interrupt-controller@fee00000 {
compatible = "arm,gic-400";
reg = <0x0 0xfee00000 0 0x10000>, // GICD(分配器)
<0x0 0xfef00000 0 0x20000>, // GICC(CPU接口)
<0x0 0xfff40000 0 0x20000>; // GICV(虚拟接口)
interrupt-controller; // 声明自己是中断控制器
#interrupt-cells = <3>; // 使用3个cell来描述一个中断
// 对于GIC,这3个cell通常对应:
// - cell 1: 中断类型 (0: SPI, 共享外设中断; 1: PPI, 私有外设中断; ...)
// - cell 2: 中断号 (例如,SPI 40, PPI 29)
// - cell 3: 触发标志 (1: 上升沿, 2: 下降沿, 4: 高电平, 8: 低电平, ...)
};
第2步:描述GPIO控制器(它既是中断使用者,也是中断控制器)
GPIO控制器需要从GIC接收中断(interrupt-parent)(例如,引脚变化),同时它也能为连接到其引脚上的设备提供中断服务。
cpp
// GPIO控制器节点
gpio0: gpio@1000000 {
compatible = "vendor,some-gpio-controller";
reg = <0x0 0x1000000 0 0x1000>;
interrupts = <GIC_SPI 16 IRQ_TYPE_LEVEL_HIGH>; // GPIO控制器本身的中断
interrupt-parent = <&gic>; // 它的中断父控制器是GIC
gpio-controller;
#gpio-cells = <2>;
// GPIO控制器本身也是一个中断控制器(为每个引脚提供中断)
interrupt-controller;
#interrupt-cells = <2>; // 对于这个GPIO中断控制器,需要2个cell
// - cell 1: GPIO引脚号
// - cell 2: 触发标志
};
interrupts = <GIC_SPI 16 IRQ_TYPE_LEVEL_HIGH>;: 这里使用了GIC的3-cell格式。GIC_SPI通常是0(表示共享外设中断),16是中断号,IRQ_TYPE_LEVEL_HIGH是触发标志(通常是4,表示高电平触发)。这些宏通常在.dtsi头文件中定义。
总结
| 属性 | 所在节点 | 作用 |
|---|---|---|
interrupt-controller |
中断控制器节点 | 声明"我是一个中断控制器" |
#interrupt-cells |
中断控制器节点 | 定义"描述我的一个中断需要几个参数" |
interrupt-parent |
设备节点(中断源) | 指定"我的中断线连接到哪个中断控制器" |
interrupts |
设备节点(中断源) | 描述"我连接到父控制器的哪个输入,以及如何触发" |
设备树描述时钟
在复杂的片上系统(SoC)中,各个功能模块(如CPU、总线、外设)通常需要以不同的频率工作。时钟系统就如同整个SoC的"脉搏",为这些模块提供节拍。设备树(Device Tree)的核心作用是为操作系统提供静态的硬件描述,而时钟拓扑结构正是其中至关重要的一环。
一、时钟的基本概念
在深入设备树之前,先了解几个关键概念:
-
- 时钟源:最原始的时钟信号来源,通常是外部晶振或内部RC振荡器,提供固定频率(如24MHz)。
-
- PLL:锁相环,用于倍频。它可以将低频的时钟源倍频到CPU、总线等需要的高频。
-
- 分频器:将高频时钟进行分频,得到较低频率的时钟。
-
- 门控时钟:一种开关,可以开启或关闭流向某个功能模块的时钟,是低功耗管理的关键。
-
- 多路选择器:可以从多个时钟源中选择一个输出给后续电路。
一个典型的时钟路径可能是:晶振 → PLL → 分频器 → 门控 → 外设。
二、设备树中的时钟描述结构
设备树通过两类节点来描述时钟:时钟提供者 和 时钟使用者。
A. 时钟提供者
时钟提供者是指能够产生和分配时钟信号的硬件模块,如晶振、PLL(压控振荡器)、分频器等。它们通过以下属性进行描述:
-
- #clock-cells :这是时钟提供者节点 的核心属性,定义了唯一指定该提供者输出的一个时钟所需的具体参数数量。
-
•
#clock-cells = <0>:表示该提供者只输出一个时钟,不需要参数来区分。例如,一个固定的晶振。 -
•
#clock-cells = <1>:表示该提供者可以输出多个时钟,需要一个参数(通常是索引号)来指定具体是哪一个。例如,一个时钟控制器可以管理多个PLL和分频器的输出。
-
- clock-output-names:(可选)为输出时钟提供名称,便于调试和阅读。
-
- clocks :对于非原始时钟源(如PLL、分频器),需要此属性来指定其输入时钟的来源。其值是一个指向其父时钟提供者的phandle,可能还需要参数。
B. 时钟使用者
时钟使用者是指需要时钟信号的功能模块,如UART、I2C控制器、USB控制器等。它们通过以下属性进行描述:
-
- clocks :这是一个列表,包含该设备所需的所有时钟的phandle。如果时钟提供者的
#clock-cells不为0,则phandle后还需要跟上相应的参数。
- • 例如:
clocks = <&clkctl 10>, <&xtal>;表示该设备需要两个时钟,一个来自clkctl控制器的第10号输出,另一个来自晶振xtal。
- clocks :这是一个列表,包含该设备所需的所有时钟的phandle。如果时钟提供者的
-
- clock-names :(强烈推荐使用)为
clocks属性中的每个时钟指定一个名称。驱动代码可以通过名称(如"baudclk","apb_pclk")来获取对应的时钟,而不是依赖容易出错的顺序索引。
- • 例如:
clock-names = "baudclk", "apb_pclk";
- clock-names :(强烈推荐使用)为
-
- assigned-clocks 、
assigned-clock-rates、assigned-clock-parents:这些属性用于指定启动时应设置的固定时钟配置。Bootloader或内核时钟框架会根据这些配置来初始化时钟树。
-
•
assigned-clocks:列出需要配置的时钟(phandle + 参数)。 -
•
assigned-clock-rates:为assigned-clocks中列出的时钟指定期望的频率(单位Hz)。 -
•
assigned-clock-parents:为assigned-clocks中列出的时钟指定父时钟。
- assigned-clocks 、
示例
假设一个SoC有以下时钟组件:
-
• 一个24MHz外部晶振。
-
• 一个PLL(PLL1),可以将输入时钟倍频。
-
• 一个分频器,将PLL1的输出进行分频。
-
• 一个UART控制器,需要工作时钟。
设备树源码:
cpp
/* 1. 定义最原始的时钟源:24MHz晶振 */
&xtal_24m {
compatible = "fixed-clock"; // 固定频率时钟
#clock-cells = <0>; // 只输出一个时钟,无需参数
clock-frequency = <24000000>;
clock-output-names = "xtal_24m";
};
/* 2. 定义主时钟控制器(包含PLL、分频器等) */
&clk_ctrl: clock-controller@10000000 {
compatible = "vendor,some-clock-controller";
reg = <0x10000000 0x1000>; // 控制寄存器地址
#clock-cells = <1>; // 输出多个时钟,需要1个参数(索引)来指定
clocks = <&xtal_24m>; // 本控制器的输入时钟是24M晶振
clock-names = "refclk";
/* 可选:在控制器内部定义输出时钟的名称(方便引用) */
clock-output-names = "pll1", "pll1_div2", "cpu_clk", "uart_clk_sel";
};
/* 3. 定义UART控制器(时钟使用者) */
&uart0: serial@20000000 {
compatible = "vendor,some-uart";
reg = <0x20000000 0x1000>;
interrupts = <GIC_SPI 50 IRQ_TYPE_LEVEL_HIGH>;
/* 关键:描述UART所需的时钟 */
clocks = <&clk_ctrl 15>; // 使用时钟控制器的第15号时钟输出
clock-names = "baudclk"; // 命名为"baudclk",驱动中会按名查找
/* 可选但重要:指定UART时钟的固定配置 */
assigned-clocks = <&clk_ctrl 15>; // 我们要配置第15号时钟
assigned-clock-parents = <&clk_ctrl 3>; // 将其父时钟设置为控制器的第3号输出(假设是PLL1的分频)
assigned-clock-rates = <115200 * 16>; // 将其频率设置为115200 * 16 Hz(UART常用)
};
关键点解析:
-
• 时钟索引(15, 3) : 这些索引号(
#clock-cells = <1>中的参数)的具体含义完全依赖于SoC的数据手册和时钟控制器的驱动实现。索引3可能对应PLL1的分频输出,索引15可能对应UART的时钟多路选择器。驱动和文档必须明确定义这些索引。 -
• 时钟配置流程:
-
- Bootloader或内核初始化
xtal_24m。
- Bootloader或内核初始化
-
- 初始化
clk_ctrl,配置PLL1等。
- 初始化
-
- 处理UART0的
assigned-clocks属性:将clk_ctrl的第15号时钟的父源设为第3号时钟,并将其频率设置为1843200 Hz。
- 处理UART0的
-
-
• 驱动使用 : UART驱动在 probe 函数中,会执行类似如下的操作:
struct clk *baudclk; baudclk = devm_clk_get(&pdev->dev, "baudclk"); // 通过名字"baudclk"获取时钟句柄 clk_prepare_enable(baudclk); // 准备并开启时钟
特殊类型的时钟提供者
-
- 固定时钟 : 如上例的晶振,使用
fixed-clock兼容性。
- 固定时钟 : 如上例的晶振,使用
-
- 固定分频器时钟 : 使用
fixed-factor-clock。
cpppll1_div2: pll1-div2 { compatible = "fixed-factor-clock"; #clock-cells = <0>; clocks = <&clk_ctrl 1>; // 输入是PLL1 clock-div = <2>; // 分频比 clock-mult = <1>; // 倍频比(通常为1) // 输出频率 = (输入频率 * clock-mult) / clock-div }; - 固定分频器时钟 : 使用
总结
| 属性 | 适用节点 | 作用 |
|---|---|---|
#clock-cells |
时钟提供者 | 定义标识一个输出时钟所需的参数个数 |
clocks |
提供者/使用者 | 提供者:声明输入时钟;使用者:声明所需时钟 |
clock-names |
提供者/使用者 | 为clocks中的时钟命名 |
assigned-clocks |
时钟使用者 | 列出需要固件配置的时钟 |
assigned-clock-rates |
时钟使用者 | 为列出的时钟指定频率 |
assigned-clock-parents |
时钟使用者 | 为列出的时钟指定父源 |
设备树描述CPU
cpu的层次结构是通过不同的节点来描述系统中物理cpu的布局
节点详解:
-
cpu-map节点
功能: 作为描述 CPU 物理拓扑结构的专用容器,超越简单的线性 CPU 列表,表达 CPU 的分组(集群、核心、线程)和物理位置(插槽)。
父节点: :必须是
cpus节点。
子节点: 必须包含一个或多个socket节点(描述物理插槽)或cluster节点(如果系统只有一个物理插槽,或者插槽内结构是描述的重点)。 -
socket节点
功能: 代表主板上的一个独立的物理 CPU 插槽。一个物理 CPU 封装(可能是一个独立的 CPU 芯片,或一个包含多个核心集群的 SoC 芯片)就安装在一个
socket中。数量: 系统中物理 CPU 插槽的数量决定了
socket节点的数量(1 个插槽 = 1 个socket节点)。父节点: 必须是
cpu-map节点。
命名: 必须遵循socket<N>的格式,其中N是从 0 开始的整数(例如socket0,socket1)。子节点: 必须包含一个或多个
cluster节点。一个物理封装(socket)内部可以包含多个核心集群(cluster)。 -
cluster节点
功能: 代表一组共享特定资源或具有相同微架构/性能特性的 CPU 核心。这是描述大小核(HMP)架构的关键层级。在大小核中:通常有一个
cluster包含所有"大"核(高性能核心),另一个cluster包含所有"小"核(高能效核心)。在同构多核中:一个物理封装(
socket)内的所有核心可能属于一个cluster,或者根据缓存共享情况(如 L2 缓存簇)划分为多个cluster。位置: :父节必须是
socket节点(或直接是cpu-map节点,如果省略socket且只有一个物理封装)。命名: 必须 遵循
cluster<N>的格式,其中N是从 0 开始的整数(例如cluster0,cluster1)。子节点: 必须包含一个或多个
core节点。一个集群(cluster)包含多个物理核心(core)。 -
core节点
功能: 代表一个独立的物理 CPU 核心(Processing Unit)。这是实际执行指令的硬件单元。
**位置:父节点必须是
cluster节点。命名 :必须 遵循
core<N>的格式,其中N是从 0 开始的整数(例如core0,core1)。子节点: 可能包含一个或多个
thread节点(如果该物理核心支持 SMT/超线程技术)。如果不支持 SMT,则core节点通常直接关联到一个逻辑 CPU(cpu节点)。 -
thread节点
功能 : 代表一个物理核心(
core)通过同步多线程技术(SMT,如 Intel Hyper-Threading, AMD SMT)虚拟出来的逻辑处理器。一个物理核心可以支持多个硬件线程。位置: :父节点必须是
core节点。命名 :必须遵循
thread<N>的格式,其中N是从 0 开始的整数(例如thread0,thread1)。
完整层级结构示例
cpus (根节点)
├── cpu@0 (逻辑 CPU 0)
├── cpu@1 (逻辑 CPU 1)
├── cpu@2 (逻辑 CPU 2)
├── ... (更多逻辑 CPU 节点)
└── cpu-map (CPU 拓扑映射节点)
├── socket0 (物理插槽 0)
│ ├── cluster0 (大核集群)
│ │ ├── core0 (物理大核 0)
│ │ │ ├── thread0 -> cpu@0 (逻辑 CPU 0)
│ │ │ └── thread1 -> cpu@1 (逻辑 CPU 1)
│ │ └── core1 (物理大核 1)
│ │ ├── thread0 -> cpu@2 (逻辑 CPU 2)
│ │ └── thread1 -> cpu@3 (逻辑 CPU 3)
│ └── cluster1 (小核集群)
│ ├── core0 (物理小核 0)
│ │ ├── thread0 -> cpu@4 (逻辑 CPU 4)
│ │ └── thread1 -> cpu@5 (逻辑 CPU 5)
│ └── core1 (物理小核 1)
│ ├── thread0 -> cpu@6 (逻辑 CPU 6)
│ └── thread1 -> cpu@7 (逻辑 CPU 7)
└── socket1 (物理插槽 1)
├── cluster0 (大核集群)
│ ├── core0 (物理大核 0)
│ │ ├── thread0 -> cpu@8 (逻辑 CPU 8)
│ │ └── thread1 -> cpu@9 (逻辑 CPU 9)
│ └── core1 (物理大核 1)
│ ├── thread0 -> cpu@10 (逻辑 CPU 10)
│ └── thread1 -> cpu@11 (逻辑 CPU 11)
└── cluster1 (小核集群)
├── core0 (物理小核 0)
│ ├── thread0 -> cpu@12 (逻辑 CPU 12)
│ └── thread1 -> cpu@13 (逻辑 CPU 13)
└── core1 (物理小核 1)
├── thread0 -> cpu@14 (逻辑 CPU 14)
└── thread1 -> cpu@15 (逻辑 CPU 15)
总结与关键关系:
-
•
cpu-map是起点: 容纳整个物理拓扑描述。 -
•
socket定义物理位置: 对应主板插槽/物理封装,是 NUMA 的基础。 -
•
cluster定义核心类型/组: 关键于区分大小核或缓存域。 -
•
core是物理执行单元: 真正的硬件核心。 -
•
thread是逻辑视图 (SMT): 一个物理核心 (core) 可以暴露多个逻辑 CPU (thread->cpu节点)。 -
•
cpu节点是叶子: 位于cpus节点下,代表操作系统调度的最小单位(逻辑 CPU)。它们通过属性被thread或core节点引用。 -
• 层次结构:
cpu-map->socket->cluster->core->thread-> (引用)cpu。
例子
双核Cortex-A72 + 四核Cortex-A53这种典型big.LITTLE架构的CPU设备树结构。
cpp
// 设备树片段
cpus {
#address-cells = <2>;
#size-cells = <0>;
// 逻辑CPU节点 - A53小核集群
cpu_l0: cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a53";
reg = <0x0 0x0>;
enable-method = "psci";
capacity-dmips-mhz = <362>;
clocks = <&cru ARMCLKL>;
operating-points-v2 = <&cluster1_opp>;
cpu-idle-states = <&CPU_SLEEP>;
next-level-cache = <&l2>;
};
cpu_l1: cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-a53";
reg = <0x0 0x1>;
enable-method = "psci";
capacity-dmips-mhz = <362>;
clocks = <&cru ARMCLKL>;
operating-points-v2 = <&cluster1_opp>;
cpu-idle-states = <&CPU_SLEEP>;
next-level-cache = <&l2>;
};
cpu_l2: cpu@2 {
device_type = "cpu";
compatible = "arm,cortex-a53";
reg = <0x0 0x2>;
enable-method = "psci";
capacity-dmips-mhz = <362>;
clocks = <&cru ARMCLKL>;
operating-points-v2 = <&cluster1_opp>;
cpu-idle-states = <&CPU_SLEEP>;
next-level-cache = <&l2>;
};
cpu_l3: cpu@3 {
device_type = "cpu";
compatible = "arm,cortex-a53";
reg = <0x0 0x3>;
enable-method = "psci";
capacity-dmips-mhz = <362>;
clocks = <&cru ARMCLKL>;
operating-points-v2 = <&cluster1_opp>;
cpu-idle-states = <&CPU_SLEEP>;
next-level-cache = <&l2>;
};
// 逻辑CPU节点 - A72大核集群
cpu_b0: cpu@100 {
device_type = "cpu";
compatible = "arm,cortex-a72";
reg = <0x0 0x100>;
enable-method = "psci";
capacity-dmips-mhz = <1024>;
clocks = <&cru ARMCLKB>;
operating-points-v2 = <&cluster0_opp>;
cpu-idle-states = <&CPU_SLEEP>;
next-level-cache = <&l2>;
};
cpu_b1: cpu@101 {
device_type = "cpu";
compatible = "arm,cortex-a72";
reg = <0x0 0x101>;
enable-method = "psci";
capacity-dmips-mhz = <1024>;
clocks = <&cru ARMCLKB>;
operating-points-v2 = <&cluster0_opp>;
cpu-idle-states = <&CPU_SLEEP>;
next-level-cache = <&l2>;
};
// CPU拓扑映射
cpu-map {
socket0: socket {
cluster0: cluster0 {
core0: core0 {
cpu = <&cpu_b0>;
};
core1: core1 {
cpu = <&cpu_b1>;
};
};
cluster1: cluster1 {
core0: core0 {
cpu = <&cpu_l0>;
};
core1: core1 {
cpu = <&cpu_l1>;
};
core2: core2 {
cpu = <&cpu_l2>;
};
core3: core3 {
cpu = <&cpu_l3>;
};
};
};
};
// 空闲状态定义
idle-states {
entry-method = "psci";
CPU_SLEEP: cpu-sleep {
compatible = "arm,idle-state";
local-timer-stop;
arm,psci-suspend-param = <0x0010000>;
entry-latency-us = <120>;
exit-latency-us = <250>;
min-residency-us = <900>;
};
};
// L2缓存定义
l2: l2-cache0 {
compatible = "cache";
};
};
三、Linux驱动设备树-GPIO
基本属性
控制器与客户端
在设备树中描述 GPIO 时,需要理解两个核心角色:
GPIO 控制器:是指提供通用输入输出(GPIO)功能的硬件模块,例如 SoC 内部集成的 GPIO 控制器,或者通过 I2C/SPI 接口扩展的 GPIO 芯片(如 PCA9535、TCA9554 等)。它的设备节点负责声明"我这里可以提供一组 GPIO"。
GPIO 客户端:是指使用 GPIO 的设备,例如 LED 灯、按键或触摸屏的复位引脚。它们的设备节点需要引用 GPIO 控制器提供的特定 GPIO。
控制器节点的必须属性
一个典型的 GPIO 控制器节点如下所示(以 NXP i.MX6ULL 的 GPIO1 控制器为例):
cpp
// 在 .dtsi 文件中定义的 SoC 级 GPIO 控制器
soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
ranges;
gpio1: gpio@0209c000 {
compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
reg = <0x0209c000 0x4000>; // 控制器的内存映射地址和长度
interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
gpio-controller; // 声明本节点是一个 GPIO 控制器
#gpio-cells = <2>; // 指定客户端节点中每个 GPIO 标识符由几个 cell 组成
interrupt-controller; // 声明本节点也是一个中断控制器(因为 GPIO 可用于中断)
#interrupt-cells = <2>; // 指定 GPIO 中断描述符由几个 cell 组成
};
};
gpio-controller 是一个空属性,其存在表明该设备节点是一个 GPIO 控制器。
#gpio-cells : 这是一个整数,表示客户端节点在引用此控制器时,需要提供多少个 32 位数值(即 cells)来描述一个 GPIO。最常见的值是 <2>。
-
•
第一个 cell: 通常是 GPIO 编号(偏移量),例如<0>表示控制器的第一个 GPIO。 -
•
第二个 cell: 通常是 GPIO 标志,用于指定初始电平、输入输出方向等。-
• GPIO_ACTIVE_HIGH:
0,表示高电平有效。 -
• GPIO_ACTIVE_LOW:
1,表示低电平有效。 -
• 还可以包含方向控制:,如
GPIO_OUTPUT_LOW、GPIO_INPUT等(具体取决于内核版本和驱动实现)。
-
GPIO 客户端节点的属性
// 在板级 .dts 文件中
/ {
leds {
compatible = "gpio-leds";
status-led {
label = "heartbeat";
gpios = <&gpio1 5 GPIO_ACTIVE_HIGH>; // 使用 gpio1 控制器的第 5 号引脚,高电平点亮
linux,default-trigger = "heartbeat";
default-state = "off";
};
};
};
&gpio1: 引用了上面定义的 gpio1 控制器的 phandle。
5: GPIO 编号,即该控制器上的第 5 号引脚。
GPIO_ACTIVE_HIGH: 标志位,表示"有效状态"为高电平。对于 LED 来说,当提供高电平信号时,LED 将亮起。
关键属性说明
| #gpio-cells | 含义 | 客户端 gpios 属性格式 | 典型场景 | 驱动配置复杂度 |
|---|---|---|---|---|
| 0 | 无需参数 | <&controller_phandle> |
控制器只提供一个逻辑 GPIO | 低 |
| 1 | 需要引脚号 (arg0) | <&controller_phandle pin> |
简单/旧控制器,无复杂配置 | 高 |
| 2 | 需要引脚号 (arg0) + 标志位 (arg1) | <&controller_phandle pin flags> |
现代标准,支持丰富配置(电平、方向等) | 低 |
场景 1:#gpio-cells = <0>;
控制器节点示例
cpp
dummy_gpio: dummy-gpio-controller {
compatible = "vendor,dummy-gpio";
gpio-controller;
#gpio-cells = <0>; // 客户端引用时不需要参数
// 可能还有其他属性定义了这个"唯一"GPIO 的行为
};
客户端节点示例
my_device {
compatible = "vendor,my-device";
enable-gpios = <&dummy_gpio>; // 仅引用 phandle,没有参数
};
场景 2:#gpio-cells = <1>;
控制器节点示例
cpp
gpio0: gpio@10000000 {
compatible = "snps,dw-apb-gpio"; // 例如 DesignWare GPIO
reg = <0x10000000 0x1000>;
gpio-controller;
#gpio-cells = <1>; // 客户端引用时只需要引脚号
ngpios = <32>; // 该控制器有 32 个 GPIO
};
客户端节点示例
led0 {
compatible = "gpio-leds";
label = "status_led";
gpios = <&gpio0 5>; // 使用 gpio0 控制器的第 5 号引脚
// 注意:没有指定有效电平!驱动可能需要硬编码或从其他地方推断
};
场景 3:#gpio-cells = <2>;(最常见)
控制器节点示例
cpp
gpio1: gpio@20000000 {
compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio"; // 例如 NXP i.MX
reg = <0x20000000 0x4000>;
interrupts = ...;
gpio-controller;
#gpio-cells = <2>; // 客户端引用时需要引脚号和标志位
};
客户端节点示例
power_button {
compatible = "gpio-keys";
label = "Power Button";
gpios = <&gpio1 18 GPIO_ACTIVE_LOW>; // 引脚 18,低电平有效(按下时拉低)
linux,code = <KEY_POWER>;
};
status_led {
compatible = "gpio-leds";
label = "heartbeat";
gpios = <&gpio1 5 GPIO_ACTIVE_HIGH>; // 引脚 5,高电平有效(高电平点亮)
linux,default-trigger = "heartbeat";
default-state = "off";
};
i2c_gpio_sda {
// 假设使用 GPIO 模拟 I2C (bitbanging)
gpios = <&gpio1 10 (GPIO_ACTIVE_HIGH | GPIO_OPEN_DRAIN)>; // 引脚 10,高电平有效,开漏输出
};
例子(驱动与设备树交互)
设备树
cpp
/ {
compatible = "fsl,imx8mm";
// GPIO 控制器定义 (由 SoC 提供)
soc {
#address-cells = <1>;
#size-cells = <1>;
gpio1: gpio@30200000 {
compatible = "fsl,imx8mm-gpio", "fsl,imx35-gpio";
reg = <0x30200000 0x10000>;
interrupts = <GIC_SPI 64 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 65 IRQ_TYPE_LEVEL_HIGH>;
gpio-controller; // 声明这是 GPIO 控制器
#gpio-cells = <2>; // 每个 GPIO 说明符有 2 个 cell
interrupt-controller;
#interrupt-cells = <2>;
};
gpio4: gpio@30230000 {
compatible = "fsl,imx8mm-gpio", "fsl,imx35-gpio";
reg = <0x30230000 0x10000>;
interrupts = <GIC_SPI 70 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 71 IRQ_TYPE_LEVEL_HIGH>;
gpio-controller;
#gpio-cells = <2>;
};
};
// GPIO 客户端节点
simple_gpio_device {
compatible = "vendor,gpio-simple";
status = "okay";
// 使用 gpio1 的第 5 个引脚
led-gpio = <&gpio1 5 GPIO_ACTIVE_HIGH>;
// 使用 gpio4 的第 10 个引脚
button-gpio = <&gpio4 10 GPIO_ACTIVE_LOW>;
};
};
驱动代码
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
struct gpio_data {
struct gpio_desc *led_gpio;
struct gpio_desc *button_gpio;
};
static int gpio_simple_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct gpio_data *data;
int button_state;
data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
// 从设备树获取LED GPIO
data->led_gpio = devm_gpiod_get(dev, "led", GPIOD_OUT_LOW);
if (IS_ERR(data->led_gpio)) {
dev_err(dev, "Failed to get LED GPIO\n");
return PTR_ERR(data->led_gpio);
}
// 配置LED GPIO为输出并设置初始值
gpiod_direction_output(data->led_gpio, 0);
gpiod_set_value(data->led_gpio, 1); // 点亮LED
// 从设备树获取按钮 GPIO
data->button_gpio = devm_gpiod_get(dev, "button", GPIOD_IN);
if (IS_ERR(data->button_gpio)) {
dev_err(dev, "Failed to get button GPIO\n");
return PTR_ERR(data->button_gpio);
}
// 读取按钮状态
button_state = gpiod_get_value(data->button_gpio);
dev_info(dev, "Button state: %d\n", button_state);
platform_set_drvdata(pdev, data);
return 0;
}
static const struct of_device_id gpio_simple_of_match[] = {
{ .compatible = "vendor,gpio-simple" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, gpio_simple_of_match);
static struct platform_driver gpio_simple_driver = {
.driver = {
.name = "gpio-simple",
.of_match_table = gpio_simple_of_match,
},
.probe = gpio_simple_probe,
};
module_platform_driver(gpio_simple_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Simple GPIO Driver for NXP Platform");
MODULE_AUTHOR("XSX");
引脚复用与 pinctrl子系统
pinctrl(Pin Control)是 Linux 内核中用于管理芯片引脚复用和配置的子系统。在现代 SoC(系统级芯片)中,一个物理引脚往往可以复用为多种功能,例如 GPIO、I2C、SPI、UART 等。pinctrl 子系统的主要职责包括:
-
• 引脚复用:将引脚配置为特定功能(如 GPIO、外设等)。
-
• 电气特性配置:设置引脚的上下拉电阻、驱动强度、斜率控制等。
-
• 引脚状态管理:管理设备在不同电源状态下的引脚配置。
语法示例
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_hog_1>;
状态定义
pinctrl-names = "default" 定义了一个名为 default 的状态,这是设备正常工作时的默认引脚配置状态。
配置映射
pinctrl-0 对应第 0 个状态(即 default 状态),该状态使用 pinctrl_hog_1 节点中定义的引脚配置。
实际应用场景(例子1)
/* 引脚配置组定义 */
pinctrl_hog_1: hoggrp-1 {
fsl,pins = <
MX6UL_PAD_GPIO1_IO00__GPIO1_IO00 0x80000000 /* 引脚1配置 */
MX6UL_PAD_GPIO1_IO01__GPIO1_IO01 0x80000000 /* 引脚2配置 */
>;
};
/* 设备节点使用 */
&usdhc1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_hog_1>;
status = "okay";
};
多状态配置(例2)
/* 默认状态引脚配置 */
pinctrl_hog_1: default_grp {
fsl,pins = <
MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX 0x000010B0
MX6UL_PAD_UART1_RX_DATA__UART1_DCE_RX 0x000010B0
>;
};
/* 唤醒状态引脚配置 */
pinctrl_hog_2: wakeup_grp {
fsl,pins = <
MX6UL_PAD_UART1_TX_DATA__GPIO1_IO16 0x000010B0 /* 默认是UART1_TX复用为GPIO1中的IO16 0x000010B0 是电气属性:例如IO 速度、上下拉等*/
MX6UL_PAD_UART1_RX_DATA__GPIO1_IO17 0x000010B0 /* 默认是UART1_RX复用为GPIO1中的IO17 0x000010B0 是电气属性:例如IO 速度、上下拉等*/
>;
};
/* 设备节点 - 支持状态切换 */
&uart1 {
pinctrl-names = "default", "wake_up";
pinctrl-0 = <&pinctrl_hog_1>; /* 正常工作时为UART功能 */
pinctrl-1 = <&pinctrl_hog_2>; /* 唤醒时配置为GPIO功能 */
status = "okay";
};
多状态定义
定义了两种状态:"default"(默认状态)和 "wake_up"(唤醒状态)。
-
• "default" → 索引 0
-
• "wake_up" → 索引 1
状态映射关系
-
• pinctrl-0 → pinctrl_hog_1 → "default" 状态
-
• pinctrl-1 → pinctrl_hog_2 → "wake_up" 状态
例子(GPIO复用)
设备树
cpp
/ {
compatible = "fsl,imx8mm";
// GPIO 控制器定义 (由 SoC 提供)
soc {
#address-cells = <1>;
#size-cells = <1>;
gpio1: gpio@30200000 {
compatible = "fsl,imx8mm-gpio", "fsl,imx35-gpio";
reg = <0x30200000 0x10000>;
interrupts = <GIC_SPI 64 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 65 IRQ_TYPE_LEVEL_HIGH>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};
gpio4: gpio@30230000 {
compatible = "fsl,imx8mm-gpio", "fsl,imx35-gpio";
reg = <0x30230000 0x10000>;
interrupts = <GIC_SPI 70 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 71 IRQ_TYPE_LEVEL_HIGH>;
gpio-controller;
#gpio-cells = <2>;
};
// 引脚控制器
iomuxc: pinctrl@30330000 {
compatible = "fsl,imx8mm-iomuxc";
reg = <0x30330000 0x10000>;
};
};
// 有复用 GPIO 客户端节点
muxed_gpio_device {
compatible = "vendor,gpio-muxed";
status = "okay";
// 引脚复用配置
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_gpio_mux>;
// 使用 gpio1 的第 15 个引脚 (需要复用)
led-gpio = <&gpio1 15 GPIO_ACTIVE_HIGH>;
// 使用 gpio4 的第 20 个引脚 (需要复用)
button-gpio = <&gpio4 20 GPIO_ACTIVE_LOW>;
};
};
// 引脚复用配置
&iomuxc {
pinctrl_gpio_mux: gpiomuxgrp {
fsl,pins = <
// 将 SD2_RESET_B 引脚复用为 GPIO1_IO15
MX8MM_IOMUXC_SD2_RESET_B_GPIO1_IO15 0x41
// 将 SAIL5_RX 引脚复用为 GPIO4_IO20
MX8MM_IOMUXC_SAIL5_RX_GPIO4_IO20 0x41
>;
};
};
驱动代码
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <linux/pinctrl/consumer.h>
struct muxed_gpio_data {
struct gpio_desc *led_gpio;
struct gpio_desc *button_gpio;
struct pinctrl *pinctrl;
struct pinctrl_state *pinctrl_state;
};
static int gpio_muxed_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct muxed_gpio_data *data;
int button_state;
int ret;
data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
// 获取并配置引脚复用
data->pinctrl = devm_pinctrl_get(dev);
if (IS_ERR(data->pinctrl)) {
dev_err(dev, "Failed to get pinctrl\n");
return PTR_ERR(data->pinctrl);
}
data->pinctrl_state = pinctrl_lookup_state(data->pinctrl, "default");
if (IS_ERR(data->pinctrl_state)) {
dev_err(dev, "Failed to lookup pinctrl state\n");
return PTR_ERR(data->pinctrl_state);
}
ret = pinctrl_select_state(data->pinctrl, data->pinctrl_state);
if (ret) {
dev_err(dev, "Failed to select pinctrl state\n");
return ret;
}
// 从设备树获取LED GPIO
data->led_gpio = devm_gpiod_get(dev, "led", GPIOD_OUT_LOW);
if (IS_ERR(data->led_gpio)) {
dev_err(dev, "Failed to get LED GPIO\n");
return PTR_ERR(data->led_gpio);
}
// 配置LED GPIO为输出并设置初始值
gpiod_direction_output(data->led_gpio, 0);
gpiod_set_value(data->led_gpio, 1); // 点亮LED
// 从设备树获取按钮 GPIO
data->button_gpio = devm_gpiod_get(dev, "button", GPIOD_IN);
if (IS_ERR(data->button_gpio)) {
dev_err(dev, "Failed to get button GPIO\n");
return PTR_ERR(data->button_gpio);
}
// 读取按钮状态
button_state = gpiod_get_value(data->button_gpio);
dev_info(dev, "Button state: %d\n", button_state);
platform_set_drvdata(pdev, data);
return 0;
}
static const struct of_device_id gpio_muxed_of_match[] = {
{ .compatible = "vendor,gpio-muxed" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, gpio_muxed_of_match);
static struct platform_driver gpio_muxed_driver = {
.driver = {
.name = "gpio-muxed",
.of_match_table = gpio_muxed_of_match,
},
.probe = gpio_muxed_probe,
};
module_platform_driver(gpio_muxed_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Muxed GPIO Driver for NXP Platform");
MODULE_AUTHOR("XSX");
四、Linux设备节点与平台总线
前提
前面的分析中我们知道,设备树文件最初的目的就是为了代替平台总线中的platform中的device的部份,那么设备树的dts 文件就必须在内核其中后传递给内核,那设备树是如何传递给内核?

编译流程
编译:DTC工具将dts 设备树文本文件编译为二进制dtb文件这种二进制文件是机器可读的格式,包含所有硬件信息的结构化数据。
二进制文件被加载到内存
从 .dtb 文件开始,根据不同的系统设计,主要有两种路径将其加载到内存中:
-
• 路径一:将.dtb 文件在构建内核时直接打包进内核镜像文件中。这样,设备树就和内核本身成为一个整体文件。
-
• 路径二:.dtb 文件独立于内核镜像文件存在。这是一种更通用和灵活的方式。
无论采用哪种路径,接下来的关键步骤都由 Bootloader 完成。Bootloader 的职责是将 内核(无论是包含了内嵌DTB的内核,还是独立的内核文件)和 设备树二进制文件(如果是独立存在的) 一同加载到系统内存的特定地址上。Bootloader 会告知内核设备树在内存中的位置。
展开:内核解析并构建设备树
当内核开始初始化时,它会根据 Bootloader 提供的信息,找到内存中的 .dtb 文件。内核会将 .dtb 文件中的二进制数据展开(解析)成其内部可以识别和操作的数据结构。
设备节点
设备节点结构体
cpp
struct device_node {
const char *name; /* 节点名,对应设备树中节点名称的第一部分(如 i2c1)*/
const char *type; /* 设备类型,通常对应设备树中的 device_type 属性 */
phandle phandle; /* 节点的唯一标识符 */
const char *full_name; /* 节点全名,如 i2c@40013000 */
struct property *properties; /* 指向属性链表的头指针 */
struct property *deadprops; /* 已删除的属性列表 */
struct device_node *parent; /* 指向父节点的指针 */
struct device_node *child; /* 指向第一个子节点的指针 */
struct device_node *sibling; /* 指向下一个兄弟节点的指针 */
#if defined(CONFIG_OF_KOBJ)
struct kobject kobj; /* 内核对象(用于 sysfs) */
#endif
unsigned long _flags; /* 标志位 */
void *data; /* 设备驱动私有数据指针 */
#if defined(CONFIG_SPARC)
/* ... SPARC 架构特定字段 ... */
#endif
};
内核在解析 .dtb文件后,所创建的内部数据结构核心就是 struct device_node。每一个设备树节点都会被转换为一个 device_node结构。如上代码结构体。device_node的结构体具体说明位于include/linux/of.h的路径下。其中上面说的到struct property *properties;指向属性链表的头指针 其机构体原型如下
属性结构体
cpp
struct property {
char *name; /* 属性名称 */
int length; /* 属性值的长度(字节数) */
void *value; /* 指向属性值数据的指针 */
struct property *next; /* 指向下一个属性的指针 */
unsigned long _flags; /* 内部使用的标志位 */
unsigned int unique_id; /* 唯一标识符 */
/* 以下字段用于支持 sysfs 接口 */
#if defined(CONFIG_OF_KOBJ)
struct bin_attribute attr; /* 二进制属性,用于 sysfs */
#endif
};
不难发现当前节点属性都是用一个单线链表给串联起来,整体的数据结构如下图
cpp
device_node
|
+-- properties (指向第一个属性)
| |
| +-- property #1: name = "compatible", value = "vendor,device"
| |
| +-- property #2: name = "reg", value = <0x1000 0x100>
| |
| +-- property #3: name = "interrupts", value = <0 10 4>
| |
| +-- ... (更多属性)
|
+-- parent, child, sibling (树形结构指针)
例子
cpp
/dts-v1/;
/ {
model = "This is my devicetree!";
#address-cells = <1>;
#size-cells = <1>;
chosen {
bootargs = "root=/dev/nfs ...";
};
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 = <0 1 2 3 4>;
};
gpio@22020101 {
compatible = "led";
reg = <0x20220101 0x40>;
status = "okay";
};
};
};
};
Linux内核中,上面的dts 文件中的节点会被转换为如下图device_node结构体关系

platform_device
上面提到dts最终会在内核中转换为设备节点(device_node)那么是否代表内核就能直接使用device_node 了么?显示不是。
设备树最初设计的目的是什么,在Linux内核驱动分析(13)文章中我们提到了platform平台总线的概念,其中最关键就是platform_device与platform_driver,platform_driver是对硬件的操作如(读写)而platform_device就是对硬件进行描述(如GPIO 的输入/输出、高低电平、IO速度、中断、复用等)
设备树替换了平台总线模型当中对硬件资源描述的device部分。所以设备树也是对硬件资源进行描述的文件。
在平台总线模型中,device部分是用platform device结构体来描述硬件资源的。所以内核最终会将内核认识的device node树转换platform device。
device node特换plattorm device
并不是所有的节点都会被转换成platform device,只有满足要求的才会转换成platform device,转换成platform device的节点可以在sys/bus/platform/devices下查看。
节点要满足什么要求才会被转换成platform device呢?
转换规则
-
- 根节点下包含
compatible属性的子节点。
- 根节点下包含
-
- 节点中的
compatible属性包含simple-bus、simple-mfd或isa中的任意一个。
- 节点中的
-
- 如果节点的
compatible属性包含arm,primecell值,则该节点不会被转换为 AMBA 设备,也不会被转换为 platform 设备。
- 如果节点的
例子
例子1
cpp
/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 = <0 1 2 3 4>;
};
};
gpio@22020101{
compatible = "led";
reg = <0x20220101 0x40>;
status="okay";
};
};
上面代码中根节点下存在chosen、cpu1、aliases、node1、node1、gpio@22020101这6个节点,根据规则1,根节点下包含 compatible 属性的子节点。所以,只有cpu1与 gpio@22020101会被编译为platform_device
例子2
cpp
/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>;
compatible = "simple-bus";
gpio@22020102 {
reg = <0x22020102 0x40>;
};
};
node2 {
node1-child {
pinnum = <0 1 2 3 4>;
};
};
gpio@22020101 {
compatible = "led";
reg = <0x22020101 0x40>;
status = "okay";
};
};
这里尽管node1节点中存在compatible且compatible的值为"simple-bus",但是由于其子节gpio@22020102 中不存在compatible属性所以node1节点不会编译为platform device
例子3
cpp
/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>;
compatible = "simple-bus";
gpio@22020102 {
compatible="gpio";
reg = <0x22020102 0x40>;
};
};
node2 {
node1-child {
pinnum = <0 1 2 3 4>;
};
};
gpio@22020101 {
compatible = "led";
reg = <0x22020101 0x40>;
status = "okay";
};
};
这里node1存在compatible且为simple-bus同时子节点gpio@22020102中存在compatible为"gpio"所以node1会被编译为platform device
例子4
cpp
/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>;
};
amba {
compatible = "simple-bus";
#address-cells = <2>;
#size-cells = <2>;
ranges;
dmac_peri: dma-controller@ff250000 {
compatible = "arm,pl330", "arm,primecell";
reg = <0x0 0xff250000 0x0 0x4000>;
interrupts = <GIC_SPI 2 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 3 IRQ_TYPE_LEVEL_HIGH>;
#dma-cells = <1>;
arm,pl330-broken-no-flushp;
arm,pl330-periph-burst;
clocks = <&cru ACLK_DMAC_PERI>;
clock-names = "apb_pclk";
};
dmac_bus: dma-controller@ff600000 {
compatible = "arm,pl330", "arm,primecell";
reg = <0x0 0xff600000 0x0 0x4000>;
interrupts = <GIC_SPI 0 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 1 IRQ_TYPE_LEVEL_HIGH>;
#dma-cells = <1>;
arm,pl330-broken-no-flushp;
arm,pl330-periph-burst;
clocks = <&cru ACLK_DMAC_BUS>;
clock-names = "apb_pclk";
};
};
};
这里的amba节点即便其compatible为"simple-bus",其子节点都有compatible属性但是compatible中属性是arm,primecell,所以amba节点不会编译为platform device
AMBA 设备
AMBA(Advanced Microcontroller Bus Architecture)是 ARM 公司定义的一套总线标准,包括 AHB、APB 和 AXI 等子总线。AMBA 设备是遵循这些总线规范的外设,包括 DMA 控制器、定时器、UART、SPI 控制器等。
在 Linux 内核中,提供了一个专门的 AMBA 总线驱动层 (drivers/amba/bus.c) 来管理这些设备。
定义特征
-
- 设备表示 :设备通常通过
arm,primecell或arm,plxxx(如pl330、pl011)等compatible属性表示。
- 设备表示 :设备通常通过
-
- 驱动注册 :驱动使用
amba_driver注册。
- 驱动注册 :驱动使用
-
- 设备节点 :设备节点通常包含在
amba { ... }总线定义下。
- 设备节点 :设备节点通常包含在
-
- 自动化匹配机制 :支持基于 AMBA ID 或
compatible属性的自动化匹配机制。
- 自动化匹配机制 :支持基于 AMBA ID 或
Platform 设备(Platform Device)
Platform 设备是 Linux 内核中最常见的一类设备类型,主要用于没有可探测机制的片上外设(on-chip peripherals)。这些设备通常直接连接在 SoC(System on Chip)的内部总线上,而非标准的外部总线(如 PCI、USB、I2C 等),因此需要手动在设备树或平台代码中声明其资源信息(如寄存器基地址、中断号、时钟等)。
定义特征:
-
• 定义位置 :通常定义在
.dts文件中。 -
• 驱动注册 :使用
platform_driver和platform_device。 -
• 手动注册:不可自动探测,需要静态注册。
-
• 适用范围:一般用于 SoC 内部的 UART、GPIO、SPI 控制器、DMA 控制器等。
对比
| 项目 | Platform 设备 | AMBA 设备 |
|---|---|---|
| 总线类型 | platform_bus | amba_bus |
| 驱动结构体 | struct platform_driver | struct amba_driver |
| 设备结构体 | struct platform_device | struct amba_device |
| 匹配方式 | device tree compatible/name 匹配 | AMBA ID/compatible 匹配 |
| 探测函数 | platform_driver.probe() | amba_driver.probe() |
| 注册机制 | 静态注册(设备树或 platform_add_devices) | 自动注册(AMBA 总线扫描) |
| 资源管理 | 通过 platform_get_resource() 获取寄存器、中断 | 内核自动从 amba_device 中解析资源 |
| 典型使用场景 | GPIO、I2C、SPI 控制器、LCD、音频 | ARM PrimeCell 外设,如 PL011 UART、PL330 DMA |
-
• 如果你的设备是 SoC 厂商自定义的外设(如特定的 GPIO 控制器、SPI 控制器等),应使用 Platform 设备模型。
-
• 如果你的设备是 ARM PrimeCell 外设(符合 AMBA 规范的,例如 PL330 DMA 控制器、PL011 UART 等),应使用 AMBA 设备模型。
-
• 在设备树中,两者结构类似,但 AMBA 节点通常放在
amba {}总线下,并且必须包含"arm,primecell"兼容字符串。
五、Linux驱动-of操作节点
匹配
上一篇中我们学习了从 device_node 到 platform_device 的转换流程,转换完成之后操作platform_driver 进行匹配。在本章中,我们将详细了解设备树下 platform_device 和 platform_driver 的匹配过程。
of_match_table
在前面学习平台总线相关章节时,我们知道只有 platform_device 结构体中的 name 属性与 platform_driver 结构体中嵌套的 driver 结构体中的 name 属性或 id_table 中的项相匹配时,驱动程序的 probe 初始化函数才会被加载。
为了使设备树能够与驱动程序进行匹配,需要在 platform_driver 驱动程序中添加 driver 结构体的 of_match_table 属性。这个属性是一个指向 const struct of_device_id 结构的指针,用于描述设备树节点和驱动程序之间的匹配规则。of_device_id 结构体定义在内核源码文件 /include/linux/mod_devicetable.h 中,具体内容如下所示:
cpp
struct of_device_id {
char name[32];
char type[32];
char compatible[128];
const void *data;
};
struct of_device_id 结构体通常作为一个数组在驱动程序中定义,用于描述设备树节点和驱 动程序之间的匹配规则。数组的最后一个元素必须是一个空的结构体,以标记数组的结束。
以下是一个示例,展示了如何在驱动程序中使用 struct of_device_id 进行设备树匹配:
cpp
static const struct [] = {
{.compatible = "vendor,device-1"},
{.compatible = "vendor,device-2" },
{},
};
在上述示例中,my_driver_match 是一个 struct of_device_id 结构体数组。每个数组元素都 包含了一个compatible字段,用于指定设备树节点的兼容性字符串。驱动程序将根据这些兼容 性字符串与设备树中的节点进行匹配。
例子
dts文件
cpp
#include "rk3568-evb1-ddr4-v10.dtsi"
#include "rk3568-linux.dtsi"
#include <dt-bindings/display/rockchip_vop.h>
/{
testLed{
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
myLed{
compatible = "my devicetree";
reg = <0xFDD60000 0x00000004>;
};
};
};
驱动代码
cpp
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
// 平台设备的初始化函数
static int my_platform_probe(struct platform_device *pdev)
{
printk(KERN_INFO "my_platform_probe: Probing platform device\n");
// 添加设备特定的操作
// ...
return 0;
}
// 平台设备的移除函数
static int my_platform_remove(struct platform_device *pdev)
{
printk(KERN_INFO "my_platform_remove: Removing platform device\n");
// 清理设备特定的操作
// ...
return 0;
}
const struct of_device_id of_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 = of_match_table_id,
},
};
// 模块初始化函数
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");
获取设备树节点
上面实现了设备树节点和驱动匹配的过程,现在也只是让他们匹配在了一起,但这样显然是不够的,为了完成一些和硬件相关的需求,我们还 需要获取到在设备树中编写的一些属性,那驱动是如何获取设备树中的属性呢?
of 操作:获取设备树节点
在 Linux 内核源码中提供了一系列的 of 操作函数来帮助我们获取到设备树中编写的属性, 在内核中以 device_node 结构体来对设备树进行描述,所以 of 操作函数实际上就是获取 device_node 结构体,所以接下来我们学习的 of 操作函数的返回值都是 device_node 结构体
cpp
struct device_node {
const char *name; /* 节点名,对应设备树中节点名称的第一部分(如 i2c1)*/
const char *type; /* 设备类型,通常对应设备树中的 device_type 属性 */
phandle phandle; /* 节点的唯一标识符 */
const char *full_name; /* 节点全名,如 i2c@40013000 */
struct property *properties; /* 指向属性链表的头指针 */
struct property *deadprops; /* 已删除的属性列表 */
struct device_node *parent; /* 指向父节点的指针 */
struct device_node *child; /* 指向第一个子节点的指针 */
struct device_node *sibling; /* 指向下一个兄弟节点的指针 */
#if defined(CONFIG_OF_KOBJ)
struct kobject kobj; /* 内核对象(用于 sysfs) */
#endif
unsigned long _flags; /* 标志位 */
void *data; /* 设备驱动私有数据指针 */
#if defined(CONFIG_SPARC)
/* ... SPARC 架构特定字段 ... */
#endif
};
of_find_node_by_name 函数
cpp
#include <linux/of.h>
struct device_node *of_find_node_by_name(struct device_node *from,
const char *nam e);
函数作用
- • 该函数通过指定的节点名称在设备树中进行查找,返回匹配的节点的 struct device_nod e 指针。
参数含义
-
•
from:指定起始节点,表示从哪个节点开始查找。如果 from 参数为 NULL,则从设备树的 根节点开始查找。 -
•
name:要查找的节点名称。
返回值
- • 如果找到匹配的节点,则返回对应的struct device_node指针。如果未找到匹配的节点,则返回 NULL。
of_find_node_by_path 函数
of_find_node_by_path 是 Linux 内核中用于通过节点路径查找设备树节点的函数。下面是 对 of_find_node_by_path 函数的详细介绍:
#include <linux/of.h>
struct device_node *of_find_node_by_path(const char *path);
函数作用
- • 该函数根据节点路径在设备树中进行查找,返回匹配的节点的struct device_node指针。
参数含义
- • 节点的路径,以斜杠分隔的字符串表示。路径格式为设备树节点的绝对路径,例如 /test/myLed。
返回值
- • 如果找到匹配的节点,则返回对应的struct device_node指针。如果未找到匹配的节点,则返回 NULL。
of_find_node_by_path 函数通过节点路径在设备树中进行查找。路径是设备树节点从根节点到目标节点的完整路径。可以通过指定正确的路径来准确地访问设备树中的特定节点。使用该函数时,可以直接传递节点的完整路径作为 path 参数,函数会 在设备树中查找匹配的节点。这对于已知节点路径的情况非常有用。
of_get_parent 函数
在 Linux 内核中,of_get_parent 函数用于获取设备树节点的父节点。下面是对 of_get_parent 函数的详细介绍:
#include <linux/of.h>
struct device_node *of_get_parent(const struct device_node *node);
函数作用
- • 该函数接收一个指向设备树节点的指针 node,并返回该节点的父节点的指针。
参数含义
- • node:要获取父节点的设备树节点指针。
返回值
- • 如果找到匹配的节点,则返回对应的struct device_node指针。如果未找到匹配的节点,则返回 NULL。
使用 of_get_parent 函数时,可以将特定的设备树节点作为参数传递给函数,然后它将返 回该节点的父节点。这对于在设备树中导航和访问节点之间的层次关系非常有用。父节点在设备树中表示了节点之间的层次结构关系。通过获取父节点,你可以访问上一级 节点的属性和配置信息,从而更好地理解设备树中的节点之间的关系。
of_get_next_child
在 Linux 内核中,of_get_next_child 函数用于获取设备树节点的下一个子节点。下面是对 of_get_next_child 函数的详细介绍
#include <linux/of.h>
struct device_node *of_get_next_child(const struct device_node *node, struct device_nod e *prev);
函数作用
- • 该函数接收两个参数:node 是当前节点,prev 是上一个子节点。它返回下一个子节点的指 针。
参数含义
-
• node:当前节点,用于指定要获取子节点的起始节点。
-
• prev:上一个子节点,用于指定从哪个子节点开始获取下一个子节点。如果为 NULL,则从 起始节点的第一个子节点开始。
返回值
- • 如果找到匹配的节点,则返回对应的struct device_node指针。如果未找到匹配的节点,则返回 NULL。
of_find_compatible_ node 函数
当设备树中存在多个设备节点,需要根据设备的兼容性字符串进行匹配时,可以使用 of_find_compatible_node 函数。该函数用于在设备树中查找与指定兼容性字符串匹配的节
点。
#include <linux/of.h>
struct device_node *of_find_compatible_node(struct device_node *from,
const char *typ e,
const char *compatible);
函数作用
- • 在设备树中查找与指定兼容性字符串匹配的节点。
参数含义
-
• from:指定起始节点,表示从哪个节点开始查找。如果 from 参数为 NULL,则从设备树的 根节点开始查找。
-
• type:要匹配的设备类型字符串,通常是 compatible 属性中的一部分。
-
• compatible:要匹配的兼容性字符串,通常是设备树节点的 compatible 属性中的值。
返回值
- • 如果找到匹配的节点,则返回对应的struct device_node指针。如果未找到匹配的节点,则返回 NULL。
使用 of_find_compatible_node 函数时,可以指定起始节点和需要匹配的设备类型字符串以 及兼容性字符串。函数会从起始节点开始遍历设备树,查找与指定兼容性字符串匹配的节点, 并返回匹配节点的指针。
of_find_matching_node_and_match
在 Linux 内核中,of_ find matching node_ and_ match 函数用于根据给定的 of_device_id 匹 配表在设备树中查找匹配的节点。
#include <linux/of.h>
struct device_node *of_find_matching_node_and_match(struct device_node *from,
const st ruct of_device_id *matches,
const struct of_device_id **match);
函数作用
- • 根据给定的 of_device_id 匹配表在设备树中查找匹配的节点。
参数含义
-
• from:指定起始节点,表示从哪个节点开始查找。如果 from 参数为 NULL,则从设备树的 根节点开始查找。
-
• matches:指向一个 of_device_id 类型的匹配表,该表包含要搜索的匹配项。
-
• 用于输出匹配到的 of_device_id 条目的指针。
返回值
- • 如果找到匹配的节点,则返回对应的struct device_node指针。如果未找到匹配的节点,则返回 NULL。
of_find_matching_node_and_match 函数在设备树中遍历节点,对每个节点使用__of_matc h_node 函数进行匹配。如果找到匹配的节点,将返回该节点的指针,并将 match 指针更新为 匹配到的 of_device_id 条目,函数会自动增加匹配节点的引用计数。以下是使用 of_find_matc hing_node_and_match 函数的示例代码:
cpp
#include
<linux/of.h>
static const struct of_find_matching_node_and_match{
{.compatible ="vendor,devoce" },
{}
};
const struct of_device_id *match;
struct device_node *np;
//
np =of_find_matching_node_and_match(NULL,my_match_table,&match);
在上述示例中,我们定义了一个 of_device_id 匹配表 my_match_table,其中包含了一个兼 容性字符串为"vendor,device"的匹配项。然后,我们使用 of_find_matching_node_and_match 函 数从根节点开始查找匹配的节点。
例子
cpp
#include "rk3568-evb1-ddr4-v10.dtsi"
#include "rk3568-linux.dtsi"
#include <dt-bindings/display/rockchip_vop.h>
/{
testLed{
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
myLed{
compatible = "my devicetree";
reg = <0xFDD60000 0x00000004>;
};
};
};
cpp
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
#include <linux/of.h>
struct device_node *mydevice_node;
const struct of_device_id *mynode_match;
struct of_device_id mynode_of_match[] = {
{.compatible="my devicetree"},
{},
};
// 平台设备的初始化函数
static int my_platform_probe(struct platform_device *pdev)
{
printk(KERN_INFO "my_platform_probe: Probing platform device\n");
// 通过节点名称查找设备树节点
mydevice_node = of_find_node_by_name(NULL, "myLed");
printk("mydevice node is %s\n", mydevice_node->name);
// 通过节点路径查找设备树节点
mydevice_node = of_find_node_by_path("/testLed/myLed");
printk("mydevice node is %s\n", mydevice_node->name);
// 获取父节点
mydevice_node = of_get_parent(mydevice_node);
printk("myled's parent node is %s\n", mydevice_node->name);
// 获取子节点
mydevice_node = of_get_next_child(mydevice_node, NULL);
printk("myled's sibling node is %s\n", mydevice_node->name);
// 使用compatible值查找节点
mydevice_node=of_find_compatible_node(NULL ,NULL, "my devicetree");
printk("mydevice node is %s\n" , mydevice_node->name);
//根据给定的of_device_id匹配表在设备树中查找匹配的节点
mydevice_node=of_find_matching_node_and_match(NULL , mynode_of_match, &mynode_match);
printk("mydevice node is %s\n" ,mydevice_node->name);
return 0;
}
// 平台设备的移除函数
static int my_platform_remove(struct platform_device *pdev)
{
printk(KERN_INFO "my_platform_remove: Removing platform device\n");
// 清理设备特定的操作
// ...
return 0;
}
const struct of_device_id of_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 = of_match_table_id,
},
};
// 模块初始化函数
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");
of 操作:获取属性
of_find_property 函数用于在设备树中查找节点 下具有指定名称的属性。如果找到了该属 性,可以通过返回的属性结构体指针进行进一步的操作,比如获取属性值、属性长度等。
cpp
#include <linux/of.h>
struct property *of_find_property(const struct device_node *np,
const char *name,
int *lenp)
函数作用
- • 该函数用于在节点 np 下查找指定名称 name 的属性。
参数含义
-
• np: 要查找的节点。
-
• name: 要查找的属性的属性名。
-
• 一个指向整数的指针,用于接收属性值的字节数。
返回值
-
• 如果成功找到了指定名称的属性,则返回对应的属性结构体指针 struct property *;如果
未找到,则返回 NULL。
cpp
struct property {
char *name; // 属性名(如 "compatible", "reg" 等)
int length; // 属性值的长度(字节数)
void *value; // 指向属性值的指针
struct property *next; // 指向下一个属性的指针(链表结构)
unsigned long _flags; // 内部标志位
unsigned int unique_id; // 唯一标识符
};
of_property_count_elems_of_size 函数
该函数在设备树中的设备节点下查找指定名称的属性,并获取该属性中元素的数量。调用 该函数可以用于获取设备树属性中某个属性的元素数量,比如一个字符串列表的元素数量或一 个整数数组的元素数量等。
cpp
int of_property_count_elems_of_size(const struct device_node *np,
const char *propname,
int elem_size)
函数作用
- • 该函数用于获取属性中指定元素的数量。
参数含义
-
• np: 设备节点。
-
• propname: 需要获取元素数量的属性名。
-
• elem_size: 单个元素的尺寸。
返回值
- • 如果成功获取了指定属性中元素的数量,则返回该数量;如果未找到属性或属性中没有元 素,则返回 0。
of_property_read_u32_index 函数
该函数在设备树中的设备节点下查找指定名称的属性,并获取该属性在给定索引位置处的 u32 类型的数据值。
这个函数通常用于从设备树属性中读取特定索引位置的整数值。通过指定属性名和索引,
可以获取属性中指定位置的具体数值。
cpp
#include <linux/of.h>
int of_property_read_u32_index(const struct device_node *np, const char *propname, u3
2 index, u32 *out_value)
函数作用
- • 该函数用于从指定属性中获取指定索引位置的 u32 类型的数据值。
参数含义
-
• np: 设备节点。
-
• propname: 需要获取元素数量的属性名。
-
• index: 要读取的属性值在属性中的索引,索引从 0 开始。
-
• out_value: 用于存储读取到的值的指针。
返回值
-
• 如果成功读取到了指定属性指定索引位置的 u32 类型的数据值,则返回 0;如果未找到
属性或读取失败,则返回相应的错误码。
of_property_read_u64_index 函数
该函数在设备树中的设备节点下查找指定名称的属性,并获取该属性在给定索引位置处的 u64 类型的数据值。这个函数通常用于从设备树属性中读取特定索引位置的 64 位整数值。通过指定属性名和索引,可以获取属性中指定位置的具体数值
#include <linux/of.h>
static inline int of_property_read_u64_index(const struct device_node *np,
const char *propname,
u32 index,
u64 *out_value)
函数作用
- • 该函数用于从指定属性中获取指定索引位置的 u64 类型的数据值。
参数含义
-
• np: 设备节点。
-
• propname: 需要获取元素数量的属性名。
-
• index: 要读取的属性值在属性中的索引,索引从 0 开始。
-
• out_value: 用于存储读取到的值的指针。
返回值
-
• 如果成功读取到了指定属性指定索引位置的 u64 类型的数据值,则返回 0;如果未找到
属性或读取失败,则返回相应的错误码。
of_property_read_variable_u32_array
该函数用于从设备树中读取指定属性名的变长数组。通过提供设备节点、属性名和输出数 组的指针,可以将设备树中的数组数据读取到指定的内存区域中。同时,还需要指定数组的最 小大小和最大大小,以确保读取到的数组符合预期的大小范围。
#include <linux/of.h>
int of_property_read_variable_u32_array(const struct device_node *np,
const char *propn ame,
u32 *out_values,
size_t SZ_min,
size_t SZ_max)
函数作用
- • 从指定属性中读取变长的 u32 数组。
参数含义
-
• np: 设备节点。
-
• propname: 需要获取元素数量的属性名。
-
• out_values: 用于存储读取到的 u8 数组的指针。
-
• SZ_min: 数组的最小大小。
-SZ_max: 数组的最大大小。
返回值
- • 如果成功读取到了指定属性的 u8 数组,则返回数组的大小。如果未找到属性或读取失败,则返回相应的错误码。
上面介绍的函数用于从指定属性中读取变长的 u32 数组,下面是另外三个读取其他数组 大小的函数:
这里给出了四个函数,用于从设备树中读取数组类型的属性值: 从指定属性中读取变长的 u8 数组:
从指定属性中读取变长的 u8 数组:
int of_property_read_variable_u8_array(const struct device_node *np,
const char *propname,
u8 *out_values,
size_t sz_min,
size_t sz_max)
从指定属性中读取变长的 u16 数组:
int of_property_read_variable_u16_array(const struct device_node *np,
const char *propname,
u16 *out_values,
size_t sz_min,
size_t sz_max)
从指定属性中读取变长的 u64 数组:
int of_property_read_variable_u64_array(const struct device_node *np,
const char *propname,
u64 *out_values,
size_t sz_min,
size_t sz_max)
of_property_read_string 函数
该函数在设备树中的设备节点下查找指定名称的属性,并获取该属性的字符串值,最后返 回读取到的字符串的指针,通常用于从设备树属性中读取字符串值。通过指定属性名,可以获 取属性中的字符串数据。
#include <linux/of.h>
static inline int of_property_read_string(const struct device_node *np,
const char *propname,
const char **out_string)
函数作用
- • 该函数用于从指定属性中读取字符串。
参数含义
-
• np: 设备节点。
-
• propname: 需要获取元素数量的属性名。
-
• out_string: 用于存储读取到的字符串的指针。
返回值
- • 如果成功读取到了指定属性的字符串,则返回 0;如果未找到属性或读取失败,则返回相 应的错误码。
例子
设备树文件
cpp
#include "rk3568-evb1-ddr4-v10.dtsi"
#include "rk3568-linux.dtsi"
#include <dt-bindings/display/rockchip_vop.h>
/{
testLed{
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
myLed{
compatible = "my devicetree";
reg = <0xFDD60000 0x00000004>;
};
};
};
驱动文件
cpp
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
#include <linux/of.h>
struct device_node *mydevice_node;
const struct of_device_id *mynode_match;
struct of_device_id mynode_of_match[] = {
{.compatible="my devicetree"},
{},
};
// 平台设备的初始化函数
static int my_platform_probe(struct platform_device *pdev)
{
printk(KERN_INFO "my_platform_probe: Probing platform device\n");
// 通过节点名称查找设备树节点
mydevice_node = of_find_node_by_name(NULL, "myLed");
printk("mydevice node is %s\n", mydevice_node->name);
// 通过节点路径查找设备树节点
mydevice_node = of_find_node_by_path("/testLed/myLed");
printk("mydevice node is %s\n", mydevice_node->name);
// 获取父节点
mydevice_node = of_get_parent(mydevice_node);
printk("myled's parent node is %s\n", mydevice_node->name);
// 获取子节点
mydevice_node = of_get_next_child(mydevice_node, NULL);
printk("myled's sibling node is %s\n", mydevice_node->name);
// 使用compatible值查找节点
mydevice_node=of_find_compatible_node(NULL ,NULL, "my devicetree");
printk("mydevice node is %s\n" , mydevice_node->name);
//根据给定的of_device_id匹配表在设备树中查找匹配的节点
mydevice_node=of_find_matching_node_and_match(NULL , mynode_of_match, &mynode_match);
printk("mydevice node is %s\n" ,mydevice_node->name);
return 0;
}
// 平台设备的移除函数
static int my_platform_remove(struct platform_device *pdev)
{
printk(KERN_INFO "my_platform_remove: Removing platform device\n");
// 清理设备特定的操作
// ...
return 0;
}
const struct of_device_id of_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 = of_match_table_id,
},
};
// 模块初始化函数
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");
运行效果

六、Linux-ranges属性+of操作中断
ranges 属性
描述
ranges 属性是一种用于描述设备之间地址映射关系的属性。它在设备树(Device Tree)中 使用,用于描述子设备地址空间如何映射到父设备地址空间。设备树是一种硬件描述语言,用 于描述嵌入式系统中的硬件组件和它们之间的连接关系。
设备树中的每个设备节点都可以具有 ranges 属性,其中包含了地址映射的信息。下面是一个常见的格式:
ranges = <child-bus-address parent-bus-address length>;
或
ranges;
child-bus-address : 子设备地址空间的起始地址。它指定了子设备在父设备地址空间中的位置。具体的字长由 ranges 所在节点的 #address-cells 属性决定。
parent-bus-address : 父设备地址空间的起始地址。它指定了父设备中用于映射子设备的地址范围。具体的字长由 ranges 的父节点的 #address-cells 属性决定。
length : 映射的大小。它指定了子设备地址空间在父设备地址空间中的长度。具体的字长由 ranges 的父节点的 #size-cells 属性决定。
当 ranges 属性的值为空时,表示子设备地址空间和父设备地址空间具有完全相同的映射,即 1:1 映射。这通常用于描述内存区域,其中子设备和父设备具有相同的地址范围。
当 ranges 属性的值不为空时,按照指定的映射规则将子设备地址空间映射到父设备地址空间。具体的映射规则取决于设备树的结构和设备的特定要求。
示例
cpp
/dts-v1/;
/{
compatible = "acme ,coyotes=revenge";
#address-cells=<1>;
#size-cess=<1>;
external-bus{
#address-cells=<2>;
#size-cells=<1>;
ranges=<0 0 0x10100000 0x10000
1 0 0x10160000 0x10000
2 0 0x30000000 0x30000000>
};
};
在 external-bus 节点中,#address-cells 属性值为 2 表示 child-bus-address 使用两个值表示,即 0 和 0。父节点的 #address-cells 属性值和 #size-cells 属性值为 1,表示 parent-bus-address 和 length 都由一个值表示,即 0x10100000 和 0x10000。因此,ranges 属性的值表示将子地址空间 (0x0-0xFFFF) 映射到父地址空间 0x10100000 到 0x1010FFFF。
作用
在嵌入式系统中,不同的设备可能连接到相同的总线或总线控制器上,需要在物理地址空间中进行正确的映射,以实现数据交换和通信。例如,一个设备可能通过总线连接到主处理器或其他设备,而这些设备的物理地址范围可能不同。ranges 属性正是用于描述这种地址映射关系的。
根据上述映射关系,设备可以分为两类:内存映射型设备和非内存映射型设备。
内存映射型设备
内存映射型设备是指可以通过内存地址进行直接访问的设备。这类设备在物理地址空间中的部分被映射到系统的内存地址空间中,使 CPU 可以通过读写内存地址的方式与设备进行通信和控制。
特点:
-
- 直接访问:内存映射型设备可以被 CPU 直接访问,类似于访问内存中的数据。这种直接访问方式提供了高速的数据传输和低延迟的设备操作。
-
- 内存映射:设备的寄存器、缓冲区等资源被映射到系统的内存地址空间中,使用读写内存的方式与设备进行通信。
-
- 读写操作:CPU 可以通过读取和写入映射的内存地址来与设备进行数据交换和控制。
例子
cpp
/dts-v1/;
/{
#address-cells=<1>;
#size-cells=<1>;
ranges;
serial@101f0000{
compatible="arm,pi011";
reg=<0x101f0000 0x1000>
};
gpio@101f30000{
compatible="arm,pl061";
reg=<0x101f3000 0x1000
0x101f4000 0x10>
};
spi@101115000{
compatible="arm,pl022";
reg=<>0x10115000 0x1000>;
};
};
ranges 属性表示该设备树中会进行 1:1 的地址范围映射。使得 CPU 可以通过读写内存地址的方式与设备进 行通信和控制。
非内存映射型设备
非内存映射型设备是指不能通过内存地址直接访问的设备。这类设备可能采用其他方式与 CPU 进行通信,例如通过 I/O 端口、专用总线或特定的通信协议。
特点:
-
- 非内存访问:非内存映射型设备不能像内存映射型设备那样直接通过内存地址进行访问。它们可能使用独立的 I/O 端口或专用总线进行通信。
-
- 特定接口:设备通常使用特定的接口和协议与 CPU 进行通信和控制,例如 SPI、I2C、UART 等。
-
- 驱动程序:非内存映射型设备通常需要特定的设备驱动程序来实现与 CPU 的通信和控制。
例子
cpp
/dts-v1/;
/ {
compatible = "acme,coyotes-revenge";
#address-cells = <1>;
#size-cells = <1>;
external-bus {
compatible = "acme,external-bus";
#address-cells = <2>;
#size-cells = <1>;
ranges = <0 0 0x10100000 0x10000
1 0 0x10160000 0x10000
2 0 0x30000000 0x30000000>;
ethernet@0,0 {
compatible = "smc,smc91c111";
reg = <0 0 0x1000>;
};
i2c@1,0 {
compatible = "acme,a1234-i2c-bus";
#address-cells = <1>;
#size-cells = <0>;
reg = <1 0 0x1000>;
rtc@58 {
compatible = "maxim,ds1338";
reg = <0x58>;
};
};
};
};
首先,找到 ethernet@0 所在的节点,并查看其 reg 属性。在给定的设备树片段中,ethernet@0 的 reg 属性为 <0 0 0x1000>。在根节点中,#address-cells 的值为 1,表示地址由一个单元格组成。
接下来,根据 ranges 属性进行地址映射计算。在 external-bus 节点的 ranges 属性中有三个映射条目:
-
- 第一个映射条目为
0 0 0x10100000 0x10000,表示外部总线的地址范围为0x10100000到0x1010FFFF。该映射条目的第一个值为0,表示与 external-bus 节点的第一个子节点 ethernet@0 相关联。
- 第一个映射条目为
-
- 第二个映射条目为
1 0 0x10160000 0x10000,表示外部总线的地址范围为0x10160000到0x1016FFFF。该映射条目的第一个值为1,表示与 external-bus节点的第二个子节点i2c@1相关联。
- 第二个映射条目为
-
- 第三个映射条目为
2 0 0x30000000 0x30000000,表示外部总线的地址范围为0x30000000到0x5FFFFFFF。该映射条目的第一个值为2,表示与 external-bus 节点的第三个子节点相关联。
- 第三个映射条目为
由于 ethernet@0 与 external-bus 的第一个子节点相关联,并且它的 reg 属性为 <0 0 0x1000>,我们可以进行以下计算:
-
• ethernet@0 的物理起始地址 = 外部总线地址起始值 = 0x10100000
-
• ethernet@0 的物理结束地址 = 外部总线地址起始值 + (ethernet@0 的 reg 属性的第二个值 - 1)
= 0x10100000 + 0xFFF = 0x10100FFF
因此,ethernet@0 的物理地址范围为 0x10100000 - 0x10100FFF。
同样地,根据同样的方法可以计算 i2c@1的物理地址。
驱动代码
cpp
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/io.h>
#define DRIVER_NAME "external_bus_driver"
// 定义 ranges 条目结构
struct bus_range_entry {
u32 child_addr_high; // 子地址高位
u32 child_addr_low; // 子地址低位
u32 parent_addr; // 父地址
u32 size; // 映射大小
};
// 解析 ranges 属性的函数
static int parse_ranges_property(struct device_node *np)
{
struct bus_range_entry *ranges;
int num_entries, i, len;
const __be32 *prop;
// 获取 ranges 属性
prop = of_get_property(np, "ranges", &len);
if (!prop) {
pr_err("Failed to get ranges property\n");
return -ENODEV;
}
if (len == 0) {
pr_info("Empty ranges property (1:1 mapping)\n");
return 0;
}
// 计算条目数量
// 每个条目: child_addr(2 cells) + parent_addr(1 cell) + size(1 cell) = 4 cells
num_entries = len / (4 * sizeof(__be32));
pr_info("Found %d range entries, total length: %d bytes\n", num_entries, len);
// 分配内存存储解析结果
ranges = kcalloc(num_entries, sizeof(struct bus_range_entry), GFP_KERNEL);
if (!ranges) {
pr_err("Failed to allocate memory for ranges\n");
return -ENOMEM;
}
// 解析每个条目
for (i = 0; i < num_entries; i++) {
ranges[i].child_addr_high = be32_to_cpu(prop[0]);
ranges[i].child_addr_low = be32_to_cpu(prop[1]);
ranges[i].parent_addr = be32_to_cpu(prop[2]);
ranges[i].size = be32_to_cpu(prop[3]);
prop += 4; // 移动到下一个条目
pr_info("Range[%d]: child=0x%08x%08x, parent=0x%08x, size=0x%08x\n",
i, ranges[i].child_addr_high, ranges[i].child_addr_low,
ranges[i].parent_addr, ranges[i].size);
}
// 使用解析后的 ranges 进行地址转换演示
pr_info("\n=== Address Translation Examples ===\n");
// 示例1: 转换 ethernet@0,0 的地址
u32 ethernet_child_addr = 0x00000000; // child address from ethernet node
u32 translated_addr = 0;
for (i = 0; i < num_entries; i++) {
if (ethernet_child_addr >= ranges[i].child_addr_low &&
ethernet_child_addr < (ranges[i].child_addr_low + ranges[i].size)) {
translated_addr = ranges[i].parent_addr +
(ethernet_child_addr - ranges[i].child_addr_low);
pr_info("Ethernet child addr 0x%08x -> parent addr 0x%08x\n",
ethernet_child_addr, translated_addr);
break;
}
}
// 示例2: 转换 i2c@1,0 的地址
u32 i2c_child_addr = 0x00001000; // Assuming offset within the range
for (i = 0; i < num_entries; i++) {
if (ranges[i].child_addr_high == 1 && // Chip-select 1
i2c_child_addr < ranges[i].size) {
translated_addr = ranges[i].parent_addr + i2c_child_addr;
pr_info("I2C child addr 0x%08x -> parent addr 0x%08x\n",
i2c_child_addr, translated_addr);
break;
}
}
kfree(ranges);
return 0;
}
// 使用 OF 地址转换API的简化版本
static int parse_ranges_using_of_api(struct device_node *np)
{
struct resource res;
int ret;
pr_info("\n=== Using OF Address Translation API ===\n");
// 获取 ethernet 节点的转换后地址
struct device_node *ethernet_np = of_get_child_by_name(np, "ethernet@0,0");
if (ethernet_np) {
ret = of_address_to_resource(ethernet_np, 0, &res);
if (!ret) {
pr_info("Ethernet: start=0x%08llx, end=0x%08llx, size=0x%08llx\n",
res.start, res.end, resource_size(&res));
}
of_node_put(ethernet_np);
}
// 获取 i2c 节点的转换后地址
struct device_node *i2c_np = of_get_child_by_name(np, "i2c@1,0");
if (i2c_np) {
ret = of_address_to_resource(i2c_np, 0, &res);
if (!ret) {
pr_info("I2C: start=0x%08llx, end=0x%08llx, size=0x%08llx\n",
res.start, res.end, resource_size(&res));
}
of_node_put(i2c_np);
}
return 0;
}
// 驱动的 probe 函数
static int external_bus_probe(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
int ret;
pr_info("=== External Bus Driver Probe ===\n");
pr_info("Compatible: %s\n", of_get_property(np, "compatible", NULL));
// 方法1: 手动解析 ranges 属性
ret = parse_ranges_property(np);
if (ret) {
pr_err("Failed to parse ranges property\n");
return ret;
}
// 方法2: 使用 OF API 进行地址转换
parse_ranges_using_of_api(np);
return 0;
}
static int external_bus_remove(struct platform_device *pdev)
{
pr_info("External Bus Driver Removed\n");
return 0;
}
// 设备树匹配表
static const struct of_device_id external_bus_of_match[] = {
{ .compatible = "acme,coyotes-revenge", }, // 注意:修正了设备树中的空格和等号问题
{},
};
MODULE_DEVICE_TABLE(of, external_bus_of_match);
// 平台驱动结构
static struct platform_driver external_bus_driver = {
.driver = {
.name = DRIVER_NAME,
.of_match_table = external_bus_of_match,
},
.probe = external_bus_probe,
.remove = external_bus_remove,
};
module_platform_driver(external_bus_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("XSX");
of 操作获取中断资源
irq_of_parse_and_map 函数
该函数的主要功能是解析设备节点的"interrupts"属性,并将对应的中断号映射到系统的中 断号。"interrupts"属性通常以一种特定的格式表示,可以包含一个或多个中断号。通过提供索 引号,可以获取对应的中断号。
#include <linux/of_irq.h>
unsigned int irq_of_parse_and_map(struct device_node *dev, int index);
函数作用
- • 从设备节点的"interrupts"属性中解析和映射对应的中断号。
参数含义
-
• dev: 设备节点,表示要解析的设备节点。
-
• index: 索引号,表示从"interrupts"属性中获取第几个中断号。
-
• out_string: 用于存储读取到的字符串的指针。
返回值
- • 是一个无符号整数,表示成功解析和映射的中断号。
irq_of_parse_and_map 函数
该函数的主要功能是从给定的中断数据结构中提取中断触发类型。中断触发类型描述了中 断信号的触发条件,例如边沿触发(edge-triggered)或电平触发(level-triggered)等。
#include <linux/irq.h>
u32 irqd_get_trigger_type(struct irq_data *d);
函数作用
- • 从中断数据结构(irq_data)中获取对应的中断触发类型。
参数含义
- • d:中断数据结构(irq_data),表示要获取中断触发类型的中断。
返回值
- • 是一个无符号 32 位整数,表示成功获取的中断触发类型。
irq_get_irq_data 函数
#include <linux/irq.h>
struct irq_data *irq_get_irq_data(unsigned int irq);
函数作用
- • 根据中断号获取对应的中断数据结构。
参数含义
- • irq:中断号,表示要获取中断数据结构的中断号。
返回值
- • 指向 irq_data 结构体的指针,表示成功获取的中断数据结构。
of_irq_get 函数
该函数的主要功能是从给定的设备节点的"interrupts"属性中解析并获取对应的中断号。 "interrupts"属性通常以一种特定的格式表示,可以包含一个或多个中断号。通过提供索引号, 可以获取对应的中断号
#include <linux/irq.h>
int of_irq_get(struct device_node *dev, int index);
函数作用
- • 是从设备节点的"interrupts"属性中获取对应的中断号。
参数含义
-
• dev:设备节点,表示要获取中断号的设备节点。
-
• index:索引号,表示从"interrupts"属性中获取第几个中断号。
返回值
- • 是一个整数,表示成功获取的中断号。
platform_get_irq 函数
platform_get_irq 函数的主要功能是根据给定的平台设备和索引号获取对应的中断号。平 台设备是指与特定硬件平台相关的设备。在某些情况下,平台设备可能具有多个中断号,通过 提供索引号,可以获取对应的中断号。
#include <linux/irq.h>
int platform_get_irq(struct platform_device *dev, unsigned int num);
函数作用
- • 根据平台设备和索引号获取对应的中断号。
参数含义
-
• dev:平台设备,表示要获取中断号的平台设备。
-
• num:索引号,表示从中获取第几个中断号。
返回值
- • 是一个整数,表示成功获取的中断号。
例子
设备树代码
cpp
#include "rk3568-evb1-ddr4-v10.dtsi"
#include "rk3568-linux.dtsi"
#include <dt-bindings/display/rockchip_vop.h>
/{
testnode{
#address-cells = <1>;
#size-cells = <1>;
ranges;
compatible = "simple-bus";
myLed{
compatible = "my devicetree";
reg = <0xFDD60000 0x00000004>;
};
myirq {
compatible = "my_devicetree_irq";
interrupt-parent = <&gpio3>;
interrupts = <RK_PA5 IRQ_TYPE_LEVEL_LOW>;
};
};
};
&vp0 {
cursor-win-id = <ROCKCHIP_VOP2_CLUSTER0>;
};
&vp1 {
cursor-win-id = <ROCKCHIP_VOP2_CLUSTER1>;
};
&uart7 {
status ="okay";
pinctrl-name = "default";
pinctrl-0 = <&uart7m1_xfer>;
};
&uart4 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&uart4m1_xfer>;
};
&uart9 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&uart9m1_xfer>;
};
&can1 {
status = "okay";
compatible = "rockchip,canfd-1.0";
assigned-clocks = <&cru CLK_CAN1>;
assigned-clock-rates = <150000000>; //If can bitrate lower than 3M,the clock-rates should set 100M,else set 200M.
pinctrl-names = "default";
pinctrl-0 = <&can1m1_pins>;
};
驱动代码
cpp
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/gpio.h>
int num;
int irq;
struct irq_data *my_irq_data;
struct device_node *mydevice_node;
u32 trigger_type;
// 平台设备的初始化函数
static int my_platform_probe(struct platform_device *pdev)
{
printk(KERN_INFO "my_platform_probe: Probing platform device\n");
// 查找设备节点
mydevice_node = of_find_node_by_name(NULL, "myirq");
// 解析和映射中断
irq = irq_of_parse_and_map(mydevice_node, 0);
printk("irq is %d\n", irq);
// 获取中断数据结构
my_irq_data = irq_get_irq_data(irq);
// 获取中断触发类型
trigger_type = irqd_get_trigger_type(my_irq_data);
printk("trigger type is 0x%x\n", trigger_type);
// 将GPIO转换为中断号
irq = gpio_to_irq(101);
printk("irq is %d\n", irq);
// 从设备节点获取中断号
irq = of_irq_get(mydevice_node, 0);
printk("irq is %d\n", irq);
// 获取平台设备的中断号
irq = platform_get_irq(pdev, 0);
printk("irq is %d\n", irq);
return 0;
}
// 平台设备的移除函数
static int my_platform_remove(struct platform_device *pdev)
{
printk(KERN_INFO "my_platform_remove: Removing platform device\n");
// 清理设备特定的操作
// ...
return 0;
}
const struct of_device_id of_match_table_id[] = {
{.compatible="my_devicetree_irq"},
};
// 定义平台驱动结构体
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 = of_match_table_id,
},
};
// 模块初始化函数
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");
运行效果

可以看到总共有 5 个打印,第 1、3、4、5 个打印都是获取的中断号为 113,第2个打印的是中断的类型:IRQ_TYPE_LEVEL_LOW
七、Linux驱动-设备树插件
什么是设备树插件
Linux 4.4 以后引入了动态设备树机制,设备树插件是其一种扩展机制。
设备树插件允许在运行时动态修改设备树的内容,以便添加、修改或删除设备节点和属性。这提供了一种灵活的方式来配置和管理硬件设备,而无需重新编译整个设备树。通过使用设备树插件,开发人员可以在不重新启动系统的情况下对硬件进行配置更改,从而提高了系统的灵活性和可维护性。
应用场景
使用设备树插件,可以实现一些常见的配置变化,比如添加外部设备,禁用不需要的设备, 修改设备属性等。这对于嵌入式系统的开发和调试非常有用,特别是面对多种硬件配置或需要 频繁更改硬件配置的情况下。
语法
假如有下面设备树,现在需要动态修改设备树的内容。
cpp
vcc_camera:vcc-camera.regulator{
compatible = "regulater-fixed";
gpio = <&gpio0 RK_PC1 GPIO_ACTIVE_HIGH>;
pinctrl-name ="default";
pinctrl-0 = <&camera_pwr>;
regulator-name ="vcc_camera";
enable-active-high;
regulator-always-on;
regulator-boot-on;
};
rk_485_ctl:rk-485-ctl{
compatible = "topeet,rs485_ctl";
gpios=<&gpio0 22 GPIO_ACTIVE_HIGH>;
pinctrl-names = "default";
pinctrl-0 = <&rk_485_gpio>;
};
那么我们如果在设备树插件中要为这个节点添加 overlay_node 节点,可以有如下几种表达方式:


实验
编译
编译设备树 插件 overlay.dts,输入以下命令:
/linux_sdk/kernel/scripts/dtc/dtc -I dts -O dtb overlay.dts -o overlay.dtbo
反编译设备树,输入以下命令
/linux_sdk/kernel/scripts/dtc/dtc -I dtb -O dts overlay.dtbo -o 1.dts
使用 dtc 编译器编译得到 dtbo 文件,并将 dtbo 拷贝到开发板上。
/linux_sdk/kernel/scripts/dtc/dtc -I dts -O dtb overlay.dts -o overlay.dtbo
添加节点
我们将编译好的 dtbo 文件拷贝到开发板上,如下图

在/sys/kernel/config/device-tree/overlays/(这个目录需要加载设备树插件才会生成)目录下。创建一个内核对象mkdir test。

进到 test 文件夹

使用以下命令写入 dtbo 中

使用以下命令echo 1 > status使能 dtbo

此时我们可以使用以下命令看到加载的节点。

删除节点
如果我们想删掉使用 dtbo 修改节点在/sys/kernel/config/device-tree/overlays 下使用
"rmdirtest"即可

此时我们可以使用命令"ls /proc/device-tree/rk-485-ctl/"查看,已经看不到添加的
overlay_node 节点了。

加载多个 dtbo
准备第二个 dtbo 文件,修改 overlay_node 节点中的 status 属性。如下
cpp
/dts-v1/;
/plugin/;
&{/rk-485-ctl}
{
ovwelay_node{
status="disable";
};
};
在这个目录下使用命令 mkdir test1 创建一个内核对象。如下图

用命令"cd test"进到 test1 文件夹

使用命令"cat /overlay2.dtbo > dtbo"写进 dtbo 中

使用命令"echo 1 > status"使用 dtbo

此时我们可以使用命令"cat /proc/device-tree/rk-485-ctl/overlay_node/status"看到属性值已经被修改了过来

删除 test1 文件夹可以看到 status 的属性值已经被修改了回来,如下图

八、Linux-虚拟文件系统 ConfigFS
简述
在 Linux 内核中,有几种常用的虚拟文件系统,它们提供了一个统一的内核抽象层,使得应用程序可以通过统一的文件访问接口操作不同类型的文件和设备。这些虚拟文件系统简化了应用程序的开发和维护,提高了系统的可移植性和灵活性,并提供了管理文件系统和访问底层硬件的功能。
其中最常见的虚拟文件系统如下所示:
-
- procfs 是一个虚拟文件系统,提供了对系统内核运行时状态的访问接口。它以文件和目录的形式表示内核中的进程、设备、驱动程序和其他系统信息。通过读取和写入 procfs 中的文件,可以获取和修改有关系统状态的信息。
-
- sysfs 是一个虚拟文件系统,用于表示系统中的设备、驱动程序和其他内核对象。它提供了一种统一的接口,通过文件和目录来访问和配置这些对象的属性和状态。Sysfs 常用于设备驱动程序和系统管理工具,用于查看和控制系统的硬件和内核对象。
-
- configfs 是一个虚拟文件系统,用于动态配置和管理内核对象。它提供了一种以文件和目录的形式访问内核对象的接口,允许用户在运行时添加、修改和删除内核对象,而无需重新编译内核或重新启动系统。ConfigFS 常用于配置和管理设备、驱动程序和子系统。
设备树插件选择 ConfigFS
在前面Linux驱动20中 ,我们学习了设备树插件的使用,从而理出设备树插件的实现方式,通过上节课设备树插件的使用我们可以理出设备树的插件的实现方式,如下图

要实现上述功能,用户空间需要与内核进行交互,即将设备树插件(DTBO)加载到内存中。sysfs 虚拟文件系统的作用是将内核中的数据和属性以文件的形式导出到用户空间,使得用户可以通过读写这些文件来访问和控制设备状态。
configfs 的作用可以英文解释为 "Userspace-driven kernel object configuration",翻译过来即用户空间配置内核对象。因此,configfs 与 sysfs 恰恰相反:sysfs 是将内核对象导出给用户空间,而 configfs 则是从用户空间配置内核对象,且不需要重新编译内核或修改内核代码。因此,configfs 更适合用于设备树插件的技术。
数据结构
ConfigFS 的核心数据结构主要包括以下几个部分:
-
- configfs_subsystem: 这是顶层的数据结构,用于表示整个 ConfigFS 子系统。它包含根配置项组的指针,以及其他属性和状态信息。
-
- config_group : 这是一种特殊类型的配置项,表示一个配置项组。它可以包含一组相关的配置项,形成一个层次结构。
config_group结构中包含了父配置项的指针以及指向子配置项的链表。
- config_group : 这是一种特殊类型的配置项,表示一个配置项组。它可以包含一组相关的配置项,形成一个层次结构。
-
- config_item : 这是 ConfigFS 中最基本的数据结构,用于表示一个配置项。每个配置项都是一个内核对象,可以是设备、驱动程序、子系统等。
config_item结构包含了配置项的类型、名称、属性、状态等信息,以及指向父配置项和子配置项的指针。
- config_item : 这是 ConfigFS 中最基本的数据结构,用于表示一个配置项。每个配置项都是一个内核对象,可以是设备、驱动程序、子系统等。
这些数据结构之间的关系可以形成一个树形结构这可以通过以下关系表示:

子系统、容器和 config_item
configfs_subsystem 结构体,如下所示:
cpp
struct configfs_subsystem {
structconfig_group su_group;
struct mutex su_mutex;
};
configfs_subsystem 结构体中包含config_group结构体,config_group 结构体如下所示:
cpp
struct config_group {
struct config_item cg_item;
struct list_head cg_children;
struct configfs_subsystem *cg_subsys;
struct list_head default_groups;
struct list_head group_entry;
};
config_group 结构体中包含 config_item 结构体,config_item 结构体如下所示:
cpp
struct config_item {
char *ci_name;
char ci_namebuf[CONFIGFS_ITEM_NAME_LEN]; //目录的名字 ci_kref;
struct kref
struct list_head ci_entry;
struct config_item *ci_parent;
struct config_group *ci_group;
const struct config_item_type *ci_type;
struct dentry *ci_dentry;
};
//目录下属性文件和属性操作
设备树插件驱动代码分析
cpp
static struct
dtbocfg root subsys ={
.su group = {
.cg_item = {
.ci_namebuf = "device-tree",
.ci_typr = &dtbocfg_root_type,
},
},
.su_mutex = _MUTEX_INITIALIZER(dtbofg_root_subsys.su_mutex),
};
这段代码定义了一个名为 dtbocfg_root_subsys 的 configfs_subsystem 结构体实例,表示 ConfigFS 中的一个子系统。
首先,dtbocfg_root_subsys.su_group 是一个 config_group 结构体,表示子系统的根配置项组。该结构体的 cg_item 字段表示根配置项组的基本配置项。具体配置如下:
-
•
.ci_namebuf = "device-tree": 配置项的名称设置为 "device-tree",表示该配置项的名称为 "device-tree"。 -
•
.ci_type = &dtbocfg_root_type: 配置项的类型设置为dtbocfg_root_type,这是一个自定义的配置项类型。
接着,.su_mutex 字段是一个互斥锁,用于保护子系统的操作。在这里,使用了 __MUTEX_INITIALIZER 宏来初始化互斥锁。
通过这段代码,创建了一个名为"device-tree"的子系统,它的根配置项组为空。可以在该子系统下添加更多的配置项和配置项组,用于动态配置和管理设备树相关的内核对象。Linux 系统下创建了 device-tree 这个子系统,如下图

设备树插件驱动代码中注册配置项组
cpp
static int __init dtbocfg_module_init(void)
{
int retval = 0;
pr_info("%s\n", __func__);
config_group_init(&dtbocfg_root_subsys.su_group);
config_group_init_type_name(&dtbocfg_overlay_group, "overlays", &dtbocfg_overlays_type);
retval = configfs_register_subsystem(&dtbocfg_root_subsys);
if (retval != 0) {
pr_err("%s: couldn't register subsystem\n", __func__);
goto register_subsystem_failed;
}
retval = configfs_register_group(&dtbocfg_root_subsys.su_group, &dtbocfg_overlay_group);
if (retval != 0) {
pr_err("%s: couldn't register group\n", __func__);
goto register_group_failed;
}
pr_info("%s: OK\n", __func__);
return 0;
register_group_failed:
configfs_unregister_subsystem(&dtbocfg_root_subsys);
register_subsystem_failed:
return retval;
}
dtbocfg_module_init(),用于初始化和注册 ConfigFS 子系统及配置项组。具体步骤如下:
-
- 初始化根配置项组:
- • 通过
config_group_init()函dtbocfg_root_subsys.su_group,即子系统的根配置项组。
-
- 初始化 "overlays" 配置项组:
- • 使用
config_group_init_type_name()函数初始化了dtbocfg_overlay_group,表示名为 "overlays" 的配置项组,并指定dtbocfg_overlays_type,这是一个自定义的配置项类型。
-
- 注册子系统:
- • 调用
configfs_register_subsystem()函数注册了dtbocfg_root_subsys子系统。如果注册失败,将打印错误信息,并跳转到register_subsystem_failed标签处进行错误处理。
-
- 注册配置项组:
- • 调用
configfs_register_group()函数注册了dtbocfg_overlay_group配置项组,并将其添加到dtbocfg_root_subsys.su_group下。如果注册失败,同样会打印错误信息,并跳转到register_group_failed标签处进行错误处理。
-
- 成功处理:
- • 如果所有注册过程都成功,将打印 "OK" 消息,并返回 0,表示初始化成功。
-
- 失败处理:
- • 如果在注册配置项组失败时,先调用
configfs_unregister_subsystem()函数注销之前注册的子系统,然后返回注册失败的错误码retval。
这段代码的主要作用是初始化和注册一个名为 "device-tree" 的 ConfigFS 子系统,并在其下创建一个名为 "overlays" 的配置项组。在 Linux 系统中,这会在 device-tree 子系统下创建一个 overlays 容器,如下图所示

属性和方法
我们要在容器下放目录或属性文件,所以我们看一下 config_item 结构体,如下所示
cpp
struct config_item {
char *ci_name;
char ci_namebuf[CONFIGFS_ITEM_NAME_LEN]; //目录的名字 ci_kref;
struct kref
struct list_head
struct config_item *ci_parent; struct config_group *ci_group; const struct config_item_type struct dentry
struct *ci_dentry;
};
config_item结构体中包含了config_item_type结构体,config_item_type结构体如下所示:
cpp
struct config_item_type {
struct module *ct_owner;
struct configfs_item_operations;
struct configfs_group_operations *ct_group_ops; //group(容器)的操作方法
struct configfs_attribute **ct_attrs; //属性文件的操作方法
struct configfs_bin_attribute **ct_bin_attrs; //bin 属性文件的操作方法
};
config_item_type 结构体中包含了 struct configfs_item_operations 结构体,如下所示:
cpp
struct configfs_item_operations {
//删除 item 方法,在 group 下面使用 rmdir 命令会调用这个方法
void (*release)(struct config_item *);
int (*allow_link)(struct config_item *src, struct config_item *target); void (*drop_link)(struct config_item *src, struct config_item *target);
};
config_item_type 结构体中包含了 struct configfs_group_operations 结构体,如下所示:
cpp
struct configfs_group_operations {
//创建 item 的方法,在 group 下面使用 mkdir 命令会调用这个方法
struct config_item *(*make_item)(struct config_group *group, const char *name);
//创建 group 的方法
struct config_group *(*make_group)(struct config_group *group, const char *name);
int (*commit_item)(struct config_item *item);
void (*disconnect_notify)(struct config_group *group, struct config_item *item); void (*drop_item)(struct config_group *group, struct config_item *item);
};
config_item_type 结构体中包含了 struct configfs_attribute 结构体 ,如下所示:
cpp
struct configfs_attribute {
const char struct module umode_t
*ca_name; 属性文件的名字
*ca_owner; 属性文件文件的所属模块 ca_mode; 属性文件访问权限
读写方法的函数指针,具体功能需要自行实现。
ssize_t (*show)(struct config_item *, char *);
ssize_t (*store)(struct config_item *, const char *, size_t); };
总结

例子
注册 configfs 子系统
编写驱动代码创建一个名为myconfigfs的 configfs 子系统,并将其注册到内核中。 编写完成的configfs_subsystem.c 代码如下所示:
cpp
#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>
//定义名为"myconfig_item_type"的配置项类型结构体
static const struct config_item_type myconfig_item_type ={
.ct_owner = THIS_MODULE,
.ct_item_ops = NULL,
.ct_group_ops = NULL,
.ct_attrs = NULL,
};
//定义一个configfs_subsystem结构体实例"myconfigfs_subsystem"
static struct configfs_subsystem myconfigfs_subsystem ={
.su_group = {
.cg_item = {
.ci_namebuf = "myconfigfs",
.ci_type = &myconfig_item_type,
},
},
};
//模块的初始化函数
static int myconfigfs_init(void)
{
//初始化配置组
config_group_init(&myconfigfs_subsystem.su_group);
//注册子系统
configfs_register_subsystem(&myconfigfs_subsystem);
return 0;
}
// 模块退出函数
static void myconfigfs_exit(void)
{
configfs_unregister_subsystem(&myconfigfs_subsystem);
}
module_init(myconfigfs_init); // 指定模块的初始化函数
module_exit(myconfigfs_exit); // 指定模块的退出函数
MODULE_LICENSE("GPL"); // 模块使用的许可证
MODULE_AUTHOR("xsx");
开发板启动之后,使用以下命令进行驱动模块的加载,如下图

进入/sys/kernel/config 目录下,可以看到注册生成的 myconfigfs 子系统

最后可以使用以下命令进行驱动的卸载如下图

注册 group 容器实验
cpp
#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>
// 定义一个名为"mygroup"的config_group结构体
static struct config_group mygroup;
// 定义一个名为"mygroup_config_item_type"的config_item_type结构体,用于描述配置项类型。
static const struct config_item_type mygroup_config_item_type = {
.ct_owner = THIS_MODULE,
.ct_item_ops = NULL,
.ct_group_ops = NULL,
.ct_attrs = NULL,
};
// 定义名为"myconfig_item_type"的配置项类型结构体
static const struct config_item_type myconfig_item_type = {
.ct_owner = THIS_MODULE,
.ct_group_ops = NULL,
};
// 定义一个configfs_subsystem结构体实例"myconfigfs_subsystem"
static struct configfs_subsystem myconfigfs_subsystem = {
.su_group = {
.cg_item = {
.ci_namebuf = "myconfigfs",
.ci_type = &myconfig_item_type,
},
},
};
// 模块的初始化函数
static int myconfig_group_init(void)
{
// 初始化配置组
config_group_init(&myconfigfs_subsystem.su_group);
// 注册子系统
configfs_register_subsystem(&myconfigfs_subsystem);
// 初始化配置组"mygroup"
config_group_init_type_name(&mygroup, "mygroup", &mygroup_config_item_type);
// 在子系统中注册配置组"mygroup"
configfs_register_group(&myconfigfs_subsystem.su_group, &mygroup);
return 0;
}
// 模块退出函数
static void myconfig_group_exit(void)
{
// 注销子系统
configfs_unregister_subsystem(&myconfigfs_subsystem);
}
module_init(myconfig_group_init); // 指定模块的初始化函数
module_exit(myconfig_group_exit); // 指定模块的退出函数
MODULE_LICENSE("GPL"); // 模块使用的许可证
MODULE_AUTHOR("xsx");
开发板启动之后,使用以下命令进行驱动模块的加载,如下图

进入/sys/kernel/config 目录下,可以看到注册生成的 myconfigfs 子系统,如下图所示:

然后我们进入注册生成的 myconfigfs 子系mygroup容器。

最后可以使用以下命令进行驱动的卸载,如下图(图 77-7)所示:

用户空间创建 item
cpp
#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>
// 定义一个名为"mygroup"的config_group结构体
static struct config_group mygroup;
// 自定义的配置项结构体
struct myitem
{
struct config_item item;
};
// 配置项释放函数
void myitem_release(struct config_item *item)
{
struct myitem *myitem = container_of(item, struct myitem, item);
kfree(myitem);
printk("%s\n", __func__);
}
// 配置项操作结构体
struct configfs_item_operations myitem_ops = {
.release = myitem_release,
};
// 配置项类型结构体
static struct config_item_type mygroup_item_type = {
.ct_owner = THIS_MODULE,
.ct_item_ops = &myitem_ops,
};
// 创建配置项函数
struct config_item *mygroup_make_item(struct config_group *group, const char *name)
{
struct myitem *myconfig_item;
printk("%s\n", __func__);
myconfig_item = kzalloc(sizeof(*myconfig_item), GFP_KERNEL);
config_item_init_type_name(&myconfig_item->item, name, &mygroup_item_type);
return &myconfig_item->item;
}
// 配置组操作结构体
struct configfs_group_operations mygroup_ops = {
.make_item = mygroup_make_item,
};
// 定义名为"mygroup_config_item_type"的config_item_type结构体,用于描述配置项类型。
static const struct config_item_type mygroup_config_item_type = {
.ct_owner = THIS_MODULE,
.ct_group_ops = &mygroup_ops,
};
// 定义名为"myconfig_item_type"的配置项类型结构体
static const struct config_item_type myconfig_item_type = {
.ct_owner = THIS_MODULE,
.ct_group_ops = NULL,
};
// 定义一个configfs_subsystem结构体实例"myconfigfs_subsystem"
static struct configfs_subsystem myconfigfs_subsystem = {
.su_group = {
.cg_item = {
.ci_namebuf = "myconfigfs",
.ci_type = &myconfig_item_type,
},
},
};
// 模块的初始化函数
static int myconfig_group_init(void)
{
// 初始化配置组
config_group_init(&myconfigfs_subsystem.su_group);
// 注册子系统
configfs_register_subsystem(&myconfigfs_subsystem);
// 初始化配置组"mygroup"
config_group_init_type_name(&mygroup, "mygroup", &mygroup_config_item_type);
// 在子系统中注册配置组"mygroup"
configfs_register_group(&myconfigfs_subsystem.su_group, &mygroup);
return 0;
}
// 模块退出函数
static void myconfig_group_exit(void)
{
// 注销子系统
configfs_unregister_subsystem(&myconfigfs_subsystem);
}
module_init(myconfig_group_init); // 指定模块的初始化函数
module_exit(myconfig_group_exit); // 指定模块的退出函数
MODULE_LICENSE("GPL"); // 模块使用的许可证
MODULE_AUTHOR("xsx"); // 模块的作者
开发板启动之后,使用以下命令进行驱动模块的加载

进入/sys/kernel/config 目录下,可以看到注册生成的 myconfigfs 子系统,如下图

然后我们进入注册生成的 myconfigfs 子系统,如下图可以看到注册生成的 mygroup 容器。

然后输入"mkdir test"命令创建 config_item,如下图所示,创建成功之后,打印"mygroup_make_item",说明驱动程序中 mygroup_make_item 函数成功执行

输入"rmdir test"命令删除 item,如下图

最后可以使用以下命令进行驱动的卸载,如下图

九、Linux-Configs驱动完善
drop 函数
我们在命令行使用 rmdir 命令删除 item 时,会执行驱动中的 release 函数,本章节来学 习一个新函数------drop 函数。
release 和 drop 函数的区别
release 和 .drop_item 是两个不同的成员字段,用于不同的目的:
-
• .release 成员字段是
struct config_item_type结构体中的一个回调函数指针。它指向一个函数,当 ConfigFS 中的配置项被释放或删除时,内核会调用该函数来执行相应的资源释放操作。通常,这个函数用于释放与配置项相关的资源,例如释放动态分配的内存或关闭打开的文件描述符。 -
• .drop_item 成员字段是
struct configfs_group_operations结构体中的一个回调函数指针。它指向一个函数,当 ConfigFS 中的配置组被删除时,内核会调用该函数来处理与配置组相关的操作。这个函数通常用于清理配置组的状态、释放相关的资源以及执行其他必要的清理操作。.drop_item函数在删除配置组时被调用,而不是在删除单个配置项时被调用。
.release 成员字段用于配置项的释放操作,而 .drop_item 成员字段用于配置组的删除操作。它们分别在不同的上下文中执行不同的任务,但都与资源释放和清理有关。
例子
cpp
#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>
// 定义一个名为"mygroup"的config_group结构体
static struct config_group mygroup;
// 自定义的配置项结构体
struct myitem
{
struct config_item item;
};
// 配置项释放函数
void myitem_release(struct config_item *item)
{
struct myitem *myitem = container_of(item, struct myitem, item);
kfree(myitem);
printk("%s\n", __func__);
}
// 配置项操作结构体
struct configfs_item_operations myitem_ops = {
.release = myitem_release,
};
// 配置项类型结构体
static struct config_item_type mygroup_item_type = {
.ct_owner = THIS_MODULE,
.ct_item_ops = &myitem_ops,
};
// 创建配置项函数
struct config_item *mygroup_make_item(struct config_group *group, const char *name)
{
struct myitem *myconfig_item;
printk("%s\n", __func__);
myconfig_item = kzalloc(sizeof(*myconfig_item), GFP_KERNEL);
config_item_init_type_name(&myconfig_item->item, name, &mygroup_item_type);
return &myconfig_item->item;
}
// 删除配置项函数
void mygroup_delete_item(struct config_group *group, struct config_item *item)
{
struct myitem *myitem = container_of(item, struct myitem, item);
config_item_put(&myitem->item);
printk("%s\n", __func__);
}
// 配置组操作结构体
struct configfs_group_operations mygroup_ops = {
.make_item = mygroup_make_item,
.drop_item = mygroup_delete_item,
};
// 定义名为"mygroup_config_item_type"的config_item_type结构体,用于描述配置项类型。
static const struct config_item_type mygroup_config_item_type = {
.ct_owner = THIS_MODULE,
.ct_group_ops = &mygroup_ops,
};
// 定义名为"myconfig_item_type"的配置项类型结构体
static const struct config_item_type myconfig_item_type = {
.ct_owner = THIS_MODULE,
.ct_group_ops = NULL,
};
// 定义一个configfs_subsystem结构体实例"myconfigfs_subsystem"
static struct configfs_subsystem myconfigfs_subsystem = {
.su_group = {
.cg_item = {
.ci_namebuf = "myconfigfs",
.ci_type = &myconfig_item_type,
},
},
};
// 模块的初始化函数
static int myconfig_group_init(void)
{
// 初始化配置组
config_group_init(&myconfigfs_subsystem.su_group);
// 注册子系统
configfs_register_subsystem(&myconfigfs_subsystem);
// 初始化配置组"mygroup"
config_group_init_type_name(&mygroup, "mygroup", &mygroup_config_item_type);
// 在子系统中注册配置组"mygroup"
configfs_register_group(&myconfigfs_subsystem.su_group, &mygroup);
return 0;
}
// 模块退出函数
static void myconfig_group_exit(void)
{
// 注销子系统
configfs_unregister_subsystem(&myconfigfs_subsystem);
}
module_init(myconfig_group_init); // 指定模块的初始化函数
module_exit(myconfig_group_exit); // 指定模块的退出函数
MODULE_LICENSE("GPL"); // 模块使用的许可证
MODULE_AUTHOR("xsx");
开发板启动之后,使用以下命令进行驱动模块的加载,如下图

驱动加载后进入/sys/kernel/config 目录下,可以看到注册生成的 myconfigfs 子系统,如下图

然后我们进入注册生成的 myconfigfs 子系统,如下图,可以看到注册生成的mygroup容器。

然后输入"mkdir test"命令创建config_item,如下图所示,创建成功之后,打印"mygroup_make_item"。

输入"rmdir test"命令删除 item,如下图所示,执行 rmdir 命令之后,依次执行了mygroup_delete_item 函数和 myitem_release 函数。

最后可以使用以下命令进行驱动的卸载,如下图

注册 attribute
在前面的分析中,我们编写驱动程序,实现了注册 ConfigFS 子系统,注册 group 容器, 支持使用 mkdir 命令创建 item,完善了 drop 和 release 函数的功能。在之前的实验中,我们成功 创建了 item,但是 item 下面没有创建属性和操作项,那么下面我们来学习如何注册属性。
例子
cpp
#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>
// 定义一个名为"mygroup"的config_group结构体
static struct config_group mygroup;
// 自定义的配置项结构体
struct myitem
{
struct config_item item;
int size;
void *addr;
};
// 配置项释放函数
void myitem_release(struct config_item *item)
{
struct myitem *myitem = container_of(item, struct myitem, item);
kfree(myitem);
printk("%s\n", __func__);
};
// 读取配置项内容的回调函数
ssize_t myread_show(struct config_item *item, char *page)
{
struct myitem *myitem = container_of(item, struct myitem, item);
memcpy(page, myitem->addr, myitem->size);
printk("%s\n", __func__);
return myitem->size;
};
// 写入配置项内容的回调函数
ssize_t mywrite_store(struct config_item *item, const char *page, size_t size)
{
struct myitem *myitem = container_of(item, struct myitem, item);
myitem->addr = kmemdup(page, size, GFP_KERNEL);
myitem->size = size;
printk("%s\n", __func__);
return myitem->size;
};
// 创建只读配置项
CONFIGFS_ATTR_RO(my, read);
// 创建只写配置项
CONFIGFS_ATTR_WO(my, write);
// 配置项属性数组
struct configfs_attribute *my_attrs[] = {
&myattr_read,
&myattr_write,
NULL,
};
// 配置项操作结构体
struct configfs_item_operations myitem_ops = {
.release = myitem_release,
};
// 配置项类型结构体
static struct config_item_type mygroup_item_type = {
.ct_owner = THIS_MODULE,
.ct_item_ops = &myitem_ops,
.ct_attrs = my_attrs,
};
// 创建配置项函数
struct config_item *mygroup_make_item(struct config_group *group, const char *name)
{
struct myitem *myconfig_item;
printk("%s\n", __func__);
myconfig_item = kzalloc(sizeof(*myconfig_item), GFP_KERNEL);
config_item_init_type_name(&myconfig_item->item, name, &mygroup_item_type);
return &myconfig_item->item;
}
// 删除配置项函数
void mygroup_delete_item(struct config_group *group, struct config_item *item)
{
struct myitem *myitem = container_of(item, struct myitem, item);
config_item_put(&myitem->item);
printk("%s\n", __func__);
}
// 配置组操作结构体
struct configfs_group_operations mygroup_ops = {
.make_item = mygroup_make_item,
.drop_item = mygroup_delete_item,
};
// 配置项类型结构体
static const struct config_item_type mygroup_config_item_type = {
.ct_owner = THIS_MODULE,
.ct_group_ops = &mygroup_ops,
};
// 配置项类型结构体
static const struct config_item_type myconfig_item_type = {
.ct_owner = THIS_MODULE,
.ct_group_ops = NULL,
};
// 定义一个configfs_subsystem结构体实例"myconfigfs_subsystem"
static struct configfs_subsystem myconfigfs_subsystem = {
.su_group = {
.cg_item = {
.ci_namebuf = "myconfigfs",
.ci_type = &myconfig_item_type,
},
},
};
// 模块的初始化函数
static int myconfig_group_init(void)
{
// 初始化配置组
config_group_init(&myconfigfs_subsystem.su_group);
// 注册子系统
configfs_register_subsystem(&myconfigfs_subsystem);
// 初始化配置组"mygroup"
config_group_init_type_name(&mygroup, "mygroup", &mygroup_config_item_type);
// 在子系统中注册配置组"mygroup"
configfs_register_group(&myconfigfs_subsystem.su_group, &mygroup);
return 0;
}
// 模块退出函数
static void myconfig_group_exit(void)
{
// 注销子系统
configfs_unregister_subsystem(&myconfigfs_subsystem);
}
module_init(myconfig_group_init); // 指定模块的初始化函数
module_exit(myconfig_group_exit); // 指定模块的退出函数
MODULE_LICENSE("GPL"); // 模块使用的许可证
MODULE_AUTHOR("xsx");
开发板启动之后,使用以下命令进行驱动模块的加载,如下图

驱动加载后,进入/sys/kernel/config 目录下,可以看到注册生成的 myconfigfs 子系统,如下图

然后我们进入注册生成的 myconfigfs 子系统,如下图,可以看到注册生成的mygroup 容器。

然后输入"mkdir test"命令创建 config_item,如下图,创建成功之后,
打印"mygroup_make_item"

然后进入到 test 目录下,如下图(图 80-8)所示。有生成的属性:read 和 write

我们输入以下命令对属性进行读写操作,如下图

在上图中,我们分别对属性 read 和 write 进行读写操作后,分别打印"myread_show"和"mywrite_store"。
输入"rmdir test"命令删除 item,如下图

最后可以使用以下命令进行驱动的卸载

多级目录
经过前面理论的分析,我们了解到一个配置组也就是(group)可以包含多个配置(item) 和子配置组。配置项和配置组都是作为配置组的成员存在的。配置项和配置组之间通过指针进 行关联,以形成一个层次结构。下面呢我们来实现在设备树插件驱动程序中多级目录。
例子
cpp
#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>
// 定义一个名为"mygroup"的config_group结构体
static struct config_group mygroup;
// 自定义的配置项结构体
struct myitem
{
struct config_item item;
int size;
void *addr;
};
// 自定义的配置组结构体
struct mygroup
{
struct config_group group;
};
// 配置项释放函数
void myitem_release(struct config_item *item)
{
struct myitem *myitem = container_of(item, struct myitem, item);
kfree(myitem);
printk("%s\n", __func__);
};
// 读取配置项内容的回调函数
ssize_t myread_show(struct config_item *item, char *page)
{
struct myitem *myitem = container_of(item, struct myitem, item);
memcpy(page, myitem->addr, myitem->size);
printk("%s\n", __func__);
return myitem->size;
};
// 写入配置项内容的回调函数
ssize_t mywrite_store(struct config_item *item, const char *page, size_t size)
{
struct myitem *myitem = container_of(item, struct myitem, item);
myitem->addr = kmemdup(page, size, GFP_KERNEL);
myitem->size = size;
printk("%s\n", __func__);
return myitem->size;
};
// 创建只读配置项
CONFIGFS_ATTR_RO(my, read);
// 创建只写配置项
CONFIGFS_ATTR_WO(my, write);
// 配置项属性数组
struct configfs_attribute *my_attrs[] = {
&myattr_read,
&myattr_write,
NULL,
};
// 配置项操作结构体
struct configfs_item_operations myitem_ops = {
.release = myitem_release,
};
// 配置项类型结构体
static struct config_item_type mygroup_item_type = {
.ct_owner = THIS_MODULE,
.ct_item_ops = &myitem_ops,
.ct_attrs = my_attrs,
};
// 配置组类型结构体
static struct config_item_type mygroup_type = {
.ct_owner = THIS_MODULE,
};
// 创建配置项函数
struct config_item *mygroup_make_item(struct config_group *group, const char *name)
{
struct myitem *myconfig_item;
printk("%s\n", __func__);
myconfig_item = kzalloc(sizeof(*myconfig_item), GFP_KERNEL);
config_item_init_type_name(&myconfig_item->item, name, &mygroup_item_type);
return &myconfig_item->item;
}
// 删除配置项函数
void mygroup_delete_item(struct config_group *group, struct config_item *item)
{
struct myitem *myitem = container_of(item, struct myitem, item);
config_item_put(&myitem->item);
printk("%s\n", __func__);
}
// 创建配置组函数
struct config_group *mygroup_make_group(struct config_group *group, const char *name)
{
struct mygroup *mygroup;
printk("%s\n", __func__);
mygroup = kzalloc(sizeof(*mygroup), GFP_KERNEL);
config_group_init_type_name(&mygroup->group, name, &mygroup_type);
return &mygroup->group;
};
// 配置组操作结构体
struct configfs_group_operations mygroup_ops = {
.make_item = mygroup_make_item,
.drop_item = mygroup_delete_item,
.make_group = mygroup_make_group,
};
// 配置项类型结构体
static const struct config_item_type mygroup_config_item_type = {
.ct_owner = THIS_MODULE,
.ct_group_ops = &mygroup_ops,
};
// 配置项类型结构体
static const struct config_item_type myconfig_item_type = {
.ct_owner = THIS_MODULE,
.ct_group_ops = NULL,
};
// 定义一个configfs_subsystem结构体实例"myconfigfs_subsystem"
static struct configfs_subsystem myconfigfs_subsystem = {
.su_group = {
.cg_item = {
.ci_namebuf = "myconfigfs",
.ci_type = &myconfig_item_type,
},
},
};
// 模块的初始化函数
static int myconfig_group_init(void)
{
// 初始化配置组
config_group_init(&myconfigfs_subsystem.su_group);
// 注册子系统
configfs_register_subsystem(&myconfigfs_subsystem);
// 初始化配置组"mygroup"
config_group_init_type_name(&mygroup, "mygroup", &mygroup_config_item_type);
// 在子系统中注册配置组"mygroup"
configfs_register_group(&myconfigfs_subsystem.su_group, &mygroup);
return 0;
}
// 模块退出函数
static void myconfig_group_exit(void)
{
// 注销子系统
configfs_unregister_subsystem(&myconfigfs_subsystem);
}
module_init(myconfig_group_init); // 指定模块的初始化函数
module_exit(myconfig_group_exit); // 指定模块的退出函数
MODULE_LICENSE("GPL"); // 模块使用的许可证
MODULE_AUTHOR("xsx");
开发板启动之后,使用以下命令进行驱动模块的加载

进入/sys/kernel/config 目录下,可以看到注册生成的 myconfigfs 子系
统,如下图

然后我们进入注册生成的 myconfigfs 子系统,如下图所示,可以看到注册生
成的 mygroup 容器。

然后输入"mkdir test"命令创建 config_item,如下图所示,创建成功之后,打印"mygroup_make_group"。我们进入创建的 group------test 目录下,此时可以在 test 目录下 创建 item------test2。但由于在驱动中我们并没有实现在 group 下创建 item 功能,所以会提示创 建 test2 没有权限。

输入"rmdir test"命令删除 group,如下图

最后可以使用以下命令进行驱动的卸载,如下图
