用 ESP32-S3 做了个监控摄像头 —— WiFi、TF 卡、视频输出踩坑实录
起因
家里养了几只鹦鹉,白天上班的时候没人在家,想随时看看它们在干嘛。需求说起来简单:能实时看画面、能录像存下来、最好还能自动备份到 NAS 上。市面上的摄像头要么价格不便宜,要么要装各种 APP 注册账号绑手机号,隐私方面心里没底——我就是想看看鸟,不想把视频传到别人的服务器上。
手上正好有一块 Seeed 的 XIAO ESP32-S3 Sense 开发板,自带 OV2640 摄像头,还有 TF 卡槽。ESP32-S3 本身有 WiFi,双核 240MHz,外挂 8MB PSRAM,跑个摄像头应用理论上完全够。于是就打算自己做一个:MiBeeHomeCam。
既然打算开源,就把功能尽量做全面一些。目标很明确:用 ESP-IDF 从零写一个监控摄像头固件,MJPEG 实时流浏览器直接看,AVI 分段录像存 TF 卡,FTP/WebDAV 自动上传 NAS,再配个 Web 管理界面,加上 Prometheus 指标对接监控系统。后续我还计划做一个 NVR 来收集视频文件做视觉分析,不过那是后话了,这篇只聊摄像头本身。
最终实现了这些功能:
- 浏览器打开设备 IP 就能看实时画面
- 自动分段录像(默认 5 分钟一段),循环存储不怕卡满
- FTP/WebDAV 自动上传到 NAS
- Web 界面管理配置、浏览/下载/删除录像文件
- Prometheus
/metrics端点对接监控系统 - WiFi AP/STA 双模式,首次上电手机配网
功能听起来挺完整,但背后的坑是真不少。
硬件选型
用的是 Seeed XIAO ESP32-S3 Sense 这块板子,体积很小(拇指大小),但该有的都有:
- ESP32-S3 双核 240MHz,8MB Octal PSRAM
- 板载 OV2640 摄像头(也兼容 OV3660)
- TF 卡槽(SPI 模式,1-bit SDMMC)
- WiFi 2.4GHz(注意不支持 5GHz)
- USB-C 供电
说实话选这块板子主要是看中了体积小和自带摄像头+TF 卡槽,不用额外接线。但后面踩坑的时候才发现,板子本身也有一些"特色"问题。
WiFi:连接都连不上,还谈什么监控
WiFi 问题是第一个把我搞崩溃的。固件写好了,烧进去,满心期待能看到画面,结果设备怎么都连不上路由器。
认证超时:auth expired / assoc expired
串口日志里一直刷这些东西:
| |
auth -> init (0x200) 是认证超时,assoc -> init (0x400) 是关联超时。设备明明能扫描到路由器的 AP,但认证帧就是过不去。
折腾了好久,换路由器、改信道、改密码,各种操作都不行。最后发现是 XIAO ESP32-S3 这块板子的 WiFi 发射功率默认偏低——这是 Seeed 官方已知的问题,2025年8月之前批次的板子都有这个问题。功率太低,认证帧到不了路由器就被当作丢包了。
修复方法是在 esp_wifi_start() 之后手动把发射功率拉到 15 dBm:
| |
就这一行代码,找问题花了我大半天。Seeed 官方 wiki 上有写,但你得知道这个问题存在才会去查。这种硬件层面的坑,ESP-IDF 的文档里一个字都没提。
事件循环重复创建导致 crash loop
WiFi 模块初始化的时候还有个坑。设备上电后会进入 AP 模式(给用户配网),配完 WiFi 重启后切 STA 模式。但 wifi_init() 里用 ESP_ERROR_CHECK 包了 esp_event_loop_create_default(),如果这个函数在 WiFi 扫描阶段已经被调用过,就会返回 ESP_ERR_INVALID_STATE,ESP_ERROR_CHECK 直接 abort,设备 crash 重启,重启又 crash,无限循环。
日志长这样:
| |
修复方式是不用 ESP_ERROR_CHECK,改为容错处理:
| |
已经是 INVALID_STATE 就忽略它,说明事件循环已经存在了,没必要 crash。这种问题说白了就是对 ESP-IDF 的事件模型理解不够深,文档里也没明确说这个函数不能重复调用。
WiFi 的其他注意事项
ESP32-S3 只支持 2.4 GHz,5 GHz 路由器的信号搜不到,这个不算坑但得知道。另外 AP/STA 双模式的设计也需要注意状态管理,不能在 AP 模式下去连外网,也不能在 STA 没连上的时候启动 SNTP 时间同步。我的做法是 19 步启动流程里,WiFi 初始化之后根据当前模式决定后续步骤的走向,STA 连接成功后再启动时间同步和录像。
WiFi 的模式切换流程大致是这样:
flowchart TD
A[上电] --> B{有 WiFi 配置?}
B -->|否| C[进入 AP 热点模式]
C --> D[手机配网 192.168.4.1]
D --> E[保存凭据并重启]
B -->|是| F[STA 连接路由器]
E --> F
F --> G{连接成功?}
G -->|是| H[同步时间 启动录像]
G -->|否| FTF 卡:能挂上只是第一步
TF 卡的问题比 WiFi 更隐蔽。WiFi 连不上好歹日志里能看到,TF 卡的问题有时候看起来"正常工作"了,实际上数据是坏的。
SPI 高速模式初始化失败
XIAO ESP32-S3 Sense 的 TF 卡槽走的是 SPI 模式(1-bit SDMMC),不是 SDMMC 4-bit 模式。这意味着速度上限就比标准 SDMMC 低。我一开始把 SPI 时钟设成了 SDMMC_FREQ_HIGHSPEED(40MHz),想着能快则快。
结果一堆 TF 卡初始化失败:
| |
查了半天发现,SPI 模式下的 CMD6 高速模式切换命令在 40MHz 时钟下会失败——不是所有 TF 卡都支持高速 SPI 协商。有些卡能过,有些卡不能,同一张卡换个批次可能就不行了。
最后老老实实降到 20MHz(SDMMC_FREQ_DEFAULT)才稳定:
| |
这个问题的教训是:SPI 模式的速度能力和 SDMMC 模式(4-bit)不一样,改参数的时候必须在真实硬件上验证。看起来能编译通过的东西,跑起来可能全是问题。
TF 卡格式和兼容性
格式必须是 FAT32,exFAT 和 NTFS 不支持。这在嵌入式领域是常识,但如果你手上的 TF 卡是 64GB 以上的,出厂格式大概率是 exFAT,需要手动格式化。Windows 的右键格式化如果超过 32GB 可能不显示 FAT32 选项,得用第三方工具。
速度等级方面,Class 10 或以上是必须的,录像写入速度跟不上就会丢帧。推荐用已知品牌的卡,杂牌卡的读写速度波动太大。
0 字节录像文件
这个问题困扰了我一阵。文件管理器里经常出现 0 字节的 .avi 文件,虽然不影响使用,但看着很碍眼。
根因是分段录像的回调函数没有检查实际写入的字节数。当 SD 卡出现异常,分段文件被打开后立即关闭,文件缓存里就注册了一个 0 字节的条目。
修复很简单,在回调前加个判断:
| |
热插拔检测
TF 卡热插拔是实际使用中的刚需。我的方案是在 Core 1 上跑一个监控任务,每 10 秒轮询一次 SD 卡状态。拔出时停止录像并设 LED 为错误状态,插入时重新挂载并自动恢复录像。关键是检测到拔出后不能立刻尝试重新挂载,要等检测到插入信号后再操作,不然会疯狂重试挂载。
整个热插拔的状态机如下:
flowchart TD
A[上电 初始化 TF 卡] --> B[已挂载]
B --> C[自动开始录像
每5分钟分段写入]
C --> E[检测到 TF 卡移除
LED 双闪报错]
E --> G[检测到 TF 卡插入]
G --> H[重新挂载 成功]
H --> C视频输出:能录像 ≠ 能播放
视频输出这块是最让我头疼的。摄像头能采集帧数据、能写入文件、文件大小也正常,但下载下来播放——各种花式报错。
AVI 文件头偏移量写错
这是最经典的一个 bug。录好的 AVI 文件在播放器里显示 “0 帧”,文件看起来有数据但就是播放不了。
根因在 close_segment() 函数里,AVI 头部的回填偏移量算错了:
dwTotalFrames被写到了偏移量 40 的位置,但实际应该在 48strh dwLength被写到了偏移量 136,实际应该在 140
偏移量差了几个字节,导致播放器读到的帧数和时长都是 0,认为文件里没有视频数据。
正确的计算方式:
| |
AVI 文件格式是 RIFF 容器,每一层都有固定大小的头部。手动拼 AVI 文件的时候,每个字段的偏移量都必须精确计算,差一个字节整个文件就废了。调试的时候我直接用十六进制编辑器对着 AVI 规范一个字节一个字节地比对,那感觉真是酸爽。
biSize 字段缺失导致"编解码器暂不支持"
这个问题比上一个更隐蔽。VLC 打开录像文件报错 “编解码器暂不支持: VLC 无法解码格式”,文件属性显示视频尺寸异常(比如 600x1572865 这种离谱数字)。
用 VLC 的详细日志模式排查:
| |
发现 biWidth 和 biHeight 的值完全不对,biCompression 字段不是 “MJPG” 而是一堆乱码。但代码里明明写了正确的值,为什么读出来不对?
查了半天,根因是 write_hdrl() 函数在生成 AVI 的 BITMAPINFOHEADER(strf chunk)时,漏掉了 biSize 字段。这个字段是固定值 40,表示头部结构体的大小。strf chunk 声明了 40 字节数据但只写了 36 字节,导致后面所有字段全部偏移 4 字节:
| 偏移位置 | 应写入 | 实际写入 |
|---|---|---|
| biSize (0) | 40 | 800(被 biWidth 覆盖) |
| biWidth (4) | 800 | 600(被 biHeight 覆盖) |
| biHeight (8) | 600 | 垃圾数据 |
| biCompression (20) | “MJPG” | 垃圾数据 |
修复就是加回缺失的 biSize:
| |
biSize 这个字段看着没用(永远是 40),但播放器依赖它来确定头部大小,然后跳到正确的位置读取后面的数据。少了这 4 字节,整个文件结构的解析就全错了。
这个 bug 教会我一件事:BITMAPINFOHEADER 里面没有"可选"字段,每个字段播放器都会读,少写一个就是全盘崩溃。排查 AVI 文件结构问题,应该从 strf chunk 的每个字段逐一验证,而不是只看 RIFF/LIST 层级对不对。
停止录像后无法下载文件
还有一个看起来不大但很烦人的问题:停止录像后立刻下载文件,API 返回 409 “File is currently being recorded”。明明已经停了,为什么还报正在录制?
原因是 s_current_file 这个全局变量在录像任务退出后没被清空。下载接口通过 recorder_get_current_file() 检查当前文件名,始终返回上一次的文件路径,导致已关闭的文件被误判为正在录制。
修复就是在录像任务的清理代码里加一行:
| |
MJPEG 实时流的带宽瓶颈
实时视频流这块的限制主要在带宽。ESP32-S3 的 WiFi 吞吐量就那么多,SVGA(800×600)10fps 的 MJPEG 流在信号不好的时候会明显卡顿。我的做法是限制并发客户端最多 2 个,超过直接返回 503。实际使用中,把分辨率降到 VGA(640×480)或者把 JPEG 质量参数调高(数值越大压缩越狠,画质越差但带宽占用小),在 WiFi 信号一般的场景下会流畅很多。
录像数据流
一段完整的录像从采集到上传,经过的流程:
flowchart TD
A[摄像头采集帧] --> B[写入 AVI 文件]
B --> C{分段时长到?}
C -->|否| A
C -->|是| D[回填 AVI 头部]
D --> E[回调:加入上传队列]
E --> F[上传到 NAS]
F --> G{剩余 < 20%?}
G -->|是| H[删除最早录像]
G -->|否| A
H --> A整体架构
系统基于 FreeRTOS,双核分工明确——Core 0 专注实时任务确保不丢帧,Core 1 处理非实时任务:
flowchart TD
A[摄像头采集帧] --> B[PSRAM 双缓冲]
B --> C[录像任务 Core0]
C --> D[TF 卡 AVI 分段]
D --> E[NAS 上传 Core1]
E --> F[FTP / WebDAV]
B --> G[MJPEG 流]
G --> H[浏览器 :80]核心设计是双核分工:Core 0 跑录像任务(高优先级,确保帧采集不被抢占导致丢帧),Core 1 跑上传、SD 监控、健康检测等非实时任务。摄像头帧缓冲分配在 PSRAM 上做双缓冲,没有 PSRAM 这事干不了。
启动流程是 app_main() 里的 19 步顺序初始化,关键步骤失败会记录错误日志但继续执行(功能降级),不会直接 crash。TF 卡热插拔检测每 10 秒轮询一次,循环存储在剩余空间低于 20% 时自动删除最早的录像。看门狗设了 30 秒超时,真卡死了就 panic 重启。
Web 管理界面做了 4 个页面:仪表盘看状态、配置页改参数、文件管理器浏览下载删除录像、预览页看实时流。所有配置通过 NVS 持久化,也支持 TF 卡上的配置文件覆盖(方便批量部署)。
写在后面
回顾这个项目,最难的不是写功能代码,而是排查那些 “明明代码看起来没问题但就是不工作” 的 bug。WiFi 发射功率低是硬件问题、TF 卡 SPI 高速模式不稳定是协议兼容问题、AVI 头部偏移量错误是二进制文件格式问题——这些问题每一个都不是看代码能看出来的,都得在实际硬件上运行、用日志和工具一步步排查。
几个我觉得比较重要的经验:
- AVI/RIFF 文件格式手动拼装很坑。每个字段的偏移量必须精确计算,建议写完之后用十六进制编辑器对照规范逐一验证,别等播放器报错了再查
- SPI 模式的 TF 卡速度有限。别想当然设 40MHz,20MHz 稳定比什么都重要
- XIAO ESP32-S3 的 WiFi 要手动设功率。不然在某些路由器上根本连不上,这个坑 Seeed 官方文档有提但很容易忽略
- VLC 的
-vvv --file-logging是调试视频文件问题的利器。能看到每个字段的实际解析值,比猜快多了
项目开源在 GitHub 上:Mi-Bee-Studio/seeed-esp32s3-cam,感兴趣的可以试试。固件可以直接从 Release 下载烧录,不需要自己编译。