DMA - amir1387aht/MicroCS GitHub Wiki
DMA and asynchronous transfers (HAL API v3)
DmaBuffer is a block of DMA-capable memory. The …Async calls start a
transfer and return a DmaTransfer at once; the script keeps running while the
DMA engine moves the bytes, then waits for the result or gets a callback.
var frame = new DmaBuffer(128 * 64 / 8); // an OLED frame buffer
DrawInto(frame);
var t = SPI.WriteAsync(1, frame, 5); // CS on GPIO5, returns immediately
ReadSensors(); // runs while the frame goes out
t.Wait(); // or t.OnComplete(n => ...)
The same script runs on every board: where the board has no DMA for an
operation, the call runs synchronously and the returned transfer is already
complete. Hal.Has("DMA.SPI") (DMA, DMA.Memory, DMA.I2C, DMA.UART,
DMA.QSPI) tells whether it really runs in the background.
A walk-through is in the DMA guide; bus settings
(SpiConfig.Dma, MaxTransfer …) are in peripheral configuration.
Contents: DmaBuffer · Starting transfers · DmaTransfer · Dma · Rules · Boards · Ports: implementing DMA
DmaBuffer
| Member | Description |
|---|---|
new DmaBuffer(size) · new DmaBuffer(byte[] / string / DmaBuffer) |
zero-filled, or a copy; up to 16 MiB |
buf[i] (get / set) · Length |
bytes |
Fill(value[, offset[, count]]) |
|
CopyFrom(byte[] / string / DmaBuffer[, offset]) → bytes copied |
|
ToArray([offset[, count]]) → byte[] · GetString([offset[, count]]) |
UTF-8 |
Busy |
a transfer is using the buffer |
The memory comes from the board's DMA allocator (ESP32: internal DMA-capable
RAM; elsewhere the VM heap, aligned to Dma.Alignment) and is freed with the
object. While a transfer uses a buffer, writing to it throws
InvalidOperationException; reading is allowed.
Starting transfers
Data arguments take a DmaBuffer, or a byte[] / string that MicroCS copies
into a temporary buffer (receive buffers must be DmaBuffers). The synchronous
calls (SPI.Write(bus, buf), SPI.Read(bus, buf), QSPI.Read(…, buf)) also
take DmaBuffers, with no size limit.
| Call | Completes when |
|---|---|
SPI.TransferAsync(bus, tx, rx[, cs]) · SPI.WriteAsync(bus, data[, cs]) · SPI.ReadAsync(bus, rx[, cs]) |
all bytes clocked; cs is driven around the transfer |
I2C.WriteAsync(bus, addr, data) · I2C.ReadAsync(bus, addr, rx) · I2C.WriteReadAsync(bus, addr, tx, rx) |
stop condition (a NACK fails the transfer) |
UART.WriteAsync(port, data) · UART.ReadAsync(port, rx) |
sent / rx is full |
QSPI.ReadAsync(bus, instr, addr, rx[, dummy[, lines[, addrBytes]]]) · QSPI.WriteAsync(bus, instr, addr, data[, lines[, addrBytes]]) |
command done |
Dma.CopyAsync(dst, src[, count[, dstOffset[, srcOffset]]]) · Dma.FillAsync(dst, value[, offset[, count]]) |
memory to memory |
I2cDevice.WriteAsync / ReadAsync / WriteReadAsync, SpiDevice.TransferAsync / WriteAsync / ReadAsync |
same, on the device's bus / address / CS |
DmaTransfer
| Member | Description |
|---|---|
Wait([timeoutMs]) → bool |
true when finished, false on timeout; throws IOException / TimeoutException if the transfer failed |
IsCompleted · Succeeded · Error (text or null) |
non-blocking state |
Result |
bytes transferred (waits for the end) |
OnComplete(fn) · OnComplete(n => …) |
runs on the VM thread between statements, in Thread.Sleep, Hal.Run … (at once when already finished) |
Cancel() → bool |
stop it (best effort); the transfer completes with an error |
Id |
the HAL transfer id |
Dma
| Member | Description |
|---|---|
Dma.Copy(dst, src[, count[, dstOffset[, srcOffset]]]) → bytes · Dma.Fill(dst, value[, offset[, count]]) |
synchronous memory DMA (CPU copy where there is none) |
Dma.CopyAsync(...) · Dma.FillAsync(...) |
see above |
Dma.WaitAll([timeoutMs]) → bool · Dma.Pending |
transfers in flight |
Dma.Available · Dma.Channels · Dma.Alignment · Dma.MaxTransfers |
board facts; MaxTransfers = MCS_HAL_MAX_DMA |
Rules
- Order per bus. Transfers on one bus run in order: a new transfer, async or not, waits for the previous one on that bus. Different buses run in parallel.
- In flight. Up to
MCS_HAL_MAX_DMAtransfers (default 4, 1–8, inmcs_user_config.h); more wait for a free slot. - Lifetime. The buffers and the
DmaTransferstay alive until the transfer ends, even if the script drops its references. CallDma.WaitAll()before a script ends if the last transfers matter. - Errors are reported when the transfer ends (
Waitthrows,Succeededis false,Errorholds the reason), not when it starts — except arguments that are wrong from the start (ArgumentException). - Callbacks never run inside an interrupt: completion is an
MCS_HAL_EV_DMAevent dispatched like GPIO / timer events. SpiConfig.Dma = falsekeeps a bus on CPU transfers;trueasks for DMA where the board decides per transfer.MCS_ENABLE_HAL_DMA 0(inmcs_user_config.h) leavesDmaBuffer,Dma,DmaTransferand the…Asynccalls out of the firmware (~11 KB of flash on Cortex-M0);Hal.Has("DMA")is thenfalse.
Boards
| Board | Background DMA | Notes |
|---|---|---|
| ESP32 family | SPI, QSPI (all chips), memory copy (GDMA chips: C2/C3/C6/H2/S3/P4) | DmaBuffer in internal DMA-capable RAM; SpiConfig.MaxTransfer sizes the driver |
| RP2040 / RP2350 | SPI, memory copy / fill | two channels per SPI transfer, completion on DMA_IRQ_1 |
Simulator (--sim) |
all | completions go through the same event path as on hardware, so scripts can be tested on a PC |
| STM32, Zephyr, Arduino, Cortex-M | — (synchronous) | the async API works and completes at once |
Ports: implementing DMA
All members are optional (mcs_hal.h, API v3):
hal->dma_channels = 8; /* > 0: Dma.Available, Hal.Has("DMA") */
hal->dma_align = 4;
hal->dma_alloc = my_alloc; /* DMA-capable memory for DmaBuffer, NULL = VM heap */
hal->dma_free = my_free;
hal->dma_copy = my_copy; /* (ctx, dst, src, n, flags MCS_DMA_SRC_FIXED/DST_FIXED, id) */
hal->dma_abort = my_abort; /* (ctx, id): no completion event follows */
hal->spi_transfer_async = my_spi_async; /* (ctx, bus, tx, rx, n, id) */
- Start the transfer and return 0; when it ends, post
mcs_hal_post(MCS_HAL_EV_DMA, id, bytes_or_negative_error)— usually from the DMA-complete interrupt. The buffers stay valid until then. - Return
MCS_HAL_ENOTSUPfor a request the engine cannot do (a fill, an odd size, no free channel): the library runs the synchronous call instead and completes the transfer at once.MCS_HAL_EBUSYand other errors fail it. spi_transfer_asyncmay gettxorrxNULL (send0xFF/ discard).- On cores with a data cache, clean
txbefore and invalidaterxafter the transfer, or return memory fromdma_allocthat is not cached.
See ports/esp32/mcs_port_esp32.c and ports/rp2/mcs_port_rp2.c for complete
implementations and ports/template/mcs_port_template.c for a skeleton.