M-Design设计竞赛 基于ESP32-S3实现的局域网UDP单向语音监听器
该项目使用了ESP32S3,实现了局域网单向音频监听器的设计,它的主要功能为:通过两块ESP32S3开发板将I2S音频数据通过UDP协议进行传输和反响解析播放。
标签
嵌入式系统
数字逻辑
显示
开发板
接口
Wang Chong
更新2025-04-01
61

项目介绍和创意介绍

当前的项目参加无线通信、物联网和物联网方向的内容竞赛。


ESP32-S3局域网UDP单向语音监听器, 是一款基于ESP32S3和INMP441, MAX98357开发的一款可以在局域网内进行音频传输和播放的对讲设备. 项目通过使用ESP32S3读取来自INMP441的I2S数据,然后将数据通过UDP协议发送到另一台局域网内的ESP32-S3设备. 服务端通过对客户端数据的读取,从而配置I2S输出,来驱动MAX98357来实现语音的实时播放. 理论上支持双向收发,但是由于我手上并没有另一个MAX98357, 所以仅仅实现了单向音频传输的功能.

硬件介绍

ESP32S3N8R8 是乐鑫推出的一款WIFI和蓝牙射频芯片. 具有高性能的双核心处理器. N8R8即具备8MB的flash和8MB的PSRAM, 可以为应用程序提供充足的容量.

image.png

INMP441 是一款 高性能、低功耗数字 MEMS(微机电系统)麦克风,通过 I²S(Inter-IC Sound)接口 直接输出 数字音频数据,无需外部模数转换(ADC),非常适用于嵌入式音频采集应用,如语音识别、音频录制等。

image.png

MAX98357 是一款 I²S 数字音频放大器,集成了 DAC(数模转换器)D 类功放,可以将 I²S 数字音频信号 直接转换为 模拟音频输出,驱动 扬声器 进行播放。

image.png

方案框图和项目设计思路介绍

流程图

image.png

设计思路介绍

项目的主要核心处理步骤是在I2S的数据读取和播放上, 当来自INMP441的数据之后, 通过和UDP服务器建立通讯,然后将读取到的音频数据发送给UDP服务器. 然后当UDP服务器接受到来自ESP32-S3-Client的数据的时候, 则将I2S数据发送给MAX98357从而实现了音频数据的播放

原理图和PCB介绍

本次项目所采用的模组是在贸泽电子购买的ESP32S3N8R8模组, 项目的开发板是我前一段时间自己画的. 我之前有一块ESP32S3N4, 由于乐鑫的这个N8R8和N4的外部引脚都是一样的, 所以PCB的设计是一样的. 因此我沿用了之前我的PCB, 并且把N8R8焊接上去来完成了我本次的任务.


主要的特点就是引出了所有的IO方便在本地做一些调试. 然后集成了两个下载电路, 第一个是使用CH340的下载电路. 另一个是ESP32-S3支持的USB to serial. 核心板所有功能已经验证, 全部可用.

WechatIMG34.jpg

左侧为本次使用的N8R8模组, 右侧为N4模组 (右侧少了IO8引出)

软件流程图和关键代码介绍

软件流程图

image.png

首先, client端和server端都初始化WIFI, 然后Server端等待client端进行连接, 当Client端连接到Server端之后便开始读取INMP441的数据,然后通过UDP发送给Server端, 同样,当Server端收到来自Client端的连接之后,便开始从UDP client端读取数据,从而驱动MAX98357进行音频的播放.


关键代码介绍

项目是采用ESP-IDF框架基于VSCODE的esp-idf插件进行开发. 客户端和服务端之间介于传输距离和信号, 并没有采用点对点通讯, 而是两个开发板都连接到路由器, 由路由器进行了一次数据的中继再传输数据到目标设备. 这样的话可以拓展设备的连接距离.


ESP32-S3-Client

对于Client端, 相对于Server端不同的是需要明确Server端的地址, 首先定义下面宏信息

#define UDP_SERVER_IP "192.168.1.114" // 替换成你的服务器 IP
#define UDP_SERVER_PORT 12345 // 替换成你的服务器端口


// 引脚定义
#define I2S_WS_GPIO 6 // LRCLK
#define I2S_SCK_GPIO 7 // BCLK
#define I2S_SD_GPIO 4 // INMP441 的 DOUT (数据输入)
#define I2S_DOUT_GPIO 5 // MAX98357 的 DIN (数据输出)


// I2S 配置
#define I2S_SAMPLE_RATE 44100 // 采样率
#define I2S_BUFFER_SIZE 512 // 缓冲区大小


// Wi-Fi 配置
#define WIFI_SSID "WIFI ID"
#define WIFI_PASSWORD "Wi-Fi密码"


// 日志标签
static const char *TAG = "AUDIO_CAPTURE";


在程序的主入口初始化I2S,需要注意的一点是, 这里初始化I2S的时候仅仅初始化了I2S的输入, 并没有初始化输出, 因为我们在Client端的主要任务就是接收INMP441的数据. 然后启动UDP服务.

void app_main(void)
{
// 初始化 Wi-Fi
ESP_LOGI(TAG, "Initializing Wi-Fi...");
wifi_init_sta();
vTaskDelay(5000 / portTICK_PERIOD_MS); // 等待 5 秒,确保 Wi-Fi 连接成功


// 初始化 I2S
ESP_LOGI(TAG, "Initializing I2S...");
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = I2S_SAMPLE_RATE,
.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 = 8,
.dma_buf_len = I2S_BUFFER_SIZE,
.use_apll = false};
i2s_pin_config_t pin_config = {
.bck_io_num = I2S_SCK_GPIO,
.ws_io_num = I2S_WS_GPIO,
.data_out_num = -1,
.data_in_num = I2S_SD_GPIO};
i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
i2s_set_pin(I2S_NUM_0, &pin_config);


// 启动 UDP 客户端任务
xTaskCreate(udp_client_task, "udp_client", 4096, NULL, 5, NULL);
}

WIFI的连接代码, 我这里省略了, 没有做详细的说明. 接下来是对应的UDP task, 其主要作用就是连接服务器,然后将I2S数据发送给服务器

static void udp_client_task(void *pvParameters)
{
struct sockaddr_in server_addr;
int sock;
char udp_buffer[I2S_BUFFER_SIZE]; // 存放 I2S 数据的缓冲区


// 创建 UDP Socket
sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sock < 0)
{
ESP_LOGE(TAG, "Socket creation failed");
vTaskDelete(NULL);
return;
}


// 配置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(UDP_SERVER_PORT);
server_addr.sin_addr.s_addr = inet_addr(UDP_SERVER_IP);


ESP_LOGI(TAG, "UDP client started, sending audio data...");


size_t bytes_read = 0;
while (1)
{
// 读取 I2S 音频数据
i2s_read(I2S_NUM_0, udp_buffer, sizeof(udp_buffer), &bytes_read, portMAX_DELAY);
if (bytes_read > 0)
{
// 发送数据到服务器
sendto(sock, udp_buffer, bytes_read, 0, (struct sockaddr *)&server_addr, sizeof(server_addr));
}
}


// 关闭 Socket(不会执行到这里)
close(sock);
vTaskDelete(NULL);
}

这样的话客户端的所有代码已经完毕了. 如果想要测试UDP数据是否被正常发送了, 可以在电脑端启动一个Python的UDP服务器,来进行测试验证.

import socket
import struct

UDP_IP = "0.0.0.0" # 监听所有 IP
UDP_PORT = 12345 # 和 ESP32 端口匹配

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((UDP_IP, UDP_PORT))

print("Listening for incoming audio data...")

while True:
data, addr = sock.recvfrom(1024)
print(f"Received {len(data)} bytes from {addr}")

num_samples = len(data) // 4
samples = struct.unpack(f"{num_samples}i", data)

print("Audio Samples:", samples[:10])

ESP32-S3-server

i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
.sample_rate = I2S_SAMPLE_RATE,
.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 = 8,
.dma_buf_len = I2S_BUFFER_SIZE,
.use_apll = false};
i2s_pin_config_t pin_config = {
.bck_io_num = I2S_SCK_GPIO,
.ws_io_num = I2S_WS_GPIO,
.data_out_num = I2S_DOUT_GPIO, // 连接到 MAX98357
.data_in_num = -1}; // 禁用输入

Server 端和Client端代码不同的地方是在于, 这里的I2S并不需要接收数据, 因为我们只连接了MAX98357, 因此只需要将数据从UDPClient里读取到然后发送出去即可, 所以仅仅配置了TX模式.


/ UDP 服务器任务
static void udp_server_task(void *pvParameters)
{
struct sockaddr_in server_addr, client_addr;
int sock;
socklen_t client_addr_len = sizeof(client_addr);
char udp_buffer[I2S_BUFFER_SIZE]; // 存放音频数据


// 创建 UDP Socket
sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sock < 0)
{
ESP_LOGE(TAG, "Socket creation failed");
vTaskDelete(NULL);
return;
}


// 绑定端口
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(UDP_SERVER_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有 IP


if (bind(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
ESP_LOGE(TAG, "Socket bind failed");
close(sock);
vTaskDelete(NULL);
return;
}


ESP_LOGI(TAG, "UDP server started, waiting for audio data...");


size_t bytes_written = 0;
while (1)
{
// 接收 UDP 数据
int len = recvfrom(sock, udp_buffer, sizeof(udp_buffer), 0, (struct sockaddr *)&client_addr, &client_addr_len);
if (len > 0)
{
// 将数据写入 I2S(播放音频)
i2s_write(I2S_NUM_0, udp_buffer, len, &bytes_written, portMAX_DELAY);
}
}


// 关闭 Socket(不会执行到这里)
close(sock);
vTaskDelete(NULL);
}

对应的UDPServer task, 和client的不同的是: 这里主要负责数据的读取和使用MAX98357播放.

功能展示及说明

实际的效果为音频效果, 无法通过眼睛观察到, 请查看视频.

设计中遇到的难题和解决方法

项目设计中遇见的难题是最初的时候对I2S的库的使用, 以及对UDP数据收发的控制. 首先ESP-IDF现在并不推荐使用之前的I2S.h库, 而是将之前的I2S.h分成了其余三个功能粒度细化对应不同I2S音频的小库. 所以在熟悉对应的API的时候具备一定的难度. 对于UDP的数据传输,我觉得实现的非常好.因为对于音频数据而言,没有人在乎是否在传输途中是否丢包. 目前实际测试起来效果非常不错.

对本次竞赛的心得体会

这次竞赛让我学习到了很多的音频处理的知识, 之前我一直都想DIY一些音频处理方面的小作品. 这次终于可以将想法落地了. 之后也会尝试一下将音频数据增加WAV头,然后保存到Flash里发送给ASR然后再将文本数据发送给大模型. 然后调用TTS服务来DIY一个对话机器人. 非常感谢电子森林和贸泽电子提供的本次竞赛机会!


软硬件
元器件
ESP32-S3(R8)
应用领域:蓝牙,WiFi 频率范围:2.412GHz~2.484GHz 数据速率:150Mbps 灵敏度:-98.4dBm 接口:I2C,I2S,SPI,UART,USB
电路图
附件下载
Client and Server code.zip
SCH_Schematic1_2025-03-13.pdf
ProPrj_圣诞ESP32蓝牙遥控器_2025-03-13.zip
团队介绍
Swinburne University of Technology & Inti international college 学生
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号