ESP32-P4 野外数据集采集系统:MIPI-CSI摄像头 + ESP-Hosted WiFi + IndexedDB浏览器端完整方案
项目背景
在边缘 AI 开发中,真实场景数据集的质量往往比模型结构更重要。训练一个能在野外识别垃圾类别的 MobileNet 检测器,如果只用 ImageNet 预训练权重做迁移学习,缺乏目标场景的真实数据,实际部署效果必然大打折扣。
本项目从零构建了一条完整的数据采集流水线:
OV5647 摄像头 → MIPI-CSI 接收 → ISP 管线处理 → 硬件 JPEG 编码 → HTTP 服务器 → 浏览器端 IndexedDB 持久化存储
选用微雪 ESP32-P4-WIFI6 开发板作为目标硬件,利用 ESP32-P4 内置的 MIPI-CSI 接口、硬件 ISP 管线、硬件 JPEG 编码器,配合 ESP32-C6 协处理器提供的 WiFi 6 连接,构建一个低成本、低功耗、可独立工作的野外图像采集终端。
关键指标:
| 指标 | 数值 |
|---|---|
| 处理器 | ESP32-P4 双核 RISC-V @360MHz |
| 摄像头 | OV5647 500万像素, MIPI-CSI 2-lane |
| 输出格式 | JPEG, 1280×960, Q60-Q95 |
| PSRAM | 32MB Octal SPI @200MHz |
| WiFi | ESP32-C6 WiFi 6, SDIO 4-bit @40MHz |
| JPEG 上限 | 180KB per frame, 自动降质保护 |
| MJPEG 帧率 | ~5fps @Q60 |
| 浏览器存储 | IndexedDB, 支持 ZIP 批量导出 |
硬件架构全景
1 | |
为什么选 ESP32-P4?
- ESP32-P4 是目前 Espressif 唯一支持 MIPI-CSI 摄像头接口的高性能 MCU
- 内置 硬件 ISP 管线(Demosaic → AWB → Color Correction),无需 CPU 干预
- 内置 硬件 JPEG 编码器,1280×960 帧编码仅需几毫秒
- SDIO 4-bit 40MHz 高速外设接口,能与 ESP32-C6 协处理器高效通信
为什么 WiFi 要独立用 ESP32-C6?
ESP32-P4 自身没有 WiFi 射频。Espressif 的 ESP-Hosted 框架允许 P4 通过 SDIO/SPI/UART 连接一个 ESP32-C6 作为 WiFi/BLE 协处理器,C6 运行从机固件,P4 通过 RPC 调用 WiFi 功能。
软件架构分层
graph TB
A[app_main.c<br/>入口: NVS→摄像头→WiFi→OTA→HTTP→按键] --> B[camera_hal.c<br/>V4L2 + ISP + HW JPEG]
A --> C[wifi_manager.c<br/>STA/AP 自动切换]
A --> D[ota_partition.c<br/>C6 从机分区 OTA]
A --> E[test_server.c<br/>HTTP :80 + :81 + SSE]
E --> F[web_ui.h<br/>内嵌 Web 前端]
B --> G[esp_video V4L2<br/>/dev/video]
B --> H[driver/jpeg_encode<br/>HW JPEG 编码器]
C --> I[esp_wifi_remote<br/>ESP-Hosted RPC]
D --> I
I --> J[ESP32-C6 从机<br/>WiFi 6 / BLE 5.3]
各模块职责
| 模块 | 文件 | 职责 |
|---|---|---|
| 摄像头 HAL | camera_hal.c | V4L2 接口封装, ISP 管线控制, HW JPEG 编码, JPEG 尺寸保护 |
| WiFi 管理器 | wifi_manager.c | STA/AP 双模, NVS 凭据存储, 自动回退配网 |
| HTTP 服务器 | test_server.c | 端口 80 控制面 + 端口 81 流面, SSE 非阻塞推送 |
| 从机 OTA | ota_partition.c | flash 分区读取, 版本比较, 分块 RPC 传输 |
| Web 前端 | web_ui.h | 单 HTML 文件嵌入固件, MJPEG 预览, IndexedDB 相册 |
摄像头驱动深度解析
V4L2 数据管道
基于 esp_video 组件(V4L2 POSIX API),数据流如下:
1 | |
关键: g_pixel_format = V4L2_PIX_FMT_RGB565 (FourCC 0x50424752)。ISP 管线完成 RAW10 → RGB565 的转换,CPU 零参与。
HW JPEG 编码器
ESP32-P4 内置 JPEG 硬件加速器 (via driver/jpeg_encode.h):
1 | |
互斥锁策略
/capture(单帧拍摄,5s 超时)和 /stream(MJPEG 连续流,100ms 超时)共享同一个 g_io_lock 互斥锁:
1 | |
效果:浏览器访问 /capture 时,MJPEG 流自动暂停一帧,不会死锁。
JPEG 尺寸保护机制
某些场景(高细节纹理)下,Q85 编码产生的 JPEG 可能超过 339KB,通过 WiFi 发送耗时超过 6 秒,触发 TCP 超时。解决思路:输出 size > 阈值 → 降质重编码。
1 | |
实测效果:一段 380KB 的帧经 3 次降质重编码后稳定在 183KB,发送成功。
ESP-Hosted WiFi 协处理器方案
SDIO 连接拓扑
ESP32-P4 与 ESP32-C6 通过 4-bit SDIO 物理连接,跑 streaming mode,时钟 40MHz。逻辑层使用 ESP-Hosted 框架:
1 | |
STA/AP 双模自动切换
1 | |
NVS 凭据管理
- 命名空间:
wifi_cfg - Key:
ssid,password - STA 连接失败 10 次后自动清除凭据并重启进入 AP 模式
- Web 前端 POST
/config保存凭据写入 NVS
1 | |
C6 从机固件 OTA 远程更新
ESP32-C6 的固件存放在 P4 Flash 的 slave_fw 分区中,P4 上电时自动检测版本并执行 OTA。
分区表设计
1 | |
OTA 流程
1 | |
关键设计:版本比较跳过机制—C6 当前版本与分区固件版本相同则不重复刷写,避免每次上电都执行耗时的 OTA。
HTTP 服务器与 SSE 推送
非阻塞 SSE 设计
传统的 SSE handler 会在内部 while(true) 循环保持 HTTP 连接,这会绑定一个 httpd task 线程。对于资源有限的 MCU,我们采用非阻塞 SSE模式:
handler 发送初始 chunk 后立即返回,后续推送通过 httpd_socket_send() 从独立 task 执行。
1 | |
SO_LINGER 避免 TIME_WAIT 积压
默认的 TCP close 发送 FIN 包后进入 TIME_WAIT 状态,持续数分钟占用 socket。在 MCU 有限 socket 资源下(LWIP 最多 16 个),这会造成 “Address in use” 错误。通过 SO_LINGER {1,0} 直接发送 RST 立即释放:
1 | |
MJPEG 流 — 动态端口 81
流服务器按需启动(/stream_ctrl?action=start),独立在端口 81 运行,避免与端口 80 的控制请求争抢 socket:
1 | |
流 handler 使用 100ms 锁超时 避让 capture 请求(5s 超时),二者共享硬件管道互不饿死:
1 | |
Web 前端:IndexedDB 持久化
前端作为一个完整的 HTML 字符串嵌入固件(web_ui.h 中的 MAIN_PAGE_HTML),约 26KB。通过 http://<设备IP> 直接访问。
功能矩阵:
| 功能 | 实现方式 |
|---|---|
| MJPEG 实时预览 | <img src="/stream">, 支持开/关切换 |
| 单张拍摄 | fetch("/capture") → Blob → IndexedDB |
| SSE 物理按钮触发 | EventSource("/events") → 自动拍摄 |
| 相册管理 | 3 列 CSS Grid, 缩略图预览, 支持删除 |
| 全屏查看 | 点击缩略图 Modal 展开, 下载按钮 |
| ZIP 批量导出 | JSZip Stored 模式, dataset_YYYYMMDD_HHMMSS.zip |
| 画质调整 | <input type="range"> 60-95, POST /quality?v=X |
| 配网 | AP 模式下表单 POST /config |
IndexedDB 存储结构:
1 | |
编译部署实操
1. 环境准备
1 | |
2. 获取项目代码
1 | |
3. 准备 C6 从机固件
将 ESP-Hosted 的 C6 从机固件烧录到 slave_fw 分区:
1 | |
4. 编译与烧录
1 | |
5. 首次配网
设备启动后无 WiFi 凭据,自动进入 AP 配网模式:
- 连接热点
CamSetup(无密码) - 浏览器访问
http://192.168.4.1 - 填写 WiFi 信息 → 保存 → 设备自动重启连接
关键 sdkconfig 配置
1 | |
实战踩坑记录
坑 #1: GCC 嵌套函数 Trampoline 崩溃
症状: stream handler 作为嵌套函数定义在 stream_ctrl_handler 内部,父函数返回后,httpd 调用 handler 时触发 illegal instruction。
根因: GCC 对嵌套函数使用栈上的 trampoline 代码(可执行栈),父函数返回后 trampoline 所在的栈帧被回收。
修复: 将 stream handler 定义为文件作用域的 static 函数(stream_mjpeg_handler),在注册 URI handler 时通过函数指针传递。
这是 ESP-IDF + GCC 开发中最隐蔽的坑之一。务必所有 httpd URI handler 使用文件作用域函数。
坑 #2: Mutex 死锁导致 Capture 超时
症状: /capture 耗时 > 5 秒后返回 “capture failed”,且 /stream 同时停止工作。
根因: 流和 capture 使用同一个互斥锁 g_io_lock,流帧循环抢占锁后 capture 一直等待。原始代码使用统一超时,没有区分场景。
修复: 给 camera_capture() 增加 timeout_ms 参数——stream 用 100ms(拿不到跳过),capture 用 5000ms(阻塞等待)。
坑 #3: JPEG 尺寸过大导致 TCP 超时
症状: 部分场景产生 339KB 的 JPEG,WiFi 发送超时 (> 6s),客户端收到空响应。
修复: 引入 MAX_JPEG_BYTES=180000 上限+逐级降质重编码(最多 3 次,每次降低 12 quality)。
坑 #4: COM8 端口被残留进程占用
症状: idf.py flash 报 “could not open port COM8”。
根因: 多次 idf.py monitor 被 Ctrl+C 中断后,Python 子进程未完全退出持续持有 COM 端口。
排查方法:
1 | |
坑 #5: Windows GBK 编码错误
症状: idf.py flash 在 Windows 中文版上报 UnicodeEncodeError (GBK codec)。
绕过: 使用 python -m esptool 直接烧录,绕过 idf.py 的包装层。
坑 #6: DQBUF DONE 标志未置位
症状: 偶发捕获失败,日志显示 DQBUF: flags=0x0 (DONE not set)。
修复: 检查 vbuf.flags & V4L2_BUF_FLAG_DONE,未置位则将 buffer 重新入队并返回错误,上层重试。
坑 #7: SSE 阻塞式 Handler 耗尽 Socket
症状: SSE 连接建立后,新请求报 “no socket available”。
根因: 传统 SSE handler 在内部 while(true) 循环,阻塞一个 httpd task 线程。多个 SSE 连接迅速耗尽 max_open_sockets。
修复: 非阻塞 SSE — handler 发送初始 chunk 后立即 return ESP_OK,后续推送通过 httpd_socket_send() 从独立 task 进行。
坑 #8: 原 HTTPD_200 响应码 Bug
症状: httpd_resp_set_status(req, "200 OK") 导致 httpd 内部断言失败。
根因: httpd 默认状态码就是 200,显式调 set_status("200 OK") 会触发重复设置的检查。
修复: 移除所有 set_status("200 OK") 调用,只对非 200 响应(如 500, 400, 503)使用。
总结与展望
本项目从零构建了一个可直接用于野外数据采集的嵌入式系统,核心成果:
- 完整的 MIPI-CSI 摄像头驱动 — 基于 esp_video V4L2 + HW JPEG, 含 JPEG 尺寸保护
- 高可用 WiFi 方案 — ESP-Hosted SDIO + STA/AP 自动切换 + NVS 凭据管理
- C6 从机 OTA — 分区烧录 + 版本比较 + 分块 RPC 传输
- Web 全栈 — SSE 非阻塞推送 + MJPEG 实时流 + IndexedDB 持久化 + ZIP 导出
下一步计划:
- 增加 OV5647 自动曝光/白平衡的运行时调节 API
- 对接 TensorFlow Lite Micro 做板端实时推理(分类当前拍摄的物体类别)
- 添加 LCD 触摸屏支持,实现脱离手机的独立手持设备
- 支持 USB MSC 模式,直接将采集数据导出为 U 盘
项目地址
完整源代码及文档:E:\code\Dataset_clcting_sys_ontargetdevice
依赖组件版本(dependencies.lock 精密锁定,确保可复现构建):
| 组件 | 版本 | 用途 |
|---|---|---|
| espressif/esp_video | 2.2.0 | V4L2 接口 |
| espressif/esp_cam_sensor | 2.2.0 | OV5647 传感器驱动 |
| espressif/esp_wifi_remote | 1.6.1 | WiFi 远程 RPC |
| espressif/esp_hosted | 2.12.9 | SDIO 传输层 |
| espressif/esp_ipa | 2.1.0 | ISP 管线 |
| espressif/esp_h264 | 1.3.0 | H.264 编码器 |
| espressif/esp_driver_jpeg | (IDF 内置) | HW JPEG 编码器 |
| espressif/eppp_link | 1.1.5 | SDIO 底层链路 |
打开微信「扫一扫」