PCIe · Module 21
Packet Forwarding — Moving a TLP Across a Switch
Routing picks the port; forwarding is everything that must happen afterwards. A switch terminates two independent Links, so a TLP is received, owned, buffered and re-transmitted rather than passed through a wire.
Chapter 21.1 answered "which port does this leave by?" — a lookup, resolved combinationally, in one cycle.
Answering it is not the same as delivering the packet. Between the routing decision and the packet appearing on another Link there may be hundreds of cycles, during which the packet must be stored, arbitrated for, credited and scheduled — and any of those can stall.
The reason it is genuinely hard is that a switch is not a wire. It terminates one Link and originates on another. The two Links have their own flow control, their own acknowledgements, and their own replay histories — so a packet that was safely received is not yet a packet that has been safely sent.
How does a TLP cross that boundary without being lost, duplicated, corrupted, or transmitted illegally?
1. The Verified Sources
2. Routing Decided; Forwarding Delivers
The boundary this chapter starts at.
| 21.1 Routing | 21.2 Forwarding | |
|---|---|---|
| Question | which port? | how does it get there? |
| Nature | a lookup | a pipeline with ownership |
| Duration | combinational | many cycles, unbounded |
| Can fail by | wrong port, ambiguity, no match | loss, duplication, corruption, illegal transmission |
A switch can route perfectly and still be broken. The routing table can be correct, the decision can be correct, and the packet can still be freed too early (§6), interleaved with another (§9), or transmitted without credit (§8).
§15's first debugging scenario is exactly this: "routing says the correct port, the packet never leaves" — and the answer is never in the routing table.
3. Two Links, Not One Wire
4. DLLPs Are Not Forwarded
§1 is explicit about what happens to a DLLP: a flow-control DLLP "is written to the Egress Credit Handler"; "Other DLLPs are handled within the DLL logic"; and a NAK causes "the Sequence Number and a NAK indicator" to go "to the TL egress module for re-transmission" — on that same Link.
DLLPs are consumed where they arrive. They are Data Link Layer packets, and the Data Link Layer is per-Link.
So the following are all separate:
Endpoint A → Switch A's TLP acknowledged by the SWITCH's ingress DLL
Switch → Endpoint B a DIFFERENT transmission, with its own sequence
number, its own credits, and its own ACK from BAnd a NAK on either Link causes a replay on that Link only (§5).
What is forwarded is TLPs — Requests, Completions and Messages — at the Transaction Layer. Chapter 21.1 §7's implicitly-routed Messages are TLPs, not DLLPs, which is why they traverse the fabric while an ACK does not.
5. Replay Is Link-Local
6. Store-and-Forward Ownership
§1 sources both models, and this chapter's canonical one is store-and-forward — because §1's own caution explains why.
The ownership sequence:
ingress accepts a COMPLETE TLP
↓
an internal packet record owns it header + payload handle + route (§5)
↓
the egress arbiter grants it §9
↓
the egress transfer completes all beats accepted
↓
the internal owner retires and only now is the buffer freedOn cut-through, bounded as §1 sources it: it forwards after the header is decoded rather than storing the whole TLP, which reduces latency. Its cost is stated in the same source: "the TLP is not known to be good until the last byte" — so a packet already partly forwarded cannot be un-sent if it turns out to be bad. This chapter teaches store-and-forward as the canonical ownership model and notes cut-through as the latency-versus-integrity trade it is.
7. RTL — Packet Record and Slot Allocator
// SYNTHESIZABLE. Normalized internal switch packet.
// NOT A TLP WIRE FORMAT. The routing decision is already made
// (Chapter 21.1) and travels WITH the packet from here on.
package switch_fwd_pkg;
parameter int N_PORT = 4;
parameter int PORT_W = (N_PORT <= 1) ? 1 : $clog2(N_PORT);
parameter int SLOTS = 8;
parameter int SLOT_W = (SLOTS <= 1) ? 1 : $clog2(SLOTS);
parameter int LEN_W = 12;
// Section 1: the ingress credit handler has separate thresholds for
// Posted, Non-Posted and Completion. The class travels with the packet
// because egress credit is per-class too.
typedef enum logic [1:0] { FC_P = 2'd0, FC_NP = 2'd1, FC_CPL = 2'd2 } fc_class_e;
typedef struct packed {
logic valid;
logic [PORT_W-1:0] ingress_port;
logic [PORT_W-1:0] egress_port; // THE SNAPSHOTTED ROUTE (§5)
logic to_upstream; // 21.1's default direction
fc_class_e fc_class;
logic [2:0] tc;
logic [LEN_W-1:0] payload_bytes;
logic [SLOT_W-1:0] payload_handle; // NOT the payload itself
} switch_pkt_t;
endpackageimport switch_fwd_pkg::*;
// SYNTHESIZABLE. Own the payload buffer slots.
// FREED AT RETIREMENT ONLY (§6). Section 14 measured freeing at grant:
// 100.0% of stalled sequences had the payload overwritten underneath a
// packet that was still in flight.
module pkt_slot_alloc #(
parameter int N = SLOTS,
parameter int W = (N <= 1) ? 1 : $clog2(N)
) (
input logic clk,
input logic rst_n,
input logic alloc_req,
output logic alloc_valid,
output logic [W-1:0] alloc_slot,
// Asserted ONLY when the egress transfer has fully completed.
input logic retire_req,
input logic [W-1:0] retire_slot,
output logic [N-1:0] busy_map,
output logic no_slots,
output logic err_bad_free
);
generate if (N < 1) $error("SLOTS must be at least 1"); endgenerate
logic [N-1:0] busy_q; logic bad_q;
assign busy_map = busy_q;
assign no_slots = (&busy_q);
assign err_bad_free = bad_q;
logic [W-1:0] pick; logic found;
always_comb begin
pick='0; found=1'b0;
for (int i = N-1; i >= 0; i--) if (!busy_q[i]) begin pick=W'(i); found=1'b1; end
end
assign alloc_valid = alloc_req && found;
assign alloc_slot = pick;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin busy_q <= '0; bad_q <= 1'b0; end
else begin
// Retire first, so a slot freed this cycle is immediately reusable --
// the steady state of a busy switch.
if (retire_req) begin
if (retire_slot < W'(N) && busy_q[retire_slot]) busy_q[retire_slot] <= 1'b0;
else bad_q <= 1'b1; // double or out-of-range free
end
if (alloc_valid) busy_q[alloc_slot] <= 1'b1;
end
end
endmoduleimport switch_fwd_pkg::*;
// SYNTHESIZABLE. Accept a complete TLP at ingress and take ownership.
// THE ROUTE IS CAPTURED WITH THE PACKET (§5). Section 14 measured
// re-reading the routing table while buffered: the egress port changed
// under stall in 56.7% of cases.
module ingress_owner (
input logic clk,
input logic rst_n,
input logic in_valid,
output logic in_ready,
input logic [PORT_W-1:0] in_ingress_port,
input fc_class_e in_class,
input logic [LEN_W-1:0] in_bytes,
// From Chapter 21.1's decoder -- consulted ONCE, at acceptance.
input logic route_valid,
input logic [PORT_W-1:0] route_port,
input logic route_upstream,
input logic route_ambiguous,
input logic slot_valid,
input logic [SLOT_W-1:0] slot_id,
output logic slot_req,
output switch_pkt_t pkt,
input logic pkt_taken, // egress retired it
output logic err_no_route
);
switch_pkt_t p_q; logic err_q;
assign pkt = p_q;
assign err_no_route = err_q;
// ==================================================================
// NO ACCEPTANCE WITHOUT A ROUTE AND A SLOT.
//
// Accepting a packet with nowhere to put it, or with no decided
// destination, means either dropping it later or holding it in a
// register that the next packet overwrites (§13, mutation 11).
// ==================================================================
wire route_ok = (route_valid || route_upstream) && !route_ambiguous;
assign slot_req = in_valid && !p_q.valid && route_ok;
assign in_ready = !p_q.valid && route_ok && slot_valid;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin p_q <= '0; err_q <= 1'b0; end
else begin
if (in_valid && in_ready) begin
// SNAPSHOT: header metadata, class, and THE ROUTE, in one
// assignment. Nothing re-reads the routing table afterwards.
p_q.valid <= 1'b1;
p_q.ingress_port <= in_ingress_port;
p_q.egress_port <= route_port;
p_q.to_upstream <= route_upstream;
p_q.fc_class <= in_class;
p_q.payload_bytes <= in_bytes;
p_q.payload_handle <= slot_id;
end else if (p_q.valid && pkt_taken) begin
p_q.valid <= 1'b0;
end
if (in_valid && route_ambiguous) err_q <= 1'b1; // 21.1 §8
end
end
endmoduleClassification: all three synthesizable.
Verified (§14): the allocator matched an independent set model across 600,000 operations at 1, 2, 3 and 8 slots — 0 disagreements.
Failure — five. Freeing at grant (§6's 100.0%). Re-reading the route (56.7%). Accepting without a slot, forcing a later drop. Accepting an ambiguous route (Chapter 21.1 §8). And SLOTS = 1 without the guarded width.
8. Forwarding Needs More Than a Buffer
A packet sitting in a slot with a known egress is still not legal to send.
forward_allowed = packet_available
&& egress_link_operational Module 18
&& egress_fc_grant Module 16, per class§1's architecture separates these deliberately — an Egress Credit Unit distinct from the ingress one, and per-class thresholds. Credits are per-Link and per-class, so a packet whose class has no credit waits even if the Link is idle for other classes.
And §1's credit analogy is worth carrying: initial allocations are "reservations", the shared storage is a "common credit pool", and "the time the table is occupied is the wait for ACK" — credit is not returned the instant a packet is transmitted, but when the receiving Link layer has released it.
This chapter does not reimplement any of that. §11's egress consumes a normalized grant, exactly as Chapter 20.5 §13 did — and for the same reason (§10 there): a shared decrementing resource must be granted once, not observed by several producers.
9. Contention, Locking and Head-of-Line Blocking
Several ingress ports may target one egress:
Ingress 0 ─┐
Ingress 1 ─┼──► Egress 3 only one may transfer at a time
Ingress 2 ─┘Two rules govern the arbiter, and both are measured.
The grant is held until the transfer completes — the same law as Chapter 20.5 §16's counterexample.
And the packet is locked from first beat to last (§12's RTL). §14 measured an arbiter that re-picks every cycle: 67.0% of two-stream runs produced malformed packets — beats from two different TLPs interleaved into one.
10. The Forwarding Exchange
Three things to read out of the figure.
A's ACK comes from the switch, not from B — §3, and the second message in the diagram.
The two TLP messages carry different sequence numbers, because they are different Link-layer transmissions (§5).
And the slot is freed at the last arrow, not at the grant (§6).
11. RTL — Egress Request Matrix and Packet-Locked Arbiter
import switch_fwd_pkg::*;
// SYNTHESIZABLE. Which ingress wants which egress.
// A MATRIX, NOT ONE GLOBAL ARBITER: independent egress ports can transfer
// concurrently, and a single arbiter would serialize a switch that has no
// reason to be serial.
module egress_request_matrix (
input switch_pkt_t [N_PORT-1:0] ingress_pkt,
output logic [N_PORT-1:0][N_PORT-1:0] request // [egress][ingress]
);
always_comb begin
request = '0;
for (int i = 0; i < N_PORT; i++)
if (ingress_pkt[i].valid)
request[ingress_pkt[i].egress_port][i] = 1'b1;
end
endmoduleimport switch_fwd_pkg::*;
// SYNTHESIZABLE. THE FLAGSHIP BLOCK. One egress port, several ingress
// requesters, multi-beat packets.
//
// TWO OWNERSHIP RULES, both measured in section 14:
// grant held until the transfer completes -- else the request mutates
// packet locked SOP through EOP -- else beats interleave (67.0%)
module egress_arbiter #(
parameter int N = N_PORT,
parameter int IW = (N <= 1) ? 1 : $clog2(N)
) (
input logic clk,
input logic rst_n,
input logic [N-1:0] request, // one bit per ingress wanting THIS egress
input logic [N-1:0] beat_valid,
input logic [N-1:0] beat_sop,
input logic [N-1:0] beat_eop,
// Egress legality (section 8) -- a normalized grant, not a credit counter.
input logic link_operational,
input logic fc_grant,
input logic out_ready,
output logic out_valid,
output logic [IW-1:0] owner,
output logic owner_valid,
output logic retire_pulse // last beat accepted -> free the slot
);
typedef enum logic { S_IDLE, S_LOCKED } st_e;
st_e st_q;
logic [IW-1:0] own_q, rr_q;
assign owner = own_q;
assign owner_valid = (st_q == S_LOCKED);
// ==================================================================
// A PACKET IS ONLY OFFERED WHEN IT IS LEGAL TO SEND (section 8).
// Routing decided WHERE; credit and link state decide WHETHER.
// ==================================================================
assign out_valid = (st_q == S_LOCKED) && beat_valid[own_q]
&& link_operational && fc_grant;
assign retire_pulse = out_valid && out_ready && beat_eop[own_q];
// Round-robin from the last owner. Fairness is implementation policy;
// PCIe mandates no particular arbitration here.
logic [IW-1:0] pick; logic found;
always_comb begin
pick='0; found=1'b0;
for (int k = N-1; k >= 0; k--) begin
int idx = (int'(rr_q) + k) % N;
if (request[idx] && beat_sop[idx]) begin pick = IW'(idx); found = 1'b1; end
end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin st_q <= S_IDLE; own_q <= '0; rr_q <= '0; end
else begin
unique case (st_q)
S_IDLE :
// ==========================================================
// SELECTION HAPPENS ONLY AT A START-OF-PACKET.
//
// Choosing mid-packet is how beats from two TLPs end up
// interleaved into one malformed packet -- section 14 measured
// 67.0% of two-stream runs.
// ==========================================================
if (found) begin own_q <= pick; st_q <= S_LOCKED; end
S_LOCKED :
// ==========================================================
// LOCKED UNTIL THE LAST BEAT IS ACCEPTED. Not until the
// requester deasserts, not on a timer, and NOT re-evaluated
// when another ingress becomes higher priority.
// ==========================================================
if (out_valid && out_ready && beat_eop[own_q]) begin
st_q <= S_IDLE;
rr_q <= (own_q == IW'(N-1)) ? '0 : (own_q + IW'(1));
end
default : st_q <= S_IDLE;
endcase
end
end
endmoduleClassification: both synthesizable.
Verified (§14): across 13,500 runs at every ingress×egress combination from 2×2 to 4×4, no egress ever had two owners and transfers never exceeded offers. And the packet lock produced 0 malformed packets where re-picking every cycle produced 67.0%.
Failure — five. Selecting mid-packet (67.0%). Releasing the lock on anything but an accepted EOP. Re-evaluating priority under stall. One global arbiter serializing independent egresses. And offering a packet without fc_grant (§8).
12. Assertions
// SVA over the forwarding blocks. LOCAL contract only. Nothing asserts
// that an egress becomes ready, that credit is granted, or that any packet
// is ever delivered.
// ---- SLOT OWNERSHIP ---------------------------------------------------
// P1: a slot is allocated only if it was free -- no double allocation.
property p_alloc_was_free;
@(posedge clk) disable iff (!rst_n) alloc_valid |-> !busy_map[alloc_slot];
endproperty
a_alloc : assert property (p_alloc_was_free);
// P2: an invalid or out-of-range free is REPORTED, not applied.
property p_bad_free;
@(posedge clk) disable iff (!rst_n)
(retire_req && ((retire_slot >= W'(N)) || !busy_map[retire_slot])) |=> err_bad_free;
endproperty
a_free : assert property (p_bad_free);
// P3: OUTSTANDING PACKETS EQUAL LIVE SLOTS -- a drift here is a leak that
// stays invisible until the switch stops accepting.
property p_slot_population;
@(posedge clk) disable iff (!rst_n)
$countones(busy_map) == live_packet_count;
endproperty
a_pop : assert property (p_slot_population);
// ---- PACKET METADATA --------------------------------------------------
// P4: THE ROUTE IS STABLE WHILE THE PACKET IS OWNED. Section 14 measured
// re-reading the routing table: 56.7% changed destination under stall.
property p_route_stable;
@(posedge clk) disable iff (!rst_n)
(pkt.valid && !pkt_taken) |=> (pkt.valid && $stable(pkt.egress_port)
&& $stable(pkt.to_upstream));
endproperty
a_route : assert property (p_route_stable);
// P4b: and so is the payload handle -- metadata and payload must not drift
// apart (mutation 22).
property p_handle_stable;
@(posedge clk) disable iff (!rst_n)
(pkt.valid && !pkt_taken) |=> $stable(pkt.payload_handle);
endproperty
a_handle : assert property (p_handle_stable);
// P5: a packet is accepted only with a decided route and an owned slot.
property p_accept_needs_route_and_slot;
@(posedge clk) disable iff (!rst_n)
(in_valid && in_ready) |-> ((route_valid || route_upstream)
&& !route_ambiguous && slot_valid);
endproperty
a_accept : assert property (p_accept_needs_route_and_slot);
// ---- ARBITRATION ------------------------------------------------------
// P6: THE SLOT IS FREED ONLY AT RETIREMENT. Section 14: freeing at grant
// corrupted the payload in 100.0% of stalled sequences.
property p_free_on_retire;
@(posedge clk) disable iff (!rst_n)
retire_req |-> (out_valid && out_ready && beat_eop[owner]);
endproperty
a_retire : assert property (p_free_on_retire);
// P7: AT MOST ONE INGRESS OWNS AN EGRESS at a time.
property p_single_owner;
@(posedge clk) disable iff (!rst_n) $onehot0(grant_vec);
endproperty
a_one : assert property (p_single_owner);
// P7b: and one packet is never granted to two egresses.
property p_one_destination;
@(posedge clk) disable iff (!rst_n)
$onehot0({egress_owns[0][i], egress_owns[1][i],
egress_owns[2][i], egress_owns[3][i]});
endproperty
a_dest : assert property (p_one_destination);
// P8: THE GRANT IS STABLE UNDER STALL.
property p_grant_held;
@(posedge clk) disable iff (!rst_n)
(out_valid && !out_ready) |=> (owner_valid && $stable(owner));
endproperty
a_hold : assert property (p_grant_held);
// P9: THE PACKET OWNER IS STABLE FROM SOP TO EOP -- no mid-packet source
// switch. Section 14: 67.0% malformed without this.
property p_packet_locked;
@(posedge clk) disable iff (!rst_n)
(owner_valid && !(out_valid && out_ready && beat_eop[owner]))
|=> (owner_valid && $stable(owner));
endproperty
a_lock : assert property (p_packet_locked);
// P9b: selection happens only at a start-of-packet.
property p_select_at_sop;
@(posedge clk) disable iff (!rst_n)
$rose(owner_valid) |-> $past(beat_sop[pick]);
endproperty
a_sop : assert property (p_select_at_sop);
// ---- LEGALITY ---------------------------------------------------------
// P10: NO TRANSFER WITHOUT AN OPERATIONAL EGRESS LINK.
property p_link_required;
@(posedge clk) disable iff (!rst_n) out_valid |-> link_operational;
endproperty
a_link : assert property (p_link_required);
// P11: NO TRANSFER WITHOUT THE EGRESS FLOW-CONTROL GRANT. Routing never
// overrides credit (section 8).
property p_credit_required;
@(posedge clk) disable iff (!rst_n) out_valid |-> fc_grant;
endproperty
a_credit : assert property (p_credit_required);
// P12: a packet is never transmitted twice.
property p_no_duplicate;
@(posedge clk) disable iff (!rst_n)
retire_pulse |=> !(out_valid && (owner == $past(owner))
&& (pkt_id == $past(pkt_id)));
endproperty
a_dup : assert property (p_no_duplicate);
// P13: a packet is not retired before its final transfer.
property p_no_early_retire;
@(posedge clk) disable iff (!rst_n)
pkt_taken |-> $past(retire_pulse);
endproperty
a_early : assert property (p_no_early_retire);
// P14: beat data is stable while the egress stalls.
property p_beat_stable;
@(posedge clk) disable iff (!rst_n)
(out_valid && !out_ready) |=> $stable(out_beat);
endproperty
a_beat : assert property (p_beat_stable);
// ---- CROSS-LINK INDEPENDENCE ------------------------------------------
// P15: NO DLLP IS FORWARDED. Section 1: DLLPs are handled within the DLL
// logic of the port that received them (§4).
property p_no_dllp_forward;
@(posedge clk) disable iff (!rst_n)
dut_ingress.dllp_received |-> !dut_egress.tlp_offered_this_cycle;
endproperty
a_dllp : assert property (p_no_dllp_forward);
// P16: an ingress ACK does not depend on the egress transfer (§3).
property p_ack_independent;
@(posedge clk) disable iff (!rst_n)
dut_ingress.ack_sent |-> !$past(egress_transfer_required);
endproperty
a_ack : assert property (p_ack_independent);
// P17: reset clears ownership -- no live slot, no locked owner.
property p_reset;
@(posedge clk)
!rst_n |=> ((busy_map == '0) && !owner_valid && !pkt.valid);
endproperty
a_reset : assert property (p_reset);P6, P8 and P9 are the three ownership properties this chapter turns on, and each has a measured failure rate behind it: 100.0%, the grant-mutation class, and 67.0%.
P4 with P4b keep metadata coherent — the route and the payload handle must not drift apart from the packet or from each other.
P15 and P16 are the cross-Link properties. They are unusual in asserting what does not happen, and they exist because §3's confusion is the most common one in switch debugging.
No liveness. "The egress becomes ready", "credit is granted" and "a packet is eventually delivered" are all environment properties — §9's head-of-line blocking is precisely a case where a packet legitimately waits indefinitely.
13. Verification, Fault Injection, and Model Verification
Executed before publication, and before the mutation table was written.
Slot allocator — 600,000 operations
Across SLOTS = 1, 2, 3, 8 against an independent set model: 0 disagreements, no double allocation, no invalid free accepted.
N×M arbitration — 13,500 runs
Every ingress×egress combination from 2×2 to 4×4 with random requests and stalls:
| Check | Violations |
|---|---|
| two ingress granted to one egress simultaneously | 0 |
| transfers exceeding offers (duplication) | 0 |
Packet lock — 40,000 two-stream runs
Multi-beat packets from two ingress ports into one egress:
| Arbiter | Malformed (interleaved) packets |
|---|---|
| packet-locked (§11) | 0 |
| re-picks every cycle | 26,783 — 67.0% |
Route snapshot — 60,000 stall/update sequences
| Implementation | Egress changed under stall |
|---|---|
| route snapshotted at acceptance | 0 |
| route re-read from the table | 33,993 — 56.7% |
Buffer release point — 60,000 sequences
Allocate, grant, stall 1–5 cycles, transfer, at two slots:
| Release point | Sequences with a corrupted payload |
|---|---|
| packet retirement (§6) | 0 |
| arbiter grant | 60,000 — 100.0% |
One note on that 100%. The first version of this model reported 0 for the buggy variant — it granted and retired in the same step, so no window existed. That was a measurement failure, not a result, and the corrected model introduces the stall the bug actually needs. The published figure is from the corrected run.
Directed tests
- One ingress, one egress — baseline; single-beat and long multi-beat packets.
- Two ingress contending for one egress — verify one owner and fair alternation (P7, P8). Required.
- Two independent egresses — verify concurrent transfer (the matrix's purpose).
- Egress stalled 1, 2, 50 cycles — verify grant, owner and beat stability (P8, P9, P14). Required.
fc_grantlow with a buffered packet — verify no transfer (P11). Required.- Egress Link not operational — verify no transfer (P10).
- Routing table rewritten while a packet is buffered — verify the egress is unchanged (P4). Required.
- Slot exhaustion — verify ingress backpressures rather than accepting and dropping (P5). Required.
- Retirement and allocation of the same slot in one cycle (§ same-cycle audit).
- Head-of-line scenario — a blocked destination ahead of a free one; verify the second waits (§9, and that this is correct behaviour for a single queue).
- Reset with buffered packets (P17).
The scoreboard assigns each injected TLP a unique test-only packet ID and checks that every accepted packet is forwarded exactly once, to its snapshotted egress — it never reads busy_map, owner or the packet record.
Mutations
| # | Mutation | Caught by | Symptom |
|---|---|---|---|
| 1 | slot freed when the route is selected | P6 | payload overwritten before transmission |
| 2 | slot freed at arbiter grant | P6 | payload corrupted — 100.0% (measured) |
| 3 | route re-read after buffering | P4 | packet leaves by the wrong port — 56.7% (measured) |
| 4 | selected ingress changes under stall | P8 | the request that transfers is not the one offered |
| 5 | source changes mid-packet | P9 | malformed TLP from two sources — 67.0% (measured) |
| 6 | lock released on something other than EOP | P9 | truncated packet, then a spurious fragment |
| 7 | two ingress granted one egress | P7 | interleaved or lost packets |
| 8 | one packet granted to two egresses | P7b | packet duplicated into the fabric |
| 9 | flow-control grant ignored | P11 | transmission without credit — a protocol violation |
| 10 | egress link state ignored | P10 | TLP offered to a Link not in L0 |
| 11 | packet accepted with no free slot | P5 | accept-then-drop |
| 12 | invalid free aliases slot 0 | P2 | a live packet's buffer released |
| 13 | non-power-of-two slot width aliases | P1 | two packets share a slot |
| 14 | packet count increments on valid | P3 | slot population drifts; switch wedges |
| 15 | packet retired at SOP | P13 | slot reused while the packet is still transmitting |
| 16 | reset leaves a live slot | P17 | a stale packet forwarded after reset |
| 17 | ingress ACK withheld pending egress transfer | P16 | source stalls on an unrelated Link's congestion (§3) |
| 18 | a DLLP forwarded as a TLP | P15 | a flow-control update injected into another Link |
| 19 | egress replay treated as a source retransmission | scoreboard | wrong Link blamed; wrong cable replaced (§5) |
| 20 | fixed priority starves one ingress | review + P8 | one port never progresses under sustained load |
| 21 | routing update mutates an in-flight packet | P4 | same as 3, from the software side |
| 22 | payload handle changes while stalled | P4b | metadata and payload describe different packets |
| 23 | head-of-line blocking described as a PCIe rule | review + §9 | queue architecture presented as protocol |
| 24 | egress credit observed by two producers | 20.5 P5 | one credit spent twice |
| 25 | metadata and payload owned by different retirement points | P4b, P6 | slot freed while metadata still live |
Same-cycle audit
| Case | Declared resolution |
|---|---|
| packet arrival + slot allocation | allocation is a precondition of acceptance (P5); no accept without a slot |
| retirement + allocation of the same slot | retire first, so the slot is immediately reusable (§7) |
| two ingress requests for one egress | one owner, chosen at SOP (P7, P9b) |
| selected packet + egress stall | grant, owner and beats all hold (P8, P9, P14) |
| final EOP + a new request | retirement completes; selection happens the following cycle |
| egress link-down + a selected packet | no transfer (P10); the packet stays owned |
| routing-table update + a buffered packet | the packet's snapshot wins (P4) |
| reset + a buffered packet | reset wins (P17) |
14. Debugging
Symptom → routing or forwarding? → which Link? → signal.
The first question is always which of §2's two stages, and the second is which Link — because §3 means an observation on one says little about the other.
Routing says the correct port and the packet never leaves
Routing is not the problem (§2). Read, in order:
| Check | Meaning |
|---|---|
fc_grant for the packet's class | credit-blocked (§8) — the commonest cause |
egress link_operational | the Link is not in L0 (18.6 §7) |
arbiter owner | another ingress holds the egress |
| head-of-line (§9) | a packet ahead of it is blocked |
Note the packet's class matters. Per-class credit (§1's INCH thresholds) means a Completion can flow while a Posted write waits, on the same Link.
The packet appears twice downstream
The retirement boundary — mutations 8 or 15.
Either one packet was granted to two egresses (P7b), or it was retired at SOP and the slot reallocated while it was still transmitting (P13).
The distinguishing experiment: tag packets with a test-only ID and check whether the duplicate carries the same ID (one packet, two transfers) or a different one (a slot reused too early, so the second packet inherited the first's metadata).
Corruption only under contention
Three candidates, all §13-measured, and they are distinguished by what is wrong.
Header right, payload from another packet → mutation 2, the slot freed at grant (100.0%).
Beats visibly interleaved → mutation 5, no packet lock (67.0%).
Correct packet, wrong port → mutation 3, the route re-read (56.7%).
All three appear only with concurrent traffic, which is why single-stream testing passes.
The analyzer shows A's TLP acknowledged and B never received it
Not a contradiction (§3), and this is the single most useful entry in this ladder.
A's ACK came from the switch's ingress Data Link Layer — it means the switch received the TLP correctly, and that A may free its replay copy. It says nothing about the egress side.
The packet may still be buffered, waiting for credit or arbitration (§8). Read the switch's internal state, not A's Link.
Replay errors on one Link only
Expected, and localizing (§5). Each Link has its own replay history.
A rising replay count on Switch↔B describes the Switch↔B channel — signal integrity, connector, or Link margin on that segment. It is not evidence about A, and replacing A's cable is a wasted afternoon.
Failure only after software changes routing windows
In-flight route snapshots — mutation 21, and §13 measured the exposure at 56.7%.
The tell is the timing: steady-state traffic is fine, and only packets buffered across a configuration change go wrong. The distinguishing experiment is to stall an egress and rewrite the routing table during the stall (P4).
15. Common Misconceptions
- "A switch forwards bits from port to port." It terminates one Link and originates on another (§3).
- "Routing and forwarding are the same operation." One is a lookup; the other is a pipeline with ownership (§2).
- "DLLPs pass through a switch." They are consumed by the port that receives them (§1, §4).
- "An upstream ACK means the endpoint received it." It means the switch did (§3).
- "An egress replay means the source retransmitted." Replay is Link-local (§5).
- "The route can be re-evaluated while the packet waits." 56.7% change destination (§13).
- "The buffer can be freed when the arbiter grants." 100.0% payload corruption (§13).
- "An arbiter may re-pick while the output is stalled." The grant must be owned (P8).
- "Beats from different packets can interleave." 67.0% malformed (§13).
- "Once buffered, flow control no longer applies." Egress credit is required to transmit (§8).
- "One FIFO per ingress has no performance cost." §1's own worked example shows the blocking (§9).
- "Store-and-forward means no internal pipelining." It describes the ownership boundary, not the implementation.
- "Cut-through is strictly better because it is faster." §1's caution: a bad TLP may already be gone (§6).
16. Understanding Check
17. What's Next
Forwarding is where the routing decision becomes a delivered packet, and it is a different problem entirely.
The switch terminates one Link and originates on another (§3), so credits, ACKs and replay are per-Link — which resolves the most persistent confusion in switch debugging (§5).
Three ownership rules carry the chapter, each measured: the route is packet metadata (56.7% without), the buffer is held until retirement (100.0% without), and the packet is locked SOP to EOP (67.0% without).
And routing never overrides credit (§8): a buffered packet with a known destination is still not legal to send.
Chapter 21.3 — Fabric Scaling asks what changes when one switch becomes a tree. The same virtual-bridge rules compose recursively — but bus ranges and address windows must nest correctly across levels, every hop adds latency and another place to stall, and a shared uplink means the downstream Links do not add up.
The idea to carry forward: an arbiter must own its decision until the transfer completes.