USB · Module 23
Device Controller RTL
Every device on a USB bus sees every token, so the decode that answers 'is this mine?' is the only thing preventing two devices driving the same wire — and the select must be one-hot by construction.
Module 21 built a device controller from the protocol's rules. This module is about the RTL engineering underneath it — decode, arbitration, memory sizing, state-machine resynchronisation, and clock-domain crossing. Different concerns, different failure modes, and the first one starts at the very front of the device.
1. Every Device Sees Every Token
USB is a broadcast medium. A hub repeats a downstream token to every enabled port, so a token addressed to one device is physically received by all of them.
The only thing that stops six devices answering at once is that five of them look at the address field and say nothing.
2. The Address Compare Is an Equality, and Nothing Else
A device responds to exactly one address: its own. There is no wildcard, no broadcast address, and no "also respond to 0 just in case".
Address 0 feels like it should be special, and it is — but not here. A device in the DEFAULT state has address 0 (Chapter 21.5), so a plain equality already covers it.
3. The Address Is Seven Bits
Not eight. Not six.
A seven-bit compare done in six bits makes address 64 indistinguishable from address 0, so a device at address 64 answers the enumeration traffic meant for whatever was just plugged in.
That failure needs 65 devices on one bus before it can happen — which is why it survives bring-up, survives the lab, and appears at a customer site with a large hub tree. Mutation Q6.
4. A Non-Existent Endpoint Gets Silence
The second question the decode answers is "does the endpoint exist?", and the answer changes what the device does in a way that is easy to get backwards:
| Condition | Response |
|---|---|
| endpoint exists, halted | STALL (Chapter 21.1) |
| endpoint does not exist | nothing at all |
A STALL is a statement about an endpoint: "this one is halted." Sending it for an endpoint that does not exist tells the host the endpoint does exist and is merely stalled — so the host issues CLEAR_FEATURE(ENDPOINT_HALT) and tries again. For ever.
Silence lets the host's timeout conclude what is actually true.
Two questions, four outcomes — and three of them are 'drive nothing'
5. And the Select Must Be One-Hot by Construction
Each endpoint is a block with its own state and its own FIFO port. They share a datapath, so two selects asserted at once is a contention inside the device — and unlike the bus contention in §1, nothing external will ever reveal it.
So the decode produces a one-hot-or-zero vector, and it is built by shifting a single bit:
assign ep_select = ep_exists ? (1 << token_ep) : 0;rather than by a set of comparisons:
// what NOT to do -- a mistake in one comparison produces CONTENTION
assign ep_select[0] = ep_exists && (token_ep == 0);
assign ep_select[1] = ep_exists && (token_ep == 1);
assign ep_select[2] = ep_exists && (token_ep == 1); // typo: two hot
assign ep_select[3] = ep_exists && (token_ep == 3);The difference is what a mistake costs. In the shift form a wrong index selects the wrong endpoint — bad, and visible as soon as the wrong endpoint's FIFO gets data. In the comparison form a wrong index selects two endpoints, and what appears on the shared datapath is the AND or the OR of two drivers, depending on the technology.
6. What We Are Building
usb_ep_decode #(EP_N = 4, EPW = 2)
inputs outputs
------ -------
dev_address [6:0] for_us
token_valid ep_select [3:0] ONE-HOT or zero
token_addr [6:0] ep_exists
token_ep [1:0] unknown_ep
ep_enabled [3:0] silent
verdict IDLE / NOT_OURS /
NO_SUCH_EP / SELECTED
n_tokens / n_for_us / n_not_ours / n_unknown_ep / n_selected7. Verilog-2005 Implementation
// usb_ep_decode -- the first block a token reaches inside a device, and the
// two questions it has to answer before anything else can happen.
//
// EVERY DEVICE ON THE BUS SEES EVERY TOKEN
//
// USB is a broadcast medium. A hub repeats a downstream token to every
// enabled port, so a token addressed to one device is physically received by
// all of them. The only thing that stops six devices answering at once is
// that five of them look at the address field and say nothing.
//
// So this block is the device's answer to "is this mine?", and getting it
// wrong does not produce a slow device or a dropped transfer. It produces
// TWO DEVICES DRIVING THE SAME WIRE, which is a bus contention that corrupts
// the transaction for both of them and for everyone downstream.
//
// THE ADDRESS COMPARE IS AN EQUALITY, AND NOTHING ELSE
//
// A device responds to exactly one address: its own. There is no wildcard,
// no broadcast address, and no "also respond to 0 just in case".
//
// Address 0 feels like it should be special, and it is -- but not here. A
// device in the DEFAULT state HAS address 0 (chapter 21.5), so the plain
// equality already covers it. A device that has been assigned an address
// must NOT also answer address 0, because an unaddressed device on the same
// bus is using it, and the whole enumeration sequence depends on exactly one
// device holding address 0 at a time.
//
// THE ADDRESS IS SEVEN BITS
//
// Not eight, not six. A seven-bit compare done in six bits makes address 64
// indistinguishable from address 0 -- so a device at address 64 answers the
// enumeration traffic meant for whatever was just plugged in. That failure
// needs 65 devices on one bus before it can happen, which is why it survives
// bring-up and appears in the field.
//
// A NON-EXISTENT ENDPOINT GETS SILENCE
//
// The second question is "does the endpoint exist?", and the answer changes
// what the device does in a way that is easy to get backwards:
//
// endpoint exists, halted -> STALL (chapter 21.1)
// endpoint does not exist -> NOTHING AT ALL
//
// A STALL is a statement about an endpoint: "this one is halted". Sending it
// for an endpoint that does not exist tells the host the endpoint DOES exist
// and is merely stalled -- so the host clears the halt and tries again, for
// ever. Silence lets the host's timeout conclude what is actually true.
//
// AND THE SELECT MUST BE ONE-HOT BY CONSTRUCTION
//
// Each endpoint is a block with its own state and its own FIFO port. They
// share a datapath, so two selects asserted at once is a contention INSIDE
// the device. The decode produces a one-hot-or-zero vector and nothing else,
// and that is a property worth asserting rather than assuming.
module usb_ep_decode #(
parameter EP_N = 4, // endpoints implemented
parameter EPW = 2 // width of the endpoint index
) (
input wire clk,
input wire rst_n,
input wire [6:0] dev_address, // OUR address (0 while in DEFAULT)
input wire token_valid,
input wire [6:0] token_addr, // the address in the token packet
input wire [EPW-1:0] token_ep,
input wire [EP_N-1:0] ep_enabled, // which endpoints this device has
output wire for_us, // the address matched
output wire [EP_N-1:0] ep_select, // ONE-HOT, or all zero
output wire ep_exists,
output wire unknown_ep, // ours, but no such endpoint
output wire silent, // say nothing at all
output wire [1:0] verdict, // the same decision, named
output reg [31:0] n_tokens,
output reg [31:0] n_for_us,
output reg [31:0] n_not_ours,
output reg [31:0] n_unknown_ep,
output reg [31:0] n_selected
);
// ---- QUESTION ONE: is this token addressed to us? ----
//
// A plain seven-bit equality. Every other formulation is a bug:
// (token_addr == dev_address) || (token_addr == 0) -> collides with
// an unaddressed
// device
// (token_addr[5:0] == dev_address[5:0]) -> address 64
// aliases to 0
assign for_us = token_valid && (token_addr == dev_address);
// ---- QUESTION TWO: does the endpoint exist? ----
//
// ep_enabled is a mask, one bit per implemented endpoint. An endpoint that
// is not in it is not "disabled" -- it does not exist, and the device has
// nothing to say about it.
assign ep_exists = for_us && ep_enabled[token_ep];
// ---- THE SELECT. One-hot or zero, by construction. ----
//
// Built by shifting a single bit, so two bits set is not a case the logic
// can produce at all -- as opposed to a decoder built from comparisons,
// where a mistake in one comparison produces contention rather than a
// wrong answer.
assign ep_select = ep_exists ? ({{(EP_N-1){1'b0}}, 1'b1} << token_ep)
: {EP_N{1'b0}};
// Ours, but there is no such endpoint. The device says NOTHING -- a STALL
// would assert that the endpoint exists and is halted.
assign unknown_ep = for_us && !ep_enabled[token_ep];
assign silent = unknown_ep;
// The same decision as one named value. Three of the four outcomes are
// "the device drives nothing", and they are three different facts:
// another device's business, ours-and-unanswerable, and no token at all.
localparam [1:0] TOK_IDLE = 2'd0,
TOK_NOT_OURS = 2'd1,
TOK_NO_SUCH_EP = 2'd2,
TOK_SELECTED = 2'd3;
assign verdict = !token_valid ? TOK_IDLE
: !for_us ? TOK_NOT_OURS
: unknown_ep ? TOK_NO_SUCH_EP
: TOK_SELECTED;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_tokens <= 32'd0;
n_for_us <= 32'd0;
n_not_ours <= 32'd0;
n_unknown_ep <= 32'd0;
n_selected <= 32'd0;
end else if (token_valid) begin
n_tokens <= n_tokens + 32'd1;
if (for_us) n_for_us <= n_for_us + 32'd1;
else n_not_ours <= n_not_ours + 32'd1;
if (unknown_ep) n_unknown_ep <= n_unknown_ep + 32'd1;
if (|ep_select) n_selected <= n_selected + 32'd1;
end
end
endmodule8. SystemVerilog Implementation
// usb_ep_decode -- the first block a token reaches inside a device, and the
// two questions it has to answer before anything else can happen.
//
// EVERY DEVICE ON THE BUS SEES EVERY TOKEN
//
// USB is a broadcast medium. A hub repeats a downstream token to every
// enabled port, so a token addressed to one device is physically received by
// all of them. The only thing that stops six devices answering at once is
// that five of them look at the address field and say nothing.
//
// So this block is the device's answer to "is this mine?", and getting it
// wrong does not produce a slow device or a dropped transfer. It produces
// TWO DEVICES DRIVING THE SAME WIRE, which is a bus contention that corrupts
// the transaction for both of them and for everyone downstream.
//
// THE ADDRESS COMPARE IS AN EQUALITY, AND NOTHING ELSE
//
// A device responds to exactly one address: its own. There is no wildcard,
// no broadcast address, and no "also respond to 0 just in case".
//
// Address 0 feels like it should be special, and it is -- but not here. A
// device in the DEFAULT state HAS address 0 (chapter 21.5), so the plain
// equality already covers it. A device that has been assigned an address
// must NOT also answer address 0, because an unaddressed device on the same
// bus is using it, and the whole enumeration sequence depends on exactly one
// device holding address 0 at a time.
//
// THE ADDRESS IS SEVEN BITS
//
// Not eight, not six. A seven-bit compare done in six bits makes address 64
// indistinguishable from address 0 -- so a device at address 64 answers the
// enumeration traffic meant for whatever was just plugged in. That failure
// needs 65 devices on one bus before it can happen, which is why it survives
// bring-up and appears in the field.
//
// A NON-EXISTENT ENDPOINT GETS SILENCE
//
// The second question is "does the endpoint exist?", and the answer changes
// what the device does in a way that is easy to get backwards:
//
// endpoint exists, halted -> STALL (chapter 21.1)
// endpoint does not exist -> NOTHING AT ALL
//
// A STALL is a statement about an endpoint: "this one is halted". Sending it
// for an endpoint that does not exist tells the host the endpoint DOES exist
// and is merely stalled -- so the host clears the halt and tries again, for
// ever. Silence lets the host's timeout conclude what is actually true.
//
// AND THE SELECT MUST BE ONE-HOT BY CONSTRUCTION
//
// Each endpoint is a block with its own state and its own FIFO port. They
// share a datapath, so two selects asserted at once is a contention INSIDE
// the device. The decode produces a one-hot-or-zero vector and nothing else,
// and that is a property worth asserting rather than assuming.
package usb_decode_pkg;
// What the device decided about a token it saw. Every device on the bus
// sees every token, so THREE of these mean "say nothing" and they are not
// the same thing at all: NOT_OURS is another device's business, NO_SUCH_EP
// is ours and unanswerable, and IDLE is no token at all.
typedef enum logic [1:0] {
TOK_IDLE = 2'd0, // no token present
TOK_NOT_OURS = 2'd1, // addressed to another device
TOK_NO_SUCH_EP = 2'd2, // ours, but no such endpoint: SILENCE
TOK_SELECTED = 2'd3 // ours, and the endpoint exists
} token_verdict_e;
endpackage
module usb_ep_decode
import usb_decode_pkg::*;
#(
parameter int EP_N = 4, // endpoints implemented
parameter int EPW = 2 // width of the endpoint index
) (
input logic clk,
input logic rst_n,
input logic [6:0] dev_address, // OUR address (0 while in DEFAULT)
input logic token_valid,
input logic [6:0] token_addr, // the address in the token packet
input logic [EPW-1:0] token_ep,
input logic [EP_N-1:0] ep_enabled, // which endpoints this device has
output logic for_us, // the address matched
output logic [EP_N-1:0] ep_select, // ONE-HOT, or all zero
output logic ep_exists,
output logic unknown_ep, // ours, but no such endpoint
output logic silent, // say nothing at all
output token_verdict_e verdict, // the same decision, named
output logic [31:0] n_tokens,
output logic [31:0] n_for_us,
output logic [31:0] n_not_ours,
output logic [31:0] n_unknown_ep,
output logic [31:0] n_selected
);
// ---- QUESTION ONE: is this token addressed to us? ----
//
// A plain seven-bit equality. Every other formulation is a bug:
// (token_addr == dev_address) || (token_addr == 0) -> collides with
// an unaddressed
// device
// (token_addr[5:0] == dev_address[5:0]) -> address 64
// aliases to 0
assign for_us = token_valid && (token_addr == dev_address);
// ---- QUESTION TWO: does the endpoint exist? ----
//
// ep_enabled is a mask, one bit per implemented endpoint. An endpoint that
// is not in it is not "disabled" -- it does not exist, and the device has
// nothing to say about it.
assign ep_exists = for_us && ep_enabled[token_ep];
// ---- THE SELECT. One-hot or zero, by construction. ----
//
// Built by shifting a single bit, so two bits set is not a case the logic
// can produce at all -- as opposed to a decoder built from comparisons,
// where a mistake in one comparison produces contention rather than a
// wrong answer.
assign ep_select = ep_exists ? (EP_N'(1) << token_ep) : '0;
// Ours, but there is no such endpoint. The device says NOTHING -- a STALL
// would assert that the endpoint exists and is halted.
assign unknown_ep = for_us && !ep_enabled[token_ep];
assign silent = unknown_ep;
// The same decision as one named value. Three of the four outcomes are
// "the device drives nothing", and they are three different facts.
always_comb begin
if (!token_valid) verdict = TOK_IDLE;
else if (!for_us) verdict = TOK_NOT_OURS;
else if (unknown_ep) verdict = TOK_NO_SUCH_EP;
else verdict = TOK_SELECTED;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_tokens <= '0;
n_for_us <= '0;
n_not_ours <= '0;
n_unknown_ep <= '0;
n_selected <= '0;
end else if (token_valid) begin
n_tokens <= n_tokens + 1;
if (for_us) n_for_us <= n_for_us + 1;
else n_not_ours <= n_not_ours + 1;
if (unknown_ep) n_unknown_ep <= n_unknown_ep + 1;
if (|ep_select) n_selected <= n_selected + 1;
end
end
endmodule9. VHDL-2008 Implementation
-- usb_ep_decode -- the first block a token reaches inside a device, and the
-- two questions it has to answer before anything else can happen.
--
-- EVERY DEVICE ON THE BUS SEES EVERY TOKEN
--
-- USB is a broadcast medium. A hub repeats a downstream token to every
-- enabled port, so a token addressed to one device is physically received by
-- all of them. The only thing that stops six devices answering at once is
-- that five of them look at the address field and say nothing.
--
-- So this block is the device's answer to "is this mine?", and getting it
-- wrong does not produce a slow device or a dropped transfer. It produces
-- TWO DEVICES DRIVING THE SAME WIRE, which corrupts the transaction for both
-- of them and for everyone downstream.
--
-- THE ADDRESS COMPARE IS AN EQUALITY, AND NOTHING ELSE
--
-- A device responds to exactly one address: its own. There is no wildcard,
-- no broadcast address, and no "also respond to 0 just in case".
--
-- Address 0 feels like it should be special, and it is -- but not here. A
-- device in the DEFAULT state HAS address 0 (chapter 21.5), so the plain
-- equality already covers it. A device that has been assigned an address must
-- NOT also answer address 0, because an unaddressed device on the same bus is
-- using it, and the whole enumeration sequence depends on exactly one device
-- holding address 0 at a time.
--
-- THE ADDRESS IS SEVEN BITS
--
-- Not eight, not six. A seven-bit compare done in six bits makes address 64
-- indistinguishable from address 0 -- so a device at address 64 answers the
-- enumeration traffic meant for whatever was just plugged in. That failure
-- needs 65 devices on one bus before it can happen, which is why it survives
-- bring-up and appears in the field.
--
-- A NON-EXISTENT ENDPOINT GETS SILENCE
--
-- endpoint exists, halted -> STALL (chapter 21.1)
-- endpoint does not exist -> NOTHING AT ALL
--
-- A STALL is a statement about an endpoint: "this one is halted". Sending it
-- for an endpoint that does not exist tells the host the endpoint DOES exist
-- and is merely stalled -- so the host clears the halt and tries again, for
-- ever. Silence lets the host's timeout conclude what is actually true.
--
-- AND THE SELECT MUST BE ONE-HOT BY CONSTRUCTION
--
-- Each endpoint is a block with its own state and its own FIFO port. They
-- share a datapath, so two selects asserted at once is a contention INSIDE
-- the device. The decode produces a one-hot-or-zero vector and nothing else.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package usb_decode_pkg is
-- What the device decided about a token it saw. THREE of these mean "say
-- nothing" and they are not the same thing at all: TOK_NOT_OURS is another
-- device's business, TOK_NO_SUCH_EP is ours and unanswerable, and TOK_IDLE
-- is no token at all.
type token_verdict_t is (
TOK_IDLE, -- no token present
TOK_NOT_OURS, -- addressed to another device
TOK_NO_SUCH_EP, -- ours, but no such endpoint: SILENCE
TOK_SELECTED -- ours, and the endpoint exists
);
function verdict_code(v : token_verdict_t) return std_logic_vector;
end package;
package body usb_decode_pkg is
function verdict_code(v : token_verdict_t) return std_logic_vector is
begin
return std_logic_vector(to_unsigned(token_verdict_t'pos(v), 2));
end function;
end package body;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.usb_decode_pkg.all;
entity usb_ep_decode is
generic (
EP_N : natural := 4; -- endpoints implemented
EPW : natural := 2 -- width of the endpoint index
);
port (
clk : in std_logic;
rst_n : in std_logic;
dev_address : in std_logic_vector(6 downto 0); -- OUR address
token_valid : in std_logic;
token_addr : in std_logic_vector(6 downto 0);
token_ep : in std_logic_vector(EPW-1 downto 0);
ep_enabled : in std_logic_vector(EP_N-1 downto 0);
for_us : out std_logic;
ep_select : out std_logic_vector(EP_N-1 downto 0); -- ONE-HOT or zero
ep_exists : out std_logic;
unknown_ep : out std_logic;
silent : out std_logic;
verdict : out std_logic_vector(1 downto 0);
n_tokens : out std_logic_vector(31 downto 0);
n_for_us : out std_logic_vector(31 downto 0);
n_not_ours : out std_logic_vector(31 downto 0);
n_unknown_ep : out std_logic_vector(31 downto 0);
n_selected : out std_logic_vector(31 downto 0)
);
end entity;
architecture rtl of usb_ep_decode is
signal us_s, exists_s, unk_s : std_logic;
signal sel_s : std_logic_vector(EP_N-1 downto 0);
signal vd_s : token_verdict_t;
signal tok_c, us_c, no_c, unk_c, sel_c : unsigned(31 downto 0)
:= (others => '0');
begin
-- ---- QUESTION ONE: is this token addressed to us? ----
--
-- A plain seven-bit equality. Every other formulation is a bug:
-- token_addr = dev_address or token_addr = 0 -> collides with an
-- unaddressed device
-- token_addr(5 downto 0) = dev_address(5 downto 0)
-- -> address 64 aliases to 0
us_s <= '1' when (token_valid = '1' and token_addr = dev_address) else '0';
-- ---- QUESTION TWO: does the endpoint exist? ----
--
-- ep_enabled is a mask, one bit per implemented endpoint. An endpoint not
-- in it is not "disabled" -- it does not exist, and the device has nothing
-- to say about it.
exists_s <= '1' when (us_s = '1'
and ep_enabled(to_integer(unsigned(token_ep))) = '1')
else '0';
-- ---- THE SELECT. One-hot or zero, by construction. ----
--
-- Built by shifting a single bit, so two bits set is not a case the logic
-- can produce at all -- as opposed to a decoder built from comparisons,
-- where a mistake in one comparison produces contention rather than a
-- wrong answer.
onehot : process (exists_s, token_ep)
variable v : std_logic_vector(EP_N-1 downto 0);
begin
v := (others => '0');
if exists_s = '1' then
v(to_integer(unsigned(token_ep))) := '1';
end if;
sel_s <= v;
end process;
-- Ours, but there is no such endpoint. The device says NOTHING -- a STALL
-- would assert that the endpoint exists and is halted.
unk_s <= '1' when (us_s = '1'
and ep_enabled(to_integer(unsigned(token_ep))) = '0')
else '0';
for_us <= us_s;
ep_exists <= exists_s;
ep_select <= sel_s;
unknown_ep <= unk_s;
silent <= unk_s;
-- The same decision as one named value. Three of the four outcomes are
-- "the device drives nothing", and they are three different facts.
classify : process (token_valid, us_s, unk_s)
begin
if token_valid = '0' then
vd_s <= TOK_IDLE;
elsif us_s = '0' then
vd_s <= TOK_NOT_OURS;
elsif unk_s = '1' then
vd_s <= TOK_NO_SUCH_EP;
else
vd_s <= TOK_SELECTED;
end if;
end process;
verdict <= verdict_code(vd_s);
regs : process (clk, rst_n)
begin
if rst_n = '0' then
tok_c <= (others => '0');
us_c <= (others => '0');
no_c <= (others => '0');
unk_c <= (others => '0');
sel_c <= (others => '0');
elsif rising_edge(clk) then
if token_valid = '1' then
tok_c <= tok_c + 1;
if us_s = '1' then
us_c <= us_c + 1;
else
no_c <= no_c + 1;
end if;
if unk_s = '1' then
unk_c <= unk_c + 1;
end if;
if sel_s /= (sel_s'range => '0') then
sel_c <= sel_c + 1;
end if;
end if;
end if;
end process;
n_tokens <= std_logic_vector(tok_c);
n_for_us <= std_logic_vector(us_c);
n_not_ours <= std_logic_vector(no_c);
n_unknown_ep <= std_logic_vector(unk_c);
n_selected <= std_logic_vector(sel_c);
end architecture;9.1 Seeing the four verdicts
Four tokens on the wire, and four different silences
usb_ep_decode — one bus, four devices, one answer
10 cyclesep_select is 0000 on seven of the eight token cycles, and the row below it is the only thing that says why. Cycles 3, 5 and 7 are three different silences — one of them ours to produce, two of them not.
10. The Testbenches
Two exhaustive domains, because the address compare and the endpoint decode are independent questions and one of them has a very large domain:
| Domain | What it enumerates | Size |
|---|---|---|
| A — address compare | every device address × every token address | 16384 |
| B — endpoint decode | 16 enable masks × 4 endpoints × address match/mismatch × token valid/not | 256 |
10.1 Verilog testbench
`timescale 1ns/1ps
module tb_dc_v;
localparam EP_N = 4, EPW = 2;
reg clk=0, rst_n=0;
reg token_valid=0;
reg [6:0] dev_address=0, token_addr=0;
reg [EPW-1:0] token_ep=0;
reg [EP_N-1:0] ep_enabled=0;
wire for_us, ep_exists, unknown_ep, silent;
wire [EP_N-1:0] ep_select;
wire [1:0] verdict;
wire [31:0] n_tokens, n_for_us, n_not_ours, n_unknown_ep, n_selected;
always #5 clk=~clk;
usb_ep_decode #(.EP_N(EP_N), .EPW(EPW)) dut (
.clk(clk), .rst_n(rst_n), .dev_address(dev_address),
.token_valid(token_valid), .token_addr(token_addr), .token_ep(token_ep),
.ep_enabled(ep_enabled), .for_us(for_us), .ep_select(ep_select),
.ep_exists(ep_exists), .unknown_ep(unknown_ep), .silent(silent),
.verdict(verdict),
.n_tokens(n_tokens), .n_for_us(n_for_us), .n_not_ours(n_not_ours),
.n_unknown_ep(n_unknown_ep), .n_selected(n_selected));
integer errors=0, i, j, k, m, v;
integer n_addr_exh=0, n_ep_exh=0;
integer n_match=0, n_nomatch=0, n_sel=0, n_unk=0;
integer n_perep [0:3];
integer m_tok, m_us, m_no, m_unk, m_sel;
// count the set bits of a vector -- used for the one-hot property
function integer popcount;
input [EP_N-1:0] v;
integer b, c;
begin
c = 0;
for (b=0; b<EP_N; b=b+1) if (v[b]) c = c + 1;
popcount = c;
end
endfunction
task check(input cond, input [639:0] msg);
begin if (!cond) begin errors=errors+1;
if (errors <= 25)
$display(" FAIL: %0s (vld=%b dev=%0d tok=%0d ep=%0d en=%b | for_us=%b exists=%b sel=%b unk=%b silent=%b, t=%0t)",
msg, token_valid, dev_address, token_addr, token_ep,
ep_enabled, for_us, ep_exists, ep_select, unknown_ep,
silent, $time);
end end
endtask
localparam [1:0] TOK_IDLE=0, TOK_NOT_OURS=1, TOK_NO_SUCH_EP=2,
TOK_SELECTED=3;
task check_comb;
reg e_for_us, e_exists, e_unk;
reg [EP_N-1:0] e_sel;
reg [1:0] e_verd;
integer b;
begin
// The model builds the one-hot select by a LOOP over the endpoints
// where the design shifts a single bit -- a different route, and one
// that would happily produce two bits set if the rule allowed it.
e_for_us = token_valid && (token_addr == dev_address);
e_exists = e_for_us && ep_enabled[token_ep];
e_unk = e_for_us && !ep_enabled[token_ep];
e_sel = {EP_N{1'b0}};
for (b=0; b<EP_N; b=b+1)
if (e_exists && (token_ep == b[EPW-1:0])) e_sel[b] = 1'b1;
check(for_us === e_for_us, "for_us matches the model");
check(ep_exists === e_exists, "ep_exists matches the model");
check(ep_select === e_sel, "ep_select matches the model");
check(unknown_ep === e_unk, "unknown_ep matches the model");
check(silent === e_unk, "silent matches unknown_ep");
if (!token_valid) e_verd = TOK_IDLE;
else if (!e_for_us) e_verd = TOK_NOT_OURS;
else if (e_unk) e_verd = TOK_NO_SUCH_EP;
else e_verd = TOK_SELECTED;
check(verdict === e_verd, "verdict matches the model");
// The named verdict must agree with the signals it summarises.
check((verdict === TOK_SELECTED) === (|ep_select),
"verdict disagrees with ep_select");
check((verdict === TOK_NO_SUCH_EP) === silent,
"verdict disagrees with silent");
// ---- SAFETY PROPERTIES, independent of the model ----
// 1. THE property. The select is ONE-HOT or zero. Two endpoint blocks
// driving the shared datapath is a contention inside the device.
check(popcount(ep_select) <= 1,
"more than one endpoint was selected -- that is a bus contention inside the device");
// 2. A token for another device selects nothing and says nothing.
if (token_valid && (token_addr !== dev_address)) begin
check(!for_us, "a token for another address was claimed");
check(ep_select === {EP_N{1'b0}},
"a token for another device selected an endpoint");
check(!silent,
"a token for another device produced a silence WE are responsible for");
end
// 3. Address 0 is not a wildcard. An ADDRESSED device must not answer
// it -- an unaddressed device on the same bus is using it.
if (token_valid && (token_addr === 7'd0) && (dev_address !== 7'd0))
check(!for_us,
"an addressed device answered address 0 -- it will collide with the device being enumerated");
// 4. The address is SEVEN bits. Address 64 must not alias to 0.
if (token_valid && (token_addr === 7'd64) && (dev_address === 7'd0))
check(!for_us,
"address 64 matched address 0 -- the compare is only six bits wide");
if (token_valid && (token_addr === 7'd0) && (dev_address === 7'd64))
check(!for_us, "address 0 matched address 64");
// 5. A non-existent endpoint gets SILENCE, and selects nothing.
if (unknown_ep) begin
check(ep_select === {EP_N{1'b0}},
"a non-existent endpoint was selected");
check(silent,
"a non-existent endpoint did not produce silence -- a STALL would claim it exists");
end
// 6. Selecting and not-existing are mutually exclusive.
check(!(|ep_select && unknown_ep),
"an endpoint was both selected and unknown");
// 7. Nothing at all happens without a token.
if (!token_valid)
check(!for_us && !silent && (ep_select === {EP_N{1'b0}}),
"the decode responded with no token present");
// 8. A selected endpoint is the one the token named.
if (|ep_select)
check(ep_select[token_ep],
"the selected endpoint is not the one the token addressed");
if (e_for_us) n_match = n_match + 1; else if (token_valid) n_nomatch = n_nomatch + 1;
if (|e_sel) begin n_sel = n_sel + 1; n_perep[token_ep] = n_perep[token_ep] + 1; end
if (e_unk) n_unk = n_unk + 1;
end
endtask
task step;
begin
#1;
check_comb;
if (token_valid) begin
m_tok = m_tok + 1;
if (for_us) m_us = m_us + 1; else m_no = m_no + 1;
if (unknown_ep) m_unk = m_unk + 1;
if (|ep_select) m_sel = m_sel + 1;
end
@(posedge clk); #1;
check(n_tokens === m_tok[31:0], "n_tokens matches the model");
check(n_for_us === m_us[31:0], "n_for_us matches the model");
check(n_not_ours === m_no[31:0], "n_not_ours matches the model");
check(n_unknown_ep === m_unk[31:0], "n_unknown_ep matches the model");
check(n_selected === m_sel[31:0], "n_selected matches the model");
end
endtask
task hard_reset;
begin
rst_n=0; token_valid=0; dev_address=0; token_addr=0; token_ep=0;
ep_enabled=0;
@(posedge clk); #1; @(posedge clk); #1; rst_n=1; #1;
m_tok=0; m_us=0; m_no=0; m_unk=0; m_sel=0;
end
endtask
initial begin
for (i=0;i<4;i=i+1) n_perep[i]=0;
hard_reset;
check(!for_us, "no token, no match");
check(ep_select === 4'b0000, "and nothing selected");
// ===== A. EXHAUSTIVE address-compare sweep =====
// Every one of the 128 device addresses against every one of the 128
// token addresses = 16384 points. That is the ENTIRE address domain,
// which is what it takes to prove the compare is seven bits wide and
// has no wildcard anywhere in it.
token_valid=1; ep_enabled=4'b1111; token_ep=2'd0;
for (i=0; i<128; i=i+1)
for (j=0; j<128; j=j+1) begin
dev_address = i[6:0]; token_addr = j[6:0];
step;
n_addr_exh = n_addr_exh + 1;
end
token_valid=0;
$display(" exhaustive address-compare sweep: %0d of 16384 pairs verified",
n_addr_exh);
// ===== B. EXHAUSTIVE endpoint-decode sweep =====
// Every endpoint-enable mask against every endpoint index, with the
// address matching and not matching, and with and without a token:
// 16 masks x 4 endpoints x 2 (address match) x 2 (token_valid) = 256
for (m=0; m<16; m=m+1)
for (k=0; k<4; k=k+1)
for (i=0; i<2; i=i+1)
for (v=0; v<2; v=v+1) begin
dev_address = 7'd37;
token_addr = i[0] ? 7'd37 : 7'd38;
token_ep = k[EPW-1:0];
ep_enabled = m[EP_N-1:0];
token_valid = v[0];
step;
n_ep_exh = n_ep_exh + 1;
end
token_valid=0;
$display(" exhaustive endpoint-decode sweep: %0d of 256 points verified",
n_ep_exh);
// ===== C. directed: the cases that cause bus contention =====
hard_reset;
dev_address = 7'd9; ep_enabled = 4'b1011;
// 1. Our address, an endpoint we have.
token_valid=1; token_addr=7'd9; token_ep=2'd1; #1;
check(for_us, "a token for our address is ours");
check(ep_exists, "endpoint 1 exists");
check(ep_select === 4'b0010, "and exactly endpoint 1 is selected");
step;
// 2. Our address, an endpoint we do NOT have.
token_valid=1; token_addr=7'd9; token_ep=2'd2; #1;
check(for_us, "still our address");
check(!ep_exists, "but endpoint 2 does not exist on this device");
check(ep_select === 4'b0000, "so nothing is selected");
check(silent,
"and the device says NOTHING -- a STALL would claim the endpoint exists");
step;
// 3. Somebody else's address.
token_valid=1; token_addr=7'd10; token_ep=2'd1; #1;
check(!for_us, "a token for address 10 is not ours");
check(ep_select === 4'b0000, "nothing selected");
check(!silent, "and the silence is not ours to produce");
step;
// 4. THE enumeration hazard. We are addressed; a device being enumerated
// is at address 0. We must not answer its tokens.
token_valid=1; token_addr=7'd0; token_ep=2'd0; #1;
check(!for_us,
"an ADDRESSED device must not answer address 0 -- the device being enumerated is using it");
check(ep_select === 4'b0000, "so nothing is selected");
step;
// 5. ...and while WE are unaddressed, address 0 IS ours.
dev_address = 7'd0;
token_valid=1; token_addr=7'd0; token_ep=2'd0; #1;
check(for_us,
"a device in the DEFAULT state HAS address 0, so the plain equality covers it");
check(ep_select === 4'b0001, "and endpoint 0 is selected");
step;
// 6. THE seven-bit hazard. Address 64 differs from address 0 in bit 6
// alone. A six-bit compare makes them the same device.
dev_address = 7'd0;
token_valid=1; token_addr=7'd64; token_ep=2'd0; #1;
check(!for_us,
"address 64 is NOT address 0 -- the compare is seven bits wide");
step;
dev_address = 7'd64;
token_valid=1; token_addr=7'd0; token_ep=2'd0; #1;
check(!for_us, "and address 0 is not address 64");
step;
// 7. The highest address, exactly.
dev_address = 7'd127;
token_valid=1; token_addr=7'd127; token_ep=2'd0; #1;
check(for_us, "address 127 matches itself");
step;
token_valid=1; token_addr=7'd126; token_ep=2'd0; #1;
check(!for_us, "and does not match 126");
step;
token_valid=0;
// ===== D. randomised =====
hard_reset;
for (i=0;i<40000;i=i+1) begin
dev_address = {$random}%128;
// bias hard toward a matching address -- a random 7-bit pair matches
// once in 128, and the interesting half of this block is downstream
// of a match
token_addr = (({$random}%3)!=0) ? dev_address : ({$random}%128);
token_ep = {$random}%4;
ep_enabled = {$random}%16;
token_valid = ({$random}%8)!=0;
step;
end
for (i=0;i<4;i=i+1)
check(n_perep[i] > 500, "every endpoint was selected many times");
check(n_match > 5000, "address matches happened often");
check(n_nomatch > 5000, "address mismatches happened often");
check(n_unk > 2000, "unknown endpoints were addressed often");
$display("");
$display(" REACH: address-pairs=%0d ep-points=%0d | matched=%0d not-ours=%0d selected=%0d unknown-ep=%0d",
n_addr_exh, n_ep_exh, n_match, n_nomatch, n_sel, n_unk);
$display(" PER-ENDPOINT selects: ep0=%0d ep1=%0d ep2=%0d ep3=%0d",
n_perep[0], n_perep[1], n_perep[2], n_perep[3]);
$display(" COUNTERS: tokens=%0d for-us=%0d not-ours=%0d unknown-ep=%0d selected=%0d",
n_tokens, n_for_us, n_not_ours, n_unknown_ep, n_selected);
$display(" [Verilog] usb_ep_decode: %0d errors", errors);
$display(" [Verilog] %0s", errors==0 ? "PASS" : "FAIL");
$display("");
$finish;
end
endmodule10.2 SystemVerilog testbench
`timescale 1ns/1ps
module tb_dc_sv;
import usb_decode_pkg::*;
localparam EP_N = 4, EPW = 2;
logic clk=0, rst_n=0;
logic token_valid=0;
logic [6:0] dev_address=0, token_addr=0;
logic [EPW-1:0] token_ep=0;
logic [EP_N-1:0] ep_enabled=0;
logic for_us, ep_exists, unknown_ep, silent;
logic [EP_N-1:0] ep_select;
token_verdict_e verdict;
logic [31:0] n_tokens, n_for_us, n_not_ours, n_unknown_ep, n_selected;
// Icarus seeds $random and $urandom identically, so an unseeded run would
// replay the Verilog suite's stimulus exactly. See chapter 20.5 section 9.2.
int urandom_seed = 23101;
always #5 clk=~clk;
usb_ep_decode #(.EP_N(EP_N), .EPW(EPW)) dut (
.clk, .rst_n, .dev_address, .token_valid, .token_addr, .token_ep,
.ep_enabled, .for_us, .ep_select, .ep_exists, .unknown_ep, .silent,
.verdict, .n_tokens, .n_for_us, .n_not_ours, .n_unknown_ep,
.n_selected);
int errors=0, i, j, k, m, v;
int n_addr_exh=0, n_ep_exh=0;
int n_match=0, n_nomatch=0, n_sel=0, n_unk=0;
int n_perep [4];
int m_tok, m_us, m_no, m_unk, m_sel;
// count the set bits of a vector -- used for the one-hot property
function automatic int popcount(input logic [EP_N-1:0] vec);
int c = 0;
for (int b = 0; b < EP_N; b++) if (vec[b]) c++;
return c;
endfunction
task automatic check(input bit cond, input string msg);
// Icarus will not call .name() on a net, so the enum output is copied
// into a variable of the same type before being printed.
token_verdict_e vd_v;
if (!cond) begin
errors++;
vd_v = verdict;
if (errors <= 25)
$display(" FAIL: %0s (vld=%b dev=%0d tok=%0d ep=%0d en=%b | for_us=%b exists=%b sel=%b verdict=%s, t=%0t)",
msg, token_valid, dev_address, token_addr, token_ep,
ep_enabled, for_us, ep_exists, ep_select, vd_v.name(),
$time);
end
endtask
task automatic check_comb;
bit e_for_us, e_exists, e_unk;
logic [EP_N-1:0] e_sel;
token_verdict_e e_verd;
int b;
begin
// The model builds the one-hot select by a LOOP over the endpoints
// where the design shifts a single bit -- a different route, and one
// that would happily produce two bits set if the rule allowed it.
e_for_us = token_valid && (token_addr == dev_address);
e_exists = e_for_us && ep_enabled[token_ep];
e_unk = e_for_us && !ep_enabled[token_ep];
e_sel = '0;
for (b=0; b<EP_N; b++)
if (e_exists && (token_ep == EPW'(b))) e_sel[b] = 1'b1;
check(for_us === e_for_us, "for_us matches the model");
check(ep_exists === e_exists, "ep_exists matches the model");
check(ep_select === e_sel, "ep_select matches the model");
check(unknown_ep === e_unk, "unknown_ep matches the model");
check(silent === e_unk, "silent matches unknown_ep");
if (!token_valid) e_verd = TOK_IDLE;
else if (!e_for_us) e_verd = TOK_NOT_OURS;
else if (e_unk) e_verd = TOK_NO_SUCH_EP;
else e_verd = TOK_SELECTED;
check(verdict === e_verd, "verdict matches the model");
// The named verdict must agree with the signals it summarises.
check((verdict === TOK_SELECTED) === (|ep_select),
"verdict disagrees with ep_select");
check((verdict === TOK_NO_SUCH_EP) === silent,
"verdict disagrees with silent");
// ---- SAFETY PROPERTIES, independent of the model ----
// 1. THE property. The select is ONE-HOT or zero. Two endpoint blocks
// driving the shared datapath is a contention inside the device.
check(popcount(ep_select) <= 1,
"more than one endpoint was selected -- that is a bus contention inside the device");
// 2. A token for another device selects nothing and says nothing.
if (token_valid && (token_addr !== dev_address)) begin
check(!for_us, "a token for another address was claimed");
check(ep_select === '0,
"a token for another device selected an endpoint");
check(!silent,
"a token for another device produced a silence WE are responsible for");
end
// 3. Address 0 is not a wildcard. An ADDRESSED device must not answer
// it -- an unaddressed device on the same bus is using it.
if (token_valid && (token_addr === 7'd0) && (dev_address !== 7'd0))
check(!for_us,
"an addressed device answered address 0 -- it will collide with the device being enumerated");
// 4. The address is SEVEN bits. Address 64 must not alias to 0.
if (token_valid && (token_addr === 7'd64) && (dev_address === 7'd0))
check(!for_us,
"address 64 matched address 0 -- the compare is only six bits wide");
if (token_valid && (token_addr === 7'd0) && (dev_address === 7'd64))
check(!for_us, "address 0 matched address 64");
// 5. A non-existent endpoint gets SILENCE, and selects nothing.
if (unknown_ep) begin
check(ep_select === '0,
"a non-existent endpoint was selected");
check(silent,
"a non-existent endpoint did not produce silence -- a STALL would claim it exists");
end
// 6. Selecting and not-existing are mutually exclusive.
check(!(|ep_select && unknown_ep),
"an endpoint was both selected and unknown");
// 7. Nothing at all happens without a token.
if (!token_valid)
check(!for_us && !silent && (ep_select === '0),
"the decode responded with no token present");
// 8. A selected endpoint is the one the token named.
if (|ep_select)
check(ep_select[token_ep],
"the selected endpoint is not the one the token addressed");
if (e_for_us) n_match = n_match + 1; else if (token_valid) n_nomatch = n_nomatch + 1;
if (|e_sel) begin n_sel++; n_perep[int'(token_ep)]++; end
if (e_unk) n_unk = n_unk + 1;
end
endtask
task automatic step;
begin
#1;
check_comb;
if (token_valid) begin
m_tok = m_tok + 1;
if (for_us) m_us = m_us + 1; else m_no = m_no + 1;
if (unknown_ep) m_unk = m_unk + 1;
if (|ep_select) m_sel = m_sel + 1;
end
@(posedge clk); #1;
check(n_tokens === 32'(m_tok), "n_tokens matches the model");
check(n_for_us === 32'(m_us), "n_for_us matches the model");
check(n_not_ours === 32'(m_no), "n_not_ours matches the model");
check(n_unknown_ep === 32'(m_unk), "n_unknown_ep matches the model");
check(n_selected === 32'(m_sel), "n_selected matches the model");
end
endtask
task automatic hard_reset;
begin
rst_n=0; token_valid=0; dev_address=0; token_addr=0; token_ep=0;
ep_enabled=0;
@(posedge clk); #1; @(posedge clk); #1; rst_n=1; #1;
m_tok=0; m_us=0; m_no=0; m_unk=0; m_sel=0;
end
endtask
initial begin
void'($urandom(urandom_seed));
foreach (n_perep[i]) n_perep[i]=0;
hard_reset;
check(!for_us, "no token, no match");
check(ep_select === '0, "and nothing selected");
// ===== A. EXHAUSTIVE address-compare sweep =====
// Every one of the 128 device addresses against every one of the 128
// token addresses = 16384 points. That is the ENTIRE address domain,
// which is what it takes to prove the compare is seven bits wide and
// has no wildcard anywhere in it.
token_valid=1; ep_enabled=4'b1111; token_ep=2'd0;
for (i=0; i<128; i=i+1)
for (j=0; j<128; j=j+1) begin
dev_address = 7'(i); token_addr = 7'(j);
step;
n_addr_exh = n_addr_exh + 1;
end
token_valid=0;
$display(" exhaustive address-compare sweep: %0d of 16384 pairs verified",
n_addr_exh);
// ===== B. EXHAUSTIVE endpoint-decode sweep =====
// Every endpoint-enable mask against every endpoint index, with the
// address matching and not matching, and with and without a token:
// 16 masks x 4 endpoints x 2 (address match) x 2 (token_valid) = 256
for (m=0; m<16; m=m+1)
for (k=0; k<4; k=k+1)
for (i=0; i<2; i=i+1)
for (v=0; v<2; v=v+1) begin
dev_address = 7'd37;
token_addr = i[0] ? 7'd37 : 7'd38;
token_ep = EPW'(k);
ep_enabled = EP_N'(m);
token_valid = v[0];
step;
n_ep_exh = n_ep_exh + 1;
end
token_valid=0;
$display(" exhaustive endpoint-decode sweep: %0d of 256 points verified",
n_ep_exh);
// ===== C. directed: the cases that cause bus contention =====
hard_reset;
dev_address = 7'd9; ep_enabled = 4'b1011;
// 1. Our address, an endpoint we have.
token_valid=1; token_addr=7'd9; token_ep=2'd1; #1;
check(for_us, "a token for our address is ours");
check(ep_exists, "endpoint 1 exists");
check(ep_select === 4'b0010, "and exactly endpoint 1 is selected");
step;
// 2. Our address, an endpoint we do NOT have.
token_valid=1; token_addr=7'd9; token_ep=2'd2; #1;
check(for_us, "still our address");
check(!ep_exists, "but endpoint 2 does not exist on this device");
check(ep_select === '0, "so nothing is selected");
check(silent,
"and the device says NOTHING -- a STALL would claim the endpoint exists");
step;
// 3. Somebody else's address.
token_valid=1; token_addr=7'd10; token_ep=2'd1; #1;
check(!for_us, "a token for address 10 is not ours");
check(ep_select === '0, "nothing selected");
check(!silent, "and the silence is not ours to produce");
step;
// 4. THE enumeration hazard. We are addressed; a device being enumerated
// is at address 0. We must not answer its tokens.
token_valid=1; token_addr=7'd0; token_ep=2'd0; #1;
check(!for_us,
"an ADDRESSED device must not answer address 0 -- the device being enumerated is using it");
check(ep_select === '0, "so nothing is selected");
step;
// 5. ...and while WE are unaddressed, address 0 IS ours.
dev_address = 7'd0;
token_valid=1; token_addr=7'd0; token_ep=2'd0; #1;
check(for_us,
"a device in the DEFAULT state HAS address 0, so the plain equality covers it");
check(ep_select === 4'b0001, "and endpoint 0 is selected");
step;
// 6. THE seven-bit hazard. Address 64 differs from address 0 in bit 6
// alone. A six-bit compare makes them the same device.
dev_address = 7'd0;
token_valid=1; token_addr=7'd64; token_ep=2'd0; #1;
check(!for_us,
"address 64 is NOT address 0 -- the compare is seven bits wide");
step;
dev_address = 7'd64;
token_valid=1; token_addr=7'd0; token_ep=2'd0; #1;
check(!for_us, "and address 0 is not address 64");
step;
// 7. The highest address, exactly.
dev_address = 7'd127;
token_valid=1; token_addr=7'd127; token_ep=2'd0; #1;
check(for_us, "address 127 matches itself");
step;
token_valid=1; token_addr=7'd126; token_ep=2'd0; #1;
check(!for_us, "and does not match 126");
step;
token_valid=0;
// ===== D. randomised =====
hard_reset;
for (i=0;i<40000;i=i+1) begin
dev_address = 7'($urandom%128);
// bias hard toward a matching address -- a random 7-bit pair matches
// once in 128, and the interesting half of this block is downstream
// of a match
token_addr = (($urandom%3)!=0) ? dev_address : 7'($urandom%128);
token_ep = EPW'($urandom%4);
ep_enabled = EP_N'($urandom%16);
token_valid = ($urandom%8)!=0;
step;
end
foreach (n_perep[i])
check(n_perep[i] > 500, "every endpoint was selected many times");
check(n_match > 5000, "address matches happened often");
check(n_nomatch > 5000, "address mismatches happened often");
check(n_unk > 2000, "unknown endpoints were addressed often");
$display("");
$display(" REACH: address-pairs=%0d ep-points=%0d | matched=%0d not-ours=%0d selected=%0d unknown-ep=%0d",
n_addr_exh, n_ep_exh, n_match, n_nomatch, n_sel, n_unk);
$display(" PER-ENDPOINT selects: ep0=%0d ep1=%0d ep2=%0d ep3=%0d",
n_perep[0], n_perep[1], n_perep[2], n_perep[3]);
$display(" COUNTERS: tokens=%0d for-us=%0d not-ours=%0d unknown-ep=%0d selected=%0d",
n_tokens, n_for_us, n_not_ours, n_unknown_ep, n_selected);
$display(" [SystemVerilog] usb_ep_decode: %0d errors", errors);
$display(" [SystemVerilog] %0s", errors==0 ? "PASS" : "FAIL");
$display("");
$finish;
end
endmodule10.3 VHDL testbench
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all;
use work.usb_decode_pkg.all;
entity tb_dc_vhdl is
end entity;
architecture sim of tb_dc_vhdl is
constant EP_N : natural := 4;
constant EPW : natural := 2;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal token_valid : std_logic := '0';
signal dev_address, token_addr : std_logic_vector(6 downto 0)
:= (others => '0');
signal token_ep : std_logic_vector(EPW-1 downto 0) := (others => '0');
signal ep_enabled : std_logic_vector(EP_N-1 downto 0) := (others => '0');
signal for_us, ep_exists, unknown_ep, silent : std_logic;
signal ep_select : std_logic_vector(EP_N-1 downto 0);
signal verdict : std_logic_vector(1 downto 0);
signal n_tokens, n_for_us, n_not_ours, n_unknown_ep, n_selected
: std_logic_vector(31 downto 0);
signal running : boolean := true;
type cnt4_t is array (0 to 3) of integer;
begin
clk <= not clk after 5 ns when running else '0';
dut : entity work.usb_ep_decode
generic map (EP_N => EP_N, EPW => EPW)
port map (clk => clk, rst_n => rst_n, dev_address => dev_address,
token_valid => token_valid, token_addr => token_addr,
token_ep => token_ep, ep_enabled => ep_enabled,
for_us => for_us, ep_select => ep_select,
ep_exists => ep_exists, unknown_ep => unknown_ep,
silent => silent, verdict => verdict, n_tokens => n_tokens,
n_for_us => n_for_us, n_not_ours => n_not_ours,
n_unknown_ep => n_unknown_ep, n_selected => n_selected);
stim : process
variable seed1 : positive := 8419;
variable seed2 : positive := 2617;
variable r1 : real;
-- VHDL-2008 requires a shared variable to have a protected type, so the
-- bookkeeping lives inside the single stimulus process instead.
variable errors : integer := 0;
variable n_addr_exh, n_ep_exh : integer := 0;
variable n_match, n_nomatch, n_sel, n_unk : integer := 0;
variable n_perep : cnt4_t := (others => 0);
variable m_tok, m_us, m_no, m_unk, m_sel : integer := 0;
-- count the set bits of a vector -- used for the one-hot property
function popcount(v : std_logic_vector) return integer is
variable c : integer := 0;
begin
for b in v'range loop
if v(b) = '1' then c := c + 1; end if;
end loop;
return c;
end function;
procedure check(cond : boolean; msg : string) is
begin
if not cond then
errors := errors + 1;
if errors <= 25 then
report " FAIL: " & msg
& " (vld=" & std_logic'image(token_valid)(2)
& " dev=" & integer'image(to_integer(unsigned(dev_address)))
& " tok=" & integer'image(to_integer(unsigned(token_addr)))
& " ep=" & integer'image(to_integer(unsigned(token_ep)))
& " | for_us=" & std_logic'image(for_us)(2)
& " exists=" & std_logic'image(ep_exists)(2)
& " unk=" & std_logic'image(unknown_ep)(2)
& " verdict=" & integer'image(to_integer(unsigned(verdict)))
& ")" severity note;
end if;
end if;
end procedure;
procedure rnd(variable v : out integer; m : integer) is
begin
uniform(seed1, seed2, r1);
v := integer(floor(r1 * real(m)));
end procedure;
procedure check_comb is
variable e_us, e_exists, e_unk : boolean;
variable e_sel : std_logic_vector(EP_N-1 downto 0);
variable e_verd : token_verdict_t;
variable epi : integer;
begin
-- The model builds the one-hot select by a LOOP over the endpoints
-- where the design indexes a variable -- a different route, and one
-- that would happily produce two bits set if the rule allowed it.
epi := to_integer(unsigned(token_ep));
e_us := token_valid = '1' and token_addr = dev_address;
e_exists := e_us and ep_enabled(epi) = '1';
e_unk := e_us and ep_enabled(epi) = '0';
e_sel := (others => '0');
for b in 0 to EP_N-1 loop
if e_exists and epi = b then e_sel(b) := '1'; end if;
end loop;
check((for_us = '1') = e_us, "for_us matches the model");
check((ep_exists = '1') = e_exists, "ep_exists matches the model");
check(ep_select = e_sel, "ep_select matches the model");
check((unknown_ep = '1') = e_unk, "unknown_ep matches the model");
check((silent = '1') = e_unk, "silent matches unknown_ep");
if token_valid = '0' then e_verd := TOK_IDLE;
elsif not e_us then e_verd := TOK_NOT_OURS;
elsif e_unk then e_verd := TOK_NO_SUCH_EP;
else e_verd := TOK_SELECTED;
end if;
check(verdict = verdict_code(e_verd), "verdict matches the model");
-- The named verdict must agree with the signals it summarises.
check((verdict = verdict_code(TOK_SELECTED))
= (ep_select /= (ep_select'range => '0')),
"verdict disagrees with ep_select");
check((verdict = verdict_code(TOK_NO_SUCH_EP)) = (silent = '1'),
"verdict disagrees with silent");
-- ---- SAFETY PROPERTIES, independent of the model ----
-- 1. THE property. The select is ONE-HOT or zero.
check(popcount(ep_select) <= 1,
"more than one endpoint was selected -- that is a bus contention inside the device");
-- 2. A token for another device selects nothing and says nothing.
if token_valid = '1' and token_addr /= dev_address then
check(for_us = '0', "a token for another address was claimed");
check(ep_select = (ep_select'range => '0'),
"a token for another device selected an endpoint");
check(silent = '0',
"a token for another device produced a silence WE are responsible for");
end if;
-- 3. Address 0 is not a wildcard.
if token_valid = '1' and to_integer(unsigned(token_addr)) = 0
and to_integer(unsigned(dev_address)) /= 0 then
check(for_us = '0',
"an addressed device answered address 0 -- it will collide with the device being enumerated");
end if;
-- 4. The address is SEVEN bits. Address 64 must not alias to 0.
if token_valid = '1' and to_integer(unsigned(token_addr)) = 64
and to_integer(unsigned(dev_address)) = 0 then
check(for_us = '0',
"address 64 matched address 0 -- the compare is only six bits wide");
end if;
if token_valid = '1' and to_integer(unsigned(token_addr)) = 0
and to_integer(unsigned(dev_address)) = 64 then
check(for_us = '0', "address 0 matched address 64");
end if;
-- 5. A non-existent endpoint gets SILENCE, and selects nothing.
if unknown_ep = '1' then
check(ep_select = (ep_select'range => '0'),
"a non-existent endpoint was selected");
check(silent = '1',
"a non-existent endpoint did not produce silence -- a STALL would claim it exists");
end if;
-- 6. Selecting and not-existing are mutually exclusive.
check(not (ep_select /= (ep_select'range => '0') and unknown_ep = '1'),
"an endpoint was both selected and unknown");
-- 7. Nothing at all happens without a token.
if token_valid = '0' then
check(for_us = '0' and silent = '0'
and ep_select = (ep_select'range => '0'),
"the decode responded with no token present");
end if;
-- 8. A selected endpoint is the one the token named.
if ep_select /= (ep_select'range => '0') then
check(ep_select(epi) = '1',
"the selected endpoint is not the one the token addressed");
end if;
if e_us then n_match := n_match + 1;
elsif token_valid = '1' then n_nomatch := n_nomatch + 1; end if;
if e_sel /= (e_sel'range => '0') then
n_sel := n_sel + 1;
n_perep(epi) := n_perep(epi) + 1;
end if;
if e_unk then n_unk := n_unk + 1; end if;
end procedure;
procedure step is
begin
wait for 1 ns;
check_comb;
if token_valid = '1' then
m_tok := m_tok + 1;
if for_us = '1' then m_us := m_us + 1; else m_no := m_no + 1; end if;
if unknown_ep = '1' then m_unk := m_unk + 1; end if;
if ep_select /= (ep_select'range => '0') then m_sel := m_sel + 1; end if;
end if;
wait until rising_edge(clk);
wait for 1 ns;
check(n_tokens = std_logic_vector(to_unsigned(m_tok, 32)),
"n_tokens matches the model");
check(n_for_us = std_logic_vector(to_unsigned(m_us, 32)),
"n_for_us matches the model");
check(n_not_ours = std_logic_vector(to_unsigned(m_no, 32)),
"n_not_ours matches the model");
check(n_unknown_ep = std_logic_vector(to_unsigned(m_unk, 32)),
"n_unknown_ep matches the model");
check(n_selected = std_logic_vector(to_unsigned(m_sel, 32)),
"n_selected matches the model");
end procedure;
procedure hard_reset is
begin
rst_n <= '0'; token_valid <= '0';
dev_address <= (others => '0'); token_addr <= (others => '0');
token_ep <= (others => '0'); ep_enabled <= (others => '0');
wait until rising_edge(clk); wait for 1 ns;
wait until rising_edge(clk); wait for 1 ns;
rst_n <= '1'; wait for 1 ns;
m_tok := 0; m_us := 0; m_no := 0; m_unk := 0; m_sel := 0;
end procedure;
variable iv, dva : integer;
begin
hard_reset;
check(for_us = '0', "no token, no match");
check(ep_select = (ep_select'range => '0'), "and nothing selected");
-- ===== A. EXHAUSTIVE address-compare sweep =====
-- Every one of the 128 device addresses against every one of the 128
-- token addresses = 16384 points. That is the ENTIRE address domain,
-- which is what it takes to prove the compare is seven bits wide and has
-- no wildcard anywhere in it.
token_valid <= '1'; ep_enabled <= (others => '1');
token_ep <= (others => '0');
for i in 0 to 127 loop
for j in 0 to 127 loop
dev_address <= std_logic_vector(to_unsigned(i, 7));
token_addr <= std_logic_vector(to_unsigned(j, 7));
step;
n_addr_exh := n_addr_exh + 1;
end loop;
end loop;
token_valid <= '0';
report " exhaustive address-compare sweep: " & integer'image(n_addr_exh)
& " of 16384 pairs verified" severity note;
-- ===== B. EXHAUSTIVE endpoint-decode sweep =====
-- Every endpoint-enable mask against every endpoint index, with the
-- address matching and not matching, and with and without a token:
-- 16 masks x 4 endpoints x 2 (address match) x 2 (token_valid) = 256
for m in 0 to 15 loop
for k in 0 to 3 loop
for i in 0 to 1 loop
for v in 0 to 1 loop
dev_address <= std_logic_vector(to_unsigned(37, 7));
if i = 1 then
token_addr <= std_logic_vector(to_unsigned(37, 7));
else
token_addr <= std_logic_vector(to_unsigned(38, 7));
end if;
token_ep <= std_logic_vector(to_unsigned(k, EPW));
ep_enabled <= std_logic_vector(to_unsigned(m, EP_N));
if v = 1 then token_valid <= '1'; else token_valid <= '0'; end if;
step;
n_ep_exh := n_ep_exh + 1;
end loop;
end loop;
end loop;
end loop;
token_valid <= '0';
report " exhaustive endpoint-decode sweep: " & integer'image(n_ep_exh)
& " of 256 points verified" severity note;
-- ===== C. directed: the cases that cause bus contention =====
hard_reset;
dev_address <= std_logic_vector(to_unsigned(9, 7));
ep_enabled <= "1011";
-- 1. Our address, an endpoint we have.
token_valid <= '1'; token_addr <= std_logic_vector(to_unsigned(9, 7));
token_ep <= "01"; wait for 1 ns;
check(for_us = '1', "a token for our address is ours");
check(ep_exists = '1', "endpoint 1 exists");
check(ep_select = "0010", "and exactly endpoint 1 is selected");
step;
-- 2. Our address, an endpoint we do NOT have.
token_valid <= '1'; token_addr <= std_logic_vector(to_unsigned(9, 7));
token_ep <= "10"; wait for 1 ns;
check(for_us = '1', "still our address");
check(ep_exists = '0', "but endpoint 2 does not exist on this device");
check(ep_select = "0000", "so nothing is selected");
check(silent = '1',
"and the device says NOTHING -- a STALL would claim the endpoint exists");
step;
-- 3. Somebody else's address.
token_valid <= '1'; token_addr <= std_logic_vector(to_unsigned(10, 7));
token_ep <= "01"; wait for 1 ns;
check(for_us = '0', "a token for address 10 is not ours");
check(ep_select = "0000", "nothing selected");
check(silent = '0', "and the silence is not ours to produce");
step;
-- 4. THE enumeration hazard.
token_valid <= '1'; token_addr <= (others => '0'); token_ep <= "00";
wait for 1 ns;
check(for_us = '0',
"an ADDRESSED device must not answer address 0 -- the device being enumerated is using it");
check(ep_select = "0000", "so nothing is selected");
step;
-- 5. ...and while WE are unaddressed, address 0 IS ours.
dev_address <= (others => '0');
token_valid <= '1'; token_addr <= (others => '0'); token_ep <= "00";
wait for 1 ns;
check(for_us = '1',
"a device in the DEFAULT state HAS address 0, so the plain equality covers it");
check(ep_select = "0001", "and endpoint 0 is selected");
step;
-- 6. THE seven-bit hazard.
dev_address <= (others => '0');
token_valid <= '1'; token_addr <= std_logic_vector(to_unsigned(64, 7));
token_ep <= "00"; wait for 1 ns;
check(for_us = '0',
"address 64 is NOT address 0 -- the compare is seven bits wide");
step;
dev_address <= std_logic_vector(to_unsigned(64, 7));
token_valid <= '1'; token_addr <= (others => '0'); token_ep <= "00";
wait for 1 ns;
check(for_us = '0', "and address 0 is not address 64");
step;
-- 7. The highest address, exactly.
dev_address <= std_logic_vector(to_unsigned(127, 7));
token_valid <= '1'; token_addr <= std_logic_vector(to_unsigned(127, 7));
token_ep <= "00"; wait for 1 ns;
check(for_us = '1', "address 127 matches itself");
step;
token_valid <= '1'; token_addr <= std_logic_vector(to_unsigned(126, 7));
wait for 1 ns;
check(for_us = '0', "and does not match 126");
step;
token_valid <= '0';
-- ===== D. randomised =====
-- ieee.math_real.uniform is a genuinely different generator from either
-- Verilog builtin, which is what makes this column independent evidence.
hard_reset;
for i in 0 to 39999 loop
-- The device address is kept in a VARIABLE as well as driven onto the
-- signal, because the bias below needs the address chosen THIS
-- iteration. Reading dev_address back would read the PREVIOUS value: a
-- signal assignment does not take effect until the process waits, and
-- the result is a run in which almost no address ever matches. This is
-- the same trap as chapter 22.3 section 9, and it was made twice.
rnd(dva, 128);
dev_address <= std_logic_vector(to_unsigned(dva, 7));
-- bias hard toward a matching address -- a random 7-bit pair matches
-- once in 128, and the interesting half of this block is downstream of
-- a match
rnd(iv, 3);
if iv /= 0 then
token_addr <= std_logic_vector(to_unsigned(dva, 7));
else
rnd(iv, 128); token_addr <= std_logic_vector(to_unsigned(iv, 7));
end if;
rnd(iv, 4); token_ep <= std_logic_vector(to_unsigned(iv, EPW));
rnd(iv, 16); ep_enabled <= std_logic_vector(to_unsigned(iv, EP_N));
rnd(iv, 8); if iv /= 0 then token_valid <= '1';
else token_valid <= '0'; end if;
step;
end loop;
for i in 0 to 3 loop
check(n_perep(i) > 500, "every endpoint was selected many times");
end loop;
check(n_match > 5000, "address matches happened often");
check(n_nomatch > 5000, "address mismatches happened often");
check(n_unk > 2000, "unknown endpoints were addressed often");
report " REACH: address-pairs=" & integer'image(n_addr_exh)
& " ep-points=" & integer'image(n_ep_exh)
& " | matched=" & integer'image(n_match)
& " not-ours=" & integer'image(n_nomatch)
& " selected=" & integer'image(n_sel)
& " unknown-ep=" & integer'image(n_unk) severity note;
report " PER-ENDPOINT selects: ep0=" & integer'image(n_perep(0))
& " ep1=" & integer'image(n_perep(1))
& " ep2=" & integer'image(n_perep(2))
& " ep3=" & integer'image(n_perep(3)) severity note;
report " COUNTERS: tokens="
& integer'image(to_integer(unsigned(n_tokens)))
& " for-us=" & integer'image(to_integer(unsigned(n_for_us)))
& " not-ours=" & integer'image(to_integer(unsigned(n_not_ours)))
& " unknown-ep=" & integer'image(to_integer(unsigned(n_unknown_ep)))
& " selected=" & integer'image(to_integer(unsigned(n_selected)))
severity note;
report " [VHDL] usb_ep_decode: " & integer'image(errors) & " errors"
severity note;
if errors = 0 then
report " [VHDL] PASS" severity note;
else
report " [VHDL] FAIL" severity failure;
end if;
running <= false;
wait;
end process;
end architecture;11. Exhaustive Verification and Mutation Testing
| Measure | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| Address pairs | 16384 / 16384 | 16384 / 16384 | 16384 / 16384 |
| Endpoint-decode points | 256 / 256 | 256 / 256 | 256 / 256 |
| address matches | 23511 | 23524 | 23617 |
| address mismatches | 27960 | 27914 | 27912 |
| endpoints selected | 11819 | 11789 | 11890 |
| unknown endpoints addressed | 11692 | 11735 | 11727 |
| selects per endpoint (0/1/2/3) | 3065 / 2905 / 2892 / 2957 | 3020 / 2966 / 2844 / 2959 | 3044 / 2920 / 2955 / 2971 |
| Result | PASS | PASS | PASS |
| # | Mutation | Verilog | SysVer | VHDL |
|---|---|---|---|---|
| Q1 | address 0 is treated as a wildcard | 1622 | 1574 | 1560 |
| Q2 | the select ignores whether the endpoint exists | 46769 | 46941 | 46909 |
| Q3 | the select ignores whether the token was ours | 66488 | 65897 | 66149 |
| Q4 | the select also sets bit 0 — two hot | 17509 | 17539 | 17693 |
| Q5 | for_us ignores token_valid | 17311 | 17191 | 16841 |
| Q6 | the address compare is six bits | 1354 | 1348 | 1306 |
| Q7 | a valid endpoint also produces silence | 23638 | 23578 | 23780 |
| — | unmutated baseline | 0 | 0 | 0 |
All seven die in all three languages, all counts distinct, columns within 3%.
Q1 and Q6 are the two smallest and the two most dangerous. Q1 is wrong only when a token carries address 0 and the device has a different address — one address value out of 128. Q6 is wrong only for address pairs differing in bit 6 alone — 64 pairs out of 16 384. Both produce bus contention between two devices, which is the worst failure mode in this chapter, and both are near the bottom of the table.
Q3 is the largest at ~66 000, and it is the same bug as Q1 with the check removed entirely rather than widened. The ratio between them — 40× — is the ratio between "answers one extra address" and "answers all of them".
Q4 is the one-hot violation, at ~17 500. Note that it is caught by the model and by safety property 1, and it is worth having both: the model check would pass a design where the shift produced the wrong single bit, and the popcount check would pass a design that selected the wrong endpoint. Neither subsumes the other, and the design is wrong in different ways under each.
12. UVM, Assertions, and the Bus-Contention Property
12.1 The sequence that matters
class usb_token_item extends uvm_sequence_item;
`uvm_object_utils(usb_token_item)
rand bit [6:0] dev_address;
rand bit token_valid;
rand bit [6:0] token_addr;
rand bit [1:0] token_ep;
rand bit [3:0] ep_enabled;
// A random 7-bit pair matches once in 128, and the whole interesting half
// of this block is downstream of a match.
constraint c_mostly_matching {
token_addr dist { dev_address := 2, [0:127] := 1 };
}
// A real device has endpoint 0 and usually one or two more.
constraint c_realistic_eps { ep_enabled[0] == 1; }
constraint c_mostly_valid { token_valid dist {1 := 7, 0 := 1}; }
function new(string name = "usb_token_item"); super.new(name); endfunction
endclass
// THE sequence for this chapter. It reproduces the enumeration hazard: this
// device is ADDRESSED, and the host is talking to address 0 because another
// device has just been reset. Every one of these tokens must be ignored.
//
// Random stimulus reaches this by accident once in 128 tokens; the bug it
// finds causes two devices to drive the bus at once, so it is worth
// reaching on purpose.
class enumeration_hazard_seq extends uvm_sequence #(usb_token_item);
`uvm_object_utils(enumeration_hazard_seq)
function new(string name = "enumeration_hazard_seq"); super.new(name); endfunction
task body();
repeat (500) begin
usb_token_item it = usb_token_item::type_id::create("it");
start_item(it);
it.c_mostly_matching.constraint_mode(0);
if (!it.randomize() with { token_valid == 1;
dev_address != 7'd0; // we ARE addressed
token_addr == 7'd0; })// ...and this is not ours
`uvm_error("RAND", "enumeration-hazard randomize failed")
finish_item(it);
end
endtask
endclass
// The seven-bit hazard: address pairs that differ in bit 6 ALONE. A six-bit
// compare makes every one of these a match, and there are only 64 of them in
// the whole 16384-pair space.
class bit6_alias_seq extends uvm_sequence #(usb_token_item);
`uvm_object_utils(bit6_alias_seq)
function new(string name = "bit6_alias_seq"); super.new(name); endfunction
task body();
for (int a = 0; a < 64; a++) begin
usb_token_item it = usb_token_item::type_id::create("it");
start_item(it);
it.c_mostly_matching.constraint_mode(0);
if (!it.randomize() with { token_valid == 1;
dev_address == a;
token_addr == (a + 64); })
`uvm_error("RAND", "bit6 randomize failed")
finish_item(it);
end
endtask
endclass
// Tokens for endpoints this device does not implement. Each must produce
// SILENCE -- a STALL would tell the host the endpoint exists.
class phantom_endpoint_seq extends uvm_sequence #(usb_token_item);
`uvm_object_utils(phantom_endpoint_seq)
function new(string name = "phantom_endpoint_seq"); super.new(name); endfunction
task body();
repeat (400) begin
usb_token_item it = usb_token_item::type_id::create("it");
start_item(it);
it.c_mostly_matching.constraint_mode(0);
if (!it.randomize() with { token_valid == 1;
token_addr == dev_address; // ours
ep_enabled[token_ep] == 0; })// but absent
`uvm_error("RAND", "phantom randomize failed")
finish_item(it);
end
endtask
endclass12.2 The scoreboard
class usb_decode_scoreboard extends uvm_scoreboard;
`uvm_component_utils(usb_decode_scoreboard)
uvm_analysis_imp #(usb_decode_mon_item, usb_decode_scoreboard) ap;
int unsigned n_ours, n_theirs, n_phantom, n_selected;
int unsigned n_per_ep[4];
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
function automatic int popcount(bit [3:0] v);
int c = 0;
foreach (v[i]) if (v[i]) c++;
return c;
endfunction
function void write(usb_decode_mon_item t);
bit ours = t.token_valid && (t.token_addr == t.dev_address);
bit exists = ours && t.ep_enabled[t.token_ep];
bit phantom = ours && !t.ep_enabled[t.token_ep];
// ---- THE property. The select is ONE-HOT or zero, always. Two
// ---- endpoint blocks driving the shared datapath is a contention
// ---- INSIDE the device, and nothing on the bus will reveal it.
if (popcount(t.ep_select) > 1)
`uvm_error("CONTENTION",
$sformatf("ep_select=%b has %0d bits set -- two endpoint blocks are driving the shared datapath",
t.ep_select, popcount(t.ep_select)))
// ---- A token for another device is answered by NOTHING ----
if (t.token_valid && !ours) begin
if (t.for_us)
`uvm_error("ADDRESS",
$sformatf("claimed a token for address %0d while at address %0d -- two devices will drive the bus together",
t.token_addr, t.dev_address))
if (t.ep_select != '0)
`uvm_error("ADDRESS", "selected an endpoint for another device's token")
if (t.silent)
`uvm_error("ADDRESS",
"produced a silence we are not responsible for -- another device is answering this")
n_theirs++;
end
// ---- Address 0 is not a wildcard ----
if (t.token_valid && (t.token_addr == 7'd0) && (t.dev_address != 7'd0)
&& t.for_us)
`uvm_error("ENUMERATION",
"an addressed device answered address 0 -- it is colliding with the device being enumerated")
// ---- The compare is SEVEN bits ----
if (t.token_valid && t.for_us && (t.token_addr[6] != t.dev_address[6]))
`uvm_error("WIDTH",
"addresses differing in bit 6 matched -- the compare is only six bits wide")
// ---- A phantom endpoint gets SILENCE, never a select ----
if (phantom) begin
if (t.ep_select != '0)
`uvm_error("PHANTOM", "selected an endpoint this device does not have")
if (!t.silent)
`uvm_error("PHANTOM",
"a non-existent endpoint did not produce silence -- a STALL would tell the host it exists and the host will clear the halt for ever")
n_phantom++;
end
if (exists) begin
if (!t.ep_select[t.token_ep])
`uvm_error("SELECT", "the wrong endpoint was selected")
n_selected++;
n_per_ep[t.token_ep]++;
end
if (ours) n_ours++;
endfunction
function void report_phase(uvm_phase phase);
`uvm_info("SB", $sformatf("ours=%0d theirs=%0d phantom=%0d selected=%0d",
n_ours, n_theirs, n_phantom, n_selected), UVM_LOW)
if (n_theirs == 0) `uvm_error("COVERAGE",
"no token for another device was ever presented -- the address compare is untested")
if (n_phantom == 0) `uvm_error("COVERAGE",
"no token for a non-existent endpoint was ever presented")
foreach (n_per_ep[i])
if (n_per_ep[i] == 0)
`uvm_error("COVERAGE",
$sformatf("endpoint %0d was never selected", i))
endfunction
endclass12.3 Assertions
module usb_ep_decode_sva
import usb_decode_pkg::*;
#(
parameter int EP_N = 4,
parameter int EPW = 2
) (
input logic clk,
input logic rst_n,
input logic [6:0] dev_address,
input logic token_valid,
input logic [6:0] token_addr,
input logic [EPW-1:0] token_ep,
input logic [EP_N-1:0] ep_enabled,
input logic for_us,
input logic [EP_N-1:0] ep_select,
input logic ep_exists,
input logic unknown_ep,
input logic silent,
input token_verdict_e verdict
);
default clocking cb @(posedge clk); endclocking
default disable iff (!rst_n);
// ---- 1. THE property. The select is ONE-HOT or zero. ----
property p_select_is_onehot0;
$onehot0(ep_select);
endproperty
a_select_is_onehot0 : assert property (p_select_is_onehot0)
else $error("ep_select=%b -- two endpoint blocks are driving the shared datapath",
ep_select);
// ---- 2. The address compare is an EQUALITY, in seven bits. ----
property p_for_us_iff_exact_match;
for_us == (token_valid && (token_addr == dev_address));
endproperty
a_for_us_iff_exact_match : assert property (p_for_us_iff_exact_match)
else $error("the address compare is not a plain 7-bit equality");
// ---- 3. An addressed device never answers address 0. ----
property p_no_address_zero_wildcard;
(token_valid && (token_addr == 7'd0) && (dev_address != 7'd0))
|-> !for_us;
endproperty
a_no_address_zero_wildcard :
assert property (p_no_address_zero_wildcard)
else $error("an addressed device answered address 0 -- bus contention with the device being enumerated");
// ---- 4. Addresses differing in bit 6 alone never match. ----
property p_seven_bit_compare;
(token_valid && (token_addr[6] != dev_address[6])) |-> !for_us;
endproperty
a_seven_bit_compare : assert property (p_seven_bit_compare)
else $error("address bit 6 was ignored -- address 64 aliases to address 0");
// ---- 5. A token for another device produces NOTHING. ----
property p_others_token_is_ignored;
(token_valid && !for_us) |-> ((ep_select == '0) && !silent);
endproperty
a_others_token_is_ignored :
assert property (p_others_token_is_ignored);
// ---- 6. A phantom endpoint gets silence and no select. ----
property p_phantom_is_silent;
unknown_ep |-> ((ep_select == '0) && silent);
endproperty
a_phantom_is_silent : assert property (p_phantom_is_silent)
else $error("a non-existent endpoint was answered -- a STALL claims it exists");
// ---- 7. A selected endpoint is the one the token named, and it is
// ---- one this device implements.
property p_select_is_the_named_endpoint;
(ep_select != '0) |-> (ep_select[token_ep] && ep_enabled[token_ep]);
endproperty
a_select_is_the_named_endpoint :
assert property (p_select_is_the_named_endpoint);
// ---- 8. Nothing happens with no token. ----
property p_idle_is_quiet;
!token_valid |-> (!for_us && !silent && (ep_select == '0));
endproperty
a_idle_is_quiet : assert property (p_idle_is_quiet);
// ---- 9. The named verdict agrees with the signals it summarises. ----
property p_verdict_agrees;
((verdict == TOK_SELECTED) == (ep_select != '0))
&& ((verdict == TOK_NO_SUCH_EP) == silent)
&& ((verdict == TOK_IDLE) == !token_valid);
endproperty
a_verdict_agrees : assert property (p_verdict_agrees);
// ---- Cover: the narrow, dangerous populations were reached. ----
c_addr_zero_not_ours : cover property ((token_valid
&& (token_addr == 7'd0) && (dev_address != 7'd0)));
c_bit6_pair : cover property ((token_valid
&& (token_addr[5:0] == dev_address[5:0])
&& (token_addr[6] != dev_address[6])));
c_phantom : cover property ((unknown_ep));
c_each_ep : cover property ((ep_select != '0));
endmodule
bind usb_ep_decode usb_ep_decode_sva #(.EP_N(EP_N), .EPW(EPW)) u_sva (.*);13. Common Misconceptions
"The hub routes tokens to the right device." A USB 2.0 hub in high-speed mode repeats downstream traffic to every enabled port. The device does the filtering.
"Address 0 is a broadcast address." It is the address of a device in the DEFAULT state. Exactly one device may hold it at a time, and an addressed device must not answer it.
"Responding to an unknown endpoint with a STALL is the polite answer." It tells the host the endpoint exists and is halted, so the host clears the halt and retries for ever. Silence is the honest answer.
"A wrong endpoint select just sends data to the wrong place." In a comparison-built decoder it selects two endpoints, and what reaches the shared datapath depends on the technology rather than on the logic.
"Seven bits versus six only matters above 64 devices." It matters above 64 addresses in use, and a hub tree hands out addresses without reusing them within a session.
"token_valid is redundant — the address will not match when idle." It will match whenever the stale address bus happens to hold our address, which is most of the time on a bus that just talked to us. Mutation Q5, ~17 300 failures.
14. Exercises
1. Widen the address compare to (token_addr == dev_address) || (token_addr == 0) and predict which of the eight safety properties fires first. Then explain why the count (~1600) is so much smaller than Q3's (~66 000) despite both being address bugs.
2. Q6 is wrong on 64 of 16 384 address pairs. Compute the probability that a 1000-token random regression contains at least one of them, and use the answer to argue for the exhaustive sweep.
3. Build the endpoint select from four comparisons instead of a shift, introduce a typo in one of them, and confirm that safety property 1 catches it while the model check does not. Then find the mutation the model check catches and property 1 does not.
4. Add a second device instance on the same inputs, at a different address, and write the assertion that at most one of them asserts for_us. Which of this chapter's mutations does that assertion catch that the single-instance properties do not?
5. silent and unknown_ep are the same signal. Is one of them dead? Apply the dead-versus-unreachable test from this curriculum's mutation discipline and justify keeping or removing it.
6. The VHDL testbench read a signal back in the iteration that drove it, producing 454 matches instead of 23 500. Write the reach assertion that catches it in one line, and say why "the model checks all passed" was not enough.
15. Summary
| Idea | Why it matters |
|---|---|
| Every device sees every token | five devices staying silent is the only thing preventing contention |
| The address compare is a plain equality | no wildcard, no broadcast, no "just in case" |
| An addressed device never answers address 0 | enumeration needs exactly one device holding it |
| The address is seven bits | six bits makes address 64 alias to 0 |
| A non-existent endpoint gets silence | a STALL claims it exists and is merely halted |
| The select is one-hot by construction | built by a shift, not by comparisons |
| Three outcomes all mean "drive nothing" | and they are three different facts |
| 16384 address pairs, exhaustively | the dangerous bugs are specific pairs |
| 7 mutations, all killed in 3 languages | the two most dangerous are the two smallest counts |
Tooling
| Step | Command |
|---|---|
| Verilog-2005 | iverilog -g2005 -o dc_v.out dc_v.v dc_v_tb.v && ./dc_v.out |
| SystemVerilog | iverilog -g2012 -o dc_sv.out dc_sv.sv dc_sv_tb.sv && ./dc_sv.out |
| VHDL-2008 analyse | nvc --std=2008 -a dc_vhdl.vhd dc_vhdl_tb.vhd |
| VHDL-2008 elaborate | nvc --std=2008 -e tb_dc_vhdl |
| VHDL-2008 run | nvc --std=2008 -r tb_dc_vhdl |
| One mutation | iverilog -g2005 -DMUT_Q1 -o mm dc_v_mut.v dc_v_tb.v && ./mm |
All three implementations pass with 0 errors: 16 384 of 16 384 address pairs, 256 of 256 endpoint-decode points, 40 000 randomised tokens, every endpoint selected and asserted selected.
Chapter 23.2 — Endpoint RTL takes the one-hot select this block produces and asks what happens when several endpoints want the shared FIFO port at the same time. The answer is a round-robin arbiter — and its defining property is not fairness in the vague sense but bounded waiting: no requester ever waits more than N−1 grants, which is a property you can enumerate rather than argue about.
Continue learning
Related tutorials
- Related topic
Endpoint Logic
A lost ACK and a lost data packet look identical to the host, so it resends the same bytes — and the data toggle is the only thing that tells a device a retransmission from new data.
- Related topic
FIFO Architecture
An endpoint FIFO stores packets, not bytes — a zero-length packet carries nothing and must still occupy a buffer, because it is the only thing that terminates a transfer ending on a packet boundary.
- Related topic
Descriptor Engine
wLength is the size of the host's buffer, not a preference — and whether a zero-length packet must follow depends on comparing what was sent against what was asked for, not against what exists.
- Related topic
Protocol Engine
The CRC is the last thing on the wire, so a device must write the payload before it knows whether the payload is good — provisional writes, commit and rollback, and why you cannot change your mind mid-packet.
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.
