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:
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user