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 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)
| Situation | Best 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
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)
- The event expression is evaluated when the triggering signal changes (e.g. when
achanges), not whenenablechanges. Ifais stable and onlyenablechanges from 0 to 1, the event does NOT trigger. iffhas higher precedence thanorin 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
iffjust 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.
guard is true at the moment of the change. 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
@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 propertystatement 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).
🔎 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
@seqis 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.
triggeredpersists through the rest of the current time step.- Use with
||to wait for the first of N sequences.
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: 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;
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
| Feature | disable blockName | disable 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
iffhas higher precedence thanor— 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).
stringvariables — new in SV.refarguments (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.triggeredpersists 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/releasein new testbench code — it also works on nets. - A
forceon a variable that has an active proceduralassignoverrides theassign; when theforceis released, theassigneffect becomes visible again.
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.
