参考:I2S - ESP32 - — ESP-IDF 编程指南 v5.2.5 文档

简介

I2S(Inter-IC Sound,集成电路内置音频总线)是一种同步串行通信协议,通常用于在两个数字音频设备之间传输音频数据。

ESP32-S3 包含 2 个 I2S 外设。通过配置这些外设,可以借助 I2S 驱动来输入和输出采样数据。

标准或 TDM 通信模式下的 I2S 总线包含以下几条线路:

  • MCLK:主时钟线。该信号线可选,具体取决于从机,主要用于向 I2S 从机提供参考时钟。
  • BCLK:位时钟线。用于数据线的位时钟。
  • WS:字(声道)选择线。通常用于识别声道(除 PDM 模式外)。
  • DIN/DOUT:串行数据输入/输出线。如果 DIN 和 DOUT 被配置到相同的 GPIO,数据将在内部回环。

PDM 通信模式下的 I2S 总线包含以下几条线路:

  • CLK:PDM 时钟线。
  • DIN/DOUT:串行数据输入/输出线。

每个 I2S 控制器都具备以下功能,可由 I2S 驱动进行配置:

  • 可用作系统主机或从机
  • 可用作发射器或接收器
  • DMA 控制器支持流数据采样,CPU 无需单独复制每个采样数据

每个控制器都有独立的 RX 和 TX 通道,连接到不同 GPIO 管脚,能够在不同的时钟和声道配置下工作。注意,尽管在一个控制器上 TX 通道和 RX 通道的内部 MCLK 相互独立,但输出的 MCLK 信号只能连接到一个通道。如果需要两个互相独立的 MCLK 输出,必须将其分配到不同的 I2S 控制器上。

I2S 文件结构

请添加图片描述

需要包含在 I2S 应用中的公共头文件如下所示:

  • i2s.h:提供原有 I2S API(用于使用原有驱动的应用)。
  • i2s_std.h:提供标准通信模式的 API(用于使用标准模式的新驱动程序的应用)。
  • i2s_pdm.h:提供 PDM 通信模式的 API(用于使用 PDM 模式的新驱动程序的应用)。
  • i2s_tdm.h:提供 TDM 通信模式的 API(用于使用 TDM 模式的新驱动的应用)。

备注:原有驱动与新驱动无法共存。包含 i2s.h 以使用原有驱动,或包含其他三个头文件以使用新驱动。原有驱动未来可能会被删除。

已包含在上述头文件中的公共头文件如下所示:

  • i2s_types_legacy.h:提供只在原有驱动中使用的原有公共类型。
  • i2s_types.h:提供公共类型。
  • i2s_common.h:提供所有通信模式通用的 API。

I2S 时钟

时钟源

  • i2s_clock_src_t::I2S_CLK_SRC_DEFAULT:默认 PLL 时钟。
  • i2s_clock_src_t::I2S_CLK_SRC_PLL_160M:160 MHz PLL 时钟。
  • i2s_clock_src_t::I2S_CLK_SRC_APLL:音频 PLL 时钟,在高采样率应用中比 I2S_CLK_SRC_PLL_160M 更精确。其频率可根据采样率进行配置,但如果 APLL 已经被 EMAC 或其他通道占用,则无法更改 APLL 频率,驱动程序将尝试在原有 APLL 频率下工作。如果原有 APLL 频率无法满足 I2S 的需求,时钟配置将失败。

时钟术语

  • 采样率:单声道每秒采样数据数量。
  • SCLK:源时钟频率,即时钟源的频率。
  • MCLK:主时钟频率,BCLK 由其产生。MCLK 信号通常作为参考时钟,用于同步 I2S 主机和从机之间的 BCLK 和 WS。
  • BCLK:位时钟频率,一个 BCLK 时钟周期代表数据管脚上的一个数据位。通过 i2s_std_slot_config_t::slot_bit_width 配置的通道位宽即为一个声道中的 BCLK 时钟周期数量,因此一个声道中可以有 8/16/24/32 个 BCLK 时钟周期。
  • LRCK / WS:左/右时钟或字选择时钟。在非 PDM 模式下,其频率等于采样率。

备注

通常,MCLK 应该同时是 采样率 和 BCLK 的倍数。字段 i2s_std_clk_config_t::mclk_multiple 表示 MCLK 相对于 采样率 的倍数。在大多数情况下,将其设置为 I2S_MCLK_MULTIPLE_256 即可。但如果 slot_bit_width 被设置为 I2S_SLOT_BIT_WIDTH_24BIT,为了保证 MCLK 是 BCLK 的整数倍,应该将 i2s_std_clk_config_t::mclk_multiple 设置为能被 3 整除的倍数,如 I2S_MCLK_MULTIPLE_384,否则 WS 会不精准。

I2S 通信模式

模式概览

芯片 I2S 标准 PDM TX PDM RX TDM ADC/DAC LCD/摄像头
ESP32 I2S 0/1 I2S 0 I2S 0 I2S 0 I2S 0
ESP32-S2 I2S 0 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

标准模式

标准模式中有且仅有左右两个声道,驱动中将声道称为 slot。这些声道可以支持 8/16/24/32 位宽的采样数据,声道的通信格式主要包括以下几种:

  • Philips 格式:数据信号与 WS 信号相比有一个位的位移。WS 信号的占空比为 50%。

在这里插入图片描述

  • MSB 格式:与 Philips 格式基本相同,但其数据没有位移。
    在这里插入图片描述

  • PCM 帧同步:数据有一个位的位移,同时 WS 信号变成脉冲,持续一个 BCLK 周期。
    在这里插入图片描述

PDM 模式 (TX)

在 PDM(Pulse-density Modulation,脉冲密度调制)模式下,TX 通道可以将 PCM 数据转换为 PDM 格式,该格式始终有左右两个声道。PDM TX 只在 I2S0 中受支持,且只支持 16 位宽的采样数据。PDM TX 至少需要一个 CLK 管脚用于时钟信号,一个 DOUT 管脚用于数据信号(即下图中的 WS 和 SD 信号。BCK 信号为内部位采样时钟,在 PDM 设备之间不需要)。PDM 模式允许用户配置上采样参数 i2s_pdm_tx_clk_config_t::up_sample_fpi2s_pdm_tx_clk_config_t::up_sample_fs,上采样率可以通过公式 up_sample_rate = i2s_pdm_tx_clk_config_t::up_sample_fp / i2s_pdm_tx_clk_config_t::up_sample_fs 来计算。在 PDM TX 中有以下两种上采样模式:

  • 固定时钟频率模式:在这种模式下,上采样率将根据采样率的变化而变化。设置 fp = 960fs = sample_rate / 100,则 CLK 管脚上的时钟频率 (Fpdm) 将固定为 128 * 48 KHz = 6.144 MHz。注意此频率不等于采样率 (Fpcm)。
  • 固定上采样率模式:在这种模式下,上采样率固定为 2。设置 fp = 960fs = 480,则 CLK 管脚上的时钟频率 (Fpdm) 将为 128 * sample_rate
    在这里插入图片描述

PDM 模式 (RX)

在 PDM(Pulse-density Modulation,脉冲密度调制)模式下,RX 通道可以接收 PDM 格式的数据并将数据转换成 PCM 格式。PDM RX 只在 I2S0 中受支持,且只支持 16 位宽的采样数据。PDM RX 至少需要一个 CLK 管脚用于时钟信号,一个 DIN 管脚用于数据信号。此模式允许用户配置下采样参数 i2s_pdm_rx_clk_config_t::dn_sample_mode。在 PDM RX 中有以下两种下采样模式:

功能概览

I2S 驱动提供以下服务:

资源管理

I2S 驱动中的资源可分为三个级别:

  • 平台级资源:当前芯片中所有 I2S 控制器的资源。
  • 控制器级资源:一个 I2S 控制器的资源。
  • 通道级资源:一个 I2S 控制器 TX 或 RX 通道的资源。

公开的 API 都是通道级别的 API,通道句柄 i2s_chan_handle_t 可以帮助用户管理特定通道下的资源,而无需考虑其他两个级别的资源。高级别资源为私有资源,由驱动自动管理。用户可以调用 i2s_new_channel() 来分配通道句柄,或调用 i2s_del_channel() 来删除该句柄。

有限状态机

I2S 通道有三种状态,分别为 registered(已注册)ready(准备就绪)running(运行中),它们的关系如下图所示:
在这里插入图片描述

图中的 可用相应的 I2S 通信模式来代替,如 std 代表标准的双声道模式。更多关于通信模式的信息,请参考 I2S 通信模式 小节。

数据传输

2S 的数据传输(包括数据发送和接收)由 DMA 实现。在传输数据之前,请调用 i2s_channel_enable() 来启用特定的通道。发送或接收的数据达到 DMA 缓冲区的大小时,将触发 I2S_OUT_EOFI2S_IN_SUC_EOF 中断。注意,DMA 缓冲区的大小不等于 i2s_chan_config_t::dma_frame_num,这里的一帧是指一个 WS 周期内的所有采样数据。因此, dma_buffer_size = dma_frame_num * slot_num * slot_bit_width / 8。传输数据时,可以调用 i2s_channel_write() 来输入数据,并把数据从源缓冲区复制到 DMA TX 缓冲区等待传输完成。此过程将重复进行,直到发送的字节数达到配置的大小。接收数据时,用户可以调用函数 i2s_channel_read() 来等待接收包含 DMA 缓冲区地址的消息队列,从而将数据从 DMA RX 缓冲区复制到目标缓冲区。

i2s_channel_write()i2s_channel_read() 都是阻塞函数,在源缓冲区的数据发送完毕前,或是整个目标缓冲区都被加载数据占用时,它们会一直保持等待状态。在等待时间达到最大阻塞时间时,返回 ESP_ERR_TIMEOUT 错误。要实现异步发送或接收数据,可以通过 i2s_channel_register_event_callback() 注册回调,随即便可在回调函数中直接访问 DMA 缓冲区,无需通过这两个阻塞函数来发送或接收数据。但请注意,该回调是一个中断回调,不要在该回调中添加复杂的逻辑、进行浮点运算或调用不可重入函数。

应用实例

标准 TX/RX 模式的应用

不同声道的通信格式可通过以下标准模式的辅助宏来生成。如上所述,在标准模式下有三种格式,辅助宏分别为:

时钟配置的辅助宏为:

请参考 标准模式 了解 STD API 的相关信息。更多细节请参考 driver/i2s/include/driver/i2s_std.h

STD TX 模式

以 16 位数据位宽为例,如果 uint16_t 写缓冲区中的数据如下所示:

数据 0 数据 1 数据 2 数据 3 数据 4 数据 5 数据 6 数据 7
0x0001 0x0002 0x0003 0x0004 0x0005 0x0006 0x0007 0x0008

下表展示了在不同 i2s_std_slot_config_t::slot_mode 和 i2s_std_slot_config_t::slot_mask 设置下线路上的真实数据。

数据位宽 声道模式 声道掩码 WS 低电平 WS 高电平 WS 低电平 WS 高电平 WS 低电平 WS 高电平 WS 低电平 WS 高电平
16 位
单声道
0x0002 0x0000 0x0001 0x0000 0x0004 0x0000 0x0003 0x0000
0x0000 0x0002 0x0000 0x0001 0x0000 0x0004 0x0000 0x0003
左右 0x0002 0x0002 0x0001 0x0001 0x0004 0x0004 0x0003 0x0003
立体声
0x0001 0x0001 0x0003 0x0003 0x0005 0x0005 0x0007 0x0007
0x0002 0x0002 0x0004 0x0004 0x0006 0x0006 0x0008 0x0008
左右 0x0001 0x0002 0x0003 0x0004 0x0005 0x0006 0x0007 0x0008

备注

当数据位宽为 32 位时,情况与上表类似,但当位宽为 8 位和 24 位时需要额外注意。数据位宽为 8 时,写入的缓冲区仍应使用 uint16_t (即以 2 字节对齐),并且只有高 8 位有效,低 8 位将被丢弃;数据位宽为 24 时,缓冲区应该使用 uint32_t ( 即以 4 字节对齐),并且只有高 24 位有效,低 8 位将被丢弃。

另外,在 8 位宽和 16 位宽单声道模式下,线路上的真实数据顺序会被调换。为了获取正确的数据顺序,写入缓冲区时,每两个字节需要调换一次数据顺序。

#include "driver/i2s_std.h"
#include "driver/gpio.h"

i2s_chan_handle_t tx_handle;
/* 通过辅助宏获取默认的通道配置
 * 这个辅助宏在 'i2s_common.h' 中定义,由所有 I2S 通信模式共享
 * 它可以帮助指定 I2S 角色和端口 ID */
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
/* 分配新的 TX 通道并获取该通道的句柄 */
i2s_new_channel(&chan_cfg, &tx_handle, NULL);

/* 进行配置,可以通过宏生成声道配置和时钟配置
 * 这两个辅助宏在 'i2s_std.h' 中定义,只能用于 STD 模式
 * 它们可以帮助初始化或更新声道和时钟配置 */
i2s_std_config_t std_cfg = {
    .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(48000),
    .slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_STEREO),
    .gpio_cfg = {
        .mclk = I2S_GPIO_UNUSED,
        .bclk = GPIO_NUM_4,
        .ws = GPIO_NUM_5,
        .dout = GPIO_NUM_18,
        .din = I2S_GPIO_UNUSED,
        .invert_flags = {
            .mclk_inv = false,
            .bclk_inv = false,
            .ws_inv = false,
        },
    },
};
/* 初始化通道 */
i2s_channel_init_std_mode(tx_handle, &std_cfg);

/* 在写入数据之前,先启用 TX 通道 */
i2s_channel_enable(tx_handle);
i2s_channel_write(tx_handle, src_buf, bytes_to_write, bytes_written, ticks_to_wait);

/* 如果需要更新声道或时钟配置
 * 需要在更新前先禁用通道 */
// i2s_channel_disable(tx_handle);
// std_cfg.slot_cfg.slot_mode = I2S_SLOT_MODE_MONO; // 默认为立体声
// i2s_channel_reconfig_std_slot(tx_handle, &std_cfg.slot_cfg);
// std_cfg.clk_cfg.sample_rate_hz = 96000;
// i2s_channel_reconfig_std_clock(tx_handle, &std_cfg.clk_cfg);

/* 删除通道之前必须先禁用通道 */
i2s_channel_disable(tx_handle);
/* 如果不再需要句柄,删除该句柄以释放通道资源 */
i2s_del_channel(tx_handle);
STD RX 模式

例如,当数据位宽为 16 时,如线路上的数据如下所示:

WS 低电平 WS 高电平 WS 低电平 WS 高电平 WS 低电平 WS 高电平 WS 低电平 WS 高电平
0x0001 0x0002 0x0003 0x0004 0x0005 0x0006 0x0007 0x0008

不同 i2s_std_slot_config_t::slot_mode 和 i2s_std_slot_config_t::slot_mask 配置下缓冲区中收到的数据如下所示。

数据位宽 声道模式 声道掩码 数据 0 数据 1 数据 2 数据 3 数据 4 数据 5 数据 6 数据 7
16 位
单声道
0x0001 0x0000 0x0005 0x0003 0x0009 0x0007 0x000d 0x000b
0x0002 0x0000 0x0006 0x0004 0x000a 0x0008 0x000e 0x000c
立体声 任意 0x0001 0x0002 0x0003 0x0004 0x0005 0x0006 0x0007 0x0008

备注

ESP32 上的接收有些复杂。首先,当数据位宽为 8 位或 24 位时,接收的数据仍将以 2 个字节或 4 个字节对齐,这意味着有效数据被放在每两个字节的高 8 位和每四个字节的高 24 位。例如,当线路上的数据是 8 位宽度的 0x5A 时,接收的数据将是 0x5A00;当数据是 0x00 005A 时,则收到 0x0000 5A00。其次,在 8 位宽和 16 位宽单声道传输中,缓冲区内每两个数据会进行一次数据翻转,因此可能需要手动将顺序回转,以获取正确的数据顺序。

#include "driver/i2s_std.h"
#include "driver/gpio.h"

i2s_chan_handle_t rx_handle;
/* 通过辅助宏获取默认的通道配置
 * 这个辅助宏在 'i2s_common.h' 中定义,由所有 I2S 通信模式共享
 * 它可以帮助指定 I2S 角色和端口 ID */
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
/* 分配新的 TX 通道并获取该通道的句柄 */
i2s_new_channel(&chan_cfg, NULL, &rx_handle);

/* 进行配置,可以通过宏生成声道配置和时钟配置
 * 这两个辅助宏在 'i2s_std.h' 中定义,只能用于 STD 模式
 * 它们可以帮助初始化或更新声道和时钟配置 */
i2s_std_config_t std_cfg = {
    .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(48000),
    .slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_STEREO),
    .gpio_cfg = {
        .mclk = I2S_GPIO_UNUSED,
        .bclk = GPIO_NUM_4,
        .ws = GPIO_NUM_5,
        .dout = I2S_GPIO_UNUSED,
        .din = GPIO_NUM_19,
        .invert_flags = {
            .mclk_inv = false,
            .bclk_inv = false,
            .ws_inv = false,
        },
    },
};
/* 初始化通道 */
i2s_channel_init_std_mode(rx_handle, &std_cfg);

/* 在读取数据之前,先启动 RX 通道 */
i2s_channel_enable(rx_handle);
i2s_channel_read(rx_handle, desc_buf, bytes_to_read, bytes_read, ticks_to_wait);

/* 删除通道之前必须先禁用通道 */
i2s_channel_disable(rx_handle);
/* 如果不再需要句柄,删除该句柄以释放通道资源 */
i2s_del_channel(rx_handle);

PDM TX 模式的应用

针对 TX 通道的 PDM 模式,声道配置的辅助宏为:

时钟配置的辅助宏为:

PDM TX API 的相关信息,可参考 PDM 模式。更多细节请参阅 driver/i2s/include/driver/i2s_pdm.h

PDM 数据位宽固定为 16 位。如果 int16_t 写缓冲区中的数据如下:

数据 0 数据 1 数据 2 数据 3 数据 4 数据 5 数据 6 数据 7
0x0001 0x0002 0x0003 0x0004 0x0005 0x0006 0x0007 0x0008

下表展示了不同 i2s_pdm_tx_slot_config_t::slot_modei2s_pdm_tx_slot_config_t::slot_mask 设置下线路上的真实数据。为方便理解,已将线路上的数据格式由 PDM 转为 PCM。

声道模式 声道掩码
单声道
0x0001 0x0000 0x0002 0x0000 0x0003 0x0000 0x0004 0x0000
0x0000 0x0001 0x0000 0x0002 0x0000 0x0003 0x0000 0x0004
左右 0x0001 0x0001 0x0002 0x0002 0x0003 0x0003 0x0004 0x0004
立体声
0x0001 0x0001 0x0003 0x0003 0x0005 0x0005 0x0007 0x0007
0x0002 0x0002 0x0004 0x0004 0x0006 0x0006 0x0008 0x0008
左右 0x0001 0x0002 0x0003 0x0004 0x0005 0x0006 0x0007 0x0008
#include "driver/i2s_pdm.h"
#include "driver/gpio.h"

/* 分配 I2S TX 通道 */
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
i2s_new_channel(&chan_cfg, &tx_handle, NULL);

/* 初始化通道为 PDM TX 模式 */
i2s_pdm_tx_config_t pdm_tx_cfg = {
    .clk_cfg = I2S_PDM_TX_CLK_DEFAULT_CONFIG(36000),
    .slot_cfg = I2S_PDM_TX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
    .gpio_cfg = {
        .clk = GPIO_NUM_5,
        .dout = GPIO_NUM_18,
        .invert_flags = {
            .clk_inv = false,
        },
    },
};
i2s_channel_init_pdm_tx_mode(tx_handle, &pdm_tx_cfg);

...

PDM RX 模式的应用

针对 RX 通道的 PDM 模式,声道配置的辅助宏为:

时钟配置的辅助宏为:

PDM RX API 的相关信息,可参考 PDM 模式。更多细节请参阅 driver/i2s/include/driver/i2s_pdm.h

PDM 数据位宽固定为 16 位。如果线路上的数据如下所示。为方便理解,已将线路上的数据格式由 PDM 转为 PCM。

0x0001 0x0002 0x0003 0x0004 0x0005 0x0006 0x0007 0x0008

下表展示了不同 i2s_pdm_rx_slot_config_t::slot_modei2s_pdm_rx_slot_config_t::slot_mask 设置下 ‘int16_t’ 缓冲区接收的数据。

声道模式 声道掩码 数据 0 数据 1 数据 2 数据 3 数据 4 数据 5 数据 6 数据 7
单声道
0x0001 0x0003 0x0005 0x0007 0x0009 0x000b 0x000d 0x000f
0x0002 0x0004 0x0006 0x0008 0x000a 0x000c 0x000e 0x0010
立体声 左右 0x0001 0x0002 0x0003 0x0004 0x0005 0x0006 0x0007 0x0008
#include "driver/i2s_pdm.h"
#include "driver/gpio.h"

i2s_chan_handle_t rx_handle;

/* 分配 I2S RX 通道 */
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
i2s_new_channel(&chan_cfg, NULL, &rx_handle);

/* 初始化通道为 PDM RX 模式 */
i2s_pdm_rx_config_t pdm_rx_cfg = {
    .clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG(36000),
    .slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
    .gpio_cfg = {
        .clk = GPIO_NUM_5,
        .din = GPIO_NUM_19,
        .invert_flags = {
            .clk_inv = false,
        },
    },
};
i2s_channel_init_pdm_rx_mode(rx_handle, &pdm_rx_cfg);

...

全双工

全双工模式可以在 I2S 端口中同时注册 TX 和 RX 通道,同时通道共享 BCLK 和 WS 信号。目前,STD 和 TDM 通信模式支持以下方式的全双工通信,但不支持 PDM 全双工模式,因为 PDM 模式下 TX 和 RX 通道的时钟不同。

请注意,一个句柄只能代表一个通道,因此仍然需要对 TX 和 RX 通道逐个进行声道和时钟配置。

以下示例展示了如何分配两个全双工通道:

#include "driver/i2s_std.h"
#include "driver/gpio.h"

i2s_chan_handle_t tx_handle;
i2s_chan_handle_t rx_handle;

/* 分配两个 I2S 通道 */
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
/* 同时分配给 TX 和 RX 通道,使其进入全双工模式。 */
i2s_new_channel(&chan_cfg, &tx_handle, &rx_handle);

/* 配置两个通道,因为在全双工模式下,TX 和 RX 通道必须相同。 */
i2s_std_config_t std_cfg = {
    .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(32000),
    .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
    .gpio_cfg = {
        .mclk = I2S_GPIO_UNUSED,
        .bclk = GPIO_NUM_4,
        .ws = GPIO_NUM_5,
        .dout = GPIO_NUM_18,
        .din = GPIO_NUM_19,
        .invert_flags = {
            .mclk_inv = false,
            .bclk_inv = false,
            .ws_inv = false,
        },
    },
};
i2s_channel_init_std_mode(tx_handle, &std_cfg);
i2s_channel_init_std_mode(rx_handle, &std_cfg);

i2s_channel_enable(tx_handle);
i2s_channel_enable(rx_handle);

...

单工模式

在单工模式下分配通道句柄,应该为每个通道调用 i2s_new_channel()。在 ESP32 上,TX/RX 通道的时钟和 GPIO 管脚不是相互独立的,因此在单工模式下,TX 和 RX 通道不能共存于同一个 I2S 端口中。

#include "driver/i2s_std.h"
#include "driver/gpio.h"

i2s_chan_handle_t tx_handle;
i2s_chan_handle_t rx_handle;

i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
i2s_new_channel(&chan_cfg, &tx_handle, NULL);
i2s_std_config_t std_tx_cfg = {
    .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(48000),
    .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
    .gpio_cfg = {
        .mclk = GPIO_NUM_0,
        .bclk = GPIO_NUM_4,
        .ws = GPIO_NUM_5,
        .dout = GPIO_NUM_18,
        .din = I2S_GPIO_UNUSED,
        .invert_flags = {
            .mclk_inv = false,
            .bclk_inv = false,
            .ws_inv = false,
        },
    },
};
/* 初始化通道 */
i2s_channel_init_std_mode(tx_handle, &std_tx_cfg);
i2s_channel_enable(tx_handle);

/* 如果没有找到其他可用的 I2S 设备,RX 通道将被注册在另一个 I2S 上
 * 并返回 ESP_ERR_NOT_FOUND */
i2s_new_channel(&chan_cfg, NULL, &rx_handle);
i2s_std_config_t std_rx_cfg = {
    .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(16000),
    .slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_STEREO),
    .gpio_cfg = {
        .mclk = I2S_GPIO_UNUSED,
        .bclk = GPIO_NUM_6,
        .ws = GPIO_NUM_7,
        .dout = I2S_GPIO_UNUSED,
        .din = GPIO_NUM_19,
        .invert_flags = {
            .mclk_inv = false,
            .bclk_inv = false,
            .ws_inv = false,
        },
    },
};
i2s_channel_init_std_mode(rx_handle, &std_rx_cfg);
i2s_channel_enable(rx_handle);

应用注意事项

防止数据丢失

对于需要高频采样率的应用,数据的巨大吞吐量可能会导致数据丢失。用户可以通过注册 ISR 回调函数来接收事件队列中的数据丢失事件:

static IRAM_ATTR bool i2s_rx_queue_overflow_callback(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx)
{
    // 处理 RX 队列溢出事件 ...
    return false;
}

i2s_event_callbacks_t cbs = {
    .on_recv = NULL,
    .on_recv_q_ovf = i2s_rx_queue_overflow_callback,
    .on_sent = NULL,
    .on_send_q_ovf = NULL,
};
TEST_ESP_OK(i2s_channel_register_event_callback(rx_handle, &cbs, NULL));

请按照以下步骤操作,以防止数据丢失:

  1. 确定中断间隔。通常来说,当发生数据丢失时,为减少中断次数,中断间隔应该越久越好。因此,在保证 DMA 缓冲区大小不超过最大值 4092 的前提下,应使 dma_frame_num 尽可能大。具体转换关系如下:

    interrupt_interval(unit: sec) = dma_frame_num / sample_rate
    dma_buffer_size = dma_frame_num * slot_num * data_bit_width / 8 <= 4092
    
  2. 确定 dma_desc_num 的值。dma_desc_num 由 i2s_channel_read 轮询周期的最大时间决定,所有接收到的数据都应该存储在两个 i2s_channel_read 之间。这个周期可以通过计时器或输出 GPIO 信号来计算。具体转换关系如下:

dma_desc_num > polling_cycle / interrupt_interval
  1. 确定接收缓冲区大小。在 i2s_channel_read 中提供的接收缓冲区应当能够容纳所有 DMA 缓冲区中的数据,这意味着它应该大于所有 DMA 缓冲区的总大小:

    recv_buffer_size > dma_desc_num * dma_buffer_size
    

例如,如果某个 I2S 应用的已知值包括:

sample_rate = 144000 Hz
data_bit_width = 32 bits
slot_num = 2
polling_cycle = 10 ms

那么可以按照以下公式计算出参数 dma_frame_numdma_desc_numrecv_buf_size:

dma_frame_num * slot_num * data_bit_width / 8 = dma_buffer_size <= 4092
dma_frame_num <= 511
interrupt_interval = dma_frame_num / sample_rate = 511 / 144000 = 0.003549 s = 3.549 ms
dma_desc_num > polling_cycle / interrupt_interval = cell(10 / 3.549) = cell(2.818) = 3
recv_buffer_size > dma_desc_num * dma_buffer_size = 3 * 4092 = 12276 bytes

API 参考

标准模式 API

添加头文件

#include "driver/i2s_std.h"

并在CMakeLists.txt中添加

REQUIRES driver
或
PRIV_REQUIRES driver
将I2S通道初始化为标准模式。
esp_err_t i2s_channel_init_std_mode(i2s_chan_handle_t handle, const i2s_std_config_t *std_cfg)

参数
handle – [输入] I2S通道句柄

std_cfg -- [输入] 标准模式的配置,包括时钟、时隙和通用输入输出(GPIO)。时钟配置可通过辅助宏I2S_STD_CLK_DEFAULT_CONFIG生成。时隙配置可通过辅助宏I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG、I2S_STD_PCM_SLOT_DEFAULT_CONFIG或I2S_STD_MSB_SLOT_DEFAULT_CONFIG生成。

返回值
ESP_OK 初始化成功

ESP_ERR_NO_MEM 没有用于存储通道信息的内存

ESP_ERR_INVALID_ARG 空指针或无效配置

ESP_ERR_INVALID_STATE 此通道未注册

注意

仅允许在通道状态为“已注册”时调用(即通道已分配但未初始化)。若初始化成功,状态将更新为“就绪”;否则,状态将恢复为“已注册”。

为标准模式重新配置I2S时钟。
esp_err_t i2s_channel_reconfig_std_clock(i2s_chan_handle_t handle, const i2s_std_clk_config_t *clk_cfg)

参数:
handle – [输入] I2S通道句柄
clk_cfg – [输入] 标准模式时钟配置,可由I2S_STD_CLK_DEFAULT_CONFIG生成

返回值:
ESP_OK 时钟设置成功
ESP_ERR_INVALID_ARG 空指针、无效配置或非标准模式
ESP_ERR_INVALID_STATE 此通道未初始化或未停止

注意:

  1. 仅允许在通道状态为“就绪”时调用,即通道已初始化但尚未启动,此函数不会改变通道状态。如果I2S已启动,调用此函数前应先调用i2s_channel_disable 。
  2. 输入的通道句柄必须已初始化为标准模式,即在重新配置之前必须已调用过i2s_channel_init_std_mode 。
为标准模式重新配置I2S时隙。

esp_err_t i2s_channel_reconfig_std_slot(i2s_chan_handle_t handle, const i2s_std_slot_config_t *slot_cfg)

参数:
handle – [输入] I2S通道句柄
slot_cfg – [输入] 标准模式时隙配置,可由I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG、I2S_STD_PCM_SLOT_DEFAULT_CONFIG和 I2S_STD_MSB_SLOT_DEFAULT_CONFIG生成。

返回值:
ESP_OK 成功设置时隙
ESP_ERR_NO_MEM 无用于DMA缓冲区的内存
ESP_ERR_INVALID_ARG 空指针、无效配置或非标准模式
ESP_ERR_INVALID_STATE 此通道未初始化或未停止

注意

  1. 仅当通道状态为“就绪”时(即通道已初始化但尚未启动)才允许调用此函数,该函数不会改变通道状态。如果I2S已经启动,在调用此函数之前应先调用i2s_channel_disable 。
  2. 输入的通道句柄必须已初始化为标准模式,也就是说,在重新配置之前必须已经调用过i2s_channel_init_std_mode函数。
为标准模式重新配置I2S通用输入输出引脚(GPIO)。

esp_err_t i2s_channel_reconfig_std_gpio(i2s_chan_handle_t handle, const i2s_std_gpio_config_t *gpio_cfg)

参数:
handle – [输入] I2S通道句柄
gpio_cfg – [输入] 由用户指定的标准模式GPIO配置

返回值:
ESP_OK 成功设置GPIO
ESP_ERR_INVALID_ARG 空指针、无效配置或非标准模式
ESP_ERR_INVALID_STATE 此通道未初始化或未停止

注意:

  1. 仅当通道状态为“就绪”时(即通道已初始化但尚未启动)才允许调用此函数,该函数不会改变通道状态。如果I2S已经启动,在调用此函数之前应先调用i2s_channel_disable 。
  2. 输入的通道句柄必须已初始化为标准模式,也就是说,在重新配置之前必须已经调用过i2s_channel_init_std_mode函数。

I2S 驱动

添加头文件

#include "driver/i2s_common.h"

并在CMakeLists.txt中添加

REQUIRES driver
或
PRIV_REQUIRES driver
分配新的I2S通道。

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)

参数:
chan_cfg – [输入] I2S控制器通道配置
ret_tx_handle – [输出] 用于管理发送通道的I2S通道句柄(可选)
ret_rx_handle – [输出] 用于管理接收通道的I2S通道句柄(可选)

返回值:
ESP_OK 成功分配新通道
ESP_ERR_NOT_SUPPORTED 当前芯片不支持此通信模式
ESP_ERR_INVALID_ARG i2s_chan_config_t 中存在空指针或非法参数
ESP_ERR_NOT_FOUND 未找到可用的I2S通道

注意

  1. 新创建的I2S通道句柄在成功分配后将处于“已注册”状态。
  2. 当通道配置中的端口ID为I2S_NUM_AUTO时,驱动程序将在I2S控制器之一上自动分配I2S端口;否则,驱动程序将尝试在选定的端口上分配新通道。
  3. 如果tx_handle和rx_handle都不为空,这意味着此I2S控制器将工作在全双工模式,在这种情况下,接收(RX)和发送(TX)通道将被分配在同一个I2S端口上。请注意,在ESP32和ESP32S2上,TX/RX通道的一些配置是共享的,因此请确保它们在相同条件和相同状态(启动/停止)下工作。目前,全双工模式无法保证TX/RX通道同步进行写入/读取操作,它们目前仅能共享时钟信号。
  4. 如果tx_handle或rx_handle为空,这意味着此I2S控制器将工作在单工模式。对于ESP32和ESP32S2,即使仅注册了接收(RX)或发送(TX)通道中的一个,整个I2S控制器(即RX和TX通道)仍将被占用。对于其他目标设备,该控制器上的另一个通道仍可使用。
删除I2S通道。

esp_err_t i2s_del_channel(i2s_chan_handle_t handle)

参数:
handle – [输入] I2S通道句柄

返回值:
ESP_OK 删除成功
ESP_ERR_INVALID_ARG 空指针

注意

  1. 仅允许在I2S通道处于“已注册”或“就绪”状态时调用(即在删除前应先停止该通道)。
  2. 如果一个端口中的所有通道都被删除,资源将自动释放。
获取I2S通道信息

esp_err_t i2s_channel_get_info(i2s_chan_handle_t handle, i2s_chan_info_t *chan_info)

参数:
handle – [输入] I2S通道句柄
chan_info – [输出] I2S通道基本信息

返回值:
ESP_OK 成功获取I2S通道信息
ESP_ERR_NOT_FOUND 输入的句柄与任何已注册的I2S通道都不匹配,它可能不是I2S通道句柄,或者已不再可用
ESP_ERR_INVALID_ARG 输入的句柄或chan_info指针为空

启用I2S通道

esp_err_t i2s_channel_enable(i2s_chan_handle_t handle)

参数:
handle – [输入] I2S通道句柄

返回值:
ESP_OK 成功启动
ESP_ERR_INVALID_ARG 空指针
ESP_ERR_INVALID_STATE 此通道尚未初始化或已启动

注意:

  1. 仅当通道状态为“就绪”(即通道已初始化但尚未启动)时才允许调用此函数。一旦成功启用,通道将进入“运行”状态。
  2. 启用通道可以在硬件上启动I2S通信。它将开始输出位时钟(BCLK)和字选通(WS)信号。至于主时钟(MCLK)信号,将在初始化完成时开始输出。
禁止I2S通道

esp_err_t i2s_channel_disable(i2s_chan_handle_t handle)

参数:
handle – [输入] I2S通道句柄

返回值:
ESP_OK 成功启动
ESP_ERR_INVALID_ARG 空指针
ESP_ERR_INVALID_STATE 此通道尚未启动

注意:

  1. 仅当通道状态为“运行”(即通道已启动)时才允许调用此函数。一旦成功禁用,通道将进入“就绪”状态。
  2. 禁用该通道会停止硬件上的I2S通信。它会停止位时钟(BCLK)和字选通(WS)信号,但不会停止主时钟(MCLK)信号。
将数据预加载到发送(TX)直接内存访问(DMA)缓冲区中

esp_err_t i2s_channel_preload_data(i2s_chan_handle_t tx_handle, const void *src, size_t size, size_t *bytes_loaded)

参数:
tx_handle – [输入] I2S发送通道句柄
src – [输入] 要加载的源缓冲区指针
size – [输入] 源缓冲区大小
bytes_loaded – [输出] 成功加载到发送DMA缓冲区中的字节数

返回值:
ESP_OK 数据加载成功
ESP_ERR_INVALID_ARG 空指针或非发送方向
ESP_ERR_INVALID_STATE 此通道尚未启动

注意:

  1. 仅允许在通道状态为“就绪”时调用(即通道已初始化,但尚未启动)。
  2. 由于初始的DMA缓冲区中没有数据,通道启用后它将传输空缓冲区,此函数用于将数据预加载到DMA缓冲区中,以便通道启用后能立即传输有效数据。
  3. 在启用通道前,此函数可多次调用,后加载的缓冲区数据会连接在前次加载的缓冲区数据之后。但当所有DMA缓冲区都已加载满时,便无法再进行预加载。请查看bytes_loaded参数,以了解成功加载了多少字节。当bytes_loaded小于size时,意味着DMA缓冲区已满。
I2S写入数据

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)

参数:
handle – [输入] I2S通道句柄
src – [输入] 发送数据缓冲区的指针
size – [输入] 最大数据缓冲区长度
bytes_written – [输出] 实际发送的字节数,如果不需要,可为空
timeout_ms – [输入] 最大阻塞时间

返回值:
ESP_OK 写入成功
ESP_ERR_INVALID_ARG 空指针或此句柄不是发送句柄
ESP_ERR_TIMEOUT 写入超时,在指定等待时间内未从中断服务程序接收到写入事件
ESP_ERR_INVALID_STATE I2S未准备好写入

注意:

仅允许在通道状态为“运行”时调用(即发送通道已启动且当前未在写入数据),但“运行”仅代表软件状态,并不意味着线路上没有信号传输。

I2S读出数据

esp_err_t i2s_channel_read(i2s_chan_handle_t handle, void *dest, size_t size, size_t *bytes_read, uint32_t timeout_ms)

参数:
handle – [输入] I2S通道句柄
dest – [输入] 接收数据缓冲区的指针
size – [输入] 最大数据缓冲区长度
bytes_read – [输出] 实际读取的字节数,若不需要可设为NULL
timeout_ms – [输入] 最大阻塞时间

返回值:
ESP_OK 读取成功
ESP_ERR_INVALID_ARG 空指针或此句柄不是接收句柄
ESP_ERR_TIMEOUT 读取超时,在指定等待时间内未从中断服务程序接收到读取事件
ESP_ERR_INVALID_STATE I2S未准备好读取

注意:

仅允许在通道状态为“运行”时调用,不过“运行”仅代表软件状态,并不意味着线路上没有信号传输。

设置I2S通道的事件回调函数

esp_err_t i2s_channel_register_event_callback(i2s_chan_handle_t handle, const i2s_event_callbacks_t *callbacks, void *user_data)

参数:
handle – [输入] I2S通道句柄
callbacks – [输入] 回调函数组
user_data – [输入] 用户数据,将直接传递给回调函数

返回值:
ESP_OK 成功设置事件回调
ESP_ERR_INVALID_ARG 因参数无效导致设置事件回调失败
ESP_ERR_INVALID_STATE 因当前通道状态不是“已注册”或“就绪”导致设置事件回调失败

注意:

  1. 仅允许在通道状态为“已注册”/“就绪”时调用(即在通道启动之前)。
  2. 用户可以通过调用此函数,并将callbacks结构体中的回调成员设置为NULL,来注销先前注册的回调函数。
  3. 当启用CONFIG_I2S_ISR_IRAM_SAFE时,回调函数本身及其调用的函数应放置在IRAM中。函数中使用的变量也应位于SRAM中。user_data也应位于SRAM或内部RAM中。

i2s_event_callbacks_t 结构体说明

typedef struct {
    i2s_isr_callback_t on_recv;             /**< 数据接收事件的回调,仅适用于接收(RX)通道。事件数据包括刚刚完成数据接收的DMA缓冲区地址和大小。*/                                             */
    i2s_isr_callback_t on_recv_q_ovf;       /**< 接收队列溢出事件的回调,仅适用于接收(RX)通道。事件数据包含已被覆盖的缓冲区大小。*/
    i2s_isr_callback_t on_sent;             /**< 数据发送事件的回调,仅适用于发送(TX)通道。事件数据包括刚刚完成数据发送的DMA缓冲区地址和大小。*/
    i2s_isr_callback_t on_send_q_ovf;       /**< 发送队列溢出事件的回调,仅适用于发送(TX)通道。事件数据包含已被覆盖的缓冲区大小。 */    
} i2s_event_callbacks_t;
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐