4. I2C子系统详解:从协议到驱动分离模型

I2C总线,说实话,是嵌入式开发里最常用的低速总线之一。传感器、EEPROM、触摸屏、音频Codec……十有八九都挂在I2C上。我刚开始做驱动时,总觉得I2C很简单——不就两根线嘛。直到被一个时序问题折腾了两天,才老老实实把协议细节啃了一遍。

这一章,咱们把I2C子系统拆开揉碎。从硬件协议到Linux内核的软件架构,再到驱动怎么分层、怎么解耦,一条线讲清楚。

4.1 I2C总线协议基础

I2C只有两根线:SCL(时钟)和SDA(数据)。所有设备都挂在这两根线上,靠地址区分。主设备发起通信,从设备响应。

核心要点:I2C是同步、半双工、多主从的总线。标准模式100kHz,快速模式400kHz,高速模式3.4MHz。传感器驱动里,100kHz和400kHz最常见。

4.1.1 通信时序

一个完整的I2C传输包含以下阶段:

  1. 起始条件(START):SCL高电平时,SDA从高变低。
  2. 从设备地址 + 读写位:主设备发送7位地址(或10位),最后一位是R/W。
  3. 应答(ACK/NACK):从设备拉低SDA表示应答。
  4. 数据字节:每字节8位,MSB先发,后面跟应答位。
  5. 停止条件(STOP):SCL高电平时,SDA从低变高。

我遇到过一个问题:某个传感器在400kHz下偶尔丢数据。后来用示波器一看,发现从设备的ACK信号延迟超过了规范。降到100kHz就稳了。嗯,有时候不是代码的问题,是硬件时序余量不够。

经验之谈:调试I2C时,示波器或逻辑分析仪是必备的。别光靠printk,你看不到ACK的波形。

4.1.2 数据格式

I2C的数据传输格式很灵活。传感器驱动里最常用的是这种:

// 写寄存器:先发寄存器地址,再发数据
START + 设备地址(W) + ACK + 寄存器地址 + ACK + 数据 + ACK + STOP

// 读寄存器:先写寄存器地址,再重新发起START读数据
START + 设备地址(W) + ACK + 寄存器地址 + ACK + 
RESTART + 设备地址(R) + ACK + 数据 + NACK + STOP

说白了,读操作需要一次“重复起始条件”(Repeated START)。很多新手在这里犯错——以为读之前要先发STOP再发START。其实不用,RESTART更高效,也避免了总线被其他主设备抢走。

4.2 Linux I2C核心层

Linux内核把I2C子系统分成了三层。我画了张图,你看一眼就明白了:

Linux I2C 子系统三层架构 用户空间 应用程序 / i2c-tools / sysfs I2C 核心层 (i2c-core) i2c_add_adapter / i2c_add_driver / i2c_transfer 设备与驱动匹配 / 总线管理 / 地址检测 I2C 总线驱动层 i2c_adapter / i2c_algorithm 硬件相关:控制I2C控制器寄存器 例:i2c-imx, i2c-s3c, i2c-omap I2C 设备驱动层 i2c_driver / i2c_client 协议相关:读写传感器寄存器 例:bmp280, mpu6050, ds1307 硬件层 I2C控制器(SoC内部) + I2C从设备(传感器) 核心层隔离了总线驱动和设备驱动,双方只需调用标准接口,无需了解对方硬件细节

这张图你得记住。三层架构的核心思想就是分离——总线驱动只管怎么发数据,设备驱动只管发什么数据。两者通过核心层对接。

4.2.1 核心层关键数据结构

核心层有几个关键结构体,驱动开发时天天打交道:

结构体 作用 关键字段
i2c_adapter 代表一个I2C控制器 algo(算法指针)、nr(总线编号)
i2c_algorithm 定义收发数据的底层方法 master_xfer、smbus_xfer
i2c_driver 代表一个设备驱动 probe、remove、id_table
i2c_client 代表一个具体的I2C从设备 addr(7位地址)、adapter(所属总线)

注意:i2c_client的地址是7位,左对齐的。很多数据手册写的是8位地址(含读写位),你得右移1位再赋值。我见过有人直接拿8位地址填进去,结果驱动死活找不到设备。

4.3 I2C设备驱动与总线驱动分离模型

这是Linux设备驱动模型在I2C子系统上的具体体现。说白了,就是“谁负责控制硬件,谁负责解析协议”分开。

4.3.1 总线驱动(Adapter Driver)

总线驱动由SoC厂商提供,负责操作I2C控制器的寄存器。它要实现i2c_algorithm中的master_xfer函数。这个函数接收一个i2c_msg数组,然后按照I2C协议在硬件上发出START、地址、数据、STOP。

举个例子,i2c-imx驱动里,master_xfer最终会操作I2C_IADR、I2C_IFDR、I2C_I2DR等寄存器。这部分代码,你作为传感器驱动开发者,基本不用碰。除非你在移植内核到新板子。

4.3.2 设备驱动(Client Driver)

这才是咱们传感器驱动开发的主战场。设备驱动通过核心层提供的API与硬件通信,不关心底层是哪个I2C控制器。

核心API就这几个:

// 最常用的读写接口
int i2c_master_send(struct i2c_client *client, const char *buf, int count);
int i2c_master_recv(struct i2c_client *client, char *buf, int count);

// 带寄存器地址的读写(SMBus风格)
int i2c_smbus_read_byte_data(struct i2c_client *client, u8 command);
int i2c_smbus_write_byte_data(struct i2c_client *client, u8 command, u8 value);

// 更灵活的传输(支持多个msg)
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);

我的建议:传感器驱动里,优先用i2c_smbus_xxx系列。它封装了重复起始条件,代码更简洁。如果传感器有特殊时序要求(比如连续读取多个寄存器),再用i2c_transfer自己组装msg。

4.3.3 驱动匹配机制

设备驱动怎么找到对应的硬件设备?Linux用设备树(Device Tree)来完成匹配。在设备树里声明I2C从设备,驱动通过of_match_table匹配。

设备树片段示例:

&i2c1 {
    status = "okay";
    clock-frequency = <400000>;

    bmp280@76 {
        compatible = "bosch,bmp280";
        reg = <0x76>;
    };
};

驱动里对应的匹配表:

static const struct of_device_id bmp280_of_match[] = {
    { .compatible = "bosch,bmp280" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, bmp280_of_match);

static struct i2c_driver bmp280_driver = {
    .driver = {
        .name = "bmp280",
        .of_match_table = bmp280_of_match,
    },
    .probe = bmp280_probe,
    .remove = bmp280_remove,
    .id_table = bmp280_id,  // 传统ID匹配,用于非设备树平台
};

匹配成功后,核心层调用驱动的probe函数。你在probe里做初始化、注册输入设备或IIO设备、创建sysfs文件等。

4.4 实战:写一个完整的I2C传感器驱动骨架

光说不练假把式。咱们写一个虚拟温度传感器的驱动骨架,把上面讲的知识点串起来。

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

#define TEMP_SENSOR_REG_TEMP    0x00
#define TEMP_SENSOR_REG_CONFIG  0x01

struct temp_sensor_data {
    struct i2c_client *client;
    int temperature;
};

static int temp_sensor_read_reg(struct i2c_client *client, u8 reg, u8 *val)
{
    int ret;
    ret = i2c_smbus_read_byte_data(client, reg);
    if (ret < 0)
        return ret;
    *val = ret & 0xff;
    return 0;
}

static int temp_sensor_write_reg(struct i2c_client *client, u8 reg, u8 val)
{
    return i2c_smbus_write_byte_data(client, reg, val);
}

static int temp_sensor_probe(struct i2c_client *client)
{
    struct temp_sensor_data *data;
    u8 chip_id;
    int ret;

    // 1. 检测设备是否存在
    ret = temp_sensor_read_reg(client, TEMP_SENSOR_REG_CONFIG, &chip_id);
    if (ret < 0) {
        dev_err(&client->dev, "failed to read config register\n");
        return ret;
    }

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

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

    // 3. 初始化设备(比如设置连续转换模式)
    temp_sensor_write_reg(client, TEMP_SENSOR_REG_CONFIG, 0x01);

    dev_info(&client->dev, "temperature sensor probed, chip_id=0x%02x\n", chip_id);
    return 0;
}

static void temp_sensor_remove(struct i2c_client *client)
{
    // 关闭设备,释放资源
    temp_sensor_write_reg(client, TEMP_SENSOR_REG_CONFIG, 0x00);
    dev_info(&client->dev, "temperature sensor removed\n");
}

static const struct i2c_device_id temp_sensor_id[] = {
    { "temp-sensor", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, temp_sensor_id);

static const struct of_device_id temp_sensor_of_match[] = {
    { .compatible = "virtual,temp-sensor" },
    { }
};
MODULE_DEVICE_TABLE(of, temp_sensor_of_match);

static struct i2c_driver temp_sensor_driver = {
    .driver = {
        .name = "temp-sensor",
        .of_match_table = temp_sensor_of_match,
    },
    .probe = temp_sensor_probe,
    .remove = temp_sensor_remove,
    .id_table = temp_sensor_id,
};

module_i2c_driver(temp_sensor_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Virtual Temperature Sensor Driver");

避坑指南:我曾经在probe里忘了调用i2c_set_clientdata,结果remove时拿不到私有数据,导致空指针panic。这个小细节,记牢了。

4.5 调试技巧

I2C驱动调试,我总结了几条实用经验:

  • 用i2c-tools验证硬件:在用户空间用i2cdetect -r 1扫描总线,确认设备地址。再用i2cget和i2cset手动读写寄存器。硬件没问题了,再写驱动。
  • 打开内核调试信息:# echo 0x10 > /sys/module/i2c_core/parameters/debug,可以看到每次传输的细节。
  • 检查ACK:如果i2c_transfer返回负值,大概率是从设备没应答。检查地址、电源、上拉电阻。
  • 时序问题:用示波器抓SCL和SDA,看START条件、数据位、ACK位是否正常。我遇到过SCL频率偏差太大导致通信失败的案例。

重要提醒:I2C从设备的地址是7位,但很多数据手册写的是8位(含读写位)。比如手册写0xD0,实际7位地址是0x68。填错了,驱动永远probe失败。