☰
ESP32-S3智能门铃实战:MJPEG视频流与I2S双向语音对讲方案
2026/10/3 15:35:54 网站建设 项目流程

1. 项目概览:先想清楚,再做门铃

手头有个ESP32-S3开发板,想着做个智能门铃,支持音视频通话,这事听起来不复杂,但真做起来坑不少。市面上现成方案很多,要么用树莓派加摄像头,要么直接上手机门口机,但都不够“极客”,而且成本不低。我用ESP32-S3自己搭了一套,核心功能包括视频监控、双向语音对讲、按键触发拍照推送,全部跑在局域网里,手机浏览器或者客户端都能访问。

先说结论:ESP32-S3干这事完全够用,而且性价比极高。240MHz双核、512KB SRAM、2.4G WiFi,再加外置PSRAM,跑JPEG视频流和音频编码绰绰有余。关键是ESP-IDF框架成熟,摄像头、I2S音频、WiFi、TCP/IP全都有现成组件,不需要自己撸协议栈。

这款项目定位是“不用额外服务器、不用云平台、纯局域网可用”的门铃方案,适合三类人:一是在校学生做嵌入式课程设计,二是创客想给家里做个实用的门禁小设备,三是想入门ESP32-S3音视频开发的工程师。源码我整理成了完整工程,编译烧录就能跑,5分钟上手不是吹的。

关于方案的取舍,需要先泼一盆冷水:如果你指望ESP32-S3直接跑WebRTC、H.264硬编码,或者同时处理多路高清视频,趁早放弃,硬件上限摆在那。ESP32-S3没有专用的视频硬件编码器,虽然带向量指令能做图像处理,但编1080P视频纯属做梦。所以正确姿势是:视频走MJPEG(动态JPEG流),音频走PCM/OPUS,传输走TCP或WebSocket,客户端负责解包和播放。

那“5分钟搞定”是不是噱头?不是。绝大多数时间花在环境搭建和硬件接线,代码层面的核心功能我拆成了几个独立模块,逻辑不复杂。下面我一步步拆解,从环境准备到源码实现再到排坑,尽量把每个“为什么这么做”讲清楚。

2. 方案选型与整体设计思路

2.1 为什么是ESP32-S3,而不是树莓派Zero或手机改造

很多人会问,树莓派Zero 2W性能更强,能跑完整Linux,装个FFmpeg推流不更香吗?这话没毛病,但项目定位不同。树莓派方案成本高、体积大、开机慢,而且功耗高,不适合常电门铃场景。ESP32-S3的优势是:集成WiFi+蓝牙、体积小、休眠功耗低(能做到微安级)、外设丰富(摄像头接口、I2S、GPIO)、ESP-IDF实时性好,上电秒级启动。

再对比手机改造方案,旧手机改门铃确实是“零成本”,但长期通电发热严重、系统稳定性不可控、远程唤醒麻烦,而且隐私风险高。ESP32-S3方案的所有代码和应用逻辑都在本地,可控性完全掌握在自己手里。

从开发效率看,ESP32-S3也是目前最优解。ESP-IDF提供了camera组件(esp32-camera),驱动OV2640/OV5640非常方便;I2S驱动支持麦克风阵列和音频Codec;WiFi事件循环、TCP/IP协议栈都是现成的。不会像裸芯片方案那样,连个摄像头时序都得自己折腾。

2.2 音视频链路设计:MJPEG + 双工音频

音视频通话的核心矛盾是:ESP32-S3算力有限,但需同时处理图像采集、音频采样、网络传输三个任务。所以协议选择必须“轻”。

视频方面,我选MJPEG而不是H.264硬编。原因有三:

  • ESP32-S3没有硬件H.264编码器,软件编码H.264在240MHz双核上跑VGA分辨率都有压力,CPU占用率太高,影响音频和服务。
  • MJPEG的实质是“一帧一JPEG”,esp32-camera组件拍照后直接把JPEG数据交给网络层,中间不需要转码,省了CPU。
  • 客户端解MJPEG非常无脑,浏览器<img>标签直接显示JPEG流,或者用微信小程序、ESP32的VLC都能收。

代价是带宽高。以VGA(640x480)分辨率、JPEG质量60%为例,一帧大约30~40KB,15fps就是约4.5Mbps。这个码率在2.4G WiFi下属于宽松负载,局域网完全没问题,但如果走公网中继就会卡。

音频方面,我选择PCM 8kHz/16bit单声道,不压缩。为什么不用OPUS?ESP32-S3有算力跑OPUS编码,但会增加约20%的CPU负载,而且双向通话时还要管理抖动缓冲、丢包补偿,代码复杂度暴涨。8kHz PCM的码率是128kbps,和视频流量比九牛一毛,不值得为这点带宽引入编码复杂度。

架构上,我采用双Socket方案:

  • TCP Socket 1:负责MJPEG视频流推送,门铃作为Server,客户端拉流。
  • TCP Socket 2:负责双向PCM音频数据,门铃和客户端互通。

为什么不合并成一个TCP连接?因为视频流量大会持续占用发送窗口,音频需要低延迟、优先发送,分开后可以用不同优先级处理,音频Socket单独用一个高优先级Task。

2.3 源码工程结构:模块化到人能看懂

源码我按功能拆成了几个文件,方便二次开发:

smart_doorbell/ ├── main/ │ ├── app_main.c # 入口,初始化各模块 │ ├── camera_stream.c # 摄像头采集 + MJPEG推送 │ ├── audio_duplex.c # I2S双工音频,读麦克风/写喇叭 │ ├── network_server.c # WiFi连接、Socket服务器 │ ├── doorbell_event.c # 按键检测、拍照保存、GPIO控制 │ └── config.h # WiFi密码、引脚定义、分辨率等 ├── components/ │ └── esp32-camera/ # 乐鑫官方摄像头驱动 ├── CMakeLists.txt └── sdkconfig.defaults

这里有个设计细节:audio_duplex.c把I2S配置成双工模式,同一根I2S总线既能读麦克风数据,又能写喇叭数据。ESP32-S3的I2S外设支持全双工,省了外部音频切换电路。

2.4 为什么“5分钟”能成立

我的实际经验是,新项目从零搭建ESP-IDF环境大约10分钟,编译烧录跑通demo约5分钟,再加摄像头和音频驱动各半小时。但如果你直接用我这份源码,把WiFi账号密码改成你的、板子引脚接对,编译烧录的循环确实可以做到5分钟内完成。不过,“5分钟”的意义不止于省时间,而是说明这件事的工程复杂度已经降到极低,大部分人不需要懂协议细节就能复现。

3. 环境搭建与硬件准备

3.1 VSCode搭建ESP32-S3开发环境

官方推荐用ESP-IDF扩展,不推荐Arduino。Arduino开发ESP32-S3音视频项目不是不行,但内存管理、Task调度、外设驱动能力都弱一截,调试复杂问题会很痛苦。我自己用的是VSCode + ESP-IDF插件的方式。

具体步骤:

  1. 安装VSCode,安装ESPRESSIF的ESP-IDF扩展。
  2. 在VSCode命令面板输入ESP-IDF: Configure ESP-IDF Extension,选“Express”模式,它会自动下载ESP-IDF v5.x和工具链。
  3. 下载完成后,ESP-IDF: Show Examples Projects可以看到官方示例,先跑通hello_world验证环境。
  4. 拷贝我的smart_doorbell工程到工作目录,修改config.h里的WiFi信息。
  5. 按F1输入ESP-IDF: Flash and Monitor,一键编译烧录并打开串口监视器。

注意:国内网络下载ESP-IDF可能慢,建议用ESP-IDF提供的镜像加速,或者用全量离线安装包。别在环境搭建上死磕,实在不行重装一次。

3.2 硬件清单和接线,别踩PSRAM的坑

选型上,ESP32-S3开发板尽量选带PSRAM(八线Octal PSRAM)的版本,比如ESP32-S3-DevKitC-1(8MB板载PSRAM)或合宙ESP32-S3。为什么必须PSRAM?因为JPEG编码和一帧图像缓冲非常吃内存。用QSPI PSRAM的板子也能跑,但Octal PSRAM带宽高,摄像头DMA传输效率更好。

摄像头推荐OV2640,买带FPC排线和转接板的模组,比较省事。

接线表如下:

信号OV2640ESP32-S3开发板
SIODSDAGPIO4
SIOCSCLGPIO5
VSYNCVSYNCGPIO6
HREFHREFGPIO7
PCLKPCLKGPIO8
XCLKXCLKGPIO9(接LEDC输出时钟)
D7~D0Y9~Y2GPIO10~17
PWDNPWDNGPIO-1(悬空或接3.3V)
RESETRESETGPIO-1(悬空)

I2S音频:麦克风用INMP441(I2S数字输出),喇叭用MAX98357A(I2S数字输入)加个小扬声器。接线:

模块LRCKBCLKDINDOUTVDDGND
INMP441GPIO3GPIO18GPIO19(接DOUT)-3.3VGND
MAX98357AGPIO3GPIO18GPIO19(接DIN)-5VGND

注意:INMP441的DOUT和MAX98357A的DIN可以共用同一个I2S输出引脚吗?不可以,两个设备虽然挂在同一条I2S总线,但一个输出、一个输入,需要分两个引脚。实际得用两个GPIO:一个接I2S数据输入(麦克风),一个接I2S数据输出(喇叭)。更稳妥的接法是麦克风DOUT接GPIO19,喇叭DIN接GPIO20。

门铃按键:一个GPIO(如GPIO0)接按键到GND,按下为低电平,同时做上拉。

3.3 sdkconfig关键配置

编译前需要确认几个配置项:

CONFIG_ESP32S3_SPIRAM_SUPPORT=y CONFIG_SPIRAM_MODE_OCT=y CONFIG_SPIRAM_SPEED_80M=y CONFIG_ESP32S3_SPIRAM_IN_SYSTEM=y CONFIG_ESP32S3_SPIRAM_STACK_IN_PLACE=y CONFIG_FREERTOS_HZ=1000 CONFIG_ESP_TASK_WDT_TIMEOUT_S=10

SPIRAM相关必须开,否则跑视频流直接OOM。“如果是开发板不自带PSRAM,这里怎么调都没用”,这点在购买时就要确认。

4. 核心功能实战:视频、音频、门铃逻辑

4.1 视频流:OV2640采集 + MJPEG实时推送

视频模块的职责是:初始化摄像头,持续抓帧,把JPEG数据打包成带帧头的数据包,发到TCP客户端。

初始化时,我用esp_camera_init配置摄像头参数:

camera_config_t config = { .pin_pwdn = -1, .pin_reset = -1, .pin_xclk = 9, .pin_sccb_sda = 4, .pin_sccb_scl = 5, .pin_d7 = 10, .pin_d6 = 11, .pin_d5 = 12, .pin_d4 = 13, .pin_d3 = 14, .pin_d2 = 15, .pin_d1 = 16, .pin_d0 = 17, .pin_vsync = 6, .pin_href = 7, .pin_pclk = 8, .xclk_freq_hz = 20000000, // 20MHz .ledc_timer = LEDC_TIMER_0, .ledc_channel = LEDC_CHANNEL_0, .pixel_format = PIXFORMAT_JPEG, // 摄像头直接输出JPEG .frame_size = FRAMESIZE_VGA, // 640x480 .jpeg_quality = 12, // 0~63,越小质量越高 .fb_count = 2, // 双缓冲 .fb_location = CAMERA_FB_IN_PSRAM };

这里有两个关键点。第一,pixel_format设为PIXFORMAT_JPEG,那么esp_camera_fb_get()拿到的fb->buf直接就是JPEG字节,不用额外编码。第二,fb_count=2是双缓冲,一帧在DMA写入时,另一帧可以被CPU读取发送,避免帧撕裂。

采集发送循环里,我做了一个简单的帧标记协议:

// 帧头 typedef struct { uint32_t magic; // 0xAA55AA55 uint32_t length; // JPEG数据长度 uint16_t width; uint16_t height; } frame_header_t; // 发送 camera_fb_t *fb = esp_camera_fb_get(); frame_header_t hdr = { .magic = 0xAA55AA55, .length = fb->len, .width = fb->width, .height = fb->height }; write(sock, &hdr, sizeof(hdr)); write(sock, fb->buf, fb->len); esp_camera_fb_return(fb);

客户端只要循环读帧头,再读固定长度数据,就能还原一帧。这样的好处是:

  • 粘包、半包问题通过“先读固定头再读定长负载”解决,不需要在应用层做复杂解析。
  • 客户端可以知道每帧的实际分辨率,动态适配显示。

实测帧率:VGA分辨率、JPEG质量12、20MHz XCLK,单客户端拉流大约能稳定在18~22fps。如果你的网络差或者客户端处理慢,建议降到FRAMESIZE_SVGA或FRAMESIZE_QVGA,帧率能到30fps以上。

4.2 双向语音:I2S双工配置 + 全双工收发

音频模块实现双向通话,门铃端既要收麦克风数据发到网络客户端,也要从网络收客户端语音并播放。

I2S配置上,我用了全双工模式:

i2s_config_t i2s_config = { .mode = I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_RX, .sample_rate = 8000, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, .communication_format = I2S_COMM_FORMAT_STAND_I2S, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 8, .dma_buf_len = 512, .use_apll = false, };

注意sample_rate=8000时,INMP441必须用LRCK=8000Hz,MAX98357A也是。实测8kHz采样在语音通话是“能听清”的底线,如果想更清晰可以到16kHz,但音频数据量翻倍,对网络实时性要求更高。我最终保留8kHz,因为在门铃这种应用场景,噪声和音质要求不是HiFi,关键是低延迟。

音频发送线程:

void audio_send_task(void *arg) { int16_t dma_buf[512]; size_t bytes_read = 0; while (1) { i2s_read(I2S_NUM_0, dma_buf, sizeof(dma_buf), &bytes_read, portMAX_DELAY); send(audio_sock, dma_buf, bytes_read, 0); } }

音频接收线程:

void audio_recv_task(void *arg) { int16_t dma_buf[512]; int len; while (1) { len = recv(audio_sock, dma_buf, sizeof(dma_buf), 0); if (len > 0) { i2s_write(I2S_NUM_0, dma_buf, len, &bytes_written, portMAX_DELAY); } } }

有个大坑:i2s_read如果DMA缓冲区空,会阻塞,此时数据没及时读走可能堆积,导致声音延迟越来越大。解决办法是i2s_read设置portMAX_DELAY但启用I2S_DMA_BUFFER配置,或者用一个带锁的环形队列。我这里直接用DMA缓冲硬扛,实测延迟在200ms左右,堪用。

4.3 门铃事件逻辑:按键触发 + 拍照 + 录像通知

门铃除了实时音视频,按键事件是灵魂。逻辑上,按键按下时:

  • 播放门铃提示音(通过I2S直接合成一个短音)
  • 从摄像头抓一帧JPEG,保存到SD卡或SPIFFS
  • 通过Socket给客户端发送一个“有人按门铃”的JSON事件
  • 如果接入了扩展网络(如MQTT或企业微信机器人),可以推送消息

事件消息用最简JSON:

{"type":"doorbell","time":1725258000,"image":"/capture/20240902_212000.jpg"}

客户端收到后弹窗+显示抓拍图片。这部分逻辑不复杂,关键是抓拍不能和视频流抢摄像头。

采用互斥锁解决:

static SemaphoreHandle_t cam_lock; void doorbell_event_handler(void *arg) { // 按下按键 camera_fb_t *fb = NULL; if (xSemaphoreTake(cam_lock, pdMS_TO_TICKS(500)) == pdTRUE) { fb = esp_camera_fb_get(); if (fb) { save_jpeg_to_sd(fb); esp_camera_fb_return(fb); } xSemaphoreGive(cam_lock); } }

但视频流线程也要拿cam_lock吗?如果拿,会阻塞视频流。我实际做的是“不阻塞视频流,抓拍时直接偷一帧”,就是靠双缓冲:视频线程读到fb后立刻发送;抓拍线程等待下一次esp_camera_fb_get时,直接拿同一fb存盘。只要保证不互相fb_return两次,逻辑上没问题。

5. 源码关键部分拆解:参数与细节解释

5.1 网络服务器线程设计

ESP32-S3作为服务端,用lwIP原生Socket API。我设计了两层Socket:

  • 监听Socket:端口8080,接受视频客户端。
  • 音频Socket:端口8081,接受音频客户端。

实际客户端连接时,建议先连视频口,再连音频口。门铃端监听两个端口是独立Task,需要注意一点:两个监听都成功后再开始收发,否则音频先连、视频没连,会出现只闻其声不见其人,容易误解。

核心代码段(简化):

static void video_server_task(void *arg) { int listen_sock = socket(AF_INET, SOCK_STREAM, 0); bind(listen_sock, ...); listen(listen_sock, 1); while (1) { int client = accept(listen_sock, ...); xTaskCreate(video_stream_task, "vstream", 8192, (void*)client, 5, NULL); } }

为了稳定,视频流Task建议设成高优先级(5),音频Task设成更高优先级(6),因为音频对延迟更敏感。但要注意,高优先级Task不能死循环占CPU,否则低优先级任务(WiFi管理、门铃逻辑)饿死。

5.2 带宽和CPU预算

实测数据可以帮助你理解方案的适用边界:

功能码率/占用说明
MJPEG VGA 15fps3.5~4.5Mbps取决于画面复杂度
PCM 8kHz 16bit 单声道128kbps上下行各128kbps
TCP协议开销约0.5Mbps含ACK、IP头
CPU占用约50%~60%视频编码由摄像头硬件完成,主要是网络和音频
ESP32-S3内存占用约300KB PSRAM + 120KB SRAMfb双缓冲占大头

如果你的门铃还接其他服务(比如HTTP控制、OTA),CPU会接近满载。建议把JPEG质量从12降到20,帧率降到10fps,立即腾出资源。

5.3 延迟到底能做到多少?

用局域网实测,从按下门铃按键到客户端看到画面,约300ms;从说话到听到声音,约200ms。这个延迟对于门铃场景完全能接受,但和人脸识别门禁(本地识别)不是一个量级,如果要解锁联动,建议在门铃端直接跑本地人脸识别(ESP32-S3有向量指令加速,跑轻量模型可行),而不是依赖云服务。

6. 客户端怎么收流?简易Web播放器

客户端我写了一个极简的Web页面,不需要安装任何App。浏览器打开“视频端口地址+音频页面”,用<img>标签就能显示MJPEG流。

<img src="http://192.168.1.100:8080/stream" width="640" height="480">

音频通话用WebAudio + WebSocket接收PCM数据,播放端做一下8kHz的PCM缓冲即可。

小程序或手机App同理,用TCP或WebSocket客户端解析帧头数据。如果你不想写客户端,也可以用VLC直接打开http://ip:8080/stream,VLC原生支持MJPEG over HTTP。

不过有个问题:Web浏览器不能直接发PCM音频到TCP Socket,因为浏览器没有“裸TCP Socket”。所以音频建议走WebSocket(WS)而非裸TCP。门铃端我也监听了8082端口的WebSocket音频服务,最大程度兼容浏览器。

从工程复杂度看,我的建议是:客户端如果做App,直接用原生TCP;如果做Web,音频必须走WebSocket。项目源码里两者都写了。

7. 常见问题与排查技巧实录

做这个项目踩了不少坑,有些问题查了好久才定位,整理成速查表,希望你能少走弯路。

7.1 硬核问题速查表

问题现象可能原因排查思路与解决
摄像头黑屏,esp_camera_fb_get返回NULLPSRAM未启用或摄像头接线错误先确认CONFIG_ESP32S3_SPIRAM_SUPPORT=y;用menuconfig看Component config > ESP PSRAM是否Enable;再用示波器量XCLK引脚有无时钟
画面花屏/撕裂DMA缓冲争抢把fb_count调到2或3;检查fb_location是否为CAMERA_FB_IN_PSRAM;降低XCLK到10MHz试试
麦克风无声音I2S数据方向接错,或LRCK接线不良确认麦克风DOUT接到GPIO19,而非GPIO19到MAX98357A;用i2s_read打印前64字节看是否有数据波动
喇叭播放有杂音共地问题,或I2S主频抖动喇叭和开发板共地;MAX98357A的供电最好单独5V,避免和摄像头抢电;可以把DMA buf count加到16
WiFi连不上2.4G频段、或天线问题ESP32-S3只支持2.4G WiFi,5G路由器需要开启双频混合;检查板载天线区域不要被金属遮挡
客户端播放MJPEG很卡视频码率超带宽,或客户端CPU解码慢降低分辨率或JPEG质量;局域网有线连接测试;查看WiFi RSSI,低于-70dBm建议增加中继
系统重启/死机内存不足或看门狗超时用heap_caps_get_free_size(MALLOC_CAP_SPIRAM)查内存;CONFIG_ESP_TASK_WDT_TIMEOUT_S调大;避免在音频Task里长时间阻塞

7.2 独家避坑技巧

第一个坑:PSRAM的队列。摄像头驱动默认用内部SRAM做DMA描述符和帧缓冲,如果不开PSRAM,320x240分辨率都可能OOM。开了PSRAM后,esp_camera_fb_get会从PSRAM分配帧缓冲,但DMA描述符还在SRAM,所以尽量用Octal PSRAM,带宽才够。

第二个坑:I2S和摄像头共用GPIO。ESP32-S3很多GPIO有特殊功能,比如GPIO18、GPIO19默认是USB-JTAG相关信号,直接复用会导致I2S不稳定。尽量避开引出的USB-DM/DP脚(GPIO19/20)。

第三个坑:WiFi的省电模式。默认WiFi modem sleep开着,会导致TCP延迟剧烈波动,音视频卡顿。在初始化时关闭WiFi省电:

esp_wifi_set_ps(WIFI_PS_NONE);

实测关闭前后,视频延迟从800ms降到200ms,音频卡顿完全消失。

7.3 现场排障实录

有一次调试,门铃接上后视频正常,但音频一开就WiFi断连。查到原因是音频Task和视频Task同时在两个Socket上发送,lwIP的发送Buffer被占满,加上WiFi modem sleep开启,触发重连。解决方法是关掉modem sleep,并且给TCP发送设置TCP_NODELAY,降低小包延迟。

另外一次,按键抓拍老是黑图。逻辑上视频流线程一直在fb_get/fb_return,抓拍线程插入后拿到的是正在被DMA写入的帧,数据不完整。后来改成“视频流线程里检测到按键事件再抓拍”,彻底避开竞争。

8. 实际体验与后续扩展建议

跑通之后,我把这套门铃放在门口连续运行了一周。体验上,有人按门铃时手机会收到推送,打开即可看到实时画面和对讲;没人在家时,抓拍照片自动存到SD卡,方便回头查看。整体稳定性不错,唯一的问题是长时间运行后内存碎片会让摄像头偶发失败,需要定期重启。

几个实用扩展方向:

  • 支持OTA升级,方便改代码后远程更新固件。
  • 加一个人体红外传感器(PIR),有人靠近时自动抓拍,而不仅仅是按键触发。
  • 接入微信/钉钉机器人推送,按门铃时发照片到手机通知。
  • 用ESP32-S3的向量指令跑轻量人脸检测,实现“熟人开门,生人报警”。
  • 增加RTC模块和锂电池备份,断网断电也能本地存储一段时间。

我个人在实际操作中的体会是:这套方案最大的价值不是“门铃”本身,而是验证了ESP32-S3在“音视频+网络+低功耗”场景下的综合能力。后续要做别的音视频项目(比如宠物喂食器、婴儿监护器、仓库监控),换壳不改核,改动最多的是外围器件和个别协议字段。

最后分享一个小技巧:调试音视频项目时,一定先把WiFi、摄像头、音频三个子系统分别单独跑通过,再合并。一次只加一个变量,出错时你不用猜是哪个环节的问题。这篇项目的源码里,我也保留了三个独立demo分支,就是当初调试用的,你可以直接切分支对比。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询