PCIe · Module 18
LTSSM Overview — What the Link Is Currently Trying to Do
Detection, training, normal traffic, recovery, power states, disable and reset are behaviours. One hardware state machine decides which one is happening — and therefore which behaviours are legal right now.
Module 17 built the Physical Layer's machinery and left one question open in every chapter.
Chapter 17.1's distributor needed to know which lanes are active. Chapter 17.2's aligner needed a partner transmitting recognisable evidence. Chapter 17.3 produced a negotiated configuration but declined to say who ran the negotiation. Chapter 17.4 noted that equalization is coordinated during training and recovery, and pointed here.
Every one of those is a behaviour that is legal at some times and not at others — and something must decide which.
That something is the LTSSM: the Physical Layer's master state machine. It does not carry packets, route transactions or process credits. It decides what the Link is currently trying to accomplish, and therefore which signalling and control behaviours are permitted right now.
What is it, why does PCIe need one, and how should RTL expose that control architecture without collapsing the entire specification into one enormous case statement?
1. Scope, Sources, and What Belongs to the Child Chapters
2. State Is Not Packet Type
The first confusion to remove, because the vocabulary invites it.
| Question | Answered by |
|---|---|
| What is the Link doing? | LTSSM state |
| What information is this packet carrying? | TLP / DLLP type |
A Memory Write is not a state. Polling is not a packet.
3. It Is Hardware
The LTSSM reacts at Link timescales to events no software loop could service.
Its inputs are physical and immediate: receiver detection, PHY lock and alignment status, received training patterns, timers, errors, power-management requests, and reset.
Software's role is real but different. It sets policy and configuration — whether ASPM is enabled, target link speed, whether the Link may be retrained — through configuration registers, and it observes the result. It does not step the machine through states.
4. Terminology Collisions
PCIe reuses several words across completely unrelated axes, and this is where they collide.
5. The State Families
One sentence each. The chapters own the rest.
| State | What it is for | Owned by |
|---|---|---|
| Detect | determines whether a receiver is present and begins Link establishment | 18.2 |
| Polling | establishes the low-level synchronization and training progress that configuration requires | 18.3 |
| Configuration | turns usable trained lanes into a coherent negotiated Link configuration | 18.4 |
| L0 | normal, fully-active Link operation — the state in which traffic flows | 18.6 |
| Recovery | re-establishes or changes a Link that cannot simply remain in normal operation | 18.5 |
| L0s | a shallow low-power state with low exit latency | 18.7 |
| L1 | a deeper low-power state | 18.8 |
| Disabled | the Link is deliberately not participating in normal operation | 18.9 |
| Hot Reset | in-band reset signalling and its propagation | 18.10 |
Read the table as a map, not as a specification. Each row is a purpose, and every entry condition, exit condition, substate, timeout and transition criterion belongs to the chapter named beside it.
6. The Shape
7. What the Machine Consumes and Produces
The orchestration view, and it is the reason a single controller exists at all.
INPUTS OUTPUTS
receiver detection ─┐ ┌─ PHY mode and power state
PHY lock / alignment ─┤ ├─ training pattern generation
received ordered sets ─┤ LTSSM├─ lane enable and configuration
timers ─┤ ├─ NORMAL TRAFFIC ENABLE
errors ─┤ ├─ recovery request
power-management req ─┤ ├─ power-state control
reset / hot reset ─┘ └─ link status, for software8. Traffic Is Only Legal Sometimes
The consequence that reaches all the way up to Chapter 16.5's transmit path.
Normal TLP and DLLP traffic flows in normal operation. In Detect, Polling, Configuration, Recovery, the low-power states, Disabled and Hot Reset, the Link is doing something else — and a packet handed to the PHY during those states has nowhere to go.
So there must be a gate, and Chapter 17.3 §12 already built its ownership discipline: gate both valid and ready, or a packet is destroyed at the boundary.
9. Why This Chapter Does Not Build an LTSSM
10. RTL — State Encoding and Observability
// SYNTHESIZABLE. Normalized LTSSM state representation and observability.
//
// ============================================================
// THIS IS AN INTERNAL DEBUG ENUM, NOT A PCIe WIRE ENCODING.
//
// The values are this design's; PCIe does not define an encoding for
// LTSSM state on any interface. The STATE NAMES are the canonical major
// families (section 5); their entry/exit criteria belong to chapters
// 18.2-18.10 and appear nowhere in this module.
// ============================================================
package ltssm_pkg;
typedef enum logic [3:0] {
ST_DETECT = 4'd0,
ST_POLLING = 4'd1,
ST_CONFIGURATION = 4'd2,
ST_L0 = 4'd3,
ST_RECOVERY = 4'd4,
ST_L0S = 4'd5,
ST_L1 = 4'd6,
ST_DISABLED = 4'd7,
ST_HOT_RESET = 4'd8
} ltssm_state_e;
localparam int N_STATES = 9;
// Why a transition happened, for the history register (section 11).
// ILLUSTRATIVE categories -- not a PCIe enumeration.
typedef enum logic [2:0] {
RSN_NONE = 3'd0,
RSN_PROGRESS = 3'd1, // normal forward progress
RSN_TIMEOUT = 3'd2,
RSN_ERROR = 3'd3,
RSN_PM = 3'd4, // power-management request
RSN_RESET = 3'd5,
RSN_SOFTWARE = 3'd6 // configuration-driven
} ltssm_reason_e;
function automatic bit state_legal(input logic [3:0] s);
return (s < 4'(N_STATES));
endfunction
// NORMAL TLP/DLLP TRAFFIC IS LEGAL ONLY IN NORMAL OPERATION.
// Scoped as this teaching model's contract: the exact set of states in
// which traffic may flow is chapter 18.6's, and a design must take it
// from there rather than from this one-line function.
function automatic bit traffic_legal(input ltssm_state_e s);
return (s == ST_L0);
endfunction
endpackageClassification: synthesizable (package) — and the enum is a debug representation, not a wire format.
import ltssm_pkg::*;
// SYNTHESIZABLE. State observability: what the machine is doing, what it
// was doing, and how it got here.
// This is the register set an engineer reads FIRST when a Link fails
// (section 16). It observes; it never drives.
module ltssm_observe #(
parameter int CNT_W = 16
) (
input logic clk,
input logic rst_n,
// ---- From the state machine (which this module does NOT implement) ----
input ltssm_state_e state,
input ltssm_reason_e reason,
// ---- Debug-visible history ---------------------------------------------
output ltssm_state_e cur_state,
output ltssm_state_e prev_state,
output ltssm_reason_e last_reason,
output logic state_changed, // one cycle per transition
output logic [CNT_W-1:0] transition_count,
output logic [CNT_W-1:0] recovery_count, // entries into Recovery
output logic illegal_state
);
generate
if (CNT_W < 1) $error("CNT_W must be at least 1");
endgenerate
ltssm_state_e state_q, prev_q;
ltssm_reason_e reason_q;
logic [CNT_W-1:0] tcnt_q, rcnt_q;
logic ill_q;
assign cur_state = state_q;
assign prev_state = prev_q;
assign last_reason = reason_q;
assign transition_count = tcnt_q;
assign recovery_count = rcnt_q;
assign illegal_state = ill_q;
// THE TRANSITION EVENT. Compared against the REGISTERED state, so it is
// exactly one cycle wide per change (P3) -- a comparison against a
// combinational next-state would glitch.
wire changed = (state != state_q);
assign state_changed = changed;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state_q <= ST_DETECT;
prev_q <= ST_DETECT;
reason_q <= RSN_NONE;
tcnt_q <= '0; rcnt_q <= '0; ill_q <= 1'b0;
end else begin
state_q <= state;
if (changed) begin
// PREVIOUS STATE UPDATES ON THE TRANSITION, not before it. An
// early update makes prev_state equal cur_state and destroys the
// one piece of history that says where the machine came from
// (section 18, mutation 2).
prev_q <= state_q;
reason_q <= reason;
// SATURATING. A wrapped count reads low and looks healthy, which
// is the reading least distinguishable from a stable Link.
if (tcnt_q != {CNT_W{1'b1}}) tcnt_q <= tcnt_q + CNT_W'(1);
// RECOVERY ENTRIES ARE COUNTED SEPARATELY, because a Link that
// keeps re-entering Recovery is the single most useful physical-
// layer signal available to software (section 16).
if ((state == ST_RECOVERY) && (rcnt_q != {CNT_W{1'b1}}))
rcnt_q <= rcnt_q + CNT_W'(1);
end
if (!state_legal(state)) ill_q <= 1'b1;
end
end
endmoduleClassification: synthesizable.
Architecture. Registered state, previous state, last reason, and two saturating counters.
recovery_count earns its own counter rather than being inferred from the transition count. A Link that enters Recovery repeatedly is reporting a physical problem (§16), and that signal is far more valuable when it is a single number software can read than when it must be reconstructed from a history buffer.
Failure — four. Updating prev_state before the transition makes it equal cur_state and destroys the history. Comparing against a combinational next-state produces a glitching state_changed. A wrapping counter reads low and looks healthy. And counting Recovery entries while in Recovery rather than on entry inflates the number by its dwell time.
11. RTL — State Dwell Timer
// SYNTHESIZABLE. A reusable dwell timer primitive.
// THE LTSSM USES TIMING CONDITIONS EXTENSIVELY, but NO PCIe TIMEOUT
// CONSTANT APPEARS HERE -- the normative values belong to the chapters
// that own each state (section 1). This is the mechanism only.
module ltssm_timer #(
parameter int CNT_W = 24
) (
input logic clk,
input logic rst_n,
input logic enable,
input logic restart, // e.g. a state change
input logic [CNT_W-1:0] limit,
output logic [CNT_W-1:0] count,
output logic expired
);
generate
if (CNT_W < 1) $error("CNT_W must be at least 1");
endgenerate
logic [CNT_W-1:0] cnt_q;
assign count = cnt_q;
// ==================================================================
// limit == 0 CONTRACT, DECLARED: the timer is DISABLED, never
// instantly expired. A zero limit almost always means "not configured
// yet", and treating it as immediate expiry fires every timeout in the
// design during reset (section 18, mutation 6).
// ==================================================================
assign expired = enable && (limit != '0) && (cnt_q >= limit);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt_q <= '0;
end else if (restart) begin
// RESTART HAS PRIORITY OVER COUNTING, declared. A state change must
// reset the dwell measurement even if the timer would have ticked
// in the same cycle -- otherwise the new state inherits part of the
// old state's elapsed time.
cnt_q <= '0;
end else if (enable && !expired) begin
// SATURATING at the limit. A wrapping dwell timer would silently
// re-arm and fire again, turning one timeout into a periodic one.
cnt_q <= cnt_q + CNT_W'(1);
end
end
endmoduleClassification: synthesizable.
Three contracts declared rather than left implicit: limit == 0 disables, restart beats counting, and the counter saturates at the limit rather than wrapping.
Each has an opposite that is worse. A zero limit read as immediate expiry fires every timeout during reset. Counting winning over restart lets a new state inherit the previous state's elapsed time — so a short state can appear to time out instantly. And wrapping turns a single timeout into a repeating one, which presents as a Link that cycles on a period nobody configured.
12. RTL — Entry Pulses
import ltssm_pkg::*;
// SYNTHESIZABLE. One-cycle entry pulses, one per state.
// A small block with an outsized architectural effect (see below).
module ltssm_entry_pulse (
input logic clk,
input logic rst_n,
input ltssm_state_e state,
output logic [N_STATES-1:0] enter, // one-hot, one cycle per entry
output logic exit_any
);
ltssm_state_e state_q;
wire changed = (state != state_q);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) state_q <= ST_DETECT;
else state_q <= state;
end
always_comb begin
enter = '0;
// EXACTLY ONE BIT, EXACTLY ONE CYCLE. One-hot by construction, because
// it is indexed by the new state rather than assembled from
// per-state comparisons that could disagree.
if (changed && state_legal(state)) enter[state] = 1'b1;
end
assign exit_any = changed;
endmoduleClassification: synthesizable (combinational plus one register).
13. RTL — Traffic Gate and Event Aggregation
import ltssm_pkg::*;
// SYNTHESIZABLE. Normal traffic is permitted only when the Link state
// says it is legal.
// THE GATING DISCIPLINE is Chapter 17.3 section 12's: gate BOTH
// directions, or a packet is destroyed at the boundary. Which states
// permit traffic is chapter 18.6's -- this model uses L0 only.
module ltssm_traffic_gate (
input ltssm_state_e state,
input logic up_valid,
output logic up_ready,
output logic down_valid,
input logic down_ready,
output logic traffic_enabled
);
assign traffic_enabled = traffic_legal(state);
// ==================================================================
// BOTH DIRECTIONS GATED.
//
// WRONG: down_valid = up_valid && traffic_enabled;
// up_ready = down_ready; // <-- not gated
//
// With traffic_enabled = 0, up_valid = 1 and down_ready = 1, upstream
// sees valid && ready and RELEASES the packet. Downstream sees
// valid = 0 and receives nothing. The packet is destroyed -- and this
// happens during Recovery and bring-up, when traffic is most likely to
// be pushed and least likely to be instrumented (section 18).
// ==================================================================
assign down_valid = up_valid && traffic_enabled;
assign up_ready = down_ready && traffic_enabled;
endmoduleimport ltssm_pkg::*;
// SYNTHESIZABLE. Normalize control events from several engines into one
// prioritized stream for the state machine to consume.
// THE PRIORITY ORDER IS LOCAL IMPLEMENTATION POLICY, not a PCIe rule --
// the normative precedence among these conditions belongs to the
// chapters that own the states (section 1).
module ltssm_event_norm (
input logic clk,
input logic rst_n,
// ---- From the physical and control engines -----------------------------
input logic detect_done,
input logic training_progress,
input logic config_done,
input logic recovery_needed,
input logic pm_request,
input logic hot_reset_seen,
input logic disable_request,
// ---- Normalized, to the state machine -----------------------------------
output logic evt_valid,
input logic evt_ready,
output ltssm_reason_e evt_reason,
output logic [2:0] evt_source,
output logic evt_overflow
);
// ==================================================================
// PRIORITY, DECLARED AND ORDERED BY IRREVERSIBILITY.
//
// Reset-class events outrank power management, which outranks normal
// progress -- because a reset invalidates the outcome of everything
// below it, so deferring it would let the machine act on a decision
// that is about to be discarded.
//
// This is THIS DESIGN'S ordering. PCIe's normative precedence among
// these conditions is owned by chapters 18.2-18.10.
// ==================================================================
logic v_q;
ltssm_reason_e r_q;
logic [2:0] s_q;
logic ovf_q;
assign evt_valid = v_q;
assign evt_reason = r_q;
assign evt_source = s_q;
assign evt_overflow = ovf_q;
logic sel_any;
ltssm_reason_e sel_reason;
logic [2:0] sel_source;
always_comb begin
sel_any = 1'b1;
if (hot_reset_seen) begin sel_reason = RSN_RESET; sel_source = 3'd0; end
else if (disable_request) begin sel_reason = RSN_SOFTWARE; sel_source = 3'd1; end
else if (recovery_needed) begin sel_reason = RSN_ERROR; sel_source = 3'd2; end
else if (pm_request) begin sel_reason = RSN_PM; sel_source = 3'd3; end
else if (config_done) begin sel_reason = RSN_PROGRESS; sel_source = 3'd4; end
else if (detect_done) begin sel_reason = RSN_PROGRESS; sel_source = 3'd5; end
else if (training_progress) begin sel_reason = RSN_PROGRESS; sel_source = 3'd6; end
else begin sel_any = 1'b0; sel_reason = RSN_NONE; sel_source = 3'd7; end
end
wire fire = v_q && evt_ready;
wire can_take = !v_q || evt_ready;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
v_q <= 1'b0; r_q <= RSN_NONE; s_q <= 3'd7; ovf_q <= 1'b0;
end else begin
// Capture beats drain, so a same-cycle accept-and-consume holds the
// new event rather than dropping it.
if (sel_any && can_take) begin
v_q <= 1'b1; r_q <= sel_reason; s_q <= sel_source;
end else if (fire) begin
v_q <= 1'b0;
end
// An event that could not be held is REPORTED. A lost reset or
// recovery request is not something to discover in the lab.
if (sel_any && !can_take) ovf_q <= 1'b1;
end
end
endmoduleClassification: synthesizable.
The traffic gate is two lines, and the symmetry is the entire content — Chapter 17.3 §12 proved why, and this is the same discipline applied to Link state rather than to link readiness.
The event normalizer's priority is ordered by irreversibility. Reset outranks power management outranks progress, because a reset discards the outcome of everything beneath it — so acting on a lower-priority event first means acting on a decision that is about to be thrown away.
And the priority is declared as this design's, not as PCIe's. §1's boundary applies: normative precedence among these conditions belongs to the state chapters.
14. Hierarchical Architecture
The alternative to §9's monolith, and the reason this chapter's RTL looks the way it does.
top-level major-mode controller
│
┌────────┬───────┼────────┬──────────┐
Detect Polling Config Recovery Power
subctl subctl subctl subctl subctlThe top level owns the major mode. Each subcontroller owns its own local progress, substates and timers.
The interface between them is narrow, and narrowness is the point:
child_start → "begin your work"
child_active ← "I am working"
child_done ← "I finished successfully"
child_fail ← "I could not"
child_result ← whatever the parent needs (width, lane map, rate)15. RTL — Stuck-State Monitor
import ltssm_pkg::*;
// SYNTHESIZABLE. Lab-visibility monitor: warn when the machine has not
// moved for an unusually long time.
//
// ============================================================
// THIS IS DIAGNOSTIC ONLY. IT NEVER DRIVES A STATE TRANSITION.
//
// A monitor that could force the machine would be a second, undeclared
// controller -- and the resulting behaviour would depend on a debug
// threshold rather than on protocol conditions. The real timeouts belong
// to the chapters that own each state (section 1); this warns, and that
// is all it does.
// ============================================================
module ltssm_stuck_monitor #(
parameter int CNT_W = 24
) (
input logic clk,
input logic rst_n,
input ltssm_state_e state,
input logic state_changed,
input logic [CNT_W-1:0] warn_limit, // debug threshold, software-set
output logic stuck_warning,
output ltssm_state_e stuck_state,
output logic [CNT_W-1:0] dwell
);
logic [CNT_W-1:0] dwell_q;
logic warn_q;
ltssm_state_e stuck_q;
assign dwell = dwell_q;
assign stuck_warning = warn_q;
assign stuck_state = stuck_q;
wire at_limit = (warn_limit != '0) && (dwell_q >= warn_limit);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
dwell_q <= '0; warn_q <= 1'b0; stuck_q <= ST_DETECT;
end else if (state_changed) begin
// A transition clears the dwell measurement. The WARNING is sticky
// so a transient stall that has since resolved is still readable --
// which is the whole reason a debug register exists.
dwell_q <= '0;
end else begin
if (dwell_q != {CNT_W{1'b1}}) dwell_q <= dwell_q + CNT_W'(1);
if (at_limit && !warn_q) begin
warn_q <= 1'b1;
stuck_q <= state; // WHICH state it was stuck in
end
end
end
endmoduleClassification: synthesizable (diagnostic).
stuck_state is captured, not just stuck_warning. By the time software reads the register the machine may have moved on — and "it was stuck in Polling" is a completely different investigation from "it was stuck in Recovery" (§16). A flag without the state is half a diagnostic.
And the warning is sticky while the dwell counter is not. The counter answers how long now; the sticky flag answers did this ever happen. Both are needed, and they are different questions.
16. A Trace
Internal teaching signals, not PCIe wire signals.
step 1 2 3 4 5 6 7 8
state DET POL CFG L0 L0 RECOV RECOV L0
state_changed - 1 1 1 0 1 0 1
prev_state DET DET POL CFG CFG L0 L0 RECOV
last_reason - PROG PROG PROG PROG ERROR ERROR PROG
transition_cnt 0 1 2 3 3 4 4 5
recovery_cnt 0 0 0 0 0 1 1 1
traffic_enabled 0 0 0 1 1 0 0 1
up_valid 1 1 1 1 1 1 1 1
up_ready 0 0 0 1 1 0 0 1
down_valid 0 0 0 1 1 0 0 1Read steps 1–3. Bring-up. A packet has been waiting since step 1 — up_ready is low, nothing transfers, and it stays upstream.
Read step 4. L0. traffic_enabled rises, up_ready follows, and the packet finally moves.
Read step 6 — the case the whole chapter is about. Recovery is entered. traffic_enabled goes low and up_ready goes low with it. The packet in flight at that moment does not transfer — it remains upstream-owned, exactly as §8 requires. Nothing is dropped.
Read recovery_cnt at step 6. It increments once, on entry — not once per cycle spent in Recovery. That distinction is §10's, and it is what makes the counter readable.
Read step 8. Recovery completes, L0 returns, and the waiting packet transfers.
And note what never happens: up_ready is never high while traffic_enabled is low. That single invariant is P8, and it is the difference between a stalled packet and a destroyed one.
17. Assertions
// SVA over ltssm_observe, ltssm_timer, ltssm_entry_pulse,
// ltssm_traffic_gate, ltssm_event_norm and ltssm_stuck_monitor.
//
// These assert the OVERVIEW INFRASTRUCTURE's contract. They assert
// NOTHING about PCIe's legal state transitions, entry or exit criteria,
// or timeout values -- all owned by chapters 18.2-18.10 (section 1) --
// and nothing about training eventually succeeding, which depends on a
// partner and a channel this design does not control.
// ---- ENVIRONMENT ------------------------------------------------------
// A1: the state input is a legal encoding.
assume property (@(posedge clk) disable iff (!rst_n) state_legal(state));
// A2: reason accompanies a transition.
assume property (@(posedge clk) disable iff (!rst_n)
(state != $past(state)) |-> (reason != RSN_NONE));
// ---- STATE OBSERVABILITY ----------------------------------------------
// P1: the exposed state tracks the machine with a defined one-cycle
// relationship -- never an ambiguous mixture.
property p_state_tracks;
@(posedge clk) disable iff (!rst_n)
cur_state == $past(state);
endproperty
a_track : assert property (p_state_tracks);
// P2: PREVIOUS STATE IS THE STATE BEFORE THE TRANSITION. Updating it
// early makes it equal cur_state and destroys the history.
property p_prev_correct;
@(posedge clk) disable iff (!rst_n)
$rose(state_changed) |=> (prev_state == $past(cur_state, 2));
endproperty
a_prev : assert property (p_prev_correct);
// P3: the transition count increments EXACTLY ONCE per change.
property p_count_once;
@(posedge clk) disable iff (!rst_n)
(transition_count != $past(transition_count))
|-> ($past(state_changed)
&& (transition_count == $past(transition_count) + 1));
endproperty
a_count : assert property (p_count_once);
// P3b: and it does not move while the state is stable.
property p_count_stable;
@(posedge clk) disable iff (!rst_n)
!state_changed |=> $stable(transition_count);
endproperty
a_count_stable : assert property (p_count_stable);
// P3c: recovery_count increments on ENTRY to Recovery, not per cycle in it.
property p_recovery_on_entry;
@(posedge clk) disable iff (!rst_n)
(recovery_count != $past(recovery_count))
|-> ($past(state_changed) && ($past(state) == ST_RECOVERY));
endproperty
a_rec : assert property (p_recovery_on_entry);
// ---- ENTRY PULSES -----------------------------------------------------
// P4: EXACTLY ONE entry pulse per transition, and it is one-hot.
property p_entry_one_hot;
@(posedge clk) disable iff (!rst_n)
(state != $past(state)) |-> ($countones(enter) == 1);
endproperty
a_entry : assert property (p_entry_one_hot);
// P5: NO entry pulse while the state is stable. The complementary half --
// a design producing a level rather than a pulse fails here.
property p_no_pulse_when_stable;
@(posedge clk) disable iff (!rst_n)
(state == $past(state)) |-> (enter == '0);
endproperty
a_stable : assert property (p_no_pulse_when_stable);
// P6: the pulse names the state being ENTERED.
property p_entry_names_new_state;
@(posedge clk) disable iff (!rst_n)
(enter != '0) |-> enter[state];
endproperty
a_names : assert property (p_entry_names_new_state);
// ---- TIMER ------------------------------------------------------------
// P7: the timer RESTARTS on a state change -- a new state never inherits
// the previous state's elapsed time.
property p_timer_restarts;
@(posedge clk) disable iff (!rst_n)
restart |=> (count == '0);
endproperty
a_restart : assert property (p_timer_restarts);
// P7b: limit == 0 means DISABLED, never instantly expired.
property p_zero_limit_disables;
@(posedge clk) disable iff (!rst_n)
(limit == '0) |-> !expired;
endproperty
a_zero : assert property (p_zero_limit_disables);
// P7c: SATURATES rather than wrapping -- the count never decreases
// except on a restart.
property p_timer_saturates;
@(posedge clk) disable iff (!rst_n)
(count < $past(count)) |-> $past(restart);
endproperty
a_sat : assert property (p_timer_saturates);
// ---- TRAFFIC GATE -----------------------------------------------------
// P8: THE CHAPTER'S CENTRAL PROPERTY. A packet blocked by an illegal
// state remains UPSTREAM-OWNED -- neither transferred nor destroyed.
// This is the property section 18's counterexample fails.
property p_packet_stays_upstream;
@(posedge clk) disable iff (!rst_n)
(up_valid && !traffic_enabled) |-> (!up_ready && !down_valid);
endproperty
a_ownership : assert property (p_packet_stays_upstream);
// P9: no transfer occurs while traffic is not legal.
property p_no_transfer_when_illegal;
@(posedge clk) disable iff (!rst_n)
!traffic_enabled |-> !(up_valid && up_ready);
endproperty
a_gated : assert property (p_no_transfer_when_illegal);
// P10: traffic_enabled implies the teaching model's traffic state.
property p_enabled_implies_l0;
@(posedge clk) disable iff (!rst_n)
traffic_enabled |-> (cur_state == ST_L0);
endproperty
a_l0 : assert property (p_enabled_implies_l0);
// P11: when traffic IS legal the gate is transparent -- it adds no
// behaviour of its own.
property p_transparent;
@(posedge clk) disable iff (!rst_n)
traffic_enabled |-> ((down_valid == up_valid) && (up_ready == down_ready));
endproperty
a_transparent : assert property (p_transparent);
// ---- EVENTS -----------------------------------------------------------
// P12: an event that could not be held is REPORTED, never silent.
property p_event_overflow_reported;
@(posedge clk) disable iff (!rst_n)
(sel_any && !can_take) |=> evt_overflow;
endproperty
a_ovf : assert property (p_event_overflow_reported);
// P13: the declared priority holds -- a reset-class event outranks
// everything beneath it.
property p_reset_priority;
@(posedge clk) disable iff (!rst_n)
(hot_reset_seen && can_take) |=> (evt_reason == RSN_RESET);
endproperty
a_priority : assert property (p_reset_priority);
// ---- SCOPE ------------------------------------------------------------
// P14: THE DIAGNOSTIC MONITOR NEVER DRIVES THE STATE MACHINE. A monitor
// that could force a transition would be a second, undeclared controller
// whose behaviour depended on a debug threshold (section 15).
property p_monitor_does_not_drive;
@(posedge clk) disable iff (!rst_n)
$rose(stuck_warning) |=> $stable(cur_state);
endproperty
a_scope : assert property (p_monitor_does_not_drive);
// P15: the stuck monitor captures WHICH state it was stuck in.
property p_stuck_state_captured;
@(posedge clk) disable iff (!rst_n)
$rose(stuck_warning) |-> (stuck_state == cur_state);
endproperty
a_stuck : assert property (p_stuck_state_captured);
// P16: reset initializes the observability set to a defined value.
property p_reset;
@(posedge clk)
!rst_n |=> ((transition_count == '0) && (recovery_count == '0)
&& !stuck_warning && !traffic_enabled);
endproperty
a_reset : assert property (p_reset);P8 and P9 are not the same property, and the difference is Chapter 17.3 §12's counterexample all over again. P9 forbids a transfer reaching downstream; P8 forbids the asymmetric state in which upstream believes one happened. A design gating only valid satisfies P9 and fails P8 — which is why a property watching only the consumer cannot catch it.
P4 and P5 are the entry-pulse pair. One pulse per transition, and no pulse while stable — a design producing a level rather than a pulse passes P4 and fails P5.
P7, P7b and P7c are the three declared timer contracts, each asserted rather than left to inspection.
P14 is a scope property, in the style established since Chapter 15.5 §11: the diagnostic monitor must never become a controller. It cannot be written inside either module, and it is what catches a refactor that wires a debug threshold into the state machine.
No liveness is asserted. "The Link eventually reaches L0" depends on a partner and a physical channel, and asserting it would require assuming away precisely the failures Module 18 exists to explain.
18. Verification and Fault Injection
This verifies the overview infrastructure. It does not verify a PCIe LTSSM — there is no LTSSM here to verify (§9).
The scoreboard maintains its own state-history, timer and gate model, driven from observed transitions, and never reads the DUT's registers as its expected value.
State observability
- A single transition — verify
prev_state,last_reason, one increment (P2, P3). - The same state held for many cycles — verify no counting, no pulse (P3b, P5).
- A transition to Recovery — verify
recovery_countincrements once (P3c). - Many cycles in Recovery — verify it does not keep incrementing. Required.
- Counter to saturation — verify it holds rather than wrapping.
- An illegal state encoding — verify
illegal_stateand no corruption.
Timer
restartand a would-be tick in the same cycle — verify restart wins (P7). Required.limit = 0— verify disabled, not instantly expired (P7b). Required.- Count to the limit — verify
expiredand saturation (P7c). CNT_W= 1 and 24.
Entry pulses
- Every state entered in turn — verify one-hot, one cycle, naming the new state (P4, P6).
- A transition back to a previously-visited state — verify a fresh pulse.
- State stable across many cycles — verify no pulses (P5).
Traffic gate
- Traffic illegal,
up_validhigh,down_readyhigh — verifyup_readyis low and the packet stays (P8). Required, and the counterexample test. - Transition into L0 with a packet waiting — verify it transfers.
- Transition out of L0 mid-stall — verify the packet remains offered.
- Downstream stalling while in L0 — verify transparency (P11).
Events
- Two events in the same cycle — verify the declared priority (P13).
- An event while the machine is not ready — verify holding, then overflow if it cannot be held (P12).
- Hot reset concurrent with a progress event — verify reset wins.
Mutations
| # | Mutation | Caught by | Lab symptom |
|---|---|---|---|
| 1 | traffic enabled in Recovery | P10, P9 | packets sent into a Link that cannot carry them |
| 2 | prev_state updates before the transition | P2 | history register useless; prev == cur always |
| 3 | transition counter increments every cycle | P3b | counts dwell time, not transitions; huge numbers |
| 4 | entry pulse stays high | P5 | blocks re-initialise continuously |
| 5 | timer does not restart on a state change | P7 | new states inherit elapsed time; spurious timeouts |
| 6 | limit = 0 treated as instant expiry | P7b | every timeout fires during reset |
| 7 | timer wraps instead of saturating | P7c | one timeout becomes periodic |
| 8 | illegal enum accepted | illegal_state never asserts | undefined behaviour downstream |
| 9 | stuck monitor drives a transition | P14 | Link behaviour depends on a debug threshold |
| 10 | recovery_count increments per cycle in Recovery | P3c | count reflects dwell, not entries; useless |
| 11 | up_ready not gated (only valid) | P8 | packets silently destroyed during Recovery |
| 12 | hot reset treated as an ordinary PM event | P13 | reset deferred behind power management |
| 13 | cur_state and the machine differ by two cycles | P1 | debug register disagrees with reality |
| 14 | stuck warning without capturing the state | P15 | "something was stuck" — no idea what |
19. Debugging
The LTSSM state is the first thing a hardware engineer reads when a PCIe Link misbehaves, and this section is the map from state to investigation.
The Link never appears
The first question is not "why is the Link down" — it is "where did the machine stop".
| Stuck in | Investigate |
|---|---|
| Detect | physical presence, termination, power, the partner being alive at all |
| Polling | bit and symbol lock, signal quality, the partner transmitting recognisable patterns |
| Configuration | lane usability and numbering, width negotiation, partner capability |
| cycling through Recovery | rate stability, equalization, channel margin |
§15's stuck_state exists for exactly this, and it captures the state rather than only the fact — because these four rows lead to four completely different investigations.
And it separates a PHY problem from an enumeration problem in one reading. Chapter 7.3's discovery cannot find a device whose Link never trained. "Not in lspci" plus "stuck in Detect" is a physical problem; "not in lspci" plus "reached L0" is a software one.
The Link repeatedly enters Recovery
Read recovery_count first (§10). A Link entering Recovery occasionally is normal; a Link entering it continuously is reporting a physical problem it keeps trying to fix.
Look below the packet layers first: PHY error counters and lock-loss counts (Chapter 17.5 §16), equalization results (Chapter 17.4), and channel margin.
The distinguishing experiment: correlate recovery_count against rate. If it stops at a lower negotiated speed, the cause is margin. If it persists at every rate, look at the control plane.
And resist the terminology trap (§4): repeated Recovery is not Data Link retry. A high replay count and a high recovery count can both be present, and the recovery count is the one closer to the cause.
The device disappears intermittently after boot
Capture LTSSM history, not just software logs.
Software sees a device vanish. transition_count, recovery_count, prev_state and last_reason show what the Link did — and whether it went through Recovery, Hot Reset, Disabled, or a power state on the way.
last_reason is the highest-value field here (§10). A departure from L0 for RSN_PM is power management working; for RSN_ERROR it is not, and the two are indistinguishable from software's point of view.
The Link reports up but packets disappear
Check the traffic gate before anything else (§18's counterexample).
Inspect whether up_ready is ever high while traffic_enabled is low. That single condition is the bug, and it is a one-line fix.
The distinguishing experiment: hold traffic off until the state has been stable in L0 for many cycles. If packets stop disappearing, it is the gate.
A state name alone is not a root cause
"Stuck in Polling" is a location, not a diagnosis. It narrows the search enormously — and the actual cause is in the physical layer, the partner, or the channel.
The child chapters (18.2–18.10) provide the per-state detail that turns a location into a cause.
20. Common Misconceptions
- "The LTSSM is software." Hardware, reacting at Link timescales. Software sets policy and observes (§3).
- "The LTSSM only runs at boot." It runs continuously — Recovery, power states and reset all happen during normal operation (§6).
- "L0 means the device is in D0." Different axes: Link state versus Function power state (§4).
- "Polling means polling a register." A name collision with a software idiom (§4).
- "Recovery means Transaction Layer retry." Physical re-training versus Data Link replay (§4).
- "Configuration state means Configuration Space access." Physical Link configuration versus a software register read (§4).
- "Detect means software discovery." Hardware receiver presence versus enumeration (§4).
- "The LTSSM processes Memory Reads." It carries no packets and interprets none (§2).
- "Equalization and the LTSSM are one block." Equalization is coordinated by states owned by Chapter 18.5; it is not the machine (§7).
- "All LTSSM states may carry normal traffic." Traffic is legal in normal operation only, and the gate enforces it (§8).
- "TS1 and TS2 are Transaction Layer packets." Physical-layer ordered sets, used before either upper layer can operate (§2).
- "One giant
casestatement is the best RTL architecture." It is harder to verify, debug, reuse and change (§9, §14). - "A state name alone diagnoses the root cause." It localises the search; the cause is usually below it (§19).
21. Module 18 Learning Map
What each remaining chapter answers.
| Engineering question | ||
|---|---|---|
| 18.2 | Detect | how does a port determine that a receiver is present at all? |
| 18.3 | Polling | how are bit and symbol lock established before anything else can proceed? |
| 18.4 | Configuration | how do lane numbering and width negotiation turn lanes into a Link? |
| 18.5 | Recovery | how does a Link re-train, and how does a speed change actually happen? |
| 18.6 | L0 | what is true during normal operation, and what may happen there? |
| 18.7 | L0s | how is a shallow low-power state entered and exited quickly? |
| 18.8 | L1 | what does the deeper state save, and what does exit cost? |
| 18.9 | Disabled | how and why is a Link deliberately taken out of operation? |
| 18.10 | Hot Reset | how does in-band reset propagate through a hierarchy? |
22. Understanding Check
23. What's Next
The LTSSM answers one question continuously — what is the Link doing? — and that answer determines which behaviours are legal. It is hardware, it runs for the Link's whole life rather than only at boot, and its state is the first register an engineer reads when a Link misbehaves.
This chapter built what surrounds it: observability and history so a nanosecond-scale machine leaves a readable record; a dwell timer with declared contracts; a shared entry pulse so several blocks agree about when; a traffic gate that keeps a packet upstream rather than destroying it; and an event normalizer with a declared priority.
And it deliberately did not build the machine, because the states are where the engineering is — and each of them is a chapter.
Chapter 18.2 — Detect begins there, with the first question any Link must answer: is there anything on the other end at all?
The idea to carry forward: a mode controller's real product is not the mode — it is the guarantee that everything incompatible with it is off.