Skip to content
VLSI Mentor

USB · Module 20

Link Training

A SuperSpeed link must train itself before carrying anything — and cannot use the link to do it, so the earliest signalling runs with the high-speed transmitter off.

Chapter 20.2 treated ss_training_ok and ss_training_fail as inputs — signals that arrive from somewhere and decide which bus wins.

This is that somewhere.

Drive a USB 2 pair after a reset and both ends already agree on what the voltages mean and roughly when to sample them. Nothing has to be negotiated because nothing was ever in question.

A SuperSpeed link cannot carry one packet until it has:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   1. found out whether there is a receiver at the far end AT ALL
   2. agreed on bit timing, recovered a clock, aligned symbol boundaries
   3. confirmed all of that in BOTH directions

That is what "the link trains itself" means, and it is the deepest architectural difference between USB 2 and USB 3 — deeper than the speed, deeper than the extra wires.

2. The Chicken and the Egg

Steps 1 and 2 cannot use the trained link, because the link is what they are trying to train.

So the earliest signalling happens with the high-speed transmitter switched off, using LFPS — Low Frequency Periodic Signaling — which is slow enough that an untrained receiver can detect it without a recovered clock.

LFPSHigh speed
Frequencylow, out of band5 Gb/s, in band
Needs a trained receiver?noyes
Usedbefore training, and in U3once the link is up

3. Rx.Detect Is Not a Handshake

The first step is a DC measurement, not an exchange.

The transmitter briefly changes the common-mode voltage and watches how fast the line settles. A terminated receiver at the far end loads it differently from an open cable — so the answer comes from the electrical behaviour of the wire, not from anything the far end chooses to send.

Nothing is sent and nothing replies, which is why it works against a device that is not yet powered enough to answer.

And so there is no timeout in Rx.Detect. An empty port simply keeps looking, for ever, at almost no cost. A timeout there would be a timeout on a question nobody was asked.

4. Polling Times Out Into Compliance, Not Into Failure

This is the detail that surprises people.

A link that reaches Polling and never completes does not go to an error state. It goes to Compliance — a mode that exists for test equipment to drive specified patterns into.

The reasoning is sound and worth following. A port stuck in Polling with nothing at the far end is exactly what a compliance tester looks like: something is there electrically, it is exchanging LFPS, and it never completes training. A port that fell into an error state instead would be impossible to test, because the act of testing it would break it.

Mutation L2 sends the timeout to SS.Inactive instead and dies 14 944 times.

Recovery is different, and fails properly. A link that was up and cannot be retrained is broken rather than absent, and a tester is not the likely explanation — so Recovery times out into SS.Inactive, which is what 20.2's arbiter reads as give up on SuperSpeed.

5. Two Dead Ends, One Fallback Signal

training_fail covers both. SS.Inactive and Compliance are different states for different reasons — but from the arbiter's point of view they are identical: neither will ever carry data, and both need the same decision taken about them.

Mutation L6 covers only SS.Inactive and dies 7472 times — the smallest count in Module 20, because it is wrong only while the link sits in Compliance, which is one of ten states.

6. The State Machine, Drawn

A state machine diagram of the SuperSpeed Link Training and Status State Machine, with eight nodes arranged in three rows. Along the top row, from left to right: Disabled, which is where the machine sits when the port is not enabled; Rx dot Detect, where the transmitter performs a DC measurement to discover whether a terminated receiver exists at the far end, sending nothing and expecting no reply; Polling, where the two ends exchange low frequency periodic signalling and then training sequences; and U0, the state in which the link actually carries data. In the middle row are Compliance on the left, Recovery in the centre, and the merged low power states U1, U2 and U3 on the right. In the bottom row, below Recovery, is SS dot Inactive. The transitions are as follows. Enabling the port moves Disabled to Rx dot Detect. Detecting a receiver moves Rx dot Detect to Polling; there is no timeout on this step because nothing was asked of the far end. Completing training moves Polling to U0. If Polling instead times out it moves to Compliance, which is a test mode rather than an error state, because a port stuck in Polling is indistinguishable from a port attached to a compliance tester. From U0, a link error moves to Recovery, and a low power request moves to the merged U1, U2 and U3 node. A wakeup from any low power state returns through Recovery rather than directly to U0, because the receiver must re-lock before data can flow. Recovery returns to U0 if retraining succeeds, and times out into SS dot Inactive if it does not, because a link that was up and cannot be retrained is broken rather than absent. Both Compliance and SS dot Inactive are dead ends that produce the training failure signal the dual bus arbiter of the previous chapter reads, and both are escaped only by a warm reset, which returns to Rx dot Detect from any state at all.DisabledRx.DetectPollingU0ComplianceRecoveryU1 / U2 /U3SS.Inactiveenableenablereceiver detected (DC)receiver detected (DC)receiverdetected…trainedtrainedtimeout → TEST modetimeout → TEST modelink errorlink errorretrainedretrainedtimeout → FAILUREtimeout → FAILURElow-power requestlow-power requestwakeupwakeup
Figure 1 — the LTSSM. U1, U2 and U3 are drawn as one node: they differ in how deeply the PHY powers down, not in how they are entered or left, and all three return through Recovery. Note the two exits that are NOT the same: Polling times out into Compliance (a test mode), while Recovery times out into SS.Inactive (a failure). A warm reset returns to Rx.Detect from any state.

The two timeout edges are the chapter. They leave states that look similar and arrive at states that mean opposite things.

7. The Hardware, Before Any Language

Ten states, one timer, and two transmitter enables that are decodes of the state.

LFPS is driven in Rx.Detect, Polling, Compliance and U3. The first three are before-or-instead-of training; U3 is the interesting one — it is SuperSpeed's suspend, the link is fully down, and LFPS is the only thing that can wake it. That is why lfps_tx is asserted there and hs_tx_enable is not.

An error outranks a low-power request in U0. A link that has gone bad must not be put to sleep — it would wake into the same fault, having lost the chance to recover from it. Mutation L3, 38 805 errors.

Every low-power state returns through Recovery, never straight to U0, because the receiver has to re-lock before data can flow. Mutation L4, 51 591 errors.

And a warm reset works from every state, not only the dead ones — a reset that only escaped SS.Inactive and Compliance could not recover a link that was merely confused. Mutation L7, 92 808 errors — the largest in Module 20 after 20.2's D3.

8. Verilog-2005

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// usb3_ltssm -- the Link Training and Status State Machine, and the
// chicken-and-egg problem at the start of it.
//
// A USB 2 bus simply works after a reset. Drive the lines, and both ends
// already agree on what the voltages mean and roughly when to sample them.
//
// A SuperSpeed link does not work after a reset. Before one packet can be
// carried it must:
//
//   1. find out whether there is a receiver at the far end AT ALL;
//   2. agree on bit timing, recover a clock, and align symbol boundaries;
//   3. confirm all of that in BOTH directions;
//
// and only then carry data. That is what "the link trains itself" means, and
// it is the deepest architectural difference between USB 2 and USB 3.
//
// THE CHICKEN AND EGG
//
// Steps 1 and 2 cannot use the trained link, because the link is what they
// are trying to train. So the earliest signalling happens with the
// high-speed transmitter SWITCHED OFF, using LFPS -- Low Frequency Periodic
// Signaling -- which is slow enough that an untrained receiver can detect it
// without a recovered clock.
//
//   LFPS         out-of-band, low frequency, works before training
//   HIGH SPEED   in-band, 5 Gb/s, requires a trained receiver
//
// They are two different transmitters on the SAME wires, so the one property
// this block must never violate is that BOTH ARE NEVER ON AT ONCE. That is
// not a protocol error; it is two drivers fighting over a differential pair.
//
// AND Rx.Detect IS NOT A HANDSHAKE
//
// The first step is a DC measurement, not an exchange. The transmitter
// briefly changes the common-mode voltage and watches how fast the line
// settles; a terminated receiver at the far end loads it differently from an
// open cable. Nothing is sent and nothing replies -- which is why it works
// against a device that is not powered enough to answer yet.
//
// POLLING TIMES OUT INTO COMPLIANCE, NOT INTO FAILURE
//
// The detail that surprises people. A link that reaches Polling and never
// completes does NOT go to an error state -- it goes to COMPLIANCE, a mode
// that exists for test equipment to drive specified patterns into. The
// reasoning is that a port stuck in Polling with nothing at the far end is
// exactly what a compliance tester looks like, and a port that fell into an
// error state instead would be untestable.
module usb3_ltssm #(
  parameter integer POLL_TIMEOUT     = 12,  // Polling -> Compliance
  parameter integer RECOVERY_TIMEOUT = 8    // Recovery -> SS.Inactive
) (
  input  wire       clk,
  input  wire       rst_n,

  input  wire       enable,           // the port is enabled at all
  input  wire       rx_detected,      // a terminated receiver is out there
  input  wire       training_done,    // TS1/TS2 exchange completed
  input  wire       link_error,       // an established link went bad
  input  wire       lp_request,       // the host asked for a low-power state
  input  wire [1:0] lp_level,         // which one: U1, U2 or U3
  input  wire       wakeup,           // leave the low-power state
  input  wire       warm_reset,       // the only way out of the dead states

  output wire [3:0] ltssm_state,
  output wire       lfps_tx,          // the LOW-frequency transmitter
  output wire       hs_tx_enable,     // the HIGH-speed transmitter
  output wire       link_up,          // U0: the link carries data
  output wire       training_ok,      // --> chapter 20.2's arbiter
  output wire       training_fail,
  output reg [15:0] timer,

  output reg [31:0] poll_timeouts,
  output reg [31:0] recoveries,
  output reg        ever_compliance
);
  localparam [3:0] L_DISABLED   = 4'd0,
                   L_RX_DETECT  = 4'd1,
                   L_POLLING    = 4'd2,
                   L_U0         = 4'd3,   // the link is up
                   L_U1         = 4'd4,
                   L_U2         = 4'd5,
                   L_U3         = 4'd6,
                   L_RECOVERY   = 4'd7,
                   L_INACTIVE   = 4'd8,   // failed; waits for a warm reset
                   L_COMPLIANCE = 4'd9;   // test mode, NOT an error state

  reg [3:0] state;

  assign ltssm_state = state;

  // THE MUTUAL EXCLUSION, and it is electrical rather than logical: these
  // are two different transmitters on the SAME differential pair.
  //
  // LFPS is driven in the states that happen BEFORE the link is trained,
  // plus Compliance, where a tester expects it. The high-speed transmitter
  // is enabled only where a trained receiver exists to hear it.
  assign lfps_tx      = (state == L_RX_DETECT) || (state == L_POLLING)
                     || (state == L_COMPLIANCE) || (state == L_U3);
  assign hs_tx_enable = (state == L_U0) || (state == L_RECOVERY);

  assign link_up      = (state == L_U0);
  assign training_ok  = (state == L_U0);
  // The arbiter of chapter 20.2 needs ONE signal meaning "give up on
  // SuperSpeed", and both dead ends produce it: a link that timed out into
  // Compliance and one that failed recovery are equally unusable for data.
  assign training_fail = (state == L_INACTIVE) || (state == L_COMPLIANCE);

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      state           <= L_DISABLED;
      timer           <= 16'd0;
      poll_timeouts   <= 32'd0;
      recoveries      <= 32'd0;
      ever_compliance <= 1'b0;
    end else if (!enable) begin
      // Disabling the port outranks everything, from any state.
      state <= L_DISABLED;
      timer <= 16'd0;
    end else if (warm_reset) begin
      // THE ONLY WAY OUT of Inactive and Compliance -- and it works from
      // every state, because a reset that only worked from the dead states
      // could not recover a link that was merely confused.
      state <= L_RX_DETECT;
      timer <= 16'd0;
    end else begin
      case (state)
        L_DISABLED: begin
          state <= L_RX_DETECT;
          timer <= 16'd0;
        end

        L_RX_DETECT: begin
          // A DC measurement, not an exchange. Nothing is sent and nothing
          // replies, so there is no timeout here -- an empty port simply
          // keeps looking, for ever, at almost no cost.
          if (rx_detected) begin
            state <= L_POLLING;
            timer <= 16'd0;
          end
        end

        L_POLLING: begin
          if (training_done) begin
            state <= L_U0;
            timer <= 16'd0;
          end else if (timer + 16'd1 >= POLL_TIMEOUT[15:0]) begin
            // NOT an error state. A port stuck here with nothing at the far
            // end is indistinguishable from a port attached to a compliance
            // tester, and a port that failed into an error state instead
            // would be impossible to test.
            state           <= L_COMPLIANCE;
            timer           <= 16'd0;
            poll_timeouts   <= poll_timeouts + 32'd1;
            ever_compliance <= 1'b1;
          end else begin
            timer <= timer + 16'd1;
          end
        end

        L_U0: begin
          // Precedence: an error outranks a low-power request. A link that
          // has gone bad must not be put to sleep -- it would wake into the
          // same fault having lost the chance to recover from it.
          if (link_error) begin
            state      <= L_RECOVERY;
            timer      <= 16'd0;
            recoveries <= recoveries + 32'd1;
          end else if (lp_request) begin
            case (lp_level)
              2'd0:    state <= L_U1;
              2'd1:    state <= L_U2;
              default: state <= L_U3;
            endcase
            timer <= 16'd0;
          end
        end

        L_U1, L_U2: begin
          // U1 and U2 differ only in how deeply the PHY is powered down and
          // therefore in how long they take to leave. Both return through
          // RECOVERY rather than straight to U0, because the receiver has
          // to re-lock before data can flow.
          if (wakeup) begin
            state      <= L_RECOVERY;
            timer      <= 16'd0;
            recoveries <= recoveries + 32'd1;
          end
        end

        L_U3: begin
          // U3 is SuperSpeed's suspend, and it is the deepest: the link is
          // fully down and only LFPS can wake it, which is why lfps_tx is
          // asserted here and hs_tx_enable is not.
          if (wakeup) begin
            state      <= L_RECOVERY;
            timer      <= 16'd0;
            recoveries <= recoveries + 32'd1;
          end
        end

        L_RECOVERY: begin
          // Re-training an ESTABLISHED link. It keeps the configuration U0
          // had -- which is what makes recovery cheaper than starting from
          // Rx.Detect, and why a transient does not cost an enumeration.
          if (training_done) begin
            state <= L_U0;
            timer <= 16'd0;
          end else if (timer + 16'd1 >= RECOVERY_TIMEOUT[15:0]) begin
            // Recovery DOES fail into a dead state, unlike Polling: a link
            // that was up and cannot be retrained is broken rather than
            // absent, and a tester is not the likely explanation.
            state <= L_INACTIVE;
            timer <= 16'd0;
          end else begin
            timer <= timer + 16'd1;
          end
        end

        L_INACTIVE: begin
          // Waits for the warm reset handled above. Chapter 20.2's arbiter
          // sees training_fail from here and falls back to USB 2.
          state <= L_INACTIVE;
        end

        L_COMPLIANCE: begin
          // Also waits for the warm reset. A tester drives patterns at the
          // port and the port answers with LFPS; nothing here is an error.
          state <= L_COMPLIANCE;
        end

        default: state <= L_DISABLED;
      endcase
    end
  end
endmodule

training_fail is one expression covering both dead ends (§5) rather than two signals the arbiter would have to OR together. The arbiter needs one bit meaning give up; giving it two would move that OR across a module boundary for no benefit.

9. SystemVerilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
package usb3_ltssm_pkg;
  // The LTSSM states this design distinguishes. Two of them are dead ends
  // and they are NOT the same dead end: L_INACTIVE is a link that was up and
  // could not be retrained, L_COMPLIANCE is a port that never found anything
  // and is now assumed to be attached to a tester.
  typedef enum logic [3:0] {
    L_DISABLED,
    L_RX_DETECT,   // a DC measurement, not an exchange
    L_POLLING,     // LFPS handshake, then TS1/TS2
    L_U0,          // the link carries data
    L_U1,          // light sleep
    L_U2,          // deeper sleep
    L_U3,          // SuperSpeed's suspend -- only LFPS wakes it
    L_RECOVERY,    // re-train an ESTABLISHED link
    L_INACTIVE,    // failed; waits for a warm reset
    L_COMPLIANCE   // test mode -- NOT an error state
  } ltssm_state_e;
endpackage

// usb3_ltssm_sv -- the Link Training and Status State Machine, and the
// chicken-and-egg problem at the start of it.
//
// A USB 2 bus simply works after a reset. Drive the lines, and both ends
// already agree on what the voltages mean and roughly when to sample them.
//
// A SuperSpeed link does not work after a reset. Before one packet can be
// carried it must:
//
//   1. find out whether there is a receiver at the far end AT ALL;
//   2. agree on bit timing, recover a clock, and align symbol boundaries;
//   3. confirm all of that in BOTH directions;
//
// and only then carry data. That is what "the link trains itself" means, and
// it is the deepest architectural difference between USB 2 and USB 3.
//
// THE CHICKEN AND EGG
//
// Steps 1 and 2 cannot use the trained link, because the link is what they
// are trying to train. So the earliest signalling happens with the
// high-speed transmitter SWITCHED OFF, using LFPS -- Low Frequency Periodic
// Signaling -- which is slow enough that an untrained receiver can detect it
// without a recovered clock.
//
//   LFPS         out-of-band, low frequency, works before training
//   HIGH SPEED   in-band, 5 Gb/s, requires a trained receiver
//
// They are two different transmitters on the SAME wires, so the one property
// this block must never violate is that BOTH ARE NEVER ON AT ONCE. That is
// not a protocol error; it is two drivers fighting over a differential pair.
//
// AND Rx.Detect IS NOT A HANDSHAKE
//
// The first step is a DC measurement, not an exchange. The transmitter
// briefly changes the common-mode voltage and watches how fast the line
// settles; a terminated receiver at the far end loads it differently from an
// open cable. Nothing is sent and nothing replies -- which is why it works
// against a device that is not powered enough to answer yet.
//
// POLLING TIMES OUT INTO COMPLIANCE, NOT INTO FAILURE
//
// The detail that surprises people. A link that reaches Polling and never
// completes does NOT go to an error state -- it goes to COMPLIANCE, a mode
// that exists for test equipment to drive specified patterns into. The
// reasoning is that a port stuck in Polling with nothing at the far end is
// exactly what a compliance tester looks like, and a port that fell into an
// error state instead would be untestable.
module usb3_ltssm_sv
  import usb3_ltssm_pkg::*;
#(
  parameter int unsigned POLL_TIMEOUT     = 12, // Polling -> Compliance
  parameter int unsigned RECOVERY_TIMEOUT = 8   // Recovery -> SS.Inactive
) (
  input  logic      clk,
  input  logic      rst_n,

  input  logic      enable,           // the port is enabled at all
  input  logic      rx_detected,      // a terminated receiver is out there
  input  logic      training_done,    // TS1/TS2 exchange completed
  input  logic      link_error,       // an established link went bad
  input  logic      lp_request,       // the host asked for a low-power state
  input  logic [1:0] lp_level,         // which one: U1, U2 or U3
  input  logic      wakeup,           // leave the low-power state
  input  logic      warm_reset,       // the only way out of the dead states

  output ltssm_state_e ltssm_state,
  output logic      lfps_tx,          // the LOW-frequency transmitter
  output logic      hs_tx_enable,     // the HIGH-speed transmitter
  output logic      link_up,          // U0: the link carries data
  output logic      training_ok,      // --> chapter 20.2's arbiter
  output logic      training_fail,
  output logic [15:0] timer,

  output logic [31:0] poll_timeouts,
  output logic [31:0] recoveries,
  output logic      ever_compliance
);
  ltssm_state_e state;

  assign ltssm_state = state;

  // THE MUTUAL EXCLUSION, and it is electrical rather than logical: these
  // are two different transmitters on the SAME differential pair.
  //
  // LFPS is driven in the states that happen BEFORE the link is trained,
  // plus Compliance, where a tester expects it. The high-speed transmitter
  // is enabled only where a trained receiver exists to hear it.
  assign lfps_tx      = (state == L_RX_DETECT) || (state == L_POLLING)
                     || (state == L_COMPLIANCE) || (state == L_U3);
  assign hs_tx_enable = (state == L_U0) || (state == L_RECOVERY);

  assign link_up      = (state == L_U0);
  assign training_ok  = (state == L_U0);
  // The arbiter of chapter 20.2 needs ONE signal meaning "give up on
  // SuperSpeed", and both dead ends produce it: a link that timed out into
  // Compliance and one that failed recovery are equally unusable for data.
  assign training_fail = (state == L_INACTIVE) || (state == L_COMPLIANCE);

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      state           <= L_DISABLED;
      timer           <= 16'd0;
      poll_timeouts   <= '0;
      recoveries      <= '0;
      ever_compliance <= 1'b0;
    end else if (!enable) begin
      // Disabling the port outranks everything, from any state.
      state <= L_DISABLED;
      timer <= 16'd0;
    end else if (warm_reset) begin
      // THE ONLY WAY OUT of Inactive and Compliance -- and it works from
      // every state, because a reset that only worked from the dead states
      // could not recover a link that was merely confused.
      state <= L_RX_DETECT;
      timer <= 16'd0;
    end else begin
      case (state)
        L_DISABLED: begin
          state <= L_RX_DETECT;
          timer <= 16'd0;
        end

        L_RX_DETECT: begin
          // A DC measurement, not an exchange. Nothing is sent and nothing
          // replies, so there is no timeout here -- an empty port simply
          // keeps looking, for ever, at almost no cost.
          if (rx_detected) begin
            state <= L_POLLING;
            timer <= 16'd0;
          end
        end

        L_POLLING: begin
          if (training_done) begin
            state <= L_U0;
            timer <= 16'd0;
          end else if (timer + 16'd1 >= 16'(POLL_TIMEOUT)) begin
            // NOT an error state. A port stuck here with nothing at the far
            // end is indistinguishable from a port attached to a compliance
            // tester, and a port that failed into an error state instead
            // would be impossible to test.
            state           <= L_COMPLIANCE;
            timer           <= 16'd0;
            poll_timeouts   <= poll_timeouts + 1;
            ever_compliance <= 1'b1;
          end else begin
            timer <= timer + 16'd1;
          end
        end

        L_U0: begin
          // Precedence: an error outranks a low-power request. A link that
          // has gone bad must not be put to sleep -- it would wake into the
          // same fault having lost the chance to recover from it.
          if (link_error) begin
            state      <= L_RECOVERY;
            timer      <= 16'd0;
            recoveries <= recoveries + 1;
          end else if (lp_request) begin
            case (lp_level)
              2'd0:    state <= L_U1;
              2'd1:    state <= L_U2;
              default: state <= L_U3;
            endcase
            timer <= 16'd0;
          end
        end

        L_U1, L_U2: begin
          // U1 and U2 differ only in how deeply the PHY is powered down and
          // therefore in how long they take to leave. Both return through
          // RECOVERY rather than straight to U0, because the receiver has
          // to re-lock before data can flow.
          if (wakeup) begin
            state      <= L_RECOVERY;
            timer      <= 16'd0;
            recoveries <= recoveries + 1;
          end
        end

        L_U3: begin
          // U3 is SuperSpeed's suspend, and it is the deepest: the link is
          // fully down and only LFPS can wake it, which is why lfps_tx is
          // asserted here and hs_tx_enable is not.
          if (wakeup) begin
            state      <= L_RECOVERY;
            timer      <= 16'd0;
            recoveries <= recoveries + 1;
          end
        end

        L_RECOVERY: begin
          // Re-training an ESTABLISHED link. It keeps the configuration U0
          // had -- which is what makes recovery cheaper than starting from
          // Rx.Detect, and why a transient does not cost an enumeration.
          if (training_done) begin
            state <= L_U0;
            timer <= 16'd0;
          end else if (timer + 16'd1 >= 16'(RECOVERY_TIMEOUT)) begin
            // Recovery DOES fail into a dead state, unlike Polling: a link
            // that was up and cannot be retrained is broken rather than
            // absent, and a tester is not the likely explanation.
            state <= L_INACTIVE;
            timer <= 16'd0;
          end else begin
            timer <= timer + 16'd1;
          end
        end

        L_INACTIVE: begin
          // Waits for the warm reset handled above. Chapter 20.2's arbiter
          // sees training_fail from here and falls back to USB 2.
          state <= L_INACTIVE;
        end

        L_COMPLIANCE: begin
          // Also waits for the warm reset. A tester drives patterns at the
          // port and the port answers with LFPS; nothing here is an error.
          state <= L_COMPLIANCE;
        end

        default: state <= L_DISABLED;
      endcase
    end
  end
endmodule

Ten named states in two bits fewer than you would guess. ltssm_state_e is logic [3:0] and names ten of sixteen encodings — which is exactly why the Verilog needs its default: arm and this does not: six encodings exist there that the case statement does not describe.

10. VHDL-2008

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

package usb3_ltssm_pkg is
  -- The LTSSM states this design distinguishes. Two of them are dead ends
  -- and they are NOT the same dead end: L_INACTIVE is a link that was up and
  -- could not be retrained, L_COMPLIANCE is a port that never found anything
  -- and is now assumed to be attached to a tester.
  type ltssm_state_t is (
    L_DISABLED,
    L_RX_DETECT,   -- a DC measurement, not an exchange
    L_POLLING,     -- LFPS handshake, then TS1/TS2
    L_U0,          -- the link carries data
    L_U1,          -- light sleep
    L_U2,          -- deeper sleep
    L_U3,          -- SuperSpeed's suspend -- only LFPS wakes it
    L_RECOVERY,    -- re-train an ESTABLISHED link
    L_INACTIVE,    -- failed; waits for a warm reset
    L_COMPLIANCE   -- test mode -- NOT an error state
  );
end package;

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.usb3_ltssm_pkg.all;

-- usb3_ltssm_vhdl -- the Link Training and Status State Machine, and the
-- chicken-and-egg problem at the start of it.
--
-- A USB 2 bus simply works after a reset. Drive the lines, and both ends
-- already agree on what the voltages mean and roughly when to sample them.
--
-- A SuperSpeed link does not work after a reset. Before one packet can be
-- carried it must:
--
--   1. find out whether there is a receiver at the far end AT ALL;
--   2. agree on bit timing, recover a clock, and align symbol boundaries;
--   3. confirm all of that in BOTH directions;
--
-- and only then carry data. That is what "the link trains itself" means, and
-- it is the deepest architectural difference between USB 2 and USB 3.
--
-- THE CHICKEN AND EGG
--
-- Steps 1 and 2 cannot use the trained link, because the link is what they
-- are trying to train. So the earliest signalling happens with the
-- high-speed transmitter SWITCHED OFF, using LFPS -- Low Frequency Periodic
-- Signaling -- which is slow enough that an untrained receiver can detect it
-- without a recovered clock.
--
--   LFPS         out-of-band, low frequency, works before training
--   HIGH SPEED   in-band, 5 Gb/s, requires a trained receiver
--
-- They are two different transmitters on the SAME wires, so the one property
-- this block must never violate is that BOTH ARE NEVER ON AT ONCE. That is
-- not a protocol error; it is two drivers fighting over a differential pair.
--
-- AND Rx.Detect IS NOT A HANDSHAKE
--
-- The first step is a DC measurement, not an exchange. The transmitter
-- briefly changes the common-mode voltage and watches how fast the line
-- settles; a terminated receiver at the far end loads it differently from an
-- open cable. Nothing is sent and nothing replies -- which is why it works
-- against a device that is not powered enough to answer yet.
--
-- POLLING TIMES OUT INTO COMPLIANCE, NOT INTO FAILURE
--
-- The detail that surprises people. A link that reaches Polling and never
-- completes does NOT go to an error state -- it goes to COMPLIANCE, a mode
-- that exists for test equipment to drive specified patterns into. The
-- reasoning is that a port stuck in Polling with nothing at the far end is
-- exactly what a compliance tester looks like, and a port that fell into an
-- error state instead would be untestable.
entity usb3_ltssm_vhdl is
  generic (
    POLL_TIMEOUT     : positive := 12;  -- Polling -> Compliance
    RECOVERY_TIMEOUT : positive := 8    -- Recovery -> SS.Inactive
  );
  port (
    clk             : in  std_logic;
    rst_n           : in  std_logic;

    enable          : in  std_logic;
    rx_detected     : in  std_logic;
    training_done   : in  std_logic;
    link_error      : in  std_logic;
    lp_request      : in  std_logic;
    lp_level        : in  unsigned(1 downto 0);
    wakeup          : in  std_logic;
    warm_reset      : in  std_logic;

    ltssm_state     : out ltssm_state_t;
    lfps_tx         : out std_logic;
    hs_tx_enable    : out std_logic;
    link_up         : out std_logic;
    training_ok     : out std_logic;
    training_fail   : out std_logic;
    timer           : out unsigned(15 downto 0);

    poll_timeouts   : out unsigned(31 downto 0);
    recoveries      : out unsigned(31 downto 0);
    ever_compliance : out std_logic
  );
end entity;

architecture rtl of usb3_ltssm_vhdl is
  signal st_r   : ltssm_state_t := L_DISABLED;
  signal tmr_r  : unsigned(15 downto 0) := (others => '0');
  signal pt_r   : unsigned(31 downto 0) := (others => '0');
  signal rec_r  : unsigned(31 downto 0) := (others => '0');
  signal comp_r : std_logic := '0';
begin
  ltssm_state     <= st_r;
  timer           <= tmr_r;
  poll_timeouts   <= pt_r;
  recoveries      <= rec_r;
  ever_compliance <= comp_r;

  -- THE MUTUAL EXCLUSION, and it is electrical rather than logical: these
  -- are two different transmitters on the SAME differential pair.
  --
  -- LFPS is driven in the states that happen BEFORE the link is trained,
  -- plus Compliance, where a tester expects it. The high-speed transmitter
  -- is enabled only where a trained receiver exists to hear it.
  lfps_tx      <= '1' when (st_r = L_RX_DETECT or st_r = L_POLLING
                            or st_r = L_COMPLIANCE or st_r = L_U3) else '0';
  hs_tx_enable <= '1' when (st_r = L_U0 or st_r = L_RECOVERY) else '0';

  link_up     <= '1' when st_r = L_U0 else '0';
  training_ok <= '1' when st_r = L_U0 else '0';
  -- The arbiter of chapter 20.2 needs ONE signal meaning "give up on
  -- SuperSpeed", and both dead ends produce it.
  training_fail <= '1' when (st_r = L_INACTIVE or st_r = L_COMPLIANCE)
                   else '0';

  process (clk, rst_n)
  begin
    if rst_n = '0' then
      st_r <= L_DISABLED; tmr_r <= (others => '0');
      pt_r <= (others => '0'); rec_r <= (others => '0'); comp_r <= '0';
    elsif rising_edge(clk) then
      if enable = '0' then
        -- Disabling the port outranks everything, from any state.
        st_r <= L_DISABLED; tmr_r <= (others => '0');
      elsif warm_reset = '1' then
        -- THE ONLY WAY OUT of Inactive and Compliance -- and it works from
        -- every state, because a reset that only worked from the dead
        -- states could not recover a link that was merely confused.
        st_r <= L_RX_DETECT; tmr_r <= (others => '0');
      else
        case st_r is
          when L_DISABLED =>
            st_r <= L_RX_DETECT; tmr_r <= (others => '0');

          when L_RX_DETECT =>
            -- A DC measurement, not an exchange. Nothing is sent and
            -- nothing replies, so there is no timeout here -- an empty
            -- port simply keeps looking, for ever, at almost no cost.
            if rx_detected = '1' then
              st_r <= L_POLLING; tmr_r <= (others => '0');
            end if;

          when L_POLLING =>
            if training_done = '1' then
              st_r <= L_U0; tmr_r <= (others => '0');
            elsif tmr_r + 1 >= to_unsigned(POLL_TIMEOUT, 16) then
              -- NOT an error state. A port stuck here with nothing at the
              -- far end is indistinguishable from a port attached to a
              -- compliance tester, and a port that failed into an error
              -- state instead would be impossible to test.
              st_r <= L_COMPLIANCE; tmr_r <= (others => '0');
              pt_r <= pt_r + 1; comp_r <= '1';
            else
              tmr_r <= tmr_r + 1;
            end if;

          when L_U0 =>
            -- Precedence: an error outranks a low-power request. A link
            -- that has gone bad must not be put to sleep -- it would wake
            -- into the same fault having lost the chance to recover.
            if link_error = '1' then
              st_r <= L_RECOVERY; tmr_r <= (others => '0');
              rec_r <= rec_r + 1;
            elsif lp_request = '1' then
              case to_integer(lp_level) is
                when 0      => st_r <= L_U1;
                when 1      => st_r <= L_U2;
                when others => st_r <= L_U3;
              end case;
              tmr_r <= (others => '0');
            end if;

          when L_U1 | L_U2 =>
            -- U1 and U2 differ only in how deeply the PHY is powered down.
            -- Both return through RECOVERY rather than straight to U0,
            -- because the receiver has to re-lock before data can flow.
            if wakeup = '1' then
              st_r <= L_RECOVERY; tmr_r <= (others => '0');
              rec_r <= rec_r + 1;
            end if;

          when L_U3 =>
            -- U3 is SuperSpeed's suspend, and it is the deepest: the link
            -- is fully down and only LFPS can wake it, which is why
            -- lfps_tx is asserted here and hs_tx_enable is not.
            if wakeup = '1' then
              st_r <= L_RECOVERY; tmr_r <= (others => '0');
              rec_r <= rec_r + 1;
            end if;

          when L_RECOVERY =>
            -- Re-training an ESTABLISHED link. It keeps the configuration
            -- U0 had -- which is what makes recovery cheaper than starting
            -- from Rx.Detect, and why a transient does not cost an
            -- enumeration.
            if training_done = '1' then
              st_r <= L_U0; tmr_r <= (others => '0');
            elsif tmr_r + 1 >= to_unsigned(RECOVERY_TIMEOUT, 16) then
              -- Recovery DOES fail into a dead state, unlike Polling: a
              -- link that was up and cannot be retrained is broken rather
              -- than absent, and a tester is not the likely explanation.
              st_r <= L_INACTIVE; tmr_r <= (others => '0');
            else
              tmr_r <= tmr_r + 1;
            end if;

          when L_INACTIVE =>
            -- Waits for the warm reset handled above. Chapter 20.2's
            -- arbiter sees training_fail from here and falls back to USB 2.
            st_r <= L_INACTIVE;

          when L_COMPLIANCE =>
            -- Also waits for the warm reset. A tester drives patterns at
            -- the port and the port answers with LFPS; nothing is an error.
            st_r <= L_COMPLIANCE;
        end case;
      end if;
    end if;
  end process;
end architecture;

when L_U1 | L_U2 => collapses two states into one arm without the fall-through ambiguity Verilog's L_U1, L_U2: carries — and VHDL rejects the case at analysis time if any enumerator is left unhandled, so the ten arms are provably the whole machine.

11. The Waveform

The handover: LFPS off, high speed on, same pair

10 cycles
A waveform of the link training state machine over ten ticks, with the polling and recovery timeouts reduced so an entire link life fits in one figure. At ticks zero and one the port is disabled and neither transmitter is driving. At tick two the port has been enabled and the machine is in Rx dot Detect, with the low frequency transmitter on and the high speed transmitter off, because nothing is trained yet. At tick three it is still in Rx dot Detect looking for a receiver; there is no timeout on this step. At tick four a receiver has been detected and the machine has moved to Polling, still using low frequency signalling. At tick five it is still polling and the timer has begun counting toward the compliance timeout. At tick six training has completed and the machine is in U0: this is the handover, where the low frequency transmitter drops to zero and the high speed transmitter rises to one in the same instant, both driving the same differential pair, and the link up output asserts. At tick seven the link is still up but a link error input has been asserted. At tick eight the machine has entered Recovery, where the high speed transmitter remains on because the link is being retrained rather than restarted, and link up has dropped. At tick nine retraining has succeeded and the machine is back in U0 with the link up again. At no tick in the entire figure are the two transmitters both asserted.LFPS only — nothing is trainedLFPS only — nothing istrainedreceiver found → Pollingreceiver found → PollingHANDOVER: LFPS off, high speed onHANDOVER: LFPS off, highspeed onRecovery keeps the high-speed pairRecovery keeps thehigh-speed pairclkenablerx_dettrainedlink_errstateDISDISRXDRXDPOLLPOLLU0U0RECU0lfps_txhs_tx_enlink_upt0t1t2t3t4t5t6t7t8t9
Figure 2 — ten ticks from the simulator, with the timeouts reduced so a whole link life fits. Cycle 6 is the handover: LFPS drops and the high-speed transmitter takes the same pair in the same instant. At no tick are both asserted.

Add lfps_tx and hs_tx_en at any tick and the answer is never 2. That is §2's property seen as a picture, and it is worth seeing because the failure it prevents does not look like a protocol bug in a trace — it looks like a transceiver that stopped working.

Cycle 8 is worth a second look. Recovery keeps the high-speed transmitter, not LFPS: it is re-training an established link, which retains the configuration U0 had. That is what makes a transient cost a few microseconds instead of a full enumeration.

12. The Testbench: 5120 Transitions, Exhaustively

Ten states × 512 input combinations — seven control signals plus a two-bit low-power level — is the entire one-step domain.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    // 10 states x all 128 combinations of the seven control inputs
    // (enable, rx_detected, training_done, link_error, lp_request, wakeup,
    // warm_reset) x 4 low-power levels = 5120 transitions.
    for (pos=0; pos<10; pos=pos+1)
     for (ic=0; ic<512; ic=ic+1) begin
       goto(pos);
       tick(ic[0], ic[1], ic[2], ic[3], ic[4], ic[8:7], ic[5], ic[6]);

goto drives to each of the ten states legitimately — through a real sequence, including counting out a full timeout to reach Compliance and SS.Inactive — so no state is reached by forcing.

Three properties are checked against no model:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
      // ---- SAFETY PROPERTIES, independent of the model ----
      // 1. THE one that matters, and it is electrical: the low-frequency
      //    and high-speed transmitters share a differential pair. Both on
      //    is two drivers fighting, not a protocol error.
      check(!(lfps_tx && hs_tx_enable),
            "LFPS and the high-speed transmitter both driving the same pair");
      // 2. Data only ever flows on a trained link.
      check(!link_up || hs_tx_enable,
            "the link reported up with its high-speed transmitter off");
      // 3. A link cannot be simultaneously trained and failed.
      check(!(training_ok && training_fail),
            "the link reported trained AND failed at once");

And the model derives the transmitter enables from a list of states where the design uses a boolean expression — same answer, different route, so a mistake in one is not automatically a mistake in the other.

Measured reach:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  exhaustive transition sweep: 5120 of 5120 transitions verified

  Verilog / SystemVerilog:
  REACH: lfps=28040 hs=18059 u0=11997 poll-timeouts=328 recoveries=1370
  STATES: dis=6360 rxd=7228 poll=11017 u0=11997 u1=1418 u2=1377
          u3=2324 rec=6062 inact=8854 comp=7471

  VHDL:
  REACH: lfps=27818 hs=19248 u0=12904 poll-timeouts=295 recoveries=1481
  [VHDL] usb3_ltssm_vhdl: 0 errors — PASS

Every one of the ten states is visited over a thousand times, and the run asserts that explicitly rather than assuming it — a state never entered is a state never tested, however many transitions the sweep counted.

12.1 The complete Verilog testbench

The excerpts above are the parts worth arguing about. Here is the whole thing — the sweeps, the reference model, the safety properties and the reach assertions, exactly as simulated against the usb3_ltssm listing published in this chapter.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`timescale 1ns/1ps
module tb_lt_v;
  localparam PT = 5, RT = 4;     // small timeouts so a whole life fits
  reg clk=0, rst_n=0;
  reg en=0, rxd=0, tdone=0, lerr=0, lpr=0, wk=0, wrst=0;
  reg [1:0] lpl=0;
  wire [3:0] ltssm_state;
  wire lfps_tx, hs_tx_enable, link_up, training_ok, training_fail;
  wire [15:0] timer;
  wire [31:0] poll_timeouts, recoveries;
  wire ever_compliance;
  always #5 clk=~clk;

  usb3_ltssm #(.POLL_TIMEOUT(PT), .RECOVERY_TIMEOUT(RT)) dut (
    .clk(clk), .rst_n(rst_n), .enable(en), .rx_detected(rxd),
    .training_done(tdone), .link_error(lerr), .lp_request(lpr),
    .lp_level(lpl), .wakeup(wk), .warm_reset(wrst),
    .ltssm_state(ltssm_state), .lfps_tx(lfps_tx),
    .hs_tx_enable(hs_tx_enable), .link_up(link_up),
    .training_ok(training_ok), .training_fail(training_fail),
    .timer(timer), .poll_timeouts(poll_timeouts), .recoveries(recoveries),
    .ever_compliance(ever_compliance));

  localparam [3:0] S_DIS=0, S_RXD=1, S_POLL=2, S_U0=3, S_U1=4, S_U2=5,
                   S_U3=6, S_REC=7, S_INACT=8, S_COMP=9;

  integer errors=0, i, pos, ic;
  integer n_trans=0, n_lfps=0, n_hs=0, n_u0=0;
  integer n_vis [0:9];
  // ---- INDEPENDENT MODEL: its own state, timer and counters ----
  integer m_state, m_timer, m_pt, m_rec;
  reg m_comp;

  task check(input cond, input [639:0] msg);
    begin if (!cond) begin errors=errors+1;
      if (errors <= 25)
        $display("  FAIL: %0s (en=%b rxd=%b td=%b err=%b lp=%b wk=%b wr=%b | st=%0d lfps=%b hs=%b tmr=%0d, t=%0t)",
                 msg, en, rxd, tdone, lerr, lpr, wk, wrst, ltssm_state,
                 lfps_tx, hs_tx_enable, timer, $time);
    end end
  endtask

  // The model derives the two transmitter enables from a LIST of states
  // rather than from the design's expression -- a different route to the
  // same answer.
  function e_lfps(input integer s);
    begin e_lfps = (s==S_RXD)||(s==S_POLL)||(s==S_COMP)||(s==S_U3); end
  endfunction
  function e_hs(input integer s);
    begin e_hs = (s==S_U0)||(s==S_REC); end
  endfunction

  task tick(input e, input rd, input td, input le, input lp,
            input [1:0] lv, input w, input wr);
    integer nst, ntm;
    begin
      en=e; rxd=rd; tdone=td; lerr=le; lpr=lp; lpl=lv; wk=w; wrst=wr; #1;

      // ---- combinational contract ----
      check(ltssm_state   === m_state[3:0], "state matches the independent model");
      check(timer         === m_timer[15:0], "timer matches the model");
      check(lfps_tx       === e_lfps(m_state), "lfps_tx names the right states");
      check(hs_tx_enable  === e_hs(m_state),   "hs_tx_enable names the right states");
      check(link_up       === (m_state == S_U0),  "link_up is U0 and nothing else");
      check(training_ok   === (m_state == S_U0),  "training_ok is U0");
      check(training_fail === ((m_state == S_INACT) || (m_state == S_COMP)),
            "training_fail covers BOTH dead ends");

      // ---- SAFETY PROPERTIES, independent of the model ----
      // 1. THE one that matters, and it is electrical: the low-frequency
      //    and high-speed transmitters share a differential pair. Both on
      //    is two drivers fighting, not a protocol error.
      check(!(lfps_tx && hs_tx_enable),
            "LFPS and the high-speed transmitter both driving the same pair");
      // 2. Data only ever flows on a trained link.
      check(!link_up || hs_tx_enable,
            "the link reported up with its high-speed transmitter off");
      // 3. A link cannot be simultaneously trained and failed.
      check(!(training_ok && training_fail),
            "the link reported trained AND failed at once");
      if (lfps_tx)      n_lfps = n_lfps + 1;
      if (hs_tx_enable) n_hs   = n_hs + 1;
      if (link_up)      n_u0   = n_u0 + 1;
      if (m_state >= 0 && m_state <= 9) n_vis[m_state] = n_vis[m_state] + 1;

      // ---- advance the model ----
      nst = m_state; ntm = m_timer;
      if (!e)        begin nst = S_DIS; ntm = 0; end
      else if (wr)   begin nst = S_RXD; ntm = 0; end
      else case (m_state)
        S_DIS:  begin nst = S_RXD; ntm = 0; end
        S_RXD:  if (rd) begin nst = S_POLL; ntm = 0; end
        S_POLL: begin
          if (td) begin nst = S_U0; ntm = 0; end
          else if (m_timer + 1 >= PT) begin
            nst = S_COMP; ntm = 0; m_pt = m_pt + 1; m_comp = 1;
          end else ntm = m_timer + 1;
        end
        S_U0: begin
          if (le) begin nst = S_REC; ntm = 0; m_rec = m_rec + 1; end
          else if (lp) begin
            nst = (lv == 2'd0) ? S_U1 : (lv == 2'd1) ? S_U2 : S_U3;
            ntm = 0;
          end
        end
        S_U1, S_U2, S_U3:
          if (w) begin nst = S_REC; ntm = 0; m_rec = m_rec + 1; end
        S_REC: begin
          if (td) begin nst = S_U0; ntm = 0; end
          else if (m_timer + 1 >= RT) begin nst = S_INACT; ntm = 0; end
          else ntm = m_timer + 1;
        end
        S_INACT: nst = S_INACT;
        S_COMP:  nst = S_COMP;
      endcase

      @(posedge clk); #1;
      m_state = nst; m_timer = ntm;

      check(poll_timeouts   === m_pt[31:0],  "poll_timeouts matches the model");
      check(recoveries      === m_rec[31:0], "recoveries matches the model");
      check(ever_compliance === m_comp,      "ever_compliance matches the model");
    end
  endtask

  task hard_reset;
    begin
      rst_n=0; en=0; rxd=0; tdone=0; lerr=0; lpr=0; lpl=0; wk=0; wrst=0;
      @(posedge clk); #1; @(posedge clk); #1; rst_n=1; #1;
      m_state=S_DIS; m_timer=0; m_pt=0; m_rec=0; m_comp=0;
    end
  endtask

  // Drive to any of the ten states, legitimately.
  task goto(input integer p);
    integer j;
    begin
      hard_reset;
      if (p == S_DIS) begin end
      else begin
        tick(1,0,0,0,0,0,0,0);                    // -> RX_DETECT
        if (p == S_RXD) begin end
        else begin
          tick(1,1,0,0,0,0,0,0);                  // -> POLLING
          if (p == S_POLL) begin end
          else if (p == S_COMP) begin
            for (j=0;j<PT;j=j+1) tick(1,1,0,0,0,0,0,0);
          end else begin
            tick(1,1,1,0,0,0,0,0);                // -> U0
            if (p == S_U0) begin end
            else if (p == S_U1) tick(1,1,0,0,1,2'd0,0,0);
            else if (p == S_U2) tick(1,1,0,0,1,2'd1,0,0);
            else if (p == S_U3) tick(1,1,0,0,1,2'd2,0,0);
            else begin
              tick(1,1,0,1,0,0,0,0);              // error -> RECOVERY
              if (p == S_INACT)
                for (j=0;j<RT;j=j+1) tick(1,1,0,0,0,0,0,0);
            end
          end
        end
      end
    end
  endtask

  initial begin
    for (i=0;i<10;i=i+1) n_vis[i]=0;
    hard_reset;
    check(ltssm_state === S_DIS, "the LTSSM comes up disabled");
    check(!lfps_tx && !hs_tx_enable, "with neither transmitter on");

    // ===== A. EXHAUSTIVE ONE-STEP TRANSITIONS =====
    // 10 states x all 128 combinations of the seven control inputs
    // (enable, rx_detected, training_done, link_error, lp_request, wakeup,
    // warm_reset) x 4 low-power levels = 5120 transitions.
    for (pos=0; pos<10; pos=pos+1)
     for (ic=0; ic<512; ic=ic+1) begin
       goto(pos);
       tick(ic[0], ic[1], ic[2], ic[3], ic[4], ic[8:7], ic[5], ic[6]);
       n_trans = n_trans + 1;
     end
    $display("  exhaustive transition sweep: %0d of %0d transitions verified",
             n_trans, 10*512);

    // ===== B. directed: the life of a link =====
    hard_reset;
    tick(1,0,0,0,0,0,0,0);
    check(ltssm_state === S_RXD, "an enabled port looks for a receiver");
    check(lfps_tx, "using LFPS, because nothing is trained yet");
    check(!hs_tx_enable, "with the high-speed transmitter off");

    tick(1,1,0,0,0,0,0,0);
    check(ltssm_state === S_POLL, "a detected receiver moves it to Polling");
    check(lfps_tx && !hs_tx_enable, "still LFPS: training is not finished");

    tick(1,1,1,0,0,0,0,0);
    check(link_up, "training completes and the link is up");
    check(hs_tx_enable && !lfps_tx,
          "and NOW the high-speed transmitter takes the pair");
    check(training_ok && !training_fail, "reported trained");

    // Polling times out into COMPLIANCE, not failure
    hard_reset;
    tick(1,0,0,0,0,0,0,0); tick(1,1,0,0,0,0,0,0);
    for (i=0;i<PT-1;i=i+1) tick(1,1,0,0,0,0,0,0);
    check(ltssm_state === S_POLL, "still polling one tick short of the timeout");
    tick(1,1,0,0,0,0,0,0);
    check(ltssm_state === S_COMP,
          "Polling times out into COMPLIANCE -- a test mode, not an error");
    check(lfps_tx, "which drives LFPS, because a tester expects it");
    check(training_fail, "and the arbiter of 20.2 is told to give up");
    check(poll_timeouts === 32'd1, "and the timeout is counted");

    // an error outranks a low-power request
    goto(S_U0);
    tick(1,1,0,1,1,2'd0,0,0);
    check(ltssm_state === S_REC,
          "an error outranks a low-power request: recover, do not sleep");

    // Recovery DOES fail into a dead state
    goto(S_U0);
    tick(1,1,0,1,0,0,0,0);
    check(ltssm_state === S_REC, "an error enters Recovery");
    for (i=0;i<RT-1;i=i+1) tick(1,1,0,0,0,0,0,0);
    check(ltssm_state === S_REC, "still recovering");
    tick(1,1,0,0,0,0,0,0);
    check(ltssm_state === S_INACT,
          "Recovery fails into SS.Inactive -- a link that WAS up is broken");
    check(training_fail, "and that is also a fallback signal");

    // U3 is the deep one and only LFPS wakes it
    goto(S_U3);
    check(lfps_tx, "U3 keeps LFPS alive: it is the only way to wake it");
    check(!hs_tx_enable, "with the high-speed transmitter fully down");
    tick(1,1,0,0,0,0,1,0);
    check(ltssm_state === S_REC,
          "a wakeup returns through RECOVERY, not straight to U0");

    // a warm reset escapes both dead states
    goto(S_INACT);
    tick(1,0,0,0,0,0,0,1);
    check(ltssm_state === S_RXD, "a warm reset escapes SS.Inactive");
    goto(S_COMP);
    tick(1,0,0,0,0,0,0,1);
    check(ltssm_state === S_RXD, "and Compliance");

    // ===== C. randomised =====
    for (i=0;i<40000;i=i+1)
      tick(({$random}%32)!=0, ({$random}%3)!=0, ({$random}%4)==0,
           ({$random}%16)==0, ({$random}%8)==0, {$random}%4,
           ({$random}%4)==0, ({$random}%64)==0);

    for (i=0;i<10;i=i+1)
      check(n_vis[i] > 0, "every LTSSM state was reached");

    $display("");
    $display("  REACH: transitions=%0d | lfps=%0d hs=%0d u0=%0d poll-timeouts=%0d recoveries=%0d",
             n_trans, n_lfps, n_hs, n_u0, poll_timeouts, recoveries);
    $display("  STATES: dis=%0d rxd=%0d poll=%0d u0=%0d u1=%0d u2=%0d u3=%0d rec=%0d inact=%0d comp=%0d",
             n_vis[0], n_vis[1], n_vis[2], n_vis[3], n_vis[4], n_vis[5],
             n_vis[6], n_vis[7], n_vis[8], n_vis[9]);
    $display("  [Verilog] usb3_ltssm: %0d errors", errors);
    $display("  [Verilog] %0s", errors==0 ? "PASS" : "FAIL");
    $display("");
    $finish;
  end
endmodule

12.2 The complete SystemVerilog testbench

Same structure, with the enumerated types doing the work that localparams do in the Verilog build — which is what makes a failure message name a state instead of printing a number.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`timescale 1ns/1ps
module tb_lt_sv;
  import usb3_ltssm_pkg::*;
  localparam PT = 5, RT = 4;     // small timeouts so a whole life fits
  reg clk=0, rst_n=0;
  reg en=0, rxd=0, tdone=0, lerr=0, lpr=0, wk=0, wrst=0;
  reg [1:0] lpl=0;
  ltssm_state_e ltssm_state;
  wire lfps_tx, hs_tx_enable, link_up, training_ok, training_fail;
  wire [15:0] timer;
  wire [31:0] poll_timeouts, recoveries;
  wire ever_compliance;
  always #5 clk=~clk;

  usb3_ltssm_sv #(.POLL_TIMEOUT(PT), .RECOVERY_TIMEOUT(RT)) dut (
    .clk(clk), .rst_n(rst_n), .enable(en), .rx_detected(rxd),
    .training_done(tdone), .link_error(lerr), .lp_request(lpr),
    .lp_level(lpl), .wakeup(wk), .warm_reset(wrst),
    .ltssm_state(ltssm_state), .lfps_tx(lfps_tx),
    .hs_tx_enable(hs_tx_enable), .link_up(link_up),
    .training_ok(training_ok), .training_fail(training_fail),
    .timer(timer), .poll_timeouts(poll_timeouts), .recoveries(recoveries),
    .ever_compliance(ever_compliance));

  localparam [3:0] S_DIS=0, S_RXD=1, S_POLL=2, S_U0=3, S_U1=4, S_U2=5,
                   S_U3=6, S_REC=7, S_INACT=8, S_COMP=9;

  integer errors=0, i, pos, ic;
  integer n_trans=0, n_lfps=0, n_hs=0, n_u0=0;
  integer n_vis [0:9];
  // ---- INDEPENDENT MODEL: its own state, timer and counters ----
  integer m_state, m_timer, m_pt, m_rec;
  reg m_comp;

  task check(input cond, input [639:0] msg);
    begin if (!cond) begin errors=errors+1;
      if (errors <= 25)
        $display("  FAIL: %0s (en=%b rxd=%b td=%b err=%b lp=%b wk=%b wr=%b | st=%0d lfps=%b hs=%b tmr=%0d, t=%0t)",
                 msg, en, rxd, tdone, lerr, lpr, wk, wrst, ltssm_state,
                 lfps_tx, hs_tx_enable, timer, $time);
    end end
  endtask

  // The model derives the two transmitter enables from a LIST of states
  // rather than from the design's expression -- a different route to the
  // same answer.
  function e_lfps(input integer s);
    begin e_lfps = (s==S_RXD)||(s==S_POLL)||(s==S_COMP)||(s==S_U3); end
  endfunction
  function e_hs(input integer s);
    begin e_hs = (s==S_U0)||(s==S_REC); end
  endfunction

  task tick(input e, input rd, input td, input le, input lp,
            input [1:0] lv, input w, input wr);
    integer nst, ntm;
    begin
      en=e; rxd=rd; tdone=td; lerr=le; lpr=lp; lpl=lv; wk=w; wrst=wr; #1;

      // ---- combinational contract ----
      check(ltssm_state === ltssm_state_e'(m_state[3:0]),
            "state matches the independent model");
      check(timer         === m_timer[15:0], "timer matches the model");
      check(lfps_tx       === e_lfps(m_state), "lfps_tx names the right states");
      check(hs_tx_enable  === e_hs(m_state),   "hs_tx_enable names the right states");
      check(link_up       === (m_state == S_U0),  "link_up is U0 and nothing else");
      check(training_ok   === (m_state == S_U0),  "training_ok is U0");
      check(training_fail === ((m_state == S_INACT) || (m_state == S_COMP)),
            "training_fail covers BOTH dead ends");

      // ---- SAFETY PROPERTIES, independent of the model ----
      // 1. THE one that matters, and it is electrical: the low-frequency
      //    and high-speed transmitters share a differential pair. Both on
      //    is two drivers fighting, not a protocol error.
      check(!(lfps_tx && hs_tx_enable),
            "LFPS and the high-speed transmitter both driving the same pair");
      // 2. Data only ever flows on a trained link.
      check(!link_up || hs_tx_enable,
            "the link reported up with its high-speed transmitter off");
      // 3. A link cannot be simultaneously trained and failed.
      check(!(training_ok && training_fail),
            "the link reported trained AND failed at once");
      if (lfps_tx)      n_lfps = n_lfps + 1;
      if (hs_tx_enable) n_hs   = n_hs + 1;
      if (link_up)      n_u0   = n_u0 + 1;
      if (m_state >= 0 && m_state <= 9) n_vis[m_state] = n_vis[m_state] + 1;

      // ---- advance the model ----
      nst = m_state; ntm = m_timer;
      if (!e)        begin nst = S_DIS; ntm = 0; end
      else if (wr)   begin nst = S_RXD; ntm = 0; end
      else case (m_state)
        S_DIS:  begin nst = S_RXD; ntm = 0; end
        S_RXD:  if (rd) begin nst = S_POLL; ntm = 0; end
        S_POLL: begin
          if (td) begin nst = S_U0; ntm = 0; end
          else if (m_timer + 1 >= PT) begin
            nst = S_COMP; ntm = 0; m_pt = m_pt + 1; m_comp = 1;
          end else ntm = m_timer + 1;
        end
        S_U0: begin
          if (le) begin nst = S_REC; ntm = 0; m_rec = m_rec + 1; end
          else if (lp) begin
            nst = (lv == 2'd0) ? S_U1 : (lv == 2'd1) ? S_U2 : S_U3;
            ntm = 0;
          end
        end
        S_U1, S_U2, S_U3:
          if (w) begin nst = S_REC; ntm = 0; m_rec = m_rec + 1; end
        S_REC: begin
          if (td) begin nst = S_U0; ntm = 0; end
          else if (m_timer + 1 >= RT) begin nst = S_INACT; ntm = 0; end
          else ntm = m_timer + 1;
        end
        S_INACT: nst = S_INACT;
        S_COMP:  nst = S_COMP;
      endcase

      @(posedge clk); #1;
      m_state = nst; m_timer = ntm;

      check(poll_timeouts   === m_pt[31:0],  "poll_timeouts matches the model");
      check(recoveries      === m_rec[31:0], "recoveries matches the model");
      check(ever_compliance === m_comp,      "ever_compliance matches the model");
    end
  endtask

  task hard_reset;
    begin
      rst_n=0; en=0; rxd=0; tdone=0; lerr=0; lpr=0; lpl=0; wk=0; wrst=0;
      @(posedge clk); #1; @(posedge clk); #1; rst_n=1; #1;
      m_state=S_DIS; m_timer=0; m_pt=0; m_rec=0; m_comp=0;
    end
  endtask

  // Drive to any of the ten states, legitimately.
  task goto(input integer p);
    integer j;
    begin
      hard_reset;
      if (p == S_DIS) begin end
      else begin
        tick(1,0,0,0,0,0,0,0);                    // -> RX_DETECT
        if (p == S_RXD) begin end
        else begin
          tick(1,1,0,0,0,0,0,0);                  // -> POLLING
          if (p == S_POLL) begin end
          else if (p == S_COMP) begin
            for (j=0;j<PT;j=j+1) tick(1,1,0,0,0,0,0,0);
          end else begin
            tick(1,1,1,0,0,0,0,0);                // -> U0
            if (p == S_U0) begin end
            else if (p == S_U1) tick(1,1,0,0,1,2'd0,0,0);
            else if (p == S_U2) tick(1,1,0,0,1,2'd1,0,0);
            else if (p == S_U3) tick(1,1,0,0,1,2'd2,0,0);
            else begin
              tick(1,1,0,1,0,0,0,0);              // error -> RECOVERY
              if (p == S_INACT)
                for (j=0;j<RT;j=j+1) tick(1,1,0,0,0,0,0,0);
            end
          end
        end
      end
    end
  endtask

  initial begin
    for (i=0;i<10;i=i+1) n_vis[i]=0;
    hard_reset;
    check(ltssm_state === ltssm_state_e'(S_DIS), "the LTSSM comes up disabled");
    check(!lfps_tx && !hs_tx_enable, "with neither transmitter on");

    // ===== A. EXHAUSTIVE ONE-STEP TRANSITIONS =====
    // 10 states x all 128 combinations of the seven control inputs
    // (enable, rx_detected, training_done, link_error, lp_request, wakeup,
    // warm_reset) x 4 low-power levels = 5120 transitions.
    for (pos=0; pos<10; pos=pos+1)
     for (ic=0; ic<512; ic=ic+1) begin
       goto(pos);
       tick(ic[0], ic[1], ic[2], ic[3], ic[4], ic[8:7], ic[5], ic[6]);
       n_trans = n_trans + 1;
     end
    $display("  exhaustive transition sweep: %0d of %0d transitions verified",
             n_trans, 10*512);

    // ===== B. directed: the life of a link =====
    hard_reset;
    tick(1,0,0,0,0,0,0,0);
    check(ltssm_state === ltssm_state_e'(S_RXD), "an enabled port looks for a receiver");
    check(lfps_tx, "using LFPS, because nothing is trained yet");
    check(!hs_tx_enable, "with the high-speed transmitter off");

    tick(1,1,0,0,0,0,0,0);
    check(ltssm_state === ltssm_state_e'(S_POLL), "a detected receiver moves it to Polling");
    check(lfps_tx && !hs_tx_enable, "still LFPS: training is not finished");

    tick(1,1,1,0,0,0,0,0);
    check(link_up, "training completes and the link is up");
    check(hs_tx_enable && !lfps_tx,
          "and NOW the high-speed transmitter takes the pair");
    check(training_ok && !training_fail, "reported trained");

    // Polling times out into COMPLIANCE, not failure
    hard_reset;
    tick(1,0,0,0,0,0,0,0); tick(1,1,0,0,0,0,0,0);
    for (i=0;i<PT-1;i=i+1) tick(1,1,0,0,0,0,0,0);
    check(ltssm_state === ltssm_state_e'(S_POLL), "still polling one tick short of the timeout");
    tick(1,1,0,0,0,0,0,0);
    check(ltssm_state === ltssm_state_e'(S_COMP),
          "Polling times out into COMPLIANCE -- a test mode, not an error");
    check(lfps_tx, "which drives LFPS, because a tester expects it");
    check(training_fail, "and the arbiter of 20.2 is told to give up");
    check(poll_timeouts === 32'd1, "and the timeout is counted");

    // an error outranks a low-power request
    goto(S_U0);
    tick(1,1,0,1,1,2'd0,0,0);
    check(ltssm_state === ltssm_state_e'(S_REC),
          "an error outranks a low-power request: recover, do not sleep");

    // Recovery DOES fail into a dead state
    goto(S_U0);
    tick(1,1,0,1,0,0,0,0);
    check(ltssm_state === ltssm_state_e'(S_REC), "an error enters Recovery");
    for (i=0;i<RT-1;i=i+1) tick(1,1,0,0,0,0,0,0);
    check(ltssm_state === ltssm_state_e'(S_REC), "still recovering");
    tick(1,1,0,0,0,0,0,0);
    check(ltssm_state === ltssm_state_e'(S_INACT),
          "Recovery fails into SS.Inactive -- a link that WAS up is broken");
    check(training_fail, "and that is also a fallback signal");

    // U3 is the deep one and only LFPS wakes it
    goto(S_U3);
    check(lfps_tx, "U3 keeps LFPS alive: it is the only way to wake it");
    check(!hs_tx_enable, "with the high-speed transmitter fully down");
    tick(1,1,0,0,0,0,1,0);
    check(ltssm_state === ltssm_state_e'(S_REC),
          "a wakeup returns through RECOVERY, not straight to U0");

    // a warm reset escapes both dead states
    goto(S_INACT);
    tick(1,0,0,0,0,0,0,1);
    check(ltssm_state === ltssm_state_e'(S_RXD), "a warm reset escapes SS.Inactive");
    goto(S_COMP);
    tick(1,0,0,0,0,0,0,1);
    check(ltssm_state === ltssm_state_e'(S_RXD), "and Compliance");

    // ===== C. randomised =====
    for (i=0;i<40000;i=i+1)
      tick(({$random}%32)!=0, ({$random}%3)!=0, ({$random}%4)==0,
           ({$random}%16)==0, ({$random}%8)==0, {$random}%4,
           ({$random}%4)==0, ({$random}%64)==0);

    for (i=0;i<10;i=i+1)
      check(n_vis[i] > 0, "every LTSSM state was reached");

    $display("");
    $display("  REACH: transitions=%0d | lfps=%0d hs=%0d u0=%0d poll-timeouts=%0d recoveries=%0d",
             n_trans, n_lfps, n_hs, n_u0, poll_timeouts, recoveries);
    $display("  STATES: dis=%0d rxd=%0d poll=%0d u0=%0d u1=%0d u2=%0d u3=%0d rec=%0d inact=%0d comp=%0d",
             n_vis[0], n_vis[1], n_vis[2], n_vis[3], n_vis[4], n_vis[5],
             n_vis[6], n_vis[7], n_vis[8], n_vis[9]);
    $display("  [SystemVerilog] usb3_ltssm_sv: %0d errors", errors);
    $display("  [SystemVerilog] %0s", errors==0 ? "PASS" : "FAIL");
    $display("");
    $finish;
  end
endmodule

12.3 The complete VHDL testbench

VHDL-2008 requires a shared variable to have a protected type, so all the bookkeeping lives in process variables inside the single stimulus process. The randomisation uses ieee.math_real.uniform, which is a genuinely different generator from either Verilog builtin — see Chapter 20.5 §9.2 for why that distinction turned out to matter across this whole module.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all;
use work.usb3_ltssm_pkg.all;

entity tb_lt_vhdl is end entity;

architecture sim of tb_lt_vhdl is
  constant PT : positive := 5;   -- small timeouts so a whole life fits
  constant RT : positive := 4;

  signal clk, rst_n : std_logic := '0';
  signal en, rxd, tdone, lerr, lpr, wk, wrst : std_logic := '0';
  signal lpl : unsigned(1 downto 0) := (others => '0');
  signal ltssm_state : ltssm_state_t;
  signal lfps_tx, hs_tx_enable, link_up, training_ok, training_fail
       : std_logic;
  signal timer : unsigned(15 downto 0);
  signal poll_timeouts, recoveries : unsigned(31 downto 0);
  signal ever_compliance : std_logic;
  signal done : boolean := false;
begin
  clk <= not clk after 5 ns when not done else '0';

  dut : entity work.usb3_ltssm_vhdl
    generic map (POLL_TIMEOUT => PT, RECOVERY_TIMEOUT => RT)
    port map (clk, rst_n, en, rxd, tdone, lerr, lpr, lpl, wk, wrst,
              ltssm_state, lfps_tx, hs_tx_enable, link_up, training_ok,
              training_fail, timer, poll_timeouts, recoveries,
              ever_compliance);

  stim : process
    variable errors : natural := 0;
    variable n_trans, n_lfps, n_hs, n_u0 : natural := 0;
    type vis_t is array (ltssm_state_t) of natural;
    variable n_vis : vis_t := (others => 0);
    -- INDEPENDENT MODEL: its own state, timer and counters.
    variable m_state : ltssm_state_t := L_DISABLED;
    variable m_timer, m_pt, m_rec : natural := 0;
    variable m_comp : boolean := false;
    variable seed1 : positive := 103; variable seed2 : positive := 59;
    variable r1, r2, r3, r4, r5, r6, r7, r8 : real;
    variable icv : std_logic_vector(8 downto 0);

    function sl (b : boolean) return std_logic is
    begin
      if b then return '1'; else return '0'; end if;
    end function;

    procedure chk (c : boolean; m : string) is
    begin
      if not c then
        errors := errors + 1;
        if errors <= 25 then report "FAIL: " & m severity warning; end if;
      end if;
    end procedure;

    -- The model derives the two transmitter enables from a LIST of states
    -- rather than from the design's expression.
    function e_lfps (s : ltssm_state_t) return boolean is
    begin
      return s = L_RX_DETECT or s = L_POLLING
          or s = L_COMPLIANCE or s = L_U3;
    end function;
    function e_hs (s : ltssm_state_t) return boolean is
    begin
      return s = L_U0 or s = L_RECOVERY;
    end function;

    procedure tick (e, rd, td, le, lp : std_logic;
                    lv : unsigned(1 downto 0); w, wr : std_logic) is
      variable nst : ltssm_state_t;
      variable ntm : natural;
    begin
      en <= e; rxd <= rd; tdone <= td; lerr <= le; lpr <= lp;
      lpl <= lv; wk <= w; wrst <= wr;
      wait for 1 ns;

      -- ---- combinational contract ----
      chk(ltssm_state = m_state, "state matches the independent model");
      chk(timer = to_unsigned(m_timer, 16), "timer matches the model");
      chk((lfps_tx = '1') = e_lfps(m_state), "lfps_tx names the right states");
      chk((hs_tx_enable = '1') = e_hs(m_state),
          "hs_tx_enable names the right states");
      chk((link_up = '1') = (m_state = L_U0),
          "link_up is U0 and nothing else");
      chk((training_ok = '1') = (m_state = L_U0), "training_ok is U0");
      chk((training_fail = '1')
          = (m_state = L_INACTIVE or m_state = L_COMPLIANCE),
          "training_fail covers BOTH dead ends");

      -- ---- SAFETY PROPERTIES, independent of the model ----
      -- 1. THE one that matters, and it is electrical.
      chk(not (lfps_tx = '1' and hs_tx_enable = '1'),
          "LFPS and the high-speed transmitter both driving the same pair");
      -- 2. Data only ever flows on a trained link.
      chk(link_up = '0' or hs_tx_enable = '1',
          "the link reported up with its high-speed transmitter off");
      -- 3. A link cannot be simultaneously trained and failed.
      chk(not (training_ok = '1' and training_fail = '1'),
          "the link reported trained AND failed at once");
      if lfps_tx = '1' then n_lfps := n_lfps + 1; end if;
      if hs_tx_enable = '1' then n_hs := n_hs + 1; end if;
      if link_up = '1' then n_u0 := n_u0 + 1; end if;
      n_vis(m_state) := n_vis(m_state) + 1;

      -- ---- advance the model ----
      nst := m_state; ntm := m_timer;
      if e = '0' then nst := L_DISABLED; ntm := 0;
      elsif wr = '1' then nst := L_RX_DETECT; ntm := 0;
      else
        case m_state is
          when L_DISABLED => nst := L_RX_DETECT; ntm := 0;
          when L_RX_DETECT =>
            if rd = '1' then nst := L_POLLING; ntm := 0; end if;
          when L_POLLING =>
            if td = '1' then nst := L_U0; ntm := 0;
            elsif m_timer + 1 >= PT then
              nst := L_COMPLIANCE; ntm := 0;
              m_pt := m_pt + 1; m_comp := true;
            else ntm := m_timer + 1; end if;
          when L_U0 =>
            if le = '1' then
              nst := L_RECOVERY; ntm := 0; m_rec := m_rec + 1;
            elsif lp = '1' then
              case to_integer(lv) is
                when 0      => nst := L_U1;
                when 1      => nst := L_U2;
                when others => nst := L_U3;
              end case;
              ntm := 0;
            end if;
          when L_U1 | L_U2 | L_U3 =>
            if w = '1' then
              nst := L_RECOVERY; ntm := 0; m_rec := m_rec + 1;
            end if;
          when L_RECOVERY =>
            if td = '1' then nst := L_U0; ntm := 0;
            elsif m_timer + 1 >= RT then nst := L_INACTIVE; ntm := 0;
            else ntm := m_timer + 1; end if;
          when L_INACTIVE => nst := L_INACTIVE;
          when L_COMPLIANCE => nst := L_COMPLIANCE;
        end case;
      end if;

      wait until rising_edge(clk); wait for 1 ns;
      m_state := nst; m_timer := ntm;

      chk(poll_timeouts = to_unsigned(m_pt, 32),
          "poll_timeouts matches the model");
      chk(recoveries = to_unsigned(m_rec, 32),
          "recoveries matches the model");
      chk((ever_compliance = '1') = m_comp,
          "ever_compliance matches the model");
    end procedure;

    procedure hard_reset is
    begin
      rst_n <= '0'; en <= '0'; rxd <= '0'; tdone <= '0'; lerr <= '0';
      lpr <= '0'; lpl <= (others => '0'); wk <= '0'; wrst <= '0';
      wait until rising_edge(clk); wait for 1 ns;
      wait until rising_edge(clk); wait for 1 ns;
      rst_n <= '1'; wait for 1 ns;
      m_state := L_DISABLED; m_timer := 0;
      m_pt := 0; m_rec := 0; m_comp := false;
    end procedure;

    procedure goto (p : ltssm_state_t) is
      constant Z : unsigned(1 downto 0) := "00";
    begin
      hard_reset;
      if p = L_DISABLED then null;
      else
        tick('1','0','0','0','0',Z,'0','0');
        if p = L_RX_DETECT then null;
        else
          tick('1','1','0','0','0',Z,'0','0');
          if p = L_POLLING then null;
          elsif p = L_COMPLIANCE then
            for j in 1 to PT loop tick('1','1','0','0','0',Z,'0','0'); end loop;
          else
            tick('1','1','1','0','0',Z,'0','0');
            if p = L_U0 then null;
            elsif p = L_U1 then tick('1','1','0','0','1',"00",'0','0');
            elsif p = L_U2 then tick('1','1','0','0','1',"01",'0','0');
            elsif p = L_U3 then tick('1','1','0','0','1',"10",'0','0');
            else
              tick('1','1','0','1','0',Z,'0','0');
              if p = L_INACTIVE then
                for j in 1 to RT loop
                  tick('1','1','0','0','0',Z,'0','0');
                end loop;
              end if;
            end if;
          end if;
        end if;
      end if;
    end procedure;

    type pos_arr is array (0 to 9) of ltssm_state_t;
    constant POSN : pos_arr := (L_DISABLED, L_RX_DETECT, L_POLLING, L_U0,
                                L_U1, L_U2, L_U3, L_RECOVERY, L_INACTIVE,
                                L_COMPLIANCE);
    constant Z : unsigned(1 downto 0) := "00";
  begin
    hard_reset;
    chk(ltssm_state = L_DISABLED, "the LTSSM comes up disabled");
    chk(lfps_tx = '0' and hs_tx_enable = '0',
        "with neither transmitter on");

    -- ===== A. EXHAUSTIVE ONE-STEP TRANSITIONS =====
    -- 10 states x 512 input combinations = 5120 transitions.
    for pos in 0 to 9 loop
     for ic in 0 to 511 loop
       goto(POSN(pos));
       icv := std_logic_vector(to_unsigned(ic, 9));
       tick(icv(0), icv(1), icv(2), icv(3), icv(4),
            unsigned(icv(8 downto 7)), icv(5), icv(6));
       n_trans := n_trans + 1;
     end loop;
    end loop;
    report "exhaustive transition sweep: " & integer'image(n_trans)
         & " of 5120 transitions verified";

    -- ===== B. directed: the life of a link =====
    hard_reset;
    tick('1','0','0','0','0',Z,'0','0');
    chk(ltssm_state = L_RX_DETECT, "an enabled port looks for a receiver");
    chk(lfps_tx = '1', "using LFPS, because nothing is trained yet");
    chk(hs_tx_enable = '0', "with the high-speed transmitter off");

    tick('1','1','0','0','0',Z,'0','0');
    chk(ltssm_state = L_POLLING, "a detected receiver moves it to Polling");
    chk(lfps_tx = '1' and hs_tx_enable = '0',
        "still LFPS: training is not finished");

    tick('1','1','1','0','0',Z,'0','0');
    chk(link_up = '1', "training completes and the link is up");
    chk(hs_tx_enable = '1' and lfps_tx = '0',
        "and NOW the high-speed transmitter takes the pair");

    hard_reset;
    tick('1','0','0','0','0',Z,'0','0'); tick('1','1','0','0','0',Z,'0','0');
    for i in 1 to PT-1 loop tick('1','1','0','0','0',Z,'0','0'); end loop;
    chk(ltssm_state = L_POLLING,
        "still polling one tick short of the timeout");
    tick('1','1','0','0','0',Z,'0','0');
    chk(ltssm_state = L_COMPLIANCE,
        "Polling times out into COMPLIANCE -- a test mode, not an error");
    chk(lfps_tx = '1', "which drives LFPS, because a tester expects it");
    chk(training_fail = '1', "and the arbiter of 20.2 is told to give up");
    chk(poll_timeouts = to_unsigned(1, 32), "and the timeout is counted");

    goto(L_U0);
    tick('1','1','0','1','1',"00",'0','0');
    chk(ltssm_state = L_RECOVERY,
        "an error outranks a low-power request: recover, do not sleep");

    goto(L_U0);
    tick('1','1','0','1','0',Z,'0','0');
    chk(ltssm_state = L_RECOVERY, "an error enters Recovery");
    for i in 1 to RT-1 loop tick('1','1','0','0','0',Z,'0','0'); end loop;
    chk(ltssm_state = L_RECOVERY, "still recovering");
    tick('1','1','0','0','0',Z,'0','0');
    chk(ltssm_state = L_INACTIVE,
        "Recovery fails into SS.Inactive -- a link that WAS up is broken");
    chk(training_fail = '1', "and that is also a fallback signal");

    goto(L_U3);
    chk(lfps_tx = '1', "U3 keeps LFPS alive: it is the only way to wake it");
    chk(hs_tx_enable = '0', "with the high-speed transmitter fully down");
    tick('1','1','0','0','0',Z,'1','0');
    chk(ltssm_state = L_RECOVERY,
        "a wakeup returns through RECOVERY, not straight to U0");

    goto(L_INACTIVE);
    tick('1','0','0','0','0',Z,'0','1');
    chk(ltssm_state = L_RX_DETECT, "a warm reset escapes SS.Inactive");
    goto(L_COMPLIANCE);
    tick('1','0','0','0','0',Z,'0','1');
    chk(ltssm_state = L_RX_DETECT, "and Compliance");

    -- ===== C. randomised =====
    for i in 1 to 40000 loop
      uniform(seed1, seed2, r1); uniform(seed1, seed2, r2);
      uniform(seed1, seed2, r3); uniform(seed1, seed2, r4);
      uniform(seed1, seed2, r5); uniform(seed1, seed2, r6);
      uniform(seed1, seed2, r7); uniform(seed1, seed2, r8);
      tick(sl(r1 >= 0.03125), sl(r2 >= 0.3333), sl(r3 < 0.25),
           sl(r4 < 0.0625), sl(r5 < 0.125),
           to_unsigned(integer(floor(r6*4.0)), 2),
           sl(r7 < 0.25), sl(r8 < 0.015625));
    end loop;

    for s in ltssm_state_t loop
      chk(n_vis(s) > 0, "every LTSSM state was reached");
    end loop;

    report "REACH: transitions=" & integer'image(n_trans)
         & " | lfps=" & integer'image(n_lfps)
         & " hs=" & integer'image(n_hs)
         & " u0=" & integer'image(n_u0)
         & " poll-timeouts=" & integer'image(to_integer(poll_timeouts))
         & " recoveries=" & integer'image(to_integer(recoveries));
    report "STATES: dis=" & integer'image(n_vis(L_DISABLED))
         & " rxd=" & integer'image(n_vis(L_RX_DETECT))
         & " poll=" & integer'image(n_vis(L_POLLING))
         & " u0=" & integer'image(n_vis(L_U0))
         & " u1=" & integer'image(n_vis(L_U1))
         & " u2=" & integer'image(n_vis(L_U2))
         & " u3=" & integer'image(n_vis(L_U3))
         & " rec=" & integer'image(n_vis(L_RECOVERY))
         & " inact=" & integer'image(n_vis(L_INACTIVE))
         & " comp=" & integer'image(n_vis(L_COMPLIANCE));
    report "[VHDL] usb3_ltssm_vhdl: " & integer'image(errors) & " errors";
    if errors = 0 then report "[VHDL] PASS";
    else report "[VHDL] FAIL" severity error; end if;
    done <= true;
    wait;
  end process;
end architecture;

Run it with:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
nvc --std=2008 -a lt_vhdl.vhd lt_vhdl_tb.vhd
nvc --std=2008 -e tb_lt_vhdl
nvc --std=2008 -r tb_lt_vhdl

13. Mutation Testing — Across All Three Languages

MutationVerilogSystemVerilogVHDL
L1LFPS still driven while the link is up477684776849234
L2Polling times out into failure, not Compliance149441494414980
L3a low-power request outranks a link error388053880541000
L4low-power states wake straight to U0515915159149742
L5Recovery times out into U0768887688873478
L6training_fail covers only SS.Inactive747274727490
L7a warm reset escapes only the dead states928089280891223

L5 is the most dangerous of the seven and scores 76 888. It brings a link up that failed to retrain — so link_up asserts, hs_tx_enable asserts, and the device begins transmitting on a pair whose receiver never re-locked. Safety property 2 does not catch it, because U0 legitimately has the high-speed transmitter on; it dies on the model comparison, which is the case for having both kinds of check.

L7 is the largest because a warm reset that only works from two of ten states leaves the machine stuck almost everywhere the stimulus puts it.

L6 is the smallest at 7472 and is the one with the clearest field consequence: a link sitting in Compliance that never tells 20.2's arbiter to fall back is a device that stays silent on both buses for ever.

14. The Mutation That Deleted Instead of Demoting

The first run had L3 at 38 805 in Verilog and SystemVerilog against 71 329 in VHDL — a 1.8× divergence.

The Verilog mutation reorders the two branches:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`ifdef MUT_L3
          if (lp_request) begin ... end
          else if (link_error) begin ... end

The VHDL one had been written as if lp_request = '1' and false then on the error branch — which removed the error path entirely rather than demoting it below the low-power one. With no link_error handling at all, a link that went bad simply stayed in U0.

That is a strictly stronger mutation, not the same one, and the counts said so.

Rewriting it as a genuine reorder brought VHDL to 41 000, within 6 % of the other two.

15. A UVM Environment for a State Machine With Timeouts

The LTSSM's interesting behaviour is what happens at the boundaries of its timers, and that is a constrained-random problem rather than a directed one.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// A link partner, as an agent. Its QUALITY is a configuration: a good
// partner trains immediately, a marginal one trains late, and a dead one
// never trains at all -- and the third is the only one that reaches
// Compliance, which is the state section 4 is about.
typedef enum { PARTNER_GOOD, PARTNER_MARGINAL, PARTNER_ABSENT } partner_e;

class link_partner_cfg extends uvm_object;
  `uvm_object_utils(link_partner_cfg)
  rand partner_e kind;
  rand int unsigned train_delay;

  constraint c_kind {
    // A marginal partner trains just either side of the timeout -- which is
    // the only place the Polling/Compliance boundary is observable.
    kind == PARTNER_MARGINAL -> train_delay inside {[POLL_TIMEOUT-2:
                                                     POLL_TIMEOUT+2]};
    kind == PARTNER_GOOD     -> train_delay inside {[0:2]};
    kind == PARTNER_ABSENT   -> train_delay == 1000;   // never
  }
endclass

// Walk the timeout: train at every tick from well inside to well past the
// limit. A directed test picks one; this picks all of them.
class timeout_walk_seq extends uvm_sequence #(ltssm_item);
  `uvm_object_utils(timeout_walk_seq)
  rand int unsigned train_at;
  constraint c_at { train_at inside {[0:POLL_TIMEOUT+3]}; }

  task body();
    ltssm_item it;
    `uvm_do_with(it, { enable == 1; rx_detected == 1; })
    repeat (train_at)
      `uvm_do_with(it, { enable == 1; rx_detected == 1; training_done == 0; })
    // Either this lands inside the window and reaches U0, or it lands past
    // it and the machine has already gone to Compliance. Both are correct;
    // the boundary between them is what is being tested.
    `uvm_do_with(it, { enable == 1; training_done == 1; })
  endtask
endclass

class ltssm_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(ltssm_scoreboard)
  local ltssm_state_e m_prev = L_DISABLED;

  function void write(ltssm_txn t);
    // THE property, and the severity reflects what it costs: two drivers
    // on one differential pair is not a protocol error that a retry fixes.
    if (t.lfps_tx && t.hs_tx_enable)
      `uvm_fatal("LTSSM/CONTENTION",
        "LFPS and the high-speed transmitter both driving the same pair")

    // Section 13's L5: U0 is only ever entered from a state that actually
    // completed training.
    if (t.ltssm_state == L_U0 && m_prev != L_U0)
      if (!(m_prev inside {L_POLLING, L_RECOVERY}))
        `uvm_error("LTSSM/ENTRY", $sformatf(
          "U0 entered from %s without completing training", m_prev.name()))

    // Section 5: both dead ends must produce the fallback signal, or the
    // arbiter of 20.2 waits for ever.
    if (t.ltssm_state inside {L_INACTIVE, L_COMPLIANCE} && !t.training_fail)
      `uvm_error("LTSSM/DEADEND",
        "a dead-end state did not report training_fail")

    m_prev = t.ltssm_state;
  endfunction
endclass

covergroup ltssm_cg with function sample(
    ltssm_state_e st, int unsigned timer, bit trained, partner_e partner);
  // Every state, and the assertion that every one was reached. A state
  // never entered is a state never tested.
  cp_state : coverpoint st;
  // The timeout boundary, from both sides. Section 4's whole distinction
  // lives in the two bins either side of the limit.
  cp_timeout : coverpoint timer {
    bins inside_window = {[0:POLL_TIMEOUT-2]};
    bins at_limit      = {POLL_TIMEOUT-1};
    bins past          = {[POLL_TIMEOUT:$]};
  }
  cp_partner : coverpoint partner;
  // The cross that matters: an ABSENT partner is the only way to reach
  // Compliance, and a lab with only working cables never generates one.
  x_partner_state : cross cp_partner, cp_state;
endgroup

16. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  // THE property: two transmitters, one pair, never both.
  property p_no_contention;
    @(posedge clk) disable iff (!rst_n)
      !(lfps_tx && hs_tx_enable);
  endproperty
  a_no_contention : assert property (p_no_contention)
    else $fatal(1, "LFPS and high-speed both driving the same pair");

  // U0 is only ever entered from a state that completed training.
  // Mutation L5.
  property p_u0_entry;
    @(posedge clk) disable iff (!rst_n)
      ($changed(ltssm_state) && ltssm_state == L_U0)
        |-> ($past(ltssm_state) inside {L_POLLING, L_RECOVERY});
  endproperty
  a_u0_entry : assert property (p_u0_entry)
    else $error("U0 entered without completing training");

  // Both dead ends report the fallback. Mutation L6.
  property p_deadend_reports;
    @(posedge clk) disable iff (!rst_n)
      (ltssm_state inside {L_INACTIVE, L_COMPLIANCE}) |-> training_fail;
  endproperty
  a_deadend_reports : assert property (p_deadend_reports);

  // A warm reset works from EVERY state. Mutation L7, stated as the
  // universal it is rather than as a list of the states it covers.
  property p_warm_reset_universal;
    @(posedge clk) disable iff (!rst_n)
      (enable && warm_reset) |=> (ltssm_state == L_RX_DETECT);
  endproperty
  a_warm_reset_universal : assert property (p_warm_reset_universal);

p_no_contention has no antecedent at all — it is true in every state, every cycle, for every input. That is the right shape for an electrical constraint, and it is the one to hand to a formal tool: a prover settles it over the whole state space in negligible time.

These were written but not simulated; Icarus supports no concurrent assertions.

17. Debugging: the Port That Enumerates at USB 2 on One Cable

The report: a SuperSpeed device enumerates at USB 2 speed with one cable and at SuperSpeed with another. Both cables are physically fine and both carry USB 2 data perfectly.

The procedure:

1. Establish that it fell back rather than never tried. 20.2's training_attempts distinguishes them: zero attempts means ss_rx_detect never asserted — the SuperSpeed pairs are not connected, which is a cable with only USB 2 wiring. Non-zero means training was attempted and failed, which is this chapter.

2. Read the LTSSM's resting state. A link that gave up sits in SS.Inactive or Compliance, and they mean different things (§4): Compliance means Polling never completed — nothing ever trained; SS.Inactive means a link that was up could not be retrained.

3. Compliance points at the cable's SuperSpeed pairs. Training exchanged LFPS and never converged, which is a signal-integrity problem in the high-speed pairs specifically — and it is entirely consistent with USB 2 working perfectly, because USB 2 does not use those wires.

4. SS.Inactive points at something intermittent. The link trained once, so the pairs are good enough to train. Something later broke it and recovery failed within its timeout — marginal margin, a connector under strain, or interference.

5. Check poll_timeouts and recoveries. A high recovery count with the link still up is a marginal cable that keeps being rescued; a single poll timeout and a rest in Compliance is a cable that never worked at SuperSpeed at all.

18. Common Misconceptions

"A link is up as soon as the cable is plugged in." It must be trained first, in both directions, before one packet can move (§1).

"Training uses the link to negotiate the link." It cannot — that is the chicken and egg (§2). The earliest signalling is LFPS, with the high-speed transmitter off.

"Rx.Detect is a handshake." It is a DC measurement (§3). Nothing is sent and nothing replies, which is why it works against an unpowered device and why it has no timeout.

"A training timeout is an error." Polling times out into Compliance, a test mode (§4) — a port stuck in Polling looks exactly like a port attached to a tester. Mutation L2.

"SS.Inactive and Compliance are the same dead end." Different causes, different meanings — and the same consequence for the arbiter (§5), which is why one signal covers both. Mutation L6.

"A low-power request should be honoured promptly." Not over a link error (§7) — a bad link put to sleep wakes into the same fault. Mutation L3.

"Waking from U1 goes straight back to U0." It returns through Recovery, because the receiver must re-lock (§7). Mutation L4.

"A warm reset is for recovering dead links." It works from every state (§7); one that only escaped the dead ends could not rescue a confused link. Mutation L7, 92 808 errors.

19. Exercises

1. §3 argues Rx.Detect needs no timeout. Construct the argument for why Polling does, and determine what the design would do if Polling had none.

2. Mutation L5 brings up an untrained link and is not caught by safety property 2. Write the property that would catch it without comparing against a model.

3. §14 lists five distinct causes of a mutation meaning different things across languages. Propose a mechanical check over a tri-HDL mutation matrix that would flag all five, and state its false-positive behaviour.

4. The design merges U1, U2 and U3 in its transition logic but keeps them as separate states. Determine what would have to change to give them different wake latencies, and which of §13's mutations would then become two.

5. A link reaches Compliance. Trace what 20.2's arbiter does, what 20.1's credit block does, and what the user sees.

6. Write the SVA property that catches L4 — waking straight to U0 — without naming L_RECOVERY.

20. Summary

A USB 2 bus works after a reset; a SuperSpeed link does not (§1). It must find a receiver, agree on timing, and confirm both directions before carrying one packet.

And it cannot use the link to do it (§2). The earliest signalling is LFPS, low enough in frequency for an untrained receiver — two transmitters on one differential pair, which must never both drive. That property has no antecedent and is checked every cycle (L1, 47 768 errors).

Rx.Detect is a DC measurement, not a handshake (§3) — nothing is sent, nothing replies, and there is no timeout, because nothing was asked.

Polling times out into Compliance, a test mode — not into failure (§4). A port stuck in Polling is indistinguishable from a port attached to a tester, and one that failed into an error state would be untestable. Recovery is different and fails properly, because a link that was up is broken rather than absent.

Two dead ends, one fallback signal (§5): SS.Inactive and Compliance mean different things and need the same decision (L6, 7472 — the smallest count and the clearest field consequence).

All three HDL implementations were simulated (§21) and seven mutations died in all three (§13), verified over all 5120 one-step transitions — ten states × 512 input combinations — with every state visited over a thousand times and asserted to have been (§12).

And a mutation deleted where it should have demoted (§14). VHDL's L3 removed the error path instead of ranking it below the low-power one, and scored 1.8× the other two. That is the fifth distinct cause in three modules of a mutation not meaning the same thing in every language — after a duplicate, identical ternary branches, a duplicated design condition, and last-assignment-wins. One detector has caught all five: a column out of line with its neighbours.

21. Tooling, Honestly

LanguageDesignTestbenchAnalysed / compiledSimulatedMutations
Verilog-2005usb3_ltssmlt_v_tb.v✅ Icarus -g2005✅ 0 errors, 5120/5120✅ all seven
SystemVerilogusb3_ltssm_svlt_sv_tb.sv✅ Icarus -g2012✅ 0 errors, 5120/5120✅ all seven
VHDL-2008usb3_ltssm_vhdllt_vhdl_tb.vhd✅ nvc 1.23.0✅ 0 errors, 5120/5120✅ all seven
UVM (§15)——❌ no UVM-capable simulator here❌—
SVA (§16)——❌ unsupported by Icarus❌—

The timeouts are 5 and 4 in simulation against defaults of 12 and 8, so an entire link life fits in a ten-tick figure. Each appears exactly once in the design, which is what makes the substitution safe.

This chapter models a faithful subset of the LTSSM, not the whole of it. The real machine has additional substates within Polling and Recovery, hot-reset handling, and loopback — omitted because they multiply the state count without adding a distinct idea. The ten states here are the ones with different consequences, and §6's diagram merges U1/U2/U3 on the same grounds.

22. What Comes Next

The link is trained and carrying data. What is it carrying?

Chapter 20.4 — USB 3.x Packets is about the four packet types SuperSpeed defines, and the structural decision behind all of them: the header is protected separately from the payload.

Every packet begins with a 14-byte header and its own CRC — and a corrupt header is a link-layer problem while a corrupt payload is a protocol-layer one. They are detected by different checks, reported to different layers, and recovered in completely different ways.

That separation is why a SuperSpeed link can retry a lost data packet without renegotiating anything, and it is the last piece of architecture this module has to describe.

Browse the full path on the USB tutorials index.

Continue learning

Standards & specifications

Governing standard
USB-IF (Universal Serial Bus Specification)(opens USB Implementers Forum (USB-IF) in a new tab)

Defines the USB bus — its electrical signalling, connectors, packet and transaction model, device framework and the descriptors a device must expose — together with the device-class specifications layered on it. It does not define host-controller register interfaces (xHCI and EHCI are separate documents) nor any operating system's driver architecture.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the USB curriculum.