Merge branch 'feat/i2s_edma' into 'master'

feat(i2s): supports DMA buffer in PSRAM

Closes IDF-15256, IDF-11059, and IDF-4471

See merge request espressif/esp-idf!51877
This commit is contained in:
morris
2026-09-03 16:14:03 +08:00
33 changed files with 757 additions and 162 deletions
@@ -5,8 +5,10 @@
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
@@ -17,6 +19,7 @@
#include "esp_private/gpio.h"
#include "esp_err.h"
#include "esp_attr.h"
#include "esp_memory_utils.h"
#include "unity.h"
#include "math.h"
#include "esp_rom_gpio.h"
@@ -45,12 +48,11 @@
#include "../../test_inc/test_i2s.h"
#if I2S_LL_GET(INST_NUM) > 1 && !CONFIG_ESP32P4_SELECTS_REV_LESS_V3
#define I2S_TEST_MODE_SLAVE_TO_MASTER 0
#define I2S_TEST_MODE_MASTER_TO_SLAVE 1
#define I2S_TEST_MODE_LOOPBACK 2
// mode: 0, master rx, slave tx. mode: 1, master tx, slave rx. mode: 2, master tx rx loop-back
// Since ESP32-S2 has only one I2S, only loop back test can be tested.
// mode: 0, master rx, slave tx. mode: 1, master tx, slave rx.
static void i2s_test_io_config(int mode)
{
// Connect internal signals using IO matrix.
@@ -88,18 +90,13 @@ static void i2s_test_io_config(int mode)
}
break;
#endif
case I2S_TEST_MODE_LOOPBACK: {
esp_rom_gpio_connect_out_signal(DATA_OUT_IO, i2s_periph_signal[0].data_out_sig, 0, 0);
esp_rom_gpio_connect_in_signal(DATA_OUT_IO, i2s_periph_signal[0].data_in_sig, 0);
}
break;
default: {
TEST_FAIL_MESSAGE("error: mode not supported");
}
break;
}
}
#endif
void i2s_read_write_test(i2s_chan_handle_t tx_chan, i2s_chan_handle_t rx_chan)
{
@@ -879,41 +876,270 @@ TEST_CASE("I2S_memory_leak_test", "[i2s]")
printf("\r\nHeap size after: %"PRIu32"\n", esp_get_free_heap_size());
}
TEST_CASE("I2S_loopback_test", "[i2s]")
#if SOC_PSRAM_DMA_CAPABLE && CONFIG_SPIRAM
static IRAM_ATTR bool i2s_record_external_ram_buf(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx)
{
*((volatile bool *)user_ctx) |= esp_ptr_external_ram(event->dma_buf);
return false;
}
#endif
/* Re-enable after disable checks that the circular DMA link restarts from its head. */
static void i2s_std_master_loopback_test(bool dma_buffer_in_psram)
{
i2s_chan_handle_t tx_handle;
i2s_chan_handle_t rx_handle;
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
chan_cfg.dma_buffer_in_psram = dma_buffer_in_psram;
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(SAMPLE_BITS, I2S_SLOT_MODE_STEREO),
.gpio_cfg = I2S_TEST_MASTER_DEFAULT_PIN,
};
std_cfg.gpio_cfg.din = std_cfg.gpio_cfg.dout;
#if SOC_PSRAM_DMA_CAPABLE && CONFIG_SPIRAM
volatile bool tx_in_ext = false;
volatile bool rx_in_ext = false;
#endif
TEST_ESP_OK(i2s_new_channel(&chan_cfg, &tx_handle, &rx_handle));
TEST_ESP_OK(i2s_channel_init_std_mode(tx_handle, &std_cfg));
TEST_ESP_OK(i2s_channel_init_std_mode(rx_handle, &std_cfg));
i2s_test_io_config(I2S_TEST_MODE_LOOPBACK);
#if SOC_PSRAM_DMA_CAPABLE && CONFIG_SPIRAM
if (dma_buffer_in_psram) {
i2s_event_callbacks_t tx_cbs = {
.on_sent = i2s_record_external_ram_buf,
};
i2s_event_callbacks_t rx_cbs = {
.on_recv = i2s_record_external_ram_buf,
};
TEST_ESP_OK(i2s_channel_register_event_callback(tx_handle, &tx_cbs, (void *)&tx_in_ext));
TEST_ESP_OK(i2s_channel_register_event_callback(rx_handle, &rx_cbs, (void *)&rx_in_ext));
}
#endif
TEST_ESP_OK(i2s_channel_enable(tx_handle));
TEST_ESP_OK(i2s_channel_enable(rx_handle));
i2s_read_write_test(tx_handle, rx_handle);
TEST_ESP_OK(i2s_channel_disable(tx_handle));
TEST_ESP_OK(i2s_channel_disable(rx_handle));
/* Verify the circular DMA link restarts from its head after both channels are re-enabled. */
TEST_ESP_OK(i2s_channel_enable(tx_handle));
TEST_ESP_OK(i2s_channel_enable(rx_handle));
i2s_read_write_test(tx_handle, rx_handle);
TEST_ESP_OK(i2s_channel_disable(tx_handle));
TEST_ESP_OK(i2s_channel_disable(rx_handle));
TEST_ESP_OK(i2s_channel_enable(tx_handle));
TEST_ESP_OK(i2s_channel_enable(rx_handle));
i2s_read_write_test(tx_handle, rx_handle);
TEST_ESP_OK(i2s_channel_disable(tx_handle));
TEST_ESP_OK(i2s_channel_disable(rx_handle));
#if SOC_PSRAM_DMA_CAPABLE && CONFIG_SPIRAM
if (dma_buffer_in_psram) {
TEST_ASSERT_TRUE(tx_in_ext);
TEST_ASSERT_TRUE(rx_in_ext);
}
#endif
TEST_ESP_OK(i2s_del_channel(tx_handle));
TEST_ESP_OK(i2s_del_channel(rx_handle));
}
TEST_CASE("I2S_loopback_test", "[i2s]")
{
i2s_std_master_loopback_test(false);
#if SOC_PSRAM_DMA_CAPABLE && CONFIG_SPIRAM
i2s_std_master_loopback_test(true);
#endif
}
#if !(SOC_PSRAM_DMA_CAPABLE && CONFIG_SPIRAM)
TEST_CASE("I2S_psram_dma_buffer_rejected_when_unavailable", "[i2s]")
{
i2s_chan_handle_t tx_handle = NULL;
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
chan_cfg.dma_buffer_in_psram = true;
TEST_ESP_ERR(ESP_ERR_NOT_SUPPORTED, i2s_new_channel(&chan_cfg, &tx_handle, NULL));
}
#endif
#if CONFIG_I2S_ISR_IRAM_SAFE && SOC_PSRAM_DMA_CAPABLE && CONFIG_SPIRAM
TEST_CASE("I2S_psram_auto_clear_rejected_with_iram_safe", "[i2s]")
{
i2s_chan_handle_t tx_handle = NULL;
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
chan_cfg.dma_buffer_in_psram = true;
chan_cfg.auto_clear_before_cb = true;
chan_cfg.auto_clear_after_cb = false;
TEST_ESP_ERR(ESP_ERR_NOT_SUPPORTED, i2s_new_channel(&chan_cfg, &tx_handle, NULL));
chan_cfg.auto_clear_before_cb = false;
chan_cfg.auto_clear_after_cb = true;
TEST_ESP_ERR(ESP_ERR_NOT_SUPPORTED, i2s_new_channel(&chan_cfg, &tx_handle, NULL));
}
#endif
#if SOC_I2S_SUPPORTS_TDM
#define I2S_TDM_INTEGRITY_MAGIC 0xA55A5AA5UL
#define I2S_TDM_INTEGRITY_DESC_NUM 6
#define I2S_TDM_INTEGRITY_FRAME_NUM 32
#define I2S_TDM_INTEGRITY_VERIFY_FRAMES 1024
#define I2S_TDM_INTEGRITY_PREAMBLE_FRAMES 200
#define I2S_TDM_INTEGRITY_SEND_FRAMES (I2S_TDM_INTEGRITY_PREAMBLE_FRAMES + I2S_TDM_INTEGRITY_VERIFY_FRAMES)
typedef struct {
uint32_t magic;
uint32_t seq;
uint32_t seq_inv;
uint32_t checksum;
} i2s_tdm_integrity_frame_t;
_Static_assert(sizeof(i2s_tdm_integrity_frame_t) == 16, "TDM integrity frame must be 16 bytes");
static uint32_t i2s_tdm_integrity_checksum(uint32_t magic, uint32_t seq, uint32_t seq_inv)
{
/* Mix seq before combining with seq_inv so seq ^ ~seq cannot collapse to a constant. */
uint32_t c = magic ^ (seq * 0x9E3779B1UL);
c = (c << 7) | (c >> 25);
return c ^ seq_inv ^ 0x13579BDFUL;
}
static void i2s_tdm_integrity_fill_frame(i2s_tdm_integrity_frame_t *frame, uint32_t seq)
{
frame->magic = I2S_TDM_INTEGRITY_MAGIC;
frame->seq = seq;
frame->seq_inv = ~seq;
frame->checksum = i2s_tdm_integrity_checksum(frame->magic, frame->seq, frame->seq_inv);
}
static bool i2s_tdm_integrity_frame_valid(const i2s_tdm_integrity_frame_t *frame)
{
return frame->magic == I2S_TDM_INTEGRITY_MAGIC &&
frame->seq_inv == ~frame->seq &&
frame->checksum == i2s_tdm_integrity_checksum(frame->magic, frame->seq, frame->seq_inv);
}
typedef struct {
i2s_chan_handle_t tx_handle;
const uint8_t *buf;
size_t len;
volatile esp_err_t ret;
volatile bool done;
} i2s_tdm_integrity_writer_ctx_t;
static void i2s_tdm_integrity_writer_task(void *args)
{
i2s_tdm_integrity_writer_ctx_t *ctx = (i2s_tdm_integrity_writer_ctx_t *)args;
size_t bytes_written = 0;
ctx->ret = i2s_channel_write(ctx->tx_handle, ctx->buf, ctx->len, &bytes_written, 2000);
if (ctx->ret == ESP_OK && bytes_written != ctx->len) {
ctx->ret = ESP_ERR_INVALID_SIZE;
}
ctx->done = true;
vTaskDelete(NULL);
}
static void i2s_tdm_integrity_loopback_test(bool dma_buffer_in_psram)
{
i2s_chan_handle_t tx_handle = NULL;
i2s_chan_handle_t rx_handle = NULL;
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
chan_cfg.dma_desc_num = I2S_TDM_INTEGRITY_DESC_NUM;
chan_cfg.dma_frame_num = I2S_TDM_INTEGRITY_FRAME_NUM;
chan_cfg.dma_buffer_in_psram = dma_buffer_in_psram;
i2s_tdm_config_t tdm_cfg = {
.clk_cfg = I2S_TDM_CLK_DEFAULT_CONFIG(SAMPLE_RATE),
.slot_cfg = I2S_TDM_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_STEREO, 0x0F),
.gpio_cfg = I2S_TEST_MASTER_DEFAULT_PIN,
};
tdm_cfg.gpio_cfg.din = tdm_cfg.gpio_cfg.dout;
/* Full-duplex RX is forced to slave; default mclk/bclk=3 is below the driver's measured minimum of 4. */
tdm_cfg.clk_cfg.mclk_multiple = I2S_MCLK_MULTIPLE_512;
#if CONFIG_IDF_TARGET_ESP32S31
// S31 uses XTAL as default clock source, which cannot support high sample rate in this test
tdm_cfg.clk_cfg.clk_src = I2S_CLK_SRC_APLL;
#endif
const size_t send_bytes = I2S_TDM_INTEGRITY_SEND_FRAMES * sizeof(i2s_tdm_integrity_frame_t);
/* Read less than we write so TX is still feeding the bus while RX finishes. */
const size_t recv_frame_budget = I2S_TDM_INTEGRITY_PREAMBLE_FRAMES + I2S_TDM_INTEGRITY_VERIFY_FRAMES;
const size_t recv_bytes = recv_frame_budget * sizeof(i2s_tdm_integrity_frame_t);
i2s_tdm_integrity_frame_t *send_frames = calloc(I2S_TDM_INTEGRITY_SEND_FRAMES, sizeof(i2s_tdm_integrity_frame_t));
uint8_t *recv_buf = calloc(1, recv_bytes);
TEST_ASSERT_NOT_NULL(send_frames);
TEST_ASSERT_NOT_NULL(recv_buf);
for (uint32_t i = 0; i < I2S_TDM_INTEGRITY_SEND_FRAMES; i++) {
i2s_tdm_integrity_fill_frame(&send_frames[i], i);
}
TEST_ESP_OK(i2s_new_channel(&chan_cfg, &tx_handle, &rx_handle));
TEST_ESP_OK(i2s_channel_init_tdm_mode(tx_handle, &tdm_cfg));
TEST_ESP_OK(i2s_channel_init_tdm_mode(rx_handle, &tdm_cfg));
TEST_ESP_OK(i2s_channel_enable(tx_handle));
TEST_ESP_OK(i2s_channel_enable(rx_handle));
i2s_tdm_integrity_writer_ctx_t writer_ctx = {
.tx_handle = tx_handle,
.buf = (const uint8_t *)send_frames,
.len = send_bytes,
.ret = ESP_FAIL,
.done = false,
};
TEST_ASSERT_EQUAL(pdPASS, xTaskCreate(i2s_tdm_integrity_writer_task, "i2s_tdm_wr", 4096, &writer_ctx, 5, NULL));
size_t bytes_read = 0;
TEST_ESP_OK(i2s_channel_read(rx_handle, recv_buf, recv_bytes, &bytes_read, 2000));
TEST_ASSERT_EQUAL(recv_bytes, bytes_read);
while (!writer_ctx.done) {
vTaskDelay(pdMS_TO_TICKS(1));
}
/* Writer already called vTaskDelete; yield so idle can reclaim its TCB/stack. */
vTaskDelay(1);
TEST_ESP_OK(writer_ctx.ret);
TEST_ESP_OK(i2s_channel_disable(tx_handle));
TEST_ESP_OK(i2s_channel_disable(rx_handle));
TEST_ESP_OK(i2s_del_channel(tx_handle));
TEST_ESP_OK(i2s_del_channel(rx_handle));
const i2s_tdm_integrity_frame_t *recv_frames = (const i2s_tdm_integrity_frame_t *)recv_buf;
const size_t recv_frame_count = bytes_read / sizeof(i2s_tdm_integrity_frame_t);
bool synced = false;
uint32_t expected_seq = 0;
uint32_t verified = 0;
for (size_t i = 0; i + 1 < recv_frame_count && verified < I2S_TDM_INTEGRITY_VERIFY_FRAMES; i++) {
if (!synced) {
if (i2s_tdm_integrity_frame_valid(&recv_frames[i])) {
expected_seq = recv_frames[i].seq;
synced = true;
} else { // skip non-synced preamble frames
continue;
}
}
TEST_ASSERT_TRUE_MESSAGE(i2s_tdm_integrity_frame_valid(&recv_frames[i]),
"corrupted TDM integrity frame after sync");
TEST_ASSERT_EQUAL_UINT32(expected_seq, recv_frames[i].seq);
expected_seq++;
verified++;
}
TEST_ASSERT_TRUE_MESSAGE(synced, "failed to find TDM integrity sync frame\n");
TEST_ASSERT_EQUAL_UINT32(I2S_TDM_INTEGRITY_VERIFY_FRAMES, verified);
free(send_frames);
free(recv_buf);
}
TEST_CASE("I2S_tdm_integrity_loopback_test", "[i2s]")
{
i2s_tdm_integrity_loopback_test(false);
#if SOC_PSRAM_DMA_CAPABLE && CONFIG_SPIRAM
i2s_tdm_integrity_loopback_test(true);
#endif
}
#endif // SOC_I2S_SUPPORTS_TDM
#if I2S_LL_GET(INST_NUM) > 1 && !CONFIG_ESP32P4_SELECTS_REV_LESS_V3
TEST_CASE("I2S_master_write_slave_read_test", "[i2s]")
{
@@ -0,0 +1,2 @@
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=0
@@ -0,0 +1,2 @@
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=0
@@ -0,0 +1,2 @@
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=0
@@ -0,0 +1,2 @@
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=0
@@ -0,0 +1,2 @@
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=0