Skip to content

PCIe · Module 23

TLP Generation — One Packet, One Owner, Until EOP

A generator turns a normalized request into a packet. Its whole job is to guarantee that the header and the payload describe the same transaction — under stall, under a configuration write, and under a new request arriving mid-packet.

Chapter 23.3 emits request descriptors: write 128 bytes to this address, read 512 bytes from that one. Something must turn those into packets.

Module 11 built the other direction. 11.2 parses structure, 11.3 parses fields — both receive. This chapter transmits, and transmitting has a problem receiving does not: the packet does not exist yet, so nothing stops it from changing while you build it.

1. Sources, Scope, and the Direction Module 11 Did Not Build

2. A Serialization Boundary

Upstream, a request is a meaning: an address, a length, a direction, an identity. Downstream, it is bytes in an order.

The generator is where meaning becomes representation, and the reason that boundary is dangerous is that both sides move independently: upstream produces new requests whenever it likes, downstream accepts beats whenever it likes, and configuration can change at any moment.

So the generator is defined by what it freezes. At the instant it accepts a request it takes ownership of a set of values, and from that instant until EOP transfers, nothing upstream and nothing in the configuration space may alter them.

That single sentence generates the FSM (§8), the snapshot register (§4), the ownership lock (§3), and eighteen of §11's properties.

3. One Packet, One Owner

The generator owns at most one packet at a time (§10 uses a single-entry model; §10 also notes how to compose a queue in front of it without changing the contract).

Ownership begins on the accepted request and ends on the accepted EOP. Between those two events:

Must be trueBecause
req_ready is lowa second request would overwrite the first
header fields are stable§4
payload comes from this request's source§7
the packet cannot be abandoneda partial TLP on the link is malformed

The third row is the subtle one and it has a name: metadata skew. Suppose the header owner is locked to request A, but the payload multiplexer follows the current arbiter selection. Packet A's header now carries B's data — and every field in the header is individually correct, which is why this survives inspection.

Chapter 21.2 §11 met the same failure in a switch and solved it the same way: select at SOP, hold until EOP transfers. The arbiter there and the generator here are the same pattern in different subsystems, and 23.6 names it.

4. The Header Must Be a Snapshot

5. Length Is Encoded, and Someone Must Own the Conversion

Chapter 11.3 §5 owns what the Length field means and this chapter does not restate it. What belongs here is the architectural question: which block converts a byte count into that field, and what happens when the byte count cannot be converted?

Two defensible architectures, and this chapter picks the second:

ArchitectureConsequence
the generator rounds a byte count to whole DWit silently changes what the requester asked for
the generator requires a legal byte count and reports otherwisea malformed request is a named error, not a mutated packet

Rounding is the dangerous option, and it is dangerous in a specific way. A request for 6 bytes rounded up to 8 writes two bytes the requester did not ask to write. In a DMA path that is memory corruption outside the intended buffer — and it happens on exactly the unaligned tail that Chapter 22.4 §6's chunker works hard to produce correctly.

So §10 states the precondition explicitly — the byte count and the byte enables arriving at this boundary must already be legal — and emits err_unencodable rather than adjusting anything. P12 asserts that no packet is emitted when the precondition fails.

Where the precondition is established is the request former's job (23.3 §12 for DMA), and saying so is the point: one block establishes it, one block checks it, and neither silently repairs the other's output.

6. Byte Enables Are Owned Once

Chapter 11.3 §7 establishes why byte enables exist — DW granularity is not enough to express an arbitrary byte range.

The architectural rule this chapter adds is about duplication. If the request former already computed the first and last byte enables, the generator must snapshot them and not recompute. Two blocks computing the same value from the same inputs will eventually disagree — after a parameter change, a refactor, or an off-by-one fix applied to only one of them.

And the disagreement is invisible in normal operation, because both are usually right. §10 therefore treats byte enables as carried data, not derived data, and P8 asserts they are stable exactly like the address is.

If a design does place the computation here, the rule is unchanged: compute once, at accept, into packet-owned state — never combinationally from the live request.

7. The Payload Streamer

8. The Generator FSM

The TLP generator state machine. From IDLE, an accepted request moves to OWN, where every header field is snapshotted. OWN moves to SEND_HEADER. If the request cannot be classified or its length cannot be encoded, OWN moves to ERROR instead. SEND_HEADER moves to SEND_DATA when the header beat is accepted, or directly to RETIRE for a request with no payload. SEND_DATA remains until the final payload beat is accepted, then moves to RETIRE. RETIRE returns to IDLE. ERROR returns to IDLE after the error is reported, without emitting any packet.IDLEOWN - snapshot all fieldsOWN -snapshotall…SEND_HEADERSEND_DATA- beatsRETIRE -releaseownerERROR - no partial packetERROR -nopartial…request acceptedrequest acceptedencodableencodableunsupported or unencodableunsupported or unencodableunsupportedor…header accepted, has payloadheader accepted, has payloadheaderaccepted, ha…header accepted, no payloadheader accepted, no payloadheaderaccepted, no…final beat acceptedfinal beat acceptedowner releasedownerreleasederror reportederrorreported
Figure 1 — the generator's packet lifecycle. It accepts a request only from IDLE, snapshots every header field on entry to OWN, emits the header and then streams payload beats, and returns to IDLE only after the end-of-packet beat has been accepted. A request that cannot be encoded goes to ERROR without emitting a partial packet.

Four things to read out of the figure.

IDLE is the only state that accepts a request. That is §3's ownership rule expressed structurally rather than as a condition someone can forget.

OWN exists as a distinct state so the snapshot has a cycle to happen in. Everything downstream reads packet-owned registers, never the request interface.

SEND_HEADER has two exits because a Memory Read carries no payload (12.1) — and a generator that always visits SEND_DATA emits a zero-length payload phase, which is mutation 17.

And ERROR does not pass through the send states. An unencodable request produces no partial packet, because a truncated TLP on the link is worse than a rejected request (§5).

9. The Waveform

A new request arrives mid-packet and cannot touch it

10 cycles
Ten cycles of a TLP generator. At cycle 0 a request is accepted and header fields are snapshotted. Beats transfer at cycles 1 and 2. At cycle 3 transmit valid is high and transmit ready is low, so nothing moves, and although a new request is valid upstream the request ready signal is low. A beat transfers at cycle 4 and the final beat with last asserted transfers at cycle 5. The owner lock releases at cycle 6. The next request is accepted at cycle 7 and its beats transfer at cycles 8 and 9.stalled — new request not acceptedstalled — new request notacceptedlast beat transfers, owner releaseslast beat transfers, ownerreleasesonly now is the next request takenonly now is the nextrequest takenclkreq_validreq_readyowner_locktx_validtx_readytx_lastt0t1t2t3t4t5t6t7t8t9
Figure 2 — one packet generated with a stall in the middle, and a second request arriving while the first is still owned. The request is accepted at cycle 0 and every header field is snapshotted there. The interface stalls at cycle 3 while a new request is already pending upstream, and the new request is not accepted. The final beat transfers at cycle 5, the owner is released, and only then is the next request accepted.

Four things to read out of the figure.

Cycle 3 is the whole chapter. req_valid is high — upstream has the next request ready — and req_ready is low. A combinational header would already be showing that request's address while the current packet is mid-flight (§4).

owner_lock spans cycles 1 to 5, not 1 to 4. The owner releases after the accepted EOP, not after tx_last is merely asserted (mutation 19).

Nothing advances at cycle 3. No beat, no remaining decrement, no address step (§7).

And cycle 6 is idle by construction. The owner released, and the next request is accepted at cycle 7 — one packet at a time, exactly as §3 requires.

10. RTL — The Generator

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Normalized internal request and packet metadata.
// NOT a wire layout -- Chapter 11.3 §2 owns the field map. These are the
// values this block freezes at accept and guarantees until EOP.
package tlpgen_pkg;
 
  parameter int ADDR_W = 64;
  parameter int DATA_W = 256;                 // datapath width, bits
  parameter int BYTES  = DATA_W/8;
  parameter int KEEP_W = BYTES;
  parameter int LEN_W  = 13;                  // payload bytes in one packet
  parameter int OFF_W  = (LEN_W > 1) ? LEN_W : 2;
  parameter int TAG_W  = 8;
  parameter int RID_W  = 16;
 
  typedef enum logic [2:0] {
    REQ_MEM_RD = 3'd0,
    REQ_MEM_WR = 3'd1,
    REQ_OTHER  = 3'd2                          // deliberately unsupported (§10)
  } req_kind_e;
 
  typedef struct packed {
    req_kind_e         kind;
    logic [ADDR_W-1:0] addr;
    logic [LEN_W-1:0]  byte_count;
    logic [RID_W-1:0]  requester_id;
    logic [TAG_W-1:0]  tag;
    logic [2:0]        tc;
    logic [3:0]        first_be;               // CARRIED, not derived (§6)
    logic [3:0]        last_be;
  } req_t;
 
  // Guarded so BYTES = 1 is legal: the mask for n bytes, n in 1..BYTES.
  function automatic logic [KEEP_W-1:0] keep_mask(input int unsigned n);
    logic [KEEP_W-1:0] m;
    m = '0;
    for (int i = 0; i < KEEP_W; i++) if (i < n) m[i] = 1'b1;
    return m;
  endfunction
 
  function automatic bit carries_payload(input req_kind_e k);
    return (k == REQ_MEM_WR);
  endfunction
 
endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import tlpgen_pkg::*;
 
// SYNTHESIZABLE. Classification and the encodability precondition (§5).
// This block REPORTS rather than repairs: a byte count that cannot be
// encoded, or a kind this generator does not support, produces an error
// and NO packet -- never a silently rounded or defaulted one.
module tlp_classifier (
  input  req_t  req,
  input  logic  req_present,
 
  output logic  supported,
  output logic  encodable,
  output logic  needs_payload,
  output logic  err_unsupported,
  output logic  err_unencodable
);
  // Chapter 11.3 §5 owns what Length means. What is owned HERE is the
  // precondition: the byte count arriving at this boundary must already
  // be legal. Rounding 6 bytes up to 8 writes two bytes nobody asked for.
  localparam int unsigned MAX_PAYLOAD_BYTES = (1 << LEN_W) - 1;
 
  assign supported     = req_present && (req.kind inside {REQ_MEM_RD, REQ_MEM_WR});
  assign needs_payload = supported && carries_payload(req.kind);
  assign encodable     = supported
                      && (req.byte_count != '0)
                      && (req.byte_count <= LEN_W'(MAX_PAYLOAD_BYTES))
                      && ((req.byte_count[1:0] == 2'b00) || (req.first_be != 4'hF)
                                                          || (req.last_be != 4'hF));
  assign err_unsupported = req_present && !supported;
  assign err_unencodable = supported && !encodable;
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import tlpgen_pkg::*;
 
// SYNTHESIZABLE. THE FLAGSHIP BLOCK. The packet owner and header snapshot.
// §12 Model 6: 68.6% of packets experience a stall while a new request is
// already pending upstream. A combinational header takes the NEW values in
// every one of those cycles; this one cannot.
module tlp_packet_owner (
  input  logic clk,
  input  logic rst_n,
 
  input  logic  req_valid,
  output logic  req_ready,
  input  req_t  req,
 
  input  logic  hdr_accepted,          // the header beat transferred
  input  logic  eop_accepted,          // the final payload beat transferred
  input  logic  abort,                 // classifier rejected it
 
  output logic  owner_lock,
  output req_t  pkt,                   // THE SNAPSHOT -- immutable while owned
  output logic  pkt_has_payload,
  output logic  hdr_pending
);
  req_t pkt_q;
  logic own_q, hdr_q;
 
  assign pkt             = pkt_q;
  assign owner_lock      = own_q;
  assign hdr_pending     = hdr_q;
  assign pkt_has_payload = carries_payload(pkt_q.kind);
  // ONE packet at a time (§3). A second request cannot be accepted while
  // this one is owned, which is what makes the snapshot meaningful.
  assign req_ready       = !own_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin own_q <= 1'b0; hdr_q <= 1'b0; pkt_q <= '0; end
    else begin
      if (!own_q) begin
        if (req_valid) begin
          pkt_q <= req;                        // EVERY field, at once
          own_q <= 1'b1;
          hdr_q <= 1'b1;
        end
      end else begin
        if (hdr_accepted) hdr_q <= 1'b0;
        // Released on the ACCEPTED end-of-packet -- not on tx_last being
        // merely asserted (mutation 19), and not on abort mid-packet.
        if (eop_accepted || (abort && hdr_q)) begin
          own_q <= 1'b0; hdr_q <= 1'b0;
        end
      end
      // Nothing above can write pkt_q while own_q is high: the snapshot is
      // immutable by construction, and P3-P9 prove it.
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import tlpgen_pkg::*;
 
// SYNTHESIZABLE. Multi-beat payload streamer (§7).
// Two measured bugs live here: an all-ones mask on the final beat injected
// 15.5 phantom bytes per packet, and decrementing on `valid` terminated
// 79.1% of stalled packets early. Both are prevented structurally.
module tlp_payload_streamer (
  input  logic clk,
  input  logic rst_n,
 
  input  logic             start,          // header accepted, payload follows
  input  logic [LEN_W-1:0] total_bytes,    // from the SNAPSHOT, not the request
 
  output logic             dat_valid,
  input  logic             dat_ready,
  output logic [KEEP_W-1:0] dat_keep,
  output logic             dat_first,
  output logic             dat_last,
  output logic [OFF_W-1:0] dat_offset,     // byte offset into the payload
 
  output logic             busy,
  output logic             eop_accepted,
  output logic             err_underflow   // sticky
);
  logic [LEN_W-1:0] rem_q;
  logic [OFF_W-1:0] off_q;
  logic             busy_q, first_q, e_q;
  logic [LEN_W-1:0] this_beat;
 
  // The beat is min(remaining, BYTES) -- so the final beat is exactly the
  // tail, and the mask is built from that count rather than defaulted.
  always_comb this_beat = (rem_q > LEN_W'(BYTES)) ? LEN_W'(BYTES) : rem_q;
 
  assign busy         = busy_q;
  assign dat_valid    = busy_q && (rem_q != '0);
  assign dat_keep     = keep_mask(int'(this_beat));      // EXACT, never all-ones
  assign dat_first    = first_q;
  assign dat_last     = busy_q && (rem_q == this_beat);
  assign dat_offset   = off_q;
  assign eop_accepted = dat_valid && dat_ready && dat_last;
  assign err_underflow = e_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      rem_q <= '0; off_q <= '0; busy_q <= 1'b0; first_q <= 1'b0; e_q <= 1'b0;
    end else begin
      if (!busy_q) begin
        if (start && (total_bytes != '0)) begin
          rem_q <= total_bytes; off_q <= '0; busy_q <= 1'b1; first_q <= 1'b1;
        end
      end else if (dat_valid && dat_ready) begin
        // ON THE HANDSHAKE ONLY. Decrementing on `valid` ended 79.1% of
        // stalled packets early (§12 Model 7).
        rem_q   <= rem_q - this_beat;
        off_q   <= off_q + OFF_W'(this_beat);
        first_q <= 1'b0;
        if (rem_q == this_beat) busy_q <= 1'b0;
      end
      if (busy_q && (rem_q == '0)) e_q <= 1'b1;    // cannot happen: prove it
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import tlpgen_pkg::*;
 
// SYNTHESIZABLE. The FSM of Figure 1. It sequences the packet's phases and
// owns nothing else -- the snapshot lives in the owner, the counters live
// in the streamer. Same separation as Chapter 23.3 §3, one level down.
module tlp_gen_fsm (
  input  logic clk,
  input  logic rst_n,
 
  input  logic req_valid,
  input  logic supported,
  input  logic encodable,
  input  logic needs_payload,
  input  logic hdr_accepted,
  input  logic payload_eop_accepted,
 
  output logic hdr_valid,
  output logic payload_start,
  output logic abort,
  output logic err_reported,
  output logic [2:0] state
);
  typedef enum logic [2:0] {
    S_IDLE=3'd0, S_OWN=3'd1, S_HDR=3'd2, S_DATA=3'd3, S_RETIRE=3'd4, S_ERR=3'd5
  } st_e;
  st_e st_q;
 
  assign state         = st_q;
  assign hdr_valid     = (st_q == S_HDR);
  assign payload_start = (st_q == S_HDR) && hdr_accepted && needs_payload;
  assign abort         = (st_q == S_ERR);
  assign err_reported  = (st_q == S_ERR);
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) st_q <= S_IDLE;
    else unique case (st_q)
      S_IDLE:   if (req_valid) st_q <= S_OWN;
      // ERROR never passes through the send states: no partial packet (§5).
      S_OWN:    st_q <= (supported && encodable) ? S_HDR : S_ERR;
      S_HDR:    if (hdr_accepted) st_q <= needs_payload ? S_DATA : S_RETIRE;
      S_DATA:   if (payload_eop_accepted) st_q <= S_RETIRE;
      S_RETIRE: st_q <= S_IDLE;
      S_ERR:    st_q <= S_IDLE;
      default:  st_q <= S_IDLE;
    endcase
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import tlpgen_pkg::*;
 
// VERIFICATION-ONLY. Byte-for-byte payload oracle. It reconstructs what
// left the interface and compares against what the request asked for --
// deliberately from the ACCEPTED beats only, so a design that emits extra
// bytes on a stalled beat cannot hide.
module tlp_payload_oracle (
  input logic clk, rst_n,
  input logic              start,
  input logic [LEN_W-1:0]  total_bytes,
  input logic              dat_valid, dat_ready, dat_last,
  input logic [KEEP_W-1:0] dat_keep,
 
  output logic [LEN_W+4:0] bytes_emitted,
  output logic             err_byte_count,
  output logic             err_extra_beat,
  output logic             err_last_count
);
  logic [LEN_W+4:0] acc_q, exp_q;
  logic [7:0]       lastcnt_q;
  logic             active_q;
 
  assign bytes_emitted = acc_q;
 
  always_comb begin
    err_byte_count = !active_q && (exp_q != '0) && (acc_q != exp_q);
    err_extra_beat = active_q && (acc_q > exp_q);
    err_last_count = (lastcnt_q > 8'd1);
  end
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin acc_q<='0; exp_q<='0; lastcnt_q<='0; active_q<=1'b0; end
    else begin
      if (start) begin acc_q<='0; exp_q<={4'd0,total_bytes}; lastcnt_q<='0; active_q<=1'b1; end
      if (dat_valid && dat_ready) begin
        acc_q <= acc_q + ($countones(dat_keep));      // ACCEPTED beats only
        if (dat_last) begin lastcnt_q <= lastcnt_q + 8'd1; active_q <= 1'b0; end
      end
    end
  end
endmodule

Classification: five synthesizable, one verification-only.

Failure — seven. A combinational header (68.6% exposed). A payload multiplexer following the arbiter while the header is locked. Decrementing on valid (79.1% short packets). An all-ones final mask (15.5 phantom bytes/packet). Releasing the owner on tx_last asserted rather than accepted. Rounding an unencodable byte count instead of reporting. And defaulting an unsupported kind to a write.

11. Same-Cycle Audit and Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ==================================================================
// OWNERSHIP (§3) -- one packet, one owner, until accepted EOP.
// ==================================================================
 
// P1: the packet is captured exactly once, on the accepted request.
property p_captured_once;
  @(posedge clk) disable iff (!rst_n)
    (pkt != $past(pkt)) |-> $past(req_valid && req_ready);
endproperty
 
// P2: no second request is accepted while a packet is owned.
property p_no_accept_while_owned;
  @(posedge clk) disable iff (!rst_n)
    owner_lock |-> !req_ready;
endproperty
 
// ==================================================================
// HEADER IMMUTABILITY (§4) -- §12 Model 6: 68.6% of packets exposed.
// ==================================================================
 
// P3: the address is stable for the life of the packet.
property p_addr_stable;
  @(posedge clk) disable iff (!rst_n)
    (owner_lock && $past(owner_lock)) |-> $stable(pkt.addr);
endproperty
 
// P4: the kind is stable -- a packet does not change type mid-flight.
property p_kind_stable;
  @(posedge clk) disable iff (!rst_n)
    (owner_lock && $past(owner_lock)) |-> $stable(pkt.kind);
endproperty
 
// P5: the byte count is stable.
property p_len_stable;
  @(posedge clk) disable iff (!rst_n)
    (owner_lock && $past(owner_lock)) |-> $stable(pkt.byte_count);
endproperty
 
// P6: the Requester ID is stable -- it cannot come from the next request.
property p_rid_stable;
  @(posedge clk) disable iff (!rst_n)
    (owner_lock && $past(owner_lock)) |-> $stable(pkt.requester_id);
endproperty
 
// P7: the Tag is stable. A Tag changing mid-packet breaks correlation for
// the whole transaction (Chapter 11.3 §6).
property p_tag_stable;
  @(posedge clk) disable iff (!rst_n)
    (owner_lock && $past(owner_lock)) |-> $stable(pkt.tag);
endproperty
 
// P8: byte enables are stable -- they are CARRIED, not re-derived (§6).
property p_be_stable;
  @(posedge clk) disable iff (!rst_n)
    (owner_lock && $past(owner_lock)) |-> ($stable(pkt.first_be) && $stable(pkt.last_be));
endproperty
 
// P9: traffic class is stable.
property p_tc_stable;
  @(posedge clk) disable iff (!rst_n)
    (owner_lock && $past(owner_lock)) |-> $stable(pkt.tc);
endproperty
 
// ==================================================================
// CLASSIFICATION (§5) -- report, never repair.
// ==================================================================
 
// P10: a supported kind is one of the bounded set; anything else errors.
property p_unsupported_reported;
  @(posedge clk) disable iff (!rst_n)
    (req_present && !(req.kind inside {REQ_MEM_RD, REQ_MEM_WR})) |-> err_unsupported;
endproperty
 
// P11: an unencodable byte count is REPORTED, never rounded (§5).
property p_unencodable_reported;
  @(posedge clk) disable iff (!rst_n)
    (supported && !encodable) |-> err_unencodable;
endproperty
 
// P12: an errored request emits NO packet at all -- no partial TLP.
property p_no_partial_packet_on_error;
  @(posedge clk) disable iff (!rst_n)
    (state == 3'd5) |-> (!hdr_valid && !dat_valid);
endproperty
 
// P13: the error state never passes through the send states.
property p_error_bypasses_send;
  @(posedge clk) disable iff (!rst_n)
    ($past(state == 3'd1) && (state == 3'd5)) |-> !$past(hdr_valid);
endproperty
 
// ==================================================================
// PAYLOAD STREAMING (§7).
// ==================================================================
 
// P14: a stalled beat changes nothing.
property p_beat_stable_under_stall;
  @(posedge clk) disable iff (!rst_n)
    (dat_valid && !dat_ready) |=>
      (dat_valid && $stable(dat_keep) && $stable(dat_offset) && $stable(dat_last));
endproperty
 
// P15: remaining decrements ONLY on the handshake. §12 Model 7: on `valid`
// terminated 79.1% of stalled packets early.
property p_offset_on_transfer_only;
  @(posedge clk) disable iff (!rst_n)
    (dat_offset != $past(dat_offset)) |-> $past(dat_valid && dat_ready);
endproperty
 
// P16: the byte-valid mask is EXACT -- never all-ones on a partial beat.
// §12 Model 5: an all-ones final mask injected 15.5 phantom bytes/packet.
property p_keep_exact;
  @(posedge clk) disable iff (!rst_n)
    dat_valid |-> ($countones(dat_keep) == ((rem_q > LEN_W'(BYTES)) ? BYTES : int'(rem_q)));
endproperty
 
// P17: `last` asserts exactly once per packet.
property p_last_once;
  @(posedge clk) disable iff (!rst_n)
    !err_last_count;
endproperty
 
// P18: no beat is emitted after the final one.
property p_no_beat_after_last;
  @(posedge clk) disable iff (!rst_n)
    (dat_valid && dat_ready && dat_last) |=> !dat_valid;
endproperty
 
// P19: the emitted byte count equals the requested byte count, exactly.
property p_byte_conservation;
  @(posedge clk) disable iff (!rst_n)
    !err_byte_count && !err_extra_beat;
endproperty
 
// P20: the owner releases only on the ACCEPTED end of packet.
property p_release_on_accepted_eop;
  @(posedge clk) disable iff (!rst_n)
    $fell(owner_lock) |-> $past(eop_accepted || abort);
endproperty
 
// P21: a read carries no payload phase at all (Chapter 12.1).
property p_read_has_no_payload;
  @(posedge clk) disable iff (!rst_n)
    (owner_lock && (pkt.kind == REQ_MEM_RD)) |-> !dat_valid;
endproperty
 
// ==================================================================
// RESET AND PARAMETERS.
// ==================================================================
 
// P22: reset clears ownership and drives nothing.
property p_reset_clears_owner;
  @(posedge clk)
    (!rst_n) |=> (!owner_lock && !dat_valid && !hdr_valid);
endproperty
 
// P23: a zero byte count never starts a payload phase (it is unencodable).
property p_zero_length_no_payload;
  @(posedge clk) disable iff (!rst_n)
    (start && (total_bytes == '0)) |=> !dat_valid;
endproperty
 
// P24: the streamer never claims to be busy with nothing remaining.
property p_no_busy_with_zero_remaining;
  @(posedge clk) disable iff (!rst_n)
    !err_underflow;
endproperty
 
// P25: the oracle never drives the datapath.
property p_oracle_non_functional;
  @(posedge clk) disable iff (!rst_n)
    $stable({dat_valid, dat_keep}) or !$stable(bytes_emitted);
endproperty

Twenty-five properties. P3–P9 are §4 field by field — written separately rather than as one $stable(pkt) so a failure names the field that moved. P14–P19 are the streamer, and P16 and P19 together are the two measured bugs of §7. P20 is the one-line difference between releasing on tx_last and releasing on an accepted EOP.

12. Measured Behaviour

13. Verification — DV and Mutations

DV, against the byte-for-byte oracle of §10 — which reconstructs only from accepted beats: a single-beat packet · a payload exactly one beat wide · one byte over a beat · one byte under · a read (no payload phase) · a stall on the header beat · a stall on the first payload beat · a stall on the last beat · continuous backpressure · a new request offered throughout · a configuration write mid-packet · an unsupported kind · a zero byte count · an unencodable byte count · BYTES = 1 · back-to-back packets · reset mid-packet.

#MutationSymptomCaught by
1Derive the header combinationally from the live request68.6% exposure; wrong address on a stalled packet (§12)P3–P9
2Let the payload mux follow the arbiter while the header is lockedpacket A's header with B's data (§3)P19
3Take the Requester ID from the next requestcorrelation broken for the whole transactionP6
4Change the Tag mid-packetthe Completion cannot be matched (23.5)P7
5Use stale byte enables from the previous requestwrong bytes written at the edgesP8
6Round an unencodable byte count up to a DWwrites bytes nobody asked for (§5)P11
7Round down insteadthe tail of the transfer is silently droppedP11, P19
8Default an unsupported kind to Memory Writean arbitrary request becomes a writeP10
9Emit the header, then errora partial TLP on the linkP12, P13
10Decrement remaining on valid79.1% of stalled packets short (§12)P15, P19
11Advance the offset under stallpayload beats skip bytesP14, P15
12Drive all-ones keep on the final beat15.5 phantom bytes per packet (§12)P16
13Assert last one beat earlythe packet is short and malformedP17, P19
14Assert last one beat latean extra beat after the payloadP18
15Assert last on every beatevery beat looks like a packetP17
16Emit a beat after the accepted final beatduplicated tail dataP18
17Always visit the payload phase, even for a reada zero-length payload phase on every readP21
18Accept a new request before EOPthe snapshot is overwritten mid-packetP2
19Release the owner on tx_last asserted, not acceptedthe next request overwrites an unsent beatP20
20Read MPS or Requester ID live from configurationthe same bug as mutation 1 by another door (§4)P3–P9
21Loop forever on a zero byte countthe generator hangs on a degenerate requestP23, P24
22Emit stale data when the payload source underflowsgarbage on the wire with a valid headerP24
23Increment a packet counter every stall cyclethroughput diagnostics inflated (22.1 §7)P25
24Assume BYTES > 1 in the mask functionzero-width vector at a one-byte datapathP16
25Index the keep mask without bounding ittruncation at the final beatP16
26Keep the header owner and payload owner as separate flagsthey drift; a packet with two owners (§3)P2, P19
27Duplicate the packet after a downstream stallthe same TLP appears twiceP18, P19
28Drop the final payload wordthe last DW never leavesP19

Two counterexamples worth stating explicitly.

Mutation 1 is the chapter's whole subject and it is genuinely tempting. A combinational header is fewer registers, one less cycle of latency, and obviously correct in a diagram. It is correct in silicon exactly when nothing stalls — and §12 Model 6 measured 68.6% of packets encountering a stall with the next request already pending. The corrupted packet is well-formed: correct Fmt, correct Length, correct LCRC downstream. It simply targets the wrong address, so the failure surfaces as memory corruption at a plausible-looking location, far from the generator.

Mutation 12 is the one that survives the strongest test suite. All-ones keep is correct for every beat except the last, and the last beat is only partial when the byte count is not a multiple of the datapath width. A test suite built on aligned, round-numbered transfers never produces a partial beat at all. §12 measured the cost at 15.5 phantom bytes per packet — appended to a payload that is otherwise byte-perfect, which is why §14's symptom is "only the final few bytes are wrong."

14. Debugging

Symptom — the analyzer shows a correct address and length, and the payload is wrong. Header/payload skew (§3, mutation 2). The header was locked to one request and the payload multiplexer followed another. Check whether the payload source is selected by the packet owner or by the arbiter — if the arbiter can change selection while owner_lock is high, this is it.

Symptom — only the final 1–7 bytes of each packet are corrupt. The final-beat mask (§7, mutation 12). All-ones keep on a partial beat appends whatever the datapath happened to hold. Reproduce with a transfer whose length is not a multiple of the bus width — a round-numbered test suite cannot see this.

Symptom — packets are malformed only under backpressure. Two candidates, distinguishable by whether the packet is short or long. Short means remaining decremented on valid (mutation 10, 79.1%). Long means an extra beat after last (mutation 14). Byte conservation over one packet separates them immediately, which is what §10's oracle counts.

Symptom — the Tag or Requester ID changes mid-packet. A live header field (§4, mutations 3, 4). Instantiate Chapter 23.1 §9's contract monitor on the header interface; err_payload_changed_under_stall fires on the first stalled packet with a pending request.

Symptom — failures begin after the driver reconfigures the device. A configuration value read live into the packet path (mutation 20). This is Chapter 22.4 §8's result in the generator — and the tell is that the failure correlates with configuration writes rather than with traffic.

Symptom — one TLP appears twice on the link. The owner released before the beat was accepted (mutation 19) or a beat was emitted after last (mutation 16). Check what $fell(owner_lock) is qualified by — P20 requires an accepted EOP, not an asserted tx_last.

Symptom — single-beat requests work, multi-beat ones do not. The streamer is never exercised by single-beat traffic: first and last coincide, no offset advances, no mask is partial. Every bug in §7 is invisible at one beat, which is why the DV list starts with "a payload exactly one beat wide" and then immediately adds "one byte over".

15. Misconceptions

"The header can be combinational; it is just wires." 68.6% of packets meet the window where that is wrong (§4).

"Nothing changes mid-packet in practice." Upstream requests and configuration writes both do, and neither is coordinated with your packet (§4).

"If the Length field is right, the packet is right." Length, address and Fmt can all be correct while the payload belongs to a different request (§3).

"Rounding a byte count up is harmless." It writes bytes the requester did not ask to write (§5).

"Byte enables can be recomputed here; it is the same formula." Two blocks computing the same value eventually disagree, and the disagreement is invisible (§6).

"keep can be all ones; the length field says how much is real." The final beat is partial and the extra bytes are on the wire (§7, 15.5 per packet).

"Decrementing on valid is fine; valid implies it will be taken." It implies no such thing — 79.1% of stalled packets ended early (§7).

"Release the owner when last is asserted." Release it when last is accepted (mutation 19).

"An unsupported request should default to something sensible." There is no sensible default; report it (§5, P10).

"A read still needs a payload phase of zero beats." It needs none at all (12.1, P21).

"Single-beat tests cover the streamer." They exercise none of it (§14).

"A malformed-TLP report means the receiver has a bug." It frequently means the generator sent a short or long packet (§14).

16. Understanding Check

Q1. Your generator derives the header combinationally and passes every test. What test is missing? A stall coinciding with a pending next request. Without backpressure the current request is the packet being sent, so the two designs are indistinguishable. §12 Model 6 measured 68.6% of packets meeting that window under load. Add a downstream that stalls and an upstream that always has a request ready, and the failure is immediate.

Q2. A 100-byte payload on a 32-byte datapath. How many beats, and what is the last one's byte-valid mask? Four beats: 32, 32, 32, 4. The final mask enables 4 bytes, not 32 (§7). Driving all-ones there appends 28 bytes of whatever the datapath held — and §12 measured a mean of 15.5 such bytes per packet across mixed sizes. P16 asserts the mask's population count equals the beat's byte count.

Q3. Why does the FSM have a separate OWN state instead of snapshotting in IDLE? Because the snapshot must be registered and settled before anything downstream reads it. OWN gives the capture a cycle of its own, so SEND_HEADER reads packet-owned state rather than a value that is still being latched. It also makes P1 expressible: the packet changed exactly when a request was accepted, and at no other time.

Q4. The request former already computed the byte enables. Should the generator recompute them? No — carry them (§6). Two blocks deriving the same value from the same inputs will drift after any parameter change or one-sided fix, and the drift is invisible because both are usually right. Carrying makes the value single-sourced, and P8 asserts it is stable exactly like the address.

Q5. A request arrives with a byte count your Length encoding cannot represent. Name the two wrong answers and the right one. Wrong: round up — writes bytes nobody asked for, which in a DMA path is corruption outside the buffer. Wrong: round down — silently drops the tail. Right: report err_unencodable and emit no packet (§5, P11, P12). The precondition belongs to the request former; the generator checks it and refuses, and neither block repairs the other's output.

Q6. Your generator releases the packet owner when tx_last is asserted. What breaks, and when? The next request is accepted while the final beat is still unaccepted (mutation 19). Under backpressure tx_last can be asserted for many cycles before tx_ready arrives; during those cycles the snapshot is overwritten, so the final beat is emitted with the next packet's metadata. P20 requires the release to be qualified by the accepted EOP, which is the same distinction §7 makes for the byte counters.

17. What's Next

The transmit path is now complete from descriptor to packet. 23.3 decides what to send, this chapter builds it, and the interface contract from 23.1 §3 carries it downward.

Chapter 23.5 Completion Logic takes the other half: the reads this generator sends will be answered, and something must remember every one of them. It owns the context table, the matcher, split accumulation by byte coverage, and the race this chapter's Tag stability (P7) only makes possible to solve — a Tag that changed mid-packet cannot be matched at all.

And 23.6 will name what just happened three times. The immutable snapshot (§4), select-at-SOP-hold-to-EOP (§3, and 21.2 §11 before it), and advance only on transfer (§7, and everywhere else in Modules 22 and 23). Each is now a measured result rather than an opinion, which is what makes them worth cataloguing.