PCIe · Module 9
IO BARs — The Other Address Space
An I/O BAR uses the same placement mechanism as a Memory BAR but a different low-bit encoding and a different Command Register permission. Why two address spaces exist, why the same numeric address in the wrong space must not hit, and where I/O BARs stand in modern PCIe.
Chapter 9.2 decoded bit 0 of a Base Address Register and then immediately set aside the case where it reads 1.
This is that case.
What is an I/O BAR, how does its encoding differ from a Memory BAR, and why is I/O space a distinct architectural resource in the first place?
1. Two Address Spaces, and Why That Is Not an Accident
The awkward fact a designer meets first is that the same numeric value can name two different things depending on which space it is in. That looks like a defect. It is a deliberate architectural separation with a long history, and understanding why is what makes the decode rules memorable instead of arbitrary.
The origin is processor architecture, not bus design. Some processor families provide two distinct ways to reach a device: ordinary load and store instructions, which produce addresses in the memory space, and a separate class of I/O instructions, which produce addresses in a smaller, separate I/O space. The two are not different address ranges within one space — they are different spaces, distinguished by the transaction itself rather than by the number it carries.
Bus architectures had to carry that distinction outward, because a device reached by an I/O instruction has to be reachable, and the bus is the only thing between the processor and the device. So PCI defined transactions for both spaces, a permission bit for each, and a BAR encoding that says which one a given resource lives in.
PCIe inherited the whole structure along with the rest of the configuration model. It did not invent I/O space, and it did not remove it.
2. The Verified Field Map
Two details in that table repay a second reading.
Bit 1 is reserved, not an address bit. So the finest granularity an I/O BAR can express is 4 bytes — bits 1:0 are simply not available to describe a base. A design that treats bit 1 as address gets a window displaced by 2 bytes, which is the I/O equivalent of §9.2's low-nibble error and is even harder to see in a trace.
"A full 32-bit decode must still be done" is a requirement, not advice. A Function permitted to hardwire the upper 16 bits to zero is not permitted to stop comparing them. It must still reject an access whose upper 16 address bits are nonzero, even though its own base has zeros there. Skipping that comparison makes the window alias every 64 KB across the whole I/O space — a Function that answers accesses aimed at fifteen other places.
3. Memory BAR and I/O BAR, Compared
| Dimension | Memory BAR | I/O BAR |
|---|---|---|
| Address space | Memory Space | I/O Space |
| Bit 0 (read-only) | 0 | 1 — hardwired |
| Attribute/reserved low bits | bits 3:0 — indicator, type, prefetchable | bits 1:0 — indicator, reserved |
| Address-bearing bits | 31:4 (plus 31:0 of the upper dword in the 64-bit form) | 31:2 |
| Finest expressible granularity | 16 bytes | 4 bytes |
| Address width | 32-bit or 64-bit, selected by bits 2:1 | always 32 bits — no 64-bit form |
| Prefetchable attribute | bit 3 | none — bit 3 is an address bit |
| Dword slots consumed | 1, or 2 for the 64-bit form | 1 |
| Command Register permission | Memory Space Enable — bit 1 | I/O Space Enable — bit 0 |
| Normative consumption limit | none stated; power-of-two, naturally aligned, up to 2 GB in a 32-bit register | must not exceed 256 bytes per register |
| Transaction class that reaches it | memory-space requests | I/O-space requests |
The row that carries the chapter is the last one. Everything above it is encoding; that one is architecture. Two resources can be at the same numeric address, and the transaction class is what tells them apart.
And the permission row is the one that produces the most support tickets. I/O Space Enable and Memory Space Enable are different bits of the same register. Enabling the wrong one leaves a perfectly programmed Function silent, with a BAR that reads back correctly and a Command Register that looks configured.
4. Where I/O BARs Stand in Modern PCIe
Two failure modes are available here and this section avoids both. Treating I/O BARs as a historical footnote leaves an engineer unable to debug the real ones that still ship. Implying they are common leads people to design new ones.
What is normative. The I/O BAR encoding of §2 is part of the configuration header PCIe inherits. Nothing about it has been withdrawn from the header definition, and a Function may implement one.
What official PCIe endpoint IP documentation states, and this is the practically important constraint: I/O BARs are only enabled for a Legacy PCI Express Endpoint. The distinction is one PCIe itself draws — a Legacy Endpoint is a device that presents a PCIe interface while retaining behaviours from the earlier bus model, and generating or responding to I/O transactions is one of those behaviours. A non-legacy PCI Express Endpoint does not use I/O BARs.
Two adjacent facts from the same documentation, which sharpen the picture:
- The minimum memory address range supported by a BAR is 128 bytes for a PCI Express Endpoint and 16 bytes for a Legacy PCI Express Endpoint — the Legacy variant keeps the older, finer minimum.
- The minimum I/O space allowed is 16 bytes, and the guidance is explicit that use of I/O space should be avoided in all new designs.
What is platform behaviour and varies. Whether a given host bridge implements I/O transactions at all. Some do not — and on such a platform an I/O BAR can be programmed, enabled, and still unreachable, because nothing in the path will ever issue an I/O-space request to it. That is not a Function bug and no amount of investigation inside the Function will find it. §11's first scenario exists because this failure looks exactly like a Function that is broken.
5. Space as a Coordinate
The figure has no path from one row to the other, and that is its whole point. A memory request cannot arrive at the I/O gate and an I/O request cannot arrive at the memory gate. The two rows carry the same numeric address and never meet.
The BAR kind sits between them because it decides which row this resource participates in — not which row a request travels down. A request's space is a property of the request; the BAR's kind is a property of the Function. A hit needs both to agree.
6. Microarchitecture — The Space Is Not a Hint
The temptation in an implementation is to carry the address and treat the space as metadata to be checked later, or logged, or assumed from context. Every version of that is a bug waiting for the right address.
The space belongs in the comparison, at the same level as the address. Concretely, a decoder's match condition is a conjunction of both coordinates:
hit ⟺ (request space == window space) ∧ (address ∈ window)Not address membership followed by a space check somewhere downstream, and not a shared comparator with a space flag added to the result. The two are equal partners, and writing them that way is what makes §9's P1 and P2 hold structurally rather than by inspection.
Why the distinction is not pedantic. A design that computes address membership first and qualifies it later has, for at least one logic stage, a signal that says "this address is mine" when it is not. If anything else consumes that signal — a prefetch, a counter, a debug trap, an early wake of the resource block — the space check never happens for that consumer. The bug is then in a path nobody was looking at, triggered only when a numeric address collides across spaces.
And a Function may legitimately own both. The specification is explicit that a device mapping control functions into both spaces implements two Base Address Registers, one of each kind. That Function has two windows whose numeric ranges may overlap completely and which are nonetheless distinct resources.
7. RTL — Classifying a BAR by Its Low Bits
// SYNTHESIZABLE. Classifies one Base Address Register dword and selects the
// correct address interpretation for its kind.
// Low-bit formats and the bits 2:1 type encoding: NORMATIVE (PCI Local Bus
// Specification Rev 3.0, section 6.2.5.1, Figures 6-5 and 6-6, Table 6-4).
// Module boundary, port names, and the legality output: illustrative.
module bar_kind_decode (
input logic [31:0] bar_lo,
output logic kind_io,
output logic kind_mem32,
output logic kind_mem64,
output logic kind_reserved,
// The bits of THIS dword that are not address, for the decoded kind.
output logic [31:0] attr_mask,
// The address-bearing bits of this dword, with the rest cleared.
output logic [31:0] lo_addr,
// The encoding is one this model can interpret and is internally consistent.
output logic encoding_legal
);
// ---- NORMATIVE, and DIFFERENT for the two spaces -------------------
// Memory BAR: bits 3:0 are indicator, type and prefetchable.
localparam logic [31:0] MEM_ATTR_MASK = 32'h0000_000F;
// I/O BAR: bit 0 is the indicator, bit 1 is reserved. Neither is address,
// so the finest granularity an I/O BAR can express is 4 bytes.
localparam logic [31:0] IO_ATTR_MASK = 32'h0000_0003;
localparam logic [1:0] TYPE_MEM32 = 2'b00;
localparam logic [1:0] TYPE_MEM64 = 2'b10;
wire [1:0] type_field = bar_lo[2:1];
assign kind_io = bar_lo[0];
assign kind_mem32 = !bar_lo[0] && (type_field == TYPE_MEM32);
assign kind_mem64 = !bar_lo[0] && (type_field == TYPE_MEM64);
assign kind_reserved = !bar_lo[0] && (type_field != TYPE_MEM32)
&& (type_field != TYPE_MEM64);
// The mask is selected by the kind, NOT shared between them. Using the
// memory mask on an I/O BAR would discard address bits 3:2; using the I/O
// mask on a memory BAR would carry attribute bits 3:2 into the address.
// Both errors are silent and both move the window.
assign attr_mask = kind_io ? IO_ATTR_MASK : MEM_ATTR_MASK;
// NORMATIVE: bit 1 of an I/O BAR is reserved and must return 0 on reads.
// An image with it set is not a well-formed I/O BAR, and this model refuses
// it rather than guessing which bit was intended.
wire io_reserved_clear = !kind_io || (bar_lo[1] == 1'b0);
assign encoding_legal = !kind_reserved && io_reserved_clear;
// Address bits only. Cleared entirely for an encoding this model cannot
// interpret, so a reserved type can never contribute a base.
assign lo_addr = encoding_legal ? (bar_lo & ~attr_mask) : 32'h0000_0000;
endmoduleClassification: synthesizable.
What it teaches — three things:
- One bit selects everything downstream. Bit 0 does not merely label the BAR; it changes which mask applies, how many address bits exist, whether bit 3 is an attribute or an address, and which Command Register bit governs the resource. A decoder that reads bit 0 for logging and then applies a fixed mask has read it without using it.
- The two masks are genuinely different and cannot be shared.
0x0000000Fand0x00000003differ in bits 3:2, which are address in an I/O BAR and attribute in a memory BAR. There is no conservative choice: one mask loses real address bits, the other imports real attribute bits. - An uninterpretable encoding produces nothing. A reserved type field, or an I/O BAR with its reserved bit set, clears
encoding_legaland zeroeslo_addr. The alternative — guessing — produces a window derived from bits whose meaning the model does not know.
Deliberately simplified: kind_mem64 is classified but not composed here, because composing a 64-bit base from two dwords is Chapter 9.2 §9's problem and repeating it would add nothing. No expansion-ROM handling.
Production implication: an Endpoint fixes its own kind in hardware and needs none of this classification logic; a Root Port, switch, or verification component reading foreign BAR images needs all of it, plus the 64-bit composition and the slot-stride rule of Chapter 9.2 §4.
8. RTL — Space-Qualified Window Decode
// SYNTHESIZABLE. Decodes ONE 32-bit-addressed window that belongs to exactly
// one address space, against two request channels that carry the same
// numeric address in different spaces.
// The space/permission pairing is NORMATIVE (Command Register bit 0 = I/O,
// bit 1 = Memory); the two-channel request interface is illustrative.
module space_qualified_decode #(
// Bytes. Power of two, naturally aligned. How software DISCOVERS this is
// Chapter 9.4; the Function has always known it.
parameter int unsigned WINDOW_SIZE = 32'd256
) (
// Window description, from bar_kind_decode.
input logic kind_io,
input logic kind_mem32,
input logic encoding_legal,
input logic [31:0] win_base,
// Permission, from the Command Register (Chapter 8.4).
input logic io_space_enable, // bit 0
input logic mem_space_enable, // bit 1
// TWO REQUEST CHANNELS. The space is part of the coordinate, so it is
// carried structurally rather than as a flag on a shared bus.
input logic mem_req_valid,
input logic [31:0] mem_req_addr,
input logic io_req_valid,
input logic [31:0] io_req_addr,
output logic window_legal,
output logic mem_hit,
output logic io_hit
);
localparam logic [31:0] SIZE = 32'(WINDOW_SIZE);
localparam logic [31:0] LOW = SIZE - 32'd1;
localparam logic [31:0] MASK = ~LOW;
// COMPILE-TIME checks. All arithmetic at 32 bits, so SIZE - 1 cannot wrap
// through an unsized literal's width.
localparam bit SIZE_POW2 = (SIZE != 32'd0) && ((SIZE & LOW) == 32'd0);
// Bits 1:0 of an I/O BAR and bits 3:0 of a memory BAR are not address, so
// 4 bytes is the finest granularity ANY BAR kind can express.
localparam bit SIZE_MIN = (SIZE >= 32'd4);
generate
if (!SIZE_POW2) $error("WINDOW_SIZE must be a nonzero power of two");
if (!SIZE_MIN) $error("WINDOW_SIZE must be at least 4 bytes");
endgenerate
// NORMATIVE: a device must not consume more than 256 bytes per I/O Base
// Address register. Constant-folds away in a memory configuration; kept as
// an explicit term so the rule is visible in the design, not only in prose.
wire io_size_legal = !kind_io || (SIZE <= 32'd256);
wire base_aligned = ((win_base & LOW) == 32'd0);
assign window_legal = encoding_legal && base_aligned && io_size_legal
&& (kind_io || kind_mem32);
// The FULL 32-bit comparison, for both spaces. Normative for I/O even when
// a Function is permitted to hardwire its upper 16 address bits to zero:
// a full 32-bit decode of I/O addresses must still be done. Comparing only
// the low 16 bits would alias this window every 64 KB.
wire mem_addr_match = ((mem_req_addr & MASK) == (win_base & MASK));
wire io_addr_match = ((io_req_addr & MASK) == (win_base & MASK));
// The two coordinates are equal partners in one conjunction. The window's
// kind and the request's space must agree BEFORE the address means
// anything — see section 6 for why this must not be staged.
assign mem_hit = mem_req_valid && window_legal && kind_mem32
&& mem_space_enable && mem_addr_match;
assign io_hit = io_req_valid && window_legal && kind_io
&& io_space_enable && io_addr_match;
endmoduleClassification: synthesizable.
Register and interface semantics:
| Dimension | Behaviour |
|---|---|
| State | none — this module is purely combinational decode |
| Window description | supplied by bar_kind_decode; never written here |
| Permission | two independent inputs, one per space |
| Request channels | two, structurally separate; the space is never a data-bus flag |
| Byte enables | not modelled — this module decides whether an access is claimed, not what it reads |
| Mutual exclusivity | a window has exactly one kind, so at most one hit can assert |
| Size | a parameter; discovery is Chapter 9.4 |
What it teaches — four things:
- Space and address are one conjunction.
kind_ioandio_addr_matchappear in the same expression at the same level. There is no intermediate signal meaning "the address matched" that could be consumed before the space was checked. - The permissions are not interchangeable.
mem_hitconsultsmem_space_enableandio_hitconsultsio_space_enable, and swapping them compiles, synthesises, and produces a Function that responds only when software enables the wrong thing. - The full 32-bit comparison is mandatory even when the upper bits are hardwired zero. A Function permitted to build only the low 16 address bits still has to reject accesses with nonzero upper bits, and the comparison is where that rejection happens.
- The 256-byte cap is in the design, not just in the prose.
io_size_legalfolds to a constant, which is the point: an over-large I/O window fails to decode rather than quietly working in simulation and failing compliance.
Deliberately simplified: one window; 32-bit addressing only, because I/O has no 64-bit form and Chapter 9.2 owns 64-bit memory composition; no behaviour defined for an unmatched access; no byte enables.
Production implication: a real Function instantiates one of these per BAR, arbitrates among several windows, defines a response for accesses that match nothing, and — if it implements both a memory and an I/O resource — runs two instances whose numeric ranges may overlap entirely without conflict.
9. Assertions
// SVA over bar_kind_decode and space_qualified_decode composed into one
// Function-level instance. Implementation invariants for THESE designs plus
// the normative encoding facts they implement — not a claim about PCIe
// behaviour beyond section 2. Every property refers to explicit RTL.
// SPACE — P1: a memory window never accepts an I/O-space request. Half of
// the chapter's thesis, and the half a memory-only testbench cannot reach.
property p_memory_window_rejects_io;
@(posedge clk) disable iff (!rst_n)
kind_mem32 |-> !io_hit;
endproperty
a_mem_rejects_io : assert property (p_memory_window_rejects_io);
// SPACE — P2: an I/O window never accepts a memory-space request.
property p_io_window_rejects_memory;
@(posedge clk) disable iff (!rst_n)
kind_io |-> !mem_hit;
endproperty
a_io_rejects_mem : assert property (p_io_window_rejects_memory);
// SPACE — P3: the headline case, stated directly. With an I/O window claiming
// an I/O request, the IDENTICAL numeric address presented as a memory request
// in the same cycle must not hit. Catches a decoder that compares the number
// and treats the space as metadata.
property p_same_address_wrong_space;
@(posedge clk) disable iff (!rst_n)
(io_hit && mem_req_valid && (mem_req_addr == io_req_addr)) |-> !mem_hit;
endproperty
a_wrong_space_no_hit : assert property (p_same_address_wrong_space);
// SPACE — P4: at most one hit. A window has exactly one kind, so both hits
// asserting means the kind decode produced two answers.
property p_hits_mutually_exclusive;
@(posedge clk) disable iff (!rst_n)
!(mem_hit && io_hit);
endproperty
a_one_hit : assert property (p_hits_mutually_exclusive);
// PERMISSION — P5: an I/O hit requires I/O Space Enable — Command bit 0.
property p_io_hit_requires_io_enable;
@(posedge clk) disable iff (!rst_n)
io_hit |-> io_space_enable;
endproperty
a_io_needs_io_enable : assert property (p_io_hit_requires_io_enable);
// PERMISSION — P6: a memory hit requires Memory Space Enable — Command bit 1.
property p_mem_hit_requires_mem_enable;
@(posedge clk) disable iff (!rst_n)
mem_hit |-> mem_space_enable;
endproperty
a_mem_needs_mem_enable : assert property (p_mem_hit_requires_mem_enable);
// PERMISSION — P7: the enables are not interchangeable. Catches a swapped
// pair, which produces a Function that answers only when software enables
// the space it does not implement.
property p_enables_not_crossed;
@(posedge clk) disable iff (!rst_n)
(kind_io && !io_space_enable) |-> !io_hit;
endproperty
a_enables_straight : assert property (p_enables_not_crossed);
// ENCODING — P8: an encoding this model cannot interpret produces no window.
// Covers a reserved memory type field and an I/O BAR whose reserved bit 1 is
// set, which section 2 establishes must read as 0.
property p_illegal_encoding_inert;
@(posedge clk) disable iff (!rst_n)
!encoding_legal |-> (!window_legal && !mem_hit && !io_hit
&& (lo_addr == 32'h0000_0000));
endproperty
a_illegal_encoding : assert property (p_illegal_encoding_inert);
// ENCODING — P9: non-address low bits never reach the address. The per-kind
// mask made checkable — bits 3:2 are address in an I/O BAR and attribute in a
// memory BAR, so a shared mask fails this in one direction or the other.
property p_non_address_bits_stripped;
@(posedge clk) disable iff (!rst_n)
(lo_addr & attr_mask) == 32'h0000_0000;
endproperty
a_mask_applied : assert property (p_non_address_bits_stripped);
// ENCODING — P10: the normative 256-byte cap is enforced. An over-large I/O
// window never decodes.
property p_io_size_cap;
@(posedge clk) disable iff (!rst_n)
(kind_io && !io_size_legal) |-> !io_hit;
endproperty
a_io_cap : assert property (p_io_size_cap);
// STABILITY — P11: the kind decode is a pure function of the BAR image.
// Catches a classification that leaked a dependence on request activity.
property p_kind_stable_with_image;
@(posedge clk) disable iff (!rst_n)
$stable(bar_lo) |-> $stable({kind_io, kind_mem32, kind_mem64,
kind_reserved, attr_mask, lo_addr});
endproperty
a_kind_stable : assert property (p_kind_stable_with_image);
// SAFETY — P12: no decode output is ever unknown.
property p_outputs_never_unknown;
@(posedge clk) disable iff (!rst_n)
!$isunknown({mem_hit, io_hit, window_legal, encoding_legal});
endproperty
a_no_x : assert property (p_outputs_never_unknown);P3 is the property this chapter exists to make writable. P1 and P2 state the rule in the abstract and both hold trivially in a testbench that never presents the same address in two spaces. P3 forces the collision: an I/O window is claiming an I/O request, and the identical number arrives as a memory request in the same cycle. A decoder that computed address membership and qualified it later fails here and passes P1 and P2.
P7 looks like a restatement of P5 and catches a different bug. P5 says an I/O hit implies I/O Space Enable — true even in a design that consults mem_space_enable for the I/O path, provided both enables happen to be set together in the test. P7 approaches from the negative side: with I/O Space Enable clear, no I/O hit may occur, whatever the other enable is doing. Swapped enables survive P5 in a well-behaved test and fail P7 immediately.
P9 is the per-kind mask, checked without repeating it. Comparing lo_addr against bar_lo & ~attr_mask would restate the RTL and pass on a shared-mask design that also shared the mask in the property. Asserting that lo_addr has no bits in common with attr_mask states the requirement instead, and it fails for a memory BAR carrying bits 3:2 into the address.
P11 is cheap and worth having. A BAR-kind decode that depends on anything but the register image is a design that will classify differently under traffic than at rest, and that failure is close to undebuggable from the outside.
10. Verification
Monitors observe: the BAR image; all four kind outputs plus attr_mask, lo_addr, and encoding_legal; both permission inputs; both request channels with their addresses; and both hit outputs.
The scoreboard independently classifies the BAR image from its own copy of §2's field map and independently computes membership as base <= addr < base + size. It must not reuse the design's attr_mask, because a shared mask agrees with the design about exactly P9's bug.
The environment must instantiate both kinds. Every scenario runs with an I/O BAR image and a memory BAR image, because P1's failure exists only in one configuration and P2's only in the other — the same asymmetry Chapter 8.6 §8's P5 warns about.
Space qualification — the core of the chapter
- I/O BAR, matching I/O request. Must hit.
- I/O BAR, same numeric address as a memory request. Must not hit (P2). Run both requests in the same cycle so P3 is reachable — separate cycles let a staged decoder pass.
- Memory BAR, matching memory request. Must hit.
- Memory BAR, same numeric address as an I/O request. Must not hit (P1).
- Both Functions present. One instance with an I/O window and one with a memory window, programmed to identical numeric bases and sizes, exercised concurrently. Verify each claims only its own space. This is the configuration the specification explicitly contemplates — a device mapping control functions into both spaces — and it is the strongest test in the chapter.
Permission
- I/O Space Enable clear, Memory Space Enable set. With an I/O window, verify no hit (P5). This is the swapped-enable case and it is the single most common real failure.
- Memory Space Enable clear, I/O Space Enable set. With a memory window, verify no hit (P6).
- All four enable combinations, against each window kind. Verify a hit requires the matching enable and is indifferent to the other (P7).
- Enable asserted mid-traffic. Verify hits begin, and that the window description did not change.
Encoding
- I/O BAR with reserved bit 1 set. Verify
encoding_legalclears and nothing hits (P8) — a normative violation the model refuses rather than reinterprets. - Reserved memory type encodings
01and11. Verify the same. - Bit 0 flipped in the BAR image between otherwise identical runs. Verify the mask, the address bits, and the governing enable all change together. A design that changes only one of the three is the bug this test exists for.
- Address bits 3:2 nonzero. With an I/O image, verify they survive into
lo_addr— they are address. With a memory image, verify they are stripped — they are attributes. The same two bits, opposite treatment. - Base with bits 1:0 nonzero on an I/O image. Verify they are stripped and the window is unmoved.
Address boundaries
- First and last address in the window, in the correct space. Both must hit.
- One below and one above. Neither may hit.
- Upper 16 address bits nonzero on an I/O request, with a window whose base has zeros there. Must not hit — the full-32-bit-decode requirement, and the test that catches a Function comparing only the low half. Sweep the upper half so an aliasing decoder fails at more than one point.
- Window at the top of the 32-bit space. Verify the boundary decodes.
- Misaligned base. Verify
window_legalclears. - I/O window larger than 256 bytes. Verify it never hits (P10).
Reset and stability
- Reset with a programmed window. Verify no hit until reprogrammed and re-enabled; note that both Command enables also reset to 0.
- Sustained traffic with a stable BAR image. Verify every classification output is stable (P11).
Coverage should include: both BAR kinds; all four type-field encodings; the I/O reserved bit in both states; both permission bits in all four combinations against each kind; both request channels valid alone and together; identical and differing addresses across the two channels; window sizes from 4 bytes through the 256-byte I/O cap and beyond it; and addresses at both window boundaries, one beyond each, and with the upper 16 bits both zero and nonzero.
11. Debugging
Symptom: an I/O BAR is programmed and enabled, and the device does not respond
The ladder, ordered by cost and by probability — and note that the first two rungs are outside the Function.
- Is the platform issuing I/O-space requests at all? Some host bridges do not implement I/O transactions. On such a platform the Function can be perfectly programmed and perfectly enabled and still never see a request, because none is ever generated. Check this first, because every rung below it assumes a request arrives, and no observation inside the Function can distinguish "no request arrived" from "the request was rejected" without looking at the request path.
- Is the access actually being issued as an I/O-space access? A driver reaching the resource with an ordinary load or store produces a memory-space request. Numerically it may match the window exactly. It must still be rejected (P2) — and it will be, correctly, which makes this look like a hardware failure when it is a software one.
- Is I/O Space Enable — Command Register bit 0 — set? Not Memory Space Enable, which is bit 1. Enabling the wrong bit is the most common real fault here, and it survives casual inspection because the Command Register looks configured.
- Do the parent bridges forward I/O to this Function? I/O windows in the hierarchy are separate from memory windows and are configured separately. A Function inside a subtree whose I/O window was never programmed is unreachable in I/O space and perfectly reachable in memory space at the same time.
- Did the Function decode the BAR as I/O? Read bit 0. If it reads
0, this is a memory BAR and the whole investigation was aimed at the wrong space. - Is bit 1 clear? A set reserved bit means a malformed image, and §7's model refuses it rather than guessing.
- Does the address fall in the window? Compute membership by hand from bits 31:2 and the resource size.
- Only now, the internal I/O resource.
The observation that collapses most of this: watch the request path, not the Function. If no I/O-space request ever reaches the Function, rungs 3 through 8 cannot explain anything and the fault is at rung 1, 2, or 4 — all of which are outside the device everyone is instrumenting.
Symptom: a memory access to the same numeric address reaches nothing
This is correct behaviour and recognising it quickly saves days.
The numeric address matched. The window is programmed. The Function still declined the access, because the access arrived in the wrong space and the space is part of the coordinate, not a routing hint attached to it.
Why it is worth naming as a distinct scenario. The evidence looks damning: a valid address, a programmed BAR, a device that will not answer. Nothing in that picture points at the address space unless you are already thinking about it. An engineer who has internalised the two-coordinate model checks it in one step; an engineer who has not can spend a long time inside a Function that is behaving exactly as designed.
The confirming observation. Present the same numeric address as an I/O-space request. If it hits, the Function is correct and the caller is using the wrong access mechanism — the fix is in the driver, and it is one line.
Symptom: an I/O window appears to respond at addresses far outside its range
Suspect a truncated address comparison. A Function permitted to hardwire its upper 16 address bits to zero is still required to perform a full 32-bit decode. One that compares only the low 16 bits aliases its window every 64 KB across the entire I/O space.
How the symptom presents. The device answers at the assigned address and at that address plus 64 KB, plus 128 KB, and so on — which usually surfaces as a different device misbehaving, because this Function is absorbing accesses aimed at it.
How to confirm it in one access. Issue an I/O access at the assigned address with a nonzero upper half. It must not hit. If it does, the comparison is truncated, and the fault is Chapter 9.1 §6's over-claiming failure in its I/O form — the dangerous direction, because the symptom appears at another device that is behaving correctly.
12. Common Misconceptions
- "An I/O BAR is just a small Memory BAR." Different low-bit encoding (bits 1:0 versus 3:0), different address-bearing bits, no 64-bit form, no prefetchable attribute, a different Command Register permission, and a different transaction class. The placement mechanism is shared; almost nothing else is.
- "An I/O BAR uses Memory Space Enable." I/O Space is Command Register bit 0; Memory Space is bit 1. They are independent bits and enabling one does nothing for the other.
- "The same numeric address means the same resource." It does not. A resource is named by an address and a space. Two Functions — or one Function with two BARs — can legitimately claim the same number in different spaces.
- "All PCIe systems use I/O BARs heavily." I/O BARs are enabled for Legacy PCI Express Endpoints; guidance from PCIe endpoint IP vendors is to avoid I/O space in new designs, and the specification recommends mapping control functions into Memory Space. They still exist and still ship, which is why they are worth understanding.
- "An I/O BAR means the processor's port-I/O instructions are implemented inside the Endpoint." The Endpoint responds to I/O-space transactions on the fabric. How a host processor originates them — a dedicated instruction class, a mapped region, or not at all — is a property of that platform.
- "The BAR kind can be ignored once enumeration is over." It selects which permission bit governs the resource and which transaction class can reach it, on every access, forever. §8's decode consults it in the same expression as the address for exactly that reason.
- "Memory and I/O BAR low bits mean the same thing." Bits 3:2 are address in an I/O BAR and attribute in a memory BAR. A shared mask is wrong in one direction or the other, never neither.
- "I/O BARs and MSI or MSI-X are related." They are not. MSI and MSI-X are interrupt mechanisms (Module 19); an I/O BAR is an address-space resource claim. The only thing they share is living in configuration space.
- "Bit 1 of an I/O BAR is a fine-grained address bit." It is reserved and must read as 0. The finest granularity an I/O BAR can express is 4 bytes.
- "A Function with an I/O BAR cannot also have a memory BAR." It can, and the specification describes exactly that arrangement: a device mapping control functions into both spaces implements one BAR of each kind.
13. Understanding Check
14. What's Next
Two chapters have now decoded a BAR completely: 9.2 covered the memory form and its 32-bit and 64-bit composition, and this chapter covered the I/O form and the address-space distinction that makes a number an incomplete coordinate.
Neither said where the size comes from. Go back through both field maps and there is no size field anywhere — only an address and some attributes. Yet every decoder in both chapters needed a mask, and every host allocator needs to know how much space to reserve before it can place anything.
Chapter 9.4 — BAR Sizing closes that gap. It is the deepest chapter in the module, because the mechanism is genuinely subtle: software discovers a resource's required size by writing a probe value and reading back a mask of which address bits the Function actually implements. Why that works, why the returned pattern is a statement about hardware rather than a firmware convention, and how to derive the size from it without an off-by-one, are all its subject.
The idea to carry forward: a resource is named by an address and a space, and the BAR encoding tells you which space before it tells you anything about the address.