Sitemap

DMA in Embedded Systems: How It Really Works with Peripherals

4 min readMar 13, 2026

--

When engineers first hear about DMA, it is usually described as “data transfer without CPU involvement.” That is correct, but too vague to help when you actually have to use it in firmware.

A better way to understand DMA is through a real use case. Suppose you need to send a large buffer over UART. One option is for the CPU to wait until the UART transmit register is ready, write one byte, wait again, write the next, and repeat until the full buffer is sent. The other option is to configure DMA once and let hardware feed the UART automatically.

That is where DMA becomes useful in embedded systems.

What DMA actually is

DMA stands for Direct Memory Access. It is a hardware controller inside the microcontroller or SoC that moves data between memory and peripherals, or sometimes between memory regions, without the CPU copying each item manually.

The CPU still has a role, but only at the beginning and the end. It sets up the source address, destination address, transfer count, direction, and mode. Once the transfer is enabled, the DMA controller handles the repetitive movement of data in hardware.

So DMA is not a software trick and not a background thread. It is a dedicated hardware data mover.

Where DMA sits in the system

DMA lives below the application and driver layers, at the chip interconnect level.

Inside the MCU, the CPU, RAM, DMA controller, and peripherals all sit on an internal bus. The DMA controller can access memory and peripheral registers through that bus, which means it can move data independently of CPU instruction execution.

For a UART transmit transfer, the path is:

RAM -> DMA -> UART transmit register -> UART shift logic -> TX pin

DMA does not “send UART data” by itself. It only writes bytes into the UART transmit register. The UART peripheral then serializes those bytes and drives them out on the TX line.

How DMA works with a peripheral

DMA works with a peripheral through a hardware handshake. The peripheral tells DMA when it is ready, and DMA transfers one unit of data at that time.

Get DivyeshSankhla’s stories in your inbox

Join Medium for free to get updates from this writer.

For UART transmit, the sequence is simple:

  1. UART transmit register becomes ready.
  2. UART raises a DMA request.
  3. DMA reads the next byte from RAM.
  4. DMA writes that byte into the UART transmit register.
  5. UART shifts that byte onto the TX pin.
  6. This repeats until the transfer count reaches zero.

That is the key idea: the peripheral controls the pacing, and DMA supplies the data.

UART TX with DMA: code

/* Vendor-neutral code: UART transmit using DMA */
static uint8_t tx_buf[] = "Hello from DMA UART\r\n";void uart_dma_tx_start(void)
{
/* Configure UART first: baud rate, frame format, TX enable */
UART->BAUD = UART_BAUD_115200;
UART->CTRL = UART_TX_ENABLE;
/* Configure DMA addresses
* Source = RAM buffer
* Destination = UART transmit data register
*/
DMA_CH->SRC_ADDR = (uint32_t)tx_buf;
DMA_CH->DST_ADDR = (uint32_t)&UART->TDR;
/* Number of bytes to send */
DMA_CH->COUNT = sizeof(tx_buf) - 1;
/* Configure DMA behavior
* - memory to peripheral
* - increment source after each byte
* - keep UART register address fixed
* - transfer width = 8-bit
* - enable interrupt on transfer complete
*/
DMA_CH->CTRL =
DMA_DIR_MEM_TO_PERIPH |
DMA_SRC_INC_ENABLE |
DMA_DST_INC_DISABLE |
DMA_WIDTH_8BIT |
DMA_TC_INT_ENABLE;
/* Allow UART to generate DMA requests when TX is ready */
UART->CTRL |= UART_TX_DMA_ENABLE;
/* Start DMA channel */
DMA_CH->CTRL |= DMA_CH_ENABLE;
}
void DMA_Channel_IRQHandler(void)
{
if (DMA_TC_FLAG)
{
DMA_CLEAR_TC_FLAG();
/* DMA is done feeding UART, but UART may still be
* shifting the final byte out on the line.
* Wait until transmission is really complete.
*/
while (!(UART->STATUS & UART_TX_COMPLETE))
{
}
/* Safe point: transfer is fully complete */
}
}

What the code is really doing

The CPU does all the setup work first. It configures the UART, points DMA at the RAM buffer and the UART transmit register, sets the byte count, and enables the transfer.

After that, the UART and DMA work together in hardware. Whenever UART is ready for the next byte, it requests service. DMA responds by moving one byte from memory into the UART transmit register. UART then transmits that byte on the line.

The CPU is no longer stuck in a loop feeding the peripheral one byte at a time.

The detail that trips people up

DMA transfer complete does not always mean the peripheral is fully done.

For UART, DMA completion usually means all bytes were written into the UART transmit path. But the UART may still be shifting the last byte out on the TX pin.

That is why good implementations wait for both conditions:

  • DMA transfer complete
  • UART transmission complete

Only then is the transfer truly finished.

Common mistakes to avoid

Wrong transfer width
If UART expects 8-bit transfers and DMA is configured for a wider access size, the transfer may fail or corrupt data.

Wrong increment settings
Memory should usually increment. The UART transmit register should not.

Changing the buffer while DMA is active
Once DMA starts reading from a buffer, software should not modify it until the transfer completes.

Assuming DMA complete means line idle
DMA may finish before the UART has transmitted the last byte.

Ignoring cache coherency on cached MCUs
On devices with data cache, DMA may require cache clean or invalidate operations depending on TX or RX direction.

--

--