The multicycle exception is only safe if the RTL cannot behave otherwise. The pattern has three parts:
Operand registers change only when a new operation is accepted and hold still afterwards.
A small counter or state says when the result register captures: exactly N cycles after the operands changed.
The result register has a clock enable (or the FSM gates it) so it samples only on that cycle.
This RTL justifies a 3-cycle setup with a matching hold. The table shows what the testbench checks.
# a_r, b_r change only on 'start'; y captures on the 3rd cycle after that.
set_multicycle_path 3 -setup -from [get_cells {a_r* b_r*}] -to [get_cells y*]
set_multicycle_path 2 -hold -from [get_cells {a_r* b_r*}] -to [get_cells y*]Property | Checked by |
|---|---|
Operands are stable while busy |
|
Result captured exactly 3 periods after acceptance | E0 accepts; E1/E2 count; E3 captures and asserts done after NBA. Four inclusive observations, three elapsed periods |
Result correct | Compared with |
Mutation check: load operand registers every cycle instead of only on accepted start. The stability checker rejects that change while external inputs change. This is the promise a multicycle constraint would otherwise hide.
Each slot is the cycle that follows a clock edge. Start is high in the first slot, so the edge that ends it is E0; the edges that end the next three slots are E1, E2 and E3. The inputs a and b are changed right after E0 in this simulation, to show that the registered operands inside the multiplier do not follow them. E4 is the earliest edge at which a new start could be accepted. The bench observes at falling edges after NBA; cycles=4 counts E0..E3 inclusively.
Read the picture from start down to y: the control lanes explain when y is allowed to change.
busy rises after E0 and falls after E3, so the operation owns exactly three clock periods.
cnt counts 0, 1, 2 while busy. It reaches 3 after E3 only because a 2-bit counter keeps counting; busy is already low, so it is ignored.
y holds its old value (0 in this run) until the slot after E3, then shows the product. It never changes in between, so the multiplier path has all three periods to settle.
done is high for one slot only, right after the capture.
a and b?The inputs may change at any time. The path being given three cycles starts at registers; if it started at the inputs, the constraint would have to cover the outside world, which the block cannot control. The registers freeze the data for the whole window.
The multiplier plus a finite testbench for operand stability, latency, and results.
`timescale 1ns/1ps
module mc_mult (
input logic clk, rst_n,
input logic start,
input logic [15:0] a, b,
output logic busy, done,
output logic [31:0] y
);
logic [15:0] a_r, b_r; // operand registers: written ONLY when a new operation starts
logic [1:0] cnt;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin busy <= 1'b0; done <= 1'b0; cnt <= '0; end
else begin
done <= 1'b0;
if (!busy) begin
if (start) begin busy <= 1'b1; cnt <= '0; end
end else begin
cnt <= cnt + 1'b1;
if (cnt == 2'd2) begin busy <= 1'b0; done <= 1'b1; end // result is captured on the 3rd cycle
end
end
always_ff @(posedge clk) begin
if (!busy && start) begin a_r <= a; b_r <= b; end // operands change only here
if (busy && cnt == 2'd2) y <= a_r * b_r; // the product path may take 3 clock periods
end
endmodule
Mutation check, run with Icarus Verilog 13.0 (2026-10-16): loading the operand registers every cycle makes the stability checker fail with 2000 errors.
Multiplier bench, run with Icarus Verilog 13.0 (2026-10-16): prints PASS (500 products). The ideal multiply models functionality, not an implementation delay or an STA result.