Embedded HandbookApps
All sections › II. Core and system mechanisms
STM32F103, STM32F407 · level intermediate · 23 min read
Open in the interactive handbook

9. Direct memory access (DMA)

While the processor moves bytes from an array into a peripheral register, it does nothing useful. DMA is a separate controller that does this by itself: the core sets where from, where to and how much, and is then free.


9.1 What it gives

Data transfer through the core and through DMA

The difference is not only in speed. When transferring through the core, every byte costs a read, a check of the ready flag and a write — and all this in a loop that cannot be left without losing data. When transferring through DMA the interrupt comes once, when the whole buffer has gone out.

Typical tasks where things are bad without DMA:

Terms used in the documentation

Term Meaning
direct memory access transfers done by a controller without the core
channel / stream the DMA unit on F1 / on F4
transfer complete the whole transfer is done
half transfer half of the buffer has been transferred
circular mode the transfer restarts by itself in a circle
memory increment the memory address grows after each element
peripheral increment the peripheral address grows after each element
source / destination where the data is taken from / put to
burst transfer several accesses in a row without arbitration
FIFO threshold the fill level of the FIFO buffer that triggers a transfer
double buffer mode switching between two memory buffers
arbiter the block that decides priority between requests
update DMA request (UDE) a DMA request on the timer update event
pacing timer a timer that sets the pace of the transfer
repetition counter (RCR) the counter of periods between update events

9.2 Channels on F1, streams on F4

This is one of the most noticeable differences between the platforms. The code does not port at all: both the register names and the model itself differ.

STM32F1 STM32F407
unit channel stream
how many DMA1 — 7 channels, DMA2 — 5 DMA1 and DMA2 — 8 streams each
source selection hard-wired: each channel has its own list stream + selection of one of 8 channels
FIFO buffer no yes, 4 words
burst transfer no yes
double buffering no yes
memory ↔ memory DMA1 and DMA2 can only DMA2
registers CCRx, CNDTRx, CPARx, CMARx SxCR, SxNDTR, SxPAR, SxM0AR, SxFCR
flags one ISR register for all channels LISR for streams 0–3, HISR for 4–7

What this means in practice. On F1 you do not choose which channel serves a peripheral: USART1_TX is always channel 4 of the DMA1 controller, and nothing else. The mapping table is in the reference manual, and you have to keep it at hand. Hence the conflicts too: if two peripheral blocks need the same channel, they cannot work simultaneously.

F4 is more flexible: you choose the stream, and inside it the CHSEL field specifies whose request it serves. The same USART1_TX is stream 7 of the DMA2 controller, channel 4.

Where DMA can reach at all. There are several masters on the bus: the core, both DMA controllers, Ethernet, USB HS. They are connected to memory and peripherals by the bus matrix — a set of paths by which a master reaches a slave, and an arbiter that decides who goes first when two ask for the same one. As long as the masters go to different slaves, they work truly simultaneously: DMA moves bytes from the UART to memory, while at the same time the core executes code from Flash.

The masters, the bus matrix and the slaves of F407: two APB bridges and the count of blocks on each bus

The peripherals hang not on one bus but on five. STM32F407 has 82 blocks — according to the chip description (SVD): 24 on AHB1 (ports, RCC, both DMAs, Ethernet), 8 on AHB2, one on AHB3 and the rest behind two bridges — 29 on APB1 and 20 on APB2. A bridge is needed because APB is clocked more slowly: with the core at 168 MHz APB1 runs at 42 MHz, APB2 at 84 (section 6.6). An access through a bridge costs more: the request goes through an adapter between buses with different frequencies, and the response does not come in the same cycle as with AHB1.

Hence the table row "memory ↔ memory — only DMA2", which otherwise looks arbitrary. The controller takes the source of such a transfer through the peripheral port, and on DMA1 this port is not connected to the matrix — it has no way to reach memory. DMA2 has both ports in the matrix, so it reads from one place in memory and writes to another.

⚠️ Two masters to one slave is a queue, not "simultaneously". If DMA and the core access SRAM at the same time, the arbiter will let one through, and the other will wait a cycle. By eye this is unnoticeable, but when measuring handler time a spread appears that is not in the cycle-count calculation (section 73).


9.3 What has to be set for a transfer

Regardless of the platform, the configuration comes down to five things:

What F1 F4
peripheral address CPAR SxPAR
memory address CMAR SxM0AR
number of elements CNDTR SxNDTR
direction the DIR bit the DIR field (two bits)
element size and increment PSIZE, MSIZE, PINC, MINC the same names in SxCR

Increment is the key setting that is confused most often. The memory address usually has to grow (MINC on), the peripheral address does not: the data register is always the same. Enable PINC by mistake — and DMA will go and write to the neighbouring peripheral registers.

Element size is set separately for memory and for the peripheral, and they may differ: for example, storing 16-bit ADC samples into a 16-bit array. A size mismatch is a source of mysterious "shifted" data.

⚠️ The transfer counter is 16-bit. A maximum of 65535 elements per configuration — on both F1 and F4. For more you need circular mode with processing by halves of the buffer.


9.4 Modes

Normal. It transferred NDTR elements, switched off, raised the completion flag. For a repeated transfer the channel has to be reconfigured.

Circular (CIRC on F1, CIRC on F4). Having counted to the end, the counter reloads itself and the transfer goes round in a circle. Indispensable for continuous ADC sampling and audio output: configure it once — it works forever.

Two interrupts arrive here: half buffer (HTIF) and end of buffer (TCIF). The technique is this: while DMA fills the second half, you process the first, and vice versa. The data is not overwritten under your hands as long as processing one half fits into the time of filling the other: at 48 kHz and a buffer of 2 × 256 samples this is 5.3 ms. If you do not make it, DMA silently writes over it. This is classic "poor man's" double buffering, available on F1 too.

Circular DMA: NDTR as a sawtooth from 512 to 0, HTIF at 256 and TCIF at 0; while DMA writes one half of the buffer, the other is processed, and there are 5.33 ms for that at 48 kHz

Memory to memory. DMA does not necessarily copy faster than memcpy, but the core is free meanwhile: it computes, serves interrupts or sleeps. F4 has a limitation: DMA1 cannot do this at all, only DMA2.

Peripheral to peripheral. There is no mode with that name. On F4 the direction is set by the DIR field in SxCR — two bits, with three values in use: "peripheral → memory" (0), "memory → peripheral" (1), "memory → memory" (2); on F1 the direction is the DIR bit in CCRx, and "memory to memory" is a separate MEM2MEM bit. There is no fourth option in the list, and this is not an omission: it is not needed.

A transfer between two blocks is done as an ordinary "peripheral → memory":

What to set Value
direction периферия → память
address on the peripheral side the data register of the source
address on the memory side the data register of the destination
address increment off on both sides: PINC = 0, MINC = 0

The controller does not care what is behind an address: for it both are just points on the bus. Hence the increment requirement: a block has one register, there is nowhere to move the pointer, and an enabled increment will move on to the neighbouring register and spoil the configuration of the destination.

This is how, for example, ADC samples are moved straight into the DAC register, or bytes received over SPI into the USART transmitter — without a single processor instruction.

⚠️ Only one side gives the request. A channel is bound to one block — the one whose request it listens to. The other must always be ready: there is no "wait, I am busy" mechanism here, and if the destination did not keep up, the data is silently lost. That is why blocks with obviously different speeds are connected this way: a slow source — a fast destination.

⚠️ Both addresses must be visible from the bus matrix. The CCM memory of the senior F4s is connected straight to the core and not to the matrix (section 2) — it is unsuitable for DMA either as a source or as a destination.


9.5 🔧 Recipe: UART transmission through DMA

STM32F103. USART1_TX is hard-wired to channel 4 of DMA1.

static char tx_buf[64];

void DMA_USART1_TX_Init(void)
{
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;          /* DMA на шине AHB */

    DMA1_Channel4->CCR &= ~DMA_CCR4_EN;        /* настраивать только выключенным */

    DMA1_Channel4->CPAR = (uint32_t)&USART1->DR;
    DMA1_Channel4->CMAR = (uint32_t)tx_buf;
    DMA1_Channel4->CNDTR = 0;

    DMA1_Channel4->CCR = DMA_CCR4_DIR          /* из памяти в периферию */
                       | DMA_CCR4_MINC         /* адрес памяти растёт */
                       | DMA_CCR4_TCIE         /* прерывание по завершении */
                       | DMA_CCR4_PL_1;        /* высокий приоритет */

    NVIC_SetPriority(DMA1_Channel4_IRQn, 6);
    NVIC_EnableIRQ(DMA1_Channel4_IRQn);

    USART1->CR3 |= USART_CR3_DMAT;             /* разрешить запросы от USART */
}

void DMA_USART1_Send(const char *data, uint16_t len)
{
    if (len > sizeof(tx_buf)) {
        len = sizeof(tx_buf);
    }
    while (DMA1_Channel4->CCR & DMA_CCR4_EN) { }   /* прошлая передача ещё идёт */
    memcpy(tx_buf, data, len);

    DMA1_Channel4->CCR &= ~DMA_CCR4_EN;
    DMA1_Channel4->CNDTR = len;
    DMA1_Channel4->CCR |= DMA_CCR4_EN;
}

void DMA1_Channel4_IRQHandler(void)
{
    if (DMA1->ISR & DMA_ISR_TCIF4) {
        DMA1->IFCR = DMA_IFCR_CTCIF4;          /* сброс — в отдельном регистре */
        DMA1_Channel4->CCR &= ~DMA_CCR4_EN;
    }
}

While DMA is sending the buffer, you must not write to it: that is why DMA_USART1_Send first waits until the TCIF4 handler switches the channel off.

STM32F407. The same USART1_TX is stream 7 of the DMA2 controller, channel 4.

void DMA_USART1_TX_Init(void)
{
    RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN;

    /* Выключаем поток и ЖДЁМ подтверждения: у F4 бит EN сбрасывается не сразу. */
    DMA2_Stream7->CR &= ~DMA_SxCR_EN;
    while (DMA2_Stream7->CR & DMA_SxCR_EN) { }

    /* Сбрасываем флаги прошлой передачи — иначе новая может не пойти. */
    DMA2->HIFCR = DMA_HIFCR_CTCIF7 | DMA_HIFCR_CHTIF7 | DMA_HIFCR_CTEIF7
                | DMA_HIFCR_CDMEIF7 | DMA_HIFCR_CFEIF7;

    DMA2_Stream7->PAR  = (uint32_t)&USART1->DR;
    DMA2_Stream7->M0AR = (uint32_t)tx_buf;
    DMA2_Stream7->NDTR = 0;

    DMA2_Stream7->CR = (4u << DMA_SxCR_CHSEL_Pos)   /* канал 4 = USART1_TX */
                     | DMA_SxCR_DIR_0               /* 01: память -> периферия */
                     | DMA_SxCR_MINC
                     | DMA_SxCR_TCIE
                     | DMA_SxCR_PL_1;

    DMA2_Stream7->FCR &= ~DMA_SxFCR_DMDIS;          /* прямой режим, без FIFO */

    NVIC_SetPriority(DMA2_Stream7_IRQn, 6);
    NVIC_EnableIRQ(DMA2_Stream7_IRQn);

    USART1->CR3 |= USART_CR3_DMAT;
}

Two differences worth remembering: waiting for EN to clear and mandatory clearing of the flags before a new transfer. F1 requires neither, and this is exactly where people stumble when porting.


9.6 The FIFO on F4

Each stream has a 4-word buffer. It lets small portions be collected into large bus accesses: DMA accumulates data and writes it in a batch.

DMA2_Stream7->FCR |= DMA_SxFCR_DMDIS;                 /* включить FIFO */
DMA2_Stream7->FCR |= DMA_SxFCR_FTH;                   /* порог — полный буфер */

When it is needed: at high speeds and with different widths of source and destination — for example, reading memory in words and giving it to the peripheral in bytes.

When it is not needed: for UART at ordinary speeds direct mode (DMDIS = 0) is simpler and more reliable. A badly chosen threshold gives the FEIF error and silently spoils the transfer.

Bursts: several words per bus access

With the FIFO enabled a stream can deliver what it has accumulated as a burst — 4, 8 or 16 accesses in a row, without arbitration between them (MBURST for memory, PBURST for the peripheral). In direct mode bursts are unavailable: the hardware forces single accesses. The rules whose violation is paid for with a bus error that is not visible in the DMA registers:

Rule Where it comes from
burst size × data width no larger than the FIFO threshold: with a ¼ threshold and bytes — only a burst of 4, with a full FIFO and words — also only of 4 the table of allowed combinations in the controller description
the ¾ threshold is forbidden if the peripheral burst exactly fills the FIFO (4 words) the same place
a burst must not cross a 1 KB address boundary an AHB protocol requirement: the minimum area of one slave is 1 KB
accesses inside a burst are aligned to their width the same place

HAL checks the first rule itself: HAL_DMA_Init with an incompatible threshold–burst pair returns the error HAL_DMA_ERROR_PARAM without touching the hardware. Nobody checks the 1 KB boundary — it is on the conscience of whoever chooses the buffer address.

The arbiter: who goes first

Each stream has four priority levels in PL (DMA_PRIORITY_LOW … VERY_HIGH in HAL). If two streams have the same level, the stream with the lower number wins: stream 2 goes before stream 4. Hence the practice: give the most demanding source both a high level and a low stream number, if the channel mapping allows it.


9.7 🔧 PWM from a table: an LED breathes without the processor

DMA writes not only to the data registers of interfaces but to any peripheral register — for example, to a timer's CCR1. This way the PWM duty cycle is taken from a table: the LED smoothly brightens and dims, and after configuration the processor sleeps.

There are two timers. The first produces 1 kHz PWM, the second sets the pace: 50 times a second it sends a DMA request, and DMA moves the next value from the table in a circle into the first one's CCR1. 100 values at this pace — an inhale and an exhale in 2 s.

F103 F407
PWM TIM3, channel 1: PA6 TIM4, channel 1: PD12 — the green LED of the Discovery
pace TIM4 TIM3
request TIM4_UP — channel 7 of DMA1 TIM3_UP — stream 2, channel 5 of DMA1

The table is round(1000 · pow((1 − cos(2π·i / 100)) / 2, 2.2)): a smooth inhale along a cosine, raised to the power 2.2 for gamma (57.3), so that the eye sees it as uniform. The peak 1000 = ARR + 1 — full brightness at the 50th value. 11 values at the junction of exhale and inhale are zeros: 0.22 s of darkness between breaths.

static void dma_init(void)
{
#if defined(STM32F10X_MD)
    DMA1_Channel7->CCR   = 0;                                  /* настраивать выключенным */
    DMA1_Channel7->CPAR  = (uint32_t)&PWM->CCR1;               /* куда: регистр другого таймера */
    DMA1_Channel7->CMAR  = (uint32_t)breath;
    DMA1_Channel7->CNDTR = STEPS;
    DMA1_Channel7->CCR   = DMA_CCR7_DIR | DMA_CCR7_MINC | DMA_CCR7_CIRC   /* из памяти, по кругу */
                         | DMA_CCR7_PSIZE_0 | DMA_CCR7_MSIZE_0            /* по 16 бит */
                         | DMA_CCR7_EN;
#elif defined(STM32F407xx)
    DMA1_Stream2->CR &= ~DMA_SxCR_EN;
    while (DMA1_Stream2->CR & DMA_SxCR_EN) { }
    DMA1->LIFCR = DMA_LIFCR_CTCIF2 | DMA_LIFCR_CHTIF2 | DMA_LIFCR_CTEIF2
                | DMA_LIFCR_CDMEIF2 | DMA_LIFCR_CFEIF2;
    DMA1_Stream2->PAR  = (uint32_t)&PWM->CCR1;
    DMA1_Stream2->M0AR = (uint32_t)breath;
    DMA1_Stream2->NDTR = STEPS;
    DMA1_Stream2->CR   = (5u << DMA_SxCR_CHSEL_Pos)            /* канал 5 = TIM3_UP */
                       | DMA_SxCR_DIR_0 | DMA_SxCR_MINC | DMA_SxCR_CIRC
                       | DMA_SxCR_PSIZE_0 | DMA_SxCR_MSIZE_0
                       | DMA_SxCR_EN;
#endif
}

void breath_init(void)
{
    clocks_and_pins();
    dma_init();

    /* ШИМ 1 кГц: новое CCR1 вступает в силу с началом периода */
    PWM->PSC   = PWM_PSC;
    PWM->ARR   = 999u;
    PWM->CCMR1 = TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1PE;   /* ШИМ 1, предзагрузка */
    PWM->CCER  = TIM_CCER_CC1E;
    PWM->CR1   = TIM_CR1_ARPE;
    PWM->EGR   = TIM_EGR_UG;
    PWM->CR1  |= TIM_CR1_CEN;

    /* темп: 50 раз в секунду запрос DMA, прерывания нет */
    PACE->PSC  = PACE_PSC;                  /* тик 0,1 мс */
    PACE->ARR  = 199u;                      /* 200 тиков — 20 мс */
    PACE->DIER = TIM_DIER_UDE;
    PACE->CR1  = TIM_CR1_CEN;
}

int main(void)
{
    breath_init();
    for (;;)
        __WFI();                            /* спит: таймеры и DMA работают и в Sleep */
}

Why the brightness does not jerk. Thanks to the OC1PE preload DMA writes to the shadow register, and the PWM picks up the new value at the start of the next period — there are no shortened pulses (13.4).

It breathes in sleep too. In Sleep the core stands still, but the timers and DMA work (10.2), so the whole main loop is one __WFI(). In Stop — no: the clocks are stopped there.

And if with an interrupt? The same transfer in a handler is 18 instructions after armclang with -O2. Interrupt entry and exit cost 12 cycles each on Cortex-M3 and M4; counting generously two cycles per instruction, we get 24 + 2 × 18 = 60 cycles. At 50 updates per second this is nothing, and even at 10 000 updates per second at 48 MHz it is 10 000 × 60 / 48 000 000 = 1.25 % of the processor. The gain of DMA here is not in percent: the update does not wait for other interrupts — no delays, no jitter — and runs while the core sleeps.

One timer instead of two. TIM1 and TIM8 have a repetition counter RCR: the update event comes once every RCR + 1 periods. With RCR = 19 a 1 kHz PWM itself requests DMA 1000 / 20 = 50 times a second. Just do not forget MOE (13.5).


9.8 F0 and G0: channels as on F1, DMAMUX on G0

DMA on F0 and G0 is built like F1's, not like F4's: five or seven channels (five — on F030, F031, G030, G031; seven — on F072, F091, G071), each with CCR, CNDTR, CPAR, CMAR, and shared ISR and IFCR. There are no streams and no FIFO. Recipe 9.5 ports almost word for word, but three small things in the names catch you when porting — and one big one on G0.

The small things. The clock bit on F0 is called DMAEN; the name DMA1EN is kept in the header as an obsolete alias, and in new code it is better not to mix it with the main one. The CCR bits have no channel number: DMA_CCR_DIR, DMA_CCR_MINC, DMA_CCR_TCIE; code with DMA_CCR4_DIR from the F1 recipe does not build — use of undeclared identifier 'DMA_CCR4_DIR'. And the USART data register is TDR, not DR (section 16.9). The interrupts are grouped: on F0 and G0 channel 1 has its own, channels 2 and 3 share one (DMA1_Channel2_3_IRQn), channels 4–7 share one among four; on G0 the DMAMUX overrun is added to the last one (DMA1_Ch4_7_DMAMUX1_OVR_IRQn). In the handler you first look at ISR to see whose channel it is:

void DMA1_Channel2_3_IRQHandler(void)
{
    if (DMA1->ISR & DMA_ISR_TCIF2) {
        DMA1->IFCR = DMA_IFCR_CTCIF2;
    }
}

Request mapping on F0 is hard-wired, as on F1, only the table is different: USART1_TX — channel 2, USART1_RX — channel 3; the USART1TX_DMA_RMP bit in SYSCFG->CFGR1 moves transmission to channel 4, USART1RX_DMA_RMP — reception to channel 5. The channel configuration is as in 9.5:

    RCC->AHBENR |= RCC_AHBENR_DMAEN;            /* у F0 бит зовётся DMAEN; DMA1EN — устаревший псевдоним */
    DMA1_Channel2->CCR &= ~DMA_CCR_EN;          /* настраивать только выключенным */
    DMA1_Channel2->CPAR = (uint32_t)&USART1->TDR;   /* у нового USART — TDR, не DR */
    DMA1_Channel2->CMAR = (uint32_t)tx_buf;
    DMA1_Channel2->CNDTR = 0;
    DMA1_Channel2->CCR = DMA_CCR_DIR            /* имена битов без номера канала: DMA_CCR_DIR, не DMA_CCR4_DIR */
                       | DMA_CCR_MINC
                       | DMA_CCR_TCIE
                       | DMA_CCR_PL_1;

G0 has no mapping at all — it has DMAMUX. Any request can be routed to any channel: each DMA channel has its own multiplexer channel with a single CCR register, into which the request number is written. The numbers are in the LL header stm32g0xx_ll_dmamux.h: USART1_TX — 51, USART2_TX — 53, ADC1 — 5, memory-to-memory — 0. The numbering is offset: DMA channel 1 is DMAMUX channel 0. Without a write to DMAMUX the channel is configured correctly, enabled — and silent: the request does not reach it. This is the most frequent mistake when porting from F0 and F1:

    /* канал 1 DMA1 — это канал 0 DMAMUX: нумерация со сдвигом на единицу */
    DMAMUX1_Channel0->CCR = LL_DMAMUX_REQ_USART2_TX;    /* без этой строки канал молчит */
    DMA1_Channel1->CCR &= ~DMA_CCR_EN;
    DMA1_Channel1->CPAR = (uint32_t)&USART2->TDR;

After that everything is as on F0: CMAR, CNDTR, CCR, DMAT in the USART. On the other hand, G0 has no mapping table with its conflicts of "two needed requests on one channel" — any peripheral gets any free channel.


9.9 Pitfalls

Configuring with the channel enabled. Writes to the address and counter registers with EN set are ignored. On F4 it is not enough to clear the bit — you must wait until the hardware confirms it.

Uncleared flags. On F4 a new transfer may not start if flags of the previous one remain. Clear them before the start, not after.

A forgotten enable on the peripheral side. DMA itself is configured correctly, but USART_CR3_DMAT, ADC_CR2_DMA or SPI_CR2_TXDMAEN are not enabled — there are simply no requests, and the transfer does not go. No diagnostics at all.

Through HAL: the exchange is "stuck" in BUSY. HAL_UART_Transmit_DMA returned HAL_OK, the data went out, but the next call answers HAL_BUSY. In HAL the state returns to READY not at the end of DMA but on the TC interrupt of the USART itself: the DMA handler only clears DMAT and sets TCIE. You need both interrupts in the NVIC — the DMA stream's and the USART's; the absence of either of them leaves gState = BUSY_TX forever. Reception in Normal mode after RxCpltCallback does not restart by itself: HAL clears DMAR and waits for a new HAL_UART_Receive_DMA; only Circular continues without a call.

Through HAL: the I²C buffer interrupt with DMA. In DMA mode HAL enables only ITEVTEN and ITERREN; it leaves ITBUFEN to the interrupt modes without DMA. An ITBUFEN set by hand will make the event handler parse the bytes that DMA is already moving.

A buffer in CCM. The senior F4s have 64 KB of fast memory connected straight to the core. It is inaccessible to the DMA controller: a buffer placed there will not be transferred — the stream immediately switches off with the error flag TEIFx. If this flag is not looked at, there will be silence. Keep DMA buffers in ordinary SRAM.

A local buffer. An array declared inside a function lives on the stack and disappears on exit. DMA will keep writing to its address even after other data has appeared there. Make DMA buffers static or global.

Missing volatile. The data that DMA brings changes outside of the code — as with interrupts. Without volatile the compiler is entitled to cache the value (the analysis with assembly is in section 3).

The data cache on F7 and H7. On Cortex-M7 the core works with memory through a cache, and DMA works directly with RAM, and volatile does not help here: the core reads a stale copy of the received buffer from the cache, and what the core wrote may not reach RAM by the start of the transfer. Before transmission the buffer is pushed out to RAM (SCB_CleanDCache_by_Addr), after reception it is discarded from the cache (SCB_InvalidateDCache_by_Addr), the buffer itself is aligned and made a multiple of 32 bytes — a cache line; or the buffers are put into a region that the MPU marks as non-cacheable (section 124.6).

Channel conflict on F1. Two peripheral blocks mapped to the same channel will not work simultaneously. Check against the mapping table beforehand, not after "SPI broke the ADC".

Alignment. If an element is 16- or 32-bit, the buffer addresses must be aligned accordingly. A uint8_t array cast to uint16_t* may give a transfer error.

DMA to a timer is silent. The update request is enabled by the UDE bit in the DIER of the pacing timer. Without it the DMA channel is configured correctly, but there are no transfers — as with DMAT on the UART.

The element size does not match the table. A uint16_t table with a 32-bit MSIZE is read in words: DMA takes every second value and runs past the end of the table. MSIZE follows the element size in memory, PSIZE the register.


9.10 Summary

Full sources

dma_f0.c — DMA on STM32F072: USART1_TX on channel 2.

/* Раздел 9.8: DMA на STM32F072 — как у F1: каналы, USART1_TX на канале 2
 * (перенос на канал 4 — бит USART1TX_DMA_RMP в SYSCFG). */
#include <stddef.h>
#include "stm32f0xx.h"

_Static_assert(offsetof(DMA_Channel_TypeDef, CNDTR) == 0x04u && offsetof(DMA_Channel_TypeDef, CMAR) == 0x0Cu,
               "регистры канала — как у F1");
_Static_assert(offsetof(DMA_TypeDef, IFCR) == 0x04u, "ISR и IFCR");

static char tx_buf[64];

void dma_usart1_tx_init_f0(void)
{
    RCC->AHBENR |= RCC_AHBENR_DMAEN;            /* у F0 бит зовётся DMAEN; DMA1EN — устаревший псевдоним */
    DMA1_Channel2->CCR &= ~DMA_CCR_EN;          /* настраивать только выключенным */
    DMA1_Channel2->CPAR = (uint32_t)&USART1->TDR;   /* у нового USART — TDR, не DR */
    DMA1_Channel2->CMAR = (uint32_t)tx_buf;
    DMA1_Channel2->CNDTR = 0;
    DMA1_Channel2->CCR = DMA_CCR_DIR            /* имена битов без номера канала: DMA_CCR_DIR, не DMA_CCR4_DIR */
                       | DMA_CCR_MINC
                       | DMA_CCR_TCIE
                       | DMA_CCR_PL_1;
    NVIC_SetPriority(DMA1_Channel2_3_IRQn, 2);  /* у F0 одно прерывание на каналы 2 и 3 */
    NVIC_EnableIRQ(DMA1_Channel2_3_IRQn);
    USART1->CR3 |= USART_CR3_DMAT;
}

void dma_usart1_send_f0(const char *data, uint16_t len)
{
    for (uint16_t i = 0; i < len && i < sizeof tx_buf; i++)
        tx_buf[i] = data[i];
    DMA1_Channel2->CCR &= ~DMA_CCR_EN;
    DMA1_Channel2->CNDTR = len;
    DMA1_Channel2->CCR |= DMA_CCR_EN;
}

void DMA1_Channel2_3_IRQHandler(void)
{
    if (DMA1->ISR & DMA_ISR_TCIF2) {
        DMA1->IFCR = DMA_IFCR_CTCIF2;
    }
}

dma_g0.c — DMA on STM32G071: the USART2_TX request through DMAMUX.

/* Раздел 9.8: DMA на STM32G071 — те же каналы, но запрос к каналу подводит DMAMUX:
 * USART2_TX на канале 1 DMA1 через DMAMUX1_Channel0. */
#include <stddef.h>
#include "stm32g0xx.h"
#include "stm32g0xx_ll_dmamux.h"

_Static_assert(offsetof(DMA_Channel_TypeDef, CMAR) == 0x0Cu, "регистры канала — как у F0 и F1");
_Static_assert(LL_DMAMUX_REQ_USART2_TX == 53u && LL_DMAMUX_REQ_USART1_TX == 51u && LL_DMAMUX_REQ_ADC1 == 5u,
               "номера запросов DMAMUX из LL");
_Static_assert(offsetof(DMAMUX_Channel_TypeDef, CCR) == 0u, "у канала DMAMUX один регистр CCR");

static char tx_buf[64];

void dma_usart2_tx_init_g0(void)
{
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;
    /* канал 1 DMA1 — это канал 0 DMAMUX: нумерация со сдвигом на единицу */
    DMAMUX1_Channel0->CCR = LL_DMAMUX_REQ_USART2_TX;    /* без этой строки канал молчит */
    DMA1_Channel1->CCR &= ~DMA_CCR_EN;
    DMA1_Channel1->CPAR = (uint32_t)&USART2->TDR;
    DMA1_Channel1->CMAR = (uint32_t)tx_buf;
    DMA1_Channel1->CNDTR = 0;
    DMA1_Channel1->CCR = DMA_CCR_DIR | DMA_CCR_MINC | DMA_CCR_TCIE | DMA_CCR_PL_1;
    NVIC_SetPriority(DMA1_Channel1_IRQn, 2);
    NVIC_EnableIRQ(DMA1_Channel1_IRQn);
    USART2->CR3 |= USART_CR3_DMAT;
}

void dma_usart2_send_g0(const char *data, uint16_t len)
{
    for (uint16_t i = 0; i < len && i < sizeof tx_buf; i++)
        tx_buf[i] = data[i];
    DMA1_Channel1->CCR &= ~DMA_CCR_EN;
    DMA1_Channel1->CNDTR = len;
    DMA1_Channel1->CCR |= DMA_CCR_EN;
}

void DMA1_Channel1_IRQHandler(void)
{
    if (DMA1->ISR & DMA_ISR_TCIF1) {
        DMA1->IFCR = DMA_IFCR_CTCIF1;
    }
}

Next: section 9а "GPDMA on STM32U5: linked lists, triggers and ring reception without interrupts" — on U5, instead of the CIRC bit and a mapping table, there is a chain of transfer descriptions in memory.

Open in the interactive handbook