USB · Module 18
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.
Chapter 18.2 gave every port a SetPortFeature(PORT_POWER) command and an over-current input, and treated both as inputs to a state machine.
They are not inputs. They are a resource with a budget, and the budget does not balance.
1. The Arithmetic That Does Not Work
Write it out:
a bus-powered hub may draw 500 mA from its upstream port
it must run its own controller from that same 500 mA
it must then supply 4 ports
at a full load each that would need 2000 mAA bus-powered hub is given 500 mA and asked to supply what could be 2000 mA. There is no clever allocation that fixes this; the numbers are simply not there.
So the specification changes what a port is offered. A bus-powered hub does not offer a full load. It offers one unit load — 100 mA — to each port, and a device that needs more is refused.
Linux writes exactly this, with the specification reference attached to the line:
remaining = hdev->bus_mA - hub->descriptor->bHubContrCurrent;
hub->mA_per_port = unit_load; /* 7.2.1 */2. Unit Loads and Full Loads
The numbers are fixed by the specification, not derived from what happens to be spare:
| unit load | full load | |
|---|---|---|
| USB 2.0 | 100 mA | 500 mA (five unit loads) |
| SuperSpeed | 150 mA | 900 mA (six unit loads) |
A device declares what it needs in its configuration descriptor and the host decides whether the port can supply it. A device asking for more than the port offers is not configured — it enumerates, it is visible, and it does not work.
Nothing is negotiated. The hub does not offer 300 mA because it happens to have 300 mA spare; it offers a unit load or a full load, and the choice between those two is made by how the hub itself is powered.
3. What the Hub Subtracts First
A bus-powered hub's own controller has to run on something, and that something is the same 500 mA.
bHubContrCurrent is the hub's declaration of its own consumption, and it comes out of the budget before any port sees a milliamp:
available = upstream_mA − bHubContrCurrentThe subtraction must saturate. A hub whose descriptor claims a controller current larger than its upstream allowance is malformed — and the wrapped answer is the single worst response available: a 16-bit wrap turns a deficit into an enormous budget, and the hub would grant every port and brown out the whole tree.
Mutation P2 removes the saturation and dies 4076 times. It is not a subtle bug: it converts "I have nothing" into "I have 65 000 mA".
4. Insufficient Is a Warning, Not a Refusal
When the remaining budget cannot cover every port at a unit load each, Linux logs:
"insufficient power available to use all downstream ports"Note what it does not do: refuse to work. The hub enumerates, the ports function, and devices attach — right up until the budget runs out, at which point the next port gets nothing.
So insufficient is a status output, not a gate. It says this hub cannot power every port simultaneously, which is useful to a user deciding where to plug something in, and is not a reason to disable the hub.
5. Three Refusals That Are Not the Same Refusal
A port can be refused power for three entirely independent reasons, and conflating them is the commonest bug in this block:
| Reason | Condition | What it means |
|---|---|---|
| over-current | the port is drawing too much | it gets nothing, whatever it asked for |
| per-port offer | request > mA_per_port | §1's unit-load rule — the budget may be untouched |
| budget | request > what is left | the hub has run out, though the request was legal |
The second and third come apart constantly. A 500 mA device on a bus-powered hub with a completely empty budget is still refused — not because the hub is short of power, but because no port on a bus-powered hub is ever offered more than one unit load.
And the first outranks both. An over-current port gets nothing even if it asks for 0 mA and the budget is full, because over-current is a fault condition and not a request. Mutation P4 removes that precedence and dies 115 365 times — the largest count in Module 18.
6. The Budget, Drawn
The dashed edge is the whole difference between the two kinds of hub. A self-powered hub does not compute a smaller number; it takes a different path.
7. The Hardware, Before Any Language
mA_per_port is a constant chosen by the power source, not arithmetic. A unit load or a full load — there is nothing here to get wrong, and mutation P1 gets it wrong anyway by offering a full load unconditionally (83 784 errors).
The budget subtraction saturates (§3).
Allocation runs in port order, and the order is fixed rather than a policy. The host — not the hub — decides which devices matter, and a hub that reordered would make the host's decisions non-reproducible: the same set of devices would get different answers depending on hub-internal state the host cannot see.
The running sum is one bit wider than the budget, so it cannot wrap mid-loop.
Three independent refusal conditions, in the order of §5.
Everything is combinational except one sticky observability flag. A power budget is a question about now, and there is no state to carry: the same inputs must always give the same answer, which is what makes the 40 960-point sweep meaningful.
8. Verilog-2005
// usb_hub_power_budget -- who gets how much current, and who is refused.
//
// Chapter 18.2 gave every port a PORT_POWER command and an over-current
// input and treated both as inputs to a state machine. They are not inputs;
// they are a resource with a budget, and the budget does not balance.
//
// The arithmetic that does not work:
//
// a bus-powered hub may draw 500 mA from its upstream port
// it must run its own controller from that same 500 mA
// it must then supply 4 downstream ports
// at a full load each that would need 2000 mA
//
// The resolution is that a bus-powered hub does NOT offer a full load. It
// offers ONE UNIT LOAD -- 100 mA -- to each port, and a device needing more
// is refused. Linux writes exactly this, with a specification reference
// attached to the line:
//
// remaining = hdev->bus_mA - hub->descriptor->bHubContrCurrent;
// hub->mA_per_port = unit_load; /* 7.2.1 */
//
// A SELF-powered hub has its own supply and offers a full load per port.
// This is the entire practical difference between the two kinds of hub, and
// it is why a bus-powered hub cannot run a bus-powered hard drive.
module usb_hub_power_budget #(
parameter integer NPORTS = 4,
parameter integer UNIT_LOAD = 100, // one unit load, mA (USB 2.0)
parameter integer FULL_LOAD = 500, // five unit loads, mA
parameter integer SELF_SUPPLY = 2500 // what a self-powered hub can source
) (
input wire clk,
input wire rst_n,
input wire self_powered,
input wire [15:0] upstream_mA, // what the hub may draw
input wire [15:0] contr_current, // bHubContrCurrent
input wire [NPORTS*16-1:0] req_mA_flat, // per-port request
input wire [NPORTS-1:0] req_valid,
input wire [NPORTS-1:0] overcurrent,
output wire [15:0] mA_per_port, // what a port is OFFERED
output wire [15:0] available, // what the hub can source
output wire limited_power, // ports get less than a full load
output reg [NPORTS-1:0] grant,
output reg [15:0] total_granted,
output wire insufficient, // cannot supply every port
output reg ever_insufficient
);
// A self-powered hub offers a FULL load; a bus-powered hub offers ONE
// unit load. There is no arithmetic here to get wrong -- the number is
// fixed by the specification, not derived from what happens to be spare.
assign mA_per_port = self_powered ? FULL_LOAD[15:0] : UNIT_LOAD[15:0];
assign limited_power = !self_powered;
// SATURATING subtraction. A hub whose controller is declared to draw more
// than its upstream port supplies is a malformed descriptor, and the
// wrapped answer -- an enormous budget -- is the single worst response
// available: it would grant every port and brown out the whole tree.
wire [15:0] bus_avail = (upstream_mA > contr_current)
? (upstream_mA - contr_current) : 16'd0;
assign available = self_powered ? SELF_SUPPLY[15:0] : bus_avail;
// Linux warns "insufficient power available to use all downstream ports"
// on exactly this condition. It is a WARNING, not a refusal: the hub
// still works, it simply cannot power every port at once.
assign insufficient = (available < (NPORTS * mA_per_port));
// Allocation, in port order. A fixed order rather than a policy, because
// the host -- not the hub -- decides which devices matter, and a hub that
// reordered would make the host's decisions non-reproducible.
integer i;
reg [15:0] req_i;
reg [16:0] running; // 17 bits: the sum must not wrap mid-loop
always @* begin
grant = {NPORTS{1'b0}};
running = 17'd0;
for (i = 0; i < NPORTS; i = i + 1) begin
req_i = req_mA_flat[i*16 +: 16];
// Three independent reasons to refuse, and they are not the same:
// * the port is in over-current -- it gets nothing, whatever it asked
// * the request exceeds what a port is OFFERED (the unit-load rule)
// * the request exceeds what the hub has LEFT (the budget)
if (req_valid[i] && !overcurrent[i]
&& (req_i <= mA_per_port)
&& ((running + {1'b0, req_i}) <= {1'b0, available})) begin
grant[i] = 1'b1;
running = running + {1'b0, req_i};
end
end
total_granted = running[15:0];
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) ever_insufficient <= 1'b0;
else if (insufficient) ever_insufficient <= 1'b1;
end
endmodulerunning is 17 bits against a 16-bit budget. Four ports at a full load each is 2000 mA, which fits 16 bits comfortably — so the extra bit is not needed for this parameterisation. It is needed for the general one, and a width that is correct only for the default generic is a latent bug with a configuration attached to it.
9. SystemVerilog
package usb_power_pkg;
// Where a hub's downstream current comes from. This is not a status bit:
// it decides what every port is OFFERED, and those two answers differ by
// a factor of five.
typedef enum logic {
PWR_BUS, // drawn from the upstream port -- one unit load per port
PWR_SELF // the hub has its own supply -- a full load per port
} power_src_e;
endpackage
// usb_hub_power_budget_sv -- who gets how much current, and who is refused.
//
// Chapter 18.2 gave every port a PORT_POWER command and an over-current
// input and treated both as inputs to a state machine. They are not inputs;
// they are a resource with a budget, and the budget does not balance.
//
// The arithmetic that does not work:
//
// a bus-powered hub may draw 500 mA from its upstream port
// it must run its own controller from that same 500 mA
// it must then supply 4 downstream ports
// at a full load each that would need 2000 mA
//
// The resolution is that a bus-powered hub does NOT offer a full load. It
// offers ONE UNIT LOAD -- 100 mA -- to each port, and a device needing more
// is refused. Linux writes exactly this, with a specification reference
// attached to the line:
//
// remaining = hdev->bus_mA - hub->descriptor->bHubContrCurrent;
// hub->mA_per_port = unit_load; /* 7.2.1 */
module usb_hub_power_budget_sv
import usb_power_pkg::*;
#(
parameter int unsigned NPORTS = 4,
parameter int unsigned UNIT_LOAD = 100,
parameter int unsigned FULL_LOAD = 500,
parameter int unsigned SELF_SUPPLY = 2500
) (
input logic clk,
input logic rst_n,
input power_src_e power_src,
input logic [15:0] upstream_mA,
input logic [15:0] contr_current,
input logic [NPORTS*16-1:0] req_mA_flat,
input logic [NPORTS-1:0] req_valid,
input logic [NPORTS-1:0] overcurrent,
output logic [15:0] mA_per_port,
output logic [15:0] available,
output logic limited_power,
output logic [NPORTS-1:0] grant,
output logic [15:0] total_granted,
output logic insufficient,
output logic ever_insufficient
);
initial begin
if (UNIT_LOAD > FULL_LOAD)
$fatal(1, "UNIT_LOAD=%0d exceeds FULL_LOAD=%0d: a unit load is a fraction of a full one",
UNIT_LOAD, FULL_LOAD);
if (NPORTS < 1)
$fatal(1, "NPORTS=%0d: a hub with no downstream ports is not a hub",
NPORTS);
end
wire self_powered = (power_src == PWR_SELF);
// A self-powered hub offers a FULL load; a bus-powered hub offers ONE
// unit load. There is no arithmetic here to get wrong -- the number is
// fixed by the specification, not derived from what happens to be spare.
assign mA_per_port = self_powered ? 16'(FULL_LOAD) : 16'(UNIT_LOAD);
assign limited_power = !self_powered;
// SATURATING subtraction. A hub whose controller is declared to draw more
// than its upstream port supplies is a malformed descriptor, and the
// wrapped answer -- an enormous budget -- is the single worst response
// available: it would grant every port and brown out the whole tree.
wire [15:0] bus_avail = (upstream_mA > contr_current)
? (upstream_mA - contr_current) : 16'd0;
assign available = self_powered ? 16'(SELF_SUPPLY) : bus_avail;
// Linux warns "insufficient power available to use all downstream ports"
// on exactly this condition. It is a WARNING, not a refusal: the hub
// still works, it simply cannot power every port at once.
assign insufficient = (available < 16'(NPORTS * mA_per_port));
// Declared at module scope rather than inside the process: `running` is
// 17 bits so the running sum cannot wrap mid-loop, and its low half is
// the reported total.
logic [15:0] req_i;
logic [16:0] running;
// Allocation, in port order. A fixed order rather than a policy, because
// the host -- not the hub -- decides which devices matter, and a hub that
// reordered would make the host's decisions non-reproducible.
always_comb begin
grant = '0;
running = '0;
for (int i = 0; i < int'(NPORTS); i++) begin
req_i = req_mA_flat[i*16 +: 16];
// Three independent reasons to refuse, and they are not the same:
// * the port is in over-current -- it gets nothing, whatever it asked
// * the request exceeds what a port is OFFERED (the unit-load rule)
// * the request exceeds what the hub has LEFT (the budget)
if (req_valid[i] && !overcurrent[i]
&& (req_i <= mA_per_port)
&& ((running + 17'(req_i)) <= 17'(available))) begin
grant[i] = 1'b1;
running = running + 17'(req_i);
end
end
end
// Continuous, so the 17-bit running sum is narrowed in one place and the
// process has no part-select in it.
assign total_granted = running[15:0];
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) ever_insufficient <= 1'b0;
else if (insufficient) ever_insufficient <= 1'b1;
end
endmodulepower_src_e replaces a boolean, and the reason is §1. self_powered reads as a status bit; PWR_BUS versus PWR_SELF reads as what it is — a choice that changes every port's offer by a factor of five.
The UNIT_LOAD > FULL_LOAD guard catches a parameterisation that is nonsense rather than merely unusual: a unit load is a fraction of a full load by definition, and a build that inverts them would silently offer bus-powered ports more than self-powered ones.
10. VHDL-2008
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package usb_power_pkg is
-- Where a hub's downstream current comes from. This is not a status bit:
-- it decides what every port is OFFERED, and those two answers differ by
-- a factor of five.
type power_src_t is (
PWR_BUS, -- drawn from the upstream port -- one unit load per port
PWR_SELF -- the hub has its own supply -- a full load per port
);
-- An UNCONSTRAINED array of per-port requests. The Verilog and
-- SystemVerilog flatten this into one wide vector and slice it back out
-- with a part-select, because neither has an array port that carries its
-- own element width. Here the port is what it actually is.
type ma_array_t is array (natural range <>) of unsigned(15 downto 0);
end package;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.usb_power_pkg.all;
-- usb_hub_power_budget_vhdl -- who gets how much current, and who is refused.
--
-- Chapter 18.2 gave every port a PORT_POWER command and an over-current
-- input and treated both as inputs to a state machine. They are not inputs;
-- they are a resource with a budget, and the budget does not balance.
--
-- The arithmetic that does not work:
--
-- a bus-powered hub may draw 500 mA from its upstream port
-- it must run its own controller from that same 500 mA
-- it must then supply 4 downstream ports
-- at a full load each that would need 2000 mA
--
-- The resolution is that a bus-powered hub does NOT offer a full load. It
-- offers ONE UNIT LOAD -- 100 mA -- to each port, and a device needing more
-- is refused. Linux writes exactly this, with a specification reference
-- attached to the line:
--
-- remaining = hdev->bus_mA - hub->descriptor->bHubContrCurrent;
-- hub->mA_per_port = unit_load; /* 7.2.1 */
entity usb_hub_power_budget_vhdl is
generic (
NPORTS : positive := 4;
UNIT_LOAD : positive := 100;
FULL_LOAD : positive := 500;
SELF_SUPPLY : positive := 2500
);
port (
clk : in std_logic;
rst_n : in std_logic;
power_src : in power_src_t;
upstream_mA : in unsigned(15 downto 0);
contr_current : in unsigned(15 downto 0);
req_mA : in ma_array_t(0 to NPORTS-1);
req_valid : in std_logic_vector(NPORTS-1 downto 0);
overcurrent : in std_logic_vector(NPORTS-1 downto 0);
mA_per_port : out unsigned(15 downto 0);
available : out unsigned(15 downto 0);
limited_power : out std_logic;
grant : out std_logic_vector(NPORTS-1 downto 0);
total_granted : out unsigned(15 downto 0);
insufficient : out std_logic;
ever_insufficient : out std_logic
);
end entity;
architecture rtl of usb_hub_power_budget_vhdl is
signal self_powered : boolean;
signal mpp_i : unsigned(15 downto 0);
signal avail_i : unsigned(15 downto 0);
signal insuf_i : std_logic;
signal ever_r : std_logic := '0';
begin
assert UNIT_LOAD <= FULL_LOAD
report "a unit load is a fraction of a full load, not more than one"
severity failure;
self_powered <= power_src = PWR_SELF;
-- A self-powered hub offers a FULL load; a bus-powered hub offers ONE
-- unit load. There is no arithmetic here to get wrong -- the number is
-- fixed by the specification, not derived from what happens to be spare.
mpp_i <= to_unsigned(FULL_LOAD, 16) when self_powered
else to_unsigned(UNIT_LOAD, 16);
mA_per_port <= mpp_i;
limited_power <= '0' when self_powered else '1';
-- SATURATING subtraction. A hub whose controller is declared to draw more
-- than its upstream port supplies is a malformed descriptor, and the
-- wrapped answer -- an enormous budget -- is the single worst response
-- available: it would grant every port and brown out the whole tree.
avail_i <= to_unsigned(SELF_SUPPLY, 16) when self_powered else
(upstream_mA - contr_current) when upstream_mA > contr_current
else to_unsigned(0, 16);
available <= avail_i;
-- Linux warns "insufficient power available to use all downstream ports"
-- on exactly this condition. It is a WARNING, not a refusal: the hub
-- still works, it simply cannot power every port at once.
insuf_i <= '1' when avail_i < resize(mpp_i * NPORTS, 16) else '0';
insufficient <= insuf_i;
-- Allocation, in port order. A fixed order rather than a policy, because
-- the host -- not the hub -- decides which devices matter, and a hub that
-- reordered would make the host's decisions non-reproducible.
process (req_mA, req_valid, overcurrent, mpp_i, avail_i)
variable running : unsigned(16 downto 0); -- 17 bits: no mid-loop wrap
variable g : std_logic_vector(NPORTS-1 downto 0);
begin
g := (others => '0');
running := (others => '0');
for i in 0 to NPORTS-1 loop
-- Three independent reasons to refuse, and they are not the same:
-- * the port is in over-current -- it gets nothing, whatever it asked
-- * the request exceeds what a port is OFFERED (the unit-load rule)
-- * the request exceeds what the hub has LEFT (the budget)
if req_valid(i) = '1' and overcurrent(i) = '0'
and req_mA(i) <= mpp_i
and (running + ('0' & req_mA(i))) <= ('0' & avail_i) then
g(i) := '1';
running := running + ('0' & req_mA(i));
end if;
end loop;
grant <= g;
total_granted <= running(15 downto 0);
end process;
process (clk, rst_n)
begin
if rst_n = '0' then
ever_r <= '0';
elsif rising_edge(clk) then
if insuf_i = '1' then ever_r <= '1'; end if;
end if;
end process;
ever_insufficient <= ever_r;
end architecture;ma_array_t is the clearest language win in Module 18. The Verilog and SystemVerilog flatten four 16-bit requests into a 64-bit vector and slice them back out with req_mA_flat[i*16 +: 16], because neither has a port that carries an array of a known element width. The VHDL port is what the thing actually is, and the indexing req_mA(i) cannot be off by a factor of the element width — an error the flattened form makes available and the array form does not.
11. The Testbench: 40 960 Allocations, Exhaustively
The decision has four inputs that matter: what each port requests, which ports are in over-current, how the hub is powered, and how much budget there is.
Quantising the request to the four sizes that matter — nothing, one unit load, two unit loads (a device a bus-powered hub must refuse), and a full load — gives 4⁴ = 256 request mixes. Crossed with 16 over-current masks, 2 power sources and 5 budgets: 40 960 points, the entire decision domain.
for (sp=0; sp<2; sp=sp+1)
for (r=0; r<256; r=r+1)
for (oc=0; oc<16; oc=oc+1)
for (bg=0; bg<5; bg=bg+1) begin
apply(r[7:0], {N{1'b1}}, oc[N-1:0], sp[0], UPS[bg], CCR[bg]);
n_exh = n_exh + 1;
...
endFive safety properties are checked at every point, and none comes from the reference model:
// ---- SAFETY PROPERTIES, independent of the model ----
// 1. THE one that matters: never promise more than you have.
check(total_granted <= available,
"the hub granted more current than it can source");
// 2. No granted port was offered more than the per-port limit.
for (j = 0; j < N; j = j + 1)
if (grant[j])
check(req_flat[j*16 +: 16] <= mA_per_port,
"a port was granted more than a port is offered");
// 3. A port in over-current is never granted anything.
check((grant & overcurrent) === {N{1'b0}},
"an over-current port was granted power");
// 4. A bus-powered hub never offers a full load.
check(self_powered || (mA_per_port <= UL),
"a bus-powered hub offered more than one unit load");
// 5. Nothing is granted to a port that did not ask.
check((grant & ~req_valid) === {N{1'b0}},
"a port that did not request power was granted some");Property 1 is the one that matters. Everything else in this block is policy; never promise more current than you can source is the property whose violation damages hardware.
Measured reach, identical in all three languages:
exhaustive power-budget sweep: 40960 of 40960 points verified
REACH: exhaustive=40960 grants-in-sweep=59008 budget-exactly-spent=2048
[Verilog] usb_hub_power_budget: 0 errors — PASS
[SystemVerilog] usb_hub_power_budget_sv: 0 errors — PASS
[VHDL] usb_hub_power_budget_vhdl: 0 errors — PASS12. The Budget That Never Bound
The first version of this sweep had 8192 points and fixed the supply at 500 mA upstream with a 100 mA controller. Every point therefore had exactly 400 mA available — and 400 mA is exactly four ports at one unit load each.
Under that supply the budget never binds. In bus-powered mode the per-port cap of 100 mA means the largest possible total is 4 × 100 = 400, which always fits. In self-powered mode the 2500 mA supply dwarfs the largest possible total of 2000. In 8192 points, the budget constraint was never the reason for a single refusal.
Two mutations noticed:
| Mutation | before | after |
|---|---|---|
| P3 — the budget bound becomes exclusive | 384 | 6173 |
| P7 — each request checked against the full budget, not the remainder | 63 | 1122 |
budget-exactly-spent | 1 | 2048 |
P7 is the serious one. It checks each request against the whole budget instead of what is left, so four ports each asking for 100 mA against a 150 mA budget are all granted — 450 mA promised from a 150 mA supply. That is a violation of §11's property 1, the one property in this block that can damage hardware, and it died 63 times, every one of them in the randomised phase. The exhaustive sweep contributed nothing.
The fix was to choose budgets that align with the sums the request alphabet can produce:
// available = 0, 100, 200, 300, 400. Chosen to ALIGN with the sums the
// request alphabet can actually produce (multiples of 100 under the
// bus-powered per-port cap), so the inclusive bound -- running + req
// exactly equal to the budget -- is reached constantly rather than once.And the boundary values matter as much as the range. Budgets of 0, 100, 200, 300 and 400 are not more numerous than 150 and 250 would be; they are commensurate with the request sizes, so running + req == available happens 2048 times instead of once. A sweep that never lands exactly on a boundary cannot distinguish an inclusive bound from an exclusive one, however many points it has.
13. The Insufficient Check That Was Wrong
One directed test failed on first run, and — for the fourth time in Module 18 — the design was right.
apply(8'b00000011, 4'b0001, 4'b0000, 1'b0, 16'd500, 16'd100);
check(mA_per_port === 16'd100, "a bus-powered hub offers ONE unit load");
check(!grant[0], "so a 500 mA device is refused outright");
check(insufficient, "and the hub warns it cannot supply every port"); // WRONGWith 400 mA available and four ports at 100 mA each, the hub is not short of power at all — 400 is exactly enough (§4). The 500 mA device was refused by the per-port offer, not by the budget, and §5 is precisely about those being different refusals. The check had reached for the wrong one of the three.
// 500 upstream less 100 for the controller leaves 400, which is EXACTLY
// four unit loads -- so this hub is not short of power at all. The
// device is refused by the PER-PORT offer, not by the budget, and those
// are different refusals. The first version of this check asserted
// `insufficient` here and was simply wrong about which rule applies.
check(!insufficient, "this hub CAN supply every port -- 400 = 4 x 100");
// a hub that genuinely cannot: a hungrier controller
apply(8'b01010101, 4'b1111, 4'b0000, 1'b0, 16'd500, 16'd200);
check(available === 16'd300, "500 upstream less 200 for the controller");
check(insufficient, "300 mA cannot supply four ports at 100 mA each");
check(grant === 4'b0111, "so the fourth port gets nothing");Four times in one module a check has fired against correct hardware — 18.2 §12 (an invariant missing a qualifier), 18.2 §14 (a rule missing its exception), 18.3 §10 (a rule read at the wrong instant), and here (a rule confused with its neighbour). Every one was the testbench, and every one made the testbench better than it would have been if it had passed.
14. Mutation Testing — Across All Three Languages
| Mutation | Verilog | SystemVerilog | VHDL | |
|---|---|---|---|---|
| P1 | a bus-powered hub offers a full load | 83784 | 83784 | 83969 |
| P2 | the budget subtraction wraps instead of saturating | 4076 | 4076 | 4048 |
| P3 | the budget bound becomes exclusive | 6173 | 6173 | 6159 |
| P4 | over-current no longer blocks a grant | 115365 | 115365 | 115599 |
| P5 | the per-port offer is not enforced | 21384 | 21384 | 21376 |
| P6 | the insufficient warning is off by one | 4111 | 4111 | 4104 |
| P7 | each request checked against the full budget | 1122 | 1122 | 1119 |
P4 is the largest count in Module 18 at 115 365. An over-current port is granted power on almost every point where over-current is asserted at all — which, across 16 masks, is most of the sweep.
P1 is §1 as a one-line change, and it is the mutation that describes a real, shipped product category: a bus-powered hub that advertises full-load ports it cannot supply.
P2 and P6 score almost identically (4076 and 4111) and are unrelated — one wraps the subtraction, the other moves the warning boundary by one. Close counts are not evidence of duplication; 18.4 §13's duplicate showed identical counts across three languages, which is a different signal entirely.
15. A UVM Environment for a Budget
The budget is combinational, so the value UVM adds here is constrained-random generation of request mixes that sum near the boundary — which is exactly what §12 found the directed sweep was missing.
class power_request_item extends uvm_sequence_item;
`uvm_object_utils(power_request_item)
rand bit [15:0] req_mA [4];
rand bit [3:0] req_valid;
rand bit [3:0] overcurrent;
rand bit [15:0] upstream_mA, contr_current;
rand power_src_e power_src;
// Requests are drawn from the alphabet devices actually declare, not
// uniformly over 16 bits -- a random 16-bit request is refused by the
// per-port rule before the budget is ever consulted.
constraint c_realistic {
foreach (req_mA[i]) req_mA[i] inside {0, 100, 200, 500};
contr_current inside {[0:300]};
upstream_mA inside {100, 200, 300, 400, 500};
}
// THE constraint section 12 is about: make the total land ON the budget,
// not merely near it. A sweep that never lands exactly on the boundary
// cannot tell an inclusive bound from an exclusive one.
constraint c_on_the_boundary {
solve upstream_mA, contr_current before req_mA;
(upstream_mA > contr_current) ->
(req_mA.sum() with (int'(item)) == int'(upstream_mA - contr_current));
}
endclassThe scoreboard's job is the safety property, and it is worth stating as an inequality rather than a comparison:
class power_scoreboard extends uvm_scoreboard;
`uvm_component_utils(power_scoreboard)
// Its OWN recomputation of the offer and the budget (17.3).
function void write(power_txn t);
int unsigned m_mpp = (t.power_src == PWR_SELF) ? FULL_LOAD : UNIT_LOAD;
int unsigned m_avail = (t.power_src == PWR_SELF) ? SELF_SUPPLY
: (t.upstream_mA > t.contr_current
? t.upstream_mA - t.contr_current : 0);
int unsigned total = 0;
foreach (t.grant[i]) if (t.grant[i]) total += t.req_mA[i];
// THE property. Not "the grant matched" -- this one has physical
// consequences, and it is the reason the block exists.
if (total > m_avail)
`uvm_error("PWR/OVER", $sformatf(
"granted %0d mA from a %0d mA budget", total, m_avail))
// The unit-load rule, per port.
foreach (t.grant[i])
if (t.grant[i] && t.req_mA[i] > m_mpp)
`uvm_error("PWR/OFFER", $sformatf(
"port %0d granted %0d mA where a port is offered %0d",
i, t.req_mA[i], m_mpp))
// Over-current outranks everything.
foreach (t.grant[i])
if (t.grant[i] && t.overcurrent[i])
`uvm_error("PWR/OC", $sformatf(
"port %0d granted power while in over-current", i))
endfunction
endclass
covergroup power_cg with function sample(
power_src_e src, int unsigned avail, int unsigned total, bit oc_any);
cp_src : coverpoint src;
cp_oc : coverpoint oc_any { bins fault = {1}; }
// The bin section 12's sweep did not have: the budget SPENT EXACTLY.
// With the original fixed supply this bin would have read 1.
cp_exact : coverpoint (total == avail && avail != 0) { bins spent = {1}; }
// And the budget actually binding, rather than the per-port rule.
cp_bound : coverpoint (total == avail) iff (avail != 0);
x_src_exact : cross cp_src, cp_exact;
endgroup16. Assertions
// THE property: never promise more current than you can source.
property p_never_over_budget;
@(posedge clk) disable iff (!rst_n)
total_granted <= available;
endproperty
a_never_over_budget : assert property (p_never_over_budget)
else $error("the hub granted more current than it can source");
// A bus-powered hub never offers more than one unit load. Section 1.
property p_unit_load_when_bus_powered;
@(posedge clk) disable iff (!rst_n)
(power_src == PWR_BUS) |-> (mA_per_port == UNIT_LOAD);
endproperty
a_unit_load : assert property (p_unit_load_when_bus_powered);
// Over-current outranks every other consideration. Mutation P4.
property p_overcurrent_blocks;
@(posedge clk) disable iff (!rst_n)
(grant & overcurrent) == '0;
endproperty
a_overcurrent_blocks : assert property (p_overcurrent_blocks);
// The budget saturates rather than wrapping. Mutation P2, stated as the
// consequence rather than the mechanism: a deficit yields nothing.
property p_saturating_budget;
@(posedge clk) disable iff (!rst_n)
(power_src == PWR_BUS && contr_current >= upstream_mA)
|-> (available == '0);
endproperty
a_saturating_budget : assert property (p_saturating_budget);p_never_over_budget is the property to prove formally rather than simulate. It is a pure combinational relation over the whole input space, and a prover settles it for every request mix and every budget — including the parameterisations no testbench instantiated.
These were written but not simulated; Icarus supports no concurrent assertions.
17. Debugging: the Drive That Spins Up and Stops
The report: a bus-powered portable hard drive works on a laptop port. On a hub it spins up, clicks, and disconnects — repeatedly. A different, powered hub works fine.
The procedure:
1. Establish which kind of hub it is. limited_power — or, from software, whether the hub is bus-powered. If it is bus-powered, §1 has already answered the question: no port on it is offered more than one unit load, and a drive needs five.
2. Check whether the device was ever configured. A device refused power at configuration time is enumerated but not configured. It appears in the device list, which is why users report "it's detected but doesn't work" rather than "nothing happens."
3. Distinguish the three refusals of §5. If the hub is self-powered and the drive is still refused, read available and total_granted: a self-powered hub with other devices already drawing can run out of budget while still offering a full load per port. That is a different fix — unplug something — than the bus-powered case, which no rearrangement solves.
4. Explain the spin-up-then-stop. The drive's inrush at spin-up exceeds what the port supplies, the port trips over-current, 18.2's FSM takes it to Powered-off, and the device disappears. On the next power-on it tries again. The click is the drive's head parking as its supply collapses.
5. Confirm from the change bits. An over-current change on that port (18.2 §5) distinguishes this from a plain configuration refusal — the first means the device drew too much, the second means it asked for too much and never got the chance.
18. Common Misconceptions
"A hub splits its 500 mA between its ports." It does not divide anything. It offers a fixed unit load per port and refuses what does not fit (§1, §2).
"A bus-powered hub with nothing else plugged in can supply 500 mA to one port." No. The per-port offer is one unit load regardless of how empty the budget is (§5) — the two refusals are independent.
"insufficient means the hub is broken." It is a warning that the hub cannot supply every port at once (§4). It still works.
"400 mA available for four 100 mA ports is insufficient." It is exactly enough — the bound is inclusive (§4), and asserting otherwise failed against correct hardware (§13).
"Over-current is just another reason a request does not fit." It outranks the request entirely: an over-current port gets nothing even asking for 0 mA from a full budget (§5). Mutation P4, 115 365 errors.
"The subtraction can't underflow — descriptors are valid." Mutation P2, 4076 errors. A malformed descriptor turns a deficit into 65 000 mA of imaginary budget.
"Checking each request against the budget is the same as tracking the remainder." Mutation P7: four ports at 100 mA each all granted from a 150 mA supply. The design's most important safety property, violated (§12).
"All 8192 points passed, so the allocator is verified." The supply was constant, so the budget never bound once (§12). Exhaustive over the dimensions you sweep; blind to the ones you fixed.
19. Exercises
1. §12 fixed the supply and made one refusal condition unreachable. Identify every other input this chapter's sweep holds constant, and determine which of §5's conditions each one affects.
2. A self-powered hub has SELF_SUPPLY = 2500 and four ports. Compute the largest total the request alphabet can produce and determine whether the budget can ever bind. Then choose a SELF_SUPPLY that makes it bind, and say what that does to mutation P7's count.
3. Write the SVA property that catches mutation P7 — per-request rather than cumulative budgeting — using only grant, req_mA and available.
4. A SuperSpeed hub uses a 150 mA unit load and a 900 mA full load. Recompute §1's arithmetic and determine whether the imbalance is better or worse than USB 2.0's.
5. §14 notes that P2 and P6 score almost identically and are unrelated. Define a test that distinguishes "two mutations that happen to score alike" from "two mutations that are the same", and state what 18.4 §13's case would have produced under it.
6. The allocator grants in port order. Determine what changes if it granted smallest-request-first, which of §11's five safety properties would still hold, and why the specification does not require it.
20. Summary
The arithmetic does not balance (§1). A bus-powered hub draws 500 mA, runs its own controller from it, and must supply four ports that could each want 500 mA. The resolution is that its ports are offered one unit load — 100 mA — and a device needing more is refused.
That is the entire practical difference between a bus-powered and a self-powered hub (§2, §6), and it is why a bus-powered hub cannot run a bus-powered drive. The refusal is specification-conformant, not a defect.
bHubContrCurrent comes out first (§3), with a saturating subtraction — a wrap turns a deficit into 65 000 mA of imaginary budget (P2, 4076 errors).
insufficient is a warning, not a gate (§4), and its bound is inclusive: 400 mA for four 100 mA ports is exactly enough.
Three refusals are not one refusal (§5). Over-current outranks everything (P4, 115 365 errors — the largest in Module 18); the per-port offer and the budget are independent, and a 500 mA device is refused on a bus-powered hub with a completely empty budget.
All three HDL implementations were simulated (§21) and seven mutations died in all three (§14), with the allocation decision verified exhaustively over all 40 960 points (§11).
And the first sweep's budget never bound once (§12). 8192 points, a constant 500 mA supply, and available permanently equal to exactly four unit loads — so the most dangerous mutation in the chapter, granting 450 mA from a 150 mA supply, died 63 times, every one of them by luck in the randomised phase. Aligning five budgets with the request alphabet took P7 to 1122, P3 to 6173, and exact-boundary hits from 1 to 2048.
An exhaustive sweep is exhaustive over the dimensions you sweep and blind to the ones you fixed — and a sweep that never lands exactly on a boundary cannot distinguish an inclusive bound from an exclusive one, however many points it has.
A fourth testbench check fired against correct hardware (§13), this time confusing the budget refusal with the per-port one. Four for four in this module, and every one improved the bench.
21. Tooling, Honestly
| Language | Design | Testbench | Analysed / compiled | Simulated | Mutations |
|---|---|---|---|---|---|
| Verilog-2005 | usb_hub_power_budget | pb_v_tb.v | ✅ Icarus -g2005 | ✅ 0 errors, 40960/40960 | ✅ all seven |
| SystemVerilog | usb_hub_power_budget_sv | pb_sv_tb.sv | ✅ Icarus -g2012 | ✅ 0 errors, 40960/40960 | ✅ all seven |
| VHDL-2008 | usb_hub_power_budget_vhdl | pb_vhdl_tb.vhd | ✅ nvc 1.23.0 | ✅ 0 errors, 40960/40960 | ✅ all seven |
| UVM (§15) | — | — | ❌ no UVM-capable simulator here | ❌ | — |
| SVA (§16) | — | — | ❌ unsupported by Icarus | ❌ | — |
Icarus refused an enum-typed port driven by a conditional expression, so the testbench drives power_src from a signal assigned with an explicit power_src_e'(...) cast. That is the third distinct enum restriction this module hit in the same tool — after the enum-valued ternary in 18.2 and 18.3. The published code uses the forms that compile everywhere.
All three languages report identical reach (40 960 / 59 008 / 2048) because the exhaustive sweep is fully deterministic; only the randomised tail differs, and it is checked rather than counted.
22. Module 18 Complete
Five chapters, and the hub is built.
| Chapter | What it added | |
|---|---|---|
| 18.1 | Hub Architecture | the repeater: broadcast down, select one up |
| 18.2 | Port Management | the per-port FSM that drives port_enabled |
| 18.3 | Downstream Device Discovery | the status-change endpoint and its poll race |
| 18.4 | Cascading Hubs | the depth limit, and what it really encodes |
| 18.5 | Hub Power Management | the budget, and the arithmetic that does not balance |
The chapters are connected rather than merely sequential. 18.1 left port_enabled undefined and 18.2 drove it. 18.2 produced change bits the host could not read and 18.3 reported them. 18.3's bitmap width is why 18.1 refuses to elaborate above 31 ports. 18.2's over-current input is 18.5's fault condition, and 18.1's babble isolation is 18.2's highest-priority transition.
Verification-wise, the module established one idea across five blocks: exhaustive where the domain is small — 512, 784, 32 768, 8192 and 40 960 points — and then, repeatedly, that exhaustive is a claim about a domain and not about importance. Three chapters found a critical mutation dying on a margin of single digits inside a sweep that had passed completely.
And four testbench checks accused correct hardware. A missing qualifier, a missing exception, a wrong instant, and a confused neighbour. None of them was a design bug, and all four left the testbench stronger — which is the honest case for writing checks that are specific enough to be wrong.
23. What Comes Next
Every port in this module could be powered, and nothing has yet asked what happens when the bus goes quiet.
Module 19 — USB Power Management takes that up: bus power and self power as device-level choices rather than hub-level ones, suspend and the 3 ms of idle that triggers it, resume and who is allowed to initiate it, remote wakeup — the one mechanism in USB by which a device may ask for the host's attention without being asked first — and the power budgeting that spans a whole tree rather than one hub.
That last one is the interesting inversion. Chapter 2.6 established that only the host initiates, and every chapter since has been built on it. Remote wakeup is the exception, and what it costs to have an exception to the single-master rule — in hardware, in latency, and in the number of things that can go wrong — is where Module 19 earns its place.
Browse the full path on the USB tutorials index.
Continue learning
Related tutorials
- Related topic
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.
- Related topic
Hub Architecture
A hub is three devices in one package, and its repeater is deliberately asymmetric: downstream is a broadcast, upstream is a select of exactly one — and two talkers connects neither.
- Related topic
Port Management
Three uncoordinated sources move a hub port and collide in the same cycle — and the host, whose information is stale by construction, is the one that loses.
- Related topic
Downstream Device Discovery
A hub cannot interrupt the host, so every port event waits to be asked for — and the window between the poll and the acknowledgement is where devices are silently lost.
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.
