基于ESP32 的局域网全双工对讲系统

若该文为原创文章,转载请注明原文出处。

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 损坏(可能因编译中断或杀毒软件锁文件导致)。

解决方案:

  1. 关闭 Arduino IDE。

  2. 清空 C:\Users\Administrator\AppData\Local\arduino\cores\ 下的所有内容。

  3. 清空 C:\Users\Administrator\AppData\Local\arduino\sketches\ 下的所有内容。

  4. 重新打开 IDE 编译。

  5. 若无效,可重命名工程文件夹(如 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. 使用说明

配置步骤

  1. 修改两块代码中的 WIFI_SSID 和 WIFI_PASSWORD。

  2. 先烧录一块板子,从串口监视器读取其 IP 地址。

  3. 将该 IP 填入另一块板子的 PEER_IP。

  4. 分别烧录两块板子。

  5. 上电,系统自动组网并开始对讲。

开发板配置

  • 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);
        }
    }
}

如有侵权,或需要完整代码,请及时联系博主。

相关推荐
szxinmai主板定制专家2 小时前
工业视觉新思路:FPGA图像预处理+RK NPU推理,实现高速缺陷检测
人工智能·嵌入式硬件·fpga开发·rk3576+codesys
硅基手札2 小时前
ROS2 全面解析 - 从架构到源码
单片机·架构·机器人
Molesidy5 小时前
【Embedded Development】【高级MCU篇】基于野火STM32H750XBH6_Pro开发板的高级MCU开发学习之1点亮led
stm32·单片机·学习
FPGA小徐5 小时前
【硬件实战】立创 EDA 绘制 TF/SD 卡模块电路|SPI 模式原理图 + 阻抗匹配 + PCB 布局布线全教程
单片机·嵌入式硬件·fpga开发
狂奔蜗牛(bradley)6 小时前
EtherCAT DC 分布式时钟同步的 ModelSim 仿真:从帧模板到从站行为建模
人工智能·嵌入式硬件·fpga开发·架构
铅笔小新z6 小时前
【stm32】I2C 中断模式数据收发
stm32·单片机·嵌入式硬件
铅笔小新z7 小时前
【stm32】理解弱引用
javascript·stm32·嵌入式硬件
铅笔小新z7 小时前
【stm32】DMA
stm32·单片机·嵌入式硬件
Brilliantwxx8 小时前
【STM32】 DMA中断机制、NVIC 联动与配置实战前章
stm32·单片机·嵌入式硬件·mcu