Behavioral Modelling — Part 4
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.
🔁 while Loop — Introduction
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.
📐 while Execution Flow
// 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;
❌ Infinite loop — condition never changes
while (1) begin a = b; // no time advance — hangs! end
✅ Correct — condition advances each iteration
while (count < 8) begin @(posedge clk); count = count + 1; end
while vs for vs repeat — When to use which
| 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 |
💡 while Loop Examples
Fig 2 — Polling loop: wait for handshake
// ── 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;
Fig 3 — Data-driven stimulus: send until done
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
Fig 4 — while in RTL: clock divider (synthesizable)
// 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.
♾️ forever Loop — Introduction
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
💡 forever Loop Examples
Fig 6 — Clock generator patterns
// ── 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
Fig 7 — Protocol monitor using forever
// 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
Fig 8 — Watchdog using forever with disable
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
⚡ Parallel Blocks — fork-join
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-end — Sequential
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-join — Parallel
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!
🔱 fork-join Modes (SystemVerilog)
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
💡 Parallel Block Examples
Fig 11 — Multi-channel testbench with fork-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
Fig 12 — Timeout race using fork-join
// 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.
🔐 force — release Construct
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.
🔴 force — Override everything
- Works on
wireandreg - Overrides all normal drivers
- Overrides continuous
assign - Overrides gate primitive outputs
- Stays forced until
release - Simulation only — not synthesizable
🟢 release — Restore normal drive
- Removes the force override
- Wire returns to its normal driver value
- Reg retains its forced value until next normal assignment
- Must release before the net can be normally driven again
force vs assign — Key Differences
| 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
💡 force — release Examples
Fig 14 — Corner case testing: force internal state
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
Fig 15 — Stuck-at fault injection
// ── 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
📡 Events in Verilog
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).⚡ Event Types — Implicit Events
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
Fig 17 — posedge and negedge event timing
🏷️ Named Events
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 Examples
Fig 19 — Producer-consumer synchronisation with named events
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
Fig 20 — Event-based transaction coverage tracking
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
Fig 21 — Combining events with fork-join for barrier synchronisation
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
📋 Summary Comparison
| 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.
