文档类型:ESP-IDF 通用 API 教程,不绑定单一开发板。
板级例程见 热门例程详解。
目录
- 简介
- ESP32-S3 I2S 基础知识
- ESP-IDF 中的 I2S 配置
- 方案一:使用数字麦克风和数字功放
- 方案二:使用 ES8311 编解码器和 NS4150 功放
- 常见问题与解决方案
- 高级应用
- 总结
- 参考资料
简介
I2S(Inter-IC Sound)是一种专门用于音频设备之间传输数字音频数据的串行总线接口标准。ESP32-S3 提供了多个 I2S 控制器,支持标准 I2S、PDM(脉冲密度调制)、TDM(时分多路复用)等多种模式,可以连接各种数字麦克风、编解码器和功放。
本教程将详细介绍如何在 ESP-IDF 框架下使用 I2S 功能,并提供两种常见的音频解决方案:一种使用数字麦克风和数字功放的方案,另一种使用 ES8311 编解码器和 NS4150 功放的方案。
ESP32-S3 I2S 基础知识
1. I2S 特性
ESP32-S3 的 I2S 控制器具有以下特性:
- 支持标准 I2S、左对齐、右对齐、PDM 和 TDM 模式
- 支持 8/16/24/32 位采样精度
- 支持多种采样率(8kHz ~ 48kHz 常用)
- 支持 DMA 传输,减少 CPU 负担
- 支持全双工通信(同时录音和播放)
2. I2S 信号线
标准 I2S 接口通常包含以下信号线:
- BCLK(位时钟):每个音频采样点的位传输时钟
- WS/LRCK(字选择/左右时钟):指示当前传输的是左声道还是右声道数据
- SD/DIN/DOUT(串行数据):传输音频数据的线路,可以是输入或输出
3. I2S 工作模式
ESP32-S3 的 I2S 支持多种工作模式:
- 标准 I2S 模式:最常用的模式,适用于大多数音频设备
- PDM 模式:用于连接 PDM 麦克风
- TDM 模式:支持多通道音频传输
ESP-IDF 中的 I2S 配置
ESP-IDF 提供了新的 I2S 驱动 API,使用更加简单和灵活。
1. 包含必要的头文件
#include "driver/i2s_std.h"
#include "driver/i2s_pdm.h"
#include "driver/gpio.h"
#include "esp_log.h"
2. I2S 配置步骤
在 ESP-IDF 中配置 I2S 通常遵循以下步骤:
- 创建 I2S 控制器句柄
- 配置 I2S 控制器参数
- 启动 I2S 控制器
- 读取/写入音频数据
- 停止 I2S 控制器并释放资源(如需要)
方案一:使用数字麦克风和数字功放
数字麦克风(如 INMP441)和数字功放(如 MAX98357A)可以直接通过 I2S 接口与 ESP32-S3 连接,无需额外的编解码器。
1. 硬件连接
数字麦克风(INMP441)连接:
- VDD → 3.3V
- GND → GND
- SD → ESP32-S3 I2S_DIN(如 GPIO9)
- WS → ESP32-S3 I2S_WS(如 GPIO45)
- SCK → ESP32-S3 I2S_BCK(如 GPIO8)
- L/R → GND(左声道)或 3.3V(右声道)
数字功放(MAX98357A)连接:
- VIN → 3.3V
- GND → GND
- DIN → ESP32-S3 I2S_DOUT(如 GPIO46)
- BCLK → ESP32-S3 I2S_BCK(如 GPIO8)
- LRC → ESP32-S3 I2S_WS(如 GPIO45)
- SD → GND(正常模式)
- GAIN → 根据需要连接(控制增益)
2. 录音示例(数字麦克风)
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2s_std.h"
#include "driver/gpio.h"
#include "esp_log.h"
static const char *TAG = "i2s_mic_example";
// I2S 引脚定义
#define I2S_BCK_IO 8
#define I2S_WS_IO 45
#define I2S_DIN_IO 9
#define I2S_DOUT_IO -1 // 不使用输出
// I2S 配置参数
#define I2S_SAMPLE_RATE 16000
#define I2S_SAMPLE_BITS 32
#define I2S_CHANNEL_NUM 1
#define I2S_BUFFER_SIZE 1024
void app_main(void)
{
// 创建 I2S 控制器句柄
i2s_chan_handle_t rx_handle;
// 配置 I2S 标准模式通道
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, NULL, &rx_handle));
// 配置 I2S 标准模式
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(I2S_SAMPLE_RATE),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_SAMPLE_BITS, I2S_CHANNEL_NUM),
.gpio_cfg = {
.mclk = I2S_GPIO_UNUSED,
.bclk = I2S_BCK_IO,
.ws = I2S_WS_IO,
.dout = I2S_GPIO_UNUSED,
.din = I2S_DIN_IO,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle, &std_cfg));
// 启动 I2S 控制器
ESP_ERROR_CHECK(i2s_channel_enable(rx_handle));
ESP_LOGI(TAG, "I2S 麦克风初始化完成");
// 分配接收缓冲区
size_t bytes_read = 0;
uint8_t *buffer = (uint8_t *)malloc(I2S_BUFFER_SIZE);
if (buffer == NULL) {
ESP_LOGE(TAG, "无法分配内存");
return;
}
// 录音循环
while (1) {
// 读取麦克风数据
ESP_ERROR_CHECK(i2s_channel_read(rx_handle, buffer, I2S_BUFFER_SIZE, &bytes_read, portMAX_DELAY));
if (bytes_read > 0) {
// 处理音频数据
ESP_LOGI(TAG, "读取 %d 字节音频数据", bytes_read);
// 这里可以添加音频处理代码
// 例如:计算音量、保存到 SD 卡、进行 FFT 分析等
}
}
// 释放资源(实际上不会执行到这里)
free(buffer);
ESP_ERROR_CHECK(i2s_channel_disable(rx_handle));
ESP_ERROR_CHECK(i2s_del_channel(rx_handle));
}
3. 播放示例(数字功放)
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2s_std.h"
#include "driver/gpio.h"
#include "esp_log.h"
static const char *TAG = "i2s_amp_example";
// I2S 引脚定义
#define I2S_BCK_IO 8
#define I2S_WS_IO 45
#define I2S_DIN_IO -1 // 不使用输入
#define I2S_DOUT_IO 46
// I2S 配置参数
#define I2S_SAMPLE_RATE 44100
#define I2S_SAMPLE_BITS 16
#define I2S_CHANNEL_NUM 2
#define I2S_BUFFER_SIZE 1024
// 生成正弦波
void generate_sine_wave(int16_t *buffer, size_t buffer_size, int frequency, int sample_rate)
{
static float phase = 0.0f;
float phase_increment = 2.0f * M_PI * frequency / sample_rate;
for (int i = 0; i < buffer_size / 4; i++) { // 16 位立体声,每个样本 4 字节
float sine_value = sinf(phase);
int16_t sample = (int16_t)(sine_value * 32767.0f); // 转换为 16 位有符号整数
// 左右声道都设置为相同的值
buffer[i * 2] = sample; // 左声道
buffer[i * 2 + 1] = sample; // 右声道
phase += phase_increment;
if (phase >= 2.0f * M_PI) {
phase -= 2.0f * M_PI;
}
}
}
void app_main(void)
{
// 创建 I2S 控制器句柄
i2s_chan_handle_t tx_handle;
// 配置 I2S 标准模式通道
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, NULL));
// 配置 I2S 标准模式
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(I2S_SAMPLE_RATE),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_SAMPLE_BITS, I2S_CHANNEL_NUM),
.gpio_cfg = {
.mclk = I2S_GPIO_UNUSED,
.bclk = I2S_BCK_IO,
.ws = I2S_WS_IO,
.dout = I2S_DOUT_IO,
.din = I2S_GPIO_UNUSED,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &std_cfg));
// 启动 I2S 控制器
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
ESP_LOGI(TAG, "I2S 功放初始化完成");
// 分配发送缓冲区
size_t bytes_written = 0;
int16_t *buffer = (int16_t *)malloc(I2S_BUFFER_SIZE);
if (buffer == NULL) {
ESP_LOGE(TAG, "无法分配内存");
return;
}
// 播放循环
int frequency = 1000; // 1kHz 正弦波
while (1) {
// 生成正弦波
generate_sine_wave(buffer, I2S_BUFFER_SIZE, frequency, I2S_SAMPLE_RATE);
// 写入功放数据
ESP_ERROR_CHECK(i2s_channel_write(tx_handle, buffer, I2S_BUFFER_SIZE, &bytes_written, portMAX_DELAY));
if (bytes_written > 0) {
ESP_LOGI(TAG, "写入 %d 字节音频数据", bytes_written);
}
// 每 5 秒改变频率
static int count = 0;
if (++count >= 50) {
count = 0;
frequency = (frequency == 1000) ? 2000 : 1000;
ESP_LOGI(TAG, "改变频率为 %d Hz", frequency);
}
// 短暂延时
vTaskDelay(pdMS_TO_TICKS(100));
}
// 释放资源(实际上不会执行到这里)
free(buffer);
ESP_ERROR_CHECK(i2s_channel_disable(tx_handle));
ESP_ERROR_CHECK(i2s_del_channel(tx_handle));
}
4. 回声示例(麦克风 + 功放)
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2s_std.h"
#include "driver/gpio.h"
#include "esp_log.h"
static const char *TAG = "i2s_echo_example";
// I2S 引脚定义
#define I2S_BCK_IO 8
#define I2S_WS_IO 45
#define I2S_DIN_IO 9
#define I2S_DOUT_IO 46
// I2S 配置参数
#define I2S_SAMPLE_RATE 16000
#define I2S_SAMPLE_BITS 16
#define I2S_CHANNEL_NUM 1
#define I2S_BUFFER_SIZE 1024
void app_main(void)
{
// 创建 I2S 控制器句柄
i2s_chan_handle_t rx_handle, tx_handle;
// 配置 I2S 标准模式通道(接收)
i2s_chan_config_t rx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
ESP_ERROR_CHECK(i2s_new_channel(&rx_chan_cfg, NULL, &rx_handle));
// 配置 I2S 标准模式(接收)
i2s_std_config_t rx_std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(I2S_SAMPLE_RATE),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_SAMPLE_BITS, I2S_CHANNEL_NUM),
.gpio_cfg = {
.mclk = I2S_GPIO_UNUSED,
.bclk = I2S_BCK_IO,
.ws = I2S_WS_IO,
.dout = I2S_GPIO_UNUSED,
.din = I2S_DIN_IO,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle, &rx_std_cfg));
// 配置 I2S 标准模式通道(发送)
i2s_chan_config_t tx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_1, I2S_ROLE_MASTER);
ESP_ERROR_CHECK(i2s_new_channel(&tx_chan_cfg, &tx_handle, NULL));
// 配置 I2S 标准模式(发送)
i2s_std_config_t tx_std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(I2S_SAMPLE_RATE),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_SAMPLE_BITS, I2S_CHANNEL_NUM),
.gpio_cfg = {
.mclk = I2S_GPIO_UNUSED,
.bclk = I2S_BCK_IO,
.ws = I2S_WS_IO,
.dout = I2S_DOUT_IO,
.din = I2S_GPIO_UNUSED,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &tx_std_cfg));
// 启动 I2S 控制器
ESP_ERROR_CHECK(i2s_channel_enable(rx_handle));
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
ESP_LOGI(TAG, "I2S 回声示例初始化完成");
// 分配缓冲区
size_t bytes_read = 0;
size_t bytes_written = 0;
uint8_t *buffer = (uint8_t *)malloc(I2S_BUFFER_SIZE);
if (buffer == NULL) {
ESP_LOGE(TAG, "无法分配内存");
return;
}
// 回声循环
while (1) {
// 读取麦克风数据
ESP_ERROR_CHECK(i2s_channel_read(rx_handle, buffer, I2S_BUFFER_SIZE, &bytes_read, portMAX_DELAY));
if (bytes_read > 0) {
ESP_LOGI(TAG, "读取 %d 字节音频数据", bytes_read);
// 直接将麦克风数据发送到功放(回声效果)
ESP_ERROR_CHECK(i2s_channel_write(tx_handle, buffer, bytes_read, &bytes_written, portMAX_DELAY));
if (bytes_written > 0) {
ESP_LOGI(TAG, "写入 %d 字节音频数据", bytes_written);
}
}
}
// 释放资源(实际上不会执行到这里)
free(buffer);
ESP_ERROR_CHECK(i2s_channel_disable(rx_handle));
ESP_ERROR_CHECK(i2s_channel_disable(tx_handle));
ESP_ERROR_CHECK(i2s_del_channel(rx_handle));
ESP_ERROR_CHECK(i2s_del_channel(tx_handle));
}
方案二:使用 ES8311 编解码器和 NS4150 功放
ES8311 是一款高性能的低功耗立体声音频编解码器,NS4150 是一款 3W 单声道 D 类音频功放。这种方案适用于需要更高音质的应用。
1. 硬件连接
ES8311 编解码器连接:
- VDD/VDDA/VDDR → 3.3V
- GND/GNDA → GND
- SCLK → ESP32-S3 I2C_SCL(如 GPIO18)
- SDA → ESP32-S3 I2C_SDA(如 GPIO17)
- MCLK → ESP32-S3 I2S_MCLK(如 GPIO2)
- BCLK → ESP32-S3 I2S_BCK(如 GPIO8)
- LRCK → ESP32-S3 I2S_WS(如 GPIO45)
- DOUT → ESP32-S3 I2S_DIN(如 GPIO9)
- DIN → ESP32-S3 I2S_DOUT(如 GPIO46)
NS4150 功放连接:
- VDD → 5V
- GND → GND
- IN+ → ES8311 LOUT/ROUT(根据需要选择左声道或右声道)
- IN- → GND
- OUT+/OUT- → 扬声器
2. 使用 ESP Codec Dev 组件
ESP-IDF 提供了 esp_codec_dev 组件,大大简化了音频编解码器的使用。这个组件提供了统一的 API 接口,支持多种编解码器,包括 ES8311。
2.1 安装 ESP Codec Dev 组件
首先,需要在项目中添加 esp_codec_dev 组件。可以通过 ESP-IDF 组件管理器添加:
cd 你的项目目录
idf.py add-dependency "espressif/esp_codec_dev^1.0.0"
或者在项目的 idf_component.yml 文件中添加依赖:
dependencies:
espressif/esp_codec_dev: "^1.0.0"
2.2 ES8311 + NS4150 示例
下面是一个使用 esp_codec_dev 组件驱动 ES8311 编解码器和 NS4150 功放的完整示例:
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_codec_dev.h"
#include "audio_hal.h"
#include "esp_peripherals.h"
#include "board.h"
static const char *TAG = "es8311_example";
// 音频配置
#define SAMPLE_RATE 44100
#define CHANNEL_NUM 2
#define BITS_PER_SAMPLE 16
// I2S 引脚定义
#define I2S_MCLK_IO 2
#define I2S_BCK_IO 8
#define I2S_WS_IO 45
#define I2S_DIN_IO 9
#define I2S_DOUT_IO 46
// I2C 引脚定义
#define I2C_SCL_IO 18
#define I2C_SDA_IO 17
// 生成正弦波
static void generate_sine_wave(int16_t *buffer, int buffer_size, int frequency, int sample_rate)
{
static float phase = 0.0f;
float phase_increment = 2.0f * M_PI * frequency / sample_rate;
for (int i = 0; i < buffer_size / 4; i++) { // 16 位立体声,每个样本 4 字节
float sine_value = sinf(phase);
int16_t sample = (int16_t)(sine_value * 32767.0f * 0.5f); // 转换为 16 位有符号整数,音量降低一半
// 左右声道都设置为相同的值
buffer[i * 2] = sample; // 左声道
buffer[i * 2 + 1] = sample; // 右声道
phase += phase_increment;
if (phase >= 2.0f * M_PI) {
phase -= 2.0f * M_PI;
}
}
}
void app_main(void)
{
ESP_LOGI(TAG, "初始化 ES8311 编解码器和 NS4150 功放");
// 初始化 I2C 总线
audio_board_i2c_pin_config_t i2c_cfg = {
.i2c_port = I2C_NUM_0,
.i2c_scl = I2C_SCL_IO,
.i2c_sda = I2C_SDA_IO,
};
audio_board_i2c_init(&i2c_cfg);
// 创建编解码器配置
es8311_codec_cfg_t es8311_cfg = {
.i2c_port = I2C_NUM_0,
.i2c_addr = ES8311_ADDR,
.mclk_inv = false,
};
// 创建 I2S 配置
codec_i2s_dev_cfg_t i2s_cfg = {
.port = I2S_NUM_0,
.role = CODEC_I2S_MASTER,
.fmt = CODEC_I2S_PHILIPS,
.sample_rate = SAMPLE_RATE,
.bits_per_sample = BITS_PER_SAMPLE,
.ch_num = CHANNEL_NUM,
.gpio_cfg = {
.mclk = I2S_MCLK_IO,
.bclk = I2S_BCK_IO,
.ws = I2S_WS_IO,
.dout = I2S_DOUT_IO,
.din = I2S_DIN_IO,
},
};
// 创建音频播放配置
codec_dac_dev_cfg_t dac_cfg = {
.dev_type = CODEC_DEV_TYPE_I2S,
.codec_type = CODEC_TYPE_ES8311,
.codec_i2s_cfg = &i2s_cfg,
.es8311_cfg = &es8311_cfg,
};
// 创建音频录制配置
codec_adc_dev_cfg_t adc_cfg = {
.dev_type = CODEC_DEV_TYPE_I2S,
.codec_type = CODEC_TYPE_ES8311,
.codec_i2s_cfg = &i2s_cfg,
.es8311_cfg = &es8311_cfg,
};
// 创建 DAC 设备(播放)
esp_codec_dev_handle_t dac_dev = esp_codec_dev_new(&dac_cfg);
if (dac_dev == NULL) {
ESP_LOGE(TAG, "创建 DAC 设备失败");
return;
}
// 创建 ADC 设备(录音)
esp_codec_dev_handle_t adc_dev = esp_codec_dev_new(&adc_cfg);
if (adc_dev == NULL) {
ESP_LOGE(TAG, "创建 ADC 设备失败");
esp_codec_dev_close(dac_dev);
return;
}
// 设置音量(0-100)
esp_codec_dev_set_out_vol(dac_dev, 80);
// 设置增益(0-100)
esp_codec_dev_set_in_gain(adc_dev, 70);
ESP_LOGI(TAG, "ES8311 编解码器和 NS4150 功放初始化完成");
// 分配音频缓冲区
size_t buffer_size = 1024;
int16_t *buffer = (int16_t *)malloc(buffer_size);
if (buffer == NULL) {
ESP_LOGE(TAG, "无法分配内存");
esp_codec_dev_close(adc_dev);
esp_codec_dev_close(dac_dev);
return;
}
// 播放正弦波示例
ESP_LOGI(TAG, "播放 1kHz 正弦波...");
int frequency = 1000; // 1kHz 正弦波
for (int i = 0; i < 10; i++) { // 播放 10 次
// 生成正弦波
generate_sine_wave(buffer, buffer_size, frequency, SAMPLE_RATE);
// 播放音频
int ret = esp_codec_dev_write(dac_dev, buffer, buffer_size);
if (ret < 0) {
ESP_LOGE(TAG, "播放音频失败");
} else {
ESP_LOGI(TAG, "播放 %d 字节音频数据", ret);
}
// 短暂延时
vTaskDelay(pdMS_TO_TICKS(500));
}
// 录音回放示例
ESP_LOGI(TAG, "开始录音回放...");
for (int i = 0; i < 5; i++) { // 录音回放
# ESP-IDF I2S 音频开发教程(续)
## 方案二:使用 ES8311 编解码器和 NS4150 功放(续)
```c
// 录音回放示例
ESP_LOGI(TAG, "开始录音回放...");
for (int i = 0; i < 5; i++) { // 录音回放 5 次
// 录音
ESP_LOGI(TAG, "开始录音...");
int read_len = esp_codec_dev_read(adc_dev, buffer, buffer_size);
if (read_len < 0) {
ESP_LOGE(TAG, "录音失败");
continue;
}
ESP_LOGI(TAG, "录制了 %d 字节音频数据", read_len);
// 短暂延时
vTaskDelay(pdMS_TO_TICKS(500));
// 回放
ESP_LOGI(TAG, "开始回放...");
int write_len = esp_codec_dev_write(dac_dev, buffer, read_len);
if (write_len < 0) {
ESP_LOGE(TAG, "回放失败");
} else {
ESP_LOGI(TAG, "回放 %d 字节音频数据", write_len);
}
// 短暂延时
vTaskDelay(pdMS_TO_TICKS(1000));
}
// 释放资源
free(buffer);
esp_codec_dev_close(adc_dev);
esp_codec_dev_close(dac_dev);
ESP_LOGI(TAG, "示例完成");
}
3. 播放 MP3 文件示例
如果需要播放 MP3 文件,可以结合 ESP-ADF(ESP Audio Development Framework)使用。以下是一个简单的示例:
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_codec_dev.h"
#include "audio_hal.h"
#include "audio_element.h"
#include "audio_pipeline.h"
#include "audio_event_iface.h"
#include "mp3_decoder.h"
#include "i2s_stream.h"
#include "fatfs_stream.h"
#include "esp_peripherals.h"
#include "periph_sdcard.h"
#include "board.h"
static const char *TAG = "mp3_example";
void app_main(void)
{
ESP_LOGI(TAG, "初始化 ES8311 编解码器和 NS4150 功放");
// 初始化外设管理器
esp_periph_config_t periph_cfg = DEFAULT_ESP_PERIPH_SET_CONFIG();
esp_periph_set_handle_t set = esp_periph_set_init(&periph_cfg);
// 初始化 SD 卡
periph_sdcard_cfg_t sdcard_cfg = {
.root = "/sdcard",
.card_detect_pin = GPIO_NUM_34, // 根据实际硬件调整
};
esp_periph_handle_t sdcard = periph_sdcard_init(&sdcard_cfg);
esp_periph_start(set, sdcard);
// 等待 SD 卡就绪
while (!periph_sdcard_is_mounted(sdcard)) {
vTaskDelay(pdMS_TO_TICKS(100));
}
// 创建音频管道
audio_pipeline_handle_t pipeline = audio_pipeline_init(&(audio_pipeline_cfg_t){
.rb_size = 8192,
});
// 创建 fatfs 流,用于读取 SD 卡上的 MP3 文件
fatfs_stream_cfg_t fatfs_cfg = FATFS_STREAM_CFG_DEFAULT();
fatfs_cfg.type = AUDIO_STREAM_READER;
audio_element_handle_t fatfs_stream = fatfs_stream_init(&fatfs_cfg);
// 创建 MP3 解码器
mp3_decoder_cfg_t mp3_cfg = DEFAULT_MP3_DECODER_CONFIG();
audio_element_handle_t mp3_decoder = mp3_decoder_init(&mp3_cfg);
// 创建 I2S 流,用于输出到 ES8311
i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT();
i2s_cfg.type = AUDIO_STREAM_WRITER;
i2s_cfg.i2s_port = I2S_NUM_0;
i2s_cfg.i2s_config.sample_rate = 44100;
i2s_cfg.i2s_config.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT;
i2s_cfg.i2s_config.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT;
audio_element_handle_t i2s_stream = i2s_stream_init(&i2s_cfg);
// 注册音频元素到管道
audio_pipeline_register(pipeline, fatfs_stream, "file");
audio_pipeline_register(pipeline, mp3_decoder, "mp3");
audio_pipeline_register(pipeline, i2s_stream, "i2s");
// 连接音频元素
audio_pipeline_link(pipeline, (const char *[]){"file", "mp3", "i2s"}, 3);
// 设置 URI(MP3 文件路径)
audio_element_set_uri(fatfs_stream, "/sdcard/test.mp3");
// 启动管道
audio_pipeline_run(pipeline);
// 等待播放完成
while (1) {
audio_event_iface_msg_t msg;
esp_err_t ret = audio_event_iface_listen(audio_pipeline_get_event_iface(pipeline), &msg, portMAX_DELAY);
if (ret != ESP_OK) {
continue;
}
if (msg.source_type == AUDIO_ELEMENT_TYPE_ELEMENT &&
msg.source == (void *)mp3_decoder &&
msg.cmd == AEL_MSG_CMD_REPORT_STATUS) {
audio_element_state_t el_state = audio_element_get_state(mp3_decoder);
if (el_state == AEL_STATE_FINISHED) {
ESP_LOGI(TAG, "播放完成");
break;
}
}
}
// 停止管道
audio_pipeline_stop(pipeline);
audio_pipeline_wait_for_stop(pipeline);
audio_pipeline_terminate(pipeline);
// 释放资源
audio_pipeline_unregister(pipeline, fatfs_stream);
audio_pipeline_unregister(pipeline, mp3_decoder);
audio_pipeline_unregister(pipeline, i2s_stream);
audio_element_deinit(fatfs_stream);
audio_element_deinit(mp3_decoder);
audio_element_deinit(i2s_stream);
audio_pipeline_deinit(pipeline);
esp_periph_set_stop_all(set);
esp_periph_set_destroy(set);
ESP_LOGI(TAG, "示例完成");
}
常见问题与解决方案
1. 音频噪声问题
如果音频输出有噪声,可能的原因和解决方法:
-
电源噪声:
- 使用滤波电容稳定电源
- 使用独立的模拟电源
- 优化 PCB 布局,减少干扰
-
时钟抖动:
- 确保 MCLK 信号稳定
- 使用更高质量的晶振
- 减少时钟线路的长度
-
接地问题:
- 确保良好的接地连接
- 分离数字地和模拟地
- 避免接地环路
2. 音频失真
如果音频输出失真,可能的原因和解决方法:
-
音量过大:
- 降低音量设置
- 检查信号链中的增益设置
-
采样率不匹配:
- 确保所有设备使用相同的采样率
- 使用采样率转换
-
位深度不匹配:
- 确保位深度设置正确
- 使用适当的量化和抖动
3. 无声音输出
如果没有声音输出,可能的原因和解决方法:
-
硬件连接问题:
- 检查所有引脚连接
- 确保扬声器正常工作
-
软件配置问题:
- 检查 I2S 配置参数
- 确保音量设置不为零
- 检查静音设置
-
编解码器初始化失败:
- 检查 I2C 地址是否正确
- 确保编解码器电源正常
- 检查初始化序列
高级应用
1. 音频处理
ESP32-S3 可以进行简单的音频处理,如音量控制、均衡器等:
// 音量控制函数
void adjust_volume(int16_t *buffer, size_t size, float volume)
{
for (int i = 0; i < size / 2; i++) {
buffer[i] = (int16_t)(buffer[i] * volume);
}
}
// 简单的低通滤波器
void low_pass_filter(int16_t *buffer, size_t size, float alpha)
{
static int16_t prev_sample = 0;
for (int i = 0; i < size / 2; i++) {
buffer[i] = (int16_t)(alpha * buffer[i] + (1.0f - alpha) * prev_sample);
prev_sample = buffer[i];
}
}
2. 语音识别
ESP32-S3 可以进行简单的语音识别,如关键词检测:
#include "esp_wn_iface.h"
#include "esp_wn_models.h"
#include "esp_mn_iface.h"
#include "esp_mn_models.h"
// 语音唤醒回调函数
static esp_err_t wake_word_cb(void *arg)
{
ESP_LOGI(TAG, "检测到唤醒词!");
return ESP_OK;
}
// 语音命令回调函数
static esp_err_t speech_cmd_cb(void *arg)
{
ESP_LOGI(TAG, "检测到语音命令!");
return ESP_OK;
}
// 初始化语音识别
void init_speech_recognition(void)
{
// 创建多网络接口
esp_mn_iface_t *multinet = esp_mn_handle_from_name("multinet");
// 加载模型
model_iface_data_t *model_data = multinet->create(NULL, NULL);
// 注册回调函数
multinet->set_callback(model_data, wake_word_cb, speech_cmd_cb, NULL);
// 添加命令词
multinet->add_speech_commands(model_data, "打开灯", 1);
multinet->add_speech_commands(model_data, "关闭灯", 2);
// 在音频处理循环中调用识别函数
// multinet->detect(model_data, audio_buffer, buffer_size);
}
3. 蓝牙音频
ESP32-S3 不支持蓝牙 A2DP,只有ESP32支持,还需要使用ESP-ADF,可以实现蓝牙音频接收和发送:
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_bt_device.h"
#include "esp_gap_bt_api.h"
#include "esp_a2dp_api.h"
#include "esp_avrc_api.h"
// 初始化蓝牙 A2DP 接收器
void init_bluetooth_a2dp_sink(void)
{
// 初始化 NVS
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
// 初始化蓝牙控制器
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_bt_controller_init(&bt_cfg));
ESP_ERROR_CHECK(esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT));
// 初始化蓝牙主机
ESP_ERROR_CHECK(esp_bluedroid_init());
ESP_ERROR_CHECK(esp_bluedroid_enable());
// 设置设备名称
esp_bt_dev_set_device_name("ESP32-S3 BT Speaker");
// 初始化 A2DP 接收器
esp_a2d_sink_init();
// 注册回调函数
esp_a2d_register_callback(bt_app_a2d_cb);
esp_a2d_sink_register_data_callback(bt_app_a2d_data_cb);
// 设置可发现和可连接模式
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
}
ESP-IDF I2S 音频开发教程(续)
方案三:使用 ES7210 + ES8311 + NS4150 组合
ES7210 是一款高性能的四通道 ADC 编解码器,特别适合用于麦克风阵列应用。结合 ES8311 和 NS4150,可以实现高质量的音频录制和播放系统,非常适合语音识别、会议系统等应用场景。
1. 硬件连接
ES7210 麦克风阵列编解码器连接:
- VDD → 3.3V
- GND → GND
- SCL → ESP32-S3 I2C_SCL(如 GPIO18)
- SDA → ESP32-S3 I2C_SDA(如 GPIO17)
- MCLK → ESP32-S3 I2S_MCLK(如 GPIO2)
- SCLK → ESP32-S3 I2S_BCK(如 GPIO8)
- LRCK → ESP32-S3 I2S_WS(如 GPIO45)
- SDOUT → ESP32-S3 I2S_DIN(如 GPIO9)
- MIC1~MIC4 → 麦克风阵列
ES8311 编解码器连接:
(与方案二相同)
NS4150 功放连接:
(与方案二相同)
2. 使用 ESP Codec Dev 组件驱动 ES7210 + ES8311 + NS4150
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_codec_dev.h"
#include "audio_hal.h"
#include "esp_peripherals.h"
#include "board.h"
static const char *TAG = "es7210_es8311_example";
// 音频配置
#define SAMPLE_RATE 48000
#define CHANNEL_NUM 2 // ES8311 播放通道数
#define MIC_CHANNEL_NUM 4 // ES7210 麦克风通道数
#define BITS_PER_SAMPLE 16
// I2S 引脚定义
#define I2S_MCLK_IO 2
#define I2S_BCK_IO 8
#define I2S_WS_IO 45
#define I2S_DIN_IO 9 // ES7210 SDOUT 连接到这里
#define I2S_DOUT_IO 46 // ES8311 DIN 连接到这里
// I2C 引脚定义
#define I2C_SCL_IO 18
#define I2C_SDA_IO 17
// 音频处理函数 - 简单的通道混合(将4通道混合为2通道)
static void process_audio(int16_t *src, int16_t *dst, int samples)
{
for (int i = 0; i < samples / MIC_CHANNEL_NUM; i++) {
// 将4个麦克风通道混合为2个通道(左右声道)
// 麦克风1和2混合到左声道,麦克风3和4混合到右声道
dst[i * 2] = (src[i * MIC_CHANNEL_NUM] + src[i * MIC_CHANNEL_NUM + 1]) / 2; // 左声道
dst[i * 2 + 1] = (src[i * MIC_CHANNEL_NUM + 2] + src[i * MIC_CHANNEL_NUM + 3]) / 2; // 右声道
}
}
void app_main(void)
{
ESP_LOGI(TAG, "初始化 ES7210 + ES8311 + NS4150 音频系统");
// 初始化 I2C 总线
audio_board_i2c_pin_config_t i2c_cfg = {
.i2c_port = I2C_NUM_0,
.i2c_scl = I2C_SCL_IO,
.i2c_sda = I2C_SDA_IO,
};
audio_board_i2c_init(&i2c_cfg);
// 创建 ES8311 编解码器配置(播放)
es8311_codec_cfg_t es8311_cfg = {
.i2c_port = I2C_NUM_0,
.i2c_addr = ES8311_ADDR,
.mclk_inv = false,
};
// 创建 ES7210 编解码器配置(录音)
es7210_codec_cfg_t es7210_cfg = {
.i2c_port = I2C_NUM_0,
.i2c_addr = ES7210_ADDR,
.mic_bias = ES7210_MIC_BIAS_2_87V, // 根据麦克风要求设置偏置电压
.mic_gain = ES7210_MIC_GAIN_30DB, // 麦克风增益
.tdm_enable = false, // 不使用 TDM 模式
};
// 创建 I2S 配置(播放)
codec_i2s_dev_cfg_t i2s_tx_cfg = {
.port = I2S_NUM_0,
.role = CODEC_I2S_MASTER,
.fmt = CODEC_I2S_PHILIPS,
.sample_rate = SAMPLE_RATE,
.bits_per_sample = BITS_PER_SAMPLE,
.ch_num = CHANNEL_NUM,
.gpio_cfg = {
.mclk = I2S_MCLK_IO,
.bclk = I2S_BCK_IO,
.ws = I2S_WS_IO,
.dout = I2S_DOUT_IO,
.din = I2S_GPIO_UNUSED, // 播放不需要输入
},
};
// 创建 I2S 配置(录音)
codec_i2s_dev_cfg_t i2s_rx_cfg = {
.port = I2S_NUM_1,
.role = CODEC_I2S_MASTER,
.fmt = CODEC_I2S_PHILIPS,
.sample_rate = SAMPLE_RATE,
.bits_per_sample = BITS_PER_SAMPLE,
.ch_num = MIC_CHANNEL_NUM,
.gpio_cfg = {
.mclk = I2S_MCLK_IO,
.bclk = I2S_BCK_IO,
.ws = I2S_WS_IO,
.dout = I2S_GPIO_UNUSED, // 录音不需要输出
.din = I2S_DIN_IO,
},
};
// 创建音频播放配置
codec_dac_dev_cfg_t dac_cfg = {
.dev_type = CODEC_DEV_TYPE_I2S,
.codec_type = CODEC_TYPE_ES8311,
.codec_i2s_cfg = &i2s_tx_cfg,
.es8311_cfg = &es8311_cfg,
};
// 创建音频录制配置
codec_adc_dev_cfg_t adc_cfg = {
.dev_type = CODEC_DEV_TYPE_I2S,
.codec_type = CODEC_TYPE_ES7210,
.codec_i2s_cfg = &i2s_rx_cfg,
.es7210_cfg = &es7210_cfg,
};
// 创建 DAC 设备(播放)
esp_codec_dev_handle_t dac_dev = esp_codec_dev_new(&dac_cfg);
if (dac_dev == NULL) {
ESP_LOGE(TAG, "创建 DAC 设备失败");
return;
}
// 创建 ADC 设备(录音)
esp_codec_dev_handle_t adc_dev = esp_codec_dev_new(&adc_cfg);
if (adc_dev == NULL) {
ESP_LOGE(TAG, "创建 ADC 设备失败");
esp_codec_dev_close(dac_dev);
return;
}
// 设置音量(0-100)
esp_codec_dev_set_out_vol(dac_dev, 80);
// 设置增益(0-100)
esp_codec_dev_set_in_gain(adc_dev, 70);
ESP_LOGI(TAG, "ES7210 + ES8311 + NS4150 音频系统初始化完成");
// 分配音频缓冲区
size_t mic_buffer_size = 1024 * MIC_CHANNEL_NUM; // 4通道麦克风缓冲区
size_t spk_buffer_size = 1024 * CHANNEL_NUM; // 2通道扬声器缓冲区
int16_t *mic_buffer = (int16_t *)malloc(mic_buffer_size);
int16_t *spk_buffer = (int16_t *)malloc(spk_buffer_size);
if (mic_buffer == NULL || spk_buffer == NULL) {
ESP_LOGE(TAG, "无法分配内存");
free(mic_buffer);
free(spk_buffer);
esp_codec_dev_close(adc_dev);
esp_codec_dev_close(dac_dev);
return;
}
// 麦克风阵列回声示例
ESP_LOGI(TAG, "开始麦克风阵列回声示例...");
for (int i = 0; i < 10; i++) { // 运行10次
// 录音
ESP_LOGI(TAG, "从麦克风阵列录音...");
int read_len = esp_codec_dev_read(adc_dev, mic_buffer, mic_buffer_size);
if (read_len < 0) {
ESP_LOGE(TAG, "录音失败");
continue;
}
ESP_LOGI(TAG, "录制了 %d 字节音频数据(%d 通道)", read_len, MIC_CHANNEL_NUM);
// 处理音频 - 将4通道混合为2通道
process_audio(mic_buffer, spk_buffer, read_len / sizeof(int16_t));
// 短暂延时
vTaskDelay(pdMS_TO_TICKS(100));
// 回放
ESP_LOGI(TAG, "开始回放...");
int write_len = esp_codec_dev_write(dac_dev, spk_buffer, read_len / 2); // 通道数减半,所以缓冲区大小也减半
if (write_len < 0) {
ESP_LOGE(TAG, "回放失败");
} else {
ESP_LOGI(TAG, "回放 %d 字节音频数据(%d 通道)", write_len, CHANNEL_NUM);
}
// 短暂延时
vTaskDelay(pdMS_TO_TICKS(500));
}
// 波束形成示例(简化版)
ESP_LOGI(TAG, "开始波束形成示例...");
// 波束方向(0-3,对应4个麦克风)
int beam_direction = 0;
for (int i = 0; i < 4; i++) { // 尝试4个方向
ESP_LOGI(TAG, "波束方向: %d", beam_direction);
// 录音
int read_len = esp_codec_dev_read(adc_dev, mic_buffer, mic_buffer_size);
if (read_len < 0) {
ESP_LOGE(TAG, "录音失败");
continue;
}
// 简化的波束形成 - 只选择一个麦克风的信号
for (int j = 0; j < read_len / sizeof(int16_t) / MIC_CHANNEL_NUM; j++) {
// 将选定麦克风的信号复制到左右声道
spk_buffer[j * 2] = mic_buffer[j * MIC_CHANNEL_NUM + beam_direction]; // 左声道
spk_buffer[j * 2 + 1] = mic_buffer[j * MIC_CHANNEL_NUM + beam_direction]; // 右声道
}
// 回放
esp_codec_dev_write(dac_dev, spk_buffer, read_len / 2);
// 切换到下一个方向
beam_direction = (beam_direction + 1) % MIC_CHANNEL_NUM;
// 延时
vTaskDelay(pdMS_TO_TICKS(1000));
}
// 释放资源
free(mic_buffer);
free(spk_buffer);
esp_codec_dev_close(adc_dev);
esp_codec_dev_close(dac_dev);
ESP_LOGI(TAG, "示例完成");
}
3. ES7210 + ES8311 + NS4150 组合的优势
-
多通道麦克风支持:
- ES7210 支持最多4个麦克风输入,适合语音识别和会议系统
- 可以实现波束形成、回声消除等高级音频处理功能
-
高质量音频:
- ES8311 提供高质量的音频播放
- NS4150 功放提供足够的功率驱动扬声器
-
灵活的配置:
- 可以根据需要调整麦克风增益、偏置电压等参数
- 支持多种采样率和位深度
4. 应用场景
-
智能音箱:
- 使用麦克风阵列实现远场语音识别
- 高质量音频播放音乐和语音反馈
-
会议系统:
- 多通道麦克风捕获不同方向的声音
- 波束形成技术提高特定方向的语音清晰度
-
语音交互设备:
- 实现语音唤醒和命令识别
- 提供高质量的语音反馈
-
音频录制设备:
- 高质量的多通道音频录制
- 实时音频处理和回放
总结
本教程详细介绍了三种常见的 ESP32-S3 音频解决方案:
- 数字麦克风 + 数字功放:简单直接,适合基本应用
- ES8311 + NS4150:高质量音频,适合音乐播放和语音应用
- ES7210 + ES8311 + NS4150:多通道麦克风阵列,适合高级语音交互应用
ESP32-S3 强大的音频处理能力和丰富的外设接口使其成为音频应用的理想平台。通过 esp_codec_dev 组件,可以大大简化音频编解码器的使用,让开发者专注于应用逻辑而非底层驱动。
在实际应用中,应根据具体需求选择合适的音频方案和配置参数,以获得最佳的音质和性能。同时,还应注意优化代码,合理利用 ESP32-S3 的资源,确保系统的稳定性和可靠性。