HomeAbout UsContact Us

Fixing STM32 DMA Double Buffering Data Corruption

By Jithin Tom
Published in Embedded C/C++
August 24, 2026
3 min read
Fixing STM32 DMA Double Buffering Data Corruption

Table Of Contents

01
The Corruption Mechanisms
02
Complete Implementation
03
Buffer Placement and Alignment
04
Timing Analysis
05
Common Failure Modes
06
Verification Checklist
07
Summary
08
Related Reading
09
References
10
Frequently Asked Questions

STM32 DMA double buffering is the standard pattern for continuous high-throughput data acquisition — ADC sampling, audio streaming, communication protocols. The concept is simple: two buffers, DMA fills one while CPU processes the other, swap on transfer complete. In practice, data corruption appears in three forms: torn reads (CPU reads half-old half-new data), stale data (CPU reads cached copy), and buffer overrun (DMA overwrites unprocessed buffer). Each stems from a specific hardware-software interaction that the reference manual mentions but doesn’t emphasize.

The Corruption Mechanisms

1. The Software Buffer Swap Fallacy

A common mistake is implementing double buffering manually by disabling the DMA stream in the Transfer Complete (TC) interrupt, swapping pointers in software, and re-enabling it. For continuous data acquisition like ADC sampling, this introduces a fatal gap: while the DMA is disabled, the peripheral continues generating data. The ADC data register is overwritten, and samples are permanently lost (ADC Overrun).

// WRONG: Manual pointer swap causes dropped ADC samples
void DMA1_Stream0_IRQHandler(void) {
if (LL_DMA_IsActiveFlag_TC0(DMA1)) {
LL_DMA_ClearFlag_TC0(DMA1);
LL_DMA_DisableStream(DMA1, LL_DMA_STREAM_0);
while (LL_DMA_IsEnabledStream(DMA1, LL_DMA_STREAM_0)); // ADC overrun happens here!
active_buffer = (active_buffer == buffer_a) ? buffer_b : buffer_a;
LL_DMA_SetMemoryAddress(DMA1, LL_DMA_STREAM_0, (uint32_t)active_buffer);
LL_DMA_EnableStream(DMA1, LL_DMA_STREAM_0);
}
}

The true fix is to never swap pointers in software. Instead, use the STM32’s hardware Double Buffer Mode (DBM) or Circular Mode. These modes automatically wrap or swap memory targets at the hardware level with zero latency, entirely eliminating the race condition.

2. Cache Coherency on Cortex-M7

Cortex-M7 (STM32H7, STM32F7) has a data cache. DMA bypasses cache entirely — it reads/writes physical RAM. The CPU reads cached data. Without explicit cache maintenance, the CPU sees stale data after DMA writes, or DMA reads stale data after CPU writes.

// Before DMA reads from buffer (CPU wrote to it)
// Second argument is size in bytes
SCB_CleanDCache_by_Addr((uint32_t*)buffer, BUFFER_SIZE_BYTES);
// After DMA writes to buffer (CPU will read it)
// Second argument is size in bytes
SCB_InvalidateDCache_by_Addr((uint32_t*)buffer, BUFFER_SIZE_BYTES);

For double buffering, the inactive buffer must be invalidated before CPU access. The active buffer must be cleaned before DMA starts if CPU modified it.

3. Half-Transfer Interrupt Neglect

When using Circular Mode as a double buffer, the Half-Transfer (HT) interrupt is critical. It fires at the exact midpoint of the buffer. It signals “first half ready for processing” while the DMA continues filling the second half uninterrupted.

If you neglect the HT interrupt and only process data on the TC interrupt, you force the CPU to process the entire buffer before the DMA wraps around to the beginning. This doubles the effective latency budget requirement and severely risks data overrun if a processing spike occurs.

Complete Implementation

#include "stm32h7xx_hal.h"
#define HALF_BUFFER_SIZE 1024
#define FULL_BUFFER_SIZE (HALF_BUFFER_SIZE * 2)
// 32-byte aligned for Cortex-M7 D-Cache lines
static uint32_t dma_buffer[FULL_BUFFER_SIZE] __attribute__((section(".dma_buffer"), aligned(32)));
static volatile uint8_t buffer_half_ready = 0; // 0 = none, 1 = first half, 2 = second half
static volatile uint32_t overrun_count = 0;
void DMA1_Stream0_IRQHandler(void) {
// Half-transfer: first half of the buffer is ready
if (LL_DMA_IsActiveFlag_HT0(DMA1)) {
LL_DMA_ClearFlag_HT0(DMA1);
// Invalidate cache for the first half before CPU read
SCB_InvalidateDCache_by_Addr((uint32_t*)&dma_buffer[0], HALF_BUFFER_SIZE * 4);
buffer_half_ready = 1;
}
// Transfer complete: second half of the buffer is ready
if (LL_DMA_IsActiveFlag_TC0(DMA1)) {
LL_DMA_ClearFlag_TC0(DMA1);
// Invalidate cache for the second half before CPU read
SCB_InvalidateDCache_by_Addr((uint32_t*)&dma_buffer[HALF_BUFFER_SIZE], HALF_BUFFER_SIZE * 4);
buffer_half_ready = 2;
}
// Transfer error
if (LL_DMA_IsActiveFlag_TE0(DMA1)) {
LL_DMA_ClearFlag_TE0(DMA1);
overrun_count++;
}
}
int main(void) {
HAL_Init();
SystemClock_Config();
// Enable D-cache for Cortex-M7
SCB_EnableDCache();
// DMA clock enable
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1);
// DMA stream configuration
LL_DMA_SetChannelSelection(DMA1, LL_DMA_STREAM_0, LL_DMA_CHANNEL_0);
LL_DMA_SetDataTransferDirection(DMA1, LL_DMA_STREAM_0, LL_DMA_DIRECTION_PERIPH_TO_MEMORY);
LL_DMA_SetStreamPriorityLevel(DMA1, LL_DMA_STREAM_0, LL_DMA_PRIORITY_HIGH);
LL_DMA_SetMode(DMA1, LL_DMA_STREAM_0, LL_DMA_MODE_CIRCULAR); // Crucial for continuous streaming!
LL_DMA_SetPeriphIncMode(DMA1, LL_DMA_STREAM_0, LL_DMA_PERIPH_NOINCREMENT);
LL_DMA_SetMemoryIncMode(DMA1, LL_DMA_STREAM_0, LL_DMA_MEMORY_INCREMENT);
LL_DMA_SetPeriphSize(DMA1, LL_DMA_STREAM_0, LL_DMA_PDATAALIGN_WORD);
LL_DMA_SetMemorySize(DMA1, LL_DMA_STREAM_0, LL_DMA_MDATAALIGN_WORD);
LL_DMA_SetFIFOMode(DMA1, LL_DMA_STREAM_0, LL_DMA_FIFOMODE_ENABLE);
LL_DMA_SetFIFOThreshold(DMA1, LL_DMA_STREAM_0, LL_DMA_FIFOTHRESHOLD_FULL);
LL_DMA_SetMemoryBurst(DMA1, LL_DMA_STREAM_0, LL_DMA_MBURST_SINGLE);
LL_DMA_SetPeriphBurst(DMA1, LL_DMA_STREAM_0, LL_DMA_PBURST_SINGLE);
// Set addresses and size
LL_DMA_SetPeriphAddress(DMA1, LL_DMA_STREAM_0, (uint32_t)&ADC1->DR);
LL_DMA_SetMemoryAddress(DMA1, LL_DMA_STREAM_0, (uint32_t)&dma_buffer[0]);
LL_DMA_SetDataLength(DMA1, LL_DMA_STREAM_0, FULL_BUFFER_SIZE);
// Enable HT, TC, and TE interrupts
LL_DMA_EnableIT_HT(DMA1, LL_DMA_STREAM_0);
LL_DMA_EnableIT_TC(DMA1, LL_DMA_STREAM_0);
LL_DMA_EnableIT_TE(DMA1, LL_DMA_STREAM_0);
NVIC_SetPriority(DMA1_Stream0_IRQn, 5);
NVIC_EnableIRQ(DMA1_Stream0_IRQn);
// Start ADC + DMA
LL_DMA_EnableStream(DMA1, LL_DMA_STREAM_0);
LL_ADC_REG_StartConversion(ADC1);
while (1) {
// Process ready buffers
if (buffer_half_ready == 1) {
process_adc_data(&dma_buffer[0], HALF_BUFFER_SIZE);
buffer_half_ready = 0;
} else if (buffer_half_ready == 2) {
process_adc_data(&dma_buffer[HALF_BUFFER_SIZE], HALF_BUFFER_SIZE);
buffer_half_ready = 0;
}
}
}

Buffer Placement and Alignment

Place the DMA buffer in a dedicated RAM section with explicit alignment to ensure the buffer starts on a cache line boundary:

/* In linker script */
.dma_buffer (NOLOAD) :
{
. = ALIGN(32);
*(.dma_buffer)
. = ALIGN(32);
} > RAM_D2

32-byte alignment ensures the buffer starts on a cache line boundary (Cortex-M7 cache line = 32 bytes). Since HALF_BUFFER_SIZE * 4 = 4096 bytes is a multiple of 32, the boundary between the two buffer halves also falls on a cache line boundary. This prevents false sharing where invalidating one half’s cache lines affects the other.

Timing Analysis

Full DMA Transfer (2048 samples @ 1 MSPS) = 2.048 ms
Half Transfer (1024 samples @ 1 MSPS) = 1.024 ms
CPU Processing Budget per half = 1.024 ms
Cache Invalidate (1024 words = 4 KB) = ~15 µs
Total ISR Overhead (flag clear + invalidate) = ~18 µs

With HT + TC handling, the CPU gets 1.024 ms to process each 1024-sample half while the DMA fills the other half continuously. Without HT handling (TC only), the CPU would need to process all 2048 samples within 2.048 ms — but with zero margin for variability since the DMA immediately wraps around.

Common Failure Modes

SymptomRoot CauseFix
Periodic glitches at buffer boundaryCache line shared between buffers32-byte alignment + separate cache lines
Missed samples / ADC OverrunSoftware buffer swap via stream disableUse Circular Mode with HT/TC interrupts
Random corruption under loadHT interrupt not handled, overrunImplement HT processing or increase buffer size
Hard fault in ISRUnaligned memory accessEnsure buffer alignment, use word-aligned pointers
Data stale after first transferD-cache not enabled or maintainedSCB_EnableDCache() + Clean/Invalidate protocol

Verification Checklist

  1. Buffer alignment: addr % 32 == 0 for both buffer halves
  2. Cache maintenance: Invalidate before CPU read, clean before DMA read
  3. Hardware Continuous Mode: Use Circular Mode instead of software disable/re-enable
  4. HT handling: Signal readiness in HT and TC interrupts, process each half in the main loop
  5. Error recovery: TE interrupt tracks overruns
  6. Overrun counter: Monitor overrun_count in production

Summary

STM32 DMA double buffering corruption stems from three hardware realities: DMA and CPU share memory without hardware coherency (Cortex-M7), software buffer swapping creates fatal gaps (dropped ADC samples), and half-transfer handling is necessary for variable processing loads. The fix is disciplined cache maintenance, utilizing Hardware Circular Mode to continuously stream without disabling the DMA, and precise HT/TC interrupt utilization. The implementation above runs corruption-free at 1 MSPS ADC sampling on STM32H743.

  • Zero-Copy DMA Patterns on ARM Cortex-M
  • Cortex-M Cache Maintenance for DMA Coherency
  • Fixing UART DMA Overrun Errors on STM32

References

  1. STMicroelectronics, STM32H743 Reference Manual, RM0433, Section 13.5.5 (DMA double buffer mode)
  2. ARM, Cortex-M7 Processor Technical Reference Manual, Section 7.3 (Cache maintenance operations)
  3. STMicroelectronics, AN4891: STM32H7 Series DMA Optimization, Section 3.2 (Cache coherency)
  4. ARM, Cache Maintenance for DMA Transfers, Application Note 321
  5. FreeRTOS, Stream Buffer and Message Buffer Implementation, DMA integration guide
  6. STM32CubeH7 HAL Driver, stm32h7xx_hal_dma_ex.c - Double buffer mode implementation

Frequently Asked Questions

What causes data corruption in STM32 DMA double buffering?

Data corruption occurs when the CPU reads a buffer half that the DMA is still writing to, when cache coherency is not maintained between the CPU and DMA memory views (Cortex-M7), or when software manually disables and re-enables DMA to swap pointers, creating a gap where peripheral data is lost.

Why does half-transfer interrupt matter for double buffering?

The half-transfer (HT) interrupt signals the midpoint of a DMA transfer, giving the CPU time to process the first half while DMA fills the second half. Without HT handling, the CPU only sees complete transfers, doubling effective latency and risking overrun if processing exceeds half-transfer time.

How do you fix cache coherency issues with DMA on Cortex-M7?

On Cortex-M7 with data cache, use SCB_CleanDCache_by_Addr() before DMA reads from memory and SCB_InvalidateDCache_by_Addr() after DMA writes to memory. For double buffering, invalidate the inactive buffer before CPU reads it, and clean the active buffer before DMA writes to it.

What is the correct buffer swap sequence to avoid race conditions?

Use the DMA's hardware Double Buffer Mode (DBM) or Circular Mode. Manually disabling the DMA in software to swap pointers creates a gap where peripheral data (like ADC samples) is lost, causing overruns. Hardware modes wrap pointers automatically with zero latency.

When should you use double buffering vs circular DMA?

In STM32, Double Buffer Mode (DBM) allows swapping the inactive memory address on-the-fly (e.g., for scatter-gather streaming). Circular DMA uses a single static memory buffer that wraps automatically. Both utilize Half-Transfer and Transfer Complete interrupts to process data seamlessly.

Tags

stm32dmadouble-bufferingcortex-mdata-corruption

Share


Previous Article
Automating Firmware Release Pipelines with GitHub Actions
Jithin Tom

Jithin Tom

A Closer Look at C/C++, RTOS, and Embedded Systems

Related Posts

Cortex-M Floating-Point Unit Lazy Stacking Optimization
Cortex-M Floating-Point Unit Lazy Stacking Optimization
August 19, 2026
3 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media