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.
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
# Бинарный протокол 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/`,
|
||||
дальше он сокращён до `<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`:
|
||||
|
||||
```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<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. Почему поле мощности читается, но не используется
|
||||
|
||||
В парсере стоит:
|
||||
|
||||
```python
|
||||
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`, выходные каналы, ограничения стадий |
|
||||
Reference in New Issue
Block a user