From b9f5b691829c1541365e3d12bb8c0444088fd59a Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Tue, 10 Nov 2026 07:56:02 -0600 Subject: [PATCH] audiopump: a port with no threads can render from an interrupt A driver that renders from an interrupt sets the new render_kick hook. audiopump.service() on the interpreter then asks it to render instead of running the pump itself, and the interpreter's changes to the pump in service mode (spawn, stop, park, teardown, and a service call from C) take the pump lock, which such a driver implements by holding its interrupt off. With render_kick NULL, as on every port today, nothing changes. --- src/audiopump/audiopump.c | 33 ++++++++++++++++++++++++++++++++- src/shared/audiodsp_port.h | 15 +++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/audiopump/audiopump.c b/src/audiopump/audiopump.c index 55afb47..7c39b21 100644 --- a/src/audiopump/audiopump.c +++ b/src/audiopump/audiopump.c @@ -1627,7 +1627,9 @@ static void audiopump_teardown(bool release_guard) { audiopump_ctx.stop = true; audiopump_ctx.park_req = false; if (audiopump_ctx.service_mode) { + audiodsp_pump_lock_acquire(); audiopump_tail_drop(&audiopump_ctx, false); + audiodsp_pump_lock_release(); } audiodsp_pump_set_active(false); const audiodsp_port_ops_t *port = audiodsp_port(); @@ -1854,9 +1856,13 @@ static int audiopump_launch(int core, int prio, int stack, bool psram) { // -2, not -1: a caller that prints what spawn() returned should be // able to tell "no core affinity" from "no thread at all", and on a // build that should have had one that number is the symptom. + // Under the lock, for a driver that renders from an interrupt: the + // first render must find the loop begun. + audiodsp_pump_lock_acquire(); audiopump_ctx.service_mode = true; audiopump_live = true; audiopump_run_begin(&audiopump_ctx); + audiodsp_pump_lock_release(); return -2; } @@ -2001,7 +2007,9 @@ void audiopump_c_stop(void) { audiopump_ctx.stop = true; audiopump_ctx.park_req = false; if (audiopump_ctx.service_mode) { + audiodsp_pump_lock_acquire(); audiopump_tail_drop(&audiopump_ctx, false); + audiodsp_pump_lock_release(); } const audiodsp_port_ops_t *port = audiodsp_port(); if (port->thread_wake != NULL) { @@ -2035,10 +2043,21 @@ void audiopump_c_service(uint64_t budget) { || audiopump_ctx.finished) { return; } + // A driver that renders from an interrupt calls this from it, where the + // lock is nothing; a call from the interpreter (an output's stop() + // finishing the loop) holds the interrupt off while it runs, so the two + // never pull at once. Without render_kick the lock isn't taken, as before. + const bool hold = audiodsp_port()->render_kick != NULL; + if (hold) { + audiodsp_pump_lock_acquire(); + } if (audiopump_run_blocks(&audiopump_ctx, budget ? budget : UINT64_MAX) == AUDIOPUMP_SERVICE_DONE) { audiopump_run_end(&audiopump_ctx); } + if (hold) { + audiodsp_pump_lock_release(); + } } // --- the park protocol ---------------------------------------------------- @@ -2115,7 +2134,9 @@ static bool audiopump_c_park_fade(uint32_t timeout_us, bool fade) { } } if (audiopump_ctx.service_mode) { + audiodsp_pump_lock_acquire(); audiopump_tail_drop(&audiopump_ctx, true); + audiodsp_pump_lock_release(); // The loop is not running -- this call IS the only thread -- so the // pump is already at a block boundary by construction and park() is a // store. The wait below would spin until the timeout and then report @@ -2178,7 +2199,17 @@ static mp_obj_t audiopump_service(size_t n_args, const mp_obj_t *args) { } int why = AUDIOPUMP_SERVICE_MORE; uint64_t before = audiopump_ctx.acc_blocks; - if (audiopump_ctx.service_mode) { + void (*kick)(void) = audiodsp_port()->render_kick; + if (kick != NULL) { + // The driver renders from an interrupt: running the pump here as + // well would be two pumps at once. Ask it to render, and report no + // blocks, as a threaded port does. + if (audiopump_live && !audiopump_ctx.finished) { + kick(); + } else { + why = AUDIOPUMP_SERVICE_DONE; + } + } else if (audiopump_ctx.service_mode) { if (audiopump_live && !audiopump_ctx.finished) { why = audiopump_run_blocks(&audiopump_ctx, budget); if (why == AUDIOPUMP_SERVICE_DONE) { diff --git a/src/shared/audiodsp_port.h b/src/shared/audiodsp_port.h index b28fa32..a0b463b 100644 --- a/src/shared/audiodsp_port.h +++ b/src/shared/audiodsp_port.h @@ -157,8 +157,23 @@ typedef struct { // AUDIODSP_PORT_MEM_HOT_HOOKS. void *(*mem_alloc_hot)(size_t size); void (*mem_free_hot)(void *ptr); + + // --- a port that renders from an interrupt ------------------------------- + // + // A port with no threads can still render away from the interpreter, from + // an interrupt the driver raises (the STM32H7's DAC does). The interrupt + // is then the pump's thread: audiopump.service() on the interpreter calls + // render_kick() to ask the driver to render, instead of rendering there, + // and the interpreter's changes to the pump take the lock, which such a + // driver implements by holding its interrupt off. NULL everywhere else, + // which changes nothing. A driver that also builds against an older + // audiodsp fills it only #ifdef AUDIODSP_PORT_IRQ_HOOKS. + void (*render_kick)(void); } audiodsp_port_ops_t; +// This header has render_kick. +#define AUDIODSP_PORT_IRQ_HOOKS (1) + // This header has the mem_* hooks above. #define AUDIODSP_PORT_MEM_HOOKS (1) // And the mem_*_hot pair.