4、I2C设备驱动编写:设备注册与注销、probe与remove回调、设备树节点解析
好,咱们今天聊聊I2C设备驱动的核心骨架。说白了,就是怎么让你的驱动被内核“认领”,怎么在设备接入时干活,怎么在设备移除时善后。这部分内容我当年刚入门时也绕了不少弯路,今天一次性给你讲透。
4.1 I2C设备驱动的注册与注销
每个I2C设备驱动,本质上就是一个i2c_driver结构体。你把它注册到内核,内核才知道“哦,原来有个驱动能管这个设备”。
注册和注销的接口很简单:
// 注册
int i2c_add_driver(struct i2c_driver *driver);
// 或者用这个宏,更省事
module_i2c_driver(my_i2c_driver);
// 注销
void i2c_del_driver(struct i2c_driver *driver);
我个人习惯用module_i2c_driver这个宏。它把module_init和module_exit都封装好了,代码看起来干净。你想想看,少写两行样板代码,出错的概率也小一点。
来看一个完整的驱动骨架:
static struct i2c_driver my_sensor_driver = {
.driver = {
.name = "my_sensor",
.of_match_table = my_sensor_of_match,
},
.probe = my_sensor_probe,
.remove = my_sensor_remove,
.id_table = my_sensor_id,
};
module_i2c_driver(my_sensor_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("My I2C Sensor Driver");
关键点:id_table和of_match_table至少得有一个。前者用于传统板级代码匹配,后者用于设备树匹配。现在主流都是用设备树,所以of_match_table基本是标配。
4.2 probe回调函数——驱动的“入场券”
probe函数是驱动和设备的“相亲现场”。内核发现一个设备与你的驱动匹配时,就会调用你的probe。这时候你要做几件事:
- 分配并初始化设备私有数据结构
- 获取硬件资源(中断号、GPIO、时钟等)
- 注册字符设备或输入设备等上层接口
- 初始化硬件(比如设置寄存器、校准传感器)
我写一个典型的probe函数给你看:
static int my_sensor_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct my_sensor_data *data;
int ret;
// 1. 分配私有数据
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
// 2. 保存client指针,后面读写要用
data->client = client;
i2c_set_clientdata(client, data);
// 3. 读取设备树中的配置
ret = device_property_read_u32(&client->dev, "measure-interval-ms",
&data->interval);
if (ret)
data->interval = 100; // 默认100ms
// 4. 初始化硬件
ret = my_sensor_hw_init(client);
if (ret) {
dev_err(&client->dev, "硬件初始化失败\n");
return ret;
}
// 5. 注册字符设备或输入设备
ret = my_sensor_register_dev(data);
if (ret)
return ret;
dev_info(&client->dev, "传感器驱动加载成功\n");
return 0;
}
我的经验:probe函数里尽量用devm_系列API(比如devm_kzalloc)。这些资源会在设备移除时自动释放,省得你在remove里一个个手动清理。我曾经因为漏掉一个kfree导致内存泄漏,排查了两天才找到原因。
4.3 remove回调函数——体面地退场
remove函数是probe的逆操作。当设备被拔出或驱动被卸载时,内核会调用它。你要做的就是把probe里申请的资源都释放掉。
static int my_sensor_remove(struct i2c_client *client)
{
struct my_sensor_data *data = i2c_get_clientdata(client);
// 1. 注销上层设备(字符设备、输入设备等)
my_sensor_unregister_dev(data);
// 2. 关闭硬件(比如进入低功耗模式)
my_sensor_power_down(client);
// 3. 如果用了devm_系列API,这里不用手动释放内存
// 但如果有手动申请的资源,记得释放
dev_info(&client->dev, "传感器驱动已卸载\n");
return 0;
}
注意:如果probe失败了,remove不会被调用。所以probe里如果中途出错,要自己把已经申请的资源释放干净。我建议用goto错误处理模式,清晰又安全。
4.4 I2C设备树节点解析
设备树是驱动和设备之间的“红娘”。你得在设备树里描述好设备的信息,驱动才能正确找到它。
一个典型的I2C设备节点长这样:
&i2c1 {
status = "okay";
clock-frequency = <400000>; // 400kHz快速模式
my_sensor: sensor@48 {
compatible = "mycompany,my-sensor";
reg = <0x48>; // I2C地址
interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
measure-interval-ms = <200>;
};
};
驱动里怎么解析这些属性呢?看代码:
static const struct of_device_id my_sensor_of_match[] = {
{ .compatible = "mycompany,my-sensor" },
{ /* 结束标记 */ }
};
MODULE_DEVICE_TABLE(of, my_sensor_of_match);
// 在probe中解析
static int my_sensor_parse_dt(struct i2c_client *client,
struct my_sensor_data *data)
{
struct device *dev = &client->dev;
u32 val;
int ret;
// 读取自定义属性
ret = device_property_read_u32(dev, "measure-interval-ms", &val);
if (ret == 0)
data->interval = val;
else
data->interval = 100; // 默认值
// 读取中断号(设备树中已描述)
data->irq = client->irq;
if (data->irq < 0) {
dev_warn(dev, "未配置中断,将使用轮询模式\n");
}
return 0;
}
避坑指南:我曾经在设备树里把reg地址写成了0x48,但实际芯片地址是0x48左移一位(7位地址)。结果驱动死活匹配不上,查了两天发现是设备树地址格式问题。记住:reg里填的是7位I2C地址,不要左移。
4.5 知识体系总览
下面这张图帮你理清整个I2C设备驱动的核心逻辑:
这张图把整个流程串起来了:设备树描述设备 → 内核匹配驱动 → probe初始化 → 正常工作 → remove清理。嗯,核心逻辑就这么简单。
4.6 完整示例:一个温度传感器驱动
最后给你一个完整的驱动示例,把上面所有知识点揉在一起:
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/of.h>
#include <linux/delay.h>
struct temp_sensor_data {
struct i2c_client *client;
int irq;
int interval_ms;
};
static int temp_sensor_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct temp_sensor_data *data;
int ret;
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->client = client;
i2c_set_clientdata(client, data);
// 解析设备树
device_property_read_u32(&client->dev, "measure-interval-ms",
&data->interval_ms);
data->irq = client->irq;
// 初始化硬件(示例:读取设备ID)
ret = i2c_smbus_read_byte_data(client, 0x0F);
if (ret < 0) {
dev_err(&client->dev, "读取设备ID失败\n");
return ret;
}
dev_info(&client->dev, "设备ID: 0x%02x\n", ret);
// 注册字符设备(略)
dev_info(&client->dev, "温度传感器就绪\n");
return 0;
}
static int temp_sensor_remove(struct i2c_client *client)
{
// 关闭硬件(示例:进入休眠模式)
i2c_smbus_write_byte_data(client, 0x01, 0x00);
dev_info(&client->dev, "温度传感器已关闭\n");
return 0;
}
static const struct of_device_id temp_sensor_of_match[] = {
{ .compatible = "mycompany,temp-sensor" },
{ }
};
MODULE_DEVICE_TABLE(of, temp_sensor_of_match);
static struct i2c_driver temp_sensor_driver = {
.driver = {
.name = "temp_sensor",
.of_match_table = temp_sensor_of_match,
},
.probe = temp_sensor_probe,
.remove = temp_sensor_remove,
};
module_i2c_driver(temp_sensor_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("I2C Temperature Sensor Driver");
我的建议:刚开始写I2C驱动时,先用i2c_smbus_*系列函数读写寄存器。这些函数封装了I2C协议细节,不容易出错。等熟悉了再考虑直接操作i2c_transfer。
好了,I2C设备驱动的核心骨架就这些。说白了就是:注册驱动 → 设备树匹配 → probe干活 → remove收尾。把这个流程刻在脑子里,写任何I2C驱动都不会跑偏。