第1章:I2C协议详解
各位同学,今天我们来聊聊I2C协议。说实话,I2C是我在血氧仪项目里打交道最多的协议之一。传感器读取、屏幕配置、甚至有些AFE芯片的寄存器设置,都离不开它。你想想看,一个血氧仪里可能同时挂着三四颗I2C设备,搞不定协议细节,调试起来真的会让人抓狂。
1.1 I2C总线物理层
I2C总线就两根线:SCL(时钟线)和SDA(数据线)。嗯,就这么简单。但简单背后有门道。
这两根线都是开漏输出,必须接上拉电阻。我刚开始做项目时,有次忘了接上拉,结果总线一直低电平,设备死活不响应。查了半天才发现是电阻没焊。所以,上拉电阻是必须的,不是可选项。
电阻值怎么选?一般来说,4.7kΩ是通用值。但如果你总线上的设备多,或者走线长,可能需要换成2.2kΩ甚至1kΩ。我在一个血氧仪项目里,因为传感器离主控有10cm远,标准4.7kΩ拉不动,换成2.2kΩ才稳定。这个坑我踩过,你们记住就好。
关键参数速查表:
| 参数 | 标准模式 | 快速模式 | 高速模式 |
|---|---|---|---|
| 最大速率 | 100kHz | 400kHz | 3.4MHz |
| 上拉电阻 | 4.7kΩ | 2.2kΩ | 1kΩ |
| 最大总线电容 | 400pF | 400pF | 100pF |
总线电容这个参数容易被忽略。线越长、设备越多,电容越大。电容大了,信号上升沿就变缓,容易误判。我建议,如果总线上挂超过4个设备,最好用示波器看看波形,别光靠猜。
1.2 I2C协议时序
时序是I2C的灵魂。说白了,就是SCL和SDA怎么配合,才能把数据传出去。
起始条件(START): SCL高电平时,SDA从高变低。这个动作告诉所有设备:准备,要开始通信了。
停止条件(STOP): SCL高电平时,SDA从低变高。这个动作告诉所有设备:完事,可以休息了。
数据传输: SCL低电平时,SDA可以变化。SCL高电平时,SDA必须稳定。每个SCL脉冲传1位数据,8个脉冲传1个字节。
我记得有一次调试血氧仪,发现传感器偶尔不响应。用逻辑分析仪抓波形,发现起始条件之后SDA没拉低。原来是主控的GPIO配置错了,开漏输出没设好。这种问题,光看代码是看不出来的,必须看波形。
我的调试习惯: 每次新板子回来,第一件事就是用逻辑分析仪抓I2C波形。确认起始、停止、数据位都对,再开始写驱动。这能省下至少半天调试时间。
1.3 I2C地址与读写操作
每个I2C设备都有一个7位或10位地址。血氧仪里常用的传感器,比如MAX30102,地址是0x57(7位)。但注意,有些芯片的地址可以配置,通过拉高或拉低某个引脚来改变。
读写操作其实很简单:
- 写操作: 主设备发送起始条件 → 发送设备地址+写位(0) → 等待从设备应答 → 发送寄存器地址 → 等待应答 → 发送数据 → 等待应答 → 发送停止条件
- 读操作: 主设备发送起始条件 → 发送设备地址+写位(0) → 等待应答 → 发送寄存器地址 → 等待应答 → 发送重复起始条件 → 发送设备地址+读位(1) → 等待应答 → 读取数据 → 主设备发送非应答 → 发送停止条件
这里有个容易搞混的地方:设备地址是7位,但发送时左移1位,最低位是读写位。比如地址0x57,左移后是0xAE(写)或0xAF(读)。我见过不少新手直接拿0x57去发,结果设备不响应。嗯,这个细节一定要注意。
// 伪代码示例:I2C写寄存器
uint8_t i2c_write_reg(uint8_t dev_addr, uint8_t reg_addr, uint8_t data) {
i2c_start(); // 发送起始条件
i2c_send_byte(dev_addr << 1); // 发送设备地址+写位
if (!i2c_wait_ack()) return 1; // 等待应答
i2c_send_byte(reg_addr); // 发送寄存器地址
if (!i2c_wait_ack()) return 1;
i2c_send_byte(data); // 发送数据
if (!i2c_wait_ack()) return 1;
i2c_stop(); // 发送停止条件
return 0;
}
1.4 I2C仲裁与时钟拉伸
这两个概念,很多教程一笔带过。但实际项目中,它们经常是问题的根源。
仲裁: 当多个主设备同时尝试控制总线时,谁先拉低SDA谁就赢。输的那个自动退出。说白了,就是总线上的"抢话"机制。我在一个多主控的项目里遇到过,两个MCU同时去读同一个传感器,结果数据全乱了。后来加了互斥锁才解决。
时钟拉伸: 从设备可以拉低SCL,告诉主设备"慢点,我还没准备好"。主设备检测到SCL被拉低,就得等着,直到从设备释放SCL。血氧仪里的AFE芯片有时会用到这个功能,特别是在处理数据转换时。
避坑指南: 我曾经在一个项目里,从设备时钟拉伸时间过长,超过了主控的超时阈值,导致通信失败。解决办法是:要么延长主控的超时时间,要么检查从设备为什么拉这么久。如果是硬件问题,比如电源不稳,那就要先解决电源。
时钟拉伸在标准模式下很常见,但快速模式和高速模式下,有些从设备不支持。选型时一定要看数据手册,别想当然。
小结
I2C协议看起来简单,但坑不少。物理层的上拉电阻、时序里的起始停止条件、地址的移位操作、仲裁和时钟拉伸的异常处理,每一个都可能让你调试到怀疑人生。我个人建议,先把协议吃透,再动手写驱动。磨刀不误砍柴工,这话在嵌入式开发里特别适用。
下一章,我们会讲SPI协议。到时候你会发现,SPI和I2C的思路完全不同。做好准备吧。