Skip to content

LV130-GPIO模拟I2C

一、GPIO 模拟 I2C 简介

在 Linux 项目中,如果出现硬件 I2C 不够用的情况,就可以使用模拟 i2c 来解决。其实 I2C 只是规定了数据传输的协议,并没有说一定要使用物理的 I2C 外设,我们知道 2C 协议信号如下:

image-20210220151524099

这里其实就是一根时钟线,一根数据线,这些我们完全可以用两个 GPIO 来进行模拟。之前学习单片机的时候其实一开始用的都是 gpio 模拟 i2c:STM32_HAL_Prj/Drivers/BSP/IIC/myiic.c

image-20250329131418887

使用 GPIO 模拟 I2C 的要点:

  • 引脚设为 GPIO。
  • GPIO 设为输出、开极/开漏(open collector/open drain)。
  • 要有上拉电阻,为什么需要上拉电阻,在前面已经学习过了,主要就是可以方便的实现数据双向传输。以及实现多个设备对 I2C 总线的使用。

二、GPIO 引脚定义

既然是 GPIO 模拟 I2C,那其实,只需要两个 I2C 就可以了,但是在不同版本的内核中,会支持一些新的写法。

c
Example nodes:

#include <dt-bindings/gpio/gpio.h>

i2c@0 {
	compatible = "i2c-gpio";
	sda-gpios = <&pioA 23 (GPIO_ACTIVE_HIGH|GPIO_OPEN_DRAIN)>;
	scl-gpios = <&pioA 24 (GPIO_ACTIVE_HIGH|GPIO_OPEN_DRAIN)>;
	i2c-gpio,delay-us = <2>;	/* ~100 kHz */
	#address-cells = <1>;
	#size-cells = <0>;

	rv3029c2@56 {
		compatible = "rv3029c2";
		reg = <0x56>;
	};
};
c
i2c@0 {
	compatible = "i2c-gpio";
	gpios = <&pioA 23 0 /* sda */
		 &pioA 24 0 /* scl */
		>;
	i2c-gpio,sda-open-drain;
	i2c-gpio,scl-open-drain;
	i2c-gpio,delay-us = <2>;	/* ~100 kHz */
	#address-cells = <1>;
	#size-cells = <0>;

	rv3029c2@56 {
		compatible = "rv3029c2";
		reg = <0x56>;
	};
};

三、i2c-gpio 基本框架

Lnux 内核的 i2c-gpio 是使用 GPIO 模拟 I2C 协议的驱动,在内核中已经实现了,我们要做的只需要配置 2 个 GPIO(SDA 和 SCL)即可。

image-20250329140210360

(1)解析设备树中的引脚配置信息

(2)提供 GPIO SDA 和 SCL 引脚配置接口。

(1)向 I2C Core 注册一个 adapter

(2)提供 I2C 通信时的算法,然后通过 i2c-gpio.c 提供 GPIO 配置接口来收发数据。

注册成功后,"i2c-dev" 驱动就会自动创建对应的 "/dev/i2c-x" 字符设备,然后我们就可以在应用层和驱动层操作该总线。

四、如何添加一个模拟 I2C?

1. 驱动相关文件放在那里?

GPIO 模拟 I2C 协议的驱动位于 busses - drivers/i2c/busses 目录。驱动名称为“i2c-gpio”,驱动文件为 i2c-gpio.c - drivers/i2c/busses/i2c-gpio.c,这个也是 platform 驱动:

image-20250329135722971

2. 使能内核的 I2C GPIO 驱动

我们在内核源码目录下输入:

shell
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v6_v7_defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

按以下路径找到对应的配置项:

shell
Device Drivers->
    I2C support  --->
        I2C Hardware Bus support  --->
            <*> GPIO-based bitbanging I2C
image-20250329140631954

确认配置后,i2c-gpio 相关驱动就会被编译进内核。当然我们也可以编译成驱动模块,然后手动加载,不过还是直接编译到内核更方便一些。

3. 设备树配置

3.1 模拟 I2C 节点

i2c-gpio 的 i2c_adapter 设备树节点这样写:

c
i2c5:i2c5_gpio {
	#address-cells = <1>;
	#size-cells = <0>;
	compatible = "i2c-gpio";
	gpios = <&gpio1 29 GPIO_ACTIVE_HIGH>, 	/* sda */
			<&gpio1 28 GPIO_ACTIVE_HIGH>; 	/* scl */
	i2c-gpio,delay-us = <5>;		/* ~100 kHz */
	status = "disabled";
};

我们打开 imx6ul.dtsi 添加以上内容,需要注意的是添加需要模拟 i2c 的 gpio 的时候,一定是先放 sda 再放 scl,因为它是在 i2c-gpio.c 里面定义好的,必须这么写才可以。

3.2 修改 aliases

这里以 imx6ul.dtsi - arch/arm/boot/dts/imx6ul.dtsi 为例,我们添加如下内容:

c
/ {
	#address-cells = <1>;
	#size-cells = <1>;
	//......
	aliases {
		ethernet0 = &fec1;
		ethernet1 = &fec2;
		gpio0 = &gpio1;
		gpio1 = &gpio2;
		gpio2 = &gpio3;
		gpio3 = &gpio4;
		gpio4 = &gpio5;
		i2c0 = &i2c1;
		i2c1 = &i2c2;
		i2c2 = &i2c3;
		i2c3 = &i2c4;
        i2c4 = &i2c5; // 这行是新添加的
		mmc0 = &usdhc1;
		//......
	};
};

为什么需要修改 aliases 呢? 因为在添加添加 adapter 时,会通过 aliases 的别名编号配置 adapter->nr 总线编号。注册成功后,会创建 /dev/i2c-4 设备。在 i2c_add_adapter() 函数中:

c
int i2c_add_adapter(struct i2c_adapter *adapter)
{
	struct device *dev = &adapter->dev;
	int id;

	if (dev->of_node) {
		id = of_alias_get_id(dev->of_node, "i2c");
		if (id >= 0) {
			adapter->nr = id;
			return __i2c_add_numbered_adapter(adapter);
		}
	}
	//......
	adapter->nr = id;

	return i2c_register_adapter(adapter);
}

这样在注册 i2c_adapter 的时候 i2c_add_adapter() 函数就会获取到对应的一个编号。

3.3 open drain 属性

使用 GPIO 模拟 I2C 模式时,一般 GPIO 需要工作在开漏模式。在 of_i2c_gpio_get_props() 函数中,解析是否有定义 open drain 相关属性。如下:

c
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");
}

当定义 i2c-gpio, sda-open-drain 和 i2c-gpio, scl-open-drain 属性后,说明是其它子系统已经将该 GPIO 配置成开漏输出了,这里不再进行开漏的配置。如果 dts 里面不定义,就启动 GPIOD_OUT_HIGH_OPEN_DRAIN 配置 GPIO,我们可以看一下 i2c_gpio_probe()

c
	if (pdata->sda_is_open_drain)
		gflags = GPIOD_OUT_HIGH;
	else
		gflags = GPIOD_OUT_HIGH_OPEN_DRAIN;
	//......
	if (pdata->scl_is_open_drain)
		gflags = GPIOD_OUT_HIGH;
	else
		gflags = GPIOD_OUT_HIGH_OPEN_DRAIN;

所以,这里可以不定义该属性,它将会在函数 i2c_gpio_probe() 中配置为开漏。

3.4 添加 i2c 设备

有了 i2c 适配器,就可以添加对应的 i2c 设备了,这里就和前面一样了。为了方便后面测试模拟 I2C 总线,这里还是需要一个设备:

c
&i2c5 {
	status = "okay";
	
	ap3216c@1e {
		compatible = "alpha,ap3216c";
		reg = <0x1e>;
	};
};

不过,我们并没有实际的设备挂载在上面,后面可以直接用下面的命令创建模拟的 i2c 设备:

shell
// 创建一个i2c_client, .name = "eeprom", .addr=0x50, .adapter是i2c-4
# echo eeprom 0x50 > /sys/bus/i2c/devices/i2c-4/new_device

// 删除一个i2c_client
# echo 0x50 > /sys/bus/i2c/devices/i2c-4/delete_device

3.5 开发板测试

我们更新开发板的设备树,就会发现这里新增了 /dev/i2c-4 总线设备,它就是我们新增的 GPIO 模拟 I2C 总线设备。

image-20250330170557596

使用 i2c_tools 测试该总线,可以正常的识别到设备,说明移植已经成功了。但是我这里实际上并没有接入 i2c 设备,就算手动创建一个 i2c 设备,也只会创建对应的设备,但是 i2cdetect 不会有反应,因为我们实际并没有对应的 i2c 设备存在,这就会导致 i2c 向这个地址发数据的时候,并不会收到回应,所以就探测不出来了:

image-20250330170813059

五、i2c-gpio 驱动分析

前面知道,GPIO 模拟 I2C 协议的驱动位于 busses - drivers/i2c/busses 目录。驱动名称为“i2c-gpio”,驱动文件为 i2c-gpio.c - drivers/i2c/busses/i2c-gpio.c,是一个平台设备驱动,它的平台设备驱动结构体为 i2c_gpio_driver

c
static struct platform_driver i2c_gpio_driver = {
	.driver		= {
		.name	= "i2c-gpio",
		.of_match_table	= of_match_ptr(i2c_gpio_dt_ids),
	},
	.probe		= i2c_gpio_probe,
	.remove		= i2c_gpio_remove,
};

1. i2c_gpio_driver.driver.of_match_table

先来看一下这个匹配表,对应的数组为 i2c_gpio_dt_ids

c
static const struct of_device_id i2c_gpio_dt_ids[] = {
	{ .compatible = "i2c-gpio", },
	{ /* sentinel */ }
};

MODULE_DEVICE_TABLE(of, i2c_gpio_dt_ids);

所以我们前面定义的模拟 I2C 的 compatible 属性名称就包含的有 "i2c-gpio"。

2. i2c_gpio_driver.probe

当 i2c 适配器设备和驱动匹配的时候,就会执行.probe 函数,在这里就是 i2c_gpio_probe()

c
static int i2c_gpio_probe(struct platform_device *pdev)
{
	//......
    // (1)解析设备树
	if (np) {
		of_i2c_gpio_get_props(np, pdata);
	} else {
	//......
	}
    
    //......
    // (2)解析 dts 设备树文件里面定义 gpios 配置:gpios = <&gpio1 29 GPIO_ACTIVE_HIGH>, <&gpio1 28 GPIO_ACTIVE_HIGH>;
    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);
	if (IS_ERR(priv->sda))
		return PTR_ERR(priv->sda);

	if (pdata->scl_is_open_drain)
		gflags = GPIOD_OUT_HIGH;
	else
		gflags = GPIOD_OUT_HIGH_OPEN_DRAIN;
	priv->scl = i2c_gpio_get_desc(dev, "scl", 1, gflags);
	if (IS_ERR(priv->scl))
		return PTR_ERR(priv->scl);
	//......
    // (3)配置操作 SDA 和 SCL 2 个 GPIO 的函数接口,后面可以通过它设置和获取 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;
	//......
    // (4)注册到 i2c-algo-bit.c
	adap->algo_data = bit_data;
	adap->class = I2C_CLASS_HWMON | I2C_CLASS_SPD;
	adap->dev.parent = dev;
	adap->dev.of_node = np;

	adap->nr = pdev->id;
	ret = i2c_bit_add_numbered_bus(adap);
	if (ret)
		return ret;
	//......
	return 0;
}

2.1 解析设备树

我们看一下这个 of_i2c_gpio_get_props()

c
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");
}
  • i2c-gpio, delay-us:配置每个 bit 的使用时间,也就是 I2C 通信时 Clock 的频率。
  • i2c-gpio, timeout-ms:配置 i2c 通信时的超时时间,如果超过这个时间没有收到 ack,说明通信失败。
  • i2c-gpio, sda-open-drain:是否有在其它子系统里面定义了 sda gpio 为开漏模式,如果有就定义该属性。
  • i2c-gpio, scl-open-drain:是否有在其它子系统里面定义了 scl gpio 为开漏模式,如果有就定义该属性。
  • i2c-gpio, scl-output-only:配置 scl gpio 只支持输出模式,不支持输入模式。

2.2 注册到 i2c-algo-bit.c

我们看一下 i2c_bit_add_numbered_bus()

c
int i2c_bit_add_numbered_bus(struct i2c_adapter *adap)
{
	return __i2c_bit_add_bus(adap, i2c_add_numbered_adapter);
}

它调用的是__i2c_bit_add_busr()函数:

c
static int __i2c_bit_add_bus(struct i2c_adapter *adap,
			     int (*add_adapter)(struct i2c_adapter *))
{
	//......

	/* register new adapter to i2c module... */
	adap->algo = &i2c_bit_algo;
	//......

	ret = add_adapter(adap);
	//......
	return 0;
}

可以看到这里为 i2c-gpio 添加了对应的 i2c_algorithm ,这里对应的是 i2c_bit_algo,它的 master_xfer 成员指向的函数就为模拟 i2c 添加用于产生 i2c 访问周期需要的 start stop ack 信号操作函数。就算我们自己写 GPIO 模拟 I2C 驱动,也都必须实现这些函数。

c
const struct i2c_algorithm i2c_bit_algo = {
	.master_xfer	= bit_xfer,
	.functionality	= bit_func,
};

这个我们后面再看。然后就是调用 add_adapter() 向 I2C Core 注册一个 adapter,注册成功后,"i2c-dev" 驱动就会自动创建对应的 "/dev/i2c-x" 字符设备,然后我们就可以在应用层和驱动层操作该总线。

3. i2c_bit_algo

3.1 i2c_bit_algo.functionality

i2c_bit_algo.functionality 对应的函数是 bit_func()

c
static u32 bit_func(struct i2c_adapter *adap)
{
	return I2C_FUNC_I2C | I2C_FUNC_NOSTART | I2C_FUNC_SMBUS_EMUL |
	       I2C_FUNC_SMBUS_READ_BLOCK_DATA |
	       I2C_FUNC_SMBUS_BLOCK_PROC_CALL |
	       I2C_FUNC_10BIT_ADDR | I2C_FUNC_PROTOCOL_MANGLING;
}

这里就是返回支持的 I2C 协议。

3.2 i2c_bit_algo.master_xfer

i2c_bit_algo.master_xfer 对应的是 bit_xfer() 函数,这个函数就是为 i2c 的访问产生 i2c 访问周期需要的 start stop ack 信号:

c
static int bit_xfer(struct i2c_adapter *i2c_adap,
		    struct i2c_msg msgs[], int num)
{
	//......
	i2c_start(adap);
	for (i = 0; i < num; i++) {
		//......
		if (pmsg->flags & I2C_M_RD) {
			/* read bytes into buffer*/
			ret = readbytes(i2c_adap, pmsg);
			//......
		} else {
			/* write bytes from buffer */
			ret = sendbytes(i2c_adap, pmsg);
			//......
		}
	}
	ret = i;
	//......
	i2c_stop(adap);
	//......
	return ret;
}

当我们使用 i2c_transfer() 函数收发数据的时候,就会调用到这个函数。

六、GPIO 模拟 I2Cdemo

1. demo 源码

这个 demo 源码可以看这里:30_i2c_subsystem/04_i2c_gpio/device_tree/imx6ull-alpha-emmc/imx6ull-alpha-emmc.dtsi · 苏木/imx6ull-driver-demo - 码云 - 开源中国

这个 demo 主要是修改设备树,驱动是不需要的,另外还修改了 kernel/dts_common/imx6ul.dtsi · 苏木/imx6ull-driver-demo - 码云 - 开源中国

2. 开发板测试

就和本篇笔记 第四节的 3.5 的测试现象一样。

参考资料:

【驱动】linux 下 I2C 驱动架构全面分析 - Leo.cheng - 博客园

Linux i2c 子系统(一) _动手写一个 i2c 设备驱动_Linux 编程_Linux 公社-Linux 系统门户网站

I2C 子系统–mpu6050 驱动实验 野火嵌入式 Linux 驱动开发实战指南——基于 i.MX6ULL 系列 文档

【I2C】Linux 使用 GPIO 模拟 I2C_i2c dts 配置-CSDN 博客

Linux 内核驱动:gpio 模拟 i2c 驱动_i2c-gpio-CSDN 博客