Two flops share one clock and are joined by a bare wire, the link of a shift register. The capture flop's clock arrives 50 ps late, and a simulation with a hold check prints HOLD VIOLATION ... d changed 10 ps after the clock edge ..., hold is 30 ps on every cycle.
Hold is the other half of the flop's sampling window. After the edge, D must stay still for t_hold so the flop can finish taking the old value. Here the next value races through so fast that it lands inside that window.
Read each data row as the moment the next value reaches flop B's D pin. It must land to the right of the shaded band.
Launch: flop A's clock rises at 0 ps. Its Q changes 50 ps later and the wire adds 10 ps, so B's D pin moves at 60 ps.
Capture: B's clock rises at 50 ps, so B must hold its old D value until 50 + 30 = 80 ps.
Top data row: the change at 60 ps is 20 ps too early. B may capture the new value, a mix, or go metastable.
Bottom data row: a 40 ps buffer moves the change to 100 ps, 20 ps clear of the window.
Hold compares the earliest arrival of new data with the time the old data is still needed, both measured from the same launch edge:
Term | Value | Why this value |
|---|---|---|
t_cq,min | 50 ps | Fastest clock-to-Q of flop A: hold uses the minimum |
t_logic,min | 10 ps | A direct Q-to-D wire, no gates |
Arrival | 60 ps | 50 + 10 |
t_hold | 30 ps | B's hold requirement |
skew | 50 ps | B's clock arrives 50 ps after A's (positive skew) |
Required | 80 ps | 30 + 50 |
Hold slack | -20 ps | 60 - 80: a violation by 20 ps |
Positive skew is what turns a harmless wire into a failure. With zero skew the same path has 60 - 30 = +30 ps of slack. The late capture clock pushes the end of the window 50 ps to the right while the data arrives at the same time.
The clock period is not in the equation. Hold checks one edge against itself: the edge that launches the next value at A is the same edge whose window B must protect. Doubling the period moves the next edge further away, which helps setup, but the race between A's new value and B's window happens within the first 80 ps after every edge.
The simulation shows this directly. The bench below runs the broken link at a 1000 ps period, then again at 2000 ps:
$ iverilog -g2012 -s tb_hold -o h hold.sv && vvp h
HOLD VIOLATION tb_hold.bad.ff_b: d changed 10 ps after the clock edge at 550 ps, hold is 30 ps
HOLD VIOLATION tb_hold.bad.ff_b: d changed 10 ps after the clock edge at 1550 ps, hold is 30 ps
...
violations: bad=7 buffered=0 deskewed=0; bad q_b unknown in 5 cycles
PASS: hold check fires on the short skewed path and is silent after either fix
$ iverilog -g2012 -s tb_hold -P tb_hold.HALF=2000 -o h4 hold.sv && vvp h4
HOLD VIOLATION tb_hold.bad.ff_b: d changed 10 ps after the clock edge at 2050 ps, hold is 30 ps
HOLD VIOLATION tb_hold.bad.ff_b: d changed 10 ps after the clock edge at 6050 ps, hold is 30 ps
...
violations: bad=7 buffered=0 deskewed=0; bad q_b unknown in 5 cyclesRead the log line by line:
Edge at 550 ps: the first rising edge is at 500 ps; B sees it 50 ps later. A's new Q reaches B at 560 ps, 10 ps after B's edge, inside the 30 ps window.
2000 ps period: the edges move to 2050, 6050 ps and so on, but the gap is still 10 ps. The period changed and the violation did not.
Counts: the broken link reports 7 violations and its output goes unknown in 5 checked cycles. Both fixed links report 0 and shift the pattern correctly.
RTL simulation normally checks none of this: a plain always_ff samples D at the edge in zero time and would shift the pattern correctly. The model below adds the two numbers that matter, a clock-to-Q delay and a hand-written hold check, and turns Q unknown on a violation, as a gate-level model does.
// snippet: the hold check inside dff_hold (full listing under Testbench)
always @(posedge clk) begin
t_edge = $time;
q <= #TCQ d; // new Q appears TCQ after the edge
end
always @(d) // D moved: was it inside the hold window?
if ($time - t_edge < THOLD) begin
violations++;
$display("HOLD VIOLATION %m: d changed %0d ps after the clock edge at %0d ps, hold is %0d ps",
$time - t_edge, t_edge, THOLD);
q <= #TCQ 1'bx; // what a gate-level model does: Q becomes unknown
endFix | Arrival | Required | Hold slack | Cost |
|---|---|---|---|---|
None | 50 + 10 = 60 ps | 30 + 50 = 80 ps | -20 ps | Fails at every clock period |
Add a 40 ps delay buffer on the wire | 50 + 10 + 40 = 100 ps | 80 ps | +20 ps | Area and power; setup slack on this path drops by 40 ps |
Cut skew from 50 to 10 ps | 60 ps | 30 + 10 = 40 ps | +20 ps | A clock-tree change, which moves other paths too |
The buffer costs setup slack on this path only, and a direct flop-to-flop link has almost a full period of setup margin to spare. That is why hold fixing inserts delay cells on short paths after the clock tree is built, when the real skew is known. The hold failure example in the timing section works a second case and lists every fix option; the slack calculator lets you move each term.
An asynchronous reset pin has the same window, applied to the moment reset is released. The flop must see the release either well before an edge or well after it. Release inside the window and the flop may leave reset on this edge, the next, or go metastable, while its neighbours choose differently.
Check | Data-pin twin | Reset release must be | Example numbers |
|---|---|---|---|
Recovery | Setup | At least t_rec before the clock edge | t_rec = 40 ps: release at -60 ps passes, at -20 ps fails by 20 ps |
Removal | Hold | At least t_rem after the clock edge | t_rem = 30 ps: release at +10 ps fails by 20 ps |
The fix is structural rather than a buffer: release the reset from a flop clocked by the same clock, so the release always lands a clock-to-Q after an edge and timing analysis can check it like any data path. That is the async-assert, sync-deassert pattern in the reset template lesson; recovery and removal covers how they are verified.
Arrival 40 + 15 = 55 ps; required 25 + 45 = 70 ps; slack -15 ps. For +10 ps the arrival must reach 80 ps, so add 25 ps of delay. Halving the clock frequency changes nothing.
Required 25 - 5 = 20 ps; slack 55 - 20 = +35 ps. Setup loses the same 50 ps that hold gained, because an earlier capture edge leaves less time for the slow path.
Three copies of the two-flop link run on one clock: the broken one (skew 50 ps, no buffer), one with a 40 ps buffer and one with skew cut to 10 ps. The bench passes when the broken link reports a hold violation, both fixed links report none, and both fixed links shift the pattern like a zero-delay model. Times are in picoseconds.
// Behavioural flop with clock-to-Q and a hand-written hold check (times in ps).
module dff_hold #(parameter int TCQ = 50, THOLD = 30) (
input logic clk, d,
output logic q
);
timeunit 1ps; timeprecision 1ps;
time t_edge = -1000; // time of the last rising edge on this flop's clk pin
int violations = 0;
always @(posedge clk) begin
t_edge = $time;
q <= #TCQ d; // new Q appears TCQ after the edge
end
always @(d) // D moved: was it inside the hold window?
if ($time - t_edge < THOLD) begin
violations++;
$display("HOLD VIOLATION %m: d changed %0d ps after the clock edge at %0d ps, hold is %0d ps",
$time - t_edge, t_edge, THOLD);
q <= #TCQ 1'bx; // what a gate-level model does: Q becomes unknown
end
endmodule
// Two flops on one clock: A launches, B captures. B's clock is late by SKEW;
// the A-to-B wire takes WIRE ps plus BUF ps of inserted delay.
module shift2 #(parameter int SKEW = 50, WIRE = 10, BUF = 0) (
input logic clk, d_in,
output logic q_b
);
timeunit 1ps; timeprecision 1ps;
logic clk_b, q_a, d_b;
assign #SKEW clk_b = clk; // capture clock arrives late
assign #(WIRE + BUF) d_b = q_a; // the short path, plus any hold buffer
dff_hold ff_a (.clk(clk), .d(d_in), .q(q_a));
dff_hold ff_b (.clk(clk_b), .d(d_b), .q(q_b));
endmodule
Tool: Icarus Verilog 13.0, run on 2026-10-17. Commands: iverilog -g2012 -s tb_hold -o h hold.sv && vvp h (1000 ps period) and iverilog -g2012 -s tb_hold -P tb_hold.HALF=2000 -o h4 hold.sv && vvp h4 (2000 ps period), where hold.sv is the Testbench listing on the page.
Result: both runs print 7 HOLD VIOLATION lines for the broken link, each 10 ps after the capture edge (edges at 550, 1550 ... ps and at 2050, 6050 ... ps), violations: bad=7 buffered=0 deskewed=0, the broken output unknown in 5 checked cycles, and PASS. The log on the page is copied from these runs, trimmed with ... lines.
A specify block with $hold(posedge clk, d, 30) was also tried (iverilog -g2012 -gspecify): Icarus prints warning: Timing checks are not supported and ignores it, which is why the flop model writes the check by hand.
Scope and limits: a behavioural model with fixed delays, one corner, no metastability; the X on a violation imitates a gate-level timing-check notifier. Real hold analysis is done by STA over every corner after clock-tree synthesis. The recovery and removal numbers are worked by hand, not simulated.