Skip to content
VLSI Mentor

USB · Module 28

USB vs Ethernet

USB has one authority that assigns every address; Ethernet has none, so a switch infers the topology from traffic — and an inferred table is wrong 294 times out of 1065 where an assigned one is wrong 0 times out of 130.

The third of four comparisons. The first two were about a single master addressing its peripherals. This one is about a bus with no master at all.

1. The Comparison That Is Not About Speed

The usual answer reaches for bandwidth and reach: Ethernet does kilometres and terabits, USB does metres and gigabits. Both true, and neither explains anything, because the two were not built to different scales of the same idea. They were built on opposite answers to a prior question.

That is a measurable claim, so this chapter measures it. The same question is put to both mechanisms, in hardware, under the same stimulus:

frames whose destination existsdelivered to the wrong port
Ethernet, learned table1065294
USB, assigned map1300

Those are the directed-phase figures, identical in Verilog, SystemVerilog and VHDL. Section 6 explains exactly what they count and why the comparison is fair.

2. Where The Authority Sits

Ethernet does have an address-assigning authority — DHCP. It is worth being precise about why that does not help, because it is the whole architectural point.

Ethernet's only address authority sits ABOVE the layer that forwards

Ethernet's layers and where each gets its names. The application layer names by IP and port. DHCP assigns IP addresses, which is an authority, but it operates above the link layer and knows nothing about switch ports. The link layer uses MAC addresses burned in by the manufacturer, which no local authority assigned. The switch forwarding layer must map MAC address to port and has no authority to ask, so it learns from traffic. The physical layer reports its own link going down but cannot report where a station went.Ethernet: the authority and the forwarding decision are on differentlayersApplication / socketnames a peer by IP address and port — a name, not a locationnames a peer by IP address and port — a name, not a locationNetwork (IP) — DHCP lives herean authority DOES assign these — but it assigns IP, and knows nothing about portsan authority DOES assign these — but it assigns IP, and knows nothing about portsLink (MAC)burned in by the manufacturer — no local authority assigned it or can change itburned in by the manufacturer — no local authority assigned it or can change itSwitch forwarding — the decision that mattersmust map MAC → port with nobody to ask, so it LEARNS from trafficmust map MAC → port with nobody to ask, so it LEARNS from trafficPhysicalreports its OWN link going down — never where a departed station wentreports its OWN link going down — never where a departed station went
Read it bottom-up. The forwarding decision is made at the switch, from a MAC address that no local authority assigned. DHCP hands out IP addresses one layer higher and knows nothing about ports. So the layer that must decide WHERE has no authority to ask, and the layer with an authority does not make that decision.

USB's stack inverts that relationship. Its authority is at the bottom: the host owns the physical ports, detects connect and disconnect electrically, and performs the address assignment itself. Every layer above inherits a mapping that is correct by construction rather than by inference.

3. Two Designs, One Question

Both modules answer exactly one question — which port is this address on? — and both are driven by the same testbench from the same stimulus source, so the comparison happens inside the verification rather than in prose afterwards.

eth_learn_tableusb_hub_route_map
where the mapping comes frominferred from fr_srcwritten by the host
is there a source-address portyes — that is the evidenceno — nothing to infer from
can traffic change the mappingyesno
agingrequirednone — a record does not decay
evictionrequirednone — indexed by address
unknown destinationflooded to every portanswered "no record"
a device leavesfound out from traffic, eventuallya port event, immediately

The row that decides the measurement is the last one. Everything else on that table follows from the first.

4. The Designs (Verilog-2005)

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  eth_learn_table -- the mechanism Ethernet needs and USB does not.
//
//  CLASSIFICATION: simplified synthesisable teaching RTL.
//  This is NOT a switch. There is no MAC, no CRC, no VLAN, no spanning
//  tree, no queueing and no multicast handling. It is one mechanism: the
//  forwarding table, and how a switch comes to have one.
//
//  WHY THIS MECHANISM IS WORTH RTL
//  -------------------------------
//  USB has exactly one host, and that host ASSIGNS every address. A USB
//  device cannot choose its own address, cannot keep an address across a
//  bus reset, and cannot be reached at an address the host did not hand
//  out. The mapping from address to port is therefore KNOWN, by
//  construction, to the only party that needs it.
//
//  Ethernet has no host. There is no authority to assign addresses and
//  no authority to be told where anything is. A switch is handed a wire
//  and must work out the topology by itself, which it does by the only
//  means available: it WATCHES. Every frame carries a source address,
//  and the port it arrived on is evidence of where that address lives.
//
//      USB:      the mapping is ASSIGNED, so it is correct by authority.
//      Ethernet: the mapping is INFERRED, so it is correct by evidence
//                -- and evidence goes stale, can be missing, and can be
//                manufactured.
//
//  That is the trade this module measures. Learning is what lets
//  Ethernet span networks with no central authority at all, which USB
//  cannot do in principle. The price is a table that can be WRONG, and
//  the chapter puts numbers on all four ways it can be wrong:
//
//      1. missing   -- nothing learned yet, so the frame is FLOODED
//      2. stale     -- the station moved and has not spoken since
//      3. full      -- no room to learn, so flooding is permanent
//      4. forged    -- a frame lied about its source and moved an entry
//
//  A table you were handed cannot be any of those things. A table you
//  learned can be all four.
// =====================================================================
module eth_learn_table #(
  parameter integer N_PORT  = 4,
  parameter integer N_ENTRY = 4,
  // Ticks before an unrefreshed entry is discarded. Real switches use
  // 300 seconds; the value only has to be small enough to reach in
  // simulation and large enough that aging is not the common case.
  parameter integer AGE_MAX = 7
) (
  input  wire       clk,
  input  wire       rst_n,

  // ---- one frame arriving ----
  input  wire       fr_valid,
  input  wire [7:0] fr_src,     // who sent it: this is the EVIDENCE
  input  wire [7:0] fr_dst,     // who it is for: this is the QUERY
  input  wire [$clog2(N_PORT)-1:0] fr_port,   // which port it arrived on

  // ---- the aging clock, one tick at a time ----
  input  wire       age_tick,

  // ---- the forwarding decision ----
  output wire       fwd_valid,  // forward to exactly one port
  output wire [$clog2(N_PORT)-1:0] fwd_port,
  output wire       fwd_flood,  // destination unknown: send everywhere
  output wire       fwd_drop,   // destination is on the ingress port

  // ---- observability ----
  output wire [31:0] n_learn,     // a new address was recorded
  output wire [31:0] n_relearn,   // a known address moved port
  output wire [31:0] n_hit,       // the table answered
  output wire [31:0] n_flood,     // the table could not answer
  output wire [31:0] n_evict,     // a live entry was displaced
  output wire [31:0] n_aged,      // an entry expired
  output wire [31:0] n_occupied   // how many entries are currently valid
);

  localparam integer PW = $clog2(N_PORT);
  localparam integer EW = $clog2(N_ENTRY);

  // The table. Per-field arrays rather than an array of structs: a
  // variable field-select into an unpacked array of packed structs
  // aborts the Icarus elaborator.
  reg           e_val  [0:N_ENTRY-1];
  reg [7:0]     e_addr [0:N_ENTRY-1];
  reg [PW-1:0]  e_port [0:N_ENTRY-1];
  reg [7:0]     e_age  [0:N_ENTRY-1];

  integer i;

  // -------------------------------------------------------------------
  //  LOOKUP (combinational) -- does the table know where fr_dst lives?
  //
  //  Walk downwards so the LOWEST matching entry wins. Addresses are
  //  supposed to be unique in the table, and the design maintains that;
  //  the direction is fixed anyway so the duplicate case has a defined
  //  answer rather than an undefined one.
  // -------------------------------------------------------------------
  reg          hit;
  reg [PW-1:0] hit_port;
  always @(*) begin
    hit      = 1'b0;
    hit_port = {PW{1'b0}};
    for (i = N_ENTRY - 1; i >= 0; i = i - 1) begin
      if (e_val[i] && (e_addr[i] == fr_dst)) begin
        hit      = 1'b1;
        hit_port = e_port[i];
      end
    end
  end

  // A frame whose destination is on the port it arrived from must NOT be
  // sent back out that port. Real switches drop it, and so does this:
  // forwarding it would create a loop of exactly one hop.
  wire dst_is_ingress = hit && (hit_port == fr_port);

  assign fwd_drop  = fr_valid && dst_is_ingress;
  assign fwd_valid = fr_valid && hit && !dst_is_ingress;
  assign fwd_port  = hit_port;
  // The cost of not being told. With no entry there is no choice but to
  // send the frame to every port -- which is a bandwidth cost on every
  // link and a confidentiality cost on every one of them too.
  assign fwd_flood = fr_valid && !hit;

  // -------------------------------------------------------------------
  //  LEARN (combinational index, one registered write)
  //
  //  Three cases, in priority order:
  //    1. fr_src is already in the table  -> refresh it, and move it if
  //       the port changed (that is a station having moved)
  //    2. there is a free entry           -> take it
  //    3. the table is full               -> evict the OLDEST entry
  //
  //  The index is computed combinationally and written exactly once.
  //  Writing inside the search loop would produce several non-blocking
  //  assignments to the same register, where the last one silently wins.
  // -------------------------------------------------------------------
  reg          src_found;
  reg [EW-1:0] src_idx;
  reg          free_found;
  reg [EW-1:0] free_idx;
  reg [EW-1:0] old_idx;
  reg [7:0]    old_age;

  always @(*) begin
    src_found  = 1'b0;
    src_idx    = {EW{1'b0}};
    free_found = 1'b0;
    free_idx   = {EW{1'b0}};
    old_idx    = {EW{1'b0}};
    old_age    = 8'd0;
    for (i = N_ENTRY - 1; i >= 0; i = i - 1) begin
      if (e_val[i] && (e_addr[i] == fr_src)) begin
        src_found = 1'b1;
        src_idx   = i[EW-1:0];
      end
      if (!e_val[i]) begin
        free_found = 1'b1;
        free_idx   = i[EW-1:0];
      end
      // >= so the lowest index wins a tie, matching the walk direction
      if (e_val[i] && (e_age[i] >= old_age)) begin
        old_age = e_age[i];
        old_idx = i[EW-1:0];
      end
    end
  end

  wire [EW-1:0] learn_idx = src_found  ? src_idx  :
                            free_found ? free_idx : old_idx;
  // A station that moved: the address is known but the evidence now
  // points somewhere else. The table believes the newest evidence, which
  // is the only policy that can ever converge -- and is also exactly
  // what makes the table forgeable.
  wire moved   = src_found && (e_port[src_idx] != fr_port);
  wire evicted = !src_found && !free_found;

  reg [31:0] learn_c, relearn_c, hit_c, flood_c, evict_c, aged_c;
  assign n_learn   = learn_c;
  assign n_relearn = relearn_c;
  assign n_hit     = hit_c;
  assign n_flood   = flood_c;
  assign n_evict   = evict_c;
  assign n_aged    = aged_c;


  // How many entries will expire on this tick. Computed combinationally
  // for the same reason as learn_idx: one write per register.
  reg [31:0] n_age_now;
  always @(*) begin
    n_age_now = 32'd0;
    for (i = 0; i < N_ENTRY; i = i + 1)
      if (e_val[i] && (e_age[i] >= AGE_MAX)) n_age_now = n_age_now + 32'd1;
  end

  // Combinational, so it reports the table as it stands rather than as
  // it stood a cycle ago. A registered version would lag every learn by
  // one cycle and make the bench's occupancy checks off-by-one.
  reg [31:0] occ_now;
  always @(*) begin
    occ_now = 32'd0;
    for (i = 0; i < N_ENTRY; i = i + 1) if (e_val[i]) occ_now = occ_now + 32'd1;
  end
  assign n_occupied = occ_now;

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (i = 0; i < N_ENTRY; i = i + 1) begin
        e_val[i]  <= 1'b0;
        e_addr[i] <= 8'd0;
        e_port[i] <= {PW{1'b0}};
        e_age[i]  <= 8'd0;
      end
      learn_c   <= 32'd0;
      relearn_c <= 32'd0;
      hit_c     <= 32'd0;
      flood_c   <= 32'd0;
      evict_c   <= 32'd0;
      aged_c    <= 32'd0;
    end else begin
      // ---- aging happens first, so a tick and a frame in the same
      //      cycle leave the frame's own entry fresh rather than expired
      if (age_tick) begin
        for (i = 0; i < N_ENTRY; i = i + 1) begin
          if (e_val[i]) begin
            if (e_age[i] >= AGE_MAX) begin
              // Expired. The station may still be there; the switch has
              // simply stopped having evidence that it is. Forgetting a
              // live station costs a flood, which is cheap. NOT
              // forgetting a dead one costs mis-delivery, which is not.
              e_val[i] <= 1'b0;
              e_age[i] <= 8'd0;
            end else begin
              e_age[i] <= e_age[i] + 8'd1;
            end
          end
        end
        aged_c <= aged_c + n_age_now;
      end

      // ---- then the frame's own evidence ----
      if (fr_valid) begin
        e_val [learn_idx] <= 1'b1;
        e_addr[learn_idx] <= fr_src;
        e_port[learn_idx] <= fr_port;
        e_age [learn_idx] <= 8'd0;          // refreshed by being seen

        if (src_found) begin
          if (moved) relearn_c <= relearn_c + 32'd1;
        end else begin
          learn_c <= learn_c + 32'd1;
          if (evicted) evict_c <= evict_c + 32'd1;
        end

        if (hit) hit_c   <= hit_c   + 32'd1;
        else     flood_c <= flood_c + 32'd1;
      end
    end
  end

endmodule


// =====================================================================
//  usb_hub_route_map -- the same question, answered by an authority.
//
//  CLASSIFICATION: simplified synthesisable teaching RTL.
//  This is NOT a hub. There is no packet path, no speed handling and no
//  suspend logic. It is the routing table, and how a host comes to have
//  one.
//
//  It answers exactly the question eth_learn_table answers -- "which
//  port is this address on?" -- and it is built to be compared with it
//  line for line. The differences are the protocol difference:
//
//    * There is no learning. The map is WRITTEN by the host, which knows
//      the answer because it enumerated the device on that port itself.
//      There is no `fr_src` port here at all: this module never infers
//      anything from traffic, so traffic cannot teach it a lie.
//
//    * There is no aging. An entry is not evidence that decays; it is a
//      record of an assignment, and it stays true until the host changes
//      it. Nothing has to be forgotten to stay correct.
//
//    * There is no eviction. The map is indexed BY ADDRESS over the
//      whole address space, so it cannot be full while addresses remain.
//
//    * THE ONE THAT ACTUALLY MATTERS: there is a `port_event` input.
//      A USB port reports connect and disconnect as a hardware event.
//      When a device leaves, the map is corrected AT THAT INSTANT --
//      before any traffic is sent to the port it left.
//
//  That last mechanism is what Ethernet has no equivalent of. A switch
//  can see its own link go down, but a station that moved from one
//  switch to another produces no event anywhere that says where it went.
//  The only evidence is traffic, and until the station sends some, the
//  switch is confidently wrong.
// =====================================================================
module usb_hub_route_map #(
  parameter integer N_PORT = 4,
  // Addresses 1..N_ADDR are assignable. Address 0 is the default address
  // and is deliberately not routable here: a device at address 0 has not
  // been given an identity yet.
  parameter integer N_ADDR = 8
) (
  input  wire       clk,
  input  wire       rst_n,

  // ---- the host writes the map, because the host assigned it ----
  input  wire       cfg_valid,
  input  wire [7:0] cfg_addr,
  input  wire [$clog2(N_PORT)-1:0] cfg_port,
  input  wire       cfg_remove,

  // ---- the hub reports physical events ----
  //
  // The mechanism with no Ethernet counterpart. `connect` is informational
  // here (the host must still enumerate); `disconnect` is what keeps the
  // map honest, because it invalidates the entry before any traffic can
  // be misrouted to a vacated port.
  input  wire       ev_valid,
  input  wire [$clog2(N_PORT)-1:0] ev_port,
  input  wire       ev_connect,     // 1 = connect, 0 = disconnect

  // ---- the query ----
  input  wire       fr_valid,
  input  wire [7:0] fr_dst,

  output wire       sel_valid,
  output wire [$clog2(N_PORT)-1:0] sel_port,
  output wire       sel_unknown,

  output wire [31:0] n_assigned,
  output wire [31:0] n_removed,
  output wire [31:0] n_hit,
  output wire [31:0] n_unknown,
  output wire [31:0] n_ev_removed,
  output wire [31:0] n_occupied
);

  localparam integer PW = $clog2(N_PORT);

  // Indexed BY ADDRESS. There is no search and no tie to resolve,
  // because the host guarantees one address maps to one port -- it is the
  // only party that can create an entry.
  reg          a_val  [0:N_ADDR];
  reg [PW-1:0] a_port [0:N_ADDR];

  integer i;

  wire in_range = (fr_dst >= 8'd1) && (fr_dst <= N_ADDR[7:0]);
  // Read through a temporary rather than indexing inside the condition:
  // an out-of-range index must not be evaluated at all.
  reg          look_val;
  reg [PW-1:0] look_port;
  always @(*) begin
    look_val  = 1'b0;
    look_port = {PW{1'b0}};
    if (in_range) begin
      look_val  = a_val[fr_dst];
      look_port = a_port[fr_dst];
    end
  end

  assign sel_valid   = fr_valid && look_val;
  assign sel_port    = look_port;
  // An unknown address is not flooded. The host simply has nothing to
  // send it to, and it knows that this is so -- which is the difference
  // between "I have no record" and "I have a record and it is stale".
  assign sel_unknown = fr_valid && !look_val;

  reg [31:0] asg_c, rem_c, hit_c, unk_c, evrem_c;
  assign n_assigned   = asg_c;
  assign n_removed    = rem_c;
  assign n_hit        = hit_c;
  assign n_unknown    = unk_c;
  assign n_ev_removed = evrem_c;

  reg [31:0] occ_now;
  always @(*) begin
    occ_now = 32'd0;
    for (i = 1; i <= N_ADDR; i = i + 1) if (a_val[i]) occ_now = occ_now + 32'd1;
  end
  assign n_occupied = occ_now;

  // How many entries this event will clear. Computed combinationally so
  // the counter takes one write rather than one per entry.
  reg [31:0] n_ev_now;
  always @(*) begin
    n_ev_now = 32'd0;
    if (ev_valid && !ev_connect)
      for (i = 1; i <= N_ADDR; i = i + 1)
        if (a_val[i] && (a_port[i] == ev_port)) n_ev_now = n_ev_now + 32'd1;
  end

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (i = 0; i <= N_ADDR; i = i + 1) begin
        a_val[i]  <= 1'b0;
        a_port[i] <= {PW{1'b0}};
      end
      asg_c   <= 32'd0;
      rem_c   <= 32'd0;
      hit_c   <= 32'd0;
      unk_c   <= 32'd0;
      evrem_c <= 32'd0;
    end else begin
      // ---- a physical disconnect invalidates the entry IMMEDIATELY ----
      //
      // This is the whole mechanism. No traffic was required, no timeout
      // elapsed, and no inference was made: the port said the device had
      // gone, so the record of it went too.
      if (ev_valid && !ev_connect) begin
        for (i = 1; i <= N_ADDR; i = i + 1)
          if (a_val[i] && (a_port[i] == ev_port)) a_val[i] <= 1'b0;
        evrem_c <= evrem_c + n_ev_now;
      end

      // ---- then any host write ----
      if (cfg_valid && (cfg_addr >= 8'd1) && (cfg_addr <= N_ADDR[7:0])) begin
        if (cfg_remove) begin
          a_val[cfg_addr] <= 1'b0;
          rem_c <= rem_c + 32'd1;
        end else begin
          a_val[cfg_addr]  <= 1'b1;
          a_port[cfg_addr] <= cfg_port;
          asg_c <= asg_c + 32'd1;
        end
      end

      if (fr_valid) begin
        if (look_val) hit_c <= hit_c + 32'd1;
        else          unk_c <= unk_c + 32'd1;
      end
    end
  end

endmodule

5. The Measurement

The bench holds ground truth. It decides where each station actually lives, in an array called true_port, and nothing in either design can see it. So "the table is wrong" stops being a figure of speech and becomes a comparison between two arrays — which is the only way to measure what learning costs.

A USB host needs no such array. It assigned every address, so its mapping and the truth are the same object and cannot diverge. That is not a better implementation of the same idea; it is a different idea, available only to a bus with a single authority.

The result

Directed phases only, so every figure is reproducible without a random seed. Identical in all three languages:

destinations that existdelivered to the wrong portrate
eth_learn_table106529427.6%
usb_hub_route_map13000%

And the two named causes, each measured within its own window:

causeframes in the windowmis-delivered
a station moved and had not yet spoken66 (100%)
a source address was forged66 (100%)

Why 0 is a fair number and not a rigged one

The USB side is given nothing extra. Not more traffic, not a shorter timeout, not a cleverer table. It gets exactly one thing the Ethernet side cannot have:

a port event. A USB port reports connect and disconnect as a hardware event, so when a device leaves, the map is corrected at that instant — before any traffic can be sent to the port it vacated.

Phase 8 runs the identical move scenario through both designs to make this checkable rather than assertable. Six frames in the window that cost the Ethernet table six mis-deliveries cost the USB map zero, and the check is not "the map is empty" but "the map does not name a stale port":

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    for (k = 0; k < 6; k = k + 1) begin
      uframe(8'd1);
      ck(obs_uvalid === 1'b0,
         "the map still routed a device whose port reported a disconnect");
    end

The same move, seen by both mechanisms

A station moves from port 0 to port 3. The Ethernet table continues to name port 0 for five cycles until the station transmits, and every frame in that window is mis-delivered. The USB map is invalidated by the port disconnect event at the instant of the move, so it names no port at all and mis-delivers nothing.both correctboth correctthe stale windowthe stale windowconvergedconvergedboth name port 0, correctlyboth name port 0, correctlymoves; the PORT reports itmoves; the PORT reports itswitch still says port 0switch still says port 0station speaks / re-enumeratedstation speaks /re-enumeratedclkreally_onp0p0p3p3p3p3p3p3p3p3eth_saysp0p0p0p0p0p0p0p3p3p3eth_rightport_eventusb_validusb_saysp0p0nonenonenonenonenonep3p3p3usb_wrongt0t1t2t3t4t5t6t7t8t9
At cycle 2 the station moves. The switch has no way to know and keeps naming port 0 for five more cycles, until the station happens to transmit. The USB map is corrected at the same instant by the port's own disconnect event — the difference in the stimulus is the event, and nothing else.

Note what usb_says does in the window: nothing. It is not wrong, it is silent, and the host knows it has no record. That distinction — between "I have no record" and "I have a record and it is stale" — is the entire difference, and it is why the USB row has no flood column at all.

6. Four Ways A Learned Table Is Wrong

Each is a separate directed phase, because each fails differently and needs a different check.

1. Missing — nothing learned yet, so the frame is flooded

Before a station has spoken, the switch has no evidence about it, and the only thing it can do with a frame for that station is send it to every port. Over the full run: 2523 floods.

That is a bandwidth cost on every link and a confidentiality cost on every one of them too — the frame is delivered to everybody, including whoever was not meant to see it. The USB map has no equivalent outcome: an unknown address is answered "no record", not broadcast.

2. Stale — the station moved and has not spoken since

Measured at 6 of 6, 100% inside the window. The switch is behaving exactly as designed and is wrong anyway, and nothing in the protocol can tell it so. The window closes only when the station transmits, which is entirely up to the station.

3. Full — no room to learn, so the flooding is permanent

Five stations, four entries. One address cannot be held, and traffic for it floods forever — not for a window, permanently, until the topology changes. The switch has no way to choose which station deserves the slot, because it has no idea which one matters.

A USB host cannot run out of table: the address space is the table, 127 entries, allocated by the party that owns it.

4. Forged — a frame lied about its source

The table believes the newest evidence, because that is the only policy that can ever converge. So an attacker who sends one frame claiming to be the victim moves the victim's entry to the attacker's port, and the victim's traffic follows. Measured at 6 of 6.

This is not a defect in the module. It is what learning means.

And what aging is for

Aging looks like pure loss: it throws away a correct entry on a timer. Its purpose is case 2, and a suite that only checks that entries disappear has not tested the purpose.

So phase 4c removes a station, ages its entry out, and then checks that traffic for it floods rather than going to the port it left. Flooding is the correct outcome there, and asserting it that way round is what makes the phase meaningful:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    forgetting a live station   costs one flood       -- cheap
    remembering a dead station costs mis-delivery     -- for as long as the
                                                         memory lasts

That asymmetry is the whole justification for a 300-second timer in real switches, and the mutation that removes expiry scores 133 against it.

7. The Testbench (Verilog)

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  Testbench for eth_learn_table.
//
//  THE BENCH HOLDS GROUND TRUTH, WHICH IS THE WHOLE POINT.
//
//  The design's table is an INFERENCE about where each station lives.
//  The bench decides where each station ACTUALLY lives, in `true_port`,
//  and nothing in the design can see that array. So "the table is wrong"
//  stops being a figure of speech and becomes a comparison between two
//  arrays -- which is the only way to measure the cost of learning.
//
//  A USB host needs no equivalent array. It ASSIGNED every address, so
//  its mapping and the truth are the same object and cannot diverge.
//  That is not a better implementation of the same idea; it is a
//  different idea, available only to a bus with a single authority.
//
//  THE SHADOW TABLE IS FORMULATED IN THE OPPOSITE DIRECTION.
//  The design walks its entries DOWNWARDS so the lowest index wins. The
//  model walks UPWARDS and stops at the first hit. Same answer,
//  different derivation.
// =====================================================================
`timescale 1ns/1ps
module tb_lt_v;

  localparam integer N_PORT  = 4;
  localparam integer N_ENTRY = 4;
  localparam integer AGE_MAX = 7;

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

  reg        fr_valid = 1'b0;
  reg [7:0]  fr_src = 8'd0, fr_dst = 8'd0;
  reg [1:0]  fr_port = 2'd0;
  reg        age_tick = 1'b0;

  wire        fwd_valid, fwd_flood, fwd_drop;
  wire [1:0]  fwd_port;
  wire [31:0] n_learn, n_relearn, n_hit, n_flood, n_evict, n_aged, n_occupied;

  eth_learn_table #(.N_PORT(N_PORT), .N_ENTRY(N_ENTRY), .AGE_MAX(AGE_MAX)) dut (
    .clk(clk), .rst_n(rst_n),
    .fr_valid(fr_valid), .fr_src(fr_src), .fr_dst(fr_dst), .fr_port(fr_port),
    .age_tick(age_tick),
    .fwd_valid(fwd_valid), .fwd_port(fwd_port),
    .fwd_flood(fwd_flood), .fwd_drop(fwd_drop),
    .n_learn(n_learn), .n_relearn(n_relearn), .n_hit(n_hit),
    .n_flood(n_flood), .n_evict(n_evict), .n_aged(n_aged),
    .n_occupied(n_occupied)
  );

  // ---- the USB counterpart, driven by the SAME bench ----
  //
  // Same question, answered by an authority instead of by inference. Note
  // there is no src port to drive: this module cannot be taught anything
  // by traffic.
  localparam integer N_ADDR = 8;
  reg        u_cfg_valid = 1'b0, u_cfg_remove = 1'b0;
  reg [7:0]  u_cfg_addr = 8'd0;
  reg [1:0]  u_cfg_port = 2'd0;
  reg        u_ev_valid = 1'b0, u_ev_connect = 1'b0;
  reg [1:0]  u_ev_port = 2'd0;
  reg        u_fr_valid = 1'b0;
  reg [7:0]  u_fr_dst = 8'd0;

  wire       u_sel_valid, u_sel_unknown;
  wire [1:0] u_sel_port;
  wire [31:0] u_n_assigned, u_n_removed, u_n_hit, u_n_unknown,
              u_n_ev_removed, u_n_occupied;

  usb_hub_route_map #(.N_PORT(N_PORT), .N_ADDR(N_ADDR)) udut (
    .clk(clk), .rst_n(rst_n),
    .cfg_valid(u_cfg_valid), .cfg_addr(u_cfg_addr),
    .cfg_port(u_cfg_port), .cfg_remove(u_cfg_remove),
    .ev_valid(u_ev_valid), .ev_port(u_ev_port), .ev_connect(u_ev_connect),
    .fr_valid(u_fr_valid), .fr_dst(u_fr_dst),
    .sel_valid(u_sel_valid), .sel_port(u_sel_port),
    .sel_unknown(u_sel_unknown),
    .n_assigned(u_n_assigned), .n_removed(u_n_removed),
    .n_hit(u_n_hit), .n_unknown(u_n_unknown),
    .n_ev_removed(u_n_ev_removed), .n_occupied(u_n_occupied)
  );

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

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

  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

  // ---- what the design said, sampled at a DEFINED instant ----
  reg       obs_flood, obs_fvalid, obs_fdrop;
  reg [1:0] obs_fport;
  reg       obs_uvalid, obs_uunknown;
  reg [1:0] obs_uport;

  // ---- the shadow table, maintained by the bench ----
  reg        m_val  [0:N_ENTRY-1];
  reg [7:0]  m_addr [0:N_ENTRY-1];
  reg [1:0]  m_port [0:N_ENTRY-1];
  reg [7:0]  m_age  [0:N_ENTRY-1];

  // ---- GROUND TRUTH: where each station really is ----
  //
  // Invisible to the design. 255 means "this station does not exist", so
  // a frame addressed to it can never be mis-delivered, only flooded.
  reg [7:0]  true_port [0:255];

  // ---- the measurements ----
  integer g_frames      = 0;   // frames offered
  integer g_flood       = 0;   // ... flooded because the table did not know
  integer g_fwd         = 0;   // ... forwarded to one port
  integer g_drop        = 0;   // ... dropped as same-port
  integer g_misdeliver  = 0;   // ... forwarded to the WRONG port
  integer g_deliverable = 0;   // frames whose destination actually exists
  integer g_stale_win   = 0;   // mis-deliveries attributable to a move
  integer g_poison_win  = 0;   // mis-deliveries attributable to a forgery

  // ---- the USB side of the same measurement ----
  reg        um_val  [0:N_ADDR];
  reg [1:0]  um_port [0:N_ADDR];
  integer u_frames      = 0;
  integer u_deliverable = 0;
  integer u_hit         = 0;
  integer u_unknown     = 0;
  integer u_misdeliver  = 0;   // forwarded to a port the device has left
  integer u_assigned_c  = 0;   // host writes, accumulated across resets
  integer u_evrem_c     = 0;   // entries cleared by a port event

  // Accumulated across resets for the same reason: the DUT's counters are
  // zeroed by every reset_dut, so a summary that read them directly would
  // report only whatever happened after the last one.
  integer e_learn_c = 0, e_relearn_c = 0, e_hit_c = 0,
          e_flood_c = 0, e_evict_c = 0, e_aged_c = 0;

  // ---------------------------------------------------------------
  //  Model lookup: upward, stop at the first hit.
  // ---------------------------------------------------------------
  function m_hit_f(input [7:0] a);
    integer j; reg f;
    begin
      f = 1'b0;
      for (j = 0; j < N_ENTRY; j = j + 1)
        if (m_val[j] && (m_addr[j] == a) && !f) f = 1'b1;
      m_hit_f = f;
    end
  endfunction

  function [1:0] m_port_f(input [7:0] a);
    integer j; reg f; reg [1:0] p;
    begin
      f = 1'b0; p = 2'd0;
      for (j = 0; j < N_ENTRY; j = j + 1)
        if (m_val[j] && (m_addr[j] == a) && !f) begin p = m_port[j]; f = 1'b1; end
      m_port_f = p;
    end
  endfunction

  function [31:0] m_occ_f;
    input dummy;
    integer j; reg [31:0] c;
    begin
      c = 32'd0;
      for (j = 0; j < N_ENTRY; j = j + 1) if (m_val[j]) c = c + 32'd1;
      m_occ_f = c;
    end
  endfunction

  // Which entry will the model learn into? Upward-and-stop, again the
  // opposite direction from the design's downward walk.
  function [1:0] m_learn_idx(input [7:0] a);
    integer j; reg fs, ff; reg [1:0] si, fi, oi; reg [7:0] oa;
    begin
      fs = 1'b0; ff = 1'b0; si = 2'd0; fi = 2'd0; oi = 2'd0; oa = 8'd0;
      for (j = 0; j < N_ENTRY; j = j + 1) begin
        if (m_val[j] && (m_addr[j] == a) && !fs) begin si = j[1:0]; fs = 1'b1; end
        if (!m_val[j] && !ff) begin fi = j[1:0]; ff = 1'b1; end
        // strictly-greater, walking up, gives the lowest index among the
        // oldest -- the same winner the design's >= walking down picks
        if (m_val[j] && (m_age[j] > oa)) begin oa = m_age[j]; oi = j[1:0]; end
      end
      m_learn_idx = fs ? si : (ff ? fi : oi);
    end
  endfunction

  // ---------------------------------------------------------------
  //  Offer one frame and check the forwarding decision.
  // ---------------------------------------------------------------
  task frame(input [7:0] src, input [7:0] dst, input [1:0] port,
             input why_stale, input why_poison);
    reg        e_hit, e_flood, e_fwd, e_drop, e_known_src, e_moved;
    reg [1:0]  e_port, li;
    reg [31:0] l0, r0, h0, f0;
    integer    j;
    begin
      e_hit   = m_hit_f(dst);
      e_port  = m_port_f(dst);
      e_drop  = e_hit && (e_port == port);
      e_fwd   = e_hit && !e_drop;
      e_flood = !e_hit;
      e_known_src = m_hit_f(src);
      e_moved = e_known_src && (m_port_f(src) != port);

      l0 = n_learn; r0 = n_relearn; h0 = n_hit; f0 = n_flood;

      fr_valid = 1'b1; fr_src = src; fr_dst = dst; fr_port = port;
      #1;

      // ---- capture what the design said WHILE the request is asserted ----
      //
      // Every valid-gated output (fwd_flood, fwd_valid, fwd_drop) is only
      // meaningful while fr_valid is high. A check placed after this task
      // returns reads those wires after fr_valid has been deasserted, and
      // whether it sees the pre- or post-deassert value then depends on
      // delta-cycle ordering -- which differs between simulators and between
      // languages. One mutation scored 1516 in Verilog and 1522 in VHDL for
      // exactly that reason, on benches that were otherwise identical.
      //
      // An assertion whose outcome depends on delta ordering is not an
      // assertion. So the observation is taken here, once, at a defined
      // instant, and every later check uses the captured value.
      obs_flood  = fwd_flood;
      obs_fvalid = fwd_valid;
      obs_fdrop  = fwd_drop;
      obs_fport  = fwd_port;

      // ---- PROPERTY 1: exactly one outcome ----
      //
      // Flood, forward and drop must be mutually exclusive and one of
      // them must happen. A frame with no decision is a frame the switch
      // silently lost.
      ck((fwd_flood + fwd_valid + fwd_drop) == 1,
         "the switch did not reach exactly one forwarding decision");
      // ---- PROPERTY 2: a miss floods ----
      ck(fwd_flood === e_flood,
         "flooding does not correspond to an unknown destination");
      // ---- PROPERTY 3: a hit off the ingress port forwards ----
      ck(fwd_valid === e_fwd, "forwarding does not correspond to a known destination");
      // ---- PROPERTY 4: a hit ON the ingress port is dropped ----
      //
      // Forwarding it back out of the port it came from would be a
      // one-hop loop, and on a real switch it doubles the traffic on that
      // link for no benefit.
      ck(fwd_drop === e_drop, "a frame for the ingress port was not dropped");
      // ---- PROPERTY 5: the chosen port is the learned port ----
      if (e_fwd) ck(fwd_port === e_port, "the frame went to a port the table did not name");

      // ---- THE MEASUREMENT: was the decision RIGHT? ----
      //
      // Not "did it match the table" -- that is property 5. This asks
      // whether the table matched REALITY, which is a question only the
      // bench can ask because only the bench knows where the station is.
      g_frames = g_frames + 1;
      if (true_port[dst] !== 8'hFF) begin
        g_deliverable = g_deliverable + 1;
        if (fwd_valid && (fwd_port !== true_port[dst][1:0])) begin
          g_misdeliver = g_misdeliver + 1;
          if (why_stale)  g_stale_win  = g_stale_win  + 1;
          if (why_poison) g_poison_win = g_poison_win + 1;
        end
      end
      if (fwd_flood) g_flood = g_flood + 1;
      if (fwd_valid) g_fwd   = g_fwd + 1;
      if (fwd_drop)  g_drop  = g_drop + 1;
      if (e_hit) e_hit_c = e_hit_c + 1; else e_flood_c = e_flood_c + 1;
      if (e_known_src) begin
        if (e_moved) e_relearn_c = e_relearn_c + 1;
      end else begin
        e_learn_c = e_learn_c + 1;
        if (m_occ_f(0) == N_ENTRY) e_evict_c = e_evict_c + 1;
      end

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

      // ---- update the model, then compare occupancy ----
      li = m_learn_idx(src);
      m_val[li]  = 1'b1;
      m_addr[li] = src;
      m_port[li] = port;
      m_age[li]  = 8'd0;

      // ---- PROPERTY 6: the tables agree on how full they are ----
      //
      // A weaker check than entry-by-entry equality and a deliberately
      // chosen one: the two tables use opposite search directions, so
      // they may legitimately place the same address in different slots
      // after an eviction. What they may NOT disagree about is how many
      // addresses they know, and every lookup above already checks the
      // answers they give.
      ck(n_occupied === m_occ_f(0),
         "the design and the model disagree about how many addresses are known");

      // ---- PROPERTY 7: learning is counted correctly ----
      if (e_known_src) begin
        ck(n_learn == l0, "a known source was counted as a new address");
        ck(n_relearn == r0 + (e_moved ? 32'd1 : 32'd0),
           "a station move was miscounted");
      end else begin
        ck(n_learn == l0 + 32'd1, "a new address was not counted");
        ck(n_relearn == r0, "a new address was counted as a move");
      end
      // ---- PROPERTY 8: hits and floods are counted correctly ----
      ck(n_hit   == h0 + (e_hit ? 32'd1 : 32'd0), "hit counter disagrees with the model");
      ck(n_flood == f0 + (e_hit ? 32'd0 : 32'd1), "flood counter disagrees with the model");

      steps = steps + 1;
    end
  endtask

  // ---------------------------------------------------------------
  //  One aging tick, mirrored in the model.
  // ---------------------------------------------------------------
  task tick;
    integer j;
    begin
      age_tick = 1'b1;
      @(posedge clk); #1;
      age_tick = 1'b0;
      for (j = 0; j < N_ENTRY; j = j + 1) begin
        if (m_val[j]) begin
          if (m_age[j] >= AGE_MAX) begin
            m_val[j] = 1'b0; m_age[j] = 8'd0;
            e_aged_c = e_aged_c + 1;
          end else begin
            m_age[j] = m_age[j] + 8'd1;
          end
        end
      end
      ck(n_occupied === m_occ_f(0),
         "the design and the model disagree about occupancy after aging");
      steps = steps + 1;
    end
  endtask

  // ---------------------------------------------------------------
  //  Host writes the map. It knows the answer because it enumerated the
  //  device on that port -- there is nothing to infer.
  // ---------------------------------------------------------------
  task umap_cfg(input [7:0] a, input [1:0] p, input rem);
    begin
      u_cfg_valid = 1'b1; u_cfg_addr = a; u_cfg_port = p; u_cfg_remove = rem;
      @(posedge clk); #1;
      u_cfg_valid = 1'b0;
      if ((a >= 8'd1) && (a <= N_ADDR)) begin
        if (rem) um_val[a] = 1'b0;
        else begin um_val[a] = 1'b1; um_port[a] = p; u_assigned_c = u_assigned_c + 1; end
      end
      steps = steps + 1;
    end
  endtask

  // ---------------------------------------------------------------
  //  A physical port event. THE mechanism with no Ethernet counterpart:
  //  a disconnect corrects the map before any traffic can be misrouted.
  // ---------------------------------------------------------------
  task uev(input [1:0] p, input conn);
    integer j;
    reg [31:0] e0, expect_cleared;
    begin
      e0 = u_n_ev_removed;
      expect_cleared = 32'd0;
      if (!conn)
        for (j = 1; j <= N_ADDR; j = j + 1)
          if (um_val[j] && (um_port[j] == p)) expect_cleared = expect_cleared + 32'd1;

      u_ev_valid = 1'b1; u_ev_port = p; u_ev_connect = conn;
      @(posedge clk); #1;
      u_ev_valid = 1'b0;

      if (!conn)
        for (j = 1; j <= N_ADDR; j = j + 1)
          if (um_val[j] && (um_port[j] == p)) begin
            um_val[j] = 1'b0;
            u_evrem_c = u_evrem_c + 1;
          end

      // ---- PROPERTY U4: a disconnect clears every entry on that port ----
      //
      // Immediately, with no traffic and no timeout. This is the property
      // the Ethernet table cannot have, because nothing tells it.
      ck(u_n_ev_removed == e0 + expect_cleared,
         "a disconnect event did not clear the entries on that port");
      ck(u_n_occupied === um_occ(0),
         "the map and the model disagree about occupancy after an event");
      steps = steps + 1;
    end
  endtask

  // ---------------------------------------------------------------
  //  Query the map and check the answer -- and whether it was RIGHT.
  // ---------------------------------------------------------------
  task uframe(input [7:0] dst);
    reg e_val_x; reg [1:0] e_port_x;
    reg [31:0] h0, k0;
    begin
      e_val_x  = ((dst >= 8'd1) && (dst <= N_ADDR)) ? um_val[dst]  : 1'b0;
      e_port_x = ((dst >= 8'd1) && (dst <= N_ADDR)) ? um_port[dst] : 2'd0;
      h0 = u_n_hit; k0 = u_n_unknown;

      u_fr_valid = 1'b1; u_fr_dst = dst;
      #1;

      // Captured for the same reason as in frame(): sel_valid is gated
      // by fr_valid, so it is only meaningful at this instant.
      obs_uvalid   = u_sel_valid;
      obs_uunknown = u_sel_unknown;
      obs_uport    = u_sel_port;

      // ---- PROPERTY U1: a hit means the host assigned that address ----
      ck(u_sel_valid === e_val_x,
         "usb map sel_valid does not mean the address was assigned");
      // ---- PROPERTY U2: the port is the assigned port ----
      if (e_val_x) ck(u_sel_port === e_port_x,
                      "usb map named a port the host did not assign");
      // ---- PROPERTY U3: valid and unknown are exact complements ----
      //
      // There is no third outcome. In particular there is no FLOOD: an
      // unknown address is not sent everywhere, because the host knows it
      // never assigned it.
      ck((u_sel_valid ^ u_sel_unknown) === 1'b1,
         "usb map did not reach exactly one outcome");
      // ---- PROPERTY U6: address 0 and out-of-range are never routed ----
      //
      // Address 0 is the default address: a device there has no identity
      // yet, so routing to it would route to whichever device is
      // mid-enumeration.
      if ((dst == 8'd0) || (dst > N_ADDR))
        ck(u_sel_valid === 1'b0, "usb map routed an unassignable address");

      // ---- THE MEASUREMENT, identical in form to the Ethernet one ----
      u_frames = u_frames + 1;
      if (true_port[dst] !== 8'hFF) begin
        u_deliverable = u_deliverable + 1;
        if (u_sel_valid && (u_sel_port !== true_port[dst][1:0]))
          u_misdeliver = u_misdeliver + 1;
      end
      if (u_sel_valid)   u_hit = u_hit + 1;
      if (u_sel_unknown) u_unknown = u_unknown + 1;

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

      ck(u_n_hit     == h0 + (e_val_x ? 32'd1 : 32'd0), "usb map hit counter wrong");
      ck(u_n_unknown == k0 + (e_val_x ? 32'd0 : 32'd1), "usb map unknown counter wrong");
      steps = steps + 1;
    end
  endtask

  function [31:0] um_occ;
    input dummy;
    integer j; reg [31:0] c;
    begin
      c = 32'd0;
      for (j = 1; j <= N_ADDR; j = j + 1) if (um_val[j]) c = c + 32'd1;
      um_occ = c;
    end
  endfunction

  task reset_dut;
    integer j;
    begin
      rst_n = 1'b0; fr_valid = 1'b0; age_tick = 1'b0;
      @(posedge clk); @(posedge clk);
      rst_n = 1'b1;
      @(posedge clk); #1;
      for (j = 0; j < N_ENTRY; j = j + 1) begin
        m_val[j] = 1'b0; m_addr[j] = 8'd0; m_port[j] = 2'd0; m_age[j] = 8'd0;
      end
        for (j = 0; j <= N_ADDR; j = j + 1) begin
        um_val[j] = 1'b0; um_port[j] = 2'd0;
      end
    end
  endtask

  task nobody_exists;
    integer j;
    begin
      for (j = 0; j < 256; j = j + 1) true_port[j] = 8'hFF;
    end
  endtask

  // ---- exhaustive reach ----
  //
  // mask(16) x src(8) x dst(8) x port(4) = 4096. Every dimension is an
  // independent input with no forbidden combinations: the mask selects
  // which of the four stations have spoken, and src/dst/port are the
  // probe frame. So the denominator is exactly 4096.
  reg reach [0:4095];
  integer nr, ri;

  // USB map reach: ev_mode(5) x mask(16) x query(10) = 800. ev_mode 0 is
  // "no event"; 1..4 disconnect port ev_mode-1. Independent dimensions,
  // no forbidden combinations, so the denominator is exactly 800.
  reg reach_u [0:799];
  integer nru, rui, evm, q;

  integer mask, s, d, p, k, j;
  reg [31:0] snap;

  initial begin
    for (ri = 0; ri < 4096; ri = ri + 1) reach[ri] = 1'b0;
    seed = 32'd28003;
    nobody_exists;

    reset_dut;

    // =============================================================
    //  PHASE 1 (DIRECTED, EXHAUSTIVE) -- every table state crossed
    //  with every probe frame.
    //
    //  For each of the 16 subsets of {station 1..4}, the members speak
    //  once (station i from port i-1, so the table learns it), and then
    //  all 8 x 8 x 4 probe frames are offered against that table.
    // =============================================================
    for (mask = 0; mask < 16; mask = mask + 1) begin
      reset_dut;
      nobody_exists;
      // populate: station (j+1) lives on port j and says so
      for (j = 0; j < 4; j = j + 1) begin
        if (mask[j]) begin
          true_port[j+1] = j[7:0];
          frame((j+1), 8'hF0, j[1:0], 1'b0, 1'b0);   // dst F0 is nobody
        end
      end
      for (s = 0; s < 8; s = s + 1)
      for (d = 0; d < 8; d = d + 1)
      for (p = 0; p < 4; p = p + 1) begin
        // The probe's source is learned too -- a switch cannot choose not
        // to learn. So each probe perturbs the table, which is realistic
        // and is why the model is updated in lockstep rather than
        // recomputed from the mask.
        frame(s[7:0], d[7:0], p[1:0], 1'b0, 1'b0);
        ri = ((mask * 8 + s) * 8 + d) * 4 + p;
        reach[ri] = 1'b1;
      end
    end

    // =============================================================
    //  PHASE 2 (DIRECTED) -- A STATION MOVES.
    //
    //  THE measurement. Station 1 lives on port 0 and has spoken, so the
    //  table knows it. Then it is unplugged and moved to port 3 -- and it
    //  does not immediately say anything, because a station that has just
    //  been plugged in usually has nothing to send.
    //
    //  Every frame addressed to station 1 in that window goes to port 0,
    //  where station 1 is not. The switch is behaving exactly as designed
    //  and is wrong anyway, and nothing in the protocol can tell it so.
    //
    //  A USB host cannot be in this state. It assigned the address; if
    //  the device moved, the host performed the move.
    // =============================================================
    reset_dut;
    nobody_exists;
    true_port[1] = 8'd0;
    frame(8'd1, 8'hF0, 2'd0, 1'b0, 1'b0);       // station 1 speaks from port 0
    true_port[2] = 8'd1;
    frame(8'd2, 8'd1, 2'd1, 1'b0, 1'b0);        // station 2 -> 1, correct
    ck(obs_fport === 2'd0, "traffic for station 1 did not go to port 0");

    // ---- the move ----
    true_port[1] = 8'd3;                        // physically now on port 3
    // Six frames for station 1 while it stays silent. All six are
    // mis-delivered, and the measurement counts them.
    for (k = 0; k < 6; k = k + 1)
      frame(8'd2, 8'd1, 2'd1, 1'b1, 1'b0);      // why_stale = 1

    // ---- the station finally speaks, and the table corrects itself ----
    snap = n_relearn;
    frame(8'd1, 8'hF0, 2'd3, 1'b0, 1'b0);
    ck(n_relearn == snap + 32'd1, "a station moving ports was not recorded as a move");
    frame(8'd2, 8'd1, 2'd1, 1'b0, 1'b0);
    ck(obs_fport === 2'd3, "after the station spoke, traffic still went to the old port");

    // =============================================================
    //  PHASE 3 (DIRECTED) -- AGING.
    //
    //  An entry with no refreshing evidence is discarded. That LOSES
    //  information and is the correct policy anyway: forgetting a live
    //  station costs one flood, while remembering a dead one costs
    //  mis-delivery for as long as the memory lasts.
    // =============================================================
    reset_dut;
    nobody_exists;
    true_port[1] = 8'd0;
    true_port[2] = 8'd1;
    frame(8'd1, 8'hF0, 2'd0, 1'b0, 1'b0);
    frame(8'd2, 8'hF0, 2'd1, 1'b0, 1'b0);
    ck(n_occupied === 32'd2, "two stations spoke and the table does not hold two");
    // Age it out. AGE_MAX + 2 ticks is enough for an entry created at
    // age 0 to pass AGE_MAX and be discarded.
    for (k = 0; k < AGE_MAX + 2; k = k + 1) tick;
    ck(n_occupied === 32'd0, "entries did not expire");
    // and a frame for a forgotten station floods rather than going astray
    frame(8'd3, 8'd1, 2'd2, 1'b0, 1'b0);
    ck(obs_flood === 1'b1, "a frame for an expired station was not flooded");

    // refreshing keeps an entry alive indefinitely
    reset_dut;
    nobody_exists;
    true_port[1] = 8'd0;
    for (k = 0; k < 3 * AGE_MAX; k = k + 1) begin
      frame(8'd1, 8'hF0, 2'd0, 1'b0, 1'b0);
      tick;
    end
    ck(n_occupied === 32'd1, "a station that kept speaking was still forgotten");

    // =============================================================
    //  PHASE 4 (DIRECTED) -- THE TABLE IS FULL.
    //
    //  Five stations, four entries. One address cannot be held, so
    //  traffic for it floods forever -- not for a window, permanently,
    //  until the topology changes.
    //
    //  A USB host cannot run out of table: the address space IS the
    //  table, 127 entries, allocated by the same party that owns it.
    // =============================================================
    reset_dut;
    nobody_exists;
    for (k = 0; k < 4; k = k + 1) begin
      true_port[k+1] = k[7:0];
      frame((k+1), 8'hF0, k[1:0], 1'b0, 1'b0);
    end
    ck(n_occupied === 32'd4, "four stations did not fill four entries");
    snap = n_evict;
    true_port[5] = 8'd0;
    frame(8'd5, 8'hF0, 2'd0, 1'b0, 1'b0);       // a fifth station
    ck(n_evict == snap + 32'd1, "a full table did not report an eviction");
    ck(n_occupied === 32'd4, "an eviction changed how many entries are valid");
    // Exactly one of the five is now unknown, and the switch has no way
    // to choose which one deserves the slot.
    snap = n_flood;
    for (k = 1; k <= 5; k = k + 1)
      frame(8'd6, k[7:0], 2'd2, 1'b0, 1'b0);
    ck(n_flood >= snap + 32'd1,
       "with five stations and four entries, nothing flooded");

    // =============================================================
    //  PHASE 4b (DIRECTED, EXHAUSTIVE over which entry is oldest)
    //
    //  A mutation that always evicts entry 0 instead of the oldest scored
    //  ZERO against everything above, and the reason is worth more than
    //  the phase: entry 0 held the oldest station anyway, because the
    //  first address learned takes the lowest free slot. The two policies
    //  agreed on every case the suite could reach.
    //
    //  The fix is not more stimulus but the RIGHT stimulus: make each of
    //  the four entries be the oldest in turn. Populate, age everything,
    //  then refresh every station EXCEPT one -- which leaves that one
    //  uniquely oldest, in a known entry. Three of the four arrangements
    //  put the oldest somewhere other than entry 0, and those are the
    //  three that can tell the two policies apart.
    // =============================================================
    for (j = 0; j < 4; j = j + 1) begin
      reset_dut;
      nobody_exists;
      for (k = 0; k < 4; k = k + 1) true_port[k+1] = k[7:0];

      // stations 1..4 speak in order, one tick apart, so their ages are
      // staggered and entry i holds station i+1
      frame(8'd1, 8'hF0, 2'd0, 1'b0, 1'b0); tick;
      frame(8'd2, 8'hF0, 2'd1, 1'b0, 1'b0); tick;
      frame(8'd3, 8'hF0, 2'd2, 1'b0, 1'b0); tick;
      frame(8'd4, 8'hF0, 2'd3, 1'b0, 1'b0); tick;

      // everyone speaks again EXCEPT station j+1, so station j+1 is now
      // uniquely the oldest entry in the table
      for (k = 0; k < 4; k = k + 1)
        if (k != j) frame((k+1), 8'hF0, k[1:0], 1'b0, 1'b0);
      ck(n_occupied === 32'd4, "the table should hold four stations here");

      // a fifth station forces exactly one eviction
      true_port[5] = 8'd0;
      snap = n_evict;
      frame(8'd5, 8'hF0, 2'd0, 1'b0, 1'b0);
      ck(n_evict == snap + 32'd1, "a full table did not report an eviction");
      ck(n_occupied === 32'd4, "an eviction changed how many entries are valid");

      // Probe all four original stations. The bench's model tracks which
      // entry it expects to have been reused, so PROPERTY 2 inside
      // frame() compares flood-versus-hit station by station -- which is
      // what distinguishes "evict the oldest" from "evict entry 0".
      //
      // Probing from station 5, which is already in the table, so the
      // probes themselves cannot evict anything.
      for (k = 0; k < 4; k = k + 1)
        frame(8'd5, (k+1), 2'd0, 1'b0, 1'b0);

      // And the station that was NOT refreshed is the one that should be
      // gone. Stated directly as well as through the model, because this
      // is the property the phase exists for.
      frame(8'd5, (j+1), 2'd0, 1'b0, 1'b0);
      ck(obs_flood === 1'b1,
         "the entry that was not refreshed was not the one evicted");
    end

    // =============================================================
    //  PHASE 4c (DIRECTED) -- WHAT AGING IS FOR.
    //
    //  Aging looks like pure loss: it throws away a correct entry. Its
    //  purpose is the case below, and a suite that only checks that
    //  entries disappear has not tested the purpose.
    //
    //  A station is removed from the network. Nothing tells the switch.
    //  The ONLY thing that stops its traffic being sent to a port it has
    //  left is the entry expiring -- so after expiry the frame must
    //  flood, and flooding is the correct outcome rather than a failure.
    // =============================================================
    for (p = 0; p < 4; p = p + 1) begin
      reset_dut;
      nobody_exists;
      true_port[1] = p[7:0];
      frame(8'd1, 8'hF0, p[1:0], 1'b0, 1'b0);
      ck(n_occupied === 32'd1, "the station was not learned");
      // it is physically removed, and nothing reports that
      true_port[1] = 8'hFF;
      for (k = 0; k < AGE_MAX + 2; k = k + 1) tick;
      ck(n_occupied === 32'd0, "the entry for a departed station did not expire");
      // four frames for the departed station, from a port that is not p
      for (k = 0; k < 4; k = k + 1) begin
        frame(8'd7, 8'd1, ((p + 1) % 4), 1'b0, 1'b0);
        ck(obs_flood === 1'b1,
           "traffic for a departed station was still sent to the port it left");
        ck(obs_fvalid === 1'b0,
           "the table named a port for a station that had expired");
      end
    end

    // =============================================================
    //  PHASE 5 (DIRECTED) -- A FORGED SOURCE ADDRESS.
    //
    //  The table believes the newest evidence, because that is the only
    //  policy that converges. So an attacker who sends ONE frame claiming
    //  to be the victim moves the victim's entry to the attacker's port,
    //  and the victim's traffic follows.
    //
    //  This is not a defect in this module. It is what learning MEANS,
    //  and it is why real switches need port security, sticky MACs and
    //  802.1X -- three mechanisms that exist to put an authority back
    //  into a protocol that does not have one.
    //
    //  USB has no analogue. A device cannot assign itself an address, so
    //  there is no statement it can make that the host would believe.
    // =============================================================
    reset_dut;
    nobody_exists;
    true_port[1] = 8'd0;   // the victim, on port 0
    true_port[9] = 8'd2;   // the attacker, on port 2
    frame(8'd1, 8'hF0, 2'd0, 1'b0, 1'b0);       // victim speaks
    true_port[2] = 8'd1;
    frame(8'd2, 8'd1, 2'd1, 1'b0, 1'b0);        // traffic reaches the victim
    ck(obs_fport === 2'd0, "traffic for the victim did not reach its real port");

    // ---- one forged frame ----
    frame(8'd1, 8'hF0, 2'd2, 1'b0, 1'b0);       // "I am station 1", from port 2
    // Now every frame for the victim goes to the attacker. Counted
    // against the poisoning measurement.
    for (k = 0; k < 6; k = k + 1)
      frame(8'd2, 8'd1, 2'd1, 1'b0, 1'b1);      // why_poison = 1
    ck(obs_fport === 2'd2, "the forged frame did not redirect the victim's traffic");

    // =============================================================
    //  PHASE 6 (RANDOM)
    // =============================================================
`ifndef DIRECTED_ONLY
    reset_dut;
    nobody_exists;
    for (k = 1; k <= 6; k = k + 1) true_port[k] = (k - 1) % 4;
    for (k = 0; k < 600; k = k + 1) begin
      if ((urand(0) % 11) == 0) tick;
      frame(1 + (urand(0) % 6), 1 + (urand(0) % 6), (urand(0) % 4), 1'b0, 1'b0);
    end
`endif

    // =============================================================
    //  PHASE 7 (DIRECTED, EXHAUSTIVE) -- the USB map.
    //
    //  Every subset of assigned addresses, crossed with every port
    //  disconnect event and every query. The event dimension is the one
    //  that matters: it is the mechanism Ethernet has no counterpart for,
    //  so it has to be swept rather than demonstrated once.
    // =============================================================
    for (evm = 0; evm < 5; evm = evm + 1)
    for (mask = 0; mask < 16; mask = mask + 1) begin
      reset_dut;
      nobody_exists;
      // the host assigns address (j+1) to port j for each member
      for (j = 0; j < 4; j = j + 1) begin
        if (mask[j]) begin
          true_port[j+1] = j[7:0];
          umap_cfg((j+1), j[1:0], 1'b0);
        end
      end
      // then, in four of the five modes, a device is unplugged. The bench
      // updates ground truth FIRST, so a map that failed to react would
      // be measured as mis-delivering rather than merely as out of date.
      if (evm > 0) begin
        for (j = 1; j <= 4; j = j + 1)
          if (true_port[j] === (evm - 1)) true_port[j] = 8'hFF;
        uev((evm - 1), 1'b0);
      end
      for (q = 0; q < 10; q = q + 1) begin
        uframe(q[7:0]);
        rui = (evm * 16 + mask) * 10 + q;
        reach_u[rui] = 1'b1;
      end
    end

    // =============================================================
    //  PHASE 8 (DIRECTED) -- THE SAME MOVE, ON BOTH DESIGNS.
    //
    //  Phase 2 moved a station and measured six mis-delivered frames. The
    //  identical scenario is now run through the USB map, and the only
    //  difference in the stimulus is the one the protocol actually
    //  provides: the port reports the disconnect.
    //
    //  Nothing else is given to the USB side. It gets no extra traffic, no
    //  shorter timeout and no cleverer table -- only the event.
    // =============================================================
    reset_dut;
    nobody_exists;
    true_port[1] = 8'd0;
    umap_cfg(8'd1, 2'd0, 1'b0);       // host enumerated device 1 on port 0
    uframe(8'd1);
    ck(obs_uport === 2'd0, "the map did not route device 1 to port 0");

    // ---- the move: the device is unplugged from port 0 ----
    true_port[1] = 8'hFF;             // in transit, physically nowhere
    uev(2'd0, 1'b0);                  // the PORT reports it
    // Six frames in the window that cost Ethernet six mis-deliveries.
    for (k = 0; k < 6; k = k + 1) begin
      uframe(8'd1);
      // ---- THE COMPARISON ----
      // Not "the map is empty" but "the map does not name a stale port".
      // A map that still answered would be answering with port 0, where
      // the device provably is not.
      ck(obs_uvalid === 1'b0,
         "the map still routed a device whose port reported a disconnect");
    end

    // ---- it is plugged into port 3 and re-enumerated ----
    uev(2'd3, 1'b1);                  // connect event
    true_port[1] = 8'd3;
    umap_cfg(8'd1, 2'd3, 1'b0);       // the host assigns it again
    uframe(8'd1);
    ck(obs_uport === 2'd3, "after re-enumeration the map did not follow the device");

    nr = 0; for (ri = 0; ri < 4096; ri = ri + 1) if (reach[ri]) nr = nr + 1;
    nru = 0; for (rui = 0; rui < 800; rui = rui + 1) if (reach_u[rui]) nru = nru + 1;

    $display("steps=%0d checks=%0d reach_eth=%0d/4096 reach_usb=%0d/800 errors=%0d",
             steps, checks, nr, nru, errors);
    $display("[eth] learned=%0d relearned=%0d hits=%0d floods=%0d evictions=%0d aged=%0d",
             e_learn_c, e_relearn_c, e_hit_c, e_flood_c, e_evict_c, e_aged_c);
    $display("[eth] frames=%0d forwarded=%0d flooded=%0d dropped=%0d",
             g_frames, g_fwd, g_flood, g_drop);
    $display("--- what learning costs that assignment does not ---");
    $display("[eth] frames whose destination exists   = %0d", g_deliverable);
    $display("[eth] delivered to the WRONG port       = %0d", g_misdeliver);
    $display("[eth]   ... because a station had moved = %0d", g_stale_win);
    $display("[eth]   ... because a source was forged = %0d", g_poison_win);
    $display("--- the same question, answered by an authority ---");
    $display("[usb] assigned=%0d removed_by_PORT_EVENT=%0d",
             u_assigned_c, u_evrem_c);
    $display("[usb] queries=%0d answered=%0d unknown=%0d flooded=0 (no such outcome)",
             u_frames, u_hit, u_unknown);
    $display("[usb] queries whose device exists      = %0d", u_deliverable);
    $display("[usb] routed to the WRONG port         = %0d", u_misdeliver);
    if (nr != 4096 || nru != 800) 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

8. The Assertion That Was Not An Assertion

Worth its own section, because it is the most transferable thing this chapter found and it was invisible until three languages disagreed.

Mutation M9 scored 1516 in Verilog and SystemVerilog and 1522 in VHDL. Every other directed column matched exactly, the benches had identical check counts (43,673), and the mutation was textually equivalent in all three files.

The cause: every valid-gated output is only meaningful while its request is asserted. The bench's frame() and uframe() tasks deassert the request before returning, so a check placed after the call reads those outputs after the deassert — and whether it then sees the pre- or post-deassert value depends on delta-cycle ordering, which differs between languages.

This is also an argument for the three-language discipline as verification rather than as presentation. The defect was not findable from any single language's run — every one of them passed. It was findable only because two columns that had to be equal were not.

9. SystemVerilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  eth_learn_table -- SystemVerilog.
//
//  Same hardware contract as the Verilog file: same ports, same widths,
//  same reset values, same cycle-by-cycle behaviour. What changes is
//  `always_comb` and `always_ff` in place of `always @(*)` and
//  `always @(posedge ...)`, and loop variables declared in the loop.
//
//  The table is still four PER-FIELD arrays rather than one array of
//  structs, and that is deliberate rather than a leftover: a variable
//  field-select into an unpacked array of packed structs aborts the
//  Icarus elaborator, so the struct version cannot be simulated with the
//  tool these numbers were measured on.
//
//  THE MECHANISM ETHERNET NEEDS AND USB DOES NOT.
//
//  CLASSIFICATION: simplified synthesisable teaching RTL.
//  This is NOT a switch. There is no MAC, no CRC, no VLAN, no spanning
//  tree, no queueing and no multicast handling. It is one mechanism: the
//  forwarding table, and how a switch comes to have one.
//
//  WHY THIS MECHANISM IS WORTH RTL
//  -------------------------------
//  USB has exactly one host, and that host ASSIGNS every address. A USB
//  device cannot choose its own address, cannot keep an address across a
//  bus reset, and cannot be reached at an address the host did not hand
//  out. The mapping from address to port is therefore KNOWN, by
//  construction, to the only party that needs it.
//
//  Ethernet has no host. There is no authority to assign addresses and
//  no authority to be told where anything is. A switch is handed a wire
//  and must work out the topology by itself, which it does by the only
//  means available: it WATCHES. Every frame carries a source address,
//  and the port it arrived on is evidence of where that address lives.
//
//      USB:      the mapping is ASSIGNED, so it is correct by authority.
//      Ethernet: the mapping is INFERRED, so it is correct by evidence
//                -- and evidence goes stale, can be missing, and can be
//                manufactured.
//
//  That is the trade this module measures. Learning is what lets
//  Ethernet span networks with no central authority at all, which USB
//  cannot do in principle. The price is a table that can be WRONG, and
//  the chapter puts numbers on all four ways it can be wrong:
//
//      1. missing   -- nothing learned yet, so the frame is FLOODED
//      2. stale     -- the station moved and has not spoken since
//      3. full      -- no room to learn, so flooding is permanent
//      4. forged    -- a frame lied about its source and moved an entry
//
//  A table you were handed cannot be any of those things. A table you
//  learned can be all four.
// =====================================================================
module eth_learn_table #(
  parameter int N_PORT  = 4,
  parameter int N_ENTRY = 4,
  // Ticks before an unrefreshed entry is discarded. Real switches use
  // 300 seconds; the value only has to be small enough to reach in
  // simulation and large enough that aging is not the common case.
  parameter int AGE_MAX = 7
) (
  input  logic       clk,
  input  logic       rst_n,

  // ---- one frame arriving ----
  input  logic       fr_valid,
  input  logic [7:0] fr_src,     // who sent it: this is the EVIDENCE
  input  logic [7:0] fr_dst,     // who it is for: this is the QUERY
  input  logic [$clog2(N_PORT)-1:0] fr_port,   // which port it arrived on

  // ---- the aging clock, one tick at a time ----
  input  logic       age_tick,

  // ---- the forwarding decision ----
  output logic       fwd_valid,  // forward to exactly one port
  output logic [$clog2(N_PORT)-1:0] fwd_port,
  output logic       fwd_flood,  // destination unknown: send everywhere
  output logic       fwd_drop,   // destination is on the ingress port

  // ---- observability ----
  output logic [31:0] n_learn,     // a new address was recorded
  output logic [31:0] n_relearn,   // a known address moved port
  output logic [31:0] n_hit,       // the table answered
  output logic [31:0] n_flood,     // the table could not answer
  output logic [31:0] n_evict,     // a live entry was displaced
  output logic [31:0] n_aged,      // an entry expired
  output logic [31:0] n_occupied   // how many entries are currently valid
);

  localparam int PW = $clog2(N_PORT);
  localparam int EW = $clog2(N_ENTRY);

  // The table. 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           e_val  [N_ENTRY];
  logic [7:0]     e_addr [N_ENTRY];
  logic [PW-1:0]  e_port [N_ENTRY];
  logic [7:0]     e_age  [N_ENTRY];


  // -------------------------------------------------------------------
  //  LOOKUP (combinational) -- does the table know where fr_dst lives?
  //
  //  Walk downwards so the LOWEST matching entry wins. Addresses are
  //  supposed to be unique in the table, and the design maintains that;
  //  the direction is fixed anyway so the duplicate case has a defined
  //  answer rather than an undefined one.
  // -------------------------------------------------------------------
  logic          hit;
  logic [PW-1:0] hit_port;
  always_comb begin
    hit      = 1'b0;
    hit_port = '0;
    for (int i = N_ENTRY - 1; i >= 0; i--) begin
      if (e_val[i] && (e_addr[i] == fr_dst)) begin
        hit      = 1'b1;
        hit_port = e_port[i];
      end
    end
  end

  // A frame whose destination is on the port it arrived from must NOT be
  // sent back out that port. Real switches drop it, and so does this:
  // forwarding it would create a loop of exactly one hop.
  // Declared and assigned SEPARATELY. `logic x = expr;` is a one-shot
  // variable initialiser in SystemVerilog, not a continuous assignment --
  // the Verilog `wire x = expr;` this came from IS continuous, so the
  // translation is not mechanical. Left as an initialiser, every lookup in
  // this file missed and the bench reported 29,580 failures against a
  // design that was correct.
  logic dst_is_ingress;
  assign dst_is_ingress = hit && (hit_port == fr_port);

  assign fwd_drop  = fr_valid && dst_is_ingress;
  assign fwd_valid = fr_valid && hit && !dst_is_ingress;
  assign fwd_port  = hit_port;
  // The cost of not being told. With no entry there is no choice but to
  // send the frame to every port -- which is a bandwidth cost on every
  // link and a confidentiality cost on every one of them too.
  assign fwd_flood = fr_valid && !hit;

  // -------------------------------------------------------------------
  //  LEARN (combinational index, one registered write)
  //
  //  Three cases, in priority order:
  //    1. fr_src is already in the table  -> refresh it, and move it if
  //       the port changed (that is a station having moved)
  //    2. there is a free entry           -> take it
  //    3. the table is full               -> evict the OLDEST entry
  //
  //  The index is computed combinationally and written exactly once.
  //  Writing inside the search loop would produce several non-blocking
  //  assignments to the same register, where the last one silently wins.
  // -------------------------------------------------------------------
  logic          src_found;
  logic [EW-1:0] src_idx;
  logic          free_found;
  logic [EW-1:0] free_idx;
  logic [EW-1:0] old_idx;
  logic [7:0]    old_age;

  always_comb begin
    src_found  = 1'b0;
    src_idx    = '0;
    free_found = 1'b0;
    free_idx   = '0;
    old_idx    = '0;
    old_age    = 8'd0;
    for (int i = N_ENTRY - 1; i >= 0; i--) begin
      if (e_val[i] && (e_addr[i] == fr_src)) begin
        src_found = 1'b1;
        src_idx   = EW'(i);
      end
      if (!e_val[i]) begin
        free_found = 1'b1;
        free_idx   = EW'(i);
      end
      // >= so the lowest index wins a tie, matching the walk direction
      if (e_val[i] && (e_age[i] >= old_age)) begin
        old_age = e_age[i];
        old_idx = EW'(i);
      end
    end
  end

  logic [EW-1:0] learn_idx;
  assign learn_idx = src_found  ? src_idx  :
                     free_found ? free_idx : old_idx;
  // A station that moved: the address is known but the evidence now
  // points somewhere else. The table believes the newest evidence, which
  // is the only policy that can ever converge -- and is also exactly
  // what makes the table forgeable.
  logic moved, evicted;
  assign moved   = src_found && (e_port[src_idx] != fr_port);
  assign evicted = !src_found && !free_found;

  logic [31:0] learn_c, relearn_c, hit_c, flood_c, evict_c, aged_c;
  assign n_learn   = learn_c;
  assign n_relearn = relearn_c;
  assign n_hit     = hit_c;
  assign n_flood   = flood_c;
  assign n_evict   = evict_c;
  assign n_aged    = aged_c;


  // How many entries will expire on this tick. Computed combinationally
  // for the same reason as learn_idx: one write per register.
  logic [31:0] n_age_now;
  always_comb begin
    n_age_now = 32'd0;
    for (int i = 0; i < N_ENTRY; i++)
      if (e_val[i] && (e_age[i] >= AGE_MAX)) n_age_now = n_age_now + 32'd1;
  end

  // Combinational, so it reports the table as it stands rather than as
  // it stood a cycle ago. A registered version would lag every learn by
  // one cycle and make the bench's occupancy checks off-by-one.
  logic [31:0] occ_now;
  always_comb begin
    occ_now = 32'd0;
    for (int i = 0; i < N_ENTRY; i++) if (e_val[i]) occ_now = occ_now + 32'd1;
  end
  assign n_occupied = occ_now;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int i = 0; i < N_ENTRY; i++) begin
        e_val[i]  <= 1'b0;
        e_addr[i] <= 8'd0;
        e_port[i] <= '0;
        e_age[i]  <= 8'd0;
      end
      learn_c   <= 32'd0;
      relearn_c <= 32'd0;
      hit_c     <= 32'd0;
      flood_c   <= 32'd0;
      evict_c   <= 32'd0;
      aged_c    <= 32'd0;
    end else begin
      // ---- aging happens first, so a tick and a frame in the same
      //      cycle leave the frame's own entry fresh rather than expired
      if (age_tick) begin
        for (int i = 0; i < N_ENTRY; i++) begin
          if (e_val[i]) begin
            if (e_age[i] >= AGE_MAX) begin
              // Expired. The station may still be there; the switch has
              // simply stopped having evidence that it is. Forgetting a
              // live station costs a flood, which is cheap. NOT
              // forgetting a dead one costs mis-delivery, which is not.
              e_val[i] <= 1'b0;
              e_age[i] <= 8'd0;
            end else begin
              e_age[i] <= e_age[i] + 8'd1;
            end
          end
        end
        aged_c <= aged_c + n_age_now;
      end

      // ---- then the frame's own evidence ----
      if (fr_valid) begin
        e_val [learn_idx] <= 1'b1;
        e_addr[learn_idx] <= fr_src;
        e_port[learn_idx] <= fr_port;
        e_age [learn_idx] <= 8'd0;          // refreshed by being seen

        if (src_found) begin
          if (moved) relearn_c <= relearn_c + 32'd1;
        end else begin
          learn_c <= learn_c + 32'd1;
          if (evicted) evict_c <= evict_c + 32'd1;
        end

        if (hit) hit_c   <= hit_c   + 32'd1;
        else     flood_c <= flood_c + 32'd1;
      end
    end
  end

endmodule


// =====================================================================
//  usb_hub_route_map -- SystemVerilog. Same contract as the Verilog file.
//
//  THE SAME QUESTION, ANSWERED BY AN AUTHORITY.
//
//  CLASSIFICATION: simplified synthesisable teaching RTL.
//  This is NOT a hub. There is no packet path, no speed handling and no
//  suspend logic. It is the routing table, and how a host comes to have
//  one.
//
//  It answers exactly the question eth_learn_table answers -- "which
//  port is this address on?" -- and it is built to be compared with it
//  line for line. The differences are the protocol difference:
//
//    * There is no learning. The map is WRITTEN by the host, which knows
//      the answer because it enumerated the device on that port itself.
//      There is no `fr_src` port here at all: this module never infers
//      anything from traffic, so traffic cannot teach it a lie.
//
//    * There is no aging. An entry is not evidence that decays; it is a
//      record of an assignment, and it stays true until the host changes
//      it. Nothing has to be forgotten to stay correct.
//
//    * There is no eviction. The map is indexed BY ADDRESS over the
//      whole address space, so it cannot be full while addresses remain.
//
//    * THE ONE THAT ACTUALLY MATTERS: there is a `port_event` input.
//      A USB port reports connect and disconnect as a hardware event.
//      When a device leaves, the map is corrected AT THAT INSTANT --
//      before any traffic is sent to the port it left.
//
//  That last mechanism is what Ethernet has no equivalent of. A switch
//  can see its own link go down, but a station that moved from one
//  switch to another produces no event anywhere that says where it went.
//  The only evidence is traffic, and until the station sends some, the
//  switch is confidently wrong.
// =====================================================================
module usb_hub_route_map #(
  parameter int N_PORT = 4,
  // Addresses 1..N_ADDR are assignable. Address 0 is the default address
  // and is deliberately not routable here: a device at address 0 has not
  // been given an identity yet.
  parameter int N_ADDR = 8
) (
  input  logic       clk,
  input  logic       rst_n,

  // ---- the host writes the map, because the host assigned it ----
  input  logic       cfg_valid,
  input  logic [7:0] cfg_addr,
  input  logic [$clog2(N_PORT)-1:0] cfg_port,
  input  logic       cfg_remove,

  // ---- the hub reports physical events ----
  //
  // The mechanism with no Ethernet counterpart. `connect` is informational
  // here (the host must still enumerate); `disconnect` is what keeps the
  // map honest, because it invalidates the entry before any traffic can
  // be misrouted to a vacated port.
  input  logic       ev_valid,
  input  logic [$clog2(N_PORT)-1:0] ev_port,
  input  logic       ev_connect,     // 1 = connect, 0 = disconnect

  // ---- the query ----
  input  logic       fr_valid,
  input  logic [7:0] fr_dst,

  output logic       sel_valid,
  output logic [$clog2(N_PORT)-1:0] sel_port,
  output logic       sel_unknown,

  output logic [31:0] n_assigned,
  output logic [31:0] n_removed,
  output logic [31:0] n_hit,
  output logic [31:0] n_unknown,
  output logic [31:0] n_ev_removed,
  output logic [31:0] n_occupied
);

  localparam int PW = $clog2(N_PORT);

  // Indexed BY ADDRESS. There is no search and no tie to resolve,
  // because the host guarantees one address maps to one port -- it is the
  // only party that can create an entry.
  logic          a_val  [N_ADDR+1];
  logic [PW-1:0] a_port [N_ADDR+1];


  logic in_range;
  assign in_range = (fr_dst >= 8'd1) && (fr_dst <= N_ADDR[7:0]);
  // Read through a temporary rather than indexing inside the condition:
  // an out-of-range index must not be evaluated at all.
  logic          look_val;
  logic [PW-1:0] look_port;
  always_comb begin
    look_val  = 1'b0;
    look_port = '0;
    if (in_range) begin
      look_val  = a_val[fr_dst];
      look_port = a_port[fr_dst];
    end
  end

  assign sel_valid   = fr_valid && look_val;
  assign sel_port    = look_port;
  // An unknown address is not flooded. The host simply has nothing to
  // send it to, and it knows that this is so -- which is the difference
  // between "I have no record" and "I have a record and it is stale".
  assign sel_unknown = fr_valid && !look_val;

  logic [31:0] asg_c, rem_c, hit_c, unk_c, evrem_c;
  assign n_assigned   = asg_c;
  assign n_removed    = rem_c;
  assign n_hit        = hit_c;
  assign n_unknown    = unk_c;
  assign n_ev_removed = evrem_c;

  logic [31:0] occ_now;
  always_comb begin
    occ_now = 32'd0;
    for (int i = 1; i <= N_ADDR; i++) if (a_val[i]) occ_now = occ_now + 32'd1;
  end
  assign n_occupied = occ_now;

  // How many entries this event will clear. Computed combinationally so
  // the counter takes one write rather than one per entry.
  logic [31:0] n_ev_now;
  always_comb begin
    n_ev_now = 32'd0;
    if (ev_valid && !ev_connect)
      for (int i = 1; i <= N_ADDR; i++)
        if (a_val[i] && (a_port[i] == ev_port)) n_ev_now = n_ev_now + 32'd1;
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int i = 0; i <= N_ADDR; i++) begin
        a_val[i]  <= 1'b0;
        a_port[i] <= '0;
      end
      asg_c   <= 32'd0;
      rem_c   <= 32'd0;
      hit_c   <= 32'd0;
      unk_c   <= 32'd0;
      evrem_c <= 32'd0;
    end else begin
      // ---- a physical disconnect invalidates the entry IMMEDIATELY ----
      //
      // This is the whole mechanism. No traffic was required, no timeout
      // elapsed, and no inference was made: the port said the device had
      // gone, so the record of it went too.
      if (ev_valid && !ev_connect) begin
        for (int i = 1; i <= N_ADDR; i++)
          if (a_val[i] && (a_port[i] == ev_port)) a_val[i] <= 1'b0;
        evrem_c <= evrem_c + n_ev_now;
      end

      // ---- then any host write ----
      if (cfg_valid && (cfg_addr >= 8'd1) && (cfg_addr <= N_ADDR[7:0])) begin
        if (cfg_remove) begin
          a_val[cfg_addr] <= 1'b0;
          rem_c <= rem_c + 32'd1;
        end else begin
          a_val[cfg_addr]  <= 1'b1;
          a_port[cfg_addr] <= cfg_port;
          asg_c <= asg_c + 32'd1;
        end
      end

      if (fr_valid) begin
        if (look_val) hit_c <= hit_c + 32'd1;
        else          unk_c <= unk_c + 32'd1;
      end
    end
  end

endmodule

The SystemVerilog testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  Testbench for eth_learn_table and usb_hub_route_map -- SystemVerilog.
//
//  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.
//
//  THE BENCH HOLDS GROUND TRUTH, WHICH IS THE WHOLE POINT.
//
//  The design's table is an INFERENCE about where each station lives.
//  The bench decides where each station ACTUALLY lives, in `true_port`,
//  and nothing in the design can see that array. So "the table is wrong"
//  stops being a figure of speech and becomes a comparison between two
//  arrays -- which is the only way to measure the cost of learning.
//
//  A USB host needs no equivalent array. It ASSIGNED every address, so
//  its mapping and the truth are the same object and cannot diverge.
//  That is not a better implementation of the same idea; it is a
//  different idea, available only to a bus with a single authority.
//
//  THE SHADOW TABLE IS FORMULATED IN THE OPPOSITE DIRECTION.
//  The design walks its entries DOWNWARDS so the lowest index wins. The
//  model walks UPWARDS and stops at the first hit. Same answer,
//  different derivation.
// =====================================================================
`timescale 1ns/1ps
module tb_lt_sv;

  localparam int N_PORT  = 4;
  localparam int N_ENTRY = 4;
  localparam int AGE_MAX = 7;

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

  logic        fr_valid = 1'b0;
  logic [7:0]  fr_src = 8'd0, fr_dst = 8'd0;
  logic [1:0]  fr_port = 2'd0;
  logic        age_tick = 1'b0;

  logic        fwd_valid, fwd_flood, fwd_drop;
  logic [1:0]  fwd_port;
  logic [31:0] n_learn, n_relearn, n_hit, n_flood, n_evict, n_aged, n_occupied;

  eth_learn_table #(.N_PORT(N_PORT), .N_ENTRY(N_ENTRY), .AGE_MAX(AGE_MAX)) dut (
    .clk(clk), .rst_n(rst_n),
    .fr_valid(fr_valid), .fr_src(fr_src), .fr_dst(fr_dst), .fr_port(fr_port),
    .age_tick(age_tick),
    .fwd_valid(fwd_valid), .fwd_port(fwd_port),
    .fwd_flood(fwd_flood), .fwd_drop(fwd_drop),
    .n_learn(n_learn), .n_relearn(n_relearn), .n_hit(n_hit),
    .n_flood(n_flood), .n_evict(n_evict), .n_aged(n_aged),
    .n_occupied(n_occupied)
  );

  // ---- the USB counterpart, driven by the SAME bench ----
  //
  // Same question, answered by an authority instead of by inference. Note
  // there is no src port to drive: this module cannot be taught anything
  // by traffic.
  localparam int N_ADDR = 8;
  logic        u_cfg_valid = 1'b0, u_cfg_remove = 1'b0;
  logic [7:0]  u_cfg_addr = 8'd0;
  logic [1:0]  u_cfg_port = 2'd0;
  logic        u_ev_valid = 1'b0, u_ev_connect = 1'b0;
  logic [1:0]  u_ev_port = 2'd0;
  logic        u_fr_valid = 1'b0;
  logic [7:0]  u_fr_dst = 8'd0;

  logic       u_sel_valid, u_sel_unknown;
  logic [1:0] u_sel_port;
  logic [31:0] u_n_assigned, u_n_removed, u_n_hit, u_n_unknown,
              u_n_ev_removed, u_n_occupied;

  usb_hub_route_map #(.N_PORT(N_PORT), .N_ADDR(N_ADDR)) udut (
    .clk(clk), .rst_n(rst_n),
    .cfg_valid(u_cfg_valid), .cfg_addr(u_cfg_addr),
    .cfg_port(u_cfg_port), .cfg_remove(u_cfg_remove),
    .ev_valid(u_ev_valid), .ev_port(u_ev_port), .ev_connect(u_ev_connect),
    .fr_valid(u_fr_valid), .fr_dst(u_fr_dst),
    .sel_valid(u_sel_valid), .sel_port(u_sel_port),
    .sel_unknown(u_sel_unknown),
    .n_assigned(u_n_assigned), .n_removed(u_n_removed),
    .n_hit(u_n_hit), .n_unknown(u_n_unknown),
    .n_ev_removed(u_n_ev_removed), .n_occupied(u_n_occupied)
  );

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

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

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

  // ---- what the design said, sampled at a DEFINED instant ----
  logic       obs_flood, obs_fvalid, obs_fdrop;
  logic [1:0] obs_fport;
  logic       obs_uvalid, obs_uunknown;
  logic [1:0] obs_uport;

  // ---- the shadow table, maintained by the bench ----
  logic        m_val  [0:N_ENTRY-1];
  logic [7:0]  m_addr [0:N_ENTRY-1];
  logic [1:0]  m_port [0:N_ENTRY-1];
  logic [7:0]  m_age  [0:N_ENTRY-1];

  // ---- GROUND TRUTH: where each station really is ----
  //
  // Invisible to the design. 255 means "this station does not exist", so
  // a frame addressed to it can never be mis-delivered, only flooded.
  logic [7:0]  true_port [0:255];

  // ---- the measurements ----
  int g_frames      = 0;   // frames offered
  int g_flood       = 0;   // ... flooded because the table did not know
  int g_fwd         = 0;   // ... forwarded to one port
  int g_drop        = 0;   // ... dropped as same-port
  int g_misdeliver  = 0;   // ... forwarded to the WRONG port
  int g_deliverable = 0;   // frames whose destination actually exists
  int g_stale_win   = 0;   // mis-deliveries attributable to a move
  int g_poison_win  = 0;   // mis-deliveries attributable to a forgery

  // ---- the USB side of the same measurement ----
  logic        um_val  [0:N_ADDR];
  logic [1:0]  um_port [0:N_ADDR];
  int u_frames      = 0;
  int u_deliverable = 0;
  int u_hit         = 0;
  int u_unknown     = 0;
  int u_misdeliver  = 0;   // forwarded to a port the device has left
  int u_assigned_c  = 0;   // host writes, accumulated across resets
  int u_evrem_c     = 0;   // entries cleared by a port event

  // Accumulated across resets for the same reason: the DUT's counters are
  // zeroed by every reset_dut, so a summary that read them directly would
  // report only whatever happened after the last one.
  integer e_learn_c = 0, e_relearn_c = 0, e_hit_c = 0,
          e_flood_c = 0, e_evict_c = 0, e_aged_c = 0;

  // ---------------------------------------------------------------
  //  Model lookup: upward, stop at the first hit.
  // ---------------------------------------------------------------
  function automatic m_hit_f(input [7:0] a);
    int j; logic f;
    begin
      f = 1'b0;
      for (int j = 0; j < N_ENTRY; j++)
        if (m_val[j] && (m_addr[j] == a) && !f) f = 1'b1;
      m_hit_f = f;
    end
  endfunction

  function automatic logic [1:0] m_port_f(input [7:0] a);
    int j; logic f; reg [1:0] p;
    begin
      f = 1'b0; p = 2'd0;
      for (int j = 0; j < N_ENTRY; j++)
        if (m_val[j] && (m_addr[j] == a) && !f) begin p = m_port[j]; f = 1'b1; end
      m_port_f = p;
    end
  endfunction

  function automatic logic [31:0] m_occ_f;
    input dummy;
    int j; logic [31:0] c;
    begin
      c = 32'd0;
      for (int j = 0; j < N_ENTRY; j++) if (m_val[j]) c = c + 32'd1;
      m_occ_f = c;
    end
  endfunction

  // Which entry will the model learn into? Upward-and-stop, again the
  // opposite direction from the design's downward walk.
  function automatic logic [1:0] m_learn_idx(input [7:0] a);
    int j; logic fs, ff; logic [1:0] si, fi, oi; logic [7:0] oa;
    begin
      fs = 1'b0; ff = 1'b0; si = 2'd0; fi = 2'd0; oi = 2'd0; oa = 8'd0;
      for (int j = 0; j < N_ENTRY; j++) begin
        if (m_val[j] && (m_addr[j] == a) && !fs) begin si = j[1:0]; fs = 1'b1; end
        if (!m_val[j] && !ff) begin fi = j[1:0]; ff = 1'b1; end
        // strictly-greater, walking up, gives the lowest index among the
        // oldest -- the same winner the design's >= walking down picks
        if (m_val[j] && (m_age[j] > oa)) begin oa = m_age[j]; oi = j[1:0]; end
      end
      m_learn_idx = fs ? si : (ff ? fi : oi);
    end
  endfunction

  // ---------------------------------------------------------------
  //  Offer one frame and check the forwarding decision.
  // ---------------------------------------------------------------
  task automatic frame(input [7:0] src, input [7:0] dst, input [1:0] port,
             input why_stale, input why_poison);
    logic        e_hit, e_flood, e_fwd, e_drop, e_known_src, e_moved;
    logic [1:0]  e_port, li;
    logic [31:0] l0, r0, h0, f0;
    begin
      e_hit   = m_hit_f(dst);
      e_port  = m_port_f(dst);
      e_drop  = e_hit && (e_port == port);
      e_fwd   = e_hit && !e_drop;
      e_flood = !e_hit;
      e_known_src = m_hit_f(src);
      e_moved = e_known_src && (m_port_f(src) != port);

      l0 = n_learn; r0 = n_relearn; h0 = n_hit; f0 = n_flood;

      fr_valid = 1'b1; fr_src = src; fr_dst = dst; fr_port = port;
      #1;

      // ---- capture what the design said WHILE the request is asserted ----
      //
      // Every valid-gated output (fwd_flood, fwd_valid, fwd_drop) is only
      // meaningful while fr_valid is high. A check placed after this task
      // returns reads those wires after fr_valid has been deasserted, and
      // whether it sees the pre- or post-deassert value then depends on
      // delta-cycle ordering -- which differs between simulators and between
      // languages. One mutation scored 1516 in Verilog and 1522 in VHDL for
      // exactly that reason, on benches that were otherwise identical.
      //
      // An assertion whose outcome depends on delta ordering is not an
      // assertion. So the observation is taken here, once, at a defined
      // instant, and every later check uses the captured value.
      obs_flood  = fwd_flood;
      obs_fvalid = fwd_valid;
      obs_fdrop  = fwd_drop;
      obs_fport  = fwd_port;

      // ---- PROPERTY 1: exactly one outcome ----
      //
      // Flood, forward and drop must be mutually exclusive and one of
      // them must happen. A frame with no decision is a frame the switch
      // silently lost.
      ck((fwd_flood + fwd_valid + fwd_drop) == 1,
         "the switch did not reach exactly one forwarding decision");
      // ---- PROPERTY 2: a miss floods ----
      ck(fwd_flood === e_flood,
         "flooding does not correspond to an unknown destination");
      // ---- PROPERTY 3: a hit off the ingress port forwards ----
      ck(fwd_valid === e_fwd, "forwarding does not correspond to a known destination");
      // ---- PROPERTY 4: a hit ON the ingress port is dropped ----
      //
      // Forwarding it back out of the port it came from would be a
      // one-hop loop, and on a real switch it doubles the traffic on that
      // link for no benefit.
      ck(fwd_drop === e_drop, "a frame for the ingress port was not dropped");
      // ---- PROPERTY 5: the chosen port is the learned port ----
      if (e_fwd) ck(fwd_port === e_port, "the frame went to a port the table did not name");

      // ---- THE MEASUREMENT: was the decision RIGHT? ----
      //
      // Not "did it match the table" -- that is property 5. This asks
      // whether the table matched REALITY, which is a question only the
      // bench can ask because only the bench knows where the station is.
      g_frames = g_frames + 1;
      if (true_port[dst] !== 8'hFF) begin
        g_deliverable = g_deliverable + 1;
        if (fwd_valid && (fwd_port !== true_port[dst][1:0])) begin
          g_misdeliver = g_misdeliver + 1;
          if (why_stale)  g_stale_win  = g_stale_win  + 1;
          if (why_poison) g_poison_win = g_poison_win + 1;
        end
      end
      if (fwd_flood) g_flood = g_flood + 1;
      if (fwd_valid) g_fwd   = g_fwd + 1;
      if (fwd_drop)  g_drop  = g_drop + 1;
      if (e_hit) e_hit_c = e_hit_c + 1; else e_flood_c = e_flood_c + 1;
      if (e_known_src) begin
        if (e_moved) e_relearn_c = e_relearn_c + 1;
      end else begin
        e_learn_c = e_learn_c + 1;
        if (m_occ_f(0) == N_ENTRY) e_evict_c = e_evict_c + 1;
      end

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

      // ---- update the model, then compare occupancy ----
      li = m_learn_idx(src);
      m_val[li]  = 1'b1;
      m_addr[li] = src;
      m_port[li] = port;
      m_age[li]  = 8'd0;

      // ---- PROPERTY 6: the tables agree on how full they are ----
      //
      // A weaker check than entry-by-entry equality and a deliberately
      // chosen one: the two tables use opposite search directions, so
      // they may legitimately place the same address in different slots
      // after an eviction. What they may NOT disagree about is how many
      // addresses they know, and every lookup above already checks the
      // answers they give.
      ck(n_occupied === m_occ_f(0),
         "the design and the model disagree about how many addresses are known");

      // ---- PROPERTY 7: learning is counted correctly ----
      if (e_known_src) begin
        ck(n_learn == l0, "a known source was counted as a new address");
        ck(n_relearn == r0 + (e_moved ? 32'd1 : 32'd0),
           "a station move was miscounted");
      end else begin
        ck(n_learn == l0 + 32'd1, "a new address was not counted");
        ck(n_relearn == r0, "a new address was counted as a move");
      end
      // ---- PROPERTY 8: hits and floods are counted correctly ----
      ck(n_hit   == h0 + (e_hit ? 32'd1 : 32'd0), "hit counter disagrees with the model");
      ck(n_flood == f0 + (e_hit ? 32'd0 : 32'd1), "flood counter disagrees with the model");

      steps = steps + 1;
    end
  endtask

  // ---------------------------------------------------------------
  //  One aging tick, mirrored in the model.
  // ---------------------------------------------------------------
  task automatic tick;
    begin
      age_tick = 1'b1;
      @(posedge clk); #1;
      age_tick = 1'b0;
      for (int j = 0; j < N_ENTRY; j++) begin
        if (m_val[j]) begin
          if (m_age[j] >= AGE_MAX) begin
            m_val[j] = 1'b0; m_age[j] = 8'd0;
            e_aged_c = e_aged_c + 1;
          end else begin
            m_age[j] = m_age[j] + 8'd1;
          end
        end
      end
      ck(n_occupied === m_occ_f(0),
         "the design and the model disagree about occupancy after aging");
      steps = steps + 1;
    end
  endtask

  // ---------------------------------------------------------------
  //  Host writes the map. It knows the answer because it enumerated the
  //  device on that port -- there is nothing to infer.
  // ---------------------------------------------------------------
  task automatic umap_cfg(input [7:0] a, input [1:0] p, input rem);
    begin
      u_cfg_valid = 1'b1; u_cfg_addr = a; u_cfg_port = p; u_cfg_remove = rem;
      @(posedge clk); #1;
      u_cfg_valid = 1'b0;
      if ((a >= 8'd1) && (a <= N_ADDR)) begin
        if (rem) um_val[a] = 1'b0;
        else begin um_val[a] = 1'b1; um_port[a] = p; u_assigned_c = u_assigned_c + 1; end
      end
      steps = steps + 1;
    end
  endtask

  // ---------------------------------------------------------------
  //  A physical port event. THE mechanism with no Ethernet counterpart:
  //  a disconnect corrects the map before any traffic can be misrouted.
  // ---------------------------------------------------------------
  task automatic uev(input [1:0] p, input conn);
    logic [31:0] e0, expect_cleared;
    begin
      e0 = u_n_ev_removed;
      expect_cleared = 32'd0;
      if (!conn)
        for (int j = 1; j <= N_ADDR; j++)
          if (um_val[j] && (um_port[j] == p)) expect_cleared = expect_cleared + 32'd1;

      u_ev_valid = 1'b1; u_ev_port = p; u_ev_connect = conn;
      @(posedge clk); #1;
      u_ev_valid = 1'b0;

      if (!conn)
        for (int j = 1; j <= N_ADDR; j++)
          if (um_val[j] && (um_port[j] == p)) begin
            um_val[j] = 1'b0;
            u_evrem_c = u_evrem_c + 1;
          end

      // ---- PROPERTY U4: a disconnect clears every entry on that port ----
      //
      // Immediately, with no traffic and no timeout. This is the property
      // the Ethernet table cannot have, because nothing tells it.
      ck(u_n_ev_removed == e0 + expect_cleared,
         "a disconnect event did not clear the entries on that port");
      ck(u_n_occupied === um_occ(0),
         "the map and the model disagree about occupancy after an event");
      steps = steps + 1;
    end
  endtask

  // ---------------------------------------------------------------
  //  Query the map and check the answer -- and whether it was RIGHT.
  // ---------------------------------------------------------------
  task automatic uframe(input [7:0] dst);
    logic e_val_x; reg [1:0] e_port_x;
    logic [31:0] h0, k0;
    begin
      e_val_x  = ((dst >= 8'd1) && (dst <= N_ADDR)) ? um_val[dst]  : 1'b0;
      e_port_x = ((dst >= 8'd1) && (dst <= N_ADDR)) ? um_port[dst] : 2'd0;
      h0 = u_n_hit; k0 = u_n_unknown;

      u_fr_valid = 1'b1; u_fr_dst = dst;
      #1;

      // Captured for the same reason as in frame(): sel_valid is gated
      // by fr_valid, so it is only meaningful at this instant.
      obs_uvalid   = u_sel_valid;
      obs_uunknown = u_sel_unknown;
      obs_uport    = u_sel_port;

      // ---- PROPERTY U1: a hit means the host assigned that address ----
      ck(u_sel_valid === e_val_x,
         "usb map sel_valid does not mean the address was assigned");
      // ---- PROPERTY U2: the port is the assigned port ----
      if (e_val_x) ck(u_sel_port === e_port_x,
                      "usb map named a port the host did not assign");
      // ---- PROPERTY U3: valid and unknown are exact complements ----
      //
      // There is no third outcome. In particular there is no FLOOD: an
      // unknown address is not sent everywhere, because the host knows it
      // never assigned it.
      ck((u_sel_valid ^ u_sel_unknown) === 1'b1,
         "usb map did not reach exactly one outcome");
      // ---- PROPERTY U6: address 0 and out-of-range are never routed ----
      //
      // Address 0 is the default address: a device there has no identity
      // yet, so routing to it would route to whichever device is
      // mid-enumeration.
      if ((dst == 8'd0) || (dst > N_ADDR))
        ck(u_sel_valid === 1'b0, "usb map routed an unassignable address");

      // ---- THE MEASUREMENT, identical in form to the Ethernet one ----
      u_frames = u_frames + 1;
      if (true_port[dst] !== 8'hFF) begin
        u_deliverable = u_deliverable + 1;
        if (u_sel_valid && (u_sel_port !== true_port[dst][1:0]))
          u_misdeliver = u_misdeliver + 1;
      end
      if (u_sel_valid)   u_hit = u_hit + 1;
      if (u_sel_unknown) u_unknown = u_unknown + 1;

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

      ck(u_n_hit     == h0 + (e_val_x ? 32'd1 : 32'd0), "usb map hit counter wrong");
      ck(u_n_unknown == k0 + (e_val_x ? 32'd0 : 32'd1), "usb map unknown counter wrong");
      steps = steps + 1;
    end
  endtask

  function automatic logic [31:0] um_occ;
    input dummy;
    int j; logic [31:0] c;
    begin
      c = 32'd0;
      for (int j = 1; j <= N_ADDR; j++) if (um_val[j]) c = c + 32'd1;
      um_occ = c;
    end
  endfunction

  task automatic reset_dut;
    begin
      rst_n = 1'b0; fr_valid = 1'b0; age_tick = 1'b0;
      @(posedge clk); @(posedge clk);
      rst_n = 1'b1;
      @(posedge clk); #1;
      for (int j = 0; j < N_ENTRY; j++) begin
        m_val[j] = 1'b0; m_addr[j] = 8'd0; m_port[j] = 2'd0; m_age[j] = 8'd0;
      end
        for (int j = 0; j <= N_ADDR; j++) begin
        um_val[j] = 1'b0; um_port[j] = 2'd0;
      end
    end
  endtask

  task automatic nobody_exists;
    begin
      for (int j = 0; j < 256; j++) true_port[j] = 8'hFF;
    end
  endtask

  // ---- exhaustive reach ----
  //
  // mask(16) x src(8) x dst(8) x port(4) = 4096. Every dimension is an
  // independent input with no forbidden combinations: the mask selects
  // which of the four stations have spoken, and src/dst/port are the
  // probe frame. So the denominator is exactly 4096.
  logic reach [0:4095];
  int nr, ri;

  // USB map reach: ev_mode(5) x mask(16) x query(10) = 800. ev_mode 0 is
  // "no event"; 1..4 disconnect port ev_mode-1. Independent dimensions,
  // no forbidden combinations, so the denominator is exactly 800.
  logic reach_u [0:799];
  int nru, rui, evm, q;

  int mask, s, d, p, k, j;
  logic [31:0] snap;

  initial begin
    for (ri = 0; ri < 4096; ri = ri + 1) reach[ri] = 1'b0;
    seed = 32'd28003;
    nobody_exists;

    reset_dut;

    // =============================================================
    //  PHASE 1 (DIRECTED, EXHAUSTIVE) -- every table state crossed
    //  with every probe frame.
    //
    //  For each of the 16 subsets of {station 1..4}, the members speak
    //  once (station i from port i-1, so the table learns it), and then
    //  all 8 x 8 x 4 probe frames are offered against that table.
    // =============================================================
    for (mask = 0; mask < 16; mask = mask + 1) begin
      reset_dut;
      nobody_exists;
      // populate: station (j+1) lives on port j and says so
      for (int j = 0; j < 4; j++) begin
        if (mask[j]) begin
          true_port[j+1] = j[7:0];
          frame((j+1), 8'hF0, j[1:0], 1'b0, 1'b0);   // dst F0 is nobody
        end
      end
      for (s = 0; s < 8; s = s + 1)
      for (d = 0; d < 8; d = d + 1)
      for (p = 0; p < 4; p = p + 1) begin
        // The probe's source is learned too -- a switch cannot choose not
        // to learn. So each probe perturbs the table, which is realistic
        // and is why the model is updated in lockstep rather than
        // recomputed from the mask.
        frame(s[7:0], d[7:0], p[1:0], 1'b0, 1'b0);
        ri = ((mask * 8 + s) * 8 + d) * 4 + p;
        reach[ri] = 1'b1;
      end
    end

    // =============================================================
    //  PHASE 2 (DIRECTED) -- A STATION MOVES.
    //
    //  THE measurement. Station 1 lives on port 0 and has spoken, so the
    //  table knows it. Then it is unplugged and moved to port 3 -- and it
    //  does not immediately say anything, because a station that has just
    //  been plugged in usually has nothing to send.
    //
    //  Every frame addressed to station 1 in that window goes to port 0,
    //  where station 1 is not. The switch is behaving exactly as designed
    //  and is wrong anyway, and nothing in the protocol can tell it so.
    //
    //  A USB host cannot be in this state. It assigned the address; if
    //  the device moved, the host performed the move.
    // =============================================================
    reset_dut;
    nobody_exists;
    true_port[1] = 8'd0;
    frame(8'd1, 8'hF0, 2'd0, 1'b0, 1'b0);       // station 1 speaks from port 0
    true_port[2] = 8'd1;
    frame(8'd2, 8'd1, 2'd1, 1'b0, 1'b0);        // station 2 -> 1, correct
    ck(obs_fport === 2'd0, "traffic for station 1 did not go to port 0");

    // ---- the move ----
    true_port[1] = 8'd3;                        // physically now on port 3
    // Six frames for station 1 while it stays silent. All six are
    // mis-delivered, and the measurement counts them.
    for (k = 0; k < 6; k = k + 1)
      frame(8'd2, 8'd1, 2'd1, 1'b1, 1'b0);      // why_stale = 1

    // ---- the station finally speaks, and the table corrects itself ----
    snap = n_relearn;
    frame(8'd1, 8'hF0, 2'd3, 1'b0, 1'b0);
    ck(n_relearn == snap + 32'd1, "a station moving ports was not recorded as a move");
    frame(8'd2, 8'd1, 2'd1, 1'b0, 1'b0);
    ck(obs_fport === 2'd3, "after the station spoke, traffic still went to the old port");

    // =============================================================
    //  PHASE 3 (DIRECTED) -- AGING.
    //
    //  An entry with no refreshing evidence is discarded. That LOSES
    //  information and is the correct policy anyway: forgetting a live
    //  station costs one flood, while remembering a dead one costs
    //  mis-delivery for as long as the memory lasts.
    // =============================================================
    reset_dut;
    nobody_exists;
    true_port[1] = 8'd0;
    true_port[2] = 8'd1;
    frame(8'd1, 8'hF0, 2'd0, 1'b0, 1'b0);
    frame(8'd2, 8'hF0, 2'd1, 1'b0, 1'b0);
    ck(n_occupied === 32'd2, "two stations spoke and the table does not hold two");
    // Age it out. AGE_MAX + 2 ticks is enough for an entry created at
    // age 0 to pass AGE_MAX and be discarded.
    for (k = 0; k < AGE_MAX + 2; k = k + 1) tick;
    ck(n_occupied === 32'd0, "entries did not expire");
    // and a frame for a forgotten station floods rather than going astray
    frame(8'd3, 8'd1, 2'd2, 1'b0, 1'b0);
    ck(obs_flood === 1'b1, "a frame for an expired station was not flooded");

    // refreshing keeps an entry alive indefinitely
    reset_dut;
    nobody_exists;
    true_port[1] = 8'd0;
    for (k = 0; k < 3 * AGE_MAX; k = k + 1) begin
      frame(8'd1, 8'hF0, 2'd0, 1'b0, 1'b0);
      tick;
    end
    ck(n_occupied === 32'd1, "a station that kept speaking was still forgotten");

    // =============================================================
    //  PHASE 4 (DIRECTED) -- THE TABLE IS FULL.
    //
    //  Five stations, four entries. One address cannot be held, so
    //  traffic for it floods forever -- not for a window, permanently,
    //  until the topology changes.
    //
    //  A USB host cannot run out of table: the address space IS the
    //  table, 127 entries, allocated by the same party that owns it.
    // =============================================================
    reset_dut;
    nobody_exists;
    for (k = 0; k < 4; k = k + 1) begin
      true_port[k+1] = k[7:0];
      frame((k+1), 8'hF0, k[1:0], 1'b0, 1'b0);
    end
    ck(n_occupied === 32'd4, "four stations did not fill four entries");
    snap = n_evict;
    true_port[5] = 8'd0;
    frame(8'd5, 8'hF0, 2'd0, 1'b0, 1'b0);       // a fifth station
    ck(n_evict == snap + 32'd1, "a full table did not report an eviction");
    ck(n_occupied === 32'd4, "an eviction changed how many entries are valid");
    // Exactly one of the five is now unknown, and the switch has no way
    // to choose which one deserves the slot.
    snap = n_flood;
    for (k = 1; k <= 5; k = k + 1)
      frame(8'd6, k[7:0], 2'd2, 1'b0, 1'b0);
    ck(n_flood >= snap + 32'd1,
       "with five stations and four entries, nothing flooded");

    // =============================================================
    //  PHASE 4b (DIRECTED, EXHAUSTIVE over which entry is oldest)
    //
    //  A mutation that always evicts entry 0 instead of the oldest scored
    //  ZERO against everything above, and the reason is worth more than
    //  the phase: entry 0 held the oldest station anyway, because the
    //  first address learned takes the lowest free slot. The two policies
    //  agreed on every case the suite could reach.
    //
    //  The fix is not more stimulus but the RIGHT stimulus: make each of
    //  the four entries be the oldest in turn. Populate, age everything,
    //  then refresh every station EXCEPT one -- which leaves that one
    //  uniquely oldest, in a known entry. Three of the four arrangements
    //  put the oldest somewhere other than entry 0, and those are the
    //  three that can tell the two policies apart.
    // =============================================================
    for (int j = 0; j < 4; j++) begin
      reset_dut;
      nobody_exists;
      for (k = 0; k < 4; k = k + 1) true_port[k+1] = k[7:0];

      // stations 1..4 speak in order, one tick apart, so their ages are
      // staggered and entry i holds station i+1
      frame(8'd1, 8'hF0, 2'd0, 1'b0, 1'b0); tick;
      frame(8'd2, 8'hF0, 2'd1, 1'b0, 1'b0); tick;
      frame(8'd3, 8'hF0, 2'd2, 1'b0, 1'b0); tick;
      frame(8'd4, 8'hF0, 2'd3, 1'b0, 1'b0); tick;

      // everyone speaks again EXCEPT station j+1, so station j+1 is now
      // uniquely the oldest entry in the table
      for (k = 0; k < 4; k = k + 1)
        if (k != j) frame((k+1), 8'hF0, k[1:0], 1'b0, 1'b0);
      ck(n_occupied === 32'd4, "the table should hold four stations here");

      // a fifth station forces exactly one eviction
      true_port[5] = 8'd0;
      snap = n_evict;
      frame(8'd5, 8'hF0, 2'd0, 1'b0, 1'b0);
      ck(n_evict == snap + 32'd1, "a full table did not report an eviction");
      ck(n_occupied === 32'd4, "an eviction changed how many entries are valid");

      // Probe all four original stations. The bench's model tracks which
      // entry it expects to have been reused, so PROPERTY 2 inside
      // frame() compares flood-versus-hit station by station -- which is
      // what distinguishes "evict the oldest" from "evict entry 0".
      //
      // Probing from station 5, which is already in the table, so the
      // probes themselves cannot evict anything.
      for (k = 0; k < 4; k = k + 1)
        frame(8'd5, (k+1), 2'd0, 1'b0, 1'b0);

      // And the station that was NOT refreshed is the one that should be
      // gone. Stated directly as well as through the model, because this
      // is the property the phase exists for.
      frame(8'd5, (j+1), 2'd0, 1'b0, 1'b0);
      ck(obs_flood === 1'b1,
         "the entry that was not refreshed was not the one evicted");
    end

    // =============================================================
    //  PHASE 4c (DIRECTED) -- WHAT AGING IS FOR.
    //
    //  Aging looks like pure loss: it throws away a correct entry. Its
    //  purpose is the case below, and a suite that only checks that
    //  entries disappear has not tested the purpose.
    //
    //  A station is removed from the network. Nothing tells the switch.
    //  The ONLY thing that stops its traffic being sent to a port it has
    //  left is the entry expiring -- so after expiry the frame must
    //  flood, and flooding is the correct outcome rather than a failure.
    // =============================================================
    for (p = 0; p < 4; p = p + 1) begin
      reset_dut;
      nobody_exists;
      true_port[1] = p[7:0];
      frame(8'd1, 8'hF0, p[1:0], 1'b0, 1'b0);
      ck(n_occupied === 32'd1, "the station was not learned");
      // it is physically removed, and nothing reports that
      true_port[1] = 8'hFF;
      for (k = 0; k < AGE_MAX + 2; k = k + 1) tick;
      ck(n_occupied === 32'd0, "the entry for a departed station did not expire");
      // four frames for the departed station, from a port that is not p
      for (k = 0; k < 4; k = k + 1) begin
        frame(8'd7, 8'd1, ((p + 1) % 4), 1'b0, 1'b0);
        ck(obs_flood === 1'b1,
           "traffic for a departed station was still sent to the port it left");
        ck(obs_fvalid === 1'b0,
           "the table named a port for a station that had expired");
      end
    end

    // =============================================================
    //  PHASE 5 (DIRECTED) -- A FORGED SOURCE ADDRESS.
    //
    //  The table believes the newest evidence, because that is the only
    //  policy that converges. So an attacker who sends ONE frame claiming
    //  to be the victim moves the victim's entry to the attacker's port,
    //  and the victim's traffic follows.
    //
    //  This is not a defect in this module. It is what learning MEANS,
    //  and it is why real switches need port security, sticky MACs and
    //  802.1X -- three mechanisms that exist to put an authority back
    //  into a protocol that does not have one.
    //
    //  USB has no analogue. A device cannot assign itself an address, so
    //  there is no statement it can make that the host would believe.
    // =============================================================
    reset_dut;
    nobody_exists;
    true_port[1] = 8'd0;   // the victim, on port 0
    true_port[9] = 8'd2;   // the attacker, on port 2
    frame(8'd1, 8'hF0, 2'd0, 1'b0, 1'b0);       // victim speaks
    true_port[2] = 8'd1;
    frame(8'd2, 8'd1, 2'd1, 1'b0, 1'b0);        // traffic reaches the victim
    ck(obs_fport === 2'd0, "traffic for the victim did not reach its real port");

    // ---- one forged frame ----
    frame(8'd1, 8'hF0, 2'd2, 1'b0, 1'b0);       // "I am station 1", from port 2
    // Now every frame for the victim goes to the attacker. Counted
    // against the poisoning measurement.
    for (k = 0; k < 6; k = k + 1)
      frame(8'd2, 8'd1, 2'd1, 1'b0, 1'b1);      // why_poison = 1
    ck(obs_fport === 2'd2, "the forged frame did not redirect the victim's traffic");

    // =============================================================
    //  PHASE 6 (RANDOM)
    // =============================================================
`ifndef DIRECTED_ONLY
    reset_dut;
    nobody_exists;
    for (k = 1; k <= 6; k = k + 1) true_port[k] = (k - 1) % 4;
    for (k = 0; k < 600; k = k + 1) begin
      if ((urand() % 11) == 0) tick;
      frame(1 + (urand() % 6), 1 + (urand() % 6), (urand() % 4), 1'b0, 1'b0);
    end
`endif

    // =============================================================
    //  PHASE 7 (DIRECTED, EXHAUSTIVE) -- the USB map.
    //
    //  Every subset of assigned addresses, crossed with every port
    //  disconnect event and every query. The event dimension is the one
    //  that matters: it is the mechanism Ethernet has no counterpart for,
    //  so it has to be swept rather than demonstrated once.
    // =============================================================
    for (evm = 0; evm < 5; evm = evm + 1)
    for (mask = 0; mask < 16; mask = mask + 1) begin
      reset_dut;
      nobody_exists;
      // the host assigns address (j+1) to port j for each member
      for (int j = 0; j < 4; j++) begin
        if (mask[j]) begin
          true_port[j+1] = j[7:0];
          umap_cfg((j+1), j[1:0], 1'b0);
        end
      end
      // then, in four of the five modes, a device is unplugged. The bench
      // updates ground truth FIRST, so a map that failed to react would
      // be measured as mis-delivering rather than merely as out of date.
      if (evm > 0) begin
        for (int j = 1; j <= 4; j++)
          if (true_port[j] === (evm - 1)) true_port[j] = 8'hFF;
        uev((evm - 1), 1'b0);
      end
      for (q = 0; q < 10; q = q + 1) begin
        uframe(q[7:0]);
        rui = (evm * 16 + mask) * 10 + q;
        reach_u[rui] = 1'b1;
      end
    end

    // =============================================================
    //  PHASE 8 (DIRECTED) -- THE SAME MOVE, ON BOTH DESIGNS.
    //
    //  Phase 2 moved a station and measured six mis-delivered frames. The
    //  identical scenario is now run through the USB map, and the only
    //  difference in the stimulus is the one the protocol actually
    //  provides: the port reports the disconnect.
    //
    //  Nothing else is given to the USB side. It gets no extra traffic, no
    //  shorter timeout and no cleverer table -- only the event.
    // =============================================================
    reset_dut;
    nobody_exists;
    true_port[1] = 8'd0;
    umap_cfg(8'd1, 2'd0, 1'b0);       // host enumerated device 1 on port 0
    uframe(8'd1);
    ck(obs_uport === 2'd0, "the map did not route device 1 to port 0");

    // ---- the move: the device is unplugged from port 0 ----
    true_port[1] = 8'hFF;             // in transit, physically nowhere
    uev(2'd0, 1'b0);                  // the PORT reports it
    // Six frames in the window that cost Ethernet six mis-deliveries.
    for (k = 0; k < 6; k = k + 1) begin
      uframe(8'd1);
      // ---- THE COMPARISON ----
      // Not "the map is empty" but "the map does not name a stale port".
      // A map that still answered would be answering with port 0, where
      // the device provably is not.
      ck(obs_uvalid === 1'b0,
         "the map still routed a device whose port reported a disconnect");
    end

    // ---- it is plugged into port 3 and re-enumerated ----
    uev(2'd3, 1'b1);                  // connect event
    true_port[1] = 8'd3;
    umap_cfg(8'd1, 2'd3, 1'b0);       // the host assigns it again
    uframe(8'd1);
    ck(obs_uport === 2'd3, "after re-enumeration the map did not follow the device");

    nr = 0; for (ri = 0; ri < 4096; ri = ri + 1) if (reach[ri]) nr = nr + 1;
    nru = 0; for (rui = 0; rui < 800; rui = rui + 1) if (reach_u[rui]) nru = nru + 1;

    $display("steps=%0d checks=%0d reach_eth=%0d/4096 reach_usb=%0d/800 errors=%0d",
             steps, checks, nr, nru, errors);
    $display("[eth] learned=%0d relearned=%0d hits=%0d floods=%0d evictions=%0d aged=%0d",
             e_learn_c, e_relearn_c, e_hit_c, e_flood_c, e_evict_c, e_aged_c);
    $display("[eth] frames=%0d forwarded=%0d flooded=%0d dropped=%0d",
             g_frames, g_fwd, g_flood, g_drop);
    $display("--- what learning costs that assignment does not ---");
    $display("[eth] frames whose destination exists   = %0d", g_deliverable);
    $display("[eth] delivered to the WRONG port       = %0d", g_misdeliver);
    $display("[eth]   ... because a station had moved = %0d", g_stale_win);
    $display("[eth]   ... because a source was forged = %0d", g_poison_win);
    $display("--- the same question, answered by an authority ---");
    $display("[usb] assigned=%0d removed_by_PORT_EVENT=%0d",
             u_assigned_c, u_evrem_c);
    $display("[usb] queries=%0d answered=%0d unknown=%0d flooded=0 (no such outcome)",
             u_frames, u_hit, u_unknown);
    $display("[usb] queries whose device exists      = %0d", u_deliverable);
    $display("[usb] routed to the WRONG port         = %0d", u_misdeliver);
    if (nr != 4096 || nru != 800) 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

Same seed and phase order as the Verilog bench, so any difference between those two mutation columns is a real difference between the designs. The independent-stimulus role is VHDL's.

10. VHDL-2008

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- =====================================================================
--  THE TABLE ETHERNET NEEDS AND USB DOES NOT -- VHDL-2008.
--
--  CLASSIFICATION: simplified synthesisable teaching RTL.
--  Two entities, same hardware contract as the Verilog and SystemVerilog
--  files: same ports, same widths, same reset values, same
--  cycle-by-cycle behaviour.
--
--  USB has exactly one host and that host ASSIGNS every address, so the
--  mapping from address to port is KNOWN by construction to the only
--  party that needs it.
--
--  Ethernet has no host. There is no authority to assign addresses and
--  none to be told where anything is, so a switch works the topology out
--  by watching: every frame carries a source address, and the port it
--  arrived on is evidence of where that address lives.
--
--      USB:      the mapping is ASSIGNED, so it is correct by authority.
--      Ethernet: the mapping is INFERRED, so it is correct by evidence
--                -- and evidence goes stale, can be missing, and can be
--                manufactured.
--
--  Both combinational processes use `process (all)`. In a file that gets
--  mutated nine times that is not a convenience: a mutation which adds a
--  branch reading a new signal would otherwise need the sensitivity list
--  extended by hand, and forgetting produces a mutant that fails for the
--  wrong reason.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

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

package body lt_pkg is
  function lt_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;


-- ---------------------------------------------------------------------
--  eth_learn_table -- the mapping, inferred from traffic.
-- ---------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.lt_pkg.all;

entity eth_learn_table is
  generic (
    N_PORT  : natural := 4;
    N_ENTRY : natural := 4;
    -- Ticks before an unrefreshed entry is discarded. Real switches use
    -- 300 seconds; the value only has to be small enough to reach in
    -- simulation and large enough that aging is not the common case.
    AGE_MAX : natural := 7
  );
  port (
    clk   : in std_logic;
    rst_n : in std_logic;

    -- one frame arriving
    fr_valid : in std_logic;
    fr_src   : in std_logic_vector(7 downto 0);   -- the EVIDENCE
    fr_dst   : in std_logic_vector(7 downto 0);   -- the QUERY
    fr_port  : in std_logic_vector(lt_clog2(N_PORT) - 1 downto 0);

    -- the aging clock, one tick at a time
    age_tick : in std_logic;

    -- the forwarding decision
    fwd_valid : out std_logic;
    fwd_port  : out std_logic_vector(lt_clog2(N_PORT) - 1 downto 0);
    fwd_flood : out std_logic;
    fwd_drop  : out std_logic;

    n_learn     : out std_logic_vector(31 downto 0);
    n_relearn   : out std_logic_vector(31 downto 0);
    n_hit       : out std_logic_vector(31 downto 0);
    n_flood     : out std_logic_vector(31 downto 0);
    n_evict     : out std_logic_vector(31 downto 0);
    n_aged      : out std_logic_vector(31 downto 0);
    n_occupied  : out std_logic_vector(31 downto 0)
  );
end entity;

architecture rtl of eth_learn_table is
  constant PW : natural := lt_clog2(N_PORT);
  constant EW : natural := lt_clog2(N_ENTRY);

  type val_arr_t  is array (0 to N_ENTRY - 1) of std_logic;
  type addr_arr_t is array (0 to N_ENTRY - 1) of std_logic_vector(7 downto 0);
  type port_arr_t is array (0 to N_ENTRY - 1) of unsigned(PW - 1 downto 0);
  type age_arr_t  is array (0 to N_ENTRY - 1) of unsigned(7 downto 0);

  signal e_val  : val_arr_t  := (others => '0');
  signal e_addr : addr_arr_t := (others => (others => '0'));
  signal e_port : port_arr_t := (others => (others => '0'));
  signal e_age  : age_arr_t  := (others => (others => '0'));

  signal hit      : std_logic := '0';
  signal hit_port : unsigned(PW - 1 downto 0) := (others => '0');

  signal src_found  : std_logic := '0';
  signal src_idx    : unsigned(EW - 1 downto 0) := (others => '0');
  signal free_found : std_logic := '0';
  signal free_idx   : unsigned(EW - 1 downto 0) := (others => '0');
  signal old_idx    : unsigned(EW - 1 downto 0) := (others => '0');

  signal learn_idx : unsigned(EW - 1 downto 0) := (others => '0');
  signal moved     : std_logic := '0';
  signal evicted   : std_logic := '0';

  signal learn_c   : unsigned(31 downto 0) := (others => '0');
  signal relearn_c : unsigned(31 downto 0) := (others => '0');
  signal hit_c     : unsigned(31 downto 0) := (others => '0');
  signal flood_c   : unsigned(31 downto 0) := (others => '0');
  signal evict_c   : unsigned(31 downto 0) := (others => '0');
  signal aged_c    : unsigned(31 downto 0) := (others => '0');

  signal n_age_now : unsigned(31 downto 0) := (others => '0');
  signal occ_now   : unsigned(31 downto 0) := (others => '0');
begin

  -- -------------------------------------------------------------------
  --  LOOKUP -- does the table know where fr_dst lives?
  --
  --  Walk downwards so the LOWEST matching entry wins. Addresses are
  --  supposed to be unique and the design maintains that; the direction
  --  is fixed anyway so the duplicate case has a defined answer.
  -- -------------------------------------------------------------------
  lookup : process (all)
    variable h : std_logic;
    variable p : unsigned(PW - 1 downto 0);
  begin
    h := '0';
    p := (others => '0');
    for i in N_ENTRY - 1 downto 0 loop
      if e_val(i) = '1' and e_addr(i) = fr_dst then
        h := '1';
        p := e_port(i);
      end if;
    end loop;
    hit      <= h;
    hit_port <= p;
  end process;

  -- A frame whose destination is on the port it arrived from must NOT be
  -- sent back out that port: that is a loop of exactly one hop, and on a
  -- real link it doubles the traffic for no benefit.
  fwd_drop  <= '1' when (fr_valid = '1' and hit = '1' and hit_port = unsigned(fr_port))
               else '0';
  fwd_valid <= '1' when (fr_valid = '1' and hit = '1' and hit_port /= unsigned(fr_port))
               else '0';
  fwd_port  <= std_logic_vector(hit_port);
  -- The cost of not being told. With no entry there is no choice but to
  -- send the frame to every port -- a bandwidth cost on every link and a
  -- confidentiality cost on every one of them too.
  fwd_flood <= '1' when (fr_valid = '1' and hit = '0') else '0';

  -- -------------------------------------------------------------------
  --  LEARN -- three cases in priority order:
  --    1. fr_src is already known  -> refresh, and move it if the port
  --       changed (that is a station having moved)
  --    2. there is a free entry    -> take it
  --    3. the table is full        -> evict the OLDEST entry
  --
  --  The index is computed here and written exactly once in the clocked
  --  process. Writing inside the search loop would give several
  --  assignments to one signal, where the last silently wins.
  -- -------------------------------------------------------------------
  search : process (all)
    variable sf, ff : std_logic;
    variable si, fi, oi : unsigned(EW - 1 downto 0);
    variable oa : unsigned(7 downto 0);
  begin
    sf := '0'; ff := '0';
    si := (others => '0');
    fi := (others => '0');
    oi := (others => '0');
    oa := (others => '0');
    for i in N_ENTRY - 1 downto 0 loop
      if e_val(i) = '1' and e_addr(i) = fr_src then
        sf := '1';
        si := to_unsigned(i, EW);
      end if;
      if e_val(i) = '0' then
        ff := '1';
        fi := to_unsigned(i, EW);
      end if;
      -- >= so the lowest index wins a tie, matching the walk direction
      if e_val(i) = '1' and e_age(i) >= oa then
        oa := e_age(i);
        oi := to_unsigned(i, EW);
      end if;
    end loop;
    src_found  <= sf;
    src_idx    <= si;
    free_found <= ff;
    free_idx   <= fi;
    old_idx    <= oi;
  end process;

  learn_idx <= src_idx  when src_found = '1'  else
               free_idx when free_found = '1' else
               old_idx;
  -- A station that moved: the address is known but the evidence now
  -- points elsewhere. The table believes the newest evidence, which is
  -- the only policy that can converge -- and is exactly what makes the
  -- table forgeable.
  moved   <= '1' when (src_found = '1' and e_port(to_integer(src_idx)) /= unsigned(fr_port))
             else '0';
  evicted <= '1' when (src_found = '0' and free_found = '0') else '0';

  n_learn   <= std_logic_vector(learn_c);
  n_relearn <= std_logic_vector(relearn_c);
  n_hit     <= std_logic_vector(hit_c);
  n_flood   <= std_logic_vector(flood_c);
  n_evict   <= std_logic_vector(evict_c);
  n_aged    <= std_logic_vector(aged_c);

  -- Combinational, so it reports the table as it stands rather than as it
  -- stood a cycle ago.
  occupancy : process (all)
    variable c : unsigned(31 downto 0);
  begin
    c := (others => '0');
    for i in 0 to N_ENTRY - 1 loop
      if e_val(i) = '1' then c := c + 1; end if;
    end loop;
    occ_now <= c;
  end process;
  n_occupied <= std_logic_vector(occ_now);

  -- How many entries will expire on this tick, so the counter takes one
  -- write rather than one per entry.
  expiring : process (all)
    variable c : unsigned(31 downto 0);
  begin
    c := (others => '0');
    for i in 0 to N_ENTRY - 1 loop
      if e_val(i) = '1' and e_age(i) >= to_unsigned(AGE_MAX, 8) then
        c := c + 1;
      end if;
    end loop;
    n_age_now <= c;
  end process;

  process (clk, rst_n)
  begin
    if rst_n = '0' then
      e_val     <= (others => '0');
      e_addr    <= (others => (others => '0'));
      e_port    <= (others => (others => '0'));
      e_age     <= (others => (others => '0'));
      learn_c   <= (others => '0');
      relearn_c <= (others => '0');
      hit_c     <= (others => '0');
      flood_c   <= (others => '0');
      evict_c   <= (others => '0');
      aged_c    <= (others => '0');
    elsif rising_edge(clk) then

      -- ---- aging first, so a tick and a frame in the same cycle leave
      --      the frame's own entry fresh rather than expired. Within one
      --      process the later assignment to the same element wins, which
      --      is what implements that priority.
      if age_tick = '1' then
        for i in 0 to N_ENTRY - 1 loop
          if e_val(i) = '1' then
            if e_age(i) >= to_unsigned(AGE_MAX, 8) then
              -- Expired. The station may still be there; the switch has
              -- simply stopped having evidence that it is. Forgetting a
              -- live station costs a flood, which is cheap. NOT
              -- forgetting a dead one costs mis-delivery, which is not.
              e_val(i) <= '0';
              e_age(i) <= (others => '0');
            else
              e_age(i) <= e_age(i) + 1;
            end if;
          end if;
        end loop;
        aged_c <= aged_c + n_age_now;
      end if;

      -- ---- then the frame's own evidence ----
      if fr_valid = '1' then
        e_val (to_integer(learn_idx)) <= '1';
        e_addr(to_integer(learn_idx)) <= fr_src;
        e_port(to_integer(learn_idx)) <= unsigned(fr_port);
        e_age (to_integer(learn_idx)) <= (others => '0');   -- refreshed

        if src_found = '1' then
          if moved = '1' then
            relearn_c <= relearn_c + 1;
          end if;
        else
          learn_c <= learn_c + 1;
          if evicted = '1' then
            evict_c <= evict_c + 1;
          end if;
        end if;

        if hit = '1' then
          hit_c <= hit_c + 1;
        else
          flood_c <= flood_c + 1;
        end if;
      end if;
    end if;
  end process;

end architecture;


-- ---------------------------------------------------------------------
--  usb_hub_route_map -- the same question, answered by an authority.
--
--  It answers exactly the question eth_learn_table answers, and is built
--  to be compared with it line for line. The differences ARE the protocol
--  difference:
--
--    * No learning. The map is WRITTEN by the host, which knows the
--      answer because it enumerated the device on that port. There is no
--      fr_src port at all, so traffic cannot teach it a lie.
--
--    * No aging. An entry is not decaying evidence; it is a record of an
--      assignment, true until the host changes it.
--
--    * No eviction. The map is indexed BY ADDRESS over the whole address
--      space, so it cannot be full while addresses remain.
--
--    * THE ONE THAT MATTERS: a port_event input. A USB port reports
--      connect and disconnect as a hardware event, so when a device
--      leaves the map is corrected AT THAT INSTANT, before any traffic is
--      sent to the port it left.
--
--  Ethernet has no counterpart for the last one. A switch can see its own
--  link go down, but a station that moved to another switch produces no
--  event anywhere saying where it went. The only evidence is traffic, and
--  until the station sends some, the switch is confidently wrong.
-- ---------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.lt_pkg.all;

entity usb_hub_route_map is
  generic (
    N_PORT : natural := 4;
    -- Addresses 1..N_ADDR are assignable. Address 0 is the default
    -- address and is deliberately not routable: a device there has not
    -- been given an identity yet.
    N_ADDR : natural := 8
  );
  port (
    clk   : in std_logic;
    rst_n : in std_logic;

    -- the host writes the map, because the host assigned it
    cfg_valid  : in std_logic;
    cfg_addr   : in std_logic_vector(7 downto 0);
    cfg_port   : in std_logic_vector(lt_clog2(N_PORT) - 1 downto 0);
    cfg_remove : in std_logic;

    -- the hub reports physical events
    ev_valid   : in std_logic;
    ev_port    : in std_logic_vector(lt_clog2(N_PORT) - 1 downto 0);
    ev_connect : in std_logic;

    fr_valid : in std_logic;
    fr_dst   : in std_logic_vector(7 downto 0);

    sel_valid   : out std_logic;
    sel_port    : out std_logic_vector(lt_clog2(N_PORT) - 1 downto 0);
    sel_unknown : out std_logic;

    n_assigned   : out std_logic_vector(31 downto 0);
    n_removed    : out std_logic_vector(31 downto 0);
    n_hit        : out std_logic_vector(31 downto 0);
    n_unknown    : out std_logic_vector(31 downto 0);
    n_ev_removed : out std_logic_vector(31 downto 0);
    n_occupied   : out std_logic_vector(31 downto 0)
  );
end entity;

architecture rtl of usb_hub_route_map is
  constant PW : natural := lt_clog2(N_PORT);

  type aval_arr_t is array (0 to N_ADDR) of std_logic;
  type aport_arr_t is array (0 to N_ADDR) of unsigned(PW - 1 downto 0);

  -- Indexed BY ADDRESS. There is no search and no tie to resolve, because
  -- the host guarantees one address maps to one port -- it is the only
  -- party that can create an entry.
  signal a_val  : aval_arr_t  := (others => '0');
  signal a_port : aport_arr_t := (others => (others => '0'));

  signal in_range  : std_logic := '0';
  signal look_val  : std_logic := '0';
  signal look_port : unsigned(PW - 1 downto 0) := (others => '0');

  signal asg_c   : unsigned(31 downto 0) := (others => '0');
  signal rem_c   : unsigned(31 downto 0) := (others => '0');
  signal hit_c   : unsigned(31 downto 0) := (others => '0');
  signal unk_c   : unsigned(31 downto 0) := (others => '0');
  signal evrem_c : unsigned(31 downto 0) := (others => '0');

  signal occ_now  : unsigned(31 downto 0) := (others => '0');
  signal n_ev_now : unsigned(31 downto 0) := (others => '0');
begin

  in_range <= '1' when (unsigned(fr_dst) >= 1 and unsigned(fr_dst) <= N_ADDR)
              else '0';

  -- The bound check is INSIDE the process that does the lookup. Reading
  -- a_val(fr_dst) outside it would index the array before the range test
  -- had been applied, which is a fatal runtime error in VHDL rather than
  -- a harmless wrap.
  lookup : process (all)
  begin
    look_val  <= '0';
    look_port <= (others => '0');
    if unsigned(fr_dst) >= 1 and unsigned(fr_dst) <= N_ADDR then
      look_val  <= a_val(to_integer(unsigned(fr_dst)));
      look_port <= a_port(to_integer(unsigned(fr_dst)));
    end if;
  end process;

  sel_valid <= '1' when (fr_valid = '1' and look_val = '1') else '0';
  sel_port  <= std_logic_vector(look_port);
  -- An unknown address is not flooded. The host simply has nothing to
  -- send it to, and it knows that this is so -- which is the difference
  -- between "I have no record" and "I have a record and it is stale".
  sel_unknown <= '1' when (fr_valid = '1' and look_val = '0') else '0';

  n_assigned   <= std_logic_vector(asg_c);
  n_removed    <= std_logic_vector(rem_c);
  n_hit        <= std_logic_vector(hit_c);
  n_unknown    <= std_logic_vector(unk_c);
  n_ev_removed <= std_logic_vector(evrem_c);

  occupancy : process (all)
    variable c : unsigned(31 downto 0);
  begin
    c := (others => '0');
    for i in 1 to N_ADDR loop
      if a_val(i) = '1' then c := c + 1; end if;
    end loop;
    occ_now <= c;
  end process;
  n_occupied <= std_logic_vector(occ_now);

  clearing : process (all)
    variable c : unsigned(31 downto 0);
  begin
    c := (others => '0');
    if ev_valid = '1' and ev_connect = '0' then
      for i in 1 to N_ADDR loop
        if a_val(i) = '1' and a_port(i) = unsigned(ev_port) then
          c := c + 1;
        end if;
      end loop;
    end if;
    n_ev_now <= c;
  end process;

  process (clk, rst_n)
  begin
    if rst_n = '0' then
      a_val   <= (others => '0');
      a_port  <= (others => (others => '0'));
      asg_c   <= (others => '0');
      rem_c   <= (others => '0');
      hit_c   <= (others => '0');
      unk_c   <= (others => '0');
      evrem_c <= (others => '0');
    elsif rising_edge(clk) then

      -- ---- a physical disconnect invalidates the entry IMMEDIATELY ----
      --
      -- This is the whole mechanism. No traffic was required, no timeout
      -- elapsed, and no inference was made: the port said the device had
      -- gone, so the record of it went too.
      if ev_valid = '1' and ev_connect = '0' then
        for i in 1 to N_ADDR loop
          if a_val(i) = '1' and a_port(i) = unsigned(ev_port) then
            a_val(i) <= '0';
          end if;
        end loop;
        evrem_c <= evrem_c + n_ev_now;
      end if;

      -- ---- then any host write ----
      if cfg_valid = '1' and unsigned(cfg_addr) >= 1
                         and unsigned(cfg_addr) <= N_ADDR then
        if cfg_remove = '1' then
          a_val(to_integer(unsigned(cfg_addr))) <= '0';
          rem_c <= rem_c + 1;
        else
          a_val(to_integer(unsigned(cfg_addr)))  <= '1';
          a_port(to_integer(unsigned(cfg_addr))) <= unsigned(cfg_port);
          asg_c <= asg_c + 1;
        end if;
      end if;

      if fr_valid = '1' then
        if look_val = '1' then
          hit_c <= hit_c + 1;
        else
          unk_c <= unk_c + 1;
        end if;
      end if;
    end if;
  end process;

end architecture;

The VHDL testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- =====================================================================
--  Testbench for eth_learn_table and usb_hub_route_map -- VHDL-2008.
--
--  THE BENCH HOLDS GROUND TRUTH, WHICH IS THE WHOLE POINT.
--
--  The learning table is an INFERENCE about where each station lives. The
--  bench decides where each station ACTUALLY lives, in `true_port`, and
--  nothing in either design can see that array. So "the table is wrong"
--  stops being a figure of speech and becomes a comparison between two
--  arrays -- the only way to measure what learning costs.
--
--  THE SHADOW TABLE IS FORMULATED IN THE OPPOSITE DIRECTION.
--  The designs walk their entries DOWNWARDS so the lowest index wins. The
--  models walk UPWARDS and stop at the first hit. Same answer, different
--  derivation.
--
--  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.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;

entity tb_lt_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.
    DIRECTED_ONLY : boolean := false
  );
end entity;

architecture sim of tb_lt_vhdl is

  constant N_PORT  : natural := 4;
  constant N_ENTRY : natural := 4;
  constant AGE_MAX : natural := 7;
  constant N_ADDR  : natural := 8;

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

  -- Ethernet side
  signal fr_valid  : std_logic := '0';
  signal fr_src    : std_logic_vector(7 downto 0) := (others => '0');
  signal fr_dst    : std_logic_vector(7 downto 0) := (others => '0');
  signal fr_port   : std_logic_vector(1 downto 0) := "00";
  signal age_tick  : std_logic := '0';
  signal fwd_valid : std_logic;
  signal fwd_port  : std_logic_vector(1 downto 0);
  signal fwd_flood : std_logic;
  signal fwd_drop  : std_logic;
  signal n_learn, n_relearn, n_hit, n_flood, n_evict, n_aged, n_occupied
         : std_logic_vector(31 downto 0);

  -- USB side
  signal u_cfg_valid  : std_logic := '0';
  signal u_cfg_addr   : std_logic_vector(7 downto 0) := (others => '0');
  signal u_cfg_port   : std_logic_vector(1 downto 0) := "00";
  signal u_cfg_remove : std_logic := '0';
  signal u_ev_valid   : std_logic := '0';
  signal u_ev_port    : std_logic_vector(1 downto 0) := "00";
  signal u_ev_connect : std_logic := '0';
  signal u_fr_valid   : std_logic := '0';
  signal u_fr_dst     : std_logic_vector(7 downto 0) := (others => '0');
  signal u_sel_valid  : std_logic;
  signal u_sel_port   : std_logic_vector(1 downto 0);
  signal u_sel_unknown : std_logic;
  signal u_n_assigned, u_n_removed, u_n_hit, u_n_unknown,
         u_n_ev_removed, u_n_occupied : std_logic_vector(31 downto 0);

begin

  dut : entity work.eth_learn_table
    generic map (N_PORT => N_PORT, N_ENTRY => N_ENTRY, AGE_MAX => AGE_MAX)
    port map (
      clk => clk, rst_n => rst_n,
      fr_valid => fr_valid, fr_src => fr_src, fr_dst => fr_dst,
      fr_port => fr_port, age_tick => age_tick,
      fwd_valid => fwd_valid, fwd_port => fwd_port,
      fwd_flood => fwd_flood, fwd_drop => fwd_drop,
      n_learn => n_learn, n_relearn => n_relearn, n_hit => n_hit,
      n_flood => n_flood, n_evict => n_evict, n_aged => n_aged,
      n_occupied => n_occupied
    );

  udut : entity work.usb_hub_route_map
    generic map (N_PORT => N_PORT, N_ADDR => N_ADDR)
    port map (
      clk => clk, rst_n => rst_n,
      cfg_valid => u_cfg_valid, cfg_addr => u_cfg_addr,
      cfg_port => u_cfg_port, cfg_remove => u_cfg_remove,
      ev_valid => u_ev_valid, ev_port => u_ev_port,
      ev_connect => u_ev_connect,
      fr_valid => u_fr_valid, fr_dst => u_fr_dst,
      sel_valid => u_sel_valid, sel_port => u_sel_port,
      sel_unknown => u_sel_unknown,
      n_assigned => u_n_assigned, n_removed => u_n_removed,
      n_hit => u_n_hit, n_unknown => u_n_unknown,
      n_ev_removed => u_n_ev_removed, n_occupied => u_n_occupied
    );

  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;

    -- ---- what the design said, sampled at a DEFINED instant ----
    --
    -- Every valid-gated output (fwd_flood, fwd_valid, fwd_drop, sel_valid)
    -- is only meaningful while its request is asserted. A check placed after
    -- the procedure returns reads them after the deassert, and whether it
    -- sees the pre- or post-deassert value depends on delta-cycle ordering --
    -- which differs between languages. One mutation scored 1516 in Verilog
    -- and 1522 here for exactly that reason, on benches that were otherwise
    -- identical in every other column.
    --
    -- An assertion whose outcome depends on delta ordering is not an
    -- assertion. So the observation is taken once, at a defined instant.
    variable obs_flood, obs_fvalid, obs_fdrop : std_logic := '0';
    variable obs_fport : integer := 0;
    variable obs_uvalid, obs_uunknown : std_logic := '0';
    variable obs_uport : integer := 0;

    -- the shadow table
    type mval_t  is array (0 to N_ENTRY - 1) of std_logic;
    type maddr_t is array (0 to N_ENTRY - 1) of std_logic_vector(7 downto 0);
    type mport_t is array (0 to N_ENTRY - 1) of integer;
    type mage_t  is array (0 to N_ENTRY - 1) of integer;
    variable m_val  : mval_t  := (others => '0');
    variable m_addr : maddr_t := (others => (others => '0'));
    variable m_port : mport_t := (others => 0);
    variable m_age  : mage_t  := (others => 0);

    -- the shadow USB map
    type umval_t  is array (0 to N_ADDR) of std_logic;
    type umport_t is array (0 to N_ADDR) of integer;
    variable um_val  : umval_t  := (others => '0');
    variable um_port : umport_t := (others => 0);

    -- GROUND TRUTH: where each station really is. 255 means "does not
    -- exist", so a frame for it can only be flooded, never mis-delivered.
    type truth_t is array (0 to 255) of integer;
    variable true_port : truth_t := (others => 255);

    -- the measurements
    variable g_frames      : integer := 0;
    variable g_flood       : integer := 0;
    variable g_fwd         : integer := 0;
    variable g_drop        : integer := 0;
    variable g_misdeliver  : integer := 0;
    variable g_deliverable : integer := 0;
    variable g_stale_win   : integer := 0;
    variable g_poison_win  : integer := 0;
    variable u_frames      : integer := 0;
    variable u_deliverable : integer := 0;
    variable u_hit_n       : integer := 0;
    variable u_unknown_n   : integer := 0;
    variable u_misdeliver  : integer := 0;
    variable u_assigned_c  : integer := 0;
    variable u_evrem_c     : integer := 0;

    -- Accumulated across resets: the DUTs' own counters are zeroed by
    -- every reset_dut, so a summary that read them directly would report
    -- only whatever happened after the last one.
    variable e_learn_c, e_relearn_c, e_hit_c, e_flood_c, e_evict_c, e_aged_c
             : integer := 0;

    type reach_t  is array (0 to 4095) of boolean;
    type reachu_t is array (0 to 799) of boolean;
    variable reach   : reach_t  := (others => false);
    variable reach_u : reachu_t := (others => false);
    variable nr, 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"000906D3";
    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);
      -- The HIGH bits. In an LCG with a power-of-two modulus bit i has
      -- period 2**(i+1), so `urand mod 4` off the low bits cycles
      -- 3,0,1,2,... in lockstep while its histogram stays perfectly
      -- uniform. That defect cost a whole chapter's random phase before it
      -- was caught by a cross-language counter comparison.
      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;

    -- ---- model lookup: upward, stop at the first hit ----
    impure function m_hit_f (a : std_logic_vector(7 downto 0)) return boolean is
      variable f : boolean := false;
    begin
      for j in 0 to N_ENTRY - 1 loop
        if m_val(j) = '1' and m_addr(j) = a and not f then f := true; end if;
      end loop;
      return f;
    end function;

    impure function m_port_f (a : std_logic_vector(7 downto 0)) return integer is
      variable f : boolean := false;
      variable p : integer := 0;
    begin
      for j in 0 to N_ENTRY - 1 loop
        if m_val(j) = '1' and m_addr(j) = a and not f then
          p := m_port(j); f := true;
        end if;
      end loop;
      return p;
    end function;

    impure function m_occ_f return integer is
      variable c : integer := 0;
    begin
      for j in 0 to N_ENTRY - 1 loop
        if m_val(j) = '1' then c := c + 1; end if;
      end loop;
      return c;
    end function;

    -- Which entry will the model learn into? Upward-and-stop, again the
    -- opposite direction from the design's downward walk. Strictly-greater
    -- walking up picks the lowest index among the oldest, which is the
    -- same winner the design's >= walking down picks.
    impure function m_learn_idx (a : std_logic_vector(7 downto 0)) return integer is
      variable fs, ff : boolean := false;
      variable si, fi, oi, oa : integer := 0;
    begin
      for j in 0 to N_ENTRY - 1 loop
        if m_val(j) = '1' and m_addr(j) = a and not fs then si := j; fs := true; end if;
        if m_val(j) = '0' and not ff then fi := j; ff := true; end if;
        if m_val(j) = '1' and m_age(j) > oa then oa := m_age(j); oi := j; end if;
      end loop;
      if fs then return si; elsif ff then return fi; else return oi; end if;
    end function;

    impure function um_occ return integer is
      variable c : integer := 0;
    begin
      for j in 1 to N_ADDR loop
        if um_val(j) = '1' then c := c + 1; end if;
      end loop;
      return c;
    end function;

    -- ---------------------------------------------------------------
    --  Offer one frame and check the forwarding decision.
    -- ---------------------------------------------------------------
    procedure frame (src, dst : std_logic_vector(7 downto 0);
                     prt : integer;
                     why_stale, why_poison : boolean) is
      variable e_hit, e_flood, e_fwd, e_drop, e_known_src, e_moved : boolean;
      variable e_port, li : integer;
      variable l0, r0, h0, f0 : integer;
      variable n_out : integer;
    begin
      e_hit       := m_hit_f(dst);
      e_port      := m_port_f(dst);
      e_drop      := e_hit and (e_port = prt);
      e_fwd       := e_hit and not e_drop;
      e_flood     := not e_hit;
      e_known_src := m_hit_f(src);
      e_moved     := e_known_src and (m_port_f(src) /= prt);

      l0 := to_integer(unsigned(n_learn));
      r0 := to_integer(unsigned(n_relearn));
      h0 := to_integer(unsigned(n_hit));
      f0 := to_integer(unsigned(n_flood));

      fr_valid <= '1';
      fr_src   <= src;
      fr_dst   <= dst;
      fr_port  <= std_logic_vector(to_unsigned(prt, 2));
      wait for 1 ns;

      obs_flood  := fwd_flood;
      obs_fvalid := fwd_valid;
      obs_fdrop  := fwd_drop;
      obs_fport  := to_integer(unsigned(fwd_port));

      -- ---- PROPERTY 1: exactly one outcome ----
      --
      -- A frame with no decision is a frame the switch silently lost.
      n_out := 0;
      if fwd_flood = '1' then n_out := n_out + 1; end if;
      if fwd_valid = '1' then n_out := n_out + 1; end if;
      if fwd_drop  = '1' then n_out := n_out + 1; end if;
      ck(n_out = 1, "the switch did not reach exactly one forwarding decision");
      -- ---- PROPERTY 2: a miss floods ----
      ck((fwd_flood = '1') = e_flood,
         "flooding does not correspond to an unknown destination");
      -- ---- PROPERTY 3: a hit off the ingress port forwards ----
      ck((fwd_valid = '1') = e_fwd,
         "forwarding does not correspond to a known destination");
      -- ---- PROPERTY 4: a hit ON the ingress port is dropped ----
      ck((fwd_drop = '1') = e_drop, "a frame for the ingress port was not dropped");
      -- ---- PROPERTY 5: the chosen port is the learned port ----
      if e_fwd then
        ck(to_integer(unsigned(fwd_port)) = e_port,
           "the frame went to a port the table did not name");
      end if;

      -- ---- THE MEASUREMENT: was the decision RIGHT? ----
      --
      -- Not "did it match the table" -- that is property 5. This asks
      -- whether the table matched REALITY, a question only the bench can
      -- ask because only the bench knows where the station is.
      g_frames := g_frames + 1;
      if true_port(to_integer(unsigned(dst))) /= 255 then
        g_deliverable := g_deliverable + 1;
        if fwd_valid = '1' and
           to_integer(unsigned(fwd_port)) /= true_port(to_integer(unsigned(dst))) then
          g_misdeliver := g_misdeliver + 1;
          if why_stale  then g_stale_win  := g_stale_win  + 1; end if;
          if why_poison then g_poison_win := g_poison_win + 1; end if;
        end if;
      end if;
      if fwd_flood = '1' then g_flood := g_flood + 1; end if;
      if fwd_valid = '1' then g_fwd   := g_fwd + 1; end if;
      if fwd_drop  = '1' then g_drop  := g_drop + 1; end if;
      if e_hit then e_hit_c := e_hit_c + 1; else e_flood_c := e_flood_c + 1; end if;
      if e_known_src then
        if e_moved then e_relearn_c := e_relearn_c + 1; end if;
      else
        e_learn_c := e_learn_c + 1;
        if m_occ_f = N_ENTRY then e_evict_c := e_evict_c + 1; end if;
      end if;

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

      -- ---- update the model, then compare occupancy ----
      li := m_learn_idx(src);
      m_val(li)  := '1';
      m_addr(li) := src;
      m_port(li) := prt;
      m_age(li)  := 0;

      -- ---- PROPERTY 6: the tables agree on how full they are ----
      --
      -- Deliberately weaker than entry-by-entry equality: the two tables
      -- search in opposite directions, so they may legitimately place the
      -- same address in different slots after an eviction. What they may
      -- NOT disagree about is how many addresses they know -- and every
      -- lookup above already checks the answers they give.
      ck(to_integer(unsigned(n_occupied)) = m_occ_f,
         "the design and the model disagree about how many addresses are known");

      -- ---- PROPERTY 7: learning is counted correctly ----
      if e_known_src then
        ck(to_integer(unsigned(n_learn)) = l0,
           "a known source was counted as a new address");
        if e_moved then
          ck(to_integer(unsigned(n_relearn)) = r0 + 1, "a station move was miscounted");
        else
          ck(to_integer(unsigned(n_relearn)) = r0, "a station move was miscounted");
        end if;
      else
        ck(to_integer(unsigned(n_learn)) = l0 + 1, "a new address was not counted");
        ck(to_integer(unsigned(n_relearn)) = r0, "a new address was counted as a move");
      end if;
      -- ---- PROPERTY 8: hits and floods are counted correctly ----
      if e_hit then
        ck(to_integer(unsigned(n_hit)) = h0 + 1, "hit counter disagrees with the model");
        ck(to_integer(unsigned(n_flood)) = f0, "flood counter disagrees with the model");
      else
        ck(to_integer(unsigned(n_hit)) = h0, "hit counter disagrees with the model");
        ck(to_integer(unsigned(n_flood)) = f0 + 1, "flood counter disagrees with the model");
      end if;

      steps := steps + 1;
    end procedure;

    -- ---------------------------------------------------------------
    --  One aging tick, mirrored in the model.
    -- ---------------------------------------------------------------
    procedure tick is
    begin
      age_tick <= '1';
      wait until rising_edge(clk);
      wait for 1 ns;
      age_tick <= '0';
      for j in 0 to N_ENTRY - 1 loop
        if m_val(j) = '1' then
          if m_age(j) >= AGE_MAX then
            m_val(j) := '0';
            m_age(j) := 0;
            e_aged_c := e_aged_c + 1;
          else
            m_age(j) := m_age(j) + 1;
          end if;
        end if;
      end loop;
      ck(to_integer(unsigned(n_occupied)) = m_occ_f,
         "the design and the model disagree about occupancy after aging");
      steps := steps + 1;
    end procedure;

    -- ---------------------------------------------------------------
    --  Host writes the map. It knows the answer because it enumerated the
    --  device on that port -- there is nothing to infer.
    -- ---------------------------------------------------------------
    procedure umap_cfg (a : integer; prt : integer; rem_it : boolean) is
    begin
      u_cfg_valid  <= '1';
      u_cfg_addr   <= std_logic_vector(to_unsigned(a, 8));
      u_cfg_port   <= std_logic_vector(to_unsigned(prt, 2));
      if rem_it then u_cfg_remove <= '1'; else u_cfg_remove <= '0'; end if;
      wait until rising_edge(clk);
      wait for 1 ns;
      u_cfg_valid <= '0';
      if a >= 1 and a <= N_ADDR then
        if rem_it then
          um_val(a) := '0';
        else
          um_val(a) := '1';
          um_port(a) := prt;
          u_assigned_c := u_assigned_c + 1;
        end if;
      end if;
      steps := steps + 1;
    end procedure;

    -- ---------------------------------------------------------------
    --  A physical port event. THE mechanism with no Ethernet counterpart:
    --  a disconnect corrects the map before any traffic can be misrouted.
    -- ---------------------------------------------------------------
    procedure uev (prt : integer; conn : boolean) is
      variable e0, expect_cleared : integer;
    begin
      e0 := to_integer(unsigned(u_n_ev_removed));
      expect_cleared := 0;
      if not conn then
        for j in 1 to N_ADDR loop
          if um_val(j) = '1' and um_port(j) = prt then
            expect_cleared := expect_cleared + 1;
          end if;
        end loop;
      end if;

      u_ev_valid <= '1';
      u_ev_port  <= std_logic_vector(to_unsigned(prt, 2));
      if conn then u_ev_connect <= '1'; else u_ev_connect <= '0'; end if;
      wait until rising_edge(clk);
      wait for 1 ns;
      u_ev_valid <= '0';

      if not conn then
        for j in 1 to N_ADDR loop
          if um_val(j) = '1' and um_port(j) = prt then
            um_val(j) := '0';
            u_evrem_c := u_evrem_c + 1;
          end if;
        end loop;
      end if;

      -- ---- PROPERTY U4: a disconnect clears every entry on that port ----
      --
      -- Immediately, with no traffic and no timeout. This is the property
      -- the Ethernet table cannot have, because nothing tells it.
      ck(to_integer(unsigned(u_n_ev_removed)) = e0 + expect_cleared,
         "a disconnect event did not clear the entries on that port");
      ck(to_integer(unsigned(u_n_occupied)) = um_occ,
         "the map and the model disagree about occupancy after an event");
      steps := steps + 1;
    end procedure;

    -- ---------------------------------------------------------------
    --  Query the map and check the answer -- and whether it was RIGHT.
    -- ---------------------------------------------------------------
    procedure uframe (dst : integer) is
      variable e_val_x : boolean;
      variable e_port_x : integer;
      variable h0, k0 : integer;
    begin
      if dst >= 1 and dst <= N_ADDR then
        e_val_x  := (um_val(dst) = '1');
        e_port_x := um_port(dst);
      else
        e_val_x  := false;
        e_port_x := 0;
      end if;
      h0 := to_integer(unsigned(u_n_hit));
      k0 := to_integer(unsigned(u_n_unknown));

      u_fr_valid <= '1';
      u_fr_dst   <= std_logic_vector(to_unsigned(dst, 8));
      wait for 1 ns;

      -- Captured for the same reason as in frame(): sel_valid is gated by
      -- fr_valid, so it is only meaningful at this instant.
      obs_uvalid   := u_sel_valid;
      obs_uunknown := u_sel_unknown;
      obs_uport    := to_integer(unsigned(u_sel_port));

      -- ---- PROPERTY U1: a hit means the host assigned that address ----
      ck((u_sel_valid = '1') = e_val_x,
         "usb map sel_valid does not mean the address was assigned");
      -- ---- PROPERTY U2: the port is the assigned port ----
      if e_val_x then
        ck(to_integer(unsigned(u_sel_port)) = e_port_x,
           "usb map named a port the host did not assign");
      end if;
      -- ---- PROPERTY U3: valid and unknown are exact complements ----
      --
      -- There is no third outcome, and in particular no FLOOD: an unknown
      -- address is not sent everywhere, because the host knows it never
      -- assigned it.
      ck((u_sel_valid xor u_sel_unknown) = '1',
         "usb map did not reach exactly one outcome");
      -- ---- PROPERTY U6: address 0 and out-of-range are never routed ----
      if dst = 0 or dst > N_ADDR then
        ck(u_sel_valid = '0', "usb map routed an unassignable address");
      end if;

      -- ---- THE MEASUREMENT, identical in form to the Ethernet one ----
      u_frames := u_frames + 1;
      if true_port(dst) /= 255 then
        u_deliverable := u_deliverable + 1;
        if u_sel_valid = '1' and
           to_integer(unsigned(u_sel_port)) /= true_port(dst) then
          u_misdeliver := u_misdeliver + 1;
        end if;
      end if;
      if u_sel_valid = '1'   then u_hit_n := u_hit_n + 1; end if;
      if u_sel_unknown = '1' then u_unknown_n := u_unknown_n + 1; end if;

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

      if e_val_x then
        ck(to_integer(unsigned(u_n_hit)) = h0 + 1, "usb map hit counter wrong");
        ck(to_integer(unsigned(u_n_unknown)) = k0, "usb map unknown counter wrong");
      else
        ck(to_integer(unsigned(u_n_hit)) = h0, "usb map hit counter wrong");
        ck(to_integer(unsigned(u_n_unknown)) = k0 + 1, "usb map unknown counter wrong");
      end if;
      steps := steps + 1;
    end procedure;

    procedure reset_dut is
    begin
      rst_n <= '0';
      fr_valid <= '0'; age_tick <= '0';
      u_cfg_valid <= '0'; u_ev_valid <= '0'; u_fr_valid <= '0';
      wait until rising_edge(clk);
      wait until rising_edge(clk);
      rst_n <= '1';
      wait until rising_edge(clk);
      wait for 1 ns;
      m_val  := (others => '0');
      m_addr := (others => (others => '0'));
      m_port := (others => 0);
      m_age  := (others => 0);
      um_val  := (others => '0');
      um_port := (others => 0);
    end procedure;

    procedure nobody_exists is
    begin
      true_port := (others => 255);
    end procedure;

    variable ri, rui : integer;
    variable maskv  : std_logic_vector(3 downto 0);
    variable snap   : integer;

  begin
    nobody_exists;
    reset_dut;

    -- ===============================================================
    --  PHASE 1 (DIRECTED, EXHAUSTIVE) -- every table state crossed with
    --  every probe frame.
    --
    --  For each of the 16 subsets of station 1..4, the members speak once
    --  (station i from port i-1) and then all 8 x 8 x 4 probe frames are
    --  offered against that table. 4096 points, all reachable.
    -- ===============================================================
    for mask in 0 to 15 loop
      maskv := std_logic_vector(to_unsigned(mask, 4));
      reset_dut;
      nobody_exists;
      for j in 0 to 3 loop
        if maskv(j) = '1' then
          true_port(j + 1) := j;
          frame(std_logic_vector(to_unsigned(j + 1, 8)), x"F0", j, false, false);
        end if;
      end loop;
      for s in 0 to 7 loop
        for d in 0 to 7 loop
          for p in 0 to 3 loop
            -- The probe's source is learned too: a switch cannot choose
            -- not to learn. So each probe perturbs the table, which is
            -- realistic and is why the model is updated in lockstep
            -- rather than recomputed from the mask.
            frame(std_logic_vector(to_unsigned(s, 8)),
                  std_logic_vector(to_unsigned(d, 8)), p, false, false);
            ri := ((mask * 8 + s) * 8 + d) * 4 + p;
            reach(ri) := true;
          end loop;
        end loop;
      end loop;
    end loop;

    -- ===============================================================
    --  PHASE 2 (DIRECTED) -- A STATION MOVES.
    --
    --  THE measurement. Station 1 lives on port 0 and has spoken, so the
    --  table knows it. Then it is moved to port 3 and says nothing,
    --  because a station that has just been plugged in usually has
    --  nothing to send.
    --
    --  Every frame for station 1 in that window goes to port 0, where
    --  station 1 is not. The switch behaves exactly as designed and is
    --  wrong anyway, and nothing in the protocol can tell it so.
    -- ===============================================================
    reset_dut;
    nobody_exists;
    true_port(1) := 0;
    frame(x"01", x"F0", 0, false, false);
    true_port(2) := 1;
    frame(x"02", x"01", 1, false, false);
    ck((obs_fport) = 0, "traffic for station 1 did not go to port 0");

    true_port(1) := 3;                    -- physically now on port 3
    for k in 0 to 5 loop
      frame(x"02", x"01", 1, true, false);  -- why_stale
    end loop;

    snap := to_integer(unsigned(n_relearn));
    frame(x"01", x"F0", 3, false, false);
    ck(to_integer(unsigned(n_relearn)) = snap + 1,
       "a station moving ports was not recorded as a move");
    frame(x"02", x"01", 1, false, false);
    ck((obs_fport) = 3,
       "after the station spoke, traffic still went to the old port");

    -- ===============================================================
    --  PHASE 3 (DIRECTED) -- AGING.
    -- ===============================================================
    reset_dut;
    nobody_exists;
    true_port(1) := 0;
    true_port(2) := 1;
    frame(x"01", x"F0", 0, false, false);
    frame(x"02", x"F0", 1, false, false);
    ck(to_integer(unsigned(n_occupied)) = 2,
       "two stations spoke and the table does not hold two");
    for k in 0 to AGE_MAX + 1 loop tick; end loop;
    ck(to_integer(unsigned(n_occupied)) = 0, "entries did not expire");
    frame(x"03", x"01", 2, false, false);
    ck(obs_flood = '1', "a frame for an expired station was not flooded");

    reset_dut;
    nobody_exists;
    true_port(1) := 0;
    for k in 0 to 3 * AGE_MAX - 1 loop
      frame(x"01", x"F0", 0, false, false);
      tick;
    end loop;
    ck(to_integer(unsigned(n_occupied)) = 1,
       "a station that kept speaking was still forgotten");

    -- ===============================================================
    --  PHASE 4 (DIRECTED) -- THE TABLE IS FULL.
    -- ===============================================================
    reset_dut;
    nobody_exists;
    for k in 0 to 3 loop
      true_port(k + 1) := k;
      frame(std_logic_vector(to_unsigned(k + 1, 8)), x"F0", k, false, false);
    end loop;
    ck(to_integer(unsigned(n_occupied)) = 4, "four stations did not fill four entries");
    snap := to_integer(unsigned(n_evict));
    true_port(5) := 0;
    frame(x"05", x"F0", 0, false, false);
    ck(to_integer(unsigned(n_evict)) = snap + 1,
       "a full table did not report an eviction");
    ck(to_integer(unsigned(n_occupied)) = 4,
       "an eviction changed how many entries are valid");
    snap := to_integer(unsigned(n_flood));
    for k in 1 to 5 loop
      frame(x"06", std_logic_vector(to_unsigned(k, 8)), 2, false, false);
    end loop;
    ck(to_integer(unsigned(n_flood)) >= snap + 1,
       "with five stations and four entries, nothing flooded");

    -- ===============================================================
    --  PHASE 4b (DIRECTED, EXHAUSTIVE over which entry is oldest)
    --
    --  A mutation that always evicts entry 0 instead of the oldest scored
    --  ZERO against everything above, because entry 0 held the oldest
    --  station anyway: the first address learned takes the lowest free
    --  slot, so the two policies agreed on every reachable case.
    --
    --  The fix is the RIGHT stimulus, not more of it. Populate, age
    --  everything, then refresh every station EXCEPT one, which leaves
    --  that one uniquely oldest in a known entry. Three of the four
    --  arrangements put the oldest somewhere other than entry 0.
    -- ===============================================================
    for j in 0 to 3 loop
      reset_dut;
      nobody_exists;
      for k in 0 to 3 loop true_port(k + 1) := k; end loop;

      frame(x"01", x"F0", 0, false, false); tick;
      frame(x"02", x"F0", 1, false, false); tick;
      frame(x"03", x"F0", 2, false, false); tick;
      frame(x"04", x"F0", 3, false, false); tick;

      for k in 0 to 3 loop
        if k /= j then
          frame(std_logic_vector(to_unsigned(k + 1, 8)), x"F0", k, false, false);
        end if;
      end loop;
      ck(to_integer(unsigned(n_occupied)) = 4,
         "the table should hold four stations here");

      true_port(5) := 0;
      snap := to_integer(unsigned(n_evict));
      frame(x"05", x"F0", 0, false, false);
      ck(to_integer(unsigned(n_evict)) = snap + 1,
         "a full table did not report an eviction");
      ck(to_integer(unsigned(n_occupied)) = 4,
         "an eviction changed how many entries are valid");

      -- Probe all four original stations from station 5, which is already
      -- in the table so the probes cannot evict anything. PROPERTY 2
      -- inside frame() compares flood-versus-hit station by station,
      -- which is what distinguishes the two eviction policies.
      for k in 0 to 3 loop
        frame(x"05", std_logic_vector(to_unsigned(k + 1, 8)), 0, false, false);
      end loop;

      frame(x"05", std_logic_vector(to_unsigned(j + 1, 8)), 0, false, false);
      ck(obs_flood = '1',
         "the entry that was not refreshed was not the one evicted");
    end loop;

    -- ===============================================================
    --  PHASE 4c (DIRECTED) -- WHAT AGING IS FOR.
    --
    --  Aging looks like pure loss: it throws away a correct entry. Its
    --  purpose is this case, and a suite that only checks that entries
    --  disappear has not tested the purpose. A station is removed and
    --  nothing tells the switch; the ONLY thing that stops its traffic
    --  going to a port it has left is the entry expiring.
    -- ===============================================================
    for p in 0 to 3 loop
      reset_dut;
      nobody_exists;
      true_port(1) := p;
      frame(x"01", x"F0", p, false, false);
      ck(to_integer(unsigned(n_occupied)) = 1, "the station was not learned");
      true_port(1) := 255;                  -- removed, and nothing reports it
      for k in 0 to AGE_MAX + 1 loop tick; end loop;
      ck(to_integer(unsigned(n_occupied)) = 0,
         "the entry for a departed station did not expire");
      for k in 0 to 3 loop
        frame(x"07", x"01", (p + 1) mod 4, false, false);
        ck(obs_flood = '1',
           "traffic for a departed station was still sent to the port it left");
        ck(obs_fvalid = '0',
           "the table named a port for a station that had expired");
      end loop;
    end loop;

    -- ===============================================================
    --  PHASE 5 (DIRECTED) -- A FORGED SOURCE ADDRESS.
    --
    --  The table believes the newest evidence, because that is the only
    --  policy that converges. So an attacker who sends ONE frame claiming
    --  to be the victim moves the victim's entry to the attacker's port.
    --
    --  This is not a defect in this module. It is what learning MEANS,
    --  and it is why real switches need port security, sticky MACs and
    --  802.1X -- three mechanisms that exist to put an authority back into
    --  a protocol that does not have one.
    -- ===============================================================
    reset_dut;
    nobody_exists;
    true_port(1) := 0;     -- the victim, on port 0
    true_port(9) := 2;     -- the attacker, on port 2
    frame(x"01", x"F0", 0, false, false);
    true_port(2) := 1;
    frame(x"02", x"01", 1, false, false);
    ck((obs_fport) = 0,
       "traffic for the victim did not reach its real port");

    frame(x"01", x"F0", 2, false, false);   -- "I am station 1", from port 2
    for k in 0 to 5 loop
      frame(x"02", x"01", 1, false, true);  -- why_poison
    end loop;
    ck((obs_fport) = 2,
       "the forged frame did not redirect the victim's traffic");

    -- ===============================================================
    --  PHASE 6 (RANDOM)
    -- ===============================================================
    if not DIRECTED_ONLY then
      reset_dut;
      nobody_exists;
      for k in 1 to 6 loop true_port(k) := (k - 1) mod 4; end loop;
      for k in 0 to 599 loop
        if (urand mod 11) = 0 then tick; end if;
        frame(std_logic_vector(to_unsigned(1 + (urand mod 6), 8)),
              std_logic_vector(to_unsigned(1 + (urand mod 6), 8)),
              urand mod 4, false, false);
      end loop;
    end if;

    -- ===============================================================
    --  PHASE 7 (DIRECTED, EXHAUSTIVE) -- the USB map.
    --
    --  Every subset of assigned addresses, crossed with every port
    --  disconnect event and every query. The event dimension is the one
    --  that matters -- it is the mechanism Ethernet has no counterpart
    --  for, so it is swept rather than demonstrated once. 5 x 16 x 10.
    -- ===============================================================
    for evm in 0 to 4 loop
      for mask in 0 to 15 loop
        maskv := std_logic_vector(to_unsigned(mask, 4));
        reset_dut;
        nobody_exists;
        for j in 0 to 3 loop
          if maskv(j) = '1' then
            true_port(j + 1) := j;
            umap_cfg(j + 1, j, false);
          end if;
        end loop;
        -- In four of the five modes a device is unplugged. Ground truth is
        -- updated FIRST, so a map that failed to react would be measured
        -- as mis-delivering rather than merely as out of date.
        if evm > 0 then
          for j in 1 to 4 loop
            if true_port(j) = evm - 1 then true_port(j) := 255; end if;
          end loop;
          uev(evm - 1, false);
        end if;
        for q in 0 to 9 loop
          uframe(q);
          rui := (evm * 16 + mask) * 10 + q;
          reach_u(rui) := true;
        end loop;
      end loop;
    end loop;

    -- ===============================================================
    --  PHASE 8 (DIRECTED) -- THE SAME MOVE, ON BOTH DESIGNS.
    --
    --  Phase 2 moved a station and measured six mis-delivered frames. The
    --  identical scenario now runs through the USB map, and the only
    --  difference in the stimulus is the one the protocol actually
    --  provides: the port reports the disconnect.
    --
    --  Nothing else is given to the USB side -- no extra traffic, no
    --  shorter timeout, no cleverer table. Only the event.
    -- ===============================================================
    reset_dut;
    nobody_exists;
    true_port(1) := 0;
    umap_cfg(1, 0, false);
    uframe(1);
    ck((obs_uport) = 0,
       "the map did not route device 1 to port 0");

    true_port(1) := 255;              -- in transit, physically nowhere
    uev(0, false);                    -- the PORT reports it
    for k in 0 to 5 loop
      uframe(1);
      -- THE COMPARISON. Not "the map is empty" but "the map does not name
      -- a stale port" -- a map that still answered would answer with
      -- port 0, where the device provably is not.
      ck(obs_uvalid = '0',
         "the map still routed a device whose port reported a disconnect");
    end loop;

    uev(3, true);                     -- connect event
    true_port(1) := 3;
    umap_cfg(1, 3, false);            -- the host assigns it again
    uframe(1);
    ck((obs_uport) = 3,
       "after re-enumeration the map did not follow the device");

    nr := 0;
    for i in 0 to 4095 loop
      if reach(i) then nr := nr + 1; end if;
    end loop;
    nru := 0;
    for i in 0 to 799 loop
      if reach_u(i) then nru := nru + 1; end if;
    end loop;

    write(lo, string'("steps=") & integer'image(steps) &
              string'(" checks=") & integer'image(checks) &
              string'(" reach_eth=") & integer'image(nr) &
              string'("/4096 reach_usb=") & integer'image(nru) &
              string'("/800 errors=") & integer'image(errors));
    writeline(output, lo);
    write(lo, string'("[eth] learned=") & integer'image(e_learn_c) &
              string'(" relearned=") & integer'image(e_relearn_c) &
              string'(" hits=") & integer'image(e_hit_c) &
              string'(" floods=") & integer'image(e_flood_c) &
              string'(" evictions=") & integer'image(e_evict_c) &
              string'(" aged=") & integer'image(e_aged_c));
    writeline(output, lo);
    write(lo, string'("[eth] frames=") & integer'image(g_frames) &
              string'(" forwarded=") & integer'image(g_fwd) &
              string'(" flooded=") & integer'image(g_flood) &
              string'(" dropped=") & integer'image(g_drop));
    writeline(output, lo);
    write(lo, string'("--- what learning costs that assignment does not ---"));
    writeline(output, lo);
    write(lo, string'("[eth] frames whose destination exists   = ") &
              integer'image(g_deliverable));
    writeline(output, lo);
    write(lo, string'("[eth] delivered to the WRONG port       = ") &
              integer'image(g_misdeliver));
    writeline(output, lo);
    write(lo, string'("[eth]   ... because a station had moved = ") &
              integer'image(g_stale_win));
    writeline(output, lo);
    write(lo, string'("[eth]   ... because a source was forged = ") &
              integer'image(g_poison_win));
    writeline(output, lo);
    write(lo, string'("--- the same question, answered by an authority ---"));
    writeline(output, lo);
    write(lo, string'("[usb] assigned=") & integer'image(u_assigned_c) &
              string'(" removed_by_PORT_EVENT=") & integer'image(u_evrem_c));
    writeline(output, lo);
    write(lo, string'("[usb] queries=") & integer'image(u_frames) &
              string'(" answered=") & integer'image(u_hit_n) &
              string'(" unknown=") & integer'image(u_unknown_n) &
              string'(" flooded=0 (no such outcome)"));
    writeline(output, lo);
    write(lo, string'("[usb] queries whose device exists      = ") &
              integer'image(u_deliverable));
    writeline(output, lo);
    write(lo, string'("[usb] routed to the WRONG port         = ") &
              integer'image(u_misdeliver));
    writeline(output, lo);

    if nr /= 4096 or nru /= 800 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;

11. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
//  Properties for the learned table and the assigned map.
//
//  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.
//
//  As in chapter 28.2, read the SHAPE of the two groups. The learned
//  table's properties are all internal consistency: it cannot promise
//  anything about correctness, because correctness is a property of a
//  network it cannot see. The assigned map's properties include one about
//  the OUTSIDE WORLD -- and it can, because a port event tells it.
// ---------------------------------------------------------------------
module lt_sva #(parameter int N_PORT = 4, parameter int N_ENTRY = 4,
                parameter int N_ADDR = 8) (
  input logic        clk,
  input logic        rst_n,
  // learned table
  input logic        fr_valid,
  input logic [7:0]  fr_src,
  input logic [7:0]  fr_dst,
  input logic [1:0]  fr_port,
  input logic        age_tick,
  input logic        fwd_valid,
  input logic [1:0]  fwd_port,
  input logic        fwd_flood,
  input logic        fwd_drop,
  input logic [31:0] n_learn,
  input logic [31:0] n_occupied,
  // assigned map
  input logic        ev_valid,
  input logic        ev_connect,
  input logic [1:0]  ev_port,
  input logic        u_fr_valid,
  input logic        u_sel_valid,
  input logic [1:0]  u_sel_port,
  input logic        u_sel_unknown,
  input logic [31:0] u_n_occupied
);

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

  // ---- 1. exactly one forwarding decision ----
  //
  // A frame with no decision is a frame the switch silently lost, and a
  // frame with two is a frame whose fate depends on which consumer wins.
  a_one_outcome : assert property
    (fr_valid |-> ($countones({fwd_flood, fwd_valid, fwd_drop}) == 1));

  // ---- 2. no frame, no decision ----
  a_gated : assert property
    (!fr_valid |-> (!fwd_flood && !fwd_valid && !fwd_drop));

  // ---- 3. a forwarded frame never goes back out the ingress port ----
  //
  // A one-hop loop, and on a real link it doubles the traffic for no
  // benefit.
  a_no_hairpin : assert property (fwd_valid |-> (fwd_port != fr_port));

  // ---- 4. flooding and forwarding are exclusive ----
  a_excl : assert property (!(fwd_flood && fwd_valid));

  // ---- 5. the table never exceeds its capacity ----
  //
  // The property an eviction policy has to preserve. A table that could
  // hold N+1 entries would make every capacity measurement meaningless.
  a_capacity : assert property (n_occupied <= N_ENTRY);

  // ---- 6. a frame always teaches the table something ----
  //
  // A switch cannot choose not to learn. Either the source was already
  // known, or the count of known addresses went up, or an entry was
  // displaced -- but the source is in the table afterwards either way.
  a_always_learns : assert property (fr_valid |=> (n_occupied > 0));

  // ---- 7. aging only ever removes ----
  //
  // A tick must never invent an entry. Stated because the aging and
  // learning paths write the same registers in the same cycle, and the
  // priority between them is a real design decision.
  a_age_monotone : assert property
    ((age_tick && !fr_valid) |=> (n_occupied <= $past(n_occupied)));

  // ---- 8. THE ONE THE LEARNED TABLE CANNOT HAVE ----
  //
  // After a port reports a disconnect, the assigned map must not name that
  // port for anybody. This is a property about the OUTSIDE WORLD, and the
  // map can promise it only because the port told it.
  //
  // There is no counterpart for eth_learn_table. Not a weaker version --
  // none at all, because nothing tells it.
  property p_event_clears;
    (ev_valid && !ev_connect) |=>
      always (u_sel_valid |-> (u_sel_port != $past(ev_port)));
  endproperty
  a_event_clears : assert property (p_event_clears);

  // ---- 9. the assigned map has exactly two outcomes ----
  //
  // No flood. An unknown address is answered "no record", which is a
  // different statement from "I have a record and it is stale".
  a_two_outcomes : assert property
    (u_fr_valid |-> (u_sel_valid ^ u_sel_unknown));

  // ---- COVER: the failure modes are reached ----
  // Assertions over stimulus that never floods, never ages anything out and
  // never disconnects a port prove nothing. These are the denominator.
  c_flood   : cover property (fwd_flood);
  c_drop    : cover property (fwd_drop);
  c_full    : cover property (n_occupied == N_ENTRY);
  c_aged    : cover property (age_tick && (n_occupied < $past(n_occupied)));
  c_event   : cover property (ev_valid && !ev_connect && (u_n_occupied > 0));
  c_unknown : cover property (u_sel_unknown);

endmodule

12. Where UVM Fits

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
//  UVM structure for the two forwarding mechanisms.
//
//  NOT SIMULATED IN THIS CHAPTER. Icarus cannot compile UVM -- it breaks
//  on virtual method dispatch -- so every number comes from the procedural
//  benches. This is the structure a production environment would use.
//
//  THE DESIGN DECISION: the environment owns a TOPOLOGY MODEL, and the
//  sequences move stations around in it. That is what makes the
//  measurement possible at all -- if the sequence library only described
//  frames, the scoreboard would have no way to know where anything really
//  was, and "mis-delivered" would be unanswerable.
// ---------------------------------------------------------------------

// ---- the topology IS the reference model ----
//
// Not a scoreboard helper: the thing the whole environment is about. It
// holds where each station physically is, which is information neither DUT
// has and the learned table can only guess at.
class topology_model extends uvm_object;
  `uvm_object_utils(topology_model)

  // 255 means "this station is not on the network", so a frame for it can
  // only be flooded, never mis-delivered.
  int unsigned where_is [256];

  function new(string name = "topology_model");
    super.new(name);
    foreach (where_is[i]) where_is[i] = 255;
  endfunction

  // A station moving is an event in the MODEL first and the DUTs second.
  // That ordering matters: update ground truth before the stimulus, so a
  // design that fails to react is measured as mis-delivering rather than
  // merely as out of date.
  function void move(int station, int to_port);
    where_is[station] = to_port;
  endfunction

  function void remove(int station);
    where_is[station] = 255;
  endfunction
endclass

// ---- one abstract event, two very different agents ----
class net_event extends uvm_sequence_item;
  `uvm_object_utils(net_event)

  typedef enum { EV_FRAME, EV_MOVE, EV_REMOVE, EV_AGE_TICK, EV_FORGE } kind_e;

  rand kind_e kind;
  rand int    src;
  rand int    dst;
  rand int    ingress_port;
  rand int    new_port;

  constraint c_sane {
    src          inside {[1:6]};
    dst          inside {[1:6]};
    ingress_port inside {[0:3]};
    new_port     inside {[0:3]};
  }
  // Frames dominate, because a topology that changes as often as it is used
  // measures a broken network rather than a working one. An earlier chapter
  // in this track learned this the expensive way: an unbiased distribution
  // left the two interesting outcome classes barely exercised.
  constraint c_realistic {
    kind dist { EV_FRAME := 80, EV_AGE_TICK := 10,
                EV_MOVE := 5, EV_REMOVE := 3, EV_FORGE := 2 };
  }

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

// ---- the scoreboard measures DETECTABILITY, not just correctness ----
//
// Both designs are expected to answer consistently with their own
// contents; that is what the assertions are for. What this counts is
// whether the answer matched REALITY -- a question only the topology model
// can settle.
class forwarding_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(forwarding_scoreboard)

  topology_model topo;

  int unsigned eth_deliverable, eth_misdelivered, eth_flooded;
  int unsigned usb_deliverable, usb_misdelivered, usb_unknown;

  // Attribution: which mechanism was responsible for each mis-delivery.
  // Without this the total is a number with no cause attached, and a
  // number with no cause cannot be designed against.
  int unsigned because_stale, because_forged, because_full;

  function void report_phase(uvm_phase phase);
    `uvm_info("FWD", $sformatf(
      "eth: %0d/%0d mis-delivered (%0d stale, %0d forged, %0d table-full), %0d flooded",
      eth_misdelivered, eth_deliverable,
      because_stale, because_forged, because_full, eth_flooded), UVM_LOW)
    `uvm_info("FWD", $sformatf(
      "usb: %0d/%0d mis-delivered, %0d answered 'no record'",
      usb_misdelivered, usb_deliverable, usb_unknown), UVM_LOW)

    // The USB figure is expected to be ZERO, and a non-zero value is a bug
    // in the ENVIRONMENT before it is a bug in the design: it would mean a
    // move happened without the port event that USB hardware always
    // generates. An environment that can move a device silently is
    // measuring a bus nobody builds.
    if (usb_misdelivered != 0)
      `uvm_error("FWD",
        "the assigned map mis-delivered: check that every move raised a port event")
  endfunction
endclass

// ---- coverage: the CAUSES, not just the frames ----
class forwarding_coverage extends uvm_subscriber #(net_event);
  `uvm_component_utils(forwarding_coverage)

  covergroup cg with function sample(net_event e);
    cp_kind : coverpoint e.kind;
    cp_port : coverpoint e.ingress_port { bins p[] = {[0:3]}; }
    // The cross that matters: a move to each port from each port, because
    // the stale window depends on both and a single move proves nothing
    // about the others.
    x_move : cross cp_port, cp_kind {
      ignore_bins not_a_move = binsof(cp_kind) with
        (cp_kind != net_event::EV_MOVE);
    }
  endgroup

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

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

13. Mutation Testing

Nine defects, six in the learned table and three in the assigned map, injected one at a time into all three languages. Every replacement asserted; each mutation generated as its own file.

#the injected defectV-allV-dirSV-allSV-dirVHDL-allVHDL-dir
BASEunmodified designs000000
M1ingress-port frames forwarded back out918714918714918714
M2an unknown destination is dropped, not flooded506746475067464750494647
M3a station that moves is never relocated377533163775331638213316
M4entries never expire133133133133133133
M5eviction always takes entry 0153039153039124239
M6the destination is learned, not the source207081834620708183462060518346
M7Ethernet's staleness, injected into the map187187187187187187
M8an assignment records the address, not the port318318318318318318
M9every query is answered, assigned or not152215221522152215221522

Every mutation is killed, and every one by directed stimulus alone. The directed column is identical across all three languages at all nine rows.

M7 is the mutation this chapter was built for

M7 deletes the port-disconnect handling from usb_hub_route_map. Nothing else changes: the host still assigns, the map still answers, every other outcome is untouched. What it removes is the one mechanism Ethernet has no counterpart for.

It scores 187, entirely directed, identically in all three languages.

M5's 39 is the honest kind of small number

M5 is the eviction-policy mutation that scored zero until the stimulus was fixed — see section 6. Its journey is worth recording because each step tested a different idea about what "not caught" means:

state of the suiteM5 directed scorewhat it meant
4096-point sweep + full-table phase0the two policies agreed on every reachable case
plus a staggered-age eviction phase6one arrangement discriminates
plus sweeping which entry is oldest39three of four arrangements discriminate

A score of zero was not evidence of a weak suite in the usual sense. It was evidence that the suite could not construct a state in which the two policies differ — a different problem needing a different fix. More random cycles would eventually have found it by luck; the right stimulus finds it by construction.

Run totals

stepscheckseth reachusb reacherrors
Verilog, full601549,4054096 / 4096800 / 8000
Verilog, directed only537143,6734096 / 4096800 / 8000
SystemVerilog, full601549,4054096 / 4096800 / 8000
SystemVerilog, directed only537143,6734096 / 4096800 / 8000
VHDL, full604049,4394096 / 4096800 / 8000
VHDL, directed only537143,6734096 / 4096800 / 8000

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

14. What This Does Not Cover

No packet path. Neither module moves data. The comparison is about which port, and a data path would add volume without adding a difference.

Four ports, four entries, eight addresses. The sweeps are exhaustive at those sizes. The conclusions generalise — a switch with no authority must learn for any N — but the numbers in section 5 are for this configuration.

No electrical modelling. A flood is counted as one event; on real hardware it is a frame on every link, and the cost depends on the topology.

One switch. The stale window measured here closes when the station transmits. In a multi-switch network it is worse: a station that moves between switches leaves a stale entry on the first one that no traffic through the second one will ever correct, which is why real networks need topology change notifications. That mechanism is out of scope, and its absence is the reason the figures here are a lower bound.

Aging is modelled as a tick input. Real switches age on a wall clock. The tick makes the timer reachable in simulation; nothing about the mechanism depends on its period.

DHCP, ARP and 802.1X are discussed, not built. Section 2 argues structurally that DHCP cannot help the forwarding layer. That is an argument about which layer holds which information, not a measurement, and it is labelled as such.

USB hub routing is simplified to a map. A real hub repeats downstream traffic to all enabled ports and uses the address only for upstream routing and for enable state. usb_hub_route_map models the mapping, which is the half the comparison is about.

15. The Interview Answer

Three sentences, and do not start with bandwidth.

1. Name the mechanism. "USB has one authority — the host assigns every address at enumeration. Ethernet has none, so a switch has to infer the address-to-port mapping by watching source addresses."

2. Name what inference costs. "An inferred table can be missing, stale, full or forged. A learned entry is wrong for the entire window between a station moving and it next transmitting, and nothing in the protocol shortens that window. An assigned map is corrected by the port's own disconnect event, before any traffic can be misrouted."

3. Name what it buys, because this is the half people forget. "Learning is what lets Ethernet span networks with no central authority at all — across administrative boundaries, with equipment from vendors who have never heard of each other. USB cannot do that in principle, not because it is slower but because it needs a host, and there is exactly one."

16. What Carries Forward

Three comparisons, three mechanisms, three measured costs:

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
28.3, Ethernetan authority to assign addresses294 of 1065 mis-delivered, vs 0 of 130

All three costs are 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. Ethernet's switch decides where a station is from traffic alone. In every case the protocol that spends more on delivering information gets a capability that cannot be retrofitted — and in every case it gives something up to pay for it.

The last comparison changes what is being addressed. So far every chapter has been about naming a peer. PCIe's problem is not finding the peer — there is usually exactly one, and it is soldered down. PCIe's problem is that it allows many transactions to be outstanding at once, and then has to say which completion belongs to which request.

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.