往期内容

I2C子系统专栏:

  1. I2C(IIC)协议讲解-CSDN博客
  2. SMBus 协议详解-CSDN博客
  3. I2C相关结构体讲解:i2c_adapter、i2c_algorithm、i2c_msg-CSDN博客
  4. 内核提供的通用I2C设备驱动I2c-dev.c分析:注册篇
  5. 内核提供的通用I2C设备驱动I2C-dev.c分析:file_ops篇
  6. 设备驱动与设备树匹配机制详解
  7. 编写一个通用的i2c设备驱动框架
  8. 编写一个通用的i2c控制器驱动框架
  9. consumer 角度讲一下i2c外设-CSDN博客

总线和设备树专栏:

  1. 总线和设备树_憧憬一下的博客-CSDN博客
  2. 设备树与 Linux 内核设备驱动模型的整合-CSDN博客

前言

如果I2C接口、总线不够用,就可以使用GPIO来模拟I2C总线(简易的I2C控制器,没有中断休眠功能,用两个GPIO引脚模拟I2C控制器的驱动),模拟I2C协议进行输出
Bitbanging I2C bus driver using the GPIO API

注:驱动代码分析部分皆摘抄自linux内核提供的用gpio模拟IIC协议的驱动文件:i2c-gpio.c

1. 回顾I2C协议

内容在之前的章节都有讲过,具体内容跳转:

I2C(IIC)协议讲解-CSDN博客

2. 使用GPIO模拟I2C的要点

  • 引脚设为GPIO
  • GPIO设为输出、开极/开漏(open collector/open drain)
  • 要有上拉电阻

3. 驱动程序分析

3.1 平台总线设备驱动模型

img

3.2 设备树

对于GPIO引脚的定义,有两种方法:

  • 老方法:gpios
i2c-gpio{
    compatible = "i2c-gpio";
    gpios = <&gpio0 3 0
             &gpio 2 0>;
    i2c-gpio,delay-us = <2>;
    #address-cells = <1>;
    #size-cells = <0>;
    rtc0:rtc@48{
        compatible = "st,m41t00";
        reg = <0x68>;
    } 
}
  • 新方法:sda-gpios、scl-gpios
    i2cl:i2c{
        compatible = "i2c-gpio";
        sda-gpios = <&gpioa 5 (GPIO_ACTIVE_HIGH | GPIO_OPEN_DRAIN)>;
        scl-gpios = <&gpioa 4 (GPIO_ACTIVE_HIGH | GPIO_OPEN_DRAIN)>;
    } 

3.3 驱动程序分析

I2C-GPIO驱动层次

img

img

  • 分析的文件为内核提供的用gpio模拟i2c驱动源码:

  • 可以简单的看看上图,下面是深入理解,注意注释中标注(1.1)等标志的会抽出来另外讲解

Linux-4.9.88\drivers\i2c\busses\i2c-gpio.c:
static int i2c_gpio_probe(struct platform_device *pdev)
{
	struct i2c_gpio_private_data *priv;
	struct i2c_gpio_platform_data *pdata;
	struct i2c_algo_bit_data *bit_data;
	struct i2c_adapter *adap;
	unsigned int sda_pin, scl_pin;
	int ret;

	/* 从设备树中获取 I2C SDA 和 SCL 引脚号,如果失败则延迟探测 */
	if (pdev->dev.of_node) {
		ret = of_i2c_gpio_get_pins(pdev->dev.of_node, &sda_pin, &scl_pin);
		if (ret)
			return ret;
	} else {
		/* 如果没有设备树节点,获取平台设备数据 */
		if (!dev_get_platdata(&pdev->dev))
			return -ENXIO;
		pdata = dev_get_platdata(&pdev->dev);
		sda_pin = pdata->sda_pin;
		scl_pin = pdata->scl_pin;
	}

	/* 请求 SDA 引脚 */
	ret = devm_gpio_request(&pdev->dev, sda_pin, "sda");
	if (ret) {
		if (ret == -EINVAL)
			ret = -EPROBE_DEFER;	/* 如果引脚不可用,延迟探测 */
		return ret;
	}
	/* 请求 SCL 引脚 */
	ret = devm_gpio_request(&pdev->dev, scl_pin, "scl");
	if (ret) {
		if (ret == -EINVAL)
			ret = -EPROBE_DEFER;	/* 如果引脚不可用,延迟探测 */
		return ret;
	}

	/* 分配并初始化私有数据结构 */
	priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
	if (!priv)
		return -ENOMEM;
	adap = &priv->adap;
	bit_data = &priv->bit_data;
	pdata = &priv->pdata;

	/* 设置平台数据或从设备树中获取 */
	if (pdev->dev.of_node) {
		pdata->sda_pin = sda_pin;
		pdata->scl_pin = scl_pin;
		of_i2c_gpio_get_props(pdev->dev.of_node, pdata);  ----------(1.1)
	} else {
		memcpy(pdata, dev_get_platdata(&pdev->dev), sizeof(*pdata));
	}

	/* 配置 SDA 引脚为开漏输出或输入模式 */
	if (pdata->sda_is_open_drain) {
		gpio_direction_output(pdata->sda_pin, 1);  // 设置 SDA 为输出模式
		bit_data->setsda = i2c_gpio_setsda_val;    // 设置 SDA 引脚值
	} else {
		gpio_direction_input(pdata->sda_pin);       // 设置 SDA 为输入模式
		bit_data->setsda = i2c_gpio_setsda_dir;     // 切换 SDA 引脚方向
	}

	/* 配置 SCL 引脚为开漏输出或仅输出模式 */
	if (pdata->scl_is_open_drain || pdata->scl_is_output_only) {
		gpio_direction_output(pdata->scl_pin, 1);  // 设置 SCL 为输出模式
		bit_data->setscl = i2c_gpio_setscl_val;    // 设置 SCL 引脚值
	} else {
		gpio_direction_input(pdata->scl_pin);       // 设置 SCL 为输入模式
		bit_data->setscl = i2c_gpio_setscl_dir;     // 切换 SCL 引脚方向
	}

	/* 如果 SCL 不是仅输出模式,设置读取 SCL 引脚的函数 */
	if (!pdata->scl_is_output_only)
		bit_data->getscl = i2c_gpio_getscl;
	bit_data->getsda = i2c_gpio_getsda;  // 设置读取 SDA 引脚的函数

	/* 设置时钟延迟 (udelay),根据是否支持时钟伸展功能 */
	if (pdata->udelay)
		bit_data->udelay = pdata->udelay;
	else if (pdata->scl_is_output_only)
		bit_data->udelay = 50;  /* 10 kHz */
	else
		bit_data->udelay = 5;   /* 100 kHz */

	/* 设置超时值,如果未定义则设置为 100 ms */
	if (pdata->timeout)
		bit_data->timeout = pdata->timeout;
	else
		bit_data->timeout = HZ / 10;  /* 100 ms */

	bit_data->data = pdata;

	/* 设置 I2C 适配器的一些基本信息 */
	adap->owner = THIS_MODULE;
	if (pdev->dev.of_node)
		strlcpy(adap->name, dev_name(&pdev->dev), sizeof(adap->name));
	else
		snprintf(adap->name, sizeof(adap->name), "i2c-gpio%d", pdev->id);

	/* 将算法数据和适配器绑定 */
	adap->algo_data = bit_data;
	adap->class = I2C_CLASS_HWMON | I2C_CLASS_SPD;  // 支持的设备类别
	adap->dev.parent = &pdev->dev;
	adap->dev.of_node = pdev->dev.of_node;

	/* 将适配器注册到 I2C 核心 */
	adap->nr = pdev->id;
	ret = i2c_bit_add_numbered_bus(adap);  -------(1.2)
	if (ret)
		return ret;

	platform_set_drvdata(pdev, priv);

	/* 打印使用的引脚信息 */
	dev_info(&pdev->dev, "using pins %u (SDA) and %u (SCL%s)\n",
		 pdata->sda_pin, pdata->scl_pin,
		 pdata->scl_is_output_only ? ", no clock stretching" : "");

	return 0;
}
(1.1)

of_i2c_gpio_get_props(pdev->dev.of_node, pdata); 从设备树中读取 I2C GPIO 相关的属性,并将这些属性填充到 i2c_gpio_platform_data 结构体中:

Linux-4.9.88\drivers\i2c\busses\i2c-gpio.c:
static void of_i2c_gpio_get_props(struct device_node *np,
				  struct i2c_gpio_platform_data *pdata)
{
	u32 reg;

	/* 从设备树中读取 "i2c-gpio,delay-us" 属性,设置 I2C 信号切换的延迟(微秒) */
	of_property_read_u32(np, "i2c-gpio,delay-us", &pdata->udelay);

	/* 从设备树中读取 "i2c-gpio,timeout-ms" 属性,设置超时时间(毫秒),并将其转换为 jiffies */
	if (!of_property_read_u32(np, "i2c-gpio,timeout-ms", &reg))
		pdata->timeout = msecs_to_jiffies(reg);

	/* 判断设备树中是否有 "i2c-gpio,sda-open-drain" 属性,设置 SDA 引脚是否为开漏模式 */
	pdata->sda_is_open_drain =
		of_property_read_bool(np, "i2c-gpio,sda-open-drain");

	/* 判断设备树中是否有 "i2c-gpio,scl-open-drain" 属性,设置 SCL 引脚是否为开漏模式 */
	pdata->scl_is_open_drain =
		of_property_read_bool(np, "i2c-gpio,scl-open-drain");

	/* 判断设备树中是否有 "i2c-gpio,scl-output-only" 属性,设置 SCL 引脚是否为仅输出模式 */
	pdata->scl_is_output_only =
		of_property_read_bool(np, "i2c-gpio,scl-output-only");
}
(1.2)

ret = i2c_bit_add_numbered_bus(adap);用于在运行时注册 I2C bit-banging 算法。该函数主要负责将 I2C 适配器与 bit-banging 算法相关联,并执行必要的检查和初始化。

\Linux-4.9.88\drivers\i2c\algos\i2c-algo-bit.c
int i2c_bit_add_numbered_bus(struct i2c_adapter *adap)
{
	return __i2c_bit_add_bus(adap, i2c_add_numbered_adapter); //继续往下看
}


/*
 * 函数 __i2c_bit_add_bus
 * 该函数的作用是注册 I2C bit-banging 算法到指定的 I2C 适配器,并执行相关的初始化。
 *
 * 参数:
 * - adap: 指向 I2C 适配器的指针。
 * - add_adapter: 用于将适配器注册到系统的回调函数。
 */
static int __i2c_bit_add_bus(struct i2c_adapter *adap,
			     int (*add_adapter)(struct i2c_adapter *))
{
	/* 获取与适配器关联的 bit-banging 数据 */
	struct i2c_algo_bit_data *bit_adap = adap->algo_data;
	int ret;

	/* 如果设置了 bit_test 标志,则进行总线测试 */
	if (bit_test) {
		ret = test_bus(adap);  // 测试 I2C 总线
		/* 如果 bit_test 标志值大于等于 2 且测试失败,返回 -ENODEV 表示设备不存在 */
		if (bit_test >= 2 && ret < 0)
			return -ENODEV;
	}

	/* 注册新的适配器到 I2C 模块 */
	adap->algo = &i2c_bit_algo;  --------(2.1)
    // 设置适配器的算法为 i2c_bit_algo
	adap->retries = 3;           // 设置重试次数为 3 次

	/* 如果没有设置 getscl 函数,表示不支持读取 SCL,引脚可能仅支持输出模式 */
	if (bit_adap->getscl == NULL)
		adap->quirks = &i2c_bit_quirk_no_clk_stretch;  // 记录适配器的 quirks,表示不支持时钟伸展

	/* 调用传入的回调函数将适配器注册到系统中 */
	ret = add_adapter(adap);
	if (ret < 0)
		return ret;  // 如果注册失败,则返回错误代码

	/* 检查是否能够读取 SCL */
	if (bit_adap->getscl == NULL) {
		/* 如果不能读取 SCL,引发警告,表示该 I2C 总线可能不完全符合 I2C 规范 */
		dev_warn(&adap->dev, "Not I2C compliant: can't read SCL\n");
		dev_warn(&adap->dev, "Bus may be unreliable\n");  // 总线可能不稳定
	}
	return 0;  // 返回 0 表示成功
}

(2.1)adap->algo = &i2c_bit_algo; 将适配器的 algo 设置为 i2c_bit_algo,这是 bit-banging I2C 的算法。 来看看i2c_bit_algo定义的内容:

const struct i2c_algorithm i2c_bit_algo = {
	.master_xfer	= bit_xfer,   // 这个是用于执行传输的函数,进入看
	.functionality	= bit_func,
};

/*
 * 函数 bit_xfer
 * 该函数通过 I2C bit-banging 机制传输 I2C 消息。
 *
 * 参数:
 * - i2c_adap: 指向 I2C 适配器的指针。
 * - msgs[]: 要传输的 I2C 消息数组。
 * - num: 消息数组中的消息数量。
 *
 * 返回值:
 * - 成功返回消息数量,失败返回负值表示错误。
 */
static int bit_xfer(struct i2c_adapter *i2c_adap,
		    struct i2c_msg msgs[], int num)
{
	struct i2c_msg *pmsg;  // 当前消息的指针
	struct i2c_algo_bit_data *adap = i2c_adap->algo_data;  // 获取适配器的 bit-banging 算法数据
	int i, ret;  // 循环计数器和返回值
	unsigned short nak_ok;  // 是否允许 NAK 信号(非应答信号)

	/* 在传输之前,执行可选的 pre_xfer 函数 */
	if (adap->pre_xfer) {
		ret = adap->pre_xfer(i2c_adap);
		if (ret < 0)
			return ret;  // 如果 pre_xfer 失败,则返回错误
	}

	/* 发送开始条件 */
	bit_dbg(3, &i2c_adap->dev, "emitting start condition\n");
	i2c_start(adap);  // 发送 I2C 开始信号

	for (i = 0; i < num; i++) {
		pmsg = &msgs[i];  // 获取当前消息
		nak_ok = pmsg->flags & I2C_M_IGNORE_NAK;  // 判断是否允许忽略 NAK(非应答)

		/* 如果没有设置 I2C_M_NOSTART 标志,则发送(重复)启动条件 */
		if (!(pmsg->flags & I2C_M_NOSTART)) {
			if (i) {
				/* 如果不是第一条消息,发送重复启动信号 */
				bit_dbg(3, &i2c_adap->dev, "emitting repeated start condition\n");
				i2c_repstart(adap);
			}
			/* 发送设备地址并进行检查 */
			ret = bit_doAddress(i2c_adap, pmsg);
			if ((ret != 0) && !nak_ok) {
				/* 如果设备地址返回 NAK 且不允许忽略,则退出 */
				bit_dbg(1, &i2c_adap->dev, "NAK from device addr 0x%02x msg #%d\n",
					msgs[i].addr, i);
				goto bailout;
			}
		}

		/* 根据消息类型(读/写)处理数据 */
		if (pmsg->flags & I2C_M_RD) {
			/* 读取数据到缓冲区 */
			ret = readbytes(i2c_adap, pmsg);
			if (ret >= 1)
				bit_dbg(2, &i2c_adap->dev, "read %d byte%s\n", ret, ret == 1 ? "" : "s");
			if (ret < pmsg->len) {
				/* 如果读取的字节数小于期望值,返回错误 */
				if (ret >= 0)
					ret = -EIO;
				goto bailout;
			}
		} else {
			/* 将缓冲区的数据写入设备 */
			ret = sendbytes(i2c_adap, pmsg); ------(2.2)
			if (ret >= 1)
				bit_dbg(2, &i2c_adap->dev, "wrote %d byte%s\n", ret, ret == 1 ? "" : "s");
			if (ret < pmsg->len) {
				/* 如果写入的字节数小于期望值,返回错误 */
				if (ret >= 0)
					ret = -EIO;
				goto bailout;
			}
		}
	}

	/* 所有消息成功传输,返回消息数量 */
	ret = i;

bailout:
	/* 发送停止条件 */
	bit_dbg(3, &i2c_adap->dev, "emitting stop condition\n");
	i2c_stop(adap);

	/* 在传输之后,执行可选的 post_xfer 函数 */
	if (adap->post_xfer)
		adap->post_xfer(i2c_adap);

	return ret;  // 返回结果
}

(2.2)上方中就实现了模拟IIC传输,怎么模拟的??以写为例进入看看,函数为ret = sendbytes(i2c_adap, pmsg);:

/*
 * 函数 sendbytes
 * 该函数将消息缓冲区中的字节数据发送到 I2C 设备。
 *
 * 参数:
 * - i2c_adap: 指向 I2C 适配器的指针。
 * - msg: 指向要发送的 I2C 消息的指针,包含数据缓冲区和相关标志。
 *
 * 返回值:
 * - 成功时返回写入的字节数,失败时返回负值表示错误。
 */
static int sendbytes(struct i2c_adapter *i2c_adap, struct i2c_msg *msg)
{
	const unsigned char *temp = msg->buf;  // 指向消息缓冲区的指针
	int count = msg->len;  // 要发送的字节数
	unsigned short nak_ok = msg->flags & I2C_M_IGNORE_NAK;  // 判断是否可以忽略 NAK
	int retval;  // 用于存储每次发送的结果
	int wrcount = 0;  // 成功发送的字节数

	/* 循环发送每一个字节 */
	while (count > 0) {
		/* 通过 I2C 发送当前字节 */
		retval = i2c_outb(i2c_adap, *temp);  -------下面介绍

		/* 如果发送成功(返回 ACK)或允许忽略 NAK(返回 0),继续 */
		if ((retval > 0) || (nak_ok && (retval == 0))) {
			count--;  // 减少剩余要发送的字节数
			temp++;  // 指向下一个要发送的字节
			wrcount++;  // 记录成功发送的字节数

		/* 如果设备返回 NAK 且不允许忽略 NAK,则退出 */
		} else if (retval == 0) {
			dev_err(&i2c_adap->dev, "sendbytes: NAK bailout.\n");
			return -EIO;  // 返回 I/O 错误

		/* 超时或其他错误(如总线仲裁丢失) */
		} else {
			dev_err(&i2c_adap->dev, "sendbytes: error %d\n", retval);
			return retval;  // 返回错误码
		}
	}

	/* 返回成功写入的字节数 */
	return wrcount;
}

/*
 * 函数 i2c_outb
 * 该函数将一个字节的 I2C 数据发送到总线,并等待从设备的应答。
 *
 * 参数:
 * - i2c_adap: 指向 I2C 适配器的指针。
 * - c: 要发送的字节数据。
 *
 * 返回值:
 * - 如果接收到 ACK,返回 1;如果没有接收到 ACK,返回 0;如果超时,返回 -ETIMEDOUT。
 */
static int i2c_outb(struct i2c_adapter *i2c_adap, unsigned char c)
{
	int i;  // 用于遍历字节中的各个位
	int sb; // 当前位
	int ack; // 用于存储从设备的应答
	struct i2c_algo_bit_data *adap = i2c_adap->algo_data;  // 获取 I2C 的算法位操作数据

	/* assert: scl is low */
	// 逐位发送字节(从高位到低位)
	for (i = 7; i >= 0; i--) {
		sb = (c >> i) & 1;  // 取出当前的位 (从高位开始)
		setsda(adap, sb);  // 设置 SDA 线的电平 (发送数据位)
		udelay((adap->udelay + 1) / 2);  // 等待半个延迟周期

		/* 提高时钟线,准备传输数据位 */
		if (sclhi(adap) < 0) {  // 检查 SCL 是否成功拉高 (超时检查)
			bit_dbg(1, &i2c_adap->dev, "i2c_outb: 0x%02x, "
				"timeout at bit #%d\n", (int)c, i);
			return -ETIMEDOUT;  // 如果超时则返回错误
		}

		// FIXME: 在这里进行仲裁检测(如果主设备发送了高电平但 SDA 被拉低,说明有其他设备仲裁总线)
		// if (sb && !getsda(adap)) -> 仲裁失败,退出传输

		scllo(adap);  // 将时钟线拉低,准备发送下一位
	}

	/* 发送完所有数据位后,将 SDA 拉高,准备接收 ACK */
	sdahi(adap);
	if (sclhi(adap) < 0) {  // 提高时钟线,准备读取 ACK 信号 (超时检查)
		bit_dbg(1, &i2c_adap->dev, "i2c_outb: 0x%02x, "
			"timeout at ack\n", (int)c);
		return -ETIMEDOUT;  // 超时则返回错误
	}

	/* 读取 ACK 信号: 如果从设备拉低 SDA 表示成功 (ACK),否则表示失败 (NAK) */
	ack = !getsda(adap);  // 如果 SDA 被拉低表示接收到 ACK (成功)
	bit_dbg(2, &i2c_adap->dev, "i2c_outb: 0x%02x %s\n", (int)c,
		ack ? "A" : "NA");  // 打印调试信息 (ACK 或 NAK)

	scllo(adap);  // 结束数据传输,将时钟线拉低

	return ack;  // 返回 ACK 的结果 (1 表示成功, 0 表示失败)
	/* assert: scl is low (sda undef) */
}

建议还是看着时序图去进行理解该段代码:
img

3.4 传输函数分析

drivers\i2c\algos\i2c-algo-bit.c📎i2c-algo-bit.c

/*
 * 函数 i2c_outb
 * 该函数将一个字节的 I2C 数据发送到总线,并等待从设备的应答。
 *
 * 参数:
 * - i2c_adap: 指向 I2C 适配器的指针。
 * - c: 要发送的字节数据。
 *
 * 返回值:
 * - 如果接收到 ACK,返回 1;如果没有接收到 ACK,返回 0;如果超时,返回 -ETIMEDOUT。
 */
static int i2c_outb(struct i2c_adapter *i2c_adap, unsigned char c)
{
	int i;  // 用于遍历字节中的各个位
	int sb; // 当前位
	int ack; // 用于存储从设备的应答
	struct i2c_algo_bit_data *adap = i2c_adap->algo_data;  // 获取 I2C 的算法位操作数据

	/* assert: scl is low */
	// 逐位发送字节(从高位到低位)
	for (i = 7; i >= 0; i--) {
		sb = (c >> i) & 1;  // 取出当前的位 (从高位开始)
		setsda(adap, sb);  // 设置 SDA 线的电平 (发送数据位)
		udelay((adap->udelay + 1) / 2);  // 等待半个延迟周期

		/* 提高时钟线,准备传输数据位 */
		if (sclhi(adap) < 0) {  // 检查 SCL 是否成功拉高 (超时检查)
			bit_dbg(1, &i2c_adap->dev, "i2c_outb: 0x%02x, "
				"timeout at bit #%d\n", (int)c, i);
			return -ETIMEDOUT;  // 如果超时则返回错误
		}

		// FIXME: 在这里进行仲裁检测(如果主设备发送了高电平但 SDA 被拉低,说明有其他设备仲裁总线)
		// if (sb && !getsda(adap)) -> 仲裁失败,退出传输

		scllo(adap);  // 将时钟线拉低,准备发送下一位
	}

	/* 发送完所有数据位后,将 SDA 拉高,准备接收 ACK */
	sdahi(adap);
	if (sclhi(adap) < 0) {  // 提高时钟线,准备读取 ACK 信号 (超时检查)
		bit_dbg(1, &i2c_adap->dev, "i2c_outb: 0x%02x, "
			"timeout at ack\n", (int)c);
		return -ETIMEDOUT;  // 超时则返回错误
	}

	/* 读取 ACK 信号: 如果从设备拉低 SDA 表示成功 (ACK),否则表示失败 (NAK) */
	ack = !getsda(adap);  // 如果 SDA 被拉低表示接收到 ACK (成功)
	bit_dbg(2, &i2c_adap->dev, "i2c_outb: 0x%02x %s\n", (int)c,
		ack ? "A" : "NA");  // 打印调试信息 (ACK 或 NAK)

	scllo(adap);  // 结束数据传输,将时钟线拉低

	return ack;  // 返回 ACK 的结果 (1 表示成功, 0 表示失败)
	/* assert: scl is low (sda undef) */
}

在上面已经介绍过啦,这里就额外提出来让大家看的清楚一点。

4. 怎么使用I2C-GPIO

使用 I2C-GPIO 驱动时,可以通过在设备树中添加相应的节点来配置 I2C 总线。以下是一个示例代码以及如何设置 I2C 总线所需的各个属性:

&i2c_gpio {
    compatible = "i2c-gpio";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_i2c_gpio>;    /* 配置GPIO引脚为SDA和SCL */
    
    /* GPIO配置 */
    gpios = <&gpio0 3 0>,   /* SDA 引脚,GPIO 0_3 */
            <&gpio0 4 0>;   /* SCL 引脚,GPIO 0_4 */

    i2c-gpio,sda-open-drain;  /* SDA 已经被配置为开漏模式 */
    i2c-gpio,scl-open-drain;  /* SCL 已经被配置为开漏模式 */

    /* 指定时钟频率:两种方式任选其一 */
    i2c-gpio,delay-us = <5>;       /* 设置延迟,100kHz */
    clock-frequency = <400000>;    /* 设置 I2C 频率为 400kHz */

    #address-cells = <1>;  /* 指定地址单元大小 */
    #size-cells = <0>;     /* 指定大小单元大小 */

    /* 在此处可以添加I2C设备子节点 */
    eeprom@50 {
        compatible = "at24,24c04";
        reg = <0x50>;     /* I2C 设备地址 */
    };
};
  1. compatible 属性:

    • "i2c-gpio":定义此节点为 I2C-GPIO 驱动程序实例。
  2. 引脚控制 (pinctrl):

    • pinctrl-0: 配置 I2C 使用的 GPIO 引脚,将其设置为开漏模式,可以通过 pinctrl 设置。
    • gpios: 定义 SDA 和 SCL 的 GPIO 引脚编号。
      • <&gpio0 3 0>: 指定 SDA 引脚,表示 GPIO 0 的第 3 号引脚。
      • <&gpio0 4 0>: 指定 SCL 引脚,表示 GPIO 0 的第 4 号引脚。
  3. 时钟频率:

    • 可以通过两种方式指定频率:
      • i2c-gpio,delay-us = <5>;:设置延迟时间为 5 微秒,频率大约为 100 kHz。
      • clock-frequency = <400000>;:直接设置时钟频率为 400 kHz。
  4. Open-drain 配置:

    • i2c-gpio,sda-open-drain: 该属性表示 SDA 已被其他系统或驱动配置为开漏模式,驱动程序不需要再进行配置。
    • i2c-gpio,scl-open-drain: 类似的,表示 SCL 引脚已经被配置为开漏模式。
  5. #address-cells 和 #size-cells:

    • 这些属性指定了 I2C 设备的地址和大小单元,通常分别设置为 1 和 0。
  6. 子设备节点:

    • 可以在 I2C-GPIO 节点下添加 I2C 设备,如 eeprom@50,并为其指定 compatible 属性和 I2C 地址 (reg 属性)。

关键步骤:

  • SDA 和 SCL 使用哪一对 GPIO 引脚。
  • 是否已经配置为开漏模式,影响驱动是否需要重新配置。
  • 通过 delay-us 或 clock-frequency 设置 I2C 时钟频率。
  • 在节点中定义所连接的 I2C 从设备。

通过以上设备树配置,系统就能通过 GPIO 引脚来模拟 I2C 总线,从而与 I2C 设备进行通信。

更多推荐