Part 4 · Assertions (SVA) · Intermediate

Stability, Pulse & Glitch Checks

throughout for config stability, exact and min/max pulse widths, mutual exclusion, and interrupt-held-until-serviced rules.

Rules about shape, not sequence

This family checks the shape of signals over a window: values that must not move during an operation, pulses that must be exactly or at-least-or-at-most so wide, signal pairs that must never overlap, and levels that must persist until consumed. The star operator here is throughout: expr throughout seq requires the boolean expr to hold at every tick that seq spans — the natural phrasing for “X stays true for the whole duration of Y.”

systemverilog
// Config frozen across the whole transaction window.
// Window: from start pulse to the cycle done asserts.
property p_cfg_frozen;
  @(posedge clk) disable iff (!rst_n)
    $rose(start) |->
      $stable(cfg) throughout (##1 (!done) [*0:$] ##1 done);
endproperty
a_cfg_frozen: assert property (p_cfg_frozen);

// Equivalent cycle-local phrasing — often easier to debug, because
// it fails AT the offending cycle instead of at window end:
a_cfg_frozen_local: assert property (@(posedge clk) disable iff (!rst_n)
  busy |-> $stable(cfg));
diagram
throughout window: cfg must hold from start to done

  tick  :   1     2     3     4     5     6
  start :   0     1     0     0     0     0
  busy  :   0     1     1     1     1     0
  done  :   0     0     0     0     1     0
  cfg   :   C0    C0    C0    C1    C1    C1
                              ^
  attempt @2 ($rose(start)): $stable(cfg) must hold every tick the
  window spans (3..5). Tick 4: cfg C0C1, $stable false  FAIL.
  The cycle-local form (busy |-> $stable(cfg)) also fails @4 — and
  pinpoints the tick directly, which is why many teams prefer it
  whenever a level like busy delimits the window.

Pulse-width discipline

Three distinct requirements hide under “pulse width,” and each gets its own idiom. Exactly one cycle: a rise is followed immediately by a fall. Minimum width: after a rise, the signal holds for at least N cycles. Maximum width: after a rise, a fall arrives within N cycles. Note these are clocked-sample rules — sub-cycle glitches between ticks are invisible to all of them (sampled-values edge-cases lesson); what you are policing is cycle-granular pulse discipline.

systemverilog
// Exactly one cycle wide (e.g. a kick/strobe signal)
a_pulse_1cyc: assert property (@(posedge clk) disable iff (!rst_n)
  $rose(kick) |-> ##1 !kick)
  else $error("kick wider than 1 cycle");

// Minimum width: hold at least MIN cycles after rising
a_pulse_min: assert property (@(posedge clk) disable iff (!rst_n)
  $rose(req_pulse) |-> req_pulse [*MIN])
  else $error("pulse narrower than MIN");

// Maximum width: fall within MAX cycles of rising
a_pulse_max: assert property (@(posedge clk) disable iff (!rst_n)
  $rose(req_pulse) |-> ##[1:MAX] !req_pulse)
  else $error("pulse wider than MAX");

// Min AND max together: width in [MIN:MAX]
a_pulse_band: assert property (@(posedge clk) disable iff (!rst_n)
  $rose(req_pulse) |-> (req_pulse [*MIN] ##[0:MAX-MIN] !req_pulse));
diagram
Pulse-width band [MIN=2 : MAX=4] — three candidate pulses

  tick    :   1    2    3    4    5    6    7    8    9
  pulse A :   0    1    0    0    0    0    0    0    0   width 1
  pulse B :   0    0    0    1    1    1    0    0    0   width 3
  pulse C :   0    0    0    0    0    0    1    1    1   width ≥3...

  A: $rose@2  need pulse[*2] from tick 2  tick 3 sampled 0  FAIL
  B: $rose@4  pulse holds 4,5 (=[*2]) then ##[0:2] !pulse:
     tick 6 sampled 0  PASS (width 3 inside band)
  C: if it holds through tick 11, the [*2] ##[0:2] window expires
     with no 0 observed  FAIL at tick 10 (wider than MAX=4)

Mutual exclusion and interrupt persistence

systemverilog
// Mutually exclusive controls: never both high in the same cycle
a_mutex: assert property (@(posedge clk) disable iff (!rst_n)
  !(wr_en && rd_en))
  else $error("simultaneous wr_en/rd_en");

// N-way version: at most one of a control bundle (idle allowed)
a_mutex_n: assert property (@(posedge clk) disable iff (!rst_n)
  $onehot0({mode_a, mode_b, mode_c, mode_d}));

// Interrupt held until serviced: once irq rises it must stay
// asserted until the service strobe; only then may it drop.
a_irq_held: assert property (@(posedge clk) disable iff (!rst_n)
  irq && !service |-> ##1 irq)
  else $error("irq dropped before service");

// And irq must actually clear promptly once serviced
a_irq_clears: assert property (@(posedge clk) disable iff (!rst_n)
  irq && service |-> ##[1:2] !irq)
  else $error("irq stuck after service");
diagram
Interrupt persistence: drop-before-service is the bug

  tick    :   1    2    3    4    5    6
  irq     :   0    1    1    0    0    0     ← drops at 4
  service :   0    0    0    0    1    0        but service is at 5

  a_irq_held attempt @3: irq&&!service  need irq@4  sampled 0
   FAIL@4: the CPU will read the pending-register and find nothing.
  This "wiggling interrupt" bug is invisible to directed tests that
  always service quickly — the assertion catches it on any stimulus.

  Healthy trace: irq 1 1 1 1 0 / service 0 0 0 1 0  held to service,
  cleared within 2  both assertions pass.

Note the persistence rule is the same hold-until-consumed skeleton as valid/ready's valid && !ready |-> ##1 valid and req/ack's hold rule — one structural pattern, three protocols. Recognizing these skeletons is what lets you improvise correct assertions for a protocol you have never seen.


Interview angle

Two prompts dominate. “Assert that config does not change during a transaction” — give the cycle-local busy |-> $stable(cfg) if a level delimits the window, and the throughout form when only start/end events exist; mention that the local form pinpoints the offending cycle while throughout reports at window end. “Assert a one-cycle pulse” — $rose(p) |-> ##1 !p, then expect the escalation to min/max bands and produce the [*MIN] ##[0:MAX-MIN] composition. The differentiating remark: clocked pulse checks are blind to sub-cycle glitches — say it before they ask. For mutual exclusion of N signals, reach for $onehot0 on a concatenation rather than N² pairwise rules.

Key takeaways

  • throughout pins a condition across a sequence's whole span — the idiom for protected windows.

  • Prefer cycle-local stability (busy |-> $stable) when a level delimits the window — better failure locality.

  • Pulse discipline: ##1 !p for exact-one; [*MIN] for floors; ##[1:MAX] !p for ceilings; compose for bands.

  • $onehot0 on a concatenation does N-way mutual exclusion in one assertion.

  • Hold-until-consumed is one skeleton shared by irq/valid/req rules — learn patterns, not instances.

Common pitfalls

  • throughout failures report at window end — harder to localize than per-cycle stability checks.

  • Asserting $stable(cfg) on the same tick as $rose(start) — compares against the pre-start value.

  • Believing a pulse-width assertion catches glitches between clock edges — sampling makes them invisible.

  • N(N-1)/2 pairwise mutex assertions when $onehot0 of the concat does it in one.

  • Interrupt rules without the clears-after-service half — a stuck-high irq passes the persistence rule forever.