☰
ESP32-S3 + OV3660 摄像头驱动踩坑全记录:从“找不到地址“到“稳定出图“
2026/11/5 21:02:43 网站建设 项目流程

前言

最近在玩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(注意不是Enabled或QSPI 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 数据线、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
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接口图(这也是一个大坑,比较难找)

遗留问题与优化方向

高分辨率拍照失败

目前 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 模式) ==========constchar*ssid="a";// 改为您路由器的 SSIDconstchar*password="12345678";// 改为您路由器的密码// ========== 摄像头引脚定义 ==========#defineCAM_PIN_PWDN3#defineCAM_PIN_RESET-1// 未连接#defineCAM_PIN_XCLK9#defineCAM_PIN_SIOD7#defineCAM_PIN_SIOC6#defineCAM_PIN_D710// Y9#defineCAM_PIN_D612// Y8#defineCAM_PIN_D514// Y7#defineCAM_PIN_D447// Y6#defineCAM_PIN_D321// Y5#defineCAM_PIN_D213// Y4#defineCAM_PIN_D148// Y3#defineCAM_PIN_D045// Y2#defineCAM_PIN_VSYNC8#defineCAM_PIN_HREF46#defineCAM_PIN_PCLK11// ========== Web 服务器 ==========WebServerserver(80);// ========== 摄像头配置 ==========camera_config_tconfig;// ========== 初始化摄像头 ==========boolinitCamera(){config.pin_pwdn=CAM_PIN_PWDN;config.pin_reset=CAM_PIN_RESET;// 显式设为 -1config.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;// 320x240config.jpeg_quality=12;// 降低画质提高流畅度(0~63)config.fb_count=(psramFound())?2:1;esp_err_terr=esp_camera_init(&config);if(err!=ESP_OK){Serial.printf("摄像头初始化失败: 0x%x\n",err);returnfalse;}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("摄像头初始化成功");returntrue;}// ========== 处理 /stream (MJPEG 视频流) ==========voidhandleStream(){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 (单张拍照) ==========voidhandleCapture(){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 信息) ==========voidhandleRoot(){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:12px24px;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 ==========voidsetup(){Serial.begin(115200);delay(1000);Serial.println("\n启动摄像头 Web 服务器 (STA 模式)");// 检查 PSRAMif(!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);intattempts=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 ==========voidloop(){server.handleClient();// 可添加 Wi-Fi 重连机制(非必须)if(WiFi.status()!=WL_CONNECTED){Serial.println("Wi-Fi 断开,尝试重连...");WiFi.reconnect();delay(5000);}delay(1);}

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询