嵌入式 Linux 驱动开发:从设备树配置到跨平台适配

🌸你好呀!我是 lbb小魔仙
🌟 感谢陪伴~ 小白博主在线求友
🌿 跟着小白学Linux/Java/Python
📖 专栏汇总:
《Linux》专栏 | 《Java》专栏 | 《Python》专栏

- [嵌入式 Linux 驱动开发:从设备树配置到跨平台适配](#嵌入式 Linux 驱动开发:从设备树配置到跨平台适配)
-
- [一、设备树(Device Tree)的作用与基本结构](#一、设备树(Device Tree)的作用与基本结构)
-
- [1.1 设备树的核心作用](#1.1 设备树的核心作用)
- [1.2 设备树的基本结构](#1.2 设备树的基本结构)
- [二、自定义 I2C 温度传感器的设备树节点配置](#二、自定义 I2C 温度传感器的设备树节点配置)
-
- [2.1 设备树节点源码(.dts)](#2.1 设备树节点源码(.dts))
- [2.2 节点配置说明](#2.2 节点配置说明)
- [三、Linux 内核驱动代码实现(设备树匹配与资源初始化)](#三、Linux 内核驱动代码实现(设备树匹配与资源初始化))
-
- [3.1 驱动代码源码(.c)](#3.1 驱动代码源码(.c))
- [四、驱动中关键 API 的使用分析](#四、驱动中关键 API 的使用分析)
-
- [4.1 设备树解析 API](#4.1 设备树解析 API)
-
- [4.1.1 of_get_named_gpio_flags](#4.1.1 of_get_named_gpio_flags)
- [4.1.2 of_property_read_s32](#4.1.2 of_property_read_s32)
- [4.2 I2C 总线操作 API](#4.2 I2C 总线操作 API)
-
- [4.2.1 i2c_smbus_write_byte_data](#4.2.1 i2c_smbus_write_byte_data)
- [4.2.2 i2c_smbus_read_byte_data](#4.2.2 i2c_smbus_read_byte_data)
- [4.3 驱动匹配与注册 API](#4.3 驱动匹配与注册 API)
- 五、基于设备树的跨平台适配方案
-
- [5.1 跨平台适配核心原则](#5.1 跨平台适配核心原则)
- [5.2 ARM32 与 ARM64 平台适配实践](#5.2 ARM32 与 ARM64 平台适配实践)
- [5.3 跨平台适配注意事项](#5.3 跨平台适配注意事项)
- 六、驱动加载完整流程图(Mermaid)
- 七、总结
在嵌入式 Linux 驱动开发领域,设备树(Device Tree)的引入彻底改变了传统驱动与硬件信息耦合的局面,为跨平台驱动复用提供了核心支撑。本文将从设备树的核心作用与结构出发,结合自定义 I2C 温度传感器的实战案例,详解设备树配置、驱动代码实现、关键 API 解析及跨平台适配技巧,帮助中级嵌入式开发者构建灵活可复用的驱动程序。
一、设备树(Device Tree)的作用与基本结构
1.1 设备树的核心作用
在设备树出现之前,嵌入式 Linux 驱动需通过硬编码方式将硬件信息(如外设地址、中断号、GPIO 引脚)写入驱动代码,导致驱动与特定硬件强耦合。当驱动移植到不同硬件平台时,需修改驱动源码中的硬件参数,维护成本极高。
设备树的核心价值的是分离硬件描述与驱动逻辑:将所有硬件平台相关的信息(外设节点、资源分配、总线拓扑)集中描述在设备树文件(.dts)中,驱动程序通过解析设备树动态获取硬件资源,无需修改驱动源码即可适配不同硬件平台,大幅提升驱动的可移植性。
1.2 设备树的基本结构
设备树以树形结构描述硬件,核心组成包括根节点、子节点、属性及属性值,其语法遵循 Device Tree Source (DTS) 规范,最终通过编译器(dtc)编译为 Device Tree Blob (DTB) 文件,由内核加载解析。
核心结构要素如下:
-
根节点:设备树的顶层节点,所有硬件节点均为其子孙节点,节点名为 "/"。
-
子节点:对应具体的硬件设备(如 CPU、I2C 控制器、温度传感器),节点名通常遵循 "设备类型@基地址" 格式(如 i2c@12345678),基地址用于唯一标识节点。
-
属性 :描述节点的硬件特性,由键值对组成。常见标准属性包括
compatible(用于驱动匹配)、reg(外设寄存器地址范围)、interrupts(中断配置)、status(设备使能状态)等,也可自定义属性。 -
总线节点:如 I2C、SPI、GPIO 等总线控制器节点,子节点对应总线上挂载的外设,需通过总线属性关联。
一个简化的设备树结构示例如下:
dts
/ {
#address-cells = <1>; // 地址字段长度(32位)
#size-cells = <1>; // 长度字段长度(32位)
cpus { /* CPU 节点 */ };
memory { /* 内存节点 */ };
i2c@12345678 { // I2C 控制器节点
compatible = "vendor,i2c-controller";
reg = <0x12345678 0x100>; // 地址0x12345678,长度256字节
status = "okay";
temp_sensor@48 { // I2C 总线上的温度传感器节点
compatible = "vendor,temp-sensor";
reg = <0x48>; // 传感器 I2C 从设备地址
};
};
};
二、自定义 I2C 温度传感器的设备树节点配置
以常见的 I2C 温度传感器(如 TMP102)为例,需在设备树中添加对应节点,描述传感器的 I2C 从地址、兼容属性、GPIO 告警引脚(可选)等信息。以下为完整的设备树节点配置示例,基于 ARM 平台的 I2C 总线编写。
2.1 设备树节点源码(.dts)
dts
/* 假设 I2C 控制器节点已在核心设备树中定义,路径为 /soc/i2c@11000000 */
&i2c1 { // 引用 I2C1 控制器节点(通过 &符号引用别名节点)
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&i2c1_pins>; // 绑定 I2C1 引脚配置(需在 pinctrl 节点定义)
/* 自定义 TMP102 温度传感器节点 */
tmp102@48 {
compatible = "ti,tmp102"; // 兼容属性,驱动通过此属性匹配
reg = <0x48>; // TMP102 默认 I2C 从地址(可通过硬件引脚调整)
status = "okay";
/* 自定义属性:告警 GPIO 引脚(可选,用于温度超限中断) */
temp-alert-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>; // GPIO1_20,低电平有效
/* 自定义属性:温度阈值配置(单位:毫摄氏度) */
temp-high-threshold = <30000>; // 高温阈值 30℃
temp-low-threshold = <10000>; // 低温阈值 10℃
};
};
2.2 节点配置说明
-
&i2c1:通过引用符号(&)访问 I2C1 控制器节点,避免重复定义总线控制器,符合设备树的复用原则。 -
compatible:核心属性,值为 "ti,tmp102",前半部分为厂商标识,后半部分为设备型号,驱动程序的 of_match_table 需包含此值以完成匹配。 -
reg:I2C 从设备地址,TMP102 的默认地址为 0x48(根据 A0/A1 引脚电平可调整为 0x49/0x4A/0x4B)。 -
自定义属性:
temp-alert-gpios、temp-high-threshold等为非标准属性,需驱动程序通过专门的 API 解析,用于传递硬件配置信息。
三、Linux 内核驱动代码实现(设备树匹配与资源初始化)
基于上述设备树节点,编写对应的 I2C 驱动程序,核心流程为:通过 of_match_table 匹配设备树节点 → 解析节点属性获取资源(GPIO、阈值参数)→ 初始化 I2C 客户端 → 注册设备驱动。以下为完整的驱动代码片段,适配 Linux 内核 5.x 版本。
3.1 驱动代码源码(.c)
c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>
#include <linux/delay.h>
/* 设备私有数据结构,存储资源信息与状态 */
struct tmp102_data {
struct i2c_client *client; // I2C 客户端指针
int alert_gpio; // 告警 GPIO 引脚号
int high_thresh; // 高温阈值(毫摄氏度)
int low_thresh; // 低温阈值(毫摄氏度)
};
/* 设备树匹配表,与节点 compatible 属性对应 */
static const struct of_device_id tmp102_of_match[] = {
{ .compatible = "ti,tmp102" },
{ /* Sentinel */ }
};
MODULE_DEVICE_TABLE(of, tmp102_of_match);
/* I2C 驱动探针函数,设备树匹配成功后执行 */
static int tmp102_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
struct tmp102_data *data;
struct device_node *node = client->dev.of_node;
int ret;
/* 分配私有数据内存 */
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->client = client;
i2c_set_clientdata(client, data); // 绑定私有数据到 I2C 客户端
/* 1. 解析设备树自定义属性:温度阈值 */
ret = of_property_read_s32(node, "temp-high-threshold", &data->high_thresh);
if (ret) {
dev_warn(&client->dev, "no high threshold, use default 30000\n");
data->high_thresh = 30000;
}
ret = of_property_read_s32(node, "temp-low-threshold", &data->low_thresh);
if (ret) {
dev_warn(&client->dev, "no low threshold, use default 10000\n");
data->low_thresh = 10000;
}
/* 2. 解析告警 GPIO 引脚(of_get_named_gpio 核心 API) */
data->alert_gpio = of_get_named_gpio_flags(node, "temp-alert-gpios", 0, NULL);
if (!gpio_is_valid(data->alert_gpio)) {
dev_warn(&client->dev, "invalid alert gpio\n");
data->alert_gpio = -1;
} else {
/* 请求 GPIO 资源并配置为输入模式 */
ret = devm_gpio_request(&client->dev, data->alert_gpio, "tmp102-alert");
if (ret) {
dev_err(&client->dev, "gpio request failed\n");
return ret;
}
gpio_direction_input(data->alert_gpio);
}
/* 3. 初始化传感器(通过 I2C 总线写入配置寄存器) */
// 写入高温阈值(TMP102 阈值寄存器为 16 位,高字节在前)
ret = i2c_smbus_write_byte_data(client, 0x02, (data->high_thresh / 256) & 0xFF);
if (ret < 0) {
dev_err(&client->dev, "write high thresh failed\n");
return ret;
}
ret = i2c_smbus_write_byte_data(client, 0x03, (data->high_thresh % 256) & 0xFF);
if (ret < 0) {
dev_err(&client->dev, "write high thresh lsb failed\n");
return ret;
}
dev_info(&client->dev, "tmp102 probe success, high:%d mC, low:%d mC\n",
data->high_thresh, data->low_thresh);
return 0;
}
/* I2C 驱动移除函数,设备卸载时执行 */
static int tmp102_remove(struct i2c_client *client)
{
dev_info(&client->dev, "tmp102 remove\n");
return 0;
}
/* I2C 设备 ID 表(兼容传统非设备树匹配方式) */
static const struct i2c_device_id tmp102_id[] = {
{ "tmp102", 0 },
{ /* Sentinel */ }
};
MODULE_DEVICE_TABLE(i2c, tmp102_id);
/* I2C 驱动结构体 */
static struct i2c_driver tmp102_driver = {
.probe = tmp102_probe,
.remove = tmp102_remove,
.id_table = tmp102_id,
.driver = {
.name = "tmp102",
.of_match_table = tmp102_of_match, // 关联设备树匹配表
.owner = THIS_MODULE,
},
};
module_i2c_driver(tmp102_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("TMP102 I2C Temperature Sensor Driver with Device Tree");
MODULE_AUTHOR("Embedded Developer");
四、驱动中关键 API 的使用分析
上述驱动代码中用到了多个设备树解析与 I2C 总线操作的核心 API,以下针对关键函数进行详细说明,明确其功能、参数及使用场景。
4.1 设备树解析 API
4.1.1 of_get_named_gpio_flags
功能:从设备树节点中解析指定名称的 GPIO 属性,获取 GPIO 引脚号及配置标志。
c
int of_get_named_gpio_flags(struct device_node *np, const char *propname,
int index, enum of_gpio_flags *flags);
-
np:设备树节点指针(从 I2C 客户端的 dev.of_node 获取)。 -
propname:GPIO 属性名(如 "temp-alert-gpios")。 -
index:当属性值为多个 GPIO 时的索引(单 GPIO 取 0)。 -
flags:输出参数,存储 GPIO 配置标志(如电平极性),可设为 NULL。 -
返回值:成功返回 GPIO 引脚号(大于等于 0),失败返回负数错误码。
注意:解析后需通过 gpio_is_valid() 验证引脚号有效性,再通过 devm_gpio_request() 请求 GPIO 资源,避免资源冲突。
4.1.2 of_property_read_s32
功能:从设备树节点中读取 32 位有符号整数类型的属性值,适用于解析自定义数值属性(如温度阈值)。
类似 API 还有 of_property_read_u32(无符号)、of_property_read_string(字符串)等,根据属性类型选择使用。
4.2 I2C 总线操作 API
4.2.1 i2c_smbus_write_byte_data
功能:通过 I2C 总线向从设备的指定寄存器写入一个字节数据,适用于大多数 I2C 外设的配置与数据写入。
c
s32 i2c_smbus_write_byte_data(struct i2c_client *client, u8 cmd, u8 data);
-
client:I2C 从设备客户端指针(由探针函数传入)。 -
cmd:寄存器地址(如 TMP102 的高温阈值寄存器地址 0x02)。 -
data:要写入的字节数据。 -
返回值:成功返回 0,失败返回负数错误码(如 -EIO 表示 I2C 通信失败)。
4.2.2 i2c_smbus_read_byte_data
功能:从 I2C 从设备的指定寄存器读取一个字节数据,可用于读取传感器数据(如 TMP102 的温度寄存器高位字节)。
c
s32 i2c_smbus_read_byte_data(struct i2c_client *client, u8 cmd);
若需读取 16 位数据(如 TMP102 的 12 位温度值),可组合调用两次该函数,或使用 i2c_smbus_read_word_data(注意字节序转换)。
4.3 驱动匹配与注册 API
of_device_id 结构体与 of_match_table 是设备树匹配的核心:驱动通过 of_match_table 列出支持的设备树节点兼容属性,内核解析设备树时,若节点的 compatible 属性与表中值一致,则触发驱动的 probe 函数。
五、基于设备树的跨平台适配方案
设备树的核心优势之一是支持驱动跨平台复用,无需修改驱动源码,仅需调整设备树节点配置,即可实现同一驱动在 ARM32、ARM64 等不同平台的适配。以下为具体适配原则与实践方法。
5.1 跨平台适配核心原则
-
驱动与硬件信息解耦:驱动中所有硬件相关参数(地址、GPIO、中断)均通过设备树解析获取,禁止硬编码任何平台相关值。
-
使用标准属性与兼容命名 :优先使用内核定义的标准属性(如
compatible、reg),自定义属性需明确文档说明,兼容属性命名遵循 "厂商,设备型号" 格式,确保不同平台节点一致性。 -
依赖总线抽象层 :基于内核总线抽象层(如 I2C、SPI、GPIO 子系统)编写驱动,避免直接操作硬件寄存器,总线层会适配不同平台的底层差异。

5.2 ARM32 与 ARM64 平台适配实践
以 TMP102 驱动为例,适配 ARM32(如 Cortex-A9)与 ARM64(如 Cortex-A53)平台的步骤如下:
-
平台设备树调整:
-
ARM32 平台:在对应 I2C 控制器节点下添加 tmp102 节点,指定该平台的 GPIO 引脚(如 &gpio1 20)。
-
ARM64 平台:在其设备树中,同样在对应 I2C 控制器节点下添加 tmp102 节点,仅需修改
temp-alert-gpios为该平台的 GPIO 引脚(如 &gpio2 15),其他属性(compatible、reg)保持不变。
-
-
驱动源码复用:驱动代码无需任何修改,内核在 ARM32/ARM64 平台加载时,会通过设备树匹配表找到对应的节点,解析该平台的硬件资源,完成初始化。
-
引脚配置适配 :不同平台的 I2C 引脚、GPIO 引脚定义不同,需在各自设备树的
pinctrl节点中配置对应引脚的复用功能(如将引脚设为 I2C 模式或 GPIO 模式),驱动无需感知引脚配置差异。
5.3 跨平台适配注意事项
-
字节序问题:ARM 平台为小端序,若外设采用大端序,需在驱动中通过
cpu_to_be16、be16_to_cpu等函数进行字节序转换。 -
GPIO 编号规则:不同平台的 GPIO 编号方式可能不同(如 GPIO 编号=控制器编号×32 + 引脚号),但通过
of_get_named_gpio可统一获取内核分配的全局 GPIO 号,无需关心底层编号规则。 -
中断控制器差异:若外设使用中断,需通过设备树
interrupts属性描述中断信息,内核中断子系统会适配不同平台的中断控制器(如 GICv2/GICv3)。
六、驱动加载完整流程图(Mermaid)
以下通过 Mermaid 流程图展示 "设备树解析 → 驱动匹配 → 资源映射 → 设备注册" 的完整流程,清晰呈现驱动与设备树的交互逻辑。
加载 DTB 文件
不匹配
匹配
内核启动
解析设备树,构建硬件拓扑
遍历 I2C 外设节点
节点 compatible 属性匹配?
跳过该节点,继续遍历
触发 I2C 驱动 probe 函数
分配驱动私有数据结构
解析设备树属性:GPIO、阈值参数
请求 GPIO 资源,配置引脚模式
通过 I2C API 初始化传感器(写配置寄存器)
绑定私有数据到 I2C 客户端
设备注册完成,驱动正常工作
设备卸载时,触发 remove 函数
释放资源,注销设备
七、总结
本文围绕嵌入式 Linux 驱动开发中的设备树应用与跨平台适配,从理论到实战进行了全面讲解。设备树通过分离硬件描述与驱动逻辑,为跨平台驱动复用提供了核心支撑,而基于 of_match_table 的匹配机制、设备树属性解析 API 及总线抽象层的合理使用,是构建灵活可复用驱动的关键。
在实际开发中,需严格遵循设备树规范,确保驱动与硬件信息解耦,通过调整设备树节点而非修改驱动源码,实现同一驱动在不同平台的适配。同时,熟练掌握设备树解析与总线操作 API,结合内核子系统的抽象能力,可大幅提升驱动开发效率与可维护性。
📕个人领域 :Linux/C++/java/AI
🚀 个人主页 :有点流鼻涕 · CSDN
💬 座右铭 : "向光而行,沐光而生。"
