3、I2C通信基础:I2C协议时序、主从模式、速率选择、嵌入式I2C驱动框架

好,咱们今天聊聊I2C。这个总线在嵌入式系统里太常见了,传感器、EEPROM、ADC、DAC,几乎每个板子上都能找到它的身影。我个人觉得,搞懂I2C是驱动开发的基本功,就像学武术得先扎马步一样。

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

I2C只用两根线:SCL(时钟线)和SDA(数据线)。说白了,就是靠这两根线的电平变化来传递信息。你想想看,两根线要完成地址发送、数据读写、应答确认这么多事,时序上肯定有讲究。

空闲状态:SCL和SDA都是高电平。这时候总线是空闲的,谁都可以发起通信。

起始信号:SCL为高电平时,SDA从高电平跳变到低电平。这个下降沿告诉所有从设备:“注意了,我要开始说话了!”

停止信号:SCL为高电平时,SDA从低电平跳变到高电平。这个上升沿表示:“我说完了,你们可以歇着了。”

数据传输:数据在SCL的上升沿被采样。也就是说,发送方必须在SCL低电平时改变SDA的状态,然后在SCL高电平时保持稳定。每个时钟周期传输一个bit,8个bit组成一个字节。

应答机制:每发送完一个字节,接收方要拉低SDA表示“收到了”。如果接收方没拉低,发送方就知道出问题了。我在项目中遇到过,有时候从设备没上电或者地址不对,就会一直发NACK,这时候就得检查硬件连接了。

关键时序参数

  • SCL时钟频率:标准模式100kHz,快速模式400kHz,高速模式3.4MHz
  • 上升时间:标准模式不超过1000ns,快速模式不超过300ns
  • 保持时间:起始信号后SCL低电平至少4.7μs(标准模式)

3.2 主从模式:谁说了算?

I2C总线上的设备分为主设备和从设备。主设备负责产生时钟、发起通信、决定什么时候开始什么时候结束。从设备只能被动响应,不能主动说话。

一个总线上可以有多个主设备,但同一时刻只能有一个主设备控制总线。这就涉及到仲裁机制了——如果两个主设备同时发起通信,谁先拉低SDA谁就赢。嗯,这里要注意,仲裁过程中不会丢失数据,这是I2C协议设计得很巧妙的地方。

从设备地址:每个从设备都有一个7位或10位的地址。主设备在起始信号后先发送地址字节,其中前7位是地址,第8位是读写位(0表示写,1表示读)。

我曾经调试过一个项目,两个传感器用了相同的I2C地址,结果读回来的数据全是乱的。后来查手册才发现,其中一个传感器的地址引脚可以配置,改一下硬件就解决了。所以啊,画原理图的时候就要规划好地址分配。

地址类型 地址范围 使用场景
7位地址 0x00 - 0x7F 大多数I2C设备
10位地址 0x00 - 0x3FF 地址资源紧张时使用
保留地址 0x00, 0x01, 0x7F等 特殊用途,用户不可用

3.3 速率选择:不是越快越好

I2C支持多种速率模式,常见的有:

  • 标准模式:100kHz,最常用,兼容性最好
  • 快速模式:400kHz,大多数现代设备都支持
  • 快速模式+:1MHz,需要更强的上拉驱动
  • 高速模式:3.4MHz,对硬件要求高

我个人习惯先用100kHz调试,等通信稳定了再慢慢提高频率。为什么?因为高频下信号质量容易出问题,尤其是线缆比较长或者上拉电阻没选对的时候。

上拉电阻选择经验

总线电容越大,上拉电阻就要越小。一般100kHz用4.7kΩ,400kHz用2.2kΩ。如果总线上的设备很多,可以适当减小电阻值。我曾经在一个项目里用了10个I2C设备,总线电容太大,400kHz死活跑不起来,换成2.2kΩ就好了。

3.4 嵌入式I2C驱动框架:从底层到应用

在嵌入式Linux里,I2C驱动分为三个层次:

  1. I2C核心层:提供统一的接口,管理总线、设备和驱动
  2. I2C总线驱动:操作具体的I2C控制器硬件,处理时序
  3. I2C设备驱动:针对具体设备,比如温度传感器、EEPROM

写驱动的时候,我们主要关注设备驱动层。下面是一个简单的读写示例:

// I2C读操作示例
int i2c_read(struct i2c_client *client, u8 reg, u8 *buf, int len)
{
    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 = len;
    msg[1].buf = buf;

    ret = i2c_transfer(client->adapter, msg, 2);
    if (ret != 2) {
        dev_err(&client->dev, "I2C read failed\n");
        return -EIO;
    }
    return 0;
}

这个函数先发送寄存器地址,然后读取数据。注意看,这里用了两个消息(msg),第一个是写地址,第二个是读数据。I2C协议支持这种“组合传输”,非常实用。

避坑指南

我曾经在写驱动时忘了加延时,结果连续读写时数据对不上。后来发现,有些从设备在两次操作之间需要一点时间来处理内部状态。解决办法是在每次传输之间加一个usleep_range(100, 200),给设备一点喘息的时间。

在设备树中,I2C设备的描述也很简单:

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

    temperature_sensor: tmp102@48 {
        compatible = "ti,tmp102";
        reg = <0x48>;
    };
};

这里指定了I2C控制器的频率,以及从设备的地址和兼容性字符串。驱动匹配的时候,就是靠这个compatible来找到对应的驱动。

好了,I2C的基础就聊到这儿。下一章咱们会深入I2C驱动的实际开发,包括中断处理、DMA传输、以及一些高级技巧。记住,搞I2C驱动,时序是根本,地址要核对,速率别贪快。把这些基础打牢了,后面写什么驱动都不怕。