From 372b0fd0a3a485e48c6e243390ed7f2506043624 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 03:19:37 +0000 Subject: [PATCH] Wake SAI writers from the DMA interrupt instead of sleeping a tick stm32_sai_dma_write/read/flush polled with Dmod_ThreadSleep(1). With dmod-boot's 100 Hz FreeRTOS tick that sleep lasts ~10 ms, longer than the whole TX path (256-frame queue + 2x64-frame DMA halves = 8 ms at 48 kHz), so every full queue ended in ~4 ms of silence: choppy playback and thousands of tx_underruns. Post a binary semaphore from the DMA half/complete/error callback and from stop, and wait on it (bounded at 10 ms) instead. Reproduced in Renode with an 8 s melody: before 4887 underruns and 14 s of output, after only the 4 drain-tail halves and 7.94 s. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Nr1HhezRY951yBxxTm1b65 --- src/port/CMakeLists.txt | 1 + src/port/stm32_common/stm32_sai_dma.c | 25 +++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/port/CMakeLists.txt b/src/port/CMakeLists.txt index dc56ff4..ebdbab9 100644 --- a/src/port/CMakeLists.txt +++ b/src/port/CMakeLists.txt @@ -19,4 +19,5 @@ dmod_link_modules(${DMOD_MODULE_NAME} dmclk_port@>=1.2 dmdma@>=0.5 dmheap + dmosi ) diff --git a/src/port/stm32_common/stm32_sai_dma.c b/src/port/stm32_common/stm32_sai_dma.c index 24a3f60..12f3c35 100644 --- a/src/port/stm32_common/stm32_sai_dma.c +++ b/src/port/stm32_common/stm32_sai_dma.c @@ -2,6 +2,7 @@ #include "dmdma_lease.h" #include "dmheap.h" #include "dmod.h" +#include "dmosi.h" #include #include #include @@ -9,6 +10,8 @@ #define DMA_HALF_FRAMES 64U #define DMA_QUEUE_FRAMES 256U #define DMA_ALIGNMENT 32U +/* Upper bound of one wait for a queue change. A DMA interrupt ends it early. */ +#define WAIT_SLICE_MS 10 typedef struct { struct stm32_sai_dma *owner; @@ -19,6 +22,7 @@ struct stm32_sai_dma { dmdma_lease_t tx_lease; dmdma_lease_t rx_lease; dmheap_context_t *heap; + dmosi_semaphore_t wake; uint32_t *tx_dma; uint32_t *rx_dma; uint32_t *tx_queue; @@ -124,6 +128,7 @@ static void dma_event(dmdma_lease_t lease, dmdma_event_t event, void *user_ptr) if (event & (dmdma_event_error | dmdma_event_timeout)) { __atomic_store_n(&dma->fault, true, __ATOMIC_RELEASE); __atomic_fetch_add(&dma->status->transfer_errors, 1U, __ATOMIC_RELAXED); + dmosi_semaphore_post(dma->wake, 1); return; } if (event & dmdma_event_half_complete) { @@ -140,6 +145,7 @@ static void dma_event(dmdma_lease_t lease, dmdma_event_t event, void *user_ptr) __atomic_fetch_add(&dma->tx_half_events, 1U, __ATOMIC_RELEASE); } } + dmosi_semaphore_post(dma->wake, 1); } /** @brief Allocate a circular buffer from the firmware's uncached DMA heap. */ @@ -186,6 +192,8 @@ int stm32_sai_dma_create(stm32_sai_dma_t **result, dma->transmit = config->transmit; dma->receive = config->receive; int rc = dma->heap ? 0 : -ENODEV; + if (rc == 0) dma->wake = dmosi_semaphore_create(0, 1); + if (rc == 0 && !dma->wake) rc = -ENOMEM; if (rc == 0) dma->tx_dma = alloc_dma_buffer(dma); if (rc == 0 && !dma->tx_dma) rc = -ENOMEM; if (rc == 0 && dma->receive) dma->rx_dma = alloc_dma_buffer(dma); @@ -214,6 +222,7 @@ void stm32_sai_dma_destroy(stm32_sai_dma_t *dma) if (dma->tx_dma) dmheap_free(dma->heap, dma->tx_dma, true); if (dma->rx_queue) Dmod_Free(dma->rx_queue); if (dma->tx_queue) Dmod_Free(dma->tx_queue); + if (dma->wake) dmosi_semaphore_destroy(dma->wake); Dmod_Free(dma); } @@ -265,9 +274,19 @@ void stm32_sai_dma_stop(stm32_sai_dma_t *dma) __atomic_store_n(&dma->active, false, __ATOMIC_RELEASE); if (dma->tx_lease) dmdma_lease_abort(dma->tx_lease); if (dma->rx_lease) dmdma_lease_abort(dma->rx_lease); + if (dma->wake) { + /* DMA is stopped: no ISR posts any more. Take a pending wake first so + * this post cannot overflow the binary semaphore. */ + dmosi_semaphore_wait(dma->wake, 1, 0); + dmosi_semaphore_post(dma->wake, 1); + } } -/** @brief Return a transfer error or wait for one queue change. */ +/** @brief Return a transfer error or wait for one queue change. + * + * The DMA interrupt posts the semaphore after every half buffer, so the caller + * wakes as soon as queue space or data appears. A tick-based sleep is not + * enough: with a 10 ms RTOS tick it outlasts the whole TX queue. */ static int wait_queue(stm32_sai_dma_t *dma, Dmod_Timestamp_t started, uint32_t timeout_ms) { @@ -275,7 +294,9 @@ static int wait_queue(stm32_sai_dma_t *dma, Dmod_Timestamp_t started, if (!__atomic_load_n(&dma->active, __ATOMIC_ACQUIRE)) return -ECANCELED; if (timeout_ms && Dmod_GetUptime() - started >= timeout_ms) return -ETIMEDOUT; - Dmod_ThreadSleep(1); + /* Not shortened to the remaining timeout: below one tick dmosi would + * poll instead of block. */ + dmosi_semaphore_wait(dma->wake, 1, WAIT_SLICE_MS); return 0; }