fpga: replace variable shift with a compare mask in port gating

Контрольная сборка стока в ISE показала, что первая версия правки внесла
нарушение тайминга: сток даёт Timing Score 0, а она — 1880 (hold на входе
MCU_MOSI). Причина — shift_right с переменным сдвигом синтезируется в barrel
shifter, а вызывался он четырежды: +142 LUT при +3 регистрах, из-за чего
поехало размещение и пострадала SPI-шина, по которой MCU пишет регистры FPGA.
Сбой там означал бы неверную конфигурацию развёртки.

Теперь младшие биты счётчика стадий просто маскируются, а маска декодируется
из SUBSTAGE_BITS тремя вентилями. Итог: 2182 LUT против 2189 у стока, то есть
логика стала дешевле стоковой. При SUBSTAGE_BITS=0 маска все единицы и
выражение вырождается в стоковое stage_cnt = port_stage буквально.

Заодно контракт стал однозначнее: хост присылает ПЕРВУЮ стадию блока порта, а
не индекс порта. Без под-стадий это то же число, поэтому стоковая совместимость
сохраняется, а зарезервированное "7 = порт выключен" больше не может совпасть с
настоящей стадией (при четырёх под-стадиях маскированная семёрка совпадала с
блоком второго порта).

Из VHDL убраны не-ASCII символы: GHDL отвергает их даже в комментариях.

Проверено: тестбенч проходит все четыре сценария и падает под мутацией
(8 сработавших проверок), 305 python-тестов зелёные, ISE собирает битстрим.
Остаточное нарушение hold на MCU_MOSI ловится подбором cost table размещателя —
ограничение маргинально уже в стоке (запас +0.458 нс).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ayzen
2026-09-10 14:02:30 +03:00
co-authored by Claude Opus 5
parent dfb285ab93
commit 59c9d4f255
4 changed files with 45 additions and 20 deletions
@@ -102,31 +102,46 @@ architecture Behavioral of Sweep is
signal config_reg : std_logic_vector(95 downto 0); signal config_reg : std_logic_vector(95 downto 0);
signal source_active : std_logic; 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: -- 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 -- 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 -- position and the bits above them select the port. With SUBSTAGE_BITS = 0 the
-- reduces to the stock exact comparison, one stage per port. -- mask is all ones and 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( function port_excites(
stage : unsigned(2 downto 0); stage : unsigned(2 downto 0);
port_stage : std_logic_vector(2 downto 0); port_stage : std_logic_vector(2 downto 0);
substage_width : std_logic_vector(2 downto 0); compare_mask : std_logic_vector(2 downto 0)
last_stage : std_logic_vector(2 downto 0)
) return boolean is ) return boolean is
begin 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; return false;
end if; 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; end function;
begin begin
CONFIG_ADDRESS <= std_logic_vector(point_cnt); 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 -- assemble registers
-- source register 0: N divider and fractional division value -- source register 0: N divider and fractional division value
SOURCE_REG_0 <= MAX2871_DEF_0(31) & "000000000" & config_reg(93) & config_reg(5 downto 0) & config_reg(26 downto 15) & "000"; SOURCE_REG_0 <= MAX2871_DEF_0(31) & "000000000" & config_reg(93) & config_reg(5 downto 0) & config_reg(26 downto 15) & "000";
@@ -221,13 +236,13 @@ begin
when Settling => when Settling =>
NEW_DATA <= '0'; NEW_DATA <= '0';
source_active <= '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'; PORT1_ACTIVE <= '1';
source_active <= '1'; source_active <= '1';
else else
PORT1_ACTIVE <= '0'; PORT1_ACTIVE <= '0';
end if; 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'; PORT2_ACTIVE <= '1';
source_active <= '1'; source_active <= '1';
else else
@@ -6,6 +6,9 @@
-- excite across a BLOCK of consecutive stages instead of a single one, while -- excite across a BLOCK of consecutive stages instead of a single one, while
-- SUBSTAGE_BITS = 0 must behave exactly like the stock design. -- 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 -- 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. -- 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. -- Per-point switching: port 1 excites in stages 0..1, port 2 in stages 2..3.
run_scenario( 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')), (0 => ('1', '0'), 1 => ('1', '0'), 2 => ('0', '1'), 3 => ('0', '1')),
4); 4);
@@ -263,7 +266,7 @@ begin
-- Four switch positions on one port: stages 0..3 all excite port 1. -- Four switch positions on one port: stages 0..3 all excite port 1.
run_scenario( 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'), (0 => ('1', '0'), 1 => ('1', '0'), 2 => ('1', '0'), 3 => ('1', '0'),
4 => ('0', '1'), 5 => ('0', '1'), 6 => ('0', '1'), 7 => ('0', '1')), 4 => ('0', '1'), 5 => ('0', '1'), 6 => ('0', '1'), 7 => ('0', '1')),
8); 8);
@@ -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 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 selector. Zero there is the stock layout, so this payload stays compatible with an
unmodified device as long as ``switch_substage_count`` is 1. 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) stimulus_ports = normalize_master_stimulus_ports(master_stimulus_ports)
substage_count = normalize_switch_substage_count(switch_substage_count) substage_count = normalize_switch_substage_count(switch_substage_count)
@@ -97,10 +100,11 @@ def build_sweep_settings_payload(
) )
if is_synchronization_master: if is_synchronization_master:
# The port selector lives in the stage bits ABOVE the substage block, so the # A port is matched by the stage bits ABOVE the substage block, so the value
# value sent here is the port's index, not its first stage number. # 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_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_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) port_2_stimulus_stage = port_stage_by_port.get(2, DISABLED_PORT_STAGE)
@@ -465,8 +465,11 @@ class PerPointStimulusSwitchingTest(unittest.TestCase):
) )
stage_word = struct.unpack_from("<H", master, 25)[0] stage_word = struct.unpack_from("<H", master, 25)[0]
self.assertEqual(stage_word & 0x07, 3) # 4 stages -> last index 3 self.assertEqual(stage_word & 0x07, 3) # 4 stages -> last index 3
self.assertEqual((stage_word >> 3) & 0x07, 0) # port 1 selected by upper stage bits 0 # Port stages are the FIRST stage of each port's block: port 1 owns stages
self.assertEqual((stage_word >> 6) & 0x07, 1) # port 2 -> 1 # 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 self.assertEqual((stage_word >> 9) & 0x07, 1) # log2(2 positions) = 1 substage bit
slave = build_sweep_settings_payload( slave = build_sweep_settings_payload(