UNIT 12 · LESSON 3 OF 6

Transfer Widths, Alignment, and Address Increment

What does a DMA channel count, and what else about the buffer must match the transfer?

INTERACTIVETransfer size, count and address increment
Bytes written by a DMA channel compared with the destination buffer0x2000_01000x2000_01100x2000_01200x2000_01300x2000_01408 transfers × 2 bytes = 16 bytes: exactly the buffer.
Bytes written by a DMA channel compared with the destination buffer…100…110…120…130…1408 transfers × 2 bytes = 16 bytes: exactly thebuffer.

Try this

Transfer size
Transfer count
Buffer address
Exactly fills the buffer.

A buffer uint16_t buf[8] (16 bytes) receives data from a peripheral. The channel writes count transfers of the chosen size, and its write address advances by the transfer size after each one. The count is in transfers, not bytes: pass sizeof(buf) with 16-bit transfers and the channel writes twice as far as the buffer goes. Grey outline: the buffer; teal: bytes written inside it; orange: bytes written outside it.

What you will be able to do
  • Choose a transfer size that matches the peripheral register and the buffer’s element type.
  • Compute the transfer count for a buffer and predict the bytes written when the count is given in the wrong unit.
  • Explain why DMA buffers must be aligned to at least the transfer size, and how to guarantee it.
  • Compute the minimum bus time for a block with byte, half-word and word transfers.
  • Describe FIFO packing between different peripheral and memory widths, and when it is not available.
Before you start
  • A DMA channel’s registers and transfer directions (lessons 1 and 2).
  • Pointers, arrays and sizeof (unit 2, lesson 3); alignment and data layout (unit 2, lesson 4).
Steps in this lesson
  1. Transfer size
  2. The count is in transfers
  3. Fixed and incrementing addresses
  4. Alignment
  5. Wider transfers use fewer bus cycles
  6. Byte order
  7. Worked example: 64 samples, one wrong count
  8. Common misconceptions

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 NN and transfer size ss:

bytes moved=N×s\text{bytes moved} = N \times s

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.

↑ This step uses the figure at the top of the page.

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 BB bytes needs at least

tmin⁡=Bs×fclkt_{\min} = \frac{B}{s \times f_{\text{clk}}}

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.

INTERACTIVEDifferent widths on each side
Peripheral-side and memory-side bus accesses with FIFO packingperipheral side: 8 reads of 2 bytesmemory side: 4 writes of 4 bytesThe FIFO packs 2 peripheral items into each memory write: 16 bytes in 4 memoryaccesses instead of 8.
Peripheral-side and memory-side bus accesses with FIFO packingperipheral side: 8 reads of 2 bytesmemory side: 4 writes of 4 bytesThe FIFO packs 2 peripheral items into each memorywrite: 16 bytes in 4 memory accesses instead of 8.
Peripheral width
Memory width
8 peripheral reads, 4 memory writes.

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)
  1. 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
  2. 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”
  3. 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”
  4. 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)”
  5. 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()
  6. 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]