Ограничение MCU_MOSI OFFSET hold маргинально уже в стоке: запас +0.458 нс. Поэтому любое изменение нетлиста переставляет размещение и может его опрокинуть. A/B в ISE: сток - Timing Score 0, версия на маске при cost table 1 - 5480. Cost table 2 даёт Timing Score 0, причём запас по hold на MCU_MOSI больше стокового: +0.642 против +0.458 нс. Значение зафиксировано в VNA.xise одной строкой, иначе следующая сборка снова словит нарушение. Ресурсы финальной сборки: 2302 LUT против 2189 у стока (40% против 38% ёмкости xc6slx9). Промежуточные 2182, которые я приводил раньше, были из сборки с cost table 1 - её перебор потом переделал, и упаковка map изменилась. Битстрим положен как top_master_per_point_switching.bin, а не поверх top_master.bin: последний - одна из двух копий невосстановимого битстрима, из которого собран текущий combined.vnafw. Документ приведён в согласованное состояние: таблица контракта и описание Sweep.vhd отставали от версии на маске. Собрано и проверено на машине сборки: ISE 14.7 в виртуалке, xtclsh в батч-режиме. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
21 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 — первая стадия блока порта 1 |
| 6..8 | port2Stage |
2 — первая стадия блока порта 2 |
| 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/.
Sweep.vhd
Порт включался по точному совпадению, поэтому излучал ровно в одной стадии, и во
второй под-стадии зондирующего сигнала просто не было. Сравнение вынесено в
функцию port_excites: младшие SUBSTAGE_BITS бит счётчика стадий — положение
ключа, и они маскируются из сравнения, а старшие биты выбирают порт.
if port_stage = PORT_STAGE_DISABLED then -- "111", порт не излучает
return false;
end if;
return (std_logic_vector(stage) and compare_mask) = (port_stage and compare_mask);
Маска декодируется из SUBSTAGE_BITS тремя вентилями, причём нераспознанная
ширина даёт все единицы — чтобы неконфигурированный прибор никогда не выродился
в «каждый порт излучает в каждой стадии».
Совместимость со стоком строгая, а не приблизительная. При SUBSTAGE_BITS = 0
маска все единицы, и выражение становится буквально стоковым
stage_cnt = port_stage. Значит для всех существующих вызовов поведение
идентично — включая ручной режим и анализатор спектра, где 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.
Почему маска, а не сдвиг
Первая версия сравнивала shift_right(stage_cnt, substage_bits) с номером порта.
Функционально верно, но переменный сдвиг синтезируется в barrel shifter, а
вызывался он четырежды (по два на порт): +142 LUT при всего +3 регистрах.
Разводка поехала, и контрольная сборка показала нарушение hold на входе
MCU_MOSI — на шине, по которой MCU пишет регистры FPGA. Сбой там означал бы
неверную конфигурацию развёртки, то есть крайне неприятный в поиске отказ.
Маска обходится несколькими вентилями и такого эффекта не даёт.
Cost table размещателя — обязательно
Ограничение MCU_MOSI OFFSET = IN ... BEFORE MCU_SCK маргинально уже в стоке:
запас по hold всего +0.458 нс. Поэтому любое изменение нетлиста переставляет
размещение и может его опрокинуть. Проверено прямым A/B: сток даёт Timing Score 0,
первая версия правки — 1880, версия на маске при cost table 1 — 5480.
Решается штатно, подбором cost table. В VNA.xise зафиксировано:
<property xil_pn:name="Starting Placer Cost Table (1-100) Map spartan6" xil_pn:value="2" .../>
С этим значением Timing Score 0, а запас по hold на MCU_MOSI даже больше
стокового: +0.642 нс против +0.458. Ресурсы: 2302 LUT против 2189 у стока (40%
против 38% ёмкости xc6slx9).
Не сбрасывайте это свойство в 1. Если после правок в VHDL тайминг снова
поедет — перебирайте cost table дальше (3, 4, 5…) и проверяйте Timing Score в
top.par; сборка каждого варианта около шести минут, синтез при этом кэширован.
Проверка
Рядом лежит самопроверяющийся тестбенч 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
Тест проверен мутацией: если выбросить маску из сравнения, сценарии с под-стадиями падают (8 сработавших проверок), а стоковый продолжает проходить.
VHDL держите в чистом ASCII: GHDL отвергает не-ASCII символы даже в комментариях.
Прошивка 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.
Контроллер ключа (отдельный 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. Так чётность чинится на каждой точке. Порог берётся как минимум паузы по скользящему окну с запасом, то есть подстраивается сам под любые настройки развёртки; отдельный провод для маркера начала развёртки не нужен.
Порядок ввода в строй
Кого перепрошивать
Только мастер. Слейвы остаются на текущей прошивке, и вот почему: хост
выставляет им обе порт-стадии в 7, а стоковое сравнение stage_cnt = PORTn_STAGE
при счётчике 0..3 с семёркой не совпадает никогда — то есть слейв не излучает и
без доработки. Число стадий он берёт из того же поля STAGES, что и раньше, так
что четыре стадии на точку отработает как есть, а тегирование значений номером
стадии в стоковой прошивке уже общее.
Это заметно экономит силы: slave-битстрим пересобирать не надо, а он, напомню, побайтово невоспроизводим.
1. Битстрим FPGA мастера (Xilinx ISE 14.7)
Уже собран. Лежит в FPGA/VNA/top_master_per_point_switching.bin,
sha256 e184eb92bef1c698849511845fa8d754…, Timing Score 0. Пересобирать нужно
только после правок в VHDL.
Имя отдельное намеренно: top_master.bin оставлен нетронутым — это одна из двух
копий невосстановимого битстрима, из которого собран текущий combined.vnafw
(вторая — extracted/master_fpga.bin). Заменить его новым стоит только после
проверки на железе.
Как собрать заново — в GUI:
- Скопировать
multi_libreVNA_firmware/reproduce/LibreVNA/FPGAв общую папку виртуалки (C:\shared\LibreVNA\FPGA). - Открыть
VNA.xise— проект мастера. НеVNA_external_lo_slave.xise. - Process → Generate Programming File.
- Забрать
top.binсразу и переименовать вtop_master_per_point_switching.bin: оба проекта пишут в один и тот же файл, и следующая сборка его затрёт. Именно так был потерян исходный мастер-битстрим. - Проверить
Timing Scoreвtop.par— должен быть 0 (см. про cost table выше).
Или из командной строки в госте, без GUI:
cd ~/shared/LibreVNA/FPGA/VNA # лучше скопировать на локальный диск: vboxsf медленная
source /opt/Xilinx/14.7/ISE_DS/settings64.sh
printf 'project open VNA.xise\nprocess run "Generate Programming File"\nproject close\n' > /tmp/bit.tcl
xtclsh /tmp/bit.tcl
grep "Timing Score" top.par
Битстрим невоспроизводим побайтово (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_per_point_switching.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) работает как раньше — включать переключение.
Порядок проверки на стенде:
- Осциллографом на PB2 и PB5 контроллера: на одной частоте четыре стадии и два
периода
A B A B, PB5 молчит. - По USB CDC контроллера (развёртку остановить):
parity corrections : 0иstages per point : 4. - Сверить трейсы: выходы 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 возвращает всё в прежний режим.