From 7e854e10f0f8ccd28f539db939b7ede0bd20e3d1 Mon Sep 17 00:00:00 2001 From: Gronod Date: Wed, 16 Sep 2026 22:20:53 +0100 Subject: [PATCH 1/2] Add I2S speaker audio for S3 --- components/hal_audio/CMakeLists.txt | 3 + components/hal_audio/s3/hal_audio.c | 212 ++++++++++++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 components/hal_audio/s3/hal_audio.c diff --git a/components/hal_audio/CMakeLists.txt b/components/hal_audio/CMakeLists.txt index a421f3e..64c83ca 100644 --- a/components/hal_audio/CMakeLists.txt +++ b/components/hal_audio/CMakeLists.txt @@ -4,6 +4,9 @@ set(priv_req driver) if(IDF_TARGET STREQUAL "esp32") list(APPEND srcs wroom/hal_audio.c) list(APPEND priv_req board_wroom) +elseif(IDF_TARGET STREQUAL "esp32s3") + list(APPEND srcs s3/hal_audio.c) + list(APPEND priv_req board_jc4827w543) else() list(APPEND srcs stub/hal_audio.c) endif() diff --git a/components/hal_audio/s3/hal_audio.c b/components/hal_audio/s3/hal_audio.c new file mode 100644 index 0000000..747656b --- /dev/null +++ b/components/hal_audio/s3/hal_audio.c @@ -0,0 +1,212 @@ +#include +#include +#include + +#include "esp_err.h" +#include "esp_log.h" +#include "driver/i2s_std.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/queue.h" + +#include "board.h" + +#define SPK_SAMPLE_RATE 48000 +#define TONE_HZ 2000 +#define TONE_PERIOD_SMP (SPK_SAMPLE_RATE / TONE_HZ) /* 24 samples */ +#define CHUNK_MS 10 +#define CHUNK_FRAMES (SPK_SAMPLE_RATE / 1000 * CHUNK_MS) /* 480, multiple of period */ +#define TONE_AMPLITUDE 12000 +#define DMA_DESC_NUM 4 +#define DMA_FRAME_NUM 240 + +static const char *TAG = "audio"; + +enum { + AUDIO_SHORT = 1, + AUDIO_ALARM, + AUDIO_CANCEL, +}; + +static QueueHandle_t s_q; +static TaskHandle_t s_task; +static i2s_chan_handle_t s_tx; +static bool s_chan_on; +static bool s_inited; + +static int16_t s_tone[CHUNK_FRAMES * 2]; +static int16_t s_silence[CHUNK_FRAMES * 2]; + +static void chan_on(void) +{ + if (!s_chan_on && s_tx != NULL) { + i2s_channel_enable(s_tx); + s_chan_on = true; + } +} + +static void chan_off(void) +{ + if (s_chan_on && s_tx != NULL) { + size_t w = 0; + i2s_channel_write(s_tx, s_silence, sizeof(s_silence), &w, 50); + i2s_channel_disable(s_tx); + s_chan_on = false; + } +} + +static bool drain_cancel(void) +{ + int msg; + while (xQueueReceive(s_q, &msg, 0) == pdTRUE) { + if (msg == AUDIO_CANCEL) { + return true; + } + } + return false; +} + +/* Write buf for ~ms wall time. DMA pacing makes each 10 ms write block + * in this task only; the UI path never touches I2S. */ +static bool play_buf_ms(const int16_t *buf, size_t len, uint32_t ms) +{ + TickType_t t0 = xTaskGetTickCount(); + while ((xTaskGetTickCount() - t0) < pdMS_TO_TICKS(ms)) { + if (drain_cancel()) { + return false; + } + size_t w = 0; + if (i2s_channel_write(s_tx, buf, len, &w, portMAX_DELAY) != ESP_OK) { + return false; + } + } + return true; +} + +static void play_short(void) +{ + chan_on(); + play_buf_ms(s_tone, sizeof(s_tone), 100); + chan_off(); +} + +static void play_alarm(void) +{ + chan_on(); + for (int i = 0; i < 10; i++) { + if (!play_buf_ms(s_tone, sizeof(s_tone), 500)) { + break; + } + if (!play_buf_ms(s_silence, sizeof(s_silence), 250)) { + break; + } + } + chan_off(); +} + +static void audio_task(void *arg) +{ + (void)arg; + int msg; + for (;;) { + if (xQueueReceive(s_q, &msg, portMAX_DELAY) != pdTRUE) { + continue; + } + if (msg == AUDIO_SHORT) { + play_short(); + } else if (msg == AUDIO_ALARM) { + play_alarm(); + } else if (msg == AUDIO_CANCEL) { + chan_off(); + } + } +} + +static void post(int msg) +{ + if (s_q == NULL) { + return; + } + xQueueSend(s_q, &msg, 0); +} + +void autofilm_audio_task(void *arg) +{ + audio_task(arg); +} + +void hal_audio_init(void) +{ + if (s_inited) { + return; + } + gpio_num_t bclk = board_pin_spk_bclk(); + gpio_num_t lrclk = board_pin_spk_lrclk(); + gpio_num_t din = board_pin_spk_din(); + if (bclk == GPIO_NUM_NC || lrclk == GPIO_NUM_NC || din == GPIO_NUM_NC) { + ESP_LOGW(TAG, "no speaker pins on this board"); + s_inited = true; + return; + } + + for (int i = 0; i < CHUNK_FRAMES; i++) { + int16_t v = ((i % TONE_PERIOD_SMP) < (TONE_PERIOD_SMP / 2)) + ? TONE_AMPLITUDE : -TONE_AMPLITUDE; + s_tone[i * 2] = v; + s_tone[i * 2 + 1] = v; + } + + i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER); + chan_cfg.dma_desc_num = DMA_DESC_NUM; + chan_cfg.dma_frame_num = DMA_FRAME_NUM; + if (i2s_new_channel(&chan_cfg, &s_tx, NULL) != ESP_OK) { + ESP_LOGE(TAG, "i2s_new_channel failed"); + s_inited = true; + return; + } + i2s_std_config_t std_cfg = { + .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SPK_SAMPLE_RATE), + .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, + I2S_SLOT_MODE_STEREO), + .gpio_cfg = { + .mclk = I2S_GPIO_UNUSED, + .bclk = bclk, + .ws = lrclk, + .dout = din, + .din = I2S_GPIO_UNUSED, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false, + }, + }, + }; + if (i2s_channel_init_std_mode(s_tx, &std_cfg) != ESP_OK) { + ESP_LOGE(TAG, "i2s_channel_init_std_mode failed"); + i2s_del_channel(s_tx); + s_tx = NULL; + s_inited = true; + return; + } + s_q = xQueueCreate(8, sizeof(int)); + if (s_task == NULL) { + xTaskCreate(audio_task, "audio", 3072, NULL, 2, &s_task); + } + s_inited = true; + ESP_LOGI(TAG, "init i2s bclk=%d ws=%d dout=%d", bclk, lrclk, din); +} + +void hal_audio_beep_short(void) +{ + post(AUDIO_SHORT); +} + +void hal_audio_alarm_complete(void) +{ + post(AUDIO_ALARM); +} + +void hal_audio_alarm_cancel(void) +{ + post(AUDIO_CANCEL); +} -- 2.39.5 From d70df219f8e4037913979f38c134f80f795f4bd3 Mon Sep 17 00:00:00 2001 From: Gronod Date: Wed, 16 Sep 2026 22:28:17 +0100 Subject: [PATCH 2/2] Mark I04 status DONE --- docs/megaplans/INTEGRATION-MEGAPLAN.md | 2 +- docs/megaplans/integration/I04-audio-i2s.md | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/megaplans/INTEGRATION-MEGAPLAN.md b/docs/megaplans/INTEGRATION-MEGAPLAN.md index 83e8a49..465d55b 100644 --- a/docs/megaplans/INTEGRATION-MEGAPLAN.md +++ b/docs/megaplans/INTEGRATION-MEGAPLAN.md @@ -38,4 +38,4 @@ If blocked: stop, commit nothing broken, write `BLOCKED:` at top of the phase fi | I01 | `integration/I01-board-configs.md` | Board pin allocations & accessors | DONE | | I02 | `integration/I02-temp-fix.md` | Temp HAL shared & starvation fix | DONE | | I03 | `integration/I03-motor-hal.md` | Motor HAL shared across boards | DONE | -| I04 | `integration/I04-audio-i2s.md` | I2S audio implementation for S3 | TODO | +| I04 | `integration/I04-audio-i2s.md` | I2S audio implementation for S3 | DONE | diff --git a/docs/megaplans/integration/I04-audio-i2s.md b/docs/megaplans/integration/I04-audio-i2s.md index c4ade63..0128f37 100644 --- a/docs/megaplans/integration/I04-audio-i2s.md +++ b/docs/megaplans/integration/I04-audio-i2s.md @@ -1,5 +1,6 @@ -STATUS: TODO +STATUS: DONE DEPENDS: I01 +Notes: `idf.py build` for both targets verified via CI run 38684 (no local IDF). **READ:** `docs/megaplans/INTEGRATION-MEGAPLAN.md`, `components/hal_audio/*` @@ -17,6 +18,6 @@ DEPENDS: I01 1. `Add I2S speaker audio for S3` **DoD checkboxes:** -- [ ] `hal_audio` I2S implementation added for S3. -- [ ] Queue protocol and non-blocking semantics maintained. -- [ ] Build succeeds. +- [x] `hal_audio` I2S implementation added for S3. +- [x] Queue protocol and non-blocking semantics maintained. +- [x] Build succeeds. -- 2.39.5