Skip to content

PCIe · Module 11

TLP Structure — Counting a Packet Before Decoding It

Every TLP is a header, optionally a payload, optionally a digest. Why the header comes in two lengths, how header, payload and packet length differ, why DW and interface beat are not the same unit, and a structural parser that frames a packet without decoding a field.

Chapter 11.1 named the three regions of a TLP and did not size any of them. That was deliberate: sizing is a subject in itself, and it is the one an RTL engineer meets first.

What structural regions make up a PCIe TLP, what is common across packet classes, and why does header length and payload presence vary?

1. Structure Before Semantics

A receiver faced with an incoming packet has two questions and they are not equally urgent.

"What operation is this?" determines what the device should do. It needs the header's fields decoded and interpreted.

"Where does this packet end?" determines whether the device can process the next packet at all. It needs only the packet's shape.

Getting the semantics wrong corrupts one operation. Getting the structure wrong desynchronises the receiver and corrupts everything after it.

Chapter 11.1 §14's last question worked through why: a content error is confined to one packet, while a framing error offsets the receiver's idea of "start of packet" and every subsequent packet is parsed from the wrong position.

Which is why this chapter comes before the header-field chapter, and why its RTL decodes nothing. §9's parser counts regions and finds boundaries with no knowledge of what any field means — and that independence is what makes it verifiable on its own.

2. The Verified Structure

3. Units — Byte, DW, and Beat

Three units appear in any discussion of packet structure, and conflating the last two is the most common source of RTL bugs in this area.

UnitSizeWhere it comes from
Byte8 bitsuniversal
DW (DWORD)32 bits = 4 bytesthe unit PCIe describes TLP structure in
Beatthe width of one transfer on an internal interfacethe implementation's choice, not PCIe's

PCIe describes packets in DW. Headers are 3 or 4 DW; payload is "in increments of four-Byte Double Words"; the digest is 1 DW. Every structural quantity in this chapter is a DW count.

A datapath moves beats. A 128-bit interface carries 4 DW per beat; a 256-bit interface carries 8. Nothing in PCIe says what that width should be — it is chosen from the link rate and the clock frequency the design can close at.

4. The Three Regions

Two TLP structural shapes. The upper shape is a header-only packet: a three DW header, optionally followed by a one DW digest. The lower shape is a packet with data: a four DW header, followed by an N DW payload, optionally followed by a one DW digest. Both shapes contribute to a total packet length that is the sum of the regions present.Header — 3 DWformshortest legal packetstartDigest — 1 DW,optionalpresent only whenenabledHeader — 4 DWformcarries the wideraddressPayload — N DWwhole DW only, DWalignedDigest — 1 DW,optionalpresent only whenenabledTotal packetlengthsum of the regionspresent12
Figure 1 — two packet shapes, drawn to the same scale. The header is always present and is either 3 or 4 DW. A payload follows it only for packet types that carry data, in whole DW. A digest, when enabled, occupies one DW at the very end. Total packet length is the sum of whichever regions are present.

Three facts the figure encodes.

The order is fixed. Header, then payload if present, then digest if present. Nothing appears between the header and the payload, and nothing follows the digest.

Two regions are conditional and one is not. Every TLP starts with a header. Whether the other two exist is a per-packet property.

Both conditional regions are indicated in the header. The packet is self-describing: everything a receiver needs to find the end is in the part it reads first. That is what makes §9's parser possible at all.

5. Why the Header Has Two Lengths

The architectural reason is simple once stated, and it explains why the variation is exactly two rather than many.

Different packet categories need different information (Chapter 11.1 §4). A memory Request needs an address; a configuration Request identifies a Function; a Completion must identify the Request it answers. Most of that context fits in three DW.

One thing does not: a 64-bit address. PCIe supports both 32-bit and 64-bit addressing formats for Memory Requests, and the wider address needs one more DW than the shorter one. That is the whole reason the longer form exists.

Header formSizeUsed by
3 DW12 bytespackets whose context fits — including the 32-bit-addressing Memory Request form and Completions
4 DW16 bytespackets needing the extra DW — including the 64-bit-addressing Memory Request form

What the field that selects the form is called, where it sits, and how it encodes both the form and the payload presence are Chapter 11.3's. This chapter needs only that such an indication exists and is available early.

6. Three Different Lengths

The word "length" is used for three different quantities in PCIe discussions, and mixing them is a real source of bugs.

QuantityWhat it measuresRange in this chapter's model
Header lengththe header region3 or 4 DW
Payload lengththe data region0 DW when absent, otherwise a whole number of DW
Total packet lengththe whole TLPheader + payload + digest

They are not derivable from one another. A 4 DW header says nothing about payload size; a large payload says nothing about which header form is in use.

And none of them is the same as the size of the software operation that caused the packet. A software transfer may become several packets, and how payload size is bounded is Chapter 11.4's subject. This chapter's arithmetic is entirely about the packet on the wire.

The structural length calculation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
total_dw     =  header_dw  +  payload_dw  +  digest_dw
total_bytes  =  total_dw × 4

where header_dw is 3 or 4, payload_dw is 0 when no payload is present, and digest_dw is 0 or 1.

Worked shapes

ShapeHeaderPayloadDigestTotal DWTotal bytes
Header-only, short form, no digest300312
Header-only, long form, no digest400416
Short header + 1 DW of data310416
Long header + 1 DW of data410520
Short header + 16 DW of data31601976
Long header + 16 DW of data, digest enabled41612184
Short header + 64 DW of data, digest enabled364168272

7. Microarchitecture — Two Decoders, One Packet

The receive path has two consumers of the header and they want different things at different times.

Structural decodeSemantic decode
Askshow many DW, where do the regions endwhat operation is this
Needsthe form, payload presence, payload length, digest presenceevery field
Must completebefore the next packet arrivesbefore the operation is acted on
Failure modereceiver desynchronises; all subsequent packets corruptone operation is wrong
Owned bythis chapterChapter 11.3

The structural decode needs four numbers and nothing else. Header form, payload present, payload length, digest present. Everything else in the header is irrelevant to finding the end of the packet.

So the design separates them, and the separation buys three things:

  • The structural path can be verified exhaustively against packet shapes, with no model of what a correct header contains.
  • The two failure modes stay distinguishable, which §11's debugging depends on entirely.
  • The structural path is on the critical timing path and the semantic path usually is not. Framing has to keep up with the link; interpretation can be pipelined.

8. RTL — Structural Length

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// COMPILE-TIME. Structural constants and length arithmetic for a TLP.
// DW = 32 bits, header = 3 or 4 DW, digest = 1 DW, payload in whole DW:
// NORMATIVE. The struct and the legality rule: illustrative.
package tlp_struct_pkg;
 
  localparam int DW_BITS      = 32;   // a DW is 32 bits
  localparam int HDR_DW_SHORT = 3;    // 12 bytes
  localparam int HDR_DW_LONG  = 4;    // 16 bytes
  localparam int DIGEST_DW    = 1;    // one DW at the end, when present
 
  // The four numbers a structural parser needs, and nothing else. This is
  // NOT a TLP header and carries no field encoding — Chapter 11.3 owns the
  // header field that a real design would decode to produce these.
  typedef struct packed {
    logic        long_header;   // 4 DW rather than 3
    logic        has_payload;
    logic [10:0] payload_dw;    // whole DW; the encoded Length is 11.3's
    logic        has_digest;
  } tlp_struct_t;
 
  typedef enum logic [1:0] {
    R_HDR = 2'd0,
    R_PAY = 2'd1,
    R_DIG = 2'd2
  } tlp_region_e;
 
  function automatic int unsigned hdr_dw(input bit long_header);
    return long_header ? HDR_DW_LONG : HDR_DW_SHORT;
  endfunction
 
  // A shape this model can frame. The two contradictions are separated
  // deliberately: a flag without a length and a length without a flag are
  // different producer bugs, and lumping them loses that.
  function automatic bit struct_legal(input tlp_struct_t s,
                                      input int unsigned max_pl_dw);
    if (s.has_payload  && (s.payload_dw == 11'd0))       return 1'b0;
    if (!s.has_payload && (s.payload_dw != 11'd0))       return 1'b0;
    if (s.has_payload  && (int'(s.payload_dw) > max_pl_dw)) return 1'b0;
    return 1'b1;
  endfunction
 
  // Total DW. Written as three separate terms rather than a fused expression
  // so that a region added later has one obvious place to go.
  function automatic int unsigned total_dw(input tlp_struct_t s);
    int unsigned t;
    t = hdr_dw(s.long_header);
    if (s.has_payload) t = t + int'(s.payload_dw);
    if (s.has_digest)  t = t + DIGEST_DW;
    return t;
  endfunction
 
  function automatic int unsigned total_bytes(input tlp_struct_t s);
    return total_dw(s) * 4;
  endfunction
 
endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. The same arithmetic in hardware, with widths derived from
// the parameterised bound so nothing can overflow silently.
import tlp_struct_pkg::*;
 
module tlp_length_calc #(
  // The largest payload this model frames. The REAL bound on payload size is
  // Chapter 11.4's; this is a local design parameter.
  parameter int MAX_PL_DW = 1024,
  // Widths derived so the maximum representable total always fits.
  parameter int TOT_DW_W  = $clog2(HDR_DW_LONG + MAX_PL_DW + DIGEST_DW + 1),
  parameter int TOT_B_W   = TOT_DW_W + 2      // × 4 needs two more bits
) (
  input  tlp_struct_t          s,
  output logic [TOT_DW_W-1:0]  total_dw_o,
  output logic [TOT_B_W-1:0]   total_bytes_o,
  output logic                 struct_ok
);
 
  generate
    if (MAX_PL_DW < 1) $error("MAX_PL_DW must be at least 1");
    if (MAX_PL_DW > 2047) $error("payload_dw is 11 bits in this model");
  endgenerate
 
  wire [TOT_DW_W-1:0] hdr_term = s.long_header ? TOT_DW_W'(HDR_DW_LONG)
                                               : TOT_DW_W'(HDR_DW_SHORT);
  wire [TOT_DW_W-1:0] pl_term  = s.has_payload ? TOT_DW_W'(s.payload_dw)
                                               : TOT_DW_W'(0);
  wire [TOT_DW_W-1:0] dg_term  = s.has_digest  ? TOT_DW_W'(DIGEST_DW)
                                               : TOT_DW_W'(0);
 
  assign struct_ok = struct_legal(s, MAX_PL_DW);
 
  // The sum cannot overflow TOT_DW_W by construction: TOT_DW_W was derived
  // from the maximum of exactly these three terms. That is why the width is
  // computed from the bound rather than picked.
  assign total_dw_o = hdr_term + pl_term + dg_term;
 
  // × 4 as a shift, in a wider result. Multiplying in the narrow width and
  // widening afterwards is the classic way to lose the top two bits.
  assign total_bytes_o = {total_dw_o, 2'b00};
 
endmodule

Classification: compile-time (package) and synthesizable (module).

What it teaches — four things:

  1. The widths are derived from the bound, not chosen. TOT_DW_W comes from $clog2 of the maximum possible total, so the sum provably fits. A hand-picked width is correct until someone raises MAX_PL_DW.
  2. The byte conversion widens first. {total_dw_o, 2'b00} is a concatenation into a wider result. Computing total_dw_o * 4 in the DW width and assigning to a wider signal loses the top two bits for any packet near the maximum — and only for those, which is the worst possible distribution of a bug.
  3. The legality rule separates two producer errors. A payload flag with no length and a length with no flag are different mistakes with different causes, and a single combined check would report them identically.
  4. The total is three named terms. When a region is added — and current PCIe has more of them than this base structure shows (§2) — there is exactly one place to add it, and the width derivation updates with it.

Deliberately simplified: the base structure only, with no prefix region; a single digest size; and a payload bound that is a local parameter rather than a protocol limit.

9. RTL — A Structural Parser

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Tracks a TLP's structural regions beat by beat, finds the
// packet end, and flags a structure that disagrees with the framing.
// Region ordering and DW granularity: NORMATIVE. The interface, the hint
// port and the error codes: illustrative.
import tlp_struct_pkg::*;
 
module tlp_struct_parser #(
  parameter int MAX_PL_DW = 1024,
  parameter int TOT_DW_W  = $clog2(HDR_DW_LONG + MAX_PL_DW + DIGEST_DW + 1)
) (
  input  logic               clk,
  input  logic               rst_n,
 
  // ---- Inbound beat stream, one DW per beat ---------------------------
  input  logic               in_valid,
  output logic               in_ready,
  input  logic [31:0]        in_data,
  input  logic               in_sop,
  // End-of-packet from the framing below the Transaction Layer. The parser
  // CHECKS this against the structure rather than trusting either alone.
  input  logic               in_eop,
  // The four structural numbers, valid with in_sop (section 7).
  input  tlp_struct_t        hint,
 
  // ---- Classified beat stream out --------------------------------------
  output logic               out_valid,
  input  logic               out_ready,
  output logic [31:0]        out_data,
  output tlp_region_e        out_region,
  output logic               out_last,        // final DW of the packet
 
  // ---- Structural results and faults -----------------------------------
  output logic               pkt_done,        // a packet ended cleanly
  output logic               struct_error,
  output logic [2:0]         err_code
);
 
  localparam logic [2:0] ERR_NONE      = 3'd0;
  localparam logic [2:0] ERR_BAD_HINT  = 3'd1;  // shape this model cannot frame
  localparam logic [2:0] ERR_EARLY_EOP = 3'd2;  // framing ended before the structure did
  localparam logic [2:0] ERR_LATE_EOP  = 3'd3;  // structure ended and framing did not
  localparam logic [2:0] ERR_NO_SOP    = 3'd4;  // a beat arrived with no packet open
 
  tlp_region_e      region_q;
  logic [TOT_DW_W-1:0] rem_q;      // DW remaining in the CURRENT region
  logic [TOT_DW_W-1:0] pl_q, dg_q; // DW held for the regions still to come
  logic                active_q;   // a packet is open
 
  // Ready depends on the DOWNSTREAM ready only — never on in_valid, and
  // never on the hint. A malformed packet is consumed and reported, not
  // stalled, so a structural fault cannot wedge the receive path.
  assign in_ready = out_ready;
  wire beat = in_valid && in_ready;
 
  // ---- The shape, as DW counts ----------------------------------------
  wire [TOT_DW_W-1:0] hdr_n = hint.long_header ? TOT_DW_W'(HDR_DW_LONG)
                                               : TOT_DW_W'(HDR_DW_SHORT);
  wire [TOT_DW_W-1:0] pl_n  = hint.has_payload ? TOT_DW_W'(hint.payload_dw)
                                               : TOT_DW_W'(0);
  wire [TOT_DW_W-1:0] dg_n  = hint.has_digest  ? TOT_DW_W'(DIGEST_DW)
                                               : TOT_DW_W'(0);
 
  wire hint_ok = struct_legal(hint, MAX_PL_DW);
 
  // How many DW remain in the current region AFTER this beat. On the first
  // beat the header count comes from the hint; afterwards from the register.
  wire [TOT_DW_W-1:0] rem_after = in_sop ? (hdr_n - TOT_DW_W'(1))
                                         : (rem_q - TOT_DW_W'(1));
  wire region_ends = (rem_after == '0);
 
  // The counts for the regions still to come. On the first beat they are
  // still in the hint; afterwards they are registered. A header is always at
  // least 3 DW, so a region change can never occur on the first beat and the
  // registered values are always available when they are needed.
  wire [TOT_DW_W-1:0] pl_sel = in_sop ? pl_n : pl_q;
  wire [TOT_DW_W-1:0] dg_sel = in_sop ? dg_n : dg_q;
 
  // ---- Region sequencing ------------------------------------------------
  tlp_region_e         next_region;
  logic [TOT_DW_W-1:0] next_rem;
  logic                struct_last;   // the structure says this is the last DW
 
  always_comb begin
    next_region = region_q;
    next_rem    = rem_after;
    struct_last = 1'b0;
 
    if (region_ends) begin
      case (in_sop ? R_HDR : region_q)
        R_HDR: begin
          if (pl_sel != '0)      begin next_region = R_PAY; next_rem = pl_sel; end
          else if (dg_sel != '0) begin next_region = R_DIG; next_rem = dg_sel; end
          else                    struct_last = 1'b1;
        end
        R_PAY: begin
          if (dg_sel != '0)      begin next_region = R_DIG; next_rem = dg_sel; end
          else                    struct_last = 1'b1;
        end
        default:                  struct_last = 1'b1;   // R_DIG
      endcase
    end
  end
 
  assign out_valid  = in_valid;
  assign out_data   = in_data;
  assign out_region = in_sop ? R_HDR : region_q;
  assign out_last   = struct_last;
 
  // ---- Faults -----------------------------------------------------------
  // Structure and framing are two independent statements about where the
  // packet ends. Checking them AGAINST EACH OTHER is the whole point: either
  // alone can be wrong and neither can detect it.
  wire err_bad_hint  = beat &&  in_sop && !hint_ok;
  wire err_no_sop    = beat && !in_sop && !active_q;
  wire err_early_eop = beat &&  in_eop && !struct_last && !err_bad_hint;
  wire err_late_eop  = beat && !in_eop &&  struct_last;
 
  assign struct_error = err_bad_hint | err_no_sop | err_early_eop | err_late_eop;
  assign err_code = err_bad_hint  ? ERR_BAD_HINT
                  : err_no_sop    ? ERR_NO_SOP
                  : err_early_eop ? ERR_EARLY_EOP
                  : err_late_eop  ? ERR_LATE_EOP
                                  : ERR_NONE;
 
  // A packet ends cleanly only when both statements agree.
  assign pkt_done = beat && struct_last && in_eop && !err_bad_hint;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      region_q <= R_HDR;
      rem_q    <= '0;
      pl_q     <= '0;
      dg_q     <= '0;
      active_q <= 1'b0;
    end else if (beat) begin
      if (in_sop && !hint_ok) begin
        // An unframeable shape opens no packet. Guessing a length would
        // desynchronise the receiver for every packet that follows.
        active_q <= 1'b0;
      end else if (in_sop) begin
        region_q <= next_region;
        rem_q    <= next_rem;
        pl_q     <= pl_n;
        dg_q     <= dg_n;
        active_q <= !struct_last;
      end else if (active_q) begin
        region_q <= next_region;
        rem_q    <= next_rem;
        active_q <= !struct_last;
      end
 
      // Framing always wins the question "is a packet open". A truncated
      // packet must not leave the parser waiting for DW that will never come.
      if (in_eop) active_q <= 1'b0;
    end
  end
 
endmodule

Classification: synthesizable.

Semantics — every dimension:

DimensionBehaviour
Resetno packet open; region defaults to header
in_readydownstream ready only — never in_valid, never the hint
out_validfollows in_valid; the parser classifies, it does not buffer
Region on the first beatalways header, by construction
Region sequencingheader → payload if any → digest if any → done
Packet endasserted only when structure and framing agree
Unframeable hintreported; no packet opens; the beat is consumed
in_eopalways closes the packet, whatever the structure said
Beat with no packet openreported as ERR_NO_SOP; state unchanged
Reset mid-packetthe packet is abandoned; the next non-sop beat is reported

Trace a short header with one DW of data and no digest.

Beat 1in_sop, hint says 3 DW header, payload 1 DW. rem_after = 2, so the region does not end; out_region is header. pl_q captures 1, dg_q captures 0, active_q sets.

Beat 2rem_after = 1, still header.

Beat 3rem_after = 0, region ends. pl_sel is 1, so next_region is payload with next_rem = 1. Not the last DW.

Beat 4 — region is payload, rem_after = 0, pl region ends, dg_sel is 0, so struct_last asserts. If in_eop is also high, pkt_done fires and active_q clears. If in_eop is low, ERR_LATE_EOP fires — the structure says four DW and the framing says more.

Trace the same packet with a digest. Beat 4 ends the payload, dg_sel is 1, so next_region is digest with one DW remaining. Beat 5 is the digest and is the last DW.

What it teaches — five things:

  1. Structure and framing are checked against each other. The lower layers say where the packet ends; the header says how long it should be. Either can be wrong and neither can detect its own error — comparing them is what turns a silent corruption into ERR_EARLY_EOP or ERR_LATE_EOP.
  2. Framing wins for closing the packet. in_eop always clears active_q, even when the structure expected more DW. A parser that trusted the structure would sit waiting for DW that a truncated packet will never supply, and would then misparse the next packet as a continuation.
  3. An unframeable shape opens no packet. Both guesses desynchronise the receiver; refusing localises the fault to the packet that caused it — the same discipline as Chapter 11.1 §9's meta_illegal.
  4. in_ready never depends on the hint. A malformed packet is consumed and reported. A parser that stalled on a structure it disliked would convert a recoverable framing error into a hang, and the link would stop rather than report.
  5. The region-change-on-first-beat case is impossible and it is documented as such. A header is at least 3 DW, so region_ends cannot be true on the in_sop beat, which is what makes pl_q/dg_q reliably available when they are first needed. Relying on that without saying so would be exactly the kind of hidden assumption a later edit breaks.

Deliberately simplified: one DW per beat (§3); no prefix region; no buffering — the parser classifies a stream it does not hold; and a single digest size.

Production implication: a real parser handles a beat several DW wide with per-DW region marking and partial final beats, may need to handle two packets in one beat, tolerates prefix regions where the link uses them, and feeds a semantic decode that runs in parallel rather than after it.

10. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SVA over tlp_struct_parser and tlp_length_calc. Structural invariants for
// THESE designs plus the normative region ordering they implement — no claim
// about any header field.
 
// ENVIRONMENT ASSUMPTIONS. The source owes valid- and payload-stability, and
// the hint must be stable for the beat it accompanies. Well-formed PACKETS
// are NOT assumed — malformed structure is what half these properties are
// for.
assume property (@(posedge clk) disable iff (!rst_n)
  (in_valid && !in_ready) |=> (in_valid && $stable(in_data) && $stable(in_sop)
                               && $stable(in_eop) && $stable(hint)));
 
// ORDERING — P1: the first beat of a packet is always a header beat. The
// normative "all TLPs must start with a header", made structural.
property p_first_beat_is_header;
  @(posedge clk) disable iff (!rst_n)
  (beat && in_sop) |-> (out_region == R_HDR);
endproperty
a_starts_with_header : assert property (p_first_beat_is_header);
 
// ORDERING — P2: no payload beat precedes the end of the header.
property p_payload_after_header;
  @(posedge clk) disable iff (!rst_n)
  (beat && (out_region == R_PAY)) |-> ($past(region_q) == R_PAY
                                       || $past(next_region) == R_PAY);
endproperty
a_payload_ordered : assert property (p_payload_after_header);
 
// ORDERING — P3: the digest, if present, is last. Nothing follows it.
property p_digest_is_final;
  @(posedge clk) disable iff (!rst_n)
  (beat && (out_region == R_DIG)) |-> out_last;
endproperty
a_digest_last : assert property (p_digest_is_final);
 
// COUNTING — P4: the header occupies exactly the number of DW the shape
// declared. Catches an off-by-one in the header count, which is the bug that
// makes a 4 DW header lose the first payload DW.
property p_header_dw_exact;
  @(posedge clk) disable iff (!rst_n)
  (beat && in_sop && hint_ok)
    |-> (rem_after == (hint.long_header ? TOT_DW_W'(HDR_DW_LONG - 1)
                                        : TOT_DW_W'(HDR_DW_SHORT - 1)));
endproperty
a_hdr_count : assert property (p_header_dw_exact);
 
// COUNTING — P5: a shape with no payload never enters the payload region.
property p_no_payload_region_when_absent;
  @(posedge clk) disable iff (!rst_n)
  (active_q && (pl_q == '0)) |-> (region_q != R_PAY);
endproperty
a_no_stray_payload : assert property (p_no_payload_region_when_absent);
 
// COUNTING — P6: a shape with no digest never enters the digest region.
property p_no_digest_region_when_absent;
  @(posedge clk) disable iff (!rst_n)
  (active_q && (dg_q == '0)) |-> (region_q != R_DIG);
endproperty
a_no_stray_digest : assert property (p_no_digest_region_when_absent);
 
// COUNTING — P7: the remaining count never underflows. A wrapped counter
// would run a packet for thousands of beats and swallow every packet after.
property p_rem_never_wraps;
  @(posedge clk) disable iff (!rst_n)
  (beat && active_q && !in_sop) |-> (rem_q != '0);
endproperty
a_no_underflow : assert property (p_rem_never_wraps);
 
// AGREEMENT — P8: a packet ends cleanly only when the structure and the
// framing agree. The chapter's central check.
property p_clean_end_needs_agreement;
  @(posedge clk) disable iff (!rst_n)
  pkt_done |-> (struct_last && in_eop);
endproperty
a_end_agreed : assert property (p_clean_end_needs_agreement);
 
// AGREEMENT — P9: a disagreement is always reported. Neither direction may
// pass silently.
property p_disagreement_reported;
  @(posedge clk) disable iff (!rst_n)
  (beat && !err_bad_hint && (struct_last != in_eop)) |-> struct_error;
endproperty
a_disagreement_flagged : assert property (p_disagreement_reported);
 
// LEGALITY — P10: an unframeable shape opens no packet.
property p_bad_hint_opens_nothing;
  @(posedge clk) disable iff (!rst_n)
  err_bad_hint |=> !active_q;
endproperty
a_bad_hint_refused : assert property (p_bad_hint_opens_nothing);
 
// LEGALITY — P11: framing always closes the packet, whatever the structure
// expected. The property that prevents a truncated packet hanging the parser.
property p_eop_always_closes;
  @(posedge clk) disable iff (!rst_n)
  (beat && in_eop) |=> !active_q;
endproperty
a_eop_closes : assert property (p_eop_always_closes);
 
// INTERFACE — P12: ready depends on the downstream only. Catches a ready
// that acquired a dependence on the structure, which would let a malformed
// packet stall the receive path.
property p_ready_from_downstream;
  @(posedge clk) disable iff (!rst_n)
  in_ready == out_ready;
endproperty
a_ready_passthrough : assert property (p_ready_from_downstream);
 
// ARITHMETIC — P13: the calculator's total equals the sum of its regions,
// computed independently in the property.
property p_total_dw_exact;
  @(posedge clk) disable iff (!rst_n)
  struct_ok |-> (total_dw_o == ((s.long_header ? TOT_DW_W'(HDR_DW_LONG)
                                               : TOT_DW_W'(HDR_DW_SHORT))
                              + (s.has_payload ? TOT_DW_W'(s.payload_dw)
                                               : TOT_DW_W'(0))
                              + (s.has_digest  ? TOT_DW_W'(DIGEST_DW)
                                               : TOT_DW_W'(0))));
endproperty
a_total_exact : assert property (p_total_dw_exact);
 
// ARITHMETIC — P14: the byte count is exactly four times the DW count, in a
// width that holds it. Catches the multiply-then-widen bug.
property p_bytes_exact;
  @(posedge clk) disable iff (!rst_n)
  total_bytes_o == {total_dw_o, 2'b00};
endproperty
a_bytes_exact : assert property (p_bytes_exact);
 
// SAFETY — P15: no interface output is ever unknown.
property p_outputs_never_unknown;
  @(posedge clk) disable iff (!rst_n)
  !$isunknown({in_ready, out_valid, out_last, pkt_done, struct_error, err_code});
endproperty
a_no_x : assert property (p_outputs_never_unknown);

P8 and P9 are the pair this chapter exists for. The packet's structure and the lower layers' framing are two independent statements about where a packet ends. Each can be wrong; neither can detect its own error. P8 requires agreement for a clean end and P9 requires any disagreement to be reported — together they make the receive path unable to accept a packet whose two accounts differ, which is precisely the condition that desynchronises everything downstream.

P7 is the counter-safety property, and its failure mode is spectacular. A rem_q that decrements past zero wraps to its maximum and the parser runs the packet for thousands of beats, consuming — and misclassifying — every packet that follows. The signature at the receiver is that traffic stops making sense after a single bad packet, which is the hardest kind of failure to trace back to its cause.

P4 is the off-by-one property, written against the header form specifically. It is the direct check for §11's second scenario: a parser that used the short count for a long header ends the header one DW early and the first payload DW is classified as a header DW. The property compares rem_after against the declared form on the first beat, so it fires immediately rather than after the payload has been mangled.

P12 looks trivial and prevents a real class of failure. The tempting edit is to hold in_ready low while the parser "sorts out" a malformed structure. That converts a reportable framing error into a stalled receive path — and a link that stops is worse than a link that reports.

11. Verification

Monitors observe: every beat with in_sop, in_eop and the hint; the classified region and out_last; pkt_done; and struct_error with its code.

The scoreboard computes the expected shape independently from the observed hint — its own header count, its own region boundaries, its own total — and compares the observed region sequence against it. It must not call total_dw() or struct_legal() from the design's package, and it must not read rem_q, pl_q or region_q. Those are the structures under test, and a checker sharing the design's arithmetic agrees with it about exactly P13's bug.

Packet shapes

  • Short header, no payload, no digest. Three beats; out_last on the third; pkt_done when in_eop agrees.
  • Long header, no payload, no digest. Four beats. Run this immediately after the previous case, because a parser that latched the header count once and reused it fails here and nowhere else.
  • Short header + 1 DW payload. Four beats total — the same length as the previous case and a different region sequence. §6's callout is this test.
  • Long header + 1 DW payload. Five beats.
  • Short header + a large payload. Verify the payload region count is exact for a length near MAX_PL_DW.
  • Every shape above with the digest present. Verify one extra beat, in the digest region, and that it is out_last.
  • Consecutive packets of alternating shapes. Verify each is parsed independently and no state carries between them.

Back-pressure

  • out_ready low in the header region. Verify the beat is held, the count does not advance, and in_ready drops with it.
  • out_ready low in the payload region, and in the digest region. Verify the same at each boundary.
  • out_ready low across a region transition. Verify the transition happens exactly once when the beat finally transfers, not on the stalled cycles.
  • out_ready toggling every other cycle across a long packet. Verify the region sequence and total are unchanged.

Structural faults

  • Payload shorter than declared. Assert in_eop before the structure's last DW. Verify ERR_EARLY_EOP, that the packet closes anyway (P11), and that the next packet parses cleanly — the recovery is the point of the test.
  • Payload longer than declared. Withhold in_eop at the structure's last DW. Verify ERR_LATE_EOP and that the framing still governs when the packet closes.
  • A header form the model does not support. Verify the elaboration bound and, at run time, that an out-of-range payload length is refused as ERR_BAD_HINT (P10).
  • Payload flag set with zero length, and zero flag with a non-zero length. Verify both are ERR_BAD_HINT and no packet opens.
  • A beat with no packet open. Send a non-sop beat after a completed packet. Verify ERR_NO_SOP and that state is undisturbed.
  • Reset mid-packet. Verify the packet is abandoned, and that the first beat afterwards is reported as ERR_NO_SOP unless it is a sop. Verify no pkt_done is fabricated — a reset is not a packet ending.

Parameter and counter corners

  • MAX_PL_DW = 1. Verify TOT_DW_W is wide enough for a 4 DW header plus 1 DW payload plus 1 DW digest, and that a two-DW payload is refused.
  • MAX_PL_DW at the model's 11-bit ceiling. Verify the elaboration check and that total_dw_o reaches the maximum without wrapping.
  • A total at the width boundary. Long header, maximum payload, digest present — the largest packet the model frames. Verify total_dw_o and total_bytes_o are both exact (P13, P14).
  • Zero payload with the digest present. Verify the region sequence skips the payload entirely (P5).
  • A non-power-of-two MAX_PL_DW, such as 33. Verify $clog2 sizes the counters correctly and the bound is enforced at exactly 33.

Coverage should include: both header forms; payload absent, one DW, and near the bound; digest present and absent; every legal region sequence; every error code asserted; out_ready continuously high, continuously low and randomly toggled; a stall at each region boundary; and reset in each region.

12. Debugging

Symptom: the receiver begins treating payload as header

The header region ended in the wrong place, and there are three ways to get there.

  1. The header form was decoded incorrectly. A 4 DW header parsed with the short count ends one DW early, so the header's fourth DW is classified as the first payload DW — and everything after is shifted by one. P4 catches it. The signature is that only 64-bit-addressing packets are affected, because those are the ones using the long form.
  2. The count is off by one in the other direction. A short header parsed with the long count consumes the payload's first DW as header, which is §12's second scenario below.
  3. The hint arrived late or unstable. The structural numbers must be valid with the first beat; a hint that settles a cycle later is sampled wrong. The assumption in §10 states the requirement, and violating it is a source-side bug.

The one observation that identifies it: compare the beat index where the region changed against the declared header form. Three or four, and nothing else is legal. If the change is at the wrong index, it is cause 1 or 2; if the index matches the previous packet's form, the count was latched and not refreshed.

Symptom: every 64-bit-address Request loses its first payload DW

This is the most specific symptom in the chapter and it names its own cause: the long-header boundary is being miscounted by one.

Why the specificity helps. Packets using the short header form are unaffected, which eliminates every general counting bug. The fault is exclusively in the path that selects between three and four — a decode that reads the form bit wrongly, a count that defaults to short, or an off-by-one that only manifests in the longer case.

What makes it survive testing. A bring-up environment using 32-bit addressing exercises only the short form, and the bug is invisible. It appears when the first 64-bit-addressed traffic arrives — often much later, and often in a different team's test.

The check: run a long-header packet and a short-header packet back to back and compare the region-change indices. They must differ by exactly one. §11's second and third scenarios are the tests that force this.

Symptom: a header-only packet leaves the parser waiting for payload that never arrives

Payload-presence interpretation. The parser believed a payload region followed the header when the shape said otherwise.

Three candidates:

  1. The payload-presence indication was misread. The parser entered the payload region for a packet that declared none — P5 catches it.
  2. The payload length was non-zero with the presence flag clear. A contradictory shape that should have been refused as ERR_BAD_HINT (P10). If it was not refused, the legality check is the fault.
  3. The region sequencing skipped the "no payload" branch. The header's end must lead directly to the digest, or to the end of the packet, when there is no payload. A sequencing case that assumed a payload always follows is the bug.

And the recovery behaviour is the tell. With in_eop governing packet closure (P11), the parser abandons the packet at the framing boundary and the next one parses cleanly. If the parser is genuinely stuck, in_eop is not being honoured — which is a more serious bug than the payload misinterpretation, because it means no packet can recover from any structural error.

Symptom: a packet with a digest causes the next packet to shift

Optional-tail accounting. The digest DW was not included in the packet's length, so the receiver treats it as the next packet's first DW.

Where it goes wrong: the digest-present indication was missed, so the parser declared the packet finished one DW early; or the digest region was entered but its single DW was not counted; or the total-length calculation omitted the digest term while the region sequencing included it.

Why it shifts everything after. The extra DW becomes the next packet's first header DW, so that packet is parsed from the wrong position — and its declared length then determines where the parser thinks the next one begins. One missed DW desynchronises the stream indefinitely, which is Chapter 11.1 §14's framing argument in its most concrete form.

What catches it before the field: P9. A packet whose structure ends one DW before the framing does produces ERR_LATE_EOP on the packet that caused it, not on the packets that follow. That is the difference between a one-packet investigation and a stream-wide one.

13. Common Misconceptions

  • "Every TLP header is the same length." Headers are 3 DW or 4 DW. The longer form exists because a 64-bit address does not fit in the shorter one.
  • "4 DW means four bytes." A DW is 32 bits — four bytes. A 4 DW header is 16 bytes. The unit is the thing people drop, and dropping it turns a 16-byte header into a 4-byte one.
  • "A payload always follows the header." Data is present only for packet types that carry it. A read Request is a header and nothing else.
  • "Packet length and header length are the same thing." They are three different quantities — header, payload, total (§6) — and none is derivable from another.
  • "A 64-bit address means a 64-byte header." It means one additional DW: 16 bytes rather than 12.
  • "Every packet class uses the same header layout." Different categories need different information, which is why the field-level chapter that follows is a separate chapter (Chapter 11.3).
  • "ECRC is part of every TLP." The digest region is optional and its presence is indicated in the header. Its contents and algorithm are outside this chapter entirely.
  • "The Data Link Layer's CRC and the TLP digest are the same mechanism." They are different mechanisms at different layers with different scopes — one protects a packet across a single Link, the other is an end-to-end integrity value carried inside the TLP.
  • "A TLP Prefix is part of the payload." Prefixes are a separate mechanism that extends packet metadata ahead of the header; they are neither payload nor part of the base structure this chapter describes (§2).
  • "The parser's states are PCIe protocol states." §9's R_HDR/R_PAY/R_DIG are a local model of which region a beat belongs to. PCIe defines no such state machine.

14. Understanding Check

15. What's Next

This chapter sized the regions Chapter 11.1 named: two header forms and why exactly two, a DW-granular payload, an optional single-DW tail, the three lengths that are not the same quantity, and the DW-versus-beat distinction that decides whether a wider datapath works.

None of it required decoding a single field, which was the point: structural correctness is a separate, verifiable job that has to be right before semantics matter at all.

Chapter 11.3 — Headers opens the header and decodes its fields — including the one this chapter has referred to four times without naming: the indication that selects the header form and announces whether data follows. Chapter 11.4 takes payloads and their size limits, 11.5 the routing information, 11.6 the attributes, and 11.7 the full packet-type taxonomy.

The idea to carry forward: find the end of the packet before you try to understand it — because a structural error costs you every packet that follows, and a semantic error costs you only one.