Reducing CPU Overhead with DMA on ARM Cortex-M
Learn how to use DMA on ARM Cortex-M microcontrollers to eliminate interrupt storms and reduce CPU overhead during peripheral data transfers.
31 May 2026, 09:37 UTC

The Cost of Byte-by-Byte Handling
When a microcontroller receives data via a peripheral like a UART, the default behavior is to trigger an interrupt for every single byte. For a high-baud rate stream, the CPU spends a significant percentage of its clock cycles simply entering and exiting Interrupt Service Routines (ISRs). This "interrupt storm" prevents the processor from performing complex calculations or entering low-power sleep modes, effectively bottlenecking the system.
The solution is Direct Memory Access (DMA). DMA allows peripherals to move data directly to or from system memory without involving the CPU. Instead of the CPU acting as the middleman for every byte, it simply configures the transfer and receives a single notification when the entire block of data is ready.
How DMA Offloads the Processor
A DMA controller acts as a secondary, specialized processor dedicated to data movement. It manages the bus requests and memory writes that the CPU would otherwise handle. By offloading these tasks, you achieve two primary goals:
- Increased Throughput: Data moves at the hardware's maximum speed without being gated by software execution time.
- Power Efficiency: The CPU can enter a sleep state (like Sleep or Stop mode on STM32/Cortex-M series) while the DMA continues to fill a buffer in the background.
Implementing UART-to-Memory Transfers
To move data from a UART peripheral to a memory buffer, the DMA controller must be mapped to the specific peripheral request line. In the ARM Cortex-M ecosystem, this usually involves configuring a specific Stream (the data path) and a Channel (the peripheral request source).
Configuration Example (CMSIS-style)
The following conceptual configuration demonstrates how to set up a DMA transfer for an incoming UART stream. This assumes a Cortex-M4 environment using a standard CMSIS-compliant driver.
// Buffer to hold incoming UART data
uint8_t rx_buffer[64];
void configure_uart_dma(void) {
// 1. Enable DMA Clock
RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN;
// 2. Configure DMA Stream
// Set peripheral address (UART Data Register)
DMA2_Stream2->PAR = (uint32_t)(&USART1->DR);
// Set memory address (Our local buffer)
DMA2_Stream2->M0AR = (uint32_t)rx_buffer;
// Set number of bytes to transfer
DMA2_Stream2->NDTR = 64;
// 3. Set Control Register
// CHSEL: Select Channel 4 (UART RX)
// DIR: Peripheral-to-Memory
// MINC: Memory increment mode enabled
// CIRC: Circular mode enabled (for continuous streaming)
DMA2_Stream2->CR |= (4 << DMA_CCR_CHSEL_Pos) | DMA_CCR_MINC | DMA_CCR_CIRC;
// 4. Enable the stream
DMA2_Stream2->CR |= DMA_CCR_EN;
}
Execution Context: Run this during the hardware initialization phase. Ensure you have privileged access to the RCC (Reset and Clock Control) and DMA registers. The DMA_CCR_CIRC flag is critical here; it tells the DMA to wrap back to the start of rx_buffer once it reaches the 64th byte, creating a circular buffer.
Critical Trade-offs and Limitations
DMA is not a "set and forget" feature; it introduces specific architectural challenges:
Bus Contention
Both the CPU and the DMA controller share the same system bus. If the DMA is performing massive, high-priority bursts, the CPU may experience "stalls" while waiting for bus access. This is rarely an issue for UART but can be significant for high-speed SPI or ADC sampling.
Cache Coherency
On higher-end Cortex-M cores (like the M7) that feature a Data Cache (D-Cache), the CPU might read a stale value from the cache while the DMA has updated the actual physical RAM. To prevent this, you must either:
- Clean/Invalidate the cache before reading the DMA buffer.
- Configure the DMA buffer area as "Non-Cacheable" using the MPU (Memory Protection Unit).
Silent Failures
A common risk is a mismatch between the DMA channel and the peripheral mapping. If you configure Stream 2 but the hardware only maps UART RX to Stream 5, the code will compile and run, but no data will ever move. Always check the DMA_HISR or DMA_LISR (Interrupt Status Registers) to verify that the transfer is actually progressing.
Verifying the Implementation
To confirm the DMA is functioning correctly, perform these three checks:
- Buffer Inspection: Use a debugger to inspect
rx_bufferwhile sending a known pattern (e.g., 0x01, 0x02, 0x03) from a PC. If the buffer fills without the CPU entering the UART RX interrupt, DMA is active. - CPU Load Test: Implement a simple counter in the main loop. Compare the number of increments per second when using interrupt-driven RX versus DMA-driven RX. The DMA version should show a significantly higher count.
- Error Flag Check: Read the DMA status registers. If the
TEIF(Transfer Error Interrupt Flag) is set, check for memory alignment issues or invalid address configurations.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.