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:
@@ -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)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user