3. I2C协议栈实现:从总线信号到驱动落地

I2C,搞嵌入式的没人不知道它。两根线,一主多从,简单得让人想笑。但真要在项目里把I2C驱动写得稳、写得快,光会调库可不够。今天我就带你从总线协议到底层适配器,再到从设备驱动,把整个I2C协议栈捋一遍。

3.1 I2C总线协议:两根线上的舞蹈

I2C只有两条线:SCL(时钟)和SDA(数据)。所有设备都挂在这两根线上,靠地址区分。你想想看,一个MCU挂十几个传感器,只用两根线,省GPIO省到极致。

但省线的代价是什么?协议时序必须精确。我早期有个项目,I2C挂了个温湿度传感器,死活读不到数据。示波器一抓,发现SCL高电平时间偏短,从机不认。嗯,这就是时序的坑。

3.1.1 基本时序单元

I2C的每个操作都由几个基本信号组成:

信号描述时序要求
起始条件 (S)SCL高电平时,SDA从高变低tHD:STA ≥ 4μs (100kHz)
停止条件 (P)SCL高电平时,SDA从低变高tSU:STO ≥ 4μs
数据采样SCL低电平时改变SDA,高电平时采样tSU:DAT ≥ 250ns
应答 (ACK)第9个SCL周期,从机拉低SDA从机必须在SCL高前准备好

这里有个关键点:数据只能在SCL低电平时变化。SCL高电平时SDA必须稳定。我见过不少新手写的软件模拟I2C,在SCL高电平期间动了SDA,结果从机直接当成起始或停止条件——数据全乱套。

3.1.2 完整读写流程

一个典型的I2C写操作流程:

  1. 主机发送起始条件
  2. 主机发送7位从机地址 + 写位(0)
  3. 从机应答ACK
  4. 主机发送寄存器地址
  5. 从机应答ACK
  6. 主机发送数据字节,每字节后等待ACK
  7. 主机发送停止条件

读操作稍微复杂一点,需要先写寄存器地址,再重启起始条件,然后读数据:

// I2C读操作伪代码
start();
write(addr << 1 | 0);  // 写地址+写位
wait_ack();
write(reg_addr);         // 写寄存器地址
wait_ack();
repeated_start();        // 重启条件
write(addr << 1 | 1);  // 写地址+读位
wait_ack();
data = read();           // 读数据
send_nack();             // 主机发送非应答
stop();
我的习惯:每次写完寄存器地址后,加一个小的延时再发重启条件。有些从机芯片处理速度慢,连续操作会丢ACK。这个延时我一般设10μs,够用又不拖性能。

3.2 Linux I2C核心层:别重复造轮子

Linux内核已经把I2C协议栈分成了三层:核心层、适配器驱动、从设备驱动。你写驱动时,大部分时候只需要关心从设备驱动这一层。

3.2.1 核心层数据结构

核心层主要维护三个结构体:

  • struct i2c_adapter:代表一个I2C控制器,包含算法函数指针
  • struct i2c_algorithm:master_xfer等底层传输函数
  • struct i2c_client:代表一个从设备,挂载在适配器上

它们的关系是这样的:

Linux I2C 核心层结构关系 struct i2c_adapter I2C控制器抽象 包含: algo, nr, name, dev struct i2c_algorithm 底层传输方法 master_xfer / smbus_xfer struct i2c_client 从设备实例 包含: addr, adapter, driver struct i2c_driver 驱动绑定层 probe / remove / id_table 核心层负责设备与驱动的匹配,适配器负责硬件传输,从设备驱动只关心协议逻辑

3.2.2 适配器驱动:连接硬件与核心层

适配器驱动负责实现i2c_algorithm中的master_xfer函数。这个函数接收一个i2c_msg数组,里面包含读写标志、数据缓冲区和长度。

我曾经调试过一个I2C适配器驱动,发现master_xfer里每次只处理一条消息。但上层驱动有时会发多条消息(比如先写地址再读数据),适配器必须支持消息合并。如果适配器不支持,核心层会自动拆成多次传输,但效率会下降。

注意:如果你的适配器硬件支持自动发送停止条件,记得在消息边界处正确处理。否则会出现「粘包」现象——两次传输之间没有停止条件,从机状态机就乱了。

3.3 从设备驱动开发:实战出真知

好了,到了最核心的部分——写一个I2C从设备驱动。我以一个常见的温度传感器为例,带你走一遍完整流程。

3.3.1 驱动框架搭建

先定义设备结构体和设备ID表:

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

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

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

然后实现probe和remove函数:

static int temp_sensor_probe(struct i2c_client *client,
                             const struct i2c_device_id *id)
{
    struct temp_sensor_data *data;
    int ret;

    // 检查设备是否存在
    ret = i2c_smbus_read_byte_data(client, 0x0F); // 读设备ID寄存器
    if (ret < 0) {
        dev_err(&client->dev, "device not found\n");
        return -ENODEV;
    }

    data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;

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

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

static void temp_sensor_remove(struct i2c_client *client)
{
    dev_info(&client->dev, "temp sensor removed\n");
}

3.3.2 数据读写:避开常见的坑

读写数据时,我建议优先使用i2c_smbus_*系列函数。它们封装了重复起始条件,代码更简洁:

static int temp_sensor_read_temp(struct i2c_client *client)
{
    int temp_raw;

    // 读温度寄存器 (0x00),16位数据
    temp_raw = i2c_smbus_read_word_swapped(client, 0x00);
    if (temp_raw < 0)
        return temp_raw;

    // TMP117: 分辨率 0.0078125°C/bit
    return temp_raw * 78125 / 10000;
}

这里有个细节:i2c_smbus_read_word_swapped会自动交换字节序。为什么?因为I2C设备通常是大端传输,而ARM/Little Endian处理器需要转换。我刚开始写驱动时没注意这个,读出来的温度值直接除以16,结果全是乱码。后来查了芯片手册才发现字节序问题。

3.3.3 注册驱动

最后,把驱动注册到内核:

static struct i2c_driver temp_sensor_driver = {
    .driver = {
        .name = "tmp117",
        .of_match_table = of_match_ptr(temp_sensor_of_match),
    },
    .probe    = temp_sensor_probe,
    .remove   = temp_sensor_remove,
    .id_table = temp_sensor_id,
};

module_i2c_driver(temp_sensor_driver);

MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("TMP117 Temperature Sensor Driver");
MODULE_LICENSE("GPL");
关键点总结:
  • probe函数里一定要做设备存在性检查,别假设设备一定在线
  • 使用devm_*系列函数管理内存,省去手动释放的麻烦
  • 注意字节序,I2C设备大多是大端,ARM/Linux默认小端
  • 调试时多用dev_dbg和dev_err,别用printk

3.4 调试技巧:示波器是你的眼睛

写I2C驱动,光看代码不够。我调试I2C问题,第一件事就是抓波形。逻辑分析仪便宜又好用,几百块的就能抓100MHz采样率。

常见问题排查清单:

现象可能原因排查方法
无ACK响应地址错误/设备未上电检查地址7位/8位对齐,测VDD
数据全为0xFF上拉电阻缺失加4.7kΩ上拉到VDD
偶尔通信失败时序裕量不足降低速率到50kHz测试
多设备冲突地址重复检查设备地址引脚配置

我记得有一次,客户反馈设备每100次通信就有1次失败。抓波形发现SCL上有毛刺,原来是PCB走线太长,信号反射导致的。加了个33pF电容到地,问题解决。嗯,硬件问题有时候比软件更难搞。

我的调试三板斧:第一,降速到10kHz看能不能稳定通信;第二,用重复读操作验证设备是否真的在线;第三,在probe函数里加i2c_smbus_read_byte读设备ID,确认硬件连接没问题再往下走。

I2C协议栈说简单也简单,说复杂也复杂。核心就三点:理解时序、用好核心层、写好从设备驱动。把这三点吃透了,市面上99%的I2C设备你都能搞定。