最近在使用 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, 
    &reg_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, &reg_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 要求的时序流:

  1. 写周期: [START] -> [设备写地址] -> [寄存器地址] -> [STOP]
  2. (中间必须断开)
  3. 读周期: [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, &reg_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 是通用标准,但硬件世界里充满了“方言”。

  1. OV7670 (及大多数老款 OV 传感器) 使用的是 SCCB 协议。
  2. SCCB ≠ 标准 I2C,核心区别在于对 Repeated Start 的支持。
  3. 在驱动此类设备时,必须在写地址和读数据之间插入 STOP 信号。
  4. 如果你使用 i2c_master_transmit_receive 失败,请尝试拆分成 transmit + receive 两步走,往往会有奇效。

希望这个踩坑记录能帮到正在调试摄像头的你!

Leave a comment