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:
| Contract | Asks | Broken looks like |
|---|---|---|
| entry actions | what happens exactly once on arrival? | leaves immediately, or never leaves (§9) |
| resident invariants | what stays true while here? | inconsistent status while occupied |
| exit guards | what permits leaving? | never exits, or exits when it should not |
| guard priority | which wins when several are true? | takes the wrong edge (§4) |
| exit actions | what 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:
| # | Question | Instrument (§13) |
|---|---|---|
| 1 | Did we really enter the state? | the entry pulse and transition history |
| 2 | Did the entry actions run exactly once? | entry-clear observation |
| 3 | Are the resident invariants holding? | status snapshot while occupied |
| 4 | Which exit guards are currently true? | the guard vector (§5) |
| 5 | Which guard has priority? | the declared priority encoder (§4) |
| 6 | Did the chosen transition actually occur? | transition-reason capture |
| 7 | Did the exit actions run? | next-state entry conditions |
| 8 | Did 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 entry | Symptom |
|---|---|
| the state timer | enters already expired → leaves immediately |
| the training/event counter | a previous state's evidence satisfies this guard → leaves immediately, wrongly |
| the lane mask | stale lanes appear locked → wrong width negotiated |
| the state epoch | stale events accepted (§8) |
| a speed-change flag | a 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 exit | Symptom in the next state |
|---|---|
| release the transmitter/owner | the next state cannot drive |
| clear a status flag | software reads a stale condition |
| consume the pending event | the 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.
10. Legal Retrain Versus a Real Loop
11. The Loop, Drawn
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 cyclesFour 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
// 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;
endpackageimport 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
endmoduleimport 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
endmoduleimport 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
endmoduleimport 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
endmoduleimport 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
endmoduleimport 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
endmoduleClassification: 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
// ---- 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;
endpropertyThirty-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
| # | Mutation | Symptom | Caught by |
|---|---|---|---|
| 1 | Timer not reset on entry | enters expired; leaves immediately | P9, P13 |
| 2 | Timer increments while outside the state | expires on an occupancy that just began | P13 |
| 3 | timer == LIMIT at unit step | one cycle late | P14 |
| 4 | timer == LIMIT with step > 1 | never fires; 20.0% of trials (§15) | P14 |
| 5 | Old-value compare for a same-cycle decision | the decision lags the counter | P14 |
| 6 | Stale event satisfies the current guard | 12,738 wrong-reason transitions (§15) | P17, P19 |
| 7 | Epoch omitted entirely | every delayed event is accepted | P16, P17 |
| 8 | Epoch stamped at delivery, not detection | records arrival, not observation | P17 |
| 9 | Stale events dropped silently | looks identical to never receiving them | P18 |
| 10 | Two guards true, priority undocumented | the edge taken varies build to build | P5, P7 |
| 11 | Guard vector read after the transition | the new state's conditions recorded (§16 E) | P20 |
| 12 | Reset loses priority to another guard | the machine is not resettable | P4 |
| 13 | Disable loses priority | an explicit command ignored | P2 |
| 14 | Selected guard was not actually true | the history is fiction | P2 |
| 15 | Entry pulse held two cycles | every transition counted twice | P12 |
| 16 | Entry action executed every resident cycle | counters permanently zero; guards never satisfied | P9 |
| 17 | Exit action runs on a combinational next-state glitch | the next state inherits a half-done exit | P28 |
| 18 | Transition counter counts attempted, not accepted | the history and the counter disagree | P6 |
| 19 | An invalid state encoding defaults to L0 | an illegal state reported as operational | P27 |
| 20 | State width truncated | two states alias | P27 |
| 21 | Illegal transition silently accepted | the machine leaves a state it cannot leave | P3 |
| 22 | Loop detector fires on repetition alone | a legal retrain reported as a loop (§15) | P25 |
| 23 | Loop detector misses an A→B→C→A cycle | a real loop unreported | P25 |
| 24 | Milestones cleared on every entry | progress never accumulates; everything is a loop | P23 |
| 25 | A debug budget reported as a normative timeout | a specification violation claimed that did not occur (§6) | review |
| 26 | The next-state pattern applied to a normative timer | local arithmetic overriding a specified behaviour (§7) | review |
| 27 | Watchdog reset by irrelevant debug activity | never fires on the failure it exists to catch | P24 |
| 28 | Frozen history continues writing | the captured window destroyed | P21 |
| 29 | Residency counter wraps | a stuck state reports a small number | P30 |
| 30 | The epoch transmitted as a protocol field | a PCIe field invented (23.6 §7) | review |
| 31 | History ring assumes a power-of-two depth | out-of-range write at depth 1 | P22 |
| 32 | Debug logic feeds next_state | the measured machine is not the shipping one | P28 |
| 33 | An async PHY indication consumed without synchronization | metastable evidence drives a transition | review |
| 34 | An exact LTSSM transition rule written from memory | fails correct hardware with total confidence (§1) | review |
| 35 | A vendor substate described as universal PCIe | one implementation generalized | review |
| 36 | The transition cause inferred from the destination | two guards leading to one state are indistinguishable | P20 |
| 37 | Current speed confused with target speed | a successful phase reported as a failure | review |
| 38 | Lanes aggregated so one bad lane is invisible | a per-lane fault reported as a link fault | review |
| 39 | A configuration field changed during an owned transition | the transition uses two different configurations | P28 |
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 where — 25.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.
| Chapter | Asks |
|---|---|
| 25.1 Debugging Overview | which layer, and what is the last provable event? |
| 25.2 Enumeration Failures | where did the configuration conversation stop? |
| 25.3 Link Training Failures | which 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.