Guide DMA - amir1387aht/MicroCS GitHub Wiki
DMA: send and receive in the background
Writing a full OLED frame or reading a block of flash keeps the CPU busy for milliseconds. With
DMA the hardware moves the bytes while your C# keeps running. This guide goes from the first
DmaBuffer to a display loop that draws the next frame while the last one is sent. The
reference is DMA and asynchronous transfers.
Works on every board running MicroCS 1.12+: ESP32 and RP2 really run SPI (and memory copies) in
the background; elsewhere the same calls finish immediately, so the script does not change.
The PC simulator (mcs --sim --sim-virtual) runs every example below.
In MicroCS Studio open Templates (Alt+T) ā DMA for seven ready-to-run
examples (DmaBuffer, SPI in the background, double-buffered display, I2C / UART / QSPI reads,
memory copy), or type the snippets dmabuf and spiasync.
1. Check what runs in the background
Console.WriteLine($"DMA {Hal.Has("DMA")} SPI {Hal.Has("DMA.SPI")} memory {Hal.Has("DMA.Memory")}");
Console.WriteLine($"channels {Dma.Channels}, alignment {Dma.Alignment}, in flight {Dma.MaxTransfers}");
2. A DMA buffer
var frame = new DmaBuffer(1024); // 128 x 64 pixels, 1 bit each
frame.Fill(0x00);
frame[0] = 0xFF;
frame.CopyFrom(new byte[] { 1, 2, 3 }, 10);
Console.WriteLine(frame); // DmaBuffer[1024]
A DmaBuffer lives in memory the DMA engine can reach (on ESP32: internal RAM). Use it like a
byte[].
3. Start a transfer, do something else, finish
SPI.Open(1, new SpiConfig(20_000_000, 0, 6, 7) { Cs = 10 });
var t = SPI.WriteAsync(1, frame); // returns at once
int work = 0;
while (!t.IsCompleted) work++; // the CPU is free meanwhile
Console.WriteLine($"{t.Result} bytes sent, {work} loops done meanwhile");
Instead of polling you can wait (t.Wait(), t.Wait(100) with a timeout) or get a callback:
SPI.WriteAsync(1, frame).OnComplete(n => Console.WriteLine($"frame out ({n} bytes)"));
Callbacks run between statements and while the script sleeps ā never inside an interrupt.
4. Double buffering: draw while sending
var a = new DmaBuffer(1024);
var b = new DmaBuffer(1024);
DmaTransfer sending = null;
for (int f = 0; f < 100; f++) {
var draw = (f % 2 == 0) ? a : b;
draw.Fill(0);
draw[f % 1024] = 0xFF; // ... render the next frame here
if (sending != null) sending.Wait(); // the previous frame is out
sending = SPI.WriteAsync(1, draw);
}
sending.Wait();
While a transfer uses a buffer, writing to it throws InvalidOperationException ā that is why
the loop draws into the other buffer.
5. Reading: sensors, flash, UART
var regs = new DmaBuffer(6);
I2C.WriteReadAsync(0, 0x68, new byte[] { 0x3B }, regs).Wait(); // MPU-6050 accelerometer
var id = new DmaBuffer(3);
QSPI.ReadAsync(0, 0x9F, -1, id).Wait(); // flash JEDEC id
UART.Open(1, 9600);
var gps = new DmaBuffer(64);
var line = UART.ReadAsync(1, gps); // completes when 64 bytes arrived
if (!line.Wait(2000)) line.Cancel();
A failed transfer (an I²C NACK, a timeout) throws when you Wait() for it; Succeeded and
Error tell you without an exception.
6. Memory to memory
var copy = new DmaBuffer(1024);
Dma.Copy(copy, frame); // DMA engine where there is one, CPU copy elsewhere
Dma.Fill(copy, 0x55, 0, 512);
Dma.CopyAsync(copy, frame).Wait();
Troubleshooting
DmaBuffer is in use by a DMA transferāWait()for the transfer before writing to its buffer, or use a second buffer (step 4).too many DMA transfers in flightā more thanDma.MaxTransfers(MCS_HAL_MAX_DMA) were started without waiting;Dma.WaitAll()or raiseMCS_HAL_MAX_DMA(1ā8).OutOfMemoryException: no DMA-capable memoryā smaller buffers; ESP32 DMA memory is the internal RAM.- Nothing is faster ā
Hal.Has("DMA.SPI")isfalseon this board: transfers are synchronous there. - Bus settings (pins,
Dma = false,MaxTransfer): peripheral settings guide.