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 通讯设备之间的常用连接方式如下图,常用于控制摄像头模块、音频设备和显示模块,实现图像的捕获、音频处理和显示。

功能原理
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 通信。