STM32 UART + DMA Communication Guide
Interrupt-vs-DMA, Circular Buffers, IDLE Detection, Error Handling & Baud-Rate Math
1. Introduction — Why DMA Changes UART Design
A UART is the most common serial interface on STM32 parts, and its implementation choice — poll, interrupt (RXNE/TXE) or DMA — decides the CPU load, the maximum sustainable throughput and the robustness of the link. Polling burns the core; per-byte interrupts are fine at low rates but at high baud rates they consume most of the CPU and miss bytes under interrupt latency jitter. DMA moves each byte between memory and the USART without CPU involvement, letting the core run the application while the peripheral streams data. The catch: DMA needs careful handling of the “unknown length” problem (how do you know a message ended?), of the half/full/circular buffer semantics, and of errors (overrun, framing, noise, break) that the DMA path can silently swallow. This guide covers the UART+DMA pipelining, the baud-rate math and BRR setup, the circular-buffer + IDLE-line detection pattern that is the de-facto standard on STM32, error handling, the DMA stream configuration, flow-control options, and a complete worked example with code. It complements the STM32 DMA design guide and the CAN guide on this site.
2. UART Basics and Baud-Rate Math
The USART’s baud rate is generated by dividing the peripheral clock (PCLK, typically APB1/APB2) by the BRR register. In the oversampling-by-16 mode (default, OVER8=0):
USARTDIV = f_PCLK / (16 · baud) → BRR = USARTDIV
So for f_PCLK = 16 MHz and a target of 115200 baud: USARTDIV = 16e6/(16·115200) = 8.68 → BRR = 0x08B (integer 8, fraction 0.68 → 0x0B in the Frac[3:0]). The actual resulting baud = 16e6/(16·8.68) ≈ 115207 — well within the standard ±3% tolerance. The accuracy depends on the exact PCLK: if the APB clock is not a clean multiple of the baud rate (e.g. an 8 MHz crystal driving a 72 MHz system clock with a 36 MHz APB1), the error grows and long multi-byte frames drift; the rule is to derive the baud from a clock that makes USARTDIV nearly integral or to accept < 3% error. When designing the clock tree, the STM32 clock configuration calculator pins down the exact PCLK1/PCLK2 values, which is the first step of any UART baud budget — a “115200 that is really 4% off” is the classic source of sporadic first-byte garbage in multi-peer links.
For the 8-bit/8N1 standard frame, the bit-time at 115200 baud is 8.68 µs; at 9600 it is 104 µs. The DMA approach shines exactly here: at 115200 (≈11.5 kbyte/s) a per-byte interrupt would fire every ~87 µs; with DMA the CPU never attends the data path at all.
3. Interrupt vs DMA — When Each Wins
| Criterion | Interrupt (RXNE/TXE) | DMA (circular) |
|---|---|---|
| CPU load per byte | High (entry/exit overhead) | Near zero |
| Max sustainable baud | ~ few hundred kbaud (dep. on ISR) | Full peripheral rate |
| Unknown-length RX | Easy (byte-by-byte) | Needs IDLE/character-detection |
| Burst/long packets | Latency jitter may drop bytes | Robust, no jitter |
| Overrun risk | If ISR too slow | Managed with double-buffer/half flag |
| Complexity | Low | Higher (buffer semantics) |
Rule of thumb: for < 57600 baud with short, event-driven messages, interrupt RX is simple and adequate; for sustained high throughput, bursty telemetry, or when the core must not be disturbed, use DMA + circular buffer + IDLE detection. Many designs use interrupt TX for short command replies and DMA RX for the continuous stream.
4. The Circular DMA Buffer + IDLE Pattern
The de-facto STM32 receive pattern:
- Configure the UART so its RX request triggers a DMA channel, and the DMA runs in circular mode into a RAM buffer (e.g. 256 bytes).
- Enable the UART’s IDLE-line interrupt (the line going idle after a frame = end of an Ethernet/AT-command style message) — the IDLE event clears the receive FIFO state and signals that at least one frame boundary occurred.
- In the IDLE ISR, read the DMA’s current pointer (DMA_GetCurrDataCounter → how many bytes are left) and compute how many new bytes arrived since the last pointer snapshot; copy them to the application line/message buffer.
- Because DMA loops circularly, handle the wrap: if the new bytes cross the ring end, copy the two segments separately.
This pattern gives variable-length packets with a fixed-size buffer, zero per-byte interrupts, and no bus contention. The memory injected into this flow needs no copies beyond the message boundary; the CPU wakes only at the message granularity.
5. DMA Stream Setup on STM32 (CubeMX / LL Example)
Practical, shortened flow (STM32G4/STM32F4 family, LL driver for clarity):
// 1) UART + DMA clocks
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1);
// 2) Configure the DMA stream for USART2_RX (channel per the device mapping)
DMA_Init.Instance = DMA1_Channel6; // USART2_RX on this part
DMA_Init.PeriphOrM2MSrcAddress = (uint32_t)&USART2->RDR;
DMA_Init.MemoryOrM2MDstAddress = (uint32_t)uart_rx_buf;
DMA_Init.Direction = LL_DMA_DIRECTION_PERIPH_TO_MEMORY;
DMA_Init.Mode = LL_DMA_MODE_CIRCULAR;
DMA_Init.PeriphOrM2MSrcIncMode = LL_DMA_PERIPH_NOINCREMENT;
DMA_Init.MemoryOrM2MDstIncMode = LL_DMA_MEMORY_INCREMENT;
LL_DMA_Init(DMA1, DMA1_Channel6, &DMA_Init);
LL_DMA_EnableStream(DMA1, DMA1_Channel6);
// 3) Gas the UART's RX-DMA request and the IDLE interrupt
LL_USART_EnableDMAReq_RX(USART2);
LL_USART_EnableIT_IDLE(USART2);
LL_USART_Enable(USART2);
The DMA’s “current data counter” (NDTR) decreases as bytes arrive; the number of received bytes since the last snapshot = initial_N − NDTR, wrapped modulo the buffer size. The IDLE ISR then: read NDTR, compute the delta, extract the frame, and update the snapshot pointer. Choosing the DMA stream and channel that maps to the target USART on the specific STM32 is where the DMA stream allocator helps — it removes the guesswork from the peripheral-to-stream/channel mapping table for the whole family at once.
6. Error Handling in the DMA Path
DMA masks the byte-level events, so the UART status register (SR) errors are easy to miss. Before processing the received frame, always check SR bits: ORE (overrun), FE (framing), NE (noise) and if used, the break flag. On an ORE, the incoming data has been permanently lost — the receive must be re-synchronized: read DR to clear, reset the DMA state index, and (if required by the protocol) request a retransmission. A common practice is to reset the circular-index snapshot and drop the already-buffered half-frame when the error bit is set, then re-arm. The DMA transfer-complete (TC) and half-transfer (HT) interrupts can also drive the “process lower half while upper fills” double-buffered pattern, giving the CPU the biggest continuous block with the least ISR overhead.
7. Worked Example — 2 Mbaud Telemetry Stream, 128-byte Messages, 256-byte Ring
Target: receive a continuous 2 Mbaud 8N1 stream of 128-byte frames from a sensor over USART2, with the CPU free to run a control loop, wake only per frame.
- Clock: PCLK1 must be a multiple that keeps 2 Mbaud accurate: choose PCLK1 = 64 MHz → USARTDIV = 64e6/(16·2e6) = 2.0 exactly → BRR = 0x20, 0% error. Confirm with the clock calculator that the PLL can deliver 64 MHz on APB1.
- Buffer: a 256-byte circular DMA buffer (two full frames per lap) — with 128-byte frames, wrap happens at most once per message; the ISR handles the two-segment copy.
- DMA: USART2_RX → DMA1 stream/channel confirmed by the stream allocator, circular mode, memory increment, peripheral no-increment.
- Firmware: enable DMA, enable USART RX-DMA request, enable IDLE IT. IDLE ISR (~a few dozen cycles) runs once per 128-byte frame, i.e. ~ every 640 µs at 2 Mbaud — <1% CPU; the control loop runs freely meanwhile.
- Error path: in the ISR, read SR; if ORE or FE set during a frame, drop the fragment, reset the snapshot, re-arm; the protocol layer then requests a re-sync chunk from the sensor.
- Benchmark: while streaming, toggle a GPIO in the ~2 kHz control loop; measure jitter < 1 µs to prove the DMA path is transparent.
8. Common Mistakes
- Wrong DMA channel/stream mapping: the RX DMA never fills the buffer because the request is routed to the wrong stream — check the part’s DMAMUX/mapping table.
- Reading NDTR as a “length from the start”: in circular mode NDTR counts down and wraps; always compute the delta from your last snapshot modulo the ring size.
- Ignoring ORE/FE in DMA mode: the DMA keeps receiving past an error; stale garbage enters the buffer and your parser misaligns —always inspect SR first.
- Running the RX buffer to the exact frame size: a 128-byte frame in a 128-byte ring puts the wrap exactly at the boundary — size the ring ≥ 2 frames or handle the exact-wrap case.
- Not clearing the IDLE flag before re-arming: a stale IDLE flag fires an immediate phantom “message complete”.
- Baud error over 3%: the first bytes arrive corrupted on long frames — always compute USARTDIV from the real PCLK, not a nominal 72/36 MHz.
9. FAQ
Q: Does DMA reduce the interrupt latency jitter? A: Yes — the data path never enters an ISR per byte, so the application’s worst-case latency depends only on its own scheduling; DMA RX adds no byte-level jitter.
Q: How do I know a message ended with DMA? A: Use the IDLE-line interrupt (detects the bus going idle after the last byte) or the character-match/start-bit detection; the IDLE+DMA circular buffer is the STM32-standard answer to variable-length frames.
Q: Can I DMA both TX and RX simultaneously? A: Yes — TX and RX use independent DMA streams/requests; just follow the mapping and keep the buffers separate.
Q: What buffer size should I use? A: At least two maximum-frame lengths (so a frame never straddles the wrap without a copy), rounded to the DMA’s natural power-of-two or your alignment requirements.
10. Conclusion
UART+DMA turns the serial port from a CPU tax into a background peripheral: set the baud from a clean PCLK, map the DMA stream correctly, run a circular buffer with the IDLE-line signal, and gate every frame on the status register. The core then attends only at message boundaries — at 2 Mbaud that is a handful of microseconds every frame, not a byte interrupt every microsecond. It is the difference between a working telemetry link and an interrupt-bound board.