Linux i2c-gpio 驱动 v6.x 深度解析:从设备树到 /dev/i2c-x 的 5 个关键步骤

在嵌入式Linux开发中,当硬件I2C控制器资源不足时,GPIO模拟I2C成为了一种经济高效的解决方案。本文将深入剖析Linux内核中i2c-gpio驱动的实现机制,特别针对v6.x内核版本,从设备树配置到用户空间设备节点创建的完整流程。

1. 设备树配置与属性解析

i2c-gpio驱动的起点始于设备树(Device Tree)的配置。一个典型的i2c-gpio节点示例如下:

i2c_gpio: i2c-gpio {
    compatible = "i2c-gpio";
    gpios = <&gpio1 29 GPIO_ACTIVE_HIGH>, /* SDA */
            <&gpio1 28 GPIO_ACTIVE_HIGH>; /* SCL */
    i2c-gpio,delay-us = <5>;    /* ~100 kHz */
    #address-cells = <1>;
    #size-cells = <0>;
};

关键属性解析:

属性 作用 默认值 备注
gpios 指定SDA和SCL使用的GPIO 必须按SDA、SCL顺序排列
i2c-gpio,delay-us 位间隔时间(μs) 5 决定I2C时钟频率
i2c-gpio,sda-open-drain SDA开漏模式 自动配置 仅在外部已配置开漏时使用
i2c-gpio,scl-open-drain SCL开漏模式 自动配置 同上
i2c-gpio,scl-output-only SCL仅输出 false 特殊场景使用

注意: i2c-gpio,sda-open-drain i2c-gpio,scl-open-drain 属性在新版内核中已被标记为deprecated,推荐直接在gpios属性中使用 GPIO_OPEN_DRAIN 标志。

驱动在probe阶段通过 of_i2c_gpio_get_props() 函数解析这些属性,转换为 i2c_gpio_platform_data 结构体:

static void of_i2c_gpio_get_props(struct device_node *np,
                   struct i2c_gpio_platform_data *pdata)
{
    u32 reg;
    
    of_property_read_u32(np, "i2c-gpio,delay-us", &pdata->udelay);
    if (!of_property_read_u32(np, "i2c-gpio,timeout-ms", &reg))
        pdata->timeout = msecs_to_jiffies(reg);
    pdata->sda_is_open_drain = of_property_read_bool(np, "i2c-gpio,sda-open-drain");
    pdata->scl_is_open_drain = of_property_read_bool(np, "i2c-gpio,scl-open-drain");
    pdata->scl_is_output_only = of_property_read_bool(np, "i2c-gpio,scl-output-only");
}

2. GPIO初始化和算法接口准备

获取设备树配置后,驱动需要完成GPIO的初始化和算法接口的准备:

  1. GPIO获取与配置

    • 通过 i2c_gpio_get_desc() 获取SDA和SCL GPIO描述符
    • 根据是否指定open-drain属性,决定GPIO配置模式:
      if (pdata->sda_is_open_drain)
          gflags = GPIOD_OUT_HIGH;
      else
          gflags = GPIOD_OUT_HIGH_OPEN_DRAIN;
      priv->sda = i2c_gpio_get_desc(dev, "sda", 0, gflags);
      
  2. 算法接口准备

    • 填充 i2c_algo_bit_data 结构体,提供GPIO操作函数:
      bit_data->setsda = i2c_gpio_setsda_val;
      bit_data->setscl = i2c_gpio_setscl_val;
      if (!pdata->scl_is_output_only)
          bit_data->getscl = i2c_gpio_getscl;
      bit_data->getsda = i2c_gpio_getsda;
      bit_data->udelay = pdata->udelay;
      bit_data->timeout = pdata->timeout;
      
  3. 时钟频率计算

    • 根据 delay-us 属性计算实际传输速率
    • 标准模式(100kHz)通常对应 delay-us = 5
    • 快速模式(400kHz)需要 delay-us = 1

GPIO状态操作函数实现示例

static void i2c_gpio_setsda_val(void *data, int state)
{
    struct i2c_gpio_private_data *priv = data;
    gpiod_set_value(priv->sda, state);
}

static int i2c_gpio_getsda(void *data)
{
    struct i2c_gpio_private_data *priv = data;
    return gpiod_get_value(priv->sda);
}

3. I2C适配器注册流程

准备好算法接口后,驱动需要向I2C核心层注册适配器:

  1. 适配器结构体初始化

    adap->owner = THIS_MODULE;
    adap->algo_data = bit_data;
    adap->class = I2C_CLASS_HWMON | I2C_CLASS_SPD;
    adap->dev.parent = dev;
    adap->dev.of_node = np;
    
  2. 总线编号分配

    • 通过 pdev->id 获取初始编号
    • 最终编号由 i2c_add_numbered_adapter() 根据aliases节点确定
    • 设备树aliases示例:
      aliases {
          i2c3 = &i2c_gpio;
      }
      
  3. 适配器注册

    • 调用 i2c_bit_add_numbered_bus() 完成注册
    • 内部通过 __i2c_bit_add_bus() 实现核心注册逻辑

注册过程关键代码路径

ret = i2c_bit_add_numbered_bus(adap);
    → __i2c_bit_add_bus(adap, i2c_add_numbered_adapter)
        → adap->algo = &i2c_bit_algo;
        → add_adapter(adap) // i2c_add_numbered_adapter
            → __i2c_add_numbered_adapter
                → i2c_register_adapter

成功注册后,内核会为该适配器创建 /dev/i2c-x 设备节点,用户空间程序可通过标准I2C接口访问。

4. 位操作算法实现

i2c-algo-bit.c提供了GPIO模拟I2C的核心位操作算法,主要包括:

1. 主传输函数 bit_xfer()

  • 处理I2C消息的起始条件
  • 实现字节级别的发送/接收
  • 处理ACK/NACK响应
  • 生成停止条件

2. 关键时序操作

static void i2c_start(struct i2c_algo_bit_data *adap)
{
    /* START条件: SDA在SCL高电平时拉低 */
    adap->setsda(adap->data, 1);
    adap->setscl(adap->data, 1);
    ndelay(adap->udelay);
    adap->setsda(adap->data, 0);
    ndelay(adap->udelay);
    adap->setscl(adap->data, 0);
    ndelay(adap->udelay);
}

3. 字节传输流程

  1. SCL拉低,准备发送位
  2. 设置SDA电平(发送阶段)或读取SDA(接收阶段)
  3. SCL拉高,保持稳定时钟周期
  4. SCL拉低,完成位传输
  5. 重复8次完成一个字节传输

4. 时钟同步机制

  • 通过 udelay 保证时序符合I2C规范
  • 超时处理通过 timeout 参数实现

5. 用户空间访问与调试接口

注册成功后,用户可通过多种方式访问GPIO模拟的I2C总线:

1. 标准设备节点

  • /dev/i2c-x :提供基本的I2C设备访问
  • 使用 i2c-tools 进行测试:
    # 扫描总线上的设备
    i2cdetect -y 4
    
    # 读取设备寄存器
    i2cget -y 4 0x50 0x00
    

2. 调试文件系统接口 新版内核为i2c-gpio提供了debugfs接口,可用于底层调试:

debugfs_create_file("incomplete_address_phase", 0200, 
                   priv->adap.debugfs, priv, &fops_incomplete_addr_phase);

3. 故障注入测试 驱动内置了多种故障注入测试功能:

  • 模拟仲裁丢失
  • 不完整地址相位
  • 异常终止条件

性能优化建议:

  • 根据实际需求调整 delay-us 参数
  • 对于高速传输,考虑降低GPIO操作延迟
  • 避免在中断上下文中执行长距离传输

通过这五个关键步骤,Linux内核实现了完整的GPIO模拟I2C功能。相比硬件I2C控制器,这种方案虽然性能较低,但在资源受限的场景下提供了灵活可靠的解决方案。实际开发中,建议通过示波器验证信号质量,确保时序符合I2C规范。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐