当前位置:首页>Linux>Linux I2C调试技巧

Linux I2C调试技巧

  • 2026-09-15 12:10:53
Linux I2C调试技巧
大家好,我是王鸽,调试编写驱动涉及软件硬件问题,一定需要分层思考,切忌胡子眉毛一把抓,分类讨论,遵循先硬件、后软件、逐层排查的思路,核心是用i2c-tools快速验证总线与设备,结合内核日志定位驱动 / 协议问题,再用示波器 / 逻辑分析仪抓时序,最后通过代码调试解决驱动逻辑问题。
一.硬件排查(最常见问题来源)
基础检查:
  1. 接线:SDA/SCL 是否接反、是否虚焊 / 接触不良、GND 是否共地;
  2. 供电:芯片的供电是不是按照规格书范围,电压电平(3.3V/5V)是否匹配;
  3. 时序:上电的时序是不是正常,reset  pwd 引脚等是不是上电按照规格书?同样的下电有没有按照规格书?这种尤其是调试mipi 接口的camera 中很重要的;
  4. 上拉电阻多少的?
I2C的上拉电阻可以是1.5K,2.2K,4.7K, 电阻的大小对时序有一定影响,对信号的上升时间和下降时间也有影响,一般接1.5K或2.2K。

硬件工具(时序问题必备)

示波器:看 SCL/SDA 电平、上升沿、是否有毛刺、ACK 是否正。
SCL 有没有波形出来, 速率多大;如果没有拆掉外设芯片后,下发命令还有没有clock 波形出来,没有的话是I2C总线有问题了,利用示波器的single 按键加上上下沿触发方式tigger 抓取波形。另外同个通道有没有挂载其他的外设芯片,它们的通讯是否成功,这个也是一个判断的依据。
逻辑分析仪:解码 I2C 协议,看地址、数据、ACK/NACK、仲裁、时钟延展。
常见波形问题:
  • 无 ACK:从机未响应(地址错 / 未上电 / 接线断)
  • 上升沿过缓:增大上拉、缩短走线、降低频率
  • 仲裁丢失:多主机冲突、总线被拉死

二、必备调试工具(优先用工具验证)

在写代码前,先用 Linux 自带的 i2c-tools 验证 I2C 读写,能快速区分 “硬件问题” 和 “代码问题”。

1. 安装工具

# Debian/Ubuntu 系列apt install i2c-tools# CentOS/RHEL 系列yum install i2c-tools

2. 核心工具用法(基础验证)

# 1. 查看系统所有 I2C 总线(确认总线编号)i2cdetect -l# 2. 扫描指定总线(如 0 号总线)上的外设地址(验证外设是否被识别)i2cdetect -y 0# 3. 读取外设寄存器(核心验证:addr=外设地址,reg=寄存器地址)# 格式:i2cget -y 总线号 外设地址 寄存器地址i2cget -y 0 0x48 0x00# 4. 写入外设寄存器(验证写功能)# 格式:i2cset -y 总线号 外设地址 寄存器地址 写入值i2cset -y 0 0x48 0x01 0x05
  • 如果工具操作返回 Remote I/O error:优先排查硬件(地址错误、上拉电阻、外设供电、虚焊);
  • 如果工具操作成功:说明硬件无问题,问题出在代码层面。
三、设备配置确认

确认硬件与设备树 / 板级配置匹配

这是调试的前提,90% 的初期问题都源于配置不匹配:

  • 1.核对 I2C 总线编号
    Linux 中 I2C 控制器对应 /dev/i2c-X(X 为总线号),需确认设备树中 i2c@xxxx 的 reg 地址、中断号与硬件手册一致;
  • 2.核对外设地址
    I2C 外设地址(7 位 / 10 位)必须正确(比如常见的 0x48、0x50),可通过 i2cdetect -y X 扫描总线(X 为总线号),确认外设是否被识别:
# 扫描 0 号 I2C 总线,查看挂载的外设地址
i2cdetect -y 0//I2C总线有没有挂载外设?
比如:/sys/bus/i2c/devices
0-0051  1-0032  i2c-0   i2c-1
3.I2C 通道有没有对?
  • 比如:ls /sys/bus/i2c/devices/i2c-0
    0-0051         delete_device  device         i2c-dev        name           new_device     of_node        power          subsystem
  • I2C地址在设备树配置有没有对?
    4.在最底层的读写上加上log 看寄存器有没有读写数据正确?对着数据手册查看。
    5.中断引脚配置上,触发条件的设置比如
    interrupt-parent = <&gpio1>;        interrupts = <17 IRQ_TYPE_LEVEL_LOW>;       interrupt-controller;       #interrupt
    6.I2C的最大频率设置
    clock-frequency = <400000>;
    S(标准模式)测量与控制场合,100kb/s
    F(快速模式),速率为 400kb/s
    Hs(高速模式),速率为 3.4Mb/s。
  • 具体看器件要求,单片机一般是400k或以下常用。IIC协议是有规定的,其总线的容性负载要求,目前最高的1M左右。普通的芯片只有 低速 100K 与 高速 400K 两种规格。

三、应用层 I2C 读写函数调试(简单易上手)

应用层通过 /dev/i2c-X 字符设备操作 I2C,代码简单,适合快速验证读写逻辑。

1. 应用层读写函数示例(带调试日志)

#include<stdio.h>#include<fcntl.h>#include<unistd.h>#include<sys/ioctl.h>#include<errno.h>#include<string.h>#include<linux/i2c-dev.h>// 调试宏:打印错误信息和行号#define DBG_ERR(fmt, ...) fprintf(stderr, "ERROR [%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)// I2C 读寄存器函数(带详细调试)inti2c_read_reg(int fd, unsignedchar dev_addr, unsignedchar reg_addr, unsignedchar *val){    // 步骤1:设置外设地址(调试:打印关键参数)    printf("【调试】设置 I2C 外设地址:0x%02x\n", dev_addr);    if (ioctl(fd, I2C_SLAVE, dev_addr) < 0) {        DBG_ERR("设置外设地址失败,errno=%d (%s)", errno, strerror(errno));        return -1;    }    // 步骤2:发送寄存器地址(写操作,告知外设要读哪个寄存器)    printf("【调试】发送寄存器地址:0x%02x\n", reg_addr);    if (write(fd, &reg_addr, 1) != 1) {        DBG_ERR("发送寄存器地址失败,errno=%d (%s)", errno, strerror(errno));        return -1;    }    // 步骤3:读取寄存器值(读操作)    if (read(fd, val, 1) != 1) {        DBG_ERR("读取寄存器值失败,errno=%d (%s)", errno, strerror(errno));        return -1;    }    printf("【调试】读取成功:寄存器 0x%02x = 0x%02x\n", reg_addr, *val);    return 0;}// I2C 写寄存器函数(带详细调试)inti2c_write_reg(int fd, unsignedchar dev_addr, unsignedchar reg_addr, unsignedchar val){    // 步骤1:设置外设地址    printf("【调试】设置 I2C 外设地址:0x%02x\n", dev_addr);    if (ioctl(fd, I2C_SLAVE, dev_addr) < 0) {        DBG_ERR("设置外设地址失败,errno=%d (%s)", errno, strerror(errno));        return -1;    }    // 步骤2:组装要发送的数据(寄存器地址 + 写入值)    unsigned char buf[2] = {reg_addr, val};    printf("【调试】发送数据:寄存器 0x%02x = 0x%02x\n", reg_addr, val);    if (write(fd, buf, 2) != 2) {        DBG_ERR("写入寄存器失败,errno=%d (%s)", errno, strerror(errno));        return -1;    }    return 0;}intmain(){    int fd;    unsigned char val;    const char *i2c_bus = "/dev/i2c-0";  // 替换为实际总线号    unsigned char dev_addr = 0x48;       // 替换为实际外设地址    // 打开 I2C 设备    fd = open(i2c_bus, O_RDWR);    if (fd < 0) {        DBG_ERR("打开 I2C 总线失败,errno=%d (%s)", errno, strerror(errno));        return -1;    }    // 调试1:写寄存器    if (i2c_write_reg(fd, dev_addr, 0x01, 0x0A) != 0) {        close(fd);        return -1;    }    // 调试2:读寄存器(验证写是否成功)    if (i2c_read_reg(fd, dev_addr, 0x01, &val) != 0) {        close(fd);        return -1;    }    close(fd);    return 0;}

2. 应用层调试关键技巧

  1. 编译与运行:
gcc i2c_debug.c -o i2c_debugsudo ./i2c_debug  # 需要 root 权限访问 /dev/i2c-X
  1. 关键调试点
打印 errno 和 strerror(errno):直接定位错误原因(如 EIO= 外设无应答,ENXIO= 总线不存在);
  • 验证 “写后读”:写入一个已知值,再读回来,确认读写逻辑一致;
  • 检查参数:总线号、外设地址、寄存器地址是否与硬件手册一致。

四、内核驱动层 I2C 读写函数调试(核心)

调试的话可以打开DEBUG;
永久启用:编译内核时开启 CONFIG_I2C_DEBUG_CORE、CONFIG_I2C_DEBUG_ALGO、CONFIG_I2C_DEBUG_BUS 配置(Kernel Configuration → Device Drivers → I2C support → I2C debugging options)。

内核驱动中 I2C 读写依赖 i2c_transfer/i2c_smbus_xfer,调试重点是返回值和内核日志。

1. 驱动层读写函数示例(带调试日志)

#include<linux/i2c.h>#include<linux/module.h>#include<linux/device.h>// 调试宏:打印内核日志(带函数名、行号)#define DRV_ERR(fmt, ...) dev_err(&client->dev, "[%s:%d] " fmt, __func__, __LINE__, ##__VA_ARGS__)#define DRV_INFO(fmt, ...) dev_info(&client->dev, "[%s:%d] " fmt, __func__, __LINE__, ##__VA_ARGS__)// 驱动层读寄存器(核心:i2c_transfer)inti2c_drv_read_reg(struct i2c_client *client, u8 reg, u8 *val){    // 定义 I2C 消息:2 个消息(写地址 + 读数据)    struct i2c_msg msgs[2] = {        // 消息1:发送寄存器地址(写操作,flags=0)        {            .addr = client->addr,            .flags = 0,            .len = 1,            .buf = &reg,        },        // 消息2:读取寄存器值(读操作,flags=I2C_M_RD)        {            .addr = client->addr,            .flags = I2C_M_RD,            .len = 1,            .buf = val,        },    };    // 调试:打印关键参数    DRV_INFO("准备读取:外设地址 0x%02x,寄存器 0x%02x", client->addr, reg);    // 执行 I2C 传输(核心:返回值=成功的消息数)    int ret = i2c_transfer(client->adapter, msgs, ARRAY_SIZE(msgs));    if (ret != ARRAY_SIZE(msgs)) {        DRV_ERR("读取失败,ret=%d,期望=%d,errno=%d", ret, ARRAY_SIZE(msgs), ret);        return ret < 0 ? ret : -EIO;    }    DRV_INFO("读取成功:寄存器 0x%02x = 0x%02x", reg, *val);    return 0;}// 驱动层写寄存器inti2c_drv_write_reg(struct i2c_client *client, u8 reg, u8 val){    u8 buf[2] = {reg, val};    struct i2c_msg msg = {        .addr = client->addr,        .flags = 0,        .len = ARRAY_SIZE(buf),        .buf = buf,    };    DRV_INFO("准备写入:外设地址 0x%02x,寄存器 0x%02x = 0x%02x", client->addr, reg, val);    int ret = i2c_transfer(client->adapter, &msg, 1);    if (ret != 1) {        DRV_ERR("写入失败,ret=%d,errno=%d", ret, ret);        return ret < 0 ? ret : -EIO;    }    return 0;}// 驱动 probe 函数(测试读写)staticinti2c_drv_probe(struct i2c_client *client, conststruct i2c_device_id *id){    u8 val;    // 调试1:写寄存器    if (i2c_drv_write_reg(client, 0x01, 0x05) != 0) {        return -EIO;    }    // 调试2:读寄存器    if (i2c_drv_read_reg(client, 0x01, &val) != 0) {        return -EIO;    }    return 0;}// 驱动注册(省略部分代码,仅保留核心)static const struct i2c_device_id i2c_drv_id[] = {    {"my-i2c-dev", 0},    {},};MODULE_DEVICE_TABLE(i2c, i2c_drv_id);static struct i2c_driver i2c_drv = {    .driver = {        .name = "my-i2c-dev",        .owner = THIS_MODULE,    },    .probe = i2c_drv_probe,    .id_table = i2c_drv_id,};module_i2c_driver(i2c_drv);MODULE_LICENSE("GPL");

2. 驱动层调试关键技巧

  1. 开启 I2C 内核调试日志(临时生效)
# 开启 I2C 核心层调试echo 7 > /sys/module/i2c_core/parameters/debug# 开启 I2C 适配器驱动调试(以 i2c-imx 为例)echo 7 > /sys/module/i2c_imx/parameters/debug
查看内核日志
dmesg -w  # 实时查看内核日志
3.关键调试点
检查 i2c_transfer 返回值:成功返回消息数量(如 2),失败返回负数(-EIO= 外设无应答,-ENXIO= 总线不存在);
  • 验证 i2c_msg 参数:addr(外设地址)、flags(读 / 写)、len(数据长度)是否正确;
  • 对比外设手册:寄存器地址宽度(1 字节 / 2 字节)、读写时序是否匹配。
  • 返回值的具体含义可以参考代码:

  • kernel/include/uapi/asm-generic/errno-base.h

常见问题与排查方案
现象
可能原因
排查方法
Remote I/O error
(EIO)
1. 外设地址错误
2. 上拉电阻缺失
3. 外设未供电 / 虚焊4. 时钟速率过高
1. 用 i2cdetect 重新扫描地址2. 用示波器检查 SCL/SDA 电平3. 降低设备树中 clock-frequency
No such device
(ENXIO)
1. I2C 总线号错误
2. I2C 控制器未使能
1. i2cdetect -l 确认总线存在
2. 检查设备树中 status = "okay"
读写值为 0xFF/0x00(固定值)
1. SDA 线断路
2. 外设未正确应答
1. 用示波器抓 SDA 波形
2. 检查外设复位逻辑
多字节读写错乱
1. 寄存器地址宽度不匹配
2. 消息拆分传输
1. 确认外设寄存器地址是 1/2 字节
2. 用多个 i2c_msg 一次性传输

  1. 总结

    核心原则:

  2. I2C 调试的关键是 “分层定位”—— 先确认总线能识别外设,再验证基础读写,最后排查驱动逻辑,避免跳过硬件直接调试软件。

    1. 先硬后软:
      先用 i2cdetect/i2cget 验证硬件通信,排除物理层问题后再调试驱动;
    2. 日志优先:
      开启 I2C 内核调试日志,通过返回值和日志定位通信失败原因;
    3. 时序匹配:
      严格按照外设手册配置 I2C 时钟速率、寄存器地址宽度、读写时序;
    4. 工具辅助:
      示波器抓波形(硬件)、i2c-tools 验证通信(用户层)、dmesg 查看内核日志(驱动层)是核心调试手段。
    5. 谢谢点赞阅读收藏!

最新文章

随机文章