From a0e69048166450bbc8558d386b9e44e198aab9a0 Mon Sep 17 00:00:00 2001 From: Jax Calder Date: Sun, 27 Sep 2026 09:54:20 +0100 Subject: [PATCH 1/2] Add centrally arbitrated DMA leases (dmdma_lease.h) Lets peripheral drivers (dmuart, dmspi, dmsdio, ...) reserve and drive a DMA stream directly by calling dmdma as a DIF (Dmod_GetModuleContext + Dmod_GetDifFunction), instead of going through a /dev/dmdmaN/M device node - acquire/release, configure+start/abort, status, and callback registration. Both reservation paths (dmdrvi_open() and dmdma_lease_acquire()) share the same stream-slot bookkeeping, so a stream open as a device node can't also be leased and vice versa, and the same stream can never be acquired twice through any API combination. Also adds a real per-transfer watchdog: dmdma_transfer_config_t.timeout_ms arms a dmosi_timer that auto-aborts the stream and fires dmdma_event_timeout if it elapses, shared by both the lease and device-node ioctl paths. A new dmdma_event_aborted fires (synchronously, only if something was actually in flight) when a caller explicitly aborts. Transfer validation (direction vs. request-line consistency, address alignment, non-zero length) is now shared between both start paths via validate_transfer_config(). Verified on real STM32F746G-DISCO hardware (DMA2 stream 0): acquire, double- acquire rejection, a real mem-to-mem transfer with data verification and correct callback/user_ptr delivery, malformed-config rejection, abort while busy, the dmosi_timer watchdog firing dmdma_event_timeout, and clean release+re-acquire. Two real bugs only surfaced this way and are fixed here: dmod_dmdma_dif_api_declaration() never emits a runtime-visible registration when the implementing module is also the DIF-defining module (dmdma implementing its own "dmdma" DIF family) - worked around with an explicit registration macro - and the watchdog was being disarmed on every circular wrap instead of only on a genuinely terminal event, since dmdma_event_complete fires on every wrap for a circular transfer, not just at the end. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 1 + include/dmdma.h | 1 + include/dmdma_lease.h | 103 +++++++++ include/dmdma_types.h | 11 + src/dmdma.c | 488 +++++++++++++++++++++++++++++++++++++-- tests/CMakeLists.txt | 32 ++- tests/dmdma_test_dev.c | 1 + tests/dmdma_test_lease.c | 376 ++++++++++++++++++++++++++++++ tests/dmdma_test_port.c | 1 + 9 files changed, 986 insertions(+), 28 deletions(-) create mode 100644 include/dmdma_lease.h create mode 100644 tests/dmdma_test_lease.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 80c24ad..57cc736 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -79,6 +79,7 @@ target_include_directories(${DMOD_MODULE_NAME}_if INTERFACE dmod_link_modules(${DMOD_MODULE_NAME} dmdrvi dmini + dmosi ) add_subdirectory(src/port) diff --git a/include/dmdma.h b/include/dmdma.h index 7256c5f..f3a6a65 100644 --- a/include/dmdma.h +++ b/include/dmdma.h @@ -3,6 +3,7 @@ #include "dmdma_defs.h" #include "dmdma_types.h" +#include "dmdma_lease.h" /** * @brief DMA driver configuration structure diff --git a/include/dmdma_lease.h b/include/dmdma_lease.h new file mode 100644 index 0000000..dd91c14 --- /dev/null +++ b/include/dmdma_lease.h @@ -0,0 +1,103 @@ +#ifndef DMDMA_LEASE_H +#define DMDMA_LEASE_H + +#include "dmod.h" +#include "dmdma_defs.h" +#include "dmdma_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief DMA lease API + * + * An alternative to opening /dev/dmdmaN/M through dmdrvi, for peripheral + * drivers (dmuart, dmspi, dmsdio, ...) that want to reserve and drive a DMA + * stream directly from their own code instead of going through the VFS path + * lookup. Both reservation paths share the exact same bookkeeping in + * dmdma.c - a stream already open as a device node cannot also be leased, + * and vice versa (see "Streams are devices, not a pool" in docs/README.md). + * + * This is a DIF, like dmdrvi itself - dmdma is an optional dependency for a + * peripheral driver (it can always fall back to polled/interrupt-only I/O), + * so a consumer resolves it by name and checks for NULL at every step: + * + * Dmod_Context_t* dma = Dmod_GetModuleContext("dmdma"); + * dmod_dmdma_lease_acquire_t lease_acquire = + * dma ? (dmod_dmdma_lease_acquire_t)Dmod_GetDifFunction(dma, dmod_dmdma_lease_acquire_sig) : NULL; + * dmdma_lease_t lease = lease_acquire ? lease_acquire(controller, stream) : NULL; + * + * (see dmdevfs.c's or dmvfs.c's own DIF consumer code for the established + * pattern this mirrors exactly). + */ + +/** + * @brief Opaque handle to one leased (controller, stream) pair + */ +struct dmdma_lease; +typedef struct dmdma_lease *dmdma_lease_t; + +/** + * @brief Lease event callback + * + * Fired from interrupt context for dmdma_event_complete/_half_complete/_error + * (whatever the hardware reported) or dmdma_event_timeout (the + * dmdma_transfer_config_t.timeout_ms watchdog armed at dmdma_lease_start() + * elapsed - see dmdma.c), and fired synchronously (from within the calling + * thread) with dmdma_event_aborted by dmdma_lease_abort() itself. + * + * @param lease The lease the event belongs to (the same value _lease_acquire() returned) + * @param event Which event(s) fired + * @param user_ptr Caller-owned pointer supplied to dmdma_lease_set_callback(), unchanged + */ +typedef void (*dmdma_lease_callback_t)(dmdma_lease_t lease, dmdma_event_t event, void *user_ptr); + +/* --- Reservation --- + * + * Fails (returns NULL) if controller/stream is out of range for this build, + * or the stream is already reserved through any API combination. + */ +dmod_dmdma_dif(1.0, dmdma_lease_t, _lease_acquire, ( dmdma_controller_t controller, dmdma_stream_t stream ) ); + +/** + * Release the lease: aborts any transfer in flight, disables the interrupt + * routing for the stream (so a racing ISR cannot fire into it once this + * returns), then returns the stream to the free pool. A driver must release + * every lease it holds before it unloads - dmdma has no way to know when a + * consumer module goes away, so nothing else guarantees a stale callback + * can never be invoked after that point. + */ +dmod_dmdma_dif(1.0, void, _lease_release, ( dmdma_lease_t lease ) ); + +/* --- Transfer control --- + * + * Same semantics and validation as dmdma_ioctl_cmd_start_transfer / + * dmdma_ioctl_cmd_stop_transfer on the device-node path - controller, + * request-line/direction consistency, address alignment, and element count + * are all checked before anything touches the port. + */ +dmod_dmdma_dif(1.0, int, _lease_start, ( dmdma_lease_t lease, const dmdma_transfer_config_t *config ) ); + +/** + * Abort whatever transfer is in flight; safe to call when idle. Fires the + * lease's callback with dmdma_event_aborted, but only if a transfer was + * actually in flight at the time of the call - aborting an already-idle + * (or already-finished) stream is a no-op and does not fire a spurious + * event on top of whatever dmdma_event_complete already fired for it. + */ +dmod_dmdma_dif(1.0, void, _lease_abort, ( dmdma_lease_t lease ) ); +dmod_dmdma_dif(1.0, bool, _lease_is_busy, ( dmdma_lease_t lease ) ); +dmod_dmdma_dif(1.0, size_t, _lease_get_remaining, ( dmdma_lease_t lease ) ); + +/** + * Register (callback != NULL) or remove (callback == NULL) this lease's + * event callback. + */ +dmod_dmdma_dif(1.0, int, _lease_set_callback, ( dmdma_lease_t lease, dmdma_lease_callback_t callback, void *user_ptr ) ); + +#ifdef __cplusplus +} +#endif + +#endif // DMDMA_LEASE_H diff --git a/include/dmdma_types.h b/include/dmdma_types.h index f443a9c..791994d 100644 --- a/include/dmdma_types.h +++ b/include/dmdma_types.h @@ -86,6 +86,12 @@ typedef struct bool circular; /**< Wrap back to the start instead of stopping at element_count */ dmdma_priority_t priority; /**< Arbitration priority against other active streams */ size_t element_count; /**< Number of elements (not bytes) to transfer */ + uint32_t timeout_ms; /**< Abort the transfer and fire dmdma_event_timeout if it hasn't + * reached dmdma_event_complete within this many milliseconds of + * being started (dmosi_timer-backed, see dmdma.c); 0 = no + * watchdog, wait indefinitely. For a circular transfer this is a + * total-run-time cap (dmdma_event_half_complete wrap points do + * not reset it) rather than a per-element deadline. */ } dmdma_transfer_config_t; /* @@ -110,6 +116,11 @@ typedef enum dmdma_event_complete = (1 << 0), /**< element_count elements transferred (or a circular wrap point) */ dmdma_event_half_complete = (1 << 1), /**< Half of element_count transferred - circular transfers only */ dmdma_event_error = (1 << 2), /**< Transfer, FIFO, or direct-mode error reported by the hardware */ + dmdma_event_aborted = (1 << 3), /**< Fired synchronously by dmdma_lease_abort() / dmdma_ioctl_cmd_stop_transfer + * while a transfer was actually in flight - an explicit, caller-initiated cancel */ + dmdma_event_timeout = (1 << 4), /**< dmdma_transfer_config_t.timeout_ms elapsed before the transfer + * completed - dmdma itself stopped the stream via the dmosi_timer + * armed at dmdma_lease_start()/dmdma_ioctl_cmd_start_transfer time */ } dmdma_event_t; /** diff --git a/src/dmdma.c b/src/dmdma.c index 5881fed..0c95051 100644 --- a/src/dmdma.c +++ b/src/dmdma.c @@ -4,6 +4,7 @@ #include "dmdma_port.h" #include "dmdrvi.h" #include "dmini.h" +#include "dmosi.h" #include @@ -20,17 +21,62 @@ #define DMDMA_FLUSH_TIMEOUT_ITERATIONS 1000000UL +/** + * @brief Which API reserved a stream slot + * + * A slot is reserved through exactly one of two mutually exclusive paths - + * dmdrvi_open() (device node) or dmdma_lease_acquire() (direct call) - and + * this tag is what makes the two paths share one reservation pool instead of + * fighting over separate bookkeeping. It also lets internal_interrupt_handler() + * invoke the right callback shape (dmdrvi's vs. the lease's) for a given + * stream, and stops a handle obtained from one API from being fed into the + * other's calls (slot_from_handle()/slot_from_lease() each only accept their + * own owner tag). + */ +typedef enum +{ + dmdma_slot_owner_none = 0, + dmdma_slot_owner_dmdrvi, + dmdma_slot_owner_lease, +} dmdma_slot_owner_t; + /** * @brief One DMA stream within a controller * - * This is also the opaque handle dmdrvi_open()/_close() hand back and forth - - * a stream is reserved for the lifetime of the open() that returned it. + * This is also the opaque handle dmdrvi_open()/_close() and + * dmdma_lease_acquire()/_release() hand back and forth - a stream is + * reserved for the lifetime of whichever call reserved it. */ typedef struct { - dmdma_stream_t stream; /**< Stream index within the controller */ - bool reserved; /**< Currently open (== reserved on the port) */ - dmdma_interrupt_handler_t handler; /**< Completion/error callback, NULL = none registered */ + dmdma_stream_t stream; /**< Stream index within the controller */ + bool reserved; /**< Currently open/leased (== reserved on the port) */ + dmdma_slot_owner_t owner; /**< Which API currently holds this slot */ + union + { + dmdma_interrupt_handler_t dmdrvi_handler; /**< owner == dmdma_slot_owner_dmdrvi */ + struct + { + dmdma_lease_callback_t callback; + void *user_ptr; + } lease; /**< owner == dmdma_slot_owner_lease */ + } cb; + /* Lazily created the first time a transfer on this slot sets + * timeout_ms != 0 (see arm_timeout()/disarm_timeout()), then reused for + * the rest of this slot's lifetime regardless of how many + * open/close or acquire/release cycles it goes through - only + * dmdrvi_free() (whole-context teardown) ever destroys it, so no + * open/close/acquire/release path may reset this back to NULL without + * leaking the dmosi_timer_t it points to. */ + dmosi_timer_t timer; + /* Whether the transfer currently armed on this stream is circular - the + * hardware's "complete" flag fires at *every* wrap point for a circular + * transfer, not just a final one (see stm32_dma_stream_irq()), so + * internal_interrupt_handler() needs this to tell a genuinely-finished + * one-shot transfer apart from a circular one that's merely still + * going - otherwise the watchdog armed by arm_timeout() gets disarmed + * on the very first wrap instead of running for its full timeout_ms. */ + bool circular; } dmdma_stream_slot_t; /** @@ -55,7 +101,10 @@ static int is_valid_context(dmdrvi_context_t context) } /* Validates that `handle` is one of `context`'s own stream slots and that it - * is currently open, before it gets dereferenced. */ + * is currently open *through dmdrvi*, before it gets dereferenced - a slot + * reserved via dmdma_lease_acquire() is the same C type but must never be + * driven through the dmdrvi ioctl/flush/etc. path (see slot_from_lease() + * for the mirror-image check on the lease side). */ static dmdma_stream_slot_t *slot_from_handle(dmdrvi_context_t context, void *handle) { if (!is_valid_context(context) || handle == NULL) @@ -69,7 +118,103 @@ static dmdma_stream_slot_t *slot_from_handle(dmdrvi_context_t context, void *han return NULL; } - return slot->reserved ? slot : NULL; + return (slot->reserved && slot->owner == dmdma_slot_owner_dmdrvi) ? slot : NULL; +} + +/* Finds which controller's context owns `slot` - the lease API identifies a + * stream purely by the opaque dmdma_lease_t it was handed back (unlike + * dmdrvi, which always carries context alongside handle), so this is how + * lease calls recover the controller number they need for port calls. + * DMDMA_MAX_CONTROLLERS is small (<=4) so a linear scan is fine. */ +static dmdrvi_context_t context_from_slot(dmdma_stream_slot_t *slot) +{ + for (uint32_t i = 0; i < DMDMA_MAX_CONTROLLERS; i++) + { + dmdrvi_context_t context = g_contexts[i]; + if (is_valid_context(context) && slot >= &context->streams[0] && slot < &context->streams[context->stream_count]) + { + return context; + } + } + + return NULL; +} + +/* Validates that `lease` is a real, currently-leased stream slot before it + * gets dereferenced - the mirror image of slot_from_handle() for the lease + * API (see the comment there for why the owner check matters). */ +static dmdma_stream_slot_t *slot_from_lease(dmdma_lease_t lease) +{ + dmdma_stream_slot_t *slot = (dmdma_stream_slot_t *)lease; + if (slot == NULL || context_from_slot(slot) == NULL) + { + return NULL; + } + + return (slot->reserved && slot->owner == dmdma_slot_owner_lease) ? slot : NULL; +} + +/* Shared by dmdma_ioctl_cmd_start_transfer and dmdma_lease_start() so both + * reservation paths reject the same malformed configs the same way, rather + * than relying on whatever a given port's _stream_start() happens to check. + */ +static int validate_transfer_config(dmdma_controller_t controller, const dmdma_transfer_config_t *config) +{ + if (config == NULL) + { + return -EINVAL; + } + if (config->element_count == 0) + { + return -EINVAL; + } + if (config->destination_address == NULL) + { + return -EINVAL; + } + + if (config->direction == dmdma_direction_memory_to_memory) + { + if (!dmdma_port_supports_memory_to_memory(controller)) + { + return -ENOTSUP; + } + if (config->request != DMDMA_REQUEST_NONE) + { + /* mem-to-mem needs no peripheral trigger - a caller supplying + * one is almost certainly confused about which address is "the + * peripheral one" for this direction. */ + return -EINVAL; + } + if (config->source_address == NULL) + { + return -EINVAL; + } + } + else + { + if (config->source_address == NULL) + { + return -EINVAL; + } + if (config->request == DMDMA_REQUEST_NONE) + { + /* A peripheral direction with no request line can never fire - + * almost certainly a caller that forgot to set it. */ + return -EINVAL; + } + } + + if (((uintptr_t)config->source_address % (uintptr_t)config->source_width) != 0) + { + return -EINVAL; + } + if (((uintptr_t)config->destination_address % (uintptr_t)config->destination_width) != 0) + { + return -EINVAL; + } + + return 0; } /* [dmdma] is the fast path; anything else falls back to whichever section @@ -93,6 +238,89 @@ static const char *detect_config_section(dmini_context_t config) return "dmdma"; } +/* ---- Transfer watchdog (dmosi_timer-backed) ---- + * + * dmdma_transfer_config_t.timeout_ms, armed by validate-and-start on either + * reservation path. The timer object itself is created lazily and then + * kept for the rest of the slot's lifetime (see the comment on + * dmdma_stream_slot_t.timer) - only dmdrvi_free() ever destroys it. + */ +static void deliver_event(dmdrvi_context_t context, dmdma_stream_slot_t *slot, dmdma_event_t event) +{ + switch (slot->owner) + { + case dmdma_slot_owner_dmdrvi: + if (slot->cb.dmdrvi_handler != NULL) + { + slot->cb.dmdrvi_handler(context, slot, event); + } + break; + + case dmdma_slot_owner_lease: + if (slot->cb.lease.callback != NULL) + { + slot->cb.lease.callback((dmdma_lease_t)slot, event, slot->cb.lease.user_ptr); + } + break; + + default: + break; + } +} + +static void dmdma_timeout_fired(void *arg); + +static void disarm_timeout(dmdma_stream_slot_t *slot) +{ + if (slot->timer != NULL) + { + dmosi_timer_stop(slot->timer); + } +} + +static int arm_timeout(dmdma_stream_slot_t *slot, uint32_t timeout_ms) +{ + if (timeout_ms == 0) + { + disarm_timeout(slot); + return 0; + } + + if (slot->timer == NULL) + { + slot->timer = dmosi_timer_create(dmdma_timeout_fired, slot, timeout_ms, false /* one-shot */); + if (slot->timer == NULL) + { + return -ENOMEM; + } + } + else + { + dmosi_timer_stop(slot->timer); + dmosi_timer_set_period(slot->timer, timeout_ms); + } + + return dmosi_timer_start(slot->timer); +} + +/* dmosi_timer callback (fires from timer/interrupt context, same as a real + * hardware DMA event) - stops the stream and delivers dmdma_event_timeout to + * whichever owner is still holding it. If the slot was released/closed in + * the meantime, arm_timeout()'s disarm_timeout() call already stopped this + * timer, so there is no unsynchronized-teardown window to worry about here. */ +static void dmdma_timeout_fired(void *arg) +{ + dmdma_stream_slot_t *slot = (dmdma_stream_slot_t *)arg; + dmdrvi_context_t context = context_from_slot(slot); + if (context == NULL || !slot->reserved) + { + return; + } + + dmdma_port_stream_stop(context->config.controller, slot->stream); + deliver_event(context, slot, dmdma_event_timeout); +} + /* ---- Interrupt dispatch ---- * * A single dispatcher is registered with the port once, at dmod_init() - @@ -119,10 +347,29 @@ static void internal_interrupt_handler(void *user_ptr, dmdma_controller_t contro } dmdma_stream_slot_t *slot = &context->streams[stream]; - if (slot->reserved && slot->handler != NULL) + if (!slot->reserved) { - slot->handler(context, slot, event); + return; + } + + /* A terminal event settles the watchdog race in the transfer's favor - + * no point letting a timeout fire moments later for a transfer that + * already finished (or already reported its own error). dmdma_event_complete + * is only terminal for a one-shot transfer - a circular one fires it at + * *every* wrap, so disarming there too would stop the watchdog after its + * first lap instead of running for its full timeout_ms (see + * dmdma_stream_slot_t.circular and timeout_ms's doc comment in + * dmdma_types.h). */ + if (event & dmdma_event_error) + { + disarm_timeout(slot); } + else if ((event & dmdma_event_complete) && !slot->circular) + { + disarm_timeout(slot); + } + + deliver_event(context, slot, event); } /* ---- DMOD lifecycle ---- */ @@ -197,7 +444,10 @@ dmod_dmdrvi_dif_api_declaration(2.0, dmdma, dmdrvi_context_t, _create, { context->streams[i].stream = i; context->streams[i].reserved = false; - context->streams[i].handler = NULL; + context->streams[i].owner = dmdma_slot_owner_none; + context->streams[i].cb.dmdrvi_handler = NULL; + context->streams[i].timer = NULL; + context->streams[i].circular = false; } g_contexts[controller] = context; @@ -238,6 +488,11 @@ dmod_dmdrvi_dif_api_declaration(2.0, dmdma, void, _free, ( dmdrvi_context_t cont dmdma_port_stream_stop(context->config.controller, i); dmdma_port_stream_release(context->config.controller, i); } + if (context->streams[i].timer != NULL) + { + dmosi_timer_destroy(context->streams[i].timer); + context->streams[i].timer = NULL; + } } g_contexts[context->config.controller] = NULL; @@ -276,7 +531,8 @@ dmod_dmdrvi_dif_api_declaration(2.0, dmdma, void*, _open, } slot->reserved = true; - slot->handler = NULL; + slot->owner = dmdma_slot_owner_dmdrvi; + slot->cb.dmdrvi_handler = NULL; return slot; } @@ -288,10 +544,12 @@ dmod_dmdrvi_dif_api_declaration(2.0, dmdma, void, _close, ( dmdrvi_context_t con return; } + disarm_timeout(slot); dmdma_port_stream_stop(context->config.controller, slot->stream); dmdma_port_stream_release(context->config.controller, slot->stream); slot->reserved = false; - slot->handler = NULL; + slot->owner = dmdma_slot_owner_none; + slot->cb.dmdrvi_handler = NULL; } dmod_dmdrvi_dif_api_declaration(2.0, dmdma, dmdrvi_ssize_t, _read, @@ -352,25 +610,36 @@ dmod_dmdrvi_dif_api_declaration(2.0, dmdma, int, _ioctl, { case dmdma_ioctl_cmd_start_transfer: { - if (arg == NULL) + const dmdma_transfer_config_t *cfg = (const dmdma_transfer_config_t *)arg; + int rc = validate_transfer_config(controller, cfg); + if (rc != 0) { - return -EINVAL; + DMOD_LOG_ERROR("dmdma: rejecting transfer config on controller %u (rc=%d)\n", (unsigned)controller, rc); + return rc; } - const dmdma_transfer_config_t *cfg = (const dmdma_transfer_config_t *)arg; - if (cfg->direction == dmdma_direction_memory_to_memory && - !dmdma_port_supports_memory_to_memory(controller)) + slot->circular = cfg->circular; + rc = dmdma_port_stream_start(controller, slot->stream, cfg); + if (rc == 0) { - DMOD_LOG_ERROR("dmdma: controller %u cannot do memory-to-memory transfers\n", (unsigned)controller); - return -ENOTSUP; + rc = arm_timeout(slot, cfg->timeout_ms); } - - return dmdma_port_stream_start(controller, slot->stream, cfg); + return rc; } case dmdma_ioctl_cmd_stop_transfer: + { + /* Same was_busy-before-stop reasoning as dmdma_lease_abort() - + * see the comment there. */ + bool was_busy = dmdma_port_stream_is_busy(controller, slot->stream); + disarm_timeout(slot); dmdma_port_stream_stop(controller, slot->stream); + if (was_busy) + { + deliver_event(context, slot, dmdma_event_aborted); + } return 0; + } case dmdma_ioctl_cmd_is_busy: if (arg == NULL) return -EINVAL; @@ -383,7 +652,7 @@ dmod_dmdrvi_dif_api_declaration(2.0, dmdma, int, _ioctl, return 0; case dmdma_ioctl_cmd_set_interrupt_handler: - slot->handler = (arg != NULL) ? *(dmdma_interrupt_handler_t *)arg : NULL; + slot->cb.dmdrvi_handler = (arg != NULL) ? *(dmdma_interrupt_handler_t *)arg : NULL; return 0; default: @@ -424,3 +693,178 @@ dmod_dmdrvi_dif_api_declaration(2.0, dmdma, int, _stat, stat->mode = 0666; return 0; } + +/* ---- DMA lease API (see dmdma_lease.h) ---- + * + * dmod_dmdma_dif_api_declaration() only emits a real .dmod.inputs + * registration when the *implementing* module differs from the module that + * *defines* the DIF family - see dmdma_defs.h's `#ifdef DMOD_dmdma` branch, + * which assumes a DIF is always implemented by some other module and + * degrades to a plain, unregistered function declaration otherwise (the + * lookup-free "you already know it's your own function" case that macro + * was written for). Here dmdma implements its *own* "dmdma" DIF family, so + * that assumption doesn't hold and every function below needs its + * registration written out by hand - otherwise Dmod_GetDifFunction() can + * never find these at runtime, no matter how correct the function bodies + * are (confirmed on real hardware: dmdma loads fine, but every consumer's + * Dmod_GetDifFunction() call returns NULL until this macro is used instead + * of the plain one above). */ +#define DMDMA_DIF_API_DECLARATION(VERSION, RET, NAME, PARAMS) \ + RET DMOD_MAKE_DIF_API_FUNCTION_NAME(dmdma, dmdma, NAME) PARAMS; \ + _DMOD_DIF_API_REGISTRATION(dmdma, dmdma, DMOD_MAKE_VERSION(VERSION,0.1), NAME) \ + RET DMOD_MAKE_DIF_API_FUNCTION_NAME(dmdma, dmdma, NAME) PARAMS + +DMDMA_DIF_API_DECLARATION(1.0, dmdma_lease_t, _lease_acquire, + ( dmdma_controller_t controller, dmdma_stream_t stream )) +{ + if (controller >= DMDMA_MAX_CONTROLLERS) + { + return NULL; + } + + dmdrvi_context_t context = g_contexts[controller]; + if (!is_valid_context(context) || stream >= context->stream_count) + { + DMOD_LOG_ERROR("dmdma: invalid lease stream %u/%u\n", (unsigned)controller, (unsigned)stream); + return NULL; + } + + dmdma_stream_slot_t *slot = &context->streams[stream]; + if (slot->reserved) + { + DMOD_LOG_ERROR("dmdma: stream %u/%u already reserved\n", (unsigned)controller, (unsigned)stream); + return NULL; + } + + if (dmdma_port_stream_acquire(controller, slot->stream) != 0) + { + DMOD_LOG_ERROR("dmdma: failed to acquire stream %u/%u for lease\n", (unsigned)controller, (unsigned)stream); + return NULL; + } + + slot->reserved = true; + slot->owner = dmdma_slot_owner_lease; + slot->cb.lease.callback = NULL; + slot->cb.lease.user_ptr = NULL; + return (dmdma_lease_t)slot; +} + +DMDMA_DIF_API_DECLARATION(1.0, void, _lease_release, ( dmdma_lease_t lease )) +{ + dmdma_stream_slot_t *slot = slot_from_lease(lease); + if (slot == NULL) + { + return; + } + + dmdrvi_context_t context = context_from_slot(slot); + dmdma_controller_t controller = context->config.controller; + + /* Stop the hardware, then disable/reset it via the port, *then* clear + * the software reservation - the same ordering _close() uses, so a + * racing ISR can never observe reserved==true with the stream already + * disabled, or vice versa. Disarming (not destroying, see + * dmdma_stream_slot_t.timer) the watchdog first means a timeout that + * was already in flight can never fire into this slot after release. */ + disarm_timeout(slot); + dmdma_port_stream_stop(controller, slot->stream); + dmdma_port_stream_release(controller, slot->stream); + + slot->reserved = false; + slot->owner = dmdma_slot_owner_none; + slot->cb.lease.callback = NULL; + slot->cb.lease.user_ptr = NULL; +} + +DMDMA_DIF_API_DECLARATION(1.0, int, _lease_start, + ( dmdma_lease_t lease, const dmdma_transfer_config_t *config )) +{ + dmdma_stream_slot_t *slot = slot_from_lease(lease); + if (slot == NULL) + { + return -EINVAL; + } + + dmdrvi_context_t context = context_from_slot(slot); + dmdma_controller_t controller = context->config.controller; + + int rc = validate_transfer_config(controller, config); + if (rc != 0) + { + DMOD_LOG_ERROR("dmdma: rejecting lease transfer config on controller %u (rc=%d)\n", (unsigned)controller, rc); + return rc; + } + + slot->circular = config->circular; + rc = dmdma_port_stream_start(controller, slot->stream, config); + if (rc == 0) + { + rc = arm_timeout(slot, config->timeout_ms); + } + return rc; +} + +DMDMA_DIF_API_DECLARATION(1.0, void, _lease_abort, ( dmdma_lease_t lease )) +{ + dmdma_stream_slot_t *slot = slot_from_lease(lease); + if (slot == NULL) + { + return; + } + + dmdrvi_context_t context = context_from_slot(slot); + dmdma_controller_t controller = context->config.controller; + + /* Read busy state *before* stopping - stopping an already-idle stream is + * a harmless no-op, but firing "aborted" for it would misreport a + * transfer that had already finished (and already got its own + * dmdma_event_complete) as cancelled instead. */ + bool was_busy = dmdma_port_stream_is_busy(controller, slot->stream); + disarm_timeout(slot); + dmdma_port_stream_stop(controller, slot->stream); + + /* Fired synchronously (there is no hardware event for a software abort) - + * see dmdma_event_aborted in dmdma_types.h. */ + if (was_busy) + { + deliver_event(context, slot, dmdma_event_aborted); + } +} + +DMDMA_DIF_API_DECLARATION(1.0, bool, _lease_is_busy, ( dmdma_lease_t lease )) +{ + dmdma_stream_slot_t *slot = slot_from_lease(lease); + if (slot == NULL) + { + return false; + } + + dmdrvi_context_t context = context_from_slot(slot); + return dmdma_port_stream_is_busy(context->config.controller, slot->stream); +} + +DMDMA_DIF_API_DECLARATION(1.0, size_t, _lease_get_remaining, ( dmdma_lease_t lease )) +{ + dmdma_stream_slot_t *slot = slot_from_lease(lease); + if (slot == NULL) + { + return 0; + } + + dmdrvi_context_t context = context_from_slot(slot); + return dmdma_port_stream_get_remaining(context->config.controller, slot->stream); +} + +DMDMA_DIF_API_DECLARATION(1.0, int, _lease_set_callback, + ( dmdma_lease_t lease, dmdma_lease_callback_t callback, void *user_ptr )) +{ + dmdma_stream_slot_t *slot = slot_from_lease(lease); + if (slot == NULL) + { + return -EINVAL; + } + + slot->cb.lease.callback = callback; + slot->cb.lease.user_ptr = user_ptr; + return 0; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index cd7180b..054cdf5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2,12 +2,14 @@ set(DMOD_MODULE_VERSION ${PROJECT_VERSION}) set(DMOD_AUTHOR_NAME "Patryk Kubiak") set(DMOD_STACK_SIZE 2048) -# Two independent on-target diagnostics (Applications, not -# dmod_add_test()/DMOD_TEST_STEP unit tests - both need argv and are meant -# to be run manually from the shell). Both allocate their transfer buffers -# from the existing dmheap context named "dma" (dmheap_get_context_by_name(), -# added in dmheap 0.6 - see dmdma_test_heap.h) - neither tool sets that -# context up itself. +# Three independent on-target diagnostics (Applications, not +# dmod_add_test()/DMOD_TEST_STEP unit tests - all need argv and are meant +# to be run manually from the shell). dmdma_test_port/_dev allocate their +# transfer buffers from the existing dmheap context named "dma" +# (dmheap_get_context_by_name(), added in dmheap 0.6 - see +# dmdma_test_heap.h) - neither sets that context up itself, so both require +# a board whose linker script actually carves out a "dma" MEMORY region +# (see linker/common.ld in dmod-boot) - not every board does. # dmdma_test_port talks to dmdma_port.h directly (the same Built-in API # dmdma.c itself calls) - only dmdma_port needs to be bundled alongside it, @@ -39,3 +41,21 @@ target_include_directories(dmdma_test_dev PRIVATE dmod_link_modules(dmdma_test_dev dmheap@>=0.6 ) + +# dmdma_test_lease exercises the dmdma_lease.h DIF the way a peripheral +# driver (dmuart, dmspi, dmsdio, ...) would: resolved by module name at +# runtime (Dmod_GetModuleContext("dmdma") + Dmod_GetDifFunction()), not +# through a device node. It links against dmdma_if (the same interface +# target dmdma_test_port uses for dmdma_port_if) purely to get dmdma.h and +# the generated dmdma_defs.h on its include path at compile time - it does +# not call into dmdma directly, only through the DIF lookup at runtime. Its +# transfer buffers are plain static arrays (not the "dma" dmheap context +# dmdma_test_port/_dev need) so it runs on any board regardless of whether +# one has been carved out. +dmod_add_executable(dmdma_test_lease ${DMOD_MODULE_VERSION} + dmdma_test_lease.c +) + +target_link_libraries(dmdma_test_lease + dmdma_if +) diff --git a/tests/dmdma_test_dev.c b/tests/dmdma_test_dev.c index 61447a5..f1e222c 100644 --- a/tests/dmdma_test_dev.c +++ b/tests/dmdma_test_dev.c @@ -38,6 +38,7 @@ static int run_transfer_test(void *handle, const uint8_t *src, uint8_t *dst) transfer.circular = false; transfer.priority = dmdma_priority_medium; transfer.element_count = DMDMA_TEST_BYTES; + transfer.timeout_ms = 0; /* no watchdog for this smoke test */ Dmod_Printf("Starting memory-to-memory transfer of %u byte(s) via ioctl()...\n", DMDMA_TEST_BYTES); if (Dmod_Ioctl(handle, dmdma_ioctl_cmd_start_transfer, &transfer) != 0) diff --git a/tests/dmdma_test_lease.c b/tests/dmdma_test_lease.c new file mode 100644 index 0000000..2d4add7 --- /dev/null +++ b/tests/dmdma_test_lease.c @@ -0,0 +1,376 @@ +/* Needed so this translation unit gets real storage for the DIF signature + * string constants (dmod_dmdma_lease_*_sig) it references below, not just + * an extern declaration - see dmdevfs.c/dmvfs.c for the same pattern on + * every other DIF consumer in the ecosystem. */ +#define ENABLE_DIF_REGISTRATIONS ON +#include "dmod.h" +#include "dmdma.h" +#include +#include + +/** + * @brief dmdma_test_lease - on-target smoke test for the DMA lease API + * (dmdma_lease.h), exercised exactly the way a peripheral driver + * (dmuart, dmspi, dmsdio, ...) is meant to use it: by resolving + * "dmdma" as a DIF at runtime, the same way dmdevfs.c/dmvfs.c resolve + * dmdrvi/dmfsi - not by linking against dmdma or going through a + * device node. + * + * Usage: + * dmdma_test_lease + * e.g. dmdma_test_lease 1 0 (DMA2, the mem-to-mem capable controller, + * stream 0) + * + * Unlike dmdma_test_dev.c (device node / dmdrvi) and dmdma_test_port.c (raw + * dmdma_port.h), this tool never touches dmdevfs, dmdrvi, or dmdma_port - + * only "dmdma" itself needs to be loaded. It also doesn't need dmdma_test_dev/ + * _test_port's shared "dma" dmheap context - a plain static buffer is a + * perfectly valid memory-to-memory DMA endpoint, and not every board carves + * out a dedicated DMA heap region (see linker/common.ld's optional "dma" + * MEMORY region). + */ + +/* Large enough that a circular transfer's wrap period is milliseconds, not + * microseconds - a tiny buffer wraps (and re-fires dmdma_event_complete) so + * fast in circular mode that the resulting interrupt storm can starve every + * other task on the board (confirmed on real STM32F746G-DISCO hardware: + * a 64-byte circular buffer here reliably wedged the system). The one-shot + * completion test earlier only ever fires a single interrupt regardless of + * size, so this size increase is free for it too. */ +#define DMDMA_TEST_BYTES 16384U +#define DMDMA_TEST_BUSY_POLL_ITERATIONS 1000000UL +#define DMDMA_TEST_TIMEOUT_MS 50U +#define DMDMA_TEST_TIMEOUT_MARGIN_MS 100U + +static volatile dmdma_event_t s_last_event = (dmdma_event_t)0; +static volatile int s_callback_count = 0; +static void *s_expected_user_ptr = NULL; +static volatile bool s_user_ptr_mismatch = false; + +static void on_lease_event(dmdma_lease_t lease, dmdma_event_t event, void *user_ptr) +{ + (void)lease; + s_last_event = event; + s_callback_count++; + if (user_ptr != s_expected_user_ptr) + { + s_user_ptr_mismatch = true; + } +} + +typedef struct +{ + dmod_dmdma_lease_acquire_t acquire; + dmod_dmdma_lease_release_t release; + dmod_dmdma_lease_start_t start; + dmod_dmdma_lease_abort_t abort; + dmod_dmdma_lease_is_busy_t is_busy; + dmod_dmdma_lease_get_remaining_t get_remaining; + dmod_dmdma_lease_set_callback_t set_callback; +} lease_api_t; + +/* Resolves every lease function by name, exactly the way a real consumer + * must - "dmdma" is an optional dependency, so every step here can fail if + * it isn't loaded, without that being a bug in either module. */ +static int resolve_lease_api(lease_api_t *api) +{ + Dmod_Context_t *dma = Dmod_GetModuleContext("dmdma"); + if (dma == NULL) + { + Dmod_Printf("ERROR: 'dmdma' module is not loaded\n"); + return -1; + } + + api->acquire = (dmod_dmdma_lease_acquire_t)Dmod_GetDifFunction(dma, dmod_dmdma_lease_acquire_sig); + api->release = (dmod_dmdma_lease_release_t)Dmod_GetDifFunction(dma, dmod_dmdma_lease_release_sig); + api->start = (dmod_dmdma_lease_start_t)Dmod_GetDifFunction(dma, dmod_dmdma_lease_start_sig); + api->abort = (dmod_dmdma_lease_abort_t)Dmod_GetDifFunction(dma, dmod_dmdma_lease_abort_sig); + api->is_busy = (dmod_dmdma_lease_is_busy_t)Dmod_GetDifFunction(dma, dmod_dmdma_lease_is_busy_sig); + api->get_remaining = (dmod_dmdma_lease_get_remaining_t)Dmod_GetDifFunction(dma, dmod_dmdma_lease_get_remaining_sig); + api->set_callback = (dmod_dmdma_lease_set_callback_t)Dmod_GetDifFunction(dma, dmod_dmdma_lease_set_callback_sig); + + if (api->acquire == NULL || api->release == NULL || api->start == NULL || api->abort == NULL || + api->is_busy == NULL || api->get_remaining == NULL || api->set_callback == NULL) + { + Dmod_Printf("ERROR: 'dmdma' does not implement the lease DIF\n"); + return -1; + } + + return 0; +} + +/* This tool targets DMOD_USE_STDLIB=OFF builds (see cortex-m7's + * tools-cfg.cmake) - no atoi()/strtoul(), so argv is parsed by hand. */ +static uint32_t parse_uint(const char *s) +{ + uint32_t value = 0; + while (*s >= '0' && *s <= '9') + { + value = (value * 10U) + (uint32_t)(*s - '0'); + s++; + } + return value; +} + +static bool wait_until_idle(const lease_api_t *api, dmdma_lease_t lease) +{ + for (uint32_t i = 0; i < DMDMA_TEST_BUSY_POLL_ITERATIONS; i++) + { + if (!api->is_busy(lease)) + { + return true; + } + } + return false; +} + +static uint8_t s_src[DMDMA_TEST_BYTES]; +static uint8_t s_dst[DMDMA_TEST_BYTES]; + +int main(int argc, char *argv[]) +{ + Dmod_Printf("\n=== DMDMA Lease API Smoke Test ===\n\n"); + + if (argc < 3) + { + Dmod_Printf("Usage: dmdma_test_lease \n"); + Dmod_Printf("e.g. dmdma_test_lease 1 0 (DMA2 stream 0 - mem-to-mem capable)\n\n"); + return -1; + } + + dmdma_controller_t controller = (dmdma_controller_t)parse_uint(argv[1]); + dmdma_stream_t stream = (dmdma_stream_t)parse_uint(argv[2]); + + lease_api_t api; + if (resolve_lease_api(&api) != 0) + { + return -1; + } + + uint8_t *src = s_src; + uint8_t *dst = s_dst; + dmdma_lease_t lease = NULL; + int result = -1; + + for (uint32_t i = 0; i < DMDMA_TEST_BYTES; i++) + { + src[i] = (uint8_t)((i * 5U) + 3U); + dst[i] = 0; + } + + /* --- 1. Acquire --- */ + + lease = api.acquire(controller, stream); + if (lease == NULL) + { + Dmod_Printf("ERROR: could not acquire controller %u stream %u\n", (unsigned)controller, (unsigned)stream); + goto cleanup; + } + Dmod_Printf("Acquired lease on controller %u stream %u\n", (unsigned)controller, (unsigned)stream); + + /* --- 2. The same stream cannot be acquired twice, through any API combination --- */ + + { + dmdma_lease_t second = api.acquire(controller, stream); + if (second != NULL) + { + Dmod_Printf("FAIL: double-acquire of controller %u stream %u unexpectedly succeeded\n", + (unsigned)controller, (unsigned)stream); + api.release(second); + goto cleanup_lease; + } + Dmod_Printf("PASS: double-acquire correctly rejected\n"); + } + + /* --- 3. Idle abort must not fire a callback - nothing was in flight --- */ + + { + int idle_marker = 0xAAAA; + s_expected_user_ptr = &idle_marker; + s_callback_count = 0; + api.set_callback(lease, on_lease_event, &idle_marker); + api.abort(lease); + if (s_callback_count != 0) + { + Dmod_Printf("FAIL: aborting an idle stream fired %d callback(s), expected 0\n", s_callback_count); + goto cleanup_lease; + } + Dmod_Printf("PASS: aborting an idle stream fired no callback\n"); + } + + /* --- 4. A real transfer completes, copies correctly, and notifies the right owner --- */ + + { + int user_marker = 0x1234; + s_expected_user_ptr = &user_marker; + s_callback_count = 0; + s_user_ptr_mismatch = false; + api.set_callback(lease, on_lease_event, &user_marker); + + dmdma_transfer_config_t transfer; + transfer.direction = dmdma_direction_memory_to_memory; + transfer.request = DMDMA_REQUEST_NONE; + transfer.source_address = src; + transfer.destination_address = dst; + transfer.source_width = dmdma_data_width_byte; + transfer.destination_width = dmdma_data_width_byte; + transfer.source_increment = true; + transfer.destination_increment = true; + transfer.circular = false; + transfer.priority = dmdma_priority_medium; + transfer.element_count = DMDMA_TEST_BYTES; + transfer.timeout_ms = 0; /* no watchdog for this run */ + + Dmod_Printf("Starting memory-to-memory transfer of %u byte(s) via the lease API...\n", DMDMA_TEST_BYTES); + if (api.start(lease, &transfer) != 0) + { + Dmod_Printf("ERROR: dmdma_lease_start() failed\n"); + goto cleanup_lease; + } + + if (!wait_until_idle(&api, lease)) + { + Dmod_Printf("FAIL: transfer did not complete (timed out polling is_busy)\n"); + goto cleanup_lease; + } + + size_t remaining = api.get_remaining(lease); + if (remaining != 0) + { + Dmod_Printf("FAIL: %u byte(s) reported remaining after completion\n", (unsigned)remaining); + goto cleanup_lease; + } + + for (uint32_t i = 0; i < DMDMA_TEST_BYTES; i++) + { + if (src[i] != dst[i]) + { + Dmod_Printf("FAIL: mismatch at offset %u (src=0x%02X, dst=0x%02X)\n", i, src[i], dst[i]); + goto cleanup_lease; + } + } + + if (s_callback_count < 1 || (s_last_event & dmdma_event_complete) == 0) + { + Dmod_Printf("FAIL: completion callback did not fire with dmdma_event_complete (count=%d, last_event=0x%X)\n", + s_callback_count, (unsigned)s_last_event); + goto cleanup_lease; + } + if (s_user_ptr_mismatch) + { + Dmod_Printf("FAIL: callback received the wrong user_ptr\n"); + goto cleanup_lease; + } + Dmod_Printf("PASS: transfer completed, data verified, callback fired with the right user_ptr\n"); + + /* --- 5. Malformed configs must be rejected before touching hardware --- */ + + dmdma_transfer_config_t bad = transfer; + bad.element_count = 0; + if (api.start(lease, &bad) == 0) + { + Dmod_Printf("FAIL: zero-length transfer was accepted\n"); + goto cleanup_lease; + } + + bad = transfer; + bad.request = 1; /* mem-to-mem must not carry a request line */ + if (api.start(lease, &bad) == 0) + { + Dmod_Printf("FAIL: mem-to-mem transfer with a request line was accepted\n"); + goto cleanup_lease; + } + Dmod_Printf("PASS: malformed transfer configs correctly rejected\n"); + + /* --- 6. Abort while a transfer is genuinely in flight notifies the owner --- */ + /* + * Circular transfers never self-clear their enable bit - is_busy() + * stays true until something explicitly stops the stream - so + * unlike a one-shot transfer, there is no race between "did it + * finish on its own" and "did our abort() call catch it in flight". + */ + transfer.circular = true; + if (api.start(lease, &transfer) != 0) + { + Dmod_Printf("ERROR: dmdma_lease_start() failed on the abort run\n"); + goto cleanup_lease; + } + if (!api.is_busy(lease)) + { + Dmod_Printf("FAIL: circular transfer was not busy right after starting\n"); + goto cleanup_lease; + } + + s_callback_count = 0; + s_last_event = (dmdma_event_t)0; + api.abort(lease); + + if (api.is_busy(lease)) + { + Dmod_Printf("FAIL: stream still busy right after dmdma_lease_abort()\n"); + goto cleanup_lease; + } + if (s_callback_count < 1 || s_last_event != dmdma_event_aborted) + { + Dmod_Printf("FAIL: abort did not fire a dmdma_event_aborted callback (count=%d, last_event=0x%X)\n", + s_callback_count, (unsigned)s_last_event); + goto cleanup_lease; + } + Dmod_Printf("PASS: abort correctly stopped the stream and notified the owner\n"); + + /* --- 7. A watchdog (timeout_ms) auto-aborts and reports dmdma_event_timeout --- */ + /* + * Same circular trick as the abort test above - it never completes + * on its own, so the only way it can stop is the dmosi_timer watchdog + * armed by dmdma_lease_start() firing. + */ + transfer.timeout_ms = DMDMA_TEST_TIMEOUT_MS; + if (api.start(lease, &transfer) != 0) + { + Dmod_Printf("ERROR: dmdma_lease_start() failed on the timeout run\n"); + goto cleanup_lease; + } + + s_callback_count = 0; + s_last_event = (dmdma_event_t)0; + Dmod_ThreadSleep(DMDMA_TEST_TIMEOUT_MS + DMDMA_TEST_TIMEOUT_MARGIN_MS); + + if (api.is_busy(lease)) + { + Dmod_Printf("FAIL: stream still busy %u ms after its %u ms watchdog should have fired\n", + DMDMA_TEST_TIMEOUT_MS + DMDMA_TEST_TIMEOUT_MARGIN_MS, DMDMA_TEST_TIMEOUT_MS); + goto cleanup_lease; + } + if (s_callback_count < 1 || s_last_event != dmdma_event_timeout) + { + Dmod_Printf("FAIL: watchdog did not fire a dmdma_event_timeout callback (count=%d, last_event=0x%X)\n", + s_callback_count, (unsigned)s_last_event); + goto cleanup_lease; + } + Dmod_Printf("PASS: watchdog auto-stopped the stream and reported dmdma_event_timeout\n"); + } + + /* --- 8. Release must return the stream to the free pool cleanly --- */ + + api.release(lease); + lease = api.acquire(controller, stream); + if (lease == NULL) + { + Dmod_Printf("FAIL: could not re-acquire controller %u stream %u after release\n", + (unsigned)controller, (unsigned)stream); + goto cleanup; + } + Dmod_Printf("PASS: stream cleanly re-acquired after release\n"); + + result = 0; + Dmod_Printf("\nPASS: all dmdma lease API checks passed on controller %u stream %u\n\n", + (unsigned)controller, (unsigned)stream); + +cleanup_lease: + if (lease != NULL) + { + api.release(lease); + } +cleanup: + return result; +} diff --git a/tests/dmdma_test_port.c b/tests/dmdma_test_port.c index dee4f98..d17bd36 100644 --- a/tests/dmdma_test_port.c +++ b/tests/dmdma_test_port.c @@ -63,6 +63,7 @@ static int run_transfer_test(const uint8_t *src, uint8_t *dst, size_t bytes_read transfer.circular = false; transfer.priority = dmdma_priority_medium; transfer.element_count = bytes_read; + transfer.timeout_ms = 0; /* no watchdog for this smoke test */ Dmod_Printf("Starting memory-to-memory transfer of %u byte(s)...\n", (unsigned)bytes_read); if (dmdma_port_stream_start(DMDMA_TEST_CONTROLLER, DMDMA_TEST_STREAM, &transfer) != 0) From 297062159568055079215491e000619a5c606cc5 Mon Sep 17 00:00:00 2001 From: Jax Calder Date: Sun, 27 Sep 2026 10:13:24 +0100 Subject: [PATCH 2/2] Use plain Module API instead of DIF for the lease interface dmdma is the one and only implementation of its lease functions - DIF is for a 1:N "multiple modules implement the same interface, discovered at runtime" relationship (e.g. dmramfs/dmffs both implementing dmfsi), which doesn't apply here. The previous DIF-based design needed a hand-written registration workaround because dmod_dmdma_dif_api_declaration() only registers when the implementing module differs from the DIF-defining one - a symptom of forcing the wrong tool onto a 1:1 relationship dmod's own "Module API" pattern already covers directly, with no such caveat. Consumers now just call dmdma_lease_acquire()/etc. directly - no Dmod_GetModuleContext()/Dmod_GetDifFunction() lookup - patched by the loader against dmdma's own registration exactly like dmdma_port_stream_*() already is. Whether a board needs DMA at all becomes a build-time choice (is dmdma bundled into that firmware image), not a per-call runtime check. Re-verified end to end on real STM32F746G-DISCO hardware after the switch; all dmdma_test_lease checks still pass. Co-Authored-By: Claude Sonnet 5 --- include/dmdma_lease.h | 36 ++++++------- src/dmdma.c | 38 +++++-------- tests/CMakeLists.txt | 23 ++++---- tests/dmdma_test_lease.c | 113 +++++++++++---------------------------- 4 files changed, 74 insertions(+), 136 deletions(-) diff --git a/include/dmdma_lease.h b/include/dmdma_lease.h index dd91c14..1ee503a 100644 --- a/include/dmdma_lease.h +++ b/include/dmdma_lease.h @@ -19,17 +19,17 @@ extern "C" { * dmdma.c - a stream already open as a device node cannot also be leased, * and vice versa (see "Streams are devices, not a pool" in docs/README.md). * - * This is a DIF, like dmdrvi itself - dmdma is an optional dependency for a - * peripheral driver (it can always fall back to polled/interrupt-only I/O), - * so a consumer resolves it by name and checks for NULL at every step: - * - * Dmod_Context_t* dma = Dmod_GetModuleContext("dmdma"); - * dmod_dmdma_lease_acquire_t lease_acquire = - * dma ? (dmod_dmdma_lease_acquire_t)Dmod_GetDifFunction(dma, dmod_dmdma_lease_acquire_sig) : NULL; - * dmdma_lease_t lease = lease_acquire ? lease_acquire(controller, stream) : NULL; - * - * (see dmdevfs.c's or dmvfs.c's own DIF consumer code for the established - * pattern this mirrors exactly). + * This is a plain Module API (dmod's README, "Module API" section) - dmdma + * is the one and only implementation of it, not a plugin-style interface + * with several interchangeable backends (that's what DIF is for, e.g. + * dmfsi with dmramfs/dmffs simultaneously). A consumer just includes this + * header and calls the functions directly, e.g. dmdma_lease_acquire(...) - + * no Dmod_GetModuleContext()/Dmod_GetDifFunction() lookup involved, same as + * calling dmdma_port_stream_start() from within dmdma.c itself. Whether a + * given board actually needs DMA is a *build-time* choice (whether "dmdma" + * is declared as a dependency and bundled into that firmware image at + * all - see dmod_link_modules() in the consumer's CMakeLists.txt), not a + * runtime NULL-check. */ /** @@ -58,7 +58,7 @@ typedef void (*dmdma_lease_callback_t)(dmdma_lease_t lease, dmdma_event_t event, * Fails (returns NULL) if controller/stream is out of range for this build, * or the stream is already reserved through any API combination. */ -dmod_dmdma_dif(1.0, dmdma_lease_t, _lease_acquire, ( dmdma_controller_t controller, dmdma_stream_t stream ) ); +dmod_dmdma_api(1.0, dmdma_lease_t, _lease_acquire, ( dmdma_controller_t controller, dmdma_stream_t stream ) ); /** * Release the lease: aborts any transfer in flight, disables the interrupt @@ -68,7 +68,7 @@ dmod_dmdma_dif(1.0, dmdma_lease_t, _lease_acquire, ( dmdma_controller_t controll * consumer module goes away, so nothing else guarantees a stale callback * can never be invoked after that point. */ -dmod_dmdma_dif(1.0, void, _lease_release, ( dmdma_lease_t lease ) ); +dmod_dmdma_api(1.0, void, _lease_release, ( dmdma_lease_t lease ) ); /* --- Transfer control --- * @@ -77,7 +77,7 @@ dmod_dmdma_dif(1.0, void, _lease_release, ( dmdma_lease_t lease ) ); * request-line/direction consistency, address alignment, and element count * are all checked before anything touches the port. */ -dmod_dmdma_dif(1.0, int, _lease_start, ( dmdma_lease_t lease, const dmdma_transfer_config_t *config ) ); +dmod_dmdma_api(1.0, int, _lease_start, ( dmdma_lease_t lease, const dmdma_transfer_config_t *config ) ); /** * Abort whatever transfer is in flight; safe to call when idle. Fires the @@ -86,15 +86,15 @@ dmod_dmdma_dif(1.0, int, _lease_start, ( dmdma_lease_t lease, const d * (or already-finished) stream is a no-op and does not fire a spurious * event on top of whatever dmdma_event_complete already fired for it. */ -dmod_dmdma_dif(1.0, void, _lease_abort, ( dmdma_lease_t lease ) ); -dmod_dmdma_dif(1.0, bool, _lease_is_busy, ( dmdma_lease_t lease ) ); -dmod_dmdma_dif(1.0, size_t, _lease_get_remaining, ( dmdma_lease_t lease ) ); +dmod_dmdma_api(1.0, void, _lease_abort, ( dmdma_lease_t lease ) ); +dmod_dmdma_api(1.0, bool, _lease_is_busy, ( dmdma_lease_t lease ) ); +dmod_dmdma_api(1.0, size_t, _lease_get_remaining, ( dmdma_lease_t lease ) ); /** * Register (callback != NULL) or remove (callback == NULL) this lease's * event callback. */ -dmod_dmdma_dif(1.0, int, _lease_set_callback, ( dmdma_lease_t lease, dmdma_lease_callback_t callback, void *user_ptr ) ); +dmod_dmdma_api(1.0, int, _lease_set_callback, ( dmdma_lease_t lease, dmdma_lease_callback_t callback, void *user_ptr ) ); #ifdef __cplusplus } diff --git a/src/dmdma.c b/src/dmdma.c index 0c95051..9421285 100644 --- a/src/dmdma.c +++ b/src/dmdma.c @@ -696,25 +696,13 @@ dmod_dmdrvi_dif_api_declaration(2.0, dmdma, int, _stat, /* ---- DMA lease API (see dmdma_lease.h) ---- * - * dmod_dmdma_dif_api_declaration() only emits a real .dmod.inputs - * registration when the *implementing* module differs from the module that - * *defines* the DIF family - see dmdma_defs.h's `#ifdef DMOD_dmdma` branch, - * which assumes a DIF is always implemented by some other module and - * degrades to a plain, unregistered function declaration otherwise (the - * lookup-free "you already know it's your own function" case that macro - * was written for). Here dmdma implements its *own* "dmdma" DIF family, so - * that assumption doesn't hold and every function below needs its - * registration written out by hand - otherwise Dmod_GetDifFunction() can - * never find these at runtime, no matter how correct the function bodies - * are (confirmed on real hardware: dmdma loads fine, but every consumer's - * Dmod_GetDifFunction() call returns NULL until this macro is used instead - * of the plain one above). */ -#define DMDMA_DIF_API_DECLARATION(VERSION, RET, NAME, PARAMS) \ - RET DMOD_MAKE_DIF_API_FUNCTION_NAME(dmdma, dmdma, NAME) PARAMS; \ - _DMOD_DIF_API_REGISTRATION(dmdma, dmdma, DMOD_MAKE_VERSION(VERSION,0.1), NAME) \ - RET DMOD_MAKE_DIF_API_FUNCTION_NAME(dmdma, dmdma, NAME) PARAMS - -DMDMA_DIF_API_DECLARATION(1.0, dmdma_lease_t, _lease_acquire, + * A plain Module API (dmod is the one and only implementation of it), so + * these are ordinary dmod_dmdma_api_declaration() definitions - registered + * for direct calling by any other module the moment it includes + * dmdma_lease.h and links against dmdma, no DIF-style runtime lookup + * involved. */ + +dmod_dmdma_api_declaration(1.0, dmdma_lease_t, _lease_acquire, ( dmdma_controller_t controller, dmdma_stream_t stream )) { if (controller >= DMDMA_MAX_CONTROLLERS) @@ -749,7 +737,7 @@ DMDMA_DIF_API_DECLARATION(1.0, dmdma_lease_t, _lease_acquire, return (dmdma_lease_t)slot; } -DMDMA_DIF_API_DECLARATION(1.0, void, _lease_release, ( dmdma_lease_t lease )) +dmod_dmdma_api_declaration(1.0, void, _lease_release, ( dmdma_lease_t lease )) { dmdma_stream_slot_t *slot = slot_from_lease(lease); if (slot == NULL) @@ -776,7 +764,7 @@ DMDMA_DIF_API_DECLARATION(1.0, void, _lease_release, ( dmdma_lease_t lease )) slot->cb.lease.user_ptr = NULL; } -DMDMA_DIF_API_DECLARATION(1.0, int, _lease_start, +dmod_dmdma_api_declaration(1.0, int, _lease_start, ( dmdma_lease_t lease, const dmdma_transfer_config_t *config )) { dmdma_stream_slot_t *slot = slot_from_lease(lease); @@ -804,7 +792,7 @@ DMDMA_DIF_API_DECLARATION(1.0, int, _lease_start, return rc; } -DMDMA_DIF_API_DECLARATION(1.0, void, _lease_abort, ( dmdma_lease_t lease )) +dmod_dmdma_api_declaration(1.0, void, _lease_abort, ( dmdma_lease_t lease )) { dmdma_stream_slot_t *slot = slot_from_lease(lease); if (slot == NULL) @@ -831,7 +819,7 @@ DMDMA_DIF_API_DECLARATION(1.0, void, _lease_abort, ( dmdma_lease_t lease )) } } -DMDMA_DIF_API_DECLARATION(1.0, bool, _lease_is_busy, ( dmdma_lease_t lease )) +dmod_dmdma_api_declaration(1.0, bool, _lease_is_busy, ( dmdma_lease_t lease )) { dmdma_stream_slot_t *slot = slot_from_lease(lease); if (slot == NULL) @@ -843,7 +831,7 @@ DMDMA_DIF_API_DECLARATION(1.0, bool, _lease_is_busy, ( dmdma_lease_t lease )) return dmdma_port_stream_is_busy(context->config.controller, slot->stream); } -DMDMA_DIF_API_DECLARATION(1.0, size_t, _lease_get_remaining, ( dmdma_lease_t lease )) +dmod_dmdma_api_declaration(1.0, size_t, _lease_get_remaining, ( dmdma_lease_t lease )) { dmdma_stream_slot_t *slot = slot_from_lease(lease); if (slot == NULL) @@ -855,7 +843,7 @@ DMDMA_DIF_API_DECLARATION(1.0, size_t, _lease_get_remaining, ( dmdma_lease_t lea return dmdma_port_stream_get_remaining(context->config.controller, slot->stream); } -DMDMA_DIF_API_DECLARATION(1.0, int, _lease_set_callback, +dmod_dmdma_api_declaration(1.0, int, _lease_set_callback, ( dmdma_lease_t lease, dmdma_lease_callback_t callback, void *user_ptr )) { dmdma_stream_slot_t *slot = slot_from_lease(lease); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 054cdf5..cd392b8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -42,16 +42,19 @@ dmod_link_modules(dmdma_test_dev dmheap@>=0.6 ) -# dmdma_test_lease exercises the dmdma_lease.h DIF the way a peripheral -# driver (dmuart, dmspi, dmsdio, ...) would: resolved by module name at -# runtime (Dmod_GetModuleContext("dmdma") + Dmod_GetDifFunction()), not -# through a device node. It links against dmdma_if (the same interface -# target dmdma_test_port uses for dmdma_port_if) purely to get dmdma.h and -# the generated dmdma_defs.h on its include path at compile time - it does -# not call into dmdma directly, only through the DIF lookup at runtime. Its -# transfer buffers are plain static arrays (not the "dma" dmheap context -# dmdma_test_port/_dev need) so it runs on any board regardless of whether -# one has been carved out. +# dmdma_test_lease exercises the dmdma_lease.h Module API the way a +# peripheral driver (dmuart, dmspi, dmsdio, ...) would: dmdma_lease_*() +# called directly, patched by the loader against dmdma's own registration +# the same way dmdma_port_stream_start() is - no device node, no runtime +# lookup. It links against dmdma_if (the same interface target +# dmdma_test_port uses for dmdma_port_if) to get dmdma.h/dmdma_defs.h on its +# include path - the runtime dependency on dmdma itself is picked up +# automatically (todmd derives it straight from the "@dmdma:" signatures +# dmdma_lease.h's dmod_dmdma_api() calls embed into this binary's own +# .dmod.outputs, the same way it derives dmdma's own dependency on +# dmdma_port). Its transfer buffers are plain static arrays (not the "dma" +# dmheap context dmdma_test_port/_dev need) so it runs on any board +# regardless of whether one has been carved out. dmod_add_executable(dmdma_test_lease ${DMOD_MODULE_VERSION} dmdma_test_lease.c ) diff --git a/tests/dmdma_test_lease.c b/tests/dmdma_test_lease.c index 2d4add7..ac452cb 100644 --- a/tests/dmdma_test_lease.c +++ b/tests/dmdma_test_lease.c @@ -1,8 +1,3 @@ -/* Needed so this translation unit gets real storage for the DIF signature - * string constants (dmod_dmdma_lease_*_sig) it references below, not just - * an extern declaration - see dmdevfs.c/dmvfs.c for the same pattern on - * every other DIF consumer in the ecosystem. */ -#define ENABLE_DIF_REGISTRATIONS ON #include "dmod.h" #include "dmdma.h" #include @@ -11,10 +6,9 @@ /** * @brief dmdma_test_lease - on-target smoke test for the DMA lease API * (dmdma_lease.h), exercised exactly the way a peripheral driver - * (dmuart, dmspi, dmsdio, ...) is meant to use it: by resolving - * "dmdma" as a DIF at runtime, the same way dmdevfs.c/dmvfs.c resolve - * dmdrvi/dmfsi - not by linking against dmdma or going through a - * device node. + * (dmuart, dmspi, dmsdio, ...) is meant to use it: dmdma_lease_*() + * called directly, like any other Module API function (see + * dmdma_lease.h) - no runtime lookup involved. * * Usage: * dmdma_test_lease @@ -23,11 +17,11 @@ * * Unlike dmdma_test_dev.c (device node / dmdrvi) and dmdma_test_port.c (raw * dmdma_port.h), this tool never touches dmdevfs, dmdrvi, or dmdma_port - - * only "dmdma" itself needs to be loaded. It also doesn't need dmdma_test_dev/ - * _test_port's shared "dma" dmheap context - a plain static buffer is a - * perfectly valid memory-to-memory DMA endpoint, and not every board carves - * out a dedicated DMA heap region (see linker/common.ld's optional "dma" - * MEMORY region). + * it links directly against dmdma (see tests/CMakeLists.txt). It also + * doesn't need dmdma_test_dev/_test_port's shared "dma" dmheap context - a + * plain static buffer is a perfectly valid memory-to-memory DMA endpoint, + * and not every board carves out a dedicated DMA heap region (see + * linker/common.ld's optional "dma" MEMORY region). */ /* Large enough that a circular transfer's wrap period is milliseconds, not @@ -58,47 +52,6 @@ static void on_lease_event(dmdma_lease_t lease, dmdma_event_t event, void *user_ } } -typedef struct -{ - dmod_dmdma_lease_acquire_t acquire; - dmod_dmdma_lease_release_t release; - dmod_dmdma_lease_start_t start; - dmod_dmdma_lease_abort_t abort; - dmod_dmdma_lease_is_busy_t is_busy; - dmod_dmdma_lease_get_remaining_t get_remaining; - dmod_dmdma_lease_set_callback_t set_callback; -} lease_api_t; - -/* Resolves every lease function by name, exactly the way a real consumer - * must - "dmdma" is an optional dependency, so every step here can fail if - * it isn't loaded, without that being a bug in either module. */ -static int resolve_lease_api(lease_api_t *api) -{ - Dmod_Context_t *dma = Dmod_GetModuleContext("dmdma"); - if (dma == NULL) - { - Dmod_Printf("ERROR: 'dmdma' module is not loaded\n"); - return -1; - } - - api->acquire = (dmod_dmdma_lease_acquire_t)Dmod_GetDifFunction(dma, dmod_dmdma_lease_acquire_sig); - api->release = (dmod_dmdma_lease_release_t)Dmod_GetDifFunction(dma, dmod_dmdma_lease_release_sig); - api->start = (dmod_dmdma_lease_start_t)Dmod_GetDifFunction(dma, dmod_dmdma_lease_start_sig); - api->abort = (dmod_dmdma_lease_abort_t)Dmod_GetDifFunction(dma, dmod_dmdma_lease_abort_sig); - api->is_busy = (dmod_dmdma_lease_is_busy_t)Dmod_GetDifFunction(dma, dmod_dmdma_lease_is_busy_sig); - api->get_remaining = (dmod_dmdma_lease_get_remaining_t)Dmod_GetDifFunction(dma, dmod_dmdma_lease_get_remaining_sig); - api->set_callback = (dmod_dmdma_lease_set_callback_t)Dmod_GetDifFunction(dma, dmod_dmdma_lease_set_callback_sig); - - if (api->acquire == NULL || api->release == NULL || api->start == NULL || api->abort == NULL || - api->is_busy == NULL || api->get_remaining == NULL || api->set_callback == NULL) - { - Dmod_Printf("ERROR: 'dmdma' does not implement the lease DIF\n"); - return -1; - } - - return 0; -} - /* This tool targets DMOD_USE_STDLIB=OFF builds (see cortex-m7's * tools-cfg.cmake) - no atoi()/strtoul(), so argv is parsed by hand. */ static uint32_t parse_uint(const char *s) @@ -112,11 +65,11 @@ static uint32_t parse_uint(const char *s) return value; } -static bool wait_until_idle(const lease_api_t *api, dmdma_lease_t lease) +static bool wait_until_idle(dmdma_lease_t lease) { for (uint32_t i = 0; i < DMDMA_TEST_BUSY_POLL_ITERATIONS; i++) { - if (!api->is_busy(lease)) + if (!dmdma_lease_is_busy(lease)) { return true; } @@ -141,12 +94,6 @@ int main(int argc, char *argv[]) dmdma_controller_t controller = (dmdma_controller_t)parse_uint(argv[1]); dmdma_stream_t stream = (dmdma_stream_t)parse_uint(argv[2]); - lease_api_t api; - if (resolve_lease_api(&api) != 0) - { - return -1; - } - uint8_t *src = s_src; uint8_t *dst = s_dst; dmdma_lease_t lease = NULL; @@ -160,7 +107,7 @@ int main(int argc, char *argv[]) /* --- 1. Acquire --- */ - lease = api.acquire(controller, stream); + lease = dmdma_lease_acquire(controller, stream); if (lease == NULL) { Dmod_Printf("ERROR: could not acquire controller %u stream %u\n", (unsigned)controller, (unsigned)stream); @@ -171,12 +118,12 @@ int main(int argc, char *argv[]) /* --- 2. The same stream cannot be acquired twice, through any API combination --- */ { - dmdma_lease_t second = api.acquire(controller, stream); + dmdma_lease_t second = dmdma_lease_acquire(controller, stream); if (second != NULL) { Dmod_Printf("FAIL: double-acquire of controller %u stream %u unexpectedly succeeded\n", (unsigned)controller, (unsigned)stream); - api.release(second); + dmdma_lease_release(second); goto cleanup_lease; } Dmod_Printf("PASS: double-acquire correctly rejected\n"); @@ -188,8 +135,8 @@ int main(int argc, char *argv[]) int idle_marker = 0xAAAA; s_expected_user_ptr = &idle_marker; s_callback_count = 0; - api.set_callback(lease, on_lease_event, &idle_marker); - api.abort(lease); + dmdma_lease_set_callback(lease, on_lease_event, &idle_marker); + dmdma_lease_abort(lease); if (s_callback_count != 0) { Dmod_Printf("FAIL: aborting an idle stream fired %d callback(s), expected 0\n", s_callback_count); @@ -205,7 +152,7 @@ int main(int argc, char *argv[]) s_expected_user_ptr = &user_marker; s_callback_count = 0; s_user_ptr_mismatch = false; - api.set_callback(lease, on_lease_event, &user_marker); + dmdma_lease_set_callback(lease, on_lease_event, &user_marker); dmdma_transfer_config_t transfer; transfer.direction = dmdma_direction_memory_to_memory; @@ -222,19 +169,19 @@ int main(int argc, char *argv[]) transfer.timeout_ms = 0; /* no watchdog for this run */ Dmod_Printf("Starting memory-to-memory transfer of %u byte(s) via the lease API...\n", DMDMA_TEST_BYTES); - if (api.start(lease, &transfer) != 0) + if (dmdma_lease_start(lease, &transfer) != 0) { Dmod_Printf("ERROR: dmdma_lease_start() failed\n"); goto cleanup_lease; } - if (!wait_until_idle(&api, lease)) + if (!wait_until_idle(lease)) { Dmod_Printf("FAIL: transfer did not complete (timed out polling is_busy)\n"); goto cleanup_lease; } - size_t remaining = api.get_remaining(lease); + size_t remaining = dmdma_lease_get_remaining(lease); if (remaining != 0) { Dmod_Printf("FAIL: %u byte(s) reported remaining after completion\n", (unsigned)remaining); @@ -267,7 +214,7 @@ int main(int argc, char *argv[]) dmdma_transfer_config_t bad = transfer; bad.element_count = 0; - if (api.start(lease, &bad) == 0) + if (dmdma_lease_start(lease, &bad) == 0) { Dmod_Printf("FAIL: zero-length transfer was accepted\n"); goto cleanup_lease; @@ -275,7 +222,7 @@ int main(int argc, char *argv[]) bad = transfer; bad.request = 1; /* mem-to-mem must not carry a request line */ - if (api.start(lease, &bad) == 0) + if (dmdma_lease_start(lease, &bad) == 0) { Dmod_Printf("FAIL: mem-to-mem transfer with a request line was accepted\n"); goto cleanup_lease; @@ -290,12 +237,12 @@ int main(int argc, char *argv[]) * finish on its own" and "did our abort() call catch it in flight". */ transfer.circular = true; - if (api.start(lease, &transfer) != 0) + if (dmdma_lease_start(lease, &transfer) != 0) { Dmod_Printf("ERROR: dmdma_lease_start() failed on the abort run\n"); goto cleanup_lease; } - if (!api.is_busy(lease)) + if (!dmdma_lease_is_busy(lease)) { Dmod_Printf("FAIL: circular transfer was not busy right after starting\n"); goto cleanup_lease; @@ -303,9 +250,9 @@ int main(int argc, char *argv[]) s_callback_count = 0; s_last_event = (dmdma_event_t)0; - api.abort(lease); + dmdma_lease_abort(lease); - if (api.is_busy(lease)) + if (dmdma_lease_is_busy(lease)) { Dmod_Printf("FAIL: stream still busy right after dmdma_lease_abort()\n"); goto cleanup_lease; @@ -325,7 +272,7 @@ int main(int argc, char *argv[]) * armed by dmdma_lease_start() firing. */ transfer.timeout_ms = DMDMA_TEST_TIMEOUT_MS; - if (api.start(lease, &transfer) != 0) + if (dmdma_lease_start(lease, &transfer) != 0) { Dmod_Printf("ERROR: dmdma_lease_start() failed on the timeout run\n"); goto cleanup_lease; @@ -335,7 +282,7 @@ int main(int argc, char *argv[]) s_last_event = (dmdma_event_t)0; Dmod_ThreadSleep(DMDMA_TEST_TIMEOUT_MS + DMDMA_TEST_TIMEOUT_MARGIN_MS); - if (api.is_busy(lease)) + if (dmdma_lease_is_busy(lease)) { Dmod_Printf("FAIL: stream still busy %u ms after its %u ms watchdog should have fired\n", DMDMA_TEST_TIMEOUT_MS + DMDMA_TEST_TIMEOUT_MARGIN_MS, DMDMA_TEST_TIMEOUT_MS); @@ -352,8 +299,8 @@ int main(int argc, char *argv[]) /* --- 8. Release must return the stream to the free pool cleanly --- */ - api.release(lease); - lease = api.acquire(controller, stream); + dmdma_lease_release(lease); + lease = dmdma_lease_acquire(controller, stream); if (lease == NULL) { Dmod_Printf("FAIL: could not re-acquire controller %u stream %u after release\n", @@ -369,7 +316,7 @@ int main(int argc, char *argv[]) cleanup_lease: if (lease != NULL) { - api.release(lease); + dmdma_lease_release(lease); } cleanup: return result;