Lazy loaded image
IIC 驱动专题05 - GPIO模拟I2C源码分析和实例
Words 2305Read Time 6 min
2025-4-29
参考资料:
  • i2c_spec.pdf
  • Linux文档
    • Linux-5.4\Documentation\devicetree\bindings\i2c\i2c-gpio.yaml
    • Linux-4.9.88\Documentation\devicetree\bindings\i2c\i2c-gpio.txt
  • Linux驱动源码
    • Linux-5.4\drivers\i2c\busses\i2c-gpio.c
    • Linux-4.9.88\drivers\i2c\busses\i2c-gpio.c
 
软件实现的I2C总线驱动虽然速度不如硬件I2C控制器,但具有极高的灵活性,可以在任何有GPIO引脚的系统上实现I2C通信,非常适合嵌入式系统和原型开发。

1. I2C-GPIO设备树定义的两种方法

根据Linux 4.9.88和Linux 5.4版本的I2C-GPIO驱动代码,我可以对比两种GPIO引脚定义方式:

方法1:使用gpios属性(Linux 4.9.88中的方法)

说明:
  1. Linux 4.9.88版本中,I2C-GPIO驱动通过of_i2c_gpio_get_pins函数获取GPIO引脚
  1. 该函数使用of_gpio_countof_get_gpio函数从设备树的gpios属性中获取SDA和SCL引脚
  1. 第一个引脚(索引0)被识别为SDA,第二个引脚(索引1)被识别为SCL
  1. 这是Linux 4.9.88版本中唯一支持的方法

方法2:使用sda-gpios和scl-gpios属性(Linux 5.4中新增的方法)

说明:
  1. 在Linux 5.4版本中,I2C-GPIO驱动增加了对sda-gpiosscl-gpios属性的支持
  1. 这种方法使用独立的属性分别定义SDA和SCL引脚,更加明确
  1. 驱动会首先尝试使用sda-gpiosscl-gpios属性,如果不存在则回退到使用gpios属性
  1. 这种方法符合Linux设备树的命名规范,更加直观和明确
在Linux 4.9.88版本中,只支持使用gpios属性的方法1。而在Linux 5.4版本中,两种方法都支持,但推荐使用方法2,因为它更加明确和符合Linux设备树的命名规范。
如果您使用的是Linux 4.9.88版本,只能使用方法1。如果您使用的是Linux 5.4或更高版本,建议使用方法2。

2. I2C-GPIO驱动层次与函数调用栈分析

2.1 I2C-GPIO驱动层次图

2.2 源码分析解释传输函数

I2C-GPIO驱动架构

I2C-GPIO驱动是一个软件实现的I2C总线驱动,它通过GPIO引脚模拟I2C总线的SDA和SCL信号线。整个驱动分为两个主要部分:
  1. i2c-gpio.c: 负责GPIO引脚的配置和操作,提供底层的SDA/SCL控制函数
  1. i2c-algo-bit.c: 实现I2C协议的位操作算法,处理I2C总线的时序和传输

关键传输函数分析

1. 底层GPIO操作函数 (i2c-gpio.c)

  • i2c_gpio_setsda_dir/val: 设置SDA引脚的方向或值
  • i2c_gpio_setscl_dir/val: 设置SCL引脚的方向或值
  • i2c_gpio_getsda: 读取SDA引脚的值
  • i2c_gpio_getscl: 读取SCL引脚的值
这些函数是I2C位操作算法的基础,通过GPIO API实现对物理引脚的控制。

2. I2C位操作算法核心函数 (i2c-algo-bit.c)

  • bit_xfer: 主要的传输函数,处理整个I2C消息传输过程
    • 该函数接收一个消息数组和消息数量,按顺序处理每个消息。它负责:
    • 发送起始条件
    • 处理设备地址
    • 根据消息类型执行读或写操作
    • 发送停止条件
  • i2c_start/i2c_repstart: 产生I2C起始条件和重复起始条件
    • 这些函数通过控制SDA和SCL线产生I2C协议规定的起始信号。
  • bit_doAddress: 处理设备地址传输
    • 该函数负责发送设备地址,支持7位和10位地址模式,并处理读写方向位。
  • sendbytes/readbytes: 数据传输函数
    • 这些函数分别处理数据的发送和接收,包括应答/非应答的处理。
  • i2c_outb/i2c_inb: 字节级传输函数
    • 这些函数实现单个字节的发送和接收,包括位操作和时序控制。
  • i2c_stop: 产生I2C停止条件
    • 该函数通过控制SDA和SCL线产生I2C协议规定的停止信号。

3. 传输过程详解

  1. 初始化阶段:
      • i2c_gpio_probe函数注册I2C适配器
      • 配置GPIO引脚并设置回调函数
  1. 传输阶段:
      • 当上层调用I2C传输函数时,请求传递到bit_xfer
      • bit_xfer发送起始条件,然后循环处理每个消息
      • 对每个消息,先发送设备地址,然后根据读/写标志调用相应函数
      • 读操作调用readbytes,写操作调用sendbytes
      • 最后发送停止条件
  1. 时序控制:
      • 通过sdalo/sdahiscllo/sclhi函数控制SDA和SCL线
      • 使用udelay函数实现精确的时序延迟
      • sclhi函数支持时钟拉伸(某些设备可能需要更多时间处理数据)
  1. 错误处理:
      • 检测设备是否应答
      • 处理超时情况
      • 支持重试机制

3. 怎么使用I2C-GPIO

设置设备数,在里面添加一个节点即可,示例代码看上面:
  • compatible = "i2c-gpio";
  • 使用pinctrl把 SDA、SCL所涉及引脚配置为GPIO、开极
    • 可选
  • 指定SDA、SCL所用的GPIO
  • 指定频率(2种方法):
    • i2c-gpio,delay-us = <5>; /* ~100 kHz */
    • clock-frequency = <400000>;
  • #address-cells = <1>;
  • #size-cells = <0>;
  • i2c-gpio,sda-open-drain
    • 它表示其他驱动、其他系统已经把SDA设置为open drain了
    • 在驱动里不需要在设置为open drain
    • 如果需要驱动代码自己去设置SDA为open drain,就不要提供这个属性
  • i2c-gpio,scl-open-drain
    • 它表示其他驱动、其他系统已经把SCL设置为open drain了
    • 在驱动里不需要在设置为open drain
    • 如果需要驱动代码自己去设置SCL为open drain,就不要提供这个属性

4. 实例实验

4.1. 根据原理图编写设备树

4.1 原理图
/
4.2 编写设备树
把上述代码,放入arch/arm/boot/dts/100ask_imx6ull-14x14.dts的根节点下面。

4.2 确认内核已经配置了I2C-GPIO

查看内核目录下的.config,如果未设置CONFIG_I2C_GPIO,上机实验时需要配置内核、编译I2C-GPIO驱动。

4.3 上机实验

4.1 设置工具链
  • IMX6ULL
    4.2 编译、替换设备树
    • 编译设备树: 在Ubuntu的IMX6ULL内核目录下执行如下命令, 得到设备树文件:arch/arm/boot/dts/100ask_imx6ull-14x14.dtb
      • 复制到NFS目录:
        • 开发板上挂载NFS文件系统
          • vmware使用NAT(假设windowsIP为192.168.1.100)
            • vmware使用桥接,或者不使用vmware而是直接使用服务器:假设Ubuntu IP为192.168.1.137
            • 更新设备树
              • 重启开发板

              4.4 编译I2C-GPIO驱动

              1. 配置内核
              在IMX6ULL内核源码目录下执行make menuconfig命令,如下配置内核:
              2. 编译模块
              设置工具链后,在内核目录下执行:

              4.5 测试

              插入EEPROM模块,在开发板上执行:
              上一篇
              模板设计模式:让你的代码结构更清晰
              下一篇
              Guide to Linux System

              Comments
              Loading...