Linux下i2ctransfer命令实战:从EEPROM读写数据到避坑指南
调试一块新到的嵌入式板卡,发现系统启动后某个传感器数据死活读不出来。

用万用表量了电源和地,电压正常;示波器抓了一下I2C总线的波形,SCL和SDA都有信号,但就是没数据返回。
折腾了半天,最后用i2cdetect扫了一下总线,发现设备地址明明在那里,但用传统的i2cget就是读不到数据。
直到我翻出i2ctransfer这个工具,才发现这个传感器的寄存器地址是16位的,而i2cget和i2cset只能处理8位地址。
那一刻的感觉,就像在工具箱里翻出了一把专门对付特殊螺丝的扳手——问题迎刃而解。
i2ctransfer不是i2c-tools里最出名的命令,但绝对是功能最强大的。
它不像i2cdetect那样只是简单地探测设备,也不像i2cget和i2cset那样只能进行简单的单次读写。
它能让你像搭积木一样,把多个读写操作组合成一个完整的I2C传输序列,中间没有STOP信号打断,这在调试某些“脾气古怪”的I2C设备时至关重要。
今天我就结合自己踩过的坑,详细聊聊怎么用i2ctransfer玩转EEPROM读写,以及那些官方文档里不会告诉你的实战技巧。
1.
环境准备与工具安装
在开始之前,你得确保手头有可以操作的硬件环境。
我建议准备一块常见的嵌入式开发板,比如树莓派、RockPi或者任何带有I2C接口的ARM板卡。
这些板卡通常都有现成的I2C总线暴露出来,方便我们连接外设。
如果你手头有I2C接口的EEPROM芯片(比如AT24C系列),那就更好了,可以直接拿来练手。
1.1
安装i2c-tools
大多数Linux发行版都自带了i2c-tools,但版本可能比较老。
i2ctransfer命令是在i2c-tools4.0版本之后才加入的,所以先确认一下你的版本:
i2ctransfer-V
如果提示命令不存在,或者版本低于4.0,那就需要手动安装或升级。
在Debian/Ubuntu系统上:
sudoapt
i2c-tools
对于其他发行版,比如Fedora或CentOS:
sudodnf
7
如果你想从源码编译最新版本,可以去内核镜像站下载:
wgethttps://mirrors.edge.kernel.org/pub/software/utils/i2c-tools/i2c-tools-4.4.tar.gz
tar
install
注意:从源码安装时,记得把
/usr/local/sbin添加到你的PATH环境变量里,否则可能找不到新安装的命令。
1.2
启用I2C内核模块
光有工具还不够,内核得支持I2C总线。
检查一下当前系统有没有加载I2C相关的内核模块:
lsmodgrep
i2c
你应该能看到类似i2c_dev、i2c_bcm2835(树莓派)这样的模块。
如果没有,可能需要手动加载:
sudomodprobe
i2c-dev
有些嵌入式板卡还需要加载特定的I2C控制器驱动,这个得看具体硬件。
一般来说,主流开发板的官方系统镜像都已经配置好了。
1.3
检查I2C总线
安装好工具并加载模块后,用i2cdetect看看系统里有哪些I2C总线:
sudoi2cdetect
-l
输出大概长这样:
i2c-0i2c
adapter
这里i2c-0和i2c-1就是两条可用的I2C总线。
记下你想用的那条总线的编号,后面会用到。
2.
i2ctransfer命令详解
很多人第一次看到i2ctransfer的命令行参数会觉得头大,其实它的设计逻辑很清晰。
我们先从最基础的语法开始,慢慢拆解。
2.1
基本语法结构
i2ctransfer的核心思想是“一个传输包含多个消息”。
这听起来有点抽象,我举个例子你就明白了。
假设你要从EEPROM的某个地址开始读数据,传统的做法是先发一个写操作设置地址指针,再发一个读操作读取数据。
这两个操作之间总线会有一个STOP信号,然后重新START。
而i2ctransfer可以把这两个操作打包成一个传输,中间用Repeated
Start连接,没有STOP打断。
看看最基本的命令格式:
i2ctransfer[选项]
[数据]]...
- I2CBUS:I2C总线编号,就是刚才
i2cdetect列出来的那个数字。-l
- 描述块:描述一个消息是读还是写,长度多少,地址是什么。
格式是
{r|w}长度@地址。 - 数据:只有当描述块是写消息时才需要,就是要写入的字节数据。
2.2
描述块的奥秘
描述块是i2ctransfer的灵魂,它的语法{r|w}长度@地址有几个关键点:
读写标志:r表示读,w表示写。
这个很好理解。
长度:可以是0到65535之间的数字,表示要读写多少个字节。
这里有个隐藏功能——如果设备支持SMBus
Block传输,长度可以写?,让设备自己决定返回多少数据。
不过这个功能用得不多,大多数情况下我们还是明确指定长度。
地址:7位的I2C设备地址。
注意这里是7位地址,不是8位!很多新手会在这里栽跟头。
比如一个EEPROM的地址是0x50,那这里就写0x50,不要写成0xA0(那是8位地址,包含了读写位)。
地址可以省略,如果省略就沿用上一个描述块的地址。
这个特性在连续操作同一个设备时特别方便。
2.3
数据后缀技巧
写数据的时候,i2ctransfer提供了一些很酷的后缀,可以快速生成数据序列:
| 后缀 | 含义 | 示例 | 生成的数据 |
|---|---|---|---|
= | 保持值不变 | 0xaa= | 0xaa,0xaa,0xaa... |
+ | 每次加1 | 0x00+ | 0x00,0x01,0x02... |
- | 每次减1 | 0xff- | 0xff,0xfe,0xfd... |
p | 伪随机序列 | 0x00p | 0x00,0x50,0xb0... |
这些后缀在测试的时候特别有用。
比如你要往EEPROM里写一个递减的测试序列,不用手动计算每个字节,直接0xff-就搞定了。
2.4
常用选项解析
i2ctransfer有几个选项需要特别注意:
-y:禁用交互模式。默认情况下,
i2ctransfer会问你“Areyou
sure?”。
在脚本里用的时候加上这个选项,让它直接执行。
-f:强制访问。如果设备已经被内核驱动占用,默认是不让访问的。
加上
-f可以强行操作,但非常危险,可能把系统搞崩。除非你知道自己在干什么,否则别用。
-v:详细输出。会把所有消息的信息都打印出来,包括写消息。
调试的时候很有用。
-a:允许使用保留地址。I2C协议里0x00-0x07和0x78-0x7f是保留地址,正常情况下不让用。
这个选项可以绕过限制,但同样不建议随便用。
3.
EEPROM读写实战
理论讲得差不多了,现在来点实际的。
我手头有个AT24C64
EEPROM,挂在树莓派的I2C-1总线上,地址是0x50。
咱们就用它来演示各种操作。
3.1
基础读取操作
先从最简单的开始——读取EEPROM从地址0x00开始的16个字节。
EEPROM的读取通常需要两步:先写一个地址指针,告诉它从哪里开始读;然后再发起读操作。
sudoi2ctransfer
r16
分解一下这个命令:
-y:在I2C-1总线上执行,不交互确认1
w1@0x50:向地址0x50的设备写1个字节0x00:要写的那个字节,也就是EEPROM的起始地址r16:从同一个设备读16个字节
执行后,你会看到类似这样的输出:
0x120x34
0x88
这就是EEPROM里存储的数据。
如果全是0xff,说明这块区域还没写过数据。
3.2
写入数据
写入操作稍微复杂一点,因为要指定写入的数据。
假设我们要在地址0x100处写入“Hello
EEPROM!”这个字符串(加上结束符共15字节):
sudoi2ctransfer
0x00
注意这里地址0x100用了两个字节表示(0x01和0x00),因为AT24C64的地址空间是16位的。
不同的EEPROM地址宽度可能不同,AT24C02是8位地址,AT24C64是16位,AT24C512可能更宽。
如果你要写入的数据有规律,可以用后缀简化。
比如写入一个从0开始递增的256字节序列:
sudoi2ctransfer
0x00+
这个命令会:
- 先写两个字节的地址0x0000
- 然后写256个字节,从0x00开始每次加1
3.3
跨页写入注意事项
EEPROM有个很重要的特性——页写入。
大多数EEPROM一次只能写一页,页大小通常是16、32或64字节。
如果你要写入的数据跨页了,必须分多次写。
假设页大小是32字节,你要从地址0x1E开始写20个字节。
虽然20<32,但0x1E+20=0x32,跨页了。
正确的做法是分两次写:
#第一页写2个字节(0x1E-0x1F)
sudo
第二页写18个字节(0x20-0x31)
sudo
0xff+
如果你试图一次性写入跨页的数据,EEPROM可能会只写入第一页的部分,或者产生不可预知的行为。
这个问题我踩过坑,当时调试了半天才发现数据没写全。
3.4
读取校验
写入之后当然要读出来校验。
但这里有个坑——EEPROM的写入不是立即生效的,它内部有个写周期,通常是5-10ms。
在这期间读取,可能读到旧数据,也可能读不到数据。
安全的做法是写入后稍微延迟一下再读取:
#写入数据
r9
如果读取的数据和写入的不一致,可能是写周期还没结束,或者发生了页边界错误。
4.
高级技巧与避坑指南
掌握了基础操作后,来看看一些高级用法和常见问题。
这些都是实战中总结出来的经验,文档里可找不到。
4.1
处理16位寄存器地址
很多I2C设备的寄存器地址是16位的,比如一些传感器、音频编解码器等。
i2cget和i2cset处理不了这种情况,但i2ctransfer可以。
假设有个温度传感器在地址0x48,要读取寄存器0x1A3B的值:
sudoi2ctransfer
r2
这里w3表示写3个字节:两个字节的寄存器地址(0x1A和0x3B),然后r2读两个字节的数据。
整个操作在一个传输内完成,中间没有STOP信号。
4.2
复合操作
i2ctransfer最强大的地方在于可以组合多个读写操作。
比如你要先写配置寄存器,然后读状态寄存器,再写数据寄存器:
sudoi2ctransfer
0x55
这个命令做了三件事:
- 向寄存器0x01写入0x80
- 从同一设备读取1个字节(可能是状态寄存器)
- 向寄存器0x02写入0x55
所有操作在一个传输内完成,设备不会在中间被其他主设备打断。
这对于时序要求严格的设备特别重要。
4.3
调试技巧
当你遇到奇怪的问题时,可以试试这些调试方法:
启用详细输出:加上-v选项,看看实际发送和接收的数据。
sudoi2ctransfer
r4
输出会显示每个消息的详细信息,有助于判断问题出在哪里。
用示波器或逻辑分析仪:如果软件层面查不出问题,硬件工具就该上场了。
抓一下I2C总线的波形,看看:
- START和STOP信号是否正常
- 地址是否正确(7位地址+读写位)
- ACK/NACK信号
- 数据时序是否符合规格
检查上拉电阻:I2C总线需要上拉电阻,通常是4.7kΩ。
如果电阻太大,上升沿太慢;太小,功耗又太高。
我曾经遇到过因为上拉电阻虚焊导致通信不稳定的问题。
4.4
常见错误与解决方案
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
Error: | 设备已被内核驱动占用 | 1./>2.如果确定要直接访问,加上 |
Error: | 硬件连接问题或设备无响应 | 1.检查电源和地线用 |
| 读取的数据全为0xFF | 设备未响应或写保护使能 | 1./>2.尝试写入再读取,看是否变化 |
| 写入后读取的数据不对 | 页边界问题或写周期未结束 | 1.确保写入不跨页检查地址宽度是否正确 |
| 只能操作部分地址 | 地址宽度配置错误 | 1./>2.调整地址字节数(8位设备1字节,16位设备2字节) |
4.5
性能优化
如果你需要大量读写数据,可以考虑这些优化:
批量操作:尽量一次性读写多个字节,而不是多次单字节操作。
I2C每次传输都有开销(地址、ACK等),批量操作能显著提高效率。
合理使用后缀:生成测试数据时,用后缀代替手动输入,既快又不容易出错。
脚本化:把常用的操作写成脚本。
比如下面这个备份EEPROM的脚本:
#!/bin/bashbackup_eeprom.sh
OUTPUT="eeprom_backup.bin"
echo
$OUTPUT"
这个脚本每次读取32字节(一页),适合大多数EEPROM。
你可以根据实际情况调整页大小。
5.
安全注意事项与最佳实践
i2ctransfer是个强大的工具,但也是个危险的工具。
用不好可能损坏硬件,甚至让系统无法启动。
下面这些安全准则,每条都是血的教训换来的。
5.1
绝对不要碰的内存地址
官方文档里明确警告:不要向地址在0x50-0x57之间的EEPROM写数据。
这些地址通常用于内存条上的SPD
EEPROM,乱写可能导致内存无法识别,系统无法启动。
我有个同事不信邪,非要在服务器上试,结果第二天就得重新买内存条。
记住,生产环境永远不要用i2ctransfer操作未知设备。
5.2
备份优先
在修改任何设备之前,先备份当前数据。
特别是配置寄存器,改错了可能让设备“变砖”。
#备份整个EEPROM
if=/sys/bus/i2c/devices/1-0050/eeprom
bs=1
count=8192
5.3
使用限制地址
默认情况下,i2ctransfer不允许访问保留地址(0x00-0x07和0x78-0x7f)。
这是为了保护系统,因为有些地址可能对应着关键的系统组件。
除非你100%确定自己在做什么,否则不要用-a选项绕过这个限制。
5.4
避免强制访问
-f选项能强制访问被内核占用的设备,但这就像在手术时绕过麻醉直接开刀——风险极高。
可能导致的后果包括:
- 内核驱动状态混乱
- 写入错误的寄存器
- 系统崩溃或数据丢失
如果设备已经被驱动占用,最好通过标准的sysfs接口或者驱动提供的API来操作,而不是强行用i2ctransfer。
5.5
测试环境先行
任何新的I2C操作,先在测试板或开发板上验证,确认没问题了再上生产环境。
测试时可以用逻辑分析仪监控总线,确保信号符合预期。
6.
实际项目案例
最后分享两个我实际项目中用i2ctransfer解决问题的案例,希望能给你一些启发。
6.1
案例一:修复损坏的显示器EDID
有次客户反映一台工业显示器接上我们的板子后分辨率不对。
排查发现是显示器的EDID(Extended
Display
Data)EEPROM数据损坏了。
EDID存储在地址0x50的EEPROM里,我们需要重新刷写正确的数据。
首先读取当前的EDID数据:
#sudo
edid_bad.bin
用edid-decode工具分析,确认数据确实损坏。
然后从厂家拿到正确的EDID文件,准备写入。
但这里有个问题:EDID
EEPROM通常有写保护。
需要先通过另一个地址(通常是0x30或0x37)发送解锁序列。
经过一番研究,找到了这个特定显示器的解锁方法:
#解锁EEPROM
写入EDID数据(分页写入,每页16字节)
for
byte="0x${data:$j:2}"
cmd="$cmd
0x01
写入后重启显示器,分辨率恢复正常。
这个案例的关键在于找到了正确的解锁方法,并且处理了EEPROM的写周期延迟。
6.2
案例二:调试I2C从设备
另一个项目需要调试一个自定义的I2C从设备。
这个设备有些特殊要求:
- 寄存器地址是16位的
- 某些操作需要先写命令寄存器,再读状态寄存器,中间不能有STOP
- 设备响应较慢,需要额外的延迟
用i2ctransfer可以完美满足这些需求。
下面是我们调试时用的几个典型命令:
读取设备ID(寄存器0xF000):
sudoi2ctransfer
r4
配置设备(需要连续写多个寄存器):
#sudo
0x67
执行命令并等待完成:
#发送启动命令
done
调试过程中发现设备对时序很敏感,如果STOP信号出现的位置不对,设备就会复位。
i2ctransfer的复合消息功能正好解决了这个问题,确保多个操作在一个传输内完成。
这两个案例让我深刻体会到,i2ctransfer不是万能的,但没有它是万万不能的。
当其他I2C工具束手无策时,它往往能提供最灵活的解决方案。
不过,强大的功能也意味着更大的责任,使用时务必小心谨慎,特别是在生产环境中。


