PCIe · Module 9
Address Assignment — Choosing a Base and Committing It
Sizing told the host how much space a Function needs; assignment picks a legal base and programs it. What makes a base legal, why alignment failures are silent, and how a 64-bit pair is committed without ever exposing a spliced address.
Chapter 9.4 ended with the host holding one number per BAR: how much address space that resource requires. It deliberately stopped there, because measuring and deciding are different acts.
After software knows how large a BAR-backed resource is, how does the system choose and program a legal host address for it?
1. Four Steps, Four Owners, One Chapter
Module 9 keeps colliding with a vocabulary problem: "the BAR is configured" can mean any of four different things. Here is the whole lifecycle, with this chapter's territory marked.
| Step | Who acts | What changes | Chapter |
|---|---|---|---|
| Sizing | host software | nothing permanent — the register is restored | 9.4 |
| Allocation | host software | nothing in any device — a decision is reached | 7.8 and §4 here |
| Programming | host software | the BAR holds the assigned base | this chapter |
| Access | host and Function | traffic reaches the resource | 9.6 |
Allocation and programming are the pair this chapter owns, and they are worth holding apart even though the same software does both in the same pass. Allocation is a decision problem over every Function in the hierarchy at once. Programming is a register write to one BAR. A system can get the first right and the second wrong, and the symptoms are completely different.
2. What Makes a Base Legal
Five constraints, and only one of them is about the Function on its own.
| Constraint | Where it comes from | Consequence if violated |
|---|---|---|
| Alignment — base is a multiple of the resource size | the BAR does not implement the low address bits (9.4 §2) | the write is silently rounded down; the window is not where software thinks |
| Non-overlap — no two windows claim the same address | 7.8 | two Functions answer the same access; the symptom appears at the innocent one |
| Containment — inside every parent routing window | 7.4, 7.8 §5 | the access never reaches the Function; the Function is blameless |
| Address-space kind — a memory resource in memory space | 9.3 | nothing matches; the number was never a complete coordinate |
| Representable reach — the window fits in the address width the BAR can express | 9.2 §2 | a 32-bit BAR cannot be placed above 4 GB at all |
Only alignment is enforced by the Function. The BAR physically cannot store a misaligned base, so a misaligned assignment fails quietly and locally — §15's first scenario. Every other constraint is a property of the whole system, and the Function has no way to check any of them: a BAR programmed to overlap another device's window, or outside its parent's routing window, looks exactly as correct from inside the Function as one programmed perfectly.
The Function validates alignment because alignment is the only constraint it can see. Everything else is the allocator's to get right, and nothing in the device will tell you if it did not.
3. Alignment, Derived
The rule "a BAR's base must be aligned to its size" is easy to memorise and easy to misapply. It is worth deriving once, because the derivation says why the failure is silent.
Start from the register. Chapter 9.4 §2 established that a Function whose resource is S = 2^n bytes implements address bits at and above bit n, and hardwires everything below to zero. That is not a convention — it is what makes sizing work.
Now try to program a base that is not a multiple of S. Such a base has at least one nonzero bit below bit n. Those bits are not storage. The write reaches the register, the low bits are discarded, and the value that lands is the largest multiple of S at or below what software wrote.
S = 64 KiB = 2^16
base software intended = 0xE001_8000
implemented address bits = 31:16
value actually stored = 0xE001_0000 ← 32 KiB lowerNothing reports an error. The write succeeded. The readback is self-consistent. The Function decodes a perfectly legal 64 KiB window — 32 KiB below where the allocator recorded it.
The general form. A window of size S = 2^n at base B is legal only if B mod S == 0, equivalently B & (S − 1) == 0, equivalently the low n bits of B are zero. All three are the same statement, and the third is the one the hardware implements.
4. A Worked Assignment
Two Functions, one region, and the three things that can go wrong.
The requirements, discovered by Chapter 9.4's probe:
| Function | Resource size | Alignment required |
|---|---|---|
| Device A | 64 KiB = 0x1_0000 | 64 KiB |
| Device B | 1 MiB = 0x10_0000 | 1 MiB |
The region the host has available for non-prefetchable memory below 4 GB: 0xE000_0000 through 0xE03F_FFFF — 4 MiB, itself 1 MiB-aligned.
A legal assignment
| Function | Base | Last address | Aligned? |
|---|---|---|---|
| Device B | 0xE000_0000 | 0xE00F_FFFF | 0xE000_0000 mod 1 MiB = 0 ✓ |
| Device A | 0xE010_0000 | 0xE010_FFFF | 0xE010_0000 mod 64 KiB = 0 ✓ |
Both inside the region, no overlap, both aligned. Placing the larger, more constrained window first is not an accident — it is Chapter 7.8 §4's packing argument, and it is why allocators sort by size descending.
Invalid — misaligned
Suppose the allocator places Device B immediately after Device A instead:
| Function | Base | Problem |
|---|---|---|
| Device A | 0xE000_0000 | fine — 64 KiB-aligned |
| Device B | 0xE001_0000 | 0xE001_0000 mod 1 MiB = 0x1_0000 ≠ 0 |
What actually happens if software writes it anyway. Device B implements address bits 31:20. Bits 19:0 of 0xE001_0000 are discarded, and the stored base is 0xE000_0000 — on top of Device A. A misalignment bug has become an overlap bug, silently, inside the device.
Invalid — overlap
| Function | Base | Last address |
|---|---|---|
| Device B | 0xE000_0000 | 0xE00F_FFFF |
| Device A | 0xE00F_0000 | 0xE00F_FFFF |
Device A is 64 KiB-aligned, so nothing is rounded and both BARs hold exactly what software wrote. Both Functions decode 0xE00F_0000–0xE00F_FFFF and both are correct to do so. Neither device is faulty; the allocation was.
Notice what distinguishes the three cases. The legal one and the overlapping one are indistinguishable from inside either Function — both hold the value software wrote, both decode a legal window. Only the misaligned one leaves evidence in the device, and it leaves that evidence as a changed readback.
5. The Assignment Sequence
The sequence, with its dependencies marked. Some of this ordering is forced; some is convention, and saying which is which matters more than the list itself.
- The Function is discovered and made accessible (Chapters 7.1–7.7).
- Each BAR's kind and form are identified — memory or I/O, 32-bit or 64-bit (9.2, 9.3). Forced: the form determines the slot stride, so a scan cannot proceed correctly without it.
- Each BAR's required size is discovered (9.4). Forced: step 4 cannot begin without it.
- The allocator chooses a base satisfying §2's constraints. Forced to be after step 3 for the whole hierarchy, not just for one Function — a decision for one window depends on every other window's requirement.
- The base is programmed into the BAR's address-bearing bits. This chapter.
- The BAR is read back and compared against what was written. Convention, and a good one — it is the only cheap check that catches §3's silent rounding.
- Parent routing windows are established so the range is reachable (7.4, §9 here). Forced relative to traffic, not relative to step 5.
- The relevant Command Register enable is set (8.4). Forced to be before traffic; independent of step 5.
- Operational access begins (9.6).
What is genuinely variable. Whether firmware assigns and the OS re-assigns; whether the whole hierarchy is sized before anything is programmed or handled subtree by subtree; how a host responds when the requirements do not fit. This chapter does not claim one universal ordering beyond the dependencies named above.
6. Programming a 32-Bit Memory BAR
The mechanics are almost anticlimactic, which is the point — the difficulty was all in choosing the number.
One configuration write to the BAR's offset, carrying the chosen base. The Function applies the two behaviours Chapter 9.4 §11 built:
- The read-only attribute bits are preserved. Bits 3:0 of a Memory BAR are read-only (9.2 §2). Whatever software puts there is discarded and the Function's implemented values stand. Software does not need to preserve them and cannot damage them.
- Unimplemented address bits are discarded. §3's rounding. This is where a misaligned base loses its low bits.
So the write is "just write the base" — including, harmlessly, whatever the allocator happens to have in the low bits, because the register will not keep them. The historical driver habit of read-modify-write on a BAR to preserve attributes is unnecessary for that purpose; the hardware already guarantees it.
And the readback is the only confirmation available. After the write, readback == (base & implemented_mask) | attributes. Comparing the address portion against the value written is step 6 of §5 and catches misalignment immediately.
7. Programming a 64-Bit Pair
One address, two configuration writes, and no way to make them simultaneous.
What the writes are. The lower dword's address-bearing bits are 31:4; the upper dword's are 31:0 and carry address bits 63:32 (9.2 §4). Software writes both. The attribute nibble in the lower dword is read-only and is preserved by the Function, as in §6.
8. Where the Command Register Sits
Chapter 8.4 established that Memory Space Enable is bit 1 of the Command Register, that it resets to 0, and that it controls whether the Function responds to memory-space accesses. Chapter 9.1 §3 established that programming and permission are independent.
Assignment is where that independence stops being a curiosity and starts being useful.
Programming a BAR grants nothing. After step 5 the Function knows where its window is and is still forbidden to serve it. That is not an inconvenience — it is the property that makes reprogramming safe, because it gives software a way to take the window out of service without changing the window.
The safe reprogramming sequence follows directly:
clear the relevant Command Register enable ← the Function stops responding
program the BAR (both dwords, if 64-bit) ← the splice is unobservable
read back and compare ← catches silent rounding
set the Command Register enable ← the Function resumes, with one coherent address9. A Legal BAR Is Not a Reachable One
The last constraint in §2 is the one with no local evidence at all.
An assigned base must lie inside every routing window between the Root Complex and the Function. A switch or bridge forwards a memory-space access downstream only if its address falls in the window that bridge has been programmed to forward (Chapter 7.4, Chapter 7.8 §5). Every level of the hierarchy applies its own window.
So there are two independent questions, and confusing them costs days:
| Question | Owner | How to check |
|---|---|---|
| Does the Function's decoder claim this address? | the Function | read the BAR; compute membership by hand |
| Does an access to this address reach the Function? | the hierarchy | inspect parent windows, level by level |
A perfectly programmed BAR outside its parent's window is silent, and looks identical to a broken device. The configuration path still works — configuration accesses are routed by identity, not by address (Chapter 7.7) — so the BAR reads back beautifully while no memory access ever arrives.
This is why allocation is hierarchical rather than per-device. The allocator cannot choose a Function's base independently; it must choose a base inside a range it has already committed to forwarding at every level above. Bridge window encodings are Type 1 header territory and are not published here; the architectural requirement is what this chapter needs, and it is simply containment at every level.
10. Microarchitecture — Two Clocks of a Different Kind
The design shape follows from the timing asymmetry, exactly as Chapter 9.1 §7 framed it, but now with a third element.
Assignment time happens once, during enumeration, at software's pace, with the Function not decoding. Everything expensive can happen here.
Run time happens on every inbound access. Nothing may be expensive here.
And between them sits a commit. The assignment-time state and the run-time operand are not required to be the same registers. Separating them is what lets a Function accept a partial update without ever exposing one.
11. RTL — Staged Programming With a Coherent Commit
// SYNTHESIZABLE. Assignment-time BAR programming for one memory resource,
// 32-bit or 64-bit, with a staged pair and a coherent commit.
// Read-only attribute bits, unimplemented-bit behaviour, and the meaning of
// Memory Space Enable: NORMATIVE.
// Shadow/commit microarchitecture and the commit trigger: ILLUSTRATIVE.
import bar_size_pkg::*; // Chapter 9.4 section 10 — one mask definition
module bar_program_state #(
parameter longint unsigned RESOURCE_SIZE = 64'h0000_0000_0001_0000, // 64 KiB
parameter bit MEM64 = 1'b1,
parameter bit PREFETCHABLE = 1'b0
) (
input logic clk,
input logic rst_n,
// Configuration write, already decoded to this BAR (Chapter 8.6).
input logic cfg_wr,
input logic cfg_wr_upper, // 0 = lower dword, 1 = upper dword
input logic [31:0] cfg_wdata,
input logic [3:0] cfg_be, // byte enables — APPLIED
// Command Register bit 1 (Chapter 8.4). Two roles here: the permission it
// always had, and — by this model's LOCAL contract — the commit trigger.
input logic mem_space_enable,
// Software-visible readback: always the SHADOW state, so a read-back-and-
// compare (section 5, step 6) sees what software just wrote.
output logic [31:0] cfg_rdata_lo,
output logic [31:0] cfg_rdata_hi,
// Runtime operand. Never a spliced pair, by construction.
output logic [63:0] active_base,
output logic base_valid,
// Diagnostic: the shadows have moved since the last commit, so the runtime
// base is no longer what software last wrote. Not a PCIe concept.
output logic base_stale
);
// ---- Shape, from Chapter 9.4's single definition --------------------
localparam logic [63:0] MASK64 = impl_mask64(RESOURCE_SIZE);
localparam logic [31:0] MASK_LO = impl_mask_lo(RESOURCE_SIZE);
localparam logic [31:0] MASK_HI = impl_mask_hi(RESOURCE_SIZE);
// NORMATIVE: bit 0 = 0 (memory), bits 2:1 = form, bit 3 = prefetchable.
localparam logic [3:0] ATTR_RO = {PREFETCHABLE,
(MEM64 ? 2'b10 : 2'b00),
1'b0};
generate
if (!size_is_pow2(RESOURCE_SIZE))
$error("RESOURCE_SIZE must be a nonzero power of two");
if (!size_meets_pcie_endpoint_min(RESOURCE_SIZE))
$error("A PCI Express Endpoint memory resource must be >= 128 bytes");
if (!MEM64 && (RESOURCE_SIZE > 64'h8000_0000))
$error("A 32-bit Memory BAR supports at most 2 GB");
endgenerate
// ---- Shadow state: what software has written ------------------------
logic [31:0] shadow_lo_q, shadow_hi_q;
logic written_q; // at least one programming write since reset
// ---- Active state: what the decoder uses ----------------------------
logic [63:0] active_q;
logic active_valid_q;
assign cfg_rdata_lo = shadow_lo_q;
assign cfg_rdata_hi = MEM64 ? shadow_hi_q : 32'h0000_0000;
assign active_base = active_q;
assign base_valid = active_valid_q;
// ---- Byte-enable merge, then the two normative constraints ----------
// Order is load-bearing: merge first, constrain second, so no enabled byte
// can defeat a read-only bit or set an unimplemented address bit.
logic [31:0] merged_lo, merged_hi;
always_comb begin
merged_lo = shadow_lo_q;
merged_hi = shadow_hi_q;
for (int b = 0; b < 4; b++) begin
if (cfg_be[b]) begin
merged_lo[8*b +: 8] = cfg_wdata[8*b +: 8];
merged_hi[8*b +: 8] = cfg_wdata[8*b +: 8];
end
end
merged_lo = merged_lo & MASK_LO; // unimplemented bits stay zero
merged_lo[3:0] = ATTR_RO; // read-only attribute nibble
merged_hi = merged_hi & MASK_HI; // the upper dword has no attributes
end
// In the 32-bit form the next dword is a DIFFERENT BAR and is never ours.
wire wr_lo = cfg_wr && !cfg_wr_upper;
wire wr_hi = cfg_wr && cfg_wr_upper && MEM64;
// The composed value the shadows currently describe.
wire [63:0] shadow_base = MEM64 ? {shadow_hi_q, shadow_lo_q & ~32'h0000_000F}
: {32'h0000_0000,
shadow_lo_q & ~32'h0000_000F};
// ---- Commit trigger (ILLUSTRATIVE LOCAL CONTRACT) -------------------
// The rising edge of Memory Space Enable. Software's safe sequence
// (section 8) is disable -> program -> enable, so the commit lands exactly
// when the pair is whole. PCIe mandates none of this.
logic mse_q;
wire mse_rise = mem_space_enable && !mse_q;
assign base_stale = active_valid_q && (active_q != shadow_base);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
shadow_lo_q <= {28'h000_0000, ATTR_RO};
shadow_hi_q <= 32'h0000_0000;
written_q <= 1'b0;
active_q <= 64'h0000_0000_0000_0000;
active_valid_q <= 1'b0;
mse_q <= 1'b0;
end else begin
mse_q <= mem_space_enable;
// Programming writes land in the SHADOWS only. The runtime base does
// not move here, in either order, however many times.
if (wr_lo) begin
shadow_lo_q <= merged_lo;
written_q <= 1'b1;
end
if (wr_hi) begin
shadow_hi_q <= merged_hi;
written_q <= 1'b1;
end
// Commit. One coherent address appears atomically.
if (mse_rise && written_q) begin
active_q <= shadow_base;
active_valid_q <= 1'b1;
end
end
end
endmoduleClassification: synthesizable.
Register semantics — every dimension:
| Dimension | Behaviour |
|---|---|
| Reset | shadows cleared except the attribute nibble; active base cleared; base_valid low |
| Read (software) | the shadow state, so read-back-and-compare sees what was written |
| Read (decoder) | the active state, which is never a splice |
| Write | shadow address bits only |
| Byte enables | applied — enabled bytes merge, disabled bytes are preserved |
| Fixed bits | lower dword bits 3:0 (normative); the upper dword has none |
| Unimplemented bits | masked away on the write path (normative behaviour, 9.4 §11's mechanism reused) |
| Commit | on the rising edge of mem_space_enable, if anything has been programmed |
| Side effects | a write moves the shadows and may raise base_stale; it never moves the active base |
| Hardware may update | no |
| Software may update | yes, through configuration |
| 32/64 relationship | with MEM64 low the upper dword is a different BAR and is never captured |
| Sizing behaviour | inherited from 9.4 via impl_mask64; not re-implemented |
Trace the safe sequence. Enable is clear. Software writes the lower dword, then the upper — or the reverse, or the lower twice. active_q does not move; base_valid stays as it was. Software reads both shadows back and compares. Software sets the enable. On that edge, active_q takes shadow_base in one step and base_valid rises. The runtime side has never seen a partial address.
Trace a violation of the sequence. Software writes a shadow while the enable is already high. The shadows move; active_q does not; base_stale rises. The Function keeps decoding the last committed — coherent — address, and the disagreement is visible on a wire instead of being a silent split between what software believes and what hardware does. Clearing and setting the enable commits the new base.
What it teaches — four things:
- Coherence is a structural property here, not a timing hope. There is no code path by which
active_qcan hold half of one address and half of another, because the only assignment to it takesshadow_basewhole. - Readback and runtime operand are deliberately different registers. Software must see what it wrote (to catch §3's rounding); the decoder must see only committed state. Serving both from one register forces one of those to be wrong.
- A policy needs a defined behaviour for its own violation. Writing while enabled is not forbidden — it is defined, and it is observable. A design that silently ignored the write, or silently committed it, would be equally coherent and much harder to debug.
- Ordering independence is worth designing for. Real software writes the pair in whichever order its data structures make convenient. A design that works under both orders has one less hidden dependency.
Deliberately simplified: one BAR; a single commit trigger with no alternative; no interaction with accesses already in flight when the commit happens; base_stale as a bare wire rather than a status bit software can read.
Production implication: a real Function instantiates this per BAR, chooses a coherence policy appropriate to whether it must be reprogrammed under traffic, defines what happens to accepted-but-unfinished accesses at a commit, and surfaces anything like base_stale through whatever debug interface it provides — or omits it and gates decode instead (9.2 §10).
12. RTL — Checking an Assignment Before Committing to It
The Function can only see alignment (§2). A Root Port, a hardware allocator, or a verification environment can see more — and the checks are cheap and easy to get wrong.
// SYNTHESIZABLE (and equally usable as a verification-side reference).
// A pure legality check on a proposed assignment. Every comparison is written
// so that no intermediate can wrap: there is no `base + size` anywhere.
// The constraint set is section 2's; the interface is illustrative.
module bar_assignment_check #(
parameter int ADDR_W = 64
) (
input logic [ADDR_W-1:0] base,
input logic [ADDR_W-1:0] size, // expected power of two
input logic bar_is_64, // 32-bit BARs cannot reach above 4 GB
// The region the allocator is placing within, inclusive.
input logic [ADDR_W-1:0] region_base,
input logic [ADDR_W-1:0] region_last,
output logic size_legal,
output logic base_aligned,
output logic fits_addr_space,
output logic fits_bar_form,
output logic within_region,
output logic assignment_legal,
// Valid only when fits_addr_space — see the comment below.
output logic [ADDR_W-1:0] last_addr
);
localparam logic [ADDR_W-1:0] ADDR_MAX = {ADDR_W{1'b1}};
localparam logic [ADDR_W-1:0] FOUR_GB_LAST = ADDR_W > 32
? {{(ADDR_W-32){1'b0}}, 32'hFFFF_FFFF}
: ADDR_MAX;
wire [ADDR_W-1:0] size_minus_1 = size - {{(ADDR_W-1){1'b0}}, 1'b1};
assign size_legal = (size != '0) && ((size & size_minus_1) == '0);
assign base_aligned = ((base & size_minus_1) == '0);
// OVERFLOW, checked WITHOUT computing base + size. Comparing against
// (MAX - (size-1)) can never wrap, because size >= 1 makes size-1 <= MAX.
// A design that computed `base + size - 1` and tested for a small result
// would be relying on the very wrap it is trying to detect.
assign fits_addr_space = size_legal && (base <= (ADDR_MAX - size_minus_1));
// A 32-bit Memory BAR carries address bits 31:4 and no upper dword, so its
// window must end at or below the 4 GiB boundary (Chapter 9.2 section 2).
assign fits_bar_form = bar_is_64
? 1'b1
: (size_legal && (base <= (FOUR_GB_LAST - size_minus_1)));
// Safe only once fits_addr_space holds; guarded so the output is never a
// wrapped value that a downstream comparison could believe.
assign last_addr = fits_addr_space ? (base + size_minus_1) : '0;
assign within_region = fits_addr_space
&& (base >= region_base)
&& (last_addr <= region_last);
assign assignment_legal = size_legal
&& base_aligned
&& fits_addr_space
&& fits_bar_form
&& within_region;
endmoduleClassification: synthesizable.
What it teaches — three things:
- Overflow is avoided rather than detected.
base <= ADDR_MAX − (size−1)cannot wrap for any legalsize, and it answers exactly the questionbase + size − 1 ≤ ADDR_MAXasks. Computing the sum and inspecting the result is the pattern that fails at the top of the address space — the one place it matters. last_addris guarded at its source. An unguarded end address is a value that looks usable and is wrong precisely when the window is near the top. Zeroing it when it is not meaningful means a downstream comparison cannot quietly consume nonsense.- The reasons are separate outputs.
assignment_legalalone tells an allocator to try again; the individual flags tell it why, which is the difference between a retry loop and a diagnosable failure.
Deliberately simplified: one window against one region — no cross-window overlap check, because that is a set problem over every assignment at once and belongs to the allocator (Chapter 7.8), not to a per-window checker. No I/O-space form; no prefetchable-vs-non-prefetchable region separation.
Production implication: this is normally software. A hardware allocator or a Root Port that assigns downstream resources needs it in logic, and a verification environment needs it as a reference regardless of where the real one lives.
13. Assertions
// SVA over bar_program_state and bar_assignment_check. Implementation
// invariants for THESE designs plus the normative behaviours they implement —
// not claims about PCIe beyond sections 2, 6 and 8. Every property refers to
// explicit RTL state or inputs.
// NORMATIVE — P1: address programming never changes the attribute bits.
// Bits 3:0 of a Memory BAR are read-only, whatever software writes.
property p_attrs_fixed_under_programming;
@(posedge clk) disable iff (!rst_n)
cfg_rdata_lo[3:0] == ATTR_RO;
endproperty
a_attrs_fixed : assert property (p_attrs_fixed_under_programming);
// NORMATIVE — P2: a write can never set an unimplemented address bit, so a
// misaligned base cannot be stored. Section 3's rounding, as an invariant.
property p_no_unimplemented_bits_stored;
@(posedge clk) disable iff (!rst_n)
((shadow_lo_q & ~(MASK_LO | 32'(ATTR_RO))) == 32'h0000_0000)
&& ((shadow_hi_q & ~MASK_HI) == 32'h0000_0000);
endproperty
a_bits_masked : assert property (p_no_unimplemented_bits_stored);
// CORRECTNESS — P3: whenever the active base is valid, it is aligned to the
// resource size. The chapter's central legality claim, structural here rather
// than checked, because a misaligned value cannot reach the shadows (P2).
property p_active_base_aligned;
@(posedge clk) disable iff (!rst_n)
base_valid |-> ((active_base & (64'(RESOURCE_SIZE) - 64'd1)) == 64'd0);
endproperty
a_active_aligned : assert property (p_active_base_aligned);
// COHERENCE — P4: the active base is never a spliced pair. It is always some
// value the shadows described as a whole — either the current shadow_base or
// the one committed earlier, never a mixture.
property p_active_never_spliced;
@(posedge clk) disable iff (!rst_n)
base_valid |-> (active_base == $past(shadow_base) || $stable(active_base));
endproperty
a_no_splice : assert property (p_active_never_spliced);
// COHERENCE — P5: a programming write never moves the runtime base. The
// staging contract of section 11, and the property that makes write order
// irrelevant.
property p_write_does_not_move_active;
@(posedge clk) disable iff (!rst_n)
((wr_lo || wr_hi) && !mse_rise) |=> $stable(active_base);
endproperty
a_write_stages_only : assert property (p_write_does_not_move_active);
// COHERENCE — P6: the active base changes only on a commit. Nothing else in
// the design may write it.
property p_active_changes_only_on_commit;
@(posedge clk) disable iff (!rst_n)
!$stable(active_base) |-> $past(mse_rise && written_q);
endproperty
a_commit_only : assert property (p_active_changes_only_on_commit);
// COHERENCE — P7: a commit takes the whole composed shadow value.
property p_commit_takes_whole_shadow;
@(posedge clk) disable iff (!rst_n)
(mse_rise && written_q) |=> (active_base == $past(shadow_base) && base_valid);
endproperty
a_commit_whole : assert property (p_commit_takes_whole_shadow);
// FORM — P8: a 32-bit BAR never captures the upper dword, and its active base
// has no upper half. The neighbouring slot belongs to a different BAR.
property p_mem32_has_no_upper;
@(posedge clk) disable iff (!rst_n)
(!MEM64) |-> ((shadow_hi_q == 32'h0000_0000)
&& (active_base[63:32] == 32'h0000_0000));
endproperty
a_mem32_no_upper : assert property (p_mem32_has_no_upper);
// SAFETY — P9: nothing changes without a configuration write or a commit.
// Runtime traffic cannot alter assignment state (Chapter 9.1's thesis).
property p_stable_without_cfg;
@(posedge clk) disable iff (!rst_n)
(!cfg_wr && !mse_rise) |=> ($stable(shadow_lo_q) && $stable(shadow_hi_q)
&& $stable(active_base));
endproperty
a_no_spontaneous_change : assert property (p_stable_without_cfg);
// SAFETY — P10: a byte with its enable low is preserved above the attribute
// nibble. The executable form of "byte enables are applied".
property p_byte_enables_respected;
@(posedge clk) disable iff (!rst_n)
(wr_lo && !cfg_be[3]) |=> (shadow_lo_q[31:24] == $past(shadow_lo_q[31:24]));
endproperty
a_be_respected : assert property (p_byte_enables_respected);
// DIAGNOSTIC — P11: base_stale means exactly what it claims. Catches an
// indicator that drifts from the condition it reports, which is worse than no
// indicator at all.
property p_stale_is_accurate;
@(posedge clk) disable iff (!rst_n)
base_stale == (base_valid && (active_base != shadow_base));
endproperty
a_stale_exact : assert property (p_stale_is_accurate);
// CHECKER — P12: a legal assignment never wraps the address space. Written
// against the guarded output, so a wrapped last_addr cannot satisfy it.
property p_legal_assignment_no_wrap;
@(posedge clk) disable iff (!rst_n)
assignment_legal |-> (last_addr >= base);
endproperty
a_no_wrap : assert property (p_legal_assignment_no_wrap);
// CHECKER — P13: a 32-bit BAR is never declared legal above the 4 GiB
// boundary. The form constraint of Chapter 9.2 section 2.
property p_mem32_within_4gb;
@(posedge clk) disable iff (!rst_n)
(assignment_legal && !bar_is_64) |-> (last_addr <= FOUR_GB_LAST);
endproperty
a_mem32_reach : assert property (p_mem32_within_4gb);
// SAFETY — P14: no output is ever unknown.
property p_outputs_never_unknown;
@(posedge clk) disable iff (!rst_n)
!$isunknown({active_base, base_valid, base_stale, assignment_legal});
endproperty
a_no_x : assert property (p_outputs_never_unknown);P4 is the chapter's coherence claim, and it is written to be hard to satisfy accidentally. The obvious formulation — "the active base equals the shadow base" — is false by design here, because the whole point is that they diverge between programming and commit. P4 instead says the active base is either a value the shadows described as a whole at some point or unchanged. A design that latched one dword directly into active_q fails it on the first partial write, which is precisely the state that must never be observable.
P6 and P7 are a pair and neither is sufficient alone. P7 says a commit does the right thing; a design could satisfy it and still have a second, wrong path that also writes active_q. P6 closes that by requiring every change to active_base to have been preceded by a commit — it asserts the absence of other writers, which is the kind of property that survives until someone adds one.
P11 asserts a diagnostic against its own definition, which sounds circular and is not. base_stale is produced by a continuous assignment today; if a later revision registers it, buffers it, or computes it from a stale copy, P11 fires. A debug output that can lie is worse than no debug output, because an investigation will trust it.
P12 is the arithmetic-safety property. It is deliberately written against last_addr rather than recomputing base + size, so a checker that wrapped would produce a last_addr below base and fail — which is the only externally visible symptom a wrap has.
14. Verification
Monitors observe: every configuration write with its dword select, data, and byte enables; both shadow registers; mem_space_enable; the active base with base_valid and base_stale; and every output of the assignment checker.
The scoreboard independently models the shadow state by applying its own byte-enable merge and its own implemented-bit mask derived from RESOURCE_SIZE, and independently composes the expected active base by bit extraction. It must not import bar_size_pkg, because sharing the mask function would make the scoreboard agree with the design about a mask off by one bit position.
Every scenario runs in both forms — MEM64 = 1 and MEM64 = 0 — because P8's failure exists only in the 32-bit configuration and the pair scenarios only in the 64-bit one.
Programming
- Legal aligned 32-bit assignment. Write, read back, verify the address portion equals what was written and the attribute nibble is untouched.
- Misaligned assignment. Write a base with nonzero bits below the resource size. Verify the readback differs from the value written — this is §3's silent rounding made loud, and it is the single most valuable test in the chapter.
- A base with every implemented bit set. The topmost legal placement for the form.
- Maximum-address placement. For the 64-bit form, a base whose window ends at the very top of the address space. Verify nothing wraps and the commit is exact.
- Write to the attribute nibble. All byte enables, low nibble different from the implemented one. Verify it is unchanged (P1).
- Partial-byte writes. Each of the fifteen nonzero byte-enable patterns, on each dword. Verify enabled bytes change, disabled bytes do not (P10), and the attribute nibble is still immovable.
- A 32-bit BAR with an address above 4 GB offered to the checker. Verify
fits_bar_formclears andassignment_legalis low (P13).
The pair
- Lower then upper, enable clear throughout, then enable. Verify the active base does not move during programming (P5) and commits whole at the enable edge (P7).
- Upper then lower. Verify identical results. The order-independence test, and the one a design with a hidden ordering assumption fails.
- Lower written twice, then upper. Verify only the last value participates in the commit.
- A partial pair — one dword written, then the enable set. Verify the commit takes the composed shadow value, which now mixes a new half with the reset value of the other. This is not a bug and the test must say so: the model commits what software actually programmed. The lesson is that staging protects against observing a splice, not against software forgetting a write.
- Programming while the enable is already high. Verify the active base does not move,
base_stalerises (P11), and a clear-then-set of the enable commits the new value. - Reset during an update. Assert reset between the two dword writes. Verify the shadows return to their reset values, the active base clears, and
base_validdrops.
Commit behaviour
- Enable set with nothing ever programmed. Verify no commit occurs and
base_validstays low. - Enable toggled repeatedly with no intervening writes. Verify the active base is stable and each commit is idempotent.
- Sustained runtime traffic with no configuration writes. Verify every piece of assignment state is stable (P9).
The assignment checker
- A legal assignment in the middle of a region. All flags set.
- Non-power-of-two size, and zero size. Verify
size_legalclears andassignment_legalis low. - Misaligned base. Verify
base_alignedclears. - A window whose last address is exactly the top of the address space. Verify
fits_addr_spaceholds andlast_addris correct — the case an unguardedbase + sizecomputation gets wrong. - A window that would wrap. A base near the top with a large size. Verify
fits_addr_spaceclears,last_addris zeroed, andassignment_legalis low (P12). - Region boundaries. A window starting exactly at
region_base, one ending exactly atregion_last, and one of each just outside.
Coverage should include: both forms; resource sizes from the 128-byte Endpoint minimum through a size near the address-space maximum; aligned and misaligned bases; all byte-enable patterns on both dwords; both pair-write orders plus the repeat and partial cases; the enable in both states during programming; commits with and without prior programming; and every individual checker flag observed both set and clear.
15. Debugging
Symptom: the BAR reads back a different address than software wrote
This is §3's silent rounding, and the readback is the evidence. It is also the only assignment fault that leaves any trace inside the device, which makes recognising it on sight worth a great deal.
Confirm it in one step. Compare the value written against the address portion of the readback. If the readback is the value written with some low bits cleared, the base was not aligned to the resource size.
Then establish which side is wrong:
- Compute the required alignment from the discovered size. A resource of
Sbytes requiresbase mod S == 0. If the intended base fails that test, software's allocator ignored alignment and the hardware behaved correctly. - If the intended base was aligned to the size software believes, then software and hardware disagree about the size — go back to Chapter 9.4 §15, because the sizing derivation is wrong and the alignment error is a downstream symptom.
- If more low bits were cleared than the size explains, the Function implements fewer address bits than its reported size requires. That is a Function bug and it will also have reported the wrong size.
What this fault becomes if nobody notices. The window sits below where the allocator recorded it, so the allocator may hand the vacated upper half to another Function — turning an alignment bug into an overlap bug (§4). The device that then misbehaves is not this one.
Symptom: a 64-bit BAR intermittently decodes the wrong address during reconfiguration
"Intermittently" and "during reconfiguration" together point at exactly one thing: the pair is being observed mid-update.
The ladder:
- Is decode disabled during programming? If the relevant Command Register enable is set while both dwords are written, any access arriving between them meets whatever the design does with a partial pair. Clearing the enable first (§8) removes the exposure regardless of everything below.
- What does the design do with a partial pair? Three answers are possible (9.2 §10 and §10 here): decode the splice, gate decode until complete, or stage and commit. Only the first produces this symptom. Determining which one you have is a design question with a one-line answer, and it resolves the investigation.
- Is the commit trigger firing when you think? In a staged design, check that the trigger occurs after both writes. A commit between the two writes commits a splice — coherently, and wrongly.
- Is software writing the pair at all? A reconfiguration that updates only the lower dword leaves the previous upper half. In a staged design that commits cleanly and is still the wrong address — §14's partial-pair test exists to make this distinction explicit.
- Is the address landing in a range the fabric routes here? Only after the local questions are settled.
The observation that separates a hardware coherence bug from a software sequence bug. Watch the runtime base across the two writes. If it changes twice, the design is not staging and the first change is the splice. If it changes once, at the commit, the design is coherent and the fault is in what software programmed or when it triggered the commit.
Symptom: the BAR is programmed correctly and the device is still unreachable
Everything local checks out, which is more informative than it looks: it eliminates the entire contents of this chapter.
What remains, in cost order:
- Is the relevant Command Register enable set? Programming grants nothing (§8). One configuration read settles it, and this is the most common cause by a wide margin.
- Do the parent routing windows contain the assigned range? §9. A perfectly programmed BAR outside its parent's window is silent, and configuration access still works because it is routed by identity rather than address.
- Is the host actually issuing accesses to the assigned physical address? A driver using a stale mapping, or the wrong Function's base, produces identical symptoms.
- Does the access reach the Function, and does the Function's decoder claim it? That is Chapter 9.6's ladder, and it is where this investigation hands off.
Why the handoff matters. Steps 1 and 2 are assignment-domain questions with cheap answers. Everything past them is runtime decode, and instrumenting the Function before checking a Command Register bit and a bridge window is the expensive way to reach the same conclusion.
16. Common Misconceptions
- "Sizing and assignment are the same step." Sizing measures and restores the register; assignment decides and writes. They run at different times, and the whole hierarchy must be sized before anything can be assigned.
- "A Function chooses its own system address during enumeration." The host chooses. A Function that picked its own base could not coexist with an identical second card — the problem the entire configuration model exists to solve.
- "Writing arbitrary low bits shifts the window slightly." Those bits are not implemented. The write is discarded and the window snaps down to the nearest multiple of its size — §3, and the reason that bug is silent.
- "A 64-bit BAR can be programmed as two unrelated resources." The two dwords are one address (9.2 §4). Programming one is programming half of one thing.
- "Assigning a BAR enables traffic." It does not. The Command Register enable is a separate, independent step (8.4), and that independence is what makes reprogramming safe.
- "A legal BAR value guarantees the address is reachable." Local decode correctness and end-to-end reachability are different properties with different owners (§9). Every parent window must also contain the range.
- "Reprogramming a base changes the resource's size." Size is fixed by the implementation and discovered once. Assignment moves a window of unchanged size, which is why sizing does not have to be repeated.
- "Programming a BAR writes something into the resource." It changes a mapping. There is no path from a configuration write to the payload behind the window — Chapter 9.1's thesis, and P9 asserts the converse here.
- "Any non-overlapping address will do." Non-overlap is one of five constraints (§2). Alignment, containment, address-space kind, and representable reach are all independently necessary.
- "Software must read-modify-write a BAR to preserve its attribute bits." The attribute bits are read-only in hardware; the Function preserves them whatever software writes (§6).
17. Understanding Check
18. What's Next
The resource has been described, its size discovered, a legal address chosen, and that address committed into the Function as one coherent base. The Command Register enable has been set.
Nothing has yet been accessed.
Chapter 9.6 — Host Access closes Module 9 by following an actual access: what the host issues, how it reaches the Function, how the Function turns a system address into an offset inside its own resource, and what the ladder is when it does not arrive.
The idea to carry forward: assignment produces one number and commits it; every other property of that number is a promise the allocator made and the Function cannot check.