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
@@ -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)
@@ -465,8 +465,11 @@ class PerPointStimulusSwitchingTest(unittest.TestCase):
)
stage_word = struct.unpack_from("<H", master, 25)[0]
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
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(