第二讲:Linux内核I2C子系统——从协议到驱动实战

大家好,欢迎来到第二讲。今天我们来聊聊I2C子系统。说实话,I2C在嵌入式系统里太常见了——传感器、触摸屏、音频编解码器,十有八九都挂在I2C总线上。我刚开始做驱动时,总觉得I2C很简单,不就是两根线嘛。直到有一次调试一个加速度计,死活读不到数据,最后发现是时序问题……嗯,从那以后我再也不敢小看I2C了。

一、I2C总线协议基础

I2C总线,说白了就是两根线搞定一切通信:一根时钟线(SCL),一根数据线(SDA)。它是个多主从架构,但咱们嵌入式里最常见的就是一个主控(比如CPU)挂一堆从设备(传感器、芯片)。

为什么I2C这么流行?我总结了几点:

  • 引脚少——就两根线,省PCB空间
  • 支持多设备——一条总线上可以挂几十个从设备
  • 速度够用——标准模式100kHz,快速模式400kHz,高速模式能到3.4MHz

但要注意,I2C也有短板。我在项目中遇到过一个问题:总线长度超过半米后,信号就开始变形了。所以长距离通信别指望I2C。

1.1 通信流程

I2C的通信流程其实很清晰:

  1. 主设备发送起始信号(SCL高电平时,SDA从高变低)
  2. 发送7位从设备地址 + 1位读写位
  3. 从设备应答(ACK)
  4. 传输数据(每字节后跟一个ACK)
  5. 主设备发送停止信号(SCL高电平时,SDA从低变高)

关键点:地址是7位的,但很多数据手册会给你8位的地址(左移了一位)。我刚开始就吃过这个亏——手册上写0x68,结果我直接用了,发现不对。后来才明白,0x68是7位地址左移后的结果,实际要右移一位才是真正的7位地址。

1.2 时序图

下面我用SVG画了个I2C的典型读写时序,你一看就明白了:

I2C总线读写时序图 SCL(时钟线) SDA(数据线) 起始 7位从设备地址 R/W ACK 8位数据(1字节) 停止 SCL SDA

二、Linux I2C核心数据结构

Linux内核把I2C子系统抽象得很漂亮。我当年第一次看源码时,觉得这设计真巧妙——把硬件细节全封装了,驱动开发者只需要关心几个核心结构体。

2.1 三个核心结构体

结构体 作用 我常用的字段
i2c_adapter 代表I2C控制器(主控端) nr(总线编号)、algo(通信算法)
i2c_client 代表I2C从设备 addr(设备地址)、adapter(所属总线)
i2c_driver 代表I2C设备驱动 probe、remove、id_table

这三个结构体之间的关系,我打个比方:i2c_adapter就像一条高速公路,i2c_client是路上的收费站,i2c_driver是收费站的管理员。管理员(驱动)负责管理收费站(设备),通过高速公路(总线)跟外界通信。

2.2 数据传输的核心函数

Linux内核提供了几个封装好的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);

// 更灵活的方式——组合传输
struct i2c_msg msg[2];
msg[0].addr = client->addr;
msg[0].flags = 0;        // 写
msg[0].len = 1;
msg[0].buf = ®_addr;

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

i2c_transfer(client->adapter, msg, 2);

我的经验:能用i2c_transfer就别用i2c_master_send/recv。为什么?因为很多传感器需要先写寄存器地址再读数据,组合传输能保证原子性。我曾经用i2c_master_send和i2c_master_recv分开调用,结果在中断上下文里被抢占了,读到的数据全乱套。

三、I2C设备驱动编写模板

好了,理论说完了,咱们直接上代码。我写I2C驱动有个固定套路,你照着这个模板改,基本不会出大问题。

3.1 驱动框架代码

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

// 设备结构体——存放私有数据
struct my_sensor_data {
    struct i2c_client *client;
    int irq;
    // 其他自定义字段
};

// 读寄存器函数
static int my_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 = ®

    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, "读寄存器0x%02x失败\n", reg);
        return -EIO;
    }
    return 0;
}

// 写寄存器函数
static int my_sensor_write_reg(struct i2c_client *client, u8 reg, u8 val)
{
    u8 buf[2] = {reg, val};
    int ret;

    ret = i2c_master_send(client, buf, 2);
    if (ret != 2) {
        dev_err(&client->dev, "写寄存器0x%02x失败\n", reg);
        return -EIO;
    }
    return 0;
}

// probe函数——驱动入口
static int my_sensor_probe(struct i2c_client *client, 
                           const struct i2c_device_id *id)
{
    struct my_sensor_data *data;
    u8 chip_id;
    int ret;

    dev_info(&client->dev, "设备探测开始\n");

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

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

    // 2. 读取芯片ID验证
    ret = my_sensor_read_reg(client, 0x0F, &chip_id);
    if (ret < 0) {
        dev_err(&client->dev, "读取芯片ID失败\n");
        return ret;
    }

    // 3. 初始化设备
    my_sensor_write_reg(client, 0x10, 0x01); // 软复位
    msleep(10);
    my_sensor_write_reg(client, 0x11, 0x00); // 配置模式

    dev_info(&client->dev, "设备初始化完成,芯片ID: 0x%02x\n", chip_id);
    return 0;
}

// remove函数——驱动退出
static int my_sensor_remove(struct i2c_client *client)
{
    dev_info(&client->dev, "设备移除\n");
    // 关闭设备、释放中断等
    return 0;
}

// 设备ID表——匹配硬件
static const struct i2c_device_id my_sensor_id[] = {
    { "my_sensor", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, my_sensor_id);

// 设备树匹配表
static const struct of_device_id my_sensor_of_match[] = {
    { .compatible = "vendor,my-sensor" },
    { }
};
MODULE_DEVICE_TABLE(of, my_sensor_of_match);

// 驱动结构体
static struct i2c_driver my_sensor_driver = {
    .driver = {
        .name = "my_sensor",
        .of_match_table = my_sensor_of_match,
    },
    .probe = my_sensor_probe,
    .remove = my_sensor_remove,
    .id_table = my_sensor_id,
};

module_i2c_driver(my_sensor_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("My I2C Sensor Driver");

3.2 设备树配置

别忘了在设备树里注册你的设备:

&i2c1 {
    status = "okay";
    clock-frequency = <100000>;  // 100kHz标准模式

    my_sensor@48 {
        compatible = "vendor,my-sensor";
        reg = <0x48>;           // 7位从设备地址
        interrupt-parent = <&gpio1>;
        interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
    };
};

避坑指南:我曾经在设备树里把reg写成了8位地址(0x90),结果内核怎么都匹配不上设备。查了两天才发现——设备树的reg必须是7位地址!记住,是7位,不是8位。

3.3 调试技巧

调试I2C驱动时,我一般用这几招:

  • i2cdetect——先看看总线上有哪些设备,地址对不对
  • i2cget/i2cset——手动读写寄存器,验证硬件是否正常
  • 逻辑分析仪——抓波形,看时序对不对。我有个USB逻辑分析仪,才几十块钱,但救过我无数次

嗯,这一讲的内容就到这儿。I2C子系统其实不难,关键是理解那三个核心结构体,然后照着模板写驱动。下一讲我们会深入HAL层,看看怎么把驱动数据往上传递到Android框架层。

总结一下:I2C总线协议就是两根线、一个起始条件、一个停止条件、中间传地址和数据。Linux内核用i2c_adapter、i2c_client、i2c_driver三个结构体把整个子系统封装得干干净净。写驱动时,照着模板填probe和remove函数就行,别忘了用i2c_transfer做组合传输。

可可精品资料库