ESP32-S3 CAM 刷入 MicroPython 摄像头固件、OV3660 拍照排坑及 HTTP MJPEG 局域网视频串流的完整过程。

ESP32-S3 CAM (OV3660) MicroPython 驱动与串流实战
/ Update
7 mins
1499 words
Loading views

固件准备与烧录h2

折腾前先准备好工具链:

这里有个坑:千万别图省事直接刷官方普通的 ESP32_GENERIC_S3 固件,里面没有编译 esp32-camera 驱动和软解模块。直接去 MicroPython Camera API 下载适配好 Freenove 的发布包:

mpy_cam-v1.27.0-FREENOVE_ESP32S3_CAM.zip

解压得到 firmware.bin。打开终端,把 COM10 换成你电脑识别到的实际串口,先全盘擦除 Flash 再写入固件:

Terminal window
# 1. 擦除 Flash
esptool --chip esp32s3 --port COM10 erase-flash
# 2. 写入固件
esptool --chip esp32s3 --port COM10 --baud 460800 write-flash 0x0 firmware.bin

烧录完成打开串口监视器复位开发板,看到类似下面的输出,确认识别出 ESP32-S3 且挂载了 8 MB PSRAM 即说明刷入成功:

image-20260907161044124

引脚配置与拍照验证h2

Freenove ESP32-S3 CAM 的 OV3660 引脚映射如下,代码初始化时需要严格对应:

功能GPIO
D0~D711, 9, 8, 10, 12, 18, 17, 16
VSYNC6
HREF7
PCLK13
SDA/SIOD4
SCL/SIOC5
XCLK15
PWDN-1
RESET-1

这颗 OV3660 如果在 MicroPython 中直接开启硬件 JPEG(PixelFormat.JPEG),极易抛出 Failed to capture initial frame 导致初始化崩溃。目前最稳定的方案是让传感器以 PixelFormat.RGB565 输出原始帧,再调用固件内置的 jpeg 模块做软件压缩编码。

将以下测试脚本保存为 camera_test.py,用 MicroForge 上传并运行:

import gc
import jpeg
from camera import Camera, PixelFormat, FrameSize, GrabMode
cam = Camera(
data_pins=[11, 9, 8, 10, 12, 18, 17, 16],
vsync_pin=6,
href_pin=7,
pclk_pin=13,
sda_pin=4,
scl_pin=5,
xclk_pin=15,
xclk_freq=20_000_000,
pixel_format=PixelFormat.RGB565,
frame_size=FrameSize.XGA, # 1024x768;不稳定时改为 VGA
fb_count=1,
grab_mode=GrabMode.WHEN_EMPTY,
)
# OV3660 图像调节;这些属性在 v0.6.2 中可用
cam.sharpness = 1
cam.denoise = 1
cam.contrast = 1
cam.saturation = 0
cam.brightness = 0
cam.exposure_ctrl = True
cam.gain_ctrl = True
cam.whitebal = True
cam.awb_gain = True
cam.aec2 = True
cam.lenc = True
cam.bpc = True
cam.wpc = True
cam.raw_gma = True
# 丢弃启动后的不稳定画面(传感器刚通电需要给自动曝光和白平衡几帧收敛时间)
for _ in range(5):
cam.capture()
cam.free_buffer()
width = 512
height = 512
raw = cam.capture()
try:
encoder = jpeg.Encoder(
width=width,
height=height,
pixel_format="RGB565_BE",
quality=92,
rotation=0,
)
image = encoder.encode(raw)
finally:
cam.free_buffer()
with open("/photo.jpg", "wb") as file:
file.write(image)
print("照片保存成功:", len(image), "字节")
print("剩余内存:", gc.mem_free())

顺带一提,如果 XGA(1024×768)在抓取时偶发内存不足或丢帧,把分辨率切到 VGA 会轻量许多:

frame_size=FrameSize.VGA
width = 640
height = 480

拍摄成像质量受镜头焦距和光照影响明显。这类定焦摄像头出厂通常聚焦在 1~2 米位置,可对准带文字的物体轻微旋动镜头调焦(注意:若镜头螺纹被点胶锁死切勿大力硬拧):

2026-09-07_16-17-10

局域网视频串流h2

视频串流走的是经典的 HTTP MJPEG 方案,核心流向清晰明了:

RGB565 原生帧 → jpeg.Encoder 软件压缩 → multipart/x-mixed-replace 响应流 → 浏览器前端

分辨率建议首选 VGA(640×480)或 QVGA(320×240)。XGA 分辨率下 ESP32-S3 进行软件 JPEG 压缩负担较重,帧率会直接跌入幻灯片水平。

将以下代码保存为 main.py,填入你家里的 Wi-Fi 信息(ESP32 只支持 2.4 GHz 频段)后上传板子:

import gc
import time
import network
import socket
import jpeg
from camera import Camera, PixelFormat, FrameSize, GrabMode
WIFI_SSID = "你的WiFi名称"
WIFI_PASSWORD = "你的WiFi密码"
WIDTH = 640
HEIGHT = 480
JPEG_QUALITY = 88
def send_all(sock, data):
view = memoryview(data)
while len(view):
count = sock.send(view)
if not count:
raise OSError("客户端断开")
view = view[count:]
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(WIFI_SSID, WIFI_PASSWORD)
timeout = 20
while not wlan.isconnected() and timeout:
time.sleep(1)
timeout -= 1
if not wlan.isconnected():
raise RuntimeError("Wi-Fi 连接失败")
ip = wlan.ifconfig()[0]
print("Wi-Fi IP:", ip)
cam = Camera(
data_pins=[11, 9, 8, 10, 12, 18, 17, 16],
vsync_pin=6,
href_pin=7,
pclk_pin=13,
sda_pin=4,
scl_pin=5,
xclk_pin=15,
xclk_freq=20_000_000,
pixel_format=PixelFormat.RGB565,
frame_size=FrameSize.VGA,
fb_count=1,
grab_mode=GrabMode.WHEN_EMPTY,
)
cam.sharpness = 1
cam.denoise = 1
cam.contrast = 1
cam.exposure_ctrl = True
cam.gain_ctrl = True
cam.whitebal = True
cam.awb_gain = True
cam.aec2 = True
cam.lenc = True
cam.bpc = True
cam.wpc = True
cam.raw_gma = True
encoder = jpeg.Encoder(
width=WIDTH,
height=HEIGHT,
pixel_format="RGB565_BE",
quality=JPEG_QUALITY,
rotation=0,
)
for _ in range(5):
cam.capture()
cam.free_buffer()
server = socket.socket()
try:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
except Exception:
pass
server.bind(("0.0.0.0", 80))
server.listen(1)
html = (
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Connection: close\r\n\r\n"
"<html><body style='margin:0;background:#111;text-align:center'>"
"<img src='/stream' style='max-width:100%;height:auto'>"
"</body></html>"
).encode()
print("浏览器访问: http://%s/" % ip)
while True:
client = None
try:
client, address = server.accept()
request = client.recv(1024)
if b"GET /stream " not in request:
send_all(client, html)
else:
send_all(client,
b"HTTP/1.1 200 OK\r\n"
b"Content-Type: multipart/x-mixed-replace; boundary=frame\r\n"
b"Cache-Control: no-cache\r\n"
b"Connection: close\r\n\r\n")
while True:
raw = cam.capture()
try:
image = encoder.encode(raw)
finally:
cam.free_buffer()
header = ("--frame\r\n"
"Content-Type: image/jpeg\r\n"
"Content-Length: %d\r\n\r\n" % len(image)).encode()
send_all(client, header)
send_all(client, image)
send_all(client, b"\r\n")
del image
gc.collect()
except OSError as error:
print("客户端断开:", error)
except Exception as error:
print("串流错误:", error)
finally:
if client:
try:
client.close()
except Exception:
pass

在同一局域网下的电脑或手机浏览器输入终端打印的 http://[设备IP]/,就能看到低延迟的实时监控画面:

2026-09-07_16-25-29

避坑指南与排错调优h2

调试过程中遇到的几个常见问题与解决思路:

  • 抓帧失败 Failed to capture initial frame:仔细核对 GPIO 引脚映射与 XCLK 时钟。推荐将 xclk_freq 固定在 20_000_000,抓帧模式设为 GrabMode.WHEN_EMPTY,帧缓冲数量 fb_count=1。排查时先换用 VGA 或 QVGA 分辨率定位是否为传感器供电或带宽问题。
  • 格式报错 Format RGB565 not supported:调用内置 jpeg.Encoder 时,像素格式必须显式传大端序 "RGB565_BE",不要写成 "RGB565",否则编码器会因为不认识格式直接报错。
  • 属性报错 Camera object has no attribute 'aec':固件集成的 camera 库在 v0.6.2 版本对曝光控制属性做了重构,旧教程里的 cam.aeccam.agccam.awb 已变更为:
    cam.exposure_ctrl = True
    cam.gain_ctrl = True
    cam.whitebal = True
  • 画面模糊与调参:QVGA 拉伸到全屏必然有马赛克,优先选用 VGA;JPEG_QUALITY 设在 88~92 之间能在体积和细节间取得平衡;开启 sharpness=1denoise=1 可以压制暗部噪点;另外检查镜头出厂膜是否撕掉,并保证使用供电充沛的 5V 接口,供电不足极易引起图像杂波。
  • 串流帧率调优策略:如果串流卡顿严重,可按阶梯降载:先将分辨率由 XGA 降为 VGA;再将 JPEG_QUALITY 从 88 降到 75~80;若网络条件严苛再考虑 QVGA。全程务必维持 fb_count=1,避免 PSRAM 缓冲和垃圾回收(GC)拖慢主循环。

相关项目参考: