【ESP32-IDF】高级外设开发3:I2S

系列文章目录

持续更新...


文章目录


前言

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 采样位宽。

docs.espressif.com

TDM 模式(i2s_tdm.h):标准/PCM 等时序的 多槽扩展,适合多通道音频。

docs.espressif.com

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, &reg_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、麦克风等外设,示例通过全双工回环结合回声处理,展示了从录音到播放的完整链路,适用于音频采集、播放等多媒体场景。

相关推荐
up向上up1 天前
【普中】基于普中51开发板单片机的8_8点阵滚动显示设计
单片机·51单片机·proteus
EXtreme351 天前
征服 C 语言文件 I/O:透视数据流、FILE* 核心机制与高效实践全指南
c语言··文件io
Bona Sun1 天前
单片机手搓掌上游戏机(十二)—esp8266运行gameboy模拟器之编译上传
c语言·c++·单片机·游戏机
恒锐丰小吕1 天前
晶准 RB302B 内置MOSFET锂电池保护芯片技术解析
嵌入式硬件·硬件工程
星期天21 天前
3.2联合体和枚举enum,还有动态内存malloc,free,calloc,realloc
c语言·开发语言·算法·联合体·动态内存·初学者入门·枚举enum
TangDuoduo00051 天前
【电感基础与特性】
stm32·单片机·嵌入式硬件
许商1 天前
【stm32】【SD】SDIO fatfs
stm32·单片机·嵌入式硬件
自信150413057591 天前
初学者小白复盘23之——联合与枚举
c语言·1024程序员节
就是蠢啊1 天前
51单片机——独立按钮、矩阵按键
单片机·嵌入式硬件·51单片机
云山工作室1 天前
多传感器融合的办公室智能门禁系统(论文+源码)
stm32·单片机·嵌入式硬件·物联网·毕业设计·课程设计