DMA: the worker that moves data without the CPU
Hardware takes over copying between memory and peripherals. How ADC sampling, UART reception and SPI display transfers get faster.
Receive 1000 bytes over UART and the interrupt fires 1000 times. With DMA you configure once, "move 1000 bytes from the UART data register to this buffer", and get a single interrupt when it is finished.
Values to configure
- Source and destination addresses: for example
&USART1->DR→rx_buf - Transfer count: 1000
- Increment: the peripheral address is fixed, the memory address increases by 1
- Data size: byte, half-word, word
- Trigger: which peripheral event moves one item each time
- Mode: once (normal), or wrap to the start and repeat (circular)
Common combinations
| Combination | Effect |
|---|---|
| Timer → ADC → DMA | Samples stacked in a buffer at an exact period |
| UART receive + circular DMA + IDLE interrupt | Receiving packets of unknown length |
| Memory → SPI | Sending a display frame |
Cautions
- If the CPU reads a buffer DMA is writing, it sees half-updated data. Use double buffering, splitting the buffer in two with the half-complete interrupt.
- On cores with a cache (M7), cache cleaning and invalidation are needed.
It takes over the copying the CPU was doing
When receiving 1000 bytes over UART, the interrupt method calls the handler 1000 times. With DMA, hardware moves each byte straight to the buffer as it arrives, and the CPU is notified just once, when everything is done.
| Polling | Interrupt | DMA | |
|---|---|---|---|
| CPU use | Waits the whole time | Interrupted for every byte | Once at the end |
| Fast data | Easily missed | Missed if the handler is delayed | Copes well |
| Set-up | Easiest | Medium | Most complex |
| Suited to | Slow, infrequent work | Byte-by-byte handling | Long buffers, high speeds |
Five things the set-up needs
- Source: the address of the peripheral's data register (for example
&USART1->RDR) - Destination: the address of a buffer in RAM
- Count: the number of items to move
- Direction and size: peripheral → memory or the reverse, and how many bits at a time
- Address increment: increasing on the buffer side, fixed on the peripheral side
Setting the peripheral-side address to increment is a common mistake. The data register is always at the same address, so it must be fixed.
RoadmapSee where this note sits in Computer Architecture→Receiving UART in circular mode
static uint8_t rx_buf[256];
// DMA configured in circular mode: when full, it starts filling again from the beginning
uint16_t rx_tail;
void uart_poll(void) { // from the main loop or a 1 ms tick
uint16_t head = 256u - dma_remaining(); // where DMA has written up to
while (rx_tail != head) {
process_byte(rx_buf[rx_tail]);
rx_tail = (rx_tail + 1u) & 255u;
}
}
The position DMA is writing to acts as the head of a ring buffer. No bytes are lost even with no interrupts at all. For packets of varying length, combine it with the UART IDLE interrupt (the line has been quiet for one character time) to know where a packet ends.
Several ADC channels at once
Put the ADC in scan mode and connect DMA, and the values fill an array in channel order.
volatile uint16_t adc_val[4]; // [0]=current A, [1]=current B, [2]=voltage, [3]=temperature
With a timer triggering the ADC and DMA moving the results, the code only has to read the array.
Seamless handling with the half-complete interrupt
For data that flows continuously, such as audio, split the buffer in two. Process the front half on the notification that it is full, and the back half on the notification that the end was reached. The half DMA is writing and the half the code is reading never overlap.
Problems that come up in practice
- The buffer is a local variable: when the function ends the stack is reused, but DMA keeps writing to that address. Make the buffer global or
static. - MCUs with a cache (Cortex-M7): the memory DMA changed and the cache contents diverge. Align the buffer to cache lines and invalidate, or place it in a non-cached region.
- Channel conflicts: the DMA channels each peripheral can use are fixed, and two may want the same one. Check with a table early in the design.
- Changing settings during a transfer: always disable the channel, change it, then enable it again.
- Error flags not handled: a transfer error stops the channel. Restart it in the error interrupt.