系列文章目录
持续更新...
文章目录
前言
I2S(Inter-IC Sound)是一种专为音频传输设计的串行总线协议,广泛用于音频 Codec、DAC、ADC、扬声器等设备的互联。ESP32 系列(如 ESP32-S3)内置高性能 I2S 控制器,支持主机 / 从机模式、多种音频格式(PCM、TDM)及 DMA 高速传输,可满足从简单音频播放到复杂多通道录音的全场景需求。
新版 ESP-IDF(v5.0+)采用 "通道 - 模式" 驱动架构,简化了 I2S 配置流程,支持动态创建发送 / 接收通道、灵活切换工作模式,并原生适配主流音频 Codec(如 ES8311、PCM5102)。本文将基于新版驱动,详解 I2S 原理、配置及实战示例。
参考文档:ESP32-S3技术参考手册;ESP32-S3编程指南
一、I2S概述
ESP32-S3 内置两个 I2S 接口(即 I2S0 和 I2S1),为多媒体应用,尤其是为数字音频应用提供了灵活的数据通信接口。
I2S 标准总线定义了三种信号:串行时钟信号 BCK、字选择信号 WS 和串行数据信号 SD
一个基本的 I2S 数据总线有一个主机和一个从机。主机和从机的角色在通信过程中保持不变。ESP32-S3 的 I2S 模块包含独立的发送单元和接收单元,能够保证优良的通信性能。
1.主要特性
1.双控制器 + 全双工通信:
两路 I2S;每个控制器的 TX/RX 彼此独立,可在不同时钟/时隙/GPIO 下工作;硬件集成 DMA 流式搬运,降低 CPU 占用。
2.多通信模式(分别对应不同头文件):
Standard 模式(i2s_std.h):左右两个"槽位(slot)",支持 8/16/24/32 bit 采样位宽。
TDM 模式(i2s_tdm.h):标准/PCM 等时序的 多槽扩展,适合多通道音频。
PDM 模式(i2s_pdm.h):支持原始 PDM 流;ESP32-S3 的 I2S0 额外集成 PDM↔PCM 硬件变换(TX 方向 PCM→PDM,RX 方向 PDM→PCM),无该变换的端口仅能收发原始 PDM,需要软件滤波器完成 PDM↔PCM。
3.高精度时钟与采样率:
支持 8 kHz ~ 192 kHz 标准采样率(不支持从机 192 kHz 32 位模式),时钟源可选择 XTAL、PLL 或外部输入,支持整数 / 小数分频,保证时钟精度。
4.高效数据搬运(DMA):
支持 8/16/24/32 位数据传输,内置 64×32 位 TX/RX FIFO 缓冲区,支持 GDMA 直接访问内存(内部 / 外部),减少 CPU 干预,提升传输效率。
5.中断与状态监测:
支持 TX 空、RX 满、传输完成、错误等中断,便于实时监控传输状态。
2.系统架构
每个 I2S 控制器由 TX 控制单元 + RX 控制单元 + 时钟发生器 + DMA 接口 组成。
应用通过为 TX/RX 分别创建"通道",在所选模式下配置 时钟(采样率、MCLK 倍频、BCLK)、时隙(位宽、左右声道/多槽布局) 与 GPIO,然后启用通道即可收发;
TX 与 RX 可以完全不同步地工作(例如 RX=PDM 麦克风输入、TX=Standard 外置功放输出做监听回环),但一个控制器的外部 MCLK 输出线只能挂到 TX 或 RX 其中一侧。
ESP32-S3 I2Sn 模块的结构框图
核心结构如图所示,主要包含以下单元:
1.发送单元(TX Control) :负责串行数据发送,包含独立的 BCK、WS 时钟生成器和数据移位器,支持主机 / 从机模式,输出信号为 I2SnO_BCK_out、I2SnO_WS_out、I2SnO_Data_out;
2.接收单元(RX Control) :负责串行数据接收,同样包含独立的时钟和数据处理模块,支持主机 / 从机模式,输入信号为 I2SnI_BCK_in、I2SnI_WS_in、I2SnI_Data_in;
3.I/O 同步单元(I/O Sync) :调节输入输出信号的时序,确保数据采样和发送的同步性;
4.时钟分频器(Clock Generator) :从源时钟(XTAL、PLL 或外部输入)分频生成 I2S 核心时钟(I2Sn_TX/RX_CLK),再进一步分频得到 BCK 时钟;
5.FIFO 缓冲区 :64×32 位 TX FIFO 和 RX FIFO,用于暂存数据,平衡 CPU 与外设的速度差异;
6.压缩 / 解压缩模块 :支持数据格式转换(如 PDM 与 PCM 的互转,仅 I2S0 具备);
GDMA 接口:直接与通用 DMA 控制器对接,实现内存与 FIFO 之间的高速数据搬运,无需 CPU 参与字节级处理。
3.I2S模块时钟
ESP32-S3 I2S 模块时钟系统以 XTAL、PLL 或外部 MCLK 为源,通过整数 / 小数分频生成核心时钟(I2Sn_TX/RX_CLK),再经分频得到串行时钟 BCK(主机模式下 BCK 频率由核心时钟分频计算,从机模式下外部输入且需保证核心时钟≥8×BCK 频率),同时可输出 MCLK 作为外部设备时钟(由核心时钟分频生成),以此支撑从 8kHz 到 192kHz 的采样率需求。
4.I2S音频协议
ESP32-S3 支持多种音频协议,通过寄存器配置(如 TDM_EN、PDM_EN、MSB_SHIFT 等)选择,适用于不同外设场景:
TDM Philips 标准模式
在 Philips 标准下,在 BCK 的下降沿,WS 信号先于 SD 信号一个 BCK 时钟周期开始变化,即 WS 信号从当前通道数据的第一个位之前的一个时钟开始有效,并在当前通道数据发送结束前一个 BCK 时钟周期开始变化。SD信号线上首先传输音频数据的最高位。
与 Philips 标准相比,TDM Philips 标准支持更多的通道。
时序图 -- TDM Philips 标准
TDM MSB 对齐标准模式
MSB 对齐标准下,在 BCK 下降沿,WS 信号和 SD 信号同时变化。WS 持续到当前通道数据发送结束,SD 信号线上首先传输音频数据的最高位。
与 MSB 对齐标准相比,TDM MSB 对齐标准支持更多的通道。
时序图 -- TDM MSB 对齐标准
TDM PCM 标准模式
在 PCM 标准的短帧同步模式下,在 BCK 的下降沿,WS 信号先于 SD 信号一个 BCK 时钟周期开始变化,即 WS信号从当前通道数据的第一个位之前的一个时钟开始有效,并持续一个 BCK 时钟周期。SD 信号线上首先传输音频数据的最高位。
与 PCM 标准相比,TDM PCM 标准支持更多的通道
时序图 -- TDM PCM 标准
PDM 标准模式
如图所示,在 PDM 标准下,WS 代表左/右声道,在 BCK 的下降沿,WS 与 SD 同时变化。WS 在数据发送过程中持续变化,WS 的高低对应两个声道。
时序图 -- PDM 标准
二、I2S类型定义及相关API
I2S类型定义
c
/* ========== 通道/通用 ========== */
typedef void *i2s_chan_handle_t; // 通道句柄(TX 或 RX,各自独立)
typedef enum {
I2S_ROLE_MASTER = 0, // 主机:本端出 BCK/WS
I2S_ROLE_SLAVE, // 从机:本端入 BCK/WS
} i2s_role_t;
typedef struct {
int id; // I2S_NUM_0 / I2S_NUM_1
i2s_role_t role; // 主/从
uint32_t dma_desc_num; // DMA 描述符个数
uint32_t dma_frame_num; // 每个描述符的"帧"大小(字节数对齐到样本宽度)
// 可能还有目标芯片特定字段(以官方头文件为准)
} i2s_chan_config_t;
/* 常用默认宏(按端口与角色快速给出通道配置) */
#define I2S_CHANNEL_DEFAULT_CONFIG(port, role) /* 官方提供的便捷宏 */
/* ========== Standard(I2S/TDM-2slot) ========== */
typedef enum {
I2S_SLOT_MODE_MONO = 1, // 单声道
I2S_SLOT_MODE_STEREO = 2, // 立体声(2 slot)
} i2s_slot_mode_t;
typedef enum {
I2S_DATA_BIT_WIDTH_16BIT = 16,
I2S_DATA_BIT_WIDTH_24BIT = 24,
I2S_DATA_BIT_WIDTH_32BIT = 32,
} i2s_data_bit_width_t;
typedef enum {
I2S_SLOT_BIT_WIDTH_AUTO = 0, // 槽宽跟随 data 宽
I2S_SLOT_BIT_WIDTH_16BIT = 16,
I2S_SLOT_BIT_WIDTH_24BIT = 24,
I2S_SLOT_BIT_WIDTH_32BIT = 32,
} i2s_slot_bit_width_t;
typedef enum { // MCLK 倍频(常见 256×fs / 384×fs)
I2S_MCLK_MULTIPLE_256 = 256,
I2S_MCLK_MULTIPLE_384 = 384,
} i2s_mclk_multiple_t;
typedef enum { // 时钟源选择(目标芯片支持项以 TRM 为准)
I2S_CLK_SRC_DEFAULT = 0,
I2S_CLK_SRC_EXTERNAL, // 外部 MCLK(从 MCLK 脚输入)
/* 还可能包括 XTAL、PLL_F160M、PLL_D2 等选项 */
} i2s_clock_src_t;
/* Standard 时钟/槽位/GPIO */
typedef struct {
uint32_t sample_rate_hz;
i2s_clock_src_t clk_src;
i2s_mclk_multiple_t mclk_multiple;
uint32_t ext_clk_freq_hz; // 仅当外部时钟源时有效(部分芯片)
} i2s_std_clk_config_t;
typedef struct {
i2s_data_bit_width_t data_bit_width; // 有效位宽(PCM 位宽)
i2s_slot_bit_width_t slot_bit_width; // 槽宽(帧内总位数)
i2s_slot_mode_t slot_mode; // MONO / STEREO
/* 以及:左右槽选择、WS 宽度/极性、是否 Philips 位移、对齐/端序/位序 等 */
} i2s_std_slot_config_t;
typedef struct {
int mclk; // MCLK GPIO(主机输出 / 外部输入)
int bclk; // BCK
int ws; // LRCLK/WS
int dout; // DATA 输出(TX)
int din; // DATA 输入(RX)
struct { unsigned mclk_inv:1, bclk_inv:1, ws_inv:1; } invert_flags;
} i2s_std_gpio_config_t;
typedef struct {
i2s_std_clk_config_t clk_cfg;
i2s_std_slot_config_t slot_cfg;
i2s_std_gpio_config_t gpio_cfg;
} i2s_std_config_t;
/* Standard 便捷默认宏(三大协议族) */
#define I2S_STD_CLK_DEFAULT_CONFIG(rate) /* 默认 256×fs */
#define I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(bits, mono_stereo)
#define I2S_STD_MSB_SLOT_DEFAULT_CONFIG(bits, mono_stereo)
#define I2S_STD_PCM_SLOT_DEFAULT_CONFIG(bits, mono_stereo)
/* ========== PDM ========== */
typedef struct { // PDM RX/TX 的时钟、抽取/过采样等
uint32_t sample_rate_hz;
/* 其它与抽取率、时钟源相关的字段 */
} i2s_pdm_clk_config_t;
typedef struct { // PDM RX 槽位(解码到 PCM 后的位宽/声道)
i2s_data_bit_width_t data_bit_width;
i2s_slot_mode_t slot_mode;
} i2s_pdm_rx_slot_config_t;
typedef struct {
int clk; // PDM CLK
int din; // PDM DIN(RX)
struct { unsigned clk_inv:1; } invert_flags;
} i2s_pdm_rx_gpio_config_t;
typedef struct {
i2s_pdm_clk_config_t clk_cfg;
i2s_pdm_rx_slot_config_t slot_cfg;
i2s_pdm_rx_gpio_config_t gpio_cfg;
} i2s_pdm_rx_config_t;
/* TX 方向类似,GPIO 为 dout;亦有相应的默认宏 */
typedef struct { /* ... */ } i2s_pdm_tx_config_t;
/* ========== TDM(多时隙 PCM) ========== */
typedef struct {
uint32_t sample_rate_hz;
i2s_clock_src_t clk_src;
i2s_mclk_multiple_t mclk_multiple;
} i2s_tdm_clk_config_t;
typedef struct {
i2s_data_bit_width_t data_bit_width;
i2s_slot_bit_width_t slot_bit_width;
i2s_slot_mode_t slot_mode; // 立体声/多声道(配合 slot mask)
uint32_t slot_mask; // 选择有效时隙(最多 16 路)
/* 以及 Philips/MSB/PCM 对齐、WS 宽度/极性 等 */
} i2s_tdm_slot_config_t;
typedef struct {
int mclk, bclk, ws, dout, din;
struct { unsigned mclk_inv:1, bclk_inv:1, ws_inv:1; } invert_flags;
} i2s_tdm_gpio_config_t;
typedef struct {
i2s_tdm_clk_config_t clk_cfg;
i2s_tdm_slot_config_t slot_cfg;
i2s_tdm_gpio_config_t gpio_cfg;
} i2s_tdm_config_t;
/* 常用默认宏 */
#define I2S_TDM_CLK_DEFAULT_CONFIG(rate)
#define I2S_TDM_PHILIPS_SLOT_DEFAULT_CONFIG(bits, mode, slot_mask)
#define I2S_TDM_MSB_SLOT_DEFAULT_CONFIG(bits, mode, slot_mask)
#define I2S_TDM_PCM_SLOT_DEFAULT_CONFIG(bits, mode, slot_mask)
I2S相关API
c
/* ========== 通道生命周期(所有模式通用) ========== */
esp_err_t i2s_new_channel(const i2s_chan_config_t *chan_cfg,
i2s_chan_handle_t *ret_tx_handle,
i2s_chan_handle_t *ret_rx_handle);
esp_err_t i2s_del_channel(i2s_chan_handle_t handle);
esp_err_t i2s_channel_enable(i2s_chan_handle_t handle);
esp_err_t i2s_channel_disable(i2s_chan_handle_t handle);
/* 阻塞式 I/O:写/读直到完成或超时(portMAX_DELAY 可一直等待) */
esp_err_t i2s_channel_write(i2s_chan_handle_t handle,
const void *src, size_t size,
size_t *bytes_written, uint32_t timeout_ms);
esp_err_t i2s_channel_read(i2s_chan_handle_t handle,
void *dst, size_t size,
size_t *bytes_read, uint32_t timeout_ms);
/* 可注册中断回调实现"异步收发"(在回调里直接访问 DMA 缓冲) */
typedef struct {
bool (*on_recv)(i2s_chan_handle_t handle, void *user_data);
bool (*on_send)(i2s_chan_handle_t handle, void *user_data);
} i2s_event_callbacks_t;
esp_err_t i2s_channel_register_event_callback(i2s_chan_handle_t handle,
const i2s_event_callbacks_t *cbs,
void *user_data);
/* ========== Standard 模式 ========== */
esp_err_t i2s_channel_init_std_mode(i2s_chan_handle_t handle,
const i2s_std_config_t *std_cfg);
/* 运行前可单独重配(需处于 STOP/未使能状态) */
esp_err_t i2s_channel_reconfig_std_clock(i2s_chan_handle_t handle,
const i2s_std_clk_config_t *clk_cfg);
esp_err_t i2s_channel_reconfig_std_slot(i2s_chan_handle_t handle,
const i2s_std_slot_config_t *slot_cfg);
esp_err_t i2s_channel_reconfig_std_gpio(i2s_chan_handle_t handle,
const i2s_std_gpio_config_t *gpio_cfg);
/* ========== PDM 模式 ========== */
esp_err_t i2s_channel_init_pdm_rx_mode(i2s_chan_handle_t handle,
const i2s_pdm_rx_config_t *pdm_rx_cfg);
esp_err_t i2s_channel_init_pdm_tx_mode(i2s_chan_handle_t handle,
const i2s_pdm_tx_config_t *pdm_tx_cfg);
/* 不同 IDF 版本提供相应 reconfig_* 接口,字段同理 */
/* ========== TDM 模式(多路 PCM) ========== */
esp_err_t i2s_channel_init_tdm_mode(i2s_chan_handle_t handle,
const i2s_tdm_config_t *tdm_cfg);
/* 同理可用 reconfig_* 接口重配 clock/slot/gpio */
三、I2S示例程序
麦克风输入延迟从扬声器输出的全双工音频回环:
main.c
c
#include <stdio.h>
#include "esp_log.h" // ESP日志系统
#include "freertos/FreeRTOS.h" // FreeRTOS实时操作系统
#include "freertos/task.h" // FreeRTOS任务管理
#include "myi2s.h" // 自定义I2S音频模块
#include "pca9557.h" // PCA9557 IO扩展芯片
// 日志标签
static const char *TAG = "app";
/* 回声效果参数配置 */
#define LOOP_BUF_BYTES 1024 // 每次处理的字节数(必须是4的倍数,16bit立体声=4字节/帧)
#define ECHO_DELAY_MS 100 // 回声延迟时间(毫秒) - 100ms后听到回声
#define ECHO_GAIN_NUM 128 // 回声增益(0~256) - 128约等于50%音量
#define DIRECT_GAIN_NUM 256 // 直达声增益(0~256) - 256等于100%音量
/**
* @brief 音频回环处理任务
* @param arg 任务参数(未使用)
*
* 此任务完成实时音频处理:
* 1. 从ES7210读取麦克风数据
* 2. 添加延迟回声效果
* 3. 将处理后的音频发送到ES8311播放
*
* 回声原理:
* - 维护一个环形延迟缓冲区
* - 当前输入 = 直达声 + 延迟后的回声
* - 同时将当前输入存入延迟缓冲区供未来使用
*/
static void loopback_task(void *arg)
{
// 分配音频数据缓冲区
uint8_t rx_buf[LOOP_BUF_BYTES]; // 从麦克风接收的原始数据
uint8_t tx_buf[LOOP_BUF_BYTES]; // 处理后发送到扬声器的数据
// 计算回声延迟缓冲区大小
// frames_delay = 采样率 × 延迟时间(秒) = 16000 × 0.1 = 1600帧
size_t frames_delay = (EXAMPLE_SAMPLE_RATE * ECHO_DELAY_MS) / 1000;
// 立体声16bit:每帧包含左右两个16bit样本
size_t delay_samples = frames_delay * 2; // 总样本数 = 1600 × 2 = 3200个int16
if (delay_samples < 32) // 确保最小缓冲区大小
delay_samples = 32;
// 分配延迟线缓冲区(环形缓冲区),用于存储延迟的音频样本
int16_t *delay_line = heap_caps_calloc(delay_samples, sizeof(int16_t), MALLOC_CAP_DMA);
if (!delay_line)
{
ESP_LOGE(TAG, "Failed to allocate delay line memory");
vTaskDelete(NULL); // 内存分配失败,删除任务
}
size_t delay_idx = 0; // 延迟缓冲区的写入索引
ESP_LOGI(TAG, "Loopback task start: delay=%dms samples=%d",
ECHO_DELAY_MS, (int)delay_samples);
// 主处理循环 - 永续运行
while (1)
{
// 第一步:从ES7210读取麦克风数据
size_t read_bytes = 0;
esp_err_t read_result = i2s_channel_read(rx_handle, rx_buf, LOOP_BUF_BYTES,
&read_bytes, portMAX_DELAY);
if (read_result != ESP_OK)
{
ESP_LOGW(TAG, "I2S read failed, retrying...");
continue; // 读取失败,重试
}
// 第二步:处理音频数据 - 添加回声效果
int sample_count = read_bytes / 2; // 计算16bit样本数量
int16_t *in = (int16_t *)rx_buf; // 输入样本指针
int16_t *out = (int16_t *)tx_buf; // 输出样本指针
// 逐样本处理,添加回声效果
for (int i = 0; i < sample_count; i++)
{
// 获取当前输入样本(来自麦克风)
int16_t mic = in[i];
// 从延迟线获取回声样本(100ms前的声音)
int16_t echo = delay_line[delay_idx];
// 混合直达声和回声
// mix = (当前声音×100% + 回声×50%) / 256
int32_t mix = (mic * DIRECT_GAIN_NUM + echo * ECHO_GAIN_NUM) / 256;
// 防止音频溢出(限制在16bit范围内)
if (mix > 32767) // 正向溢出
mix = 32767;
else if (mix < -32768) // 负向溢出
mix = -32768;
// 输出混合后的样本
out[i] = (int16_t)mix;
// 将当前输入存入延迟线,用于未来的回声
delay_line[delay_idx] = mic;
// 更新延迟线索引(环形缓冲区)
delay_idx++;
if (delay_idx >= delay_samples)
{
delay_idx = 0; // 回到缓冲区开始位置
}
}
// 第三步:将处理后的音频发送到ES8311播放
size_t written = 0;
esp_err_t write_result = i2s_channel_write(tx_handle, tx_buf, read_bytes,
&written, portMAX_DELAY);
if (write_result != ESP_OK)
{
ESP_LOGW(TAG, "I2S write failed");
}
}
// 清理资源(实际上永远不会执行到这里)
free(delay_line);
}
/**
* @brief 应用主函数
*
* 系统启动流程:
* 1. 初始化I2C总线
* 2. 初始化I2S全双工通信
* 3. 初始化ES8311播放芯片
* 4. 初始化ES7210录音芯片
* 5. 初始化PCA9557 IO扩展芯片
* 6. 启用功放
* 7. 创建音频处理任务
*/
void app_main(void)
{
ESP_LOGI(TAG, "=== Audio loopback demo starting ===");
// 第一步:初始化I2C接口(ES8311、ES7210、PCA9557都使用I2C通信)
ESP_ERROR_CHECK(bsp_i2c_init());
ESP_LOGI(TAG, "✓ I2C bus initialized");
// 第二步:初始化I2S全双工通信(录音+播放)
ESP_ERROR_CHECK(i2s_full_duplex_init());
ESP_LOGI(TAG, "✓ I2S full duplex initialized");
// 第三步:初始化ES8311播放芯片(DAC - 数字到模拟转换)
ESP_ERROR_CHECK(es8311_playback_init());
ESP_LOGI(TAG, "✓ ES8311 playback initialized");
// 第四步:初始化ES7210录音芯片(ADC - 模拟到数字转换)
ESP_ERROR_CHECK(es7210_capture_init());
ESP_LOGI(TAG, "✓ ES7210 capture initialized");
// 第五步:初始化PCA9557 IO扩展芯片(控制功放等外设)
ESP_ERROR_CHECK(pca9557_init());
ESP_LOGI(TAG, "✓ PCA9557 IO expander initialized");
// 第六步:启用功放(通过PCA9557控制PA_EN引脚)
pa_en(1);
ESP_LOGI(TAG, "✓ Power amplifier enabled");
// 第七步:创建音频处理任务
// 任务名:"loopback", 栈大小:4KB, 优先级:5
BaseType_t task_result = xTaskCreate(loopback_task, "loopback", 4096, NULL, 5, NULL);
if (task_result == pdPASS)
{
ESP_LOGI(TAG, "✓ Audio loopback task created successfully");
ESP_LOGI(TAG, "=== System ready! Speak into microphone ===");
}
else
{
ESP_LOGE(TAG, "✗ Failed to create loopback task");
}
}
myi2s.c
c
#include "myi2s.h"
// 日志标签,用于识别输出的日志来源
static const char *TAG = "i2s_audio";
// I2S通道句柄全局变量
i2s_chan_handle_t tx_handle = NULL; // 发送通道(播放)
i2s_chan_handle_t rx_handle = NULL; // 接收通道(录音)
/**
* @brief 初始化I2S外设为全双工模式
* @return ESP_OK 成功初始化
*
* 此函数完成以下工作:
* 1. 创建I2S发送和接收通道
* 2. 配置I2S为标准飞利浦模式
* 3. 设置GPIO引脚映射
* 4. 启用I2S通道
*/
esp_err_t i2s_full_duplex_init(void)
{
// 第一步:创建I2S通道配置
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM, I2S_ROLE_MASTER);
chan_cfg.auto_clear = true; // 自动清除缓冲区,防止旧数据干扰
// 创建TX(发送)和RX(接收)通道,ESP32作为主设备控制时钟
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, &rx_handle));
// 第二步:配置I2S标准模式参数
i2s_std_config_t base_cfg = {
// 时钟配置:设置采样率
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(EXAMPLE_SAMPLE_RATE),
// 插槽配置:16bit立体声飞利浦格式
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT,
I2S_SLOT_MODE_STEREO),
// GPIO引脚配置
.gpio_cfg = {
.mclk = I2S_MCK_IO, // 主时钟输出到ES8311和ES7210
.bclk = I2S_BCK_IO, // 位时钟,同步数据传输
.ws = I2S_WS_IO, // 字选择信号,区分左右声道
.dout = I2S_DO_IO, // 数据输出到ES8311(播放)
.din = I2S_DI_IO, // 数据输入从ES7210(录音)
.invert_flags = {0}, // 不反转任何信号
},
};
// 设置主时钟倍数,确保ES8311和ES7210时钟同步
base_cfg.clk_cfg.mclk_multiple = EXAMPLE_MCLK_MULTIPLE;
// 第三步:用相同配置初始化TX和RX通道
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &base_cfg)); // 播放通道
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle, &base_cfg)); // 录音通道
// 第四步:启用I2S通道,开始时钟输出
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle)); // 启用播放
ESP_ERROR_CHECK(i2s_channel_enable(rx_handle)); // 启用录音
// 输出初始化成功信息
ESP_LOGI(TAG, "I2S full duplex init done: %dHz, %d-bit, %dx MCLK",
EXAMPLE_SAMPLE_RATE, EXAMPLE_BIT_WIDTH, EXAMPLE_MCLK_MULTIPLE);
return ESP_OK;
}
/**
* @brief 初始化ES8311播放芯片(仅DAC功能)
* @return ESP_OK 成功初始化
*
* 此函数完成以下工作:
* 1. 创建ES8311设备句柄
* 2. 配置时钟参数使其与I2S同步
* 3. 初始化为DAC模式
* 4. 设置播放音量
*/
esp_err_t es8311_playback_init(void)
{
// 第一步:创建ES8311设备句柄
// BSP_I2C_NUM: I2C总线号, ES8311_ADDRRES_0: ES8311的I2C地址
es8311_handle_t es_handle = es8311_create(BSP_I2C_NUM, ES8311_ADDRRES_0);
if (!es_handle)
{
ESP_LOGE(TAG, "Failed to create ES8311 handle");
return ESP_FAIL;
}
// 第二步:配置ES8311时钟参数,必须与I2S时钟同步
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芯片
// ES8311_RESOLUTION_16: 输入16bit, 输出16bit分辨率
ESP_RETURN_ON_ERROR(es8311_init(es_handle, &es_clk,
ES8311_RESOLUTION_16, ES8311_RESOLUTION_16),
TAG, "ES8311 initialization failed");
// 第四步:设置播放音量
ESP_RETURN_ON_ERROR(es8311_voice_volume_set(es_handle, EXAMPLE_VOICE_VOLUME, NULL),
TAG, "Failed to set ES8311 volume");
ESP_LOGI(TAG, "ES8311 playback init done - Volume: %d%%", EXAMPLE_VOICE_VOLUME);
return ESP_OK;
}
/**
* @brief 初始化ES7210录音芯片(仅ADC功能)
* @return ESP_OK 成功初始化
*
* 此函数完成以下工作:
* 1. 创建ES7210设备句柄
* 2. 配置编解码器参数
* 3. 设置麦克风增益和偏置
* 4. 配置ADC音量
*/
esp_err_t es7210_capture_init(void)
{
// 第一步:创建ES7210设备句柄
es7210_dev_handle_t es7210_handle = NULL;
es7210_i2c_config_t es7210_i2c_conf = {
.i2c_port = BSP_I2C_NUM, // I2C总线号
.i2c_addr = EXAMPLE_ES7210_I2C_ADDR // ES7210的I2C地址
};
ESP_ERROR_CHECK(es7210_new_codec(&es7210_i2c_conf, &es7210_handle));
// 第二步:配置ES7210编解码器参数
ESP_LOGI(TAG, "Configure ES7210 codec parameters");
es7210_codec_config_t codec_conf = {
.i2s_format = EXAMPLE_I2S_FORMAT, // I2S数据格式(与ES8311一致)
.mclk_ratio = EXAMPLE_MCLK_MULTIPLE, // 主时钟倍数(与ES8311一致)
.sample_rate_hz = EXAMPLE_SAMPLE_RATE, // 采样率(与ES8311一致)
.bit_width = ES7210_I2S_BITS_16B, // 16bit数据位宽(与ES8311一致)
.mic_bias = EXAMPLE_ES7210_MIC_BIAS, // 麦克风偏置电压
.mic_gain = EXAMPLE_ES7210_MIC_GAIN, // 麦克风增益
.flags.tdm_enable = false // 关闭TDM,使用标准I2S模式
};
// 第三步:应用配置到ES7210芯片
ESP_ERROR_CHECK(es7210_config_codec(es7210_handle, &codec_conf));
// 第四步:设置ADC音量
ESP_ERROR_CHECK(es7210_config_volume(es7210_handle, EXAMPLE_ES7210_ADC_VOLUME));
ESP_LOGI(TAG, "ES7210 capture init done - Gain: %ddB, Volume: %d",
30, EXAMPLE_ES7210_ADC_VOLUME); // 30dB对应ES7210_MIC_GAIN_30DB
return ESP_OK;
}
myi2s.h
c
#ifndef MY_I2S_H
#define MY_I2S_H
// 包含必要的头文件
#include "driver/i2s_std.h" // I2S标准模式驱动
#include "es8311.h" // ES8311 DAC芯片驱动
#include "pca9557.h" // PCA9557 IO扩展芯片驱动
#include "esp_log.h" // ESP日志系统
#include "esp_err.h" // ESP错误处理
#include "esp_check.h" // ESP检查宏
#include "es7210.h" // ES7210 ADC芯片驱动
/* I2S外设和GPIO引脚定义 */
#define I2S_NUM (0) // 使用I2S外设0
#define I2S_MCK_IO (GPIO_NUM_38) // 主时钟引脚(MCLK) - 为ES8311和ES7210提供主时钟
#define I2S_BCK_IO (GPIO_NUM_14) // 位时钟引脚(BCLK) - 同步每个bit的传输
#define I2S_WS_IO (GPIO_NUM_13) // 字选择引脚(WS/LRCK) - 区分左右声道
#define I2S_DO_IO (GPIO_NUM_45) // 数据输出引脚(DOUT) - ESP32→ES8311播放数据
#define I2S_DI_IO (GPIO_NUM_12) // 数据输入引脚(DIN) - ES7210→ESP32录音数据
/* 统一的音频参数 - ES7210和ES8311必须完全一致 */
#define EXAMPLE_SAMPLE_RATE (16000) // 采样率16kHz - 控制音频质量和数据量
#define EXAMPLE_MCLK_MULTIPLE (256) // MCLK倍数 - MCLK = 采样率 × 倍数
#define EXAMPLE_MCLK_FREQ_HZ (EXAMPLE_SAMPLE_RATE * EXAMPLE_MCLK_MULTIPLE) // 计算主时钟频率
#define EXAMPLE_I2S_FORMAT (ES7210_I2S_FMT_I2S) // I2S数据格式 - 标准飞利浦格式
#define EXAMPLE_BIT_WIDTH (16) // 音频位宽16bit - 每个样本的精度
#define EXAMPLE_CHANNELS (2) // 立体声 - 左右两个声道
/* 缓冲区和音量配置 */
#define EXAMPLE_RECV_BUF_SIZE (2400) // 接收缓冲区大小(字节)
#define EXAMPLE_VOICE_VOLUME (73) // 播放音量 (0-100)
/* ES7210 ADC芯片配置参数 */
#define EXAMPLE_ES7210_I2C_ADDR (0x41) // ES7210的I2C地址
#define EXAMPLE_ES7210_I2C_CLK (100000) // I2C时钟频率100kHz
#define EXAMPLE_ES7210_MIC_GAIN (ES7210_MIC_GAIN_15DB) // 麦克风增益15dB
#define EXAMPLE_ES7210_MIC_BIAS (ES7210_MIC_BIAS_2V87) // 麦克风偏置电压2.87V
#define EXAMPLE_ES7210_ADC_VOLUME (0) // ADC音量设置
// I2S通道句柄 - 全局变量,用于其他文件访问
extern i2s_chan_handle_t tx_handle; // 发送通道句柄(播放)
extern i2s_chan_handle_t rx_handle; // 接收通道句柄(录音)
/* 函数声明 */
/**
* @brief 初始化I2S全双工通信
* @return ESP_OK 成功, 其他值表示失败
* @note 配置I2S为主模式,同时支持录音和播放
*/
esp_err_t i2s_full_duplex_init(void);
/**
* @brief 初始化ES8311播放芯片(仅DAC功能)
* @return ESP_OK 成功, 其他值表示失败
* @note 配置ES8311为DAC模式,负责音频播放输出
*/
esp_err_t es8311_playback_init(void);
/**
* @brief 初始化ES7210录音芯片(仅ADC功能)
* @return ESP_OK 成功, 其他值表示失败
* @note 配置ES7210为ADC模式,负责音频录音输入
*/
esp_err_t es7210_capture_init(void);
#endif // MY_I2S_H
pca9557.c
c
#include "pca9557.h"
static const char *TAG = "pca9557";
// 读取PCA9557寄存器的值
esp_err_t pca9557_register_read(uint8_t reg_addr, uint8_t *data, size_t len)
{
return i2c_master_write_read_device(BSP_I2C_NUM, PCA9557_SENSOR_ADDR, ®_addr, 1, data, len, 1000 / portTICK_PERIOD_MS);
}
// 给PCA9557的寄存器写值
esp_err_t pca9557_register_write_byte(uint8_t reg_addr, uint8_t data)
{
uint8_t write_buf[2] = {reg_addr, data};
return i2c_master_write_to_device(BSP_I2C_NUM, PCA9557_SENSOR_ADDR, write_buf, sizeof(write_buf), 1000 / portTICK_PERIOD_MS);
}
// 初始化PCA9557 IO扩展芯片
esp_err_t pca9557_init(void)
{
// 写入控制引脚默认值 DVP_PWDN=1 PA_EN = 0 LCD_CS = 1
pca9557_register_write_byte(PCA9557_OUTPUT_PORT, 0x05);
// 把PCA9557芯片的IO0 IO1 IO2设置为输出 其它引脚保持默认的输入
pca9557_register_write_byte(PCA9557_CONFIGURATION_PORT, 0xf8);
ESP_LOGI(TAG, "PCA9557初始化完成");
return ESP_OK;
}
// 设置PCA9557芯片的某个IO引脚输出高低电平
esp_err_t pca9557_set_output_state(uint8_t gpio_bit, uint8_t level)
{
uint8_t data;
esp_err_t res = ESP_FAIL;
pca9557_register_read(PCA9557_OUTPUT_PORT, &data, 1);
res = pca9557_register_write_byte(PCA9557_OUTPUT_PORT, SET_BITS(data, gpio_bit, level));
return res;
}
// 控制 PCA9557_LCD_CS 引脚输出高低电平 参数0输出低电平 参数1输出高电平
void lcd_cs(uint8_t level)
{
pca9557_set_output_state(LCD_CS_GPIO, level);
}
// 控制 PCA9557_PA_EN 引脚输出高低电平 参数0输出低电平 参数1输出高电平
void pa_en(uint8_t level)
{
pca9557_set_output_state(PA_EN_GPIO, level);
}
// 控制 PCA9557_DVP_PWDN 引脚输出高低电平 参数0输出低电平 参数1输出高电平
void dvp_pwdn(uint8_t level)
{
pca9557_set_output_state(DVP_PWDN_GPIO, level);
}
// 初始化I2C总线
esp_err_t bsp_i2c_init(void)
{
i2c_config_t i2c_conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = BSP_I2C_SDA,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_io_num = BSP_I2C_SCL,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = BSP_I2C_FREQ_HZ};
i2c_param_config(BSP_I2C_NUM, &i2c_conf);
return i2c_driver_install(BSP_I2C_NUM, i2c_conf.mode, 0, 0, 0);
}
pca9557.h
c
#ifndef PCA9557_H
#define PCA9557_H
#include "driver/i2c.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#define PCA9557_SENSOR_ADDR 0x19 /*!< Slave address of the PCA9557 sensor */
#define BSP_I2C_SDA (GPIO_NUM_1) // SDA引脚
#define BSP_I2C_SCL (GPIO_NUM_2) // SCL引脚
#define BSP_I2C_NUM (0) // I2C外设
#define BSP_I2C_FREQ_HZ 100000 // 100kHz
#define PCA9557_INPUT_PORT 0x00
#define PCA9557_OUTPUT_PORT 0x01
#define PCA9557_POLARITY_INVERSION_PORT 0x02
#define PCA9557_CONFIGURATION_PORT 0x03
#define LCD_CS_GPIO BIT(0) // PCA9557_GPIO_NUM_1
#define PA_EN_GPIO BIT(1) // PCA9557_GPIO_NUM_2
#define DVP_PWDN_GPIO BIT(2) // PCA9557_GPIO_NUM_3
#define SET_BITS(_m, _s, _v) ((_v) ? (_m) | ((_s)) : (_m) & ~((_s)))
esp_err_t pca9557_init(void);
void pa_en(uint8_t level);
esp_err_t bsp_i2c_init(void);
#endif // !PCA9557_H
总结
ESP32-S3 内置双 I2S 控制器,支持全双工通信及 Standard、TDM、PDM 多种模式,通过独立 TX/RX 单元与 DMA 实现高效音频传输,适配 8kHz~192kHz 采样率及多协议(Philips、MSB、PCM、PDM)。新版驱动采用 "通道 - 模式" 架构,简化配置流程,可灵活对接音频 Codec、麦克风等外设,示例通过全双工回环结合回声处理,展示了从录音到播放的完整链路,适用于音频采集、播放等多媒体场景。