3、I2C总线基础:I2C协议时序、Linux I2C子系统架构、I2C设备驱动编写模板

各位同学,咱们今天聊聊I2C总线。说实话,在触控驱动这个领域,I2C是绝对绕不开的。你想想看,市面上90%以上的触控芯片,跟主控之间的通信接口都是I2C。我最早接触I2C是在做一款电容式触摸屏驱动的时候,当时被时序问题折腾得够呛——嗯,今天就把这些经验都抖出来。

3.1 I2C协议时序:从物理层说起

I2C总线就两根线:SCL(时钟线)和SDA(数据线)。别小看这两根线,里面的门道可不少。我个人的习惯是,先把时序图画在纸上,再动手写代码。

核心时序要素:

  • 起始条件(START):SCL高电平时,SDA从高变低
  • 停止条件(STOP):SCL高电平时,SDA从低变高
  • 数据采样:SCL低电平时改变SDA,高电平时采样数据
  • 应答位(ACK):每8位数据后,接收方拉低SDA表示应答

说白了,I2C的通信就像两个人对话。主设备说“我要开始说话了”(START),然后报上自己的地址和操作类型,从设备听到是自己的地址就回一声“嗯”(ACK),然后开始传数据。传完了说“我说完了”(STOP)。

我曾经在一个项目中遇到过时序问题:触控芯片偶尔不响应。查了半天,发现是SCL频率设置得太高,芯片跟不上。后来把频率从400kHz降到100kHz,问题就解决了。所以啊,别一味追求高速,稳定才是王道。

3.2 Linux I2C子系统架构

Linux内核的I2C子系统分三层,我画个图给你看:

层级 组件 职责
应用层 用户空间程序 通过设备节点(/dev/i2c-X)访问
核心层 i2c-core 总线管理、设备注册、驱动匹配
适配器层 i2c-adapter 硬件相关的收发实现

这里有个关键点:i2c-core 负责把设备和驱动配对。你写一个I2C设备驱动,本质上就是实现一个 i2c_driver 结构体,然后告诉内核:“嘿,我支持这个厂商ID和设备ID的设备。”

我的经验:调试I2C驱动时,先用 i2cdetect 工具扫描总线,确认设备地址是否正确。我见过太多人一上来就写驱动,结果发现设备地址都写错了。

3.3 I2C设备驱动编写模板

好,咱们直接上代码。这是我多年总结出来的模板,你拿去改改就能用:

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

/* 设备结构体 */
struct my_touch_device {
    struct i2c_client *client;
    int irq;
};

/* 读寄存器函数 */
static int my_touch_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 = &reg;

    /* 读数据 */
    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, "I2C read failed\n");
        return -EIO;
    }
    return 0;
}

/* probe函数 */
static int my_touch_probe(struct i2c_client *client,
                          const struct i2c_device_id *id)
{
    struct my_touch_device *touch;
    u8 chip_id;

    /* 分配设备结构体 */
    touch = devm_kzalloc(&client->dev, sizeof(*touch), GFP_KERNEL);
    if (!touch)
        return -ENOMEM;

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

    /* 读取芯片ID确认设备存在 */
    my_touch_read_reg(client, 0x00, &chip_id);
    dev_info(&client->dev, "Chip ID: 0x%02x\n", chip_id);

    /* 注册中断等操作... */
    return 0;
}

/* 设备ID表 */
static const struct i2c_device_id my_touch_id[] = {
    { "my_touch", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, my_touch_id);

/* 设备树匹配表 */
static const struct of_device_id my_touch_of_match[] = {
    { .compatible = "mycompany,my-touch" },
    { }
};
MODULE_DEVICE_TABLE(of, my_touch_of_match);

/* I2C驱动结构体 */
static struct i2c_driver my_touch_driver = {
    .driver = {
        .name = "my_touch",
        .of_match_table = my_touch_of_match,
    },
    .probe = my_touch_probe,
    .id_table = my_touch_id,
};

module_i2c_driver(my_touch_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");

避坑指南:我曾经犯过一个低级错误——在probe函数里用了 msleep()。要知道,probe函数是在原子上下文调用的,不能睡眠。后来改用 mdelay() 才解决问题。记住:probe里别睡觉

3.4 设备树配置

现在的嵌入式平台都用设备树来描述硬件。你的I2C设备在设备树里长这样:

&i2c1 {
    status = "okay";
    clock-frequency = <100000>;  /* 100kHz */

    touch@38 {
        compatible = "mycompany,my-touch";
        reg = <0x38>;           /* I2C地址 */
        interrupt-parent = <&gpio1>;
        interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
        reset-gpios = <&gpio2 10 GPIO_ACTIVE_LOW>;
    };
};

这里要注意 reg 字段,它指定了I2C设备的从机地址。我见过有人把地址写成了7位格式,但内核用的是8位格式(左移一位),结果设备死活找不到。其实内核会自动处理这个转换,你只要写7位地址就行。

3.5 调试技巧

最后分享几个调试技巧:

  1. 用逻辑分析仪抓波形:我习惯用Saleae,便宜又好用。看START、STOP、ACK信号对不对,一目了然。
  2. 内核调试开关:打开 CONFIG_I2C_DEBUG_BUSCONFIG_I2C_DEBUG_CORE,内核会打印详细的I2C通信日志。
  3. 模拟I2C:如果硬件I2C控制器有问题,可以用GPIO模拟I2C时序。虽然慢,但调试起来方便。

我的习惯:每次写I2C驱动前,先用 i2cgeti2cset 工具手动读写几个寄存器,确认硬件通路没问题。这一步能省下大量调试时间。

好了,I2C的基础就讲到这里。下一章咱们会深入触控芯片的通信协议,到时候你会看到这些基础知识的实际应用。记住:I2C看似简单,但细节决定成败。多动手,多抓波形,你很快就能成为I2C高手。