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 |
Прибор «пока прибора нет», как и анализатор из 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 разбирает кадры из вывода опыта:
- все 5 кадров — по 1550 байт, CRC сошлась у всех;
- своя модель на Python (тот же генератор, своё прореживание и свой автомат запуска) дала те же отсчёты, что ядро на C, во всех пяти кадрах;
- частота по фронтам с интерполяцией: опыт 1 — 10 000,9 Гц, опыт 2 — 10 000,0 Гц, опыт 3 «каждый десятый» — 3000,0 Гц; размах опыта 1 — 1,642 В;
- картинка — matplotlib 3.10.9, PNG 900 × 600: кадр опыта 1 с меткой запуска и оба кадра опыта 3.
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 Итоги
- F407:
fADC= 84 / 4 = 21 МГц (не выше 36), 3 + 12 тактов — 1,4 Мвыб/с подряд, по таймеру не выше 1,235; выбрано 1 Мвыб/с. - Полоса — до 500 кГц по Котельникову, форма — до 100 кГц (десять отсчётов на период); младший разряд 0,806 мВ.
- При 3 тактах выборки формула DS8626 даёт отрицательный предел сопротивления источника — нужен повторитель; при 15 тактах — до 11 788 Ом.
- Двойной буфер DMA по 512 отсчётов даёт 512 мкс на разбор половины.
- Кольцо 1024 отсчёта: предзапись 256, после запуска 768; гистерезис 400 кодов вернул 100 фронтов из 100 вместо 1517.
- Среднее по 10 отсчётам: шум 10,17 → 3,21 кода, 97 кГц ослаблены до 62 кодов размаха (−30,1 дБ); «каждый десятый» нарисовал ложные 3000 Гц.
- Кадр 1550 байт с CRC: 7,75 мс по UART на 2 Мбод, 1,27 мс по USB CDC; модель на Python совпала с ядром на C во всех 5 кадрах, частота 10 000,9 Гц.
Полные исходники
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».