18 Linux I2C Adapter Driver

在 Linux 系统中,I2C(Inter-Integrated Circuit)是一种广泛使用的通信协议,允许多个设备在同一条数据线上进行通信。I2C 适配器驱动是与 I2C 总线相关的核心组件,它连接了内核与 I2C 设备,负责数据的读写操作。

由17章中所描述的inux kernel I2C框架, 我们知道 I2C 适配器驱动、I2C 设备驱动和用户空间应用程序之间有一个清晰的层次结构:

  • I2C 适配器驱动:负责与底层硬件交互,管理 I2C 总线的通信。

  • I2C 设备驱动:注册在适配器上的设备,每个设备都有一个对应的驱动。

  • 用户空间应用程序:通过系统调用与设备驱动进行交互,进行数据读取和写入。

18.1 I2C 适配器驱动的作用

  • 桥梁:I2C 适配器驱动就像一座桥,将用户空间的请求转化为底层硬件可以理解的信号。

  • 管理总线:适配器负责管理 I2C 总线的访问和控制,从而确保数据的准确性和稳定性。

  • 执行操作:它实现了读写操作的具体细节,使得上层驱动和用户应用不需要关心底层硬件的复杂性。

数据传递过程

1.) 用户空间到内核空间
  • 打开设备文件:用户空间的应用程序首先通过 /dev/i2c-X 文件打开 I2C 设备。

  • 发起请求:应用程序使用 ioctl 系统调用,发送读取或写入的请求。

2.) 内核空间的处理

I2C 适配器驱动

  • 收到请求后,会调用适配器的 i2c_transfer() 函数, 定义在 drivers/i2c/i2c-core.c 文件中。该函数负责将 I2C 消息传递给注册的 I2C 适配器,并执行实际的数据传输。

    int i2c_transfer(struct i2c_adapter *adapter, struct i2c_msg *msgs, int num) {
        // 实现 I2C 消息的传输
    }
  • 在 i2c_transfer() 中,i2c_msg 结构体传递了具体的 I2C 消息信息(包括设备地址和要发送/接收的数据)。

I2C 设备驱动

  • 适配器驱动会调用相应的设备驱动函数,来处理特定设备的逻辑。

  • 设备驱动实现具体的读写逻辑,并与适配器驱动进行数据交互

3.) 从内核空间到用户空间
  • 返回数据:读取完成后,适配器驱动将数据返回到用户空间,用户应用程序接收到结果。

18.2 创建 I2C Adapter驱动

在Linux kernel中, 已经有一个基于GPIO来模拟I2C Adapter驱动 (也可以说是I2C bus驱动), 这里我们来配置及启用此驱动。

18.2.1 添加DTS

在DTS里设置具体的GPIO, 如下:

        demoi2c: i2c@20 {
                compatible = "i2c-gpio";
                sda-gpios = <&gpio1 RK_PA2 0 >;
                scl-gpios = <&gpio1 RK_PA4 0 >;
                i2c-gpio,scl-open-drain;
                i2c-gpio,delay-us = <1>;        /* ~100 kHz */
                #address-cells = <1>;
                #size-cells = <0>;
                pinctrl-names = "default";
                pinctrl-0 = <&demo_i2c_pin>;

        };

        aliases {
                i2c20 = &demoi2c;
        };

&pinctrl {
        demo_i2c {
                demo_i2c_pin: demo_i2c_pin {
                        rockchip,pins = <1 RK_PA2 RK_FUNC_GPIO &pcfg_pull_none>,
                                        <1 RK_PA4 RK_FUNC_GPIO &pcfg_pull_none>;
                };
        };
};

18.2.2 驱动代码 i2c-gpio.c

驱动代码在kernel/driver/i2c/buses/i2c-gpio.c, 编译的时候, 需要把CONFIG_I2C_GPIO选项勾选, 如下:

18.2.3 数据结构

  1. struct i2c_gpio_private_data数据结构, 定义如下:

    struct i2c_gpio_private_data {
    	struct gpio_desc *sda;
    	struct gpio_desc *scl;
    	struct i2c_adapter adap;
    	struct i2c_algo_bit_data bit_data;
    	struct i2c_gpio_platform_data pdata;
    #ifdef CONFIG_I2C_GPIO_FAULT_INJECTOR
    	struct dentry *debug_dir;
    	/* these must be protected by bus lock */
    	struct completion scl_irq_completion;
    	u64 scl_irq_data;
    #endif
    };

    sda:指向 SDA(数据线)的 GPIO 描述符,表示 I2C 数据线的控制。
    scl: 指向 SCL(时钟线)的 GPIO 描述符,表示 I2C 时钟线的控制。
    adap: 通过适配器,驱动可以向 I2C 核心注册自己,并提供 I2C 设备的访问接口。
    bit_data: 该结构体用于实现通过 GPIO 控制 I2C 总线的具体方法,如设置和获取 SDA 和 SCL 的电平。
    pdata:存储与平台相关的配置数据,如 SDA 和 SCL 的特性(例如是否为开漏)。
    CONFIG_I2C_GPIO_FAULT_INJECTOR, 用于调试的目录项,方便调试信息的输出。用于在故障注入调试时跟踪和管理中断,增强驱动的可靠性和调试能力。
    通过定义 i2c_gpio_private_data 结构体,驱动可以集中管理与 I2C GPIO 操作相关的所有状态和配置信息。

  2. struct i2c_adapter数据结构, 定义在 Linux 内核的 linux/i2c.h 头文件中, 如下:

    struct i2c_adapter {
        struct device dev;                // 设备结构体
        char name[I2C_NAME_SIZE];         // 适配器名称
        unsigned int id;                  // 适配器 ID
        struct i2c_algo_driver *algo;     // 算法驱动
        void *algo_data;                  // 算法数据
        unsigned int retries;              // 重试次数
        unsigned long timeout;             // 超时时间
        // 其他字段...
    };

    i2c_adapter 结构体通过包含与设备管理、传输算法、重试和超时相关的字段,使得 I2C 驱动能够有效地与 I2C 核心进行交互,并灵活地适应各种硬件平台和通信需求。

  3. struct i2c_algo_bit_data, 定义在 Linux 内核的头文件 include/linux/i2c-algo-bit.h 中, 如下:

    struct i2c_algo_bit_data {
        void *data;                            /* 私有数据,供驱动使用 */
        void (*setsda)(void *data, int state); /* 设置 SDA 线状态 */
        void (*setscl)(void *data, int state); /* 设置 SCL 线状态 */
        int (*getsda)(void *data);             /* 获取 SDA 线状态 */
        int (*getscl)(void *data);             /* 获取 SCL 线状态 */
        unsigned long udelay;                  /* 延迟时间(微秒) */
        unsigned long timeout;                 /* 超时时间 */
        bool can_do_atomic;                    /* 是否可以原子操作 */
    };

    这个结构体用于存储与位操作相关的算法数据。它包含了一些指向操作函数的指针(如 setsda 和 setscl),以及一些配置参数(如 udelay 和 timeout)。 在 I2C 驱动中,位操作算法允许通过 GPIO 引脚模拟 I2C 的时钟和数据线。这意味着它使得驱动能够通过软件控制 GPIO 来实现 I2C 协议。这种灵活性对于没有硬件 I2C 控制器的系统尤其重要。

18.2.4 重要函数

  1. 初始化

    subsys_initcall(i2c_gpio_init);
    module_exit(i2c_gpio_exit);

    其中, 使用 subsys_initcall 进行初始化的方式主要是为了在 Linux 内核启动时,以特定的顺序初始化子系统。这样可以确保在系统启动过程中,相关的子系统按正确顺序初始化。

  2. i2c_gpio_init

    static int __init i2c_gpio_init(void)
    {
    	int ret;
    
    	ret = platform_driver_register(&i2c_gpio_driver);
    	if (ret)
    		printk(KERN_ERR "i2c-gpio: probe failed: %d\n", ret);
    
    	return ret;
    }

    驱动注册: platform_driver_register(&i2c_gpio_driver); 这一行代码将 i2c-gpio 驱动注册到平台驱动模型中,使得内核能够识别并管理该驱动。在成功调用 platform_driver_register 后,内核将能够识别和管理该驱动,设备的实例化(通过 platform_device)会触发相应的 probe 函数进行设备的初始化。

  3. i2c_gpio_probe
    i2c_gpio_probe 是 I2C GPIO 驱动的 probe 函数,负责在设备被发现时初始化与该设备相关的资源。这是 I2C 驱动与硬件交互的关键步骤,确保 GPIO 引脚正确配置以支持 I2C 通信。
    (1). 内存分配, 如下:

    priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);

    分配 i2c_gpio_private_data 结构体的内存,用于存储设备的私有数据。
    (2). 获取平台数据, 如下:

    if (np) {
        of_i2c_gpio_get_props(np, pdata);
    } else {
        if (dev_get_platdata(dev))
            memcpy(pdata, dev_get_platdata(dev), sizeof(*pdata));
    }

    从设备树或平台数据中提取 GPIO 配置参数,确保必要的配置被正确获取。
    (3). 获取 GPIO 描述符, 如下:

    priv->sda = i2c_gpio_get_desc(dev, "sda", 0, gflags);
    priv->scl = i2c_gpio_get_desc(dev, "scl", 1, gflags);

    根据配置获取 SDA 和 SCL 引脚的 GPIO 描述符,并检查返回值以确保没有错误。
    (4). 设置算法数据, 如下:

    bit_data->setsda = i2c_gpio_setsda_val;
    bit_data->setscl = i2c_gpio_setscl_val;

    指定设置 SDA 和 SCL 引脚值的函数,确保 I2C 数据传输操作的实现。
    (5). 添加 I2C 总线, 如下:

    ret = i2c_bit_add_numbered_bus(adap);
    if (ret)
        return ret;

    将适配器注册到 I2C 核心,创建与 I2C 总线的连接。
    (6). 最后, 保存驱动数据, 如下:

    platform_set_drvdata(pdev, priv);

    将私有数据与平台设备绑定,以便后续访问。

18.2.5 加载驱动

首先使用下面命令编译kernel:

rk3588-linux$ ./build.sh kernel

编译成功后, 在设备上更新boot.img。 查看gpio管脚是否配置正确, 如下:

root@LPA3588:~# cat /sys/kernel/debug/gpio
gpiochip1: GPIOs 32-63, parent: platform/fec20000.gpio, gpio1:
 gpio-34  (                    |sda                 ) out hi
 gpio-36  (                    |scl                 ) out hi
 gpio-40  (                    |gpio_export         ) out lo
 gpio-41  (                    |vcc-mipidphy1-regula) out lo
 gpio-42  (                    |vcc-mipidphy0-regula) out lo
 gpio-44  (                    |reset               ) out hi
 gpio-52  (                    |hp-det              ) in  hi ACTIVE LOW
 gpio-54  (                    |work1               ) out hi ACTIVE LOW

可以看见有gpio-34, gpio-36分别被设置为sda及scl。

查看i2c节点是否创建成功, 如下:

neardi@LPA3588:/sys/bus/i2c/devices/i2c-20$ ls /dev/i2c-20 -l
crw-rw---- 1 root i2c 89, 20 Sep 20 18:25 /dev/i2c-20
neardi@LPA3588:/sys/bus/i2c/devices/i2c-20$ cat name
i2c@20

这里成功地创建了i2c节点, 由于我们在DTS里设置了别名, 故有/dev/i2c-20。