Embedded HandbookПриложения
Все разделы › III. Периферия
STM32F103, STM32F407 · уровень средний · 20 мин чтения
Открыть в интерактивном справочнике

16б. Приём UART без потерь: DMA в кольцо, IDLE и половина буфера

В разделе 16.5 схема «DMA складывает байты в кольцо, а пауза в линии зовёт разбор» описана словами. В разделе 5а.5 та же задача решена обёрткой HAL ReceiveToIdle в обычном режиме DMA: принять порцию, разобрать, запустить заново. Здесь — кольцевой режим целиком своим кодом: позиция записи по счётчику NDTR, три события (половина кольца, конец кольца, пауза), разбор кадров прямо в кольце без копирования, размер кольца от скорости и худшей задержки обработчика и переполнение — и кольца, и приёмника (ORE).

Опоры: справочные руководства RM0008 (F1: DMA — 13.3, USART — 27.3–27.6) и RM0090 (F4: DMA — 10.3–10.5, USART — 30.3–30.6), заголовки CMSIS из паков Keil, даташит STM32F103x8/xB (объём ОЗУ), HAL F4 из пака Keil 2.17.1 и HAL F1 1.8.7 из архива STM32Cube. Сам разбор (uring.c) прогнан на компьютере против модели «USART + DMA по кругу» на потоке из 20 000 кадров со случайными паузами и длинами и тот же прогон — в QEMU на Cortex-M4. Код для F103 и F407 собран armclang.

Как это называется в документации

Оригинал Перевод
circular mode (CIRC) кольцевой режим DMA
number of data to transfer (CNDTR, SxNDTR) счётчик оставшихся передач
half transfer (HTIF, HTIE) пройдена половина
transfer complete (TCIF, TCIE) пройден конец
idle line detected (IDLE, IDLEIE) пауза в линии
overrun error (ORE) переполнение приёмника
error interrupt enable (EIE) прерывание по ошибкам при приёме через DMA
DMA enable receiver (DMAR) запросы DMA от приёмника
direct mode (DMDIS = 0) прямой режим потока, без FIFO
reception to idle (ReceiveToIdle) приём до паузы — обёртка HAL

16б.1 Кольцо, в которое пишет DMA

Канал DMA настроен «из регистра данных USART в память» с приращением адреса памяти и битом CIRC. На каждый принятый байт он переносит его в кольцо и уменьшает счётчик. Дойдя до нуля, счётчик в кольцевом режиме сам загружается прежним значением, и запись идёт снова с начала — так описывают кольцевой режим оба руководства (RM0008, 13.3.3; RM0090, 10.3.8). Настроили один раз — канал принимает вечно.

Сколько байт уже в кольце, говорит сам счётчик: пока канал включён, CNDTR у F1 и SxNDTR у F4 только читаются и показывают, сколько передач осталось до конца круга. Отсюда позиция, куда DMA положит следующий байт:

wr = size − NDTR

Вторую позицию — докуда разобрано — ведёт программа (rd). Всё между rd и wr по кругу и есть неразобранное. Больше ничего не нужно: ни счётчика принятых байт, ни перезапуска канала.

Кольцо DMA на 512 байт: позиция записи wr = 512 − NDTR = 90, начало неразобранного rd = 470, целый кадр 67 байт через конец кольца двумя кусками по 42 и 25 байт, следующий кадр ждёт в кольце; метки HT на половине и TC на конце

У этой пары указателей один слепой случай: wr = rd. Так выглядит и пустое кольцо, и кольцо, которое DMA прошёл целиком, пока разбор спал. По счётчику их не различить — поэтому размер кольца считают так, чтобы второго случая не было (16б.5).


16б.2 Три события: половина, конец и пауза

Разбор надо позвать вовремя. Поводов три.

Половина кольца (HT) и конец кольца (TC) — прерывания самого канала DMA: флаг HTIF поднимается, когда передана половина, TCIF — когда передан последний элемент (RM0008, 13.3.3; RM0090, 10.3.17). В сплошном потоке без пауз они приходят через каждые size/2 байт и не дают кольцу переполниться.

Пауза в линии (IDLE) — флаг USART: линия молчала целый символ после последнего байта (RM0008, 27.3.1: «idle character» — целый кадр единиц). Поднимается один раз на паузу: снова он встанет только после нового принятого байта (примечание к биту IDLE). Он отдаёт хвост сообщения сразу, не дожидаясь половины кольца.

Что будет без одного из них, показал прогон uring_sim.c: 921 600 бод, 20 000 кадров, всего 715 183 байта, обработчик запускается через случайную задержку от 0 до 1000 мкс после события:

События Кольцо Принято кадров Затёрто байт Наибольшая занятость Наибольшая задержка кадра
HT + TC + IDLE 512 20 000 0 382 3428 мкс
только IDLE 512 19 801 877 501 5982 мкс
только IDLE 2048 20 000 0 695 7156 мкс
только HT + TC 512 19 994 0 401 61 320 мкс

Только IDLE теряет кадры, как только кадры идут вплотную дольше, чем помещается в кольцо: пауз нет — разбор не зовут. Помогает лишь кольцо больше самой длинной пачки, а её длину заранее обычно не знают. Только HT и TC не теряет ни байта, но кадр перед паузой ждёт, пока DMA не дойдёт до следующей половины: до 61 320 мкс в прогоне, а шесть последних кадров так и не отданы — событий больше нет. Все три вместе — 0 потерь и задержка не больше 3428 мкс.

Эта задержка — не ошибка. В сплошном потоке пауз нет, и кадр ждёт ближайшей половины кольца плюс задержку обработчика: для 512 байт это 2778 + 1000 = 3778 мкс сверху. Нужна меньшая — уменьшают кольцо или разбирают ещё и по таймеру.


16б.3 🔧 Код: F103 и F407

USART1 принимает на 921 600 бод. У F103 его запросы приёма обслуживает канал 5 DMA1 (RM0008, таблица 78), у F407 — поток 2 или 5 DMA2 на канале 4 (RM0090, таблица 43); здесь взят поток 2. Делитель BRR одинаково даёт 923 077 бод, ошибка +0,16 %:

Камень Тактовая USART1 BRR DMA
STM32F103 APB2 72 МГц 0x4E (4 + 14/16) DMA1, канал 5
STM32F407 APB2 84 МГц 0x5B (5 + 11/16) DMA2, поток 2, канал 4

Код для 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;

Направление «из периферии в память» и байтовые размеры — нули в CCR, их не пишут. DMAR включает запросы DMA от приёмника, EIE — прерывание по ошибкам кадра, шума и переполнения, которое при приёме через DMA иначе не придёт (RM0008, таблица 196: EIE нужен только при приёме через DMA). Обработчики:

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();
}

Оба прерывания стоят на одном приоритете (5): разбор не может прервать сам себя, и общий rx не нужно защищать. В листинге armclang настройка сводится к записям CCR = 0xA7, CR3 = 0x41 и CR1 = 0x2014; адрес регистра данных USART1 в CPAR — 0x40013804.

У F407 (uring_f407.c) отличий три. Поток перед настройкой выключают и ждут, пока EN станет нулём, — он гаснет не сразу. Флаги потока сбрасывают в LIFCR до включения прерываний: иначе поднятый с прошлого раза флаг сразу даст прерывание (RM0090, 10.4). И FIFO потока не включают:

    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;

В листинге это LIFCR = 0x3D0000 (пять флагов потока 2) и SxCR = 0x08000519; PAR — 0x40011004, регистр данных USART1 у F407.


16б.4 Разбор кадров прямо в кольце

Кадр в примере: 0x55, длина (1…64), данные, сумма — дополнение суммы длины и данных. Самый длинный кадр — 67 байт. Разбор (uring.c) идёт от rd к wr и не копирует ничего:

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;

Дальше в цикле: не 0x55 или длина вне 1…64 — сдвиг на байт (поиск начала); кадр пришёл не весь — выход, rd остаётся на его начале; сумма не сошлась — сдвиг на байт; иначе кадр отдаётся обратному вызову и rd прыгает за него. Кадр, перешедший через конец кольца, отдаётся двумя кусками — «до конца кольца» и «с начала»:

        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));

Такой разбор не делит на размер кольца (индекс поправляется вычитанием), поэтому кольцо не обязано быть степенью двойки. Цена «без копирования» видна в 16б.5: пока кадр не пришёл целиком, его начало занимает место в кольце. И обратный вызов должен успеть воспользоваться данными до выхода: указатели смотрят в кольцо, которое DMA скоро перепишет.

hiwater — наибольшая занятость кольца, какую видел разбор. Это дешёвый датчик запаса: в прогоне с кольцом 512 и задержкой до 1000 мкс она дошла до 382.


16б.5 Размер кольца: скорость и худшая задержка

На 921 600 бод при 8N1 символ длится 10,85 мкс — 92 160 байт в секунду. Кольцо не теряет данных, пока DMA не догоняет rd. К моменту разбора в кольце может лежать начало недошедшего кадра — до 66 байт — плюс всё, что пришло с прошлого запуска. А занять все size байт нельзя: тогда wr = rd, и кольцо выглядит пустым (16б.1). Отсюда условие: size ≥ FMAX + W, где FMAX = 67 — самый длинный кадр, W — сколько байт DMA пишет между двумя запусками разбора.

Опрос по таймеру раз в P: W = P × скорость. HT + TC + IDLE с задержкой обработчика от 0 до L: события приходят через size/2 байт, но один запуск может быть сразу, а следующий — опоздать на L. Тогда W = size/2 + L × скорость и size ≥ 2 × (FMAX + L × скорость).

Формула (uring16c.py) против прогона (uring_sim.c: наименьшее чётное кольцо без единой потери, и восемь следующих — тоже без потерь):

Как зовут разбор Байт за L или P По формуле Прогон Степень двойки
HT + TC + IDLE, L до 250 мкс 23,04 181 166 256
HT + TC + IDLE, L до 500 мкс 46,08 227 204 256
HT + TC + IDLE, L до 1000 мкс 92,16 319 266 512
HT + TC + IDLE, L до 2000 мкс 184,32 503 382 512
опрос раз в 1000 мкс 92,16 160 160 256
опрос раз в 2000 мкс 184,32 252 252 256
опрос раз в 5000 мкс 460,8 528 512 1024
опрос раз в 10 000 мкс 921,6 989 926 1024

Формула нигде не меньше прогона: это верхняя оценка. При опросе она совпала с прогоном для 1 и 2 мс; на 5 и 10 мс в случайном потоке не нашлось столь длинной сплошной пачки с длинным кадром на краю. При событиях прогон меньше формулы: худшее сочетание «этот запуск сразу, следующий через L, и кадр на краю самый длинный» случается редко. Проверять кольцо на него случаем не стоит — берут формулу и округляют вверх.

Два вывода из таблицы. Задержка обработчика дорогая: от 250 до 2000 мкс кольцо растёт почти втрое. И опрос раз в 1 мс, при таком же размере, не хуже прерываний с задержкой до 1 мс — просто задержка доставки кадра тогда до 999 мкс без пауз в линии. Сам буфер невелик: 512 байт — 2,5 % ОЗУ F103 (20 КБ) и 0,39 % у F407 (128 КБ основного ОЗУ).


16б.6 Переполнение, ORE и ReceiveToIdle

Переполнение кольца — DMA обогнал разбор на круг. Счётчик о нём молчит: позиция по NDTR снова «впереди», и разбор честно читает смесь старого и нового. Ловит его только проверка кадра. В прогоне с кольцом 256 и задержкой до 1000 мкс (формула требует 319) затёрто 12 байт, пропало 7 кадров, при поиске начала выброшено 42 байта — и 0 ложных кадров: сумма их не пропустила. Без суммы или номера кадра такая порча прошла бы внутрь программы.

Переполнение приёмника (ORE) — другое. Байт стоит в регистре данных, пока его не заберёт DMA; если следующий байт принят раньше, чем это случилось, поднимается ORE. Содержимое регистра данных при этом не теряется, пропадает байт в сдвиговом регистре (RM0008 и RM0090, описание бита ORE). На это у DMA есть время одного символа: 10,85 мкс на 921 600 бод — 781 такт при 72 МГц и 1823 при 168 МГц; на 115 200 бод — 86,81 мкс. Не успевает DMA, только если шину надолго заняли другие каналы или потоки с большим приоритетом. Прерывание по ORE при приёме через DMA дают только через EIE. Код 16б.3 считает такие случаи в uart_ore и снимает флаг, не отнимая байт у DMA.

HAL. HAL_UARTEx_ReceiveToIdle_DMA (раздел 5а.5) с каналом DMA в кольцевом режиме делает почти то же, но иначе:

HAL: ReceiveToIdle_DMA, DMA по кругу Свой код этого раздела
что приходит в обратный вызов Size — позиция в буфере, не приращение разбор сам берёт позицию из NDTR
«докуда разобрано» вести самому rd в uring_t
пауза ровно на конце буфера вызова от IDLE нет: условие 0 < NDTR < размера; позицию уже сообщил вызов по TC разбор зовут все три события
от чего пришёл вызов в HAL F1 1.8.7 — HAL_UARTEx_GetRxEventType (TC, HT, IDLE); в HAL F4 из пака 2.17.1 её нет не важно: разбор одинаковый
ошибка при приёме через DMA любая — блокирующая: HAL выключает DMAR, останавливает DMA, зовёт HAL_UART_ErrorCallback счёт в uart_ore, приём идёт дальше

Последняя строка — главное отличие на практике: в HAL один ORE, шум или ошибка кадра останавливают приём, пока его не запустят заново из HAL_UART_ErrorCallback (16.4). Своё кольцо просто продолжает работать, а порчу отбрасывает проверка кадра.


16б.7 Грабли

Байт в кольце позже, чем говорит NDTR (F4). Включён FIFO потока: в режиме «периферия → память» поток копит данные в FIFO и сливает в память по достижении порога (RM0090, 10.3.6), а SxNDTR считает передачи со стороны периферии. Для приёма UART — прямой режим, DMDIS = 0: каждый запрос сразу даёт одну запись в память.

Прерывание сразу после включения потока. Флаг HTIF или TCIF остался с прошлого раза. Сбросить в LIFCR/HIFCR (F4) или IFCR (F1) до включения.

Кадры пропадают только под нагрузкой. Кольцо посчитано по средней, а не по худшей задержке обработчика. Считать по 16б.5 и смотреть hiwater: если он подбирается к размеру кольца — запаса нет.

Разбор портит собственное состояние. Прерывания DMA и USART на разных приоритетах: вызов от IDLE вклинивается в разбор от HT. Один приоритет на оба — или запрет прерываний на время разбора.

Снятие IDLE отнимает байт. Пара «чтение SR, затем DR» при RXNE = 1 забирает байт, который ждал DMA. Читать DR только при RXNE = 0; при RXNE = 1 флаг снимется на следующем входе.

После ошибки HAL молчит. В HAL ошибка при приёме через DMA останавливает приём (16б.6); без перезапуска в HAL_UART_ErrorCallback порт глух до сброса.

Данные кадра испорчены позже. Обратный вызов сохранил указатель на кусок кольца и прочёл его потом. К тому времени DMA переписал место — копировать то, что нужно после выхода.

На F7 и H7 в кольце старые байты. DMA пишет в память мимо кэша данных ядра — кольцо либо в некэшируемой области, либо с аннулированием строк кэша перед разбором (раздел 11б).


16б.8 Итоги

Полные исходники

uring.c — разбор кадров прямо в кольце DMA.

/* 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 и DMA1 канал 5 по кругу, события HT, TC и 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 и DMA2 поток 2 по кругу.

/* 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();
}

Дальше: раздел 16в «Bluetooth-модуль HC-05/HC-06: прозрачный UART, AT-режим, сопряжение и протокол поверх канала» — тот же UART без провода: настройка модуля AT-командами и кадры с подтверждением поверх радиоканала.

Открыть в интерактивном справочнике