大家好,我是王鸽,调试编写驱动涉及软件硬件问题,一定需要分层思考,切忌胡子眉毛一把抓,分类讨论,遵循先硬件、后软件、逐层排查的思路,核心是用i2c-tools快速验证总线与设备,结合内核日志定位驱动 / 协议问题,再用示波器 / 逻辑分析仪抓时序,最后通过代码调试解决驱动逻辑问题。- 接线:SDA/SCL 是否接反、是否虚焊 / 接触不良、GND 是否共地;
- 供电:芯片的供电是不是按照规格书范围,电压电平(3.3V/5V)是否匹配;
- 时序:上电的时序是不是正常,reset pwd 引脚等是不是上电按照规格书?同样的下电有没有按照规格书?这种尤其是调试mipi 接口的camera 中很重要的;
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总线有没有挂载外设?
0-0051 1-0032 i2c-0 i2c-1
- 比如:ls /sys/bus/i2c/devices/i2c-0
0-0051 delete_device device i2c-dev name new_device of_node power subsystem
- 4.在最底层的读写上加上log 看寄存器有没有读写数据正确?对着数据手册查看。
interrupt-parent = <&gpio1>; interrupts = <17 IRQ_TYPE_LEVEL_LOW>; interrupt-controller; #interrupt
clock-frequency = <400000>; - 具体看器件要求,单片机一般是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, ®_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. 应用层调试关键技巧
- 编译与运行:
gcc i2c_debug.c -o i2c_debugsudo ./i2c_debug # 需要 root 权限访问 /dev/i2c-X
- 关键调试点
打印 errno 和 strerror(errno):直接定位错误原因(如 EIO= 外设无应答,ENXIO= 总线不存在);- 验证 “写后读”:写入一个已知值,再读回来,确认读写逻辑一致;
- 检查参数:总线号、外设地址、寄存器地址是否与硬件手册一致。
四、内核驱动层 I2C 读写函数调试(核心)
永久启用:编译内核时开启 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 = ®, }, // 消息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. 驱动层调试关键技巧
- 开启 I2C 内核调试日志(临时生效)
# 开启 I2C 核心层调试echo 7 > /sys/module/i2c_core/parameters/debug# 开启 I2C 适配器驱动调试(以 i2c-imx 为例)echo 7 > /sys/module/i2c_imx/parameters/debug
检查 i2c_transfer 返回值:成功返回消息数量(如 2),失败返回负数(-EIO= 外设无应答,-ENXIO= 总线不存在); | | |
|---|
Remote I/O error | | 1. 用 i2cdetect 重新扫描地址2. 用示波器检查 SCL/SDA 电平3. 降低设备树中 clock-frequency |
No such device | | 2. 检查设备树中 status = "okay" |
| | |
| | |
- 总结
核心原则:
I2C 调试的关键是 “分层定位”—— 先确认总线能识别外设,再验证基础读写,最后排查驱动逻辑,避免跳过硬件直接调试软件。
- 先硬后软:先用
i2cdetect/i2cget 验证硬件通信,排除物理层问题后再调试驱动; - 日志优先:开启 I2C 内核调试日志,通过返回值和日志定位通信失败原因;
- 时序匹配:严格按照外设手册配置 I2C 时钟速率、寄存器地址宽度、读写时序;
- 工具辅助:示波器抓波形(硬件)、
i2c-tools 验证通信(用户层)、dmesg 查看内核日志(驱动层)是核心调试手段。