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

36к. Генераторы сигналов: функциональный, DDS, произвольной формы и генератор на STM32

Осциллограф смотрит на сигнал, генератор его создаёт. Для прошивки это способ подать на вход то, чего на столе ещё нет: датчик, который пока не припаян, частоту, которую надо измерить, синус, на котором проверяют фильтр. Генератор кажется простым прибором — выставил частоту и размах, включил выход, — но и он обманывает по физике, как приборы из 36а: на экране генератора 1 В, а на щупе 2 В; синус из точек таблицы несёт рядом с собой лишние частоты; выброс на фронте оказывается свойством кабеля, а не генератора.

Раздел идёт от устройства к применению: какие генераторы бывают, как устроен прямой цифровой синтез (DDS — direct digital synthesis: частота собирается из счётчика фазы и таблицы), откуда берутся «50 Ом», что делают частота отсчётов и фильтр у генератора произвольной формы, как читать параметры импульса. Потом — генератор, встроенный в осциллограф RIGOL DHO900, с диаграммой Боде (кривая усиления и сдвига фазы по частоте), проверка прошивки генератором и, наконец, свой генератор на STM32: фазовый аккумулятор, который дописывает буфер DMA для ЦАП F407 или для ШИМ F103.

Опоры: DS8626 Rev 9 (STM32F405/407: таблица 74 — характеристики ЦАП); RM0090 Rev 19 (раздел 14.3.4 — когда данные ЦАП попадают на выход, таблица 42 — запросы DMA1); RM0008 Rev 17 (таблица 78 — запросы DMA1 у F103); RIGOL DHO900 Datasheet — с. 10 (вход канала), 19–21 (Bode Plot и AFG), 23 (разъём AFG OUT), 26 (примечание о моделях); DHO900 User Guide — с. 58 (входное сопротивление), 186–189 (глава 14, AFG), 194–199 (глава 15, Bode Plot); номера страниц — по PDF-файлам из Books/. Числа — прогон _verify/gen36k: стенд sim36k.c на компьютере и тем же исходником в QEMU на Cortex-M3, независимо — ref36k.py (numpy), фронты через кабель — ngspice. Прошивки fw36k_f4.c и fw36k_f1.c собраны с -Os и измерены, на плате не запускались.

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

Оригинал Перевод
function generator функциональный генератор
DDS, direct digital synthesis прямой цифровой синтез
phase accumulator, tuning word фазовый аккумулятор, шаг фазы (слово настройки)
phase truncation, spur, SFDR усечение фазы, паразитная составляющая, динамический диапазон без помех
AWG, arbitrary waveform generator генератор произвольной формы
image, reconstruction filter образ (зеркальная составляющая), фильтр восстановления
sinc rolloff, zero-order hold спад sinc, удержание отсчёта (ступенька ЦАП)
output impedance, High Z load выходное сопротивление, нагрузка «высокоомная»
rise time, overshoot, duty cycle длительность фронта, выброс, коэффициент заполнения
sweep, burst качание частоты, пачка импульсов
Bode plot, gain / phase margin диаграмма Боде, запас по усилению / по фазе
update rate, settling time частота обновления, время установления

36к.1 Какие бывают генераторы

вид как получается форма сильное слабое
функциональный (аналоговый) интегратор и компаратор дают треугольник и меандр, диодный формирователь гнёт треугольник в синус плавная перестройка, нет ступенек и образов частота задана RC и плывёт, синус с искажениями формирователя
DDS счётчик фазы от кварца, таблица синуса, ЦАП частота цифрой, с точностью кварца; перестройка без разрыва фазы рядом с сигналом — помехи усечения и образы (36к.2, 36к.4)
произвольной формы (AWG) память точек, ЦАП с заданной частотой отсчётов любая форма: записанный сигнал датчика, импульс с дребезгом длина памяти и частота отсчётов ограничивают и форму, и частоту
импульсный счётчики задают период, длительность, задержку отдельно регулируются фронты, длительность, пачка синуса нет
качающейся частоты (sweep) частота меняется по закону за заданное время вместе с осциллографом даёт АЧХ быстрое качание искажает результат (36к.7)

Аналоговый функциональный генератор — тот же релаксационный генератор, что разобран в 179, только с переключаемыми конденсаторами и формирователем синуса. Сегодня настольный «функциональный генератор» почти всегда цифровой: DDS для синуса, меандра и пилы и память точек для произвольной формы в одном корпусе. Импульсный режим и качание частоты — обычно пункты меню того же прибора. Поэтому дальше речь в основном о DDS и о точках.


36к.2 DDS изнутри: фазовый аккумулятор и помехи усечения

Сердце DDS — 32-битный регистр фазы. Каждый такт частоты отсчётов fs к нему прибавляется шаг M, и регистр переполняется — это и есть период сигнала: 2³² соответствует одному обороту фазы. Частота выхода

f = M · fs / 2³²        M = round(f · 2³² / fs)

Старшие биты фазы — номер точки в таблице синуса, точка уходит в ЦАП. Шаг частоты — fs/2³²: при fs = 1 МГц это 232,831 мкГц. Целый шаг M даёт частоту чуть мимо заказанной, но очень близко (стенд sim36k.c, fs = 1 МГц):

заказано шаг M получилось ошибка
1 Гц 4295 1,000008 Гц 8 мкГц
1000 Гц 4294967 999,999931 Гц −69 мкГц
1234,56 Гц 5302395 1234,560041 Гц 41 мкГц
10000 Гц 42949673 10000,000009 Гц 9 мкГц

Для сравнения — способ из 15.4: таблица 64 точки, частоту меняет ARR таймера (84 МГц). Около 10 кГц делитель должен быть 131,25, а бывает только целым: 131 даёт 10019,08 Гц, 132 — 9943,18 Гц. Шаг в 76 Гц там, где DDS промахивается на микрогерцы. И второе отличие: новая частота DDS — это запись одного слова M, фаза при этом продолжается с того же места, без скачка; у таймера смена ARR рвёт период.

Плата за это — усечение фазы. Таблица берёт только старшие биты (в нашем генераторе 8 из 32, таблица 256 точек), а младшие 24 отбрасывает. Ошибка фазы повторяется с периодом, и в спектре рядом с сигналом вырастают паразитные составляющие. Модель ref36k.py: спектр 2²⁰ отсчётов, шаг M — кратный 2¹² (тогда последовательность ровно периодична и спектр сходится точно на бинах), перебор 24 значений M, таблица с точным синусом:

бит фазы в таблицу точек худшая помеха (модель) оценка −6,02 дБ на бит
8 256 −48,13 дБн −48,16 дБн
10 1024 −60,20 дБн −60,20 дБн
12 4096 −72,24 дБн −72,24 дБн

(дБн — децибелы относительно несущей, то есть самого сигнала.) Каждый бит фазы — около 6 дБ. С настоящей таблицей нашего генератора — 12-битные коды — худшая помеха та же: −48,13 дБн; её задаёт усечение фазы, а не разрядность ЦАП. Чтобы помехи опустились до −72,24 дБн, таблице нужно 4096 точек — 8 КБ флеш-памяти вместо 512 байт.

Генератор DDS: шаг фазы M прибавляется к 32-битному аккумулятору на каждом такте fs; старшие 8 бит фазы выбирают точку таблицы синуса 256 × 12 бит, точка уходит в ЦАП, затем фильтр восстановления и выход через 50 Ом. Ниже — фаза растёт ступенями и переполняется, выход ЦАП — ступеньки вокруг синуса. Разрешение по частоте fs/2³² = 232,831 мкГц при fs = 1 МГц; худшая помеха усечения в модели: 8 бит — −48,13 дБн, 10 бит — −60,20 дБн, 12 бит — −72,24 дБн


36к.3 Выход 50 Ом: на экране генератора одно, на щупе вдвое больше

Внутри генератора — источник ЭДС и последовательно с ним резистор 50 Ом. Генератор не знает, что к нему подключено, и по умолчанию считает, что нагрузка тоже 50 Ом: тогда на ней ровно половина ЭДС, и эту половину он показывает на экране. Выставлено «1 Vpp» — значит, ЭДС 2 В размаха. Что окажется на разных нагрузках (ref36k.py, делитель 50 Ом / нагрузка):

нагрузка на ней, при «1 Vpp» на экране
50 Ом 1,0000 В
100 Ом 1,3333 В
1 кОм 1,9048 В
10 кОм 1,9900 В
1 МОм (вход осциллографа) 1,9999 В

Вход осциллографа и вход микроконтроллера — высокоомные, и на них выходит вдвое больше, чем написано. Это не ошибка прибора: в меню выхода у генератора есть настройка нагрузки (50 Ом или High Z), и она меняет только пересчёт числа на экране, а не сам сигнал. Мощность в радиотехнике считают на 50 Ом: 0 дБм — это 1 мВт, 0,2236 В действующего, или 0,6325 В размаха синуса.

Где это бьёт по прошивке. Нужен сигнал 0…3,3 В на вывод АЦП. Режим выхода остался «50 Ом», выставлено 3,3 Vpp со смещением 1,65 В — экран обещает 0…3,30 В, а на высокоомном выводе будет 0…6,60 В: выше любого допустимого для 3,3-вольтового вывода. Правильно — либо переключить нагрузку на High Z, либо в режиме «50 Ом» выставить 1,650 Vpp и смещение 0,825 В: на выводе получится 0…3,30 В.

Выходное сопротивление своего генератора проверяют за минуту: размах без нагрузки (на входе осциллографа) и с резистором 51 Ом. R = Rн · (Uхх / Uн − 1); для 2,00 В и 1,01 В на 51 Омах это 49,99 Ом.

У осциллографа DHO900 вход только 1 МОм ± 1 % и 15 пФ ± 3 пФ (Datasheet, с. 10; User Guide, с. 58 — других режимов входа нет). Чтобы смотреть быстрый генератор на согласованной нагрузке, на вход ставят внешнюю проходную нагрузку 50 Ом. А выходное сопротивление его собственного генератора AFG и то, на какую нагрузку указан размах 2 мВ…10 В, в документах не названы (Datasheet, с. 20) — их и меряют способом выше.


36к.4 Генератор произвольной формы: частота отсчётов, образы, sinc

ЦАП держит каждый отсчёт до следующего — выход состоит из ступенек. У ступенек свой спектр: сигнал f повторяется вокруг частоты отсчётов и её кратных — на fs − f, fs + f, 2fs − f… Эти повторы называют образами. Кроме того, сама ступенька ослабляет всё по закону sinc(f/fs) = sin(π·f/fs)/(π·f/fs) — чем ближе к fs, тем сильнее (ref36k.py, формула):

f / fs сигнал образ на fs − f образ относительно сигнала
0,10 −0,14 дБ −19,23 дБ −19,08 дБ
0,25 −0,91 дБ −10,45 дБ −9,54 дБ
0,40 −2,42 дБ −5,94 дБ −3,52 дБ
0,50 −3,92 дБ −3,92 дБ 0,00 дБ

Численная модель — ступеньки по 64 подотсчёта на отсчёт и спектр — дала те же числа с точностью 0,01 дБ. Вывод: десять отсчётов на период (f/fs = 0,1) — это ещё не «чистый синус», а синус с образом на −19 дБ. Убирает образы фильтр восстановления после ЦАП. Пример: fs = 1 МГц, сигнал до 100 кГц, образ на 900 кГц, нужно −60 дБн. Фильтр Баттерворта со срезом 200 кГц:

порядок ослабление на 900 кГц образ в итоге спад на 100 кГц
3 39,19 дБ −58,28 дБн 0,067 дБ
4 52,26 дБ −71,34 дБн 0,017 дБ

Четвёртый порядок (178 — как его собрать на ОУ) решает задачу, а спад sinc на 100 кГц — 0,143 дБ — при желании поправляют в самой таблице.

Длина таблицы. Если таблицу из L точек выдавать по точке на отсчёт (как в 15.4), первые лишние гармоники стоят на (L − 1)·f и (L + 1)·f, и первая слабее сигнала ровно в L − 1 раз (ref36k.py, спектр против формулы):

точек L первая лишняя уровень
16 15·f −23,52 дБ
32 31·f −29,82 дБ
64 63·f −35,98 дБ
256 255·f −48,13 дБ

У генератора из 15.4 (64 точки по 10 мкс, 1562,5 Гц) это составляющая на 98,4375 кГц при −35,98 дБ — её и убирает фильтр. Строка 256 точек дала те же −48,13 дБ, что и худшая помеха усечения в 36к.2: это одна и та же ступенька таблицы, увиденная с двух сторон.

Две схемы генератора произвольной формы. В «настоящей» каждая точка памяти выдаётся ровно один раз, а частоту меняет частота отсчётов — форма сохраняется точно. В схеме с фазовым аккумулятором частота отсчётов постоянная, и точки пропускаются или повторяются — короткие детали формы дрожат. Пример из чисел DHO900: память до 16 kpts, 156 MSa/s, частота произвольной формы до 10 МГц (Datasheet, с. 19–20). Каждую из 16384 точек при 156 MSa/s удаётся выдать только до 9521,48 Гц; выше точки неизбежно пропускаются. Какая схема внутри — в документах не сказано, это вывод из арифметики.


36к.5 Меандр и импульсы: фронт, выброс, скважность

Три числа описывают импульс: длительность фронта (от 10 до 90 % размаха), выброс (на сколько процентов сигнал перелетает установившийся уровень) и коэффициент заполнения (доля периода в единице). Как полоса осциллографа растягивает фронт — 36а.3; автоматические измерения этих величин — 36е.

Выброс на экране часто принадлежит не генератору, а проводам. Модель ngspice (edge36k.cir): ЭДС 0 → 2 В с фронтом 8 нс (10…90 %), кабель 1 м — 50 Ом и 5 нс, на конце вход осциллографа 1 МОм и 15 пФ — как у DHO900:

источник и нагрузка уровень пик выброс фронт 10…90 %
генератор 50 Ом → вход 1 МОм 1,9999 В 1,9999 В нет 8,08 нс
генератор 50 Ом → нагрузка 50 Ом 1,0000 В 1,0000 В нет 8,01 нс
источник 10 Ом → вход 1 МОм 1,9999 В 3,31 В 65,5 % 4,95 нс

Генератор с 50 Ом на ненагруженном кабеле работает чисто: отражённая от открытого конца волна возвращается и гасится в его же 50 Ом. А источник с малым сопротивлением — вывод микросхемы — на том же метре кабеля даёт выброс на две трети размаха: отражение уходит обратно и звенит. Отсюда правило стола: быстрый сигнал от вывода к осциллографу — коротким щупом, не кабелем.

Скважность и коэффициент заполнения. В русской технической литературе скважность — отношение периода к длительности импульса, T/τ, она всегда больше единицы. Генератор в меню пишет Duty — это обратная величина, доля периода:

Duty коэффициент заполнения скважность T/τ
50 % 0,50 2
20 % 0,20 5
10 % 0,10 10
1 % 0,01 100

В этом справочнике, как и в разговорной речи, «скважностью» часто называют именно долю периода (57ж, 13.5) — читая чужое ТЗ, уточняйте.

Меандр из DDS дрожит на шаг отсчёта. Если меандр получать сравнением фазы с порогом, фронт может стоять только на отсчёте. Стенд: 3 кГц при fs = 1 МГц, секунда отсчётов — 3000 периодов ровно, но каждый период — то 333, то 334 отсчёта, импульс — 166 или 167. Фронты прыгают на 1 мкс, средняя частота точная. Поэтому хорошие генераторы делают меандр компаратором из отфильтрованного синуса: у DHO900 указано дрожание 500 пс при шаге отсчёта 6,41 нс (Datasheet, с. 20).


36к.6 Генератор в осциллографе: AFG и диаграмма Боде у DHO900

У моделей DHO914S и DHO924S в осциллограф встроен одноканальный генератор AFG и режим диаграммы Боде (Datasheet, с. 26, примечание 7; User Guide, с. 186). Выход — разъём BNC [AFG OUT] на задней панели (Datasheet, с. 23). Что он умеет по даташиту — все числа помечены там как типовые (Datasheet, с. 19–21):

параметр значение
отсчёты и разрядность 156 MSa/s, 14 бит
формы Sine, Square, Ramp, DC, Noise и своя (Arb)
Sine 2 мГц…25 МГц, неравномерность ±0,5 дБ к 1 кГц, гармоники −40 дБн, КНИ < 1 %
Square 2 мГц…15 МГц, фронт < 15 нс, выброс < 5 %, Duty 1…99 %
Ramp 2 мГц…150 кГц, симметрия 0…100 %
Arb 2 мГц…10 МГц, 2 точки…16 kpts
размах 2 мВ…10 В до 10 МГц, 2 мВ…5 В выше
смещение −5…5 В
точность частоты 100 ppm
модуляция AM, FM, PM, внутренняя, частота модуляции 2 мГц…1 МГц

100 ppm — это ±0,1 Гц на 1 кГц: для проверки таймера прошивки с кварцем такой генератор сам может оказаться неточнее платы. Мерять частоту лучше частотомером, а не верить шкале генератора.

Как включить (User Guide, с. 186–189): кнопка AFG на передней панели или метка AFG внизу экрана включает выход, повторное нажатие открывает меню. В меню — Function (форма), Frequency, Amplitude, Offset, Phase, для меандра Duty, для пилы Symm и выключатель Output. Генератор и осциллограф в одном корпусе, поэтому экран разъёма AFG OUT, скорее всего, соединён с той же землёй, что и щупы (36а.5); в документах это прямо не сказано — проверьте прозвонкой, прежде чем подавать сигнал на плату с «плавающей» землёй.

Диаграмма Боде (User Guide, с. 194–199). Прибор сам качает синус генератора по частоте и на каждой частоте сравнивает два канала: In — сигнал на входе цепи, Out — на выходе. Результат — кривые усиления и фазы, курсор с Freq, Gain и Phase и запасы PM и GM для петли обратной связи. Настройки — в меню Bode Plots: In и Out (каналы CH1–CH4), Sweep Type (Log или Line), Start Freq от 10 Гц до 24,99 МГц, Stop Freq от 100 Гц до 25 МГц, число точек от 10 до 300 (по умолчанию 10), Amp — размах от 20 мВ до 5 В, Var.Amp — свой размах на разных участках частот; Display Type — Wave или Chart; сохранение в csv или html. Даташит называет число точек «на октаву» (с. 19), руководство — «на декаду» (с. 199): проверьте на своём приборе, сколько точек получилось.

Схема подключения, которую прибор показывает по кнопке Tips, — для петли импульсного источника: развязывающий трансформатор и резистор ввода в разрыв обратной связи (User Guide, с. 198). Для обычного фильтра проще: AFG OUT — на вход фильтра и туда же CH1 (In), выход фильтра — на CH2 (Out). Усиление считается как отношение Out к In, поэтому ни выходное сопротивление генератора, ни потери в кабеле в результат не входят.

Пример — RC-фильтр генератора на F103 из 36к.8 (1 кОм и 10 нФ, затем 10 кОм и 1 нФ). Start Freq 100 Гц, Stop Freq 1 МГц, 10 точек на декаду — 41 точка. Какой должна получиться кривая (ref36k.py):

частота усиление фаза
1 кГц −0,04 дБ −7,5°
5 кГц −0,96 дБ −36,2°
9561 Гц −3 дБ −63,1°
100 кГц −32,19 дБ −161,1°
1 МГц −71,93 дБ −178,1°

На 1 МГц из 1 В размаха на выходе остаётся 0,253 мВ — это уже на уровне шумов канала, и кривая там рассыплется; руководство прямо предупреждает, что для шумных цепей режим не годится (User Guide, с. 195). Выручает Var.Amp: больший размах на высоких частотах. Или конец качания на 100 кГц, где на выходе ещё 24,6 мВ.


36к.7 Генератор проверяет прошивку

Датчик, которого ещё нет. Аналоговый датчик с выходом 0,3…3,0 В — это для генератора размах 2,70 В и смещение 1,65 В при нагрузке High Z, или 1,350 Vpp и 0,825 В, если выход остался в режиме «50 Ом» (36к.3). Медленная пила проверяет пересчёт кода АЦП в величину по всей шкале (ошибки АЦП — 14а), синус — фильтрацию, Noise — пороги и гистерезис. Импульсы с заданной частотой заменяют датчик расхода или тахометр на входе захвата таймера (13.6); пачка из N импульсов (burst) проверяет, что счётчик не теряет и не добавляет фронтов.

ШИМ-сигналы. Сигнал рулевой машинки — период 20 мс, импульс 1,5 мс, то есть Duty 7,5 %. Прошивка, которая меряет его захватом с тиком таймера 1 мкс, различает 0,005 % периода — гораздо мельче, чем шаг настройки Duty у большинства генераторов. Значит, проверять прошивку удобнее длительностью импульса (режим Pulse), а не процентами.

АЧХ фильтра в прошивке. Цепочка: генератор → вывод АЦП → фильтр в прошивке → ЦАП (F407, 36к.8) → осциллограф. Фильтр — одно звено БИХ (81а.3), y += (x − y)/16 при 10 кГц отсчётов: срез 102,75 Гц (приближённая формула −ln(1 − 1/16)·fs/2π даёт 102,72 Гц), на нём −3,01 дБ. Диаграмма Боде DHO900 строит такую кривую сама: In — на выводе АЦП, Out — на выходе ЦАП, Start Freq 10 Гц, Stop Freq 4 кГц — ниже половины частоты отсчётов, 5000 Гц. Одна деталь: в меню Bode Plots у синуса есть размах, но нет смещения (User Guide, с. 199), а вывод АЦП отрицательного напряжения не терпит. Нужна развязка: конденсатор 10 мкФ и делитель 20 кОм / 20 кОм от 3,3 В — середина 1,65 В, срез 1,59 Гц, на 10 Гц потеря 0,109 дБ.

Качать медленно. Фильтр отвечает на изменение частоты не сразу — у нашего звена постоянная времени 1/(2π·fc) = 1,549 мс. Модель ref36k.py: логарифмическое качание 10 Гц → 1 кГц непрерывно, усиление на срезе по огибающей выхода:

время качания усиление на 102,75 Гц правильно
0,05 с −1,78 дБ −3,01 дБ
0,2 с −2,70 дБ −3,01 дБ
1 с −2,93 дБ −3,01 дБ
5 с −2,99 дБ −3,01 дБ

Быстрое качание показывает фильтр «шире», чем он есть: выход ещё помнит низкую частоту. Сколько диаграмма Боде DHO900 ждёт на каждой точке, в документах не сказано — проверьте, что кривая не меняется при вдвое большем числе точек. Как читать АЧХ и ФЧХ — 50а, аналоговые фильтры перед АЦП — 178.


36к.8 Генератор на STM32: фазовый аккумулятор в прерывании и в DMA

Табличный генератор на ЦАП уже собран в 15.4: таймер TIM6 запускает ЦАП, DMA по кругу гоняет таблицу, частота = частота запуска / число точек. Встроенные треугольник и шум ЦАП — 15.5, фазовый аккумулятор в звуке — 198л и 198п. Здесь новое — DDS поверх той же связки: частота с шагом в микрогерцы и без разрыва фазы. Общая логика — dds36k.c, одна для F407 и F103; сердце — цикл заполнения:

        while (n--) {
            *buf++ = (uint16_t)(dds_sine[ph >> (32u - DDS_TAB_BITS)] >> sh);
            ph += w;
        }

F407, ЦАП и DMA. TIM6 считает 84 МГц / 84 — запуск ЦАП 1 млн раз в секунду, ровно предел из таблицы 74 DS8626 (1 MS/s). DMA1 Stream5, канал 7 — запрос DAC1 (таблица 42 RM0090) — гонит по кругу буфер из двух половин по 256 отсчётов. На середине буфера и в конце приходит прерывание, и обработчик заполняет ту половину, которую DMA только что прошёл:

void gen_set(uint32_t f_centihz)
{
    gen.word = dds_word(f_centihz, FS_HZ);
}

#ifndef DDS_IRQ
void DMA1_Stream5_IRQHandler(void)
{
    uint32_t isr = DMA1->HISR;
    if (isr & DMA_HISR_HTIF5) {            /* DMA ушёл во вторую половину — первая свободна */
        DMA1->HIFCR = DMA_HIFCR_CHTIF5;
        dds_fill(&gen, buf, HALF);
    }
    if (isr & DMA_HISR_TCIF5) {            /* DMA вернулся в начало — свободна вторая */
        DMA1->HIFCR = DMA_HIFCR_CTCIF5;
        dds_fill(&gen, buf + HALF, HALF);
    }
}

Отсчёт выходит на вывод через три такта APB1 после запуска таймера — задержка постоянная, отсчёты идут ровно (RM0090, 14.3.4). Обработчик приходит раз в 256 мкс и не обязан успеть «к отсчёту» — только за 256 мкс.

F407, отсчёт в прерывании таймера (-DDDS_IRQ) — вариант без DMA, проще для понимания и отладки:

void TIM6_DAC_IRQHandler(void)
{
    TIM6->SR = 0u;
    DAC->DHR12R1 = dds_next(&gen);         /* без запуска ЦАП берёт значение через такт APB1 */
}

Без аппаратного запуска ЦАП переносит значение на выход через один такт APB1 после записи (RM0090, 14.3.4) — то есть в момент, когда обработчик до этого добрался. Любое другое прерывание с высшим приоритетом сдвигает отсчёт, и синус дрожит по времени. Поэтому прерывание — для низких частот отсчётов, DMA — для всего остального.

F103 без ЦАП: ШИМ и RC-фильтр. У F103C8 ЦАП нет (15.1). TIM2 даёт ШИМ на PA0 с ARR = 255 — 8 бит, 72 МГц / 256 = 281250 Гц, это и частота отсчётов. Запрос TIM2_UP — DMA1 Channel 2 (таблица 78 RM0008) — на каждом обновлении кладёт следующий отсчёт в CCR1, а предзагрузка CCR1 вводит его с начала следующего периода. Тот же dds_fill, сдвиг на 4 бита — коды 0…255. Разрядность и частота ШИМ меняются местами:

бит ШИМ частота ШИМ = отсчётов ступенька при 3,3 В
8 281250 Гц 12,94 мВ
10 70312,5 Гц 3,23 мВ
12 17578,125 Гц 0,81 мВ

Фильтр — две RC-цепи: 1 кОм и 10 нФ, затем 10 кОм и 1 нФ (вторая в десять раз высокоомнее, чтобы меньше нагружать первую). Срез по −3 дБ — 9561 Гц, на 1 кГц потеря 0,04 дБ, на 5 кГц — 0,96 дБ; несущая 281250 Гц ослаблена на 49,92 дБ, и пульсации при половинном заполнении — 13,00 мВ размаха, около одной ступеньки 8 бит. Рабочая полоса такого генератора — до единиц килогерц; ту же кривую можно снять диаграммой Боде (36к.6).

Размеры (arm-none-eabi-gcc 12.2.1, -Os -Wall -Wextra -Werror):

вариант своя прошивка dds36k.o образ во флеш-памяти ОЗУ
F407, ЦАП + DMA 356 байт 656 байт 2108 байт 1040 байт
F407, отсчёт в прерывании 260 байт 656 байт 1908 байт 16 байт
F103, ШИМ + DMA 320 байт 656 байт 1908 байт 528 байт

Из 656 байт dds36k.o таблица синуса — 512, dds_fill — 100, dds_word — 26. Неожиданная строка в карте памяти — __udivmoddi4, 700 байт у F407 и 702 у F103: это 64-битное деление в dds_word, библиотека тянет его целиком. Если частота задана один раз, шаг считают заранее, константой.

Сколько стоит процессору. Тело цикла синуса у F407 после -Os — семь команд на отсчёт (дизассемблер):

lsrs    r7, r6, #24
add r6, r5
ldrh.w  r7, [lr, r7, lsl #1]
asr.w   r7, r7, ip
strh.w  r7, [r1], #2
subs    r4, #1
bcs.n   80002dc <dds_fill+0x40>

Оценка снизу, по такту на команду: 1 млн отсчётов в секунду — 7 млн тактов из 168 млн, около 4,2 % ядра F407; у F103 при 281250 отсчётах — 2,7 % из 72 МГц. Вариант в прерывании: 16 команд обработчика плюс 12 тактов входа (7а.5) и примерно столько же на выход — около 40 тактов: при 100 кГц это 2,4 % ядра, при 1 МГц было бы 24 %. Ожидание флеш-памяти добавит к этим цифрам — на железе их не мерили.

Пределы по частоте у F407:


36к.9 Грабли

На щупе вдвое больше, чем на экране генератора. Выход рассчитан на 50 Ом, а нагрузка высокоомная: при «1 Vpp» на входе осциллографа 1,9999 В (36к.3). Переключить нагрузку на High Z — или помнить про ×2.

Вывод АЦП получил 6,6 В. Тот же эффект со смещением: 3,3 Vpp и 1,65 В в режиме «50 Ом» — на высокоомном выводе 0…6,60 В. Перед подключением к плате посмотреть выход генератора осциллографом.

«Чистый» синус с лишними частотами рядом. Усечение фазы DDS: таблица 256 точек — помеха до −48,13 дБн; 4096 точек — до −72,24 дБн (36к.2).

Синус из 10 точек на период с образом на −19 дБ. Ступеньки ЦАП: образ на fs − f относительно сигнала −19,08 дБ при f = fs/10. Нужен фильтр восстановления (36к.4).

Выброс 65,5 % на фронте. Источник с малым сопротивлением на кабеле без согласования (36к.5). Генератор с 50 Ом на том же кабеле выброса не даёт.

Duty 20 % в ТЗ записали как «скважность 20 %». Скважность по-русски — T/τ: при Duty 20 % она равна 5 (36к.5).

Меандр из таблицы дрожит. Фронт привязан к отсчёту: при 1 МГц — скачки на 1 мкс, периоды 333 и 334 отсчёта (36к.5).

Диаграмма Боде рассыпается на высоких частотах. Сигнал на выходе фильтра ушёл в шум: на 1 МГц у RC-фильтра 0,253 мВ на 1 В. Var.Amp или конец качания пониже (36к.6).

АЧХ фильтра прошивки «шире», чем рассчитано. Качание быстрее постоянной времени фильтра: 0,05 с на две декады показали −1,78 дБ вместо −3,01 дБ на срезе (36к.7).

Прошивка генератора толще, чем ожидалось. 64-битное деление в шаге фазы притянуло 700 байт библиотеки (36к.8).

Синус из прерывания дрожит, когда работает UART. Отсчёт выходит в момент записи DHR12R1, а обработчик сдвигают другие прерывания. Запуск ЦАП от таймера и DMA (36к.8).


36к.10 Итоги

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

dds36k.h — фазовый аккумулятор: структура, шаг, отсчёт для прерывания.

/* dds36k.h — раздел 36к: прямой цифровой синтез (DDS) для генератора на STM32.
 * Общая логика для ЦАП F407 и ШИМ F103, на компьютере и в QEMU — одна и та же. */
#ifndef DDS36K_H
#define DDS36K_H
#include <stdint.h>

#define DDS_TAB_BITS 8u                     /* 256 точек синуса: старшие 8 бит фазы */
#define DDS_TAB_LEN  (1u << DDS_TAB_BITS)
#define DDS_FULL     4095u                  /* 12 бит, полная шкала ЦАП */

extern const uint16_t dds_sine[DDS_TAB_LEN];

typedef struct {
    uint32_t phase;   /* фазовый аккумулятор: 2^32 — один период */
    uint32_t word;    /* шаг фазы за отсчёт (tuning word) */
    uint32_t duty;    /* 0 — синус; иначе меандр: 1, пока phase < duty */
    uint8_t  shift;   /* сдвиг отсчёта вправо: 0 — 12 бит для ЦАП, 4 — 8 бит для ШИМ */
} dds_t;

/* Шаг для частоты f (в сотых долях герца) при частоте отсчётов fs, с округлением. */
uint32_t dds_word(uint32_t f_centihz, uint32_t fs_hz);
/* Порог фазы для меандра: доля периода в десятых процента (500 — 50 %). */
uint32_t dds_duty(uint32_t permille);
/* n следующих отсчётов в buf; фаза продолжается с того места, где кончился прошлый вызов. */
void dds_fill(dds_t *d, uint16_t *buf, uint32_t n);

/* Один отсчёт синуса — для варианта «в прерывании таймера». */
static inline uint16_t dds_next(dds_t *d)
{
    uint16_t v = (uint16_t)(dds_sine[d->phase >> (32u - DDS_TAB_BITS)] >> d->shift);
    d->phase += d->word;
    return v;
}
#endif

dds36k.c — таблица синуса, шаг фазы, порог меандра и заполнение буфера.

/* dds36k.c — раздел 36к: шаг фазы, порог меандра и заполнение буфера отсчётами. */
#include "dds36k.h"

/* round(2048 + 2047 * sin(2 * pi * k / 256)), k = 0…255 */
const uint16_t dds_sine[DDS_TAB_LEN] = {
    2048, 2098, 2148, 2199, 2249, 2299, 2348, 2398, 2447, 2497, 2545, 2594, 2642, 2690, 2738, 2785,
    2831, 2878, 2923, 2968, 3013, 3057, 3100, 3143, 3185, 3227, 3267, 3307, 3347, 3385, 3423, 3459,
    3495, 3531, 3565, 3598, 3630, 3662, 3692, 3722, 3750, 3777, 3804, 3829, 3853, 3876, 3898, 3919,
    3939, 3958, 3975, 3992, 4007, 4021, 4034, 4045, 4056, 4065, 4073, 4080, 4085, 4089, 4093, 4094,
    4095, 4094, 4093, 4089, 4085, 4080, 4073, 4065, 4056, 4045, 4034, 4021, 4007, 3992, 3975, 3958,
    3939, 3919, 3898, 3876, 3853, 3829, 3804, 3777, 3750, 3722, 3692, 3662, 3630, 3598, 3565, 3531,
    3495, 3459, 3423, 3385, 3347, 3307, 3267, 3227, 3185, 3143, 3100, 3057, 3013, 2968, 2923, 2878,
    2831, 2785, 2738, 2690, 2642, 2594, 2545, 2497, 2447, 2398, 2348, 2299, 2249, 2199, 2148, 2098,
    2048, 1998, 1948, 1897, 1847, 1797, 1748, 1698, 1649, 1599, 1551, 1502, 1454, 1406, 1358, 1311,
    1265, 1218, 1173, 1128, 1083, 1039,  996,  953,  911,  869,  829,  789,  749,  711,  673,  637,
     601,  565,  531,  498,  466,  434,  404,  374,  346,  319,  292,  267,  243,  220,  198,  177,
     157,  138,  121,  104,   89,   75,   62,   51,   40,   31,   23,   16,   11,    7,    3,    2,
       1,    2,    3,    7,   11,   16,   23,   31,   40,   51,   62,   75,   89,  104,  121,  138,
     157,  177,  198,  220,  243,  267,  292,  319,  346,  374,  404,  434,  466,  498,  531,  565,
     601,  637,  673,  711,  749,  789,  829,  869,  911,  953,  996, 1039, 1083, 1128, 1173, 1218,
    1265, 1311, 1358, 1406, 1454, 1502, 1551, 1599, 1649, 1698, 1748, 1797, 1847, 1897, 1948, 1998,
};

uint32_t dds_word(uint32_t f_centihz, uint32_t fs_hz)
{
    uint64_t den = (uint64_t)fs_hz * 100u;            /* fs в сотых долях герца */
    return (uint32_t)((((uint64_t)f_centihz << 32) + den / 2u) / den);
}

uint32_t dds_duty(uint32_t permille)
{
    return (uint32_t)(((uint64_t)permille << 32) / 1000u);
}

void dds_fill(dds_t *d, uint16_t *buf, uint32_t n)
{
    uint32_t ph = d->phase;
    const uint32_t w = d->word;
    const uint8_t sh = d->shift;
    if (d->duty != 0u) {                               /* меандр: сравнение фазы с порогом */
        const uint32_t duty = d->duty;
        const uint16_t hi = (uint16_t)(DDS_FULL >> sh);
        while (n--) {
            *buf++ = ph < duty ? hi : 0u;
            ph += w;
        }
    } else {                                           /* синус: старшие 8 бит фазы — номер точки */
        while (n--) {
            *buf++ = (uint16_t)(dds_sine[ph >> (32u - DDS_TAB_BITS)] >> sh);
            ph += w;
        }
    }
    d->phase = ph;
}

fw36k_f4.c — F407: ЦАП + DMA или отсчёт в прерывании TIM6.

/* fw36k_f4.c — раздел 36к: генератор DDS на STM32F407, выход ЦАП PA4.
 * Основной вариант: TIM6 запускает ЦАП миллион раз в секунду, DMA1 Stream5 по кругу гонит буфер
 * из двух половин в DHR12R1 (связка из 15.4), а прерывание DMA дописывает освободившуюся половину
 * отсчётами из фазового аккумулятора. Вариант -DDDS_IRQ: без DMA, один отсчёт в прерывании TIM6
 * сто тысяч раз в секунду. Такт: HSE 8 МГц -> PLL 168 МГц, APB1 42 МГц, таймеры APB1 — 84 МГц. */
#include "stm32f4xx.h"
#include "dds36k.h"

#ifndef DDS_IRQ
#define FS_HZ   1000000u                   /* 84 МГц / 84 */
#define HALF    256u                       /* отсчётов в половине буфера: прерывание раз в 256 мкс */
static uint16_t buf[2u * HALF];
#else
#define FS_HZ   100000u                    /* 84 МГц / 840 */
#endif

static dds_t gen;

/* Частота меняется на ходу: шаг — одно 32-битное слово, запись неделимая. */
void gen_set(uint32_t f_centihz)
{
    gen.word = dds_word(f_centihz, FS_HZ);
}

#ifndef DDS_IRQ
void DMA1_Stream5_IRQHandler(void)
{
    uint32_t isr = DMA1->HISR;
    if (isr & DMA_HISR_HTIF5) {            /* DMA ушёл во вторую половину — первая свободна */
        DMA1->HIFCR = DMA_HIFCR_CHTIF5;
        dds_fill(&gen, buf, HALF);
    }
    if (isr & DMA_HISR_TCIF5) {            /* DMA вернулся в начало — свободна вторая */
        DMA1->HIFCR = DMA_HIFCR_CTCIF5;
        dds_fill(&gen, buf + HALF, HALF);
    }
}
#else
void TIM6_DAC_IRQHandler(void)
{
    TIM6->SR = 0u;
    DAC->DHR12R1 = dds_next(&gen);         /* без запуска ЦАП берёт значение через такт APB1 */
}
#endif

static void clock_168(void)
{
    RCC->CR |= RCC_CR_HSEON;
    while (!(RCC->CR & RCC_CR_HSERDY)) {
    }
    FLASH->ACR = FLASH_ACR_ICEN | FLASH_ACR_DCEN | FLASH_ACR_PRFTEN | FLASH_ACR_LATENCY_5WS;
    RCC->PLLCFGR = (8u << RCC_PLLCFGR_PLLM_Pos) | (336u << RCC_PLLCFGR_PLLN_Pos)
                 | RCC_PLLCFGR_PLLSRC_HSE | (7u << RCC_PLLCFGR_PLLQ_Pos);      /* PLLP = 2 */
    RCC->CFGR = RCC_CFGR_PPRE1_DIV4 | RCC_CFGR_PPRE2_DIV2;
    RCC->CR |= RCC_CR_PLLON;
    while (!(RCC->CR & RCC_CR_PLLRDY)) {
    }
    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) {
    }
}

int main(void)
{
    clock_168();
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_DMA1EN;
    RCC->APB1ENR |= RCC_APB1ENR_DACEN | RCC_APB1ENR_TIM6EN;
    GPIOA->MODER |= GPIO_MODER_MODER4;     /* PA4 — аналоговый */
    gen.shift = 0u;
    gen_set(100000u);                      /* 1000,00 Гц */
    TIM6->PSC = 0u;
    TIM6->ARR = 84000000u / FS_HZ - 1u;
#ifndef DDS_IRQ
    dds_fill(&gen, buf, 2u * HALF);        /* обе половины готовы до первого запуска */
    DMA1_Stream5->PAR  = (uint32_t)&DAC->DHR12R1;
    DMA1_Stream5->M0AR = (uint32_t)buf;
    DMA1_Stream5->NDTR = 2u * HALF;
    DMA1_Stream5->CR = (7u << DMA_SxCR_CHSEL_Pos) | DMA_SxCR_DIR_0 | DMA_SxCR_MINC | DMA_SxCR_CIRC
                     | DMA_SxCR_PSIZE_0 | DMA_SxCR_MSIZE_0 | DMA_SxCR_PL_1 | DMA_SxCR_HTIE | DMA_SxCR_TCIE;
    DMA1_Stream5->CR |= DMA_SxCR_EN;
    NVIC_EnableIRQ(DMA1_Stream5_IRQn);
    TIM6->CR2 = TIM_CR2_MMS_1;             /* 010: событие обновления — запуск ЦАП */
    DAC->CR = DAC_CR_TEN1 | DAC_CR_DMAEN1 | DAC_CR_EN1;   /* TSEL1 = 000: TIM6 TRGO */
#else
    DAC->CR = DAC_CR_EN1;
    TIM6->DIER = TIM_DIER_UIE;
    NVIC_EnableIRQ(TIM6_DAC_IRQn);
#endif
    TIM6->CR1 = TIM_CR1_CEN;
    for (;;) {
        __WFI();
    }
}

fw36k_f1.c — F103: ШИМ TIM2 + DMA вместо ЦАП.

/* fw36k_f1.c — раздел 36к: генератор DDS на STM32F103 без ЦАП: ШИМ TIM2_CH1 (PA0) + RC-фильтр.
 * ШИМ 8 бит: ARR = 255, 72 МГц / 256 = 281250 Гц — это и частота отсчётов. На каждом обновлении
 * TIM2 запрос TIM2_UP (DMA1 Channel 2) кладёт следующий отсчёт в CCR1; предзагрузка CCR1 вводит его
 * с начала следующего периода. Прерывание DMA дописывает освободившуюся половину буфера.
 * Такт: HSE 8 МГц -> PLL x9 = 72 МГц, APB1 36 МГц, таймер TIM2 — 72 МГц. */
#include "stm32f10x.h"
#include "dds36k.h"

#define FS_HZ   281250u                    /* 72 МГц / 256 */
#define HALF    128u                       /* прерывание раз в 455 мкс */
static uint16_t buf[2u * HALF];
static dds_t gen;

void gen_set(uint32_t f_centihz)
{
    gen.word = dds_word(f_centihz, FS_HZ);
}

void DMA1_Channel2_IRQHandler(void)
{
    uint32_t isr = DMA1->ISR;
    if (isr & DMA_ISR_HTIF2) {
        DMA1->IFCR = DMA_IFCR_CHTIF2;
        dds_fill(&gen, buf, HALF);
    }
    if (isr & DMA_ISR_TCIF2) {
        DMA1->IFCR = DMA_IFCR_CTCIF2;
        dds_fill(&gen, buf + HALF, HALF);
    }
}

static void clock_72(void)
{
    RCC->CR |= RCC_CR_HSEON;
    while (!(RCC->CR & RCC_CR_HSERDY)) {
    }
    FLASH->ACR = FLASH_ACR_PRFTBE | FLASH_ACR_LATENCY_2;
    RCC->CFGR = RCC_CFGR_PLLSRC | RCC_CFGR_PLLMULL9 | RCC_CFGR_PPRE1_DIV2;
    RCC->CR |= RCC_CR_PLLON;
    while (!(RCC->CR & RCC_CR_PLLRDY)) {
    }
    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) {
    }
}

int main(void)
{
    clock_72();
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
    GPIOA->CRL = (GPIOA->CRL & ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0))
               | GPIO_CRL_MODE0 | GPIO_CRL_CNF0_1;        /* PA0: альтернативная, push-pull, 50 МГц */
    gen.shift = 4u;                        /* 12 бит таблицы -> 0…255 под ARR = 255 */
    gen_set(100000u);                      /* 1000,00 Гц */
    dds_fill(&gen, buf, 2u * HALF);
    DMA1_Channel2->CPAR  = (uint32_t)&TIM2->CCR1;
    DMA1_Channel2->CMAR  = (uint32_t)buf;
    DMA1_Channel2->CNDTR = 2u * HALF;
    DMA1_Channel2->CCR = DMA_CCR2_MINC | DMA_CCR2_CIRC | DMA_CCR2_DIR | DMA_CCR2_PSIZE_0 | DMA_CCR2_MSIZE_0
                       | DMA_CCR2_PL_1 | DMA_CCR2_HTIE | DMA_CCR2_TCIE | DMA_CCR2_EN;
    NVIC_EnableIRQ(DMA1_Channel2_IRQn);
    TIM2->PSC = 0u;
    TIM2->ARR = 255u;
    TIM2->CCR1 = buf[0];
    TIM2->CCMR1 = TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1PE;   /* ШИМ, режим 1 */
    TIM2->CCER = TIM_CCER_CC1E;
    TIM2->DIER = TIM_DIER_UDE;             /* запрос DMA на каждом обновлении */
    TIM2->EGR = TIM_EGR_UG;
    TIM2->CR1 = TIM_CR1_ARPE | TIM_CR1_CEN;
    for (;;) {
        __WFI();
    }
}

start36k.c — стартовый код и таблица векторов для F103 и F407.

/* start36k.c — раздел 36к: стартовый код Cortex-M3/M4 (F103, F407 и стенд в QEMU netduino2).
 * Таблица векторов: F103 — DMA1 Channel 2 (прерывание 12), F407 — DMA1 Stream5 (16) и TIM6_DAC (54).
 * Копия .data, обнуление .bss, main. */
#include <stdint.h>

extern uint32_t _estack, _sidata, _sdata, _edata, _sbss, _ebss;
int main(void);

void Default_Handler(void)
{
    for (;;)
        ;
}
void HardFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void DMA1_Channel2_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void DMA1_Stream5_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void TIM6_DAC_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));

void Reset_Handler(void)
{
    uint32_t *s = &_sidata, *d = &_sdata;
    while (d < &_edata)
        *d++ = *s++;
    for (d = &_sbss; d < &_ebss;)
        *d++ = 0;
    main();
    for (;;)
        ;
}

#ifdef STM32F407xx
#define NVEC (16 + 55)
#else
#define NVEC (16 + 13)
#endif
__attribute__((section(".isr_vector"), used)) void (*const vectors[NVEC])(void) = {
    [0] = (void (*)(void))&_estack,
    [1] = Reset_Handler,
    [2] = Default_Handler,   /* NMI */
    [3] = HardFault_Handler,
    [4 ... 15] = Default_Handler,
#ifdef STM32F407xx
    [16 ... 16 + 15] = Default_Handler,
    [16 + 16] = DMA1_Stream5_IRQHandler,
    [16 + 17 ... 16 + 53] = Default_Handler,
    [16 + 54] = TIM6_DAC_IRQHandler,
#else
    [16 ... 16 + 11] = Default_Handler,
    [16 + 12] = DMA1_Channel2_IRQHandler,
#endif
};

f407_36k.ld — сценарий компоновки для F407.

/* f407_36k.ld — раздел 36к: STM32F407VG: 1 МБ флеш-памяти, 128 КБ основного ОЗУ (SRAM1 + SRAM2). */
ENTRY(Reset_Handler)
MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 1024K
  RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
_estack = ORIGIN(RAM) + LENGTH(RAM);
SECTIONS
{
  .isr_vector : { KEEP(*(.isr_vector)) } > FLASH
  .text : { *(.text .text.*) *(.rodata .rodata.*) . = ALIGN(4); } > FLASH
  .ARM.exidx : { *(.ARM.exidx*) } > FLASH
  .data : { _sdata = .; *(.data .data.*) . = ALIGN(4); _edata = .; } > RAM AT > FLASH
  _sidata = LOADADDR(.data);
  .bss (NOLOAD) : { _sbss = .; *(.bss .bss.*) *(COMMON) . = ALIGN(8); _ebss = .; } > RAM
  end = .;
}

f103_36k.ld — сценарий компоновки для F103 и стенда в QEMU.

/* f103_36k.ld — раздел 36к: STM32F103C8 (BluePill): 64 КБ флеш-памяти, 20 КБ ОЗУ.
 * Тем же сценарием собран стенд для QEMU netduino2 (STM32F205: памяти там больше, хватает и этой). */
ENTRY(Reset_Handler)
MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 64K
  RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}
_estack = ORIGIN(RAM) + LENGTH(RAM);
SECTIONS
{
  .isr_vector : { KEEP(*(.isr_vector)) } > FLASH
  .text : { *(.text .text.*) *(.rodata .rodata.*) . = ALIGN(4); } > FLASH
  .ARM.exidx : { *(.ARM.exidx*) } > FLASH
  .data : { _sdata = .; *(.data .data.*) . = ALIGN(4); _edata = .; } > RAM AT > FLASH
  _sidata = LOADADDR(.data);
  .bss (NOLOAD) : { _sbss = .; *(.bss .bss.*) *(COMMON) . = ALIGN(8); _ebss = .; } > RAM
  end = .;
}

Дальше: раздел 36л «Анализаторы: логический, спектра, цепей (NanoVNA) и шин».

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