做 ESP32-CAM 的开发,有些硬约束是平台本身决定的,代码写得再好也绕不过去。不是 bug,是硅片和框架的设计取舍。下面列的是做类似项目(包括本项目 MiBee Cam)时反复撞上的东西,写在这里当个参考。
一、主板硬件层
1. 双核只有一个真正能用
WiFi 协议栈和 LWIP 的tcpip_task默认绑在 Core 0(PRO_CPU),这是 ESP-IDF 写死的,没法挪。所以看起来是双核,实际上有一核长期被网络栈占着。应用层那些吃资源的活——相机采集、JPEG 编码、SD 文件写入——只能在剩下的一个核上挤。
缓解就是手动分核:网络 I/O(HTTP 服务器、流推送)跟 WiFi 放同一个核;计算密集的活挪到 Core 1。核不能加,只能把活分清楚。
2. GPIO14 冲突——SD 卡和相机共用一根线
几乎所有 AI Thinker 系的 ESP32-CAM 板都把 microSD 的 CLK 接到 GPIO14,而相机 XCLK 也是这个脚。这是 PCB 布线决定的,固件改不了。相机跑起来之后 SD 操作就不靠谱了:目录遍历可能卡死,偶尔看门狗复位。
解决办法:相机和 SD 别同时跑,用互斥量串行化。目录列表启动时缓存到 RAM,之后别去实时遍历。反正别指望相机开着的时候 SD 能正常用。
3. 初代 ESP32 的 PSRAM DMA 不可用
相机采集的帧缓冲放在 PSRAM 里,但数据不是 DMA 直接送进去的,多了一次搬运的开销。CONFIG_CAMERA_PSRAM_DMA=n是硬件限制(只影响 ESP32 初代,S2/S3 支持),不是配置没开对。
只能用双缓冲 +CAMERA_GRAB_LATEST模式,快速交还相机缓冲,只保留最新帧。搬运开销消除不了,只能减少次数。
4. I2S DMA 和 SPI 总线抢资源
相机通过 I2S/DMA 持续往 PSRAM 搬数据,SD 卡走 SPI 也用 DMA。两者在同一颗芯片的内部总线上竞争,真正的并行做不到。
缓解方法跟上面一样:串行化、错峰、热数据缓存到 RAM 减少 SD 访问。
5. OV2640 只有 MJPEG,没有硬件 H.264
OV2640 内置 JPEG 编码器,但 H.264/H.265 这种帧间压缩编码不支持。ESP32 自己也没有视频编码硬核。所以 ESP32-CAM 上所谓的"视频流",本质就是一帧一帧独立的 JPEG 往外推,带宽效率跟 H.264 没法比。
降分辨率、降质量、限帧率是老三样。实时预览和录像可以考虑分开——录像存本地 MJPEG/AVI,预览用低分辨率。想省带宽只能服务端转码,或者换带硬件编码的模组。
6. PCB 天线弱,WiFi 距离不行
板载 PCB 天线增益低,多数版本没有外接天线座。远一点吞吐就掉得厉害,丢包重连频繁。固件层面没法突破发射功率的法规上限和 PA 能力。
靠近 AP 最直接。20MHz 频宽比 40MHz 稳定。每次启动做全 RF 校准(代价是启动慢些)。LWIP 调一下窗口参数适应高 RTT 场景。
7. SD 卡只有 1-bit SPI 模式
引脚被相机和 Flash 占光了,凑不出 4-bit SDMMC 需要的引脚数。所以 SD 卡只能走 1-bit SPI,读写速度大概 1–4 MB/s。
提高 SPI 时钟到稳定上限;热数据用 PSRAM 缓存减少 SD 读。想彻底解决得换支持 SDMMC 4-bit 的板子。
8. IRAM 太小
ESP32 的 IRAM(指令 RAM)大约 128KB,WiFi/BT 驱动必须驻留在 IRAM 里。功能一多(WiFi + 蓝牙 + 相机 + HTTP + TLS),编译就容易报iram0_0_seg overflowed。
做法是取舍:CONFIG_COMPILER_OPTIMIZATION_PERF能省 IRAM 但有性能折损;不常用的 FreeRTOS 函数可以放 Flash。功能越多 IRAM 越紧,没有零代价的全都要。
9. SRAM 520KB,PSRAM 慢且共享总线
内部 SRAM 大约 520KB(部分被硬件占用),放不了多少东西。PSRAM 大(4/8MB)但挂在 SPI 总线上,访问延迟和吞吐都比内部 SRAM 差一截。所有 DMA/CPU 访问共享这一条总线。
大块帧缓冲用MALLOC_CAP_SPIRAM分配,小对象留在内部 RAM。减少 memcpy 的次数和体积,用指针交换代替拷贝。
二、ESP-IDF / 系统层
1. HTTP 服务器单事件循环
esp_http_server本质是单线程事件驱动的。一个 handler 不返回,所有其他请求都得等着。推 MJPEG 流、下载大文件这种长耗时操作不能直接在 handler 里做——本项目用httpd_req_async_handler_begin/complete把长连接摘到独立任务来规避这个问题。
2. LWIP 默认参数偏保守
LWIP 的默认参数面向通用低资源场景,在 ESP32 的 WiFi 链路上不一定最优。MSS、发送缓冲、窗口大小、RTO 这些得根据实际链路质量调。没有万能值,部署环境不同值就不同,得反复试。
3. WiFi 省电默认开着
802.11 省电模式本质就是周期性关射频,唤醒有延迟。如果对延迟敏感(流媒体、实时控制),显式调用esp_wifi_set_ps(WIFI_PS_NONE)关掉。
4. 任务看门狗对阻塞零容忍
Task Watchdog(TWDT)默认启用。某个任务长时间阻塞不交出 CPU,IDLE 任务没法喂狗,设备就重启了。所有耗时操作必须非阻塞化、分片、主动让出 CPU(vTaskDelay/yield)。
5. 文件系统跑在串行 Flash 上
Web UI 页面放在 SPIFFS 或 LittleFS 里,但底层还是串行 SPI Flash。随机小文件读取和目录遍历都有明显开销。LittleFS 比 SPIFFS 好一些,但仍然是 Flash。
gzip 压缩、长缓存头、启动时预载小资源到 RAM——能减少访问就减少访问。本项目打包时将 Web UI 资源 gzip 压缩后嵌入 SPIFFS 分区。
6. FreeRTOS 任务不自动绑核
xTaskCreate创建的线程默认不绑定核心,可能在两个核之间漂移,可能恰好跟 WiFi 抢同一个核。需要显式用xTaskCreatePinnedToCore把活分清楚。
三、通用缓解模式
下面这些是从实际项目里沉淀下来的套路,跟具体代码无关:
- 核分工:Core 0 跑 WiFi + LWIP + HTTP + 流推送,Core 1 跑相机 + 运动检测 + SD 写。网络 I/O 跟 WiFi 同核,内存带宽重的活隔离出去。
- 长连接异步化:任何长耗时操作(MJPEG 流、大文件下载)都从 httpd handler 里摘出来,用 async handler + 独立任务。
- 生产者/消费者 + 指针交换:相机只持一份最新帧,消费者用指针交换读取,临界区里不做 memcpy。
- 带宽预算:流推送和控制信令共享同一 WiFi 链路,不分时就会互相影响。非预览时降帧率或暂停流,控制走轻量通道。
- 缓存优先:目录列表、文件列表缓存到 RAM,不实时遍历 SD。热文件缓存到 PSRAM。静态资源 gzip + 长缓存头。
- 串行化 SD:所有 SD 操作走同一个互斥量/队列,相机活跃时 SD 让行。
- 架构分流:ESP32-CAM 只管采集,照片/视频上传到服务端(NAS、树莓派、对象存储),由服务端做相册和下载。
- 缩略图 + Range + 缓存头:相册用缩略图 sidecar;下载支持 HTTP 206 Range + ETag + Cache-Control。
- 硬件升级:换 ESP32-S3 或更新的型号——引脚富余(没 GPIO14 冲突)、支持 SDMMC 4-bit、PSRAM 更大、支持 camera PSRAM DMA。
四、速查表
| 现象 | 根因 | 应用层能消掉吗 | 怎么缓解 |
|---|---|---|---|
| 控制命令偶尔卡顿 | WiFi 占一核 + 没绑核 | 不能,只能分工 | 核绑定 |
| 推流时 SD 卡挂起/重启 | GPIO14 冲突 + 总线争抢 | 不能,PCB 限制 | SD 串行化 + 缓存 + 互斥 |
| 视频流带宽高 | OV2640 只有 MJPEG | 不能,无 H.264 硬件 | 降分辨率/质量/FPS;服务端转码 |
| 远距离吞吐低/断连 | 天线弱 | 不能,射频硬件 | 靠近 AP / 外接天线 / 全 RF 校准 |
| 读图慢 | SD 1-bit SPI | 不能,引脚限制 | 提高 SPI 时钟 / PSRAM 缓存 |
| IRAM 溢出,编译不过 | IRAM 太小 | 不能,硅片限制 | 优化 Flash 放置 / 精简组件 |
| 堆紧张/malloc 慢 | SRAM 小 + PSRAM 慢 | 不能,硅片限制 | 大块 PSRAM,小对象 SRAM |
| handler 长耗时阻塞全站 | HTTP 单事件循环 | 不能,框架设计 | async handler + 独立任务 |
| 弱网吞吐差/重传慢 | LWIP 默认保守 | 不能,安全默认 | 按链路调 MSS/窗口/RTO |
| 流/控制偶发高延迟 | WiFi 省电 | 不能,协议特性 | 主动关 WIFI_PS_NONE |
| 阻塞就重启 | 任务看门狗 | 不能,防呆机制 | 非阻塞化 + 分片 + 让出 CPU |
| 静态资源读慢 | 串行 Flash FS | 不能,Flash 本质 | gzip + 缓存头 + 预载 RAM |
| 任务漂移抢核 | FreeRTOS 不自动绑核 | 不能,框架不管 | 显式 xTaskCreatePinnedToCore |
几点说明
ESP32-CAM 的性能天花板主要由三件事决定:
- 一核长期被 WiFi 占着
- OV2640 只有 MJPEG,没有高效视频编码
- 板载资源极度受限——IRAM 128KB、SRAM 520KB、天线弱、SD 1-bit SPI、GPIO14 冲突
这些不是 bug,是平台的本质。好的固件不是"消除"它们,是用合理的分工、异步化、缓存和架构分流把它们压到可接受的范围。
如果产品需要正经的文件/视频服务,终点通常是两条路之一:
- 架构上:边缘采集 + 服务端分发——ESP32 只管拍,剩下的转码、相册、下载交给服务器
- 硬件上:升级到 ESP32-S3 或更新的型号
以本项目 MiBee Cam 为例,上面列的大部分问题都在代码里做了针对性处理——比如 GPIO14 冲突通过串行化 SD 操作规避,HTTP 长连接用 async handler 摘到独立任务,核心绑定在main.c里显式指定。具体实现可以参考各模块源码。