Skip to content

PCIe · Module 7

Device Number Assignment — Position Within a Bus

The device coordinate identifies a position within one bus and means nothing without it. Why PCIe's point-to-point links leave most of that space unused below a Root Port, where device numbers still vary, and how a target decoder and candidate scanner encode the locality rule.

Chapter 7.4 got a configuration access to the right region of the hierarchy. Region membership is where its job ended.

Arriving in the right region is not arriving at the right thing.

Once the correct bus is known, how does the configuration namespace distinguish positions within that bus?

1. Device Identity Is Contextual

Take the hierarchy from Chapter 7.4. Bus 2 is the switch's internal region, holding two downstream ports. Bus 3 and bus 4 each hold one endpoint.

Now ask what "Device 0" refers to.

On bus 2 it is downstream port A. On bus 3 it is Endpoint A. On bus 4 it is Endpoint B. Three different things, same device number, and no ambiguity anywhere — because the bus coordinate distinguishes them.

This is not a quirk to be worked around. It is the design: the identifier mirrors the tree (Chapter 2.6), so each coordinate is interpreted within the scope established by the one above it.

2. The Field

Under the conventional Routing ID interpretation, the device coordinate is 5 bits wide, giving 32 positions numbered 0 through 31 within any one bus.

For completeness, since this chapter is where the numbers become concrete: the bus coordinate is 8 bits (0–255), the device coordinate is 5 bits (0–31), and the function coordinate Chapter 7.6 covers is 3 bits (0–7). Together they form the 16-bit identifier commonly written bus:device.function and abbreviated BDF.

Why the widths matter here. The field width creates a finite candidate space, and a finite candidate space is what makes discovery by probing tractable at all. Software cannot ask a bus what it contains (Chapter 7.3) — it can only direct accesses at positions. Thirty-two is small enough to enumerate exhaustively and large enough to accommodate the arrangements the conventional scheme was designed for.

That is the whole significance of the number. Memorising 5 bits is not the lesson; understanding that a bounded space is what makes probe-based discovery possible is.

3. The PCIe Difference

Here the intuition inherited from PCI stops applying, and the difference is structural rather than incidental.

In PCI, a bus was a shared multi-drop wire (Chapter 1.3). Several physically distinct cards could sit on one bus at different device positions, and the 32-position space existed to distinguish them. Device numbers varied because there were genuinely several devices to distinguish.

In PCIe, a Link is point-to-point (Chapter 6.1). Exactly one component sits at the far end of a Root Port's Link or a switch downstream port's Link. The region that Link creates therefore contains one device position worth caring about, and the device attached there appears at device 0.

So on a bus created by a Root Port or a switch downstream port, positions 1 through 31 correspond to nothing. The candidate space is 32 wide and one position is occupied.

4. Where Device Numbers Still Vary

The interesting case is the one §3's rule does not cover.

A switch's internal region is not created by a point-to-point Link — it is created by the switch to hold its own downstream ports. Several downstream ports sit on it, and they need distinguishing.

Two buses compared. Bus 2, the switch internal region, holds downstream port A at device 0 and downstream port B at device 1, with positions 2 to 31 unoccupied. Bus 3, created by a point-to-point link below downstream port A, holds endpoint A at device 0 with positions 1 to 31 corresponding to nothing.Bus 2 — switchinternalseveral positionsoccupiedDevice 0downstream port ADevice 1downstream port BDevices 2–31unoccupied on this busBus 3 — below aLinkpoint-to-point: onedevice12
Figure 1 — two buses from the same hierarchy, with very different occupancy. Bus 2 is the switch's internal region, where several downstream ports sit at distinct device positions. Bus 3 is created by a point-to-point Link, so it holds a single device at position 0 and positions 1 through 31 correspond to nothing.

So the device coordinate does real work in PCIe — just not where a PCI-trained intuition expects it. It distinguishes ports within a switch, and it degenerates to a single value on the regions created by Links.

The practical consequence for verification. A test environment modelling only endpoints behind Root Ports will exercise device number 0 and nothing else, and will never test the decode path for any other value. A switch's internal region is where the device coordinate is genuinely exercised, and §9 makes it a required scenario for that reason.

5. Probing a Bus

With the bus known and the candidate space bounded, discovery of what a bus contains is a directed sweep of positions, using Chapter 7.3's probe-and-response model:

For each candidate position on this bus: direct a configuration access at it, classify the outcome as present or absent, record it, and move to the next.

This is host software's process, not endpoint hardware. No PCIe component contains a loop over device numbers. §7 shows a hardware-assisted scanner because the ownership problem it solves — one probe outstanding, results attributed to the right candidate — is a genuine hardware problem, not because PCIe requires enumeration to be built that way.

What the sweep produces is a statement about one bus: which of its positions are occupied. That statement is meaningless without the bus it refers to, which is §1's point turned into a data structure — and §7's device_present bitmap carries its bus with it for exactly that reason.

6. RTL — Hierarchical Target Match

The receiving side of the identity is much simpler than the discovering side, and it encodes the locality rule directly.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Decides whether an incoming configuration access targets
// this function's bus and device position.
// NOT a packet parser and NOT a complete configuration-access decoder.
module cfg_target_match #(
  parameter int BUS_W = 8,     // verified: PCIe bus numbers are 8 bits
  parameter int DEV_W = 5      // verified: PCIe device numbers are 5 bits
) (
  // Identity carried by the incoming access, already extracted.
  input  logic [BUS_W-1:0] req_bus,
  input  logic [DEV_W-1:0] req_device,
 
  // This function's own captured identity. Not valid until enumeration has
  // established it — see identity_valid.
  input  logic [BUS_W-1:0] local_bus,
  input  logic [DEV_W-1:0] local_device,
  input  logic             identity_valid,
 
  output logic             bus_match,
  output logic             device_match,
  output logic             position_match
);
 
  // The bus comparison comes FIRST and everything else depends on it. This is
  // the locality rule expressed in logic: a device number is only meaningful
  // within a bus, so a device comparison that does not require a bus match is
  // comparing something that has no meaning on its own.
  assign bus_match = identity_valid && (req_bus == local_bus);
 
  // Deliberately gated by bus_match rather than computed independently. An
  // ungated device comparison would match accesses aimed at the same device
  // position on a DIFFERENT bus — which, per Chapter 7.4, is a position that
  // legitimately exists and belongs to somebody else.
  assign device_match = bus_match && (req_device == local_device);
 
  // The function coordinate is deliberately absent. Chapter 7.6 adds it, and
  // the name says "position" rather than "target" because a position is not
  // yet a complete target.
  assign position_match = device_match;
 
endmodule

Classification: synthesizable.

What it models: the receiving end of hierarchical identity — how a function decides whether an access is aimed at its position.

What it teaches: that the coordinate ordering is not stylistic. Writing device_match = (req_device == local_device) without the bus qualifier produces a function that claims accesses aimed at the same device position anywhere in the hierarchy. In a system with one endpoint behind one Root Port that bug is undetectable, because every access that arrives is for the only device that exists. It becomes visible when a second bus appears — which is often when a switch is added late in a project.

Deliberately simplified: no function coordinate (Chapter 7.6); no access-type distinction; no handling of accesses that match nothing; and identity is an input rather than modelled as captured.

Production implication: a real decoder must extract identity from the access as Chapter 7.7 defines, include the function coordinate, capture its own identity at the point enumeration establishes it, define behaviour for non-matching accesses, and coordinate with the readiness gating of Chapter 7.2.

7. RTL — Candidate Scanner and Presence Bitmap

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Sweeps the device-position space of ONE bus and records
// which positions responded.
// NOT required by PCIe; enumeration is normally host software.
module device_scanner #(
  parameter int BUS_W = 8,
  parameter int DEV_W = 5                    // 32 candidate positions
) (
  input  logic                    clk,
  input  logic                    rst_n,
 
  // Start a scan of one bus. Ignored unless idle.
  input  logic                    scan_start,
  input  logic [BUS_W-1:0]        scan_bus,
 
  // Probe interface, toward Chapter 7.3's probe engine.
  output logic                    probe_valid,
  input  logic                    probe_ready,
  output logic [BUS_W-1:0]        probe_bus,
  output logic [DEV_W-1:0]        probe_device,
 
  // Probe outcome. `present` is the classification, not raw response data.
  input  logic                    result_valid,
  input  logic                    result_present,
 
  output logic                    scan_busy,
  output logic                    scan_done,
  output logic [(1<<DEV_W)-1:0]   device_present,
  output logic [BUS_W-1:0]        scanned_bus     // the bitmap's bus context
);
 
  localparam int NUM_DEVICES = 1 << DEV_W;
 
  typedef enum logic [1:0] {
    SCAN_IDLE  = 2'd0,
    SCAN_ISSUE = 2'd1,   // probe presented, awaiting acceptance
    SCAN_WAIT  = 2'd2,   // probe accepted, awaiting outcome
    SCAN_DONE  = 2'd3
  } scan_state_e;
 
  scan_state_e            state_q;
  logic [DEV_W-1:0]       cand_q;
  logic [BUS_W-1:0]       bus_q;
  logic [NUM_DEVICES-1:0] present_q;
 
  // The bus is captured once at scan start and held for the entire sweep. A
  // bitmap assembled from probes to different buses describes no bus that
  // exists — the locality rule of §1 as a hardware requirement.
  assign scanned_bus    = bus_q;
  assign device_present = present_q;
 
  // probe_valid is derived from REGISTERED state and held until accepted. It
  // never observes probe_ready.
  assign probe_valid  = (state_q == SCAN_ISSUE);
  assign probe_bus    = bus_q;
  assign probe_device = cand_q;
 
  assign scan_busy = (state_q != SCAN_IDLE);
  assign scan_done = (state_q == SCAN_DONE);
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      state_q   <= SCAN_IDLE;
      cand_q    <= '0;
      bus_q     <= '0;
      present_q <= '0;
    end else begin
      case (state_q)
        SCAN_IDLE: begin
          if (scan_start) begin
            bus_q     <= scan_bus;
            cand_q    <= '0;
            present_q <= '0;     // a new scan starts from no knowledge
            state_q   <= SCAN_ISSUE;
          end
        end
 
        SCAN_ISSUE: begin
          if (probe_ready) state_q <= SCAN_WAIT;
        end
 
        SCAN_WAIT: begin
          if (result_valid) begin
            // Exactly one bit is written, indexed by the candidate that is
            // still held in cand_q. Because cand_q cannot advance while a
            // probe is outstanding, the result cannot land on the wrong bit.
            present_q[cand_q] <= result_present;
 
            if (cand_q == DEV_W'(NUM_DEVICES - 1)) begin
              state_q <= SCAN_DONE;
            end else begin
              cand_q  <= cand_q + 1'b1;
              state_q <= SCAN_ISSUE;
            end
          end
        end
 
        SCAN_DONE: begin
          if (scan_start) begin
            bus_q     <= scan_bus;
            cand_q    <= '0;
            present_q <= '0;
            state_q   <= SCAN_ISSUE;
          end
        end
 
        default: state_q <= SCAN_IDLE;
      endcase
    end
  end
 
endmodule

Classification: synthesizable.

What it models: ownership of a sweep over a bounded candidate space — one probe outstanding, one result per candidate, one bit written per result.

What it teaches — three things:

  1. The bus context belongs with the bitmap. scanned_bus is an output, not internal state, because a presence bitmap without its bus describes nothing. This is §1's locality rule made structural rather than remembered.
  2. The candidate cannot advance while a probe is outstanding. cand_q only increments in SCAN_WAIT on result_valid, so the index used to write the bitmap is necessarily the index that was probed. Advancing on issue rather than on result would attribute every result to the following candidate.
  3. A new scan clears the bitmap. Retaining bits from a previous sweep of a different bus produces a result that is a mixture of two buses and looks entirely plausible.

Deliberately simplified: one probe at a time, so a full sweep is 32 sequential probes; result_valid is assumed to be a response to the outstanding probe, with the stale-response protection living in Chapter 7.3's probe engine; no distinction between absent and unresolved outcomes; and no early termination.

Production implication: real enumeration is normally software, and where hardware assists it would overlap probes for speed — which reintroduces the attribution problem this design avoids by construction — distinguish absent from unresolved so a timeout is not recorded as absence, and handle a bus that becomes unreachable mid-sweep.

8. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SVA over cfg_target_match and device_scanner. Implementation invariants
// for THESE designs — not PCIe protocol requirements.
 
// LOCALITY — P1: a device match requires a bus match. THE property of this
// chapter. Without it a function claims accesses aimed at the same device
// position on a different bus, and in a single-bus test system that bug is
// completely invisible.
property p_device_requires_bus;
  @(posedge clk) disable iff (!rst_n)
  device_match |-> bus_match;
endproperty
a_device_needs_bus : assert property (p_device_requires_bus);
 
// SAFETY — P2: nothing matches before identity is established. During
// enumeration a function's identity genuinely is unset, and matching on a
// reset-value identity claims accesses aimed elsewhere.
property p_no_match_without_identity;
  @(posedge clk) disable iff (!rst_n)
  !identity_valid |-> (!bus_match && !device_match && !position_match);
endproperty
a_no_match_unset : assert property (p_no_match_without_identity);
 
// STABILITY — P3: the candidate index is stable while a probe is outstanding.
// If it could advance, the result would be written to the wrong bitmap bit —
// producing a presence map that is shifted relative to reality.
property p_candidate_stable_while_outstanding;
  @(posedge clk) disable iff (!rst_n)
  (state_q == SCAN_WAIT && !result_valid) |=> $stable(cand_q);
endproperty
a_candidate_stable : assert property (p_candidate_stable_while_outstanding);
 
// OWNERSHIP — P4: no second probe is issued before the current one resolves.
property p_single_outstanding_probe;
  @(posedge clk) disable iff (!rst_n)
  (state_q == SCAN_WAIT) |-> !probe_valid;
endproperty
a_single_probe : assert property (p_single_outstanding_probe);
 
// CONSERVATION — P5: at most one bitmap bit changes per result. Catches a
// write path that updates a range of bits or the wrong index width.
property p_one_bit_per_result;
  @(posedge clk) disable iff (!rst_n)
  (state_q == SCAN_WAIT && result_valid)
    |=> ($countones(device_present ^ $past(device_present)) <= 1);
endproperty
a_one_bit : assert property (p_one_bit_per_result);
 
// CORRECTNESS — P6: the bit written corresponds to the candidate probed.
// Catches an off-by-one in the advance, which produces a presence map shifted
// by one position — plausible-looking and completely wrong.
property p_bit_matches_candidate;
  @(posedge clk) disable iff (!rst_n)
  (state_q == SCAN_WAIT && result_valid)
    |=> (device_present[$past(cand_q)] == $past(result_present));
endproperty
a_bit_correct : assert property (p_bit_matches_candidate);
 
// SAFETY — P7: the bus identity is stable for the entire scan. A bitmap
// assembled from probes to different buses describes no real bus.
property p_bus_stable_during_scan;
  @(posedge clk) disable iff (!rst_n)
  (scan_busy && !(state_q == SCAN_DONE && scan_start)) |=> $stable(scanned_bus);
endproperty
a_bus_stable : assert property (p_bus_stable_during_scan);
 
// COMPLETENESS — P8: the scan does not skip candidates. The index advances by
// exactly one per resolved probe, so every position in the space is probed.
property p_no_candidate_skipped;
  @(posedge clk) disable iff (!rst_n)
  ($changed(cand_q) && scan_busy && $past(state_q) == SCAN_WAIT)
    |-> (cand_q == DEV_W'($past(cand_q) + 1'b1));
endproperty
a_no_skip : assert property (p_no_candidate_skipped);

P1 is the chapter's property and the one most likely to be absent in real RTL. A design verified only against a topology with one endpoint behind one Root Port has exactly one bus, so an ungated device comparison behaves identically to a correct one. Every test passes. The bug surfaces when a switch is introduced and two buses exist — frequently late, in a system-level bring-up, where a function claiming accesses meant for its sibling presents as data corruption rather than as a decode bug.

P6 catches the off-by-one with the worst signature. A scanner that advances the candidate on issue rather than on result writes each outcome to the next position's bit. The resulting presence map is shifted by one: a device at position 0 is reported at position 1, and position 31's result is lost. Everything about the sweep looks normal — 32 probes issued, 32 results received, a plausible bitmap produced.

P7 exists because the failure it catches is silent. Nothing in a bitmap indicates which bus it describes unless the design carries it, and a bitmap mixing two buses reports devices that exist, at positions that exist, on a bus where they do not.

9. Verification

Monitors observe: the probe interface handshake with its bus and device, results, the presence bitmap, the scanned bus, and both match outputs of the decoder.

The scoreboard independently models: which positions are occupied on each bus in the modelled topology, from its own topology description. It must not derive expected presence from the design's bitmap, and it must model presence per bus so the locality scenarios below are checkable.

Scenarios:

  • Empty bus. No positions occupied. Verify a full sweep completes and the bitmap is entirely clear.
  • One device at position 0. The common PCIe case below a Root Port. Verify exactly one bit set.
  • Several positions occupied. The switch-internal case from §4 — positions 0 and 1 occupied, the rest empty. This is the scenario that exercises the device coordinate at all, and an environment that omits it never tests any device value but 0.
  • First position occupied. Position 0 — the boundary at the low end.
  • Last legal position occupied. Position 31 — the boundary at the high end, where an index-width or loop-bound error shows. A scanner that stops at 30 never probes it and one that wraps writes its result to position 0.
  • Alternating present and absent. Verify no bleed between adjacent bits (P5).
  • Delayed results. Vary probe-to-result latency. Verify the candidate holds (P3) and the bit still lands correctly (P6).
  • Absent results throughout. Verify the sweep completes rather than stalling on the first absence.
  • Reset mid-scan. Verify a clean restart with a cleared bitmap and no stale bus context.
  • Same device number on two buses. Occupy position 3 on bus 2 and position 3 on bus 4. Scan both. Verify each bitmap reports its own bus's occupancy, and — with the decoder — verify a function on bus 2 at position 3 does not match an access aimed at bus 4 position 3. This is P1's scenario and the chapter's central lesson made executable.

Coverage should include: every device position as the sole occupied one; positions 0 and 31 specifically; occupancy patterns of density 0, 1, 2, and full; result latency buckets; reset in each scan state; and at least two distinct buses scanned in one run.

10. Debugging

Symptom: a device appears on one bus but not on another, at the same device position

Why this is not a contradiction. Two positions in two regions are two different things (Chapter 7.4). One being occupied says nothing about the other. The report is only surprising if the device number is being read as a global identifier — which is exactly the mental model this chapter exists to correct.

What to establish, in order:

  1. Which bus was actually scanned? Read the bus context alongside the bitmap. A scanner that lost or overwrote its bus identity produces a result attributed to the wrong region.
  2. Was the access forwarded to the right region? Bus-level forwarding is Chapter 7.4's subject. A range that excludes the target region means the probe never arrived, and absence is the correct — and misleading — result.
  3. Did the target decoder include the bus comparison? P1's failure mode inverted: a function that matches on device alone would respond to accesses for both buses, so a device appearing where it should not is as diagnostic as one missing.
  4. Is the position genuinely occupied on that bus? For a bus created by a point-to-point Link, only position 0 is meaningful (§3). A sweep of positions 1–31 there is expected to find nothing, and reporting that as a fault is a misreading of the topology.

The signature worth recognising. If a device is missing at a position that cannot be occupied given the topology, the fault is in the expectation, not the hardware. Checking §3's rule against the topology costs nothing and closes a surprising number of these.

Symptom: devices shift position in software logs when one is absent

What this actually indicates. Software-visible ordering in a log is a property of how the tool enumerated and printed, not of the identity scheme. A device's bus and device coordinates are established by its position in the hierarchy — they do not renumber because a sibling failed to respond.

Why the symptom appears. A listing produced in discovery order, or an index assigned by the order results came back, shifts when an entry is missing. That is a property of the listing.

What to do. Compare identities, not list positions. If the same bus and device coordinates appear in both the working and failing case, nothing renumbered and the investigation belongs at whichever position is genuinely absent. If the coordinates themselves differ between runs, that is a much more serious finding and points at bus-number assignment (Chapter 7.4) rather than at anything in this chapter.

What not to conclude. How any particular platform's software orders, caches, or renumbers its device list is implementation behaviour this chapter does not model. The defensible claim is narrower and sufficient: a position in a printed list is not an identity, and reasoning from list order is reasoning from an artefact.

11. Common Misconceptions

  • "A device number is globally unique." It identifies a position within one bus. The same device number exists on every bus in the hierarchy and refers to something different on each. Only the full identifier names one thing.
  • "A device number identifies a physical card." It identifies a position in a configuration region. The addressable unit is a function (Chapter 2.6), and one physical component may present several — which is Chapter 7.6's subject.
  • "A device number is the switch port number." A switch's downstream ports do sit at device positions on the switch's internal region, so the two correlate there. They are not the same thing: the device number is a coordinate in the configuration namespace, and it says nothing about which physical port a component is plugged into.
  • "Device numbering is arbitrary display metadata." It is part of the identity an access carries, and a function's decode logic compares against it. A wrong comparison makes a function claim or ignore accesses.
  • "A device number is enough to address a function." It is one of three coordinates. Without the bus it is unqualified, and without the function coordinate it may not identify a single addressable entity.
  • "Enumeration order defines the device number." Position in the hierarchy defines it. Enumeration discovers it; a listing's ordering is a property of the listing.
  • "The same device number cannot exist on two buses." It routinely does, and there is no ambiguity because the bus coordinate distinguishes them. Building hardware or a test environment that assumes otherwise produces the P1 bug.
  • "PCIe uses the device-number space the way PCI did." PCI had multi-drop buses where several cards sat at different positions. PCIe's Links are point-to-point, so a bus created by a Link holds one device at position 0. The field width was kept for software continuity; the space is largely unused except on switch-internal regions.
  • "Every physical device exposes exactly one software-visible function." Many do. Many do not, and the identifier's third coordinate exists precisely because "which device" is not always a complete answer — Chapter 7.6 takes that up.

12. Understanding Check

13. What's Next

Bus names a region; device names a position within it. For many components that is a complete address — but not for all of them.

Chapter 7.6 — Function Number Assignment adds the third coordinate and resolves the distinction Chapter 2.6 flagged and this chapter's last misconception raised: a physical device is not necessarily one software-visible function. One device position may present several independently addressable functions, each with its own configuration state and its own driver relationship — and that has real consequences for how a multifunction device's RTL is partitioned, and for what a config write to one function must never do to another.