第1章:I2C协议基础
各位同学好,我是你们这堂课的主讲人。做了十几年MEMS传感器开发,我调试过的I2C设备少说也有上百种了。今天咱们就来聊聊这个看似简单、实则暗藏玄机的I2C协议。
说实话,I2C协议在MEMS传感器领域太常见了。你随便拿个加速度计、陀螺仪、气压计,十有八九都支持I2C接口。为什么?因为它只需要两根线,省引脚啊!我当年做第一块传感器板子时,就因为I2C时序没调好,折腾了整整三天。嗯,今天我就把这些经验教训都分享给你们。
1.1 I2C总线物理层
先看物理层。I2C总线就两根线:SCL(时钟线)和SDA(数据线)。这两根线都是开漏输出,需要外接上拉电阻。
关键参数速查表:
| 参数 | 标准模式 | 快速模式 | 高速模式 |
|---|---|---|---|
| 最大时钟频率 | 100 kHz | 400 kHz | 3.4 MHz |
| 上拉电阻典型值 | 4.7 kΩ | 2.2 kΩ | 1.0 kΩ |
| 最大总线电容 | 400 pF | 400 pF | 100 pF |
| 最大从机数量 | 127 | 127 | 127 |
这里有个坑,我必须要提醒你们。上拉电阻不是随便选的!电阻太大,信号上升沿会变慢;电阻太小,功耗又上去了。我有个项目,传感器离MCU有20厘米远,用了10kΩ上拉,结果400kHz下波形都成三角波了。后来换成2.2kΩ,问题解决。
避坑指南:我曾经在一条总线上挂了8个传感器,总电容超过400pF,结果通信时好时坏。后来加了I2C总线缓冲器才搞定。记住:总线电容是隐形杀手!
1.2 I2C协议帧格式
I2C的通信帧格式,说白了就是一套握手规则。我习惯把它拆成几个基本单元来看:
- 起始条件(S):SCL高电平时,SDA从高变低
- 从机地址:7位或10位地址,加上读写位
- 应答位(ACK):接收方拉低SDA表示收到
- 数据字节:每次8位,MSB先发
- 停止条件(P):SCL高电平时,SDA从低变高
你想想看,这个协议设计得挺巧妙的。起始和停止条件都是SCL高电平时SDA的变化,而数据传输时SDA只能在SCL低电平时变化。这样就不会混淆了。
个人经验:我调试时最喜欢用逻辑分析仪抓起始和停止条件。如果这两个信号不对,后面的数据全是白搭。有一次我发现从机不响应,抓波形一看,原来是起始条件保持时间不够,从机根本没识别到。
1.3 I2C时序分析
时序这东西,光看数据手册容易晕。我给大家画个图,一看就明白。
这张图展示了I2C一次完整的数据传输过程。从起始条件开始,到停止条件结束。中间是地址、读写位、应答和数据。
我给大家几个关键时序参数,做项目时一定要核对:
- tHD:STA - 起始条件保持时间,至少4.7μs(标准模式)
- tSU:STO - 停止条件建立时间,至少4.7μs
- tHIGH - SCL高电平时间,至少4.0μs
- tLOW - SCL低电平时间,至少4.7μs
- tR/tF - 上升/下降时间,最大1000ns
实战经验:我遇到过最奇葩的问题,是某款传感器的数据手册里写的时序参数和实际不符。它说支持400kHz,但实际测量发现tHD:STA只有1μs。后来我只好把时钟降到200kHz才稳定。所以,拿到新传感器,第一件事就是用逻辑分析仪抓一下实际波形!
1.4 I2C地址与读写操作
I2C地址分7位和10位两种。MEMS传感器几乎都用7位地址。每个从机设备都有一个固定地址,通常是7位,比如0x68、0x76之类的。
读写操作其实很简单:
- 写操作:主机发送从机地址 + 写位(0),然后发送寄存器地址,再发送数据
- 读操作:主机先发送从机地址 + 写位(0),发送寄存器地址,然后重新发送起始条件,再发送从机地址 + 读位(1),最后读取数据
这里有个细节,很多新手会搞混。读操作时,那个「重新发送起始条件」叫重复起始条件(Repeated Start),不是停止条件加起始条件。为什么要这么设计?说白了就是为了原子操作,防止其他主机在中间插一脚。
我的习惯:写代码时,我会把I2C读写封装成两个函数:i2c_write_reg()和i2c_read_reg()。这样上层代码就干净了。但要注意,有些传感器支持多字节读写,这时候要处理好地址自增逻辑。
来看一个实际的代码示例,这是我从某款气压传感器驱动里摘出来的:
// I2C写寄存器函数
int i2c_write_reg(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t len)
{
// 发送起始条件
i2c_start();
// 发送设备地址 + 写位
if (i2c_send_byte(dev_addr << 1 | 0) != ACK) {
i2c_stop();
return -1; // 设备无应答
}
// 发送寄存器地址
if (i2c_send_byte(reg_addr) != ACK) {
i2c_stop();
return -2;
}
// 发送数据
for (uint16_t i = 0; i < len; i++) {
if (i2c_send_byte(data[i]) != ACK) {
i2c_stop();
return -3;
}
}
// 发送停止条件
i2c_stop();
return 0;
}
重要提醒:我曾经犯过一个低级错误,把设备地址左移了一位忘了。I2C的7位地址在发送时是左移1位,最低位是读写位。比如设备地址0x76,发送时要变成0xEC(写)或0xED(读)。这个坑,我至少见过5个工程师踩过。
最后说一句,I2C协议虽然简单,但实际项目中坑不少。我建议大家:
- 上拉电阻别省,该用2.2kΩ就别用10kΩ
- 总线电容要控制,别超过400pF
- 时序参数要核对,别信数据手册100%
- 逻辑分析仪是好朋友,调试时多用用
好了,I2C协议基础就讲到这里。下一章我们聊聊SPI协议,那个速度更快,但线也更多。嗯,到时候再细说。