Skip to content
VLSI Mentor

USB · Module 18

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.

A hub has ports. A port can have another hub on it. That is what makes USB a tree rather than a cable, and it is where the rule everybody half-remembers comes from: seven tiers, five hubs, don't daisy-chain too far.

The rule is usually explained as an architectural choice. It is not. It is a timing constraint that was quantised into a topology rule because a topology rule is something a host can check with an integer compare at enumeration time — which is the only moment it can check anything.

This chapter builds the check, and the interesting part is not the number. It is the asymmetry inside it that almost everyone gets wrong.

1. Why Depth Costs Anything At All

A hub is not free to pass through. Chapter 18.1 built its repeater, and the repeater has a turnaround time: it must detect that a downstream port has begun driving, connect it to the upstream path, and carry the signal onward. That takes time in both directions.

Insert a hub and you add, for every transaction beneath it:

  • the cable to the hub, one way;
  • the hub's own repeater delay;
  • the cable onward, one way;
  • and all of it again on the way back.

Now recall what Module 17 established. The host sends a token and waits a bounded time for a response before declaring the transaction lost. That bound is fixed by the protocol — it is the same number regardless of how the tree is wired — and it has to cover the worst case the topology allows.

This is why the number is not negotiable by better engineering. A faster hub does not buy you a deeper tree, because the limit is fixed at the protocol level against a worst-case hub — the host has no way to know how fast yours is, and no way to re-derive the bound per-topology.

2. Where the Number Comes From

Linux encodes it as a constant, and refuses to enumerate past it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  MAX_TOPO_LEVEL = 6

  if (hdev->level == MAX_TOPO_LEVEL)
          → "Unsupported bus topology: hub nested too deep"

The root hub is level 0. A hub at level 6 is refused, so hubs exist at levels 0 through 5 — the root hub plus five external hubs, which is where the familiar "maximum five hubs" comes from. Counting the device at the end gives the "seven tiers" phrasing.

These are three descriptions of one constraint, and confusing them is the commonest source of off-by-one arguments about USB topology:

PhrasingCountsValue
tiersroot hub → device, inclusive7
external hubshubs you can buy5
MAX_TOPO_LEVELlevel at which a hub is refused6

3. The Asymmetry Everybody Misses

Here is the part that matters, and it is a one-character difference in the RTL.

The limit applies to hubs. It does not apply to devices.

A hub at the maximum level is refused. A mouse at that same level is perfectly legal.

The reason is that they do different things to the tree. A hub extends the chain — it must leave room below itself for whatever attaches to it, or the thing that attaches will be past the budget. A function terminates the chain: nothing attaches below it, so it only has to fit, not leave room.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   hub:       child_level  <   MAX_LEVEL      (must leave room below)
   function:  child_level  <=  MAX_LEVEL      (only has to fit)

4. The Tree, Drawn

A hierarchy diagram of a maximum-depth USB topology, drawn as a tree with seven levels. At the top is the root hub, embedded in the host controller, at level zero. Beneath it are two branches. The first branch descends through a chain of external hubs: hub one at level one, hub two at level two, hub three at level three, hub four at level four, and hub five at level five. Beneath hub five, at level six, sits a keyboard, which is a function rather than a hub. This is legal precisely because the keyboard terminates the chain and nothing attaches below it; a hub in that same position would be refused with the message that the bus topology is nested too deep, because a hub must leave room below itself. The second branch from the root hub shows a mouse attached directly at level one, illustrating that most devices sit nowhere near the limit. Every hub in the chain adds its repeater turnaround and two cable traversals to the round trip the host must wait out, which is the timing constraint the depth rule encodes.Keyboardlevel 6 — legalHub 5level 5Hub 4level 4Hub 3level 3Hub 2level 2Hub 1level 1Mouselevel 1 — functionRoot hublevel 0
Figure 1 — a legal maximum-depth topology. The root hub is level 0; five external hubs bring the chain to level 5. The shaded device at level 6 is legal because it terminates the chain — a hub in that position is refused, and that asymmetry is §3.

A hub in the keyboard's position is refused. That single distinction is the whole of §3, and §11's structural properties check it across all 8192 attach decisions rather than at the one boundary point.

5. Two Constraints, Not One

The design computes both forms — the tier count and the accumulated delay — and accepts only if both pass.

Why keep the timing form at all, if the tier limit already encodes it? Because the tier limit is the worst case. A real topology may run out of round-trip budget before running out of tiers: long cables, a slow hub, or a design that must hold a tighter bound than the standard's. Computing the delay explicitly makes that case visible instead of surprising.

And the two failures need different advice. "Too deep" means remove a hub. "Too slow" means shorten the cabling. A host that reports only a boolean cannot tell the user which — which is why reject_reason is an output and not a comment.

Depth is reported first when both fail, because it is the actionable one: a user can count hubs, and cannot measure nanoseconds.

6. The Hardware, Before Any Language

Everything here is combinational except three counters kept purely for observability.

The child's level is the parent's plus one, saturating. A level that wrapped would turn "impossibly deep" into "at the root" — the one answer that must never come out of this block, and mutation T4's 3729 errors.

The delay accumulates one cable and one hub per tier, widened before the addition so the intermediate cannot wrap. Chapter 17.5's signedness bug in its width form: the sum must be computed at a width that holds it, not at the width of its operands.

The round trip is twice the one-way delay. Mutation T3 forgets the doubling and dies 30 376 times — it is the single easiest error to make here, because every intermediate quantity in the module is one-way and only the final comparison is not.

The elaboration-time guard is worth stating: if a single tier already costs more than the whole round-trip budget, nothing can ever be enumerated and the two constraints contradict each other. That is a parameterisation error, and it should stop the build rather than produce hardware that refuses everything.

7. Verilog-2005

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// usb_topology_guard -- the rule that stops a USB tree getting too deep.
//
// The constraint is usually quoted as "seven tiers", as though it were an
// architectural choice. It is not. It is a TIMING constraint that has been
// quantised into a topology rule, and this module computes both forms so
// that the relationship between them is visible.
//
// Every hub inserted between the host and a device adds delay in BOTH
// directions: the cable to it, its own repeater turnaround (chapter 18.1),
// and the cable onward. The host, having sent a token, waits a bounded time
// for the response before declaring the transaction lost -- and that bound
// is fixed by the protocol, not by the topology. So the tree may only be as
// deep as the round trip allows.
//
// The depth rule is the timing rule rounded down to something a host can
// check with an integer compare at enumeration time, which is the only
// moment it actually can check it.
//
// One asymmetry matters and is the most commonly missed part of the rule:
// THE LIMIT IS ON HUBS, NOT ON DEVICES. The last tier is reserved for
// functions. A hub at the maximum level is refused; a mouse at that same
// level is perfectly legal, because it terminates the chain instead of
// extending it.
module usb_topology_guard #(
  parameter integer MAX_LEVEL    = 6,   // Linux calls this MAX_TOPO_LEVEL
  parameter [16:0]  HUB_DELAY    = 17'd11, // repeater turnaround, delay units
  parameter [16:0]  CABLE_DELAY  = 17'd5,  // one cable, delay units
  parameter integer MAX_ROUNDTRIP = 90  // what the host will wait out
) (
  input  wire        clk,
  input  wire        rst_n,

  input  wire        attach_valid,
  input  wire [3:0]  parent_level,      // 0 = the root hub
  input  wire [15:0] parent_delay,      // accumulated one-way delay to parent
  input  wire        is_hub,            // the attaching device is itself a hub

  output wire [3:0]  child_level,
  output wire [15:0] child_delay,
  output wire [15:0] round_trip,
  output wire        accept,
  output wire [1:0]  reject_reason,     // 0 none, 1 too deep, 2 too slow

  output reg  [31:0] reject_depth_count,
  output reg  [31:0] reject_timing_count,
  output reg         ever_rejected
);
  localparam [1:0] REJ_NONE = 2'd0, REJ_DEPTH = 2'd1, REJ_TIMING = 2'd2;

  // The child sits one tier below its parent. Saturating, because a level
  // that wrapped would turn "impossibly deep" into "at the root", which is
  // the one answer that must never come out of this block.
  wire [4:0] next_level = {1'b0, parent_level} + 5'd1;
  assign child_level = (next_level > 5'd15) ? 4'd15 : next_level[3:0];

  // One more cable and one more repeater, one way.
  // Widened to 17 bits BEFORE the addition, so the sum cannot wrap in the
  // intermediate -- chapter 17.5's signedness lesson in its width form.
  wire [16:0] per_tier  = CABLE_DELAY[16:0] + HUB_DELAY[16:0];
  wire [16:0] next_delay = {1'b0, parent_delay} + per_tier;
  assign child_delay = (next_delay > 17'hFFFF) ? 16'hFFFF : next_delay[15:0];

  // The host's wait is a ROUND trip: out and back.
  wire [17:0] rt = {1'b0, child_delay} * 18'd2;
  assign round_trip = (rt > 18'hFFFF) ? 16'hFFFF : rt[15:0];

  // THE ASYMMETRY. A hub extends the chain, so it must leave room below it;
  // a function terminates the chain and only has to fit. Linux refuses a hub
  // whose parent is already at MAX_TOPO_LEVEL, which is this comparison.
  wire depth_ok  = is_hub ? (child_level < MAX_LEVEL[3:0])
                          : (child_level <= MAX_LEVEL[3:0]);
  wire timing_ok = (round_trip <= MAX_ROUNDTRIP[15:0]);

  assign accept = attach_valid && depth_ok && timing_ok;

  // Depth is reported FIRST when both fail. The depth rule is the coarse,
  // checkable form of the timing rule, so a host that is told "too deep"
  // gets the actionable message -- "remove a hub" -- rather than a timing
  // number it cannot act on.
  assign reject_reason = !attach_valid ? REJ_NONE
                       : !depth_ok     ? REJ_DEPTH
                       : !timing_ok    ? REJ_TIMING
                                       : REJ_NONE;

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      reject_depth_count  <= 32'd0;
      reject_timing_count <= 32'd0;
      ever_rejected       <= 1'b0;
    end else if (attach_valid && !accept) begin
      ever_rejected <= 1'b1;
      if (reject_reason == REJ_DEPTH)
        reject_depth_count  <= reject_depth_count  + 32'd1;
      else
        reject_timing_count <= reject_timing_count + 32'd1;
    end
  end
endmodule

HUB_DELAY and CABLE_DELAY are declared with an explicit width, not as integer. Verilog-2005 has no size-cast syntax, so an integer parameter in a 17-bit expression forces the addition to 32 bits and then truncates — which happens to be harmless here and would not be if the widths were closer. Declaring the width is how you say what you meant.

8. SystemVerilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
package usb_topo_pkg;
  // Why an attach was refused. Two DIFFERENT constraints can refuse the same
  // attach, and a host that cannot tell them apart cannot advise the user:
  // "too deep" means remove a hub, "too slow" means shorten the cabling.
  typedef enum logic [1:0] {
    REJ_NONE,
    REJ_DEPTH,    // the tier limit -- the quantised form of the rule
    REJ_TIMING    // the round-trip budget -- the rule itself
  } reject_e;
endpackage

// usb_topology_guard_sv -- the rule that stops a USB tree getting too deep.
//
// The constraint is usually quoted as "seven tiers", as though it were an
// architectural choice. It is not. It is a TIMING constraint that has been
// quantised into a topology rule, and this module computes both forms so
// that the relationship between them is visible.
//
// Every hub inserted between the host and a device adds delay in BOTH
// directions: the cable to it, its own repeater turnaround (chapter 18.1),
// and the cable onward. The host, having sent a token, waits a bounded time
// for the response before declaring the transaction lost -- and that bound
// is fixed by the protocol, not by the topology.
//
// One asymmetry matters and is the most commonly missed part of the rule:
// THE LIMIT IS ON HUBS, NOT ON DEVICES. The last tier is reserved for
// functions. A hub at the maximum level is refused; a mouse at that same
// level is legal, because it terminates the chain instead of extending it.
module usb_topology_guard_sv
  import usb_topo_pkg::*;
#(
  parameter int unsigned MAX_LEVEL     = 6,   // Linux: MAX_TOPO_LEVEL
  parameter int unsigned HUB_DELAY     = 11,
  parameter int unsigned CABLE_DELAY   = 5,
  parameter int unsigned MAX_ROUNDTRIP = 90
) (
  input  logic        clk,
  input  logic        rst_n,

  input  logic        attach_valid,
  input  logic [3:0]  parent_level,
  input  logic [15:0] parent_delay,
  input  logic        is_hub,

  output logic [3:0]  child_level,
  output logic [15:0] child_delay,
  output logic [15:0] round_trip,
  output logic        accept,
  output reject_e     reject_reason,

  output logic [31:0] reject_depth_count,
  output logic [31:0] reject_timing_count,
  output logic        ever_rejected
);
  initial begin
    if (MAX_LEVEL < 1)
      $fatal(1, "MAX_LEVEL=%0d: a tree with no tiers holds nothing",
             MAX_LEVEL);
    // The shallowest possible attach must still fit, or nothing can ever be
    // enumerated and the two constraints contradict each other.
    if (2*(CABLE_DELAY + HUB_DELAY) > MAX_ROUNDTRIP)
      $fatal(1, "a single tier already costs %0d of a %0d budget",
             2*(CABLE_DELAY + HUB_DELAY), MAX_ROUNDTRIP);
  end

  // The child sits one tier below its parent. Saturating, because a level
  // that wrapped would turn "impossibly deep" into "at the root", which is
  // the one answer that must never come out of this block.
  wire [4:0] next_level = 5'(parent_level) + 5'd1;
  assign child_level = (next_level > 5'd15) ? 4'd15 : next_level[3:0];

  // One more cable and one more repeater, one way. Widened before the add.
  wire [16:0] next_delay = 17'(parent_delay) + 17'(CABLE_DELAY)
                                             + 17'(HUB_DELAY);
  assign child_delay = (next_delay > 17'hFFFF) ? 16'hFFFF : next_delay[15:0];

  // The host's wait is a ROUND trip: out and back.
  wire [17:0] rt = 18'(child_delay) * 18'd2;
  assign round_trip = (rt > 18'hFFFF) ? 16'hFFFF : rt[15:0];

  // THE ASYMMETRY. A hub extends the chain, so it must leave room below it;
  // a function terminates the chain and only has to fit.
  wire depth_ok  = is_hub ? (child_level <  4'(MAX_LEVEL))
                          : (child_level <= 4'(MAX_LEVEL));
  wire timing_ok = (round_trip <= 16'(MAX_ROUNDTRIP));

  assign accept = attach_valid && depth_ok && timing_ok;

  // Depth is reported FIRST when both fail: it is the actionable message.
  always_comb begin
    if      (!attach_valid) reject_reason = REJ_NONE;
    else if (!depth_ok)     reject_reason = REJ_DEPTH;
    else if (!timing_ok)    reject_reason = REJ_TIMING;
    else                    reject_reason = REJ_NONE;
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      reject_depth_count <= '0; reject_timing_count <= '0;
      ever_rejected <= 1'b0;
    end else if (attach_valid && !accept) begin
      ever_rejected <= 1'b1;
      if (reject_reason == REJ_DEPTH)
        reject_depth_count  <= reject_depth_count  + 1;
      else
        reject_timing_count <= reject_timing_count + 1;
    end
  end
endmodule

The elaboration guard is the difference worth having. 2*(CABLE_DELAY + HUB_DELAY) > MAX_ROUNDTRIP is a contradiction between two parameters that would otherwise produce hardware which refuses every attach — and a block that refuses everything looks, in a bring-up lab, exactly like a block that is not connected.

9. VHDL-2008

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

package usb_topo_pkg is
  -- Why an attach was refused. Two DIFFERENT constraints can refuse the same
  -- attach, and a host that cannot tell them apart cannot advise the user:
  -- "too deep" means remove a hub, "too slow" means shorten the cabling.
  type reject_t is (REJ_NONE, REJ_DEPTH, REJ_TIMING);
end package;

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

-- usb_topology_guard_vhdl -- the rule that stops a USB tree getting too deep.
--
-- The constraint is usually quoted as "seven tiers", as though it were an
-- architectural choice. It is not. It is a TIMING constraint that has been
-- quantised into a topology rule, and this module computes both forms so
-- that the relationship between them is visible.
--
-- Every hub inserted between the host and a device adds delay in BOTH
-- directions: the cable to it, its own repeater turnaround (chapter 18.1),
-- and the cable onward. The host, having sent a token, waits a bounded time
-- for the response before declaring the transaction lost.
--
-- One asymmetry matters and is the most commonly missed part of the rule:
-- THE LIMIT IS ON HUBS, NOT ON DEVICES. The last tier is reserved for
-- functions. A hub at the maximum level is refused; a mouse at that same
-- level is legal, because it terminates the chain instead of extending it.
entity usb_topology_guard_vhdl is
  generic (
    MAX_LEVEL     : positive := 6;      -- Linux: MAX_TOPO_LEVEL
    HUB_DELAY     : natural  := 11;
    CABLE_DELAY   : natural  := 5;
    MAX_ROUNDTRIP : positive := 90
  );
  port (
    clk                 : in  std_logic;
    rst_n               : in  std_logic;

    attach_valid        : in  std_logic;
    parent_level        : in  unsigned(3 downto 0);
    parent_delay        : in  unsigned(15 downto 0);
    is_hub              : in  std_logic;

    child_level         : out unsigned(3 downto 0);
    child_delay         : out unsigned(15 downto 0);
    round_trip          : out unsigned(15 downto 0);
    accept_o            : out std_logic;
    reject_reason       : out reject_t;

    reject_depth_count  : out unsigned(31 downto 0);
    reject_timing_count : out unsigned(31 downto 0);
    ever_rejected       : out std_logic
  );
end entity;

architecture rtl of usb_topology_guard_vhdl is
  signal cl_i    : unsigned(3 downto 0)  := (others => '0');
  signal cd_i    : unsigned(15 downto 0) := (others => '0');
  signal rt_i    : unsigned(15 downto 0) := (others => '0');
  signal acc_i   : std_logic := '0';
  signal rej_i   : reject_t  := REJ_NONE;
  signal depth_ok, timing_ok : boolean := false;

  signal rd_r, rt_r : unsigned(31 downto 0) := (others => '0');
  signal ever_r     : std_logic := '0';
begin
  assert MAX_LEVEL >= 1
    report "a tree with no tiers holds nothing" severity failure;
  -- The shallowest possible attach must still fit, or nothing can ever be
  -- enumerated and the two constraints contradict each other.
  assert 2*(CABLE_DELAY + HUB_DELAY) <= MAX_ROUNDTRIP
    report "a single tier already exceeds the round-trip budget"
    severity failure;

  -- The child sits one tier below its parent. Saturating, because a level
  -- that wrapped would turn "impossibly deep" into "at the root", which is
  -- the one answer that must never come out of this block.
  process (parent_level)
    variable nl : unsigned(4 downto 0);
  begin
    nl := ('0' & parent_level) + 1;
    if nl > 15 then cl_i <= to_unsigned(15, 4);
    else            cl_i <= nl(3 downto 0); end if;
  end process;

  -- One more cable and one more repeater, one way. Widened before the add.
  process (parent_delay)
    variable nd : unsigned(16 downto 0);
  begin
    nd := ('0' & parent_delay)
          + to_unsigned(CABLE_DELAY, 17) + to_unsigned(HUB_DELAY, 17);
    if nd > 65535 then cd_i <= to_unsigned(65535, 16);
    else               cd_i <= nd(15 downto 0); end if;
  end process;

  -- The host's wait is a ROUND trip: out and back.
  process (cd_i)
    -- cd_i & '0' IS cd_i * 2, exactly, in 17 bits. Written as a shift
    -- rather than a multiply because numeric_std's "unsigned * natural"
    -- returns a result as wide as both operands together -- 34 bits here,
    -- which does not fit the variable and is a runtime error, not a
    -- truncation. The concatenation states the intent and the width.
    variable r : unsigned(16 downto 0);
  begin
    r := cd_i & '0';
    if r > 65535 then rt_i <= to_unsigned(65535, 16);
    else              rt_i <= r(15 downto 0); end if;
  end process;

  -- THE ASYMMETRY. A hub extends the chain, so it must leave room below it;
  -- a function terminates the chain and only has to fit.
  depth_ok  <= (cl_i < to_unsigned(MAX_LEVEL, 4)) when is_hub = '1'
               else (cl_i <= to_unsigned(MAX_LEVEL, 4));
  timing_ok <= rt_i <= to_unsigned(MAX_ROUNDTRIP, 16);

  acc_i <= '1' when (attach_valid = '1' and depth_ok and timing_ok) else '0';

  -- Depth is reported FIRST when both fail: it is the actionable message.
  rej_i <= REJ_NONE   when attach_valid = '0' else
           REJ_DEPTH  when not depth_ok       else
           REJ_TIMING when not timing_ok      else
           REJ_NONE;

  child_level         <= cl_i;
  child_delay         <= cd_i;
  round_trip          <= rt_i;
  accept_o            <= acc_i;
  reject_reason       <= rej_i;
  reject_depth_count  <= rd_r;
  reject_timing_count <= rt_r;
  ever_rejected       <= ever_r;

  process (clk, rst_n)
  begin
    if rst_n = '0' then
      rd_r <= (others => '0'); rt_r <= (others => '0'); ever_r <= '0';
    elsif rising_edge(clk) then
      if attach_valid = '1' and acc_i = '0' then
        ever_r <= '1';
        if rej_i = REJ_DEPTH then rd_r <= rd_r + 1;
        else                      rt_r <= rt_r + 1; end if;
      end if;
    end if;
  end process;
end architecture;

The output port is accept_o, not accept. accept is not a VHDL reserved word, but naming an output after a verb that reads as a command is how the Verilog version got away with it; in VHDL the surrounding when ... else syntax makes accept genuinely hard to read. A rename forced by a language is worth accepting rather than working around.

And the multiply is the language difference that actually cost time. ('0' & cd_i) * 2 analysed cleanly and then failed at runtime: value length 34 does not match variable R length 18. In numeric_std, unsigned * natural returns a result as wide as both operands combined — 17 + 17 = 34 bits — where Verilog would have silently truncated to whatever the target was.

VHDL turned a width assumption into a runtime error naming the variable. Verilog would have produced a number. Neither is "safer" in the abstract, but only one of them told me which line was wrong.

10. The Testbench: 8192 Decisions, Exhaustively

The decision has three inputs: parent level (4 bits), parent delay, and hub or function. Truncating the delay sweep to 8 bits — which comfortably covers every boundary, since the budget is exhausted by parent delay 29 — gives 2 × 16 × 256 = 8192 points, the entire decision domain.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    for (H=0; H<2; H=H+1)
     for (L=0; L<16; L=L+1)
      for (D=0; D<256; D=D+1) begin
        probe(L[3:0], D[15:0], H[0]);
        acc_tbl[H][L][D] = accept;
        n_exh = n_exh + 1;
        ...
      end

The model computes the two constraints separately and combines them last, where the design derives a shared child_delay first and tests both against it. Same answer, different route — 18.1 §11's rule.

11. Three Properties About the Shape of the Answer

The most valuable checks in this chapter are not comparisons against a model at all. They are statements about the shape of the decision surface, and they can only be checked across points — which is why the sweep records its results into a table instead of discarding them.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    // These are statements about the SHAPE of the decision surface. No
    // reference model supplies them, and they are the reason the table
    // above was recorded.
    for (L=0; L<16; L=L+1)
     for (D=0; D<256; D=D+1) begin
       // 1. THE ASYMMETRY: anywhere a hub may attach, a function may too.
       //    The converse is false, and that is the whole point -- the last
       //    tier is reserved for devices that terminate the chain.
       check(!acc_tbl[1][L][D] || acc_tbl[0][L][D],
             "a hub was accepted where a function was not");
       // 2. MONOTONIC IN DEPTH: rejection never un-rejects deeper down.
       if (L > 0)
         check(!acc_tbl[1][L][D] || acc_tbl[1][L-1][D],
               "a hub accepted deeper than one that was rejected");
       // 3. MONOTONIC IN DELAY: more delay never becomes acceptable.
       if (D > 0)
         check(!acc_tbl[1][L][D] || acc_tbl[1][L][D-1],
               "a slower path accepted where a faster one was not");
     end

Monotonicity is the property a topology rule must have and no single point can express. If attaching a hub at level 4 is legal, attaching one at level 5 may or may not be — but if it is illegal at level 4, it must be illegal at level 5. A design that violated that would accept a deep topology while refusing a shallower one, which no amount of per-point checking would notice because every individual answer would look reasonable.

Measured reach, identical in all three languages because the sweep is fully deterministic:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  exhaustive topology sweep: 8192 of 8192 points verified
  structural properties: asymmetry + two monotonicities held

  REACH: exhaustive=8192 accepted=330 rejected-depth=5376 rejected-timing=2486
  [Verilog] usb_topology_guard: 0 errors — PASS
  [SystemVerilog] usb_topology_guard_sv: 0 errors — PASS
  [VHDL] usb_topology_guard_vhdl: 0 errors — PASS

Only 330 of 8192 attaches are accepted, and that is not a stimulus weakness — it is the shape of the rule. The delay sweep runs to 255 while the budget is exhausted at 29, so most of the domain is legitimately out of bounds. The check that matters is that the boundary is in the right place, and the exhaustive sweep puts a point on both sides of it at every level.

12. Mutation Testing — Across All Three Languages

MutationVerilogSystemVerilogVHDL
T1one depth test for hubs and functions alike956956959
T2the stricter test applied to functions too915915902
T3round_trip is one-way, not doubled303763037630392
T4the level wraps instead of saturating372937293665
T5timing is reported before depth171821718217240
T6the cable is forgotten in the per-tier cost566635666356653
T7accept ignores the timing constraint177771777717737

T1 and T2 are the two halves of §3, and they score lowest — 956 and 915 — because each is wrong at exactly one level for exactly one kind of device. That is the entire asymmetry: a single comparison at a single boundary. Both are killed decisively, and the checks that kill them are §11's asymmetry property rather than any per-point comparison.

T5 changes no decision at all — accept is identical — and still dies 17 182 times, because reject_reason is checked. A mutation that only affects a diagnostic output is only detectable if the diagnostic is checked, which is the argument for §5's reasoning being an output rather than a comment.

T6 scores highest because forgetting the cable changes the cost of every tier, so it diverges across essentially the whole sweep.

13. T1 and T2 Were the Same Mutation

The first version of this matrix had T1 and T2 scoring identically: 956, 956, 959 — in every language.

That is 18.1 §15's duplicate detector firing, and this time it caught my own mutation, not a language mismatch.

T2 had been written as:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  wire depth_ok  = is_hub ? (child_level <= MAX_LEVEL[3:0])
                          : (child_level <= MAX_LEVEL[3:0]);

A ternary with identical branches. It simplifies to T1 exactly. I had intended "an off-by-one on the tier limit" as a mutation distinct from "no asymmetry", and they are not distinct: for a hub, removing the asymmetry is the off-by-one. The matrix had six mutations and a duplicate, presented as seven.

The replacement is the genuinely different error — the opposite asymmetry, in which the function loses a tier too:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  wire depth_ok  = (child_level < MAX_LEVEL[3:0]);    // function loses a tier

It now scores 915 against T1's 956, and the two bracket the correct design from opposite sides: T1 is too lax for hubs, T2 too strict for functions.

Two mutations scoring identically across three independent languages is not a coincidence, and it has now caught two different defects in two chapters. In 18.1 the mutation did not mean what its label said; here two labels meant the same thing. The check costs nothing: compare the columns.

14. A UVM Environment for a Topology Rule

The guard is combinational, which makes a conventional driver-monitor-scoreboard environment overkill — and makes it the best example in this module for a different UVM tool: constrained-random generation against a layered constraint.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// A topology is a PATH, not a point. Generating one means generating a
// chain and asking the guard about each link in turn, which is what makes
// this worth a sequence rather than a directed loop.
class topology_item extends uvm_sequence_item;
  `uvm_object_utils(topology_item)
  rand bit [3:0]  parent_level;
  rand bit [15:0] parent_delay;
  rand bit        is_hub;

  // Bias toward the boundary. Uniform random over the whole space spends
  // almost all its time deep in "obviously rejected" territory -- section 11
  // measured that: only 330 of 8192 uniform points are accepted at all.
  constraint c_near_boundary {
    parent_level dist { [0:3] := 2, [4:6] := 6, [7:15] := 1 };
    parent_delay dist { [0:29] := 8, [30:60] := 3, [61:511] := 1 };
  }
endclass

// Build a whole tree and walk it, so the ACCUMULATION is exercised rather
// than just the comparison.
class cascade_seq extends uvm_sequence #(topology_item);
  `uvm_object_utils(cascade_seq)
  rand int unsigned depth;
  constraint c_depth { depth inside {[1:9]}; }

  task body();
    topology_item it;
    bit [3:0]  lvl = 0;
    bit [15:0] dly = 0;
    for (int i = 0; i < depth; i++) begin
      it = topology_item::type_id::create($sformatf("link%0d", i));
      start_item(it);
      it.parent_level = lvl;
      it.parent_delay = dly;
      // Only the LAST link is a function; everything above it must be a hub
      // for the chain to exist at all.
      it.is_hub = (i < depth-1);
      finish_item(it);
      // carry the accumulated cost forward -- this is the part a
      // point-at-a-time test never exercises
      lvl = it.child_level;
      dly = it.child_delay;
    end
  endtask
endclass

The scoreboard checks the structural properties of §11, which is where UVM's ability to accumulate across a run earns its place:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class topology_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(topology_scoreboard)

  // accept[is_hub][level][delay], accumulated across the whole run.
  local bit seen   [bit[20:0]];
  local bit accept [bit[20:0]];

  function void write(topology_txn t);
    bit [20:0] key = {t.is_hub, t.parent_level, t.parent_delay[15:0]};
    seen[key] = 1; accept[key] = t.accept;

    // THE ASYMMETRY, checked whenever both halves have been observed.
    if (t.is_hub && t.accept) begin
      bit [20:0] fkey = {1'b0, t.parent_level, t.parent_delay[15:0]};
      if (seen.exists(fkey) && !accept[fkey])
        `uvm_error("TOPO/ASYM", $sformatf(
          "a hub was accepted at level %0d where a function was refused",
          t.parent_level))
    end
  endfunction

  // Monotonicity can only be judged once the run is over, because it is a
  // relationship between two DIFFERENT inputs (section 11).
  function void check_phase(uvm_phase phase);
    foreach (accept[k]) begin
      bit [20:0] shallower = k - 21'(1 << 16);   // one tier up
      if (accept[k] && seen.exists(shallower) && !accept[shallower])
        `uvm_error("TOPO/MONO",
          "a deeper attach was accepted where a shallower one was refused")
    end
  endfunction
endclass

covergroup topo_cg with function sample(
    bit [3:0] lvl, bit is_hub, bit acc, reject_e why);
  cp_level : coverpoint lvl { bins shallow = {[0:3]};
                              bins at_limit = {4, 5, 6};   // the boundary
                              bins deep = {[7:15]}; }
  cp_kind  : coverpoint is_hub;
  cp_why   : coverpoint why;
  // The bin that matters: BOTH kinds tested at EVERY boundary level, which
  // is the only way the asymmetry is observable at all.
  x_asym : cross cp_level, cp_kind, cp_why;
endgroup

15. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  // A hub is never accepted where a function would be refused. Section 3.
  // Checked with is_hub as a free variable, which is what makes it a
  // statement about the RULE rather than about one attach.
  property p_hub_implies_function;
    @(posedge clk) disable iff (!rst_n)
      (attach_valid && is_hub && accept)
        |-> (child_level <= MAX_LEVEL);
  endproperty
  a_hub_implies_function : assert property (p_hub_implies_function);

  // The level never wraps: a child is always deeper than its parent unless
  // the counter has saturated. Mutation T4.
  property p_level_monotone;
    @(posedge clk) disable iff (!rst_n)
      (child_level > parent_level) || (child_level == 4'd15);
  endproperty
  a_level_monotone : assert property (p_level_monotone)
    else $error("child_level wrapped: an impossibly deep tree read as shallow");

  // Round trip is exactly twice one-way. Mutation T3.
  property p_round_trip;
    @(posedge clk) disable iff (!rst_n)
      (child_delay < 16'h8000) |-> (round_trip == child_delay * 2);
  endproperty
  a_round_trip : assert property (p_round_trip);

  // Accept and reason never disagree.
  property p_reason_consistent;
    @(posedge clk) disable iff (!rst_n)
      accept |-> (reject_reason == REJ_NONE);
  endproperty
  a_reason_consistent : assert property (p_reason_consistent);

p_level_monotone is the one to hand to a formal tool. It is a pure combinational relation over a 4-bit input, so a prover settles it instantly and exhaustively — and unlike the simulation, it settles it for every parameterisation rather than the one the testbench instantiated.

These were written but not simulated; Icarus supports no concurrent assertions.

16. Debugging: the Hub That Works Alone

The report: a customer has a monitor with a built-in hub, a dock with a hub, and a hub on the desk. A webcam plugged into the last one is not detected. Plugged into any of the hubs individually it works fine. Plugged into the desk hub with the dock removed from the chain, it works.

The trap: three hubs, so everyone counts to three and concludes the seven-tier limit cannot be the problem.

The procedure:

1. Count the hubs the host sees, not the hubs the user sees. The root hub is level 0 and the user does not own it. A monitor "with a USB port" frequently contains two cascaded hubs internally — one for the panel's own functions and one for the external ports. Three visible enclosures can easily be five levels.

2. Check the kernel log for the exact message. "Unsupported bus topology: hub nested too deep" is MAX_TOPO_LEVEL and settles it. Its absence does not clear the topology, because the limit may be binding on timing rather than depth (§5).

3. Note which device fails. If a hub at the end fails and a mouse in the same position works, that is §3's asymmetry, and it is the signature that you are exactly at the limit rather than past it.

4. Explain "it works if I remove the dock." Removing one hub from the middle of the chain moves everything below it up one level. The webcam did not change; its depth did.

5. If depth is within bounds and it still fails, look at cable lengths. §5's second constraint binds independently, and a chain of long cables can exhaust the round-trip budget at a legal depth — which is the case reject_reason exists to distinguish.

17. Common Misconceptions

"Seven tiers is an architectural choice." It is a round-trip timing budget converted into an integer compare (§1).

"A faster hub lets you go deeper." The bound is fixed at the protocol level against a worst case (§1). The host cannot know your hub is fast.

"You can have seven hubs." Five external ones, plus the root hub (§2). Seven counts tiers, including the device.

"The limit applies to everything below the last hub." It applies to hubs (§3). A function at the maximum level is legal, and a design that forbids it — mutation T2 — silently loses a tier.

"Applying the same check to hubs and devices is simpler and safe." It is mutation T1, and it makes a legal topology unusable. Neither T1 nor T2 breaks anything that can be observed as a failure — they remove a capability, which is why they ship.

"If the depth is legal the topology is legal." Timing binds independently (§5). Long cables exhaust the budget at a legal depth.

"reject_reason is a nicety." Mutation T5 changes no accept decision and dies 17 182 times. "Remove a hub" and "shorten the cabling" are different instructions to a user.

"Every attach decision checked means the rule is verified." Monotonicity is a relationship between two different inputs (§11) — no single decision can express it, however many you check.

18. Exercises

1. §13 found two mutations that were secretly the same. Write out the four possible depth comparisons over (is_hub, <, <=) and determine how many distinct behaviours they produce, and why.

2. A monitor contains two internal hubs. Compute the deepest external hub chain a user can add and still attach a function, and state which of §2's three phrasings makes the answer easiest to get right.

3. §11 checks monotonicity in depth and in delay separately. Construct a design that satisfies both separately and is still wrong, or prove none exists.

4. The VHDL unsigned * natural returned 34 bits (§9). Determine what the equivalent Verilog expression produces, and whether the Verilog version was correct by design or by accident.

5. Write the SVA property that catches mutation T6 — the forgotten cable — without referring to CABLE_DELAY.

6. §14's cascade_seq carries child_level and child_delay forward into the next link. Determine what a bug in the carry would look like in the mutation matrix of §12, and whether any of the seven would catch it.

19. Summary

The tier limit is a timing constraint in topology clothing (§1). Every hub adds a repeater turnaround and two cable traversals to a round trip the host will only wait out for a fixed, protocol-defined time. The depth rule is that budget divided by the cost of a tier, rounded down into something checkable at enumeration.

MAX_TOPO_LEVEL is 6 (§2) — the root hub plus five external hubs, seven tiers counting the device. Three phrasings, one constraint.

The limit is on hubs, not on devices (§3). A hub must leave room below itself; a function only has to fit. That is one comparison operator, and getting it wrong in either direction (T1, 956 errors; T2, 915) produces a working system that is quietly one tier shallower than the specification allows.

Both forms are computed (§5), because a topology can exhaust its round-trip budget at a legal depth, and because "remove a hub" and "shorten the cabling" are different instructions.

All three HDL implementations were simulated (§20) and seven mutations died in all three (§12), with the attach decision verified exhaustively over all 8192 points (§10).

The checks that matter most are not comparisons (§11). The asymmetry and the two monotonicities are statements about the shape of the decision surface — relationships between different inputs — so no per-point scoreboard can express them, and the sweep records its results into a table specifically so they can be checked across points.

T1 and T2 were the same mutation (§13), scoring identically in all three languages, because T2 had been written as a ternary with identical branches. 18.1 §15's duplicate detector has now caught two different defects in one module — there a mutation that did not mean what its label said, here two labels meaning the same thing.

And VHDL turned a width assumption into a runtime error (§9): unsigned * natural returns a result as wide as both operands together, 34 bits, which failed loudly and named the line. Verilog would have produced a number.

20. Tooling, Honestly

LanguageDesignTestbenchAnalysed / compiledSimulatedMutations
Verilog-2005usb_topology_guardtg_v_tb.v✅ Icarus -g2005✅ 0 errors, 8192/8192✅ all seven
SystemVerilogusb_topology_guard_svtg_sv_tb.sv✅ Icarus -g2012✅ 0 errors, 8192/8192✅ all seven
VHDL-2008usb_topology_guard_vhdltg_vhdl_tb.vhd✅ nvc 1.23.0✅ 0 errors, 8192/8192✅ all seven
UVM (§14)——❌ no UVM-capable simulator here❌—
SVA (§15)——❌ unsupported by Icarus❌—

All three languages report identical reach — 8192 points, 330 accepted, 5376 depth rejections, 2486 timing rejections — because the sweep is fully deterministic and the randomised phase is checked but not counted into those figures. This is the only chapter in Module 18 where the three agree exactly, and the reason is worth noting: every other chapter's reach depends on a random stream, and three languages do not share one.

Verilog-2005 has no size-cast syntax, so HUB_DELAY and CABLE_DELAY are declared with explicit widths there and as plain integers in the other two. The arithmetic is identical; only the way the width is stated differs.

21. What Comes Next

Four chapters have moved bits around. None of them has asked where the power comes from.

Every port in 18.2 had a SetPortFeature(PORT_POWER) command and an over-current input, and both were treated as inputs to a state machine rather than as a resource with a budget. They are a resource with a budget, and it is a tight one.

Chapter 18.5 — Hub Power Management is about that budget, and its central fact is arithmetic that does not work: a bus-powered hub is allowed to draw 500 mA from its upstream port, must run its own controller from that, and must then supply four downstream ports — which at a full 500 mA each would need 2 A it does not have.

The resolution is that a bus-powered hub does not offer its ports a full load. It offers one unit load each — 100 mA — and the Linux hub driver says so in a single line with a specification reference attached. What that means for a device that needs more, and what a hub must do when a port draws more than it was promised, is the last chapter of this module.

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.