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

嵌入式 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-gpiostemp-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 跨平台适配核心原则

  1. 驱动与硬件信息解耦:驱动中所有硬件相关参数(地址、GPIO、中断)均通过设备树解析获取,禁止硬编码任何平台相关值。

  2. 使用标准属性与兼容命名 :优先使用内核定义的标准属性(如 compatiblereg),自定义属性需明确文档说明,兼容属性命名遵循 "厂商,设备型号" 格式,确保不同平台节点一致性。

  3. 依赖总线抽象层 :基于内核总线抽象层(如 I2C、SPI、GPIO 子系统)编写驱动,避免直接操作硬件寄存器,总线层会适配不同平台的底层差异。

5.2 ARM32 与 ARM64 平台适配实践

以 TMP102 驱动为例,适配 ARM32(如 Cortex-A9)与 ARM64(如 Cortex-A53)平台的步骤如下:

  1. 平台设备树调整

    • ARM32 平台:在对应 I2C 控制器节点下添加 tmp102 节点,指定该平台的 GPIO 引脚(如 &gpio1 20)。

    • ARM64 平台:在其设备树中,同样在对应 I2C 控制器节点下添加 tmp102 节点,仅需修改 temp-alert-gpios 为该平台的 GPIO 引脚(如 &gpio2 15),其他属性(compatiblereg)保持不变。

  2. 驱动源码复用:驱动代码无需任何修改,内核在 ARM32/ARM64 平台加载时,会通过设备树匹配表找到对应的节点,解析该平台的硬件资源,完成初始化。

  3. 引脚配置适配 :不同平台的 I2C 引脚、GPIO 引脚定义不同,需在各自设备树的 pinctrl 节点中配置对应引脚的复用功能(如将引脚设为 I2C 模式或 GPIO 模式),驱动无需感知引脚配置差异。

5.3 跨平台适配注意事项

  • 字节序问题:ARM 平台为小端序,若外设采用大端序,需在驱动中通过 cpu_to_be16be16_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

💬 座右铭 : "向光而行,沐光而生。"

相关推荐
豆是浪个2 小时前
Linux(Centos 7.6)命令详解:ps
linux·windows·centos
乾元2 小时前
ISP 级别的异常洪泛检测与防护——大流量事件的 AI 自动识别与响应工程
运维·网络·人工智能·安全·web安全·架构
Run_Teenage2 小时前
Linux:深刻理解缓冲区
linux
youxiao_903 小时前
kubernetes 概念与安装(一)
linux·运维·服务器
凡梦千华3 小时前
logrotate日志切割
linux·运维·服务器
wdfk_prog3 小时前
[Linux]学习笔记系列 -- [fs][proc]
linux·笔记·学习
ELI_He9994 小时前
Airflow docker 部署
运维·docker·容器
拜托啦!狮子4 小时前
安装和使用Homer(linux)
linux·运维·服务器
liulilittle4 小时前
XDP VNP虚拟以太网关(章节:一)
linux·服务器·开发语言·网络·c++·通信·xdp
Sapphire~4 小时前
Linux-13 火狐浏览器书签丢失解决
linux