The puzzle
An ADC produces a sample every 2 µs into a small FIFO, and a DMA channel is set to copy 1000 of them into a buffer. The first attempt finishes almost instantly and the buffer holds nothing useful; the second works, until the display update starts and a few samples go missing every frame. A DMA channel copies as fast as it is allowed to. Who decides when it is allowed?
STEP 1
Where the data comes from and goes to
Every transfer has a source and a destination, each either memory or a peripheral register. The combination decides which addresses increment:
- memory to memory: both addresses increment; a fast
memcpythat runs beside the CPU; - memory to peripheral: the read address walks through a buffer, the write address stays on the peripheral’s data register (a UART or SPI transmit FIFO, a PWM compare register);
- peripheral to memory: the read address stays on the peripheral’s data register (a receive FIFO, the ADC result FIFO), the write address walks through a buffer.
Zephyr’s DMA API also names peripheral-to-peripheral transfers, and some controllers can decrement an address instead of incrementing it. A peripheral’s data register is a single address, often in front of a FIFO: reading it repeatedly usually returns successive entries.
The same channel hardware copies memory to memory, memory to a peripheral, or a peripheral to memory. What changes is which address increments (a peripheral’s data register stays at one address) and what paces the transfers (a peripheral’s data request, or nothing). The settings are shown with the pico-sdk’s names and DREQ numbers, the STM32 HAL’s direction constant and Zephyr’s channel direction.
STEP 2
Who says “now”: transfer requests
A peripheral knows when it can accept or supply data; the DMA controller does not. So a peripheral raises a data request (DREQ) when it has data to read or room to write, and a channel set to that request transfers only as fast as the peripheral asks. On the RP2040 each channel’s TREQ_SEL field chooses its pacing: 0x00–0x3a select a DREQ (for example 0x14 for UART0 transmit, 0x15 for UART0 receive, 0x24 for the ADC), 0x3b–0x3e select one of the DMA’s pacing timers, and 0x3f is a permanent request for unpaced transfers. On an STM32F4 the request is chosen by the stream and its channel number, from a table in the reference manual; in Zephyr it is the dma_slot.
↑ This step uses the figure at the top of the page.
Pick the wrong pacing and the channel runs at full speed regardless: an unpaced read from a receive FIFO mostly finds it empty (the RP2040 ADC records that as an underflow in its UNDER flag), and an unpaced write to a transmit FIFO overfills it. Unpaced (DREQ_FORCE) is for targets that are always ready: memory, or another channel’s registers.
STEP 3
How much delay a FIFO absorbs
Pacing does not make a channel instantaneous. Another master may own the bus, a higher-priority channel may be scheduled first, or the channel may be waiting to be re-armed. Meanwhile the peripheral keeps producing, and its FIFO holds the backlog. With a FIFO of entries and a new item every :
The RP2040’s ADC FIFO holds 4 samples, and “if a conversion is completed and the FIFO is full, the result is dropped”, setting the OVER flag. At 500 ksps (a sample every 2 µs) that is about 8 µs of tolerance; at 250 ksps about 16 µs. The RP2040’s DMA tracks, for each channel, how many accesses it expects it can make on the peripheral “without overflow/underflow”: the handshake is a running count, not one request per transfer.
STEP 4
Pacing from a timer
Some data must move at a steady rate without a peripheral asking: waveform samples to a PWM compare register, words to a DAC, a display line every so often. Two common sources:
- a peripheral timer’s update event can raise a DMA request (unit 8, lesson 2), and on the RP2040 each PWM slice’s wrap is a DREQ (
DREQ_PWM_WRAP0is 0x18); - the RP2040 DMA’s own pacing timers assert a request at a fraction X/Y of the system clock, set with
dma_timer_set_fraction(timer, X, Y). At 125 MHz, X/Y = 1/2500 gives 50 000 requests a second.
STEP 5
Starting, chaining and finishing
A channel starts when one of its trigger registers is written (or several at once through MULTI_CHAN_TRIGGER), or when another channel finishes and names it in its CHAIN_TO field. Chaining lets channels run one after another with no CPU involvement; pico-examples’ control_blocks uses one channel to reprogram another from a list in memory, sending a sequence of strings to a UART.
A finished channel stops and raises its interrupt flag. A bus error (for example, an address that no slave answers) also stops it: on the RP2040 the channel “halts when it encounters any bus error, and always raises its channel IRQ flag”, with READ_ERROR or WRITE_ERROR set; the STM32 HAL reports a transfer error through XferErrorCallback. Aborting a running channel needs care: on the RP2040, dma_channel_abort() waits until in-flight transfers have drained, and erratum RP2040-E13 means an abort can be followed by a spurious completion interrupt, which the SDK documentation tells you to expect and ignore.
STEP 6
Worked example: capturing 1000 ADC samples
pico-examples’ dma_capture runs the ADC at 500 ksps and shifts each result to 8 bits so it fits a byte buffer. The channel reads the ADC FIFO (read increment off), writes capture_buf[] (write increment on), moves 8-bit items, and is paced by DREQ_ADC:
channel_config_set_transfer_data_size(&cfg, DMA_SIZE_8);
channel_config_set_read_increment(&cfg, false);
channel_config_set_write_increment(&cfg, true);
channel_config_set_dreq(&cfg, DREQ_ADC);
dma_channel_configure(dma_chan, &cfg, capture_buf, &adc_hw->fifo, CAPTURE_DEPTH, true);
The 1000 transfers take 1000 × 2 µs = 2 ms, set by the ADC, not the DMA. With DREQ_FORCE instead, the channel would make its 1000 transfers as fast as the bus allows, microseconds rather than milliseconds, reading an empty FIFO almost every time. And if something holds the channel off for more than about 4 × 2 = 8 µs while the ADC runs, samples are dropped: check the ADC’s OVER flag after a capture, give the capturing channel high priority among the DMA channels, or raise the DMA’s bus priority (unit 3, lesson 3).
MYTHS AND FACTS
Common misconceptions
Unpaced is faster, so it is better
It suits targets that are always ready, such as memory; a peripheral must pace its own data.
With DREQ pacing no data can be lost
The FIFO covers only of delay; beyond that the peripheral overflows (or, for transmit, underruns).
The DMA always reads memory
It reads and writes addresses. A peripheral register, another channel’s control registers (as in control_blocks) or memory are all just addresses to it.
After an abort, no completion interrupt can arrive
On the RP2040 one can (erratum RP2040-E13); disable the channel’s interrupt, abort, clear its flag, then re-enable it (the SDK’s workaround).
Check yourself
Answer in your head, then open the card.
A channel sends a buffer to UART0 on an RP2040. What are its read increment, write increment and TREQ_SEL?
Read increment on, write increment off (the UART data register), and TREQ_SEL = DREQ_UART0_TX (0x14), so it writes only when the transmit FIFO has room.
A peripheral has an 8-entry receive FIFO and produces an item every 5 µs. How long can the DMA channel be held off without losing data?
About 8 × 5 = 40 µs.
A DMA timer is set with X = 1 and Y = 1250 on a 125 MHz system clock. How often does it request a transfer?
125 × 10⁶ × 1 / 1250 = 100 000 times a second, every 10 µs.
A memory-to-peripheral transfer uses DREQ_FORCE by mistake. What happens?
The channel writes as fast as the bus allows, much faster than the peripheral drains its FIFO, so most of the data is lost; what a full transmit FIFO does with extra writes depends on the peripheral.
Sources (6)
- Raspberry Pi Ltd, pico-sdk 1.5.1, hardware_regs/dreq.h and hardware_dma/include/hardware/dma.h — DREQ numbers: DREQ_SPI0_TX 0x10, DREQ_UART0_TX 0x14, DREQ_UART0_RX 0x15, DREQ_PWM_WRAP0 0x18, DREQ_ADC 0x24; dma.h: channel_config_set_dreq “0x0 to 0x3a -> select DREQ n as TREQ, 0x3b -> Select Timer 0 as TREQ … 0x3f -> Permanent request, for unpaced transfers”; dma_timer_set_fraction: “The timer will run at the system_clock_freq * numerator / denominator”; dma_channel_abort() and erratum RP2040-E13 (a spurious completion interrupt after an abort)
- Raspberry Pi Ltd, pico-sdk 1.5.1, RP2040 register header hardware_regs/dma.h — TREQ_SEL: “The channel uses the transfer request signal to pace its data transfer rate”; CH0_DBG_CTDREQ: the channel’s DREQ counter, “how many accesses the DMA expects it can perform on the peripheral without overflow/underflow”; CHAIN_TO: “When this channel completes, it will trigger the channel indicated by CHAIN_TO”; AHB_ERROR: “The channel halts when it encounters any bus error, and always raises its channel IRQ flag”; MULTI_CHAN_TRIGGER starts several channels at once
- Raspberry Pi Ltd, pico-examples (tag sdk-1.5.1), adc/dma_capture/dma_capture.c and dma/control_blocks/control_blocks.c — dma_capture: ADC “in free-running capture mode at 0.5 Msps”, “Each conversion takes 96 cycles … timed by the 48 MHz ADC clock”; 8-bit transfers, “Reading from constant address, writing to incrementing byte addresses”, “Pace transfers based on availability of ADC samples” with DREQ_ADC; control_blocks: a data channel paced by uart_get_dreq(uart_default, true) writing to the UART data register
- Raspberry Pi Ltd, pico-sdk 1.5.1, hardware_adc/include/hardware/adc.h and hardware_regs/adc.h — adc_fifo_setup: “FIFO is 4 samples long, if a conversion is completed and the FIFO is full, the result is dropped”, with dreq_en and dreq_thresh; register FCS: OVER “1 if the FIFO has been overflowed”, UNDER “1 if the FIFO has been underflowed”, both write 1 to clear
- STMicroelectronics, STM32CubeF4 HAL, Inc/stm32f4xx_hal_dma.h and Src/stm32f4xx_hal_dma.c — DMA_InitTypeDef: Channel (“the channel used for the specified stream”), Direction (memory to peripheral, memory to memory, peripheral to memory), PeriphInc, MemInc; the driver notes: “refer to Reference manual for connection between peripherals and DMA requests”; HAL_DMA_IRQHandler handles transfer error (TE), FIFO error (FE) and direct mode error (DME) flags
- Zephyr Project, include/zephyr/drivers/dma.h — struct dma_config: dma_slot “Which peripheral and direction, HW specific”; channel_direction memory to memory, memory to peripheral, peripheral to memory, peripheral to peripheral; source_addr_adj / dest_addr_adj: increment, decrement or no change