In short: DMA double (ping-pong) buffering keeps a peripheral streaming continuously by having the CPU process one buffer while the DMA controller fills the other, with the half-transfer and transfer-complete interrupts signalling exactly when each half is safe to touch — so no sample is ever dropped or overwritten mid-flight.
If you have ever captured audio, read an ADC at 1 MSPS, or pulled packets off a high-speed serial link and seen glitches, dropped samples, or torn frames, the culprit is almost always a buffering race: the CPU and the DMA engine fighting over the same memory. Double buffering is the classic, deterministic fix. This article explains the mechanism precisely, works through the half-transfer interrupt timing, and shows the real numbers that decide your buffer size. It builds on the fundamentals in our introduction to DMA in embedded systems.
Why a single buffer fails
With one buffer, you have a producer (the DMA controller) and a consumer (the CPU) sharing a region with no coordination. Suppose a UART DMA fills a 256-byte buffer and raises a transfer-complete (TC) interrupt. The CPU starts copying or parsing those 256 bytes. But the instant DMA is re-armed, it begins overwriting byte 0 again. If your parse routine is slower than the incoming data rate — even occasionally, because a higher-priority ISR preempted it — DMA laps the CPU and you lose data silently. There is no backpressure on a streaming peripheral: the ADC does not wait for you.
The core problem is temporal overlap: a single buffer forces the window "DMA is writing" and the window "CPU is reading" to share the same bytes. Double buffering makes those windows touch different memory, so they can safely overlap in time.
The ping-pong mechanism
Allocate two equal buffers, A and B (or equivalently one array of length 2N, split into halves). DMA fills A while the CPU is idle or working on the previous data. When A is full, the controller is reconfigured — automatically in circular mode, or in the TC ISR — to fill B. The TC interrupt tells the CPU "A is complete, go process it." While the CPU chews through A, DMA is independently filling B. When B completes, roles swap: CPU takes B, DMA refills A. This alternation is the "ping-pong."
The half-transfer interrupt: one buffer, two events
On STM32 and many other MCUs you rarely allocate two named arrays. Instead you use one circular buffer of size 2N and enable two interrupts: Half-Transfer (HT) fires when N elements are done, Transfer-Complete (TC) fires at 2N. HT means "the first half is full and stable — process indices 0..N-1." TC means "the second half is full — process indices N..2N-1," and because the buffer is circular, DMA has already wrapped to index 0 and is refilling the first half. You get the ping-pong effect from a single contiguous region with zero reconfiguration.
Sizing the buffer: the only number that matters
The design constraint is simple: the CPU must finish processing one half before DMA finishes filling the other. Let the sample rate be R, the half-buffer hold N samples, and the worst-case processing time for N samples be Tproc. You need:
Tproc < N / R (the time DMA takes to fill one half). Add margin for ISR latency and preemption — aim for Tproc below ~70% of N/R.
Example: a stereo I2S microphone at 48 kHz, 2 channels, 16-bit. If N = 256 frames per half, DMA fills one half in 256 / 48000 ≈ 5.33 ms. Your FFT or filter on those 256 frames must complete in well under 5.33 ms. Make N too small and the HT/TC interrupt rate climbs (ISR overhead dominates); make N too large and latency and RAM footprint grow. The table shows the trade-off:
| Half size N (frames) | DMA fill time @48 kHz | IRQ rate (HT+TC) | Latency added |
|---|---|---|---|
| 64 | 1.33 ms | ~1500 /s | low |
| 256 | 5.33 ms | ~375 /s | moderate |
| 1024 | 21.3 ms | ~94 /s | high |
Cache and coherency pitfalls
On Cortex-M7, Cortex-A, or any core with a data cache, double buffering has a sharp edge: DMA writes to physical RAM, but the CPU may read stale data from cache. After a TC/HT interrupt you must invalidate the cache lines for the half you are about to read (SCB_InvalidateDCache_by_Addr), and before a memory-to-peripheral DMA you must clean (flush) the lines you wrote. Align each buffer to a 32-byte cache line and pad to a multiple of 32 bytes, or a neighbouring variable sharing a line will get corrupted on invalidate. This is one of the most common streaming bugs seen in the field and in logic-analyzer traces.
Verifying it in practice
Treat correctness as measurable, not hopeful. Toggle a GPIO high at the start of your processing ISR and low at the end, then watch it on a scope or logic analyzer (Saleae, or the trace tools bundled with Siemens/Mentor and Synopsys verification suites for the RTL side of a custom DMA engine). If the high pulse ever approaches N/R, you are on the edge of overrun. For a soft-core or ASIC DMA controller, gate-level and system-level verification in Cadence Xcelium or Synopsys VCS should include a directed test where the consumer stalls, proving the HT/TC flags and overrun status bit behave. Teams building custom silicon can engage specialist help through Avecas VLSI design services.
For a broader treatment of interrupt latency budgeting that feeds directly into the sizing math above, see our guide to interrupt latency optimization, and browse the full CourseTron course catalogue.
Want to implement ping-pong DMA on real STM32 hardware, with cache maintenance and scope-verified timing? Our hands-on Embedded Systems & Firmware course on CourseTron walks you through it line by line, with graded labs.
Frequently asked questions
What is the difference between double buffering and circular (ring) buffer DMA?
They overlap heavily. Circular DMA with HT and TC interrupts is the most common implementation of double buffering on microcontrollers: one 2N region, two events. "True" double-buffer mode (e.g. STM32 DBM) uses two distinct memory addresses the controller swaps between and can let you change the next address on the fly. Both achieve the same goal — concurrent fill and drain.
Why do I need the half-transfer interrupt — can't I just use transfer-complete?
With a single 2N circular buffer, TC alone only tells you the whole thing is full, by which point DMA has already wrapped and is overwriting the first half you never processed. HT gives you the mid-point event so each half is handed to the CPU exactly when it becomes stable and before DMA returns to it.
What happens if the CPU is too slow and DMA overruns?
The controller keeps writing regardless — streaming peripherals have no flow control — so DMA laps the CPU and you silently lose or corrupt the half still being read. Many controllers set an overrun/error flag you should enable and check. The fix is larger N, faster processing, or raising the processing task's priority.
Do I always need cache maintenance?
Only on cores with a data cache (Cortex-M7/M55, Cortex-A, etc.). On cacheless Cortex-M0/M3/M4 the RAM the DMA writes is what the CPU reads, so no invalidate/clean is needed — though you still need the volatile qualifier and correct memory barriers.