第4章 I2C总线驱动开发:从协议到实战
I2C总线,说白了就是光模块里最常用的通信方式。我刚开始做光通信芯片驱动时,第一个要啃的硬骨头就是它。你想想看,光模块里的DDM监控、激光器偏置电流设置、温度补偿参数读写,全得靠这条两线制的总线来完成。
这一章,我会从协议细节讲到Linux驱动框架,再到实际调试中的那些坑。嗯,都是我在项目里真金白银换来的经验。
4.1 I2C协议详解:别小看这两根线
I2C只有两根线:SCL(时钟)和SDA(数据)。但就是这两根线,藏着不少门道。
物理层要点:
- 开漏输出,需要上拉电阻(4.7kΩ典型值,高速模式用1kΩ)
- 标准模式100kHz,快速模式400kHz,高速模式3.4MHz
- 总线电容限制:标准模式400pF,快速模式400pF
核心经验:光模块里I2C总线长度通常不超过10cm,但如果你遇到信号畸变,先查上拉电阻。我遇到过一块板子,上拉电阻焊成了10kΩ,结果400kHz下波形完全变形。
传输协议细节:
- 起始条件:SCL高电平时,SDA从高到低跳变
- 从机地址:7位地址+读写位(0写1读)
- 应答机制:每字节后接收方拉低SDA表示ACK
- 数据传输:MSB先行,每字节8位
- 停止条件:SCL高电平时,SDA从低到高跳变
这里有个容易翻车的地方——应答超时。我曾经调试一个光模块,读寄存器时总是返回0xFF。抓波形一看,从机根本没拉低SDA做ACK。后来发现是地址写错了,从机地址左移了一位没处理。
注意:I2C从机地址是7位,但Linux驱动里用的地址是左移1位后的8位值。比如从机地址0x50,驱动里要写0xA0。这个坑我至少见新人踩过三次。
4.2 Linux I2C子系统架构:三层结构
Linux的I2C子系统分三层,我习惯这么理解:
| 层级 | 角色 | 对应代码 |
|---|---|---|
| I2C核心层 | 总线管理、设备枚举 | drivers/i2c/i2c-core.c |
| I2C总线驱动 | 硬件适配器操作 | drivers/i2c/busses/ |
| I2C设备驱动 | 具体外设控制 | 你自己写的驱动 |
核心数据结构:
struct i2c_adapter:代表一个I2C控制器struct i2c_algorithm:控制器的通信方法struct i2c_client:代表一个I2C从设备struct i2c_driver:设备驱动结构体
我个人习惯把设备驱动和总线驱动分开写。总线驱动一般芯片厂商已经提供了,比如i2c-imx.c、i2c-omap.c这些。我们主要写的是设备驱动。
4.3 I2C设备驱动编写:实战代码
下面是一个光模块DDM监控芯片的驱动骨架。我拿实际项目里的代码简化了一下:
#include <linux/i2c.h>
#include <linux/module.h>
struct opt_module_data {
struct i2c_client *client;
struct mutex lock;
u8 tx_buf[256];
u8 rx_buf[256];
};
static int opt_module_read_reg(struct i2c_client *client,
u8 reg_addr, u8 *value)
{
struct i2c_msg msg[2];
int ret;
/* 第一段:写寄存器地址 */
msg[0].addr = client->addr;
msg[0].flags = 0; // 写操作
msg[0].len = 1;
msg[0].buf = ®_addr;
/* 第二段:读数据 */
msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD; // 读操作
msg[1].len = 1;
msg[1].buf = value;
ret = i2c_transfer(client->adapter, msg, 2);
if (ret != 2) {
dev_err(&client->dev, "I2C read failed: %d\n", ret);
return -EIO;
}
return 0;
}
static int opt_module_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct opt_module_data *data;
u8 chip_id;
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->client = client;
mutex_init(&data->lock);
i2c_set_clientdata(client, data);
/* 读取芯片ID确认通信正常 */
if (opt_module_read_reg(client, 0x00, &chip_id) < 0) {
dev_err(&client->dev, "Failed to read chip ID\n");
return -ENODEV;
}
dev_info(&client->dev, "Chip ID: 0x%02x\n", chip_id);
return 0;
}
static const struct i2c_device_id opt_module_id[] = {
{ "opt_monitor", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, opt_module_id);
static struct i2c_driver opt_module_driver = {
.driver = {
.name = "opt_monitor",
},
.probe = opt_module_probe,
.id_table = opt_module_id,
};
module_i2c_driver(opt_module_driver);
提示:注意看i2c_transfer的返回值。它返回成功传输的消息段数,不是字节数。我见过有人写成if (ret < 0),结果返回1时也被当成错误处理了。
4.4 读写时序调试:示波器是最好的老师
调试I2C时序,我强烈建议你备一台示波器。逻辑分析仪也行,但示波器能看到信号质量。
常见问题排查清单:
- 波形无变化:检查GPIO复用配置,确认SCL/SDA引脚功能正确
- ACK丢失:从机地址错误、从机未上电、总线被拉死
- 数据错位:时钟频率不匹配、上升沿过缓
- 总线死锁:从机异常拉低SDA,需要复位从机或重新初始化总线
我曾经遇到一个特别诡异的bug。光模块在高温下I2C通信偶尔失败,常温下完全正常。抓波形发现,高温时SCL的上升沿变缓了,导致从机采样点偏移。最后把上拉电阻从4.7kΩ换成2.2kΩ,问题解决。
调试技巧:在驱动里加调试打印,把每次I2C传输的地址、数据、返回值都打出来。配合示波器波形,能快速定位问题。我习惯在i2c_transfer前后加trace点:
dev_dbg(&client->dev, "I2C WR: addr=0x%02x reg=0x%02x val=0x%02x\n",
client->addr, reg_addr, value);
ret = i2c_transfer(client->adapter, msg, 2);
dev_dbg(&client->dev, "I2C ret=%d\n", ret);
时序参数检查表:
| 参数 | 标准模式 | 快速模式 | 说明 |
|---|---|---|---|
| SCL时钟频率 | 100kHz | 400kHz | 不要超过规格 |
| 上升时间tr | ≤1000ns | ≤300ns | 上拉电阻影响大 |
| 数据保持时间thd | ≥0ns | ≥0ns | SCL下降沿后SDA保持 |
| 总线电容Cb | ≤400pF | ≤400pF | 过长走线会超标 |
嗯,说到调试,我再分享一个经验。有时候I2C通信失败,不一定是驱动问题。光模块的I2C从机地址可能被其他器件占用,或者模块的电源纹波太大导致从机复位。我建议你在probe函数里先读芯片ID,确认通信链路正常,再往下走。
避坑指南:我曾经在批量测试时发现,部分光模块的I2C通信偶尔会卡死。排查了两天才发现,是内核的I2C超时时间设置太短(默认1秒),而某些模块的EEPROM读操作需要更长时间。解决方案:在设备树里增加clock-frequency和timeout属性,或者驱动里用i2c_check_functionality确认适配器能力。
最后说一句,I2C驱动开发没有捷径。多抓波形、多看datasheet、多写测试代码,慢慢就熟练了。下一章我们会讲SPI总线驱动,那个又是另一番天地了。