Embedded HandbookApps
All sections › III. Peripherals
STM32F103, STM32F407 · level intermediate · 20 min read
Open in the interactive handbook

16б. Lossless UART reception: DMA into a ring, IDLE and the half buffer

In section 16.5 the scheme "DMA stores bytes in a ring, and a pause on the line calls the parser" is described in words. In section 5а.5 the same task is solved with the HAL wrapper ReceiveToIdle in normal DMA mode: receive a portion, parse it, start again. Here — circular mode entirely in our own code: the write position from the NDTR counter, three events (half of the ring, end of the ring, pause), parsing frames right in the ring without copying, the ring size from the baud rate and the worst handler latency, and overrun — of both the ring and the receiver (ORE).

Prerequisites: reference manuals RM0008 (F1: DMA — 13.3, USART — 27.3–27.6) and RM0090 (F4: DMA — 10.3–10.5, USART — 30.3–30.6), the CMSIS headers from the Keil packs, the STM32F103x8/xB datasheet (RAM size), HAL F4 from the Keil pack 2.17.1 and HAL F1 1.8.7 from the STM32Cube archive. The parser itself (uring.c) was run on a computer against a model "USART + circular DMA" on a stream of 20 000 frames with random pauses and lengths, and the same run in QEMU on Cortex-M4. The code for the F103 and F407 was built with armclang.

Terms used in the documentation

Term Meaning
circular mode (CIRC) DMA restarts from the beginning by itself
number of data to transfer (CNDTR, SxNDTR) the counter of remaining transfers
half transfer (HTIF, HTIE) half of the buffer has been passed
transfer complete (TCIF, TCIE) the end of the buffer has been passed
idle line detected (IDLE, IDLEIE) a pause on the line
overrun error (ORE) receiver overrun
error interrupt enable (EIE) the error interrupt when receiving through DMA
DMA enable receiver (DMAR) DMA requests from the receiver
direct mode (DMDIS = 0) the stream's direct mode, without FIFO
reception to idle (ReceiveToIdle) receive until a pause — a HAL wrapper

16б.1 A ring that DMA writes into

The DMA channel is set up "from the USART data register to memory" with memory address increment and the CIRC bit. For each received byte it moves it into the ring and decrements the counter. On reaching zero, the counter in circular mode reloads itself with the previous value, and writing starts again from the beginning — this is how both manuals describe circular mode (RM0008, 13.3.3; RM0090, 10.3.8). Set it up once — the channel receives for ever.

How many bytes are already in the ring is told by the counter itself: while the channel is enabled, CNDTR on the F1 and SxNDTR on the F4 are read-only and show how many transfers remain until the end of the round. Hence the position where DMA will put the next byte:

wr = size − NDTR

The second position — how far the data has been parsed — is kept by the program (rd). Everything between rd and wr around the ring is the unparsed data. Nothing else is needed: no counter of received bytes, no channel restart.

A 512-byte DMA ring: the write position wr = 512 − NDTR = 90, the start of unparsed data rd = 470, a whole 67-byte frame across the end of the ring in two pieces of 42 and 25 bytes, the next frame waits in the ring; marks HT at the half and TC at the end

This pair of pointers has one blind case: wr = rd. That is how both an empty ring and a ring that DMA went all the way round while the parser slept look. They cannot be told apart by the counter — so the ring size is calculated so that the second case never happens (16б.5).


16б.2 Three events: half, end and pause

The parser has to be called in time. There are three reasons.

Half of the ring (HT) and end of the ring (TC) are interrupts of the DMA channel itself: the HTIF flag rises when half has been transferred, TCIF when the last element has been transferred (RM0008, 13.3.3; RM0090, 10.3.17). In a continuous stream without pauses they come every size/2 bytes and do not let the ring overflow.

A pause on the line (IDLE) is a USART flag: the line has been silent for a whole character after the last byte (RM0008, 27.3.1: "idle character" — a whole frame of ones). It rises once per pause: it will be set again only after a new byte has been received (the note to the IDLE bit). It delivers the tail of a message at once, without waiting for half of the ring.

What happens without one of them was shown by the run uring_sim.c: 921 600 baud, 20 000 frames, 715 183 bytes in total, the handler starts after a random delay of 0 to 1000 µs after the event:

Events Ring Frames received Bytes overwritten Largest occupancy Largest frame delay
HT + TC + IDLE 512 20 000 0 382 3428 µs
IDLE only 512 19 801 877 501 5982 µs
IDLE only 2048 20 000 0 695 7156 µs
HT + TC only 512 19 994 0 401 61 320 µs

IDLE only loses frames as soon as frames come back to back for longer than fits in the ring: there are no pauses, so the parser is not called. Only a ring larger than the longest burst helps, and its length is usually not known in advance. HT and TC only does not lose a single byte, but the frame before a pause waits until DMA reaches the next half: up to 61 320 µs in the run, and the six last frames were never delivered — there are no more events. All three together give 0 losses and a delay of no more than 3428 µs.

This delay is not an error. In a continuous stream there are no pauses, and a frame waits for the nearest half of the ring plus the handler latency: for 512 bytes this is 2778 + 1000 = 3778 µs at most. If you need less, make the ring smaller or also parse on a timer.


16б.3 🔧 Code: F103 and F407

USART1 receives at 921 600 baud. On the F103 its receive requests are served by channel 5 of DMA1 (RM0008, table 78), on the F407 by stream 2 or 5 of DMA2 on channel 4 (RM0090, table 43); stream 2 is used here. The divider BRR gives 923 077 baud in both cases, an error of +0.16 %:

Chip USART1 clock BRR DMA
STM32F103 APB2 72 MHz 0x4E (4 + 14/16) DMA1, channel 5
STM32F407 APB2 84 MHz 0x5B (5 + 11/16) DMA2, stream 2, channel 4

Code for the F103 (uring_f103.c):

    uring_init(&rx, rx_buf, RX_SIZE, app_frame);
    DMA1_Channel5->CCR = 0;                     /* канал выключен: только так пишутся CNDTR и адреса */
    DMA1_Channel5->CPAR = (uint32_t)&USART1->DR;
    DMA1_Channel5->CMAR = (uint32_t)rx_buf;
    DMA1_Channel5->CNDTR = RX_SIZE;
    DMA1->IFCR = DMA_IFCR_CGIF5;
    DMA1_Channel5->CCR = DMA_CCR5_MINC | DMA_CCR5_CIRC | DMA_CCR5_HTIE | DMA_CCR5_TCIE | DMA_CCR5_EN;

    USART1->BRR = 78u;                          /* 72 МГц / 78 = 923 077 бод: +0,16 % */
    USART1->CR3 = USART_CR3_DMAR | USART_CR3_EIE;
    USART1->CR1 = USART_CR1_UE | USART_CR1_RE | USART_CR1_IDLEIE;

The "peripheral to memory" direction and byte sizes are zeros in CCR, they are not written. DMAR enables DMA requests from the receiver, EIE the interrupt on framing, noise and overrun errors, which with reception through DMA would not come otherwise (RM0008, table 196: EIE is needed only with DMA reception). The handlers:

static void rx_poll(void)
{
    uring_poll(&rx, (uint16_t)DMA1_Channel5->CNDTR);
}

void DMA1_Channel5_IRQHandler(void)             /* HT или TC: пройдена половина или конец кольца */
{
    DMA1->IFCR = DMA_IFCR_CGIF5;
    rx_poll();
}

void USART1_IRQHandler(void)
{
    uint32_t sr = USART1->SR;
    if (sr & USART_SR_ORE)
        uart_ore++;
    /* IDLE, ORE, NE, FE снимает пара «чтение SR, затем DR». При RXNE = 1 байт ещё ждёт DMA:
       его не трогаем, флаг снимется при следующем входе */
    if ((sr & (USART_SR_IDLE | USART_SR_ORE | USART_SR_NE | USART_SR_FE)) && !(sr & USART_SR_RXNE))
        (void)USART1->DR;
    if (sr & USART_SR_IDLE)
        rx_poll();
}

Both interrupts are at the same priority (5): the parser cannot interrupt itself, and the shared rx does not need protection. In the armclang listing the setup comes down to the writes CCR = 0xA7, CR3 = 0x41 and CR1 = 0x2014; the address of the USART1 data register in CPAR is 0x40013804.

On the F407 (uring_f407.c) there are three differences. Before setup the stream is disabled and you wait until EN becomes zero — it does not clear at once. The stream flags are cleared in LIFCR before enabling interrupts: otherwise a flag left from last time would raise an interrupt at once (RM0090, 10.4). And the stream FIFO is not enabled:

    DMA2_Stream2->FCR = 0;                      /* прямой режим, без FIFO */
    DMA2->LIFCR = S2_FLAGS;                     /* старые флаги иначе сразу дадут прерывание */
    DMA2_Stream2->CR = (4u << DMA_SxCR_CHSEL_Pos) | DMA_SxCR_MINC | DMA_SxCR_CIRC
                     | DMA_SxCR_HTIE | DMA_SxCR_TCIE | DMA_SxCR_EN;

In the listing this is LIFCR = 0x3D0000 (the five flags of stream 2) and SxCR = 0x08000519; PAR is 0x40011004, the USART1 data register on the F407.


16б.4 Parsing frames right in the ring

The frame in the example: 0x55, length (1…64), data, checksum — the complement of the sum of the length and data. The longest frame is 67 bytes. The parser (uring.c) goes from rd to wr and copies nothing:

void uring_poll(uring_t *r, uint16_t ndtr)
{
    uint16_t wr = (uint16_t)(r->size - ndtr);          /* куда DMA положит следующий байт */
    if (wr >= r->size)                                 /* NDTR = 0 в миг перезагрузки */
        wr = 0;
    uint16_t used = (uint16_t)(wr >= r->rd ? wr - r->rd : wr + r->size - r->rd);
    if (used > r->hiwater)
        r->hiwater = used;

Further in the loop: not 0x55 or a length outside 1…64 — shift by a byte (searching for the start); the frame has not fully arrived — exit, rd stays at its start; the checksum does not match — shift by a byte; otherwise the frame is handed to the callback and rd jumps past it. A frame that crosses the end of the ring is handed over in two pieces — "to the end of the ring" and "from the start":

        uint16_t s = idx(r, 2);                        /* данные: кусок до конца кольца и остаток */
        uint16_t n1 = (uint16_t)(r->size - s < len ? r->size - s : len);
        r->on_frame(&r->buf[s], n1, n1 < len ? r->buf : 0, (uint16_t)(len - n1));

Such a parser does not divide by the ring size (the index is corrected by subtraction), so the ring does not have to be a power of two. The price of "no copying" is visible in 16б.5: until a frame has arrived in full, its start occupies space in the ring. And the callback must use the data before returning: the pointers look into the ring, which DMA will soon overwrite.

hiwater is the largest ring occupancy the parser has seen. It is a cheap headroom gauge: in the run with a 512 ring and a delay of up to 1000 µs it reached 382.


16б.5 Ring size: baud rate and the worst latency

At 921 600 baud with 8N1 a character lasts 10.85 µs — 92 160 bytes per second. The ring loses no data as long as DMA does not catch up with rd. At the moment of parsing the ring may hold the start of a frame that has not fully arrived — up to 66 bytes — plus everything that came in since the previous run. And all size bytes cannot be occupied: then wr = rd, and the ring looks empty (16б.1). Hence the condition: size ≥ FMAX + W, where FMAX = 67 is the longest frame, and W is how many bytes DMA writes between two runs of the parser.

Polling on a timer once every P: W = P × rate. HT + TC + IDLE with a handler latency of 0 to L: the events come every size/2 bytes, but one run may be immediate, and the next late by L. Then W = size/2 + L × rate and size ≥ 2 × (FMAX + L × rate).

The formula (uring16c.py) against the run (uring_sim.c: the smallest even ring without a single loss, and the next eight also without losses):

How the parser is called Bytes in L or P By the formula Run Power of two
HT + TC + IDLE, L up to 250 µs 23.04 181 166 256
HT + TC + IDLE, L up to 500 µs 46.08 227 204 256
HT + TC + IDLE, L up to 1000 µs 92.16 319 266 512
HT + TC + IDLE, L up to 2000 µs 184.32 503 382 512
polling every 1000 µs 92.16 160 160 256
polling every 2000 µs 184.32 252 252 256
polling every 5000 µs 460.8 528 512 1024
polling every 10 000 µs 921.6 989 926 1024

The formula is nowhere smaller than the run: it is an upper bound. For polling it matched the run at 1 and 2 ms; at 5 and 10 ms the random stream did not contain such a long continuous burst with a long frame at the edge. With events the run is smaller than the formula: the worst combination "this run immediate, the next after L, and the frame at the edge the longest" happens rarely. Testing the ring for it by chance is not worth it — take the formula and round up.

Two conclusions from the table. Handler latency is expensive: from 250 to 2000 µs the ring grows almost threefold. And polling once every 1 ms, at the same size, is no worse than interrupts with a latency of up to 1 ms — it is just that frame delivery is then delayed by up to 999 µs when there are no pauses on the line. The buffer itself is small: 512 bytes are 2.5 % of the F103's RAM (20 KB) and 0.39 % on the F407 (128 KB of main RAM).


16б.6 Overrun, ORE and ReceiveToIdle

Ring overrun — DMA has lapped the parser. The counter says nothing about it: the position from NDTR is "ahead" again, and the parser honestly reads a mixture of old and new. Only the frame check catches it. In a run with a 256 ring and a latency of up to 1000 µs (the formula requires 319) 12 bytes were overwritten, 7 frames lost, 42 bytes thrown away while searching for a start — and 0 false frames: the checksum did not let them through. Without a checksum or a frame number such corruption would get inside the program.

Receiver overrun (ORE) is something else. A byte stays in the data register until DMA takes it; if the next byte is received before this happens, ORE rises. The contents of the data register are not lost, the byte in the shift register is (RM0008 and RM0090, the description of the ORE bit). DMA has the time of one character for this: 10.85 µs at 921 600 baud — 781 cycles at 72 MHz and 1823 at 168 MHz; at 115 200 baud 86.81 µs. DMA fails to keep up only if the bus is held for a long time by other channels or streams with a higher priority. An interrupt on ORE with reception through DMA is given only through EIE. The code of 16б.3 counts such cases in uart_ore and clears the flag without taking the byte away from DMA.

HAL. HAL_UARTEx_ReceiveToIdle_DMA (section 5а.5) with the DMA channel in circular mode does almost the same, but differently:

HAL: ReceiveToIdle_DMA, circular DMA Our own code in this section
what comes into the callback Size — a position in the buffer, not an increment the parser takes the position from NDTR itself
"how far parsed" keep it yourself rd in uring_t
a pause exactly at the end of the buffer no call from IDLE: the condition is 0 < NDTR < size; the position was already reported by the TC call the parser is called by all three events
what caused the call in HAL F1 1.8.7 — HAL_UARTEx_GetRxEventType (TC, HT, IDLE); HAL F4 from pack 2.17.1 does not have it does not matter: the parser is the same
an error during DMA reception any is blocking: HAL clears DMAR, stops DMA, calls HAL_UART_ErrorCallback counted in uart_ore, reception continues

The last row is the main difference in practice: in HAL a single ORE, noise or framing error stops reception until it is restarted from HAL_UART_ErrorCallback (16.4). Our own ring simply keeps working, and the frame check throws away the corruption.


16б.7 Pitfalls

A byte appears in the ring later than NDTR says (F4). The stream FIFO is enabled: in "peripheral → memory" mode the stream accumulates data in the FIFO and flushes it to memory when the threshold is reached (RM0090, 10.3.6), while SxNDTR counts transfers on the peripheral side. For UART reception use direct mode, DMDIS = 0: each request immediately gives one write to memory.

An interrupt right after enabling the stream. The HTIF or TCIF flag was left from last time. Clear it in LIFCR/HIFCR (F4) or IFCR (F1) before enabling.

Frames are lost only under load. The ring was sized for the average, not the worst handler latency. Size it by 16б.5 and watch hiwater: if it approaches the ring size, there is no headroom.

The parser corrupts its own state. The DMA and USART interrupts are at different priorities: the call from IDLE cuts into the parsing from HT. One priority for both — or disable interrupts while parsing.

Clearing IDLE takes a byte away. The pair "read SR, then DR" with RXNE = 1 takes the byte DMA was waiting for. Read DR only with RXNE = 0; with RXNE = 1 the flag will be cleared on the next entry.

After an error HAL goes silent. In HAL an error during DMA reception stops reception (16б.6); without a restart in HAL_UART_ErrorCallback the port is deaf until reset.

Frame data is corrupted later. The callback saved a pointer to a piece of the ring and read it afterwards. By then DMA had overwritten the place — copy what you need after returning.

On the F7 and H7 the ring holds old bytes. DMA writes to memory bypassing the core's data cache — the ring goes either in a non-cacheable region or with cache lines invalidated before parsing (section 11б).


16б.8 Summary

Full sources

uring.c — parsing frames right in the DMA ring.

/* 16б: разбор кадров прямо в кольце DMA. Позиция записи — size - NDTR. */
#include "uring.h"

void uring_init(uring_t *r, const uint8_t *buf, uint16_t size, uring_frame_fn fn)
{
    r->buf = buf;
    r->size = size;
    r->rd = 0;
    r->hiwater = 0;
    r->frames = 0;
    r->skipped = 0;
    r->on_frame = fn;
}

static uint16_t idx(const uring_t *r, uint32_t i)        /* rd + i по кругу, без деления */
{
    i += r->rd;
    return (uint16_t)(i >= r->size ? i - r->size : i);
}

void uring_poll(uring_t *r, uint16_t ndtr)
{
    uint16_t wr = (uint16_t)(r->size - ndtr);          /* куда DMA положит следующий байт */
    if (wr >= r->size)                                 /* NDTR = 0 в миг перезагрузки */
        wr = 0;
    uint16_t used = (uint16_t)(wr >= r->rd ? wr - r->rd : wr + r->size - r->rd);
    if (used > r->hiwater)
        r->hiwater = used;

    while (used >= 4u) {                               /* короче кадра не бывает */
        uint8_t len = r->buf[idx(r, 1)];
        if (r->buf[r->rd] != URING_SYNC || len == 0u || len > URING_MAXPAY) {
            r->rd = idx(r, 1);                         /* не начало кадра: сдвиг на байт */
            used--;
            r->skipped++;
            continue;
        }
        if (used < len + 3u)
            break;                                     /* кадр пришёл не весь: ждать, не копируя */
        uint8_t sum = len;
        for (uint32_t i = 0; i < len; i++)
            sum = (uint8_t)(sum + r->buf[idx(r, 2u + i)]);
        if ((uint8_t)(sum + r->buf[idx(r, 2u + len)]) != 0xFFu) {   /* сумма + ~сумма = 0xFF */
            r->rd = idx(r, 1);
            used--;
            r->skipped++;
            continue;
        }
        uint16_t s = idx(r, 2);                        /* данные: кусок до конца кольца и остаток */
        uint16_t n1 = (uint16_t)(r->size - s < len ? r->size - s : len);
        r->on_frame(&r->buf[s], n1, n1 < len ? r->buf : 0, (uint16_t)(len - n1));
        r->rd = idx(r, len + 3u);
        used = (uint16_t)(used - (len + 3u));
        r->frames++;
    }
}

uring_f103.c — STM32F103: USART1 and DMA1 channel 5 in circular mode, events HT, TC and IDLE.

/* 16б: STM32F103 — USART1 (RX на PA10) на 921 600 бод, DMA1 канал 5 по кругу в кольцо 512 байт.
   Разбор зовут три события: половина кольца (HT), конец кольца (TC) и пауза в линии (IDLE).
   Собирается armclang: -DSTM32F10X_MD -include stm32f10x.h. */
#include <stdint.h>
#include "uring.h"

#define RX_SIZE 512u
_Static_assert(RX_SIZE % 2u == 0u && RX_SIZE <= 65535u, "HT — ровно половина; NDTR 16-битный");
_Static_assert(DMA_CCR5_CIRC == 0x20 && DMA_CCR5_HTIE == 0x4 && DMA_CCR5_TCIE == 0x2, "CIRC, HTIE, TCIE (RM0008, DMA_CCRx)");
_Static_assert(USART_CR3_DMAR == 0x40 && USART_CR3_EIE == 0x1 && USART_SR_IDLE == 0x10 && USART_SR_ORE == 0x8,
               "DMAR и EIE в CR3, IDLE и ORE в SR (RM0008)");

extern void app_frame(const uint8_t *a, uint16_t na, const uint8_t *b, uint16_t nb);

static uint8_t rx_buf[RX_SIZE];
static uring_t rx;
volatile uint32_t uart_ore;                     /* сколько раз DMA не успел забрать байт */

void uart_rx_start(void)
{
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN;
    GPIOA->CRH = (GPIOA->CRH & ~(0xFu << 8)) | (0x8u << 8);   /* PA10: вход с подтяжкой */
    GPIOA->BSRR = 1u << 10;                                  /* подтяжка вверх: линия в покое — 1 */

    uring_init(&rx, rx_buf, RX_SIZE, app_frame);
    DMA1_Channel5->CCR = 0;                     /* канал выключен: только так пишутся CNDTR и адреса */
    DMA1_Channel5->CPAR = (uint32_t)&USART1->DR;
    DMA1_Channel5->CMAR = (uint32_t)rx_buf;
    DMA1_Channel5->CNDTR = RX_SIZE;
    DMA1->IFCR = DMA_IFCR_CGIF5;
    DMA1_Channel5->CCR = DMA_CCR5_MINC | DMA_CCR5_CIRC | DMA_CCR5_HTIE | DMA_CCR5_TCIE | DMA_CCR5_EN;

    USART1->BRR = 78u;                          /* 72 МГц / 78 = 923 077 бод: +0,16 % */
    USART1->CR3 = USART_CR3_DMAR | USART_CR3_EIE;
    USART1->CR1 = USART_CR1_UE | USART_CR1_RE | USART_CR1_IDLEIE;

    NVIC_SetPriority(DMA1_Channel5_IRQn, 5);    /* один приоритет: разбор не прерывает сам себя */
    NVIC_SetPriority(USART1_IRQn, 5);
    NVIC_EnableIRQ(DMA1_Channel5_IRQn);
    NVIC_EnableIRQ(USART1_IRQn);
}

static void rx_poll(void)
{
    uring_poll(&rx, (uint16_t)DMA1_Channel5->CNDTR);
}

void DMA1_Channel5_IRQHandler(void)             /* HT или TC: пройдена половина или конец кольца */
{
    DMA1->IFCR = DMA_IFCR_CGIF5;
    rx_poll();
}

void USART1_IRQHandler(void)
{
    uint32_t sr = USART1->SR;
    if (sr & USART_SR_ORE)
        uart_ore++;
    /* IDLE, ORE, NE, FE снимает пара «чтение SR, затем DR». При RXNE = 1 байт ещё ждёт DMA:
       его не трогаем, флаг снимется при следующем входе */
    if ((sr & (USART_SR_IDLE | USART_SR_ORE | USART_SR_NE | USART_SR_FE)) && !(sr & USART_SR_RXNE))
        (void)USART1->DR;
    if (sr & USART_SR_IDLE)
        rx_poll();
}

uring_f407.c — STM32F407: USART1 and DMA2 stream 2 in circular mode.

/* 16б: STM32F407 — USART1 (RX на PA10, AF7) на 921 600 бод, DMA2 поток 2 канал 4 по кругу.
   FIFO потока не включаем: в прямом режиме каждый байт сразу уходит в память.
   Собирается armclang: -DSTM32F407xx -include stm32f4xx.h. */
#include <stdint.h>
#include "uring.h"

#define RX_SIZE 512u
_Static_assert(DMA_SxCR_CHSEL_Pos == 25u && DMA_SxCR_CIRC == 0x100u, "CHSEL с бита 25, CIRC — бит 8 (RM0090, DMA_SxCR)");
_Static_assert(DMA_SxFCR_DMDIS == 0x4u, "DMDIS = 0 — прямой режим (RM0090, DMA_SxFCR)");
_Static_assert(USART_CR3_DMAR == 0x40u && USART_SR_IDLE == 0x10u, "DMAR в CR3, IDLE в SR — как у F1");

extern void app_frame(const uint8_t *a, uint16_t na, const uint8_t *b, uint16_t nb);

static uint8_t rx_buf[RX_SIZE];
static uring_t rx;
volatile uint32_t uart_ore;

#define S2_FLAGS (DMA_LIFCR_CTCIF2 | DMA_LIFCR_CHTIF2 | DMA_LIFCR_CTEIF2 | DMA_LIFCR_CDMEIF2 | DMA_LIFCR_CFEIF2)

void uart_rx_start(void)
{
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_DMA2EN;
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
    GPIOA->MODER = (GPIOA->MODER & ~(3u << 20)) | (2u << 20);       /* PA10 — альтернативная функция */
    GPIOA->PUPDR = (GPIOA->PUPDR & ~(3u << 20)) | (1u << 20);       /* подтяжка вверх */
    GPIOA->AFR[1] = (GPIOA->AFR[1] & ~(0xFu << 8)) | (7u << 8);     /* AF7 — USART1_RX */

    uring_init(&rx, rx_buf, RX_SIZE, app_frame);
    DMA2_Stream2->CR = 0;
    while (DMA2_Stream2->CR & DMA_SxCR_EN) {    /* поток гаснет не сразу */
    }
    DMA2_Stream2->PAR = (uint32_t)&USART1->DR;
    DMA2_Stream2->M0AR = (uint32_t)rx_buf;
    DMA2_Stream2->NDTR = RX_SIZE;
    DMA2_Stream2->FCR = 0;                      /* прямой режим, без FIFO */
    DMA2->LIFCR = S2_FLAGS;                     /* старые флаги иначе сразу дадут прерывание */
    DMA2_Stream2->CR = (4u << DMA_SxCR_CHSEL_Pos) | DMA_SxCR_MINC | DMA_SxCR_CIRC
                     | DMA_SxCR_HTIE | DMA_SxCR_TCIE | DMA_SxCR_EN;

    USART1->BRR = 0x5Bu;                        /* 84 МГц: 5 + 11/16 -> 923 077 бод, +0,16 % */
    USART1->CR3 = USART_CR3_DMAR | USART_CR3_EIE;
    USART1->CR1 = USART_CR1_UE | USART_CR1_RE | USART_CR1_IDLEIE;

    NVIC_SetPriority(DMA2_Stream2_IRQn, 5);
    NVIC_SetPriority(USART1_IRQn, 5);
    NVIC_EnableIRQ(DMA2_Stream2_IRQn);
    NVIC_EnableIRQ(USART1_IRQn);
}

static void rx_poll(void)
{
    uring_poll(&rx, (uint16_t)DMA2_Stream2->NDTR);
}

void DMA2_Stream2_IRQHandler(void)
{
    DMA2->LIFCR = S2_FLAGS;
    rx_poll();
}

void USART1_IRQHandler(void)
{
    uint32_t sr = USART1->SR;
    if (sr & USART_SR_ORE)
        uart_ore++;
    if ((sr & (USART_SR_IDLE | USART_SR_ORE | USART_SR_NE | USART_SR_FE)) && !(sr & USART_SR_RXNE))
        (void)USART1->DR;
    if (sr & USART_SR_IDLE)
        rx_poll();
}

Next: section 16в "Bluetooth module HC-05/HC-06: transparent UART, AT mode, pairing and a protocol over the link" — the same UART without a wire: setting the module up with AT commands and acknowledged frames over the radio link.

Open in the interactive handbook