← 返回机器人编程
ESP32-S3 实战

📡 ESP32-S3 开发实战

从入门到项目落地,每天一篇 ESP32-S3 硬核实战教程,搭配精选视频

ESP32-S3 摄像头:OV2640 实时视频流

🎬 精选教程

1. 概述

摄像头是物联网设备最性感的外设之一。试想一下:一个饼干盒大小的设备,通上电就能在浏览器里看到实时画面——这就是今天我们要实现的东西。

ESP32-S3 搭配 OV2640 摄像头模块,几乎是最低成本实现 WiFi 视频流的方案组合。OV2640 是 OmniVision 出品的一款 200 万像素(UXGA,1600×1200)图像传感器,支持 JPEG 硬件压缩,帧率最高可达 60fps(在较低分辨率下)。配合 ESP32-S3 的 LX7 双核处理器和内置的相机接口,搭建一个 Web 视频流服务器简直是天作之合。

今天我们将从零搭建一个完整的 ESP32-CAM 视频流项目,涵盖硬件接线、Arduino 框架配置、Web 服务器代码编写到浏览器端实时播放的全流程。你将得到一个能在局域网内通过浏览器访问的实时监控摄像头。

2. 硬件准备

开始之前,先确认你手头的硬件齐了没有:

  • ESP32-S3 开发板(推荐 ESP32-S3-DevKitC-1 或 ESP32-S3-N16R8)—— 注意选引脚引出比较全的板子
  • OV2640 摄像头模块(带 FPC 排线,通常和 ESP32-CAM 一起买的那种)
  • ESP32-CAM 主板(如果直接用 AI-Thinker ESP32-CAM 模组,相当于摄像头和主板一体,更方便)
  • USB 转 TTL 下载器(CH340 / CP2102,部分板子有板载 USB,但 ESP32-CAM 通常没有)
  • 5V 电源(摄像头模块电流较大,建议用独立供电,不要只靠 USB 口)
  • 杜邦线若干(母对母为主)

如果是 AI-Thinker ESP32-CAM,OV2640 已经通过 FPC 排线连好了,你只需要接电源和 UART 线就能开始编程。如果是自己用 ESP32-S3 开发板 搭,接线方式如下:

OV2640 → ESP32-S3
SIOC   → GPIO12 (SCL)
SIOD   → GPIO11 (SDA)
VSYNC  → GPIO46 (VSYNC)
HREF   → GPIO21 (HREF)
PCLK   → GPIO13 (PCLK)
XCLK   → GPIO15 (XCLK)
D7     → GPIO48 (Y9)
D6     → GPIO47 (Y8)
D5     → GPIO36 (Y7)
D4     → GPIO39 (Y6)
D3     → GPIO40 (Y5)
D2     → GPIO41 (Y4)
D1     → GPIO42 (Y3)
D0     → GPIO45 (Y2)
RESET  → GPIO14 (RESET)
PWDN   → -1 (不使用)

⚠️ 不同版本的 ESP32-S3 开发板引脚定义可能不同,务必对照板子的 pins_arduino.h 确认。ESP32-CAM 模组(如 AI-Thinker)有固定的引脚映射,稍后代码里有预设的 camera_pins 可以直接用。

3. 代码实现

我们将基于 Arduino 框架编写代码。使用 ESP32 官方的 esp32-camera 库,这是 Espressif 官方维护的摄像头驱动。

3.1 安装依赖库

在 Arduino IDE 或 PlatformIO 中添加库:

  • esp32-camera — 搜索 "ESP32 Camera" by Espressif Systems
  • WebServer — Arduino 内置(ESP32 核心库自带)

3.2 完整代码

#include "esp_camera.h"
#include 
#include 

// ===== WiFi 配置 =====
const char* ssid = "你的WiFi名称";
const char* password = "你的WiFi密码";

// ===== 摄像头引脚配置(AI-Thinker ESP32-CAM)=====
#define PWDN_GPIO_NUM     32
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM      0
#define SIOD_GPIO_NUM     26
#define SIOC_GPIO_NUM     27
#define Y9_GPIO_NUM       35
#define Y8_GPIO_NUM       34
#define Y7_GPIO_NUM       39
#define Y6_GPIO_NUM       36
#define Y5_GPIO_NUM       21
#define Y4_GPIO_NUM       19
#define Y3_GPIO_NUM       18
#define Y2_GPIO_NUM        5
#define VSYNC_GPIO_NUM    25
#define HREF_GPIO_NUM     23
#define PCLK_GPIO_NUM     22

WebServer server(80);

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);

  // 1. 初始化摄像头
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer   = LEDC_TIMER_0;
  config.pin_d0       = Y2_GPIO_NUM;
  config.pin_d1       = Y3_GPIO_NUM;
  config.pin_d2       = Y4_GPIO_NUM;
  config.pin_d3       = Y5_GPIO_NUM;
  config.pin_d4       = Y6_GPIO_NUM;
  config.pin_d5       = Y7_GPIO_NUM;
  config.pin_d6       = Y8_GPIO_NUM;
  config.pin_d7       = Y9_GPIO_NUM;
  config.pin_xclk     = XCLK_GPIO_NUM;
  config.pin_pclk     = PCLK_GPIO_NUM;
  config.pin_vsync    = VSYNC_GPIO_NUM;
  config.pin_href     = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn     = PWDN_GPIO_NUM;
  config.pin_reset    = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_JPEG;
  config.frame_size   = FRAMESIZE_QVGA;
  config.jpeg_quality = 12;
  config.fb_count     = 2;  // 双缓冲,提高帧率

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

  // 2. 连接 WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\n✅ WiFi 连接成功");
  Serial.print("IP 地址: ");
  Serial.println(WiFi.localIP());

  // 3. 注册 HTTP 路由
  server.on("/", HTTP_GET, handleRoot);
  server.on("/stream", HTTP_GET, handleStream);
  server.on("/capture", HTTP_GET, handleCapture);
  server.onNotFound(handleNotFound);

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

// ===== HTTP 处理函数 =====

/** 主页 */
void handleRoot() {
  String html = R"rawliteral(



ESP32-CAM 实时监控



📷 ESP32-S3 实时视频流

点击下方按钮切换模式

)rawliteral"; server.send(200, "text/html", html); } /** 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"; response += "Cache-Control: no-cache\r\n"; response += "Connection: close\r\n\r\n"; client.write(response.c_str(), response.length()); while (client.connected()) { camera_fb_t* fb = esp_camera_fb_get(); if (!fb) { Serial.println("获取帧失败"); continue; } char head[64]; size_t hlen = snprintf(head, 64, "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: %zu\r\n\r\n", fb->len); client.write(head, hlen); client.write(fb->buf, fb->len); client.write("\r\n", 2); esp_camera_fb_return(fb); delay(50); // ~20fps 限制 } } /** 单帧抓拍 */ void handleCapture() { camera_fb_t* fb = esp_camera_fb_get(); if (!fb) { server.send(500, "text/plain", "相机错误"); return; } server.send_P(200, "image/jpeg", (const char*)fb->buf, fb->len); esp_camera_fb_return(fb); } /** 404 */ void handleNotFound() { server.send(404, "text/plain", "404 Not Found"); } void loop() { server.handleClient(); }

4. 原理分析

这个项目看似复杂,核心原理其实就三个层次:

4.1 图像采集层

OV2640 传感器通过 SCCB 总线(类似 I2C)接收配置指令。ESP32-S3 的相机接口(Camera Interface)接收 8 位并行数据,包含像素时钟 PCLK、行同步 HREF、帧同步 VSYNC 等信号。关键参数:

  • XCLK:外部时钟输入,通常设为 20MHz,越高帧率越快但功耗也越大
  • pixel_format:设为 PIXFORMAT_JPEG 时,OV2640 内部 JPEG 编码器直接输出压缩好的 JPEG 数据流,大大减轻 ESP32 的 CPU 负担。如果设成 RGB565 或 YUV422,则输出裸数据,CPU 需要自己做压缩(不推荐)
  • fb_count = 2:双缓冲机制。ESP32 在写新帧到其中一个缓冲区的同时,CPU 可以读取另一个缓冲区的内容发送给浏览器。单缓冲会导致发送期间无法采集新帧,帧率砍半

4.2 传输层

视频流用的是 MJPEG over HTTP(Motion JPEG),这是一种很古老但极度简单的视频流协议。核心是 HTTP 的 multipart/x-mixed-replace 响应类型——服务端持续发送 JPEG 帧,每帧之间用自定义 boundary 字符串分隔:

--frame
Content-Type: image/jpeg

[JPEG 二进制数据]
--frame
Content-Type: image/jpeg

[JPEG 二进制数据]
...

浏览器的 <img> 标签天然支持这个协议:只要 src 指向一个返回 multipart 响应的 URL,浏览器会持续更新图片内容,形成"伪视频流"效果。这种方式虽然不如 WebRTC 或 HLS 高级,但兼容性极好——任何浏览器打开就能看,无需任何插件。

4.3 资源管理

ESP32-S3 的 PSRA(伪静态 RAM)是视频流的关键。外部 PSRAM 不够快、延迟大,但容量足够存放多帧 JPEG 数据。O(1) 的帧缓冲区分配开销至关重要:

  • esp_camera_fb_get() 从双缓冲中获取当前完成帧
  • esp_camera_fb_return() 将缓冲区归还给驱动
  • 务必保证每次 get 都有对应的 return,否则内存泄漏

5. 扩展与优化

基础视频流跑通后,以下是几个值得一试的升级方向:

5.1 分辨率与帧率调优

camera_config_t 中调整:

  • frame_size:FRAMESIZE_QVGA (320×240) 最流畅,FRAMESIZE_VGA (640×480) 画质可接受,FRAMESIZE_UXGA (1600×1200) 帧率很低
  • jpeg_quality:0-63,值越小质量越高。实测 QVGA 下 quality=10 画质/速度比最优
  • WiFi 带宽瓶颈:QVGA/30fps 约需 2-3Mbps 带宽,VGA/15fps 约需 5-8Mbps

5.2 添加 PTZ 控制

用两个 SG90 舵机做云台:水平 180° + 垂直 180°。通过 HTTP GET 参数控制 PWM 占空比:

http://esp32-ip/pan?angle=90
http://esp32-ip/tilt?angle=45

舵机的控制引脚建议用 LEDC PWM 通道输出,注意不要和摄像头引脚冲突。

5.3 运动检测(AI 方向)

ESP32-S3 内置向量扩展指令集(PIE),可以做轻量级的帧差运动检测:

  • 缓存上一帧的灰度数据
  • 当前帧逐像素做差分
  • 超过阈值的像素点超过一定数量即触发"检测到运动"
  • 触发后发送告警或录制片段到 SD 卡

5.4 WebRTC 低延迟流(高阶)

MJPEG 流延迟约 200-500ms,WebRTC 可以将延迟压到 50-100ms。ESP32-S3 跑 WebRTC 支持 libPeerConnection 或 esp-webrtc 库,但需要至少 8MB PSRAM 和一定量的代码优化。

6. 踩坑记录

🙈 以下是亲身踩过的坑,写下来帮你省半天调试时间:

坑 1:白屏无画面,摄像头初始化失败

现象:串口输出 Camera init failed with error 0x20004 或别的错误码。

原因:最常见的是坏卡、FPC 排线未插紧或插反。OV2640 的 FPC 排线有金属触点的一面朝下(朝向 PCB),反了也能插进去但完全不通。

解法:用万用表测量摄像头模块 VCC 和 GND 之间的电压,确认板子供了 3.3V。再检查排线方向。如果确定接线正确,换一个摄像头模块试试——OV2640 良品率不是 100%,坏的不少。

坑 2:WiFi 连接后流不稳定,画面卡顿

现象:刚刷新时能显示几帧,然后卡死或者非常慢。

原因:常见有三个原因。第一,WiFi 信号弱——ESP32 在 2.4GHz 下信号穿透力有限。第二,电源电流不足——OV2640 在采集 JPEG 时需要突然的大电流,如果 USB 供电不稳会掉帧。第三,delay() 设置太短——handleStream() 中的 delay(0) 会让 CPU 疯狂发送帧,导致 TCP 缓冲区满、丢包重传。

解法:保持 ESP32 靠近路由器。供电用 5V/2A 适配器。delay 设到 50-100ms 获得稳定帧率。

坑 3:PSRAM 不识别,编译不通过

现象:编译报 Camera not found. PSRAM is required 或内存分配失败。

解法:在 Arduino IDE 中选择正确的开发板配置。AI-Thinker ESP32-CAM 需要在 Tools 菜单开启:PSRAM → EnabledFlash Mode → QIO。PlatformIO 中在 board_build.arduino.menu.PSRAM.enabled 项设为 true

坑 4:手机浏览器看不到流

现象:电脑 Chrome 正常,iPhone Safari 白屏。

原因:iOS Safari 对 multipart/x-mixed-replace 支持很有限。2019 年后 Safari 在 <img> 标签中不再刷新 multipart 流。

解法:方案一,用 <video> + JavaScript fetch() 流式读取并绘制到 canvas。方案二,改用 WebRTC。对大多数非 iOS 场景,当前方案够用了。

坑 5:闪光灯 LED 爆炸

现象:ESP32-CAM 的板载闪光灯 LED 特别亮,长时间亮会过热,甚至把板子烧坏。

解法:快门同步模式下才点亮,不要常亮。在代码中通过 GPIO4 控制,拍照前点亮,拍照后立即熄灭。或者干脆把 LED 焊掉。


以上就是 ESP32-S3 + OV2640 实时视频流从零开始的完整实现。从硬件接线到代码编写再到原理理解和踩坑记录,一套下来你应该能稳稳抓出一台自己的 WiFi 监控摄像头。下期我们进阶:聊一聊如何在视频流上叠加 OSD 文字,或者用 ESP-NOW 做多摄像头组网。有什么想看的主题,评论区支个声~

📋 全部文章
#1
ESP32-S3 机器狗实战(一):硬件选型与系统架构
2026年5月3日 · 项目
#2
ESP32-S3 GPIO 编程:点亮你的第一盏LED
2026年5月4日 · 基础
#3
ESP32-S3 ADC 与传感器:读取模拟世界的信号
2026年5月5日 · 传感器
#4
ESP32-S3 Wi-Fi 连接:让你的设备上网
2026年5月6日 · 网络
#5
ESP32-S3 HTTP 客户端:获取天气API数据
2026年5月7日 · 网络
#6
ESP32-S3 Web Server:自建控制面板
2026年5月8日 · 网络