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
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 does | What happens |
|---|---|
| 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 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
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:
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:
| Code | Fires when | Points at |
|---|---|---|
E_SAG | VBUS below the floor outside the window | the rail, or something loading it |
E_INRUSH_LONG | still drawing when the window expired | oversized bulk capacitance, or a soft short |
E_OVERCURRENT | past the port limit, in any state | a short, or a device that is on fire |
E_BUDGET | past the budget for the current enumeration state | device firmware |
E_OSC | OSC_MAX trips | something 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
6. Verilog-2005 Implementation
// 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
endmodule7. SystemVerilog Implementation
// 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
endmodule8. VHDL-2008 Implementation
-- 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 cyclesAnd 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 cycles10. 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:
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 3Seven phases:
| Phase | What it establishes |
|---|---|
| 1 | the window edge, swept at every offset from 0 to INRUSH_CYC + 2 |
| 2 | a legal attach — sagging rail, 400 mA, the whole window — reports nothing |
| 3 | an in-rush that has not finished when the window closes is reported once |
| 4 | the budget follows the enumeration state, and its boundary is exact |
| 5 | six trips produce six trips and one oscillation report |
| 6 | every phase × rail-low × current band — 32 situations, reached on the wire |
| 7 | 40000 random measurements |
Verilog-2005 testbench
`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
endmoduleSystemVerilog testbench
`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
endmoduleVHDL-2008 testbench
-- 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
| Measure | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| window offsets swept | 103 / 103 | 103 / 103 | 103 / 103 |
| silent inside / reported outside | 100 / 3 | 100 / 3 | 100 / 3 |
| phase × rail × current band | 32 / 32 | 32 / 32 | 32 / 32 |
| Steps | 47594 | 47594 | 47594 |
| Checks executed | 666316 | 666316 | 666316 |
| measurements evaluated | 7431 | 7431 | 7508 |
| attaches | 595 | 595 | 709 |
| port trips | 778 | 778 | 716 |
| sags | 1265 | 1265 | 1823 |
| in-rush overruns | 2 | 2 | 2 |
| over-currents | 778 | 778 | 716 |
| budget violations | 1764 | 1764 | 2384 |
| oscillation reports | 1 | 1 | 1 |
| Result | PASS | PASS | PASS |
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
| # | Mutation | Verilog | SysVer | VHDL |
|---|---|---|---|---|
| T4 | the port limit is suppressed during in-rush | 330880 | 330880 | 317368 |
| T2 | the window never expires | 179437 | 179437 | 178177 |
| T6 | the window counts samples instead of cycles | 177529 | 177529 | 176599 |
| T1 | the window is ignored: every attach reports a sag | 143187 | 143187 | 141185 |
| T3 | the budget stops depending on the enumeration state | 85226 | 85226 | 85644 |
| T7 | the window-close check is dropped | 84694 | 84694 | 84694 |
| T5 | the trip count is cleared on detach | 2242 | 2242 | 2041 |
| — | unmutated baseline | 0 | 0 | 0 |
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 phases | Directed only | Random |
|---|---|---|---|
| T1 | 143187 | 17465 | 125722 |
| T2 | 179437 | 16039 | 163398 |
| T3 | 85226 | 3454 | 81772 |
| T4 | 330880 | 7359 | 323521 |
| T5 | 2242 | 1711 | 531 |
| T6 | 177529 | 15822 | 161707 |
| T7 | 84694 | 4694 | 80000 |
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:
n_sample as written every measurement OFFERED
n_sample as meant every measurement ACTED ONA 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.
// 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
endclass16. 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
| Idea | Why it matters |
|---|---|
| Identical readings, opposite verdicts | the axis is time since attach, not the measurement |
| In-rush is legal | the rail is charging a capacitor, and that is what attaching looks like |
| The spec bounds capacitance, not current | because a duration is the checkable thing |
| No window → a false alarm every attach | which is how a checker gets switched off |
| Window too long → a short is invisible | for exactly as long as the damage takes |
| The window gates policy, not protection | the port limit applies in every state |
| The budget follows the enumeration state | 100 mA until SET_CONFIGURATION, and it works on every desktop |
| Over-current is the hub working | the oscillation is the fault, and it is reported once |
| The trip count survives a detach | or a pattern across attach cycles is invisible |
| Count the window in cycles, not samples | or 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 accumulator | phases 4 and 5 observed nothing until they reset |
| 103 offsets swept, 32/32 situations | 7 mutations, all killed in 3 languages, all by directed stimulus |
Tooling
| Step | Command |
|---|---|
| Verilog-2005 | iverilog -g2005 -o pm_v.out pm_v.v pm_v_tb.v && ./pm_v.out |
| SystemVerilog | iverilog -g2012 -o pm_sv.out pm_sv.sv pm_sv_tb.sv && ./pm_sv.out |
| VHDL-2008 analyse | nvc --std=2008 -a pm_vhdl.vhd pm_vhdl_tb.vhd |
| VHDL-2008 elaborate | nvc --std=2008 -e tb_pm_vhdl |
| VHDL-2008 run | nvc --std=2008 -r tb_pm_vhdl |
| One mutation | iverilog -g2005 -DMUT_T4 -o mm pm_v_mut.v pm_v_tb.v && ./mm |
| Directed only | iverilog -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
Related tutorials
- Related topic
Hub Power Management
A bus-powered hub gets 500 mA and must supply four ports that could each want 500 mA — so its ports are offered one unit load, and a device needing more is refused.
- Related topic
Bus Power
A device's current allowance changes exactly once during enumeration — and bMaxPower is counted in 2 mA units, not milliamps.
- Related topic
USB 3.x vs USB 2.0 Differences
A SuperSpeed-capable device behind a USB 2 hub is a USB 2 device: capability is a property of the link that trained, never of the descriptor the device published.
- Related topic
Enumeration Failures
A failing enumeration retries from the beginning, so the current step is always ATTACH and tells you nothing — the furthest step ever reached is the diagnosis, and a bus reset must not clear it.
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.
