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

10 KiB
Raw Blame History

Переключение стимула внутри частотной точки

Режим, в котором одна развёртка даёт матрицу 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

Сейчас порт включается по точному совпадению, поэтому излучает ровно в одной стадии, и во второй под-стадии зондирующего сигнала просто не будет:

if std_logic_vector(stage_cnt) = PORT1_STAGE then

Меняется на сравнение по старшим битам плюс явное «порт выключен». Сдвиг на переменное число бит синтезируется в трёхбитный barrel shifter:

-- в 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 свободны. Берём три из них:

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 значения. Нужно только протащить новое поле:

// 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. Отдельно: битстрим мастера единственный в своём роде, 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:

"multi_device": { "stimulus_switch_positions": 2 },
"sweep": { "dwell_us": 2 }

При этом расбери перестаёт трогать ключ: SwitchedMatrixRadarService уходит из тракта, матрица 4×4 собирается за одну развёртку. stimulus_switch_positions: 1 возвращает всё в прежний режим.