Skip to content
VLSI Mentor

USB · Module 28

USB vs SPI

SPI selects a peripheral with a wire routed at layout time and USB with an address the host assigned — so a chip-select contention is invisible to every slave (0 of 11) while a duplicate USB address is detected every time (274 of 274).

The second of four comparisons. Chapter 28.1 compared USB with a protocol that has no clock wire. This one compares it with a protocol that does — so timing is not the difference, and the difference turns out to be something more interesting.

1. The Comparison That Is Not A Wire Count

The usual answer is arithmetic: SPI needs four wires plus one chip select per device, USB needs two wires for up to 127 devices, therefore USB wins on pin count. The arithmetic is right. It is also the least interesting thing about the difference, and it leads people to the wrong conclusion — because if pin count were the axis, nobody would still be putting SPI flash on boards, and everybody does.

SPI has a clock wire. It has no tolerance budget worth measuring, no framing to guess, no synchronisation to recover. On the axis that chapter 28.1 was about, SPI is better than UART and arguably simpler than USB.

What SPI does not have is any way to ask a device who it is.

That last sentence is a measurable claim, so this chapter measures it. The result is 0 out of 11 against 274 out of 274, and section 6 explains what those numbers count.

2. Two Designs, One Question

Not one module with a mode switch. Two separate designs, because they are two separate designs, and forcing them into one would hide the thing being compared. Both are driven by the same testbench from the same stimulus source, so the comparison happens inside the verification rather than in prose afterwards.

spi_cs_selectusb_addr_select
what selectsone of N wiresan address field
who knows the mappingthe boardthe host, at run time
where the mapping lives in RTLa parametera register file
can it change after power-onnoyes
configuration portnonecfg_valid / cfg_slot / cfg_addr
wires for N devicesN + 32

The row that matters is the third. In one design the mapping is an elaboration-time constant; in the other it is state. That is not a stylistic difference between the two files — it is the protocol difference, expressed in the only way hardware can express it.

3. The Designs (Verilog-2005)

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  TWO SELECTION DISCIPLINES, SIDE BY SIDE.
//
//  CLASSIFICATION: simplified synthesisable teaching RTL.
//  Two separate modules, because they ARE two separate designs -- that
//  is the comparison. Neither is a complete controller: there is no SPI
//  shift register here and no USB packet decoder, only the part that
//  answers one question.
//
//      "Which peripheral should respond to this transaction?"
//
//  SPI answers it with a WIRE. A board designer routes one chip-select
//  line per device, and the master pulls one of them low. The answer is
//  fixed at layout time and is not visible to anybody except the master
//  and the one slave being addressed.
//
//  USB answers it with a FIELD. Every token on the bus carries an
//  address and an endpoint number, every device sees every token, and
//  the address was ASSIGNED BY THE HOST at enumeration -- so the answer
//  is run-time state that can be changed, queried and checked.
//
//  The difference that matters is not the wire count. It is this:
//
//      SPI's selection information is DISTRIBUTED (each slave sees only
//      its own select line). USB's is BROADCAST (every device sees the
//      whole address field).
//
//  A distributed selector cannot detect its own worst failure. Section
//  7 of the chapter measures exactly that, and the RTL below is built
//  so the measurement is possible rather than assumed.
// =====================================================================

// ---------------------------------------------------------------------
//  spi_cs_select -- selection by wire.
//
//  IMPORTANT AND EASY TO MISS: this module is given ALL N chip-select
//  lines. No real SPI slave has that. A real slave receives exactly one
//  CS pin and has no way to know whether any other slave is also
//  selected. So `contention` below is a BUS MONITOR output -- it is
//  observable only from a vantage point that no SPI device on a real
//  board occupies.
//
//  That is not a limitation of this model. It is the finding.
// ---------------------------------------------------------------------
module spi_cs_select #(
  parameter integer N_DEV = 4
) (
  input  wire                     clk,
  input  wire                     rst_n,

  // Active-low chip selects, one per device, routed on the board.
  input  wire [N_DEV-1:0]         cs_n,

  // Resolved selection.
  output wire                     sel_valid,
  output wire [$clog2(N_DEV)-1:0] sel_idx,

  // The failure a real slave cannot see.
  output wire                     contention,

  output wire [31:0]              n_sel,
  output wire [31:0]              n_idle,
  output wire [31:0]              n_contend
);

  localparam integer IW = $clog2(N_DEV);

  // How many selects are asserted. On a correct board this is 0 or 1;
  // two at once is a firmware or routing fault, and on real hardware it
  // means two slaves drive MISO simultaneously.
  reg [IW:0] n_low;
  reg [IW-1:0] first_low;
  integer i;

  always @(*) begin
    n_low     = {(IW+1){1'b0}};
    first_low = {IW{1'b0}};
    // Walk downwards so the LOWEST asserted index wins the tie. The
    // direction is arbitrary but it must be DEFINED: an undefined
    // winner makes the contention case unverifiable.
    for (i = N_DEV - 1; i >= 0; i = i - 1) begin
      if (!cs_n[i]) begin
        n_low     = n_low + 1'b1;
        first_low = i[IW-1:0];
      end
    end
  end

  assign contention = (n_low > 1);
  assign sel_valid  = (n_low == 1);
  assign sel_idx    = first_low;

  reg [31:0] sel_c, idle_c, con_c;
  assign n_sel    = sel_c;
  assign n_idle   = idle_c;
  assign n_contend = con_c;

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      sel_c  <= 32'd0;
      idle_c <= 32'd0;
      con_c  <= 32'd0;
    end else begin
      if (n_low == 0)      idle_c <= idle_c + 32'd1;
      else if (n_low == 1) sel_c  <= sel_c  + 32'd1;
      else                 con_c  <= con_c  + 32'd1;
    end
  end

endmodule


// ---------------------------------------------------------------------
//  usb_addr_select -- selection by assigned name.
//
//  A device slot holds an address the HOST gave it, plus a mask of
//  which endpoints are configured. A token matches a slot only if the
//  address matches AND that endpoint is configured.
//
//  Two real USB rules are enforced here because both are places where
//  an implementation can be plausibly wrong:
//
//    * Address 0 is the DEFAULT address. A device sitting at address 0
//      is mid-enumeration and has only endpoint 0. Honouring any other
//      endpoint at address 0 would let a half-enumerated device answer
//      traffic it has no configuration for.
//
//    * An UNASSIGNED slot matches nothing at all, including address 0.
//      A slot that answered before being assigned would respond to the
//      enumeration of a different device.
//
//  Unlike the SPI decoder, this one CAN see its own worst failure: two
//  slots holding the same address is directly detectable, because the
//  address is a field every slot compares against.
// ---------------------------------------------------------------------
module usb_addr_select #(
  parameter integer N_SLOT = 4
) (
  input  wire                      clk,
  input  wire                      rst_n,

  // ---- the assignment interface: this is the whole difference ----
  //
  // The SPI decoder above has no equivalent port. Its mapping is fixed
  // at elaboration. This one's mapping is a register file, writable at
  // run time, which is what "the host assigns an address" means in
  // hardware.
  input  wire                      cfg_valid,
  input  wire [$clog2(N_SLOT)-1:0] cfg_slot,
  input  wire                      cfg_assigned,
  input  wire [6:0]                cfg_addr,
  input  wire [15:0]               cfg_ep_mask,

  // ---- a token ----
  input  wire                      req_valid,
  input  wire [6:0]                req_addr,
  input  wire [3:0]                req_ep,

  // ---- the answer ----
  output wire                      sel_valid,
  output wire [$clog2(N_SLOT)-1:0] sel_slot,
  output wire [2:0]                sel_reason,

  // The failure this discipline CAN see.
  output wire                      conflict,

  output wire [31:0]               n_match,
  output wire [31:0]               n_no_addr,
  output wire [31:0]               n_no_ep,
  output wire [31:0]               n_conflict
);

  localparam integer SW = $clog2(N_SLOT);

  localparam [2:0] R_NONE    = 3'd0,  // no token this cycle
                   R_MATCH   = 3'd1,  // addressed and configured
                   R_NO_ADDR = 3'd2,  // nobody holds that address
                   R_NO_EP   = 3'd3;  // address matched, endpoint did not

  // The assignment register file -- per-field arrays rather than an
  // array of structs, because a variable field-select into an unpacked
  // array of packed structs aborts the Icarus elaborator.
  reg              sl_asg  [0:N_SLOT-1];
  reg [6:0]        sl_addr [0:N_SLOT-1];
  reg [15:0]       sl_epm  [0:N_SLOT-1];

  integer j;

  // ---- the match, combinational over the registered table ----
  reg          m_any, m_ep_any;
  reg [SW-1:0] m_slot;
  reg [SW:0]   m_addr_count;

  always @(*) begin
    m_any        = 1'b0;
    m_ep_any     = 1'b0;
    m_slot       = {SW{1'b0}};
    m_addr_count = {(SW+1){1'b0}};
    // Walk downwards so the LOWEST matching slot wins. Defined, not
    // arbitrary: the conflict case must have a predictable winner or it
    // cannot be checked.
    for (j = N_SLOT - 1; j >= 0; j = j - 1) begin
      if (sl_asg[j] && (sl_addr[j] == req_addr)) begin
        m_addr_count = m_addr_count + 1'b1;
        m_any        = 1'b1;
        m_slot       = j[SW-1:0];
        // Address 0 is the default address: endpoint 0 only. A device
        // there has no configuration yet, so honouring any other
        // endpoint would answer traffic it cannot service.
        if (sl_addr[j] == 7'd0) begin
          if (req_ep == 4'd0) m_ep_any = 1'b1;
        end else begin
          if (sl_epm[j][req_ep]) m_ep_any = 1'b1;
        end
      end
    end
  end

  assign conflict = (m_addr_count > 1);

  assign sel_valid  = req_valid && m_any && m_ep_any;
  assign sel_slot   = m_slot;
  assign sel_reason = !req_valid ? R_NONE    :
                      !m_any     ? R_NO_ADDR :
                      !m_ep_any  ? R_NO_EP   : R_MATCH;

  reg [31:0] match_c, noaddr_c, noep_c, conf_c;
  assign n_match    = match_c;
  assign n_no_addr  = noaddr_c;
  assign n_no_ep    = noep_c;
  assign n_conflict = conf_c;

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (j = 0; j < N_SLOT; j = j + 1) begin
        sl_asg[j]  <= 1'b0;
        sl_addr[j] <= 7'd0;
        sl_epm[j]  <= 16'd0;
      end
      match_c  <= 32'd0;
      noaddr_c <= 32'd0;
      noep_c   <= 32'd0;
      conf_c   <= 32'd0;
    end else begin
      if (cfg_valid) begin
        sl_asg[cfg_slot]  <= cfg_assigned;
        sl_addr[cfg_slot] <= cfg_addr;
        sl_epm[cfg_slot]  <= cfg_ep_mask;
      end

      if (req_valid) begin
        case (sel_reason)
          R_MATCH:   match_c  <= match_c  + 32'd1;
          R_NO_ADDR: noaddr_c <= noaddr_c + 32'd1;
          R_NO_EP:   noep_c   <= noep_c   + 32'd1;
          default:   ;
        endcase
        if (conflict) conf_c <= conf_c + 32'd1;
      end
    end
  end

endmodule

The one comment in that file worth reading twice

spi_cs_select is handed all N chip-select lines. No real SPI slave has that.

A slave on a real board receives exactly one CS pin. It cannot see the other N−1 lines, it has no port through which it could, and adding one would mean routing every select line to every device — which is both a different bus and a worse one.

So the contention output is a bus monitor: it is observable only from a vantage point that nothing on a real SPI board occupies. That is not a shortcoming of the model. It is the finding, and section 6 measures it.

4. The Two Real USB Rules In The Matcher

usb_addr_select enforces two rules that are easy to state and easy to get wrong, and both are places a plausible implementation goes astray.

Address 0 is the default address, and it exposes endpoint 0 only. A device sitting at address 0 is mid-enumeration: it has been reset and given the default address, but it has not yet been configured. Honouring any other endpoint there would let a half-enumerated device answer traffic it has no configuration to service. This is mutation L5.

An unassigned slot matches nothing at all, including address 0. A slot that answered before being assigned would respond during another device's enumeration — which is the single most destructive thing a USB device can do on a shared bus, because it corrupts the enumeration of a device that was behaving correctly. This is mutation L4, and it is the highest-scoring single-condition mutation in the chapter.

Tie resolution is defined, not arbitrary

Both designs walk their index space downwards so the lowest index wins. Either direction would be defensible; what is not defensible is leaving it unspecified, because then the conflict case has no predictable outcome and cannot be checked at all. Mutation L3 flips the direction, and the testbench catches it on every one of the 11 patterns where it makes a difference.

Both shadow models walk upwards and stop at the first hit. Same answer, different derivation — which is the only way a model can fail to inherit the design's mistake.

5. Distributed Versus Broadcast

The same question, answered from two different amounts of information

SPI routes one chip-select wire per device so each slave sees only its own line and cannot detect that another slave is also selected, while USB broadcasts an address field that every device compares, making a duplicate address directly countableSPI masterpulls one line lowN select wiresfixed at layoutSlave ksees cs[k] only0 of 11contentions detectableUSB hostsends a tokenaddr + ep fieldassigned at run timeEvery devicecompares the field274 of 274conflicts detectableroutesone pincannotsendsall seecounts12
The top row's failure is invisible to every participant: a selected slave sees its own line low, which is exactly what it sees when it is correctly selected. The bottom row's failure is a count over a field every device already receives.

The asymmetry is not about effort or cleverness. It is about how much information reaches the place where the decision is made. A slave that receives one bit cannot compute a property of four bits, whatever logic you put behind that bit. A device that receives the whole address field gets the count for free.

6. The Measurement

For each failure the two disciplines admit, could a real device detect it from what that device actually receives?

This is not a pass/fail. It is a distinguishability test, and it is constructed rather than asserted — the bench builds each observer's view and compares it against the view that observer would have had in the clean case. If the two views are equal, no logic inside that observer can tell them apart, whatever it does.

The SPI side

With four devices there are 2⁴ = 16 chip-select patterns, of which 11 assert two or more selects. For each of those, and for each selected slave k:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    slave k's entire view of the world        =  cs_n[k]
    slave k's view if it alone were selected  =  0 (its own line low)

    the two are EQUAL for every k and every contention pattern

So the result is 0 detectable out of 11, and it is not a measurement of a weak design. It is a proof: the two situations are represented by identical inputs at the only place the slave can observe, so no slave can distinguish them. On real hardware both selected slaves drive MISO, the master reads the wired-AND of two answers, and every participant believes the transaction succeeded.

The bench also confirms the bus monitor sees all 11 — which is the point. The detector exists only because spi_cs_select was handed every wire.

The USB side

Measured on every one of the 2560 exhaustive tokens, not on a directed handful, so the denominator is every duplicate-address token the sweep produces: 274 detected out of 274.

failures admitteddetectable by a real device
SPI chip-select contention11 patterns0
USB duplicate address274 tokens274

Where the 2560 comes from

dimensionvalueswhy
assigned mask16which of the 4 slots have been given an address
address mode4all distinct · all identical · all at address 0 · pairwise duplicates
endpoint mode2all four endpoints configured · only endpoints 0 and 2
request address50 through 4
request endpoint40 through 3

16 × 4 × 2 × 5 × 4 = 2560, every point reachable, because the five dimensions are independent inputs with no forbidden combinations.

The address-mode dimension is the one that earns its place. Without modes 1 and 3 every slot would hold a distinct address, the conflict logic would never fire, and the sweep would report complete coverage of a design whose conflict detector had never been exercised. An exhaustive sweep that cannot reach a mechanism is not exhaustive over anything that matters.

7. What Run-Time Addressing Actually Buys

A device being addressed, re-addressed, and switched off — all without touching the board

The host assigns address 5 to a slot which then answers at address 5, re-assigns it to address 9 so address 5 goes silent and address 9 answers, then unassigns it so the slot answers nothing including the default addressassigned address 5assigned address 5re-addressed to 9re-addressed to 9unassigned entirelyunassigned entirelyanswers at address 5answers at address 5re-addressed: 5 is silentre-addressed: 5 is silentanswers at address 9answers at address 9unassigned: silent even at 0unassigned: silent even at0clkcfg_validcfg_addr5599999999cfg_asgreq_validreq_addr0555999000sel_validsel_reasonNONEMATCHNONENOADDRMATCHNONENOADDRNOADDRNONENONEt0t1t2t3t4t5t6t7t8t9
Each configuration write takes effect on the NEXT cycle, so the token in the same cycle as a write still sees the old table. The SPI decoder has no equivalent sequence because it has no configuration port at all.

Three capabilities in that trace, none of which spi_cs_select can express:

A device can be given an address it did not have. That is enumeration. On SPI the equivalent operation is soldering.

A device can be given a different address later. USB does this on every bus reset: every device drops back to the default address and is re-enumerated. A bus that can renumber its participants can also recover from a participant disappearing, which is what hot-plug is.

A device can be switched off logically. An unassigned slot answers nothing, including the default address. On SPI, deselecting a device means not asserting its wire — which is not the same thing, because the wire is still there and still capable of selecting it.

8. When A Configuration Takes Effect

The design registers its table, so a token presented in the same cycle as a configuration write sees the old table. The write lands on the next edge.

This sounds pedantic and is not. It is the single most common shadow-model error in this whole track: the bench applies the write immediately, the design applies it a cycle later, and the resulting disagreement gets mistaken for a design bug. So the bench mirrors each write only after the edge, and phase 5 checks the boundary explicitly:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    cfg_valid = 1'b1; cfg_slot = 2'd1; cfg_assigned = 1'b1;
    cfg_addr = 7'd8; cfg_ep_mask = 16'h000F;
    req_valid = 1'b1; req_addr = 7'd7; req_ep = 4'd0;
    #1;
    ck(usb_sel_valid === 1'b1,
       "a configuration write took effect in the same cycle as the token");

A design that applied the write early would disagree here and nowhere else in the entire run — 2560 exhaustive tokens would all still pass, because none of them writes and reads in the same cycle. That is what a one-cycle boundary check is for.

9. The Testbench (Verilog)

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  Testbench for spi_cs_select and usb_addr_select.
//
//  BOTH DISCIPLINES, ONE BENCH, ONE STIMULUS SOURCE. The point of the
//  chapter is a comparison, so the comparison happens inside the
//  verification rather than in prose afterwards.
//
//  THE SHADOW MODELS ARE FORMULATED IN THE OPPOSITE DIRECTION.
//  Both designs resolve ties by walking their index space DOWNWARDS so
//  the lowest index wins. Both models walk UPWARDS and stop at the
//  first hit. Same answer, different derivation -- which is the only
//  way a model can fail to inherit the design's mistake.
//
//  THE HEADLINE MEASUREMENT is not a pass/fail. It is:
//
//      of the selection failures each discipline admits, how many are
//      DETECTABLE FROM THE VANTAGE POINT A REAL DEVICE OCCUPIES?
//
//  An SPI slave receives exactly one chip-select pin. A USB device sees
//  every token's whole address field. That asymmetry is measured in
//  phase 3, and it is measured by CONSTRUCTING each observer's view and
//  comparing it against the view it would have had in the clean case --
//  not by asserting the conclusion.
// =====================================================================
`timescale 1ns/1ps
module tb_sl_v;

  localparam integer N_DEV  = 4;
  localparam integer N_SLOT = 4;

  reg clk = 1'b0, rst_n = 1'b0;
  always #5 clk = ~clk;

  // ---- SPI side ----
  reg  [N_DEV-1:0] cs_n = {N_DEV{1'b1}};
  wire             spi_sel_valid, spi_contention;
  wire [1:0]       spi_sel_idx;
  wire [31:0]      spi_n_sel, spi_n_idle, spi_n_contend;

  spi_cs_select #(.N_DEV(N_DEV)) dut_spi (
    .clk(clk), .rst_n(rst_n), .cs_n(cs_n),
    .sel_valid(spi_sel_valid), .sel_idx(spi_sel_idx),
    .contention(spi_contention),
    .n_sel(spi_n_sel), .n_idle(spi_n_idle), .n_contend(spi_n_contend)
  );

  // ---- USB side ----
  reg        cfg_valid = 1'b0;
  reg [1:0]  cfg_slot = 2'd0;
  reg        cfg_assigned = 1'b0;
  reg [6:0]  cfg_addr = 7'd0;
  reg [15:0] cfg_ep_mask = 16'd0;
  reg        req_valid = 1'b0;
  reg [6:0]  req_addr = 7'd0;
  reg [3:0]  req_ep = 4'd0;

  wire        usb_sel_valid, usb_conflict;
  wire [1:0]  usb_sel_slot;
  wire [2:0]  usb_sel_reason;
  wire [31:0] usb_n_match, usb_n_no_addr, usb_n_no_ep, usb_n_conflict;

  usb_addr_select #(.N_SLOT(N_SLOT)) dut_usb (
    .clk(clk), .rst_n(rst_n),
    .cfg_valid(cfg_valid), .cfg_slot(cfg_slot),
    .cfg_assigned(cfg_assigned), .cfg_addr(cfg_addr),
    .cfg_ep_mask(cfg_ep_mask),
    .req_valid(req_valid), .req_addr(req_addr), .req_ep(req_ep),
    .sel_valid(usb_sel_valid), .sel_slot(usb_sel_slot),
    .sel_reason(usb_sel_reason), .conflict(usb_conflict),
    .n_match(usb_n_match), .n_no_addr(usb_n_no_addr),
    .n_no_ep(usb_n_no_ep), .n_conflict(usb_n_conflict)
  );

  localparam [2:0] R_NONE = 3'd0, R_MATCH = 3'd1,
                   R_NO_ADDR = 3'd2, R_NO_EP = 3'd3;

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

  // ---- the headline measurement ----
  integer spi_contend_patterns = 0;  // patterns with >= 2 selects asserted
  integer spi_contend_visible  = 0;  // ... distinguishable by SOME slave
  integer usb_conflict_cases   = 0;  // tokens hitting a duplicate address
  integer usb_conflict_visible = 0;  // ... detected by the device logic
  integer seed;

  // $random is SIGNED: mask the sign bit before any modulo, or every
  // `% N` is negative about half the time and the sweep silently
  // collapses onto a subset of its range.
  function [31:0] urand;
    input dummy;
    begin urand = $random(seed) & 32'h3FFF_FFFF; end
  endfunction

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

  // ---- the shadow copy of the assignment table ----
  //
  // Maintained by the BENCH from the writes it issued, never read back
  // from the design. A model that reads the design's own table cannot
  // detect a design that writes the wrong entry.
  reg        s_asg  [0:N_SLOT-1];
  reg [6:0]  s_addr [0:N_SLOT-1];
  reg [15:0] s_epm  [0:N_SLOT-1];

  // ---------------------------------------------------------------
  //  SPI model -- counts upward, unlike the design's downward walk.
  // ---------------------------------------------------------------
  function [2:0] spi_popcount(input [N_DEV-1:0] p);
    integer i; reg [2:0] c;
    begin
      c = 3'd0;
      for (i = 0; i < N_DEV; i = i + 1) if (!p[i]) c = c + 3'd1;
      spi_popcount = c;
    end
  endfunction

  function [1:0] spi_first_low(input [N_DEV-1:0] p);
    integer i; reg found; reg [1:0] r;
    begin
      found = 1'b0; r = 2'd0;
      // upward, stop at the first. The design walks downward and
      // overwrites, so the two agree only if both are right.
      for (i = 0; i < N_DEV; i = i + 1)
        if (!p[i] && !found) begin r = i[1:0]; found = 1'b1; end
      spi_first_low = r;
    end
  endfunction

  // ---------------------------------------------------------------
  //  USB model -- also upward-and-stop, over the bench's own table.
  // ---------------------------------------------------------------
  function [2:0] usb_addr_count(input [6:0] a);
    integer j; reg [2:0] c;
    begin
      c = 3'd0;
      for (j = 0; j < N_SLOT; j = j + 1)
        if (s_asg[j] && (s_addr[j] == a)) c = c + 3'd1;
      usb_addr_count = c;
    end
  endfunction

  function [1:0] usb_first_slot(input [6:0] a);
    integer j; reg found; reg [1:0] r;
    begin
      found = 1'b0; r = 2'd0;
      for (j = 0; j < N_SLOT; j = j + 1)
        if (s_asg[j] && (s_addr[j] == a) && !found) begin r = j[1:0]; found = 1'b1; end
      usb_first_slot = r;
    end
  endfunction

  // Does ANY assigned slot at address `a` expose endpoint `e`?
  //
  // The default-address rule lives here, stated independently of the
  // design: a slot at address 0 is mid-enumeration and has endpoint 0
  // only, whatever its ep_mask happens to contain.
  function usb_ep_ok(input [6:0] a, input [3:0] e);
    integer j; reg ok;
    begin
      ok = 1'b0;
      for (j = 0; j < N_SLOT; j = j + 1) begin
        if (s_asg[j] && (s_addr[j] == a)) begin
          if (s_addr[j] == 7'd0) begin
            if (e == 4'd0) ok = 1'b1;
          end else begin
            if (s_epm[j][e]) ok = 1'b1;
          end
        end
      end
      usb_ep_ok = ok;
    end
  endfunction

  // ---------------------------------------------------------------
  //  Drive one SPI pattern and check the resolution.
  // ---------------------------------------------------------------
  task spi_step(input [N_DEV-1:0] p);
    reg [2:0]  nlow;
    reg [1:0]  efirst;
    reg [31:0] s0, i0, c0;
    begin
      nlow   = spi_popcount(p);
      efirst = spi_first_low(p);
      s0 = spi_n_sel; i0 = spi_n_idle; c0 = spi_n_contend;

      cs_n = p;
      #1;

      // ---- PROPERTY 1: exactly one low is a selection ----
      ck(spi_sel_valid === (nlow == 3'd1),
         "spi sel_valid does not mean exactly one chip select is asserted");
      // ---- PROPERTY 2: two or more low is contention ----
      ck(spi_contention === (nlow > 3'd1),
         "spi contention does not mean two or more chip selects are asserted");
      // ---- PROPERTY 3: a selection and a contention are exclusive ----
      ck(!(spi_sel_valid && spi_contention),
         "spi reported a selection and a contention at the same time");
      // ---- PROPERTY 4: the winner is the lowest asserted index ----
      if (nlow >= 3'd1)
        ck(spi_sel_idx === efirst,
           "spi did not resolve the tie to the lowest asserted index");

      @(posedge clk); #1;

      // ---- PROPERTY 5: exactly one counter moved ----
      if (nlow == 3'd0) begin
        ck(spi_n_idle == i0 + 32'd1 && spi_n_sel == s0 && spi_n_contend == c0,
           "spi counters wrong for an idle bus");
      end else if (nlow == 3'd1) begin
        ck(spi_n_sel == s0 + 32'd1 && spi_n_idle == i0 && spi_n_contend == c0,
           "spi counters wrong for a clean selection");
      end else begin
        ck(spi_n_contend == c0 + 32'd1 && spi_n_sel == s0 && spi_n_idle == i0,
           "spi counters wrong for a contention");
      end
      steps = steps + 1;
    end
  endtask

  // ---------------------------------------------------------------
  //  Write one assignment slot, mirroring it into the bench's table.
  //  The mirror happens AFTER the edge on purpose -- see phase 5.
  // ---------------------------------------------------------------
  task do_cfg(input [1:0] slot, input asg, input [6:0] a, input [15:0] epm);
    begin
      cfg_valid = 1'b1; cfg_slot = slot; cfg_assigned = asg;
      cfg_addr = a; cfg_ep_mask = epm;
      @(posedge clk); #1;
      cfg_valid = 1'b0;
      s_asg[slot]  = asg;
      s_addr[slot] = a;
      s_epm[slot]  = epm;
    end
  endtask

  // ---------------------------------------------------------------
  //  Present one token and check the resolution.
  // ---------------------------------------------------------------
  task usb_step(input [6:0] a, input [3:0] e);
    reg [2:0]  nmatch, ereason;
    reg [1:0]  efirst;
    reg        eep, eany;
    reg [31:0] m0, na0, ne0, cf0;
    begin
      nmatch  = usb_addr_count(a);
      efirst  = usb_first_slot(a);
      eany    = (nmatch > 3'd0);
      eep     = usb_ep_ok(a, e);
      ereason = !eany ? R_NO_ADDR : (!eep ? R_NO_EP : R_MATCH);

      m0 = usb_n_match; na0 = usb_n_no_addr;
      ne0 = usb_n_no_ep; cf0 = usb_n_conflict;

      req_valid = 1'b1; req_addr = a; req_ep = e;
      #1;

      // ---- PROPERTY 6: selection means addressed AND configured ----
      ck(usb_sel_valid === (eany && eep),
         "usb sel_valid does not mean the address matched and the endpoint was configured");
      // ---- PROPERTY 7: the reason code is exact ----
      ck(usb_sel_reason === ereason, "usb sel_reason disagrees with the model");
      // ---- PROPERTY 8: the winner is the lowest matching slot ----
      if (eany)
        ck(usb_sel_slot === efirst,
           "usb did not resolve a duplicate address to the lowest slot");
      // ---- PROPERTY 9: a duplicate address is DETECTED ----
      //
      // The property the SPI decoder has no equivalent of, and the
      // reason is structural rather than a matter of effort: the address
      // is a field every slot compares against, so the count is
      // available on the spot. A chip select is a wire only one slave
      // ever receives.
      ck(usb_conflict === (nmatch > 3'd1),
         "usb conflict does not mean two assigned slots share the address");

      // The measurement, taken on EVERY token rather than on a directed
      // handful. The denominator is therefore every duplicate-address
      // token the exhaustive sweep produces, which makes it comparable
      // in weight to the SPI enumeration in phase 3.
      if (nmatch > 3'd1) begin
        usb_conflict_cases = usb_conflict_cases + 1;
        if (usb_conflict === 1'b1)
          usb_conflict_visible = usb_conflict_visible + 1;
      end

      @(posedge clk); #1;
      req_valid = 1'b0;

      // ---- PROPERTY 10: exactly one reason counter moved ----
      case (ereason)
        R_MATCH:   ck(usb_n_match == m0 + 32'd1 && usb_n_no_addr == na0 && usb_n_no_ep == ne0,
                      "usb counters wrong for a match");
        R_NO_ADDR: ck(usb_n_no_addr == na0 + 32'd1 && usb_n_match == m0 && usb_n_no_ep == ne0,
                      "usb counters wrong for an unknown address");
        R_NO_EP:   ck(usb_n_no_ep == ne0 + 32'd1 && usb_n_match == m0 && usb_n_no_addr == na0,
                      "usb counters wrong for an unconfigured endpoint");
        default: ;
      endcase
      ck(usb_n_conflict == cf0 + ((nmatch > 3'd1) ? 32'd1 : 32'd0),
         "usb conflict counter disagrees with the model");
      steps = steps + 1;
    end
  endtask

  task reset_dut;
    integer j;
    begin
      rst_n = 1'b0;
      cs_n = {N_DEV{1'b1}};
      cfg_valid = 1'b0; req_valid = 1'b0;
      @(posedge clk); @(posedge clk);
      rst_n = 1'b1;
      @(posedge clk); #1;
      for (j = 0; j < N_SLOT; j = j + 1) begin
        s_asg[j] = 1'b0; s_addr[j] = 7'd0; s_epm[j] = 16'd0;
      end
    end
  endtask

  // ---- exhaustive reach ----
  //
  // SPI: 2**N_DEV chip-select patterns = 16.
  // USB: asg_mask(16) x addr_mode(4) x ep_mode(2) x req_addr(5) x req_ep(4)
  //      = 2560. Every dimension is an independent input with no
  //      forbidden combinations, so the denominator is exactly 2560 and a
  //      sweep reporting less is broken rather than constrained.
  reg reach_spi [0:15];
  reg reach_usb [0:2559];
  integer nrs, nru, ri;


  integer pat, am, em, ra, re, asg, j2, k;
  reg [6:0]  addr_of;
  reg [15:0] epm_of;
  reg        any_slave_knows, view_here, view_clean;

  initial begin
    for (ri = 0; ri < 16;   ri = ri + 1) reach_spi[ri] = 1'b0;
    for (ri = 0; ri < 2560; ri = ri + 1) reach_usb[ri] = 1'b0;
    seed = 32'd28002;

    reset_dut;

    // =============================================================
    //  PHASE 1 (DIRECTED, EXHAUSTIVE) -- every chip-select pattern.
    //  16 of 16, nothing unreachable.
    // =============================================================
    for (pat = 0; pat < 16; pat = pat + 1) begin
      spi_step(pat[N_DEV-1:0]);
      reach_spi[pat] = 1'b1;
    end
    cs_n = {N_DEV{1'b1}};

    // =============================================================
    //  PHASE 2 (DIRECTED, EXHAUSTIVE) -- the USB selection space.
    //
    //  The table is reconfigured once per (asg_mask, addr_mode,
    //  ep_mode) and then every (req_addr, req_ep) is presented against
    //  it: 128 configurations x 20 tokens.
    //
    //  addr_mode exists so duplicate addresses are REACHED rather than
    //  assumed away. Mode 1 gives every slot the same address, mode 3
    //  gives pairwise duplicates, mode 2 puts everything at the default
    //  address. A sweep with distinct addresses only would leave the
    //  conflict logic at zero coverage while reporting completeness.
    // =============================================================
    for (asg = 0; asg < 16; asg = asg + 1)
    for (am = 0; am < 4; am = am + 1)
    for (em = 0; em < 2; em = em + 1) begin
      for (j2 = 0; j2 < N_SLOT; j2 = j2 + 1) begin
        case (am)
          0: addr_of = j2[6:0] + 7'd1;                 // all distinct
          1: addr_of = 7'd1;                           // all identical
          2: addr_of = 7'd0;                           // all at default
          default: addr_of = (j2[6:0] % 7'd2) + 7'd1;  // pairwise duplicates
        endcase
        // ep_mode 1 leaves endpoints 1 and 3 unconfigured, so R_NO_EP is
        // reachable at a NON-zero address and not only through the
        // default-address rule.
        epm_of = (em == 0) ? 16'h000F : 16'h0005;
        do_cfg(j2[1:0], asg[j2], addr_of, epm_of);
      end
      for (ra = 0; ra < 5; ra = ra + 1)
      for (re = 0; re < 4; re = re + 1) begin
        usb_step(ra[6:0], re[3:0]);
        ri = ((((asg * 4 + am) * 2 + em) * 5 + ra) * 4 + re);
        reach_usb[ri] = 1'b1;
      end
    end

    // =============================================================
    //  PHASE 3 (DIRECTED, EXHAUSTIVE) -- THE MEASUREMENT.
    //
    //  Not a pass/fail. For every failure each discipline admits, could
    //  a real device detect it from what that device actually receives?
    //
    //  An SPI slave receives ONE chip-select pin, so slave k's entire
    //  view of the world is cs_n[k]. The test is a DISTINGUISHABILITY
    //  test, constructed rather than asserted: compare slave k's view
    //  under the contention pattern against its view under the clean
    //  pattern in which only k is selected. If the two views are equal,
    //  no logic inside slave k can tell them apart, whatever it does.
    // =============================================================
    for (pat = 0; pat < 16; pat = pat + 1) begin
      if (spi_popcount(pat[N_DEV-1:0]) > 3'd1) begin
        spi_contend_patterns = spi_contend_patterns + 1;

        any_slave_knows = 1'b0;
        for (k = 0; k < N_DEV; k = k + 1) begin
          if (!pat[k]) begin
            // what slave k sees now
            view_here  = pat[k];
            // what slave k would see if it alone were selected: its own
            // line low. Every other line is invisible to it.
            view_clean = 1'b0;
            if (view_here !== view_clean) any_slave_knows = 1'b1;
          end
        end
        if (any_slave_knows) spi_contend_visible = spi_contend_visible + 1;

        // And confirm the BUS MONITOR does see it -- the detector exists
        // only because this module was handed all N lines, which no slave
        // on a real board is.
        cs_n = pat[N_DEV-1:0]; #1;
        ck(spi_contention === 1'b1,
           "the bus monitor failed to see a contention it was given every wire for");
        @(posedge clk); #1;
      end
    end
    cs_n = {N_DEV{1'b1}};

    // The USB side of this question is measured inside usb_step, on
    // every one of the 2560 exhaustive tokens, so there is nothing to
    // add here -- the numbers are reported at the end of the run.

    // =============================================================
    //  PHASE 4 (DIRECTED) -- RE-ADDRESSING AT RUN TIME.
    //
    //  The capability SPI does not have and cannot be given without
    //  changing the board. Assign a slot, verify it answers, re-assign
    //  it, verify the OLD address has gone silent and the new one
    //  answers, then unassign it entirely.
    //
    //  The absence of a comparable phase on the SPI side is not an
    //  omission in this bench. spi_cs_select HAS NO CONFIGURATION PORT
    //  -- its mapping is fixed at elaboration -- and that missing port
    //  is the finding.
    // =============================================================
    reset_dut;
    do_cfg(2'd0, 1'b1, 7'd5, 16'h000F);
    usb_step(7'd5, 4'd0);                 // answers at 5
    do_cfg(2'd0, 1'b1, 7'd9, 16'h000F);   // re-addressed to 9
    usb_step(7'd5, 4'd0);                 // must now be silent at 5
    usb_step(7'd9, 4'd0);                 // and answer at 9
    do_cfg(2'd0, 1'b0, 7'd9, 16'h000F);   // unassigned entirely
    usb_step(7'd9, 4'd0);                 // silent again
    // An unassigned slot must not answer even at the default address, or
    // it would respond during another device's enumeration.
    usb_step(7'd0, 4'd0);

    // =============================================================
    //  PHASE 5 (DIRECTED) -- WHEN A CONFIGURATION TAKES EFFECT.
    //
    //  The design registers the table, so a token presented in the SAME
    //  cycle as a write must see the OLD table. Getting this backwards
    //  is a classic shadow-model error, and it earns an explicit phase:
    //  do_cfg mirrors the write only AFTER the edge, so a design that
    //  applied it early would disagree here and nowhere else in the run.
    // =============================================================
    reset_dut;
    do_cfg(2'd1, 1'b1, 7'd7, 16'h000F);
    // A same-cycle write moving the slot to a different address, plus a
    // token at the OLD one: the token must still match.
    cfg_valid = 1'b1; cfg_slot = 2'd1; cfg_assigned = 1'b1;
    cfg_addr = 7'd8; cfg_ep_mask = 16'h000F;
    req_valid = 1'b1; req_addr = 7'd7; req_ep = 4'd0;
    #1;
    ck(usb_sel_valid === 1'b1,
       "a configuration write took effect in the same cycle as the token");
    ck(usb_sel_reason === R_MATCH,
       "same-cycle reason code should still be MATCH at the old address");
    @(posedge clk); #1;
    cfg_valid = 1'b0; req_valid = 1'b0;
    s_asg[1] = 1'b1; s_addr[1] = 7'd8; s_epm[1] = 16'h000F;
    // and from the next cycle the new address is the live one
    usb_step(7'd8, 4'd0);
    usb_step(7'd7, 4'd0);

    // =============================================================
    //  PHASE 6 (RANDOM)
    // =============================================================
`ifndef DIRECTED_ONLY
    reset_dut;
    for (k = 0; k < 400; k = k + 1) begin
      if ((urand(0) % 3) == 0)
        do_cfg((urand(0) % 4), (urand(0) % 4) != 0,
               (urand(0) % 6), (urand(0) % 2) ? 16'h000F : 16'h0005);
      usb_step((urand(0) % 6), (urand(0) % 4));
      spi_step((urand(0) % 16));
    end
`endif

    nrs = 0; for (ri = 0; ri < 16;   ri = ri + 1) if (reach_spi[ri]) nrs = nrs + 1;
    nru = 0; for (ri = 0; ri < 2560; ri = ri + 1) if (reach_usb[ri]) nru = nru + 1;

    $display("steps=%0d checks=%0d reach_spi=%0d/16 reach_usb=%0d/2560 errors=%0d",
             steps, checks, nrs, nru, errors);
    $display("[spi] selections=%0d idle=%0d CONTENTIONS=%0d",
             spi_n_sel, spi_n_idle, spi_n_contend);
    $display("[usb] matches=%0d no_addr=%0d no_ep=%0d CONFLICTS=%0d",
             usb_n_match, usb_n_no_addr, usb_n_no_ep, usb_n_conflict);
    $display("--- detectability from the vantage point a real device occupies ---");
    $display("[spi] contention patterns=%0d  distinguishable by any slave=%0d",
             spi_contend_patterns, spi_contend_visible);
    $display("[usb] duplicate-address tokens=%0d  detected by the device=%0d",
             usb_conflict_cases, usb_conflict_visible);
    if (nrs != 16 || nru != 2560) 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

10. SystemVerilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  TWO SELECTION DISCIPLINES, SIDE BY SIDE -- SystemVerilog.
//
//  CLASSIFICATION: simplified synthesisable teaching RTL.
//  Same hardware contract as the Verilog file: same ports, same widths,
//  same reset values, same cycle-by-cycle behaviour. What changes is
//  that the reason code becomes a named type, so a reason that does not
//  exist cannot be produced.
//
//  The question both modules answer:
//
//      "Which peripheral should respond to this transaction?"
//
//  SPI answers it with a WIRE routed at layout time. USB answers it with
//  a FIELD every device sees, holding an address the HOST assigned.
//
//  The difference that matters is not the wire count:
//
//      SPI's selection information is DISTRIBUTED -- each slave sees
//      only its own select line. USB's is BROADCAST -- every device
//      sees the whole address field.
//
//  A distributed selector cannot detect its own worst failure.
// =====================================================================

// ---------------------------------------------------------------------
//  spi_cs_select -- selection by wire.
//
//  IMPORTANT: this module is given ALL N chip-select lines. No real SPI
//  slave has that. A real slave receives exactly one CS pin and cannot
//  know whether another slave is also selected. So `contention` is a
//  BUS MONITOR output, observable only from a vantage point that no SPI
//  device on a real board occupies.
//
//  That is not a limitation of the model. It is the finding.
// ---------------------------------------------------------------------
module spi_cs_select #(
  parameter int N_DEV = 4
) (
  input  logic                     clk,
  input  logic                     rst_n,

  // Active-low chip selects, one per device, routed on the board.
  input  logic [N_DEV-1:0]         cs_n,

  output logic                     sel_valid,
  output logic [$clog2(N_DEV)-1:0] sel_idx,

  // The failure a real slave cannot see.
  output logic                     contention,

  output logic [31:0]              n_sel,
  output logic [31:0]              n_idle,
  output logic [31:0]              n_contend
);

  localparam int IW = $clog2(N_DEV);

  // How many selects are asserted. On a correct board 0 or 1; two at
  // once is a firmware or routing fault, and on real hardware it means
  // two slaves drive MISO simultaneously.
  logic [IW:0]   n_low;
  logic [IW-1:0] first_low;

  always_comb begin
    n_low     = '0;
    first_low = '0;
    // Downwards, so the LOWEST asserted index wins the tie. The
    // direction is arbitrary but it must be DEFINED: an undefined winner
    // makes the contention case unverifiable.
    for (int i = N_DEV - 1; i >= 0; i--) begin
      if (!cs_n[i]) begin
        n_low     = n_low + 1'b1;
        first_low = IW'(i);
      end
    end
  end

  assign contention = (n_low > 1);
  assign sel_valid  = (n_low == 1);
  assign sel_idx    = first_low;

  logic [31:0] sel_c, idle_c, con_c;
  assign n_sel     = sel_c;
  assign n_idle    = idle_c;
  assign n_contend = con_c;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      sel_c  <= 32'd0;
      idle_c <= 32'd0;
      con_c  <= 32'd0;
    end else begin
      if (n_low == 0)      idle_c <= idle_c + 32'd1;
      else if (n_low == 1) sel_c  <= sel_c  + 32'd1;
      else                 con_c  <= con_c  + 32'd1;
    end
  end

endmodule


// ---------------------------------------------------------------------
//  usb_addr_select -- selection by assigned name.
//
//  Two real USB rules are enforced, because both are places an
//  implementation can be plausibly wrong:
//
//    * Address 0 is the DEFAULT address. A device there is
//      mid-enumeration and has endpoint 0 only. Honouring any other
//      endpoint would let a half-enumerated device answer traffic it has
//      no configuration for.
//
//    * An UNASSIGNED slot matches nothing, including address 0. A slot
//      that answered before being assigned would respond during another
//      device's enumeration.
//
//  Unlike the SPI decoder, this one CAN see its own worst failure: two
//  slots holding the same address is directly detectable, because the
//  address is a field every slot compares against.
// ---------------------------------------------------------------------
module usb_addr_select #(
  parameter int N_SLOT = 4
) (
  input  logic                      clk,
  input  logic                      rst_n,

  // ---- the assignment interface: this is the whole difference ----
  //
  // spi_cs_select has no equivalent port. Its mapping is fixed at
  // elaboration. This one's mapping is a register file, writable at run
  // time, which is what "the host assigns an address" means in hardware.
  input  logic                      cfg_valid,
  input  logic [$clog2(N_SLOT)-1:0] cfg_slot,
  input  logic                      cfg_assigned,
  input  logic [6:0]                cfg_addr,
  input  logic [15:0]               cfg_ep_mask,

  input  logic                      req_valid,
  input  logic [6:0]                req_addr,
  input  logic [3:0]                req_ep,

  output logic                      sel_valid,
  output logic [$clog2(N_SLOT)-1:0] sel_slot,
  output logic [2:0]                sel_reason,

  // The failure this discipline CAN see.
  output logic                      conflict,

  output logic [31:0]               n_match,
  output logic [31:0]               n_no_addr,
  output logic [31:0]               n_no_ep,
  output logic [31:0]               n_conflict
);

  localparam int SW = $clog2(N_SLOT);

  // A named type, exported as 3 bits so all three languages present one
  // identical observable contract.
  typedef enum logic [2:0] {
    R_NONE    = 3'd0,   // no token this cycle
    R_MATCH   = 3'd1,   // addressed and configured
    R_NO_ADDR = 3'd2,   // nobody holds that address
    R_NO_EP   = 3'd3    // address matched, endpoint did not
  } reason_e;

  // Per-field arrays rather than an array of structs: a variable
  // field-select into an unpacked array of packed structs aborts the
  // Icarus elaborator.
  logic        sl_asg  [N_SLOT];
  logic [6:0]  sl_addr [N_SLOT];
  logic [15:0] sl_epm  [N_SLOT];

  logic          m_any, m_ep_any;
  logic [SW-1:0] m_slot;
  logic [SW:0]   m_addr_count;
  reason_e       reason_q;

  always_comb begin
    m_any        = 1'b0;
    m_ep_any     = 1'b0;
    m_slot       = '0;
    m_addr_count = '0;
    // Downwards, so the LOWEST matching slot wins. Defined, not
    // arbitrary: the conflict case must have a predictable winner or it
    // cannot be checked.
    for (int j = N_SLOT - 1; j >= 0; j--) begin
      if (sl_asg[j] && (sl_addr[j] == req_addr)) begin
        m_addr_count = m_addr_count + 1'b1;
        m_any        = 1'b1;
        m_slot       = SW'(j);
        // Address 0 is the default address: endpoint 0 only. A device
        // there has no configuration yet, so honouring any other
        // endpoint would answer traffic it cannot service.
        if (sl_addr[j] == 7'd0) begin
          if (req_ep == 4'd0) m_ep_any = 1'b1;
        end else begin
          if (sl_epm[j][req_ep]) m_ep_any = 1'b1;
        end
      end
    end
  end

  always_comb begin
    if      (!req_valid) reason_q = R_NONE;
    else if (!m_any)     reason_q = R_NO_ADDR;
    else if (!m_ep_any)  reason_q = R_NO_EP;
    else                 reason_q = R_MATCH;
  end

  assign conflict   = (m_addr_count > 1);
  assign sel_valid  = req_valid && m_any && m_ep_any;
  assign sel_slot   = m_slot;
  assign sel_reason = reason_q;

  logic [31:0] match_c, noaddr_c, noep_c, conf_c;
  assign n_match    = match_c;
  assign n_no_addr  = noaddr_c;
  assign n_no_ep    = noep_c;
  assign n_conflict = conf_c;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int j = 0; j < N_SLOT; j++) begin
        sl_asg[j]  <= 1'b0;
        sl_addr[j] <= 7'd0;
        sl_epm[j]  <= 16'd0;
      end
      match_c  <= 32'd0;
      noaddr_c <= 32'd0;
      noep_c   <= 32'd0;
      conf_c   <= 32'd0;
    end else begin
      if (cfg_valid) begin
        sl_asg[cfg_slot]  <= cfg_assigned;
        sl_addr[cfg_slot] <= cfg_addr;
        sl_epm[cfg_slot]  <= cfg_ep_mask;
      end

      if (req_valid) begin
        case (reason_q)
          R_MATCH:   match_c  <= match_c  + 32'd1;
          R_NO_ADDR: noaddr_c <= noaddr_c + 32'd1;
          R_NO_EP:   noep_c   <= noep_c   + 32'd1;
          default:   ;
        endcase
        if (conflict) conf_c <= conf_c + 32'd1;
      end
    end
  end

endmodule

The reason code becomes a named type, so a reason that does not exist cannot be produced. It is still exported as three bits, so all three languages present one identical observable contract to their benches.

The SystemVerilog testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  Testbench for spi_cs_select and usb_addr_select -- SystemVerilog.
//
//  BOTH DISCIPLINES, ONE BENCH, ONE STIMULUS SOURCE. The chapter's point
//  is a comparison, so the comparison happens inside the verification
//  rather than in prose afterwards.
//
//  THE SHADOW MODELS ARE FORMULATED IN THE OPPOSITE DIRECTION. Both
//  designs resolve ties by walking their index space DOWNWARDS so the
//  lowest index wins; both models walk UPWARDS and stop at the first
//  hit. Same answer, different derivation -- the only way a model can
//  fail to inherit the design's mistake.
//
//  SAME SEED AND SAME PHASE ORDER AS THE VERILOG BENCH, deliberately.
//  Icarus seeds $random identically, so both drive identical stimulus and
//  any difference between the two mutation columns is a real difference
//  between the two DESIGNS. The independent-stimulus role is VHDL's.
// =====================================================================
`timescale 1ns/1ps
module tb_sl_sv;

  localparam int N_DEV  = 4;
  localparam int N_SLOT = 4;

  logic clk = 1'b0, rst_n = 1'b0;
  always #5 clk = ~clk;

  // ---- SPI side ----
  logic [N_DEV-1:0] cs_n = '1;
  logic             spi_sel_valid, spi_contention;
  logic [1:0]       spi_sel_idx;
  logic [31:0]      spi_n_sel, spi_n_idle, spi_n_contend;

  spi_cs_select #(.N_DEV(N_DEV)) dut_spi (
    .clk(clk), .rst_n(rst_n), .cs_n(cs_n),
    .sel_valid(spi_sel_valid), .sel_idx(spi_sel_idx),
    .contention(spi_contention),
    .n_sel(spi_n_sel), .n_idle(spi_n_idle), .n_contend(spi_n_contend)
  );

  // ---- USB side ----
  logic        cfg_valid = 1'b0;
  logic [1:0]  cfg_slot = 2'd0;
  logic        cfg_assigned = 1'b0;
  logic [6:0]  cfg_addr = 7'd0;
  logic [15:0] cfg_ep_mask = 16'd0;
  logic        req_valid = 1'b0;
  logic [6:0]  req_addr = 7'd0;
  logic [3:0]  req_ep = 4'd0;

  logic        usb_sel_valid, usb_conflict;
  logic [1:0]  usb_sel_slot;
  logic [2:0]  usb_sel_reason;
  logic [31:0] usb_n_match, usb_n_no_addr, usb_n_no_ep, usb_n_conflict;

  usb_addr_select #(.N_SLOT(N_SLOT)) dut_usb (
    .clk(clk), .rst_n(rst_n),
    .cfg_valid(cfg_valid), .cfg_slot(cfg_slot),
    .cfg_assigned(cfg_assigned), .cfg_addr(cfg_addr),
    .cfg_ep_mask(cfg_ep_mask),
    .req_valid(req_valid), .req_addr(req_addr), .req_ep(req_ep),
    .sel_valid(usb_sel_valid), .sel_slot(usb_sel_slot),
    .sel_reason(usb_sel_reason), .conflict(usb_conflict),
    .n_match(usb_n_match), .n_no_addr(usb_n_no_addr),
    .n_no_ep(usb_n_no_ep), .n_conflict(usb_n_conflict)
  );

  localparam logic [2:0] R_NONE = 3'd0, R_MATCH = 3'd1,
                         R_NO_ADDR = 3'd2, R_NO_EP = 3'd3;

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

  // ---- the headline measurement ----
  int spi_contend_patterns = 0;  // patterns with >= 2 selects asserted
  int spi_contend_visible  = 0;  // ... distinguishable by SOME slave
  int usb_conflict_cases   = 0;  // tokens hitting a duplicate address
  int usb_conflict_visible = 0;  // ... detected by the device logic

  // $random is SIGNED: mask the sign bit before any modulo, or every
  // `% N` is negative about half the time and the sweep silently
  // collapses onto a subset of its range.
  function automatic logic [31:0] urand();
    return $random(seed) & 32'h3FFF_FFFF;
  endfunction

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

  // ---- the shadow copy of the assignment table ----
  //
  // Maintained by the BENCH from the writes it issued, never read back
  // from the design. A model that reads the design's own table cannot
  // detect a design that writes the wrong entry.
  logic        s_asg  [N_SLOT];
  logic [6:0]  s_addr [N_SLOT];
  logic [15:0] s_epm  [N_SLOT];

  // ---- SPI model: counts upward, unlike the design's downward walk ----
  function automatic logic [2:0] spi_popcount(input logic [N_DEV-1:0] p);
    logic [2:0] c = 3'd0;
    for (int i = 0; i < N_DEV; i++) if (!p[i]) c = c + 3'd1;
    return c;
  endfunction

  function automatic logic [1:0] spi_first_low(input logic [N_DEV-1:0] p);
    logic found = 1'b0;
    logic [1:0] r = 2'd0;
    // upward, stop at the first. The design walks downward and
    // overwrites, so the two agree only if both are right.
    for (int i = 0; i < N_DEV; i++)
      if (!p[i] && !found) begin r = 2'(i); found = 1'b1; end
    return r;
  endfunction

  // ---- USB model: also upward-and-stop, over the bench's own table ----
  function automatic logic [2:0] usb_addr_count(input logic [6:0] a);
    logic [2:0] c = 3'd0;
    for (int j = 0; j < N_SLOT; j++)
      if (s_asg[j] && (s_addr[j] == a)) c = c + 3'd1;
    return c;
  endfunction

  function automatic logic [1:0] usb_first_slot(input logic [6:0] a);
    logic found = 1'b0;
    logic [1:0] r = 2'd0;
    for (int j = 0; j < N_SLOT; j++)
      if (s_asg[j] && (s_addr[j] == a) && !found) begin r = 2'(j); found = 1'b1; end
    return r;
  endfunction

  // Does ANY assigned slot at address `a` expose endpoint `e`?
  //
  // The default-address rule lives here, stated independently of the
  // design: a slot at address 0 is mid-enumeration and has endpoint 0
  // only, whatever its ep_mask happens to contain.
  function automatic logic usb_ep_ok(input logic [6:0] a, input logic [3:0] e);
    logic ok = 1'b0;
    for (int j = 0; j < N_SLOT; j++) begin
      if (s_asg[j] && (s_addr[j] == a)) begin
        if (s_addr[j] == 7'd0) begin
          if (e == 4'd0) ok = 1'b1;
        end else begin
          if (s_epm[j][e]) ok = 1'b1;
        end
      end
    end
    return ok;
  endfunction

  // ---------------------------------------------------------------
  //  Drive one SPI pattern and check the resolution.
  // ---------------------------------------------------------------
  task automatic spi_step(input logic [N_DEV-1:0] p);
    logic [2:0]  nlow;
    logic [1:0]  efirst;
    logic [31:0] s0, i0, c0;
    begin
      nlow   = spi_popcount(p);
      efirst = spi_first_low(p);
      s0 = spi_n_sel; i0 = spi_n_idle; c0 = spi_n_contend;

      cs_n = p;
      #1;

      // ---- PROPERTY 1: exactly one low is a selection ----
      ck(spi_sel_valid === (nlow == 3'd1),
         "spi sel_valid does not mean exactly one chip select is asserted");
      // ---- PROPERTY 2: two or more low is contention ----
      ck(spi_contention === (nlow > 3'd1),
         "spi contention does not mean two or more chip selects are asserted");
      // ---- PROPERTY 3: a selection and a contention are exclusive ----
      ck(!(spi_sel_valid && spi_contention),
         "spi reported a selection and a contention at the same time");
      // ---- PROPERTY 4: the winner is the lowest asserted index ----
      if (nlow >= 3'd1)
        ck(spi_sel_idx === efirst,
           "spi did not resolve the tie to the lowest asserted index");

      @(posedge clk); #1;

      // ---- PROPERTY 5: exactly one counter moved ----
      if (nlow == 3'd0) begin
        ck(spi_n_idle == i0 + 32'd1 && spi_n_sel == s0 && spi_n_contend == c0,
           "spi counters wrong for an idle bus");
      end else if (nlow == 3'd1) begin
        ck(spi_n_sel == s0 + 32'd1 && spi_n_idle == i0 && spi_n_contend == c0,
           "spi counters wrong for a clean selection");
      end else begin
        ck(spi_n_contend == c0 + 32'd1 && spi_n_sel == s0 && spi_n_idle == i0,
           "spi counters wrong for a contention");
      end
      steps++;
    end
  endtask

  // ---------------------------------------------------------------
  //  Write one assignment slot, mirroring it into the bench's table.
  //  The mirror happens AFTER the edge on purpose -- see phase 5.
  // ---------------------------------------------------------------
  task automatic do_cfg(input logic [1:0] slot, input logic asg,
                        input logic [6:0] a, input logic [15:0] epm);
    cfg_valid = 1'b1; cfg_slot = slot; cfg_assigned = asg;
    cfg_addr = a; cfg_ep_mask = epm;
    @(posedge clk); #1;
    cfg_valid = 1'b0;
    s_asg[slot]  = asg;
    s_addr[slot] = a;
    s_epm[slot]  = epm;
  endtask

  // ---------------------------------------------------------------
  //  Present one token and check the resolution.
  // ---------------------------------------------------------------
  task automatic usb_step(input logic [6:0] a, input logic [3:0] e);
    logic [2:0]  nmatch, ereason;
    logic [1:0]  efirst;
    logic        eep, eany;
    logic [31:0] m0, na0, ne0, cf0;
    begin
      nmatch  = usb_addr_count(a);
      efirst  = usb_first_slot(a);
      eany    = (nmatch > 3'd0);
      eep     = usb_ep_ok(a, e);
      ereason = !eany ? R_NO_ADDR : (!eep ? R_NO_EP : R_MATCH);

      m0 = usb_n_match; na0 = usb_n_no_addr;
      ne0 = usb_n_no_ep; cf0 = usb_n_conflict;

      req_valid = 1'b1; req_addr = a; req_ep = e;
      #1;

      // ---- PROPERTY 6: selection means addressed AND configured ----
      ck(usb_sel_valid === (eany && eep),
         "usb sel_valid does not mean the address matched and the endpoint was configured");
      // ---- PROPERTY 7: the reason code is exact ----
      ck(usb_sel_reason === ereason, "usb sel_reason disagrees with the model");
      // ---- PROPERTY 8: the winner is the lowest matching slot ----
      if (eany)
        ck(usb_sel_slot === efirst,
           "usb did not resolve a duplicate address to the lowest slot");
      // ---- PROPERTY 9: a duplicate address is DETECTED ----
      //
      // The property the SPI decoder has no equivalent of, and the
      // reason is structural rather than a matter of effort: the address
      // is a field every slot compares against, so the count is
      // available on the spot. A chip select is a wire only one slave
      // ever receives.
      ck(usb_conflict === (nmatch > 3'd1),
         "usb conflict does not mean two assigned slots share the address");

      // The measurement, taken on EVERY token rather than on a directed
      // handful, so its denominator is every duplicate-address token the
      // exhaustive sweep produces.
      if (nmatch > 3'd1) begin
        usb_conflict_cases++;
        if (usb_conflict === 1'b1) usb_conflict_visible++;
      end

      @(posedge clk); #1;
      req_valid = 1'b0;

      // ---- PROPERTY 10: exactly one reason counter moved ----
      case (ereason)
        R_MATCH:   ck(usb_n_match == m0 + 32'd1 && usb_n_no_addr == na0 && usb_n_no_ep == ne0,
                      "usb counters wrong for a match");
        R_NO_ADDR: ck(usb_n_no_addr == na0 + 32'd1 && usb_n_match == m0 && usb_n_no_ep == ne0,
                      "usb counters wrong for an unknown address");
        R_NO_EP:   ck(usb_n_no_ep == ne0 + 32'd1 && usb_n_match == m0 && usb_n_no_addr == na0,
                      "usb counters wrong for an unconfigured endpoint");
        default: ;
      endcase
      ck(usb_n_conflict == cf0 + ((nmatch > 3'd1) ? 32'd1 : 32'd0),
         "usb conflict counter disagrees with the model");
      steps++;
    end
  endtask

  task automatic reset_dut();
    rst_n = 1'b0;
    cs_n = '1;
    cfg_valid = 1'b0; req_valid = 1'b0;
    @(posedge clk); @(posedge clk);
    rst_n = 1'b1;
    @(posedge clk); #1;
    for (int j = 0; j < N_SLOT; j++) begin
      s_asg[j] = 1'b0; s_addr[j] = 7'd0; s_epm[j] = 16'd0;
    end
  endtask

  // ---- exhaustive reach ----
  //
  // SPI: 2**N_DEV chip-select patterns = 16.
  // USB: asg_mask(16) x addr_mode(4) x ep_mode(2) x req_addr(5) x req_ep(4)
  //      = 2560. Every dimension is an independent input with no
  //      forbidden combinations, so the denominator is exactly 2560.
  bit reach_spi [0:15];
  bit reach_usb [0:2559];
  int nrs, nru, ri;

  int pat, am, em, ra, re, asg, j2, k;
  logic [6:0]  addr_of;
  logic [15:0] epm_of;
  logic        any_slave_knows, view_here, view_clean;

  initial begin
    for (ri = 0; ri < 16;   ri++) reach_spi[ri] = 1'b0;
    for (ri = 0; ri < 2560; ri++) reach_usb[ri] = 1'b0;
    seed = 32'd28002;

    reset_dut();

    // =============================================================
    //  PHASE 1 (DIRECTED, EXHAUSTIVE) -- every chip-select pattern.
    //  16 of 16, nothing unreachable.
    // =============================================================
    for (pat = 0; pat < 16; pat++) begin
      spi_step(pat[N_DEV-1:0]);
      reach_spi[pat] = 1'b1;
    end
    cs_n = '1;

    // =============================================================
    //  PHASE 2 (DIRECTED, EXHAUSTIVE) -- the USB selection space.
    //
    //  The table is reconfigured once per (asg_mask, addr_mode, ep_mode)
    //  and every (req_addr, req_ep) is presented against it: 128
    //  configurations x 20 tokens.
    //
    //  addr_mode exists so duplicate addresses are REACHED rather than
    //  assumed away. A sweep with distinct addresses only would leave the
    //  conflict logic at zero coverage while reporting completeness.
    // =============================================================
    for (asg = 0; asg < 16; asg++)
    for (am = 0; am < 4; am++)
    for (em = 0; em < 2; em++) begin
      for (j2 = 0; j2 < N_SLOT; j2++) begin
        case (am)
          0: addr_of = 7'(j2) + 7'd1;               // all distinct
          1: addr_of = 7'd1;                        // all identical
          2: addr_of = 7'd0;                        // all at default
          default: addr_of = (7'(j2) % 7'd2) + 7'd1; // pairwise duplicates
        endcase
        // ep_mode 1 leaves endpoints 1 and 3 unconfigured, so R_NO_EP is
        // reachable at a NON-zero address and not only through the
        // default-address rule.
        epm_of = (em == 0) ? 16'h000F : 16'h0005;
        do_cfg(2'(j2), asg[j2], addr_of, epm_of);
      end
      for (ra = 0; ra < 5; ra++)
      for (re = 0; re < 4; re++) begin
        usb_step(7'(ra), 4'(re));
        ri = ((((asg * 4 + am) * 2 + em) * 5 + ra) * 4 + re);
        reach_usb[ri] = 1'b1;
      end
    end

    // =============================================================
    //  PHASE 3 (DIRECTED, EXHAUSTIVE) -- THE MEASUREMENT.
    //
    //  An SPI slave receives ONE chip-select pin, so slave k's entire
    //  view of the world is cs_n[k]. The test is a DISTINGUISHABILITY
    //  test, constructed rather than asserted: compare slave k's view
    //  under the contention pattern against its view under the clean
    //  pattern in which only k is selected. If the two views are equal,
    //  no logic inside slave k can tell them apart, whatever it does.
    // =============================================================
    for (pat = 0; pat < 16; pat++) begin
      if (spi_popcount(pat[N_DEV-1:0]) > 3'd1) begin
        spi_contend_patterns++;

        any_slave_knows = 1'b0;
        for (k = 0; k < N_DEV; k++) begin
          if (!pat[k]) begin
            view_here  = pat[k];   // what slave k sees now
            view_clean = 1'b0;     // what it would see if it alone were selected
            if (view_here !== view_clean) any_slave_knows = 1'b1;
          end
        end
        if (any_slave_knows) spi_contend_visible++;

        // And confirm the BUS MONITOR does see it -- the detector exists
        // only because this module was handed every wire, which no slave
        // on a real board is.
        cs_n = pat[N_DEV-1:0]; #1;
        ck(spi_contention === 1'b1,
           "the bus monitor failed to see a contention it was given every wire for");
        @(posedge clk); #1;
      end
    end
    cs_n = '1;

    // The USB side of this question is measured inside usb_step, on every
    // one of the 2560 exhaustive tokens, so there is nothing to add here.

    // =============================================================
    //  PHASE 4 (DIRECTED) -- RE-ADDRESSING AT RUN TIME.
    //
    //  The capability SPI does not have and cannot be given without
    //  changing the board.
    //
    //  The absence of a comparable phase on the SPI side is not an
    //  omission. spi_cs_select HAS NO CONFIGURATION PORT -- its mapping
    //  is fixed at elaboration -- and that missing port is the finding.
    // =============================================================
    reset_dut();
    do_cfg(2'd0, 1'b1, 7'd5, 16'h000F);
    usb_step(7'd5, 4'd0);                 // answers at 5
    do_cfg(2'd0, 1'b1, 7'd9, 16'h000F);   // re-addressed to 9
    usb_step(7'd5, 4'd0);                 // must now be silent at 5
    usb_step(7'd9, 4'd0);                 // and answer at 9
    do_cfg(2'd0, 1'b0, 7'd9, 16'h000F);   // unassigned entirely
    usb_step(7'd9, 4'd0);                 // silent again
    // An unassigned slot must not answer even at the default address, or
    // it would respond during another device's enumeration.
    usb_step(7'd0, 4'd0);

    // =============================================================
    //  PHASE 5 (DIRECTED) -- WHEN A CONFIGURATION TAKES EFFECT.
    //
    //  The design registers the table, so a token presented in the SAME
    //  cycle as a write must see the OLD table. do_cfg mirrors the write
    //  only AFTER the edge, so a design that applied it early would
    //  disagree here and nowhere else in the run.
    // =============================================================
    reset_dut();
    do_cfg(2'd1, 1'b1, 7'd7, 16'h000F);
    cfg_valid = 1'b1; cfg_slot = 2'd1; cfg_assigned = 1'b1;
    cfg_addr = 7'd8; cfg_ep_mask = 16'h000F;
    req_valid = 1'b1; req_addr = 7'd7; req_ep = 4'd0;
    #1;
    ck(usb_sel_valid === 1'b1,
       "a configuration write took effect in the same cycle as the token");
    ck(usb_sel_reason === R_MATCH,
       "same-cycle reason code should still be MATCH at the old address");
    @(posedge clk); #1;
    cfg_valid = 1'b0; req_valid = 1'b0;
    s_asg[1] = 1'b1; s_addr[1] = 7'd8; s_epm[1] = 16'h000F;
    usb_step(7'd8, 4'd0);
    usb_step(7'd7, 4'd0);

    // =============================================================
    //  PHASE 6 (RANDOM)
    // =============================================================
`ifndef DIRECTED_ONLY
    reset_dut();
    for (k = 0; k < 400; k++) begin
      if ((urand() % 3) == 0)
        do_cfg(2'(urand() % 4), (urand() % 4) != 0,
               7'(urand() % 6), (urand() % 2) ? 16'h000F : 16'h0005);
      usb_step(7'(urand() % 6), 4'(urand() % 4));
      spi_step(4'(urand() % 16));
    end
`endif

    nrs = 0; for (ri = 0; ri < 16;   ri++) if (reach_spi[ri]) nrs++;
    nru = 0; for (ri = 0; ri < 2560; ri++) if (reach_usb[ri]) nru++;

    $display("steps=%0d checks=%0d reach_spi=%0d/16 reach_usb=%0d/2560 errors=%0d",
             steps, checks, nrs, nru, errors);
    $display("[spi] selections=%0d idle=%0d CONTENTIONS=%0d",
             spi_n_sel, spi_n_idle, spi_n_contend);
    $display("[usb] matches=%0d no_addr=%0d no_ep=%0d CONFLICTS=%0d",
             usb_n_match, usb_n_no_addr, usb_n_no_ep, usb_n_conflict);
    $display("--- detectability from the vantage point a real device occupies ---");
    $display("[spi] contention patterns=%0d  distinguishable by any slave=%0d",
             spi_contend_patterns, spi_contend_visible);
    $display("[usb] duplicate-address tokens=%0d  detected by the device=%0d",
             usb_conflict_cases, usb_conflict_visible);
    if (nrs != 16 || nru != 2560) begin
      $display("FAIL: exhaustive sweep incomplete"); errors++;
    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

Same seed and same phase order as the Verilog bench, deliberately — so any difference between the two mutation columns is a real difference between the two designs. The independent-stimulus role belongs to VHDL.

11. VHDL-2008

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- =====================================================================
--  TWO SELECTION DISCIPLINES, SIDE BY SIDE -- VHDL-2008.
--
--  CLASSIFICATION: simplified synthesisable teaching RTL.
--  Same hardware contract as the Verilog and SystemVerilog files: same
--  ports, same widths, same reset values, same cycle-by-cycle behaviour.
--
--  The question both entities answer:
--
--      "Which peripheral should respond to this transaction?"
--
--  SPI answers it with a WIRE routed at layout time. USB answers it with
--  a FIELD every device sees, holding an address the HOST assigned.
--
--  The difference that matters is not the wire count:
--
--      SPI's selection information is DISTRIBUTED -- each slave sees
--      only its own select line. USB's is BROADCAST -- every device
--      sees the whole address field.
--
--  A distributed selector cannot detect its own worst failure.
--
--  Both combinational processes use `process (all)`. That is a
--  deliberate choice for a file that gets mutated: a mutation which adds
--  a branch reading a new signal would otherwise need the sensitivity
--  list extended by hand, and forgetting to do so produces a mutant that
--  fails for the wrong reason.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

package sel_pkg is
  -- Ceiling log2, for index widths. Named so it cannot collide with a
  -- port: a VHDL port shadows a same-named package object, and VHDL is
  -- case-insensitive, so the collision would be silent.
  function sel_clog2 (n : natural) return natural;
end package;

package body sel_pkg is
  function sel_clog2 (n : natural) return natural is
    variable r : natural := 0;
    variable v : natural := 1;
  begin
    while v < n loop
      v := v * 2;
      r := r + 1;
    end loop;
    if r = 0 then
      return 1;
    else
      return r;
    end if;
  end function;
end package body;


-- ---------------------------------------------------------------------
--  spi_cs_select -- selection by wire.
--
--  IMPORTANT: this entity is given ALL N chip-select lines. No real SPI
--  slave has that. A real slave receives exactly one CS pin and cannot
--  know whether another slave is also selected. So `contention` is a BUS
--  MONITOR output, observable only from a vantage point that no SPI
--  device on a real board occupies.
--
--  That is not a limitation of the model. It is the finding.
-- ---------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.sel_pkg.all;

entity spi_cs_select is
  generic (
    N_DEV : natural := 4
  );
  port (
    clk   : in std_logic;
    rst_n : in std_logic;

    -- Active-low chip selects, one per device, routed on the board.
    cs_n : in std_logic_vector(N_DEV - 1 downto 0);

    sel_valid : out std_logic;
    sel_idx   : out std_logic_vector(sel_clog2(N_DEV) - 1 downto 0);

    -- The failure a real slave cannot see.
    contention : out std_logic;

    n_sel     : out std_logic_vector(31 downto 0);
    n_idle    : out std_logic_vector(31 downto 0);
    n_contend : out std_logic_vector(31 downto 0)
  );
end entity;

architecture rtl of spi_cs_select is
  constant IW : natural := sel_clog2(N_DEV);

  -- How many selects are asserted. On a correct board 0 or 1; two at
  -- once is a firmware or routing fault, and on real hardware it means
  -- two slaves drive MISO simultaneously.
  signal n_low     : unsigned(IW downto 0) := (others => '0');
  signal first_low : unsigned(IW - 1 downto 0) := (others => '0');

  signal sel_c  : unsigned(31 downto 0) := (others => '0');
  signal idle_c : unsigned(31 downto 0) := (others => '0');
  signal con_c  : unsigned(31 downto 0) := (others => '0');
begin

  count : process (all)
    variable c : unsigned(IW downto 0);
    variable f : unsigned(IW - 1 downto 0);
  begin
    c := (others => '0');
    f := (others => '0');
    -- Downwards, so the LOWEST asserted index wins the tie. The
    -- direction is arbitrary but it must be DEFINED: an undefined winner
    -- makes the contention case unverifiable.
    for i in N_DEV - 1 downto 0 loop
      if cs_n(i) = '0' then
        c := c + 1;
        f := to_unsigned(i, IW);
      end if;
    end loop;
    n_low     <= c;
    first_low <= f;
  end process;

  contention <= '1' when n_low > 1 else '0';
  sel_valid  <= '1' when n_low = 1 else '0';
  sel_idx    <= std_logic_vector(first_low);

  n_sel     <= std_logic_vector(sel_c);
  n_idle    <= std_logic_vector(idle_c);
  n_contend <= std_logic_vector(con_c);

  process (clk, rst_n)
  begin
    if rst_n = '0' then
      sel_c  <= (others => '0');
      idle_c <= (others => '0');
      con_c  <= (others => '0');
    elsif rising_edge(clk) then
      if n_low = 0 then
        idle_c <= idle_c + 1;
      elsif n_low = 1 then
        sel_c <= sel_c + 1;
      else
        con_c <= con_c + 1;
      end if;
    end if;
  end process;

end architecture;


-- ---------------------------------------------------------------------
--  usb_addr_select -- selection by assigned name.
--
--  Two real USB rules are enforced, because both are places an
--  implementation can be plausibly wrong:
--
--    * Address 0 is the DEFAULT address. A device there is
--      mid-enumeration and has endpoint 0 only. Honouring any other
--      endpoint would let a half-enumerated device answer traffic it has
--      no configuration for.
--
--    * An UNASSIGNED slot matches nothing, including address 0. A slot
--      that answered before being assigned would respond during another
--      device's enumeration.
--
--  Unlike the SPI decoder, this one CAN see its own worst failure: two
--  slots holding the same address is directly detectable, because the
--  address is a field every slot compares against.
-- ---------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.sel_pkg.all;

entity usb_addr_select is
  generic (
    N_SLOT : natural := 4
  );
  port (
    clk   : in std_logic;
    rst_n : in std_logic;

    -- ---- the assignment interface: this is the whole difference ----
    --
    -- spi_cs_select has no equivalent port. Its mapping is fixed at
    -- elaboration. This one's mapping is a register file, writable at run
    -- time, which is what "the host assigns an address" means in
    -- hardware.
    cfg_valid    : in std_logic;
    cfg_slot     : in std_logic_vector(sel_clog2(N_SLOT) - 1 downto 0);
    cfg_assigned : in std_logic;
    cfg_addr     : in std_logic_vector(6 downto 0);
    cfg_ep_mask  : in std_logic_vector(15 downto 0);

    req_valid : in std_logic;
    req_addr  : in std_logic_vector(6 downto 0);
    req_ep    : in std_logic_vector(3 downto 0);

    sel_valid  : out std_logic;
    sel_slot   : out std_logic_vector(sel_clog2(N_SLOT) - 1 downto 0);
    sel_reason : out std_logic_vector(2 downto 0);

    -- The failure this discipline CAN see.
    conflict : out std_logic;

    n_match    : out std_logic_vector(31 downto 0);
    n_no_addr  : out std_logic_vector(31 downto 0);
    n_no_ep    : out std_logic_vector(31 downto 0);
    n_conflict : out std_logic_vector(31 downto 0)
  );
end entity;

architecture rtl of usb_addr_select is
  constant SW : natural := sel_clog2(N_SLOT);

  constant R_NONE    : std_logic_vector(2 downto 0) := "000";  -- no token
  constant R_MATCH   : std_logic_vector(2 downto 0) := "001";  -- addressed + configured
  constant R_NO_ADDR : std_logic_vector(2 downto 0) := "010";  -- nobody holds it
  constant R_NO_EP   : std_logic_vector(2 downto 0) := "011";  -- endpoint not configured

  type asg_arr_t  is array (0 to N_SLOT - 1) of std_logic;
  type addr_arr_t is array (0 to N_SLOT - 1) of std_logic_vector(6 downto 0);
  type epm_arr_t  is array (0 to N_SLOT - 1) of std_logic_vector(15 downto 0);

  signal sl_asg  : asg_arr_t  := (others => '0');
  signal sl_addr : addr_arr_t := (others => (others => '0'));
  signal sl_epm  : epm_arr_t  := (others => (others => '0'));

  -- Initialised so the concurrent comparisons below are never
  -- evaluated against 'U' in the first delta cycle, which numeric_std
  -- reports as a metavalue warning on every run.
  signal m_any        : std_logic := '0';
  signal m_ep_any     : std_logic := '0';
  signal m_slot       : unsigned(SW - 1 downto 0) := (others => '0');
  signal m_addr_count : unsigned(SW downto 0) := (others => '0');
  signal reason_q     : std_logic_vector(2 downto 0) := "000";

  signal match_c  : unsigned(31 downto 0) := (others => '0');
  signal noaddr_c : unsigned(31 downto 0) := (others => '0');
  signal noep_c   : unsigned(31 downto 0) := (others => '0');
  signal conf_c   : unsigned(31 downto 0) := (others => '0');
begin

  match : process (all)
    variable any_v   : std_logic;
    variable ep_v    : std_logic;
    variable slot_v  : unsigned(SW - 1 downto 0);
    variable count_v : unsigned(SW downto 0);
  begin
    any_v   := '0';
    ep_v    := '0';
    slot_v  := (others => '0');
    count_v := (others => '0');
    -- Downwards, so the LOWEST matching slot wins. Defined, not
    -- arbitrary: the conflict case must have a predictable winner or it
    -- cannot be checked.
    for j in N_SLOT - 1 downto 0 loop
      if sl_asg(j) = '1' and sl_addr(j) = req_addr then
        count_v := count_v + 1;
        any_v   := '1';
        slot_v  := to_unsigned(j, SW);
        -- Address 0 is the default address: endpoint 0 only. A device
        -- there has no configuration yet, so honouring any other
        -- endpoint would answer traffic it cannot service.
        if sl_addr(j) = "0000000" then
          if req_ep = "0000" then
            ep_v := '1';
          end if;
        else
          if sl_epm(j)(to_integer(unsigned(req_ep))) = '1' then
            ep_v := '1';
          end if;
        end if;
      end if;
    end loop;
    m_any        <= any_v;
    m_ep_any     <= ep_v;
    m_slot       <= slot_v;
    m_addr_count <= count_v;
  end process;

  reason : process (all)
  begin
    if req_valid = '0' then
      reason_q <= R_NONE;
    elsif m_any = '0' then
      reason_q <= R_NO_ADDR;
    elsif m_ep_any = '0' then
      reason_q <= R_NO_EP;
    else
      reason_q <= R_MATCH;
    end if;
  end process;

  conflict   <= '1' when m_addr_count > 1 else '0';
  sel_valid  <= '1' when (req_valid = '1' and m_any = '1' and m_ep_any = '1') else '0';
  sel_slot   <= std_logic_vector(m_slot);
  sel_reason <= reason_q;

  n_match    <= std_logic_vector(match_c);
  n_no_addr  <= std_logic_vector(noaddr_c);
  n_no_ep    <= std_logic_vector(noep_c);
  n_conflict <= std_logic_vector(conf_c);

  process (clk, rst_n)
  begin
    if rst_n = '0' then
      sl_asg   <= (others => '0');
      sl_addr  <= (others => (others => '0'));
      sl_epm   <= (others => (others => '0'));
      match_c  <= (others => '0');
      noaddr_c <= (others => '0');
      noep_c   <= (others => '0');
      conf_c   <= (others => '0');
    elsif rising_edge(clk) then
      if cfg_valid = '1' then
        sl_asg(to_integer(unsigned(cfg_slot)))  <= cfg_assigned;
        sl_addr(to_integer(unsigned(cfg_slot))) <= cfg_addr;
        sl_epm(to_integer(unsigned(cfg_slot)))  <= cfg_ep_mask;
      end if;

      if req_valid = '1' then
        case reason_q is
          when R_MATCH   => match_c  <= match_c  + 1;
          when R_NO_ADDR => noaddr_c <= noaddr_c + 1;
          when R_NO_EP   => noep_c   <= noep_c   + 1;
          when others    => null;
        end case;
        if m_addr_count > 1 then
          conf_c <= conf_c + 1;
        end if;
      end if;
    end if;
  end process;

end architecture;

The VHDL testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- =====================================================================
--  Testbench for spi_cs_select and usb_addr_select -- VHDL-2008.
--
--  BOTH DISCIPLINES, ONE BENCH, ONE STIMULUS SOURCE. The chapter's point
--  is a comparison, so the comparison happens inside the verification
--  rather than in prose afterwards.
--
--  THE SHADOW MODELS ARE FORMULATED IN THE OPPOSITE DIRECTION. Both
--  designs resolve ties by walking their index space DOWNWARDS so the
--  lowest index wins; both models walk UPWARDS and stop at the first hit.
--  Same answer, different derivation -- the only way a model can fail to
--  inherit the design's mistake.
--
--  THIS IS THE INDEPENDENT BENCH. The directed phases are structurally
--  identical to the Verilog and SystemVerilog benches, so the DIRECTED
--  mutation columns must agree EXACTLY across all three languages and any
--  disagreement is a real finding. The random phase uses a VHDL-native
--  generator and therefore a different stream, so the ALL columns are
--  expected to differ.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;

entity tb_sl_vhdl is
  generic (
    -- Set true (nvc -e -gDIRECTED_ONLY=true) to run the directed phases
    -- alone. They must pass and must kill every mutation by themselves; a
    -- suite that needs its random phase to find a defect has not
    -- characterised the defect.
    DIRECTED_ONLY : boolean := false
  );
end entity;

architecture sim of tb_sl_vhdl is

  constant N_DEV  : natural := 4;
  constant N_SLOT : natural := 4;

  signal clk   : std_logic := '0';
  signal rst_n : std_logic := '0';
  signal done  : boolean := false;

  -- SPI side
  signal cs_n           : std_logic_vector(N_DEV - 1 downto 0) := (others => '1');
  signal spi_sel_valid  : std_logic;
  signal spi_sel_idx    : std_logic_vector(1 downto 0);
  signal spi_contention : std_logic;
  signal spi_n_sel      : std_logic_vector(31 downto 0);
  signal spi_n_idle     : std_logic_vector(31 downto 0);
  signal spi_n_contend  : std_logic_vector(31 downto 0);

  -- USB side
  signal cfg_valid    : std_logic := '0';
  signal cfg_slot     : std_logic_vector(1 downto 0) := "00";
  signal cfg_assigned : std_logic := '0';
  signal cfg_addr     : std_logic_vector(6 downto 0) := (others => '0');
  signal cfg_ep_mask  : std_logic_vector(15 downto 0) := (others => '0');
  signal req_valid    : std_logic := '0';
  signal req_addr     : std_logic_vector(6 downto 0) := (others => '0');
  signal req_ep       : std_logic_vector(3 downto 0) := (others => '0');

  signal usb_sel_valid  : std_logic;
  signal usb_sel_slot   : std_logic_vector(1 downto 0);
  signal usb_sel_reason : std_logic_vector(2 downto 0);
  signal usb_conflict   : std_logic;
  signal usb_n_match    : std_logic_vector(31 downto 0);
  signal usb_n_no_addr  : std_logic_vector(31 downto 0);
  signal usb_n_no_ep    : std_logic_vector(31 downto 0);
  signal usb_n_conflict : std_logic_vector(31 downto 0);

  constant R_NONE    : std_logic_vector(2 downto 0) := "000";
  constant R_MATCH   : std_logic_vector(2 downto 0) := "001";
  constant R_NO_ADDR : std_logic_vector(2 downto 0) := "010";
  constant R_NO_EP   : std_logic_vector(2 downto 0) := "011";

begin

  dut_spi : entity work.spi_cs_select
    generic map (N_DEV => N_DEV)
    port map (
      clk => clk, rst_n => rst_n, cs_n => cs_n,
      sel_valid => spi_sel_valid, sel_idx => spi_sel_idx,
      contention => spi_contention,
      n_sel => spi_n_sel, n_idle => spi_n_idle, n_contend => spi_n_contend
    );

  dut_usb : entity work.usb_addr_select
    generic map (N_SLOT => N_SLOT)
    port map (
      clk => clk, rst_n => rst_n,
      cfg_valid => cfg_valid, cfg_slot => cfg_slot,
      cfg_assigned => cfg_assigned, cfg_addr => cfg_addr,
      cfg_ep_mask => cfg_ep_mask,
      req_valid => req_valid, req_addr => req_addr, req_ep => req_ep,
      sel_valid => usb_sel_valid, sel_slot => usb_sel_slot,
      sel_reason => usb_sel_reason, conflict => usb_conflict,
      n_match => usb_n_match, n_no_addr => usb_n_no_addr,
      n_no_ep => usb_n_no_ep, n_conflict => usb_n_conflict
    );

  clkgen : process
  begin
    while not done loop
      clk <= '0'; wait for 5 ns;
      clk <= '1'; wait for 5 ns;
    end loop;
    wait;
  end process;

  main : process

    variable errors : integer := 0;
    variable checks : integer := 0;
    variable steps  : integer := 0;
    variable lo     : line;

    -- ---- the headline measurement ----
    variable spi_contend_patterns : integer := 0;
    variable spi_contend_visible  : integer := 0;
    variable usb_conflict_cases   : integer := 0;
    variable usb_conflict_visible : integer := 0;

    -- ---- the shadow copy of the assignment table ----
    --
    -- Maintained by the BENCH from the writes it issued, never read back
    -- from the design. A model that reads the design's own table cannot
    -- detect a design that writes the wrong entry.
    type asg_arr_t  is array (0 to N_SLOT - 1) of std_logic;
    type addr_arr_t is array (0 to N_SLOT - 1) of std_logic_vector(6 downto 0);
    type epm_arr_t  is array (0 to N_SLOT - 1) of std_logic_vector(15 downto 0);
    variable s_asg  : asg_arr_t  := (others => '0');
    variable s_addr : addr_arr_t := (others => (others => '0'));
    variable s_epm  : epm_arr_t  := (others => (others => '0'));

    -- exhaustive reach
    type rspi_t is array (0 to 15) of boolean;
    type rusb_t is array (0 to 2559) of boolean;
    variable reach_spi : rspi_t := (others => false);
    variable reach_usb : rusb_t := (others => false);
    variable nrs, nru  : integer := 0;

    -- A VHDL-native LCG. Deliberately NOT the same stream as the Verilog
    -- bench, so the random phases are genuinely independent.
    variable rnd_state : unsigned(31 downto 0) := x"000606D2";
    impure function urand return integer is
    begin
      -- resize() back to 32 bits: `unsigned * unsigned` widens to 64 in
      -- VHDL, and truncating to 32 is exactly the LCG's mod 2**32.
      rnd_state := resize(rnd_state * to_unsigned(1103515245, 32), 32)
                   + to_unsigned(12345, 32);
      -- Return the HIGH bits, not the low ones. In an LCG with a
      -- power-of-two modulus, bit i has period 2**(i+1): bit 0 alternates,
      -- bit 1 cycles in four. So `urand mod 4` taken from the low bits
      -- returns 3,0,1,2,3,0,1,2,... in perfect lockstep -- while its
      -- histogram is exactly uniform, which is why the defect survives
      -- every distribution check anyone would think to run.
      return to_integer(rnd_state(30 downto 15));
    end function;

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

    -- ---- SPI model: counts upward, unlike the design's downward walk ----
    function spi_popcount (p : std_logic_vector) return integer is
      variable c : integer := 0;
    begin
      for i in 0 to N_DEV - 1 loop
        if p(i) = '0' then c := c + 1; end if;
      end loop;
      return c;
    end function;

    function spi_first_low (p : std_logic_vector) return integer is
      variable found : boolean := false;
      variable r     : integer := 0;
    begin
      -- upward, stop at the first. The design walks downward and
      -- overwrites, so the two agree only if both are right.
      for i in 0 to N_DEV - 1 loop
        if p(i) = '0' and not found then
          r := i; found := true;
        end if;
      end loop;
      return r;
    end function;

    -- ---- USB model: also upward-and-stop, over the bench's own table ----
    impure function usb_addr_count (a : std_logic_vector(6 downto 0)) return integer is
      variable c : integer := 0;
    begin
      for j in 0 to N_SLOT - 1 loop
        if s_asg(j) = '1' and s_addr(j) = a then c := c + 1; end if;
      end loop;
      return c;
    end function;

    impure function usb_first_slot (a : std_logic_vector(6 downto 0)) return integer is
      variable found : boolean := false;
      variable r     : integer := 0;
    begin
      for j in 0 to N_SLOT - 1 loop
        if s_asg(j) = '1' and s_addr(j) = a and not found then
          r := j; found := true;
        end if;
      end loop;
      return r;
    end function;

    -- Does ANY assigned slot at address `a` expose endpoint `e`?
    --
    -- The default-address rule lives here, stated independently of the
    -- design: a slot at address 0 is mid-enumeration and has endpoint 0
    -- only, whatever its ep_mask happens to contain.
    impure function usb_ep_ok (a : std_logic_vector(6 downto 0);
                               e : std_logic_vector(3 downto 0)) return boolean is
      variable ok : boolean := false;
    begin
      for j in 0 to N_SLOT - 1 loop
        if s_asg(j) = '1' and s_addr(j) = a then
          if s_addr(j) = "0000000" then
            if e = "0000" then ok := true; end if;
          else
            if s_epm(j)(to_integer(unsigned(e))) = '1' then ok := true; end if;
          end if;
        end if;
      end loop;
      return ok;
    end function;

    -- ---------------------------------------------------------------
    --  Drive one SPI pattern and check the resolution.
    -- ---------------------------------------------------------------
    procedure spi_step (p : std_logic_vector(N_DEV - 1 downto 0)) is
      variable nlow, efirst    : integer;
      variable s0, i0, c0      : integer;
    begin
      nlow   := spi_popcount(p);
      efirst := spi_first_low(p);
      s0 := to_integer(unsigned(spi_n_sel));
      i0 := to_integer(unsigned(spi_n_idle));
      c0 := to_integer(unsigned(spi_n_contend));

      cs_n <= p;
      wait for 1 ns;

      -- ---- PROPERTY 1: exactly one low is a selection ----
      ck((spi_sel_valid = '1') = (nlow = 1),
         "spi sel_valid does not mean exactly one chip select is asserted");
      -- ---- PROPERTY 2: two or more low is contention ----
      ck((spi_contention = '1') = (nlow > 1),
         "spi contention does not mean two or more chip selects are asserted");
      -- ---- PROPERTY 3: a selection and a contention are exclusive ----
      ck(not (spi_sel_valid = '1' and spi_contention = '1'),
         "spi reported a selection and a contention at the same time");
      -- ---- PROPERTY 4: the winner is the lowest asserted index ----
      if nlow >= 1 then
        ck(to_integer(unsigned(spi_sel_idx)) = efirst,
           "spi did not resolve the tie to the lowest asserted index");
      end if;

      wait until rising_edge(clk);
      wait for 1 ns;

      -- ---- PROPERTY 5: exactly one counter moved ----
      if nlow = 0 then
        ck(to_integer(unsigned(spi_n_idle)) = i0 + 1 and
           to_integer(unsigned(spi_n_sel)) = s0 and
           to_integer(unsigned(spi_n_contend)) = c0,
           "spi counters wrong for an idle bus");
      elsif nlow = 1 then
        ck(to_integer(unsigned(spi_n_sel)) = s0 + 1 and
           to_integer(unsigned(spi_n_idle)) = i0 and
           to_integer(unsigned(spi_n_contend)) = c0,
           "spi counters wrong for a clean selection");
      else
        ck(to_integer(unsigned(spi_n_contend)) = c0 + 1 and
           to_integer(unsigned(spi_n_sel)) = s0 and
           to_integer(unsigned(spi_n_idle)) = i0,
           "spi counters wrong for a contention");
      end if;
      steps := steps + 1;
    end procedure;

    -- ---------------------------------------------------------------
    --  Write one assignment slot, mirroring it into the bench's table.
    --  The mirror happens AFTER the edge on purpose -- see phase 5.
    -- ---------------------------------------------------------------
    procedure do_cfg (slot : integer; asg : std_logic;
                      a : std_logic_vector(6 downto 0);
                      epm : std_logic_vector(15 downto 0)) is
    begin
      cfg_valid    <= '1';
      cfg_slot     <= std_logic_vector(to_unsigned(slot, 2));
      cfg_assigned <= asg;
      cfg_addr     <= a;
      cfg_ep_mask  <= epm;
      wait until rising_edge(clk);
      wait for 1 ns;
      cfg_valid <= '0';
      s_asg(slot)  := asg;
      s_addr(slot) := a;
      s_epm(slot)  := epm;
    end procedure;

    -- ---------------------------------------------------------------
    --  Present one token and check the resolution.
    -- ---------------------------------------------------------------
    procedure usb_step (a : std_logic_vector(6 downto 0);
                        e : std_logic_vector(3 downto 0)) is
      variable nmatch, efirst        : integer;
      variable eep, eany             : boolean;
      variable ereason               : std_logic_vector(2 downto 0);
      variable m0, na0, ne0, cf0     : integer;
    begin
      nmatch := usb_addr_count(a);
      efirst := usb_first_slot(a);
      eany   := nmatch > 0;
      eep    := usb_ep_ok(a, e);
      if not eany then
        ereason := R_NO_ADDR;
      elsif not eep then
        ereason := R_NO_EP;
      else
        ereason := R_MATCH;
      end if;

      m0  := to_integer(unsigned(usb_n_match));
      na0 := to_integer(unsigned(usb_n_no_addr));
      ne0 := to_integer(unsigned(usb_n_no_ep));
      cf0 := to_integer(unsigned(usb_n_conflict));

      req_valid <= '1';
      req_addr  <= a;
      req_ep    <= e;
      wait for 1 ns;

      -- ---- PROPERTY 6: selection means addressed AND configured ----
      ck((usb_sel_valid = '1') = (eany and eep),
         "usb sel_valid does not mean the address matched and the endpoint was configured");
      -- ---- PROPERTY 7: the reason code is exact ----
      ck(usb_sel_reason = ereason, "usb sel_reason disagrees with the model");
      -- ---- PROPERTY 8: the winner is the lowest matching slot ----
      if eany then
        ck(to_integer(unsigned(usb_sel_slot)) = efirst,
           "usb did not resolve a duplicate address to the lowest slot");
      end if;
      -- ---- PROPERTY 9: a duplicate address is DETECTED ----
      --
      -- The property the SPI decoder has no equivalent of, and the reason
      -- is structural rather than a matter of effort: the address is a
      -- field every slot compares against, so the count is available on
      -- the spot. A chip select is a wire only one slave ever receives.
      ck((usb_conflict = '1') = (nmatch > 1),
         "usb conflict does not mean two assigned slots share the address");

      -- The measurement, taken on EVERY token rather than on a directed
      -- handful, so its denominator is every duplicate-address token the
      -- exhaustive sweep produces.
      if nmatch > 1 then
        usb_conflict_cases := usb_conflict_cases + 1;
        if usb_conflict = '1' then
          usb_conflict_visible := usb_conflict_visible + 1;
        end if;
      end if;

      wait until rising_edge(clk);
      wait for 1 ns;
      req_valid <= '0';

      -- ---- PROPERTY 10: exactly one reason counter moved ----
      if ereason = R_MATCH then
        ck(to_integer(unsigned(usb_n_match)) = m0 + 1 and
           to_integer(unsigned(usb_n_no_addr)) = na0 and
           to_integer(unsigned(usb_n_no_ep)) = ne0,
           "usb counters wrong for a match");
      elsif ereason = R_NO_ADDR then
        ck(to_integer(unsigned(usb_n_no_addr)) = na0 + 1 and
           to_integer(unsigned(usb_n_match)) = m0 and
           to_integer(unsigned(usb_n_no_ep)) = ne0,
           "usb counters wrong for an unknown address");
      elsif ereason = R_NO_EP then
        ck(to_integer(unsigned(usb_n_no_ep)) = ne0 + 1 and
           to_integer(unsigned(usb_n_match)) = m0 and
           to_integer(unsigned(usb_n_no_addr)) = na0,
           "usb counters wrong for an unconfigured endpoint");
      end if;
      if nmatch > 1 then
        ck(to_integer(unsigned(usb_n_conflict)) = cf0 + 1,
           "usb conflict counter disagrees with the model");
      else
        ck(to_integer(unsigned(usb_n_conflict)) = cf0,
           "usb conflict counter disagrees with the model");
      end if;
      steps := steps + 1;
    end procedure;

    procedure reset_dut is
    begin
      rst_n     <= '0';
      cs_n      <= (others => '1');
      cfg_valid <= '0';
      req_valid <= '0';
      wait until rising_edge(clk);
      wait until rising_edge(clk);
      rst_n <= '1';
      wait until rising_edge(clk);
      wait for 1 ns;
      s_asg  := (others => '0');
      s_addr := (others => (others => '0'));
      s_epm  := (others => (others => '0'));
    end procedure;

    variable addr_of : std_logic_vector(6 downto 0);
    variable epm_of  : std_logic_vector(15 downto 0);
    variable ri      : integer;
    variable pv      : std_logic_vector(N_DEV - 1 downto 0);
    variable asgv    : std_logic_vector(3 downto 0);
    variable any_slave_knows : boolean;
    variable view_here, view_clean : std_logic;

  begin
    reset_dut;

    -- ===============================================================
    --  PHASE 1 (DIRECTED, EXHAUSTIVE) -- every chip-select pattern.
    --  16 of 16, nothing unreachable.
    -- ===============================================================
    for pat in 0 to 15 loop
      pv := std_logic_vector(to_unsigned(pat, N_DEV));
      spi_step(pv);
      reach_spi(pat) := true;
    end loop;
    cs_n <= (others => '1');

    -- ===============================================================
    --  PHASE 2 (DIRECTED, EXHAUSTIVE) -- the USB selection space.
    --
    --  The table is reconfigured once per (asg_mask, addr_mode, ep_mode)
    --  and every (req_addr, req_ep) is presented against it: 128
    --  configurations x 20 tokens.
    --
    --  addr_mode exists so duplicate addresses are REACHED rather than
    --  assumed away. A sweep with distinct addresses only would leave the
    --  conflict logic at zero coverage while reporting completeness.
    -- ===============================================================
    for asg in 0 to 15 loop
      asgv := std_logic_vector(to_unsigned(asg, 4));
      for am in 0 to 3 loop
        for em in 0 to 1 loop
          for j2 in 0 to N_SLOT - 1 loop
            case am is
              when 0 => addr_of := std_logic_vector(to_unsigned(j2 + 1, 7));
              when 1 => addr_of := std_logic_vector(to_unsigned(1, 7));
              when 2 => addr_of := (others => '0');
              when others => addr_of := std_logic_vector(to_unsigned((j2 mod 2) + 1, 7));
            end case;
            -- ep_mode 1 leaves endpoints 1 and 3 unconfigured, so R_NO_EP
            -- is reachable at a NON-zero address and not only through the
            -- default-address rule.
            if em = 0 then epm_of := x"000F"; else epm_of := x"0005"; end if;
            do_cfg(j2, asgv(j2), addr_of, epm_of);
          end loop;
          for ra in 0 to 4 loop
            for re in 0 to 3 loop
              usb_step(std_logic_vector(to_unsigned(ra, 7)),
                       std_logic_vector(to_unsigned(re, 4)));
              ri := ((((asg * 4 + am) * 2 + em) * 5 + ra) * 4 + re);
              reach_usb(ri) := true;
            end loop;
          end loop;
        end loop;
      end loop;
    end loop;

    -- ===============================================================
    --  PHASE 3 (DIRECTED, EXHAUSTIVE) -- THE MEASUREMENT.
    --
    --  An SPI slave receives ONE chip-select pin, so slave k's entire
    --  view of the world is cs_n(k). The test is a DISTINGUISHABILITY
    --  test, constructed rather than asserted: compare slave k's view
    --  under the contention pattern against its view under the clean
    --  pattern in which only k is selected. If the two views are equal,
    --  no logic inside slave k can tell them apart, whatever it does.
    -- ===============================================================
    for pat in 0 to 15 loop
      pv := std_logic_vector(to_unsigned(pat, N_DEV));
      if spi_popcount(pv) > 1 then
        spi_contend_patterns := spi_contend_patterns + 1;

        any_slave_knows := false;
        for k in 0 to N_DEV - 1 loop
          if pv(k) = '0' then
            view_here  := pv(k);   -- what slave k sees now
            view_clean := '0';     -- what it would see if it alone were selected
            if view_here /= view_clean then any_slave_knows := true; end if;
          end if;
        end loop;
        if any_slave_knows then
          spi_contend_visible := spi_contend_visible + 1;
        end if;

        -- And confirm the BUS MONITOR does see it -- the detector exists
        -- only because this entity was handed every wire, which no slave
        -- on a real board is.
        cs_n <= pv;
        wait for 1 ns;
        ck(spi_contention = '1',
           "the bus monitor failed to see a contention it was given every wire for");
        wait until rising_edge(clk);
        wait for 1 ns;
      end if;
    end loop;
    cs_n <= (others => '1');

    -- The USB side of this question is measured inside usb_step, on every
    -- one of the 2560 exhaustive tokens, so there is nothing to add here.

    -- ===============================================================
    --  PHASE 4 (DIRECTED) -- RE-ADDRESSING AT RUN TIME.
    --
    --  The capability SPI does not have and cannot be given without
    --  changing the board.
    --
    --  The absence of a comparable phase on the SPI side is not an
    --  omission. spi_cs_select HAS NO CONFIGURATION PORT -- its mapping
    --  is fixed at elaboration -- and that missing port is the finding.
    -- ===============================================================
    reset_dut;
    do_cfg(0, '1', std_logic_vector(to_unsigned(5, 7)), x"000F");
    usb_step(std_logic_vector(to_unsigned(5, 7)), "0000");   -- answers at 5
    do_cfg(0, '1', std_logic_vector(to_unsigned(9, 7)), x"000F");
    usb_step(std_logic_vector(to_unsigned(5, 7)), "0000");   -- silent at 5
    usb_step(std_logic_vector(to_unsigned(9, 7)), "0000");   -- answers at 9
    do_cfg(0, '0', std_logic_vector(to_unsigned(9, 7)), x"000F");
    usb_step(std_logic_vector(to_unsigned(9, 7)), "0000");   -- silent again
    -- An unassigned slot must not answer even at the default address, or
    -- it would respond during another device's enumeration.
    usb_step((others => '0'), "0000");

    -- ===============================================================
    --  PHASE 5 (DIRECTED) -- WHEN A CONFIGURATION TAKES EFFECT.
    --
    --  The design registers the table, so a token presented in the SAME
    --  cycle as a write must see the OLD table. do_cfg mirrors the write
    --  only AFTER the edge, so a design that applied it early would
    --  disagree here and nowhere else in the run.
    -- ===============================================================
    reset_dut;
    do_cfg(1, '1', std_logic_vector(to_unsigned(7, 7)), x"000F");
    cfg_valid    <= '1';
    cfg_slot     <= "01";
    cfg_assigned <= '1';
    cfg_addr     <= std_logic_vector(to_unsigned(8, 7));
    cfg_ep_mask  <= x"000F";
    req_valid    <= '1';
    req_addr     <= std_logic_vector(to_unsigned(7, 7));
    req_ep       <= "0000";
    wait for 1 ns;
    ck(usb_sel_valid = '1',
       "a configuration write took effect in the same cycle as the token");
    ck(usb_sel_reason = R_MATCH,
       "same-cycle reason code should still be MATCH at the old address");
    wait until rising_edge(clk);
    wait for 1 ns;
    cfg_valid <= '0';
    req_valid <= '0';
    s_asg(1)  := '1';
    s_addr(1) := std_logic_vector(to_unsigned(8, 7));
    s_epm(1)  := x"000F";
    usb_step(std_logic_vector(to_unsigned(8, 7)), "0000");
    usb_step(std_logic_vector(to_unsigned(7, 7)), "0000");

    -- ===============================================================
    --  PHASE 6 (RANDOM)
    -- ===============================================================
    if not DIRECTED_ONLY then
      reset_dut;
      for k in 0 to 399 loop
        if (urand mod 3) = 0 then
          if (urand mod 2) = 0 then epm_of := x"000F"; else epm_of := x"0005"; end if;
          if (urand mod 4) /= 0 then
            do_cfg(urand mod 4, '1', std_logic_vector(to_unsigned(urand mod 6, 7)), epm_of);
          else
            do_cfg(urand mod 4, '0', std_logic_vector(to_unsigned(urand mod 6, 7)), epm_of);
          end if;
        end if;
        usb_step(std_logic_vector(to_unsigned(urand mod 6, 7)),
                 std_logic_vector(to_unsigned(urand mod 4, 4)));
        spi_step(std_logic_vector(to_unsigned(urand mod 16, N_DEV)));
      end loop;
    end if;

    nrs := 0;
    for i in 0 to 15 loop
      if reach_spi(i) then nrs := nrs + 1; end if;
    end loop;
    nru := 0;
    for i in 0 to 2559 loop
      if reach_usb(i) then nru := nru + 1; end if;
    end loop;

    write(lo, string'("steps=") & integer'image(steps) &
              string'(" checks=") & integer'image(checks) &
              string'(" reach_spi=") & integer'image(nrs) &
              string'("/16 reach_usb=") & integer'image(nru) &
              string'("/2560 errors=") & integer'image(errors));
    writeline(output, lo);
    write(lo, string'("[spi] selections=") & integer'image(to_integer(unsigned(spi_n_sel))) &
              string'(" idle=") & integer'image(to_integer(unsigned(spi_n_idle))) &
              string'(" CONTENTIONS=") & integer'image(to_integer(unsigned(spi_n_contend))));
    writeline(output, lo);
    write(lo, string'("[usb] matches=") & integer'image(to_integer(unsigned(usb_n_match))) &
              string'(" no_addr=") & integer'image(to_integer(unsigned(usb_n_no_addr))) &
              string'(" no_ep=") & integer'image(to_integer(unsigned(usb_n_no_ep))) &
              string'(" CONFLICTS=") & integer'image(to_integer(unsigned(usb_n_conflict))));
    writeline(output, lo);
    write(lo, string'("--- detectability from the vantage point a real device occupies ---"));
    writeline(output, lo);
    write(lo, string'("[spi] contention patterns=") & integer'image(spi_contend_patterns) &
              string'("  distinguishable by any slave=") & integer'image(spi_contend_visible));
    writeline(output, lo);
    write(lo, string'("[usb] duplicate-address tokens=") & integer'image(usb_conflict_cases) &
              string'("  detected by the device=") & integer'image(usb_conflict_visible));
    writeline(output, lo);

    if nrs /= 16 or nru /= 2560 then
      write(lo, string'("FAIL: exhaustive sweep incomplete")); writeline(output, lo);
      errors := errors + 1;
    end if;
    if errors = 0 then
      write(lo, string'("PASS: 0 errors in ") & integer'image(checks) & string'(" checks"));
    else
      write(lo, string'("FAIL: ") & integer'image(errors) &
                string'(" errors in ") & integer'image(checks) & string'(" checks"));
    end if;
    writeline(output, lo);

    done <= true;
    wait;
  end process;

end architecture;

What the third language found here

The generator defect described in chapter 28.1 was found on this chapter. The VHDL random phase reported matches=0 no_addr=400 — four hundred tokens and not a single address match — while the Verilog stream on identical directed stimulus reported 127 matches.

The cause was that urand returned the low bits of a linear congruential generator, where bit i has period 2 to the power (i+1). So urand mod 4, used to pick which slot to configure, returned

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, ...

in perfect lockstep with the loop — phase-locked against the address picked by a later call, so the configured slot and the requested address never coincided. Its histogram over 2000 draws is exactly uniform, 500 of each, which is why no distribution check would have caught it.

12. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
//  Properties for the two selection disciplines.
//
//  NOT SIMULATED IN THIS CHAPTER. Icarus Verilog does not support
//  concurrent assertions, so every number published here comes from the
//  procedural checks in the testbenches. These are the same obligations
//  in the form a commercial simulator or a formal tool would take.
//
//  Note which module gets which properties. The SPI module's properties
//  are all about ITS OWN OUTPUTS, because that is all it can promise. The
//  USB module gets properties about the RELATIONSHIP between a token and
//  the table, because it has the information to support them.
// ---------------------------------------------------------------------
module sel_sva #(parameter int N_DEV = 4, parameter int N_SLOT = 4) (
  input logic        clk,
  input logic        rst_n,
  // spi
  input logic [N_DEV-1:0] cs_n,
  input logic        spi_sel_valid,
  input logic [1:0]  spi_sel_idx,
  input logic        spi_contention,
  // usb
  input logic        cfg_valid,
  input logic [1:0]  cfg_slot,
  input logic        req_valid,
  input logic [6:0]  req_addr,
  input logic [3:0]  req_ep,
  input logic        usb_sel_valid,
  input logic [2:0]  usb_sel_reason,
  input logic        usb_conflict,
  input logic [31:0] usb_n_match
);

  localparam logic [2:0] R_NONE = 3'd0, R_MATCH = 3'd1,
                         R_NO_ADDR = 3'd2, R_NO_EP = 3'd3;

  default clocking cb @(posedge clk); endclocking
  default disable iff (!rst_n);

  // ---- 1. a selection and a contention are mutually exclusive ----
  // If both could be true the master would have no way to interpret the
  // pair, and the monitor's whole output would be ambiguous.
  a_spi_excl : assert property (!(spi_sel_valid && spi_contention));

  // ---- 2. an idle bus selects nothing ----
  a_spi_idle : assert property ((&cs_n) |-> !spi_sel_valid);

  // ---- 3. the selected line is actually asserted ----
  // Weaker than "it is the lowest asserted line", which needs a count and
  // is left to the procedural bench. This is the part expressible locally.
  a_spi_low : assert property (spi_sel_valid |-> !cs_n[spi_sel_idx]);

  // ---- 4. no token, no selection ----
  a_usb_gated : assert property (!req_valid |-> !usb_sel_valid);

  // ---- 5. the reason code and the valid bit agree ----
  // Two outputs encoding overlapping information must never disagree, or
  // a consumer that trusts one and a consumer that trusts the other will
  // behave differently on the same cycle.
  a_usb_reason : assert property (usb_sel_valid == (usb_sel_reason == R_MATCH));

  // ---- 6. NONE is reserved for the no-token case ----
  a_usb_none : assert property ((usb_sel_reason == R_NONE) |-> !req_valid);

  // ---- 7. the match counter moves ONLY on a match ----
  // Stated in this direction on purpose. "A match implies the counter
  // incremented" is the easy half; a counter that also advanced on some
  // other condition would satisfy it while making every published total
  // unfalsifiable, because the totals are read from these counters.
  a_usb_count : assert property
    ((usb_n_match != $past(usb_n_match)) |-> $past(usb_sel_valid));

  // ---- 8. a configuration write cannot change the CURRENT answer ----
  //
  // THE boundary property, and the one section 8 exists for. The table is
  // registered, so a write and a token in the same cycle must resolve
  // against the pre-write table. 2560 exhaustive tokens cannot reach this,
  // because none of them writes and reads simultaneously.
  property p_cfg_next_cycle;
    (cfg_valid && req_valid) |-> (usb_sel_reason == $past(usb_sel_reason, 0));
  endproperty
  a_cfg_next : assert property (p_cfg_next_cycle);

  // ---- COVER: the failure modes are actually reached ----
  // Assertions over stimulus that never produces a contention or a
  // duplicate address prove nothing. These covers are the denominator.
  c_contend  : cover property (spi_contention);
  c_conflict : cover property (usb_conflict);
  c_no_ep    : cover property (usb_sel_reason == R_NO_EP);
  c_no_addr  : cover property (usb_sel_reason == R_NO_ADDR);
  c_cfg_race : cover property (cfg_valid && req_valid);

endmodule

13. Where UVM Fits

The verification problem here has a shape worth naming, because it recurs whenever two implementations of one idea are compared:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
//  UVM structure for the two selection disciplines.
//
//  NOT SIMULATED IN THIS CHAPTER. Icarus cannot compile UVM -- it breaks
//  on virtual method dispatch -- so every number published comes from the
//  procedural benches. This is the structure a production environment
//  would use, and the mapping from the procedural bench is exact.
//
//  THE DESIGN DECISION: one abstract selection request, TWO agents.
//  The sequence library describes what is being asked for; each agent
//  knows how its own protocol asks it. That is what makes the two
//  disciplines comparable at all -- if each agent had its own transaction
//  type, the scoreboard would be comparing two things it could not line
//  up.
// ---------------------------------------------------------------------

// ---- one abstract request, protocol-independent ----
class select_request extends uvm_sequence_item;
  `uvm_object_utils(select_request)

  // What the transaction WANTS, expressed without reference to either bus.
  rand int  target;        // which peripheral is intended
  rand int  endpoint;      // which endpoint (ignored by SPI: it has none)

  // The failure to inject. Modelled as a first-class field rather than as
  // an error hook, because both failure modes are reachable only on
  // purpose and the covergroup has to be able to see them.
  rand bit  inject_double; // SPI: assert a second chip select
  rand bit  inject_dup;    // USB: give two slots the same address

  constraint c_sane {
    target   inside {[0:3]};
    endpoint inside {[0:3]};
  }
  // Failures stay rare so the clean path keeps most of the cycles; making
  // them 50/50 would measure a broken bus rather than a working one.
  constraint c_rare { inject_double dist {0 := 9, 1 := 1};
                      inject_dup    dist {0 := 9, 1 := 1}; }

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

// ---- the SPI agent translates a request into WIRES ----
class spi_select_driver extends uvm_driver #(select_request);
  `uvm_component_utils(spi_select_driver)
  virtual spi_if vif;

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

  task run_phase(uvm_phase phase);
    forever begin
      select_request tr;
      logic [3:0] mask;
      seq_item_port.get_next_item(tr);
      mask = '1;
      mask[tr.target] = 1'b0;
      // The injected failure is a second line, which is exactly how the
      // real fault arrives: one extra GPIO write.
      if (tr.inject_double) mask[(tr.target + 1) % 4] = 1'b0;
      vif.cs_n <= mask;
      @(posedge vif.clk);
      seq_item_port.item_done();
    end
  endtask
endclass

// ---- the USB agent translates the SAME request into a TOKEN ----
//
// Note what this driver has that the SPI driver does not: a configuration
// phase. It must assign an address before it can use one. That asymmetry
// between the two drivers is the protocol difference, and it is the reason
// the two agents are not interchangeable even though their sequence item
// is.
class usb_select_driver extends uvm_driver #(select_request);
  `uvm_component_utils(usb_select_driver)
  virtual usb_if vif;

  task run_phase(uvm_phase phase);
    forever begin
      select_request tr;
      seq_item_port.get_next_item(tr);
      assign_address(tr.target, tr.target + 1, tr.inject_dup);
      send_token(tr.target + 1, tr.endpoint);
      seq_item_port.item_done();
    end
  endtask

  task assign_address(int slot, int addr, bit dup);
    vif.cfg_valid <= 1'b1;
    vif.cfg_slot  <= slot[1:0];
    vif.cfg_assigned <= 1'b1;
    vif.cfg_addr  <= addr[6:0];
    vif.cfg_ep_mask <= 16'h000F;
    @(posedge vif.clk);
    if (dup) begin
      // A second slot at the same address -- the failure this discipline
      // CAN see, which is the whole point of injecting it.
      vif.cfg_slot <= ((slot + 1) % 4);
      @(posedge vif.clk);
    end
    vif.cfg_valid <= 1'b0;
  endtask

  task send_token(int addr, int ep);
    vif.req_valid <= 1'b1;
    vif.req_addr  <= addr[6:0];
    vif.req_ep    <= ep[3:0];
    @(posedge vif.clk);
    vif.req_valid <= 1'b0;
  endtask
endclass

// ---- the scoreboard scores DETECTABILITY, not just correctness ----
//
// This is the part that differs from a conventional environment. Both
// designs are expected to resolve selection correctly; the measurement is
// whether the injected failure was REPORTED. So the scoreboard counts two
// things per discipline: failures injected, and failures observed.
class detectability_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(detectability_scoreboard)

  int spi_injected, spi_observed;
  int usb_injected, usb_observed;

  function void report_phase(uvm_phase phase);
    `uvm_info("DETECT", $sformatf(
      "spi: %0d/%0d injected contentions observable by a slave; usb: %0d/%0d duplicate addresses reported",
      spi_observed, spi_injected, usb_observed, usb_injected), UVM_LOW)
    // The SPI figure is expected to be ZERO, and a non-zero value is the
    // bug -- it would mean the environment gave a slave information no
    // slave has. An environment that models a slave with all N select
    // lines measures a bus nobody builds.
    if (spi_observed != 0)
      `uvm_error("DETECT", "a modelled SPI slave saw a contention: the slave model has too many pins")
  endfunction
endclass

// ---- coverage is the cross, as always ----
class select_coverage extends uvm_subscriber #(select_request);
  `uvm_component_utils(select_coverage)

  covergroup cg with function sample(select_request tr);
    cp_target   : coverpoint tr.target        { bins t[] = {[0:3]}; }
    cp_endpoint : coverpoint tr.endpoint      { bins e[] = {[0:3]}; }
    cp_double   : coverpoint tr.inject_double { bins b[] = {0, 1}; }
    cp_dup      : coverpoint tr.inject_dup    { bins b[] = {0, 1}; }
    // The failure injection crossed with the target, because a contention
    // between slots 0 and 1 is a different circuit path from one between
    // 3 and 0, and the tie-resolution direction only shows up in the
    // cross.
    x_fail : cross cp_target, cp_double;
    x_dup  : cross cp_target, cp_dup;
  endgroup

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

  function void write(select_request t);
    cg.sample(t);
  endfunction
endclass

14. Mutation Testing

Eight defects, injected one at a time into all three languages. Every replacement is asserted by the generator, and each mutation is generated as its own file rather than through nested ifdefs — because L5 and L7 touch overlapping regions of the same if/else, and a nested guard makes it easy to neutralise a different statement set in one language than in another.

#the injected defectV-allV-dirSV-allSV-dirVHDL-allVHDL-dir
BASEunmodified designs000000
L1a contention reported as a clean selection572225722254822
L2only three-way contention flagged160121601216712
L3the tie resolves to the highest index286112861127411
L4an unassigned slot matches184216481842164817961648
L5the default-address rule is dropped216180216180273180
L6SPI's blindness, injected into USB548480548480548480
L7the endpoint mask is ignored507426507426501426
L8every config write lands in slot 0260020732600207326522073

Every mutation is killed, and every one is killed by directed stimulus alone. The directed column is identical across all three languages at all eight rows — 22, 12, 11, 1648, 180, 480, 426, 2073.

L6 is the mutation this chapter was built for

L6 clamps m_addr_count at one, so usb_addr_select can never report a duplicate address. Nothing else changes: the selection still resolves to the correct slot, the reason code is still right, every byte of the data path is untouched. The only thing removed is the ability to know.

In other words, L6 injects SPI's structural blindness into the USB matcher.

It scores 480 in the directed phase, identically in all three languages. That matters because it makes the chapter's central claim falsifiable rather than rhetorical:

Why the SPI scores are small, and why that is correct

L1, L2 and L3 score 22, 12 and 11 in the directed phase. Those look weak next to L8's 2073 and they are not weak at all — they are exactly the size of the domain:

#reachable instances of the defectchecks it breaks each timepredictedmeasured
L111 contention patterns2 (sel_valid, exclusivity)2222
L26 exactly-two patterns2 (phase 1, phase 3 monitor)1212
L311 contention patterns1 (lowest-index)1111

With four devices there are only 16 chip-select patterns and only 11 of them contain a contention. A score of 22 against a domain of 11 means every reachable instance is caught, twice over. There is no stimulus that could raise it, because there is no seventeenth pattern.

Run totals

stepschecksSPI reachUSB reacherrors
Verilog, full338317,76316 / 162560 / 25600
Verilog, directed only258313,61816 / 162560 / 25600
SystemVerilog, full338317,76316 / 162560 / 25600
SystemVerilog, directed only258313,61816 / 162560 / 25600
VHDL, full338317,77616 / 162560 / 25600
VHDL, directed only258313,61816 / 162560 / 25600

The directed-only rows are identical across all three languages in every column, including both exhaustive denominators. The full rows differ only in VHDL's check count, by 13, from its independent random stream.

15. What This Does Not Cover

Neither module moves data. There is no SPI shift register and no USB packet decoder. The comparison is about selection, and adding the data path would add volume without adding a difference.

The SPI slave model has one pin, by construction. That is the point of section 6, but it is worth stating as a limit: this chapter proves that a slave with one select pin cannot detect contention. A bus that routed every select line to every device could detect it — and would be a different bus, with N² routing instead of N.

No electrical modelling. On real hardware two selected slaves drive MISO simultaneously and the result depends on the drivers, the pull-ups and the timing. RTL simulation cannot model that contention; it models the logical consequence, which is that no participant knows.

Four devices and four slots. The SPI sweep is exhaustive at N = 4 (16 patterns). The detectability result generalises trivially — a slave with one pin cannot see N−1 others for any N — but the numbers in section 6 are for N = 4.

USB address space truncated to 0–4 in the sweep. The real field is 7 bits. Five values with four slots is enough to reach every structural case (no match, one match, duplicate, default address); the remaining 123 addresses add points, not cases.

Enumeration is not modelled, only its output. The host's SET_ADDRESS sequence, the control transfer that carries it, and the reset that precedes it are chapter 27.3's subject. Here the assignment is a register write, which is what that sequence eventually becomes in hardware.

16. The Interview Answer

"USB versus SPI" is usually answered with pin counts, and the pin count is the weakest true thing you can say. Three sentences:

1. Name the mechanism. "SPI selects a peripheral with a wire that a board designer routed. USB selects one with an address the host assigned at enumeration. So SPI's selection mapping is structure and USB's is state."

2. Name the consequence that is not pin count. "Because a chip select is a wire only one slave receives, an SPI slave cannot tell a correct selection from a bus contention — both look like its own line going low. A USB device sees the whole address field, so it can detect a duplicate address in the cycle it arrives."

3. Say what that buys and what it costs. "That is why USB gets enumeration, hot-plug and re-numbering after a reset, and why SPI bring-up failures are symptoms rather than signatures. It is also why SPI needs no host, no driver stack and no protocol engine — which is exactly why SPI flash is still on your board."

17. What Carries Forward

Two comparisons, two mechanisms, two costs measured:

chapterthe mechanism the other protocol lacksthe measured cost
28.1, UARTsynchronisation from a single edgea tolerance budget shrinking as 1/N
28.2, SPIasking a device who it is0 of 11 failures detectable

Both costs turned out to be about information: how much of it reaches the place where a decision is made. UART's receiver decides where a bit is from one edge. SPI's slave decides whether it is selected from one wire. In both cases the protocol that spends more on delivering information gets a capability that cannot be retrofitted.

The next chapter changes the question. UART and SPI are both local buses with exactly one master, and every difference so far has been about how that single master addresses its peripherals. Ethernet has no master at all.

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.