使用read()/write()系统调用读取LTC2992的I2C寄存器返回0,但I2C_RDWR可正常读取的问题咨询
问题描述
我现在正尝试用用户空间程序读取LTC2992电源监控器的寄存器,遇到了一个无法解释的现象:
当我通过write()系统调用、i2cset或者I2C_RDWR向LTC2992的某个寄存器写入值后,用i2cget或I2C_RDWR可以正常读回该值,但使用write()+read()系统调用组合读取时,返回值始终是0。
用write()+read()的代码示例
#include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <fcntl.h> #include <unistd.h> #include <linux/i2c-dev.h> #include <sys/ioctl.h> int main() { // ... 省略device和addr的定义 int file = open(device, O_RDWR); if (file < 0) { perror("Failed to open I2C bus"); return 1; } if (ioctl(file, I2C_SLAVE, addr) < 0) { perror("Failed to set I2C address"); close(file); return 1; } uint8_t reg = 0xXX; // 目标寄存器地址 uint8_t data; if (write(file, ®, 1) != 1) { perror("Failed to write register address"); close(file); return 1; } if (read(file, &data, 1) != 1) { perror("Failed to read from register"); close(file); return 1; } close(file); return 0; }
用I2C_RDWR的代码示例
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #include <linux/i2c-dev.h> #include <linux/i2c.h> #include <string.h> int main() { // ... 省略device和addr的定义 int file = open(device, O_RDWR); if (file < 0) { perror("Failed to open I2C device"); return 1; } struct i2c_rdwr_ioctl_data packets; struct i2c_msg messages[2]; uint8_t reg = 0xXX; // 目标寄存器地址 uint8_t data; messages[0].addr = addr; messages[0].flags = 0; messages[0].len = 1; messages[0].buf = ® messages[1].addr = addr; messages[1].flags = I2C_M_RD; messages[1].len = 1; messages[1].buf = &data; packets.msgs = messages; packets.nmsgs = 2; if (ioctl(file, I2C_RDWR, &packets) < 0) { perror("I2C_RDWR ioctl failed"); close(file); return 1; } close(file); return 0; }
我的理解是,读取寄存器需要重复起始条件,所以猜测第一种方法失败是因为write()系统调用会生成这样的事务:START -> [ADDR+W] -> [REG] -> STOP
而read()系统调用生成的是:START -> [ADDR+R] -> [DATA] -> STOP
也就是两次事务之间有STOP信号,没有重复起始。
但我看到某个教程里用类似的write()+read()方法成功读取了另一个器件的寄存器,而那个器件的 datasheet 也要求重复起始条件。所以我不确定我的直觉是否正确,也不确定这是不是时序问题,或者两种方法有什么关键差异?
专家解答
嗨,这个问题的核心确实和你猜测的**重复起始条件(Repeated Start)**有关,咱们来详细拆解:
1. write()+read()失效的原因
Linux的i2c-dev驱动对独立的write()和read()系统调用的处理逻辑是:
- 每次
write()调用都会触发一个完整的I2C事务,发送完数据后一定会发送STOP信号,结束当前通信 - 后续的
read()是一个全新的独立事务,需要重新发送START信号+设备读地址
但LTC2992要求的寄存器读取流程是无中断的连续事务:写完寄存器地址后直接发送REPEATED START(重复起始)信号,再发起读操作,这样器件才会关联“写寄存器地址”和“读数据”这两个动作,返回对应寄存器的值。
分开的write()和read()中间插入了STOP,器件会认为这是两个无关的请求,自然不会返回你期望的数据,而是返回默认的0值。
2. I2C_RDWR能正常工作的原因
I2C_RDWR是通过ioctl一次性提交一组I2C消息(你这里是“写寄存器地址”和“读数据”两个消息),驱动会把这两个消息合并成一个带重复起始的完整事务:START -> [ADDR+W] -> [REG] -> REPEATED START -> [ADDR+R] -> [DATA] -> STOP
这种方式完全符合LTC2992的通信协议要求,所以能正确读取寄存器值。
3. 为什么有些教程的write()+read()能成功?
这是因为不同I2C器件的设计兼容性不同:
- 部分器件在收到
STOP信号后,会在短时间内保留最后一次指定的寄存器地址,如果你在这个时间窗口内发起读操作,就能读到正确值 - 还有些器件的协议设计允许这种“拆分式”的事务,不需要严格的重复起始
但LTC2992显然不支持这种宽松的操作,或者你的系统中write()和read()之间的系统调用延迟超过了器件的地址保留时间,所以这种方式对你的场景无效。
4. 解决方案
- 优先使用I2C_RDWR方式:这是Linux官方推荐的处理复杂I2C事务的方法,兼容性最好,能适配所有需要重复起始条件的器件,稳定性最高
- 如果一定要尝试write()+read()的方式,可以通过
ioctl设置I2C_RETRIES(重试次数)和I2C_TIMEOUT(超时时间)来缩短操作间隔,但这种方式依赖器件特性和内核版本,可靠性远不如I2C_RDWR
你也可以用逻辑分析仪抓取两种方式的I2C波形,直观看到STOP信号的差异,这会帮你更清晰地理解问题所在。
内容来源于stack exchange




