Один блок захвата в прошивке NanoVNA-D занимает 0,25 мс: частота дискретизации 192 кГц, 48 стереокадров. Промежуточная частота 12 кГц укладывает в этот интервал ровно три периода. Отсюда следствие, которое легко пропустить. Следующий блок начинается практически с той же фазы, что и предыдущий, хотя саму фазу нигде не измеряют и в коде не хранят. На этом совпадении держится всё накопление в проекте Дмитрия (DiSlord).

Уберите совпадение, и два полностью пригодных блока дают 0/0. Разбор опубликован у momentics на Хабре 30 октября; год в доступном тексте страницы не указан, страна автора тоже не названа. Публикация не рецензирована. Это технический блог, а не статья в журнале, и экспериментальная часть ограничена вычислениями на персональном компьютере. Дальше речь о том, что именно ломается, чем предложенная замена отличается от исходного кода и где проходят границы заявленных проверок.

Что складывается в непрерывном потоке

Векторный анализатор цепей измеряет не абсолютный уровень, а отношение двух согласованно записанных каналов: опорного и канала отклика. Демодуляция каждого блока даёт комплексные амплитуды R и S, нужная величина равна их отношению H. Шум на отсчётах мешает, поэтому блоков берут много и усредняют.

В dsp.c проекта NanoVNA-D функция dsp_process() накапливает четыре компоненты: синусную и косинусную части измерительного канала и такие же две для опорного. Сам фрагмент короткий:

acc_samp_s += samp_s;
acc_samp_c += samp_c;
acc_ref_s += ref_s;
acc_ref_c += ref_c;

В обычной ветке накопители имеют тип float, в варианте с DSP-инструкциями Cortex-M4 используются int64_t. Затем calculate_gamma() делит накопленное по каналам. Математический смысл при ненулевых знаменателях простой: сумма S по блокам делится на сумму R по блокам. Заголовок файла указывает авторство Dmitry (DiSlord) и происхождение от кода TAKAHASHI Tomohiro (edy555).

Такая схема законна ровно до тех пор, пока общая фаза у блоков одна и та же. Тогда суммирование усиливает полезный сигнал, а независимый шум гасится лишь частично. Согласование длины блока с промежуточной частотой как раз и обеспечивает это условие: в рассматриваемой конфигурации 48/192000 = 0,25 мс, а 12 кГц дают три полных периода.

Пауза, которая меняет условие задачи

В собственной прошивке NanoVNA-X-BM автор перешёл на захват с подтверждённой остановкой: перед обслуживанием USB и экрана запись прекращается, потом возобновляется. Генератор при этом не выключается. Следствие автор формулирует прямо: «На время паузы мы прекращаем запись отсчетов, но генератор не выключаем. После возобновления записи первый отсчет необязательно попадет в ту же точку периода, что и в предыдущем блоке».

Важно, что каналы остаются согласованными между собой. Если отклик отставал на четверть периода, он отстаёт на неё и после паузы: сдвинулось начало записи, а не один сигнал относительно другого. Портится только общий фазовый множитель блока.

Дальше работает арифметика. Пусть S = H·R для каждого блока, но сам R равен A·exp(jφ), причём φ меняется от блока к блоку. Возьмём два блока с одинаковой амплитудой и противоположными фазами, а отношение пусть будет −0,5j. Тогда в первом блоке R = 1 и S = −0,5j, во втором R = −1 и S = +0,5j. Сумма опорных амплитуд ноль, сумма откликов тоже ноль. Деление даёт 0/0, хотя внутри каждого блока отношение определено полностью.

Автор оговаривает, что идеальная модель без шума ещё может дать верный ответ, если сумма опорных амплитуд не равна нулю, но приближение этой суммы к нулю делает оценку неустойчивой. В реальных данных шум сдвигает числитель и знаменатель от точного нуля, и результат начинает зависеть от случайных остатков. «Вместо полезных сигналов мы будем делить их шумовые остатки», пишет он.

Что накапливают вместо сумм каналов

Предлагаемая замена убирает общий поворот до сложения. Каждый блок даёт произведение амплитуды отклика на комплексно-сопряжённую амплитуду опорного канала, а также квадрат модуля опорной амплитуды. Итоговая оценка выглядит так:

Ĥ = Σ Sₖ·conj(Rₖ) / Σ |Rₖ|²

В произведении Sₖ·conj(Rₖ) множитель exp(jφₖ) сокращается, поэтому смена общей фазы между блоками больше не приводит к взаимному гашению. На том же примере оба произведения равны −0,5j, числитель равен −j, знаменатель равен 2, результат снова −0,5j. Автор называет это оценкой передаточной функции типа H₁, известной в обработке сигналов, и отмечает, что восстанавливать начальную фазу следующего блока не требуется.

У этой формулы есть следствие, которое легко пропустить. Блоки входят в результат с разным весом, пропорциональным |Rₖ|². Если амплитуда опорного сигнала во втором блоке в четыре раза больше, его вклад становится больше в шестнадцать раз. Автор приводит численный пример: отношения 0,8 и 0,5 дают обычное среднее 0,65 и взвешенное около 0,518. Тут же он оговаривает, что эти два числа ничего не доказывают: за большим весом стоит предположение о свойствах шума, а не правило «чем громче, тем правильнее». Перегруженный усилитель способен выдать сильный, но искажённый сигнал.

Откуда берутся веса, показано через метод наименьших квадратов. Если измерительный канал описывается моделью Sₖ = H·Rₖ + nₖ, а опорная амплитуда считается известной точно, то минимизация суммы квадратов комплексных ошибок по действительной и мнимой частям H даёт в точности ту же дробь. Это комплексная линейная регрессия через начало координат. Для статистического выигрыша нужны постоянное H, шум с нулевым средним, не коррелированный с опорным сигналом, и сопоставимая дисперсия шума в разных блоках. При разной дисперсии следовало бы применять взвешенный метод наименьших квадратов, но текущий накопитель этого не делает.

Отдельно разобрано, что происходит при шуме в самом опорном канале. В знаменателе появляется средняя мощность этого шума, в числителе такой добавки нет. Возникает смещение к нулю, которое не исчезает при более долгом накоплении. Минимальный порог амплитуды опорного сигнала, в прошивке это 32 отсчёта АЦП для демодулированной составляющей, отбрасывает заведомо слабые блоки, но не измеряет отношение сигнал/шум.

Как это устроено в коде и что проверено

Арифметика накопителя занимает несколько строк. В методе PacketAccumulator::accept_block() сначала вычисляется мощность опорного сигнала, затем взаимное произведение, причём новые суммы считаются отдельно и только после проверок заменяют старые. Так уже набранное защищено от отклонённого блока. Финальное деление делает finish(): знаменатель вещественный, поэтому нужны два вещественных деления. Общая нормировка демодулятора сокращается, так как входит и в числитель, и в знаменатель в квадрате, поэтому сырые суммы Q15 не переводят в амплитуду заранее.

Логика приёма блока шире одной формулы. Каждый блок несёт поколение захвата и номер последовательности, дубликаты и блоки чужого поколения в накопитель не попадают. Пропуск учитывается как потеря, но следующий корректный блок принимается. После старта нового захвата первый блок отбрасывается, при смене частоты или порта накопитель сбрасывается, снижение усиления после перегрузки запускает накопление заново. Нулевой отклик при достаточном опорном сигнале считается допустимым результатом, а не отказом.

Проверки описаны как host-тесты на персональном компьютере с конфигурациями STM32F072 и STM32F303, а не на самих Cortex-M0 и Cortex-M4F. Запускались две команды, cargo test-host --test host dsp_packet и cargo test-host-f303 --test host dsp_packet, в каждом запуске прошли 12 тестов. Ожидаемый ответ считается независимо, прямым дискретным преобразованием Фурье в f64, без таблиц и функций прошивки, поэтому ошибка в таблице коэффициентов не воспроизвелась бы автоматически.

В сравнении демодуляции с эталоном перебирались блоки по 48 и 96 стереокадров, промежуточные частоты от 4 до 92 кГц с шагом 4 кГц, разные амплитуды, общие и относительные фазы. На 88320 сравнениях максимальная абсолютная комплексная ошибка составила 2,44422·10⁻⁵, средняя 4,985·10⁻⁷. Фаза проверялась на подмножестве с амплитудой опорного сигнала не ниже 4096 отсчётов и модулем отношения не ниже 0,01, максимальное расхождение 0,0006938713°. В проверке накопления перебирались обе длины блока и количества от 1 до 256; для двух блоков общие фазы различались на 180°, в остальных случаях фаза менялась детерминированно, менялись амплитуда опорного сигнала и модуль отношения, добавлялся небольшой шум. Максимальное абсолютное комплексное расхождение для 512 итоговых наборов составило 3,705·10⁻⁷.

Сам автор ограничивает чтение этих чисел: «Скромные ошибки из тестов не стоит переносить на весь прибор. Здесь сравнивались две численные реализации на одних отсчетах. Это ничего не говорит об измеренной точности усилителей, смесителей и тактирования на конкретной плате». Независимых аппаратных измерений, которые сравнили бы два способа накопления на реальном приборе, в доступных источниках нет. Числа относятся к вычислениям на ПК.

Чего оценка не исправляет

Формула убирает общий поворот двух каналов, а не любые фазовые ошибки. Дополнительная задержка только в измерительном канале изменит отношение, и отличить её от сдвига фазы в самом фильтре по одному результату нельзя. Если объект меняется, например при нагреве, накопитель объединит блоки, относящиеся к разным его состояниям, и выдаст взвешенный результат по принятым блокам, а не характеристику фильтра в конце измерения.

Отдельный сюжет это рассогласование частоты. Демодуляция сравнивает отсчёты с синусом и косинусом выбранной частоты; если частота сигнала отличается, за время блока фазы расходятся, возникает спектральная утечка, и отношение может зависеть от начальной фазы блока. Профиль захвата проверяет эффективную промежуточную частоту с допуском Fs/(1000N), где Fs это частота дискретизации, а N число стереокадров. При номинальных 192 кГц это около 4 Гц для 48 кадров и около 2 Гц для 96. Это программный допуск, а не измеренная стабильность физического генератора.

Перегрузка тоже остаётся снаружи. Усилитель искажает форму синусоиды иногда ещё до того, как отсчёты достигнут предельных значений АЦП, и усреднение таких данных не вернёт исходный сигнал. Цифровой контроль клиппинга не доказывает аналоговую линейность.

Что здесь на самом деле произошло

Вывод авторов скромнее, чем мог бы быть заголовок. Он состоит в том, что когерентное накопление NanoVNA-D не ошибочно, а условно: оно опирается на непрерывность потока и целое число периодов в блоке. Как только ради отзывчивости интерфейса захват сделали прерывистым, условие исчезло, и прежний способ нельзя переносить без изменений. Оценка H₁ возвращает работоспособность за счёт смены того, что именно накапливается: вместо сумм амплитуд каналов хранится величина, инвариантная к общему повороту блока.

Наша интерпретация касается цены этого решения. Формула не делает измерение точнее вообще; она меняет набор допущений. Взамен устойчивости к паузе появляется чувствительность к шуму опорного канала и смещение, которое не уходит с числом блоков. При этом в модели без шума и при постоянном H обе схемы дают одно и то же отношение, так что сравнивать их можно только вместе с шумовой моделью и с учётом времени, потерянного на паузах. Автор это и делает, оговаривая, что при одинаковом числе блоков нельзя заранее обещать выигрыш по точности.

Практический смысл для похожих задач простой. Когда в прошивке появляется пауза, меняется не только тайминг, но и статистическая модель накопления, даже если код усреднения остался прежним. Проверять такое удобно на синтетических последовательностях с заданными фазами: два блока с противоположными начальными фазами и известным отношением сразу разделяют две схемы, и ждать подходящего сочетания фаз на реальном приборе не нужно. Остаётся вопрос, который числа из блога не закрывают: как поведёт себя оценка на конкретной плате с её шумом опорного тракта и с реальным рассогласованием частот. Для этого нужны измерения на приборе, а не сравнение двух реализаций на одних и тех же отсчётах. Похожий сюжет о том, как представление числа теряет исходный смысл при переходе между форматами, разобран в материале о том, почему после float исходная десятичная запись не восстанавливается.

Источники: публикация momentics на Хабре, репозиторий NanoVNA-D, файлы dsp.c, main.c и nanovna.h, а также репозиторий NanoVNA-X.

Источники и литература

Схема показывает, что при разных общих фазах блоков прямое суммирование комплексных амплитуд каналов даёт взаимное гашение, а накопление произведений отклика на сопряжённую опорную амплитуду сохраняет отношение каналов.

Фото: Ludovic Delot / Pexels