第2章:I2C协议详解与实战
各位同学,今天我们来啃一块硬骨头——I2C协议。说实话,I2C这玩意儿在嵌入式开发里太常见了,智能手表里的加速度计、陀螺仪、心率传感器,十有八九都是挂I2C总线上的。我当年刚入行时,被I2C的时序折腾得够呛,逻辑分析仪一接上去,波形乱七八糟的。后来才明白,搞懂物理层和协议层,是写好驱动的基础。
2.1 I2C物理层与协议层
I2C总线就两根线:SCL(时钟线)和SDA(数据线)。所有设备都挂在这两根线上,主设备负责产生时钟,从设备响应数据。说白了,这就是一个主从通信的架构。
物理层要点:
- SCL和SDA都是开漏输出,需要上拉电阻(典型值4.7kΩ)
- 总线空闲时,两根线都是高电平
- 每个从设备有唯一7位或10位地址
- 标准模式100kHz,快速模式400kHz,高速模式3.4MHz
我在项目中遇到过一个问题:手表上同时挂了三个I2C传感器,结果通信老是出错。后来用示波器一量,发现上拉电阻选得太大了,信号上升沿太慢。换成2.2kΩ的电阻,问题就解决了。嗯,这里要注意,上拉电阻的选择跟总线电容有关,不是随便选的。
2.2 I2C时序分析
I2C的时序其实不复杂,但细节很多。我们一个一个来看。
起始条件(START): SCL高电平时,SDA从高变低。这个信号告诉所有从设备:准备接收数据。
停止条件(STOP): SCL高电平时,SDA从低变高。这个信号表示传输结束。
数据传输: 每个数据字节8位,MSB先传。SCL低电平时改变SDA,SCL高电平时采样SDA。你想想看,为什么这样设计?就是为了保证数据在时钟高电平时是稳定的。
应答位(ACK): 每发送完一个字节,接收方要在第9个时钟周期拉低SDA,表示收到了。如果没拉低,就是NACK,发送方就知道出问题了。
关键时序参数:
| 参数 | 标准模式 | 快速模式 |
|---|---|---|
| SCL频率 | 100kHz | 400kHz |
| 上升时间 | ≤1000ns | ≤300ns |
| 下降时间 | ≤300ns | ≤300ns |
| 数据保持时间 | ≥0ns | ≥0ns |
我曾经调试一个心率传感器,读出来的数据总是跳变。用逻辑分析仪抓波形,发现SCL的上升沿太缓了,超过了300ns。查了半天,原来是PCB走线太长,寄生电容太大。后来把走线缩短,问题就解决了。所以啊,时序参数不是纸上谈兵,实际项目中一定要验证。
2.3 在RTOS中编写I2C主机驱动
在RTOS里写I2C驱动,跟裸机最大的区别就是:要考虑任务调度和互斥访问。我习惯把I2C操作封装成一个驱动层,上层应用只管调用API,不用关心底层细节。
先看一个简单的I2C读写函数:
// I2C主机写操作
int i2c_master_write(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t len)
{
// 1. 发送起始条件
i2c_start();
// 2. 发送设备地址 + 写位
i2c_send_byte(dev_addr << 1 | 0);
if (i2c_wait_ack() != 0) {
i2c_stop();
return -1;
}
// 3. 发送寄存器地址
i2c_send_byte(reg_addr);
if (i2c_wait_ack() != 0) {
i2c_stop();
return -2;
}
// 4. 发送数据
for (int i = 0; i < len; i++) {
i2c_send_byte(data[i]);
if (i2c_wait_ack() != 0) {
i2c_stop();
return -3;
}
}
// 5. 发送停止条件
i2c_stop();
return 0;
}
在RTOS里,我建议加上互斥锁保护。因为多个任务可能同时访问I2C总线,不加锁的话,数据就乱套了。
我的经验: 在FreeRTOS里,用二进制信号量保护I2C总线。每个I2C操作前获取信号量,操作完后释放。这样既保证了互斥,又不会造成死锁。
// RTOS环境下的I2C写操作
int i2c_master_write_rtos(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t len)
{
int ret;
// 获取I2C总线信号量
if (xSemaphoreTake(i2c_semaphore, pdMS_TO_TICKS(100)) != pdTRUE) {
return -TIMEOUT;
}
// 执行I2C操作
ret = i2c_master_write(dev_addr, reg_addr, data, len);
// 释放信号量
xSemaphoreGive(i2c_semaphore);
return ret;
}
你可能会问:为什么不用互斥量?其实都可以,但我个人习惯用二进制信号量,因为它的语义更清晰——谁拿到谁用,用完就还回来。
2.4 使用逻辑分析仪调试I2C总线
调试I2C总线,逻辑分析仪是神器。我刚开始做驱动时,全靠printf打印调试信息,效率低得可怜。后来学会了用逻辑分析仪,问题定位快多了。
调试步骤:
- 把逻辑分析仪的通道0接SCL,通道1接SDA,共地
- 设置采样率,至少是I2C频率的4倍(我一般设2MHz)
- 触发条件设为SCL下降沿,这样能抓到完整的通信过程
- 抓取波形后,用协议解析功能自动解码
注意: 逻辑分析仪的输入电压范围要跟I2C总线匹配。3.3V的系统就用3.3V的逻辑分析仪,别用5V的,会烧芯片。我曾经就干过这种事,还好只是烧了一个传感器,不然后果严重。
看波形时,我主要关注几个点:
- 起始条件是否正确?SCL高电平时SDA有没有正确跳变?
- 地址字节对不对?7位地址有没有左移一位?
- 应答位有没有拉低?如果从设备没应答,可能是地址错了或者设备没上电
- 数据字节的时序对不对?SCL低电平时SDA有没有稳定?
我记得有一次调试一个气压传感器,读出来的数据全是0xFF。用逻辑分析仪一看,发现从设备在应答位之后没有释放SDA总线,导致主设备无法继续发送时钟。后来查手册,发现这个传感器需要额外的延时才能准备好数据。加上一个10ms的延时后,问题就解决了。
嗯,说到延时,这里有个小技巧:在RTOS里不要用忙等延时,用vTaskDelay()让出CPU。不然的话,你的系统响应会变得很慢。
好了,I2C协议的内容就讲到这里。下一章我们讲SPI协议,那个速度更快,但线也更多。到时候我们再细聊。