Матрица 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>
10 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)
Врезается в триггерную петлю между TRIGGER_OUT мастера и TRIGGER_IN слейва.
Электрически это чисто: в режиме Mode::Trigger прошивка LibreVNA уводит STM32
прибора в Hi-Z на обеих линиях (GPIO_MODE_INPUT, NOPULL, EXTI снят), так что
обе линии свободны.
Три требования:
- Прозрачный повторитель обоих фронтов. Задержка суб-микросекундная (bare-metal EXTI), иначе она добавляется к каждой из 4 стадий каждой точки.
- Ключ переключается по СПАДУ, а не по фронту. Спад означает «оцифровка
стадии закончилась»; дальше идёт
SamplingDone→Settlingследующей стадии, то есть окно успокоения. Приdwell_us = 2и штатной задержке PLL это окно ~62 мкс — запас огромный. Переключение по фронту заставило бы удерживать кольцо на время установления ключа. - Одна нога на два ключа. Ключи на порту 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 возвращает всё в прежний режим.