Embedded HandbookПриложения
Все разделы › II. Ядро и системные механизмы
STM32U585 · уровень продвинутый · 44 мин чтения
Открыть в интерактивном справочнике

9а. GPDMA на STM32U5: связные списки, триггеры и приём в кольцо без прерываний

Раздел 9 разобрал DMA у F1 и F4: каналы и потоки, бит CIRC, счётчик NDTR. У STM32U5 этого контроллера нет. На его месте — GPDMA: у него нет ни бита CIRC, ни двойной буферизации, ни таблицы привязок. Всё это заменяет один механизм — связный список: цепочка описаний пересылок в памяти, которую канал проходит сам. Здесь — как список устроен в памяти, как из него собрать кольцо для приёма UART (сравнение с 16б), два буфера для АЦП и вывод таблицы по триггеру таймера; чем отличается LPDMA и что добавляет TrustZone.

Опоры: справочное руководство RM0456 (ред. 4), главы 17 «GPDMA» и 18 «LPDMA», заголовок stm32u585xx.h и HAL 1.3.0 из пака Keil STM32U5xx_DFP 2.2.1, пакет платы B-U585I-IOT02A_BSP 1.1.0. Свой код собран armclang и arm-none-eabi-gcc под Cortex-M33. Тот же код прогнан на компьютере над моделью канала, написанной по тексту RM0456, а сборка элементов списка сверена слово в слово с кодом HAL. QEMU U5 не моделирует, на плате не запускалось.

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

Оригинал Перевод
general purpose DMA (GPDMA) DMA общего назначения
low-power DMA (LPDMA) DMA домена малого потребления
linked-list item (LLI) элемент связного списка
linked-list register file регистры канала, которые обновляет список
link transfer загрузка следующего элемента из памяти
update bits (UT1, UT2, UB1, USA, UDA, ULL) биты обновления: какие регистры лежат в элементе
run-to-completion mode список выполняется до конца сам
link step mode (LSM) один элемент за одно включение канала
block / burst / beat блок / пакет / одна пересылка
request (REQSEL) / trigger (TRIGSEL) запрос периферии / триггер
transfer complete event mode (TCEM) на каком уровне поднимать «передача завершена»
padding/alignment mode (PAM) дополнение и выравнивание при разной ширине
user setting error (USEF) ошибка настройки
update link error (ULEF) ошибка шины при чтении элемента
trigger overrun (TOF) потерянный триггер
2D addressing двумерная адресация

9а.1 Что изменилось против F1 и F4

F4 (раздел 9) STM32U585
контроллеры DMA1, DMA2 по 8 потоков GPDMA1 — 16 каналов, LPDMA1 — 4 канала
выбор источника поток + поле CHSEL, таблица привязок любой запрос на любой канал: REQSEL
что задаёт пересылку периферия, память, направление источник и приёмник, каждый со своей шириной и приращением
счётчик NDTR, в элементах BNDT, в байтах
по кругу бит CIRC связный список
два буфера бит DBM связный список из двух элементов
старт по событию нет триггер: TRIGSEL, TRIGPOL, TRIGM
флаги LISR/HISR на четыре потока свой CxSR у каждого канала, сброс в CxFCR
выключить сбросить EN и ждать SUSP, ждать SUSPF, RESET
защита нет каналы делятся на защищённые и привилегированные

Запросы. Таблицы привязок нет. Номер запроса пишут в поле REQSEL регистра CxTR2 — семь бит, в таблице 136 руководства номера от 0 до 125: adc1_dma — 0, usart1_rx_dma — 24, usart1_tx_dma — 25. Конфликтов «два блока на одном канале» больше нет, но появился обратный: один запрос нельзя отдавать двум включённым каналам, и ошибку настройки за это контроллер не поднимает.

Каналы неравные (2д.4): у каналов 0…11 очередь на 8 байт и линейная адресация, у 12…15 — 32 байта и двумерная. Очередь есть всегда: пересылка идёт в два приёма — из источника в очередь и из очереди в приёмник. Режима «без FIFO», как у F4, у GPDMA нет.

Приоритет — поле PRIO в CxCR, четыре значения. Значение 3 — отдельный класс: его обслуживают раньше всех. Значения 0, 1, 2 делят оставшееся время шины с весами: по примеру из RM0456, запрос с приоритетом 0 рядом с запросом приоритета 1 получает 1/4 оставшейся полосы против 3/4, рядом с приоритетом 2 — 1/6 против 5/6.

Имена. В RM0456 регистры зовут GPDMA_CxCR, GPDMA_CxTR1; в заголовке это поля CCR, CTR1, CTR2, CBR1, CSAR, CDAR, CTR3, CBR2, CLLR, CLBAR, CSR, CFCR структуры DMA_Channel_TypeDef, а биты — DMA_CCR_EN, DMA_CTR2_REQSEL без номера канала. Канал x лежит по смещению 0x50 + 0x80 · x от начала контроллера. У LPDMA1 структура та же.


9а.2 Связный список: CxLBAR, CxLLR и элемент в памяти

Шесть регистров канала описывают одну пересылку блока: CxTR1, CxTR2, CxBR1, CxSAR, CxDAR и CxLLR. У каналов 12…15 к ним добавлены CxTR3 и CxBR2. Когда блок закончен, канал смотрит в CxLLR:

Адрес элемента — старшие 16 бит из CxLBAR (поле LBA) плюс младшие 16 из CxLLR (поле LA, биты 15:2). Отсюда два правила RM0456: элемент выровнен на 4 байта, а весь список одного канала лежит в одном окне 64 КБ. Остальные биты CxLLR — биты обновления: какие регистры лежат в следующем элементе.

Бит CxLLR Номер Что обновить из памяти
UT1 31 CxTR1
UT2 30 CxTR2
UB1 29 CxBR1
USA 28 CxSAR
UDA 27 CxDAR
UT3 26 CxTR3 — только каналы 12…15
UB2 25 CxBR2 — только каналы 12…15
ULL 16 сам CxLLR

Элемент — не структура с фиксированными полями. Это слова подряд, без пропусков: по одному на каждый взведённый бит, в порядке таблицы. Все шесть бит — элемент в 6 слов (24 байта), у канала 12…15 со всеми восемью — 8 слов (32 байта). Один бит — одно слово. Важно, чей это CxLLR: раскладку элемента описывают биты предыдущего элемента (для первого — самого регистра). Слово LLR внутри элемента описывает уже следующий.

Приём в кольцо на GPDMA: слева шесть регистров канала — CxTR1, CxTR2 с номером запроса 24, CxBR1 со счётчиком BNDT 512, CxSAR с адресом регистра данных USART1, CxDAR и CxLLR; в середине элемент списка в памяти из одного слова — адрес начала кольца, который канал читает в CxDAR в конце каждого круга; справа кольцо 512 байт с событиями HTF на половине и TCF в конце; ниже размеры элементов: 5 слов на круг у HAL, 6 слов у полного элемента, 8 слов у каналов 12…15

Типы и константы, на которые опираются все листинги раздела (gpdma.h):

/* Значения регистров одного элемента списка — в том порядке, в каком GPDMA читает их из памяти */
typedef struct {
    uint32_t tr1, tr2, br1, sar, dar;   /* есть у всех каналов */
    uint32_t tr3, br2;                  /* только каналы 12…15: двумерная адресация */
} lli_regs_t;

/* Биты обновления CxLLR: какие регистры лежат в следующем элементе */
#define LLI_UPD_LINEAR (DMA_CLLR_UT1 | DMA_CLLR_UT2 | DMA_CLLR_UB1 | DMA_CLLR_USA | DMA_CLLR_UDA | DMA_CLLR_ULL)
#define LLI_UPD_2D     (LLI_UPD_LINEAR | DMA_CLLR_UT3 | DMA_CLLR_UB2)

#define GPDMA_FLAGS    (DMA_CFCR_TCF | DMA_CFCR_HTF | DMA_CFCR_DTEF | DMA_CFCR_ULEF | DMA_CFCR_USEF \
                        | DMA_CFCR_SUSPF | DMA_CFCR_TOF)
#define GPDMA_ERRORS   (DMA_CSR_DTEF | DMA_CSR_ULEF | DMA_CSR_USEF)

#define GPDMA_W8   0u                   /* SDW_LOG2 / DDW_LOG2: двоичный логарифм ширины в байтах */
#define GPDMA_W16  1u
#define GPDMA_W32  2u
#define GPDMA_TRIG_RISING 1u            /* TRIGPOL */
#define GPDMA_TRIG_BLOCK  0u            /* TRIGM: триггер разрешает блок */
#define GPDMA_TRIG_LINK   2u            /*        загрузку следующего элемента */
#define GPDMA_TRIG_EACH   3u            /*        каждую пересылку */

Сборка элемента и значения CxLLR (gpdma.c):

/* Сколько 32-битных слов занимает элемент, который описывают биты обновления upd */
uint32_t lli_words(uint32_t upd)
{
    uint32_t n = 0;
    for (upd &= LLI_UPD_2D; upd != 0u; upd &= upd - 1u)
        n++;
    return n;
}

/* Собрать элемент в памяти. upd — биты Uxx из CxLLR ПРЕДЫДУЩЕГО элемента (для первого — из самого
   регистра CxLLR): они говорят, какие регистры лежат в этом элементе. Слова идут подряд, без
   пропусков, в порядке TR1, TR2, BR1, SAR, DAR, TR3, BR2, LLR. llr — то, что попадёт в CxLLR:
   описание следующего элемента. Возвращает число записанных слов. */
uint32_t lli_pack(uint32_t *mem, const lli_regs_t *r, uint32_t upd, uint32_t llr)
{
    uint32_t n = 0;
    if (upd & DMA_CLLR_UT1) mem[n++] = r->tr1;
    if (upd & DMA_CLLR_UT2) mem[n++] = r->tr2;
    if (upd & DMA_CLLR_UB1) mem[n++] = r->br1;
    if (upd & DMA_CLLR_USA) mem[n++] = r->sar;
    if (upd & DMA_CLLR_UDA) mem[n++] = r->dar;
    if (upd & DMA_CLLR_UT3) mem[n++] = r->tr3;
    if (upd & DMA_CLLR_UB2) mem[n++] = r->br2;
    if (upd & DMA_CLLR_ULL) mem[n++] = llr;
    return n;
}

/* Значение CxLLR: какие регистры обновить (upd) и где лежит элемент — младшие 16 бит адреса.
   Старшие 16 бит держит CxLBAR, один на весь список: список целиком лежит в одном окне 64 КБ. */
uint32_t lli_link(uint32_t upd, uint32_t addr)
{
    return (upd & LLI_UPD_2D) | (addr & DMA_CLLR_LA);
}

Что с CxBR1, если его нет в элементе. Счётчик BNDT к концу блока дошёл до нуля. Если UB1 = 0, а хоть один другой бит обновления взведён, контроллер сам возвращает в BNDT запрограммированное значение. На этом и держится кольцо (9а.4): длину блока в элементе повторять не нужно.

Два режима прохода. LSM = 0 в CxCR — список идёт сам до CxLLR = 0. Если последний элемент указывает на себя или назад, канал не остановится никогда — это и есть «по кругу». LSM = 1 — режим одного шага: канал выполняет один элемент, загружает следующий и выключается; программа включает его снова. За это время она может дописать или подменить следующий элемент в памяти.

Прогон на модели. Список из трёх блоков: первый — в регистрах, второй — полный элемент в 6 слов, третий — сжатый, как в примере RM0456 (UT2, UDA, ULL: 3 слова), с CxLLR = 0. Копирование 64 байт в три разных места: 3 блока, 2 загрузки, 9 слов прочитано, в конце EN = 0 и BNDT = 0. В режиме одного шага те же 64 байта потребовали 3 включения канала.

Сверка с HAL. HAL_DMAEx_List_BuildNode кладёт регистры в массив LinkRegisters[8] в том же порядке: для линейного канала CxLLR — слово с индексом 5, для двумерного TR3 и BR2 — 5 и 6, CxLLR — 7. Пять слов линейного элемента и семь слов двумерного, собранные lli_pack, совпали с HAL побайтно. Структура DMA_NodeTypeDef у HAL занимает 36 байт: 8 слов регистров и слово служебных сведений, которое в канал не уходит.


9а.3 Регистры пересылки: CxTR1, CxTR2, CxBR1

CxTR1 — как переносить. Две одинаковые половины: младшая про источник, старшая про приёмник.

Поле Биты Смысл
SDW_LOG2 / DDW_LOG2 1:0 / 17:16 ширина: 0 — байт, 1 — полуслово, 2 — слово; 3 — ошибка настройки
SINC / DINC 3 / 19 адрес растёт после каждой пересылки
SBL_1 / DBL_1 9:4 / 25:20 длина пакета минус один: 0…63
PAM 12:11 что делать при разной ширине
SBX, DBX, DHX 13, 26, 27 перестановка байтов и полуслов
SAP / DAP 14 / 30 какой из двух портов AHB использовать
SSEC / DSEC 15 / 31 пересылка защищённая — только у защищённого канала

Направления «периферия → память» больше нет: есть источник и приёмник. Растёт адрес той стороны, где память.

PAM заменяет сразу несколько приёмов F4. Приёмник шире источника: 00 — значение в младших битах, старшие — нули; 01 — расширить знаком; 1x — собрать несколько единиц источника в одну единицу приёмника. Приёмник уже источника: 00 — оставить младшую часть, 01 — старшую, 1x — разложить на несколько записей. Так 16-битная таблица ложится в 32-битный регистр без промежуточного массива (9а.5).

Пакет программа задаёт длиной до 64 пересылок, но контроллер сам режет его на части: если пакет больше половины очереди канала, если он переходит границу 1 КБ адресов или длиннее 16 пересылок. Целостность данных при этом сохраняется — в отличие от F4, где за границу 1 КБ отвечал программист (9.6).

CxTR2 — когда переносить.

Поле Биты Смысл
REQSEL 6:0 номер запроса периферии
SWREQ 9 1 — запрос программный, REQSEL не смотрят: память → память
DREQ 10 0 — запрос даёт источник, 1 — приёмник
BREQ 11 1 — один запрос на целый блок
TRIGM 15:14 что разрешает триггер
TRIGSEL 22:16 номер триггера
TRIGPOL 25:24 01 — фронт, 10 — спад, 00 и 11 — триггера нет
TCEM 31:30 уровень события «передача завершена»

DREQ — то, что осталось от бита направления: при передаче в периферию запрос даёт приёмник, и DREQ = 1. BREQ нужен двум запросам из таблицы 137: lptim1_ue_dma и lptim3_ue_dma.

Запрос и триггер — разные вещи. Запрос говорит «данные готовы» и приходит от того блока, с которым идёт обмен. Триггер говорит «можно начинать» и приходит со стороны: линии EXTI (номера 0…7 в таблице 138), завершение другого канала (gpdma1_ch0_tc — 22 … gpdma1_ch15_tc — 37), таймеры (tim2_trgo — 42, tim15_trgo — 43). TRIGM выбирает, что именно ждёт триггера: 00 — начало каждого блока, 10 — загрузку следующего элемента, 11 — каждую пересылку. Один сработавший триггер контроллер запоминает; второй, пришедший до конца пересылки, теряется, и поднимается TOF. Канал при этом не выключается.

⚠️ В заголовке U585 поле TRIGSEL — 6 бит (маска 0x3F), а в RM0456 ред. 4 — 7 бит, и таблица триггеров идёт до номера 67. Старшие номера — у других камней серии; tim3_trgo, tim4_trgo и tim5_trgo (44…46) по сноске к таблице есть только у U59x/5Ax и U5Fx/5Gx.

TCEM — на каком уровне поднимать TCF и HTF: 00 — каждый блок, 01 — повторённый блок целиком (каналы 12…15), 10 — каждый элемент вместе с загрузкой следующего, 11 — только конец всего списка. У бесконечного списка с TCEM = 11 событий не будет вовсе.

Два построителя полей (gpdma.c):

/* CxTR1: ширина и приращение адреса источника и приёмника; пакет — одна пересылка, PAM = 00 */
uint32_t gpdma_tr1(uint32_t sdw_log2, uint32_t sinc, uint32_t ddw_log2, uint32_t dinc)
{
    return ((sdw_log2 << DMA_CTR1_SDW_LOG2_Pos) & DMA_CTR1_SDW_LOG2) | (sinc ? DMA_CTR1_SINC : 0u)
         | ((ddw_log2 << DMA_CTR1_DDW_LOG2_Pos) & DMA_CTR1_DDW_LOG2) | (dinc ? DMA_CTR1_DINC : 0u);
}

/* Поля триггера в CxTR2: номер входа, фронт, что именно триггер разрешает */
uint32_t gpdma_trig(uint32_t trigsel, uint32_t pol, uint32_t mode)
{
    return ((trigsel << DMA_CTR2_TRIGSEL_Pos) & DMA_CTR2_TRIGSEL)
         | ((pol << DMA_CTR2_TRIGPOL_Pos) & DMA_CTR2_TRIGPOL)
         | ((mode << DMA_CTR2_TRIGM_Pos) & DMA_CTR2_TRIGM);
}

CxBR1 — сколько. BNDT (биты 15:0) — длина блока в байтах, до 65 535; пока канал включён, поле читается и убывает. Длина обязана делиться на ширину источника, иначе — USEF. Половина блока для HTF — BNDT/2 с округлением вверх.

Двумерная адресация есть только у каналов 12…15. BRC (биты 26:16 в CxBR1) — сколько раз повторить блок, 0…2047. CxTR3 — прибавка к адресу после каждого пакета (SAO, DAO, по 13 бит), CxBR2 — после каждого блока (BRSAO, BRDAO, по 16 бит); знак задают биты SDEC, DDEC, BRSDEC, BRDDEC в CxBR1. Так читают прямоугольник из кадра или раскладывают поток по столбцам. Кольцо на таком канале RM0456 предлагает делать иначе: отрицательная прибавка после блока сама возвращает адрес к началу.


9а.4 🔧 Приём UART в кольцо: список из одного слова

Задача та же, что в 16б: USART1 принимает на 921 600 бод, DMA кладёт байты в кольцо 512 байт, разбор зовут три события. Отличается только то, как сделано «по кругу». RM0456 (17.4.13) описывает это так: первый блок задают регистры, а единственный элемент списка возвращает адрес памяти в начало; длину блока контроллер восстанавливает сам. Для приёма это элемент с одним битом UDA — одно слово. ULL = 0: CxLLR не обновляется и продолжает указывать на тот же элемент.

/* Приём в кольцо: периферия -> память, байт на запрос, по кругу без участия программы.
   Список — один элемент в одно слово: после каждого круга GPDMA читает из node новый CxDAR,
   начало кольца. CxBR1 он восстанавливает сам, а CxLLR не обновляется (ULL = 0) и продолжает
   указывать на тот же элемент. Это замена биту CIRC у F1 и F4. */
void gpdma_ring_rx(DMA_Channel_TypeDef *ch, uint32_t reqsel, uint32_t src_reg,
                   uint32_t buf_addr, uint16_t size, uint32_t *node, uint32_t node_addr)
{
    lli_regs_t r = {0};

    (void)gpdma_abort(ch);
    r.dar = buf_addr;
    (void)lli_pack(node, &r, DMA_CLLR_UDA, 0u);     /* в элементе одно слово — CxDAR */

    ch->CTR1  = gpdma_tr1(GPDMA_W8, 0u, GPDMA_W8, 1u);  /* байт -> байт, растёт адрес приёмника */
    ch->CTR2  = reqsel & DMA_CTR2_REQSEL;           /* запрос даёт источник: DREQ = 0; TCEM = 00 */
    ch->CBR1  = size;                               /* длина блока — в БАЙТАХ */
    ch->CSAR  = src_reg;
    ch->CDAR  = buf_addr;
    ch->CLBAR = node_addr & DMA_CLBAR_LBA;          /* старшие 16 бит адреса списка */
    ch->CLLR  = lli_link(DMA_CLLR_UDA, node_addr);  /* обновлять только CxDAR, брать по node_addr */
    ch->CCR   = DMA_CCR_HTIE | DMA_CCR_TCIE | DMA_CCR_DTEIE | DMA_CCR_ULEIE | DMA_CCR_USEIE
              | DMA_CCR_PRIO_1;                     /* LSM = 0: список идёт сам */
    ch->CCR  |= DMA_CCR_EN;
}

/* Куда GPDMA положит следующий байт кольца: BNDT — сколько байт блока осталось, как NDTR у F4 */
uint16_t gpdma_ring_wr(const DMA_Channel_TypeDef *ch, uint16_t size)
{
    uint16_t left = (uint16_t)(ch->CBR1 & DMA_CBR1_BNDT);
    if (left == 0u || left > size)                  /* BNDT = 0 — миг между концом круга и загрузкой элемента */
        return 0u;
    return (uint16_t)(size - left);
}

Позиция записи считается как в 16б.1: wr = size − BNDT. Разбор кадров — тот же uring.c из 16б.4 без единой правки: ему вместо NDTR передают BNDT.

Константы примеров (gpdma_u585.c):

#define PCLK2_HZ          160000000u
#define BAUD              921600u
#define RX_SIZE           512u
#define ADC_N             256u              /* отсчётов в одном буфере */
#define PAT_N             8u

#define REQ_ADC1          0u                /* GPDMA1: adc1_dma */
#define REQ_USART1_RX     24u               /* GPDMA1: usart1_rx_dma */
#define TRIG_TIM2_TRGO    42u               /* GPDMA1: tim2_trgo */
#define LPREQ_LPUART1_RX  0u                /* LPDMA1: lpuart1_rx_dma */
#define ADC_EXT_TIM2_TRGO 11u               /* ADC1: adc_ext_trg11 = tim2_trgo */

Буферы и элементы списков — статические: канал читает элемент после каждого круга, пока включён.

static uint8_t  rx_buf[RX_SIZE];
static uint32_t rx_node[1];                 /* элемент списка кольца: одно слово */
static uring_t  rx;
static uint16_t adc_a[ADC_N], adc_b[ADC_N];
static uint32_t adc_nodes[4];               /* два элемента по два слова */
static const uint16_t pattern[PAT_N] = {0x0001u, 0x0003u, 0x0007u, 0x000Fu, 0x000Eu, 0x000Cu, 0x0008u, 0x0000u};
static uint32_t pat_node[1];
static uint8_t  lp_buf[64];
static uint32_t lp_node[1];

volatile uint32_t rx_frames, dma_errors, uart_ore, adc_blocks;
volatile const uint16_t *adc_ready;         /* буфер, который можно обрабатывать */

static void app_frame(const uint8_t *a, uint16_t na, const uint8_t *b, uint16_t nb)
{
    (void)a; (void)na; (void)b; (void)nb;
    rx_frames++;
}

Привязка к USART1:

static void uart_ring_arm(void)
{
    gpdma_ring_rx(GPDMA1_Channel0, REQ_USART1_RX, (uint32_t)&USART1->RDR,
                  (uint32_t)rx_buf, RX_SIZE, rx_node, (uint32_t)rx_node);
}

void uart_ring_init(void)
{
    RCC->AHB1ENR |= RCC_AHB1ENR_GPDMA1EN;
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
    (void)RCC->APB2ENR;

    uring_init(&rx, rx_buf, RX_SIZE, app_frame);
    uart_ring_arm();                                    /* сначала канал, потом запросы */

    USART1->BRR = (PCLK2_HZ + BAUD / 2u) / BAUD;        /* 174: 919 540 бод */
    USART1->CR3 = USART_CR3_DMAR | USART_CR3_EIE;
    USART1->CR1 = USART_CR1_UE | USART_CR1_RE | USART_CR1_IDLEIE;

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

static void rx_poll(void)
{
    uring_poll(&rx, (uint16_t)(GPDMA1_Channel0->CBR1 & DMA_CBR1_BNDT));
}

void GPDMA1_Channel0_IRQHandler(void)                   /* HT, TC или ошибка */
{
    uint32_t sr = GPDMA1_Channel0->CSR;
    GPDMA1_Channel0->CFCR = sr & GPDMA_FLAGS;           /* сброс — записью единиц */
    if (sr & GPDMA_ERRORS) {                            /* канал уже выключен железом */
        dma_errors++;
        rx.rd = 0;
        uart_ring_arm();                                /* сброс канала и запуск с начала кольца */
        return;
    }
    rx_poll();
}

void USART1_IRQHandler(void)
{
    uint32_t isr = USART1->ISR;
    if (isr & USART_ISR_ORE)
        uart_ore++;
    /* у U5 флаги снимают записью в ICR: регистр данных не читают, байт у GPDMA не отнять */
    USART1->ICR = USART_ICR_IDLECF | USART_ICR_ORECF | USART_ICR_NECF | USART_ICR_FECF;
    if (isr & USART_ISR_IDLE)
        rx_poll();
}

При такте 160 МГц (2д.5) делитель BRR = 174 даёт 919 540 бод, ошибка −0,22 %. Что получилось в регистрах, показала модель: CxTR1 = 0x00080000 (один бит DINC), CxTR2 = 0x00000018 (запрос 24), CxBR1 = 512, CxCR = 0x00801F01; в CxLLR — бит 27 и младшие 16 бит адреса элемента.

Против 16б:

F407 (16б.3) U585
«по кругу» бит CIRC элемент списка в одно слово
счётчик SxNDTR BNDT в CxBR1
перед настройкой сбросить EN, ждать, сбросить флаги в LIFCR gpdma_abort: сброс канала и флагов
FIFO выключить: прямой режим очередь есть всегда
снять IDLE чтение SR, затем DR — и только при RXNE = 0 запись IDLECF в ICR

Ловушка 16б.7 «снятие IDLE отнимает байт» у U5 исчезла: флаг снимают отдельным регистром.

«Без прерываний» — про само кольцо: перезапускать канал не нужно, он идёт сам, пока есть питание. Прерывания здесь только зовут разбор. Можно обойтись и без них: опрашивать BNDT по таймеру, как в 16б.5.

Прогон на модели: 200 кадров формата 16б, 7478 байт подряд через кольцо 512 — 14 полных кругов. После каждого круга CxDAR вернулся в начало кольца, BNDT — к 512; из списка прочитано 14 слов, по одному на круг. HTF поднялся 15 раз, TCF — 14. Позиция size − BNDT совпала с числом принятых байт после каждого из 7478. Разбор получил все 200 кадров; занятость кольца не превысила 306 байт.

А HAL? Пакет платы B-U585I-IOT02A_BSP делает такое же кольцо из одного элемента (звук с микрофона), но средствами HAL. HAL_DMAEx_List_SetCircularMode оставляет в элементе все шесть регистров — 6 слов на круг. После HAL_DMAEx_List_ConvertQToDynamic в CxLLR элемента остаётся ULL = 0, а пять остальных бит взведены — 5 слов на круг. Свой элемент — 1 слово. Разница — четыре или пять лишних чтений памяти раз в 512 байт: на скорость не влияет, но показывает, что именно делает список.


9а.5 🔧 АЦП по таймеру и таблица по триггеру

Два буфера по очереди. У F4 для этого был режим двойной буферизации. Здесь — два элемента по два слова, CxDAR и CxLLR, которые указывают друг на друга:

/* Два буфера по очереди — замена двойной буферизации F4. Два элемента по два слова: CxDAR и
   CxLLR. Первый блок идёт по регистрам в буфер A, дальше B, A, B ... Событие TC — после каждого
   блока: заполненный буфер можно обрабатывать, пока пишется второй. */
void gpdma_pingpong(DMA_Channel_TypeDef *ch, uint32_t tr1, uint32_t tr2, uint32_t src_reg,
                    uint32_t a_addr, uint32_t b_addr, uint16_t bytes, uint32_t *nodes, uint32_t nodes_addr)
{
    const uint32_t upd = DMA_CLLR_UDA | DMA_CLLR_ULL;
    lli_regs_t r = {0};
    uint32_t n;

    (void)gpdma_abort(ch);
    r.dar = b_addr;                                 /* элемент 0: писать в B, за ним — элемент 1 */
    n = lli_pack(&nodes[0], &r, upd, lli_link(upd, nodes_addr + 4u * lli_words(upd)));
    r.dar = a_addr;                                 /* элемент 1: писать в A, за ним — элемент 0 */
    (void)lli_pack(&nodes[n], &r, upd, lli_link(upd, nodes_addr));

    ch->CTR1  = tr1;
    ch->CTR2  = tr2;
    ch->CBR1  = bytes;
    ch->CSAR  = src_reg;
    ch->CDAR  = a_addr;
    ch->CLBAR = nodes_addr & DMA_CLBAR_LBA;
    ch->CLLR  = lli_link(upd, nodes_addr);
    ch->CCR   = DMA_CCR_TCIE | DMA_CCR_DTEIE | DMA_CCR_ULEIE | DMA_CCR_USEIE | DMA_CCR_PRIO_1;
    ch->CCR  |= DMA_CCR_EN;
}

АЦП запускает таймер, а не GPDMA. Запрос adc1_dma (номер 0) приходит на каждый готовый отсчёт; темп отсчётов задаёт внешний запуск АЦП — для ADC1 tim2_trgo стоит под номером 11 (adc_ext_trg11). В CFGR1 у ADC1 поле DMNGT = 11 — «DMA по кругу», EXTEN = 01 — запуск по фронту.

void adc_dma_arm(uint32_t psc, uint32_t arr)            /* АЦП уже включён, канал выбран */
{
    RCC->AHB1ENR  |= RCC_AHB1ENR_GPDMA1EN;
    RCC->APB1ENR1 |= RCC_APB1ENR1_TIM2EN;
    (void)RCC->APB1ENR1;

    adc_ready = 0;
    gpdma_pingpong(GPDMA1_Channel1, gpdma_tr1(GPDMA_W16, 0u, GPDMA_W16, 1u), REQ_ADC1,
                   (uint32_t)&ADC1->DR, (uint32_t)adc_a, (uint32_t)adc_b,
                   (uint16_t)(2u * ADC_N), adc_nodes, (uint32_t)adc_nodes);
    NVIC_SetPriority(GPDMA1_Channel1_IRQn, 6);
    NVIC_EnableIRQ(GPDMA1_Channel1_IRQn);

    /* запрос GPDMA даёт АЦП на каждый отсчёт; темп отсчётов — фронт TRGO таймера */
    ADC1->CFGR1 = (ADC1->CFGR1 & ~(ADC_CFGR1_DMNGT | ADC_CFGR1_EXTSEL | ADC_CFGR1_EXTEN | ADC_CFGR1_CONT))
                | (3u << ADC_CFGR1_DMNGT_Pos)                   /* 11: DMA по кругу */
                | (ADC_EXT_TIM2_TRGO << ADC_CFGR1_EXTSEL_Pos)
                | ADC_CFGR1_EXTEN_0;                            /* 01: по фронту */
    ADC1->CR |= ADC_CR_ADSTART;                         /* ждать запуска от таймера */

    TIM2->PSC = psc;
    TIM2->ARR = arr;
    TIM2->CR2 = TIM_CR2_MMS_1;                          /* TRGO по событию обновления */
    TIM2->EGR = TIM_EGR_UG;
    TIM2->CR1 = TIM_CR1_CEN;
}

void GPDMA1_Channel1_IRQHandler(void)                   /* TC после каждого блока */
{
    uint32_t sr = GPDMA1_Channel1->CSR;
    GPDMA1_Channel1->CFCR = sr & GPDMA_FLAGS;
    if (sr & GPDMA_ERRORS) {
        dma_errors++;
        return;
    }
    adc_ready = (adc_blocks & 1u) ? adc_b : adc_a;      /* чётные блоки — в A, нечётные — в B */
    adc_blocks++;
}

Буфер на 256 отсчётов по 16 бит — это BNDT = 512: счёт в байтах. Модель прогнала 1024 отсчёта: 4 блока, 4 загрузки по 2 слова — 8 слов; блоки легли в A, B, A, B без единой ошибки. HAL для той же пары буферов после сжатия оставляет первый элемент полным (6 слов), второй — в 3 слова (CxSAR, CxDAR, CxLLR): адреса HAL из элемента не убирает никогда, даже одинаковые. Второй элемент HAL и lli_pack с теми же битами совпали побайтно.

Таблица по триггеру. В 9.7 таблицу в регистр таймера переносил запрос TIMx_UP. У GPDMA это делается без запроса периферии: SWREQ = 1 и триггер на каждую пересылку.

/* Таблица -> регистр по триггеру, по кругу. Запроса периферии нет (SWREQ = 1): каждую пересылку
   разрешает один фронт триггера (TRIGM = 11). Полуслово таблицы дополняется нулями до слова
   регистра (PAM = 00). Элемент списка — одно слово: CxSAR, начало таблицы. */
void gpdma_table_out(DMA_Channel_TypeDef *ch, uint32_t trigsel, uint32_t dst_reg,
                     uint32_t tab_addr, uint16_t bytes, uint32_t *node, uint32_t node_addr)
{
    lli_regs_t r = {0};

    (void)gpdma_abort(ch);
    r.sar = tab_addr;
    (void)lli_pack(node, &r, DMA_CLLR_USA, 0u);

    ch->CTR1  = gpdma_tr1(GPDMA_W16, 1u, GPDMA_W32, 0u);
    ch->CTR2  = DMA_CTR2_SWREQ | gpdma_trig(trigsel, GPDMA_TRIG_RISING, GPDMA_TRIG_EACH);
    ch->CBR1  = bytes;
    ch->CSAR  = tab_addr;
    ch->CDAR  = dst_reg;
    ch->CLBAR = node_addr & DMA_CLBAR_LBA;
    ch->CLLR  = lli_link(DMA_CLLR_USA, node_addr);
    ch->CCR   = DMA_CCR_DTEIE | DMA_CCR_ULEIE | DMA_CCR_USEIE | DMA_CCR_TOIE;
    ch->CCR  |= DMA_CCR_EN;
}

Здесь кольцо «в другую сторону»: элемент возвращает на начало адрес источника, бит USA. Вызов — таблица pattern из восьми полуслов в регистр ODR порта B по tim2_trgo:

/* ── Таблица -> GPIOB->ODR по TRGO таймера TIM2: канал 2 GPDMA1 ───────── */
void pattern_start(void)
{
    RCC->AHB1ENR  |= RCC_AHB1ENR_GPDMA1EN;
    RCC->AHB2ENR1 |= RCC_AHB2ENR1_GPIOBEN;
    (void)RCC->AHB2ENR1;
    gpdma_table_out(GPDMA1_Channel2, TRIG_TIM2_TRGO, (uint32_t)&GPIOB->ODR,
                    (uint32_t)pattern, (uint16_t)sizeof pattern, pat_node, (uint32_t)pat_node);
}

Выводы порта B к этому моменту настроены на выход, а TIM2 запущен, как в adc_dma_arm: без фронтов TRGO канал стоит. В модели таблица из 8 полуслов и 20 фронтов триггера tim2_trgo (номер 42) дали 20 записей в регистр, 2 загрузки элемента; CxTR2 = 0x012AC200. Старшая половина каждого записанного слова — нули, хотя память вокруг таблицы была забита единицами.


9а.6 Флаги, остановка, LPDMA и TrustZone

Флаги — в CxSR своего канала, сброс — записью единицы в тот же бит CxFCR. Прерывание у каждого канала своё.

Флаг Бит Что случилось Канал после этого
IDLEF 0 канал стоит; после сброса — 1 —
TCF 8 передача завершена на уровне TCEM работает, если список не кончился
HTF 9 половина блока работает
DTEF 10 ошибка шины на данных выключен, нужен RESET
ULEF 11 ошибка шины при чтении элемента выключен, регистры списка писать заново
USEF 12 недопустимая настройка выключен, не начав
SUSPF 13 пауза наступила стоит, EN = 1
TOF 14 триггер потерян работает

USEF поднимают, например: BNDT = 0 без UB1; длина блока, не кратная ширине источника; адрес, не выровненный на ширину; ширина 3 (8 байт).

Выключить канал нельзя сбросом EN: запись нуля в этот бит RM0456 называет игнорируемой. EN сбрасывает только само железо — по концу списка и по ошибке — или сброс канала. Последовательность остановки (17.4.4):

/* Остановить канал — RM0456, 17.4.4. 0 — канал остановлен и сброшен, -1 — пауза не наступила. */
int gpdma_abort(DMA_Channel_TypeDef *ch)
{
    if (ch->CCR & DMA_CCR_EN) {
        uint32_t guard = 100000u;
        ch->CCR |= DMA_CCR_SUSP;                    /* 1. попросить паузу: EN сам не сбрасывается */
        while (!(ch->CSR & DMA_CSR_SUSPF)) {        /* 2. дождаться конца идущей пересылки */
            if (--guard == 0u)
                return -1;
        }
    }
    ch->CCR = DMA_CCR_RESET;                        /* 3. сброс: очередь, состояние, EN и SUSP */
    ch->CFCR = GPDMA_FLAGS;                         /*    флаги снимают записью единиц в CxFCR */
    return 0;                                       /* 4. дальше — заново CxBR1, CxSAR, CxDAR и EN */
}

Пауза без сброса — те же шаги 1 и 2, а продолжение — запись нуля в SUSP. На паузе можно спокойно читать BNDT и адреса: они не меняются. Запись RESET действует только на остановленный или стоящий на паузе канал, поэтому шаг 2 пропускать нельзя.

LPDMA1 — второй контроллер, в домене, который живёт в Stop 2 (10а). Регистры канала те же, код 9а.4 работает без правок:

void lpuart_ring_init(void)
{
    RCC->AHB3ENR |= RCC_AHB3ENR_LPDMA1EN;
    RCC->SRDAMR  |= RCC_SRDAMR_LPDMA1AMEN;              /* самостоятельная работа в Stop 0, 1 и 2 */
    (void)RCC->AHB3ENR;
    gpdma_ring_rx(LPDMA1_Channel0, LPREQ_LPUART1_RX, (uint32_t)&LPUART1->RDR,
                  (uint32_t)lp_buf, (uint16_t)sizeof lp_buf, lp_node, (uint32_t)lp_node);
}

/* ── Привилегии и TrustZone: только привилегированный код, до включения канала ── */
void gpdma_channel_privileged(uint32_t n)
{
    GPDMA1->PRIVCFGR |= 1u << n;                        /* PRIVn: канал виден только привилегированному коду */
#if defined(__ARM_FEATURE_CMSE) && (__ARM_FEATURE_CMSE == 3U)
    GPDMA1->SECCFGR  |= 1u << n;                        /* SECn: канал принадлежит защищённому миру */
    GPDMA1->RCFGLOCKR |= 1u << n;                       /* LOCKn: до сброса атрибуты не изменить */
#endif
}
GPDMA1 LPDMA1
каналов 16 4
порты AHB два один
очередь 8 или 32 байта нет
пакеты до 64 пересылок только одиночные
двумерная адресация каналы 12…15 нет
PAM с упаковкой без упаковки
REQSEL / TRIGSEL 7 бит 5 бит
запросы таблица 136 17 запросов: LPUART1, SPI3, I2C3, ADC4, DAC1, ADF1, LPTIM1, LPTIM3
сам работает в Sleep, Stop 0, Stop 1 Sleep, Stop 0, Stop 1, Stop 2

Номера запросов у LPDMA1 свои: lpuart1_rx_dma там 0, а не 34, как у GPDMA1; adc4_dma — 7. В Stop канал сам просит такт у RCC, когда пришёл запрос или триггер, и отпускает его после пересылки; будит ядро обычное прерывание канала.

TrustZone и привилегии. Три общих регистра по биту на канал: SECCFGR — канал защищённый, PRIVCFGR — привилегированный, RCFGLOCKR — заморозить оба атрибута до сброса. Чужое чтение регистров такого канала возвращает ноль, чужая запись ничего не делает. Менять атрибуты можно только при выключенном канале. У защищённого канала биты SSEC и DSEC в CxTR1 отдельно задают, защищённым ли обращением читать источник и писать приёмник, а элементы списка он читает защищённым обращением всегда. Запросы прерываний разведены по двум регистрам: MISR — незащищённые каналы, SMISR — защищённые. Если TrustZone выключена (TZEN = 0, раздел 2б), RCFGLOCKR не пишется.


9а.7 Грабли

Канал включён и молчит, USEF = 1. Чаще всего — длина в BNDT посчитана в элементах, а не в байтах, и не делится на ширину; или адрес буфера uint8_t отдан под 16-битные пересылки. У F4 то же самое давало сдвинутые данные, здесь — отказ начинать.

Принята половина ожидаемого. BNDT = числу отсчётов при 16-битной ширине: 256 отсчётов — это 512 байт.

Кольцо остановилось после первого круга. В CxLLR ноль или элемент собран не по тем битам: CxLLR говорит «в элементе CxDAR», а в памяти лежит полный элемент — в CxDAR попало значение CxTR1. Число слов в элементе обязано равняться числу взведённых бит.

ULEF сразу после первого блока. Элемент лежит не там: CxLBAR не записан или элементы списка в разных окнах 64 КБ.

Список в стеке. Элемент объявлен локальной переменной. Канал читает его после каждого круга — через секунду там другие данные. Элементы списка — статические, как и буферы (9.9).

EN не сбрасывается. Запись нуля в EN игнорируется. Только SUSP → SUSPF → RESET.

После RESET канал перезапущен, а пишет не туда. CxBR1, CxSAR и CxDAR железо меняло по ходу работы; после сброса их пишут заново.

Один запрос на двух каналах. Ошибки настройки не будет — будут пропавшие данные. Следить самому.

Триггеры теряются. TOF поднимается, когда второй триггер пришёл, пока первая пересылка не кончилась. Канал при этом работает дальше, и без TOIE потерю не видно.

Номер запроса от GPDMA записан в LPDMA. Таблицы разные, а поле одинаковое. lpuart1_rx_dma: 34 у GPDMA1 и 0 у LPDMA1.

Позиция по BNDT на миг впереди памяти. BNDT считает прочитанное из источника; байт может ещё лежать в очереди канала. К моменту прерывания IDLE — через целый символ — он давно в памяти. HAL считает позицию так же, по BNDT. На железе не проверялось.


9а.8 Итоги

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

gpdma.h — типы и константы GPDMA и LPDMA: элемент связного списка, кольцо, остановка канала.

/* Раздел 9а: GPDMA и LPDMA STM32U5 — элемент связного списка, кольцевой приём, остановка канала.
   Регистры канала — DMA_Channel_TypeDef из stm32u585xx.h; раскладка элемента в памяти — RM0456, 17.4.5.
   Адреса передаются числами uint32_t: тот же код собирается и для модели на компьютере. */
#ifndef GPDMA_H
#define GPDMA_H
#include <stdint.h>
#include "stm32u5xx.h"

/* Значения регистров одного элемента списка — в том порядке, в каком GPDMA читает их из памяти */
typedef struct {
    uint32_t tr1, tr2, br1, sar, dar;   /* есть у всех каналов */
    uint32_t tr3, br2;                  /* только каналы 12…15: двумерная адресация */
} lli_regs_t;

/* Биты обновления CxLLR: какие регистры лежат в следующем элементе */
#define LLI_UPD_LINEAR (DMA_CLLR_UT1 | DMA_CLLR_UT2 | DMA_CLLR_UB1 | DMA_CLLR_USA | DMA_CLLR_UDA | DMA_CLLR_ULL)
#define LLI_UPD_2D     (LLI_UPD_LINEAR | DMA_CLLR_UT3 | DMA_CLLR_UB2)

#define GPDMA_FLAGS    (DMA_CFCR_TCF | DMA_CFCR_HTF | DMA_CFCR_DTEF | DMA_CFCR_ULEF | DMA_CFCR_USEF \
                        | DMA_CFCR_SUSPF | DMA_CFCR_TOF)
#define GPDMA_ERRORS   (DMA_CSR_DTEF | DMA_CSR_ULEF | DMA_CSR_USEF)

#define GPDMA_W8   0u                   /* SDW_LOG2 / DDW_LOG2: двоичный логарифм ширины в байтах */
#define GPDMA_W16  1u
#define GPDMA_W32  2u
#define GPDMA_TRIG_RISING 1u            /* TRIGPOL */
#define GPDMA_TRIG_BLOCK  0u            /* TRIGM: триггер разрешает блок */
#define GPDMA_TRIG_LINK   2u            /*        загрузку следующего элемента */
#define GPDMA_TRIG_EACH   3u            /*        каждую пересылку */

uint32_t lli_words(uint32_t upd);
uint32_t lli_pack(uint32_t *mem, const lli_regs_t *r, uint32_t upd, uint32_t llr);
uint32_t lli_link(uint32_t upd, uint32_t addr);
uint32_t gpdma_tr1(uint32_t sdw_log2, uint32_t sinc, uint32_t ddw_log2, uint32_t dinc);
uint32_t gpdma_trig(uint32_t trigsel, uint32_t pol, uint32_t mode);
int      gpdma_abort(DMA_Channel_TypeDef *ch);
void     gpdma_ring_rx(DMA_Channel_TypeDef *ch, uint32_t reqsel, uint32_t src_reg,
                       uint32_t buf_addr, uint16_t size, uint32_t *node, uint32_t node_addr);
uint16_t gpdma_ring_wr(const DMA_Channel_TypeDef *ch, uint16_t size);
void     gpdma_pingpong(DMA_Channel_TypeDef *ch, uint32_t tr1, uint32_t tr2, uint32_t src_reg,
                        uint32_t a_addr, uint32_t b_addr, uint16_t bytes, uint32_t *nodes, uint32_t nodes_addr);
void     gpdma_table_out(DMA_Channel_TypeDef *ch, uint32_t trigsel, uint32_t dst_reg,
                         uint32_t tab_addr, uint16_t bytes, uint32_t *node, uint32_t node_addr);

#endif

gpdma.c — построители элементов списка, полей CxTR1/CxTR2 и запуск и остановка канала.

/* Раздел 9а: GPDMA и LPDMA STM32U5 регистрами. Один файл на оба контроллера: каналы LPDMA1 —
   тот же DMA_Channel_TypeDef. Порядок действий — RM0456, 17.4.4, 17.4.5 и 17.4.13. */
#include "gpdma.h"

/* Сколько 32-битных слов занимает элемент, который описывают биты обновления upd */
uint32_t lli_words(uint32_t upd)
{
    uint32_t n = 0;
    for (upd &= LLI_UPD_2D; upd != 0u; upd &= upd - 1u)
        n++;
    return n;
}

/* Собрать элемент в памяти. upd — биты Uxx из CxLLR ПРЕДЫДУЩЕГО элемента (для первого — из самого
   регистра CxLLR): они говорят, какие регистры лежат в этом элементе. Слова идут подряд, без
   пропусков, в порядке TR1, TR2, BR1, SAR, DAR, TR3, BR2, LLR. llr — то, что попадёт в CxLLR:
   описание следующего элемента. Возвращает число записанных слов. */
uint32_t lli_pack(uint32_t *mem, const lli_regs_t *r, uint32_t upd, uint32_t llr)
{
    uint32_t n = 0;
    if (upd & DMA_CLLR_UT1) mem[n++] = r->tr1;
    if (upd & DMA_CLLR_UT2) mem[n++] = r->tr2;
    if (upd & DMA_CLLR_UB1) mem[n++] = r->br1;
    if (upd & DMA_CLLR_USA) mem[n++] = r->sar;
    if (upd & DMA_CLLR_UDA) mem[n++] = r->dar;
    if (upd & DMA_CLLR_UT3) mem[n++] = r->tr3;
    if (upd & DMA_CLLR_UB2) mem[n++] = r->br2;
    if (upd & DMA_CLLR_ULL) mem[n++] = llr;
    return n;
}

/* Значение CxLLR: какие регистры обновить (upd) и где лежит элемент — младшие 16 бит адреса.
   Старшие 16 бит держит CxLBAR, один на весь список: список целиком лежит в одном окне 64 КБ. */
uint32_t lli_link(uint32_t upd, uint32_t addr)
{
    return (upd & LLI_UPD_2D) | (addr & DMA_CLLR_LA);
}

/* CxTR1: ширина и приращение адреса источника и приёмника; пакет — одна пересылка, PAM = 00 */
uint32_t gpdma_tr1(uint32_t sdw_log2, uint32_t sinc, uint32_t ddw_log2, uint32_t dinc)
{
    return ((sdw_log2 << DMA_CTR1_SDW_LOG2_Pos) & DMA_CTR1_SDW_LOG2) | (sinc ? DMA_CTR1_SINC : 0u)
         | ((ddw_log2 << DMA_CTR1_DDW_LOG2_Pos) & DMA_CTR1_DDW_LOG2) | (dinc ? DMA_CTR1_DINC : 0u);
}

/* Поля триггера в CxTR2: номер входа, фронт, что именно триггер разрешает */
uint32_t gpdma_trig(uint32_t trigsel, uint32_t pol, uint32_t mode)
{
    return ((trigsel << DMA_CTR2_TRIGSEL_Pos) & DMA_CTR2_TRIGSEL)
         | ((pol << DMA_CTR2_TRIGPOL_Pos) & DMA_CTR2_TRIGPOL)
         | ((mode << DMA_CTR2_TRIGM_Pos) & DMA_CTR2_TRIGM);
}

/* Остановить канал — RM0456, 17.4.4. 0 — канал остановлен и сброшен, -1 — пауза не наступила. */
int gpdma_abort(DMA_Channel_TypeDef *ch)
{
    if (ch->CCR & DMA_CCR_EN) {
        uint32_t guard = 100000u;
        ch->CCR |= DMA_CCR_SUSP;                    /* 1. попросить паузу: EN сам не сбрасывается */
        while (!(ch->CSR & DMA_CSR_SUSPF)) {        /* 2. дождаться конца идущей пересылки */
            if (--guard == 0u)
                return -1;
        }
    }
    ch->CCR = DMA_CCR_RESET;                        /* 3. сброс: очередь, состояние, EN и SUSP */
    ch->CFCR = GPDMA_FLAGS;                         /*    флаги снимают записью единиц в CxFCR */
    return 0;                                       /* 4. дальше — заново CxBR1, CxSAR, CxDAR и EN */
}

/* Приём в кольцо: периферия -> память, байт на запрос, по кругу без участия программы.
   Список — один элемент в одно слово: после каждого круга GPDMA читает из node новый CxDAR,
   начало кольца. CxBR1 он восстанавливает сам, а CxLLR не обновляется (ULL = 0) и продолжает
   указывать на тот же элемент. Это замена биту CIRC у F1 и F4. */
void gpdma_ring_rx(DMA_Channel_TypeDef *ch, uint32_t reqsel, uint32_t src_reg,
                   uint32_t buf_addr, uint16_t size, uint32_t *node, uint32_t node_addr)
{
    lli_regs_t r = {0};

    (void)gpdma_abort(ch);
    r.dar = buf_addr;
    (void)lli_pack(node, &r, DMA_CLLR_UDA, 0u);     /* в элементе одно слово — CxDAR */

    ch->CTR1  = gpdma_tr1(GPDMA_W8, 0u, GPDMA_W8, 1u);  /* байт -> байт, растёт адрес приёмника */
    ch->CTR2  = reqsel & DMA_CTR2_REQSEL;           /* запрос даёт источник: DREQ = 0; TCEM = 00 */
    ch->CBR1  = size;                               /* длина блока — в БАЙТАХ */
    ch->CSAR  = src_reg;
    ch->CDAR  = buf_addr;
    ch->CLBAR = node_addr & DMA_CLBAR_LBA;          /* старшие 16 бит адреса списка */
    ch->CLLR  = lli_link(DMA_CLLR_UDA, node_addr);  /* обновлять только CxDAR, брать по node_addr */
    ch->CCR   = DMA_CCR_HTIE | DMA_CCR_TCIE | DMA_CCR_DTEIE | DMA_CCR_ULEIE | DMA_CCR_USEIE
              | DMA_CCR_PRIO_1;                     /* LSM = 0: список идёт сам */
    ch->CCR  |= DMA_CCR_EN;
}

/* Куда GPDMA положит следующий байт кольца: BNDT — сколько байт блока осталось, как NDTR у F4 */
uint16_t gpdma_ring_wr(const DMA_Channel_TypeDef *ch, uint16_t size)
{
    uint16_t left = (uint16_t)(ch->CBR1 & DMA_CBR1_BNDT);
    if (left == 0u || left > size)                  /* BNDT = 0 — миг между концом круга и загрузкой элемента */
        return 0u;
    return (uint16_t)(size - left);
}

/* Два буфера по очереди — замена двойной буферизации F4. Два элемента по два слова: CxDAR и
   CxLLR. Первый блок идёт по регистрам в буфер A, дальше B, A, B ... Событие TC — после каждого
   блока: заполненный буфер можно обрабатывать, пока пишется второй. */
void gpdma_pingpong(DMA_Channel_TypeDef *ch, uint32_t tr1, uint32_t tr2, uint32_t src_reg,
                    uint32_t a_addr, uint32_t b_addr, uint16_t bytes, uint32_t *nodes, uint32_t nodes_addr)
{
    const uint32_t upd = DMA_CLLR_UDA | DMA_CLLR_ULL;
    lli_regs_t r = {0};
    uint32_t n;

    (void)gpdma_abort(ch);
    r.dar = b_addr;                                 /* элемент 0: писать в B, за ним — элемент 1 */
    n = lli_pack(&nodes[0], &r, upd, lli_link(upd, nodes_addr + 4u * lli_words(upd)));
    r.dar = a_addr;                                 /* элемент 1: писать в A, за ним — элемент 0 */
    (void)lli_pack(&nodes[n], &r, upd, lli_link(upd, nodes_addr));

    ch->CTR1  = tr1;
    ch->CTR2  = tr2;
    ch->CBR1  = bytes;
    ch->CSAR  = src_reg;
    ch->CDAR  = a_addr;
    ch->CLBAR = nodes_addr & DMA_CLBAR_LBA;
    ch->CLLR  = lli_link(upd, nodes_addr);
    ch->CCR   = DMA_CCR_TCIE | DMA_CCR_DTEIE | DMA_CCR_ULEIE | DMA_CCR_USEIE | DMA_CCR_PRIO_1;
    ch->CCR  |= DMA_CCR_EN;
}

/* Таблица -> регистр по триггеру, по кругу. Запроса периферии нет (SWREQ = 1): каждую пересылку
   разрешает один фронт триггера (TRIGM = 11). Полуслово таблицы дополняется нулями до слова
   регистра (PAM = 00). Элемент списка — одно слово: CxSAR, начало таблицы. */
void gpdma_table_out(DMA_Channel_TypeDef *ch, uint32_t trigsel, uint32_t dst_reg,
                     uint32_t tab_addr, uint16_t bytes, uint32_t *node, uint32_t node_addr)
{
    lli_regs_t r = {0};

    (void)gpdma_abort(ch);
    r.sar = tab_addr;
    (void)lli_pack(node, &r, DMA_CLLR_USA, 0u);

    ch->CTR1  = gpdma_tr1(GPDMA_W16, 1u, GPDMA_W32, 0u);
    ch->CTR2  = DMA_CTR2_SWREQ | gpdma_trig(trigsel, GPDMA_TRIG_RISING, GPDMA_TRIG_EACH);
    ch->CBR1  = bytes;
    ch->CSAR  = tab_addr;
    ch->CDAR  = dst_reg;
    ch->CLBAR = node_addr & DMA_CLBAR_LBA;
    ch->CLLR  = lli_link(DMA_CLLR_USA, node_addr);
    ch->CCR   = DMA_CCR_DTEIE | DMA_CCR_ULEIE | DMA_CCR_USEIE | DMA_CCR_TOIE;
    ch->CCR  |= DMA_CCR_EN;
}

gpdma_u585.c — примеры на STM32U585: приём USART1 в кольцо, АЦП по таймеру, таблица в порт, LPUART1 через LPDMA1.

/* Раздел 9а: STM32U585 — приём USART1 в кольцо через GPDMA1, АЦП по таймеру в два буфера,
   таблица в порт по триггеру, LPUART1 через LPDMA1. Такт 160 МГц и выводы — как в разделе 2д;
   включение и калибровка АЦП здесь не показаны. Номера запросов и триггеров — RM0456,
   таблицы 136, 138 и 146; внешний запуск ADC1 от TIM2 — таблица внешних запусков ADC12. */
#include "gpdma.h"
#include "uring.h"

#define PCLK2_HZ          160000000u
#define BAUD              921600u
#define RX_SIZE           512u
#define ADC_N             256u              /* отсчётов в одном буфере */
#define PAT_N             8u

#define REQ_ADC1          0u                /* GPDMA1: adc1_dma */
#define REQ_USART1_RX     24u               /* GPDMA1: usart1_rx_dma */
#define TRIG_TIM2_TRGO    42u               /* GPDMA1: tim2_trgo */
#define LPREQ_LPUART1_RX  0u                /* LPDMA1: lpuart1_rx_dma */
#define ADC_EXT_TIM2_TRGO 11u               /* ADC1: adc_ext_trg11 = tim2_trgo */

_Static_assert(DMA_CTR2_REQSEL_Msk == 0x7Fu, "REQSEL — биты 6:0");
_Static_assert(DMA_CTR2_SWREQ_Pos == 9 && DMA_CTR2_DREQ_Pos == 10 && DMA_CTR2_BREQ_Pos == 11, "SWREQ, DREQ, BREQ");
_Static_assert(DMA_CTR2_TRIGM_Pos == 14 && DMA_CTR2_TRIGSEL_Pos == 16 && DMA_CTR2_TRIGPOL_Pos == 24, "поля триггера");
_Static_assert(DMA_CTR2_TRIGSEL_Msk == (0x3Fu << 16), "в заголовке U585 TRIGSEL — 6 бит");
_Static_assert(DMA_CTR2_TCEM_Pos == 30, "TCEM — биты 31:30");
_Static_assert(DMA_CTR1_SDW_LOG2_Pos == 0 && DMA_CTR1_SINC_Pos == 3 && DMA_CTR1_SBL_1_Pos == 4, "источник в CxTR1");
_Static_assert(DMA_CTR1_PAM_Pos == 11 && DMA_CTR1_DDW_LOG2_Pos == 16 && DMA_CTR1_DINC_Pos == 19, "PAM и приёмник");
_Static_assert(DMA_CTR1_DBL_1_Pos == 20 && DMA_CTR1_SSEC_Pos == 15 && DMA_CTR1_DSEC_Pos == 31, "DBL_1, SSEC, DSEC");
_Static_assert(DMA_CBR1_BNDT_Msk == 0xFFFFu && DMA_CBR1_BRC_Msk == (0x7FFu << 16), "BNDT 16 бит, BRC 11 бит");
_Static_assert(DMA_CLLR_UT1_Pos == 31 && DMA_CLLR_UT2_Pos == 30 && DMA_CLLR_UB1_Pos == 29, "UT1, UT2, UB1");
_Static_assert(DMA_CLLR_USA_Pos == 28 && DMA_CLLR_UDA_Pos == 27 && DMA_CLLR_ULL_Pos == 16, "USA, UDA, ULL");
_Static_assert(DMA_CLLR_UT3_Pos == 26 && DMA_CLLR_UB2_Pos == 25, "UT3, UB2 — каналы 12…15");
_Static_assert(DMA_CLLR_LA_Msk == 0xFFFCu && DMA_CLBAR_LBA_Msk == 0xFFFF0000u, "LA 15:2, LBA 31:16");
_Static_assert(DMA_CSR_IDLEF_Pos == 0 && DMA_CSR_TCF_Pos == 8 && DMA_CSR_HTF_Pos == 9 && DMA_CSR_DTEF_Pos == 10, "флаги");
_Static_assert(DMA_CSR_ULEF_Pos == 11 && DMA_CSR_USEF_Pos == 12 && DMA_CSR_SUSPF_Pos == 13 && DMA_CSR_TOF_Pos == 14, "флаги");
_Static_assert(DMA_CCR_EN_Pos == 0 && DMA_CCR_RESET_Pos == 1 && DMA_CCR_SUSP_Pos == 2, "EN, RESET, SUSP");
_Static_assert(DMA_CCR_LSM_Pos == 16 && DMA_CCR_LAP_Pos == 17 && DMA_CCR_PRIO_Pos == 22, "LSM, LAP, PRIO");
_Static_assert(RX_SIZE <= 0xFFFFu && 2u * ADC_N <= 0xFFFFu, "блок — не больше 65 535 байт");
_Static_assert((PCLK2_HZ + BAUD / 2u) / BAUD == 174u, "BRR на 921 600 бод при 160 МГц");

static uint8_t  rx_buf[RX_SIZE];
static uint32_t rx_node[1];                 /* элемент списка кольца: одно слово */
static uring_t  rx;
static uint16_t adc_a[ADC_N], adc_b[ADC_N];
static uint32_t adc_nodes[4];               /* два элемента по два слова */
static const uint16_t pattern[PAT_N] = {0x0001u, 0x0003u, 0x0007u, 0x000Fu, 0x000Eu, 0x000Cu, 0x0008u, 0x0000u};
static uint32_t pat_node[1];
static uint8_t  lp_buf[64];
static uint32_t lp_node[1];

volatile uint32_t rx_frames, dma_errors, uart_ore, adc_blocks;
volatile const uint16_t *adc_ready;         /* буфер, который можно обрабатывать */

static void app_frame(const uint8_t *a, uint16_t na, const uint8_t *b, uint16_t nb)
{
    (void)a; (void)na; (void)b; (void)nb;
    rx_frames++;
}

/* ── USART1 -> кольцо: канал 0 GPDMA1 ─────────────────────────────────── */
static void uart_ring_arm(void)
{
    gpdma_ring_rx(GPDMA1_Channel0, REQ_USART1_RX, (uint32_t)&USART1->RDR,
                  (uint32_t)rx_buf, RX_SIZE, rx_node, (uint32_t)rx_node);
}

void uart_ring_init(void)
{
    RCC->AHB1ENR |= RCC_AHB1ENR_GPDMA1EN;
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
    (void)RCC->APB2ENR;

    uring_init(&rx, rx_buf, RX_SIZE, app_frame);
    uart_ring_arm();                                    /* сначала канал, потом запросы */

    USART1->BRR = (PCLK2_HZ + BAUD / 2u) / BAUD;        /* 174: 919 540 бод */
    USART1->CR3 = USART_CR3_DMAR | USART_CR3_EIE;
    USART1->CR1 = USART_CR1_UE | USART_CR1_RE | USART_CR1_IDLEIE;

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

static void rx_poll(void)
{
    uring_poll(&rx, (uint16_t)(GPDMA1_Channel0->CBR1 & DMA_CBR1_BNDT));
}

void GPDMA1_Channel0_IRQHandler(void)                   /* HT, TC или ошибка */
{
    uint32_t sr = GPDMA1_Channel0->CSR;
    GPDMA1_Channel0->CFCR = sr & GPDMA_FLAGS;           /* сброс — записью единиц */
    if (sr & GPDMA_ERRORS) {                            /* канал уже выключен железом */
        dma_errors++;
        rx.rd = 0;
        uart_ring_arm();                                /* сброс канала и запуск с начала кольца */
        return;
    }
    rx_poll();
}

void USART1_IRQHandler(void)
{
    uint32_t isr = USART1->ISR;
    if (isr & USART_ISR_ORE)
        uart_ore++;
    /* у U5 флаги снимают записью в ICR: регистр данных не читают, байт у GPDMA не отнять */
    USART1->ICR = USART_ICR_IDLECF | USART_ICR_ORECF | USART_ICR_NECF | USART_ICR_FECF;
    if (isr & USART_ISR_IDLE)
        rx_poll();
}

/* ── ADC1 по TIM2 -> два буфера: канал 1 GPDMA1 ───────────────────────── */
void adc_dma_arm(uint32_t psc, uint32_t arr)            /* АЦП уже включён, канал выбран */
{
    RCC->AHB1ENR  |= RCC_AHB1ENR_GPDMA1EN;
    RCC->APB1ENR1 |= RCC_APB1ENR1_TIM2EN;
    (void)RCC->APB1ENR1;

    adc_ready = 0;
    gpdma_pingpong(GPDMA1_Channel1, gpdma_tr1(GPDMA_W16, 0u, GPDMA_W16, 1u), REQ_ADC1,
                   (uint32_t)&ADC1->DR, (uint32_t)adc_a, (uint32_t)adc_b,
                   (uint16_t)(2u * ADC_N), adc_nodes, (uint32_t)adc_nodes);
    NVIC_SetPriority(GPDMA1_Channel1_IRQn, 6);
    NVIC_EnableIRQ(GPDMA1_Channel1_IRQn);

    /* запрос GPDMA даёт АЦП на каждый отсчёт; темп отсчётов — фронт TRGO таймера */
    ADC1->CFGR1 = (ADC1->CFGR1 & ~(ADC_CFGR1_DMNGT | ADC_CFGR1_EXTSEL | ADC_CFGR1_EXTEN | ADC_CFGR1_CONT))
                | (3u << ADC_CFGR1_DMNGT_Pos)                   /* 11: DMA по кругу */
                | (ADC_EXT_TIM2_TRGO << ADC_CFGR1_EXTSEL_Pos)
                | ADC_CFGR1_EXTEN_0;                            /* 01: по фронту */
    ADC1->CR |= ADC_CR_ADSTART;                         /* ждать запуска от таймера */

    TIM2->PSC = psc;
    TIM2->ARR = arr;
    TIM2->CR2 = TIM_CR2_MMS_1;                          /* TRGO по событию обновления */
    TIM2->EGR = TIM_EGR_UG;
    TIM2->CR1 = TIM_CR1_CEN;
}

void GPDMA1_Channel1_IRQHandler(void)                   /* TC после каждого блока */
{
    uint32_t sr = GPDMA1_Channel1->CSR;
    GPDMA1_Channel1->CFCR = sr & GPDMA_FLAGS;
    if (sr & GPDMA_ERRORS) {
        dma_errors++;
        return;
    }
    adc_ready = (adc_blocks & 1u) ? adc_b : adc_a;      /* чётные блоки — в A, нечётные — в B */
    adc_blocks++;
}

/* ── Таблица -> GPIOB->ODR по TRGO таймера TIM2: канал 2 GPDMA1 ───────── */
void pattern_start(void)
{
    RCC->AHB1ENR  |= RCC_AHB1ENR_GPDMA1EN;
    RCC->AHB2ENR1 |= RCC_AHB2ENR1_GPIOBEN;
    (void)RCC->AHB2ENR1;
    gpdma_table_out(GPDMA1_Channel2, TRIG_TIM2_TRGO, (uint32_t)&GPIOB->ODR,
                    (uint32_t)pattern, (uint16_t)sizeof pattern, pat_node, (uint32_t)pat_node);
}

/* ── LPUART1 -> кольцо через LPDMA1: тот же код, другой контроллер ────── */
void lpuart_ring_init(void)
{
    RCC->AHB3ENR |= RCC_AHB3ENR_LPDMA1EN;
    RCC->SRDAMR  |= RCC_SRDAMR_LPDMA1AMEN;              /* самостоятельная работа в Stop 0, 1 и 2 */
    (void)RCC->AHB3ENR;
    gpdma_ring_rx(LPDMA1_Channel0, LPREQ_LPUART1_RX, (uint32_t)&LPUART1->RDR,
                  (uint32_t)lp_buf, (uint16_t)sizeof lp_buf, lp_node, (uint32_t)lp_node);
}

/* ── Привилегии и TrustZone: только привилегированный код, до включения канала ── */
void gpdma_channel_privileged(uint32_t n)
{
    GPDMA1->PRIVCFGR |= 1u << n;                        /* PRIVn: канал виден только привилегированному коду */
#if defined(__ARM_FEATURE_CMSE) && (__ARM_FEATURE_CMSE == 3U)
    GPDMA1->SECCFGR  |= 1u << n;                        /* SECn: канал принадлежит защищённому миру */
    GPDMA1->RCFGLOCKR |= 1u << n;                       /* LOCKn: до сброса атрибуты не изменить */
#endif
}

Дальше: раздел 10 «Питание и энергосбережение» — режимы сна, пробуждение и что происходит с периферией, пока камень спит.

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