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:
Ayzen
2026-09-09 21:26:56 +03:00
co-authored by Claude Opus 5
parent d2338ff10c
commit 6693ece0aa
11 changed files with 645 additions and 64 deletions
+9 -1
View File
@@ -193,6 +193,7 @@ def run_config_from_dict(payload: dict[str, Any]) -> RunConfigModel:
sweep_payload, "if_bandwidth_hz", model.radar.sweep.if_bandwidth_hz
)
model.radar.sweep.power_dbm = _read_float(sweep_payload, "stimulus_power_dbm", model.radar.sweep.power_dbm)
model.radar.sweep.dwell_us = _read_int(sweep_payload, "dwell_us", model.radar.sweep.dwell_us)
slave_serials_payload = multi_device_payload.get(
"slave_serials",
multi_device_payload.get("slave_serial_numbers", model.radar.multi_device.slave_serials),
@@ -229,6 +230,11 @@ def run_config_from_dict(payload: dict[str, Any]) -> RunConfigModel:
"input_switch_positions",
model.radar.multi_device.input_switch_positions,
)
model.radar.multi_device.stimulus_switch_positions = _read_int(
multi_device_payload,
"stimulus_switch_positions",
model.radar.multi_device.stimulus_switch_positions,
)
model.radar.kamil_adc.project_dir = _read_str(
kamil_adc_payload, "project_dir", model.radar.kamil_adc.project_dir
)
@@ -491,6 +497,7 @@ def run_config_to_dict(model: RunConfigModel) -> dict[str, Any]:
"stop_hz": model.radar.sweep.stop_hz,
"if_bandwidth_hz": model.radar.sweep.if_bandwidth_hz,
"stimulus_power_dbm": model.radar.sweep.power_dbm,
"dwell_us": model.radar.sweep.dwell_us,
}
if not model.is_kamil_adc:
sweep_payload["points"] = model.radar.sweep.points
@@ -509,7 +516,8 @@ def run_config_to_dict(model: RunConfigModel) -> dict[str, Any]:
"force_external_reference": model.radar.multi_device.force_external_reference,
"recovery_attempts": model.radar.multi_device.recovery_attempts,
"output_switch_positions": model.radar.multi_device.output_switch_positions,
"input_switch_positions": model.radar.multi_device.input_switch_positions
"input_switch_positions": model.radar.multi_device.input_switch_positions,
"stimulus_switch_positions": model.radar.multi_device.stimulus_switch_positions
},
"kamil_adc": {
"project_dir": model.radar.kamil_adc.project_dir,
+32 -1
View File
@@ -31,6 +31,10 @@ class RadarSweepModel:
points: int = 1
if_bandwidth_hz: float = 1.0
power_dbm: float = -30.0
# Extra per-stage settling requested from the device, on top of its own PLL
# settling delay (60 us by default). With per-point stimulus switching this is the
# window the switch controller has to move the switch between stages.
dwell_us: int = 0
@dataclass(slots=True)
@@ -42,6 +46,10 @@ class RadarMultiDeviceModel:
recovery_attempts: int = 3
output_switch_positions: int = 1 # 1 = свитча нет
input_switch_positions: int = 1 # 1 = свитча нет
# Positions of the stimulus switch stepped through WITHIN one frequency point,
# driven by the controller in the hardware trigger loop rather than by this host.
# 1 = выключено (стоковое поведение). Требует доработанного битстрима мастера.
stimulus_switch_positions: int = 1
@dataclass(slots=True)
@@ -354,6 +362,10 @@ class RunConfigModel:
SN9000_MODEL = "sn9000"
MULTI_DEVICE_INPUT_POSITIONS = 4
MULTI_DEVICE_OUTPUT_POSITIONS = 2
# Stimulus ports the master itself owns. The virtual output axis is this times the
# per-point stimulus switch positions, so it equals MULTI_DEVICE_OUTPUT_POSITIONS
# unless per-point switching is enabled.
MULTI_DEVICE_MASTER_PORTS = 2
@staticmethod
def build_full_combos(input_positions: int, output_positions: int) -> list[ComboModel]:
@@ -406,6 +418,25 @@ class RunConfigModel:
return 1
return max(1, int(self.radar.multi_device.input_switch_positions))
@property
def matrix_stimulus_switch_positions(self) -> int:
"""Positions the stimulus switch steps through inside one frequency point."""
if not self.is_multi_device:
return 1
return max(1, int(self.radar.multi_device.stimulus_switch_positions))
@property
def matrix_virtual_output_positions(self) -> int:
"""Virtual output axis size produced by one acquisition.
Per-point stimulus switching multiplies the master's own ports, because each
port is measured once per switch position at every frequency — so those extra
outputs cost no extra sweep and are NOT driven by this host.
"""
if not self.is_multi_device:
return self.MULTI_DEVICE_OUTPUT_POSITIONS
return self.MULTI_DEVICE_MASTER_PORTS * self.matrix_stimulus_switch_positions
def build_runtime_combos(self) -> list[ComboModel]:
"""Build the combo matrix from the effective switch axis sizes."""
return self.build_full_combos(self.input_switch.positions, self.output_switch.positions)
@@ -477,7 +508,7 @@ class RunConfigModel:
self.output_switch.driver_mode = "mock"
self.output_switch.driver = self.output_switch.driver or "h7992"
self.output_switch.radar_port = 1
self.output_switch.positions = out_physical * self.MULTI_DEVICE_OUTPUT_POSITIONS
self.output_switch.positions = out_physical * self.matrix_virtual_output_positions
self.output_switch.default_position = 0
self.input_switch.name = self.input_switch.name or "virtual_input"