Complete coverage of while loops, forever loops, parallel blocks (fork-join), the force-release construct, and Verilog events — with practical RTL and testbench examples throughout.
The while loop executes a statement or block as long as a condition remains true. Unlike repeat (fixed count) or for (counted index), while supports dynamic, data-dependent iteration where the number of iterations isn’t known in advance.
The condition is evaluated before each iteration — if it is false at the start, the body never executes.
// Syntax while (condition) begin statement_1; statement_2; // must eventually make condition FALSE end // Single-statement form (no begin/end needed) while (count < 8) count = count + 1;
while (1) begin a = b; // no time advance — hangs! end
while (count < 8) begin @(posedge clk); count = count + 1; end
| Construct | Use when | Has index? | Count known? |
|---|---|---|---|
| repeat(N) | Fixed number of iterations, no index needed | ❌ | ✅ Yes, at start |
| for | Counted iterations, need index variable in body | ✅ | ✅ Yes, constant |
| while | Data-dependent iterations, not known in advance | ❌ (manual) | ❌ Dynamic |
// ── Polling for a ready signal ──────────────────────────────── initial begin req = 1'b1; // assert request // Poll ack — check on every rising clock edge while (!ack) @(posedge clk); // waits until ack=1 data_captured = data_bus; // capture after handshake req = 1'b0; // deassert request end // ── Wait for FIFO to become non-empty ──────────────────────── while (fifo_empty) @(posedge clk); rd_en = 1'b1; // read first available entry @(posedge clk); rd_en = 1'b0;
integer pkt_len, byte_idx; reg [7:0] pkt_data [0:255]; initial begin pkt_len = 12; byte_idx = 0; // Transmit all bytes — length known only at run time while (byte_idx < pkt_len) begin @(posedge clk); tx_data = pkt_data[byte_idx]; tx_valid = 1'b1; // Respect backpressure — stall if receiver not ready while (!tx_ready) @(posedge clk); // nested while byte_idx = byte_idx + 1; end tx_valid = 1'b0; end
// Clock divider — divide by N using while in testbench clock gen parameter N = 4; reg clk_div; integer cnt; initial begin clk_div = 0; cnt = 0; while (1) begin // run forever (simulation) @(posedge clk); cnt = cnt + 1; if (cnt == N) begin clk_div = ~clk_div; cnt = 0; end end end
while loop is synthesizable only if the simulator can determine the loop terminates with a fixed number of iterations at elaboration time. In practice, only use while in testbenches. Use for with constant bounds in synthesizable RTL.The forever statement creates an infinite loop that runs continuously without any termination condition. It is the simplest and most explicit way to model continuously repeating behavior — clock generators, protocol monitors, and watchdog timers.
A forever loop must contain a timing control statement (such as @, #, or wait). Without one, the simulator will spin infinitely at the same time step, hanging the simulation.
$finish or disable from another block.#delay, @event, or wait(). Without one, simulation deadlocks at t=0.forever #5 clk = ~clk; — equivalent to always #5 clk = ~clk; but inside an initial block.forever only in testbenches and behavioral simulation models.// ── Basic syntax ────────────────────────────────────────────── forever statement; // single statement forever begin statement_1; statement_2; end // ── ✅ Correct: always has a timing control ─────────────────── initial begin clk = 0; forever #5 clk = ~clk; // toggles every 5 units end // ── ❌ Deadlock: no timing control ──────────────────────────── forever clk = ~clk; // simulation hangs at t=0! // ── forever vs always ───────────────────────────────────────── always #5 clk = ~clk; // module-level — equivalent initial begin forever #5 clk = ~clk; // inside initial — also equivalent end
// ── Simple symmetric clock (50% duty cycle) ─────────────────── initial begin clk = 1'b0; forever #5 clk = ~clk; // period = 10 time units end // ── Asymmetric clock (33% high, 67% low) ───────────────────── initial begin clk = 1'b0; forever begin #3 clk = 1'b1; // high for 3 #7 clk = 1'b0; // low for 7, period=10 end end // ── Multi-phase clocks ───────────────────────────────────────── initial begin clk_a = 0; clk_b = 0; fork forever #5 clk_a = ~clk_a; // 100 MHz forever #10 clk_b = ~clk_b; // 50 MHz join end // ── Periodic strobe every N clocks ──────────────────────────── initial begin strobe = 0; forever begin repeat(8) @(posedge clk); // every 8 clocks strobe = 1; #1; strobe = 0; // 1-unit pulse end end
// AXI-style valid/ready monitor — runs throughout simulation initial begin forever begin @(posedge clk); // Check: valid must not deassert while waiting for ready if (valid && !ready) @(posedge clk); // stall — check next cycle if (valid && !ready && !$past(valid)) $error("Protocol violation: valid dropped before ready"); // Check: data must be stable while valid and not ready if (valid && !ready && data !== $past(data)) $error("Protocol violation: data changed while valid"); end end
initial begin : watchdog forever begin fork begin : timer #1000; // timeout window $display("WATCHDOG TIMEOUT"); $finish; end begin @(heartbeat); // wait for heartbeat event disable timer; // heartbeat received — cancel timeout end join end end
The fork-join construct executes multiple statement blocks simultaneously in parallel. Unlike a begin-end block (sequential), all branches of a fork start at the same simulation time and run concurrently — modelling hardware parallelism within a procedural context.
This is most useful in testbenches for concurrent stimulus generation, multi-threaded protocol checking, and timeout mechanisms.
begin a = 1; // t=0 #5; b = 1; // t=5 #5; c = 1; // t=10 end // Total duration: 10 units // a, b, c change in sequence
fork begin a = 1; end // t=0 begin #5; b = 1; end // t=5 begin #5; c = 1; end // t=5 join // Total duration: 5 units // b and c change simultaneously!
Standard Verilog provides one join mode. SystemVerilog adds two more, giving flexible control over when parallel execution resumes:
// ── Single-statement threads (compact) ─────────────────────── fork #10 a = 1; // thread 1: at t=10, a←1 #20 b = 1; // thread 2: at t=20, b←1 #30 c = 1; // thread 3: at t=30, c←1 join // resumes at t=30 (longest) // ── Multi-statement threads (each in begin-end) ─────────────── fork begin // thread A rst_n = 0; #20 rst_n = 1; end begin // thread B — runs concurrently repeat(5) @(posedge clk); load = 1; @(posedge clk); load = 0; end join
initial begin rst_n = 0; repeat(2) @(posedge clk); rst_n = 1; // Drive all three channels at the same time fork begin : ch_a // AXI master channel A axi_wr(32'h1000, 32'hDEADBEEF); axi_wr(32'h1004, 32'hCAFEBABE); axi_rd(32'h1000, rd_data_a); end begin : ch_b // AXI master channel B — runs simultaneously @(posedge clk); // slight stagger axi_wr(32'h2000, 32'hA5A5A5A5); axi_rd(32'h2000, rd_data_b); end begin : ch_mon // Protocol monitor — concurrent repeat(100) begin @(posedge clk); if (arb_err) $error("Arbitration error!"); end end join // wait for all three to complete $display("All channels finished. Checking results..."); end
// Standard Verilog (join) — simulate join_any using disable initial begin : top fork begin : wait_done // thread: wait for completion wait(dut_done); $display("DUT finished at t=%0t", $time); disable timeout; // cancel timeout thread end begin : timeout // thread: watchdog timer #50000; $error("TIMEOUT: DUT did not complete within 50us"); disable wait_done; // cancel the wait thread $finish; end join end
fork branch are local to that branch. Variables from the enclosing block are shared — all threads can read and write them. Be careful about race conditions when multiple threads write the same variable.The force and release statements are the most powerful signal override mechanism in Verilog. Unlike assign/deassign (which only works on reg), force can override any net or variable — including wires driven by continuous assignments and gate outputs — regardless of its normal drivers.
wire and regassignrelease| Feature | assign / deassign | force / release |
|---|---|---|
| Target types | reg only | reg and wire |
| Overrides assign? | ❌ No — only procedural assignments | ✅ Yes — overrides continuous assign too |
| Overrides gates? | ❌ No | ✅ Yes — overrides gate outputs |
| After release (wire) | — | Returns to driven value immediately |
| After release (reg) | Retains last forced value | Retains last forced value |
| Synthesizable | ❌ No | ❌ No |
| Primary use | Async clear/preset models | Testbench debugging, fault injection |
// ── Force a reg ─────────────────────────────────────────────── force reg_var = expression; // override reg_var with expression release reg_var; // release; retains last forced value // ── Force a wire (even driven by assign or gate) ─────────────── force wire_var = expression; // override wire — ignores normal driver release wire_var; // release; wire immediately returns to normal driver // ── Hierarchical force ──────────────────────────────────────── force dut.alu.carry_out = 1'b1; // force internal net via hierarchy release dut.alu.carry_out; // restore normal operation
initial begin // Normal reset and run rst_n = 0; @(posedge clk); @(posedge clk); rst_n = 1; repeat(5) @(posedge clk); // Force FSM to ERROR state — bypass normal state machine logic force dut.fsm.state = 3'b101; // directly set internal state reg @(posedge clk); // Verify error output asserts correctly if (!dut.err_flag) $error("FAIL: err_flag should assert in ERROR state"); else $display("PASS: err_flag asserts in ERROR state"); // Release and verify FSM recovers release dut.fsm.state; repeat(10) @(posedge clk); if (dut.err_flag) $error("FAIL: err_flag did not clear after recovery"); end
// ── Stuck-at-0 fault on data bus bit 3 ─────────────────────── task inject_stuck_at_0; input integer duration; // how many cycles to inject begin $display("Injecting stuck-at-0 on data[3] for %0d cycles", duration); force dut.data_bus[3] = 1'b0; // stuck at 0 repeat(duration) @(posedge clk); release dut.data_bus[3]; // remove fault $display("Fault removed at t=%0t", $time); end endtask // ── Stuck-at-1 fault on a combinational wire ───────────────── task inject_stuck_at_1_wire; begin force dut.alu.carry_chain = 1'b1; // override even continuous assign repeat(20) @(posedge clk); release dut.alu.carry_chain; // wire instantly returns to assign result end endtask
An event in Verilog is a synchronisation mechanism — a way for one procedural block to signal another that something has happened. Events carry no data, only the occurrence of a trigger. They are used to coordinate concurrent processes in testbenches and behavioural models.
Verilog has two categories of events: implicit events (signal edges and level changes) and named events (user-defined synchronisation points).
->event_name, waited on with @(event_name).Implicit events are triggered by signal value changes. They are the core mechanism behind sensitivity lists and edge-triggered flip-flop modelling.
// ── Edge events ─────────────────────────────────────────────── @(posedge clk) // rising edge of clk (0→1 or x→1) @(negedge clk) // falling edge of clk (1→0 or x→0) @(posedge clk or negedge rst_n) // either event // ── Level / change events ───────────────────────────────────── @(a) // any change of signal a @(a or b or c) // change of a, b, or c @(*) // any change of any RHS signal (Verilog-2001) // ── Event as a statement (suspend until event) ──────────────── always begin @(posedge clk); // suspend here, wake on rising clk q <= d; // execute after edge end // ── Event in conditional expression ────────────────────────── always @(posedge clk) q <= d; // inline — no begin/end needed // ── Waiting for a value change using @(signal) ──────────────── initial begin @(done); // wait for done to change (any edge) $display("Done changed!"); end
A named event is a user-defined synchronisation object declared with the event keyword. It carries no data value — only the occurrence of the trigger. One block triggers it with -> and other blocks wait for it with @(event_name).
// ── Declaring a named event ─────────────────────────────────── event tx_complete; // declare: no width, no initial value event rx_ready, error_detected; // multiple in one line // ── Triggering an event: the '->' operator ──────────────────── initial begin // ... do some work ... repeat(8) @(posedge clk); ->tx_complete; // trigger the event end // ── Waiting for an event: @ ─────────────────────────────────── initial begin @(tx_complete); // suspend until tx_complete is triggered $display("TX finished at t=%0t", $time); end // ── Checking if event already triggered: 'triggered' property ─ if (tx_complete.triggered) // true if triggered in current time step $display("Event already fired this time step");
-> trigger and the @(event) wait execute at exactly the same simulation time step but in different always/initial blocks, the order depends on the simulator’s scheduling. Use the .triggered property (SystemVerilog) or careful design to avoid this race condition.event data_ready, data_consumed; reg [7:0] shared_data; // ── Producer ───────────────────────────────────────────────── initial begin repeat(4) begin repeat(5) @(posedge clk); // produce every 5 cycles shared_data = $random; // write new data ->data_ready; // signal consumer @(data_consumed); // wait for consumer to finish $display("Producer: cycle complete"); end end // ── Consumer ───────────────────────────────────────────────── initial begin repeat(4) begin @(data_ready); // wait for producer $display("Consumer: got 0x%02h", shared_data); @(posedge clk); // process for one cycle ->data_consumed; // signal producer end end
event wr_trans, rd_trans, err_trans; integer wr_count=0, rd_count=0, err_count=0; // ── Transaction counters ───────────────────────────────────── always @(wr_trans) wr_count = wr_count + 1; always @(rd_trans) rd_count = rd_count + 1; always @(err_trans) err_count = err_count + 1; // ── Trigger from driver tasks ────────────────────────────────── task do_write; input [31:0] addr, data; begin // ... drive bus signals ... @(posedge clk); ->wr_trans; // log the transaction end endtask // ── Print summary at end ────────────────────────────────────── initial begin #100000; $display("Writes: %0d Reads: %0d Errors: %0d", wr_count, rd_count, err_count); $finish; end
event barrier; integer barrier_cnt = 0; task task_with_barrier; input integer delay_cycles; begin repeat(delay_cycles) @(posedge clk); // Arrive at barrier barrier_cnt = barrier_cnt + 1; if (barrier_cnt == 3) ->barrier; // last one signals else @(barrier); // others wait $display("Task finished, barrier released"); end endtask initial begin fork task_with_barrier(5); // finishes at t=50 task_with_barrier(12); // finishes at t=120 (last) task_with_barrier(8); // finishes at t=80 join // all three released simultaneously at t=120 end
| Construct | Purpose | Terminates how | Synthesizable | Typical use |
|---|---|---|---|---|
| while(cond) | Loop while condition true | Condition becomes false | ⚠️ Const only | Polling, handshake, data-driven TB stimulus |
| forever | Infinite loop | $finish or disable | ❌ No | Clock gen, monitors, watchdogs in TB |
| fork-join | Parallel concurrent threads | All (join), any (join_any), never (join_none) | ❌ No | Multi-channel TB, concurrent stimulus, timeouts |
| force / release | Override any net or reg | release statement | ❌ No | Corner case testing, fault injection, debug |
| event (named) | Zero-data synchronisation flag | N/A — triggered on demand | ❌ No | Producer-consumer sync, transaction logging, barriers |
| @(posedge/negedge) | Wait for signal edge | On occurrence of edge | ✅ Yes | All synchronous RTL — flip-flops, latches |
// Testbench showing while, forever, fork-join, force, and events event dut_ready; event test_done; module combined_tb; reg clk = 0, rst_n; wire [7:0] dut_out; // ── Clock: forever ──────────────────────────────────────── initial forever #5 clk = ~clk; // ── Reset and initialise ────────────────────────────────── initial begin rst_n = 0; repeat(3) @(posedge clk); rst_n = 1; ->dut_ready; // signal: DUT out of reset end // ── Parallel test threads ───────────────────────────────── initial begin @(dut_ready); // wait for DUT ready event fork begin : stimulus // while loop sending data integer i = 0; while (i < 10) begin @(posedge clk); i = i + 1; end ->test_done; end begin : corner_case // force an internal net midway repeat(5) @(posedge clk); force dut.internal_cnt = 8'hFF; @(posedge clk); release dut.internal_cnt; end join @(test_done); $display("Test complete at t=%0t", $time); $finish; end endmodule
while, forever, fork-join, force/release, and named event — are the backbone of advanced testbench architecture. Together they enable concurrent stimulus generation, protocol monitoring, fault injection, and synchronised verification flows that would be impossible with sequential initial blocks alone.