Skip to content

PCIe · Module 18

L0 — The State in Which Everything Else Is Finally Allowed to Work

L0 is not “nothing happening”. It is the state in which the physical Link contract holds and every other protocol mechanism is finally permitted to do its normal job — and it never means a packet can actually move.

Every chapter of Module 18 so far has been about establishing something: presence, communication, a configuration, or a repair.

L0 is what all of it was for.

And it is the state most often described in one word — normal — which teaches nothing. An engineer who knows "L0 means normal" cannot answer why a Link sitting in L0 is moving no data, which is one of the most common questions in PCIe bring-up.

What exactly becomes legal when a Link reaches L0, what must remain perfectly still while packets flow, and what does L0 conspicuously fail to guarantee?

1. The Verified Substates

2. L0 Is Not Device D0

3. The LTSSM Does Not Sleep in L0

§1's substate table is the evidence, and it is unambiguous.

Two of L0's four substates are about leaving it: L0_TX_EL_IDLE and L0_TX_IDLE_MIN, both mapped to "Tx_L0s.Entry, L1.Entry, or L2.Entry."

So the machine in L0 is doing at least three things at once. It is permitting normal traffic. It is monitoring for the fifteen conditions §1's l0_to_rec register enumerates. And when a low-power state is to be entered, it begins that entry — announcing it to the partner and guaranteeing minimum idle timing — while still in L0.

§1's ltssm_status register exposes pl_link_up and dl_link_up separately. That separation is a design decision worth understanding, because engineers routinely ask for "link up" as though it were one thing.

pl_link_updl_link_up
LayerPhysicalData Link
Meansthe physical Link is establishedthe Data Link Layer is operational
Requirestraining complete, configuration committedflow-control initialization complete (16.1)
Established byChapters 18.218.4Chapter 16.1's InitFC sequence

The interesting state is the one between them. pl_link_up rises when the Physical Layer completes; dl_link_up cannot rise until flow-control initialization has exchanged InitFC DLLPs in both directions and both ends have credits to work with.

So there is a real window in which the Physical Layer is up and no TLP can be sent — not because anything is wrong, but because the Data Link Layer has not finished its own bring-up.

Reading one bit and calling it "link up" therefore produces two distinct false diagnoses. Reading only pl_link_up says the Link is ready when TLPs still cannot flow. Reading only dl_link_up reports the Link down when the physical Link is perfectly healthy and the fault is in flow-control initialization — a completely different investigation, one layer up.

5. What Must Hold Perfectly Still

In continuous L0 residency, four things must not move:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
active_speed       the negotiated signalling rate
active_width       the number of lanes in the Link
active_lane_map    which lanes, and their logical order
active_config_valid

Every one of them is a consumer-facing inputChapter 17.1 §10's distributor and Chapter 17.2 §7's recombiner act on them continuously, on every packet.

And they cannot change while in L0, because the mechanism that changes them is Recovery (Chapter 18.5), and Recovery is a different state. A design in which the operating configuration changes without leaving L0 has a serious ownership bug — §19 treats that observation as a red alert rather than as a curiosity.

Which is why §12's snapshot exposes only committed state. Candidate and provisional values from Configuration or Recovery must never appear here; that is Chapter 18.4 §8's law, and L0 is where its violation costs the most, because L0 is the only state in which anyone is reading it.

§1's source is direct: in L0, "All PCI Express Transactions and operations are enabled."

That covers the two kinds of protocol traffic this curriculum has built: TLPs (Module 11) — Requests and Completions carrying actual work; and DLLPs (Module 14) — ACK/NAK, flow-control updates and power-management messages.

7. L0 Does Not Mean a Packet Can Move

8. Replay Happens in L0

A companion to §7, and a common false alarm.

When a TLP is not acknowledged, the Data Link Layer replays it (Chapter 15.1). The Link stays in L0 throughout. Replay is a Data Link mechanism operating over a working physical Link — it is what the reliability layer is for.

So an implementation that forced Recovery on a replay would be breaking a working Link to fix a problem the Data Link Layer had already handled — and it would turn a recoverable single-packet event into a full traffic outage.

The one place they connect is the escalation Chapter 18.5 §1 sources: repeated failure — "the original and three replay TLP transmissions" — is promoted to a Recovery entry, because four failures in a row is evidence about the Link rather than about the packet.

The distinction for a debug log: occasional replays with the Link in L0 are the reliability layer working. Replays followed by a Recovery entry are one story with one physical root cause.

9. An Idle L0 Is Still L0

A Link in L0 may have nothing to transmit for long periods. It is still in L0, and §6 established that it is still signalling.

This is the observation that motivates the next chapter. If the Link is idle and the transmitter is nevertheless fully powered, driving a serial stream to nobody, that is energy spent for no transport — and on a real system, links are idle far more often than they are busy.

Chapter 18.7 is the response: quiet one direction of the transmitter during idleness, cheaply enough that resuming costs very little. And §1 already showed the seamL0_TX_EL_IDLE and L0_TX_IDLE_MIN are how that departure begins, from inside L0.

10. L0 and Its Exits

L0 as the central operational state. Configuration and Recovery lead into L0. From L0 the link may go to Recovery for repair or change, to Tx L0s for shallow low power, to L1 for deeper low power, or to Disabled and Hot Reset when directed. Recovery and the low power states return to L0.fromConfigurationL0 -normaloperationRecoveryTx L0sL1Disabled/ HotResetcommittedcommittedrepair or changerepair or changerecoveredrecoveredtransmit idletransmit idlewakewakeextended idleextended idlewakewakedirecteddirected
Figure 1 — L0 as the operational centre of the LTSSM, with the exits this curriculum can source. Recovery, the low-power states and the directed states are all reached from L0; the fifteen distinct reasons enumerated in §1's l0_to_rec register are what §16's exit-reason capture records. Only L0 is taught here — every destination state's internals belong to its own chapter, and this diagram deliberately shows no substate detail and no entry criteria.

Three things to read out of the figure.

Every arrow out of L0 is bidirectional except one. Recovery and the low-power states return; Disabled and Hot Reset do not — they are destinations, not detours.

L0 is reached from exactly two places: initial Configuration, and Recovery. A Link arrives in L0 either newly built or newly repaired, and §16's exit-reason capture is what makes the difference visible after the fact.

And the fifteen exit reasons in §1's register all collapse into these few arrows. "Which state did we go to" is a much smaller question than "why did we leave" — which is precisely why recording the state transition is not enough.

11. A Trace

Internal teaching signals, not PCIe pins. A packet is offered, stalls, and the Link leaves L0 before it can be transferred.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
step               1     2     3     4     5     6     7     8     9    10
ltssm_in_l0        0     1     1     1     1     1     0     0     0     1
link_operational   0     1     1     1     1     1     0     0     0     1
 
link_up            0     1     0     0     0     0     0     0     0     1
link_down          0     0     0     0     0     0     1     0     0     0
l0_entry_count     0     1     1     1     1     1     1     1     1     2
cycles_in_l0       0     1     2     3     4     5     5     5     5     6
 
up_valid           0     0     1     1     1     1     1     1     1     1
down_ready         0     0     0     0     1     0     0     0     0     1
up_ready           0     0     0     0     1     0     0     0     0     1
down_valid         0     0     1     1     1     1     0     0     0     1
 
exit_reason       --    --    --    --    --    --    REC   REC   REC   REC

Read step 2. L0 is entered. link_up pulses for exactly one cycle — and note it does not pulse again at steps 3, 4, 5, even though the Link is continuously in L0. §14's whole job.

Read steps 3–4. A packet is offered, downstream is not ready. up_ready stays low, down_valid stays high. No transfer; the packet is still upstream's.

Read step 5. Downstream is ready. Both sides see a transfer in the same cycle — the only cycle in this trace where a packet actually moves.

Read steps 6–7, the interesting part. At step 6 a second packet is offered and downstream is not ready. Then at step 7 the Link leaves L0 with that packet still offered.

down_valid drops to 0 because the gate closed. up_ready was already 0. So no transfer happened on either side — the packet is still upstream's, intact. §13 is why, and §18 enumerated the alternative exhaustively.

Read cycles_in_l0 at steps 6 through 9. It stops at 5 and holds. Residency counts residency, and the counter that keeps incrementing outside L0 is mutation 3.

And read l0_entry_count at step 10. It becomes 2 — one increment for the re-entry, not one per cycle of residency.

12. RTL — Operational Snapshot

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. The operational view of the Link.
// EVERY OUTPUT COMES FROM COMMITTED STATE. Candidate values from
// Configuration or Recovery must not appear here -- L0 is the only state
// in which anything is reading this, so a leak costs the most here
// (section 5).
package l0_pkg;
 
  parameter int MAX_LANES = 16;
  parameter int WIDTH_W   = 5;
  parameter int RATE_W    = 3;
 
  // Normalized L0 exit reasons. SECTION 1's l0_to_rec register is the
  // model: a real implementation records WHY L0 was left, because the
  // destination state alone does not say. These are normalized kinds --
  // the exact trigger list is implementation- and generation-dependent.
  typedef enum logic [2:0] {
    XR_NONE      = 3'd0,
    XR_RECOVERY  = 3'd1,
    XR_L0S       = 3'd2,
    XR_L1        = 3'd3,
    XR_DISABLED  = 3'd4,
    XR_HOT_RESET = 3'd5,
    XR_OTHER     = 3'd6
  } exit_reason_e;
 
endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import l0_pkg::*;
 
// SYNTHESIZABLE. Observability only -- this block decides nothing.
module l0_snapshot (
  input  logic clk,
  input  logic rst_n,
 
  input  logic ltssm_in_l0,          // from the LTSSM (Chapter 18.1)
 
  // ---- COMMITTED configuration only (Chapters 18.4, 18.5) ----------------
  input  logic                 active_config_valid,
  input  logic [RATE_W-1:0]    active_speed,
  input  logic [WIDTH_W-1:0]   active_width,
  input  logic [MAX_LANES-1:0] active_lane_map,
 
  output logic                 link_operational,
  output logic [RATE_W-1:0]    op_speed,
  output logic [WIDTH_W-1:0]   op_width,
  output logic [MAX_LANES-1:0] op_lane_map
);
 
  // ==================================================================
  // BOTH CONDITIONS, NOT EITHER.
  //
  // In L0 without a committed configuration, there is nothing to operate
  // WITH. With a committed configuration but not in L0, there is no
  // permission to operate. Reporting operational on one of the two is a
  // claim the design cannot support.
  // ==================================================================
  assign link_operational = ltssm_in_l0 && active_config_valid;
 
  // Outputs are FORCED TO ZERO when not operational, rather than passed
  // through. A stale width read during Recovery would describe a Link
  // that is currently being renegotiated (section 18, mutation 6).
  assign op_speed    = link_operational ? active_speed    : '0;
  assign op_width    = link_operational ? active_width    : '0;
  assign op_lane_map = link_operational ? active_lane_map : '0;
 
endmodule

Classification: synthesizable, purely combinational.

Architecture. A gated view, requiring both L0 and a committed configuration.

Forcing the outputs to zero rather than passing them through is the decision worth defending: a consumer that samples this during Recovery gets an obviously-invalid answer instead of a plausible stale one. A width of zero is unmistakable; a stale x8 is not.

Failure — three. link_operational = ltssm_in_l0 alone reports operational on a Link with no configuration. Passing the configuration through unconditionally exposes stale values outside L0. And sourcing from candidate registers reintroduces Chapter 18.4 §8's whole failure class in the one state where anyone is reading.

13. RTL — Normal Traffic Gate

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import l0_pkg::*;
 
// SYNTHESIZABLE. The gate that decides whether normal packet traffic may
// cross. This is Chapter 17.3 / 18.1's contract, in the state where it
// finally matters -- and section 18 enumerated its whole truth table.
//
// IT IS STATELESS. It holds no packet, so on an L0 exit there is nothing
// half-owned to resolve.
module l0_traffic_gate #(
  parameter int W = 256
) (
  input  logic         link_operational,   // section 12
 
  // ---- Upstream (the packet source) --------------------------------------
  input  logic         up_valid,
  input  logic [W-1:0] up_payload,
  output logic         up_ready,
 
  // ---- Downstream (toward the PHY) ---------------------------------------
  output logic         down_valid,
  output logic [W-1:0] down_payload,
  input  logic         down_ready
);
 
  // ==================================================================
  // BOTH DIRECTIONS ARE GATED. This is the entire lesson.
  //
  //   down_valid = up_valid && operational    -- offer only when legal
  //   up_ready   = down_ready && operational  -- ACCEPT only when legal
  //
  // Gating only down_valid is the classic bug (section 18): upstream sees
  // valid && ready and considers the packet transferred, while downstream
  // never saw a valid. The packet is destroyed with no error anywhere.
  //
  // Section 18 enumerated all 8 input combinations: with both gated, 0
  // asymmetric transfers. With only down_valid gated, exactly 1 -- and
  // it is a reachable, ordinary combination.
  // ==================================================================
  assign down_valid   = up_valid  && link_operational;
  assign up_ready     = down_ready && link_operational;
 
  // Payload is a pass-through. It is stable while up_valid is held,
  // because the SOURCE holds it -- this block introduces no storage and
  // therefore no ownership question.
  assign down_payload = up_payload;
 
endmodule

Classification: synthesizable, purely combinational.

Architecture. Two gated signals and a pass-through — and its correctness is entirely in the symmetry.

Verified exhaustively (§18): across all 8 combinations of (link_operational, up_valid, down_ready), the correct gate produces 0 cases where one side sees a transfer and the other does not. The version gating only down_valid produces exactly 1 — and it is not exotic: up_valid = 1, down_ready = 1, link_operational = 0, which is what happens every single time the Link leaves L0 with a packet offered and downstream ready.

Why statelessness is the right choice here. A gate with a pipeline register would hold a packet that is neither upstream's nor downstream's when the Link exits L0, and something would have to decide what happens to it. With no storage, the question does not arise: a transfer either completed before the exit or did not happen at all.

Failure — three. Gating only down_valid destroys packets (§18's counterexample). Gating only up_ready offers a valid packet to a PHY that must not transmit. And adding a register without an ownership contract creates a packet with no owner across the exit.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. One-cycle events on L0 entry and exit.
// THESE ARE OBSERVABILITY EVENTS. They feed statistics, debug capture and
// higher-level notification -- they must NOT drive protocol state that
// belongs to the LTSSM or the Data Link Layer.
module l0_events (
  input  logic clk,
  input  logic rst_n,
 
  input  logic link_operational,
 
  output logic link_up,      // one cycle, on entry
  output logic link_down     // one cycle, on exit
);
 
  logic prev_q;
 
  // ==================================================================
  // EDGE DETECTION, not level. The counters in section 15 depend on this
  // being exactly one pulse per transition.
  //
  // A design that asserted link_up whenever link_operational is high
  // would make l0_entry_count count CYCLES OF RESIDENCY, not entries --
  // section 18's mutation 3, and one of the most common metrics bugs in
  // any state machine.
  // ==================================================================
  assign link_up   =  link_operational && !prev_q;
  assign link_down = !link_operational &&  prev_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    // RESET SEEDS prev_q LOW, so coming out of reset into an operational
    // Link produces exactly one link_up -- and reset itself produces no
    // link_down, because there was no L0 residency to end.
    if (!rst_n) prev_q <= 1'b0;
    else        prev_q <= link_operational;
  end
 
endmodule

Classification: synthesizable.

Architecture. One register, two edge detectors.

The reset seeding is deliberate. prev_q starts low, so reset never manufactures a link_down for an L0 residency that never happened — and the first genuine entry after reset produces exactly one link_up.

Failure — three. Level instead of edge turns entry counts into cycle counts. Seeding prev_q high on reset emits a spurious link_down at every reset. And driving protocol state from these pulses puts a statistics signal in the control path, where a missed or doubled pulse becomes a functional failure rather than a wrong number.

15. RTL — Residency and Transition Counters

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Silicon-debug instrumentation.
// A LINK THAT SPENDS ITS LIFE ENTERING AND LEAVING L0 IS THE SYMPTOM
// (section 19), and these counters are how it is seen at all -- the
// oscillation may be far too fast to observe by polling the state.
module l0_counters #(
  parameter int CYC_W = 32,
  parameter int EVT_W = 16
) (
  input  logic clk,
  input  logic rst_n,
 
  input  logic link_operational,
  input  logic link_up,
  input  logic link_down,
  input  logic clear,
 
  output logic [CYC_W-1:0] cycles_in_l0,
  output logic [EVT_W-1:0] l0_entry_count,
  output logic [EVT_W-1:0] l0_exit_count
);
 
  logic [CYC_W-1:0] cyc_q;
  logic [EVT_W-1:0] ent_q, exi_q;
 
  assign cycles_in_l0   = cyc_q;
  assign l0_entry_count = ent_q;
  assign l0_exit_count  = exi_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || clear) begin
      cyc_q <= '0; ent_q <= '0; exi_q <= '0;
    end else begin
      // ==============================================================
      // ALL THREE SATURATE. A wrapping statistics counter is worse than
      // a saturated one: it reports a small number for a large event,
      // and the reader has no way to tell that it wrapped.
      // ==============================================================
      if (link_operational && !(&cyc_q)) cyc_q <= cyc_q + CYC_W'(1);
 
      // ONE INCREMENT PER TRANSITION, driven by the EDGE from section 14
      // -- never by the level.
      if (link_up   && !(&ent_q)) ent_q <= ent_q + EVT_W'(1);
      if (link_down && !(&exi_q)) exi_q <= exi_q + EVT_W'(1);
    end
  end
 
endmodule

Classification: synthesizable (instrumentation).

Architecture. Three saturating counters, residency driven by the level and transitions driven by the edges.

Why saturation rather than wrapping. A statistics counter that wraps reports a small number for a large event — 65,540 entries reads as 4 — and nothing in the reading indicates it happened. Saturation is visibly pinned at maximum, which is honest.

A useful invariant to hold onto: l0_entry_count and l0_exit_count differ by at most one, and which one leads says whether the Link is currently in L0. Any larger divergence means the events are wrong, not the Link.

Failure — three. Counting entries from the level yields residency cycles (mutation 3). Wrapping silently misreports. And failing to count an exit — for instance by only counting exits to Recovery — makes the two counters diverge and destroys the invariant above.

16. RTL — Exit Reason Capture

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import l0_pkg::*;
 
// SYNTHESIZABLE. Record WHY L0 was left.
// SECTION 1's l0_to_rec register is the model: fifteen distinct reasons
// for what is, from the state machine's point of view, one transition.
// The destination state does not say why -- section 1's own evidence
// includes causes as different as a directed speed change and a burst of
// PHY errors, both landing in Recovery.
module l0_exit_reason (
  input  logic clk,
  input  logic rst_n,
 
  input  logic         link_down,       // one-cycle, from section 14
  input  exit_reason_e live_reason,     // normalized, from the LTSSM
 
  input  logic         clear,
 
  output exit_reason_e exit_reason,
  output logic         exit_reason_valid
);
 
  exit_reason_e reason_q;
  logic         valid_q;
 
  assign exit_reason       = reason_q;
  assign exit_reason_valid = valid_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || clear) begin
      reason_q <= XR_NONE; valid_q <= 1'b0;
    end else if (link_down) begin
      // ==============================================================
      // SAMPLED ON THE EXIT EDGE, IN THAT CYCLE.
      //
      // One cycle later the LTSSM has already moved and `live_reason`
      // describes the state it is now in, not the reason it left. That
      // off-by-one is section 18's mutation 10, and it is invisible in
      // any test where the reason happens to persist.
      // ==============================================================
      reason_q <= live_reason;
      valid_q  <= 1'b1;
    end
    // AND THEN FROZEN until explicitly cleared -- same discipline as
    // Chapter 18.5's cause latch, for the same reason.
  end
 
endmodule

Classification: synthesizable (instrumentation).

Architecture. Sample on the exit edge, then freeze until cleared.

The timing is the whole block. live_reason is only meaningful in the cycle of the exit; one cycle later it describes wherever the machine now is. Sampling late records the destination, not the cause — and since destination and cause often agree, the bug passes most tests and fails exactly when the information would have been useful.

Freezing until cleared matches Chapter 18.5 §11's cause latch. A record that updates itself is not a record.

Failure — three. Sampling one cycle late (mutation 10). A level-triggered capture overwrites the first exit with the last. And auto-clearing on re-entry destroys the reason before anyone reads it — the Link is usually back in L0 long before software looks.

17. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SVA over the L0 blocks. These assert the LOCAL contract: what may
// transfer, what must hold still, and that the instrumentation means what
// it says. They assert NOTHING about remaining in L0, about traffic
// existing, or about any packet ever being transferable (section 7).
 
// ---- ENVIRONMENT ------------------------------------------------------
// A1: the source holds its payload while offering.
assume property (@(posedge clk) disable iff (!rst_n)
  (up_valid && !up_ready) |=> (up_valid && $stable(up_payload)));
 
// ---- TRAFFIC ----------------------------------------------------------
 
// P1: NORMAL PACKET TRANSFER IMPLIES L0. The headline property.
property p_transfer_implies_l0;
  @(posedge clk) disable iff (!rst_n)
  (down_valid && down_ready) |-> link_operational;
endproperty
a_xfer : assert property (p_transfer_implies_l0);
 
// P2: and the upstream side too -- this is the half the broken gate
// violates (section 18: 1 of 8 input combinations).
property p_accept_implies_l0;
  @(posedge clk) disable iff (!rst_n)
  (up_valid && up_ready) |-> link_operational;
endproperty
a_accept : assert property (p_accept_implies_l0);
 
// P3: NO PACKET LOSS. Both sides transfer, or neither does. Stated
// symmetrically so it cannot be satisfied by gating one direction.
property p_no_loss;
  @(posedge clk) disable iff (!rst_n)
  (up_valid && up_ready) <-> (down_valid && down_ready);
endproperty
a_noloss : assert property (p_no_loss);
 
// P4: A PACKET OFFERED BUT NOT TRANSFERRED BEFORE AN L0 EXIT REMAINS
// UPSTREAM-OWNED. The ownership law, at the exact moment it is tested.
property p_stalled_packet_survives;
  @(posedge clk) disable iff (!rst_n)
  (up_valid && !up_ready && link_down) |=> up_valid;
endproperty
a_survive : assert property (p_stalled_packet_survives);
 
// ---- CONFIGURATION STABILITY ------------------------------------------
 
// P5: THE OPERATING CONFIGURATION DOES NOT MOVE DURING CONTINUOUS L0
// RESIDENCY. Changing it is Recovery's job, and Recovery is not L0.
property p_config_frozen_in_l0;
  @(posedge clk) disable iff (!rst_n)
  (link_operational && !link_down)
    |=> ($stable(op_speed) && $stable(op_width) && $stable(op_lane_map));
endproperty
a_frozen : assert property (p_config_frozen_in_l0);
 
// P6: operational implies a committed configuration exists.
property p_operational_needs_commit;
  @(posedge clk) disable iff (!rst_n)
  link_operational |-> active_config_valid;
endproperty
a_commit : assert property (p_operational_needs_commit);
 
// P7: width and lane map agree whenever operational.
property p_width_map_agree;
  @(posedge clk) disable iff (!rst_n)
  link_operational |-> ($countones(op_lane_map) == op_width);
endproperty
a_agree : assert property (p_width_map_agree);
 
// P8: NO PROVISIONAL STATE LEAKS. Bound against the candidate registers
// of Chapters 18.4 and 18.5.
property p_no_candidate_leak;
  @(posedge clk) disable iff (!rst_n)
  link_operational |-> (op_lane_map != dut_rec_cand.cand_lane_map)
                    || (dut_rec_cand.cand_lane_map == active_lane_map);
endproperty
a_leak : assert property (p_no_candidate_leak);
 
// ---- EVENTS AND COUNTERS ----------------------------------------------
 
// P9: link_up is EXACTLY ONE CYCLE per entry -- not a level.
property p_up_pulse;
  @(posedge clk) disable iff (!rst_n)
  link_up |=> !link_up;
endproperty
a_uppulse : assert property (p_up_pulse);
 
// P10: and it occurs only on a genuine transition.
property p_up_only_on_edge;
  @(posedge clk) disable iff (!rst_n)
  link_up |-> (link_operational && !$past(link_operational));
endproperty
a_upedge : assert property (p_up_only_on_edge);
 
// P11: NO link_down WHILE CONTINUOUSLY IN L0.
property p_no_spurious_down;
  @(posedge clk) disable iff (!rst_n)
  (link_operational && $past(link_operational)) |-> !link_down;
endproperty
a_nodown : assert property (p_no_spurious_down);
 
// P12: residency counts only residency.
property p_residency;
  @(posedge clk) disable iff (!rst_n)
  (cycles_in_l0 > $past(cycles_in_l0)) |-> $past(link_operational);
endproperty
a_resid : assert property (p_residency);
 
// P13: the entry counter increments once per entry, never per cycle.
property p_entry_count;
  @(posedge clk) disable iff (!rst_n)
  (l0_entry_count > $past(l0_entry_count)) |-> $past(link_up);
endproperty
a_entcnt : assert property (p_entry_count);
 
// P14: entries and exits stay within one of each other.
property p_counter_invariant;
  @(posedge clk) disable iff (!rst_n)
  (l0_entry_count >= l0_exit_count)
    && ((l0_entry_count - l0_exit_count) <= EVT_W'(1));
endproperty
a_inv : assert property (p_counter_invariant);
 
// P15: the exit reason is captured ONLY on an exit, and in that cycle.
property p_reason_on_exit;
  @(posedge clk) disable iff (!rst_n)
  $changed(exit_reason) |-> $past(link_down);
endproperty
a_reason : assert property (p_reason_on_exit);
 
// P16: and it is frozen afterwards.
property p_reason_frozen;
  @(posedge clk) disable iff (!rst_n)
  (exit_reason_valid && !link_down && !clear) |=> $stable(exit_reason);
endproperty
a_rfrozen : assert property (p_reason_frozen);
 
// P17: reset clears the operational claim.
property p_reset;
  @(posedge clk)
  !rst_n |=> (!link_operational && !link_up && !link_down
                                && (cycles_in_l0 == '0));
endproperty
a_reset : assert property (p_reset);

P3 is the property this whole chapter turns on, and it is stated as a biconditional deliberately. "Downstream transfer implies L0" alone is satisfied by the broken gate — because the broken gate does gate down_valid. Only the symmetric form catches an upstream transfer with no downstream transfer.

P5 is the stability property. The operating configuration is an input to two consumers on every packet; it changing inside L0 would mean Recovery's job was done somewhere it should not have been.

And P13 with P14 are the metrics properties. They exist because a wrong statistic is worse than a missing one: it gets believed.

No liveness. "L0 remains" is false by design; "a packet eventually transfers" depends on credits, replay capacity, ordering and arbitration (§7) — none of which this block can see, and none of which it should assume.

18. Verification, Fault Injection, and Model Verification

The gate's truth table was enumerated, not reasoned about.

The traffic gate — exhaustive

All 8 combinations of (link_operational, up_valid, down_ready), checking whether the upstream and downstream sides ever disagree about whether a transfer happened:

ImplementationAsymmetric-transfer cases
§13 as written (both directions gated)0 of 8
up_ready = down_ready (only down_valid gated)1 of 8

And the one failing case is completely ordinary:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
link_operational = 0     the Link just left L0
up_valid         = 1     a packet is being offered
down_ready       = 1     downstream would have taken it
 
  →  upstream sees   up_valid && up_ready   = 1   TRANSFERRED
  →  downstream sees down_valid && down_ready = 0  NOTHING ARRIVED

That is not a corner case; it is what happens every time the Link enters Recovery with a packet offered and the PHY path ready.

Directed tests

  • Enter L0, remain, transfer a packet — verify P1, P2, P3 on every cycle.
  • Downstream stalls for 1, 2 and 50 cycles — verify the payload is stable and no transfer occurs.
  • Exit L0 with a packet offered and downstream ready — verify the packet survives (P4). Required, and the mutation-5 test.
  • Exit L0 with a packet offered and downstream stalled — same verdict, different path.
  • Re-enter L0 — verify exactly one link_up, and that the counters advanced by exactly one.
  • Idle L0 with no traffic for many cycles — verify no link_down, and residency still counting (P11, P12).
  • Credits unavailable while in L0 — verify the LTSSM is unaffected and no exit occurs (§7).
  • Replay in progress while in L0 — verify no Recovery entry is generated locally (§8).
  • Repeated L0 ↔ Recovery cycles — verify the counter invariant (P14) across hundreds of transitions.
  • Configuration change attempted while in L0 — verify it does not happen (P5).
  • Exit reason for each destination, sampled on the exit cycle (P15).
  • Reset during L0 residency, and immediately after an exit (P17).

The scoreboard runs its own L0-residency and packet-transfer model and never reads link_operational, the gate outputs, or the counters — it recomputes expected transfers from (ltssm_in_l0, active_config_valid, up_valid, down_ready) alone.

Mutations

#MutationCaught byLab symptom
1up_ready not gated by L0P2, P3packets vanish exactly when the Link enters Recovery
2down_valid not gatedP1a valid packet offered to the PHY while the Link is training
3link_up driven by the level, not the edgeP9, P13l0_entry_count reports residency cycles; a healthy Link looks like it re-trained millions of times
4exit count misses L0 → RecoveryP14entries and exits diverge; oscillation invisible
5packet accepted in the L0-exit cycleP4one packet destroyed per exit; appears as rare, random data loss
6operational configuration passed through outside L0P5stale width read during Recovery looks authoritative
7candidate configuration sourced instead of committedP8width changes as Recovery renegotiates, while reporting L0
8link_operational = ltssm_in_l0 aloneP6operational reported with no committed configuration
9credit starvation drives an L0 exitP11Link retrains whenever a receiver is briefly full — traffic collapse under load
10exit reason sampled one cycle lateP15reason records the destination, not the cause
11link_up pulses every L0 cycleP9statistics unusable; downstream notification storms
12reset leaves link_operational highP17operational claimed before any configuration exists
13replay forces a local Recovery entryP11a single lost ACK takes the whole Link down (§8)
14counters wrap instead of saturatingreview + P1465,540 entries reads as 4

Same-cycle audit

CaseDeclared resolution
L0 entry and a packet already offeredthe gate opens combinationally; the packet may transfer that cycle — no ownership question, since both sides agree
L0 exit and a packet offeredno transfer on either side (P3, P4); the packet stays upstream
L0 exit and a transfer completingthe gate is combinational, so link_operational low means neither side transferred — there is no half-completed case
link_up and link_down togetherimpossible by construction — they are opposite edges of one register
exit reason and reset in the same cyclereset wins; there is no record to preserve across a reset
entry and exit counters in the same cycleimpossible — the pulses are mutually exclusive

19. Debugging

Symptom → what L0 does and does not tell you → signal → distinguishing experiment.

The LTSSM says L0 and nothing is moving

This is the most common L0 question and the LTSSM has already given its answer: not my layer (§7).

Do not investigate the LTSSM further. Read five things, in this order:

#CheckChapter
1dl_link_up — has flow-control initialization completed?16.1
2Credits for the blocked packet's pool16.5
3Replay-buffer occupancy15.1
4Available Tags for Non-Posted Requests13.3
5Ordering — is something ahead of it blocked?13.4

Start with dl_link_up (§4), because it is the one that produces a completely dead Link rather than a slow one. A Link with pl_link_up high and dl_link_up low is not a physical problem at all — the Physical Layer did its job and flow-control initialization did not complete.

And if credits are the answer, the next question is at the far end, not here: something is not returning them.

Traffic-gate ownership — §18's counterexample, and the correlation with Recovery entries is the diagnosis.

The distinguishing experiment: count accepted packets at the gate's upstream side and packets delivered at its downstream side, and compare the two counts across an L0 exit. If they differ by exactly the number of exits that occurred while a packet was offered, the gate is asymmetric. P3 is that check made permanent.

L0 alone diagnoses nothing here, because none of the plausible causes are visible in the LTSSM.

Candidates, and what distinguishes each: credit starvation — periodic, correlates with receiver buffer occupancy (Chapter 16.6); replay activity — correlates with error counters and DLLP traffic, and each replay costs the round trip (Chapter 15.1); ordering stalls — correlate with Posted-Write bursts (Chapter 13.4); payload size — a fixed per-TLP overhead means small payloads collapse efficiency (Chapter 11.4); and physical error bursts that have not yet escalated to Recovery — visible as replays without Recovery entries.

The one distinguishing experiment that separates the whole group: check whether l0_entry_count is changing. If it is not, the Link never left L0 and every physical explanation is eliminated at once — the collapse is above the Physical Layer.

l0_entry_count reports an enormous number

Almost certainly mutation 3, not a Link that retrained a million times.

The distinguishing experiment: compare it against cycles_in_l0. If they track each other, the counter is counting residency, driven by a level rather than an edge (§14). A genuine oscillation would show entry counts far smaller than residency cycles, and l0_exit_count tracking entries within one (P14).

The speed or width changed while the state stayed L0

Stop and treat this as a serious ownership defect (§5).

The mechanism that changes those values is Recovery, and Recovery is a different state. Observing a change without a state transition means either the configuration is being sourced from candidate registers (mutation 7) or something outside the Recovery path is writing the active configuration.

Both are worse than they look, because Chapter 17.1's distributor is acting on those values continuously, and Chapter 18.5 §20 measured what a mid-flight configuration change does: corruption that surfaces as an LCRC error on a channel with no errors in it.

20. Common Misconceptions

  • "L0 means the device is in D0." Different axes entirely (§2).
  • "L0 means traffic is present." L0 is a permission; an idle Link in L0 is completely normal (§9).
  • "In L0, any queued packet can transmit." Five independent resources can legally block it (§7).
  • "Credit starvation means the Link left L0." It means the receiver has no room — the Link is fine (§7).
  • "A replay means the LTSSM left L0." Replay is a Data Link mechanism operating over a working Link (§8).
  • "The LTSSM does nothing while in L0." Two of its four substates are about preparing to leave (§3).
  • "An LTSSM in L0 can never be entering electrical idle." §1's L0_TX_EL_IDLE is exactly that.
  • "The negotiated configuration can be changed live in L0." Changing it is Recovery's job (§5, P5).
  • "Software controls every L0 exit." §1's fifteen exit reasons include PHY error bursts and partner-directed entry.
  • "No signalling happens during L0 idle." Periodic ordered sets are load-bearing — their absence is inferred as failure (§6).
  • "link_ready means the packet was transferred." It means a transfer is permitted (P3).
  • "Entering Recovery may silently drop queued traffic." It must not, and §18's counterexample is what happens when it does.
  • "L0 is a device state." It is a Link state, describing two ports (§2).
  • "One link-up bit is enough." pl_link_up and dl_link_up answer different questions (§4).
  • "An L0 entry counter and an L0 residency counter measure the same thing." Mutation 3, and it is one of the most common metrics bugs anywhere.

21. Understanding Check

22. What's Next

L0 is where the Link finally does its job — and where "the state machine is fine" stops being an answer.

It is a permission, not a capability. §1's source says all transactions and operations are enabled; whether any particular packet moves depends on credits, replay capacity, Tags, ordering and arbitration — five resources in four other modules, none visible in the LTSSM.

Four things must hold perfectly still — speed, width, lane map and their validity — because two consumers read them on every packet, and the mechanism for changing them is Recovery, which is a different state.

And the departure begins inside L0. §1's own substate table places Tx_L0s.Entry, L1.Entry and L2.Entry inside the L0 major state, because announcing a quiet period requires a transmitter that is still active.

Chapter 18.7 — L0s picks up exactly there. A Link in L0 with nothing to send is still fully powered, driving a serial stream to nobody — and L0s is the mechanism for quieting one direction of that, cheaply enough that resuming costs almost nothing. The hard parts are the ones §1 already hinted at: which direction sleeps, what a packet arriving during the quiet period is allowed to do, and why "electrical idle" never identifies a state on its own.

The idea to carry forward: being permitted to transmit and being able to transmit are different claims, and only one of them is a Link state.