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.
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 beginsThe canonical ruleset
Hold: req stays high from assertion until the ack arrives.
Window: ack arrives within [MIN:MAX] cycles of the request rising.
No spurious ack: ack never asserts while no request is pending.
Pulse discipline: exactly one single-cycle ack per request.
Release: req deasserts within a bound after ack.
Spacing: a new request waits at least GAP cycles after the previous one completes.
Complete assertion module
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
endmoduleNote 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
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 immediatelyThe 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.