The examples in SystemVerilog Concurrent Assertions checked simple boolean expressions on every single clock edge. But sequential checks take several clock cycles to complete, and the time delay between two parts of a sequence is specified with the ## operator.

12 min read | Intermediate Level

What You'll Learn

  • What the ## operator means inside a sequence, and how it differs from a plain boolean check
  • Why a fresh attempt at the sequence starts on every clock edge, resulting in several overlapping instances tracked in parallel
  • How to read a "started at Xns, failed/passed at Yns" log message for one specific pipelined instance
  • Why a sequence's outcome for a given attempt is only known some cycles after it starts, not immediately

The ## Operator

If a is not high on a given clock cycle, that attempt at the sequence starts and fails on the very same cycle. But if a is high, the attempt starts and succeeds if b is high exactly 2 clocks later — it fails if b is low 2 clocks later instead:


module tb;
  bit a, b;
  bit clk;
  
  // This is a sequence that says 'b' should be high 2 clocks after
  // 'a' is found high. The sequence is checked on every positive
  // edge of the clock which ultimately ends up having multiple
  // assertions running in parallel since they all span for more
  // than a single clock cycle.
  sequence s_ab;
    @(posedge clk) a ##2 b;
  endsequence
  
  // Print a display statement if the assertion passed
  assert property(s_ab)
  	$display ("[%0t] Assertion passed !", $time);
  
  always #10 clk = ~clk;
  
  initial begin
    for (int i = 0; i < 10; i++) begin
      @(posedge clk);
      a <= $random;
      b <= $random;  
  
      $display("[%0t] a=%b b=%b", $time, a, b);
    end
  
    #20 $finish;
  end
endmodule

Multiple Instances Run in Parallel

Since the assertion is checked on every clock edge, a brand-new attempt at s_ab begins each time — and since each attempt takes 2 full cycles to resolve, several attempts end up "in flight" at once, each independently tracked from its own starting point:

Time (ns)abSequence StartResult
1000NoFAIL (Start@10, Fail@10)
3001NoFAIL (Start@30, Fail@30)
5011Yes
7011Yes
9010YesFAIL (Start@50, Fail@90)
11011YesPASS (Start@70, Pass@110)
13001NoPASS (Start@90, Pass@130)
15010YesFAIL (Start@110, Fail@150)
17010Yes
19010YesFAIL (Start@150, Fail@190)

Walking through two specific instances end to end makes the pattern concrete:

  • At 50ns, a=1, so an attempt starts there. It needs b=1 two edges later, at 90ns — but b=0 at 90ns, so this specific attempt fails, reported on the 90ns row as FAIL (Start@50, Fail@90).
  • At 70ns, a separate attempt starts (a=1 again). It needs b=1 at 110ns — and b=1 there, so this attempt passes, reported on the 110ns row as PASS (Start@70, Pass@110).

Notice these two attempts overlap in time — the one started at 50ns is still unresolved when the one at 70ns begins, and both are being tracked simultaneously. Each row in the table can carry two separate pieces of information: whether a new attempt starts on that exact edge, and whether some earlier attempt happens to resolve on that same edge.

Output

Output
Compiler version P-2019.06-1; Runtime version P-2019.06-1;  Jan 16 06:49 2020
[10] a=0 b=0
"testbench.sv", 14: tb.unnamed$$_0: started at 10ns failed at 10ns
	Offending 'a'
[30] a=0 b=1
"testbench.sv", 14: tb.unnamed$$_0: started at 30ns failed at 30ns
	Offending 'a'
[50] a=1 b=1
[70] a=1 b=1
[90] a=1 b=0
"testbench.sv", 14: tb.unnamed$$_0: started at 50ns failed at 90ns
	Offending 'b'
[110] a=1 b=1
[110] Assertion passed !
[130] a=0 b=1
"testbench.sv", 14: tb.unnamed$$_0: started at 130ns failed at 130ns
	Offending 'a'
[130] Assertion passed !
[150] a=1 b=0
"testbench.sv", 14: tb.unnamed$$_0: started at 110ns failed at 150ns
	Offending 'b'
[170] a=1 b=0
[190] a=1 b=0
"testbench.sv", 14: tb.unnamed$$_0: started at 150ns failed at 190ns
	Offending 'b'
$finish called from file "testbench.sv", line 27.
$finish at simulation time                  210
           V C S   S i m u l a t i o n   R e p o r t
Time: 210 ns

Each log line names exactly which attempt it's reporting on, by its own start time — "started at 50ns failed at 90ns" is the same attempt traced above, and "started at 130ns failed at 130ns" is a brand-new attempt failing immediately because a=0 right at its own start.

Quick Check

Q: At 130ns, two separate things are reported: a new attempt's start status ("No"), and an older attempt's outcome ("PASS"). Which earlier attempt is that PASS actually reporting on?

Show Answer

The attempt that started at 90ns. It needed b=1 two edges later, at 130ns — and b=1 there, so it passes. This is completely independent of the fact that a different, brand-new attempt also happens to fail immediately at that same 130ns edge, since a=0 there.

Quick Check

Q: With a fresh attempt starting on every edge and each one taking exactly 2 edges to resolve, how many unresolved attempts can be "in flight" at any given moment (not counting the one resolving on that exact edge)?

Show Answer

Up to 2 — the attempt that started 1 edge ago (needing 1 more edge to resolve) and the attempt that just started this edge (needing 2 more). Since resolution always takes exactly 2 edges, no more than 2 unresolved attempts ever need to be tracked simultaneously for a ##2 delay.

Where Is This Used?

Modern pipelined bus protocols routinely have several transactions in flight at once — a second request can be issued before the first one's response has arrived. Checking a protocol like this with assertions requires exactly this overlapping-instance behavior: each request needs its own independently-tracked check for its corresponding response, without interfering with checks already in progress for earlier requests.

Common Beginner Mistakes

Mistake #1: Assuming only one sequence attempt runs at a time

As this article shows, a concurrent assertion starts a fresh attempt on every clock edge by default. Why? Several attempts can be genuinely in flight simultaneously, each tracked from its own independent starting point — a summary table or log needs to be read with that in mind, not as if only one check were ever active.

Mistake #2: Assuming a ##2 sequence's outcome is known immediately

An attempt that starts because a is high doesn't resolve until 2 full cycles later, when b is finally checked. Why? There's a real gap between when a sequence begins and when its outcome is known — assuming an assertion has already "passed" the moment its first condition is met is premature.

Mistake #3: Misreading a summary table's row-to-outcome mapping

A row showing values for a given edge doesn't necessarily report the outcome of an attempt that started on that same row. Why? As the Quick Check above shows, the outcome shown on a given row can belong to an attempt that started several rows earlier — always check the "Start@" annotation to know which attempt an outcome actually belongs to.

Mistake #4: Assuming one attempt's immediate failure affects other attempts

A new attempt failing right at its own start (because a=0 there) has no effect on any other attempt still in flight from an earlier edge. Why? Every attempt is tracked completely independently — one failing doesn't cancel, pause, or otherwise interfere with any of the others.

Practice Exercise

Challenge: Trace a ##1 Sequence

Given the sequence a ##1 b (a 1-cycle delay instead of 2), and a/b sampled at six consecutive edges as (1,0), (0,1), (1,1), (0,0), (1,1), (1,0), identify every attempt that starts, and report each one's outcome.

Hint

With a 1-cycle delay, each attempt resolves on the very next edge after it starts — there's never more than one unresolved attempt in flight at a time.

Solution
  • Edge 1 (a=1): starts. Resolves at Edge 2 (b=1) → PASS
  • Edge 2 (a=0): its own new attempt fails immediately
  • Edge 3 (a=1): starts. Resolves at Edge 4 (b=0) → FAIL
  • Edge 4 (a=0): its own new attempt fails immediately
  • Edge 5 (a=1): starts. Resolves at Edge 6 (b=0) → FAIL
  • Edge 6 (a=1): starts, but its resolution would need a 7th edge, beyond this trace — left unresolved

With only a 1-cycle delay, each attempt is fully resolved before the next one even has a chance to overlap with it — a simpler case than the article's own ##2 example, but built on exactly the same "fresh attempt every edge" principle.

Summary

In this tutorial, you learned:

  • The ## operator specifies a delay between two parts of a sequence, measured in clock cycles
  • A concurrent assertion starts a fresh attempt on every single clock edge, not just when its first condition happens to be true
  • Because each attempt takes multiple cycles to resolve, several attempts can be genuinely in flight and independently tracked at the same time
  • A log message's "started at Xns" annotation identifies exactly which attempt an outcome belongs to — which may not be the attempt that started on the same row where the outcome is reported
  • One attempt failing has no effect on any other attempt still in flight from a different starting edge
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5