ESP32-S3 WiFi UDP 局域网对讲机)
一、项目总体说明1、 整体功能总览自动对等设备发现UDP 广播 beacon同局域网内两台设备互相识别超时自动丢失 peer 并重新搜索半双工 PTT 对讲GPIO0 (BOOT 键) 按下 本地拾音 UDP 发送音频松开 接收 UDP 音频并 I2S 播放音频链路INMP441 (I2S0 RX) 采集 → 16kHz 16bit 单声道 PCM → UDP 数据包传输 → MAX98357 (I2S1 TX) 功放外放WS2812 (GPIO48) 状态灯规则 红色常亮PTT 发射中本地说话 绿色短时点亮正在收到对方音频音频包到来后保持 150ms静音熄灭 黄色周期闪烁正在搜索对等机、未找到 peer⚫ 熄灭已找到 peer、当前无收发、空闲静默音频预处理接收端内置软件增益当前 1.8 倍可修改自带限幅防削波爆音2、硬件与接线说明⚠️ ESP32-S3 通用两台必须一模一样接线否则 I2S 不通1. 外设清单外设型号功能I2S 通道数字麦INMP441音频采集输入I2S_NUM_0RXD 类功放MAX98357A音频播放输出I2S_NUM_1TXPTT 按键板载 BOOT发话按键GPIO0内部上拉RGB LEDWS2812 单珠状态指示GPIO482. 固定引脚映射I2S0INMP441 麦克风I2S0 BCLK → GPIO5I2S0 WS(LRCLK) → GPIO4I2S0 DIN → GPIO6INMP441 注意SD 引脚接 GPIO6L/R 脚拉低 左声道代码配置 ONLY_LEFTI2S1MAX98357 功放I2S1 BCLK → GPIO15I2S1 WS(LRCLK) → GPIO16I2S1 DOUT → GPIO7MAX98357SD 引脚可固定高电平常开无需 MCLKI2S_PIN_NO_CHANGE其他BOOT PTT KEYGPIO0INPUT_PULLUP按下 LOWWS2812 DATAGPIO48供电提示INMP441 / MAX98357 推荐 3.3V严禁 5V 直供 INMP441扬声器4Ω/8Ω 小喇叭MAX98357 最大输出注意不要长时间大音量3、 端口分配8888UDP 音频数据流端口传输 PCM 音频包8889UDP 设备发现广播端口beacon 握手识别令牌ESP32S3_INTERCOM_PEER防止局域网内其他 UDP 设备误识别4、 时序说明音频块512 样本16k 单声道 → 单块时长 512/16000 32msUDP 每 32ms 左右发送一包低延迟局域网对讲注意UDP 无重传弱 WiFi 下会丢包、声音卡顿、断音适合近距离稳定 LAN不适合远距离 WiFi5、 已知特性 局限半双工不能同时说话按住发射就听不到对方松开才接收UDP 不可靠无纠错、无重传WiFi 信号差会出现断音、咔哒噪声仅一对设备对讲多 ESP32-S3 同时在线会抢 peer仅 2.4G WiFi5G AP 无法连接依赖 legacy I2S不能直接升级新版 ESP32 Arduino Core二、软件逻辑分析1. WiFi 初始化固定 2.4G WiFiESP32-S3 此代码不支持 5G WiFi连接成功后同时启动两个 UDP 实例udpAudio(8888)、udpDisc(8889)修改WIFI_SSID / WIFI_PASS改成你的路由器 2.4G 信息两台设备配同一个 WiFi2. I2S 双驱动独立运行本方案双 I2S 硬件独立I2S0 专门收 MIC、I2S1 专门推功放互不抢占I2S0(RX)i2s_driver_install接收 INMP441DMA 持续采集到mic_buf[512]I2S1(TX)tx_desc_auto_cleartrue音频包写完自动清理避免杂音残留上电执行i2s_zero_dma_buffer清空 DMA消除上电 POP 爆音3. Peer 自动发现机制流程本机每 1s 向255.255.255.255广播一次识别 TOKEN监听 8889 端口收到合法 TOKEN → 判断不是自己 IP → 更新peerIP、刷新存活时间lastPeerRx3 秒没有收到对端 beacon → 判定 peer 离线清空 peerIP重新进入搜索黄灯闪烁限制同一局域网内只支持一对对讲只会保存最后一个合法 peer多设备会冲突4. PTT 发射流程BOOT(GPIO0) LOW → isPttTxtrue → LED红 → i2s_read 从INMP441读取一帧512样本PCM → udpAudio发包发送到peerIP:8888半双工发射期间不会执行音频接收播放避免自激啸叫5. 音频接收 播放udpAudio解析8888端口数据包 → 读到rx_buf ✅ 内置软件增益处理 int32_t temp raw_sample * gain; 限幅 ±32767/-32768 防止溢出削波失真 → i2s_write送入MAX98357播放 → 更新rxAudioLastTick维持绿灯150ms当前增益 1.8 倍小声可提高增益过高会破音6. WS2812 状态机优先级顺序PTT 发射中 → 红空闲、无 peer → 黄灯 500ms 闪烁空闲、已有 peer最近 150ms 收到音频 → 绿超时无音频 → ⚫熄灭三、项目源码项目使用 Arduino IDE 开发环境esp32-core 使用 3.0 以上版本。#include Arduino.h #include driver/i2s.h #include WiFi.h #include WiFiUdp.h #include Adafruit_NeoPixel.hESP32 Core 自带内置#include Arduino.hArduino 基础核心头文件所有 Arduino 工程默认内置无需额外操作。#include driver/i2s.hESP32 Arduino Core 自带 ESP-IDF 底层驱动头文件I2S 音频驱动随 esp32 核心包一起安装。#include WiFi.hESP32 内置 WiFi 驱动库属于 ESP32 核心自带。#include WiFiUdp.hUDP 网络通信和 WiFi 配套ESP32 核心原生自带。⚠️ 第三方库必须手动安装#include Adafruit_NeoPixel.hAdafruit NeoPixel控制 WS2812 灯带 / LED安装方法Arduino IDE顶部菜单项目 → 加载库 → 管理库搜索Adafruit NeoPixel作者Adafruit点击安装建议安装最新稳定版#include Arduino.h #include driver/i2s.h #include WiFi.h #include WiFiUdp.h #include Adafruit_NeoPixel.h // CONFIG const char* WIFI_SSID wifi名称; const char* WIFI_PASS wifi密码; const uint16_t AUDIO_PORT 8888; // Audio stream port const uint16_t DISC_PORT 8889; // Discovery broadcast port const char* DISC_TOKEN ESP32S3_INTERCOM_PEER; #define SAMPLE_RATE 16000 #define BUFFER_SAMPLES 512 #define BOOT_KEY_PIN 0 #define WS2812_PIN 48 #define RX_LED_HOLD_MS 150 #define DISC_INTERVAL 1000 #define PEER_TIMEOUT 3000 #define SEARCH_BLINK_MS 500 // 黄灯闪烁周期 // // I2S PIN DEF (same as your reference i2s demo) #define I2S0_BCLK 5 #define I2S0_WS 4 #define I2S0_IN 6 #define I2S1_BCLK 15 #define I2S1_WS 16 #define I2S1_OUT 7 // WiFiUDP udpAudio; WiFiUDP udpDisc; Adafruit_NeoPixel led Adafruit_NeoPixel(1, WS2812_PIN, NEO_GRB NEO_KHZ800); int16_t mic_buf[BUFFER_SAMPLES]; int16_t rx_buf[BUFFER_SAMPLES]; // Peer discovery state IPAddress peerIP; unsigned long lastDiscSend 0; unsigned long lastPeerRx 0; unsigned long blinkTimer 0; bool blinkState false; // I2S Config (unchanged) const i2s_config_t i2s_config_rx { .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 0, .dma_buf_count 4, .dma_buf_len BUFFER_SAMPLES, .use_apll true, .tx_desc_auto_clear false, .fixed_mclk 0 }; const i2s_config_t i2s_config_tx { .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 0, .dma_buf_count 4, .dma_buf_len BUFFER_SAMPLES, .use_apll true, .tx_desc_auto_clear true, .fixed_mclk 0 }; const i2s_pin_config_t pin_cfg_rx { .mck_io_num I2S_PIN_NO_CHANGE, .bck_io_num I2S0_BCLK, .ws_io_num I2S0_WS, .data_out_num I2S_PIN_NO_CHANGE, .data_in_num I2S0_IN }; const i2s_pin_config_t pin_cfg_tx { .mck_io_num I2S_PIN_NO_CHANGE, .bck_io_num I2S1_BCLK, .ws_io_num I2S1_WS, .data_out_num I2S1_OUT, .data_in_num I2S_PIN_NO_CHANGE }; bool isPttTx false; unsigned long rxAudioLastTick 0; // FUNCTION void setLed(uint8_t r, uint8_t g, uint8_t b){ led.setPixelColor(0, led.Color(r,g,b)); led.show(); } void wifiInit(){ Serial.print(Connect WiFi: ); Serial.println(WIFI_SSID); WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASS); while(WiFi.status() ! WL_CONNECTED){ delay(300); Serial.print(.); } Serial.printf(\nWiFi Connected, Local IP: %s\n, WiFi.localIP().toString().c_str()); udpAudio.begin(AUDIO_PORT); udpDisc.begin(DISC_PORT); Serial.printf(Audio Port:%d | Discovery Port:%d\n,AUDIO_PORT,DISC_PORT); } void i2sAudioInit(){ i2s_driver_install(I2S_NUM_0, i2s_config_rx, 0, NULL); i2s_set_pin(I2S_NUM_0, pin_cfg_rx); i2s_zero_dma_buffer(I2S_NUM_0); i2s_driver_install(I2S_NUM_1, i2s_config_tx, 0, NULL); i2s_set_pin(I2S_NUM_1, pin_cfg_tx); i2s_zero_dma_buffer(I2S_NUM_1); Serial.println(I2S Audio Hardware Ready); } // Discovery: broadcast beacon listen peer beacon void peerDiscoveryTask(){ unsigned long now millis(); // Send broadcast beacon if(now - lastDiscSend DISC_INTERVAL){ udpDisc.beginPacket(IPAddress(255,255,255,255), DISC_PORT); udpDisc.write((uint8_t*)DISC_TOKEN, strlen(DISC_TOKEN)); udpDisc.endPacket(); lastDiscSend now; } // Receive peer beacon int pktLen udpDisc.parsePacket(); if(pktLen 0){ char buf[64] {0}; udpDisc.readBytes(buf, pktLen); // Skip self packet if(udpDisc.remoteIP() WiFi.localIP()) return; if(strcmp(buf, DISC_TOKEN) 0){ // First time found peer if(peerIP ! udpDisc.remoteIP()){ peerIP udpDisc.remoteIP(); Serial.print(✅ Found Peer IP: ); Serial.println(peerIP); } lastPeerRx now; } } // Peer timeout, clear if(peerIP ! IPAddress(0,0,0,0) (now - lastPeerRx PEER_TIMEOUT)){ Serial.println(❌ Peer offline, re-searching...); peerIP IPAddress(0,0,0,0); } } void audioPttTransmit(){ // No peer, skip transmit if(peerIP IPAddress(0,0,0,0)) return; size_t read_len 0; i2s_read(I2S_NUM_0, mic_buf, sizeof(mic_buf), read_len, 10); if(read_len 0) return; udpAudio.beginPacket(peerIP, AUDIO_PORT); udpAudio.write((uint8_t*)mic_buf, read_len); udpAudio.endPacket(); } /* void audioReceivePlay(){ int pkt_len udpAudio.parsePacket(); if(pkt_len 0 || pkt_len sizeof(rx_buf)) return; udpAudio.readBytes((uint8_t*)rx_buf, pkt_len); size_t written 0; i2s_write(I2S_NUM_1, rx_buf, pkt_len, written, portMAX_DELAY); rxAudioLastTick millis(); }*/ void audioReceivePlay(){ int pkt_len udpAudio.parsePacket(); if(pkt_len 0 || pkt_len sizeof(rx_buf)) return; udpAudio.readBytes((uint8_t*)rx_buf, pkt_len); // 接收端软件增益 int gain 1.8; int sampleCnt pkt_len / sizeof(int16_t); for(int i0; isampleCnt; i){ int32_t temp (int32_t)rx_buf[i] * gain; if(temp 32767) temp 32767; if(temp -32768) temp -32768; rx_buf[i] (int16_t)temp; } // size_t written 0; i2s_write(I2S_NUM_1, rx_buf, pkt_len, written, portMAX_DELAY); rxAudioLastTick millis(); } // void setup() { Serial.begin(115200); pinMode(BOOT_KEY_PIN, INPUT_PULLUP); led.begin(); setLed(0,0,0); wifiInit(); i2sAudioInit(); Serial.println( WiFi LAN Intercom [Auto Discovery] Ready ); Serial.println( Searching peer device...); } void loop() { unsigned long now millis(); peerDiscoveryTask(); bool keyDown (digitalRead(BOOT_KEY_PIN) LOW); if(keyDown){ if(!isPttTx){ isPttTx true; setLed(255,0,0); Serial.println(PTT START TALK); } audioPttTransmit(); }else{ if(isPttTx){ isPttTx false; Serial.println(PTT STOP TALK); } audioReceivePlay(); if(!isPttTx){ // 优先级找到对等设备 - 接收绿灯/熄灭未找到 - 黄灯闪烁 if(peerIP IPAddress(0,0,0,0)){ if(now - blinkTimer SEARCH_BLINK_MS){ blinkTimer now; blinkState !blinkState; if(blinkState){ setLed(255,255,0); // Yellow }else{ setLed(0,0,0); } } }else{ if(millis() - rxAudioLastTick RX_LED_HOLD_MS){ setLed(0,255,0); }else{ setLed(0,0,0); } } } } }四、编译与测试1、 项目编译说明Arduino IDE 板选择ESP32S3 Dev ModuleFlash/PSRAM 按你的开发板正常配置PSRAM 不强制开启当前缓冲区很小不依赖 PSRAM编译前修改项两台设备填相同 WiFiconst char* WIFI_SSID wifi密码名称; const char* WIFI_PASS wifi密码;其余参数两台设备完全不需要修改直接烧录相同固件即可配对对讲2、 快速上手操作步骤两台 ESP32-S3 按完全一样的原理图焊接 INMP441、MAX98357、喇叭修改 WiFi 名称密码两台烧完全相同代码上电黄灯闪烁 搜索识别 peer 后黄灯熄灭按住 BOOT 键说话红灯亮松开即可收听对方收到声音闪绿灯3、 调试排查清单1. 找不到 peer、黄灯一直闪✅ 两台设备连同一个 2.4G WiFi不要一个 2.4G 一个 5G✅ 路由器是否隔离 AP 客户端AP 隔离开启→广播包无法互通必须关闭✅ 串口看是否打印Found Peer IP收不到 beacon 优先排查 AP 隔离2.能找到 peer按下说话对方无声INMP441 接线、LR 引脚电平、I2S0 BCLK/WS/DIN串口观察是否持续 PTT 打印PTT START TALK示波器逻辑分析仪看 I2S0 有没有波形3. 收到信号、绿灯闪但喇叭无声 / 杂音大MAX98357 I2S1 引脚确认 BCLK/WS/DOUT增益不要过大检查 MAX98357 SD 使能脚拉高上电 i2s_zero_dma_buffer 减少 POP 噪声4. 爆音、破音增益调低确认限幅代码生效WiFi 干扰、丢包也会带来咔嗒噪声UDP 固有