Skip to content

PCIe · Module 15

ACK Packets — Carrying a Frontier Across a Link

Chapter 14.2 said what an ACK means. This says how it travels: a 12-bit sequence field split across two bytes, a receiver that keeps one pending frontier rather than a queue of them, and a transmitter that must not lose a decoded event.

Chapter 14.2 explained what an acknowledgement means: a cumulative frontier that releases a prefix of retained packets and nothing beyond it. It built the retirement window and deliberately never said how the frontier crosses the Link.

Chapter 15.1 named the packet and declined its format.

This chapter is the join.

What does an ACK DLLP actually contain, how does its carried reliability identity map into the retirement machinery from 14.2, and when is that control packet generated and consumed on one Link?

1. The Verified Packet

2. From Packet to Frontier

Three boundaries, and each is a different block.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ACK DLLP on the wire
   → integrity verdict                    ← lower receive machinery
   → Type classification                  ← Chapter 15.1's dispatcher
   → AckNak_Seq_Num extraction            ← §7's decoder
   → pending cumulative frontier          ← §7's coalescer
   → retirement frontier advance          ← Chapter 14.2's window

The chapter's contribution is the middle three, and the discipline is Chapter 11.7 §8's: raw bits are decoded exactly once, into a normalized event, and nothing downstream sees the packet again.

Which matters here more than usual, because the sequence field is split (§1). A second block that re-extracted it would have a second chance to assume it is contiguous.

3. The Field Is Split

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
AckNak_Seq_Num[11:0]
 
   Byte 2                     Byte 3
   ┌───┬───┬───┬───┬───────┐  ┌───────────────┐
   │ reserved  │ [11:8]    │  │    [7:0]      │
   └───┴───┴───┴───┴───────┘  └───────────────┘
        high nibble               low byte

4. The Frontier Is Cumulative — So One Pending Value, Not a Queue

The generation side, and this is the chapter's most transferable RTL idea.

A receiver accepts TLPs 10, 11 and 12 before it emits any acknowledgement. How many ACK DLLPs does it owe?

One — naming 12. The acknowledgement is cumulative (Chapter 14.2 §5), so an ACK naming 12 covers 10 and 11 as well. Three separate acknowledgements would carry strictly less information than one and cost three times the return bandwidth.

5. Timing — What This Chapter Will Not Publish

The registry says "format and timing", and the format is §1. The timing needs care.

What is safe to say architecturally. A receiver need not acknowledge every TLP immediately; because the frontier is cumulative, deferring an acknowledgement lets one packet cover more progress, which is a real bandwidth saving on the return path. Deferring it too long stalls the transmitter, because retained storage is not released (Chapter 14.4 §11) and the transmit throttle eventually bites (Chapter 14.5 §1).

So there is a genuine tension — fewer control packets against earlier retirement — and PCIe resolves it with defined scheduling requirements.

6. A Trace

Internal teaching signals, not PCIe wire signals.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
step             1    2    3    4    5    6    7    8
rx_accept        1    1    1    0    0    1    0    0
rx_seq          10   11   12    -    -   13    -    -
 
pending_valid    1    1    1    1    1    1    1    0
pending_seq     10   11   12   12   12   13   13    -
 
emit_trigger     0    0    0    0    1    0    1    0
ack_valid        0    0    0    0    1    1    1    1
ack_ready        0    0    0    0    0    0    1    1
ack_seq          -    -    -    -   12   12   13   13

Read steps 1–3. Three TLPs accepted, one pending value — overwritten twice. No acknowledgement has been emitted and none is owed separately (§4).

Read step 5. The scheduling policy fires. A descriptor is offered naming 12, covering all three.

Read step 6 — the case worth staring at. ack_ready is still low, and TLP 13 is accepted. The pending value advances to 13; the offered descriptor still names 12. Both are correct: 12 is a true statement about progress and remains safe to send.

Read step 7. The transport takes it — and note ack_seq now reads 13, because the offer was refreshed to the newest pending value before the handshake completed. A design that instead emitted 12 and then queued a second descriptor for 13 would send two packets where one suffices.

And note what never happens: pending_seq never decreases. That is P6.

7. RTL — ACK Decode and Frontier Coalescing

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Extract an acknowledgement event from a received DLLP.
// The 12-bit AckNak_Seq_Num and its split position: verified layout
// (section 1). The Type value is a PARAMETER because this chapter does not
// publish the encoding — a design supplies it from the specification.
// The event interface and error outputs: ILLUSTRATIVE.
module ack_dllp_decode #(
  parameter int SEQ_W = 12,
  // Supplied by the integrator from the Base Specification. NOT published
  // here (section 1).
  parameter logic [7:0] ACK_TYPE = 8'h00
) (
  // ---- Received DLLP ----------------------------------------------------
  // The 4-byte DLLP core. Framing and CRC checking happen upstream.
  input  logic              dllp_valid,
  input  logic [7:0]        dllp_byte0,   // Type
  input  logic [7:0]        dllp_byte1,
  input  logic [7:0]        dllp_byte2,   // [3:0] = AckNak_Seq_Num[11:8]
  input  logic [7:0]        dllp_byte3,   // [7:0] = AckNak_Seq_Num[7:0]
  // ABSTRACT integrity verdict from lower receive machinery (section 1).
  input  logic              dllp_integrity_ok,
 
  // ---- Normalized ACK event out, to the coalescer below ----------------
  // NOT a decoupled interface. This block holds nothing; ownership begins
  // one stage later, and two holding stages in series would be one more
  // place for progress to be dropped.
  output logic              ack_hit,
  output logic [SEQ_W-1:0]  ack_seq,
 
  output logic              decode_error       // integrity failed
);
 
  generate
    if (SEQ_W != 12)
      $error("This decoder implements the 12-bit AckNak_Seq_Num of section 1");
  endgenerate
 
  // ---- FIELD REASSEMBLY -------------------------------------------------
  // The field is SPLIT (section 3). Writing the concatenation explicitly,
  // high nibble first, is what makes the layout auditable — a design that
  // sliced {dllp_byte2, dllp_byte3}[11:0] would take byte 2's HIGH nibble
  // into bits [11:8] and be wrong by construction.
  wire [SEQ_W-1:0] seq_field = {dllp_byte2[3:0], dllp_byte3[7:0]};
 
  // INTEGRITY FIRST. A DLLP that failed its check has an unreliable Type
  // and an unreliable sequence field; classifying it would be asking a
  // question about data already known to be bad (Chapter 14.1 section 9).
  wire usable = dllp_valid && dllp_integrity_ok;
 
  assign ack_hit      = usable && (dllp_byte0 == ACK_TYPE);
  assign ack_seq      = seq_field;
  // Reported. Note ack_hit is gated by `usable`, so a failed-integrity DLLP
  // cannot become an acknowledgement by any path.
  assign decode_error = dllp_valid && !dllp_integrity_ok;
 
endmodule

Classification: synthesizable (combinational).

Architecture. Field reassembly and a Type comparison. It holds nothing — ownership begins in the coalescer below, and two holding stages in series would be one more place for progress to be dropped.

The extraction is written as an explicit concatenation, not a slice, so the split layout is visible in the source.

Failure — three. Slicing {byte2, byte3}[11:0] puts byte 2's high nibble into the sequence value (§3). Reading only byte 3 truncates to 8 bits, which is correct until sequence 256. And classifying before checking integrity trusts a Type field from a corrupted packet.

Deliberately simplified: no framing, no CRC computation, one DLLP per cycle, the Type value parameterised.

The stage that owns the progress

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. ACK CUMULATIVE FRONTIER COALESCER.
// That acknowledgement is cumulative is NORMATIVE (section 1). That a
// cumulative frontier can therefore be COALESCED rather than queued -- and
// must never be silently dropped -- is the design consequence.
// The subsumed report is ILLUSTRATIVE.
module ack_frontier_coalescer #(
  parameter int SEQ_W  = 12,
  parameter int WINDOW = (1 << (SEQ_W - 1))
) (
  input  logic clk,
  input  logic rst_n,
 
  // ---- From ack_dllp_decode. NOT backpressurable: it came off the wire. --
  input  logic              in_valid,
  input  logic [SEQ_W-1:0]  in_seq,
 
  // ---- To Chapter 14.2's retirement window ------------------------------
  output logic              out_valid,
  input  logic              out_ready,
  output logic [SEQ_W-1:0]  out_seq,
 
  // An arrival that carried no new progress. Not an error -- it is the one
  // legitimate reason an incoming ACK leaves no trace -- but reported,
  // because a healthy Link should not produce many.
  output logic              subsumed
);
 
  // MODULAR COMPARISON, from Chapter 14.5 section 9. An unsigned test would
  // refuse a legitimate advance from 4095 to 0 and accept a stale value
  // after the wrap.
  //
  // NOTE FOR PRODUCTION: this helper is restated here so the module reads
  // standalone. A real design must share ONE verified sequence-comparison
  // definition across 14.2, 14.3, 14.5 and this block -- two copies of a
  // wrap rule is exactly how they come to disagree at the boundary.
  function automatic bit seq_advances(input logic [SEQ_W-1:0] cand,
                                      input logic [SEQ_W-1:0] cur);
    logic [SEQ_W-1:0] d;
    d = cand - cur;                    // fixed width => modular subtraction
    return (d != '0) && (d < SEQ_W'(WINDOW));
  endfunction
 
  logic             v_q;
  logic [SEQ_W-1:0] seq_q;
  logic             sub_q;
 
  assign out_valid = v_q;
  assign out_seq   = seq_q;
  assign subsumed  = sub_q;
 
  wire fire = v_q && out_ready;
  wire adv  = seq_advances(in_seq, seq_q);
 
  // THE THREE CASES, and they are mutually exclusive by construction.
  //
  // CAPTURE -- nothing pending, or the pending value is being delivered
  // this very cycle AND the arrival advances beyond it. The second half is
  // the consume-and-arrive race: the old frontier leaves on the handshake
  // and the new one takes the slot in the same cycle.
  wire captures = in_valid && (!v_q || (fire && adv));
 
  // REPLACE -- occupied, stalled, and the arrival carries MORE progress.
  // Overwriting is not a loss: a cumulative frontier makes the older value
  // a strict subset of the newer one.
  wire replaces = in_valid && v_q && !fire && adv;
 
  // CLEAR -- delivered, and nothing took the slot.
  wire clears   = fire && !captures;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      v_q <= 1'b0; seq_q <= '0; sub_q <= 1'b0;
    end else begin
      if (captures) begin
        v_q   <= 1'b1;
        seq_q <= in_seq;
      end else if (replaces) begin
        seq_q <= in_seq;               // v_q stays set
      end else if (clears) begin
        v_q <= 1'b0;
      end
 
      // Anything that neither captured nor replaced was SUBSUMED -- either
      // by the frontier still pending, or by the one just delivered.
      if (in_valid && !captures && !replaces) sub_q <= 1'b1;
    end
  end
 
endmodule

Classification: synthesizable.

Architecture. One valid bit, one sequence register, and a modular comparison. No depth, no pointers, no occupancy — there is nothing to accumulate, because newer progress subsumes older.

Cycle behaviour — the full case table, and every row is a test in §10.

v_qout_readyarrivalResult
0anycaptured — becomes the pending frontier
10advancesreplaces — pending moves forward, still one event
10stale/duplicatesubsumed — pending unchanged, reported
11advancesold delivered and new captured in the same cycle
11stale/duplicateold delivered, slot cleared, arrival subsumed
11nonedelivered, slot cleared

Contract. in_valid is asserted only for an ACK that passed integrity (the decoder gates it). Downstream takes the frontier on a handshake and may stall indefinitely. The guarantee offered upward is the one in the callout: retained or subsumed, never lost.

Failure — five. A one-entry register with can_take drops an arrival while stalled — the defect this block exists to remove. A queue preserves every arrival and emits redundant events (§4). An unsigned comparison in seq_advances freezes the frontier at the wrap (Chapter 14.5 §6). Omitting the fire && adv term in captures loses the arrival that races a consume. And letting a stale arrival replace a newer pending frontier hands the retirement window less progress than the receiver actually reported.

Deliberately simplified: the wrap helper is restated locally rather than shared (see the comment); one arrival per cycle; no interaction with the NAK path (Chapter 15.4).

8. RTL — Pending-ACK Generator

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Receiver-side acknowledgement generation: hold the newest
// accepted progress and offer a descriptor when the scheduling policy fires.
// That the frontier is cumulative — and therefore that newer progress
// OVERWRITES older pending progress — follows from section 1's semantics.
// The emit trigger is an INPUT: the specification's acknowledgement
// scheduling thresholds and timers are NOT modelled here (section 5).
module ack_generator #(
  parameter int SEQ_W  = 12,
  parameter int WINDOW = (1 << (SEQ_W - 1))
) (
  input  logic clk,
  input  logic rst_n,
 
  // ---- Accepted receive progress (Chapter 14.5's tracker) --------------
  // Pulses when a TLP is accepted IN SEQUENCE and delivered upward. A
  // duplicate or a gap must NOT pulse this — acknowledging progress the
  // receiver has not made is the failure P7 forbids.
  input  logic              rx_accept,
  input  logic [SEQ_W-1:0]  rx_seq,
 
  // ---- Scheduling trigger ----------------------------------------------
  // ABSTRACT. The policy that decides WHEN to acknowledge is not modelled
  // (section 5). Everything else about the pending state is.
  input  logic              emit_trigger,
 
  // ---- Descriptor out, to the DLLP transmit path -----------------------
  output logic              ack_req_valid,
  input  logic              ack_req_ready,
  output logic [SEQ_W-1:0]  ack_req_seq,
 
  output logic              pending_valid,
  output logic [SEQ_W-1:0]  pending_seq
);
 
  logic             pend_v_q;
  logic [SEQ_W-1:0] pend_q;
  logic             req_v_q;
  logic [SEQ_W-1:0] req_q;
 
  assign pending_valid = pend_v_q;
  assign pending_seq   = pend_q;
 
  // MONOTONIC WITHIN THE WINDOW. Progress may only move forward. Compared
  // modularly (Chapter 14.5 section 9) — an unsigned test would refuse a
  // legitimate advance across the wrap and accept a stale value after it.
  wire [SEQ_W-1:0] fwd_delta = rx_seq - pend_q;
  wire advances = !pend_v_q
               || ((fwd_delta != '0) && (fwd_delta < SEQ_W'(WINDOW)));
 
  wire req_fire = req_v_q && ack_req_ready;
 
  assign ack_req_valid = req_v_q;
  assign ack_req_seq   = req_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      pend_v_q <= 1'b0; pend_q <= '0;
      req_v_q  <= 1'b0; req_q  <= '0;
    end else begin
      // ---- Pending progress: OVERWRITE with newer (section 4) -----------
      if (rx_accept && advances) begin
        pend_v_q <= 1'b1;
        pend_q   <= rx_seq;
      end
 
      // ---- Descriptor offer ---------------------------------------------
      // Two things happen here, and the order is the design decision.
      //
      // A pending offer is REFRESHED to the newest progress while it waits.
      // That is safe because the frontier is cumulative — naming MORE
      // progress is strictly better — and it avoids emitting two packets
      // where one suffices (section 6, step 7).
      if (req_v_q && !ack_req_ready && pend_v_q
                  && (req_q != pend_q)) begin
        req_q <= pend_q;
      end
 
      if (emit_trigger && pend_v_q && !req_v_q) begin
        req_v_q <= 1'b1;
        req_q   <= pend_q;
      end else if (req_fire) begin
        req_v_q <= 1'b0;
      end
    end
  end
 
endmodule

Classification: synthesizable.

Architecture. One pending value — not a queue (§4) — plus one descriptor register.

The refresh-while-offered behaviour is the interesting part. A descriptor waiting for the transmit path is updated to the newest pending progress. This is legitimate precisely because the frontier is cumulative: replacing "12" with "13" before the packet leaves means the same packet conveys more, and the transmitter retires more.

Cycle behaviour.

SituationResult
rx_accept with forward progresspending overwritten
rx_accept with non-forward progressignored — pending never moves backward
emit_trigger, nothing offereddescriptor offered at the pending value
offered, ack_req_ready low, newer progressdescriptor refreshed to the newer value
offered and takendescriptor cleared

Contract. rx_accept pulses only for a TLP accepted in sequence and delivered upward (Chapter 14.5 §10) — never on a duplicate and never on a gap. That obligation is on the caller and P7 asserts its consequence.

Failure — four. A FIFO of pending acknowledgements emits redundant packets (§4). An unsigned comparison in advances refuses a legitimate advance across the wrap (Chapter 14.5 §6). Letting pending move backward emits an acknowledgement naming less progress than the receiver has made, so the transmitter under-retires. And pulsing rx_accept on a gap acknowledges packets that never arrived — which tells the transmitter to discard them.

Deliberately simplified: the scheduling policy is an input (§5); no interaction with the NAK path (Chapter 15.4); no DLLP framing or CRC generation.

9. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SVA over ack_dllp_decode and ack_generator. These assert the verified
// field layout of section 1 and the LOCAL generation/decode contracts. They
// assert nothing about acknowledgement scheduling timing (not modelled,
// section 5), nothing about the retirement algorithm (Chapter 14.2), and
// nothing about credit flow (Chapter 15.2).
 
// ---- ENVIRONMENT -----------------------------------------------------
// A1: rx_accept pulses only for in-sequence accepted progress — never on a
// duplicate or a gap (Chapter 14.5 section 10). The caller's obligation.
assume property (@(posedge clk) disable iff (!rst_n)
  rx_accept |-> !$isunknown(rx_seq));
// A2: the DLLP core bytes are stable while dllp_valid is asserted.
assume property (@(posedge clk) disable iff (!rst_n)
  dllp_valid |-> !$isunknown({dllp_byte0, dllp_byte2, dllp_byte3}));
 
// ---- DECODE ----------------------------------------------------------
 
// P1: only an ACK-Type DLLP produces an acknowledgement event.
property p_only_ack_type_decodes;
  @(posedge clk) disable iff (!rst_n)
  (dllp_valid && (dllp_byte0 != ACK_TYPE)) |-> !ack_hit;
endproperty
a_type : assert property (p_only_ack_type_decodes);
 
// P2: THE FIELD PROPERTY. The extracted identity equals an INDEPENDENT
// reassembly of the split field. Catches every slicing mistake in section 3.
property p_field_extraction_exact;
  @(posedge clk) disable iff (!rst_n)
  ack_hit |-> (ack_seq == {dllp_byte2[3:0], dllp_byte3});
endproperty
a_field : assert property (p_field_extraction_exact);
 
// P3: a DLLP that failed its integrity check NEVER becomes an event.
property p_corrupt_not_decoded;
  @(posedge clk) disable iff (!rst_n)
  (dllp_valid && !dllp_integrity_ok) |-> !ack_hit;
endproperty
a_corrupt : assert property (p_corrupt_not_decoded);
 
// ---- COALESCER: THE OWNERSHIP CONTRACT -------------------------------
 
// P4: THE CENTRAL PROPERTY. Every integrity-valid arrival is RETAINED or
// SUBSUMED -- never silently lost. "Retained" means it became the pending
// frontier; "subsumed" means an equal-or-newer frontier is pending or was
// delivered this cycle. A one-entry register with can_take FAILS this.
property p_retained_or_subsumed;
  @(posedge clk) disable iff (!rst_n)
  in_valid |-> (captures || replaces || subsumed_now);
endproperty
a_no_loss : assert property (p_retained_or_subsumed);
 
// P4b: and "subsumed" is a CLAIM, not a label. Whenever an arrival leaves
// no trace, the frontier it was measured against really was equal-or-newer.
// Written as an independent modular test, so a broken seq_advances cannot
// satisfy it by agreeing with itself.
property p_subsumed_is_justified;
  @(posedge clk) disable iff (!rst_n)
  (in_valid && !captures && !replaces)
    |-> (v_q && !(((in_seq - seq_q) != '0)
               && ((in_seq - seq_q) < SEQ_W'(WINDOW))));
endproperty
a_subsumed : assert property (p_subsumed_is_justified);
 
// P4c: THE PENDING FRONTIER NEVER MOVES BACKWARD. A stale arrival cannot
// replace a newer pending value.
property p_frontier_monotonic;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && $past(out_valid) && !$past(fire) && !$stable(out_seq))
    |-> (((out_seq - $past(out_seq)) != '0)
      && ((out_seq - $past(out_seq)) < SEQ_W'(WINDOW)));
endproperty
a_monotonic_frontier : assert property (p_frontier_monotonic);
 
// P4d: a pending frontier is STABLE while stalled unless REPLACED by a
// verified newer arrival. Catches spontaneous mutation.
property p_pending_stable_unless_replaced;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && !out_ready && !replaces) |=> (out_valid && $stable(out_seq));
endproperty
a_stable : assert property (p_pending_stable_unless_replaced);
 
// P4e: THE RACE. Consume and arrive in the same cycle, with the arrival
// advancing: the old frontier is delivered AND the new one is captured.
property p_consume_and_capture;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && out_ready && in_valid && adv)
    |=> (out_valid && (out_seq == $past(in_seq)));
endproperty
a_race : assert property (p_consume_and_capture);
 
// P4f: NOT A QUEUE. Three advancing arrivals while stalled produce ONE
// delivered event naming the newest -- not three.
property p_coalesces_not_queues;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && out_ready) |-> (out_seq == newest_arrived_seq);
endproperty
a_coalesce : assert property (p_coalesces_not_queues);
 
// P5: THE LAYER PROPERTY. A decoded acknowledgement never produces a
// Transaction Layer event (Chapter 15.1 section 6).
property p_not_a_completion;
  @(posedge clk) disable iff (!rst_n)
  (ack_hit || out_valid) |-> !(tl_cpl_valid || tl_mem_valid);
endproperty
a_layer : assert property (p_not_a_completion);
 
// ---- GENERATION ------------------------------------------------------
 
// P6: PENDING PROGRESS IS MONOTONIC in the sequence window. It never names
// less progress than the receiver has made. Modular, not unsigned.
property p_pending_monotonic;
  @(posedge clk) disable iff (!rst_n)
  (pending_valid && !$stable(pending_seq))
    |-> (((pending_seq - $past(pending_seq)) != '0)
      && ((pending_seq - $past(pending_seq)) < SEQ_W'(WINDOW)));
endproperty
a_monotonic : assert property (p_pending_monotonic);
 
// P7: pending progress reflects ACCEPTED progress only. Bound against the
// receive tracker so an acknowledgement can never name a gap.
property p_pending_tracks_accepted;
  @(posedge clk) disable iff (!rst_n)
  (pending_valid && !$stable(pending_seq)) |-> $past(rx_accept);
endproperty
a_accepted_only : assert property (p_pending_tracks_accepted);
 
// P8: ONE PENDING VALUE, NOT A QUEUE (section 4). Three accepts before an
// emit produce ONE descriptor. (req_count is a testbench counter.)
property p_one_descriptor_per_trigger;
  @(posedge clk) disable iff (!rst_n)
  (ack_req_valid && ack_req_ready) |-> (req_count + 1 <= trigger_count);
endproperty
a_one_desc : assert property (p_one_descriptor_per_trigger);
 
// P9: an offered descriptor never names LESS progress than when offered.
// The refresh may only increase it (section 8).
property p_offer_never_regresses;
  @(posedge clk) disable iff (!rst_n)
  (ack_req_valid && !ack_req_ready && !$stable(ack_req_seq))
    |-> (((ack_req_seq - $past(ack_req_seq)) != '0)
      && ((ack_req_seq - $past(ack_req_seq)) < SEQ_W'(WINDOW)));
endproperty
a_offer_forward : assert property (p_offer_never_regresses);
 
// P10: an emitted descriptor never names progress beyond what is pending.
property p_offer_within_pending;
  @(posedge clk) disable iff (!rst_n)
  (ack_req_valid && pending_valid)
    |-> ((pending_seq - ack_req_seq) < SEQ_W'(WINDOW));
endproperty
a_within_pending : assert property (p_offer_within_pending);
 
// P11: reset clears every stage that owns progress.
property p_reset_clears;
  @(posedge clk)
  !rst_n |=> (!out_valid && !ack_req_valid && !pending_valid && !subsumed);
endproperty
a_reset : assert property (p_reset_clears);

P2 is the chapter's field property and it is deliberately written as an independent reassembly. A property that compared ack_seq against the module's own seq_field would verify only that the wire equals itself — it would agree with a wrong slice. Restating the concatenation in the property means a slicing mistake produces a mismatch.

P4 and P4b are the pair that makes the ownership contract checkable. P4 says every arrival lands in one of three buckets; on its own it is satisfiable by a design that labels everything "subsumed" and drops it. P4b closes that by requiring the subsumption claim to be true — and states the modular test independently, so a broken seq_advances cannot satisfy the property by agreeing with itself. Neither half is sufficient alone, which is why the buggy one-entry register would have passed a naively-written "no loss" property.

P4f is the anti-queue property, and it is the one that fails if someone "fixes" the drop by adding a FIFO: the delivered value must be the newest arrival, not the oldest still queued.

P6 and P9 are the same monotonicity, at two stages, and both use modular comparison. An unsigned version would refuse a legitimate advance from 4095 to 0 (Chapter 14.5 §6) — the receiver would stop acknowledging at the wrap, the transmitter would stop retiring, and the Link would stall exactly once per wrap period.

P7 is bound against the receive tracker, not asserted locally. The obligation it encodes — never acknowledge a gap — is about a signal this module receives, and binding it across the boundary is the only place it is checkable.

10. Fault Injection and Verification

The scoreboard reassembles the field itself from the observed bytes, and tracks the expected frontier from the observed accept events — never from pending_seq or ack_seq.

Decode

  • A well-formed ACK at sequence 0, a mid-range value, 255, 256, 4095. 255 and 256 are the pair that catches an 8-bit truncation (§3).
  • A value with byte 2's upper nibble non-zero. Verify the extracted identity ignores it — the test that catches a 16-bit slice.
  • dllp_integrity_ok low. Verify no event and decode_error (P3).
  • A non-ACK Type. Verify no event (P1).
  • ack_ready low across several arrivals. Verify the frontier advances to the newest and nothing is lost (P4, P4c).

Coalescing — the required tests

  • Stalled, then 12, 13, 14 arrive. Verify one delivered event naming 14 (P4f) — the test the old one-entry register fails on the second arrival, and a queue fails by delivering three.
  • Stalled, then a duplicate or stale ACK arrives. Verify the pending frontier does not move and subsumed is reported (P4b, P4c).
  • Consume and arrive in the same cycle, arrival advancing. Verify the old frontier is delivered and the new one is captured (P4e).
  • Consume and arrive in the same cycle, arrival stale. Verify the slot clears and the arrival is subsumed — not captured as a regression.
  • Arrivals across the wrap — 4094, 4095, 0, 1 while stalled. Verify the frontier advances at every step; the unsigned-comparison test.
  • A long stall with no arrivals. Verify the frontier is stable (P4d).
  • Reset with a frontier pending (P11).

Generation

  • Three accepts, then one trigger. One descriptor naming the third (P8, §4).
  • A trigger with nothing pending. Verify no descriptor.
  • Newer progress while a descriptor is offered. Verify the offer is refreshed upward (P9) and only one packet results.
  • Progress across the wrap — 4094, 4095, 0, 1. Verify pending advances at every step (P6) — the unsigned-comparison test.
  • A stale or duplicate rx_seq. Verify pending does not move.
  • Reset with a descriptor offered.

Mutations

#MutationCaught by
1field sliced as {byte2, byte3}[11:0]P2, with byte 2's upper nibble set
2only byte 3 readP2, at sequence 256
3descriptor names the previous pending valueP9, and the scoreboard's frontier
4pending allowed to move backwardP6
5rx_accept pulsed on a gapP7, bound against the tracker
6one-entry register with can_take — arrival dropped while stalledP4, on the second arrival of the 12/13/14 test
6aarrivals queued in a FIFO instead of coalescedP4f — three events delivered where one was owed
6bfire && adv omitted from capturesP4e — the arrival racing a consume disappears
6cstale arrival allowed to replace a newer frontierP4c, and the window retires a shorter prefix
6deverything labelled subsumed and droppedP4b — the claim is checked, not trusted
7integrity checked after Type classificationP3, with a corrupted Type
8a queue of pending acknowledgementsP8 — three packets where one was owed
9unsigned comparison in advancesthe wrap test — pending freezes at 4095
10decoded ACK routed to the Completion engineP5

11. Debugging

The receiver accepts traffic but the transmitter's replay buffer never retires

Walk the chain in order; each step eliminates the ones before it.

  1. Is rx_accept pulsing? If not, the receive tracker is not accepting — a sequence-classification problem (Chapter 14.5), not an acknowledgement one.
  2. Is pending_seq advancing? If not, advances is refusing — check for an unsigned comparison if this only happens past a wrap.
  3. Is a descriptor being offered? If not, emit_trigger never fires — the scheduling policy (§5), which this model does not own.
  4. Is the DLLP on the wire? Between 3 and 4 lies the transmit path.
  5. Does the decoder produce an event? Check decode_error first — an integrity failure means the packet arrived damaged, not that the decode is wrong.
  6. Does the retirement window accept it? Chapter 14.2's ack_unknown answers this: set means the identity matched nothing retained.

Step 6 setting ack_unknown while step 5 produced an event is the field-extraction signature — the value decoded to something the transmitter never assigned.

The ACK looks correct on the analyzer but the wrong packets retire

The packet is right, so decode or interpretation is wrong.

Compare the analyzer's sequence value against the decoder's ack_seq, byte by byte. A mismatch is extraction (§3). A match means the fault is downstream — the retirement window's frontier computation, or Chapter 14.5's comparison.

And check whether it only happens past 255 or past 4095. Either boundary names its own bug.

Acknowledgement traffic is normal until sequence rollover

Either the field extraction or the modular comparison — and they are distinguishable.

If the receiver stops acknowledging, advances is unsigned: at pending = 4095 and rx_seq = 0, an unsigned test sees no advance and pending freezes forever.

If acknowledgements continue but retirement stops, the comparison at the transmitter is the problem (Chapter 14.5 §13), not this chapter's.

12. Common Misconceptions

  • "An ACK DLLP is a Completion." Different layer, different scope. A Completion resolves a transaction (Chapter 13.1); an ACK reports Link progress (Chapter 14.2 §2).
  • "An ACK carries a Requester Tag." It carries AckNak_Seq_Num — a Link-local sequence identity (Chapter 14.5 §2).
  • "An ACK is forwarded through a Switch." It terminates at the port that receives it (Chapter 15.1 §3).
  • "Each accepted TLP needs its own ACK packet." The frontier is cumulative — one packet covers all progress up to the value it names (§4).
  • "An ACK proves destination software observed the transaction." It proves a neighbour received a packet (Chapter 14.2 §4).
  • "The ACK sequence number is end to end." Each Link assigns and acknowledges its own (Chapter 14.5 §2).
  • "An ACK DLLP updates flow-control credits." Different category, different engine (Chapter 15.2). An ACK retires replay storage; a credit update returns receive-buffer capacity.
  • "An ACK may name progress past a missing packet." Acknowledging a gap tells the transmitter to discard packets that never arrived (P7).
  • "The ACK parser belongs in the Transaction Layer." It is Data Link machinery and never produces a Transaction Layer event (P5).
  • "Acknowledgement timing can be chosen freely." PCIe defines scheduling requirements. This chapter does not publish them (§5) — but a design that ignores them either wastes return bandwidth or stalls its partner.
  • "The sequence field is a contiguous 12-bit slice." It is split across two bytes (§1, §3).

13. Understanding Check

14. What's Next

This chapter closed the loop Chapter 14.2 opened. A frontier crosses the Link in a 12-bit field split across two bytes, generated from a single pending value that newer progress overwrites, and received into a coalescer rather than a register or a queue — because the retirement window can be busy, and the thing being carried is progress, not events.

The receive and transmit sides turned out to be the same structure. One pending cumulative value, advanced by newer arrivals, delivered when the far side is ready. That symmetry is not a coincidence — it is what "cumulative" means, applied at both ends of the wire.

Chapter 15.4 — NAK Packets takes the other reliability packet — and the opposite structure. A Nak is not cumulative, so it cannot be coalesced: losing one is unrecoverable, and the delivery path needs a genuine queue.

Chapter 15.5 then covers the last DLLP category, and Module 16 finally takes the credit system that Chapter 15.2 fed.

The idea to carry forward: a cumulative frontier travels as one value that overwrites, not as a queue of events — and the packet that carries it is one field, in two pieces.