Skip to content
VLSI Mentor

USB · Module 27

The Endpoints Question

Endpoint 1 IN and endpoint 1 OUT are two different endpoints — separate buffers, toggles, halt states and packet sizes — and a table indexed by number alone is a device where halting a read kills its writes.

Chapter 27.3 ended with SET_CONFIGURATION and endpoints becoming usable. This is the question that follows, and it has a stock answer that is correct and incomplete in a way that matters.

1. The Question

"What is an endpoint?"

The stock answer: a logical channel on a device, identified by a number 0 to 15, with a direction and a maximum packet size, and a transfer type set by its descriptor. Every word of that is true. It will get you through a screening call.

It will not get you through a design interview, because it hides the thing the question is really testing.

2. Why This Is the Question

Because it is where the bugs are. A device controller that indexes its endpoint table by number alone compiles, enumerates, passes casual testing, and then:

  • halting endpoint 1 IN silently halts endpoint 1 OUT, so a driver that stalls a broken read direction kills its writes as well;
  • the two directions share a data toggle, so traffic in one direction desynchronises the other and packets are silently dropped as duplicates;
  • an IN transfer uses the OUT direction's maximum packet size, which is usually the same number and occasionally is not.

None of those produce an error. All of them produce a device that works until it does not.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   Indexed by NUMBER:              Indexed by (NUMBER, DIRECTION):

     table[4]                        table[4][2]
       halt(1) -> kills both           halt(1, IN) -> kills IN only
       toggle(1) shared                toggle(1, IN) independent
       maxp(1) shared                  maxp(1, IN) independent

   One of these is a device. The other is a device that
   works until somebody halts an endpoint.

3. Endpoint 0 Is the Exception That Proves the Rule

Endpoint 0 is genuinely bidirectional, and for a specific reason: a control transfer is one conversation that runs both ways. The SETUP goes out, the data may go either way, the status stage comes back — and all of it is one logical exchange, not two pipes that happen to share a number.

So EP0's two halves share a packet size, share a configuration, and exist from the moment the device powers up. They have to: EP0 is how the host talks to a device it knows nothing about, before any descriptor has been read.

4. What We Are Building

usb_ep_table is the lookup that sits between a token arriving off the bus and the endpoint state that answers it. Four endpoint numbers, two directions, and the whole design is the insistence that those are eight endpoints rather than four.

Four numbers, eight endpoints

An endpoint table indexed by both number and direction, showing that endpoint 1 IN and endpoint 1 OUT are separate entries with independent stateTokenIndexEP1 INEP1 OUTEP0 IN + OUTboth fieldsdir = INdir = OUTnumber 012
Halting endpoint 1 IN leaves endpoint 1 OUT completely untouched: different buffer, different toggle, different halt state, different packet size.

EP1 IN is halted and EP1 OUT is running, at the same moment, on the same endpoint number. That is the normal, correct state of a device whose read direction has failed and whose write direction has not.

Halting one direction, and the other carrying on

A halt written to endpoint 1 IN takes effect on the IN lookup only, while the OUT direction of the same endpoint number continues to report not halted and keeps advancing its data toggleboth directions runningbothdirectio…IN halted, OUT unaffectedIN halted, OUT unaffectedhalt EP1 INhalt EP1 ININ reports haltedIN reports haltedOUT still runningOUT still runningOUT toggle advancedOUT toggle advancedclkhalt_wrhalt_intok_inep_haltedep_togglexact_ackt0t1t2t3t4t5t6t7t8
halt_wr targets endpoint 1 IN in cycle 2. From cycle 3 the IN lookup reports halted and the OUT lookup does not — and the OUT toggle keeps advancing.

ep_halted alternates with tok_in from cycle 2 onward — high when the lookup names the IN direction, low when it names OUT. That alternation is the entire property, and a table indexed by number alone would show it high in both.

5. Seven Properties

#Property
1A lookup reports the state of that (number, direction) and no other.
2Halting one direction leaves the other's halt state untouched.
3The two directions have independent data toggles.
4The two directions have independent maximum packet sizes.
5Endpoint 0 is bidirectional: configuring it configures both halves.
6Endpoint 0 is not haltable, because clearing a halt requires it.
7An endpoint that is out of range or unconfigured reports invalid, with packet size 0.

6. Verilog-2005 RTL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  usb_ep_table -- "Explain endpoints" answered in hardware.
//
//  The answer everybody gives is "a logical channel, numbered 0 to 15,
//  with a direction and a maximum packet size". All true, and it hides
//  the fact the question is actually about:
//
//      Endpoint 1 IN and endpoint 1 OUT are TWO DIFFERENT ENDPOINTS.
//
//  Separate buffers, separate data toggles, separate halt states,
//  separate max packet sizes. The number is not an identifier; the pair
//  (number, direction) is. A device with "four endpoints" may have four
//  or eight of them depending on what the descriptors say, and a table
//  indexed by number alone is a device where halting the IN direction
//  silently halts the OUT direction with it.
//
//  Endpoint 0 is the exception that proves the rule: it is the one
//  endpoint that is genuinely bidirectional, because control transfers
//  need both directions of the SAME conversation.
// =====================================================================
module usb_ep_table #(
  parameter N_NUM = 4            // endpoint numbers 0..N_NUM-1
) (
  input  wire        clk,
  input  wire        rst_n,

  // ---- a token, as it arrives off the bus ----
  input  wire        tok_valid,
  input  wire [3:0]  tok_ep,      // endpoint NUMBER
  input  wire        tok_in,      // direction: 1 = IN (device to host)

  // ---- configuration, from the descriptors ----
  input  wire        cfg_wr,
  input  wire [3:0]  cfg_ep,
  input  wire        cfg_in,
  input  wire        cfg_enable,
  input  wire [10:0] cfg_maxp,

  // ---- halt control, per (number, direction) ----
  input  wire        halt_wr,
  input  wire [3:0]  halt_ep,
  input  wire        halt_in,
  input  wire        halt_set,

  // ---- a transaction completing, which advances the toggle ----
  input  wire        xact_ack,

  // ---- lookup results for the token above ----
  output wire        ep_valid,    // this (number, direction) exists
  output wire        ep_halted,
  output wire [10:0] ep_maxp,
  output wire        ep_toggle,

  // ---- observability ----
  output wire [31:0] n_lookup,
  output wire [31:0] n_unknown_ep,
  output wire [31:0] n_halted_hit,
  output wire [31:0] n_toggle_flip,
  // Evidence that the two directions were driven INDEPENDENTLY: a halt
  // applied to one direction while the other stayed unhalted. This is a
  // coverage counter, not a self-check -- the design cannot detect its own
  // cross-direction leak, because a leak is indistinguishable from a
  // deliberate write from inside the module. The TESTBENCH does that, with
  // an independent shadow of both directions.
  output wire [31:0] n_split_halt
);

  // ---- the table is indexed by (number, direction), never by number ----
  //
  // Two separate arrays rather than one array of structs, so that an index
  // that forgets the direction cannot compile into something plausible.
  // IN and OUT are not two views of one endpoint; they are two endpoints.
  reg              en_in  [0:N_NUM-1];
  reg              en_out [0:N_NUM-1];
  reg              hl_in  [0:N_NUM-1];
  reg              hl_out [0:N_NUM-1];
  reg  [10:0]      mp_in  [0:N_NUM-1];
  reg  [10:0]      mp_out [0:N_NUM-1];
  reg              tg_in  [0:N_NUM-1];
  reg              tg_out [0:N_NUM-1];

  reg [31:0] look_c, unk_c, halt_c, flip_c, split_c;

  // ---- endpoint 0 is bidirectional, and it is the ONLY one ----
  //
  // A control transfer is one conversation that runs both ways, so EP0's
  // two directions share a halt state and a packet size by definition.
  // Every other endpoint number describes two independent pipes that
  // merely happen to be written with the same digit.
  wire in_range = tok_valid && (tok_ep < N_NUM);
  wire is_ep0   = (tok_ep == 4'd0);

  wire sel_in   = tok_in;

  // The lookup. Note that every read is indexed by BOTH tok_ep and the
  // direction -- there is no path in this module that reads a table entry
  // without having decided which direction it wants.
  wire       e_valid  = in_range && (sel_in ? en_in[tok_ep]  : en_out[tok_ep]);
  wire       e_halted = in_range && (sel_in ? hl_in[tok_ep]  : hl_out[tok_ep]);
  wire [10:0] e_maxp  = in_range ? (sel_in ? mp_in[tok_ep]   : mp_out[tok_ep])
                                 : 11'd0;
  wire       e_toggle = in_range && (sel_in ? tg_in[tok_ep]  : tg_out[tok_ep]);

  assign ep_valid  = e_valid;
  assign ep_halted = e_halted;
  assign ep_maxp   = e_valid ? e_maxp : 11'd0;
  assign ep_toggle = e_toggle;

  assign n_lookup      = look_c;
  assign n_unknown_ep  = unk_c;
  assign n_halted_hit  = halt_c;
  assign n_toggle_flip = flip_c;
  assign n_split_halt  = split_c;

  integer i;

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (i = 0; i < N_NUM; i = i + 1) begin
        // Endpoint 0 exists in both directions from the moment the device
        // powers up -- it is how the host talks to a device it knows
        // nothing about. Every other endpoint exists only once a
        // descriptor has said so.
        en_in[i]  <= (i == 0);
        en_out[i] <= (i == 0);
        hl_in[i]  <= 1'b0;
        hl_out[i] <= 1'b0;
        mp_in[i]  <= (i == 0) ? 11'd64 : 11'd0;
        mp_out[i] <= (i == 0) ? 11'd64 : 11'd0;
        tg_in[i]  <= 1'b0;
        tg_out[i] <= 1'b0;
      end
      look_c  <= 32'd0;
      unk_c   <= 32'd0;
      halt_c  <= 32'd0;
      flip_c  <= 32'd0;
      split_c <= 32'd0;
    end else begin
      // ---- configuration writes one direction of one number ----
      if (cfg_wr && (cfg_ep < N_NUM)) begin
        if (cfg_ep == 4'd0) begin
          // EP0 is bidirectional: configuring it configures both halves,
          // because they are one conversation rather than two pipes.
          en_in[cfg_ep]  <= cfg_enable;
          en_out[cfg_ep] <= cfg_enable;
          mp_in[cfg_ep]  <= cfg_maxp;
          mp_out[cfg_ep] <= cfg_maxp;
        end else if (cfg_in) begin
          en_in[cfg_ep]  <= cfg_enable;
          mp_in[cfg_ep]  <= cfg_maxp;
        end else begin
          en_out[cfg_ep] <= cfg_enable;
          mp_out[cfg_ep] <= cfg_maxp;
        end
      end

      // ---- halt applies to ONE direction ----
      //
      // Except on EP0, where a halt would make the device unrecoverable:
      // clearing a halt is itself a control transfer, so EP0 is not
      // haltable at all.
      if (halt_wr && (halt_ep < N_NUM) && (halt_ep != 4'd0)) begin
        if (halt_in) hl_in[halt_ep]  <= halt_set;
        else         hl_out[halt_ep] <= halt_set;
      end

      // ---- a completed transaction flips ONE toggle ----
      if (tok_valid && in_range && e_valid && !e_halted && xact_ack) begin
        if (sel_in) tg_in[tok_ep]  <= ~tg_in[tok_ep];
        else        tg_out[tok_ep] <= ~tg_out[tok_ep];
        flip_c <= flip_c + 32'd1;
      end

      // ---- counters ----
      if (tok_valid) begin
        look_c <= look_c + 32'd1;
        if (!in_range || !e_valid) unk_c  <= unk_c  + 32'd1;
        if (e_halted)              halt_c <= halt_c + 32'd1;
      end

      // ---- coverage: the directions were driven SEPARATELY ----
      //
      // A halt set on one direction of a number whose other direction is
      // not halted. If this reads zero, every halt in the run moved both
      // directions together and the independence claim was never tested --
      // which is the whole point of the chapter, so the number is published.
      if (halt_wr && (halt_ep < N_NUM) && (halt_ep != 4'd0) && halt_set) begin
        if (halt_in && !hl_out[halt_ep]) split_c <= split_c + 32'd1;
        if (!halt_in && !hl_in[halt_ep]) split_c <= split_c + 32'd1;
      end
    end
  end

endmodule

7. SystemVerilog RTL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  usb_ep_table -- SystemVerilog.
//
//  The eight parallel arrays of the Verilog become ONE array of a struct
//  indexed by direction, which is the right shape: an endpoint's state
//  belongs together, and the direction is part of its address rather
//  than a reason to duplicate every field.
//
//  It also makes the central claim structural. `ep[num][dir]` cannot be
//  written without naming a direction, so the cross-direction leak this
//  chapter is about becomes a thing you have to go out of your way to
//  write rather than a thing you can do by forgetting an index.
//
//  The answer everybody gives is "a logical channel, numbered 0 to 15,
//  with a direction and a maximum packet size". All true, and it hides
//  the fact the question is actually about:
//
//      Endpoint 1 IN and endpoint 1 OUT are TWO DIFFERENT ENDPOINTS.
//
//  Separate buffers, separate data toggles, separate halt states,
//  separate max packet sizes. The number is not an identifier; the pair
//  (number, direction) is. A device with "four endpoints" may have four
//  or eight of them depending on what the descriptors say, and a table
//  indexed by number alone is a device where halting the IN direction
//  silently halts the OUT direction with it.
//
//  Endpoint 0 is the exception that proves the rule: it is the one
//  endpoint that is genuinely bidirectional, because control transfers
//  need both directions of the SAME conversation.
// =====================================================================
module usb_ep_table #(
  parameter int N_NUM = 4            // endpoint numbers 0..N_NUM-1
) (
  input  logic       clk,
  input  logic       rst_n,

  // ---- a token, as it arrives off the bus ----
  input  logic       tok_valid,
  input  logic [3:0] tok_ep,      // endpoint NUMBER
  input  logic       tok_in,      // direction: 1 = IN (device to host)

  // ---- configuration, from the descriptors ----
  input  logic       cfg_wr,
  input  logic [3:0] cfg_ep,
  input  logic       cfg_in,
  input  logic       cfg_enable,
  input  logic [10:0]cfg_maxp,

  // ---- halt control, per (number, direction) ----
  input  logic       halt_wr,
  input  logic [3:0] halt_ep,
  input  logic       halt_in,
  input  logic       halt_set,

  // ---- a transaction completing, which advances the toggle ----
  input  logic       xact_ack,

  // ---- lookup results for the token above ----
  output logic       ep_valid,    // this (number, direction) exists
  output logic       ep_halted,
  output logic [10:0]ep_maxp,
  output logic       ep_toggle,

  // ---- observability ----
  output logic [31:0]n_lookup,
  output logic [31:0]n_unknown_ep,
  output logic [31:0]n_halted_hit,
  output logic [31:0]n_toggle_flip,
  // Evidence that the two directions were driven INDEPENDENTLY: a halt
  // applied to one direction while the other stayed unhalted. This is a
  // coverage counter, not a self-check -- the design cannot detect its own
  // cross-direction leak, because a leak is indistinguishable from a
  // deliberate write from inside the module. The TESTBENCH does that, with
  // an independent shadow of both directions.
  output logic [31:0]n_split_halt
);

  // ---- the table is indexed by (number, direction), never by number ----
  //
  // Two separate arrays rather than one array of structs, so that an index
  // that forgets the direction cannot compile into something plausible.
  // IN and OUT are not two views of one endpoint; they are two endpoints.
  // Every field is indexed [number][direction]. Writing one of these
  // without naming a direction is a compile error, which is exactly the
  // property this chapter is about: the cross-direction leak becomes
  // something you have to go out of your way to write.
  //
  // DIR_OUT is 0 and DIR_IN is 1, matching the token's direction bit, so
  // the token's own field is the index with no translation.
  //
  // A packed struct would read better still, and Icarus Verilog 13 aborts
  // its elaborator on a variable field-select into a 2-D unpacked array of
  // one -- so the fields are separate arrays that share an index shape.
  localparam int DIR_OUT = 0, DIR_IN = 1;
  logic        st_en [N_NUM][2];
  logic        st_hl [N_NUM][2];
  logic [10:0] st_mp [N_NUM][2];
  logic        st_tg [N_NUM][2];

  logic [31:0] look_c, unk_c, halt_c, flip_c, split_c;

  // ---- endpoint 0 is bidirectional, and it is the ONLY one ----
  //
  // A control transfer is one conversation that runs both ways, so EP0's
  // two directions share a halt state and a packet size by definition.
  // Every other endpoint number describes two independent pipes that
  // merely happen to be written with the same digit.
  wire in_range = tok_valid && (tok_ep < N_NUM);
  wire is_ep0   = (tok_ep == 4'd0);

  wire sel_in   = tok_in;
  wire [0:0] d  = tok_in;   // the direction index

  // The lookup. Note that every read is indexed by BOTH tok_ep and the
  // direction -- there is no path in this module that reads a table entry
  // without having decided which direction it wants.
  wire       e_valid  = in_range && st_en[tok_ep][d];
  wire       e_halted = in_range && st_hl[tok_ep][d];
  wire [10:0] e_maxp  = in_range ? st_mp[tok_ep][d] : 11'd0;
  wire       e_toggle = in_range && st_tg[tok_ep][d];

  assign ep_valid  = e_valid;
  assign ep_halted = e_halted;
  assign ep_maxp   = e_valid ? e_maxp : 11'd0;
  assign ep_toggle = e_toggle;

  assign n_lookup      = look_c;
  assign n_unknown_ep  = unk_c;
  assign n_halted_hit  = halt_c;
  assign n_toggle_flip = flip_c;
  assign n_split_halt  = split_c;


  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int i = 0; i < N_NUM; i++) begin
        // Endpoint 0 exists in both directions from the moment the device
        // powers up -- it is how the host talks to a device it knows
        // nothing about. Every other endpoint exists only once a
        // descriptor has said so.
        for (int j = 0; j < 2; j++) begin
          st_en[i][j]     <= (i == 0);
          st_hl[i][j] <= 1'b0;
          st_mp[i][j]   <= (i == 0) ? 11'd64 : 11'd0;
          st_tg[i][j] <= 1'b0;
        end
      end
      look_c  <= 32'd0;
      unk_c   <= 32'd0;
      halt_c  <= 32'd0;
      flip_c  <= 32'd0;
      split_c <= 32'd0;
    end else begin
      // ---- configuration writes one direction of one number ----
      if (cfg_wr && (cfg_ep < N_NUM)) begin
        if (cfg_ep == 4'd0) begin
          // EP0 is bidirectional: configuring it configures both halves,
          // because they are one conversation rather than two pipes.
          for (int j = 0; j < 2; j++) begin
            st_en[cfg_ep][j]   <= cfg_enable;
            st_mp[cfg_ep][j] <= cfg_maxp;
          end
        end else begin
          st_en[cfg_ep][cfg_in]   <= cfg_enable;
          st_mp[cfg_ep][cfg_in] <= cfg_maxp;
        end
      end

      // ---- halt applies to ONE direction ----
      //
      // Except on EP0, where a halt would make the device unrecoverable:
      // clearing a halt is itself a control transfer, so EP0 is not
      // haltable at all.
      if (halt_wr && (halt_ep < N_NUM) && (halt_ep != 4'd0)) begin
        st_hl[halt_ep][halt_in] <= halt_set;
      end

      // ---- a completed transaction flips ONE toggle ----
      if (tok_valid && in_range && e_valid && !e_halted && xact_ack) begin
        st_tg[tok_ep][d] <= ~st_tg[tok_ep][d];
        flip_c <= flip_c + 32'd1;
      end

      // ---- counters ----
      if (tok_valid) begin
        look_c <= look_c + 32'd1;
        if (!in_range || !e_valid) unk_c  <= unk_c  + 32'd1;
        if (e_halted)              halt_c <= halt_c + 32'd1;
      end

      // ---- coverage: the directions were driven SEPARATELY ----
      //
      // A halt set on one direction of a number whose other direction is
      // not halted. If this reads zero, every halt in the run moved both
      // directions together and the independence claim was never tested --
      // which is the whole point of the chapter, so the number is published.
      if (halt_wr && (halt_ep < N_NUM) && (halt_ep != 4'd0) && halt_set) begin
        if (!st_hl[halt_ep][!halt_in]) split_c <= split_c + 32'd1;
      end
    end
  end

endmodule

8. VHDL-2008 RTL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- =====================================================================
--  usb_ep_table -- VHDL-2008.
--
--  VHDL gets the shape the SystemVerilog wanted: a record per endpoint
--  state, in a two-dimensional array indexed by (number, direction).
--  There is no way to write an entry without naming a direction, which
--  is the property this chapter is about -- a cross-direction leak has
--  to be written deliberately rather than by forgetting an index.
--
--  The Verilog carries eight parallel one-dimensional arrays instead,
--  because Verilog-2005 has neither records nor multi-dimensional
--  arrays of them. Eight arrays that must be kept in step is exactly
--  the situation in which a direction gets forgotten.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

package ep_pkg is
  -- DIR_OUT is 0 and DIR_IN is 1, matching the token's direction bit, so
  -- the token's own field indexes the table with no translation.
  constant DIR_OUT : natural := 0;
  constant DIR_IN  : natural := 1;

  type ep_state_t is record
    en     : std_logic;
    halted : std_logic;
    maxp   : unsigned(10 downto 0);
    toggle : std_logic;
  end record;

  type ep_table_t is array (natural range <>, natural range <>) of ep_state_t;
end package;

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

entity usb_ep_table is
  generic (
    N_NUM : natural := 4
  );
  port (
    clk        : in  std_logic;
    rst_n      : in  std_logic;

    tok_valid  : in  std_logic;
    tok_ep     : in  std_logic_vector(3 downto 0);
    tok_in     : in  std_logic;

    cfg_wr     : in  std_logic;
    cfg_ep     : in  std_logic_vector(3 downto 0);
    cfg_in     : in  std_logic;
    cfg_enable : in  std_logic;
    cfg_maxp   : in  std_logic_vector(10 downto 0);

    halt_wr    : in  std_logic;
    halt_ep    : in  std_logic_vector(3 downto 0);
    halt_in    : in  std_logic;
    halt_set   : in  std_logic;

    xact_ack   : in  std_logic;

    ep_valid   : out std_logic;
    ep_halted  : out std_logic;
    ep_maxp    : out std_logic_vector(10 downto 0);
    ep_toggle  : out std_logic;

    n_lookup      : out std_logic_vector(31 downto 0);
    n_unknown_ep  : out std_logic_vector(31 downto 0);
    n_halted_hit  : out std_logic_vector(31 downto 0);
    n_toggle_flip : out std_logic_vector(31 downto 0);
    n_split_halt  : out std_logic_vector(31 downto 0)
  );
end entity;

architecture rtl of usb_ep_table is
  signal tbl : ep_table_t(0 to N_NUM-1, 0 to 1);

  signal look_c, unk_c, halt_c, flip_c, split_c : unsigned(31 downto 0)
    := (others => '0');

  signal in_range : std_logic;
  signal e_valid, e_halted, e_toggle : std_logic;
  signal e_maxp : unsigned(10 downto 0);

  -- The direction index, derived once from the token's own bit.
  function dir_of(b : std_logic) return natural is
  begin
    if b = '1' then return DIR_IN; else return DIR_OUT; end if;
  end function;
begin

  in_range <= '1' when (tok_valid = '1' and
                        to_integer(unsigned(tok_ep)) < N_NUM) else '0';

  -- Every read names BOTH the number and the direction. There is no path
  -- through this architecture that reads a table entry without one.
  -- The bound check is made INSIDE this process, from tok_ep directly.
  -- Gating on the separate `in_range` signal is a crash rather than a
  -- mis-read: a signal lags its inputs within a delta cycle, so tok_ep can
  -- already be 13 while in_range is still '1' from the previous value, and
  -- VHDL's range checking then aborts the run rather than returning
  -- rubbish the way Verilog would.
  lookup : process(all)
    variable n, d : natural;
  begin
    n := to_integer(unsigned(tok_ep));
    if tok_valid = '1' and n < N_NUM then
      d := dir_of(tok_in);
      e_valid  <= tbl(n, d).en;
      e_halted <= tbl(n, d).halted;
      e_maxp   <= tbl(n, d).maxp;
      e_toggle <= tbl(n, d).toggle;
    else
      e_valid  <= '0';
      e_halted <= '0';
      e_maxp   <= (others => '0');
      e_toggle <= '0';
    end if;
  end process;

  ep_valid  <= e_valid;
  ep_halted <= e_halted;
  ep_toggle <= e_toggle;
  ep_maxp   <= std_logic_vector(e_maxp) when e_valid = '1'
               else (others => '0');

  n_lookup      <= std_logic_vector(look_c);
  n_unknown_ep  <= std_logic_vector(unk_c);
  n_halted_hit  <= std_logic_vector(halt_c);
  n_toggle_flip <= std_logic_vector(flip_c);
  n_split_halt  <= std_logic_vector(split_c);

  main : process(clk, rst_n)
    variable cn, hn, tn, cd, hd, td : natural;
  begin
    if rst_n = '0' then
      for i in 0 to N_NUM-1 loop
        for j in 0 to 1 loop
          -- Endpoint 0 exists in both directions from power-up: it is how
          -- the host talks to a device it knows nothing about. Every other
          -- endpoint exists only once a descriptor has said so.
          if i = 0 then
            tbl(i, j).en   <= '1';
            tbl(i, j).maxp <= to_unsigned(64, 11);
          else
            tbl(i, j).en   <= '0';
            tbl(i, j).maxp <= (others => '0');
          end if;
          tbl(i, j).halted <= '0';
          tbl(i, j).toggle <= '0';
        end loop;
      end loop;
      look_c  <= (others => '0');
      unk_c   <= (others => '0');
      halt_c  <= (others => '0');
      flip_c  <= (others => '0');
      split_c <= (others => '0');

    elsif rising_edge(clk) then
      -- ---- configuration writes one direction of one number ----
      if cfg_wr = '1' and to_integer(unsigned(cfg_ep)) < N_NUM then
        cn := to_integer(unsigned(cfg_ep));
        cd := dir_of(cfg_in);
        if cn = 0 then
          -- EP0 is bidirectional: configuring it configures both halves,
          -- because they are one conversation rather than two pipes.
          for j in 0 to 1 loop
            tbl(cn, j).en   <= cfg_enable;
            tbl(cn, j).maxp <= unsigned(cfg_maxp);
          end loop;
        else
          tbl(cn, cd).en   <= cfg_enable;
          tbl(cn, cd).maxp <= unsigned(cfg_maxp);
        end if;
      end if;

      -- ---- halt applies to ONE direction ----
      --
      -- Except on EP0, where a halt would make the device unrecoverable:
      -- clearing a halt is itself a control transfer.
      if halt_wr = '1' and to_integer(unsigned(halt_ep)) < N_NUM
         and to_integer(unsigned(halt_ep)) /= 0 then
        hn := to_integer(unsigned(halt_ep));
        hd := dir_of(halt_in);
        tbl(hn, hd).halted <= halt_set;
      end if;

      -- ---- coverage: the directions were driven SEPARATELY ----
      --
      -- A halt SET on one direction whose other direction is not halted.
      -- Zero here means every halt in the run moved both directions
      -- together and the independence claim was never tested.
      --
      -- Deliberately its OWN block with its own endpoint-0 exclusion, so
      -- that it matches the Verilog statement for statement: folding it
      -- inside the halt-write block above gave mutation D4 an extra effect
      -- in VHDL that it does not have in Verilog, and the two directed
      -- columns then read 296 against 319.
      if halt_wr = '1' and to_integer(unsigned(halt_ep)) < N_NUM
         and to_integer(unsigned(halt_ep)) /= 0 and halt_set = '1' then
        hn := to_integer(unsigned(halt_ep));
        hd := dir_of(halt_in);
        if tbl(hn, 1 - hd).halted = '0' then
          split_c <= split_c + 1;
        end if;
      end if;

      -- ---- a completed transaction flips ONE toggle ----
      if tok_valid = '1' and in_range = '1' and e_valid = '1'
         and e_halted = '0' and xact_ack = '1' then
        tn := to_integer(unsigned(tok_ep));
        td := dir_of(tok_in);
        tbl(tn, td).toggle <= not tbl(tn, td).toggle;
        flip_c <= flip_c + 1;
      end if;

      -- ---- counters ----
      if tok_valid = '1' then
        look_c <= look_c + 1;
        if in_range = '0' or e_valid = '0' then unk_c  <= unk_c  + 1; end if;
        if e_halted = '1' then                  halt_c <= halt_c + 1; end if;
      end if;
    end if;
  end process;

end architecture;

9. The Testbench, and the Only Check That Can See a Leak

The headline property is negative and it is about independence, so the bench does something deliberately expensive: after every single operation it compares both directions of every endpoint number. Not the direction that was written. All of them.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   Checking only the targeted direction:

     halt(1, IN);  check halted(1, IN) == 1   -- PASSES

   A design that halts both directions passes that check
   perfectly. The write landed correctly. It also landed
   somewhere nobody looked.

   Checking every direction of every number:

     halt(1, IN);
     for n in 0..3: for d in {IN, OUT}:
        compare against an INDEPENDENT shadow   -- FAILS

The shadow keeps the two directions in completely separate arrays — s_en_in / s_en_out and so on — and never merges them, so a leak in the design cannot be mirrored by a leak in the model.

Verilog-2005 testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  Testbench for usb_ep_table.
//
//  The headline property is a NEGATIVE one about independence: touching
//  one direction of an endpoint number must never move the other. So the
//  shadow keeps the two directions in completely separate arrays and, after
//  every single operation, compares BOTH directions of EVERY number --
//  not only the one that was targeted.
//
//  Checking only the targeted direction is how a cross-direction leak
//  survives a testbench: the write landed correctly, and it also landed
//  somewhere nobody looked.
// =====================================================================
`timescale 1ns/1ps
module tb_ep_v;

  localparam N_NUM = 4;

  reg         clk = 1'b0, rst_n = 1'b0;
  reg         tok_valid = 1'b0;
  reg  [3:0]  tok_ep = 4'd0;
  reg         tok_in = 1'b0;
  reg         cfg_wr = 1'b0;
  reg  [3:0]  cfg_ep = 4'd0;
  reg         cfg_in = 1'b0;
  reg         cfg_enable = 1'b0;
  reg  [10:0] cfg_maxp = 11'd0;
  reg         halt_wr = 1'b0;
  reg  [3:0]  halt_ep = 4'd0;
  reg         halt_in = 1'b0;
  reg         halt_set = 1'b0;
  reg         xact_ack = 1'b0;

  wire        ep_valid, ep_halted, ep_toggle;
  wire [10:0] ep_maxp;
  wire [31:0] n_lookup, n_unknown_ep, n_halted_hit, n_toggle_flip, n_split_halt;

  usb_ep_table #(.N_NUM(N_NUM)) dut (
    .clk(clk), .rst_n(rst_n),
    .tok_valid(tok_valid), .tok_ep(tok_ep), .tok_in(tok_in),
    .cfg_wr(cfg_wr), .cfg_ep(cfg_ep), .cfg_in(cfg_in),
    .cfg_enable(cfg_enable), .cfg_maxp(cfg_maxp),
    .halt_wr(halt_wr), .halt_ep(halt_ep), .halt_in(halt_in),
    .halt_set(halt_set),
    .xact_ack(xact_ack),
    .ep_valid(ep_valid), .ep_halted(ep_halted),
    .ep_maxp(ep_maxp), .ep_toggle(ep_toggle),
    .n_lookup(n_lookup), .n_unknown_ep(n_unknown_ep),
    .n_halted_hit(n_halted_hit), .n_toggle_flip(n_toggle_flip),
    .n_split_halt(n_split_halt)
  );

  always #5 clk = ~clk;

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

  // ---- $random is SIGNED: mask the sign bit before any modulo ----
  function [31:0] urand;
    input dummy;
    begin urand = $random(seed) & 32'h3FFF_FFFF; end
  endfunction

  // ---- the shadow: two directions, two sets of arrays, never merged ----
  reg         s_en_in  [0:N_NUM-1];
  reg         s_en_out [0:N_NUM-1];
  reg         s_hl_in  [0:N_NUM-1];
  reg         s_hl_out [0:N_NUM-1];
  reg [10:0]  s_mp_in  [0:N_NUM-1];
  reg [10:0]  s_mp_out [0:N_NUM-1];
  reg         s_tg_in  [0:N_NUM-1];
  reg         s_tg_out [0:N_NUM-1];
  reg [31:0]  x_look, x_unk, x_halt, x_flip, x_split;
  // Run-wide totals. The DUT's counters are cleared by every reset, so a
  // summary printed from them describes the last window rather than the run.
  integer g_look = 0, g_unk = 0, g_halt = 0, g_flip = 0, g_split = 0;

  // ---- the headline counter ----
  //
  // Every cycle in which any direction of any number disagreed with the
  // shadow. A cross-direction leak shows up here and nowhere else, because
  // the direction that was written is always correct.
  integer n_cross = 0;

  // ---- exhaustive reach over (number, dir, enabled, halted, toggle) ----
  reg reach [0:63];
  integer ri, n_reach;

  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

  // ---------------------------------------------------------------
  //  Compare EVERY direction of EVERY number against the shadow.
  //
  //  Called after every operation. This is the expensive way to do it
  //  and it is the only way that can see a leak.
  // ---------------------------------------------------------------
  task audit_all;
    integer a;
    begin
      for (a = 0; a < N_NUM; a = a + 1) begin
        // drive a lookup at (a, IN) and check it, then (a, OUT)
        tok_valid = 1'b1; tok_ep = a[3:0]; tok_in = 1'b1;
        #1;
        if (ep_valid  !== s_en_in[a]) n_cross = n_cross + 1;
        if (ep_halted !== s_hl_in[a]) n_cross = n_cross + 1;
        if (ep_toggle !== s_tg_in[a]) n_cross = n_cross + 1;
        ck(ep_valid  === s_en_in[a], "IN  enable  disagrees");
        ck(ep_halted === s_hl_in[a], "IN  halt    disagrees");
        ck(ep_toggle === s_tg_in[a], "IN  toggle  disagrees");
        ck(ep_maxp   === (s_en_in[a] ? s_mp_in[a] : 11'd0),
           "IN  maxp    disagrees");

        tok_in = 1'b0;
        #1;
        if (ep_valid  !== s_en_out[a]) n_cross = n_cross + 1;
        if (ep_halted !== s_hl_out[a]) n_cross = n_cross + 1;
        if (ep_toggle !== s_tg_out[a]) n_cross = n_cross + 1;
        ck(ep_valid  === s_en_out[a], "OUT enable  disagrees");
        ck(ep_halted === s_hl_out[a], "OUT halt    disagrees");
        ck(ep_toggle === s_tg_out[a], "OUT toggle  disagrees");
        ck(ep_maxp   === (s_en_out[a] ? s_mp_out[a] : 11'd0),
           "OUT maxp    disagrees");
      end
      tok_valid = 1'b0;
      ck(n_cross == 0, "a direction changed that nothing touched");
    end
  endtask

  // ---------------------------------------------------------------
  //  A combinational lookup with NO clocked side effect.
  //
  //  Positioned at a negedge deliberately. Driving tok_valid and then
  //  waiting #1 lands exactly on the next posedge once the preceding audit
  //  has consumed its 8 ns -- the DUT then counts a lookup the shadow knows
  //  nothing about. Two errors, entirely a testbench timing accident.
  // ---------------------------------------------------------------
  task peek(input [3:0] e, input d);
    begin
      @(negedge clk);
      tok_valid = 1'b1; tok_ep = e; tok_in = d;
      #1;
    end
  endtask

  // ---------------------------------------------------------------
  //  One clocked operation, then a full audit.
  // ---------------------------------------------------------------
  task op(input cw, input [3:0] ce, input ci, input cen, input [10:0] cm,
          input hw, input [3:0] he, input hi, input hs,
          input tv, input [3:0] te, input ti, input xa);
    begin
      cfg_wr = cw;  cfg_ep = ce;  cfg_in = ci;
      cfg_enable = cen;  cfg_maxp = cm;
      halt_wr = hw; halt_ep = he; halt_in = hi; halt_set = hs;
      tok_valid = tv; tok_ep = te; tok_in = ti; xact_ack = xa;

      // ---- advance the shadow ----
      //
      // The LOOKUP is evaluated first, because the design's lookup is a
      // combinational read of REGISTERED table entries -- it sees the
      // values as they stood before this edge. A shadow that applies the
      // configuration write first sees the new values and disagrees on
      // every cycle that configures and looks up at once.
      if (tv && (te < N_NUM)) begin
        x_look = x_look + 1;  g_look = g_look + 1;
        if (ti) begin
          if (!s_en_in[te])  begin x_unk = x_unk + 1; g_unk = g_unk + 1; end
          if (s_hl_in[te])   begin x_halt = x_halt + 1; g_halt = g_halt + 1; end
          if (s_en_in[te] && !s_hl_in[te] && xa) begin
            s_tg_in[te] = ~s_tg_in[te];
            x_flip = x_flip + 1;  g_flip = g_flip + 1;
          end
        end else begin
          if (!s_en_out[te]) begin x_unk = x_unk + 1; g_unk = g_unk + 1; end
          if (s_hl_out[te])  begin x_halt = x_halt + 1; g_halt = g_halt + 1; end
          if (s_en_out[te] && !s_hl_out[te] && xa) begin
            s_tg_out[te] = ~s_tg_out[te];
            x_flip = x_flip + 1;  g_flip = g_flip + 1;
          end
        end
      end else if (tv) begin
        x_look = x_look + 1;  g_look = g_look + 1;
        x_unk  = x_unk + 1;  g_unk = g_unk + 1;
      end

      if (cw && (ce < N_NUM)) begin
        if (ce == 4'd0) begin
          // EP0 is bidirectional: one conversation, both halves
          s_en_in[ce]  = cen;  s_en_out[ce] = cen;
          s_mp_in[ce]  = cm;   s_mp_out[ce] = cm;
        end else if (ci) begin
          s_en_in[ce]  = cen;  s_mp_in[ce]  = cm;
        end else begin
          s_en_out[ce] = cen;  s_mp_out[ce] = cm;
        end
      end

      if (hw && (he < N_NUM) && (he != 4'd0)) begin
        // The design counts a SPLIT HALT only when a halt is being SET,
        // not when it is cleared. Omitting `hs` here made the shadow count
        // clears as well -- 3202 errors, none of them the design.
        if (hs) begin
          if (hi && !s_hl_out[he]) begin x_split = x_split + 1; g_split = g_split + 1; end
          if (!hi && !s_hl_in[he]) begin x_split = x_split + 1; g_split = g_split + 1; end
        end
        if (hi) s_hl_in[he]  = hs;
        else    s_hl_out[he] = hs;
      end

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

      cfg_wr = 1'b0; halt_wr = 1'b0; tok_valid = 1'b0; xact_ack = 1'b0;

      ck(n_lookup      === x_look,  "lookup count disagrees");
      ck(n_unknown_ep  === x_unk,   "unknown-endpoint count disagrees");
      ck(n_halted_hit  === x_halt,  "halted-hit count disagrees");
      ck(n_toggle_flip === x_flip,  "toggle-flip count disagrees");
      ck(n_split_halt  === x_split, "split-halt count disagrees");

      audit_all;
    end
  endtask

  task reset_dut;
    integer a;
    begin
      rst_n = 1'b0;
      cfg_wr = 0; halt_wr = 0; tok_valid = 0; xact_ack = 0;
      @(posedge clk); @(posedge clk);
      rst_n = 1'b1;
      for (a = 0; a < N_NUM; a = a + 1) begin
        s_en_in[a]  = (a == 0);  s_en_out[a] = (a == 0);
        s_hl_in[a]  = 1'b0;      s_hl_out[a] = 1'b0;
        s_mp_in[a]  = (a == 0) ? 11'd64 : 11'd0;
        s_mp_out[a] = (a == 0) ? 11'd64 : 11'd0;
        s_tg_in[a]  = 1'b0;      s_tg_out[a] = 1'b0;
      end
      x_look = 0; x_unk = 0; x_halt = 0; x_flip = 0; x_split = 0;
      @(posedge clk); #1;
    end
  endtask

  integer ei, di, en, hl, tg, k, a2;

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

    // =============================================================
    //  PHASE 1 (DIRECTED, EXHAUSTIVE) -- every (number, direction) in
    //  every (enabled, halted, toggle) state. 4 x 2 x 2 x 2 x 2 = 64.
    // =============================================================
    for (ei = 0; ei < N_NUM; ei = ei + 1)
    for (di = 0; di < 2; di = di + 1)
    for (en = 0; en < 2; en = en + 1)
    for (hl = 0; hl < 2; hl = hl + 1)
    for (tg = 0; tg < 2; tg = tg + 1) begin
      reset_dut;
      // configure this direction
      op(1'b1, ei[3:0], di[0], en[0], 11'd512,
         1'b0, 4'd0, 1'b0, 1'b0,  1'b0, 4'd0, 1'b0, 1'b0);
      // halt it if wanted
      if (hl) op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
                 1'b1, ei[3:0], di[0], 1'b1,  1'b0, 4'd0, 1'b0, 1'b0);
      // flip the toggle if wanted
      if (tg) op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
                 1'b0, 4'd0, 1'b0, 1'b0,  1'b1, ei[3:0], di[0], 1'b1);
      // and look it up
      op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
         1'b0, 4'd0, 1'b0, 1'b0,  1'b1, ei[3:0], di[0], 1'b0);

      ri = (ei << 4) | (di << 3) | (en << 2) | (hl << 1) | tg;
      reach[ri] = 1'b1;
    end

    // =============================================================
    //  PHASE 2 (DIRECTED, EXHAUSTIVE) -- THE CHAPTER'S POINT.
    //
    //  For every endpoint number except 0, enable BOTH directions, then
    //  halt exactly ONE of them, and prove the other is untouched --
    //  enable, halt, max packet size and toggle all independently.
    //
    //  Both orders, both directions: 3 numbers x 2 directions x 2 orders.
    // =============================================================
    for (ei = 1; ei < N_NUM; ei = ei + 1)
    for (di = 0; di < 2; di = di + 1)
    for (k = 0; k < 2; k = k + 1) begin
      reset_dut;
      // both directions exist, with DIFFERENT packet sizes so a shared
      // field cannot pass by accident
      op(1'b1, ei[3:0], 1'b1, 1'b1, 11'd64,
         1'b0, 4'd0, 1'b0, 1'b0,  1'b0, 4'd0, 1'b0, 1'b0);
      op(1'b1, ei[3:0], 1'b0, 1'b1, 11'd512,
         1'b0, 4'd0, 1'b0, 1'b0,  1'b0, 4'd0, 1'b0, 1'b0);

      if (k == 0) begin
        // advance one toggle, then halt one direction
        op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
           1'b0, 4'd0, 1'b0, 1'b0,  1'b1, ei[3:0], di[0], 1'b1);
        op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
           1'b1, ei[3:0], di[0], 1'b1,  1'b0, 4'd0, 1'b0, 1'b0);
      end else begin
        // halt one direction, then try to use the OTHER
        op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
           1'b1, ei[3:0], di[0], 1'b1,  1'b0, 4'd0, 1'b0, 1'b0);
        op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
           1'b0, 4'd0, 1'b0, 1'b0,  1'b1, ei[3:0], ~di[0], 1'b1);
      end

      // the untouched direction must still work
      peek(ei[3:0], ~di[0]);
      ck(ep_valid === 1'b1, "the other direction stopped existing");
      ck(ep_halted === 1'b0, "halting one direction halted the other");
      tok_valid = 1'b0;

      // and clearing the halt leaves the other alone too
      op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
         1'b1, ei[3:0], di[0], 1'b0,  1'b0, 4'd0, 1'b0, 1'b0);
    end

    // =============================================================
    //  PHASE 3 (DIRECTED) -- endpoint 0 is the exception.
    //
    //  It is bidirectional, it exists from reset, and it is not
    //  haltable: clearing a halt is itself a control transfer, so a
    //  device that could halt EP0 would be unrecoverable.
    // =============================================================
    reset_dut;
    for (di = 0; di < 2; di = di + 1) begin
      // it exists in both directions before anything configures it
      peek(4'd0, di[0]);
      ck(ep_valid === 1'b1, "EP0 did not exist at reset");
      ck(ep_maxp  === 11'd64, "EP0 had no default packet size");
      tok_valid = 1'b0;
      // and a halt attempt does nothing
      op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
         1'b1, 4'd0, di[0], 1'b1,  1'b0, 4'd0, 1'b0, 1'b0);
      peek(4'd0, di[0]);
      ck(ep_halted === 1'b0, "EP0 was halted, which is unrecoverable");
      tok_valid = 1'b0;
    end
    // configuring EP0 configures both halves
    op(1'b1, 4'd0, 1'b1, 1'b1, 11'd8,
       1'b0, 4'd0, 1'b0, 1'b0,  1'b0, 4'd0, 1'b0, 1'b0);
    for (di = 0; di < 2; di = di + 1) begin
      peek(4'd0, di[0]);
      ck(ep_maxp === 11'd8, "configuring EP0 did not apply to both directions");
      tok_valid = 1'b0;
    end

    // =============================================================
    //  PHASE 4 (DIRECTED) -- an endpoint number that does not exist.
    // =============================================================
    reset_dut;
    for (k = N_NUM; k < 16; k = k + 1)
    for (di = 0; di < 2; di = di + 1) begin
      peek(k[3:0], di[0]);
      ck(ep_valid === 1'b0, "an out-of-range endpoint reported valid");
      ck(ep_maxp  === 11'd0, "an out-of-range endpoint reported a packet size");
      tok_valid = 1'b0;
      @(posedge clk); #1;
    end

    // =============================================================
    //  PHASE 5 (RANDOM) -- a driver configuring and halting freely.
    // =============================================================
`ifndef DIRECTED_ONLY
    reset_dut;
    for (k = 0; k < 8000; k = k + 1) begin
      op((urand(0) % 4) == 0, urand(0) % 16, (urand(0) % 2) == 0,
         (urand(0) % 4) != 0, (urand(0) % 2) ? 11'd64 : 11'd512,
         (urand(0) % 5) == 0, urand(0) % 16, (urand(0) % 2) == 0,
         (urand(0) % 3) != 0,
         (urand(0) % 2) == 0, urand(0) % 16, (urand(0) % 2) == 0,
         (urand(0) % 3) != 0);
      if ((k % 128) == 127) reset_dut;
    end
`endif

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

    $display("steps=%0d checks=%0d reach=%0d/64 errors=%0d",
             steps, checks, n_reach, errors);
    $display("[ep] lookups=%0d unknown=%0d halted_hits=%0d toggles=%0d split_halts=%0d",
             g_look, g_unk, g_halt, g_flip, g_split);
    $display("[the whole point] cross-direction leaks = %0d", n_cross);
    if (n_reach != 64) 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_ep_table.
//
//  The headline property is a NEGATIVE one about independence: touching
//  one direction of an endpoint number must never move the other. So the
//  shadow keeps the two directions in completely separate arrays and, after
//  every single operation, compares BOTH directions of EVERY number --
//  not only the one that was targeted.
//
//  Checking only the targeted direction is how a cross-direction leak
//  survives a testbench: the write landed correctly, and it also landed
//  somewhere nobody looked.
// =====================================================================
`timescale 1ns/1ps
module tb_ep_sv;

  localparam N_NUM = 4;

  logic       clk = 1'b0, rst_n = 1'b0;
  logic       tok_valid = 1'b0;
  logic [3:0] tok_ep = 4'd0;
  logic       tok_in = 1'b0;
  logic       cfg_wr = 1'b0;
  logic [3:0] cfg_ep = 4'd0;
  logic       cfg_in = 1'b0;
  logic       cfg_enable = 1'b0;
  logic [10:0] cfg_maxp = 11'd0;
  logic       halt_wr = 1'b0;
  logic [3:0] halt_ep = 4'd0;
  logic       halt_in = 1'b0;
  logic       halt_set = 1'b0;
  logic       xact_ack = 1'b0;

  logic       ep_valid, ep_halted, ep_toggle;
  logic [10:0] ep_maxp;
  logic [31:0] n_lookup, n_unknown_ep, n_halted_hit, n_toggle_flip, n_split_halt;

  usb_ep_table #(.N_NUM(N_NUM)) dut (
    .clk(clk), .rst_n(rst_n),
    .tok_valid(tok_valid), .tok_ep(tok_ep), .tok_in(tok_in),
    .cfg_wr(cfg_wr), .cfg_ep(cfg_ep), .cfg_in(cfg_in),
    .cfg_enable(cfg_enable), .cfg_maxp(cfg_maxp),
    .halt_wr(halt_wr), .halt_ep(halt_ep), .halt_in(halt_in),
    .halt_set(halt_set),
    .xact_ack(xact_ack),
    .ep_valid(ep_valid), .ep_halted(ep_halted),
    .ep_maxp(ep_maxp), .ep_toggle(ep_toggle),
    .n_lookup(n_lookup), .n_unknown_ep(n_unknown_ep),
    .n_halted_hit(n_halted_hit), .n_toggle_flip(n_toggle_flip),
    .n_split_halt(n_split_halt)
  );

  always #5 clk = ~clk;

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

  // ---- $random is SIGNED: mask the sign bit before any modulo ----
  function automatic logic [31:0] urand(bit dummy);
    return $random(seed) & 32'h3FFF_FFFF;
  endfunction

  // ---- the shadow: two directions, two sets of arrays, never merged ----
  logic       s_en_in  [0:N_NUM-1];
  logic       s_en_out [0:N_NUM-1];
  logic       s_hl_in  [0:N_NUM-1];
  logic       s_hl_out [0:N_NUM-1];
  logic [10:0] s_mp_in  [0:N_NUM-1];
  logic [10:0] s_mp_out [0:N_NUM-1];
  logic       s_tg_in  [0:N_NUM-1];
  logic       s_tg_out [0:N_NUM-1];
  logic [31:0] x_look, x_unk, x_halt, x_flip, x_split;
  // Run-wide totals. The DUT's counters are cleared by every reset, so a
  // summary printed from them describes the last window rather than the run.
  integer g_look = 0, g_unk = 0, g_halt = 0, g_flip = 0, g_split = 0;

  // ---- the headline counter ----
  //
  // Every cycle in which any direction of any number disagreed with the
  // shadow. A cross-direction leak shows up here and nowhere else, because
  // the direction that was written is always correct.
  integer n_cross = 0;

  // ---- exhaustive reach over (number, dir, enabled, halted, toggle) ----
  logic reach [0:63];
  integer ri, n_reach;

  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

  // ---------------------------------------------------------------
  //  Compare EVERY direction of EVERY number against the shadow.
  //
  //  Called after every operation. This is the expensive way to do it
  //  and it is the only way that can see a leak.
  // ---------------------------------------------------------------
  task audit_all;
    integer a;
    begin
      for (a = 0; a < N_NUM; a = a + 1) begin
        // drive a lookup at (a, IN) and check it, then (a, OUT)
        tok_valid = 1'b1; tok_ep = a[3:0]; tok_in = 1'b1;
        #1;
        if (ep_valid  !== s_en_in[a]) n_cross = n_cross + 1;
        if (ep_halted !== s_hl_in[a]) n_cross = n_cross + 1;
        if (ep_toggle !== s_tg_in[a]) n_cross = n_cross + 1;
        ck(ep_valid  === s_en_in[a], "IN  enable  disagrees");
        ck(ep_halted === s_hl_in[a], "IN  halt    disagrees");
        ck(ep_toggle === s_tg_in[a], "IN  toggle  disagrees");
        ck(ep_maxp   === (s_en_in[a] ? s_mp_in[a] : 11'd0),
           "IN  maxp    disagrees");

        tok_in = 1'b0;
        #1;
        if (ep_valid  !== s_en_out[a]) n_cross = n_cross + 1;
        if (ep_halted !== s_hl_out[a]) n_cross = n_cross + 1;
        if (ep_toggle !== s_tg_out[a]) n_cross = n_cross + 1;
        ck(ep_valid  === s_en_out[a], "OUT enable  disagrees");
        ck(ep_halted === s_hl_out[a], "OUT halt    disagrees");
        ck(ep_toggle === s_tg_out[a], "OUT toggle  disagrees");
        ck(ep_maxp   === (s_en_out[a] ? s_mp_out[a] : 11'd0),
           "OUT maxp    disagrees");
      end
      tok_valid = 1'b0;
      ck(n_cross == 0, "a direction changed that nothing touched");
    end
  endtask

  // ---------------------------------------------------------------
  //  A combinational lookup with NO clocked side effect.
  //
  //  Positioned at a negedge deliberately. Driving tok_valid and then
  //  waiting #1 lands exactly on the next posedge once the preceding audit
  //  has consumed its 8 ns -- the DUT then counts a lookup the shadow knows
  //  nothing about. Two errors, entirely a testbench timing accident.
  // ---------------------------------------------------------------
  task peek(input logic [3:0] e, input logic d);
    begin
      @(negedge clk);
      tok_valid = 1'b1; tok_ep = e; tok_in = d;
      #1;
    end
  endtask

  // ---------------------------------------------------------------
  //  One clocked operation, then a full audit.
  // ---------------------------------------------------------------
  task op(input logic cw, input logic [3:0] ce, input logic ci, input logic cen,
          input logic [10:0] cm,
          input logic hw, input logic [3:0] he, input logic hi, input logic hs,
          input logic tv, input logic [3:0] te, input logic ti, input logic xa);
    begin
      cfg_wr = cw;  cfg_ep = ce;  cfg_in = ci;
      cfg_enable = cen;  cfg_maxp = cm;
      halt_wr = hw; halt_ep = he; halt_in = hi; halt_set = hs;
      tok_valid = tv; tok_ep = te; tok_in = ti; xact_ack = xa;

      // ---- advance the shadow ----
      //
      // The LOOKUP is evaluated first, because the design's lookup is a
      // combinational read of REGISTERED table entries -- it sees the
      // values as they stood before this edge. A shadow that applies the
      // configuration write first sees the new values and disagrees on
      // every cycle that configures and looks up at once.
      if (tv && (te < N_NUM)) begin
        x_look = x_look + 1;  g_look = g_look + 1;
        if (ti) begin
          if (!s_en_in[te])  begin x_unk = x_unk + 1; g_unk = g_unk + 1; end
          if (s_hl_in[te])   begin x_halt = x_halt + 1; g_halt = g_halt + 1; end
          if (s_en_in[te] && !s_hl_in[te] && xa) begin
            s_tg_in[te] = ~s_tg_in[te];
            x_flip = x_flip + 1;  g_flip = g_flip + 1;
          end
        end else begin
          if (!s_en_out[te]) begin x_unk = x_unk + 1; g_unk = g_unk + 1; end
          if (s_hl_out[te])  begin x_halt = x_halt + 1; g_halt = g_halt + 1; end
          if (s_en_out[te] && !s_hl_out[te] && xa) begin
            s_tg_out[te] = ~s_tg_out[te];
            x_flip = x_flip + 1;  g_flip = g_flip + 1;
          end
        end
      end else if (tv) begin
        x_look = x_look + 1;  g_look = g_look + 1;
        x_unk  = x_unk + 1;  g_unk = g_unk + 1;
      end

      if (cw && (ce < N_NUM)) begin
        if (ce == 4'd0) begin
          // EP0 is bidirectional: one conversation, both halves
          s_en_in[ce]  = cen;  s_en_out[ce] = cen;
          s_mp_in[ce]  = cm;   s_mp_out[ce] = cm;
        end else if (ci) begin
          s_en_in[ce]  = cen;  s_mp_in[ce]  = cm;
        end else begin
          s_en_out[ce] = cen;  s_mp_out[ce] = cm;
        end
      end

      if (hw && (he < N_NUM) && (he != 4'd0)) begin
        // The design counts a SPLIT HALT only when a halt is being SET,
        // not when it is cleared. Omitting `hs` here made the shadow count
        // clears as well -- 3202 errors, none of them the design.
        if (hs) begin
          if (hi && !s_hl_out[he]) begin x_split = x_split + 1; g_split = g_split + 1; end
          if (!hi && !s_hl_in[he]) begin x_split = x_split + 1; g_split = g_split + 1; end
        end
        if (hi) s_hl_in[he]  = hs;
        else    s_hl_out[he] = hs;
      end

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

      cfg_wr = 1'b0; halt_wr = 1'b0; tok_valid = 1'b0; xact_ack = 1'b0;

      ck(n_lookup      === x_look,  "lookup count disagrees");
      ck(n_unknown_ep  === x_unk,   "unknown-endpoint count disagrees");
      ck(n_halted_hit  === x_halt,  "halted-hit count disagrees");
      ck(n_toggle_flip === x_flip,  "toggle-flip count disagrees");
      ck(n_split_halt  === x_split, "split-halt count disagrees");

      audit_all;
    end
  endtask

  task reset_dut;
    integer a;
    begin
      rst_n = 1'b0;
      cfg_wr = 0; halt_wr = 0; tok_valid = 0; xact_ack = 0;
      @(posedge clk); @(posedge clk);
      rst_n = 1'b1;
      for (a = 0; a < N_NUM; a = a + 1) begin
        s_en_in[a]  = (a == 0);  s_en_out[a] = (a == 0);
        s_hl_in[a]  = 1'b0;      s_hl_out[a] = 1'b0;
        s_mp_in[a]  = (a == 0) ? 11'd64 : 11'd0;
        s_mp_out[a] = (a == 0) ? 11'd64 : 11'd0;
        s_tg_in[a]  = 1'b0;      s_tg_out[a] = 1'b0;
      end
      x_look = 0; x_unk = 0; x_halt = 0; x_flip = 0; x_split = 0;
      @(posedge clk); #1;
    end
  endtask

  integer ei, di, en, hl, tg, k, a2;

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

    // =============================================================
    //  PHASE 1 (DIRECTED, EXHAUSTIVE) -- every (number, direction) in
    //  every (enabled, halted, toggle) state. 4 x 2 x 2 x 2 x 2 = 64.
    // =============================================================
    for (ei = 0; ei < N_NUM; ei = ei + 1)
    for (di = 0; di < 2; di = di + 1)
    for (en = 0; en < 2; en = en + 1)
    for (hl = 0; hl < 2; hl = hl + 1)
    for (tg = 0; tg < 2; tg = tg + 1) begin
      reset_dut;
      // configure this direction
      op(1'b1, ei[3:0], di[0], en[0], 11'd512,
         1'b0, 4'd0, 1'b0, 1'b0,  1'b0, 4'd0, 1'b0, 1'b0);
      // halt it if wanted
      if (hl) op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
                 1'b1, ei[3:0], di[0], 1'b1,  1'b0, 4'd0, 1'b0, 1'b0);
      // flip the toggle if wanted
      if (tg) op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
                 1'b0, 4'd0, 1'b0, 1'b0,  1'b1, ei[3:0], di[0], 1'b1);
      // and look it up
      op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
         1'b0, 4'd0, 1'b0, 1'b0,  1'b1, ei[3:0], di[0], 1'b0);

      ri = (ei << 4) | (di << 3) | (en << 2) | (hl << 1) | tg;
      reach[ri] = 1'b1;
    end

    // =============================================================
    //  PHASE 2 (DIRECTED, EXHAUSTIVE) -- THE CHAPTER'S POINT.
    //
    //  For every endpoint number except 0, enable BOTH directions, then
    //  halt exactly ONE of them, and prove the other is untouched --
    //  enable, halt, max packet size and toggle all independently.
    //
    //  Both orders, both directions: 3 numbers x 2 directions x 2 orders.
    // =============================================================
    for (ei = 1; ei < N_NUM; ei = ei + 1)
    for (di = 0; di < 2; di = di + 1)
    for (k = 0; k < 2; k = k + 1) begin
      reset_dut;
      // both directions exist, with DIFFERENT packet sizes so a shared
      // field cannot pass by accident
      op(1'b1, ei[3:0], 1'b1, 1'b1, 11'd64,
         1'b0, 4'd0, 1'b0, 1'b0,  1'b0, 4'd0, 1'b0, 1'b0);
      op(1'b1, ei[3:0], 1'b0, 1'b1, 11'd512,
         1'b0, 4'd0, 1'b0, 1'b0,  1'b0, 4'd0, 1'b0, 1'b0);

      if (k == 0) begin
        // advance one toggle, then halt one direction
        op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
           1'b0, 4'd0, 1'b0, 1'b0,  1'b1, ei[3:0], di[0], 1'b1);
        op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
           1'b1, ei[3:0], di[0], 1'b1,  1'b0, 4'd0, 1'b0, 1'b0);
      end else begin
        // halt one direction, then try to use the OTHER
        op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
           1'b1, ei[3:0], di[0], 1'b1,  1'b0, 4'd0, 1'b0, 1'b0);
        op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
           1'b0, 4'd0, 1'b0, 1'b0,  1'b1, ei[3:0], ~di[0], 1'b1);
      end

      // the untouched direction must still work
      peek(ei[3:0], ~di[0]);
      ck(ep_valid === 1'b1, "the other direction stopped existing");
      ck(ep_halted === 1'b0, "halting one direction halted the other");
      tok_valid = 1'b0;

      // and clearing the halt leaves the other alone too
      op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
         1'b1, ei[3:0], di[0], 1'b0,  1'b0, 4'd0, 1'b0, 1'b0);
    end

    // =============================================================
    //  PHASE 3 (DIRECTED) -- endpoint 0 is the exception.
    //
    //  It is bidirectional, it exists from reset, and it is not
    //  haltable: clearing a halt is itself a control transfer, so a
    //  device that could halt EP0 would be unrecoverable.
    // =============================================================
    reset_dut;
    for (di = 0; di < 2; di = di + 1) begin
      // it exists in both directions before anything configures it
      peek(4'd0, di[0]);
      ck(ep_valid === 1'b1, "EP0 did not exist at reset");
      ck(ep_maxp  === 11'd64, "EP0 had no default packet size");
      tok_valid = 1'b0;
      // and a halt attempt does nothing
      op(1'b0, 4'd0, 1'b0, 1'b0, 11'd0,
         1'b1, 4'd0, di[0], 1'b1,  1'b0, 4'd0, 1'b0, 1'b0);
      peek(4'd0, di[0]);
      ck(ep_halted === 1'b0, "EP0 was halted, which is unrecoverable");
      tok_valid = 1'b0;
    end
    // configuring EP0 configures both halves
    op(1'b1, 4'd0, 1'b1, 1'b1, 11'd8,
       1'b0, 4'd0, 1'b0, 1'b0,  1'b0, 4'd0, 1'b0, 1'b0);
    for (di = 0; di < 2; di = di + 1) begin
      peek(4'd0, di[0]);
      ck(ep_maxp === 11'd8, "configuring EP0 did not apply to both directions");
      tok_valid = 1'b0;
    end

    // =============================================================
    //  PHASE 4 (DIRECTED) -- an endpoint number that does not exist.
    // =============================================================
    reset_dut;
    for (k = N_NUM; k < 16; k = k + 1)
    for (di = 0; di < 2; di = di + 1) begin
      peek(k[3:0], di[0]);
      ck(ep_valid === 1'b0, "an out-of-range endpoint reported valid");
      ck(ep_maxp  === 11'd0, "an out-of-range endpoint reported a packet size");
      tok_valid = 1'b0;
      @(posedge clk); #1;
    end

    // =============================================================
    //  PHASE 5 (RANDOM) -- a driver configuring and halting freely.
    // =============================================================
`ifndef DIRECTED_ONLY
    reset_dut;
    for (k = 0; k < 8000; k = k + 1) begin
      op((urand(0) % 4) == 0, urand(0) % 16, (urand(0) % 2) == 0,
         (urand(0) % 4) != 0, (urand(0) % 2) ? 11'd64 : 11'd512,
         (urand(0) % 5) == 0, urand(0) % 16, (urand(0) % 2) == 0,
         (urand(0) % 3) != 0,
         (urand(0) % 2) == 0, urand(0) % 16, (urand(0) % 2) == 0,
         (urand(0) % 3) != 0);
      if ((k % 128) == 127) reset_dut;
    end
`endif

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

    $display("steps=%0d checks=%0d reach=%0d/64 errors=%0d",
             steps, checks, n_reach, errors);
    $display("[ep] lookups=%0d unknown=%0d halted_hits=%0d toggles=%0d split_halts=%0d",
             g_look, g_unk, g_halt, g_flip, g_split);
    $display("[the whole point] cross-direction leaks = %0d", n_cross);
    if (n_reach != 64) 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_ep_table (VHDL-2008).
--
--  The headline property is negative and about independence: touching one
--  direction of an endpoint number must never move the other. So the
--  shadow keeps the two directions in separate arrays and, after every
--  operation, compares BOTH directions of EVERY number -- not only the one
--  that was targeted. Checking only the targeted direction is how a leak
--  survives a testbench: the write landed correctly, and it also landed
--  somewhere nobody looked.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.ep_pkg.all;

entity tb_ep_vhdl is
  generic (DIRECTED_ONLY : boolean := false);
end entity;

architecture sim of tb_ep_vhdl is
  constant N_NUM : natural := 4;

  signal clk        : std_logic := '0';
  signal rst_n      : std_logic := '0';
  signal tok_valid  : std_logic := '0';
  signal tok_ep     : std_logic_vector(3 downto 0) := (others => '0');
  signal tok_in     : std_logic := '0';
  signal cfg_wr     : std_logic := '0';
  signal cfg_ep     : std_logic_vector(3 downto 0) := (others => '0');
  signal cfg_in     : std_logic := '0';
  signal cfg_enable : std_logic := '0';
  signal cfg_maxp   : std_logic_vector(10 downto 0) := (others => '0');
  signal halt_wr    : std_logic := '0';
  signal halt_ep    : std_logic_vector(3 downto 0) := (others => '0');
  signal halt_in    : std_logic := '0';
  signal halt_set   : std_logic := '0';
  signal xact_ack   : std_logic := '0';

  signal ep_valid, ep_halted, ep_toggle : std_logic;
  signal ep_maxp : std_logic_vector(10 downto 0);
  signal n_lookup, n_unknown_ep, n_halted_hit, n_toggle_flip, n_split_halt
    : std_logic_vector(31 downto 0);

  signal done : boolean := false;
begin

  dut : entity work.usb_ep_table
    generic map (N_NUM => N_NUM)
    port map (
      clk => clk, rst_n => rst_n,
      tok_valid => tok_valid, tok_ep => tok_ep, tok_in => tok_in,
      cfg_wr => cfg_wr, cfg_ep => cfg_ep, cfg_in => cfg_in,
      cfg_enable => cfg_enable, cfg_maxp => cfg_maxp,
      halt_wr => halt_wr, halt_ep => halt_ep, halt_in => halt_in,
      halt_set => halt_set,
      xact_ack => xact_ack,
      ep_valid => ep_valid, ep_halted => ep_halted,
      ep_maxp => ep_maxp, ep_toggle => ep_toggle,
      n_lookup => n_lookup, n_unknown_ep => n_unknown_ep,
      n_halted_hit => n_halted_hit, n_toggle_flip => n_toggle_flip,
      n_split_halt => n_split_halt);

  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;

    type sl_arr  is array (0 to N_NUM-1) of std_logic;
    type mp_arr  is array (0 to N_NUM-1) of unsigned(10 downto 0);
    variable s_en_in, s_en_out, s_hl_in, s_hl_out, s_tg_in, s_tg_out : sl_arr;
    variable s_mp_in, s_mp_out : mp_arr;

    variable x_look, x_unk, x_halt, x_flip, x_split : natural := 0;
    -- Run-wide totals: the DUT's counters are cleared by every reset.
    variable g_look, g_unk, g_halt, g_flip, g_split : natural := 0;

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

    variable rnd : unsigned(31 downto 0) := x"0003B7D1";
    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;

    -- boolean to std_logic. VHDL has no implicit conversion, and a
    -- conditional expression in argument position is VHDL-2019, not 2008.
    function sl_of(b : boolean) return std_logic is
    begin
      if b then return '1'; else return '0'; end if;
    end function;

    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;

    -- Compare EVERY direction of EVERY number. The expensive way, and the
    -- only way that can see a leak.
    procedure audit_all is
    begin
      for a in 0 to N_NUM-1 loop
        tok_valid <= '1';
        tok_ep    <= std_logic_vector(to_unsigned(a, 4));
        tok_in    <= '1';
        wait for 1 ns;
        if ep_valid  /= s_en_in(a) then n_cross := n_cross + 1; end if;
        if ep_halted /= s_hl_in(a) then n_cross := n_cross + 1; end if;
        if ep_toggle /= s_tg_in(a) then n_cross := n_cross + 1; end if;
        ck(ep_valid  = s_en_in(a), "IN  enable  disagrees");
        ck(ep_halted = s_hl_in(a), "IN  halt    disagrees");
        ck(ep_toggle = s_tg_in(a), "IN  toggle  disagrees");
        if s_en_in(a) = '1' then
          ck(ep_maxp = std_logic_vector(s_mp_in(a)), "IN  maxp disagrees");
        else
          ck(ep_maxp = std_logic_vector'("00000000000"), "IN  maxp disagrees");
        end if;

        tok_in <= '0';
        wait for 1 ns;
        if ep_valid  /= s_en_out(a) then n_cross := n_cross + 1; end if;
        if ep_halted /= s_hl_out(a) then n_cross := n_cross + 1; end if;
        if ep_toggle /= s_tg_out(a) then n_cross := n_cross + 1; end if;
        ck(ep_valid  = s_en_out(a), "OUT enable  disagrees");
        ck(ep_halted = s_hl_out(a), "OUT halt    disagrees");
        ck(ep_toggle = s_tg_out(a), "OUT toggle  disagrees");
        if s_en_out(a) = '1' then
          ck(ep_maxp = std_logic_vector(s_mp_out(a)), "OUT maxp disagrees");
        else
          ck(ep_maxp = std_logic_vector'("00000000000"), "OUT maxp disagrees");
        end if;
      end loop;
      tok_valid <= '0';
      ck(n_cross = 0, "a direction changed that nothing touched");
    end procedure;

    procedure op(cw : std_logic; ce : std_logic_vector(3 downto 0);
                 ci, cen : std_logic; cm : std_logic_vector(10 downto 0);
                 hw : std_logic; he : std_logic_vector(3 downto 0);
                 hi, hs : std_logic;
                 tv : std_logic; te : std_logic_vector(3 downto 0);
                 ti, xa : std_logic) is
      variable cn, hn, tn : natural;
    begin
      cfg_wr <= cw;  cfg_ep <= ce;  cfg_in <= ci;
      cfg_enable <= cen;  cfg_maxp <= cm;
      halt_wr <= hw; halt_ep <= he; halt_in <= hi; halt_set <= hs;
      tok_valid <= tv; tok_ep <= te; tok_in <= ti; xact_ack <= xa;

      cn := to_integer(unsigned(ce));
      hn := to_integer(unsigned(he));
      tn := to_integer(unsigned(te));

      -- ---- advance the shadow ----
      --
      -- The LOOKUP is evaluated first, because the design's lookup is a
      -- combinational read of REGISTERED table entries: it sees the values
      -- as they stood before this edge. A shadow that applies the
      -- configuration write first disagrees on every cycle that configures
      -- and looks up at once.
      if tv = '1' and tn < N_NUM then
        x_look := x_look + 1;  g_look := g_look + 1;
        if ti = '1' then
          if s_en_in(tn) = '0' then x_unk := x_unk + 1; g_unk := g_unk + 1; end if;
          if s_hl_in(tn) = '1' then x_halt := x_halt + 1; g_halt := g_halt + 1; end if;
          if s_en_in(tn) = '1' and s_hl_in(tn) = '0' and xa = '1' then
            s_tg_in(tn) := not s_tg_in(tn);
            x_flip := x_flip + 1;  g_flip := g_flip + 1;
          end if;
        else
          if s_en_out(tn) = '0' then x_unk := x_unk + 1; g_unk := g_unk + 1; end if;
          if s_hl_out(tn) = '1' then x_halt := x_halt + 1; g_halt := g_halt + 1; end if;
          if s_en_out(tn) = '1' and s_hl_out(tn) = '0' and xa = '1' then
            s_tg_out(tn) := not s_tg_out(tn);
            x_flip := x_flip + 1;  g_flip := g_flip + 1;
          end if;
        end if;
      elsif tv = '1' then
        x_look := x_look + 1;  g_look := g_look + 1;
        x_unk  := x_unk + 1;   g_unk  := g_unk + 1;
      end if;

      if cw = '1' and cn < N_NUM then
        if cn = 0 then
          s_en_in(cn)  := cen;  s_en_out(cn) := cen;
          s_mp_in(cn)  := unsigned(cm);  s_mp_out(cn) := unsigned(cm);
        elsif ci = '1' then
          s_en_in(cn)  := cen;  s_mp_in(cn)  := unsigned(cm);
        else
          s_en_out(cn) := cen;  s_mp_out(cn) := unsigned(cm);
        end if;
      end if;

      if hw = '1' and hn < N_NUM and hn /= 0 then
        -- A split halt is counted only when a halt is being SET.
        if hs = '1' then
          if hi = '1' and s_hl_out(hn) = '0' then
            x_split := x_split + 1;  g_split := g_split + 1;
          end if;
          if hi = '0' and s_hl_in(hn) = '0' then
            x_split := x_split + 1;  g_split := g_split + 1;
          end if;
        end if;
        if hi = '1' then s_hl_in(hn)  := hs;
        else             s_hl_out(hn) := hs;
        end if;
      end if;

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

      cfg_wr <= '0'; halt_wr <= '0'; tok_valid <= '0'; xact_ack <= '0';

      ck(to_integer(unsigned(n_lookup))      = x_look,  "lookup count disagrees");
      ck(to_integer(unsigned(n_unknown_ep))  = x_unk,   "unknown-endpoint count disagrees");
      ck(to_integer(unsigned(n_halted_hit))  = x_halt,  "halted-hit count disagrees");
      ck(to_integer(unsigned(n_toggle_flip)) = x_flip,  "toggle-flip count disagrees");
      ck(to_integer(unsigned(n_split_halt))  = x_split, "split-halt count disagrees");

      audit_all;
    end procedure;

    -- A combinational lookup with no clocked side effect, positioned at a
    -- negedge deliberately: driving tok_valid then waiting 1 ns lands on the
    -- next rising edge once the preceding audit has consumed its 8 ns, and
    -- the DUT then counts a lookup the shadow knows nothing about.
    procedure peek(e : std_logic_vector(3 downto 0); d : std_logic) is
    begin
      wait until falling_edge(clk);
      tok_valid <= '1'; tok_ep <= e; tok_in <= d;
      wait for 1 ns;
    end procedure;

    procedure reset_dut is
    begin
      rst_n <= '0';
      cfg_wr <= '0'; halt_wr <= '0'; tok_valid <= '0'; xact_ack <= '0';
      wait until rising_edge(clk);
      wait until rising_edge(clk);
      rst_n <= '1';
      for a in 0 to N_NUM-1 loop
        if a = 0 then
          s_en_in(a) := '1';  s_en_out(a) := '1';
          s_mp_in(a) := to_unsigned(64, 11);  s_mp_out(a) := to_unsigned(64, 11);
        else
          s_en_in(a) := '0';  s_en_out(a) := '0';
          s_mp_in(a) := (others => '0');  s_mp_out(a) := (others => '0');
        end if;
        s_hl_in(a) := '0';  s_hl_out(a) := '0';
        s_tg_in(a) := '0';  s_tg_out(a) := '0';
      end loop;
      x_look := 0; x_unk := 0; x_halt := 0; x_flip := 0; x_split := 0;
      wait until rising_edge(clk);
      wait for 1 ns;
    end procedure;

    constant Z4  : std_logic_vector(3 downto 0)  := "0000";
    constant Z11 : std_logic_vector(10 downto 0) := (others => '0');
    variable ri  : natural;
    variable ev, dv2 : std_logic_vector(3 downto 0);
    variable db  : std_logic;
    variable m11 : std_logic_vector(10 downto 0);
  begin
    -- PHASE 1 (DIRECTED, EXHAUSTIVE) -- 4 numbers x 2 dirs x enabled x halted x toggle
    for ei in 0 to N_NUM-1 loop
      for di in 0 to 1 loop
        for en in 0 to 1 loop
          for hl in 0 to 1 loop
            for tg in 0 to 1 loop
              reset_dut;
              ev := std_logic_vector(to_unsigned(ei, 4));
              if di = 1 then db := '1'; else db := '0'; end if;
              if en = 1 then
                op('1', ev, db, '1', std_logic_vector(to_unsigned(512, 11)),
                   '0', Z4, '0', '0', '0', Z4, '0', '0');
              else
                op('1', ev, db, '0', std_logic_vector(to_unsigned(512, 11)),
                   '0', Z4, '0', '0', '0', Z4, '0', '0');
              end if;
              if hl = 1 then
                op('0', Z4, '0', '0', Z11, '1', ev, db, '1', '0', Z4, '0', '0');
              end if;
              if tg = 1 then
                op('0', Z4, '0', '0', Z11, '0', Z4, '0', '0', '1', ev, db, '1');
              end if;
              op('0', Z4, '0', '0', Z11, '0', Z4, '0', '0', '1', ev, db, '0');

              ri := ei*16 + di*8 + en*4 + hl*2 + tg;
              reach(ri) := '1';
            end loop;
          end loop;
        end loop;
      end loop;
    end loop;

    -- PHASE 2 (DIRECTED, EXHAUSTIVE) -- THE CHAPTER'S POINT.
    -- Enable BOTH directions with DIFFERENT packet sizes, halt exactly one,
    -- and prove the other is untouched in every field.
    for ei in 1 to N_NUM-1 loop
      for di in 0 to 1 loop
        for k in 0 to 1 loop
          reset_dut;
          ev := std_logic_vector(to_unsigned(ei, 4));
          if di = 1 then db := '1'; else db := '0'; end if;
          op('1', ev, '1', '1', std_logic_vector(to_unsigned(64, 11)),
             '0', Z4, '0', '0', '0', Z4, '0', '0');
          op('1', ev, '0', '1', std_logic_vector(to_unsigned(512, 11)),
             '0', Z4, '0', '0', '0', Z4, '0', '0');

          if k = 0 then
            op('0', Z4, '0', '0', Z11, '0', Z4, '0', '0', '1', ev, db, '1');
            op('0', Z4, '0', '0', Z11, '1', ev, db, '1', '0', Z4, '0', '0');
          else
            op('0', Z4, '0', '0', Z11, '1', ev, db, '1', '0', Z4, '0', '0');
            op('0', Z4, '0', '0', Z11, '0', Z4, '0', '0', '1', ev, not db, '1');
          end if;

          peek(ev, not db);
          ck(ep_valid  = '1', "the other direction stopped existing");
          ck(ep_halted = '0', "halting one direction halted the other");
          tok_valid <= '0';

          op('0', Z4, '0', '0', Z11, '1', ev, db, '0', '0', Z4, '0', '0');
        end loop;
      end loop;
    end loop;

    -- PHASE 3 (DIRECTED) -- endpoint 0 is the exception
    reset_dut;
    for di in 0 to 1 loop
      if di = 1 then db := '1'; else db := '0'; end if;
      peek(Z4, db);
      ck(ep_valid = '1', "EP0 did not exist at reset");
      ck(ep_maxp = std_logic_vector(to_unsigned(64, 11)),
         "EP0 had no default packet size");
      tok_valid <= '0';
      op('0', Z4, '0', '0', Z11, '1', Z4, db, '1', '0', Z4, '0', '0');
      peek(Z4, db);
      ck(ep_halted = '0', "EP0 was halted, which is unrecoverable");
      tok_valid <= '0';
    end loop;
    op('1', Z4, '1', '1', std_logic_vector(to_unsigned(8, 11)),
       '0', Z4, '0', '0', '0', Z4, '0', '0');
    for di in 0 to 1 loop
      if di = 1 then db := '1'; else db := '0'; end if;
      peek(Z4, db);
      ck(ep_maxp = std_logic_vector(to_unsigned(8, 11)),
         "configuring EP0 did not apply to both directions");
      tok_valid <= '0';
    end loop;

    -- PHASE 4 (DIRECTED) -- an endpoint number that does not exist
    reset_dut;
    for k in N_NUM to 15 loop
      for di in 0 to 1 loop
        if di = 1 then db := '1'; else db := '0'; end if;
        peek(std_logic_vector(to_unsigned(k, 4)), db);
        ck(ep_valid = '0', "an out-of-range endpoint reported valid");
        ck(ep_maxp = std_logic_vector'("00000000000"),
           "an out-of-range endpoint reported a packet size");
        tok_valid <= '0';
        wait until rising_edge(clk);
        wait for 1 ns;
      end loop;
    end loop;

    -- PHASE 5 (RANDOM)
    if not DIRECTED_ONLY then
      reset_dut;
      for k in 0 to 7999 loop
        if (nxt mod 2) = 1 then m11 := std_logic_vector(to_unsigned(64, 11));
        else                    m11 := std_logic_vector(to_unsigned(512, 11));
        end if;
        op(sl_of(nxt mod 4 = 0), std_logic_vector(to_unsigned(nxt mod 16, 4)),
           sl_of(nxt mod 2 = 0), sl_of(nxt mod 4 /= 0), m11,
           sl_of(nxt mod 5 = 0), std_logic_vector(to_unsigned(nxt mod 16, 4)),
           sl_of(nxt mod 2 = 0), sl_of(nxt mod 3 /= 0),
           sl_of(nxt mod 2 = 0), std_logic_vector(to_unsigned(nxt mod 16, 4)),
           sl_of(nxt mod 2 = 0), sl_of(nxt mod 3 /= 0));
        if (k mod 128) = 127 then reset_dut; end if;
      end loop;
    end if;

    n_reach := 0;
    for i in 0 to 63 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'("/64")
          & string'(" errors=") & integer'image(errors));
    writeline(output, ln);
    write(ln, string'("[ep] lookups=") & integer'image(g_look)
          & string'(" unknown=") & integer'image(g_unk)
          & string'(" halted_hits=") & integer'image(g_halt)
          & string'(" toggles=") & integer'image(g_flip)
          & string'(" split_halts=") & integer'image(g_split));
    writeline(output, ln);
    write(ln, string'("[the whole point] cross-direction leaks = ")
          & integer'image(n_cross));
    writeline(output, ln);
    if n_reach /= 64 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
(number × direction × enabled × halted × toggle) reached64 / 6464 / 6464 / 64
independence scenarios swept12 / 1212 / 1212 / 12
out-of-range numbers swept24 / 2424 / 2424 / 24
Steps825582558255
Checks executed313770313770313770
lookups401940194033
lookups of an unconfigured endpoint351235123565
lookups that hit a halted endpoint212212174
toggle advances268268263
halts applied to one direction only197197178
cross-direction leaks000
ResultPASSPASSPASS

11. Mutation Testing

#MutationVerilogSysVerVHDL
D5any in-range endpoint reports valid, configured or not777667776683556
D1a halt applies to BOTH directions of the number213602136021025
D3the two directions share one data toggle144731447314568
D4endpoint 0 becomes haltable127921279213807
D6a halted endpoint's toggle still advances10135101359786
D7a disabled endpoint still reports a packet size606360637541
D2the packet size is read from the IN table whatever the direction505150515219
—unmutated baseline000

All seven die in all three languages.

D1 is the mutation this chapter exists for, and it is worth noticing what it does not break. Every lookup still returns a defensible value. Every write lands where it was aimed. The device still enumerates, still transfers, still passes a functional test — and a driver that halts a failed read direction has silently killed its writes.

Directed against random

#V allV directedV randomVHDL allVHDL directedVHDL random
D121360327210332102532720698
D250518449675219845135
D314473290141831456829014278
D412792296124961380729613511
D57776618617590583556186181695
D610135209992697862099577
D76063120594375411207421

Every directed column is identical across Verilog and VHDL, which localises the whole spread to the random half.

12. D4's Directed Columns Read 296 and 319, and That Was the Mutant Again

Every other directed column matched to the digit on the first run. D4's did not, and a 7% disagreement in a column that is supposed to be identical stimulus against identical logic is a much louder signal than a 30% disagreement in the random half.

The cause was structural rather than semantic. In the Verilog the split-halt coverage counter is its own if statement with its own endpoint-0 exclusion. In the VHDL it had been folded inside the halt-write block:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   Verilog:                      VHDL (before):

     if (halt && ep != 0)          if halt and ep /= 0 then
       write halt                    write halt
                                     count split halt
     if (halt && ep != 0 && set)   end if
       count split halt

   D4 removes "ep != 0" from the halt-write condition.

   In Verilog that changes ONE block.
   In VHDL it changed TWO, because the counter was inside.

So the VHDL mutation was strictly larger than the Verilog one — it also began counting split halts for endpoint 0 — and the two columns were measuring different experiments. Splitting the VHDL counter into its own block, statement for statement identical to the Verilog, brought D4's directed score to 296 in both.

13. Follow-Ups the Interviewer Will Ask

"How many endpoints does a device with endpoints 0, 1 and 2 have?" Between three and five. EP0 is one bidirectional endpoint; 1 and 2 are each up to two. The descriptors decide, and the question is deliberately ambiguous.

"What is the data toggle for?" Detecting a lost acknowledgement. If the device's ACK is lost, the host retries; the device sees a packet with the toggle it already accepted and discards it as a duplicate rather than writing the same bytes twice. It is per-endpoint-per-direction, which is why D3 is a data-corruption bug.

"What resets the toggle?" A bus reset, a SET_CONFIGURATION, and CLEAR_FEATURE(ENDPOINT_HALT) on that endpoint. A device that clears a halt without resetting the toggle is out of sync with the host by exactly one packet, forever.

"Can endpoint 1 IN be bulk while endpoint 1 OUT is interrupt?" Yes. They are separate endpoints with separate descriptors. It is unusual and entirely legal.

"Why can't endpoint 0 be halted?" Because clearing a halt is a control transfer over endpoint 0. A halted EP0 cannot be told to un-halt itself.

"What does a device do with a token for an endpoint it does not have?" Nothing — it does not respond at all, which the host sees as a timeout. That is different from STALL, which is an active refusal from an endpoint that exists.

"How big can a packet be?" Depends on transfer type and speed: 8–64 for full-speed control and bulk, up to 512 for high-speed bulk, up to 1024 for high-speed interrupt and isochronous. The field here is 11 bits for that reason.

14. UVM: A Register Model Indexed by Two Things

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The bug this component exists to catch is a WRITE THAT LANDS TWICE. The
// targeted location is always correct, so a scoreboard that checks the
// location it just wrote will never see it.
//
// The technique: model every endpoint independently, and after every
// operation audit EVERY entry -- then report how many entries were
// touched. A correct write touches exactly one.
typedef enum bit { DIR_OUT = 1'b0, DIR_IN = 1'b1 } ep_dir_e;

class ep_op extends uvm_sequence_item;
  `uvm_object_utils(ep_op)

  typedef enum { OP_LOOKUP, OP_CONFIG, OP_HALT } kind_e;

  rand kind_e   kind;
  rand bit [3:0] num;
  rand ep_dir_e  dir;
  rand bit       enable;
  rand bit [10:0] maxp;
  rand bit       halt_set;
  rand bit       xact_ack;

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

  // The two directions must get DIFFERENT packet sizes, or a design that
  // reads the wrong one cannot be distinguished from a correct one. This is
  // the single most important constraint in the file.
  constraint c_asymmetric_maxp {
    (dir == DIR_IN)  -> maxp inside {8, 64};
    (dir == DIR_OUT) -> maxp inside {512, 1024};
  }

  // Out-of-range numbers on purpose: an endpoint a device does not have is
  // a real thing a host asks for, and the answer is silence rather than STALL.
  constraint c_some_out_of_range { num dist { [0:3] := 85, [4:15] := 15 }; }
endclass


class ep_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(ep_scoreboard)

  uvm_analysis_imp #(ep_op, ep_scoreboard) ap;

  localparam int N = 4;

  // [number][direction] -- and the model has no field that is indexed by
  // number alone, deliberately, so it cannot mirror the bug it is looking for.
  bit        m_en     [N][2];
  bit        m_halted [N][2];
  bit [10:0] m_maxp   [N][2];
  bit        m_toggle [N][2];

  int unsigned n_split_halt;      // a halt on one direction, other unhalted
  int unsigned n_asym_maxp;       // the two directions held different sizes
  int unsigned n_leak;            // entries changed that nothing addressed

  function new(string name, uvm_component parent);
    super.new(name, parent);
    ap = new("ap", this);
    foreach (m_en[i, j]) begin
      m_en[i][j]     = (i == 0);
      m_halted[i][j] = 1'b0;
      m_maxp[i][j]   = (i == 0) ? 11'd64 : 11'd0;
      m_toggle[i][j] = 1'b0;
    end
  endfunction

  function void write(ep_op t);
    int d = int'(t.dir);

    if (t.num >= N) return;   // a device does not answer for what it lacks

    case (t.kind)
      OP_CONFIG: begin
        if (t.num == 0) begin
          // EP0 is one conversation, so both halves move together. This is
          // the ONLY place in this model where two entries change at once,
          // and it is deliberate rather than a leak.
          for (int j = 0; j < 2; j++) begin
            m_en[0][j]   = t.enable;
            m_maxp[0][j] = t.maxp;
          end
        end else begin
          m_en[t.num][d]   = t.enable;
          m_maxp[t.num][d] = t.maxp;
        end
        if (m_maxp[t.num][0] != m_maxp[t.num][1]) n_asym_maxp++;
      end

      OP_HALT: begin
        // EP0 is not haltable: clearing a halt is a control transfer over
        // endpoint 0, so a halted EP0 could never be un-halted.
        if (t.num == 0) return;
        if (t.halt_set && !m_halted[t.num][1-d]) n_split_halt++;
        m_halted[t.num][d] = t.halt_set;
      end

      OP_LOOKUP: begin
        if (m_en[t.num][d] && !m_halted[t.num][d] && t.xact_ack)
          m_toggle[t.num][d] = ~m_toggle[t.num][d];
      end
    endcase
  endfunction

  // Called by the monitor after every operation, with the DUT's whole table.
  //
  // Auditing everything is the only check that can see a write landing
  // twice, because the location that was written is always right.
  function void audit(bit dut_en [N][2], bit dut_hl [N][2],
                     bit [10:0] dut_mp [N][2], bit dut_tg [N][2]);
    foreach (m_en[i, j]) begin
      if (dut_en[i][j] !== m_en[i][j]) begin
        n_leak++;
        `uvm_error("EP/LEAK",
          $sformatf("endpoint %0d %s enable is %0b, model says %0b",
                    i, j ? "IN" : "OUT", dut_en[i][j], m_en[i][j]))
      end
      if (dut_hl[i][j] !== m_halted[i][j]) begin
        n_leak++;
        `uvm_error("EP/LEAK",
          $sformatf("endpoint %0d %s halt is %0b, model says %0b -- a halt reached a direction nobody addressed",
                    i, j ? "IN" : "OUT", dut_hl[i][j], m_halted[i][j]))
      end
      if (dut_mp[i][j] !== m_maxp[i][j]) begin
        n_leak++;
        `uvm_error("EP/LEAK",
          $sformatf("endpoint %0d %s maxp is %0d, model says %0d",
                    i, j ? "IN" : "OUT", dut_mp[i][j], m_maxp[i][j]))
      end
      if (dut_tg[i][j] !== m_toggle[i][j]) begin
        n_leak++;
        `uvm_error("EP/LEAK",
          $sformatf("endpoint %0d %s toggle is %0b, model says %0b -- the two directions are sharing one",
                    i, j ? "IN" : "OUT", dut_tg[i][j], m_toggle[i][j]))
      end
    end
  endfunction

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

    `uvm_info("EP",
      $sformatf("%0d split halts | %0d asymmetric-size cycles | %0d leaks",
                n_split_halt, n_asym_maxp, n_leak), UVM_LOW)

    // Without these two, zero leaks is a statement about the stimulus.
    if (n_split_halt == 0)
      `uvm_error("EP/COV",
        "every halt in this run moved both directions together: independence was never tested")
    if (n_asym_maxp == 0)
      `uvm_error("EP/COV",
        "the two directions never held different packet sizes: a design reading the wrong one would have passed")
  endfunction
endclass

15. Common Misconceptions

"Endpoint 1 is one endpoint." It is up to two: endpoint 1 IN and endpoint 1 OUT, with independent everything.

"The endpoint number identifies the endpoint." The pair (number, direction) does. The number alone is ambiguous.

"A device with four endpoints has four endpoints." It has between four and eight. The phrase is ambiguous every time it is used.

"Halting an endpoint halts it." It halts one direction. The other keeps working, and that is correct.

"The data toggle is per endpoint number." Per number and direction. Sharing one corrupts data in both directions and reports nothing.

"Endpoint 0 is just endpoint 0." It is the one genuinely bidirectional endpoint, it exists before any descriptor is read, and it cannot be halted.

"Both directions have the same max packet size." Usually, and not necessarily — which is why a design that reads the wrong one passes most tests.

"An unknown endpoint gets a STALL." It gets silence. STALL is an active refusal from an endpoint that exists.

"A halted endpoint's toggle is frozen, so it does not matter." It must be frozen. D6 advances it and scores 10,135.

16. Exercises

1. A device has EP0, EP1 IN (bulk, 512), EP1 OUT (interrupt, 64) and EP2 IN (iso, 1024). How many endpoints is that, and what is the smallest table that can hold their state?

2. D2 reads the packet size from the wrong direction and scores lowest of the seven. Explain why, and write the one constraint that makes it the highest.

3. Construct the failure sequence for D3 (a shared data toggle): give the exact traffic pattern that causes a packet to be silently discarded as a duplicate.

4. Endpoint 0 cannot be halted. Work out what a host would have to do to recover a device that halted it anyway, and say why that is unacceptable.

5. The bench audits every direction of every number after every operation, which is O(2N) work per step. Propose a cheaper check that still catches a cross-direction leak, and say what it gives up.

6. D4's directed columns read 296 and 319 because one language's mutation covered two statements. Design a procedure that would have caught this before the scores were published.

7. Add CLEAR_FEATURE(ENDPOINT_HALT) with its toggle reset. Which of the seven properties change, and what new one is needed?

17. Summary

IdeaWhy it matters
(number, direction) is the identifierthe number alone is ambiguous
EP1 IN and EP1 OUT are two endpointsseparate buffer, toggle, halt, size, type
Halting one direction leaves the other runningand that is the correct behaviour
The data toggle is per directionsharing it corrupts data silently
The packet sizes are independentusually equal, which is why D2 hides
EP0 is bidirectionalone conversation, not two pipes
EP0 cannot be haltedclearing a halt needs EP0
An unknown endpoint gets silence, not STALLSTALL is a refusal from something that exists
A halted endpoint's toggle is frozenor it desynchronises on recovery
Eight parallel arrays is a bug generatorindex by [num][dir] where the language allows
VHDL crashes on the out-of-range indexso the bound check must be local to its use
The design cannot self-check a leakonly an independent shadow can see it
Audit every entry after every writethe targeted one is always correct
A disagreeing directed column is the mutantfourth time this module
64 states, 7 mutations, 3 languages0 cross-direction leaks in 313,770 checks

Tooling

StepCommand
Verilog-2005iverilog -g2005 -o ep_v.out ep_v.v ep_v_tb.v && ./ep_v.out
SystemVerilogiverilog -g2012 -o ep_sv.out ep_sv.sv ep_sv_tb.sv && ./ep_sv.out
VHDL-2008 analysenvc --std=2008 -a ep_vhdl.vhd ep_vhdl_tb.vhd
VHDL-2008 elaboratenvc --std=2008 -e tb_ep_vhdl
VHDL-2008 runnvc --std=2008 -r tb_ep_vhdl
One mutationiverilog -g2005 -DMUT_D1 -o mm ep_v_mut.v ep_v_tb.v && ./mm
Directed only (Verilog)iverilog -g2005 -DDIRECTED_ONLY -o mm ep_v_mut.v ep_v_tb.v && ./mm
Directed only (VHDL)nvc --std=2008 -e -gDIRECTED_ONLY=true tb_ep_vhdl

All three implementations pass with 0 errors: all 64 combinations of number, direction, enable, halt and toggle; 197 halts applied to a single direction with both directions audited after each; asymmetric packet sizes throughout; zero cross-direction leaks in 313,770 checks; and every one of the seven mutations killed by directed stimulus alone, with all seven directed scores identical across languages.


Chapter 27.5 — The Transfer-Types Question moves from what an endpoint is to what it is for. Its central trade is the one candidates state backwards: isochronous transfers get guaranteed bandwidth and no retries, and those are the same property — a retry would need a slot the schedule has already given away.

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.