Skip to content

PCIe · Module 25

LTSSM Issues — Which Contract Inside the State Is Wrong?

Residency names the state. It cannot name the missing condition — two faults gating the same edge produce identical signatures. Guard vectors, state epochs and next-state timers are what separate them.

Chapter 25.3 §13 ended on a limit it could not pass. Two different faults — no training exchange and no lane lock — produced identical residency, identical loop counts, identical final state. Both gate the same edge, so every counter reports the same values.

Separating them means looking inside the state, and that is what this chapter is: not which state, but which contract inside it.

1. Sources, Scope, and the Boundary With 25.3

2. A State Is Not a Name

Draw a state as a circle and you have recorded almost nothing. The behaviour lives in five contracts, and each fails in a characteristic way:

ContractAsksBroken looks like
entry actionswhat happens exactly once on arrival?leaves immediately, or never leaves (§9)
resident invariantswhat stays true while here?inconsistent status while occupied
exit guardswhat permits leaving?never exits, or exits when it should not
guard prioritywhich wins when several are true?takes the wrong edge (§4)
exit actionswhat happens exactly once on departure?the next state inherits stale state (§9)

Two of these produce the same external symptom and have opposite causes.

"Never leaves" can be a guard that never becomes true (§5) or an entry action that failed to clear a counter the guard depends on (§9). Identical residency, opposite fixes.

"Leaves immediately" can be a stale event from the previous state satisfying this state's guard (§8) or a timer that entered already expired (§9).

So the diagnostic method is to interrogate the contracts in order (§3), and each question has an instrument in §13 that answers it directly.

3. The Eight Questions

Applied to any stuck, looping or wrong-transition state:

#QuestionInstrument (§13)
1Did we really enter the state?the entry pulse and transition history
2Did the entry actions run exactly once?entry-clear observation
3Are the resident invariants holding?status snapshot while occupied
4Which exit guards are currently true?the guard vector (§5)
5Which guard has priority?the declared priority encoder (§4)
6Did the chosen transition actually occur?transition-reason capture
7Did the exit actions run?next-state entry conditions
8Did the next state inherit stale state?the state epoch (§8)

Question 4 is the one that breaks 25.3's tie. Two faults gating the same edge produce identical residency — but a guard vector shows which of the edge's conditions is false, and that is the finding.

And question 8 is the one that is invisible without instrumentation. A transition that occurred for the wrong reason looks exactly like a correct transition in every counter. Only the epoch (§8) reveals that the event which caused it belonged to a previous state.

4. Exit Guards and Priority

5. The Guard Vector

6. Three Kinds of Timer, Never Conflated

7. The Next-State Timer Contract

8. The State Epoch and Stale Events

9. Entry and Exit Actions

An entry action that does not run, or runs more than once, produces symptoms at the opposite end of the spectrum from where you would look.

Entry-action failures:

Not cleared on entrySymptom
the state timerenters already expired → leaves immediately
the training/event countera previous state's evidence satisfies this guard → leaves immediately, wrongly
the lane maskstale lanes appear locked → wrong width negotiated
the state epochstale events accepted (§8)
a speed-change flaga phase repeats that already completed

§15 measured the general case: with entry actions omitted, 199,993 of 200,000 transitions entered a state with dirty counters — effectively every one.

Exit-action failures are the mirror image, and they show up in the next state:

Not done on exitSymptom in the next state
release the transmitter/ownerthe next state cannot drive
clear a status flagsoftware reads a stale condition
consume the pending eventthe next state re-uses it (§8)

The diagnostic instruction is counter-intuitive and worth stating plainly. When a state leaves immediately, look at its entry actions — not its exit guards. The guard was satisfied by evidence the state should have cleared on arrival, and staring at the guard logic shows nothing wrong, because nothing is wrong with it.

And the assertion that catches the whole class is small (§14, P9): on entry, the state's own counters read zero.

11. The Loop, Drawn

One LTSSM state's exit contract. The state under analysis remains occupied while no exit guard is true. If the success guard is true it moves to the next state. If the timeout guard is true it moves to the fallback state, which returns to the state under analysis and forms the observed loop. A stale event path also leads to the next state, marked as a transition taken for a reason that no longer existed. Reset and disable guards outrank both success and timeout.stateunderanalysisnextstate -successfallback-timeoutreset ordisablesame edge, stale eventsameedge,stale…guard success (priority 3)guard success(priority 3)guard timeout (priority 4)guard timeout(priority 4)retry - the observed loopretry - the observed loopretry - theobserved…guard reset (priority 1)guard reset(priority 1)epoch not checkedepoch not checkedwrong reason, right edgewrong reason, right edgewrongreason,…
Figure 1 — one state's exit contract, drawn as the guards rather than as the destinations. The state remains while no guard is true. Success and timeout can both become true in the same cycle, and the declared priority decides which edge is taken. A stale event from a previous state can satisfy the success guard, producing a transition that looks correct and was taken for a reason that no longer existed.

Four things to read out of the figure.

The edges are labelled with guards and priorities, not with destinations. That is the difference between a state diagram and a debugging diagram — the destination is visible in any trace; the guard that selected it is not.

Success and timeout both leave the same state, and §4's declared priority is the only thing that decides which. §15 measured 1,333 wrong selections when that priority is inverted.

The fallback edge returns, which is the loop 25.3 §13 measured at 802 and 1,922 transitions — and the loop is a symptom of the guard never becoming true, not a fault of its own.

And the stale-event path reaches the same next state as the legitimate one. It is drawn separately because no counter distinguishes them: same edge, same destination, same history entry. Only the epoch does (§8), and §15 measured 12,738 such transitions.

12. The Waveform

A clean entry, then one where the entry actions did not clear

10 cycles
Ten cycles of one LTSSM state. The state entry pulse asserts at cycle 1 and cycle 6. In the first occupancy the timer counts from zero, the success guard asserts at cycle 4 and the transition is taken with reason success. At cycle 6 the state is entered again but the entry clear does not occur, so the event count is already above threshold, the success guard is asserted immediately at cycle 6, and the state exits at cycle 7 having established nothing.entry clear runs — counters start at zeroentry clear runs — countersstart at zeroentry WITHOUT clear — guard already trueentry WITHOUT clear — guardalready trueexits having established nothingexits having establishednothingclkstate_enterentry_clearg_successg_timeouttransitionepoch_okt0t1t2t3t4t5t6t7t8t9
Figure 2 — the same state entered twice. In the first entry the entry actions clear the timer and the event counter, the success guard becomes true, and the transition is taken for the recorded reason. In the second entry the entry actions did not clear, so a previous state's event count already satisfies the guard and the state leaves immediately for a reason that was never established in this occupancy.

Four things to read out of the figure.

Cycle 1 and cycle 6 are both entries, and only one clears. entry_clear is the difference, and it is the whole bug (§9).

In the second occupancy g_success is true on the entry cycle itself. No evidence was gathered in this state — the guard is satisfied by the previous state's residue.

epoch_ok falls at cycles 6–7, which is the independent detection: the event satisfying the guard belongs to a prior epoch (§8). Without that signal the second transition is indistinguishable from the first.

And the state exits at cycle 7 having done nothing. The trace shows a state that was entered and left — which in a residency counter looks like a healthy fast transition, and is why 25.3's instruments cannot see it.

13. RTL — The Contract Instruments

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. LTSSM contract-debug types, on 18.1 §10's state enum.
package ltssm_contract_pkg;
  import ltssm_pkg::*;                       // ST_DETECT, ST_POLLING, ... (18.1)
 
  parameter int N_GUARD  = 8;
  parameter int GUARD_W  = (N_GUARD <= 1) ? 1 : $clog2(N_GUARD);
  parameter int HIST_D   = 16;
  parameter int HIST_W   = (HIST_D <= 1) ? 1 : $clog2(HIST_D);
  parameter int N_MILE   = 4;
  parameter int EPOCH_W  = 8;
 
  // LOCAL guard identity. WHICH guards a state has is Module 18's; this is
  // only how this design exposes them (§5).
  typedef enum logic [GUARD_W-1:0] {
    G_RESET   = 3'd0,   // priority 1 -- outranks everything (§4)
    G_DISABLE = 3'd1,   // priority 2
    G_SUCCESS = 3'd2,   // priority 3 -- deliberately above timeout
    G_TIMEOUT = 3'd3,   // priority 4
    G_OTHER   = 3'd4
  } guard_e;
 
  typedef struct packed {
    logic [3:0]         from_state;
    logic [3:0]         to_state;
    logic [N_GUARD-1:0] guards_true;    // ALL guards true at the decision
    guard_e             selected;       // the one priority chose
    logic [EPOCH_W-1:0] epoch;
    logic [31:0]        residency;
    logic               stale_rejected; // an event was dropped this cycle
    logic [31:0]        cycle;
  } contract_event_t;
 
endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import ltssm_contract_pkg::*;
 
// SYNTHESIZABLE. THE GUARD VECTOR (§5). The instrument that separates
// faults residency cannot: 25.3 §13's two identical rows differ by one bit
// here. Guards are exposed INDIVIDUALLY, never collapsed into `can_exit`.
module ltssm_guard_observe (
  input  logic clk,
  input  logic rst_n,
  input  logic g_reset,
  input  logic g_disable,
  input  logic g_success,
  input  logic g_timeout,
  input  logic g_other,
  input  logic dbg_clear,
 
  output logic [N_GUARD-1:0] guards_true,
  output guard_e             selected,
  output logic               any_guard,
  output logic               multiple_guards,   // more than one true at once
  output logic [31:0]        guard_true_count [N_GUARD],
  output logic [31:0]        contention_count   // cycles with >1 guard true
);
  logic [31:0] cnt_q [N_GUARD], cont_q;
 
  always_comb begin
    guards_true = '0;
    guards_true[G_RESET]   = g_reset;
    guards_true[G_DISABLE] = g_disable;
    guards_true[G_SUCCESS] = g_success;
    guards_true[G_TIMEOUT] = g_timeout;
    guards_true[G_OTHER]   = g_other;
    any_guard       = |guards_true;
    multiple_guards = !$onehot0(guards_true);
    // THE DECLARED PRIORITY (§4), in one place, matching the FSM's own.
    if      (g_reset)   selected = G_RESET;
    else if (g_disable) selected = G_DISABLE;
    else if (g_success) selected = G_SUCCESS;   // success ABOVE timeout
    else if (g_timeout) selected = G_TIMEOUT;
    else                selected = G_OTHER;
  end
 
  always_comb for (int i=0;i<N_GUARD;i++) guard_true_count[i]=cnt_q[i];
  assign contention_count = cont_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || dbg_clear) begin
      cont_q<='0; for (int i=0;i<N_GUARD;i++) cnt_q[i]<='0;
    end else begin
      for (int i=0;i<N_GUARD;i++)
        if (guards_true[i] && !(&cnt_q[i])) cnt_q[i] <= cnt_q[i] + 32'd1;
      // Contention is the evidence that §4's priority MATTERS in this design.
      if (multiple_guards && !(&cont_q)) cont_q <= cont_q + 32'd1;
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import ltssm_contract_pkg::*;
 
// SYNTHESIZABLE. State epoch and stale-event rejection (§8).
// §15: without this, 12,738 transitions in 200,000 cycles were fired by an
// event that belonged to a PREVIOUS state. The epoch is LOCAL state and is
// never transmitted (23.6 §7's warning).
module ltssm_state_epoch (
  input  logic clk,
  input  logic rst_n,
  input  logic       state_enter,
  input  logic       event_valid,
  input  logic [EPOCH_W-1:0] event_epoch,      // stamped AT DETECTION
  input  logic       dbg_clear,
 
  output logic [EPOCH_W-1:0] epoch,
  output logic               event_qualified,   // safe for a guard to use
  output logic               event_stale,
  output logic [31:0]        stale_rejected_count
);
  logic [EPOCH_W-1:0] ep_q;
  logic [31:0]        stale_q;
 
  assign epoch           = ep_q;
  assign event_stale     = event_valid && (event_epoch != ep_q);
  assign event_qualified = event_valid && (event_epoch == ep_q);
  assign stale_rejected_count = stale_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || dbg_clear) begin ep_q <= '0; stale_q <= '0; end
    else begin
      if (state_enter) ep_q <= ep_q + EPOCH_W'(1);
      // COUNTED, never silently dropped: a design that discards quietly
      // looks identical to one that never received the event (§8).
      if (event_stale && !(&stale_q)) stale_q <= stale_q + 32'd1;
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import ltssm_contract_pkg::*;
 
// SYNTHESIZABLE. Next-state timer (§7).
// §15: `== LIMIT` is one cycle late at unit steps and NEVER FIRES at steps
// of 3 or more -- a stuck state with a healthy-looking counter. It missed
// in 20.0% of trials with a counter stepping 1-3.
module ltssm_state_timer #(parameter int unsigned LIMIT = 1024) (
  input  logic clk,
  input  logic rst_n,
  input  logic        state_enter,
  input  logic [7:0]  step,                 // may exceed 1 (lane-aggregated)
  input  logic        tick,
  input  logic        dbg_clear,
 
  output logic [31:0] timer,
  output logic        timer_done,
  output logic [31:0] worst_timer,
  output logic        entered_expired       // sticky: entry did not clear (§9)
);
  logic [31:0] t_q, worst_q;
  logic        ee_q;
  logic [32:0] t_next;
 
  // NEXT-STATE comparison, and >= rather than == so an overshoot cannot
  // step over the threshold (23.6 pattern 9).
  always_comb t_next = {1'b0, t_q} + (tick ? 33'(step) : 33'd0);
  assign timer      = t_q;
  assign timer_done = (t_next >= 33'(LIMIT));
  assign worst_timer = worst_q;
  assign entered_expired = ee_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || dbg_clear) begin t_q<='0; worst_q<='0; ee_q<=1'b0; end
    else if (state_enter) begin
      // ENTRY ACTION: the timer starts at zero. A state that enters with a
      // non-zero timer can expire immediately (§9, §12's figure).
      if (t_q != '0 && worst_q < t_q) worst_q <= t_q;
      t_q <= '0;
    end else begin
      t_q <= t_next[31:0];
      // If a guard is already satisfied on the entry cycle, the entry
      // actions did not run. Report it rather than transition silently.
      if (timer_done && $past(state_enter)) ee_q <= 1'b1;
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import ltssm_contract_pkg::*;
 
// SYNTHESIZABLE. Entry-action observer (§9).
// §15: with entry actions omitted, 199,993 of 200,000 transitions entered
// a state with dirty counters. This block turns that into one sticky bit.
module ltssm_entry_check (
  input  logic clk,
  input  logic rst_n,
  input  logic        state_enter,
  input  logic [31:0] state_timer,
  input  logic [31:0] event_count,
  input  logic [15:0] lane_mask,
  input  logic        dbg_clear,
 
  output logic        entry_dirty,           // sticky
  output logic [3:0]  dirty_fields,          // which one was not cleared
  output logic [31:0] dirty_entry_count
);
  logic ed_q; logic [3:0] df_q; logic [31:0] cnt_q;
 
  assign entry_dirty=ed_q; assign dirty_fields=df_q; assign dirty_entry_count=cnt_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || dbg_clear) begin ed_q<=1'b0; df_q<='0; cnt_q<='0; end
    else if ($past(state_enter)) begin
      // One cycle AFTER entry, the state's own counters must read zero.
      automatic logic [3:0] d = {1'b0,
                                 (lane_mask   != '0),
                                 (event_count != '0),
                                 (state_timer != '0)};
      if (d != '0) begin
        if (!ed_q) begin ed_q <= 1'b1; df_q <= d; end   // FIRST one, latched
        if (!(&cnt_q)) cnt_q <= cnt_q + 32'd1;
      end
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import ltssm_contract_pkg::*;
 
// SYNTHESIZABLE. Transition-reason capture with the full guard vector.
// Records the INPUTS and the DECISION separately (§5) -- a history that
// stores only the destination cannot answer "why".
module ltssm_transition_history (
  input  logic clk,
  input  logic rst_n,
  input  logic               state_enter,
  input  logic [3:0]         state,
  input  logic [3:0]         prev_state,
  input  logic [N_GUARD-1:0] guards_true,
  input  guard_e             selected,
  input  logic [EPOCH_W-1:0] epoch,
  input  logic [31:0]        residency,
  input  logic               stale_rejected,
  input  logic [31:0]        cycle,
  input  logic               freeze,
  input  logic               dbg_clear,
 
  output contract_event_t history [HIST_D],
  output logic [HIST_W-1:0] wr_ptr,
  output logic              wrapped,
  output logic [31:0]       dropped_while_frozen
);
  contract_event_t h_q [HIST_D];
  logic [HIST_W-1:0] wr_q; logic wrap_q; logic [31:0] drop_q;
 
  always_comb for (int i=0;i<HIST_D;i++) history[i]=h_q[i];
  assign wr_ptr=wr_q; assign wrapped=wrap_q; assign dropped_while_frozen=drop_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || dbg_clear) begin
      wr_q<='0; wrap_q<=1'b0; drop_q<='0;
      for (int i=0;i<HIST_D;i++) h_q[i]<='0;
    end else if (state_enter) begin
      if (freeze) begin
        if (!(&drop_q)) drop_q <= drop_q + 32'd1;
      end else begin
        // Sampled WITH the transition. Reading guards afterwards gets the
        // NEW state's conditions (§5, §17 mutation 11).
        h_q[wr_q] <= '{from_state: prev_state, to_state: state,
                       guards_true: guards_true, selected: selected,
                       epoch: epoch, residency: residency,
                       stale_rejected: stale_rejected, cycle: cycle};
        if (HIST_D == 1) wrap_q <= 1'b1;
        else if (wr_q == HIST_W'(HIST_D-1)) begin wr_q<='0; wrap_q<=1'b1; end
        else wr_q <= wr_q + HIST_W'(1);
      end
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import ltssm_contract_pkg::*;
 
// SYNTHESIZABLE. Progress milestones and loop detection (§10).
// §15: two runs both re-entered 1,002 times; one gained 3 milestones and
// one gained 0. Re-entry count CANNOT separate them -- progress can.
module ltssm_progress_loop #(parameter int unsigned LOOP_LIMIT = 64) (
  input  logic clk,
  input  logic rst_n,
  input  logic       state_enter,
  input  logic [3:0] state,
  input  logic [3:0] prev_state,
  input  logic       ms_speed_phase_done,
  input  logic       ms_more_lanes_locked,
  input  logic       ms_wider_width,
  input  logic       ms_packets_flowed,
  input  logic       dbg_clear,
 
  output logic [N_MILE-1:0] milestones,
  output logic [31:0]       reentry_count,
  output logic              loop_no_progress,     // sticky: the REAL finding
  output logic [31:0]       entries_since_progress
);
  logic [N_MILE-1:0] ms_q;
  logic [31:0] re_q, since_q;
  logic loop_q;
  logic [3:0] last_from_q, last_to_q;
 
  assign milestones=ms_q; assign reentry_count=re_q;
  assign loop_no_progress=loop_q; assign entries_since_progress=since_q;
 
  logic gained;
  assign gained = (ms_speed_phase_done  && !ms_q[0])
               || (ms_more_lanes_locked && !ms_q[1])
               || (ms_wider_width       && !ms_q[2])
               || (ms_packets_flowed    && !ms_q[3]);
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || dbg_clear) begin
      ms_q<='0; re_q<='0; since_q<='0; loop_q<=1'b0;
      last_from_q<='0; last_to_q<='0;
    end else begin
      // Milestones are MONOTONIC within the epoch (§10, P23).
      if (ms_speed_phase_done)  ms_q[0] <= 1'b1;
      if (ms_more_lanes_locked) ms_q[1] <= 1'b1;
      if (ms_wider_width)       ms_q[2] <= 1'b1;
      if (ms_packets_flowed)    ms_q[3] <= 1'b1;
 
      if (state_enter) begin
        // A repeat of the SAME transition pair is a re-entry.
        if ((prev_state == last_from_q) && (state == last_to_q)) begin
          if (!(&re_q)) re_q <= re_q + 32'd1;
          if (gained) since_q <= '0;
          else begin
            if (!(&since_q)) since_q <= since_q + 32'd1;
            // A loop is re-entry WITHOUT progress -- never repetition alone.
            if (since_q >= 32'(LOOP_LIMIT)) loop_q <= 1'b1;
          end
        end else if (gained) since_q <= '0;
        last_from_q <= prev_state; last_to_q <= state;
      end
    end
  end
endmodule

Classification: all seven synthesizable debug hooks.

Failure — eight. Guards collapsed into one can_exit (§5). Guards read after the transition. No epoch check (12,738 stale transitions). == LIMIT instead of >= on the next-state value (never fires at step ≥ 3). Entry actions that do not clear (199,993 dirty entries). A loop detector based on repetition alone (1,002 = 1,002). A history that records only the destination. And an epoch transmitted as if it were a protocol field.

14. Same-Cycle Priority and Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---- GUARDS AND PRIORITY (§4, §5) -------------------------------
// P1: guards are exposed individually, never collapsed.
property p_guards_individual;
  @(posedge clk) disable iff (!rst_n)
    any_guard |-> (|guards_true);
endproperty
 
// P2: the selected guard was actually true.
property p_selected_was_true;
  @(posedge clk) disable iff (!rst_n)
    any_guard |-> guards_true[selected];
endproperty
 
// P3: with no guard true, the state does not change.
property p_no_guard_no_transition;
  @(posedge clk) disable iff (!rst_n)
    (!any_guard) |=> $stable(state);
endproperty
 
// P4: RESET OUTRANKS EVERYTHING. A machine that can ignore reset because
// another guard was also true is not resettable (§4).
property p_reset_priority_absolute;
  @(posedge clk) disable iff (!rst_n)
    guards_true[G_RESET] |-> (selected == G_RESET);
endproperty
 
// P5: success outranks timeout -- the declared policy (§4).
// §15: inverting this took the wrong edge 1,333 times.
property p_success_over_timeout;
  @(posedge clk) disable iff (!rst_n)
    (guards_true[G_SUCCESS] && guards_true[G_TIMEOUT] && !guards_true[G_RESET]
      && !guards_true[G_DISABLE]) |-> (selected == G_SUCCESS);
endproperty
 
// P6: the observer's priority matches the FSM's actual choice.
property p_observer_matches_fsm;
  @(posedge clk) disable iff (!rst_n)
    (state_enter && !freeze) |=> (history[$past(wr_ptr)].selected == $past(selected));
endproperty
 
// P7: contention is COUNTED -- evidence that priority matters here.
property p_contention_counted;
  @(posedge clk) disable iff (!rst_n)
    multiple_guards |=> (contention_count == $past(contention_count) + 32'd1);
endproperty
 
// P8: EVIDENCE -- simultaneous guards actually occurred in this run.
property c_guard_contention_reached;
  @(posedge clk) disable iff (!rst_n) multiple_guards;
endproperty
 
// ---- ENTRY ACTIONS (§9) -----------------------------------------
// P9: on entry, the state's own counters read zero. §15: omitting entry
// actions produced 199,993 dirty entries out of 200,000 transitions.
property p_entry_clears_counters;
  @(posedge clk) disable iff (!rst_n)
    state_enter |=> ((state_timer == '0) && (event_count == '0));
endproperty
 
// P10: a dirty entry is REPORTED and names the field.
property p_dirty_entry_flagged;
  @(posedge clk) disable iff (!rst_n)
    ($past(state_enter) && ((state_timer != '0) || (event_count != '0)))
      |=> (entry_dirty && (dirty_fields != '0));
endproperty
 
// P11: the first dirty field is latched, not overwritten by later ones.
property p_dirty_fields_sticky;
  @(posedge clk) disable iff (!rst_n)
    (entry_dirty && !dbg_clear) |=> $stable(dirty_fields);
endproperty
 
// P12: a state does not exit on its own entry cycle -- if it does, the
// guard was satisfied by residue (§9, §12's figure).
property p_no_exit_on_entry_cycle;
  @(posedge clk) disable iff (!rst_n)
    state_enter |-> !(state_enter && $past(state_enter));
endproperty
 
// ---- TIMERS (§6, §7) --------------------------------------------
// P13: the timer restarts at entry.
property p_timer_restarts_on_entry;
  @(posedge clk) disable iff (!rst_n)
    state_enter |=> (timer == '0);
endproperty
 
// P14: `timer_done` uses the NEXT-STATE value with >=, so an overshoot
// cannot step over the threshold. §15: `== LIMIT` never fires at step 3.
property p_timer_next_state_compare;
  @(posedge clk) disable iff (!rst_n)
    (tick && ((timer + 32'(step)) >= 32'(LIMIT))) |-> timer_done;
endproperty
 
// P15: entering already expired is REPORTED, not silently transitioned.
property p_entered_expired_flagged;
  @(posedge clk) disable iff (!rst_n)
    (timer_done && $past(state_enter)) |=> entered_expired;
endproperty
 
// ---- EPOCH AND STALE EVENTS (§8) --------------------------------
// P16: the epoch increments exactly once per transition.
property p_epoch_once_per_transition;
  @(posedge clk) disable iff (!rst_n)
    state_enter |=> (epoch == $past(epoch) + EPOCH_W'(1));
endproperty
 
// P17: a stale event is never qualified. §15: 12,738 transitions fired by
// a stale event when this check is absent.
property p_stale_never_qualified;
  @(posedge clk) disable iff (!rst_n)
    (event_valid && (event_epoch != epoch)) |-> !event_qualified;
endproperty
 
// P18: rejected stale events are COUNTED, never silently dropped.
property p_stale_counted;
  @(posedge clk) disable iff (!rst_n)
    event_stale |=> (stale_rejected_count == $past(stale_rejected_count) + 32'd1);
endproperty
 
// P19: a transition never fires on a stale event.
property p_no_transition_on_stale;
  @(posedge clk) disable iff (!rst_n)
    (state_enter && $past(event_stale) && !$past(event_qualified))
      |-> (selected != G_SUCCESS);
endproperty
 
// ---- HISTORY (§13) ----------------------------------------------
// P20: guards are sampled WITH the transition, not read afterwards.
property p_guards_sampled_at_transition;
  @(posedge clk) disable iff (!rst_n)
    (state_enter && !freeze) |=> (history[$past(wr_ptr)].guards_true == $past(guards_true));
endproperty
 
// P21: a frozen history is immutable and drops are counted.
property p_frozen_history;
  @(posedge clk) disable iff (!rst_n)
    freeze |=> $stable(history[0]);
endproperty
 
// P22: the write pointer is in range -- HIST_D = 1 included.
property p_hist_ptr_range;
  @(posedge clk) disable iff (!rst_n) (wr_ptr < HIST_W'(HIST_D));
endproperty
 
// ---- PROGRESS AND LOOPS (§10) -----------------------------------
// P23: milestones are MONOTONIC within an epoch -- a link that gains and
// loses the same milestone would otherwise report progress forever.
property p_milestones_monotonic;
  @(posedge clk) disable iff (!rst_n)
    (!dbg_clear) |=> ((milestones & $past(milestones)) == $past(milestones));
endproperty
 
// P24: a re-entry WITH progress resets the no-progress counter.
property p_progress_resets_counter;
  @(posedge clk) disable iff (!rst_n)
    (state_enter && gained) |=> (entries_since_progress == '0);
endproperty
 
// P25: the loop flag requires re-entry WITHOUT progress. §15: 1,002
// re-entries with 3 milestones is a legal retrain, not a loop.
property p_loop_requires_no_progress;
  @(posedge clk) disable iff (!rst_n)
    $rose(loop_no_progress) |-> ($past(entries_since_progress) >= 32'(LOOP_LIMIT));
endproperty
 
// P26: the loop flag is sticky until an explicit clear.
property p_loop_sticky;
  @(posedge clk) disable iff (!rst_n)
    (loop_no_progress && !dbg_clear) |=> loop_no_progress;
endproperty
 
// ---- STATE HYGIENE ----------------------------------------------
// P27: the state encoding is always defined (18.1 §10).
property p_state_encoding_valid;
  @(posedge clk) disable iff (!rst_n)
    (state inside {ST_DETECT, ST_POLLING, ST_CONFIGURATION, ST_L0,
                   ST_RECOVERY, ST_L0S, ST_L1, ST_DISABLED, ST_HOT_RESET});
endproperty
 
// P28: no debug output drives the state machine.
property p_debug_non_functional;
  @(posedge clk) disable iff (!rst_n)
    $stable({state, next_state}) or !$stable({stale_rejected_count, reentry_count});
endproperty
 
// P29: reset clears every contract instrument.
property p_reset_clears_instruments;
  @(posedge clk) disable iff (!rst_n)
    (!rst_n) |=> (!entry_dirty && !loop_no_progress && (epoch == '0));
endproperty
 
// P30: counters saturate rather than wrap.
property p_counters_saturate;
  @(posedge clk) disable iff (!rst_n)
    (reentry_count == 32'hFFFF_FFFF) |=> (reentry_count == 32'hFFFF_FFFF);
endproperty
 
// P31: EVIDENCE -- a stale event was actually presented in this run.
property c_stale_event_reached;
  @(posedge clk) disable iff (!rst_n) event_stale;
endproperty

Thirty-one properties. P4–P7 are the priority contract, and P5 is the one §15 measured being violated 1,333 times. P17–P19 are the epoch contract — 12,738 violations without it — and P25 is the property that stops a loop detector from being a false-positive generator. P8 and P31 are the cover properties: without them, a run in which contention and stale events never occurred reports the same clean result as a correct design.

15. Measured Behaviour

16. Executable Counterexamples

Six traces, each minimal.

A — the old-value timer steps over its threshold. LIMIT = 8, step = 3. Timer goes 0, 3, 6, 9, 12… timer == 8 is never true. The state waits forever while its counter climbs. Fix: timer_next >= LIMIT (§7, P14).

B — a stale event crosses a state boundary. At cycle 10 the detector observes training evidence and stamps epoch 4. At cycle 11 the machine transitions; the epoch becomes 5. At cycle 12 the event is delivered and satisfies the new state's success guard. The transition is taken for evidence gathered in a state that no longer exists. §15 measured 12,738 such events. Fix: qualify on the epoch (§8, P17).

C — simultaneous timeout and success choose the wrong edge. Both guards true in one cycle; the if/else chain tests timeout first. The state falls back and retrains despite having succeeded — §15's 1,333 wrong selections. Fix: declare the priority and assert it (§4, P5).

D — the entry action fails to clear the event counter. The state is entered with the previous occupancy's count already above threshold. g_success is true on the entry cycle and the state exits having established nothing (§12's figure). Fix: clear on entry; assert counters are zero one cycle later (P9).

E — the transition reason is sampled one cycle late. The guard vector is read after state changed, so it reflects the new state's conditions. The history reports a plausible reason that had nothing to do with the transition — §15's 5,556 skewed captures. Fix: sample with the transition (§13, P20).

F — the loop detector false-positives on a legal retrain. A link re-enters Recovery 1,002 times while completing speed phases and locking more lanes. A repetition-threshold detector flags it as a loop; the link is converging. Fix: require re-entry without progress (§10, P25).

17. Verification — Mutations

#MutationSymptomCaught by
1Timer not reset on entryenters expired; leaves immediatelyP9, P13
2Timer increments while outside the stateexpires on an occupancy that just beganP13
3timer == LIMIT at unit stepone cycle lateP14
4timer == LIMIT with step > 1never fires; 20.0% of trials (§15)P14
5Old-value compare for a same-cycle decisionthe decision lags the counterP14
6Stale event satisfies the current guard12,738 wrong-reason transitions (§15)P17, P19
7Epoch omitted entirelyevery delayed event is acceptedP16, P17
8Epoch stamped at delivery, not detectionrecords arrival, not observationP17
9Stale events dropped silentlylooks identical to never receiving themP18
10Two guards true, priority undocumentedthe edge taken varies build to buildP5, P7
11Guard vector read after the transitionthe new state's conditions recorded (§16 E)P20
12Reset loses priority to another guardthe machine is not resettableP4
13Disable loses priorityan explicit command ignoredP2
14Selected guard was not actually truethe history is fictionP2
15Entry pulse held two cyclesevery transition counted twiceP12
16Entry action executed every resident cyclecounters permanently zero; guards never satisfiedP9
17Exit action runs on a combinational next-state glitchthe next state inherits a half-done exitP28
18Transition counter counts attempted, not acceptedthe history and the counter disagreeP6
19An invalid state encoding defaults to L0an illegal state reported as operationalP27
20State width truncatedtwo states aliasP27
21Illegal transition silently acceptedthe machine leaves a state it cannot leaveP3
22Loop detector fires on repetition alonea legal retrain reported as a loop (§15)P25
23Loop detector misses an A→B→C→A cyclea real loop unreportedP25
24Milestones cleared on every entryprogress never accumulates; everything is a loopP23
25A debug budget reported as a normative timeouta specification violation claimed that did not occur (§6)review
26The next-state pattern applied to a normative timerlocal arithmetic overriding a specified behaviour (§7)review
27Watchdog reset by irrelevant debug activitynever fires on the failure it exists to catchP24
28Frozen history continues writingthe captured window destroyedP21
29Residency counter wrapsa stuck state reports a small numberP30
30The epoch transmitted as a protocol fielda PCIe field invented (23.6 §7)review
31History ring assumes a power-of-two depthout-of-range write at depth 1P22
32Debug logic feeds next_statethe measured machine is not the shipping oneP28
33An async PHY indication consumed without synchronizationmetastable evidence drives a transitionreview
34An exact LTSSM transition rule written from memoryfails correct hardware with total confidence (§1)review
35A vendor substate described as universal PCIeone implementation generalizedreview
36The transition cause inferred from the destinationtwo guards leading to one state are indistinguishableP20
37Current speed confused with target speeda successful phase reported as a failurereview
38Lanes aggregated so one bad lane is invisiblea per-lane fault reported as a link faultreview
39A configuration field changed during an owned transitionthe transition uses two different configurationsP28

Two counterexamples worth stating explicitly.

Mutation 4 is the one that produces a hang with no evidence of a hang. timer == LIMIT looks correct in review, passes any test where the counter increments by one, and never fires the moment the counter aggregates across lanes or counts events rather than cycles. §15 measured a 20.0% miss rate with a 1–3 step — and the symptom is a state resident forever with a timer that is visibly running, which is the most misleading combination in this chapter. Every instinct says the timer is working. P14 and a >= are the entire fix.

Mutation 22 is the one that discredits the instrument. A loop detector that fires on repetition alone reports every legal retrain as a fault. §15's two runs both re-entered 1,002 times — one gaining three milestones, one gaining none. After a few false positives, engineers stop reading the flag, and the real loop it was built for goes unnoticed. P25 requires re-entry without progress, which is the difference between an instrument people trust and one they mute.

18. Debugging

Symptom — the state never exits and its guards look correct. Look at the entry actions, not the guards (§9). If a counter the guard depends on was not cleared, the guard's logic is fine and its input is wrong. entry_dirty and dirty_fields name it in one read — §15 measured 199,993 dirty entries when entry actions are omitted.

Symptom — the state exits immediately, having done nothing. Same cause, opposite symptom (§9, §12's figure). Residue from the previous occupancy satisfies the guard on the entry cycle. Check whether g_success is true at state_enter — P12 asserts it must not be.

Symptom — the state alternates between two values forever. Read the guard vector in the history for each transition. A→B on success and B→A on timeout is a state pair where one guard never becomes true — and the guard vector says which condition is missing, which is precisely what 25.3 could not tell you (§5).

Symptom — a transition happened although the guard appears false. A stale event (§8, §16 B). The guard was true when the event arrived; the event belonged to a previous state. Read stale_rejected_count: if it is zero and the design has no epoch, that is the finding — nothing was being rejected because nothing was being checked.

Symptom — the state exits one cycle later than expected. Old-value timer comparison (§7, §16 A). Harmless in itself; check the increment. If the counter can step by more than one, the same bug becomes "never exits", and §15 measured that at 20.0%.

Symptom — the timer is visibly counting and the state never leaves. The threshold was stepped over (mutation 4). This is the case that looks least like a timer bug. Compare the counter against the limit: if it is above and still counting, == is the comparison.

Symptom — Recovery is entered periodically under traffic. Read the milestone vector, not the count (§10). §15's two runs both re-entered 1,002 times with completely different meanings. Milestones accumulating means the link is converging; zero milestones across many entries is a loop — and 18.5 §5's four substates then say which of four problems it is.

Symptom — the reason register always reports the same cause. Sampled after the event it describes (§16 E, mutation 11). It is reading the conditions the new state created. §15 measured 5,556 skewed captures, and the tell is a reason that never varies across genuinely different failures.

Symptom — it works in RTL simulation and fails on the FPGA. Two candidates. A detector pipeline that is shorter in simulation makes stale events rare — the epoch bug appears only when the delay is real (§8). Or an asynchronous PHY indication consumed without synchronization (mutation 33), which simulation may not model.

Symptom — only fails with simultaneous reset and retrain. Guard priority (§4, P4). Reset must outrank everything unconditionally; a machine that can be talked out of resetting by a coincident guard is not resettable, and the failure is by definition rare and reproducible only under that coincidence.

Symptom — lane 0 trains and another lane does not, and the link reports a generic failure. Lane aggregation hiding a per-lane fault (mutation 38). Read the per-lane mask (25.3 §11) rather than an aggregated "all lanes ready" — the aggregate cannot distinguish "no lane locked" from "seven of eight locked."

19. Misconceptions

"The state is stuck." A state is five contracts; say which one (§2).

"Residency tells you what is missing." It tells you where25.3 §13's identical rows (§5).

"Guard order in the code is the priority." It is a priority; declare the one you intend (§4).

"Timeout should win — it is the safety net." Success arriving means the wait is over; 1,333 wrong edges otherwise (§15).

"== LIMIT is fine, the counter increments by one." Until it does not — 20.0% miss rate (§7).

"A one-cycle-late transition is harmless." Usually. The same bug becomes a hang at step ≥ 2 (§7).

"The event arrived, so the guard is satisfied." Not if it belongs to a previous state (§8).

"Dropping stale events silently is fine, they are stale." Then you cannot tell them from events that never arrived (§8).

"Entry actions are bookkeeping." They are why a state can leave immediately or never (§9).

"Many re-entries means a loop." 1,002 = 1,002 with opposite meanings (§10).

"One health score summarizes progress." Four substates are four problems (18.5 §5).

"The epoch is basically a sequence number." It is local state; transmitting it invents a protocol (23.6 §7).

"A debug budget expiring is a protocol violation." Three kinds of timer, never conflated (§6).

"The destination tells you the reason." Two guards can lead to one state (mutation 36).

20. Understanding Check

Q1. Two links sit in the same state with identical residency and loop counts. How do you separate them? The guard vector (§5). Residency names the state; it cannot name the missing condition, because both faults gate the same edge — that is exactly 25.3 §13's measured result. Exposing each exit condition individually makes the difference one bit: guard[0]=0, guard[1]=0 is no training evidence; guard[0]=1, guard[1]=0 is evidence received without lock.

Q2. A state's timer is visibly counting and the state never exits. What is your first hypothesis? The threshold comparison is == and the counter stepped over it (§7, mutation 4). §15 measured == LIMIT never firing at increments of 3 or more, and missing in 20.0% of trials with a 1–3 step. The tell is exactly what you observe: a healthy-looking counter above the limit, still counting. The fix is timer_next >= LIMIT.

Q3. A transition occurred and the guard that supposedly caused it reads false. Explain. A stale event (§8, §16 B). The event was detected while the machine was in a previous state, delivered a few cycles later, and satisfied this state's guard — so the guard was true at the moment the decision was made and false when you read it. §15 measured 12,738 such transitions in 200,000 cycles. The epoch is what distinguishes them, and P17 asserts a stale event is never qualified.

Q4. Success and timeout become true in the same cycle. Which should win, and why is the answer not arbitrary? Success (§4, P5). The timeout exists to bound the wait for success; if success arrived, the wait is over, and falling back discards a completed negotiation because a counter happened to reach its limit simultaneously. §15 measured 1,333 wrong edges when the priority is inverted — each one an unnecessary retrain. Reset, however, outranks both unconditionally (P4), because a machine that can ignore reset is not resettable.

Q5. Your loop detector fires on a link that is training successfully. What is wrong with it? It counts repetition rather than measuring progress (§10, mutation 22). §15's two runs both re-entered 1,002 times; one gained three milestones and one gained none. A repetition threshold cannot separate them at any setting — and after a few false positives the flag gets muted, so the real loop it exists for goes unnoticed. P25 requires re-entry without progress.

Q6. A state leaves immediately after entry. Where do you look, and where do you not look? Look at the entry actions; do not look at the exit guards (§9). The guard logic is almost certainly correct — it was satisfied by a counter the entry action should have cleared. §15 measured 199,993 dirty entries out of 200,000 transitions when entry clearing is omitted, and P9 turns it into a single assertion: one cycle after entry, the state's own counters read zero.

Q7. Why does this chapter publish no LTSSM timer values or exit conditions? Because it cannot source them (§1), and a debugging chapter that invents one produces a false diagnosis stated with total confidence — mutation 34. Module 18 owns the states and their conditions; this chapter owns how to instrument whatever they are. §6's three-way timer classification exists so that every threshold in §13 is explicitly a debug heuristic, and a reader can never mistake one for a specification requirement.

21. Module 25 So Far

Four chapters, each one layer deeper.

ChapterAsks
25.1 Debugging Overviewwhich layer, and what is the last provable event?
25.2 Enumeration Failureswhere did the configuration conversation stop?
25.3 Link Training Failureswhich state is it in, and which exit is missing?
25.4 (this)which contract inside the state is wrong?

And the escalation is measurable. 25.2 turned one symptom into eight milestones. 25.3 turned "won't train" into five distinguishable residency signatures — and hit its limit when two faults shared an edge. This chapter turned that edge into a guard vector, and every contract inside it into an instrument with a measured failure rate.

One idea runs through all four, and it is the same one Modules 22–24 kept producing: a design that records only outcomes cannot be debugged. A transition without its guard vector, an error without its first-failure record, a milestone without its link state, a re-entry without its progress — each is an outcome with the evidence discarded.

Five chapters remain, and they return to the layers above. 25.5 takes the BAR stage 25.2 §9 handed over; 25.6 the DMA path; 25.7 completion timeouts; 25.8 credit deadlocks; and 25.9 the protocol analyzer that 25.1 §3 argued should never be the first tool — and is the right one once these four chapters have produced the question.