参考: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_fp
和 i2s_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 = 960
、fs = sample_rate / 100
,则 CLK 管脚上的时钟频率 (Fpdm) 将固定为128 * 48 KHz = 6.144 MHz
。注意此频率不等于采样率 (Fpcm)。 - 固定上采样率模式:在这种模式下,上采样率固定为 2。设置
fp = 960
、fs = 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_pdm_dsr_t::I2S_PDM_DSR_8S
:在这种模式下,WS 管脚的时钟频率 (Fpdm) 将为sample_rate (Fpcm) * 64
。i2s_pdm_dsr_t::I2S_PDM_DSR_16S
: 在这种模式下,WS 管脚的时钟频率 (Fpdm) 将为sample_rate (Fpcm) * 128
。
功能概览
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_EOF
或 I2S_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 模式的应用
不同声道的通信格式可通过以下标准模式的辅助宏来生成。如上所述,在标准模式下有三种格式,辅助宏分别为:
I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG
I2S_STD_PCM_SLOT_DEFAULT_CONFIG
I2S_STD_MSB_SLOT_DEFAULT_CONFIG
时钟配置的辅助宏为:
I2S_STD_CLK_DEFAULT_CONFIG
。
请参考 标准模式 了解 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 模式,声道配置的辅助宏为:
I2S_PDM_TX_SLOT_DEFAULT_CONFIG
时钟配置的辅助宏为:
I2S_PDM_TX_CLK_DEFAULT_CONFIG
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_mode
和 i2s_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 模式,声道配置的辅助宏为:
I2S_PDM_RX_SLOT_DEFAULT_CONFIG
时钟配置的辅助宏为:
I2S_PDM_RX_CLK_DEFAULT_CONFIG
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_mode
和 i2s_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));
请按照以下步骤操作,以防止数据丢失:
-
确定中断间隔。通常来说,当发生数据丢失时,为减少中断次数,中断间隔应该越久越好。因此,在保证 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
-
确定 dma_desc_num 的值。dma_desc_num 由 i2s_channel_read 轮询周期的最大时间决定,所有接收到的数据都应该存储在两个 i2s_channel_read 之间。这个周期可以通过计时器或输出 GPIO 信号来计算。具体转换关系如下:
dma_desc_num > polling_cycle / interrupt_interval
-
确定接收缓冲区大小。在
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_num
、 dma_desc_num
和 recv_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 此通道未初始化或未停止
注意:
- 仅允许在通道状态为“就绪”时调用,即通道已初始化但尚未启动,此函数不会改变通道状态。如果I2S已启动,调用此函数前应先调用i2s_channel_disable 。
- 输入的通道句柄必须已初始化为标准模式,即在重新配置之前必须已调用过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 此通道未初始化或未停止
注意
- 仅当通道状态为“就绪”时(即通道已初始化但尚未启动)才允许调用此函数,该函数不会改变通道状态。如果I2S已经启动,在调用此函数之前应先调用i2s_channel_disable 。
- 输入的通道句柄必须已初始化为标准模式,也就是说,在重新配置之前必须已经调用过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 此通道未初始化或未停止
注意:
- 仅当通道状态为“就绪”时(即通道已初始化但尚未启动)才允许调用此函数,该函数不会改变通道状态。如果I2S已经启动,在调用此函数之前应先调用i2s_channel_disable 。
- 输入的通道句柄必须已初始化为标准模式,也就是说,在重新配置之前必须已经调用过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通道
注意
- 新创建的I2S通道句柄在成功分配后将处于“已注册”状态。
- 当通道配置中的端口ID为I2S_NUM_AUTO时,驱动程序将在I2S控制器之一上自动分配I2S端口;否则,驱动程序将尝试在选定的端口上分配新通道。
- 如果tx_handle和rx_handle都不为空,这意味着此I2S控制器将工作在全双工模式,在这种情况下,接收(RX)和发送(TX)通道将被分配在同一个I2S端口上。请注意,在ESP32和ESP32S2上,TX/RX通道的一些配置是共享的,因此请确保它们在相同条件和相同状态(启动/停止)下工作。目前,全双工模式无法保证TX/RX通道同步进行写入/读取操作,它们目前仅能共享时钟信号。
- 如果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 空指针
注意
- 仅允许在I2S通道处于“已注册”或“就绪”状态时调用(即在删除前应先停止该通道)。
- 如果一个端口中的所有通道都被删除,资源将自动释放。
获取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 此通道尚未初始化或已启动
注意:
- 仅当通道状态为“就绪”(即通道已初始化但尚未启动)时才允许调用此函数。一旦成功启用,通道将进入“运行”状态。
- 启用通道可以在硬件上启动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 此通道尚未启动
注意:
- 仅当通道状态为“运行”(即通道已启动)时才允许调用此函数。一旦成功禁用,通道将进入“就绪”状态。
- 禁用该通道会停止硬件上的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 此通道尚未启动
注意:
- 仅允许在通道状态为“就绪”时调用(即通道已初始化,但尚未启动)。
- 由于初始的DMA缓冲区中没有数据,通道启用后它将传输空缓冲区,此函数用于将数据预加载到DMA缓冲区中,以便通道启用后能立即传输有效数据。
- 在启用通道前,此函数可多次调用,后加载的缓冲区数据会连接在前次加载的缓冲区数据之后。但当所有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 因当前通道状态不是“已注册”或“就绪”导致设置事件回调失败
注意:
- 仅允许在通道状态为“已注册”/“就绪”时调用(即在通道启动之前)。
- 用户可以通过调用此函数,并将callbacks结构体中的回调成员设置为NULL,来注销先前注册的回调函数。
- 当启用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;