USB · Module 18
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.
Chapter 18.2 produced five change bits on every port and gave the host no way to read them.
That is this chapter, and it is harder than a register read for one structural reason: a hub cannot interrupt the host. Chapter 2.6's single-master model has no upstream-initiated anything. A device attached to a hub five metres away is invisible until the host happens to ask, and the mechanism by which it asks has a race in it that, if you get it wrong, makes devices silently never appear.
1. Nobody Can Tell the Host Anything
On a bus where only the host initiates, "notify" does not exist. A hub with news must wait.
So the hub exposes an interrupt IN endpoint — the status-change endpoint — and the host polls it at an interval it chose during enumeration. The hub's answer is a bitmap.
Bit 0 is the hub itself (its own over-current). Bits N..1 are the downstream ports. One bit per port, and that is all.
The cost is one extra round trip per event, and the benefit is that the common case — nothing happened — costs almost nothing.
2. Nothing to Report Is Not Zero
The common case needs its own answer, and it is not a bitmap of zeros.
If no bit is set, the hub NAKs the transfer.
The distinction is not pedantic. A zero DATA packet is a successful transfer: it completes, it consumes a data toggle, and it tells the host nothing it did not already assume. A NAK is the endpoint saying "ask again later" — which is precisely what an interrupt endpoint with no news is for.
| NAK | zero DATA | |
|---|---|---|
| Transfer completes | no | yes |
| Data toggle advances | no | yes |
| Information conveyed | "nothing yet" | "nothing yet" |
| Bus time | minimal | a full data packet |
Mutation S4 replaces the NAK with an always-valid report and dies 34 947 times. The two carry the same information and completely different protocol consequences.
3. The Race That Loses Devices
Here is the part worth slowing down for.
A poll and its follow-up are not atomic. The sequence is:
- the host polls; the hub returns a bitmap;
- the host reads that bitmap and decides which ports to investigate;
- the host issues
GetPortStatusand then clears the change bits it has dealt with.
Steps 2 and 3 take time — microseconds at least, and the clearing is a separate control transfer per port. Throughout that window, other ports keep having events.
Now: what should the clear clear?
The tempting answer is "everything" — the host has just serviced the hub, so wipe the slate. That answer silently loses devices.
A device that attaches between the poll and the clear sets a bit the host never saw. A blanket clear wipes it. The port is now attached, quiet, and flagged as having nothing to report. The host will not look at it again until something else happens on that port — which, for a device sitting there waiting to be enumerated, may be never.
The fix is a snapshot rule: an acknowledgement may only clear bits that were in the bitmap the host was actually handed.
The hub latches the bitmap it sent. When the clear arrives, it is masked by that snapshot. A bit set after the snapshot was taken is not in the mask, so the clear cannot touch it, and it survives into the next poll.
The host is acknowledging what it read, not what is there. Those are the same thing only if nothing happened in between — which is exactly the assumption that loses devices.
4. The Discovery Sequence, Drawn
Note what the host never receives: an unsolicited anything. Every arrow into Host is a reply to an arrow the host sent first.
5. The Hardware, Before Any Language
State retained: the pending bitmap, the previous level of each source, the last snapshot sent, and a NAK counter.
Sources are edge-detected, not levels. Chapter 18.2 paid 22 009 errors for this lesson on connect-change, and it applies again one level up: a port asserting "I have news" as a level would re-set its bitmap bit every cycle, so the host's clear could never take effect and it would poll forever on a port it had already serviced.
report_valid is pending != 0. That is §2.
A NAKed poll takes no snapshot. There was no bitmap to hand over, so there is nothing for a later clear to be authorised against. Mutation S5 snapshots anyway and dies 3513 times.
The snapshot updates only on a reported poll — not continuously. A snapshot that tracked pending every cycle (mutation S6, 103 527 errors) would authorise clearing bits the host never saw, which is §3's failure wearing a different hat.
Set beats clear, the same ordering rule and the same justification as 18.2 §5.
And late_event is an output. It names the bits set after the current snapshot — the ones the mask exists to protect. A window this narrow is otherwise invisible, and the testbench needs to prove it was actually entered rather than assume it.
6. Verilog-2005
// usb_hub_status_change -- the hub's status-change endpoint.
//
// A hub cannot interrupt the host. Chapter 2.6's single-master model means
// the hub only ever ANSWERS, so every event chapter 18.2 recorded on a port
// waits in a register until the host asks for it.
//
// The asking is an interrupt IN transfer to this endpoint, and the answer is
// a BITMAP: bit 0 is the hub itself, bits N..1 are the downstream ports. One
// bit per port, regardless of how many things happened on it -- the bitmap
// says WHERE to look, never WHAT happened. The host follows up with a
// GetPortStatus control transfer to each port whose bit was set.
//
// Two things make this harder than a register read:
//
// 1. NOTHING TO REPORT IS NOT ZERO. If no bit is set the hub must NAK the
// transfer, not return a bitmap of zeros. A zero DATA packet is a
// successful transfer that consumed a data toggle and told the host
// nothing; a NAK is the hub saying "ask again later", which is what an
// interrupt endpoint with no news is supposed to say.
//
// 2. THE HOST CLEARS WHAT IT READ, NOT WHAT IS THERE. Between the poll and
// the clear, other ports keep setting bits. A host that cleared the
// whole bitmap would discard events it never saw -- a device that
// attached in that window would simply never be enumerated, and nothing
// anywhere would record that it had been lost. So the clear is MASKED by
// the snapshot the host actually received, and any event arriving after
// that snapshot survives into the next poll.
module usb_hub_status_change #(
parameter integer NPORTS = 4
) (
input wire clk,
input wire rst_n,
// --- from chapter 18.2, one per port: "this port has something to report"
input wire [NPORTS-1:0] port_change,
input wire hub_change, // over-current on the hub itself
// --- the host polling the status-change endpoint ---
input wire poll_req,
output wire [NPORTS:0] bitmap, // {ports, hub} -- bit 0 is the hub
output wire report_valid, // 1 = DATA returned, 0 = NAK
// --- the host acknowledging what it read ---
input wire clear_valid,
input wire [NPORTS:0] clear_mask,
output wire [NPORTS:0] pending,
output reg [NPORTS:0] snapshot, // the last bitmap actually sent
output reg snapshot_valid,
output reg [31:0] nak_count, // polls answered with no news
output wire [NPORTS:0] late_event, // set AFTER the current snapshot
output reg [31:0] late_count // how often that window was hit
);
localparam integer W = NPORTS + 1;
reg [W-1:0] pending_r;
reg [W-1:0] src_q; // previous level of the sources
wire [W-1:0] src = {port_change, hub_change};
// EDGE, not level -- chapter 18.2 learned this the expensive way. A level
// would re-set the bit every cycle the port still had news, so a clear
// could never take effect and the host would poll forever.
wire [W-1:0] set_evt = src & ~src_q;
assign pending = pending_r;
assign bitmap = pending_r;
// NOTHING TO REPORT IS NOT ZERO: with no bits set the endpoint NAKs.
assign report_valid = (pending_r != {W{1'b0}});
// The clear is MASKED BY THE SNAPSHOT. A bit the host did not see in the
// bitmap it was given cannot be cleared by this acknowledgement, however
// the host addressed it -- which is what makes an event arriving between
// the poll and the clear survive.
wire [W-1:0] eff_clear = (clear_valid && snapshot_valid)
? (clear_mask & snapshot)
: {W{1'b0}};
// Events arriving AFTER the snapshot the host is holding. These are the
// ones the snapshot mask exists to protect: the host cannot have seen
// them, so its acknowledgement must not be able to clear them. An output,
// because a window this narrow is otherwise invisible -- and because the
// testbench needs to prove the window was actually entered.
assign late_event = set_evt & ~snapshot & {W{snapshot_valid}};
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
pending_r <= {W{1'b0}};
src_q <= {W{1'b0}};
snapshot <= {W{1'b0}};
snapshot_valid <= 1'b0;
nak_count <= 32'd0;
late_count <= 32'd0;
end else begin
src_q <= src;
// SET BEATS CLEAR, as in chapter 18.2 and for the same reason: an
// event arriving in the same cycle as the acknowledgement must
// survive. Losing an ack costs one more poll; losing an event costs
// a device that is never enumerated.
pending_r <= (pending_r & ~eff_clear) | set_evt;
if (|late_event) late_count <= late_count + 32'd1;
if (poll_req) begin
if (pending_r != {W{1'b0}}) begin
// The host is handed THIS bitmap, so this is what it may clear.
snapshot <= pending_r;
snapshot_valid <= 1'b1;
end else begin
nak_count <= nak_count + 32'd1;
end
end
end
end
endmoduleeff_clear is the whole chapter in three lines. The clear is gated on snapshot_valid — so a host that clears before it has ever polled clears nothing — and masked by snapshot, so it can only touch bits it was shown.
7. SystemVerilog
package usb_status_pkg;
// What the endpoint answers a poll with. NAK is a first-class response,
// not the absence of one: an interrupt endpoint with no news says "ask
// again later", and that is different from successfully reporting zero.
typedef enum logic [1:0] {
RESP_NAK, // nothing pending -- the transfer does not complete
RESP_DATA // a bitmap is returned
} poll_resp_e;
endpackage
// usb_hub_status_change_sv -- the hub's status-change endpoint.
//
// A hub cannot interrupt the host. Chapter 2.6's single-master model means
// the hub only ever ANSWERS, so every event chapter 18.2 recorded on a port
// waits in a register until the host asks for it.
//
// The asking is an interrupt IN transfer to this endpoint, and the answer is
// a BITMAP: bit 0 is the hub itself, bits N..1 are the downstream ports. One
// bit per port, regardless of how many things happened on it -- the bitmap
// says WHERE to look, never WHAT happened.
//
// Two things make this harder than a register read:
//
// 1. NOTHING TO REPORT IS NOT ZERO -- see poll_resp_e above.
// 2. THE HOST CLEARS WHAT IT READ, NOT WHAT IS THERE. Between the poll and
// the clear, other ports keep setting bits. A host that cleared the
// whole bitmap would discard events it never saw, and a device that
// attached in that window would never be enumerated with nothing
// anywhere recording the loss. So the clear is MASKED by the snapshot
// the host actually received.
module usb_hub_status_change_sv
import usb_status_pkg::*;
#(
parameter int unsigned NPORTS = 4
) (
input logic clk,
input logic rst_n,
input logic [NPORTS-1:0] port_change,
input logic hub_change,
input logic poll_req,
output logic [NPORTS:0] bitmap,
output logic report_valid,
output poll_resp_e poll_resp,
input logic clear_valid,
input logic [NPORTS:0] clear_mask,
output logic [NPORTS:0] pending,
output logic [NPORTS:0] snapshot,
output logic snapshot_valid,
output logic [31:0] nak_count,
output logic [NPORTS:0] late_event,
output logic [31:0] late_count
);
localparam int unsigned W = NPORTS + 1;
initial begin
if (NPORTS < 1)
$fatal(1, "NPORTS=%0d: a hub with no downstream ports is not a hub",
NPORTS);
if (NPORTS > 31)
$fatal(1, "NPORTS=%0d: the status-change bitmap would not fit",
NPORTS);
end
logic [W-1:0] pending_r, src_q;
wire [W-1:0] src = {port_change, hub_change};
// EDGE, not level -- chapter 18.2 learned this the expensive way. A level
// would re-set the bit every cycle the port still had news, so a clear
// could never take effect and the host would poll forever.
wire [W-1:0] set_evt = src & ~src_q;
assign pending = pending_r;
assign bitmap = pending_r;
assign report_valid = (pending_r != '0);
// Written as a branch rather than a ternary: a conditional expression
// yielding an enum needs an explicit cast in some tools.
always_comb begin
if (pending_r != '0) poll_resp = RESP_DATA;
else poll_resp = RESP_NAK;
end
// The clear is MASKED BY THE SNAPSHOT. A bit the host did not see in the
// bitmap it was given cannot be cleared by this acknowledgement, however
// the host addressed it.
wire [W-1:0] eff_clear = (clear_valid && snapshot_valid)
? (clear_mask & snapshot) : '0;
// Events arriving AFTER the snapshot the host is holding -- the ones the
// snapshot mask exists to protect. An output, because a window this
// narrow is otherwise invisible.
assign late_event = set_evt & ~snapshot & {W{snapshot_valid}};
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
pending_r <= '0; src_q <= '0; snapshot <= '0; snapshot_valid <= 1'b0;
nak_count <= '0; late_count <= '0;
end else begin
src_q <= src;
// SET BEATS CLEAR, as in chapter 18.2 and for the same reason: losing
// an acknowledgement costs one more poll; losing an event costs a
// device that is never enumerated.
pending_r <= (pending_r & ~eff_clear) | set_evt;
if (|late_event) late_count <= late_count + 1;
if (poll_req) begin
if (pending_r != '0) begin
snapshot <= pending_r; // what the host may now clear
snapshot_valid <= 1'b1;
end else begin
nak_count <= nak_count + 1;
end
end
end
end
endmodulepoll_resp_e makes §2 a type rather than a convention. report_valid is a bit that a reader must interpret; RESP_NAK is a named answer. The two carry identical information and only one of them is self-documenting in a waveform.
8. VHDL-2008
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package usb_status_pkg is
-- What the endpoint answers a poll with. NAK is a first-class response,
-- not the absence of one: an interrupt endpoint with no news says "ask
-- again later", and that is different from successfully reporting zero.
type poll_resp_t is (RESP_NAK, RESP_DATA);
end package;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.usb_status_pkg.all;
-- usb_hub_status_change_vhdl -- the hub's status-change endpoint.
--
-- A hub cannot interrupt the host. Chapter 2.6's single-master model means
-- the hub only ever ANSWERS, so every event chapter 18.2 recorded on a port
-- waits in a register until the host asks for it.
--
-- The asking is an interrupt IN transfer to this endpoint, and the answer is
-- a BITMAP: bit 0 is the hub itself, bits N..1 are the downstream ports. One
-- bit per port, regardless of how many things happened on it -- the bitmap
-- says WHERE to look, never WHAT happened.
--
-- Two things make this harder than a register read:
--
-- 1. NOTHING TO REPORT IS NOT ZERO -- see poll_resp_t above.
-- 2. THE HOST CLEARS WHAT IT READ, NOT WHAT IS THERE. Between the poll and
-- the clear, other ports keep setting bits. A host that cleared the
-- whole bitmap would discard events it never saw, and a device that
-- attached in that window would never be enumerated with nothing
-- anywhere recording the loss. So the clear is MASKED by the snapshot
-- the host actually received.
entity usb_hub_status_change_vhdl is
generic ( NPORTS : positive := 4 );
port (
clk : in std_logic;
rst_n : in std_logic;
port_change : in std_logic_vector(NPORTS-1 downto 0);
hub_change : in std_logic;
poll_req : in std_logic;
bitmap : out std_logic_vector(NPORTS downto 0);
report_valid : out std_logic;
poll_resp : out poll_resp_t;
clear_valid : in std_logic;
clear_mask : in std_logic_vector(NPORTS downto 0);
pending : out std_logic_vector(NPORTS downto 0);
snapshot : out std_logic_vector(NPORTS downto 0);
snapshot_valid : out std_logic;
nak_count : out unsigned(31 downto 0);
late_event : out std_logic_vector(NPORTS downto 0);
late_count : out unsigned(31 downto 0)
);
end entity;
architecture rtl of usb_hub_status_change_vhdl is
constant W : positive := NPORTS + 1;
constant ZERO : std_logic_vector(W-1 downto 0) := (others => '0');
signal pending_r : std_logic_vector(W-1 downto 0) := (others => '0');
signal src_q : std_logic_vector(W-1 downto 0) := (others => '0');
signal snap_r : std_logic_vector(W-1 downto 0) := (others => '0');
signal snapv_r : std_logic := '0';
signal nak_r : unsigned(31 downto 0) := (others => '0');
signal late_r : unsigned(31 downto 0) := (others => '0');
signal src, set_evt, eff_clear, late_i : std_logic_vector(W-1 downto 0);
begin
assert NPORTS >= 1
report "a hub with no downstream ports is not a hub" severity failure;
assert NPORTS <= 31
report "the status-change bitmap would not fit" severity failure;
src <= port_change & hub_change;
-- EDGE, not level -- chapter 18.2 learned this the expensive way. A level
-- would re-set the bit every cycle the port still had news, so a clear
-- could never take effect and the host would poll forever.
set_evt <= src and (not src_q);
pending <= pending_r;
bitmap <= pending_r;
snapshot <= snap_r;
snapshot_valid <= snapv_r;
nak_count <= nak_r;
late_count <= late_r;
report_valid <= '1' when pending_r /= ZERO else '0';
poll_resp <= RESP_DATA when pending_r /= ZERO else RESP_NAK;
-- The clear is MASKED BY THE SNAPSHOT. A bit the host did not see in the
-- bitmap it was given cannot be cleared by this acknowledgement.
eff_clear <= (clear_mask and snap_r)
when (clear_valid = '1' and snapv_r = '1') else ZERO;
-- Events arriving AFTER the snapshot the host is holding -- the ones the
-- snapshot mask exists to protect. An output, because a window this
-- narrow is otherwise invisible.
late_i <= set_evt and (not snap_r) when snapv_r = '1' else ZERO;
late_event <= late_i;
process (clk, rst_n)
begin
if rst_n = '0' then
pending_r <= (others => '0'); src_q <= (others => '0');
snap_r <= (others => '0'); snapv_r <= '0';
nak_r <= (others => '0'); late_r <= (others => '0');
elsif rising_edge(clk) then
src_q <= src;
-- SET BEATS CLEAR, as in chapter 18.2 and for the same reason: losing
-- an acknowledgement costs one more poll; losing an event costs a
-- device that is never enumerated.
pending_r <= (pending_r and (not eff_clear)) or set_evt;
if late_i /= ZERO then late_r <= late_r + 1; end if;
if poll_req = '1' then
if pending_r /= ZERO then
snap_r <= pending_r; -- what the host may now clear
snapv_r <= '1';
else
nak_r <= nak_r + 1;
end if;
end if;
end if;
end process;
end architecture;The VHDL needs late_i as an internal signal because a VHDL out port cannot be read inside the architecture. The Verilog and SystemVerilog read late_event directly in the counter. A restriction that forces the intermediate to be named — and the named version is the clearer of the two.
9. The Waveform
A blanket clear that could not reach the bit the host had not seen
10 cyclesThese are controller-domain clocks, not bus time. A real poll interval is milliseconds and a real clear is a separate control transfer; the figure compresses both to one cycle so the ordering is visible.
Cycle 4 against cycle 5 is the entire chapter. The host asked for everything to be cleared. It got what it was entitled to clear, and the bit it had never seen is still there.
10. The Testbench: 32 768 Points, Exhaustively
The interaction this chapter is about has three inputs — what was pending and snapshotted, what the host clears, and what arrives late — and with 4 ports each is a 5-bit mask. 32 × 32 × 32 = 32 768 points, which is the entire domain, so it is enumerated rather than sampled.
// 32 pending patterns x 32 clear masks x 32 new-event patterns = 32768
// points -- the entire interaction this chapter is about.
for (a=0; a<32; a=a+1)
for (b=0; b<32; b=b+1)
for (c=0; c<32; c=c+1) begin
hard_reset;
// raise the sources to create `a` as edges
step(a[4:1], a[0], 0, 0, 5'd0);
// poll: the host receives this bitmap and may clear only these bits
step(a[4:1], a[0], 1, 0, 5'd0);
// NOW: new events on other bits arrive in the SAME cycle as the
// host's acknowledgement of what it read a cycle ago.
step(a[4:1] | c[4:1], a[0] | c[0], 0, 1, b[4:0]);
n_exh = n_exh + 1;
endThe model keeps its own pending, snapshot and source history and never reads the design's — 17.3's rule. And two checks are independent of the model entirely:
// --- THE INVARIANT, independent of the model ---
// An event bit is never removed from pending unless the host cleared a
// bit it had ACTUALLY BEEN SHOWN. This is the whole chapter.
check(((~pending) & m_pend & ~eff) === {W{1'b0}},
"a pending event vanished without an acknowledged clear");
// A bit the host was never shown can never be cleared by an ack.
check((eff & ~snap_before) === {W{1'b0}},
"a clear reached a bit that was not in the snapshot it acknowledged");The second of those failed 761 times on first run, and the design was right.
eff_clear is computed from the snapshot as it stands before the clock edge — the host is acknowledging a bitmap it was handed earlier. The check was comparing against the snapshot after the edge, and in the randomised phase a poll and a clear can land in the same cycle, installing a new snapshot before the comparison ran.
So the check was reading the right rule against the wrong instant. Capturing snap_before fixed it:
// The clear refers to the bitmap the host was handed EARLIER, so it
// is governed by the snapshot as it stands BEFORE this edge. A poll
// arriving in the same cycle installs a new snapshot, and that new
// one governs the NEXT acknowledgement, not this one.
snap_before = m_snap;
eff = (clr && m_snapv) ? (cm & snap_before) : {W{1'b0}};This is 18.2 §12's lesson a second time in one module. A check fired, the design was correct, and the bug was in the statement of the rule — there, a missing qualifier; here, a missing instant. The exhaustive sweep passed all 32 768 points while this was broken, because the sweep never issues a poll and a clear in the same cycle. It took randomisation to produce the combination.
Measured reach, all three languages:
exhaustive snapshot/clear/late sweep: 32768 of 32768 points verified
Verilog / SystemVerilog:
REACH: exhaustive=32768 polls=46173 naks=1065 late-event cycles=32008
[Verilog] usb_hub_status_change: 0 errors — PASS
VHDL:
REACH: exhaustive=32768 polls=45943 naks=1068 late-event cycles=31966
[VHDL] usb_hub_status_change_vhdl: 0 errors — PASSlate-event cycles=32008 is the number that matters. It says the window in §3 was actually entered 32 008 times, rather than assumed to exist. A run reporting zero there would prove nothing about the snapshot rule no matter how many points it swept — which is 18.1 §14's dirty-reset counter serving the same purpose one chapter later.
11. Mutation Testing — Across All Three Languages
| Mutation | Verilog | SystemVerilog | VHDL | |
|---|---|---|---|---|
| S1 | blanket clear — not masked by the snapshot | 59613 | 59735 | 62850 |
| S2 | clear beats set | 93133 | 93545 | 95134 |
| S3 | sources are levels, not edges | 108260 | 108372 | 108893 |
| S4 | always report — a zero bitmap instead of a NAK | 34947 | 34947 | 34952 |
| S5 | a NAKed poll takes a snapshot anyway | 3513 | 3513 | 3465 |
| S6 | the snapshot tracks pending continuously | 103527 | 103598 | 112445 |
| S7 | late_event ignores the snapshot | 26074 | 26074 | 25922 |
S1 is §3 as a one-line change, and it is the mutation that produces the field failure this chapter exists to prevent: devices that attach during a poll window and are never enumerated.
S1 and S6 are the same bug from opposite ends. S1 removes the mask; S6 keeps the mask but makes the snapshot always current, so the mask never excludes anything. Either way the host can clear what it never saw, and that both die in six figures is the sweep doing its job.
S5 is the smallest at 3513, and unlike 18.2's M1 that number needed no investigation: its distinguishing condition — a poll while nothing is pending, followed by a clear — is reached on roughly a thousand of the 46 173 polls, and every one of them is checked.
No mutation in this matrix required a stimulus change. That is the first time in Module 18, and the reason is structural: the exhaustive sweep here covers the interaction rather than a single transition, so the cases that matter are not rare within it — the contrast with 18.2 §14, where 784 points reached the critical case exactly once, is the lesson.
12. A UVM Environment for a Polled Endpoint
The discovery mechanism is a natural UVM target because the host side is a protocol, not a signal — poll, read, follow up, clear — and sequences express protocols far better than directed stimulus does.
// The host's side of section 3, as a sequence. The GAP between the poll and
// the clear is the point: it is randomised, because a window that is always
// the same length is a window whose corner cases never appear.
class host_poll_seq extends uvm_sequence #(host_txn);
`uvm_object_utils(host_poll_seq)
rand int unsigned gap;
constraint c_gap { gap inside {[0:8]}; }
task body();
host_txn poll, clr;
`uvm_do_with(poll, { kind == POLL; })
if (poll.rsp == RESP_NAK) return; // nothing to acknowledge
repeat (gap) #10ns; // the host thinks; ports keep firing
// Clear ONLY what the poll returned. A sequence that cleared '1 here
// would be modelling the buggy host of section 3 -- which is exactly
// what bad_host_seq below does deliberately.
`uvm_do_with(clr, { kind == CLEAR; mask == poll.bitmap; })
endtask
endclass
// The buggy host, kept as a NEGATIVE test: the DUT must survive it.
class bad_host_seq extends uvm_sequence #(host_txn);
`uvm_object_utils(bad_host_seq)
task body();
host_txn poll, clr;
`uvm_do_with(poll, { kind == POLL; })
if (poll.rsp == RESP_NAK) return;
repeat (4) #10ns;
`uvm_do_with(clr, { kind == CLEAR; mask == '1; }) // blanket clear
endtask
endclassThe scoreboard's single most important check is a conservation property, not a comparison:
class discovery_scoreboard extends uvm_scoreboard;
`uvm_component_utils(discovery_scoreboard)
// Its OWN model of what has been reported and acknowledged (17.3).
local bit [31:0] m_pending, m_snapshot;
local bit m_snap_valid;
// Every event that ever occurred, and every event the host was told about.
local int unsigned events_raised [int];
local int unsigned events_reported [int];
function void write_port_event(int port);
events_raised[port]++;
endfunction
function void write_poll(bit [31:0] bmp);
foreach (bmp[i]) if (bmp[i]) events_reported[i]++;
m_snapshot = bmp; m_snap_valid = 1;
endfunction
// THE property: no event is ever lost. Not "the bitmap matched" -- that
// is checkable cycle by cycle and misses the failure entirely, because
// the bitmap is correct at every instant while the EVENT disappears.
function void check_phase(uvm_phase phase);
foreach (events_raised[p])
if (events_reported.exists(p) == 0 || events_reported[p] == 0)
`uvm_error("DISC/LOST", $sformatf(
"port %0d raised %0d event(s) and the host was never told about any",
p, events_raised[p]))
endfunction
endclassAnd the coverage model makes §3's window a bin:
covergroup discovery_cg with function sample(
int unsigned gap, bit late_during_gap, bit blanket);
// How long the host took between reading and acknowledging.
cp_gap : coverpoint gap { bins none = {0}; bins short = {[1:3]};
bins long = {[4:8]}; }
// Did a port fire inside that window? This is the bin that matters:
// without it, a run can pass with the race never once occurring.
cp_late : coverpoint late_during_gap { bins raced = {1}; }
cp_blanket : coverpoint blanket { bins buggy_host = {1}; }
x_race : cross cp_gap, cp_late, cp_blanket;
endgroup13. Assertions
// Nothing to report is a NAK, not a zero bitmap.
property p_nak_when_empty;
@(posedge clk) disable iff (!rst_n)
(pending == '0) |-> !report_valid;
endproperty
a_nak_when_empty : assert property (p_nak_when_empty);
// THE property: a clear never reaches a bit outside the snapshot the host
// was handed. Section 3 in one line.
property p_clear_within_snapshot;
@(posedge clk) disable iff (!rst_n)
(clear_valid && snapshot_valid)
|=> ((~pending & $past(pending) & ~$past(snapshot)) == '0);
endproperty
a_clear_within_snapshot : assert property (p_clear_within_snapshot)
else $error("an event the host never saw was cleared");
// An event arriving in the same cycle as its own clear survives it.
property p_set_beats_clear;
@(posedge clk) disable iff (!rst_n)
($rose(port_change[0]) && clear_valid) |=> pending[1];
endproperty
a_set_beats_clear : assert property (p_set_beats_clear);
// A NAKed poll installs no snapshot.
property p_nak_takes_no_snapshot;
@(posedge clk) disable iff (!rst_n)
(poll_req && pending == '0) |=> $stable(snapshot);
endproperty
a_nak_no_snapshot : assert property (p_nak_takes_no_snapshot);p_clear_within_snapshot is mutation S1 as a single assertion, and it is the one worth handing to a formal tool: the property is a one-cycle implication over a small state space, which is where formal beats 32 768 simulated points outright.
These were written but not simulated — Icarus supports no concurrent assertions.
14. Debugging: the Device That Only Appears If You Unplug Something Else
The report: a 7-port hub with several devices. Plugging in a new device does nothing — no enumeration, no log entry, no error. Unplugging a different device makes the new one appear immediately.
The trap: this looks like a power problem, and 18.5 is genuinely about power problems. It is not one.
The procedure:
1. Check whether the port is powered and the device attached. If GetPortStatus on that port shows a device present, the hub knows about it. The hub is not the problem; the reporting is.
2. Compare the port's change bits with the hub's bitmap. If the port has a connect-change set and the bitmap does not have that port's bit, the event was lost between the port and the endpoint.
3. Look at what the host clears. A host that clears the full bitmap rather than the bitmap it read is §3's buggy host. Against a hub with mutation S1's behaviour — no snapshot mask — any event arriving during the poll window is destroyed.
4. Explain "unplugging another device fixes it." A detach on another port raises a new event, setting that port's bit and, critically, causing a new poll cycle in which the still-attached device's port has its change bit set again — or, on a hub that reports levels rather than edges, simply forces another report. The new device is not being discovered; it is being re-discovered as a side effect of unrelated traffic.
5. Confirm with late_count. A non-zero late_count says events are landing inside poll windows. Combined with a device that never enumerates, that is the diagnosis.
15. Common Misconceptions
"The hub interrupts the host when a device attaches." Nothing on USB interrupts the host (§1). The hub waits to be polled, and "interrupt endpoint" names a polling schedule, not an interrupt.
"The status-change bitmap says what happened." It says where (§1). The host must follow up with GetPortStatus per flagged port.
"A port with five change bits sets five bits in the bitmap." One (§1). The bitmap is one bit per port.
"If nothing happened the hub returns zeros." It NAKs (§2). A zero DATA packet completes a transfer and advances a data toggle; a NAK does neither.
"After servicing the hub, the host should clear everything." That discards events it never saw (§3) — mutation S1, 59 613 errors, and in the field a device that never enumerates.
"The hub should just re-report anything still outstanding." That is mutation S3 — levels instead of edges — and it means a serviced port re-reports forever, 108 260 errors.
"A snapshot that tracks the current state is simpler and equivalent." Mutation S6, 103 527 errors: a mask that always matches excludes nothing, which is S1 with extra steps.
"The bitmap being correct every cycle means no events are lost." It does not (§12). Under S1 the bitmap is correct at every instant and events still disappear — the failure exists only as a property over the whole run.
16. Exercises
1. §10's check compared against the post-edge snapshot and failed 761 times against a correct design. Identify every other check in that testbench that reads model state after the edge, and determine which of them would break if a poll and a clear arrived together.
2. The exhaustive sweep never issues a poll and a clear in the same cycle, which is why it passed while §10's check was broken. Define the sweep that would cover that combination and compute how many points it has.
3. A hub has 7 ports. Compute the bitmap width, and determine what the NPORTS <= 31 guard in §7 is actually protecting.
4. Write the SVA property that catches mutation S5 — a snapshot taken on a NAKed poll — and state why $stable is not sufficient on its own.
5. §12's scoreboard checks in check_phase rather than per transaction. Construct a run in which a per-transaction scoreboard passes and the check_phase property fails, and state the minimum number of ports it needs.
6. The host's poll interval is chosen at enumeration. Determine the relationship between that interval, the length of §3's window, and the probability that any given event lands inside it — and say what that implies for a hub with many active ports.
17. Summary
Nobody can tell the host anything (§1). The hub exposes a polled interrupt endpoint and answers with a bitmap that says where to look, one bit per port, never what happened.
Nothing to report is a NAK, not a bitmap of zeros (§2). The two carry identical information and entirely different protocol consequences — a NAK completes no transfer and advances no data toggle.
A poll and its acknowledgement are not atomic (§3), and the window between them is where devices are lost. A host that clears the whole bitmap discards events it never saw, and the failure is a device that sits attached and never enumerates with nothing anywhere recording that anything went wrong.
The snapshot rule is the fix: a clear may only touch bits that were in the bitmap the host was actually handed. Set still beats clear (§5), as in 18.2.
All three HDL implementations were simulated (§18) and seven mutations died in all three (§11), with the snapshot/clear/late-event interaction verified exhaustively over all 32 768 points (§10) — and, for the first time in this module, no mutation needed a stimulus change, because the sweep covers an interaction rather than a single transition.
A testbench check failed 761 times against correct hardware (§10). It stated the right rule against the wrong instant: the clear is governed by the snapshot as it stood before the edge, and a poll landing in the same cycle installs a new one. The exhaustive sweep passed all 32 768 points while this was broken, because it never puts a poll and a clear together — it took randomisation to find it.
That is the second time in one module that a correct design was accused by its own testbench (18.2 §12 was the first, a missing qualifier). Both times the count was the diagnostic: 8 and 761, not zero and not millions.
And the lost-device bug has no single cycle to point at (§12). Under mutation S1 every bitmap, every report_valid and every response is correct at every instant; what is wrong is a property over the whole run.
18. Tooling, Honestly
| Language | Design | Testbench | Analysed / compiled | Simulated | Mutations |
|---|---|---|---|---|---|
| Verilog-2005 | usb_hub_status_change | sc_v_tb.v | ✅ Icarus -g2005 | ✅ 0 errors, 32768/32768 | ✅ all seven |
| SystemVerilog | usb_hub_status_change_sv | sc_sv_tb.sv | ✅ Icarus -g2012 | ✅ 0 errors, 32768/32768 | ✅ all seven |
| VHDL-2008 | usb_hub_status_change_vhdl | sc_vhdl_tb.vhd | ✅ nvc 1.23.0 | ✅ 0 errors, 32768/32768 | ✅ all seven |
| UVM (§12) | — | — | ❌ no UVM-capable simulator here | ❌ | — |
| SVA (§13) | — | — | ❌ unsupported by Icarus | ❌ | — |
Icarus again rejected a conditional expression yielding an enum (poll_resp), as it did in 18.2. The published SystemVerilog uses an always_comb branch instead — the form that compiles everywhere.
The VHDL's reach differs slightly from the other two (45 943 polls against 46 173) because the three benches draw from different random generators. The exhaustive portion is identical by construction, and every mutation count agrees to within 6 %.
19. What Comes Next
Three chapters have treated the hub as a single box with N ports below it. That box can have another box below it, and the tree that results is not unlimited.
Chapter 18.4 — Cascading Hubs is about the depth limit, and the interesting thing about it is that it is a timing constraint wearing a topology costume. The rule is usually quoted as "seven tiers", as though it were an architectural choice. It is not: each hub adds propagation delay and each adds its own contribution to the worst-case turnaround a host must wait out before declaring a transaction lost — and Module 17's timeouts are what actually set the bound.
The Linux kernel enforces it as a constant, MAX_TOPO_LEVEL, and refuses to enumerate past it with a message about a bus topology being nested too deep. Where that number comes from, and what the hardware must track to enforce it, is the next chapter.
Browse the full path on the USB tutorials index.
Continue learning
Related tutorials
- 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
Cascading Hubs
The seven-tier rule is a timing constraint wearing a topology costume — and the limit applies to hubs, not to devices, which is one comparison operator almost everyone gets wrong.
- 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.
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.
