# Переключение стимула внутри частотной точки Режим, в котором одна развёртка даёт матрицу 4×4 вместо 2×4: каждый порт мастера измеряется на **каждой** частоте дважды — с двумя положениями выходного ключа. ```text 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) Реализован: [`switch_controller/`](switch_controller/) — WeAct STM32G431CB, PlatformIO, тот же тулчейн, что у пульта тележки. Подробности подключения и диагностики в [его README](switch_controller/README.md), здесь только суть. Врезается в триггерную петлю между `TRIGGER_OUT` мастера и `TRIGGER_IN` первого слейва. Электрически чисто: в режиме `Mode::Trigger` прошивка LibreVNA уводит STM32 прибора в Hi-Z на обеих линиях (`GPIO_MODE_INPUT`, `NOPULL`, EXTI снят). Логика сводится к трём правилам: 1. **Зеркалирование обоих фронтов.** Задержка не влияет на измерения: мастер стартует оцифровку по своему `TRIGGER_IN`, который приходит с конца кольца, то есть тоже после контроллера. Она общая для всех трёх приборов и в отношении «приёмник/опорник» сокращается — только чуть удлиняет развёртку. 2. **Инверсия ноги по СПАДУ триггера.** Положение ключа — младший бит номера стадии, а период 2 нацело делит 4 стадии, поэтому чередование не рвётся и на границе точки. Спад означает «оцифровка закончилась», дальше идёт окно успокоения следующей стадии (~62 мкс при `dwell_us = 2`), которого ключу хватает с огромным запасом. 3. **Одна нога на оба ключа.** Нужное положение зависит только от номера стадии, а не от того, какой порт излучает, поэтому ключи на порту 1 и порту 2 ходят синхронно. Страховка от сбоя чётности: один потерянный фронт инвертирует чередование навсегда и молча меняет антенны местами. Контроллер распознаёт границу точки по длине паузы (между точками к времени успокоения добавляется перезагрузка регистров PLL, между развёртками — ещё и работа хоста) и принудительно ставит положение A. Так чётность чинится на **каждой** точке. Порог берётся как минимум паузы по скользящему окну с запасом, то есть подстраивается сам под любые настройки развёртки; отдельный провод для маркера начала развёртки не нужен. ## Хост Уже сделано. Включается одним полем в `run_config.json`: ```json "multi_device": { "stimulus_switch_positions": 2 }, "sweep": { "dwell_us": 2 } ``` При этом расбери **перестаёт трогать ключ**: `SwitchedMatrixRadarService` уходит из тракта, матрица 4×4 собирается за одну развёртку. `stimulus_switch_positions: 1` возвращает всё в прежний режим.