4. I2C设备驱动编写实战:注册I2C从设备、probe/remove回调、设备树匹配与platform数据

在DRM显示驱动中,I2C总线主要用于连接DDC/CI、触摸控制器、背光调节器等外围从设备。本章将聚焦于I2C从设备驱动的标准编写流程,包括设备注册、probe/remove回调实现、设备树匹配规则以及platform数据的传递方式。掌握这些内容后,你将能够为显示子系统中的任何I2C从设备编写健壮的驱动。

4.1 I2C设备驱动核心结构

Linux内核中的I2C设备驱动遵循标准的总线-设备-驱动模型。驱动开发者需要实现一个i2c_driver结构体,并将其注册到I2C核心子系统。该结构体包含了驱动名称、ID表、probe/remove回调以及设备树匹配表等关键成员。

关键数据结构: struct i2c_driver 是I2C设备驱动的入口点,所有回调函数和匹配信息都封装在此结构中。
// i2c_driver 结构体核心成员
struct i2c_driver {
    unsigned int class;
    int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);
    int (*remove)(struct i2c_client *client);
    void (*shutdown)(struct i2c_client *client);
    struct device_driver driver;
    const struct i2c_device_id *id_table;
    const struct of_device_id *of_match_table;
    // ... 其他成员
};
i2c_driver 核心成员说明
成员类型作用
probe回调函数指针设备匹配成功后调用,完成硬件初始化、资源分配、注册子设备
remove回调函数指针设备移除或驱动卸载时调用,释放资源、注销设备
id_tablei2c_device_id 数组传统I2C设备ID匹配表,用于非设备树平台
of_match_tableof_device_id 数组设备树兼容性匹配表,用于设备树平台
driver.name字符串驱动名称,用于sysfs和内核日志标识
实战建议: 在DRM显示驱动中,I2C从设备驱动通常作为子模块存在,probe回调中会获取platform数据(如GPIO编号、I2C地址、时序参数),并注册到DRM子系统中。

4.2 设备树匹配与platform数据传递

设备树(Device Tree)是嵌入式Linux平台描述硬件拓扑的标准方式。I2C从设备在设备树中作为I2C控制器的子节点存在,驱动通过of_match_table与设备树节点进行匹配。匹配成功后,驱动可以从设备树节点中提取platform数据(如reg地址、中断号、GPIO、时钟频率等)。

I2C设备树匹配与platform数据传递流程 设备树节点 i2c@xxx { compatible = "vendor,dev"; reg = <0x36>; 解析 of_match_table .compatible = "vendor,dev" .data = &platform_cfg 匹配 probe() 回调执行 初始化设备 platform数据提取方式 of_device_get_match_data() / device_get_match_data() of_property_read_u32() / of_get_named_gpio() 获取数据 i2c_add_driver() 注册驱动 内核自动遍历I2C总线,匹配设备树节点
设备树匹配优先级: 内核首先尝试 of_match_table 进行设备树匹配,若失败则回退到 id_table 进行传统I2C ID匹配。在嵌入式系统中,强烈建议优先使用设备树方式。

4.3 probe/remove回调实现规范

probe回调是I2C设备驱动的核心,负责完成硬件初始化、资源分配、中断注册以及向其他子系统(如DRM、输入子系统)注册设备。remove回调则执行逆操作,确保资源完全释放。

probe回调标准流程

  1. 获取platform数据: 通过 of_device_get_match_data() 或 device_get_match_data() 获取设备树中定义的配置数据。
  2. 硬件初始化: 设置I2C从设备寄存器、配置工作模式、校验设备ID。
  3. 资源分配: 分配内存、获取GPIO、注册中断、创建内核线程或工作队列。
  4. 注册子设备: 向DRM核心注册I2C适配器、向输入子系统注册输入设备、向backlight子系统注册背光设备。
  5. 设置私有数据: 使用 i2c_set_clientdata() 将私有数据结构绑定到client。

remove回调标准流程

  1. 注销子设备: 从DRM/输入/backlight等子系统注销设备。
  2. 释放中断: 调用 free_irq() 释放注册的中断。
  3. 释放GPIO: 调用 gpio_free() 或 devm_gpio_free()。
  4. 释放内存: 释放私有数据结构及其他动态分配的内存。
  5. 关闭硬件: 将I2C从设备置于低功耗或复位状态。
// 完整的I2C设备驱动probe/remove示例
#include <linux/i2c.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/module.h>
#include <linux/gpio/consumer.h>

struct my_i2c_dev {
    struct i2c_client *client;
    struct gpio_desc *reset_gpio;
    int irq;
    u32 config_value;
};

static const struct of_device_id my_i2c_of_match[] = {
    { .compatible = "vendor,my-i2c-device", .data = &my_platform_cfg },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_i2c_of_match);

static int my_i2c_probe(struct i2c_client *client,
                        const struct i2c_device_id *id)
{
    struct device *dev = &client->dev;
    struct my_i2c_dev *priv;
    const struct my_platform_config *pdata;
    int ret;

    // 1. 获取platform数据(设备树匹配数据)
    pdata = of_device_get_match_data(dev);
    if (!pdata) {
        dev_err(dev, "No platform data found\n");
        return -EINVAL;
    }

    // 2. 分配私有数据结构
    priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    priv->client = client;
    priv->config_value = pdata->default_config;
    i2c_set_clientdata(client, priv);

    // 3. 获取GPIO资源(使用devm自动管理)
    priv->reset_gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_LOW);
    if (IS_ERR(priv->reset_gpio)) {
        ret = PTR_ERR(priv->reset_gpio);
        dev_err(dev, "Failed to get reset GPIO: %d\n", ret);
        return ret;
    }

    // 4. 读取设备树属性
    ret = of_property_read_u32(dev->of_node, "vendor,config-val",
                               &priv->config_value);
    if (ret)
        dev_warn(dev, "Using default config value: %u\n", priv->config_value);

    // 5. 硬件初始化:校验设备ID
    ret = i2c_smbus_read_byte_data(client, REG_DEVICE_ID);
    if (ret < 0) {
        dev_err(dev, "Failed to read device ID\n");
        return ret;
    }
    if (ret != EXPECTED_DEVICE_ID) {
        dev_err(dev, "Unexpected device ID: 0x%02x\n", ret);
        return -ENODEV;
    }

    // 6. 注册中断(使用devm自动管理)
    priv->irq = client->irq;
    if (priv->irq > 0) {
        ret = devm_request_threaded_irq(dev, priv->irq, NULL,
                                        my_i2c_irq_handler,
                                        IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
                                        "my_i2c_dev", priv);
        if (ret) {
            dev_err(dev, "Failed to request IRQ %d: %d\n", priv->irq, ret);
            return ret;
        }
    }

    // 7. 向DRM子系统注册(示例)
    // ret = drm_xxx_register(dev, priv);
    // if (ret) return ret;

    dev_info(dev, "I2C device probed successfully (config=0x%x)\n",
             priv->config_value);
    return 0;
}

static int my_i2c_remove(struct i2c_client *client)
{
    struct my_i2c_dev *priv = i2c_get_clientdata(client);

    // 1. 从DRM子系统注销
    // drm_xxx_unregister(priv);

    // 2. 关闭硬件:复位设备
    if (priv->reset_gpio)
        gpiod_set_value_cansleep(priv->reset_gpio, 1);

    // 3. devm框架自动释放GPIO、中断、内存
    dev_info(&client->dev, "I2C device removed\n");
    return 0;
}

static const struct i2c_device_id my_i2c_id[] = {
    { "my-i2c-device", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, my_i2c_id);

static struct i2c_driver my_i2c_driver = {
    .driver = {
        .name = "my_i2c_driver",
        .of_match_table = my_i2c_of_match,
    },
    .probe = my_i2c_probe,
    .remove = my_i2c_remove,
    .id_table = my_i2c_id,
};

module_i2c_driver(my_i2c_driver);

MODULE_AUTHOR("DRM Developer");
MODULE_DESCRIPTION("Example I2C device driver for DRM subsystem");
MODULE_LICENSE("GPL");
关键注意事项:
  • 优先使用 devm_ 系列API(如 devm_kzalloc、devm_gpiod_get、devm_request_threaded_irq),这些资源会在设备解绑时自动释放,减少remove回调中的手动清理工作。
  • probe回调中如果失败,必须确保已分配的资源全部释放。使用devm API可以简化错误处理路径。
  • I2C通信使用 i2c_smbus_read/write_byte_data() 或 i2c_transfer() 接口,注意在probe中避免长时间阻塞。

4.4 设备树节点编写与匹配规则

设备树节点是I2C从设备驱动的硬件描述入口。正确的设备树节点编写能够确保驱动与硬件正确匹配,并传递必要的platform数据。

I2C从设备树节点关键属性
属性类型说明示例
compatible字符串驱动匹配标识,必须与 of_match_table 中的值一致"vendor,my-i2c-device"
regu32数组I2C从设备地址(7位或10位)<0x36>
interruptsu32数组中断号及触发类型<12 IRQ_TYPE_EDGE_FALLING>
reset-gpiosGPIO描述复位引脚GPIO<&gpio1 15 GPIO_ACTIVE_LOW>
vendor,config-valu32自定义配置参数(驱动通过 of_property_read_u32 读取)<0x100>
// 设备树节点示例(位于I2C控制器节点下)
&i2c1 {
    status = "okay";
    clock-frequency = <400000>;  // 400kHz快速模式

    my_i2c_device: my-device@36 {
        compatible = "vendor,my-i2c-device";
        reg = <0x36>;                    // 7位I2C地址
        interrupt-parent = <&gpio0>;
        interrupts = <12 IRQ_TYPE_EDGE_FALLING>;
        reset-gpios = <&gpio1 15 GPIO_ACTIVE_LOW>;
        vendor,config-val = <0x100>;     // 自定义配置
    };
};
匹配规则: 内核I2C核心在注册驱动时,会遍历I2C总线上的所有设备节点,将设备节点的 compatible 属性与驱动的 of_match_table 中的 .compatible 字段进行字符串匹配。匹配成功后,调用驱动的probe回调,并将 of_match_table 中对应的 .data 指针传递给驱动。

4.5 实战调试要点

  • I2C地址验证: 使用 i2cdetect -y <bus> 命令确认从设备地址是否正确,避免设备树中reg地址与实际硬件不符。
  • probe失败排查: 检查 dmesg 输出,确认probe回调是否被调用。常见失败原因包括:compatible字符串不匹配、I2C通信超时、GPIO获取失败。
  • 设备树覆盖: 使用 dtc -I dtb -O dts 反编译设备树二进制文件,确认节点属性是否正确加载。
  • 资源泄漏检测: 在remove回调中添加调试日志,确认所有资源被正确释放。使用 kmemleak 工具检测内存泄漏。
调试命令速查:
  • i2cdetect -l — 列出所有I2C总线
  • i2cdetect -y <bus> 0x00-0x7f — 扫描总线上的设备地址
  • i2cget -y <bus> <addr> <reg> — 读取寄存器值
  • i2cset -y <bus> <addr> <reg> <val> — 写入寄存器值
  • cat /sys/kernel/debug/gpio — 查看GPIO状态
I2C设备驱动调试流程 1. 确认设备树 dtc反编译检查 2. 扫描I2C地址 i2cdetect -y <bus> 3. 加载驱动模块 insmod / modprobe 4. 检查dmesg probe日志 probe失败?检查compatible/reg/GPIO 修正设备树或驱动代码后重新加载 失败 probe成功 → 继续DRM子系统集成 成功

4.6 总结

本章详细介绍了I2C设备驱动的完整编写流程,从核心数据结构、设备树匹配规则、probe/remove回调实现到调试方法。在DRM显示驱动开发中,I2C从设备驱动是连接显示控制器与外围器件(如DDC、触摸屏、背光)的关键桥梁。掌握本章内容后,你将能够独立为任何I2C接口的显示外围设备编写健壮的Linux内核驱动,并能够通过设备树灵活配置硬件参数。