Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • How to Use This Course
  • Reading Paths
  • Concept Map
  • How to Read the Diagrams
  • Part 0: What Is a Protocol?10
  • Part 1: Timing and Clocking11
  • Part 2: Encoding and Framing12
  • Part 3: Handshakes13
  • Part 4: Flow Control17
  • Part 5: Transactions19
  • Part 6: Concurrency and Performance17
  • Part 7: Ordering13
  • Part 8: Sharing and Arbitration12
  • Part 9: Deadlock and Liveness13
  • Part 10: Reliability and Recovery20
  • Part 11: Bring-Up and Link Management10
  • Part 12: Power8
  • Part 13: Shared State and Coherence9
  • Part 14: Security and Isolation7
  • Part 15: Specifying and Verifying11
    • Writing an Interface Contract
    • Safety and Liveness
    • Protocol Assertions
    • Monitors and Transaction Reconstruction
    • Ordering-Aware Scoreboards
    • Protocol Coverage
    • Formal Methods for Protocols
    • Compliance and VIP
    • Observability and Tracing
    • A Waveform Debug Method
    • Drills: Specifying and Verifying
  • Part 16: Protocols Dissected17
  • Part 17: Capstones5
  • Part 18: Interview Preparation6
Loading lesson

‹Part 15: Specifying and Verifying
  1. Topics
  2. First Principles of Hardware Protocols
  3. Part 15: Specifying and Verifying
  4. Protocol Assertions

Part 15 · Specifying and Verifying · Working (W)

Protocol Assertions

A small standard set of assertions catches most interface bugs: valid held until accepted, payload stable while stalled, no unknown values, counts conserved, limits respected. DCLink step 12a builds them as a reusable checker and shows it catching two planted bugs.

On this page

    Note

    Level: Working (W) · Answers: How do we know it is correct?

    Before this: Valid/Ready Rules, Safety and Liveness

    A producer occasionally changes its data while the consumer is stalling it. Most of the time the consumer is ready, so nothing goes wrong; once in a few thousand transfers the stalled word is replaced, and one packet is corrupted. The scoreboard reports a data mismatch a hundred cycles later, three blocks downstream, and the debug takes a day. An assertion on the interface itself would have stopped the simulation at the exact cycle, with the rule's number in the message.

    Which checks should every interface carry, and how are they packaged so that every testbench uses them?

    The obvious answer, and why it breaks

    The obvious answer is to rely on the end-to-end scoreboard: if data arrives intact, the interface works. The scoreboard sees the effect far from the cause, after the evidence has scrolled away. And it misses rule violations that happen not to corrupt data in this particular test: a producer that drops valid early works with a consumer that never stalls, then fails with the next consumer that does. Interface rules need checking at the interface, on every cycle, whatever the traffic.

    The idea: a standard set, in one reusable checker

    Check

    Rule

    Kind

    Hold

    Once valid, stay valid until the transfer

    Safety

    Stable

    While stalled, payload does not change

    Safety

    Known

    While valid, no X or Z on payload or control

    Safety

    Conserved counts

    Credits never negative or above the maximum; outstanding requests within the limit

    Safety

    Bounded length

    A packet or burst ends within its maximum length

    Safety

    Bounded response

    Every request answered within N cycles

    Liveness, bounded (lesson 15.2)

    Put them in one checker module with the interface's signals as inputs, parameterised by its limits, and connect an instance to every copy of the interface in every testbench (and, through bind, inside the design). The checker is written once, from the specification's numbered rules, and reused everywhere.

    Build it: DCLink step 12a

    The checker holds four of the rules and counts each one's violations. The testbench connects one checker to a correct producer and one to a faulty producer that changes its data while stalled (cycle 6) and drops valid before its transfer completes (cycle 13). A second reset arrives at cycle 25, after the first beat of a packet; both producers abandon that packet and start a new one.

    This listing uses concurrent assertions with disable iff, which Icarus Verilog 13 does not parse. Run it with Verilator (verilator --binary --timing --assert file.sv); it is the one listing in the course that needs Verilator.

    dclink_checker.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    // DCLink step 12a: a reusable protocol checker for one valid/ready hop.
    // It holds the interface's rules as assertions and counts every violation:
    //   hold:   once valid, valid stays high until the transfer (valid && ready)
    //   stable: while stalled, data and last do not change
    //   known:  while valid, data and last are never X or Z
    //   length: a packet ends (last) within MAX_BEATS beats
    // The checker's own state (the beat count) follows the protocol's reset: a packet cut
    // short by reset is abandoned, so the count restarts. The testbench connects one
    // checker to a correct producer and one to a faulty producer that changes its data
    // while stalled and later drops valid before the transfer; a second reset arrives in
    // the middle of a packet. The checker must stay silent on the first producer, also
    // across the mid-packet reset, and catch both bugs in the second.
    module dclink_checker #(parameter int MAX_BEATS = 8) (
      input logic clk, rst_n, vld, rdy, last,
      input logic [31:0] data,
      output int n_hold, n_stable, n_known, n_length
    );
      initial begin n_hold = 0; n_stable = 0; n_known = 0; n_length = 0; end
      int beats = 0;                                   // beats of the current packet so far
      always @(posedge clk)
        if (!rst_n)          beats <= 0;               // reset abandons a partial packet
        else if (vld && rdy) beats <= last ? 0 : beats + 1;
    
      a_hold:   assert property (@(posedge clk) disable iff (!rst_n) vld && !rdy |=> vld)                      else n_hold++;
      a_stable: assert property (@(posedge clk) disable iff (!rst_n) vld && !rdy |=> $stable(data) && $stable(last)) else n_stable++;
      a_known:  assert property (@(posedge clk) disable iff (!rst_n) vld |-> !$isunknown({data, last}))         else n_known++;
      a_length: assert property (@(posedge clk) disable iff (!rst_n) vld && rdy && !last |-> beats < MAX_BEATS - 1) else n_length++;
    endmodule
    
    module tb;
      logic clk = 0;
      always #5 clk = ~clk;
      int cyc = 0;
      logic rst_n = 0;
      // Reset at the start, and again for two cycles from cycle 25, in the middle of a packet
      always @(posedge clk) begin cyc <= cyc + 1; rst_n <= cyc >= 1 && !(cyc == 24 || cyc == 25); end
    
      wire rdy = (cyc % 3 == 2);                       // the receiver accepts every third cycle
    
      // A producer sending 3-beat packets: words 1, 2, 3 (last on 3), then 4, 5, 6, ...
      // When FAULTY, it changes the word while stalled in cycle 6 and drops valid in cycle 13.
      logic        g_vld = 0, f_vld = 0, g_last = 0, f_last = 0;
      logic [31:0] g_data = 0, f_data = 0;
      int          g_n = 0, f_n = 0;
      always @(posedge clk) if (!rst_n) begin
        g_vld <= 0; f_vld <= 0; g_n <= 0; f_n <= 0;    // reset: both producers start a new packet
      end else begin
        if (!g_vld || rdy) begin g_vld <= 1; g_data <= g_n + 1; g_last <= (g_n % 3 == 2); g_n <= g_n + 1; end
        if (!f_vld || rdy) begin f_vld <= 1; f_data <= f_n + 1; f_last <= (f_n % 3 == 2); f_n <= f_n + 1; end
        else if (cyc == 6)  f_data <= 32'hdead;        // bug 1: data changed while stalled
        else if (cyc == 13) f_vld  <= 0;               // bug 2: valid dropped before the transfer
      end
    
      int g_h, g_s, g_k, g_l, f_h, f_s, f_k, f_l;
      dclink_checker #(.MAX_BEATS(3)) good  (.clk, .rst_n, .vld(g_vld), .rdy, .last(g_last), .data(g_data), .n_hold(g_h), .n_stable(g_s), .n_known(g_k), .n_length(g_l));
      dclink_checker #(.MAX_BEATS(3)) fault (.clk, .rst_n, .vld(f_vld), .rdy, .last(f_last), .data(f_data), .n_hold(f_h), .n_stable(f_s), .n_known(f_k), .n_length(f_l));
    
      initial begin
        #450;
        if (g_h + g_s + g_k + g_l == 0 && f_h == 1 && f_s >= 1 && f_k + f_l == 0)
          $display("PASS: checker silent on the correct producer, also across a mid-packet reset; caught the faulty one changing data while stalled and dropping valid");
        else $display("FAIL: good %0d/%0d/%0d/%0d faulty %0d/%0d/%0d/%0d", g_h, g_s, g_k, g_l, f_h, f_s, f_k, f_l);
        $finish;
      end
    endmodule

    When run, it prints a PASS line: the checker on the correct producer stays silent, and the checker on the faulty producer records one hold violation and the stable violation, at the cycles where the bugs happened. In a real testbench, each failure would also print the rule number and stop the test.

    Look at the beat counter: it is cleared by rst_n as well as by last. disable iff only switches the assertions off during reset; it does not touch the checker's own state. Without the reset term, the beat already counted before the second reset carries into the next packet, and the correct producer's next 3-beat packet is reported as too long: a false failure that makes engineers distrust the checker. A checker's state must follow the protocol's reset rules exactly as the design's does.

    Making assertions useful in practice

    • Disable during reset, and reset the checker's state: disable iff (!rst_n) stops reset values triggering false failures, but any counters or flags inside the checker must also be cleared on reset, or a packet cut short by reset contaminates the next one.

    • Name each assertion after its rule: a failure message that says "R3" points straight at the specification.

    • Check both directions: the same checker verifies the producer's rules (in the producer's testbench) and lets the consumer's testbench assume them.

    • Add cover properties: assertions that never had a chance to fail prove nothing; cover the stalls and corner cases they check (lesson 15.6).

    How real protocols chose

    Protocol

    Assertions

    AMBA AXI

    Arm publishes protocol assertions for AXI4, AXI4-Lite and AXI4-Stream (fact A29), built on rules A1, A2 and A21

    Any standard interface

    Verification IP includes protocol checkers (lesson 15.8)

    DCLink

    Step 12a, this checker

    What breaks

    The opening corruption (illustrative) was found within minutes once the stable assertion was bound into the design at that interface: the failure pointed at the producer and the cycle. The same checker, bound at the other six instances of the interface in the chip, found a second producer with the same bug that had not yet corrupted anything in testing.

    Why this matters

    Senior lens. Deliver the checker with the specification and bind it at every instance of the interface, in block, subsystem and chip testbenches and in formal. Pair every assertion with a cover property so you know it was exercised. The cost is an afternoon; the payoff is that interface bugs fail at their cause, on the cycle, with the rule's name.

    Interview

    Asked: "Write an assertion that data is stable while valid is high and ready is low."

    Weak answer: assert (valid && !ready) -> data == data;

    Strong answer: assert property (@(posedge clk) disable iff (!rst_n) valid && !ready |=> $stable(data)); The |=> checks the next cycle: if this cycle is a stall, the data in the next cycle must equal this cycle's. I'd add the companion rule that valid stays high, valid && !ready |=> valid, and a no-X check, valid |-> !$isunknown(data), put them in a reusable checker named after the spec's rules, and pair them with cover properties for stalls so I know they were exercised.

    Follow-ups: (1) Why |=> and not |->? (2) How do you know the assertion was ever exercised?

    Check yourself Why is the hold rule checked with |=> rather than |->?

    It is a promise about the next cycle: if this cycle is a stall, valid must still be high in the following one.

    Check yourself A testbench never stalls the producer. What do the hold and stable assertions prove?

    Nothing: their trigger (valid and not ready) never occurs, which is why each needs a cover property.

    Key takeaways

    • A standard set of interface assertions (hold, stable, known, conserved counts, bounded length and response) catches most interface bugs at their cause.

    • Package them as one reusable checker, named after the specification's rules, and bind it at every instance.

    • Disable during reset and pair every assertion with a cover property.

    Keep going

    Next: Lesson 15.4, Monitors and Transaction Reconstruction. Watching the wires and rebuilding whole transactions from them.

    Related topics

    • Part 15 overview: Specifying and Verifying

    • CDC and RDC course: Assertions for Protocols

    Previous

    198 / 234

    Next: Monitors and Transaction Reconstruction

    Up next

    Up nextMonitors and Transaction Reconstruction

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Writing an Interface Contract
    • Ordering-Aware Scoreboards
    • Protocol Coverage