第4章 I2C总线驱动开发:从协议到实战

I2C总线,说白了就是光模块里最常用的通信方式。我刚开始做光通信芯片驱动时,第一个要啃的硬骨头就是它。你想想看,光模块里的DDM监控、激光器偏置电流设置、温度补偿参数读写,全得靠这条两线制的总线来完成。

这一章,我会从协议细节讲到Linux驱动框架,再到实际调试中的那些坑。嗯,都是我在项目里真金白银换来的经验。

4.1 I2C协议详解:别小看这两根线

I2C只有两根线:SCL(时钟)和SDA(数据)。但就是这两根线,藏着不少门道。

物理层要点:

  • 开漏输出,需要上拉电阻(4.7kΩ典型值,高速模式用1kΩ)
  • 标准模式100kHz,快速模式400kHz,高速模式3.4MHz
  • 总线电容限制:标准模式400pF,快速模式400pF

核心经验:光模块里I2C总线长度通常不超过10cm,但如果你遇到信号畸变,先查上拉电阻。我遇到过一块板子,上拉电阻焊成了10kΩ,结果400kHz下波形完全变形。

传输协议细节:

  1. 起始条件:SCL高电平时,SDA从高到低跳变
  2. 从机地址:7位地址+读写位(0写1读)
  3. 应答机制:每字节后接收方拉低SDA表示ACK
  4. 数据传输:MSB先行,每字节8位
  5. 停止条件: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 = &reg_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-frequencytimeout属性,或者驱动里用i2c_check_functionality确认适配器能力。

最后说一句,I2C驱动开发没有捷径。多抓波形、多看datasheet、多写测试代码,慢慢就熟练了。下一章我们会讲SPI总线驱动,那个又是另一番天地了。