Skip to content
VLSI Mentor

USB · Module 18

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.

Module 17 built the host's schedule on an assumption it never had to defend: that every device sits on one flat bus, directly reachable, all running at the same speed.

Almost no real USB system looks like that. Between the host and the device there is usually a hub — often several — and the hub is what turns a point-to-point link into the tiered tree Chapter 2.8 described.

This chapter builds the part of a hub that carries the traffic, and the interesting thing about it is how little it does. A hub does not route. It does not know which device an address belongs to, and it never needs to. What it does instead is something stranger and more specific, and getting it exactly right is the whole job.

1. A Hub Is Three Devices in One Package

The word "hub" names a package, not a block. Inside it there are three distinct pieces of hardware with three unrelated jobs.

The hub controller is a USB device in its own right. It has a device address, endpoint 0, a standard descriptor set with bDeviceClass = 9, and — the part that matters for 18.3 — a status-change endpoint that reports when something happened on a port. The host talks to the hub the same way it talks to a mouse: control transfers to endpoint 0, interrupt transfers from the status-change endpoint. Every port command in 18.2 is a control transfer to this device.

The repeater is the wiring fabric — the subject of this chapter. It carries bus traffic between the upstream port and the downstream ports. It has no address, no endpoints, and no software interface. The host never talks to it; the host talks through it.

The transaction translator (TT) exists only on high-speed hubs, and only because of a problem Module 17 created: a full-speed device cannot run at 480 Mb/s, so its transactions cannot simply be dropped into a 125 µs microframe. §5 describes what the TT does about that.

2. The Asymmetry That Defines the Repeater

The repeater carries traffic in both directions, and it does not behave the same way in the two directions. This asymmetry is the module's entire content.

Downstream — host to devices — is a broadcast. When the host drives the bus, the traffic goes to every enabled port. All of them. The hub does not choose a port, because it has no basis on which to choose: packets carry a device address, and the hub does not maintain any mapping from addresses to ports.

Upstream — device to host — is a select. There is exactly one upstream path, physically, so at most one downstream port may be connected to it at a time.

DownstreamUpstream
Fan-outone to manymany to one
Decisionnone — every enabled portwhich single port
If two parties actimpossible; there is one hosta fault (§3)
Hub needs addresses?nono

Why broadcast rather than route? Because routing would require the hub to know which address lives behind which port, and nothing in USB ever tells it. Device addresses are assigned by the host during enumeration; the hub sees those transactions pass through but is not required to track them. Broadcasting is not a simplification the designers settled for — it is what makes the hub stateless with respect to addressing, which is why a hub can be cheap and why re-addressing a device needs no hub involvement at all.

The cost is paid by the devices. Every enabled device sees every downstream packet and must decode the address field to decide whether it is being spoken to. That decode is already in every device for other reasons, so the broadcast costs nothing extra.

A hub is a wire that fans out in one direction and multiplexes in the other. Everything else in it exists to manage which ports participate.

3. Why Two Talkers Is Not a Race to Arbitrate

Here is the design decision that separates a correct repeater from a plausible one.

Two downstream ports drive upstream in the same cycle. What should the hub do?

The instinctive answer — the one most engineers give, and the one a priority encoder implements — is pick one. Lowest port number wins, or round-robin, or whatever policy seems fair.

That answer is wrong, and it is wrong for a reason worth stating precisely.

The host is a single master (Chapter 2.6). It asked exactly one device to speak — it issued an IN token to one address, and every other device on the bus saw that token and knows it was not addressed. So two devices talking at once does not mean two devices legitimately want the bus. It means something is broken: a device responding to an address that is not its own, a device that failed to stop transmitting, or a genuinely faulty port.

If the hub picks a winner, it forwards that winner's data upstream, and the host accepts it as the response from the device it addressed. If the hub picked the wrong one, the host has just received data from the wrong device with no indication that anything is unusual. The corruption is silent and the host's own error checking cannot catch it, because the packet is well-formed — it is simply from somebody else.

So the repeater connects neither. When two or more ports drive upstream it withholds the connection entirely, raises a sticky flag, and isolates every port that was talking — because it cannot tell which one was addressed, so it cannot tell which one misbehaved.

Isolating both is not over-reaction; it is the only honest option. The hub has strictly less information than the host: it does not know the address that was addressed. Cutting off both and reporting the event lets the host — which does know — sort it out through 18.2's port commands.

The condition has a name: babble, the same word 17.5 used for a transaction that outlived its frame. Both are "a device on the bus when it should not be," seen from two different places.

4. The Repeater Has No Idea Who Anyone Is

It is worth making the statelessness explicit, because it drives the port list of the module in §7.

The repeater's inputs are: which ports are enabled, whether the host is driving, and which ports are driving. That is all. There is no address input, no endpoint number, no packet type, no token decode.

A repeater that needed to see inside packets would be a fundamentally different and more expensive device — it would need a full protocol decoder at every port and would have to track enumeration to keep an address-to-port map current. The choice to make it blind is what keeps a 4-port hub a trivial piece of silicon.

What it does track is exactly one thing: which ports it has cut off, and that exists only because of §3.

5. The Transaction Translator, and Why It Exists

The TT is not implemented here, but it must be described, because it is the piece of a hub that Module 17 makes necessary.

A high-speed hub runs at 480 Mb/s. A full-speed device below it runs at 12 Mb/s. The host schedules in 125 µs microframes; the device cannot do anything meaningful in 125 µs at 12 Mb/s. If the hub simply repeated the traffic down, the host would have to hold the whole high-speed bus idle at full-speed rates for the duration — wasting roughly 97 % of the bandwidth for the benefit of one slow device.

The TT solves this by splitting the transaction in time. The host issues a start-split at high speed, the TT buffers it and then runs the actual transaction downstream at full speed on its own schedule, and the host later issues a complete-split to collect the result. The high-speed bus is occupied only for the two short split packets, and everything between them happens at full speed underneath, in parallel with unrelated high-speed traffic.

A hub may implement one TT for the whole hub or one TT per port. The distinction is visible in software — Linux records it as a multi flag on its usb_tt structure, meaning one TT per port — and it matters because a single shared TT becomes a bottleneck when several full-speed devices are active at once. The same structure carries a think_time field, the hub's internal latency, which the host must add to its scheduling arithmetic.

Note what the TT does not change: the repeater's broadcast/select asymmetry is identical either way. The TT sits beside the repeater, not inside it.

6. The Three Blocks, Drawn

A block diagram of a USB hub's internal structure, arranged in four rows. At the top sits the upstream port, which connects toward the host or a parent hub. Below it, three blocks sit side by side. On the left is the hub controller, a USB device in its own right with endpoint zero and a status change endpoint, addressed by the host like any other device. In the centre, highlighted, is the repeater, the wiring fabric that this chapter implements: it broadcasts downstream to every enabled port and selects exactly one port upstream. On the right is the transaction translator, present only on high-speed hubs, which buffers split transactions so that a full-speed device below does not stall the high-speed bus above. The upstream port connects bidirectionally to the repeater carrying bus traffic, and also feeds the hub controller, because the hub is itself an addressable device on that same bus. The hub controller drives a control-only path into the repeater carrying port enable and isolate commands, and carries no packet data. The repeater connects bidirectionally to a per-port logic block below it, and that block fans out to the individual downstream ports one, two and three at the bottom. The asymmetry is the key point: the downward path is a broadcast reaching every enabled port, while the upward path is a selection of at most one.Upstream porttoward the host or parent hubHub controllera USB device: EP0 +status-change EPRepeaterbroadcast down, select ONE upTransaction translatorhigh-speed hubs only (§5)Per-port logicenable / isolate, per port(18.2)Downstream port 1Downstream port 2Downstream port 3bus traffichub is a devicesplit txnport ctrlbcast / select12
Figure 1 — the three blocks inside a hub. The shaded repeater is what this chapter implements; the hub controller is a USB device in its own right (18.2, 18.3), and the transaction translator is present only on high-speed hubs (§5). Note that the controller's connection to the repeater carries port enable and isolate only — never packet data.

The dashed TT edge is the only optional block. A full-speed hub omits it entirely, and everything else in the figure is unchanged.

7. The Hardware, Before Any Language

Inputs: which ports are enabled; whether the host is driving downstream; which ports are driving upstream. Plus clock, reset, and a bus reset.

State retained: an isolation mask — the ports the repeater has cut off — and two sticky fault flags.

The downstream path is combinational and unconditional. If the host is driving, every enabled, non-isolated port is enabled for transmit. It is not gated on the upstream condition: the two directions are independent, and a hub may be broadcasting downstream in the same cycle a device is answering an earlier transaction upstream.

The upstream path is combinational and conditional. Count the talkers. If exactly one, connect it. Otherwise connect nobody.

Counting, not encoding. The exactly-one test is a population count. Writing it as a priority encoder would produce a design that silently picks — §3's failure — and the two are indistinguishable in every test where only one port talks, which is almost every test.

A disabled port is out of the bus in both directions. Qualifying only one direction would leave a "disabled" port still able to inject traffic upstream, which makes the disable a status bit rather than an isolation mechanism. 18.2 depends on it being real.

On two or more talkers: set both flags, and OR the talking ports into the isolation mask. Sticky, and cumulative. Sticky because a transient that corrupted upstream traffic is a fact the host needs after the ports fall silent. Cumulative because a second fault on a different port must not release the first.

On bus reset: everything clears. This is the only path that clears isolation — and §14 is about how nearly that path went untested.

8. Verilog-2005

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// usb_hub_repeater -- the part of a hub that is not a controller.
//
// A hub is three things in one package, and this module is the first of them:
//
//   * the HUB CONTROLLER -- a USB device in its own right, with endpoint 0
//     and a status-change endpoint (chapter 18.3);
//   * the REPEATER -- this module: a wiring fabric that makes N downstream
//     ports look like one bus to the host;
//   * the TRANSACTION TRANSLATOR -- present only on high-speed hubs, which
//     this chapter describes but does not implement.
//
// The repeater's whole job is directional, and it is asymmetric:
//
//   HOST -> DEVICES   is a BROADCAST. Every enabled port sees it. A hub does
//                     not route; it has no idea which device an address
//                     belongs to, and it does not need one.
//   DEVICE -> HOST    is a SELECT. Exactly one port may drive upstream at a
//                     time, because upstream is a single shared path.
//
// That asymmetry is the module's entire content, and the invariant that
// falls out of it is: AT MOST ONE upstream talker. Two devices driving at
// once is not a race to arbitrate -- the host asked exactly one of them to
// speak, so two talkers means something is broken, and the hub's job is to
// REPORT it rather than to pick a winner.
module usb_hub_repeater #(
  parameter integer NPORTS = 4
) (
  input  wire               clk,
  input  wire               rst_n,
  input  wire               bus_reset,
  input  wire [NPORTS-1:0]  port_enabled,   // ports in the Enabled state
  input  wire               up_rx_active,   // the host is driving downstream
  input  wire [NPORTS-1:0]  dn_rx_active,   // these ports are driving upstream
  output wire [NPORTS-1:0]  dn_tx_en,       // broadcast enable, per port
  output wire [NPORTS-1:0]  up_sel,         // one-hot: connected upstream
  output wire               up_tx_en,       // the hub is driving upstream
  output wire               multi_talker,   // sticky: two ports drove at once
  output wire               babble_port_dis,// sticky: a port was disabled for it
  output wire [NPORTS-1:0]  port_isolated   // ports the repeater has cut off
);
  // A disabled port is not part of the bus in either direction. Qualifying
  // BOTH directions with `port_enabled` is what makes disabling a port a
  // real isolation mechanism rather than a status bit -- chapter 18.2 relies
  // on it, and mutations H1 and H5 each remove one of the two guards.
  wire [NPORTS-1:0] talkers = dn_rx_active & port_enabled;

  // Exactly-one detection, written as a population count rather than a
  // priority encoder: a priority encoder would silently PICK one, which is
  // the behaviour this module exists to refuse.
  integer i;
  reg [31:0] talker_count;
  always @* begin
    talker_count = 32'd0;
    for (i = 0; i < NPORTS; i = i + 1)
      if (talkers[i]) talker_count = talker_count + 32'd1;
  end

  wire one_talker  = (talker_count == 32'd1);
  wire many_talker = (talker_count >  32'd1);

  reg  multi_r, babble_r;
  reg [NPORTS-1:0] disabled_r;   // ports the repeater has isolated

  assign multi_talker    = multi_r;
  assign babble_port_dis = babble_r;
  // An OUTPUT, not internal state. Chapter 17.3 established that a scheduler
  // which cannot say WHY it did nothing cannot be debugged; the same applies
  // here, and it is also what lets a testbench keep its own copy instead of
  // reading the design's.
  assign port_isolated   = disabled_r;

  // DOWNSTREAM: broadcast to every enabled port that the repeater has not
  // isolated. Note this is not gated on `one_talker` -- the host may drive
  // downstream at any time, and the two directions are independent.
  assign dn_tx_en = up_rx_active ? (port_enabled & ~disabled_r) : {NPORTS{1'b0}};

  // UPSTREAM: connect the single talker, and ONLY when there is exactly one.
  // With two or more the connection is withheld entirely: forwarding either
  // one would put corrupted data on the upstream bus and the host would
  // charge it to whichever device it had addressed.
  assign up_sel   = one_talker ? (talkers & ~disabled_r) : {NPORTS{1'b0}};
  assign up_tx_en = one_talker && ((talkers & ~disabled_r) != {NPORTS{1'b0}});

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      multi_r <= 1'b0; babble_r <= 1'b0; disabled_r <= {NPORTS{1'b0}};
    end else if (bus_reset) begin
      multi_r <= 1'b0; babble_r <= 1'b0; disabled_r <= {NPORTS{1'b0}};
    end else begin
      if (many_talker) begin
        // Isolate every port that was talking. The hub cannot tell which of
        // them was addressed, so it disables all of them and lets the host
        // re-enable through chapter 18.2's port state machine. Sticky,
        // because a transient that corrupted upstream traffic is a fact the
        // host needs even after the ports fall silent.
        multi_r    <= 1'b1;
        babble_r   <= 1'b1;
        disabled_r <= disabled_r | talkers;
      end
    end
  end
endmodule

Three details are load-bearing.

talkers masks with port_enabled once, at the top. Every later use inherits it. Chapter 16.2 is the reason: a condition written inline several times instead of named once produced measurably weaker mutation scores, because each copy is a separate opportunity to get it wrong and a separate place a mutation can hide.

up_sel masks with ~disabled_r as well as requiring one_talker. Both guards are needed and they are not redundant: one_talker counts talkers, which deliberately includes isolated ports. That is what makes a babbling isolated port keep re-asserting the fault instead of silently disappearing from the count.

port_isolated is an output. §11 explains why that decision is about verification rather than about the hub.

9. SystemVerilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
package usb_hub_pkg;
  // What the repeater's fabric is doing this cycle. Naming the four cases
  // makes the asymmetry explicit -- downstream is a broadcast, upstream is a
  // select -- and makes FAULT a first-class outcome rather than merely the
  // absence of a connection. "No port is connected" and "two ports tried"
  // are different facts, and a hub that conflates them cannot be debugged.
  typedef enum logic [1:0] {
    C_IDLE,        // nobody is driving
    C_BROADCAST,   // the host is driving: every enabled port hears it
    C_UPSTREAM,    // exactly one port is driving: connect it upstream
    C_FAULT        // two or more ports are driving: connect NEITHER
  } conn_e;
endpackage

module usb_hub_repeater_sv
  import usb_hub_pkg::*;
#(
  parameter int unsigned NPORTS = 4
) (
  input  logic              clk,
  input  logic              rst_n,
  input  logic              bus_reset,
  input  logic [NPORTS-1:0] port_enabled,
  input  logic              up_rx_active,
  input  logic [NPORTS-1:0] dn_rx_active,
  output logic [NPORTS-1:0] dn_tx_en,
  output logic [NPORTS-1:0] up_sel,
  output logic              up_tx_en,
  output logic              multi_talker,
  output logic              babble_port_dis,
  output logic [NPORTS-1:0] port_isolated,
  output conn_e             conn_state
);
  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 exceeds what a single status-change bitmap holds",
             NPORTS);
  end

  logic [NPORTS-1:0] disabled_r;
  assign port_isolated = disabled_r;

  // A disabled port is not part of the bus in EITHER direction. Qualifying
  // both is what makes disabling a port real isolation rather than a status
  // bit -- chapter 18.2 depends on it.
  wire [NPORTS-1:0] talkers = dn_rx_active & port_enabled;
  wire [NPORTS-1:0] live    = talkers & ~disabled_r;

  // $countones states the question directly, where the Verilog needs a loop.
  // Neither is a priority encoder, and that is the point: an encoder would
  // silently PICK a winner, which is the behaviour this module refuses.
  always_comb begin
    if      ($countones(talkers) > 1)  conn_state = C_FAULT;
    else if ($countones(talkers) == 1) conn_state = C_UPSTREAM;
    else if (up_rx_active)             conn_state = C_BROADCAST;
    else                               conn_state = C_IDLE;
  end

  // Downstream and upstream are independent: the host may drive at any time.
  assign dn_tx_en = up_rx_active ? (port_enabled & ~disabled_r) : '0;
  assign up_sel   = (conn_state == C_UPSTREAM) ? live : '0;
  assign up_tx_en = (conn_state == C_UPSTREAM) && (live != '0);

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || bus_reset) begin
      multi_talker <= 1'b0; babble_port_dis <= 1'b0; disabled_r <= '0;
    end else if (conn_state == C_FAULT) begin
      // Isolate every port that was talking. The hub cannot know which was
      // addressed, so it cuts off all of them and leaves re-enabling to the
      // host through chapter 18.2's port state machine. Sticky, because a
      // transient that corrupted upstream traffic is a fact the host needs
      // after the ports fall silent.
      multi_talker    <= 1'b1;
      babble_port_dis <= 1'b1;
      disabled_r      <= disabled_r | talkers;
    end
  end
endmodule

conn_e is the version worth keeping. The Verilog has the same four cases but only as combinations of one_talker and many_talker; a waveform shows two booleans and the reader reconstructs the meaning. Here the state is named, and C_FAULT is a value rather than the absence of one.

The NPORTS > 31 guard is not arbitrary. A hub reports port events as a bitmap in a status-change transfer, and Linux's hub driver contains a compile-time #error on exactly this boundary: USB_MAXCHILDREN > 31 breaks the event_bits[] array that holds it. A parameter that quietly exceeds what the reporting mechanism can express is worse than one that refuses to elaborate — 18.3 builds that bitmap.

10. VHDL-2008

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

package usb_hub_pkg is
  -- What the repeater's fabric is doing this cycle. A distinct type with no
  -- numeric encoding: FAULT is a first-class outcome rather than merely the
  -- absence of a connection. "No port is connected" and "two ports tried"
  -- are different facts, and a hub that conflates them cannot be debugged.
  type conn_t is (C_IDLE, C_BROADCAST, C_UPSTREAM, C_FAULT);

  -- Population count over a port mask. Declared in the package because both
  -- the design and its testbench need it, and a second copy would be a
  -- second opinion.
  function popcount (v : std_logic_vector) return natural;
end package;

package body usb_hub_pkg is
  function popcount (v : std_logic_vector) return natural is
    variable c : natural := 0;
  begin
    for i in v'range loop
      if v(i) = '1' then c := c + 1; end if;
    end loop;
    return c;
  end function;
end package body;

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

entity usb_hub_repeater_vhdl is
  generic ( NPORTS : positive := 4 );
  port (
    clk             : in  std_logic;
    rst_n           : in  std_logic;
    bus_reset       : in  std_logic;
    port_enabled    : in  std_logic_vector(NPORTS-1 downto 0);
    up_rx_active    : in  std_logic;
    dn_rx_active    : in  std_logic_vector(NPORTS-1 downto 0);
    dn_tx_en        : out std_logic_vector(NPORTS-1 downto 0);
    up_sel          : out std_logic_vector(NPORTS-1 downto 0);
    up_tx_en        : out std_logic;
    multi_talker    : out std_logic;
    babble_port_dis : out std_logic;
    port_isolated   : out std_logic_vector(NPORTS-1 downto 0);
    conn_state      : out conn_t
  );
end entity;

architecture rtl of usb_hub_repeater_vhdl is
  constant ZERO : std_logic_vector(NPORTS-1 downto 0) := (others => '0');

  signal disabled_r : std_logic_vector(NPORTS-1 downto 0) := (others => '0');
  signal multi_r, babble_r : std_logic := '0';
  signal talkers, live : std_logic_vector(NPORTS-1 downto 0);
  signal conn : conn_t;
begin
  assert NPORTS >= 1
    report "a hub with no downstream ports is not a hub" severity failure;
  assert NPORTS <= 31
    report "NPORTS exceeds what a single status-change bitmap holds"
    severity failure;

  -- A disabled port is not part of the bus in EITHER direction.
  talkers <= dn_rx_active and port_enabled;
  live    <= talkers and (not disabled_r);

  conn <= C_FAULT    when popcount(talkers) > 1 else
          C_UPSTREAM when popcount(talkers) = 1 else
          C_BROADCAST when up_rx_active = '1' else
          C_IDLE;

  conn_state      <= conn;
  port_isolated   <= disabled_r;
  multi_talker    <= multi_r;
  babble_port_dis <= babble_r;

  -- Downstream and upstream are independent: the host may drive at any time.
  dn_tx_en <= (port_enabled and (not disabled_r)) when up_rx_active = '1'
              else ZERO;
  up_sel   <= live when conn = C_UPSTREAM else ZERO;
  up_tx_en <= '1' when (conn = C_UPSTREAM and live /= ZERO) else '0';

  process (clk, rst_n)
  begin
    if rst_n = '0' then
      multi_r <= '0'; babble_r <= '0'; disabled_r <= (others => '0');
    elsif rising_edge(clk) then
      if bus_reset = '1' then
        multi_r <= '0'; babble_r <= '0'; disabled_r <= (others => '0');
      elsif conn = C_FAULT then
        -- Isolate every port that was talking: the hub cannot know which was
        -- addressed. Sticky, because a transient that corrupted upstream
        -- traffic is a fact the host needs after the ports fall silent.
        multi_r    <= '1';
        babble_r   <= '1';
        disabled_r <= disabled_r or talkers;
      end if;
    end if;
  end process;
end architecture;

conn_t is an enumerated type with no encoding at all — no logic [1:0], no numeric value. A state that cannot be compared to an integer cannot be accidentally compared to one, and the VHDL version is the only one of the three where conn_state = 2 is a compile error rather than a silent success.

popcount lives in the package, and the testbench imports the same function. That is a deliberate exception to the independence rule §11 is about, and the reasoning is narrow: a population count is a primitive, not a behaviour. The testbench must derive the expected connectivity independently, but it does not need a second implementation of "count the set bits" — a second copy would be a second chance to write it wrong, checking nothing.

11. The Testbench, and Why port_isolated Is an Output

Chapter 17.3 produced the most expensive verification lesson in this track, and it is applied here from the first line rather than discovered again.

There, the scoreboard checked the arbiter's grant against a reference model — and passed that model the DUT's own pending register and rr_ptr. The model was therefore checking only the final combinational step, with every piece of state handed to it by the thing under test. Four separate invariant mutations each died by exactly one check. When the model was rewritten to predict its own state, those counts went to 12456, 6338, 2046 and 4019.

Here the model keeps its own isolation mask, updated from the observed inputs, and never reads port_isolated:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  // INDEPENDENT MODEL. It keeps its OWN isolation mask and derives the
  // expected connectivity from the OBSERVED inputs -- never from the design's
  // port_isolated. Chapter 17.3 measured what happens when a model is handed
  // the state it is supposed to be checking: four invariant mutations each
  // died by a single check.
  reg [N-1:0] m_iso = 0;
  reg m_multi = 0, m_babble = 0;

  // Expected connectivity, derived a DIFFERENT way from the design: the model
  // builds the talker set then asks how large it is, where the design counts
  // first and masks after. Same answer, different route.
  task expect_comb(input [N-1:0] en, input [N-1:0] dn, input up,
                   input [N-1:0] iso);
    reg [N-1:0] tk, live;
    integer c;
    begin
      tk   = dn & en;
      live = tk & ~iso;
      c    = popcount(tk);
      check(dn_tx_en === (up ? (en & ~iso) : {N{1'b0}}),
            "dn_tx_en broadcasts to enabled, non-isolated ports only");
      check(up_sel === ((c==1) ? live : {N{1'b0}}),
            "up_sel connects exactly one talker, or none");
      check(up_tx_en === ((c==1) && (live != {N{1'b0}})),
            "up_tx_en tracks a single live talker");
    end
  endtask

  task step(input [N-1:0] en, input [N-1:0] dn, input up);
    reg [N-1:0] tk;
    begin
      p_en=en; dn_rx=dn; up_rx=up;
      #1;
      expect_comb(en, dn, up, m_iso);     // the MODEL's mask, not the DUT's
      tk = dn & en;
      if (popcount(tk) > 1) n_many = n_many + 1;
      else if (popcount(tk) == 1) n_one = n_one + 1;
      else n_none = n_none + 1;

      @(posedge clk); #1;
      // advance the MODEL's own state from the observed inputs
      if (popcount(tk) > 1) begin
        m_iso = m_iso | tk; m_multi = 1; m_babble = 1; n_iso_ev = n_iso_ev + 1;
      end
      #1;
      check(isolated === m_iso, "port_isolated matches the independent model");
      check(multi  === m_multi,  "multi_talker matches the model");
      check(babble === m_babble, "babble_port_dis matches the model");
    end
  endtask

port_isolated exists as an output precisely so this comparison can be made. Without it the isolation mask would be internal, the testbench would have to reach into the hierarchy to see it or infer it from behaviour, and the check isolated === m_iso — which is what kills mutation H4 8177 times — could not be written at all.

The model also derives the answer by a different route than the design. The design counts talkers and masks afterwards; the model builds the live set and then asks how large the talker set is. Same answer, different arithmetic — so a mistake in one is not automatically a mistake in the other.

12. 512 Points, Exhaustively

The connectivity decision has four inputs: a 4-bit enable mask, a 4-bit talker mask, one host-driving bit, and the isolation mask. With isolation held clear, that is 16 × 16 × 2 = 512 points — the entire combinational domain.

Following 16.4, 17.3 and 17.5: where the domain is small enough to enumerate, enumerate it. Randomisation on a 512-point space is strictly worse than the sweep.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    // ===== A. EXHAUSTIVE connectivity sweep from a clean isolation state =====
    // 16 enable masks x 16 talker masks x 2 host-drive states = 512 points,
    // the whole combinational domain with nothing isolated.
    for (e=0;e<16;e=e+1)
     for (d=0;d<16;d=d+1)
      for (u=0;u<2;u=u+1) begin
        do_bus_reset;
        p_en=e[N-1:0]; dn_rx=d[N-1:0]; up_rx=u[0];
        #1;
        n_exh = n_exh + 1;
        expect_comb(e[N-1:0], d[N-1:0], u[0], {N{1'b0}});
        // THE INVARIANT: at most one upstream connection, always.
        check(popcount(up_sel) <= 1, "up_sel is at most one-hot, always");
        // A disabled port is never connected in either direction.
        check((up_sel & ~e[N-1:0]) === 0, "a disabled port is never selected");
        check((dn_tx_en & ~e[N-1:0]) === 0, "a disabled port is never broadcast to");
      end

The invariant is checked at every one of the 512 points, not merely at the points where it seemed likely to fail. popcount(up_sel) <= 1 is the property §3 is about, and asserting it everywhere is what makes H2 and H6 die.

Then directed cases for the behaviour that needs a sequence rather than a point:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    // the invariant under pressure: two talkers
    do_bus_reset;
    step(4'b1111, 4'b0101, 1'b0);
    check(up_sel === 4'b0000,
          "TWO talkers connects NEITHER -- the hub refuses to pick");
    check(!up_tx_en, "and drives nothing upstream");
    check(multi, "and reports multi_talker");
    check(isolated === 4'b0101, "and isolates both offending ports");

    step(4'b1111, 4'b0001, 1'b0);
    check(up_sel === 4'b0000, "an isolated port stays cut off even alone");
    step(4'b1111, 4'b0010, 1'b1);
    check(dn_tx_en === 4'b1010, "and receives no broadcast either");
    check(multi, "multi_talker is STICKY");

The third of those is the one that matters most. A port that was isolated for babbling, now talking alone, must still be refused — otherwise isolation only holds while the fault persists, which is exactly backwards.

Measured reach, all three languages:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  exhaustive connectivity sweep: 512 of 512 points verified

  Verilog / SystemVerilog:
  REACH: exhaustive=512 one-talker=2537 multi-talker=1565 silent=1904 isolations=1565
  REACH: bus resets=701 of which from a DIRTY state=164
  [Verilog] usb_hub_repeater: 0 errors
  [Verilog] PASS

  VHDL:
  REACH: exhaustive=512 one-talker=2519 multi-talker=1598 silent=1889 isolations=1598
  REACH: bus resets=704 of which from a DIRTY state=170
  [VHDL] usb_hub_repeater_vhdl: 0 errors
  [VHDL] PASS

13. Mutation Testing — Across All Three Languages

Seven mutations, each a plausible design, run against all three benches. The number is how many checks failed; every mutation must die in every language.

MutationVerilogSystemVerilogVHDL
H1broadcast ignores port_enabled106310631131
H2two talkers → pick the lowest port504504531
H3a disabled port may still talk upstream414741474151
H4isolation overwrites instead of accumulating817781777774
H5isolation not applied to the broadcast250025002411
H6upstream selection is not exclusive638638665
H7bus reset does not clear isolation318144203431

H2 is the mutation this chapter exists for. It replaces the refusal with a priority pick — the design most engineers write first. It is not a typo or an off-by-one; it is a coherent alternative design that happens to be wrong for the reason §3 gives, and it is indistinguishable from correct behaviour in every test where only one port talks.

H4 scores highest because it is checked every cycle. isolated === m_iso runs on every step, so a mask that overwrites rather than accumulates diverges from the model immediately and stays diverged.

H1 and H5 are the two halves of one guard. dn_tx_en masks with both port_enabled and ~disabled_r; each mutation drops one. That they score differently (1063 vs 2500) is itself informative: isolated ports are rarer than disabled ones in the stimulus, so removing the isolation guard is caught more often only because isolation, once set, persists.

14. H7 Survived on a Two-Error Margin

The first run of this matrix looked like this, and it passed — every mutation killed:

MutationVerilogSystemVerilogVHDL
H1483483481
H2138138272
H3536536499
H4103001030010643
H5274327432906
H6272272272
H7127172

H7 — "a bus reset does not clear the isolation mask" — died by two errors in VHDL. Two. A killed mutation, a green matrix, and a number that should stop anyone reading it.

The cause is Chapter 17.2's U6 lesson, exactly. There, a clearing operation was exercised only from an already-clear state and its mutation survived outright. Here the shape is nearly identical:

  • The exhaustive sweep calls do_bus_reset 512 times — but the sweep is combinational, no clock edge advances the isolation register inside it, so every one of those 512 resets ran from a clean state. A reset that clears nothing proves nothing.
  • The 6000-cycle randomised phase issued no bus reset at all.
  • Exactly one directed check exercised clearing from a dirty state.

That single check was the entire kill. Delete it and H7 survives a 6512-point testbench.

A clearing operation is only proven from a dirty state, and the count of resets is not the count of meaningful resets. 512 resets had run and none of them had tested anything.

There was a second problem hiding behind the same gap. Because nothing ever cleared isolation during the random phase, the first multi-talker event isolated ports that never came back — so the run saturated at "everything cut off" and spent most of its 6000 cycles in a degenerate state, never re-entering the partially-isolated region where H1, H3 and H5 are distinguishable.

The fix is one stimulus change, plus a counter that makes the hole visible if it ever reopens:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  task do_bus_reset;
    begin
      // A clearing operation is only PROVEN from a dirty state; count how
      // often this reset actually had something to clear.
      n_rst = n_rst + 1;
      if (isolated !== 0 || multi || babble) n_dirty_rst = n_dirty_rst + 1;
      bus_reset=1; @(posedge clk); #1; bus_reset=0;
      m_iso=0; m_multi=0; m_babble=0; #1;
      check(isolated === 0,        "a bus reset clears every isolation");
      check(!multi && !babble,     "a bus reset clears the fault flags");
    end
  endtask

    // ===== C. randomised =====
    for (i=0;i<6000;i=i+1) begin
      step({$random}%16, {$random}%16, ({$random}%2)==0);
      // Recover the bus periodically. Without this the first multi-talker
      // event isolates ports that NEVER come back, so the run saturates at
      // "everything cut off" and stops re-entering the partially-isolated
      // region -- and the clear path is never exercised while dirty.
      if (({$random}%32) == 0) do_bus_reset;
    end

Dirty resets went from 1 to 164, and H7 from 2 errors to 3431. Three orders of magnitude.

And the rest of the matrix improved with it, which is the part worth noticing: H1 483 → 1063, H3 536 → 4147, H6 272 → 638. The stimulus hole was never only about H7. A run that saturates in a corner is weak everywhere, and the mutation that nearly escaped was the symptom, not the disease.

15. The VHDL Mutation That Was Not the Mutation

One more row of that first table was wrong, and in a way that mutation testing is uniquely prone to.

VHDL H2 scored 272 — identical to H6. That coincidence is the tell.

H2 is supposed to be "pick the lowest-numbered talker instead of refusing." In Verilog and SystemVerilog it is written as a priority loop. In VHDL, a priority encoder does not fit in a concurrent conditional assignment, so it had been written as the nearest convenient thing:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  -- what was written -- NOT a priority pick
  up_sel <= live when (conn = C_UPSTREAM or conn = C_FAULT) else ZERO;

That connects all the talkers in the fault case, which is H6, not H2. The VHDL matrix had six distinct mutations and one duplicate, and the duplicate was labelled as the most important mutation in the chapter. The "all three languages agree" claim was false for the one row it mattered most for.

The real mutation needs a process:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  -- MUT H2: with two talkers, PICK the lowest-index one instead of refusing.
  mut_h2 : process (live)
    variable p : std_logic_vector(NPORTS-1 downto 0);
  begin
    p := (others => '0');
    for i in NPORTS-1 downto 0 loop
      if live(i) = '1' then p := (others => '0'); p(i) := '1'; end if;
    end loop;
    up_sel <= p;
  end process;

It now scores 531 against Verilog's 504 — close, as a genuinely equivalent mutation across languages should be, and no longer equal to H6's 665.

A mutation is a claim about what was tested. If the mutation does not mean what its label says, the claim is false even when the number is large — and two mutations scoring identically is the cheapest available detector.

A third asymmetry was worth fixing while here. The VHDL bench drew its random masks as integer(r1*15.0), and VHDL's integer() rounds rather than truncates — so masks 0000 and 1111 each received half the probability of every other mask, while Verilog's %16 is uniform. The all-enabled and all-silent cases are not incidental here; they are the boundary of the sweep. Changing it to integer(floor(r1*16.0)) brought the VHDL reach numbers into line with the other two (one-talker 2519 vs 2537, multi-talker 1598 vs 1565), which is what makes the cross-language comparison in §13 meaningful at all.

16. A UVM Environment for a Multi-Port Fabric

The repeater is a genuinely good UVM candidate, and for a specific structural reason: it has N symmetric interfaces plus one asymmetric one. That is precisely the shape a parameterised agent array serves better than a directed testbench.

The agent is per-port, and instantiated NPORTS times:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class hub_port_item extends uvm_sequence_item;
  `uvm_object_utils(hub_port_item)
  rand bit enabled;     // is this port in the Enabled state
  rand bit driving;     // is the device below driving upstream
  function new(string name = "hub_port_item"); super.new(name); endfunction
endclass

class hub_port_agent extends uvm_agent;
  `uvm_component_utils(hub_port_agent)
  int unsigned port_id;                 // which downstream port this drives
  hub_port_driver    drv;
  hub_port_monitor   mon;
  uvm_sequencer #(hub_port_item) sqr;
  // ... build_phase / connect_phase as usual
endclass

The contention scenario is a virtual sequence, because it is a statement about several agents at once — which is exactly what a virtual sequence is for and what a per-port sequence cannot express:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class multi_talker_vseq extends uvm_sequence;
  `uvm_object_utils(multi_talker_vseq)
  hub_vsequencer vsqr;

  task body();
    hub_port_item a, b;
    // Two ports driving IN THE SAME CYCLE. A per-port sequence cannot
    // express this: the contention is a property of the pair, not of
    // either port, which is the entire reason this is a virtual sequence.
    fork
      begin a = hub_port_item::type_id::create("a");
            start_item(a, null, vsqr.port_sqr[0]);
            a.enabled = 1; a.driving = 1;
            finish_item(a, null); end
      begin b = hub_port_item::type_id::create("b");
            start_item(b, null, vsqr.port_sqr[2]);
            b.enabled = 1; b.driving = 1;
            finish_item(b, null); end
    join
  endtask
endclass

The scoreboard keeps its own isolation mask — §11's rule, restated in UVM terms, and the single most important line in the environment:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class hub_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(hub_scoreboard)
  uvm_analysis_imp #(hub_bus_txn, hub_scoreboard) ap;

  // The scoreboard's OWN copy. It is never assigned from the DUT's
  // port_isolated output; that output is COMPARED against this, never
  // copied into it. Chapter 17.3 measured the cost of getting this
  // backwards: four mutations each died by a single check.
  local bit [31:0] m_iso;
  local bit        m_multi, m_babble;

  function void write(hub_bus_txn t);
    bit [31:0] talkers = t.driving & t.enabled;
    bit [31:0] live    = talkers & ~m_iso;

    // THE invariant, checked on every transaction (section 3).
    if ($countones(t.up_sel) > 1)
      `uvm_error("HUB/MULTI", "two ports connected upstream simultaneously")

    if ($countones(talkers) == 1) begin
      if (t.up_sel !== live)
        `uvm_error("HUB/SEL", $sformatf(
          "single talker not connected: exp %0h got %0h", live, t.up_sel))
    end else begin
      if (t.up_sel !== '0)
        `uvm_error("HUB/SEL", $sformatf(
          "%0d talkers must connect NOBODY, got %0h",
          $countones(talkers), t.up_sel))
    end

    // advance our own state, from the observed inputs only
    if ($countones(talkers) > 1) begin
      m_iso |= talkers; m_multi = 1; m_babble = 1;
    end
    if (t.bus_reset) begin m_iso = '0; m_multi = 0; m_babble = 0; end

    if (t.port_isolated !== m_iso)
      `uvm_error("HUB/ISO", $sformatf(
        "isolation mask diverged: exp %0h got %0h", m_iso, t.port_isolated))
  endfunction
endclass

And the coverage model is where §14's hole would have been caught automatically:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
covergroup hub_cg with function sample(conn_e conn, bit dirty_reset);
  cp_conn : coverpoint conn {
    bins idle      = {C_IDLE};
    bins broadcast = {C_BROADCAST};
    bins upstream  = {C_UPSTREAM};
    bins fault     = {C_FAULT};
  }
  // A bus reset applied while something was actually isolated. Section 14's
  // hole is a hole in THIS bin: 512 resets had been applied and this bin
  // would have held 1.
  cp_dirty_reset : coverpoint dirty_reset { bins dirty = {1}; }
  x_conn_reset : cross cp_conn, cp_dirty_reset;
endgroup

17. Assertions

The invariant is a single-cycle property and states directly:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  // THE invariant: at most one port is ever connected upstream.
  property p_one_hot_upstream;
    @(posedge clk) disable iff (!rst_n)
      $countones(up_sel) <= 1;
  endproperty
  a_one_hot_upstream : assert property (p_one_hot_upstream)
    else $error("two ports connected upstream at once");

  // A port that is not enabled is on the bus in NEITHER direction.
  property p_disabled_is_isolated;
    @(posedge clk) disable iff (!rst_n)
      ((up_sel | dn_tx_en) & ~port_enabled) == '0;
  endproperty
  a_disabled_is_isolated : assert property (p_disabled_is_isolated);

  // Isolation is sticky and cumulative: it only ever grows, except on a
  // bus reset. This is H4 and H7 as one property.
  property p_isolation_monotone;
    @(posedge clk) disable iff (!rst_n || bus_reset)
      (port_isolated & $past(port_isolated)) == $past(port_isolated);
  endproperty
  a_isolation_monotone : assert property (p_isolation_monotone)
    else $error("an isolated port was released without a bus reset");

  // A fault is never silent: two talkers always raise the flag.
  property p_fault_reported;
    @(posedge clk) disable iff (!rst_n || bus_reset)
      ($countones(dn_rx_active & port_enabled) > 1) |=> multi_talker;
  endproperty
  a_fault_reported : assert property (p_fault_reported);

p_isolation_monotone is worth reading twice. It says the isolation mask never loses a bit, expressed as the intersection with its own previous value equals that previous value — a subset test written without a subset operator. It covers H4 and H7 together, and it is the property a formal tool would prove in seconds where §12's 6512-cycle simulation covers it statistically.

These were written but not simulated. Icarus Verilog does not support concurrent assertions, and no tool in this environment does. §20 states that plainly rather than implying coverage that does not exist.

18. Debugging: the Port That Went Quiet

The report: a 4-port hub. A keyboard on port 2 works for hours, then stops. Re-plugging it does nothing. Power-cycling the hub fixes it. Other ports keep working. No errors are logged anywhere.

The trap: every instinct says the keyboard or the port hardware. It is neither.

The procedure, using the signals this design exposes:

1. Read port_isolated. If bit 2 is set, the repeater cut the port off. The device is fine and the cable is fine; the hub is refusing to carry its traffic.

2. Read multi_talker and babble_port_dis. Both sticky, both set. This tells you the event already happened — possibly hours ago — and that the ports fell silent afterwards. A design that cleared these when the fault ended would leave you with an isolated port and no explanation.

3. Count the bits in port_isolated. If two bits are set, you have §3's real story: two ports drove simultaneously, the hub could not tell which one misbehaved, and it isolated both. The keyboard on port 2 may be the innocent party — the other isolated port is as likely to be the culprit, and the hub deliberately does not guess.

4. Explain the power-cycle. A hub power-cycle asserts bus reset, which is the only path that clears isolation (§7). That is why the fix works and why re-plugging the device does not: re-plugging changes nothing the repeater tracks.

5. Find which port is really at fault by re-enabling them one at a time through 18.2's port commands and watching which one re-triggers multi_talker.

Without port_isolated as an output, this procedure has no step 1, and the investigation starts at the keyboard and stays there.

19. Common Misconceptions

"A hub routes packets to the right port." It does not, and it cannot — it has no address-to-port mapping and nothing ever gives it one (§4). Downstream is a broadcast to every enabled port.

"The hub picks a winner when two devices talk." It connects neither (§3). One of the two is necessarily misbehaving, and picking would forward a wrong device's data to the host as though it were the right one.

"Two talkers is a bus contention to arbitrate." Arbitration is for parties with legitimate competing claims (17.3). Here the host addressed exactly one device, so at most one claim can be legitimate.

"Isolating both ports is over-reaction." The hub does not know which port was addressed — the host does. Isolating both and reporting is the only response that does not require information the hub lacks (§3).

"A disabled port just stops being serviced." If the disable is qualified in only one direction it can still inject upstream (H3, 4147 errors). A disable that is not enforced in both directions is a status bit (§7).

"The fault flags should clear when the fault ends." Then §18 has no step 2. The flags record that something happened, not that something is happening.

"The transaction translator is part of the repeater." It sits beside it (§5) and does not change the broadcast/select asymmetry at all.

"A killed mutation means that behaviour is verified." H7 was killed — by two errors, out of a single check, in a 6512-cycle run (§14).

"Exhaustive testing of the decision means the block is verified." All 512 connectivity points passed while the clear path was untested (§14). Exhaustive over one dimension says nothing about another.

20. Exercises

1. up_sel masks with ~disabled_r and requires one_talker, while talkers deliberately does not exclude isolated ports. Explain what breaks if talkers excludes them, and which mutation in §13 that change would make unkillable.

2. Extend the design so that isolating a port also raises a per-port event bit for 18.3's status-change endpoint. State what happens if the host reads the bitmap in the same cycle a new event sets a bit.

3. §15 found a VHDL mutation that duplicated another. Describe a mechanical check over a mutation matrix that flags this class of error, and say what it would produce as a false positive.

4. The SystemVerilog refuses to elaborate above NPORTS = 31. Find the constraint in 18.3's reporting mechanism that this number comes from, and determine whether a 40-port hub is possible at all.

5. Write the stimulus that would have made §14's cp_dirty_reset bin reach 164 without knowing H7 existed — that is, argue from the design's structure rather than from the mutation result.

6. A hub implements one TT per port rather than one shared (§5). Determine which of this chapter's signals change, and explain why the repeater is unaffected.

21. Summary

A hub is three devices in one package (§1) — a hub controller that is an addressable USB device, a repeater that carries traffic, and a transaction translator present only on high-speed hubs. Questions about "the hub" are usually ambiguous until you say which.

The repeater is asymmetric (§2): downstream is a broadcast to every enabled port, upstream is a select of exactly one. It broadcasts because it has no address-to-port mapping and never needs one (§4), which is what makes a hub cheap and stateless.

Two upstream talkers is not a race to arbitrate (§3). The host addressed exactly one device, so two means something is broken. The repeater connects neither, raises sticky flags, and isolates both — because it cannot tell which one misbehaved, and picking would deliver a wrong device's data to the host as a well-formed, uncheckable packet.

All three HDL implementations were simulated (§22) and seven mutations died in all three (§13), with the connectivity decision verified exhaustively over all 512 points (§12).

H7 was killed by two errors (§14). A green matrix, and a number one check away from a false pass: 512 bus resets had been applied and every one of them ran from an already-clean state, exactly 17.2's U6 shape. One stimulus change took it to 3431 — and took H1 from 483 to 1063 and H3 from 536 to 4147, because a run that saturates in a corner is weak everywhere.

A mutation also turned out not to be the mutation it was labelled (§15): VHDL's H2 was a duplicate of H6, so the chapter's central claim — that a priority encoder is caught — was unverified in one of three languages while the matrix looked complete. Two mutations scoring identically is the cheapest detector for this.

And the reference model keeps its own isolation mask (§11), never reading port_isolated — 17.3's lesson applied from the first line instead of discovered again at a cost of four mutations.

22. Tooling, Honestly

LanguageDesignTestbenchAnalysed / compiledSimulatedMutations
Verilog-2005usb_hub_repeaterrp_v_tb.v✅ Icarus -g2005✅ 0 errors, 512/512✅ all seven
SystemVerilogusb_hub_repeater_svrp_sv_tb.sv✅ Icarus -g2012✅ 0 errors, 512/512✅ all seven
VHDL-2008usb_hub_repeater_vhdlrp_vhdl_tb.vhd✅ nvc 1.23.0✅ 0 errors, 512/512✅ all seven
UVM (§16)——❌ no UVM-capable simulator here❌—
SVA (§17)——❌ unsupported by Icarus❌—

The UVM environment and the assertions in §16 and §17 were written and reviewed, not simulated. No tool in this environment runs either. They are included because they are the right way to verify this block at scale, and marked unsimulated because claiming otherwise would be the same failure §15 is about.

H7's SystemVerilog count (4420) exceeds the other two (3181, 3431), and the cause is the design's coding style rather than the testbench. The Verilog and VHDL keep the asynchronous reset and the bus reset in separate branches, so H7 removes the clear from the bus_reset branch only. The SystemVerilog merges them — if (!rst_n || bus_reset) — so the same one-line mutation removes both clears, and disabled_r is never initialised at all. It is therefore a strictly stronger mutation, not a differently-tested one, and it cannot be narrowed without restructuring the design.

That is worth noting rather than smoothing over. §15 caught a VHDL mutation that was weaker than its label; this is the same class of finding with the opposite sign, and the only reason it is benign is that a stronger mutation still dies. A mutation matrix compares implementations, and identical source edits do not always mean identical mutations.

23. What Comes Next

This chapter built the fabric and left one thing conspicuously unexplained: port_enabled is an input. Something decides which ports are enabled, and it is not the repeater.

That something is a per-port state machine, one instance per downstream port, and it is considerably more involved than a single enable bit suggests. A port is not merely on or off — it is disconnected, powered, resetting, enabled, suspended, or disabled for cause, and the transitions between those states are driven by three uncoordinated sources: the host issuing port commands, the device below attaching and detaching, and the repeater itself isolating a port for babbling (§3).

Chapter 18.2 — Port Management builds that state machine, and the first problem it has to solve is the one this chapter created: what happens when the host commands a port to enable in the same cycle the repeater is isolating it for a fault.

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.