Skip to main content

Timing Controls

Verilog has three ways to make a procedural statement wait: #delay, @(event), and wait. All three are essential for writing a simulation model or testbench that behaves realistically over time — and every one of them is either partially or entirely meaningless to a synthesis tool, which is the single most important thing to internalize before using any of them.

#delay: wait a fixed amount of simulated time​

initial begin
a = 1;
#10 a = 0; // wait 10 time units, then set a to 0
end

#10 pauses the block for 10 simulation time units (whatever `timescale has defined a "unit" to be) before the next statement executes. This is the standard way to space out testbench stimulus in time — "assert a, wait 10ns, deassert it" — and appears constantly in initial blocks and tasks used for verification.

#delay is not synthesizable

There is no hardware equivalent of "wait some fixed number of nanoseconds" written directly into RTL — real timing comes from the clock period and the physical propagation delay of gates after synthesis, neither of which a #delay statement in source code controls. A #delay inside code meant for synthesis is simply ignored or flagged as an error by synthesis tools; reserve it entirely for testbenches and simulation-only models.

@(event): wait for a signal transition​

always @(posedge clk) begin
q <= d;
end

@(posedge clk) waits for a rising edge on clk before proceeding — this specific form, unlike #delay, is fully synthesizable, and is in fact the standard way every sequential (clocked) always block in this entire topic has been written since Behavioral Modeling first introduced it. @(negedge clk) waits for a falling edge; @(a or b or c) (or the @(*) shorthand from Behavioral Modeling) waits for a change on any listed signal, used for combinational sensitivity lists.

The distinction that matters: @(posedge clk) used as an always block's sensitivity list (controlling when the whole block re-runs) synthesizes directly to a flip-flop's clock input. @(event) used inside a block, in the middle of a sequence of statements — waiting for a second, different event partway through a task, for instance — is a simulation-only pattern with no direct synthesis meaning, since real hardware can't "pause mid-instruction" the way a procedural wait implies.

wait: level-sensitive waiting​

initial begin
wait (ready == 1); // block here until ready is already 1, or becomes 1
start_transfer = 1;
end

wait (condition) blocks until condition is true — checking immediately (proceeding right away if already true) rather than waiting for a specific edge. This level-sensitive behavior is a natural fit for testbench handshaking ("don't proceed until the DUT signals it's ready") but, like #delay, has no direct synthesis equivalent — real combinational or sequential logic doesn't "block" execution, it continuously computes or samples on a clock edge.

Intra-assignment delay: which side of = the # sits on matters​

#delay can also sit inside a blocking assignment rather than in front of the whole statement, and where it sits changes when the right-hand side is evaluated:

initial begin
b = 1;
#5 a = b; // (1) wait 5 units, THEN evaluate b and assign it to a
end

initial begin
b = 1;
a = #5 b; // (2) evaluate b NOW, hold that captured value, assign it to a 5 units later
end

In (1), the statement itself is delayed — nothing happens for 5 time units, and only then does simulation read the current value of b and assign it. In (2), b is sampled immediately when the statement is reached, and only the assignment of that already-captured value to a is deferred by 5 units. If b changes during those 5 time units, the two forms can produce different results in a — this is the single most common source of confusion around #delay, and a frequent interview question in its own right. Form (1), with the # in front of the whole statement, is called an inter-assignment delay; form (2), with the # between = and the right-hand side, is the intra-assignment delay.

The same intra-assignment delay works with non-blocking assignment too: a <= #5 b; samples b now and schedules the update to a 5 units later, exactly like form (2) — except, being non-blocking, it never blocks the enclosing process from moving on to the next statement, regardless of where the delay sits.

Named events: signaling between blocks​

Besides waiting on a signal's edge, Verilog also supports a dedicated event type purely for synchronization — no data, just a trigger:

event data_ready;

initial begin
#20;
-> data_ready; // trigger the event
end

initial begin
@(data_ready); // block here until data_ready is triggered
$display("data is ready");
end

event declares a named event, -> triggers it, and @(event_name) blocks until it's triggered — a clean way for one initial/always block to signal another without routing an extra signal through the design. Like #delay and wait, named events are a simulation/testbench construct with no synthesis equivalent.

repeat with event control: waiting a fixed number of clock edges​

repeat(n) (from Loops) combines with event control to wait for a specific number of clock edges before proceeding — a very common testbench idiom:

initial begin
repeat (4) @(posedge clk); // wait for 4 rising edges of clk, then continue
$display("4 clock cycles have passed");
end

This reads as "wait for the next 4 posedge clk events," and is a compact alternative to writing @(posedge clk); four times in a row. Because it's built from @(posedge clk) used mid-block rather than as a block's own sensitivity list, it inherits the same non-synthesizable status as any other mid-block event control.

disable: terminating a block, task, or function early​

disable immediately stops the execution of a named block, task, or function — a procedural "break out of this, right now," even from outside the block itself:

initial begin: watchdog
fork
begin
wait (done == 1);
disable watchdog; // done arrived in time — cancel the timeout branch
end
begin
#1000;
$display("ERROR: timeout waiting for done");
end
join
end

disable block_name; (or disable task_name; for a task) terminates that block's execution wherever it currently is, without waiting for it to finish naturally — commonly used for timeout/watchdog patterns like the one above, or to abandon a task call already in progress. Like the other testbench-oriented constructs on this page, disable has no direct synthesis equivalent and belongs in verification code, not RTL.

Synthesizability summary​

ConstructSynthesizable?Where it belongs
@(posedge/negedge clk) as a block's sensitivity listYes — this is how flip-flops are describedSequential RTL
@(*) / explicit signal list as a block's sensitivity listYes — this is how combinational logic is describedCombinational RTL
#delay (including intra-assignment a = #5 b; / a <= #5 b;)NoTestbenches, simulation-only models
@(event) used mid-block (not as the block's trigger)NoTestbenches, tasks
wait (condition)NoTestbenches, handshaking code
Named events (event, ->, @(event_name))NoTestbench synchronization
repeat(n) @(posedge clk)No (built from mid-block event control)Testbenches, waiting N cycles
disableNoTestbenches, watchdog/timeout patterns

The pattern across this entire page — and, really, across all of Section C — is the same one first raised in Introduction to Verilog: Verilog is one language read by two very different tools, and a construct's legality in simulation says nothing about its legality (or meaning) in synthesis. Every one of the timing constructs here except the sensitivity-list form of @(posedge/negedge clk) belongs firmly on the simulation side of that line.

What's next​

Section C has now covered the full procedural vocabulary — conditionals, loops, functions/tasks, and timing control — along with which parts of each are synthesizable. Section D turns that vocabulary into a set of concrete rules for writing synthesizable RTL correctly: how sensitivity lists and assignment style determine exactly what hardware an always block produces, starting with the combinational-vs-sequential distinction underlying nearly everything covered so far.