
Привет, Хабр!
Меня зовут @momentics. Я строю HW/SW команды, процессы производства, делаю реинвент технологий, починяю старые и создаю новые HW/SW продукты. Хочу поведать свою недавнюю историю. Надеюсь, вам понравится.
Прошивок для NanoVNA хватает. Есть оригинальные проекты, есть форки, есть версии под разные ревизии board. Я тоже в свое время сделал NanoVNA-X. Взял low-level у DiSlord, а организацию приложения существенно переделал (началось все с вполне прикладной задачи. Мне понадобились исправления для работы над другим устройством, электротомографом. Нужные и запрошенные мне изменения в проекте DiSlord, не появлялись, поэтому я занялся ими сам. Электротомограф и та версия прошивки, заслуживают отдельного рассказа. Сейчас не о них).
Позже захотелось написать для этих приборов прошивку с нуля на no_std Rust. Не перенести существующее приложение строка в строку, а заново разобраться с его устройством. Без ОС, с прямой работой с аппаратурой. Так появился NanoVNA-X-BM для NanoVNA-H и NanoVNA-H4, F V2/V3 Что именно мне дал Rust, хочу разобрать отдельно и собираюсь написать отдельную статью.
При таком подходе приходится снова проходить через вопросы, с которыми @hugen79, @edy555 и @DiSlord отлаживались длительное время. Пока пользуешься готовым измерительным трактом, многие решения принимаешь как данность. Когда собираешь его программную часть заново, уже нужно понимать, почему они работают…
С накоплением у Дмитрия как раз есть готовое решение. В NanoVNA-D отсчеты поступают непрерывно, а программа складывает результаты нескольких блоков отдельно для каждого канала. Но для получения новых функционально-качественных характеристик, я изменил организацию захвата, а вместе с ней изменились и требования к накоплению. Ниже сравню оба варианта.
Меня интересовал вполне практический вопрос. Как оставить время экрану и USB, если для уменьшения шума нужно накапливать данные? Можно ли прерывать захват, а потом продолжать с того же места?
При неудачном способе накопления два совершенно пригодных блока могут дать 0/0. Ниже покажу, откуда это берется и что с этим делать. Формулы и код убраны под раскрывающиеся блоки, основной рассказ можно читать без них.
Зачем прибору два приемных канала
Если вы не работали с VNA, проще всего представить проверку радиочастотного фильтра. Подаем на его вход синусоиду и смотрим, что получилось на выходе. Затем меняем частоту и повторяем. Так можно узнать, где фильтр пропускает сигнал, а где ослабляет.
Но одного уровня недостаточно. Нас интересует и сдвиг по фазе, то есть насколько выходная синусоида опережает опорную или отстает от нее. Поэтому приемных канала два. Один записывает опорный сигнал, другой отклик фильтра. Сравниваем их амплитуды и фазы.
Это пример измерения передачи в векторном анализаторе цепей, или VNA. У прибора есть и измерение отражения, но для нашего вопроса достаточно примера с фильтром. Высокочастотные сигналы внутри приемника переносятся на более низкую промежуточную частоту. Дальше прошивка работает с оцифрованными отсчетами этих двух каналов.

По отсчетам одного блока можно вычислить отношение амплитуд и разность фаз. Дальше для краткости буду говорить об отношении каналов. Сам приемник тоже вносит погрешности, которые нужно учитывать калибровкой. Здесь ее разбирать не будем. Пока мы получаем сырое отношение, а не откалиброванную характеристику фильтра.
По одному короткому блоку такое отношение вычислить можно. Но на отсчетах есть шум. Хочется повторить измерение несколько раз и усреднить результат. Тут и возникает вопрос, который легко пропустить при работе с готовым кодом.
Что меняется после паузы в захвате
В NanoVNA-X-BM отсчеты записываются в буфер через DMA, без переноса каждого значения процессором. Готовый блок обрабатывается в основном цикле. Длительное накопление ограничено по времени за один проход, чтобы можно было остановить захват, обслужить USB и экран, а потом продолжить.
На время паузы мы прекращаем запись отсчетов, но генератор не выключаем. После возобновления записи первый отсчет необязательно попадет в ту же точку периода, что и в предыдущем блоке. Например, первый блок начинался возле перехода опорного сигнала через ноль, а новый начался возле его максимума.
При обработке каждого блока отсчет фазы начинается заново. Поэтому после паузы код может получить другие фазы в обоих каналах только из-за нового момента начала записи.
При этом каналы записываются согласованно. Если отклик отставал от опорного сигнала на четверть периода и в тракте ничего не поменялось, после паузы он отстает на ту же четверть периода. Мы сдвинули начало записи для обоих сигналов, а не один сигнал относительно другого.
В идеальном случае пауза не мешает измерить эту разность внутри нового блока. Можно начать запись возле нуля или возле максимума синусоиды и получить одинаковый ответ (о шуме и неточном совпадении частот еще поговорим ниже).

Чтобы понять, почему после паузы это важно, сначала посмотрим, на что опирается накопление в NanoVNA-D.
Почему у DiSlord можно складывать блоки
В NanoVNA-D DMA работает непрерывно, циклически. Пока заполняется вторая половина буфера, обработчик прерывания считает результат для первой. Потом половины меняются ролями. Для начала нового измерения программа не выключает и не запускает приемник заново. Она ждет установления сигнала, сбрасывает накопленные суммы и начинает принимать в них следующие блоки из уже работающего потока.
Есть еще одна важная деталь. Длина блока согласована с промежуточной частотой. При настройках в рассматриваемых исходниках за один блок проходят ровно три периода сигнала. Поэтому при точном соответствии частот настройкам следующий блок начинается с той же фазы, что и предыдущий. Ее значение может быть неизвестно, но между соседними блоками оно не меняется.
В такой ситуации результаты каждого канала можно складывать. При независимом шуме его случайная часть частично компенсируется, а полезный сигнал складывается. В конце достаточно разделить накопленную комплексную амплитуду отклика на накопленную комплексную амплитуду опорного сигнала. Отдельно делить оба результата на число блоков не нужно, этот множитель все равно сократится.
Такое накопление называют когерентным. Оно обходится небольшим числом сумм и хорошо подходит для непрерывного согласованного потока. Более узкая настройка полосы в NanoVNA-D означает, что для одной точки будет обработано больше блоков. Измерение занимает дольше, зато при подходящих свойствах шума его влияние уменьшается.

Это не означает, что у DiSlord экран и USB перестают работать. Захват и обработка DMA обслуживаются прерываниями. Но программа должна успевать обработать готовую половину буфера, прежде чем DMA снова начнет в нее писать. В NanoVNA-X-BM выбран другой режим работы, с подтвержденной остановкой захвата перед обычными операциями ввода-вывода. Сам по себе код на Rust этого не требует, это решение об организации приложения, с целью получения новых свойств в других частях прошивки.
Как устроено накопление NanoVNA-D в исходниках
Вdsp.c функция dsp_process() накапливает четыре компоненты. Две принадлежат измерительному каналу, две опорному. В обычной ветке используются накопители float, в варианте с DSP-инструкциями Cortex-M4 используются int64_t. После накопления calculate_gamma() вычисляет комплексное отношение сумм.
Если обозначить комплексные результаты отдельных блоков как Rk и Sk, смысл вычисления такой:
Это не сумма отношений отдельных блоков и не сумма взаимных произведений. В активной реализации деление записано через промежуточные отношения к косинусной компоненте опорного канала; приведенная формула описывает его математический смысл при допустимых знаменателях.
В main.c макрос DSP_START() задает задержку и счетчик, но не перезапускает DMA. Первое подходящее событие после задержки сбрасывает накопители и отбрасывает соответствующий блок. Затем обрабатывается config._bandwidth + 1 блоков. DSP вызывается из обработчика прерывания DMA.
В рассматриваемой конфигурации частота дискретизации равна 192 кГц, длина блока составляет 48 стереокадров, IF равна 12 кГц. Поэтому:
Таблица демодуляции начинается заново в каждом блоке, но за время блока сам сигнал тоже проходит целое число периодов. Это номинальное согласование настроек. Фактическое рассогласование частот и пропуски обслуживания требуют отдельного анализа, исходники сами по себе не подтверждают аппаратные временные запасы.
Настройка полосы вычисляется как
где M является числом принимаемых блоков. Например, 1 кГц соответствует четырем блокам и 1 мс принимаемых данных, а 100 Гц соответствует сорока блокам и 10 мс. Задержка установления и отброшенный блок в эти времена не входят. По счетчику допускается от 1 до 512 принимаемых блоков.
Эти решения находятся в dsp.c, main.c, nanovna.h и аппаратных i2s.c проекта NanoVNA-D.
Откуда берется 0/0
Теперь вернемся к захвату с паузами. Если сохранить накопление из предыдущего раздела, но разрешить блокам начинаться с разных фаз, его условие работы уже не выполняется.
Здесь удобно нарисовать по стрелке для каждого канала. Длина показывает амплитуду, угол показывает фазу. Именно такую пару значений дает обработка блока, когда программа сравнивает отсчеты с синусом и косинусом выбранной частоты. В коде это комплексное число, но для объяснения достаточно стрелок.
Пусть в первом блоке стрелка опорного сигнала смотрит вправо. Стрелка отклика направлена вниз и вдвое короче. Значит, отклик вдвое слабее и отстает на четверть периода.
Теперь возьмем второй блок с начальной фазой, отличающейся на пол-оборота. Стрелка опорного сигнала смотрит влево, стрелка отклика вверх. Обе стрелки развернулись, но отношение между ними прежнее. Отклик по-прежнему вдвое слабее и отстает на четверть периода.
Попробуем сначала накопить данные каждого канала отдельно. Складываем две стрелки опорного сигнала одинаковой длины, направленные вправо и влево. Они компенсируют друг друга. Складываем стрелки отклика, направленные вниз и вверх. Их сумма тоже равна нулю.
Теперь нужно разделить сумму комплексных амплитуд отклика на сумму комплексных амплитуд опорного сигнала, чтобы найти отношение каналов. Но обе суммы нулевые. Получаем 0/0 вместо ответа, хотя по каждому блоку отдельно могли определить и ослабление, и сдвиг фазы.

В реальных данных есть шум, поэтому суммы, скорее всего, окажутся не точно нулевыми, а просто маленькими. Вместо полезных сигналов мы будем делить их шумовые остатки. Результат станет нестабильным, а если знаменатель окажется особенно близок к нулю, получим большой выброс.
Теперь видно, почему этот пример не опровергает решение DiSlord. В его схеме согласованность поддерживается непрерывным потоком и целым числом периодов в блоке. Если убрать это условие, прежний способ нельзя переносить без изменений. Именно с таким случаем приходится считаться в моей схеме захвата.
Как убрать общую фазу перед накоплением
По отдельности оба блока давали правильное отношение. Значит, ошибку мы внесли уже при объединении. До сложения нужно убрать то, что изменилось после паузы, а именно общий поворот двух стрелок.
Мысленно повернем каждую пару так, чтобы стрелка опорного сигнала смотрела вправо. В первом блоке ничего менять не надо. Во втором повернем обе стрелки на пол-оборота. В обоих случаях отклик теперь смотрит вниз.
Вычислять угол и отдельно поворачивать стрелки не требуется. Берем комплексную амплитуду опорного сигнала после демодуляции и меняем знак ее мнимой части. Так получаем комплексно-сопряженное значение. Затем умножаем на него комплексную амплитуду отклика. Это убирает общий поворот и одновременно умножает длину стрелки отклика на амплитуду опорного сигнала.
Теперь результаты двух блоков складываются, а не гасят друг друга. Но осталось учесть амплитуду опорного сигнала. Для каждого блока считаем ее квадрат и тоже добавляем к сумме. Когда нужное число блоков набрано, делим накопленное произведение каналов на накопленную сумму квадратов амплитуд опорного сигнала. Получаем искомое отношение.

При таком расчете блоки влияют на результат не поровну. Если в одном из них амплитуда опорного сигнала вдвое больше, его вклад будет в четыре раза весомее. Это разумно, когда шум измерительного канала примерно одинаков в разных блоках. При слабом опорном сигнале тот же шум сильнее искажает измеренное отношение, поэтому давать такому блоку равный вес невыгодно.
В обработке сигналов это известная оценка передаточной функции типа H1, в нашем случае для одной частоты. Для меня важно было понять, что именно она разрешает изменить в прошивке. Начальную фазу следующего блока можно не восстанавливать. При неизменном отношении каналов накопление продолжается и после паузы.
Разница с NanoVNA-D получается вполне конкретная. Там до сложения сохраняется общая фаза, здесь до сложения мы от нее избавляемся. Оба подхода сохраняют нужную разность фаз каналов, но предъявляют разные требования к последовательности блоков.
Тот же пример с комплексными числами
Обозначим комплексные амплитуды опорного сигнала и отклика после демодуляции в блоке k как Rk и Sk. Для постоянного отношения H в модели без шума:
Амплитуда опорного сигнала Ak и общая фаза φk могут меняться между блоками. Само H пока считаем неизменным.
Запишем тот же пример с откликом вдвое слабее опорного сигнала и отставанием на четверть периода. Для него H = −j0.5. Два противофазных блока дадут:
|
Блок |
Опорный сигнал Rk |
Отклик Sk |
Отношение Sk/Rk |
|---|---|---|---|
|
1 |
1 |
−j0.5 |
−j0.5 |
|
2 |
−1 |
j0.5 |
−j0.5 |
При делении сумм получим неопределенность. Если сумма комплексных амплитуд опорного сигнала ненулевая, идеальная модель еще дает правильный ответ, но ее приближение к нулю делает оценку неустойчивой к шуму.
Теперь используем сопряжение:
Общий фазовый множитель сократился. Накапливаем взаимное произведение и мощность опорного сигнала:
В нашем примере оба произведения равны -j0.5. Числитель равен -j, знаменатель равен 2. Результат снова -j0.5. Его модуль равен 0.5, а аргумент равен −90∘. Полезная разность фаз сохранилась.
Под мощностью здесь понимается |Rk|2. При фиксированных нормировке и сопротивлении она пропорциональна физической мощности. Для пиковой амплитуды напряжения A на чисто активном сопротивлении R средняя мощность синусоиды равна A2/(2R) .
Почему блоки получают разный вес
Здесь возникает нормальный вопрос. Зачем все эти произведения, если можно найти отношение в каждом блоке, а потом усреднить готовые ответы?
Можно и так. Общая фаза тоже сократится. Но у обычного среднего все блоки влияют на результат поровну, а у выбранной оценки их влияние зависит от мощности опорного сигнала.
Допустим, один блок дал отношение 0.8, другой 0.5. Амплитуда опорного сигнала во втором в четыре раза больше. Обычное среднее получится 0.65, а накопление с весами по мощности даст около 0.518. Второй блок повлиял в шестнадцать раз сильнее.
По этим двум числам нельзя доказать, что 0.518 ближе к истине. Пример только показывает, чем отличаются способы накопления. За большим весом сильного блока стоит предположение о шуме, а не правило «чем громче, тем правильнее». Перегруженный усилитель вполне способен дать сильный, но искаженный сигнал.

Через метод наименьших квадратов можно показать, при каких условиях такая оценка обоснована. Мы считаем, что шум в опорном канале достаточно мал. Если это не так, долгое накопление может дать устойчивый, но смещенный ответ.
Откуда берутся веса и что меняет шум в опорном канале
Пусть измерительный канал описывается моделью:
Здесь nk обозначает добавочный шум в комплексном результате обработки измерительного канала. Комплексную амплитуду опорного сигнала пока считаем известной без ошибки.
Минимизируем сумму квадратов комплексных ошибок по действительной и мнимой частям h:
При ненулевой суммарной мощности опорного сигнала решение имеет уже знакомый вид:
Это комплексная линейная регрессия через начало координат. Для статистического выигрыша нужны постоянный H, шум с нулевым средним, не коррелированный с опорным сигналом, и сопоставимая дисперсия шума в разных блоках. При различной дисперсии можно использовать взвешенный МНК, учитывающий ее обратную величину. Текущий накопитель этого не делает.
В примере из текста:
Теперь добавим шум в опорный канал. Пусть комплексная амплитуда опорного сигнала без шума равна Xk, а доступны нам:
Величины uk и vk обозначают шумы в комплексных результатах обработки опорного и измерительного каналов соответственно.
Если сигнальная составляющая и оба шума независимы, шумы имеют нулевое среднее, а накопление устойчиво оценивает средние мощности, то при большом числе блоков:
Здесь PX обозначает среднее |Xk|2, а Pu обозначает среднее |uk|2. В знаменатель добавилась мощность шума опорного канала. В полезном числителе такой добавки нет. Возникает смещение к нулю, которое не исчезает от более долгого накопления.
Минимальный порог амплитуды опорного сигнала помогает не принимать заведомо неудачные блоки, но сам по себе не измеряет SNR (отношение сигнал/шум). Он не заменяет проверку качества приемного тракта.
Что меняется в накопителе NanoVNA-X-BM
Если поток непрерывен и согласован, нет причины объявлять когерентное накопление хуже только потому, что здесь выбрана другая формула. Оно позволяет уменьшать влияние случайного шума в обоих каналах еще до их деления. Для этого сигнал должен оставаться стабильным, а шум не должен повторяться одинаково в каждом блоке. Вариант с H1 нужен мне не как стремление к большей точности при любых условиях, а как способ работать с блоками, чья общая фаза может различаться.
В накопителе NanoVNA-X-BM не нужна история отсчетов. После обработки блока его можно освободить. Остаются две компоненты суммы произведений, сумма мощности опорного сигнала и счетчик. Еще объект хранит профиль и заданное число блоков, но сами данные предыдущих захватов ему уже не нужны.
Экономия памяти сама по себе не преимущество перед NanoVNA-D. Там тоже хранятся только несколько накопленных компонент, а не история всех отсчетов. Разница в том, какие величины остаются между блоками и можно ли продолжить накопление после смены общей фазы.
Это удобно не только для возникшей паузы. Если программа не успела получить один блок, следующий еще можно использовать. Его начальная фаза не обязана продолжать фазу пропущенного.
Демодуляция и накопление у меня выполняются в основном цикле. Обработчик DMA подтверждает завершение передачи и управляет банками памяти. Внутри прерывания вся эта арифметика не считается. Для каждого блока выполняются произведения и сложения, а два деления для финального отношения остаются на конец накопления.
То есть пауза не вынуждает выбрасывать уже накопленный результат. Можно вернуться к захвату после обслуживания остальных задач. Именно это свойство было нужно мне при организации суперцикла и обеспечения высокой отзывчивости IO при гораздо большем периоде усреднения.

У этого есть своя цена. В каждом блоке нужно посчитать взаимное произведение и мощность опорного сигнала. Шум опорного канала может дать смещение оценки, о котором говорилось выше. При одинаковом числе блоков или одинаковом времени работы нельзя заранее обещать, что результат будет точнее, чем при когерентном накоплении. Для такого сравнения нужны заданная шумовая модель и учет времени, потерянного на паузах.
Две формулы и разные требования к блокам
Для NanoVNA-D математический смысл накопления:
Для NanoVNA-X-BM:
В модели без шума, при постоянном H и согласованной фазе блоков обе оценки дают одно и то же отношение. При независимых общих поворотах блоков первая может потерять сигнал из-за взаимного сокращения слагаемых. Во второй общий поворот сокращается внутри каждого произведения.
Если же опорный канал шумит, у формул различаются статистические свойства. При постоянных комплексных амплитудах и независимых шумах с нулевым средним усредненные амплитуды в когерентном накоплении стремятся к своим истинным значениям. Их отношение при ненулевом опорном сигнале тоже стремится к истинному H. Это не обещание отсутствия смещения при конечном числе блоков или систематических ошибках.
Для H1 при модели шума, разобранной выше, в знаменателе остается средняя мощность шума опорного канала. Поэтому оценка может сохранять смещение и при долгом накоплении. Здесь нельзя заменить сравнение условий фразой «новая формула лучше подавляет шум».
Место исполнения DSP тоже не является свойством этих формул. В NanoVNA-D он вызывается из DMA ISR, в NanoVNA-X-BM из суперцикла.
Что все равно приходится проверять вокруг DMA
В прошивке используются два normal-mode DMA-банка с разделением владения между CPU и аппаратурой (про это я постараюсь рассказать в преимуществах Rust для подобных устройств в другой статье). DSP получает завершенный блок только после подтверждения окончания записи. Пока CPU читает банк, DMA не может начать его перезаписывать. Фазоинвариантная формула не делает одновременное чтение и запись безопасными.
У меня, каждый блок сопровождается поколением захвата и номером последовательности. Дубликаты, старые версии и блоки чужого поколения не попадают повторно в накопитель. Пропущенная версия учитывается как потеря, но следующий корректный блок можно принять.
Так как менять общую фазу допустимо, смешивать разные измерительные состояния нельзя, то после старта нового packet-захвата первый блок отбрасывается. При смене частоты или порта накопитель сбрасывается. Снижение усиления после перегрузки тоже запускает накопление текущего порта заново.
Код накопителя и проверки перед сложением
В коде арифметика занимает несколько строк. Гораздо больше условий приходится проверить до нее. Пригоден ли блок, не повторный ли он, достаточно ли силен опорный сигнал, нет ли перегрузки?
Слабый опорный сигнал делает отношение неустойчивым. Клиппинг в любом из каналов искажает измерение. Но нулевой отклик при достаточном уровне опорного сигнала вполне допустим. Если фильтр ничего не пропустил, программа должна сохранить этот результат, а не спутать его с отказом захвата.
Проверять это нужно до изменения накопленных сумм. И сами новые суммы сначала вычисляются отдельно. Если при проверке возникла ошибка, старое накопление остается нетронутым (иначе отклоненный блок мог бы незаметно изменить знаменатель или счетчик).
Код накопителя из прошивки NanoVNA-X-BM
Фрагмент взят из PacketAccumulator::accept_block() в src/dsp/packet.rs. Опущены ранние проверки заполнения накопителя, клиппинга и минимальной амплитуды опорного сигнала. Арифметика и порядок фиксации состояния сохранены.
В packet-пути block.reference и block.sample содержат четыре целочисленные Q15-суммы демодулятора, уже переведенные в f32, но еще не нормированные в амплитуду. Complex32 в проекте собственный, без num-complex.
// Общая нормировка демодулятора сократится// при финальном делении.let (r, s) = (block.reference, block.sample);let power = r.re * r.re + r.im * r.im;// Умножение на сопряженную амплитуду опорного// сигнала убирает общий поворот.let cross = Complex32::new( s.re * r.re + s.im * r.im, s.im * r.re - s.re * r.im,);// Проверяем будущие суммы, чтобы ошибка// не испортила накопление.let next_cross = self .cross .checked_add(cross) .map_err(PacketError::Numerical)?;let next_power = self.power + power;if !next_power.is_finite() { return Err(PacketError::Numerical(NumericalError::NonFinite));}self.cross = next_cross;self.power = next_power;self.accepted += 1;Ok(())
Нужное количество принятых блоков задается от 1 до 256. После его набора вызывается finish(). Этот метод приведен целиком.
pub fn finish(&self) -> Result<Complex32, PacketError> { if self.accepted != self.target { return Err(PacketError::Incomplete); } // Знаменатель вещественный, поэтому нужны // два вещественных деления. let value = Complex32::new( self.cross.re / self.power, self.cross.im / self.power, ); value.check().map_err(PacketError::Numerical)}
К этому моменту проверки при приеме блоков уже гарантируют положительную конечную мощность. Неполное накопление не выдается как готовый результат.
Порог когерентной амплитуды опорного сигнала составляет 32 отсчета АЦП. Речь об амплитуде демодулированной составляющей, не об отдельном мгновенном отсчете и не об оценке шумовой полки.
Общая нормировка позволяет немного сэкономить. Для перевода сырых Q15-сумм в пиковую амплитуду нужен множитель 2/(N ⋅ 32767). В числитель и знаменатель он вошел бы в квадрате, поэтому при делении сократится. Накапливаем сырые суммы без этой промежуточной операции. Профиль, длина блока и масштаб каналов внутри накопления фиксированы. При произвольной смене нормировки между блоками изменились бы веса.
Как проверить накопитель на известных сигналах
Чтобы проверить накопитель, в тесте можно задать два блока с одинаковой амплитудой и противоположными начальными фазами. Отношение каналов в обоих блоках известно заранее. Программа должна получить его и после накопления. Если вместо этого она сначала сложит комплексные амплитуды каждого канала, обе суммы окажутся близки к нулю и тест не пройдет. Для такой проверки не нужно ждать подходящего сочетания фаз на реальном приборе.
Дальше проверяются последовательности из большего числа блоков. У них меняются начальная фаза и амплитуда опорного сигнала. В отдельном сценарии меняется еще и отношение каналов. Это нужно, чтобы проверить веса. Если каждый блок дает один и тот же ответ, обычное среднее и среднее с весами совпадут, даже если программа использует не тот способ накопления. А когда ответы различаются, уже можно проверить, что блок с более сильным опорным сигналом действительно влияет на итог сильнее.
Ожидаемый ответ для каждого набора отсчетов рассчитывается отдельно на ПК. Для этого используется прямое вычисление по формулам дискретного преобразования Фурье, причем с более высокой численной точностью, чем в прошивке. Эта реализация не использует ни таблицы коэффициентов, ни функции обработки из прошивки. Поэтому ошибка в таблице или в ее применении не будет автоматически повторена при расчете ожидаемого ответа.
Условия численных тестов и результаты
Host-тесты выполнялись на ПК с конфигурациями STM32F072 и STM32F303, а не на самих Cortex-M0 и Cortex-M4F. Запускались команды:
cargo test-host --test host dsp_packet -- --nocapturecargo test-host-f303 --test host dsp_packet -- --nocapture
В каждом запуске прошли 12 тестов. Ниже результаты двух численных проверок из этого набора.
Эталонное дискретное преобразование Фурье (ДПФ) вычисляется в f64. При сравнении демодуляции и отношения с ним перебирались блоки по 48 и 96 стереокадров, промежуточные частоты (IF) от 4 до 92 кГц с шагом 4 кГц, различные амплитуды, общие и относительные фазы, коэффициенты передачи. Сравнение по тем же квантованным отсчетам отделяет ошибку численной реализации от ошибки квантования исходного сигнала.
Для 88320 принятых сравнений максимальная абсолютная комплексная ошибка составила 2.44422 ⋅ 10−5, средняя составила 4.985 ⋅ 10−7. Фаза проверялась на подмножестве с заданной амплитудой опорного сигнала не ниже 4096 отсчетов и модулем отношения не ниже 0.01. Максимальное расхождение составило 0.0006938713∘. У нулевого отклика определенной фазы нет.
В проверке накопления перебирались обе длины блока и все количества от 1 до 256. Для двух блоков общие фазы различались на 180∘. В остальных случаях фаза менялась по детерминированной последовательности, менялись амплитуда опорного сигнала и модуль отношения, добавлялся небольшой шум. Максимальное абсолютное комплексное расхождение для 512 итоговых наборов составило 3.705 ⋅ 10−7.
Тесты находятся в tests/host/dsp_packet.rs. Генератор сигналов и эталонная ДПФ находятся в tests/host/measurement_vectors.rs.
Скромные ошибки из тестов не стоит переносить на весь прибор. Здесь сравнивались две численные реализации на одних отсчетах. Это ничего не говорит об измеренной точности усилителей, смесителей и тактирования на конкретной board.
Какие ошибки усреднение не исправит
Мы убрали общий поворот двух каналов, а не любые фазовые ошибки. Если дополнительная задержка появилась только в измерительном канале, программа увидит изменение отношения. Отличить задержку приемника от сдвига фазы в фильтре по одному этому результату она не сможет.
Сам фильтр тоже может измениться, например при нагреве. Тогда накопитель объединит блоки для разных его состояний. Получится взвешенный результат по принятым блокам, а не характеристика фильтра в конце измерения (что происходило в паузах, мы вообще не знаем).
Еще важно, на какую частоту настроена обработка. Программа сравнивает отсчеты с синусом и косинусом выбранной частоты. Если частота принятого сигнала отличается, за время блока их фазы будут расходиться. Из-за этого амплитуда и фаза могут вычисляться с ошибкой. Небольшое расхождение допустимо, но его влияние нужно проверять отдельно. Усреднение не гарантирует, что эта ошибка исчезнет.
Слишком сильный сигнал тоже создает проблемы. При перегрузке усилитель искажает форму синусоиды, причем это может начаться еще до того, как отсчеты достигнут предельных значений АЦП. Усреднение таких данных не вернет исходную форму сигнала. Если перегрузка обнаружена, блок нужно отклонить. При этом отсутствие предельных отсчетов еще не доказывает, что усилитель работает без искажений.
Что проверяется для частоты и конечного окна
Демодуляция рассчитана на когерентный сигнал. Для реального сигнала при отклонении частоты возникает спектральная утечка, из-за которой отношение может зависеть от начальной фазы блока. Формула накопления не устраняет все последствия этого рассогласования.
Профиль захвата проверяет эффективную IF относительно эффективной частоты дискретизации, полученных из фактически закодированных настроек. Допуск составляет Fs/(1000N), где Fs является частотой дискретизации, а N количеством стереокадров в блоке. При номинальных 192 кГц это около 4 Гц для 48 кадров и 2 Гц для 96 кадров. Это программный допуск, а не измеренная стабильность физического генератора.
Вес блока определяется мощностью опорного сигнала. Клиппинг измерительного канала не обязан дать ему большой вес, но нарушает линейную модель и все равно делает данные непригодными для нее. Цифровой контроль клиппинга сам по себе не доказывает аналоговую линейность.
Измерение можно продолжить после паузы
В NanoVNA-X я мог опираться на готовый код измерений. При работе над NanoVNA-X-BM пришлось заново разбираться и с тем, как получать из отсчетов результат измерения. У DiSlord непрерывный поток позволяет складывать комплексные амплитуды. В моей схеме с паузами нужно сначала убрать общий поворот внутри каждого блока.
Практический результат довольно конкретный. Обрабатываем готовый блок, сохраняем его вклад и освобождаем память. При необходимости останавливаем захват, обслуживаем экран и USB, затем продолжаем. Привязывать новый блок к прежней начальной фазе не требуется, а накопленные суммы сохраняются.
Это работает для отношения двух согласованных каналов на выбранной частоте, пока объект и тракт достаточно стабильны. Метод не восстанавливает пропущенные переходные процессы, не исправляет перегрузку и не заменяет калибровку. Зато позволяет сделать паузу там, где она нужна программе, не потеряв само измеряемое отношение.
Ради этого и стоило разобраться, что именно происходит между двумя вызовами захвата!
ссылка на оригинал статьи https://habr.com/ru/articles/1101962/