Skip to content
VLSI Mentor

USB · Module 31

“Endpoints Are Physical Ports”

Ordinary English makes an endpoint sound like a place, and every neighbouring bus has ports that are sockets. The prediction is that endpoint 2 is one thing. A four-line decoder shows that an endpoint is a number, a direction, and a declaration.

1. The Belief

"An endpoint is a place on the device — a port, a channel, a socket. A device with four endpoints has four of something, and you find them by looking at the hardware."

2. Why An Intelligent Engineer Believes It

This one is almost forced by the vocabulary, and it is reinforced by every neighbouring technology.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    THE WORD
    "Endpoint" in ordinary English is a PLACE -- the end of a line, a
    terminus. "Port" is its synonym everywhere else in engineering.

    THE NEIGHBOURS
      TCP/IP        a port is a number, but it identifies a SOCKET that
                    software opens and closes -- a place data goes
      an SoC bus    a port is pins. Physically countable.
      a switch      a port is a hole you plug a cable into
      AXI           a port is a bundle of wires with a name

    THE DESCRIPTORS
    A device's descriptors LIST its endpoints, one entry each, with an
    address and attributes -- which reads exactly like an inventory of
    physical resources.

    THE DOCUMENTATION
    Every USB device datasheet says things like "4 endpoints, 64 bytes
    each", in the same table and the same tone as "2 UARTs, 1 SPI".

3. The Prediction It Makes

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    IF AN ENDPOINT WERE A PHYSICAL PORT, THEN:

    1  "endpoint 2" would name ONE thing, and IN and OUT would be
       directions of travel through it -- not part of its name

    2  a device's endpoint count would be a property of the silicon,
       fixed and discoverable by inspection

    3  the same endpoint number could not simultaneously be two
       differently-behaved things

    4  an endpoint would exist as soon as the hardware exists -- before
       enumeration, before configuration, always

    5  a descriptor would only be DESCRIBING a resource that already
       exists independently of it

4. The Counterexample

Two of them, because they falsify different predictions.

4a. Endpoint 2 is two endpoints

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Read the endpoint descriptors of a device that has both:

      bEndpointAddress = 0x02     ->  endpoint 2, OUT
      bEndpointAddress = 0x82     ->  endpoint 2, IN

    TWO descriptors. TWO entries. And they may differ in:

      transfer type      one bulk, one interrupt
      max packet size    64 vs 8
      interval           meaningless vs 10 ms
      halted state       one STALLed, the other running fine

Two endpoints, both "number 2", with different types, different sizes, independent halt state and independent data toggles. Prediction 1 fails, and prediction 3 fails with it. "Endpoint 2" is not an address. It is half of one.

4b. The count changes without the silicon changing

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    One device. Two of its interface alternate settings:

      alt 0      0 endpoints (the bandwidth-free idle setting)
      alt 1      1 isochronous IN endpoint, 192 bytes/frame
      alt 2      1 isochronous IN endpoint, 384 bytes/frame

    A SET_INTERFACE request changes which of these is in force, and
    therefore changes how many endpoints the device HAS -- with no
    change to the silicon whatsoever.

This is the standard structure of every USB audio and video device, and it exists precisely so the host can grant bandwidth it has and refuse bandwidth it does not. Prediction 2 fails, and prediction 4 fails: before configuration, a device has exactly one endpoint — endpoint 0 — no matter what its silicon contains.

5. The Corrected Model

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    AN ENDPOINT IS IDENTIFIED BY A PAIR

        ( endpoint number , direction )

    and both halves are part of the name. 0x82 is not "endpoint 130";
    it is the top bit meaning IN, and 2 meaning number 2.

    SO:
      the address space is       16 numbers x 2 directions = 32 names
      endpoint 0 is special      it exists in BOTH directions, always,
                                 and is the only one that does
      every other endpoint       exists because a descriptor in the
                                 ACTIVE CONFIGURATION says it does

    AND THE THINGS THAT ARE PER-ENDPOINT ARE PER-PAIR:
      the data toggle            EP2 IN and EP2 OUT have separate ones
      the halt condition         independent
      the max packet size        independent
      the transfer type          independent
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    THREE LEVELS THAT THE BELIEF COLLAPSES INTO ONE

      the NAME            (number, direction) -- an address
      the DECLARATION     a descriptor in the active configuration
      the IMPLEMENTATION  a buffer and some control bits

    The belief says these are the same thing. The protocol keeps them
    separate, and every one of the three can exist without the others:

      a name with no declaration     -> the host will not address it;
                                        a request to it is an error
      a declaration with no hardware -> a device that NAKs forever, or
                                        worse, a device that lies
      hardware with no declaration   -> a buffer nobody can reach. Very
                                        common, and the engineer stares
                                        at the buffer.

6. The Hardware Contract

The point of this specimen is that both halves of the name are physically part of the decode. If you drop either one, the design still compiles and still looks sensible.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    PURPOSE     given an endpoint number and a direction arriving on ONE
                shared token path, select the one logical endpoint they
                name -- or refuse, if the configuration does not declare
                it

    PARAMETERS  NEP  = 4      endpoint numbers 0..3
                NIDX = 8      NEP x 2 directions

    INPUTS      clk, rst_n
                cfg_en [7:0]         which of the 8 (direction, number)
                                     pairs the configuration declares.
                                     Bit index is {dir, number}.
                tok_valid            a token arrived
                tok_ep [3:0]         the number it carries
                tok_is_in            the direction it carries
                data_in [7:0]
                data_we              an OUT payload accompanies it

    OUTPUTS     sel_valid            a declared endpoint was selected
                sel_index [2:0]      which one: {direction, number}
                sel_stall            addressed something not there
                buf_out [7:0]        the selected endpoint's byte
                n_sel, n_stall
                n_multi              two endpoints selected at once

    AUTHORITATIVE STATE
                ep_buf[0:7]          ONE BYTE PER LOGICAL ENDPOINT.
                                     Eight of them. One token path.

    DERIVED     THE IDENTITY, and it is one line:
                  idx    = {tok_is_in, tok_ep[1:0]}
                  num_ok = (tok_ep < NEP)
                  exists = num_ok && cfg_en[idx]
                  sel_valid = tok_valid &&  exists
                  sel_stall = tok_valid && !exists

    RESET       every buffer and counter cleared. cfg_en is an input,
                so what exists is not this module's state at all.

    WRITE RULE  an OUT payload lands in the buffer the token selected
                and in NO OTHER. The IN buffer of the SAME NUMBER is
                untouched -- which is the whole claim, as a write.

    LATENCY     the decode is combinational in the token; the buffers
                are registered.

    BOUNDARY    number 2 IN and number 2 OUT land on DIFFERENT indices
                (6 and 2) with different storage. A number at or above
                NEP is out of range and stalls. A declared-but-disabled
                endpoint stalls too, and the two reasons are counted
                separately.

    ASSUMPTIONS four numbers and two directions, so an eight-bit
                cfg_en. The real space is 16 x 2. One byte of storage
                stands in for what a real part implements as a RAM.

    OMISSIONS   the PHY, the serial interface engine, packet framing,
                descriptor parsing, alternate settings as a MECHANISM
                (cfg_en simply arrives), halt state, data toggles,
                max packet sizes, and endpoint 0's special status --
                which is 31.5's subject.

    MISCONCEPTION DEMONSTRATED
                "an endpoint is a number" and
                "an endpoint exists because the silicon has one"

usb_ep_decode.v — the design, Verilog-2005

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  usb_ep_decode -- what an endpoint actually IS, in hardware.
//
//  CLASSIFICATION: simplified synthesisable teaching RTL, built for one
//  purpose: to show that an endpoint's identity is a NUMBER AND A
//  DIRECTION, resolved from a token arriving on ONE shared physical
//  connection -- not a connector, not a pin, not a wire.
//
//  It is NOT a USB device controller. There is no PHY, no serial
//  interface engine, no packet framing, no data toggle, no transfer
//  layer. One byte of storage per logical endpoint stands in for what a
//  real part would implement as a RAM.
//
//  THE THING TO NOTICE IN THE PORT LIST
//  ------------------------------------
//  There is ONE tok_* group and ONE data path. Eight logical endpoints
//  are served by it. If endpoints were physical, this module would need
//  eight of something, and it needs one.
//
//  IDENTITY
//      index = {direction, number}
//  Endpoint 1 IN and endpoint 1 OUT are DIFFERENT ENDPOINTS with
//  different storage, different enables and different behaviour. They
//  share a number and nothing else. Mutation D-M1 drops the direction
//  from the index -- which is the misconception, written as RTL.
// =====================================================================
module usb_ep_decode #(
  // Endpoint numbers 0 .. NEP-1, each with an IN and an OUT form.
  parameter integer NEP  = 4,
  parameter integer NIDX = 8      // NEP * 2
) (
  input  wire       clk,
  input  wire       rst_n,

  // ---- firmware configuration: which logical endpoints exist ----
  // Bit {dir, number}. An endpoint that the descriptors do not declare
  // is not there, and addressing it is a request error.
  input  wire [NIDX-1:0] cfg_en,

  // ---- ONE token path, ONE data path ----
  input  wire       tok_valid,
  input  wire [3:0] tok_ep,        // the number carried by the token
  input  wire       tok_is_in,     // the direction carried by the token
  input  wire [7:0] data_in,
  input  wire       data_we,       // an OUT payload accompanies the token

  // ---- what got selected ----
  output wire       sel_valid,
  output wire [2:0] sel_index,     // {direction, number}
  output wire       sel_stall,     // addressed an endpoint that is not there
  output wire [7:0] buf_out,       // the selected endpoint's byte

  output wire [15:0] n_sel,
  output wire [15:0] n_stall,
  // Two endpoints selected at once. Structurally impossible: the index
  // is a single value. The counter exists so that a build in which it
  // stopped being a single value would say so.
  output wire [15:0] n_multi
);

  // One byte per LOGICAL endpoint. Eight of these, one connector.
  reg [7:0] ep_buf [0:NIDX-1];
  reg [15:0] c_sel, c_stall, c_multi;
  integer k;

  // Identity. Both halves, always.
  wire        num_ok = (tok_ep < NEP[3:0]);
  wire [2:0]  idx    = {tok_is_in, tok_ep[1:0]};
  wire        exists = num_ok && cfg_en[idx];

  assign sel_valid = tok_valid && exists;
  assign sel_index = idx;
  assign sel_stall = tok_valid && !exists;
  assign buf_out   = ep_buf[idx];

  // A one-hot decode of the same index, used only to check the claim
  // that exactly one endpoint is ever selected.
  reg [NIDX-1:0] onehot;
  always @(*) begin
    onehot = {NIDX{1'b0}};
    if (sel_valid) onehot[idx] = 1'b1;
  end
  reg [3:0] popcnt;
  always @(*) begin
    popcnt = 4'd0;
    for (k = 0; k < NIDX; k = k + 1)
      if (onehot[k]) popcnt = popcnt + 4'd1;
  end

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (k = 0; k < NIDX; k = k + 1) ep_buf[k] <= 8'd0;
      c_sel   <= 16'd0;
      c_stall <= 16'd0;
      c_multi <= 16'd0;
    end else begin
      // An OUT payload lands in the buffer the token selected, and in
      // no other. The IN buffer of the same NUMBER is untouched.
      if (sel_valid && data_we && !tok_is_in) ep_buf[idx] <= data_in;
      if (sel_valid) c_sel   <= c_sel   + 16'd1;
      if (sel_stall) c_stall <= c_stall + 16'd1;
      if (popcnt > 4'd1) c_multi <= c_multi + 16'd1;
    end
  end

  assign n_sel   = c_sel;
  assign n_stall = c_stall;
  assign n_multi = c_multi;

endmodule

The three levels the belief collapses into one

A block diagram in three columns. The left column holds four endpoint names: endpoint 0 both directions, endpoint 2 OUT, endpoint 2 IN, and endpoint 3 IN. The middle column holds declarations from the active configuration: a control declaration for endpoint 0, a bulk OUT declaration, an interrupt IN declaration, and no declaration for endpoint 3 IN. The right column holds the silicon: a control buffer, two data buffers, and a fourth buffer that nothing reaches. Arrows connect names to declarations to buffers, showing that endpoint 2 OUT and endpoint 2 IN are separate paths with different types and sizes, that endpoint 3 IN has a name but no declaration and so cannot be reached, and that one buffer exists in silicon with no name mapped to it.EP0 IN + OUTalways existsEP2, OUTdir 0, num 2 -> idx 2EP2, INdir 1, num 2 -> idx 6EP3, INdir 1, num 3 -> idx 7controlby specificationbulk OUT, 64 Ba descriptor says sointerrupt IN, 8 Bother type, other sizeNO DECLARATIONcfg_en[7] is zeroEP0 buffercontrol logicbuffer A64 bytesbuffer B8 bytesbuffer Dno name reaches itsel_stall12
Read left to right. The left column is the name — thirty-two of them, and the diagram shows four. The middle column is the declaration, which is what an active configuration contains and what changes when a SET_INTERFACE changes an alternate setting. The right column is the silicon. The belief says these three columns are one column. Note that number 2 appears twice on the left, that one of the four names has no declaration at all, and that one buffer has no name reaching it.

Three of the diagram's features are the chapter:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    EP2 appears TWICE in the left column, and its two rows end at
    declarations with different TYPES and different SIZES

    EP3 IN has a NAME and no declaration -- the arrow stops at a red
    box. A perfectly good number, addressable in principle, and a token
    to it raises sel_stall rather than selecting anything

    BUFFER D has silicon and no name. It cannot be reached by any
    token. An engineer who believes the buffer IS the endpoint will
    spend a day looking at that buffer.

7. The Testbench

The intent phase states the corrected model in its own terms and consults no model at all:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    FOR EVERY ENDPOINT NUMBER
      write a distinct byte to its OUT form
      then read BOTH forms
      and require that they are not the same storage

That is the whole of phase 1, and it is worth seeing why it has to be shaped that way. If endpoints were identified by number alone — which is what "endpoints are ports" amounts to in hardware — the two reads would return the same byte. The check therefore asserts that two things differ, which is a claim about the architecture rather than about any implementation of it.

A reference model that indexed its own storage the way the design does would agree with the design about this even if both ignored direction. That is exactly the failure 30.4 produced, where a design and its model shared one wrong expression and agreed for 39,108 checks. So the claim is written as a paired write-and-read across the two directions, and not as a comparison against a mirror of the RTL.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    PHASES
      1 INTENT      IN and OUT of one number are independent
      2 EXHAUSTIVE  every (number, direction, enabled) triple
      3 SCENARIO    the named cases, including out-of-range numbers
      4 RANDOM      supplementary, audited

tb_usb_ep_decode.v — the testbench, Verilog-2005

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  tb_usb_ep_decode -- Verilog-2005 testbench for usb_ep_decode.
//
//  PHASE 1 is the intent phase and it does not consult a model. It
//  states the architectural claim and requires it:
//
//      endpoint N IN and endpoint N OUT are DIFFERENT ENDPOINTS.
//      Writing one does not change the other.
//
//  A reference model that indexed its own storage the same way the
//  design does would agree with the design about this even if both
//  ignored direction -- which is exactly the failure 30.4 produced.
//  So the claim is written as a paired write-and-read over the two
//  directions, not as a comparison against a mirror of the RTL.
//
//  PHASES
//    1 INTENT      IN and OUT of one number are independent
//    2 EXHAUSTIVE  every (number, direction, enabled) triple
//    3 SCENARIO    the named cases, including out-of-range numbers
//    4 RANDOM      supplementary, audited
// =====================================================================
`timescale 1ns/1ps

module tb_usb_ep_decode;

  localparam integer NEP  = 4;
  localparam integer NIDX = 8;

  reg        clk = 1'b0;
  reg        rst_n;
  reg  [NIDX-1:0] cfg_en;
  reg        tok_valid, tok_is_in, data_we;
  reg  [3:0] tok_ep;
  reg  [7:0] data_in;

  wire       sel_valid, sel_stall;
  wire [2:0] sel_index;
  wire [7:0] buf_out;
  wire [15:0] n_sel, n_stall, n_multi;

  usb_ep_decode #(.NEP(NEP), .NIDX(NIDX)) dut (
    .clk(clk), .rst_n(rst_n), .cfg_en(cfg_en),
    .tok_valid(tok_valid), .tok_ep(tok_ep), .tok_is_in(tok_is_in),
    .data_in(data_in), .data_we(data_we),
    .sel_valid(sel_valid), .sel_index(sel_index), .sel_stall(sel_stall),
    .buf_out(buf_out), .n_sel(n_sel), .n_stall(n_stall), .n_multi(n_multi)
  );

  always #5 clk = ~clk;

  // ---- the independent reference model ---------------------------
  // Written from the architectural rules:
  //   R1  a token names an endpoint by NUMBER AND DIRECTION
  //   R2  an endpoint that is not configured is not addressable
  //   R3  a number outside the implemented range is not addressable
  //   R4  an OUT payload lands in that endpoint alone
  // The storage below is deliberately a flat array indexed by a value
  // the model computes ITSELF from the two halves of the identity.
  reg [7:0]  rm_buf [0:NIDX-1];
  reg [15:0] rm_sel, rm_stall;

  integer chk_dir, chk_rnd, err, in_random;
  integer m_sel, m_stall, m_in, m_out, m_writes, m_oob, m_disabled,
          m_setupfail;
  integer i, j, e, d, g;
  reg [7:0] cap_buf;
  reg       cap_valid, cap_stall;
  reg [2:0] cap_index;

  task bump; begin
    if (in_random) chk_rnd = chk_rnd + 1; else chk_dir = chk_dir + 1;
  end endtask

  task ck;
    input [255:0] what;
    input [31:0]  got;
    input [31:0]  exp;
    begin
      bump;
      if (got !== exp) begin
        err = err + 1;
        if (!in_random && err <= 40)
          $display("  ** %0s: got %0d expected %0d  (t=%0t)", what, got, exp, $time);
      end
    end
  endtask

  // The model's own identity computation, from the rules rather than
  // from the RTL's expression.
  function [2:0] ref_index;
    input       dir;
    input [3:0] num;
    begin ref_index = {dir, num[1:0]}; end
  endfunction

  function ref_exists;
    input       dir;
    input [3:0] num;
    begin
      ref_exists = (num < NEP) && cfg_en[ref_index(dir, num)];
    end
  endfunction

  task ref_step;
    begin
      if (!rst_n) begin
        for (i = 0; i < NIDX; i = i + 1) rm_buf[i] = 8'd0;
        rm_sel = 0; rm_stall = 0;
      end else if (tok_valid) begin
        if (ref_exists(tok_is_in, tok_ep)) begin
          rm_sel = rm_sel + 1;
          if (data_we && !tok_is_in)
            rm_buf[ref_index(tok_is_in, tok_ep)] = data_in;
          m_sel = m_sel + 1;
          if (tok_is_in) m_in = m_in + 1; else m_out = m_out + 1;
          if (data_we && !tok_is_in) m_writes = m_writes + 1;
        end else begin
          rm_stall = rm_stall + 1;
          m_stall  = m_stall + 1;
          if (tok_ep >= NEP) m_oob = m_oob + 1;
          else               m_disabled = m_disabled + 1;
        end
      end
    end
  endtask

  task cmp_comb;
    reg exp_valid, exp_stall;
    begin
      exp_valid = tok_valid &&  ref_exists(tok_is_in, tok_ep);
      exp_stall = tok_valid && !ref_exists(tok_is_in, tok_ep);
      cap_valid = sel_valid; cap_stall = sel_stall;
      cap_index = sel_index; cap_buf   = buf_out;
      ck("sel_valid", {31'd0, sel_valid}, {31'd0, exp_valid});
      ck("sel_stall", {31'd0, sel_stall}, {31'd0, exp_stall});
      ck("sel_index", {29'd0, sel_index}, {29'd0, ref_index(tok_is_in, tok_ep)});
      if (exp_valid)
        ck("buf_out", {24'd0, buf_out}, {24'd0, rm_buf[ref_index(tok_is_in, tok_ep)]});
      else bump;
    end
  endtask

  task cmp_regs; begin
    ck("n_sel",   {16'd0, n_sel},   {16'd0, rm_sel});
    ck("n_stall", {16'd0, n_stall}, {16'd0, rm_stall});
    ck("n_multi", {16'd0, n_multi}, 32'd0);
  end endtask

  // The claim, asserted directly: at most one endpoint at a time.
  task intent_check; begin
    bump;
    if (sel_valid && sel_stall) begin
      err = err + 1;
      $display("  ** INTENT VIOLATED: selected and stalled at once (t=%0t)", $time);
    end
  end endtask

  task step; begin
    #1;
    cmp_comb;
    intent_check;
    @(posedge clk);
    ref_step;
    #1;
    cmp_regs;
    tok_valid = 0; tok_ep = 0; tok_is_in = 0; data_we = 0; data_in = 0;
  end endtask

  task idle; begin step; end endtask

  task hard_reset; begin
    rst_n = 0; cfg_en = {NIDX{1'b1}};
    tok_valid = 0; tok_ep = 0; tok_is_in = 0; data_we = 0; data_in = 0;
    repeat (3) begin @(posedge clk); ref_step; end
    #1; rst_n = 1;
    @(posedge clk); ref_step; #1; cmp_regs;
  end endtask

  task token;
    input       dir;
    input [3:0] num;
    input       we;
    input [7:0] v;
    begin
      tok_valid = 1; tok_is_in = dir; tok_ep = num;
      data_we = we; data_in = v;
      step;
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 1 -- THE INDEPENDENCE CLAIM.
  //
  //  For every endpoint NUMBER: write a distinct byte to its OUT form,
  //  then read both forms and require that they are not the same
  //  storage. If endpoints were identified by number alone -- which is
  //  what "endpoints are ports" amounts to in hardware -- these two
  //  would be one thing and the check would fail.
  // -----------------------------------------------------------------
  integer pairs_tested;

  task phase_intent;
    reg [7:0] out_val, in_val;
    begin
      hard_reset;
      pairs_tested = 0;
      for (e = 0; e < NEP; e = e + 1) begin
        // seed the IN form of this number with a known, different byte
        // by writing its OUT form and then checking the IN form is
        // untouched. The IN buffers all start at zero.
        token(1'b0, e[3:0], 1'b1, 8'hE0 + e[7:0]);   // OUT write
        token(1'b0, e[3:0], 1'b0, 8'h00);            // OUT read back
        out_val = cap_buf;
        token(1'b1, e[3:0], 1'b0, 8'h00);            // IN read
        in_val  = cap_buf;
        ck("OUT holds what was written", {24'd0, out_val}, {24'd0, 8'hE0 + e[7:0]});
        ck("IN of the same number is untouched", {24'd0, in_val}, 32'd0);
        bump;
        if (out_val === in_val) begin
          err = err + 1;
          $display("  ** INTENT VIOLATED: endpoint %0d IN and OUT are one buffer", e);
        end
        pairs_tested = pairs_tested + 1;
      end
      // and the reverse direction of the same claim: writing every OUT
      // form leaves every IN form at zero
      for (e = 0; e < NEP; e = e + 1) begin
        token(1'b1, e[3:0], 1'b0, 8'h00);
        ck("every IN still zero", {24'd0, cap_buf}, 32'd0);
      end
      bump;
      if (pairs_tested != NEP) begin
        err = err + 1;
        $display("  ** intent: only %0d pairs tested", pairs_tested);
      end
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 2 -- the exhaustive sweep.
  //
  //  AXES:
  //    endpoint number    0 .. NEP-1, plus one out of range        5
  //    direction          OUT, IN                                  2
  //    configured         no, yes                                  2
  //    ------------------------------------------------------------
  //                                        5 x 2 x 2 =            20
  //
  //  WHAT IS ABSENT: history. Two tokens in a row to related
  //  endpoints is phase 3. An exhaustive sweep is exhaustive over the
  //  axes it has.
  // -----------------------------------------------------------------
  task phase_sweep;
    begin
      for (e = 0; e < NEP + 1; e = e + 1)
      for (d = 0; d < 2; d = d + 1)
      for (g = 0; g < 2; g = g + 1) begin
        hard_reset;
        cfg_en = g ? {NIDX{1'b1}} : {NIDX{1'b0}};
        idle;
        bump;
        if (cfg_en !== (g ? {NIDX{1'b1}} : {NIDX{1'b0}})) begin
          err = err + 1; m_setupfail = m_setupfail + 1;
          $display("  ** setup: configuration not applied");
        end
        token(d[0], e[3:0], 1'b1, 8'h90 + e[7:0]);
        idle;
      end
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 3 -- the named scenarios.
  // -----------------------------------------------------------------
  task phase_scenarios;
    begin
      // S1 eight logical endpoints, one token path. Write all four OUT
      //    forms with distinct bytes and read them all back.
      hard_reset;
      for (e = 0; e < NEP; e = e + 1) token(1'b0, e[3:0], 1'b1, 8'h10 + e[7:0]);
      for (e = 0; e < NEP; e = e + 1) begin
        token(1'b0, e[3:0], 1'b0, 8'h00);
        ck("S1 each OUT kept its own byte", {24'd0, cap_buf}, {24'd0, 8'h10 + e[7:0]});
      end
      ck("S1 eight selections", {16'd0, n_sel}, 32'd8);

      // S2 a number outside the implemented range is not an endpoint.
      hard_reset;
      token(1'b0, 4'd7, 1'b1, 8'hFF);
      ck("S2 stalled",       {31'd0, cap_stall}, 32'd1);
      ck("S2 not selected",  {31'd0, cap_valid}, 32'd0);
      ck("S2 counted",       {16'd0, n_stall},   32'd1);

      // S3 a number that exists but is not configured is not addressable.
      //    "Present in the silicon" and "declared by the descriptors"
      //    are different things, and the host only knows the second.
      hard_reset;
      cfg_en = 8'b0000_0001;   // only OUT endpoint 0
      idle;
      token(1'b0, 4'd0, 1'b0, 8'h00);
      ck("S3 EP0 OUT exists", {31'd0, cap_valid}, 32'd1);
      token(1'b0, 4'd1, 1'b0, 8'h00);
      ck("S3 EP1 OUT does not", {31'd0, cap_stall}, 32'd1);
      token(1'b1, 4'd0, 1'b0, 8'h00);
      ck("S3 EP0 IN does not either", {31'd0, cap_stall}, 32'd1);

      // S4 the direction half of the identity, isolated. Same number,
      //    same cycle count, different endpoint.
      hard_reset;
      token(1'b0, 4'd2, 1'b1, 8'h55);
      token(1'b1, 4'd2, 1'b0, 8'h00);
      ck("S4 IN 2 is not OUT 2",   {24'd0, cap_buf},   32'd0);
      ck("S4 and its index differs", {29'd0, cap_index}, 32'd6);
      token(1'b0, 4'd2, 1'b0, 8'h00);
      ck("S4 OUT 2 still has it",  {24'd0, cap_buf},   {24'd0, 8'h55});
      ck("S4 and its index",       {29'd0, cap_index}, 32'd2);

      // S5 never two at once.
      ck("S5 nothing multi-selected", {16'd0, n_multi}, 32'd0);
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 4 -- random, audited.
  // -----------------------------------------------------------------
  task phase_random;
    integer r;
    begin
      in_random = 1;
      hard_reset;
      for (j = 0; j < 4000; j = j + 1) begin
        r = {$random} % 100;
        if (r < 70) begin
          token(({$random} % 2), ({$random} % 6), (({$random} % 100) < 60),
                ({$random} % 256));
        end else if (r < 82) begin
          // reconfigure: which endpoints the descriptors declare
          cfg_en = {$random} % 256;
          idle;
        end else begin
          idle;
        end
      end
      in_random = 0;
    end
  endtask

  initial begin
    chk_dir = 0; chk_rnd = 0; err = 0; in_random = 0;
    m_sel=0; m_stall=0; m_in=0; m_out=0; m_writes=0; m_oob=0;
    m_disabled=0; m_setupfail=0;

    phase_intent;
    $display("  phase 1 intent      : %0d checks, %0d errors  (%0d IN/OUT pairs)",
             chk_dir, err, pairs_tested);
    phase_sweep;
    $display("  phase 2 exhaustive  : %0d checks, %0d errors  (20 combinations)", chk_dir, err);
    phase_scenarios;
    $display("  phase 3 scenarios   : %0d checks, %0d errors", chk_dir, err);
    $display("  ---- DIRECTED-ONLY  : %0d checks, %0d errors ----", chk_dir, err);
    phase_random;

    $display("");
    $display("  measured reachability (all phases)");
    $display("    endpoints selected ..... %0d", m_sel);
    $display("      IN forms ............. %0d", m_in);
    $display("      OUT forms ............ %0d", m_out);
    $display("    OUT payloads written ... %0d", m_writes);
    $display("    stalled: out of range .. %0d", m_oob);
    $display("    stalled: not configured  %0d", m_disabled);
    $display("    setup failures ......... %0d", m_setupfail);
    $display("");
    $display("  directed checks ........ %0d", chk_dir);
    $display("  random checks .......... %0d", chk_rnd);
    $display("  TOTAL checks ........... %0d", chk_dir + chk_rnd);
    $display("  ERRORS ................. %0d", err);
    if (err == 0) $display("  PASS"); else $display("  FAIL");
    $finish;
  end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
                          VERILOG   SYSTEMVERILOG   VHDL-2008
    phase 1 intent            148           148          148
    phase 2 exhaustive        708           708          708
    phase 3 scenarios         864           864          864
    ---- DIRECTED             864           864          864
    errors                      0             0            0
    TOTAL                  32,867        32,867       32,867

    measured reachability, Verilog run
      endpoints selected ................... 961
        IN forms ........................... 515
        OUT forms .......................... 446
      OUT payloads written ................. 277
      stalled: number out of range ......... 958
      stalled: not declared by cfg_en ...... 908
      two endpoints selected at once ......... 0

Four of those rows carry the argument.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    515 IN + 446 OUT SELECTIONS

      Both forms of the same four numbers, reached separately. Under the
      belief there would be four endpoints here and these two rows
      would be one row.

    277 OUT PAYLOADS WRITTEN

      Each one landed in exactly one of eight buffers and left the IN
      buffer of the same number untouched. That is the independence
      claim, executed 277 times rather than asserted once.

    958 OUT-OF-RANGE vs 908 NOT-DECLARED

      TWO DIFFERENT REASONS to refuse, counted separately, because they
      are different failures with different fixes: one is a number that
      cannot exist, the other is a name the configuration did not
      declare. The belief has no vocabulary for the second.

    0 TWO-AT-ONCE

      A structural zero, from a one-hot decode of the same index that
      exists only to be counted. It is the negative control: a build in
      which "one endpoint" stopped being one value would say so.

The exhaustive sweep, and its named axes

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    AXES
      endpoint number    0, 1, 2, 3, plus one OUT OF RANGE        5
      direction          OUT, IN                                  2
      configured         no, yes                                  2
      ------------------------------------------------------------
                                        5 x 2 x 2 =              20

All 20 reached. The direction axis is the one the misconception lives on, and the fifth value of the first axis — a number at or above NEP — is what separates "this number cannot exist" from "this name was not declared".

8. SystemVerilog

usb_ep_decode_sv.sv — the design, SystemVerilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  usb_ep_decode_sv -- the same contract in SystemVerilog. Same ports,
//  same identity rule, same reset, same latency.
//
//  What the types add is that the claim "exactly one endpoint is
//  selected" becomes $countones of a one-hot vector, which says the
//  claim rather than computing it -- and the SVA block at the bottom
//  states the identity rule itself as a property that D-M1 breaks.
//
//  ------------------------------------------------------------------
//  What an endpoint actually IS, in hardware.
//
//  CLASSIFICATION: simplified synthesisable teaching RTL, built for one
//  purpose: to show that an endpoint's identity is a NUMBER AND A
//  DIRECTION, resolved from a token arriving on ONE shared physical
//  connection -- not a connector, not a pin, not a wire.
//
//  It is NOT a USB device controller. There is no PHY, no serial
//  interface engine, no packet framing, no data toggle, no transfer
//  layer. One byte of storage per logical endpoint stands in for what a
//  real part would implement as a RAM.
//
//  THE THING TO NOTICE IN THE PORT LIST
//  ------------------------------------
//  There is ONE tok_* group and ONE data path. Eight logical endpoints
//  are served by it. If endpoints were physical, this module would need
//  eight of something, and it needs one.
//
//  IDENTITY
//      index = {direction, number}
//  Endpoint 1 IN and endpoint 1 OUT are DIFFERENT ENDPOINTS with
//  different storage, different enables and different behaviour. They
//  share a number and nothing else. Mutation D-M1 drops the direction
//  from the index -- which is the misconception, written as RTL.
// =====================================================================
module usb_ep_decode_sv #(
  // Endpoint numbers 0 .. NEP-1, each with an IN and an OUT form.
  parameter int NEP  = 4,
  parameter int NIDX = 8      // NEP * 2
) (
  input  logic       clk,
  input  logic       rst_n,

  // ---- firmware configuration: which logical endpoints exist ----
  // Bit {dir, number}. An endpoint that the descriptors do not declare
  // is not there, and addressing it is a request error.
  input  logic [NIDX-1:0] cfg_en,

  // ---- ONE token path, ONE data path ----
  input  logic       tok_valid,
  input  logic [3:0] tok_ep,        // the number carried by the token
  input  logic       tok_is_in,     // the direction carried by the token
  input  logic [7:0] data_in,
  input  logic       data_we,       // an OUT payload accompanies the token

  // ---- what got selected ----
  output logic       sel_valid,
  output logic [2:0] sel_index,     // {direction, number}
  output logic       sel_stall,     // addressed an endpoint that is not there
  output logic [7:0] buf_out,       // the selected endpoint's byte

  output logic [15:0] n_sel,
  output logic [15:0] n_stall,
  // Two endpoints selected at once. Structurally impossible: the index
  // is a single value. The counter exists so that a build in which it
  // stopped being a single value would say so.
  output logic [15:0] n_multi
);

  // One byte per LOGICAL endpoint. Eight of these, one connector.
  logic [7:0] ep_buf [NIDX];
  logic [15:0] c_sel, c_stall, c_multi;
  int k;

  // Identity. Both halves, always.
  logic num_ok;
  assign num_ok = (tok_ep < 4'(NEP));
  logic [2:0] idx;
  assign idx = {tok_is_in, tok_ep[1:0]};
  logic exists;
  assign exists = num_ok && cfg_en[idx];

  assign sel_valid = tok_valid && exists;
  assign sel_index = idx;
  assign sel_stall = tok_valid && !exists;
  assign buf_out   = ep_buf[idx];

  // A one-hot decode of the same index, used only to check the claim
  // that exactly one endpoint is ever selected.
  logic [NIDX-1:0] onehot;
  always_comb begin
    onehot = '0;
    if (sel_valid) onehot[idx] = 1'b1;
  end
  // $countones states the claim -- "exactly one" -- in the language of
  // the claim rather than as a loop that happens to compute it.
  logic [3:0] popcnt;
  assign popcnt = 4'($countones(onehot));

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (k = 0; k < NIDX; k = k + 1) ep_buf[k] <= 8'd0;
      c_sel   <= 16'd0;
      c_stall <= 16'd0;
      c_multi <= 16'd0;
    end else begin
      // An OUT payload lands in the buffer the token selected, and in
      // no other. The IN buffer of the same NUMBER is untouched.
      if (sel_valid && data_we && !tok_is_in) ep_buf[idx] <= data_in;
      if (sel_valid) c_sel   <= c_sel   + 16'd1;
      if (sel_stall) c_stall <= c_stall + 16'd1;
      if (popcnt > 4'd1) c_multi <= c_multi + 16'd1;
    end
  end

  assign n_sel   = c_sel;
  assign n_stall = c_stall;
  assign n_multi = c_multi;


`ifdef SVA_ON
  // The misconception as a property: endpoint identity has TWO halves.
  // Icarus rejects SVA; under Icarus the named procedural checks in the
  // testbench enforce each of these.

  // SAFETY. The index always carries the direction. D-M1 drops it.
  property p_index_carries_direction;
    @(posedge clk) disable iff (!rst_n) sel_index[2] == tok_is_in;
  endproperty
  a_index_carries_direction: assert property (p_index_carries_direction);

  // SAFETY. Exactly one endpoint, or none. Never two.
  property p_at_most_one;
    @(posedge clk) disable iff (!rst_n) popcnt <= 4'd1;
  endproperty
  a_at_most_one: assert property (p_at_most_one);

  // SAFETY. Selected and stalled are exclusive.
  property p_select_xor_stall;
    @(posedge clk) disable iff (!rst_n) !(sel_valid && sel_stall);
  endproperty
  a_select_xor_stall: assert property (p_select_xor_stall);

  // SAFETY. An OUT payload lands in ONE buffer -- the one addressed.
  // Written as: no buffer changes unless it is the selected index.
  property p_write_is_local;
    @(posedge clk) disable iff (!rst_n)
      (sel_valid && data_we && !tok_is_in) |=> (ep_buf[$past(idx)] == $past(data_in));
  endproperty
  a_write_is_local: assert property (p_write_is_local);

  c_in_sel:    cover property (@(posedge clk) sel_valid &&  tok_is_in);
  c_out_sel:   cover property (@(posedge clk) sel_valid && !tok_is_in);
  c_oob:       cover property (@(posedge clk) tok_valid && !num_ok);
  c_disabled:  cover property (@(posedge clk) tok_valid && num_ok && !cfg_en[idx]);
`endif

endmodule

tb_usb_ep_decode_sv.sv — the testbench, SystemVerilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  tb_usb_ep_decode -- Verilog-2005 testbench for usb_ep_decode.
//
//  PHASE 1 is the intent phase and it does not consult a model. It
//  states the architectural claim and requires it:
//
//      endpoint N IN and endpoint N OUT are DIFFERENT ENDPOINTS.
//      Writing one does not change the other.
//
//  A reference model that indexed its own storage the same way the
//  design does would agree with the design about this even if both
//  ignored direction -- which is exactly the failure 30.4 produced.
//  So the claim is written as a paired write-and-read over the two
//  directions, not as a comparison against a mirror of the RTL.
//
//  PHASES
//    1 INTENT      IN and OUT of one number are independent
//    2 EXHAUSTIVE  every (number, direction, enabled) triple
//    3 SCENARIO    the named cases, including out-of-range numbers
//    4 RANDOM      supplementary, audited
// =====================================================================
`timescale 1ns/1ps

module tb_usb_ep_decode_sv;

  localparam int NEP  = 4;
  localparam int NIDX = 8;

  logic      clk = 1'b0;
  logic      rst_n;
  logic [NIDX-1:0] cfg_en;
  logic      tok_valid, tok_is_in, data_we;
  logic [3:0] tok_ep;
  logic [7:0] data_in;

  wire       sel_valid, sel_stall;
  wire [2:0] sel_index;
  wire [7:0] buf_out;
  wire [15:0] n_sel, n_stall, n_multi;

  usb_ep_decode_sv #(.NEP(NEP), .NIDX(NIDX)) dut (
    .clk(clk), .rst_n(rst_n), .cfg_en(cfg_en),
    .tok_valid(tok_valid), .tok_ep(tok_ep), .tok_is_in(tok_is_in),
    .data_in(data_in), .data_we(data_we),
    .sel_valid(sel_valid), .sel_index(sel_index), .sel_stall(sel_stall),
    .buf_out(buf_out), .n_sel(n_sel), .n_stall(n_stall), .n_multi(n_multi)
  );

  always #5 clk = ~clk;

  // ---- the independent reference model ---------------------------
  // Written from the architectural rules:
  //   R1  a token names an endpoint by NUMBER AND DIRECTION
  //   R2  an endpoint that is not configured is not addressable
  //   R3  a number outside the implemented range is not addressable
  //   R4  an OUT payload lands in that endpoint alone
  // The storage below is deliberately a flat array indexed by a value
  // the model computes ITSELF from the two halves of the identity.
  logic [7:0] rm_buf [NIDX];
  logic [15:0] rm_sel, rm_stall;

  int  chk_dir, chk_rnd, err;
  bit  in_random;
  int  m_sel, m_stall, m_in, m_out, m_writes, m_oob, m_disabled,
       m_setupfail;
  int  i, j, e, d, g;
  logic [7:0] cap_buf;
  logic     cap_valid, cap_stall;
  logic [2:0] cap_index;

  task bump; begin
    if (in_random) chk_rnd = chk_rnd + 1; else chk_dir = chk_dir + 1;
  end endtask

  task ck(string what, logic [31:0] got, logic [31:0] exp);
    begin
      bump;
      if (got !== exp) begin
        err = err + 1;
        if (!in_random && err <= 40)
          $display("  ** %s: got %0d expected %0d  (t=%0t)", what, got, exp, $time);
      end
    end
  endtask

  // The model's own identity computation, from the rules rather than
  // from the RTL's expression.
  function logic [2:0] ref_index(logic dir, logic [3:0] num);
    return {dir, num[1:0]};
  endfunction

  function logic ref_exists(logic dir, logic [3:0] num);
    return (num < NEP) && cfg_en[ref_index(dir, num)];
  endfunction

  task ref_step;
    begin
      if (!rst_n) begin
        for (i = 0; i < NIDX; i = i + 1) rm_buf[i] = 8'd0;
        rm_sel = 0; rm_stall = 0;
      end else if (tok_valid) begin
        if (ref_exists(tok_is_in, tok_ep)) begin
          rm_sel = rm_sel + 1;
          if (data_we && !tok_is_in)
            rm_buf[ref_index(tok_is_in, tok_ep)] = data_in;
          m_sel = m_sel + 1;
          if (tok_is_in) m_in = m_in + 1; else m_out = m_out + 1;
          if (data_we && !tok_is_in) m_writes = m_writes + 1;
        end else begin
          rm_stall = rm_stall + 1;
          m_stall  = m_stall + 1;
          if (tok_ep >= NEP) m_oob = m_oob + 1;
          else               m_disabled = m_disabled + 1;
        end
      end
    end
  endtask

  task cmp_comb;
    logic exp_valid, exp_stall;
    begin
      exp_valid = tok_valid &&  ref_exists(tok_is_in, tok_ep);
      exp_stall = tok_valid && !ref_exists(tok_is_in, tok_ep);
      cap_valid = sel_valid; cap_stall = sel_stall;
      cap_index = sel_index; cap_buf   = buf_out;
      ck("sel_valid", {31'd0, sel_valid}, {31'd0, exp_valid});
      ck("sel_stall", {31'd0, sel_stall}, {31'd0, exp_stall});
      ck("sel_index", {29'd0, sel_index}, {29'd0, ref_index(tok_is_in, tok_ep)});
      if (exp_valid)
        ck("buf_out", {24'd0, buf_out}, {24'd0, rm_buf[ref_index(tok_is_in, tok_ep)]});
      else bump;
    end
  endtask

  task cmp_regs; begin
    ck("n_sel",   {16'd0, n_sel},   {16'd0, rm_sel});
    ck("n_stall", {16'd0, n_stall}, {16'd0, rm_stall});
    ck("n_multi", {16'd0, n_multi}, 32'd0);
  end endtask

  // The claim, asserted directly: at most one endpoint at a time.
  task intent_check; begin
    bump;
    if (sel_valid && sel_stall) begin
      err = err + 1;
      $display("  ** INTENT VIOLATED: selected and stalled at once (t=%0t)", $time);
    end
  end endtask

  task step; begin
    #1;
    cmp_comb;
    intent_check;
    @(posedge clk);
    ref_step;
    #1;
    cmp_regs;
    tok_valid = 0; tok_ep = 0; tok_is_in = 0; data_we = 0; data_in = 0;
  end endtask

  task idle; step; endtask

  task hard_reset; begin
    rst_n = 0; cfg_en = {NIDX{1'b1}};
    tok_valid = 0; tok_ep = 0; tok_is_in = 0; data_we = 0; data_in = 0;
    repeat (3) begin @(posedge clk); ref_step; end
    #1; rst_n = 1;
    @(posedge clk); ref_step; #1; cmp_regs;
  end endtask

  task token(logic dir, logic [3:0] num, logic we, logic [7:0] v);
    begin
      tok_valid = 1; tok_is_in = dir; tok_ep = num;
      data_we = we; data_in = v;
      step;
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 1 -- THE INDEPENDENCE CLAIM.
  //
  //  For every endpoint NUMBER: write a distinct byte to its OUT form,
  //  then read both forms and require that they are not the same
  //  storage. If endpoints were identified by number alone -- which is
  //  what "endpoints are ports" amounts to in hardware -- these two
  //  would be one thing and the check would fail.
  // -----------------------------------------------------------------
  int  pairs_tested;

  task phase_intent;
    logic [7:0] out_val, in_val;
    begin
      hard_reset;
      pairs_tested = 0;
      for (e = 0; e < NEP; e = e + 1) begin
        // seed the IN form of this number with a known, different byte
        // by writing its OUT form and then checking the IN form is
        // untouched. The IN buffers all start at zero.
        token(1'b0, 4'(e), 1'b1, 8'hE0 + 8'(e));   // OUT write
        token(1'b0, 4'(e), 1'b0, 8'h00);            // OUT read back
        out_val = cap_buf;
        token(1'b1, 4'(e), 1'b0, 8'h00);            // IN read
        in_val  = cap_buf;
        ck("OUT holds what was written", {24'd0, out_val}, {24'd0, 8'hE0 + 8'(e)});
        ck("IN of the same number is untouched", {24'd0, in_val}, 32'd0);
        bump;
        if (out_val === in_val) begin
          err = err + 1;
          $display("  ** INTENT VIOLATED: endpoint %0d IN and OUT are one buffer", e);
        end
        pairs_tested = pairs_tested + 1;
      end
      // and the reverse direction of the same claim: writing every OUT
      // form leaves every IN form at zero
      for (e = 0; e < NEP; e = e + 1) begin
        token(1'b1, 4'(e), 1'b0, 8'h00);
        ck("every IN still zero", {24'd0, cap_buf}, 32'd0);
      end
      bump;
      if (pairs_tested != NEP) begin
        err = err + 1;
        $display("  ** intent: only %0d pairs tested", pairs_tested);
      end
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 2 -- the exhaustive sweep.
  //
  //  AXES:
  //    endpoint number    0 .. NEP-1, plus one out of range        5
  //    direction          OUT, IN                                  2
  //    configured         no, yes                                  2
  //    ------------------------------------------------------------
  //                                        5 x 2 x 2 =            20
  //
  //  WHAT IS ABSENT: history. Two tokens in a row to related
  //  endpoints is phase 3. An exhaustive sweep is exhaustive over the
  //  axes it has.
  // -----------------------------------------------------------------
  task phase_sweep;
    begin
      for (e = 0; e < NEP + 1; e = e + 1)
      for (d = 0; d < 2; d = d + 1)
      for (g = 0; g < 2; g = g + 1) begin
        hard_reset;
        cfg_en = g ? {NIDX{1'b1}} : {NIDX{1'b0}};
        idle;
        bump;
        if (cfg_en !== (g ? {NIDX{1'b1}} : {NIDX{1'b0}})) begin
          err = err + 1; m_setupfail = m_setupfail + 1;
          $display("  ** setup: configuration not applied");
        end
        token(d[0], 4'(e), 1'b1, 8'h90 + 8'(e));
        idle;
      end
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 3 -- the named scenarios.
  // -----------------------------------------------------------------
  task phase_scenarios;
    begin
      // S1 eight logical endpoints, one token path. Write all four OUT
      //    forms with distinct bytes and read them all back.
      hard_reset;
      for (e = 0; e < NEP; e = e + 1) token(1'b0, 4'(e), 1'b1, 8'h10 + 8'(e));
      for (e = 0; e < NEP; e = e + 1) begin
        token(1'b0, 4'(e), 1'b0, 8'h00);
        ck("S1 each OUT kept its own byte", {24'd0, cap_buf}, {24'd0, 8'h10 + 8'(e)});
      end
      ck("S1 eight selections", {16'd0, n_sel}, 32'd8);

      // S2 a number outside the implemented range is not an endpoint.
      hard_reset;
      token(1'b0, 4'd7, 1'b1, 8'hFF);
      ck("S2 stalled",       {31'd0, cap_stall}, 32'd1);
      ck("S2 not selected",  {31'd0, cap_valid}, 32'd0);
      ck("S2 counted",       {16'd0, n_stall},   32'd1);

      // S3 a number that exists but is not configured is not addressable.
      //    "Present in the silicon" and "declared by the descriptors"
      //    are different things, and the host only knows the second.
      hard_reset;
      cfg_en = 8'b0000_0001;   // only OUT endpoint 0
      idle;
      token(1'b0, 4'd0, 1'b0, 8'h00);
      ck("S3 EP0 OUT exists", {31'd0, cap_valid}, 32'd1);
      token(1'b0, 4'd1, 1'b0, 8'h00);
      ck("S3 EP1 OUT does not", {31'd0, cap_stall}, 32'd1);
      token(1'b1, 4'd0, 1'b0, 8'h00);
      ck("S3 EP0 IN does not either", {31'd0, cap_stall}, 32'd1);

      // S4 the direction half of the identity, isolated. Same number,
      //    same cycle count, different endpoint.
      hard_reset;
      token(1'b0, 4'd2, 1'b1, 8'h55);
      token(1'b1, 4'd2, 1'b0, 8'h00);
      ck("S4 IN 2 is not OUT 2",   {24'd0, cap_buf},   32'd0);
      ck("S4 and its index differs", {29'd0, cap_index}, 32'd6);
      token(1'b0, 4'd2, 1'b0, 8'h00);
      ck("S4 OUT 2 still has it",  {24'd0, cap_buf},   {24'd0, 8'h55});
      ck("S4 and its index",       {29'd0, cap_index}, 32'd2);

      // S5 never two at once.
      ck("S5 nothing multi-selected", {16'd0, n_multi}, 32'd0);
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 4 -- random, audited.
  // -----------------------------------------------------------------
  task phase_random;
    int r;
    begin
      in_random = 1;
      hard_reset;
      for (j = 0; j < 4000; j = j + 1) begin
        r = $urandom_range(99);
        if (r < 70) begin
          token(1'($urandom_range(1)), 4'($urandom_range(5)),
                ($urandom_range(99) < 60), 8'($urandom_range(255)));
        end else if (r < 82) begin
          // reconfigure: which endpoints the descriptors declare
          cfg_en = NIDX'($urandom_range(255));
          idle;
        end else begin
          idle;
        end
      end
      in_random = 0;
    end
  endtask

  initial begin
    chk_dir = 0; chk_rnd = 0; err = 0; in_random = 0;
    m_sel=0; m_stall=0; m_in=0; m_out=0; m_writes=0; m_oob=0;
    m_disabled=0; m_setupfail=0;

    phase_intent;
    $display("  phase 1 intent      : %0d checks, %0d errors  (%0d IN/OUT pairs)",
             chk_dir, err, pairs_tested);
    phase_sweep;
    $display("  phase 2 exhaustive  : %0d checks, %0d errors  (20 combinations)", chk_dir, err);
    phase_scenarios;
    $display("  phase 3 scenarios   : %0d checks, %0d errors", chk_dir, err);
    $display("  ---- DIRECTED-ONLY  : %0d checks, %0d errors ----", chk_dir, err);
    phase_random;

    $display("");
    $display("  measured reachability (all phases)");
    $display("    endpoints selected ..... %0d", m_sel);
    $display("      IN forms ............. %0d", m_in);
    $display("      OUT forms ............ %0d", m_out);
    $display("    OUT payloads written ... %0d", m_writes);
    $display("    stalled: out of range .. %0d", m_oob);
    $display("    stalled: not configured  %0d", m_disabled);
    $display("    setup failures ......... %0d", m_setupfail);
    $display("");
    $display("  directed checks ........ %0d", chk_dir);
    $display("  random checks .......... %0d", chk_rnd);
    $display("  TOTAL checks ........... %0d", chk_dir + chk_rnd);
    $display("  ERRORS ................. %0d", err);
    if (err == 0) $display("  PASS"); else $display("  FAIL");
    $finish;
  end

endmodule

The structural claim becomes a one-line property:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  property p_index_carries_direction;
    @(posedge clk) disable iff (!rst_n) sel_index[2] == tok_is_in;
  endproperty

Which is unusual and worth flagging: it is an assertion about an encoding, not about a behaviour over time. No antecedent delay, no consequent sequence, no implication at all — just an equality that must hold in every cycle. It is written that way because the encoding is the architectural claim, and a mutation that changes it is a mutation that changes what an endpoint is.

Beside it sit the negative control and the locality claim, and the second one is the independence check promoted from the testbench into one line:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  property p_at_most_one;
    @(posedge clk) disable iff (!rst_n) popcnt <= 4'd1;
  endproperty

  property p_write_is_local;   // an OUT payload changes ONE buffer

p_at_most_one is the assertion form of the n_multi counter: both say "exactly one endpoint, or none, ever", and both are structurally satisfied. Having the claim in both forms is deliberate — Icarus rejects concurrent SVA, so under Icarus the counter and the named procedural checks in the testbench are what enforce it.

9. VHDL-2008

usb_ep_decode.vhd — the design, VHDL-2008

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- =====================================================================
--  usb_ep_decode (VHDL-2008) -- the same contract. Same ports, same
--  identity rule, same reset, same latency.
--
--  VHDL's contribution to THIS misconception is that the identity has
--  to be constructed explicitly from its two halves and converted to an
--  index with a written conversion. There is no way to write the index
--  without mentioning the direction, which is precisely the thing the
--  misconception leaves out.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity usb_ep_decode is
  generic (NEP : natural := 4; NIDX : natural := 8);
  port (
    clk       : in  std_logic;
    rst_n     : in  std_logic;
    cfg_en    : in  std_logic_vector(NIDX - 1 downto 0);
    tok_valid : in  std_logic;
    tok_ep    : in  unsigned(3 downto 0);
    tok_is_in : in  std_logic;
    data_in   : in  std_logic_vector(7 downto 0);
    data_we   : in  std_logic;
    sel_valid : out std_logic;
    sel_index : out unsigned(2 downto 0);
    sel_stall : out std_logic;
    buf_out   : out std_logic_vector(7 downto 0);
    n_sel     : out unsigned(15 downto 0);
    n_stall   : out unsigned(15 downto 0);
    n_multi   : out unsigned(15 downto 0)
  );
end entity usb_ep_decode;

architecture rtl of usb_ep_decode is
  type buf_arr is array (0 to NIDX - 1) of std_logic_vector(7 downto 0);
  signal ep_buf : buf_arr := (others => (others => '0'));
  signal c_sel, c_stall, c_multi : unsigned(15 downto 0) := (others => '0');

  signal num_ok, exists, sv_i, ss_i : std_logic;
  signal idx    : unsigned(2 downto 0);
  signal onehot : std_logic_vector(NIDX - 1 downto 0);
  signal popcnt : natural range 0 to NIDX;

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

  num_ok <= '1' when tok_ep < NEP else '0';
  -- The identity, both halves, written out.
  idx    <= tok_is_in & tok_ep(1 downto 0);
  exists <= '1' when (num_ok = '1' and cfg_en(to_integer(idx)) = '1') else '0';

  sv_i <= tok_valid and exists;
  ss_i <= tok_valid and (not exists);

  sel_valid <= sv_i;
  sel_index <= idx;
  sel_stall <= ss_i;
  buf_out   <= ep_buf(to_integer(idx));

  oh : process (sv_i, idx)
    variable v : std_logic_vector(NIDX - 1 downto 0);
  begin
    v := (others => '0');
    if sv_i = '1' then v(to_integer(idx)) := '1'; end if;
    onehot <= v;
  end process oh;

  popcnt <= popcount(onehot);

  seq : process (clk, rst_n)
  begin
    if rst_n = '0' then
      ep_buf  <= (others => (others => '0'));
      c_sel   <= (others => '0');
      c_stall <= (others => '0');
      c_multi <= (others => '0');
    elsif rising_edge(clk) then
      if sv_i = '1' and data_we = '1' and tok_is_in = '0' then
        ep_buf(to_integer(idx)) <= data_in;
      end if;
      if sv_i = '1' then c_sel   <= c_sel   + 1; end if;
      if ss_i = '1' then c_stall <= c_stall + 1; end if;
      if popcnt > 1 then c_multi <= c_multi + 1; end if;
    end if;
  end process seq;

  n_sel   <= c_sel;
  n_stall <= c_stall;
  n_multi <= c_multi;

end architecture rtl;

tb_usb_ep_decode.vhd — the testbench, VHDL-2008

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- =====================================================================
--  tb_usb_ep_decode -- VHDL-2008 testbench for usb_ep_decode.
--  Phases 1-3 present the SAME directed stimulus as the Verilog and
--  SystemVerilog benches, so their directed counts must agree.
--
--  Phase 1 states the independence claim directly rather than comparing
--  against a model that indexes its storage the way the design does.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all;

entity tb_usb_ep_decode is
end entity tb_usb_ep_decode;

architecture sim of tb_usb_ep_decode is
  constant NEP  : natural := 4;
  constant NIDX : natural := 8;
  constant HALF : time    := 10 ns;

  signal clk       : std_logic := '0';
  signal rst_n     : std_logic := '0';
  signal cfg_en    : std_logic_vector(NIDX-1 downto 0) := (others => '1');
  signal tok_valid : std_logic := '0';
  signal tok_ep    : unsigned(3 downto 0) := (others => '0');
  signal tok_is_in : std_logic := '0';
  signal data_in   : std_logic_vector(7 downto 0) := (others => '0');
  signal data_we   : std_logic := '0';

  signal sel_valid, sel_stall : std_logic;
  signal sel_index : unsigned(2 downto 0);
  signal buf_out   : std_logic_vector(7 downto 0);
  signal n_sel, n_stall, n_multi : unsigned(15 downto 0);

  signal done_flag : boolean := false;

  function b2i (s : std_logic) return integer is
  begin
    if s = '1' then return 1; else return 0; end if;
  end function b2i;
begin

  dut : entity work.usb_ep_decode
    generic map (NEP => NEP, NIDX => NIDX)
    port map (clk => clk, rst_n => rst_n, cfg_en => cfg_en,
              tok_valid => tok_valid, tok_ep => tok_ep, tok_is_in => tok_is_in,
              data_in => data_in, data_we => data_we,
              sel_valid => sel_valid, sel_index => sel_index,
              sel_stall => sel_stall, buf_out => buf_out,
              n_sel => n_sel, n_stall => n_stall, n_multi => n_multi);

  clkgen : process
  begin
    while not done_flag loop
      clk <= '0'; wait for HALF;
      clk <= '1'; wait for HALF;
    end loop;
    wait;
  end process clkgen;

  stim : process
    type buf_arr is array (0 to NIDX-1) of std_logic_vector(7 downto 0);
    variable rm_buf : buf_arr := (others => (others => '0'));
    variable rm_sel, rm_stall : natural := 0;

    variable chk_dir, chk_rnd, errs, shown : natural := 0;
    variable in_random : boolean := false;
    variable m_sel, m_stall, m_in, m_out, m_writes : natural := 0;
    variable m_oob, m_disabled, m_setupfail, pairs_tested : natural := 0;

    variable cap_valid, cap_stall : std_logic := '0';
    variable cap_index : unsigned(2 downto 0) := (others => '0');
    variable cap_buf   : std_logic_vector(7 downto 0) := (others => '0');

    variable seed1 : positive := 337_711;
    variable seed2 : positive := 64_223;

    procedure bump is
    begin
      if in_random then chk_rnd := chk_rnd + 1; else chk_dir := chk_dir + 1; end if;
    end procedure bump;

    procedure ck (what : string; got : integer; exp : integer) is
    begin
      bump;
      if got /= exp then
        errs := errs + 1;
        if (not in_random) and shown < 40 then
          shown := shown + 1;
          report "  ** " & what & ": got " & integer'image(got) &
                 " expected " & integer'image(exp) severity warning;
        end if;
      end if;
    end procedure ck;

    -- The model computes the identity from the two halves ITSELF.
    impure function ref_index (dir : std_logic; num : unsigned(3 downto 0))
      return integer is
    begin
      return b2i(dir) * 4 + to_integer(num(1 downto 0));
    end function ref_index;

    impure function ref_exists (dir : std_logic; num : unsigned(3 downto 0))
      return boolean is
    begin
      return (num < NEP) and (cfg_en(ref_index(dir, num)) = '1');
    end function ref_exists;

    procedure ref_step is
    begin
      if rst_n = '0' then
        rm_buf := (others => (others => '0'));
        rm_sel := 0; rm_stall := 0;
      elsif tok_valid = '1' then
        if ref_exists(tok_is_in, tok_ep) then
          rm_sel := rm_sel + 1;
          if data_we = '1' and tok_is_in = '0' then
            rm_buf(ref_index(tok_is_in, tok_ep)) := data_in;
            m_writes := m_writes + 1;
          end if;
          m_sel := m_sel + 1;
          if tok_is_in = '1' then m_in := m_in + 1; else m_out := m_out + 1; end if;
        else
          rm_stall := rm_stall + 1;
          m_stall  := m_stall + 1;
          if tok_ep >= NEP then m_oob := m_oob + 1;
          else                  m_disabled := m_disabled + 1; end if;
        end if;
      end if;
    end procedure ref_step;

    procedure cmp_comb is
      variable ev, es : integer;
    begin
      if tok_valid = '1' and ref_exists(tok_is_in, tok_ep) then ev := 1; else ev := 0; end if;
      if tok_valid = '1' and not ref_exists(tok_is_in, tok_ep) then es := 1; else es := 0; end if;
      cap_valid := sel_valid; cap_stall := sel_stall;
      cap_index := sel_index; cap_buf   := buf_out;
      ck("sel_valid", b2i(sel_valid), ev);
      ck("sel_stall", b2i(sel_stall), es);
      ck("sel_index", to_integer(sel_index), ref_index(tok_is_in, tok_ep));
      if ev = 1 then
        ck("buf_out", to_integer(unsigned(buf_out)),
           to_integer(unsigned(rm_buf(ref_index(tok_is_in, tok_ep)))));
      else
        bump;
      end if;
    end procedure cmp_comb;

    procedure cmp_regs is
    begin
      ck("n_sel",   to_integer(n_sel),   rm_sel);
      ck("n_stall", to_integer(n_stall), rm_stall);
      ck("n_multi", to_integer(n_multi), 0);
    end procedure cmp_regs;

    procedure intent_check is
    begin
      bump;
      if sel_valid = '1' and sel_stall = '1' then
        errs := errs + 1;
        report "  ** INTENT VIOLATED: selected and stalled at once" severity warning;
      end if;
    end procedure intent_check;

    procedure step is
    begin
      wait for 1 ns;
      cmp_comb;
      intent_check;
      wait until rising_edge(clk);
      ref_step;
      wait for 1 ns;
      cmp_regs;
      tok_valid <= '0'; tok_ep <= (others => '0'); tok_is_in <= '0';
      data_we <= '0'; data_in <= (others => '0');
    end procedure step;

    procedure idle is begin step; end procedure;

    procedure hard_reset is
    begin
      rst_n <= '0'; cfg_en <= (others => '1');
      tok_valid <= '0'; tok_ep <= (others => '0'); tok_is_in <= '0';
      data_we <= '0'; data_in <= (others => '0');
      for i in 0 to 2 loop wait until rising_edge(clk); ref_step; end loop;
      wait for 1 ns; rst_n <= '1';
      wait until rising_edge(clk); ref_step; wait for 1 ns; cmp_regs;
    end procedure hard_reset;

    procedure token (dir : std_logic; num : natural; we : std_logic; v : natural) is
    begin
      tok_valid <= '1'; tok_is_in <= dir;
      tok_ep    <= to_unsigned(num, 4);
      data_we   <= we;
      data_in   <= std_logic_vector(to_unsigned(v, 8));
      step;
    end procedure token;

    impure function rnd (n : positive) return natural is
      variable x : real;
    begin
      uniform(seed1, seed2, x);
      return natural(real(n - 1) * x);
    end function rnd;

    variable out_val, in_val : std_logic_vector(7 downto 0);
    variable r : natural;
  begin
    -- ---- PHASE 1 : the independence claim ----
    hard_reset;
    pairs_tested := 0;
    for e in 0 to NEP-1 loop
      token('0', e, '1', 16#E0# + e);
      token('0', e, '0', 0);
      out_val := cap_buf;
      token('1', e, '0', 0);
      in_val  := cap_buf;
      ck("OUT holds what was written", to_integer(unsigned(out_val)), 16#E0# + e);
      ck("IN of the same number is untouched", to_integer(unsigned(in_val)), 0);
      bump;
      if out_val = in_val then
        errs := errs + 1;
        report "  ** INTENT VIOLATED: IN and OUT are one buffer" severity warning;
      end if;
      pairs_tested := pairs_tested + 1;
    end loop;
    for e in 0 to NEP-1 loop
      token('1', e, '0', 0);
      ck("every IN still zero", to_integer(unsigned(cap_buf)), 0);
    end loop;
    bump;
    if pairs_tested /= NEP then
      errs := errs + 1;
      report "  ** intent: too few pairs" severity warning;
    end if;
    report "  phase 1 intent      : " & integer'image(chk_dir) &
           " checks, " & integer'image(errs) & " errors  (" &
           integer'image(pairs_tested) & " IN/OUT pairs)";

    -- ---- PHASE 2 : 5 x 2 x 2 = 20 ----
    for e in 0 to NEP loop
      for d in 0 to 1 loop
        for g in 0 to 1 loop
          hard_reset;
          if g = 1 then cfg_en <= (others => '1'); else cfg_en <= (others => '0'); end if;
          idle;
          bump;
          if (g = 1 and cfg_en /= (cfg_en'range => '1')) or
             (g = 0 and cfg_en /= (cfg_en'range => '0')) then
            errs := errs + 1; m_setupfail := m_setupfail + 1;
            report "  ** setup: configuration not applied" severity warning;
          end if;
          if d = 1 then token('1', e, '1', 16#90# + e);
          else          token('0', e, '1', 16#90# + e); end if;
          idle;
        end loop;
      end loop;
    end loop;
    report "  phase 2 exhaustive  : " & integer'image(chk_dir) &
           " checks, " & integer'image(errs) & " errors  (20 combinations)";

    -- ---- PHASE 3 : the named scenarios ----
    hard_reset;
    for e in 0 to NEP-1 loop token('0', e, '1', 16#10# + e); end loop;
    for e in 0 to NEP-1 loop
      token('0', e, '0', 0);
      ck("S1 each OUT kept its own byte", to_integer(unsigned(cap_buf)), 16#10# + e);
    end loop;
    ck("S1 eight selections", to_integer(n_sel), 8);

    hard_reset;
    token('0', 7, '1', 16#FF#);
    ck("S2 stalled",      b2i(cap_stall), 1);
    ck("S2 not selected", b2i(cap_valid), 0);
    ck("S2 counted",      to_integer(n_stall), 1);

    hard_reset;
    cfg_en <= "00000001";
    idle;
    token('0', 0, '0', 0);
    ck("S3 EP0 OUT exists", b2i(cap_valid), 1);
    token('0', 1, '0', 0);
    ck("S3 EP1 OUT does not", b2i(cap_stall), 1);
    token('1', 0, '0', 0);
    ck("S3 EP0 IN does not either", b2i(cap_stall), 1);

    hard_reset;
    token('0', 2, '1', 16#55#);
    token('1', 2, '0', 0);
    ck("S4 IN 2 is not OUT 2",     to_integer(unsigned(cap_buf)), 0);
    ck("S4 and its index differs", to_integer(cap_index), 6);
    token('0', 2, '0', 0);
    ck("S4 OUT 2 still has it",    to_integer(unsigned(cap_buf)), 16#55#);
    ck("S4 and its index",         to_integer(cap_index), 2);

    ck("S5 nothing multi-selected", to_integer(n_multi), 0);

    report "  phase 3 scenarios   : " & integer'image(chk_dir) &
           " checks, " & integer'image(errs) & " errors";
    report "  ---- DIRECTED-ONLY  : " & integer'image(chk_dir) &
           " checks, " & integer'image(errs) & " errors ----";

    -- ---- PHASE 4 : random ----
    in_random := true;
    hard_reset;
    for j in 0 to 3999 loop
      r := rnd(100);
      if r < 70 then
        if rnd(2) = 1 then
          token('1', rnd(6), '0', rnd(256));
        else
          if rnd(100) < 60 then token('0', rnd(6), '1', rnd(256));
          else                  token('0', rnd(6), '0', rnd(256)); end if;
        end if;
      elsif r < 82 then
        cfg_en <= std_logic_vector(to_unsigned(rnd(256), NIDX));
        idle;
      else
        idle;
      end if;
    end loop;
    in_random := false;

    report "  measured reachability (all phases)";
    report "    endpoints selected ..... " & integer'image(m_sel);
    report "      IN forms ............. " & integer'image(m_in);
    report "      OUT forms ............ " & integer'image(m_out);
    report "    OUT payloads written ... " & integer'image(m_writes);
    report "    stalled: out of range .. " & integer'image(m_oob);
    report "    stalled: not configured  " & integer'image(m_disabled);
    report "    setup failures ......... " & integer'image(m_setupfail);
    report "  directed checks ........ " & integer'image(chk_dir);
    report "  random checks .......... " & integer'image(chk_rnd);
    report "  TOTAL checks ........... " & integer'image(chk_dir + chk_rnd);
    report "  ERRORS ................. " & integer'image(errs);
    if errs = 0 then report "  PASS"; else report "  FAIL" severity failure; end if;
    done_flag <= true;
    wait;
  end process stim;

end architecture sim;

VHDL makes the index construction unusually legible, because the concatenation is explicit about what is the high bit:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  idx    <= tok_is_in & tok_ep(1 downto 0);
  exists <= '1' when (num_ok = '1' and cfg_en(to_integer(idx)) = '1') else '0';

The direction is the leading element of the concatenation that addresses the buffer array, and cfg_en is indexed by the result. So "is direction part of the address" and "does existence depend on the declaration" are both answered by reading two lines, with nothing to trace and no comment to trust.

10. The Misconception As Hardware

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    MUT    THE BELIEF ENCODED                      V-DIR  SV-DIR  VH-DIR
    D-M1   an endpoint is a number
           (direction dropped from the index)         48      48      48
    D-M2   an endpoint exists because the
           silicon has one (map ignored)              58      58      58

BASE is zero in all six columns; both directed columns match across the languages, so the entire detection is from directed stimulus.

D-M1 scores 48, and the shape of those 48 is the interesting part. The mutant collapses IN and OUT of one number onto one buffer, so it is wrong only where the two forms are supposed to hold different bytes. A stimulus that wrote one form and read the same form back would find nothing: the mutant is correct whenever the two directions are never distinguished. This is why 48 rather than hundreds, and why the paired write-and-read of phase 1 is the check that finds it.

D-M2 scores 58. It makes every name that the silicon implements exist, regardless of the configuration's map. A device built this way responds to endpoints it never declared, which in the field looks like a device that works until a host validates its descriptors — and then looks like a host bug.

11. What The Wrong Model Does To Debugging

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    SYMPTOM      "endpoint 2 works in one direction and not the other"

    WRONG MODEL  endpoint 2 is one thing
    WRONG QUESTION   why is my endpoint working intermittently /
                     partially / only for reads?
    WASTED ON        buffer sizing, arbitration between the directions,
                     a suspected shared-resource conflict inside one
                     endpoint -- an investigation of a thing that does
                     not exist

    CORRECT MODEL  those are TWO endpoints
    THE QUESTIONS, and they are ordinary once asked:
                 are BOTH declared in the active configuration?
                 do their descriptors have the sizes and types you
                   think they do?
                 is one of them HALTED? (independent state)
                 are their data toggles independent, and did you reset
                   the right one?
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    SYMPTOM      "the host never touches my endpoint"

    WRONG MODEL  the endpoint exists because I built it
    WRONG QUESTION   why won't the host talk to my endpoint?
    WASTED ON        the endpoint's logic, which is fine

    CORRECT MODEL  an endpoint exists because the active configuration
                   declares it
    THE QUESTION   is it in the descriptors of the configuration and
                   alternate setting currently in force -- not the
                   descriptors you wrote, the ones the host READ?
                   Dump them from the host side.

The second one deserves emphasis because of how often it happens with alternate settings. A device that works after a manual SET_INTERFACE and not otherwise has silicon that is entirely correct and a declaration that the host never activated. The buffer is innocent every time.

12. Interview Reasoning

"How many endpoints can a USB device have?"

The question has a trap in it, and walking into the trap knowingly is the answer.

The address space is four bits of endpoint number and one bit of direction, so 32 names — but the count is not really the interesting part, because an endpoint is identified by the pair (number, direction), not by the number alone. Endpoint 2 IN and endpoint 2 OUT are two different endpoints with their own transfer type, their own maximum packet size, their own halt state and their own data toggle.

Endpoint 0 is the exception that shows the rule: it exists in both directions, always, and it exists by specification rather than because a descriptor declares it. It is the only one like that.

And the other half of the answer is that a device does not "have" endpoints in a fixed sense at all. Before configuration it has exactly one — endpoint 0 — no matter what its silicon contains. After configuration it has the ones that the active configuration's descriptors declare, which is why a device with alternate settings can change how many endpoints it has with a single control request and no change in hardware. That mechanism is how isochronous devices negotiate bandwidth the host actually has.

So the practical version of the answer is: 32 names available, one endpoint before configuration, and after that, exactly as many as the active configuration says — which is a property of the descriptors, not of the chip.

13. Exercises

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    1  IDENTITY
       Write out the (number, direction) pair for 0x03, 0x83, 0x00 and
       0x80, and say which of the four always exist.

    2  DESCRIPTORS
       A device declares 0x02 as bulk OUT 64 B and 0x82 as interrupt
       IN 8 B. List every piece of state these two do NOT share.

    3  ALTERNATE SETTINGS
       Explain why an isochronous device offers an alternate setting
       with zero endpoints, and what breaks if it does not.

    4  VERILOG
       Set NEP to 16. What is the index width, what is the width of
       cfg_en, and which lines of the module change?

    5  SYSTEMVERILOG
       Add per-endpoint halt state. Write the property that says a
       halted endpoint in one direction does not affect the other.

    6  VHDL
       Implement the extension in exercise 4 and say what the range
       constraint buys you that the Verilog version does not have.

    7  TESTBENCH
       Phase 2 sweeps three axes and contains no history. Name the
       fourth axis that halt state would add, and the first SEQUENCE it
       would make necessary.

    8  MUTATION
       Write a third mutation for the belief "endpoint 0 is just
       another endpoint" and predict its score. Then say which intent
       check catches it.

    9  DEBUG
       A device's bulk IN works and its bulk OUT NAKs forever. Give
       four hypotheses in the order you would test them, and say which
       ones the wrong model would never generate.

   10  REFERENCE MODEL
       Write the decode a belief-sharing model would contain, and prove
       to yourself that it agrees with D-M1 in every cycle -- so that
       a comparison against it reports a clean pass.

14. What Carries Forward

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    THE CORRECTION
    o  an endpoint is a (NUMBER, DIRECTION) pair, and 0x82 is
       "number 2, IN" rather than "endpoint 130"
    o  32 names, of which endpoint 0's two are the only ones that
       always exist and the only ones declared by the SPECIFICATION
    o  every other endpoint exists because the ACTIVE CONFIGURATION
       declares it -- so the count changes with SET_INTERFACE and not
       with the silicon
    o  toggle, halt, size and type are per-PAIR, not per-number
    o  three levels the belief collapses: NAME, DECLARATION,
       IMPLEMENTATION -- and each can exist without the others

    THE HARDWARE
    o  idx = {tok_is_in, tok_ep} -- the direction is the high bit of
       the address, not a qualifier
    o  exists needs BOTH halves of the name AND the declaration:
       num_ok && cfg_en[idx]
    o  a token to a name with no declaration is an ERROR, not a NAK

    THE METHOD
    o  an intent check can say "these two must DIFFER", which no
       comparison against a belief-sharing model can say
    o  an assertion can be about an ENCODING rather than a behaviour,
       when the encoding is the architectural claim
    o  the check that catches the misconception is the one that asserts
       two things DIFFER -- and it is only obvious once the mutation has
       been named: write the mutation first, then the check

    THE DEBUG CONSEQUENCE
    o  "endpoint 2 half-works" is a question about a thing that does
       not exist. There are two endpoints; ask about each.
    o  "the host ignores my endpoint" is answered in the descriptors
       the host READ, never in the buffer.

The next belief is about speed, and it is the one that survives longest because it is usually true — which makes it far more dangerous than a belief that is simply wrong.

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.