dmsai is a PCM SAI device driver for DMOD. It implements the dmdrvi
DIF used by dmdevfs, following the same device model as dmeth: an INI
section selects the driver and each configured controller appears as
/dev/dmsaiN. Applications use ordinary file read/write operations and
the standard dmdrvi audio stream ioctl commands. There is no separate stream Module API.
dmdevfs owns device node creation and mounting. dmsai parses the
selected INI section, reserves the controller through dmsai_port, and
provides exclusive file handles. The STM32F7 port transfers PCM with
circular DMA.
The dmdrvi DIF supplies create/free, open/close, read/write, ioctl,
flush and stat. The core parses the active INI section selected by
dmdevfs, allocates the device context and exposes a major number equal to the
zero-based instance (/dev/dmsai0, /dev/dmsai1, ...). Unknown ioctls return -ENOTTY.
read and write transfer bytes of interleaved PCM in complete frames.
Enabled slots are packed in ascending slot order. The sample representation
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. 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.
The node implements the standard audio stream commands from dmdrvi_ioctl.h
(dmdrvi 2.7, DMDRVI_IOCTL_AUDIO_*) - the same contract a codec node such as
dmwm8994 implements - so a client plays the same way on both and needs no
dmsai header:
| Command | Argument | Purpose |
|---|---|---|
DMDRVI_IOCTL_AUDIO_GET_STREAM |
dmdrvi_audio_stream_t * |
Read buffer layout (rate, format, channels = active slots) and link |
DMDRVI_IOCTL_AUDIO_CONFIGURE |
const dmdrvi_audio_config_t * |
Succeeds only when rate, channels and width match the ini format |
DMDRVI_IOCTL_AUDIO_START |
NULL |
Start configured directions; succeeds when already running |
DMDRVI_IOCTL_AUDIO_STOP |
NULL |
Stop transfer and wake pending I/O |
DMDRVI_IOCTL_AUDIO_GET_STATUS |
dmdrvi_audio_status_t * |
Read rate and error counters |
DMDRVI_IOCTL_AUDIO_SET_TIMEOUT |
const uint32_t * |
Set this handle's wait limit in ms; zero means no limit |
DMDRVI_IOCTL_AUDIO_GET_TIMEOUT |
uint32_t * |
Read this handle's wait limit |
DMDRVI_IOCTL_AUDIO_DRAIN |
NULL |
Wait until queued TX data reaches the output |
The SAI has no analog stage: AUDIO_GET_INFO, AUDIO_SET_VOLUME and
AUDIO_SET_MUTE return -ENOTSUP. That also keeps dmdevfs from reporting
the node as an audio device - the codec in front of it is the one.
dmdevfs discovers the section from driver_name=dmsai and restricts the
dmini context to it before calling the driver. Board pin routing and codec
initialization are configured separately.
[audio0]
driver_name=dmsai
instance=0
clock_role=master
framing=i2s
pcm_format=s16_le
sample_rate_hz=48000
tolerance_ppm=500
slot_bits=16
slot_count=2
frame_bits=32
active_slots=3
transmit=on
receive=offThe port decides which rate, role, framing and directions its target supports. Invalid values or an already reserved controller prevent node creation.
Board GPIO mappings and pin-agnostic MCU defaults are in configs/README.md. Select one device section per SAI controller; the board examples are based on ST's audio BSP pin definitions.
This sequence sends PCM and waits for it to leave the DMA output before STOP.
#include "dmod.h"
#include "dmsai.h"
#include "dmdrvi_ioctl.h"
int play_stereo(const int16_t *pcm, size_t frames)
{
void *audio = Dmod_FileOpen("/dev/dmsai0", "w");
if (audio == NULL)
return -1;
int result = Dmod_Ioctl(audio, DMDRVI_IOCTL_AUDIO_START, NULL);
if (result == 0)
{
size_t bytes = frames * 2u * sizeof(int16_t);
if (Dmod_FileWrite(pcm, 1, bytes, audio) != bytes)
result = -1;
if (result == 0)
result = Dmod_Ioctl(audio, DMDRVI_IOCTL_AUDIO_DRAIN, NULL);
Dmod_Ioctl(audio, DMDRVI_IOCTL_AUDIO_STOP, NULL);
}
Dmod_FileClose(audio);
return result;
}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:
#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);
dmsai_port_flush(1, 100);
dmsai_port_stop(1);
}
dmsai_port_deinit(1);
}dmsai handles 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 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 for
compatibility with older dmdma releases; its STM32 port maps the low three
request bits to the hardware channel. 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; DMDRVI_IOCTL_AUDIO_DRAIN waits until the
queued audio has reached the output before STOP. 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.
cmake -S . -B build -DDMOD_CPU_FAMILY=stm32f7 -DDMOD_DIR=/path/to/dmod
cmake --build buildThe existing test_dmsai target is retained. dmsai_driver_board_test
opens /dev/dmsai1 through dmdevfs and checks configuration, exclusive open,
start/stop, 300-frame DMA write, drain, RX read and status. Run it after
mounting configs/board/stm32f746g-disco/sai2.ini. Running Cortex-M7 test
modules requires a compatible loader or target board; the Raspberry Pi
AArch64 loader cannot execute them. See API reference
for the detailed contracts.