Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 73 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions configs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
23 changes: 17 additions & 6 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
9 changes: 8 additions & 1 deletion docs/port-implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -23,3 +25,8 @@ touching architecture-independent logic.
5. Do not introduce a module-specific variable (e.g. `<MODULE>_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.
27 changes: 18 additions & 9 deletions include/dmsai_port.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 ));

Expand All @@ -21,31 +20,38 @@ 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 ));

/**
* @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 ));

/**
* @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 ));

Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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 ));
Expand Down
2 changes: 1 addition & 1 deletion include/dmsai_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
6 changes: 6 additions & 0 deletions src/port/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Loading
Loading