3. I2C总线驱动开发:I2C核心层API、I2C适配器驱动、I2C设备驱动编写
好,咱们进入I2C总线驱动开发这一章。I2C这玩意儿,在机顶盒里太常见了——调电压的PMIC、读温度的传感器、控制HDMI的EDID,几乎都靠它。我个人习惯把I2C驱动分成三层来看:核心层、适配器层、设备层。搞懂了这三层,写驱动就跟搭积木一样。
3.1 I2C核心层API:你离不开的那些函数
核心层是Linux内核帮你封装好的。说白了,你不用管底层怎么发START信号、怎么ACK,直接用API就行。我刚开始做驱动时,总想自己操作寄存器,后来发现——嗯,内核大佬们已经搞定了,别重复造轮子。
常用的核心API就这几个:
- i2c_add_adapter() / i2c_del_adapter():注册和注销I2C适配器。适配器就是你的I2C控制器硬件。
- i2c_new_device() / i2c_unregister_device():动态添加或移除I2C设备。设备就是挂在总线上的从机。
- i2c_transfer():最底层的传输函数。一次调用可以发多个消息(读+写组合)。
- i2c_master_send() / i2c_master_recv():封装好的单次发送和接收。适合简单场景。
- i2c_smbus_read_byte_data() / i2c_smbus_write_byte_data():SMBus风格的读写。很多传感器都支持这种协议。
核心要点:i2c_transfer() 是万能钥匙。其他API底层都调它。你如果遇到奇葩设备,直接拼struct i2c_msg数组,用i2c_transfer()搞定。
举个例子,我写一个温度传感器的读操作:
#include <linux/i2c.h>
int temp_sensor_read(struct i2c_client *client, u8 reg, u8 *val)
{
struct i2c_msg msg[2];
int ret;
// 第一条消息:写寄存器地址
msg[0].addr = client->addr;
msg[0].flags = 0; // 写标志
msg[0].len = 1;
msg[0].buf = ®
// 第二条消息:读数据
msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD; // 读标志
msg[1].len = 1;
msg[1].buf = val;
ret = i2c_transfer(client->adapter, msg, 2);
if (ret < 0)
return ret;
return 0;
}
你看,代码很简洁。核心就是填充msg结构体,然后交给i2c_transfer。我在项目中遇到过一个问题:有些从机在写地址后需要一点延时才能读数据。这时候你可以在两条消息之间加个delay,或者用SMBus的读写组合函数。
3.2 I2C适配器驱动:让硬件跑起来
适配器驱动,说白了就是你的I2C控制器驱动。机顶盒里常见的I2C控制器有DesignWare、SUNXI、Rockchip等。适配器驱动的核心是填充一个struct i2c_algorithm结构体。
这个结构体里最重要的两个回调:
- master_xfer():实现I2C总线的数据传输。你要在这里操作控制器的寄存器,发送START、发送地址、读写数据、发送STOP。
- functionality():返回适配器支持的功能。比如是否支持10位地址、是否支持SMBus等。
我写一个简化的适配器驱动框架:
static int my_i2c_master_xfer(struct i2c_adapter *adap,
struct i2c_msg *msgs, int num)
{
int i;
// 遍历每个消息
for (i = 0; i < num; i++) {
// 发送START信号
// 发送从机地址 + 读写位
// 发送或接收数据
// 发送STOP信号
// 处理错误和重试
}
return num;
}
static u32 my_i2c_functionality(struct i2c_adapter *adap)
{
return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL;
}
static struct i2c_algorithm my_i2c_algo = {
.master_xfer = my_i2c_master_xfer,
.functionality = my_i2c_functionality,
};
注意:master_xfer() 里一定要处理好超时和错误重试。我曾经遇到一个坑——某款机顶盒的I2C控制器在总线忙时不会自动重试,导致系统卡死。后来我在xfer里加了忙等待和超时退出,才解决问题。
适配器注册的完整流程:
- 分配并初始化
struct i2c_adapter结构体 - 设置
algo指针指向你的算法结构体 - 设置
dev.parent指向你的平台设备 - 调用
i2c_add_adapter()注册
代码示例:
static int my_i2c_probe(struct platform_device *pdev)
{
struct i2c_adapter *adap;
int ret;
adap = kzalloc(sizeof(*adap), GFP_KERNEL);
if (!adap)
return -ENOMEM;
adap->owner = THIS_MODULE;
adap->class = I2C_CLASS_DEPRECATED;
adap->algo = &my_i2c_algo;
adap->dev.parent = &pdev->dev;
snprintf(adap->name, sizeof(adap->name), "my_i2c_adapter");
ret = i2c_add_adapter(adap);
if (ret) {
kfree(adap);
return ret;
}
platform_set_drvdata(pdev, adap);
return 0;
}
3.3 I2C设备驱动:跟从机对话
设备驱动就是你的从机驱动。比如温度传感器、EEPROM、音频Codec。设备驱动的核心是struct i2c_driver结构体。
关键字段:
- probe():设备匹配成功后调用。在这里初始化硬件、注册字符设备或输入设备。
- remove():设备移除时调用。释放资源。
- id_table:设备ID表。告诉内核这个驱动支持哪些设备。
我写一个温度传感器驱动的probe函数:
static int temp_sensor_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct temp_sensor_data *data;
int ret;
// 检查设备是否存在
ret = i2c_smbus_read_byte_data(client, 0x00);
if (ret < 0) {
dev_err(&client->dev, "device not present\n");
return -ENODEV;
}
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->client = client;
i2c_set_clientdata(client, data);
// 注册一个输入设备或者字符设备
// ...
dev_info(&client->dev, "temp sensor probed\n");
return 0;
}
static const struct i2c_device_id temp_sensor_id[] = {
{ "tmp102", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, temp_sensor_id);
static struct i2c_driver temp_sensor_driver = {
.driver = {
.name = "temp_sensor",
},
.probe = temp_sensor_probe,
.remove = temp_sensor_remove,
.id_table = temp_sensor_id,
};
module_i2c_driver(temp_sensor_driver);
小技巧:在probe里先读一个寄存器确认设备是否存在。我遇到过设备地址冲突导致probe成功但读写失败的情况。加个验证,能省很多调试时间。
3.4 设备树配置:让驱动和设备匹配
现在的机顶盒基本都是设备树(Device Tree)了。你需要在dts里描述I2C控制器和挂载的设备。
适配器节点示例:
&i2c0 {
status = "okay";
clock-frequency = <100000>; // 100KHz标准模式
temp_sensor: tmp102@48 {
compatible = "ti,tmp102";
reg = <0x48>;
};
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
pagesize = <8>;
};
};
驱动里通过of_match_table来匹配:
static const struct of_device_id temp_sensor_of_match[] = {
{ .compatible = "ti,tmp102" },
{ }
};
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,
},
// ...
};
3.5 调试与避坑指南
调试I2C驱动,我常用的工具:
- i2cdetect:扫描总线上的设备。看看地址对不对。
- i2cget / i2cset:手动读写寄存器。验证硬件是否正常。
- /sys/kernel/debug/i2c/:查看适配器和设备信息。
- 逻辑分析仪:抓波形。这是终极手段。
避坑指南:我曾经遇到一个很隐蔽的问题——某款机顶盒的I2C总线在系统休眠后时钟线被拉低,导致唤醒后总线卡死。后来发现是某个从机在休眠时没释放总线。解决办法是在resume回调里重新初始化控制器,发送一个STOP信号恢复总线。
常见问题汇总:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| i2cdetect扫不到设备 | 地址错误、上拉电阻缺失、控制器未使能 | 检查硬件连接,确认时钟频率 |
| 读写返回-ETIMEDOUT | 从机无应答、总线被占用 | 加超时重试,检查从机电源 |
| 数据错位 | 字节序问题、从机协议理解错误 | 用逻辑分析仪对比波形 |
嗯,I2C驱动开发其实不难。核心就是理解三层架构,把适配器驱动写对,设备驱动写稳。你想想看,机顶盒里那么多I2C设备,只要掌握了这套方法,换什么芯片都不怕。下一章咱们聊聊SPI总线,那个比I2C快,但协议更灵活。