Files
radar_system/device_firmware/librevna_protocol.md
T
Ayzen 62c0266a6b per-point switching: hardware bring-up, profiles, protocol doc
Verified on the bench that the modified master bitstream excites in every stage
and the RF switch routes: 16 traces per sweep, 62 dB between the two switch
positions of one master port, 9.9k measurements/s. Adds the built master image,
two run profiles, a verification script and librevna_protocol.md, and drops the
stale prototype driver copy that the firmware tree still carried.
2026-09-15 14:41:54 +03:00

15 KiB
Raw Blame History

Бинарный протокол LibreVNA: что шлёт и парсит наш драйвер

Разбор USB-протокола LibreVNA и того, какое его подмножество использует python_app/hardware_full/librevna_multi_device_driver. Про переключение стимула внутри частотной точки — отдельный документ, per_point_switching.md.

Прошивка в репозитории лежит под device_firmware/multi_libreVNA_firmware/reproduce/LibreVNA/Software/VNA_embedded/Application/, дальше он сокращён до <FW>. Версия протокола, заявленная прошивкой — Protocol::Version = 14 (<FW>/Communication/Protocol.hpp:13).

1. Транспорт

Поверх USB у LibreVNA свой бинарный протокол, а не tty/serial. Endpoints:

Endpoint Направление
0x01 хост -> устройство
0x81 устройство -> хост
0x82 отладочные ASCII-логи

Наш драйвер шлёт в 0x01, читает из 0x81, 0x82 не использует. Приёмный поток и очередь пакетов — в librevna_multi_device_driver/transport.py.

Очередь принятых пакетов ограничена и работает по принципу drop-oldest: блокировать USB-поток чтения нельзя, а устаревшие пакеты всё равно выбрасываются предсборочным дренажом (transport.py:109).

2. Структура кадра

Одна и та же почти для всех типов пакетов (<FW>/Communication/PacketConstants.h):

Поле Длина Значение
Header 1 всегда 0x5A
Length 2 длина всего кадра, little-endian
Type 1 номер типа пакета
Payload переменная
CRC32 4 по всему кадру, включая header

Итого накладных расходов 8 байт: длина кадра = 8 + len(payload).

Кодирование и разбор на хосте — в общем модуле одиночного драйвера, а не в multi-device: python_app/hardware_full/librevna_driver/protocol/frame.py, функции encode_frame (строка 25) и decode_frame (строка 49). Поток байт разбирает FrameScanner, который умеет ресинхронизироваться после битого кадра, не роняя устройство.

CRC: у VNADatapoint особый случай

Прошивка не считает CRC для VNADatapoint и пишет туда ноль, потому что расчёт занимает около 18 мкс — это основная часть времени кодирования и передачи точки (<FW>/Communication/Protocol.cpp:149). На приёме она, наоборот, требует ровно нуля: непустой CRC у datapoint считается ошибкой (Protocol.cpp:69 и Protocol.cpp:80).

Наш хост повторяет это поведение точно: decode_frame проверяет CRC32 для всех типов, а для VNADatapoint требует ноль (frame.py:20 — список исключений).

Дополнительно multi-device парсер отбраковывает пакет, у которого тело не кратно 9 байтам на значение: раз CRC у datapoint нет, это единственная защита от ресинхронизации на 0x5A, случайно оказавшийся внутри payload (librevna_multi_device_driver/protocol.py:137).

3. Типы пакетов

Полный enum — <FW>/Communication/Protocol.hpp:573, там 30+ типов. Наш драйвер использует шесть (librevna_multi_device_driver/protocol.py:17):

Тип Номер Зачем
SweepSettings 2 настройка развёртки, она же её запуск
Ack 7 подтверждение команды
Nack 10 отказ
Reference 11 выбор источника опорной частоты
SetIdle 20 остановить развёртку, перевести в idle
VNADatapoint 27 поток измерений

То есть это узкий VNA-sweep-драйвер, а не полная реализация всех режимов прибора. Не используются RequestDeviceInfo, проверка версии протокола, SetTrigger/ClearTrigger, генератор, анализатор спектра, калибровочные пакеты.

4. Пакеты, которые шлём

Reference (11)

Структура — Protocol.hpp:186: uint32 ExtRefOuputFreq плюс битовое поле AutomaticSwitch и UseExternalRef.

Шлём output_frequency_hz = 0 и флаг внешней опоры: выход Ref Out не включаем, а устройству принудительно указываем использовать внешние 10 МГц. Строит build_reference_settings_payload (protocol.py:48), отправляет controller.py.

SweepSettings (2)

Структура — Protocol.hpp:155:

uint64 f_start, f_stop;
uint16 points;
uint32 if_bandwidth;
int16  cdbm_excitation_start;
uint8  standby:1, syncMaster:1, suppressPeaks:1, fixedPowerSetting:1,
       logSweep:1, syncMode:2, unused1:1;
uint16 stages:3, port1Stage:3, port2Stage:3, port3Stage:3, port4Stage:3, unused2:1;
int16  cdbm_excitation_stop;
uint16 dwell_time;          // в микросекундах

Что кладём (build_sweep_settings_payload, protocol.py:58):

  • f_start/f_stop/points/if_bandwidth — из SweepConfiguration;
  • мощность старта и стопа одинаковая, fixedPowerSetting = 1 — аттенюатор и мощность PLL не меняются по развёртке;
  • suppressPeaks = 1, logSweep = 0, standby = 0;
  • syncMode = 3 (триггерная синхронизация), если синхронизация включена;
  • syncMaster = 1 только у мастера;
  • stages — номер последней стадии, не их количество;
  • порт-стадии — см. ниже.

Про syncMode = 3: в стоковой прошивке рядом с полем стоит комментарий «Trigger synchronization (not supported yet by hardware)». В нашем форке этот режим и используется — устройство переводит обе триггерные линии FPGA в Hi-Z.

Порт-стадии — не то, что было в первой версии драйвера. Раньше мастер получал port1Stage = 0, а слейв 1. Сейчас:

  • мастеру отправляется первая стадия блока порта (index * substage_count); без под-стадий это просто индекс порта, то есть стоковая раскладка;
  • приёмным устройствам обе порт-стадии выставляются в DISABLED_PORT_STAGE = 7 (protocol.py:36) — «порт выключен»;
  • ширина блока под-стадий передаётся как log2 в неиспользуемое поле port3Stage.

Почему именно так и почему семёрка безопасна — в per_point_switching.md.

SetIdle (20)

Без payload. Выключает активный режим и останавливает развёртку. Используется как служебная команда: остановить непрерывную развёртку, убраться при ошибке, close().

Ack (7) / Nack (10)

Ответы устройства на команды хоста. wait_for_acknowledgement (transport.py:96) читает входящие пакеты, на Ack считает команду применённой, на Nack бросает исключение, прочие асинхронные пакеты пропускает.

5. VNADatapoint (27) — поток измерений

Шаблон VNADatapoint<s> — Protocol.hpp:25; у прошивки s = 32, то есть до 32 значений в точке (три на стадию при восьми стадиях).

Payload:

Поле Тип Длина
frequency uint64 8
cdBm int16 2
pointNum uint16 2
real_values n × float32 4n
imag_values n × float32 4n
descr_values n × uint8 n

Фиксированная часть — 12 байт, дальше по 9 байт на значение, причём не чередуясь: сначала все реальные части, потом все мнимые, потом все дескрипторы. Отсюда n = (len(payload) - 12) / 9.

Разбирает parse_vna_datapoint_payload (protocol.py:137).

Байт дескриптора

Смещения битов — <FW>/Communication/PacketConstants.h:42:

Биты Значение
0 порт 1
1 порт 2
2 порт 3
3 порт 4
4 опорный приёмник (Reference)
5..7 номер стадии

То же отражено в enum class Source (Protocol.hpp:17): Port1 = 0x01, Port2 = 0x02, Port3 = 0x04, Port4 = 0x08, Reference = 0x10. Прошивка собирает байт как stage << 5 | sourceMask.

На каждую стадию приходит ровно три значения (<FW>/VNA.cpp:407): порт 1, порт 2 и опорный приёмник. Ловушка: у опорного значения маска не 0x10, а Port1 | Port2 | Reference = 0x13 — то есть биты портов у него тоже стоят. Поэтому отбирать сигнальное значение только по биту порта нельзя, надо ещё требовать сброшенный бит 4; именно так и сделано в find_receiver_value.

Ищет значение по этому байту find_receiver_value (protocol.py:174) — фильтрует по стадии, по признаку опорного приёмника и, если задан, по порту. Например: «стадия 0 + ref» — опорный приёмник мастера; «стадия 0 + порт 1, не ref» — сигнал на первом приёмнике.

6. Как из этого получаются S-параметры

Устройство S-параметры не считает — оно отдаёт только сырые комплексные отсчёты приёмников. Считает хост, в librevna_multi_device_driver/cycle_collection.py.

Нормировка (calculate_last_cycle_s_parameters, строка 783): каждый канал делится на опорный приёмник мастера, а не на локальный опорный приёмник слейва — потому что излучает именно мастер. Опорные измерения хранятся по номеру выходного канала (master_reference_measurements_by_output), так как с переключением стимула выходов больше, чем портов мастера.

Имена S-параметров: отражение мастера — master_reflection_s_parameter_name (строка 760), прямые каналы — build_forward_s_parameter_names (строка 765). Нумерация приёмных портов слейвов начинается после всех выходов, чтобы S33/S44 отражения не столкнулись с прямыми каналами.

7. Почему поле мощности читается, но не используется

В парсере стоит:

frequency_hz, _power_level_centidecibels_milliwatt, point_index = struct.unpack_from(
    "<QhH", payload, 0
)

Поле мощности в payload реально существует и стоит между частотой и номером точки, поэтому прочитать его необходимо, чтобы правильно достать point_index. Подчёркивание в имени — обычный питоновский признак «прочитали намеренно, но не используем».

По смыслу оно не нужно: для сбора циклов достаточно частоты, индекса точки и массива отсчётов, а S-параметры считаются как отношение сигнала к опоре. К тому же развёртка идёт с фиксированной мощностью (cdbm_excitation_start == cdbm_excitation_stop, fixedPowerSetting = 1), так что поле лишь дублирует уже известное из настройки.

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

8. Чего в драйвере сознательно нет

  • запроса DeviceInfo и проверки версии протокола устройства;
  • отладочного endpoint 0x82;
  • пакетов калибровки, генератора, анализатора спектра;
  • SetTrigger/ClearTrigger — синхронизация настраивается полем syncMode в SweepSettings, а сама петля триггера аппаратная.

CRC входящих пакетов, в отличие от первой версии драйвера, проверяется — см. раздел 2.

9. Карта модулей хоста

Файл Роль
librevna_driver/protocol/frame.py кадр: кодирование, разбор, CRC, ресинхронизация
librevna_multi_device_driver/protocol.py номера пакетов, сборка payload, парсер datapoint, поиск по дескриптору
librevna_multi_device_driver/transport.py USB bulk, приёмный поток, очередь, ожидание Ack
librevna_multi_device_driver/controller.py высокоуровневые команды: настроить развёртку, собрать циклы, остановить
librevna_multi_device_driver/cycle_collection.py сбор точек в полные циклы, нормировка в S-параметры, имена трасс
librevna_multi_device_driver/models.py SweepConfiguration, выходные каналы, ограничения стадий