Skip to content

PCIe · Module 7

Configuration Access — From Identity to a Reached Function

How BDF plus a register offset becomes an access that traverses the hierarchy and reaches exactly one function. The ECAM address layout derived rather than memorised, why ECAM is not the device's MMIO, and why an access failing is not the same as a device being absent.

Three chapters built an identifier. Chapter 7.4 named regions, Chapter 7.5 named positions, Chapter 7.6 named functions — and all three ended by saying that knowing what a thing is called is not knowing how to reach it.

Once software knows a function's identity, how does a configuration read or write actually reach that function and return a result?

1. The Path, Before the Arithmetic

Software issues something. A function answers. Between those two events:

Host software performs a load or store to an address it computed from the target's identity.

The host's configuration-access mechanism recognises that address as belonging to the configuration region, extracts the target identity and register offset from it, and generates a configuration access — not a memory access.

The Root Complex issues that access into the hierarchy.

Hierarchy elements forward it toward the target region, using the bus ranges of Chapter 7.4.

The target function receives it, matches it against its own identity (Chapter 7.5, Chapter 7.6), and performs the access against its configuration space.

The response returns along the path and is correlated back to the original software operation.

Nine chapters of Module 7 sit inside that sequence. §12's debugging walks it as a ladder, because a failure at any stage produces the same symptom at the top.

2. ECAM — Configuration Space Made Memory-Addressable

A processor issues loads and stores. It has no instruction for "configuration read." Something must bridge that gap.

ECAM — the Enhanced Configuration Access Mechanism — maps configuration space into the system's memory address space, so that an ordinary load or store to the right address becomes a configuration access to a specific function's register.

The mapping is regular by construction, which is what makes the address arithmetic simple:

QuantityVerified value
Configuration space per function4096 bytes (extended from 256 bytes in legacy PCI)
Functions per device position8 (conventional 3-bit field)
Device positions per bus32 (conventional 5-bit field)
Buses per segment group256 (8-bit field)
ECAM region per segment group256 MB, 256 MB-aligned

Those numbers are not independent — they multiply. 4 KB per function × 8 functions = 32 KB per device position. × 32 device positions = 1 MB per bus. × 256 buses = 256 MB. The region size is a consequence of the identifier's field widths and the per-function space, not a separate fact to remember.

3. The Address Layout

Within the ECAM region, the offset of a function's register is built from the identity's fields:

Address bitsFieldSelects
above bit 27ECAM basethe 256 MB-aligned base of the region
27:20Bus1 of 256 buses
19:15Device1 of 32 device positions
14:12Function1 of 8 functions
11:2Register dword1 of 1024 dwords in the function's 4 KB space
1:0Bytethe byte within that dword

As a formula:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ECAM_address = ECAM_base
             + (Bus      << 20)
             + (Device   << 15)
             + (Function << 12)
             + Register_offset          // 0 .. 4095

On segment groups. The 256 MB region above covers 256 buses — one segment group. PCIe defines segment groups so a system can exceed 256 buses in total, each group having its own ECAM region. This chapter's examples use a single segment group, and the address formula above is the within-group form. A system with several groups selects the group by which ECAM region the access falls in, which is platform configuration rather than part of this arithmetic.

4. A Worked Derivation

Target: bus 4, device 0, function 1 — 4:00.1 from Chapter 7.6 — at register offset 0x10, chosen arbitrarily to exercise the arithmetic and not a claim about what lives at that offset. ECAM base 0xE000_0000 (256 MB-aligned).

TermComputationValue
ECAM basegiven, 256 MB-aligned0xE000_0000
Bus contribution4 << 20 = 4 × 1 MB0x0040_0000
Device contribution0 << 150x0000_0000
Function contribution1 << 12 = 1 × 4 KB0x0000_1000
Register offsetarbitrary, within 0–40950x0000_0010
ECAM addresssum0xE040_1010

The region spans 0xE000_0000 to 0xEFFF_FFFF — 256 MB.

Sanity-check the result against the structure. The address is 4 MB past the base (bus 4 × 1 MB), then 4 KB into that bus's megabyte (function 1 × 4 KB), then 16 bytes into that function's space. Every term is a position in a containment hierarchy, and the address decomposes back into the identity by the reverse operation — which is exactly what §8's decoder does.

5. ECAM Is Not the Device's MMIO

This is the misconception that causes the most confusion in practice, and it is worth stating before anything else can build on it.

The ECAM address is not the device's address. It is an address in the host's memory map that the host's configuration mechanism recognises and converts. The access delivered to the function is a configuration access carrying the function's identity and a register offset — not a memory access carrying 0xE040_1010.

Configuration space is not MMIO. They are different address spaces reaching different things:

Configuration spaceMMIO / resource space
What it containsstandardised identification, control, status, resource descriptionthe device's own operational registers and data windows
How it is reachedconfiguration accesses, addressed by identity + offsetmemory accesses, addressed by an assigned address range
When it becomes availableas soon as the function is accessibleonly after resources have been assigned (Chapter 7.8)
Layoutstandardised across all functionsentirely device-specific

The practical consequence. A function whose configuration space reads perfectly may have no working MMIO at all, because MMIO depends on resource assignment that has not happened or has gone wrong. That is exactly Chapter 7.8's debugging scenario, and it is only intelligible once these two spaces are kept apart.

6. The Access Path

A configuration read. Host software issues a load to an ECAM address. The host configuration block checks the address is in the ECAM window and extracts bus, device, function and register offset. The Root Complex issues a configuration request. The switch path forwards it by bus range to the target function. The function reads its configuration space and returns a response. The response returns through the path to the Root Complex, is correlated by the host configuration block, and delivered to software as the load result.Host softwareConfig blockRoot ComplexSwitch pathFunctionload from ECAMaddressidentity + registeroffsetconfigurationrequestforwarded by busrangeresponse from configspaceresponse returnedcorrelated tooutstanding requestload result
Figure 1 — a configuration read traversing the path. The host's configuration mechanism converts an ECAM load into a configuration access; the hierarchy forwards it by bus range; the target function answers from its configuration space; the response returns and is correlated back to the original load. Solid arrows travel outward, dashed arrows return.

A write follows the same path with the data travelling outward and the response carrying completion rather than data. The important structural point is identical: one software operation, one configuration access, one response, correlated back. §9's RTL is about owning that correspondence.

7. Microarchitecture — The Host-Side Front End

The path in §6 shows who talks to whom. This is what sits inside the "config block" actor, because that is where the chapter's RTL lives.

Outbound. A system-bus request arrives carrying an address, a direction, data, and byte enables. An ECAM-range check decides whether this address belongs to the configuration window at all — and if it does not, the request is not this block's business. If it does, field extraction decomposes the offset into bus, device, function, and register offset. A configuration-request engine takes that identity and issues a configuration access into the hierarchy.

Inbound. A response returns from the hierarchy. Requester correlation matches it to the outstanding request — the step that makes a response mean something, since a response carries no memory of which system-bus operation caused it. A system-bus response is then produced and returned to the requester.

Illustrative host and Root Complex implementation — not PCIe-mandated partitioning. Real designs place these boundaries differently and frequently integrate the configuration path with other host-bridge functions.

8. RTL — The ECAM Decoder

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Converts a system-bus access falling in the ECAM window into
// an internal configuration request.
// Address bit ranges: NORMATIVE (ECAM). Interface and structure: illustrative.
module ecam_decode #(
  parameter int SYS_ADDR_W = 64
) (
  input  logic                  clk,
  input  logic                  rst_n,
 
  // System-bus side.
  input  logic                  sys_valid,
  output logic                  sys_ready,
  input  logic [SYS_ADDR_W-1:0] sys_addr,
  input  logic                  sys_write,
  input  logic [31:0]           sys_wdata,
  input  logic [3:0]            sys_be,
 
  // ECAM window configuration. One segment group; see §3.
  input  logic [SYS_ADDR_W-1:0] ecam_base,     // must be 256 MB aligned
  input  logic                  ecam_enable,
 
  // Internal configuration request.
  output logic                  cfg_valid,
  input  logic                  cfg_ready,
  output logic [7:0]            cfg_bus,        // conventional BDF
  output logic [4:0]            cfg_device,
  output logic [2:0]            cfg_function,
  output logic [11:0]           cfg_offset,     // 0..4095, dword-aligned
  output logic                  cfg_write,
  output logic [31:0]           cfg_wdata,
  output logic [3:0]            cfg_be,
 
  // Locally rejected: in-window but malformed, or out of window entirely.
  // NO configuration request is generated for these.
  output logic                  reject_valid,
  input  logic                  reject_ready,
  output logic                  reject_unaligned,
  output logic                  reject_out_of_window
);
 
  // 8 (bus) + 5 (device) + 3 (function) + 12 (offset) = 28 bits = 256 MB.
  localparam int ECAM_ADDR_BITS = 28;
  localparam logic [SYS_ADDR_W-1:0] ECAM_SIZE = SYS_ADDR_W'(1) << ECAM_ADDR_BITS;
 
  // Window test. The >= comparison is evaluated on the raw address so the
  // subtraction below is only meaningful when it succeeds — computing the
  // offset first and testing it would wrap for addresses below the base.
  wire in_window = ecam_enable
                && (sys_addr >= ecam_base)
                && ((sys_addr - ecam_base) < ECAM_SIZE);
 
  wire [SYS_ADDR_W-1:0] off = sys_addr - ecam_base;
 
  // Field extraction. These bit positions are the ECAM layout of §3.
  wire [7:0]  d_bus      = off[27:20];
  wire [4:0]  d_device   = off[19:15];
  wire [2:0]  d_function = off[14:12];
  wire [11:0] d_offset   = {off[11:2], 2'b00};   // dword-aligned offset
 
  // Configuration accesses are dword-based: bits 11:2 select the dword and
  // bits 1:0 select bytes within it. A byte-granular address is expressed via
  // byte enables, not via the low address bits, so a non-zero off[1:0] is a
  // malformed request rather than a byte access.
  wire unaligned = in_window && (off[1:0] != 2'b00);
 
  wire req_ok  = in_window && !unaligned;
  wire req_bad = !in_window || unaligned;
 
  // One outstanding operation. Ready depends on which output path is busy,
  // never on sys_valid.
  logic pend_q;          // a configuration request is presented
  logic rej_q;           // a rejection is presented
  logic rej_unal_q, rej_oow_q;
 
  assign sys_ready = !pend_q && !rej_q;
  wire   accept    = sys_valid && sys_ready;
 
  // Both valids are REGISTERED and never observe their own ready.
  assign cfg_valid            = pend_q;
  assign reject_valid         = rej_q;
  assign reject_unaligned     = rej_unal_q;
  assign reject_out_of_window = rej_oow_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      pend_q <= 1'b0; rej_q <= 1'b0;
      rej_unal_q <= 1'b0; rej_oow_q <= 1'b0;
      cfg_bus <= '0; cfg_device <= '0; cfg_function <= '0; cfg_offset <= '0;
      cfg_write <= 1'b0; cfg_wdata <= '0; cfg_be <= '0;
    end else begin
      if (pend_q && cfg_ready)    pend_q <= 1'b0;
      if (rej_q  && reject_ready) rej_q  <= 1'b0;
 
      if (accept) begin
        if (req_ok) begin
          // Decoded fields are captured, so they are stable for the whole
          // time the request is presented regardless of what the system bus
          // does next.
          cfg_bus      <= d_bus;
          cfg_device   <= d_device;
          cfg_function <= d_function;
          cfg_offset   <= d_offset;
          cfg_write    <= sys_write;
          cfg_wdata    <= sys_wdata;
          cfg_be       <= sys_be;
          pend_q       <= 1'b1;
        end else begin
          // Rejected LOCALLY. No configuration request is generated, so a
          // malformed or out-of-window access never reaches the hierarchy.
          rej_unal_q <= unaligned;
          rej_oow_q  <= !in_window;
          rej_q      <= 1'b1;
        end
      end
    end
  end
 
  // Elaboration and runtime expectations, stated rather than assumed.
  //   - ecam_base must be 256 MB aligned: ecam_base[27:0] == 0.
  //   - See §10 P1 for the assertion.
 
endmodule

Classification: synthesizable. Address bit ranges are normative; everything else is illustrative.

What it models: the conversion of a processor-issued address into a configuration target identity.

What it teaches — three things:

  1. The window test must precede the subtraction. sys_addr - ecam_base wraps for addresses below the base, so an implementation that computes the offset first and range-checks it will accept addresses far outside the window. Comparing the raw address first makes the offset meaningful only when it is used.
  2. Rejection is a separate outcome from a configuration request. An out-of-window or malformed access produces no PCIe activity. Conflating "the host rejected this" with "the device did not answer" is the confusion §9 is about.
  3. Decoded fields are captured, not re-derived. Holding the extracted identity in registers means it is stable for the life of the request no matter what the system bus does, which is what P3 asserts.

Deliberately simplified: one outstanding operation; one segment group, so no group selection; 32-bit data with 4 byte enables; and no distinction among access sizes beyond byte enables.

Production implication: a real host mechanism supports multiple outstanding accesses, handles several segment groups, integrates with the processor's memory ordering rules, and must define behaviour for accesses that the hierarchy cannot deliver.

9. RTL — Outstanding Access and Terminal Outcome

A configuration access can end in several ways, and the host must produce exactly one system-visible result whichever way it ends.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Tracks one outstanding configuration access and classifies
// how it terminated. The TAG is implementation metadata for this teaching
// model — not a PCIe protocol identifier.
package cfg_outcome_pkg;
  typedef enum logic [2:0] {
    CFG_OK          = 3'd0,  // target answered with data / completion
    CFG_UNSUPPORTED = 3'd1,  // target answered, but the access was refused
    CFG_NO_TARGET   = 3'd2,  // hierarchy reported no function at that identity
    CFG_TIMEOUT     = 3'd3,  // no answer within this implementation's window
    CFG_LOCAL_REJECT= 3'd4   // never left the host: malformed or out of window
  } cfg_outcome_e;
endpackage
 
module cfg_access_tracker
  import cfg_outcome_pkg::*;
#(
  parameter int TAG_W          = 4,
  parameter int TIMEOUT_CYCLES = 4096
) (
  input  logic         clk,
  input  logic         rst_n,
 
  // Request accepted toward the hierarchy, with the identity that produced it.
  input  logic         req_accept,
  input  logic [7:0]   req_bus,
  input  logic [4:0]   req_device,
  input  logic [2:0]   req_function,
  input  logic [11:0]  req_offset,
  output logic [TAG_W-1:0] req_tag,
 
  // Response from the hierarchy.
  input  logic         rsp_valid,
  input  logic [TAG_W-1:0] rsp_tag,
  input  logic [31:0]  rsp_data,
  input  logic         rsp_unsupported,
  input  logic         rsp_no_target,
 
  // Locally rejected access, from ecam_decode.
  input  logic         local_reject,
 
  // Exactly one system-visible result per accepted access.
  output logic         res_valid,
  input  logic         res_ready,
  output logic [31:0]  res_data,
  output cfg_outcome_e res_outcome,
 
  // The identity that produced this result — retained for diagnosis.
  output logic [7:0]   res_bus,
  output logic [4:0]   res_device,
  output logic [2:0]   res_function,
  output logic [11:0]  res_offset,
 
  output logic         busy
);
 
  localparam int TMO_W = (TIMEOUT_CYCLES <= 1) ? 1 : $clog2(TIMEOUT_CYCLES);
 
  initial begin
    if (TIMEOUT_CYCLES < 1) $fatal(1, "TIMEOUT_CYCLES must be at least 1");
  end
 
  logic             out_q;       // an access is outstanding
  logic [TAG_W-1:0] tag_q;
  logic [TMO_W-1:0] tmo_q;
  logic [7:0]       bus_q;
  logic [4:0]       dev_q;
  logic [2:0]       fn_q;
  logic [11:0]      off_q;
 
  assign req_tag = tag_q;
  assign busy    = out_q;
 
  assign res_bus      = bus_q;
  assign res_device   = dev_q;
  assign res_function = fn_q;
  assign res_offset   = off_q;
 
  // A response belongs to this access only if one is outstanding and the tag
  // matches. A late response from a timed-out access carries a stale tag —
  // the same protection Chapter 7.3 established for discovery probes.
  wire rsp_match = out_q && rsp_valid && (rsp_tag == tag_q);
  wire tmo_fire  = out_q && (tmo_q == '0);
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      out_q <= 1'b0; tag_q <= '0; tmo_q <= '0;
      bus_q <= '0; dev_q <= '0; fn_q <= '0; off_q <= '0;
      res_valid <= 1'b0; res_data <= '0; res_outcome <= CFG_OK;
    end else begin
      if (res_valid && res_ready) res_valid <= 1'b0;
 
      if (local_reject && !out_q) begin
        // Never entered the hierarchy. A result is still owed to software,
        // and it must be distinguishable from a device that did not answer.
        res_valid   <= 1'b1;
        res_data    <= '0;
        res_outcome <= CFG_LOCAL_REJECT;
      end else if (req_accept && !out_q) begin
        out_q <= 1'b1;
        tag_q <= tag_q + 1'b1;      // new tag: stale responses cannot match
        tmo_q <= TMO_W'(TIMEOUT_CYCLES - 1);
        bus_q <= req_bus; dev_q <= req_device;
        fn_q  <= req_function; off_q <= req_offset;
      end else if (out_q) begin
        if (rsp_match) begin
          // RESPONSE WINS the race with the timeout: tested first, so exactly
          // one branch writes state.
          out_q       <= 1'b0;
          res_valid   <= 1'b1;
          res_data    <= rsp_unsupported || rsp_no_target ? 32'h0 : rsp_data;
          res_outcome <= rsp_no_target   ? CFG_NO_TARGET
                       : rsp_unsupported ? CFG_UNSUPPORTED
                                         : CFG_OK;
        end else if (tmo_fire) begin
          out_q       <= 1'b0;
          res_valid   <= 1'b1;
          res_data    <= '0;
          res_outcome <= CFG_TIMEOUT;    // UNRESOLVED — not "absent"
        end else begin
          tmo_q <= tmo_q - 1'b1;
        end
      end
    end
  end
 
endmodule

Classification: synthesizable, with a compile-time enumeration.

What it models: ownership of one configuration access from issue to a single system-visible result, with the terminal outcome classified rather than collapsed.

What it teaches — three things:

  1. Every accepted operation owes exactly one result, including ones that never left the host. The CFG_LOCAL_REJECT path exists because software issued a load and is waiting for it. A design that only produces results for accesses that entered the hierarchy hangs the requester on a malformed address.
  2. The identity is retained until the result is delivered. res_bus/res_device/res_function/res_offset are outputs, not internal state, because a result that does not say what it was about forces the consumer to remember — and a consumer that remembers wrongly misattributes the failure.
  3. The tag makes stale attribution impossible rather than unlikely, on the same reasoning as Chapter 7.3: a late response for a timed-out access cannot match the next access's tag, so it is discarded instead of returning one function's register content for another.

Deliberately simplified: one outstanding access; the tag is invented for this model and its space must be large enough that a stale response cannot alias after a wrap; and there is no retry policy.

Production implication: a real host supports multiple outstanding accesses, uses whatever request/response association the protocol provides rather than an invented tag, sizes timeouts against the platform's real worst case, and defines a retry and error-reporting policy.

10. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SVA over ecam_decode and cfg_access_tracker. Implementation invariants for
// THESE designs — not PCIe protocol requirements.
 
// LEGALITY — P1: the ECAM base is 256 MB aligned. The address layout assumes
// the field bits are the low 28 bits of the offset; a misaligned base makes
// every extracted identity wrong while every address still looks plausible.
property p_ecam_base_aligned;
  @(posedge clk) disable iff (!rst_n)
  ecam_enable |-> (ecam_base[ECAM_ADDR_BITS-1:0] == '0);
endproperty
a_base_aligned : assert property (p_ecam_base_aligned);
 
// SAFETY — P2: an out-of-window or malformed access never produces a
// configuration request. Catches a decoder that emits into the hierarchy for
// an address it should have rejected locally.
property p_no_request_when_rejected;
  @(posedge clk) disable iff (!rst_n)
  (accept && req_bad) |=> !$rose(cfg_valid);
endproperty
a_no_bad_request : assert property (p_no_request_when_rejected);
 
// STABILITY — P3: the decoded identity and payload are stable while the
// configuration request is presented. A field that changed mid-handshake
// would deliver an access to a function nobody addressed.
property p_decoded_stable_under_stall;
  @(posedge clk) disable iff (!rst_n)
  (cfg_valid && !cfg_ready) |=> (cfg_valid && $stable(cfg_bus) && $stable(cfg_device)
                                 && $stable(cfg_function) && $stable(cfg_offset)
                                 && $stable(cfg_write) && $stable(cfg_wdata)
                                 && $stable(cfg_be));
endproperty
a_decoded_stable : assert property (p_decoded_stable_under_stall);
 
// CORRECTNESS — P4: the extracted identity matches the ECAM layout. This is
// the property that catches a shift or bit-range error, which produces a
// perfectly well-formed access aimed at the wrong function.
property p_extraction_matches_layout;
  @(posedge clk) disable iff (!rst_n)
  (accept && req_ok) |=> (cfg_bus      == $past(off[27:20]))
                      && (cfg_device   == $past(off[19:15]))
                      && (cfg_function == $past(off[14:12]))
                      && (cfg_offset   == $past({off[11:2], 2'b00}));
endproperty
a_extraction_correct : assert property (p_extraction_matches_layout);
 
// OWNERSHIP — P5: no new access is accepted while one is outstanding.
property p_single_outstanding;
  @(posedge clk) disable iff (!rst_n)
  busy |-> !(req_accept && !out_q);
endproperty
a_single_outstanding : assert property (p_single_outstanding);
 
// CONSERVATION — P6: exactly one result per accepted access. Catches both a
// dropped result (software waits forever) and a duplicated one.
property p_one_result_per_access;
  @(posedge clk) disable iff (!rst_n)
  (res_valid && !res_ready) |=> res_valid;
endproperty
a_result_sticky : assert property (p_one_result_per_access);
 
// SAFETY — P7: a stale response never satisfies the outstanding access.
// Without this, a late answer for a timed-out access is attributed to the
// next one — reporting one function's register content for another's.
property p_stale_response_ignored;
  @(posedge clk) disable iff (!rst_n)
  (out_q && rsp_valid && rsp_tag != tag_q && !tmo_fire) |=> out_q;
endproperty
a_stale_ignored : assert property (p_stale_response_ignored);
 
// RACE — P8: a matching response and a timeout expiring together resolve as
// the response. The documented contract made checkable.
property p_response_beats_timeout;
  @(posedge clk) disable iff (!rst_n)
  (rsp_match && tmo_fire) |=> (res_valid && res_outcome != CFG_TIMEOUT);
endproperty
a_response_wins : assert property (p_response_beats_timeout);
 
// CORRECTNESS — P9: a result carries the identity that produced it. Catches a
// result reported against an identity the host never addressed, which sends
// the whole diagnosis to the wrong function.
property p_result_identity_retained;
  @(posedge clk) disable iff (!rst_n)
  (req_accept && !out_q) |=> ((res_bus == $past(req_bus))
                           && (res_device == $past(req_device))
                           && (res_function == $past(req_function)))
                             until_with (res_valid && res_ready);
endproperty
a_identity_retained : assert property (p_result_identity_retained);

P4 is the highest-value property here. A shift error — << 16 instead of << 15 for device, say — produces an access that is perfectly well formed, correctly delivered, and answered by the wrong function. Nothing errors anywhere. The host reads a plausible value from a function it did not intend to address, and the resulting topology description is quietly wrong. No end-to-end test detects this unless it happens to have two populated identities that the error maps between.

P1 catches the same class one level up. If ecam_base is not 256 MB aligned, the base's low bits overlap the field bits, so every extracted identity is corrupted by the base's own value. Every address still looks reasonable and every access goes somewhere plausible.

P2 draws the boundary the whole chapter rests on. A malformed access must not become PCIe traffic. Without it, an address arithmetic bug in software produces configuration accesses to arbitrary identities.

11. Verification

Monitors observe: the system-bus handshake with its address, the extracted identity and offset, the reject path, the outstanding-access state, responses with their tags, and the system-visible result with its outcome and retained identity.

The scoreboard independently computes the expected identity and offset from the system address, using its own implementation of the §3 layout. It must not call the design's extraction logic — a checker that reuses the design's shifts agrees with the design about a shift error, and P4 becomes the only thing standing between that bug and silicon.

Scenarios:

  • Valid read and valid write. Baseline for each: correct identity extracted, one request, one result, correct outcome.
  • Base address of the window. Offset 0 — identity 0:00.0, register 0. Every field at its minimum.
  • Top address of the window. Offset 0x0FFF_FFFF — bus 255, device 31, function 7, register 0xFFC. Every field at its maximum. A width or shift error shows here and often nowhere else.
  • Each field swept alone. Vary bus with device/function/offset fixed, then device, then function. Verify only the intended field changes — this is what catches a shift that overlaps two fields.
  • First and last register offset. 0 and 4092 (the last dword). Verify offset extraction covers the full 4 KB.
  • One byte below the window and one byte above. Verify both reject as out-of-window and generate no configuration request (P2).
  • Unaligned address. off[1:0] != 0. Verify local rejection, not a malformed request into the hierarchy.
  • Absent function. Response indicates no target. Verify CFG_NO_TARGET, distinct from timeout.
  • Delayed response. Sweep latency up to and beyond the timeout. Verify the response is used right up to the boundary and that the timeout fires past it (P8).
  • Stale response after timeout. Time out one access, issue another, deliver the first's response with its old tag. Verify P7 — it must not satisfy the new access. This must be written deliberately; random stimulus does not produce it.
  • Reset with an access outstanding. Verify no stale result is presented afterwards and the next access is accepted normally.
  • Misaligned ECAM base. Configure a base that is not 256 MB aligned. Verify P1 fires — a property never observed to fail has not been shown to work.

Coverage should include: every field at minimum and maximum; addresses at both window boundaries and one beyond each; aligned and unaligned; every terminal outcome in cfg_outcome_e; response latency buckets around the timeout boundary; and reset in each tracker state.

12. Debugging

Reference scenario: the BDF in software logs is correct, but a configuration read returns no device.

The identity is right, so Chapters 7.4–7.6 are not the problem. The failure is somewhere in the conversion or the path — and the method is the last-known-good-boundary walk from Chapter 7.1.

1. Is the ECAM address what it should be? Compute it by hand from §3 and compare with what software issued. An arithmetic error in the host's address formation is common and costs nothing to exclude.

2. Is the ECAM base correct and 256 MB aligned? A wrong base sends every access to a plausible-looking wrong place. A misaligned base corrupts every extracted identity (P1).

3. Did the decoder extract the right identity? Observe the decoded fields. A shift error here produces a well-formed access to the wrong function (P4) — the identity in the log is right and the identity on the wire is not.

4. Was a configuration request emitted at all? If the outcome is CFG_LOCAL_REJECT, nothing was ever sent and no device is implicated. This single check separates a host-side bug from everything downstream.

5. Was it forwarded? Bus-range forwarding is Chapter 7.4. A range that excludes the target region means the access never arrives, and the outcome will be no-target or timeout for a healthy function.

6. Did the function receive it? Observe at the target. Arrival distinguishes a path fault from an endpoint fault.

7. Did the function respond? A function that receives and does not answer is Chapter 7.2's readiness question or a configuration-block fault.

8. Did the response return? The return path fails independently of the outbound one.

9. Was the response correlated correctly? A stale response attributed to this access (P7) returns another function's data. A dropped result leaves software waiting.

13. Common Misconceptions

  • "ECAM is the device's MMIO." ECAM is a host-side memory-mapped window that the host's configuration mechanism converts into configuration accesses. A device's MMIO is a separate address range assigned during resource allocation (Chapter 7.8) reaching entirely different registers.
  • "BDF and the ECAM address are the same thing." BDF is the identity. The ECAM address is a host address computed from that identity plus a register offset and an implementation-specific base. The same function has one BDF and a different ECAM address on a platform with a different base.
  • "A configuration read is a memory read that goes all the way to the device." The processor issues a load, and the host's configuration mechanism converts it into a configuration access carrying identity and offset. The device never sees the ECAM address.
  • "A valid BDF guarantees the function exists." A BDF is a coordinate in a namespace. Most coordinates are unoccupied. Forming a valid address for an unoccupied identity is normal — that is exactly what discovery does (Chapter 7.3).
  • "An access that does not return data means the endpoint is bad." It may mean the address was out of window, the request was malformed and locally rejected, the path did not forward it, the function is not yet ready, or the response was lost. §9's outcomes exist to distinguish these.
  • "The ECAM formula is independent of how the Routing ID is interpreted." The shifts of 20, 15, and 12 follow directly from an 8-bit bus, 5-bit device, and 3-bit function field. A different Routing ID interpretation implies a different decomposition of the same address bits.
  • "Software reads endpoint registers directly, without Root Complex participation." Every configuration access is generated by the host's configuration mechanism and issued into the hierarchy by the Root Complex. The processor cannot address a function's configuration space in any other way.
  • "Configuration accesses and ordinary transaction traffic are the same." They target different spaces, are addressed differently — identity plus offset rather than an assigned address — and are available at different times. Configuration access works before any resources are assigned; ordinary traffic to a device's regions does not.

14. Understanding Check

15. What's Next

Configuration access works as soon as a function is reachable. It is deliberately independent of everything else, because it is the mechanism through which everything else is set up.

Chapter 7.8 — Resource Allocation uses it for the last stage of enumeration: discovering what each function requires and assigning it non-conflicting system resources. That is the step that turns a function which is discovered, identified, numbered, and readable into one that is usable — and its characteristic failure is a device whose configuration space reads perfectly while nothing else about it works.