The puzzle
The firmware captures 64 ADC samples into uint16_t samples[64] with DMA. It works, until someone notices that a status variable declared after the array is sometimes garbage. The channel was given sizeof(samples) as its count. What does a DMA channel count, and what else about the buffer must match the transfer?
STEP 1
Transfer size
A channel moves one item per transfer, of the size set in its control word: a byte, a half-word (16 bits) or a word (32 bits). The pico-sdk names these DMA_SIZE_8, DMA_SIZE_16 and DMA_SIZE_32; the STM32 HAL has separate peripheral and memory widths (DMA_PDATAALIGN_…, DMA_MDATAALIGN_…); Zephyr gives source and destination widths in bytes. Two things decide the size:
- the peripheral register: a data register is designed to be accessed with a particular width, and each access typically pops or pushes one FIFO entry;
- the element type of the buffer: 16-bit samples need 16-bit transfers, or the peripheral must be told to produce a narrower format. pico-examples’ ADC capture shifts each 12-bit result down to 8 bits so that it can use byte transfers into a byte buffer.
STEP 2
The count is in transfers
The RP2040’s TRANS_COUNT counts transfers, and the STM32 HAL’s data length counts data items, not bytes (1 to 65 535; its UART driver passes a count of uint8_t or uint16_t elements straight through). Zephyr’s block_size, by contrast, is in bytes. For a transfer count and transfer size :
sizeof gives bytes. For an array of elements the right count is usually the element count, sizeof(buf) / sizeof(buf[0]), with the transfer size equal to the element size.
STEP 3
Fixed and incrementing addresses
After each transfer an address either stays put or advances by the transfer size. A peripheral data register must stay put; a buffer must advance. Get it wrong and a memory-to-peripheral transfer sends the first byte over and over (read address fixed) or writes into the registers that follow the data register (write address incrementing); a peripheral-to-memory transfer with a fixed write address puts every item in the same location, so only the last survives. The RP2040’s default configuration (32-bit, read increment on, write increment off, unpaced) has the address pattern of memory to peripheral; for receiving you must change both increments. Some controllers can also decrement, useful for reversing a buffer.
STEP 4
Alignment
Transfers of a half-word or a word should use addresses that are multiples of 2 or 4. What a controller does with a misaligned address varies (a bus error, the low bits ignored, the access split in pieces), so portable code never relies on it. C already aligns an array to its element type, so uint16_t buf[8] is safe for 16-bit transfers. The dangers come from addresses you compute:
- a pointer into the middle of a byte array, such as
&packet[3], used for 32-bit transfers; - a member of a packed structure (unit 2, lesson 4);
- a buffer placed by hand in a linker section without
ALIGN(unit 4, lesson 4).
When in doubt, declare the buffer with __attribute__((aligned(4))), or stricter where lesson 4 (ring buffers) or lesson 6 (cache lines) needs it.
STEP 5
Wider transfers use fewer bus cycles
Each transfer is one read and one write, however wide. The pico-sdk describes the RP2040’s DMA as able to perform one read and one write of up to 32 bits every clock cycle, so a block of bytes needs at least
At 125 MHz, 1 KiB takes at least 2.05 µs as 256 word transfers but 8.19 µs as 1024 byte transfers, and it occupies the bus for four times as many cycles. For memory-to-memory copies of aligned buffers whose length is a multiple of 4, use words.
Controllers whose two sides can differ in width go further. An STM32F4 stream with its FIFO enabled can read half-words from a peripheral and write whole words to memory, packing two items per memory access. Without the FIFO (direct mode) the HAL applies the peripheral width to both sides. The RP2040 has one transfer size for both.
Some DMA controllers can read and write different widths. On an STM32F4 stream with its FIFO enabled, eight 16-bit reads from a peripheral can become four 32-bit writes to memory: fewer memory-bus accesses for the same data. With the FIFO disabled (direct mode) the ST HAL uses the peripheral width on both sides. The RP2040 has a single transfer size for reads and writes and does no packing.
STEP 6
Byte order
Data from a big-endian device (many sensors and network protocols send the most significant byte first; unit 2, lesson 4) arrives in the wrong byte order for a little-endian CPU. Some DMA controllers can fix it on the way: the RP2040’s BSWAP option swaps the two bytes of each half-word, or reverses the four bytes of each word, during the transfer.
STEP 7
Worked example: 64 samples, one wrong count
uint16_t samples[64] occupies 128 bytes. With 16-bit transfers and read increment off (the peripheral’s data register), write increment on:
- correct: count = 64 elements, 64 × 2 = 128 bytes, exactly the array;
- with
sizeof(samples)= 128 as the count: 128 × 2 = 256 bytes, 128 bytes past the end, over whatever the linker placed next (here, the status variable); - with 8-bit transfers and count 64: only 64 bytes, half the array, and each 8-bit read of the peripheral’s 16-bit data register returns at most part of a sample.
The fix is dma_channel_configure(ch, &c, samples, &periph_data, count_of(samples), true) with DMA_SIZE_16, where count_of is the pico-sdk’s element-count macro. A map file (unit 4, lesson 5) shows what lies after the array, which explains the symptom.
MYTHS AND FACTS
Common misconceptions
The count is the buffer size in bytes
It is in transfers on the RP2040 and STM32 (but in bytes for Zephyr’s block_size): always check which.
Any width works; the DMA will sort it out
The peripheral register expects its width, and the element type sets the width in memory.
The CPU handles unaligned access, so DMA can too
The DMA is a separate bus master with its own rules; align buffers to at least the transfer size.
Wider transfers are always possible
Only if both addresses are aligned and the peripheral accepts that width; packing different widths needs a controller that supports it.
Check yourself
Answer in your head, then open the card.
A channel uses 32-bit transfers with TRANS_COUNT = sizeof(uint32_t buf[32]). How many bytes does it write, and how far past the buffer?
sizeof = 128, so 128 transfers × 4 bytes = 512 bytes: 384 bytes past the 128-byte buffer.
What is the minimum time to copy 4 KiB memory to memory on a DMA that can do one 32-bit read and write per cycle at 125 MHz, and with byte transfers?
4096 / (4 × 125 × 10⁶) ≈ 8.2 µs with words; 4096 / 125 × 10⁶ ≈ 32.8 µs with bytes. Contention with other masters makes both longer.
A receive channel has write increment off by mistake. What does the buffer contain afterwards?
Only its first element changes: every transfer writes the same address, so it holds the last item received and the rest of the buffer is untouched.
An STM32F4 stream reads 16-bit items from a peripheral and should write 32-bit words to memory. What must be enabled?
The stream’s FIFO: in direct mode (FIFO disabled) the HAL applies the peripheral width to both sides.
Sources (6)
- Raspberry Pi Ltd, pico-sdk 1.5.1, hardware_dma/include/hardware/dma.h — enum dma_channel_transfer_size: DMA_SIZE_8, DMA_SIZE_16, DMA_SIZE_32; channel_config_set_transfer_data_size: “The read and write addresses advance by the specific amount (1/2/4 bytes) with each transfer”; dma_channel_set_trans_count: “The number of transfers (not NOT bytes …)”; channel_config_set_bswap: for halfword data “the two bytes of each halfword are swapped. For word data, the four bytes of each word are swapped”; the default configuration is 32-bit, read increment on, write increment off
- Raspberry Pi Ltd, pico-sdk 1.5.1, RP2040 register header hardware_regs/dma.h — DATA_SIZE: “Set the size of each bus transfer (byte/halfword/word). READ_ADDR and WRITE_ADDR advance by this amount (1/2/4 bytes) with each transfer”; INCR_READ: “Generally this should be disabled for peripheral-to-memory transfers”; INCR_WRITE: “Generally this should be disabled for memory-to-peripheral transfers”
- STMicroelectronics, STM32CubeF4 HAL, Src/stm32f4xx_hal_dma.c and Inc/stm32f4xx_hal_dma.h — “The FIFO is used mainly to reduce bus usage and to allow data packing/unpacking: it is possible to set different Data Sizes for the Peripheral and the Memory (ie. you can set Half-Word data size for the peripheral … and set Word data size for the Memory …)”; “When FIFO is disabled, it is not allowed to configure different Data Sizes for Source and Destination. In this case the Peripheral Data Size will be applied to both”; IS_DMA_BUFFER_SIZE accepts 1 to 0xFFFF; burst mode “is possible only if the address Increment mode is enabled”
- Zephyr Project, include/zephyr/drivers/dma.h — struct dma_block_config: block_size “Number of bytes to be transferred for this block”; source_addr_adj / dest_addr_adj “0b00 increment, 0b01 decrement, 0b10 no change”; struct dma_config: source_data_size / dest_data_size “Width of source [destination] data (in bytes)”
- STMicroelectronics, STM32CubeF4 HAL, Src/stm32f4xx_hal_uart.c — HAL_UARTEx_ReceiveToIdle_DMA(huart, pData, Size): “Size Amount of data elements (uint8_t or uint16_t) to be received”; with 9-bit words and no parity “the received data is handled as a set of uint16_t. In this case, Size must indicate the number of uint16_t available through pData”; UART_Start_Receive_DMA() passes Size unchanged to HAL_DMA_Start_IT()
- Raspberry Pi Ltd, pico-examples (tag sdk-1.5.1), adc/dma_capture/dma_capture.c — “Configure the ADC to right-shift samples to 8 bits of significance, so we can DMA into a byte buffer”, then DMA_SIZE_8 transfers into uint8_t capture_buf[1000]