A multicycle setup of N cycles gives the data N periods to arrive. By default the hold check is derived from the setup capture edge, so it moves too: the data must now stay stable until N-1 periods after the launch. For a path that is only slow, not delayed, that requirement is far larger than its real minimum delay. The fix is a separate hold exception.
T, min_delay, hold = 4.0, 1.0, 0.1
hold_default = min_delay - hold # hold checked at the launch edge (no multicycle)
hold_setup_only = min_delay - (T * 1 + hold) # setup 2 without a hold exception: hold edge one period later
hold_fixed = min_delay - hold # setup 2 plus hold 1: hold edge restored to the launch edge
assert round(hold_default, 3) == 0.9
assert round(hold_setup_only, 3) == -3.1
assert round(hold_fixed, 3) == 0.9
print("PASS: hold slack default", round(hold_default, 3), "; multicycle setup only", round(hold_setup_only, 3), "; with the hold exception", round(hold_fixed, 3))# not-run: SDC is not executable here
set_multicycle_path 2 -setup -from [get_clocks clk] -to [get_pins u_slow/*/D]
set_multicycle_path 1 -hold -from [get_clocks clk] -to [get_pins u_slow/*/D]With only the setup exception, hold slack becomes -3.1 ns: the check now wants the data to stay put for a whole extra period. Adding the hold exception with a value of N-1 (here 1) moves the hold edge back to the launch edge and restores the 0.9 ns slack. The unusual signature, a hold violation of about a period on paths that were fine, is how this bug is found. The opposite bug is also dangerous: adding a hold exception with too large a value relaxes the check beyond what the design allows.
Every multicycle setup exception has a matching hold exception, or a written statement of why not.
Hold values are N-1 for same-clock paths unless the design needs another value; the reason is in the exception record.
Hold slack on multicycle paths is compared with the pre-exception value: a change of about a period is the bug.
2 (N - 1): it moves the hold check back by two periods to the launch edge.