From 61ef8720e1864e7f3a6576f426bf99b707ad8be7 Mon Sep 17 00:00:00 2001 From: Ayzen Date: Thu, 17 Sep 2026 18:19:19 +0300 Subject: [PATCH] number stimulus outputs in device stage order The output index now equals the device stage, so outputs follow the order the device measures them: master port 1 through its switch positions, then port 2. The previous position-major numbering permuted the middle two against the physical order, so data captured before this needs its calibration re-captured. --- device_firmware/per_point_switching.md | 91 ++++++++++++------- .../librevna_multi_device_driver/models.py | 30 ++++-- python_app/tests/test_librevna_multidevice.py | 22 ++++- 3 files changed, 97 insertions(+), 46 deletions(-) diff --git a/device_firmware/per_point_switching.md b/device_firmware/per_point_switching.md index 6167471..3db3a76 100644 --- a/device_firmware/per_point_switching.md +++ b/device_firmware/per_point_switching.md @@ -18,16 +18,25 @@ ## Раскладка стадий -| Стадия | Порт мастера | Ключ | Выход | -|---|---|---|---| -| 0 | 1 | A | 0 | -| 1 | 1 | B | 2 | -| 2 | 2 | A | 1 | -| 3 | 2 | B | 3 | +| Стадия | Порт мастера | Ключ | Выход | Отражение | +|---|---|---|---|---| +| 0 | 1 | A | 0 | `S11` | +| 1 | 1 | B | 1 | `S22` | +| 2 | 2 | A | 2 | `S33` | +| 3 | 2 | B | 3 | `S44` | -Стадии нумеруются **порт-мажорно**: старшие биты счётчика стадий выбирают порт, -младший — положение ключа. Выходы нумеруются **позиция-мажорно**, поэтому выходы -0 и 1 сохраняют тот же смысл, что и без переключения. +И стадии, и выходы нумеруются **порт-мажорно**: старшие биты счётчика стадий +выбирают порт, младший — положение ключа. **Номер выхода равен номеру стадии**, +поэтому выходы идут ровно в том порядке, в котором прибор их и меряет — а это тот +же порядок, который видно осциллографом на линии ключа при развешивании антенн. + +> **Смена нумерации 2026-09-17.** До этой даты выходы нумеровались +> позиция-мажорно: выходы 0 и 1 сохраняли смысл из стокового режима, но средние +> два оказывались переставлены относительно физического порядка — слоты 0,1,2,3 +> попадали на выходы 0,2,1,3. Физический порядок подтверждён замером. Данные, +> снятые до смены, **несопоставимы** с новыми: выходы 1 и 2 поменялись смыслом, +> поэтому калибровки и референсы надо переснять. Имена S-параметров те же, но +> `S22` теперь означает «порт 1, положение B», а не «порт 2, положение A». Из этого следует главное свойство: **положение ключа — это младший бит номера стадии**, а период 2 нацело делит 4 стадии. Значит через границу точки чередование @@ -169,22 +178,26 @@ VHDL держите в чистом ASCII: GHDL отвергает не-ASCII с снял 8 коллекций по 16 трасс: ```text -выход 0: -71.5 дБ выходы 0 и 1: расхождение 0.5 дБ -выход 1: -72.0 дБ выходы 2 и 3: расхождение 1.3 дБ -выход 2: -70.6 дБ -выход 3: -71.9 дБ +порт 1, положение A: -71.5 дБ порт 1, A против B: расхождение 0.9 дБ +порт 1, положение B: -70.6 дБ порт 2, A против B: расхождение 0.1 дБ +порт 2, положение A: -72.0 дБ +порт 2, положение B: -71.9 дБ ``` +(Числа те же, что были сняты; подписаны по портам и положениям, а не по номерам +выходов, чтобы не зависеть от смены нумерации 2026-09-17. Пары для сравнения — +один порт в двух положениях, а не соседние индексы.) + Что именно этим доказано: - **Битстрим в приборе доработанный.** Со стоковым сравнением «стадия равна - порт-стадии» излучали бы только стадии 0 и 2, а выходы 1 и 3 провалились бы в - шум. Здесь все четыре на одном уровне. + порт-стадии» излучали бы только стадии 0 и 2, то есть положение A каждого + порта, а положение B провалилось бы в шум. Здесь все четыре на одном уровне. - **Петля триггера с контроллером замкнута.** При `syncMode = 3` мастер семплирует по вернувшемуся триггеру, так что без исправного зеркалирования в STM32 данных не было бы вовсе. -- **Выходы попарно совпадают**, как и должно быть при отключённом ключе: обе его - позиции — физически один путь. +- **Положения одного порта совпадают** (0.9 и 0.1 дБ), как и должно быть при + отключённом ключе: обе его позиции — физически один путь. Считать фронты для этой проверки **бессмысленно**: поле `stages` стоковое, сток тоже отработает четыре стадии на точку, просто без стимула в 1 и 3. @@ -202,20 +215,30 @@ VHDL держите в чистом ASCII: GHDL отвергает не-ASCII с I2C, останавливает развёртку и даёт нерегулярные длинные паузы. Верхняя граница 200 МГц лежит в полосе осциллографа, поэтому стимул видно напрямую. -Медиана `|S21|` по трём повторам, дБ: +Подключён был **один** тракт: вход слейва 1 (вход 0) в порт 1 мастера через ключ. +Остальные 15 комбинаций законно лежали на уровне шума −69…−78 дБ. -| вход | выход 0 (A) | выход 1 (B) | выход 2 (A) | выход 3 (B) | -|---|---|---|---|---| -| 0 | **−5.4** | −77.1 | −69.8 | −78.0 | -| 1 | −69.2 | −68.9 | −69.0 | −69.1 | -| 2 | −77.1 | −77.7 | −76.4 | −77.9 | -| 3 | −73.6 | −73.8 | −72.8 | −73.5 | +Медиана `|S21|` для входа 0, дБ, по портам и положениям ключа: -Подключён был один тракт — вход 0 в выход 0. Разница между двумя положениями -одного и того же порта мастера (выходы 0 и 1) составила **72 дБ**: в положении A -сигнал идёт в подключённый кабель, в положении B — в открытый порт. Это и есть -прямое доказательство, что ключ переключается по стадиям. Повторяемость -0.0–1.8 дБ, на сигнальном тракте 0.0 дБ. +| порт мастера | положение A | положение B | +|---|---|---| +| порт 1 | −69.8 | **−5.2** | +| порт 2 | −77.1 | −77.6 | + +Разница между двумя положениями **одного и того же** порта 1 — **62–64 дБ**: в +одном положении сигнал идёт в подключённый кабель, в другом в открытый выход. Это +и есть прямое доказательство, что ключ переключается по стадиям. Повторяемость +0.0–1.8 дБ, на сигнальном тракте 0.0 дБ, и положение не переехало за **30 свипов +подряд** (`parity corrections: 0` на 200 тысячах измерений). + +> Два предупреждения к этим числам. Первое: измерение сделано **до** смены +> нумерации выходов, поэтому в логах того дня сигнал стоит на «выходе 2» — в +> нынешней нумерации это выход **1**. Здесь таблица приведена по портам и +> положениям, чтобы от нумерации не зависеть. Второе: в первой редакции этого +> раздела я сравнивал выходы 0 и 1 и называл разницу между ними доказательством +> переключения. Это было **неверно** — при старой нумерации соседние индексы были +> разными портами, а не двумя положениями одного. Вывод подтвердился, но на +> правильной паре. **Ограничение по IFBW.** При `if_bandwidth_hz = 1000` прибор воспроизводимо падает: `LIBUSB_ERROR_IO`, затем устройства отваливаются с шины @@ -381,9 +404,13 @@ GUI сам разложит битстрим и прошивку MCU. два периода `A B A B`, PB2 и PB6 меняются одновременно и в одну сторону, PB5 молчит. 2. По USB CDC контроллера (развёртку остановить): `parity corrections : 0` и - `stages per point : 4`. -3. Сверить трейсы: выходы 0 и 1 должны совпасть с тем, что даёт старый режим — - это те же порт 1 и порт 2 в положении A. + `stages per resync` около `число точек x 4`. Строку про «stages per point» из + старых версий прошивки не искать, её больше нет — см. + [README контроллера](switch_controller/README.md). +3. Сверить трейсы: **выходы 0 и 2** должны совпасть с тем, что даёт старый режим — + это порт 1 и порт 2 в положении A. Выходы 1 и 3 — те же порты в положении B, + их в старом режиме не было. (До смены нумерации 2026-09-17 стоковыми были + выходы 0 и 1.) ## Хост diff --git a/python_app/hardware_full/librevna_multi_device_driver/models.py b/python_app/hardware_full/librevna_multi_device_driver/models.py index 00261e5..754a04c 100644 --- a/python_app/hardware_full/librevna_multi_device_driver/models.py +++ b/python_app/hardware_full/librevna_multi_device_driver/models.py @@ -73,13 +73,24 @@ def build_output_channels( ) -> tuple[OutputChannel, ...]: """Map every (master port, switch position) pair to an output and a device stage. - Outputs are numbered switch-position-major, so positions of the FIRST switch - setting keep the output indices they have without per-point switching (with - ``switch_substage_count=1`` this is the identity). Stages are numbered - port-major, because the master bitstream takes the excited port from the upper - bits of the stage counter and the switch position from the lower bits — which - is also what makes the switch line a plain alternation the controller can drive - without tracking an absolute stage number. + **Output index equals the device stage**, so outputs are numbered in exactly the + order the device measures them: port 1 through all switch positions, then port 2 + through all of them. That is the order an operator sees on a scope while hanging + antennas, and matching it is worth more than any renumbering cleverness. + + Both numbers are port-major because the master bitstream takes the excited port + from the UPPER bits of the stage counter and the switch position from the lower + bits. The low bit being the switch position is what lets the trigger-loop + controller just invert its pin every cycle instead of tracking an absolute stage. + + Before 2026-09-17 outputs were numbered switch-position-major, which kept + outputs 0/1 meaning the same thing with and without per-point switching but + permuted the middle two relative to the physical order (slots 0,1,2,3 landed on + outputs 0,2,1,3). Data captured under that layout is NOT comparable with data + captured now: outputs 1 and 2 swapped meaning, so calibration and reference sets + from before the change have to be re-captured. + + With ``switch_substage_count=1`` the layout is the stock one either way. This is the single source of truth shared by the protocol builder, the cycle collector and the acquisition service, so the mapping cannot drift between them. @@ -94,12 +105,13 @@ def build_output_channels( ) return tuple( OutputChannel( - index=substage * len(stimulus_ports) + port_index, + # Один и тот же номер: выход — это и есть стадия прибора. + index=port_index * substage_count + substage, stimulus_port=port, stage_index=port_index * substage_count + substage, ) - for substage in range(substage_count) for port_index, port in enumerate(stimulus_ports) + for substage in range(substage_count) ) diff --git a/python_app/tests/test_librevna_multidevice.py b/python_app/tests/test_librevna_multidevice.py index 4c1944b..f9bbffb 100644 --- a/python_app/tests/test_librevna_multidevice.py +++ b/python_app/tests/test_librevna_multidevice.py @@ -399,20 +399,32 @@ class PerPointStimulusSwitchingTest(unittest.TestCase): def test_two_positions_give_four_outputs(self) -> None: channels = build_output_channels((1, 2), 2) - # Outputs are switch-position-major, so outputs 0/1 stay exactly what they are - # without switching; stages are port-major, matching the bitstream taking the - # port from the stage counter's UPPER bits. + # Port-major in BOTH numbers: the output index is the device stage, so outputs + # come in the order the device measures them — port 1 through its switch + # positions, then port 2. Verified against a scope on the switch line. self.assertEqual( [(c.index, c.stimulus_port, c.stage_index) for c in channels], - [(0, 1, 0), (1, 2, 2), (2, 1, 1), (3, 2, 3)], + [(0, 1, 0), (1, 1, 1), (2, 2, 2), (3, 2, 3)], ) + def test_output_index_is_the_device_stage(self) -> None: + # The whole point of the 2026-09-17 renumbering: an operator hanging antennas + # sees stage order on a scope, and the output numbering must not permute it. + for substage_count in (1, 2, 4): + channels = build_output_channels((1, 2), substage_count) + self.assertEqual( + [c.index for c in channels], + [c.stage_index for c in channels], + msg=f"substages={substage_count}", + ) + self.assertEqual([c.index for c in channels], list(range(len(channels)))) + def test_switch_position_is_the_low_stage_bit(self) -> None: # This is what lets the controller just invert its pin on every trigger cycle # instead of tracking an absolute stage number. channels = build_output_channels((1, 2), 2) for channel in channels: - expected_position = channel.index // 2 # switch-position-major output order + expected_position = channel.index & 1 self.assertEqual(channel.stage_index & 1, expected_position) def test_stage_budget_is_enforced(self) -> None: