PCIe · Module 21
Routing — How a Switch Decides Which Port a Packet Leaves By
A switch has several ports and must pick exactly one for every packet. Three independent methods decide it — by address, by identifier, or implicitly — and each is a different lookup against a different set of registers.
Every chapter so far has assumed one Link. Two ports, point to point, and a packet leaving one end arrives at the other because there is nowhere else for it to go.
A switch breaks that assumption. It has several ports, and for every packet it must answer one question: which port does this leave by?
The question is harder than it sounds, because different packet types carry entirely different information about where they are going. A Memory Write carries an address. A Completion carries an identifier. A power-management message carries neither — and still has a destination.
So how does a switch decide, and what happens when the answer is "none of them" or "more than one"?
1. The Verified Sources
2. Every Port Is a Bridge
§1's model sentence is the foundation: "each PCI Express port is a virtual PCI-to-PCI bridge device, with its own set of PCI Express Configuration registers."
upstream port
(virtual P2P bridge)
│
─────────┴───────── internal virtual PCI bus
│ │ │
P2P P2P P2P downstream ports
│ │ │ (virtual P2P bridges)Three consequences, and they explain almost everything that follows.
Each port has its own windows. §1: "each downstream bridge has its own Base and Limit." Routing is therefore not one table but a set of per-port configurations — which is why §9's hardware is a CAM per method rather than a single decoder.
Software configures a switch by configuring bridges. There is no "switch driver" — enumeration walks P2P bridges, and the routing configuration is a side effect of assigning bus numbers and address windows. Chapter 21.4 owns enumeration at scale.
And the internal bus is a real routing destination. §1 shows Type 1 Configuration Requests targeting "the internal virtual PCI Bus" by matching the upstream bridge's Secondary Bus Number — so "which port" sometimes means "none; it is for a bridge register here" (§6).
3. Three Methods, Three Lookups
Different TLP types carry different destination information, so a switch performs different lookups.
| Method | Used by | Looks up | Against |
|---|---|---|---|
| Address | Memory and I/O Requests | the address | Base/Limit windows (§4) |
| ID | Completions, Configuration Requests | the Bus Number | Secondary/Subordinate ranges (§5) |
| Implicit | certain Messages | the TLP type | no window at all (§7) |
§1's three CAMs map one-to-one onto the first two rows — AMCAM for memory addresses, IOAMCAM for I/O addresses, BusNoCAM for bus numbers.
And the third row is the one people forget. A message with no address and no meaningful destination ID still has to go somewhere, and the answer comes from what kind of packet it is rather than from any field being compared.
4. Address Routing
§1's rule, in both directions:
downstream if the address falls within a downstream bridge's Base..Limit window
upstream if it does not fall within the upstream bridge's windowTwo windows per bridge, per §1: a non-prefetchable Memory Base/Limit pair, and a Prefetchable Memory Base/Limit pair — and the upstream decision explicitly accounts for both ("provided the address is not within the range defined by the Prefetchable Memory Base… and Prefetchable Memory Limit").
On the window-enable convention (§1: "when the Base is less than or equal to the Limit"): a bridge whose Base exceeds its Limit has no window — the range is disabled, not wrapped.
§12 measured what treating it as a wrapped range costs: 33.3% of random (base, limit, address) combinations produce a spurious match — so a disabled window would capture a third of all traffic.
5. ID Routing
§1's rule is two sentences and complete:
"Completions route by the Bus Number established in the Configuration registers. If the Bus Number is in the Secondary or Subordinate range, the packet goes downstream; otherwise, the packet goes upstream."
So ID routing is the same shape as address routing — a range check with an upstream default — against different registers.
Secondary Bus Number the bus immediately below this bridge
Subordinate Bus Number the highest bus number reachable below itA Completion's destination is a Requester it must return to, and that Requester lives at some Bus/Device/Function (Chapter 11.3). The switch compares only the Bus Number against each bridge's range.
Which is why the ranges must nest properly. Enumeration assigns them so that a bridge's Subordinate covers everything below it; §12 verified that non-overlapping ranges produce zero ambiguity across 200,000 lookups — and §8 is about what happens when they overlap.
And §1 shows where a switch's own identity comes from: the upstream port "captures the Type 0 Configuration Write Target Bus Number and Device Number" and uses them "as part of the Requester ID and Completer ID for the requests and completions generated by the upstream port." A switch learns its own address by being configured, not by knowing it.
6. Configuration Requests: Convert, Forward, or Refuse
§1 gives a complete three-way rule, and it is the most precisely specified routing decision in this chapter.
target Bus == this port's Secondary Bus Number
→ CONVERT the Type 1 Request into a Type 0 Request
(the target is ON the bus directly below this bridge)
Secondary < target Bus ≤ Subordinate
→ FORWARD the Type 1 Request unchanged
(the target is further down; some bridge below will convert it)
anything else
→ UNSUPPORTED REQUESTComputed boundary cases (§12), with Secondary = 8 and Subordinate = 11:
| Target bus | Result |
|---|---|
| 7 | Unsupported Request |
| 8 | convert to Type 0 |
| 9 | forward Type 1 |
| 11 | forward Type 1 |
| 12 | Unsupported Request |
And the degenerate case is instructive — Secondary = 8, Subordinate = 8, meaning nothing exists below that bus:
| Target bus | Result |
|---|---|
| 8 | convert to Type 0 |
| 9 | Unsupported Request |
Read the middle row of the first table again. The conversion happens exactly at the Secondary Bus Number, not across the range — because Type 0 means "you are the target", and only the devices on the immediately-adjacent bus are.
And the third outcome is a real, defined response. §1: "invalid, and is terminated… as an Unsupported Request (UR)." A Configuration Request to a bus that does not exist is answered, not dropped — which is what lets enumeration discover the shape of the hierarchy by probing.
7. Implicit Routing
Some Messages carry no address and no meaningful destination identifier. They are still routed — by what they are.
§1's example, verbatim: "PM_PME messages are always routed toward the Root Complex."
No lookup, no window, no comparison. The TLP type determines the direction, and every switch on the path applies the same rule independently.
Why this method has to exist: a device signalling a power-management event does not know the Root Complex's address, and there is no identifier it could put in a header that would mean "whoever is in charge." The routing information is the packet's kind.
This chapter publishes one sourced example and does not enumerate the rest (§1). What generalizes is the mechanism: a third routing class whose lookup is a type decode rather than a range check — which is why §9's RTL has three parallel paths and not two.
8. Exactly One Port, or It Is Misconfigured
9. The Routing Path
Three things to read out of the figure.
The classifier comes first, because the method is determined by the TLP type before any comparison happens.
The three lookups are parallel and independent — §1's three CAMs — and exactly one is consulted per packet.
And the resolver is a separate stage with three possible outcomes (§8), not a mux.
10. RTL — Types, Window Decoder and ID Decoder
// SYNTHESIZABLE. Normalized switch-routing types.
// THE ROUTING METHODS are canonical (§3, §1's three CAMs). The struct
// layouts and widths here are internal; no TLP header encoding is
// published in this chapter.
package switch_route_pkg;
parameter int N_DOWN = 4; // downstream ports
parameter int PW = (N_DOWN <= 1) ? 1 : $clog2(N_DOWN);
typedef enum logic [1:0] {
RM_ADDRESS = 2'd0, // Memory / I/O Requests -> AMCAM / IOAMCAM
RM_ID = 2'd1, // Completions, Config -> BusNoCAM
RM_IMPLICIT = 2'd2 // certain Messages -> by TLP type
} route_method_e;
typedef enum logic [2:0] {
TT_MEM_REQ = 3'd0, TT_IO_REQ = 3'd1, TT_CFG0 = 3'd2,
TT_CFG1 = 3'd3, TT_CPL = 3'd4, TT_MSG = 3'd5
} tlp_kind_e;
// Per-port window set. Section 1: "each downstream bridge has its own
// Base and Limit", and there are separate non-prefetchable and
// prefetchable Memory windows plus an I/O window.
typedef struct packed {
logic [63:0] mem_base, mem_limit;
logic [63:0] pref_base, pref_limit;
logic [31:0] io_base, io_limit;
logic [7:0] sec_bus, sub_bus; // ID routing (§5)
} port_cfg_t;
// ==================================================================
// A WINDOW IS ENABLED ONLY WHEN Base <= Limit.
//
// Section 1: "when the Base is less than or equal to the Limit".
// Section 12 measured the alternative -- treating Base > Limit as a
// WRAPPED range -- at 33.3% spurious matches, so a disabled window
// would capture a third of all traffic.
// ==================================================================
function automatic bit win_hit(input logic [63:0] base,
input logic [63:0] limit,
input logic [63:0] addr);
return (base <= limit) && (addr >= base) && (addr <= limit);
endfunction
// Section 1: "If the Bus Number is in the Secondary or Subordinate
// range, the packet goes downstream; otherwise, the packet goes
// upstream." Note the range is INCLUSIVE at both ends.
function automatic bit bus_hit(input logic [7:0] sec,
input logic [7:0] sub,
input logic [7:0] bus);
return (bus >= sec) && (bus <= sub);
endfunction
endpackageimport switch_route_pkg::*;
// SYNTHESIZABLE. Address routing -- section 1's AMCAM / IOAMCAM.
// THREE OUTCOMES, never a priority encode (§8). Section 12 measured
// randomized configurations: 26.1% one match, 67.3% NO match (upstream by
// default), 6.6% MORE THAN ONE -- and the last is a misconfiguration that
// a priority encoder would answer silently.
module route_address (
input port_cfg_t [N_DOWN-1:0] cfg,
input logic port_enabled [N_DOWN],
input tlp_kind_e kind,
input logic [63:0] addr,
output logic [N_DOWN-1:0] match_vec,
output logic hit_down,
output logic route_upstream,
output logic ambiguous,
output logic [PW-1:0] egress
);
wire is_mem = (kind == TT_MEM_REQ);
wire is_io = (kind == TT_IO_REQ);
always_comb begin
for (int i = 0; i < N_DOWN; i++) begin
match_vec[i] = port_enabled[i] && (
(is_mem && (win_hit(cfg[i].mem_base, cfg[i].mem_limit, addr)
|| win_hit(cfg[i].pref_base, cfg[i].pref_limit, addr)))
|| (is_io && win_hit({32'd0, cfg[i].io_base},
{32'd0, cfg[i].io_limit}, addr)));
end
end
// ==================================================================
// NO MATCH IS AN ANSWER, NOT AN ERROR (§4).
//
// Section 1: "The Request goes upstream if it does not target anything
// within the upstream bridge's Base and Limit range." A bridge knows
// only what is BELOW it; anything else is somebody else's problem.
// Section 12: this is the majority path at 67.3%.
// ==================================================================
assign hit_down = (is_mem || is_io) && $onehot(match_vec);
assign route_upstream = (is_mem || is_io) && (match_vec == '0);
assign ambiguous = (is_mem || is_io) && !$onehot0(match_vec);
// Meaningful ONLY on hit_down -- an ambiguous vector must not produce a
// plausible index that downstream logic might use anyway.
always_comb begin
egress = '0;
if (hit_down)
for (int i = 0; i < N_DOWN; i++) if (match_vec[i]) egress = PW'(i);
end
endmoduleimport switch_route_pkg::*;
// SYNTHESIZABLE. ID routing -- section 1's BusNoCAM.
// Same three-outcome shape, different registers: Secondary and
// Subordinate Bus Number rather than Base and Limit (§5).
module route_id (
input port_cfg_t [N_DOWN-1:0] cfg,
input logic port_enabled [N_DOWN],
input logic [7:0] target_bus,
output logic [N_DOWN-1:0] match_vec,
output logic hit_down,
output logic route_upstream,
output logic ambiguous,
output logic [PW-1:0] egress
);
always_comb
for (int i = 0; i < N_DOWN; i++)
match_vec[i] = port_enabled[i]
&& bus_hit(cfg[i].sec_bus, cfg[i].sub_bus, target_bus);
assign hit_down = $onehot(match_vec);
assign route_upstream = (match_vec == '0); // §1: "otherwise... upstream"
assign ambiguous = !$onehot0(match_vec);
always_comb begin
egress = '0;
if (hit_down)
for (int i = 0; i < N_DOWN; i++) if (match_vec[i]) egress = PW'(i);
end
// Section 12: with properly nested ranges, 0 ambiguity across 200,000
// lookups. With deliberately overlapping ranges (1-5 and 3-8), 29.9%.
endmoduleClassification: all three synthesizable.
win_hit encodes §1's enable convention — base <= limit — and §12 measured the alternative at 33.3% spurious matches.
Both decoders return the same three outcomes, because §8's rule is method-independent: one, none, or misconfigured.
Failure — five. A priority encoder answers ambiguity silently (29.9% exposure). Treating a miss as an error breaks every upstream transfer (67.3% of traffic). Treating base > limit as a wrapped range (33.3%). Ignoring the prefetchable window, so half the address map misroutes. And an egress computed regardless of hit_down.
11. RTL — Configuration Decision and Egress Resolver
import switch_route_pkg::*;
// SYNTHESIZABLE. Section 1's three-way Configuration Request rule.
// This is the most precisely sourced decision in the chapter, and the
// boundaries matter: section 12 walked them.
module route_config (
input tlp_kind_e kind,
input logic [7:0] target_bus,
input logic [7:0] this_sec_bus, // this bridge's Secondary
input logic [7:0] this_sub_bus, // this bridge's Subordinate
output logic convert_to_type0,
output logic forward_type1,
output logic unsupported_request
);
wire is_cfg1 = (kind == TT_CFG1);
// ==================================================================
// EXACTLY AT THE SECONDARY BUS NUMBER -> CONVERT.
//
// Section 1: "If the Configuration Request matches the ... downstream
// port Secondary Bus Number, the [switch] converts the Type 1
// Configuration Request into a Type 0 Configuration Request."
//
// Type 0 means "you are the target", and only devices on the
// immediately-adjacent bus are. Converting across the whole range
// would address the wrong devices entirely (§13, mutation 6).
// ==================================================================
assign convert_to_type0 = is_cfg1 && (target_bus == this_sec_bus);
// Section 1: "if the Bus Number does not match the Secondary Bus
// Number, but falls within the Subordinate Bus Number range, the
// Type 1 Configuration Request is forwarded ... unchanged."
assign forward_type1 = is_cfg1 && (target_bus > this_sec_bus)
&& (target_bus <= this_sub_bus);
// Section 1: "A Type 1 Configuration Request that targets a Bus Number
// that is not within range is invalid, and is terminated ... as an
// Unsupported Request (UR)."
//
// A DEFINED RESPONSE, not a drop -- which is what lets enumeration
// discover the hierarchy by probing (§6).
assign unsupported_request = is_cfg1 && !convert_to_type0 && !forward_type1;
endmoduleimport switch_route_pkg::*;
// SYNTHESIZABLE. Combine the three methods into ONE egress decision.
// Exactly one lookup is consulted per packet, chosen by TLP type (§3).
module egress_resolver (
input tlp_kind_e kind,
input logic addr_hit, addr_up, addr_amb,
input logic [PW-1:0] addr_port,
input logic id_hit, id_up, id_amb,
input logic [PW-1:0] id_port,
// Implicit: section 1 states PM_PME "always routed toward the Root
// Complex". No window is consulted; the TLP type IS the routing
// information (§7).
input logic msg_toward_root,
output route_method_e method,
output logic egress_valid,
output logic egress_upstream,
output logic [PW-1:0] egress_port,
output logic err_ambiguous,
output logic err_no_route
);
always_comb begin
method = RM_ADDRESS;
egress_valid = 1'b0;
egress_upstream = 1'b0;
egress_port = '0;
err_ambiguous = 1'b0;
err_no_route = 1'b0;
unique case (kind)
TT_MEM_REQ, TT_IO_REQ : begin
method = RM_ADDRESS;
egress_valid = addr_hit;
egress_port = addr_port;
egress_upstream = addr_up; // §4's default, NOT an error
err_ambiguous = addr_amb;
end
TT_CPL, TT_CFG1 : begin
method = RM_ID;
egress_valid = id_hit;
egress_port = id_port;
egress_upstream = id_up;
err_ambiguous = id_amb;
end
TT_MSG : begin
method = RM_IMPLICIT;
// No lookup, no comparison -- the type decides.
egress_upstream = msg_toward_root;
egress_valid = !msg_toward_root;
err_no_route = 1'b0;
end
// A Type 0 Configuration Request targets THIS bridge's own
// registers; it is not forwarded anywhere (§2).
TT_CFG0 : begin
method = RM_ID;
egress_valid = 1'b0;
egress_upstream = 1'b0;
end
default : err_no_route = 1'b1;
endcase
end
// INVARIANT (§8): at most one of {egress_valid, egress_upstream,
// err_ambiguous} is ever true. P7 asserts it.
endmoduleClassification: both synthesizable.
route_config's three outputs are mutually exclusive by construction, and §12 walked the boundaries: at Secondary 8 / Subordinate 11, bus 7 → UR, bus 8 → convert, bus 9 → forward, bus 11 → forward, bus 12 → UR.
And the resolver keeps upstream separate from valid. egress_upstream is a successful outcome, not a failure — which is §4's point expressed in signals.
Failure — four. Converting across the whole range rather than at the Secondary Bus Number exactly. >= instead of > in the forward condition, so the Secondary bus is both converted and forwarded. Dropping a UR instead of terminating it, which breaks enumeration's probing. And merging upstream into an error path.
12. Assertions
// SVA over the routing blocks. LOCAL contract only. Nothing asserts that
// software configures the windows correctly, that a downstream port
// accepts, or that any packet is ever delivered.
// ---- WINDOW SEMANTICS -------------------------------------------------
// P1: A WINDOW WITH Base > Limit IS DISABLED, not wrapped. Section 12
// measured the wrap interpretation at 33.3% spurious matches.
property p_disabled_window;
@(posedge clk) disable iff (!rst_n)
(cfg[i].mem_base > cfg[i].mem_limit) |-> !addr_match_mem[i];
endproperty
a_dis : assert property (p_disabled_window);
// P2: a match means the address really is inside an enabled window.
property p_match_sound;
@(posedge clk) disable iff (!rst_n)
match_vec[i] |-> (win_hit(cfg[i].mem_base, cfg[i].mem_limit, addr)
|| win_hit(cfg[i].pref_base, cfg[i].pref_limit, addr)
|| win_hit({32'd0,cfg[i].io_base}, {32'd0,cfg[i].io_limit}, addr));
endproperty
a_sound : assert property (p_match_sound);
// P2b: BOTH memory windows participate -- ignoring the prefetchable range
// misroutes half the address map (mutation 5).
property p_pref_window_used;
@(posedge clk) disable iff (!rst_n)
(win_hit(cfg[i].pref_base, cfg[i].pref_limit, addr) && port_enabled[i]
&& (kind == TT_MEM_REQ))
|-> match_vec[i];
endproperty
a_pref : assert property (p_pref_window_used);
// ---- THE THREE OUTCOMES -----------------------------------------------
// P3: NO MATCH ROUTES UPSTREAM. Section 1's default, and section 12's
// majority path at 67.3% -- it must never be an error.
property p_miss_is_upstream;
@(posedge clk) disable iff (!rst_n)
((kind inside {TT_MEM_REQ, TT_IO_REQ}) && (match_vec == '0))
|-> (route_upstream && !err_no_route);
endproperty
a_up : assert property (p_miss_is_upstream);
// P4: MORE THAN ONE MATCH IS REPORTED, NOT RESOLVED. Section 12: 29.9%
// against deliberately overlapping ranges.
property p_ambiguity_reported;
@(posedge clk) disable iff (!rst_n)
!$onehot0(match_vec) |-> (ambiguous && !hit_down);
endproperty
a_amb : assert property (p_ambiguity_reported);
// P5: a downstream route implies exactly one match, and the index is in
// range and actually set.
property p_onehot_egress;
@(posedge clk) disable iff (!rst_n)
hit_down |-> ($onehot(match_vec) && (egress < PW'(N_DOWN))
&& match_vec[egress]);
endproperty
a_one : assert property (p_onehot_egress);
// ---- ID ROUTING -------------------------------------------------------
// P6: the bus range is INCLUSIVE at both ends (§1, §5).
property p_bus_range_inclusive;
@(posedge clk) disable iff (!rst_n)
((target_bus == cfg[i].sec_bus) || (target_bus == cfg[i].sub_bus))
&& port_enabled[i] |-> id_match_vec[i];
endproperty
a_incl : assert property (p_bus_range_inclusive);
// ---- RESOLUTION -------------------------------------------------------
// P7: AT MOST ONE OUTCOME. valid, upstream and ambiguous are mutually
// exclusive -- the resolver's core invariant (§11).
property p_exclusive_outcome;
@(posedge clk) disable iff (!rst_n)
$onehot0({egress_valid, egress_upstream, err_ambiguous});
endproperty
a_excl : assert property (p_exclusive_outcome);
// P8: THE METHOD IS CHOSEN BY TLP TYPE, and only that method's result is
// used (§3).
property p_method_by_type;
@(posedge clk) disable iff (!rst_n)
((kind inside {TT_MEM_REQ, TT_IO_REQ}) |-> (method == RM_ADDRESS))
&& ((kind inside {TT_CPL, TT_CFG1}) |-> (method == RM_ID))
&& ((kind == TT_MSG) |-> (method == RM_IMPLICIT));
endproperty
a_meth : assert property (p_method_by_type);
// P8b: an implicitly-routed Message consults NO window (§7).
property p_implicit_no_lookup;
@(posedge clk) disable iff (!rst_n)
(kind == TT_MSG) |-> (!err_ambiguous && (egress_upstream == msg_toward_root));
endproperty
a_imp : assert property (p_implicit_no_lookup);
// ---- CONFIGURATION REQUESTS -------------------------------------------
// P9: THE THREE OUTCOMES ARE MUTUALLY EXCLUSIVE AND EXHAUSTIVE for a
// Type 1 Configuration Request (§6).
property p_cfg_exclusive;
@(posedge clk) disable iff (!rst_n)
(kind == TT_CFG1) |-> $onehot({convert_to_type0, forward_type1,
unsupported_request});
endproperty
a_cfg : assert property (p_cfg_exclusive);
// P10: CONVERSION HAPPENS EXACTLY AT THE SECONDARY BUS NUMBER -- not
// across the range (mutation 6).
property p_convert_exact;
@(posedge clk) disable iff (!rst_n)
convert_to_type0 |-> (target_bus == this_sec_bus);
endproperty
a_conv : assert property (p_convert_exact);
// P10b: forwarding is strictly ABOVE the Secondary and at most the
// Subordinate -- so the two never both fire (mutation 7).
property p_forward_range;
@(posedge clk) disable iff (!rst_n)
forward_type1 |-> ((target_bus > this_sec_bus) && (target_bus <= this_sub_bus));
endproperty
a_fwd : assert property (p_forward_range);
// P11: AN OUT-OF-RANGE CONFIGURATION REQUEST IS TERMINATED, NOT DROPPED.
// Section 1: "terminated ... as an Unsupported Request (UR)". Enumeration
// depends on getting a response (§6).
property p_ur_not_dropped;
@(posedge clk) disable iff (!rst_n)
((kind == TT_CFG1) && (target_bus != this_sec_bus)
&& !((target_bus > this_sec_bus) && (target_bus <= this_sub_bus)))
|-> unsupported_request;
endproperty
a_ur : assert property (p_ur_not_dropped);
// P12: a Type 0 Configuration Request is NOT forwarded -- it targets this
// bridge's own registers (§2).
property p_type0_not_forwarded;
@(posedge clk) disable iff (!rst_n)
(kind == TT_CFG0) |-> (!egress_valid && !egress_upstream);
endproperty
a_t0 : assert property (p_type0_not_forwarded);
// P13: routing is COMBINATIONAL and stateless here -- the same inputs
// always give the same egress. Any future pipelining must snapshot the
// configuration, which is Chapter 19.4 P5's rule.
property p_deterministic;
@(posedge clk) disable iff (!rst_n)
($stable(kind) && $stable(addr) && $stable(target_bus) && $stable(cfg))
|-> $stable({egress_valid, egress_upstream, egress_port});
endproperty
a_det : assert property (p_deterministic);
// P14: reset leaves no route asserted.
property p_reset;
@(posedge clk) !rst_n |=> (!egress_valid && !err_ambiguous);
endproperty
a_reset : assert property (p_reset);P3 and P4 are the pair that defines correct routing. P3 makes a miss a successful upstream route — §12's 67.3% majority path. P4 makes an overlap an error rather than a preference — 29.9% exposure.
P10 with P10b and P11 are §1's Configuration rule as three properties, and together they are exhaustive: every Type 1 Request converts, forwards, or is terminated.
And P13 is the forward-looking one. This decoder is combinational; Chapter 21.2's store-and-forward will pipeline it, and at that point Chapter 19.4 §13's snapshot rule applies — the configuration must be captured at the ownership boundary, not re-read.
No liveness. "The egress port accepts" is Chapter 21.2's problem, not this chapter's.
13. Verification, Fault Injection, and Model Verification
Executed before publication.
Address routing — 200,000 randomized configurations
| Outcome | Cases | |
|---|---|---|
| exactly one window | 52,130 | 26.1% → downstream |
| no window | 134,607 | 67.3% → upstream by default |
| more than one | 13,263 | 6.6% → misconfiguration |
Two things this measures. The upstream default is the majority path, so treating a miss as an error breaks two thirds of traffic. And ambiguity is not rare in randomized configurations, so a priority encoder answers 6.6% of lookups silently and wrongly.
The window-enable convention — 100,000 cases
Interpretation of Base > Limit | Spurious matches |
|---|---|
| disabled (§1's rule) | 0 |
| a wrapped range | 33,294 — 33.3% |
A disabled window read as wrapped captures a third of all traffic.
ID routing — 200,000 lookups
Properly nested ranges: 56.5% downstream, 43.5% upstream, 0 ambiguous.
Deliberately overlapping ranges (1–5 and 3–8), 100,000 lookups: 29,894 ambiguous — 29.9%.
Type 1 Configuration — 200,000 randomized cases
| Outcome | Share |
|---|---|
| convert to Type 0 | 1.4% |
| forward Type 1 | 5.6% |
| Unsupported Request | 92.9% |
Boundary walk at Secondary 8 / Subordinate 11: 7 → UR · 8 → convert · 9 → forward · 11 → forward · 12 → UR.
Degenerate case, Secondary 8 / Subordinate 8 (nothing below): 8 → convert · 9 → UR.
One-hot egress — 200,000 lookups across all three methods
With non-overlapping configuration: 0 results that were not exactly one egress.
Directed tests
- Address exactly at
base, exactly atlimit, one below, one above — the four boundaries, for both the non-prefetchable and prefetchable windows (P2, P2b). Required. - A window with
base > limit— verify it never matches (P1). Required. - An address in no window — verify upstream, and that no error is raised (P3). Required.
- Two overlapping windows — verify ambiguity is reported and nothing is forwarded (P4). Required.
- Bus number exactly at Secondary and exactly at Subordinate — verify both match (P6).
- Type 1 at every boundary from §13's table (P9–P11). Required.
- Type 1 with
sec == sub— the degenerate case. - Type 0 — verify it is not forwarded (P12).
- A Message — verify no window is consulted and no ambiguity can arise (P8b).
N_DOWN= 1, 2, 4 — index widths and one-hot logic at the boundary.- All ports disabled — verify everything routes upstream.
The scoreboard maintains an independent routing dictionary — a plain map from (method, key) to expected port, built from the configuration alone — and never reads match_vec, egress or the decoder outputs.
Mutations
| # | Mutation | Caught by | System symptom |
|---|---|---|---|
| 1 | overlapping windows priority-encoded | P4 | packet to a real but wrong port — 29.9% exposure (measured) |
| 2 | base > limit treated as a wrapped range | P1 | a disabled window captures 33.3% of traffic (measured) |
| 3 | a miss treated as an error | P3 | every upstream transfer breaks — 67.3% of traffic (measured) |
| 4 | a miss defaulted to port 0 | P3 | packets delivered to an unrelated endpoint |
| 5 | prefetchable window ignored | P2b | half the memory map misroutes |
| 6 | Type 1 converted across the whole range | P10 | Type 0 to devices that are not on the adjacent bus |
| 7 | forward condition uses >= instead of > | P10b | the Secondary bus both converts and forwards |
| 8 | an out-of-range Config Request dropped | P11 | enumeration hangs — the probe never answers (§6) |
| 9 | Type 0 forwarded downstream | P12 | a bridge's own register access leaves the bridge |
| 10 | ID routing uses Device/Function as well as Bus | scoreboard | Completions to the right bus, wrong port |
| 11 | bus range treated as exclusive at one end | P6 | the boundary bus is unreachable |
| 12 | Completions routed by address | P8 | a Completion has no address to route by |
| 13 | Messages routed through the address CAM | P8b | an implicit Message misroutes or is dropped |
| 14 | egress used without checking hit_down | P5 | ambiguous lookups produce a plausible index |
| 15 | disabled ports still matched | P2 | traffic to a port that is not operational |
| 16 | upstream and valid both asserted | P7 | the packet is duplicated or ambiguous downstream |
| 17 | configuration re-read mid-decision (once pipelined) | P13 | 19.4 §16's 57.8% class |
Same-cycle audit
| Case | Declared resolution |
|---|---|
| address matches a window and the configuration is being rewritten | this decoder is combinational and stateless; the result reflects the configuration presented that cycle (P13). Pipelining it requires a snapshot — 21.2's problem |
| address in both the non-prefetchable and prefetchable window of one port | one match — the same port; match_vec[i] is an OR, so no ambiguity arises |
| address in windows of two different ports | ambiguous (P4), reported |
Type 1 with target_bus == sec == sub | convert — the equality test wins, and forward requires strictly greater (P10, P10b) |
| a Message while an address lookup would also hit | the method is chosen by type (P8); the address result is not consulted |
| all ports disabled | everything routes upstream (P3) |
14. Debugging
Symptom → which method → which register → experiment.
The first question is always which routing method should this packet have used (§3), because the three consult entirely different registers and a wrong answer in one says nothing about the others.
Downstream traffic works, upstream traffic vanishes
Suspect the upstream default (§13's first counterexample) — mutation 3 or 4.
The asymmetry is the diagnosis. Downstream traffic matches windows and is routed; upstream traffic matches nothing and depends on the default being upstream rather than an error.
The distinguishing experiment: issue a Memory Write from an endpoint to an address deliberately outside every downstream window, and watch whether it appears at the upstream port. If it is dropped, the miss path is wrong — and P3 is the permanent check.
One endpoint is unreachable; its neighbours are fine
Read that bridge's windows and bus range.
If its Memory Base exceeds its Memory Limit, the window is disabled (§4) — which may be correct (nothing was allocated) or may be a configuration that never completed.
If its Secondary/Subordinate range does not contain the endpoint's bus, ID-routed traffic — Completions — cannot reach it, so Requests may go out and nothing comes back. That produces completion timeouts at the requester, which look like an endpoint fault.
The distinguishing experiment: compare the endpoint's assigned Bus Number against every bridge's sec/sub on the path. One gap anywhere breaks the return path only.
Traffic reaches the wrong endpoint
Overlapping configuration (§8) — mutation 1.
Read every port's windows and check for intersection. §13 measured 29.9% ambiguity against two overlapping bus ranges, and 6.6% against randomized address windows.
A switch that priority-encodes will not tell you, so the detection has to be external: dump the configuration and intersect the ranges yourself. P4 is what makes the switch report it instead.
And check when it happens. If it appears only during bring-up or after a hot-plug event, it is a transient overlap while one bridge has been updated and its neighbour has not (§8) — which is a software sequencing issue rather than a static misconfiguration.
Enumeration is extremely slow or never completes
§13's second counterexample — mutation 8.
Software probes buses that do not exist, and 92.9% of the probe space is out of range. If those are dropped instead of terminated with a UR, every probe costs a completion timeout instead of a fast response.
The distinguishing experiment: issue a Configuration Request to a bus number known to be outside every range and watch for a Completion. A UR should return promptly; silence is the bug.
A Completion never returns, though the Request clearly went out
ID routing, not address routing (§5).
The Request routed by address and arrived. The Completion routes by Bus Number — and if the bridges' Secondary/Subordinate ranges do not cover the requester's bus, the return path fails while the forward path works.
This asymmetry is the signature, and it is why §5 exists as a separate section: a packet and its Completion can take different routing methods and therefore fail independently.
15. Common Misconceptions
- "A switch has a routing table." It has per-port bridge windows — three lookups against three register sets (§1, §3).
- "Routing is one mechanism." Address, ID and implicit are independent (§3).
- "A miss is an error." It is the upstream default, and the majority path at 67.3% (§4).
- "A miss should go to port 0." It goes upstream (§4).
- "Overlapping windows are a priority question." They are a misconfiguration — 29.9% ambiguity measured (§8).
- "
Base > Limitmeans a wrapped range." It means disabled — 33.3% spurious matches otherwise (§4). - "Completions route by address." They carry none; they route by Bus Number (§5).
- "Type 1 converts anywhere in the range." Exactly at the Secondary Bus Number (§6).
- "An out-of-range Configuration Request can be dropped." It is terminated as a UR — enumeration depends on it (§6).
- "Messages need a destination field." Implicit routing uses the TLP type (§7).
- "A switch runs a routing protocol." Software configures bridge registers; the switch compares (§2).
- "If the Request arrived, the Completion will return." Different methods, independent failure (§14).
16. Understanding Check
17. What Module 21 Still Owns
This chapter answered one question: which port does a packet leave by?
| Chapter | What it uniquely owns |
|---|---|
| 21.2 Packet Forwarding | store-and-forward mechanics — buffering a TLP through a port, per-port flow control, the forwarding datapath, and the pipelining that makes §12's P13 matter |
| 21.3 Fabric Scaling | switch trees — depth, bandwidth aggregation, oversubscription |
| 21.4 Multi-Endpoint Systems | many endpoints — enumeration at scale, resource allocation, and how the windows this chapter reads get assigned in the first place |
What this chapter deliberately kept: the virtual-bridge model (§2), the three methods (§3), address windows and the upstream default (§4), bus ranges (§5), the Configuration three-way rule (§6), implicit routing (§7), and the one-hot requirement (§8).
What it deliberately declined: buffering and flow control between ports, anything about depth or aggregate bandwidth, how software computes the window assignments, non-transparent bridging, and peer-to-peer policy between downstream ports.
18. What's Next
A switch decides one thing per packet, and it decides it three different ways.
Address routing compares against per-port Base/Limit windows, with upstream as the default — §13's 67.3% majority path, and the rule that makes hierarchy scale.
ID routing compares a Bus Number against Secondary/Subordinate ranges, which is why a Request and its Completion can fail independently (§16).
Implicit routing consults nothing at all — the packet's type is the destination.
And every lookup must produce exactly one egress (§8), because ambiguity is a misconfiguration rather than a preference — 29.9% measured against overlapping ranges, and a priority encoder answers every one of them silently.
Chapter 21.2 — Packet Forwarding takes the next question. Deciding where a packet goes is not moving it there. A switch must buffer it, respect the egress port's flow control, and hand it on — and the moment that decision becomes pipelined, the configuration it was based on can change underneath it, which is Chapter 19.4 §13's snapshot problem arriving in a new subsystem.
The idea to carry forward: "not mine" is an answer, and a protocol that can say "nothing is there" should.