Files
radar_system/device_firmware/per_point_switching.md
T
AyzenandClaude Opus 5 7d1879ad79 per-point switching: FPGA and LibreVNA MCU changes
FPGA мастера: порт излучает не в одной стадии, а в блоке подряд идущих —
младшие SUBSTAGE_BITS бит счётчика стадий стали положением выходного ключа,
старшие выбирают порт. Вынесено в функцию port_excites в Sweep.vhd, новое поле
приезжает из свободных битов 8:6 регистра SweepSetup через SPIConfig и top.vhd.

Совместимость со стоком строгая: при SUBSTAGE_BITS=0 сдвиг нулевой, а
добавленная проверка "порт-стадия выше последней" ничего не меняет, потому что
счётчик стадий и так пробегает только 0..STAGES. Ручной режим и анализатор
спектра, где STAGES=0, ведут себя ровно как раньше.

MCU: у SetupSweep добавлен параметр substage_bits со значением по умолчанию 0,
поэтому вызовы из Manual.cpp и SpectrumAnalyzer.cpp не тронуты. VNA.cpp берёт
ширину под-стадии из port3Stage — порта 3 у этого железа нет, поле свободно, и
формат пакета не меняется.

Проверено: GHDL анализирует все три VHDL-файла чисто; новый самопроверяющийся
Test_Sweep.vhd прогоняет четыре сценария (сток, два положения, приёмник без
излучения, четыре положения) и проверен мутацией — с возвращённым стоковым
сравнением сценарии с под-стадиями падают. Обе сборки MCU собираются без ошибок.
Битовые позиции сверены сквозь всю цепочку: хост шлёт port3Stage=1 в слове
0x243, MCU пишет регистр 0x7041, FPGA читает SUBSTAGE_BITS=1.

Слейвы перепрошивать не нужно: им шлётся порт-стадия 7, которая со счётчиком
0..3 не совпадает и в стоковом сравнении.

Исходники теперь опережают лежащие рядом бинарники — предупреждение добавлено в
начало reproduce/README.md, хеш README в SHA256SUMS обновлён. Порядок ввода в
строй расписан в per_point_switching.md.

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

17 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 мастера

Сделано в multi_libreVNA_firmware/reproduce/LibreVNA/FPGA/VNA/, осталось собрать битстрим в ISE.

Sweep.vhd

Порт включался по точному совпадению, поэтому излучал ровно в одной стадии, и во второй под-стадии зондирующего сигнала просто не было. Сравнение вынесено в функцию port_excites: младшие SUBSTAGE_BITS бит счётчика стадий — положение ключа, старшие — номер порта. Сдвиг на переменное число бит синтезируется в трёхбитный barrel shifter.

if unsigned(port_stage) > shift_right(unsigned(last_stage), to_integer(unsigned(substage_width))) then
    return false;
end if;
return shift_right(stage, to_integer(unsigned(substage_width))) = unsigned(port_stage);

Совместимость со стоком строгая, а не приблизительная. При SUBSTAGE_BITS = 0 сдвиг нулевой и условие вырождается в stage_cnt = port_stage плюс проверка port_stage <= STAGES. Эта проверка не может ничего изменить: счётчик стадий пробегает только 0..STAGES, поэтому при port_stage > STAGES совпадения не было бы и так. То есть для всех существующих вызовов поведение идентично — включая ручной режим и анализатор спектра, где STAGES = 0.

Значение выше последней порт-стадии никогда не совпадает — так хост глушит излучение слейвов, посылая им 7. Старый хост слал «одну за последней», это тоже глушит.

SPIConfig.vhd и top.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 добавлен сигнал sweep_substage_bits и проброшен от SPIConfig к Sweep.

Проверка

Рядом лежит самопроверяющийся тестбенч Test_Sweep.vhd на четыре сценария: сток, два положения ключа, приёмник без излучения, четыре положения. Он гоняет автомат без триггерной петли (SYNC_ENABLED = '0') и сверяет PORT1_ACTIVE/PORT2_ACTIVE в момент старта каждой оцифровки.

ghdl -a --std=93 Sweep.vhd Test_Sweep.vhd && ghdl -r --std=93 Test_Sweep

Тест проверен мутацией: если вернуть стоковое точное сравнение, сценарии с под-стадиями падают, а стоковый продолжает проходить.

Прошивка 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,
                      bool synchronize, bool syncMaster, uint8_t substage_bits) {
    ...
    value |= (uint16_t) (substage_bits & 0x07) << 6;

// VNA.cpp — ширина под-стадии приезжает в port3Stage: порта 3 у этого железа нет,
// поле свободно, формат пакета не меняется
FPGA::SetupSweep(s.stages, s.port1Stage, s.port2Stage, s.syncMode != 0,
                 s.syncMaster, s.port3Stage);

Собирать так же, как описано в 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. Так чётность чинится на каждой точке. Порог берётся как минимум паузы по скользящему окну с запасом, то есть подстраивается сам под любые настройки развёртки; отдельный провод для маркера начала развёртки не нужен.

Порядок ввода в строй

Кого перепрошивать

Только мастер. Слейвы остаются на текущей прошивке, и вот почему: хост выставляет им обе порт-стадии в 7, а стоковое сравнение stage_cnt = PORTn_STAGE при счётчике 0..3 с семёркой не совпадает никогда — то есть слейв не излучает и без доработки. Число стадий он берёт из того же поля STAGES, что и раньше, так что четыре стадии на точку отработает как есть, а тегирование значений номером стадии в стоковой прошивке уже общее.

Это заметно экономит силы: slave-битстрим пересобирать не надо, а он, напомню, побайтово невоспроизводим.

1. Битстрим FPGA мастера (Xilinx ISE 14.7)

  1. Скопировать multi_libreVNA_firmware/reproduce/LibreVNA/FPGA в общую папку виртуалки (C:\shared\LibreVNA\FPGA).
  2. Открыть VNA.xise — проект мастера. Не VNA_external_lo_slave.xise.
  3. Process → Generate Programming File.
  4. Забрать top.bin сразу и переименовать в top_master.bin: оба проекта пишут в один и тот же файл, и следующая сборка его затрёт. Именно так был потерян исходный мастер-битстрим.

Битстрим невоспроизводим побайтово (place & route недетерминирован плюс timestamp внутри), так что новый файл будет отличаться от старого при любом исходе — это нормально.

2. Прошивка MCU мастера

cd multi_libreVNA_firmware/reproduce/LibreVNA/Software/VNA_embedded
make BINPATH="<путь к arm-none-eabi из CubeIDE>/tools/bin" -j4

Без EXTERNAL_MASTER_LO_SLAVE — это и есть сборка мастера. Проверено: с этими правками собирается без ошибок.

3. Упаковка .vnafw

cd multi_libreVNA_firmware/reproduce/LibreVNA
python3 AssembleFirmware.py \
    --bitstream FPGA/VNA/top_master.bin \
    --mcu       Software/VNA_embedded/build/VNA_embedded.bin \
    --output    ../combined_per_point_switching.vnafw

Имя намеренно новое: не перезаписывайте combined.vnafw, пока не проверите всё на железе — это ваш путь отката. После успешной проверки замените им combined.vnafw, обновите extracted/, build/, SHA256SUMS и уберите предупреждение в начале reproduce/README.md.

Учтите: ./build.sh сверяет результат со старым эталоном и теперь сообщит о расхождении. Это ожидаемо, а не поломка.

4. Заливка

Штатным LibreVNA-GUI: Device → Firmware Update, скормить .vnafw целиком — GUI сам разложит битстрим и прошивку MCU.

5. Включение режима и проверка

Только после того, как мастер поднялся на новой прошивке и обычный режим (stimulus_switch_positions: 1) работает как раньше — включать переключение. Порядок проверки на стенде:

  1. Осциллографом на PB2 и PB5 контроллера: на одной частоте четыре стадии и два периода A B A B, PB5 молчит.
  2. По USB CDC контроллера (развёртку остановить): parity corrections : 0 и stages per point : 4.
  3. Сверить трейсы: выходы 0 и 1 должны совпасть с тем, что даёт старый режим — это те же порт 1 и порт 2 в положении A.

Хост

Уже сделано. Включается одним полем в run_config.json:

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

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