per-point switching: FPGA and LibreVNA MCU changes

FPGA мастера: порт излучает не в одной стадии, а в блоке подряд идущих —
младшие SUBSTAGE_BITS бит счётчика стадий стали положением выходного ключа,
старшие выбирают порт. Вынесено в функцию port_excites в Sweep.vhd, новое поле
приезжает из свободных битов 8:6 регистра SweepSetup через SPIConfig и top.vhd.

Совместимость со стоком строгая: при SUBSTAGE_BITS=0 сдвиг нулевой, а
добавленная проверка "порт-стадия выше последней" ничего не меняет, потому что
счётчик стадий и так пробегает только 0..STAGES. Ручной режим и анализатор
спектра, где STAGES=0, ведут себя ровно как раньше.

MCU: у SetupSweep добавлен параметр substage_bits со значением по умолчанию 0,
поэтому вызовы из Manual.cpp и SpectrumAnalyzer.cpp не тронуты. VNA.cpp берёт
ширину под-стадии из port3Stage — порта 3 у этого железа нет, поле свободно, и
формат пакета не меняется.

Проверено: GHDL анализирует все три VHDL-файла чисто; новый самопроверяющийся
Test_Sweep.vhd прогоняет четыре сценария (сток, два положения, приёмник без
излучения, четыре положения) и проверен мутацией — с возвращённым стоковым
сравнением сценарии с под-стадиями падают. Обе сборки MCU собираются без ошибок.
Битовые позиции сверены сквозь всю цепочку: хост шлёт port3Stage=1 в слове
0x243, MCU пишет регистр 0x7041, FPGA читает SUBSTAGE_BITS=1.

Слейвы перепрошивать не нужно: им шлётся порт-стадия 7, которая со счётчиком
0..3 не совпадает и в стоковом сравнении.

Исходники теперь опережают лежащие рядом бинарники — предупреждение добавлено в
начало reproduce/README.md, хеш README в SHA256SUMS обновлён. Порядок ввода в
строй расписан в per_point_switching.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ayzen
2026-09-09 22:06:23 +03:00
co-authored by Claude Opus 5
parent 0db3dbe295
commit 7d1879ad79
10 changed files with 504 additions and 51 deletions
@@ -56,6 +56,9 @@ entity SPICommands is
SYNC_MASTER : out STD_LOGIC;
PORT1_STAGE : out STD_LOGIC_VECTOR (2 downto 0);
PORT2_STAGE : out STD_LOGIC_VECTOR (2 downto 0);
-- How many LOW bits of the stage counter select the stimulus switch
-- position instead of the excited port. Zero is the stock layout.
SUBSTAGE_BITS : out STD_LOGIC_VECTOR (2 downto 0);
PORT1_EN : out STD_LOGIC;
PORT2_EN : out STD_LOGIC;
REF_EN : out STD_LOGIC;
@@ -288,6 +291,7 @@ begin
when 5 => ADC_PHASEINC <= spi_buf_out(11 downto 0);
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);
when 7 => SPI_OVERWRITE_ENABLED <= spi_buf_out(15);
@@ -77,7 +77,11 @@ entity Sweep is
STAGES : in STD_LOGIC_VECTOR (2 downto 0);
PORT1_STAGE : in STD_LOGIC_VECTOR (2 downto 0);
PORT2_STAGE : in STD_LOGIC_VECTOR (2 downto 0);
-- Number of LOW bits of the stage counter that select the stimulus
-- switch position rather than the excited port. Zero restores the
-- stock behaviour of one stage per port.
SUBSTAGE_BITS : in STD_LOGIC_VECTOR (2 downto 0);
PORT1_ACTIVE : out STD_LOGIC;
PORT2_ACTIVE : out STD_LOGIC;
@@ -97,6 +101,28 @@ architecture Behavioral of Sweep is
signal stage_cnt : unsigned (2 downto 0);
signal config_reg : std_logic_vector(95 downto 0);
signal source_active : std_logic;
-- A port excites during a BLOCK of consecutive stages instead of a single one:
-- the low SUBSTAGE_BITS bits of the stage counter are the stimulus switch
-- position and the bits above them select the port. With SUBSTAGE_BITS = 0 this
-- reduces to the stock exact comparison, one stage per port.
--
-- A port number beyond the last port stage can never match, which is how the
-- host silences a receive-only device (it sends 7). The stock "one past the last
-- stage" sentinel cannot be used once the port comes from the upper bits: with
-- four stages those bits only ever hold 0 or 1, leaving no free value.
function port_excites(
stage : unsigned(2 downto 0);
port_stage : std_logic_vector(2 downto 0);
substage_width : std_logic_vector(2 downto 0);
last_stage : std_logic_vector(2 downto 0)
) return boolean is
begin
if unsigned(port_stage) > shift_right(unsigned(last_stage), to_integer(unsigned(substage_width))) then
return false;
end if;
return shift_right(stage, to_integer(unsigned(substage_width))) = unsigned(port_stage);
end function;
begin
CONFIG_ADDRESS <= std_logic_vector(point_cnt);
@@ -195,13 +221,13 @@ begin
when Settling =>
NEW_DATA <= '0';
source_active <= '0';
if std_logic_vector(stage_cnt) = PORT1_STAGE then
if port_excites(stage_cnt, PORT1_STAGE, SUBSTAGE_BITS, STAGES) then
PORT1_ACTIVE <= '1';
source_active <= '1';
else
PORT1_ACTIVE <= '0';
end if;
if std_logic_vector(stage_cnt) = PORT2_STAGE then
if port_excites(stage_cnt, PORT2_STAGE, SUBSTAGE_BITS, STAGES) then
PORT2_ACTIVE <= '1';
source_active <= '1';
else
@@ -0,0 +1,278 @@
----------------------------------------------------------------------------------
-- Self-checking testbench for the stage/port gating in Sweep.
--
-- Covers the per-point stimulus switching change (see
-- device_firmware/per_point_switching.md): with SUBSTAGE_BITS > 0 a port must
-- excite across a BLOCK of consecutive stages instead of a single one, while
-- SUBSTAGE_BITS = 0 must behave exactly like the stock design.
--
-- Runs without a trigger loop (SYNC_ENABLED = '0'), so the sweep advances on its
-- own and every stage is observed at the moment sampling starts.
--
-- ghdl -a --std=93 Sweep.vhd Test_Sweep.vhd
-- ghdl -r --std=93 Test_Sweep --assert-level=error
----------------------------------------------------------------------------------
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity Test_Sweep is
end Test_Sweep;
architecture Behavioral of Test_Sweep is
constant CLK_PERIOD : time := 10 ns;
signal clk : std_logic := '0';
signal reset : std_logic := '1';
signal simulation_done : boolean := false;
-- Sweep configuration driven per scenario.
signal npoints : std_logic_vector(12 downto 0) := (others => '0');
signal stages : std_logic_vector(2 downto 0) := "001";
signal port1_stage : std_logic_vector(2 downto 0) := "000";
signal port2_stage : std_logic_vector(2 downto 0) := "001";
signal substage_bits : std_logic_vector(2 downto 0) := "000";
-- DUT interface.
signal config_data : std_logic_vector(95 downto 0);
signal sampling_busy : std_logic := '0';
signal start_sampling : std_logic;
signal reload_pll_regs : std_logic;
signal pll_reload_done : std_logic := '1';
signal port1_active : std_logic;
signal port2_active : std_logic;
signal source_ce : std_logic;
-- Expected (port1, port2) excitation per stage, indexed by stage number.
type excitation is record
port1 : std_logic;
port2 : std_logic;
end record;
type excitation_list is array (natural range <>) of excitation;
-- Observation bookkeeping, shared between the checker and the stimulus.
signal observed_stages : natural := 0;
signal failures : natural := 0;
signal expected_port1 : std_logic_vector(7 downto 0) := (others => '0');
signal expected_port2 : std_logic_vector(7 downto 0) := (others => '0');
signal stages_per_point : positive := 2;
signal scenario_name : string(1 to 24) := (others => ' ');
-- config_reg(95) = 0 keeps the sweep from halting; bits 92..90 = "001" picks the
-- shortest sample count so the simulation stays fast. Everything else is unused
-- by the state machine under test.
function make_config return std_logic_vector is
variable value : std_logic_vector(95 downto 0) := (others => '0');
begin
value(90) := '1';
return value;
end function;
begin
config_data <= make_config;
clk <= not clk after CLK_PERIOD / 2 when not simulation_done else '0';
dut : entity work.Sweep
port map (
CLK => clk,
RESET => reset,
NPOINTS => npoints,
CONFIG_ADDRESS => open,
CONFIG_DATA => config_data,
USER_NSAMPLES => std_logic_vector(to_unsigned(6, 13)),
NSAMPLES => open,
SETTLING_TIME => std_logic_vector(to_unsigned(3, 20)),
SAMPLING_BUSY => sampling_busy,
SAMPLING_DONE => '0',
START_SAMPLING => start_sampling,
BAND_SELECT => open,
MAX2871_DEF_4 => (others => '0'),
MAX2871_DEF_3 => (others => '0'),
MAX2871_DEF_1 => (others => '0'),
MAX2871_DEF_0 => (others => '0'),
SOURCE_REG_4 => open,
SOURCE_REG_3 => open,
SOURCE_REG_1 => open,
SOURCE_REG_0 => open,
LO_REG_4 => open,
LO_REG_3 => open,
LO_REG_1 => open,
LO_REG_0 => open,
RELOAD_PLL_REGS => reload_pll_regs,
PLL_RELOAD_DONE => pll_reload_done,
PLL_LOCKED => '1',
SWEEP_HALTED => open,
SWEEP_RESUME => '0',
SYNC_ENABLED => '0',
SYNC_MASTER => '0',
TRIGGER_IN => '0',
TRIGGER_OUT => open,
NEW_DATA => open,
ATTENUATOR => open,
SOURCE_FILTER => open,
STAGES => stages,
PORT1_STAGE => port1_stage,
PORT2_STAGE => port2_stage,
SUBSTAGE_BITS => substage_bits,
PORT1_ACTIVE => port1_active,
PORT2_ACTIVE => port2_active,
SOURCE_CE => source_ce,
DEBUG_STATUS => open,
RESULT_INDEX => open
);
-- Minimal ADC model: busy for a few cycles once sampling is requested.
sampling_model : process(clk)
variable countdown : natural := 0;
begin
if rising_edge(clk) then
if start_sampling = '1' and countdown = 0 then
countdown := 4;
sampling_busy <= '1';
elsif countdown > 0 then
countdown := countdown - 1;
if countdown = 0 then
sampling_busy <= '0';
end if;
end if;
end if;
end process;
-- Minimal PLL model: the reload takes a few cycles, then reports done.
pll_model : process(clk)
variable countdown : natural := 0;
begin
if rising_edge(clk) then
if reload_pll_regs = '1' then
pll_reload_done <= '0';
countdown := 3;
elsif countdown > 0 then
countdown := countdown - 1;
if countdown = 0 then
pll_reload_done <= '1';
end if;
end if;
end if;
end process;
-- Checks the excitation pattern at the instant each stage starts sampling.
-- The stage number is not read back from the DUT on purpose: counting sampling
-- starts independently is what makes a wrong stage order visible.
checker : process(clk)
variable stage_index : natural;
variable was_sampling : std_logic := '0';
begin
if rising_edge(clk) then
if reset = '1' then
was_sampling := '0';
-- Owned solely by this process: a second driver from the stimulus
-- would make the signal unresolved.
observed_stages <= 0;
else
if start_sampling = '1' and was_sampling = '0' then
stage_index := observed_stages mod stages_per_point;
assert port1_active = expected_port1(stage_index)
report scenario_name & ": stage " & integer'image(stage_index)
& " port1_active=" & std_logic'image(port1_active)
& ", expected " & std_logic'image(expected_port1(stage_index))
severity error;
assert port2_active = expected_port2(stage_index)
report scenario_name & ": stage " & integer'image(stage_index)
& " port2_active=" & std_logic'image(port2_active)
& ", expected " & std_logic'image(expected_port2(stage_index))
severity error;
-- The source must be powered exactly when some port is excited.
assert source_ce = (port1_active or port2_active)
report scenario_name & ": stage " & integer'image(stage_index)
& " source_ce disagrees with the active ports"
severity error;
if port1_active /= expected_port1(stage_index)
or port2_active /= expected_port2(stage_index)
or source_ce /= (port1_active or port2_active) then
failures <= failures + 1;
end if;
observed_stages <= observed_stages + 1;
end if;
was_sampling := start_sampling;
end if;
end if;
end process;
stimulus : process
-- Applies one scenario and waits until the requested number of stages has
-- been observed, so the test fails loudly on a stalled state machine.
procedure run_scenario(
name : string;
cfg_npoints : natural;
cfg_stages : natural;
cfg_port1 : natural;
cfg_port2 : natural;
cfg_substage_bits : natural;
pattern : excitation_list;
stages_to_observe : positive
) is
variable deadline : natural := 0;
begin
reset <= '1';
scenario_name <= (others => ' ');
scenario_name(1 to name'length) <= name;
npoints <= std_logic_vector(to_unsigned(cfg_npoints, 13));
stages <= std_logic_vector(to_unsigned(cfg_stages, 3));
port1_stage <= std_logic_vector(to_unsigned(cfg_port1, 3));
port2_stage <= std_logic_vector(to_unsigned(cfg_port2, 3));
substage_bits <= std_logic_vector(to_unsigned(cfg_substage_bits, 3));
stages_per_point <= pattern'length;
expected_port1 <= (others => '0');
expected_port2 <= (others => '0');
for index in pattern'range loop
expected_port1(index) <= pattern(index).port1;
expected_port2(index) <= pattern(index).port2;
end loop;
-- Held long enough for the checker to zero its own stage counter.
wait for CLK_PERIOD * 4;
reset <= '0';
while observed_stages < stages_to_observe loop
wait for CLK_PERIOD;
deadline := deadline + 1;
assert deadline < 20000
report name & ": sweep stalled, only " & integer'image(observed_stages)
& " stage(s) observed"
severity failure;
end loop;
report name & ": " & integer'image(observed_stages) & " stage(s) checked";
end procedure;
begin
-- Stock layout: one stage per port, must be untouched by the change.
run_scenario(
"stock, no substages", 1, 1, 0, 1, 0,
(0 => ('1', '0'), 1 => ('0', '1')),
4); -- two points, so the stage counter is seen wrapping
-- Per-point switching: port 1 excites in stages 0..1, port 2 in stages 2..3.
run_scenario(
"two switch positions", 0, 3, 0, 1, 1,
(0 => ('1', '0'), 1 => ('1', '0'), 2 => ('0', '1'), 3 => ('0', '1')),
4);
-- Receive-only device: the host parks both ports at 7, nothing may excite.
run_scenario(
"receiver only", 0, 3, 7, 7, 1,
(0 => ('0', '0'), 1 => ('0', '0'), 2 => ('0', '0'), 3 => ('0', '0')),
4);
-- Four switch positions on one port: stages 0..3 all excite port 1.
run_scenario(
"four switch positions", 0, 7, 0, 1, 2,
(0 => ('1', '0'), 1 => ('1', '0'), 2 => ('1', '0'), 3 => ('1', '0'),
4 => ('0', '1'), 5 => ('0', '1'), 6 => ('0', '1'), 7 => ('0', '1')),
8);
assert failures = 0
report integer'image(failures) & " check(s) failed"
severity failure;
report "Test_Sweep: all scenarios passed" severity note;
simulation_done <= true;
wait;
end process;
end Behavioral;
@@ -150,6 +150,7 @@ architecture Behavioral of top is
STAGES : in STD_LOGIC_VECTOR (2 downto 0);
PORT1_STAGE : in STD_LOGIC_VECTOR (2 downto 0);
PORT2_STAGE : in STD_LOGIC_VECTOR (2 downto 0);
SUBSTAGE_BITS : in STD_LOGIC_VECTOR (2 downto 0);
PORT1_ACTIVE : out STD_LOGIC;
PORT2_ACTIVE : out STD_LOGIC;
@@ -262,6 +263,7 @@ architecture Behavioral of top is
SYNC_MASTER : out STD_LOGIC;
PORT1_STAGE : out STD_LOGIC_VECTOR (2 downto 0);
PORT2_STAGE : out STD_LOGIC_VECTOR (2 downto 0);
SUBSTAGE_BITS : out STD_LOGIC_VECTOR (2 downto 0);
PORT1_EN : out STD_LOGIC;
PORT2_EN : out STD_LOGIC;
REF_EN : out STD_LOGIC;
@@ -386,6 +388,7 @@ architecture Behavioral of top is
signal sweep_sync_master : STD_LOGIC;
signal sweep_port1_stage : STD_LOGIC_VECTOR (2 downto 0);
signal sweep_port2_stage : STD_LOGIC_VECTOR (2 downto 0);
signal sweep_substage_bits : STD_LOGIC_VECTOR (2 downto 0);
signal sweep_config_data : std_logic_vector(95 downto 0);
signal sweep_config_address : std_logic_vector(12 downto 0);
signal sweep_source_filter : std_logic_vector(1 downto 0);
@@ -731,6 +734,7 @@ begin
STAGES => sweep_stages,
PORT1_STAGE => sweep_port1_stage,
PORT2_STAGE => sweep_port2_stage,
SUBSTAGE_BITS => sweep_substage_bits,
PORT1_ACTIVE => sweep_excite_port1,
PORT2_ACTIVE => sweep_excite_port2,
@@ -815,6 +819,7 @@ begin
SYNC_MASTER => sweep_sync_master,
PORT1_STAGE => sweep_port1_stage,
PORT2_STAGE => sweep_port2_stage,
SUBSTAGE_BITS => sweep_substage_bits,
SPI_OVERWRITE_ENABLED => HW_overwrite_enabled,
SPI_OVERWRITE_DATA => HW_overwrite_data,
DFT_BIN1_PHASEINC => dft_bin1_phaseinc,
@@ -143,12 +143,14 @@ void FPGA::SetSettlingTime(uint16_t us) {
WriteRegister(Reg::SettlingTimeHigh, value >> 16);
}
void FPGA::SetupSweep(uint8_t stages, uint8_t port1_stage, uint8_t port2_stage, bool synchronize, bool syncMaster) {
void FPGA::SetupSweep(uint8_t stages, uint8_t port1_stage, uint8_t port2_stage, bool synchronize, bool syncMaster, uint8_t substage_bits) {
uint16_t value = 0x0000;
value |= (uint16_t) (stages & 0x07) << 13;
if(synchronize) {
value |= 0x1000;
}
// Bits 11..9 of this register are still unused.
value |= (uint16_t) (substage_bits & 0x07) << 6;
value |= (port1_stage & 0x07) << 3;
value |= (port2_stage & 0x07) << 0;
WriteRegister(Reg::SweepSetup, value);
@@ -110,7 +110,11 @@ void WriteRegister(FPGA::Reg reg, uint16_t value);
void SetNumberOfPoints(uint16_t npoints);
void SetSamplesPerPoint(uint32_t nsamples);
void SetSettlingTime(uint16_t us);
void SetupSweep(uint8_t stages, uint8_t port1_stage, uint8_t port2_stage, bool synchronize = false, bool syncMaster = false);
// substage_bits is how many LOW bits of the stage counter select the stimulus
// switch position instead of the excited port, letting a port excite across a
// block of consecutive stages (per-point stimulus switching). Zero, the default,
// is the stock layout of exactly one stage per port.
void SetupSweep(uint8_t stages, uint8_t port1_stage, uint8_t port2_stage, bool synchronize = false, bool syncMaster = false, uint8_t substage_bits = 0);
void Enable(Periphery p, bool enable = true);
void Disable(Periphery p);
bool IsEnabled(Periphery p);
@@ -338,7 +338,11 @@ bool VNA::Setup(Protocol::SweepSettings s) {
FPGA::Enable(FPGA::Periphery::LO1Chip);
FPGA::Enable(FPGA::Periphery::LO1RF);
#endif
FPGA::SetupSweep(s.stages, s.port1Stage, s.port2Stage, s.syncMode != 0, s.syncMaster);
// port3Stage carries the stimulus switch substage width: this hardware has no
// port 3, so the field is free, and reusing it keeps the packet layout intact.
// A host that does not know about per-point switching leaves it at zero, which
// is the stock one-stage-per-port layout.
FPGA::SetupSweep(s.stages, s.port1Stage, s.port2Stage, s.syncMode != 0, s.syncMaster, s.port3Stage);
Trigger::SetMode((Trigger::Mode) s.syncMode);
FPGA::Enable(FPGA::Periphery::PortSwitch);
pointCnt = 0;