per-point stimulus switching: host side
Матрица 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>
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
# Переключение стимула внутри частотной точки
|
||||
|
||||
Режим, в котором одна развёртка даёт матрицу 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` возвращает всё в прежний режим.
|
||||
Reference in New Issue
Block a user