Skip to content

PCIe · Module 12

Memory Write — Posted, and Therefore About Ownership

A posted Memory Write carries its address and payload forward and gets no Completion back. That removes correlation state and replaces it with a harder problem: knowing exactly when each buffer may release the data it holds, and updating the target exactly once.

Chapter 12.1 traced a read and found that almost all of the engineering was in the waiting — the state that survives the round trip, the identifier that must not be reused, the buffer that holds an answer until someone takes it.

A Memory Write has no round trip. Take away the Completion and most of that machinery disappears.

What replaces it is a different question, and it is not an easier one.

What happens from the moment a local agent produces a PCIe memory write until the payload reaches and updates the target resource, given that no normal Completion returns?

1. Not "Fire and Forget"

That phrase gets used for posted writes and it is worth refusing at the outset, because it is wrong in the direction that matters.

"Forget" implies there is nothing to manage. There is a great deal to manage — it is simply forward-path management rather than round-trip management.

What a posted write does not needWhat it does need
a correlation identifiera buffer that holds address and payload together
an outstanding-transaction entrya defined release point at every stage
a Completion to correlatea target update that happens exactly once
a result bufferbackpressure that reaches the local producer
a timeoutpayload stability under every stall

The accurate statement is: the Requester's obligation ends earlier, not that it never existed. It ends when ownership of the address and payload transfers downstream — and knowing when that is, precisely, is §3's entire subject.

2. The Verified Semantics

3. The Lifetime, Stage by Stage

This is the chapter's central contribution, and the only column that really matters is the last one.

StageOwns addressOwns payloadCan stallMay release when
local produceryesyeswaits on readythe write buffer accepts it
posted write bufferyesyeson fullthe TX path accepts the descriptor
outbound TX pathyesyeson credits, arbitrationthe packet is committed to the link
fabricon congestionout of scope for this chapter
Endpoint receiveyesyeson internal backpressurethe resource front end accepts it
resource front endoffsetyeson resource busythe resource handshake completes
target resourceappliedthe update has happened

4. Address and Payload Travel Together

A read Request is metadata only. A write Request is metadata and data, and that changes what a buffer must be.

5. MPS Is the Write's Constraint

Chapter 11.4 established the rule and Chapter 12.1 §8 established the contrast. This is where it becomes concrete, because a Memory Write is the packet MPS was written for.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
payload_bytes(Memory Write TLP) ≤ operative MPS

Both facts about that inequality matter.

The operative MPS is a run-time value. Software wrote it into Device Control after enumeration (Chapter 11.4 §5). A design that hard-codes it at synthesis has decided the answer before the system that determines it has booted, and on any link configured lower, every packet it emits violates the transmit obligation.

And MRRS is not involved. A device may be configured with MRRS 4096 and MPS 128. That combination says nothing about writes — it says the device may ask for 4 KB in one read, and may carry 128 bytes in one payload. §10's fragmenter takes MPS and only MPS.

What happens to a larger local write. It becomes several Memory Write TLPs, each within MPS. §10 models the arithmetic; the alignment and boundary rules that further constrain where a transfer may legally be divided are Module 12's continuing subject and are not published here, so §10 divides on the MPS boundary only and says so.

6. A Worked Write

Software writes four bytes — 0xDEADBEEF — to 0x8000_0120. The device's BAR base is 0x8000_0000.

StepWhat happens
1the store targets 0x8000_0120; the Requester recognises PCIe space
2a Memory Write Request is built: address 0x8000_0120, Length 1 DW, payload 0xDEADBEEF, byte validity for all four bytes
3the fabric routes by address — each Switch port compares against its Base/Limit windows (Chapter 11.5 §4)
4the Endpoint receives it; BAR decode matches (Chapter 9.6)
5internal offset = 0x8000_0120 − 0x8000_0000 = 0x120
6the resource front end offers offset 0x120, data 0xDEADBEEF, all bytes valid
7the resource accepts; the register changes
8nothing returns

Step 8 is the chapter. The producer at step 1 learned that its write was accepted by a local buffer. It never learns that step 7 happened.

On byte validity. §11's model carries an internal byte_mask, one bit per byte of the payload. That is an implementation abstraction, not a wire field — the packet expresses byte granularity through the First and Last DW Byte Enable fields (Chapter 11.3 §7), whose legality rules are Module 12's and are not modelled here. The two are related but they are not the same object, and §11 labels its abstraction accordingly.

7. The Lifecycle

A local producer offers a write. The requester transaction layer's posted write buffer takes ownership of the address and payload together, then launches a Memory Write Request carrying both. The fabric routes it by address to the endpoint transaction layer, which performs a BAR decode to obtain an internal offset and hands the data to the resource front end. The resource accepts the write and the register updates. No completion returns to the requester.Accept, own, launch, route, decode, apply — and nothing returnsLocal producerRequester TLFabricEndpoint TLEndpointresourcewrite 0x8000_0120 =0xDEADBEEFbuffer owns addressAND payloadproducer may reuseits data busMemory Write Request- payload includedrouted by addressBAR hit - offset0x120, byte validityregister updated -exactly onceinternal handshakeonly
Figure 1 — a posted Memory Write. Address and payload travel forward together; each arrow is an ownership transfer, and after the last one nothing comes back. Compare Chapter 12.1's figure, where half the diagram is the return path.

8. RTL — Posted Write Buffer

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Take ownership of a posted Memory Write — address and
// payload together — and hold it until the outbound path accepts it.
// Posted semantics (payload travels with the Request, no Completion
// returns): NORMATIVE. The descriptor layout, the byte_mask abstraction and
// the interface: ILLUSTRATIVE implementation.
package post_wr_pkg;
 
  // NORMALIZED INTERNAL DESCRIPTOR — not a wire-format TLP.
  // byte_mask is an INTERNAL byte-validity abstraction. It is NOT the
  // wire First/Last DW Byte Enable fields, whose legality rules are
  // Module 12's and are not modelled here (section 6).
  typedef struct packed {
    logic [63:0]  addr;
    logic [10:0]  len_dw;      // represented DW count, 1..1024
    logic [2:0]   tc;
    logic         relaxed_ordering;
    logic         no_snoop;
    logic         id_ordering;
  } wr_meta_t;
 
endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import post_wr_pkg::*;
 
module posted_write_buffer #(
  parameter int DEPTH  = 4,
  parameter int DATA_W = 128            // payload bits held per entry
) (
  input  logic clk,
  input  logic rst_n,
 
  // ---- From the local producer — ONE atomic handshake ------------------
  // Metadata and payload transfer TOGETHER. Splitting them is section 4's
  // corruption class, and this interface makes it unrepresentable.
  input  logic                 in_valid,
  output logic                 in_ready,
  input  wr_meta_t             in_meta,
  input  logic [DATA_W-1:0]    in_data,
  input  logic [DATA_W/8-1:0]  in_byte_mask,
 
  // ---- To the outbound TX path -----------------------------------------
  output logic                 out_valid,
  input  logic                 out_ready,
  output wr_meta_t             out_meta,
  output logic [DATA_W-1:0]    out_data,
  output logic [DATA_W/8-1:0]  out_byte_mask
);
 
  generate
    if (DEPTH < 1)   $error("DEPTH must be at least 1");
    if (DATA_W < 32) $error("DATA_W must hold at least one DW");
  endgenerate
 
  // Width-safe at the stated minimum: $clog2(1) is 0 and a zero-width index
  // is illegal. The count is separate from the pointers so full and empty
  // are distinguishable at DEPTH == 1, where the pointers are always equal.
  localparam int IDX_W = (DEPTH <= 1) ? 1 : $clog2(DEPTH);
  localparam int CNT_W = $clog2(DEPTH + 1);
 
  typedef struct packed {
    wr_meta_t            meta;
    logic [DATA_W-1:0]   data;
    logic [DATA_W/8-1:0] bmask;
  } 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 out_valid = !empty;
 
  wire pop = out_valid && out_ready;
 
  // EFFECTIVE capacity, not instantaneous capacity: a slot being vacated
  // this cycle is usable this cycle. Gating in_ready on `!full` alone would
  // stall the producer for one cycle every time the buffer ran at capacity
  // — correct, but needlessly throttling at exactly full rate.
  assign in_ready = !full || pop;
 
  wire push = in_valid && in_ready;
 
  // Outputs come from STORED state only. A bypass path from in_* to out_*
  // would mean the producer's live bus drives an offer already in progress,
  // which is section 4's split-ownership bug arriving by another route.
  assign out_meta      = mem_q[rd_q].meta;
  assign out_data      = mem_q[rd_q].data;
  assign out_byte_mask = mem_q[rd_q].bmask;
 
  // Explicit wrap: correct at every DEPTH, including non-powers of two.
  // Natural rollover wraps at 2^IDX_W, which only equals DEPTH 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
        // Address, payload and byte validity are written in ONE event.
        // There is no state in which an entry holds one without the others.
        mem_q[wr_q].meta  <= in_meta;
        mem_q[wr_q].data  <= in_data;
        mem_q[wr_q].bmask <= in_byte_mask;
        wr_q <= next_idx(wr_q);
      end
      if (pop) rd_q <= next_idx(rd_q);
 
      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 (package: compile-time).

Architecture. A queue whose entry is a whole transaction. There is deliberately no separate metadata path and no separate data path.

State. Entries, pointers, occupancy. No correlation state of any kind — that is the posted difference, stated structurally: there is nowhere in this module to put an outstanding-transaction entry, so a design cannot accidentally allocate one.

Cycle behaviour.

Occupancypopin_validResult
below DEPTH01accepted, occupancy +1
at DEPTH11accepted — occupancy unchanged
at DEPTH01refused, in_ready low
any10occupancy −1

Contract. The producer relies on in_ready reflecting real capacity, and must hold in_meta, in_data and in_byte_mask stable while in_valid is asserted. Downstream relies on the offered entry being byte-for-byte what was accepted, and stable for as long as out_ready is low.

Failure — four. A bypass path from the input to the output re-couples the producer's live bus to an in-flight offer (§4). Gating in_ready on !full alone throttles at exactly the full-rate steady state. Natural pointer rollover aliases entries at any non-power-of-two DEPTH. And incrementing cnt_q on in_valid rather than on push overflows under backpressure, which corrupts entries the producer believes were accepted.

Deliberately simplified: one payload beat per entry rather than a multi-beat payload stream — the beat arithmetic is Chapter 11.4 §9's and is not duplicated; no ordering interaction; no fragmentation (§10 is separate); no flow-control credit interaction.

Production implication: a real posted path interacts with flow control, may hold payloads in a separate data RAM keyed by entry index, and must honour the ordering rules. What does not change is that address and payload must remain provably associated.

9. RTL — MPS Fragmenter

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Divide a larger local write operation into packet-sized
// fragments bounded by the RUN-TIME operative Max_Payload_Size.
// The MPS transmit obligation: NORMATIVE (section 2).
// The fragment descriptor and the division policy: ILLUSTRATIVE.
// ALIGNMENT AND BOUNDARY RULES ARE NOT MODELLED — this divides on the MPS
// boundary only. A production fragmenter must also honour the alignment and
// boundary rules that Module 12 continues to develop.
module posted_write_fragmenter #(
  parameter int ADDR_W = 64,
  parameter int TOT_W  = 20            // total DW in the local operation
) (
  input  logic clk,
  input  logic rst_n,
 
  // Operative limit in BYTES, decoded from Device Control bits 7:5. An
  // input, not a parameter: software sets it after elaboration (section 5).
  input  logic [12:0]        mps_bytes,
  input  logic               mps_valid,
 
  input  logic               op_valid,
  output logic               op_ready,
  input  logic [ADDR_W-1:0]  op_addr,
  input  logic [TOT_W-1:0]   op_len_dw,
 
  output logic               frag_valid,
  input  logic               frag_ready,
  output logic [ADDR_W-1:0]  frag_addr,
  output logic [10:0]        frag_len_dw,
  output logic               frag_first,
  output logic               frag_last,
 
  // The operative limit was not available when an operation was offered.
  output logic               no_limit_error
);
 
  wire [10:0] mps_dw   = mps_bytes[12:2];       // four bytes per DW
  wire        limit_ok = mps_valid && (mps_dw != 11'd0);
 
  logic [TOT_W-1:0]  rem_q;
  logic [ADDR_W-1:0] addr_q;
  logic              active_q, first_q, err_q;
 
  wire [TOT_W-1:0] limit_ext = TOT_W'(mps_dw);
  // Same min() shape as Chapter 11.4's segmenter, for the same reason: the
  // final short fragment falls out of the general expression, so there is
  // no separate last-fragment arm to get wrong.
  wire [TOT_W-1:0] this_frag = (rem_q >= limit_ext) ? limit_ext : rem_q;
 
  assign op_ready       = !active_q && limit_ok;
  assign frag_valid     = active_q;
  assign frag_addr      = addr_q;
  assign frag_len_dw    = 11'(this_frag);
  assign frag_first     = active_q && first_q;
  assign frag_last      = active_q && (rem_q <= limit_ext);
  assign no_limit_error = err_q;
 
  wire accept = op_valid && op_ready && (op_len_dw != '0);
  wire fire   = frag_valid && frag_ready;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      rem_q <= '0; addr_q <= '0;
      active_q <= 1'b0; first_q <= 1'b0; err_q <= 1'b0;
    end else begin
      // Refuse rather than guess a limit. A default would emit packets sized
      // by assumption, and on a link configured lower every one of them
      // violates the transmit obligation (Chapter 11.4 section 10).
      if (op_valid && !limit_ok) err_q <= 1'b1;
 
      if (accept) begin
        rem_q    <= op_len_dw;
        addr_q   <= op_addr;
        active_q <= 1'b1;
        first_q  <= 1'b1;
      end else if (fire) begin
        first_q <= 1'b0;
        // Address advances by the fragment actually sent, in bytes.
        addr_q  <= addr_q + ADDR_W'(this_frag) * ADDR_W'(4);
        if (rem_q <= limit_ext) begin
          rem_q    <= '0;
          active_q <= 1'b0;
        end else begin
          rem_q <= rem_q - limit_ext;
        end
      end
    end
  end
 
endmodule

Classification: synthesizable.

Architecture. A remainder counter plus an address cursor. mps_bytes is an input, not a parameter, for §5's reason.

State. Remaining DW, current address, active and first flags, a sticky error flag.

Contract. The caller relies on the fragment addresses forming a contiguous ascending sequence covering the operation exactly once, and on the sum of frag_len_dw equalling op_len_dw.

Failure — four, and the address one is the subtle one. Advancing addr_q by the limit rather than by this_frag corrupts the address of the fragment after a short one — which only ever happens on the last fragment, so it is invisible unless a test uses a length that is not a multiple of MPS. Using mps_bytes directly as a DW count sends fragments four times too large. A strict > in the last-fragment test emits a trailing zero-length fragment. And defaulting the limit when mps_valid is low emits packets sized by a guess.

Deliberately simplified: no alignment or boundary rules — this divides on the MPS boundary only; no payload movement, only descriptors; no interaction with ordering.

Production implication: a real fragmenter honours the alignment and boundary rules, generates per-fragment byte validity, and moves the payload alongside the descriptors. The MPS arithmetic above does not change.

10. RTL — Endpoint Write Front End

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Apply a decoded Memory Write to an internal resource.
// This block does NOT parse a TLP — it consumes normalized semantic
// metadata produced by the header decoder (Chapter 11.3 section 12).
// The offset computation and the resource interface: ILLUSTRATIVE.
module ep_write_frontend #(
  parameter int ADDR_W = 64,
  parameter int OFF_W  = 20,
  parameter int DATA_W = 128
) (
  input  logic clk,
  input  logic rst_n,
 
  // BAR base for the matched region, and whether the region is enabled.
  input  logic [ADDR_W-1:0]   bar_base,
  input  logic                bar_enabled,
 
  // ---- Decoded inbound write -------------------------------------------
  input  logic                in_valid,
  output logic                in_ready,
  input  logic                in_bar_hit,     // decode already performed
  input  logic [ADDR_W-1:0]   in_addr,
  input  logic [DATA_W-1:0]   in_data,
  input  logic [DATA_W/8-1:0] in_byte_mask,
 
  // ---- To the internal resource ----------------------------------------
  output logic                res_valid,
  input  logic                res_ready,
  output logic [OFF_W-1:0]    res_offset,
  output logic [DATA_W-1:0]   res_data,
  output logic [DATA_W/8-1:0] res_byte_mask,
 
  // A write arrived that this front end will not apply. Reported, and
  // routed nowhere — it must not silently become a resource update.
  output logic                unclaimed_write
);
 
  wire claimed = in_valid && in_bar_hit && bar_enabled;
 
  // Internal offset. Meaningful only when claimed.
  wire [ADDR_W-1:0] off_full = in_addr - bar_base;
 
  assign res_valid     = claimed;
  assign res_offset    = OFF_W'(off_full);
  assign res_data      = in_data;
  assign res_byte_mask = in_byte_mask;
 
  // Backpressure comes from the resource for a claimed write. An unclaimed
  // write is consumed immediately so it cannot wedge the receive path — it
  // is reported instead.
  assign in_ready = claimed ? res_ready : 1'b1;
 
  assign unclaimed_write = in_valid && !(in_bar_hit && bar_enabled);
 
endmodule

Classification: synthesizable.

Architecture. Purely combinational address translation and a pass-through handshake. It consumes normalized metadata, not raw header bits — §11's principle applied at the receive boundary.

State. None; the resource owns the side effect.

Contract. The resource relies on res_valid meaning this write is claimed and should be applied, and on the update happening on res_valid && res_ready and not before (§11). The decode stage guarantees in_bar_hit reflects a real match.

Failure — three. Omitting bar_enabled applies writes to a region software has not enabled. Computing the offset without checking claimed produces a plausible number for an unclaimed write, which a careless resource may then apply. And making in_ready unconditionally high drops claimed writes whenever the resource is busy — a silent loss with no error anywhere, since nothing returns to notice.

Deliberately simplified: one BAR region; no multi-beat payload; no error response for an unclaimed write; the decode itself is upstream.

11. The Side Effect Happens Exactly Once

A fundamental invariant, and the highest-value single RTL point in the chapter.

A write updates the target on valid && ready — never on valid alone.

12. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SVA over posted_write_buffer, posted_write_fragmenter and
// ep_write_frontend. LOCAL contracts plus the normative posted classification
// and MPS obligation of section 2 — not claims about ordering, error
// handling or the Data Link Layer.
 
// BUFFER — P1: ATOMIC OWNERSHIP. What is offered downstream is exactly what
// was accepted — address, payload and byte validity together. THE property
// that makes section 4's split-ownership corruption checkable.
// (exp_meta/exp_data/exp_bmask are a testbench model keyed by a shadow ID.)
property p_atomic_preservation;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && out_ready)
    |-> ((out_meta      == exp_meta[out_seq])
      && (out_data      == exp_data[out_seq])
      && (out_byte_mask == exp_bmask[out_seq]));
endproperty
a_atomic : assert property (p_atomic_preservation);
 
// BUFFER — P2: the head is stable while the TX path stalls. Address and
// payload do not drift apart, and neither moves.
property p_head_stable;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && !out_ready)
    |=> (out_valid && $stable({out_meta, out_data, out_byte_mask}));
endproperty
a_head_stable : assert property (p_head_stable);
 
// BUFFER — P3: full + pop + push is accepted, not refused. The full-rate
// steady state must not throttle.
property p_full_pop_push_accepted;
  @(posedge clk) disable iff (!rst_n)
  (full && pop) |-> in_ready;
endproperty
a_full_pop : assert property (p_full_pop_push_accepted);
 
// BUFFER — P4: occupancy is bounded and simultaneous push/pop conserves it.
property p_occupancy_sane;
  @(posedge clk) disable iff (!rst_n)
  (cnt_q <= CNT_W'(DEPTH)) && !(pop && (cnt_q == '0));
endproperty
a_occupancy : assert property (p_occupancy_sane);
 
property p_simul_conserves;
  @(posedge clk) disable iff (!rst_n)
  (push && pop) |=> (cnt_q == $past(cnt_q));
endproperty
a_simul : assert property (p_simul_conserves);
 
// BUFFER — P5: a full buffer refuses. Backpressure reaches the producer.
property p_full_blocks;
  @(posedge clk) disable iff (!rst_n)
  (full && !pop) |-> !in_ready;
endproperty
a_full_blocks : assert property (p_full_blocks);
 
// POSTED — P6: THE CLASS PROPERTY. A posted write never allocates
// Completion-tracking state. Bound against the read path's context table to
// show the two transaction classes do not share resources.
// (ctx_reserve is Chapter 12.1's read-context reservation.)
property p_posted_allocates_no_context;
  @(posedge clk) disable iff (!rst_n)
  push |-> !ctx_reserve;
endproperty
a_no_ctx : assert property (p_posted_allocates_no_context);
 
// TARGET — P7: THE SIDE-EFFECT PROPERTY. The resource updates once per
// accepted write, never once per offered cycle. Stated over counts because a
// value comparison cannot distinguish one write from ten identical ones.
// (res_xfer_count and apply_count are testbench counters.)
property p_side_effect_once;
  @(posedge clk) disable iff (!rst_n)
  (apply_count == res_xfer_count);
endproperty
a_once : assert property (p_side_effect_once);
 
// TARGET — P8: no update without a handshake. The direct form of P7, which
// fires on the first stalled cycle rather than at the end of a run.
property p_no_update_without_handshake;
  @(posedge clk) disable iff (!rst_n)
  (res_valid && !res_ready) |-> !resource_updated;
endproperty
a_no_early_update : assert property (p_no_update_without_handshake);
 
// TARGET — P9: an unclaimed write never becomes a resource update.
property p_unclaimed_not_applied;
  @(posedge clk) disable iff (!rst_n)
  unclaimed_write |-> !res_valid;
endproperty
a_unclaimed : assert property (p_unclaimed_not_applied);
 
// TARGET — P10: a claimed write is never dropped. in_ready must reflect the
// resource, not be tied high.
property p_claimed_not_dropped;
  @(posedge clk) disable iff (!rst_n)
  (in_valid && in_bar_hit && bar_enabled && !res_ready) |-> !in_ready;
endproperty
a_not_dropped : assert property (p_claimed_not_dropped);
 
// FRAGMENT — P11: THE NORMATIVE TRANSMIT OBLIGATION. No fragment exceeds the
// operative MPS.
property p_fragment_within_mps;
  @(posedge clk) disable iff (!rst_n)
  (frag_valid && mps_valid) |-> (frag_len_dw <= mps_dw);
endproperty
a_within_mps : assert property (p_fragment_within_mps);
 
// FRAGMENT — P12: CONSERVATION. The fragments cover the operation exactly.
// (frag_sum and op_total are testbench accumulators.)
property p_fragments_conserve;
  @(posedge clk) disable iff (!rst_n)
  (frag_valid && frag_last && frag_ready)
    |-> (frag_sum + frag_len_dw == op_total);
endproperty
a_frag_conserved : assert property (p_fragments_conserve);
 
// FRAGMENT — P13: address progression. Each fragment starts where the last
// one ended. Catches an address advanced by the LIMIT rather than by the
// fragment actually sent — invisible except after a short fragment.
property p_address_progression;
  @(posedge clk) disable iff (!rst_n)
  (fire && !frag_last)
    |=> (frag_addr == $past(frag_addr) + ADDR_W'($past(frag_len_dw)) * ADDR_W'(4));
endproperty
a_addr_progress : assert property (p_address_progression);
 
// FRAGMENT — P14: no zero-length fragment, and no operation is started
// without a known limit.
property p_fragment_nonzero;
  @(posedge clk) disable iff (!rst_n)
  frag_valid |-> (frag_len_dw != '0);
endproperty
a_frag_nonzero : assert property (p_fragment_nonzero);
 
property p_no_op_without_limit;
  @(posedge clk) disable iff (!rst_n)
  (!mps_valid || (mps_dw == 11'd0)) |-> !op_ready;
endproperty
a_needs_limit : assert property (p_no_op_without_limit);
 
// RESET — P15: reset clears local ownership. This is a LOCAL TEACHING-MODEL
// contract about these blocks; it is NOT a statement about PCIe's system
// reset semantics, which later chapters own.
property p_reset_clears_ownership;
  @(posedge clk)
  !rst_n |=> (!out_valid && (cnt_q == '0) && !frag_valid && !res_valid);
endproperty
a_reset : assert property (p_reset_clears_ownership);

Liveness, with its assumptions stated separately:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// A1: the outbound TX path eventually accepts an offered write.
assume property (@(posedge clk) disable iff (!rst_n)
  out_valid |-> s_eventually out_ready);
// A2: the target resource eventually accepts a claimed write.
assume property (@(posedge clk) disable iff (!rst_n)
  res_valid |-> s_eventually res_ready);
 
// L1: under A1-A2, an accepted local write eventually reaches the target.
// NOT valid without the assumptions: the fabric may congest and the
// resource may stall indefinitely, and PCIe guarantees neither.
property p_write_eventually_applied;
  @(posedge clk) disable iff (!rst_n)
  push |-> s_eventually (res_valid && res_ready);
endproperty
a_liveness : assert property (p_write_eventually_applied);

P7 and P8 are the same fact at two timescales and both are worth having. P8 fires on the first stalled cycle, which makes it the debugging property. P7 is the one that catches a duplication that P8 misses — a design that applies the write on a derived signal that happens to pulse twice per handshake satisfies P8 and fails P7.

P6 is bound across two chapters' blocks on purpose. The posted classification is only meaningful as a contrast, and asserting it locally within the write path would be vacuous — there is no context table there to allocate from. Binding it against Chapter 12.1's reservation signal is what makes "posted writes do not consume correlation resources" a checkable statement rather than an architectural intention.

P13 exists because P12 is satisfiable with wrong addresses. Conservation only counts DW; a fragmenter that advanced its address by the limit rather than by the fragment sent would deliver the right amount of data to the wrong places. And the error appears only after a short fragment, which means only on the last one, which means only when the length is not a multiple of MPS.

P1 uses a testbench shadow ID for the same reason Chapter 11.6's P2 did. A posted write carries no correlation field — that is the definition of posted — so nothing in the design lets an assertion say this output is that input. The shadow ID is verification apparatus and is not a Tag, not a correlation identifier, and not present in any packet.

13. Verification

Monitors observe: the local write interface with a shadow sequence number; the buffer's output; the fragmenter's operation and fragment interfaces; and the resource interface with a separate observer on the resource's actual state changes.

The scoreboard stores each accepted write independently — address, payload, byte mask, attributes, keyed by shadow ID — and compares against what reaches the resource. It must not read mem_q, reuse the fragmenter's this_frag, or call the front end's offset computation. It computes its own expected offset from its own copy of the BAR base.

And it must explicitly assert the absence of a Completion. A posted write producing no Completion is expected behaviour, so it needs a positive check: the Completion monitor must observe zero Completions attributable to any write in the run. A scoreboard that simply does not look for one cannot distinguish "correctly absent" from "not checked."

Functional

  • One write, minimum payload, all bytes valid.
  • Continuous writes at full rate with no backpressure.
  • The same address written twice with different data. Verify both reach the resource and the second value survives.
  • Different addresses, ascending and descending. Verify no address carries between entries.
  • Payload patterns: all-zero, all-ones, alternating, and a distinctive per-write pattern derived from the shadow ID so a payload/address mismatch is unmistakable.
  • Every byte-mask value for the modelled width, including single-byte and no-bytes-valid cases.
  • The maximum modelled payload.
  • Back-to-back writes with opposite attributes. Verify Chapter 11.6's ownership rule holds through this buffer too.

Backpressure and same-cycle

  • The TX path stalled with the buffer non-empty. Verify head stability (P2) — address and payload together, for the whole stall.
  • The buffer driven to full. Verify in_ready drops (P5) and the producer is held.
  • Full with a simultaneous pop and push. Verify the write is accepted (P3) and occupancy is unchanged.
  • The resource stalled while a claimed write is offered. Verify in_ready drops (P10) and the resource does not update (P8) — the duplication test.
  • A long resource stall, then release. Verify the resource updates exactly once (P7).
  • DEPTH = 1, and a non-power-of-two DEPTH (3, 5, 7). Parameter corners: verify the index guard and the explicit wrap.

Target boundary

  • bar_enabled low with a matching address. Verify unclaimed_write and no resource update (P9).
  • in_bar_hit low. Same.
  • An address exactly at the BAR base and one at the top of the modelled region. Offset boundary checks.
  • An unclaimed write immediately followed by a claimed one. Verify the claimed write is applied and the unclaimed one did not wedge the path.

Fragmentation

  • An operation exactly equal to MPS. One fragment.
  • An operation of MPS + 1 DW. Two fragments, the second of one DW (P14).
  • An operation of exactly N × MPS. Verify no trailing zero-length fragment.
  • An operation with a short final fragment. Verify the address of every fragment (P13) — the only stimulus that catches an address advanced by the limit.
  • Each MPS encoding decoded to bytes: 128, 256, 512, 1024, 2048, 4096. Verify no fragment exceeds it (P11).
  • mps_valid low. Verify refusal and no_limit_error.

Which test kills which bug

Injected faultWhat catches it
payload droppedscoreboard: accepted count vs. applied count
payload duplicatedP7, and the long-resource-stall test
target updated on valid aloneP8, on the first stalled cycle
address incremented by the limit, not the fragmentP13, with a short final fragment
old payload paired with a new addressP1, with per-write distinctive payloads
byte mask shifted by one laneper-byte comparison at the resource; a whole-word check misses it
buffer entry freed before the TX handshakeP2, under TX backpressure
posted write allocates a read contextP6
claimed write dropped when the resource is busyP10
in_ready gated on !full aloneP3, at full-rate steady state
write applied for a disabled BARP9

Coverage should include: every byte-mask value; occupancy from empty to full including simultaneous push/pop at each level; DEPTH = 1 and a non-power-of-two depth; every MPS encoding; operations below, at, above and at exact multiples of MPS; claimed and unclaimed writes; and both attribute states on each of the three attribute bits.

14. Debugging

A host write is accepted locally but the Endpoint register never changes

Walk §3's table and find the first boundary that did not transfer. Do not start anywhere else — and in particular, do not start at the Endpoint.

  1. Did the producer's write handshake complete? in_valid && in_ready. If not, the buffer was full — and why it was full is a different question, usually a stalled TX path.
  2. Is there an entry in the posted buffer? If the handshake completed and occupancy did not rise, cnt_q is being driven from in_valid rather than push.
  3. Was an outbound Request generated? If the buffer is non-empty and out_valid never rose, the buffer's output logic is broken; if out_valid rose and out_ready never did, it is flow control or arbitration.
  4. Is the address correct in the packet? Compare against what the producer supplied. A wrong address routes perfectly to the wrong place.
  5. Did it route? Check each Switch port's Base/Limit windows (Chapter 11.5 §4). An address matching no downstream window goes upstream rather than being dropped.
  6. Did the Endpoint receive it? If not, the fault is between 4 and 5.
  7. Did BAR decode hit? Check in_bar_hit and bar_enabled — a disabled region produces unclaimed_write, which is a report, not a failure to arrive.
  8. Did the resource handshake complete? res_valid && res_ready. If res_valid rose and res_ready never did, the resource is stalled or wedged.
  9. Did the register change? If the handshake completed and the state did not, the fault is inside the resource.

The method generalises and it is the point: find the last boundary where address, payload and ownership are all correct, then inspect the next one. Nine steps sounds long; in practice each one is a single signal and the search is logarithmic in frustration.

The same write happens twice

Four candidates, and the first is by far the most likely.

The resource updates on valid rather than on valid && ready (§11). Check whether the duplication count matches the stall length — if a ten-cycle stall produced ten writes, this is certain and nothing else needs checking.

Otherwise: the buffer's read pointer did not advance on pop, so the same entry is offered twice. Or the fragmenter emitted a fragment twice because fire was mis-derived. Or — and this one sends people to the wrong layer entirely — someone is confusing a Data Link Layer retransmission with a duplicated transaction. Those are different mechanisms at different layers (Module 14), and a link-layer replay does not produce two transaction-layer writes.

The address is correct but the data is wrong

Address and payload came apart. §4 is the class, and the specific cause is one of four.

Split ownership: metadata and payload handshaked separately and drifted. Check whether the wrong data belongs to an adjacent write — if the data is the previous write's, it is a pointer or a bypass; if it is the next write's, the payload path is running ahead.

A FIFO pointer error: wr_q and rd_q advancing on different conditions, or natural rollover at a non-power-of-two depth. Check the depth first — this one is deterministic and depth-dependent.

A stale payload: the buffer stored the metadata and re-read the payload from a live bus at output time.

A byte-lane mapping error: the data is right but shifted. Compare byte by byte rather than word by word — a lane swap can produce a word that differs in a way a quick glance reads as "wrong data" rather than "right data, wrong lanes."

Throughput is half of expected and every write is well-formed

Two candidates, distinguished by one number.

The fragmenter is under-filling — emitting fragments smaller than MPS permits, so every operation costs more headers than it needs. Compute the ratio of fragment size to the operative MPS; below 1.0 the fragmenter is the problem.

Or in_ready is gated on !full alone, which inserts a bubble every time the buffer runs at capacity. The signature is that throughput degrades specifically when the buffer is full — that is, exactly when the design should be running fastest.

15. Common Misconceptions

  • "Posted means no buffering." Posted means no Completion. The payload still has to be held by every stage until the next one takes it (§3).
  • "Posted means the remote side has already executed the write." It means the Requester will not be told. The write may still be in a buffer three hops away (§3).
  • "Posted means no error can occur." Errors can occur and PCIe has mechanisms to report them — they are simply not Completions (§7).
  • "Posted means PCIe has no acknowledgements anywhere." The Data Link Layer has its own reliability mechanism between adjacent components (Module 14). It is invisible at the transaction level and is not a Completion (§7).
  • "A Memory Write receives a Completion with success status." It does not. A packet arriving that claims to complete a write is either a different transaction or a bug.
  • "The requester may release the payload as soon as software issues the write." It may release it when a downstream stage has taken ownership — which is a handshake, not an instruction retiring (§3).
  • "The payload can be recomputed later." It is owned state from acceptance, exactly like the attributes (Chapter 11.6 §7). Re-reading a live bus at output time is §14's stale-payload bug.
  • "MPS and MRRS constrain the same thing." MPS bounds the payload a TLP carries; MRRS bounds how much a read request asks for. For a write, only MPS applies (§5).
  • "The Endpoint's BAR performs the fabric routing." The fabric routes using Switch Base/Limit windows; BAR decode runs after arrival to find the internal offset (§6).
  • "The internal register may update whenever the packet's valid is high." It updates on valid && ready, once. Updating on valid writes once per stalled cycle (§11).
  • "A posted transaction has no state." It has forward-path state at every stage — just no correlation state (§1).
  • "A Memory Write always fits in one TLP." Its payload is bounded by the operative MPS, which is commonly 128 or 256 bytes. Larger operations become several packets (§5).

16. Understanding Check

17. What's Next

This chapter traced the other half of memory traffic. A posted write turned out to need none of Chapter 12.1's correlation machinery and all of its discipline: address and payload owned together, a defined release point at every boundary, a target updated exactly once, and backpressure that reaches all the way back to the producer — because with nothing returning, a local buffer is the only thing that can say no.

Chapter 12.3 — Completion Flow goes back to the read side and takes the return path seriously: how a Completer turns a finished target operation into Completion packets, how those packets are queued and routed home, and how the Requester's progress accounting decides when a read is actually finished.

Chapter 12.4 — Examples then puts reads and writes together in worked traces. Chapter 12.5 takes the performance implications this chapter deliberately only gestured at, and Module 14 owns the link-layer reliability that §7 was careful not to confuse with a Completion.

The idea to carry forward: posted does not mean stateless — it means the state is all in front of the packet instead of behind it.