Files
radar_system/device_firmware/per_point_switching.md
T
AyzenandClaude Opus 5 0db3dbe295 switch controller firmware + per-point switching spec
Прошивка STM32 (WeAct G431CB, тот же тулчейн, что у пульта тележки), которая
стоит в разрыве триггерной петли между мастером и первым слейвом и переключает
выходной ключ внутри частотной точки.

Положение ключа — младший бит номера стадии, а период 2 нацело делит 4 стадии,
поэтому чередование не рвётся на границе точки и контроллеру достаточно
инвертировать ногу на каждый цикл триггера. Переключение по СПАДУ: спад означает
«оцифровка закончилась», дальше идёт окно успокоения следующей стадии (~62 мкс
при dwell_us=2), которого ключу хватает с запасом, и кольцо удерживать не надо.

Один потерянный фронт инвертировал бы чётность навсегда и молча поменял антенны
местами, поэтому контроллер распознаёт границу точки по длине паузы (между
точками к успокоению добавляется перезагрузка регистров PLL) и принудительно
ставит положение A — чётность чинится на каждой точке, а не раз на развёртку.
Порог берётся как минимум паузы по скользящему окну с запасом, то есть
подстраивается сам; отдельный провод-маркер начала развёртки не нужен.

Задержку зеркалирования проверил по Sweep.vhd: мастер стартует оцифровку по
своему TRIGGER_IN с конца кольца, то есть тоже после контроллера. Задержка
общая для всех приборов и сокращается в отношении «приёмник/опорник» — фазовой
ошибки не вносит, только чуть удлиняет развёртку.

Собирается: RAM 13.9%, Flash 20.0%.

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

11 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)

Реализован: switch_controller/ — WeAct STM32G431CB, PlatformIO, тот же тулчейн, что у пульта тележки. Подробности подключения и диагностики в его README, здесь только суть.

Врезается в триггерную петлю между 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:

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

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