前言

最近在玩 ESP32-S3 搭配 OV3660 摄像头模块,本来以为照着开源例程改改引脚就能跑起来,结果前前后后折腾了好几天,踩了不少坑,我还专门画了一块板子用来测试(24pin的接口真的是太难焊了,总是连锡)。这篇文章记录了我从完全无法驱动到最终稳定拍照的完整过程,希望对遇到类似问题的朋友有所帮助。注意:以下的代码是运行在我自己画的一块板子,板子的文件在https://gitcode.com/wonderful___day/ESP32OV3660,可以嘉立创免费打板,因为我把ESP32S3的引脚全部引出了(IO19,IO20给typec了,EN也没有引出),故而IO引脚的分配有点奇怪,有兴趣复刻我的板子你可以自由分配,如果是带摄像头的开发板,阅读相应引脚后,修改对应引脚的定义就可,十分要注意开启OPI
在这里插入图片描述

硬件环境

  • 主控:ESP32-S3 (带 8MB OPI PSRAM,自己画的一块板子)
  • 摄像头:OV3660 (24Pin DVP接口)
  • 开发环境:Arduino IDE
    在这里插入图片描述
    (这块板子设计了一个陀螺仪,可以用来实现一些好玩的开发,例如避障小车之类的)

问题一:供电电压的迷思

现象

一开始我参考了 OV2640 的资料,给摄像头的 DVDD 引脚提供了 1.2V 电压,结果用万用表测量电压,1.5V,不对怎么会是1.5V呢。把摄像头一拔,再一测就是1.2V

解决

这其实是内部电路通过 I2C 信号线的 ESD 保护二极管获得了微弱上拉,触发了内部稳压器启动。

问题二:I2C 扫描不到地址 (0x3C)

现象

供电没问题后,I2C 扫描仍然找不到 OV3660 的默认地址 0x3C

排查与发现

尝试只连接 SDA、SCL、XCLK 三条线进行 I2C 扫描,失败。

意外发现:当我额外连接了 PCLK 引脚后,I2C 扫描成功找到了 0x3C 地址。

原因分析

PCLK 的存在可能触发了摄像头内部的正确上电时序,或满足了软件驱动的初始化前提条件(例如驱动中可能等待 PCLK 稳定后才认为硬件就绪)。这说明 OV3660 对启动条件有一定要求,不仅仅是供电和 I2C 通就能响应。

解决

确保 SDA、SCL、XCLK、PCLK 四根线全部正确连接,即可正常扫描到 I2C 地址。

最终完整驱动需要连接所有数据线和同步信号线,但仅为了验证 I2C 通信,这四根线是必须的,前提是电源线与地线都连接了。


问题三:esp_camera_init 失败 - frame buffer malloc failed(这个问题最要紧,浪费了很多时间)

现象

I2C 能扫描到地址了,但调用 esp_camera_init() 时失败,报错:

E (2629) cam_hal: cam_dma_config(300): frame buffer malloc failed
E (2629) cam_hal: cam_config(384): cam_dma_config failed
E (2630) camera: Camera config failed with error 0xffffffff

排查过程

摄像头需要大块连续内存来存放图像帧缓冲(Frame Buffer),QVGA 分辨率约需 150KB,更高分辨率则需要更大内存。这些内存需要分配在 PSRAM 上。

在代码中添加 PSRAM 检测:

if(psramFound()){
    Serial.printf("PSRAM 已找到,大小: %d 字节\n", ESP.getPsramSize());
} else {
    Serial.println("PSRAM 未找到!");
}

结果打印出 “PSRAM 未找到!”

原因

在 Arduino IDE 中,XIAO ESP32-S3 的 PSRAM 默认是关闭的,且该板使用的是 OPI (Octal) PSRAM,与常见的 QSPI PSRAM 配置不同。

解决

在 Arduino IDE 的「工具」菜单中:

  1. 开发板 → 选择 Seeed Studio XIAO ESP32-S3
  2. PSRAM → 选择 OPI PSRAM (注意不是 EnabledQSPI PSRAM)
    在这里插入图片描述

配置正确后,再次运行 psramFound() 即可正确识别 8MB PSRAM,esp_camera_init() 顺利通过。


问题四:初始化成功但拍照失败

现象

esp_camera_init() 返回成功,但调用 esp_camera_fb_get() 拍照时返回空。

排查

检查硬件连接后发现,我只连接了 SDA、SCL、XCLK、PCLK 四根线。

初始化只需要控制通道(I2C)和时钟(XCLK),但拍照需要完整的数据通道(DVP),包括:

信号作用状态
D0 ~ D78位数据总线❌ 未连接
VSYNC垂直同步 (帧同步)❌ 未连接
HREF水平同步 (行同步)❌ 未连接
PCLK像素时钟✅ 已连接
XCLK主时钟✅ 已连接
SDA/SCLI2C/SCCB✅ 已连接

解决

将所有 D0-D7 数据线VSYNCHREF 全部连接到 ESP32-S3 对应的 GPIO,并确保代码中的引脚定义与实际接线一致。


最终成果

接好所有数据线后,成功输出 QVGA (320x240) JPEG 照片:

========================================
ESP32-S3 OV3660/OV2640 摄像头驱动测试
========================================
PSRAM 已找到,大小: 8386295 字节

====== 初始化摄像头 ======
[OK]   esp_camera_init
[INFO] 传感器型号: 0x3660
[INFO] 检测到 OV3660
[OK]   摄像头初始化完成

[INFO] 拍照成功: 320x240, 3585 bytes, format=4
[OK]   JPEG 文件头正确 (FF D8)

问题5:ESP32S3反复重启问题

现象:

使用 ESP32-S3 配合 OV3660/OV2640 摄像头搭建 Web 视频流服务器时,遇到频繁重启的问题。代码编译烧录正常,摄像头初始化偶有失败,Wi-Fi 连接后访问 /stream 页面时芯片很快复位。串口监视器能看到 Brownout detector was triggeredrst:0xc 等复位信息,视频流卡顿甚至无法显示。起初我尝试软件层面的优化:降低 XCLK 频率(从 20MHz 到 10MHz 乃至 8MHz)、在视频流循环中加入 delay(50) 让出 CPU、检查 PSRAM 配置,这些调整有一定改善但未能根治。最终定位到根本原因——供电不足。我使用的是 CH340 USB 转串口模块为开发板供电,该模块通常只能提供 5V/500mA 甚至更低的电流,而 ESP32-S3 开启 Wi-Fi 并传输摄像头 JPEG 数据时,瞬时峰值电流可达 600mA~1A,电压跌落触发了芯片内部的欠压锁定(BOR)保护,导致反复强制复位。

解决:

还好我在设计开发板的时候设计了USB供电与LDO降压模块,把5V降为3.3V,为芯片提供了稳定的供电。额外建议:若条件允许,可将 XCLK 频率恢复至 10MHz 或 20MHz 以提升帧率;串口调试时留意是否有 BOR 报错,这是供电问题的直接信号。经验教训:ESP32 摄像头项目,供电永远是第一道防线,优先保证 5V/2A 以上的稳定输入,远比软件调参更关键。

附:完整引脚定义 (该引脚配置只说明使用这些引脚可以驱动摄像头,不是针对任意一块开发板)

我在开发的时候就遇到过这个疑问,市面上有好多带ov3660的开发板,他们板子对应引脚的IO是不一样的,导致我问AI某某引脚一定是要是特定的IO口吗,他总是回答的模棱两可,导致我一气之下画了一块板子用来开发,经过我的测试,我认为不是特定的,可以自由换,但是要注意Spring引脚,这玩意阴的很,我有次开发时几个Spring引脚接了10k上拉(有些放在底层,有些放在顶层),我焊完顶层烧录发现没毛,等后面一焊接,死活烧不上,给我气的一个个底部元件一个个拆,最后发现是某个Spring引脚的上拉导致启动异常,解决倒也简单,把那个引脚的上拉电阻拆了就可以了。

摄像头信号GPIO
SDA (SIO_D)17
SCL (SIO_C)18
XCLK8
PCLK48
VSYNC21
HREF38
D0 (Y2)45
D1 (Y3)47
D2 (Y4)14
D3 (Y5)40
D4 (Y6)5
D5 (Y7)42
D6 (Y8)41
D7 (Y9)39
RESET4
PWDN3

开发时我认为摄像头的引脚一定是要那个某某IO口,其实实际上ESP32S3内置了IO矩阵,理论上可以使用任意一个引脚作为摄像头的一个引脚,但是要注意最好避免使用Spring引脚可能会影响ESP32S3的运行

OV3660的24pin接口图(这也是一个大坑,比较难找)

24pin接口图

遗留问题与优化方向

高分辨率拍照失败

目前 VGA 及以上分辨率拍照失败,原因是高分辨率 RAW 帧需要更大的连续内存块,8MB PSRAM 可能因碎片化无法分配足够大的连续空间。

优化建议

  1. 降低 fb_count:从 2 改为 1,节省一半帧缓冲内存。
  2. 提高 JPEG 质量数值jpeg_quality 从 12 改为 20+,减少编码时内存占用。
  3. 调整帧大小:按需选择合适分辨率,如 800x600 可用 FRAMESIZE_SVGA

总结

这次调试最大的收获是:

  1. 供电必须严格按数据手册:OV3660 的 DVDD 是 1.5V,也可以是 1.2V。
  2. ESP32-S3 的 PSRAM 配置有坑:ESP32-S3 必须选 OPI PSRAM,不是普通的 Enabled。
  3. I2C 扫不到地址时,检查所有可能影响芯片启动的信号:PCLK 的存在也能影响 I2C 通信,说明芯片内部状态机对启动条件有要求。
  4. 初始化成功 ≠ 能拍照:数据总线(D0-D7)和同步信号(VSYNC/HREF)一根都不能少。

希望这篇文章能帮到正在折腾 OV3660 的你!如果有问题,欢迎在评论区交流。

本次的代码(我的代码仓库里面也有)

/**
 * ESP32-S3 OV3660/OV2640 摄像头 Web 服务器(STA 模式)
 * 
 * 引脚定义(与您的连接完全一致):
 *   PWDN  = 3   (必须拉低)
 *   RESET = -1  (未连接,硬件上拉)
 *   XCLK  = 9
 *   SIOD  = 7
 *   SIOC  = 6
 *   D7..D0 = 10,12,14,47,21,13,48,45
 *   VSYNC = 8
 *   HREF  = 46
 *   PCLK  = 11
 */

#include <WiFi.h>
#include <WebServer.h>
#include <esp_camera.h>

// ========== Wi-Fi 配置(改为 STA 模式) ==========
const char* ssid     = "a";      // 改为您路由器的 SSID
const char* password = "12345678";      // 改为您路由器的密码

// ========== 摄像头引脚定义 ==========
#define CAM_PIN_PWDN  3
#define CAM_PIN_RESET -1    // 未连接
#define CAM_PIN_XCLK  9
#define CAM_PIN_SIOD  7
#define CAM_PIN_SIOC  6

#define CAM_PIN_D7   10    // Y9
#define CAM_PIN_D6   12    // Y8
#define CAM_PIN_D5   14    // Y7
#define CAM_PIN_D4   47    // Y6
#define CAM_PIN_D3   21    // Y5
#define CAM_PIN_D2   13    // Y4
#define CAM_PIN_D1   48    // Y3
#define CAM_PIN_D0   45    // Y2

#define CAM_PIN_VSYNC 8
#define CAM_PIN_HREF  46
#define CAM_PIN_PCLK  11

// ========== Web 服务器 ==========
WebServer server(80);

// ========== 摄像头配置 ==========
camera_config_t config;

// ========== 初始化摄像头 ==========
bool initCamera() {
  config.pin_pwdn  = CAM_PIN_PWDN;
  config.pin_reset = CAM_PIN_RESET;   // 显式设为 -1
  config.pin_xclk  = CAM_PIN_XCLK;
  config.pin_sccb_sda = CAM_PIN_SIOD;
  config.pin_sccb_scl = CAM_PIN_SIOC;
  config.pin_d7 = CAM_PIN_D7;
  config.pin_d6 = CAM_PIN_D6;
  config.pin_d5 = CAM_PIN_D5;
  config.pin_d4 = CAM_PIN_D4;
  config.pin_d3 = CAM_PIN_D3;
  config.pin_d2 = CAM_PIN_D2;
  config.pin_d1 = CAM_PIN_D1;
  config.pin_d0 = CAM_PIN_D0;
  config.pin_vsync = CAM_PIN_VSYNC;
  config.pin_href  = CAM_PIN_HREF;
  config.pin_pclk  = CAM_PIN_PCLK;

  config.xclk_freq_hz = 10000000;        // 尝试 20MHz(如不稳定可改回 10000000)
  config.ledc_timer   = LEDC_TIMER_0;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.pixel_format = PIXFORMAT_JPEG;
  config.frame_size   = FRAMESIZE_QVGA;   // 320x240
  config.jpeg_quality = 12;               // 降低画质提高流畅度(0~63)
  config.fb_count     = (psramFound()) ? 2 : 1;

  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("摄像头初始化失败: 0x%x\n", err);
    return false;
  }

  sensor_t *s = esp_camera_sensor_get();
  s->set_brightness(s, 0);
  s->set_contrast(s, 0);
  s->set_saturation(s, 0);
  s->set_whitebal(s, 1);
  s->set_gain_ctrl(s, 1);
  s->set_exposure_ctrl(s, 1);
  s->set_hmirror(s, 0);
  s->set_vflip(s, 0);

  Serial.println("摄像头初始化成功");
  return true;
}

// ========== 处理 /stream (MJPEG 视频流) ==========
void handleStream() {
  WiFiClient client = server.client();
  String boundary = "123456789000000000000987654321";
  
  client.println("HTTP/1.1 200 OK");
  client.println("Content-Type: multipart/x-mixed-replace; boundary=" + boundary);
  client.println("Connection: close");
  client.println();

  while (client.connected()) {
    camera_fb_t *fb = esp_camera_fb_get();
    if (!fb) {
      Serial.println("获取帧失败");
      break;
    }
    client.print("--" + boundary + "\r\n");
    client.print("Content-Type: image/jpeg\r\n");
    client.print("Content-Length: " + String(fb->len) + "\r\n\r\n");
    client.write(fb->buf, fb->len);
    client.print("\r\n");
    esp_camera_fb_return(fb);
    // 如果加了 delay,请确保在这里添加:
    // delay(50);
  }
}   // <--- 这个右大括号不能少!

// ========== 处理 /capture (单张拍照) ==========
void handleCapture() {
  camera_fb_t *fb = esp_camera_fb_get();
  if (!fb) {
    server.send(500, "text/plain", "拍照失败");
    return;
  }

  WiFiClient client = server.client();
  client.println("HTTP/1.1 200 OK");
  client.println("Content-Type: image/jpeg");
  client.println("Content-Length: " + String(fb->len));
  client.println("Connection: close");
  client.println();
  client.write(fb->buf, fb->len);

  esp_camera_fb_return(fb);
}

// ========== 根页面(显示 STA 信息) ==========
void handleRoot() {
  String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>ESP32-S3 摄像头 (STA)</title>
    <style>
        body { font-family: Arial; text-align: center; background: #1e1e2e; color: white; }
        img { max-width: 100%; height: auto; border: 2px solid #555; border-radius: 10px; }
        .btn { display: inline-block; margin: 10px; padding: 12px 24px; background: #4CAF50; color: white; text-decoration: none; border-radius: 8px; }
        .status { color: #4CAF50; font-weight: bold; }
    </style>
</head>
<body>
    <h1>📷 实时监控 (STA 模式)</h1>
    <img src="/stream" id="stream" alt="视频流加载中...">
    <br>
    <a class="btn" href="/capture" target="_blank">📸 抓拍</a>
    <a class="btn" href="/" style="background:#f44336;">🔄 刷新</a>
    <p>📶 已连接到:)rawliteral" + String(ssid) + R"rawliteral( | IP:)rawliteral" + WiFi.localIP().toString() + R"rawliteral(</p>
    <p class="status">✅ 摄像头已就绪</p>
</body>
</html>
)rawliteral";
  server.send(200, "text/html", html);
}

// ========== setup ==========
void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("\n启动摄像头 Web 服务器 (STA 模式)");

  // 检查 PSRAM
  if (!psramFound()) {
    Serial.println("⚠️ PSRAM 未启用!图像可能不稳定,请启用 PSRAM。");
  } else {
    Serial.printf("✅ PSRAM 大小: %d KB\n", ESP.getPsramSize() / 1024);
  }

  // 初始化摄像头(必须在 Wi-Fi 之前,以免影响时序?通常无严格要求,但建议先初始化)
  if (!initCamera()) {
    Serial.println("摄像头初始化失败,停止运行");
    return;
  }

  // -------- 改为 STA 模式,连接路由器 --------
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.printf("正在连接 Wi-Fi %s", ssid);
  
  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 30) {  // 最多等待约 30 秒
    delay(500);
    Serial.print(".");
    attempts++;
  }
  Serial.println();

  if (WiFi.status() == WL_CONNECTED) {
    IPAddress IP = WiFi.localIP();
    Serial.println("✅ Wi-Fi 连接成功!");
    Serial.printf("📡 SSID: %s\n", ssid);
    Serial.printf("🌐 IP 地址: %s\n", IP.toString().c_str());
    Serial.printf("🌍 在局域网内访问 http://%s 查看视频流\n", IP.toString().c_str());
  } else {
    Serial.println("❌ Wi-Fi 连接失败,请检查 SSID 和密码!");
    // 仍可启动服务器但无法访问,也可选择停止运行
    return;
  }

  // 注册 Web 路由
  server.on("/", handleRoot);
  server.on("/stream", handleStream);
  server.on("/capture", handleCapture);

  server.begin();
  Serial.println("HTTP 服务器已启动");
}

// ========== loop ==========
void loop() {
  server.handleClient();
  // 可添加 Wi-Fi 重连机制(非必须)
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("Wi-Fi 断开,尝试重连...");
    WiFi.reconnect();
    delay(5000);
  }
  delay(1);
}
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐