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_0(RX) |
|
D 类功放 |
MAX98357A |
音频播放(输出) |
I2S_NUM_1(TX) |
|
PTT 按键 |
板载 BOOT |
发话按键 |
GPIO0(内部上拉) |
|
RGB LED |
WS2812 单珠 |
状态指示 |
GPIO48 |
2. 固定引脚映射
I2S0(INMP441 麦克风)
- I2S0 BCLK → GPIO5
- I2S0 WS(LRCLK) → GPIO4
- I2S0 DIN → GPIO6
INMP441 注意:SD 引脚接 GPIO6,L/R 脚拉低 = 左声道(代码配置 ONLY_LEFT)
I2S1(MAX98357 功放)
- I2S1 BCLK → GPIO15
- I2S1 WS(LRCLK) → GPIO16
- I2S1 DOUT → GPIO7
MAX98357:SD 引脚可固定高电平常开;无需 MCLK(I2S_PIN_NO_CHANGE)
其他
- BOOT PTT KEY:GPIO0(INPUT_PULLUP,按下 = LOW)
- WS2812 DATA:GPIO48
供电提示
- INMP441 / MAX98357 推荐 3.3V,严禁 5V 直供 INMP441
- 扬声器:4Ω/8Ω 小喇叭,MAX98357 最大输出注意不要长时间大音量
3、 端口分配
8888:UDP 音频数据流端口(传输 PCM 音频包)8889:UDP 设备发现广播端口(beacon 握手)- 识别令牌:
ESP32S3_INTERCOM_PEER(防止局域网内其他 UDP 设备误识别)
4、 时序说明
- 音频块:512 样本,16k 单声道 → 单块时长 = 512/16000 = 32ms
- UDP 每 32ms 左右发送一包,低延迟局域网对讲
- 注意:UDP 无重传,弱 WiFi 下会丢包、声音卡顿、断音(适合近距离稳定 LAN,不适合远距离 WiFi)
5、 已知特性 & 局限
- 半双工:不能同时说话(按住发射就听不到对方,松开才接收)
- UDP 不可靠,无纠错、无重传,WiFi 信号差会出现断音、咔哒噪声
- 仅一对设备对讲,多 ESP32-S3 同时在线会抢 peer
- 仅 2.4G WiFi,5G AP 无法连接
- 依赖 legacy I2S,不能直接升级新版 ESP32 Arduino Core
二、软件逻辑分析
1. WiFi 初始化
- 固定 2.4G WiFi,ESP32-S3 此代码不支持 5G WiFi
- 连接成功后同时启动两个 UDP 实例:
udpAudio(8888)、udpDisc(8889)
修改 WIFI_SSID / WIFI_PASS 改成你的路由器 2.4G 信息,两台设备配同一个 WiFi
2. I2S 双驱动独立运行
本方案双 I2S 硬件独立:I2S0 专门收 MIC、I2S1 专门推功放,互不抢占
- I2S0(RX):
i2s_driver_install接收 INMP441,DMA 持续采集到mic_buf[512] - I2S1(TX):
tx_desc_auto_clear=true,音频包写完自动清理,避免杂音残留 - 上电执行
i2s_zero_dma_buffer清空 DMA,消除上电 POP 爆音
3. Peer 自动发现机制
流程:
- 本机每 1s 向
255.255.255.255广播一次识别 TOKEN - 监听 8889 端口,收到合法 TOKEN → 判断不是自己 IP → 更新
peerIP、刷新存活时间lastPeerRx - 3 秒没有收到对端 beacon → 判定 peer 离线,清空 peerIP,重新进入搜索黄灯闪烁
限制:同一局域网内只支持一对对讲(只会保存最后一个合法 peer,多设备会冲突)
4. PTT 发射流程
BOOT(GPIO0) == LOW → isPttTx=true → 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.h>
ESP32 Core 自带内置
#include <Arduino.h>Arduino 基础核心头文件,所有 Arduino 工程默认内置,无需额外操作。#include <driver/i2s.h>ESP32 Arduino Core 自带 ESP-IDF 底层驱动头文件(I2S 音频驱动),随 esp32 核心包一起安装。#include <WiFi.h>ESP32 内置 WiFi 驱动库,属于 ESP32 核心自带。#include <WiFiUdp.h>UDP 网络通信,和 WiFi 配套,ESP32 核心原生自带。
⚠️ 第三方库,必须手动安装
#include <Adafruit_NeoPixel.h>👉 Adafruit 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 i=0; i<sampleCnt; 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 Module Flash/PSRAM 按你的开发板正常配置,PSRAM 不强制开启(当前缓冲区很小,不依赖 PSRAM)
编译前修改项(两台设备填相同 WiFi)
const 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 固有)
更多推荐


所有评论(0)