diff --git a/device_firmware/multi_libreVNA_firmware/reproduce/LibreVNA/FPGA/VNA/Sweep.vhd b/device_firmware/multi_libreVNA_firmware/reproduce/LibreVNA/FPGA/VNA/Sweep.vhd index 208950e..9cdf24b 100644 --- a/device_firmware/multi_libreVNA_firmware/reproduce/LibreVNA/FPGA/VNA/Sweep.vhd +++ b/device_firmware/multi_libreVNA_firmware/reproduce/LibreVNA/FPGA/VNA/Sweep.vhd @@ -102,30 +102,45 @@ architecture Behavioral of Sweep is signal config_reg : std_logic_vector(95 downto 0); signal source_active : std_logic; + -- Stage bits that identify the PORT rather than the stimulus switch position. + -- SUBSTAGE_BITS says how many low bits belong to the switch, and those are + -- masked out of the comparison. Decoding to a mask instead of shifting the + -- stage counter at runtime matters: a variable shift synthesises to a barrel + -- shifter, and instantiating one per port cost ~140 LUTs and pushed place & + -- route into a hold violation on the MCU SPI input. Masking is a few gates. + signal port_compare_mask : std_logic_vector(2 downto 0); + + -- Reserved port stage meaning "this port never excites". The host sends it to + -- receive-only devices. (A design using all 8 stages as distinct PORT stages + -- could not address stage 7 - the stock code had the same limitation with its + -- "one past the last stage" sentinel, and no supported topology needs it.) + constant PORT_STAGE_DISABLED : std_logic_vector(2 downto 0) := "111"; + -- 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. + -- position and the bits above them select the port. With SUBSTAGE_BITS = 0 the + -- mask is all ones and this reduces to the stock exact comparison, one stage + -- per port. 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) + compare_mask : 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 + if port_stage = PORT_STAGE_DISABLED then return false; end if; - return shift_right(stage, to_integer(unsigned(substage_width))) = unsigned(port_stage); + return (std_logic_vector(stage) and compare_mask) = (port_stage and compare_mask); end function; begin CONFIG_ADDRESS <= std_logic_vector(point_cnt); + + -- Unrecognised widths fall back to the stock all-ones mask, so a device left + -- unconfigured never degenerates into "every port excites in every stage". + port_compare_mask <= "110" when SUBSTAGE_BITS = "001" else + "100" when SUBSTAGE_BITS = "010" else + "111"; -- assemble registers -- source register 0: N divider and fractional division value @@ -221,13 +236,13 @@ begin when Settling => NEW_DATA <= '0'; source_active <= '0'; - if port_excites(stage_cnt, PORT1_STAGE, SUBSTAGE_BITS, STAGES) then + if port_excites(stage_cnt, PORT1_STAGE, port_compare_mask) then PORT1_ACTIVE <= '1'; source_active <= '1'; else PORT1_ACTIVE <= '0'; end if; - if port_excites(stage_cnt, PORT2_STAGE, SUBSTAGE_BITS, STAGES) then + if port_excites(stage_cnt, PORT2_STAGE, port_compare_mask) then PORT2_ACTIVE <= '1'; source_active <= '1'; else diff --git a/device_firmware/multi_libreVNA_firmware/reproduce/LibreVNA/FPGA/VNA/Test_Sweep.vhd b/device_firmware/multi_libreVNA_firmware/reproduce/LibreVNA/FPGA/VNA/Test_Sweep.vhd index ed890e0..29cac9c 100644 --- a/device_firmware/multi_libreVNA_firmware/reproduce/LibreVNA/FPGA/VNA/Test_Sweep.vhd +++ b/device_firmware/multi_libreVNA_firmware/reproduce/LibreVNA/FPGA/VNA/Test_Sweep.vhd @@ -6,6 +6,9 @@ -- excite across a BLOCK of consecutive stages instead of a single one, while -- SUBSTAGE_BITS = 0 must behave exactly like the stock design. -- +-- Note the port stage is the FIRST stage of the port's block, so with two +-- switch positions port 2 is configured as stage 2, not as port index 1. +-- -- 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. -- @@ -251,7 +254,7 @@ begin -- 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, + "two switch positions", 0, 3, 0, 2, 1, (0 => ('1', '0'), 1 => ('1', '0'), 2 => ('0', '1'), 3 => ('0', '1')), 4); @@ -263,7 +266,7 @@ begin -- Four switch positions on one port: stages 0..3 all excite port 1. run_scenario( - "four switch positions", 0, 7, 0, 1, 2, + "four switch positions", 0, 7, 0, 4, 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); diff --git a/python_app/hardware_full/librevna_multi_device_driver/protocol.py b/python_app/hardware_full/librevna_multi_device_driver/protocol.py index a9012ee..128aa43 100644 --- a/python_app/hardware_full/librevna_multi_device_driver/protocol.py +++ b/python_app/hardware_full/librevna_multi_device_driver/protocol.py @@ -76,6 +76,9 @@ def build_sweep_settings_payload( how many LOW bits of the stage counter are the switch position rather than the port selector. Zero there is the stock layout, so this payload stays compatible with an unmodified device as long as ``switch_substage_count`` is 1. + + Port stages are sent as the FIRST stage of each port's block, which collapses to the + plain port index when there are no substages. """ stimulus_ports = normalize_master_stimulus_ports(master_stimulus_ports) substage_count = normalize_switch_substage_count(switch_substage_count) @@ -97,10 +100,11 @@ def build_sweep_settings_payload( ) if is_synchronization_master: - # The port selector lives in the stage bits ABOVE the substage block, so the - # value sent here is the port's index, not its first stage number. + # A port is matched by the stage bits ABOVE the substage block, so the value + # sent here is the FIRST stage of the port's block. Without substages that is + # just the port index, identical to the stock layout. port_stage_by_port = { - port: index for index, port in enumerate(stimulus_ports) + port: index * substage_count for index, port in enumerate(stimulus_ports) } port_1_stimulus_stage = port_stage_by_port.get(1, DISABLED_PORT_STAGE) port_2_stimulus_stage = port_stage_by_port.get(2, DISABLED_PORT_STAGE) diff --git a/python_app/tests/test_librevna_multidevice.py b/python_app/tests/test_librevna_multidevice.py index 96e7961..4c1944b 100644 --- a/python_app/tests/test_librevna_multidevice.py +++ b/python_app/tests/test_librevna_multidevice.py @@ -465,8 +465,11 @@ class PerPointStimulusSwitchingTest(unittest.TestCase): ) stage_word = struct.unpack_from(" last index 3 - self.assertEqual((stage_word >> 3) & 0x07, 0) # port 1 selected by upper stage bits 0 - self.assertEqual((stage_word >> 6) & 0x07, 1) # port 2 -> 1 + # Port stages are the FIRST stage of each port's block: port 1 owns stages + # 0..1 and port 2 owns 2..3, so the bitstream matches them on the stage bits + # above the substage block. + self.assertEqual((stage_word >> 3) & 0x07, 0) + self.assertEqual((stage_word >> 6) & 0x07, 2) self.assertEqual((stage_word >> 9) & 0x07, 1) # log2(2 positions) = 1 substage bit slave = build_sweep_settings_payload(