Files
radar_system/device_firmware/per_point_switching.md
T
AyzenandClaude Opus 5 6693ece0aa per-point stimulus switching: host side
Матрица 4x4 за одну развёртку: каждый порт мастера измеряется на каждой частоте
дважды, с двумя положениями выходного ключа. Ключ дёргает контроллер в
триггерной петле, поэтому расбери переключателями больше не управляет —
SwitchedMatrixRadarService уходит из тракта, все 16 комбинаций снимаются в
пределах одной точки вместо разброса между тремя развёртками.

Выходы нумеруются позиция-мажорно, стадии порт-мажорно (старшие биты счётчика
стадий выбирают порт, младший — положение ключа). Из этого следует, что
положение ключа = младший бит номера стадии, а период 2 нацело делит 4 стадии —
поэтому контроллеру достаточно инвертировать ногу на каждый цикл триггера, без
абсолютного счётчика. Выходы 0 и 1 при этом сохраняют прежний смысл.

Приёмники слейвов пронумерованы от числа выходов + 1, а не от фиксированного 3:
иначе при 4 выходах прямой член «приёмник 3 <- выход 3» назывался бы так же, как
отражение выхода 3. При двух выходах даёт прежние порты 3..6.

Слейвам порт-стадии выставляются в 7: стоковый приём «одна за последней стадией»
перестаёт работать, когда порт выбирается старшими битами.

Всё за флагом multi_device.stimulus_switch_positions (1 = прежнее поведение,
байт в байт). Спецификация правок FPGA/STM в device_firmware/per_point_switching.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 21:26:56 +03:00

179 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Переключение стимула внутри частотной точки
Режим, в котором одна развёртка даёт матрицу 4×4 вместо 2×4: каждый порт мастера
измеряется на **каждой** частоте дважды — с двумя положениями выходного ключа.
```
4 выхода = 2 порта мастера × 2 положения ключа
4 входа = 2 слейва × 2 порта (входной ключ не участвует)
```
Выигрыш не в скорости (стадий столько же, сколько было развёрток), а в
когерентности: все 16 комбинаций снимаются в пределах ~0.4 мс на одной частоте
вместо разброса в сотни миллисекунд между развёртками.
## Раскладка стадий
| Стадия | Порт мастера | Ключ | Выход |
|---|---|---|---|
| 0 | 1 | A | 0 |
| 1 | 1 | B | 2 |
| 2 | 2 | A | 1 |
| 3 | 2 | B | 3 |
Стадии нумеруются **порт-мажорно**: старшие биты счётчика стадий выбирают порт,
младший — положение ключа. Выходы нумеруются **позиция-мажорно**, поэтому выходы
0 и 1 сохраняют тот же смысл, что и без переключения.
Из этого следует главное свойство: **положение ключа — это младший бит номера
стадии**, а период 2 нацело делит 4 стадии. Значит через границу точки чередование
продолжается без разрыва, и контроллеру достаточно инвертировать ногу на каждый
цикл триггера — абсолютный счётчик стадий не нужен, нужна только правильная
чётность, и та только один раз на старте развёртки.
## Контракт по проводу
Хост уже отправляет это (см. `build_sweep_settings_payload`), прошивка должна
считать. Слово стадий в `SweepSettings` (16 бит, смещение 25 в payload):
| Биты | Поле | Значение в этом режиме |
|---|---|---|
| 0..2 | `stages` | 3 (то есть 4 стадии) |
| 3..5 | `port1Stage` | 0 — **индекс порта**, а не номер стадии |
| 6..8 | `port2Stage` | 1 |
| 9..11 | `port3Stage` → ширина под-стадии | `log2(положений ключа)` = 1 |
| 12..14 | `port4Stage` | не используется |
| 15 | `unused2` | не используется |
Поле `port3Stage` переиспользовано под ширину блока под-стадий: двухпортовая
прошивка его игнорирует, поэтому формат пакета не меняется. **Ноль там —
стоковая раскладка**, так что хост с `stimulus_switch_positions = 1` остаётся
полностью совместим с немодифицированным прибором.
Слейвам обе порт-стадии выставляются в **7**. Стоковый приём «одна за последней
стадией» здесь не работает: при 4 стадиях старшие биты счётчика принимают только
значения 0 и 1, свободного значения не остаётся, и слейв начал бы излучать.
## FPGA мастера
### `Sweep.vhd`
Сейчас порт включается по точному совпадению, поэтому излучает ровно в одной
стадии, и во второй под-стадии зондирующего сигнала просто не будет:
```vhdl
if std_logic_vector(stage_cnt) = PORT1_STAGE then
```
Меняется на сравнение по старшим битам плюс явное «порт выключен». Сдвиг на
переменное число бит синтезируется в трёхбитный barrel shifter:
```vhdl
-- в entity
SUBSTAGE_BITS : in STD_LOGIC_VECTOR (2 downto 0);
-- в architecture
signal substage_shift : natural range 0 to 7;
substage_shift <= to_integer(unsigned(SUBSTAGE_BITS));
-- в состоянии Settling, вместо каждой из двух проверок:
if unsigned(PORT1_STAGE) <= unsigned(STAGES)
and shift_right(stage_cnt, substage_shift) = unsigned(PORT1_STAGE) then
PORT1_ACTIVE <= '1';
source_active <= '1';
else
PORT1_ACTIVE <= '0';
end if;
```
Проверка совместимости:
* `SUBSTAGE_BITS = 0`, `STAGES = 1`, порты 0 и 1 — сдвиг нулевой, сравнение
вырождается в исходное. Стоковое поведение сохраняется бит в бит.
* Слейв с `port_stage = 7`: `7 <= 1` ложно → порт выключен. Старый хост слал 2 —
`2 <= 1` тоже ложно, так что новый битстрим работает и со старым хостом.
* `SUBSTAGE_BITS = 1`, `STAGES = 3`: порт 0 активен в стадиях 0 и 1, порт 1 — в
2 и 3.
### `SPIConfig.vhd`
В регистре `SweepSetup` (адрес 6) заняты биты 15:13, 12, 5:3 и 2:0 — **биты 11:6
свободны**. Берём три из них:
```vhdl
when 6 => STAGES <= spi_buf_out(15 downto 13);
SYNC_ENABLED <= spi_buf_out(12);
SUBSTAGE_BITS <= spi_buf_out(8 downto 6); -- новое
PORT1_STAGE <= spi_buf_out(5 downto 3);
PORT2_STAGE <= spi_buf_out(2 downto 0);
```
### `top.vhd`
Пробросить `SUBSTAGE_BITS` от `SPIConfig` к `Sweep` — новый сигнал рядом с
`sweep_stages` / `sweep_port1_stage`.
## Прошивка STM32 LibreVNA
Для самих стадий менять нечего: `stageCnt` уже считается до `settings.stages`,
значения тегируются номером стадии, `VNADatapoint<32>` вмещает 4 стадии × 3
значения. Нужно только протащить новое поле:
```c
// FPGA.cpp
void FPGA::SetupSweep(uint8_t stages, uint8_t port1_stage, uint8_t port2_stage,
uint8_t substage_bits, bool synchronize, bool syncMaster) {
...
value |= (uint16_t) (substage_bits & 0x07) << 6;
// VNA.cpp
FPGA::SetupSweep(s.stages, s.port1Stage, s.port2Stage, s.port3Stage,
s.syncMode != 0, s.syncMaster);
```
Собирать так же, как описано в [`reproduce/README.md`](multi_libreVNA_firmware/reproduce/README.md).
Отдельно: битстрим мастера единственный в своём роде, `top.bin` перезаписывается
каждой сборкой ISE — переименовывать сразу.
## Контроллер ключа (отдельный STM32)
Врезается в триггерную петлю между `TRIGGER_OUT` мастера и `TRIGGER_IN` слейва.
Электрически это чисто: в режиме `Mode::Trigger` прошивка LibreVNA уводит STM32
прибора в Hi-Z на обеих линиях (`GPIO_MODE_INPUT`, `NOPULL`, EXTI снят), так что
обе линии свободны.
Три требования:
1. **Прозрачный повторитель обоих фронтов.** Задержка суб-микросекундная
(bare-metal EXTI), иначе она добавляется к каждой из 4 стадий каждой точки.
2. **Ключ переключается по СПАДУ**, а не по фронту. Спад означает «оцифровка
стадии закончилась»; дальше идёт `SamplingDone` → `Settling` следующей стадии,
то есть окно успокоения. При `dwell_us = 2` и штатной задержке PLL это окно
~62 мкс — запас огромный. Переключение по фронту заставило бы удерживать
кольцо на время установления ключа.
3. **Одна нога на два ключа.** Ключи на порту 1 и на порту 2 ходят синхронно;
нужное положение — просто инверсия на каждом цикле триггера.
Чётность привязывается к началу развёртки. Между развёртками FPGA держится в
сбросе (`StartSweep` = снять и поднять AUX3), а хост успевает получить пакет
статуса, поэтому пауза на линии триггера на порядок длиннее межстадийной
(~100 мкс против ≥1 мс — померить осциллографом и взять порог посередине). Если
есть физический доступ к AUX3 мастера (нога FPGA P74) — лучше завести её на
контроллер как точный маркер вместо порога по паузе.
Сбой чётности меняет местами антенны 0↔2 и 1↔3 **молча**, поэтому стоит вывести
текущее положение ключа на отладочную ногу для сверки осциллографом.
## Хост
Уже сделано. Включается одним полем в `run_config.json`:
```json
"multi_device": { "stimulus_switch_positions": 2 },
"sweep": { "dwell_us": 2 }
```
При этом расбери **перестаёт трогать ключ**: `SwitchedMatrixRadarService` уходит
из тракта, матрица 4×4 собирается за одну развёртку. `stimulus_switch_positions:
1` возвращает всё в прежний режим.