PCIe · Module 8
Configuration Header — One Structure, Two Roles
The header places common fields at predictable offsets and then diverges by Function role. Why Type 0 and Type 1 exist, how software discovers which layout it is reading, and how role-selected decode keeps endpoint and bridge state from aliasing in RTL.
Four chapters have each shown one field with just enough local context to explain it. None showed where they sit relative to each other, and every one deferred the layout to here.
How do the individual configuration fields of Module 8 fit into the standardised header structures that different PCIe Function roles expose?
1. Why One Layout Is Not Enough
An endpoint and a bridge do different jobs, and software needs different things from each.
An endpoint terminates traffic. Software needs to know what address resources it requires so they can be assigned (Chapter 7.8).
A bridge forwards traffic. Software needs to establish which hierarchy regions lie behind it and which address ranges it should forward — the primary, secondary, and subordinate relationships of Chapter 7.4.
Those are incompatible requirements for the same offsets. A bridge has no use for six endpoint resource descriptors; an endpoint has no use for bus-number registers. Reserving space for both in every Function would waste most of the header in every Function.
So the header keeps a common region where every Function is the same, and diverges after it by role.
2. The Verified Layout
3. Where the Divergence Begins
Read the figure as a common trunk with two continuations. Identity, control, status, the header-type indication, and the first two resource descriptors are the same wherever you find them. From 18h the structure forks.
Why 10h and 14h are common is worth noticing: a bridge is still a Function with its own configuration space, and it may have its own resources to describe. What it does not need is four more of them, so the space they would have occupied carries the hierarchy fields instead.
4. How Software Knows Which Layout
The header contains the answer to its own interpretation. Header Type at 0Eh, bits 6:0, encodes which layout follows.
The sequence software follows is therefore:
- Read the common region, which is safe because it is layout-independent.
- Read the Header Type field.
- Interpret everything from
18honward according to that value.
Bit 7 is a separate question sharing a byte. It indicates whether the device position presents more than one Function — Chapter 7.6's multifunction indication. It says nothing about layout, and layout says nothing about multifunction. Two independent facts, packed into one byte for space.
5. Module 8 Assembled
The header is the map that connects everything Module 8 has covered:
| Region | What it is | Chapter |
|---|---|---|
00h, 02h | who made it, and what it is | 8.3, 8.2 |
04h | what software permits it to do | 8.4 |
06h | what it has observed and what it provides | 8.5 |
0Eh | which layout the rest of it uses | this chapter |
10h onward, Type 0 | what address resources it needs | Module 9 |
18h–1Ah, Type 1 | which hierarchy regions lie behind it | 7.4 |
34h | where optional feature descriptions begin | later chapters |
And the access mechanism that reaches all of it is Chapter 7.7 — identity plus a register offset, where the offset is exactly the column on the left.
That connection is worth making explicit: the offsets in this table are the same values that appear in an ECAM address's low bits. A configuration access to 4:00.1 offset 04h reaches the Command Register of that Function, and the arithmetic that gets it there is Chapter 7.7 §3's.
6. Microarchitecture — Role-Selected Decode
A Function implements one role. Its header therefore has one common bank and one role-specific bank, and the decode must route each offset to the right place.
The structure:
Offset decode determines which region an access falls in — common, or role-specific, or outside both.
A common bank holds identity, control, and status, and is present regardless of role.
A role-specific bank holds either endpoint resource state or bridge hierarchy state, never both.
A response mux gathers the read value from whichever bank was selected.
7. RTL — Role-Selected Header Decode
// SYNTHESIZABLE. Offset decode for a configuration header, with a common
// MODELLED REGION and one role-specific modelled region selected at
// elaboration.
// Offsets and the role divergence point: NORMATIVE (§2).
// Bank contents, interface, and out-of-modelled-region behaviour: illustrative.
//
// SCOPE: each region is ONE abstract bank. Individual registers inside a
// region are not modelled, so `rsp_unsupported` means "outside the regions
// this model represents" — NOT "the specification defines this location as
// unsupported".
module cfg_header_decode #(
// A Function implements one role. Fixing it here makes the two
// role-specific banks mutually exclusive in the synthesised design.
parameter bit IS_BRIDGE = 1'b0
) (
input logic clk,
input logic rst_n,
// Access, after identity decode selected this Function (Chapters 7.5-7.6).
input logic req_valid,
output logic req_ready,
input logic req_write,
input logic [7:0] req_offset, // byte offset within the header
input logic [31:0] req_wdata,
input logic [3:0] req_be,
output logic rsp_valid,
input logic rsp_ready,
output logic [31:0] rsp_rdata,
output logic rsp_unsupported
);
// The divergence point (§2). Below this, both layouts are identical.
localparam logic [7:0] ROLE_SPLIT = 8'h18;
localparam logic [7:0] COMMON_TOP = 8'h17;
localparam logic [7:0] T0_TOP = 8'h27; // through BAR 5 at 24h..27h
localparam logic [7:0] T1_TOP = 8'h1B; // through 1Ah, dword-aligned
// Modelled-region decode. Exactly one of these can be true, because the
// role-specific terms are qualified by a parameter fixed at elaboration.
// These classify an offset into a STRUCTURAL REGION. They do not claim that
// every offset inside a region is an implemented PCIe register.
wire in_common = (req_offset <= COMMON_TOP);
wire in_type0 = !IS_BRIDGE && (req_offset >= ROLE_SPLIT) && (req_offset <= T0_TOP);
wire in_type1 = IS_BRIDGE && (req_offset >= ROLE_SPLIT) && (req_offset <= T1_TOP);
wire in_range = in_common || in_type0 || in_type1;
// Ready when no response is outstanding. Depends on state, never on
// req_valid — no combinational path from a requester's valid to its ready.
assign req_ready = !rsp_valid || rsp_ready;
wire accept = req_valid && req_ready;
// ------------------------------------------------------------------
// Banks — one per MODELLED REGION. Deliberately tiny: this module teaches
// DECODE ORGANISATION, not the contents of the header. Each bank stands in
// for the whole set of registers its region would really hold, which is
// exactly why this model cannot speak about individual offsets.
// ------------------------------------------------------------------
logic [31:0] common_bank; // identity / control / status / header type
logic [31:0] t0_bank; // endpoint resource state
logic [31:0] t1_bank; // bridge hierarchy state
// Write enables. One per bank, and only the selected modelled region
// asserts — so a Type 0 write can never reach t1_bank and vice versa. With
// IS_BRIDGE fixed, one of these two is constant zero and optimises away.
wire wr_common = accept && req_write && in_common;
wire wr_t0 = accept && req_write && in_type0;
wire wr_t1 = accept && req_write && in_type1;
// Read mux: exactly one source, chosen by the same modelled-region decode.
logic [31:0] rd_mux;
always_comb begin
rd_mux = 32'h0000_0000; // assigned first: no latch, and an
if (in_common) rd_mux = common_bank; // offset outside every
else if (in_type0) rd_mux = t0_bank; // modelled region reads zero
else if (in_type1) rd_mux = t1_bank; // in this model.
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
common_bank <= 32'h0000_0000;
t0_bank <= 32'h0000_0000;
t1_bank <= 32'h0000_0000;
rsp_valid <= 1'b0;
rsp_rdata <= 32'h0000_0000;
rsp_unsupported <= 1'b0;
end else begin
if (rsp_valid && rsp_ready) rsp_valid <= 1'b0;
if (wr_common) common_bank <= req_wdata;
if (wr_t0) t0_bank <= req_wdata;
if (wr_t1) t1_bank <= req_wdata;
if (accept) begin
rsp_valid <= 1'b1;
rsp_rdata <= in_range ? rd_mux : 32'h0000_0000;
rsp_unsupported <= !in_range;
end
end
end
endmoduleClassification: synthesizable.
Register semantics for the banks shown:
| Dimension | Behaviour in this model |
|---|---|
| Reset | all banks cleared |
| Read | the selected bank, or zero if the offset is outside every modelled region |
| Write | the selected bank only; offsets outside every modelled region write nothing |
| Byte enables | not applied — deliberately, see below |
| Side effects | none; each bank stands in for a whole region's registers |
| Hardware may update | no, in this model |
| Software may update | yes, within a modelled region |
Byte enables are deliberately absent here and that is a scoping decision, not an omission by accident. Chapter 8.1 §6 and Chapter 8.4 §7 both build byte-enable merging properly. Repeating it would obscure this module's subject, which is modelled-region decode and role separation. A real header decode composes both.
What it teaches — three things:
- The role parameter makes cross-role aliasing unrepresentable. With
IS_BRIDGEfixed,in_type0andin_type1cannot both be true, and one of the two banks is optimised away entirely. That is stronger than checking for the condition at runtime. - One offset, two meanings, one decode.
18hselectst0_bankin an endpoint andt1_bankin a bridge. The decode is the only thing that makes the same address mean different things, which is why §10's failure is a decode failure rather than a register failure. - An access outside every modelled region needs defined behaviour. It reads zero, writes nothing, and reports
rsp_unsupportedrather than silently succeeding. In this local model that indication lets a testbench distinguish a region the teaching abstraction does not represent from a represented region that happens to read zero — two situations a bare zero cannot tell apart.
Deliberately simplified: one register per modelled region; no per-offset register presence; no byte enables; no field-level access classes; and the out-of-region response is a local interface contract, not a verified protocol behaviour.
Production implication: a real header decode implements every field at its own offset with its correct access class, composes the byte-enable and W1C machinery of Chapters 8.1 and 8.5, produces whatever response the specification requires for the locations it does not implement, handles the whole configuration space rather than the header alone, and instantiates per Function.
8. Assertions
// SVA over cfg_header_decode. Implementation invariants for THIS design —
// not PCIe protocol requirements. Every property refers to explicit RTL state.
// "Region" throughout means MODELLED REGION, never "implemented register".
// EXCLUSIVITY — P1: at most one modelled region is selected. With IS_BRIDGE fixed this
// holds structurally; asserted so that a later edit making the role dynamic
// fails here rather than silently aliasing the banks.
property p_regions_exclusive;
@(posedge clk) disable iff (!rst_n)
$onehot0({in_common, in_type0, in_type1});
endproperty
a_one_region : assert property (p_regions_exclusive);
// ISOLATION — P2: a Type 0 access never modifies bridge state. The property
// that makes "same offset, different meaning" safe.
property p_t0_write_isolated;
@(posedge clk) disable iff (!rst_n)
wr_t0 |=> $stable(t1_bank);
endproperty
a_t0_isolated : assert property (p_t0_write_isolated);
// ISOLATION — P3: a Type 1 access never modifies endpoint state.
property p_t1_write_isolated;
@(posedge clk) disable iff (!rst_n)
wr_t1 |=> $stable(t0_bank);
endproperty
a_t1_isolated : assert property (p_t1_write_isolated);
// ISOLATION — P4: a role-specific write never modifies the common bank, and
// a common write never modifies either role bank. Catches a decode that
// overlaps the divergence point.
property p_common_isolated;
@(posedge clk) disable iff (!rst_n)
(wr_t0 || wr_t1) |=> $stable(common_bank);
endproperty
a_common_isolated : assert property (p_common_isolated);
property p_role_banks_isolated_from_common;
@(posedge clk) disable iff (!rst_n)
wr_common |=> ($stable(t0_bank) && $stable(t1_bank));
endproperty
a_role_isolated : assert property (p_role_banks_isolated_from_common);
// LEGALITY — P5: the role parameter governs which role bank can ever be
// written. Catches a decode term that forgot its IS_BRIDGE qualifier — which
// would let an endpoint expose bridge registers.
property p_role_respected;
@(posedge clk) disable iff (!rst_n)
IS_BRIDGE ? !wr_t0 : !wr_t1;
endproperty
a_role_respected : assert property (p_role_respected);
// SAFETY — P6 (LOCAL TEACHING-MODEL INVARIANT): an offset outside every
// modelled region has no write side effect anywhere. Software probes offsets
// whose contents it does not know, so those probes must be inert. This is a
// property of THIS model's decode, not a published PCIe rule.
property p_out_of_range_inert;
@(posedge clk) disable iff (!rst_n)
(accept && !in_range) |=> ($stable(common_bank) && $stable(t0_bank)
&& $stable(t1_bank));
endproperty
a_oor_inert : assert property (p_out_of_range_inert);
// CORRECTNESS — P7 (LOCAL TEACHING-MODEL INVARIANT): an offset outside every
// modelled region is reported as unsupported rather than answered as though
// this model represented it. An ILLUSTRATIVE interface contract — PCIe's own
// response requirement for locations a Function does not implement is not
// published in this chapter.
property p_out_of_range_reported;
@(posedge clk) disable iff (!rst_n)
(accept && !in_range) |=> (rsp_valid && rsp_unsupported);
endproperty
a_oor_reported : assert property (p_out_of_range_reported);
// SAFETY — P8: a read never writes. Catches an enable derived from `accept`
// without qualifying on req_write — the failure Chapter 8.1 P7 describes,
// which corrupts state during enumeration's read-heavy discovery phase.
property p_read_has_no_write;
@(posedge clk) disable iff (!rst_n)
(accept && !req_write) |-> (!wr_common && !wr_t0 && !wr_t1);
endproperty
a_read_no_write : assert property (p_read_has_no_write);
// CORRECTNESS — P9: the response comes from the selected modelled region's bank.
property p_response_from_selected;
@(posedge clk) disable iff (!rst_n)
(accept && !req_write && in_range) |=> (rsp_rdata == $past(rd_mux));
endproperty
a_read_correct : assert property (p_response_from_selected);
// CONSERVATION — P10: one accepted access produces one response, held stable
// until taken.
property p_one_response_held;
@(posedge clk) disable iff (!rst_n)
(rsp_valid && !rsp_ready) |=> (rsp_valid && $stable(rsp_rdata)
&& $stable(rsp_unsupported));
endproperty
a_response_held : assert property (p_one_response_held);P5 is the property that would be omitted by someone who trusted the parameter. With IS_BRIDGE fixed, wr_t0 in a bridge is structurally impossible — so the property looks redundant. It is worth writing precisely because the redundancy is conditional on the qualifier being present. A decode term written as (req_offset >= ROLE_SPLIT) && (req_offset <= T0_TOP) — with the !IS_BRIDGE accidentally dropped — compiles, synthesises, and exposes endpoint resource registers on a bridge at the offsets where its bus numbers should be. P5 fires immediately; nothing else in the design would.
P2 and P3 are the pair that make the shared offset safe. 18h addresses different state in the two roles, so the isolation must hold in both directions. Asserting only one leaves the other role's aliasing undetected, and the two roles are usually verified in separate testbench configurations where a single-direction property looks sufficient.
P6 and P7 are local teaching-model invariants, and it matters that they are read that way. They are true of this decode by construction. Neither is a PCIe protocol requirement, and neither says anything about which configuration-space locations the specification defines.
What P7 buys inside the model. The explicit unsupported indication lets the testbench distinguish a region this teaching abstraction does not represent from a represented region that happens to read zero. A bare zero conflates the two, and a scoreboard that cannot tell them apart cannot check the region map at all. That is an illustrative interface contract — the value is in what it makes observable to verification, not in any claim about protocol-level response behaviour.
9. Verification
Monitors observe: the access handshake with offset, direction, and data; the modelled-region decode signals; all three banks; and the response with its unsupported indication.
The scoreboard independently models the modelled-region map from its own copy of §2's offsets and its own knowledge of the configured role — not by reading the design's decode terms. A scoreboard that reuses in_type0 agrees with the design about a missing IS_BRIDGE qualifier, which is exactly P5's bug.
The environment must instantiate both roles. Every scenario below runs twice, once with IS_BRIDGE = 0 and once with IS_BRIDGE = 1, because the failures are asymmetric and a single-role testbench cannot see them.
Type 0 configuration
- Common-region access. Each common offset, read and write. Verify the common bank responds in both roles.
- Type 0 role-specific access. Offsets
18hthrough27h. Verifyt0_bankresponds andt1_bankis untouched (P3's counterpart). - Access at exactly
18h. The divergence point. Verify it selects the endpoint bank — the single most important offset in the chapter. - Access to a Type 1-only offset. With
IS_BRIDGE = 0, an access at18hmust reacht0_bank, nevert1_bank(P5).
Type 1 configuration
- Common-region access. Verify identical behaviour to the Type 0 case — the common region is genuinely common.
- Type 1 role-specific access. Offsets
18h–1Bh. Verifyt1_bankresponds. - Access at
18h. Verify it selects the bridge bank. Comparing this against the Type 0 result is the test that proves the role decode works, and it is impossible to run without both configurations. - Access above
1Bhbut below27h. In a bridge these are outside the modelled Type 1 region. Verify they are reported unsupported (P7) and write nothing (P6) — a Type 0-shaped access aimed at a bridge. Read the check correctly: it confirms the model's own region boundary, not that PCIe leaves those offsets undefined in a Type 1 header.
Boundaries and common cases
- Offset
17hand18h. The last common offset and the first role-specific one. An off-by-one inCOMMON_TOPshows here and nowhere else. - The top of each modelled role region.
27hfor Type 0,1Bhfor Type 1, and one beyond each. - Offset outside every modelled region. Verify unsupported reported, no bank changed.
- Read with plausible data on the write bus. Verify nothing is written (P8).
- Reset with all banks written. Verify all clear.
- Response backpressure. Verify stability (P10) and no new access accepted.
Coverage should include: both role configurations; every modelled-region boundary and boundary ± 1; read and write in each region; offsets outside every modelled region in each role; and reset from each distinct bank state.
10. Debugging
Symptom: identity and Command read correctly, but resource fields are missing or wrong
What the correct common fields establish — and it is substantial. Configuration access works end to end (Chapter 7.7), identity decode selected the right Function (Chapters 7.5–7.6), and the common region of the header decodes and responds correctly.
What that leaves. Only the role-specific region — which is to say, only the decode from 18h onward.
Candidates:
- The Function's role is not what software thinks. Read Header Type at
0Ehand compare against expectation. If it says Type 1 and software expected an endpoint, everything from18his being interpreted with the wrong map and the mismatch is explained completely. - The role parameter is wrong in the design. The Function implements the wrong layout. Header Type will say so, which makes rung 1 the same observation.
- The decode is routing to the wrong bank. Header Type is correct, and the region terms select the wrong region — P5's failure, most often a dropped
IS_BRIDGEqualifier. - The region boundaries are off by one.
COMMON_TOPorROLE_SPLITwrong, so an offset lands in the neighbouring region. - The fields exist and are unprogrammed. Not a header problem at all — Chapter 7.8 territory.
The observation that resolves nearly all of this: read Header Type first. It is one access, it is in the common region so it is reachable regardless of the role, and it tells you which map software should be using. If it disagrees with expectation, the investigation is over before it starts.
Symptom: bus-number registers appear where endpoint resource fields were expected
This is the diagnostic case for the whole chapter, and it is nearly self-diagnosing once the layout is understood.
18h is Base Address Register 2 in a Type 0 header and the Primary Bus Number in a Type 1 header. Reading plausible-looking bus numbers at an offset where a resource descriptor was expected means software and hardware disagree about the role.
Which direction the disagreement runs:
- Hardware is a bridge, software expected an endpoint. Header Type reads
1. Software's interpretation is wrong — or it never read Header Type and assumed. - Hardware is an endpoint with a bridge decode. Header Type reads
0while the decode routes18hto bridge state. That is P5's failure and it is a design bug.
The one observation that separates them. Compare Header Type against the decode's actual behaviour. If Header Type says Type 0 and 18h behaves like a bus-number register, hardware is inconsistent with its own advertisement — and that is the fault, regardless of what software expected.
Why this symptom is worth recognising on sight. The values look wrong in a specific, recognisable way: small integers where an address range should be. An engineer who knows the layout diverges at 18h reads that immediately as a role mismatch rather than as corrupted resource data.
11. Common Misconceptions
- "Every PCIe Function has the same configuration header layout." The common region is identical; from
18hthe layout depends on the Function's role. That divergence is the reason the header type field exists. - "Type 0 means PCIe generation zero." It identifies a header layout, used by endpoint-like Functions. It has nothing to do with generations, link speeds, or lane widths.
- "Type 1 means the second Function." It identifies the bridge-like header layout. Function numbering is a separate coordinate entirely (Chapter 7.6).
- "The configuration header and configuration space are the same thing." The header is the standardised front portion. Configuration space extends beyond it, including the optional feature descriptions the capability pointer at
34hleads to. - "Type 0 and Type 1 determine link speed or width." They determine the meaning of header offsets. Generation and width are physical-layer properties (Chapter 6.1) with no relationship to header layout.
- "Bridge and endpoint role-specific fields can be read with the same interpretation."
18his a resource descriptor in one layout and a bus number in the other. Reading either with the wrong map produces plausible values that mean nothing. - "A header is best implemented as one large case statement over every offset." Region decode with separate banks keeps the roles structurally isolated and lets the unused bank optimise away. A flat case merges everything into one namespace, which is where the aliasing P2–P5 catch comes from.
- "Header Type only matters to software." RTL must decode
18honward according to the role it implements. A design whose decode disagrees with the Header Type it advertises is inconsistent with its own configuration space — §10's second scenario.
12. Understanding Check
13. Module 8 Complete
Six chapters, from a mechanism to a structure.
8.1 Configuration Mechanism — what configuration space is, why it is not MMIO, why every Function has its own context, and how register-access behaviour is built.
8.2 Device IDs — implementation identity, and why it is not the Device Number in BDF.
8.3 Vendor IDs — namespace ownership, and never exposing a half-initialised identity pair.
8.4 Command Register — permission that fans out into other blocks, and what may and may not be withdrawn when it is revoked.
8.5 Status Register — condition rather than control, sticky by necessity, with an explicit rule for the same-cycle conflict.
8.6 Configuration Header — the structure all of it sits in, and the role divergence that makes the structure self-describing.
The through-line: configuration space is a standardised software interface implemented by ordinary hardware, and almost every field behaves differently from its neighbours. Module 8's real contribution is the discipline of asking, for every location, who owns this value, what does a write do, and what happens when both sides act at once.
14. What's Next
The header reserves room for resource descriptors and this chapter said nothing about what they contain — only where they sit and which role has how many.
Chapter 9.1 — What BARs Are opens Module 9 with the mental-model correction that the rest of the module depends on: a Base Address Register is not the device's memory. It is a configuration-space control point associated with an addressable window, and the difference between describing where a window lives and containing what the window holds is the distinction most misunderstandings of PCIe resources come from.