Skip to content
VLSI Mentor

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.

NAKzero DATA
Transfer completesnoyes
Data toggle advancesnoyes
Information conveyed"nothing yet""nothing yet"
Bus timeminimala 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:

  1. the host polls; the hub returns a bitmap;
  2. the host reads that bitmap and decides which ports to investigate;
  3. the host issues GetPortStatus and 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

A sequence diagram with five participants: a device, a hub port, the hub's status-change endpoint, the host, and a second device on another port. First the device attaches physically to the hub port, which raises a presence-detect signal. The hub port's state machine records a connect change in its change register and moves from Disconnected to Disabled. The port reports to the status-change endpoint that it has news, which sets one bit in the endpoint's pending bitmap. Separately and on its own schedule, the host polls the status-change endpoint with an interrupt IN transfer. Because a bit is pending the endpoint returns a data packet containing the bitmap, rather than NAKing, and at that moment the hub latches a snapshot of exactly the bitmap it sent. The host reads the bitmap and learns only which port has news, not what happened, so it issues a GetPortStatus control transfer to that port and receives the port's full status and change bits in reply. While the host is doing this, a second device attaches on a different port and that port sets its own bit in the pending bitmap, a bit the host cannot possibly have seen because it was set after the snapshot was taken. The host now clears the change bits it has dealt with, and the hub masks that clear by the snapshot, so the second port's bit is untouched and survives. The host then issues a SetPortFeature RESET to the first port, waits for the reset to complete, which is the only way a port becomes enabled, and finally assigns a device address and reads descriptors. On the next poll the endpoint reports the second port, which was never lost.A device attaches, and the host finds out by askingDeviceHub port (18.2)Status-change EPHostDevice on port 2attach — presencedetectconnect change set;Disconnected →Disabledthis port has newsinterrupt IN (poll)DATA: bitmap —snapshot latchedGetPortStatusstatus + change bitsattaches NOW — setsa bit after thesnapshotclear — masked bythe snapshot, port 2survivesSetPortFeature(RESET)reset complete →port EnabledSetAddress,GetDescriptornext poll → port 2,never lost
Figure 1 — a device attaching beneath a hub, from the physical attach to the host owning an addressed device. The two shaded messages are §3's window: the host is acting on a bitmap that is already out of date, and the hub's snapshot mask is what keeps the port-2 event in the diagram from being discarded by the clear.

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endmodule

eff_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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
endmodule

poll_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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 cycles
A waveform of the hub status-change endpoint over ten clocks. At cycle 0 nothing is pending and report valid is low. At cycle 1 the host polls, and because nothing is pending the endpoint NAKs rather than returning a bitmap of zeros. At cycle 2 a device attaches on port 0, raising that port's change line. At cycle 3 the pending bitmap has become 00010 and report valid is high, and the host polls again; this time the endpoint returns data and latches a snapshot of exactly the bitmap it sent, which is 00010. At cycle 4 two things happen in the same clock: a second device attaches on port 2, setting a new bit that the host cannot possibly have seen, and the host issues a clear with a mask of all ones, an attempt to wipe the whole bitmap. The late event output shows 01000, naming the bit that arrived after the snapshot. Because the clear is masked by the snapshot of 00010, it reaches only the port 0 bit. At cycle 5 the pending bitmap is 01000: port 2's event survived the blanket clear. At cycle 6 the host polls again and is given that bitmap, and the snapshot updates to 01000. At cycle 7 the host clears port 2 specifically with a mask of 01000, and by cycle 8 pending is empty again. At cycle 9 a further poll is answered with a NAK.nothing pending → NAKnothing pending → NAKDATA returned, snapshot latchedDATA returned, snapshotlatchedport 2 fires as host clears 11111port 2 fires as host clears11111it survived — still pendingit survived — still pendingclkport_change0000000000010001010101010101010101010101poll_reqclear_validclear_mask————11111——01000——pending00000000000000000010000100100001000010000000000000report_validsnapshot00000000000000000000000100001000010010000100001000late_event00000000000000000000010000000000000000000000000000t0t1t2t3t4t5t6t7t8t9
Figure 2 — ten clocks taken from the simulator. Cycle 4 is the race: port 2 sets a bit in the very cycle the host issues a blanket clear of 11111. The snapshot was 00010, so the clear reaches only that bit, and port 2's event is still pending at cycle 5.

These 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    // 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;
      end

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
      // --- 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
      // 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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 — PASS

late-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

MutationVerilogSystemVerilogVHDL
S1blanket clear — not masked by the snapshot596135973562850
S2clear beats set931339354595134
S3sources are levels, not edges108260108372108893
S4always report — a zero bitmap instead of a NAK349473494734952
S5a NAKed poll takes a snapshot anyway351335133465
S6the snapshot tracks pending continuously103527103598112445
S7late_event ignores the snapshot260742607425922

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endclass

The scoreboard's single most important check is a conservation property, not a comparison:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
endclass

And the coverage model makes §3's window a bin:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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;
endgroup

13. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  // 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

LanguageDesignTestbenchAnalysed / compiledSimulatedMutations
Verilog-2005usb_hub_status_changesc_v_tb.v✅ Icarus -g2005✅ 0 errors, 32768/32768✅ all seven
SystemVerilogusb_hub_status_change_svsc_sv_tb.sv✅ Icarus -g2012✅ 0 errors, 32768/32768✅ all seven
VHDL-2008usb_hub_status_change_vhdlsc_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

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.