Step 18 of 56
I2S: audio that only moves by DMA
The S3's I2S has no CPU FIFO at all — samples reach the wire only through the DMA crossbar. That single fact shapes the whole API, including gapless playback and capture that runs underneath it.
No FIFO, so no polled path
Two controllers, I2S0 and I2S1. Unlike the
UART, neither has a CPU-accessible FIFO: data flows
only through GDMA. So bringing a port up
claims a GDMA channel, and that is a heavyweight, once-per-port resource —
which is why acquisition works slightly differently here:
The first Acquire of a port opens
it at the given configuration and claims its channel. Later Acquire
calls reuse it as-is — they do not re-open the port and do not
inherit a new configuration. To change the audio format on a port you already
hold, call Reconfigure (which re-claims the channel). This is the
opposite of UART, where every Acquire
re-applies the full state.
procedure Acquire
(S : in out Session;
Port : I2S_Port;
Sample_Rate : Positive := 16_000;
Bits : Sample_Bits := Bits_16; -- Bits_8 | 16 | 24 | 32
Mode : I2S_Mode := Standard; -- Standard | PDM
Bclk, Ws, Dout, Din, Mclk : ESP32S3.GPIO.Optional_Pin := No_Pin);
Every pin is optional, so a link routes only what it uses — omit
Din for a TX-only DAC, omit Dout for an RX-only
microphone. Mclk drives a codec's master-clock input and exists
only on I2S0; leave it unrouted for codecs that clock from BCLK.
Typed sample buffers
type PCM_8 is array (Natural range <>) of Interfaces.Integer_8;
type PCM_16 is array (Natural range <>) of Interfaces.Integer_16;
type PCM_32 is array (Natural range <>) of Interfaces.Integer_32;
The element type fixes the on-wire width, so the driver derives the byte count
itself — no caller-side * 2 — and the typed
Write/Read/Transfer check the
buffer's width against the port's configured Bits. They are
signed two's-complement, as PCM is. A PCM_32 buffer carries both 24-
and 32-bit samples, since both occupy a 32-bit slot. For already-framed bytes or
an opaque bit pattern there are *_Raw primitives, including
DMA_Buffer overloads with the usual alignment and size
preconditions.
Standard and PDM are the same buffers
I2S_Mode selects what sits between the buffer and the wire:
Standard— ordinary I2S/TDM. The PCM buffer appears verbatim on the data line, BCLK and WS framed. BCLK isSample_Rate * Bits * 2.PDM— the hardware sigma-delta converters are inserted. On TX, PCM2PDM turns each sample into a 1-bit pulse-density stream for a class-D amp or an RC low-pass; on RX, PDM2PCM decimates a PDM microphone back to PCM. The serial clock runs atSample_Rate * 128, the oversample ratio.
The DMA still moves ordinary PCM either way, so your transfer calls are unchanged — only the on-wire format differs.
The PDM converters high-pass filter, removing DC. A constant level does not survive a PDM round trip, so do not write a self-test that expects one to.
Gapless playback
Three escalating options, all built on the GDMA behaviour from the previous step:
| Call | What it gives you |
|---|---|
Write / Read / Transfer |
One blocking buffer, up to 4095 bytes.
Transfer is full duplex — shift out and capture
simultaneously, same length. |
Start_Continuous |
A self-looping descriptor replays one buffer forever with
no gap and no CPU involvement. The buffer must stay
valid, live in internal SRAM, and should hold a whole number of wave
periods so the wrap is seamless. Stop ends it. |
Start_Stream + Await_Half |
Gapless double-buffered streaming: the two halves of one buffer loop
forever, and Await_Half tells you which half the
hardware has finished so you can refill the other. This is how you
play audio longer than a buffer. |
Capturing while playing
Read drives the receive path as a transaction. When a continuous
transmit is already running, use Capture instead — it fills a
buffer without disturbing the TX path, so recording can run underneath
playback. There is a streaming mirror of it too, the receive counterpart of
Start_Stream.
Self-test without wiring
procedure Enable_Loopback (S : Session; Pad : ESP32S3.GPIO.Pin_Id);
TX and RX share WS and BCK internally through the hardware
SIG_LOOPBACK bit, with the data line looped through one pad, so
./x run esp32s3_i2s_loopback proves the real DMA path in both
directions byte-exact with nothing attached. Configured_Bits reports
the width the held port is currently set to, which is what the typed transfers
check against.