From dea2a2aa0bf5785533a1d85f4cdcaf6d5ee02c80 Mon Sep 17 00:00:00 2001 From: Jax Calder Date: Thu, 8 Oct 2026 20:59:07 +0100 Subject: [PATCH 1/4] Implement STM32F7 SAI port with shared register engine Acquire SAI clocks through published dmclk_port 1.2, configure STM32 SAI blocks in shared STM32 code, and recover a stalled synchronous stop with a controller reset. Add a direct board test and document the current polling and core-stub limits. --- README.md | 43 ++- docs/api-reference.md | 11 +- include/dmsai_port.h | 27 +- include/dmsai_types.h | 2 +- src/port/CMakeLists.txt | 4 + src/port/stm32_common/stm32_common.c | 385 ++++++++++++++++++++++-- src/port/stm32_common/stm32_sai.h | 30 ++ src/port/stm32f7/port.c | 21 +- tests/CMakeLists.txt | 4 + tests/port-board-test/CMakeLists.txt | 7 + tests/port-board-test/port_board_test.c | 73 +++++ 11 files changed, 548 insertions(+), 59 deletions(-) create mode 100644 src/port/stm32_common/stm32_sai.h create mode 100644 tests/port-board-test/CMakeLists.txt create mode 100644 tests/port-board-test/port_board_test.c diff --git a/README.md b/README.md index 70301bb..e9de7e5 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,9 @@ section selects the driver and each configured controller is intended to appear as `/dev/dmsaiN`. Applications use ordinary file read/write operations and driver-specific `ioctl` commands. There is no separate stream Module API. -This revision contains headers and buildable stubs only. `dmdrvi_create()` -returns `NULL`, so no device node appears yet and no hardware is touched. +The `dmdrvi` core remains a stub: `dmdrvi_create()` returns `NULL`, so no +device node appears yet. The STM32F7 `dmsai_port` now configures SAI hardware +and can be exercised directly by another DMOD module. ## Device contract @@ -26,7 +27,8 @@ is explicit: signed 16-bit little-endian, signed 24-bit in the low 24 bits of a 32-bit little-endian word, or signed 32-bit little-endian. For stereo 16-bit audio, one frame is four bytes. A request size must be a multiple of frame size; a successful transfer can return a shorter whole-frame count. The device -is non-seekable. The port owns DMA buffers; callers pass normal PCM buffers. +is non-seekable. Callers pass normal PCM buffers; the port owns its hardware +resources and transfer mechanism. An open device has one exclusive handle, so two clients cannot independently start, stop or change the same stream. @@ -65,8 +67,7 @@ receive=off ``` The port decides which rate, role, framing and directions its target supports. -The configuration format is a proposed contract; the present stub does not -parse it. +The core stub does not yet parse this configuration. Board GPIO mappings and pin-agnostic MCU defaults are in [configs/README.md](configs/README.md). Select one device section per SAI @@ -108,14 +109,40 @@ The codec and GPIO configuration still belong to their respective drivers. ## Core and port layout -`dmsai` handles the `dmdrvi` DIF, INI configuration and byte/frame semantics. -`dmsai_port` handles resource ownership, clocks, DMA and SAI registers. The +`dmsai` will handle the `dmdrvi` DIF, INI configuration and byte/frame semantics. +`dmsai_port` handles resource ownership, clocks and SAI registers. The port API uses only portable `dmsai_types.h` types. Its STM32 definitions are placed in `src/port/stm32_common/`; `src/port/stm32f7/port.c` contains only -module lifecycle now and can later provide family-specific IRQ glue. Another +module lifecycle and a small hardware descriptor. Another STM32 family selects the same common source in its `config.cmake`. A different architecture can select its own common source without changing the public API. +The STM32F7 port supports master I2S and TDM, 16-bit PCM or 24/32-bit PCM in +32-bit slots, up to 16 slots, 256 frame bits and 96 kHz. It asks `dmclk_port` +v1.2 for the SAI kernel clock before enabling the peripheral gate, then +releases the clock after stopping and gating SAI. With `MCKDIV=2`, a 48 kHz +stream requests 49.152 MHz and reports the achieved sample rate. SAI A +generates the clocks and transmits; SAI B receives synchronously when +requested. Board GPIO and codec setup remain separate. + +If a synchronous block does not acknowledge a stop within five milliseconds, +the port resets that SAI controller, restores its configuration and increments +the transfer error counter. This also allows a later start or deinitialization +when an external frame signal is absent. + +Read and write currently poll the SAI FIFOs. This supports short direct port +transfers and hardware bring-up; it does not provide continuous buffered audio +across scheduling gaps. A later DMA-backed transfer engine is needed for +sustained playback and recording. This limitation does not affect the clock, +configuration, start/stop and status operations. + +`dmsai_port_board_test` is a direct hardware test on STM32F746G-DISCO. It +configures SAI2 at 48 kHz and 44.1 kHz, checks registers, starts both blocks, +writes one PCM frame, then checks that the clock and peripheral gate are +released. Run it from the firmware shell after bundling `dmsai_port`, the test +module and published `dmclk_port` v1.2. It does not initialize the WM8994 +codec or verify sound on an external pin. + ## Build and tests ```sh diff --git a/docs/api-reference.md b/docs/api-reference.md index 4b8f346..753a829 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -49,11 +49,16 @@ none of this behavior. `include/dmsai_port.h` declares eight functions: instance count, init/deinit, start/stop, read/write and status. The port receives the same -portable `dmsai_config_t` as the core and owns its internal DMA buffers. +portable `dmsai_config_t` as the core and owns its hardware resources. `read`/`write` copy whole frames between those buffers and caller memory. It does not know about `dmdevfs`, INI files, file handles or `dmdrvi` types. STM32 port API entry points reside in `src/port/stm32_common/stm32_common.c`; the family `port.c` is reserved for lifecycle, hardware descriptors and IRQ -routing. Future STM32 families can share the common source. The current port -entry points return `-ENOSYS` and do not touch hardware. +routing. Future STM32 families can share the common source. The STM32F7 port +uses `dmclk_port` v1.2 to reserve SAI clocks, configures master block A and +optional synchronous RX block B, and polls their FIFOs for direct transfers. +It accepts frames of at most 256 bits. If a block cannot complete a stop at a +frame boundary, the port resets and reconfigures that controller and records a +transfer error. +The architecture-independent `dmdrvi` core remains a stub. diff --git a/include/dmsai_port.h b/include/dmsai_port.h index 9d34e98..a3284be 100644 --- a/include/dmsai_port.h +++ b/include/dmsai_port.h @@ -9,8 +9,7 @@ /** * @brief Report how many SAI controllers the selected target exposes. * - * @return Non-negative instance count, or a negative errno value. The stub - * returns -ENOSYS. + * @return Non-negative instance count, or -ENODEV before port initialization. */ dmod_dmsai_port_api(1.0, int, _get_instance_count, ( void )); @@ -21,7 +20,9 @@ dmod_dmsai_port_api(1.0, int, _get_instance_count, ( void )); * hardware resources. Board pin configuration and codec control are separate. * * @param config Portable configuration, valid for this call. - * @return 0 on success or negative errno. The stub returns -ENOSYS. + * @return 0 on success; -EINVAL for malformed configuration, -ENOTSUP for + * an unsupported format, -EBUSY when the controller is in use, or an error + * from the clock provider. */ dmod_dmsai_port_api(1.0, int, _init, ( const dmsai_config_t *config )); @@ -29,7 +30,8 @@ dmod_dmsai_port_api(1.0, int, _init, ( const dmsai_config_t *config )); * @brief Stop and release one configured controller. * * @param instance Zero-based controller index. - * @return 0 on success or negative errno. The stub returns -ENOSYS. + * @return 0 on success, -ENODEV if not initialized, -EBUSY while I/O is + * active, or a clock/hardware error. */ dmod_dmsai_port_api(1.0, int, _deinit, ( dmsai_instance_t instance )); @@ -37,15 +39,19 @@ dmod_dmsai_port_api(1.0, int, _deinit, ( dmsai_instance_t instance )); * @brief Enable the configured TX and/or RX stream. * * @param instance Zero-based controller index. - * @return 0 on success or negative errno. The stub returns -ENOSYS. + * @return 0 on success or -ENODEV if not initialized. */ dmod_dmsai_port_api(1.0, int, _start, ( dmsai_instance_t instance )); /** * @brief Disable the stream and wake blocked readers and writers. * + * The port may reset and reconfigure its controller if a synchronous block + * cannot finish the current frame. The transfer error counter then increases. + * * @param instance Zero-based controller index. - * @return 0 on success or negative errno. The stub returns -ENOSYS. + * @return 0 on success, -ENODEV if not initialized, or -ETIMEDOUT if the + * hardware does not acknowledge disable. */ dmod_dmsai_port_api(1.0, int, _stop, ( dmsai_instance_t instance )); @@ -61,7 +67,8 @@ dmod_dmsai_port_api(1.0, int, _stop, ( dmsai_instance_t instance )); * @param size Available bytes, a multiple of the PCM frame size. * @param received Receives copied byte count on success. * @param timeout_ms Maximum wait in milliseconds, or zero for no limit. - * @return 0 on success or negative errno. The stub returns -ENOSYS. + * @return 0 on success, including a short whole-frame transfer after a + * timeout; otherwise a negative errno value. */ dmod_dmsai_port_api(1.0, int, _read, ( dmsai_instance_t instance, void *buffer, size_t size, size_t *received, @@ -79,7 +86,8 @@ dmod_dmsai_port_api(1.0, int, _read, * @param size Source bytes, a multiple of the PCM frame size. * @param written Receives accepted byte count on success. * @param timeout_ms Maximum wait in milliseconds, or zero for no limit. - * @return 0 on success or negative errno. The stub returns -ENOSYS. + * @return 0 on success, including a short whole-frame transfer after a + * timeout; otherwise a negative errno value. */ dmod_dmsai_port_api(1.0, int, _write, ( dmsai_instance_t instance, const void *buffer, size_t size, @@ -90,7 +98,8 @@ dmod_dmsai_port_api(1.0, int, _write, * * @param instance Zero-based controller index. * @param status Receives status on success and remains unchanged on failure. - * @return 0 on success or negative errno. The stub returns -ENOSYS. + * @return 0 on success, -ENODEV if not initialized, or -EINVAL for a null + * output pointer. */ dmod_dmsai_port_api(1.0, int, _get_status, ( dmsai_instance_t instance, dmsai_status_t *status )); diff --git a/include/dmsai_types.h b/include/dmsai_types.h index f02a85b..77b9d87 100644 --- a/include/dmsai_types.h +++ b/include/dmsai_types.h @@ -58,7 +58,7 @@ typedef struct uint32_t actual_sample_rate_hz; /**< Frame rate achieved by the port. */ uint32_t tx_underruns; /**< Transmit starvation events since creation. */ uint32_t rx_overruns; /**< Receive samples lost since creation. */ - uint32_t transfer_errors; /**< Other DMA or serial transfer failures. */ + uint32_t transfer_errors; /**< Other transfer errors, including forced recovery on stop. */ bool running; /**< True after successful START. */ } dmsai_status_t; diff --git a/src/port/CMakeLists.txt b/src/port/CMakeLists.txt index 8547f78..7370b84 100644 --- a/src/port/CMakeLists.txt +++ b/src/port/CMakeLists.txt @@ -14,3 +14,7 @@ target_include_directories(${DMOD_MODULE_NAME} PRIVATE target_include_directories(${DMOD_MODULE_NAME}_if INTERFACE ${CMAKE_SOURCE_DIR}/include ) + +dmod_link_modules(${DMOD_MODULE_NAME} + dmclk_port@>=1.2 +) diff --git a/src/port/stm32_common/stm32_common.c b/src/port/stm32_common/stm32_common.c index 71eb537..ba5f064 100644 --- a/src/port/stm32_common/stm32_common.c +++ b/src/port/stm32_common/stm32_common.c @@ -1,45 +1,323 @@ #define DMOD_ENABLE_REGISTRATION ON #include "dmsai_port.h" +#include "stm32_sai.h" +#include "dmod.h" #include +#include +#include + +/* The STM32F4/F7 SAI register block is shared. Family-specific addresses and + * clock domains come from the descriptor in stm32f7/port.c. */ +#define RCC_APB2ENR 0x44U +#define RCC_APB2RSTR 0x24U +#define SAI_BLOCK_A 0x04U +#define SAI_BLOCK_B 0x24U +#define SAI_CR1 0x00U +#define SAI_CR2 0x04U +#define SAI_FRCR 0x08U +#define SAI_SLOTR 0x0CU +#define SAI_IMR 0x10U +#define SAI_SR 0x14U +#define SAI_CLRFR 0x18U +#define SAI_DR 0x1CU +#define SAIEN (1U << 16) +#define FIFO_FLUSH (1U << 3) +#define FIFO_LEVEL(sr) (((sr) >> 16) & 7U) +#define FIFO_FULL 5U +#define ERROR_FLAGS 0x75U +#define MCKDIV 2U +#define KERNEL_CLOCK_FACTOR (256U * 2U * MCKDIV) + +typedef struct { + dmsai_config_t config; + dmsai_status_t status; + uint8_t sample_bytes; + uint8_t active_count; + bool initialized; + bool busy; + bool tx_busy; + bool rx_busy; +} sai_state_t; + +static const stm32_sai_family_t *family; +static sai_state_t states[2]; + +/** @brief Return an RCC register selected by the active family descriptor. */ +static volatile uint32_t *rcc_reg(uint32_t offset) +{ + return (volatile uint32_t *)(family->rcc_base + offset); +} + +/** @brief Return a register in one SAI controller's A or B block. */ +static volatile uint32_t *block_reg(unsigned instance, bool block_b, + uint32_t offset) +{ + return (volatile uint32_t *)(family->sai_base[instance] + + (block_b ? SAI_BLOCK_B : SAI_BLOCK_A) + offset); +} + +/** @brief Find the local state for a valid controller instance. */ +static sai_state_t *get_state(dmsai_instance_t instance) +{ + return family && instance < family->count ? &states[instance] : NULL; +} + +/** @brief Count the enabled PCM slots in a frame. */ +static uint8_t count_slots(uint32_t slots) +{ + uint8_t count = 0; + while (slots) { + count += (uint8_t)(slots & 1U); + slots >>= 1; + } + return count; +} + +/** @brief Validate a portable configuration against STM32 SAI limits. */ +static int validate(const dmsai_config_t *config) +{ + if (!config || !get_state(config->instance) || !config->sample_rate_hz || + config->sample_rate_hz > 96000 || config->tolerance_ppm > 1000000 || + (unsigned)config->clock_role > dmsai_clock_slave || + (!config->transmit && !config->receive)) + return -EINVAL; + if (config->clock_role == dmsai_clock_slave) return -ENOTSUP; + if (config->slot_count < 2 || config->slot_count > 16 || + (config->slot_count & 1U) || + (config->slot_bits != 16 && config->slot_bits != 32) || + config->frame_bits != config->slot_count * config->slot_bits || + !config->active_slots || + (config->active_slots & ~((1U << config->slot_count) - 1U))) + return -EINVAL; + if (config->frame_bits > 256) return -ENOTSUP; + if (config->framing != dmsai_frame_i2s && + config->framing != dmsai_frame_tdm) + return -EINVAL; + if (config->framing == dmsai_frame_i2s && config->slot_count != 2) + return -ENOTSUP; + if ((config->pcm_format == dmsai_pcm_s16_le && config->slot_bits != 16) || + ((config->pcm_format == dmsai_pcm_s24_in32_le || + config->pcm_format == dmsai_pcm_s32_le) && config->slot_bits != 32)) + return -ENOTSUP; + return (unsigned)config->pcm_format > dmsai_pcm_s32_le ? -EINVAL : 0; +} + +/** @brief Encode a PCM sample width for the SAI data-size field. */ +static uint32_t data_size(dmsai_pcm_format_t format) +{ + return (format == dmsai_pcm_s16_le ? 4U : + format == dmsai_pcm_s24_in32_le ? 6U : 7U) << 5; +} + +/** @brief Program framing, slot and transfer registers for one SAI block. */ +static void configure_block(unsigned instance, bool block_b, + const dmsai_config_t *config) +{ + uint32_t cr1 = data_size(config->pcm_format) | (1U << 9); + if (block_b) + cr1 |= 3U | (1U << 10); /* Slave RX, synchronous with block A. */ + else + cr1 |= (1U << 13) | (MCKDIV << 20); + uint32_t frcr = (config->frame_bits - 1U) | + (((config->frame_bits / 2U) - 1U) << 8) | + (1U << 16) | (1U << 18); + uint32_t slotr = ((config->slot_bits == 16 ? 1U : 2U) << 6) | + ((config->slot_count - 1U) << 8) | (config->active_slots << 16); + *block_reg(instance, block_b, SAI_CR1) = cr1; + *block_reg(instance, block_b, SAI_CR2) = FIFO_FLUSH; + *block_reg(instance, block_b, SAI_FRCR) = frcr; + *block_reg(instance, block_b, SAI_SLOTR) = slotr; + *block_reg(instance, block_b, SAI_IMR) = 0; + *block_reg(instance, block_b, SAI_CLRFR) = 0x7FU; +} + +/** @brief Decode one little-endian PCM sample for the SAI data register. */ +static uint32_t sample_from_pcm(const uint8_t *source, uint8_t width, + dmsai_pcm_format_t format) +{ + uint32_t sample = 0; + for (uint8_t byte = 0; byte < width; ++byte) + sample |= (uint32_t)source[byte] << (8U * byte); + return format == dmsai_pcm_s24_in32_le ? sample & 0xFFFFFFU : sample; +} + +/** @brief Encode one SAI sample into little-endian PCM memory. */ +static void sample_to_pcm(uint8_t *destination, uint8_t width, + uint32_t sample, bool sign_extend) +{ + if (sign_extend && (sample & 0x800000U)) + sample |= 0xFF000000U; + for (uint8_t byte = 0; byte < width; ++byte) + destination[byte] = (uint8_t)(sample >> (8U * byte)); +} + +/** @brief Accumulate and clear the hardware error flags of active blocks. */ +static void count_errors(unsigned instance, sai_state_t *state) +{ + for (unsigned block = 0; block < 2; ++block) { + if (block && !state->config.receive) continue; + uint32_t flags = *block_reg(instance, block != 0, SAI_SR) & ERROR_FLAGS; + if (flags & 1U) { + if (block) ++state->status.rx_overruns; + else ++state->status.tx_underruns; + } + if (flags & ~1U) ++state->status.transfer_errors; + if (flags) *block_reg(instance, block != 0, SAI_CLRFR) = flags; + } +} + +/** @brief Wait until a FIFO can provide or accept one sample. */ +static int wait_fifo(unsigned instance, bool rx, sai_state_t *state, + Dmod_Timestamp_t started, uint32_t timeout_ms) +{ + uint32_t spins = 0; + while (state->status.running) { + uint32_t sr = *block_reg(instance, rx, SAI_SR); + if (rx ? FIFO_LEVEL(sr) != 0 : FIFO_LEVEL(sr) != FIFO_FULL) + return 0; + if (sr & ERROR_FLAGS) count_errors(instance, state); + if (timeout_ms && Dmod_GetUptime() - started >= timeout_ms) + return -ETIMEDOUT; + if (++spins == 10000U) { + Dmod_ThreadSleep(0); + spins = 0; + } + } + return -ECANCELED; +} + +/** @copydoc stm32_sai_set_family */ +void stm32_sai_set_family(const stm32_sai_family_t *selected) +{ + family = selected; +} + +/** @copydoc stm32_sai_can_unload */ +int stm32_sai_can_unload(void) +{ + for (unsigned i = 0; i < 2; ++i) + if (states[i].initialized || states[i].busy) return -EBUSY; + family = NULL; + return 0; +} -/* All port API definitions live in the shared STM32 source. Family port.c - * files are reserved for module lifecycle, family descriptors and IRQ glue. */ /** @copydoc dmsai_port_get_instance_count */ dmod_dmsai_port_api_declaration(1.0, int, _get_instance_count, ( void )) { - return -ENOSYS; + return family ? family->count : -ENODEV; } /** @copydoc dmsai_port_init */ dmod_dmsai_port_api_declaration(1.0, int, _init, ( const dmsai_config_t *config )) { - (void)config; - return -ENOSYS; + int rc = validate(config); + if (rc != 0) return rc; + unsigned index = config->instance; + sai_state_t *state = &states[index]; + if (state->initialized || state->busy || + (*rcc_reg(RCC_APB2ENR) & family->sai_enable[index])) + return -EBUSY; + state->busy = true; + dmclk_frequency_t target = (dmclk_frequency_t)config->sample_rate_hz * + KERNEL_CLOCK_FACTOR; + dmclk_frequency_t tolerance = + (target * config->tolerance_ppm + 999999U) / 1000000U; + dmclk_frequency_t actual; + rc = dmclk_port_acquire_domain(family->clock_domain[index], target, + tolerance, &actual); + if (rc == 0) { + *rcc_reg(RCC_APB2ENR) |= family->sai_enable[index]; + *(volatile uint32_t *)family->sai_base[index] = 0; /* GCR */ + configure_block(index, false, config); + if (config->receive) configure_block(index, true, config); + memset(state, 0, sizeof(*state)); + state->config = *config; + state->sample_bytes = config->slot_bits / 8U; + state->active_count = count_slots(config->active_slots); + state->status.actual_sample_rate_hz = + (uint32_t)((actual + KERNEL_CLOCK_FACTOR / 2U) / + KERNEL_CLOCK_FACTOR); + state->initialized = true; + } else { + state->busy = false; + } + return rc; } -/** @copydoc dmsai_port_deinit */ -dmod_dmsai_port_api_declaration(1.0, int, _deinit, +/** @copydoc dmsai_port_stop */ +dmod_dmsai_port_api_declaration(1.0, int, _stop, ( dmsai_instance_t instance )) { - (void)instance; - return -ENOSYS; + sai_state_t *state = get_state(instance); + if (!state || !state->initialized) return -ENODEV; + state->status.running = false; + if (state->config.receive) + *block_reg(instance, true, SAI_CR1) &= ~SAIEN; + *block_reg(instance, false, SAI_CR1) &= ~SAIEN; + Dmod_Timestamp_t started = Dmod_GetUptime(); + bool timed_out = false; + while ((*block_reg(instance, false, SAI_CR1) & SAIEN) || + (state->config.receive && + (*block_reg(instance, true, SAI_CR1) & SAIEN))) { + if (Dmod_GetUptime() - started >= 5U) { + timed_out = true; + break; + } + } + count_errors(instance, state); + if (timed_out) { + /* A synchronous slave can wait indefinitely for a frame boundary. + * Reset only this controller, then restore its configuration. */ + *rcc_reg(RCC_APB2RSTR) |= family->sai_enable[instance]; + *rcc_reg(RCC_APB2RSTR) &= ~family->sai_enable[instance]; + if (*rcc_reg(RCC_APB2RSTR) & family->sai_enable[instance]) + return -ETIMEDOUT; + *(volatile uint32_t *)family->sai_base[instance] = 0; + configure_block(instance, false, &state->config); + if (state->config.receive) + configure_block(instance, true, &state->config); + ++state->status.transfer_errors; + return 0; + } + *block_reg(instance, false, SAI_CR2) |= FIFO_FLUSH; + if (state->config.receive) + *block_reg(instance, true, SAI_CR2) |= FIFO_FLUSH; + return 0; } -/** @copydoc dmsai_port_start */ -dmod_dmsai_port_api_declaration(1.0, int, _start, +/** @copydoc dmsai_port_deinit */ +dmod_dmsai_port_api_declaration(1.0, int, _deinit, ( dmsai_instance_t instance )) { - (void)instance; - return -ENOSYS; + sai_state_t *state = get_state(instance); + if (!state || !state->initialized) return -ENODEV; + if (state->busy || state->tx_busy || state->rx_busy) return -EBUSY; + int rc = dmsai_port_stop(instance); + if (rc != 0) return rc; + *rcc_reg(RCC_APB2ENR) &= ~family->sai_enable[instance]; + rc = dmclk_port_release_domain(family->clock_domain[instance]); + if (rc == 0) memset(state, 0, sizeof(*state)); + return rc; } -/** @copydoc dmsai_port_stop */ -dmod_dmsai_port_api_declaration(1.0, int, _stop, +/** @copydoc dmsai_port_start */ +dmod_dmsai_port_api_declaration(1.0, int, _start, ( dmsai_instance_t instance )) { - (void)instance; - return -ENOSYS; + sai_state_t *state = get_state(instance); + if (!state || !state->initialized) return -ENODEV; + if (state->status.running) return 0; + *block_reg(instance, false, SAI_CLRFR) = 0x7FU; + if (state->config.receive) { + *block_reg(instance, true, SAI_CLRFR) = 0x7FU; + *block_reg(instance, true, SAI_CR1) |= SAIEN; + } + for (unsigned i = 0; i < state->active_count && i < 8; ++i) + *block_reg(instance, false, SAI_DR) = 0; + state->status.running = true; + *block_reg(instance, false, SAI_CR1) |= SAIEN; + return 0; } /** @copydoc dmsai_port_read */ @@ -47,12 +325,33 @@ dmod_dmsai_port_api_declaration(1.0, int, _read, ( dmsai_instance_t instance, void *buffer, size_t size, size_t *received, uint32_t timeout_ms )) { - (void)instance; - (void)buffer; - (void)size; - (void)received; - (void)timeout_ms; - return -ENOSYS; + sai_state_t *state = get_state(instance); + if (!state || !state->initialized || !state->config.receive) return -ENODEV; + size_t frame_bytes = state->active_count * state->sample_bytes; + if (!received || (!buffer && size) || size % frame_bytes) return -EINVAL; + *received = 0; + if (!state->status.running) return -EPIPE; + if (state->rx_busy) return -EBUSY; + state->rx_busy = true; + Dmod_Timestamp_t started = Dmod_GetUptime(); + uint8_t frame[16 * 4]; + int rc = 0; + while (*received < size) { + for (unsigned slot = 0; slot < state->active_count; ++slot) { + rc = wait_fifo(instance, true, state, started, timeout_ms); + if (rc != 0) goto done; + uint32_t sample = *block_reg(instance, true, SAI_DR); + sample_to_pcm(frame + slot * state->sample_bytes, + state->sample_bytes, sample, + state->config.pcm_format == dmsai_pcm_s24_in32_le); + } + memcpy((uint8_t *)buffer + *received, frame, frame_bytes); + *received += frame_bytes; + } +done: + count_errors(instance, state); + state->rx_busy = false; + return *received ? 0 : rc; } /** @copydoc dmsai_port_write */ @@ -60,19 +359,41 @@ dmod_dmsai_port_api_declaration(1.0, int, _write, ( dmsai_instance_t instance, const void *buffer, size_t size, size_t *written, uint32_t timeout_ms )) { - (void)instance; - (void)buffer; - (void)size; - (void)written; - (void)timeout_ms; - return -ENOSYS; + sai_state_t *state = get_state(instance); + if (!state || !state->initialized || !state->config.transmit) return -ENODEV; + size_t frame_bytes = state->active_count * state->sample_bytes; + if (!written || (!buffer && size) || size % frame_bytes) return -EINVAL; + *written = 0; + if (!state->status.running) return -EPIPE; + if (state->tx_busy) return -EBUSY; + state->tx_busy = true; + Dmod_Timestamp_t started = Dmod_GetUptime(); + int rc = 0; + while (*written < size) { + for (unsigned slot = 0; slot < state->active_count; ++slot) { + rc = wait_fifo(instance, false, state, started, timeout_ms); + if (rc != 0) goto done; + uint32_t sample = sample_from_pcm( + (const uint8_t *)buffer + *written + slot * state->sample_bytes, + state->sample_bytes, state->config.pcm_format); + *block_reg(instance, false, SAI_DR) = sample; + } + *written += frame_bytes; + } +done: + count_errors(instance, state); + state->tx_busy = false; + return *written ? 0 : rc; } /** @copydoc dmsai_port_get_status */ dmod_dmsai_port_api_declaration(1.0, int, _get_status, ( dmsai_instance_t instance, dmsai_status_t *status )) { - (void)instance; - (void)status; - return -ENOSYS; + sai_state_t *state = get_state(instance); + if (!state || !state->initialized) return -ENODEV; + if (!status) return -EINVAL; + count_errors(instance, state); + *status = state->status; + return 0; } diff --git a/src/port/stm32_common/stm32_sai.h b/src/port/stm32_common/stm32_sai.h new file mode 100644 index 0000000..32355b7 --- /dev/null +++ b/src/port/stm32_common/stm32_sai.h @@ -0,0 +1,30 @@ +#ifndef DMSAI_STM32_SAI_H +#define DMSAI_STM32_SAI_H + +#include +#include "dmclk_port.h" + +/* RCC and SAI addresses vary by family. Keep the register-based SAI engine + * common; each family supplies only this small hardware descriptor. */ +typedef struct +{ + uintptr_t rcc_base; + uintptr_t sai_base[2]; + uint32_t sai_enable[2]; + dmclk_domain_t clock_domain[2]; + uint8_t count; +} stm32_sai_family_t; + +/** + * @brief Select the register layout and instances supplied by an STM32 family. + * @param family Immutable descriptor kept alive for the module's lifetime. + */ +void stm32_sai_set_family(const stm32_sai_family_t *family); + +/** + * @brief Reject module unload while a SAI controller is configured. + * @return 0 when the port is idle, or -EBUSY while a controller is in use. + */ +int stm32_sai_can_unload(void); + +#endif diff --git a/src/port/stm32f7/port.c b/src/port/stm32f7/port.c index 3f71e51..88182b1 100644 --- a/src/port/stm32f7/port.c +++ b/src/port/stm32f7/port.c @@ -1,24 +1,33 @@ #define DMOD_ENABLE_REGISTRATION ON #include "dmod.h" +#include "../stm32_common/stm32_sai.h" + +static const stm32_sai_family_t stm32f7_sai = { + .rcc_base = 0x40023800U, + .sai_base = {0x40015800U, 0x40015C00U}, + .sai_enable = {1U << 22, 1U << 23}, + .clock_domain = {dmclk_domain_sai1, dmclk_domain_sai2}, + .count = 2, +}; /* Family-specific lifecycle and, later, interrupt routing only. */ /** - * @brief Initialize the STM32F7 port module without touching hardware. - * @param config DMOD loader configuration; unused by this interface stub. - * @return 0 after the module is loaded. + * @brief Install the STM32F7 SAI hardware descriptor. + * @param config DMOD loader configuration; unused by the port. + * @return 0 after the descriptor is installed. */ int dmod_init(const Dmod_Config_t *config) { (void)config; - Dmod_Printf("dmsai_port STM32F7 stub loaded\n"); + stm32_sai_set_family(&stm32f7_sai); return 0; } /** * @brief Deinitialize the STM32F7 port module. - * @return 0; no hardware resources are held by this stub. + * @return 0 when no controller is configured, otherwise -EBUSY. */ int dmod_deinit(void) { - return 0; + return stm32_sai_can_unload(); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5b106e0..557a357 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -30,3 +30,7 @@ if(DMF_GET_EXECUTABLE AND DMOD_LOADER_EXECUTABLE) else() message(WARNING "dmf-get/dmod_loader not found - 'ctest' will not run ${DMOD_MODULE_NAME}. Build the tools first with -DDMOD_BUILD_TOOLS=ON") endif() + +if(DMOD_CPU_FAMILY STREQUAL "stm32f7") + add_subdirectory(port-board-test) +endif() diff --git a/tests/port-board-test/CMakeLists.txt b/tests/port-board-test/CMakeLists.txt new file mode 100644 index 0000000..0d056d9 --- /dev/null +++ b/tests/port-board-test/CMakeLists.txt @@ -0,0 +1,7 @@ +set(DMOD_MODULE_NAME dmsai_port_board_test) +set(DMOD_AUTHOR_NAME "Patryk Kubiak") +set(DMOD_STACK_SIZE 2048) +dmod_add_executable(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} + port_board_test.c) +target_link_libraries(${DMOD_MODULE_NAME} dmsai_port_if) +dmod_link_modules(${DMOD_MODULE_NAME} dmclk_port@>=1.2) diff --git a/tests/port-board-test/port_board_test.c b/tests/port-board-test/port_board_test.c new file mode 100644 index 0000000..84025b1 --- /dev/null +++ b/tests/port-board-test/port_board_test.c @@ -0,0 +1,73 @@ +#include "dmsai_port.h" +#include "dmclk_port.h" +#include "dmod.h" +#include + +#define RCC_APB2ENR (*(volatile uint32_t *)0x40023844U) +#define RCC_CR (*(volatile uint32_t *)0x40023800U) +#define SAI2_ACR1 (*(volatile uint32_t *)0x40015C04U) +#define SAI2_BCR1 (*(volatile uint32_t *)0x40015C24U) +#define SAI2_AFRCR (*(volatile uint32_t *)0x40015C0CU) +#define SAI2_ASLOTR (*(volatile uint32_t *)0x40015C10U) +#define SAI2EN (1U << 23) + +static int failures; +#define CHECK(test) do { if (!(test)) { \ + Dmod_Printf("DMSAI PORT FAIL line %d: %s\n", __LINE__, #test); \ + ++failures; \ +} } while (0) + +/** @brief Configure, transfer on and release SAI2 at one sample rate. */ +static void exercise(uint32_t rate) +{ + dmsai_config_t config = { + .instance = 1, .clock_role = dmsai_clock_master, + .framing = dmsai_frame_tdm, .pcm_format = dmsai_pcm_s16_le, + .sample_rate_hz = rate, .tolerance_ppm = 500, + .slot_bits = 16, .slot_count = 4, .frame_bits = 64, + .active_slots = 5, .transmit = true, .receive = true, + }; + uint32_t before_cr = RCC_CR; + dmclk_frequency_t before_sai = + dmclk_port_get_domain_frequency(dmclk_domain_sai2); + int rc = dmsai_port_init(&config); + Dmod_Printf("DMSAI SAI2 init rate=%u rc=%d\n", (unsigned)rate, rc); + CHECK(rc == 0); + if (rc != 0) return; + CHECK(dmsai_port_init(&config) == -EBUSY); + CHECK((RCC_APB2ENR & SAI2EN) != 0); + CHECK((SAI2_ACR1 & (15U << 20)) == (2U << 20)); + CHECK((SAI2_BCR1 & (3U << 10)) == (1U << 10)); + CHECK(SAI2_AFRCR == 0x00051F3FU); + CHECK(SAI2_ASLOTR == 0x00050340U); + dmsai_status_t status; + CHECK(dmsai_port_get_status(1, &status) == 0); + uint32_t error = status.actual_sample_rate_hz > rate ? + status.actual_sample_rate_hz - rate : rate - status.actual_sample_rate_hz; + CHECK(error <= rate / 2000U); + CHECK(dmsai_port_start(1) == 0); + CHECK((SAI2_ACR1 & (1U << 16)) != 0); + CHECK((SAI2_BCR1 & (1U << 16)) != 0); + const int16_t pcm[4] = {0, 0, 0, 0}; + size_t written = 0; + rc = dmsai_port_write(1, pcm, sizeof(pcm), &written, 50); + CHECK(rc == 0 && written == sizeof(pcm)); + CHECK(dmsai_port_stop(1) == 0); + CHECK(dmsai_port_deinit(1) == 0); + CHECK((RCC_APB2ENR & SAI2EN) == 0); + CHECK(RCC_CR == before_cr); + CHECK(dmclk_port_get_domain_frequency(dmclk_domain_sai2) == before_sai); +} + +/** @brief Run the STM32F746G-DISCO port checks as a DMOD executable. */ +int main(int argc, char **argv) +{ + (void)argc; (void)argv; + CHECK(dmsai_port_get_instance_count() == 2); + CHECK((RCC_APB2ENR & SAI2EN) == 0); + exercise(48000); + exercise(44100); + Dmod_Printf("DMSAI PORT BOARD TEST: %s (%d failures)\n", + failures ? "FAIL" : "PASS", failures); + return failures ? 1 : 0; +} From 31f89c2740738a3dcb0cc9a825b9b4bffe25be6f Mon Sep 17 00:00:00 2001 From: Jax Calder Date: Thu, 8 Oct 2026 21:03:04 +0100 Subject: [PATCH 2/4] Clarify dmdevfs ownership of SAI device nodes --- README.md | 8 +++++--- configs/README.md | 5 +++-- docs/api-reference.md | 9 +++++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e9de7e5..5d4b526 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,11 @@ section selects the driver and each configured controller is intended to appear as `/dev/dmsaiN`. Applications use ordinary file read/write operations and driver-specific `ioctl` commands. There is no separate stream Module API. -The `dmdrvi` core remains a stub: `dmdrvi_create()` returns `NULL`, so no -device node appears yet. The STM32F7 `dmsai_port` now configures SAI hardware -and can be exercised directly by another DMOD module. +`dmdevfs` owns device node creation and mounting. The `dmsai` module's DIF +callbacks are still stubs: its `dmdrvi_create()` returns `NULL`, so `dmdevfs` +rejects this driver configuration and does not add a `/dev/dmsaiN` node yet. +The STM32F7 `dmsai_port` now configures SAI hardware and can be exercised +directly by another DMOD module. ## Device contract diff --git a/configs/README.md b/configs/README.md index 3cbccd5..08b7121 100644 --- a/configs/README.md +++ b/configs/README.md @@ -21,5 +21,6 @@ frame length. The generic MCU defaults use two 16-bit I2S slots and no board pins. The codec must be configured independently. These files are proposed inputs -for the future driver: the current `dmsai` stub returns `NULL` from -`dmdrvi_create()` and cannot expose a working `/dev/dmsaiN` device. +for the future driver: the current `dmsai` callback returns `NULL` from +`dmdrvi_create()`, so `dmdevfs` rejects the configuration instead of adding +a `/dev/dmsaiN` node. diff --git a/docs/api-reference.md b/docs/api-reference.md index 753a829..a762cc7 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -6,9 +6,10 @@ finds `driver_name=dmsai`, passes the selected `dmini_context_t` to driver implements the standard DIF methods; applications reach it through the DMOD file API, not a `dmsai_create` function. -All DIF functions are currently stubs. `create` and `open` return `NULL`; -integer operations return `-ENOSYS`; `free` and `close` do nothing. This means -the current revision registers an interface but cannot expose a working node. +All `dmsai` DIF callbacks are currently stubs. `create` and `open` return +`NULL`; integer operations return `-ENOSYS`; `free` and `close` do nothing. +`dmdevfs` already implements node mounting, but it rejects this driver when +`dmsai` returns `NULL` from `dmdrvi_create()`. No `/dev/dmsaiN` node is added. ## Configuration and node @@ -61,4 +62,4 @@ optional synchronous RX block B, and polls their FIFOs for direct transfers. It accepts frames of at most 256 bits. If a block cannot complete a stop at a frame boundary, the port resets and reconfigures that controller and records a transfer error. -The architecture-independent `dmdrvi` core remains a stub. +The architecture-independent `dmsai` DIF callbacks remain stubs. From 1aca621d32b2b4d9801c05f3d353a7708f924d60 Mon Sep 17 00:00:00 2001 From: Jax Calder Date: Fri, 9 Oct 2026 04:24:08 +0100 Subject: [PATCH 3/4] Use circular DMA for STM32F7 SAI port transfers --- README.md | 59 ++++- docs/api-reference.md | 6 +- docs/port-implementation.md | 9 +- src/port/CMakeLists.txt | 2 + src/port/stm32_common/stm32_common.c | 149 ++++------- src/port/stm32_common/stm32_sai.h | 11 + src/port/stm32_common/stm32_sai_dma.c | 331 ++++++++++++++++++++++++ src/port/stm32_common/stm32_sai_dma.h | 68 +++++ src/port/stm32f7/config.cmake | 4 +- src/port/stm32f7/port.c | 5 + tests/port-board-test/port_board_test.c | 114 ++++++++ 11 files changed, 646 insertions(+), 112 deletions(-) create mode 100644 src/port/stm32_common/stm32_sai_dma.c create mode 100644 src/port/stm32_common/stm32_sai_dma.h diff --git a/README.md b/README.md index 5d4b526..464883d 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ driver-specific `ioctl` commands. There is no separate stream Module API. `dmdevfs` owns device node creation and mounting. The `dmsai` module's DIF callbacks are still stubs: its `dmdrvi_create()` returns `NULL`, so `dmdevfs` rejects this driver configuration and does not add a `/dev/dmsaiN` node yet. -The STM32F7 `dmsai_port` now configures SAI hardware and can be exercised -directly by another DMOD module. +The STM32F7 `dmsai_port` configures SAI hardware and runs circular DMA for +direct port clients. It can be exercised by another DMOD module. ## Device contract @@ -109,6 +109,33 @@ For capture, enable `receive=on`, open the node for reading and use `Dmod_FileRead`. For full duplex, enable both directions and open with `r+`. The codec and GPIO configuration still belong to their respective drivers. +For a direct port client on STM32F746G-DISCO, configure the SAI2 pins as in +`configs/board/stm32f746g-disco/sai2.ini` and initialize the codec separately. +The following code then queues one stereo frame through circular DMA: + +```c +#include "dmod.h" +#include "dmsai_port.h" + +dmsai_config_t config = { + .instance = 1, .clock_role = dmsai_clock_master, + .framing = dmsai_frame_i2s, .pcm_format = dmsai_pcm_s16_le, + .sample_rate_hz = 48000, .tolerance_ppm = 500, + .slot_bits = 16, .slot_count = 2, .frame_bits = 32, + .active_slots = 3, .transmit = true, +}; +int16_t frame[2] = { 0, 0 }; +size_t queued = 0; +if (dmsai_port_init(&config) == 0) { + if (dmsai_port_start(1) == 0) { + dmsai_port_write(1, frame, sizeof(frame), &queued, 100); + Dmod_ThreadSleep(20); /* Let DMA drain the queue before stop. */ + dmsai_port_stop(1); + } + dmsai_port_deinit(1); +} +``` + ## Core and port layout `dmsai` will handle the `dmdrvi` DIF, INI configuration and byte/frame semantics. @@ -119,8 +146,10 @@ module lifecycle and a small hardware descriptor. Another STM32 family selects the same common source in its `config.cmake`. A different architecture can select its own common source without changing the public API. -The STM32F7 port supports master I2S and TDM, 16-bit PCM or 24/32-bit PCM in -32-bit slots, up to 16 slots, 256 frame bits and 96 kHz. It asks `dmclk_port` +The STM32F7 SAI2 port supports master I2S and TDM, 16-bit PCM or 24/32-bit PCM +in 32-bit slots, up to 16 slots, 256 frame bits and 96 kHz. SAI1 currently +returns `-ENOTSUP`: its DMA request uses channel zero, which the published +`dmdma` API reserves for `DMDMA_REQUEST_NONE`. The port asks `dmclk_port` v1.2 for the SAI kernel clock before enabling the peripheral gate, then releases the clock after stopping and gating SAI. With `MCKDIV=2`, a 48 kHz stream requests 49.152 MHz and reports the achieved sample rate. SAI A @@ -132,18 +161,22 @@ the port resets that SAI controller, restores its configuration and increments the transfer error counter. This also allows a later start or deinitialization when an external frame signal is absent. -Read and write currently poll the SAI FIFOs. This supports short direct port -transfers and hardware bring-up; it does not provide continuous buffered audio -across scheduling gaps. A later DMA-backed transfer engine is needed for -sustained playback and recording. This limitation does not affect the clock, -configuration, start/stop and status operations. +The port leases DMA2 stream 4/channel 3 for SAI2 A TX and stream 6/channel 3 +for SAI2 B RX through `dmdma` v0.5. Circular ping-pong buffers reside in the +uncached `dmheap` DMA heap. DMA half/full callbacks move whole PCM frames +between those buffers and bounded software queues; empty TX periods produce +silence, while RX queue overflow increments `rx_overruns`. This keeps DMA +memory valid across caller scheduling gaps. A successful `write` means the +port accepted bytes into its queue; call `stop` only after allowing the queued +audio time to play. The port does not configure board pins or the audio codec. `dmsai_port_board_test` is a direct hardware test on STM32F746G-DISCO. It -configures SAI2 at 48 kHz and 44.1 kHz, checks registers, starts both blocks, -writes one PCM frame, then checks that the clock and peripheral gate are +temporarily routes SAI2 pins to AF10, configures 48 kHz and 44.1 kHz streams, +checks both circular DMA streams and their progress, writes and reads PCM +through the queues, then checks that the clock and peripheral gate are released. Run it from the firmware shell after bundling `dmsai_port`, the test -module and published `dmclk_port` v1.2. It does not initialize the WM8994 -codec or verify sound on an external pin. +module, `dmdma` v0.5 and `dmclk_port` v1.2. It restores the previous GPIO +settings. The test does not initialize the WM8994 codec or verify audio quality. ## Build and tests diff --git a/docs/api-reference.md b/docs/api-reference.md index a762cc7..559b380 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -58,7 +58,11 @@ STM32 port API entry points reside in `src/port/stm32_common/stm32_common.c`; th family `port.c` is reserved for lifecycle, hardware descriptors and IRQ routing. Future STM32 families can share the common source. The STM32F7 port uses `dmclk_port` v1.2 to reserve SAI clocks, configures master block A and -optional synchronous RX block B, and polls their FIFOs for direct transfers. +optional synchronous RX block B, and uses `dmdma` circular transfers with +uncached ping-pong buffers. SAI2 uses DMA2 stream 4/channel 3 for TX and +stream 6/channel 3 for RX. Software queues decouple callers from DMA timing. +SAI1 currently returns `-ENOTSUP` because its DMA channel zero conflicts +with the published `DMDMA_REQUEST_NONE` value. It accepts frames of at most 256 bits. If a block cannot complete a stop at a frame boundary, the port resets and reconfigures that controller and records a transfer error. diff --git a/docs/port-implementation.md b/docs/port-implementation.md index fe34bd0..25cf0fe 100644 --- a/docs/port-implementation.md +++ b/docs/port-implementation.md @@ -14,7 +14,9 @@ touching architecture-independent logic. `include/dmsai_port.h` in a shared source file when the peripheral IP is common to multiple families. For STM32, these entry points are in `src/port/stm32_common/stm32_common.c` and selected by `config.cmake` through - `DMSAI_PORT_COMMON_SOURCES`. + `DMSAI_PORT_COMMON_SOURCES`. The same common source also owns the DMA + ping-pong engine in `stm32_sai_dma.c`; a family descriptor supplies only + its SAI register addresses, clock domains and DMA stream/request routes. 3. Keep `port.c` limited to family-specific lifecycle and interrupt routing. A family with different peripheral IP may provide its own implementations of the port API through `DMSAI_PORT_COMMON_SOURCES`. @@ -23,3 +25,8 @@ touching architecture-independent logic. 5. Do not introduce a module-specific variable (e.g. `_MCU_SERIES`) for this - `DMOD_CPU_FAMILY` is the ecosystem-wide convention, already wired into `dmf-get` package resolution. + +The board must configure SAI pins before `dmsai_port_init`. A direct port test +must route the target board's pins and restore their state afterward. The port +uses `dmdma` leases and obtains circular buffers from the uncached `dma` heap; +new families must provide an accessible DMA heap and valid routes. diff --git a/src/port/CMakeLists.txt b/src/port/CMakeLists.txt index 7370b84..dc56ff4 100644 --- a/src/port/CMakeLists.txt +++ b/src/port/CMakeLists.txt @@ -17,4 +17,6 @@ target_include_directories(${DMOD_MODULE_NAME}_if INTERFACE dmod_link_modules(${DMOD_MODULE_NAME} dmclk_port@>=1.2 + dmdma@>=0.5 + dmheap ) diff --git a/src/port/stm32_common/stm32_common.c b/src/port/stm32_common/stm32_common.c index ba5f064..9ad6912 100644 --- a/src/port/stm32_common/stm32_common.c +++ b/src/port/stm32_common/stm32_common.c @@ -1,6 +1,7 @@ #define DMOD_ENABLE_REGISTRATION ON #include "dmsai_port.h" #include "stm32_sai.h" +#include "stm32_sai_dma.h" #include "dmod.h" #include #include @@ -21,9 +22,8 @@ #define SAI_CLRFR 0x18U #define SAI_DR 0x1CU #define SAIEN (1U << 16) +#define SAI_DMAEN (1U << 17) #define FIFO_FLUSH (1U << 3) -#define FIFO_LEVEL(sr) (((sr) >> 16) & 7U) -#define FIFO_FULL 5U #define ERROR_FLAGS 0x75U #define MCKDIV 2U #define KERNEL_CLOCK_FACTOR (256U * 2U * MCKDIV) @@ -37,6 +37,7 @@ typedef struct { bool busy; bool tx_busy; bool rx_busy; + stm32_sai_dma_t *dma; } sai_state_t; static const stm32_sai_family_t *family; @@ -124,33 +125,14 @@ static void configure_block(unsigned instance, bool block_b, uint32_t slotr = ((config->slot_bits == 16 ? 1U : 2U) << 6) | ((config->slot_count - 1U) << 8) | (config->active_slots << 16); *block_reg(instance, block_b, SAI_CR1) = cr1; - *block_reg(instance, block_b, SAI_CR2) = FIFO_FLUSH; + *block_reg(instance, block_b, SAI_CR2) = + FIFO_FLUSH | (block_b ? 0U : 4U); /* Fill TX FIFO before first frame. */ *block_reg(instance, block_b, SAI_FRCR) = frcr; *block_reg(instance, block_b, SAI_SLOTR) = slotr; *block_reg(instance, block_b, SAI_IMR) = 0; *block_reg(instance, block_b, SAI_CLRFR) = 0x7FU; } -/** @brief Decode one little-endian PCM sample for the SAI data register. */ -static uint32_t sample_from_pcm(const uint8_t *source, uint8_t width, - dmsai_pcm_format_t format) -{ - uint32_t sample = 0; - for (uint8_t byte = 0; byte < width; ++byte) - sample |= (uint32_t)source[byte] << (8U * byte); - return format == dmsai_pcm_s24_in32_le ? sample & 0xFFFFFFU : sample; -} - -/** @brief Encode one SAI sample into little-endian PCM memory. */ -static void sample_to_pcm(uint8_t *destination, uint8_t width, - uint32_t sample, bool sign_extend) -{ - if (sign_extend && (sample & 0x800000U)) - sample |= 0xFF000000U; - for (uint8_t byte = 0; byte < width; ++byte) - destination[byte] = (uint8_t)(sample >> (8U * byte)); -} - /** @brief Accumulate and clear the hardware error flags of active blocks. */ static void count_errors(unsigned instance, sai_state_t *state) { @@ -158,34 +140,17 @@ static void count_errors(unsigned instance, sai_state_t *state) if (block && !state->config.receive) continue; uint32_t flags = *block_reg(instance, block != 0, SAI_SR) & ERROR_FLAGS; if (flags & 1U) { - if (block) ++state->status.rx_overruns; - else ++state->status.tx_underruns; + uint32_t *counter = block ? &state->status.rx_overruns : + &state->status.tx_underruns; + __atomic_fetch_add(counter, 1U, __ATOMIC_RELAXED); } - if (flags & ~1U) ++state->status.transfer_errors; + if (flags & ~1U) + __atomic_fetch_add(&state->status.transfer_errors, 1U, + __ATOMIC_RELAXED); if (flags) *block_reg(instance, block != 0, SAI_CLRFR) = flags; } } -/** @brief Wait until a FIFO can provide or accept one sample. */ -static int wait_fifo(unsigned instance, bool rx, sai_state_t *state, - Dmod_Timestamp_t started, uint32_t timeout_ms) -{ - uint32_t spins = 0; - while (state->status.running) { - uint32_t sr = *block_reg(instance, rx, SAI_SR); - if (rx ? FIFO_LEVEL(sr) != 0 : FIFO_LEVEL(sr) != FIFO_FULL) - return 0; - if (sr & ERROR_FLAGS) count_errors(instance, state); - if (timeout_ms && Dmod_GetUptime() - started >= timeout_ms) - return -ETIMEDOUT; - if (++spins == 10000U) { - Dmod_ThreadSleep(0); - spins = 0; - } - } - return -ECANCELED; -} - /** @copydoc stm32_sai_set_family */ void stm32_sai_set_family(const stm32_sai_family_t *selected) { @@ -214,6 +179,7 @@ dmod_dmsai_port_api_declaration(1.0, int, _init, int rc = validate(config); if (rc != 0) return rc; unsigned index = config->instance; + if (family->dma[index].tx_stream == UINT8_MAX) return -ENOTSUP; sai_state_t *state = &states[index]; if (state->initialized || state->busy || (*rcc_reg(RCC_APB2ENR) & family->sai_enable[index])) @@ -232,16 +198,24 @@ dmod_dmsai_port_api_declaration(1.0, int, _init, configure_block(index, false, config); if (config->receive) configure_block(index, true, config); memset(state, 0, sizeof(*state)); + state->busy = true; state->config = *config; state->sample_bytes = config->slot_bits / 8U; state->active_count = count_slots(config->active_slots); state->status.actual_sample_rate_hz = (uint32_t)((actual + KERNEL_CLOCK_FACTOR / 2U) / KERNEL_CLOCK_FACTOR); - state->initialized = true; - } else { - state->busy = false; + rc = stm32_sai_dma_create(&state->dma, &family->dma[index], + (uintptr_t)block_reg(index, false, SAI_DR), + (uintptr_t)block_reg(index, true, SAI_DR), config, + &state->status); + if (rc == 0) state->initialized = true; + else { + *rcc_reg(RCC_APB2ENR) &= ~family->sai_enable[index]; + dmclk_port_release_domain(family->clock_domain[index]); + } } + state->busy = false; return rc; } @@ -251,7 +225,11 @@ dmod_dmsai_port_api_declaration(1.0, int, _stop, { sai_state_t *state = get_state(instance); if (!state || !state->initialized) return -ENODEV; - state->status.running = false; + __atomic_store_n(&state->status.running, false, __ATOMIC_RELEASE); + *block_reg(instance, false, SAI_CR1) &= ~SAI_DMAEN; + if (state->config.receive) + *block_reg(instance, true, SAI_CR1) &= ~SAI_DMAEN; + stm32_sai_dma_stop(state->dma); if (state->config.receive) *block_reg(instance, true, SAI_CR1) &= ~SAIEN; *block_reg(instance, false, SAI_CR1) &= ~SAIEN; @@ -277,7 +255,8 @@ dmod_dmsai_port_api_declaration(1.0, int, _stop, configure_block(instance, false, &state->config); if (state->config.receive) configure_block(instance, true, &state->config); - ++state->status.transfer_errors; + __atomic_fetch_add(&state->status.transfer_errors, 1U, + __ATOMIC_RELAXED); return 0; } *block_reg(instance, false, SAI_CR2) |= FIFO_FLUSH; @@ -295,6 +274,8 @@ dmod_dmsai_port_api_declaration(1.0, int, _deinit, if (state->busy || state->tx_busy || state->rx_busy) return -EBUSY; int rc = dmsai_port_stop(instance); if (rc != 0) return rc; + stm32_sai_dma_destroy(state->dma); + state->dma = NULL; *rcc_reg(RCC_APB2ENR) &= ~family->sai_enable[instance]; rc = dmclk_port_release_domain(family->clock_domain[instance]); if (rc == 0) memset(state, 0, sizeof(*state)); @@ -307,16 +288,20 @@ dmod_dmsai_port_api_declaration(1.0, int, _start, { sai_state_t *state = get_state(instance); if (!state || !state->initialized) return -ENODEV; - if (state->status.running) return 0; + if (__atomic_load_n(&state->status.running, __ATOMIC_ACQUIRE)) return 0; *block_reg(instance, false, SAI_CLRFR) = 0x7FU; if (state->config.receive) { *block_reg(instance, true, SAI_CLRFR) = 0x7FU; - *block_reg(instance, true, SAI_CR1) |= SAIEN; } - for (unsigned i = 0; i < state->active_count && i < 8; ++i) - *block_reg(instance, false, SAI_DR) = 0; - state->status.running = true; + int rc = stm32_sai_dma_start(state->dma); + if (rc != 0) return rc; + if (state->config.receive) + *block_reg(instance, true, SAI_CR1) |= SAIEN; + __atomic_store_n(&state->status.running, true, __ATOMIC_RELEASE); *block_reg(instance, false, SAI_CR1) |= SAIEN; + *block_reg(instance, false, SAI_CR1) |= SAI_DMAEN; + if (state->config.receive) + *block_reg(instance, true, SAI_CR1) |= SAI_DMAEN; return 0; } @@ -330,28 +315,13 @@ dmod_dmsai_port_api_declaration(1.0, int, _read, size_t frame_bytes = state->active_count * state->sample_bytes; if (!received || (!buffer && size) || size % frame_bytes) return -EINVAL; *received = 0; - if (!state->status.running) return -EPIPE; - if (state->rx_busy) return -EBUSY; - state->rx_busy = true; - Dmod_Timestamp_t started = Dmod_GetUptime(); - uint8_t frame[16 * 4]; - int rc = 0; - while (*received < size) { - for (unsigned slot = 0; slot < state->active_count; ++slot) { - rc = wait_fifo(instance, true, state, started, timeout_ms); - if (rc != 0) goto done; - uint32_t sample = *block_reg(instance, true, SAI_DR); - sample_to_pcm(frame + slot * state->sample_bytes, - state->sample_bytes, sample, - state->config.pcm_format == dmsai_pcm_s24_in32_le); - } - memcpy((uint8_t *)buffer + *received, frame, frame_bytes); - *received += frame_bytes; - } -done: + if (!__atomic_load_n(&state->status.running, __ATOMIC_ACQUIRE)) return -EPIPE; + if (__atomic_exchange_n(&state->rx_busy, true, __ATOMIC_ACQ_REL)) + return -EBUSY; + int rc = stm32_sai_dma_read(state->dma, buffer, size, received, timeout_ms); count_errors(instance, state); - state->rx_busy = false; - return *received ? 0 : rc; + __atomic_store_n(&state->rx_busy, false, __ATOMIC_RELEASE); + return rc; } /** @copydoc dmsai_port_write */ @@ -364,26 +334,13 @@ dmod_dmsai_port_api_declaration(1.0, int, _write, size_t frame_bytes = state->active_count * state->sample_bytes; if (!written || (!buffer && size) || size % frame_bytes) return -EINVAL; *written = 0; - if (!state->status.running) return -EPIPE; - if (state->tx_busy) return -EBUSY; - state->tx_busy = true; - Dmod_Timestamp_t started = Dmod_GetUptime(); - int rc = 0; - while (*written < size) { - for (unsigned slot = 0; slot < state->active_count; ++slot) { - rc = wait_fifo(instance, false, state, started, timeout_ms); - if (rc != 0) goto done; - uint32_t sample = sample_from_pcm( - (const uint8_t *)buffer + *written + slot * state->sample_bytes, - state->sample_bytes, state->config.pcm_format); - *block_reg(instance, false, SAI_DR) = sample; - } - *written += frame_bytes; - } -done: + if (!__atomic_load_n(&state->status.running, __ATOMIC_ACQUIRE)) return -EPIPE; + if (__atomic_exchange_n(&state->tx_busy, true, __ATOMIC_ACQ_REL)) + return -EBUSY; + int rc = stm32_sai_dma_write(state->dma, buffer, size, written, timeout_ms); count_errors(instance, state); - state->tx_busy = false; - return *written ? 0 : rc; + __atomic_store_n(&state->tx_busy, false, __ATOMIC_RELEASE); + return rc; } /** @copydoc dmsai_port_get_status */ diff --git a/src/port/stm32_common/stm32_sai.h b/src/port/stm32_common/stm32_sai.h index 32355b7..8e70f33 100644 --- a/src/port/stm32_common/stm32_sai.h +++ b/src/port/stm32_common/stm32_sai.h @@ -2,8 +2,18 @@ #define DMSAI_STM32_SAI_H #include +#include #include "dmclk_port.h" +typedef struct +{ + uint8_t controller; + uint8_t tx_stream; + uint8_t rx_stream; + uint16_t tx_request; + uint16_t rx_request; +} stm32_sai_dma_route_t; + /* RCC and SAI addresses vary by family. Keep the register-based SAI engine * common; each family supplies only this small hardware descriptor. */ typedef struct @@ -12,6 +22,7 @@ typedef struct uintptr_t sai_base[2]; uint32_t sai_enable[2]; dmclk_domain_t clock_domain[2]; + stm32_sai_dma_route_t dma[2]; uint8_t count; } stm32_sai_family_t; diff --git a/src/port/stm32_common/stm32_sai_dma.c b/src/port/stm32_common/stm32_sai_dma.c new file mode 100644 index 0000000..592ad13 --- /dev/null +++ b/src/port/stm32_common/stm32_sai_dma.c @@ -0,0 +1,331 @@ +#include "stm32_sai_dma.h" +#include "dmdma_lease.h" +#include "dmheap.h" +#include "dmod.h" +#include +#include +#include + +#define DMA_HALF_FRAMES 64U +#define DMA_QUEUE_FRAMES 256U +#define DMA_ALIGNMENT 32U + +typedef struct { + struct stm32_sai_dma *owner; + bool receive; +} dma_lane_t; + +struct stm32_sai_dma { + dmdma_lease_t tx_lease; + dmdma_lease_t rx_lease; + dmheap_context_t *heap; + uint32_t *tx_dma; + uint32_t *rx_dma; + uint32_t *tx_queue; + uint32_t *rx_queue; + dmsai_status_t *status; + dma_lane_t tx_lane; + dma_lane_t rx_lane; + uintptr_t tx_dr; + uintptr_t rx_dr; + stm32_sai_dma_route_t route; + dmsai_pcm_format_t format; + uint32_t half_words; + uint32_t queue_words; + uint32_t tx_head; + uint32_t tx_tail; + uint32_t rx_head; + uint32_t rx_tail; + uint8_t frame_words; + uint8_t sample_bytes; + bool transmit; + bool receive; + bool active; + bool fault; + bool tx_has_data; +}; + +/** @brief Decode one little-endian PCM sample into a DMA word. */ +static uint32_t from_pcm(const uint8_t *source, uint8_t width, + dmsai_pcm_format_t format) +{ + uint32_t sample = 0; + for (uint8_t i = 0; i < width; ++i) + sample |= (uint32_t)source[i] << (i * 8U); + return format == dmsai_pcm_s24_in32_le ? sample & 0xFFFFFFU : sample; +} + +/** @brief Encode one DMA sample as little-endian PCM. */ +static void to_pcm(uint8_t *destination, uint8_t width, uint32_t sample, + dmsai_pcm_format_t format) +{ + if (format == dmsai_pcm_s24_in32_le && (sample & 0x800000U)) + sample |= 0xFF000000U; + for (uint8_t i = 0; i < width; ++i) + destination[i] = (uint8_t)(sample >> (i * 8U)); +} + +/** @brief Refill the completed TX half from queued whole frames or silence. */ +static void refill_tx(struct stm32_sai_dma *dma, unsigned half) +{ + uint32_t *destination = dma->tx_dma + half * dma->half_words; + bool underrun = false; + for (uint32_t i = 0; i < DMA_HALF_FRAMES; ++i) { + uint32_t tail = dma->tx_tail; + uint32_t head = __atomic_load_n(&dma->tx_head, __ATOMIC_ACQUIRE); + uint32_t *frame = destination + i * dma->frame_words; + if (dma->transmit && head - tail >= dma->frame_words) { + uint32_t start = tail % dma->queue_words; + memcpy(frame, dma->tx_queue + start, + dma->frame_words * sizeof(uint32_t)); + __atomic_store_n(&dma->tx_tail, tail + dma->frame_words, + __ATOMIC_RELEASE); + } else { + memset(frame, 0, dma->frame_words * sizeof(uint32_t)); + if (dma->transmit && + __atomic_load_n(&dma->tx_has_data, __ATOMIC_ACQUIRE)) + underrun = true; + } + } + if (underrun) + __atomic_fetch_add(&dma->status->tx_underruns, 1U, __ATOMIC_RELAXED); +} + +/** @brief Move a completed RX half into the software queue in whole frames. */ +static void collect_rx(struct stm32_sai_dma *dma, unsigned half) +{ + const uint32_t *source = dma->rx_dma + half * dma->half_words; + bool overrun = false; + for (uint32_t i = 0; i < DMA_HALF_FRAMES; ++i) { + uint32_t head = dma->rx_head; + uint32_t tail = __atomic_load_n(&dma->rx_tail, __ATOMIC_ACQUIRE); + if (head - tail + dma->frame_words <= dma->queue_words) { + uint32_t start = head % dma->queue_words; + memcpy(dma->rx_queue + start, source + i * dma->frame_words, + dma->frame_words * sizeof(uint32_t)); + __atomic_store_n(&dma->rx_head, head + dma->frame_words, + __ATOMIC_RELEASE); + } else { + overrun = true; + } + } + if (overrun) + __atomic_fetch_add(&dma->status->rx_overruns, 1U, __ATOMIC_RELAXED); +} + +/** @brief Handle DMA half and full buffer interrupts without touching user memory. */ +static void dma_event(dmdma_lease_t lease, dmdma_event_t event, void *user_ptr) +{ + (void)lease; + dma_lane_t *lane = user_ptr; + struct stm32_sai_dma *dma = lane->owner; + if (!__atomic_load_n(&dma->active, __ATOMIC_ACQUIRE)) return; + 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); + return; + } + if (event & dmdma_event_half_complete) { + if (lane->receive) collect_rx(dma, 0); + else refill_tx(dma, 0); + } + if (event & dmdma_event_complete) { + if (lane->receive) collect_rx(dma, 1); + else refill_tx(dma, 1); + } +} + +/** @brief Allocate a circular buffer from the firmware's uncached DMA heap. */ +static uint32_t *alloc_dma_buffer(struct stm32_sai_dma *dma) +{ + return dmheap_aligned_alloc(dma->heap, DMA_ALIGNMENT, + 2U * dma->half_words * sizeof(uint32_t), "dmsai_port"); +} + +/** @brief Reserve one route and register its interrupt callback. */ +static int reserve_lane(struct stm32_sai_dma *dma, bool receive) +{ + dmdma_lease_t *lease = receive ? &dma->rx_lease : &dma->tx_lease; + uint8_t stream = receive ? dma->route.rx_stream : dma->route.tx_stream; + dma_lane_t *lane = receive ? &dma->rx_lane : &dma->tx_lane; + *lease = dmdma_lease_acquire(dma->route.controller, stream); + if (!*lease) return -EBUSY; + lane->owner = dma; + lane->receive = receive; + return dmdma_lease_set_callback(*lease, dma_event, lane) == 0 ? 0 : -EIO; +} + +/** @copydoc stm32_sai_dma_create */ +int stm32_sai_dma_create(stm32_sai_dma_t **result, + const stm32_sai_dma_route_t *route, uintptr_t tx_dr, uintptr_t rx_dr, + const dmsai_config_t *config, dmsai_status_t *status) +{ + if (!result || !route || !config || !status) return -EINVAL; + *result = NULL; + if (route->tx_stream == UINT8_MAX) return -ENOTSUP; + stm32_sai_dma_t *dma = Dmod_Malloc(sizeof(*dma)); + if (!dma) return -ENOMEM; + memset(dma, 0, sizeof(*dma)); + dma->heap = dmheap_get_context_by_name("dma"); + dma->route = *route; + dma->tx_dr = tx_dr; + dma->rx_dr = rx_dr; + dma->status = status; + dma->format = config->pcm_format; + dma->frame_words = (uint8_t)__builtin_popcount(config->active_slots); + dma->sample_bytes = config->slot_bits / 8U; + dma->half_words = DMA_HALF_FRAMES * dma->frame_words; + dma->queue_words = DMA_QUEUE_FRAMES * dma->frame_words; + dma->transmit = config->transmit; + dma->receive = config->receive; + int rc = dma->heap ? 0 : -ENODEV; + 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); + if (rc == 0 && dma->receive && !dma->rx_dma) rc = -ENOMEM; + if (rc == 0 && dma->transmit) + dma->tx_queue = Dmod_Malloc(dma->queue_words * sizeof(uint32_t)); + if (rc == 0 && dma->transmit && !dma->tx_queue) rc = -ENOMEM; + if (rc == 0 && dma->receive) + dma->rx_queue = Dmod_Malloc(dma->queue_words * sizeof(uint32_t)); + if (rc == 0 && dma->receive && !dma->rx_queue) rc = -ENOMEM; + if (rc == 0) rc = reserve_lane(dma, false); + if (rc == 0 && dma->receive) rc = reserve_lane(dma, true); + if (rc != 0) { stm32_sai_dma_destroy(dma); return rc; } + *result = dma; + return 0; +} + +/** @copydoc stm32_sai_dma_destroy */ +void stm32_sai_dma_destroy(stm32_sai_dma_t *dma) +{ + if (!dma) return; + stm32_sai_dma_stop(dma); + if (dma->rx_lease) dmdma_lease_release(dma->rx_lease); + if (dma->tx_lease) dmdma_lease_release(dma->tx_lease); + if (dma->rx_dma) dmheap_free(dma->heap, dma->rx_dma, true); + 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); + Dmod_Free(dma); +} + +/** @brief Program one circular DMA stream to or from a SAI data register. */ +static int start_lane(stm32_sai_dma_t *dma, bool receive) +{ + void *dr = (void *)(receive ? dma->rx_dr : dma->tx_dr); + uint32_t *buffer = receive ? dma->rx_dma : dma->tx_dma; + dmdma_transfer_config_t cfg = { + .direction = receive ? dmdma_direction_peripheral_to_memory + : dmdma_direction_memory_to_peripheral, + .request = receive ? dma->route.rx_request : dma->route.tx_request, + .source_address = receive ? dr : buffer, + .destination_address = receive ? buffer : dr, + .source_width = dmdma_data_width_word, + .destination_width = dmdma_data_width_word, + .source_increment = !receive, + .destination_increment = receive, + .circular = true, + .priority = dmdma_priority_high, + .element_count = 2U * dma->half_words, + .timeout_ms = 0, + }; + return dmdma_lease_start(receive ? dma->rx_lease : dma->tx_lease, &cfg); +} + +/** @copydoc stm32_sai_dma_start */ +int stm32_sai_dma_start(stm32_sai_dma_t *dma) +{ + if (!dma) return -ENODEV; + memset(dma->tx_dma, 0, 2U * dma->half_words * sizeof(uint32_t)); + if (dma->receive) + memset(dma->rx_dma, 0, 2U * dma->half_words * sizeof(uint32_t)); + dma->tx_head = dma->tx_tail = dma->rx_head = dma->rx_tail = 0; + dma->tx_has_data = false; + __atomic_store_n(&dma->fault, false, __ATOMIC_RELEASE); + __atomic_store_n(&dma->active, true, __ATOMIC_RELEASE); + int rc = dma->receive ? start_lane(dma, true) : 0; + if (rc == 0) rc = start_lane(dma, false); + if (rc != 0) stm32_sai_dma_stop(dma); + return rc; +} + +/** @copydoc stm32_sai_dma_stop */ +void stm32_sai_dma_stop(stm32_sai_dma_t *dma) +{ + if (!dma) return; + __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); +} + +/** @brief Return a transfer error or wait for one queue change. */ +static int wait_queue(stm32_sai_dma_t *dma, Dmod_Timestamp_t started, + uint32_t timeout_ms) +{ + if (__atomic_load_n(&dma->fault, __ATOMIC_ACQUIRE)) return -EIO; + if (!__atomic_load_n(&dma->active, __ATOMIC_ACQUIRE)) return -ECANCELED; + if (timeout_ms && Dmod_GetUptime() - started >= timeout_ms) + return -ETIMEDOUT; + Dmod_ThreadSleep(1); + return 0; +} + +/** @copydoc stm32_sai_dma_write */ +int stm32_sai_dma_write(stm32_sai_dma_t *dma, const void *buffer, + size_t size, size_t *written, uint32_t timeout_ms) +{ + *written = 0; + uint32_t frame_bytes = dma->frame_words * dma->sample_bytes; + Dmod_Timestamp_t started = Dmod_GetUptime(); + int rc = 0; + while (*written < size) { + if (__atomic_load_n(&dma->fault, __ATOMIC_ACQUIRE)) { rc = -EIO; break; } + if (!__atomic_load_n(&dma->active, __ATOMIC_ACQUIRE)) { + rc = -ECANCELED; break; + } + uint32_t head = dma->tx_head; + uint32_t tail = __atomic_load_n(&dma->tx_tail, __ATOMIC_ACQUIRE); + if (head - tail + dma->frame_words <= dma->queue_words) { + uint32_t start = head % dma->queue_words; + const uint8_t *pcm = (const uint8_t *)buffer + *written; + for (uint8_t i = 0; i < dma->frame_words; ++i) + dma->tx_queue[start + i] = from_pcm(pcm + i * dma->sample_bytes, + dma->sample_bytes, dma->format); + __atomic_store_n(&dma->tx_head, head + dma->frame_words, + __ATOMIC_RELEASE); + __atomic_store_n(&dma->tx_has_data, true, __ATOMIC_RELEASE); + *written += frame_bytes; + } else if ((rc = wait_queue(dma, started, timeout_ms)) != 0) break; + } + return *written ? 0 : rc; +} + +/** @copydoc stm32_sai_dma_read */ +int stm32_sai_dma_read(stm32_sai_dma_t *dma, void *buffer, + size_t size, size_t *received, uint32_t timeout_ms) +{ + *received = 0; + uint32_t frame_bytes = dma->frame_words * dma->sample_bytes; + Dmod_Timestamp_t started = Dmod_GetUptime(); + int rc = 0; + while (*received < size) { + if (__atomic_load_n(&dma->fault, __ATOMIC_ACQUIRE)) { rc = -EIO; break; } + if (!__atomic_load_n(&dma->active, __ATOMIC_ACQUIRE)) { + rc = -ECANCELED; break; + } + uint32_t tail = dma->rx_tail; + uint32_t head = __atomic_load_n(&dma->rx_head, __ATOMIC_ACQUIRE); + if (head - tail >= dma->frame_words) { + uint32_t start = tail % dma->queue_words; + uint8_t *pcm = (uint8_t *)buffer + *received; + for (uint8_t i = 0; i < dma->frame_words; ++i) + to_pcm(pcm + i * dma->sample_bytes, dma->sample_bytes, + dma->rx_queue[start + i], dma->format); + __atomic_store_n(&dma->rx_tail, tail + dma->frame_words, + __ATOMIC_RELEASE); + *received += frame_bytes; + } else if ((rc = wait_queue(dma, started, timeout_ms)) != 0) break; + } + return *received ? 0 : rc; +} diff --git a/src/port/stm32_common/stm32_sai_dma.h b/src/port/stm32_common/stm32_sai_dma.h new file mode 100644 index 0000000..aa502eb --- /dev/null +++ b/src/port/stm32_common/stm32_sai_dma.h @@ -0,0 +1,68 @@ +#ifndef DMSAI_STM32_SAI_DMA_H +#define DMSAI_STM32_SAI_DMA_H + +#include +#include +#include "dmsai_types.h" +#include "stm32_sai.h" + +typedef struct stm32_sai_dma stm32_sai_dma_t; + +/** + * @brief Reserve DMA streams and allocate uncached circular SAI buffers. + * @param result Receives the created context on success, or NULL on failure. + * @param route Family-specific DMA controller, stream and request mapping. + * @param tx_dr Address of the transmitting SAI data register. + * @param rx_dr Address of the receiving SAI data register. + * @param config Validated port configuration retained by the caller. + * @param status Mutable counters retained by the caller. + * @return 0 on success or a negative errno value. + */ +int stm32_sai_dma_create(stm32_sai_dma_t **result, + const stm32_sai_dma_route_t *route, uintptr_t tx_dr, uintptr_t rx_dr, + const dmsai_config_t *config, dmsai_status_t *status); + +/** + * @brief Abort transfers, release their leases and free all DMA storage. + * @param dma Context to destroy; NULL is allowed. + */ +void stm32_sai_dma_destroy(stm32_sai_dma_t *dma); + +/** + * @brief Start circular DMA for the configured SAI directions. + * @param dma Initialized context. + * @return 0 on success or a negative errno value. + */ +int stm32_sai_dma_start(stm32_sai_dma_t *dma); + +/** + * @brief Stop circular DMA before disabling the SAI blocks. + * @param dma Context to stop; NULL is allowed. + */ +void stm32_sai_dma_stop(stm32_sai_dma_t *dma); + +/** + * @brief Copy complete PCM frames into the software TX queue. + * @param dma Running context. + * @param buffer Source PCM bytes. + * @param size Number of bytes to copy. + * @param written Receives accepted byte count. + * @param timeout_ms Wait limit, or zero for no limit. + * @return 0 if any frames were accepted, otherwise a negative errno value. + */ +int stm32_sai_dma_write(stm32_sai_dma_t *dma, const void *buffer, + size_t size, size_t *written, uint32_t timeout_ms); + +/** + * @brief Copy complete PCM frames out of the software RX queue. + * @param dma Running context. + * @param buffer Destination for PCM bytes. + * @param size Number of bytes to copy. + * @param received Receives copied byte count. + * @param timeout_ms Wait limit, or zero for no limit. + * @return 0 if any frames were copied, otherwise a negative errno value. + */ +int stm32_sai_dma_read(stm32_sai_dma_t *dma, void *buffer, + size_t size, size_t *received, uint32_t timeout_ms); + +#endif diff --git a/src/port/stm32f7/config.cmake b/src/port/stm32f7/config.cmake index 403e50a..ad9ca8f 100644 --- a/src/port/stm32f7/config.cmake +++ b/src/port/stm32f7/config.cmake @@ -1,2 +1,4 @@ set(DMOD_TOOLS_NAME "arch/armv7/cortex-m7" CACHE STRING "DMOD toolchain configuration") -set(DMSAI_PORT_COMMON_SOURCES stm32_common/stm32_common.c) +set(DMSAI_PORT_COMMON_SOURCES + stm32_common/stm32_common.c + stm32_common/stm32_sai_dma.c) diff --git a/src/port/stm32f7/port.c b/src/port/stm32f7/port.c index 88182b1..5ac13d1 100644 --- a/src/port/stm32f7/port.c +++ b/src/port/stm32f7/port.c @@ -7,6 +7,11 @@ static const stm32_sai_family_t stm32f7_sai = { .sai_base = {0x40015800U, 0x40015C00U}, .sai_enable = {1U << 22, 1U << 23}, .clock_domain = {dmclk_domain_sai1, dmclk_domain_sai2}, + .dma = { + { .controller = 1, .tx_stream = UINT8_MAX }, + { .controller = 1, .tx_stream = 4, .rx_stream = 6, + .tx_request = 3, .rx_request = 3 }, + }, .count = 2, }; diff --git a/tests/port-board-test/port_board_test.c b/tests/port-board-test/port_board_test.c index 84025b1..8dffaba 100644 --- a/tests/port-board-test/port_board_test.c +++ b/tests/port-board-test/port_board_test.c @@ -5,11 +5,83 @@ #define RCC_APB2ENR (*(volatile uint32_t *)0x40023844U) #define RCC_CR (*(volatile uint32_t *)0x40023800U) +#define RCC_AHB1ENR (*(volatile uint32_t *)0x40023830U) #define SAI2_ACR1 (*(volatile uint32_t *)0x40015C04U) #define SAI2_BCR1 (*(volatile uint32_t *)0x40015C24U) #define SAI2_AFRCR (*(volatile uint32_t *)0x40015C0CU) #define SAI2_ASLOTR (*(volatile uint32_t *)0x40015C10U) +#define SAI2_ASR (*(volatile uint32_t *)0x40015C18U) +#define SAI2_BSR (*(volatile uint32_t *)0x40015C38U) #define SAI2EN (1U << 23) +#define DMA2_S4CR (*(volatile uint32_t *)0x40026470U) +#define DMA2_S4NDTR (*(volatile uint32_t *)0x40026474U) +#define DMA2_S6CR (*(volatile uint32_t *)0x400264A0U) +#define DMA2_S6NDTR (*(volatile uint32_t *)0x400264A4U) +#define GPIOG_BASE 0x40021800U +#define GPIOI_BASE 0x40022000U + +typedef struct { + uint32_t ahb1enr; + uint32_t g_moder, g_otyper, g_ospeedr, g_pupdr, g_afrh; + uint32_t i_moder, i_otyper, i_ospeedr, i_pupdr, i_afrl; +} pin_backup_t; + +/** @brief Access a GPIO register on the board's SAI2 pin banks. */ +static volatile uint32_t *gpio_reg(uintptr_t base, uint32_t offset) +{ + return (volatile uint32_t *)(base + offset); +} + +/** @brief Route the Discovery board's SAI2 signals through AF10. */ +static void setup_sai2_pins(pin_backup_t *backup) +{ + backup->ahb1enr = RCC_AHB1ENR; + RCC_AHB1ENR |= (1U << 6) | (1U << 8); + backup->g_moder = *gpio_reg(GPIOG_BASE, 0); + backup->g_otyper = *gpio_reg(GPIOG_BASE, 4); + backup->g_ospeedr = *gpio_reg(GPIOG_BASE, 8); + backup->g_pupdr = *gpio_reg(GPIOG_BASE, 12); + backup->g_afrh = *gpio_reg(GPIOG_BASE, 36); + backup->i_moder = *gpio_reg(GPIOI_BASE, 0); + backup->i_otyper = *gpio_reg(GPIOI_BASE, 4); + backup->i_ospeedr = *gpio_reg(GPIOI_BASE, 8); + backup->i_pupdr = *gpio_reg(GPIOI_BASE, 12); + backup->i_afrl = *gpio_reg(GPIOI_BASE, 32); + for (unsigned pin = 4; pin <= 7; ++pin) { + *gpio_reg(GPIOI_BASE, 0) = + (*gpio_reg(GPIOI_BASE, 0) & ~(3U << (pin * 2))) | + (2U << (pin * 2)); + *gpio_reg(GPIOI_BASE, 4) &= ~(1U << pin); + *gpio_reg(GPIOI_BASE, 8) |= 3U << (pin * 2); + *gpio_reg(GPIOI_BASE, 12) &= ~(3U << (pin * 2)); + *gpio_reg(GPIOI_BASE, 32) = + (*gpio_reg(GPIOI_BASE, 32) & ~(15U << (pin * 4))) | + (10U << (pin * 4)); + } + *gpio_reg(GPIOG_BASE, 0) = + (*gpio_reg(GPIOG_BASE, 0) & ~(3U << 20)) | (2U << 20); + *gpio_reg(GPIOG_BASE, 4) &= ~(1U << 10); + *gpio_reg(GPIOG_BASE, 8) |= 3U << 20; + *gpio_reg(GPIOG_BASE, 12) &= ~(3U << 20); + *gpio_reg(GPIOG_BASE, 36) = + (*gpio_reg(GPIOG_BASE, 36) & ~(15U << 8)) | (10U << 8); +} + +/** @brief Return board GPIO registers to their pretest state. */ +static void restore_sai2_pins(const pin_backup_t *backup) +{ + *gpio_reg(GPIOG_BASE, 0) = backup->g_moder; + *gpio_reg(GPIOG_BASE, 4) = backup->g_otyper; + *gpio_reg(GPIOG_BASE, 8) = backup->g_ospeedr; + *gpio_reg(GPIOG_BASE, 12) = backup->g_pupdr; + *gpio_reg(GPIOG_BASE, 36) = backup->g_afrh; + *gpio_reg(GPIOI_BASE, 0) = backup->i_moder; + *gpio_reg(GPIOI_BASE, 4) = backup->i_otyper; + *gpio_reg(GPIOI_BASE, 8) = backup->i_ospeedr; + *gpio_reg(GPIOI_BASE, 12) = backup->i_pupdr; + *gpio_reg(GPIOI_BASE, 32) = backup->i_afrl; + RCC_AHB1ENR = backup->ahb1enr; +} static int failures; #define CHECK(test) do { if (!(test)) { \ @@ -48,10 +120,49 @@ static void exercise(uint32_t rate) CHECK(dmsai_port_start(1) == 0); CHECK((SAI2_ACR1 & (1U << 16)) != 0); CHECK((SAI2_BCR1 & (1U << 16)) != 0); + CHECK((SAI2_ACR1 & (1U << 17)) != 0); + CHECK((SAI2_BCR1 & (1U << 17)) != 0); + CHECK((DMA2_S4CR & 0x101U) == 0x101U); + CHECK((DMA2_S6CR & 0x101U) == 0x101U); + uint32_t tx_before = DMA2_S4NDTR; + uint32_t rx_before = DMA2_S6NDTR; + bool tx_progress = false; + bool rx_progress = false; + for (unsigned i = 0; i < 10; ++i) { + Dmod_ThreadSleep(1); + if (DMA2_S4NDTR != tx_before) tx_progress = true; + if (DMA2_S6NDTR != rx_before) rx_progress = true; + } + Dmod_Printf("DMA S4 %08x/%u->%u S6 %08x/%u->%u SAI SR %08x/%08x\n", + (unsigned)DMA2_S4CR, (unsigned)tx_before, (unsigned)DMA2_S4NDTR, + (unsigned)DMA2_S6CR, (unsigned)rx_before, (unsigned)DMA2_S6NDTR, + (unsigned)SAI2_ASR, (unsigned)SAI2_BSR); + CHECK(tx_progress); + CHECK(rx_progress); const int16_t pcm[4] = {0, 0, 0, 0}; size_t written = 0; rc = dmsai_port_write(1, pcm, sizeof(pcm), &written, 50); CHECK(rc == 0 && written == sizeof(pcm)); + int16_t *burst = Dmod_Malloc(300U * 2U * sizeof(int16_t)); + CHECK(burst != NULL); + if (burst) { + for (unsigned i = 0; i < 600U; ++i) burst[i] = (int16_t)i; + written = 0; + rc = dmsai_port_write(1, burst, 300U * 2U * sizeof(int16_t), + &written, 100); + CHECK(rc == 0 && written == 300U * 2U * sizeof(int16_t)); + Dmod_Free(burst); + } + int16_t captured[2] = {0}; + size_t received = 0; + rc = dmsai_port_read(1, captured, sizeof(captured), &received, 50); + CHECK(rc == 0 && received == sizeof(captured)); + CHECK(dmsai_port_get_status(1, &status) == 0); + CHECK(status.running); + CHECK(status.transfer_errors == 0); + Dmod_Printf("SAI status tx_underrun=%u rx_overrun=%u transfer_error=%u\n", + (unsigned)status.tx_underruns, (unsigned)status.rx_overruns, + (unsigned)status.transfer_errors); CHECK(dmsai_port_stop(1) == 0); CHECK(dmsai_port_deinit(1) == 0); CHECK((RCC_APB2ENR & SAI2EN) == 0); @@ -63,10 +174,13 @@ static void exercise(uint32_t rate) int main(int argc, char **argv) { (void)argc; (void)argv; + pin_backup_t pins; + setup_sai2_pins(&pins); CHECK(dmsai_port_get_instance_count() == 2); CHECK((RCC_APB2ENR & SAI2EN) == 0); exercise(48000); exercise(44100); + restore_sai2_pins(&pins); Dmod_Printf("DMSAI PORT BOARD TEST: %s (%d failures)\n", failures ? "FAIL" : "PASS", failures); return failures ? 1 : 0; From 3fe7665935ad1a05433034fe6c808d7299e33375 Mon Sep 17 00:00:00 2001 From: Jax Calder Date: Fri, 9 Oct 2026 04:48:41 +0100 Subject: [PATCH 4/4] Enable DMA transfers on STM32F7 SAI1 --- README.md | 19 +-- docs/api-reference.md | 9 +- src/port/stm32f7/port.c | 8 +- tests/port-board-test/port_board_test.c | 165 ++++++++++++++++-------- 4 files changed, 136 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 464883d..84b37a7 100644 --- a/README.md +++ b/README.md @@ -146,10 +146,9 @@ module lifecycle and a small hardware descriptor. Another STM32 family selects the same common source in its `config.cmake`. A different architecture can select its own common source without changing the public API. -The STM32F7 SAI2 port supports master I2S and TDM, 16-bit PCM or 24/32-bit PCM -in 32-bit slots, up to 16 slots, 256 frame bits and 96 kHz. SAI1 currently -returns `-ENOTSUP`: its DMA request uses channel zero, which the published -`dmdma` API reserves for `DMDMA_REQUEST_NONE`. The port asks `dmclk_port` +The STM32F7 SAI1 and SAI2 ports support master I2S and TDM, 16-bit PCM or +24/32-bit PCM in 32-bit slots, up to 16 slots, 256 frame bits and 96 kHz. +The port asks `dmclk_port` v1.2 for the SAI kernel clock before enabling the peripheral gate, then releases the clock after stopping and gating SAI. With `MCKDIV=2`, a 48 kHz stream requests 49.152 MHz and reports the achieved sample rate. SAI A @@ -161,8 +160,11 @@ the port resets that SAI controller, restores its configuration and increments the transfer error counter. This also allows a later start or deinitialization when an external frame signal is absent. -The port leases DMA2 stream 4/channel 3 for SAI2 A TX and stream 6/channel 3 -for SAI2 B RX through `dmdma` v0.5. Circular ping-pong buffers reside in the +The port leases DMA2 stream 1/channel 0 for SAI1 A TX, stream 5/channel 0 for +SAI1 B RX, stream 4/channel 3 for SAI2 A TX and stream 6/channel 3 for SAI2 B +RX through `dmdma` v0.5. STM32 channel 0 is passed as request 8: the published +STM32 port uses the low three request bits for the channel selector, while +request 0 means `DMDMA_REQUEST_NONE`. Circular ping-pong buffers reside in the uncached `dmheap` DMA heap. DMA half/full callbacks move whole PCM frames between those buffers and bounded software queues; empty TX periods produce silence, while RX queue overflow increments `rx_overruns`. This keeps DMA @@ -171,8 +173,9 @@ port accepted bytes into its queue; call `stop` only after allowing the queued audio time to play. The port does not configure board pins or the audio codec. `dmsai_port_board_test` is a direct hardware test on STM32F746G-DISCO. It -temporarily routes SAI2 pins to AF10, configures 48 kHz and 44.1 kHz streams, -checks both circular DMA streams and their progress, writes and reads PCM +temporarily routes SAI1 pins to AF6 and SAI2 pins to AF10, configures 48 kHz +and 44.1 kHz streams on both controllers, checks circular DMA progress, +writes and reads PCM through the queues, then checks that the clock and peripheral gate are released. Run it from the firmware shell after bundling `dmsai_port`, the test module, `dmdma` v0.5 and `dmclk_port` v1.2. It restores the previous GPIO diff --git a/docs/api-reference.md b/docs/api-reference.md index 559b380..7ec277c 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -59,10 +59,11 @@ family `port.c` is reserved for lifecycle, hardware descriptors and IRQ routing. Future STM32 families can share the common source. The STM32F7 port uses `dmclk_port` v1.2 to reserve SAI clocks, configures master block A and optional synchronous RX block B, and uses `dmdma` circular transfers with -uncached ping-pong buffers. SAI2 uses DMA2 stream 4/channel 3 for TX and -stream 6/channel 3 for RX. Software queues decouple callers from DMA timing. -SAI1 currently returns `-ENOTSUP` because its DMA channel zero conflicts -with the published `DMDMA_REQUEST_NONE` value. +uncached ping-pong buffers. SAI1 uses DMA2 stream 1/channel 0 for TX and +stream 5/channel 0 for RX; SAI2 uses stream 4/channel 3 for TX and stream +6/channel 3 for RX. The published STM32 `dmdma` port accepts request 8 for +hardware channel 0, keeping it distinct from `DMDMA_REQUEST_NONE` (zero). +Software queues decouple callers from DMA timing. It accepts frames of at most 256 bits. If a block cannot complete a stop at a frame boundary, the port resets and reconfigures that controller and records a transfer error. diff --git a/src/port/stm32f7/port.c b/src/port/stm32f7/port.c index 5ac13d1..f3b8b74 100644 --- a/src/port/stm32f7/port.c +++ b/src/port/stm32f7/port.c @@ -2,13 +2,19 @@ #include "dmod.h" #include "../stm32_common/stm32_sai.h" +#define STM32_DMA_CH0_REQUEST 8U + static const stm32_sai_family_t stm32f7_sai = { .rcc_base = 0x40023800U, .sai_base = {0x40015800U, 0x40015C00U}, .sai_enable = {1U << 22, 1U << 23}, .clock_domain = {dmclk_domain_sai1, dmclk_domain_sai2}, .dma = { - { .controller = 1, .tx_stream = UINT8_MAX }, + /* dmdma reserves request 0; STM32 CHSEL uses the low three bits, + * so request 8 selects hardware channel 0 without ambiguity. */ + { .controller = 1, .tx_stream = 1, .rx_stream = 5, + .tx_request = STM32_DMA_CH0_REQUEST, + .rx_request = STM32_DMA_CH0_REQUEST }, { .controller = 1, .tx_stream = 4, .rx_stream = 6, .tx_request = 3, .rx_request = 3 }, }, diff --git a/tests/port-board-test/port_board_test.c b/tests/port-board-test/port_board_test.c index 8dffaba..dbe7185 100644 --- a/tests/port-board-test/port_board_test.c +++ b/tests/port-board-test/port_board_test.c @@ -6,17 +6,9 @@ #define RCC_APB2ENR (*(volatile uint32_t *)0x40023844U) #define RCC_CR (*(volatile uint32_t *)0x40023800U) #define RCC_AHB1ENR (*(volatile uint32_t *)0x40023830U) -#define SAI2_ACR1 (*(volatile uint32_t *)0x40015C04U) -#define SAI2_BCR1 (*(volatile uint32_t *)0x40015C24U) -#define SAI2_AFRCR (*(volatile uint32_t *)0x40015C0CU) -#define SAI2_ASLOTR (*(volatile uint32_t *)0x40015C10U) -#define SAI2_ASR (*(volatile uint32_t *)0x40015C18U) -#define SAI2_BSR (*(volatile uint32_t *)0x40015C38U) -#define SAI2EN (1U << 23) -#define DMA2_S4CR (*(volatile uint32_t *)0x40026470U) -#define DMA2_S4NDTR (*(volatile uint32_t *)0x40026474U) -#define DMA2_S6CR (*(volatile uint32_t *)0x400264A0U) -#define DMA2_S6NDTR (*(volatile uint32_t *)0x400264A4U) +#define SAI_BASE 0x40015800U +#define DMA2_BASE 0x40026400U +#define GPIOE_BASE 0x40021000U #define GPIOG_BASE 0x40021800U #define GPIOI_BASE 0x40022000U @@ -26,12 +18,65 @@ typedef struct { uint32_t i_moder, i_otyper, i_ospeedr, i_pupdr, i_afrl; } pin_backup_t; +typedef struct { + uint32_t ahb1enr; + uint32_t moder, otyper, ospeedr, pupdr, afrl; +} sai1_pin_backup_t; + +/** @brief Access one register of an SAI instance and audio block. */ +static volatile uint32_t *sai_reg(unsigned instance, bool block_b, + uint32_t offset) +{ + return (volatile uint32_t *)(SAI_BASE + instance * 0x400U + + (block_b ? 0x20U : 0U) + offset); +} + +/** @brief Access one DMA2 stream register. */ +static volatile uint32_t *dma_reg(unsigned stream, uint32_t offset) +{ + return (volatile uint32_t *)(DMA2_BASE + 0x10U + stream * 0x18U + offset); +} + /** @brief Access a GPIO register on the board's SAI2 pin banks. */ static volatile uint32_t *gpio_reg(uintptr_t base, uint32_t offset) { return (volatile uint32_t *)(base + offset); } +/** @brief Route SAI1 A clocks/TX and block B RX through port E AF6. */ +static void setup_sai1_pins(sai1_pin_backup_t *backup) +{ + backup->ahb1enr = RCC_AHB1ENR; + RCC_AHB1ENR |= 1U << 4; + backup->moder = *gpio_reg(GPIOE_BASE, 0); + backup->otyper = *gpio_reg(GPIOE_BASE, 4); + backup->ospeedr = *gpio_reg(GPIOE_BASE, 8); + backup->pupdr = *gpio_reg(GPIOE_BASE, 12); + backup->afrl = *gpio_reg(GPIOE_BASE, 32); + for (unsigned pin = 2; pin <= 6; ++pin) { + *gpio_reg(GPIOE_BASE, 0) = + (*gpio_reg(GPIOE_BASE, 0) & ~(3U << (pin * 2))) | + (2U << (pin * 2)); + *gpio_reg(GPIOE_BASE, 4) &= ~(1U << pin); + *gpio_reg(GPIOE_BASE, 8) |= 3U << (pin * 2); + *gpio_reg(GPIOE_BASE, 12) &= ~(3U << (pin * 2)); + *gpio_reg(GPIOE_BASE, 32) = + (*gpio_reg(GPIOE_BASE, 32) & ~(15U << (pin * 4))) | + (6U << (pin * 4)); + } +} + +/** @brief Restore port E after the direct SAI1 test. */ +static void restore_sai1_pins(const sai1_pin_backup_t *backup) +{ + *gpio_reg(GPIOE_BASE, 0) = backup->moder; + *gpio_reg(GPIOE_BASE, 4) = backup->otyper; + *gpio_reg(GPIOE_BASE, 8) = backup->ospeedr; + *gpio_reg(GPIOE_BASE, 12) = backup->pupdr; + *gpio_reg(GPIOE_BASE, 32) = backup->afrl; + RCC_AHB1ENR = backup->ahb1enr; +} + /** @brief Route the Discovery board's SAI2 signals through AF10. */ static void setup_sai2_pins(pin_backup_t *backup) { @@ -89,98 +134,114 @@ static int failures; ++failures; \ } } while (0) -/** @brief Configure, transfer on and release SAI2 at one sample rate. */ -static void exercise(uint32_t rate) +/** @brief Configure, transfer on and release one SAI at one sample rate. */ +static void exercise(unsigned instance, uint32_t rate) { dmsai_config_t config = { - .instance = 1, .clock_role = dmsai_clock_master, + .instance = instance, .clock_role = dmsai_clock_master, .framing = dmsai_frame_tdm, .pcm_format = dmsai_pcm_s16_le, .sample_rate_hz = rate, .tolerance_ppm = 500, .slot_bits = 16, .slot_count = 4, .frame_bits = 64, .active_slots = 5, .transmit = true, .receive = true, }; + unsigned tx_stream = instance ? 4U : 1U; + unsigned rx_stream = instance ? 6U : 5U; + dmclk_domain_t domain = instance ? dmclk_domain_sai2 : dmclk_domain_sai1; + uint32_t gate = 1U << (22U + instance); + volatile uint32_t *acr1 = sai_reg(instance, false, 4); + volatile uint32_t *bcr1 = sai_reg(instance, true, 4); uint32_t before_cr = RCC_CR; - dmclk_frequency_t before_sai = - dmclk_port_get_domain_frequency(dmclk_domain_sai2); + dmclk_frequency_t before_sai = dmclk_port_get_domain_frequency(domain); int rc = dmsai_port_init(&config); - Dmod_Printf("DMSAI SAI2 init rate=%u rc=%d\n", (unsigned)rate, rc); + Dmod_Printf("DMSAI SAI%u init rate=%u rc=%d\n", + instance + 1U, (unsigned)rate, rc); CHECK(rc == 0); if (rc != 0) return; CHECK(dmsai_port_init(&config) == -EBUSY); - CHECK((RCC_APB2ENR & SAI2EN) != 0); - CHECK((SAI2_ACR1 & (15U << 20)) == (2U << 20)); - CHECK((SAI2_BCR1 & (3U << 10)) == (1U << 10)); - CHECK(SAI2_AFRCR == 0x00051F3FU); - CHECK(SAI2_ASLOTR == 0x00050340U); + CHECK((RCC_APB2ENR & gate) != 0); + CHECK((*acr1 & (15U << 20)) == (2U << 20)); + CHECK((*bcr1 & (3U << 10)) == (1U << 10)); + CHECK(*sai_reg(instance, false, 0x0CU) == 0x00051F3FU); + CHECK(*sai_reg(instance, false, 0x10U) == 0x00050340U); dmsai_status_t status; - CHECK(dmsai_port_get_status(1, &status) == 0); + CHECK(dmsai_port_get_status(instance, &status) == 0); uint32_t error = status.actual_sample_rate_hz > rate ? status.actual_sample_rate_hz - rate : rate - status.actual_sample_rate_hz; CHECK(error <= rate / 2000U); - CHECK(dmsai_port_start(1) == 0); - CHECK((SAI2_ACR1 & (1U << 16)) != 0); - CHECK((SAI2_BCR1 & (1U << 16)) != 0); - CHECK((SAI2_ACR1 & (1U << 17)) != 0); - CHECK((SAI2_BCR1 & (1U << 17)) != 0); - CHECK((DMA2_S4CR & 0x101U) == 0x101U); - CHECK((DMA2_S6CR & 0x101U) == 0x101U); - uint32_t tx_before = DMA2_S4NDTR; - uint32_t rx_before = DMA2_S6NDTR; + CHECK(dmsai_port_start(instance) == 0); + CHECK((*acr1 & (1U << 16)) != 0); + CHECK((*bcr1 & (1U << 16)) != 0); + CHECK((*acr1 & (1U << 17)) != 0); + CHECK((*bcr1 & (1U << 17)) != 0); + CHECK((*dma_reg(tx_stream, 0) & 0x101U) == 0x101U); + CHECK((*dma_reg(rx_stream, 0) & 0x101U) == 0x101U); + CHECK(((*dma_reg(tx_stream, 0) >> 25) & 7U) == (instance ? 3U : 0U)); + CHECK(((*dma_reg(rx_stream, 0) >> 25) & 7U) == (instance ? 3U : 0U)); + uint32_t tx_before = *dma_reg(tx_stream, 4); + uint32_t rx_before = *dma_reg(rx_stream, 4); bool tx_progress = false; bool rx_progress = false; for (unsigned i = 0; i < 10; ++i) { Dmod_ThreadSleep(1); - if (DMA2_S4NDTR != tx_before) tx_progress = true; - if (DMA2_S6NDTR != rx_before) rx_progress = true; + if (*dma_reg(tx_stream, 4) != tx_before) tx_progress = true; + if (*dma_reg(rx_stream, 4) != rx_before) rx_progress = true; } - Dmod_Printf("DMA S4 %08x/%u->%u S6 %08x/%u->%u SAI SR %08x/%08x\n", - (unsigned)DMA2_S4CR, (unsigned)tx_before, (unsigned)DMA2_S4NDTR, - (unsigned)DMA2_S6CR, (unsigned)rx_before, (unsigned)DMA2_S6NDTR, - (unsigned)SAI2_ASR, (unsigned)SAI2_BSR); + Dmod_Printf("DMA S%u %08x/%u->%u S%u %08x/%u->%u SAI SR %08x/%08x\n", + tx_stream, (unsigned)*dma_reg(tx_stream, 0), (unsigned)tx_before, + (unsigned)*dma_reg(tx_stream, 4), rx_stream, + (unsigned)*dma_reg(rx_stream, 0), (unsigned)rx_before, + (unsigned)*dma_reg(rx_stream, 4), + (unsigned)*sai_reg(instance, false, 0x18U), + (unsigned)*sai_reg(instance, true, 0x18U)); CHECK(tx_progress); CHECK(rx_progress); const int16_t pcm[4] = {0, 0, 0, 0}; size_t written = 0; - rc = dmsai_port_write(1, pcm, sizeof(pcm), &written, 50); + rc = dmsai_port_write(instance, pcm, sizeof(pcm), &written, 50); CHECK(rc == 0 && written == sizeof(pcm)); int16_t *burst = Dmod_Malloc(300U * 2U * sizeof(int16_t)); CHECK(burst != NULL); if (burst) { for (unsigned i = 0; i < 600U; ++i) burst[i] = (int16_t)i; written = 0; - rc = dmsai_port_write(1, burst, 300U * 2U * sizeof(int16_t), + rc = dmsai_port_write(instance, burst, 300U * 2U * sizeof(int16_t), &written, 100); CHECK(rc == 0 && written == 300U * 2U * sizeof(int16_t)); Dmod_Free(burst); } int16_t captured[2] = {0}; size_t received = 0; - rc = dmsai_port_read(1, captured, sizeof(captured), &received, 50); + rc = dmsai_port_read(instance, captured, sizeof(captured), &received, 50); CHECK(rc == 0 && received == sizeof(captured)); - CHECK(dmsai_port_get_status(1, &status) == 0); + CHECK(dmsai_port_get_status(instance, &status) == 0); CHECK(status.running); CHECK(status.transfer_errors == 0); Dmod_Printf("SAI status tx_underrun=%u rx_overrun=%u transfer_error=%u\n", (unsigned)status.tx_underruns, (unsigned)status.rx_overruns, (unsigned)status.transfer_errors); - CHECK(dmsai_port_stop(1) == 0); - CHECK(dmsai_port_deinit(1) == 0); - CHECK((RCC_APB2ENR & SAI2EN) == 0); + CHECK(dmsai_port_stop(instance) == 0); + CHECK(dmsai_port_deinit(instance) == 0); + CHECK((RCC_APB2ENR & gate) == 0); CHECK(RCC_CR == before_cr); - CHECK(dmclk_port_get_domain_frequency(dmclk_domain_sai2) == before_sai); + CHECK(dmclk_port_get_domain_frequency(domain) == before_sai); } /** @brief Run the STM32F746G-DISCO port checks as a DMOD executable. */ int main(int argc, char **argv) { (void)argc; (void)argv; - pin_backup_t pins; - setup_sai2_pins(&pins); + sai1_pin_backup_t sai1_pins; + setup_sai1_pins(&sai1_pins); CHECK(dmsai_port_get_instance_count() == 2); - CHECK((RCC_APB2ENR & SAI2EN) == 0); - exercise(48000); - exercise(44100); - restore_sai2_pins(&pins); + CHECK((RCC_APB2ENR & ((1U << 22) | (1U << 23))) == 0); + exercise(0, 48000); + exercise(0, 44100); + restore_sai1_pins(&sai1_pins); + pin_backup_t sai2_pins; + setup_sai2_pins(&sai2_pins); + exercise(1, 48000); + exercise(1, 44100); + restore_sai2_pins(&sai2_pins); Dmod_Printf("DMSAI PORT BOARD TEST: %s (%d failures)\n", failures ? "FAIL" : "PASS", failures); return failures ? 1 : 0;