A DSP block multiplies 8-bit samples. Its RTL regression is clean, static timing closed with positive slack, and the first silicon gives products whose low byte is right and whose high byte is often wrong. Run the part at a slower clock and the errors stop.
The RTL has not changed since sign-off, and simulation still passes. Where do you look for a bug that only a clock period can see?
They rerun the RTL tests with the failing operands, which pass, and conclude the silicon is marginal. Or they search the multiplier for a logic bug that does not exist. Zero-delay simulation computes a * b in no time, so it cannot fail for lack of time. Static timing passed because it checked the path against the time the constraints allowed it, not the time the design gave it.
Bug | What you see | How it moves |
|---|---|---|
Setup (path too slow) | the bits at the end of a long path are wrong: high bits of a sum or product, the last stage of a mux tree | goes away at a slower clock or higher voltage; usually worse when hot (at low voltage on newer processes, cold can be worse) |
Hold (path too fast) | a register takes the next value one cycle early: a shift register skips a stage, a pipeline passes data through twice | stays at every frequency; changes with voltage, temperature and skew |
Exception the design does not keep | static timing clean, gate-level simulation with timing or silicon fails on a path covered by a multicycle or false path | like setup or hold on exactly the excepted paths |
Unconstrained input | an input from a pin or another block is captured wrongly on some boards, cables or temperatures | follows the board and the environment, not the stimulus |
On the debug loop this is Localise and Hypothesise: the way the failure moves with frequency, voltage and temperature tells you the class before you open the netlist. The bring-up course covers measuring that on a real part.
Move the clock. Does the failure disappear at a lower frequency (setup-type) or stay at every frequency (hold-type)? Which bits fail, and are they the slow ones?
Read the exceptions on the failing path. List every multicycle path, false path and clock-group constraint that covers it, and for each one find the RTL that makes it true: an enable, a hold condition, a synchroniser. No such RTL means the exception is a wish.
Simulate with time. Run gate-level simulation with the delay file on the suspect path, or a behavioural delay model of it, and compare with zero-delay simulation on the same stimulus.
The constraint file says the multiply from the operand registers to the product register is a two-cycle multicycle path, so static timing allows it 20 ns at a 10 ns clock. In the buggy unit nobody changed the control: it takes new operands every cycle and captures the product one cycle after loading, so the path really has 10 ns. The fixed unit keeps the promise: it loads only when idle and captures two cycles later, with in_ready low in between.
mul_path stands in for the gate-level netlist. With TIMED = 0 it is ordinary zero-delay RTL. With TIMED = 1, bit i of the product shows a new value 6 + 0.6 i ns after the operands change, a transport delay like a carry chain's. The bench sends 300 random products into four units, each design zero-delay and timed.
`timescale 1ns/1ps
// mul_path: the combinational 8x8 multiply between the operand registers and the product
// register. TIMED = 0 is zero-delay RTL. TIMED = 1 is a behavioural timing model of a carry
// chain: output bit i shows a new product 6 + 0.6*i ns after the operands change (bit 0 after
// 6 ns, bit 15 after 15 ns). It keeps the last 8 operand changes and, every 0.1 ns, gives each
// bit the value of the newest product that has had time to reach it (a transport delay).
module mul_path #(parameter bit TIMED = 0) (input logic [7:0] a, b, output logic [15:0] p);
if (!TIMED) begin : g_zero
assign p = a * b;
end else begin : g_timed
real t_h [8];
logic [15:0] p_h [8];
initial for (int k = 0; k < 8; k++) begin t_h[k] = -1000.0; p_h[k] = '0; end
always @(a or b) begin // a new product starts on its way
for (int k = 7; k > 0; k--) begin t_h[k] = t_h[k - 1]; p_h[k] = p_h[k - 1]; end
t_h[0] = $realtime; p_h[0] = 16'(a) * 16'(b);
end
initial begin
p = '0;
#0.05; // off the clock edges, so no race
forever begin
for (int i = 0; i < 16; i++)
for (int k = 0; k < 8; k++) // newest first
if (t_h[k] + 6.0 + 0.6 * i <= $realtime) begin p[i] = p_h[k][i]; break; end
#0.1;
end
end
end
endmodule
// mul_unit: operands in, product out. The constraints say the multiply is a two-cycle
// multicycle path, so the multiply may take up to 20 ns at a 10 ns clock.
// FIX = 0: the control was never changed to match. It takes new operands every cycle and
// captures the product one cycle after loading, so the path really has 10 ns.
// FIX = 1: the design matches the constraint: it loads only when idle and captures two cycles
// after loading (in_ready is low in between), so the path has two cycles.
module mul_unit #(parameter bit FIX = 0, parameter bit TIMED = 0) (
input logic clk, rst_n, in_valid,
input logic [7:0] a, b,
output logic in_ready, out_valid,
output logic [15:0] p
);
logic [7:0] a_q, b_q;
logic [15:0] p_path;
logic [1:0] ph; // FIX = 1: 0 idle, 1 and 2 multiplying
logic v1;
mul_path #(.TIMED(TIMED)) u_mul (.a(a_q), .b(b_q), .p(p_path));
assign in_ready = FIX ? (ph == 2'd0) : 1'b1;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
a_q <= '0; b_q <= '0; p <= '0; ph <= '0; v1 <= 1'b0; out_valid <= 1'b0;
end else if (!FIX) begin
if (in_valid) begin a_q <= a; b_q <= b; end
v1 <= in_valid;
out_valid <= v1;
if (v1) p <= p_path; // one cycle after the load
end else begin
out_valid <= 1'b0;
case (ph)
2'd0: if (in_valid) begin a_q <= a; b_q <= b; ph <= 2'd1; end
2'd1: ph <= 2'd2;
default: begin p <= p_path; out_valid <= 1'b1; ph <= 2'd0; end // two cycles after
endcase
end
endmodule
$ iverilog -g2012 -o m.vvp mcp_tb.sv
$ vvp -n m.vvp
zero-delay: bug 0 wrong of 300, fix 0 wrong of 300
timed: bug 298 wrong of 300, fix 0 wrong of 300
PASS: zero-delay RTL passes the bug; only the timed run shows it; the fix passes both
mcp_tb.sv:131: $finish called at 10006000 (1ps)
$ vvp -n m.vvp +BAD
FAIL time=45.0 ns: product #1 is 0020, expected 1b20 (low byte right, high byte wrong)
FATAL: mcp_tb.sv:111: wrong product
Time: 45000 Scope: mcp_tb.g_u[2].$unm_blk_16
$ vvp -n m.vvp +FIX
PASS: timed fixed unit, 300 products, 0 wrong, clock 10.0 ns
mcp_tb.sv:124: $finish called at 10006000 (1ps)The first line is the RTL regression: both units pass, so a logic bug is ruled out. The second is the same stimulus with time in the path: the buggy unit gets 298 of 300 products wrong and the fixed unit none. Product #1 should be 1b20 and came out 0020: the low byte is right and the high byte still holds the previous product's (zero, from reset). Bits 0 to 6 settle in under 10 ns (6 + 0.6 × 6 = 9.6 ns); bits 7 to 15 do not. That is the scene's signature, reproduced. The two products that were right had high bytes equal to the product before.
$ for pp in 12000 14000 15000 16000; do echo "$pp: $(vvp -n m.vvp +BAD +PERIOD_PS=$pp | head -1)"; done
12000: FAIL time=54.0 ns: product #1 is 0320, expected 1b20 (low byte right, high byte wrong)
14000: FAIL time=105.0 ns: product #5 is 383c, expected 783c (low byte right, high byte wrong)
15000: FAIL time=187.5 ns: product #10 is 5b66, expected db66 (low byte right, high byte wrong)
16000: PASS: timed buggy unit, 300 products, 0 wrong, clock 16.0 nsAs the period grows, fewer bits are late and the first failure comes later: at 15 ns only bit 15 is still late (db66 against 5b66 differ in that bit alone). At 16 ns the slowest bit has time and the unit passes. A failure that goes away at a slower clock is setup-type, and the period at which it goes away measures the real path delay, here just over 15 ns.
The logic is correct; the time is not. The constraint promised 20 ns, the control gave 10, and static timing checked the promise rather than the design. The bug is in the agreement between RTL and constraints, which is why neither the RTL regression nor timing sign-off could see it alone. dbglab has no bug of this class.
Exception not kept: make the RTL keep it (load, then capture two cycles later, with a ready signal so the source waits, as the fix does) or remove the exception and meet single-cycle timing, for example by pipelining the multiply. The fix halves throughput; a pipeline keeps it at the cost of latency and flops.
Setup: shorten the path (pipeline, restructure, faster cells) or lower the clock.
Hold: add delay on the short path or reduce the skew; never by slowing the clock, which does not appear in the hold check (a hold violation, worked).
Unconstrained input: constrain it (input delay against the right clock), or synchronise it if it is asynchronous (CDC bugs).
An assertion per exception turns the promise into something the RTL regression can check. For this multicycle path: after the operands change, they stay stable for the next cycle. Added inside mul_unit in a scratch copy of the bench, it fires on the first product of the zero-delay buggy unit, a run that showed no wrong product, and stays silent on the fixed units:
// snippet: the multicycle promise as an assertion, inside mul_unit
a_mcp: assert property (@(posedge clk) disable iff (!rst_n) ($changed(a_q) || $changed(b_q)) |=> $stable(a_q) && $stable(b_q))
else $error("MCP: operands changed in the cycle after a load");$ obj_ma/Vmcp_tb # Verilator 5.048 build of the scratch copy
[45000] %Error: mcp_assert.sv:50: Assertion failed in mcp_tb.g_u[0].dut.a_mcp: MCP: operands changed in the cycle after a load
%Error: mcp_assert.sv:50: Verilog $stop
Aborting...Review every exception with its RTL owner. Each multicycle or false path names the enable or protocol that makes it true; forgotten multicycle hold and overbroad false path are the classic constraint bugs.
Timed simulation on the excepted paths, at gate level with the delay file, at least once before tape-out.
Review question: "What in the RTL stops this path from being used every cycle?"
Hold-type: data reaches the next flop before that flop's hold window closes, usually because of clock skew on a short path, and slowing the clock cannot help because the period is not in the hold check. Check the hold slack and the clock arrival times on that path in the timing report, and whether the path is excluded by an exception. The fix is delay on the short path or less skew.
set_false_path from a configuration register to the datapath because "software only writes it when idle". What must be true in the RTL, and how would you check it?Something in the RTL must make it true, such as the datapath ignoring the register while busy, or software being unable to write it while busy; otherwise a write during traffic is a real path with no timing check. Check it with an assertion (no change of the register while busy), and review the exception against that assertion, as the multicycle assertion above does.
mcp_tb.sv as shown, 2026-10-18: iverilog -g2012 -o m.vvp mcp_tb.sv and vvp -n m.vvp (Icarus Verilog 13.0, 3.7 s): zero-delay, 0 wrong of 300 for both units; timed, bug 298 of 300 wrong, fix 0 of 300; PASS. +BAD fails at product #1 as shown; +FIX passes. +BAD +PERIOD_PS=<n>: fails at 12000, 14000 and 15000 (products #1, #5, #10), passes at 16000.
verilator --binary --timing --assert -Wno-fatal --top-module mcp_tb (Verilator 5.048, 0.45 s) prints the same lines for the default run, +BAD, +FIX and the four periods.
Assertion: a_mcp as shown, added inside mul_unit in a scratch copy (mcp_assert.sv), verilator --binary --timing --assert -Wno-fatal --top-module mcp_tb: fails at 45000 ps in g_u[0], the zero-delay buggy unit; on the fixed units only, silent and PASS. A probe copy printed the two right products of the timed buggy unit, #32 (0550 after 0540) and #114 (0d93 after 0dc8).
Limits: the timing model is behavioural (bit i settles 6 + 0.6 i ns after the operands change), not a synthesised netlist with SDF; no static timing tool is installed, so no timing report is shown and the constraint is described, not run. The hold-violation case is shown in the RTL course, not re-run here.