若该文为原创文章,转载请注明原文出处。
1. 项目概述
本项目基于两块 ESP32 开发板(ESP32 WROOM32 与 ESP32-S3 N16R8),搭建了一套局域网内无按键、自动组网的全双工实时对讲系统。系统采用 UDP 协议传输 PCM 音频数据,通过 I2S 接口外接 INMP441 麦克风与 MAX98357 功放,实现双向实时语音通信。
核心功能
-
无按键设计:上电后自动连接 Wi-Fi、自动开始收发音频,无需任何人工干预。
-
全双工通信:双向同时采集、发送、接收和播放,端到端延迟约 50ms。
-
自动重连:Wi-Fi 掉线后自动重连,无需重启设备。
-
跨平台兼容:利用 ESP32 与 ESP32-S3 的 I2S 硬件差异,分别采用"单端口全双工"和"双端口独立收发"两种架构。
音频参数
| 参数 | 数值 |
|---|---|
| 采样率 | 16000 Hz |
| 位深 | 16 bit |
| 声道 | 单声道(左声道) |
| 单包大小 | 256 样本(512 字节) |
| DMA 缓冲 | 8 × 256 样本 |
| 传输协议 | UDP(低延迟优先) |
| 端口 | 12345 |
2. 硬件接线
2.1 ESP32 WROOM32 接线(修正后)
关键设计 :ESP32 只有 I2S0 一个外设支持数据发送 ,因此麦克风和功放必须共享 BCLK 和 WS 时钟线,只分离数据线。
| 外设 | 引脚 | ESP32 GPIO | 板上丝印 | 说明 |
|---|---|---|---|---|
| INMP441 | VCC | 3V3 | 3V3 | 供电 |
| INMP441 | GND | GND | GND | 地 |
| INMP441 | WS (LRCLK) | GPIO15 | D15 | 帧同步(共享) |
| INMP441 | SCK (BCLK) | GPIO4 | D4 | 位时钟(共享) |
| INMP441 | SD (数据) | GPIO21 | D21 | 麦克风数据输入 |
| MAX98357 | VIN | 3V3 | 3V3 | 供电 |
| MAX98357 | GND | GND | GND | 地 |
| MAX98357 | LRC (WS) | GPIO15 | D15 | 与 INMP441 共享 |
| MAX98357 | BCLK | GPIO4 | D4 | 与 INMP441 共享 |
| MAX98357 | DIN | GPIO18 | D18 | 功放数据输出 |
| MAX98357 | SD | 悬空 | --- | 一直使能 |
接线示意:
text
ESP32 WROOM32
┌──────────────┐
INMP441 ────┤ │
WS ──────┤ GPIO15 ──┬───┼── MAX98357 LRC
SCK ──────┤ GPIO4 ──┼───┼── MAX98357 BCLK
SD ──────┤ GPIO21 │ │
│ │ │
MAX98357 ───┤ │ │
DIN ──────┤ GPIO18 │ │
└──────────┴───┘
⚠️ 注意 :MAX98357 的 LRC 和 BCLK 不要接 GPIO22 和 GPIO19,否则无法收到时钟信号,导致完全无声。
2.2 ESP32-S3 N16R8 接线
关键设计 :ESP32-S3 的两个 I2S 外设(I2S0、I2S1)均支持独立收发,因此可以使用两个完全独立的 I2S 端口,引脚不共享。
| 外设 | 引脚 | ESP32-S3 GPIO | 说明 |
|---|---|---|---|
| INMP441 | SCK (BCLK) | GPIO6 | I2S0 位时钟 |
| INMP441 | WS (LRCLK) | GPIO5 | I2S0 帧同步 |
| INMP441 | SD (数据) | GPIO4 | I2S0 数据输入 |
| MAX98357 | SCK (BCLK) | GPIO9 | I2S1 位时钟 |
| MAX98357 | WS (LRC) | GPIO10 | I2S1 帧同步 |
| MAX98357 | SD (DIN) | GPIO11 | I2S1 数据输出 |
接线示意:
text
ESP32-S3 N16R8
┌──────────────┐
INMP441 ────┤ GPIO6 (SCK) │ I2S0
│ GPIO5 (WS) │
│ GPIO4 (SD) │
│ │
MAX98357 ───┤ GPIO9 (SCK) │ I2S1
│ GPIO10 (WS) │
│ GPIO11 (SD) │
└──────────────┘
3. 系统架构与工作原理
3.1 全双工音频流
text
麦克风(INMP441) ──I2S RX──> ESP32 ──UDP──> 路由器 ──UDP──> 对方ESP32 ──I2S TX──> 功放(MAX98357)
↑ │
└────────────────────── 声音反馈(可能引起啸叫)──────────────────────────────────────┘
3.2 ESP32 WROOM32 的 I2S 全双工实现
ESP32 仅 I2S0 支持 TX,因此采用单端口全双工模式:
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_TX), // 同时收发
.sample_rate = 16000,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.dma_buf_count = 8,
.dma_buf_len = 256,
.tx_desc_auto_clear = true, // 自动清空DMA,防杂音
};
i2s_pin_config_t pin_config = {
.bck_io_num = 4, // 共享 BCLK
.ws_io_num = 15, // 共享 WS
.data_out_num = 18, // 输出到 MAX98357
.data_in_num = 21 // 从 INMP441 输入
};
3.3 ESP32-S3 的双 I2S 独立实现
S3 使用两个独立端口,互不干扰:
// I2S0:麦克风(仅 RX)
i2s_config_t i2sMicConfig = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = 16000,
// ...
};
// I2S1:功放(仅 TX)
i2s_config_t i2sSpkConfig = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
.sample_rate = 16000,
.tx_desc_auto_clear = true,
// ...
};
3.4 Wi-Fi 自动连接机制
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true); // 底层自动重连
WiFi.begin(SSID, PASSWORD);
// 主循环中检测断线
if (WiFi.status() != WL_CONNECTED) {
connectWiFi(); // 主动重连
}
3.5 UDP 收发主循环
void loop() {
// 1. 从麦克风读取一帧数据
i2s_read(I2S_PORT, micBuf, sizeof(micBuf), &bytesRead, portMAX_DELAY);
// 2. 通过 UDP 发给对方
udp.beginPacket(PEER_IP, UDP_PORT);
udp.write((uint8_t*)micBuf, bytesRead);
udp.endPacket();
// 3. 接收对方的音频包并播放
int packetSize = udp.parsePacket();
if (packetSize > 0) {
int len = udp.read((uint8_t*)spkBuf, sizeof(spkBuf));
i2s_write(I2S_PORT, spkBuf, len, &bytesWritten, portMAX_DELAY);
}
}
4. 常见问题与解决方案
问题 1:S3 编译报错 exit status 1(缺少自定义分区表)
原因 :Tools 菜单选择了 PartitionScheme=custom,但工程目录下没有 partitions.csv 文件。
解决方案:
-
方案 A :在工程目录下创建
partitions.csv,内容如下:# Name, Type, SubType, Offset, Size nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x400000, app1, app, ota_1, 0x410000, 0x400000, spiffs, data, spiffs, 0x810000, 0x7E0000, coredump, data, coredump,0xFF0000, 0x10000, -
方案 B :将 Tools → Partition Scheme 改为
16M Flash (3MB APP/9.9MB FATFS)。
问题 2:WROOM32 编译报错 undefined reference to millis、app_main、__wrap_esp_panic_handler
原因 :WROOM32 的编译缓存 core.a 损坏(可能因编译中断或杀毒软件锁文件导致)。
解决方案:
-
关闭 Arduino IDE。
-
清空
C:\Users\Administrator\AppData\Local\arduino\cores\下的所有内容。 -
清空
C:\Users\Administrator\AppData\Local\arduino\sketches\下的所有内容。 -
重新打开 IDE 编译。
-
若无效,可重命名工程文件夹(如
ESP32_WROOM32_Intercom),强制生成新的缓存哈希。
问题 3:单向通信(S3 → WROOM32 有声音,WROOM32 → S3 无声)
原因 :WROOM32 的 MAX98357 的 LRC 和 BCLK 接在了 GPIO22/GPIO19 ,但代码中 I2S0 实际输出的时钟在 GPIO15/GPIO4,导致 MAX98357 收不到时钟信号。
解决方案:
-
将 MAX98357 的 LRC 改接 GPIO15 ,BCLK 改接 GPIO4(与 INMP441 共享时钟)。
-
DIN 保持 GPIO18 不变。
-
代码无需修改。
问题 4:啸叫/回声
原因:全双工系统无回声消除(AEC),麦克风会拾取功放播放的声音,形成正反馈。
解决方案:
-
将麦克风与喇叭物理隔离(拉开距离、加隔音棉)。
-
降低功放音量。
-
后期可引入 ESP-ADF 框架的 AEC 算法。
5. 工程结构
ESP32_LAN_Intercom/
├── ESP32_LAN_Intercom_WROOM32/
│ └── ESP32_LAN_Intercom_WROOM32.ino # WROOM32 主程序
└── ESP32_LAN_Intercom_S3/
├── ESP32_LAN_Intercom_S3.ino # S3 主程序
└── partitions.csv # S3 自定义分区表(若使用 custom)
6. 关键设计对比
| 特性 | ESP32 WROOM32 | ESP32-S3 N16R8 |
|---|---|---|
| I2S 端口数 | 1 个(I2S0 全双工) | 2 个(I2S0 RX + I2S1 TX) |
| BCLK/WS | 麦克风与功放共享 | 各自独立 |
| 数据引脚 | DIN=GPIO18, DOUT=GPIO21 | 独立配置 |
| Flash | 4MB | 16MB |
| PSRAM | 无 | 8MB(建议开启) |
| 分区方案 | Default 4MB | 16M Flash 或 Custom |
7. 使用说明
配置步骤
-
修改两块代码中的
WIFI_SSID和WIFI_PASSWORD。 -
先烧录一块板子,从串口监视器读取其 IP 地址。
-
将该 IP 填入另一块板子的
PEER_IP。 -
分别烧录两块板子。
-
上电,系统自动组网并开始对讲。
开发板配置
-
WROOM32:
ESP32 Dev Module,Flash 4MB,PSRAM Disabled。 -
S3:
ESP32S3 Dev Module,Flash 16MB,PSRAM OPI PSRAM。
串口监视器
-
波特率:115200
-
上电后会打印本机 IP、UDP 端口、I2S 初始化状态。
8. 总结
本系统利用 UDP 低延迟传输和 I2S 全双工音频采集/播放,实现了一套无需按键、自动组网的局域网对讲方案。核心难点在于 ESP32 与 ESP32-S3 的 I2S 硬件差异:
-
ESP32:只有一个 I2S TX 外设,必须共享 BCLK/WS 时钟线。
-
ESP32-S3:两个 I2S 外设均可独立收发,引脚完全分离。
理解这一差异,是硬件接线和代码配置正确的前提。
/*
* ESP32 WROOM32 局域网对讲 - 全双工
* INMP441: WS=GPIO15, SCK=GPIO4, SD=GPIO21
* MAX98357: LRC=GPIO22, BCLK=GPIO19, DIN=GPIO18
*/
#include <WiFi.h>
#include <WiFiUdp.h>
#include <driver/i2s.h>
// ==================== 网络配置 ====================
const char* WIFI_SSID = "yifeng118"; // 修改为你的WiFi名称
const char* WIFI_PASSWORD = "112233445566"; // 修改为你的WiFi密码
const char* PEER_IP = "192.168.0.104"; // 修改为对方板子的IP
const uint16_t UDP_PORT = 12345;
// ==================== I2S 引脚定义 ====================
#define I2S_PORT I2S_NUM_0
// INMP441 麦克风
#define MIC_WS_PIN 15
#define MIC_SCK_PIN 4
#define MIC_SD_PIN 21
// MAX98357 功放
#define SPK_LRC_PIN 22
#define SPK_BCLK_PIN 19
#define SPK_DIN_PIN 18
// ==================== 音频参数 ====================
#define SAMPLE_RATE 16000
#define DMA_BUF_COUNT 8
#define DMA_BUF_LEN 256
// ==================== 全局变量 ====================
WiFiUDP udp;
int16_t micBuf[DMA_BUF_LEN]; // 麦克风读取缓冲
int16_t spkBuf[DMA_BUF_LEN]; // 扬声器播放缓冲
// ==================== WiFi 自动连接 ====================
void connectWiFi() {
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("正在连接 WiFi");
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
if (millis() - start > 30000) {
Serial.println("\n连接超时,重启...");
ESP.restart();
}
}
Serial.println("\nWiFi 已连接");
Serial.print("本机 IP: ");
Serial.println(WiFi.localIP());
}
// ==================== I2S 初始化(全双工) ====================
void initI2S() {
// I2S 配置:标准模式,全双工
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_TX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, // INMP441 和 MAX98357 都用左声道
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = DMA_BUF_COUNT,
.dma_buf_len = DMA_BUF_LEN,
.use_apll = false,
.tx_desc_auto_clear = true, // 自动清空DMA,防止杂音
.fixed_mclk = 0
};
// 引脚配置
i2s_pin_config_t pin_config = {
.bck_io_num = MIC_SCK_PIN, // 共享 BCLK:GPIO4
.ws_io_num = MIC_WS_PIN, // 共享 WS:GPIO15
.data_out_num = SPK_DIN_PIN, // 输出到 MAX98357 DIN:GPIO18
.data_in_num = MIC_SD_PIN // 从 INMP441 输入:GPIO21
};
i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL);
i2s_set_pin(I2S_PORT, &pin_config);
i2s_zero_dma_buffer(I2S_PORT); // 清空DMA消除上电爆音
Serial.println("I2S 全双工初始化完成");
}
// ==================== 主程序 ====================
void setup() {
Serial.begin(115200);
delay(500);
Serial.println("\n===== ESP32 WROOM32 对讲系统启动 =====");
connectWiFi();
udp.begin(UDP_PORT);
Serial.printf("UDP 监听端口: %d\n", UDP_PORT);
initI2S();
Serial.println("系统就绪,开始对讲...");
}
void loop() {
// WiFi 掉线自动重连
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi 断开,正在重连...");
connectWiFi();
}
size_t bytesRead = 0;
// 1. 从麦克风读取音频数据
esp_err_t result = i2s_read(I2S_PORT, micBuf, sizeof(micBuf), &bytesRead, portMAX_DELAY);
if (result != ESP_OK || bytesRead == 0) return;
// 2. 发送 UDP 音频包给对方
udp.beginPacket(PEER_IP, UDP_PORT);
udp.write((uint8_t*)micBuf, bytesRead);
udp.endPacket();
// 3. 接收对方的 UDP 音频包并播放
int packetSize = udp.parsePacket();
if (packetSize > 0) {
int len = udp.read((uint8_t*)spkBuf, sizeof(spkBuf));
if (len > 0) {
size_t bytesWritten = 0;
i2s_write(I2S_PORT, spkBuf, len, &bytesWritten, portMAX_DELAY);
}
}
}
/*
* ESP32-S3 N16R8 局域网对讲 - 全双工
* INMP441: SCK=GPIO6, WS=GPIO5, SD=GPIO4
* MAX98357: SCK=GPIO9, WS=GPIO10, SD=GPIO11
* 使用两个独立 I2S 端口:I2S0=麦克风, I2S1=功放
*/
#include <WiFi.h>
#include <WiFiUdp.h>
#include <driver/i2s.h>
// ==================== 网络配置 ====================
const char* WIFI_SSID = "yifeng118"; // 修改为你的WiFi名称
const char* WIFI_PASSWORD = "112233445566"; // 修改为你的WiFi密码
const char* PEER_IP = "192.168.0.102"; // 修改为对方板子的IP
const uint16_t UDP_PORT = 12345;
// ==================== I2S 引脚定义 ====================
// I2S0 用于麦克风(RX)
#define I2S_MIC_PORT I2S_NUM_0
#define MIC_SCK_PIN 6
#define MIC_WS_PIN 5
#define MIC_SD_PIN 4
// I2S1 用于功放(TX)
#define I2S_SPK_PORT I2S_NUM_1
#define SPK_SCK_PIN 9
#define SPK_WS_PIN 10
#define SPK_SD_PIN 11
// ==================== 音频参数 ====================
#define SAMPLE_RATE 16000
#define DMA_BUF_COUNT 8
#define DMA_BUF_LEN 256
// ==================== 全局变量 ====================
WiFiUDP udp;
int16_t micBuf[DMA_BUF_LEN];
int16_t spkBuf[DMA_BUF_LEN];
// ==================== WiFi 自动连接 ====================
void connectWiFi() {
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("正在连接 WiFi");
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
if (millis() - start > 30000) {
Serial.println("\n连接超时,重启...");
ESP.restart();
}
}
Serial.println("\nWiFi 已连接");
Serial.print("本机 IP: ");
Serial.println(WiFi.localIP());
}
// ==================== I2S 初始化 ====================
void initI2S() {
// ---- I2S0:麦克风(RX) ----
i2s_config_t i2sMicConfig = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = SAMPLE_RATE,
.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 = DMA_BUF_COUNT,
.dma_buf_len = DMA_BUF_LEN,
.use_apll = false,
.tx_desc_auto_clear = false,
.fixed_mclk = 0
};
i2s_pin_config_t micPinConfig = {
.bck_io_num = MIC_SCK_PIN,
.ws_io_num = MIC_WS_PIN,
.data_out_num = I2S_PIN_NO_CHANGE,
.data_in_num = MIC_SD_PIN
};
i2s_driver_install(I2S_MIC_PORT, &i2sMicConfig, 0, NULL);
i2s_set_pin(I2S_MIC_PORT, &micPinConfig);
i2s_zero_dma_buffer(I2S_MIC_PORT);
Serial.println("I2S0 麦克风初始化完成");
// ---- I2S1:功放(TX) ----
i2s_config_t i2sSpkConfig = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
.sample_rate = SAMPLE_RATE,
.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 = DMA_BUF_COUNT,
.dma_buf_len = DMA_BUF_LEN,
.use_apll = false,
.tx_desc_auto_clear = true,
.fixed_mclk = 0
};
i2s_pin_config_t spkPinConfig = {
.bck_io_num = SPK_SCK_PIN,
.ws_io_num = SPK_WS_PIN,
.data_out_num = SPK_SD_PIN,
.data_in_num = I2S_PIN_NO_CHANGE
};
i2s_driver_install(I2S_SPK_PORT, &i2sSpkConfig, 0, NULL);
i2s_set_pin(I2S_SPK_PORT, &spkPinConfig);
i2s_zero_dma_buffer(I2S_SPK_PORT);
Serial.println("I2S1 功放初始化完成");
}
// ==================== 主程序 ====================
void setup() {
Serial.begin(115200);
delay(500);
Serial.println("\n===== ESP32-S3 N16R8 对讲系统启动 =====");
connectWiFi();
udp.begin(UDP_PORT);
Serial.printf("UDP 监听端口: %d\n", UDP_PORT);
initI2S();
Serial.println("系统就绪,开始对讲...");
}
void loop() {
// WiFi 掉线自动重连
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi 断开,正在重连...");
connectWiFi();
}
size_t bytesRead = 0;
// 1. 从麦克风读取
esp_err_t result = i2s_read(I2S_MIC_PORT, micBuf, sizeof(micBuf), &bytesRead, portMAX_DELAY);
if (result != ESP_OK || bytesRead == 0) return;
// 2. 发送 UDP 音频包给对方
udp.beginPacket(PEER_IP, UDP_PORT);
udp.write((uint8_t*)micBuf, bytesRead);
udp.endPacket();
// 3. 接收对方的 UDP 音频包并通过功放播放
int packetSize = udp.parsePacket();
if (packetSize > 0) {
int len = udp.read((uint8_t*)spkBuf, sizeof(spkBuf));
if (len > 0) {
size_t bytesWritten = 0;
i2s_write(I2S_SPK_PORT, spkBuf, len, &bytesWritten, portMAX_DELAY);
}
}
}
如有侵权,或需要完整代码,请及时联系博主。