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.
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 contentionThe last line is the one that matters most in practice, and section 12 is about it.
3. The Three Roles, Precisely
| Role | May initiate? | Has an address? | Decodes addresses? | How many upstream ports |
|---|---|---|---|---|
| Host | yes — only it | no | n/a | — |
| Device | never | yes, assigned at enumeration | yes, its own only | 1 |
| Hub | never | yes (it is also a device) | no | 1 |
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
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
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 |
|---|---|
| 1 | Downstream traffic is repeated to every live port and only live ports. |
| 2 | A repeated byte is the upstream byte — never another port's payload. |
| 3 | Exactly one live talker is forwarded upstream. |
| 4 | Zero or more than one talker forwards nothing. |
| 5 | The hub never originates a byte. |
| 6 | A non-live port is not heard, and the attempt is counted. |
| 7 | A 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
// =====================================================================
// 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
endmodule7. SystemVerilog RTL
// =====================================================================
// 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
endmodule8. VHDL-2008 RTL
-- =====================================================================
-- 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.
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
// =====================================================================
// 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
endmoduleSystemVerilog testbench
// =====================================================================
// 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
endmoduleVHDL-2008 testbench
-- =====================================================================
-- 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
| Measure | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| (talker set × enable set × upstream) reached | 512 / 512 | 512 / 512 | 512 / 512 |
| collision pairs swept | 6 / 6 | 6 / 6 | 6 / 6 |
| babble ports swept | 4 / 4 | 4 / 4 | 4 / 4 |
| Steps | 31224 | 31224 | 31224 |
| Checks executed | 437136 | 437136 | 437136 |
| downstream repeats | 20260 | 20260 | 20333 |
| upstream forwards | 26596 | 26596 | 26670 |
| collisions reported | 2592 | 2592 | 2684 |
| talkers blocked (not live) | 1808 | 1808 | 1646 |
| babble cutoffs | 11 | 11 | 5 |
| downstream-to-downstream crossings | 0 | 0 | 0 |
| Result | PASS | PASS | PASS |
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
| # | Mutation | Verilog | SysVer | VHDL |
|---|---|---|---|---|
| B5 | a downstream port's byte is repeated to the other ports | 100364 | 100364 | 100641 |
| B3 | a collision is silently resolved instead of reported | 56432 | 56432 | 57170 |
| B2 | a disabled port is still heard upstream | 46055 | 46055 | 42614 |
| B1 | downstream is repeated to disabled ports too | 32566 | 32566 | 28136 |
| B4 | the forward register is never cleared — the hub originates | 10897 | 10897 | 10591 |
| B6 | a babbling port is never cut off | 7589 | 7589 | 969 |
| B7 | the blocked count adds 1 per cycle instead of 1 per port | 3398 | 3398 | 2620 |
| — | unmutated baseline | 0 | 0 | 0 |
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 all | V directed | V random | VHDL all | VHDL directed | VHDL random |
|---|---|---|---|---|---|---|
| B1 | 32566 | 776 | 31790 | 28136 | 776 | 27360 |
| B2 | 46055 | 3922 | 42133 | 42614 | 3922 | 38692 |
| B3 | 56432 | 1944 | 54488 | 57170 | 1944 | 55226 |
| B4 | 10897 | 2518 | 8379 | 10591 | 2518 | 8073 |
| B5 | 100364 | 1406 | 98958 | 100641 | 1406 | 99235 |
| B6 | 7589 | 880 | 6709 | 969 | 880 | 89 |
| B7 | 3398 | 832 | 2566 | 2620 | 832 | 1788 |
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
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.
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:
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.
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
// 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
endclass15. 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
| Idea | Why it matters |
|---|---|
| A hub is a repeater, not a switch | one path up, broadcast down, nothing sideways |
| No downstream port reaches another | there is no path and no table that could build one |
| A hub is told no addresses | so it could not route even if it wanted to |
| Contention is reported, not arbitrated | forwarding one byte hides that the schedule broke |
| A hub never originates | B4 forwards a byte nobody sent |
| A hub is also a device | the repeater has no address; the device half does |
| Babble cutoff is the hub's own decision | the only one it makes unasked |
| A cut-off port stays cut off | silence is not recovery |
| A per-port accumulator needs a combinational count | blk_r <= blk_r + 1 in a loop adds once |
| A VHDL signal is not visible in the callee | pass arrays as parameters |
| A flat literal's byte 0 is the low byte | and an array's is index 0 — two experiments |
$random is signed | 1 << negative is zero, and the bus goes quiet |
| Delete the binary, publish BASE = 0 | a stale executable prints a plausible table |
| Decompose before diagnosing | B6's 7.8× spread is entirely random-phase noise |
| 512 states, 7 mutations, 3 languages | 0 crossings in 437,136 checks |
Tooling
| Step | Command |
|---|---|
| Verilog-2005 | iverilog -g2005 -o hr_v.out hr_v.v hr_v_tb.v && ./hr_v.out |
| SystemVerilog | iverilog -g2012 -o hr_sv.out hr_sv.sv hr_sv_tb.sv && ./hr_sv.out |
| VHDL-2008 analyse | nvc --std=2008 -a hr_vhdl.vhd hr_vhdl_tb.vhd |
| VHDL-2008 elaborate | nvc --std=2008 -e tb_hr_vhdl |
| VHDL-2008 run | nvc --std=2008 -r tb_hr_vhdl |
| One mutation | iverilog -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
Related tutorials
- Related topic
Hub Architecture
A hub is three devices in one package, and its repeater is deliberately asymmetric: downstream is a broadcast, upstream is a select of exactly one — and two talkers connects neither.
- Related topic
DMA Integration
A descriptor has a byte count and the wire has packets, and the rule that converts one to the other is not ceil(length / packet size) — the version that is hangs on exactly the buffer sizes everybody uses.
- Related topic
What Is USB?
The opening interview question answered with one load-bearing idea instead of a list — USB is host-scheduled, and polling, NAK, the frame and the missing interrupt line are all consequences of it.
- Related topic
The Enumeration Question
Attach to configured, with the one step almost everybody gets backwards — SET_ADDRESS takes effect after the status stage, and a device that switches early is invisible to the host.
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.
