Skip to content
VLSI Mentor

USB · Module 25

Power Issues

The same voltage and the same current are a healthy device at one instant and a fault a second later — in-rush is legal, sag is not, and the only thing separating them is a time window.

Chapter 25.5 argued that the useful information is in a split rather than a total. This chapter makes the same argument on a different axis, and the axis is time.

1. Two Identical Readings, One Healthy and One Broken

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   VBUS = 4.1 V, 300 mA      at t = 0 (just plugged in)
       -> normal. This is what attaching a device LOOKS like.

   VBUS = 4.1 V, 300 mA      at t = 2 seconds
       -> a fault. Something is loading the rail.

Identical numbers. The only thing that distinguishes them is when they were taken, relative to the attach event.

The reason is physical. A device's bulk capacitance is empty when it is plugged in, so connecting it to VBUS is connecting a 5 V rail to a discharged capacitor. The current that flows is limited only by the resistance of the path, and the voltage at the port drops while the capacitor charges. That is in-rush, and it is not a fault — it is what a correct device does.

2. So a Monitor Without the Window Has Two Failure Modes

What it doesWhat happens
no windowevery attach reports a sag and an over-current. One false alarm per plug-in event, for ever
window too longa real short circuit is invisible for as long as the window lasts — which is exactly when the damage happens

Both are the pair chapter 24.1 named: a checker nobody reads, or a checker that misses the thing it exists for. And the second is worse here than usual, because the thing being missed is drawing an amp through something that is on fire.

One endpoint's power state, as a machine

A four-state power model for one port. OFF is the start state; an attach moves it to INRUSH and loads the window counter. When the window expires it moves to ON, and a SET_CONFIGURATION moves ON to CONFIGURED. A detach from any state returns to OFF. An over-current from any state trips the port and returns it to OFF.OFFIN-RUSHONCONFIGattach: window opensattach: window openswindow expireswindowexpiresSET_CONFIGURATIONSET_CONFIGURATIONover-current: tripover-current: tripdetachdetachdetach or tripdetach ortrip
The window opens on attach and nothing else. Inside it, sag and high current are expected and reported by nothing. Outside it, the same readings are faults. The port limit is the exception: it is a hardware protection and it applies in every state, because a dead short does not become acceptable by being early.

3. And the Budget Depends on the Enumeration State

There is a second place where an identical measurement means two different things, and this time the axis is not time but enumeration state:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   300 mA, before SET_CONFIGURATION   -> a specification violation
   300 mA, after  SET_CONFIGURATION   -> legal

   A device may draw ONE UNIT LOAD -- 100 mA -- from the moment
   it attaches. It may draw its configured maximum, up to
   500 mA, only after the host has configured it.

4. Over-Current Is a Protection Action, Not an Error

A hub shutting off a port that exceeds its limit is the hub working. Reporting each one as a fault is the same false-positive mistake as counting NAKs in 25.3.

What is not normal is a port that shuts off, is re-enabled, and shuts off again — an oscillation, which means something downstream is latching up and recovering. So trips are counted and the oscillation is the report, once.

5. What We Are Building

usb_power_monitor watches a port and reports five things:

CodeFires whenPoints at
E_SAGVBUS below the floor outside the windowthe rail, or something loading it
E_INRUSH_LONGstill drawing when the window expiredoversized bulk capacitance, or a soft short
E_OVERCURRENTpast the port limit, in any statea short, or a device that is on fire
E_BUDGETpast the budget for the current enumeration statedevice firmware
E_OSCOSC_MAX tripssomething latching up and recovering

Inside the in-rush window, E_SAG and E_BUDGET are not reported at all. E_OVERCURRENT still is.

usb_power_monitor — the window gates the policy, not the protection

A block diagram of the power monitor. An attach event opens a time window held in a counter, which drives a phase tracker with four states. A measurement stream carrying VBUS and current feeds three checks: a sag check and a budget check, both gated off while the window is open, and a port-limit check which is not gated. The port-limit check also feeds a trip counter that survives a detach and produces the oscillation report.Attachopens the windowMeasurementVBUS · currentWindowcounted in CYCLESPhaseOFF · IN-RUSH · ON ·CONFIGSag + budgetsuppressed in the windowPort limitnever suppressedTrip countsurvives a detach12
Attach opens the window; a per-cycle counter closes it. While it is open the sag and budget checks are suppressed entirely and the port limit is not. The trip counter is the only piece of state that survives a detach, because an oscillation is a pattern across attach cycles rather than within one.

6. Verilog-2005 Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// usb_power_monitor -- the same two numbers, read at two different times,
// and one of them is a healthy device while the other is a fault.
//
// IN-RUSH IS LEGAL. SAG IS NOT. THE DIFFERENCE IS A TIME WINDOW.
//
// When a device is plugged in, its bulk capacitance is empty. Connecting it
// to VBUS is connecting a 5 volt rail to a discharged capacitor, and the
// current that flows is limited only by the resistance of the path. The
// voltage at the port DROPS while that happens.
//
//     VBUS dips to 4.1 V, 300 mA flows      at t = 0
//         -> normal. This is what attaching a device LOOKS like.
//
//     VBUS dips to 4.1 V, 300 mA flows      at t = 2 seconds
//         -> a fault. Something is loading the rail.
//
// Identical readings. The only thing that distinguishes them is WHEN they
// were taken, relative to the attach event.
//
// This is why the specification bounds the device's bulk capacitance rather
// than bounding the current: you cannot limit the in-rush current directly
// without a soft-start circuit in every device, but you CAN bound how long
// it lasts, and a bounded duration is something a monitor can check.
//
// SO A MONITOR THAT DOES NOT KNOW ABOUT THE WINDOW HAS TWO FAILURE MODES
//
//     no window        every attach reports a sag and an over-current.
//                      One false alarm per plug-in event, for ever.
//
//     window too long  a real short circuit is invisible for as long as
//                      the window lasts, which is exactly when the
//                      damage happens.
//
// Both are the familiar pair: a checker nobody reads, or a checker that
// misses the thing it exists for (chapter 24.1).
//
// AND THE BUDGET DEPENDS ON THE ENUMERATION STATE
//
// A device may draw one unit load -- 100 mA -- from the moment it attaches.
// It may draw its configured maximum, up to 500 mA, only AFTER the host has
// sent SET_CONFIGURATION. Drawing 500 mA before that is a specification
// violation that works perfectly on every desktop and fails on a bus-powered
// hub, which is the most expensive category of bug there is:
//
//     it works on the developer's machine, always.
//
// OVER-CURRENT IS A PROTECTION ACTION, NOT AN ERROR
//
// A hub shutting off a port that exceeds its limit is the hub working. What
// is NOT normal is a port that shuts off, re-enables, and shuts off again --
// an oscillation, which means something downstream is latching up and
// recovering. So trips are counted and the OSCILLATION is the report.
module usb_power_monitor #(
  parameter integer V_MIN_MV   = 4400,  // the device's floor, in millivolts
  parameter integer I_UNCFG_MA = 100,   // one unit load, before configuration
  parameter integer I_CFG_MA   = 500,   // the configured maximum
  parameter integer I_LIMIT_MA = 900,   // the hub's port limit
  parameter integer INRUSH_CYC = 100,   // how long in-rush may last
  parameter integer OSC_MAX    = 3      // trips before it is an oscillation
) (
  input  wire        clk,
  input  wire        rst_n,

  input  wire        attach,
  input  wire        detach,
  input  wire        configured,   // SET_CONFIGURATION has been accepted
  input  wire        sample,       // vbus_mv and i_ma are valid this cycle
  input  wire [12:0] vbus_mv,
  input  wire [9:0]  i_ma,
  input  wire        eot,

  output wire [1:0]  phase,
  output wire [15:0] inrush_left,  // cycles of in-rush window remaining
  output wire        port_off,
  output wire        err_pulse,
  output wire [2:0]  err_code,

  output reg [31:0] n_sample,
  output reg [31:0] n_attach,
  output reg [31:0] n_sag,
  output reg [31:0] n_inrush_long,
  output reg [31:0] n_overcurrent,
  output reg [31:0] n_budget,
  output reg [31:0] n_osc,
  output reg [31:0] n_trip
);

  localparam [1:0] P_OFF    = 2'd0,  // nothing attached
                   P_INRUSH = 2'd1,  // attached, inside the window
                   P_ON     = 2'd2,  // attached, window expired, unconfigured
                   P_CFG    = 2'd3;  // configured

  localparam [2:0] E_NONE        = 3'd0,
                   E_SAG         = 3'd1,  // VBUS low OUTSIDE the window
                   E_INRUSH_LONG = 3'd2,  // still drawing when it expired
                   E_OVERCURRENT = 3'd3,  // past the port limit
                   E_BUDGET      = 3'd4,  // past the ENUMERATION-STATE budget
                   E_OSC         = 3'd5;  // trip / re-enable / trip

  reg [1:0]  ph_r;
  reg [15:0] win_r;
  reg [9:0]  ilast_r;
  reg [7:0]  trip_r;
  reg        off_r, osc_r;      // osc_r: reported once
  reg        er_r;
  reg [2:0]  ec_r;

  assign phase       = ph_r;
  assign inrush_left = win_r;
  assign port_off    = off_r;
  assign err_pulse   = er_r;
  assign err_code    = ec_r;

  reg [1:0]  ph_n;
  reg [15:0] win_n;
  reg [9:0]  ilast_n;
  reg [7:0]  trip_n;
  reg        off_n, osc_n, er_n;
  reg [2:0]  ec_n;
  reg        sag_n, il_n, oc_n, bud_n, oscc_n, trip_pulse, att_n;
  reg [9:0]  i_now;

  always @* begin
    ph_n    = ph_r;
    win_n   = win_r;
    ilast_n = ilast_r;
    trip_n  = trip_r;
    off_n   = off_r;
    osc_n   = osc_r;
    er_n    = 1'b0;
    ec_n    = E_NONE;
    sag_n = 1'b0; il_n = 1'b0; oc_n = 1'b0; bud_n = 1'b0;
    oscc_n = 1'b0; trip_pulse = 1'b0; att_n = 1'b0;

    // ---- The most recent measurement available AT THIS INSTANT. ----
    //
    // This cycle's, if there is one, otherwise the last one taken. The
    // window-close test below runs BEFORE the sample is registered, so
    // reading `ilast_r` there uses the measurement from one sample ago --
    // and a device that drops back under one unit load on the very last
    // sample of the window is then reported as an in-rush overrun. It is
    // off by exactly one measurement, which is invisible in every test
    // except one that lands the recovery on the final cycle.
    i_now = (sample && !off_r) ? i_ma : ilast_r;

    if (eot) begin
      // Nothing: the counters are the report.
    end else if (detach) begin
      // ---- Everything resets except the trip count. ----
      //
      // The trip count is what makes an oscillation visible, and an
      // oscillation is a sequence of attach/trip/detach cycles. Clearing it
      // on detach deletes the only evidence that the port is cycling --
      // each individual trip then looks like a one-off.
      ph_n  = P_OFF;
      win_n = 16'd0;
      off_n = 1'b0;
    end else if (attach) begin
      // ---- The window opens HERE, and only here. ----
      ph_n    = P_INRUSH;
      win_n   = INRUSH_CYC;
      off_n   = 1'b0;
      ilast_n = 10'd0;
      att_n   = 1'b1;
    end else begin
      if (configured && (ph_r == P_ON)) ph_n = P_CFG;

      // ---- The window runs on TIME, not on samples. ----
      //
      // Decremented per cycle rather than per measurement, because in-rush
      // is a physical duration. Tied to the sample rate instead, a monitor
      // that samples more slowly gets a longer window -- so the same device
      // passes on one analyser and fails on another, which is the kind of
      // discrepancy that gets blamed on the device for a week.
      if (ph_r == P_INRUSH) begin
        if (win_r != 16'd0) begin
          win_n = win_r - 16'd1;
          if (win_n == 16'd0) begin
            ph_n = P_ON;
            // ---- The window closed. Is it STILL drawing? ----
            //
            // If it is, the bulk capacitance is oversized or there is a
            // short, and the distinction from a legal in-rush is precisely
            // that this one did not finish in time.
            if (i_now > I_UNCFG_MA) begin
              er_n = 1'b1; ec_n = E_INRUSH_LONG;
              il_n = 1'b1;
            end
          end
        end
      end

      if (sample && !off_r) begin
        ilast_n = i_ma;

        if (i_ma > I_LIMIT_MA) begin
          // ---- The port limit is HARDWARE. It applies during in-rush too.
          //
          // The window excuses a device from the BUDGET, not from the
          // hub's protection circuit -- a dead short still trips the port
          // on the first microsecond, and a monitor that suppresses that
          // because "we are in the in-rush window" is suppressing the one
          // reading that means something is on fire.
          er_n  = 1'b1; ec_n = E_OVERCURRENT;
          oc_n  = 1'b1;
          off_n = 1'b1;
          ph_n  = P_OFF;
          win_n = 16'd0;
          trip_pulse = 1'b1;
          if (trip_r != 8'hFF) trip_n = trip_r + 8'd1;
          if ((trip_n >= OSC_MAX) && !osc_r) begin
            // Reported ONCE. A port that is cycling produces a trip every
            // few milliseconds, and one report per trip is chapter 25.3's
            // flood with a different name on it.
            ec_n   = E_OSC;
            oscc_n = 1'b1;
            osc_n  = 1'b1;
          end
        end else if (ph_r == P_INRUSH) begin
          // ---- INSIDE THE WINDOW: NOTHING IS REPORTED. ----
          //
          // Not "reported at a lower severity", not "reported and
          // filtered". A sagging rail and a large current during in-rush
          // are what a correct device looks like, and reporting them
          // produces one false alarm per plug-in event for ever.
        end else if (vbus_mv < V_MIN_MV) begin
          er_n  = 1'b1; ec_n = E_SAG;
          sag_n = 1'b1;
        end else if ((ph_r == P_CFG) ? (i_ma > I_CFG_MA)
                                     : (i_ma > I_UNCFG_MA)) begin
          // ---- The budget depends on the ENUMERATION STATE. ----
          //
          // 100 mA before SET_CONFIGURATION, the configured maximum after.
          // A device that ignores this works on every desktop and fails on
          // a bus-powered hub.
          er_n  = 1'b1; ec_n = E_BUDGET;
          bud_n = 1'b1;
        end
      end
    end
  end

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      ph_r    <= P_OFF;
      win_r   <= 16'd0;
      ilast_r <= 10'd0;
      trip_r  <= 8'd0;
      off_r   <= 1'b0;
      osc_r   <= 1'b0;
      er_r    <= 1'b0;
      ec_r    <= E_NONE;
      n_sample      <= 32'd0;
      n_attach      <= 32'd0;
      n_sag         <= 32'd0;
      n_inrush_long <= 32'd0;
      n_overcurrent <= 32'd0;
      n_budget      <= 32'd0;
      n_osc         <= 32'd0;
      n_trip        <= 32'd0;
    end else begin
      ph_r    <= ph_n;
      win_r   <= win_n;
      ilast_r <= ilast_n;
      trip_r  <= trip_n;
      off_r   <= off_n;
      osc_r   <= osc_n;
      er_r    <= er_n;
      ec_r    <= ec_n;

      // `!off_r` is part of the condition, not an oversight: a tripped
      // port has no voltage on it, so a measurement offered while it is off
      // is not a measurement of anything. Counting it inflates the
      // denominator of every rate computed from this block by however long
      // the port stayed off -- which is longest exactly when things are
      // going worst.
      if (sample && !eot && !detach && !attach && !off_r)
        n_sample <= n_sample + 32'd1;
      if (att_n)      n_attach      <= n_attach      + 32'd1;
      if (sag_n)      n_sag         <= n_sag         + 32'd1;
      if (il_n)       n_inrush_long <= n_inrush_long + 32'd1;
      if (oc_n)       n_overcurrent <= n_overcurrent + 32'd1;
      if (bud_n)      n_budget      <= n_budget      + 32'd1;
      if (oscc_n)     n_osc         <= n_osc         + 32'd1;
      if (trip_pulse) n_trip        <= n_trip        + 32'd1;
    end
  end
endmodule

7. SystemVerilog Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// usb_power_monitor -- the same two numbers, read at two different times,
// and one of them is a healthy device while the other is a fault.
//
// IN-RUSH IS LEGAL. SAG IS NOT. THE DIFFERENCE IS A TIME WINDOW.
//
// When a device is plugged in, its bulk capacitance is empty. Connecting it
// to VBUS is connecting a 5 volt rail to a discharged capacitor, and the
// current that flows is limited only by the resistance of the path. The
// voltage at the port DROPS while that happens.
//
//     VBUS dips to 4.1 V, 300 mA flows      at t = 0
//         -> normal. This is what attaching a device LOOKS like.
//
//     VBUS dips to 4.1 V, 300 mA flows      at t = 2 seconds
//         -> a fault. Something is loading the rail.
//
// Identical readings. The only thing that distinguishes them is WHEN they
// were taken, relative to the attach event.
//
// This is why the specification bounds the device's bulk capacitance rather
// than bounding the current: you cannot limit the in-rush current directly
// without a soft-start circuit in every device, but you CAN bound how long
// it lasts, and a bounded duration is something a monitor can check.
//
// SO A MONITOR THAT DOES NOT KNOW ABOUT THE WINDOW HAS TWO FAILURE MODES
//
//     no window        every attach reports a sag and an over-current.
//                      One false alarm per plug-in event, for ever.
//
//     window too long  a real short circuit is invisible for as long as
//                      the window lasts, which is exactly when the
//                      damage happens.
//
// Both are the familiar pair: a checker nobody reads, or a checker that
// misses the thing it exists for (chapter 24.1).
//
// AND THE BUDGET DEPENDS ON THE ENUMERATION STATE
//
// A device may draw one unit load -- 100 mA -- from the moment it attaches.
// It may draw its configured maximum, up to 500 mA, only AFTER the host has
// sent SET_CONFIGURATION. Drawing 500 mA before that is a specification
// violation that works perfectly on every desktop and fails on a bus-powered
// hub, which is the most expensive category of bug there is:
//
//     it works on the developer's machine, always.
//
// OVER-CURRENT IS A PROTECTION ACTION, NOT AN ERROR
//
// A hub shutting off a port that exceeds its limit is the hub working. What
// is NOT normal is a port that shuts off, re-enables, and shuts off again --
// an oscillation, which means something downstream is latching up and
// recovering. So trips are counted and the OSCILLATION is the report.
package usb_power_pkg;
  // The phase is the whole argument of this block, so it gets a type: the
  // same measurement means different things in different phases, and a
  // two-bit output called `phase` is a number somebody has to look up.
  typedef enum logic [1:0] {
    P_OFF    = 2'd0,   // nothing attached
    P_INRUSH = 2'd1,   // attached, inside the window
    P_ON     = 2'd2,   // attached, window expired, unconfigured
    P_CFG    = 2'd3    // configured
  } phase_e;

  typedef enum logic [2:0] {
    E_NONE        = 3'd0,
    E_SAG         = 3'd1,   // VBUS low OUTSIDE the window
    E_INRUSH_LONG = 3'd2,   // still drawing when the window expired
    E_OVERCURRENT = 3'd3,   // past the port limit
    E_BUDGET      = 3'd4,   // past the ENUMERATION-STATE budget
    E_OSC         = 3'd5    // trip / re-enable / trip
  } pwr_err_e;
endpackage

module usb_power_monitor
  import usb_power_pkg::*;
 #(
  parameter int V_MIN_MV   = 4400,   // the device's floor, in millivolts
  parameter int I_UNCFG_MA = 100,    // one unit load, before configuration
  parameter int I_CFG_MA   = 500,    // the configured maximum
  parameter int I_LIMIT_MA = 900,    // the hub's port limit
  parameter int INRUSH_CYC = 100,    // how long in-rush may last
  parameter int OSC_MAX    = 3       // trips before it is an oscillation
) (
  input  logic        clk,
  input  logic        rst_n,

  input  logic        attach,
  input  logic        detach,
  input  logic        configured,   // SET_CONFIGURATION has been accepted
  input  logic        sample,       // vbus_mv and i_ma are valid this cycle
  input  logic [12:0] vbus_mv,
  input  logic [9:0]  i_ma,
  input  logic        eot,

  output phase_e      phase,
  output logic [15:0] inrush_left,  // cycles of in-rush window remaining
  output logic        port_off,
  output logic        err_pulse,
  output pwr_err_e    err_code,

  output logic [31:0] n_sample,
  output logic [31:0] n_attach,
  output logic [31:0] n_sag,
  output logic [31:0] n_inrush_long,
  output logic [31:0] n_overcurrent,
  output logic [31:0] n_budget,
  output logic [31:0] n_osc,
  output logic [31:0] n_trip
);

  phase_e     ph_r;
  logic [15:0] win_r;
  logic [9:0]  ilast_r;
  logic [7:0]  trip_r;
  logic        off_r, osc_r;    // osc_r: reported once
  logic        er_r;
  pwr_err_e    ec_r;

  assign phase       = ph_r;
  assign inrush_left = win_r;
  assign port_off    = off_r;
  assign err_pulse   = er_r;
  assign err_code    = ec_r;

  phase_e      ph_n;
  logic [15:0] win_n;
  logic [9:0]  ilast_n;
  logic [7:0]  trip_n;
  logic        off_n, osc_n, er_n;
  pwr_err_e    ec_n;
  logic        sag_n, il_n, oc_n, bud_n, oscc_n, trip_pulse, att_n;
  logic [9:0]  i_now;

  always_comb begin
    ph_n    = ph_r;
    win_n   = win_r;
    ilast_n = ilast_r;
    trip_n  = trip_r;
    off_n   = off_r;
    osc_n   = osc_r;
    er_n    = 1'b0;
    ec_n    = E_NONE;
    sag_n = 1'b0; il_n = 1'b0; oc_n = 1'b0; bud_n = 1'b0;
    oscc_n = 1'b0; trip_pulse = 1'b0; att_n = 1'b0;

    // ---- The most recent measurement available AT THIS INSTANT. ----
    //
    // This cycle's, if there is one, otherwise the last one taken. The
    // window-close test below runs BEFORE the sample is registered, so
    // reading `ilast_r` there uses the measurement from one sample ago --
    // and a device that drops back under one unit load on the very last
    // sample of the window is then reported as an in-rush overrun. It is
    // off by exactly one measurement, which is invisible in every test
    // except one that lands the recovery on the final cycle.
    i_now = (sample && !off_r) ? i_ma : ilast_r;

    if (eot) begin
      // Nothing: the counters are the report.
    end else if (detach) begin
      // ---- Everything resets except the trip count. ----
      //
      // The trip count is what makes an oscillation visible, and an
      // oscillation is a sequence of attach/trip/detach cycles. Clearing it
      // on detach deletes the only evidence that the port is cycling --
      // each individual trip then looks like a one-off.
      ph_n  = P_OFF;
      win_n = 16'd0;
      off_n = 1'b0;
    end else if (attach) begin
      // ---- The window opens HERE, and only here. ----
      ph_n    = P_INRUSH;
      win_n   = 16'(INRUSH_CYC);
      off_n   = 1'b0;
      ilast_n = 10'd0;
      att_n   = 1'b1;
    end else begin
      if (configured && (ph_r == P_ON)) ph_n = P_CFG;

      // ---- The window runs on TIME, not on samples. ----
      //
      // Decremented per cycle rather than per measurement, because in-rush
      // is a physical duration. Tied to the sample rate instead, a monitor
      // that samples more slowly gets a longer window -- so the same device
      // passes on one analyser and fails on another, which is the kind of
      // discrepancy that gets blamed on the device for a week.
      if (ph_r == P_INRUSH) begin
        if (win_r != 16'd0) begin
          win_n = win_r - 16'd1;
          if (win_n == 16'd0) begin
            ph_n = P_ON;
            // ---- The window closed. Is it STILL drawing? ----
            //
            // If it is, the bulk capacitance is oversized or there is a
            // short, and the distinction from a legal in-rush is precisely
            // that this one did not finish in time.
            if (i_now > I_UNCFG_MA) begin
              er_n = 1'b1; ec_n = E_INRUSH_LONG;
              il_n = 1'b1;
            end
          end
        end
      end

      if (sample && !off_r) begin
        ilast_n = i_ma;

        if (i_ma > I_LIMIT_MA) begin
          // ---- The port limit is HARDWARE. It applies during in-rush too.
          //
          // The window excuses a device from the BUDGET, not from the
          // hub's protection circuit -- a dead short still trips the port
          // on the first microsecond, and a monitor that suppresses that
          // because "we are in the in-rush window" is suppressing the one
          // reading that means something is on fire.
          er_n  = 1'b1; ec_n = E_OVERCURRENT;
          oc_n  = 1'b1;
          off_n = 1'b1;
          ph_n  = P_OFF;
          win_n = 16'd0;
          trip_pulse = 1'b1;
          if (trip_r != 8'hFF) trip_n = trip_r + 8'd1;
          if ((trip_n >= OSC_MAX) && !osc_r) begin
            // Reported ONCE. A port that is cycling produces a trip every
            // few milliseconds, and one report per trip is chapter 25.3's
            // flood with a different name on it.
            ec_n   = E_OSC;
            oscc_n = 1'b1;
            osc_n  = 1'b1;
          end
        end else if (ph_r == P_INRUSH) begin
          // ---- INSIDE THE WINDOW: NOTHING IS REPORTED. ----
          //
          // Not "reported at a lower severity", not "reported and
          // filtered". A sagging rail and a large current during in-rush
          // are what a correct device looks like, and reporting them
          // produces one false alarm per plug-in event for ever.
        end else if (vbus_mv < V_MIN_MV) begin
          er_n  = 1'b1; ec_n = E_SAG;
          sag_n = 1'b1;
        end else if (i_ma > ((ph_r == P_CFG) ? I_CFG_MA : I_UNCFG_MA)) begin
          // ---- The budget depends on the ENUMERATION STATE. ----
          //
          // 100 mA before SET_CONFIGURATION, the configured maximum after.
          // A device that ignores this works on every desktop and fails on
          // a bus-powered hub.
          er_n  = 1'b1; ec_n = E_BUDGET;
          bud_n = 1'b1;
        end
      end
    end
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      ph_r    <= P_OFF;
      win_r   <= 16'd0;
      ilast_r <= 10'd0;
      trip_r  <= 8'd0;
      off_r   <= 1'b0;
      osc_r   <= 1'b0;
      er_r    <= 1'b0;
      ec_r    <= E_NONE;
      n_sample      <= 32'd0;
      n_attach      <= 32'd0;
      n_sag         <= 32'd0;
      n_inrush_long <= 32'd0;
      n_overcurrent <= 32'd0;
      n_budget      <= 32'd0;
      n_osc         <= 32'd0;
      n_trip        <= 32'd0;
    end else begin
      ph_r    <= ph_n;
      win_r   <= win_n;
      ilast_r <= ilast_n;
      trip_r  <= trip_n;
      off_r   <= off_n;
      osc_r   <= osc_n;
      er_r    <= er_n;
      ec_r    <= ec_n;

      // `!off_r` is part of the condition, not an oversight: a tripped
      // port has no voltage on it, so a measurement offered while it is off
      // is not a measurement of anything. Counting it inflates the
      // denominator of every rate computed from this block by however long
      // the port stayed off -- which is longest exactly when things are
      // going worst.
      if (sample && !eot && !detach && !attach && !off_r)
        n_sample <= n_sample + 32'd1;
      if (att_n)      n_attach      <= n_attach      + 32'd1;
      if (sag_n)      n_sag         <= n_sag         + 32'd1;
      if (il_n)       n_inrush_long <= n_inrush_long + 32'd1;
      if (oc_n)       n_overcurrent <= n_overcurrent + 32'd1;
      if (bud_n)      n_budget      <= n_budget      + 32'd1;
      if (oscc_n)     n_osc         <= n_osc         + 32'd1;
      if (trip_pulse) n_trip        <= n_trip        + 32'd1;
    end
  end
endmodule

8. VHDL-2008 Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- usb_power_monitor -- the same two numbers, read at two different times,
-- and one of them is a healthy device while the other is a fault.
--
-- IN-RUSH IS LEGAL. SAG IS NOT. THE DIFFERENCE IS A TIME WINDOW.
--
-- When a device is plugged in, its bulk capacitance is empty. Connecting it
-- to VBUS is connecting a 5 volt rail to a discharged capacitor, and the
-- current that flows is limited only by the resistance of the path. The
-- voltage at the port DROPS while that happens.
--
--     VBUS dips to 4.1 V, 300 mA flows      at t = 0
--         -> normal. This is what attaching a device LOOKS like.
--
--     VBUS dips to 4.1 V, 300 mA flows      at t = 2 seconds
--         -> a fault. Something is loading the rail.
--
-- Identical readings. The only thing that distinguishes them is WHEN they
-- were taken, relative to the attach event.
--
-- This is why the specification bounds the device's bulk capacitance rather
-- than bounding the current: you cannot limit in-rush current directly
-- without a soft-start circuit in every device, but you CAN bound how long
-- it lasts, and a bounded duration is something a monitor can check.
--
-- SO A MONITOR THAT DOES NOT KNOW ABOUT THE WINDOW HAS TWO FAILURE MODES
--
--     no window        every attach reports a sag and an over-current.
--                      One false alarm per plug-in event, for ever.
--
--     window too long  a real short circuit is invisible for as long as the
--                      window lasts, which is exactly when the damage
--                      happens.
--
-- Both are the familiar pair: a checker nobody reads, or a checker that
-- misses the thing it exists for (chapter 24.1).
--
-- AND THE BUDGET DEPENDS ON THE ENUMERATION STATE
--
-- A device may draw one unit load -- 100 mA -- from the moment it attaches.
-- It may draw its configured maximum, up to 500 mA, only AFTER the host has
-- sent SET_CONFIGURATION. Drawing 500 mA before that is a violation that
-- works perfectly on every desktop and fails on a bus-powered hub, which is
-- the most expensive category of bug there is:
--
--     it works on the developer's machine, always.
--
-- OVER-CURRENT IS A PROTECTION ACTION, NOT AN ERROR
--
-- A hub shutting off a port that exceeds its limit is the hub working. What
-- is NOT normal is a port that shuts off, re-enables and shuts off again --
-- an oscillation, which means something downstream is latching up and
-- recovering. So trips are counted and the OSCILLATION is the report.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

package usb_power_pkg is
  constant P_OFF    : std_logic_vector(1 downto 0) := "00";
  constant P_INRUSH : std_logic_vector(1 downto 0) := "01";
  constant P_ON     : std_logic_vector(1 downto 0) := "10";
  constant P_CFG    : std_logic_vector(1 downto 0) := "11";

  constant E_NONE        : std_logic_vector(2 downto 0) := "000";
  constant E_SAG         : std_logic_vector(2 downto 0) := "001";
  constant E_INRUSH_LONG : std_logic_vector(2 downto 0) := "010";
  constant E_OVERCURRENT : std_logic_vector(2 downto 0) := "011";
  constant E_BUDGET      : std_logic_vector(2 downto 0) := "100";
  constant E_OSC         : std_logic_vector(2 downto 0) := "101";
end package;

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

entity usb_power_monitor is
  generic (
    V_MIN_MV   : integer := 4400;   -- the device's floor, in millivolts
    I_UNCFG_MA : integer := 100;    -- one unit load, before configuration
    I_CFG_MA   : integer := 500;    -- the configured maximum
    I_LIMIT_MA : integer := 900;    -- the hub's port limit
    INRUSH_CYC : integer := 100;    -- how long in-rush may last
    OSC_MAX    : integer := 3       -- trips before it is an oscillation
  );
  port (
    clk        : in  std_logic;
    rst_n      : in  std_logic;

    attach     : in  std_logic;
    detach     : in  std_logic;
    configured : in  std_logic;     -- SET_CONFIGURATION has been accepted
    sample     : in  std_logic;     -- vbus_mv and i_ma are valid this cycle
    vbus_mv    : in  unsigned(12 downto 0);
    i_ma       : in  unsigned(9 downto 0);
    eot        : in  std_logic;

    phase       : out std_logic_vector(1 downto 0);
    inrush_left : out unsigned(15 downto 0);
    port_off    : out std_logic;
    err_pulse   : out std_logic;
    err_code    : out std_logic_vector(2 downto 0);

    n_sample      : out unsigned(31 downto 0);
    n_attach      : out unsigned(31 downto 0);
    n_sag         : out unsigned(31 downto 0);
    n_inrush_long : out unsigned(31 downto 0);
    n_overcurrent : out unsigned(31 downto 0);
    n_budget      : out unsigned(31 downto 0);
    n_osc         : out unsigned(31 downto 0);
    n_trip        : out unsigned(31 downto 0)
  );
end entity;

architecture rtl of usb_power_monitor is
  signal ph_r    : std_logic_vector(1 downto 0) := P_OFF;
  signal win_r   : unsigned(15 downto 0) := (others => '0');
  signal ilast_r : unsigned(9 downto 0)  := (others => '0');
  signal trip_r  : unsigned(7 downto 0)  := (others => '0');
  signal off_r   : std_logic := '0';
  signal osc_r   : std_logic := '0';      -- reported once
  signal er_r    : std_logic := '0';
  signal ec_r    : std_logic_vector(2 downto 0) := E_NONE;

  signal samp_c, att_c, sag_c, il_c : unsigned(31 downto 0)
    := (others => '0');
  signal oc_c, bud_c, osc_c, trip_c : unsigned(31 downto 0)
    := (others => '0');
begin
  phase       <= ph_r;
  inrush_left <= win_r;
  port_off    <= off_r;
  err_pulse   <= er_r;
  err_code    <= ec_r;

  n_sample      <= samp_c;
  n_attach      <= att_c;
  n_sag         <= sag_c;
  n_inrush_long <= il_c;
  n_overcurrent <= oc_c;
  n_budget      <= bud_c;
  n_osc         <= osc_c;
  n_trip        <= trip_c;

  process (clk, rst_n)
    variable ph_v    : std_logic_vector(1 downto 0);
    variable win_v   : unsigned(15 downto 0);
    variable ilast_v : unsigned(9 downto 0);
    variable trip_v  : unsigned(7 downto 0);
    variable off_v, osc_v, er_v : std_logic;
    variable ec_v    : std_logic_vector(2 downto 0);
    variable i_now   : unsigned(9 downto 0);
  begin
    if rst_n = '0' then
      ph_r    <= P_OFF;
      win_r   <= (others => '0');
      ilast_r <= (others => '0');
      trip_r  <= (others => '0');
      off_r   <= '0';
      osc_r   <= '0';
      er_r    <= '0';
      ec_r    <= E_NONE;
      samp_c  <= (others => '0');
      att_c   <= (others => '0');
      sag_c   <= (others => '0');
      il_c    <= (others => '0');
      oc_c    <= (others => '0');
      bud_c   <= (others => '0');
      osc_c   <= (others => '0');
      trip_c  <= (others => '0');
    elsif rising_edge(clk) then
      ph_v    := ph_r;
      win_v   := win_r;
      ilast_v := ilast_r;
      trip_v  := trip_r;
      off_v   := off_r;
      osc_v   := osc_r;
      er_v    := '0';
      ec_v    := E_NONE;

      -- ---- The most recent measurement available AT THIS INSTANT. ----
      --
      -- This cycle's, if there is one, otherwise the last one taken. The
      -- window-close test below runs BEFORE the sample is registered, so
      -- reading ilast_r there uses the measurement from one sample ago --
      -- and a device that drops back under one unit load on the very last
      -- sample of the window is then reported as an in-rush overrun. Off by
      -- exactly one measurement, and invisible in every test except one
      -- that lands the recovery on the final cycle.
      if sample = '1' and off_r = '0' then
        i_now := i_ma;
      else
        i_now := ilast_r;
      end if;

      if eot = '1' then
        -- Nothing: the counters are the report.
        null;
      elsif detach = '1' then
        -- ---- Everything resets except the trip count. ----
        --
        -- The trip count is what makes an oscillation visible, and an
        -- oscillation is a sequence of attach/trip/detach cycles. Clearing
        -- it on detach deletes the only evidence that the port is cycling;
        -- each individual trip then looks like a one-off.
        ph_v  := P_OFF;
        win_v := (others => '0');
        off_v := '0';
      elsif attach = '1' then
        -- ---- The window opens HERE, and only here. ----
        ph_v    := P_INRUSH;
        win_v   := to_unsigned(INRUSH_CYC, 16);
        off_v   := '0';
        ilast_v := (others => '0');
        att_c   <= att_c + 1;
      else
        if configured = '1' and ph_r = P_ON then
          ph_v := P_CFG;
        end if;

        -- ---- The window runs on TIME, not on samples. ----
        --
        -- Decremented per cycle rather than per measurement, because
        -- in-rush is a physical duration. Tied to the sample rate instead,
        -- a monitor that samples more slowly gets a longer window -- so the
        -- same device passes on one analyser and fails on another, which is
        -- the kind of discrepancy that gets blamed on the device for a week.
        if ph_r = P_INRUSH then
          if win_v /= x"0000" then
            win_v := win_v - 1;
            if win_v = x"0000" then
              ph_v := P_ON;
              -- ---- The window closed. Is it STILL drawing? ----
              --
              -- If it is, the bulk capacitance is oversized or there is a
              -- short, and the distinction from a legal in-rush is
              -- precisely that this one did not finish in time.
              if i_now > to_unsigned(I_UNCFG_MA, 10) then
                er_v := '1';
                ec_v := E_INRUSH_LONG;
                il_c <= il_c + 1;
              end if;
            end if;
          end if;
        end if;

        if sample = '1' and off_r = '0' then
          -- off_r is part of the condition, not an oversight: a tripped
          -- port has no voltage on it, so a measurement offered while it is
          -- off is not a measurement of anything. Counting it inflates the
          -- denominator of every rate computed from this block.
          samp_c  <= samp_c + 1;
          ilast_v := i_ma;

          if i_ma > to_unsigned(I_LIMIT_MA, 10) then
            -- ---- The port limit is HARDWARE. It applies during in-rush.
            --
            -- The window excuses a device from the BUDGET, not from the
            -- hub's protection circuit -- a dead short still trips the port
            -- on the first microsecond, and a monitor that suppresses that
            -- because "we are in the in-rush window" is suppressing the one
            -- reading that means something is on fire.
            er_v   := '1';
            ec_v   := E_OVERCURRENT;
            oc_c   <= oc_c + 1;
            off_v  := '1';
            ph_v   := P_OFF;
            win_v  := (others => '0');
            trip_c <= trip_c + 1;
            if trip_v /= x"FF" then trip_v := trip_v + 1; end if;
            if (trip_v >= to_unsigned(OSC_MAX, 8)) and osc_v = '0' then
              -- Reported ONCE. A port that is cycling produces a trip every
              -- few milliseconds, and one report per trip is chapter 25.3's
              -- flood with a different name on it.
              ec_v  := E_OSC;
              osc_c <= osc_c + 1;
              osc_v := '1';
            end if;
          elsif ph_r = P_INRUSH then
            -- ---- INSIDE THE WINDOW: NOTHING IS REPORTED. ----
            --
            -- Not "reported at a lower severity", not "reported and
            -- filtered". A sagging rail and a large current during in-rush
            -- are what a correct device looks like, and reporting them
            -- produces one false alarm per plug-in event for ever.
            null;
          elsif vbus_mv < to_unsigned(V_MIN_MV, 13) then
            er_v  := '1';
            ec_v  := E_SAG;
            sag_c <= sag_c + 1;
          elsif ((ph_r = P_CFG) and (i_ma > to_unsigned(I_CFG_MA, 10)))
             or ((ph_r /= P_CFG) and (i_ma > to_unsigned(I_UNCFG_MA, 10)))
          then
            -- ---- The budget depends on the ENUMERATION STATE. ----
            --
            -- 100 mA before SET_CONFIGURATION, the configured maximum
            -- after. A device that ignores this works on every desktop and
            -- fails on a bus-powered hub.
            er_v  := '1';
            ec_v  := E_BUDGET;
            bud_c <= bud_c + 1;
          end if;
        end if;
      end if;

      ph_r    <= ph_v;
      win_r   <= win_v;
      ilast_r <= ilast_v;
      trip_r  <= trip_v;
      off_r   <= off_v;
      osc_r   <= osc_v;
      er_r    <= er_v;
      ec_r    <= ec_v;
    end if;
  end process;
end architecture;

9. Seeing the Window Do Its Job

A legal attach: a sagging rail and 400 mA, and nothing is reported

usb_power_monitor — inside the window, nothing is reported

10 cycles
A ten-cycle waveform. An attach pulse puts the phase into IN-RUSH and loads the window counter with one hundred. Four measurements follow at 4150 millivolts and 400 milliamps, each of which is below the voltage floor and above the unconfigured current budget, and the error pulse stays low throughout while the window counter decrements.attach: the window opens at 100attach: the window opens at1004.15 V and 400 mA: not reported4.15 V and 400 mA: notreportedstill silent, and still correctstill silent, and stillcorrectclkattachsamplevbus_mv0415041504150415041504150415041504150i_ma0400400400400400400400400400phaseOFFRUSHRUSHRUSHRUSHRUSHRUSHRUSHRUSHRUSHinrush_left01009998979695949392err_pulset0t1t2t3t4t5t6t7t8t9
The window is open for the whole of this sequence. The rail is below the device floor and the current is four times a unit load, and the error pulse never moves. This is what a correct device looks like being plugged in, and a monitor without a window produces two reports per attach for it.

And the same measurement once the window has closed:

The window expires, and the identical reading becomes a fault

usb_power_monitor — the boundary is one cycle wide

10 cycles
A ten-cycle waveform. The window counter counts down from three to zero, and as it reaches zero the phase changes from IN-RUSH to ON. Measurements at 4100 millivolts are taken throughout; those taken while the phase is IN-RUSH raise nothing, and the first one taken after the phase becomes ON raises the error pulse with the code SAG and advances the sag counter.last cycle inside: still silentlast cycle inside: stillsilentthe window closesthe window closessame reading, now a SAGsame reading, now a SAGclksamplevbus_mv4100410041004100410041004100410041004100i_ma80808080808080808080inrush_left3210000000phaseRUSHRUSHRUSHONONONONONONONerr_pulseerr_code0000SAGSAGSAGSAGSAGSAGn_sag0000123444t0t1t2t3t4t5t6t7t8t9
The window counter reaches zero and the phase becomes ON. The very next measurement — the same 4.1 V that was ignored a cycle earlier — is reported as a sag. Nothing about the measurement changed; the only thing that changed is which side of the boundary it fell on.

10. The Testbenches

The oracle is a shadow model written from sections 1 to 4 rather than from the RTL, re-derived every cycle and compared against every output — fourteen checks per cycle, including the structural one that the per-cause counters sum to the error total.

The central claim of this chapter is about one edge, so the edge is swept:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   for t in 0 .. INRUSH_CYC + 2:
       attach
       wait t cycles
       measure 4.1 V, 300 mA          <- the SAME reading every time
       require: silent    if t <  INRUSH_CYC
                reported  if t >= INRUSH_CYC

   result: silent 100, reported 3

Seven phases:

PhaseWhat it establishes
1the window edge, swept at every offset from 0 to INRUSH_CYC + 2
2a legal attach — sagging rail, 400 mA, the whole window — reports nothing
3an in-rush that has not finished when the window closes is reported once
4the budget follows the enumeration state, and its boundary is exact
5six trips produce six trips and one oscillation report
6every phase × rail-low × current band — 32 situations, reached on the wire
740000 random measurements

Verilog-2005 testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`timescale 1ns/1ps
// Testbench for usb_power_monitor.
//
// The oracle is a shadow model written from the chapter's rules rather than
// from the RTL, re-derived every cycle and compared against every output.
//
// THE CENTRAL CLAIM IS ABOUT A BOUNDARY, SO THE BOUNDARY IS SWEPT
//
// "In-rush is legal and sag is not, and the difference is a time window" is
// a statement about ONE EDGE. Phase 1 drives an identical sagging
// measurement at every offset from 0 to INRUSH_CYC + 2 cycles after attach
// and requires that it is silent on one side of the edge and reported on
// the other -- with the edge in exactly the right place.
//
// A test that checks "inside the window" and "well outside the window"
// passes against a window of any length at all.
module tb_pm_v;

  localparam integer V_MIN_MV   = 4400;
  localparam integer I_UNCFG_MA = 100;
  localparam integer I_CFG_MA   = 500;
  localparam integer I_LIMIT_MA = 900;
  localparam integer INRUSH_CYC = 100;
  localparam integer OSC_MAX    = 3;

  localparam [1:0] P_OFF = 2'd0, P_INRUSH = 2'd1, P_ON = 2'd2, P_CFG = 2'd3;

  localparam [2:0] E_NONE = 3'd0, E_SAG = 3'd1, E_INRUSH_LONG = 3'd2,
                   E_OVERCURRENT = 3'd3, E_BUDGET = 3'd4, E_OSC = 3'd5;

  reg         clk = 1'b0, rst_n = 1'b0;
  reg         attach = 1'b0, detach = 1'b0, configured = 1'b0;
  reg         sample = 1'b0, eot = 1'b0;
  reg  [12:0] vbus_mv = 13'd5000;
  reg  [9:0]  i_ma = 10'd0;

  wire [1:0]  phase;
  wire [15:0] inrush_left;
  wire        port_off, err_pulse;
  wire [2:0]  err_code;
  wire [31:0] n_sample, n_attach, n_sag, n_inrush_long;
  wire [31:0] n_overcurrent, n_budget, n_osc, n_trip;

  usb_power_monitor #(
    .V_MIN_MV(V_MIN_MV), .I_UNCFG_MA(I_UNCFG_MA), .I_CFG_MA(I_CFG_MA),
    .I_LIMIT_MA(I_LIMIT_MA), .INRUSH_CYC(INRUSH_CYC), .OSC_MAX(OSC_MAX)
  ) dut (
    .clk(clk), .rst_n(rst_n),
    .attach(attach), .detach(detach), .configured(configured),
    .sample(sample), .vbus_mv(vbus_mv), .i_ma(i_ma), .eot(eot),
    .phase(phase), .inrush_left(inrush_left), .port_off(port_off),
    .err_pulse(err_pulse), .err_code(err_code),
    .n_sample(n_sample), .n_attach(n_attach), .n_sag(n_sag),
    .n_inrush_long(n_inrush_long), .n_overcurrent(n_overcurrent),
    .n_budget(n_budget), .n_osc(n_osc), .n_trip(n_trip)
  );

  always #5 clk = ~clk;

  // ---------------- the shadow model ----------------
  reg [1:0]  m_ph;
  reg [15:0] m_win;
  reg [9:0]  m_ilast;
  reg [7:0]  m_trip;
  reg        m_off, m_osc, m_er;
  reg [2:0]  m_ec;
  reg [31:0] c_samp, c_att, c_sag, c_il, c_oc, c_bud, c_osc, c_trip;

  integer errors = 0, checks = 0, steps = 0;
  integer k;

  // reach: phase (4) x vbus low? (2) x current band (4)
  reg [0:0] reach [0:31];
  integer   n_reach;

  task ck;
    input [255:0] nm;
    input [31:0]  got, exp;
    begin
      checks = checks + 1;
      if (got !== exp) begin
        errors = errors + 1;
        if (errors < 25)
          $display("FAIL t=%0t step=%0d %0s got=%0d exp=%0d",
                   $time, steps, nm, got, exp);
      end
    end
  endtask

  function [1:0] band; input [9:0] i;
    begin
      if      (i > I_LIMIT_MA) band = 2'd3;
      else if (i > I_CFG_MA)   band = 2'd2;
      else if (i > I_UNCFG_MA) band = 2'd1;
      else                     band = 2'd0;
    end
  endfunction

  reg [1:0] m_ph_pre;

  task model_step;
    reg [9:0] i_now;
    begin
      m_er = 1'b0; m_ec = E_NONE;
      // The most recent measurement available at this instant: this cycle's
      // if there is one, otherwise the last one taken.
      i_now = (sample && !m_off) ? i_ma : m_ilast;

      if (eot) begin
        // nothing
      end else if (detach) begin
        // The trip count SURVIVES a detach. It is the only evidence that
        // the port is cycling rather than failing once.
        m_ph  = P_OFF;
        m_win = 16'd0;
        m_off = 1'b0;
      end else if (attach) begin
        m_ph    = P_INRUSH;
        m_win   = INRUSH_CYC;
        m_off   = 1'b0;
        m_ilast = 10'd0;
        c_att   = c_att + 1;
      end else begin
        if (configured && (m_ph == P_ON)) m_ph = P_CFG;

        if (m_ph == P_INRUSH) begin
          if (m_win != 16'd0) begin
            m_win = m_win - 16'd1;
            if (m_win == 16'd0) begin
              m_ph = P_ON;
              if (i_now > I_UNCFG_MA) begin
                m_er = 1'b1; m_ec = E_INRUSH_LONG;
                c_il = c_il + 1;
              end
            end
          end
        end

        if (sample && !m_off) begin
          c_samp  = c_samp + 1;
          m_ilast = i_ma;

          if (i_ma > I_LIMIT_MA) begin
            // The port limit is hardware. It applies inside the window too:
            // a dead short trips on the first microsecond, and suppressing
            // that because "we are in the in-rush window" suppresses the one
            // reading that means something is on fire.
            m_er  = 1'b1; m_ec = E_OVERCURRENT;
            c_oc  = c_oc + 1;
            m_off = 1'b1;
            m_ph  = P_OFF;
            m_win = 16'd0;
            c_trip = c_trip + 1;
            if (m_trip != 8'hFF) m_trip = m_trip + 8'd1;
            if ((m_trip >= OSC_MAX) && !m_osc) begin
              m_ec  = E_OSC;
              c_osc = c_osc + 1;
              m_osc = 1'b1;
            end
          end else if (m_ph_pre == P_INRUSH) begin
            // INSIDE THE WINDOW: NOTHING IS REPORTED.
          end else if (vbus_mv < V_MIN_MV) begin
            m_er  = 1'b1; m_ec = E_SAG;
            c_sag = c_sag + 1;
          end else if ((m_ph_pre == P_CFG) ? (i_ma > I_CFG_MA)
                                           : (i_ma > I_UNCFG_MA)) begin
            m_er  = 1'b1; m_ec = E_BUDGET;
            c_bud = c_bud + 1;
          end
        end
      end
    end
  endtask

  task check_out;
    begin
      ck("phase",       {30'd0, phase},       {30'd0, m_ph});
      ck("inrush_left", {16'd0, inrush_left}, {16'd0, m_win});
      ck("port_off",    {31'd0, port_off},    {31'd0, m_off});
      ck("err_pulse",   {31'd0, err_pulse},   {31'd0, m_er});
      ck("err_code",    {29'd0, err_code},    {29'd0, m_ec});
      ck("n_sample",      n_sample,      c_samp);
      ck("n_attach",      n_attach,      c_att);
      ck("n_sag",         n_sag,         c_sag);
      ck("n_inrush_long", n_inrush_long, c_il);
      ck("n_overcurrent", n_overcurrent, c_oc);
      ck("n_budget",      n_budget,      c_bud);
      ck("n_osc",         n_osc,         c_osc);
      ck("n_trip",        n_trip,        c_trip);
      // ---- the structural invariant ----
      //
      // Every error is exactly one of five causes, so the per-cause counters
      // sum to the total. E_OSC is NOT added here: an oscillation is
      // reported INSTEAD of the over-current that triggered it, on the same
      // pulse, so counting both would double-count one event.
      ck("cause sum", n_sag + n_inrush_long + n_overcurrent + n_budget,
                      c_sag + c_il          + c_oc          + c_bud);
    end
  endtask

  task step;
    begin
      m_ph_pre = m_ph;
      if (sample && !eot && !detach && !attach && !m_off)
        reach[{m_ph, (vbus_mv < V_MIN_MV) ? 1'b1 : 1'b0, band(i_ma)}] = 1'b1;
      model_step;
      @(posedge clk);
      #1;
      steps = steps + 1;
      check_out;
    end
  endtask

  task meas; input [12:0] v; input [9:0] i;
    begin
      attach = 1'b0; detach = 1'b0; configured = 1'b0; eot = 1'b0;
      sample = 1'b1; vbus_mv = v; i_ma = i;
      step;
    end
  endtask

  task tick;
    begin
      attach = 1'b0; detach = 1'b0; configured = 1'b0; eot = 1'b0;
      sample = 1'b0;
      step;
    end
  endtask

  task do_attach;
    begin
      attach = 1'b1; detach = 1'b0; configured = 1'b0; sample = 1'b0;
      eot = 1'b0;
      step;
      attach = 1'b0;
    end
  endtask

  task do_detach;
    begin
      attach = 1'b0; detach = 1'b1; configured = 1'b0; sample = 1'b0;
      eot = 1'b0;
      step;
      detach = 1'b0;
    end
  endtask

  task do_configure;
    begin
      attach = 1'b0; detach = 1'b0; configured = 1'b1; sample = 1'b0;
      eot = 1'b0;
      step;
      configured = 1'b0;
    end
  endtask

  integer i, t, p, v, b, w;
  integer base_sag, base_il, base_oc, base_bud, base_osc, base_trip;
  integer n_edge_silent, n_edge_reported;

  initial begin
    for (k = 0; k < 32; k = k + 1) reach[k] = 1'b0;
    m_ph = P_OFF; m_win = 16'd0; m_ilast = 10'd0; m_trip = 8'd0;
    m_off = 1'b0; m_osc = 1'b0; m_er = 1'b0; m_ec = E_NONE;
    c_samp=0; c_att=0; c_sag=0; c_il=0; c_oc=0; c_bud=0; c_osc=0; c_trip=0;

    repeat (3) @(posedge clk);
    rst_n = 1'b1;
    @(negedge clk);

    // ================= PHASE 1 -- THE WINDOW EDGE, SWEPT =================
    //
    // The same sagging measurement -- 4.1 V, 300 mA -- taken at every offset
    // from 0 to INRUSH_CYC + 2 cycles after attach. Silent before the edge,
    // reported after it, and the edge exactly where the parameter says.
    //
    // Note that the current is 300 mA throughout, which is over the
    // unconfigured budget, so the post-window report could legitimately be
    // either SAG or BUDGET. The design checks voltage first and the model
    // agrees; what phase 1 proves is that SOMETHING is reported, and which
    // one is pinned by phase 2.
    n_edge_silent = 0; n_edge_reported = 0;
    for (t = 0; t <= INRUSH_CYC + 2; t = t + 1) begin
      do_detach;
      do_attach;
      base_sag = c_sag; base_bud = c_bud;
      for (i = 0; i < t; i = i + 1) tick;
      meas(13'd4100, 10'd300);
      if (t < INRUSH_CYC) begin
        if (c_sag != base_sag || c_bud != base_bud) begin
          errors = errors + 1;
          $display("FAIL reported INSIDE the window at t=%0d", t);
        end else n_edge_silent = n_edge_silent + 1;
      end else begin
        if (c_sag == base_sag && c_bud == base_bud) begin
          errors = errors + 1;
          $display("FAIL silent OUTSIDE the window at t=%0d", t);
        end else n_edge_reported = n_edge_reported + 1;
      end
    end
    if (n_edge_silent != INRUSH_CYC || n_edge_reported != 3) begin
      errors = errors + 1;
      $display("FAIL edge sweep: silent=%0d reported=%0d expected %0d and 3",
               n_edge_silent, n_edge_reported, INRUSH_CYC);
    end

    // ================= PHASE 2 -- a legal attach is COMPLETELY silent ====
    //
    // A sagging rail and 400 mA for the entire window. Zero reports. Not
    // one at a lower severity -- zero.
    do_detach;
    do_attach;
    base_sag = c_sag; base_bud = c_bud; base_oc = c_oc; base_il = c_il;
    for (i = 0; i < INRUSH_CYC - 1; i = i + 1) meas(13'd4150, 10'd400);
    // ...and it ends with the current back under one unit load, so the
    // window closes cleanly.
    meas(13'd4900, 10'd80);
    tick;
    if (c_sag != base_sag || c_bud != base_bud || c_oc != base_oc
        || c_il != base_il) begin
      errors = errors + 1;
      $display("FAIL a legal in-rush produced reports");
    end
    if (m_ph != P_ON) begin
      errors = errors + 1;
      $display("FAIL phase after the window is %0d, expected P_ON", m_ph);
    end

    // ================= PHASE 3 -- in-rush that never finishes ============
    do_detach;
    do_attach;
    base_il = c_il;
    for (i = 0; i < INRUSH_CYC + 1; i = i + 1) meas(13'd4150, 10'd400);
    if (c_il != base_il + 1) begin
      errors = errors + 1;
      $display("FAIL in-rush overrun reports %0d, expected 1", c_il - base_il);
    end

    // ================= PHASE 4 -- the budget follows the ENUMERATION =====
    //
    // 300 mA is a violation before SET_CONFIGURATION and legal after it.
    // Identical measurement, opposite verdict -- the same shape of argument
    // as the window, with enumeration state instead of time.
    do_detach;
    do_attach;
    for (i = 0; i < INRUSH_CYC; i = i + 1) tick;
    base_bud = c_bud;
    meas(13'd5000, 10'd300);              // unconfigured: a violation
    if (c_bud != base_bud + 1) begin
      errors = errors + 1;
      $display("FAIL 300 mA before configuration was not reported");
    end
    do_configure;
    base_bud = c_bud;
    meas(13'd5000, 10'd300);              // configured: legal
    if (c_bud != base_bud) begin
      errors = errors + 1;
      $display("FAIL 300 mA after configuration WAS reported");
    end
    meas(13'd5000, 10'd600);              // over the configured maximum
    if (c_bud != base_bud + 1) begin
      errors = errors + 1;
      $display("FAIL 600 mA after configuration was not reported");
    end
    // ...and the boundary itself: exactly I_CFG_MA is legal, one more is not
    base_bud = c_bud;
    meas(13'd5000, I_CFG_MA[9:0]);
    if (c_bud != base_bud) begin
      errors = errors + 1;
      $display("FAIL exactly I_CFG_MA was reported");
    end
    meas(13'd5000, I_CFG_MA[9:0] + 10'd1);
    if (c_bud != base_bud + 1) begin
      errors = errors + 1;
      $display("FAIL I_CFG_MA+1 was not reported");
    end

    // ================= PHASE 5 -- over-current, trips, oscillation =======
    //
    // Three trips. The first two are the hub working. The third is an
    // oscillation, reported ONCE.
    do_detach;
    base_oc = c_oc; base_osc = c_osc; base_trip = c_trip;
    for (i = 0; i < 6; i = i + 1) begin
      do_attach;
      meas(13'd5000, 10'd950);           // a dead short
      do_detach;
    end
    if (c_trip != base_trip + 6) begin
      errors = errors + 1;
      $display("FAIL trips %0d expected 6", c_trip - base_trip);
    end
    if (c_osc != base_osc + 1) begin
      errors = errors + 1;
      $display("FAIL oscillation reports %0d expected 1", c_osc - base_osc);
    end
    // ...and a tripped port stops being measurable until it is re-attached.
    do_attach;
    meas(13'd5000, 10'd950);
    base_sag = c_sag;
    meas(13'd4000, 10'd50);              // the port is off: no report
    if (c_sag != base_sag) begin
      errors = errors + 1;
      $display("FAIL a tripped port still reported a sag");
    end

    // ================= PHASE 6 -- the exhaustive situation sweep =========
    //
    // Every phase x (rail low or not) x current band. Phases are reached by
    // driving the design the way the design reaches them, never by forcing.
    for (p = 0; p < 4; p = p + 1)
      for (v = 0; v < 2; v = v + 1)
        for (b = 0; b < 4; b = b + 1) begin
          do_detach;
          if (p >= 1) begin
            do_attach;
            if (p >= 2) for (i = 0; i < INRUSH_CYC; i = i + 1) tick;
            if (p == 3) do_configure;
          end
          meas((v == 1) ? 13'd4100 : 13'd5000,
               (b == 0) ? 10'd50  : (b == 1) ? 10'd300 :
               (b == 2) ? 10'd700 : 10'd950);
        end

    // The random phase is switchable, because a mutation score is only
    // interesting once it is DECOMPOSED. Phase 6 alone reaches all 32
    // situations, so the exhaustiveness proof still holds without it.
`ifndef DIRECTED_ONLY
    // ================= PHASE 7 -- random =================================
    for (i = 0; i < 40000; i = i + 1) begin
      w = $unsigned($random) % 1000;
      if (w < 12)       do_attach;
      else if (w < 24)  do_detach;
      else if (w < 34)  do_configure;
      else if (w < 120) tick;
      else meas(13'd3800 + ($unsigned($random) % 1500),
                ($unsigned($random) % 1000));
    end

`endif

    // ================= the exhaustiveness proof ==========================
    n_reach = 0;
    for (k = 0; k < 32; k = k + 1) n_reach = n_reach + reach[k];
    if (n_reach != 32) begin
      errors = errors + 1;
      $display("FAIL situation reach %0d/32", n_reach);
      for (k = 0; k < 32; k = k + 1)
        if (!reach[k])
          $display("  unreached phase=%0d low=%0d band=%0d",
                   k >> 3, (k >> 2) & 1, k & 3);
    end

    $display("steps=%0d checks=%0d reach=%0d/32 errors=%0d",
             steps, checks, n_reach, errors);
    $display("window edge: silent=%0d reported=%0d",
             n_edge_silent, n_edge_reported);
    $display("samples=%0d attaches=%0d trips=%0d", n_sample, n_attach, n_trip);
    $display("sag=%0d inrush_long=%0d overcurrent=%0d budget=%0d osc=%0d",
             n_sag, n_inrush_long, n_overcurrent, n_budget, n_osc);
    $display("%0s: %0d errors in %0d checks",
             (errors == 0) ? "PASS" : "FAIL", errors, checks);
    $finish;
  end
endmodule

SystemVerilog testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`timescale 1ns/1ps
// Testbench for usb_power_monitor.
//
// The oracle is a shadow model written from the chapter's rules rather than
// from the RTL, re-derived every cycle and compared against every output.
//
// THE CENTRAL CLAIM IS ABOUT A BOUNDARY, SO THE BOUNDARY IS SWEPT
//
// "In-rush is legal and sag is not, and the difference is a time window" is
// a statement about ONE EDGE. Phase 1 drives an identical sagging
// measurement at every offset from 0 to INRUSH_CYC + 2 cycles after attach
// and requires that it is silent on one side of the edge and reported on
// the other -- with the edge in exactly the right place.
//
// A test that checks "inside the window" and "well outside the window"
// passes against a window of any length at all.
module tb_pm_sv;
  import usb_power_pkg::*;

  localparam int V_MIN_MV   = 4400;
  localparam int I_UNCFG_MA = 100;
  localparam int I_CFG_MA   = 500;
  localparam int I_LIMIT_MA = 900;
  localparam int INRUSH_CYC = 100;
  localparam int OSC_MAX    = 3;

  logic       clk = 1'b0, rst_n = 1'b0;
  logic       attach = 1'b0, detach = 1'b0, configured = 1'b0;
  logic       sample = 1'b0, eot = 1'b0;
  logic [12:0] vbus_mv = 13'd5000;
  logic [9:0]  i_ma = 10'd0;

  phase_e      phase;
  logic [15:0] inrush_left;
  logic        port_off, err_pulse;
  pwr_err_e    err_code;
  logic [31:0] n_sample, n_attach, n_sag, n_inrush_long;
  logic [31:0] n_overcurrent, n_budget, n_osc, n_trip;

  usb_power_monitor #(
    .V_MIN_MV(V_MIN_MV), .I_UNCFG_MA(I_UNCFG_MA), .I_CFG_MA(I_CFG_MA),
    .I_LIMIT_MA(I_LIMIT_MA), .INRUSH_CYC(INRUSH_CYC), .OSC_MAX(OSC_MAX)
  ) dut (
    .clk(clk), .rst_n(rst_n),
    .attach(attach), .detach(detach), .configured(configured),
    .sample(sample), .vbus_mv(vbus_mv), .i_ma(i_ma), .eot(eot),
    .phase(phase), .inrush_left(inrush_left), .port_off(port_off),
    .err_pulse(err_pulse), .err_code(err_code),
    .n_sample(n_sample), .n_attach(n_attach), .n_sag(n_sag),
    .n_inrush_long(n_inrush_long), .n_overcurrent(n_overcurrent),
    .n_budget(n_budget), .n_osc(n_osc), .n_trip(n_trip)
  );

  always #5 clk = ~clk;

  // ---------------- the shadow model ----------------
  phase_e      m_ph;
  logic [15:0] m_win;
  logic [9:0]  m_ilast;
  logic [7:0]  m_trip;
  logic        m_off, m_osc, m_er;
  pwr_err_e    m_ec;
  int unsigned c_samp, c_att, c_sag, c_il, c_oc, c_bud, c_osc, c_trip;

  int errors = 0, checks = 0, steps = 0;
  int k;

  // reach: phase (4) x vbus low? (2) x current band (4)
  bit reach [32];
  int n_reach;

  task automatic ck(string nm, int unsigned got, int unsigned exp);
    checks++;
    if (got !== exp) begin
      errors++;
      if (errors < 25)
        $display("FAIL t=%0t step=%0d %0s got=%0d exp=%0d",
                 $time, steps, nm, got, exp);
    end
  endtask

  function automatic logic [1:0] band(input logic [9:0] i);
    begin
      if      (i > I_LIMIT_MA) band = 2'd3;
      else if (i > I_CFG_MA)   band = 2'd2;
      else if (i > I_UNCFG_MA) band = 2'd1;
      else                     band = 2'd0;
    end
  endfunction

  phase_e m_ph_pre;

  task automatic model_step;
    logic [9:0] i_now;
    begin
      m_er = 1'b0; m_ec = E_NONE;
      // The most recent measurement available at this instant: this cycle's
      // if there is one, otherwise the last one taken.
      i_now = (sample && !m_off) ? i_ma : m_ilast;

      if (eot) begin
        // nothing
      end else if (detach) begin
        // The trip count SURVIVES a detach. It is the only evidence that
        // the port is cycling rather than failing once.
        m_ph  = P_OFF;
        m_win = 16'd0;
        m_off = 1'b0;
      end else if (attach) begin
        m_ph    = P_INRUSH;
        m_win   = INRUSH_CYC;
        m_off   = 1'b0;
        m_ilast = 10'd0;
        c_att   = c_att + 1;
      end else begin
        if (configured && (m_ph == P_ON)) m_ph = P_CFG;

        if (m_ph == P_INRUSH) begin
          if (m_win != 16'd0) begin
            m_win = m_win - 16'd1;
            if (m_win == 16'd0) begin
              m_ph = P_ON;
              if (i_now > I_UNCFG_MA) begin
                m_er = 1'b1; m_ec = E_INRUSH_LONG;
                c_il = c_il + 1;
              end
            end
          end
        end

        if (sample && !m_off) begin
          c_samp  = c_samp + 1;
          m_ilast = i_ma;

          if (i_ma > I_LIMIT_MA) begin
            // The port limit is hardware. It applies inside the window too:
            // a dead short trips on the first microsecond, and suppressing
            // that because "we are in the in-rush window" suppresses the one
            // reading that means something is on fire.
            m_er  = 1'b1; m_ec = E_OVERCURRENT;
            c_oc  = c_oc + 1;
            m_off = 1'b1;
            m_ph  = P_OFF;
            m_win = 16'd0;
            c_trip = c_trip + 1;
            if (m_trip != 8'hFF) m_trip = m_trip + 8'd1;
            if ((m_trip >= OSC_MAX) && !m_osc) begin
              m_ec  = E_OSC;
              c_osc = c_osc + 1;
              m_osc = 1'b1;
            end
          end else if (m_ph_pre == P_INRUSH) begin
            // INSIDE THE WINDOW: NOTHING IS REPORTED.
          end else if (vbus_mv < V_MIN_MV) begin
            m_er  = 1'b1; m_ec = E_SAG;
            c_sag = c_sag + 1;
          end else if ((m_ph_pre == P_CFG) ? (i_ma > I_CFG_MA)
                                           : (i_ma > I_UNCFG_MA)) begin
            m_er  = 1'b1; m_ec = E_BUDGET;
            c_bud = c_bud + 1;
          end
        end
      end
    end
  endtask

  task automatic check_out;
    begin
      ck("phase",       phase,    m_ph);
      ck("inrush_left", inrush_left, m_win);
      ck("port_off",    port_off, m_off);
      ck("err_pulse",   err_pulse, m_er);
      ck("err_code",    err_code, m_ec);
      ck("n_sample",      n_sample,      c_samp);
      ck("n_attach",      n_attach,      c_att);
      ck("n_sag",         n_sag,         c_sag);
      ck("n_inrush_long", n_inrush_long, c_il);
      ck("n_overcurrent", n_overcurrent, c_oc);
      ck("n_budget",      n_budget,      c_bud);
      ck("n_osc",         n_osc,         c_osc);
      ck("n_trip",        n_trip,        c_trip);
      // ---- the structural invariant ----
      //
      // Every error is exactly one of five causes, so the per-cause counters
      // sum to the total. E_OSC is NOT added here: an oscillation is
      // reported INSTEAD of the over-current that triggered it, on the same
      // pulse, so counting both would double-count one event.
      ck("cause sum", n_sag + n_inrush_long + n_overcurrent + n_budget,
                      c_sag + c_il          + c_oc          + c_bud);
    end
  endtask

  task automatic step;
    begin
      m_ph_pre = m_ph;
      if (sample && !eot && !detach && !attach && !m_off)
        reach[int'(m_ph) * 8 + ((vbus_mv < V_MIN_MV) ? 4 : 0)
              + int'(band(i_ma))] = 1'b1;
      model_step;
      @(posedge clk);
      #1;
      steps = steps + 1;
      check_out;
    end
  endtask

  task automatic meas(logic [12:0] v, logic [9:0] i);
    begin
      attach = 1'b0; detach = 1'b0; configured = 1'b0; eot = 1'b0;
      sample = 1'b1; vbus_mv = v; i_ma = i;
      step;
    end
  endtask

  task automatic tick;
    begin
      attach = 1'b0; detach = 1'b0; configured = 1'b0; eot = 1'b0;
      sample = 1'b0;
      step;
    end
  endtask

  task automatic do_attach;
    begin
      attach = 1'b1; detach = 1'b0; configured = 1'b0; sample = 1'b0;
      eot = 1'b0;
      step;
      attach = 1'b0;
    end
  endtask

  task automatic do_detach;
    begin
      attach = 1'b0; detach = 1'b1; configured = 1'b0; sample = 1'b0;
      eot = 1'b0;
      step;
      detach = 1'b0;
    end
  endtask

  task automatic do_configure;
    begin
      attach = 1'b0; detach = 1'b0; configured = 1'b1; sample = 1'b0;
      eot = 1'b0;
      step;
      configured = 1'b0;
    end
  endtask

  int i, t, p, v, b, w;
  int base_sag, base_il, base_oc, base_bud, base_osc, base_trip;
  int n_edge_silent, n_edge_reported;

  initial begin
    foreach (reach[q]) reach[q] = 1'b0;
    m_ph = P_OFF; m_win = 16'd0; m_ilast = 10'd0; m_trip = 8'd0;
    m_off = 1'b0; m_osc = 1'b0; m_er = 1'b0; m_ec = E_NONE;
    c_samp=0; c_att=0; c_sag=0; c_il=0; c_oc=0; c_bud=0; c_osc=0; c_trip=0;

    repeat (3) @(posedge clk);
    rst_n = 1'b1;
    @(negedge clk);

    // ================= PHASE 1 -- THE WINDOW EDGE, SWEPT =================
    //
    // The same sagging measurement -- 4.1 V, 300 mA -- taken at every offset
    // from 0 to INRUSH_CYC + 2 cycles after attach. Silent before the edge,
    // reported after it, and the edge exactly where the parameter says.
    //
    // Note that the current is 300 mA throughout, which is over the
    // unconfigured budget, so the post-window report could legitimately be
    // either SAG or BUDGET. The design checks voltage first and the model
    // agrees; what phase 1 proves is that SOMETHING is reported, and which
    // one is pinned by phase 2.
    n_edge_silent = 0; n_edge_reported = 0;
    for (t = 0; t <= INRUSH_CYC + 2; t = t + 1) begin
      do_detach;
      do_attach;
      base_sag = c_sag; base_bud = c_bud;
      for (i = 0; i < t; i = i + 1) tick;
      meas(13'd4100, 10'd300);
      if (t < INRUSH_CYC) begin
        if (c_sag != base_sag || c_bud != base_bud) begin
          errors = errors + 1;
          $display("FAIL reported INSIDE the window at t=%0d", t);
        end else n_edge_silent = n_edge_silent + 1;
      end else begin
        if (c_sag == base_sag && c_bud == base_bud) begin
          errors = errors + 1;
          $display("FAIL silent OUTSIDE the window at t=%0d", t);
        end else n_edge_reported = n_edge_reported + 1;
      end
    end
    if (n_edge_silent != INRUSH_CYC || n_edge_reported != 3) begin
      errors = errors + 1;
      $display("FAIL edge sweep: silent=%0d reported=%0d expected %0d and 3",
               n_edge_silent, n_edge_reported, INRUSH_CYC);
    end

    // ================= PHASE 2 -- a legal attach is COMPLETELY silent ====
    //
    // A sagging rail and 400 mA for the entire window. Zero reports. Not
    // one at a lower severity -- zero.
    do_detach;
    do_attach;
    base_sag = c_sag; base_bud = c_bud; base_oc = c_oc; base_il = c_il;
    for (i = 0; i < INRUSH_CYC - 1; i = i + 1) meas(13'd4150, 10'd400);
    // ...and it ends with the current back under one unit load, so the
    // window closes cleanly.
    meas(13'd4900, 10'd80);
    tick;
    if (c_sag != base_sag || c_bud != base_bud || c_oc != base_oc
        || c_il != base_il) begin
      errors = errors + 1;
      $display("FAIL a legal in-rush produced reports");
    end
    if (m_ph != P_ON) begin
      errors = errors + 1;
      $display("FAIL phase after the window is %0d, expected P_ON", m_ph);
    end

    // ================= PHASE 3 -- in-rush that never finishes ============
    do_detach;
    do_attach;
    base_il = c_il;
    for (i = 0; i < INRUSH_CYC + 1; i = i + 1) meas(13'd4150, 10'd400);
    if (c_il != base_il + 1) begin
      errors = errors + 1;
      $display("FAIL in-rush overrun reports %0d, expected 1", c_il - base_il);
    end

    // ================= PHASE 4 -- the budget follows the ENUMERATION =====
    //
    // 300 mA is a violation before SET_CONFIGURATION and legal after it.
    // Identical measurement, opposite verdict -- the same shape of argument
    // as the window, with enumeration state instead of time.
    do_detach;
    do_attach;
    for (i = 0; i < INRUSH_CYC; i = i + 1) tick;
    base_bud = c_bud;
    meas(13'd5000, 10'd300);              // unconfigured: a violation
    if (c_bud != base_bud + 1) begin
      errors = errors + 1;
      $display("FAIL 300 mA before configuration was not reported");
    end
    do_configure;
    base_bud = c_bud;
    meas(13'd5000, 10'd300);              // configured: legal
    if (c_bud != base_bud) begin
      errors = errors + 1;
      $display("FAIL 300 mA after configuration WAS reported");
    end
    meas(13'd5000, 10'd600);              // over the configured maximum
    if (c_bud != base_bud + 1) begin
      errors = errors + 1;
      $display("FAIL 600 mA after configuration was not reported");
    end
    // ...and the boundary itself: exactly I_CFG_MA is legal, one more is not
    base_bud = c_bud;
    meas(13'd5000, 10'(I_CFG_MA));
    if (c_bud != base_bud) begin
      errors = errors + 1;
      $display("FAIL exactly I_CFG_MA was reported");
    end
    meas(13'd5000, 10'(I_CFG_MA) + 10'd1);
    if (c_bud != base_bud + 1) begin
      errors = errors + 1;
      $display("FAIL I_CFG_MA+1 was not reported");
    end

    // ================= PHASE 5 -- over-current, trips, oscillation =======
    //
    // Three trips. The first two are the hub working. The third is an
    // oscillation, reported ONCE.
    do_detach;
    base_oc = c_oc; base_osc = c_osc; base_trip = c_trip;
    for (i = 0; i < 6; i = i + 1) begin
      do_attach;
      meas(13'd5000, 10'd950);           // a dead short
      do_detach;
    end
    if (c_trip != base_trip + 6) begin
      errors = errors + 1;
      $display("FAIL trips %0d expected 6", c_trip - base_trip);
    end
    if (c_osc != base_osc + 1) begin
      errors = errors + 1;
      $display("FAIL oscillation reports %0d expected 1", c_osc - base_osc);
    end
    // ...and a tripped port stops being measurable until it is re-attached.
    do_attach;
    meas(13'd5000, 10'd950);
    base_sag = c_sag;
    meas(13'd4000, 10'd50);              // the port is off: no report
    if (c_sag != base_sag) begin
      errors = errors + 1;
      $display("FAIL a tripped port still reported a sag");
    end

    // ================= PHASE 6 -- the exhaustive situation sweep =========
    //
    // Every phase x (rail low or not) x current band. Phases are reached by
    // driving the design the way the design reaches them, never by forcing.
    for (p = 0; p < 4; p = p + 1)
      for (v = 0; v < 2; v = v + 1)
        for (b = 0; b < 4; b = b + 1) begin
          do_detach;
          if (p >= 1) begin
            do_attach;
            if (p >= 2) for (i = 0; i < INRUSH_CYC; i = i + 1) tick;
            if (p == 3) do_configure;
          end
          meas((v == 1) ? 13'd4100 : 13'd5000,
               (b == 0) ? 10'd50  : (b == 1) ? 10'd300 :
               (b == 2) ? 10'd700 : 10'd950);
        end

    // ================= PHASE 7 -- random =================================
    for (i = 0; i < 40000; i = i + 1) begin
      w = $unsigned($random) % 1000;
      if (w < 12)       do_attach;
      else if (w < 24)  do_detach;
      else if (w < 34)  do_configure;
      else if (w < 120) tick;
      else meas(13'd3800 + ($unsigned($random) % 1500),
                ($unsigned($random) % 1000));
    end

    // ================= the exhaustiveness proof ==========================
    n_reach = 0;
    for (k = 0; k < 32; k = k + 1) n_reach = n_reach + reach[k];
    if (n_reach != 32) begin
      errors = errors + 1;
      $display("FAIL situation reach %0d/32", n_reach);
      for (k = 0; k < 32; k = k + 1)
        if (!reach[k])
          $display("  unreached phase=%0d low=%0d band=%0d",
                   k / 8, (k / 4) % 2, k % 4);
    end

    $display("steps=%0d checks=%0d reach=%0d/32 errors=%0d",
             steps, checks, n_reach, errors);
    $display("window edge: silent=%0d reported=%0d",
             n_edge_silent, n_edge_reported);
    $display("samples=%0d attaches=%0d trips=%0d", n_sample, n_attach, n_trip);
    $display("sag=%0d inrush_long=%0d overcurrent=%0d budget=%0d osc=%0d",
             n_sag, n_inrush_long, n_overcurrent, n_budget, n_osc);
    $display("%0s: %0d errors in %0d checks",
             (errors == 0) ? "PASS" : "FAIL", errors, checks);
    $finish;
  end
endmodule

VHDL-2008 testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- Testbench for usb_power_monitor (VHDL-2008).
--
-- The oracle is a shadow model held in process variables and written from
-- the chapter's rules rather than from the RTL, re-derived every cycle and
-- compared against every output.
--
-- THE CENTRAL CLAIM IS ABOUT A BOUNDARY, SO THE BOUNDARY IS SWEPT
--
-- "In-rush is legal and sag is not, and the difference is a time window" is
-- a statement about ONE EDGE. Phase 1 drives an identical sagging
-- measurement at every offset from 0 to INRUSH_CYC + 2 cycles after attach
-- and requires that it is silent on one side of the edge and reported on the
-- other -- with the edge in exactly the right place.
--
-- A test that checks "inside the window" and "well outside the window"
-- passes against a window of any length at all.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.usb_power_pkg.all;

entity tb_pm_vhdl is
end entity;

architecture sim of tb_pm_vhdl is
  constant V_MIN_MV   : integer := 4400;
  constant I_UNCFG_MA : integer := 100;
  constant I_CFG_MA   : integer := 500;
  constant I_LIMIT_MA : integer := 900;
  constant INRUSH_CYC : integer := 100;
  constant OSC_MAX    : integer := 3;

  signal clk        : std_logic := '0';
  signal rst_n      : std_logic := '0';
  signal attach     : std_logic := '0';
  signal detach     : std_logic := '0';
  signal configured : std_logic := '0';
  signal sample     : std_logic := '0';
  signal eot        : std_logic := '0';
  signal vbus_mv    : unsigned(12 downto 0) := to_unsigned(5000, 13);
  signal i_ma       : unsigned(9 downto 0)  := (others => '0');

  signal phase_s       : std_logic_vector(1 downto 0);
  signal inrush_left_s : unsigned(15 downto 0);
  signal port_off_s    : std_logic;
  signal err_pulse_s   : std_logic;
  signal err_code_s    : std_logic_vector(2 downto 0);

  signal n_sample_s, n_attach_s, n_sag_s, n_il_s : unsigned(31 downto 0);
  signal n_oc_s, n_budget_s, n_osc_s, n_trip_s   : unsigned(31 downto 0);

  signal done : boolean := false;

  type int_array is array (natural range <>) of integer;
begin
  clk <= not clk after 5 ns when not done else '0';

  dut : entity work.usb_power_monitor
    generic map (V_MIN_MV => V_MIN_MV, I_UNCFG_MA => I_UNCFG_MA,
                 I_CFG_MA => I_CFG_MA, I_LIMIT_MA => I_LIMIT_MA,
                 INRUSH_CYC => INRUSH_CYC, OSC_MAX => OSC_MAX)
    port map (
      clk => clk, rst_n => rst_n,
      attach => attach, detach => detach, configured => configured,
      sample => sample, vbus_mv => vbus_mv, i_ma => i_ma, eot => eot,
      phase => phase_s, inrush_left => inrush_left_s,
      port_off => port_off_s, err_pulse => err_pulse_s,
      err_code => err_code_s,
      n_sample => n_sample_s, n_attach => n_attach_s, n_sag => n_sag_s,
      n_inrush_long => n_il_s, n_overcurrent => n_oc_s,
      n_budget => n_budget_s, n_osc => n_osc_s, n_trip => n_trip_s
    );

  stim : process
    -- ---------------- the shadow model ----------------
    variable m_ph    : std_logic_vector(1 downto 0) := P_OFF;
    variable m_win   : unsigned(15 downto 0) := (others => '0');
    variable m_ilast : unsigned(9 downto 0)  := (others => '0');
    variable m_trip  : unsigned(7 downto 0)  := (others => '0');
    variable m_off, m_osc, m_er : std_logic := '0';
    variable m_ec    : std_logic_vector(2 downto 0) := E_NONE;
    variable m_ph_pre : std_logic_vector(1 downto 0) := P_OFF;
    variable c_samp, c_att, c_sag, c_il : integer := 0;
    variable c_oc, c_bud, c_osc, c_trip : integer := 0;

    variable errors, checks, steps : integer := 0;
    variable reach   : int_array(0 to 31) := (others => 0);
    variable n_reach : integer := 0;
    variable base_sag, base_il, base_oc, base_bud : integer := 0;
    variable base_osc, base_trip : integer := 0;
    variable n_edge_silent, n_edge_reported : integer := 0;
    variable w_v : integer := 0;
    variable ln  : line;

    -- A deterministic LFSR, so a rerun reproduces exactly the same traffic.
    variable lfsr : unsigned(31 downto 0) := x"5EED1234";

    impure function rnd_nat return integer is
      variable u : unsigned(31 downto 0);
    begin
      lfsr := lfsr(30 downto 0) &
              (lfsr(31) xor lfsr(21) xor lfsr(1) xor lfsr(0));
      -- Only the low 30 bits: a full 32-bit unsigned does not fit in VHDL's
      -- INTEGER, and to_integer aborts the run rather than wrapping.
      return to_integer(lfsr(29 downto 0));
    end function;

    procedure ck (nm : string; got, exp : integer) is
    begin
      checks := checks + 1;
      if got /= exp then
        errors := errors + 1;
        if errors < 25 then
          write(ln, string'("FAIL step=") & integer'image(steps) & " " & nm
                    & " got=" & integer'image(got)
                    & " exp=" & integer'image(exp));
          writeline(output, ln);
        end if;
      end if;
    end procedure;

    function sl2i (s : std_logic) return integer is
    begin
      if s = '1' then return 1; else return 0; end if;
    end function;

    function band (i : unsigned(9 downto 0)) return integer is
    begin
      if    i > to_unsigned(I_LIMIT_MA, 10) then return 3;
      elsif i > to_unsigned(I_CFG_MA, 10)   then return 2;
      elsif i > to_unsigned(I_UNCFG_MA, 10) then return 1;
      else                                       return 0;
      end if;
    end function;

    procedure model_step is
      variable i_now : unsigned(9 downto 0);
    begin
      m_er := '0'; m_ec := E_NONE;
      -- The most recent measurement available at this instant: this cycle's
      -- if there is one, otherwise the last one taken.
      if sample = '1' and m_off = '0' then i_now := i_ma;
      else                                 i_now := m_ilast;
      end if;

      if eot = '1' then
        null;
      elsif detach = '1' then
        -- The trip count SURVIVES a detach. It is the only evidence that the
        -- port is cycling rather than failing once.
        m_ph  := P_OFF;
        m_win := (others => '0');
        m_off := '0';
      elsif attach = '1' then
        m_ph    := P_INRUSH;
        m_win   := to_unsigned(INRUSH_CYC, 16);
        m_off   := '0';
        m_ilast := (others => '0');
        c_att   := c_att + 1;
      else
        if configured = '1' and m_ph = P_ON then m_ph := P_CFG; end if;

        if m_ph = P_INRUSH then
          if m_win /= x"0000" then
            m_win := m_win - 1;
            if m_win = x"0000" then
              m_ph := P_ON;
              if i_now > to_unsigned(I_UNCFG_MA, 10) then
                m_er := '1'; m_ec := E_INRUSH_LONG;
                c_il := c_il + 1;
              end if;
            end if;
          end if;
        end if;

        if sample = '1' and m_off = '0' then
          c_samp  := c_samp + 1;
          m_ilast := i_ma;

          if i_ma > to_unsigned(I_LIMIT_MA, 10) then
            -- The port limit is hardware. It applies inside the window too:
            -- a dead short trips on the first microsecond.
            m_er   := '1'; m_ec := E_OVERCURRENT;
            c_oc   := c_oc + 1;
            m_off  := '1';
            m_ph   := P_OFF;
            m_win  := (others => '0');
            c_trip := c_trip + 1;
            if m_trip /= x"FF" then m_trip := m_trip + 1; end if;
            if (m_trip >= to_unsigned(OSC_MAX, 8)) and m_osc = '0' then
              m_ec  := E_OSC;
              c_osc := c_osc + 1;
              m_osc := '1';
            end if;
          elsif m_ph_pre = P_INRUSH then
            -- INSIDE THE WINDOW: NOTHING IS REPORTED.
            null;
          elsif vbus_mv < to_unsigned(V_MIN_MV, 13) then
            m_er  := '1'; m_ec := E_SAG;
            c_sag := c_sag + 1;
          elsif ((m_ph_pre = P_CFG) and (i_ma > to_unsigned(I_CFG_MA, 10)))
             or ((m_ph_pre /= P_CFG) and (i_ma > to_unsigned(I_UNCFG_MA, 10)))
          then
            m_er  := '1'; m_ec := E_BUDGET;
            c_bud := c_bud + 1;
          end if;
        end if;
      end if;
    end procedure;

    procedure check_out is
    begin
      ck("phase",       to_integer(unsigned(phase_s)),
                        to_integer(unsigned(m_ph)));
      ck("inrush_left", to_integer(inrush_left_s), to_integer(m_win));
      ck("port_off",    sl2i(port_off_s),  sl2i(m_off));
      ck("err_pulse",   sl2i(err_pulse_s), sl2i(m_er));
      ck("err_code",    to_integer(unsigned(err_code_s)),
                        to_integer(unsigned(m_ec)));
      ck("n_sample",      to_integer(n_sample_s), c_samp);
      ck("n_attach",      to_integer(n_attach_s), c_att);
      ck("n_sag",         to_integer(n_sag_s),    c_sag);
      ck("n_inrush_long", to_integer(n_il_s),     c_il);
      ck("n_overcurrent", to_integer(n_oc_s),     c_oc);
      ck("n_budget",      to_integer(n_budget_s), c_bud);
      ck("n_osc",         to_integer(n_osc_s),    c_osc);
      ck("n_trip",        to_integer(n_trip_s),   c_trip);
      -- ---- the structural invariant ----
      --
      -- Every error is exactly one of five causes, so the per-cause counters
      -- sum to the total. E_OSC is NOT added here: an oscillation is
      -- reported INSTEAD of the over-current that triggered it, on the same
      -- pulse, so counting both would double-count one event.
      ck("cause sum",
         to_integer(n_sag_s) + to_integer(n_il_s) + to_integer(n_oc_s)
         + to_integer(n_budget_s),
         c_sag + c_il + c_oc + c_bud);
    end procedure;

    procedure step is
      variable idx : integer;
    begin
      m_ph_pre := m_ph;
      if sample = '1' and eot = '0' and detach = '0' and attach = '0'
         and m_off = '0' then
        idx := to_integer(unsigned(m_ph)) * 8 + band(i_ma);
        if vbus_mv < to_unsigned(V_MIN_MV, 13) then idx := idx + 4; end if;
        reach(idx) := 1;
      end if;
      model_step;
      wait until rising_edge(clk);
      wait for 1 ns;
      steps := steps + 1;
      check_out;
    end procedure;

    procedure meas (v : integer; i : integer) is
    begin
      attach <= '0'; detach <= '0'; configured <= '0'; eot <= '0';
      sample <= '1';
      vbus_mv <= to_unsigned(v, 13);
      i_ma    <= to_unsigned(i, 10);
      wait for 0 ns;
      step;
    end procedure;

    procedure tick is
    begin
      attach <= '0'; detach <= '0'; configured <= '0'; eot <= '0';
      sample <= '0';
      wait for 0 ns;
      step;
    end procedure;

    procedure do_attach is
    begin
      attach <= '1'; detach <= '0'; configured <= '0'; sample <= '0';
      eot <= '0';
      wait for 0 ns;
      step;
      attach <= '0';
    end procedure;

    procedure do_detach is
    begin
      attach <= '0'; detach <= '1'; configured <= '0'; sample <= '0';
      eot <= '0';
      wait for 0 ns;
      step;
      detach <= '0';
    end procedure;

    procedure do_configure is
    begin
      attach <= '0'; detach <= '0'; configured <= '1'; sample <= '0';
      eot <= '0';
      wait for 0 ns;
      step;
      configured <= '0';
    end procedure;
  begin
    wait until rising_edge(clk);
    wait until rising_edge(clk);
    wait until rising_edge(clk);
    rst_n <= '1';
    wait for 1 ns;

    -- ================= PHASE 1 -- THE WINDOW EDGE, SWEPT =================
    --
    -- The same sagging measurement -- 4.1 V, 300 mA -- taken at every offset
    -- from 0 to INRUSH_CYC + 2 cycles after attach. Silent before the edge,
    -- reported after it, and the edge exactly where the parameter says.
    for t in 0 to INRUSH_CYC + 2 loop
      do_detach;
      do_attach;
      base_sag := c_sag; base_bud := c_bud;
      for i in 1 to t loop tick; end loop;
      meas(4100, 300);
      if t < INRUSH_CYC then
        if c_sag /= base_sag or c_bud /= base_bud then
          errors := errors + 1;
          write(ln, string'("FAIL reported INSIDE the window at t=")
                    & integer'image(t));
          writeline(output, ln);
        else
          n_edge_silent := n_edge_silent + 1;
        end if;
      else
        if c_sag = base_sag and c_bud = base_bud then
          errors := errors + 1;
          write(ln, string'("FAIL silent OUTSIDE the window at t=")
                    & integer'image(t));
          writeline(output, ln);
        else
          n_edge_reported := n_edge_reported + 1;
        end if;
      end if;
    end loop;
    if n_edge_silent /= INRUSH_CYC or n_edge_reported /= 3 then
      errors := errors + 1;
      write(ln, string'("FAIL edge sweep: silent=")
                & integer'image(n_edge_silent) & " reported="
                & integer'image(n_edge_reported));
      writeline(output, ln);
    end if;

    -- ================= PHASE 2 -- a legal attach is COMPLETELY silent ====
    --
    -- A sagging rail and 400 mA for the entire window. Zero reports. Not one
    -- at a lower severity -- zero.
    do_detach;
    do_attach;
    base_sag := c_sag; base_bud := c_bud; base_oc := c_oc; base_il := c_il;
    for i in 1 to INRUSH_CYC - 1 loop meas(4150, 400); end loop;
    -- ...and it ends with the current back under one unit load, so the
    -- window closes cleanly.
    meas(4900, 80);
    tick;
    if c_sag /= base_sag or c_bud /= base_bud or c_oc /= base_oc
       or c_il /= base_il then
      errors := errors + 1;
      write(ln, string'("FAIL a legal in-rush produced reports"));
      writeline(output, ln);
    end if;
    if m_ph /= P_ON then
      errors := errors + 1;
      write(ln, string'("FAIL phase after the window is not P_ON"));
      writeline(output, ln);
    end if;

    -- ================= PHASE 3 -- in-rush that never finishes ============
    do_detach;
    do_attach;
    base_il := c_il;
    for i in 1 to INRUSH_CYC + 1 loop meas(4150, 400); end loop;
    if c_il /= base_il + 1 then
      errors := errors + 1;
      write(ln, string'("FAIL in-rush overrun reports ")
                & integer'image(c_il - base_il) & " expected 1");
      writeline(output, ln);
    end if;

    -- ================= PHASE 4 -- the budget follows the ENUMERATION =====
    --
    -- 300 mA is a violation before SET_CONFIGURATION and legal after it.
    -- Identical measurement, opposite verdict -- the same shape of argument
    -- as the window, with enumeration state instead of time.
    do_detach;
    do_attach;
    for i in 1 to INRUSH_CYC loop tick; end loop;
    base_bud := c_bud;
    meas(5000, 300);                     -- unconfigured: a violation
    if c_bud /= base_bud + 1 then
      errors := errors + 1;
      write(ln, string'("FAIL 300 mA before configuration not reported"));
      writeline(output, ln);
    end if;
    do_configure;
    base_bud := c_bud;
    meas(5000, 300);                     -- configured: legal
    if c_bud /= base_bud then
      errors := errors + 1;
      write(ln, string'("FAIL 300 mA after configuration WAS reported"));
      writeline(output, ln);
    end if;
    meas(5000, 600);                     -- over the configured maximum
    if c_bud /= base_bud + 1 then
      errors := errors + 1;
      write(ln, string'("FAIL 600 mA after configuration not reported"));
      writeline(output, ln);
    end if;
    -- ...and the boundary itself: exactly I_CFG_MA is legal, one more is not
    base_bud := c_bud;
    meas(5000, I_CFG_MA);
    if c_bud /= base_bud then
      errors := errors + 1;
      write(ln, string'("FAIL exactly I_CFG_MA was reported"));
      writeline(output, ln);
    end if;
    meas(5000, I_CFG_MA + 1);
    if c_bud /= base_bud + 1 then
      errors := errors + 1;
      write(ln, string'("FAIL I_CFG_MA+1 was not reported"));
      writeline(output, ln);
    end if;

    -- ================= PHASE 5 -- over-current, trips, oscillation =======
    --
    -- Three trips. The first two are the hub working. The third is an
    -- oscillation, reported ONCE.
    do_detach;
    base_oc := c_oc; base_osc := c_osc; base_trip := c_trip;
    for i in 1 to 6 loop
      do_attach;
      meas(5000, 950);                   -- a dead short
      do_detach;
    end loop;
    if c_trip /= base_trip + 6 then
      errors := errors + 1;
      write(ln, string'("FAIL trips ") & integer'image(c_trip - base_trip)
                & " expected 6");
      writeline(output, ln);
    end if;
    if c_osc /= base_osc + 1 then
      errors := errors + 1;
      write(ln, string'("FAIL oscillation reports ")
                & integer'image(c_osc - base_osc) & " expected 1");
      writeline(output, ln);
    end if;
    -- ...and a tripped port stops being measurable until it is re-attached.
    do_attach;
    meas(5000, 950);
    base_sag := c_sag;
    meas(4000, 50);                      -- the port is off: no report
    if c_sag /= base_sag then
      errors := errors + 1;
      write(ln, string'("FAIL a tripped port still reported a sag"));
      writeline(output, ln);
    end if;

    -- ================= PHASE 6 -- the exhaustive situation sweep =========
    --
    -- Every phase x (rail low or not) x current band. Phases are reached by
    -- driving the design the way the design reaches them, never by forcing.
    for p in 0 to 3 loop
      for v in 0 to 1 loop
        for b in 0 to 3 loop
          do_detach;
          if p >= 1 then
            do_attach;
            if p >= 2 then
              for i in 1 to INRUSH_CYC loop tick; end loop;
            end if;
            if p = 3 then do_configure; end if;
          end if;
          if v = 1 then
            if    b = 0 then meas(4100, 50);
            elsif b = 1 then meas(4100, 300);
            elsif b = 2 then meas(4100, 700);
            else             meas(4100, 950);
            end if;
          else
            if    b = 0 then meas(5000, 50);
            elsif b = 1 then meas(5000, 300);
            elsif b = 2 then meas(5000, 700);
            else             meas(5000, 950);
            end if;
          end if;
        end loop;
      end loop;
    end loop;

    -- ================= PHASE 7 -- random =================================
    for i in 0 to 39999 loop
      w_v := rnd_nat mod 1000;
      if    w_v < 12  then do_attach;
      elsif w_v < 24  then do_detach;
      elsif w_v < 34  then do_configure;
      elsif w_v < 120 then tick;
      else
        meas(3800 + (rnd_nat mod 1500), rnd_nat mod 1000);
      end if;
    end loop;

    -- ================= the exhaustiveness proof ==========================
    n_reach := 0;
    for k in 0 to 31 loop n_reach := n_reach + reach(k); end loop;
    if n_reach /= 32 then
      errors := errors + 1;
      write(ln, string'("FAIL situation reach ") & integer'image(n_reach)
                & "/32");
      writeline(output, ln);
      for k in 0 to 31 loop
        if reach(k) = 0 then
          write(ln, string'("  unreached phase=") & integer'image(k / 8)
                    & " low=" & integer'image((k / 4) mod 2)
                    & " band=" & integer'image(k mod 4));
          writeline(output, ln);
        end if;
      end loop;
    end if;

    write(ln, string'("steps=") & integer'image(steps)
              & " checks=" & integer'image(checks)
              & " reach=" & integer'image(n_reach) & "/32"
              & " errors=" & integer'image(errors));
    writeline(output, ln);
    write(ln, string'("window edge: silent=") & integer'image(n_edge_silent)
              & " reported=" & integer'image(n_edge_reported));
    writeline(output, ln);
    write(ln, string'("samples=") & integer'image(to_integer(n_sample_s))
              & " attaches=" & integer'image(to_integer(n_attach_s))
              & " trips=" & integer'image(to_integer(n_trip_s)));
    writeline(output, ln);
    write(ln, string'("sag=") & integer'image(to_integer(n_sag_s))
              & " inrush_long=" & integer'image(to_integer(n_il_s))
              & " overcurrent=" & integer'image(to_integer(n_oc_s))
              & " budget=" & integer'image(to_integer(n_budget_s))
              & " osc=" & integer'image(to_integer(n_osc_s)));
    writeline(output, ln);
    if errors = 0 then
      write(ln, string'("PASS: 0 errors in ") & integer'image(checks)
                & " checks");
    else
      write(ln, string'("FAIL: ") & integer'image(errors) & " errors in " &
                integer'image(checks) & " checks");
    end if;
    writeline(output, ln);
    done <= true;
    wait;
  end process;
end architecture;

11. Exhaustive Verification

MeasureVerilogSystemVerilogVHDL
window offsets swept103 / 103103 / 103103 / 103
silent inside / reported outside100 / 3100 / 3100 / 3
phase × rail × current band32 / 3232 / 3232 / 32
Steps475944759447594
Checks executed666316666316666316
measurements evaluated743174317508
attaches595595709
port trips778778716
sags126512651823
in-rush overruns222
over-currents778778716
budget violations176417642384
oscillation reports111
ResultPASSPASSPASS

The row that carries the chapter is silent 100 / reported 3. The same measurement, driven 103 times at 103 different offsets from the attach, produced no report for exactly the first hundred and a report for exactly the last three — which pins the boundary at INRUSH_CYC rather than merely somewhere in the middle.

In-rush overruns = 2 in all three languages because it is driven exactly twice, in phase 3, and random measurements essentially never leave a device drawing above a unit load at precisely the cycle a window closes. It is the same structure as 25.3's wedge: a fault defined by a coincidence has to be constructed.

12. Mutation Testing

#MutationVerilogSysVerVHDL
T4the port limit is suppressed during in-rush330880330880317368
T2the window never expires179437179437178177
T6the window counts samples instead of cycles177529177529176599
T1the window is ignored: every attach reports a sag143187143187141185
T3the budget stops depending on the enumeration state852268522685644
T7the window-close check is dropped846948469484694
T5the trip count is cleared on detach224222422041
—unmutated baseline000

All seven die in all three languages.

T4 is the largest, and it is the one that would survive a code review. "In-rush current is high, so do not check current during in-rush" is a sentence that sounds like the whole point of the chapter. It is the opposite: the window excuses a device from the budget, which is policy, not from the port limit, which is a protection circuit. A dead short does not become acceptable by being early.

T1 and T2 are the two failure modes from section 2, and they score within 25% of each other. One produces a false alarm on every attach; the other makes a real short invisible for ever. Both are one comparison.

T5 is the outlier at 2242, and the reason is that its opportunity is rare by construction. Clearing the trip count on detach only matters when trips are separated by detaches, which is phase 5 and almost nothing else — six attach/trip/detach cycles out of a 47,594-step run.

Directed against random

#All phasesDirected onlyRandom
T114318717465125722
T217943716039163398
T385226345481772
T43308807359323521
T522421711531
T617752915822161707
T784694469480000

Every mutation is killed by directed stimulus alone. T5 is the one case in this module where the directed contribution is the larger half — 1711 against 531 — which is exactly what you would expect of a fault whose signature is a pattern across attach cycles: phase 5 constructs six of them deliberately, and 40,000 random measurements produce a handful by accident.

13. Two Defects the Shadow Model Found, Both About One Sample

Neither of these is a mutation. Both were real defects in the first version of this block, and both were found by the same thing: a shadow model that predicts every output every cycle, rather than checking the outputs that seemed interesting.

The first was a denominator. The design counted a measurement as a sample whenever sample was asserted. The model counted one only when the port was actually on. They disagreed by however many measurements arrived while a tripped port was off:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   n_sample as written    every measurement OFFERED
   n_sample as meant      every measurement ACTED ON

A tripped port has no voltage on it, so a measurement taken while it is off is not a measurement of anything — and counting it inflates the denominator of every rate computed from this block by exactly the time the port spent off, which is longest precisely when things are going worst. The design now counts what it acts on, and the comment says so.

The second was an off-by-one sample, and it is the more interesting one.

The window-close check asks is the device still drawing at the moment the window expires. The first version read ilast_r — the last registered measurement — because the window decrements before the current sample is stored. So a device that dropped back under one unit load on the very last sample of its window was reported as an in-rush overrun, on the strength of a reading from one sample earlier.

14. Debugging Walkthrough: The Device That Only Fails on Thursdays

The report. A handheld scanner in a warehouse disconnects "a few times a week, usually Thursday afternoons". It works flawlessly on every bench. The USB monitoring on the host reports over-current shutoffs.

Step 1 — over-current is the hub working, so what is drawing the current? Nothing on the scanner should exceed 500 mA. Instrument the port. Under normal use the scanner draws 380 mA steady.

Step 2 — look at the in-rush instead. Attach draws 840 mA for about 12 ms, then settles. The port limit is 900 mA. The margin is 60 mA.

Step 3 — so what varies? Temperature. The warehouse's cold aisle is 4°C and the loading dock is 30°C, and the scanner's bulk capacitance is a ceramic type with a substantial temperature coefficient. Warm, its capacitance is lower and the in-rush is shorter; cold, it is higher and the in-rush is both longer and larger.

Step 4 — and Thursday? Thursday is the delivery day. Scanners are taken from the cold aisle to the dock and plugged into the dock's hub. Cold device, and a hub with a lower port limit than the host.

Step 5 — why did the bench never show it? Because the bench is at room temperature and the machine's own ports have a 1.5 A limit. Every element of the failure is environmental, and the device is within specification at every one of them individually.

The fix. A smaller bulk capacitor and a series resistor to bound the in-rush. The device had always been marginal; Thursday was just when three tolerances lined up.

15. UVM: Power as an Environment-Level Check

Power is the one thing in this module that is not a property of a transaction. It is a property of the port, across transactions, and it has a time axis that the transaction stream does not carry.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// A power sample is not a bus transaction. It has no endpoint, no PID and no
// direction -- it is an observation of a rail, taken on a clock, and the
// only thing that makes it meaningful is WHEN it was taken relative to an
// attach. So the item carries a timestamp, and the component that consumes
// it holds the attach time.
class usb_power_item extends uvm_sequence_item;
  `uvm_object_utils(usb_power_item)

  rand int unsigned vbus_mv;
  rand int unsigned i_ma;
  time              stamp;        // NOT randomised: the monitor fills it in

  function new(string name = "usb_power_item"); super.new(name); endfunction

  // A realistic rail. Note the in-rush values are IN the distribution:
  // constraining them away produces a generator that never exercises the
  // one window this whole component exists to implement.
  constraint c_rail {
    vbus_mv inside {[4050:5250]};
    i_ma    inside {[0:950]};
  }
endclass


// ---------------------------------------------------------------------
// The power checker. A component, not a scoreboard: it has a LIFETIME and
// the checks depend on where in that lifetime a sample arrives.
// ---------------------------------------------------------------------
class usb_power_checker extends uvm_subscriber #(usb_power_item);
  `uvm_component_utils(usb_power_checker)

  int  V_MIN_MV   = 4400;
  int  I_UNCFG_MA = 100;
  int  I_CFG_MA   = 500;
  int  I_LIMIT_MA = 900;
  time INRUSH_WIN = 100us;      // a DURATION, not a sample count

  time attach_at  = 0;
  bit  attached   = 0;
  bit  configured = 0;
  int unsigned trips, n_sag, n_budget, n_overcurrent;
  int unsigned peak_inrush_ma;
  time         inrush_ended;

  function new(string name, uvm_component parent); super.new(name, parent);
  endfunction

  // Called by the enumeration agent, not by the power monitor. The two
  // streams have to meet somewhere and this is the place.
  function void note_attach();
    attach_at = $time; attached = 1; configured = 0;
    peak_inrush_ma = 0; inrush_ended = 0;
  endfunction
  function void note_configured(); configured = 1; endfunction
  function void note_detach();     attached = 0;   endfunction

  function void write(usb_power_item t);
    bit in_window;
    if (!attached) return;
    in_window = (t.stamp - attach_at) < INRUSH_WIN;

    // ---- The port limit is NOT gated by the window. ----
    //
    // The window excuses a device from the budget, which is policy. It does
    // not excuse it from a protection circuit, and a dead short trips on
    // the first microsecond.
    if (t.i_ma > I_LIMIT_MA) begin
      `uvm_error("PWR/OVERCURRENT",
        $sformatf("%0d mA exceeds the %0d mA port limit at t+%0t",
                  t.i_ma, I_LIMIT_MA, t.stamp - attach_at))
      n_overcurrent++;
      trips++;
      attached = 0;         // the port is off; nothing to measure
      return;
    end

    if (in_window) begin
      // ---- Inside the window: nothing is reported. ----
      //
      // Not at a lower severity, not behind a config knob. A sagging rail
      // and a large current here are what a correct device looks like, and
      // reporting them is one false alarm per plug-in event.
      if (t.i_ma > peak_inrush_ma) peak_inrush_ma = t.i_ma;
      return;
    end

    if (inrush_ended == 0) begin
      inrush_ended = t.stamp;
      // The measurement worth keeping is the peak AND the duration. Neither
      // alone is a decision: 840 mA against a 900 mA limit is "under the
      // limit" and has no margin at all.
      `uvm_info("PWR/INRUSH",
        $sformatf("in-rush peaked at %0d mA (%0d%% of the port limit) over %0t",
                  peak_inrush_ma, (100 * peak_inrush_ma) / I_LIMIT_MA,
                  INRUSH_WIN), UVM_LOW)
      if (peak_inrush_ma * 100 > I_LIMIT_MA * 85)
        `uvm_warning("PWR/INRUSH_MARGIN",
          $sformatf("in-rush is within %0d%% of the port limit -- that is not margin once temperature and supply tolerance are included",
                    100 - (100 * peak_inrush_ma) / I_LIMIT_MA))
    end

    if (t.vbus_mv < V_MIN_MV) begin
      `uvm_error("PWR/SAG",
        $sformatf("VBUS %0d mV is below the %0d mV floor", t.vbus_mv, V_MIN_MV))
      n_sag++;
    end else if (t.i_ma > (configured ? I_CFG_MA : I_UNCFG_MA)) begin
      // ---- The budget follows the ENUMERATION state. ----
      //
      // The unconfigured case is the one that matters: it works on every
      // desktop and fails on a bus-powered hub.
      `uvm_error("PWR/BUDGET",
        $sformatf("%0d mA exceeds the %0d mA budget (%sconfigured)",
                  t.i_ma, configured ? I_CFG_MA : I_UNCFG_MA,
                  configured ? "" : "un"))
      n_budget++;
    end
  endfunction

  // ---- The check that needs the whole run. ----
  //
  // A single over-current is the hub working. Several, separated by
  // detaches, is something latching up and recovering -- and the trip count
  // deliberately survives note_detach(), because an oscillation is a
  // pattern ACROSS attach cycles rather than within one.
  function void report_phase(uvm_phase phase);
    super.report_phase(phase);
    if (trips > 2)
      `uvm_error("PWR/OSCILLATION",
        $sformatf("the port tripped %0d times: something downstream is latching up and recovering",
                  trips))
    `uvm_info("PWR",
      $sformatf("trips=%0d sag=%0d budget=%0d overcurrent=%0d",
                trips, n_sag, n_budget, n_overcurrent), UVM_LOW)
  endfunction
endclass

16. Common Misconceptions

"A voltage dip on attach is a fault." It is what attaching a device looks like. The rail is charging a capacitor.

"The in-rush limit is a current limit." It is a capacitance limit, which is a limit on how long the current lasts. That is the checkable part.

"Suppress the current check during in-rush." Suppress the budget check. The port limit is a protection circuit and a dead short is not excused by being early.

"Under the limit means it passed." 840 mA against 900 mA is under the limit and has no margin once temperature and tolerance are included.

"A device can draw 500 mA as soon as it is plugged in." One unit load until SET_CONFIGURATION. It works on every desktop and fails on a bus-powered hub.

"An over-current shutoff is a device fault." It is the hub working. Repeated shutoffs separated by re-enables are the fault.

"Clear the counters on detach." Clear the phase and the window. Keep the trip count, or the oscillation becomes invisible.

"The window can be measured in samples." Then its length depends on the instrument, and the same device passes on one analyser and fails on another.

"Checking inside and outside the window tests the window." It passes against a window of any length, including one that never closes.

17. Exercises

1. A device presents 12 µF of bulk capacitance where the specification allows 10. Work out what changes about its in-rush and which of the five reports it produces, if any.

2. The window sweep produced "silent 100, reported 3". Derive both numbers from INRUSH_CYC and the sweep bounds, and say what a result of "silent 101, reported 2" would mean.

3. T5 is the only mutation in this module whose directed contribution exceeds its random one. Explain that in terms of what its fault signature is, without referring to the scores.

4. The design suppresses the sag check inside the window but not the port-limit check. Construct the failure that would be missed if the port-limit check were suppressed too, and estimate how long it would go unreported.

5. E_INRUSH_LONG fires at most once per attach, using the most recent measurement. Show that using the previous measurement instead moves the boundary by one sample, and construct the stimulus that distinguishes them.

6. Add a second port to the monitor. Which pieces of state must be per-port and which may be shared, and what is the 25.3 argument for each?

7. The UVM component holds INRUSH_WIN as a time. Write the version that counts samples, then write the test that distinguishes the two — and say why no test written against a single sample rate can.

18. Summary

IdeaWhy it matters
Identical readings, opposite verdictsthe axis is time since attach, not the measurement
In-rush is legalthe rail is charging a capacitor, and that is what attaching looks like
The spec bounds capacitance, not currentbecause a duration is the checkable thing
No window → a false alarm every attachwhich is how a checker gets switched off
Window too long → a short is invisiblefor exactly as long as the damage takes
The window gates policy, not protectionthe port limit applies in every state
The budget follows the enumeration state100 mA until SET_CONFIGURATION, and it works on every desktop
Over-current is the hub workingthe oscillation is the fault, and it is reported once
The trip count survives a detachor a pattern across attach cycles is invisible
Count the window in cycles, not samplesor its length depends on the instrument
Peak and duration, against the worst port"under the limit" is not margin
A phase measuring an accumulation needs a known accumulatorphases 4 and 5 observed nothing until they reset
103 offsets swept, 32/32 situations7 mutations, all killed in 3 languages, all by directed stimulus

Tooling

StepCommand
Verilog-2005iverilog -g2005 -o pm_v.out pm_v.v pm_v_tb.v && ./pm_v.out
SystemVerilogiverilog -g2012 -o pm_sv.out pm_sv.sv pm_sv_tb.sv && ./pm_sv.out
VHDL-2008 analysenvc --std=2008 -a pm_vhdl.vhd pm_vhdl_tb.vhd
VHDL-2008 elaboratenvc --std=2008 -e tb_pm_vhdl
VHDL-2008 runnvc --std=2008 -r tb_pm_vhdl
One mutationiverilog -g2005 -DMUT_T4 -o mm pm_v_mut.v pm_v_tb.v && ./mm
Directed onlyiverilog -g2005 -DDIRECTED_ONLY -o mm pm_v_mut.v pm_v_tb.v && ./mm

All three implementations pass with 0 errors: the in-rush boundary swept at all 103 offsets with the edge landing exactly where the parameter says, all 32 phase × rail × current situations reached on the wire, 666316 checks against an independently written shadow model, and every one of the seven mutations killed by directed stimulus alone.


Chapter 25.7 — Using a Protocol Analyser closes the module with the instrument the last six chapters have been implicitly describing. Its central problem is one this chapter has already met in miniature: a trigger fires on the symptom, and the symptom is the end of the story. The cause is always before the trigger, which is the part a capture that starts at the trigger does not contain.

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.