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

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

相关推荐
阿凉07022 小时前
STM32 Flash 扇区分布学习
stm32·嵌入式硬件·学习
啊哦呃咦唔鱼2 小时前
leetcode二分查找
数据结构·算法·leetcode
郝学胜-神的一滴2 小时前
[ 力扣 1124 ] 解锁最长良好时段问题:前缀和+哈希表的优雅解法
java·开发语言·数据结构·python·算法·leetcode·散列表
戴西软件2 小时前
戴西CAxWorks.VPG车辆工程仿真软件|假人+座椅双调整 汽车仿真效率直接拉满
java·开发语言·人工智能·python·算法·ui·汽车
Tairitsu_H2 小时前
C++入门指南:从基础语法到核心特性全解析
c++·算法·基础
programhelp_2 小时前
2026 高盛(Goldman Sachs)Coding Interview 真题分享|Design HashMap + 其他面试题完整解析
算法·哈希算法
Pentane.2 小时前
力扣HOT100:T.1 两数之和|循环遍历算法笔记及打卡(12/100)
c++·笔记·算法·leetcode
王老师青少年编程2 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【线性扫描贪心】:士兵站队
c++·算法·贪心算法·csp·信奥赛·线性扫描贪心·士兵战队
IMPYLH2 小时前
Linux 的 readlink 命令
linux·运维·服务器·网络·bash