Skip to content

PCIe · Module 10

Transaction Flow — One Lifecycle, Many Owners

A PCIe transaction is a distributed ownership lifecycle. Trace a posted write and a non-posted read end to end, see who owns the operation at every boundary, and learn the last-correct-boundary method that localises any Transaction Layer failure.

Module 10 has built four chapters of vocabulary and one piece of it has never been assembled: 10.1 gave the Request, 10.2 the Completion and its correlation, 10.3 forward-only ownership, 10.4 the outstanding lifetime.

How do the Request, the outstanding state, the fabric traversal, the Completer's service and the Completion return fit together as one complete transaction?

1. Why the Assembly Is Its Own Chapter

The individual chapters each held one part of the transaction still while examining it. That is how you learn a mechanism and it is not how you debug one.

A real failure does not arrive labelled. "The device does not respond" is compatible with a stalled producer, an unrouted address, a Completer that never saw the Request, a Completion that never returned, a correlation miss, and a local consumer that stopped draining — six different faults, in five different places, with one symptom.

What distinguishes them is not knowledge of any single mechanism. It is knowing the order the boundaries occur in and being able to ask, at each one, did the transaction get this far. That is a property of the whole flow and it cannot be learned from any one chapter.

This chapter's real deliverable is a method: find the last boundary where the transaction was demonstrably correct, and the first where it was not. The fault is between them.

Everything else here — the two flows, the ownership table, the scoreboard — exists to make that method precise.

2. Flow A — A Posted Memory Write

The simpler lifecycle, and simpler for a specific reason: it has no return half.

#StageWho owns the operationWhat can back-pressure hereWhen the previous owner may release
1Local producer holds address, byte enables, payloadproducer
2Outbound posted queue accepts itqueuequeue fullat the accept handshake (10.3 §5)
3Transaction Layer takes it from the queueoutbound TLTL not ready; flow-control resources unavailableat the TL handshake
4Data Link boundary takes the packetlower layersLink-level backpressureat that handshake
5Fabric carries and routes itfabriccongestion at any hopper-hop, as each hop takes it
6Completer's Transaction Layer receives and classifies itCompleter TLCompleter's receive resources
7Resource decode claims the addressCompleter's front endinternal resource not ready (9.6 §11)
8Payload applied at the resource offsetthe resource

No normal Completion returns. After stage 2 the producer has no further stake, and after stage 3 nothing the Requester holds refers to this operation at all.

3. Flow B — A Non-Posted Memory Read

Everything above, plus a return half, plus state that survives across both.

#StageWho owns the operationWhat can back-pressure hereRelease event
1Local client has a read to issueclient
2Outstanding state allocated; Request launchedRequester TL and outstanding tablepool exhausted; outbound path stalledclient releases at the launch handshake; the table does not
3Data Link boundary takes the packetlower layersLink-level backpressure
4Fabric carries and routes itfabriccongestion
5Completer's TL receives and classifies itCompleter TLreceive resources
6Target operation performed; data producedCompleter's resourceresource not ready
7Completion constructedCompleter TLoutbound arbitration; return-path resources
8Completion traverses the fabricfabriccongestion
9Requester classifies the returning CompletionRequester TLresult stage occupied
10Correlation lookup against the retained contextoutstanding table
11Result delivered to the local clientclientclient not draining
12Outstanding entry freedon the terminating Completion, not before

Stage 2 is where the two flows diverge and never converge again. In Flow A the producer's release ends the story; here the client's release at launch is only half of it, because the outstanding table's ownership continues for the entire round trip.

Two ownership arcs, not one. The packet's arc runs stages 2→5 and ends. The Sequence's arc runs 2→12. Chapter 10.4 §3 named the confusion between them as the single most consequential misreading in the module; the table above is what that confusion looks like when you lay both arcs out.

4. The Exchange, Drawn

A complete non-posted read. The requester client offers an operation to the requester Transaction Layer, which allocates outstanding state and launches the Request onto the fabric. The fabric delivers it to the Completer Transaction Layer, which issues the operation to the Completer resource. The resource returns data. The Completer Transaction Layer constructs a Completion, which the fabric returns to the requester Transaction Layer. That layer matches the Completion against the retained state, delivers the result to the client, and frees the outstanding entry.A non-posted read, end to endRequesterclientRequester TLFabricCompleter TLCompleterresourceoperation offeredstate allocated -Request sentdelivered to theCompleteroperation issued attargetdata producedCompletionconstructedCompletion returnedmatched - resultdelivered
Figure 1 — one non-posted read across all five participants. Correlation state is allocated as the Request is launched and released only when the terminating Completion has been matched and the result delivered. The Requester is free to issue other work throughout the interval between messages 2 and 7, subject to the limits Chapter 10.4 develops.

Read the figure as two independent journeys sharing one piece of state. Messages 2–4 are the forward journey; 6–8 are the return journey; and the retained state at the Requester is the only thing that exists across both. Nothing else in the system remembers that these two journeys belong together.

And note what the figure cannot show: the interval. Between messages 2 and 7 the Requester is free to issue other operations, and in a busy system it will have several outstanding at once whose Completions interleave arbitrarily (Chapter 10.2 §5).

5. The Two Flows Compared

DimensionPosted writeNon-posted read
Boundaries in the lifecycle812
Return halfnonestages 7–12
State surviving the launchnone for this operationcorrelation and destination context
Locally observable stages1–31–2 and 9–12
Blind intervalstages 4–8stages 3–8
What ends the lifecycleforward acceptancethe terminating Completion
Localisation methodwalk forward until visibility endsbisect: forward evidence and return evidence

The last row is the practical payoff. A non-posted failure gives you evidence from both ends, so you can bisect. A posted failure gives you evidence from one end, so you can only walk. That difference decides how you spend the first ten minutes of an investigation, and it is worth knowing before you start rather than after.

6. Microarchitecture — Where to Instrument

The method of §7 needs observation points, and choosing them is a design decision that has to be made before silicon, not after.

The five that pay for themselves, in the order the transaction reaches them:

PointWhat it answersCost
Request launchdid the operation leave, and with what correlation keyone event, a few bits
Target acceptance at the Completerdid it arrive and get claimedone event at the far side
Completion emissiondid the Completer produce an answerone event at the far side
Completion match at the Requesterdid the answer find its operationone event, plus a hit/miss bit
Local resolutiondid the result reach the clientone event

Between them they partition the lifecycle into regions, and each region contains a different class of fault. §10's monitor implements exactly these five and nothing else, because a sixth adds cost without adding a distinction.

7. The Method — Last Correct Boundary, First Incorrect

This is the chapter's durable contribution, and it generalises well past PCIe.

For a non-posted read, the boundaries in order:

  1. Was the local request accepted by the Requester's engine?
  2. Was an outstanding entry allocated for it?
  3. Was a Request emitted onto the outbound path?
  4. Did the Request reach the Completer?
  5. Did the target operation happen?
  6. Was a Completion created?
  7. Did the Completion return to the Requester?
  8. Did the correlation lookup hit?
  9. Was the result routed to the correct local client?
  10. Was the outstanding entry freed?

The method is not to walk the list. It is to find the highest-numbered boundary you can prove was correct, and the lowest-numbered one you can prove was not. The fault is between them, and every observation you make should aim to halve that interval rather than advance one step.

For a posted write the list is shorter and the method is weaker, because boundaries 6–10 do not exist and 4–8 of §2's table are unobservable from the Requester. The bisection degenerates into a forward walk — which is Chapter 10.3 §12's ladder, and now visible as a special case of this method rather than a separate technique.

8. RTL — A Lifecycle Scoreboard

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// VERIFICATION-ONLY. Tracks every transaction through the boundaries of
// section 7 and reports the FIRST missing transition rather than a late
// symptom. Not synthesizable, not a PCIe state machine.
package flow_sb_pkg;
 
  // A verification model of "how far has this transaction got".
  typedef enum logic [2:0] {
    LIFE_IDLE      = 3'd0,   // no transaction with this key
    LIFE_LAUNCHED  = 3'd1,   // Request emitted
    LIFE_AT_TARGET = 3'd2,   // observed accepted at the Completer
    LIFE_COMPLETING= 3'd3,   // a Completion has been emitted for it
    LIFE_MATCHED   = 3'd4,   // matched at the Requester
    LIFE_DONE      = 3'd5    // result delivered and state freed
  } life_state_e;
 
  typedef struct {
    life_state_e state;
    bit          posted;      // posted transactions stop at LIFE_AT_TARGET
    int unsigned t_launch;    // cycle stamps, for the latency report
    int unsigned t_target;
    int unsigned t_cpl;
    int unsigned t_done;
  } life_entry_t;
 
endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// VERIFICATION-ONLY. The checker body. One entry per correlation key, with
// the legal transition set stated once and enforced everywhere.
import flow_sb_pkg::*;
 
class transaction_flow_sb #(parameter int IDS = 8);
 
  life_entry_t tbl [IDS];
  int unsigned now_cyc;
 
  // The ONLY legal transitions. Everything else is a violation, and naming
  // the illegal set explicitly is what lets the checker report the first
  // MISSING transition instead of a downstream symptom.
  function automatic bit legal_step(life_state_e from, life_state_e to,
                                    bit posted);
    case (from)
      LIFE_IDLE:       return (to == LIFE_LAUNCHED);
      LIFE_LAUNCHED:   return (to == LIFE_AT_TARGET);
      // A posted transaction's lifecycle ends at the target. It has no
      // Completion, so LIFE_COMPLETING is unreachable for it — and a
      // Completion observed for a posted key is itself the bug.
      LIFE_AT_TARGET:  return posted ? (to == LIFE_DONE)
                                     : (to == LIFE_COMPLETING);
      LIFE_COMPLETING: return (to == LIFE_MATCHED);
      LIFE_MATCHED:    return (to == LIFE_DONE);
      LIFE_DONE:       return (to == LIFE_IDLE);   // key released for reuse
      default:         return 1'b0;
    endcase
  endfunction
 
  function automatic void step(int unsigned id, life_state_e to);
    life_state_e from = tbl[id].state;
    if (!legal_step(from, to, tbl[id].posted)) begin
      $error("txn %0d: illegal lifecycle step %s -> %s (posted=%0b)",
             id, from.name(), to.name(), tbl[id].posted);
      return;
    end
    tbl[id].state = to;
  endfunction
 
  // ---- Boundary event handlers (section 6's five points) --------------
  function automatic void on_launch(int unsigned id, bit is_posted);
    // A key must not be reused while its previous transaction is live.
    // Chapter 10.4 section 5's hazard, checked independently of the DUT.
    if (tbl[id].state != LIFE_IDLE)
      $error("txn %0d: launched while state=%s — correlation key reused",
             id, tbl[id].state.name());
    tbl[id].posted   = is_posted;
    tbl[id].t_launch = now_cyc;
    step(id, LIFE_LAUNCHED);
  endfunction
 
  function automatic void on_target_accept(int unsigned id);
    tbl[id].t_target = now_cyc;
    step(id, LIFE_AT_TARGET);
  endfunction
 
  function automatic void on_completion_emit(int unsigned id);
    if (tbl[id].posted)
      $error("txn %0d: Completion emitted for a POSTED transaction", id);
    tbl[id].t_cpl = now_cyc;
    step(id, LIFE_COMPLETING);
  endfunction
 
  function automatic void on_match(int unsigned id);
    step(id, LIFE_MATCHED);
  endfunction
 
  function automatic void on_resolve(int unsigned id);
    tbl[id].t_done = now_cyc;
    step(id, LIFE_DONE);
    tbl[id].state = LIFE_IDLE;    // key returns to the pool
  endfunction
 
  // ---- End of test: the report that actually helps -------------------
  function automatic void final_check();
    foreach (tbl[i]) begin
      if (tbl[i].state != LIFE_IDLE)
        $error("txn %0d STALLED at %s — first missing transition is the one "
               "out of this state", i, tbl[i].state.name());
    end
  endfunction
 
endclass

Classification: verification-only.

What it teaches — four things:

  1. The legal transition set is written once, in one function. Every event handler consults it, so an illegal step is impossible to introduce by adding a handler. A checker with the rules scattered across its handlers accumulates inconsistencies exactly as fast as it accumulates handlers.
  2. Posted and non-posted share one model with one branch. legal_step differs in a single case arm, which is the whole structural difference between the two flows expressed as code — and it makes "a Completion arrived for a posted transaction" a detected error rather than an unnoticed anomaly.
  3. Key reuse is checked by the checker, independently. on_launch rejects a launch onto a non-idle key. This is Chapter 10.4 §5's hazard verified from outside the design, which is the only place it can be verified honestly — the design's own bookkeeping is what is under test.
  4. The end-of-test report names the first missing transition, not the symptom. "Stalled at LIFE_COMPLETING" says the Completion was emitted and never matched, which is boundary 7 or 8 — a two-boundary interval handed to you at zero cost.

Deliberately simplified: one Completion per Sequence, so a multi-Completion response would need an accumulating state rather than a single step to LIFE_COMPLETING (Chapter 13.3); no modelling of ordering; cycle stamps rather than a full latency distribution.

Production implication: the same structure is what a real verification environment builds, sized for the design's outstanding capacity and fed from monitors on both sides of the Link. The far-side events usually come from a bus-functional model or a protocol analyser rather than from RTL probes.

9. RTL — Boundary Event Telemetry

The scoreboard consumes events. Something has to produce them, and in silicon that something is real logic.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Minimal boundary telemetry for the local side of a
// transaction: how many transactions passed each observable boundary, and
// how many are unresolved right now.
// Instrumentation for debug, not part of the protocol.
//
// TWO TELEMETRY DIMENSIONS, and they are not interchangeable:
//   - cumulative event counters answer "how many events have occurred"
//   - a live-occupancy counter answers "how many are outstanding NOW"
// The second cannot be inferred from the first once the first saturates.
module txn_boundary_monitor #(
  parameter int IDS   = 8,
  parameter int CNT_W = 32,
  parameter int ID_W  = (IDS <= 1) ? 1 : $clog2(IDS),
  // Live occupancy is bounded by the outstanding pool, so it needs far fewer
  // bits than a cumulative total — and sizing it from IDS is what lets the
  // over-capacity condition below be detectable rather than representable.
  parameter int OCC_W = (IDS <= 1) ? 1 : ($clog2(IDS) + 1)
) (
  input  logic            clk,
  input  logic            rst_n,
 
  // The three locally observable boundaries of section 6.
  input  logic            ev_launch,
  input  logic [ID_W-1:0] ev_launch_id,
  input  logic            ev_launch_posted,
  input  logic            ev_match,          // a Completion matched an entry
  input  logic            ev_orphan,         // a Completion matched nothing
  input  logic            ev_resolve,        // result delivered, entry freed
 
  // Counters. Saturating rather than wrapping: a wrapped debug counter is
  // worse than a stuck one, because it reads as a plausible small number.
  output logic [CNT_W-1:0] n_launched_np,
  output logic [CNT_W-1:0] n_launched_p,
  output logic [CNT_W-1:0] n_matched,
  output logic [CNT_W-1:0] n_orphan,
  output logic [CNT_W-1:0] n_resolved,
 
  // LIVE OCCUPANCY — how many non-posted operations are outstanding right
  // now. A separate counter, not a subtraction of the two totals above.
  output logic [OCC_W-1:0] n_unresolved,
  // A resolve with nothing outstanding, or a launch beyond the pool. Both
  // are impossible in a correct system, so both are reported rather than
  // silently wrapped or clamped away.
  output logic             occ_error
);
 
  function automatic logic [CNT_W-1:0] inc_sat(input logic [CNT_W-1:0] c);
    return (c == {CNT_W{1'b1}}) ? c : (c + 1'b1);
  endfunction
 
  logic [CNT_W-1:0] np_q, p_q, m_q, o_q, r_q;
  logic [OCC_W-1:0] occ_q;
  logic             occ_err_q;
 
  // The two live-occupancy events. Posted launches never enter the pool.
  wire np_launch  = ev_launch && !ev_launch_posted;
  wire np_resolve = ev_resolve;
 
  // Illegal transitions, detected BEFORE the counter is updated so the
  // counter never has to represent an impossible value.
  wire occ_underflow = np_resolve && !np_launch && (occ_q == '0);
  wire occ_overflow  = np_launch  && !np_resolve && (occ_q == OCC_W'(IDS));
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      np_q <= '0; p_q <= '0; m_q <= '0; o_q <= '0; r_q <= '0;
      occ_q <= '0; occ_err_q <= 1'b0;
    end else begin
      // Cumulative totals — saturating, because a wrapped debug counter
      // reads as a plausible small number.
      if (ev_launch &&  ev_launch_posted) p_q  <= inc_sat(p_q);
      if (np_launch)                      np_q <= inc_sat(np_q);
      if (ev_match)   m_q <= inc_sat(m_q);
      if (ev_orphan)  o_q <= inc_sat(o_q);
      if (ev_resolve) r_q <= inc_sat(r_q);
 
      // LIVE OCCUPANCY — one case, four arms. The 2'b11 arm is the one that
      // must be written explicitly: a launch and a resolve in the same cycle
      // leave the outstanding population unchanged, and two independent
      // if-statements would apply only the later assignment.
      case ({np_launch, np_resolve})
        2'b10:   occ_q <= occ_overflow  ? occ_q : (occ_q + OCC_W'(1));
        2'b01:   occ_q <= occ_underflow ? occ_q : (occ_q - OCC_W'(1));
        default: occ_q <= occ_q;          // 2'b11 and 2'b00: unchanged
      endcase
 
      // Sticky, because the transient that caused it is long gone by the
      // time anyone reads the counter.
      if (occ_underflow || occ_overflow) occ_err_q <= 1'b1;
    end
  end
 
  assign n_launched_np = np_q;
  assign n_launched_p  = p_q;
  assign n_matched     = m_q;
  assign n_orphan      = o_q;
  assign n_resolved    = r_q;
  assign n_unresolved  = occ_q;
  assign occ_error     = occ_err_q;
 
endmodule

Classification: synthesizable.

What it teaches — four things:

  1. Six numbers answer §7's shortcuts without any per-transaction storage. n_orphan non-zero is boundary 8; n_launched_np equal to n_resolved with the client still waiting is boundary 9 or 11; and n_unresolved is what makes the "is anything outstanding right now" shortcut a single read.
  2. Cumulative totals and live occupancy are different telemetry dimensions. A cumulative counter answers how many events have occurred; a live-occupancy counter answers how many operations are unresolved right now. The second must not be inferred by subtracting two independently saturating totals — once either total saturates, the difference stops tracking the population and becomes an arbitrary number that still looks plausible.
  3. The live counter is one case with four arms. Launch alone increments, resolve alone decrements, both in the same cycle leave it unchanged, and neither leaves it unchanged. Written as two independent if statements, the same-cycle case applies only the later assignment and the count drifts by one per coincidence — the same failure Chapter 10.2 §10 guards in the outstanding table.
  4. Impossible transitions are reported, not absorbed. A resolve with nothing outstanding, or a launch beyond the pool, cannot happen in a correct system — so occ_error is sticky rather than the counter wrapping or clamping. A clamped counter hides the fault; a wrapped one manufactures a plausible wrong value.

Deliberately simplified: counts and one live occupancy, with no latency histogram; local boundaries only, because the far-side events are not observable from this device (§6). There is no age tracking here — reporting how long the oldest unresolved operation has been outstanding would require a timestamp or an age counter per entry, which is outside this small boundary-counter example.

Production implication: real designs expose counters like these through a debug capability or a vendor-defined register block, and typically add the per-entry age state this example omits — usually as a watchdog per outstanding entry (Chapter 10.4 §9), so that "how old is the oldest" is available alongside "how many are there."

10. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SVA over the boundary events and the local Transaction Layer of Chapters
// 10.2-10.4. Cross-boundary lifecycle invariants — not PCIe protocol rules,
// and not claims about anything in a packet.
 
// ENVIRONMENT ASSUMPTIONS. The far-side events are produced by monitors, not
// by this device, so their well-formedness is assumed rather than asserted.
assume property (@(posedge clk) disable iff (!rst_n)
  ev_target_accept |-> (ev_target_id < IDS));
// FAIRNESS, required for the liveness property only.
assume property (@(posedge clk) disable iff (!rst_n)
  s_eventually (res_ready));
 
// LIFECYCLE — P1: a Completion is never observed for a transaction that was
// never launched. "A Completion always corresponds to a preceding Request"
// (Chapter 10.2 section 2), checked across the whole flow.
property p_no_completion_before_launch;
  @(posedge clk) disable iff (!rst_n)
  ev_match |-> sb_launched[ev_match_id];
endproperty
a_cpl_after_launch : assert property (p_no_completion_before_launch);
 
// LIFECYCLE — P2: a launch implies outstanding state exists for it. The
// Chapter 10.4 contract, re-asserted at flow scope.
property p_launch_has_state;
  @(posedge clk) disable iff (!rst_n)
  (ev_launch && !ev_launch_posted) |=> busy_mask[$past(ev_launch_id)];
endproperty
a_launch_tracked : assert property (p_launch_has_state);
 
// LIFECYCLE — P3: a POSTED transaction never allocates Completion-tracking
// state and never produces a match. The structural difference between the
// two flows, asserted rather than assumed.
property p_posted_never_matches;
  @(posedge clk) disable iff (!rst_n)
  (ev_launch && ev_launch_posted) |-> (!alloc_gnt && !ev_match);
endproperty
a_posted_untracked : assert property (p_posted_never_matches);
 
// LIFECYCLE — P4: resolution follows a match. The entry is not freed by
// anything else — not by a timeout, not by transmission.
property p_resolve_requires_match;
  @(posedge clk) disable iff (!rst_n)
  ev_resolve |-> $past(ev_match, 1) or $past(ev_match, 2) or $past(ev_match, 3);
endproperty
 
// LIFECYCLE — P4 (stronger, per key): the entry for a key is freed only
// after that key was matched. Written per key so the claim is about each
// transaction rather than about any recent activity.
generate for (genvar k = 0; k < IDS; k++) begin : g_resolve_after_match
  property p_key_resolve_after_match;
    @(posedge clk) disable iff (!rst_n)
    (ev_resolve && (ev_resolve_id == k[ID_W-1:0])) |-> sb_matched[k];
  endproperty
  a_resolve_ordered : assert property (p_key_resolve_after_match);
end endgenerate
 
// CONSERVATION — P5: no transaction reaches DONE twice. A key resolved twice
// means either a duplicate Completion resolved a live entry or the free path
// fired twice — both corrupt an unrelated operation.
generate for (genvar k = 0; k < IDS; k++) begin : g_single_resolve
  property p_resolve_once;
    @(posedge clk) disable iff (!rst_n)
    (ev_resolve && (ev_resolve_id == k[ID_W-1:0])) |=> !sb_matched[k];
  endproperty
  a_resolve_once : assert property (p_resolve_once);
end endgenerate
 
// CONSERVATION — P6: more resolutions than non-posted launches is impossible.
// Holds only while neither total has saturated, which is why P6a below is the
// one that keeps working for the whole run.
property p_unresolved_sane;
  @(posedge clk) disable iff (!rst_n)
  (n_resolved != {CNT_W{1'b1}}) && (n_launched_np != {CNT_W{1'b1}})
    |-> (n_resolved <= n_launched_np);
endproperty
a_no_extra_resolve : assert property (p_unresolved_sane);
 
// LIVE OCCUPANCY — P6a: the four-arm contract, one property per arm. These
// are LOCAL TELEMETRY invariants about this counter, not claims about any
// PCIe outstanding limit.
property p_occ_launch_only_increments;
  @(posedge clk) disable iff (!rst_n)
  (np_launch && !np_resolve && !occ_overflow)
    |=> (n_unresolved == $past(n_unresolved) + OCC_W'(1));
endproperty
a_occ_inc : assert property (p_occ_launch_only_increments);
 
property p_occ_resolve_only_decrements;
  @(posedge clk) disable iff (!rst_n)
  (np_resolve && !np_launch && !occ_underflow)
    |=> (n_unresolved == $past(n_unresolved) - OCC_W'(1));
endproperty
a_occ_dec : assert property (p_occ_resolve_only_decrements);
 
// The arm that two independent if-statements get wrong.
property p_occ_simultaneous_unchanged;
  @(posedge clk) disable iff (!rst_n)
  (np_launch && np_resolve) |=> $stable(n_unresolved);
endproperty
a_occ_same_cycle : assert property (p_occ_simultaneous_unchanged);
 
property p_occ_idle_unchanged;
  @(posedge clk) disable iff (!rst_n)
  (!np_launch && !np_resolve) |=> $stable(n_unresolved);
endproperty
a_occ_idle : assert property (p_occ_idle_unchanged);
 
// LIVE OCCUPANCY — P6b: a resolve with nothing outstanding is illegal in the
// teaching model. It is REPORTED rather than wrapped, and the counter holds.
property p_occ_no_underflow;
  @(posedge clk) disable iff (!rst_n)
  occ_underflow |-> (occ_error && $stable(n_unresolved));
endproperty
a_occ_underflow_flagged : assert property (p_occ_no_underflow);
 
// LIVE OCCUPANCY — P6c: occupancy stays within the modeled pool. This bounds
// the LOCAL counter against the LOCAL pool size; it asserts nothing about any
// PCIe-defined outstanding limit.
property p_occ_within_capacity;
  @(posedge clk) disable iff (!rst_n)
  n_unresolved <= OCC_W'(IDS);
endproperty
a_occ_bounded : assert property (p_occ_within_capacity);
 
// LIVE OCCUPANCY — P6d: reset clears it.
property p_occ_reset_clears;
  @(posedge clk) !rst_n |=> (n_unresolved == '0);
endproperty
a_occ_reset : assert property (p_occ_reset_clears);
 
// CONSERVATION — P7: an orphan never resolves anything. Chapter 10.2's rule
// at flow scope: a Completion that matched nothing changes no lifecycle.
property p_orphan_changes_nothing;
  @(posedge clk) disable iff (!rst_n)
  ev_orphan |-> (!ev_match && !ev_resolve);
endproperty
a_orphan_inert : assert property (p_orphan_changes_nothing);
 
// ORDERING — P8: a key is not relaunched while its previous transaction is
// unresolved. Chapter 10.4 section 5's hazard, checked at flow scope with
// the scoreboard's own bookkeeping rather than the design's.
generate for (genvar k = 0; k < IDS; k++) begin : g_no_reuse
  property p_no_relaunch_while_live;
    @(posedge clk) disable iff (!rst_n)
    (ev_launch && (ev_launch_id == k[ID_W-1:0]) && !ev_launch_posted)
      |-> !sb_launched[k];
  endproperty
  a_no_key_reuse : assert property (p_no_relaunch_while_live);
end endgenerate
 
// LIVENESS — P9: a matched transaction is eventually resolved. DEPENDS ON
// the consumer-fairness assumption above; a client that never drains holds
// the result forever and no property here can change that.
property p_match_eventually_resolves;
  @(posedge clk) disable iff (!rst_n)
  ev_match |-> s_eventually (ev_resolve);
endproperty
a_match_resolves : assert property (p_match_eventually_resolves);
 
// SAFETY — P10: no observed event carries an out-of-range key.
property p_ids_in_range;
  @(posedge clk) disable iff (!rst_n)
  (ev_launch  |-> (ev_launch_id  < IDS))
  and (ev_match   |-> (ev_match_id   < IDS))
  and (ev_resolve |-> (ev_resolve_id < IDS));
endproperty
a_ids_valid : assert property (p_ids_in_range);

P3 is the property that makes the two flows one verification problem. Everything else in this list is about non-posted transactions; P3 states what a posted one must not do — no tracking state, no match — so a design that accidentally allocated an entry for a Memory Write fails here rather than leaking silently until the pool empties.

P4 is shown twice deliberately, and the second form is the useful one. The first is tempting and weak: "some match happened recently" is satisfied by a match for a different transaction. The per-key form asks the question that matters — was this key matched before it was resolved — and it is the difference between a property that catches key confusion and one that does not.

P8 is the flow-scope statement of the batch's central hazard, and it is checked against the scoreboard's own bookkeeping. Asserting it against the design's busy_mask would be asking the design whether the design is right.

P9 is the only liveness property here and it names its dependency. A matched transaction whose result the client never takes stays unresolved forever, and that is a client problem, not a Transaction Layer bug. Writing the property without the assumption would report the client's back-pressure as a protocol failure.

11. Verification

Monitors observe: the local request handshake; launch with its key and class; the far-side arrival and target acceptance where a model provides them; Completion emission at the Completer; Completion arrival, match and orphan at the Requester; local result delivery; and the free.

The scoreboard is §8's, and it is independent by construction — it maintains its own per-key lifecycle state, its own key-reuse check and its own counters, reading none of the design's tables. §10's P8 and the scoreboard's on_launch check the same hazard from two directions.

Full-flow scenarios

  • One posted write, end to end. Verify the lifecycle stops at LIFE_AT_TARGET and that no tracking state was allocated (P3).
  • One non-posted read, end to end. Verify all six states are visited in order and the key returns to idle.
  • Several non-posted reads outstanding. Verify each key's lifecycle advances independently.
  • Completions returning out of issue order. Verify correlation is unaffected and every key completes (Chapter 10.2 §4).
  • Posted and non-posted interleaved. Verify the two lifecycles coexist and that n_launched_p and n_launched_np track separately.
  • Target stalled. Hold the Completer's resource not-ready. Verify transactions accumulate at LIFE_AT_TARGET and the scoreboard's final report names that state.
  • Return path stalled. Hold the Completion path back-pressured. Verify accumulation at LIFE_COMPLETING.
  • Local result consumer stalled. Verify accumulation at LIFE_MATCHED, n_unresolved climbing toward capacity and staying there rather than oscillating, and — crucially — that launching eventually stops as the pool exhausts (Chapter 10.4 §12).
  • The four live-occupancy arms. Launch alone, resolve alone, both in one cycle, and neither. Verify the same-cycle case leaves n_unresolved unchanged (P6a) — the arm two independent if statements get wrong, and the one a directed test without coincident events never reaches.
  • An illegal resolve. Drive a resolve with the pool empty. Verify occ_error sets and stays set, and that the counter holds rather than wrapping (P6b).

Negative scenarios

  • Orphan Completion. Inject a Completion for a key that is idle. Verify it changes no lifecycle (P7), increments n_orphan, and does not free anything.
  • Completion for a posted key. Inject one. Verify the scoreboard reports it explicitly — this is a case a checker without the posted/non-posted branch would silently accept.
  • Lost forward Request. Drop a launched Request before the Completer sees it. Verify the transaction stalls at LIFE_LAUNCHED and the final report names that state. This is the test that proves the report is useful, because it produces exactly the symptom §12's first scenario describes.
  • Duplicate Completion after resolution. Verify the second is an orphan and does not re-enter the lifecycle (P5).
  • Key relaunched while unresolved. A deliberate violation. Verify both the scoreboard's on_launch check and P8 fire.
  • Reset mid-flight. Verify local state clears, and that a Completion arriving afterwards for a pre-reset key is an orphan rather than resolving a new transaction. Label the expectation carefully: local reset does not un-issue a Request already on the fabric (Chapter 10.4 §8).

What the report says, and what it means

The end-of-test message names a state; each state maps to exactly one interval of §7's boundary list. This mapping is the reason the scoreboard is worth building — it converts "the test hung" into a two-boundary answer with no further investigation.

Final state reportedThe transaction got as far asSuspect boundariesMost likely cause
LIFE_LAUNCHEDthe Request left the Requester4forward transport, routing, or a Completer that never claimed it
LIFE_AT_TARGET (non-posted)the Completer accepted it5, 6the target operation stalled, or no Completion was produced
LIFE_COMPLETINGa Completion was emitted7, 8return transport, or correlation failed at the Requester
LIFE_MATCHEDthe Completion matched its entry9, 10the local client is not draining, or the terminating indication never asserted
LIFE_AT_TARGET (posted)delivered — this is not a failurea posted lifecycle correctly ends here

The last row is why the posted branch in legal_step matters. Without it, every successful posted write would be reported as a stalled transaction, and a report that cries wolf on correct behaviour is a report nobody reads.

Coverage should include: every lifecycle state entered and left; both classes launched; every key used and reused; Completions in order, reverse order and interleaved; each of the three stall points exercised; and every negative scenario producing its expected report.

12. Debugging

Each scenario below is resolved by §7's method rather than by re-deriving the chapter that owns the mechanism.

The Requester holds an outstanding entry, but the Completer never saw the Request

Boundaries 1–3 proven, boundary 4 disproven. The interval is one boundary wide before you do anything else, which is the method working.

What that interval contains: the outbound path from the Requester's Transaction Layer to the Completer's — the Data Link boundary, the Link, every switch hop, and every routing window along the way. This is a forward-transport problem and nothing at the Requester will explain it, because the Requester's evidence already ends at boundary 3.

The productive next observations, in cost order: does a posted write to the same target arrive (§7's third shortcut — if it does, routing and permission are proven and the problem is specific to this Request); does the Link show errors; do the parent routing windows contain the address (Chapter 9.5 §9).

The Completer serviced the Request, but the outstanding entry never frees

Boundaries 4–6 proven, boundary 10 disproven. The interval is 7–9, and one observation splits it.

Did a Completion arrive at the Requester at all?

No → the interval is boundary 7. The Completion was emitted and did not return: a return-path transport problem, or it was never actually emitted and the Completer's own instrumentation is misleading.

Yes, as an orphan → the interval is boundary 8. Correlation failed: a wrong decoded key, an entry freed early, or a key recycled (Chapter 10.2 §15).

Yes, matched → the interval is boundary 9 or 10. The result is stuck at the local consumer or the free never fired — check whether cpl_last ever asserted (Chapter 10.4 §12).

A posted write reaches the Completer's Transaction Layer but not the target block

Boundaries 1–6 of §2's table proven, boundary 7 disproven. The transaction arrived and was classified; it did not reach the resource.

Which puts the fault inside the Completer, in one of two places: classification and dispatch (Chapter 10.1 §12 — was it routed to the memory consumer or somewhere else), or the resource front end (Chapter 9.6 §14 — Memory Space Enable, window membership, the miss output).

The observation that splits them: watch the dispatch valids. Exactly one asserts per received transaction, and which one tells you immediately whether this is a classification fault or a decode fault.

The read result arrives but the wrong client receives it

Every boundary through 8 proven — the data is correct and it matched an entry. The fault is at boundary 9, and boundary 9 has exactly one input: the retained context.

Three causes, all in Chapter 10.2 §15's list: a lookup indexing error, a key reused while its first transaction was outstanding, or a router steering on the Completion rather than on the retrieved context.

What this chapter adds is the elimination, not the diagnosis. Arriving at "boundary 9, retained context" without instrumenting the data path is the whole value of having walked the boundaries in order.

13. Common Misconceptions

  • "A transaction is complete when the Request enters the Link." For a non-posted Request the operation is "not considered complete until after the Completion returns." Entering the Link is boundary 3 of ten.
  • "A Data Link acknowledgement means the non-posted Request resolved." It means a packet crossed one Link intact. It is a different mechanism at a different layer with a different scope, and it reports nothing about the operation (Chapter 10.3 §4).
  • "The Completer must hold the fabric while it services the Request." It must not — that is what a split transaction avoids. The Request is delivered, the fabric is free, and the Completion travels later as its own transaction.
  • "The outstanding entry should free when the Request is transmitted." Transmission transfers ownership of the packet; the Sequence ends at the Completion. Freeing at transmit discards the result or corrupts an unrelated operation (Chapter 10.4 §3).
  • "All TLPs follow the same lifecycle." A posted transaction's lifecycle has eight boundaries and no return half; a non-posted one has twelve. §5's table is the comparison.
  • "A posted write has no state anywhere." It occupies outbound buffering until each successive stage takes it (§2), and it consumes flow-control resources. What it does not need is state that survives the handoff.
  • "A non-posted transaction is exactly one packet in each direction." A Sequence is "a single Request and zero or more Completions," and a Read Request may be answered by more than one (Chapter 10.2 §7).
  • "A Sequence is the same thing as a packet." A Sequence is the logical transfer; packets are what carry it. Chapter 11.1 makes that distinction the basis of a whole module.
  • "Completions return in issue order." Completions for distinct Requests may return in either order. Correlation is by identity, never by position.
  • "Debugging starts with packet fields." It starts with boundaries. Packet contents matter once you know which boundary failed — and §7's method usually narrows that to one or two before any packet is examined.

14. Understanding Check

15. Module 10 Complete

Five chapters, and the Transaction Layer's semantics are now closed.

10.1 Requests — what the layer owns, what a Request is, the four address spaces, and who may issue which kinds.

10.2 Completions — the return half, why correlation is by identity rather than arrival order, and what a Requester must retain.

10.3 Posted Transactions — ownership that ends going out, and the difference between "no Completion" and "no consequences."

10.4 Non-Posted Transactions — ownership that ends coming back, correlation-ID safety, and outstanding depth as a throughput ceiling.

10.5 Transaction Flow — the whole lifecycle, the ownership table, and the boundary method.

The through-line: an operation is created, handed across a series of boundaries, serviced somewhere else, and — if it asked a question — answered by a transaction that has to find its way back to state the asker kept. You can now reason about a PCIe transaction completely without knowing a single header bit.

16. What's Next

That last sentence is also the setup for what follows.

Everything in Module 10 has been about what an operation means: who initiates it, who answers it, what must be remembered, and where ownership transfers. At no point did it matter how any of it was encoded — the packets were black boxes carrying semantics.

Chapter 11.1 — TLP Overview opens the box. What a Transaction Layer Packet actually is, why a transaction and a TLP are not the same object, what structural regions a packet has, and why several packet categories exist. Chapter 11.2 — TLP Structure then details how a TLP is organised — header forms, optional payload, optional integrity information, and the arithmetic of packet length. Chapter 11.3 decodes the header fields themselves, and the rest of Module 11 takes payloads, routing, attributes and the full packet taxonomy.

The idea to carry forward: a transaction is a lifecycle across boundaries; a TLP is the object that crosses one of them.