4.3.9. I2C 调试指南

4.3.9.1. 概述

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

4.3.9.2. 特点

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

功能原理

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

4.3.9.3. 驱动代码

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文件内。

备注: x5.dtsi 中的节点主要声明 SoC 共有特性,和具体电路板无关,通常情况下无需修改。

4.3.9.4. I2C 使用

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

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 总线信息

=> i2c bus
Bus 0:  i2c@340d0000

2.选择当前的 I2C 总线

=> i2c dev 0
Setting bus to 0

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

=> i2c probe
Valid chip addresses: 1C

输出中列出了当前 I2C 总线上响应的设备地址,这里是0x1C。

4.设置 I2C 总线速度

查看当前 I2C 总线的速度:

=> i2c speed
Current bus speed=100000

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

=> i2c speed 400000
Setting bus speed to 400000 Hz

5.读取 I2C 设备的寄存器

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

=> 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 设备寄存器写入数据:

=> 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 的节点内配置以下参数:

    ...
    &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 的节点内配置以下参数:

    ...
    &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 的节点内配置以下参数:

    ...
    &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)。

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

User Space

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

下面是一个简单的i2c的用户态访问代码实例:

#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):

#读取 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) 接口章节。

开源工具: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 控制器运行频率的功能:

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 下降沿时间参数:

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 节点:

root@buildroot:/# echo 400000 > /sys/class/i2c-adapter/i2c-4/speed
root@buildroot:/# cat /sys/class/i2c-adapter/i2c-4/speed
400000

修改 I2C 下降沿时间参数:

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

注意:

  • 操作 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 控制器无法正常运作,请谨慎使用。

4.3.9.5. I2C 常见问题

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

驱动能力不足

问题描述: 在 I2C 通信过程中,i2c 信号无法被有效拉低,导致通信失败。

可能原因:

  • I2C 控制器当前配置的驱动能力不足,无法提供足够的电流拉低 i2c 信号线。

  • 硬件上拉电阻配置不合理,阻值过低,影响了 I2C 的下拉电平。

排查方法:

  • 示波器观测波形:

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

  • 检查硬件电阻配置:

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

解决方案:

  • 修改设备树增加驱动能力,以 i2c4为例参照修改,并修改字段 drive-strength。

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 通信。