Embedded HandbookПриложения
Все разделы › XXV. Мини-проекты
STM32F407 · уровень средний · 19 мин чтения
Открыть в интерактивном справочнике

198к. Мини-проект: осциллограф на плате — АЦП, DMA, запуск по фронту и экран на ПК

В разделе 198д плата стала логическим анализатором: восемь цифровых входов, таймер и DMA. Здесь — аналоговый собрат: одноканальный осциллограф на F407. Таймер задаёт частоту выборки 1 Мвыб/с, АЦП пишет отсчёты через DMA в двойной буфер, ядро ищет фронт с гистерезисом и собирает кадр из 1024 отсчётов с предзаписью, при медленной развёртке прореживает сигнал, а кадр с CRC уходит по UART на компьютер, где программа на Python проверяет его, меряет частоту и рисует картинку.

Всё проверено без платы. Ядро осциллографа (scope_core.c) не трогает железо, поэтому одна и та же программа-опыт sim_scope.c собрана gcc на ПК и arm-none-eabi-gcc для Cortex-M4 и прогнана в qemu-system-arm -M netduinoplus2 (вывод через semihosting, раздел 35а): отсчёты вместо АЦП даёт генератор с целочисленной арифметикой. Вывод ПК и QEMU совпал строка в строку. Прошивка с настройкой TIM2, ADC1, DMA2 и USART1 (scope_f407.c) собрана armclang по заголовкам пака F4 без предупреждений; на железе не запускалась.

Опоры: таблица 67 «ADC characteristics» и формула 1 даташита DS8626 (STM32F405/407), RM0090 — выбор запуска EXTSEL, делитель ADCPRE, таблица 43 запросов DMA2, режим двойного буфера (10.3.9); DMA — раздел 9, таймеры — 13, АЦП — 14, потолок USB CDC — раздел 198б. Проверка — _verify/scope198k_check.py.

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

Оригинал Перевод
sampling rate частота выборки, отсчётов в секунду
external trigger (EXTSEL, EXTEN) внешний запуск преобразования: от чего и по какому фронту
TRGO выход запуска таймера для другой периферии
double buffer mode (DBM), current target (CT) двойной буфер DMA и бит «куда пишем сейчас»
trigger level, hysteresis порог запуска и гистерезис
pre-trigger предзапись: отсчёты до момента запуска
auto trigger «авто»: кадр по таймауту, если фронта нет
decimation прореживание: из D отсчётов один
aliasing наложение спектров: высокая частота выглядит низкой
dead time мёртвое время: сигнал идёт, а прибор его не смотрит

198к.1 Что делает прибор и из чего он

Функция Как
вход PA1 = ADC123_IN1, 0…3,3 В, 12 бит
частота выборки TIM2 84 МГц / 84 = 1 МГц → TRGO → запуск ADC1
перенос отсчётов DMA2 Stream0, канал 0, двойной буфер по 512 отсчётов
кадр 1024 отсчёта, из них 256 до запуска
запуск по фронту через порог с гистерезисом; «авто» по таймауту
медленная развёртка прореживание в D раз: среднее или каждый D-й
связь с ПК кадр 1550 байт с CRC-16, USART1 2 Мбод через DMA2 Stream7
на ПК scope_host.py: проверка, распаковка, частота, размах, PNG

Путь отсчёта в осциллографе на F407: TIM2 по TRGO запускает ADC1 на PA1 с частотой 1 МГц, DMA2 Stream0 пишет в двойной буфер по 512 отсчётов, прерывание по готовой половине отдаёт её ядру scope_core с кольцом на 1024 отсчёта и поиском фронта, кадр 1550 байт уходит через DMA2 Stream7 в USART1 на 2 Мбод и на ПК в scope_host.py; внизу — кадр из 256 отсчётов предзаписи и 768 после запуска

Прибор «пока прибора нет», как и анализатор из 198д: полоса — сотни килогерц, канал один, щупа с делителем и защитой входа нет — вход ограничен 0…3,3 В. Зато внутри всё видно: откуда берётся потолок частоты выборки, как не потерять начало сигнала и почему прореживать надо средним, а не выбрасыванием отсчётов.


198к.2 Частота выборки, полоса и разрешение: считаем по даташиту

Таблица 67 DS8626 даёт для АЦП F407 при питании 2,4…3,6 В тактовую частоту fADC не выше 36 МГц. Такт АЦП получается из PCLK2 делителем ADCPRE — 2, 4, 6 или 8. При PCLK2 = 84 МГц:

Делитель fADC Годится?
2 42 МГц нет: выше 36 МГц
4 21 МГц да — выбран
6 14 МГц да
8 10,5 МГц да

Преобразование 12 бит — это время выборки плюс 12 тактов. Самая короткая выборка — 3 такта, всего 15 тактов: при 21 МГц — 1,4 Мвыб/с подряд. Но по таймеру быстрее не выйдет: в той же таблице внешний запуск не чаще fADC / 17, то есть 1,235 Мвыб/с. Выбрано 1 Мвыб/с — ровно 84 такта таймера. Для сравнения, даташит при fADC = 30 МГц пишет 2 Мвыб/с для одного АЦП, 3,75 — для двух вперемежку и 6 — для трёх; при PCLK2 = 84 МГц 30 МГц не получить — только 21.

Короткая выборка требует сильного источника. Формула 1 DS8626 даёт наибольшее сопротивление источника, при котором ошибка меньше четверти младшего разряда: RAIN = (k − 0,5) / (fADC · CADC · ln 2^(N+2)) − RADC, где CADC = 4 пФ, RADC = 6 кОм (таблица 67). При 21 МГц:

Выборка, тактов RAIN не больше Потолок частоты выборки
3 меньше нуля 1400 кГц
15 11 788 Ом 777,8 кГц
28 27 736 Ом 525,0 кГц

При 3 тактах формула даёт отрицательное число: четверть разряда не обещана ни при каком источнике. Остаётся источник с самым малым сопротивлением — повторитель на ОУ — и ошибка больше четверти разряда, или выборка длиннее — ценой частоты.

Разрешение: младший разряд при опорном 3,3 В — 0,806 мВ; полная нескорректированная ошибка по таблице 68 (при fADC = 30 МГц) — до ±5 разрядов, то есть до ±4,0 мВ. Полоса: при 1 Мвыб/с теорема Котельникова ставит предел 500 кГц, но чтобы на экране была форма, а не точки, нужно хотя бы десять отсчётов на период — это 100 кГц.


198к.3 Захват: таймер, АЦП и двойной буфер DMA

Цепочка настраивается один раз. TIM2 переполняется раз в микросекунду и выдаёт TRGO; АЦП по фронту TRGO (EXTSEL = 0110 по RM0090) делает одно преобразование и просит DMA забрать результат. ADC1 обслуживает DMA2 Stream0, канал 0 (таблица 43 RM0090):

    DMA2_Stream0->PAR = (uint32_t)&ADC1->DR;
    DMA2_Stream0->M0AR = (uint32_t)buf[0];
    DMA2_Stream0->M1AR = (uint32_t)buf[1];
    DMA2_Stream0->NDTR = HALF;
    DMA2_Stream0->CR = (0u << DMA_SxCR_CHSEL_Pos)                         /* канал 0 потока 0 — ADC1 */
                     | DMA_SxCR_DBM | DMA_SxCR_PL_1 | DMA_SxCR_MSIZE_0 | DMA_SxCR_PSIZE_0
                     | DMA_SxCR_MINC | DMA_SxCR_TCIE;

В режиме двойного буфера (RM0090, 10.3.9) у потока два адреса памяти, и в конце каждой передачи DMA сам переключается на другой; круговой режим включается сам. Пока DMA пишет в одну половину, процессор читает другую. Какую — говорит бит CT:

void DMA2_Stream0_IRQHandler(void)
{
    DMA2->LIFCR = DMA_LIFCR_CTCIF0;
    const uint16_t *ready = (DMA2_Stream0->CR & DMA_SxCR_CT) ? buf[0] : buf[1];
    if (sc.state == SC_DONE)
        return;                                                           /* кадр ещё не ушёл — мёртвое время */
    sc_feed(&sc, ready, HALF);
    if (sc.state == SC_DONE && !tx_busy)
        send_frame();
}

На разбор половины у прерывания есть ровно время заполнения другой: 512 отсчётов при 1 МГц — 512 мкс. Опоздал — DMA начнёт затирать ту половину, которую ещё читают.

Частоты и поля проверяются при сборке: _Static_assert в scope_f407.c требует fADC ≤ 36 МГц, 3 + 12 тактов на отсчёт, запуск не чаще fADC / 17 и положения полей EXTSEL, EXTEN, ADCPRE, DBM, CT, CHSEL как в RM0090. armclang 6.24 собрал scope_f407.c и scope_core.c без предупреждений: 518 и 716 байт кода, 5688 байт ОЗУ у прошивки — два буфера по 1024 байта, кадр 1550 байт и состояние ядра 2088 байт.


198к.4 Запуск по фронту и предзапись

Осциллограф должен показать не только то, что было после фронта, но и то, что было до него. Поэтому отсчёты пишутся в кольцо всё время, а запуск лишь отмечает место. Кольцо на 1024 отсчёта — ровно кадр: когда после запуска набралось 1024 − 256 = 768 отсчётов, в кольце лежат 256 отсчётов до фронта и 768 после.

    case SC_WAIT:
        if ((int32_t)v < (int32_t)s->level - (int32_t)s->hyst)
            s->armed = 1;               /* сигнал ушёл ниже порога — фронт снова возможен */
        else if (s->armed && v >= s->level) {
            trig(s, SC_F_TRIG);
            break;
        }
        if (s->auto_after && ++s->waited >= s->auto_after)
            trig(s, SC_F_AUTO);
        break;

Автомат ждёт, пока в кольце наберётся предзапись, затем «взводится», когда сигнал опускается ниже порога минус гистерезис, и срабатывает на первом отсчёте не ниже порога. Опыт 1 — синус 10 кГц размахом 2000 кодов с шумом, порог 2048, гистерезис 40: запуск на отсчёте 301, в кадре отсчёт перед фронтом — 2043, на фронте — 2104. Кадр готов через три половины буфера (1536 отсчётов).

Зачем гистерезис. Опыт 5 — частотомер на том же автомате: синус 1 кГц с сильным шумом (сумма двух равномерных по ±100 кодов), 100 мс, то есть 100 настоящих фронтов:

Гистерезис, кодов Насчитано фронтов
0 1517
40 1137
200 234
400 100

Шум на склоне пересекает порог туда и обратно много раз. Помогает гистерезис больше размаха шума (здесь ±200, размах 400), а не его амплитуды.

«Авто». Если фронта нет вовсе, настоящий осциллограф всё равно показывает линию. Опыт 4 — постоянные 1500 кодов, таймаут 5000 отсчётов: кадр с флагом «авто», все отсчёты 1500.


198к.5 Прореживание: среднее против «каждого D-го»

Чтобы уместить в 1024 отсчёта 10 мс вместо 1 мс, частоту после АЦП понижают в D раз. Сделать это можно двумя способами — и они дают разное.

Среднее D отсчётов — и прореживание, и фильтр. Опыт 2: тот же синус 10 кГц с шумом, D = 10. Шум относительно чистого сигнала:

Прогон Модель
D = 1 10,17 кода 10,20 (сумма двух равномерных по ±12)
D = 10, среднее 3,21 кода 3,24 (в √10 меньше плюс округление)

Каждый D-й отсчёт фильтра не даёт, и всё, что выше половины новой частоты выборки, заворачивается в полосу. Опыт 3: синус 97 кГц, D = 10, новая частота 100 кГц:

Способ Размах в кадре Что видно
каждый десятый 1999 кодов ложный синус 3000 Гц полной амплитуды — фронт нашёлся
среднее десяти 62 кода почти ровная линия — кадр по «авто»

Модель среднего: коэффициент передачи на частоте f равен |sin(π·f·D/fs) / (D·sin(π·f/fs))|; для 97 кГц это 0,0314, −30,1 дБ, ожидаемый размах 62,7 кода — в прогоне 62. «Каждый десятый» показал сигнал, которого нет: 97 кГц выглядят как 3 кГц. Среднее не убирает наложение совсем, но давит его на десятки децибел.


198к.6 Кадр, канал до ПК и программа на компьютере

Кадр — 12 байт заголовка (метка A5 5A, номер, флаги «фронт/авто», D, предзапись, число отсчётов, порог), отсчёты и CRC-16/CCITT. Отсчёт АЦП — 12 бит, поэтому два отсчёта кладутся в три байта: 1536 байт вместо 2048, экономия 512 байт. Всего 1550 байт.

Канал: USART1 на PCLK2 = 84 МГц, 2 Мбод — BRR = 42, без ошибки. Байт на проводе — 10 бит, кадр идёт 7,75 мс: не больше 129 кадров в секунду. Потолок USB CDC полной скорости из раздела 198б — 1 216 000 байт/с; кадр по нему — 1,27 мс. А непрерывный поток при 1 Мвыб/с по 1,5 байта — 1 500 000 байт/с, больше потолка CDC. Поэтому прибор шлёт кадры, а не поток: пока кадр уходит, ядро не смотрит отсчёты — это мёртвое время.

scope_host.py разбирает кадры из вывода опыта:


198к.7 Грабли

Кадр «плывёт» на шумном сигнале. Запуск без гистерезиса срабатывает на шуме у порога: в опыте 1517 фронтов вместо 100. Гистерезис — больше размаха шума.

Не видно, что было до фронта. Запись начинают по запуску — и теряют начало. Кольцо пишется всегда, запуск только отмечает место, а предзапись должна набраться до того, как автомат взведётся.

Прерывание не успело. На половину буфера — 512 мкс. Разбор дольше — DMA начинает писать в ту же половину, и в кадр попадают отсчёты из разных моментов. Время обработки на плате здесь не измерялось.

Буфер DMA в CCM. У F407 память по адресу 0x1000 0000 видна только ядру (RM0090): буфер, который туда положил компоновщик, DMA не заполнит.

ADCPRE = 2 «для скорости». 84 / 2 = 42 МГц — выше 36 МГц из таблицы 67: работа вне характеристик даташита.

Запуск чаще fADC / 17. При 21 МГц потолок по таймеру 1,235 Мвыб/с, хотя «подряд» АЦП успевает 1,4. Чаще — снова вне таблицы 67.

3 такта выборки от делителя на 100 кОм. По формуле DS8626 при 3 тактах допустимое сопротивление источника меньше нуля. Нужен повторитель или выборка длиннее, иначе ошибка больше той, что обещает формула.

Прореживание выбрасыванием. Сигнал выше половины новой частоты выборки рисуется ложной низкой частотой: 97 кГц как 3 кГц. Прореживать средним (или ставить аналоговый фильтр перед АЦП).

«Поток по UART». 1 Мвыб/с по 12 бит — 1,5 МБ/с: больше и 2 Мбод, и потолка USB CDC. Только кадры, с мёртвым временем.


198к.8 Итоги

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

scope_core.c — ядро осциллографа: прореживание, кольцо истории, запуск по фронту.

/* Раздел 198к: ядро осциллографа. Отсчёты приходят блоками (половина двойного буфера DMA),
 * прореживаются, пишутся в кольцо истории; запуск — по фронту через порог с гистерезисом. */
#include "scope_core.h"

void sc_rearm(scope_t *s)
{
    s->wr = 0;
    s->acc = 0;
    s->accn = 0;
    s->state = SC_FILL;
    s->armed = 0;
    s->flags = 0;
    s->trig_at = 0;
    s->waited = 0;
}

void sc_init(scope_t *s, uint16_t level, uint16_t hyst, uint16_t pre, uint16_t decim, uint8_t mode, uint32_t auto_after)
{
    s->level = level;
    s->hyst = hyst;
    s->pre = pre < SC_N ? pre : (uint16_t)(SC_N - 1u);
    s->decim = decim ? decim : 1u;
    s->mode = mode;
    s->auto_after = auto_after;
    sc_rearm(s);
}

static void trig(scope_t *s, uint8_t why)
{
    s->trig_at = s->wr - 1u;            /* отсчёт, на котором сработал запуск */
    s->flags = why;
    s->state = SC_POST;
}

/* Один прореженный отсчёт: в кольцо, затем автомат запуска. */
static void push(scope_t *s, uint16_t v)
{
    s->ring[s->wr % SC_N] = v;
    s->wr++;
    switch (s->state) {
    case SC_FILL:                       /* копим историю: предзапись должна быть полной */
        if (s->wr >= s->pre)
            s->state = SC_WAIT;
        break;
    case SC_WAIT:
        if ((int32_t)v < (int32_t)s->level - (int32_t)s->hyst)
            s->armed = 1;               /* сигнал ушёл ниже порога — фронт снова возможен */
        else if (s->armed && v >= s->level) {
            trig(s, SC_F_TRIG);
            break;
        }
        if (s->auto_after && ++s->waited >= s->auto_after)
            trig(s, SC_F_AUTO);
        break;
    case SC_POST:                       /* добираем кадр после запуска */
        break;
    default:
        return;
    }
    if (s->state == SC_POST && s->wr - s->trig_at >= (uint32_t)SC_N - s->pre)
        s->state = SC_DONE;
}

uint32_t sc_feed(scope_t *s, const uint16_t *x, uint32_t n)
{
    uint32_t i;
    for (i = 0; i < n && s->state != SC_DONE; i++) {
        if (s->mode == SC_SKIP) {       /* каждый D-й: всё выше fs/2D заворачивается в полосу */
            if (s->accn == 0u)
                push(s, x[i]);
            if (++s->accn == s->decim)
                s->accn = 0;
        } else {                        /* среднее D отсчётов: и прореживание, и фильтр */
            s->acc += x[i];
            if (++s->accn == s->decim) {
                push(s, (uint16_t)((s->acc + s->decim / 2u) / s->decim));
                s->acc = 0;
                s->accn = 0;
            }
        }
    }
    return i;                           /* сколько отсчётов блока взято; остаток — мёртвое время */
}

uint16_t sc_sample(const scope_t *s, uint32_t i)
{
    return s->ring[(s->trig_at - s->pre + i) % SC_N];
}

uint16_t sc_crc16(const uint8_t *p, size_t n)
{
    uint16_t c = 0xFFFFu;               /* CRC-16/CCITT-FALSE: 0x1021, начальное 0xFFFF */
    while (n--) {
        c ^= (uint16_t)(*p++ << 8);
        for (int k = 0; k < 8; k++)
            c = (uint16_t)((c & 0x8000u) ? ((uint32_t)c << 1) ^ 0x1021u : (uint32_t)c << 1);
    }
    return c;
}

static uint8_t *le16(uint8_t *p, uint16_t v)
{
    p[0] = (uint8_t)v;
    p[1] = (uint8_t)(v >> 8);
    return p + 2;
}

/* Кадр: A5 5A, номер, флаги, D, предзапись, N, порог; отсчёты по 12 бит — два в трёх байтах; CRC-16. */
size_t sc_encode(const scope_t *s, uint8_t seq, uint8_t *buf)
{
    uint8_t *p = buf;
    *p++ = 0xA5u;
    *p++ = 0x5Au;
    *p++ = seq;
    *p++ = s->flags;
    p = le16(p, s->decim);
    p = le16(p, s->pre);
    p = le16(p, (uint16_t)SC_N);
    p = le16(p, s->level);
    for (uint32_t i = 0; i < SC_N; i += 2u) {
        uint16_t a = sc_sample(s, i) & 0x0FFFu, b = sc_sample(s, i + 1u) & 0x0FFFu;
        *p++ = (uint8_t)a;
        *p++ = (uint8_t)((a >> 8) | (b << 4));
        *p++ = (uint8_t)(b >> 4);
    }
    p = le16(p, sc_crc16(buf, (size_t)(p - buf)));
    return (size_t)(p - buf);
}

/* Частотомер: число фронтов в блоке; armed переживает границу блоков. */
uint32_t sc_edges(const uint16_t *x, uint32_t n, uint16_t level, uint16_t hyst, uint8_t *armed)
{
    uint32_t e = 0;
    for (uint32_t i = 0; i < n; i++) {
        if ((int32_t)x[i] < (int32_t)level - (int32_t)hyst)
            *armed = 1;
        else if (*armed && x[i] >= level) {
            *armed = 0;
            e++;
        }
    }
    return e;
}

scope_f407.c — осциллограф на STM32F407: TIM2, ADC1 и DMA в двойной буфер, кадр в USART1.

/* Раздел 198к: осциллограф на STM32F407 — TIM2 задаёт 1 Мвыб/с, ADC1 (PA1) пишет через DMA2 Stream0 в двойной буфер,
 * ядро scope_core ищет фронт, кадр уходит в USART1 (PA9, 2 Мбод) через DMA2 Stream7.
 * Такты: SYSCLK 168 МГц, APB2 = 84 МГц, APB1 = 42 МГц (таймеры APB1 — 84 МГц). Настройка тактов — раздел 6. */
#include "stm32f4xx.h"
#include "scope_core.h"

#define PCLK2_HZ    84000000u
#define TIMCLK_HZ   84000000u           /* делитель APB1 не 1 — частота таймеров удваивается */
#define ADCPRE_DIV  4u                  /* ADC_CCR.ADCPRE = 01 */
#define ADCCLK_HZ   (PCLK2_HZ / ADCPRE_DIV)
#define FS_HZ       1000000u
#define SMP_CYC     3u                  /* SMPR = 000: 3 такта выборки */
#define HALF        512u                /* отсчётов в половине двойного буфера */
#define UART_BAUD   2000000u

_Static_assert(ADCCLK_HZ <= 36000000u, "DS8626, табл. 67: fADC не выше 36 МГц");
_Static_assert(ADCCLK_HZ / FS_HZ >= SMP_CYC + 12u, "выборка и 12 тактов преобразования успевают до следующего запуска");
_Static_assert(FS_HZ * 17u <= ADCCLK_HZ, "DS8626, табл. 67: внешний запуск не чаще fADC / 17");
_Static_assert(TIMCLK_HZ % FS_HZ == 0u && PCLK2_HZ % UART_BAUD == 0u, "частота выборки и скорость порта делятся нацело");
_Static_assert(ADC_CR2_EXTSEL_Pos == 24u && ADC_CR2_EXTEN_Pos == 28u && ADC_CCR_ADCPRE_Pos == 16u, "поля ADC как в RM0090");
_Static_assert(DMA_SxCR_DBM_Pos == 18u && DMA_SxCR_CT_Pos == 19u && DMA_SxCR_CHSEL_Pos == 25u, "поля DMA как в RM0090");

static uint16_t buf[2][HALF];           /* обычное ОЗУ: CCM (0x1000 0000) у F407 видна только ядру, не DMA */
static uint8_t tx[SC_FRAME];
static scope_t sc;
static volatile uint8_t tx_busy;
static uint8_t seq;

void scope_start(void)
{
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_DMA2EN;
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
    RCC->APB2ENR |= RCC_APB2ENR_ADC1EN | RCC_APB2ENR_USART1EN;
    GPIOA->MODER |= 3u << GPIO_MODER_MODER1_Pos;                          /* PA1 — аналоговый вход ADC123_IN1 */
    GPIOA->MODER = (GPIOA->MODER & ~GPIO_MODER_MODER9) | (2u << GPIO_MODER_MODER9_Pos);
    GPIOA->AFR[1] = (GPIOA->AFR[1] & ~GPIO_AFRH_AFSEL9) | (7u << GPIO_AFRH_AFSEL9_Pos);     /* PA9 — USART1_TX */

    USART1->BRR = PCLK2_HZ / UART_BAUD;                                   /* 42: 2 Мбод ровно */
    USART1->CR3 = USART_CR3_DMAT;
    USART1->CR1 = USART_CR1_TE | USART_CR1_UE;

    TIM2->PSC = 0;
    TIM2->ARR = TIMCLK_HZ / FS_HZ - 1u;                                   /* 83: 1 МГц */
    TIM2->CR2 = 2u << TIM_CR2_MMS_Pos;                                    /* переполнение → TRGO */

    ADC->CCR = (ADC->CCR & ~ADC_CCR_ADCPRE) | (1u << ADC_CCR_ADCPRE_Pos); /* PCLK2 / 4 = 21 МГц */
    ADC1->SMPR2 = 0;                                                      /* канал 1: 3 такта выборки */
    ADC1->SQR1 = 0;                                                       /* одно преобразование в группе */
    ADC1->SQR3 = 1u << ADC_SQR3_SQ1_Pos;
    ADC1->CR2 = (1u << ADC_CR2_EXTEN_Pos) | (6u << ADC_CR2_EXTSEL_Pos)    /* по фронту TIM2_TRGO */
              | ADC_CR2_DMA | ADC_CR2_DDS | ADC_CR2_ADON;

    DMA2_Stream0->CR = 0;
    while (DMA2_Stream0->CR & DMA_SxCR_EN) {
    }
    DMA2_Stream0->PAR = (uint32_t)&ADC1->DR;
    DMA2_Stream0->M0AR = (uint32_t)buf[0];
    DMA2_Stream0->M1AR = (uint32_t)buf[1];
    DMA2_Stream0->NDTR = HALF;
    DMA2_Stream0->CR = (0u << DMA_SxCR_CHSEL_Pos)                         /* канал 0 потока 0 — ADC1 */
                     | DMA_SxCR_DBM | DMA_SxCR_PL_1 | DMA_SxCR_MSIZE_0 | DMA_SxCR_PSIZE_0
                     | DMA_SxCR_MINC | DMA_SxCR_TCIE;
    DMA2_Stream0->CR |= DMA_SxCR_EN;

    NVIC_EnableIRQ(DMA2_Stream0_IRQn);
    NVIC_EnableIRQ(DMA2_Stream7_IRQn);
    sc_init(&sc, 2048u, 40u, 256u, 1u, SC_AVG, 200000u);
    TIM2->CR1 = TIM_CR1_CEN;
}

static void send_frame(void)
{
    size_t n = sc_encode(&sc, seq++, tx);
    tx_busy = 1;
    DMA2_Stream7->CR = 0;
    while (DMA2_Stream7->CR & DMA_SxCR_EN) {
    }
    DMA2->HIFCR = DMA_HIFCR_CTCIF7 | DMA_HIFCR_CHTIF7 | DMA_HIFCR_CTEIF7 | DMA_HIFCR_CDMEIF7 | DMA_HIFCR_CFEIF7;
    DMA2_Stream7->PAR = (uint32_t)&USART1->DR;
    DMA2_Stream7->M0AR = (uint32_t)tx;
    DMA2_Stream7->NDTR = (uint32_t)n;
    DMA2_Stream7->CR = (4u << DMA_SxCR_CHSEL_Pos) | DMA_SxCR_DIR_0 | DMA_SxCR_MINC | DMA_SxCR_TCIE;   /* USART1_TX */
    DMA2_Stream7->CR |= DMA_SxCR_EN;
}

/* Половина буфера заполнена. CT показывает, куда DMA пишет СЕЙЧАС, — готова другая. */
void DMA2_Stream0_IRQHandler(void)
{
    DMA2->LIFCR = DMA_LIFCR_CTCIF0;
    const uint16_t *ready = (DMA2_Stream0->CR & DMA_SxCR_CT) ? buf[0] : buf[1];
    if (sc.state == SC_DONE)
        return;                                                           /* кадр ещё не ушёл — мёртвое время */
    sc_feed(&sc, ready, HALF);
    if (sc.state == SC_DONE && !tx_busy)
        send_frame();
}

void DMA2_Stream7_IRQHandler(void)
{
    DMA2->HIFCR = DMA_HIFCR_CTCIF7;
    tx_busy = 0;
    sc_rearm(&sc);                                                        /* кадр ушёл — ждём следующий фронт */
}

Дальше: раздел 198л «Мини-проект: всё, что есть на F4-Discovery — акселерометр, аудио-ЦАП, микрофон и USB».

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