Part 4 · Assertions (SVA) · Intermediate

Request/Acknowledge Protocols

Hold-until-ack, bounded ack windows, no spurious acks, one-ack-per-request pulse discipline, and request spacing — full module with waveforms.

The contract

Req/ack is the older, four-phase-flavored cousin of valid/ready: the requester raises req and holds it; the responder answers with an ack pulse within a specified window; req then drops, and a new request may only begin after some spacing. Unlike valid/ready — where both sides are symmetric levels — req/ack rulesets must police pulse discipline : acks are events, not levels, so exactly-one-ack-per-request and no-ack-without-request become first-class rules.

diagram
Well-formed req/ack transaction, ack window [2:4]

  tick :   1    2    3    4    5    6    7    8    9
  req  :   0    1    1    1    1    0    0    1    1
  ack  :   0    0    0    1    0    0    0    0    0
              ^         ^         ^         ^
              │         │         │         └ next request (spacing ≥2 ok)
              │         │         └ req released after ack
              │         └ ack pulse, 3 ticks after rise — inside [2:4]
              └ $rose(req): transaction starts, hold obligation begins

The canonical ruleset

  1. Hold: req stays high from assertion until the ack arrives.

  2. Window: ack arrives within [MIN:MAX] cycles of the request rising.

  3. No spurious ack: ack never asserts while no request is pending.

  4. Pulse discipline: exactly one single-cycle ack per request.

  5. Release: req deasserts within a bound after ack.

  6. Spacing: a new request waits at least GAP cycles after the previous one completes.


Complete assertion module

systemverilog
module req_ack_checker #(
  parameter int ACK_MIN = 1,
  parameter int ACK_MAX = 8,
  parameter int GAP     = 2
)(
  input logic clk, rst_n,
  input logic req,
  input logic ack
);
  // R1: req held until ack — req must not drop while ack still pending
  a_req_hold: assert property (@(posedge clk) disable iff (!rst_n)
    (req && !ack) |-> ##1 req)
    else $error("req dropped before ack");

  // R2: ack arrives inside the window after the request rises
  a_ack_window: assert property (@(posedge clk) disable iff (!rst_n)
    $rose(req) |-> ##[ACK_MIN:ACK_MAX] ack)
    else $error("ack missed [%0d:%0d] window", ACK_MIN, ACK_MAX);

  // R3: no ack without a pending request
  a_no_spurious_ack: assert property (@(posedge clk) disable iff (!rst_n)
    ack |-> req)
    else $error("ack with no pending req");

  // R4: ack is a single-cycle pulse (one ack per req)
  a_ack_pulse: assert property (@(posedge clk) disable iff (!rst_n)
    ack |-> ##1 !ack)
    else $error("ack wider than one cycle");

  // R5: req releases promptly after ack (here: the very next cycle)
  a_req_release: assert property (@(posedge clk) disable iff (!rst_n)
    (req && ack) |-> ##1 !req)
    else $error("req not released after ack");

  // R6: spacing — after release, quiet for at least GAP cycles
  a_req_spacing: assert property (@(posedge clk) disable iff (!rst_n)
    $fell(req) |-> !req [*GAP])
    else $error("re-request inside %0d-cycle gap", GAP);

  c_min_lat: cover property (@(posedge clk)
    $rose(req) ##ACK_MIN ack);              // fastest legal ack
  c_max_lat: cover property (@(posedge clk)
    $rose(req) ##ACK_MAX ack);              // slowest legal ack
endmodule

Note how R1 and R4 together encode “one ack per request” without any auxiliary state: R4 limits each ack to one cycle, R5 forces req down right after it, and R3 forbids an ack while no request pends — so a second ack for the same request has no legal cycle to occupy. Building multi-rule invariants out of small single-cycle assertions like this is far more debuggable than one monolithic sequence.


Violation traces, cycle by cycle

diagram
Violation 1: req dropped early (R1)

  tick :   1    2    3    4
  req  :   0    1    0    0      ← dropped at 3, no ack ever seen
  ack  :   0    0    0    0
  a_req_hold attempt @2: req&&!ack  need req@3 =1  sampled 0  FAIL@3
  a_ack_window attempt @2 keeps running — kill it with a checker
  reset or accept the cascade (one root cause, two reports).

  Violation 2: wide ack (R4) and missing release (R5)

  tick :   1    2    3    4    5
  req  :   0    1    1    1    1
  ack  :   0    0    1    1    0
  a_ack_pulse @3: ack  need !ack@4  sampled 1  FAIL@4
  a_req_release @3: req&&ack  need !req@4  sampled 1  FAIL@4

  Violation 3: spurious ack (R3)

  tick :   1    2    3
  req  :   0    0    0
  ack  :   0    1    0
  a_no_spurious_ack @2: ack=1, req=0  FAIL@2 immediately

The cascade in violation 1 is worth internalizing: one design bug (early req drop) trips R1 at tick 3 and will also trip R2 when the window expires. In triage, the earliest-failing assertion is almost always the root cause; later failures are echoes. Some teams gate secondary assertions on a sticky error flag to suppress echoes — a style choice, not a requirement.


Variations and interview angle

Common protocol variants

  • Level ack (four-phase): ack stays high until req falls — replace R4/R5 with ack |-> ##1 (ack || !req) and a fall-ordering rule ($fell(req) before $fell(ack)).

  • Zero-cycle ack allowed: combinational responders can ack in the same cycle — set ACK_MIN=0 and make R1 tolerate (req && ack) on the first cycle.

  • Tagged requests: multiple outstanding reqs need per-tag tracking — single-wire rules no longer suffice; this is where local variables or per-tag generate loops enter.

  • Credit-based: req allowed only when credits > 0 — add a side counter and assert it never underflows (see the pipeline lesson).

Interview angle

The standard prompt is “req must be acked within N cycles — write it,” and the expected core is $rose(req) |-> ##[1:N] ack. The grading happens on the follow-ups: why $rose(req) and not req (level antecedent spawns one attempt per held cycle — duplicate failures); what else you would assert (produce the other five rules — hold, spurious, pulse, release, spacing); and how the ruleset changes for a four-phase level-ack variant. If asked about multiple outstanding requests, say plainly that wire-level rules stop working and per-tag tracking (local variables) is the tool — recognizing the boundary is the point of the question.

Key takeaways

  • Req/ack rulesets police pulse discipline: window, exactly-one, no-spurious, spacing.

  • Compose invariants from small single-cycle assertions — far easier to triage than one mega-sequence.

  • Use $rose(req) for the window rule; the level form duplicates attempts every held cycle.

  • In failure cascades, the earliest assertion is the root cause; later ones are echoes.

  • Know the four-phase and multi-outstanding variants — interviewers pivot to them immediately.

Common pitfalls

  • req |-> ##[1:N] ack with a level req — N held cycles spawn N overlapping window threads.

  • Forgetting no-spurious-ack — a responder that acks randomly passes the window rule.

  • Assuming single-cycle ack when the protocol is four-phase level ack — false R4 failures.

  • No spacing rule — back-to-back glitch re-requests merge into the previous transaction unseen.

  • Applying single-wire rules to a protocol with multiple outstanding requests — needs per-tag state.