# Переключение стимула внутри частотной точки Режим, в котором одна развёртка даёт матрицу 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` возвращает всё в прежний режим.