嵌入式Linux调试利器:i2c-tool常用命令与实战排错
2026/9/13 21:06:59 网站建设 项目流程

前阵子帮同事查一块新板子上的温湿度传感器,I2C地址明明扫描到了,数据却怎么读都是0xff,折腾了一下午,最后靠i2c-tool一步步定位到设备其实处于未唤醒的待机状态。这种场景在嵌入式Linux开发里太常见了:新板子调试、外设驱动移植、硬件排错,几乎每天都离不开i2c-tool这套命令。它是Linux下调试I2C总线最基础也最实用的工具集,能帮你扫描设备、读写寄存器、验证时序、排查总线故障。这篇文章就把我这些年用i2c-tool的常用命令、参数细节、以及踩过的坑一次讲清楚,适合刚接触嵌入式Linux的开发者,也适合经常和硬件打交道的驱动工程师收藏备用。

1. 安装与环境准备:设备节点、权限和内核依赖

1.1 不同发行版下的安装方式

i2c-tool在主流Linux发行版里都有现成包,不需要源码编译。Debian/Ubuntu系用apt,CentOS/RHEL系用yum或dnf,一句话就装好:

# Debian / Ubuntu sudo apt install i2c-tools # CentOS / RHEL 7/8 sudo yum install i2c-tools # CentOS/RHEL 9+ / Fedora sudo dnf install i2c-tools

如果是嵌入式平台,比如用Buildroot或Yocto构建根文件系统,通常菜单里直接勾选i2c-tools就会编进去。BusyBox也自带了一套精简版i2c工具,但功能不完整,比如没有i2ctransfer,交叉编译环境里我一般还是建议直接用完整的i2c-tools源码,configure、make、make install三步走,依赖很少,几分钟就能编完。

1.2 内核I2C子系统与设备节点的关系

i2c-tool跑起来的前提是内核已经注册了I2C控制器并且生成了对应的设备节点。这个节点就是/dev/i2c-N,N是总线编号。要确认系统里有哪些I2C总线,直接用:

ls /dev/i2c-*

如果这个命令返回空或者报No such file or directory,说明内核没有使能I2C的设备文件支持。这时候要检查内核配置里有没有开CONFIG_I2C_CHARDEV,这个选项在Device Drivers -> I2C support -> I2C device interface下面,打开之后重新编译内核,/dev/i2c-N就会出现了。

很多嵌入式平台(比如树莓派、RK系列)默认会通过设备树把I2C控制器使能,但要注意:设备树里某个I2C控制器如果status = "disabled",即使驱动加载了也不会有对应的/dev/i2c-N节点。我遇到过好几次新板子移植,dts里I2C节点忘改状态,浪费了大量时间。

1.3 权限问题:别老用root,配置udev更省心

i2c-tool访问设备节点需要权限。开发调试时直接sudo没问题,但如果产品最终要交付,或者你自己嫌每次sudo麻烦,可以创建一个i2c用户组,把设备节点归属到组里,再把当前用户加进去:

sudo groupadd i2c sudo chown root:i2c /dev/i2c-* sudo usermod -aG i2c $USER

不过/dev/i2c-N是内核动态创建的,重启后权限会重置。正经做法是写一个udev规则,新建/etc/udev/rules.d/99-i2c.rules,内容如下:

KERNEL=="i2c-*", GROUP="i2c", MODE="0660"

保存后执行sudo udevadm control --reload-rules && sudo udevadm trigger,设备节点权限就稳定了。这一步在量产产品上很有用,避免应用程序必须用root跑,也让调试阶段省掉不少重复sudo。

1.4 确认总线号和设备树关系

多总线平台第一步要确认哪个总线对应哪路硬件。用i2cdetect -l列出所有总线:

$ i2cdetect -l i2c-0 i2c i2c-gpio I2C adapter i2c-1 i2c rk3x-i2c I2C adapter i2c-2 i2c rk3x-i2c I2C adapter

这里的adapter名字一般会对应设备树里的节点。以Rockchip平台为例,设备树里i2c1节点的compatible是rockchip,rk3x-i2c,注册后就是i2c-1。总线号和设备树alias直接相关:alias里i2c1 = &i2c1就决定了编号。这个对应关系搞反了,后面所有读写都会扑空。

2. i2cdetect:扫描总线、发现设备,这是排错第一课

2.1 基本用法和输出解读

i2cdetect干的事就是扫描总线上哪些I2C地址有设备响应。最基本的用法:

i2cdetect -y -r 1

这里1是总线号,-y表示跳过交互确认(不加-y它会先问你一句Proceed anyway?),-r表示使用SMBus read byte方式探测。执行后输出是一个从0x03到0x77的地址表:

0 1 2 3 4 5 6 7 8 9 a b c d e f 00: 10: 20: 30: 40: 50: 50 60: 70:

这个表格里每一格对应一个7位I2C地址,0x50出现在第5行第0列,说明0x50这个地址上有一个设备应答了。如果某个格子显示UU,表示该地址被内核驱动占用了,i2cdetect发探测请求时能看到应答,但出于安全考虑不显示具体设备;这时候如果要操作它,后面配合-f参数强制访问。

2.2 探测原理和参数差异

i2cdetect扫描的原理很简单:对0x03-0x77范围内每个地址发一个探测请求,看有没有ACK回来。但请求方式有讲究,这也是为什么有-r和默认两种模式。默认模式用的是SMBus quick write,向设备发送一个写命令。问题在于某些设备对quick write会触发内部状态变化,甚至会往寄存器里写入垃圾数据。所以安全起见,我几乎永远推荐加-r,用read byte代替quick write,对设备的副作用最小。

还有-q参数,也是SMBus quick write,而且要配合-y使用。除非你明确知道总线上是什么设备并且需要测试quick write响应,否则别用。曾经我就因为用-q扫描一个只读传感器,直接把它搞进了异常状态,还得断电重启才恢复。

-a参数扫描整个7位地址空间(0x03到0x77默认会跳过0x50-0x5f之间部分?其实默认范围就是0x03-0x77),默认扫描范围是0x03到0x77,-a可以扫到0x00-0x7f,包括一般不会用到的保留地址。很少需要用到,但排查特殊设备时可以试试。

2.3 扫描不到设备的排查顺序

扫描不到设备是日常高频问题,别一上来就怀疑i2cdetect有问题,按照下面顺序排查:

  • 总线号:换个总线号试试,先用i2cdetect -l确认平台上有几条总线,然后逐一扫描。很多新手以为设备挂在i2c-1上,实际dts里配的是i2c-0。
  • 设备地址:手册上写的0xA0是8位地址(包含读写位),而i2cdetect表格里显示的是7位地址,需要右移一位。比如手册写0xA0,7位地址就是0x50。这个换算错误太常见了。
  • 供电和上拉:I2C设备的VDD没供上、SCL/SDA没有上拉电阻或上拉电阻没焊,都会导致扫描不到。用示波器或者万用表量一下SCL/SDA是否都处于高电平,如果有一根被拉低,那就是硬件问题。
  • 电平转换器:如果传感器是1.8V电平而主控是3.3V,中间一定有个电平转换芯片(比如PCA9306),转换芯片的使能脚(EN)没拉起来,SCL/SDA就是断开的。
  • 地址线和冲突:很多I2C芯片的7位地址里有几位由硬件引脚决定(比如A0/A1/A2)。如果引脚没接对,设备实际响应的是另一个地址。同一总线上两个设备地址相同也会造成冲突,表现就是扫描结果不稳定或者读写互相干扰。

3. i2cdump和i2cget:把设备寄存器扒个底朝天

3.1 i2cdump的访问模式:b、w、s、i各有什么讲究

i2cdump是用来连续读取设备寄存器内容的命令,维测设备状态时比一个个读效率高得多。最基本的用法:

i2cdump -y -f 1 0x50

默认模式是b(byte模式),逐个字节读,从0x00到0xff,输出左边是地址,中间是十六进制数据,右边是ASCII字符显示。这有个好处:任何I2C设备都能用b模式读,通用性最强。但速度慢,一遍dump要发256次读事务。

实际使用中更常用的是其他模式:

模式命令适用范围
bi2cdump -y 1 0x50通用,逐字节读
wi2cdump -y 1 0x50 -m w16位寄存器宽度的设备
si2cdump -y 1 0x50 -m sSMBus设备(支持block read)
ii2cdump -y 1 0x50 -m i支持I2C block read的设备(如EEPROM)

比如一个16位ADC芯片,寄存器值是16位的,w模式一次读两个字节,效率和可读性都比b模式好。但w模式对只支持字节寻址的设备不适用,读出来的数据是乱的。s和i模式依赖设备支持对应的block read协议,不支持的话读取会返回错误。

注意:i2cdump会从0x00一直读到0xff,如果设备只实现了0x00-0x0f的寄存器,后面地址返回的数据一般是0xff,不要误以为是真实数据。所以i2cdump输出里出现大段0xff时,先看设备手册确认寄存器地址空间范围。

3.2 指定范围读取和页读取

i2cdump有一个很容易被忽略但很实用的参数:-r start-end。比如一家传感器寄存器就0x00到0x2f,没必要读256个字节,你可以指定范围:

i2cdump -y -f 1 0x48 -r 0x00-0x2f

这样只读这一小段,速度快一倍,输出也清爽。这个参数在调试EEPROM时更有价值,因为AT24C系列EEPROM的页大小通常只有8或16字节,跨越页边界连续读会出问题。用-i模式配合-r 0x00-0x07,正好读一页数据,不会触发跨越边界的地址回卷。

3.3 i2cget:单独读一个寄存器的最快方式

如果只想看某一个寄存器,i2cget比i2cdump更直接:

i2cget -y 1 0x48 0x00

这个命令表示从总线1上0x48设备的0x00寄存器读一个字节,输出形如0x2c。不带寄存器参数时,i2cget会执行一次读地址操作(read without register address),有些设备把这个当作查询设备是否就绪的指令。加-w参数可以用16位字模式读取:

i2cget -y 1 0x48 0x00 -w

这里有个细节:i2cget的-w是读一个16位字,字节序默认是大端(高字节在前)。如果你的设备是小端模式,读出来的高低字节需要自己交换。i2cget输出不会帮你处理字节序,这是很多人读数据对不上号的原因之一。

4. i2cset与i2ctransfer:写入操作才是真正的干活

4.1 i2cset基本写法和多字节连续写

读很容易,写才是真正验证驱动和硬件逻辑的关键。i2cset的基本格式:

i2cset -y 1 0x48 0x01 0x2c

意思是对总线1上的0x48设备,往寄存器0x01写入数据0x2c。这个命令会发起一次I2C写事务:先发送设备地址,再发送寄存器地址,最后发送数据。

如果要写多个字节(比如向EEPROM连续写入一串数据),可以在数据位置依次排列:

i2cset -y 1 0x50 0x00 0x01 0x02 0x03 0x04

但这里必须提醒:i2cset的多字节写入本质是发起一次连续写事务,设备的页大小限制照样生效。AT24C02页大小是8字节,你从0x00开始写9个字节,第9个字节就会回卷写到0x00,把前面的数据覆盖掉。跨页写数据必须分成多次i2cset,每次从页边界开始。

4.2 masked write:只修改指定位的原子操作

这也是i2cset一个非常实用的功能:-m参数实现读-改-写(read-modify-write):

i2cset -y -m 0x0f 1 0x48 0x01 0x05

这条命令的意思是:先读0x48设备0x01寄存器的当前值,把低4位(0x0f掩码)改成0x05,其余位保持不变,再写回去。这在配置设备时太常用了。比如某个控制寄存器的bit3-0是增益设置,bit7是使能位,你只想改增益不想动使能位,用-m掩码就能安全地做局部修改,避免先读后写时读到旧值再覆盖导致的临界问题。

-m参数在驱动调试中的价值更高:调试时经常要反复切换一个芯片的模式,如果每次都写整个寄存器,很容易把其他位的配置冲掉,然后设备行为变得诡异。用掩码操作,问题定位会清晰很多。

4.3 i2ctransfer:手动构造任意I2C时序

i2ctransfer是i2c-tools家族里最晚加入却最强大的命令。它能让你完全控制一次I2C事务的每一个细节,包括写多少字节、读多少字节、先写后读还是先读后写、要不要stop信号。基本格式:

i2ctransfer -y 1 w1@0x48 0x01 r2

这条命令在总线1上对0x48设备:先写1个字节0x01,然后紧接着读2个字节。这一个组合就实现了"读寄存器0x01的值(2字节)"的完整事务,中间没有stop信号,I2C总线上表现为一次repeated start。很多传感器(比如一些加速度计)读取多字节数据时,需要先发寄存器地址再连读,i2ctransfer就是为这种场景设计的。

如果要操作寄存器地址是16位的设备:

i2ctransfer -y 1 w3@0x50 0x00 0x10 0xff

这里w3表示写3个字节,后续跟0x00 0x10是16位寄存器地址,0xff是要写入的数据。所有字节参数都是十六进制,不需要加0x前缀(加了也可以)。

4.4 为什么i2ctransfer比i2cset/i2cget更值得掌握

我用i2ctransfer之后,调试效率提升了一个档次,原因有三点:

第一,它可以精确模拟驱动代码里的读写逻辑。驱动里经常有regmap_read(dev, reg, &val)这类调用,你完全可以把驱动里的寄存器读写搬到命令行里验证:先读当前值,再写一个测试值,再读回来确认,整个过程和驱动实际行为一模一样。

第二,一条命令完成复合事务。i2cget+i2cset是两条独立命令,中间有间隔,如果要快速连续操作,时间窗口对某些时序敏感的设备不够。i2ctransfer一条命令在同一个事务内完成组合读写,不存在间隙。

第三,调试异常设备时可以构造特殊的时序组合。比如怀疑设备在某个写序列后进入异常状态,可以用i2ctransfer精确发送那段序列复现问题。这在分析硬件bug时几乎是必须的。

5. 综合实战:用i2c-tool完整调试一颗AT24C02 EEPROM

5.1 拿到陌生I2C芯片的调试顺序

所有陌生I2C设备的调试都应该遵循一个固定顺序,能帮你快速定位问题:先扫描确认地址,再读ID或厂商寄存器确认芯片身份,然后读当前配置,最后做最小的写操作验证。比如一颗AT24C02 EEPROM(I2C地址由A2/A1/A0引脚决定,全接地时是0x50),第一步先扫描:

i2cdetect -y -r 1

如果输出里0x50位置显示50,说明芯片在总线上有响应,硬件连接基本没问题。

5.2 用i2cdump查看EEPROM内容和状态

接下来用i2cdump看芯片当前存储了什么:

i2cdump -y 1 0x50 -i -r 0x00-0x07

-i用I2C block read模式,-r指定只读前8个字节。新出厂EEPROM一般全0xff,如果看到其他数据,说明里面写过东西。这一步先确认器件通电后能否正常读取数据,排除供电和地址线问题。

5.3 写入并验证:用i2c set写一页

然后做写测试。AT24C02页大小8字节,从页边界0x00开始写满一页:

i2cset -y 1 0x50 0x00 0x11 0x22 0x33 0x44 0x55 0x66 0x77 0x88

注意EEPROM写操作需要时间(典型5ms),写入后不能立刻读,否则读回来的还是旧值或者总线NACK。要等一小段时间再验证:

sleep 0.01 i2cdump -y 1 0x50 -i -r 0x00-0x07

应该看到0x11 0x22 0x33 0x44 0x55 0x66 0x77 0x88整齐排列。如果读出来是0xff,检查WP(写保护)引脚有没有接地。AT24C02的WP引脚接高电平的时候,整个芯片只读不写,i2cset执行时不会报错,但数据写不进去。这是最典型的"写无效"问题。

5.4 用i2ctransfer模拟驱动读写操作

最后演示一下i2ctransfer怎么模拟驱动里的操作。驱动里经常这样写EEPROM:先写寄存器地址,再写数据。比如往地址0x10写一个字节0xAB:

i2ctransfer -y 1 w2@0x50 0x10 0xab

w2表示写2个字节,0x10是EEPROM内部地址,0xab是要写的数据。等写完再读回来:

i2ctransfer -y 1 w1@0x50 0x10 r1

这条命令先写1字节(内部地址0x10),然后读1字节,一个事务搞定,等价于驱动的read operation。如果返回值是0xAB,说明这次写入验证成功。

5.5 实战中容易忽略的时序细节

EEPROM调试中最容易忽略的是写周期。AT24C系列的写周期(write cycle time)通常是5ms,这个时间内芯片内部在擦写,外部对它发任何命令都会NACK。所以我刚才的例子里sleep了10ms再读。如果你发现写完立刻读返回errno 6(ENXIO)或者数据不对,先怀疑是不是撞上了写周期,加个延时再试。

另外,EEPROM I2C地址的引脚改变后要重新上电才能生效。有些板子用了GPIO来控制A2/A1/A0,改了引脚状态后没断电,扫描到的还是旧地址,这个坑困扰过不少新手。检查硬件连接时,最好用示波器确认SCL/SDA上确实有信号活动,而不只凭i2cdetect的扫描结果。

6. 高频故障排查:我在i2c-tool使用中踩过的坑

6.1 UU显示的地址用不了怎么办

有时候i2cdetect会显示UU而不是地址,意思是这个地址被内核里的某个驱动占用了。内核驱动和用户态工具争用I2C设备时,系统是禁止用户态再去访问的。要用i2c-tool访问,可以加-f参数强制操作:

i2cdetect -y -f 1 i2cdump -y -f 1 0x50 i2cget -y -f 1 0x50 0x00

-f的意思是把"由内核驱动管理"的设备强制纳入用户态访问范围。但强烈建议只在调试时用:你正在操作的芯片如果同时有内核驱动在跑,两边同时对芯片发命令,轻则数据错乱,重则把寄存器状态搞坏。更稳妥的办法是先把对应设备从设备树里disable掉,让内核不加载驱动,再随便用i2c-tool玩。

6.2 总线上有设备但i2cset写不进去

这个坑我帮人排查过很多次,症状是i2cdetect能扫到地址,i2cget也能读出数据,但是i2cset执行后没有任何效果。排查链路从这几个方向展开:

  • 确认设备是不是只读设备。像AT24C02的WP脚拉高、某些传感器的高位地址是只读寄存器、PMIC的写寄存器需要特殊口令解锁,这些情况i2cset都会"成功"但数据不变。
  • 确认写地址的字节序和寄存器地址宽度。设备手册里的寄存器地址是16位,你按8位写,写入的就是错误寄存器。这时候i2cset不会报错,设备会把你的数据当成寄存器地址的一部分解析,表现出"写不进去"。
  • 确认I2C总线上有没有MUX(如TCA9548A)。如果有MUX,需要先通过i2cset选中对应通道,否则后面的读写都是总线上另一个通道的响应。很多板子挂在同一个I2C控制器下的多路从设备,就是靠MUX区分的。

6.3 SDA/SCL被拉低,总线锁死

总线死锁是I2C调试里最令人头疼的问题。现象是i2cdetect报错,甚至执行任何i2c命令都返回I/O error,用万用表量SCL或SDA发现一直是低电平。造成这种情况的常见原因有两个:一是从设备在不满意的状态下等主机的stop信号没等到;二是主控GPIO配置错误导致引脚被拉死。

软件层面的排查顺序是先确认是不是从设备挂死。个别芯片在I2C通信中途断线后会进入异常状态,一直把SDA拉低不放。简单复位方法是给整个板子断电重新上电,大多数情况下能恢复。如果不想整板断电,可以把SCL手动翻转几个周期,让从设备退出异常状态,做法是用gpioset命令(或写GPIO sysfs)直接操作SCL引脚模拟时钟:

# 假设SCL是GPIO49,先配置为输出 gpioset gpiochip0 49=1 gpioset gpiochip0 49=0 # 重复若干次后SDA应该释放

这招救过我两次,不过要注意:手动翻时钟之前必须确保SDA也被释放为开漏高阻,否则会拉大电流。最稳妥的恢复方式依然是断电重启,如果重启后仍然死锁,基本可以断定是硬件问题,比如上拉电阻没焊、引脚短路、电平不匹配。

6.4 地址换算错误:7位和8位地址到底怎么算

I2C地址是7位的,但在设备手册和数据手册里经常看到的是8位表示法(包含读写位)。比如一个芯片手册写从机地址0xA0,实际上7位地址是0x50。因为0xA0的低位是0,右移一位就是0x50。i2cdetect显示的是7位地址0x50,i2cget、i2cset、i2ctransfer参数里的地址也是7位。这个换算关系我见过无数人搞错,尤其在拿着手册对着i2cdetect输出核对地址的时候。

记住一个原则:i2c-tools所有命令的地址参数都是7位地址,不需要左移。如果你想用8位地址0xA0,直接写0x50就行。这个错误导致扫描不到设备或者读写失败的概率,比硬件故障还高。

6.5 读寄存器返回0xff该怎么分析

0xff这个值在I2C调试里出现频率极高,但含义不一定相同。首先要判断是设备真的返回了0xff,还是总线NACK之后工具填充的假数据。一个快速区分方法:对同一个地址连续读几次,如果每次都返回0xff且无报错,一般是设备真的在响应;如果时而0xff时而又能读出正常数据,大概率是接触不良或者时钟太快导致数据线采样错误。

另一种常见情况是寄存器本身是保留位或只写位。比如某些传感器芯片,读取未实现的高位地址会返回0xff,这是正常现象,不要怀疑硬件。还有一种情况是设备处于低功耗模式(比如睡眠模式)时,I2C接口只响应地址,不响应寄存器访问,读任何寄存器都返回0xff。遇到这种情况,查手册里唤醒设备的操作序列,往往需要往特定寄存器写一个唤醒命令才能恢复正常访问。

6.6 总线速度不匹配和电平问题

i2c-tools的读写速度由I2C控制器的驱动决定,标准模式100kHz,快速模式400kHz,个别平台还支持1MHz。如果总线上挂着一个旧设备只支持100kHz,而控制器配置成400kHz,表现就是扫描偶尔成功、读写随机失败、数据出现错位。排查方法是检查控制器驱动配置,或者用示波器实测SCL频率是否符合期望。

电平不匹配问题更隐蔽。现在很多板子混合了3.3V和1.8V器件,I2C总线如果经过电平转换,两边的高电平阈值不同。有时候设备虽然供电正常,但SCL/SDA的电压达不到器件VIH阈值,就会出现"扫描到但读写失败"的诡异现象。这时候别纠结软件,直接用示波器量SCL和SDA的高电平幅值,低于设备手册VIH阈值就要检查上拉电阻的电平连接是否正确。

7. 几个能直接抄的小技巧

最后分享几个我在实际调试中摸索出来的用法,都属于手册上不会重点讲但实战价值很高的技巧。

第一个是批量读寄存器并格式化输出。调试传感器时经常要看一组寄存器的变化趋势,写个for循环就能实现:

for r in 0x00 0x01 0x02 0x03; do echo -n "reg $r: "; i2cget -y 1 0x48 $r; done

这个脚本输出比i2cdump清爽,尤其寄存器地址不连续时更实用。

第二个是用i2ctransfer加-shell通配符做多地址遍历。排查设备挂哪个地址时,可以一条条试:

for addr in 0x40 0x41 0x42 0x48 0x50; do echo "== $addr =="; i2ctransfer -y 1 w1@$addr 0x00 r1; done

这个命令会逐个地址尝试读寄存器0x00,能快速确认设备实际挂在哪个地址、返回数据是什么。比手动改数字高效太多。

第三个是i2cdetect扫描配合-w参数经常漏设备的问题。某些芯片在扫描时因为内部上电时序还没完成,首次扫描不到,过几秒再扫一次就出现了。如果新上板扫描为空,别急着怀疑硬件,先sleep 5:```

sleep 5 && i2cdetect -y -r 1

这个小技巧帮我避免过好几次误判。 第四个是把i2c-tools和逻辑分析仪/示波器配合使用。手动执行i2cget的时候用示波器抓SCL/SDA波形,能直观看到设备响应情况。有一次我自己调一个时序有问题的芯片,就是用i2cget配合示波器抓到了设备在第9个时钟后没有回复ACK,才知道是芯片处于忙状态,才去找它忙的原因。工具再强大,最终还是要和硬件测量手段结合,这样排查问题才不会被软件现象误导。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询