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,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
@@ -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);