4. I2C总线通信:从协议到实战
I2C总线,说实话,是嵌入式开发里最常用的串行通信协议之一。两根线,挂一堆设备,简单又灵活。我最早接触I2C是在一个传感器采集项目上,当时被时钟延展和应答位折腾得够呛。今天咱们就把I2C协议掰开揉碎,再结合ESP-IDF的驱动框架,手把手读写EEPROM。
4.1 I2C协议核心要点
I2C只有两条线:SCL(时钟线)和SDA(数据线)。所有设备都挂在这两根线上,通过地址区分。主设备发起通信,从设备响应。
关键特性速览
- 两线制:SCL + SDA,半双工
- 多主多从:支持总线仲裁
- 7位/10位地址:常用7位,最多127个设备
- 标准速率:100kHz(标准模式)、400kHz(快速模式)
- 开漏输出:需要上拉电阻,典型值4.7kΩ
4.2 通信时序详解
I2C的时序其实不复杂,但有几个关键点必须吃透。我画了一张流程图,把一次完整的读写过程串起来。
嗯,这里要注意:起始条件和停止条件都是由主设备控制的。SCL为高时,SDA从高变低是起始,从低变高是停止。这个时序必须严格,否则从设备会懵。
4.3 应答机制与时钟延展
I2C的应答位很有意思。每发送一个字节(8位),接收方必须在第9个时钟周期拉低SDA表示应答(ACK),否则就是非应答(NACK)。
避坑指南
我曾经在一个项目里,从设备因为忙没来得及拉低SDA,主设备没等到ACK就直接停了总线。结果整个I2C总线卡死,所有设备都动不了。后来加了超时重试机制才解决。
时钟延展(Clock Stretching)是从设备的一种流控手段。从设备可以拉低SCL,告诉主设备“我还没准备好,你等等”。主设备必须检测SCL电平,如果被拉低就等待。ESP-IDF的驱动默认支持时钟延展,但有些第三方库没处理,容易出问题。
4.4 ESP-IDF I2C驱动框架
ESP-IDF的I2C驱动封装得挺完善。核心是 i2c_master_cmd_begin() 这个函数,它把一系列命令打包成链表,一次性发送。我个人习惯用这种“命令链”的方式,代码清晰,调试也方便。
驱动初始化分三步:
- 配置参数:设置GPIO引脚、时钟频率、是否启用内部上拉
- 安装驱动:分配资源,配置中断
- 创建命令链:用
i2c_cmd_link_create()创建,然后添加读写命令
初始化代码模板
i2c_config_t conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = GPIO_NUM_21,
.scl_io_num = GPIO_NUM_22,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = 100000
};
i2c_param_config(I2C_NUM_0, &conf);
i2c_driver_install(I2C_NUM_0, I2C_MODE_MASTER, 0, 0, 0);
4.5 读写EEPROM实战
EEPROM是I2C设备的典型代表。以AT24C02为例,容量256字节,地址0x50(7位)。写操作需要先发设备地址,再发存储地址,最后发数据。读操作分两步:先伪写(发设备地址+存储地址),再重新发设备地址+读位,然后收数据。
我直接上代码,注释写清楚每一步在干什么。
EEPROM写操作
esp_err_t eeprom_write_byte(uint8_t dev_addr, uint8_t mem_addr, uint8_t data) {
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, mem_addr, true);
i2c_master_write_byte(cmd, data, true);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(I2C_NUM_0, cmd, pdMS_TO_TICKS(1000));
i2c_cmd_link_delete(cmd);
return ret;
}
EEPROM读操作
esp_err_t eeprom_read_byte(uint8_t dev_addr, uint8_t mem_addr, uint8_t *data) {
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
// 伪写:发送设备地址+存储地址
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, mem_addr, true);
// 重新开始:发送设备地址+读位
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_READ, true);
i2c_master_read_byte(cmd, data, I2C_MASTER_NACK);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(I2C_NUM_0, cmd, pdMS_TO_TICKS(1000));
i2c_cmd_link_delete(cmd);
return ret;
}
注意:EEPROM写周期
AT24C02每写一个字节后,内部需要5ms左右的时间来完成编程。这期间不会响应任何I2C命令。我建议在每次写操作后加一个10ms的延时,或者轮询设备直到收到ACK为止。否则连续写会丢数据。
4.6 常见问题与调试技巧
调试I2C,示波器是最好的朋友。没有示波器的话,用逻辑分析仪也行。我总结了几种常见故障:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 总线卡死,SCL/SDA一直低 | 从设备未释放总线 | 复位从设备,或加超时重试 |
| 收不到ACK | 地址错误/设备未上电 | 检查地址位和上拉电阻 |
| 数据错位 | 时钟频率不匹配 | 降低频率到100kHz试试 |
| 写数据后读回全0xFF | 写周期未完成 | 增加写后延时 |
调试小技巧
我习惯在代码里加一个I2C扫描函数,遍历所有可能的设备地址,看哪些设备在线。这样能快速确认硬件连接和地址配置是否正确。ESP-IDF的例程里就有现成的i2c_scanner,拿来就能用。
好了,I2C这块的核心内容就这些。协议不难,但细节多。你写代码的时候,多想想时序图,多想想应答位,基本不会出大问题。EEPROM的读写是I2C最经典的应用场景,吃透了这个,其他I2C传感器、RTC芯片什么的,原理都一样。