最近在使用 ESP-IDF v5.x 的新版 I2C 驱动(i2c_master_ 系列 API)驱动 OV7670 摄像头时,遇到了一个非常有意思的“灵异现象”:
按照标准 I2C 的逻辑,读取传感器寄存器通常使用的是 “写后读” (Write-then-Read) 的复合操作。但在 OV7670 上,如果我直接调用 ESP-IDF 提供的 i2c_master_transmit_receive 接口,读取必定失败(超时或 NACK)。
然而,如果我把这个操作拆成两步:先调用 transmit 发送寄存器地址,再调用 receive 读取数据,它竟然就神奇地跑通了!
这到底是为什么?是代码写错了,还是 ESP32 的驱动有 Bug?
经过一番示波器抓包和 Datasheet 查阅,我终于找到了真凶:SCCB 协议与标准 I2C 在“重复起始信号 (Repeated Start)”上的兼容性问题。
一、 案发现场
1. 失败的代码(标准 I2C 写法)
在 ESP-IDF 的新版驱动中,读取 I2C 设备寄存器的标准姿势是这样的:
// 试图读取 OV7670 的 PID 寄存器 (0x0A)
uint8_t reg_addr = 0x0A;
uint8_t data_buf[1] = {0};
// 标准的“写地址 -> 重复起始 -> 读数据”复合操作
esp_err_t ret = i2c_master_transmit_receive(
dev_handle,
®_addr, 1,
data_buf, 1,
-1
);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Read success: 0x%02x", data_buf[0]);
} else {
// OV7670 这里必定报错!
ESP_LOGE(TAG, "Read failed: %s", esp_err_to_name(ret));
}
这段代码在 MPU6050、AHT20 等现代 I2C 传感器上运行完美,但在 OV7670 上直接“暴毙”。
2. 成功的代码(“笨”办法)
当我把上述原子操作拆解开来:
uint8_t reg_addr = 0x0A;
uint8_t data_buf[1] = {0};
// 第一步:只发送寄存器地址(会自动产生 STOP)
esp_err_t ret = i2c_master_transmit(dev_handle, ®_addr, 1, -1);
if (ret != ESP_OK) return ret;
// 第二步:只读取数据(新的 START)
ret = i2c_master_receive(dev_handle, data_buf, 1, -1);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Read success: 0x%02x", data_buf[0]); // 成功读出数据!
}
虽然代码看起来变繁琐了,但它确实能工作。这直接指向了一个底层时序问题。
二、 核心原理:Repeated Start vs Stop
要理解这个问题,必须看懂 I2C 总线上的时序图。
1. 标准 I2C 的“写后读”
为了效率和总线控制权,标准 I2C 规定:在主机写完寄存器地址后,不需要发送停止信号 (STOP),而是直接发送一个 重复起始信号 (Repeated Start / Sr),然后紧接着发送读地址。
时序流:
[START] -> [设备写地址] -> [寄存器地址] -> [REPEATED START] -> [设备读地址] -> [数据] -> [STOP]
ESP-IDF 的 i2c_master_transmit_receive 就是严格遵循这个标准的。它中间没有 STOP。
2. OV7670 的 SCCB 协议
OV7670 使用的是 OmniVision 自家的 SCCB (Serial Camera Control Bus) 协议。虽然号称兼容 I2C,但在早期的 SCCB 规范中(OV7670 属于古董级芯片了),它不支持(或极度讨厌)Repeated Start。
SCCB 协议要求:一个写操作(Phase 1 & Phase 2)完成后,必须跟一个 STOP (P) 信号,让芯片内部的状态机复位。想要读数据,必须重新发起一个新的传输周期。
SCCB 要求的时序流:
- 写周期:
[START] -> [设备写地址] -> [寄存器地址] -> [STOP] - (中间必须断开)
- 读周期:
[START] -> [设备读地址] -> [数据] -> [STOP]
3. 真相大白
- 标准 API 发送了
Repeated Start,OV7670 甚至还没反应过来刚才那个“写地址”命令结束了,就收到了新的 Start,导致状态机混乱,不仅不回 ACK,甚至可能锁死总线。 - 拆分写 发送了
STOP,OV7670 收到 Stop 后心满意足地确认了寄存器地址,准备好在下一次 Start 时吐出数据。
三、 最佳实践封装
在 ESP-IDF 项目中,为了代码的复用性,建议专门为 SCCB 设备封装一个读取函数,不要混用标准的 I2C 读取接口。
以下是基于 ESP-IDF v5.2+ i2c_master API 的封装示例:
/**
* @brief 专门针对 SCCB 协议 (OV系列摄像头) 的读取函数
* 解决了不支持 Repeated Start 的问题
*/
esp_err_t sccb_read_reg(i2c_master_dev_handle_t dev_handle, uint8_t reg_addr, uint8_t *data)
{
esp_err_t ret;
// 1. Phase 1: Write Register Address
// i2c_master_transmit 会在发送结束后自动产生 STOP 信号
ret = i2c_master_transmit(dev_handle, ®_addr, 1, -1);
if (ret != ESP_OK) {
return ret;
}
// 2. Phase 2: Read Data
// 发起全新的传输周期 (New Start)
ret = i2c_master_receive(dev_handle, data, 1, -1);
return ret;
}
/**
* @brief SCCB 写寄存器 (这个和标准 I2C 一样)
*/
esp_err_t sccb_write_reg(i2c_master_dev_handle_t dev_handle, uint8_t reg_addr, uint8_t data)
{
uint8_t write_buf[2] = {reg_addr, data};
return i2c_master_transmit(dev_handle, write_buf, sizeof(write_buf), -1);
}
四、 总结
做嵌入式开发,最忌讳的就是“想当然”。虽然 I2C 是通用标准,但硬件世界里充满了“方言”。
- OV7670 (及大多数老款 OV 传感器) 使用的是 SCCB 协议。
- SCCB ≠ 标准 I2C,核心区别在于对 Repeated Start 的支持。
- 在驱动此类设备时,必须在写地址和读数据之间插入 STOP 信号。
- 如果你使用
i2c_master_transmit_receive失败,请尝试拆分成transmit+receive两步走,往往会有奇效。
希望这个踩坑记录能帮到正在调试摄像头的你!
Leave a comment