From 7728baf0524fefdf0c592820b6fc210b12be452a Mon Sep 17 00:00:00 2001 From: Chen Chen Date: Mon, 15 Jun 2026 17:01:24 +0800 Subject: [PATCH] feat(i2s): allow full duplex mode in less strict condition --- components/esp_driver_i2s/i2s_common.c | 121 ++++++++++++++++++ components/esp_driver_i2s/i2s_private.h | 32 +++++ components/esp_driver_i2s/i2s_std.c | 101 ++++++++------- components/esp_driver_i2s/i2s_tdm.c | 54 ++++---- .../esp_driver_i2s/include/driver/i2s_std.h | 9 +- .../esp_driver_i2s/include/driver/i2s_tdm.h | 9 +- .../test_apps/i2s/main/test_i2s.c | 84 ++++++++++++ docs/en/api-reference/peripherals/i2s.rst | 15 ++- docs/zh_CN/api-reference/peripherals/i2s.rst | 16 ++- 9 files changed, 351 insertions(+), 90 deletions(-) diff --git a/components/esp_driver_i2s/i2s_common.c b/components/esp_driver_i2s/i2s_common.c index 65ba40552ae..7b08089e85e 100644 --- a/components/esp_driver_i2s/i2s_common.c +++ b/components/esp_driver_i2s/i2s_common.c @@ -48,6 +48,8 @@ #include "driver/gpio.h" #include "esp_private/gpio.h" #include "driver/i2s_common.h" +#include "driver/i2s_std.h" +#include "driver/i2s_tdm.h" #include "i2s_private.h" #if CONFIG_IDF_TARGET_ESP32 @@ -82,6 +84,125 @@ inline void *i2s_dma_calloc(i2s_chan_handle_t handle, size_t num, size_t size) return heap_caps_aligned_calloc(4, num, size, I2S_DMA_ALLOC_CAPS); } +/*--------------------------------------------------------------------------- + Duplex Constitution Helpers + ---------------------------------------------------------------------------- + Scope: This file only + ----------------------------------------------------------------------------*/ + +/* Extract the duplex candidate from a fully-initialized channel's stored configuration. + * @note Only valid for a channel that has reached the READY state, because the slot/clock/gpio + * information is read back from `handle->mode_info`. */ +static esp_err_t s_i2s_extract_duplex_candidate(i2s_chan_handle_t handle, i2s_duplex_candidate_t *out) +{ + /* Only STD and TDM modes can constitute duplex, reject the others (e.g. PDM) up front */ +#if SOC_I2S_SUPPORTS_TDM + if (handle->mode != I2S_COMM_MODE_STD && handle->mode != I2S_COMM_MODE_TDM) { +#else + if (handle->mode != I2S_COMM_MODE_STD) { +#endif + return ESP_ERR_NOT_SUPPORTED; + } + + memset(out, 0, sizeof(*out)); + + /* The clk_cfg and gpio_cfg sub-structures share the same layout between STD and TDM modes + * (only the slot_cfg differs), so resolve the mode-specific pointers here and fill the + * candidate with the shared logic below. */ + i2s_std_clk_config_t *clk_cfg = NULL; + i2s_std_gpio_config_t *gpio_cfg = NULL; + uint32_t slot_bits = 0; + uint32_t data_bits = 0; + if (handle->mode == I2S_COMM_MODE_STD) { + i2s_std_config_t *cfg = (i2s_std_config_t *)handle->mode_info; + clk_cfg = &cfg->clk_cfg; + gpio_cfg = &cfg->gpio_cfg; + slot_bits = cfg->slot_cfg.slot_bit_width; + data_bits = cfg->slot_cfg.data_bit_width; + } +#if SOC_I2S_SUPPORTS_TDM + else { + i2s_tdm_config_t *cfg = (i2s_tdm_config_t *)handle->mode_info; + clk_cfg = (i2s_std_clk_config_t *)&cfg->clk_cfg; + gpio_cfg = (i2s_std_gpio_config_t *)&cfg->gpio_cfg; + slot_bits = cfg->slot_cfg.slot_bit_width; + data_bits = cfg->slot_cfg.data_bit_width; + } +#endif + + if (slot_bits == I2S_SLOT_BIT_WIDTH_AUTO || (int)slot_bits < (int)data_bits) { + slot_bits = data_bits; + } + + out->ws_pin = gpio_cfg->ws; + out->bclk_pin = gpio_cfg->bclk; + out->total_frame_bits = handle->total_slot * slot_bits; + out->sample_rate_hz = clk_cfg->sample_rate_hz; + out->clk_src = clk_cfg->clk_src; + out->bclk_inv = gpio_cfg->invert_flags.bclk_inv; + out->ws_inv = gpio_cfg->invert_flags.ws_inv; + return ESP_OK; +} + +/* Whether the shared BCLK/WS configurations of the two candidates are identical. + * The slot layout and clock source are intentionally NOT compared here, only the + * resulting frame timing (sample rate + total frame bits) and the shared BCLK/WS + * settings are. */ +static bool s_i2s_duplex_candidate_match(const i2s_duplex_candidate_t *a, const i2s_duplex_candidate_t *b) +{ + return a->ws_pin == b->ws_pin && + a->bclk_pin == b->bclk_pin && + a->sample_rate_hz == b->sample_rate_hz && + a->total_frame_bits == b->total_frame_bits && + a->bclk_inv == b->bclk_inv && + a->ws_inv == b->ws_inv; +} + +void i2s_channel_try_to_constitute_duplex(i2s_chan_handle_t handle, const i2s_duplex_candidate_t *candidate) +{ + i2s_chan_handle_t another_handle = handle->dir == I2S_DIR_RX ? handle->controller->tx_chan : handle->controller->rx_chan; + + /* The other direction channel must be registered and fully initialized to be compared with */ + if (!another_handle || another_handle->state < I2S_CHAN_STATE_READY) { + return; + } + + if (!handle->controller->full_duplex) { + /* Sharing BCLK/WS requires valid (used) WS and BCK pins on the current channel */ + if (candidate->ws_pin < 0 || candidate->bclk_pin < 0) { + ESP_LOGD(TAG, "%s channel on I2S%d: WS/BCK pins unused, cannot constitute duplex", + handle->dir == I2S_DIR_TX ? "tx" : "rx", handle->controller->id); + return; + } + + i2s_duplex_candidate_t another_candidate; + if (s_i2s_extract_duplex_candidate(another_handle, &another_candidate) != ESP_OK) { + return; + } + + /* Constitute duplex only when the shared BCLK/WS configurations are identical and the frame + * timing (sample rate + total frame bits) matches. The slot layout and clock config may differ. */ + if (!s_i2s_duplex_candidate_match(candidate, &another_candidate)) { + ESP_LOGD(TAG, "%s channel on I2S%d: BCLK/WS/frame configurations don't match, cannot constitute duplex", + handle->dir == I2S_DIR_TX ? "tx" : "rx", handle->controller->id); + return; + } + + /* All conditions met, constitute duplex */ + handle->controller->full_duplex = true; + ESP_LOGD(TAG, "Constitute full-duplex on port %d", handle->controller->id); + } + + /* Handle slave role fallback when both channels are master. + * The later initialized channel must be slave for full duplex */ + if (handle->controller->full_duplex && + handle->role == I2S_ROLE_MASTER && + another_handle->role == I2S_ROLE_MASTER) { + handle->role = I2S_ROLE_SLAVE; + handle->full_duplex_slave = true; + } +} + /*--------------------------------------------------------------------------- I2S Static APIs ---------------------------------------------------------------------------- diff --git a/components/esp_driver_i2s/i2s_private.h b/components/esp_driver_i2s/i2s_private.h index df28ca15bf5..5a7f43f240d 100644 --- a/components/esp_driver_i2s/i2s_private.h +++ b/components/esp_driver_i2s/i2s_private.h @@ -126,6 +126,38 @@ typedef struct { uint8_t **bufs; /*!< dma buffer array */ } i2s_dma_t; +/** + * @brief Duplex candidate information extracted from channel configuration + * @note Used by the generic duplex constitution function to check whether two channels can form duplex. + * Two channels can share the BCLK/WS lines only when the BCLK/WS configurations are identical + * and the per-frame bit count (sample_rate * total_frame_bits) is the same. The slot layout, + * clock source, external clock frequency and MCLK configuration are allowed to differ as long + * as the BCLK/WS frequency and frame timing match. + */ +typedef struct { + int ws_pin; /*!< WS GPIO pin number, -1 if unused */ + int bclk_pin; /*!< BCLK GPIO pin number, -1 if unused */ + uint32_t sample_rate_hz; /*!< Sample rate in Hz */ + uint32_t total_frame_bits; /*!< Total bits per frame: total_slot * slot_bit_width */ + i2s_clock_src_t clk_src; /*!< Clock source (informational, not compared) */ + bool bclk_inv; /*!< Whether the BCLK is inverted */ + bool ws_inv; /*!< Whether the WS is inverted */ +} i2s_duplex_candidate_t; + +/** + * @brief Try to constitute full-duplex between two channels on the same controller + * @note The two channels can constitute full-duplex only when they share the same valid WS/BCK pins, + * the same BCLK/WS invert flags, the same sample rate and the same total frame bits. + * The slot layout, clock source, external clock frequency and MCLK configuration may differ + * as long as the BCLK/WS frequency and frame timing match (e.g. STD 2ch/32bit can pair + * with TDM 4ch/16bit). + * If the conditions match, mark the controller as full-duplex. If both channels are master, + * the later one is forced to slave. + * @param handle Current channel being initialized + * @param candidate Duplex configuration of the current channel + */ +void i2s_channel_try_to_constitute_duplex(i2s_chan_handle_t handle, const i2s_duplex_candidate_t *candidate); + /** * @brief i2s controller level configurations * @note Both i2s rx and tx channel are under its control diff --git a/components/esp_driver_i2s/i2s_std.c b/components/esp_driver_i2s/i2s_std.c index 275ced22680..47e4492b998 100644 --- a/components/esp_driver_i2s/i2s_std.c +++ b/components/esp_driver_i2s/i2s_std.c @@ -43,6 +43,16 @@ static esp_err_t i2s_std_calculate_clock(i2s_chan_handle_t handle, const i2s_std if (clk_info->mclk % clk_info->bclk != 0) { ESP_LOGW(TAG, "the current mclk multiple cannot perform integer division (slot_num: %"PRIu32", slot_bits: %"PRIu32")", handle->total_slot, slot_bits); } + /* As an internal full-duplex slave, the BCLK/WS are looped back from the on-chip master, but the slave logic + * still needs enough MCLK/BCLK ratio to sample correctly. Measured minimums: TX slave >= 6, RX slave >= 4. */ + if (handle->full_duplex_slave) { + uint32_t min_bclk_div = handle->dir == I2S_DIR_TX ? 6 : 4; + if (clk_info->bclk_div < min_bclk_div) { + ESP_LOGW(TAG, "the mclk/bclk ratio %"PRIu32" is too small for the full-duplex %s slave (min %"PRIu32"), " + "data might be sampled incorrectly, please increase mclk_multiple", + clk_info->bclk_div, handle->dir == I2S_DIR_TX ? "tx" : "rx", min_bclk_div); + } + } } else { /* For slave mode, mclk >= bclk * 8, so fix bclk_div to 2 first */ clk_info->bclk_div = 8; @@ -237,61 +247,54 @@ static esp_err_t i2s_std_set_gpio(i2s_chan_handle_t handle, const i2s_std_gpio_c return ESP_OK; } -static esp_err_t s_i2s_channel_try_to_constitude_std_duplex(i2s_chan_handle_t handle, const i2s_std_config_t *std_cfg) +static esp_err_t s_i2s_channel_try_to_constitute_std_duplex(i2s_chan_handle_t handle, const i2s_std_config_t *std_cfg) { - /* Get another direction handle */ - i2s_chan_handle_t another_handle = handle->dir == I2S_DIR_RX ? handle->controller->tx_chan : handle->controller->rx_chan; - /* Condition: 1. Another direction channel is registered - * 2. Not a full-duplex channel yet - * 3. Another channel is initialized, try to compare the configurations */ - if (another_handle && another_handle->state >= I2S_CHAN_STATE_READY) { - /* Judge if the two channels can constitute full-duplex */ - if (!handle->controller->full_duplex) { - i2s_std_config_t curr_cfg = *std_cfg; - i2s_std_slot_config_t norm_slot_cfg = s_i2s_std_normalize_slot_config(&(std_cfg->slot_cfg)); - memcpy(&curr_cfg.slot_cfg, &norm_slot_cfg, sizeof(i2s_std_slot_config_t)); - /* Compare the hardware configurations of the two channels, constitute the full-duplex if they are the same */ - if (memcmp(another_handle->mode_info, &curr_cfg, sizeof(i2s_std_config_t)) == 0) { - handle->controller->full_duplex = true; - ESP_LOGD(TAG, "Constitude full-duplex on port %d", handle->controller->id); - } else { + /* Build the duplex candidate from the current channel's configuration */ + i2s_duplex_candidate_t candidate = {0}; + candidate.ws_pin = std_cfg->gpio_cfg.ws; + candidate.bclk_pin = std_cfg->gpio_cfg.bclk; + candidate.sample_rate_hz = std_cfg->clk_cfg.sample_rate_hz; + candidate.clk_src = std_cfg->clk_cfg.clk_src; + candidate.bclk_inv = std_cfg->gpio_cfg.invert_flags.bclk_inv; + candidate.ws_inv = std_cfg->gpio_cfg.invert_flags.ws_inv; + /* Compute total frame bits for standard mode: always 2 slots */ + i2s_std_slot_config_t norm_slot = s_i2s_std_normalize_slot_config(&std_cfg->slot_cfg); + candidate.total_frame_bits = 2 * norm_slot.slot_bit_width; + + i2s_channel_try_to_constitute_duplex(handle, &candidate); + /* On HW v1, if duplex constitution fails (pins/configs don't match), + * try to move the channel to another port to avoid sharing a port with incompatible configs. + * Only do this when the other channel was actually ready for comparison; if it wasn't + * initialized yet, duplex will be constituted when the second channel is initialized. */ #if SOC_I2S_HW_VERSION_1 - bool port_changed = false; - if (handle->is_port_auto) { - ESP_LOGD(TAG, "TX & RX on I2S%d are simplex", handle->controller->id); - for (int i = 0; i < SOC_I2S_NUM; i++) { - if (i == handle->controller->id) { - continue; - } - ESP_LOGD(TAG, "Trying to move %s channel from port %d to %d", - handle->dir == I2S_DIR_TX ? "TX" : "RX", handle->controller->id, i); - if (i2s_channel_change_port(handle, i) == ESP_OK) { - ESP_LOGD(TAG, "Move success!"); - port_changed = true; - break; - } else { - ESP_LOGD(TAG, "Move failed..."); - } + if (!handle->controller->full_duplex) { + i2s_chan_handle_t another = handle->dir == I2S_DIR_RX ? handle->controller->tx_chan : handle->controller->rx_chan; + if (another && another->state >= I2S_CHAN_STATE_READY) { + bool port_changed = false; + if (handle->is_port_auto) { + ESP_LOGD(TAG, "TX & RX on I2S%d are simplex", handle->controller->id); + for (int i = 0; i < SOC_I2S_NUM; i++) { + if (i == handle->controller->id) { + continue; + } + ESP_LOGD(TAG, "Trying to move %s channel from port %d to %d", + handle->dir == I2S_DIR_TX ? "TX" : "RX", handle->controller->id, i); + if (i2s_channel_change_port(handle, i) == ESP_OK) { + ESP_LOGD(TAG, "Move success!"); + port_changed = true; + break; + } else { + ESP_LOGD(TAG, "Move failed..."); } } - if (!port_changed) { - ESP_LOGE(TAG, "Can't set different channel configurations on a same port"); - return ESP_ERR_INVALID_ARG; - } -#else - ESP_LOGD(TAG, "TX & RX on I2S%d are simplex", handle->controller->id); -#endif + } + if (!port_changed) { + ESP_LOGE(TAG, "Can't set different channel configurations on a same port"); + return ESP_ERR_INVALID_ARG; } } - /* Switch to the slave role if needed */ - if (handle->controller->full_duplex && - handle->role == I2S_ROLE_MASTER && - another_handle->role == I2S_ROLE_MASTER) { - /* The later initialized channel must be slave for full duplex */ - handle->role = I2S_ROLE_SLAVE; - handle->full_duplex_slave = true; - } } +#endif return ESP_OK; } @@ -314,7 +317,7 @@ esp_err_t i2s_channel_init_std_mode(i2s_chan_handle_t handle, const i2s_std_conf ESP_GOTO_ON_FALSE(handle->mode_info, ESP_ERR_NO_MEM, err, TAG, "no memory for storing the configurations"); ESP_GOTO_ON_FALSE(handle->state == I2S_CHAN_STATE_REGISTER, ESP_ERR_INVALID_STATE, err, TAG, "the channel has initialized already"); /* Try to constitute full-duplex mode if the STD configuration is totally same as another channel */ - ret = s_i2s_channel_try_to_constitude_std_duplex(handle, std_cfg); + ret = s_i2s_channel_try_to_constitute_std_duplex(handle, std_cfg); #if SOC_I2S_HW_VERSION_1 ESP_GOTO_ON_ERROR(ret, err, TAG, "Failed to constitute full-duplex mode"); #endif diff --git a/components/esp_driver_i2s/i2s_tdm.c b/components/esp_driver_i2s/i2s_tdm.c index a6d081444e6..0dd59c6369a 100644 --- a/components/esp_driver_i2s/i2s_tdm.c +++ b/components/esp_driver_i2s/i2s_tdm.c @@ -50,6 +50,16 @@ static esp_err_t i2s_tdm_calculate_clock(i2s_chan_handle_t handle, const i2s_tdm if (clk_info->mclk % clk_info->bclk != 0) { ESP_LOGW(TAG, "the current mclk multiple cannot perform integer division (slot_num: %"PRIu32", slot_bits: %"PRIu32")", handle->total_slot, slot_bits); } + /* As an internal full-duplex slave, the BCLK/WS are looped back from the on-chip master, but the slave logic + * still needs enough MCLK/BCLK ratio to sample correctly. Measured minimums: TX slave >= 6, RX slave >= 4. */ + if (handle->full_duplex_slave) { + uint32_t min_bclk_div = handle->dir == I2S_DIR_TX ? 6 : 4; + if (clk_info->bclk_div < min_bclk_div) { + ESP_LOGW(TAG, "the mclk/bclk ratio %"PRIu32" is too small for the full-duplex %s slave (min %"PRIu32"), " + "data might be sampled incorrectly, please increase mclk_multiple", + clk_info->bclk_div, handle->dir == I2S_DIR_TX ? "tx" : "rx", min_bclk_div); + } + } } else { if (clk_cfg->bclk_div < 8) { ESP_LOGW(TAG, "the current bclk division is too small, adjust the bclk division to 8"); @@ -254,35 +264,21 @@ static esp_err_t i2s_tdm_set_gpio(i2s_chan_handle_t handle, const i2s_tdm_gpio_c return ESP_OK; } -static void s_i2s_channel_try_to_constitude_tdm_duplex(i2s_chan_handle_t handle, const i2s_tdm_config_t *tdm_cfg) +static void s_i2s_channel_try_to_constitute_tdm_duplex(i2s_chan_handle_t handle, const i2s_tdm_config_t *tdm_cfg) { - /* Get another direction handle */ - i2s_chan_handle_t another_handle = handle->dir == I2S_DIR_RX ? handle->controller->tx_chan : handle->controller->rx_chan; - /* Condition: 1. Another direction channel is registered - * 2. Not a full-duplex channel yet - * 3. Another channel is initialized, try to compare the configurations */ - if (another_handle && another_handle->state >= I2S_CHAN_STATE_READY) { - if (!handle->controller->full_duplex) { - i2s_tdm_config_t curr_cfg = *tdm_cfg; - i2s_tdm_slot_config_t norm_slot_cfg = s_i2s_tdm_normalize_slot_config(&(tdm_cfg->slot_cfg)); - memcpy(&curr_cfg.slot_cfg, &norm_slot_cfg, sizeof(i2s_tdm_slot_config_t)); - /* Compare the hardware configurations of the two channels, constitute the full-duplex if they are the same */ - if (memcmp(another_handle->mode_info, &curr_cfg, sizeof(i2s_tdm_config_t)) == 0) { - handle->controller->full_duplex = true; - ESP_LOGD(TAG, "Constitude full-duplex on port %d", handle->controller->id); - } else { - ESP_LOGD(TAG, "TX & RX on I2S%d are simplex", handle->controller->id); - } - } - /* Switch to the slave role if needed */ - if (handle->controller->full_duplex && - handle->role == I2S_ROLE_MASTER && - another_handle->role == I2S_ROLE_MASTER) { - /* The later initialized channel must be slave for full duplex */ - handle->role = I2S_ROLE_SLAVE; - handle->full_duplex_slave = true; - } - } + /* Build the duplex candidate from the current channel's configuration */ + i2s_duplex_candidate_t candidate = {0}; + candidate.ws_pin = tdm_cfg->gpio_cfg.ws; + candidate.bclk_pin = tdm_cfg->gpio_cfg.bclk; + candidate.sample_rate_hz = tdm_cfg->clk_cfg.sample_rate_hz; + candidate.clk_src = tdm_cfg->clk_cfg.clk_src; + candidate.bclk_inv = tdm_cfg->gpio_cfg.invert_flags.bclk_inv; + candidate.ws_inv = tdm_cfg->gpio_cfg.invert_flags.ws_inv; + /* Compute total frame bits for TDM mode */ + i2s_tdm_slot_config_t norm_slot = s_i2s_tdm_normalize_slot_config(&tdm_cfg->slot_cfg); + candidate.total_frame_bits = norm_slot.total_slot * norm_slot.slot_bit_width; + + i2s_channel_try_to_constitute_duplex(handle, &candidate); } esp_err_t i2s_channel_init_tdm_mode(i2s_chan_handle_t handle, const i2s_tdm_config_t *tdm_cfg) @@ -303,7 +299,7 @@ esp_err_t i2s_channel_init_tdm_mode(i2s_chan_handle_t handle, const i2s_tdm_conf handle->mode_info = calloc(1, sizeof(i2s_tdm_config_t)); ESP_GOTO_ON_FALSE(handle->mode_info, ESP_ERR_NO_MEM, err, TAG, "no memory for storing the configurations"); /* Try to constitute full-duplex mode if the TDM configuration is totally same as another channel */ - s_i2s_channel_try_to_constitude_tdm_duplex(handle, tdm_cfg); + s_i2s_channel_try_to_constitute_tdm_duplex(handle, tdm_cfg); /* i2s_set_tdm_slot should be called before i2s_set_tdm_clock while initializing, because clock is relay on the slot */ ESP_GOTO_ON_ERROR(i2s_tdm_set_slot(handle, &tdm_cfg->slot_cfg), err, TAG, "initialize channel failed while setting slot"); #if SOC_I2S_SUPPORTS_APLL diff --git a/components/esp_driver_i2s/include/driver/i2s_std.h b/components/esp_driver_i2s/include/driver/i2s_std.h index b039c9ce571..233a0d29627 100644 --- a/components/esp_driver_i2s/include/driver/i2s_std.h +++ b/components/esp_driver_i2s/include/driver/i2s_std.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -296,8 +296,11 @@ typedef struct { * @brief Initialize I2S channel to standard mode * @note Only allowed to be called when the channel state is REGISTERED, (i.e., channel has been allocated, but not initialized) * and the state will be updated to READY if initialization success, otherwise the state will return to REGISTERED. - * @note When initialize the STD mode with a same configuration as another channel on a same port, - * these two channels can constitude as full-duplex mode automatically + * @note When initializing the STD mode on a channel that shares the same BCLK/WS configuration and + * the same frame timing (sample rate and total bits per frame) as the other channel on the same port, + * these two channels can constitute full-duplex mode automatically. The slot layout, clock source, + * external clock frequency, and MCLK configuration may differ (e.g. an STD channel can pair with + * a TDM channel) as long as the total bits per frame are equal and the BCLK/WS frequency matches. * * @param[in] handle I2S channel handler * @param[in] std_cfg Configurations for standard mode, including clock, slot and GPIO diff --git a/components/esp_driver_i2s/include/driver/i2s_tdm.h b/components/esp_driver_i2s/include/driver/i2s_tdm.h index b8db9cc138f..ea5b9ad3386 100644 --- a/components/esp_driver_i2s/include/driver/i2s_tdm.h +++ b/components/esp_driver_i2s/include/driver/i2s_tdm.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -196,8 +196,11 @@ typedef struct { * @brief Initialize I2S channel to TDM mode * @note Only allowed to be called when the channel state is REGISTERED, (i.e., channel has been allocated, but not initialized) * and the state will be updated to READY if initialization success, otherwise the state will return to REGISTERED. - * @note When initialize the TDM mode with a same configuration as another channel on a same port, - * these two channels can constitude as full-duplex mode automatically + * @note When initializing the TDM mode on a channel that shares the same BCLK/WS configuration and + * the same frame timing (sample rate and total bits per frame) as the other channel on the same port, + * these two channels can constitute full-duplex mode automatically. The slot layout, clock source, + * external clock frequency, and MCLK configuration may differ (e.g. a TDM channel can pair with + * an STD channel) as long as the total bits per frame are equal and the BCLK/WS frequency matches. * * @param[in] handle I2S channel handler * @param[in] tdm_cfg Configurations for TDM mode, including clock, slot and GPIO diff --git a/components/esp_driver_i2s/test_apps/i2s/main/test_i2s.c b/components/esp_driver_i2s/test_apps/i2s/main/test_i2s.c index 043929ccbba..040da1d3a96 100644 --- a/components/esp_driver_i2s/test_apps/i2s/main/test_i2s.c +++ b/components/esp_driver_i2s/test_apps/i2s/main/test_i2s.c @@ -237,6 +237,90 @@ TEST_CASE("I2S_basic_channel_allocation_reconfig_deleting_test", "[i2s]") TEST_ESP_OK(i2s_del_channel(rx_handle)); } +#if SOC_I2S_HW_VERSION_2 && SOC_I2S_SUPPORTS_TDM +/* Cover the relaxed lazy-duplex constitution conditions: + * Two channels constitute full-duplex when they share the same valid WS/BCK pins, + * the same BCLK/WS inversion settings and the same frame timing (sample rate + total frame bits), + * even if their slot layout (mode / slot number / slot bit width) or MCLK configuration differs. */ +TEST_CASE("I2S_lazy_duplex_constitution_boundary_test", "[i2s]") +{ + i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER); + i2s_chan_info_t chan_info; + i2s_chan_handle_t tx_handle; + i2s_chan_handle_t rx_handle; + + /* STD stereo 32-bit => 2 slots * 32 bits = 64 bits per frame */ + i2s_std_config_t std_64bit = { + .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE), + .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_STEREO), + .gpio_cfg = I2S_TEST_MASTER_DEFAULT_PIN, + }; + /* TDM 4-slot 16-bit => 4 slots * 16 bits = 64 bits per frame (same frame width, different slot layout) */ + i2s_tdm_config_t tdm_64bit = { + .clk_cfg = I2S_TDM_CLK_DEFAULT_CONFIG(SAMPLE_RATE), + .slot_cfg = I2S_TDM_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO, 0x0F), + .gpio_cfg = I2S_TEST_MASTER_DEFAULT_PIN, + }; + + /* Case 1: STD and TDM with the same frame width and identical BCLK/WS config can constitute duplex + * across modes. The pair channel handle should be retrievable from either side. */ + TEST_ESP_OK(i2s_new_channel(&chan_cfg, NULL, &rx_handle)); + TEST_ESP_OK(i2s_new_channel(&chan_cfg, &tx_handle, NULL)); + TEST_ESP_OK(i2s_channel_init_std_mode(rx_handle, &std_64bit)); + TEST_ESP_OK(i2s_channel_init_tdm_mode(tx_handle, &tdm_64bit)); + TEST_ESP_OK(i2s_channel_get_info(tx_handle, &chan_info)); + TEST_ASSERT(chan_info.pair_chan == rx_handle); + TEST_ESP_OK(i2s_channel_get_info(rx_handle, &chan_info)); + TEST_ASSERT(chan_info.pair_chan == tx_handle); + TEST_ESP_OK(i2s_del_channel(tx_handle)); + TEST_ESP_OK(i2s_del_channel(rx_handle)); + + /* Case 2: Different frame width (STD 16-bit => 32 bits vs TDM => 64 bits) must NOT constitute duplex. */ + i2s_std_config_t std_32bit = std_64bit; + std_32bit.slot_cfg = (i2s_std_slot_config_t)I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO); + TEST_ESP_OK(i2s_new_channel(&chan_cfg, NULL, &rx_handle)); + TEST_ESP_OK(i2s_new_channel(&chan_cfg, &tx_handle, NULL)); + TEST_ESP_OK(i2s_channel_init_std_mode(rx_handle, &std_32bit)); + TEST_ESP_OK(i2s_channel_init_tdm_mode(tx_handle, &tdm_64bit)); + TEST_ESP_OK(i2s_channel_get_info(tx_handle, &chan_info)); + TEST_ASSERT(chan_info.pair_chan == NULL); + TEST_ESP_OK(i2s_del_channel(tx_handle)); + TEST_ESP_OK(i2s_del_channel(rx_handle)); + + /* Case 3: Same frame width and BCLK/WS pins but different MCLK/external clock configs can still + * constitute duplex. */ + i2s_std_config_t std_diff_mclk = std_64bit; + std_diff_mclk.clk_cfg.mclk_multiple = I2S_MCLK_MULTIPLE_384; + std_diff_mclk.clk_cfg.ext_clk_freq_hz = 24000000; + std_diff_mclk.gpio_cfg.mclk = I2S_GPIO_UNUSED; + std_diff_mclk.gpio_cfg.invert_flags.mclk_inv = true; + TEST_ESP_OK(i2s_new_channel(&chan_cfg, NULL, &rx_handle)); + TEST_ESP_OK(i2s_new_channel(&chan_cfg, &tx_handle, NULL)); + TEST_ESP_OK(i2s_channel_init_std_mode(rx_handle, &std_64bit)); + TEST_ESP_OK(i2s_channel_init_std_mode(tx_handle, &std_diff_mclk)); + TEST_ESP_OK(i2s_channel_get_info(tx_handle, &chan_info)); + TEST_ASSERT(chan_info.pair_chan == rx_handle); + TEST_ESP_OK(i2s_channel_get_info(rx_handle, &chan_info)); + TEST_ASSERT(chan_info.pair_chan == tx_handle); + TEST_ESP_OK(i2s_del_channel(tx_handle)); + TEST_ESP_OK(i2s_del_channel(rx_handle)); + + /* Case 4: WS/BCK left unused (-1) on both channels must NOT constitute duplex even if everything + * else matches, because there is no shared clock line to combine. */ + i2s_std_config_t std_no_clk_pin = std_64bit; + std_no_clk_pin.gpio_cfg.bclk = I2S_GPIO_UNUSED; + std_no_clk_pin.gpio_cfg.ws = I2S_GPIO_UNUSED; + TEST_ESP_OK(i2s_new_channel(&chan_cfg, NULL, &rx_handle)); + TEST_ESP_OK(i2s_new_channel(&chan_cfg, &tx_handle, NULL)); + TEST_ESP_OK(i2s_channel_init_std_mode(rx_handle, &std_no_clk_pin)); + TEST_ESP_OK(i2s_channel_init_std_mode(tx_handle, &std_no_clk_pin)); + TEST_ESP_OK(i2s_channel_get_info(tx_handle, &chan_info)); + TEST_ASSERT(chan_info.pair_chan == NULL); + TEST_ESP_OK(i2s_del_channel(tx_handle)); + TEST_ESP_OK(i2s_del_channel(rx_handle)); +} +#endif // SOC_I2S_HW_VERSION_2 && SOC_I2S_SUPPORTS_TDM + static volatile bool task_run_flag; static volatile bool read_task_success = true; static volatile bool write_task_success = true; diff --git a/docs/en/api-reference/peripherals/i2s.rst b/docs/en/api-reference/peripherals/i2s.rst index 804e07e50d9..b81470f65b5 100644 --- a/docs/en/api-reference/peripherals/i2s.rst +++ b/docs/en/api-reference/peripherals/i2s.rst @@ -872,7 +872,7 @@ There are two methods to allocate a pair of full-duplex channels: /* Allocate for TX and RX channel at the same time, then they will work in full-duplex mode */ i2s_new_channel(&chan_cfg, &tx_handle, &rx_handle); - /* Set the configurations for BOTH TWO channels, since TX and RX channel have to be same in full-duplex mode */ + /* Set the configurations for both channels. BCLK/WS and frame timing must match in full-duplex mode. */ 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), @@ -897,7 +897,7 @@ There are two methods to allocate a pair of full-duplex channels: ... -2. Allocate TX and RX handles separately, and initialize them with the same configuration. +2. Allocate TX and RX handles separately, and initialize them with compatible configurations. .. code-block:: c @@ -912,7 +912,7 @@ There are two methods to allocate a pair of full-duplex channels: /* Allocate for TX and RX channel separately, they are not full-duplex yet */ ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, NULL)); - /* Set the configurations for BOTH TWO channels, they will constitute in full-duplex mode automatically */ + /* Set compatible configurations for both channels, then they will constitute in full-duplex mode automatically */ 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), @@ -938,6 +938,15 @@ There are two methods to allocate a pair of full-duplex channels: ... +.. only:: SOC_I2S_HW_VERSION_2 + + When the TX and RX channels are allocated separately (the second method above), they do not have to be configured exactly the same to constitute full-duplex. The driver lets them share the BCLK and WS lines as long as: + + - both channels use the same valid ``bclk`` and ``ws`` pins; + - both channels use the same BCLK/WS inversion settings; + - both channels produce the same frame timing, i.e. the same ``sample_rate_hz`` and the same total bits per frame (``total_slot * slot_bit_width``). + + The clock source, external clock frequency (``ext_clk_freq_hz``), and MCLK-related configuration, including the ``mclk`` pin, ``mclk_multiple``, and MCLK inversion setting, are not used as conditions for constituting full-duplex. The slot layout itself may also differ. For example, an STD channel and a TDM channel, or a 2-slot/32-bit channel paired with a 4-slot/16-bit channel, can still constitute full-duplex because the number of bits per frame is the same. Once a pair of full-duplex channels is established, the paired channel handle can be retrieved from :cpp:type:`i2s_chan_info_t`::pair_chan returned by :cpp:func:`i2s_channel_get_info`. .. only:: SOC_I2S_HW_VERSION_1 diff --git a/docs/zh_CN/api-reference/peripherals/i2s.rst b/docs/zh_CN/api-reference/peripherals/i2s.rst index 7ec5e6411af..0f0a6c61704 100644 --- a/docs/zh_CN/api-reference/peripherals/i2s.rst +++ b/docs/zh_CN/api-reference/peripherals/i2s.rst @@ -866,7 +866,7 @@ STD RX 模式 /* 同时分配给 TX 和 RX 通道,使其进入全双工模式。 */ i2s_new_channel(&chan_cfg, &tx_handle, &rx_handle); - /* 配置两个通道,因为在全双工模式下,TX 和 RX 通道必须相同。 */ + /* 配置两个通道。全双工模式要求 BCLK/WS 与帧时序匹配。 */ 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), @@ -891,7 +891,7 @@ STD RX 模式 ... -2. 调用两次 :cpp:func:`i2s_new_channel` 函数分别分配 TX 和 RX 通道,但使用相同配置初始化 TX 和 RX 通道。 +2. 调用两次 :cpp:func:`i2s_new_channel` 函数分别分配 TX 和 RX 通道,并使用兼容配置初始化 TX 和 RX 通道。 .. code-block:: c @@ -906,7 +906,7 @@ STD RX 模式 /* 分别分配给 TX 和 RX 通道 */ ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, NULL)); - /* 为两个通道设置完全相同的配置,TX 和 RX 将自动组成全双工模式 */ + /* 为两个通道设置兼容配置,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), @@ -932,6 +932,16 @@ STD RX 模式 ... +.. only:: SOC_I2S_HW_VERSION_2 + + 当 TX 和 RX 通道分别分配时(即上述第二种方法),二者无需配置得完全相同即可组成全双工。只要满足以下条件,驱动就会让它们共享 BCLK 和 WS 信号线: + + - 两个通道使用相同且有效的 ``bclk`` 和 ``ws`` 管脚; + - 两个通道使用相同的 BCLK/WS 反相配置; + - 二者产生相同的帧时序,即 ``sample_rate_hz`` 相同且每帧总位数(``total_slot * slot_bit_width``)相同。 + + 时钟源、外部时钟频率(``ext_clk_freq_hz``)以及 MCLK 相关配置不作为组成全双工的判据,MCLK 相关配置包括 ``mclk`` 管脚、``mclk_multiple`` 和 MCLK 反相配置。槽(slot)布局本身也可以不同。例如,一个 STD 通道与一个 TDM 通道,或者 2 槽/32 位通道与 4 槽/16 位通道配对,只要每帧的位数相同,仍可组成全双工。一旦组成了一对全双工通道,便可通过 :cpp:func:`i2s_channel_get_info` 返回的 :cpp:type:`i2s_chan_info_t`::pair_chan 获取配对通道的句柄。 + .. only:: SOC_I2S_HW_VERSION_1 单工模式