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 exists | delivered to the wrong port | |
|---|---|---|
| Ethernet, learned table | 1065 | 294 |
| USB, assigned map | 130 | 0 |
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
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_table | usb_hub_route_map | |
|---|---|---|
| where the mapping comes from | inferred from fr_src | written by the host |
| is there a source-address port | yes — that is the evidence | no — nothing to infer from |
| can traffic change the mapping | yes | no |
| aging | required | none — a record does not decay |
| eviction | required | none — indexed by address |
| unknown destination | flooded to every port | answered "no record" |
| a device leaves | found out from traffic, eventually | a 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)
// =====================================================================
// 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
endmodule5. 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 exist | delivered to the wrong port | rate | |
|---|---|---|---|
eth_learn_table | 1065 | 294 | 27.6% |
usb_hub_route_map | 130 | 0 | 0% |
And the two named causes, each measured within its own window:
| cause | frames in the window | mis-delivered |
|---|---|---|
| a station moved and had not yet spoken | 6 | 6 (100%) |
| a source address was forged | 6 | 6 (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":
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");
endThe same move, seen by both mechanisms
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:
forgetting a live station costs one flood -- cheap
remembering a dead station costs mis-delivery -- for as long as the
memory lastsThat 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)
// =====================================================================
// 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
endmodule8. 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
// =====================================================================
// 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
endmoduleThe SystemVerilog testbench
// =====================================================================
// 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
endmoduleSame 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
-- =====================================================================
-- 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
-- =====================================================================
-- 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
// ---------------------------------------------------------------------
// 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);
endmodule12. Where UVM Fits
// ---------------------------------------------------------------------
// 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
endclass13. 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 defect | V-all | V-dir | SV-all | SV-dir | VHDL-all | VHDL-dir |
|---|---|---|---|---|---|---|---|
| BASE | unmodified designs | 0 | 0 | 0 | 0 | 0 | 0 |
| M1 | ingress-port frames forwarded back out | 918 | 714 | 918 | 714 | 918 | 714 |
| M2 | an unknown destination is dropped, not flooded | 5067 | 4647 | 5067 | 4647 | 5049 | 4647 |
| M3 | a station that moves is never relocated | 3775 | 3316 | 3775 | 3316 | 3821 | 3316 |
| M4 | entries never expire | 133 | 133 | 133 | 133 | 133 | 133 |
| M5 | eviction always takes entry 0 | 1530 | 39 | 1530 | 39 | 1242 | 39 |
| M6 | the destination is learned, not the source | 20708 | 18346 | 20708 | 18346 | 20605 | 18346 |
| M7 | Ethernet's staleness, injected into the map | 187 | 187 | 187 | 187 | 187 | 187 |
| M8 | an assignment records the address, not the port | 318 | 318 | 318 | 318 | 318 | 318 |
| M9 | every query is answered, assigned or not | 1522 | 1522 | 1522 | 1522 | 1522 | 1522 |
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 suite | M5 directed score | what it meant |
|---|---|---|
| 4096-point sweep + full-table phase | 0 | the two policies agreed on every reachable case |
| plus a staggered-age eviction phase | 6 | one arrangement discriminates |
| plus sweeping which entry is oldest | 39 | three 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
| steps | checks | eth reach | usb reach | errors | |
|---|---|---|---|---|---|
| Verilog, full | 6015 | 49,405 | 4096 / 4096 | 800 / 800 | 0 |
| Verilog, directed only | 5371 | 43,673 | 4096 / 4096 | 800 / 800 | 0 |
| SystemVerilog, full | 6015 | 49,405 | 4096 / 4096 | 800 / 800 | 0 |
| SystemVerilog, directed only | 5371 | 43,673 | 4096 / 4096 | 800 / 800 | 0 |
| VHDL, full | 6040 | 49,439 | 4096 / 4096 | 800 / 800 | 0 |
| VHDL, directed only | 5371 | 43,673 | 4096 / 4096 | 800 / 800 | 0 |
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:
| chapter | the mechanism the other protocol lacks | the measured cost |
|---|---|---|
| 28.1, UART | synchronisation from a single edge | a tolerance budget shrinking as 1/N |
| 28.2, SPI | asking a device who it is | 0 of 11 failures detectable |
| 28.3, Ethernet | an authority to assign addresses | 294 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
Related tutorials
- Related topic
USB vs UART
UART spends zero wires on synchronisation and pays a tolerance budget that shrinks as the frame grows; USB spends a SYNC field, an encoding rule and a PLL to buy that budget away — measured across 5376 exhaustive points, not quoted.
- Related topic
USB vs SPI
SPI selects a peripheral with a wire routed at layout time and USB with an address the host assigned — so a chip-select contention is invisible to every slave (0 of 11) while a duplicate USB address is detected every time (274 of 274).
- Related topic
USB vs PCIe
USB holds one transaction outstanding per endpoint so its throughput is exactly 1/(latency+1) whatever the wire carries; PCIe tags many at once and needs exactly latency+1 tags to saturate — both measured as closed forms over 64 points.
- Related topic
USB Flash Drives
Every flash drive speaks Bulk-Only Transport — CBW out, data, CSW in. The spec enumerates thirteen cases of host-versus-device disagreement, six of them fatal, and the two rarest are the ones that ship broken.
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.
