diff --git a/README.md b/README.md index 70301bb..84b37a7 100644 --- a/README.md +++ b/README.md @@ -9,8 +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. -This revision contains headers and buildable stubs only. `dmdrvi_create()` -returns `NULL`, so no device node appears yet and no hardware is touched. +`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` configures SAI hardware and runs circular DMA for +direct port clients. It can be exercised by another DMOD module. ## Device contract @@ -26,7 +29,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 +69,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 @@ -106,16 +109,78 @@ 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` 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 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 +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. + +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 +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 +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 +settings. The test does not initialize the WM8994 codec or verify audio quality. + ## Build and tests ```sh 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 4b8f346..7ec277c 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 @@ -49,11 +50,21 @@ 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 uses `dmdma` circular transfers with +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. +The architecture-independent `dmsai` DIF callbacks remain stubs. 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/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..dc56ff4 100644 --- a/src/port/CMakeLists.txt +++ b/src/port/CMakeLists.txt @@ -14,3 +14,9 @@ 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 + dmdma@>=0.5 + dmheap +) diff --git a/src/port/stm32_common/stm32_common.c b/src/port/stm32_common/stm32_common.c index 71eb537..9ad6912 100644 --- a/src/port/stm32_common/stm32_common.c +++ b/src/port/stm32_common/stm32_common.c @@ -1,45 +1,308 @@ #define DMOD_ENABLE_REGISTRATION ON #include "dmsai_port.h" +#include "stm32_sai.h" +#include "stm32_sai_dma.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 SAI_DMAEN (1U << 17) +#define FIFO_FLUSH (1U << 3) +#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; + stm32_sai_dma_t *dma; +} 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_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 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) { + uint32_t *counter = block ? &state->status.rx_overruns : + &state->status.tx_underruns; + __atomic_fetch_add(counter, 1U, __ATOMIC_RELAXED); + } + if (flags & ~1U) + __atomic_fetch_add(&state->status.transfer_errors, 1U, + __ATOMIC_RELAXED); + if (flags) *block_reg(instance, block != 0, SAI_CLRFR) = flags; + } +} + +/** @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; + 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])) + 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->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); + 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; } -/** @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; + __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; + 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); + __atomic_fetch_add(&state->status.transfer_errors, 1U, + __ATOMIC_RELAXED); + 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; + 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)); + 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 (__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; + } + 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; } /** @copydoc dmsai_port_read */ @@ -47,12 +310,18 @@ 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 (!__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); + __atomic_store_n(&state->rx_busy, false, __ATOMIC_RELEASE); + return rc; } /** @copydoc dmsai_port_write */ @@ -60,19 +329,28 @@ 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 (!__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); + __atomic_store_n(&state->tx_busy, false, __ATOMIC_RELEASE); + return 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..8e70f33 --- /dev/null +++ b/src/port/stm32_common/stm32_sai.h @@ -0,0 +1,41 @@ +#ifndef DMSAI_STM32_SAI_H +#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 +{ + uintptr_t rcc_base; + 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; + +/** + * @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/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 3f71e51..f3b8b74 100644 --- a/src/port/stm32f7/port.c +++ b/src/port/stm32f7/port.c @@ -1,24 +1,44 @@ #define DMOD_ENABLE_REGISTRATION ON #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 = { + /* 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 }, + }, + .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..dbe7185 --- /dev/null +++ b/tests/port-board-test/port_board_test.c @@ -0,0 +1,248 @@ +#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 RCC_AHB1ENR (*(volatile uint32_t *)0x40023830U) +#define SAI_BASE 0x40015800U +#define DMA2_BASE 0x40026400U +#define GPIOE_BASE 0x40021000U +#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; + +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) +{ + 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)) { \ + Dmod_Printf("DMSAI PORT FAIL line %d: %s\n", __LINE__, #test); \ + ++failures; \ +} } while (0) + +/** @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 = 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(domain); + int rc = dmsai_port_init(&config); + 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 & 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(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(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 (*dma_reg(tx_stream, 4) != tx_before) tx_progress = true; + if (*dma_reg(rx_stream, 4) != rx_before) rx_progress = true; + } + 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(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(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(instance, captured, sizeof(captured), &received, 50); + CHECK(rc == 0 && received == sizeof(captured)); + 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(instance) == 0); + CHECK(dmsai_port_deinit(instance) == 0); + CHECK((RCC_APB2ENR & gate) == 0); + CHECK(RCC_CR == before_cr); + 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; + sai1_pin_backup_t sai1_pins; + setup_sai1_pins(&sai1_pins); + CHECK(dmsai_port_get_instance_count() == 2); + 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; +}