Interrupts, DMA, and Ring Buffers

Practical rules for moving bytes between ISRs, DMA, and foreground code without blocking or corrupting shared state.

Embedded ยท Concurrency

Embedded concurrency usually starts at a hardware boundary: an interrupt fires, a DMA engine moves bytes, or a peripheral has a deadline. The firmware design is good when those events are short, bounded, observable, and unable to corrupt foreground state.

Keep interrupts short and specific

An interrupt handler should acknowledge the hardware condition, move the minimum data needed to preserve the event, signal foreground work, and exit. Avoid formatting strings, walking queues, allocating memory, or performing blocking peripheral transactions inside the ISR.

A useful rule: the ISR owns latency, foreground code owns policy. The UART RX ISR can push a byte into a buffer. The shell parser decides whether that byte completes a command.

Pick the ownership model first

Before adding atomics or critical sections, define ownership:

Pattern Owner Good fit
flag plus snapshot ISR writes flag, task reads state infrequent events, status changes
SPSC ring buffer one producer, one consumer UART RX/TX, samples, logs
DMA double buffer DMA fills, task processes completed half ADC, I2S, SPI streams
queue to task ISR posts event object RTOS systems with bounded queues
shared register block foreground configures, ISR observes low-rate control paths

Do not use a multi-producer structure when the topology is single producer and single consumer. Extra generality costs code size, cycles, and failure surface.

SPSC ring buffer invariants

For a single-producer, single-consumer ring:

  • producer is the only writer of head,
  • consumer is the only writer of tail,
  • both sides may read both indices,
  • capacity is one less than the allocated slot count if full and empty are represented by indices alone,
  • index updates happen after data writes on push and after data reads on pop.

On Cortex-M without data cache, volatile or atomic index access plus short critical sections can be enough, depending on compiler and language memory model. On cached MCUs or multi-core parts, cache maintenance and memory barriers become part of the design, not an optimization detail.

Backpressure is a product decision

Every buffer needs a full policy:

Policy Behavior Use when
drop newest preserve old data, reject new data command input, reliable records
drop oldest keep freshest state sensors, displays, telemetry views
block producer apply backpressure task context only, never ISR
overwrite slot newest wins low-value periodic status
signal overflow preserve failure evidence safety, debug, protocol framing

If an ISR can block, priority inversion and missed deadlines are already possible. If data can drop silently, the downstream consumer cannot distinguish a quiet system from a lossy one. Count drops.

DMA changes the contract

DMA is an independent bus master. The CPU no longer owns every write. The firmware must define:

  • buffer lifetime until transfer complete,
  • alignment and addressability required by the DMA controller,
  • cache clean or invalidate operations when applicable,
  • half-transfer and transfer-complete ownership handoff,
  • error interrupt handling,
  • peripheral overrun behavior when the consumer falls behind.

Double buffering is often simpler than a general ring for fixed-rate streams. The DMA fills half A while the task processes half B, then they swap on the half-transfer and transfer-complete interrupts.

Parse outside the interrupt

For serial protocols, separate byte collection from frame parsing. The ISR or DMA path should only preserve bytes and timestamps. The parser should run in task or foreground context where it can validate length, checksum, state transitions, and timeout behavior without extending interrupt latency.

Good parser evidence includes:

  • malformed frame rejection,
  • partial frame timeout,
  • overflow handling,
  • checksum failure path,
  • recovery after noise,
  • fuzz or property tests for host-buildable parser code.

Measure the timing

Use a spare GPIO as a timing marker around ISR entry, exit, and critical sections. Then measure:

  • worst-case ISR duration,
  • interrupt period at maximum event rate,
  • foreground drain rate,
  • high-water mark of each buffer,
  • overflow count under deliberate overload.

A system is not real-time because it uses interrupts or an RTOS. It is real-time when the worst-case path is shorter than the deadline with margin.

STM32 Bare-Metal Bring-Up, Measurement and Instrumentation, and Design Verification and Test.

Sources