ESP32-S3-CAM:接ov3660摄像头
越来越意识到,要接硬件之前要先了解这些硬件小伙伴,其实一开始我自己连怎么看摄像头是不是ov3660都不知道,接排线也是恍然大悟。
Part 1 认识摄像头并连接到主板上
1、认识摄像头


2、通过FPC柔性排线座连接摄像头
第一步先接摄像头,买到的是散件,摄像头和板子是分开的,一定要金色金属面朝下,然后塞进去白色卡座里,再卡住。
1)黑色夹片先用手往上掰起来

2)摄像头排线金色面朝下,黑色面朝上,插入卡槽中间,两边是白色底座,大概可以插进去2个毫米左右

3)黑色夹片扣下扣紧即可

三、认识柔性排线座
突然发现那个黑色卡子上有文字,44和24P,查了下,又涨知识了,原来那个黑色卡子的名字叫 柔性排线座。
这是FPC/FFC连接器(柔性排线座)的规格标识
标识解读
| 标识 | 含义 | 说明 |
|---|---|---|
| 44 | 排线宽度 4.4mm | 指FFC/FPC排线的总宽度(含导体部分) |
| 24P | 24 Pin(24针脚) | 连接器有24个金属触点/引脚 |
具体含义
44(4.4mm宽度)
-
这是排线规格代码,表示适配的FFC/FPC软排线总宽度为 4.4mm
-
常见的还有 5.0mm、6.0mm、8.0mm 等规格
-
这个尺寸必须匹配,否则排线插不进去或接触不良
24P(24 Pins)
-
表示这个连接器有 24个引脚/通道
-
用于连接摄像头模组的24根信号线(包括电源、地线、MIPI/CSI数据 lanes、时钟、控制信号等)
关于这个连接器
这是 ESP32-S3-CAM 的摄像头接口座,用于连接 OV2640/OV5640 等摄像头模组。
ESP32-S3-CAM 通常有两个独立的接口:
-
📷 摄像头接口(图片中的这个,24Pin FPC座)
-
💾 SD卡槽(一般在板子另一侧,通常是自弹式或推入式卡槽)
常见摄像头排线规格对照
| 标识 | 排线宽度 | 常见用途 |
|---|---|---|
| 44-24P | 4.4mm / 24pin | OV2640 小模组 |
| 50-24P | 5.0mm / 24pin | OV5640 等 |
| 60-30P | 6.0mm / 30pin | 高分辨率模组 |
Part 2 写摄像头自检代码
1、自检功能定义
功能:每5秒钟拍一张照,然后通过局域网显示查看拍的照片
其实一开始我也不知道居然可以用局域网打开个网址查看摄像头拍的照片,偶然一次通过AI给我一段代码检测摄像头的时候发现的,居然还有这样的牛逼功能。前提又要增加一个联WiFi的功能。
2、WiFi注意事项
注意:用手机开个热点就可以,但是WiFi热点必须是2.4G的,ESP32-S3这个型号的板子都只支持2.4G频段,WiFi命名和密码都不要太高级,简单点就行,小写字母加数字组合足够,比如我自己的设置的:WiFi名称: fksijie 密码:112233445566
3、用AI写自检代码
热点设置好以后,让AI写一段摄像头自检代码,AI建议用Kimi写代码,豆包经过我的验证,成功率比较低,用多了Kimi后发现,居然免费版每天还有问题个数上限,没办法,选择了第一个付费档位充值一个月,为了节约时间,花点钱也就花点钱吧!!!
提示词:我用ESP32-S3-CAM连接了一个ov3660摄像头,给我一段自检代码,每五秒拍一张照,通过局域网查看拍到的照片
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <esp_camera.h>
// ========== AI-Thinker ESP32-S3-CAM 引脚配置 ==========
#define CAM_PWDN -1
#define CAM_RESET -1
#define CAM_XCLK 15
#define CAM_SIOD 4
#define CAM_SIOC 5
#define CAM_D7 16
#define CAM_D6 17
#define CAM_D5 18
#define CAM_D4 12
#define CAM_D3 10
#define CAM_D2 8
#define CAM_D1 9
#define CAM_D0 11
#define CAM_VSYNC 6
#define CAM_HREF 7
#define CAM_PCLK 13
// ========== Freenove ESP32-S3-CAM 引脚配置 ==========
// #define CAM_PWDN -1
// #define CAM_RESET -1
// #define CAM_XCLK 15
// #define CAM_SIOD 4
// #define CAM_SIOC 5
// #define CAM_D7 11 // Freenove: D7=GPIO11 (AI-Thinker: GPIO16)
// #define CAM_D6 9 // Freenove: D6=GPIO9 (AI-Thinker: GPIO17)
// #define CAM_D5 8 // Freenove: D5=GPIO8 (AI-Thinker: GPIO18)
// #define CAM_D4 10 // Freenove: D4=GPIO10 (AI-Thinker: GPIO12)
// #define CAM_D3 12 // Freenove: D3=GPIO12 (AI-Thinker: GPIO10)
// #define CAM_D2 18 // Freenove: D2=GPIO18 (AI-Thinker: GPIO8)
// #define CAM_D1 17 // Freenove: D1=GPIO17 (AI-Thinker: GPIO9)
// #define CAM_D0 16 // Freenove: D0=GPIO16 (AI-Thinker: GPIO11)
// #define CAM_VSYNC 6
// #define CAM_HREF 7
// #define CAM_PCLK 13
// 替换为您的 WiFi 信息
const char* ssid = "fksijie";
const char* password = "112233445566";
WebServer server(80);
// 照片存储缓冲区
camera_fb_t * fb = NULL;
// 拍照时间控制
unsigned long lastCaptureTime = 0;
const unsigned long captureInterval = 5000; // 10秒间隔
// 最新照片数据
uint8_t* lastPhotoBuffer = NULL;
size_t lastPhotoLength = 0;
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("ESP32-S3-CAM OV3660 启动中...");
// 初始化摄像头
if (!initCamera()) {
Serial.println("摄像头初始化失败,重启...");
delay(2000);
ESP.restart();
}
// 连接 WiFi
WiFi.begin(ssid, password);
Serial.print("连接 WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("WiFi 已连接,IP: ");
Serial.println(WiFi.localIP());
// 设置 Web 服务器路由
server.on("/", handleRoot);
server.on("/capture", handleCapture);
server.on("/photo", handlePhoto);
server.on("/stream", handleStream);
server.begin();
Serial.println("Web 服务器已启动");
Serial.print("访问 http://");
Serial.println(WiFi.localIP());
}
void loop() {
server.handleClient();
// 每隔10秒自动拍照
unsigned long currentTime = millis();
if (currentTime - lastCaptureTime >= captureInterval) {
lastCaptureTime = currentTime;
capturePhoto();
Serial.println("自动拍照完成");
}
}
// ========== 摄像头初始化 ==========
bool initCamera() {
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_pwdn = CAM_PWDN;
config.pin_reset = CAM_RESET;
config.pin_xclk = CAM_XCLK;
config.pin_sccb_sda = CAM_SIOD;
config.pin_sccb_scl = CAM_SIOC;
config.pin_d7 = CAM_D7;
config.pin_d6 = CAM_D6;
config.pin_d5 = CAM_D5;
config.pin_d4 = CAM_D4;
config.pin_d3 = CAM_D3;
config.pin_d2 = CAM_D2;
config.pin_d1 = CAM_D1;
config.pin_d0 = CAM_D0;
config.pin_vsync = CAM_VSYNC;
config.pin_href = CAM_HREF;
config.pin_pclk = CAM_PCLK;
config.xclk_freq_hz = 20000000; // 20MHz XCLK
config.pixel_format = PIXFORMAT_JPEG; // JPEG 格式
// OV3660 支持 3MP (2048x1536),但建议先用较低分辨率测试
config.frame_size = FRAMESIZE_UXGA; // 1600x1200,可改为 FRAMESIZE_SXGA (1280x1024)
config.jpeg_quality = 12; // 0-63,越低质量越高
config.fb_count = 2; // 双缓冲
// 初始化摄像头
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();
if (s) {
// 水平翻转(根据实际需要调整)
s->set_hmirror(s, 0);
// 垂直翻转
s->set_vflip(s, 0);
// 亮度 -2 到 2
s->set_brightness(s, 0);
// 对比度 -2 到 2
s->set_contrast(s, 0);
// 饱和度 -2 到 2
s->set_saturation(s, 0);
// 如果是 OV3660,可以设置更高增益
s->set_gainceiling(s, GAINCEILING_16X);
Serial.println("摄像头传感器配置完成");
Serial.printf("传感器 PID: 0x%x\n", s->id.PID);
}
Serial.println("摄像头初始化成功");
return true;
}
// ========== 拍照函数 ==========
void capturePhoto() {
// 释放之前的缓冲区
if (lastPhotoBuffer != NULL) {
free(lastPhotoBuffer);
lastPhotoBuffer = NULL;
}
// 获取一帧
fb = esp_camera_fb_get();
if (!fb) {
Serial.println("拍照失败:获取帧缓冲区失败");
return;
}
// 复制照片数据
lastPhotoLength = fb->len;
lastPhotoBuffer = (uint8_t*)malloc(lastPhotoLength);
if (lastPhotoBuffer != NULL) {
memcpy(lastPhotoBuffer, fb->buf, lastPhotoLength);
Serial.printf("拍照成功: %d bytes, 分辨率: %dx%d\n",
lastPhotoLength, fb->width, fb->height);
} else {
Serial.println("内存分配失败");
}
// 释放帧缓冲区
esp_camera_fb_return(fb);
fb = NULL;
}
// ========== Web 页面:主页 ==========
void handleRoot() {
String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32-S3-CAM OV3660</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: #1a1a1a;
color: #fff;
text-align: center;
}
h1 { color: #00d4ff; }
.photo-container {
margin: 20px 0;
background: #2a2a2a;
padding: 20px;
border-radius: 10px;
min-height: 300px;
}
img {
max-width: 100%;
height: auto;
border-radius: 5px;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
}
.info {
color: #aaa;
margin: 10px 0;
}
.btn {
background: #00d4ff;
color: #000;
border: none;
padding: 12px 24px;
font-size: 16px;
border-radius: 5px;
cursor: pointer;
margin: 5px;
transition: 0.3s;
}
.btn:hover { background: #00a8cc; }
.status {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background: #0f0;
margin-right: 5px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
#timer {
font-size: 24px;
color: #00d4ff;
margin: 10px 0;
}
</style>
</head>
<body>
<h1>📷 ESP32-S3-CAM OV3660</h1>
<div class="info">
<span class="status"></span>自动拍照中(每5秒)
</div>
<div id="timer">下次拍照: 5秒</div>
<div class="photo-container">
<img id="photo" src="/photo" alt="最新照片">
</div>
<div>
<button class="btn" onclick="refreshPhoto()">🔄 立即刷新</button>
<button class="btn" onclick="toggleAuto()">⏸ 暂停/继续</button>
<button class="btn" onclick="captureNow()">📸 立即拍照</button>
</div>
<div class="info" id="photoInfo">等待照片...</div>
<script>
let autoRefresh = true;
let countdown = 5;
// 自动倒计时
setInterval(() => {
if (autoRefresh && countdown > 0) {
countdown--;
document.getElementById('timer').textContent = '下次拍照: ' + countdown + '秒';
}
if (countdown === 0) {
refreshPhoto();
countdown = 5;
}
}, 1000);
// 刷新照片
function refreshPhoto() {
const img = document.getElementById('photo');
const timestamp = new Date().getTime();
img.src = '/photo?t=' + timestamp;
document.getElementById('photoInfo').textContent = '更新时间: ' + new Date().toLocaleTimeString();
}
// 立即拍照
function captureNow() {
fetch('/capture')
.then(r => r.text())
.then(t => {
countdown = 5;
refreshPhoto();
});
}
// 暂停/继续自动刷新
function toggleAuto() {
autoRefresh = !autoRefresh;
document.getElementById('timer').style.display = autoRefresh ? 'block' : 'none';
}
// 初始加载
refreshPhoto();
</script>
</body>
</html>
)rawliteral";
server.send(200, "text/html", html);
}
// ========== Web 页面:立即拍照 ==========
void handleCapture() {
capturePhoto();
if (lastPhotoBuffer != NULL) {
server.send(200, "text/plain", "拍照成功: " + String(lastPhotoLength) + " bytes");
} else {
server.send(500, "text/plain", "拍照失败");
}
}
// ========== Web 页面:获取照片 ==========
void handlePhoto() {
if (lastPhotoBuffer == NULL) {
server.send(404, "text/plain", "暂无照片,请等待自动拍照或手动触发");
return;
}
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "0");
server.send_P(200, "image/jpeg", (const char*)lastPhotoBuffer, lastPhotoLength);
}
// ========== Web 页面:视频流(MJPEG) ==========
void handleStream() {
WiFiClient client = server.client();
String response = "HTTP/1.1 200 OK\r\n";
response += "Content-Type: multipart/x-mixed-replace; boundary=frame\r\n\r\n";
client.print(response);
while (client.connected()) {
fb = esp_camera_fb_get();
if (!fb) continue;
response = "--frame\r\n";
response += "Content-Type: image/jpeg\r\n\r\n";
client.print(response);
client.write(fb->buf, fb->len);
client.print("\r\n");
esp_camera_fb_return(fb);
delay(100); // 约10fps
}
}
4、摄像头引脚配置
代码开头就是一堆的变量定义,一开始我也不知道是什么意思,AI解释后才知道,是GPIO口的定义,这里我才逐渐醒悟过来,从淘宝店买板子的时候,一般会介绍哪个针脚接主板上哪个引脚,但是SD卡和摄像头,是没有介绍的,可能是要查看具体的引脚定义,这里一开始我不知道,摄像头引脚都是靠运气试出来的,后来我才发现,原来ESP32-S3这类板子有两种类型,一种是AI-Thinker版本,一种是Freenove ,AI的解释如下:
Freenove 有两款产品 :
-
ESP32-S3-WROOM:带摄像头接口,但引脚定义与 AI-Thinker 不同
-
ESP32-S3-CAM:通常指 AI-Thinker 形式的板子

从板子上看到印的是ESP32-S3-CAM
所以摄像头的参数配置应该按照 AI-Thinker版本来配置,不能按照Freenove版本来配置,二者在控制信号上相同,但是数据段不同。
Freenove vs AI-Thinker 引脚差异对比
| 引脚 | AI-Thinker | Freenove | 说明 |
|---|---|---|---|
| CAM_D7 | GPIO 16 | GPIO 11 | ⚠️ 不同 |
| CAM_D6 | GPIO 17 | GPIO 9 | ⚠️ 不同 |
| CAM_D5 | GPIO 18 | GPIO 8 | ⚠️ 不同 |
| CAM_D4 | GPIO 12 | GPIO 10 | ⚠️ 不同 |
| CAM_D3 | GPIO 10 | GPIO 12 | ⚠️ 不同 |
| CAM_D2 | GPIO 8 | GPIO 18 | ⚠️ 不同 |
| CAM_D1 | GPIO 9 | GPIO 17 | ⚠️ 不同 |
| CAM_D0 | GPIO 11 | GPIO 16 | ⚠️ 不同 |
| CAM_XCLK | GPIO 15 | GPIO 15 | ✅ 相同 |
| CAM_SIOD | GPIO 4 | GPIO 4 | ✅ 相同 |
| CAM_SIOC | GPIO 5 | GPIO 5 | ✅ 相同 |
| CAM_VSYNC | GPIO 6 | GPIO 6 | ✅ 相同 |
| CAM_HREF | GPIO 7 | GPIO 7 | ✅ 相同 |
| CAM_PCLK | GPIO 13 | GPIO 13 | ✅ 相同 |
AI给我做了个对比,因为手头没有丝印是ESP32-S3-WROOM的板子,所以也没法验证。
但经过测试,确实用AI-Thinker 标准这套引脚定义的参数才能运行成功,用Freenove这套参数会提示 拍照失败:获取帧缓冲区失败
| 特征 | 我这块板子 | 真正的 Freenove ESP32-S3-WROOM |
|---|---|---|
| 摄像头数据线 | D0-D7 使用 GPIO 8-12,16-18 (AI-Thinker 标准) | D0-D7 使用 GPIO 8,9,10,11,12,16,17,18 (顺序不同) |
| 控制信号 | GPIO 4,5,6,7,13,15 (与 Freenove 相同) | GPIO 4,5,6,7,13,15 |
这个时候再来看看代码里引脚的定义:
// ========== AI-Thinker ESP32-S3-CAM 引脚配置 ==========
#define CAM_PWDN -1
#define CAM_RESET -1
#define CAM_XCLK 15
#define CAM_SIOD 4
#define CAM_SIOC 5
#define CAM_D7 16
#define CAM_D6 17
#define CAM_D5 18
#define CAM_D4 12
#define CAM_D3 10
#define CAM_D2 8
#define CAM_D1 9
#define CAM_D0 11
#define CAM_VSYNC 6
#define CAM_HREF 7
#define CAM_PCLK 13
// ========== Freenove ESP32-S3-CAM 引脚配置 ==========
// #define CAM_PWDN -1
// #define CAM_RESET -1
// #define CAM_XCLK 15
// #define CAM_SIOD 4
// #define CAM_SIOC 5
// #define CAM_D7 11 // Freenove: D7=GPIO11 (AI-Thinker: GPIO16)
// #define CAM_D6 9 // Freenove: D6=GPIO9 (AI-Thinker: GPIO17)
// #define CAM_D5 8 // Freenove: D5=GPIO8 (AI-Thinker: GPIO18)
// #define CAM_D4 10 // Freenove: D4=GPIO10 (AI-Thinker: GPIO12)
// #define CAM_D3 12 // Freenove: D3=GPIO12 (AI-Thinker: GPIO10)
// #define CAM_D2 18 // Freenove: D2=GPIO18 (AI-Thinker: GPIO8)
// #define CAM_D1 17 // Freenove: D1=GPIO17 (AI-Thinker: GPIO9)
// #define CAM_D0 16 // Freenove: D0=GPIO16 (AI-Thinker: GPIO11)
// #define CAM_VSYNC 6
// #define CAM_HREF 7
// #define CAM_PCLK 13
5、代码运行结果
Build:Mar 27 2021
rst:0x1 (POWERON),boot:0x8 (SPI_FAST_FLASH_BOOT)
SPIWP:0xee
mode:DIO, clock div:1
load:0x3fce3818,len:0x508
load:0x403c9700,len:0x4
load:0x403c9704,len:0xad0
load:0x403cc700,len:0x29e4
entry 0x403c9880
ESP32-S3-CAM OV3660 启动中...
摄像头传感器配置完成
传感器 PID: 0x3660
摄像头初始化成功
连接 WiFi....
WiFi 已连接,IP: 10.172.134.242
Web 服务器已启动
访问 http://10.172.134.242
拍照成功: 49139 bytes, 分辨率: 1600x1200
自动拍照完成
拍照成功: 49190 bytes, 分辨率: 1600x1200
自动拍照完成
拍照成功: 49270 bytes, 分辨率: 1600x1200
自动拍照完成
访问 http://10.172.134.242
浏览器打开 输入上面网址,即可看到拍的图片。

6、AI说板子是混合克隆板
前面帖子里我测试出来的SD卡的引脚是Freenove标准,但是这次摄像头的引脚测试出来又是AI-Thinker标准,很奇怪,我也问了下AI,答案是 板子是混合版。
| 功能模块 | 实际使用的引脚标准 | 说明 |
|---|---|---|
| 摄像头 (Camera) | AI-Thinker | D0-D7 使用 GPIO 8-12,16-18 |
| SD卡 (SD Card) | Freenove | 使用 GPIO 38,39,40(与摄像头无冲突) |
| 控制信号 | 两者相同 | XCLK/SCCB/VSYNC 等使用 GPIO 4,5,6,7,13,15 |
AI还给出了为什么要这样设计的答案,好像很有道理,但作为硬件小白,当前我还无法判断真假,新记录下来再说。
7、为什么板子要这样设计?
这是一个聪明的 兼容性优化设计:
-
摄像头用 AI-Thinker 标准:因为 OV2640/3660 摄像头模组最普及,AI-Thinker 的引脚定义是社区标准
-
SD卡用 Freenove 标准:避开 AI-Thinker SD卡的引脚冲突问题(GPIO 15 与 CAM_XCLK 冲突)
这样设计的好处:
-
✅ 摄像头兼容性最强(AI-Thinker 示例代码直接可用)
-
✅ SD卡不占用摄像头引脚(GPIO 38-40 专用)
-
✅ 可以同时使用摄像头 + SD卡 无冲突
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)