Skip to content

PCIe · Module 14

ACK — When Retained State May Finally Be Released

An ACK is not a Completion and it is not proof the operation happened. It is cumulative Link-local progress: it advances a retirement frontier, releasing everything up to a point and nothing beyond it. What a transmitter may conclude, and what it must not.

Chapter 14.1 established the contract and left one question deliberately open. A transmitter must retain a packet it has sent, because it may have to send it again — so when may it stop?

Retaining forever is not an option. Storage is finite, and a transmitter that cannot release anything eventually refuses to accept new work from the layer above.

What does a Data Link ACK allow the transmitting port to conclude, which retained state may it release, and what does ACK explicitly not prove about the PCIe transaction itself?

1. The Verified Behaviour

2. ACK Is Not a Completion

Both are positive news arriving from the other direction. They are otherwise unrelated.

Data Link ACKTransaction Completion
LayerData LinkTransaction
Scopeone Linkthe transaction, end to end
Purposeretire Link reliability stateresolve a non-posted Request
Applies toevery TLP, including posted writesnon-posted Requests only
CarriesLink-local progress informationstatus, and possibly data (Chapter 13.1)
Proves the operation happenednoyes — that is what its status field says (Chapter 13.2)
Owned bythis moduleModules 10 and 13

3. Identity in This Chapter

A transmitter needs a way to say this packet when it retains one and up to here when it releases them. PCIe provides a Sequence Number for that, and Chapter 14.5 owns it — its width, its wrap, and the arithmetic for comparing two of them.

This chapter uses an abstract stand-in:

seq_id in this chapter and its RTL is an internal sequence identifier — not the PCIe wire-format Sequence Number encoding. It is a locally assigned, monotonically increasing value drawn from a finite teaching range. No wrap arithmetic and no modular comparison is performed on it, deliberately, because that is exactly what 14.5 teaches.

The consequence for §7's RTL is real and worth stating up front: instead of comparing identities numerically to decide what an ACK covers, the module searches the retained entries for a match and retires from the head up to it. That is slower than the arithmetic a production design uses — and it is correct without assuming a comparison rule this chapter has not earned.

4. ACK Does Not Mean the Operation Happened

The single most consequential misreading, and it is worth being concrete about a posted write.

A Memory Write is posted. It gets no Completion, ever (Chapter 12.2). So the acknowledgement is the only positive signal a transmitter ever sees about it — which makes the temptation to read more into it almost irresistible.

Here is what an ACK for a posted Memory Write proves:

The directly connected neighbour accepted the packet under Link reliability semantics.

Here is what it does not prove:

Not provenWhich chapter owns the real answer
the destination Function was reachedrouting (Chapter 11.5)
a BAR decode matchedChapter 9.6
the target register was writtenChapter 12.2 §11
the effect is visible to any other observerordering (Chapter 13.4)
software saw anythingoutside PCIe

And on a two-Link path, an ACK on the first hop proves nothing about the second. The Switch accepted the packet. What it does with it next is a separate relationship with a separate outcome.

The debugging consequence is §11's third scenario, and it is common enough to be worth naming here: "the ACK came back, so why did the register not change?" Because the ACK was never about the register.

5. Cumulative Means a Frontier, Not a Free List

This is the fact that determines the hardware.

Suppose five packets have been sent and none acknowledged. Using abstract identities:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
retained:   10  11  12  13  14
             ^                ^
        oldest unacked    newest sent

An ACK naming 12 arrives. The rule: purge everything with an identity equal to or earlier than the one named.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
retire:     10  11  12
retained:               13  14
                         ^
                    oldest unacked

Three entries released by one event. Not one.

Two names worth fixing now, because 14.3 and 14.4 both use them:

TermMeaning
oldest_unackedthe earliest retained entry whose delivery is still unresolved
acknowledgement frontierthe point an ACK moved oldest_unacked to

6. Three Kinds of ACK Event

Not every ACK advances anything, and a design must distinguish three cases rather than two.

CaseWhat it meansWhat the model does
advancingnames a retained entryretire the prefix through it, advance the head
duplicate / stalenames something already retiredretire nothing — the frontier is already past it
unknownnames nothing the transmitter is retainingretire nothing, and report it

7. A Retirement Trace

Internal teaching signals, not PCIe wire signals. seq_id is the abstract identity of §3.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
step         1     2     3     4     5     6     7     8
tl_accept    1     1     1     0     0     1     0     0
alloc_seq   10    11    12     -     -    13     -     -
sent        10    11    12     -     -    13     -     -
 
ack_valid    0     0     0     0     1     0     1     0
ack_seq      -     -     -     -    11     -    99     -
 
retained    10  10,11 10..12 10..12  12   12,13 12,13 12,13
oldest_un   10    10    10    10     12    12    12    12
occupancy    1     2     3     3      1     2     2     2
retired      0     0     0     0      2     0     0     0
ack_unknown  0     0     0     0      0     0     1     1

Read step 5. An ACK naming 11 retires two entries — 10 and 11 — in one event. Occupancy drops from 3 to 1. 12 is untouched, because it is newer than the frontier.

Read step 7. An ACK naming 99 matches nothing retained. Nothing is retired, occupancy is unchanged, and ack_unknown sets — and stays set, because it is sticky.

And read step 6 against step 5. A new packet is allocated after the retirement. Occupancy went 3 → 1 → 2, which is the shape a healthy link produces: retirement in bursts, allocation steadily.

8. RTL — ACK Retirement Window

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Cumulative ACK retirement over a window of retained
// transmitted packets.
// Cumulative coverage (an ACK releases everything up to and including the
// identity it names, and nothing newer): NORMATIVE (section 1).
// The identity scheme, the match search, and the error outputs:
// ILLUSTRATIVE teaching abstraction.
module ack_retire_window #(
  parameter int DEPTH = 8,
  parameter int SEQ_W = 8              // internal identity width, NOT PCIe
) (
  input  logic clk,
  input  logic rst_n,
 
  // ---- A packet becomes retained (it has been handed to the Link) -------
  input  logic               alloc_valid,
  output logic               alloc_ready,
  input  logic [SEQ_W-1:0]   alloc_seq,
 
  // ---- Abstract ACK event ----------------------------------------------
  // ONE event per cycle, and it is NOT backpressurable: it is the decoded
  // result of Data Link machinery this module does not implement, and there
  // is nowhere upstream to push it back to. The caller must present at most
  // one per cycle — asserted as an environment assumption in section 9.
  input  logic               ack_valid,
  input  logic [SEQ_W-1:0]   ack_seq,
 
  // ---- State -----------------------------------------------------------
  output logic [$clog2(DEPTH+1)-1:0] occupancy,
  output logic [SEQ_W-1:0]           oldest_unacked,
  output logic                       oldest_valid,
  output logic                       full,
 
  // ---- Retirement report -----------------------------------------------
  output logic                       ack_advanced,
  output logic [$clog2(DEPTH+1)-1:0] retire_count,
  // The ACK named nothing this window is retaining. Could be already-retired
  // history or an identity never sent — this model cannot distinguish them
  // without sequence arithmetic (section 6), so it reports the combination.
  output logic                       ack_unknown
);
 
  generate
    if (DEPTH < 1) $error("DEPTH must be at least 1");
  endgenerate
 
  // Width-safe at DEPTH == 1, where $clog2(1) is 0 and a zero-width index is
  // illegal. Occupancy is one bit wider so DEPTH itself is representable —
  // a same-width counter cannot express "full" and reads it as empty.
  localparam int IDX_W = (DEPTH <= 1) ? 1 : $clog2(DEPTH);
  localparam int CNT_W = $clog2(DEPTH + 1);
 
  logic [SEQ_W-1:0] seq_q  [DEPTH];
  logic [DEPTH-1:0] vld_q;
  logic [IDX_W-1:0] head_q, tail_q;
  logic [CNT_W-1:0] cnt_q;
 
  assign occupancy      = cnt_q;
  assign full           = (cnt_q == CNT_W'(DEPTH));
  assign alloc_ready    = !full;
  assign oldest_valid   = (cnt_q != '0);
  assign oldest_unacked = seq_q[head_q];
 
  // Explicit wrap at DEPTH-1. Natural rollover wraps at 2^IDX_W, which
  // equals DEPTH only at powers of two — the bug Chapter 14.4 section 12
  // develops in full.
  function automatic logic [IDX_W-1:0] next_idx (input logic [IDX_W-1:0] i);
    next_idx = (i == IDX_W'(DEPTH - 1)) ? '0 : (i + IDX_W'(1));
  endfunction
 
  // Logical position of a PHYSICAL entry, measured from the head. Used only
  // by the bound assertions in section 9 — the design itself never needs the
  // inverse mapping, but the per-entry properties do, and a property that
  // called an undefined helper would not be a property.
  function automatic logic [CNT_W-1:0] entry_logical_pos
      (input int unsigned phys);
    entry_logical_pos = CNT_W'((phys + DEPTH - int'(head_q)) % DEPTH);
  endfunction
 
  // ---- MATCH SEARCH ----------------------------------------------------
  // How many entries, counting from the head, are covered by this ACK.
  // Walk forward from the head; if the ACK's identity is found at logical
  // position k, the covered count is k+1. Found nowhere -> covers nothing.
  //
  // A production design compares sequence numbers arithmetically and does
  // not search. That comparison is Chapter 14.5's, and using it here would
  // assume a rule this chapter has not taught.
  logic [CNT_W-1:0] covered;
  logic             matched;
 
  always_comb begin
    covered = '0;
    matched = 1'b0;
    for (int k = 0; k < DEPTH; k++) begin
      // Only positions within the current occupancy are real entries.
      if (!matched && (CNT_W'(k) < cnt_q)) begin
        // Logical position k maps to physical index head_q + k, wrapped.
        automatic int unsigned phys = (int'(head_q) + k) % DEPTH;
        if (vld_q[phys] && (seq_q[phys] == ack_seq)) begin
          matched = 1'b1;
          covered = CNT_W'(k) + CNT_W'(1);
        end
      end
    end
  end
 
  wire do_retire = ack_valid && matched;
 
  assign ack_advanced = do_retire;
  assign retire_count = do_retire ? covered : '0;
 
  // ---- RETAINED-IDENTITY UNIQUENESS ------------------------------------
  // The match search above is well-defined only if no two retained entries
  // carry the same identity. That is a property of the identity SOURCE, not
  // of this module — Chapter 14.5's sequence scheme provides it — so this
  // block does not enforce it. It EXPOSES it, so the obligation can be
  // stated as a checkable assumption (section 9's A2) instead of a comment.
  //
  // Note this is a search over ALL entries, independent of any ACK: it asks
  // "is the identity being allocated already retained?", which is the actual
  // uniqueness question.
  logic alloc_seq_exists;
 
  always_comb begin
    alloc_seq_exists = 1'b0;
    for (int i = 0; i < DEPTH; i++)
      if (vld_q[i] && (seq_q[i] == alloc_seq))
        alloc_seq_exists = 1'b1;
  end
 
  logic unk_q;
  assign ack_unknown = unk_q;
 
  wire push = alloc_valid && alloc_ready;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      vld_q <= '0; head_q <= '0; tail_q <= '0; cnt_q <= '0; unk_q <= 1'b0;
      for (int i = 0; i < DEPTH; i++) seq_q[i] <= '0;
    end else begin
      // ---- Retirement ---------------------------------------------------
      // Clears exactly `covered` entries from the head. Nothing newer than
      // the frontier is touched, which is the whole normative rule.
      if (do_retire) begin
        for (int k = 0; k < DEPTH; k++)
          if (CNT_W'(k) < covered)
            vld_q[(int'(head_q) + k) % DEPTH] <= 1'b0;
      end
 
      // ---- Allocation ---------------------------------------------------
      if (push) begin
        seq_q[tail_q] <= alloc_seq;
        vld_q[tail_q] <= 1'b1;
        tail_q        <= next_idx(tail_q);
      end
 
      // ---- Head advance -------------------------------------------------
      // Advanced by exactly the retired count, never by a fixed step. A
      // head that moves one per ACK is section 5's leak.
      if (do_retire) begin
        automatic int unsigned nh = (int'(head_q) + int'(covered)) % DEPTH;
        head_q <= IDX_W'(nh);
      end
 
      // ---- Occupancy ----------------------------------------------------
      // SAME-CYCLE CONTRACT, written as one expression over both events so
      // an ACK and an allocation in the same cycle cannot be resolved by
      // statement order. Retirement is subtracted and allocation added
      // together; the result cannot underflow because `covered` is bounded
      // by cnt_q by construction.
      cnt_q <= cnt_q
             - (do_retire ? covered : CNT_W'(0))
             + (push      ? CNT_W'(1) : CNT_W'(0));
 
      // Reported, and it never frees anything.
      if (ack_valid && !matched) unk_q <= 1'b1;
    end
  end
 
endmodule

Classification: synthesizable.

Architecture. A circular window of retained identities with an occupancy counter, and a combinational match search that converts an ACK into a count of entries to retire.

State. seq_q and vld_q per entry, head and tail pointers, an occupancy count, and a sticky unknown-ACK flag.

Why occupancy is a separate counter rather than inferred from the pointers. At full, head_q == tail_q — and at empty, also head_q == tail_q. Pointer equality cannot distinguish them, and a design that tries reads a full window as empty and allocates over unresolved entries. Chapter 14.4 §13 develops this.

Same-cycle contract, stated explicitly:

Event pairResolution
ACK + allocationboth apply. Occupancy is cnt − covered + 1, computed in one expression
ACK on emptymatched is false — nothing retires, ack_unknown sets
ACK naming the entry allocated this cyclenot matched — the entry is not yet in vld_q. Retires nothing, which is the safe direction
allocation when fullrefused by alloc_ready

Contract. The caller presents at most one ACK event per cycle and must not withdraw it — the interface is not backpressurable, and §9's assume states that rather than leaving it in prose. Downstream relies on retire_count being the exact number of entries released.

Failure — five, and §10 maps each to a check. Advancing the head by one per ACK leaks storage until the window fills (§5). Retiring the whole window on any ACK releases unresolved packets that can then never be replayed. Letting an unmatched ACK retire the head does the same thing with a different trigger. Decrementing occupancy by one rather than by covered desynchronises the count from the valid bits. And natural pointer rollover breaks at any non-power-of-two DEPTH.

Deliberately simplified: identities only, no packet data (Chapter 14.4); a linear match search rather than sequence arithmetic (14.5); no replay (14.3); no replay timer; no distinction between stale and unknown.

Production implication: a real transmitter compares Sequence Numbers arithmetically, stores the packets themselves, participates in replay, and runs a replay timer. The cumulative-frontier rule is unchanged by all of it.

9. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SVA over ack_retire_window. These assert the NORMATIVE cumulative
// retirement rule of section 1 and the LOCAL window contract. They assert
// nothing about ACK DLLP format or timing (Chapter 15.3), nothing about
// sequence arithmetic (Chapter 14.5), and nothing about replay (14.3).
 
// ---- ENVIRONMENT ASSUMPTIONS, stated rather than implied -------------
// A1: at most one ACK event per cycle. The interface is not backpressurable,
// so the caller owns this obligation.
assume property (@(posedge clk) disable iff (!rst_n)
  ack_valid |-> !$isunknown(ack_seq));
// A2: RETAINED IDENTITIES ARE UNIQUE. An identity being allocated is not
// already retained. Stated over `alloc_seq_exists`, which searches the whole
// window — NOT over the current ACK, which would only forbid a coincidence
// between one allocation and one acknowledgement and would say nothing about
// uniqueness at all.
//
// This is an ASSUMPTION rather than an assertion because the obligation
// belongs to the identity source (Chapter 14.5), not to this module. A
// design whose identity source cannot guarantee it must turn this into an
// assertion and add an error output — see the note after the properties.
assume property (@(posedge clk) disable iff (!rst_n)
  push |-> !alloc_seq_exists);
 
// ---- SAFETY ----------------------------------------------------------
 
// P1: THE NORMATIVE PROPERTY. An ACK retires a contiguous prefix from the
// head, and never more entries than are retained.
property p_retire_bounded_prefix;
  @(posedge clk) disable iff (!rst_n)
  ack_advanced |-> ((retire_count >= CNT_W'(1)) && (retire_count <= cnt_q));
endproperty
a_prefix : assert property (p_retire_bounded_prefix);
 
// P2: NOTHING NEWER THAN THE FRONTIER IS RELEASED. Stated per entry, so a
// design that cleared an extra valid bit cannot hide in an aggregate count.
generate for (genvar g = 0; g < DEPTH; g++) begin : g_no_overshoot
  a_no_overshoot : assert property (@(posedge clk) disable iff (!rst_n)
    ($fell(vld_q[g]))
      |-> ($past(do_retire) && ($past(entry_logical_pos(g)) < $past(covered)))
       || $past(!rst_n));
end endgenerate
 
// P3: an ACK that matches nothing retires NOTHING and is reported.
property p_unknown_ack_inert;
  @(posedge clk) disable iff (!rst_n)
  (ack_valid && !matched)
    |-> (!ack_advanced && (retire_count == '0))
     ##1 (ack_unknown && (cnt_q == $past(cnt_q) + ($past(push) ? CNT_W'(1) : CNT_W'(0))));
endproperty
a_unknown_inert : assert property (p_unknown_ack_inert);
 
// P4: an ACK NEVER creates state. It cannot allocate, and it cannot make an
// invalid entry valid.
property p_ack_creates_nothing;
  @(posedge clk) disable iff (!rst_n)
  (ack_valid && !push) |=> (cnt_q <= $past(cnt_q));
endproperty
a_ack_no_create : assert property (p_ack_creates_nothing);
 
// P5: OCCUPANCY CONSERVATION across every event combination, including the
// same-cycle case. The single property that catches a decrement-by-one bug.
property p_occupancy_exact;
  @(posedge clk) disable iff (!rst_n)
  1'b1 |=> (cnt_q == $past(cnt_q)
                   - $past(ack_advanced ? retire_count : CNT_W'(0))
                   + $past(push ? CNT_W'(1) : CNT_W'(0)));
endproperty
a_occupancy : assert property (p_occupancy_exact);
 
// P6: occupancy is bounded and never underflows.
property p_occupancy_sane;
  @(posedge clk) disable iff (!rst_n)
  (occupancy <= CNT_W'(DEPTH));
endproperty
a_occ_bounded : assert property (p_occupancy_sane);
 
// P7: the head advances by EXACTLY the retired count, and only on a
// retirement. A head that moves independently is section 5's leak or its
// overshoot, depending on direction.
property p_head_tracks_retirement;
  @(posedge clk) disable iff (!rst_n)
  !$stable(head_q) |-> ($past(ack_advanced) && ($past(retire_count) != '0));
endproperty
a_head : assert property (p_head_tracks_retirement);
 
// P8: a retired entry never becomes pending again without a new allocation.
generate for (genvar g = 0; g < DEPTH; g++) begin : g_no_resurrect
  a_no_resurrect : assert property (@(posedge clk) disable iff (!rst_n)
    $rose(vld_q[g]) |-> ($past(push) && ($past(tail_q) == IDX_W'(g))));
end endgenerate
 
// P9: a retained identity is IMMUTABLE while valid. It is the thing the ACK
// will be matched against, so it must not move underneath the comparison.
generate for (genvar g = 0; g < DEPTH; g++) begin : g_seq_stable
  a_seq_stable : assert property (@(posedge clk) disable iff (!rst_n)
    (vld_q[g] && !(do_retire && (entry_logical_pos(g) < covered)))
      |=> $stable(seq_q[g]));
end endgenerate
 
// P10: allocation is refused when full — the layer can and must say no.
property p_full_refuses;
  @(posedge clk) disable iff (!rst_n)
  full |-> !alloc_ready;
endproperty
a_full : assert property (p_full_refuses);
 
// P11: pointers stay in range at every DEPTH, including non-powers of two.
property p_pointers_in_range;
  @(posedge clk) disable iff (!rst_n)
  (head_q < IDX_W'(DEPTH)) && (tail_q < IDX_W'(DEPTH));
endproperty
a_ptr_range : assert property (p_pointers_in_range);
 
// P12: THE LAYER-BOUNDARY PROPERTY. An ACK never produces a Transaction
// Layer event. Bound against the TL interface to make section 2's
// distinction checkable rather than merely explained.
property p_ack_is_not_a_completion;
  @(posedge clk) disable iff (!rst_n)
  ack_valid |-> !tl_completion_event;
endproperty
a_layer_boundary : assert property (p_ack_is_not_a_completion);
 
// P13: reset clears the LOCAL teaching state of this block.
property p_reset_clears;
  @(posedge clk)
  !rst_n |=> ((occupancy == '0) && (vld_q == '0) && !ack_advanced);
endproperty
a_reset : assert property (p_reset_clears);

P2 and P5 are the pair that pins cumulative retirement, and they fail on opposite mutations. P5 catches a design that miscounts — decrementing by one when three entries were released. P2 catches one that clears too many valid bits, which P5 would not see if the counter was updated to match. Per-entry properties are what stop an aggregate count from covering for a per-entry bug.

P3 is written as a two-cycle sequence deliberately. It asserts that an unmatched ACK does nothing and that occupancy is unchanged except by whatever allocation happened the same cycle — so the property survives the concurrent case rather than being written for a quiet interface it will never see.

P12 is unusual: it is bound across a layer boundary. §2's distinction is normally taught and never checked. Binding it against the Transaction Layer's completion event makes "an ACK is not a Completion" a property rather than a paragraph — and it fires immediately on a design that wired the two together.

A2 is worth reading as a statement of what 14.5 buys you, and its shape matters. The match search is correct only if no two retained entries carry the same identity — so the assumption is stated over alloc_seq_exists, a search of the whole window, and not over the current ACK. An assumption relating alloc_seq to ack_seq would forbid one coincidence and say nothing about uniqueness, which is the kind of property that looks right in a review and constrains nothing in a proof.

Why it is an assumption and not an assertion. Uniqueness is a property of the identity source — PCIe's Sequence Number scheme (Chapter 14.5) — not of this module, which only consumes identities it is handed. A design whose source cannot guarantee it should convert A2 into an assertion and add an error output, because then the check belongs here; §8's alloc_seq_exists is already the expression it would need. What is not defensible is leaving the obligation implicit, since the search silently returns the oldest duplicate and the retirement prefix is then wrong by however far apart they sit.

10. Verification

Monitors observe: the allocation interface, the ACK event, occupancy, oldest_unacked, retire_count, and the reported conditions.

The scoreboard maintains its own retained list — a simple ordered queue of identities. On an ACK it finds the identity, removes the prefix through it, and computes the expected retire count itself. It must not read vld_q, cnt_q, head_q or the DUT's covered.

The independence matters most for the count. A scoreboard that read retire_count and checked occupancy against it would verify only that the DUT is consistent with itself — and the one-per-ACK mutation keeps those two consistent while leaking storage.

Retirement

  • One packet, then an ACK naming it. Retire count 1, occupancy 0.
  • Three packets, ACK naming the oldest. Retire count 1, two retained.
  • Three packets, ACK naming the middle. Retire count 2.
  • Three packets, ACK naming the newest. Retire count 3, window empty.
  • Five packets, ACK naming the third, then an ACK naming the fifth. Two events, 3 then 2.
  • A long run of allocations with periodic ACKs. Verify occupancy never drifts from the scoreboard's.

Negative

  • A duplicate ACK — the same identity twice. Verify the second retires nothing.
  • A stale ACK — an identity already retired. Verify nothing retires.
  • An unknown ACK — an identity never allocated. Verify nothing retires and ack_unknown sets (P3).
  • An ACK on an empty window. Verify nothing happens and no underflow.
  • An ACK naming the identity allocated in the same cycle. Verify it does not match — the entry is not yet valid — and that the allocation still succeeds.
  • An ACK while full. Verify it retires correctly and alloc_ready rises.

Structural

  • Fill to DEPTH. Verify alloc_ready drops (P10) and occupancy equals DEPTH.
  • DEPTH = 1. The index-width corner.
  • Non-power-of-two DEPTH — 3, 5, 7. Fill, wrap, retire across the wrap, and verify pointers stay in range (P11). This is a required test, not a stress case: natural rollover passes every power-of-two depth.
  • Retirement spanning the physical wrap — head near DEPTH-1, retiring several entries. Verify the modular head advance lands correctly.
  • ACK and allocation in the same cycle at every occupancy from 1 to DEPTH (P5).
  • Reset with the window non-empty.

Which mutation which check kills

Injected mutationCaught by
free only the named packet, not the prefixscoreboard retire-count mismatch; occupancy drifts upward over a long run
free the whole window on any ACKP2, per-entry; and the scoreboard on the first ACK that is not the newest
a duplicate ACK frees againthe duplicate-ACK test; scoreboard occupancy mismatch
an unknown ACK frees the headP3, immediately
occupancy decremented by one regardless of retire countP5
the head advances past an unacknowledged entryP7, and P2
pointers wrap at 2^IDX_WP11, at DEPTH = 5
an ACK made an invalid entry validP8
a retained identity mutated in placeP9
ACK wired to a Transaction Layer eventP12

Coverage should include: retirement counts from 1 to DEPTH; occupancy at 0, 1, DEPTH−1, DEPTH; ACK-plus-allocation at each occupancy; the duplicate, stale and unknown paths; DEPTH = 1 and a non-power-of-two depth; and retirement spanning the physical wrap.

11. Debugging

Replay storage fills despite many ACKs arriving

The frontier is not advancing as far as the ACKs permit.

Compare retire_count against what the ACK covered. If ACKs consistently retire exactly one entry while naming identities several positions into the window, the design is treating cumulative acknowledgement as per-packet (§5) — the most common form of this bug and the one that produces a slow, steady leak.

Two others. The match search is failing, so ack_unknown is set and nothing retires — check that flag first, it is one bit and it answers the question. Or the head advances but occupancy does not, so alloc_ready stays low against a window that is actually empty.

The observation that separates them: dump occupancy alongside the count of valid entries. If they disagree, the counter is the bug; if they agree and both are high, the retirement is.

A packet newer than the ACK disappeared

Retirement overshot the frontier, and this is the dangerous direction.

The ACK named an identity; entries newer than it were released. Those packets' delivery was still unresolved, and if one is later selected for replay it is gone (Chapter 14.3).

Check the covered count against the position of the matched entry. covered must equal the matched entry's logical position plus one. If it equals the occupancy instead, the design is retiring the whole window on any match.

P2 catches it in simulation, per entry. In the field the symptom is a replay that cannot find its packet, which surfaces far from the cause.

A Memory Write's target was not updated even though an ACK was observed

The ACK proved the wrong thing, and there is no bug here yet.

An ACK says the neighbour accepted the packet (§4). It says nothing about the destination Function, the BAR decode, the target register, or any observer.

Trace forward, not backward. On a multi-hop path, ask whether the next Link carried it — the acknowledgement you saw belonged to one hop (Chapter 3.2 §2). Then walk Chapter 12.2 §14's ladder from the receiving Endpoint: BAR hit, resource handshake, the update itself.

The instinct to resist is treating the ACK as evidence that everything downstream must be fine. It is evidence about exactly one hop.

The same TLP is replayed even though an ACK for it already arrived

The ACK did not retire the corresponding retained state.

Two candidates, and one signal distinguishes them. If ack_unknown is set for that ACK, the identity did not match anything retained — an identity-mapping problem between what was allocated and what the ACK names. If it is clear and the entry is still valid afterwards, the retirement logic ran and did not clear it.

And check the ordering of the two events. If the replay was selected in the same cycle the ACK arrived, this is a same-cycle race — Chapter 14.4 §8 owns the interaction between retirement and an active replay walk, and this window model deliberately does not have one.

12. Common Misconceptions

  • "An ACK means the transaction completed." It means the neighbour accepted the packet on one Link. A Completion means the operation was serviced (§2).
  • "An ACK means remote software consumed the operation." It does not reach software at all (§4).
  • "An ACK travels end to end through Switches." Each Link has an independent relationship. A Switch terminates one and originates another (§2).
  • "One ACK acknowledges exactly one TLP." It is cumulative — it releases everything up to and including the identity it names (§1, §5).
  • "An ACK frees the whole replay buffer." It frees a prefix. Anything newer than the frontier stays retained (§5).
  • "ACK and Cpl are equivalent." Different layers, different scopes, different meanings, and different owners (§2).
  • "An ACK for a posted write is a success status." A posted write has no status. The ACK is Link-local progress (Chapter 12.2).
  • "The identity in an ACK is a Requester Tag." A Tag correlates a Completion to a Request end to end (Chapter 11.3 §6). This is a Link-local sequence identity (Chapter 14.5).
  • "A stale ACK can safely free the oldest entry." It may free nothing. Freeing the head releases a packet whose delivery is unresolved (§6).
  • "ACK belongs to the Transaction Layer." It is Data Link machinery and never produces a Transaction Layer event (§2, P12).
  • "If ACKs are fast enough, no replay storage is needed." Storage is needed because delivery is unresolved between transmission and acknowledgement — however brief that window is (Chapter 14.1 §8).

13. Understanding Check

14. What's Next

This chapter answered the question Chapter 14.1 left open: retained state is released by a cumulative acknowledgement that advances a frontier, releasing a contiguous prefix and nothing beyond it. An ACK that matches nothing releases nothing — and none of it says anything about whether an operation happened.

That is the good path. Chapter 14.3 — NAK takes the other one: what happens when Link-local reception does not succeed, which retained traffic must be sent again, and why a retransmission must never look like a new Transaction Layer operation.

Chapter 14.4 then builds the storage both paths need — an ordered structure where reading for transmission is not dequeuing — and Chapter 14.5 replaces §3's abstract identity with PCIe's actual Sequence Number mechanism, including the comparison arithmetic §8's match search deliberately avoided.

Chapter 15.3 owns the ACK DLLP itself: its format and its timing.

The idea to carry forward: an ACK moves a frontier, not a packet — and the frontier is about a Link, not about an operation.