2、I2C子系统详解:从协议到调试,打通传感器通信的任督二脉

传感器驱动开发,说白了就是跟I2C打交道。RK平台上的加速度计、陀螺仪、温湿度传感器,十有八九都挂在I2C总线上。这一章咱们就把I2C子系统彻底吃透——从总线协议回顾,到RK平台控制器驱动,再到设备驱动模板,最后是调试技巧。我做了这么多年驱动,踩过的I2C坑少说也有十几个,今天一并分享给你。

2.1 I2C总线协议回顾:那些容易忽略的细节

I2C协议本身不复杂,但细节决定成败。咱们快速过一遍核心要点。

协议要素 关键参数 常见坑点
起始条件 SCL高电平时SDA下降沿 时序不满足导致从机不响应
从机地址 7位或10位,左对齐 地址移位错误(常见!)
读写位 0写1读 与地址组合成8位时搞反
应答机制 每字节后ACK/NACK 从机忙时拉低SCL时钟拉伸
时钟频率 标准100kHz,快速400kHz RK平台默认400kHz,部分传感器不支持
数据格式 MSB先行,大端 寄存器地址和数据顺序搞混
⚠️ 我曾经踩过的坑: 有个温湿度传感器死活读不到数据,折腾了两天。最后发现是7位地址0x44,我直接写成了0x88(左移了一位)。I2C地址在数据手册里通常给的是7位值,但驱动里要左移1位拼上读写位。这个细节,新手必错。

再说说时钟拉伸(Clock Stretching)。有些慢速传感器在收到数据后,会拉低SCL线,告诉主机“我还没准备好,你等等”。RK平台的I2C控制器默认支持这个特性,但如果你用的传感器不支持,反而会出问题。我建议你在设备树里显式配置这个参数。

2.2 RK平台I2C控制器驱动:硬件怎么工作的

RK平台的I2C控制器,本质上是一个状态机。它负责把软件发来的消息转换成SCL和SDA上的时序波形。咱们来看看它的核心架构。

核心要点: RK I2C控制器支持FIFO模式,可以一次传输多个字节,效率比中断模式高不少。但要注意FIFO深度只有32字节,超过就要分段传输。

控制器驱动主要处理这几件事:

  • 时钟配置: 根据设备树里的clock-frequency计算分频系数
  • 消息拆分: 把一次I2C传输拆成起始、地址、数据、停止等阶段
  • 中断处理: 处理传输完成、错误、超时等中断
  • DMA传输: 大块数据时启用DMA,减轻CPU负担

我个人习惯在调试新传感器时,先把DMA关掉,用PIO模式。为什么?因为DMA涉及缓存一致性、描述符管理等复杂逻辑,出了问题不好定位。等PIO模式调通了,再切到DMA提升性能。

2.3 I2C设备驱动编写模板:照着写就能用

下面这个模板,是我在多个RK项目里提炼出来的。你写传感器驱动时,直接套用这个框架就行。

#include <linux/i2c.h>
#include <linux/module.h>

struct sensor_data {
    struct i2c_client *client;
    int irq;
    /* 你的传感器状态变量 */
};

static int sensor_read_reg(struct i2c_client *client, u8 reg, u8 *val)
{
    struct i2c_msg msg[2];
    int ret;

    msg[0].addr = client->addr;
    msg[0].flags = 0;       /* 写 */
    msg[0].len = 1;
    msg[0].buf = &reg;

    msg[1].addr = client->addr;
    msg[1].flags = I2C_M_RD; /* 读 */
    msg[1].len = 1;
    msg[1].buf = val;

    ret = i2c_transfer(client->adapter, msg, 2);
    if (ret != 2) {
        dev_err(&client->dev, "read reg 0x%02x failed\n", reg);
        return ret < 0 ? ret : -EIO;
    }
    return 0;
}

static int sensor_probe(struct i2c_client *client)
{
    struct sensor_data *data;
    u8 chip_id;

    /* 1. 分配私有数据 */
    data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;

    data->client = client;
    i2c_set_clientdata(client, data);

    /* 2. 读取芯片ID验证连接 */
    sensor_read_reg(client, 0x0F, &chip_id);
    if (chip_id != EXPECTED_ID) {
        dev_err(&client->dev, "chip id mismatch: 0x%02x\n", chip_id);
        return -ENODEV;
    }

    /* 3. 初始化传感器配置 */
    /* ... */

    /* 4. 注册输入设备或IIO设备 */
    /* ... */

    return 0;
}

static const struct of_device_id sensor_of_match[] = {
    { .compatible = "vendor,sensor-model" },
    { }
};
MODULE_DEVICE_TABLE(of, sensor_of_match);

static struct i2c_driver sensor_driver = {
    .driver = {
        .name = "sensor_drv",
        .of_match_table = sensor_of_match,
    },
    .probe = sensor_probe,
    .remove = sensor_remove,
    .id_table = sensor_id_table,
};
module_i2c_driver(sensor_driver);
💡 我的经验: probe函数里一定要先读芯片ID验证。我遇到过好几次设备树里配的地址不对,或者焊接虚焊,结果驱动加载了但数据全是0xFF。加个ID校验,能帮你快速定位硬件问题。

2.4 I2C通信调试技巧:三板斧解决问题

调试I2C通信,说白了就是三板斧:看波形、抓日志、测时序。咱们一个一个说。

2.4.1 逻辑分析仪看波形

这是最直接的方法。把SCL和SDA接到逻辑分析仪上,抓一段通信波形。你想想看,地址对不对?ACK有没有?数据对不对?一眼就能看出来。我建议你买一个带I2C解码的,比如Saleae或者国产的Kingst,几百块钱,能省你几天时间。

2.4.2 内核I2C调试接口

Linux内核提供了i2c-tools工具集,在RK平台上可以直接用。我最常用的是i2cdetect和i2cget/i2cset。

# 扫描总线上的设备
i2cdetect -y 2

# 读取设备寄存器
i2cget -y 2 0x44 0x0F

# 写入设备寄存器
i2cset -y 2 0x44 0x10 0x01

这里有个技巧:i2cdetect扫描时,如果地址显示UU,说明该地址已经被内核驱动占用了。如果显示--,说明没有设备响应。如果显示数字(比如44),说明有设备但没驱动。这个信息对排查很有用。

2.4.3 动态调试打印

打开I2C子系统的动态调试,可以看到每次传输的详细信息。

# 打开I2C核心调试
echo 'file drivers/i2c/i2c-core-base.c +p' > /sys/kernel/debug/dynamic_debug/control

# 打开RK平台控制器调试
echo 'file drivers/i2c/busses/i2c-rk3x.c +p' > /sys/kernel/debug/dynamic_debug/control

打开后,dmesg里会打印每次I2C传输的地址、数据、状态。嗯,这里要注意,生产环境别开这个,日志量太大。

⚠️ 避坑指南: 我曾经在一个项目里,传感器偶尔通信失败。用逻辑分析仪抓波形,发现SCL线上有毛刺。最后排查出来是I2C上拉电阻阻值不对——RK平台默认用4.7kΩ,但传感器手册要求2.2kΩ。换电阻后问题解决。所以,硬件参数一定要跟传感器数据手册对齐。

2.5 本章知识体系总览

下面这张图,把I2C子系统的核心脉络画出来了。你写驱动时,脑子里要有这张图。

I2C子系统知识体系 I2C子系统知识体系 I2C核心层 控制器驱动 设备驱动 控制器驱动核心功能 • 时钟配置与分频 • 消息拆分与状态机 • 中断处理与DMA传输 设备驱动编写模板 • probe:ID校验与初始化 • 读写:i2c_transfer封装 • 注册:i2c_driver结构体 调试技巧:波形 + 日志 + 工具 常见问题与避坑 地址移位错误 | 时钟拉伸不匹配 | 上拉电阻阻值不当 | FIFO溢出

这张图把I2C子系统分成了五层:核心层、控制器驱动、设备驱动、调试技巧、常见问题。你写驱动时,从上往下看:先搞清楚核心层的接口,再写控制器配置,然后套设备驱动模板,最后用调试技巧验证。遇到问题,去最下面那层找答案。

总结一下: I2C驱动开发,七分在协议理解,三分在代码实现。协议细节搞清楚了,代码就是模板填空。调试时别盲目改代码,先看波形确认硬件没问题。记住这三句话,能少走很多弯路。