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>
This commit is contained in:
Ayzen
2026-09-09 22:06:23 +03:00
co-authored by Claude Opus 5
parent 0db3dbe295
commit 7d1879ad79
10 changed files with 504 additions and 51 deletions
+121 -43
View File
@@ -56,49 +56,39 @@
## FPGA мастера
Сделано в [`multi_libreVNA_firmware/reproduce/LibreVNA/FPGA/VNA/`](multi_libreVNA_firmware/reproduce/LibreVNA/FPGA/VNA/),
осталось собрать битстрим в ISE.
### `Sweep.vhd`
Сейчас порт включается по точному совпадению, поэтому излучает ровно в одной
стадии, и во второй под-стадии зондирующего сигнала просто не будет:
Порт включался по точному совпадению, поэтому излучал ровно в одной стадии, и во
второй под-стадии зондирующего сигнала просто не было. Сравнение вынесено в
функцию `port_excites`: младшие `SUBSTAGE_BITS` бит счётчика стадий — положение
ключа, старшие — номер порта. Сдвиг на переменное число бит синтезируется в
трёхбитный barrel shifter.
```vhdl
if std_logic_vector(stage_cnt) = PORT1_STAGE then
```
Меняется на сравнение по старшим битам плюс явное «порт выключен». Сдвиг на
переменное число бит синтезируется в трёхбитный barrel shifter:
```vhdl
-- в 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';
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`.
* `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.
Значение выше последней порт-стадии никогда не совпадает — так хост глушит
излучение слейвов, посылая им 7. Старый хост слал «одну за последней», это тоже
глушит.
### `SPIConfig.vhd`
### `SPIConfig.vhd` и `top.vhd`
В регистре `SweepSetup` (адрес 6) заняты биты 15:13, 12, 5:3 и 2:0 — **биты 11:6
свободны**. Берём три из них:
свободны**. Заняты три из них:
```vhdl
when 6 => STAGES <= spi_buf_out(15 downto 13);
@@ -108,27 +98,41 @@ when 6 => STAGES <= spi_buf_out(15 downto 13);
PORT2_STAGE <= spi_buf_out(2 downto 0);
```
### `top.vhd`
В `top.vhd` добавлен сигнал `sweep_substage_bits` и проброшен от `SPIConfig` к
`Sweep`.
Пробросить `SUBSTAGE_BITS` от `SPIConfig` к `Sweep` — новый сигнал рядом с
`sweep_stages` / `sweep_port1_stage`.
### Проверка
Рядом лежит самопроверяющийся тестбенч `Test_Sweep.vhd` на четыре сценария: сток,
два положения ключа, приёмник без излучения, четыре положения. Он гоняет автомат
без триггерной петли (`SYNC_ENABLED = '0'`) и сверяет `PORT1_ACTIVE`/`PORT2_ACTIVE`
в момент старта каждой оцифровки.
```bash
ghdl -a --std=93 Sweep.vhd Test_Sweep.vhd && ghdl -r --std=93 Test_Sweep
```
Тест проверен мутацией: если вернуть стоковое точное сравнение, сценарии с
под-стадиями падают, а стоковый продолжает проходить.
## Прошивка STM32 LibreVNA
Для самих стадий менять нечего: `stageCnt` уже считается до `settings.stages`,
значения тегируются номером стадии, `VNADatapoint<32>` вмещает 4 стадии × 3
значения. Нужно только протащить новое поле:
Сделано, собирается. Для самих стадий менять было нечего: `stageCnt` уже считается
до `settings.stages`, значения тегируются номером стадии, `VNADatapoint<32>`
вмещает 4 стадии × 3 значения. Протащено только новое поле:
```c
// FPGA.cpp
// FPGA.cpp — параметр со значением по умолчанию, чтобы не трогать вызовы из
// ручного режима и анализатора спектра
void FPGA::SetupSweep(uint8_t stages, uint8_t port1_stage, uint8_t port2_stage,
uint8_t substage_bits, bool synchronize, bool syncMaster) {
bool synchronize, bool syncMaster, uint8_t substage_bits) {
...
value |= (uint16_t) (substage_bits & 0x07) << 6;
// VNA.cpp
FPGA::SetupSweep(s.stages, s.port1Stage, s.port2Stage, s.port3Stage,
s.syncMode != 0, s.syncMaster);
// VNA.cpp — ширина под-стадии приезжает в port3Stage: порта 3 у этого железа нет,
// поле свободно, формат пакета не меняется
FPGA::SetupSweep(s.stages, s.port1Stage, s.port2Stage, s.syncMode != 0,
s.syncMaster, s.port3Stage);
```
Собирать так же, как описано в [`reproduce/README.md`](multi_libreVNA_firmware/reproduce/README.md).
@@ -168,6 +172,80 @@ STM32 прибора в Hi-Z на обеих линиях (`GPIO_MODE_INPUT`, `N
паузы по скользящему окну с запасом, то есть подстраивается сам под любые
настройки развёртки; отдельный провод для маркера начала развёртки не нужен.
## Порядок ввода в строй
### Кого перепрошивать
**Только мастер.** Слейвы остаются на текущей прошивке, и вот почему: хост
выставляет им обе порт-стадии в 7, а стоковое сравнение `stage_cnt = PORTn_STAGE`
при счётчике 0..3 с семёркой не совпадает никогда — то есть слейв не излучает и
без доработки. Число стадий он берёт из того же поля `STAGES`, что и раньше, так
что четыре стадии на точку отработает как есть, а тегирование значений номером
стадии в стоковой прошивке уже общее.
Это заметно экономит силы: slave-битстрим пересобирать не надо, а он, напомню,
побайтово невоспроизводим.
### 1. Битстрим FPGA мастера (Xilinx ISE 14.7)
1. Скопировать [`multi_libreVNA_firmware/reproduce/LibreVNA/FPGA`](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 мастера
```bash
cd multi_libreVNA_firmware/reproduce/LibreVNA/Software/VNA_embedded
make BINPATH="<путь к arm-none-eabi из CubeIDE>/tools/bin" -j4
```
Без `EXTERNAL_MASTER_LO_SLAVE` — это и есть сборка мастера. Проверено: с этими
правками собирается без ошибок.
### 3. Упаковка .vnafw
```bash
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`](multi_libreVNA_firmware/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`: