I2C协议基础(上):物理层与协议层核心

各位同学,今天我们来聊聊I2C通信的基础。说实话,很多初学者一上来就写代码,结果OLED死活不亮,最后发现是I2C时序出了问题。我个人习惯是,先把物理层和协议层搞明白,再动手写驱动。这样出了问题,你才知道往哪个方向查。

一、I2C总线物理层特性

I2C总线只有两根线:SDA(数据线)和SCL(时钟线)。别看线少,里面的门道可不少。

1. SDA与SCL:开漏输出与线与逻辑

I2C总线的SDA和SCL都是开漏输出结构。什么叫开漏?说白了,就是引脚只能拉低到GND,不能主动输出高电平。高电平靠外部上拉电阻提供。

这种设计有个好处:多个设备可以共用一根线,谁想拉低就拉低,不会短路。这就是所谓的“线与”逻辑——只要有一个设备拉低,总线就是低电平。

我的经验:调试时用示波器看SDA/SCL波形,如果发现高电平电压不够(比如只有2V而不是3.3V),十有八九是上拉电阻太大或者总线电容太大。我曾经在一个项目里挂接了8个I2C设备,上拉电阻从4.7kΩ换到2.2kΩ才解决问题。

2. 上拉电阻:选多大才合适?

上拉电阻的取值直接影响通信质量和功耗。选小了,功耗大;选大了,上升沿变缓,高速通信容易出错。

总线电容 推荐上拉电阻 适用场景
< 100 pF 4.7 kΩ ~ 10 kΩ 低速、短距离、设备少
100 pF ~ 400 pF 2.2 kΩ ~ 4.7 kΩ 标准模式(100 kHz)
> 400 pF 1 kΩ ~ 2.2 kΩ 快速模式(400 kHz)

STM32F103的I2C外设,标准模式用4.7kΩ基本没问题。但如果你像我一样,喜欢把I2C频率调到400kHz,建议换成2.2kΩ。嗯,这里要注意:上拉电阻不是越小越好,太小的电阻会让总线拉低时的电流过大,可能损坏引脚。

避坑指南:我曾经在开发板上用10kΩ上拉电阻跑400kHz,OLED显示偶尔花屏。查了半天,发现是上升沿太缓,SCL高电平时间不够。换成2.2kΩ后,问题消失。所以,上拉电阻要根据实际总线电容和通信速率来选,别偷懒。

二、I2C协议层:通信的“交通规则”

物理层搞定了,接下来看协议层。I2C协议定义了设备之间怎么“说话”。我把它拆成四个核心要素:起始条件、停止条件、数据有效性、应答机制。

I2C协议层核心流程 起始条件 SCL高时SDA下降 地址 + R/W 7位地址 + 1位方向 数据字节 8位数据,MSB先发 应答位 (ACK) 第9个SCL时钟 停止条件 SCL高时SDA上升 每个字节后必须跟一个应答位,否则通信会终止

1. 起始条件(Start Condition)

SCL为高电平时,SDA从高电平跳变到低电平。这就是起始信号。主设备发出这个信号后,总线上的所有从设备都知道:要开始通信了,准备好接收地址。

2. 停止条件(Stop Condition)

SCL为高电平时,SDA从低电平跳变到高电平。这就是停止信号。主设备发出这个信号,表示通信结束,总线释放。

关键点:起始和停止条件都是由主设备产生的。从设备不能主动发起或终止通信。我见过有人写代码时忘了发停止条件,结果总线一直被占用,其他设备无法通信。

3. 数据有效性

I2C协议规定:SCL为高电平时,SDA上的数据必须稳定。数据只能在SCL为低电平时改变。你想想看,如果SCL高电平时SDA变了,那就会被误判为起始或停止条件。

每个数据字节是8位,从最高位(MSB)开始传输。发送完8位数据后,第9个SCL时钟用于应答位。

4. 应答机制(ACK/NACK)

这是I2C协议里最容易出问题的地方。每发送一个字节,接收方必须在第9个时钟周期拉低SDA,表示“收到了”。如果接收方不拉低(SDA保持高电平),那就是NACK,表示“没收到”或“不想收”。

应答类型 SDA状态(第9个SCL) 含义
ACK 低电平 字节接收成功,继续发送
NACK 高电平 接收失败或传输结束
避坑指南:我曾经调试一个温湿度传感器,读数据时总是卡在第一个字节。用逻辑分析仪一看,从设备发了NACK。查手册才发现,这个传感器在两次读取之间需要至少100ms的间隔。我连续读取太快,它来不及准备数据,就回了NACK。所以,遇到NACK别慌,先看看是不是时序问题。

三、I2C通信的完整流程

把上面四个要素串起来,一次完整的I2C通信流程是这样的:

  1. 主设备发出起始条件:总线被占用
  2. 主设备发送7位从设备地址 + 1位读写位(0=写,1=读)
  3. 从设备应答ACK:表示“我在,我可以通信”
  4. 主设备发送/接收数据字节,每个字节后跟一个应答位
  5. 主设备发出停止条件:总线释放

以OLED驱动为例,写一个字节到SSD1306控制器的流程:

// 伪代码:I2C写一个字节到OLED
I2C_Start();                    // 1. 起始条件
I2C_SendByte(0x78 << 1 | 0);   // 2. 地址0x3C + 写位(0)
I2C_WaitAck();                  // 3. 等待ACK
I2C_SendByte(0x00);             // 4. 发送控制字节(命令模式)
I2C_WaitAck();                  // 5. 等待ACK
I2C_SendByte(0xAF);             // 6. 发送命令(开显示)
I2C_WaitAck();                  // 7. 等待ACK
I2C_Stop();                     // 8. 停止条件

我的习惯:写I2C驱动时,我会在每步后面加一个超时判断。比如等待ACK时,如果超过一定时间没收到,就主动发停止条件并报错。这样即使从设备死机,主设备也不会卡死。这个习惯救过我很多次。

四、常见故障与定位思路

学完理论,咱们看看实际调试中会遇到哪些坑。我整理了一个表格,方便你对照排查:

现象 可能原因 排查方法
SCL/SDA一直为高 上拉电阻缺失或虚焊 万用表测电阻两端电压
起始条件后无ACK 地址错误或从设备未上电 核对地址,检查电源
数据传一半卡住 时钟拉伸或从设备忙 示波器看SCL是否被拉低
显示花屏/乱码 时序太快或上拉电阻不合适 降低I2C频率,调整上拉