ESP32-CAM 实战教程:手机连 Wi-Fi 查看网页摄像头,并采集声音

本文由 AI 协助整理,代码和验证步骤适用于学习记录。不同 ESP32-CAM 板型、摄像头模组、麦克风模块和库版本可能导致引脚与配置差异,请以实际硬件原理图为准。

ESP32-CAM 视频与声音采集结构

这次想做什么

这篇准备做一个更有画面感的小项目:用 ESP32-CAM 采集摄像头画面,让手机连接 ESP32 自己开的 Wi-Fi 热点,然后在手机浏览器里打开网页查看实时画面。同时再接一个 I2S 数字麦克风,做声音采集,至少能在网页上看到音量变化,后续再扩展成短音频片段或 WebSocket 音频流。

先把话说在前面:ESP32-CAM 很适合做网页摄像头入门项目,但它不是手机,也不是专业网络摄像机。视频可以通过 MJPEG 流比较顺利地在浏览器里显示;声音如果要做到真正的实时播放、音画同步、低延迟、抗卡顿,就会牵涉编码、缓冲、浏览器播放策略和网络带宽。学习阶段更稳妥的路线是:第一步先跑通视频流,第二步接入 I2S 麦克风,第三步把音量或短 PCM 采样展示出来。这样既能体现“视频 + 声音采集”,又不至于把 ESP32 按在地上要求它表演手机直播。

这类项目很适合拿来写博客,因为它有一个清楚的使用场景:手机不用安装 App,连接设备 Wi-Fi,打开浏览器就能看画面。它也有足够多的坑:供电不足、烧录模式、摄像头排线、PSRAM、分辨率、帧率、I2S 引脚冲突、手机浏览器缓存。踩坑素材多得很,文章自然就不会瘦得像刚出厂的杜邦线。

硬件准备

推荐硬件如下:

模块 用途
ESP32-CAM AI Thinker 摄像头采集与网页服务
OV2640 摄像头 ESP32-CAM 常见摄像头模组
USB 转 TTL 模块 给 ESP32-CAM 烧录程序
INMP441 或 SPH0645 I2S 数字麦克风
稳定 5V 电源 给 ESP32-CAM 供电
手机 连接 Wi-Fi 并打开网页查看

ESP32-CAM 最大的特点是小巧、便宜、资料多;最大的脾气是供电和引脚。它不像普通 DevKit 那样直接插 USB 就能烧录,通常需要 USB 转 TTL 模块。烧录时 GPIO0 接 GND,复位或重新上电后进入下载模式;程序烧录完成后,要断开 GPIO0 与 GND,再复位运行。这个步骤如果忘了,板子会像一个坚持留在考场不肯交卷的学生,一直待在下载模式里。

声音采集建议用 I2S 数字麦克风,例如 INMP441。模拟麦克风模块当然也能读,但 ESP32-CAM 上可用引脚紧张,模拟采样又容易受噪声影响。I2S 麦克风输出数字音频,结构更清楚,写文章也更容易解释。

ESP32-CAM 与 I2S 麦克风连接流程

引脚连接建议

如果使用 ESP32-CAM AI Thinker,并且本文方案不使用 SD 卡,可以参考下面的 I2S 麦克风连接:

I2S 麦克风 ESP32-CAM
VCC 3V3
GND GND
SCK / BCLK GPIO14
WS / LRCL GPIO15
SD / DOUT GPIO13

这里必须提醒一句:GPIO13、GPIO14、GPIO15 在很多 ESP32-CAM 板子上也和 SD 卡相关。本文默认不使用 SD 卡,把这些脚让给麦克风。不同板型不要照抄,一定先查原理图。摄像头项目一旦引脚冲突,表现往往不是很礼貌,它可能初始化失败,也可能偶尔成功,然后在你准备截图时突然罢工。

供电方面,建议用稳定 5V。很多 ESP32-CAM 的失败不是代码问题,而是摄像头启动瞬间电流上来以后电压掉下去。串口里如果反复出现 Brownout、重启、乱码,先别急着怀疑人生,先换电源、换线、降低分辨率。嵌入式世界里,“线不行”是一个朴素但杀伤力很强的答案。

访问方式设计

本文采用 SoftAP 模式,也就是 ESP32-CAM 自己开一个 Wi-Fi 热点。

手机操作流程:

  1. 给 ESP32-CAM 上电。
  2. 手机搜索 Wi-Fi,连接 ESP32-CAM-AV。
  3. 打开浏览器,访问 http://192.168.4.1。
  4. 页面显示摄像头视频流和声音音量状态。

这种方式的好处是不用家里路由器,也不用先配网。拿出去演示时,只要手机能连热点就行。缺点是手机连上 ESP32 热点后,可能暂时不能访问互联网;有些手机还会提示“此 Wi-Fi 无法上网”,这不是故障,只是因为 ESP32-CAM 不是宽带路由器,不负责带你刷短视频。

后续如果想接入家庭网络,可以改成 STA 模式,让 ESP32-CAM 连接路由器,然后手机在同一局域网访问它的 IP。学习阶段我更推荐先用 SoftAP,因为变量少,路由器、防火墙、IP 变化这些杂事先不进场。

视频流的基本思路

网页视频流常见做法是 MJPEG。简单说,就是服务器不断发送一张又一张 JPEG 图片,浏览器看起来就像视频。它不如 H.264 那样省带宽,也不适合高质量长时间监控,但胜在实现简单,浏览器支持也方便。

ESP32-CAM 常见示例里会有两个接口:

GET /          返回网页
GET /stream    返回 MJPEG 视频流

网页里只需要放一个图片标签:

<img src="/stream" />

浏览器会持续请求 /stream,服务器一帧一帧往外推 JPEG。看起来很神奇,本质上有点像一个人飞快翻动连环画。只要帧率别太高、分辨率别太大,ESP32-CAM 是能扛住入门演示的。

分辨率建议从 QVGA 或 VGA 开始,不要一上来就追求 UXGA。先跑通,再调清晰度。很多项目失败,就是因为一开始就把画质拉满,结果内存、带宽、供电一起抗议。

声音采集的基本思路

I2S 麦克风采集到的是一串音频采样。最简单的做法不是立刻在网页里播放声音,而是先计算声音强度,也就是 RMS 或平均绝对值,然后在网页上显示一个音量条。这样可以验证麦克风是否工作,也能避免一开始就陷入浏览器音频播放、缓存和延迟问题。

声音采集可以拆成三步:

  1. 初始化 I2S。
  2. 周期读取一小段采样。
  3. 计算音量值,通过 /audio-level 或 WebSocket 给网页。

如果后续想听到声音,可以把 PCM 数据打包给浏览器,用 WebAudio 播放。但这就是进阶内容了。要处理采样率、缓冲队列、播放时钟和网络抖动。别看“听到声音”这四个字很短,它背后可不是四行代码。

代码骨架

下面给一个学习版结构,重点是看清模块分工。摄像头引脚配置可以参考 Arduino ESP32 自带的 CameraWebServer 示例,板型选择 AI Thinker ESP32-CAM。

#include "esp_camera.h"
#include <WiFi.h>
#include <WebServer.h>
#include <driver/i2s.h>

const char* AP_SSID = "ESP32-CAM-AV";
const char* AP_PASS = "12345678";

WebServer server(80);
volatile int audioLevel = 0;

void setupCamera() {
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = 5;
  config.pin_d1 = 18;
  config.pin_d2 = 19;
  config.pin_d3 = 21;
  config.pin_d4 = 36;
  config.pin_d5 = 39;
  config.pin_d6 = 34;
  config.pin_d7 = 35;
  config.pin_xclk = 0;
  config.pin_pclk = 22;
  config.pin_vsync = 25;
  config.pin_href = 23;
  config.pin_sccb_sda = 26;
  config.pin_sccb_scl = 27;
  config.pin_pwdn = 32;
  config.pin_reset = -1;
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_JPEG;

  if (psramFound()) {
    config.frame_size = FRAMESIZE_VGA;
    config.jpeg_quality = 12;
    config.fb_count = 2;
  } else {
    config.frame_size = FRAMESIZE_QVGA;
    config.jpeg_quality = 15;
    config.fb_count = 1;
  }

  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("camera init failed: 0x%x\n", err);
  }
}

I2S 麦克风初始化可以这样组织:

void setupI2SMic() {
  i2s_config_t i2s_config = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate = 16000,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_STAND_I2S,
    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count = 4,
    .dma_buf_len = 256,
    .use_apll = false,
    .tx_desc_auto_clear = false,
    .fixed_mclk = 0
  };

  i2s_pin_config_t pin_config = {
    .bck_io_num = 14,
    .ws_io_num = 15,
    .data_out_num = I2S_PIN_NO_CHANGE,
    .data_in_num = 13
  };

  i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
  i2s_set_pin(I2S_NUM_0, &pin_config);
}

读取声音强度:

void updateAudioLevel() {
  int32_t samples[256];
  size_t bytesRead = 0;

  i2s_read(I2S_NUM_0, samples, sizeof(samples), &bytesRead, 0);
  int count = bytesRead / sizeof(int32_t);
  if (count <= 0) return;

  long long sum = 0;
  for (int i = 0; i < count; i++) {
    int32_t v = samples[i] >> 14;
    sum += abs(v);
  }

  audioLevel = sum / count;
}

网页接口可以先做得朴素一点:

void handleRoot() {
  server.send(200, "text/html",
    "<!doctype html><html><head><meta name='viewport' content='width=device-width,initial-scale=1'>"
    "<style>body{font-family:sans-serif;background:#111;color:#eee;text-align:center}"
    "img{width:100%;max-width:640px;border:1px solid #444}"
    ".bar{width:90%;height:24px;background:#333;margin:16px auto}.fill{height:100%;background:#22c55e;width:0}</style>"
    "</head><body><h2>ESP32-CAM AV</h2><img src='/stream'>"
    "<div class='bar'><div id='v' class='fill'></div></div>"
    "<script>setInterval(async()=>{let r=await fetch('/audio-level');let n=await r.text();"
    "document.getElementById('v').style.width=Math.min(100,Number(n)/100)+'%';},500)</script>"
    "</body></html>");
}

void handleAudioLevel() {
  server.send(200, "text/plain", String(audioLevel));
}

这里没有把 /stream 的完整实现贴满,是因为 Arduino ESP32 的 CameraWebServer 示例已经提供了成熟的 MJPEG 处理逻辑。学习时建议先跑通官方示例,再把网页和音量接口加进去。不要一上来就手写完整流处理,否则你会在 multipart 边界、HTTP 头和帧缓冲里来回迷路。

主程序结构大致如下:

void setup() {
  Serial.begin(115200);
  setupCamera();
  setupI2SMic();

  WiFi.softAP(AP_SSID, AP_PASS);
  Serial.print("AP IP: ");
  Serial.println(WiFi.softAPIP());

  server.on("/", handleRoot);
  server.on("/audio-level", handleAudioLevel);
  // server.on("/stream", handleJpgStream);
  server.begin();
}

void loop() {
  server.handleClient();
  updateAudioLevel();
}

手机端验证

烧录完成后,按这个顺序验证:

  1. 断开 GPIO0 与 GND,重新上电。
  2. 打开串口监视器,确认摄像头初始化成功。
  3. 手机连接 Wi-Fi:ESP32-CAM-AV。
  4. 浏览器访问 http://192.168.4.1。
  5. 页面能看到摄像头画面。
  6. 对着麦克风说话或拍手,网页音量条应有明显变化。

如果视频能看但音量没变化,先检查 I2S 引脚和麦克风供电。INMP441 有些模块会标 L/R 或 SEL,它决定左右声道,代码里如果只读左声道,模块却输出在右声道,读数就会像在认真听空气。可以尝试切换 I2S_CHANNEL_FMT_ONLY_LEFT 和 I2S_CHANNEL_FMT_ONLY_RIGHT。

如果手机连上 Wi-Fi 但打不开网页,先看手机是否自动切回了有互联网的 Wi-Fi。有些手机会觉得 ESP32 热点不能上网,于是热心地帮你换回家庭 Wi-Fi。它的出发点很好,但对调试来说有点添乱。

常见问题

摄像头初始化失败:检查排线方向、板型配置、供电和 PSRAM。AI Thinker 的引脚配置不要随便套到其他板子上。

画面很卡:降低分辨率,先用 QVGA 或 VGA;同时减少网页轮询频率。ESP32-CAM 做演示很合适,但别拿它和专业摄像头比肌肉。

反复重启:优先检查 5V 供电能力和 USB 线。摄像头启动瞬间的电流会让弱电源露馅。

麦克风无数据:检查 I2S 三根信号线、声道配置、模块供电,以及是否和 SD 卡引脚冲突。

手机提示无互联网:正常。本文方案是手机直连 ESP32 热点,不是让 ESP32 当路由器。

想保存视频或音频:可以先保存单张 JPEG 或短 PCM 片段,别急着做长时间录像。ESP32-CAM 的存储、带宽和稳定性都需要单独设计。

可以继续优化的方向

第一,改成家庭 Wi-Fi 模式。设备连接路由器后,手机访问串口里打印的 IP。这样手机还能同时上网,但需要处理配网问题。

第二,加入 WebSocket。音量值和短音频片段通过 WebSocket 推送,比网页定时 fetch 更顺滑。只是代码复杂度会上来,适合在视频流跑稳之后再做。

第三,增加鉴权。摄像头画面属于敏感信息,哪怕只是学习项目,也不要长期裸奔在开放网络里。至少给热点设置密码,局域网访问也可以加简单登录。

第四,加入补光灯控制。ESP32-CAM 常带闪光 LED,但 GPIO4 又常和板载 LED、SD 卡等资源有关,使用前要确认冲突。补光灯一开,画面是亮了,供电压力也跟着上来了。

本篇小结

这篇的核心目标不是打造一个完美的网络摄像机,而是把 ESP32-CAM 的几个关键能力串起来:摄像头采集、网页视频流、SoftAP 热点、手机访问、I2S 声音采集。视频部分用 MJPEG 流实现,声音部分先用音量条验证采集链路,后续再扩展到 WebSocket 或 WebAudio。

做 ESP32-CAM 项目时,最重要的不是背 API,而是保持排错顺序:先供电,再烧录,再摄像头初始化,再 Wi-Fi,再网页,再麦克风。一步一步来,设备就比较愿意配合。要是所有模块一起上,失败时就像一桌人同时说话,热闹是热闹,谁也听不清。

打赏

如果这篇学习记录对你有帮助,欢迎扫码支持后续整理。

打赏码

打赏支持:王庆岩

posted on 2026-09-16 23:17  潇洒石头  阅读(43)  评论(0)    收藏  举报

导航