# Бинарный протокол LibreVNA: что шлёт и парсит наш драйвер Разбор USB-протокола LibreVNA и того, какое его подмножество использует `python_app/hardware_full/librevna_multi_device_driver`. Про переключение стимула внутри частотной точки — отдельный документ, [per_point_switching.md](per_point_switching.md). Прошивка в репозитории лежит под `device_firmware/multi_libreVNA_firmware/reproduce/LibreVNA/Software/VNA_embedded/Application/`, дальше он сокращён до ``. Версия протокола, заявленная прошивкой — `Protocol::Version = 14` (`/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. Структура кадра Одна и та же почти для всех типов пакетов (`/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 мкс — это основная часть времени кодирования и передачи точки (`/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 — `/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`: ```c 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](per_point_switching.md). ### `SetIdle` (20) Без payload. Выключает активный режим и останавливает развёртку. Используется как служебная команда: остановить непрерывную развёртку, убраться при ошибке, `close()`. ### `Ack` (7) / `Nack` (10) Ответы устройства на команды хоста. `wait_for_acknowledgement` (`transport.py:96`) читает входящие пакеты, на `Ack` считает команду применённой, на `Nack` бросает исключение, прочие асинхронные пакеты пропускает. ## 5. `VNADatapoint` (27) — поток измерений Шаблон `VNADatapoint` — `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`). ### Байт дескриптора Смещения битов — `/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`. На каждую стадию приходит ровно три значения (`/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. Почему поле мощности читается, но не используется В парсере стоит: ```python frequency_hz, _power_level_centidecibels_milliwatt, point_index = struct.unpack_from( "