Матрица 4x4 за одну развёртку: каждый порт мастера измеряется на каждой частоте дважды, с двумя положениями выходного ключа. Ключ дёргает контроллер в триггерной петле, поэтому расбери переключателями больше не управляет — SwitchedMatrixRadarService уходит из тракта, все 16 комбинаций снимаются в пределах одной точки вместо разброса между тремя развёртками. Выходы нумеруются позиция-мажорно, стадии порт-мажорно (старшие биты счётчика стадий выбирают порт, младший — положение ключа). Из этого следует, что положение ключа = младший бит номера стадии, а период 2 нацело делит 4 стадии — поэтому контроллеру достаточно инвертировать ногу на каждый цикл триггера, без абсолютного счётчика. Выходы 0 и 1 при этом сохраняют прежний смысл. Приёмники слейвов пронумерованы от числа выходов + 1, а не от фиксированного 3: иначе при 4 выходах прямой член «приёмник 3 <- выход 3» назывался бы так же, как отражение выхода 3. При двух выходах даёт прежние порты 3..6. Слейвам порт-стадии выставляются в 7: стоковый приём «одна за последней стадией» перестаёт работать, когда порт выбирается старшими битами. Всё за флагом multi_device.stimulus_switch_positions (1 = прежнее поведение, байт в байт). Спецификация правок FPGA/STM в device_firmware/per_point_switching.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
179 lines
10 KiB
Markdown
179 lines
10 KiB
Markdown
# Переключение стимула внутри частотной точки
|
||
|
||
Режим, в котором одна развёртка даёт матрицу 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 мастера
|
||
|
||
### `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)
|
||
|
||
Врезается в триггерную петлю между `TRIGGER_OUT` мастера и `TRIGGER_IN` слейва.
|
||
Электрически это чисто: в режиме `Mode::Trigger` прошивка LibreVNA уводит STM32
|
||
прибора в Hi-Z на обеих линиях (`GPIO_MODE_INPUT`, `NOPULL`, EXTI снят), так что
|
||
обе линии свободны.
|
||
|
||
Три требования:
|
||
|
||
1. **Прозрачный повторитель обоих фронтов.** Задержка суб-микросекундная
|
||
(bare-metal EXTI), иначе она добавляется к каждой из 4 стадий каждой точки.
|
||
2. **Ключ переключается по СПАДУ**, а не по фронту. Спад означает «оцифровка
|
||
стадии закончилась»; дальше идёт `SamplingDone` → `Settling` следующей стадии,
|
||
то есть окно успокоения. При `dwell_us = 2` и штатной задержке PLL это окно
|
||
~62 мкс — запас огромный. Переключение по фронту заставило бы удерживать
|
||
кольцо на время установления ключа.
|
||
3. **Одна нога на два ключа.** Ключи на порту 1 и на порту 2 ходят синхронно;
|
||
нужное положение — просто инверсия на каждом цикле триггера.
|
||
|
||
Чётность привязывается к началу развёртки. Между развёртками FPGA держится в
|
||
сбросе (`StartSweep` = снять и поднять AUX3), а хост успевает получить пакет
|
||
статуса, поэтому пауза на линии триггера на порядок длиннее межстадийной
|
||
(~100 мкс против ≥1 мс — померить осциллографом и взять порог посередине). Если
|
||
есть физический доступ к AUX3 мастера (нога FPGA P74) — лучше завести её на
|
||
контроллер как точный маркер вместо порога по паузе.
|
||
|
||
Сбой чётности меняет местами антенны 0↔2 и 1↔3 **молча**, поэтому стоит вывести
|
||
текущее положение ключа на отладочную ногу для сверки осциллографом.
|
||
|
||
## Хост
|
||
|
||
Уже сделано. Включается одним полем в `run_config.json`:
|
||
|
||
```json
|
||
"multi_device": { "stimulus_switch_positions": 2 },
|
||
"sweep": { "dwell_us": 2 }
|
||
```
|
||
|
||
При этом расбери **перестаёт трогать ключ**: `SwitchedMatrixRadarService` уходит
|
||
из тракта, матрица 4×4 собирается за одну развёртку. `stimulus_switch_positions:
|
||
1` возвращает всё в прежний режим.
|