飞书文档https://x509p6c8to.feishu.cn/wiki/SKZzwIRH3i7lsckUOlzcuJsdnVf
I2S简介
I2S(Inter-Integrated Circuit Sound)是一种用于传输数字音频数据的通信协议,广泛应用于音频设备中。
ESP32-S3 包含 2 个 I2S 外设,通过配置这些外设,可以借助 I2S 驱动来输入和输出采样数据。
SP32-S3 的 I2S 外设支持多种模式,适用于不同的音频应用场景:
- 标准 I2S 模式:适用于大多数立体声音频设备。
- PDM 模式:适用于数字麦克风等低功耗设备。
- TDM 模式:适用于多声道音频系统。
标准 I2S 模式
作用:标准 I2S 模式是最常用的音频传输模式,支持立体声数据传输。数据帧包括左右声道信息,适用于大多数音频设备。
应用场景:
音频 DAC/ADC 接口(如连接扬声器或麦克风)。
与音频编解码器通信(如 ESP32-S3 与外部音频芯片的通信)。
立体声音频播放或录制。
PDM(Pulse Density Modulation)模式
作用:PDM 是一种单比特数据流格式,通过高采样率表示音频信号的幅度。PDM 数据需要转换为 PCM 格式后才能使用。
应用场景:
数字麦克风接口(如 MEMS 麦克风)。
低功耗音频采集设备。
需要高采样率但数据量较小的场景。
TDM(Time Division Multiplexing)模式
作用:TDM 模式通过时分复用技术支持多声道音频传输。数据帧被划分为多个时隙,每个时隙对应一个声道。
应用场景:
多声道音频系统(如家庭影院、环绕声系统)。
需要同时传输多个音频信号的场景(如多麦克风阵列)。
与多声道音频编解码器通信。
线序说明:
标准或 TDM 通信模式下的 I2S 总线包含以下几条线路:
- MCLK:主时钟线。该信号线可选,具体取决于从机,主要用于向 I2S 从机提供参考时钟。
- BCLK:位时钟线。用于数据线的位时钟。
- WS/LRCLK:字(声道)选择线。通常用于识别声道(除 PDM 模式外)。
- DIN/DOUT:串行数据输入/输出线。如果 DIN 和 DOUT 被配置到相同的 GPIO,数据将在内部回环。
PDM 通信模式下的 I2S 总线包含以下几条线路:
- CLK:PDM 时钟线。
- DIN/DOUT:串行数据输入/输出线。
如何选择合适的I2S模式?
对于ESP32S3来说,很多时候都是作为主机端,采集麦克风的声音,或者通过功放播放声音,此时应该如何选择模式呢?这就要看我们的外设电路了,拿麦克风举例,
如果我们直接用PDM MEMS数字麦克风,例如ST的MP34DT05,这颗数字麦支持PDM协议,那我们就选择PDM模式,MIC使用PDM RX
如果我们接的是ES8311这颗Codec芯片,这颗芯片支持一路ADC模拟MIC输入,一路DAC,可以理解为把上方的NS4168集成一起了,这颗芯片是支持I2S模式的,这时候我们就选择标准I2S模式。
如果我们需要多通道音频采集,例如使用ES7210芯片,这颗芯片支持TDM协议,那我们就选择TDM模式
ESP32S3有两个I2S,I2S0/1各支持什么模式?
芯片 | I2S 标准 | PDM TX | PDM RX | TDM |
ESP32 | I2S 0/1 | I2S 0 | I2S 0 | 无 |
ESP32-S2 | I2S 0 | 无 | 无 | 无 |
ESP32-C3 | I2S 0 | I2S 0 | 无 | I2S 0 |
ESP32-C6 | I2S 0 | I2S 0 | 无 | I2S 0 |
ESP32-S3 | I2S 0/1 | I2S 0 | I2S 0 | I2S 0/1 |
ESP32-H2 | I2S 0 | I2S 0 | 无 | I2S 0 |
ESP32-P4 | I2S 0~2 | I2S 0 | I2S 0 | I2S 0~2 |
ESP32-C5 | I2S 0 | I2S 0 | I2S 0 | I2S 0 |
ESP32-C61 | I2S 0 | I2S 0 | I2S 0 | I2S 0 |
前面我们根据外设电路确定了I2S的模式,例如我们外设是ES8311,我们选择TDM(标准模式)
I2S标准模式
ESP32S3标准模式下支持多种数据格式:
标准模式中有且仅有左右两个声道,驱动中将声道称为 slot。这些声道可以支持 8/16/24/32 位宽的采样数据,声道的通信格式主要包括以下几种:
- Philips 格式(常用)
- MSB 格式
- PCM 帧同步格式
如何选择这些格式呢?其实主要是根据外设支持的模式来选用的,我们逐个模式了解下”
Philips 格式(常用)
- Philips 格式(常用):数据信号与 WS 信号相比有一个位的位移。WS 信号的占空比为 50%。
data_bit_width:数据位宽
slot_bit_width:声道位宽
在 I2S 时序图中:
- data_bit_width 决定了有效数据的长度。
- slot_bit_width 决定了每个声道数据占用的总位数(包括填充位)。
例如:
- 如果 data_bit_width = 24,slot_bit_width = 32,时序图可能如下:
WS: 0 1 0 1
SCK: _|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_
SD: 00000000XXXXXXXX|00000000YYYYYYYY
填充位 左声道 填充位 右声道
MSB 格式
与 Philips 格式基本相同,但其数据没有位移。
PCM 帧同步
数据有一个位的位移,同时 WS 信号变成脉冲,持续一个 BCLK 周期。
例如:
ES8311支持以下四种数据格式, 标准I2S(Philips格式),左对齐,右对齐,PCM格式,这些模式可以通过芯片的I2C接口进行配置,如果我们配置为 标准I2S(Philips格式),那ESP32端也配置为对应角色。
那如何通过代码配置呢?我们可以参考下方图:
I2S 通道有三种状态,分别为 registered(已注册)、 ready(准备就绪) 和 running(运行中),它们的关系如下图所示:
I2S标准模式-Philips格式初始化代码如下:
static esp_err_t i2s_driver_init(void)
{
// 配置I2S通道
// 使用默认配置,指定I2S编号和主模式
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM, I2S_ROLE_MASTER);
// 启用自动清除DMA缓冲区中的遗留数据
chan_cfg.auto_clear = true;
// 创建一个新的I2S通道,并将返回的发送和接收通道句柄分别存储在tx_handle和rx_handle中
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, &rx_handle));
// 配置标准I2S模式
i2s_std_config_t std_cfg = {
// 设置时钟配置,使用默认的标准I2S时钟配置,并根据EXAMPLE_SAMPLE_RATE配置采样率
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(EXAMPLE_SAMPLE_RATE),
// 设置槽位配置,使用默认的Philips标准槽位配置,16位数据宽度和立体声模式
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
// 设置GPIO配置,指定各个I2S信号的GPIO引脚,并配置信号不反转
.gpio_cfg = {
.mclk = I2S_MCK_IO, // 主时钟引脚
.bclk = I2S_BCK_IO, // 位时钟引脚
.ws = I2S_WS_IO, // 左右声道选择引脚
.dout = I2S_DO_IO, // 数据输出引脚
.din = I2S_DI_IO, // 数据输入引脚
.invert_flags = {
.mclk_inv = false, // 主时钟不反转
.bclk_inv = false, // 位时钟不反转
.ws_inv = false, // 左右声道选择信号不反转
},
},
};
// 设置主时钟的倍数
std_cfg.clk_cfg.mclk_multiple = EXAMPLE_MCLK_MULTIPLE;
// 初始化发送通道为标准I2S模式
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &std_cfg));
// 初始化接收通道为标准I2S模式
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle, &std_cfg));
// 启用发送通道
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
// 启用接收通道
ESP_ERROR_CHECK(i2s_channel_enable(rx_handle));
}
配置完I2S模式后,我们还需要通过IIC初始化ES8311,对于这个音频芯片,ESP32官方封装成组件形式,我们是可以直接使用的。直接在工程目录下执行
idf.py add-dependency "espressif/es8311^1.0.0"
就会生成idf_component.yml文件
## IDF Component Manager Manifest File
dependencies:
## Required IDF version
idf:
version: '>=4.1.0'
# # Put list of dependencies here
# # For components maintained by Espressif:
# component: "~1.0.0"
# # For 3rd party components:
# username/component: ">=1.0.0,<2.0.0"
# username2/component2:
# version: "~1.0.0"
# # For transient dependencies `public` flag can be set.
# # `public` flag doesn't have an effect dependencies of the `main` component.
# # All dependencies of `main` are public by default.
# public: true
espressif/es8311: ^1.0.0
此时执行下方指令,就会自动下载该库到工程demo/managed_components文件夹中,这里面就是这个器件驱动的源码。
idf.py reconfigure
组件库文档说明:
https://docs.espressif.com/projects/esp-idf/zh_CN/v5.4/esp32/api-guides/tools/idf-component-manager.html
更多组件库参考:
https://components.espressif.com/
在这里,我们也可以看到ESP32生态的优势,
- 对于外设传感器、芯片厂家,我们可以提交自己芯片的驱动组件到官方组件库。
- 对于开发者,可以快速使用对应组件完成项目开发,当然你也可以制作组件提交或者自定义本地组件。
组件的架构,也可以解耦不同的芯片平台,解耦不同的项目,把业务层和驱动层完全分离开来,加速开发。
添加组件到本地后,我们就可以进行初始化配置了,下面来看看如何实现一个本地音频播放功能
demo/main/CMakeLists.txt添加音频文件,音频文件位于esp-idf/examples/peripherals/i2s/i2s_codec/i2s_es8311/main/canon.pcm
idf_component_register(
SRCS "main.c"
INCLUDE_DIRS "."
EMBED_FILES "canon.pcm"
)
实现音频播放代码如下:
#include <stdio.h>
#include <string.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2s_std.h"
#include "esp_system.h"
#include "esp_check.h"
#include "es8311.h"
/* Example configurations */
#define EXAMPLE_SAMPLE_RATE (16000) // 音频采样率,采样率被设置为16000 Hz,即每秒采样16000次。
#define EXAMPLE_MCLK_MULTIPLE (384) // 主时钟频率是采样率的倍数,用于驱动I2S接口。MCLK的倍数被设置为384。这意味着主时钟频率将是采样率的384倍。如果数据宽度不是24位,256倍数可能已经足够。
#define EXAMPLE_MCLK_FREQ_HZ (EXAMPLE_SAMPLE_RATE * EXAMPLE_MCLK_MULTIPLE) // 主时钟的频率
#define EXAMPLE_VOICE_VOLUME 90 // 音量,控制输出音量的大小。
#define EXAMPLE_MIC_GAIN ES8311_MIC_GAIN_0DB // 麦克风增益
/* I2C port and GPIOs */
#define I2C_NUM (0)
#define I2C_SCL_IO (GPIO_NUM_5)
#define I2C_SDA_IO (GPIO_NUM_7)
/* I2S port and GPIOs */
#define I2S_NUM (0)
#define I2S_MCK_IO (GPIO_NUM_6)
#define I2S_BCK_IO (GPIO_NUM_14)
#define I2S_WS_IO (GPIO_NUM_12)
#define I2S_DO_IO (GPIO_NUM_11)
#define I2S_DI_IO (GPIO_NUM_13)
#define SPKER_CTRL_PIN GPIO_NUM_10
#define SPKER_CTRL_PIN_SEL (1ULL<<SPKER_CTRL_PIN)
static const char *TAG = "i2s_es8311";
static i2s_chan_handle_t tx_handle = NULL;
static i2s_chan_handle_t rx_handle = NULL;
extern const uint8_t music_pcm_start[] asm("_binary_canon_pcm_start");
extern const uint8_t music_pcm_end[] asm("_binary_canon_pcm_end");
static esp_err_t es8311_codec_init(void)
{
/* 初始化I2C外设 */
const i2c_config_t es_i2c_cfg = {
.sda_io_num = I2C_SDA_IO, // SDA引脚编号
.scl_io_num = I2C_SCL_IO, // SCL引脚编号
.mode = I2C_MODE_MASTER, // I2C模式为主模式
.sda_pullup_en = GPIO_PULLUP_ENABLE, // 启用SDA引脚的上拉电阻
.scl_pullup_en = GPIO_PULLUP_ENABLE, // 启用SCL引脚的上拉电阻
.master.clk_speed = 100000, // I2C主时钟速度为100 kHz
};
// 配置I2C参数
ESP_RETURN_ON_ERROR(i2c_param_config(I2C_NUM, &es_i2c_cfg), TAG, "config i2c failed");
// 安装I2C驱动
ESP_RETURN_ON_ERROR(i2c_driver_install(I2C_NUM, I2C_MODE_MASTER, 0, 0, 0), TAG, "install i2c driver failed");
// 初始化ES8311编解码器 创建ES8311句柄,使用I2C_NUM和ES8311的地址
es8311_handle_t es_handle = es8311_create(I2C_NUM, ES8311_ADDRRES_0);
ESP_RETURN_ON_FALSE(es_handle, ESP_FAIL, TAG, "es8311 create failed");
// 配置ES8311的时钟
const es8311_clock_config_t es_clk = {
.mclk_inverted = false, // 主时钟不反转
.sclk_inverted = false, // 位时钟不反转
.mclk_from_mclk_pin = true, // 主时钟从MCLK引脚获取
.mclk_frequency = EXAMPLE_MCLK_FREQ_HZ, // 主时钟频率
.sample_frequency = EXAMPLE_SAMPLE_RATE // 采样频率
};
// 初始化ES8311编解码器
ESP_ERROR_CHECK(es8311_init(es_handle, &es_clk, ES8311_RESOLUTION_16, ES8311_RESOLUTION_16));
// 配置ES8311的采样频率
ESP_RETURN_ON_ERROR(es8311_sample_frequency_config(es_handle, EXAMPLE_SAMPLE_RATE * EXAMPLE_MCLK_MULTIPLE, EXAMPLE_SAMPLE_RATE), TAG, "set es8311 sample frequency failed");
// 设置ES8311的音量
ESP_RETURN_ON_ERROR(es8311_voice_volume_set(es_handle, EXAMPLE_VOICE_VOLUME, NULL), TAG, "set es8311 volume failed");
// 配置ES8311的麦克风
ESP_RETURN_ON_ERROR(es8311_microphone_config(es_handle, false), TAG, "set es8311 microphone failed");
// 设置ES8311的麦克风增益
ESP_RETURN_ON_ERROR(es8311_microphone_gain_set(es_handle, EXAMPLE_MIC_GAIN), TAG, "set es8311 microphone gain failed");
return ESP_OK;
}
static esp_err_t i2s_driver_init(void)
{
// 指定I2S编号和主模式
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM, I2S_ROLE_MASTER);
// 启用自动清除DMA缓冲区中的遗留数据
chan_cfg.auto_clear = true;
// 创建一个新的I2S通道,并将返回的发送和接收通道句柄分别存储在tx_handle和rx_handle中
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, &rx_handle));
// 配置标准I2S模式
i2s_std_config_t std_cfg = {
// 设置时钟配置,使用默认的标准I2S时钟配置,并根据EXAMPLE_SAMPLE_RATE配置采样率
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(EXAMPLE_SAMPLE_RATE),
// 设置槽位配置,使用默认的Philips标准槽位配置,16位数据宽度和立体声模式
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
// 设置GPIO配置,指定各个I2S信号的GPIO引脚,并配置信号不反转
.gpio_cfg = {
.mclk = I2S_MCK_IO, // 主时钟引脚
.bclk = I2S_BCK_IO, // 位时钟引脚
.ws = I2S_WS_IO, // 左右声道选择引脚
.dout = I2S_DO_IO, // 数据输出引脚
.din = I2S_DI_IO, // 数据输入引脚
.invert_flags = {
.mclk_inv = false, // 主时钟不反转
.bclk_inv = false, // 位时钟不反转
.ws_inv = false, // 左右声道选择信号不反转
},
},
};
// 设置主时钟的倍数
std_cfg.clk_cfg.mclk_multiple = EXAMPLE_MCLK_MULTIPLE;
// 初始化发送通道为标准I2S模式
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &std_cfg));
// 初始化接收通道为标准I2S模式
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle, &std_cfg));
// 启用发送通道
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
// 启用接收通道
ESP_ERROR_CHECK(i2s_channel_enable(rx_handle));
return ESP_OK;
}
static void i2s_music(void *args)
{
esp_err_t ret = ESP_OK;
size_t bytes_write = 0;
uint8_t *data_ptr = (uint8_t *)music_pcm_start;
// 禁用发送通道
ESP_ERROR_CHECK(i2s_channel_disable(tx_handle));
// 预加载数据到发送通道
ESP_ERROR_CHECK(i2s_channel_preload_data(tx_handle, data_ptr, music_pcm_end - data_ptr, &bytes_write));
// 移动数据指针
data_ptr += bytes_write;
// 启用发送通道
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
while (1)
{
// 将音乐数据写入发送通道
ret = i2s_channel_write(tx_handle, data_ptr, music_pcm_end - data_ptr, &bytes_write, portMAX_DELAY);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "[music] i2s write failed");
abort(); // 终止程序
}
if (bytes_write > 0)
{
ESP_LOGI(TAG, "[music] i2s music played, %d bytes are written.", bytes_write);
}
else
{
// 记录播放失败的日志
ESP_LOGE(TAG, "[music] i2s music play failed.");
abort(); // 终止程序
}
data_ptr = (uint8_t *)music_pcm_start; // 重置数据指针到音乐数据的起始位置
vTaskDelay(1000 / portTICK_PERIOD_MS); // 延迟1秒
}
vTaskDelete(NULL); // 删除任务
}
void app_main(void)
{
gpio_config_t io_conf = {};
io_conf.intr_type = GPIO_INTR_DISABLE;
io_conf.mode = GPIO_MODE_OUTPUT;
io_conf.pin_bit_mask = SPKER_CTRL_PIN_SEL;
io_conf.pull_down_en = 0;
io_conf.pull_up_en = 0;
gpio_config(&io_conf);
gpio_set_level(SPKER_CTRL_PIN, 1);
printf("i2s es8311 codec example start\n-----------------------------\n");
/* 初始化I2S外设 */
if (i2s_driver_init() != ESP_OK)
{
ESP_LOGE(TAG, "i2s driver init failed");
abort(); // 终止程序
}
else
{
ESP_LOGI(TAG, "i2s driver init success");
}
/* 初始化I2C外设并配置ES8311编解码器 */
if (es8311_codec_init() != ESP_OK)
{
ESP_LOGE(TAG, "es8311 codec init failed");
abort(); // 终止程序
}
else
{
ESP_LOGI(TAG, "es8311 codec init success");
}
/* 音乐播放任务 */
xTaskCreate(i2s_music, "i2s_music", 4096, NULL, 5, NULL);
}
这个代码运行时,需要我们接上喇叭!这部分的功能不需要底板了,我们可以把核心板拆下来,接上喇叭,仅用USB供电即可。
然后烧录运行,就会重复播放音乐。
实现边录音边播放功能
上方的程序仅用到播放功能,如果我们需要使用麦克风功能呢?那我们就可以使用下方的程序,实现边录音边播放功能。
#include <stdio.h>
#include <string.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2s_std.h"
#include "esp_system.h"
#include "esp_check.h"
#include "es8311.h"
/* Example configurations */
#define EXAMPLE_SAMPLE_RATE (16000) // 音频采样率,采样率被设置为16000 Hz,即每秒采样16000次。
#define EXAMPLE_MCLK_MULTIPLE (384) // 主时钟频率是采样率的倍数,用于驱动I2S接口。MCLK的倍数被设置为384。这意味着主时钟频率将是采样率的384倍。如果数据宽度不是24位,256倍数可能已经足够。
#define EXAMPLE_MCLK_FREQ_HZ (EXAMPLE_SAMPLE_RATE * EXAMPLE_MCLK_MULTIPLE) // 主时钟的频率
#define EXAMPLE_VOICE_VOLUME 90 // 音量,控制输出音量的大小。
#define EXAMPLE_MIC_GAIN ES8311_MIC_GAIN_0DB // 麦克风增益
#define EXAMPLE_RECV_BUF_SIZE (2400) // MIC接收缓冲区大小
/* I2C port and GPIOs */
/* I2C port and GPIOs */
#define I2C_NUM (0)
#define I2C_SCL_IO (GPIO_NUM_5)
#define I2C_SDA_IO (GPIO_NUM_7)
/* I2S port and GPIOs */
#define I2S_NUM (0)
#define I2S_MCK_IO (GPIO_NUM_6)
#define I2S_BCK_IO (GPIO_NUM_14)
#define I2S_WS_IO (GPIO_NUM_12)
#define I2S_DO_IO (GPIO_NUM_11)
#define I2S_DI_IO (GPIO_NUM_13)
#define SPKER_CTRL_PIN GPIO_NUM_10
#define SPKER_CTRL_PIN_SEL (1ULL<<SPKER_CTRL_PIN)
static const char *TAG = "i2s_es8311";
static i2s_chan_handle_t tx_handle = NULL;
static i2s_chan_handle_t rx_handle = NULL;
extern const uint8_t music_pcm_start[] asm("_binary_canon_pcm_start");
extern const uint8_t music_pcm_end[] asm("_binary_canon_pcm_end");
static esp_err_t es8311_codec_init(void)
{
/* 初始化I2C外设 */
const i2c_config_t es_i2c_cfg = {
.sda_io_num = I2C_SDA_IO, // SDA引脚编号
.scl_io_num = I2C_SCL_IO, // SCL引脚编号
.mode = I2C_MODE_MASTER, // I2C模式为主模式
.sda_pullup_en = GPIO_PULLUP_ENABLE, // 启用SDA引脚的上拉电阻
.scl_pullup_en = GPIO_PULLUP_ENABLE, // 启用SCL引脚的上拉电阻
.master.clk_speed = 100000, // I2C主时钟速度为100 kHz
};
// 配置I2C参数
ESP_RETURN_ON_ERROR(i2c_param_config(I2C_NUM, &es_i2c_cfg), TAG, "config i2c failed");
// 安装I2C驱动
ESP_RETURN_ON_ERROR(i2c_driver_install(I2C_NUM, I2C_MODE_MASTER, 0, 0, 0), TAG, "install i2c driver failed");
// 初始化ES8311编解码器 创建ES8311句柄,使用I2C_NUM和ES8311的地址
es8311_handle_t es_handle = es8311_create(I2C_NUM, ES8311_ADDRRES_0);
ESP_RETURN_ON_FALSE(es_handle, ESP_FAIL, TAG, "es8311 create failed");
// 配置ES8311的时钟
const es8311_clock_config_t es_clk = {
.mclk_inverted = false, // 主时钟不反转
.sclk_inverted = false, // 位时钟不反转
.mclk_from_mclk_pin = true, // 主时钟从MCLK引脚获取
.mclk_frequency = EXAMPLE_MCLK_FREQ_HZ, // 主时钟频率
.sample_frequency = EXAMPLE_SAMPLE_RATE // 采样频率
};
// 初始化ES8311编解码器
ESP_ERROR_CHECK(es8311_init(es_handle, &es_clk, ES8311_RESOLUTION_16, ES8311_RESOLUTION_16));
// 配置ES8311的采样频率
ESP_RETURN_ON_ERROR(es8311_sample_frequency_config(es_handle, EXAMPLE_SAMPLE_RATE * EXAMPLE_MCLK_MULTIPLE, EXAMPLE_SAMPLE_RATE), TAG, "set es8311 sample frequency failed");
// 设置ES8311的音量
ESP_RETURN_ON_ERROR(es8311_voice_volume_set(es_handle, EXAMPLE_VOICE_VOLUME, NULL), TAG, "set es8311 volume failed");
// 配置ES8311的麦克风
ESP_RETURN_ON_ERROR(es8311_microphone_config(es_handle, false), TAG, "set es8311 microphone failed");
// 设置ES8311的麦克风增益
ESP_RETURN_ON_ERROR(es8311_microphone_gain_set(es_handle, EXAMPLE_MIC_GAIN), TAG, "set es8311 microphone gain failed");
return ESP_OK;
}
static esp_err_t i2s_driver_init(void)
{
// 指定I2S编号和主模式
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM, I2S_ROLE_MASTER);
// 启用自动清除DMA缓冲区中的遗留数据
chan_cfg.auto_clear = true;
// 创建一个新的I2S通道,并将返回的发送和接收通道句柄分别存储在tx_handle和rx_handle中
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, &rx_handle));
// 配置标准I2S模式
i2s_std_config_t std_cfg = {
// 设置时钟配置,使用默认的标准I2S时钟配置,并根据EXAMPLE_SAMPLE_RATE配置采样率
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(EXAMPLE_SAMPLE_RATE),
// 设置槽位配置,使用默认的Philips标准槽位配置,16位数据宽度和立体声模式
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
// 设置GPIO配置,指定各个I2S信号的GPIO引脚,并配置信号不反转
.gpio_cfg = {
.mclk = I2S_MCK_IO, // 主时钟引脚
.bclk = I2S_BCK_IO, // 位时钟引脚
.ws = I2S_WS_IO, // 左右声道选择引脚
.dout = I2S_DO_IO, // 数据输出引脚
.din = I2S_DI_IO, // 数据输入引脚
.invert_flags = {
.mclk_inv = false, // 主时钟不反转
.bclk_inv = false, // 位时钟不反转
.ws_inv = false, // 左右声道选择信号不反转
},
},
};
// 设置主时钟的倍数
std_cfg.clk_cfg.mclk_multiple = EXAMPLE_MCLK_MULTIPLE;
// 初始化发送通道为标准I2S模式
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &std_cfg));
// 初始化接收通道为标准I2S模式
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle, &std_cfg));
// 启用发送通道
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
// 启用接收通道
ESP_ERROR_CHECK(i2s_channel_enable(rx_handle));
return ESP_OK;
}
static void i2s_echo(void *args)
{
// 分配接收缓冲区,大小为2400字节
int *mic_data = malloc(EXAMPLE_RECV_BUF_SIZE);
if (!mic_data) {
ESP_LOGE(TAG, "[echo] No memory for read data buffer");
abort(); // 终止程序
}
esp_err_t ret = ESP_OK;
size_t bytes_read = 0;
size_t bytes_write = 0;
ESP_LOGI(TAG, "[echo] Echo start");
while (1) {
// 清空接收缓冲区
memset(mic_data, 0, EXAMPLE_RECV_BUF_SIZE);
/* 从麦克风读取样本数据 */
ret = i2s_channel_read(rx_handle, mic_data, EXAMPLE_RECV_BUF_SIZE, &bytes_read, 1000);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "[echo] i2s read failed");
abort(); // 终止程序
}
/* 将样本数据写入播放器 */
ret = i2s_channel_write(tx_handle, mic_data, EXAMPLE_RECV_BUF_SIZE, &bytes_write, 1000);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "[echo] i2s write failed");
abort(); // 终止程序
}
// 检查读取和写入的字节数是否一致
if (bytes_read != bytes_write) {
ESP_LOGW(TAG, "[echo] %d bytes read but only %d bytes are written", bytes_read, bytes_write);
}
}
vTaskDelete(NULL); // 删除任务
}
void app_main(void)
{
printf("i2s es8311 codec example start\n-----------------------------\n");
gpio_config_t io_conf = {};
io_conf.intr_type = GPIO_INTR_DISABLE;
io_conf.mode = GPIO_MODE_OUTPUT;
io_conf.pin_bit_mask = SPKER_CTRL_PIN_SEL;
io_conf.pull_down_en = 0;
io_conf.pull_up_en = 0;
gpio_config(&io_conf);
gpio_set_level(SPKER_CTRL_PIN, 1);
/* 初始化I2S外设 */
if (i2s_driver_init() != ESP_OK)
{
ESP_LOGE(TAG, "i2s driver init failed");
abort(); // 终止程序
}
else
{
ESP_LOGI(TAG, "i2s driver init success");
}
/* 初始化I2C外设并配置ES8311编解码器 */
if (es8311_codec_init() != ESP_OK)
{
ESP_LOGE(TAG, "es8311 codec init failed");
abort(); // 终止程序
}
else
{
ESP_LOGI(TAG, "es8311 codec init success");
}
/* 创建任务以回声模式从麦克风播放声音 */
xTaskCreate(i2s_echo, "i2s_echo", 8192, NULL, 5, NULL);
}
编译烧录后,轻轻敲下桌面或USB头,就可以听到扬声器播放出敲击的声音。
I2S PDM模式
目前板卡是没有接支持PDM协议的麦克风的,所以这个实验在板卡上是做不了的,大家了解下即可。
在 PDM(Pulse-density Modulation,脉冲密度调制)模式下
TX 通道可以将 PCM 数据转换为 PDM 格式,该格式始终有左右两个声道。
RX 通道可以接收 PDM 格式的数据并将数据转换成 PCM 格式。
PDM TX RX 只在 I2S0 中受支持,且只支持 16 位宽的采样数据。。
PDM 至少需要一个 CLK 管脚用于时钟信号,一个 DOUT 管脚用于数据信号,即下图中的 WS 和 SD 信号。
PDM RX
i2s_chan_handle_t rx_handle = NULL;
void init_microphone(void)
{
// 使用默认配置,自动选择I2S编号,并设置为主模式
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
// 创建一个新的I2S通道,并将返回的通道句柄存储在rx_handle中
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, NULL, &rx_handle));
// 配置PDM接收模式
i2s_pdm_rx_config_t pdm_rx_cfg = {
// 设置时钟配置,使用默认的PDM接收时钟配置,配置采样率44100
.clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG(44100),
// 设置槽位配置,使用默认的16位数据宽度和单声道模式
.slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
// 设置GPIO配置,指定时钟引脚和数据输入引脚,并配置时钟不反转
.gpio_cfg = {
.clk = 18, // 时钟引脚
.din = 19, // 数据输入引脚
.invert_flags = {
.clk_inv = false, // 时钟不反转
},
},
};
// 初始化I2S通道为PDM接收模式
ESP_ERROR_CHECK(i2s_channel_init_pdm_rx_mode(rx_handle, &pdm_rx_cfg));
// 启用I2S通道
ESP_ERROR_CHECK(i2s_channel_enable(rx_handle));
}
PDM TX
#include "driver/i2s_pdm.h"
#include "driver/gpio.h"
i2s_chan_handle_t tx_handle = NULL;
void init_microphone(void)
{
// 自动选择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));
/* 初始化通道为 PDM TX 模式 */
i2s_pdm_tx_config_t pdm_tx_cfg = {
// 设置时钟配置,使用默认的PDM接收时钟配置,配置采样率44100
.clk_cfg = I2S_PDM_TX_CLK_DEFAULT_CONFIG(44100),
// 设置槽位配置,使用默认的16位数据宽度和单声道模式
.slot_cfg = I2S_PDM_TX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
// 设置GPIO配置,指定时钟引脚和数据输入引脚,并配置时钟不反转
.gpio_cfg = {
.clk = GPIO_NUM_5,
.dout = GPIO_NUM_18,
.invert_flags = {
.clk_inv = false,
},
},
};
// 初始化I2S通道为PDM发送模式
i2s_channel_init_pdm_tx_mode(tx_handle, &pdm_tx_cfg);
// 启用I2S通道
ESP_ERROR_CHECK(i2s_channel_enable(rx_handle));
}
- peripherals/i2s/i2s_recorder 演示了如何通过 I2S 外设以 PDM 数据格式用数字 MEMS 麦克风录制音频,并将其以 .wav 文件格式保存到 ESP32-S3 开发板上的 SD 卡中。
- peripherals/i2s/i2s_basic/i2s_pdm 演示了如何在 ESP32-S3 上使用 PDM TX 模式,包括必要的硬件设置和配置。