Embedded Systems · Microcontrollers

Bus Matrix Arbitration and What DMA Contention Costs the Core

How a multi-master bus fabric picks a winner when the core and a DMA engine target the same SRAM, why round-robin is not always fair, and how contention shows up as jitter.

A microcontroller with a DMA engine has at least two bus masters: the core, fetching instructions and loading data, and the DMA controller, moving bytes without the core’s involvement. Both reach the same SRAM through a fabric of switches, and when both want the same slave in the same cycle, one of them waits. The fabric’s arbitration policy determines which, and that policy is the reason a DMA transfer that “costs nothing” can still change the timing of an interrupt handler.

The topology the matrix implements

An early bus was a single shared medium: every master and every slave on one set of wires, one transfer at a time. A bus matrix replaces that with a crossbar of switches, so a master can be connected to a slave without blocking the others, as long as no two masters want the same slave.

On a typical part, the masters are the core’s instruction interface, the core’s data interface, the DMA controller, and possibly an Ethernet or USB master with its own DMA. The slaves are flash, SRAM (sometimes split into several banks), and the peripheral bus. The matrix provides a path from each master to each slave, and the number of simultaneous transfers is limited by the number of independent slaves, not by the number of masters.

Two properties follow. First, contention only occurs at a slave, never in the fabric. A DMA transfer to a peripheral does not slow the core’s fetch from flash, because the two use different slaves and different paths. Second, a slave with a single port serialises all access to it. SRAM on most parts has one port per bank, so the core and the DMA contend for it directly; a part with two SRAM banks, one wired to the core and one wired to the DMA, removes that contention at the cost of making the two regions non-interchangeable.

The distinction matters for placement. A DMA buffer in the same bank as the stack contends with every function call and interrupt entry; the same buffer in a separate bank does not. Parts that split SRAM into banks document which masters can reach which bank, and the linker script has to place the buffer accordingly.

Arbitration policies and their failure modes

When two masters request the same slave in the same cycle, the arbiter grants one and holds the other. Three policies are common.

Fixed priority always grants the higher-priority master. If the DMA has fixed priority over the core’s data interface, a long DMA burst can starve the core entirely for the burst’s duration. This is simple and predictable in the wrong direction: the core’s worst-case latency becomes the length of the longest DMA burst, which for a large transfer can be thousands of cycles.

Round-robin rotates the grant among requesting masters. Each master gets the slave for one transfer, then yields if another is waiting. It bounds the core’s wait to roughly one transfer per competing master, which is usually a few cycles. The cost is that the DMA’s throughput drops when the core is also busy, and a DMA channel with a real-time deadline — an audio stream, a display refresh — cannot rely on a guaranteed share.

Weighted or burst-limited arbitration grants a master several consecutive transfers before rotating. This is a compromise: the DMA gets a burst of a defined length, and the core waits at most that length. The burst length is a design parameter of the fabric, not something firmware configures, and it is worth knowing because it sets the worst-case latency the core can experience.

There is a fourth behaviour that is not arbitration at all: some fabrics give the core’s instruction interface a dedicated path to flash and give the DMA controller no path to it at all. On such a part a DMA channel pointed at program flash cannot work, and the transfer has to be staged through SRAM, which is a documented restriction rather than a bug.

What contention looks like from the core

A contended access is not an error and does not appear in any status register. It appears as latency. The core’s load that would have taken one cycle takes several, and the pipeline stalls waiting for data. In a tight loop the effect is a proportional slowdown; in an interrupt handler it is jitter in the time between the interrupt assertion and the handler’s completion.

The jitter is the important part, because it is invisible to any code that measures average throughput. A handler that reads an ADC result from a buffer that the DMA is writing to the same bank will sometimes find the buffer locked by the DMA and wait for the current transfer to finish. The handler’s execution time then depends on where in the DMA transfer the interrupt arrived, which depends on the sample rate, which drifts with the clock. The result is a handler whose worst-case time is several times its typical time, and a real-time budget computed from the typical time is wrong.

Two mechanisms reduce it. Placing the buffer in a bank the core does not otherwise use removes the contention entirely. Failing that, sizing the DMA transfers so that each burst is short bounds the wait, at the cost of more arbitration overhead per byte. There is no setting that makes contention free while both masters use one port.

DMA burst size against bus efficiency

A DMA transfer can be configured to move one byte, one halfword, or one word per bus transaction, and the transaction size interacts with the arbitration.

A byte-wide transfer to a thirty-two-bit slave occupies a full bus cycle for eight bits of payload. The slave may have byte-lane strobes and write only the addressed byte, but the transaction still costs one address phase and one data phase, so four byte transfers cost four address phases and four data phases to move what one word transfer moves in one of each. That means four times as many arbitration decisions and four times the chance of colliding with the core. Configuring the DMA for the widest transfer the peripheral supports is therefore not just a bandwidth optimisation; it reduces the contention window.

Peripherals constrain this. A UART data register is eight bits wide and cannot accept a word write without undefined behaviour on some parts, so a UART DMA channel moves bytes and pays the inefficiency. An SPI or I2S peripheral that shifts sixteen or thirty-two bits at a time accepts the wide transfer and benefits. The DMA’s transfer size must match the peripheral’s register width, and the part’s documentation states which sizes each request line supports.

Alignment is the second constraint. A word-wide transfer requires a word-aligned address on both sides. An unaligned buffer forces the DMA into byte transfers or into a fault, and the fault appears on the first transaction rather than at configuration time on parts that do not validate the address when the channel is enabled.

Peripheral request lines and flow control

The DMA does not decide when to transfer; the peripheral does. Each channel is bound to a request line, and the peripheral asserts the line when it has data to move or space to receive. The DMA transfers one item per assertion, or a burst per assertion if the peripheral supports it, and de-asserts its request until the next item.

This flow control is what keeps the DMA from overrunning a slow peripheral. A UART receiving at 115200 baud produces a byte roughly every 87 microseconds; the DMA can move that byte in a few bus cycles and then waits. The bus occupancy is negligible, and the contention with the core is correspondingly rare.

The opposite case is an ADC producing samples at a fixed rate into a buffer. The DMA’s occupancy is still small per sample, but the transfers are periodic and synchronised to the sample clock, so the contention with the core is periodic too. If the core’s own work is also periodic at a related rate, the two can beat against each other and produce a latency pattern that repeats every few milliseconds. This is the case where moving the buffer to another bank is worth the effort, because the alternative is a periodic stall that is hard to attribute.

A channel that is enabled while its peripheral’s request line is already asserted transfers immediately, before any data is available, which on a receive channel reads a stale register and on a transmit channel writes one. Clearing the peripheral’s pending request before enabling the channel avoids it, and the order is the same as the clock-enable ordering described in the peripheral bring-up article.

Completion, half-transfer and the interrupt the core actually takes

A DMA channel raises an interrupt when it finishes, and most channels also raise one at the halfway point of the buffer. The half-transfer interrupt exists for double buffering: the core processes the first half while the DMA fills the second, and the roles swap at each half-transfer event. The result is that the core never touches the half being written, and the two never contend for the same cache line or the same word.

The timing of that interrupt is the real-time constraint. The core has exactly one half-buffer period to finish processing the previous half, and that period is buffer_size / (2 × sample_rate). If the core is late, the DMA overwrites data the core has not read, and the loss is silent — the buffer simply contains newer samples than expected.

Contention makes the margin smaller. The handler’s execution time is the sum of its own work plus whatever stalls it takes on the bus, and the stalls are largest when the DMA is most active, which is exactly during the transfer the handler is trying to keep up with. A design that meets its deadline with the DMA idle may miss it under load, and the measurement that reveals this is the worst-case handler duration with the DMA running at full rate, not the average.

The final subtlety is that a DMA completion interrupt and a DMA error interrupt share a channel’s interrupt line on many parts. A transfer that stops early because the peripheral de-asserted its request, or because the channel’s counter reached zero in an unexpected state, raises the same interrupt as a successful completion. The handler must read the channel’s status register to distinguish them, and a handler that assumes completion will advance a buffer pointer past data that was never written. The status flags are cleared by writing to the channel’s clear register, and a flag left set produces an interrupt that fires again immediately after the handler returns.

References

  1. ARMv7-M Architecture Reference Manual (ARM DDI 0403) — Arm Limited
  2. AMBA AHB-Lite Protocol Specification (ARM IHI 0033) — Arm Limited
  3. Cortex-M7 Technical Reference Manual (ARM DDI 0489) — Arm Limited