Step 17 of 56
GDMA: the DMA engine everything else borrows
Five channel pairs, assigned at run time rather than wired per peripheral — and a buffer type whose rules exist because PSRAM is reached through a cache.
Channels are a resource, not a fixture
The S3 has one AHB GDMA block with five channel pairs (0 .. 4). Each pair has an independent transmit (OUT) and receive (IN) path, and either can be wired to any peripheral through the GDMA crossbar. A channel is therefore something you claim, not something a peripheral owns:
type Channel_Id is mod 5;
type Peripheral is (Mem2Mem, SPI2, SPI3, UHCI0, I2S0, I2S1, LCD_CAM, AES, SHA, ADC_DAC, RMT);
procedure Claim (C : in out Channel; Peri : Peripheral);
function Is_Valid (C : Channel) return Boolean;
procedure Release (C : in out Channel);
Claim goes through a protected allocator, so two tasks can never
be handed the same channel. The Channel handle is limited
(non-copyable, so it cannot be aliased into another task) and controlled (it
releases on scope exit, including on an exception, so a channel cannot leak or
be reused through a stale copy). If all five are busy, Claim leaves
the handle invalid rather than raising — check Is_Valid.
Once you hold a channel, only you touch its registers and descriptors, so the transfer operations themselves need no further locking. This is the same ownership pattern as SPI and I2C, and for the same reason.
Why a buffer type exists
DMA needs DMA-capable memory, and on this chip that is not a single rule:
- Internal SRAM always qualifies, at any alignment.
- External PSRAM qualifies only when the buffer is cache-line (32-byte) aligned — the GDMA reaches PSRAM through the DCache, so the driver writes back and invalidates around each transfer.
- Flash
.rodatais excluded. Aconstantaggregate still cannot be DMA'd.
DMA_Alignment : constant := 32;
type DMA_Buffer is array (Natural range <>) of Interfaces.Unsigned_8
with Alignment => DMA_Alignment;
function Is_DMA_Capable (A : System.Address) return Boolean;
Alignment is only half of it. The type carries the aligned start; the operations' preconditions additionally demand a whole-cache-line size. That is not implied by alignment and it matters: the PSRAM write-back/invalidate rounds the region up to a whole line, so a buffer ending mid-line would have the maintenance touch its neighbour — discarding an adjacent object's dirty cached write. Size the payload up to a multiple of 32 (128 bytes for 100 useful ones).
Hence the calling convention: pass the whole buffer plus a transfer length, never a slice. Slicing to the length would fail the size precondition, while the whole line-multiple buffer keeps every rounded maintenance op inside itself.
procedure Copy (C : Channel; Dst, Src : DMA_Buffer; Length : Natural)
with Pre => Length <= Src'Length and then Length <= Dst'Length
and then Src'Length mod DMA_Alignment = 0
and then Dst'Length mod DMA_Alignment = 0;
These preconditions are pinned on with
pragma Assertion_Policy (Pre => Check) in the spec, so they hold
even when the build has assertions off — a buffer in flash or unaligned
PSRAM corrupts the transfer silently, which is exactly the failure worth
paying a check for.
The three shapes of transfer
| Call | Behaviour |
|---|---|
Copy |
Blocking memory-to-memory, completed by looping one channel's OUT path into its own IN path. Buffers and the driver's descriptors must be in internal SRAM, because the descriptor link address is a 20-bit field. |
Start |
Arms a single-buffer peripheral transfer in one
Direction and returns immediately. You configure and
start the peripheral separately; the GDMA moves data as the
peripheral raises its DMA request. |
Start_Loop |
A single descriptor whose link points back at itself, so the engine replays the buffer forever with no gap between passes and no CPU involvement after the kick. Never completes on its own. |
Direction is Mem_To_Periph (the OUT path reads RAM
and feeds the peripheral) or Periph_To_Mem. A single descriptor
caps at Max_Transfer = 4095 bytes, the hardware's 12-bit
buffer-size field — which is where SPI's
1 .. 4095 precondition comes from.
Beyond one descriptor: the framebuffer path
For a buffer larger than 4095 bytes there is a chained ring: up to
Max_Chain = 256 descriptors that between them cover the buffer, the
last linking back to the first. This is the display path — stream an LCD
framebuffer to LCD_CAM continuously. The buffer may live in PSRAM
(32-byte aligned) and is written back before the loop starts. After the CPU draws
into a live framebuffer, Flush pushes the changes out so the running
DMA re-reads them.
The descriptor ring is one shared internal-SRAM array, so exactly one chained loop runs at a time.
Completion, and an interrupt you must not take
Completion is interrupt-driven rather than polled: Wait suspends
the calling task and the channel's end-of-transfer interrupt wakes it.
This driver owns Device_L3_1
(CPU_INT 27). An application must not attach its own handler
there. It is a level-3 slot rather than a level-2 one because the LCD RGB bounce
refill runs from this completion ISR and has to preempt the level-2 devices to
make its deadline.
Proving the coherency path
type Self_Test_Result is (Passed_PSRAM, Passed_SRAM, Failed, No_Channel);
function Self_Test (Buf_A, Buf_B : System.Address) return Self_Test_Result;
A memory-to-memory round trip between two buffers of your choosing,
which reports which memory it actually exercised. Hand it PSRAM buffers (from a
task whose stack is in PSRAM) and a Passed_PSRAM result means the
cache write-back/invalidate path really works on your board — not merely
that DMA works in SRAM. Buffers must be 32-byte aligned and at least 64 bytes.