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.
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
- The write position is size −
NDTR; the second one,rd, is kept by the program.wr=rdmeans both empty and "lapped". - The parser is called by HT, TC and IDLE: without IDLE the tail waits up to 61 320 µs, without HT and TC a 512 ring loses 199 frames out of 20 000.
- Ring size: polling — size ≥ FMAX + P × rate; events — size ≥ 2 × (FMAX + L × rate). At 921 600 baud, frames of up to 67 bytes and a latency of up to 1000 µs this gives 319, so take 512; the run: 0 losses, occupancy up to 382.
- Parsing without copying keeps the start of an incomplete frame in the ring: that is FMAX in the formula.
NDTRdoes not see a ring overrun — the frame checksum catches it: 12 overwritten bytes, 7 lost frames, 0 false ones.OREwith DMA means the bus was not granted within a character: 10.85 µs at 921 600 baud; the interrupt is only throughEIE.- HAL
ReceiveToIdlein a ring gives a position, not an increment, and stops on any error; our own code counts errors and carries on.
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.