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

261 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Переключение стимула внутри частотной точки
Режим, в котором одна развёртка даёт матрицу 4×4 вместо 2×4: каждый порт мастера
измеряется на **каждой** частоте дважды — с двумя положениями выходного ключа.
```text
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/`](multi_libreVNA_firmware/reproduce/LibreVNA/FPGA/VNA/),
осталось собрать битстрим в ISE.
### `Sweep.vhd`
Порт включался по точному совпадению, поэтому излучал ровно в одной стадии, и во
второй под-стадии зондирующего сигнала просто не было. Сравнение вынесено в
функцию `port_excites`: младшие `SUBSTAGE_BITS` бит счётчика стадий — положение
ключа, старшие — номер порта. Сдвиг на переменное число бит синтезируется в
трёхбитный barrel shifter.
```vhdl
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
свободны**. Заняты три из них:
```vhdl
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`
в момент старта каждой оцифровки.
```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 значения. Протащено только новое поле:
```c
// 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`](multi_libreVNA_firmware/reproduce/README.md).
Отдельно: битстрим мастера единственный в своём роде, `top.bin` перезаписывается
каждой сборкой ISE — переименовывать сразу.
## Контроллер ключа (отдельный STM32)
Реализован: [`switch_controller/`](switch_controller/) — WeAct STM32G431CB,
PlatformIO, тот же тулчейн, что у пульта тележки. Подробности подключения и
диагностики в [его README](switch_controller/README.md), здесь только суть.
Врезается в триггерную петлю между `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`](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`:
```json
"multi_device": { "stimulus_switch_positions": 2 },
"sweep": { "dwell_us": 2 }
```
При этом расбери **перестаёт трогать ключ**: `SwitchedMatrixRadarService` уходит
из тракта, матрица 4×4 собирается за одну развёртку. `stimulus_switch_positions:
1` возвращает всё в прежний режим.