# I2C 调试指南

## 概述

X5 Soc 提供了标准的 I2C 总线，包含串行数据线（SDA）和串行时钟线（SCL），连接在 I2C 总线上的器件分为主设备和从设备，主设备和从设备之间通过 I2C 进行数据通信。

## 特点

I2C 控制器支持以下功能：

- 支持三种速度模式：
  - standard mode(0-100Kb/s);
  - fast mode(<=400Kb/s) && fast mode plus(<=1000Kb/s);
  - high-speed mode(<=3.4Mb/s)。

- 支持主从模式配置

- 支持7位和10位寻址模式

X5总共提供8个 I2C 控制器，其中7个（I2C0~6）位于 LSIO 子系统，1个(I2C7)位于 DSP 子系统。
8个 I2C 控制器中只有 I2C4 支持 high-speed mode。

### 典型应用

I2C 通讯设备之间的常用连接方式如下图，常用于控制摄像头模块、音频设备和显示模块，实现图像的捕获、音频处理和显示。

![image-20241216-223057](_static/_images/9-I2C_Debug_Guide_zh_CN/image-20241216-223057.png)

### 功能原理

I2C 总线是一种同步串行通信协议，允许多个从设备（slave devices）通过时钟线（SCL）和数据线（SDA）与一个主设备（master device）通信。每个从设备都有一个唯一的地址，主设备通过这些地址与特定的从设备通信。


## 驱动代码
```
drivers/i2c/i2c-dev.c 				# I2C 字符设备接口代码
drivers/i2c/i2c-core-base.c 			# I2C 框架代码
drivers/i2c/busses/i2c-designware-platdrv.c 	# I2C 驱动代码源文件
```
### 内核配置位置
```
/* arch/arm64/configs/hobot_x5_soc_defconfig */
CONFIG_I2C_CHARDEV=y 			# I2C 驱动应用层配置宏
CONFIG_I2C_DESIGNWARE_PLATFORM=y 	# DW I2C 驱动配置宏
```

### 内核 DTS 节点配置

X5 SOC 总共支持8路 I2C 总线。 |
X5 I2C 控制器的设备树定义位于 BSP 源码包的 kernel 文件夹下的`arch/arm64/boot/dts/hobot/x5.dtsi`文件内。

<font color=red>备注：</font>
x5.dtsi 中的节点主要声明 SoC 共有特性，和具体电路板无关，通常情况下无需修改。

## I2C 使用

在嵌入式 Linux 系统中，I2C 总线的使用经常涉及到 U-Boot 阶段使用、Kernel 阶段使用和用户空间（User Space）使用。在这三个阶段，I2C 的配置和操作各有不同，下面介绍各阶段的使用方法。

### <span id="I2C_uboot_use"/>I2C Uboot 阶段使用

在 U-Boot 中，i2c 命令用于调试和访问 I2C 总线及其挂载的设备。通过该命令，可以进行 I2C 总线扫描、读取/写入 I2C 设备寄存器等操作。

输入 help i2c 可以查看 i2c 命令的帮助信息：

```
=> help i2c
i2c - I2C sub-system

Usage:
i2c bus [muxtype:muxaddr:muxchannel] - show I2C bus info
i2c crc32 chip address[.0, .1, .2] count - compute CRC32 checksum
i2c dev [dev] - show or set current I2C bus
i2c loop chip address[.0, .1, .2] [# of objects] - looping read of device
i2c md chip address[.0, .1, .2] [# of objects] - read from I2C device
i2c mm chip address[.0, .1, .2] - write to I2C device (auto-incrementing)
i2c mw chip address[.0, .1, .2] value [count] - write to I2C device (fill)
i2c nm chip address[.0, .1, .2] - write to I2C device (constant address)
i2c probe [address] - test for and show device(s) on the I2C bus
i2c read chip address[.0, .1, .2] length memaddress - read to memory
i2c write memaddress chip address[.0, .1, .2] length [-s] - write memory
          to I2C; the -s option selects bulk write in a single transaction
i2c flags chip [flags] - set or get chip flags
i2c olen chip [offset_length] - set or get chip offset length
i2c reset - re-init the I2C Controller
i2c speed [speed] - show or set I2C bus speed
```

1.显示 I2C 总线信息

```bash
=> i2c bus
Bus 0:  i2c@340d0000
```

2.选择当前的 I2C 总线

```bash
=> i2c dev 0
Setting bus to 0
```

3.通过 i2c probe 命令扫描总线上设备的地址

```bash
=> i2c probe
Valid chip addresses: 1C
```
输出中列出了当前 I2C 总线上响应的设备地址，这里是0x1C。

4.设置 I2C 总线速度

查看当前 I2C 总线的速度：

```bash
=> i2c speed
Current bus speed=100000
```

设置 I2C 总线的速度为 400 KHz：

```bash
=> i2c speed 400000
Setting bus speed to 400000 Hz
```

5.读取 I2C 设备的寄存器

使用 i2c md 命令读取 I2C 设备的寄存器数据：

```bash
=> i2c md 0x1C 0x00 0x20
0000: 06 0f 0f c0 00 00 00 00 3f 33 61 0d 09 00 00 00    ........?3a.....
0010: ff 00 00 00 00 00 00 33 a0 15 a0 33 a0 24 dc 33    .......3...3.$.3
```
0x1C 是设备地址（根据扫描到的设备地址填写）。
0x00 是寄存器起始地址。
0x20 是读取的字节数。

6.向 I2C 设备写入数据

使用 i2c mw 命令向 I2C 设备寄存器写入数据：

```bash
=> i2c mw 0x1C 0x00 06 1
```
0x1C 是设备地址。
0x00 是寄存器地址。
06 是要写入的数据。
1 表示写入 1 个字节。


### I2C Kernel 阶段使用
#### I2C 速度配置
默认的 I2C 速率为100Kb/s，支持100k/200k/400k/1M/3.4M 四种速率，可通过修改 DTS 中相应 I2C 节点的 clock-frequency 等字段完成速率修改。 \
以修改 i2c4 控制器的运行速率为例：
- 如果需要使用3.4MHz，需要在对应板卡对应的 dts 的 i2c4 的节点内配置以下参数：
  ```dts
  ...
  &i2c4 {
  	status = "okay";
  	pinctrl-names = "default";
  	pinctrl-0 = <&pinctrl_i2c4>;
  	clock-frequency = <3400000>;
  	i2c-scl-falling-time-ns = <78>;
  	i2c-sda-falling-time-ns = <78>;
  }
  ...
  ```

- 如果需要使用1MHz，需要在对应板卡对应的 dts 的 i2c4 的节点内配置以下参数：
  ```dts
  ...
  &i2c4 {
  	status = "okay";
  	pinctrl-names = "default";
  	pinctrl-0 = <&pinctrl_i2c4>;
  	clock-frequency = <1000000>;
  	i2c-scl-falling-time-ns = <60>;
  	i2c-sda-falling-time-ns = <60>;
  }
  ...
  ```

- 如果需要使用400kHz，需要在对应板卡对应的 dts 的 i2c4 的节点内配置以下参数：
  ```dts
  ...
  &i2c4 {
  	status = "okay";
  	pinctrl-names = "default";
  	pinctrl-0 = <&pinctrl_i2c4>;
  	clock-frequency = <400000>;
  	i2c-scl-falling-time-ns = <190>;
  }
  ...
  ```


字段说明：

- status：
  表示节点状态，"okay" 表示该设备启用，"disabled" 表示设备禁用。

- pinctrl-names 和 pinctrl-0：
  通过引脚控制子系统（Pin Control）指定该 I2C 控制器所使用的引脚配置。

- clock-frequency：
  设置 I2C 总线的时钟频率，单位为 Hz。例如，<400000> 表示 400kHz。

- i2c-scl-falling-time-ns 和 i2c-sda-falling-time-ns：
  这两个参数用于配置 I2C 时钟线（SCL）和数据线（SDA）的下降时间，单位为纳秒（ns）。

<font color=red>备注：</font>
“i2c-scl-falling-time-ns”及“i2c-sda-falling-time-ns”这两个参数在不同的 PCB 设计上可能需要通过示波器测量后调整。

### User Space

通常，I2C 设备由内核驱动程序控制，但也可以从用户态访问总线上的所有设备，通过/dev/i2c-%d 接口来访问，关于这一功能，内核文档中的 Documentation/i2c/dev-interface 提供了详细的说明。

下面是一个简单的i2c的用户态访问代码实例：
```C
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <getopt.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>

/**
 * 打开指定的 I2C 总线设备文件
 */
static int open_i2c_bus(uint32_t bus)
{
	char filename[20];
	snprintf(filename, sizeof(filename), "/dev/i2c-%d", bus);
	int file = open(filename, O_RDWR);
	if (file < 0)
	{
		perror("Failed to open the I2C bus");
	}
	return file;
}

/**
 * 设置 I2C 地址
 */
static int set_i2c_address(int file, uint8_t i2c_addr)
{
	if (ioctl(file, I2C_SLAVE, i2c_addr) < 0)
	{
		perror("Failed to set I2C address");
		return -1;
	}
	return 0;
}

/**
 * I2C 读操作：16-bit 寄存器，16-bit 数据
 */
static int32_t i2c_read_reg16_data16(int file, uint8_t i2c_addr, uint16_t reg_addr, uint16_t *value)
{
	uint8_t sendbuf[2];
	uint8_t readbuf[2];
	struct i2c_rdwr_ioctl_data data;
	struct i2c_msg msgs[2];

	sendbuf[0] = (uint8_t)((reg_addr >> 8u) & 0xFF);
	sendbuf[1] = (uint8_t)(reg_addr & 0xFF);

	msgs[0].addr = i2c_addr;
	msgs[0].flags = 0; // 写操作
	msgs[0].len = 2;
	msgs[0].buf = sendbuf;

	msgs[1].addr = i2c_addr;
	msgs[1].flags = I2C_M_RD; // 读操作
	msgs[1].len = 2;
	msgs[1].buf = readbuf;

	data.msgs = msgs;
	data.nmsgs = 2;

	if (ioctl(file, I2C_RDWR, &data) < 0)
	{
		perror("Failed to read from the I2C bus");
		return -1;
	}

	*value = (uint16_t)((readbuf[0] << 8) | readbuf[1]);
	return 0;
}

/**
 * I2C 写操作：16-bit 寄存器，16-bit 数据
 */
static int32_t i2c_write_reg16_data16(int file, uint8_t i2c_addr, uint16_t reg_addr, uint16_t value)
{
	uint8_t sendbuf[4];

	sendbuf[0] = (uint8_t)((reg_addr >> 8u) & 0xFF);
	sendbuf[1] = (uint8_t)(reg_addr & 0xFF);
	sendbuf[2] = (uint8_t)((value >> 8u) & 0xFF);
	sendbuf[3] = (uint8_t)(value & 0xFF);

	if (write(file, sendbuf, sizeof(sendbuf)) != sizeof(sendbuf))
	{
		perror("Failed to write to the I2C bus");
		return -1;
	}

	return 0;
}

/**
 * I2C 读操作：16-bit 寄存器，8-bit 数据
 */
static int32_t i2c_read_reg16_data8(int file, uint8_t i2c_addr, uint16_t reg_addr, uint8_t *value)
{
	uint8_t sendbuf[2];
	uint8_t readbuf[1];
	struct i2c_rdwr_ioctl_data data;
	struct i2c_msg msgs[2];

	sendbuf[0] = (uint8_t)((reg_addr >> 8u) & 0xFF);
	sendbuf[1] = (uint8_t)(reg_addr & 0xFF);

	msgs[0].addr = i2c_addr;
	msgs[0].flags = 0; // 写操作
	msgs[0].len = 2;
	msgs[0].buf = sendbuf;

	msgs[1].addr = i2c_addr;
	msgs[1].flags = I2C_M_RD; // 读操作
	msgs[1].len = 1;
	msgs[1].buf = readbuf;

	data.msgs = msgs;
	data.nmsgs = 2;

	if (ioctl(file, I2C_RDWR, &data) < 0)
	{
		perror("Failed to read from the I2C bus");
		return -1;
	}

	*value = readbuf[0];
	return 0;
}

/**
 * I2C 写操作：16-bit 寄存器，8-bit 数据
 */
static int32_t i2c_write_reg16_data8(int file, uint8_t i2c_addr, uint16_t reg_addr, uint8_t value)
{
	uint8_t sendbuf[3];

	sendbuf[0] = (uint8_t)((reg_addr >> 8u) & 0xFF);
	sendbuf[1] = (uint8_t)(reg_addr & 0xFF);
	sendbuf[2] = value;

	if (write(file, sendbuf, sizeof(sendbuf)) != sizeof(sendbuf))
	{
		perror("Failed to write to the I2C bus");
		return -1;
	}

	return 0;
}

/**
 * 显示帮助信息
 */
void print_help(const char *prog_name)
{
	printf("Usage: %s [options]\n", prog_name);
	printf("Options:\n");
	printf("  -b, --bus <bus>        I2C bus number\n");
	printf("  -a, --addr <address>   I2C device address\n");
	printf("  -r, --reg <reg>        Register address\n");
	printf("  -v, --value <value>    Value to write\n");
	printf("  -w, --write            Write mode (default is read)\n");
	printf("  -x, --16bit            Use 16-bit data (default is 8-bit)\n");
	printf("  -h, --help             Show this help message\n");
}

/**
 * 主函数：处理用户输入和 I2C 操作
 */
int main(int argc, char **argv)
{

	uint32_t bus = 0;
	uint8_t i2c_addr = 0;
	uint16_t reg_addr = 0;
	uint16_t value16 = 0;
	uint8_t value8 = 0;
	int is_read = 1;  // 默认读操作
	int is_16bit = 0; // 默认 8-bit 数据

	static struct option long_options[] = {
		{"bus", required_argument, 0, 'b'},
		{"addr", required_argument, 0, 'a'},
		{"reg", required_argument, 0, 'r'},
		{"value", required_argument, 0, 'v'},
		{"write", no_argument, 0, 'w'},
		{"16bit", no_argument, 0, 'x'},
		{"help", no_argument, 0, 'h'},
		{0, 0, 0, 0}};
	int opt;
	while ((opt = getopt_long(argc, argv, "b:a:r:v:wxh", long_options, NULL)) != -1)
	{
		switch (opt)
		{
		case 'b':
			bus = strtoul(optarg, NULL, 0);
			break;
		case 'a':
			i2c_addr = (uint8_t)strtoul(optarg, NULL, 0);
			break;
		case 'r':
			reg_addr = (uint16_t)strtoul(optarg, NULL, 0);
			break;
		case 'v':
			if (is_16bit)
				value16 = (uint16_t)strtoul(optarg, NULL, 0);
			else
				value8 = (uint8_t)strtoul(optarg, NULL, 0);
			is_read = 0;
			break;
		case 'w':
			is_read = 0;
			break;
		case 'x':
			is_16bit = 1;
			break;
		case 'h':
		default:
			print_help(argv[0]);
			return 0;
		}
	}
	if (argc == 1) {
		print_help(argv[0]);
		return 0;
	}
	int file = open_i2c_bus(bus);
	if (file < 0)
		return -1;

	if (set_i2c_address(file, i2c_addr) < 0)
	{
		close(file);
		return -1;
	}

	if (is_read)
	{
		if (is_16bit)
		{
			if (i2c_read_reg16_data16(file, i2c_addr, reg_addr, &value16) == 0)
			{
				printf("Read 16-bit value: 0x%04X\n", value16);
			}
		}
		else
		{
			if (i2c_read_reg16_data8(file, i2c_addr, reg_addr, &value8) == 0)
			{
				printf("Read 8-bit value: 0x%02X\n", value8);
			}
		}
	}

	if (!is_read)
	{
		if (is_16bit)
		{
			if (i2c_write_reg16_data16(file, i2c_addr, reg_addr, value16) == 0)
			{
				printf("Wrote 16-bit value: 0x%04X\n", value16);
			}
		}
		else
		{
			if (i2c_write_reg16_data8(file, i2c_addr, reg_addr, value8) == 0)
			{
				printf("Wrote 8-bit value: 0x%02X\n", value8);
			}
		}
	}

	close(file);
	return 0;
}
```

测试命令及结果参考如下(X5 为主设备，sc230 为从设备，接入 MIPI CSI3 口，挂载在 I2C bus 7)：

```bash
#读取 I2C 总线 7 上地址为 0x30 的设备中寄存器地址 0x3e01 的 8 位数据：
#./i2c_tool -b 7 -a 0x30 -r 0x3e01
Read 8-bit value: 0x02

#读取 I2C 总线 7 上地址为 0x30 的设备中寄存器地址 0x3e01 的 16 位数据：
#./i2c_tool -b 7 -a 0x30 -r 0x3e01 -x
Read 16-bit value: 0x0200

#向 I2C 总线 7 上地址为 0x30 的设备中寄存器地址 0x3e01 写入 8 位数据 0x1A
#./i2c_tool -b 7 -a 0x30 -r 0x3e01 -v 0x5A
Wrote 8-bit value: 0x5A

#向 I2C 总线 7 上地址为 0x30 的设备中寄存器地址 0x3e01 写入 16 位数据 0x1A10
#./i2c_tool -b 7 -a 0x30 -r 0x3e01 -v 0x1A10 -w -x
Wrote 16-bit value: 0x1A10
```

**注意**：当前测试使用的是 MIPI CSI3 接口，其他接口对应的 I2C bus 信息可以参考[摄像头 ( MIPI CSI) 接口](../../quick_start/x5_evb_1_b_user_guide.html#span-id-mipi-csi-port-mipi-csi)章节。

#### 开源工具：i2c-tools

i2c-tools 是一套开源工具，该工具已经被交叉编译并包含在在 X5 系统软件的 rootfs 中，客户可以直接使用：

-   i2cdetect — 用来列举 I2C bus 及该 bus 上的所有设备
-   i2cdump — 显示 I2C 设备的所有 register 值
-   i2cget — 读取 I2C 设备某个 register 的值
-   i2cset — 写入 I2C 设备某个 register 的值
-   i2ctransfer — 可以读、写 I2C 设备某个或者多个 register 的值

#### I2C Sysfs 频率控制节点

X5实现了内核 I2C 控制器 sysfs 节点内配置 I2C 控制器运行频率的功能：
```bash
root@buildroot:/# ls /sys/class/i2c-adapter/i2c-4/speed
/sys/class/i2c-adapter/i2c-4/speed
root@buildroot:~# ls /sys/class/i2c-adapter/i2c-4/scl-falling-time-ns
/sys/class/i2c-adapter/i2c-4/scl-falling-time-ns
root@buildroot:~# ls /sys/class/i2c-adapter/i2c-4/sda-falling-time-ns
/sys/class/i2c-adapter/i2c-4/sda-falling-time-ns
```

使用 cat 命令获取当前 I2C 控制器的运行频率和当前配置的 SCL/SDA 下降沿时间参数：
```bash
root@buildroot:/# cat /sys/class/i2c-adapter/i2c-4/speed
100000
root@buildroot:~# cat /sys/class/i2c-adapter/i2c-4/scl-falling-time-ns
0ns
root@buildroot:~# cat /sys/class/i2c-adapter/i2c-4/sda-falling-time-ns
0ns
```

使用 echo 命令修改目标频率（单位：Hz）配置到对应控制器的 speed 节点：
``` bash
root@buildroot:/# echo 400000 > /sys/class/i2c-adapter/i2c-4/speed
root@buildroot:/# cat /sys/class/i2c-adapter/i2c-4/speed
400000
```

修改 I2C 下降沿时间参数：
``` bash
root@buildroot:/# echo 190 > /sys/class/i2c-adapter/i2c-4/scl-falling-time-ns
root@buildroot:/# cat /sys/class/i2c-adapter/i2c-4/scl-falling-time-ns
190ns
```

<font color=red>注意：</font>
- 操作 sysfs 速率配置节点**不会**修改 dts 内配置的“i2c-scl-falling-time-ns”以及“i2c-sda-falling-time-ns”。因此，在修改 sysfs 节点配置速率后，需要根据 PCB 的实际情况，配置 scl-falling-time-ns 及 sda-falling-time-ns。
- scl-falling-time-ns 及 sda-falling-time-ns 主要作为调试节点提供，推荐用户通过 DTS 配置默认参数，而不是直接修改 sysfs 节点。
- 配置不正确的 scl-falling-time-ns 和 sda-falling-time-ns 可能导致 I2C 控制器<font color=red>**无法正常运作**</font>，请谨慎使用。

## I2C 常见问题

在使用 I2C 总线过程中，可能会遇到一些典型问题，以下列出了常见问题及对应的排查和解决方法。

### 驱动能力不足
**问题描述：**
在 I2C 通信过程中，i2c 信号无法被有效拉低，导致通信失败。

**可能原因：**
- I2C 控制器当前配置的驱动能力不足，无法提供足够的电流拉低 i2c 信号线。
- 硬件上拉电阻配置不合理，阻值过低，影响了 I2C 的下拉电平。

**排查方法：**
- 示波器观测波形：
  - 通过示波器检测 I2C 信号波形，重点查看信号是否能够被有效拉低到逻辑低电平，根据主从设备阈值并留有一定裕量。

- 检查硬件电阻配置：
  - 确认 SDA 和 SCL 信号线上拉电阻的阻值，推荐值为 速率 <=400Kb/s 时，使用2.2K、4.7K 上拉；速率 > 400Kb/s 时，使用1K 上拉；若发现问题，可以尝试减小上拉电阻值，以平衡 i2c 下拉能力。

**解决方案：**

- 修改设备树增加驱动能力，以 i2c4为例参照修改，并修改字段 drive-strength。
```dts
pconf_drv_pu_ds4_1v8: pconf-dev-pu-ds4-1v8 {
	bias-pull-up;
	power-source = <HORIZON_IO_PAD_VOLTAGE_1V8>;
	drive-strength = <4>;
};

&i2c4grp {
	horizon,pins = <
		LSIO_I2C4_SCL  LSIO_PINMUX_3 BIT_OFFSET0  MUX_ALT0 &pconf_drv_pu_ds4_1v8
		LSIO_I2C4_SDA  LSIO_PINMUX_3 BIT_OFFSET2  MUX_ALT0 &pconf_drv_pu_ds4_1v8
	>;
};
```
- 优化上拉电阻：
根据实际的波形情况，调整 SDA 和 SCL 线的上拉电阻，减小上拉阻值以提升信号的驱动能力。

### 设备供电时序异常

**问题描述：**
在 I2C 通信初始化阶段，主设备尝试访问从设备，但通信失败，设备无法响应。

**可能原因：**

- 从设备的供电初始化时序错误或较慢，导致设备未准备好就被主设备访问。

**排查方法：**

- 检测从设备供电时序：
  - 使用示波器或逻辑分析仪观察从设备的上电时序，确认供电是否稳定，是否符合 Spec 的要求。

- 确认从设备工作状态：
  - 使用 i2cdetect 工具扫描 I2C 总线，验证从设备是否能被正常检测到：
```
i2cdetect -y 4  # 4为 I2C 控制器实际编号
```
如果未检测到从设备地址，说明设备尚未准备好或供电存在问题。

**解决方案：**
- 从设备上电时序由主设备控制时，需要确认上电时序是否符合要求，并确保从设备初始化完成后再启动 I2C 通信。