DMA Controllers: Reducing CPU Load in High-Throughput Embedded Systems

A practical examination of how DMA controllers decouple data movement from instruction execution, and why their architecture is now a first-order design concern rather than a convenience. The article covers DMA fundamentals, then surveys modern controller features — linked-list descriptors, 2D addressing, request multiplexing, and security domains — in the context of gigabit-class peripherals.

Introduction

In any system where a peripheral produces or consumes data faster than the CPU can comfortably service per-word interrupts, the limiting factor stops being clock frequency and becomes data-movement overhead. A Cortex-M running an interrupt service routine (ISR) per received byte on a 10 Mbit UART link is already spending a non-trivial fraction of its cycles on context save/restore. Scale that to a MIPI CSI-2 camera, a gigabit Ethernet MAC, or an OctoSPI flash streaming at several hundred MB/s, and a per-transfer CPU model collapses entirely.

The Direct Memory Access (DMA) controller exists precisely to break this coupling: it moves blocks of data between memory and peripherals — or memory and memory — autonomously, signaling the CPU only at meaningful boundaries (block complete, half-transfer, error). What has changed in recent silicon is not the basic idea but the sophistication of the engines themselves. Modern DMA controllers are programmable, descriptor-driven, security-aware data-movement processors, and treating them as "fire-and-forget copy blocks" leaves significant performance and integration value unused.

DMA Fundamentals

At minimum, a DMA transfer is defined by a source address, a destination address, a transfer size, and an address-progression rule for each endpoint (increment, fixed, or modulo/wrap). The controller arbitrates for the system bus, performs the transfer, and raises an interrupt on completion.

The two classical arbitration strategies remain relevant:

  • Cycle stealing — the DMA takes the bus for individual beats, interleaving with CPU accesses. Lower peak throughput, lower CPU stall.
  • Burst mode — the DMA holds the bus for a full burst (e.g., an AXI INCR burst of 16 beats). Higher throughput, but the CPU may stall waiting for the bus.

Real controllers expose a configurable burst length and an internal FIFO so that a slow peripheral side and a fast memory side can be rate-matched without bus thrash.

Transfers are triggered either by software or — far more commonly in embedded work — by a hardware request line asserted by a peripheral (UART RX-not-empty, ADC end-of-conversion, timer update, SPI TX-empty). This hardware-paced model is what makes truly zero-CPU streaming possible.

Why CPU offload is not "free"

Two costs survive even a perfect DMA setup, and ignoring them produces designs that benchmark worse than expected:

  • Bus contention. DMA and CPU share the interconnect and memory. On a single-bank SRAM, a saturating DMA channel will stall the CPU on every cache miss. Multi-bank SRAM and a multi-layer AXI/AHB matrix exist specifically to let DMA and CPU hit different memories concurrently.
  • Cache coherency. On a Cortex-M7 or a Cortex-A core with L1 data cache, a DMA write to a buffer is invisible to the CPU until the line is invalidated; a CPU write may sit in cache and never reach the buffer the DMA reads. This must be handled by cache maintenance or by placing buffers in a non-cacheable MPU region.
/* Receive path on a cached core (e.g. Cortex-M7).
 * Buffer is in cacheable memory, so the cache must be told
 * the DMA wrote underneath it before the CPU reads. */
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, RX_LEN);  // discard stale lines
process(rx_buf, RX_LEN);                                   // now CPU sees DMA data

/* Transmit path: flush CPU-written data to memory
 * before starting the DMA that reads it. */
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, TX_LEN);        // push dirty lines out
dma_start(tx_buf, TX_LEN);

Modern DMA Controller Architectures

Three controller families illustrate the spread of current designs.

Controller Programming model Notable features
ARM PL330 (AMBA AXI) Executes a small DMA instruction set (microcode) DMALD/DMAST/DMALP loops; multiple independent threads; common in Cortex-A SoCs
NXP eDMA (e.g. i.MX RT) Transfer Control Descriptor (TCD) per channel Nested minor/major loops; channel linking; scatter-gather via in-memory TCDs
STMicro GPDMA (STM32U5/H5/H7) Linked-list of descriptors in memory 2D addressing, request multiplexing, per-channel security/privilege attributes

The architectural trend is clear: the controller is increasingly a descriptor-driven state machine that reads its own work items from memory, rather than a set of registers the CPU rewrites for every transfer.

Linked-list / scatter-gather descriptors

The single most important modern feature is the ability to chain transfers. Instead of one contiguous copy, the controller follows a linked list of descriptors, each describing its own source, destination, length, and a pointer to the next descriptor. This enables:

  • Gather of fragmented buffers (typical for network packets assembled from headers + payload pools) into a single stream.
  • Scatter of an incoming stream into multiple discontiguous regions.
  • Continuous operation with the CPU touching only the descriptor list, not the data path.
/* GPDMA-style linked-list node. The engine fetches one node,
 * executes the block transfer, then follows next_node.
 * The CPU builds the list once; the data path runs unattended. */
typedef struct dma_lli {
    uint32_t src;            // peripheral or memory source address
    uint32_t dst;            // destination address
    uint32_t nbytes;         // block size for this node
    struct dma_lli *next;    // NULL terminates the list (or wraps for ring)
} dma_lli_t;

dma_lli_t rx_chain[3] = {
    { ADC_DR, (uint32_t)buf_a, 1024, &rx_chain[1] },
    { ADC_DR, (uint32_t)buf_b, 1024, &rx_chain[2] },
    { ADC_DR, (uint32_t)buf_c, 1024, &rx_chain[0] }, // wrap -> triple-buffer ring
};

The wrapped list above gives a triple-buffer ring with zero CPU involvement per buffer: the controller advances through buf_a -> buf_b -> buf_c -> buf_a, raising an interrupt at each node boundary so application code can consume the just-filled buffer while the next fills.

2D / strided addressing

Newer controllers (STM32 GPDMA, eDMA via minor/major loop offsets) support a second address dimension: after transferring a block, the source and/or destination address jumps by a programmable offset. This is exactly what is needed to:

  • Extract a rectangular region of interest from a larger frame buffer (line stride != region width).
  • De-interleave multi-channel ADC samples into separate per-channel arrays.
  • Repack pixel formats during transfer.

What previously required a CPU loop with pointer arithmetic now executes entirely in the DMA engine.

Request multiplexing (DMAMUX)

Older parts hard-wired each peripheral to a fixed DMA channel, producing painful resource conflicts. The request multiplexer (DMAMUX, now standard across STM32 and many other vendors) inserts a crossbar between peripheral request lines and DMA channels, so any request can drive any channel. It also adds:

  • Request generators — convert an external trigger or timer event into a DMA request.
  • Synchronization — gate requests on an external signal, useful for sample-accurate acquisition.

Security and isolation

With TrustZone reaching microcontroller-class parts, DMA became an attack surface: a peripheral-driven engine with bus-master access can read or write anywhere unless constrained. Modern controllers attach security and privilege attributes per channel, so a non-secure context cannot program a channel to touch secure memory. In safety- and security-critical designs this is no longer optional.

Matching modern peripheral throughput

The features above exist because peripheral bandwidth has outrun per-interrupt servicing:

  • Gigabit Ethernet / USB 3.x / SDMMC ship with their own dedicated, descriptor-based DMA in the MAC/controller — the general-purpose DMA is bypassed entirely for the data path.
  • OctoSPI / HyperBus flash at hundreds of MB/s rely on burst-mode DMA with deep FIFOs to keep the external bus saturated.
  • High-speed ADCs and audio (I2S/SAI) depend on circular/linked-list DMA for glitch-free continuous capture.

At these rates the bottleneck moves off the CPU and onto memory bandwidth and interconnect arbitration — which is why the modern question is less "does it have DMA?" and more "how many independent bus masters and memory banks can run concurrently?"

Conclusion

DMA has shifted from a peripheral convenience to a central architectural lever for throughput-bound embedded systems. The practical takeaways:

  • Use hardware-triggered DMA for any peripheral that streams faster than a comfortable interrupt rate; reserve the CPU for boundary events only.
  • Reach for linked-list / scatter-gather descriptors and ring buffers to eliminate per-buffer CPU intervention in continuous acquisition.
  • Exploit 2D addressing to push pointer arithmetic (strides, ROI extraction, de-interleaving) into the engine.
  • Always budget for cache maintenance and bus contention — these are the costs that erode the theoretical CPU savings, and on multi-bank/multi-layer designs they are addressable.

DMA is not the right tool everywhere. For small, infrequent transfers, descriptor setup and cache maintenance can cost more cycles than a simple memcpy. For tight, deterministic control loops where worst-case latency matters more than throughput, the added bus contention and completion-interrupt latency may be undesirable. The discipline is to apply DMA where data volume is high and per-transfer CPU attention is the bottleneck — and to leave it out where transfer sizes are small or determinism dominates.

Return to Post List