Skip to content

PCIe · Module 18

L0s — Quieting One Direction Without Losing the Link

L0s lets a transmitter go electrically idle while the Link keeps its negotiated identity, so returning costs a short synchronization sequence instead of a retrain. It is directional, it is not L1, and it is not electrical idle.

Chapter 18.6 ended on an observation: a Link in L0 with nothing to send is still fully powered, clocking a serial stream that carries no work. Real links are idle far more often than they are busy.

But the obvious response — stop transmitting — destroys the thing that took four chapters to build. A receiver whose input goes away loses lock; a partner that stops seeing expected evidence infers a failure (Chapter 18.5 §1). Rebuilding from Detect would cost microseconds to save nanoseconds of power.

How can one direction of a Link go electrically idle to save power, while preserving enough context that returning to normal operation costs a short synchronization sequence rather than a retrain?

1. The Verified States

2. L0s Is Directional, and §1 Is Stronger Than "Directional"

3. L0s Is Not L1

A bounded contrast, built entirely from §1's sourced list. Chapter 18.8 owns L1 — this table exists only to say what L0s is not.

ASPM L0sASPM L1
Clocks and PLLs"Clock & PLLs active; core clock active""Clock & PLLs active; core clock active"
PHY power(not powered down)"Significant portion of PHY powered down"
Transmitter"PHY transmitter in electrical idle""PHY transmitter in electrical idle"
Receiver"PHY receiver in electrical idle"
Who must re-lock"Remote PHY receiver must re-establish symbol lock""Remote and local PHY must re-establish symbol lock"
Depth"Low resume latency""Deeper power savings but longer resume time than ASPM L0s"
Entry"autonomously enters L0s when the transmit side of the link is idle""autonomously negotiates L1 entry with the link partner after an extended period of link inactivity"

Three differences, all sourced, and each one explains the others.

L0s quiets a transmitter; L1 quiets both directions and powers down PHY. That is why L1 saves more.

L0s requires only the remote receiver to re-lock; L1 requires both PHYs to. That is why L1 costs more to leave.

And L0s entry is a local decision"when the transmit side of the link is idle"while L1 is negotiated with the partner "after an extended period." A local decision can be made instantly; a negotiation cannot, which is the deepest reason the two have different latencies.

4. L0s Is Not Electrical Idle

5. Why Leaving L0s Is Cheap

§1's sourced list of what remains true in ASPM L0s is the answer, and it is mostly about what does not happen.

What is retained: "Powered. Clock & PLLs active; core clock active." The Link's negotiated identity — speed, width, lane map, Link number — is untouched, because nothing renegotiated it. And the local transmit path is powered.

What must be rebuilt: "Remote PHY receiver must re-establish symbol lock during L0s exit." One thing, in one direction.

6. FTS — a Short Sequence, Not a Protocol

§1 sources the purpose precisely: NFTS is "the Number of NFTS sets required by the local PHY to recover symbol lock when exiting L0s."

Read what that sentence attributes to what. The number is a property of the local PHY — how much signal it needs to re-acquire lock — not a protocol constant. Different PHYs need different amounts, which is exactly why §1 shows it as a configured value.

And the mechanism is the minimum that solves the problem. The remote receiver lost symbol lock because the signal went away. Restoring lock requires signal with known structure — enough transitions, and a recognisable pattern, for the receiver to re-derive its sampling and find unit boundaries again (Chapter 18.3 §3).

That is all FTS is: transmitted structure whose purpose is to be re-locked onto.

Note what it is not. It is not a negotiation — nothing is agreed. It is not acknowledged. It is not training in the Chapter 18.3 sense, where consecutive qualifying observations build confidence in a Link that has never worked. The Link already works; only lock was lost.

§1's TX_L0S_FTS1 adds the second case: "Transmit additional FTS required by Cfg Register Extended Sync" — a configurable mode in which more is sent than the PHY strictly needs. A deliberately conservative setting, which exists because the cost of an insufficient sequence is a failed exit (§7).

7. A Failed L0s Exit Falls Into Recovery

§1 sources the whole escalation path, and it is worth stating as a chain:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
NFTS Timeout Extend "controls how long the core waits after the
expected L0s exit time before DIRECTING THE LINK TO RECOVERY to
recover from a FAILED L0s EXIT"

And the receive-side machine has a substate for exactly this: RX_L0S_REC"Rx_L0s.FTSWait until LTSSM Major State == Recovery due to Rx L0s exit error."

And Chapter 18.5 §1's cause register has a bit for it: rx_l0s_direct_to_recovery"Recovery was entered from L0 due to failing to receive the complete Rx_L0S FTS exit sequence within the PCIe Spec. allowed timeout period."

Three independent pieces of the same document describing one escalation — which makes this one of the best-evidenced behaviours in Module 18.

8. Policy Is Not Protocol

§1 names the policy parameter exactly: "ASPM L0s TX Entry Time, the desired amount of time for TLP and DLLP transmissions to be idle before L0s TX is entered."

Read the word desired. This is a configured local preference, not a protocol requirement.

ProtocolPolicy
Questionwhat signalling and transitions are legalwhen to choose them
Fixed bythe specificationconfiguration and implementation
Examplehow L0s is entered and exited, FTS's rolehow long idle before entering, how much extra FTS
Changing itbreaks interoperabilitychanges power/latency trade-off
Ownerthe LTSSMthe power-management policy layer

§14's RTL keeps them in separate blocks for this reason. The entry-request owner implements the mechanism — hold the request until accepted, never lose it. Whether to request at all is a policy input, and a design that computed an idle threshold inside the state machine would have hard-coded a tuning parameter into a protocol block.

The practical consequence: a Link that never enters L0s despite long idle periods is very often a policy or configuration finding — ASPM disabled, or an entry time longer than the idle periods — not a PHY failure (§21).

9. Where ASPM Fits

ASPM — Active State Power Management — is the framework that decides whether active-state Link power saving is used at all. L0s is one of its mechanisms; L1 is another.

Three facts, bounded to what §1 supports.

It is software-enabled. "When L0s is enabled by power management software, the core autonomously enters L0s…"software enables, hardware then acts autonomously. The per-packet decisions are not software's.

It is a whole-path decision, not a per-device one. §1's quoted Base Specification text says software compares acceptable latency "against the L0s exit latencies reported by all components comprising the data path from this Endpoint to the Root Complex Root Port." One slow component on the path can make L0s unusable for everything behind it.

And ASPM is not only L0s. §1's device supports "ASPM L0s, ASPM L1, L1 PM Substates"Chapter 18.8 owns the L1 side, and the power-management DLLPs that negotiate it were covered in Chapter 15.5.

11. Latency Is the Entire Point

§1's latency encodings make the trade quantitative, and the range is what matters: L0s Exit Latency spans "Less than 64 ns" to "More than 4 μs"a spread of nearly two orders of magnitude across conforming implementations.

Which is why software has to ask rather than assume. The Base Specification text §1 quotes describes exactly that computation: acceptable latency, reported by the Endpoint as "essentially an indirect measure of the Endpoint's internal buffering", compared against the exit latencies of every component on the path.

The buffering framing is the useful insight. An Endpoint can tolerate exit latency only to the extent it has somewhere to put work that arrives while the Link is waking. More buffering, more tolerance — which is why the field is a device property rather than a protocol constant.

And §1 adds one hardware note worth carrying: "Exit latencies may be significantly increased if the PCI Express reference clocks used by the two devices in the link are common or separate." The board's clocking architecture changes the exit cost — so the same silicon on two different boards can have genuinely different L0s economics.

12. The L0s Subtree

The transmit side L0s path. From L0, the machine passes through Tx L0s Entry, which is a substate of L0 itself, to Tx L0s Idle where the transmitter is electrically idle. When there is pending information to transmit, the machine moves to Tx L0s FTS to transmit fast training sequences, then waits for the PHY to indicate exit complete, and returns to L0. An exit that does not complete within the allowed time escalates to Recovery.L0Tx_L0s.Entry(in L0)Tx_L0s.IdleTx_L0s.FTSwait forPHY exitcompleteback toL0Recoverytransmit idletransmit idleEIOS sent, min time metEIOS sent, min time metEIOS sent,min time…pending informationpending informationNFTS transmittedNFTS transmittedPHY exit completePHY exit completeexit timeoutexit timeout
Figure 1 — the transmit-side L0s path, following the canonical substates verified in §1. Tx_L0s.Entry is a substate of the L0 major state, drawn in its own colour to show the seam: the announcement and minimum-idle timing happen before the state changes. Exiting requires a short synchronization sequence, and an exit that times out escalates to Recovery, not Detect. The receive-side machine runs independently (§2). Exact NFTS counts and timers are not published here.

Four things to read out of the figure.

Tx_L0s.Entry is drawn inside L0, in a muted colour, because §1 places it there — the announcement and minimum-idle guarantee happen while the transmitter is still active (Chapter 18.6 §3).

Exit is two steps, not one. Tx_L0s.FTS transmits, and then §1's TX_L0S_TO_L0 waits "for PHY to indicate exit from L0s complete." Sending the sequence is not the same as the PHY being ready — and §17's controller keeps them as separate states for exactly that reason.

The failure edge goes to Recovery, not Detect (§7) — the Link's identity was never in doubt.

And there is no arrow back from Tx_L0s.Idle to L0 directly. Every return passes through the synchronization sequence. There is no free exit, which is what makes the exit latency of §11 unavoidable.

13. A Trace

Internal teaching signals, not PCIe pins. A packet arrives while the transmitter is idle.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
step               1     2     3     4     5     6     7     8     9    10    11
policy_idle_req    1     1     0     0     0     0     0     0     0     0     0
entry_req_valid    0     1     1     0     0     0     0     0     0     0     0
entry_req_ready    0     0     1     0     0     0     0     0     0     0     0
in_l0s             0     0     0     1     1     1     1     1     1     0     0
 
up_valid           0     0     0     0     1     1     1     1     1     1     1
up_ready           0     0     0     0     0     0     0     0     0     0     1
down_valid         0     0     0     0     0     0     0     0     0     0     1
 
wake_pending       0     0     0     0     1     1     1     1     1     1     0
wake_reason       --    --    --    --    01    01    01    01    01    01    01
fts_tx             0     0     0     0     0     1     1     0     0     0     0
phy_exit_done      0     0     0     0     0     0     0     1     1     1     1
link_operational   1     1     1     0     0     0     0     0     0     1     1

Read steps 2–3 — the entry handshake. entry_req_valid rises and the macro is not ready. The request is held. At step 3 it is accepted, and policy_idle_req has already droppedbut the request was committed at step 2 and is not withdrawn (§14).

Read step 5 — the whole chapter in one cycle. A packet becomes valid while the transmitter is in L0s.

up_ready stays 0. The packet is not accepted. And wake_pending rises in the same cycle.

Those two facts are not the same thing (§11): generating a wake request is not accepting the packet, and a design that treated them as equivalent would have destroyed it.

Read steps 6–7. FTS is transmitted.

Read step 8. phy_exit_doneand note link_operational is still 0. The PHY finished; the state machine has not yet returned to L0.

Read step 10. link_operational rises. up_ready is still 0 — the gate is combinational off link_operational, and the packet transfers at step 11, six cycles after it arrived.

Those six cycles are the exit latency of §11, and the packet waited through all of them without ever being at risk, because nobody ever accepted it.

14. RTL — L0s Entry Request Owner

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Own one L0s entry request.
// NO WITHDRAWAL. Once the request is issued, it is held until accepted.
// A design that allowed policy to retract it mid-handshake would race the
// acceptor: the macro may accept in the same cycle the withdrawal is
// asserted, leaving the two sides disagreeing about whether L0s was
// entered (section 20, mutation 12).
package l0s_pkg;
 
  // Normalized wake causes. Captured as a BITMASK because several can be
  // true at once -- the same reasoning as Chapter 18.5's cause register.
  typedef struct packed {
    logic local_traffic;   // a packet is pending here
    logic remote_activity; // the partner is waking us
    logic policy_exit;     // policy withdrew L0s eligibility
    logic link_event;      // link-down, reset, directed transition
  } wake_cause_t;
 
  function automatic bit any_wake(input wake_cause_t w);
    return (|w);
  endfunction
 
endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import l0s_pkg::*;
 
// SYNTHESIZABLE. Entry request ownership.
module l0s_entry_owner (
  input  logic clk,
  input  logic rst_n,
 
  // ---- Policy inputs (section 8) -- NOT decided here ----------------------
  input  logic policy_enable,      // ASPM L0s enabled by software
  input  logic tx_idle_eligible,   // idle long enough per the entry time
  input  logic link_operational,   // must be in L0 to leave it
 
  // ---- Anything that makes L0s wrong right now ---------------------------
  input  logic abort,              // link-down, reset request, wake pending
 
  // ---- To the LTSSM / PHY entry path -------------------------------------
  output logic entry_req_valid,
  input  logic entry_req_ready,
 
  output logic entry_committed
);
 
  logic req_q, done_q;
 
  assign entry_req_valid = req_q;
  assign entry_committed = done_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      req_q <= 1'b0; done_q <= 1'b0;
    end else if (abort) begin
      // ==============================================================
      // ABORT IS CHECKED FIRST, and it is the ONLY thing that clears an
      // un-accepted request. Note it cannot clear a request that has
      // already been accepted -- once the macro takes it, entry is
      // happening and the exit path (section 17) is what reverses it.
      // ==============================================================
      req_q <= 1'b0; done_q <= 1'b0;
    end else begin
      if (!req_q && !done_q) begin
        // Issue only when EVERY condition holds. Sampled once, here.
        if (policy_enable && tx_idle_eligible && link_operational)
          req_q <= 1'b1;
      end else if (req_q && entry_req_ready) begin
        // ============================================================
        // ACCEPTED. The request drops on the HANDSHAKE, not on some
        // later "entry done" signal -- the Chapter 18.2 / 18.5 rule.
        // Holding it longer allows a second acceptance of one request.
        // ============================================================
        req_q  <= 1'b0;
        done_q <= 1'b1;
      end
      // AND NOTHING ELSE TOUCHES req_q. In particular, policy_enable or
      // tx_idle_eligible dropping while the request is outstanding does
      // NOT withdraw it -- section 13's trace step 3 is exactly this.
    end
  end
 
endmodule

Classification: synthesizable.

Architecture. Issue once, hold until the handshake, no withdrawal except by abort.

The no-withdrawal rule is the decision worth defending. Policy conditions are transient by nature — an idle timer's verdict changes the moment a packet arrives. If policy could retract a request in the same cycle the acceptor takes it, the two sides disagree about whether L0s was entered, and the resulting state is not recoverable by either.

Abort is the deliberate exception, and it is scoped narrowly: it clears only an un-accepted request. Once accepted, entry is happening, and reversing it is §17's exit path — not a race with the entry path.

Failure — four. Pulsing the request loses it when the acceptor stalls (§10 makes this frequent). Withdrawal on policy change races the acceptor. Dropping on an "entry done" signal rather than the handshake permits double acceptance. And computing the idle threshold here merges policy into protocol (§8).

15. RTL — Electrical-Idle Command Owner

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. The digital wrapper around the PHY's electrical-idle
// control. THIS MODELS NO ANALOG BEHAVIOUR -- entering electrical idle is
// a PHY operation with its own timing, and this block owns only the
// request/acknowledge transaction with it.
module elec_idle_owner (
  input  logic clk,
  input  logic rst_n,
 
  input  logic start,
  input  logic enter,          // 1 = enter idle, 0 = leave idle
 
  // ---- PHY macro ----------------------------------------------------------
  output logic req_valid,
  input  logic req_ready,
  output logic req_enter,
 
  input  logic phy_done,
 
  output logic busy,
  output logic complete,
  output logic unowned_done
);
 
  typedef enum logic [1:0] { S_IDLE, S_ISSUE, S_WAIT, S_DONE } st_e;
  st_e  st_q;
  logic enter_q, unowned_q;
 
  assign req_valid    = (st_q == S_ISSUE);
  assign req_enter    = enter_q;          // CAPTURED at start
  assign busy         = (st_q != S_IDLE);
  assign complete     = (st_q == S_DONE);
  assign unowned_done = unowned_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      st_q <= S_IDLE; enter_q <= 1'b0; unowned_q <= 1'b0;
    end else begin
      unique case (st_q)
        S_IDLE : begin
          if (start) begin
            enter_q <= enter;    // captured, never re-read (18.5's rule)
            st_q    <= S_ISSUE;
          end
          if (phy_done) unowned_q <= 1'b1;   // reported, not consumed
        end
        S_ISSUE : if (req_ready) st_q <= S_WAIT;   // leave on the handshake
        S_WAIT  : if (phy_done)  st_q <= S_DONE;
        S_DONE  :                st_q <= S_IDLE;
        default :                st_q <= S_IDLE;
      endcase
    end
  end
 
endmodule

Classification: synthesizable.

Architecture. The same four-state ownership pattern as Chapter 18.2 §11's detection owner and Chapter 18.5 §12's rate owner — deliberately identical, because the problem is identical: one operation, a macro that may stall, and a completion that cannot be backpressured.

§20 verified this contract exhaustively in Chapter 18.5: over 2,396,744 sequences, zero violations of accepts ≤ starts and results ≤ accepts — and 1,041,280 violations when the request is held until done instead of until acceptance. The same numbers apply here, because it is the same state machine.

Failure — three. Valid held until phy_done — the measured double-issue. enter re-read in S_ISSUE — the PHY commanded in the wrong direction. And consuming an unowned phy_done — a stale completion attributed to a current request.

16. RTL — Wake Latch and Coordinator

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import l0s_pkg::*;
 
// SYNTHESIZABLE. Capture every wake cause and hold it until the exit
// controller takes ownership.
// A WAKE EVENT MAY BE A ONE-CYCLE PULSE FROM THE PHY, and the exit
// controller may be busy. Losing one means a packet waits forever
// (section 20, mutation 4) -- the worst failure in this chapter, because
// nothing times it out at this layer.
module l0s_wake_coord (
  input  logic clk,
  input  logic rst_n,
 
  input  logic in_l0s,
 
  // ---- Wake sources -------------------------------------------------------
  input  logic pending_packet,     // LEVEL: a packet is offered upstream
  input  logic remote_wake,        // PULSE: PHY indicates partner activity
  input  logic policy_exit,        // LEVEL: policy no longer allows L0s
  input  logic link_event,         // PULSE: link-down / directed transition
 
  input  logic exit_taken,         // the exit controller has ownership
 
  output wake_cause_t wake_reason,
  output logic        wake_pending
);
 
  wake_cause_t reason_q;
  logic        pend_q;
 
  assign wake_reason  = reason_q;
  assign wake_pending = pend_q;
 
  // ==================================================================
  // NOTE WHAT IS *NOT* HERE: no path from wake_pending back into
  // up_ready. Section 18's gate reads link_operational only.
  //
  // A design that made up_ready depend on wake logic which depends on
  // up_valid would close a COMBINATIONAL LOOP through the gate
  // (section 20, mutation 13). The break is that the state advances
  // SEQUENTIALLY: up_valid sets wake_pending this cycle, the controller
  // exits over following cycles, and up_ready follows the STATE.
  // ==================================================================
 
  wire wake_now = pending_packet | remote_wake | policy_exit | link_event;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      reason_q <= '0; pend_q <= 1'b0;
    end else if (exit_taken) begin
      // Consumed EXACTLY ONCE, by the controller taking ownership.
      pend_q <= 1'b0;
      // reason_q is NOT cleared -- it stays readable for diagnostics
      // until the next wake overwrites it (section 21).
    end else if (in_l0s && wake_now) begin
      if (!pend_q) begin
        // FIRST wake captures the reason. Later causes accumulate into
        // the same record rather than replacing it, so a wake that had
        // two causes reports two.
        reason_q <= '{ local_traffic   : pending_packet,
                       remote_activity : remote_wake,
                       policy_exit     : policy_exit,
                       link_event      : link_event };
      end else begin
        reason_q.local_traffic   <= reason_q.local_traffic   | pending_packet;
        reason_q.remote_activity <= reason_q.remote_activity | remote_wake;
        reason_q.policy_exit     <= reason_q.policy_exit     | policy_exit;
        reason_q.link_event      <= reason_q.link_event      | link_event;
      end
      pend_q <= 1'b1;
    end
  end
 
endmodule

Classification: synthesizable.

Architecture. A sticky pending flag with an accumulating cause bitmask, consumed exactly once.

Accumulating rather than replacing matters because local and remote wakes genuinely coincide — both ends becoming busy at the same moment is the normal case on a bidirectional workload, and a record that kept only one would misattribute half of them.

And the loop-break is structural. wake_pending never feeds up_ready; the gate (§18) reads link_operational, which is state. The dependency up_valid → wake_pending → controller → state → link_operational → up_ready passes through a register, so it is a sequential path and not a loop.

Failure — four. Not latching a pulsed remote_wake loses it whenever the controller is busy — and nothing at this layer times it out. Replacing the reason instead of accumulating misattributes coincident wakes. Clearing on anything but exit_taken either drops the wake or re-triggers an exit. And feeding wake_pending into up_ready creates the combinational loop.

17. RTL — L0s Residency Controller

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import l0s_pkg::*;
 
// SYNTHESIZABLE. Local control of one L0s episode.
// THESE ARE NOT NORMATIVE PCIe SUBSTATE NAMES. They are an
// implementation-facing coordinator whose phases map onto the canonical
// substates verified in section 1 (Tx_L0s.Entry, Tx_L0s.Idle,
// Tx_L0s.FTS). Section 1 does not publish the entry/exit criteria this
// would need to be a normative encoding.
module l0s_controller #(
  parameter int TO_W = 16
) (
  input  logic clk,
  input  logic rst_n,
 
  input  logic entry_committed,     // section 14
  input  logic idle_cmd_complete,   // section 15
  input  logic wake_pending,        // section 16
 
  input  logic fts_sent,            // the short sequence was transmitted
  input  logic phy_exit_done,       // "wait for PHY to indicate exit complete"
  input  logic exit_timeout,        // NFTS timeout, section 7
 
  input  logic link_down,
 
  output logic in_l0s,
  output logic exit_taken,
  output logic start_fts,
  output logic goto_recovery,       // section 7's escalation
  output logic ready_for_l0
);
 
  typedef enum logic [2:0] {
    S_L0, S_ENTERING, S_LOW_POWER, S_FTS, S_WAIT_PHY, S_DONE, S_RECOVER
  } st_e;
  st_e st_q;
 
  // in_l0s spans entry through exit. NOTE it EXCLUDES S_DONE: the gate
  // must not reopen until the LTSSM has actually returned to L0.
  assign in_l0s        = (st_q inside {S_ENTERING, S_LOW_POWER, S_FTS,
                                       S_WAIT_PHY});
  assign exit_taken    = (st_q == S_LOW_POWER) && wake_pending;
  assign start_fts     = (st_q == S_FTS);
  assign goto_recovery = (st_q == S_RECOVER);
  assign ready_for_l0  = (st_q == S_DONE);
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      st_q <= S_L0;
    end else if (link_down) begin
      // ==============================================================
      // LINK-DOWN DOMINATES EVERY OTHER TRANSITION, including a
      // completed exit. Ignoring it while in a low-power state leaves
      // the controller waiting for a PHY handshake that will never
      // arrive (section 20, mutation 9).
      // ==============================================================
      st_q <= S_L0;
    end else begin
      unique case (st_q)
        S_L0 :
          if (entry_committed) st_q <= S_ENTERING;
 
        S_ENTERING :
          // A wake DURING entry is honoured -- but only after the PHY
          // command has completed. Interrupting a half-issued electrical
          // idle command would leave the macro's state unknown.
          if (idle_cmd_complete) st_q <= S_LOW_POWER;
 
        S_LOW_POWER :
          // WAKE BEATS REMAINING. Declared priority.
          if (wake_pending) st_q <= S_FTS;
 
        // ---- Section 1's Tx_L0s.FTS -------------------------------------
        S_FTS :
          if (exit_timeout)  st_q <= S_RECOVER;   // section 7
          else if (fts_sent) st_q <= S_WAIT_PHY;
 
        // ---- Section 1's TX_L0S_TO_L0 -----------------------------------
        // TRANSMITTING THE SEQUENCE IS NOT THE SAME AS THE PHY BEING
        // READY. Section 12's diagram keeps these separate for this
        // reason, and section 20's mutation 8 is the merged version.
        S_WAIT_PHY :
          if (exit_timeout)     st_q <= S_RECOVER;
          else if (phy_exit_done) st_q <= S_DONE;
 
        S_DONE    : st_q <= S_L0;
        S_RECOVER : st_q <= S_L0;   // the LTSSM takes it to Recovery
        default   : st_q <= S_L0;
      endcase
    end
  end
 
endmodule

Classification: synthesizable (L0s only — not the LTSSM, and not L1).

Architecture. Entry, low power, FTS, wait for the PHY, done — with link-down dominating and a timeout escalating to Recovery.

Two decisions worth naming. S_FTS and S_WAIT_PHY are separate states, because §1 makes them separate substates — "Transmit requested NFTS" and "wait for PHY to indicate exit from L0s complete" are different events, and merging them declares the Link ready before the PHY says it is. And in_l0s excludes S_DONE, so the traffic gate cannot reopen a cycle early.

Failure — four. Merging FTS and the PHY wait opens the gate before the PHY is ready (mutation 8). Including S_DONE in in_l0s — or rather, opening the gate on it — transmits before the LTSSM is back in L0. Ignoring link-down waits forever for a handshake that will not come. And interrupting S_ENTERING mid-command leaves the macro in an unknown state.

18. RTL — Power-Aware Traffic Gate

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. The same gate as Chapter 18.6 section 13, with the same
// contract, driven by a signal that is now false in two situations rather
// than one.
//
// IT IS THE SAME LAW. A packet arriving while the transmitter is in L0s
// must remain UPSTREAM-OWNED -- exactly as a packet arriving during
// Recovery must. Section 20 measured the broken version across every
// arrival time and every L0s window.
module l0s_traffic_gate #(
  parameter int W = 256
) (
  input  logic         link_operational,  // in L0 AND configured (18.6 §12)
 
  input  logic         up_valid,
  input  logic [W-1:0] up_payload,
  output logic         up_ready,
 
  output logic         down_valid,
  output logic [W-1:0] down_payload,
  input  logic         down_ready,
 
  // To the wake coordinator. NOTE THE DIRECTION: this is an OUTPUT of the
  // gate's observation, never an input to up_ready (section 16).
  output logic         pending_packet
);
 
  assign down_valid   = up_valid   && link_operational;
  assign up_ready     = down_ready && link_operational;
  assign down_payload = up_payload;
 
  // ==================================================================
  // A PACKET OFFERED WHILE NOT OPERATIONAL IS A WAKE REQUEST -- AND
  // NOTHING MORE.
  //
  // Generating a wake request is NOT accepting the packet. up_ready
  // stays low; the packet stays upstream's; the wake causes the state to
  // change over the following cycles, and the gate opens because the
  // STATE changed (section 13's trace, steps 5 through 11).
  // ==================================================================
  assign pending_packet = up_valid && !link_operational;
 
endmodule

Classification: synthesizable, purely combinational.

Architecture. Identical to Chapter 18.6 §13, plus one observation output.

The added line is the interesting one, and its direction is the whole safety argument. pending_packet is an output derived from up_valid and the state. Nothing flows back into up_ready — so there is no combinational loop, and, more importantly, no path by which wanting to wake could cause the packet to be accepted.

Verified (§20): across every packet-arrival time and every L0s window in the modelled space — 1,320 configurations — the correct gate loses 0 packets and the ungated version loses 219.

Failure — three. up_ready = down_ready destroys the packet (§20's counterexample). Deriving up_ready from wake_pending closes a combinational loop through the gate. And accepting the packet into a buffer to "hold it during L0s" creates a packet owned by neither side across a state change (Chapter 18.6 §21).

19. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SVA over the L0s blocks. These assert the LOCAL contract: request
// ownership, packet ownership, and wake preservation. They assert NOTHING
// about a wake eventually succeeding, about the partner ever transmitting
// again, or about L0s ever being entered.
 
// ---- ENVIRONMENT ------------------------------------------------------
assume property (@(posedge clk) disable iff (!rst_n)
  (up_valid && !up_ready) |=> (up_valid && $stable(up_payload)));
assume property (@(posedge clk) disable iff (!rst_n)
  remote_wake |=> !remote_wake);     // a pulse
 
// ---- ENTRY OWNERSHIP --------------------------------------------------
 
// P1: the entry request is HELD under stall.
property p_entry_held;
  @(posedge clk) disable iff (!rst_n)
  (entry_req_valid && !entry_req_ready && !abort) |=> entry_req_valid;
endproperty
a_held : assert property (p_entry_held);
 
// P2: and policy changing does NOT withdraw it (section 14).
property p_no_withdrawal;
  @(posedge clk) disable iff (!rst_n)
  (entry_req_valid && !entry_req_ready && !abort && !policy_enable)
    |=> entry_req_valid;
endproperty
a_nowd : assert property (p_no_withdrawal);
 
// P3: ONE REQUEST IS ACCEPTED ONCE.
property p_entry_once;
  @(posedge clk) disable iff (!rst_n)
  (entry_req_valid && entry_req_ready) |=> !entry_req_valid;
endproperty
a_once : assert property (p_entry_once);
 
// P4: the electrical-idle command is held under stall, with a stable
// direction -- the same contract as Chapter 18.5's rate owner.
property p_idle_cmd_held;
  @(posedge clk) disable iff (!rst_n)
  (req_valid && !req_ready) |=> (req_valid && $stable(req_enter));
endproperty
a_cmd : assert property (p_idle_cmd_held);
 
// ---- PACKET OWNERSHIP -- THE CENTRAL PROPERTIES -----------------------
 
// P5: NO NORMAL PACKET TRANSFER WHILE IN L0s.
property p_no_transfer_in_l0s;
  @(posedge clk) disable iff (!rst_n)
  in_l0s |-> !(up_valid && up_ready);
endproperty
a_noxfer : assert property (p_no_transfer_in_l0s);
 
// P6: A PENDING PACKET REMAINS UPSTREAM-OWNED across the whole episode.
property p_packet_survives_l0s;
  @(posedge clk) disable iff (!rst_n)
  (up_valid && in_l0s) |=> (up_valid || $past(up_ready));
endproperty
a_survive : assert property (p_packet_survives_l0s);
 
// P7: no packet loss, stated symmetrically (Chapter 18.6 P3).
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);
 
// P8: A PENDING PACKET IN L0s CREATES A WAKE REQUEST -- and note this is
// a SEPARATE property from P5. Wanting to wake and accepting the packet
// are different things (section 18).
property p_packet_wakes;
  @(posedge clk) disable iff (!rst_n)
  (up_valid && in_l0s) |-> pending_packet;
endproperty
a_wake : assert property (p_packet_wakes);
 
// ---- WAKE PRESERVATION ------------------------------------------------
 
// P9: A WAKE REQUEST CANNOT DISAPPEAR before the exit controller takes it.
property p_wake_held;
  @(posedge clk) disable iff (!rst_n)
  (wake_pending && !exit_taken) |=> wake_pending;
endproperty
a_wheld : assert property (p_wake_held);
 
// P10: and it is consumed exactly once.
property p_wake_consumed_once;
  @(posedge clk) disable iff (!rst_n)
  (wake_pending && exit_taken) |=> !wake_pending;
endproperty
a_wonce : assert property (p_wake_consumed_once);
 
// P11: the wake reason only ACCUMULATES while pending -- a coincident
// second cause is added, never substituted (section 16).
property p_reason_monotonic;
  @(posedge clk) disable iff (!rst_n)
  (wake_pending && !exit_taken)
    |=> ((wake_reason & $past(wake_reason)) == $past(wake_reason));
endproperty
a_rmono : assert property (p_reason_monotonic);
 
// ---- STATE INTEGRITY --------------------------------------------------
 
// P12: L0s NEVER REPORTS NORMAL TRAFFIC ENABLED.
property p_l0s_not_operational;
  @(posedge clk) disable iff (!rst_n)
  in_l0s |-> !link_operational;
endproperty
a_notop : assert property (p_l0s_not_operational);
 
// P13: THE EXIT SEQUENCE MUST COMPLETE BEFORE RETURNING TO L0. Sending
// FTS is not enough -- the PHY must say so (section 17).
property p_exit_needs_phy;
  @(posedge clk) disable iff (!rst_n)
  $rose(ready_for_l0) |-> $past(phy_exit_done);
endproperty
a_phy : assert property (p_exit_needs_phy);
 
// P14: THE NEGOTIATED CONFIGURATION SURVIVES AN L0s EPISODE UNCHANGED.
// This is the teaching contract of section 5: L0s does not renegotiate.
property p_config_survives;
  @(posedge clk) disable iff (!rst_n)
  (in_l0s && !link_down)
    |=> ($stable(active_speed) && $stable(active_width)
                               && $stable(active_lane_map));
endproperty
a_cfg : assert property (p_config_survives);
 
// P15: LINK-DOWN INVALIDATES L0s LOCAL CONTROL.
property p_linkdown;
  @(posedge clk) disable iff (!rst_n)
  link_down |=> (!in_l0s && !entry_req_valid);
endproperty
a_down : assert property (p_linkdown);
 
// P16: entry and exit cannot both be committed in the same cycle.
property p_no_double_commit;
  @(posedge clk) disable iff (!rst_n)
  !(entry_committed && ready_for_l0);
endproperty
a_excl : assert property (p_no_double_commit);
 
// P17: an L0s exit timeout escalates to Recovery, never to L0 (section 7).
property p_timeout_to_recovery;
  @(posedge clk) disable iff (!rst_n)
  (in_l0s && exit_timeout) |=> (goto_recovery || link_down);
endproperty
a_esc : assert property (p_timeout_to_recovery);
 
// P18: reset.
property p_reset;
  @(posedge clk)
  !rst_n |=> (!in_l0s && !wake_pending && !entry_req_valid);
endproperty
a_reset : assert property (p_reset);

P5 and P8 are deliberately separate properties, and their separation is the chapter. P5 says the packet is not accepted; P8 says a wake is requested. A design that satisfied P8 by accepting the packet would violate P5 — and one that satisfied P5 by ignoring the packet entirely would violate P8 and hang forever.

P9 is the one whose violation has no timeout. A lost wake means a packet waits indefinitely, and nothing at this layer notices.

P14 is the teaching contract of §5, stated as an assertion so a design that renegotiated on every L0s exit fails immediately.

No liveness. "A wake eventually succeeds" depends on the PHY and the partner — and §7 shows the protocol's own answer when it does not: a timeout, and escalation to Recovery.

20. Verification, Fault Injection, and Model Verification

The packet-ownership question was simulated across every arrival time, not argued.

Packet ownership across an L0s episode — enumerated

Every combination of packet arrival cycle and L0s window in a 14-cycle model — 1,320 configurations — checking whether the upstream side ever completes a transfer that the downstream side does not see:

ImplementationPacket-loss configurations
§18 as written (up_ready = down_ready && link_operational)0 of 1,320
up_ready = down_ready (ungated)219 of 1,320

The smallest failing case is a packet arriving in the same cycle the transmitter enters L0s — which is not a rare interleaving but the single most likely one, because a packet arriving is exactly the sort of event that coincides with an idle timer expiring.

The entry and electrical-idle owners

Both use Chapter 18.5 §12's state machine, verified there exhaustively over 2,396,744 (start, ready, done) sequences: 0 violations of accepts ≤ starts and results ≤ accepts as written, and 1,041,280 violations when the request is held until done452,512 double-accepts plus 588,768 results with no accepted request.

Directed tests

  • Enter L0s with the acceptor stalling for 1, 2 and 50 cycles — verify the request is held (P1).
  • Policy withdraws while the request is outstanding — verify no withdrawal (P2). Required.
  • Electrical-idle macro stalls — verify hold and direction stability (P4).
  • Stay in L0s with no traffic for many cycles — verify no spurious exit and no transfer (P5).
  • A local packet arrives in L0s — verify up_ready stays low and a wake is generated (P5, P8). Required, and the pair that must both hold.
  • The packet arrives in the same cycle as L0s entry — §20's smallest failing case for the broken gate.
  • remote_wake pulses while the controller is busy — verify it is latched (P9). Required.
  • Local and remote wake in the same cycle — verify both causes are recorded (P11).
  • Exit with FTS sent but phy_exit_done withheld — verify the Link does not return to L0 (P13).
  • Exit timeout — verify escalation to Recovery, not to L0 (P17). Required.
  • Link-down during each phase of the episode (P15).
  • Reset in low-power residency (P18).
  • Repeated L0 ↔ L0s cycles, hundreds of them — §10 says this is the normal case, so it must be the normal test.
  • Traffic immediately after return — verify the first packet transfers and the configuration is unchanged (P14).
  • Policy disabled entirely — verify L0s is never entered (§21's most common false alarm).

The scoreboard runs its own power-state and wake model and never reads in_l0s, wake_pending or the controller's state enum — it recomputes expected packet transfers from (link_operational, up_valid, down_ready) and expected wakes from the raw source signals.

Mutations

#MutationCaught byLab symptom
1L0s treated as L1 (receiver also idled)P14 + scoreboardreceive path loses lock; exits fail and escalate to Recovery
2PHY electrical_idle used directly as the L0s stateP12state follows a PHY output; fires during Recovery's speed change (§4)
3packet accepted while in L0sP5, P7packet destroyed; Completion times out at an innocent completer
4local or remote wake pulse lostP9a packet waits forever — nothing at this layer times it out
5wake reason replaced instead of accumulatedP11coincident local+remote wakes misattributed
6entry request pulsed under acceptor stallP1L0s entry silently skipped; power saving quietly absent
7state enters low power before the PHY command is acceptedP4controller and PHY disagree about whether the transmitter is idle
8return to L0 asserted on fts_sent rather than phy_exit_doneP13first packets after wake corrupt — transmitted before the PHY is ready
9link-down ignored during L0sP15controller waits forever for a PHY handshake that will never arrive
10configuration cleared on every L0s entryP14every wake renegotiates; exit latency becomes a retrain
11remote wake interpreted as a received TLPscoreboarda wake indication injected into the packet stream
12policy allowed to withdraw a committed entryP2entry path and acceptor disagree about whether L0s was entered
13up_ready derived from wake_pendingelaborationcombinational loop through the gate (§16)
14entry/exit counters incremented per residency cycle18.6's P13statistics report cycles as transitions
15in_l0s includes S_DONEP13gate opens one cycle before the LTSSM returns to L0

Same-cycle audit

CaseDeclared resolution
packet arrives in the same cycle L0s entry is acceptedentry proceeds; the packet is not accepted and raises a wake immediately (P5, P8)
wake asserted in the same cycle as entry commitmententry completes to S_LOW_POWER, then exits — interrupting a half-issued PHY command is worse (§17)
local and remote wake togetherboth recorded (P11); neither is preferred
exit_timeout and phy_exit_done togethertimeout is checked first in S_WAIT_PHY; a completion at the boundary of a timeout is not trusted
exit done and link-downlink-down wins (P15)
reset during low-power residencyreset wins; no L0s state survives (P18)
wake pending and policy disabledwake wins — policy governs entry, never the ability to leave

The declared priority throughout: reset / link_down > wake > remain low power. Power saving is never allowed to outrank correctness or a pending obligation.

21. Debugging

Symptom → which machine → signal → distinguishing experiment.

The first question is always which direction (§2). The main LTSSM describes the transmitter; the Rx L0s machine describes the receiver. They disagree constantly and correctly.

Check first whether this is §10 — a Link with no application traffic enters and exits L0s with regularity to carry periodic link-management DLLPs. That is correct behaviour, and reporting it as instability is the most common false alarm in this state.

The number that separates correct from pathological is not the transition count — it is whether the exits are clean. Frequent entries with clean exits is a Link saving power. Frequent entries with rx_l0s_direct_to_recovery escalations (Chapter 18.5 §1) is a Link failing to wake, which is a real defect (see below).

If the exits are clean and latency is still unacceptable, it is a policy question (§8): the ASPM L0s TX Entry Time is short enough that L0s is entered between packets of the same burst, so every burst pays exit latency. The distinguishing experiment is to raise the entry time; if latency improves and power rises, the trade-off was simply set too aggressively — not a hardware fault.

A packet waits forever after L0s entry

Walk the chain; each stage has a different owner:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
up_valid          is the packet actually offered?      -> the source
pending_packet    did the gate observe it?             -> §18
wake_pending      was the wake LATCHED?                -> §16, P9
exit_taken        did the controller take ownership?   -> §17
start_fts         was the sequence transmitted?        -> §17
phy_exit_done     did the PHY finish?                  -> the macro
link_operational  did the state return to L0?          -> §17
up_ready          did the gate reopen?                 -> §18

The first stage that never advances names the owner. pending_packet high with wake_pending low is mutation 4 — a lost wake, and the most dangerous failure in this chapter because nothing at this layer times it out. phy_exit_done never arriving is the macro, and §7's timeout should have escalated to Recovery — if it did not, check the timeout itself.

Suspect the gate opening early — mutations 8 and 15.

The signature is precise: corruption only in the first packets after a wake, clean thereafter, and correlated with L0s exits rather than with load or channel.

Two candidates, distinguished by one signal. If link_operational rose on fts_sent rather than on phy_exit_done, the Link transmitted before the PHY finished exiting (mutation 8). If in_l0s included S_DONE, the gate opened one cycle early (mutation 15). Sample link_operational against phy_exit_done across a wake — P13 is that check made permanent.

A third possibility worth eliminating: insufficient NFTS. §1 warns that "due to high latencies between a PHY's Rx Electrical Idle output and the associated Rx Data it is normally necessary to choose a relatively high NFTS and NFTS Timeout Extend." If the far end's receiver has not finished re-locking, the first symbols are lost — and the distinguishing experiment is to increase NFTS (or enable Extended Sync) and see whether the corruption stops.

Frequent rx_l0s_direct_to_recovery entries

L0s exits are failing (§7) — a specific and unusually well-localized diagnosis.

Three candidates: NFTS too low for the partner's receiver to re-lock in the allowed window; NFTS Timeout Extend too short for the PHY's electrical-idle-exit latency; or a genuine physical problem at the receiving end.

The distinguishing experiment is to raise NFTS and the timeout extend together. If the escalations stop, the configuration was too aggressive — and note that fixes it without touching the channel. If they persist, it is physical, and Chapter 18.3 §16's lock/alignment ladder applies at the far end.

L0s is never entered despite long idle periods

Very often a configuration finding, not a fault (§8, §9).

Check, in order: is ASPM L0s enabled by software? (§1: "When L0s is enabled by power management software…"). Is the advertised ASPM support correct in Link Capabilities? Is the entry time longer than the actual idle periods? And did the whole-path latency check fail — §1's quoted Base Specification text says software compares against "all components comprising the data path", so one slow component anywhere on the path can disable L0s for everything behind it.

The distinguishing experiment is to read the enable state rather than to probe the PHY. A Link that is not permitted to enter L0s is behaving perfectly.

The LTSSM says L0 but the receiver looks idle

Not a contradiction (§2). The main LTSSM controls the transmitter; the Rx L0s machine "operates independently."

A local transmitter busy while the partner's is quiet is the normal state of any asymmetric workload — a device streaming data upward transmits continuously and receives almost nothing. Read the Rx L0s state machine separately, and only treat a mismatch as a fault if the two machines disagree about the same direction.

22. Common Misconceptions

  • "L0s is the same as L1." L0s quiets a transmitter; L1 quiets both directions and powers down PHY (§3).
  • "L0s is a device D-state." It is a Link state (Chapter 18.6 §2).
  • "Electrical idle means L0s." Recovery, L0 and Disable all use it (§4).
  • "In L0s the transmitter is always idle." Tx_L0s.FTS transmits (§4).
  • "The Link must retrain from Detect after L0s." Nothing about its identity became untrue (§5).
  • "L0s clears the negotiated width and speed." It does not, and P14 asserts it.
  • "A packet arriving in L0s can just be dropped." §20's counterexample: 219 of 1,320 configurations.
  • "Generating a wake request means accepting the packet." P5 and P8 are separate properties for this reason.
  • "Wake is software-driven." §1: hardware "autonomously" enters and exits; software only enables (§9).
  • "ASPM means only L0s." §1's device supports ASPM L0s and ASPM L1 (§9).
  • "Both directions necessarily sleep together." Two independent state machines (§2).
  • "FTS rules are identical across generations." The alignment mechanism being restored is generation-specific (§6).
  • "An idle L0 Link automatically means L0s." L0s must be enabled, eligible, and entered (§8).
  • "CDR and equalization state are guaranteed unchanged." §1 states clocks and PLLs stay active and the remote receiver must re-lock; this chapter claims no more (§5).
  • "L0s is just a clock gate." It is a bilateral protocol behaviour with an announcement, a minimum idle time, a synchronization sequence and a timeout that escalates to Recovery (§7).
  • "A Link cycling L0 ↔ L0s constantly is unstable." §1 says an otherwise-idle Link does exactly that, on purpose (§10).
  • "L0s exit latency is a fixed number." §1's encodings span under 64 ns to over 4 μs, and board clocking changes it (§11).

23. Understanding Check

24. Where Module 18 Stands

Four chapters, four different relationships to one Link:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
CONFIGURATION   commits the Link
                lanes become an agreed width, order and identity
 
L0              uses the Link
                every other protocol mechanism is finally permitted to work
 
RECOVERY        repairs or changes the Link
                traffic withdraws, certainty is re-established, a new
                configuration commits atomically -- or nothing does
 
L0s             temporarily quiets part of active operation
                one direction goes electrically idle; the negotiated
                identity survives; returning costs a short sequence

And one law has now appeared five times, in five different places, with the same shape and the same consequence: a gate must gate both valid and ready (17.1, 17.3, 18.1, 18.6 §18, §20 here). Every instance destroys a packet that no layer above can recover, because the destruction happens before the retention point.

A second law has appeared four times: state that multiple consumers depend on must change all at once or not at all (16.5, 17.3, 18.4, 18.5 §15).

Both recur because the underlying situations recur — every state boundary is a place where ownership can be dropped, and every configuration is something more than one consumer reads.

25. What's Next

L0s is the shallow end of Link power management: quiet one direction, keep everything else, pay a short synchronization sequence to come back.

It is directional in a stronger sense than it first appears — §1 sources two independent state machines, one controlling the transmitter and one following the receiver, because the two directions go idle for reasons decided by different devices.

It is not electrical idle, which is a physical condition four different states use. And it is not L1, which quiets both directions, powers down PHY, and requires both ends to re-lock.

Chapter 18.8 — L1 takes that deeper state. The extra saving comes from powering down more, and every consequence follows from that: entry becomes a negotiation with the partner rather than a local decision, both PHYs must re-establish lock, exit latency grows by orders of magnitude — and the L1 PM Substates (L1.1 and L1.2) go further still, with their own signalling. The power-management DLLPs that carry the negotiation were built in Chapter 15.5.

Chapter 18.9 — Disabled covers a Link deliberately not operating, and Chapter 18.10 — Hot Reset covers in-band reset and how it propagates through a hierarchy.

The idea to carry forward: a packet offered to a sleeping transmitter must wake it, and must not be taken by it — and those are two different signals for a reason.