Interrupts vs. DMA on the Cortex-M4: When to Use Which
Both interrupts and DMA exist to keep work off your main loop, so engineers new to embedded often treat them as interchangeable. They are not. Choosing the wrong one is how you end up with a system that either drops data under load or spends all its time in interrupt handlers.
What an interrupt costs
An interrupt is the CPU stopping what it is doing, saving state, running your handler, and restoring state. On a Cortex-M4 the hardware saves and restores the caller-saved registers for you, which keeps entry and exit fast, but it is not free. Every interrupt is context-switch overhead plus whatever your handler does.
That overhead is invisible at low rates and ruinous at high ones. If a UART running at 1 Mbaud interrupts once per byte, you are taking an interrupt roughly every 10 microseconds. The handler entry and exit alone can eat a large fraction of your CPU before your actual logic runs. This is called interrupt storm, and it is a real failure mode, not a theoretical one.
What DMA costs
A DMA controller moves data between a peripheral and memory (or memory to memory) without waking the CPU at all. You configure a transfer once: source, destination, count, and whether to increment the addresses. The controller then moves the whole block and raises a single interrupt when it is done (or half done, for double-buffering).
The cost of DMA is configuration complexity and a scarcer resource. There are a fixed number of DMA streams, each mapped to specific peripherals through specific channels, and getting that mapping wrong is a common bring-up bug. DMA also shares the memory bus with the CPU, so a very heavy transfer can add latency to CPU accesses.
The decision
- Use an interrupt when the event rate is low to moderate and each event needs a decision. A button press, a command byte that changes state, a timer tick that advances a state machine. The whole point is that software reacts to each event.
- Use DMA when you are moving a block of data and the CPU has no per-item decision to make. Streaming ADC samples into a buffer, shifting out a frame over SPI, receiving a known-length packet over UART. The CPU only cares when the block is complete.
- Use both together for the common case: DMA moves the bytes, and a single completion interrupt tells the CPU the buffer is ready. This is the pattern that scales, and it is why "add DMA" is often the fix for a system that is dropping data in its receive interrupt.
The trap: half-transfer and buffer ownership
The subtle DMA bug is not configuration, it is ownership. With a circular DMA buffer feeding a stream, the controller is writing to the buffer while your code reads from it. If you process past the point the DMA has actually written, you read stale data. The half-transfer interrupt exists precisely so you can process the first half while the second half fills, then swap. Getting this ping-pong right is a timing and state-reasoning problem, which is why it is a favorite of real interview questions and of OhmWork's DMA challenges.