Files
radar_system/device_firmware/switch_controller/README.md
T
Ayzen 3f6cfc9108 switch controller: separate pin per switch, gap histogram
PB2 now drives the switch on master port 1 and PB6 the one on port 2, both
updated in a single BSRR write so they move in the same cycle. The CDC report
gained a gap histogram, which showed the point boundary sits at 70-79 us against
62 us between stages, so the resync threshold only fires on the longer
inter-sweep pauses; the misleading "stages per point" counter is renamed to
match what it actually counts.
2026-09-15 14:41:21 +03:00

192 lines
15 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.
# Контроллер выходного ключа
Прошивка STM32, которая переключает выходной СВЧ-ключ внутри частотной точки —
вторая половина режима, описанного в [`../per_point_switching.md`](../per_point_switching.md).
Плата и тулчейн те же, что у пульта тележки: WeAct STM32G431CB, PlatformIO.
## Подключение
```text
мастер TRIGGER_OUT ──► PB0 [STM32] PB1 ──► слейв 1 TRIGGER_IN
PB2 ──► ключ выходного порта 1
PB6 ──► ключ выходного порта 2
PB5 ──► отладочный выход
```
Контроллер встаёт **в разрыв** линии между мастером и первым слейвом, остальная
часть кольца (слейв 1 → слейв 2 → мастер) не трогается. Это безопасно: в режиме
`Mode::Trigger` прошивка LibreVNA уводит собственный STM32 прибора в Hi-Z на обеих
триггерных линиях, так что и вход слейва, и выход мастера свободны.
**У каждого ключа своя нога:** PB2 — ключ выходного порта 1, PB6 — ключ порта 2.
В штатном режиме оба идут в одно и то же положение, потому что положение зависит
только от номера стадии, а не от того, какой порт сейчас излучает. Раздельные
линии нужны по двум причинам: не вешать два входа параллельно на один вывод и
иметь возможность развести ключи независимо.
Оба вывода лежат на порту B, и это существенно: положение меняется **одной
записью в `BSRR`** (младшая половина слова ставит биты, старшая сбрасывает),
поэтому ноги переключаются в одном такте, без взаимного сдвига. Если менять
распиновку, оставляйте оба вывода на одном порту.
Если у конкретного ключа положению A соответствует логическая единица, инверсию
можно включить **отдельно для каждого** — константы `kSwitchPort1ActiveLow` и
`kSwitchPort2ActiveLow`. Если у ключа больше двух состояний, лишние входы
замыкаются джампером.
PB6 выбран как свободный вывод порта B без отладочной функции. **PB3 (SWO) и
PB4 (NJTRST) занимать не стоит** — они мешают отладчику.
> Прошивка рассчитана **ровно на два положения** ключа
> (`stimulus_switch_positions: 2` на хосте). Хост технически допускает и 4
> положения, но тогда положение перестаёт быть простым чередованием одного бита
> и потребуется контроллер на две управляющие линии — эта прошивка для такого
> режима не годится.
Если положению A соответствует лог. 1, а не 0 — поменяйте `kSwitchActiveLow`
в [`src/main.cpp`](src/main.cpp).
## Как это работает
Прибор измеряет каждую частоту в 4 стадии: порт 1 в положениях A и B ключа,
затем порт 2 в тех же положениях. Положение ключа — это младший бит номера
стадии, а период 2 нацело делит 4 стадии, поэтому нужная последовательность —
непрерывное чередование `A B A B A B …`, не прерывающееся на границе точки.
Контроллеру достаточно **инвертировать ногу на каждый цикл триггера**;
абсолютный счётчик стадий не нужен.
Переключение делается по **спаду** триггера. Спад означает «оцифровка стадии
закончилась»; сразу за ним прибор уходит в состояние успокоения следующей
стадии, и это окно (`dwell_us` плюс собственная задержка PLL прибора, ~62 мкс
при `dwell_us = 2`) целиком покрывает установление ключа. Переключение по фронту
заставило бы удерживать кольцо на время установления и удлинило бы каждую стадию.
### Восстановление чётности
Чередование самодостаточно, но один пропущенный или лишний фронт инвертирует
чётность навсегда и **молча** меняет антенны местами. Поэтому контроллер
дополнительно распознаёт границу точки по длине паузы:
| Пауза | Длительность | Из чего состоит |
|---|---|---|
| между стадиями | базовая | только время успокоения |
| между точками | базовая + | перезагрузка регистров PLL мастера |
| между развёртками | базовая + + | ещё и работа хоста между свипами |
Пауза длиннее межстадийной означает, что следующий триггер начнёт стадию 0, то
есть положение A. Контроллер выставляет A принудительно, независимо от
накопленной чётности — то есть чинит рассинхрон на **каждой точке**, а не один
раз на развёртку.
Порог подстраивается сам: межстадийная пауза — это минимальная регулярно
повторяющаяся пауза, поэтому за порог берётся минимум по скользящему окну,
умноженный на `kBoundaryFactorNum / kBoundaryFactorDen` (по умолчанию 1.5). Пока
минимум не набран, восстановление молчит — чередование корректно и без него.
### Измерено на железе 2026-09-11: коэффициент 1.5 мал
Первый прогон с замкнутой петлёй (мастер + 2 слейва, `dwell_us = 2`, IFBW 50 кГц,
201 точка, 4 стадии на точку) дал по CDC:
```text
trigger cycles : 6432
point boundaries : 96 <- ожидалось бы 6432 / 4 = 1608
parity corrections : 0 (ok)
stages per point : 124 (ожидается 4), отклонений: 80
gap min/max, us : 62 / 14364445
inter-stage gap, us : 62
boundary above, us : 94
```
Межстадийная пауза **62 мкс** — точно как считалось (`dwell 2` + задержка PLL 60).
А границы точек распознаются примерно раз в 67 стадий вместо каждых четырёх.
Значит **пауза на границе точки обычно не дотягивает до 1.5×**, то есть до 94 мкс:
перезагрузка регистров PLL и ожидание захвата добавляют к успокоению меньше
половины от него.
**На работу это не влияет**, и `parity corrections : 0` тому подтверждение.
Переключение идёт на каждом спаде безусловно, а стадий на точку чётное число (4),
поэтому чередование `A B A B` само собой возвращается к A на новой точке.
Распознавание границы — только страховка от уже случившегося сбоя чётности.
Но страховка сейчас почти не работает: пропущенный фронт исправится не через
четыре стадии, а через несколько десятков. И диагностика `stages per point`
бесполезна. Чтобы починить, нужно измерить осциллографом фактическое
распределение пауз на PB0 и выбрать коэффициент по нему, а не по догадке. До тех
пор значения `stages per point` и `point boundaries` в отчёте читать нельзя.
### Надо ли перекалибровывать паузу под каждую настройку
Нет, порог подстраивается сам, и вот что на него влияет, а что нет.
**IFBW не влияет.** Он меняет длительность оцифровки, то есть интервал от фронта
до спада, а контроллер меряет паузу от спада до следующего фронта. Это разные
интервалы. Плюс переключение делается по спаду, когда оцифровка уже закончилась,
а детектор границы во время оцифровки вообще неактивен — так что сдвинуть ключ
посреди измерения он не может ни при каком IFBW.
**Число точек, полоса, мощность не влияют** — паузы от них не зависят.
**Влияют `dwell_us` и задержка PLL прибора**: они и есть время успокоения, то
есть сама пауза. Но подстройка на это и рассчитана — окно минимума короткое (16
отсчётов), поэтому новое значение подхватывается за считанные точки. Ручной
калибровки не требуется.
**Смена диапазона внутри развёртки безопасна.** На точках нижнего диапазона
прибор останавливает развёртку и работает с Si5351 по I2C — такие паузы намного
длиннее обычных. Логика от этого не ломается: остановка всегда происходит на
границе точки, перед стадией 0, а значит «длинная пауза → следующая стадия
нулевая → положение A» остаётся верным.
Множитель намеренно щедрый. Ложное срабатывание **посреди** точки испортит
данные, а несрабатывание всего лишь отключит страховку. Если по отчёту видно,
что границы не распознаются вообще (`point boundaries` близко к нулю при
ненулевом `trigger cycles`), множитель можно снижать — но не ниже реального
отношения межточечной паузы к межстадийной, которое видно там же в отчёте.
### Задержка
Зеркалирование занимает ~50 нс и на измерения не влияет: мастер стартует
оцифровку по своему `TRIGGER_IN`, который приходит с конца кольца, то есть тоже
после контроллера. Задержка общая для всех трёх приборов, взаимные сдвиги
мастер/слейвы задаются только межFPGA-передачей, и в отношении
«приёмник/опорник» она сокращается. Единственный эффект — развёртка длиннее на
~2 × задержку × число стадий.
## Диагностика
**PB5** — импульс 2 мкс каждый раз, когда чётность пришлось **чинить**. В
исправной системе эта нога молчит: предсказание чередованием уже даёт нужное
положение на границе точки. Постоянные импульсы означают, что фронты теряются
или порог распознавания границы занижен.
**PB2 и PB6** — положение ключей, смотрятся осциллографом вместе с триггером: на
одной частоте должно быть ровно 4 стадии и два полных периода `A B A B`. Оба
вывода обязаны меняться одновременно и в одну сторону — расхождение между ними
означает ошибку в распиновке или в константах инверсии.
**USB CDC (115200)** — отчёт печатается один раз, когда на линии триггера
тишина дольше 200 мс, то есть когда развёртка остановлена. Во время работы
печати нет: `Serial` блокирует зеркалирование, а пропущенные фронты сбили бы
чётность. Ключевые строки:
```text
parity corrections : 0 (ok) <- в норме всегда 0
stages per point : 4 (ожидается 4), отклонений: 0
inter-stage gap, us : 62
boundary above, us : 93
```
`stages per point` — самопроверка распознавания границ: если оно верное, между
двумя границами всегда ровно 4 стадии. Любое другое число означает, что порог
подобран неправильно.
## Сборка и прошивка
```bash
cd device_firmware/switch_controller
pio run # сборка
pio run -t upload # прошить через ST-Link
pio device monitor # отчёт по USB CDC
```