-
设备树写法:
1.设备树的核心结构与语法
设备树的语法类似于 C 语言和 JSON 的结合体。一个标准的
.dts文件主要由头文件引用 、根节点 和子节点组成。/dts-v1/; // 1. 声明设备树的版本(必须写在第一行)
#include <dt-bindings/gpio/gpio.h> // 2. 引用头文件(可使用标准宏定义)
#include "imx6ull.dtsi" // 3. 包含芯片级的设备树(像C语言一样继承父模板)
/ { // 4. 根节点,用斜杠 "/" 表示
model = "Alientek i.MX6ULL Alpha Board"; // 这块板子的名字
compatible = "alientek,imx6ull-alpha", "fsl,imx6ull"; // 兼容性标志(匹配驱动的关键)
/* 5. 自定义子节点:描述具体的硬件 */
my_led {
compatible = "gpio-leds";
status = "okay";
gpios = <&gpio1 3 GPIO_ACTIVE_LOW>; // 硬件资源属性
};
};
2. 节点中高频使用的四大标准属性编写设备树时,你绝大多数时间都在跟以下几个属性打交道:
-
compatible(兼容性,最重要)作用 :驱动和设备匹配的唯一密码 。
写法 :
"厂商,具体芯片/硬件型号"。示例 :
compatible = "alientek,mini-led", "generic-led";(内核会先尝试匹配第一个精确名字,如果没有对应的驱动,则尝试匹配第二个通用驱动)。 -
status(设备状态)作用 :决定这个硬件在系统启动时是否被启用。
可选值:
-
"okay":硬件正常启用,内核会为它分配并匹配驱动。 -
"disabled":硬件被禁用。即使节点写得再完美,内核也会完全忽略它。
-
-
reg(寄存器地址与大小)作用 :描述硬件寄存器的物理基地址 和长度 。
写法 :
reg = <address length>;示例 :
reg = <0x020ac000 0x4>;(表示该硬件寄存器物理地址从0x020ac000开始,占 4 个字节)。 -
自定义属性(如
gpios、interrupts)作用 :向驱动程序传递个性化的硬件参数。
示例 :
gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;(告诉驱动,我用的是 GPIO1 组的第 3 号引脚,低电平点亮)。 -
设备树的两种编写模式
在实际工程中,芯片厂商(如 NXP、ST、Rockchip)已经把芯片内部的通用外设(如 I2C、SPI、UART、定时器)全部写好了,放在
.dtsi纯净文件中。作为板级开发者,你有两种方式添加自己的硬件:-
模式 A:直接在根节点
/下追加新节点(适合独立的硬件,如 LED、蜂鸣器)直接在根节点的括号{ ... };内部塞入一个全新的独立节点即可。 -
模式 B:使用引脚引用
&节点追加(适合挂在总线上的外设,如 I2C 传感器)如果你的 AP3216C 传感器挂在芯片的 I2C1 总线上,你千万不要在根节点下乱写,而是要使用
&符号(Label 引用) 找到 I2C1 控制器,把传感器塞进它的肚子里:/* 找到芯片原生的 i2c1 节点,并向里面追加你的设备 */
&i2c1 {
clock-frequency = <100000>; // 设置 I2C 速率为 100KHz
status = "okay"; // 记得开启 I2C1 控制器
/* 你的具体传感器挂在 i2c1 总线上 */
ap3216c@1e {
compatible = "alientek,ap3216c";
reg = <0x1e>; // 传感器的 I2C 从机设备地址
};
};
-
-
实战:从零编写一个控制 LED 灯的设备树节点。假设我们要为板子上的一个 GPIO 灯编写设备树,它连接在 GPIO5 的第 3 号引脚 上,高电平点亮。
-
第一步:检查引脚是否被别人复用
在修改设备树前,一定要确保这个引脚没有被其他外设(比如串口或网口)占用。在
.dts里全局搜索gpio5 3,如果有冲突,将其删除或把状态改为"disabled"。 -
第二步:编写 pinctrl 节点(配置引脚电气属性)
现代 SoC 要求先把引脚配置成 GPIO 功能,并设置上下拉电阻等特性。通常在
iomuxc节点下编写:
&iomuxc {
pinctrl_my_led: my_led_grp {
fsl,pins = <
/* 将 GPIO5_IO03 复用为 GPIO 功能,后面的一串十六进制是电气属性参数 */
MX6ULL_PAD_SNVS_TAMPER3__GPIO5_IO03 0x10B0
>;
};
}; -
第三步:在根节点下编写设备节点,回到根节点下,把刚才配好的引脚吃进来,并打上
compatible标签:/ {
my_gpio_led {
compatible = "user,my-gpio-led"; // 匹配驱动的暗号
status = "okay";pinctrl-names = "default";
pinctrl-0 = <&pinctrl_my_led>; // 绑定刚才写好的引脚配置led-gpio = <&gpio5 3 GPIO_ACTIVE_HIGH>; // 传入具体的引脚和有效电平
};
};
-
-
驱动程序(Driver)怎么读取你写的设备树?
设备树写好并编译好之后,驱动通过前面提到的 【方法二:设备树匹配】 成功进入probe函数。此时,驱动可以调用内核提供的of_系列 API 来提取你写在里面的参数:static int my_led_probe(struct platform_device *pdev)
{
struct device_node *node = pdev->dev.of_node; // 拿到设备树节点指针
int gpio_num;
// 1. 获取名为 "led-gpio" 的 GPIO 编号
gpio_num = of_get_named_gpio(node, "led-gpio", 0);
if (!gpio_is_valid(gpio_num)) {
dev_err(&pdev->dev, "无法获取有效的 GPIO 引脚\n");
return -EINVAL;
}
// 2. 申请并使用这个 GPIO
devm_gpio_request_one(&pdev->dev, gpio_num, GPIOF_OUT_INIT_LOW, "led_pin");
printk("驱动成功从设备树获取到引脚编号: %d,并初始化完成!\n", gpio_num);
return 0;
}
-
避坑黄金法则
-
少用新建,多用追加(
&) :芯片内部自带的外设(I2C, SPI, UART, EMMC),永远使用&外设名 { ... };的方式去覆盖和追加属性,不要在根节点重新发明轮子。 -
遵守命名规范 :节点名称通常采用
[设备类型]@[地址]的格式,例如ethernet@02188000或ap3216c@1e。如果是纯虚拟或独立的 GPIO 设备,通常直接用功能命名如my_led。
-
-
-
设备树在linux文件系统中的位置:
-
Linux 内核通过虚拟文件系统(
procfs和sysfs),将内存中的设备树以目录和文件的形式暴露了出来。你可以在板子的终端中通过以下两个路径找到它们:-
路径一:
/proc/device-tree/(最直观,查看节点和属性)这是最常用的路径。内核在这个目录下,将设备树的各个节点还原成了文件夹 ,将节点内部的属性还原成了文件。
-
路径二:
/sys/firmware/devicetree/base/(完全等价)在较新的 Linux 内核中,
/proc/device-tree实际上只是一个软链接(快捷方式),它指向的真实 sysfs 路径是:
-
-
这两个目录下的内容是完全一模一样的,你访问任意一个都可以。
-
-
设备树中的节点,能够被转换成platform_device的要求:
-
第一大类:根节点
/下的第一级子节点(重点)只要是紧跟在根节点
/下面的第一级子节点 ,并且该节点带有compatible属性,它就一定会 被转换成platform_device。示例:
/ {
/* 这是一个根节点下的第一级子节点,带 compatible,成功转换! */
my_led {
compatible = "gpio-leds";
gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
};
/* 这个节点没有 compatible 属性,忽略,不会转换 */
chosen {
bootargs = "earlycon console=ttymxc0,115200";
};
};
-
第二大类:特殊总线节点下的"子子节点"
如果一个节点不是根节点的第一级子节点,而是藏在某个大外设节点的肚子里,通常情况下它不会被转换。但是 ,如果这个大外设节点(父节点)的
compatible属性包含以下几个特殊的"总线标签"之一,那么它里面的所有子节点也都会被扫描并转换成platform_device:常见的特殊父节点
compatible属性值有:-
"simple-bus"(最常见,代表简单总线,比如芯片内部的片上系统 SoC 总线) -
"simple-mfd"(简单多功能设备) -
"isa" -
"arm,amba-bus"示例:
/ {
/* 父节点:在根线下,有 compatible,转换成 platform_device */
soc {
compatible = "simple-bus"; # 🌟 关键:因为有这个属性,内核会进到它肚子里面去扫描
#address-cells = <1>;
#size-cells = <1>;
/* 子子节点:虽然不是第一级子节点,但因为父节点是 "simple-bus",成功转换! */
my_timer@020bc000 {
compatible = "fsl,imx6ul-gpt";
reg = <0x020bc000 0x4000>;
};
};
};
-
-
第三大类:某些特定子系统软件触发的节点
有些节点本身在系统启动的默认扫描中被忽略了,但当对应的核心驱动加载后,驱动会手动调用内核 API ,强行把该节点或其子节点注册为
platform_device。例如 I2C / SPI 总线下的设备 :
正常情况下,挂在
&i2c1或&spi2节点下的子节点(如传感器、OLED 屏幕),在默认启动时绝对不会 被转换成platform_device,因为它们应该被转换成i2c_client或spi_device。但是,如果有些设备非常特殊,需要利用platform_device来管理(例如某些片选复杂的特殊片外总线),驱动中调用了of_platform_populate()或of_platform_device_create(),这些节点就会被强制转换。 -
哪些节点绝对【不会】被转换成 platform_device?
为了彻底理清边界,以下节点在启动时会被内核自动过滤,不会 生成
platform_device:-
没有
compatible属性的节点 。例如:chosen、aliases、memory等。 -
状态为禁用的节点 。只要节点里写了
status = "disabled";,内核直接视而不见。 -
已经归属于其他标准总线的子节点。
-
挂在
i2c控制器下的子节点 \\rightarrow 转换成struct i2c_client。 -
挂在
spi控制器下的子节点 \\rightarrow 转换成struct spi_device。 -
挂在
usb控制器下的子节点 \\rightarrow 转换成struct usb_device。
-
-
纯软件配置节点 。例如
pinctrl(引脚复用配置节点)、clocks(时钟树节点),它们只负责提供配置参数,由专门的子系统去解析,不属于平台设备。
-
-
一句话总结: 检查你的节点,如果它 在根节点下一级 或者 其父节点的
compatible里写着"simple-bus",只要它自己带compatible且没有被disabled,它就会变成一个platform_device。
-
-
I2C/SPI 子节点变成 platform_device方法:
-
为什么 I2C/SPI 子节点默认不会变成 platform_device?
在 Linux 内核看来,外设总线是有明确家族划分的:
-
平台总线(Platform Bus)管理
platform_device。 -
I2C 总线(I2C Bus)管理
i2c_client。 -
SPI 总线(SPI Bus)管理
spi_device。我们在前面提到过,内核启动时默认只会去扫描根节点
/以及带有"simple-bus"的通用片上总线。当内核扫描到i2c1控制器节点时,发现它的compatible可能是"fsl,imx6ul-i2c",属于平台设备。于是内核为i2c1自身创建了一个platform_device。但是,内核随即就会止步,绝对不会主动去碰i2c1内部的子节点(如传感器)。
/ {
soc {
compatible = "simple-bus";
i2c1: i2c@021a0000 {
compatible = "fsl,imx6ul-i2c"; /* 内核把它变成了 platform_device */
/* 🌲 默认扫描到此为止,下面的子节点被内核无视、跳过 */
ap3216c@1e {
compatible = "alientek,ap3216c"; /* 默认不会变成 platform_device */
};
};
};
};那它们正常是怎么被转换的?
当
i2c1控制器自己的驱动(I2C 总线核心驱动)加载并运行时,该驱动会自己去扫描自己肚子底下的子节点,然后调用i2c_new_client_device()等专用函数,把ap3216c节点转换成一个struct i2c_client(属于 I2C 总线),而不是platform_device。
-
-
重点:驱动怎么"手动调用 API 强行转换"?
既然正常流程下它们各回各家(变成
i2c_client),那什么情况下会发生你说的"强行注册为platform_device"呢?💡 核心场景:复合型芯片 / 多功能设备(MFD)
在复杂的嵌入式硬件中,有很多芯片是"二合一"甚至"多合一"的。 例如:你通过 I2C 总线连接了一个电源管理芯片(PMIC)或音频解码芯片(Codec)。这个芯片在物理上走 I2C 通信,但在逻辑上,它内部同时包含了:
-
一个电源调节器(Regulator)
-
一个时钟发生器(Clock)
-
一个音频控制接口(Audio Control)
对于这种复杂的复合芯片,Linux 社区的规范写法是:将该芯片整体注册为一个 I2C 设备;但它内部的各个子功能部件,应该抽象为独立的
platform_device,以便复用内核中现成的通用平台驱动。强行转换的设备树写法:
&i2c1 {
status = "okay";
/* 核心驱动主芯片:它首先会被 I2C 子系统正常转换为 i2c_client */
pmic@4b {
compatible = "rohm,bd71847";
reg = <0x4b>;
/* 🌟 重点:这些是藏在 I2C 节点底下的子子节点 */
/* 它们在系统启动时被默认忽略,完全不会变成任何设备 */
pmic_regulator {
compatible = "rohm,bd71847-regulator"; /* 我们希望它变成 platform_device */
};
pmic_clock {
compatible = "rohm,bd71847-clk"; /* 我们希望它变成 platform_device */
};
};
};
核心驱动中的"临门一脚"(手动调用 API)
当这个 PMIC 芯片的 I2C 驱动(核心驱动)匹配成功并进入
probe函数后,它会干一件史诗级的事情------主动帮它的子节点"逆天改命",强行把它们塞进平台总线里。驱动代码通常是这样写的:
static int bd71847_i2c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
int ret;
struct device *dev = &client->dev;
printk("1. PMIC 主芯片作为 I2C 设备加载成功!\n");
/* 🌟 核心 API:of_platform_populate
这个函数的作用是:强行让内核去扫描指定节点(当前 pmic 节点)底下的子节点,
不管它们身处何方(哪怕在 I2C 肚子里),只要带 compatible,统统强行转换成 platform_device!
*/
ret = devm_of_platform_populate(dev);
if (ret) {
dev_err(dev, "强行转换子节点失败\n");
return ret;
}
printk("2. 肚子里的 regulator 和 clk 子节点已被成功强行注册为 platform_device!\n");
return 0;
}
运行后的最终结果
当上面这段
of_platform_populate执行完毕后:-
藏在 I2C 节点下的
pmic_regulator节点,会原地变身,在内核中生成一个struct platform_device。 -
随后,内核的平台总线开始干活,拿着
"rohm,bd71847-regulator"这个兼容性字符串去匹配对侧的平台驱动。 -
最终,电源调节器的专属驱动(Platform Driver)被激活,开始工作。
-
-
-
总结
你读到的这句话,指的就是这种"父节点属于外设总线(如 I2C/SPI),但为了软件架构的解耦,由父节点的驱动在运行时手动调用
of_platform_populate(),强行将其子节点越级注册到平台总线中"的高级技术。
-
嵌入式linux学习记录四
bush42026-05-31 18:38
相关推荐
峥嵘life2 小时前
Android 蓝牙设备连接广播详解-2026lihao lihao2 小时前
软硬链接TOWE technology2 小时前
智能安防监控系统如何做好防雷?——视频信号SPD综合应用方案解析楼田莉子2 小时前
Docker学习:Docker介绍及其架构介绍YY&DS2 小时前
Qt 嵌入 CEF 在 Linux 下必须设置 `QT_XCB_GL_INTEGRATION=xcb_egl才能加载网页辰风沐阳2 小时前
ThinkPHP8.1 + think-swoole 4.1 使用指南(保姆级教程)星夜夏空993 小时前
FreeRTOS学习(7)——任务列表不羁的木木3 小时前
Form Kit(卡片开发服务)学习笔记01-核心概念与架构设计Mikowoo0073 小时前
神经网络 替代 线性模型_进行模型学习