Skip to content

PCIe · Module 10

Requests — What the Transaction Layer Sends, and Who Sends It

The Transaction Layer turns an operation into a packet. What a Request is, the four address spaces it can target, which devices may issue which kinds, and how an Endpoint arbitrates and dispatches transactions in RTL — without opening a TLP header.

Chapter 9.6 followed a host access from a CPU instruction to an offset inside an Endpoint's resource, and stopped at the one thing it could not explain: something crossed the Link.

A load instruction executed on a processor. A Function on the far side of a serial link, several switches away, produced data. Nothing about a load instruction travels over a serial link.

What responsibility does the PCIe Transaction Layer own, what exactly is a Request, and who is permitted to issue one?

1. What Module 9 Left Open

Go back to Chapter 9.6 §3's figure and look at the arrows between the Root Complex and the Function. They were labelled "routed by address" and "forwarded if in window" — descriptions of what happens to something, with the something left unnamed.

That something is a Transaction Layer Packet, and building it is a real job. The host's load carried an address and an expectation of data. The Function needs to know: that this is a read, that it targets memory space, which address, how much data, and — critically — who asked, so the data has somewhere to go.

None of that is implicit in an address. All of it has to be constructed, carried, and taken apart at the far end.

The Transaction Layer is the only layer that understands what a transaction means. The Data Link Layer moves packets reliably without knowing what they are for; the Physical Layer moves symbols without knowing they are packets.

2. What the Transaction Layer Owns

Read the services list once more and notice its shape. Three of the five bullets are conversions — core operation to TLP, received TLP to core operation, received completion to payload or status. That is the whole layer in one word: it is a translator with a packet on one side and a device on the other.

And the fifth bullet is the one designers forget. "Detect unsupported TLP and invoke appropriate mechanisms for handling them" is a first-class responsibility, not an error path bolted on afterwards. §12's RTL gives it a real output for exactly that reason.

3. Four Address Spaces, and What Can Be Done in Each

A request has to say where, and "where" has more than one meaning.

Normative (§2.2): "Transactions form the basis for information transfer between a Requester and Completer. Four address spaces are defined within the PCI Express architecture, and different Transaction types are defined, each with its own unique intended usage, within each address space."

Address spaceTransaction typesBasic usage
MemoryRead, Write"Transfer data to/from a memory-mapped location"
I/ORead, Write"Transfer data to/from an I/O-mapped location"
ConfigurationRead, Write"Device configuration/setup"
MessageBaseline, vendor-defined, and others"From event signaling mechanism to general purpose messaging"

Message Space is the one that did not exist in PCI, and its purpose is architectural rather than about data movement. §1.5.1 puts it plainly: the specification "uses Message Space to support all prior side-band signals, such as interrupts, power-management requests, and so on, as in-band Message transactions. You could think of PCI Express Message transactions as 'virtual wires' since their effect is to eliminate the wide array of sideband signals currently used in a platform implementation."

Per-space detail, verified (§2.2.1–§2.2.4):

SpaceRequests that existAddress forms
MemoryRead Request (with Completion); Write Request32-bit short format; 64-bit long format
I/ORead Request (with Completion); Write Request (with Completion)32-bit only
ConfigurationRead Request (with Completion); Write Request (with Completion)
Messagein-band event communication; peer-to-peer communicationrouted implicitly or explicitly

And one row connects straight back to Module 9. I/O space exists, per §2.2.2, "for compatability with legacy devices which require their use," and the same section states that "future revisions of this specification are expected to depreciate the use of I/O Space." That is the Base Specification's own corroboration of Chapter 9.3 §4, from the opposite direction: 9.3 approached it through the BAR, this approaches it through the transaction.

4. What a Request Is, and Who May Issue One

The specification's terms are precise, and precision here saves a great deal of confusion later.

Normative definitions (Terms and Acronyms):

TermDefinition
Request"A Packet used to initiate a Sequence… A Request includes some operation code, and, in some cases, it includes address and length, data, or other information."
Requester"A logical device that first introduces a Sequence into the PCI Express domain."
Requester ID"The combination of a Requester's Bus Number, Device Number, and Function Number that uniquely identifies the Requester."
Sequence"A single Request and zero or more Completions associated with carrying out a single logical transfer by a Requester."
TLP"A Packet generated in the Transaction Layer to convey a Request or Completion."
Unsupported Request (UR)"A Request Packet that specifies some action or access to some space that is not supported by the Target."

Three things follow immediately from those definitions.

A Request is an initiator, not a container. It "initiates a Sequence" — the unit of work is the Sequence, and the Request is its first packet. A Sequence has "zero or more Completions", which is where §3's asymmetry lives.

A Requester is identified by BDF. The Requester ID is the Bus/Device/Function triple of Chapters 7.4–7.6 — the same coordinate configuration access uses, now travelling inside an operational transaction so a response can be steered back. The identity you learned for enumeration is reused for correlation.

Every TLP is one or the other. A TLP conveys "a Request or Completion." That is the top-level taxonomy of the entire Transaction Layer, and this chapter owns the first half of it.

Who is permitted to issue what

Not every device may issue every kind of request, and the rules are normative device-type rules rather than conventions.

Device typeNormative rules (Base Spec §1.3.1–§1.3.2)
Root Complex"must support generation of configuration requests as a Requester"; "is permitted to support the generation of I/O requests as a Requester"
Endpoint (either kind)"a type of device that can be the Requester or Completer of a PCI Express transaction either on its own behalf or on behalf of a distinct non-PCI Express device"
PCI Express Endpoint"must support Configuration Requests as a Completer"; "must not generate I/O Requests"; "must not require I/O resources claimed through BAR(s)"
Legacy PCI Express Endpoint"must support Configuration Requests as a Completer"; "may support I/O Requests as a Completer"; "may generate I/O Requests"

5. What a Request Must Carry

A request has to convey enough for a Completer that has never heard of it to act. These are categories of information, not fields — the encoding is Module 11's.

CategoryWhy the Completer needs itWhere it came from in Module 9
Operationread or write, and in which address space — a memory read and a configuration read are different operationsthe host instruction plus the address space it targeted
Wherean address for address-routed transactions; an identity or an implicit rule for othersthe address assigned in Chapter 9.5
How muchthe amount of data the operation concernsthe width of the host access
Which bytesfor operations touching part of a data unitthe byte enables Chapter 9.6 §11 propagated
Payloadpresent for writes, absent for reads (§3)the store's data
Who askedso a response can be returned to the right originatorthe Requester's BDF
Correlationso a response can be matched to this request rather than another from the same Requesterassigned by the Requester's Transaction Layer
Attributeshandling hints such as no-snoop and relaxed-ordering (§1.5.1)driver and platform policy

The last two rows are the ones with no analogue in Module 9, and they are what make the split-transaction model work.

Why identity is not enough on its own. A Requester may have many operations outstanding at once. Every response coming back carries its Requester ID, so they all arrive at the right device — and without something further, that device could not tell which of its outstanding requests each one answers. §2's normative sentence covers exactly this: "Each packet has a unique identifier that enables response packets to be directed to the correct originator."

How the correlation is encoded, how many outstanding operations are permitted, and how the identifier is reused are all deferred. Module 11 owns the encoding; Chapter 10.2 owns what the response does with it.

6. The Journey, at the Transaction Level

A host read and a host write at the transaction level. The host CPU issues a load at an assigned address. The Root Complex Transaction Layer constructs a Memory Read Request and sends it to the Endpoint Transaction Layer, which converts it into a read at the resource offset for the device core. The core returns data, the Endpoint Transaction Layer constructs a Completion, and the Root Complex delivers the data so the load completes. The host CPU then issues a store, the Root Complex constructs a Memory Write Request carrying a payload, and the Endpoint Transaction Layer converts it into a write at the resource offset for the device core.A host read and a host write, as transactionsHost CPURoot Complex TLEndpoint TLDevice coreload at assignedaddressMemory Read Requestread at resourceoffsetdataCompletion - Chapter10.2load completesstore at assignedaddressMemory Write Request+ payloadwrite at resourceoffset
Figure 1 — the same host read and write as Chapter 9.6, now with the transactions named. The read's Request travels one way and a returning Completion travels the other; the write's Request travels one way and, per the transaction types of section 3, nothing returns to complete it. The Completion step is shown as a single arrow because Chapter 10.2 owns it.

Compare this with Chapter 9.6 §3's figure and notice what changed. The arrows now have names, and the names are transaction types from §3's table rather than descriptions of movement. That is the whole contribution of this module: the fabric stops being a thing that "routes accesses" and becomes a thing that carries specific, typed, identified operations.

And notice what did not change. "Read at resource offset" is Chapter 9.6's front end, unmodified. The Transaction Layer hands the device core a request; the core has no idea a packet was involved. That boundary is exactly §2's "convert received Request TLP into Requests for the device core."

7. Configuration Requests Use the Same Machinery

Chapter 7.7 taught configuration access as an addressing mechanism, and deliberately said nothing about what carried it.

It was carried by a transaction too. Configuration Read and Configuration Write are transaction types in their own address space (§3), constructed and processed by the same Transaction Layer as everything else.

What differs is the targeting semantics, not the machinery. A memory request names a location by address, and the fabric routes it by address against the windows of Chapter 9.5 §9. A configuration request names a Function by identity. Same layer, same construction and disassembly services, different address space and different routing.

Why this is worth one section rather than none. It closes a loop the curriculum opened in Module 7: enumeration was not happening outside the protocol or before it. Every configuration read in Chapters 7.7 through 9.5 was a Transaction Layer request, and the reason it kept working when address routing was broken (Chapter 9.6 §2) is that its address space and routing rule are different — not that it bypassed the fabric.

8. Where the Layer Boundary Falls

Module 3 introduced the three layers. Rather than repeat it, follow one request down and back up.

LayerWhat it does to this requestWhat it knows
Transactionconstructs a TLP conveying the operation, its target, its identity and its correlationwhat the operation means
Data Linkadds its own protection and sequencing, guarantees the packet crosses this Link intact, retransmits if notthat this is a packet — nothing about its contents
Physicalserialises and transmits; recovers and deserialises at the far endthat these are symbols — nothing about packets

Read the right-hand column downward. Each layer knows strictly less about meaning than the one above it, and that is the point of the layering: the Data Link Layer's reliability mechanism works identically for a memory write and a configuration read because it never looks.

Read it upward and the same fact becomes a debugging principle. A link that trains and passes traffic tells you the two lower layers are working — and tells you nothing about whether transactions are being constructed, targeted, or dispatched correctly. §15's scenarios all live above a healthy link.

9. Microarchitecture — Sources, Arbitration, Dispatch

An Endpoint's Transaction Layer has traffic in both directions and several independent producers on the outbound side.

Outbound, several sources converge:

  • a completion source, answering inbound requests the Function completed (Chapter 9.6's front end feeds it)
  • a configuration path, answering configuration requests
  • a requester engine, originating the Function's own traffic (§4's other role)

They share one path to the Data Link boundary, so something must choose between them each cycle without losing any of them. That is §11.

Inbound, one stream diverges. A received TLP is classified and dispatched to whichever internal consumer owns that class — memory, configuration, message, completion — with anything the design does not represent routed to explicit handling rather than silently dropped (§2's fifth service). That is §12.

10. Conceptual — An Internal Transaction Descriptor

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// CONCEPTUAL / COMPILE-TIME. INTERNAL TEACHING METADATA — NOT a PCIe TLP,
// NOT a TLP header, NOT the Fmt/Type encoding. Numeric values are arbitrary
// local encodings with no PCIe meaning whatsoever.
package txn_pkg;
 
  typedef enum logic [2:0] {
    TXN_MEM_RD = 3'd0,
    TXN_MEM_WR = 3'd1,
    TXN_CFG_RD = 3'd2,
    TXN_CFG_WR = 3'd3,
    TXN_MSG    = 3'd4,
    TXN_CPL    = 3'd5
    // 3'd6 and 3'd7 are intentionally unassigned, so section 12's dispatch
    // has a reachable "class this model does not represent" case.
  } txn_kind_e;
 
  typedef struct packed {
    txn_kind_e   kind;
    // Meaningful for address-routed kinds only. Section 3's two memory
    // address forms are both representable here; which form a real header
    // uses is Module 11's.
    logic [63:0] addr;
    // INTERNAL dword count. Not the encoded Length field.
    logic [10:0] len_dw;
    // LOCAL correlation token, so this model can match a response to the
    // request that produced it. NOT a PCIe Tag, and not encoded like one.
    logic [7:0]  ctx;
    // Section 3: writes carry data, reads do not.
    logic        has_payload;
    // Section 3: some operations are answered, some are not. The TERMINOLOGY
    // and the consequences belong to Chapters 10.2 through 10.4.
    logic        needs_response;
  } txn_desc_t;
 
endpackage

Classification: conceptual / compile-time.

What it teaches — two things:

  1. The categories of §5 are what an internal interface actually carries. Operation, where, how much, payload presence, identity/correlation. A real descriptor adds more; none of it is a different kind of information.
  2. has_payload and needs_response are independent. A memory read has no payload and expects an answer; a memory write has a payload and, per §3, is not answered. Treating them as one bit is a modelling error that makes the write case unrepresentable.

Deliberately simplified: no attributes, no traffic class, no address-space qualifier beyond the kind, no data stream, and a token width chosen for readability.

11. RTL — Outbound Request Arbitration

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Several internal transaction sources share one outbound
// Transaction Layer path. Descriptors only — no TLP is constructed here.
// The arbitration POLICY is an implementation choice; PCIe mandates none.
import txn_pkg::*;
 
module txn_arbiter #(
  parameter int N = 3
) (
  input  logic         clk,
  input  logic         rst_n,
 
  // Producers. Index 0 has highest priority — see the policy note below.
  input  logic [N-1:0] src_valid,
  output logic [N-1:0] src_ready,
  input  txn_desc_t    src_desc [N],
 
  // One shared path toward the Data Link boundary.
  output logic         out_valid,
  input  logic         out_ready,
  output txn_desc_t    out_desc
);
 
  localparam int SW = (N > 1) ? $clog2(N) : 1;
 
  // Grant state. Registered, which is what keeps src_ready free of any
  // dependence on src_valid.
  logic          busy_q;
  logic [SW-1:0] sel_q;
 
  // Fixed priority, lowest index wins. Iterating downward means index 0's
  // assignment lands last and therefore takes precedence.
  function automatic logic [SW-1:0] pick(input logic [N-1:0] v);
    logic [SW-1:0] p;
    p = '0;
    for (int i = N-1; i >= 0; i--) if (v[i]) p = SW'(i);
    return p;
  endfunction
 
  wire          any_valid = |src_valid;
  wire [SW-1:0] winner    = pick(src_valid);
 
  // out_valid depends on the GRANTED source's valid and on grant state —
  // never on out_ready. A source's data exists or it does not.
  assign out_valid = busy_q && src_valid[sel_q];
  assign out_desc  = src_desc[sel_q];
 
  // src_ready depends on GRANT STATE and out_ready ONLY. No source's ready
  // depends on any source's valid, so there is no combinational valid->ready
  // path anywhere in this module.
  always_comb begin
    src_ready = '0;
    if (busy_q) src_ready[sel_q] = out_ready;
  end
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      busy_q <= 1'b0;
      sel_q  <= '0;
    end else if (busy_q) begin
      // The grant is NON-PREEMPTIVE: it is held until the granted source's
      // descriptor is actually transferred, even if a higher-priority source
      // becomes valid meanwhile. Without this, a source could be selected,
      // stalled, and then silently dropped.
      if (out_valid && out_ready) busy_q <= 1'b0;
    end else if (any_valid) begin
      busy_q <= 1'b1;
      sel_q  <= winner;
    end
  end
 
endmodule

Classification: synthesizable.

Interface semantics:

DimensionBehaviour
Resetno grant; out_valid low
out_validgranted source's valid, gated by grant state — never by out_ready
src_readyasserted only for the granted source, and only when out_ready
Selection stabilityheld from grant until transfer; a higher-priority arrival does not preempt
Payload stabilityinherited from the sources, which owe valid- and payload-stability by contract
Throughputone descriptor per two cycles sustained — one arbitration cycle, one transfer cycle
Fairnessnone — fixed priority, documented below

The priority policy, stated as the implementation choice it is. Index 0 is intended for the completion source: a Function that cannot emit the responses it owes will eventually stall the Requesters waiting on them, and that back-pressure propagates outward. Index 1 is the configuration path, and index 2 the Function's own requester engine.

What it teaches — three things:

  1. A registered grant is what keeps ready free of valid. With the selection in a flip-flop, src_ready is a function of busy_q, sel_q and out_ready. A purely combinational arbiter cannot have that property, because deciding who is ready requires knowing who is valid.
  2. Non-preemptive grant is a conservation requirement, not a fairness one. If a higher-priority arrival could steal a grant mid-stall, the displaced source would have been offered ready-adjacent treatment and then dropped. P3 asserts the grant holds.
  3. One dead cycle between transfers is the cost of that simplicity, and it is a real cost — half throughput. A production arbiter overlaps the next arbitration with the current transfer, which is more logic and one more thing to get wrong.

Deliberately simplified: descriptor-only, no data stream alongside it; no ordering awareness; no fairness; no virtual-channel separation.

Production implication: a real outbound Transaction Layer arbitrates under the ordering rules, tracks flow-control credits per class before it may transmit at all (Module 16), carries a payload stream alongside the descriptor, and constructs the actual TLP at the Data Link boundary.

12. RTL — Inbound Classification and Dispatch

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Routes a received transaction descriptor to the internal
// consumer that owns its class.
// ILLUSTRATIVE POST-DECODE DISPATCH — the actual TLP header decoding that
// would produce this descriptor belongs to Module 11. Nothing here inspects
// a packet field.
import txn_pkg::*;
 
module txn_dispatch (
  input  logic       in_valid,
  output logic       in_ready,
  input  txn_desc_t  in_desc,
 
  // One consumer per represented class.
  output logic       mem_valid,
  input  logic       mem_ready,
  output logic       cfg_valid,
  input  logic       cfg_ready,
  output logic       msg_valid,
  input  logic       msg_ready,
  output logic       cpl_valid,
  input  logic       cpl_ready,
 
  // A class this model does not represent. Section 2's fifth Transaction
  // Layer service — "detect unsupported TLP and invoke appropriate
  // mechanisms" — given a real output instead of being dropped. What the
  // appropriate mechanism IS belongs to Module 13.
  output logic       unsup_valid,
  input  logic       unsup_ready,
 
  output txn_desc_t  out_desc
);
 
  wire is_mem = (in_desc.kind == TXN_MEM_RD) || (in_desc.kind == TXN_MEM_WR);
  wire is_cfg = (in_desc.kind == TXN_CFG_RD) || (in_desc.kind == TXN_CFG_WR);
  wire is_msg = (in_desc.kind == TXN_MSG);
  wire is_cpl = (in_desc.kind == TXN_CPL);
  wire is_unsup = !(is_mem || is_cfg || is_msg || is_cpl);
 
  // Exactly one consumer is offered the transaction. The five terms are
  // mutually exclusive by construction, which P6 checks rather than assumes.
  assign mem_valid   = in_valid && is_mem;
  assign cfg_valid   = in_valid && is_cfg;
  assign msg_valid   = in_valid && is_msg;
  assign cpl_valid   = in_valid && is_cpl;
  assign unsup_valid = in_valid && is_unsup;
 
  assign out_desc = in_desc;
 
  // Ready is selected by the descriptor's CLASS — a dependence on payload,
  // which is legal — and never on in_valid. A design that ORed all five
  // readies unconditionally would accept a transaction its actual consumer
  // could not take, and the transaction would be lost.
  assign in_ready = (is_mem   && mem_ready)
                  | (is_cfg   && cfg_ready)
                  | (is_msg   && msg_ready)
                  | (is_cpl   && cpl_ready)
                  | (is_unsup && unsup_ready);
 
endmodule

Classification: synthesizable.

What it teaches — three things:

  1. Classification must precede dispatch, and both must precede acceptance. The transaction is offered only to its own consumer, and it is taken only when that consumer is ready. A dispatch that accepted on any ready would drop transactions whose real consumer was stalled — a loss with no error indication anywhere.
  2. The unsupported case is a path, not an omission. It has a valid, a ready, and a consumer. §2's services list makes detecting unsupported TLPs a Transaction Layer responsibility; a dispatch with a default: that does nothing is not implementing it.
  3. One-hot dispatch is the conservation property in structural form. Exactly one consumer sees each transaction, so it cannot be duplicated across paths — a failure that is nearly invisible in simulation until a scoreboard counts.

Deliberately simplified: no buffering — a stalled consumer back-pressures the receive path directly; no payload stream; no per-class flow control; and the classification works on a descriptor rather than on a header, because header decoding is Module 11's.

Production implication: a real receive path decodes the header, checks it, buffers per class with credit accounting (Module 16), applies the ordering rules, and produces the specific response the specification requires for an Unsupported Request (Module 13).

13. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SVA over txn_arbiter and txn_dispatch. Implementation invariants for THESE
// designs — not PCIe protocol requirements, and not claims about anything in
// a TLP. Every property refers to explicit RTL state or inputs.
 
// ENVIRONMENT ASSUMPTIONS — stated as assumptions, because they constrain the
// producers rather than describing this module. Without valid-stability the
// non-preemptive grant would deadlock, and saying so honestly is better than
// asserting a property this module cannot enforce.
generate for (genvar i = 0; i < N; i++) begin : g_src_assume
  assume property (@(posedge clk) disable iff (!rst_n)
    (src_valid[i] && !src_ready[i]) |=> src_valid[i]);
  assume property (@(posedge clk) disable iff (!rst_n)
    (src_valid[i] && !src_ready[i]) |=> $stable(src_desc[i]));
end endgenerate
 
// SELECTION — P1: the outbound offer is stable while stalled. The core
// interface obligation, and the one a combinational arbiter fails.
property p_selection_stable_under_stall;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && !out_ready) |=> (out_valid && $stable(out_desc));
endproperty
a_sel_stable : assert property (p_selection_stable_under_stall);
 
// SELECTION — P2: at most one producer is consumed per cycle. The arbiter's
// whole purpose, made checkable.
property p_at_most_one_consumed;
  @(posedge clk) disable iff (!rst_n)
  $onehot0(src_ready & src_valid);
endproperty
a_one_consumed : assert property (p_at_most_one_consumed);
 
// OWNERSHIP — P3: a grant is held until its transfer completes. Catches a
// preemptive edit, which would silently drop the displaced source.
property p_grant_non_preemptive;
  @(posedge clk) disable iff (!rst_n)
  (busy_q && !(out_valid && out_ready)) |=> (busy_q && $stable(sel_q));
endproperty
a_grant_held : assert property (p_grant_non_preemptive);
 
// OWNERSHIP — P4: no producer is readied without holding the grant. Catches a
// ready that leaked a dependence on src_valid.
property p_ready_requires_grant;
  @(posedge clk) disable iff (!rst_n)
  (|src_ready) |-> (busy_q && $onehot(src_ready) && src_ready[sel_q]);
endproperty
a_ready_granted : assert property (p_ready_requires_grant);
 
// CONSERVATION — P5: the outbound offer is never fabricated. Every out_valid
// corresponds to a real, currently-valid producer.
property p_no_phantom_output;
  @(posedge clk) disable iff (!rst_n)
  out_valid |-> (busy_q && src_valid[sel_q]);
endproperty
a_no_phantom : assert property (p_no_phantom_output);
 
// DISPATCH — P6: exactly one consumer is offered each received transaction.
property p_dispatch_onehot;
  @(posedge clk) disable iff (!rst_n)
  in_valid |-> $onehot({mem_valid, cfg_valid, msg_valid, cpl_valid,
                        unsup_valid});
endproperty
a_dispatch_onehot : assert property (p_dispatch_onehot);
 
// DISPATCH — P7: an unrepresented class never reaches a normal consumer. The
// isolation that makes the unsupported path meaningful.
property p_unsupported_isolated;
  @(posedge clk) disable iff (!rst_n)
  (in_valid && is_unsup) |-> (!mem_valid && !cfg_valid && !msg_valid
                              && !cpl_valid);
endproperty
a_unsup_isolated : assert property (p_unsupported_isolated);
 
// DISPATCH — P8: a response only reaches the completion path. Catches a
// classifier that lumped completions in with requests, which would deliver a
// returning transaction to a request handler that cannot interpret it.
property p_completion_to_completion_path;
  @(posedge clk) disable iff (!rst_n)
  (in_valid && (in_desc.kind == TXN_CPL)) |-> (cpl_valid && !mem_valid
                                               && !cfg_valid && !msg_valid);
endproperty
a_cpl_routed : assert property (p_completion_to_completion_path);
 
// DISPATCH — P9: acceptance requires the SELECTED consumer's readiness, not
// anyone's. Catches an ORed ready, which loses transactions silently.
property p_accept_requires_selected_consumer;
  @(posedge clk) disable iff (!rst_n)
  (in_valid && in_ready) |-> ((is_mem && mem_ready) || (is_cfg && cfg_ready)
                           || (is_msg && msg_ready) || (is_cpl && cpl_ready)
                           || (is_unsup && unsup_ready));
endproperty
a_selected_ready : assert property (p_accept_requires_selected_consumer);
 
// MODEL WELL-FORMEDNESS — P10: the descriptor's payload flag matches its kind
// under THIS model's rules, which reflect section 3's transaction types.
// A LOCAL modelling invariant, assumed of producers rather than asserted of
// the fabric: it constrains what this teaching interface may carry.
property p_payload_matches_kind;
  @(posedge clk) disable iff (!rst_n)
  out_valid |-> ((out_desc.kind == TXN_MEM_RD) ? !out_desc.has_payload :
                 (out_desc.kind == TXN_MEM_WR) ?  out_desc.has_payload : 1'b1);
endproperty
a_payload_wellformed : assert property (p_payload_matches_kind);
 
// SAFETY — P11: no interface output is ever unknown.
property p_outputs_never_unknown;
  @(posedge clk) disable iff (!rst_n)
  !$isunknown({out_valid, src_ready, in_ready, mem_valid, cfg_valid,
               msg_valid, cpl_valid, unsup_valid});
endproperty
a_no_x : assert property (p_outputs_never_unknown);

P3 and P4 are the ownership pair, and P4 is the one that catches the tempting rewrite. Someone optimising away the dead cycle will reach for a combinational winner feeding src_ready directly. That change makes a source's ready depend on other sources' valids — and P4, which requires src_ready to be asserted only for the granted source, fires on the first cycle where the combinational winner differs from sel_q.

P9 is a subtle one worth stating separately from P6. One-hot dispatch (P6) says the transaction is offered to one consumer. P9 says it is accepted only when that consumer is ready. A design can satisfy P6 and still OR all five readies together — and then a memory transaction arriving while the memory consumer is stalled but the message consumer is idle is accepted and lost. There is no error indication for that anywhere, which is what makes it worth its own property.

P10 is explicitly a modelling invariant, not a protocol claim. It says this interface's descriptors are well-formed with respect to §3's transaction types — a read carries no payload, a write does. It asserts nothing about PCIe encoding, and it is written against the arbiter's output because that is where a malformed producer would surface.

The assumptions are stated as assumptions on purpose. The non-preemptive grant depends on producers holding valid until their handshake. This module cannot enforce that, so claiming it as a property would be asserting a contract the shown interface does not define — the move Chapter 9.1 §10 rejected and every chapter since has avoided. Writing it as assume puts the obligation on the producer, where it belongs, and keeps the verification honest about what this module actually guarantees.

14. Verification

Monitors observe: every producer handshake with its descriptor; the grant state; the outbound handshake; the received descriptor; and all five dispatch handshakes.

The scoreboard tracks correlation tokens independently. It maintains its own set of outstanding tokens, matching each returning completion descriptor against the request that produced it, and flags a token that is answered twice, never answered, or answered by a transaction of the wrong class. The tokens are verification and internal state — they are not claimed to be, or encoded like, any PCIe field.

And it must count. Conservation is the property class this chapter introduces, and counting is the only way to check it: transactions accepted from producers must equal transactions emitted, exactly, with none duplicated and none lost.

Arbitration

  • One producer active. Verify it is granted, transfers, and the grant releases.
  • All producers valid simultaneously. Verify exactly one is consumed per cycle (P2) and that the priority order is the documented one.
  • A higher-priority producer becomes valid while a lower-priority one holds the grant. Verify the grant is not stolen (P3) and the lower-priority descriptor still transfers. This is the conservation test that a preemptive arbiter fails.
  • out_ready held low across a grant. Verify out_valid stays high, out_desc is stable (P1), and no producer is readied.
  • out_ready toggling every other cycle. Verify no descriptor is duplicated or lost.
  • Sustained high-priority traffic with a low-priority producer waiting. Verify the starvation and record how long it lasts — this test is expected to expose the documented weakness, and a testbench that treats it as a pass is not measuring anything. It is also §15's second scenario in miniature.
  • Producer deasserting valid before its handshake. A deliberate assumption violation. Verify the assumption fires, and keep it outside the functional regression's pass/fail accounting.
  • Reset while a grant is held. Verify the grant clears and no descriptor is emitted afterwards.

Dispatch

  • One descriptor of each represented class. Memory read, memory write, configuration read, configuration write, message, completion. Verify each reaches exactly its own consumer (P6) and no other.
  • An unrepresented class. Drive one of the unassigned enum encodings. Verify unsup_valid asserts, all four normal consumers stay low (P7), and nothing is silently dropped.
  • A completion. Verify it reaches only the completion path (P8). Misrouting a returning transaction into a request handler is the failure this test exists for, and it is not caught by a one-hot check alone.
  • The selected consumer stalled while another is ready. Verify in_ready stays low (P9) — the ORed-ready bug, which loses transactions with no indication.
  • Every consumer stalled. Verify back-pressure reaches the receive path and nothing is accepted.
  • Back-to-back descriptors of alternating classes. Verify each is classified independently and no state carries between them.

Coverage should include: every descriptor kind including both unassigned encodings; every producer index granted at least once; all producers simultaneously valid; out_ready continuously high, continuously low, and randomly toggled; each dispatch consumer stalled while a transaction of its class arrives; and reset from a granted and from an idle state.

15. Debugging

Symptom: a host MMIO write reaches the Endpoint's PCIe block, but the BAR resource never sees it

What "reaches the PCIe block" already establishes — and it is a great deal. The address was right, the routing windows forwarded it, the link is healthy, the Data Link Layer accepted the packet. Chapter 9.6 §14's rungs 1 through 4 are all confirmed.

So the fault is between the packet arriving and the resource front end being offered a request — which is the receive-side Transaction Layer, and the list is short.

  1. Was the transaction classified correctly? A memory write dispatched to the configuration path, the message path, or the unsupported path never reaches the resource. Check the dispatch outputs against the descriptor's class (P6, P8).
  2. Did the transaction land on the unsupported path? That is a result, not a cause — it means the class was not one the design represents, and the interesting question becomes why the header decode produced that class.
  3. Is the selected consumer's ready stuck low? The dispatch back-pressures rather than dropping (§12), so a memory consumer that never asserts ready stalls the receive path. The signature is distinctive: traffic stops entirely, rather than this one transaction disappearing.
  4. Was the address or length extracted correctly into the descriptor? A memory write correctly classified but carrying a wrong address is delivered to the resource front end, where Chapter 9.6's miss output will show it — which is why that output is worth having.
  5. Only then, the resource front end itself, and Chapter 9.6 §14's rungs 5 through 8.

The observation that splits it fastest: watch the five dispatch valids. Exactly one asserts per received transaction. Which one asserts tells you immediately whether this is a classification problem or a downstream problem, and those need entirely different instruments.

Symptom: a read request reaches the Endpoint, the internal register returns data, and the host times out

The forward path is proven end to end. The request arrived, was classified, was dispatched, reached the resource, and the resource produced data. Everything in Module 9 and everything on the receive side of this chapter is working.

So the fault is on the return path, and this chapter can name its structure even though Chapter 10.2 owns the details.

  1. Did the completion source produce a transaction at all? If the response generator never received the data, or never constructed a descriptor, nothing will be sent. This is the first thing to look at and the easiest to observe.
  2. Was the correlation token preserved? The response must identify which request it answers (§5). A response constructed with the wrong token, or with a token from a different outstanding request, is either discarded at the Requester or matched to the wrong operation — and the second is worse.
  3. Is the completion source being granted? §11's arbiter is fixed-priority and can starve. A completion source at a low priority behind sustained requester traffic will produce responses that never leave the Function, and the symptom is exactly this: perfect forward path, data available, host timing out. This is the failure the arbiter's own documentation predicts, and it is why the priority note in §11 puts completions first.
  4. Is the outbound path blocked for another reason? A production Transaction Layer cannot transmit without flow-control credit (Module 16); an exhausted credit pool produces the same symptom from a different cause.

Why this chapter stops there. The response's structure, status, and rules are Chapter 10.2 and Module 13. What this chapter contributes is the narrowing: a working forward path plus available data means the fault is in response generation, correlation, or outbound scheduling — three places, all inside the Transaction Layer.

Symptom: transactions arrive but are handled in an unexpected relative order

Recognise the boundary rather than debugging it here.

§11's arbiter treats descriptors as opaque items and applies a fixed priority. PCIe imposes ordering rules on TLPs — §2.1 names them as a Transaction Layer responsibility — and those rules constrain which transactions may pass which.

So an ordering surprise has two possible causes, and they need different owners: the arbitration policy reordered transactions the rules do not permit to be reordered, or software expected an ordering the rules never guaranteed. Chapter 13.4 owns both questions, and attempting either from inside this chapter's model would produce a confident wrong answer.

What is worth recording before handing off: which classes were involved, in what order they were produced internally, and in what order they were transferred outbound. That is exactly what §11's monitors capture, and it is the evidence the ordering investigation will need.

16. Common Misconceptions

  • "A TLP is what appears on the wire." A TLP is generated in the Transaction Layer; the Data Link Layer adds its own protection and sequencing, and the Physical Layer encodes and serialises the result. What is on the wire contains a TLP; it is not one.
  • "The Transaction Layer handles equalization and signalling." Those are Physical Layer concerns (Module 17). The Transaction Layer never sees a symbol.
  • "BAR-backed access bypasses the Transaction Layer." Every operational access crosses the Link as a transaction. Chapter 9.6's host access is a Transaction Layer request, which is why this module follows that one.
  • "Every transaction carries a payload." A memory read request carries none — it asks for data (§3). Modelling payload presence as implicit in the transaction makes reads unrepresentable, which is why §10's descriptor has a separate flag.
  • "Every transaction requires the same kind of response." §3's table shows otherwise, and the terminology and consequences belong to Chapters 10.2–10.4. The asymmetry is why writes and reads fail differently.
  • "TLP routing is the same thing as BAR decoding." Routing moves a transaction through the fabric to a Function (Module 11). BAR decoding happens inside a Function, deciding whether an address belongs to one of its windows (Chapter 9.6). Different scopes, different mechanisms.
  • "An internal transaction descriptor is the TLP format." §10's structure is teaching metadata with arbitrary local encodings. The real header is Module 11's, and nothing in this chapter should be read as its layout.
  • "The Data Link Layer decides what a transaction means." It guarantees the packet crosses the Link intact and knows nothing about its contents (§8). Meaning lives one layer up, and only there.
  • "'Transaction Layer' means a software layer." It is hardware — the outermost layer of the PCIe architecture, "that operates at the level of transactions (for example, read, write)." Software issues operations; the Transaction Layer turns them into packets.
  • "One TLP equals one software operation." A single logical transfer is a Sequence: one Request and zero or more Completions. How a larger software operation maps onto Sequences depends on sizes and limits that later modules own.

17. Understanding Check

18. What's Next

Module 9 ended with a host access reaching an Endpoint's resource. This chapter named what carried it: a Request — a packet that initiates a Sequence, targets one of four address spaces, identifies its Requester, and, for the operations that need one, carries enough to let an answer find its way home.

It also left the obvious question wide open: what is actually inside one?

Chapter 10.2 — Completions takes the other half of the taxonomy: the transactions that return data and status, how they are matched to the requests that produced them, and what a Requester does when one never arrives. Chapters 10.3 and 10.4 give the two lifecycles of §3 their proper names and consequences, and Chapter 10.5 traces one full exchange across the fabric.

Then Module 11 opens the packet itself — structure, headers, payloads, routing, attributes, and the full taxonomy of types. Everything this chapter called "a category of information" becomes a field with a position and an encoding.

The idea to carry forward: a request is an operation made portable — enough of it to cross a fabric, be understood by a device that has never heard of it, and be answered.