I2C适配器与算法:从一次诡异的时序问题说起

上周调一块传感器板子,I2C死活读不出数据。示波器抓波形一看,SCL时钟频率飘忽不定,高电平宽度时宽时窄。第一反应是上拉电阻问题,换了几个值依旧无解。最后发现是适配器驱动里的时钟分频计算写崩了------这让我重新审视了Linux I2C子系统里最核心的两个结构:i2c_adapter和i2c_algorithm。


适配器:硬件载体的抽象

在Linux的I2C框架里,i2c_adapter代表一个物理I2C控制器。它可以是SoC内置的I2C控制器,也可以是GPIO模拟的软实现。关键字段就这几个:

c 复制代码
struct i2c_adapter {
    struct module *owner;
    const struct i2c_algorithm *algo;  // 核心!算法操作集
    void *algo_data;                    // 算法私有数据
    int nr;                            // 适配器编号(i2c-0、i2c-1那个数字)
    char name[48];                     // 适配器名字,dmesg里能看到
    struct device dev;                 // 设备模型基础
    // ... 其他省略
};

注册适配器时最常掉坑的是nr字段。如果填-1,内核会自动分配编号,但有些老驱动硬编码了编号,结果系统里多个适配器编号冲突,/dev/i2c-*对不上号。建议新驱动都让内核自动分配,通过of_i2c_get_adapter()之类接口去获取适配器指针。


算法:硬件操作的灵魂

i2c_algorithm才是真正干活的。它定义了如何在这个硬件上发起I2C传输:

c 复制代码
struct i2c_algorithm {
    int (*master_xfer)(struct i2c_adapter *adap, 
                       struct i2c_msg *msgs, int num);
    int (*smbus_xfer)(struct i2c_adapter *adap, u16 addr,
                      unsigned short flags, char read_write,
                      u8 command, int size, union i2c_smbus_data *data);
    u32 (*functionality)(struct i2c_adapter *adap);
};

master_xfer是必须实现的,SMBus那个可以留空。重点是这个functionality函数,它告诉上层这个控制器支持哪些特性:

c 复制代码
static u32 my_i2c_func(struct i2c_adapter *adap)
{
    return I2C_FUNC_I2C |              // 支持标准I2C协议
           I2C_FUNC_SMBUS_QUICK |      // 支持SMBus快速命令
           I2C_FUNC_10BIT_ADDR;        // 支持10位地址模式
}

这里踩过大坑:有一次没声明I2C_FUNC_PROTOCOL_MANGLING,结果上层想发重复START条件时直接失败。内核的I2C核心会根据这个标志决定是否帮你处理特殊时序,硬件不支持就得自己想办法。


实现一个GPIO模拟的I2C适配器

用GPIO模拟是最直观的理解方式。关键在实现master_xfer:

c 复制代码
static int gpio_i2c_xfer(struct i2c_adapter *adap, 
                         struct i2c_msg *msgs, int num)
{
    struct gpio_i2c_data *data = adap->algo_data;
    int ret, i;
    
    // 每个msg可能代表一次读或写操作
    for (i = 0; i < num; i++) {
        // 发START条件:SCL高时SDA拉低
        gpiod_set_value(data->sda_gpio, 1);
        gpiod_set_value(data->scl_gpio, 1);
        udelay(5);  // 这里要严格满足时序要求,太短设备不认
        gpiod_set_value(data->sda_gpio, 0);
        udelay(5);
        gpiod_set_value(data->scl_gpio, 0);
        
        // 发地址+读写位
        ret = gpio_i2c_send_byte(data, msgs[i].addr << 1 | 
                                 (msgs[i].flags & I2C_M_RD ? 1 : 0));
        if (ret < 0) {
            gpio_i2c_stop(data);  // 出错记得发STOP
            return ret;
        }
        
        // 处理数据段
        if (msgs[i].flags & I2C_M_RD) {
            ret = gpio_i2c_read_bytes(data, msgs[i].buf, msgs[i].len);
        } else {
            ret = gpio_i2c_write_bytes(data, msgs[i].buf, msgs[i].len);
        }
        
        // 每个msg结束发STOP,除非要求NO_STOP
        if (!(msgs[i].flags & I2C_M_NO_STOP)) {
            gpio_i2c_stop(data);
        }
    }
    return i;  // 成功处理的msg数量
}

调试这种驱动时,一定要在gpio_i2c_send_byte()里加printk打印每个字节,不然时序错了都不知道死在哪一步。曾经因为ACK检测逻辑写反,读数据总是0xFF,折腾了一整天。


硬件控制器的寄存器操作

真实硬件控制器驱动主要就是配置寄存器。以某款SoC的I2C控制器为例:

c 复制代码
static int hw_i2c_xfer(struct i2c_adapter *adap, 
                       struct i2c_msg *msgs, int num)
{
    struct hw_i2c_dev *dev = adap->algo_data;
    unsigned long timeout;
    
    // 1. 使能控制器时钟(很多SoC不默认开时钟)
    clk_prepare_enable(dev->clk);
    
    // 2. 配置时钟分频,这里容易算错
    u32 div = clk_get_rate(dev->clk) / (dev->bus_freq * 2) - 1;
    writel(div, dev->base + I2C_CLK_DIV_REG);
    
    // 3. 清中断状态,不然可能一上来就进中断
    writel(0xFF, dev->base + I2C_INT_CLEAR_REG);
    
    // 4. 遍历处理所有消息
    for (int i = 0; i < num; i++) {
        // 填目标地址
        writel(msgs[i].addr, dev->base + I2C_TARGET_ADDR_REG);
        
        // 配置传输方向和数据长度
        u32 ctrl = msgs[i].len & 0xFF;
        if (msgs[i].flags & I2C_M_RD)
            ctrl |= I2C_CTRL_READ_BIT;
        writel(ctrl, dev->base + I2C_CTRL_REG);
        
        // 触发传输
        writel(I2C_CMD_START, dev->base + I2C_CMD_REG);
        
        // 等中断或轮询状态寄存器
        timeout = jiffies + msecs_to_jiffies(1000);
        while (!(readl(dev->base + I2C_STATUS_REG) & I2C_XFER_DONE)) {
            if (time_after(jiffies, timeout)) {
                dev_err(dev->dev, "i2c transfer timeout\n");
                return -ETIMEDOUT;
            }
            cpu_relax();
        }
        
        // 读数据寄存器(如果是读操作)
        if (msgs[i].flags & I2C_M_RD) {
            for (int j = 0; j < msgs[i].len; j++) {
                msgs[i].buf[j] = readl(dev->base + I2C_DATA_REG + j);
            }
        }
    }
    
    clk_disable_unprepare(dev->clk);
    return num;
}

寄存器版本最头疼的是中断处理和DMA配置。建议先用轮询调通,再加中断优化。有个坑要注意:有些控制器要求先配置DMA再使能I2C,顺序反了数据传不出。


适配器注册的完整流程

c 复制代码
static const struct i2c_algorithm my_algo = {
    .master_xfer    = my_i2c_xfer,
    .functionality  = my_i2c_func,
};

static int my_i2c_probe(struct platform_device *pdev)
{
    struct i2c_adapter *adap;
    struct my_priv *priv;
    
    // 1. 分配私有数据结构
    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    
    // 2. 映射寄存器、申请GPIO、配置时钟等
    priv->base = devm_platform_ioremap_resource(pdev, 0);
    priv->clk = devm_clk_get(&pdev->dev, NULL);
    
    // 3. 填充适配器结构
    adap = &priv->adapter;
    adap->owner = THIS_MODULE;
    adap->class = I2C_CLASS_HWMON | I2C_CLASS_SPD;  // 声明支持的设备类
    adap->algo = &my_algo;
    adap->algo_data = priv;  // 这里赋值,xfer函数才能拿到私有数据
    adap->dev.parent = &pdev->dev;
    adap->dev.of_node = pdev->dev.of_node;
    snprintf(adap->name, sizeof(adap->name), "my-i2c-%s", 
             dev_name(&pdev->dev));
    
    // 4. 设置超时和重试次数(默认值可能不适合你的设备)
    adap->timeout = HZ;      // 1秒超时
    adap->retries = 3;       // 失败重试3次
    
    // 5. 注册到内核
    i2c_add_adapter(adap);   // 或者i2c_add_numbered_adapter()
    
    // 6. 如果是平台设备,最好也添加到设备树兼容列表
    of_i2c_register_devices(adap);
    
    platform_set_drvdata(pdev, priv);
    return 0;
}

注册失败最常见的原因是adap->owner没设对,模块卸载时内核会抱怨。还有adap->class字段,如果你明确知道这个I2C总线只接某种设备(比如只有温度传感器),就设上对应的CLASS,能避免不必要的设备探测。


调试技巧与经验

  1. 先看/sys/bus/i2c/devices/i2c-*/name,确认你的适配器注册成功,名字对不对。

  2. 用i2cdetect -l列出所有适配器 ,再用i2cdetect -y <编号>扫描设备地址。扫不出来先查硬件,再查驱动。

  3. 内核配置打开CONFIG_I2C_DEBUG_CORE ,能看到详细的I2C传输日志。不过输出量很大,最好用dmesg -w实时看。

  4. GPIO模拟I2C时,用逻辑分析仪抓波形最直接。注意SCL/SDA的边沿时间,很多设备对上升沿时间有要求。

  5. 硬件I2C控制器驱动,先确保时钟配置正确。我遇到过因为父时钟分频比不对,实际I2C频率只有预期一半的情况。

  6. 10位地址设备 ,除了声明I2C_FUNC_10BIT_ADDR,还要注意地址传递方式:msg->addr直接存10位地址,内核会自动处理两次地址发送。

  7. 适配器注销要在remove里做 ,但更推荐用devm_i2c_add_adapter(),让设备模型自动管理生命周期。


最后说点个人体会:I2C驱动调试,三分在软件,七分在硬件。波形抓对了,问题就解决了一大半。写驱动时多想想"硬件这时候在干嘛",比盲目改代码有效得多。还有,别完全相信数据手册的时序图------有些芯片的I2C实现比较"个性",稍微不符合标准但能工作,这时候该妥协就得妥协,加几个udelay()能解决的事,别死磕。

相关推荐
奋发向前wcx2 小时前
CSP-J复赛模拟赛4 王晨旭补题 2026.10.5
算法
huainingning4 小时前
RJ SW Console口忘记密码处理方法
linux·运维·服务器
布裘4 小时前
【银河麒麟】桌面系统循环登录,无法进入桌面?
linux·运维·服务器
慧都小项4 小时前
程序到了Linux才出错?用CLion把调试接到目标环境
linux·运维·服务器
念何架构之路4 小时前
zap采样器与性能优化内幕
算法·性能优化·哈希算法
晒太羊的猫5 小时前
hot100动态规划刷题
算法·动态规划
SuperByteMaster5 小时前
allegro 布线规则管理器
嵌入式硬件
weixin_531094696 小时前
heap_4内存管理
java·开发语言·算法
余额瞒着我当琳6 小时前
红黑树的实现--红黑树的概念、规则、效率、结构、旋转、查找、验证
数据结构·c++·算法
程序员南飞6 小时前
win系统安装minio
java·数据结构·算法