Прошивка 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>
11 KiB
Переключение стимула внутри частотной точки
Режим, в котором одна развёртка даёт матрицу 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 снят).
Логика сводится к трём правилам:
- Зеркалирование обоих фронтов. Задержка не влияет на измерения: мастер
стартует оцифровку по своему
TRIGGER_IN, который приходит с конца кольца, то есть тоже после контроллера. Она общая для всех трёх приборов и в отношении «приёмник/опорник» сокращается — только чуть удлиняет развёртку. - Инверсия ноги по СПАДУ триггера. Положение ключа — младший бит номера
стадии, а период 2 нацело делит 4 стадии, поэтому чередование не рвётся и на
границе точки. Спад означает «оцифровка закончилась», дальше идёт окно
успокоения следующей стадии (~62 мкс при
dwell_us = 2), которого ключу хватает с огромным запасом. - Одна нога на оба ключа. Нужное положение зависит только от номера стадии, а не от того, какой порт излучает, поэтому ключи на порту 1 и порту 2 ходят синхронно.
Страховка от сбоя чётности: один потерянный фронт инвертирует чередование навсегда и молча меняет антенны местами. Контроллер распознаёт границу точки по длине паузы (между точками к времени успокоения добавляется перезагрузка регистров PLL, между развёртками — ещё и работа хоста) и принудительно ставит положение A. Так чётность чинится на каждой точке. Порог берётся как минимум паузы по скользящему окну с запасом, то есть подстраивается сам под любые настройки развёртки; отдельный провод для маркера начала развёртки не нужен.
Хост
Уже сделано. Включается одним полем в run_config.json:
"multi_device": { "stimulus_switch_positions": 2 },
"sweep": { "dwell_us": 2 }
При этом расбери перестаёт трогать ключ: SwitchedMatrixRadarService уходит
из тракта, матрица 4×4 собирается за одну развёртку. stimulus_switch_positions: 1 возвращает всё в прежний режим.