PCIe · Module 25
BAR Problems — Where Does Address Ownership Break?
A BAR problem is an address-ownership mismatch, and it has two outcomes that need opposite debugging. Five ownership boundaries, the alias containment result, and why a truncated comparator is only fatal in company.
Chapter 25.2 §9 stopped at the moment a device's BAR windows are programmed and handed the next stage away. This chapter takes it.
A BAR problem is not a "device not responding" problem. It is an address-ownership mismatch — a disagreement about which agent owns a given address — and the whole of this chapter follows from that one framing.
1. Sources, Scope, and the Boundary With Module 9 and 23.2
2. A BAR Problem Is an Address-Ownership Mismatch
Start from the symptom vocabulary and throw most of it away. "The device doesn't respond." "Reads return all-ones." "The driver reads garbage." "It works until we enable the second BAR." These describe four different faults and none of them names a mechanism.
Every one of them is the same sentence underneath: some agent believed it owned an address, and either it was wrong, or nobody believed it.
That reframing does real work, because ownership is transitive and checkable. An address travelling from the CPU to a register passes through a chain of claims:
CPU physical address
-> the root complex claims it for PCIe (or for DRAM, or for nothing)
-> a bridge/switch window claims it for a downstream hierarchy
-> a device claims it with one of its BARs
-> that BAR's offset selects one local target
-> the target decodes the offset to a registerEach arrow is an agreement between two parties, and each can be broken alone. The chain is exactly 25.1 §2's method applied to a single address: find the last link that was provably correct, and the first that was not.
Two consequences follow immediately, and they organise the rest of the chapter.
First, a BAR problem is rarely inside the BAR. Of the five boundaries in §3, only two live in the endpoint's decoder. The other three are the parent window, the uniqueness of the claim, and the local target — and §15's cases show the parent window is the most common real-world culprit and the one engineers check last.
Second, the fault has a direction. An ownership mismatch either denies an address that should have been claimed, or grants one that should not have been. Those two produce opposite evidence and demand opposite instruments, and conflating them is the single most expensive mistake in BAR debugging. §4 is entirely about that split.
3. The Five Ownership Boundaries
The ordering is the point. These are not five independent tests you can run in any sequence — they are a pipeline, and an earlier failure masks every later one. A device whose parent window is wrong will look identical to a device whose BAR is wrong, because the request never reaches the BAR at all.
That is why §12's table reports the first disagreeing boundary, and why §15's cases proceed strictly outside-in. Checking the endpoint's BAR registers first is the natural instinct and it is the wrong order: you are inspecting boundary 2 while boundary 1 has already discarded the request.
Boundary 3 deserves a note, because it is the one that has no analogue in a single-BAR mental model. Uniqueness is not a property of any one BAR — it is a property of the set. Two BARs, each individually correct, each individually passing every self-check, can overlap. Neither one is broken. The configuration is. §10's decoder therefore reports three outcomes rather than two, and P7–P9 assert on the set rather than on any member.
Boundary 5 is where the chapter's most dangerous fault class lives, and it is the reason §4 exists.
4. Two Outcomes, and Why They Need Opposite Debugging
An ownership mismatch resolves in one of two directions, and everything about how you find it changes with the direction.
§12 measured all six fault classes against a golden decoder and sorted them by direction. The results are the reason this classification leads the chapter:
| Injected fault | Rejected an address it owns | Rejected at a different boundary | Accepted, wrong register |
|---|---|---|---|
| none (correct) | 0 | 0 | 0 |
| high address bits dropped | 0 | 36,098 | 0 |
| limit compare inclusive | 0 | 20,006 | 0 |
| two BARs claim it | 64,349 | 0 | 0 |
| local offset truncated | 0 | 0 | 87,414 |
| parent window one page short | 41,337 | 58,280 | 0 |
Read the middle column carefully, because it is the good news. "Rejected at a different boundary" means the fault was contained: a downstream check caught what an upstream one got wrong. An inclusive limit comparison (addr <= base + size) lets one byte too many past the comparator — and then the offset range check rejects it anyway, because off == size fails off < size. The bug is real, the outcome is still safe, and this is the strongest practical argument for layering the checks rather than collapsing them into one expression.
Only one fault in the table reaches Direction B, and it is the one where the offset is wrong rather than the claim. That is not a coincidence, and §5 shows why.
5. Width — the Alias, and Why Truncation Is Only Fatal in Company
The classic 64-bit BAR bug is described as "the comparator ignores the upper address bits". A 64-bit BAR is programmed above 4 GiB; the decoder compares only addr[31:0]; therefore — the story goes — an address that matches in its low 32 bits but differs above is wrongly claimed, and the device answers for memory it does not own.
The story is half right, and the half that is wrong matters.
Variant B is the one everyone calls the bug, and it rejected every single alias. The comparator did match — that part of the story is true. But the offset was computed at full width, so addr - base came out enormous, the range check off < size failed, and the request was rejected at boundary 4. The truncation was contained by the boundary downstream of it.
Variant C accepted all 200,000 and handed the local target an offset that is perfectly valid for the wrong address. Successful Completion, wrong register, no error bit anywhere. That is Direction B, and it is the only configuration of the three that produces it.
The lesson is not "never truncate". It is sharper and more useful:
The comparator and the offset must be computed at the same width, and a range check on the offset must follow both.
A design that truncates consistently is broken. A design that truncates the comparator but not the offset is self-containing — ugly, worth fixing, and not a data-corruption risk. Knowing which one you have changes the severity of the bug report, and you cannot tell them apart by reading the comparator alone. §10's bar_decode_checked computes both at parameterised width for exactly this reason, and P10 and P11 assert the two halves separately.
This also explains a confusing field observation. Engineers report 64-bit BAR aliasing bugs that "cause URs instead of corruption" and assume they have misdiagnosed. They have not. That is variant B behaving exactly as measured.
6. Boundary Arithmetic
Three addresses matter, and one of them is the bug. For size = 64 KiB at base = 0x1_0000_0000:
| address | mask comparison (addr & ~(size-1)) == base | base <= addr <= base + size |
|---|---|---|
base | true | true |
base + size - 1 (last valid byte) | true | true |
base + size (first invalid byte) | false | true |
The mask form is correct by construction and the inclusive form is off by exactly one byte. This is not a subtle arithmetic point — it is the direct consequence of a window of size bytes containing addresses base through base + size - 1.
Two things make it worth its own section.
The mask form only works because BARs are naturally aligned. (addr & ~(size-1)) == base is valid precisely because base has zeros in every bit below size, which 9.2 and 9.4 establish as a BAR property. A decoder that uses the mask form without asserting alignment has a hidden dependency, and P1 exists to make that dependency explicit rather than assumed. If a base is ever programmed misaligned — by a broken allocator, by a test, by a register write during a window where the BAR is live — the mask comparator does not degrade gracefully. It claims a differently-shaped region entirely.
And the off-by-one is contained, which is why it survives. §4 measured 20,006 affected probes and all of them were still rejected, one boundary later. An inclusive limit comparison can live in a design for years, passing every directed test, because the range check keeps cleaning up after it. It becomes visible only when someone removes the range check as redundant — and it is redundant, right up until it isn't.
7. What a Size Readback Proves
Chapter 9.4 owns the sizing handshake. This section asks a narrower, debugging-specific question: when you read a BAR back, what have you actually established?
The arithmetic, reproduced only for reference:
| size | 32-bit readback | (~readback) + 1 |
|---|---|---|
| 4 KiB | 0xFFFFF000 | 4,096 |
| 1 MiB | 0xFFF00000 | 1,048,576 |
| 256 MiB | 0xF0000000 | 268,435,456 |
A readback proves exactly one thing: which bits of the BAR are writable. That is the mechanism — low bits are hardwired to zero, so the first writable bit marks the size.
It does not prove any of the following, and every one of them is a real debugging error:
- It does not prove the BAR is enabled. Memory Space Enable is a separate control. A perfectly-sized BAR with decoding disabled claims nothing.
- It does not prove the base is programmed. Sizing and assignment are different operations (9.5); a device can report a correct size and hold a base of zero.
- It does not prove the parent window contains the assigned base. Boundary 1 is invisible from the endpoint's own registers — this is §15 case 1 and the single most common real fault.
- It does not prove the decoder implements the size it reports. The readback comes from the BAR register's writability. The comparator is separate logic. A device can advertise 1 MiB and decode 64 KiB, and nothing in the handshake catches it — which is why §10's
bar_size_selfcheckexists and P4 asserts the two agree.
That last point is the one worth internalising. The sizing handshake is a conversation with a register, not with the decoder. They are supposed to be derived from the same parameter. When they are not, every host-side check passes and every access above the real aperture is rejected.
8. The Waveform
An in-range probe, then one byte past the aperture
10 cyclesFour readings, and the third is the one to carry away.
win_ok asserts before bar_hit in both accesses. The ordering in the trace is the ordering in §3 — the parent window is upstream of the comparator, and a trace where bar_hit asserts without win_ok is a decoder that has been wired to bypass boundary 1.
In the first access all three checks agree and exactly one sel_valid results. That is the healthy signature, and P7's one-hot assertion is written directly against it.
In the second access bar_hit asserts and off_in_rng does not. This is the contained off-by-one from §6 drawn out in time. bar_hit alone is not evidence that the address was owned — a fact that matters enormously when someone probes only bar_hit on a debug port and concludes the decoder claimed an address it rejected.
And ur_out asserts two cycles after the failing probe, not in the same cycle. The delay is ordinary pipelining, and it is the reason §15's cases insist on correlating by request rather than by cycle: the UR you see is not necessarily caused by the access adjacent to it in the trace.
9. The Instruments, Named
Before the RTL, the list of what is being built and what question each answers. Every one exists because some boundary in §3 is otherwise unobservable.
| Instrument | Answers |
|---|---|
bar_window_check | did the parent window claim this address? (boundary 1) |
bar_decode_checked | does this BAR claim it, and at what width? (boundary 2) |
bar_set_decode | does exactly one BAR claim it? (boundary 3) |
bar_offset_guard | is the offset inside the aperture? (boundary 4) |
bar_target_decode | does that offset select a target that exists? (boundary 5) |
bar_size_selfcheck | does the advertised size match the decoded size? (§7) |
bar_first_reject | which boundary rejected first, sticky, per request |
bar_probe_engine | a directed sweep of the aperture edges (§15 case 4) |
The last two are the debugging instruments proper. The first six are a decoder written so that each boundary is separately observable; bar_first_reject is what turns that observability into a diagnosis, and it is the module §15 uses in seven of its nine cases.
10. RTL — The Decode Instruments
Block 1 — the parameter package and the boundary encoding. Every module below shares it, and the boundary enumeration is what makes bar_first_reject comparable across designs.
package bar_dbg_pkg;
// Boundary identifiers, ordered. A smaller value is further upstream, so
// "first rejection" is a minimum over the boundaries that rejected.
typedef enum logic [2:0] {
BND_NONE = 3'd0, // accepted by every boundary
BND_WINDOW = 3'd1, // parent/bridge window did not claim the address
BND_MATCH = 3'd2, // no BAR comparator claimed it
BND_UNIQUE = 3'd3, // more than one BAR claimed it
BND_OFFSET = 3'd4, // offset outside [0, size)
BND_TARGET = 3'd5 // offset selects a target that does not exist
} bar_bnd_e;
// Guarded width: $clog2(1) is 0 in some tools, which produces a zero-width
// vector. Every width in this chapter is computed through this function.
function automatic int unsigned gw(input int unsigned n);
return (n <= 1) ? 1 : $clog2(n);
endfunction
// The size encoding used throughout: a BAR of 2**LOG2_SIZE bytes.
// Expressing size as its log2 makes the alignment property structural —
// a base is aligned iff its low LOG2_SIZE bits are zero (see P1).
function automatic logic [63:0] size_mask(input int unsigned log2_size);
return ~((64'd1 << log2_size) - 64'd1);
endfunction
endpackageBlock 2 — the parent window check, boundary 1. It is deliberately a separate module from the BAR comparator, because in a real system it is separate silicon, and the most common BAR fault (§15 case 1) lives here rather than in the endpoint.
module bar_window_check #(
parameter int unsigned ADDR_W = 64
)(
input logic clk,
input logic rst_n,
input logic req_valid,
input logic [ADDR_W-1:0] req_addr,
// The window this hierarchy claims. Programmed by the parent bridge;
// NOT visible from the endpoint's own configuration space (§7).
input logic win_enabled,
input logic [ADDR_W-1:0] win_base,
input logic [ADDR_W-1:0] win_limit, // inclusive: last owned byte
output logic win_ok,
output logic win_reject
);
// Full-width comparison, both ends. The limit is INCLUSIVE and named so,
// because the whole of §6 is about which end is which.
always_comb begin
win_ok = win_enabled && req_valid &&
(req_addr >= win_base) && (req_addr <= win_limit);
win_reject = req_valid && !win_ok;
end
endmoduleBlock 3 — the BAR comparator, at explicitly parameterised width. §5's entire result is that the comparator width and the offset width must match, so this module takes one width and uses it for both, and exposes the offset it computed.
module bar_decode_checked #(
parameter int unsigned ADDR_W = 64,
parameter int unsigned CMP_W = 64, // width of the comparison
parameter int unsigned LOG2_SIZE = 20 // aperture is 2**LOG2_SIZE bytes
)(
input logic clk,
input logic rst_n,
input logic bar_enabled,
input logic [ADDR_W-1:0] bar_base,
input logic req_valid,
input logic [ADDR_W-1:0] req_addr,
output logic bar_hit,
output logic [ADDR_W-1:0] bar_offset,
output logic offset_in_range
);
import bar_dbg_pkg::*;
localparam logic [63:0] SZ = 64'd1 << LOG2_SIZE;
localparam logic [63:0] MASK = size_mask(LOG2_SIZE);
// The comparison, at CMP_W. CMP_W < ADDR_W is the truncated variant of §5.
logic [CMP_W-1:0] cmp_addr, cmp_base;
assign cmp_addr = req_addr[CMP_W-1:0];
assign cmp_base = bar_base[CMP_W-1:0];
always_comb begin
bar_hit = bar_enabled && req_valid &&
((cmp_addr & MASK[CMP_W-1:0]) == (cmp_base & MASK[CMP_W-1:0]));
// The offset is computed at FULL width regardless of CMP_W. This is
// variant B in §5, and it is what contains a truncated comparator:
// an aliased address produces an offset far outside the aperture.
bar_offset = req_addr - bar_base;
offset_in_range = bar_hit && (bar_offset < SZ);
end
endmoduleBlock 4 — the set decoder, boundary 3. Uniqueness is a property of the set, so this module is the only place it can be checked. It reports three outcomes, never two — the pattern 23.6 §5 establishes for any decoder that can be ambiguous.
module bar_set_decode #(
parameter int unsigned NBAR = 6
)(
input logic clk,
input logic rst_n,
input logic req_valid,
input logic [NBAR-1:0] bar_hit, // one bit per aperture
output logic sel_valid, // exactly one claimed it
output logic [bar_dbg_pkg::gw(NBAR)-1:0] sel_idx,
output logic sel_none, // nobody claimed it
output logic sel_ambig // more than one claimed it
);
import bar_dbg_pkg::*;
logic [gw(NBAR+1)-1:0] hit_count;
always_comb begin
hit_count = '0;
for (int i = 0; i < NBAR; i++) hit_count += bar_hit[i];
sel_none = req_valid && (hit_count == 0);
sel_ambig = req_valid && (hit_count > 1);
sel_valid = req_valid && (hit_count == 1);
// Priority encode only in the unambiguous case. Encoding an ambiguous
// set would silently pick the lowest index — which is exactly the
// failure mode mutation 14 injects, and it converts a detectable
// configuration error into Direction B corruption.
sel_idx = '0;
if (sel_valid) begin
for (int i = NBAR-1; i >= 0; i--) if (bar_hit[i]) sel_idx = gw(NBAR)'(i);
end
end
endmoduleBlock 5 — the local target decode, boundary 5, plus the size self-check of §7. Two small modules, together because they are the pair that catches a decoder disagreeing with its own advertised size.
module bar_target_decode #(
parameter int unsigned ADDR_W = 64,
parameter int unsigned LOG2_SIZE = 20,
parameter int unsigned NTARGET = 4
)(
input logic req_valid,
input logic [ADDR_W-1:0] bar_offset,
input logic offset_in_range,
output logic tgt_valid,
output logic [bar_dbg_pkg::gw(NTARGET)-1:0] tgt_idx,
output logic tgt_reject
);
import bar_dbg_pkg::*;
localparam int unsigned STRIDE_LOG2 = LOG2_SIZE - gw(NTARGET);
logic [ADDR_W-1:0] idx_full;
always_comb begin
idx_full = bar_offset >> STRIDE_LOG2;
tgt_valid = req_valid && offset_in_range && (idx_full < NTARGET);
tgt_idx = tgt_valid ? gw(NTARGET)'(idx_full) : '0;
// A target index at or above NTARGET means the aperture is larger than
// the targets behind it. That is a design error, not a host error,
// and it must be reported rather than wrapped (mutation 21).
tgt_reject = req_valid && offset_in_range && (idx_full >= NTARGET);
end
endmodule
module bar_size_selfcheck #(
parameter int unsigned LOG2_SIZE = 20 // what the DECODER implements
)(
input logic [31:0] bar_readback, // what the REGISTER advertises
output logic size_agrees,
output logic [63:0] advertised_size,
output logic [63:0] decoded_size
);
// The advertised size is derived exactly as §7 describes: the readback's
// writable field, inverted, plus one. Bits [3:0] carry BAR type
// information rather than address, and are excluded (see 9.2 and 9.4).
always_comb begin
advertised_size = {32'd0, (~(bar_readback & 32'hFFFF_FFF0)) + 32'd1};
decoded_size = 64'd1 << LOG2_SIZE;
size_agrees = (advertised_size == decoded_size);
end
endmoduleBlock 6 — the first-rejection recorder. This is the instrument the chapter exists to produce. It is sticky, per-request, and it records the first boundary to reject, which §3 argued is the only diagnosis that means anything.
module bar_first_reject #(
parameter int unsigned ADDR_W = 64
)(
input logic clk,
input logic rst_n,
input logic req_valid,
input logic [ADDR_W-1:0] req_addr,
// one bit per boundary, all sampled in the same cycle as req_valid
input logic rej_window,
input logic rej_match,
input logic rej_unique,
input logic rej_offset,
input logic rej_target,
input logic clear, // explicit, software-initiated
output bar_dbg_pkg::bar_bnd_e first_bnd,
output logic [ADDR_W-1:0] first_addr,
output logic captured,
output logic [31:0] reject_count // saturating
);
import bar_dbg_pkg::*;
bar_bnd_e this_bnd;
// The minimum boundary that rejected THIS request. Because BND_* is
// ordered upstream-to-downstream, "first" is a priority select, and the
// priority is the pipeline order in §3 — not an arbitrary choice.
always_comb begin
if (rej_window) this_bnd = BND_WINDOW;
else if (rej_match ) this_bnd = BND_MATCH;
else if (rej_unique) this_bnd = BND_UNIQUE;
else if (rej_offset) this_bnd = BND_OFFSET;
else if (rej_target) this_bnd = BND_TARGET;
else this_bnd = BND_NONE;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
first_bnd <= BND_NONE;
first_addr <= '0;
captured <= 1'b0;
reject_count <= '0;
end else if (clear) begin
first_bnd <= BND_NONE;
first_addr <= '0;
captured <= 1'b0;
reject_count <= '0;
end else if (req_valid && (this_bnd != BND_NONE)) begin
// Sticky: the FIRST failing request is kept, not the most recent.
// 25.1 §6 established why — the most recent failure is usually a
// consequence of the first, and overwriting destroys the cause.
if (!captured) begin
first_bnd <= this_bnd;
first_addr <= req_addr;
captured <= 1'b1;
end
// Saturating, so a wedged system does not wrap the evidence away.
if (reject_count != 32'hFFFF_FFFF) reject_count <= reject_count + 32'd1;
end
end
endmoduleBlock 7 — the aperture probe engine. §15's cases repeatedly need the same directed sweep: the edges of a window, and the aliases around it. Doing it by hand is where wrong conclusions come from, because the interesting addresses are exactly the ones a random sweep almost never generates.
module bar_probe_engine #(
parameter int unsigned ADDR_W = 64,
parameter int unsigned LOG2_SIZE = 20
)(
input logic clk,
input logic rst_n,
input logic start,
input logic [ADDR_W-1:0] bar_base,
output logic probe_valid,
output logic [ADDR_W-1:0] probe_addr,
output logic [3:0] probe_kind,
output logic done
);
localparam logic [63:0] SZ = 64'd1 << LOG2_SIZE;
// The eight addresses that matter. Every one of them is a boundary in
// §6 or an alias in §5; none of them is likely to appear in a random
// sweep, which is why 25.3's "prove the edges" rule applies here too.
localparam int unsigned NPROBE = 8;
logic [3:0] idx;
logic running;
always_comb begin
probe_addr = '0;
unique case (idx)
4'd0: probe_addr = bar_base; // first byte
4'd1: probe_addr = bar_base + SZ - 64'd1; // last valid byte
4'd2: probe_addr = bar_base + SZ; // first byte past
4'd3: probe_addr = bar_base - 64'd1; // last byte before
4'd4: probe_addr = bar_base ^ (64'd1 << 32); // 4 GiB alias
4'd5: probe_addr = bar_base + (SZ >> 1); // aperture midpoint
4'd6: probe_addr = bar_base + SZ + (SZ >> 1); // clearly outside
4'd7: probe_addr = bar_base | (SZ - 64'd1); // all offset bits set
default: probe_addr = bar_base;
endcase
probe_kind = idx;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
idx <= '0; running <= 1'b0; probe_valid <= 1'b0; done <= 1'b0;
end else begin
probe_valid <= 1'b0;
done <= 1'b0;
if (start && !running) begin
running <= 1'b1; idx <= '0;
end else if (running) begin
probe_valid <= 1'b1;
if (idx == 4'(NPROBE-1)) begin
running <= 1'b0; done <= 1'b1;
end else begin
idx <= idx + 4'd1;
end
end
end
end
endmoduleBlock 8 — the reference decoder, for the comparison §12 runs. A golden model is only useful if it is written independently of the design under test, so this one is expressed as a direct statement of §3's five boundaries with no shared code.
// verilog_lint: waive-start module-filename
module bar_golden_decode #(
parameter int unsigned ADDR_W = 64,
parameter int unsigned LOG2_SIZE = 20,
parameter int unsigned NTARGET = 4
)(
input logic [ADDR_W-1:0] addr,
input logic [ADDR_W-1:0] base,
input logic [ADDR_W-1:0] win_lo,
input logic [ADDR_W-1:0] win_hi,
input logic other_bar_hits,
output bar_dbg_pkg::bar_bnd_e verdict,
output logic [bar_dbg_pkg::gw(NTARGET)-1:0] target
);
import bar_dbg_pkg::*;
localparam logic [63:0] SZ = 64'd1 << LOG2_SIZE;
logic [ADDR_W-1:0] off;
// Written as five sequential questions, in §3's order, at full width and
// with no sharing of expressions with the DUT. This is deliberately the
// slowest possible formulation.
always_comb begin
off = addr - base;
target = '0;
if (!(addr >= win_lo && addr <= win_hi)) verdict = BND_WINDOW;
else if ((addr & size_mask(LOG2_SIZE)) != base) verdict = BND_MATCH;
else if (other_bar_hits) verdict = BND_UNIQUE;
else if (!(off < SZ)) verdict = BND_OFFSET;
else if ((off >> (LOG2_SIZE - gw(NTARGET))) >= NTARGET) verdict = BND_TARGET;
else begin
verdict = BND_NONE;
target = gw(NTARGET)'(off >> (LOG2_SIZE - gw(NTARGET)));
end
end
endmodule
// verilog_lint: waive-stop module-filename11. Assertions
Structural properties — the assumptions the decoders rest on.
// P1 — natural alignment. Every mask comparator in §10 is valid only
// under this, and §6 explained that it degrades ungracefully, not
// gradually, if violated.
property p1_base_aligned;
@(posedge clk) disable iff (!rst_n)
bar_enabled |-> ((bar_base & ((64'd1 << LOG2_SIZE) - 64'd1)) == '0);
endproperty
a_p1: assert property (p1_base_aligned);
// P2 — the aperture is a power of two. Stated over the decoded size so
// that a parameterisation error is caught at elaboration-adjacent time.
property p2_size_pow2;
@(posedge clk) disable iff (!rst_n)
(decoded_size != 0) |-> ((decoded_size & (decoded_size - 64'd1)) == '0);
endproperty
a_p2: assert property (p2_size_pow2);
// P3 — a disabled BAR claims nothing. Trivial to state, routinely broken
// by a decoder that qualifies the select but not the hit (mutation 2).
property p3_disabled_claims_nothing;
@(posedge clk) disable iff (!rst_n)
!bar_enabled |-> !bar_hit;
endproperty
a_p3: assert property (p3_disabled_claims_nothing);
// P4 — the advertised size equals the decoded size. §7's fourth
// non-proof: the sizing handshake talks to a register, not the decoder.
property p4_size_agrees;
@(posedge clk) disable iff (!rst_n)
bar_enabled |-> size_agrees;
endproperty
a_p4: assert property (p4_size_agrees);Boundary-ordering properties — §3's pipeline expressed as implications.
// P5 — the window is upstream of the comparator. A hit without a window
// claim means boundary 1 has been bypassed in the wiring.
property p5_hit_implies_window;
@(posedge clk) disable iff (!rst_n)
bar_hit |-> win_ok;
endproperty
a_p5: assert property (p5_hit_implies_window);
// P6 — the range check is downstream of the comparator, never the reverse.
property p6_range_implies_hit;
@(posedge clk) disable iff (!rst_n)
offset_in_range |-> bar_hit;
endproperty
a_p6: assert property (p6_range_implies_hit);
// P7 — exactly one select, or none, never several. This is boundary 3
// and it is a property of the SET.
property p7_select_onehot0;
@(posedge clk) disable iff (!rst_n)
req_valid |-> $onehot0(bar_hit);
endproperty
a_p7: assert property (p7_select_onehot0);
// P8 — the three set outcomes are mutually exclusive and total. A decoder
// that can report none of them, or two, has an unreachable request class.
property p8_set_outcomes_exclusive;
@(posedge clk) disable iff (!rst_n)
req_valid |-> $onehot({sel_valid, sel_none, sel_ambig});
endproperty
a_p8: assert property (p8_set_outcomes_exclusive);
// P9 — ambiguity is never resolved by picking. sel_idx must not be
// presented as meaningful when more than one aperture claimed the address.
property p9_no_pick_on_ambiguous;
@(posedge clk) disable iff (!rst_n)
sel_ambig |-> !sel_valid;
endproperty
a_p9: assert property (p9_no_pick_on_ambiguous);Width properties — §5, stated as two separate obligations.
// P10 — the offset is computed at full address width. This is the half
// of §5 that CONTAINS a truncated comparator, and it must hold even when
// CMP_W < ADDR_W.
property p10_offset_full_width;
@(posedge clk) disable iff (!rst_n)
bar_hit |-> (bar_offset == (req_addr - bar_base));
endproperty
a_p10: assert property (p10_offset_full_width);
// P11 — an accepted address agrees with the base in EVERY bit above the
// aperture. Stated at full width, this is the property a truncated
// comparator violates, independent of how the offset is computed.
property p11_upper_bits_agree;
@(posedge clk) disable iff (!rst_n)
offset_in_range |->
((req_addr & size_mask(LOG2_SIZE)) == (bar_base & size_mask(LOG2_SIZE)));
endproperty
a_p11: assert property (p11_upper_bits_agree);
// P12 — the comparator and the offset use the same width. Written as a
// consistency check rather than a parameter equality so that it holds for
// a decoder whose widths are computed rather than declared.
property p12_width_consistency;
@(posedge clk) disable iff (!rst_n)
(bar_hit && !offset_in_range) |-> (bar_offset >= (64'd1 << LOG2_SIZE));
endproperty
a_p12: assert property (p12_width_consistency);Boundary-arithmetic properties — §6.
// P13 — the last valid byte is inside.
property p13_last_byte_inside;
@(posedge clk) disable iff (!rst_n)
(req_valid && bar_enabled && (req_addr == bar_base + (64'd1 << LOG2_SIZE) - 64'd1))
|-> offset_in_range;
endproperty
a_p13: assert property (p13_last_byte_inside);
// P14 — the first byte past the aperture is outside. P13 and P14 are a
// pair; either one alone is satisfied by a decoder that is off by one in
// the direction the other would catch.
property p14_first_byte_past_outside;
@(posedge clk) disable iff (!rst_n)
(req_valid && (req_addr == bar_base + (64'd1 << LOG2_SIZE)))
|-> !offset_in_range;
endproperty
a_p14: assert property (p14_first_byte_past_outside);
// P15 — the byte immediately below the base is outside.
property p15_byte_below_outside;
@(posedge clk) disable iff (!rst_n)
(req_valid && (req_addr == bar_base - 64'd1)) |-> !offset_in_range;
endproperty
a_p15: assert property (p15_byte_below_outside);
// P16 — the offset never equals or exceeds the size when accepted. The
// direct statement of boundary 4, kept separate from P13/P14 because
// those are edge cases and this is the invariant.
property p16_offset_bounded;
@(posedge clk) disable iff (!rst_n)
offset_in_range |-> (bar_offset < (64'd1 << LOG2_SIZE));
endproperty
a_p16: assert property (p16_offset_bounded);Target properties — boundary 5.
// P17 — a selected target index is within range. The alternative is
// wrapping, which is Direction B (mutation 21).
property p17_target_in_range;
@(posedge clk) disable iff (!rst_n)
tgt_valid |-> (tgt_idx < NTARGET);
endproperty
a_p17: assert property (p17_target_in_range);
// P18 — a target is selected only for an in-range offset.
property p18_target_needs_range;
@(posedge clk) disable iff (!rst_n)
tgt_valid |-> offset_in_range;
endproperty
a_p18: assert property (p18_target_needs_range);
// P19 — an out-of-range target index is REPORTED, never silently dropped.
// 25.1 §6's rule: a condition that is detected and discarded is
// indistinguishable from one that was never detected.
property p19_target_reject_reported;
@(posedge clk) disable iff (!rst_n)
(req_valid && offset_in_range && ((bar_offset >> (LOG2_SIZE - gw(NTARGET))) >= NTARGET))
|-> tgt_reject;
endproperty
a_p19: assert property (p19_target_reject_reported);Recorder properties — the instrument must itself be trustworthy.
// P20 — the recorder captures the FIRST rejection, not the most recent.
// Once captured, the stored boundary and address are stable until clear.
property p20_first_is_sticky;
@(posedge clk) disable iff (!rst_n)
(captured && !clear) |=> (captured && $stable(first_bnd) && $stable(first_addr));
endproperty
a_p20: assert property (p20_first_is_sticky);
// P21 — the recorded boundary is the most upstream one that rejected.
// This is what makes the record a diagnosis rather than an observation.
property p21_records_most_upstream;
@(posedge clk) disable iff (!rst_n)
(req_valid && !captured && rej_window) |=> (first_bnd == BND_WINDOW);
endproperty
a_p21: assert property (p21_records_most_upstream);
// P22 — the counter saturates rather than wrapping. A wrapped counter
// reporting a small number is worse than no counter.
property p22_count_saturates;
@(posedge clk) disable iff (!rst_n)
(reject_count == 32'hFFFF_FFFF) |=> (reject_count == 32'hFFFF_FFFF);
endproperty
a_p22: assert property (p22_count_saturates);
// P23 — an accepted request never advances the reject count.
property p23_accept_no_count;
@(posedge clk) disable iff (!rst_n)
(req_valid && (this_bnd == BND_NONE)) |=> $stable(reject_count);
endproperty
a_p23: assert property (p23_accept_no_count);Cover — the anti-vacuity set.
// P24 — every request produces exactly one outcome: an accepted access or
// a recorded rejection. A request that produces neither has vanished
// inside the decoder, and no instrument downstream can tell that it did.
property p24_every_request_resolved;
@(posedge clk) disable iff (!rst_n)
req_valid |-> (sel_valid || win_reject || sel_none || sel_ambig || tgt_reject);
endproperty
a_p24: assert property (p24_every_request_resolved);
// P24's covers — the edge and alias cases must actually occur. Without these
// covers, P11, P13, P14 and P15 are all vacuously true against a
// testbench that only ever probes the middle of the aperture, which is
// the default behaviour of every random address generator.
c1_last_byte: cover property (@(posedge clk) disable iff (!rst_n)
req_valid && (req_addr == bar_base + (64'd1 << LOG2_SIZE) - 64'd1));
c2_first_past: cover property (@(posedge clk) disable iff (!rst_n)
req_valid && (req_addr == bar_base + (64'd1 << LOG2_SIZE)));
c3_alias: cover property (@(posedge clk) disable iff (!rst_n)
req_valid && (req_addr == (bar_base ^ (64'd1 << 32))));
c4_ambiguous: cover property (@(posedge clk) disable iff (!rst_n) sel_ambig);
c5_win_reject: cover property (@(posedge clk) disable iff (!rst_n) win_reject);
c6_tgt_reject: cover property (@(posedge clk) disable iff (!rst_n) tgt_reject);12. Measured Behaviour
Fault direction, 200,000 probes per row, against the golden decoder:
| Injected fault | Rejected an address it owns | Rejected at a different boundary | Accepted, wrong register |
|---|---|---|---|
| none (correct) | 0 | 0 | 0 |
| high address bits dropped | 0 | 36,098 | 0 |
| limit compare inclusive | 0 | 20,006 | 0 |
| two BARs claim it | 64,349 | 0 | 0 |
| local offset truncated | 0 | 0 | 87,414 |
| parent window one page short | 41,337 | 58,280 | 0 |
Alias containment, 200,000 aliased probes, 1 MiB aperture at 0x0000_0004_8000_0000:
| decoder variant | accepted | rejected | accepted with wrong data |
|---|---|---|---|
| 64-bit compare, 64-bit offset | 0 | 200,000 | 0 |
| low-32 compare, 64-bit offset | 0 | 200,000 | 0 |
| low-32 compare, low-32 offset | 200,000 | 0 | 200,000 |
Three readings.
The healthy row is zero in every column. That is the baseline check every measurement in this chapter depends on, and the first model defect above was caught precisely because the healthy row was not distinguishable from the faulty ones.
Only one fault reaches Direction B on its own. Truncating the offset corrupts. Truncating the comparator does not. §5 stated it; this is the measurement.
And the parent window fault appears in two columns at once, because a window that is short by one page both rejects addresses the device owns and changes which boundary rejects others. It is the messiest signature in the table, and §15 case 1 is about recognising it.
13. Executable Counterexamples
Two minimal designs, each violating exactly one property, each with the failing address stated so the result can be reproduced by hand.
Counterexample A — the consistently truncated decoder (violates P11).
// A 64-bit BAR whose comparator AND offset are both computed at 32 bits.
// This is variant C in §5. It is the only single fault in §12 that
// produces a Successful Completion carrying the wrong register's data.
module ce_a_truncated_both #(parameter int unsigned LOG2_SIZE = 20)(
input logic [63:0] addr, input logic [63:0] base,
output logic hit, output logic [31:0] offset, output logic in_range
);
localparam logic [31:0] M = ~((32'd1 << LOG2_SIZE) - 32'd1);
assign hit = ((addr[31:0] & M) == (base[31:0] & M));
assign offset = addr[31:0] - base[31:0]; // <-- the fatal half
assign in_range = hit && (offset < (32'd1 << LOG2_SIZE));
endmodule
// Failing stimulus: base = 64'h0000_0004_8000_0000, LOG2_SIZE = 20
// addr = 64'h0000_0005_8000_0040 (base ^ (1<<32)) + 64
// Golden: BND_MATCH — the upper bits disagree, nothing claims it.
// This: hit = 1, offset = 64, in_range = 1, target selected.
// P11 fails: (addr & mask) != (base & mask) yet offset_in_range is high.
// Observable consequence: Successful Completion, register at offset 64
// of an aperture the request never addressed.Counterexample B — the ambiguity that picks (violates P9 and P7).
// Two apertures overlap. The decoder priority-encodes unconditionally,
// so it resolves the ambiguity by silently preferring the lower index.
module ce_b_picks_on_ambiguous #(parameter int unsigned NBAR = 4)(
input logic req_valid,
input logic [NBAR-1:0] bar_hit,
output logic sel_valid,
output logic [1:0] sel_idx
);
always_comb begin
sel_valid = req_valid && (bar_hit != '0); // <-- "any", not "one"
sel_idx = 2'd0;
for (int i = NBAR-1; i >= 0; i--) if (bar_hit[i]) sel_idx = 2'(i);
end
endmodule
// Failing stimulus: bar_hit = 4'b0011 (apertures 0 and 1 both claim it)
// Golden: BND_UNIQUE — the configuration is ambiguous and must be reported.
// This: sel_valid = 1, sel_idx = 0. Aperture 1's register is never
// reached, and no error is raised at any layer.
// P7 fails ($onehot0 is false). P9 fails (sel_valid high while ambiguous).
// This is the fault §12 measured as 64,349 wrongly-rejected accesses when
// reported correctly — and as SILENT misdirection when it is not.Both counterexamples share a shape, and it is the one 23.6 §5 named: a decoder that cannot express "I don't know" will invent an answer. Counterexample A invents an owner for an address nobody owns; counterexample B invents a winner among two claimants. Neither reports anything.
14. Verification — Mutations
Thirty-two mutations. Every "Caught by" entry names properties defined in §11.
| # | Mutation | Symptom | Caught by |
|---|---|---|---|
| 1 | Base programmed misaligned | comparator claims a differently-shaped region | P1 |
| 2 | bar_hit not qualified by bar_enabled | a disabled BAR still claims addresses | P3 |
| 3 | Size parameter not a power of two | mask has holes; claim is non-contiguous | P2 |
| 4 | Decoder size ≠ advertised size | host maps more than the device decodes (§7) | P4 |
| 5 | Advertised size derived from the wrong bit field | sizing handshake reports a wrong aperture | P4 |
| 6 | Comparator wired before the window check | boundary 1 bypassed | P5 |
| 7 | Window limit treated as exclusive | last page of the window unreachable | P5, P13 |
| 8 | Window limit one page short | 41,337 wrongly rejected (§12) | P5 |
| 9 | Range check placed before the comparator | offset computed from an unmatched base | P6 |
| 10 | offset_in_range asserted without bar_hit | accepts on range alone | P6 |
| 11 | Comparator truncated to 32 bits, offset full width | contained; rejects aliases (§5 variant B) | P11 |
| 12 | Comparator and offset truncated | 200,000 of 200,000 accepted wrong (§5 variant C) | P10, P11 |
| 13 | Offset computed as base - addr | offset wraps; range check passes near the base | P10, P16 |
| 14 | Ambiguous set priority-encoded | silently prefers the lowest aperture | P7, P9 |
| 15 | sel_valid asserted on "any hit" | ambiguity resolved by invention | P8, P9 |
| 16 | sel_none and sel_ambig both derivable as false | a request class with no outcome | P8 |
| 17 | Uniqueness checked per-BAR instead of per-set | each BAR passes; the set is broken | P7 |
| 18 | Limit comparison inclusive (<=) | 20,006 affected; all still rejected (§12) | P14 |
| 19 | Last valid byte excluded | final byte of every aperture unreachable | P13 |
| 20 | Byte below base accepted | aperture effectively starts one byte early | P15 |
| 21 | Target index wrapped instead of rejected | offset selects an existing but wrong target | P17, P19 |
| 22 | Target selected without range check | out-of-aperture offset reaches a register | P18 |
| 23 | tgt_reject computed but not driven out | detected and discarded — invisible | P19 |
| 24 | Local offset truncated below aperture width | 87,414 accepted at the wrong register (§12) | P16, P17 |
| 25 | Recorder overwrites on each new rejection | keeps the last failure, loses the first | P20 |
| 26 | Recorder priority reversed (downstream first) | reports BND_TARGET for a window failure | P21 |
| 27 | Recorder captures address one cycle late | records the following request's address | P20 |
| 28 | Reject counter wraps at 32'hFFFF_FFFF | a wedged system reports a small count | P22 |
| 29 | Counter advances on accepted requests | every healthy device looks broken | P23 |
| 30 | clear implemented as a level, not a command | recorder never latches while software polls | P20 |
| 31 | Probe engine omits the alias probe | P11 and cover c3_alias never evaluate | P24 (c3) |
| 32 | Probe engine omits the first-byte-past probe | the off-by-one of §6 is never exercised | P24 (c2) |
Mutations 11 and 12 are the pair to study. They differ by one line — whether the offset is truncated — and they differ in outcome by everything. A mutation table that listed only "comparator truncated" would score both as the same defect, and §5's measurement is the argument that they are not.
Mutations 31 and 32 are different in kind from the rest. They do not break the design; they break the testbench, and their symptom is that assertions pass. They are in the table because 24.4 §5's rule applies here directly: a property that never evaluates is not a property that holds.
15. Debugging
Case 1 — every access to the device returns Unsupported Request; the BAR registers read back correctly.
The endpoint's registers are boundary 2 and they are fine. The request is not reaching them. Read the parent bridge's memory base and limit registers and confirm they enclose the assigned BAR range — 21.3 §5's containment property. §12 measured a window one page short producing 41,337 wrongly-rejected accesses out of 200,000, and the signature is exactly this one. This is the most common real fault in the chapter and the one checked last. Confidence: high when the BAR base is near a window edge.
Case 2 — accesses to the low part of the aperture work; the top page returns UR.
A window or aperture that is short by exactly one page. Probe base + size - 1 and base + size with bar_probe_engine (§10 Block 7, probes 1 and 2). If the last valid byte fails, the limit is being treated as exclusive somewhere (mutation 7 or 19). If the first byte past succeeds, the limit is inclusive by one (mutation 18) — and note from §12 that the latter is usually still contained downstream, so a failing last byte is the stronger signal.
Case 3 — reads return plausible but wrong values; no errors anywhere.
Direction B (§4). Stop looking at error logs; there will be none. Two candidates: the offset is truncated (mutation 24, measured at 87,414 wrong accesses) or the target index wraps (mutation 21). Discriminate by reading one register through two different offsets that alias under the suspected truncation — if both return the same value, the offset is losing bits. Confidence: high; this test has no false positives.
Case 4 — a 64-bit BAR above 4 GiB behaves oddly, but produces URs rather than corruption.
This is not a misdiagnosis. §5 measured it: a truncated comparator with a full-width offset rejects every alias, 200,000 of 200,000. You have variant B. It is a real defect worth fixing and it is not corrupting data. Confirm by probing base ^ (1 << 32) (probe 4) and checking that the rejection is recorded at BND_OFFSET rather than BND_MATCH — that boundary code is the fingerprint, and it is why bar_first_reject records which boundary rather than merely that one rejected.
Case 5 — the device works alone and fails when a second function or second BAR is enabled.
Boundary 3. Two apertures overlap. Neither BAR is individually wrong, which is why every per-BAR check passes. Read all apertures and compute the intersection by hand; then check whether the decoder reports ambiguity at all. If it priority-encodes (counterexample B), the lower-indexed aperture wins silently and the higher one appears dead. §12 measured 64,349 affected accesses.
Case 6 — the driver reports the right aperture size and accesses above some smaller boundary fail.
The sizing handshake and the decoder disagree — §7's fourth non-proof, mutation 4. The readback proved the register's writability, not the comparator's implementation. Sweep the aperture with bar_probe_engine and find the highest address that succeeds; the transition point is the decoder's real size. Confidence: high, and the fix is in the design's parameterisation rather than in configuration.
Case 7 — the first-rejection recorder reports BND_TARGET and you do not believe it.
Check the recorder before the design. Mutation 26 reverses the priority so that the most downstream boundary is reported, which turns a window failure into a target failure. The discriminator is a request that should fail at boundary 1 only: probe an address far outside the window (probe 6) and confirm the recorder reports BND_WINDOW. An instrument that has never been validated against a known fault is not evidence — this is 25.1 §5's rule applied to your own debug hardware.
Case 8 — accesses fail only after the device has been running for a while.
The BAR itself is almost certainly not involved. A decoder is combinational and does not degrade. Look for a base register being rewritten while the aperture is live (a reset sequence, a re-enumeration, a hot-reset path), or a window being reprogrammed by power management. Correlate the first rejection's timestamp with configuration writes rather than with traffic. Confidence: moderate; this case's value is in ruling the decoder out quickly.
Case 9 — the reject counter reads a small number on a system that has been failing for hours.
Suspect the counter before the conclusion. Mutation 28 wraps at full scale; a wrapped counter reporting 47 is worse than no counter, because it invites the inference that the fault is rare. Read the counter twice, separated by a known interval, and check that the delta is consistent with the observed failure rate. bar_first_reject saturates for this reason (P22), and the saturated value is itself the diagnosis: at least 4,294,967,295 rejections is a different statement from "47".
16. Misconceptions
"The BAR registers read back correctly, so the BAR is fine." They read back the register. The comparator is separate logic (§7, mutation 4), and the parent window is separate silicon (case 1). A correct readback rules out one of five boundaries.
"A 64-bit BAR bug corrupts memory." Only if the offset is truncated too (§5). Measured: the comparator-only variant rejected 200,000 of 200,000 aliases. Two different bugs, two different severities.
"If it were a BAR problem I'd see an error." Only in Direction A. §12 measured 87,414 accesses that returned Successful Completions carrying the wrong register (§4). The absence of errors is not evidence of correct decoding.
"Overlapping BARs would obviously fail." They fail silently if the decoder priority-encodes (counterexample B). Each BAR is individually correct; the set is not, and nothing checks the set unless you write P7.
"The off-by-one in the limit comparison is harmless — we've shipped it for years." It has been harmless because a downstream range check kept catching it (§12: 20,006 affected, all still rejected). It becomes a data-corruption bug the day someone removes the range check as redundant.
"Start by dumping the endpoint's configuration space." It is the easiest thing to read and boundary 2 of 5. §15 orders the cases outside-in for exactly this reason, and case 1 is the fault that hides from that dump entirely.
"Zero rejections means the decoder is correct." It means the decoder rejected nothing. Direction B produces zero rejections by construction, and mutations 31 and 32 produce zero because the interesting addresses were never probed (§14).
17. Understanding Check
Q1. A device returns Unsupported Request for every access, and its BAR registers read back exactly what the OS programmed. Where do you look, and why not at the BAR?
At the parent bridge's window (§15 case 1, §3 boundary 1). The BAR registers are boundary 2 and the readback confirms only that the register holds the right value — the request is being discarded upstream of it, so the endpoint never sees the address at all. §12 measured a window one page short producing 41,337 wrongly rejected accesses. The containment property in 21.3 §5 is the thing to verify: the parent window must enclose the assigned range, and that is invisible from the endpoint's own configuration space (§7).
Q2. Your 64-bit BAR sits above 4 GiB and the comparator ignores addr[63:32]. Is data being corrupted?
Not by that alone (§5). Measured over 200,000 aliased probes, the low-32 comparator with a full-width offset rejected every one: the comparator matched, addr - base landed far outside [0, size), and the range check caught it. Corruption requires the offset to be truncated to the same width — that variant accepted 200,000 of 200,000 and delivered an offset valid for the wrong address. The rule is that the comparator and the offset must be computed at the same width with a range check after both, and P10 and P11 assert the two halves separately.
Q3. Two BARs each pass every self-check and the device misbehaves only when both are enabled. What is the property nobody wrote?
Uniqueness — a property of the set, not of any member (§3 boundary 3, P7). Two individually-correct apertures can overlap, and a decoder that priority-encodes resolves the overlap by silently preferring one (counterexample B). No per-BAR assertion can catch it, because no per-BAR assertion can see the other BAR. $onehot0(bar_hit) is the whole fix, and §12 measured 64,349 affected accesses.
Q4. Reads return plausible values that are simply wrong, and no log anywhere shows an error. What class of fault is this and what does that rule out?
Direction B — an address granted to the wrong target (§4). It rules out every error-driven instrument: no UR, no status bit, no log entry, and 25.9 will show a byte-perfect trace. The only detection is comparing returned values against expected ones. §12's two Direction B faults were a truncated local offset (87,414 wrong accesses) and a wrapped target index — both of which return Successful Completion with correct length and Tag.
Q5. A design has shipped for years with addr <= base + size as its hit comparison. Why has nothing broken, and when will it?
Because a downstream range check has been containing it (§6, §12: 20,006 affected probes, all still rejected). The inclusive comparison lets exactly one byte past the comparator; off == size then fails off < size and the request is rejected one boundary later. It breaks the day someone removes the range check as redundant — and it is redundant, which is precisely why it gets removed. This is the strongest argument in the chapter for layering the checks rather than collapsing them.
Q6. The sizing handshake reports 1 MiB. What have you proved?
That the BAR register's low 20 bits are hardwired to zero (§7). Nothing more. It does not prove the BAR is enabled, that a base has been assigned, that the parent window contains the assigned base, or that the decoder implements 1 MiB — the handshake talks to a register and the comparator is separate logic (mutation 4, §15 case 6). A device can advertise 1 MiB and decode 64 KiB while every host-side check passes.
Q7. Your first-rejection recorder says BND_TARGET. What do you check before believing it?
The recorder (§15 case 7, mutation 26). Reversed priority reports the most downstream boundary instead of the most upstream, turning a window failure into a target failure. Validate it by probing an address far outside the window and confirming it reports BND_WINDOW. An instrument never validated against a known fault is not evidence — and P21 exists to state the priority obligation formally, because "which boundary rejected first" is the entire diagnostic value of the instrument.
18. Module 25 So Far
Five chapters, and this one changes the question.
| Chapter | Asks |
|---|---|
| 25.1 Debugging Overview | which layer, and what is the last provable event? |
| 25.2 Enumeration Failures | where did the configuration conversation stop? |
| 25.3 Link Training Failures | which state is it in, and which exit is missing? |
| 25.4 LTSSM Issues | which contract inside the state is wrong? |
| 25.5 (this) | which agent disagrees about who owns this address? |
The first four chapters debug a link that is not working. This one debugs a link that is working perfectly and delivering requests to the wrong place — which is why it is the first chapter in the module where the absence of errors is not evidence of health.
That shift brings a new obligation with it. 25.3 and 25.4 could rely on a fault announcing itself: a state that will not exit, a counter that will not advance. §4's Direction B announces nothing. The instrument has to be built before the bug appears, because after it appears there is no trace of it anywhere — and that is the argument for bar_first_reject existing in silicon rather than being added when someone files a bug.
And one result generalises past BARs. §5 measured a truncated comparator being contained by a full-width range check downstream. Layered checks at different widths do not merely duplicate each other; they convert silent corruption into visible rejection. The same shape appears in the next four chapters — a descriptor length checked against a buffer bound, a Completion checked against its request's identity, a credit checked against a reservation — and each time, the layer that looks redundant is the one that turns a Direction B fault into a Direction A one.
25.6 takes the next stage. A BAR gets a request to the right register; DMA is what the device does when it originates traffic of its own — and the ownership question moves from who owns this address to who owns this descriptor.