前言
本文专为零基础新手打造,从硬件选型与接线、开发环境准备、代码编写与编译到最终访问摄像头流,全程保姆级教程。基于 ESP-IDF v5.1.6 开发,完美适配 OV3660 摄像头与微软 Edge 浏览器,代码可直接复制使用,一次性解决「编译报错」「SOI 标记缺失」「Edge 无法解析流」等核心问题。
一、核心目标
用 ESP32-CAM 开发板 + OV3660 摄像头,搭建一个 MJPEG 视频流服务器,通过 WiFi 连接后,在浏览器中输入 ESP32 局域网 IP,即可实时查看摄像头画面。
二、硬件架构与选型
1. 核心硬件清单(新手必买,无兼容问题)
| 硬件名称 | 规格要求 | 作用 |
|---|---|---|
| ESP32-CAM 开发板 | 搭载 ESP32-WROOM-32 核心,无 PSRAM 版本即可 | 核心控制单元,负责 WiFi 通信、摄像头驱动、HTTP 服务器运行 |
| OV3660 摄像头模块 | 适配 ESP32-CAM 引脚,自带排线 | 图像采集,输出 JPEG 格式图像 |
| ESP32-CAM 专用烧录座 | 适配 ESP32-CAM 引脚,支持一键烧录 | 替代 USB 转 TTL,简化烧录流程,无需手动接 IO0 |
| USB 数据线 | Micro USB 接口,适配烧录座 | 给烧录座供电,传输烧录数据 |
2. 硬件架构原理
3. 硬件实物图(新手直观参考)
以下是 ESP32-CAM + OV3660 + 烧录座的实物图,新手可直接对照购买:

4. 硬件接线(使用烧录座,零难度)
(1)ESP32-CAM 与烧录座接线
- 将 ESP32-CAM 开发板金手指朝下,插入烧录座的对应插槽;
- 确保 ESP32-CAM 与烧录座引脚完全对齐,无偏移。
(2)烧录座与 USB 数据线接线
直接将 Micro USB 数据线插入烧录座的 Micro USB 接口,另一端插入电脑 USB 口即可(无需额外接线)。
(3)ESP32-CAM 与 OV3660 接线
直接将 OV3660 自带的排线插入 ESP32-CAM 的摄像头接口即可(注意排线方向,金手指朝向开发板正面)。
三、开发环境准备(ESP-IDF v5.1.6)
已安装 ESP-IDF v5.1.6 的新手可跳过此步骤,直接进入「四、项目搭建」。
1. 下载并安装 ESP-IDF Tools
- 访问 ESP 官方下载页:https://dl.espressif.com/dl/esp-idf/
- 下载 ESP-IDF v5.1.6 对应的 Windows 安装包(
esp-idf-tools-setup-5.1.6.exe); - 双击安装,一路默认下一步,记住安装路径(例如
D:\ESP32\esp-idf\v5.1.6)。
2. 配置 VS Code 开发环境
- 安装 VS Code,在扩展商店搜索并安装 Espressif IDF 插件(作者:Espressif Systems);
- 打开 VS Code,按
Ctrl+Shift+P,输入ESP-IDF: Configure ESP-IDF Extension; - 选择「Use an existing ESP-IDF directory」,找到并选择刚才安装的 ESP-IDF v5.1.6 路径;
- 等待插件配置完成,底部状态栏显示「ESP-IDF: 5.1.6」即成功。
四、新手重建项目(核心步骤)
步骤 1:创建新项目
- 打开 VS Code,按
Ctrl+Shift+P,输入ESP-IDF: New Project; - 项目名称填写
web-camera-ov3660,保存路径自定义(例如D:\ESP32\web-camera-ov3660); - 模板选择
esp32→blank(空白项目),点击「Create」。
步骤 2:创建核心文件
在项目的 main 目录下,删除默认的 main.c,新建以下 6 个文件:
app_main.c、app_wifi.c、app_wifi.h、app_camera.c、app_camera.h、app_httpd.c、app_httpd.h。
步骤 3:复制代码(直接粘贴,无需修改)
所有代码已适配 ESP-IDF v5.1.6,解决「SOI 标记缺失」「Edge 兼容性」「编译报错」问题。
1. app_main.c(程序入口)
#include "esp_log.h"
#include "nvs_flash.h"
#include "app_wifi.h"
#include "app_camera.h"
#include "app_httpd.h"
static const char *TAG = "MAIN";
void app_main(void)
{
// 1. 初始化 NVS(WiFi 依赖)
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
ESP_ERROR_CHECK(err);
ESP_LOGI(TAG, "NVS 初始化成功");
// 2. 初始化 WiFi(直接调用,无返回值)
wifi_init_sta();
ESP_LOGI(TAG, "WiFi 初始化完成");
// 3. 初始化 OV3660 摄像头
err = app_camera_main();
if (err != ESP_OK) {
ESP_LOGE(TAG, "摄像头初始化失败!错误码:0x%x", err);
return;
}
ESP_LOGI(TAG, "摄像头初始化成功");
// 4. 启动 HTTP 服务器
httpd_handle_t server = start_webserver();
if (server == NULL) {
ESP_LOGE(TAG, "HTTP 服务器启动失败");
return;
}
ESP_LOGI(TAG, "系统初始化完成!访问:http://ESP32_IP/ 查看摄像头");
}
2. app_wifi.c(WiFi 连接实现) 替换为你的 WiFi 名称和密码
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_netif.h"
#include "esp_event.h"
#include "app_wifi.h"
static const char *TAG = "WIFI";
static EventGroupHandle_t s_wifi_event_group;
// ===================== 新手修改处 =====================
// 替换为你的 WiFi 名称和密码
#define WIFI_SSID "替换为你的 WiFi 名称"
#define WIFI_PASSWORD "替换为你的 WiFi 密码"
// =====================================================
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1
static void wifi_event_handler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data)
{
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
ESP_LOGW(TAG, "WiFi 断开,重试连接...");
esp_wifi_connect();
xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
ESP_LOGI(TAG, "WiFi 连接成功,IP地址:" IPSTR, IP2STR(&event->ip_info.ip));
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
}
void wifi_init_sta(void)
{
s_wifi_event_group = xEventGroupCreate();
// 初始化网络接口
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
// 初始化 WiFi 驱动
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
// 注册事件回调
esp_event_handler_instance_t instance_any_id;
esp_event_handler_instance_t instance_got_ip;
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
ESP_EVENT_ANY_ID,
&wifi_event_handler,
NULL,
&instance_any_id));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
IP_EVENT_STA_GOT_IP,
&wifi_event_handler,
NULL,
&instance_got_ip));
// 配置 WiFi 账号密码
wifi_config_t wifi_config = {
.sta = {
.ssid = WIFI_SSID,
.password = WIFI_PASSWORD,
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
// 等待 WiFi 连接完成
xEventGroupWaitBits(s_wifi_event_group,
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
pdFALSE,
pdFALSE,
portMAX_DELAY);
}
3. app_wifi.h(WiFi 函数声明)
#ifndef _APP_WIFI_H_
#define _APP_WIFI_H_
#ifdef __cplusplus
extern "C" {
#endif
void wifi_init_sta(void);
#ifdef __cplusplus
}
#endif
#endif /* _APP_WIFI_H_ */
4. app_camera.c(OV3660 摄像头驱动,解决 SOI 缺失)
#include "app_camera.h"
#include "esp_camera.h"
#include "driver/gpio.h"
#include "esp_log.h"
// ESP32-CAM 与 OV3660 引脚匹配
#define CAM_PIN_PWDN 32
#define CAM_PIN_RESET -1
#define CAM_PIN_XCLK 0
#define CAM_PIN_SIOD 26
#define CAM_PIN_SIOC 27
#define CAM_PIN_D7 35
#define CAM_PIN_D6 34
#define CAM_PIN_D5 39
#define CAM_PIN_D4 36
#define CAM_PIN_D3 21
#define CAM_PIN_D2 19
#define CAM_PIN_D1 18
#define CAM_PIN_D0 5
#define CAM_PIN_VSYNC 25
#define CAM_PIN_HREF 23
#define CAM_PIN_PCLK 22
static const char *TAG = "CAMERA";
esp_err_t app_camera_main(void)
{
// OV3660 专用配置(解决 SOI 标记缺失)
camera_config_t camera_config = {
.pin_pwdn = CAM_PIN_PWDN,
.pin_reset = CAM_PIN_RESET,
.pin_xclk = CAM_PIN_XCLK,
.pin_sccb_sda = CAM_PIN_SIOD,
.pin_sccb_scl = CAM_PIN_SIOC,
.pin_d7 = CAM_PIN_D7,
.pin_d6 = CAM_PIN_D6,
.pin_d5 = CAM_PIN_D5,
.pin_d4 = CAM_PIN_D4,
.pin_d3 = CAM_PIN_D3,
.pin_d2 = CAM_PIN_D2,
.pin_d1 = CAM_PIN_D1,
.pin_d0 = CAM_PIN_D0,
.pin_vsync = CAM_PIN_VSYNC,
.pin_href = CAM_PIN_HREF,
.pin_pclk = CAM_PIN_PCLK,
.xclk_freq_hz = 10000000, // 降频到 10MHz,稳定编码
.ledc_timer = LEDC_TIMER_0,
.ledc_channel = LEDC_CHANNEL_0,
.pixel_format = PIXFORMAT_JPEG, // 强制 JPEG 格式
.frame_size = FRAMESIZE_QVGA, // 320x240,适配无 PSRAM
.jpeg_quality = 10, // 降低质量,避免数据溢出
.fb_count = 2, // 双缓冲区,防止数据丢失
.fb_location = CAMERA_FB_IN_DRAM,
.grab_mode = CAMERA_GRAB_WHEN_EMPTY
};
// 初始化摄像头
esp_err_t err = esp_camera_init(&camera_config);
if (err != ESP_OK) {
ESP_LOGE(TAG, "摄像头初始化失败!错误码:0x%x", err);
return err;
}
// 强制锁定 JPEG 配置,杜绝 RAW 格式
sensor_t *s = esp_camera_sensor_get();
if (s) {
s->set_framesize(s, FRAMESIZE_QVGA);
s->set_pixformat(s, PIXFORMAT_JPEG);
ESP_LOGI(TAG, "OV3660 JPEG 配置生效");
} else {
ESP_LOGE(TAG, "获取摄像头传感器失败");
return ESP_FAIL;
}
ESP_LOGI(TAG, "摄像头初始化成功!");
return ESP_OK;
}
5. app_camera.h(摄像头函数声明)
#ifndef APP_CAMERA_H
#define APP_CAMERA_H
#include "esp_err.h"
esp_err_t app_camera_main(void);
#endif // APP_CAMERA_H
6. app_httpd.c(HTTP 服务器 + MJPEG 流,适配 Edge)
#include "esp_err.h"
#include "esp_http_server.h"
#include "esp_log.h"
#include "esp_camera.h"
#include "string.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "app_httpd.h"
static const char *TAG = "HTTPD";
// 主页 HTML(自带样式,适配浏览器)
static const char *STREAM_HTML =
"<!DOCTYPE html>"
"<html>"
"<head>"
"<title>ESP32 OV3660 Camera</title>"
"<meta charset=\"utf-8\">"
"<style>"
"body { margin: 0; padding: 20px; background-color: #f0f0f0; }"
".camera-container { max-width: 640px; margin: 0 auto; background: white; padding: 10px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }"
"img { width: 100%; height: auto; border-radius: 4px; }"
"h1 { text-align: center; color: #333; font-family: Arial, sans-serif; }"
"</style>"
"</head>"
"<body>"
"<div class=\"camera-container\">"
"<h1>ESP32 OV3660 摄像头画面</h1>"
"<img src=\"/stream\" alt=\"Camera Stream\">"
"</div>"
"</body>"
"</html>";
static esp_err_t index_handler(httpd_req_t *req)
{
httpd_resp_set_type(req, "text/html");
httpd_resp_send(req, STREAM_HTML, strlen(STREAM_HTML));
return ESP_OK;
}
// MJPEG 流核心处理(纯 ESP-IDF v5.1.6 支持 API)
static esp_err_t stream_handler(httpd_req_t *req)
{
camera_fb_t *fb = NULL;
esp_err_t res = ESP_OK;
char part_buf[128];
char conn_hdr[32];
TickType_t start_tick;
// Edge 浏览器兼容配置
httpd_resp_set_type(req, "multipart/x-mixed-replace; boundary=frame");
httpd_resp_set_hdr(req, "Cache-Control", "no-cache");
httpd_resp_set_hdr(req, "Connection", "close");
while (true) {
// 手动超时获取帧(替代高版本函数,避免编译报错)
start_tick = xTaskGetTickCount();
fb = NULL;
while (xTaskGetTickCount() - start_tick < 500 / portTICK_PERIOD_MS) {
fb = esp_camera_fb_get();
if (fb) break;
vTaskDelay(1 / portTICK_PERIOD_MS);
}
if (!fb) {
ESP_LOGE(TAG, "获取帧超时");
continue;
}
// 校验 JPEG 起始标记(0xFFD8),解决 SOI 缺失问题
if (fb->format == PIXFORMAT_JPEG && fb->buf[0] == 0xFF && fb->buf[1] == 0xD8) {
size_t hlen = snprintf(part_buf, sizeof(part_buf),
"--frame\r\n"
"Content-Type: image/jpeg\r\n"
"Content-Length: %zu\r\n\r\n",
fb->len);
res = httpd_resp_send_chunk(req, part_buf, hlen);
if (res != ESP_OK) break;
res = httpd_resp_send_chunk(req, (const char *)fb->buf, fb->len);
if (res != ESP_OK) break;
} else {
ESP_LOGE(TAG, "JPEG 数据异常,缺少 SOI 标记");
}
// 释放缓冲区,避免内存泄漏
if (fb) {
esp_camera_fb_return(fb);
fb = NULL;
}
// 检测客户端断开连接(ESP-IDF v5.1.6 标准写法)
ssize_t conn_hdr_len = httpd_req_get_hdr_value_len(req, "Connection");
if (conn_hdr_len > 0 && conn_hdr_len < sizeof(conn_hdr)) {
httpd_req_get_hdr_value_str(req, "Connection", conn_hdr, conn_hdr_len + 1);
if (strcmp(conn_hdr, "close") == 0) {
break;
}
}
}
// 清理资源
if (fb) esp_camera_fb_return(fb);
httpd_resp_send_chunk(req, "\r\n--frame--\r\n", strlen("\r\n--frame--\r\n"));
ESP_LOGI(TAG, "流连接断开");
return res;
}
httpd_handle_t start_webserver(void)
{
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.max_uri_handlers = 16;
config.server_port = 80;
httpd_handle_t server = NULL;
if (httpd_start(&server, &config) == ESP_OK) {
// 注册主页路由
httpd_uri_t index_uri = {
.uri = "/",
.method = HTTP_GET,
.handler = index_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(server, &index_uri);
// 注册视频流路由
httpd_uri_t stream_uri = {
.uri = "/stream",
.method = HTTP_GET,
.handler = stream_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(server, &stream_uri);
ESP_LOGI(TAG, "HTTP 服务器启动成功,端口:80");
} else {
ESP_LOGE(TAG, "HTTP 服务器启动失败");
}
return server;
}
void stop_webserver(httpd_handle_t server)
{
if (server) {
httpd_stop(server);
ESP_LOGI(TAG, "HTTP 服务器已停止");
}
}
7. app_httpd.h(HTTP 函数声明)
#ifndef APP_HTTPD_H
#define APP_HTTPD_H
#include "esp_http_server.h"
httpd_handle_t start_webserver(void);
void stop_webserver(httpd_handle_t server);
#endif // APP_HTTPD_H
步骤 4:修改 CMakeLists.txt(编译配置)
打开 main 目录下的 CMakeLists.txt,替换为以下内容:
idf_component_register(SRCS "app_main.c" "app_wifi.c" "app_camera.c" "app_httpd.c"
INCLUDE_DIRS ".")
五、编译与烧录(使用烧录座,新手零门槛)
步骤 1:配置串口与目标芯片
- 按
Ctrl+Shift+P,输入ESP-IDF: Select Port,选择烧录座对应的串口(例如 COM3); - 输入
ESP-IDF: Select Target,选择esp32。
步骤 2:清理并编译
在 VS Code 终端中执行以下命令(复制粘贴即可):
# 彻底清理旧缓存(解决编译残留问题)
idf.py fullclean
# 编译项目
idf.py build
编译成功会显示「Project build complete」,无任何报错。
步骤 3:烧录程序(烧录座专用)
- 将 ESP32-CAM 插入烧录座,确保引脚对齐;
- 用 USB 数据线连接烧录座与电脑,在终端执行:
idf.py flash
- 烧录完成后,按一下 ESP32-CAM 的复位键(RST),无需手动切换 IO0。
步骤 4:查看串口日志(获取 ESP32 IP)
在终端执行:
idf.py monitor
等待日志输出,找到以下关键信息:
I (5471) WIFI: WiFi 连接成功,IP地址:192.168.31.179
I (5941) MAIN: 系统初始化完成!访问:http://ESP32_IP/ 查看摄像头
记录下你的 ESP32 IP(例如 192.168.31.179)。
六、查看摄像头画面(最终验证)
- 确保电脑/手机与 ESP32 连接同一个 WiFi;
- 打开微软 Edge/谷歌 Chrome 浏览器,输入刚才记录的 IP(例如
http://192.168.31.179); - 按
Ctrl+F5强制刷新(避免浏览器缓存),即可看到实时摄像头画面! - 画面如下

七、新手常见问题排查
| 问题现象 | 原因 | 解决方法 |
|---|---|---|
| 编译报错「unknown type name」 | 头文件缺失/顺序错误 | 严格按本文代码的头文件顺序引入 |
| 日志显示「NO-SOI」 | OV3660 编码异常 | 检查 app_camera.c 中 xclk_freq_hz 是否为 10MHz,fb_count 是否为 2 |
| Edge 只显示标题,不显示画面 | 浏览器缓存/流格式不兼容 | 按 Ctrl+F5 强制刷新,关闭 Edge 安全增强模式 |
| WiFi 连接失败 | 账号密码错误/信号差 | 检查 app_wifi.c 中的 WIFI_SSID 和 WIFI_PASSWORD,靠近路由器 |
| 烧录失败 | 烧录座接触不良/串口错误 | 重新插拔 ESP32-CAM,重新选择正确串口 |
结语
本文从硬件到软件,全程为新手打造,代码经过实测验证,完美适配 ESP-IDF v5.1.6、OV3660 摄像头与 Edge 浏览器。新手只需按步骤复制代码、接线、编译,即可快速搭建属于自己的 ESP32 摄像头流服务器,后续可在此基础上扩展人脸识别、视频录制等功能。
源码
已上传Gitee
https://gitee.com/yunjingshan/esp32-ov3660-mjpeg-server
&spm=1001.2101.3001.5002&articleId=158542939&d=1&t=3&u=b267a3800712466598c8ca6ccff005e6)
1338

被折叠的 条评论
为什么被折叠?



