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;
// ... 其他成员
};
| 成员 | 类型 | 作用 |
|---|---|---|
probe | 回调函数指针 | 设备匹配成功后调用,完成硬件初始化、资源分配、注册子设备 |
remove | 回调函数指针 | 设备移除或驱动卸载时调用,释放资源、注销设备 |
id_table | i2c_device_id 数组 | 传统I2C设备ID匹配表,用于非设备树平台 |
of_match_table | of_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、时钟频率等)。
设备树匹配优先级: 内核首先尝试
of_match_table 进行设备树匹配,若失败则回退到 id_table 进行传统I2C ID匹配。在嵌入式系统中,强烈建议优先使用设备树方式。
4.3 probe/remove回调实现规范
probe回调是I2C设备驱动的核心,负责完成硬件初始化、资源分配、中断注册以及向其他子系统(如DRM、输入子系统)注册设备。remove回调则执行逆操作,确保资源完全释放。
probe回调标准流程
- 获取platform数据: 通过
of_device_get_match_data()或device_get_match_data()获取设备树中定义的配置数据。 - 硬件初始化: 设置I2C从设备寄存器、配置工作模式、校验设备ID。
- 资源分配: 分配内存、获取GPIO、注册中断、创建内核线程或工作队列。
- 注册子设备: 向DRM核心注册I2C适配器、向输入子系统注册输入设备、向backlight子系统注册背光设备。
- 设置私有数据: 使用
i2c_set_clientdata()将私有数据结构绑定到client。
remove回调标准流程
- 注销子设备: 从DRM/输入/backlight等子系统注销设备。
- 释放中断: 调用
free_irq()释放注册的中断。 - 释放GPIO: 调用
gpio_free()或devm_gpio_free()。 - 释放内存: 释放私有数据结构及其他动态分配的内存。
- 关闭硬件: 将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数据。
| 属性 | 类型 | 说明 | 示例 |
|---|---|---|---|
compatible | 字符串 | 驱动匹配标识,必须与 of_match_table 中的值一致 | "vendor,my-i2c-device" |
reg | u32数组 | I2C从设备地址(7位或10位) | <0x36> |
interrupts | u32数组 | 中断号及触发类型 | <12 IRQ_TYPE_EDGE_FALLING> |
reset-gpios | GPIO描述 | 复位引脚GPIO | <&gpio1 15 GPIO_ACTIVE_LOW> |
vendor,config-val | u32 | 自定义配置参数(驱动通过 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状态
4.6 总结
本章详细介绍了I2C设备驱动的完整编写流程,从核心数据结构、设备树匹配规则、probe/remove回调实现到调试方法。在DRM显示驱动开发中,I2C从设备驱动是连接显示控制器与外围器件(如DDC、触摸屏、背光)的关键桥梁。掌握本章内容后,你将能够独立为任何I2C接口的显示外围设备编写健壮的Linux内核驱动,并能够通过设备树灵活配置硬件参数。