VERILOG SERIES · MODULE 13

Behavioral Modelling Part 4 — While, Forever, Parallel, Force-Release, Events — VLSI Trainers
Verilog Series · Module 13

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.

🔄
Condition-Controlled
Loops until the condition becomes false. The number of iterations is determined at run time, not compile time.
📋
Pre-test Loop
Condition is checked before each iteration. If false initially, body never runs — unlike do-while (not in Verilog).
⚠️
Infinite Loop Risk
If the condition never becomes false, simulation hangs forever. Always ensure the body modifies the condition variable.
🧪
Testbench Use
Ideal in testbenches for polling loops, handshake protocols, and data-driven stimulus generation.

📐 while Execution Flow

Fig 1 — while loop flowchart
1
Evaluate condition
Check the while expression — if FALSE, exit the loop immediately
↓ TRUE
2
Execute loop body
Run all statements inside begin…end. Must eventually change the condition!
↺ go back to step 1
// 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

ConstructUse whenHas index?Count known?
repeat(N)Fixed number of iterations, no index needed✅ Yes, at start
forCounted iterations, need index variable in body✅ Yes, constant
whileData-dependent iterations, not known in advance❌ (manual)❌ Dynamic

💡 while Loop Examples

Fig 2 — Polling loop: wait for handshake

Testbench: poll ready signal, then read data
// ── 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

while loop sending a variable-length packet from a queue
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)

Behavioural clock divider — while used with time control inside always
// 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 in RTL synthesis: A 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.

Truly Infinite
No condition, no counter — runs forever. Only way to exit is $finish or disable from another block.
Needs Timing Control
Must contain #delay, @event, or wait(). Without one, simulation deadlocks at t=0.
🕐
Clock Generation
The canonical use: forever #5 clk = ~clk; — equivalent to always #5 clk = ~clk; but inside an initial block.
🚫
Not Synthesizable
Synthesis tools cannot handle an infinite loop. Use forever only in testbenches and behavioral simulation models.
Fig 5 — forever syntax and the deadlock trap
// ── 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

Various clock and strobe generation using forever
// ── 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

Continuous monitoring: check valid-ready handshake every cycle
// 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

forever watchdog that resets if no heartbeat within timeout window
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!
Fig 9 — fork-join timeline: all threads start at t=0, join waits for all
fork Thread 1 Thread 2 (longest) Thread 3 join continues waits for all threads

🔱 fork-join Modes (SystemVerilog)

Standard Verilog provides one join mode. SystemVerilog adds two more, giving flexible control over when parallel execution resumes:

fork … join
Waits for all threads to complete before continuing. The default Verilog mode.
fork … join_any
Resumes when any one thread completes. Other threads continue running in the background. SystemVerilog only.
fork … join_none
Launches all threads and immediately continues. All threads run in background. SystemVerilog only.
Fig 10 — fork-join syntax: single statements and begin-end blocks
// ── 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

Drive three independent bus channels simultaneously
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

Classic: whichever finishes first wins — join_any pattern
// 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
Variable scope in fork-join: Variables declared inside a 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 wire and reg
  • 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

Featureassign / deassignforce / 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
Fig 13 — force / release syntax
// ── 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

Force FSM to an untested state, verify output response
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

Model a stuck-at-0 fault on a wire for fault simulation
// ── 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).

posedge
Rising Edge
Triggers on 0→1 or x/z→1 transition. Used to model clock capture in all synchronous designs.
negedge
Falling Edge
Triggers on 1→0 or x/z→0 transition. Used for active-low clocks, negative-edge triggered flip-flops.
@(signal)
Any Change
Triggers on any value change of the signal — 0→1, 1→0, x→0, etc. Used in sensitivity lists.
event
Named Event
User-declared synchronisation flag. Triggered with ->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.

Fig 16 — All implicit event forms
// ── 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

When posedge and negedge trigger
clk ↑posedge ↑posedge ↑posedge ↓negedge ↓negedge posedge = 0→1 transition  |  negedge = 1→0 transition

🏷️ 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).

Fig 18 — Named event: declaration, trigger, and wait
// ── 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");
Event timing caveat: If the -> 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

Two initial blocks synchronised via events — classic handshake pattern
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

Use events to count and log transactions in a testbench
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

All three tasks must complete before proceeding — event barrier
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
Fig 22 — Complete testbench combining all five constructs
// 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
Design principle: These five constructs — 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.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top