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.
This commit is contained in:
Ayzen
2026-10-02 13:57:33 +03:00
parent 7329750c0c
commit 61ef8720e1
3 changed files with 97 additions and 46 deletions
+59 -32
View File
@@ -18,16 +18,25 @@
## Раскладка стадий ## Раскладка стадий
| Стадия | Порт мастера | Ключ | Выход | | Стадия | Порт мастера | Ключ | Выход | Отражение |
|---|---|---|---| |---|---|---|---|---|
| 0 | 1 | A | 0 | | 0 | 1 | A | 0 | `S11` |
| 1 | 1 | B | 2 | | 1 | 1 | B | 1 | `S22` |
| 2 | 2 | A | 1 | | 2 | 2 | A | 2 | `S33` |
| 3 | 2 | B | 3 | | 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 стадии. Значит через границу точки чередование стадии**, а период 2 нацело делит 4 стадии. Значит через границу точки чередование
@@ -169,22 +178,26 @@ VHDL держите в чистом ASCII: GHDL отвергает не-ASCII с
снял 8 коллекций по 16 трасс: снял 8 коллекций по 16 трасс:
```text ```text
выход 0: -71.5 дБ выходы 0 и 1: расхождение 0.5 дБ порт 1, положение A: -71.5 дБ порт 1, A против B: расхождение 0.9 дБ
выход 1: -72.0 дБ выходы 2 и 3: расхождение 1.3 дБ порт 1, положение B: -70.6 дБ порт 2, A против B: расхождение 0.1 дБ
выход 2: -70.6 дБ порт 2, положение A: -72.0 дБ
выход 3: -71.9 дБ порт 2, положение B: -71.9 дБ
``` ```
(Числа те же, что были сняты; подписаны по портам и положениям, а не по номерам
выходов, чтобы не зависеть от смены нумерации 2026-09-17. Пары для сравнения —
один порт в двух положениях, а не соседние индексы.)
Что именно этим доказано: Что именно этим доказано:
- **Битстрим в приборе доработанный.** Со стоковым сравнением «стадия равна - **Битстрим в приборе доработанный.** Со стоковым сравнением «стадия равна
порт-стадии» излучали бы только стадии 0 и 2, а выходы 1 и 3 провалились бы в порт-стадии» излучали бы только стадии 0 и 2, то есть положение A каждого
шум. Здесь все четыре на одном уровне. порта, а положение B провалилось бы в шум. Здесь все четыре на одном уровне.
- **Петля триггера с контроллером замкнута.** При `syncMode = 3` мастер семплирует - **Петля триггера с контроллером замкнута.** При `syncMode = 3` мастер семплирует
по вернувшемуся триггеру, так что без исправного зеркалирования в STM32 данных по вернувшемуся триггеру, так что без исправного зеркалирования в STM32 данных
не было бы вовсе. не было бы вовсе.
- **Выходы попарно совпадают**, как и должно быть при отключённом ключе: обе его - **Положения одного порта совпадают** (0.9 и 0.1 дБ), как и должно быть при
позиции — физически один путь. отключённом ключе: обе его позиции — физически один путь.
Считать фронты для этой проверки **бессмысленно**: поле `stages` стоковое, сток Считать фронты для этой проверки **бессмысленно**: поле `stages` стоковое, сток
тоже отработает четыре стадии на точку, просто без стимула в 1 и 3. тоже отработает четыре стадии на точку, просто без стимула в 1 и 3.
@@ -202,20 +215,30 @@ VHDL держите в чистом ASCII: GHDL отвергает не-ASCII с
I2C, останавливает развёртку и даёт нерегулярные длинные паузы. Верхняя граница I2C, останавливает развёртку и даёт нерегулярные длинные паузы. Верхняя граница
200 МГц лежит в полосе осциллографа, поэтому стимул видно напрямую. 200 МГц лежит в полосе осциллографа, поэтому стимул видно напрямую.
Медиана `|S21|` по трём повторам, дБ: Подключён был **один** тракт: вход слейва 1 (вход 0) в порт 1 мастера через ключ.
Остальные 15 комбинаций законно лежали на уровне шума −69…−78 дБ.
| вход | выход 0 (A) | выход 1 (B) | выход 2 (A) | выход 3 (B) | Медиана `|S21|` для входа 0, дБ, по портам и положениям ключа:
|---|---|---|---|---|
| 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 |
Подключён был один тракт — вход 0 в выход 0. Разница между двумя положениями | порт мастера | положение A | положение B |
одного и того же порта мастера (выходы 0 и 1) составила **72 дБ**: в положении A |---|---|---|
сигнал идёт в подключённый кабель, в положении B — в открытый порт. Это и есть | порт 1 | −69.8 | **−5.2** |
прямое доказательство, что ключ переключается по стадиям. Повторяемость | порт 2 | −77.1 | −77.6 |
0.0–1.8 дБ, на сигнальном тракте 0.0 дБ.
Разница между двумя положениями **одного и того же** порта 1 — **62–64 дБ**: в
одном положении сигнал идёт в подключённый кабель, в другом в открытый выход. Это
и есть прямое доказательство, что ключ переключается по стадиям. Повторяемость
0.0–1.8 дБ, на сигнальном тракте 0.0 дБ, и положение не переехало за **30 свипов
подряд** (`parity corrections: 0` на 200 тысячах измерений).
> Два предупреждения к этим числам. Первое: измерение сделано **до** смены
> нумерации выходов, поэтому в логах того дня сигнал стоит на «выходе 2» — в
> нынешней нумерации это выход **1**. Здесь таблица приведена по портам и
> положениям, чтобы от нумерации не зависеть. Второе: в первой редакции этого
> раздела я сравнивал выходы 0 и 1 и называл разницу между ними доказательством
> переключения. Это было **неверно** — при старой нумерации соседние индексы были
> разными портами, а не двумя положениями одного. Вывод подтвердился, но на
> правильной паре.
**Ограничение по IFBW.** При `if_bandwidth_hz = 1000` прибор воспроизводимо **Ограничение по IFBW.** При `if_bandwidth_hz = 1000` прибор воспроизводимо
падает: `LIBUSB_ERROR_IO`, затем устройства отваливаются с шины падает: `LIBUSB_ERROR_IO`, затем устройства отваливаются с шины
@@ -381,9 +404,13 @@ GUI сам разложит битстрим и прошивку MCU.
два периода `A B A B`, PB2 и PB6 меняются одновременно и в одну сторону, PB5 два периода `A B A B`, PB2 и PB6 меняются одновременно и в одну сторону, PB5
молчит. молчит.
2. По USB CDC контроллера (развёртку остановить): `parity corrections : 0` и 2. По USB CDC контроллера (развёртку остановить): `parity corrections : 0` и
`stages per point : 4`. `stages per resync` около `число точек x 4`. Строку про «stages per point» из
3. Сверить трейсы: выходы 0 и 1 должны совпасть с тем, что даёт старый режим — старых версий прошивки не искать, её больше нет — см.
это те же порт 1 и порт 2 в положении A. [README контроллера](switch_controller/README.md).
3. Сверить трейсы: **выходы 0 и 2** должны совпасть с тем, что даёт старый режим —
это порт 1 и порт 2 в положении A. Выходы 1 и 3 — те же порты в положении B,
их в старом режиме не было. (До смены нумерации 2026-09-17 стоковыми были
выходы 0 и 1.)
## Хост ## Хост
@@ -73,13 +73,24 @@ def build_output_channels(
) -> tuple[OutputChannel, ...]: ) -> tuple[OutputChannel, ...]:
"""Map every (master port, switch position) pair to an output and a device stage. """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 **Output index equals the device stage**, so outputs are numbered in exactly the
setting keep the output indices they have without per-point switching (with order the device measures them: port 1 through all switch positions, then port 2
``switch_substage_count=1`` this is the identity). Stages are numbered through all of them. That is the order an operator sees on a scope while hanging
port-major, because the master bitstream takes the excited port from the upper antennas, and matching it is worth more than any renumbering cleverness.
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 Both numbers are port-major because the master bitstream takes the excited port
without tracking an absolute stage number. 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 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. collector and the acquisition service, so the mapping cannot drift between them.
@@ -94,12 +105,13 @@ def build_output_channels(
) )
return tuple( return tuple(
OutputChannel( OutputChannel(
index=substage * len(stimulus_ports) + port_index, # Один и тот же номер: выход — это и есть стадия прибора.
index=port_index * substage_count + substage,
stimulus_port=port, stimulus_port=port,
stage_index=port_index * substage_count + substage, stage_index=port_index * substage_count + substage,
) )
for substage in range(substage_count)
for port_index, port in enumerate(stimulus_ports) for port_index, port in enumerate(stimulus_ports)
for substage in range(substage_count)
) )
+17 -5
View File
@@ -399,20 +399,32 @@ class PerPointStimulusSwitchingTest(unittest.TestCase):
def test_two_positions_give_four_outputs(self) -> None: def test_two_positions_give_four_outputs(self) -> None:
channels = build_output_channels((1, 2), 2) channels = build_output_channels((1, 2), 2)
# Outputs are switch-position-major, so outputs 0/1 stay exactly what they are # Port-major in BOTH numbers: the output index is the device stage, so outputs
# without switching; stages are port-major, matching the bitstream taking the # come in the order the device measures them — port 1 through its switch
# port from the stage counter's UPPER bits. # positions, then port 2. Verified against a scope on the switch line.
self.assertEqual( self.assertEqual(
[(c.index, c.stimulus_port, c.stage_index) for c in channels], [(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: 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 # This is what lets the controller just invert its pin on every trigger cycle
# instead of tracking an absolute stage number. # instead of tracking an absolute stage number.
channels = build_output_channels((1, 2), 2) channels = build_output_channels((1, 2), 2)
for channel in channels: 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) self.assertEqual(channel.stage_index & 1, expected_position)
def test_stage_budget_is_enforced(self) -> None: def test_stage_budget_is_enforced(self) -> None: