Files
radar_system/device_firmware/per_point_switching.md
T
AyzenandClaude Opus 5 0db3dbe295 switch controller firmware + per-point switching spec
Прошивка STM32 (WeAct G431CB, тот же тулчейн, что у пульта тележки), которая
стоит в разрыве триггерной петли между мастером и первым слейвом и переключает
выходной ключ внутри частотной точки.

Положение ключа — младший бит номера стадии, а период 2 нацело делит 4 стадии,
поэтому чередование не рвётся на границе точки и контроллеру достаточно
инвертировать ногу на каждый цикл триггера. Переключение по СПАДУ: спад означает
«оцифровка закончилась», дальше идёт окно успокоения следующей стадии (~62 мкс
при dwell_us=2), которого ключу хватает с запасом, и кольцо удерживать не надо.

Один потерянный фронт инвертировал бы чётность навсегда и молча поменял антенны
местами, поэтому контроллер распознаёт границу точки по длине паузы (между
точками к успокоению добавляется перезагрузка регистров PLL) и принудительно
ставит положение A — чётность чинится на каждой точке, а не раз на развёртку.
Порог берётся как минимум паузы по скользящему окну с запасом, то есть
подстраивается сам; отдельный провод-маркер начала развёртки не нужен.

Задержку зеркалирования проверил по Sweep.vhd: мастер стартует оцифровку по
своему TRIGGER_IN с конца кольца, то есть тоже после контроллера. Задержка
общая для всех приборов и сокращается в отношении «приёмник/опорник» — фазовой
ошибки не вносит, только чуть удлиняет развёртку.

Собирается: RAM 13.9%, Flash 20.0%.

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

183 lines
11 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 мастера
### `Sweep.vhd`
Сейчас порт включается по точному совпадению, поэтому излучает ровно в одной
стадии, и во второй под-стадии зондирующего сигнала просто не будет:
```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';
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
свободны**. Берём три из них:
```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`
Пробросить `SUBSTAGE_BITS` от `SPIConfig` к `Sweep` — новый сигнал рядом с
`sweep_stages` / `sweep_port1_stage`.
## Прошивка 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,
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`](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. Так чётность чинится на **каждой** точке. Порог берётся как минимум
паузы по скользящему окну с запасом, то есть подстраивается сам под любые
настройки развёртки; отдельный провод для маркера начала развёртки не нужен.
## Хост
Уже сделано. Включается одним полем в `run_config.json`:
```json
"multi_device": { "stimulus_switch_positions": 2 },
"sweep": { "dwell_us": 2 }
```
При этом расбери **перестаёт трогать ключ**: `SwitchedMatrixRadarService` уходит
из тракта, матрица 4×4 собирается за одну развёртку. `stimulus_switch_positions:
1` возвращает всё в прежний режим.