Skip to content
VLSI Mentor

USB · Module 27

Host / Device / Hub Identification

Host and device take fifteen seconds; the hub is where the interview is decided — a hub is a repeater, not a switch, and no downstream port can ever reach another one.

Chapter 27.1 established that only the host may initiate. This chapter is the follow-up that answer invites, and it is the one most candidates get wrong.

1. The Question

"Name the roles on a USB bus and say what each one does."

Host and device take about fifteen seconds and almost nobody fumbles them. Then comes the hub, and the overwhelmingly common answer is some version of "a hub is a switch — it connects the devices to the host."

That answer is wrong in a way that matters, and the interviewer is asking precisely because it separates people who have read about USB from people who have debugged it.

2. Why It Has to Be That Way

This is not an arbitrary design choice — it falls directly out of chapter 27.1.

A switch needs to know which port a destination address lives on. Who would tell it? Addresses in USB are handed out by the host during enumeration, and the host talks to devices, not about them. A hub is never informed of any device's address, and it has no mechanism to ask.

More fundamentally, a switch exists to allow simultaneous conversations between different port pairs. On a host-scheduled bus there is only ever one conversation, because the host is one of the two parties in every single one. A crossbar would have nothing to switch.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   SWITCH                          HUB
   -------                         ---
   learns addresses                is told nothing
   N x N paths                     ONE path (up)
   simultaneous conversations      one conversation, ever
   forwards by destination         repeats to all / forwards up
   arbitrates contention           REPORTS contention

The last line is the one that matters most in practice, and section 12 is about it.

3. The Three Roles, Precisely

RoleMay initiate?Has an address?Decodes addresses?How many upstream ports
Hostyes — only itnon/a—
Deviceneveryes, assigned at enumerationyes, its own only1
Hubneveryes (it is also a device)no1

The row that surprises people is the hub's: it has an address, because a hub is also a device — it enumerates, it has a control endpoint, and the host configures it like anything else. But in its role as a repeater it decodes nothing. Those two jobs live in the same package and share almost no logic.

4. What We Are Building

usb_hub_route is the repeater half: four downstream ports, one upstream port, and the strict asymmetry above enforced as hardware rather than asserted as prose.

One path up, a broadcast down, and nothing sideways

A USB hub repeats host traffic to all enabled downstream ports and forwards one port's traffic upstream, with no path between downstream portsHostusb_hub_routePort 0Port 1Port 2Port 3one upstream portrepeatrepeatrepeatnot repeatedforward up12
The absent edges are the design. There is no route from any downstream port to any other, and no table that could create one.

Port 3 is enabled-off, and it receives nothing — not a repeated byte, not a token, nothing. Port 1 is the one talking this cycle and its bytes go up. Nothing in the diagram connects port 1 to port 0 or port 2, and nothing in the RTL does either.

Downstream is a broadcast; upstream is a funnel

A host byte repeated to three enabled ports, one port's byte forwarded upstream, and a two-talker collision that forwards nothingdownstream broadcastdownstream broadcastupstream forwardupstream forwardcollision reportedcollision reported5C repeated to ports 0,1,25C repeated to ports 0,1,2port 1 forwarded upstreamport 1 forwarded upstreamtwo talkers: report, forward nothingtwo talkers: report,forward nothingclkus_validus_data5C5C5C5C5C5C5C5C5Crpt_validds_validfwd_validfwd_data0000A1A1A1A1A1collisiont0t1t2t3t4t5t6t7t8
Cycle 1 repeats the host's byte 5C to all three live ports. Cycle 4 forwards port 1's byte A1 upstream. Cycle 7 is two talkers, which is a fault, not a choice.

rpt_valid reads 7 in cycle 1 — ports 0, 1 and 2, with port 3 disabled. One byte in, three copies out, no decision made about who it was for.

5. Seven Properties

#Property
1Downstream traffic is repeated to every live port and only live ports.
2A repeated byte is the upstream byte — never another port's payload.
3Exactly one live talker is forwarded upstream.
4Zero or more than one talker forwards nothing.
5The hub never originates a byte.
6A non-live port is not heard, and the attempt is counted.
7A port that transmits for BABBLE_N consecutive cycles is cut off, and stays cut off.

Property 2 is the negative claim this chapter exists for, and it is the one a data-comparison testbench cannot see: if the hub leaked port 0's bytes onto port 2, port 0's data would still have arrived upstream perfectly. It arrived in the right place and in a place it had no business being.

6. Verilog-2005 RTL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  usb_hub_route -- "Name the three USB roles" answered in hardware.
//
//  Host, device, hub. The first two are easy to say and the third is
//  where interviews are won, because almost everybody describes a hub
//  as a switch and a hub is not a switch:
//
//      Downstream traffic is REPEATED to every enabled port.
//      Upstream traffic from one port is FORWARDED to the one
//      upstream port.
//      No downstream port can ever reach another downstream port.
//
//  A switch learns addresses and builds paths between any two ports.
//  A hub has exactly one path, it is fixed, and it is a tree edge. The
//  asymmetry is the whole architecture: USB is a tree with the host at
//  the root, and a hub is the thing that makes a tree out of a wire.
// =====================================================================
module usb_hub_route #(
  parameter N_PORTS  = 4,
  parameter BABBLE_N = 8          // consecutive cycles before a port is cut off
) (
  input  wire                  clk,
  input  wire                  rst_n,

  // ---- the single upstream port: toward the host ----
  input  wire                  us_valid,
  input  wire [7:0]            us_data,

  // ---- the downstream ports: toward devices ----
  input  wire [N_PORTS-1:0]    ds_valid,
  input  wire [8*N_PORTS-1:0]  ds_data,
  input  wire [N_PORTS-1:0]    port_en,

  // ---- repeated downstream ----
  output wire [N_PORTS-1:0]    rpt_valid,
  output wire [8*N_PORTS-1:0]  rpt_data,

  // ---- forwarded upstream ----
  output wire                  fwd_valid,
  output wire [7:0]            fwd_data,

  // ---- a hub reports; it does not paper over ----
  output wire [N_PORTS-1:0]    port_disabled,
  output wire [31:0]           n_repeat,
  output wire [31:0]           n_forward,
  output wire [31:0]           n_collision,
  output wire [31:0]           n_blocked
);

  reg [N_PORTS-1:0]   rpt_valid_r;
  reg [8*N_PORTS-1:0] rpt_data_r;
  reg                 fwd_valid_r;
  reg [7:0]           fwd_data_r;
  reg [N_PORTS-1:0]   dis_r;
  reg [31:0]          rep_r, fwd_r, col_r, blk_r;

  // Per-port run length of consecutive upstream activity. A device that
  // transmits without stopping is "babbling" -- it has failed in a way
  // that would otherwise take the whole bus down with it, which is why
  // cutting it off is the hub's job and not the host's.
  reg [15:0] babble_r [0:N_PORTS-1];

  assign rpt_valid     = rpt_valid_r;
  assign rpt_data      = rpt_data_r;
  assign fwd_valid     = fwd_valid_r;
  assign fwd_data      = fwd_data_r;
  assign port_disabled = dis_r;
  assign n_repeat      = rep_r;
  assign n_forward     = fwd_r;
  assign n_collision   = col_r;
  assign n_blocked     = blk_r;

  // ---- which ports are actually live this cycle ----
  wire [N_PORTS-1:0] live = port_en & ~dis_r;

  // ---- who is talking upstream ----
  //
  // Counted, not selected. The host's schedule guarantees at most one
  // device transmits at a time, so two is not a case to arbitrate --
  // it is a fault, and a hub that quietly picks one has destroyed the
  // only evidence that the schedule was violated.
  integer j;
  reg [15:0] talkers;
  reg [7:0]  only_data;
  reg [15:0] only_idx;
  // Counted combinationally, NOT with a non-blocking add inside the
  // clocked loop: `blk_r <= blk_r + 1` in a for-loop is four
  // assignments to one register and the last one wins, so it adds at
  // most 1 per cycle however many ports were blocked.
  reg [15:0] n_blk_now;

  always @* begin
    talkers   = 16'd0;
    only_data = 8'd0;
    only_idx  = 16'd0;
    n_blk_now = 16'd0;
    for (j = 0; j < N_PORTS; j = j + 1) begin
      if (ds_valid[j] && live[j]) begin
        talkers   = talkers + 16'd1;
        only_data = ds_data[8*j +: 8];
        only_idx  = j[15:0];
      end else if (ds_valid[j]) begin
        // enabled-but-not-live, or disabled: heard by nobody
        n_blk_now = n_blk_now + 16'd1;
      end
    end
  end

  integer k;

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      rpt_valid_r <= {N_PORTS{1'b0}};
      rpt_data_r  <= {(8*N_PORTS){1'b0}};
      fwd_valid_r <= 1'b0;
      fwd_data_r  <= 8'd0;
      dis_r       <= {N_PORTS{1'b0}};
      rep_r       <= 32'd0;
      fwd_r       <= 32'd0;
      col_r       <= 32'd0;
      blk_r       <= 32'd0;
      for (k = 0; k < N_PORTS; k = k + 1) babble_r[k] <= 16'd0;
    end else begin
      // ---- DOWNSTREAM: repeat to every live port, and only those ----
      //
      // The same byte, to all of them, with no decision made about who
      // it is for. Address decoding is the DEVICE's job -- which is
      // exactly what chapter 27.1's token gate does -- and a hub that
      // tried to do it would need a device table it has no way to build.
      for (k = 0; k < N_PORTS; k = k + 1) begin
        if (us_valid && live[k]) begin
          rpt_valid_r[k]        <= 1'b1;
          rpt_data_r[8*k +: 8]  <= us_data;
        end else begin
          rpt_valid_r[k]        <= 1'b0;
          rpt_data_r[8*k +: 8]  <= 8'd0;
        end
      end
      if (us_valid) rep_r <= rep_r + 32'd1;

      // ---- UPSTREAM: forward the one talker, report any second ----
      fwd_valid_r <= 1'b0;
      fwd_data_r  <= 8'd0;

      if (talkers == 16'd1) begin
        fwd_valid_r <= 1'b1;
        fwd_data_r  <= only_data;
        fwd_r       <= fwd_r + 32'd1;
      end else if (talkers > 16'd1) begin
        // A collision. Nothing is forwarded -- forwarding either byte
        // would present the host with a transaction that never happened.
        col_r <= col_r + 32'd1;
      end

      // ---- a port that is not live is not heard ----
      //
      // One add of the combinational count, because a per-port add with
      // a non-blocking assignment would be four writes to one register.
      if (n_blk_now != 16'd0) blk_r <= blk_r + {16'd0, n_blk_now};

      // ---- babble: a device that will not stop talking ----
      for (k = 0; k < N_PORTS; k = k + 1) begin
        if (ds_valid[k] && live[k]) begin
          if (babble_r[k] >= BABBLE_N - 1) dis_r[k]    <= 1'b1;
          else                             babble_r[k] <= babble_r[k] + 16'd1;
        end else begin
          babble_r[k] <= 16'd0;
        end
      end
    end
  end

endmodule

7. SystemVerilog RTL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  usb_hub_route -- SystemVerilog.
//
//  One structural change from the Verilog, and it is the reason to
//  prefer SystemVerilog for anything with per-port state: the byte
//  lanes are an UNPACKED ARRAY of bytes rather than a flat vector
//  sliced with +:. `ds_data[p]` cannot be off by a factor of eight,
//  which is a whole class of bug that simply does not arise.
// =====================================================================
module usb_hub_route #(
  parameter int N_PORTS  = 4,
  parameter int BABBLE_N = 8
) (
  input  logic                clk,
  input  logic                rst_n,

  input  logic                us_valid,
  input  logic [7:0]          us_data,

  input  logic [N_PORTS-1:0]  ds_valid,
  input  logic [7:0]          ds_data [N_PORTS],
  input  logic [N_PORTS-1:0]  port_en,

  output logic [N_PORTS-1:0]  rpt_valid,
  output logic [7:0]          rpt_data [N_PORTS],

  output logic                fwd_valid,
  output logic [7:0]          fwd_data,

  output logic [N_PORTS-1:0]  port_disabled,
  output logic [31:0]         n_repeat,
  output logic [31:0]         n_forward,
  output logic [31:0]         n_collision,
  output logic [31:0]         n_blocked
);

  logic [31:0] rep_r, fwd_r, col_r, blk_r;
  logic [15:0] babble_r [N_PORTS];

  assign n_repeat    = rep_r;
  assign n_forward   = fwd_r;
  assign n_collision = col_r;
  assign n_blocked   = blk_r;

  // Ports that are enabled AND have not been cut off for babbling.
  wire [N_PORTS-1:0] live = port_en & ~port_disabled;

  // ---- who is talking, counted rather than selected ----
  //
  // The host's schedule guarantees at most one. Two is therefore not a
  // case to arbitrate but a fault, and a hub that picks one has erased
  // the only evidence that the schedule was broken.
  int unsigned talkers;
  logic [7:0]  only_data;
  int unsigned n_blk_now;

  always_comb begin
    talkers   = 0;
    only_data = '0;
    n_blk_now = 0;
    for (int j = 0; j < N_PORTS; j++) begin
      if (ds_valid[j] && live[j]) begin
        talkers++;
        only_data = ds_data[j];
      end else if (ds_valid[j]) begin
        n_blk_now++;
      end
    end
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      rpt_valid     <= '0;
      fwd_valid     <= 1'b0;
      fwd_data      <= '0;
      port_disabled <= '0;
      rep_r <= '0;  fwd_r <= '0;  col_r <= '0;  blk_r <= '0;
      for (int k = 0; k < N_PORTS; k++) begin
        rpt_data[k]  <= '0;
        babble_r[k]  <= '0;
      end
    end else begin
      // ---- DOWNSTREAM: repeated to every live port, unchanged ----
      //
      // The same byte to all of them, with no decision about who it is
      // for. Address decoding belongs to the device.
      for (int k = 0; k < N_PORTS; k++) begin
        if (us_valid && live[k]) begin
          rpt_valid[k] <= 1'b1;
          rpt_data[k]  <= us_data;
        end else begin
          rpt_valid[k] <= 1'b0;
          rpt_data[k]  <= '0;
        end
      end
      if (us_valid) rep_r <= rep_r + 32'd1;

      // ---- UPSTREAM: forward the one talker, report any second ----
      fwd_valid <= 1'b0;
      fwd_data  <= '0;

      if (talkers == 1) begin
        fwd_valid <= 1'b1;
        fwd_data  <= only_data;
        fwd_r     <= fwd_r + 32'd1;
      end else if (talkers > 1) begin
        col_r <= col_r + 32'd1;
      end

      // One add of the combinational count. A per-port add inside this
      // loop would be N writes to one register with the last winning.
      if (n_blk_now != 0) blk_r <= blk_r + 32'(n_blk_now);

      // ---- babble: the one decision a hub makes on its own ----
      for (int k = 0; k < N_PORTS; k++) begin
        if (ds_valid[k] && live[k]) begin
          if (babble_r[k] >= BABBLE_N - 1) port_disabled[k] <= 1'b1;
          else                             babble_r[k]      <= babble_r[k] + 16'd1;
        end else begin
          babble_r[k] <= '0;
        end
      end
    end
  end

endmodule

8. VHDL-2008 RTL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- =====================================================================
--  usb_hub_route -- VHDL-2008.
--
--  VHDL gets the per-port bytes right the same way SystemVerilog does,
--  with an array of bytes rather than a flat vector -- and unlike both
--  Verilog dialects it will refuse to compile an index that is out of
--  range rather than quietly returning the wrong eight bits.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

package hr_pkg is
  type byte_array is array (natural range <>) of std_logic_vector(7 downto 0);
end package;

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

entity usb_hub_route is
  generic (
    N_PORTS  : natural := 4;
    BABBLE_N : natural := 8
  );
  port (
    clk           : in  std_logic;
    rst_n         : in  std_logic;

    us_valid      : in  std_logic;
    us_data       : in  std_logic_vector(7 downto 0);

    ds_valid      : in  std_logic_vector(N_PORTS-1 downto 0);
    ds_data       : in  byte_array(0 to N_PORTS-1);
    port_en       : in  std_logic_vector(N_PORTS-1 downto 0);

    rpt_valid     : out std_logic_vector(N_PORTS-1 downto 0);
    rpt_data      : out byte_array(0 to N_PORTS-1);

    fwd_valid     : out std_logic;
    fwd_data      : out std_logic_vector(7 downto 0);

    port_disabled : out std_logic_vector(N_PORTS-1 downto 0);
    n_repeat      : out std_logic_vector(31 downto 0);
    n_forward     : out std_logic_vector(31 downto 0);
    n_collision   : out std_logic_vector(31 downto 0);
    n_blocked     : out std_logic_vector(31 downto 0)
  );
end entity;

architecture rtl of usb_hub_route is
  signal dis_r  : std_logic_vector(N_PORTS-1 downto 0) := (others => '0');
  signal live   : std_logic_vector(N_PORTS-1 downto 0);

  signal rep_r, fwd_r, col_r, blk_r : unsigned(31 downto 0) := (others => '0');

  type cnt_array is array (natural range <>) of natural;
  signal babble_r : cnt_array(0 to N_PORTS-1) := (others => 0);

  signal talkers   : natural;
  signal only_data : std_logic_vector(7 downto 0);
  signal n_blk_now : natural;
begin

  live          <= port_en and not dis_r;
  port_disabled <= dis_r;

  n_repeat    <= std_logic_vector(rep_r);
  n_forward   <= std_logic_vector(fwd_r);
  n_collision <= std_logic_vector(col_r);
  n_blocked   <= std_logic_vector(blk_r);

  -- ---- who is talking, counted rather than selected ----
  --
  -- Two talkers is a fault, not a case to arbitrate, so the count is
  -- kept and a hub that picked one would have destroyed the evidence.
  count_talkers : process(ds_valid, ds_data, live)
    variable t  : natural;
    variable od : std_logic_vector(7 downto 0);
    variable nb : natural;
  begin
    t  := 0;
    od := (others => '0');
    nb := 0;
    for j in 0 to N_PORTS-1 loop
      if ds_valid(j) = '1' and live(j) = '1' then
        t  := t + 1;
        od := ds_data(j);
      elsif ds_valid(j) = '1' then
        nb := nb + 1;
      end if;
    end loop;
    talkers   <= t;
    only_data <= od;
    n_blk_now <= nb;
  end process;

  main : process(clk, rst_n)
  begin
    if rst_n = '0' then
      rpt_valid <= (others => '0');
      rpt_data  <= (others => (others => '0'));
      fwd_valid <= '0';
      fwd_data  <= (others => '0');
      dis_r     <= (others => '0');
      rep_r     <= (others => '0');
      fwd_r     <= (others => '0');
      col_r     <= (others => '0');
      blk_r     <= (others => '0');
      babble_r  <= (others => 0);

    elsif rising_edge(clk) then
      -- ---- DOWNSTREAM: repeated to every live port, unchanged ----
      for k in 0 to N_PORTS-1 loop
        if us_valid = '1' and live(k) = '1' then
          rpt_valid(k) <= '1';
          rpt_data(k)  <= us_data;
        else
          rpt_valid(k) <= '0';
          rpt_data(k)  <= (others => '0');
        end if;
      end loop;
      if us_valid = '1' then rep_r <= rep_r + 1; end if;

      -- ---- UPSTREAM: forward the one talker, report any second ----
      fwd_valid <= '0';
      fwd_data  <= (others => '0');

      if talkers = 1 then
        fwd_valid <= '1';
        fwd_data  <= only_data;
        fwd_r     <= fwd_r + 1;
      elsif talkers > 1 then
        col_r <= col_r + 1;
      end if;

      -- One add of the combinational count, not one per port: a
      -- per-port signal assignment in this loop would be N drives of
      -- one signal and only the last would take effect.
      if n_blk_now /= 0 then
        blk_r <= blk_r + to_unsigned(n_blk_now, 32);
      end if;

      -- ---- babble: the one decision a hub makes on its own ----
      for k in 0 to N_PORTS-1 loop
        if ds_valid(k) = '1' and live(k) = '1' then
          if babble_r(k) >= BABBLE_N - 1 then
            dis_r(k) <= '1';
          else
            babble_r(k) <= babble_r(k) + 1;
          end if;
        else
          babble_r(k) <= 0;
        end if;
      end loop;
    end if;
  end process;

end architecture;

9. The Testbench

Three things in it are worth more than the rest.

The payload names its own port. Each port sends the byte 0xA0 + port, so a byte appearing where it should not says exactly which port it escaped from. Without that, a crossbar leak is an anonymous wrong value.

The crossing counter. n_cross counts every cycle in which a repeated byte was not the upstream byte, plus every quiet port carrying a byte that matches some other port's payload. It is asserted against zero on every step, like n_unsolicited in chapter 27.1, because a negative property needs a number rather than an absence.

Run-wide totals separate from the DUT's. The design's counters are cleared by every reset, and the random phase resets every 64 cycles to clear accumulated babble cutoffs. A summary line that printed the DUT's counters would therefore describe the last 64 cycles — and the first version of this bench did exactly that, reporting blocked=0 for a run with 1808 of them.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   First summary line published by this bench:

     [hub] repeats=30 forwards=26 collisions=3 blocked=0

   Actual run totals:

     [hub] repeats=20260 forwards=26596 collisions=2592 blocked=1808

   Both are true. One describes the run; the other
   describes the 64 cycles after the last reset.

Verilog-2005 testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  Testbench for usb_hub_route.
//
//  The property that matters most is a NEGATIVE one and it is not the
//  obvious one: no downstream port may ever reach another downstream
//  port. A hub that leaked port 0's bytes onto port 2 would still pass
//  every "did the data arrive" check, because the data DID arrive --
//  upstream, correctly, as well as in a place it had no business being.
//
//  So the bench checks each repeated byte against the UPSTREAM byte,
//  and separately proves no repeated byte ever equals a downstream
//  port's payload when it should not.
// =====================================================================
`timescale 1ns/1ps
module tb_hr_v;

  localparam N_PORTS  = 4;
  localparam BABBLE_N = 8;

  reg                   clk = 1'b0, rst_n = 1'b0;
  reg                   us_valid = 1'b0;
  reg  [7:0]            us_data  = 8'd0;
  reg  [N_PORTS-1:0]    ds_valid = 4'd0;
  reg  [8*N_PORTS-1:0]  ds_data  = 32'd0;
  reg  [N_PORTS-1:0]    port_en  = 4'hF;

  wire [N_PORTS-1:0]    rpt_valid;
  wire [8*N_PORTS-1:0]  rpt_data;
  wire                  fwd_valid;
  wire [7:0]            fwd_data;
  wire [N_PORTS-1:0]    port_disabled;
  wire [31:0]           n_repeat, n_forward, n_collision, n_blocked;

  usb_hub_route #(.N_PORTS(N_PORTS), .BABBLE_N(BABBLE_N)) dut (
    .clk(clk), .rst_n(rst_n),
    .us_valid(us_valid), .us_data(us_data),
    .ds_valid(ds_valid), .ds_data(ds_data), .port_en(port_en),
    .rpt_valid(rpt_valid), .rpt_data(rpt_data),
    .fwd_valid(fwd_valid), .fwd_data(fwd_data),
    .port_disabled(port_disabled),
    .n_repeat(n_repeat), .n_forward(n_forward),
    .n_collision(n_collision), .n_blocked(n_blocked)
  );

  always #5 clk = ~clk;

  integer errors = 0, checks = 0, steps = 0;

  // ---- shadow state, written independently of the design ----
  reg  [N_PORTS-1:0] s_dis;
  integer            s_bab [0:N_PORTS-1];
  reg  [31:0]        s_rep, s_fwd, s_col, s_blk;

  // ---- the headline negative counter ----
  integer n_cross = 0;       // a downstream port reached another one

  // Run-wide totals. The DUT's own counters are cleared by every
  // reset, so they describe a window rather than the run -- and a
  // summary line that quietly reports the last window is how a
  // suite ends up publishing `blocked=0` for a run with thousands.
  integer g_rep = 0, g_fwd = 0, g_col = 0, g_blk = 0, g_dis = 0;

  // ---- exhaustive reach over (talkers, enables, upstream) ----
  reg reach [0:511];
  integer ri, n_reach;

  integer seed;

  // ---- $random is SIGNED ----
  //
  // `$random % 4` is negative half the time, and `1 << negative` shifts
  // by a huge unsigned amount and yields ZERO. The random phase below
  // intends exactly one talker per cycle and was silently getting none
  // on half of them -- a 55% forwarding rate where 87% was intended.
  //
  // Masking off the sign bit is the whole fix.
  function [31:0] urand;
    input dummy;
    begin urand = $random(seed) & 32'h3FFF_FFFF; end
  endfunction

  task ck(input cond, input [255:0] what);
    begin
      checks = checks + 1;
      if (!cond) begin
        errors = errors + 1;
        if (errors <= 20)
          $display("  ERROR @%0t step=%0d: %0s", $time, steps, what);
      end
    end
  endtask

  integer p, q, rp, talkers, only_p;

  task step(input uv, input [7:0] ud,
            input [N_PORTS-1:0] dv, input [8*N_PORTS-1:0] dd,
            input [N_PORTS-1:0] pe);
    reg [N_PORTS-1:0] s_live;
    reg               e_fwd_valid;
    reg [7:0]         e_fwd_data;
    reg [N_PORTS-1:0] e_rpt_valid;
    // PRIVATE loop counters. Sharing `p` with the calling phase loop is
    // silent and total: step() runs its own for-loop to N_PORTS, the
    // caller's index comes back as 4, and a four-iteration phase runs
    // exactly once. It cost a babble count of 1 where 4 was expected.
    integer sp, sq;
    begin
      us_valid = uv;  us_data = ud;
      ds_valid = dv;  ds_data = dd;  port_en = pe;

      // ---- what SHOULD happen, computed from the shadow ----
      s_live = pe & ~s_dis;

      // exactly one live talker is forwarded; anything else is not
      talkers = 0; only_p = 0;
      for (sp = 0; sp < N_PORTS; sp = sp + 1)
        if (dv[sp] && s_live[sp]) begin talkers = talkers + 1; only_p = sp; end

      e_fwd_valid = (talkers == 1);
      e_fwd_data  = (talkers == 1) ? dd[8*only_p +: 8] : 8'd0;

      // downstream is repeated to every live port and no other
      for (sp = 0; sp < N_PORTS; sp = sp + 1)
        e_rpt_valid[sp] = uv && s_live[sp];

      // shadow counters
      if (uv)          begin s_rep = s_rep + 1; g_rep = g_rep + 1; end
      if (talkers == 1) begin s_fwd = s_fwd + 1; g_fwd = g_fwd + 1; end
      if (talkers >  1) begin s_col = s_col + 1; g_col = g_col + 1; end
      for (sp = 0; sp < N_PORTS; sp = sp + 1)
        if (dv[sp] && !s_live[sp]) begin s_blk = s_blk + 1; g_blk = g_blk + 1; end

      @(posedge clk);
      #1;
      steps = steps + 1;

      // ---- PROPERTY 1: repeated to every live port, and only those ----
      ck(rpt_valid === e_rpt_valid, "downstream repeat reached the wrong set of ports");

      // ---- PROPERTY 2: the repeated byte is the UPSTREAM byte ----
      //
      // This is the no-crossbar check. If the hub were a switch, some
      // port's output would carry another port's payload, and the only
      // way to notice is to compare against what came from the host.
      for (sp = 0; sp < N_PORTS; sp = sp + 1) begin
        if (e_rpt_valid[sp]) begin
          ck(rpt_data[8*sp +: 8] === ud,
             "a repeated byte was not the upstream byte");
          if (rpt_data[8*sp +: 8] !== ud) n_cross = n_cross + 1;
        end else begin
          ck(rpt_data[8*sp +: 8] === 8'd0,
             "a disabled or idle port was driven with data");
          // A quiet port carrying another port's payload is the exact
          // shape of the switch mistake, so it is counted by name.
          for (sq = 0; sq < N_PORTS; sq = sq + 1)
            if (sq != sp && dv[sq] && rpt_data[8*sp +: 8] === dd[8*sq +: 8]
                       && dd[8*sq +: 8] !== 8'd0)
              n_cross = n_cross + 1;
        end
      end

      // ---- PROPERTY 3: exactly one talker is forwarded ----
      ck(fwd_valid === e_fwd_valid, "upstream forward disagrees");
      if (e_fwd_valid) ck(fwd_data === e_fwd_data, "wrong byte forwarded upstream");
      else             ck(fwd_data === 8'd0, "a byte was forwarded with no single talker");

      // ---- PROPERTY 4: the counters agree ----
      ck(n_repeat    === s_rep, "repeat count disagrees");
      ck(n_forward   === s_fwd, "forward count disagrees");
      ck(n_collision === s_col, "collision count disagrees");
      ck(n_blocked   === s_blk, "blocked count disagrees");

      // ---- PROPERTY 5: the hub never originates ----
      ck(!(fwd_valid && talkers == 0),
         "the hub forwarded a byte nobody sent");

      // ---- babble: advance the shadow, then compare ----
      for (sp = 0; sp < N_PORTS; sp = sp + 1) begin
        if (dv[sp] && s_live[sp]) begin
          if (s_bab[sp] >= BABBLE_N - 1) begin
            if (!s_dis[sp]) g_dis = g_dis + 1;
            s_dis[sp] = 1'b1;
          end
          else                          s_bab[sp] = s_bab[sp] + 1;
        end else begin
          s_bab[sp] = 0;
        end
      end
      ck(port_disabled === s_dis, "disabled-port set disagrees");
      for (sp = 0; sp < N_PORTS; sp = sp + 1)
        if (s_dis[sp] && !dv[sp]) ; // no-op: counted once at cutoff below
      ck(n_cross == 0, "a downstream port reached another downstream port");
    end
  endtask

  task reset_dut;
    begin
      rst_n = 1'b0;
      us_valid = 1'b0; ds_valid = 4'd0; port_en = 4'hF;
      @(posedge clk); @(posedge clk);
      rst_n = 1'b1;
      s_dis = 4'd0;
      for (rp = 0; rp < N_PORTS; rp = rp + 1) s_bab[rp] = 0;
      s_rep = 0; s_fwd = 0; s_col = 0; s_blk = 0;
      @(posedge clk); #1;
    end
  endtask

  integer ti, ei, ui, k;
  reg [8*N_PORTS-1:0] dd;

  initial begin
    for (ri = 0; ri < 512; ri = ri + 1) reach[ri] = 1'b0;
    seed = 32'd27002;

    // =============================================================
    //  PHASE 1 (DIRECTED, EXHAUSTIVE) -- every talker set against
    //  every enable set, with and without downstream traffic.
    //  16 x 16 x 2 = 512.
    // =============================================================
    reset_dut;
    for (ti = 0; ti < 16; ti = ti + 1)
    for (ei = 0; ei < 16; ei = ei + 1)
    for (ui = 0; ui < 2; ui = ui + 1) begin
      // Each port sends a byte that identifies it, so a byte appearing
      // in the wrong place says exactly which port it escaped from.
      dd = 32'd0;
      for (p = 0; p < N_PORTS; p = p + 1)
        dd[8*p +: 8] = 8'hA0 + p[7:0];

      step(ui[0], 8'h5C, ti[3:0], dd, ei[3:0]);
      // an idle cycle, so the babble counters cannot accumulate across
      // unrelated scenarios
      step(1'b0, 8'd0, 4'd0, 32'd0, ei[3:0]);

      ri = (ti << 5) | (ei << 1) | ui;
      reach[ri] = 1'b1;
    end

    // =============================================================
    //  PHASE 2 (DIRECTED) -- babble, at every port.
    //
    //  A device that transmits and never stops would take the whole
    //  bus down. Cutting it off is the hub's job, and it is the one
    //  decision a hub makes on its own.
    // =============================================================
    for (p = 0; p < N_PORTS; p = p + 1) begin
      reset_dut;
      // Per-port payloads written out explicitly rather than sliced from
      // one 32-bit literal, so that all three language benches drive
      // provably identical bytes. A flat literal means byte 0 is the LOW
      // eight bits in Verilog and index 0 in an array language, and the
      // two benches then run different experiments.
      for (rp = 0; rp < N_PORTS; rp = rp + 1) dd[8*rp +: 8] = 8'hD0 + rp[7:0];
      // Held well past the cutoff, because B6 (a port that is never cut
      // off) diverges for as many cycles as the port keeps talking. Four
      // cycles past the threshold left the score dominated by a 1-in-16000
      // random event and the three language columns 27x apart.
      for (k = 0; k < BABBLE_N + 24; k = k + 1)
        step(1'b0, 8'd0, (4'h1 << p), dd, 4'hF);
      // ---- and a cut-off port STAYS cut off ----
      //
      // Going quiet does not earn it a second chance. A hub that
      // re-enabled a port on silence would re-admit the same broken
      // device every time it paused.
      for (k = 0; k < 4; k = k + 1)
        step(1'b0, 8'd0, 4'd0, dd, 4'hF);
      for (k = 0; k < 8; k = k + 1)
        step(1'b0, 8'd0, (4'h1 << p), dd, 4'hF);
      // and the other ports must still work afterwards
      for (rp = 0; rp < N_PORTS; rp = rp + 1) dd[8*rp +: 8] = 8'h11 + rp[7:0];
      for (q = 0; q < N_PORTS; q = q + 1)
        if (q != p) step(1'b1, 8'h33, (4'h1 << q), dd, 4'hF);
    end

    // =============================================================
    //  PHASE 3 (DIRECTED) -- collisions at every pair of ports.
    //
    //  Exhaustive over the 6 unordered pairs, because "two talkers"
    //  is not one situation: a hub that arbitrated by index would
    //  pass a test that only ever collided ports 0 and 1.
    // =============================================================
    reset_dut;
    for (p = 0; p < N_PORTS; p = p + 1)
    for (q = 0; q < N_PORTS; q = q + 1)
      if (p < q) begin
        dd = 32'd0;
        dd[8*p +: 8] = 8'h70 + p[7:0];
        dd[8*q +: 8] = 8'h70 + q[7:0];
        step(1'b0, 8'd0, (4'h1 << p) | (4'h1 << q), dd, 4'hF);
        step(1'b0, 8'd0, 4'd0, 32'd0, 4'hF);
      end

    // =============================================================
    //  PHASE 4 (RANDOM) -- a busy tree.
    // =============================================================
`ifndef DIRECTED_ONLY
    reset_dut;
    for (k = 0; k < 30000; k = k + 1) begin
      dd = 32'd0;
      for (p = 0; p < N_PORTS; p = p + 1)
        dd[8*p +: 8] = ($random(seed) & 8'hFF);
      // mostly one talker, because that is what a scheduled bus looks
      // like; occasionally none, occasionally two
      step(((urand(0) % 3) != 0),
           ($random(seed) & 8'hFF),
           ((urand(0) % 8) == 0) ? ($random(seed) & 4'hF)
                                      : (4'h1 << (urand(0) % 4)),
           dd,
           ((urand(0) % 16) == 0) ? ($random(seed) & 4'hF) : 4'hF);
      if ((k % 64) == 63) reset_dut;   // clear accumulated babble cutoffs
    end
`endif

    n_reach = 0;
    for (ri = 0; ri < 512; ri = ri + 1) if (reach[ri]) n_reach = n_reach + 1;

    $display("steps=%0d checks=%0d reach=%0d/512 errors=%0d",
             steps, checks, n_reach, errors);
    $display("[hub] repeats=%0d forwards=%0d collisions=%0d blocked=%0d",
             g_rep, g_fwd, g_col, g_blk);
    $display("[hub] babble cutoffs=%0d", g_dis);
    $display("[the whole point] downstream-to-downstream crossings = %0d", n_cross);
    if (n_reach != 512) begin
      $display("FAIL: exhaustive sweep incomplete"); errors = errors + 1;
    end
    if (errors == 0) $display("PASS: 0 errors in %0d checks", checks);
    else             $display("FAIL: %0d errors in %0d checks", errors, checks);
    $finish;
  end

endmodule

SystemVerilog testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  Testbench for usb_hub_route.
//
//  The property that matters most is a NEGATIVE one and it is not the
//  obvious one: no downstream port may ever reach another downstream
//  port. A hub that leaked port 0's bytes onto port 2 would still pass
//  every "did the data arrive" check, because the data DID arrive --
//  upstream, correctly, as well as in a place it had no business being.
//
//  So the bench checks each repeated byte against the UPSTREAM byte,
//  and separately proves no repeated byte ever equals a downstream
//  port's payload when it should not.
// =====================================================================
`timescale 1ns/1ps
module tb_hr_sv;

  localparam N_PORTS  = 4;
  localparam BABBLE_N = 8;

  logic                 clk = 1'b0, rst_n = 1'b0;
  logic                 us_valid = 1'b0;
  logic [7:0]           us_data  = 8'd0;
  logic [N_PORTS-1:0]   ds_valid = 4'd0;
  // Driven directly by the phase code rather than passed into step():
  // Icarus rejects an unpacked array as a subroutine port AND rejects
  // whole-array assignment, so the array itself is the interface.
  logic [7:0]           ds_data [N_PORTS];
  logic [N_PORTS-1:0]   port_en  = 4'hF;

  logic [N_PORTS-1:0]   rpt_valid;
  logic [7:0]           rpt_data [N_PORTS];
  logic                 fwd_valid;
  logic [7:0]           fwd_data;
  logic [N_PORTS-1:0]   port_disabled;
  logic [31:0]          n_repeat, n_forward, n_collision, n_blocked;

  usb_hub_route #(.N_PORTS(N_PORTS), .BABBLE_N(BABBLE_N)) dut (
    .clk(clk), .rst_n(rst_n),
    .us_valid(us_valid), .us_data(us_data),
    .ds_valid(ds_valid), .ds_data(ds_data), .port_en(port_en),
    .rpt_valid(rpt_valid), .rpt_data(rpt_data),
    .fwd_valid(fwd_valid), .fwd_data(fwd_data),
    .port_disabled(port_disabled),
    .n_repeat(n_repeat), .n_forward(n_forward),
    .n_collision(n_collision), .n_blocked(n_blocked)
  );

  always #5 clk = ~clk;

  integer errors = 0, checks = 0, steps = 0;

  // ---- shadow state, written independently of the design ----
  logic [N_PORTS-1:0] s_dis;
  integer            s_bab [0:N_PORTS-1];
  logic [31:0]        s_rep, s_fwd, s_col, s_blk;

  // ---- the headline negative counter ----
  integer n_cross = 0;       // a downstream port reached another one

  // Run-wide totals. The DUT's own counters are cleared by every
  // reset, so they describe a window rather than the run -- and a
  // summary line that quietly reports the last window is how a
  // suite ends up publishing `blocked=0` for a run with thousands.
  integer g_rep = 0, g_fwd = 0, g_col = 0, g_blk = 0, g_dis = 0;

  // ---- exhaustive reach over (talkers, enables, upstream) ----
  logic reach [0:511];
  integer ri, n_reach;

  integer seed;

  // ---- $random is SIGNED ----
  //
  // `$random % 4` is negative half the time, and `1 << negative` shifts
  // by a huge unsigned amount and yields ZERO. The random phase below
  // intends exactly one talker per cycle and was silently getting none
  // on half of them -- a 55% forwarding rate where 87% was intended.
  //
  // Masking off the sign bit is the whole fix.
  function automatic [31:0] urand(bit dummy);
    return $random(seed) & 32'h3FFF_FFFF;
  endfunction

  task ck(input logic cond, input logic [255:0] what);
    begin
      checks = checks + 1;
      if (!cond) begin
        errors = errors + 1;
        if (errors <= 20)
          $display("  ERROR @%0t step=%0d: %0s", $time, steps, what);
      end
    end
  endtask

  integer p, q, rp, talkers, only_p;

  task step(input logic uv, input logic [7:0] ud,
            input logic [N_PORTS-1:0] dv,
            input logic [N_PORTS-1:0] pe);
    logic [N_PORTS-1:0] s_live;
    logic             e_fwd_valid;
    logic [7:0]       e_fwd_data;
    logic [N_PORTS-1:0] e_rpt_valid;
    // PRIVATE loop counters. Sharing `p` with the calling phase loop is
    // silent and total: step() runs its own for-loop to N_PORTS, the
    // caller's index comes back as 4, and a four-iteration phase runs
    // exactly once. It cost a babble count of 1 where 4 was expected.
    integer sp, sq;
    begin
      us_valid = uv;  us_data = ud;
      ds_valid = dv;  port_en = pe;   // ds_data is driven by the caller

      // ---- what SHOULD happen, computed from the shadow ----
      s_live = pe & ~s_dis;

      // exactly one live talker is forwarded; anything else is not
      talkers = 0; only_p = 0;
      for (sp = 0; sp < N_PORTS; sp = sp + 1)
        if (dv[sp] && s_live[sp]) begin talkers = talkers + 1; only_p = sp; end

      e_fwd_valid = (talkers == 1);
      e_fwd_data  = (talkers == 1) ? ds_data[only_p] : 8'd0;

      // downstream is repeated to every live port and no other
      for (sp = 0; sp < N_PORTS; sp = sp + 1)
        e_rpt_valid[sp] = uv && s_live[sp];

      // shadow counters
      if (uv)          begin s_rep = s_rep + 1; g_rep = g_rep + 1; end
      if (talkers == 1) begin s_fwd = s_fwd + 1; g_fwd = g_fwd + 1; end
      if (talkers >  1) begin s_col = s_col + 1; g_col = g_col + 1; end
      for (sp = 0; sp < N_PORTS; sp = sp + 1)
        if (dv[sp] && !s_live[sp]) begin s_blk = s_blk + 1; g_blk = g_blk + 1; end

      @(posedge clk);
      #1;
      steps = steps + 1;

      // ---- PROPERTY 1: repeated to every live port, and only those ----
      ck(rpt_valid === e_rpt_valid, "downstream repeat reached the wrong set of ports");

      // ---- PROPERTY 2: the repeated byte is the UPSTREAM byte ----
      //
      // This is the no-crossbar check. If the hub were a switch, some
      // port's output would carry another port's payload, and the only
      // way to notice is to compare against what came from the host.
      for (sp = 0; sp < N_PORTS; sp = sp + 1) begin
        if (e_rpt_valid[sp]) begin
          ck(rpt_data[sp] === ud,
             "a repeated byte was not the upstream byte");
          if (rpt_data[sp] !== ud) n_cross = n_cross + 1;
        end else begin
          ck(rpt_data[sp] === 8'd0,
             "a disabled or idle port was driven with data");
          // A quiet port carrying another port's payload is the exact
          // shape of the switch mistake, so it is counted by name.
          for (sq = 0; sq < N_PORTS; sq = sq + 1)
            if (sq != sp && dv[sq] && rpt_data[sp] === ds_data[sq]
                       && ds_data[sq] !== 8'd0)
              n_cross = n_cross + 1;
        end
      end

      // ---- PROPERTY 3: exactly one talker is forwarded ----
      ck(fwd_valid === e_fwd_valid, "upstream forward disagrees");
      if (e_fwd_valid) ck(fwd_data === e_fwd_data, "wrong byte forwarded upstream");
      else             ck(fwd_data === 8'd0, "a byte was forwarded with no single talker");

      // ---- PROPERTY 4: the counters agree ----
      ck(n_repeat    === s_rep, "repeat count disagrees");
      ck(n_forward   === s_fwd, "forward count disagrees");
      ck(n_collision === s_col, "collision count disagrees");
      ck(n_blocked   === s_blk, "blocked count disagrees");

      // ---- PROPERTY 5: the hub never originates ----
      ck(!(fwd_valid && talkers == 0),
         "the hub forwarded a byte nobody sent");

      // ---- babble: advance the shadow, then compare ----
      for (sp = 0; sp < N_PORTS; sp = sp + 1) begin
        if (dv[sp] && s_live[sp]) begin
          if (s_bab[sp] >= BABBLE_N - 1) begin
            if (!s_dis[sp]) g_dis = g_dis + 1;
            s_dis[sp] = 1'b1;
          end
          else                          s_bab[sp] = s_bab[sp] + 1;
        end else begin
          s_bab[sp] = 0;
        end
      end
      ck(port_disabled === s_dis, "disabled-port set disagrees");
      for (sp = 0; sp < N_PORTS; sp = sp + 1)
        if (s_dis[sp] && !dv[sp]) ; // no-op: counted once at cutoff below
      ck(n_cross == 0, "a downstream port reached another downstream port");
    end
  endtask

  task reset_dut;
    begin
      rst_n = 1'b0;
      us_valid = 1'b0; ds_valid = 4'd0; port_en = 4'hF;
      @(posedge clk); @(posedge clk);
      rst_n = 1'b1;
      s_dis = 4'd0;
      for (rp = 0; rp < N_PORTS; rp = rp + 1) s_bab[rp] = 0;
      s_rep = 0; s_fwd = 0; s_col = 0; s_blk = 0;
      @(posedge clk); #1;
    end
  endtask

  integer ti, ei, ui, k;

  initial begin
    for (ri = 0; ri < 512; ri = ri + 1) reach[ri] = 1'b0;
    seed = 32'd27002;

    // =============================================================
    //  PHASE 1 (DIRECTED, EXHAUSTIVE) -- every talker set against
    //  every enable set, with and without downstream traffic.
    //  16 x 16 x 2 = 512.
    // =============================================================
    reset_dut;
    for (ti = 0; ti < 16; ti = ti + 1)
    for (ei = 0; ei < 16; ei = ei + 1)
    for (ui = 0; ui < 2; ui = ui + 1) begin
      // Each port sends a byte that identifies it, so a byte appearing
      // in the wrong place says exactly which port it escaped from.
      for (p = 0; p < N_PORTS; p = p + 1) ds_data[p] = 8'hA0 + p[7:0];

      step(ui[0], 8'h5C, ti[3:0], ei[3:0]);
      // an idle cycle, so the babble counters cannot accumulate across
      // unrelated scenarios
      for (rp = 0; rp < N_PORTS; rp = rp + 1) ds_data[rp] = 8'd0;
      step(1'b0, 8'd0, 4'd0, ei[3:0]);

      ri = (ti << 5) | (ei << 1) | ui;
      reach[ri] = 1'b1;
    end

    // =============================================================
    //  PHASE 2 (DIRECTED) -- babble, at every port.
    //
    //  A device that transmits and never stops would take the whole
    //  bus down. Cutting it off is the hub's job, and it is the one
    //  decision a hub makes on its own.
    // =============================================================
    for (p = 0; p < N_PORTS; p = p + 1) begin
      reset_dut;
      // Per-port payloads written out explicitly rather than sliced from
      // one 32-bit literal: byte 0 of a flat literal is the LOW eight
      // bits in Verilog and index 0 in an array language, and the two
      // benches then quietly run different experiments.
      for (rp = 0; rp < N_PORTS; rp = rp + 1) ds_data[rp] = 8'hD0 + rp[7:0];
      // Held well past the cutoff, because B6 (a port that is never cut
      // off) diverges for as many cycles as the port keeps talking. Four
      // cycles past the threshold left the score dominated by a 1-in-16000
      // random event and the three language columns 27x apart.
      for (k = 0; k < BABBLE_N + 24; k = k + 1)
        step(1'b0, 8'd0, (4'h1 << p), 4'hF);
      // ---- and a cut-off port STAYS cut off ----
      //
      // Going quiet does not earn it a second chance. A hub that
      // re-enabled a port on silence would re-admit the same broken
      // device every time it paused.
      for (k = 0; k < 4; k = k + 1)
        step(1'b0, 8'd0, 4'd0, 4'hF);
      for (k = 0; k < 8; k = k + 1)
        step(1'b0, 8'd0, (4'h1 << p), 4'hF);
      // and the other ports must still work afterwards
      for (rp = 0; rp < N_PORTS; rp = rp + 1) ds_data[rp] = 8'h11 + rp[7:0];
      for (q = 0; q < N_PORTS; q = q + 1)
        if (q != p) step(1'b1, 8'h33, (4'h1 << q), 4'hF);
    end

    // =============================================================
    //  PHASE 3 (DIRECTED) -- collisions at every pair of ports.
    //
    //  Exhaustive over the 6 unordered pairs, because "two talkers"
    //  is not one situation: a hub that arbitrated by index would
    //  pass a test that only ever collided ports 0 and 1.
    // =============================================================
    reset_dut;
    for (p = 0; p < N_PORTS; p = p + 1)
    for (q = 0; q < N_PORTS; q = q + 1)
      if (p < q) begin
        for (rp = 0; rp < N_PORTS; rp = rp + 1) ds_data[rp] = 8'd0;
        ds_data[p] = 8'h70 + p[7:0];
        ds_data[q] = 8'h70 + q[7:0];
        step(1'b0, 8'd0, (4'h1 << p) | (4'h1 << q), 4'hF);
        for (rp = 0; rp < N_PORTS; rp = rp + 1) ds_data[rp] = 8'd0;
        step(1'b0, 8'd0, 4'd0, 4'hF);
      end

    // =============================================================
    //  PHASE 4 (RANDOM) -- a busy tree.
    // =============================================================
`ifndef DIRECTED_ONLY
    reset_dut;
    for (k = 0; k < 30000; k = k + 1) begin
      for (p = 0; p < N_PORTS; p = p + 1) ds_data[p] = ($random(seed) & 8'hFF);
      // mostly one talker, because that is what a scheduled bus looks
      // like; occasionally none, occasionally two
      step(((urand(0) % 3) != 0),
           ($random(seed) & 8'hFF),
           ((urand(0) % 8) == 0) ? ($random(seed) & 4'hF)
                                      : (4'h1 << (urand(0) % 4)),
           ((urand(0) % 16) == 0) ? ($random(seed) & 4'hF) : 4'hF);
      if ((k % 64) == 63) reset_dut;   // clear accumulated babble cutoffs
    end
`endif

    n_reach = 0;
    for (ri = 0; ri < 512; ri = ri + 1) if (reach[ri]) n_reach = n_reach + 1;

    $display("steps=%0d checks=%0d reach=%0d/512 errors=%0d",
             steps, checks, n_reach, errors);
    $display("[hub] repeats=%0d forwards=%0d collisions=%0d blocked=%0d",
             g_rep, g_fwd, g_col, g_blk);
    $display("[hub] babble cutoffs=%0d", g_dis);
    $display("[the whole point] downstream-to-downstream crossings = %0d", n_cross);
    if (n_reach != 512) begin
      $display("FAIL: exhaustive sweep incomplete"); errors = errors + 1;
    end
    if (errors == 0) $display("PASS: 0 errors in %0d checks", checks);
    else             $display("FAIL: %0d errors in %0d checks", errors, checks);
    $finish;
  end

endmodule

VHDL-2008 testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- =====================================================================
--  Testbench for usb_hub_route (VHDL-2008).
--
--  Same seven properties, same phases, and an xorshift generator that
--  is unrelated to Icarus's -- so the VHDL column is a second opinion
--  rather than a third copy of the Verilog stimulus.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.hr_pkg.all;

entity tb_hr_vhdl is
  -- No preprocessor, so the directed/random split is an elaboration
  -- generic: nvc -e -gDIRECTED_ONLY=true
  generic (DIRECTED_ONLY : boolean := false);
end entity;

architecture sim of tb_hr_vhdl is
  constant N_PORTS  : natural := 4;
  constant BABBLE_N : natural := 8;

  signal clk      : std_logic := '0';
  signal rst_n    : std_logic := '0';
  signal us_valid : std_logic := '0';
  signal us_data  : std_logic_vector(7 downto 0) := (others => '0');
  signal ds_valid : std_logic_vector(N_PORTS-1 downto 0) := (others => '0');
  signal ds_data  : byte_array(0 to N_PORTS-1) := (others => (others => '0'));
  signal port_en  : std_logic_vector(N_PORTS-1 downto 0) := (others => '1');

  signal rpt_valid     : std_logic_vector(N_PORTS-1 downto 0);
  signal rpt_data      : byte_array(0 to N_PORTS-1);
  signal fwd_valid     : std_logic;
  signal fwd_data      : std_logic_vector(7 downto 0);
  signal port_disabled : std_logic_vector(N_PORTS-1 downto 0);
  signal n_repeat, n_forward, n_collision, n_blocked
                       : std_logic_vector(31 downto 0);

  signal done : boolean := false;
begin

  dut : entity work.usb_hub_route
    generic map (N_PORTS => N_PORTS, BABBLE_N => BABBLE_N)
    port map (
      clk => clk, rst_n => rst_n,
      us_valid => us_valid, us_data => us_data,
      ds_valid => ds_valid, ds_data => ds_data, port_en => port_en,
      rpt_valid => rpt_valid, rpt_data => rpt_data,
      fwd_valid => fwd_valid, fwd_data => fwd_data,
      port_disabled => port_disabled,
      n_repeat => n_repeat, n_forward => n_forward,
      n_collision => n_collision, n_blocked => n_blocked);

  clk <= not clk after 5 ns when not done else '0';

  stim : process
    variable errors  : natural := 0;
    variable checks  : natural := 0;
    variable steps   : natural := 0;
    variable n_cross : natural := 0;

    variable s_dis : std_logic_vector(N_PORTS-1 downto 0) := (others => '0');
    type cnt_a is array (natural range <>) of natural;
    variable s_bab : cnt_a(0 to N_PORTS-1) := (others => 0);
    variable s_rep, s_fwd, s_col, s_blk : natural := 0;

    -- Run-wide totals: the DUT's counters are cleared by every reset and
    -- would otherwise report only the last window.
    variable g_rep, g_fwd, g_col, g_blk, g_dis : natural := 0;

    variable reach   : std_logic_vector(0 to 511) := (others => '0');
    variable n_reach : natural := 0;

    variable rnd : unsigned(31 downto 0) := x"0007A1C3";
    variable ln  : line;

    procedure ck(cond : boolean; what : string) is
    begin
      checks := checks + 1;
      if not cond then
        errors := errors + 1;
        if errors <= 20 then
          write(ln, string'("  ERROR step=") & integer'image(steps)
                & string'(": ") & what);
          writeline(output, ln);
        end if;
      end if;
    end procedure;

    impure function nxt return natural is
    begin
      rnd := rnd xor (rnd sll 13);
      rnd := rnd xor (rnd srl 17);
      rnd := rnd xor (rnd sll 5);
      return to_integer(rnd(14 downto 0));
    end function;

    -- The payload is a PARAMETER, not a signal read from inside.
    -- A signal assigned by the caller is not visible until the next
    -- simulation cycle, so a shadow model that reads ds_data here sees
    -- the PREVIOUS cycle's bytes -- 26517 errors, every one of them the
    -- testbench being wrong about VHDL signal semantics.
    procedure step(uv : std_logic;
                   ud : std_logic_vector(7 downto 0);
                   dv : std_logic_vector(N_PORTS-1 downto 0);
                   pe : std_logic_vector(N_PORTS-1 downto 0);
                   dd : byte_array(0 to N_PORTS-1)) is
      variable s_live      : std_logic_vector(N_PORTS-1 downto 0);
      variable e_fwd_valid : boolean;
      variable e_fwd_data  : std_logic_vector(7 downto 0);
      variable e_rpt_valid : std_logic_vector(N_PORTS-1 downto 0);
      variable t, only_p   : natural;
    begin
      us_valid <= uv;  us_data <= ud;
      ds_valid <= dv;  port_en <= pe;  ds_data <= dd;

      s_live := pe and not s_dis;

      t := 0; only_p := 0;
      for p in 0 to N_PORTS-1 loop
        if dv(p) = '1' and s_live(p) = '1' then
          t := t + 1; only_p := p;
        end if;
      end loop;

      e_fwd_valid := (t = 1);
      if t = 1 then e_fwd_data := dd(only_p);
      else          e_fwd_data := (others => '0');
      end if;

      for p in 0 to N_PORTS-1 loop
        if uv = '1' and s_live(p) = '1' then e_rpt_valid(p) := '1';
        else                                 e_rpt_valid(p) := '0';
        end if;
      end loop;

      if uv = '1' then s_rep := s_rep + 1; g_rep := g_rep + 1; end if;
      if t = 1 then    s_fwd := s_fwd + 1; g_fwd := g_fwd + 1; end if;
      if t > 1 then    s_col := s_col + 1; g_col := g_col + 1; end if;
      for p in 0 to N_PORTS-1 loop
        if dv(p) = '1' and s_live(p) = '0' then
          s_blk := s_blk + 1; g_blk := g_blk + 1;
        end if;
      end loop;

      wait until rising_edge(clk);
      wait for 1 ns;
      steps := steps + 1;

      -- PROPERTY 1: repeated to every live port and only those
      ck(rpt_valid = e_rpt_valid, "downstream repeat reached the wrong set of ports");

      -- PROPERTY 2: the repeated byte is the UPSTREAM byte (no crossbar)
      for p in 0 to N_PORTS-1 loop
        if e_rpt_valid(p) = '1' then
          ck(rpt_data(p) = ud, "a repeated byte was not the upstream byte");
          if rpt_data(p) /= ud then n_cross := n_cross + 1; end if;
        else
          ck(rpt_data(p) = std_logic_vector'("00000000"),
             "a disabled or idle port was driven with data");
          for q in 0 to N_PORTS-1 loop
            if q /= p and dv(q) = '1'
               and rpt_data(p) = dd(q)
               and dd(q) /= std_logic_vector'("00000000") then
              n_cross := n_cross + 1;
            end if;
          end loop;
        end if;
      end loop;

      -- PROPERTY 3: exactly one talker is forwarded
      ck((fwd_valid = '1') = e_fwd_valid, "upstream forward disagrees");
      if e_fwd_valid then
        ck(fwd_data = e_fwd_data, "wrong byte forwarded upstream");
      else
        ck(fwd_data = std_logic_vector'("00000000"),
           "a byte was forwarded with no single talker");
      end if;

      -- PROPERTY 4: the counters agree
      ck(to_integer(unsigned(n_repeat))    = s_rep, "repeat count disagrees");
      ck(to_integer(unsigned(n_forward))   = s_fwd, "forward count disagrees");
      ck(to_integer(unsigned(n_collision)) = s_col, "collision count disagrees");
      ck(to_integer(unsigned(n_blocked))   = s_blk, "blocked count disagrees");

      -- PROPERTY 5: the hub never originates
      ck(not (fwd_valid = '1' and t = 0), "the hub forwarded a byte nobody sent");

      -- babble: advance the shadow, then compare
      for p in 0 to N_PORTS-1 loop
        if dv(p) = '1' and s_live(p) = '1' then
          if s_bab(p) >= BABBLE_N - 1 then
            if s_dis(p) = '0' then g_dis := g_dis + 1; end if;
            s_dis(p) := '1';
          else
            s_bab(p) := s_bab(p) + 1;
          end if;
        else
          s_bab(p) := 0;
        end if;
      end loop;
      ck(port_disabled = s_dis, "disabled-port set disagrees");
      ck(n_cross = 0, "a downstream port reached another downstream port");
    end procedure;

    procedure reset_dut is
    begin
      rst_n    <= '0';
      us_valid <= '0';
      ds_valid <= (others => '0');
      port_en  <= (others => '1');
      wait until rising_edge(clk);
      wait until rising_edge(clk);
      rst_n <= '1';
      s_dis := (others => '0');
      s_bab := (others => 0);
      s_rep := 0; s_fwd := 0; s_col := 0; s_blk := 0;
      wait until rising_edge(clk);
      wait for 1 ns;
    end procedure;

    variable ri : natural;
    variable tv, ev : std_logic_vector(N_PORTS-1 downto 0);
    variable db : byte_array(0 to N_PORTS-1) := (others => (others => '0'));
  begin
    -- PHASE 1 (DIRECTED, EXHAUSTIVE) -- 16 talker sets x 16 enable sets x 2
    reset_dut;
    for ti in 0 to 15 loop
      for ei in 0 to 15 loop
        for ui in 0 to 1 loop
          -- each port sends a byte that names it, so a byte in the wrong
          -- place says which port it escaped from
          for p in 0 to N_PORTS-1 loop
            db(p) := std_logic_vector(to_unsigned(16#A0# + p, 8));
          end loop;
          tv := std_logic_vector(to_unsigned(ti, N_PORTS));
          ev := std_logic_vector(to_unsigned(ei, N_PORTS));
          if ui = 1 then step('1', x"5C", tv, ev, db);
          else           step('0', x"5C", tv, ev, db);
          end if;
          db := (others => (others => '0'));
          step('0', x"00", (others => '0'), ev, db);

          ri := ti*32 + ei*2 + ui;
          reach(ri) := '1';
        end loop;
      end loop;
    end loop;

    -- PHASE 2 (DIRECTED) -- babble, at every port
    for p in 0 to N_PORTS-1 loop
      reset_dut;
      for r in 0 to N_PORTS-1 loop
        db(r) := std_logic_vector(to_unsigned(16#D0# + r, 8));
      end loop;
      tv := (others => '0');
      tv(p) := '1';
      for k in 0 to BABBLE_N + 23 loop
        step('0', x"00", tv, (others => '1'), db);
      end loop;
      -- and a cut-off port STAYS cut off: going quiet earns it nothing
      for k in 0 to 3 loop
        step('0', x"00", (others => '0'), (others => '1'), db);
      end loop;
      for k in 0 to 7 loop
        step('0', x"00", tv, (others => '1'), db);
      end loop;
      for r in 0 to N_PORTS-1 loop
        db(r) := std_logic_vector(to_unsigned(16#11# + r, 8));
      end loop;
      -- and the other ports must still work afterwards
      for q in 0 to N_PORTS-1 loop
        if q /= p then
          ev := (others => '0');
          ev(q) := '1';
          step('1', x"33", ev, (others => '1'), db);
        end if;
      end loop;
    end loop;

    -- PHASE 3 (DIRECTED) -- collisions at every unordered pair
    reset_dut;
    for p in 0 to N_PORTS-1 loop
      for q in 0 to N_PORTS-1 loop
        if p < q then
          db := (others => (others => '0'));
          db(p) := std_logic_vector(to_unsigned(16#70# + p, 8));
          db(q) := std_logic_vector(to_unsigned(16#70# + q, 8));
          tv := (others => '0');
          tv(p) := '1';
          tv(q) := '1';
          step('0', x"00", tv, (others => '1'), db);
          db := (others => (others => '0'));
          step('0', x"00", (others => '0'), (others => '1'), db);
        end if;
      end loop;
    end loop;

    -- PHASE 4 (RANDOM) -- a busy tree
    if not DIRECTED_ONLY then
      reset_dut;
      for k in 0 to 29999 loop
        for p in 0 to N_PORTS-1 loop
          db(p) := std_logic_vector(to_unsigned(nxt mod 256, 8));
        end loop;
        if (nxt mod 8) = 0 then
          tv := std_logic_vector(to_unsigned(nxt mod 16, N_PORTS));
        else
          tv := (others => '0');
          tv(nxt mod N_PORTS) := '1';
        end if;
        if (nxt mod 16) = 0 then
          ev := std_logic_vector(to_unsigned(nxt mod 16, N_PORTS));
        else
          ev := (others => '1');
        end if;
        if (nxt mod 3) /= 0 then
          step('1', std_logic_vector(to_unsigned(nxt mod 256, 8)), tv, ev, db);
        else
          step('0', std_logic_vector(to_unsigned(nxt mod 256, 8)), tv, ev, db);
        end if;
        if (k mod 64) = 63 then reset_dut; end if;
      end loop;
    end if;

    n_reach := 0;
    for i in 0 to 511 loop
      if reach(i) = '1' then n_reach := n_reach + 1; end if;
    end loop;

    write(ln, string'("steps=") & integer'image(steps)
          & string'(" checks=") & integer'image(checks)
          & string'(" reach=") & integer'image(n_reach) & string'("/512")
          & string'(" errors=") & integer'image(errors));
    writeline(output, ln);
    write(ln, string'("[hub] repeats=") & integer'image(g_rep)
          & string'(" forwards=") & integer'image(g_fwd)
          & string'(" collisions=") & integer'image(g_col)
          & string'(" blocked=") & integer'image(g_blk));
    writeline(output, ln);
    write(ln, string'("[hub] babble cutoffs=") & integer'image(g_dis));
    writeline(output, ln);
    write(ln, string'("[the whole point] downstream-to-downstream crossings = ")
          & integer'image(n_cross));
    writeline(output, ln);
    if n_reach /= 512 then
      write(ln, string'("FAIL: exhaustive sweep incomplete"));
      writeline(output, ln);
      errors := errors + 1;
    end if;
    if errors = 0 then
      write(ln, string'("PASS: 0 errors in ") & integer'image(checks)
            & string'(" checks"));
    else
      write(ln, string'("FAIL: ") & integer'image(errors)
            & string'(" errors in ") & integer'image(checks) & string'(" checks"));
    end if;
    writeline(output, ln);

    done <= true;
    wait;
  end process;

end architecture;

10. Exhaustive Verification

MeasureVerilogSystemVerilogVHDL
(talker set × enable set × upstream) reached512 / 512512 / 512512 / 512
collision pairs swept6 / 66 / 66 / 6
babble ports swept4 / 44 / 44 / 4
Steps312243122431224
Checks executed437136437136437136
downstream repeats202602026020333
upstream forwards265962659626670
collisions reported259225922684
talkers blocked (not live)180818081646
babble cutoffs11115
downstream-to-downstream crossings000
ResultPASSPASSPASS

The exhaustive sweep is all 16 talker sets against all 16 enable sets, with and without host traffic — which includes every combination of "a port is talking while disabled", "two ports are talking", and "no port is talking while the host is".

11. Mutation Testing

#MutationVerilogSysVerVHDL
B5a downstream port's byte is repeated to the other ports100364100364100641
B3a collision is silently resolved instead of reported564325643257170
B2a disabled port is still heard upstream460554605542614
B1downstream is repeated to disabled ports too325663256628136
B4the forward register is never cleared — the hub originates108971089710591
B6a babbling port is never cut off75897589969
B7the blocked count adds 1 per cycle instead of 1 per port339833982620
—unmutated baseline000

All seven die in all three languages. B5 — the switch mistake — is the highest-scoring mutation, which is the right shape for a chapter whose thesis is that a hub is not a switch.

Directed against random

Verilog and VHDL, decomposed:

#V allV directedV randomVHDL allVHDL directedVHDL random
B132566776317902813677627360
B24605539224213342614392238692
B35643219445448857170194455226
B410897251883791059125188073
B5100364140698958100641140699235
B67589880670996988089
B73398832256626208321788

12. Five Findings, None of Them in the Design

This chapter produced more testbench bugs than any other in the module, and every one is a lesson that generalises. They are worth more than the mutation scores.

1. A non-blocking add inside a loop adds once

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   for (k = 0; k < N_PORTS; k = k + 1)
     if (ds_valid[k] && !live[k]) blk_r <= blk_r + 1;

   Four assignments. One register. The last one wins.
   The counter adds 1 per CYCLE, not 1 per PORT.

The bench reported 3398 errors, every one blocked count disagrees, and it was right: the design was wrong. The fix is to count combinationally and add once. This is now mutation B7, which is the only mutation in the module that was a real defect first.

2. A VHDL signal assigned by the caller is not visible in the callee

The VHDL bench originally had the phase code drive ds_data and the step procedure read it. That produced 26,517 errors — and the design was fine.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   caller:  ds_data(p) <= payload;     -- scheduled, not applied
            step(...);                 -- reads ds_data -> OLD value

   In VHDL a signal assignment takes effect after a wait.
   The shadow model was comparing against the PREVIOUS
   cycle's bytes for the entire run.

The fix is to pass the payload as a parameter, which VHDL permits for arrays. It is also strictly better design: the expected value and the driven value are then provably the same object.

3. A flat literal and an array index disagree about which byte is byte 0

The SystemVerilog bench was derived from the Verilog one, and the Verilog passed payloads as 32-bit literals: 32'hDEADBEEF. In Verilog, dd[8*0 +: 8] is 0xEF — the low byte. In the array version, ds_data[0] was set to 0xDE.

Same literal, different port assignment, and therefore two different experiments. The baseline passed in both, because BASE never compares a repeated byte against another port's payload. Only the mutants exposed it, as a 15% spread on B1 and a 780× spread on B6.

The fix was to stop using flat literals at all and write per-port payloads explicitly in all three benches — 0xD0 + port — so that the byte on each port is unambiguous in every language.

4. $random is signed, and 1 << negative is zero

The random phase intended exactly one talker per cycle:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   ds_valid = 4'h1 << ($random(seed) % 4);

   $random returns a SIGNED 32-bit value, so `% 4` is
   negative about half the time. Verilog treats a shift
   amount as unsigned, so a negative becomes enormous
   and the result is 0 -- NO talker at all.

   Intended single-talker rate:  ~87%
   Actual:                        55%

The Verilog bench was spending nearly half its random cycles on an idle bus it had not asked for, and it made the Verilog and VHDL columns incomparable — the VHDL generator returns non-negative values, so its rate really was 86%.

Masking off the sign bit fixed both problems at once: the Verilog forwarding rate went from 17,189 to 26,596, matching VHDL's 26,670, and four of the seven mutation rows fell into line.

5. A stale binary produces a complete, plausible table

Twice during this chapter a compile failed and the previous run's executable was still on disk. The matrix printed seven confident numbers from chapter 27.1's design.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   MUT   Verilog
   B1       39060      <- A7's score, from the previous chapter
   B2       39060
   ...      39060

   Seven identical numbers is an obvious tell. Seven
   PLAUSIBLE ones would not have been.

The defence is structural, not vigilance: delete the binary before every compile, publish a BASE row, and require it to read 0. A stale binary cannot produce a zero baseline and a killed mutation from the same file.

13. Follow-Ups the Interviewer Will Ask

"Can two devices on the same hub talk to each other?" No. There is no path. Everything goes up to the host and back down, and only if the host arranges it.

"What happens if two devices transmit at once?" It is a fault, not a case. The host's schedule makes it impossible; the hub reports it rather than arbitrating, because arbitrating would forward a transaction that never happened and destroy the only evidence the schedule was violated. That is mutation B3, and it scores 56,432.

"How does a full-speed device work behind a high-speed hub?" The hub contains a transaction translator that buffers the low-speed transaction and replays it at the slower rate, so the high-speed bus is not held hostage. This is the one place a hub does something more than repeat — and it is why a bad hub can break a device that works when plugged in directly.

"What stops one broken device taking down the bus?" The hub does, by cutting off a port that transmits continuously. That is property 7 and mutation B6, and it is the only decision a hub makes on its own authority.

"How does the host learn a device was plugged in?" The hub tells it — but only when asked, through the hub's device half, as a port status change. The electrical event is detected by the hub; the reporting of it is still polled. Chapter 27.3 is this question.

14. UVM: Proving a Path Does Not Exist

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Proving that a path DOES exist is easy: send a byte, see it arrive.
// Proving one does NOT exist is the hard direction, and it is what this
// whole component is for.
//
// The technique is to give every port a payload that identifies it, then
// assert that no port's output ever carries a byte that belongs to any
// other port. A data-comparison scoreboard cannot do this: the data it
// is comparing arrived correctly.
class hub_item extends uvm_sequence_item;
  `uvm_object_utils(hub_item)

  rand bit       us_valid;
  rand bit [7:0] us_data;
  rand bit [3:0] ds_valid;
  rand bit [3:0] port_en;

  // Each port's byte NAMES its port, so a byte in the wrong place says
  // which port it escaped from rather than merely that something is off.
  function bit [7:0] payload_of(int p); return 8'hA0 + p[7:0]; endfunction

  function new(string name = "hub_item"); super.new(name); endfunction

  // A bus on which one port talks at a time is what a correct schedule
  // produces -- but a monitor that only ever sees that cannot check the
  // collision rule, so contention is deliberately injected.
  constraint c_mostly_one_talker {
    $countones(ds_valid) dist { 1 := 80, 0 := 12, [2:4] := 8 };
  }
  constraint c_mostly_enabled { port_en dist { 4'hF := 90, [0:14] := 10 }; }
endclass


class hub_monitor extends uvm_component;
  `uvm_component_utils(hub_monitor)

  virtual hub_if vif;
  localparam int N = 4;

  // ---- the negative claim ----
  int unsigned n_cross;

  // ---- and the evidence it was at risk ----
  int unsigned n_two_talkers;     // contention actually occurred
  int unsigned n_disabled_talk;   // a dead port tried to speak
  int unsigned n_repeat_cycles;   // there WAS downstream traffic to leak

  int unsigned n_repeat, n_forward, n_collision;

  function new(string name, uvm_component parent); super.new(name, parent);
  endfunction

  task run_phase(uvm_phase phase);
    bit [3:0] live;
    int       talkers;

    forever begin
      @(posedge vif.clk);
      if (!vif.rst_n) continue;

      live    = vif.port_en & ~vif.port_disabled;
      talkers = $countones(vif.ds_valid & live);

      // ---- PROPERTY 2: no downstream port reaches another ----
      //
      // Checked per port, per cycle, against TWO different wrong answers:
      // a live port carrying something other than the host's byte, and a
      // quiet port carrying a byte that belongs to a talking port.
      for (int p = 0; p < N; p++) begin
        if (vif.us_valid && live[p]) begin
          if (vif.rpt_data[p] !== vif.us_data) begin
            n_cross++;
            `uvm_error("HUB/CROSS",
              $sformatf("port %0d repeated 0x%02h, host sent 0x%02h", p,
                        vif.rpt_data[p], vif.us_data))
          end
        end else begin
          for (int q = 0; q < N; q++)
            if (q != p && vif.ds_valid[q]
                       && vif.rpt_data[p] === vif.ds_data[q]
                       && vif.ds_data[q] !== 8'h00) begin
              n_cross++;
              `uvm_error("HUB/CROSS",
                $sformatf("port %0d is carrying port %0d's byte 0x%02h -- a hub has no such path",
                          p, q, vif.ds_data[q]))
            end
        end
      end

      // ---- PROPERTY 4: contention is REPORTED, never resolved ----
      //
      // A forwarded byte during contention is the defect that matters:
      // the host receives a transaction that no single device sent, and
      // there is nothing in the data to reveal it.
      if (talkers > 1) begin
        n_two_talkers++;
        if (vif.fwd_valid)
          `uvm_error("HUB/ARBITRATE",
            $sformatf("%0d ports were talking and the hub forwarded one anyway: the collision is now invisible",
                      talkers))
      end

      // ---- PROPERTY 5: the hub never originates ----
      if (talkers == 0 && vif.fwd_valid)
        `uvm_error("HUB/ORIGINATE",
          "the hub forwarded a byte upstream that no device sent")

      // ---- PROPERTY 6: a dead port is not heard ----
      for (int p = 0; p < N; p++)
        if (vif.ds_valid[p] && !live[p]) n_disabled_talk++;

      if (vif.us_valid)   n_repeat_cycles++;
      if (vif.us_valid)   n_repeat++;
      if (talkers == 1)   n_forward++;
      if (talkers > 1)    n_collision++;
    end
  endtask

  function void report_phase(uvm_phase phase);
    super.report_phase(phase);

    `uvm_info("HUB",
      $sformatf("%0d repeats | %0d forwards | %0d collisions | %0d crossings",
                n_repeat, n_forward, n_collision, n_cross), UVM_LOW)

    // A negative property is only as strong as the audit behind it. Each
    // of these is a run in which the leak had no opportunity to appear.
    if (n_repeat_cycles == 0)
      `uvm_error("HUB/COV",
        "no downstream traffic in this run: there was never a byte available to leak")
    if (n_two_talkers == 0)
      `uvm_error("HUB/COV",
        "contention never occurred: the collision rule was never exercised")
    if (n_disabled_talk == 0)
      `uvm_error("HUB/COV",
        "no disabled port ever tried to transmit: the enable check was never exercised")

    `uvm_info("HUB/COV",
      $sformatf("at-risk: %0d repeat cycles, %0d contentions, %0d disabled-port attempts",
                n_repeat_cycles, n_two_talkers, n_disabled_talk), UVM_LOW)
  endfunction
endclass

15. Common Misconceptions

"A hub is a switch." It is a repeater. One path up, a broadcast down, nothing sideways.

"Devices on the same hub can talk directly." There is no path. Everything goes up to the host and back.

"A hub routes by address." It is never told any device's address and has no way to ask.

"A hub arbitrates contention." It reports it. Arbitrating forwards a transaction nobody sent.

"A hub has no address." Its repeater function does not use one; the hub is also a device, and that half has an address like any other.

"A hub is transparent." A high-speed hub contains a transaction translator for slower devices, and it is the most common reason a device works when plugged in directly and fails behind a hub.

"A disabled port is just idle." It receives nothing and is heard by nobody, and the attempt to speak is counted — B1 and B2 both score in the tens of thousands.

"One broken device cannot take down the bus." It can, and stopping it is the hub's job — the only decision a hub makes without being told.

"A cut-off port recovers when it goes quiet." It does not. Silence earns nothing; software must re-enable it.

16. Exercises

1. Explain why a USB hub cannot be a switch using only facts from chapter 27.1. Your argument should not need to mention hubs at all until the last line.

2. B5 leaks a downstream byte to the other ports and scores highest of the seven. Write a data-comparison scoreboard that checks "every byte a device sent arrived at the host" and show that it passes B5 completely.

3. Every directed column in section 11 is identical across Verilog and VHDL. Say precisely what that proves, and what it does not prove.

4. B6's random contribution is 6709 in Verilog and 89 in VHDL. Estimate the probability of the enabling event and show the two counts are consistent with the same underlying rate.

5. The $random sign bug halved the intended talker rate and every check still passed. Design a stimulus self-check that would have failed, and argue where it belongs.

6. Add a transaction translator: a full-speed device behind a high-speed hub. Which of the seven properties change, and what new one is needed?

7. Property 7 cuts off a babbling port permanently. Argue for automatic re-enable after a timeout, and give the failure mode it introduces.

17. Summary

IdeaWhy it matters
A hub is a repeater, not a switchone path up, broadcast down, nothing sideways
No downstream port reaches anotherthere is no path and no table that could build one
A hub is told no addressesso it could not route even if it wanted to
Contention is reported, not arbitratedforwarding one byte hides that the schedule broke
A hub never originatesB4 forwards a byte nobody sent
A hub is also a devicethe repeater has no address; the device half does
Babble cutoff is the hub's own decisionthe only one it makes unasked
A cut-off port stays cut offsilence is not recovery
A per-port accumulator needs a combinational countblk_r <= blk_r + 1 in a loop adds once
A VHDL signal is not visible in the calleepass arrays as parameters
A flat literal's byte 0 is the low byteand an array's is index 0 — two experiments
$random is signed1 << negative is zero, and the bus goes quiet
Delete the binary, publish BASE = 0a stale executable prints a plausible table
Decompose before diagnosingB6's 7.8× spread is entirely random-phase noise
512 states, 7 mutations, 3 languages0 crossings in 437,136 checks

Tooling

StepCommand
Verilog-2005iverilog -g2005 -o hr_v.out hr_v.v hr_v_tb.v && ./hr_v.out
SystemVerilogiverilog -g2012 -o hr_sv.out hr_sv.sv hr_sv_tb.sv && ./hr_sv.out
VHDL-2008 analysenvc --std=2008 -a hr_vhdl.vhd hr_vhdl_tb.vhd
VHDL-2008 elaboratenvc --std=2008 -e tb_hr_vhdl
VHDL-2008 runnvc --std=2008 -r tb_hr_vhdl
One mutationiverilog -g2005 -DMUT_B5 -o mm hr_v_mut.v hr_v_tb.v && ./mm
Directed only (Verilog)iverilog -g2005 -DDIRECTED_ONLY -o mm hr_v_mut.v hr_v_tb.v && ./mm
Directed only (VHDL)nvc --std=2008 -e -gDIRECTED_ONLY=true tb_hr_vhdl

All three implementations pass with 0 errors: all 512 combinations of talker set, enable set and upstream traffic; 2592 collisions and 1808 blocked talkers deliberately exercised; zero downstream-to-downstream crossings in 437,136 checks; and every one of the seven mutations killed by directed stimulus alone, with all seven directed scores identical across languages.


Chapter 27.3 — The Enumeration Question is the last question this chapter leaves open: if a device cannot announce itself and a hub only reports when asked, how does anything ever get discovered? The answer is a sequence with one step everybody gets backwards — SET_ADDRESS takes effect after the status stage, not when the request arrives.

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.