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.
Bug Report
Component
esp_codec_devv1.5.7 (components/esp_codec_dev/platform/audio_codec_data_i2s.c)Environment
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()inset_fs()returns 0 for the playback direction. This causesI2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(0, ...)to producedata_bit_width=0, resulting inbuf_size=0. The subsequentheap_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)
When
audio_codec_new_i2s_data()is called for the RECORD device withrx_handleset andtx_handle=NULL, it auto-detects the pair TX handle. The RECORD i2s_data now has bothin_handleandout_handleset.2.
get_paired()skips full-duplex nodes (line 156-160)Because RECORD has both handles,
get_paired(RECORD, false)skips it and looks for another node without_handle. If PLAYBACK hasn't been registered yet, it returns RECORD itself asduplex_fallback.3.
check_fs_compatible()"same params" shortcut (line 448-453)This path only copies to
channel_fs(out_fsorin_fs), but does not update the sharedi2s_data->fs. Since PLAYBACK'sfswas never initialized (the cross-pairmemcpy(&paired->fs, ...)at line 431 either targeted the wrong node or was skipped becausepaired == i2s_data), it remains all zeros.4.
get_bits()reads uninitializedfs(line 227-234)Reproduction
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 runsget_paired(), allowing thememcpy(&paired->fs, ...)at line 431 to correctly initialize PLAYBACK's sharedfs.Suggested Fix
The most robust fix would be in
check_fs_compatible()- when taking the "same params" shortcut (line 448-453), also updatei2s_data->fs:Alternatively,
get_bits()could derivetotal_bitsfromout_fs/in_fsinstead of the sharedfs, 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.