ESP32-S3 + OV3660 摄像头驱动踩坑全记录:从“找不到地址“到“稳定出图“
前言
最近在玩 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 的「工具」菜单中:
- 开发板 → 选择
Seeed Studio XIAO ESP32-S3 - PSRAM → 选择
OPI PSRAM(注意不是Enabled或QSPI PSRAM)

配置正确后,再次运行 psramFound() 即可正确识别 8MB PSRAM,esp_camera_init() 顺利通过。
问题四:初始化成功但拍照失败
现象
esp_camera_init() 返回成功,但调用 esp_camera_fb_get() 拍照时返回空。
排查
检查硬件连接后发现,我只连接了 SDA、SCL、XCLK、PCLK 四根线。
初始化只需要控制通道(I2C)和时钟(XCLK),但拍照需要完整的数据通道(DVP),包括:
| 信号 | 作用 | 状态 |
|---|---|---|
| D0 ~ D7 | 8位数据总线 | ❌ 未连接 |
| VSYNC | 垂直同步 (帧同步) | ❌ 未连接 |
| HREF | 水平同步 (行同步) | ❌ 未连接 |
| PCLK | 像素时钟 | ✅ 已连接 |
| XCLK | 主时钟 | ✅ 已连接 |
| SDA/SCL | I2C/SCCB | ✅ 已连接 |
解决
将所有 D0-D7 数据线、VSYNC、HREF 全部连接到 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 triggered 或 rst: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 |
| XCLK | 8 |
| PCLK | 48 |
| VSYNC | 21 |
| HREF | 38 |
| D0 (Y2) | 45 |
| D1 (Y3) | 47 |
| D2 (Y4) | 14 |
| D3 (Y5) | 40 |
| D4 (Y6) | 5 |
| D5 (Y7) | 42 |
| D6 (Y8) | 41 |
| D7 (Y9) | 39 |
| RESET | 4 |
| PWDN | 3 |
开发时我认为摄像头的引脚一定是要那个某某IO口,其实实际上ESP32S3内置了IO矩阵,理论上可以使用任意一个引脚作为摄像头的一个引脚,但是要注意最好避免使用Spring引脚可能会影响ESP32S3的运行
OV3660的24pin接口图(这也是一个大坑,比较难找)

遗留问题与优化方向
高分辨率拍照失败
目前 VGA 及以上分辨率拍照失败,原因是高分辨率 RAW 帧需要更大的连续内存块,8MB PSRAM 可能因碎片化无法分配足够大的连续空间。
优化建议
- 降低
fb_count:从 2 改为 1,节省一半帧缓冲内存。 - 提高 JPEG 质量数值:
jpeg_quality从 12 改为 20+,减少编码时内存占用。 - 调整帧大小:按需选择合适分辨率,如 800x600 可用
FRAMESIZE_SVGA。
总结
这次调试最大的收获是:
- 供电必须严格按数据手册:OV3660 的 DVDD 是 1.5V,也可以是 1.2V。
- ESP32-S3 的 PSRAM 配置有坑:ESP32-S3 必须选 OPI PSRAM,不是普通的 Enabled。
- I2C 扫不到地址时,检查所有可能影响芯片启动的信号:PCLK 的存在也能影响 I2C 通信,说明芯片内部状态机对启动条件有要求。
- 初始化成功 ≠ 能拍照:数据总线(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);
}
更多推荐



所有评论(0)