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

相关推荐
泽虞15 分钟前
《LINUX系统编程》笔记p3
linux·运维·服务器·c语言·笔记·面试
你怎么知道我是队长2 小时前
C语言---编译的最小单位---令牌(Token)
java·c语言·前端
bai5459363 小时前
STM32 硬件I2C读写MPU6050
stm32·单片机·嵌入式硬件
LS_learner3 小时前
6020角度双环控制一种用于电机控制的策略
嵌入式硬件
TDengine (老段)3 小时前
TDengine IDMP 运维指南(4. 使用 Docker 部署)
运维·数据库·物联网·docker·时序数据库·tdengine·涛思数据
TDengine (老段)3 小时前
TDengine IDMP 最佳实践
大数据·数据库·物联网·ai·时序数据库·tdengine·涛思数据
IT.小航4 小时前
STM32F103RC的USB上拉电阻1.5K
stm32·单片机·嵌入式硬件
m0_555762904 小时前
MCU 开发工具汇总
单片机·嵌入式硬件
AAA修煤气灶刘哥5 小时前
后端仔狂喜!手把手教你用 Java 拿捏华为云 IoTDA,设备上报数据 so easy
后端·物联网·华为