From 201c58e3564c3f2da850f4db5fa0e2f9f12ed388 Mon Sep 17 00:00:00 2001 From: gychang Date: Wed, 10 Sep 2025 12:12:08 -0700 Subject: [PATCH] Use PDM audio on esp32-c3-lyra board The I2S data pin is hooked up directly to the amplifier on this board, so while it does generate sound, it sounds terrible since it's not actually handling the data signal. The sample implementation in the esp-adf uses the I2S hardware with PDM encoding to drive the amp. https://github.com/espressif/esp-adf/blob/ad4ac707c5dffb450dbabfdcaa7443165ee4e852/examples/player/pipeline_spiffs_mp3/main/play_spiffs_mp3_example.c#L76 This change hardcodes the PDM implementation for this board. --- .../mpconfigboard.mk | 2 ++ .../espressif/common-hal/audiobusio/I2SOut.c | 18 +++++++++++++ .../common-hal/audiobusio/__init__.c | 25 ++++++++++++++++--- .../common-hal/audiobusio/__init__.h | 4 +++ ports/espressif/mpconfigport.mk | 3 +++ 5 files changed, 48 insertions(+), 4 deletions(-) diff --git a/ports/espressif/boards/espressif_esp32c3_lyra_v2/mpconfigboard.mk b/ports/espressif/boards/espressif_esp32c3_lyra_v2/mpconfigboard.mk index a233eb526fdbd..5690636d7538d 100644 --- a/ports/espressif/boards/espressif_esp32c3_lyra_v2/mpconfigboard.mk +++ b/ports/espressif/boards/espressif_esp32c3_lyra_v2/mpconfigboard.mk @@ -10,5 +10,7 @@ CIRCUITPY_ESP_FLASH_SIZE = 4MB CIRCUITPY_ESP_USB_SERIAL_JTAG = 0 ESPRESSIF_ESP32C3_LYRA_STATUS_LED_HOLD = 1 +ESPRESSIF_ESP32C3_LYRA_AUDIOBUSIO_PDMOUT = 1 CFLAGS += -DESPRESSIF_ESP32C3_LYRA_STATUS_LED_HOLD=$(ESPRESSIF_ESP32C3_LYRA_STATUS_LED_HOLD) +CFLAGS += -DESPRESSIF_ESP32C3_LYRA_AUDIOBUSIO_PDMOUT=$(ESPRESSIF_ESP32C3_LYRA_AUDIOBUSIO_PDMOUT) diff --git a/ports/espressif/common-hal/audiobusio/I2SOut.c b/ports/espressif/common-hal/audiobusio/I2SOut.c index adfc081389a13..50a9cb3b11275 100644 --- a/ports/espressif/common-hal/audiobusio/I2SOut.c +++ b/ports/espressif/common-hal/audiobusio/I2SOut.c @@ -25,12 +25,29 @@ #include "driver/i2s_std.h" +#if ESPRESSIF_ESP32C3_LYRA_AUDIOBUSIO_PDMOUT +#include "driver/i2s_pdm.h" +#endif + // Caller validates that pins are free. void common_hal_audiobusio_i2sout_construct(audiobusio_i2sout_obj_t *self, const mcu_pin_obj_t *bit_clock, const mcu_pin_obj_t *word_select, const mcu_pin_obj_t *data, const mcu_pin_obj_t *main_clock, bool left_justified) { port_i2s_allocate_init(&self->i2s, left_justified); + #if ESPRESSIF_ESP32C3_LYRA_AUDIOBUSIO_PDMOUT + i2s_pdm_tx_config_t pdm_tx_cfg = { + .clk_cfg = I2S_PDM_TX_CLK_DEFAULT_CONFIG(16000), + .slot_cfg = I2S_PDM_TX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO), + .gpio_cfg = { + .dout = data->number, + .invert_flags = { + .clk_inv = false, + }, + }, + }; + CHECK_ESP_RESULT(i2s_channel_init_pdm_tx_mode(self->i2s.handle, &pdm_tx_cfg)); + #else i2s_std_config_t i2s_config = { .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), @@ -43,6 +60,7 @@ void common_hal_audiobusio_i2sout_construct(audiobusio_i2sout_obj_t *self, } }; CHECK_ESP_RESULT(i2s_channel_init_std_mode(self->i2s.handle, &i2s_config)); + #endif self->bit_clock = bit_clock; self->word_select = word_select; self->mclk = main_clock; diff --git a/ports/espressif/common-hal/audiobusio/__init__.c b/ports/espressif/common-hal/audiobusio/__init__.c index d07a0b521ba9b..321542ebafe87 100644 --- a/ports/espressif/common-hal/audiobusio/__init__.c +++ b/ports/espressif/common-hal/audiobusio/__init__.c @@ -19,10 +19,18 @@ #define I2S_DMA_BUFFER_MAX_SIZE 4092 // The number of DMA buffers to allocate #define CIRCUITPY_BUFFER_COUNT (3) + +#if ESPRESSIF_ESP32C3_LYRA_AUDIOBUSIO_PDMOUT +// The maximum DMA buffer size in frames (at mono 16-bit) +#define CIRCUITPY_BUFFER_SIZE (I2S_DMA_BUFFER_MAX_SIZE / 8) +// The number of output channels is fixed at 1 +#define CIRCUITPY_OUTPUT_SLOTS (1) +#else // The maximum DMA buffer size in frames (at stereo 16-bit) #define CIRCUITPY_BUFFER_SIZE (I2S_DMA_BUFFER_MAX_SIZE / 4) // The number of output channels is fixed at 2 #define CIRCUITPY_OUTPUT_SLOTS (2) +#endif static void i2s_fill_buffer(i2s_t *self) { if (self->next_buffer_size == 0) { @@ -31,7 +39,7 @@ static void i2s_fill_buffer(i2s_t *self) { } int16_t *output_buffer = (int16_t *)self->next_buffer; size_t output_buffer_size = self->next_buffer_size; - const size_t bytes_per_output_frame = 4; + const size_t bytes_per_output_frame = CIRCUITPY_OUTPUT_SLOTS * 2; size_t bytes_per_input_frame = self->channel_count * self->bytes_per_sample; if (!self->playing || self->paused || !self->sample || self->stopping) { memset(output_buffer, 0, self->next_buffer_size); @@ -62,7 +70,8 @@ static void i2s_fill_buffer(i2s_t *self) { size_t sample_bytecount = self->sample_end - self->sample_data; // The framecount is the minimum of space left in the output buffer or left in the incoming sample. size_t framecount = MIN(output_buffer_size / bytes_per_output_frame, sample_bytecount / bytes_per_input_frame); - if (self->samples_signed && self->channel_count == 2) { + + if (self->samples_signed && self->channel_count == CIRCUITPY_OUTPUT_SLOTS) { if (self->bytes_per_sample == 2) { memcpy(output_buffer, self->sample_data, framecount * bytes_per_output_frame); } else { @@ -165,8 +174,16 @@ void port_i2s_play(i2s_t *self, mp_obj_t sample, bool loop) { audiosample_reset_buffer(self->sample, false, 0); uint32_t sample_rate = audiosample_get_sample_rate(sample); + + #if ESPRESSIF_ESP32C3_LYRA_AUDIOBUSIO_PDMOUT + i2s_pdm_tx_clk_config_t clk_config = I2S_PDM_TX_CLK_DEFAULT_CONFIG(sample_rate); + CHECK_ESP_RESULT(i2s_channel_reconfig_pdm_tx_clock(self->handle, &clk_config)); + size_t frame_size = sizeof(uint16_t); + #else i2s_std_clk_config_t clk_config = I2S_STD_CLK_DEFAULT_CONFIG(sample_rate); CHECK_ESP_RESULT(i2s_channel_reconfig_std_clock(self->handle, &clk_config)); + size_t frame_size = sizeof(uint32_t); + #endif // preload the data self->playing = true; @@ -183,12 +200,12 @@ void port_i2s_play(i2s_t *self, mp_obj_t sample, bool loop) { self->next_buffer = &starting_frame; self->next_buffer_size = sizeof(starting_frame); i2s_fill_buffer(self); - i2s_channel_preload_data(self->handle, &starting_frame, sizeof(uint32_t), &bytes_loaded); + i2s_channel_preload_data(self->handle, &starting_frame, frame_size, &bytes_loaded); preloaded += bytes_loaded; } // enable the channel - i2s_channel_enable(self->handle); + CHECK_ESP_RESULT(i2s_channel_enable(self->handle)); // The IDF will call us back when there is a free DMA buffer. } diff --git a/ports/espressif/common-hal/audiobusio/__init__.h b/ports/espressif/common-hal/audiobusio/__init__.h index 0088cb87d5f91..7954fd17e5a8a 100644 --- a/ports/espressif/common-hal/audiobusio/__init__.h +++ b/ports/espressif/common-hal/audiobusio/__init__.h @@ -12,6 +12,10 @@ #include "driver/i2s_std.h" +#if ESPRESSIF_ESP32C3_LYRA_AUDIOBUSIO_PDMOUT +#include "driver/i2s_pdm.h" +#endif + typedef struct { mp_obj_t *sample; bool left_justified; diff --git a/ports/espressif/mpconfigport.mk b/ports/espressif/mpconfigport.mk index 42bf3418f9639..7e68411705010 100644 --- a/ports/espressif/mpconfigport.mk +++ b/ports/espressif/mpconfigport.mk @@ -349,3 +349,6 @@ USB_NUM_IN_ENDPOINTS = 5 # Usually lots of flash space available CIRCUITPY_MESSAGE_COMPRESSION_LEVEL ?= 1 + +ESPRESSIF_ESP32C3_LYRA_AUDIOBUSIO_PDMOUT ?= 0 +CFLAGS += -DESPRESSIF_ESP32C3_LYRA_AUDIOBUSIO_PDMOUT=$(ESPRESSIF_ESP32C3_LYRA_AUDIOBUSIO_PDMOUT)