Skip to content

esp_codec_dev: get_bits() returns 0 when ADC and DAC use separate i2s_data instances with same sample params #1606

@eerenyuan

Description

@eerenyuan

Bug Report

Component

esp_codec_dev v1.5.7 (components/esp_codec_dev/platform/audio_codec_data_i2s.c)

Environment

  • Hardware: ESP32-S3 + ES7210 (ADC) + ES8311 (DAC), full-duplex I2S
  • ESP-IDF: v5.4.2
  • esp_codec_dev: v1.5.7

Description

When ADC and DAC are created as separate codec devices (each with their own audio_codec_new_i2s_data() call), and the DAC is opened after the ADC, get_bits() in set_fs() returns 0 for the playback direction. This causes I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(0, ...) to produce data_bit_width=0, resulting in buf_size=0. The subsequent heap_caps_aligned_calloc(4, 1, 0, ...) returns NULL at parameter validation (size==0), and the I2S driver dereferences the NULL pointer causing a Guru Meditation (LoadProhibited).

The ADC open succeeds. Only the DAC open crashes.

Root Cause

Three interacting behaviors in audio_codec_data_i2s.c:

1. Auto-detect duplex pair handle (line 524-529)

if (i2s_data->in_handle && i2s_data->out_handle == NULL) {
    i2s_data->out_handle = channel_info.pair_chan; // auto-detect tx_handle
}

When audio_codec_new_i2s_data() is called for the RECORD device with rx_handle set and tx_handle=NULL, it auto-detects the pair TX handle. The RECORD i2s_data now has both in_handle and out_handle set.

2. get_paired() skips full-duplex nodes (line 156-160)

if (cur->i2s_data->in_handle && cur->i2s_data->out_handle) {
    duplex_fallback = cur->i2s_data;
    cur = cur->next;
    continue; // skipped!
}

Because RECORD has both handles, get_paired(RECORD, false) skips it and looks for another node with out_handle. If PLAYBACK hasn't been registered yet, it returns RECORD itself as duplex_fallback.

3. check_fs_compatible() "same params" shortcut (line 448-453)

if (fs->channel == paired->fs.channel &&
    fs->bits_per_sample == paired->fs.bits_per_sample) {
    memcpy(channel_fs, fs, ...);  // only updates out_fs or in_fs
    return set_fs(i2s_data, playback, false);
}

This path only copies to channel_fs (out_fs or in_fs), but does not update the shared i2s_data->fs. Since PLAYBACK's fs was never initialized (the cross-pair memcpy(&paired->fs, ...) at line 431 either targeted the wrong node or was skipped because paired == i2s_data), it remains all zeros.

4. get_bits() reads uninitialized fs (line 227-234)

static uint8_t get_bits(i2s_data_t *i2s_data, bool playback) {
    uint8_t total_bits = i2s_data->fs.bits_per_sample * i2s_data->fs.channel;
    //                          ^^^ 0       *          ^^^ 0  = 0
    if (playback) return total_bits / i2s_data->out_fs.channel;
    //                               0 / 2 = 0  -> slot_bits = 0
}

Reproduction

// Init order that triggers the bug:

// 1. Create I2S full-duplex channel
i2s_new_channel(&chan_cfg, &tx_handle, &rx_handle);

// 2. Create RECORD data_if (auto-detects tx_handle as pair)
audio_codec_i2s_cfg_t rec_i2s = { .port = I2S_NUM_1, .rx_handle = rx_handle, .tx_handle = NULL };
record_data_if = audio_codec_new_i2s_data(&rec_i2s);
// Logs: "Auto get out handle 0x... from in handle 0x... when duplex mode"

// 3. Open ADC codec
esp_codec_dev_open(record_dev, &(esp_codec_dev_sample_info_t){
    .bits_per_sample=32, .sample_rate=16000, .channel=2
});
// -> succeeds

// 4. Create PLAYBACK data_if (separate i2s_data node)
audio_codec_i2s_cfg_t play_i2s = { .port = I2S_NUM_1, .rx_handle = NULL, .tx_handle = tx_handle };
play_data_if = audio_codec_new_i2s_data(&play_i2s);

// 5. Open DAC codec -> CRASH
esp_codec_dev_open(play_dev, &(esp_codec_dev_sample_info_t){
    .bits_per_sample=32, .sample_rate=16000, .channel=2
});
// get_bits() returns 0 -> buf_size=0 -> alloc fails -> NULL deref -> Guru Meditation

Workaround

Register both audio_codec_new_i2s_data() instances before opening either codec. This ensures the PLAYBACK node exists in the internal list when ADC open runs get_paired(), allowing the memcpy(&paired->fs, ...) at line 431 to correctly initialize PLAYBACK's shared fs.

// Register both data interfaces BEFORE any esp_codec_dev_open()
record_data_if = audio_codec_new_i2s_data(
    &(audio_codec_i2s_cfg_t){.port=I2S_NUM_1, .rx_handle=rx_handle, .tx_handle=NULL});
play_data_if = audio_codec_new_i2s_data(
    &(audio_codec_i2s_cfg_t){.port=I2S_NUM_1, .rx_handle=NULL, .tx_handle=tx_handle});

// Now open codecs
esp_codec_dev_open(record_dev, &fs); // get_paired finds PLAYBACK, copies fs
esp_codec_dev_open(play_dev, &fs);   // get_bits returns 32, works correctly

Suggested Fix

The most robust fix would be in check_fs_compatible() - when taking the "same params" shortcut (line 448-453), also update i2s_data->fs:

if (fs->channel == paired->fs.channel &&
    fs->bits_per_sample == paired->fs.bits_per_sample) {
+   memcpy(&i2s_data->fs, fs, sizeof(esp_codec_dev_sample_info_t));
    memcpy(channel_fs, fs, sizeof(esp_codec_dev_sample_info_t));
    return set_fs(i2s_data, playback, false);
}

Alternatively, get_bits() could derive total_bits from out_fs/in_fs instead of the shared fs, since those are always set correctly.

Impact

This affects any project using separate ADC and DAC codec devices on a full-duplex I2S port, which is a common pattern for boards with separate input (e.g., ES7210) and output (e.g., ES8311) codecs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions