一、项目介绍
使用基于ESP32-S3的人工智能硬件实验套件平台,利用核心板、Sense扩展板的数字麦克风与TF卡槽、底板上的OLED模块,以及数字功放模块,实现一个简易的语音点歌音乐盒。音乐文件存储在FAT32格式的TF卡中,由语音控制播放、切换音乐,并在OLED屏幕上显示相关信息。同时提供网页控制界面方便距离控制。
二、硬件介绍
乐鑫信息科技(Espressif Systems)是一家成立于2008年的中国科技企业,总部位于上海,在全球多地设有研发中心与技术支持团队。公司专注于低功耗Wi-Fi与蓝牙芯片的研发,致力于为物联网设备提供高集成度、高可靠性和高性价比的通信与计算平台。自成立以来,乐鑫始终坚持开源开放的技术路线,其芯片的软件开发套件、驱动程序、示例工程等均以开源方式发布,使其在全球开发者社区中建立了极高的认可度,也吸引了许多商家加入乐鑫生态环境。本次套件的核心板供应商Seeed即是其中之一,旗下的XIAO系列囊括了乐鑫的主要产品线,为几乎每一款芯片提供了至少一款超小型核心板。另外,乐鑫公司凭借稳定的产品质量、完善的生态体系以及对开发者友好的支持策略,逐渐成为了全球物联网芯片市场的重要参与者。ESP系列芯片也被广泛应用于智能家居、可穿戴设备、工业控制、消费电子等众多领域。
ESP32-S3是乐鑫在2021年推出的一款集成Wi-Fi和蓝牙低功耗的高性能MCU芯片,采用双核Xtensa LX7架构,主频可达240MHz,相比前代产品在算力、AI加速能力和安全性方面均有显著提升。其内置向量指令集,可对神经网络推理中的卷积、矩阵运算等关键步骤进行加速,使其在语音识别、图像处理等轻量级AI任务中具备更高效率。本次实验中,就是依靠在ESP32-S3芯片上运行带有DSP预处理的微型神经网络,对采集到的语音进行分类识别,以实现语音控制功能。另外,ESP32-S3配备高达512KB SRAM和8MB可寻址PSRAM接口,能够满足更复杂应用的内存需求。同时,ESP32-S3支持高速SPI、I2C、I2S、USB OTG等多种外设接口,具备更强的扩展能力。其USB功能不仅支持设备模式,还支持主机模式,使得芯片能够直接连接键盘、摄像头等USB外设,为应用场景带来更高灵活性。在安全性方面,ESP32-S3加入了硬件加密加速器、安全启动、Flash加密等机制,使其能够满足工业物联网和智能终端对数据安全的更高要求。芯片的低功耗设计也延续了乐鑫一贯的优势,支持多种睡眠模式,并可通过灵活的电源管理策略实现长时间电池供电运行。与前代产品相比,ESP32-S3在功耗与性能之间取得更优平衡,使其能够在智能语音唤醒、图像识别等需要持续感知的场景中保持较低能耗。
本次使用的人工智能硬件实验套件平台以Seeed XIAO ESP32S3 Sense核心板与Sense扩展板为基础,设计了配套的底板和二层底板,并提供了众多模块,只需要使用附带的连接线就可以轻松连接模块与底板,快速搭建各类功能的开发和测试平台。
Seeed XIAO ESP32S3 Sense核心板板载了一片ESP32-S3R8芯片,以及一片8MBytes SPI NOR Flash芯片,作为ESP32-S3的存储,型号为GD25Q64ENIGR,支持QIO 80MHz数据传输模式。另外板上还有IPEX天线插座、按键、橘黄色LED、Type-C USB母座以及几个电源芯片,并通过两侧的排针引出空闲IO。为了和Sense扩展板链接,板上预留了一个B2B插座,引出了Sense扩展板所需的信号引脚和电源。
Seeed XIAO ESP32S3 Sense扩展板为其核心板扩展了TF卡母座、摄像头FPC插座和一个数字麦克风。其中TF卡是以SPI模式连接的,CS引脚默认连接到GPIO21,与核心板上的橘黄色LED共用一个引脚。如果有需要,也可以切换至预留的D2(对应GPIO3)引脚。
人工智能套件扩展板由两块板子组成,第一块通过两列排座与Seeed XIAO ESP32S3 Sense核心板的两列排针相连,额外提供了一个按键、一块OLED屏幕和7个外设接口,用于连接到套件中的各个外围子模块,包括光敏电阻、温湿度模块、超声波收发、蜂鸣器、RGB灯带、RGB LED和舵机。板子边缘还有双排排针(2x7P)用于连接第二块扩展板。
第二块扩展板提供了套件中余下外围子模块的接口,包括扬声器功放、电机驱动、六轴加速度与角度计和电池模块。针对电池模块,还添加了拨动开关用于连接或断开电池供电,以及充放电控制,和将电池分别稳压至3.3V和升压至5V的BOOST电路。
本次项目除核心板外,还需要TF卡、OLED屏幕与扬声器功放,因此根据功能,使用到的模块为:Sense核心板、Sense扩展板、人工智能套件扩展板第一块、扬声器功放模块。
三、系统架构
在ESP-IDF平台上,配合ESP-IDF Arduino库,使用C++语言编程,与TF卡、麦克风、OLED模块和功放模块通信,同时搭建HTTP服务器。代码中分别处理TF卡上的文件、语音信息和HTTP请求,综合后得到指令和应播放的音频数据,输出到数字功放模块实现扬声器播放音频。同时OLED同步显示状态信息。

四、软件架构
在核心1上运行麦克风采样进程、功放控制进程,其中OLED由功放控制进程同时控制。在核心2上运行入口点、初始化代码、语音DSP预处理及神经网络推理进程、TF卡相关逻辑和网页服务器进程。麦克风采样进程持续写入位于PSRAM中的环形内存区域,语音DSP预处理及神经网络推理进程等待环形区域有数据后执行预处理与推理分类,实现语音识别指令,结果与网页服务器的数据均控制从TF卡读取指定音频文件和播放、暂停,并通过修改作为全局变量(也在PSRAM中)的播放音频缓冲区,以及与音频播放进程发送FreeRTOS提供的进程间IPC通信信号实现播放暂停和音乐切换。

五、硬件连接
- 首先连接天线与核心板。天线接口位置在Sense扩展板下,因此必须先安装。
- 其次连接核心板与Sense扩展板。
- 然后将核心板连接到第一层底板上。
- 由于原理图中信息不详,且没有测到第二层底板上的GND与VCC引脚位置,贸然连接风险较高,因此选择单独连接数字功放模块与第一层底板。最后使用USB数据线连接核心板与PC即可。
完成1-2后如下图左图所示。然后将左图(焊好排针)与右图对接。


找到下图所示的紫色扬声器功放模块,用杜邦线将其接到上图右图中的外侧一排母座即可。

最终完成后类似下图:(功放模块在杜邦线另一端,未展示)


六、软件代码和结构
- .ino为入口文件,负责初始化和创建各个进程。另外语音DSP预处理与神经网络推理进程也在这个文件中。
- oled.cpp包含了OLED初始化与通信的具体代码。
- webserverlogic.cpp包含了网页服务器以及音乐控制逻辑。
- core0.cpp包含了运行在核心1上的两个进程,即麦克风采样进程和功放控制进程(音频播放进程)。
- sdfatlogic.cpp包含了一些TF卡和文件系统相关的代码。
- music-license.txt文件包含了本次实验中用到的免版权音乐的信息。
- life-of-riley-by-kevin-macleod.wav、happier-by-sakura-girl.wav、countdown-by-alex-productions.wav为需要拷贝至TF卡根目录的三段演示用音乐。
- 文件夹下是EdgeImpulse编译的神经网络推理模块,提供DSP预处理与神经网络推理功能。
主要代码ino文件内容如下:
#include "FS.h"
#include "SD.h"
#include "picoproj-project-1_inferencing.h"
#include <ESP_I2S.h>
#include <WiFi.h>
#include <WiFiClient.h>
I2SClass I2S;
I2SClass I2S2;
TaskHandle_t h_task_capture_i2s = NULL;
TaskHandle_t h_task_play_i2s = NULL;
TaskHandle_t h_task_playsong_i2s = NULL;
TaskHandle_t h_task_inference = NULL;
extern TaskHandle_t h_task_webserver;
extern void init_sdcard();
extern void listDir(fs::FS &fs, const char *dirname, uint8_t max_depth);
extern int16_t detect_bias(int16_t *i2s_buffer, int buffer_size);
extern void task_capture_i2s(void *_arg);
extern void task_play_i2s(void *_arg);
extern void task_playsong_i2s(void *_arg);
extern int wrapped_minus(int head, int tail);
extern void web_server_init();
extern void task_webserver(void *_arg);
extern void control_core(int cmd);
extern bool load_file(int fno);
extern int32_t parse_wav(File *f);
extern void scan_sdcard();
extern void oled_init();
extern void oled_show(const char *line1, const char *line2 = 0, const char *line3 = 0, const char *line4 = 0);
extern void oled_bar(float a, float b, float c, float d, float e);
extern int16_t *g_i2s_buffer = 0;
extern int g_pos = 0;
extern int g_ei_pos = 0;
extern const int I2S_BUFFER_COUNT = 16000 * 10;
extern const int I2S_BUFFER_SIZE = I2S_BUFFER_COUNT * sizeof(*g_i2s_buffer);
extern const int I2S_BUFFER_WRAP_SIZE = (int)(I2S_BUFFER_SIZE - 16000) & 0xfffffff0;
extern int16_t g_i2s_bias = 0;
extern int g_song_no = 0;
extern int16_t *g_song_buf = 0;
extern int g_song_lenbyte = 0;
extern const char *g_song_title = 0;
extern int g_song_title_len = 0;
extern int g_song_vol = 15;
int16_t *g_p_ei_buffer = 0;
// Set this to true to see e.g. features generated from the raw signal
static bool debug_nn = false;
void setup() {
Serial.begin(115200);
while (!Serial)
;
WiFi.begin("ssid", "wifi_password");
Serial.print("Waiting wifi connection");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
printf("[SETUP] setup() started on core %d\n", xPortGetCoreID());
init_sdcard();
g_song_buf = (int16_t *)malloc(4);
g_song_lenbyte = 4;
g_song_title = "NULL";
g_song_title_len = strlen(g_song_title);
web_server_init();
oled_init();
oled_show("EETREE", "Voice Musicbox");
g_i2s_buffer = (int16_t *)malloc(I2S_BUFFER_SIZE);
if (!I2S.begin(I2S_MODE_PDM_RX, 16000, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO)) {
Serial.println("Failed to initialize I2S!");
while (1)
;
}
if (!I2S2.begin(I2S_MODE_STD, 16000, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO)) {
Serial.println("Failed to initialize I2S2!");
while (1)
;
}
// g_i2s_bias = detect_bias(g_i2s_buffer, I2S_BUFFER_SIZE);
g_i2s_bias = 1350;
assert(EI_CLASSIFIER_LABEL_COUNT == sizeof(ei_classifier_inferencing_categories) / sizeof(ei_classifier_inferencing_categories[0]));
// summary of inferencing settings (from model_metadata.h)
printf("Inferencing settings:\n");
printf("\tInterval: %f ms\n", (float)EI_CLASSIFIER_INTERVAL_MS);
printf("\tFrame size: %d\n", EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE);
printf("\tSample length: %d ms.\n", EI_CLASSIFIER_RAW_SAMPLE_COUNT / 16000 * 1000);
printf("\tNo. of classes: %d\n", EI_CLASSIFIER_LABEL_COUNT);
printf("\tClasses: \n");
for (int i = 0; i < EI_CLASSIFIER_LABEL_COUNT; i++) {
printf("\t\t%d : %s\n", i, ei_classifier_inferencing_categories[i]);
}
printf("\nStarting continious inference in 1 seconds...\n");
delay(1000);
xTaskCreatePinnedToCore(task_capture_i2s, "CaptureTask", 10000, NULL, 1, &h_task_capture_i2s, 0);
// xTaskCreatePinnedToCore(task_play_i2s, "PlayTask", 10000, NULL, 1, &h_task_play_i2s, 0);
xTaskCreatePinnedToCore(task_playsong_i2s, "PlaySongTask", 10000, NULL, 1, &h_task_playsong_i2s, 0);
xTaskCreatePinnedToCore(task_ei, "InferenceTask", 10000, NULL, 1, &h_task_inference, 1);
}
void loop() { delay(1000); }
void task_ei(void *_arg) {
const int EI_BUF_LEN = EI_CLASSIFIER_RAW_SAMPLE_COUNT * sizeof(g_i2s_buffer[0]);
uint64_t last_report = 0;
uint64_t last_infere_time = 0;
const float time_window_coeff = 0.75; // Move half of interval
const int infere_interval_us = EI_CLASSIFIER_RAW_SAMPLE_COUNT / 16000 * 1000000;
const int infere_window_stepforward_us = infere_interval_us * time_window_coeff;
printf("[INFERE] started on core %d\n", xPortGetCoreID());
while (true) {
last_report = esp_timer_get_time();
int delta;
while (true) {
delta = wrapped_minus(g_pos, g_ei_pos);
if (delta >= EI_BUF_LEN) {
break;
}
delay(10);
// if (esp_timer_get_time() - last_report > 1200000) {
// last_report = esp_timer_get_time();
// printf("[INFERE] waiting for buffer refilling, g_pos=%d, g_ei_pos=%d, delta=%d\n", g_pos, g_ei_pos, delta);
// }
}
last_infere_time = esp_timer_get_time();
g_p_ei_buffer = g_i2s_buffer + g_ei_pos / sizeof(g_i2s_buffer[0]);
signal_t signal;
signal.total_length = EI_CLASSIFIER_RAW_SAMPLE_COUNT;
signal.get_data = &ei_feed_data;
ei_impulse_result_t result = {0};
// printf("Start inference g_ei_pos=%d g_pos=%d g_i2s_buffer=%x g_p_ei_buffer=%x\n", g_ei_pos, g_pos, g_i2s_buffer, g_p_ei_buffer);
EI_IMPULSE_ERROR r = run_classifier(&signal, &result, debug_nn);
if (r != EI_IMPULSE_OK) {
printf("ERR: Failed to run classifier (%d)\n", r);
return;
}
// Maximum of prediction
int pred_index = -1;
float pred_value = 0;
for (int i = 0; i < 5; i++) {
if (pred_index < 0 && result.classification[i].value > 0.85) {
pred_index = i;
pred_value = result.classification[0].value;
} else {
pred_index = -2;
break;
}
}
switch (pred_value) {
case 0:
control_core(-4);
break;
case 2:
control_core(-2);
break;
case 3:
control_core(-1);
break;
case 4:
control_core(-3);
break;
}
// Tick control
int busytime = esp_timer_get_time() - last_infere_time;
int time_to_wait_ms = (infere_window_stepforward_us - busytime) / 1000 - 2;
// printf("EI (DSP CLS ANM %d %d %dms. Spare %dms. Busy %dms:\n", result.timing.dsp, result.timing.classification, result.timing.anomaly);
// oled_bar(result.classification[0].value, result.classification[1].value, result.classification[2].value, result.classification[3].value, result.classification[4].value);
// printf(
// "%6.3f %6.3f %6.3f %6.3f %6.3f\n", result.classification[0].value, result.classification[1].value, result.classification[2].value, result.classification[3].value,
// result.classification[4].value
// );
g_ei_pos += (int)(EI_BUF_LEN * time_window_coeff) & 0xfffffff0;
if (g_ei_pos >= I2S_BUFFER_WRAP_SIZE) {
g_ei_pos -= I2S_BUFFER_WRAP_SIZE;
}
delay(time_to_wait_ms);
}
}
static int ei_feed_data(size_t offset, size_t length, float *out_ptr) {
numpy::int16_to_float(g_p_ei_buffer + offset, out_ptr, length);
return 0;
}
#if !defined(EI_CLASSIFIER_SENSOR) || EI_CLASSIFIER_SENSOR != EI_CLASSIFIER_SENSOR_MICROPHONE
#error "Invalid model for current sensor."
#endif
七、测试运行
上位机触发复位,OLED屏幕显示启动画面EETREE Voice Musicbox。随后初始化完成,默认没有音乐在播放,显示NULL(曲目0)。默认音量为15。


网页控制播放指定音乐,读取文件过程中黄灯亮。灯灭后音频加载完成,开始播放。屏幕显示对应信息。下图分别为播放第一首,修改音量为1后播放第二首,以及暂停后的示意图。



网页控制台,使用浏览器访问后的界面:

详细的网页控制与语音控制演示因图片无法体现,烦请移步至测试视频。
八、总结与展望
本次项目探索了人工智能实验平台的组装、开发与调试流程,熟悉了ESP32-S3芯片的功能和在线人工智能开发平台EdgeImpulse的使用。未来可以依托两者结合进行更高级的语音控制项目开发。