Skip to content

PCIe · Module 11

Attributes — The Policy a Packet Carries About Itself

Beyond what operation it is, a TLP carries transaction-management metadata: a Traffic Class, and ordering and snoop attributes. Why TC is not priority, why Relaxed Ordering is a permission rather than an exemption, why No Snoop is not a cache instruction, and why attribute metadata must be owned by the packet rather than recomputed.

Chapter 11.3 said what kind of packet this is. Chapter 11.4 said how much data it carries. Chapter 11.5 said where it goes.

None of those said anything about how the transaction may be treated on the way. And there are bits in the header that do.

What transaction-level policy accompanies a TLP beyond its basic packet type, and how do Traffic Class, the ordering attributes, and No Snoop affect how hardware may handle that transaction?

1. Terminology First — TC Is Not an Attribute

The chapter title is the curriculum's grouping, not a claim about the packet. In the header, Traffic Class and the Attr bits are different fields carrying different kinds of information, and treating them as one thing is the first mistake to avoid.

Traffic ClassAttr
Width3 bits3 bits
Shapeone 3-bit valuethree independent flag bits
What it conveysa service class for the transactionordering and snoop policy for the transaction
Consumed bythe VC mechanism and fabric schedulingordering logic and the host coherency path

The distinction is not pedantry. TC selects a service class; the Attr bits grant permissions. They are consumed by different mechanisms, they fail differently, and — as §8's RTL shows — a design that treats them as one packed field tends to derive one of them from the wrong place. Getting two of three attribute bits right is the worst possible failure profile, because the field looks like it works.

So the precise statement is: a TLP carries a Traffic Class field and an Attr field, plus some other header context. This chapter covers both, and calls them by their own names.

2. The Verified Field Map

3. Traffic Class — a Service Class, Not a Priority Number

TC is three bits, so a transaction names one of eight Traffic Classes. What it does not do is announce a priority.

What TC does obligate. A transaction's Traffic Class must be preserved and interpreted consistently as it travels — a packet that arrives as TC3 and departs as TC0 has been silently reclassified, and every downstream scheduling decision about it is then made under the wrong class. §7's ownership rule and §14's second debugging scenario are exactly this failure.

Where the rest of this lives. TC's relationship to Virtual Channels — how many VCs exist, how TCs map onto them, how VCs are arbitrated, and how credits are accounted per VC — is the flow-control curriculum's. This chapter needs only that TC is the field that connects a transaction to that mechanism, and that the mechanism, not the number, decides the treatment.

4. Relaxed Ordering — a Permission, Not an Exemption

RO does not mean "ordering does not matter." It means the transaction permits certain specific ordering constraints to be relaxed, and the constraints in question are named.

Default ordering (RO clear)Relaxed Ordering (RO set)
a posted write vs. previously posted writes/messagesmust not passmay pass
a read completion vs. previously posted writes, same directionmust not passmay pass

The full ordering matrix is Chapter 13.4's. This chapter owns what the attribute communicates; that chapter owns the complete relationship table and the producer/consumer analysis in depth.

5. No Snoop — a Coherency Assertion, Not a Cache Instruction

NS is the most commonly misread bit in the header, and the misreading is that it tells a cache to do something.

What it actually says, in the verified formulation: with NS set, the Requester is indicating that no host cache coherency issues exist with respect to this TLP, and system hardware is not required to cause a processor cache snoop.

Read that as three separate claims, because it is three.

  • It is an indication by the Requester, not a command to a cache. The Requester is asserting a property of its own access.
  • It says coherency issues do not exist — meaning the data this transaction touches is not concurrently held in a way that snooping would need to resolve.
  • It relieves system hardware of a requirement. It does not compel anything. A system that snoops anyway has not violated the protocol.

The host-side coherency architecture — what a snoop actually does, how CPU caches participate, what the platform guarantees — is deliberately outside this chapter. It varies by platform and is not PCIe's to define. What PCIe defines is the indication, and that is what a design implements.

6. ID-Based Ordering

IDO is Attr[2], and it addresses a different problem from RO.

Default ordering restricts a transaction against previously posted writesregardless of who issued them. In a system with many independent Requesters sharing a path, that means one Requester's traffic is ordered behind another's, even though the two have no relationship whatsoever.

IDO permits passing a previously posted write when the Requester identities differ. The ordering relationship that mattered — between one Requester's own transactions — is preserved; the accidental coupling between unrelated Requesters is relaxed.

7. Attributes Are Owned Packet State

Here is the chapter's architectural core, and it is an RTL statement rather than a protocol one.

Once a design accepts a transaction, its Traffic Class and attributes are part of that transaction's owned state. They travel with it. They are not recomputed.

Why this needs saying. Most blocks in a packet path have no business interpreting attributes at all. A queue does not care about Relaxed Ordering. An arbiter does not care about No Snoop. A packet buffer does not care about Traffic Class. Their entire responsibility is to not lose them.

BlockAttribute responsibility
queue / FIFOcapture, retain, forward — unmodified
arbiterretain the association between metadata and the transaction it belongs to
packet bufferhold metadata for exactly as long as the packet
routing stageforward — routing is Chapter 11.5's field, not these
packet builderemit the metadata that arrived with the transaction

So attribute preservation is a real, checkable RTL responsibility — and it is the responsibility §9's FIFO exists to demonstrate.

8. RTL — Normalized Attribute Metadata

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// CONCEPTUAL. NORMALIZED IMPLEMENTATION METADATA — this is NOT the raw PCIe
// header encoding. In the conventional header the three Attr bits are NOT
// necessarily adjacent to each other or to TC. Layout is Base Spec section
// 2.2's; this struct is what internal logic consumes AFTER the header
// decoder has assembled it, so nothing downstream re-slices the header.
// This struct is what internal logic consumes AFTER the header decoder has
// reassembled them, so that no downstream block ever re-slices the header.
typedef struct packed {
  logic [2:0] tc;                // Traffic Class, TC0..TC7
  logic       relaxed_ordering;  // Attr[1]
  logic       no_snoop;          // Attr[0]
  logic       id_ordering;       // Attr[2]
} txn_attr_t;

Why the normalization matters more here than anywhere else so far. A field made of independent flags is exactly the kind of thing that gets re-derived inconsistently. If the routing stage, the queue and the packet builder each slice the header for Attr, they are each making an assumption about its layout — and the moment one of those assumptions is wrong, or one of them is updated and the others are not, two blocks disagree about the same packet's ordering permissions.

This is Chapter 11.3 §12's principle applied to the field where it costs most: decode once, at the boundary, and carry semantics downstream.

9. RTL — Attribute-Preserving Queue

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. A transaction queue whose entire contract is that control
// metadata stays attached to the transaction it arrived with.
// TC width (3 bits) and the three Attr bits: NORMATIVE (section 2).
// The queue interface, the shadow sequence field and the route metadata
// abstraction: ILLUSTRATIVE.
module txn_attr_queue #(
  parameter int DEPTH  = 4,
  parameter int ROUTE_W = 4,
  // Verification/debug shadow only. NOT a PCIe field and NOT a Tag.
  parameter int SEQ_W  = 8
) (
  input  logic clk,
  input  logic rst_n,
 
  // ---- Enqueue ---------------------------------------------------------
  input  logic                in_valid,
  output logic                in_ready,
  input  txn_attr_t           in_attr,
  input  logic [ROUTE_W-1:0]  in_route,
  input  logic [SEQ_W-1:0]    in_seq,
 
  // ---- Dequeue ---------------------------------------------------------
  output logic                out_valid,
  input  logic                out_ready,
  output txn_attr_t           out_attr,
  output logic [ROUTE_W-1:0]  out_route,
  output logic [SEQ_W-1:0]    out_seq
);
 
  generate
    if (DEPTH < 1)
      $error("DEPTH must be at least 1");
  endgenerate
 
  // Width-safe for DEPTH == 1, where $clog2(1) is 0 and a zero-width index
  // would be illegal. This is the parameter corner that breaks naive queues.
  localparam int IDX_W = (DEPTH <= 1) ? 1 : $clog2(DEPTH);
  localparam int CNT_W = $clog2(DEPTH + 1);
 
  typedef struct packed {
    txn_attr_t          attr;
    logic [ROUTE_W-1:0] route;
    logic [SEQ_W-1:0]   seq;
  } entry_t;
 
  entry_t           mem_q [DEPTH];
  logic [IDX_W-1:0] wr_q, rd_q;
  logic [CNT_W-1:0] cnt_q;
 
  wire full  = (cnt_q == CNT_W'(DEPTH));
  wire empty = (cnt_q == '0);
 
  assign in_ready  = !full;
  assign out_valid = !empty;
 
  wire push = in_valid  && in_ready;
  wire pop  = out_valid && out_ready;
 
  // Output is driven from STORED state, never from live inputs. That is the
  // whole point of the module (section 7) — a bypass path from in_attr to
  // out_attr would reintroduce the recomputation bug it exists to prevent.
  assign out_attr  = mem_q[rd_q].attr;
  assign out_route = mem_q[rd_q].route;
  assign out_seq   = mem_q[rd_q].seq;
 
  // Non-power-of-two DEPTH is supported: the pointers wrap explicitly rather
  // than relying on natural counter rollover, which only works at 2^N.
  function automatic logic [IDX_W-1:0] next_idx (input logic [IDX_W-1:0] i);
    next_idx = (i == IDX_W'(DEPTH - 1)) ? '0 : (i + IDX_W'(1));
  endfunction
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      wr_q <= '0; rd_q <= '0; cnt_q <= '0;
    end else begin
      if (push) begin
        mem_q[wr_q].attr  <= in_attr;
        mem_q[wr_q].route <= in_route;
        mem_q[wr_q].seq   <= in_seq;
        wr_q <= next_idx(wr_q);
      end
      if (pop)
        rd_q <= next_idx(rd_q);
 
      // Simultaneous push and pop leaves the count unchanged. Written as an
      // explicit four-way decision rather than as cnt + push - pop, so the
      // same-cycle case cannot underflow through an intermediate value.
      case ({push, pop})
        2'b10:   cnt_q <= cnt_q + CNT_W'(1);
        2'b01:   cnt_q <= cnt_q - CNT_W'(1);
        default: cnt_q <= cnt_q;     // 2'b11 and 2'b00
      endcase
    end
  end
 
endmodule

Classification: synthesizable.

Architecture. A circular buffer whose payload is control metadata rather than data. The only interesting property is negative: no path from an input to an output except through storage.

State. mem_q, write and read pointers, an occupancy count. The count is separate from the pointers so that full and empty are distinguishable at DEPTH entries without a wrap bit — and so that DEPTH == 1 works.

Cycle behaviour. Push on in_valid && in_ready; pop on out_valid && out_ready. Both in the same cycle is legal and leaves occupancy unchanged, which the explicit case handles without transiting an illegal intermediate value.

Contract. The caller relies on exact preservation: what goes in at position k comes out at position k, bit for bit. The module relies on the caller holding in_attr, in_route and in_seq stable while in_valid is asserted and in_ready is low — the valid/ready discipline, and violating it corrupts the entry that is eventually captured.

Failure — four, and each is a distinct class. A bypass path from in_attr to out_attr when empty reintroduces §7's recomputation bug on the fast path. Incrementing cnt_q on in_valid rather than on push overflows under backpressure. Natural pointer rollover instead of next_idx breaks silently at any non-power-of-two DEPTH — the pointer wraps at 2^IDX_W, not at DEPTH, so entries alias. And $clog2(DEPTH) without the DEPTH <= 1 guard yields a zero-width index at the module's own stated minimum.

Deliberately simplified: metadata only, no packet data; one queue rather than per-TC queues; no reordering; no attribute interpretation of any kind.

DV. §10's P1–P5.

10. RTL — Attribute Policy Checker

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Check a transaction's requested attributes against this
// component's local capability/policy configuration.
// The TC width and the three attributes: NORMATIVE. What a given component
// supports, and this module's DETECT-AND-REPORT stance: ILLUSTRATIVE
// IMPLEMENTATION POLICY — PCIe does not mandate a central policy engine.
module txn_attr_policy (
  input  logic clk,
  input  logic rst_n,
 
  // ---- Local capability / policy, from configuration -------------------
  // Which Traffic Classes this component is configured to handle.
  input  logic [7:0]  tc_supported,        // one bit per TC0..TC7
  // Whether this component may pass on each requested relaxation.
  input  logic        ro_permitted,
  input  logic        ns_permitted,
  input  logic        ido_permitted,
 
  // ---- Transaction in --------------------------------------------------
  input  logic        in_valid,
  output logic        in_ready,
  input  txn_attr_t   in_attr,
 
  // ---- Result out ------------------------------------------------------
  output logic        out_valid,
  input  logic        out_ready,
  output txn_attr_t   out_attr,          // forwarded UNMODIFIED
  output logic        policy_ok,
  // Which specific expectation was not met. Reported per-reason so the
  // failure is diagnosable rather than a single opaque bit.
  output logic        unsupported_tc,
  output logic        unsupported_ro,
  output logic        unsupported_ns,
  output logic        unsupported_ido
);
 
  // The checks. Note every one is a COMPARISON, never an assignment to
  // in_attr — this module does not rewrite a packet's policy.
  wire tc_bad  = !tc_supported[in_attr.tc];
  wire ro_bad  = in_attr.relaxed_ordering && !ro_permitted;
  wire ns_bad  = in_attr.no_snoop         && !ns_permitted;
  wire ido_bad = in_attr.id_ordering      && !ido_permitted;
 
  // ---- Single-entry decoupled holding stage ----------------------------
  // The stage OWNS one transaction at a time. It can accept a new one when
  // it is empty, or when the one it holds is being consumed this cycle.
  logic       hold_valid_q, ok_q;
  logic       tcb_q, rob_q, nsb_q, idob_q;
  txn_attr_t  attr_q;
 
  wire consume = hold_valid_q && out_ready;
 
  // in_ready is INDEPENDENT of in_valid — no combinational loop, and the
  // offer is never withdrawn because the offerer changed its mind.
  assign in_ready = !hold_valid_q || out_ready;
 
  wire accept = in_valid && in_ready;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      hold_valid_q <= 1'b0; ok_q <= 1'b0; attr_q <= '0;
      tcb_q <= 1'b0; rob_q <= 1'b0; nsb_q <= 1'b0; idob_q <= 1'b0;
    end else begin
      // Priority matters and it is the whole same-cycle contract:
      //   accept              -> capture the new transaction (replacement)
      //   consume, no accept  -> stage goes empty
      //   neither             -> HOLD, so a stalled output cannot be
      //                          disturbed by anything, including config
      if (accept) begin
        // Attributes and verdict are REGISTERED TOGETHER at acceptance.
        // Everything downstream reads the registers, so a configuration
        // change after this cycle cannot alter a transaction this stage
        // already owns (section 7).
        hold_valid_q <= 1'b1;
        attr_q       <= in_attr;
        ok_q         <= !(tc_bad || ro_bad || ns_bad || ido_bad);
        tcb_q        <= tc_bad;
        rob_q        <= ro_bad;
        nsb_q        <= ns_bad;
        idob_q       <= ido_bad;
      end else if (consume) begin
        hold_valid_q <= 1'b0;
      end
    end
  end
 
  assign out_valid       = hold_valid_q;
  // FORWARDED UNMODIFIED. This module detects and reports; it does not
  // silently clear a bit it dislikes. Rewriting a Requester's stated policy
  // would change the transaction's semantics without anyone observing it.
  assign out_attr        = attr_q;
  assign policy_ok       = ok_q;
  assign unsupported_tc  = hold_valid_q && tcb_q;
  assign unsupported_ro  = hold_valid_q && rob_q;
  assign unsupported_ns  = hold_valid_q && nsb_q;
  assign unsupported_ido = hold_valid_q && idob_q;
 
endmodule

Classification: synthesizable.

Architecture. A single-entry decoupled stage. Four comparisons against configuration are evaluated combinationally on the offered transaction and registered together with the attributes that produced them at the moment of acceptance. The attributes pass through unmodified — the module's entire output contribution is the verdict, not a rewritten packet.

State. One holding register: validity, the attributes, and the four per-reason verdict bits — all written in one event, never separately.

Cycle behaviour, and this is the part worth reading twice.

hold_valid_qout_readyin_readyEffect of in_valid
0x1accept — stage becomes occupied
100refused — the held transaction is untouchable
111replace — held transaction leaves, new one captured, same cycle

in_ready does not depend on in_valid, so there is no combinational loop and no offer that is withdrawn because the offerer changed its mind. The accept arm has priority over the consume arm in the register block, which is what makes same-cycle replacement work: the stage never transits through an empty state it would have to recover from.

Contract. Downstream relies on out_valid remaining asserted until out_ready, and on out_attr, policy_ok and the four reasons being stable for the whole of that time. It relies on out_attr being exactly what arrived, and on policy_ok being the verdict computed against the configuration in force at acceptance — not the configuration as it reads now.

Failure — and the first is a design-stance error rather than a coding error. Clearing relaxed_ordering when ro_permitted is low looks helpful and is not: the Requester stated a policy, the design silently changed the transaction's semantics, and nothing in the system observes it. Detect and report; do not rewrite. Beyond that, three handshake failures, each with its own property in §11: computing policy_ok combinationally from live configuration rather than from the registers reintroduces §7's bug inside the checker (P8); writing hold_valid_q <= in_valid unconditionally drops a held transaction the moment the input goes idle (P13); and giving consume priority over accept loses the new transaction on a same-cycle replacement (P14).

Deliberately simplified: one holding entry rather than a queue — deeper buffering is §9's job and this stage does not duplicate it; no interaction with the ordering rules (Chapter 13.4); no VC mapping; no per-direction policy; no required PCIe response to an unsupported request.

Production implication: what a real component does on a policy mismatch — forward anyway, apply the specification's defined error handling, or refuse — is a design and specification question this model does not answer. It answers the prior question: was the mismatch detected, and is it attributable.

11. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SVA over txn_attr_queue and txn_attr_policy. These are LOCAL ownership and
// preservation contracts plus the normative field widths of section 2 — not
// claims about PCIe ordering rules, which Chapter 13.4 owns.
 
// OWNERSHIP — P1: THE CHAPTER'S CENTRAL PROPERTY. Metadata at the head of the
// queue is stable while the consumer stalls. A design whose output is driven
// from live configuration fails here the first time it is backpressured.
property p_head_stable_under_stall;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && !out_ready) |=> (out_valid
                              && $stable({out_attr, out_route, out_seq}));
endproperty
a_head_stable : assert property (p_head_stable_under_stall);
 
// PRESERVATION — P2: what comes out equals what went in for the SAME
// transaction. The shadow sequence field is what makes "the same" checkable.
// (exp_attr/exp_route are a testbench model keyed by seq — section 12.)
property p_exact_preservation;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && out_ready)
    |-> ((out_attr  == exp_attr[out_seq])
      && (out_route == exp_route[out_seq]));
endproperty
a_preserved : assert property (p_exact_preservation);
 
// ISOLATION — P3: no cross-contamination. Adjacent transactions with opposite
// attributes must not blur into each other. Stated as an inequality on the
// shadow ID so a queue that returned entry k-1's metadata with entry k's ID
// cannot satisfy it.
property p_no_metadata_leak;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && out_ready && (exp_attr[out_seq] != out_attr)) |-> 1'b0;
endproperty
 
// OWNERSHIP — P4: head metadata changes ONLY as a result of a pop. Catches a
// design that lets a new push disturb an already-presented head.
property p_head_changes_only_on_pop;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && !$stable({out_attr, out_route, out_seq})) |-> $past(pop);
endproperty
a_head_change_cause : assert property (p_head_changes_only_on_pop);
 
// CONSERVATION — P5: simultaneous push and pop leaves occupancy unchanged and
// both transactions intact. The same-cycle event this chapter calls out.
property p_simul_push_pop_conserves;
  @(posedge clk) disable iff (!rst_n)
  (push && pop) |=> (cnt_q == $past(cnt_q));
endproperty
a_simul : assert property (p_simul_push_pop_conserves);
 
// SAFETY — P6: occupancy never exceeds DEPTH and never underflows.
property p_occupancy_bounded;
  @(posedge clk) disable iff (!rst_n)
  (cnt_q <= CNT_W'(DEPTH)) && !(pop && (cnt_q == '0));
endproperty
a_occupancy : assert property (p_occupancy_bounded);
 
// POLICY — P7: the checker NEVER modifies the attributes it owns. Anchored
// to the ACCEPTANCE event rather than to "the previous cycle", because with a
// real handshake a transaction may be held for many cycles.
property p_policy_forwards_unmodified;
  @(posedge clk) disable iff (!rst_n)
  accept |=> (out_valid && (out_attr == $past(in_attr)));
endproperty
a_unmodified : assert property (p_policy_forwards_unmodified);
 
// POLICY — P8: the verdict was computed against the configuration in force at
// ACCEPTANCE, not against whatever it is now. THE ASSERTION FOR SECTION 7'S
// BUG — a combinational path from live config to policy_ok fails it as soon
// as the configuration changes while the stage holds the transaction.
property p_verdict_from_acceptance_time;
  @(posedge clk) disable iff (!rst_n)
  accept |=> (policy_ok == $past(!( !tc_supported[in_attr.tc]
                                 || (in_attr.relaxed_ordering && !ro_permitted)
                                 || (in_attr.no_snoop         && !ns_permitted)
                                 || (in_attr.id_ordering      && !ido_permitted))));
endproperty
a_verdict_time : assert property (p_verdict_from_acceptance_time);
 
// HANDSHAKE — P8a: THE OWNERSHIP PROPERTY. Everything the stage holds is
// stable for the entire time the consumer stalls. Configuration may change
// underneath it as often as it likes; the held transaction does not move.
property p_owned_output_stable;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && !out_ready)
    |=> (out_valid && $stable({out_attr, policy_ok, unsupported_tc,
                               unsupported_ro, unsupported_ns, unsupported_ido}));
endproperty
a_owned_stable : assert property (p_owned_output_stable);
 
// HANDSHAKE — P8b: an offer, once made, is never withdrawn without a
// handshake. out_valid stays asserted until out_ready.
property p_offer_not_withdrawn;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && !out_ready) |=> out_valid;
endproperty
a_no_withdraw : assert property (p_offer_not_withdrawn);
 
// HANDSHAKE — P8c: input is accepted only when the stage can actually own it.
// A stage that accepted while full would overwrite a stalled transaction.
property p_accept_only_when_ownable;
  @(posedge clk) disable iff (!rst_n)
  accept |-> (!hold_valid_q || out_ready);
endproperty
a_accept_ownable : assert property (p_accept_only_when_ownable);
 
// POLICY — P9: a failing transaction is never reported as acceptable, and the
// reason is always attributable to at least one specific check.
property p_reason_attributable;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && !policy_ok) |-> (unsupported_tc || unsupported_ro
                              || unsupported_ns || unsupported_ido);
endproperty
a_attributable : assert property (p_reason_attributable);
 
// POLICY — P10: and the converse. Any raised reason implies the verdict is
// negative. Together with P9 this pins the verdict to the reasons exactly.
property p_reason_implies_failure;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && (unsupported_tc || unsupported_ro
              || unsupported_ns || unsupported_ido)) |-> !policy_ok;
endproperty
a_reason_consistent : assert property (p_reason_implies_failure);
 
// RESET — P11: reset clears OWNERSHIP state in BOTH blocks. It says nothing
// about the configuration inputs, which are driven from elsewhere and are not
// this module's to clear.
property p_reset_clears_ownership;
  @(posedge clk)
  !rst_n |=> (!out_valid && (cnt_q == '0) && !hold_valid_q);
endproperty
a_reset : assert property (p_reset_clears_ownership);
 
// SAFETY — P12: presented metadata is never unknown.
property p_metadata_never_unknown;
  @(posedge clk) disable iff (!rst_n)
  out_valid |-> !$isunknown({out_attr, out_route, out_seq});
endproperty
a_no_x : assert property (p_metadata_never_unknown);
 
// HANDSHAKE — P13: a held transaction survives an idle input. Catches the
// classic `hold_valid_q <= in_valid` mistake, which drops the held
// transaction the instant the producer stops offering.
property p_hold_survives_idle_input;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && !out_ready && !in_valid) |=> out_valid;
endproperty
a_hold_survives : assert property (p_hold_survives_idle_input);
 
// HANDSHAKE — P14: SAME-CYCLE REPLACEMENT. Consuming the held transaction and
// accepting a new one in the same cycle must leave the stage occupied by the
// NEW one — not empty, and not still holding the old one.
property p_simultaneous_replace;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && out_ready && in_valid)
    |=> (out_valid && (out_attr == $past(in_attr)));
endproperty
a_replace : assert property (p_simultaneous_replace);
 
// CONSERVATION — P15: exactly one output handshake per accepted input. No
// transaction is duplicated at the output and none is silently swallowed.
// (acc_count/out_count are testbench counters.)
property p_one_out_per_in;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && out_ready) |-> (out_count + 1 <= acc_count);
endproperty
a_conserved : assert property (p_one_out_per_in);

P1, P8 and P8a are the chapter, expressed three times at three different points. P1 says a queued transaction's metadata does not move; P8 says a checked transaction's verdict was fixed at acceptance; P8a says everything the checker holds is stable for the whole stall. All three fail on exactly one design error — an output driven from live state instead of from registered per-transaction state — and all three need a stall to fire.

P8 and P7 are anchored to accept, not to $past(...) of the previous cycle. With a real handshake a transaction can be held for many cycles, so "the output equals what was on the input last cycle" is simply false while the stage stalls. Anchoring to the acceptance event is what makes the property survive backpressure — and a property that only holds when nothing stalls is not checking ownership at all.

P8b, P13 and P14 are the handshake triad and they fail on three different mistakes. P8b catches an offer that is withdrawn without a handshake. P13 catches hold_valid_q <= in_valid, which drops a held transaction the instant the producer goes idle. P14 catches the priority error: if consume takes priority over accept in the register block, a same-cycle replacement leaves the stage empty and the new transaction is lost with no indication anywhere.

The shadow sequence field in P2 and P3 is verification apparatus and nothing else. It is not a PCIe Tag, not a correlation identifier, and not present in any packet. It exists so the testbench can say this output belongs to that input, which is not otherwise expressible for a queue that carries no unique protocol field.

P9 and P10 are a pair that pins the verdict to its reasons. P9 alone permits a design that reports failure with no reason; P10 alone permits one that raises reasons and still reports success. Together they make the two representations of the same fact provably consistent — which is what makes the per-reason outputs trustworthy for debugging.

P11 is deliberately narrow, and it covers both blocks. Reset clears what the modules own: the queue, its count, and the checker's holding register. It does not clear tc_supported or the permission bits, because those are configuration driven from outside and clearing them on a local reset would silently un-configure the component.

P15 is a conservation property and it needs testbench counters rather than design state. Neither module carries a field that would let an assertion pair an output with its input, so the count relation is the strongest local statement available: the number of transactions that have left never exceeds the number accepted. It catches duplication directly and starvation indirectly.

12. Verification

Monitors observe: the enqueue interface with its metadata and shadow sequence; the dequeue interface; the policy inputs; and the checker's verdict and per-reason outputs.

The scoreboard stores attributes at the moment of acceptance, keyed by the shadow sequence number, and compares them against the output when it eventually emerges. It must not derive the expected value from the DUT's live policy inputs, from mem_q, or from the checker's own comparisons. This is the discipline that makes §7's bug findable: a scoreboard that reads live configuration will agree with a DUT that reads live configuration, and both will be wrong together.

Attribute coverage

  • Every Traffic Class, TC0 through TC7. Verify each is preserved.
  • Every combination of the three attribute bits — all eight. Small enough to be exhaustive, so be exhaustive.
  • Alternating attribute patterns. Transactions with opposite attributes back to back, so that a metadata leak between adjacent entries is visible (P3).
  • The same TC with different attributes, and the same attributes with different TC. Verify the two fields are independent — a design that packed them into one register field wrongly fails one of these.

Queue behaviour

  • Long downstream stalls with the queue non-empty. Verify head stability across the whole stall (P1).
  • Simultaneous push and pop, repeatedly and at every occupancy from empty+1 to full (P5).
  • Fill to DEPTH and drain to empty. Verify order and exact preservation throughout.
  • DEPTH = 1. The parameter minimum: verify the index width guard works and push/pop in the same cycle behaves.
  • A non-power-of-two DEPTH — 3, 5, 7. Verify wrap correctness. A design relying on natural rollover passes every power-of-two depth and aliases entries here.
  • Reset while the queue is non-empty. Verify occupancy clears and no stale entry emerges (P11).

Policy

  • Each attribute requested while its permission is low. Verify the specific reason is raised and the attribute is still forwarded unmodified (P7, P9).
  • A TC outside tc_supported. Verify unsupported_tc and only that reason.
  • Multiple simultaneous violations. Verify every applicable reason is raised, not just the first.
  • All permissions high. Verify policy_ok for every attribute combination.

Policy-checker handshake

  • Offer with the stage empty. Verify in_ready is high, the transaction is accepted, and it appears at the output the next cycle.
  • Offer with the stage occupied and out_ready low. Verify in_ready is low and the held transaction is completely unaffected (P8a, P8c).
  • Hold the output stalled for a long run with in_valid low. Verify out_valid stays asserted and nothing changes (P8b, P13) — the test that kills hold_valid_q <= in_valid.
  • Same-cycle replacement: out_valid && out_ready && in_valid together. Verify the stage ends occupied by the new transaction, with the new attributes and the new verdict (P14).
  • A run of back-to-back same-cycle replacements with alternating attributes. Verify every transaction appears exactly once and none is blended with its neighbour.
  • in_ready must not depend on in_valid. Drive in_valid low and high with the stage in each state and verify in_ready is unchanged.
  • Accepted-versus-emitted counts over a long randomised run with independent producer and consumer backpressure (P15).

The configuration-change test

  • Accept a transaction, stall it, then change the policy configuration while it is in flight, then release it.

    Expected result: the already-owned transaction's metadata and verdict do not change. Its attributes are what arrived; its verdict was computed against the configuration in force when it was accepted.

    This is the chapter's highest-value test and it is the one a normal test plan omits, because it requires two independent stimuli to coincide. Run it with the change landing at every cycle of the stall — the window matters.

  • The same test on the queue rather than the checker. Change unrelated configuration during a stall and verify the head does not move (P1, P4).

Coverage should include: all eight TC values; all eight attribute combinations; both permission states for each of the three attributes; occupancy from empty to full including the simultaneous-push-pop case at each level; DEPTH = 1 and a non-power-of-two depth; every policy reason individually and in combination; and the configuration-change-during-stall case.

13. Performance Reasoning — Read This Carefully

Attributes can influence how the system is allowed to schedule, order and treat a transaction. That is not the same as making it faster, and the difference is where most attribute misuse originates.

AttributeWhat it grantsWhat it does not grant
TCmembership in a service classany latency or bandwidth guarantee; any priority, unless configuration provides one
ROpermission to relax specific ordering constraintsany actual reordering; any speedup; any exemption from the constraints that remain
NSrelief from a snoop requirementa guarantee the snoop is skipped; a guarantee that skipping it helps
IDOpermission to pass a previously posted write from a different Requestera per-Requester ordering model of your own design

Every row is a permission, and permissions are not outcomes. Whether any of them changes observable behaviour depends on the fabric, the platform and the traffic — and on a great many systems, on nothing at all, because the configuration never made the class distinction that would have mattered.

And the asymmetry is the point: the performance upside is conditional and often zero, while the correctness downside of an incorrectly-set attribute is unconditional. A Requester that sets RO on traffic with a real ordering relationship, or NS on memory that software touches coherently, has traded a possible speedup for a definite intermittent bug.

No benchmark claims appear in this chapter, and none should appear in a design review either. Chapter 12.5 takes performance implications properly.

14. Debugging

Two otherwise identical requests behave differently with respect to ordering

"Otherwise identical" is doing the work in that sentence — check whether it is true at the bit level first.

Capture both packets and compare Attr[2], Attr[1], Attr[0] and TC as decoded by a tool you trust against Base Specification §2.2 — not by a script someone wrote from memory. A decoder carrying a wrong layout assumption will report identical Attr for two packets that genuinely differ.

If the bits genuinely differ, the question moves upstream: which block set them, and from what? A policy stage that rewrites attributes (§10's forbidden behaviour) is the usual answer.

If the bits are identical, the attributes are not the explanation and the difference is in the fabric's treatment — which is Chapter 13.4's territory, not this chapter's.

A transaction enters a queue with TC3 and leaves with TC0

This is a metadata ownership bug, and the symptom names it precisely.

Three candidates, distinguishable by when it happens. If it happens on the very first transaction after reset, the output is bypassing storage and reading an uninitialised or default value. If it happens only when the queue was empty, there is a bypass path from the input to the output. If it happens only under backpressure, the output is being driven from something other than the stored entry — §7's failure.

And a fourth that looks different: if the TC that emerges belongs to a different transaction that was in the queue, the pointers or the index width are wrong, and a non-power-of-two DEPTH is the first thing to check.

P1 and P4 between them cover all four, which is why they are stated as separate properties rather than as one.

No Snoop appears set only after long backpressure

The conditional is the entire diagnosis: metadata is not stored with the packet, and the output is recomputed from live signals.

Under no backpressure, the packet departs so soon after acceptance that whatever the output is derived from has not had time to change. Introduce a stall long enough for a configuration write to land, and the packet's attributes follow the configuration instead of following the packet.

The confirming observation is one experiment: stall a packet and write the configuration register while it waits. If the departing packet's NS bit tracks the register rather than what was accepted, the path is combinational and §12's configuration-change test would have caught it.

The IDO bit is wrong but RO and NS are always correct

Two of three correct is the signature of a contiguity assumption.

The three Attr bits are independent flags whose positions are Base Specification §2.2's, and a decode built on a wrong layout assumption typically recovers most of them and misses one — picking up a neighbouring header bit instead of the attribute it wanted. Depending on which bit it caught, the wrong one may read as plausibly zero most of the time, which is why it survives inspection.

The check takes one packet: decode the attribute against Base Specification §2.2 by hand and compare with what the design reported. And the structural fix is §8's — assemble the attributes once in the header decoder, then never touch the raw header again, so that a layout assumption exists in exactly one place and can be corrected in exactly one place.

15. Common Misconceptions

  • "TC is just packet priority." TC names a service class. What that class receives is determined by TC-to-VC mapping and VC servicing, both established by configuration (§3).
  • "Higher TC always wins arbitration." TC is an identifier, not a magnitude. On a system where all TCs map to one VC, TC7 and TC0 are treated identically (§3).
  • "TC guarantees latency or bandwidth." It guarantees neither. It requests membership in a class whose treatment is a system property (§3).
  • "TC and Attr are the same field." Two separate fields, three bits each, consumed by different mechanisms — TC selects a service class, the Attr bits grant permissions (§1, §2).
  • "Relaxed Ordering means no ordering rules apply." It permits specific relaxations — posted writes passing posted writes and messages, read completions passing posted writes in the same direction. Everything else still applies (§4).
  • "Relaxed Ordering means the packet may overtake any other packet." It is a permission scoped to named relationships, and the fabric may reorder or not (§4).
  • "No Snoop means do not use the cache." It means the Requester indicates no coherency issues exist and system hardware is not required to snoop. A system that snoops anyway is conformant (§5).
  • "No Snoop is always faster." There is no protocol-level performance promise attached to it, and on many paths it changes nothing (§5, §13).
  • "An Endpoint can set No Snoop whenever it likes." Its correctness depends on the software, platform and device contract. Setting it wrongly produces stale data with no protocol error (§5).
  • "IDO means transactions from the same Requester ID are ordered and others are free." That is a design's own ordering model, not the specification's. IDO permits a specific relaxation scoped by identity (§6).
  • "Attributes can be recomputed later because the packet type is unchanged." Attributes are owned packet state from acceptance onward. Recomputing them changes an in-flight transaction's semantics (§7).
  • "Attributes only matter to software." They constrain what the fabric and the Completer may do with the transaction, and preserving them is an RTL responsibility (§7).
  • "Routing metadata and ordering attributes are interchangeable." Routing decides where (Chapter 11.5); attributes decide under what constraints. Different fields, different consumers.
  • "A policy checker should clear an attribute it does not support." Rewriting a Requester's stated policy changes the transaction's semantics unobservably. Detect and report (§10).

16. Understanding Check

17. What's Next

This chapter separated two fields the curriculum groups together and gave each its own name: a Traffic Class that names a service class rather than a priority, and an Attr field of three independent flags that grant permissions rather than produce outcomes. It then made the RTL point that matters more than any of them — attributes are owned packet state from the moment a transaction is accepted, and a design that recomputes them has changed a transaction after the system finished reasoning about it.

Chapter 11.7 — Packet Types closes Module 11 by unifying everything the module has built: which TLP families exist, what semantic lifecycle each represents, and how packet type determines payload, routing, posted/non-posted behaviour and Completion expectation — all from one normalized classification rather than from raw header bits decoded in five places.

Chapter 13.4 then owns the ordering rules themselves: what may pass what, in which direction, and why producer/consumer correctness depends on the defaults this chapter's attributes are permitted to relax. The flow-control chapters own Virtual Channels and the credit accounting that gives Traffic Class its meaning.

The idea to carry forward: an attribute is a permission the transaction carries, granted at acceptance and owned thereafter — not a switch that anything downstream is free to re-evaluate.