SYSTEMVERILOG SERIES · SV-08C

SystemVerilog Series — SV-08c: Disable, Event Control, Wait & Assign — VLSI Trainers
SystemVerilog Series · SV-08c

Disable, Event Control, Level-Sensitive Sequences & Procedural Assign

The SV additions to disable — what it reaches vs disable fork; new event control forms including iff guards, sequence events, and object-member sensitivity; level-sensitive sequence waits with triggered; and the status of procedural assign/deassign.

disable — Recap and SV Context

The Verilog-2001 disable statement terminates all processes currently executing a named block or task. SystemVerilog keeps disable but adds break, continue, and return for most common use cases, leaving disable for the specific scenarios where named-block termination is genuinely needed.

// disable: terminate all processes executing a named block
// — works on any named block, even one not in the same scope
module top;
  always always1: begin
    t1: task1();
    do_other_work();
  end

  always begin
    // Exit task1 (which was called from always1)
    disable u1.always1.t1; // hierarchical: stops all execs of t1 in u1.always1
  end
endmodule
disable uses static scope, not dynamic parentage. When you write disable blockName, it terminates all processes executing that block, regardless of which process forked them. This is a syntactic, not a dynamic, relationship. Two completely unrelated threads that both happen to be inside blockName will both be killed. Contrast with disable fork, which only terminates the children of the calling process.

When to still use disable (vs break/continue/return)

SituationBest tool
Exit the innermost loop break
Skip to next loop iteration continue
Exit the current task/function early return
Kill all children of the current process disable fork
Kill a specific named block from another scope disable blockName
Kill all executions of a named task (from any caller) disable taskName

disable fork

disable fork terminates all active descendants of the calling process — its children, their children, and so on down the dynamic process tree. It uses the runtime parent-child relationship, not the static block structure.

// Pattern: race to first device — kill the losers
task get_first(output int adr);
  fork
    wait_device(1,  adr);   // three processes compete
    wait_device(7,  adr);
    wait_device(13, adr);
  join_any                   // unblock when first one finishes
  disable fork;              // kill the other two still waiting
endtask

disable blockName — static scope

// Kills ALL processes inside that named block
// regardless of who forked them
// Can kill unrelated processes!
disable blk_A;  // kills every thread in blk_A

disable fork — dynamic scope

// Kills only THIS process's descendants
// Other processes' children are NOT affected
// Safe for use in concurrent testbenches
disable fork;  // kills MY children only
disable fork is the safe choice in concurrent testbenches. Because it only kills the calling process’s own descendants, you can use it without worrying about accidentally terminating threads belonging to other test sequences running in parallel. The classic use case is the “first responder wins” pattern: fork N workers with join_any, then disable fork to clean up the remaining N-1 workers.

Event Control with iff

SystemVerilog adds an iff qualifier to the @ event control. The event only triggers if the iff expression is true at the moment the edge occurs. This is cleaner than a separate if inside the block for simple edge-with-condition patterns.

// Classic latch sensitivity — only when enable is high
module latch(output logic[31:0] y, input[31:0] a, input enable);
  always @(a iff enable == 1)
    y <= a;  // latch transparent mode — only update when enabled
endmodule

// Clock edge with reset guard
always_ff @(posedge clk iff reset==0 or posedge reset) begin
  r1 <= reset ? 0 : r2 + 1;
end
// Sensitive to posedge clk ONLY when reset==0, OR posedge reset (unconditional)
iff important nuances:
  • The event expression is evaluated when the triggering signal changes (e.g. when a changes), not when enable changes. If a is stable and only enable changes from 0 to 1, the event does NOT trigger.
  • iff has higher precedence than or in event expressions. Use parentheses if the intent might be ambiguous: @((a iff cond) or b).
  • This is event-filtering — not sensitivity expansion. The event list is still sensitive to changes in the primary signal; the iff just filters whether those changes actually trigger the process.

📌 What Can Be in an Event Expression

SystemVerilog significantly expands what is legal inside @(...) and wait(...) expressions compared to Verilog-2001.

@(signal)
Any change on a variable or net of integral or string type. Any change on any bit (including X↔Z) triggers.
@(posedge / negedge)
Rising or falling edge. For 2-state types: posedge = 0→1, negedge = 1→0. For 4-state: X/Z transitions also qualify as edges in some tools.
@(expr iff guard)
Event triggers only when guard is true at the moment of the change. New in SV.
@(obj.member)
Change to a member of a class object. Object members and aggregate elements are legal as long as the result is singular. New in SV.
@(sequence_name)
Blocks until the named sequence reaches its endpoint. New in SV — covered in §8.10.1.
@(method_call)
Non-virtual function call that returns a singular value. Re-evaluates when any referenced member or array element changes. New in SV.
// Integral types — any change triggers
int i; string s;
@i;             // triggers on any change to i
@s;             // triggers on any change to string s

// ref argument (passed by reference) in event sensitivity
task automatic watch(ref int r);
  @r;             // triggers when r (the caller's variable) changes
endtask

// Array element — member access is legal
int arr[8];
@(arr[3]);      // triggers when element 3 changes

// Dynamic array and queue — size/element changes
real AOR[];     // dynamic array of reals
byte stream[$]; // queue of bytes
wait(AOR.size() > 0);       // wait for array to be allocated
wait($bits(stream) > 60);  // wait for total bit count to exceed 60

📚 Object Members & Method Sensitivity

Event expressions can reference members of class objects. The event fires when the object handle is updated to point to a different object, or when a referenced member’s value changes.

class Packet;
  int  status;
  byte payload[];
endclass

Packet p = new; // Packet 1
Packet q = new; // Packet 2

initial fork

  @(p.status);     // wait for status field of Packet 1 to change

  @q;              // wait for a write to the HANDLE q (not its members)

  begin
    #10 q = p;     // writes the handle q → triggers @q
                   // @(p.status) now watches status in Packet 2
                   // (because p and q both now point to Packet 2)
  end

join
Handle vs member sensitivity: @q fires when the variable q (the handle) is written with a new value — i.e., when it is redirected to point at a different object. It does not fire when members of the object that q currently points to change. @(q.status) fires when the status member of whichever object q currently points to changes. After q = p, @(p.status) now monitors the same object that q points to — because p still points to Packet 1 and now q also points to Packet 1. Wait — re-read the example above: after q = p, both p and q point to Packet 2? Actually in the example q = p makes both point to what p points to (Packet 1). The @(p.status) event now monitors Packet 1’s status through the updated reference.

Non-virtual method calls in event expressions

// Non-virtual function methods can appear in event expressions
// — they are re-evaluated when any referenced member changes
class Monitor;
  int value;
  function int doubled(); return value * 2; endfunction
endclass

Monitor m = new();
@(m.doubled());   // re-evaluates when m.value changes
                  // triggers if m.value*2 transitions

// Restriction: must be a function (not a task), must return singular value
// Task calls are ILLEGAL in event expressions

📋 Sequence Events as Event Controls

A named sequence can be used directly as an event control. The process blocks until the sequence reaches its endpoint — that is, until a complete match of the sequence is detected. This connects procedural code to assertion sequences without needing explicit handshaking signals.

sequence abc;
  @(posedge clk) a ##1 b ##1 c;
endsequence

program test;
  initial begin
    @abc;                       // block until abc completes (a then b then c)
    $display("Saw a-b-c");
    L1: continue_test();        // continues after the sequence fires
  end
endprogram

Key rules for sequence event controls

  • The sequence is instantiated as if by an assert property statement when first reached at runtime.
  • The event control synchronises to the end of the sequence regardless of when the sequence started.
  • Arguments to sequences used in event controls must be static — automatic variables cannot be used as sequence arguments.
  • The process resumes in the Observe region after the endpoint is detected (after all RTL has settled in that time step).
Practical use: Sequence events are most useful in testbench program blocks where you want to synchronise procedural test actions to a multi-cycle protocol pattern. Instead of manually counting cycles or checking individual signal values, you declare the pattern as a sequence and simply wait for it. This makes the testbench intent much clearer.

🔎 Level-Sensitive Sequence Controls

The @sequence event control is edge-sensitive — it unblocks when the sequence endpoint is newly reached. If the sequence has already completed, it will miss it. The triggered method on a sequence provides a level-sensitive alternative: it returns true if the sequence has reached its endpoint at any point during the current time step.

sequence abc;
  @(posedge clk) a ##1 b ##1 c;
endsequence

sequence de;
  @(negedge clk) d ##[2:5] e;
endsequence

program check;
  initial begin
    // Level-sensitive wait: unblocks if EITHER sequence has already triggered
    // or as soon as one of them triggers in any future time step
    wait(abc.triggered || de.triggered);

    // Now check which one(s) fired
    if(abc.triggered) $display("abc succeeded");
    if(de.triggered)  $display("de succeeded");

    L2: continue_check();
  end
endprogram

@sequence — edge-sensitive

  • Blocks until the sequence newly completes.
  • If the sequence already completed before @seq is reached, that completion is missed.
  • Suitable when you know the sequence hasn’t completed yet.

wait(seq.triggered) — level-sensitive

  • Immediately unblocks if the sequence completed earlier in the same time step.
  • triggered persists through the rest of the current time step.
  • Use with || to wait for the first of N sequences.
triggered persists only within the current time step. The triggered property is set in the Observe region and remains true until simulation time advances. In the next time step it resets. This mirrors the behaviour of the SV event type’s .triggered property — both avoid the “missed trigger” race that plagued Verilog-2001’s plain event.

Procedural Assign and Deassign — Deprecated

Verilog-2001 provided assign and deassign as procedural statements that could override a variable’s normal procedural drivers. SystemVerilog keeps them for backward compatibility but marks them as candidates for removal in a future version of the language.

Procedural assign/deassign — behaviour
// procedural assign: overrides all procedural drivers of a variable
// until a procedural deassign (or another assign) releases it
reg q;

initial begin
  assign q = preset; // q now follows preset continuously
  #10;
  deassign q;        // q returns to normal — value holds at current preset
end

always @(posedge clk)  // this driver is suppressed while assign is active
  q <= d;
Avoid procedural assign/deassign in new code. These statements are confusing, interact badly with simulation schedulers, are not synthesisable, and may be removed in a future IEEE 1800 revision. For testbench overriding of RTL signals, use force/release instead. For asynchronous preset/clear on flip-flops, model them explicitly in the always_ff sensitivity list (e.g. @(posedge clk or posedge preset)).

force/release — the preferred alternative

// force: overrides both variables AND nets (more powerful than assign)
// release: removes the force, variable/net returns to its normal drivers

initial begin
  force  dut.q = 1;    // override DUT internal signal for testing
  #10;
  release dut.q;        // restore normal operation
end

// force can also target nets (wire, tri) — procedural assign cannot
force   net_name = 0;   // overrides net driver — for test stimulus injection
release net_name;       // driver resumes

📋 Quick Reference

disable vs disable fork

Featuredisable blockNamedisable fork
What it terminates All processes executing the named block, regardless of caller Only the calling process’s descendants
Basis Static syntactic scope Dynamic parent-child relationship
Risk Can accidentally kill unrelated threads Safe in concurrent testbenches
Typical use case Kill a specific named task across all callers Kill worker threads after first responder wins

Event control with iff rules

  • iff has higher precedence than or — parenthesise if combining: @((sig iff cond) or other).
  • The iff expression is evaluated when the primary signal changes, not when the iff expression itself changes.
  • If enable goes high but the primary signal stays constant, no event fires.

Legal event expression types (SV additions)

  • Any integral type variable or net (unchanged from Verilog-2001).
  • string variables — new in SV.
  • ref arguments (by-reference parameters) — new in SV.
  • Array elements, associative array elements, class object members — new in SV.
  • Non-virtual function method calls returning a singular value — new in SV.
  • Named sequence instances (@seqName) — new in SV.

Sequence sensitivity — @seq vs wait(seq.triggered)

  • @seq — edge-sensitive: unblocks only when sequence newly reaches endpoint. Misses if already completed.
  • wait(seq.triggered) — level-sensitive: immediately unblocks if completed in the current time step.
  • triggered persists through the rest of the current time step then resets.

Procedural assign/deassign

  • Still supported in SV but deprecated — may be removed in future IEEE 1800.
  • Only works on variables (reg/logic/bit etc.) — not on nets.
  • Not synthesisable.
  • Use force/release in new testbench code — it also works on nets.
  • A force on a variable that has an active procedural assign overrides the assign; when the force is released, the assign effect becomes visible again.
Coming next: SV-09 covers Section 9 — Processes: always_comb, always_ff, always_latch, the three fork-join variants (join, join_any, join_none), wait fork, disable fork, and the process built-in class for fine-grained process control.

Leave a Comment

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

Scroll to Top