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