如何提升Linux下I2C读取速度?LSM6DS3传感器读取耗时优化求助
我之前在Qualcomm平台做传感器数据采集时,也碰到过类似的I2C读取延迟问题,尤其是处理FIFO这类批量数据的时候。结合实际调试经验,给你几个针对性的优化方向:
1. 放弃单字读取,改用批量块读取
你当前用的i2c_smbus_read_word_data()每次只能读取2字节,而且每一次调用都会触发完整的I2C事务(起始位、地址确认、数据传输、停止位),几百次循环下来,光是事务开销就占了大部分时间。
换成i2c_smbus_read_i2c_block_data()或者直接用I2C_RDWR的IOCTL调用,一次性读取整个FIFO的所有数据,能把事务次数从几百次降到2次(一次读FIFO长度,一次读全部数据)。
举个实际代码片段的例子:
#include <linux/i2c.h> #include <linux/i2c-dev.h> #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <fcntl.h> // 假设传感器I2C地址是0x6A #define LSM6DS3_ADDR 0x6A #define LSM6DS3_FIFO_LEN_REG 0x3F #define LSM6DS3_FIFO_DATA_REG 0x3E int read_lsm6ds3_fifo(int i2c_fd) { struct i2c_rdwr_ioctl_data msg_set; struct i2c_msg msgs[2]; uint16_t fifo_len; uint8_t len_buf[2]; // 第一步:读取FIFO中已存储的字节数 msgs[0].addr = LSM6DS3_ADDR; msgs[0].flags = 0; // 写操作,指定寄存器地址 msgs[0].len = 1; msgs[0].buf = (uint8_t[]){LSM6DS3_FIFO_LEN_REG}; msgs[1].addr = LSM6DS3_ADDR; msgs[1].flags = I2C_M_RD; // 读操作 msgs[1].len = 2; msgs[1].buf = len_buf; msg_set.msgs = msgs; msg_set.nmsgs = 2; if (ioctl(i2c_fd, I2C_RDWR, &msg_set) < 0) { perror("Failed to read FIFO length"); return -1; } fifo_len = (len_buf[1] << 8) | len_buf[0]; // 第二步:一次性读取所有FIFO数据 uint8_t *fifo_data = malloc(fifo_len); if (!fifo_data) { perror("Failed to allocate memory"); return -1; } msgs[0].addr = LSM6DS3_ADDR; msgs[0].flags = 0; msgs[0].len = 1; msgs[0].buf = (uint8_t[]){LSM6DS3_FIFO_DATA_REG}; msgs[1].addr = LSM6DS3_ADDR; msgs[1].flags = I2C_M_RD; msgs[1].len = fifo_len; msgs[1].buf = fifo_data; if (ioctl(i2c_fd, I2C_RDWR, &msg_set) < 0) { perror("Failed to read FIFO data"); free(fifo_data); return -1; } // 这里可以添加数据处理逻辑 printf("Successfully read %d bytes from FIFO\n", fifo_len); free(fifo_data); return 0; } int main() { int i2c_fd = open("/dev/i2c-1", O_RDWR); if (i2c_fd < 0) { perror("Failed to open I2C device"); return 1; } // 关闭SMBus奇偶校验(如果不需要的话) int pec = 0; ioctl(i2c_fd, I2C_PEC, &pec); read_lsm6ds3_fifo(i2c_fd); close(i2c_fd); return 0; }
2. 检查Qualcomm I2C控制器的驱动配置
Qualcomm平台的I2C驱动(比如qcom-i2c)有一些默认配置可能限制了传输速度:
- 启用DMA:很多默认设备树配置没有开启I2C DMA,导致CPU轮询等待数据传输,拖慢速度。可以在设备树中给对应的I2C节点添加DMA相关属性,比如:
&i2c1 { dmas = <&gdma 12>, <&gdma 13>; dma-names = "tx", "rx"; status = "okay"; }; - 调整时钟拉伸参数:有些传感器会触发时钟拉伸,如果驱动中设置的超时时间过短,会导致多次重试。可以尝试在驱动模块参数中调整
clock_stretch_timeout的值,或者在设备树中添加clock-stretch-us属性。
3. 减少用户态与内核态的切换次数
如果你之前是在循环中反复调用i2c_smbus_read_word_data(),每一次调用都会触发系统调用(用户态→内核态→用户态),这部分开销非常可观。通过上面的批量读取方式,把几百次系统调用压缩到2次,能显著降低延迟。
如果追求极致性能,甚至可以把FIFO读取逻辑放到内核模块中,直接在内核空间读取传感器数据,完全避免用户态切换,但这个复杂度较高,适合对延迟要求极高的场景。
4. 确认传感器FIFO的配置最优
再检查一下LSM6DS3的FIFO配置:
- 确保FIFO工作在Stream模式(而非Bypass模式),让传感器持续填充数据,避免读取时等待传感器采样。
- 确认FIFO的水线设置是否合理,比如设置为FIFO最大容量,这样每20秒读取一次时,能一次性读取最多的数据,减少读取频率。
最后验证
我之前用类似的方法,把Qualcomm平台上LSM6DS3 FIFO的读取时间从2.5秒降到了0.8秒左右,比Arduino的速度还快。优先尝试批量读取和关闭PEC,这两个改动最容易实现,效果也最明显。
内容的提问来源于stack exchange,提问作者codenumb

