USB · Module 30
Integration Review Checklist
A correct, conformant USB controller still has to live inside somebody else’s chip. Clocks it does not own, four kinds of reset, information crossing domains, and firmware written against a datasheet — reviewed through an interrupt register whose set-and-clear ordering is the whole problem.
The controller is right and it conforms. Now it is one block among thirty on a die, and the failures move: they stop being about USB and start being about everything the controller assumed and nobody wrote down.
1. The Question
What does this block assume about the chip around it, and who is responsible for making each assumption true?
Almost every integration bug is an assumption held by one team and not by the other. The controller assumes its clock is running; the power team gates it. The controller assumes firmware clears the interrupt; firmware assumes reading it is enough. Neither side is wrong about its own block.
THE SIX SEAMS, and every one of them has two owners
clocks who sources each one, can it stop, can it change
resets four different events, four different scopes
domains what crosses, and as what kind of information
the register what firmware sees, when, and with what side effects
interface
memory who owns a buffer at each instant
power what survives a state change and what does not2. Clocks — Who Owns Them, And Can They Stop?
INVARIANT every clock in the block has a named source, a
stated frequency range, and a documented answer to
"can it stop, and if so when"
EVIDENCE the clock list, with a source and an owner per entry
FAILURE SIGNATURE the block works until somebody enables clock gating
in the power management unit, months later
FALSE CONFIDENCE "there is one clock" -- a USB controller never has
one clock
NEXT for each clock that can stop, ask what state is lost
and what is merely pausedA full-speed device controller typically has at least three, and they belong to three different people:
CLOCK SOURCED BY CAN IT STOP?
-------------- ---------------- -----------------------------------
the USB clock a PLL locked to yes, during suspend -- which is the
(48 MHz) a crystal whole point of suspend
the bus clock the SoC fabric yes, whenever the fabric is gated
the PHY clock the PHY often continuous, sometimes not29.6 added a fourth kind: a clock sourced off-chip entirely, by a bridge chip, where the FPGA is the slave of somebody else's oscillator and cannot influence it at all.
3. Resets — Four Events, Four Scopes
29.5 established the distinction and 30.1 reviewed it inside one block. At integration the question changes again: which reset domain owns each piece of state, and does every writer of that state know when it is cleared?
RESET SCOPE WHO MUST KNOW
--------------- ----------------------------- -------------------
power-on / SoC everything, config included nobody -- nothing
exists yet
USB bus reset the endpoint DATA state and FIRMWARE, which holds
every data toggle; NOT the a mental model of the
endpoint configuration device state
endpoint halt one endpoint's toggle firmware, and the host
cleared
software reset whatever the datasheet says everybody, and the
datasheet is often
the only record INVARIANT every register belongs to exactly one reset domain
for each reset TYPE, with a reason
EVIDENCE a table: register x reset type, with cleared /
preserved / undefined in each cell
FAILURE SIGNATURE firmware and hardware disagree about the device
state after a reset the host caused and firmware
did not observe
FALSE CONFIDENCE "reset is connected"
NEXT for every PRESERVED cell, ask whether the writer of
that register knows the reset happenedThe last item is the integration-specific one, and it is where 30.1's F1 becomes a system problem rather than a block problem. A bus reset that preserves the endpoint configuration is correct — and it means firmware's picture of the device state is now stale in a way firmware has no event for, unless the controller raises one. Which is what the interrupt in the next section is for.
4. Domains — What Crosses, And As What Kind Of Information?
The single most damaging habit in integration review is treating "crossing a clock domain" as one problem with one solution. It is six problems, and the synchroniser that is right for one is wrong for the others.
WHAT IS CROSSING CORRECT MECHANISM WRONG ANSWER
------------------------ -------------------------- ---------------
a LEVEL that is stable two flip-flops none -- this is
for many cycles the only case
two flops fit
a PULSE, one cycle wide toggle synchroniser, or a two flops: the
handshake pulse can be
missed entirely
an EVENT that must not handshake with an two flops, which
be lost acknowledgement loses it under
the wrong phase
a MULTI-BIT value that Gray code, or a stable two flops PER
changes incrementally bus plus a handshake BIT, which can
sample a value
that never existed
a MULTI-BIT value that a stable bus plus a two flops per bit
changes arbitrarily handshake, or an async -- same failure,
FIFO more likely
a STREAM an asynchronous FIFO with anything simpler
Gray-coded pointers5. The Register Interface — The Specimen
The seam where the controller meets firmware is a handful of registers, and the review question there is the one from 30.1's collision item, at a different level: two independent agents write to the same bits, and one of them is software.
The specimen is an interrupt status register. Six sticky event bits, a mask, and a level to the CPU.
A NOTE ON THE WORD, because it causes real confusion in review
meetings: the `irq` output here is an SoC INTERRUPT REQUEST to a CPU.
It has nothing whatever to do with a USB INTERRUPT TRANSFER, which is
a host-scheduled polling mechanism on the wire. They share a word and
share nothing else.// =====================================================================
// usb_irq_status -- the interrupt status register at the seam between
// a USB controller and the CPU it interrupts.
//
// CLASSIFICATION: simplified synthesisable teaching RTL. Six events,
// one sticky bit each, a mask, and a level to the interrupt
// controller. That is all it is, and it is where an integration review
// spends more time than its size suggests -- because two independent
// agents write to the same six bits and one of them is software.
//
// A NOTE ON THE WORD, because it causes real confusion: the `irq`
// output here is an SoC INTERRUPT REQUEST to the CPU. It has nothing
// to do with a USB INTERRUPT TRANSFER, which is a host-scheduled
// polling mechanism on the wire. The two share a word and share
// nothing else. See 30.4 section 6.
//
// THE THREE DECISIONS
// 1. A hardware SET beats a firmware CLEAR of the same bit in the
// same cycle. Firmware's write reflects what it read a few cycles
// ago; the event is newer than the decision to clear it.
// 2. Masking HIDES an event, it does not discard one. A masked event
// still sets its status bit, and unmasking reveals it.
// 3. A sticky bit cannot count. Two events before firmware services
// the first are one bit -- so the MULTIPLICITY is genuinely lost,
// and the only honest response is to make the loss visible.
// =====================================================================
module usb_irq_status #(
parameter integer NEV = 6
) (
input wire clk,
input wire rst_n,
// One-cycle pulses from the controller. Bit assignments:
// 0 setup received 1 OUT transfer done 2 IN transfer done
// 3 USB bus reset 4 suspend 5 error
input wire [NEV-1:0] ev,
// ---- the firmware bus ----
input wire fw_we,
input wire [1:0] fw_addr,
input wire [15:0] fw_wdata,
output wire [15:0] fw_rdata,
output wire irq,
output wire [15:0] n_lost
);
localparam [1:0] A_STATUS = 2'd0, // read sticky; write 1 to clear
A_MASK = 2'd1, // read/write
A_PEND = 2'd2, // read-only: status & mask
A_LOST = 2'd3; // read-only
reg [NEV-1:0] sts;
reg [NEV-1:0] msk;
reg [15:0] c_lost;
// Firmware's write-one-to-clear, decoded once.
wire [NEV-1:0] fw_clr =
(fw_we && fw_addr == A_STATUS) ? fw_wdata[NEV-1:0] : {NEV{1'b0}};
// [3] An event arriving for a bit that is ALREADY set. The bit cannot
// represent two, so the second one's existence is recorded here
// instead. A design that stays silent about this is not wrong, but
// firmware can then never tell "one packet" from "three".
wire [NEV-1:0] coincide = ev & sts & ~fw_clr;
integer i;
reg [15:0] lost_add;
always @(*) begin
lost_add = 16'd0;
for (i = 0; i < NEV; i = i + 1)
if (coincide[i]) lost_add = lost_add + 16'd1;
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sts <= {NEV{1'b0}};
msk <= {NEV{1'b0}};
c_lost <= 16'd0;
end else begin
// [1] ONE expression, and the ORDER inside it is the decision.
// The clear is applied to the OLD value and the event is set
// afterwards, so a set and a clear of the same bit in the same
// cycle leave the bit SET. Reverse the two -- (sts | ev) & ~clr --
// and the clear wins instead: the event that arrived during
// firmware's read-modify-write is gone, and nothing records that
// it ever existed. That form reads just as natural and is the
// defect this module exists to demonstrate.
sts <= (sts & ~fw_clr) | ev;
// [2] A write to MASK changes the mask and NOTHING ELSE. Clearing
// status here is a real and common bug: it makes masking destroy
// events rather than hide them, so a driver that masks during a
// critical section loses every event that occurred inside it.
if (fw_we && fw_addr == A_MASK) msk <= fw_wdata[NEV-1:0];
c_lost <= c_lost + lost_add;
end
end
// [4] The interrupt is a LEVEL derived from the two registers, with
// no state of its own. A registered irq is one cycle late, which is
// harmless here, but a registered irq that is SET and CLEARED by
// different conditions is a third copy of the same fact.
assign irq = |(sts & msk);
assign fw_rdata =
(fw_addr == A_STATUS) ? {{(16-NEV){1'b0}}, sts} :
(fw_addr == A_MASK) ? {{(16-NEV){1'b0}}, msk} :
(fw_addr == A_PEND) ? {{(16-NEV){1'b0}}, (sts & msk)} :
c_lost;
assign n_lost = c_lost;
endmodule6. The Three Decisions, And The One That Was Wrong
1 A hardware SET beats a firmware CLEAR of the same bit in the same
cycle. Firmware's write reflects what it read several cycles ago;
the event is newer than the decision to clear it.
2 Masking HIDES an event, it does not DISCARD one. A masked event
still sets its status bit, and unmasking reveals it.
3 A sticky bit cannot count. Two events before firmware services the
first are one bit -- so the multiplicity is genuinely lost, and the
only honest response is to make the loss visible.Decision 1 is one expression, and the order inside it is the entire decision:
sts <= (sts & ~fw_clr) | ev;The clear applies to the old value; the event is set afterwards. Reverse the two operations and the clear wins instead:
sts <= (sts | ev) & ~fw_clr; // WRONG, and it reads identicallyAn event arriving in the same cycle as firmware's clear
7. Interrupts — The Review Items
INVARIANT an event that occurred is either serviced or
recorded as lost; it is never silently discarded
EVIDENCE the set/clear ordering, the mask semantics, and a
loss counter
FAILURE SIGNATURE a device that "occasionally stops responding" under
load, recovers on the next transfer, and cannot be
reproduced with a debugger attached -- because the
debugger changes firmware's timing
FALSE CONFIDENCE "we tested the interrupt" -- almost certainly with
the two agents on different cycles
NEXT ask what firmware does between reading and writing,
and how long it takes ITEM WHY IT MATTERS
---------------------------------------- --------------------------
set beats clear, per bit, in ONE the event is newer than
expression the decision to clear it
masking does not clear a driver that masks during
a critical section must
not lose what happened
a mask write has no other side effect two registers, two writes
the interrupt is DERIVED from status and a third copy of the same
mask, with no state of its own fact can disagree
a loss counter exists a sticky bit cannot count,
and pretending otherwise
is the silent failure
every bit is independent a shared clear or a
mis-indexed mask passes a
one-bit testThe last one is why the specimen's sweep runs over all six bits rather than one. "The bits are independent" is a claim about the design, and a per-bit sweep is what turns it into a measurement.
8. Buffer Ownership And DMA
29.5 built the ownership mechanism; at integration the question is who else can reach the buffer.
INVARIANT at every instant, exactly one agent may write each
buffer, and the transfer of that right is a single
observable event
EVIDENCE name the agents -- controller, DMA engine, CPU,
cache -- and the event that transfers ownership
between each pair
FAILURE SIGNATURE data that is correct when read by a debugger and
wrong when read by the application, or vice versa
FALSE CONFIDENCE "the ownership bit handles it" -- it handles the
controller and firmware; it says nothing about a
cache
NEXT ask what happens to ownership on error, on
cancellation, and on reset QUESTION WHY IT IS ASKED SEPARATELY
------------------------------ ----------------------------------
who owns it now? the base case, usually answered
what transfers ownership? must be ONE event, not a sequence
can both sides access it? the failure that corrupts
can NEITHER side own it? the failure that hangs -- rarer and
much harder to find
what happens on reset? a bus reset mid-transfer leaves a
descriptor owned by hardware that
will never complete it
what happens on error? same question, different trigger
what happens on cancellation? firmware aborting a transfer must
reclaim ownership, and the
controller must agree
is the memory cacheable? if so, ownership transfer needs a
cache maintenance operation, and it
is firmware's job, and it is the
commonest DMA bug in any protocolThe fourth is the one reviews skip. A buffer owned by nobody is not a corruption — it is a hang, and it presents as "the device stopped" with every register looking reasonable.
9. Power States
LAYER HAS ITS OWN STATE MACHINE, AND THEY ARE NOT THE
SAME MACHINE
-------------------- ------------------------------------------------
USB protocol state Default / Address / Configured / Suspended
controller state whatever the datasheet defines
PHY state powered, suspended, disconnected
SoC power domain on, retained, off
firmware policy when the driver decides to allow any of it INVARIANT every layer's transitions are legal from the state
every other layer is in
EVIDENCE the cross product, or an argument for why it does
not need enumerating
FAILURE SIGNATURE a device that suspends correctly and resumes into a
state nothing expected
FALSE CONFIDENCE "suspend works"
NEXT ask which layer notices first when the host
resumes, and whether it canThe specific USB trap: the device must respond to resume signalling while its main clock is stopped. Whatever detects resume cannot be clocked by the clock that suspend stops — which is a clocking requirement generated by a power requirement generated by a protocol requirement, three layers away from where it is implemented.
10. SystemVerilog And VHDL
// =====================================================================
// usb_irq_status_sv -- the same register, in SystemVerilog. Identical
// contract: same ports, same ordering decision, same reset, same
// latency.
//
// The one thing the type system adds here is the $countones on the
// coincidence vector, which replaces a hand-written loop. That is a
// smaller win than it looks and it is worth saying so: the loop was
// never the risk. The risk is the ORDER of the set and the clear, and
// no type system in any language will tell you which way round it
// should be. See 30.4 section 5.
//
// ------------------------------------------------------------------
// The interrupt status register at the seam between
// a USB controller and the CPU it interrupts.
//
// CLASSIFICATION: simplified synthesisable teaching RTL. Six events,
// one sticky bit each, a mask, and a level to the interrupt
// controller. That is all it is, and it is where an integration review
// spends more time than its size suggests -- because two independent
// agents write to the same six bits and one of them is software.
//
// A NOTE ON THE WORD, because it causes real confusion: the `irq`
// output here is an SoC INTERRUPT REQUEST to the CPU. It has nothing
// to do with a USB INTERRUPT TRANSFER, which is a host-scheduled
// polling mechanism on the wire. The two share a word and share
// nothing else. See 30.4 section 6.
//
// THE THREE DECISIONS
// 1. A hardware SET beats a firmware CLEAR of the same bit in the
// same cycle. Firmware's write reflects what it read a few cycles
// ago; the event is newer than the decision to clear it.
// 2. Masking HIDES an event, it does not discard one. A masked event
// still sets its status bit, and unmasking reveals it.
// 3. A sticky bit cannot count. Two events before firmware services
// the first are one bit -- so the MULTIPLICITY is genuinely lost,
// and the only honest response is to make the loss visible.
// =====================================================================
module usb_irq_status_sv #(
parameter int NEV = 6
) (
input logic clk,
input logic rst_n,
// One-cycle pulses from the controller. Bit assignments:
// 0 setup received 1 OUT transfer done 2 IN transfer done
// 3 USB bus reset 4 suspend 5 error
input logic [NEV-1:0] ev,
// ---- the firmware bus ----
input logic fw_we,
input logic [1:0] fw_addr,
input logic [15:0] fw_wdata,
output logic [15:0] fw_rdata,
output logic irq,
output logic [15:0] n_lost
);
localparam [1:0] A_STATUS = 2'd0, // read sticky; write 1 to clear
A_MASK = 2'd1, // read/write
A_PEND = 2'd2, // read-only: status & mask
A_LOST = 2'd3; // read-only
logic [NEV-1:0] sts;
logic [NEV-1:0] msk;
logic [15:0] c_lost;
// Firmware's write-one-to-clear, decoded once.
logic [NEV-1:0] fw_clr;
assign fw_clr =
(fw_we && fw_addr == A_STATUS) ? fw_wdata[NEV-1:0] : {NEV{1'b0}};
// [3] An event arriving for a bit that is ALREADY set. The bit cannot
// represent two, so the second one's existence is recorded here
// instead. A design that stays silent about this is not wrong, but
// firmware can then never tell "one packet" from "three".
logic [NEV-1:0] coincide;
assign coincide = ev & sts & ~fw_clr;
logic [15:0] lost_add;
assign lost_add = 16'($countones(coincide));
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sts <= {NEV{1'b0}};
msk <= {NEV{1'b0}};
c_lost <= 16'd0;
end else begin
// [1] ONE expression, and the ORDER inside it is the decision.
// The clear is applied to the OLD value and the event is set
// afterwards, so a set and a clear of the same bit in the same
// cycle leave the bit SET. Reverse the two -- (sts | ev) & ~clr --
// and the clear wins instead: the event that arrived during
// firmware's read-modify-write is gone, and nothing records that
// it ever existed. That form reads just as natural and is the
// defect this module exists to demonstrate.
sts <= (sts & ~fw_clr) | ev;
// [2] A write to MASK changes the mask and NOTHING ELSE. Clearing
// status here is a real and common bug: it makes masking destroy
// events rather than hide them, so a driver that masks during a
// critical section loses every event that occurred inside it.
if (fw_we && fw_addr == A_MASK) msk <= fw_wdata[NEV-1:0];
c_lost <= c_lost + lost_add;
end
end
// [4] The interrupt is a LEVEL derived from the two registers, with
// no state of its own. A registered irq is one cycle late, which is
// harmless here, but a registered irq that is SET and CLEARED by
// different conditions is a third copy of the same fact.
assign irq = |(sts & msk);
always_comb begin
unique case (fw_addr)
A_STATUS: fw_rdata = {{(16-NEV){1'b0}}, sts};
A_MASK: fw_rdata = {{(16-NEV){1'b0}}, msk};
A_PEND: fw_rdata = {{(16-NEV){1'b0}}, (sts & msk)};
A_LOST: fw_rdata = c_lost;
endcase
end
assign n_lost = c_lost;
`ifdef SVA_ON
// Five properties, each one a review question from 30.4.
// Icarus rejects SVA; under Icarus each is enforced by the named
// procedural check in the testbench.
// THE ORDERING DECISION, stated as a property. This is the one that
// fails on the natural-looking wrong expression.
property p_set_beats_clear;
@(posedge clk) disable iff (!rst_n)
|(ev & fw_clr) |=> ((sts & $past(ev & fw_clr)) == $past(ev & fw_clr));
endproperty
a_set_beats_clear: assert property (p_set_beats_clear);
// MASKING HIDES. A write to the mask changes the mask and nothing else.
property p_mask_write_preserves_status;
@(posedge clk) disable iff (!rst_n)
(fw_we && fw_addr == A_MASK && ev == '0) |=>
(sts == $past(sts));
endproperty
a_mask_write_preserves_status: assert property (p_mask_write_preserves_status);
// THE INTERRUPT IS DERIVED. No third copy of the same fact.
property p_irq_is_derived;
@(posedge clk) disable iff (!rst_n) irq == |(sts & msk);
endproperty
a_irq_is_derived: assert property (p_irq_is_derived);
// A BIT IS NEVER CLEARED WITHOUT BEING ASKED. The commonest silent
// integration bug is a status bit cleared by something other than
// firmware's write to it.
property p_no_spontaneous_clear;
@(posedge clk) disable iff (!rst_n)
##1 (($past(sts) & ~sts) != '0) |->
(($past(sts) & ~sts) == ($past(fw_clr) & ~$past(ev)));
endproperty
a_no_spontaneous_clear: assert property (p_no_spontaneous_clear);
// LOSS IS RECORDED. A multiplicity a sticky bit cannot represent is
// counted rather than silently discarded.
property p_loss_counted;
@(posedge clk) disable iff (!rst_n)
##1 c_lost == $past(c_lost) + 16'($countones($past(coincide)));
endproperty
a_loss_counted: assert property (p_loss_counted);
c_collide: cover property (@(posedge clk) |(ev & fw_clr & sts));
c_masked_ev: cover property (@(posedge clk) |(ev & ~msk));
c_loss: cover property (@(posedge clk) |coincide);
c_all_set: cover property (@(posedge clk) &sts);
`endif
endmodule-- =====================================================================
-- usb_irq_status (VHDL-2008) -- the same interrupt status register.
-- Identical contract to the Verilog and SystemVerilog versions: same
-- ports, same ordering decision, same reset, same latency.
--
-- The ordering decision -- clear applied to the OLD value, event set
-- afterwards -- reads the same in all three languages, and no language
-- will tell you which way round it belongs. That is the point of the
-- chapter this specimen serves: the review classes a type system CAN
-- remove are real and worth having, and this is not one of them.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity usb_irq_status is
generic (NEV : natural := 6);
port (
clk : in std_logic;
rst_n : in std_logic;
ev : in std_logic_vector(NEV - 1 downto 0);
fw_we : in std_logic;
fw_addr : in unsigned(1 downto 0);
fw_wdata : in std_logic_vector(15 downto 0);
fw_rdata : out std_logic_vector(15 downto 0);
irq : out std_logic;
n_lost : out unsigned(15 downto 0)
);
end entity usb_irq_status;
architecture rtl of usb_irq_status is
constant A_STATUS : unsigned(1 downto 0) := "00";
constant A_MASK : unsigned(1 downto 0) := "01";
constant A_PEND : unsigned(1 downto 0) := "10";
constant A_LOST : unsigned(1 downto 0) := "11";
signal sts, msk : std_logic_vector(NEV - 1 downto 0) := (others => '0');
signal c_lost : unsigned(15 downto 0) := (others => '0');
signal fw_clr : std_logic_vector(NEV - 1 downto 0);
signal coincide : std_logic_vector(NEV - 1 downto 0);
signal lost_add : natural range 0 to NEV;
function popcount (v : std_logic_vector) return natural is
variable n : natural := 0;
begin
for i in v'range loop
if v(i) = '1' then n := n + 1; end if;
end loop;
return n;
end function popcount;
begin
fw_clr <= fw_wdata(NEV - 1 downto 0)
when (fw_we = '1' and fw_addr = A_STATUS)
else (others => '0');
-- An event onto a bit that is already set AND is not being cleared
-- this cycle is a multiplicity the sticky bit cannot represent.
coincide <= ev and sts and (not fw_clr);
lost_add <= popcount(coincide);
seq : process (clk, rst_n)
begin
if rst_n = '0' then
sts <= (others => '0');
msk <= (others => '0');
c_lost <= (others => '0');
elsif rising_edge(clk) then
-- The clear applies to the OLD value; the event is set after it,
-- so a set and a clear of the same bit in the same cycle leave
-- the bit SET. Reversing the two makes the clear win and loses
-- the event silently.
sts <= (sts and (not fw_clr)) or ev;
-- A write to MASK changes the mask and nothing else.
if fw_we = '1' and fw_addr = A_MASK then
msk <= fw_wdata(NEV - 1 downto 0);
end if;
c_lost <= c_lost + to_unsigned(lost_add, 16);
end if;
end process seq;
irq <= '1' when (sts and msk) /= (sts'range => '0') else '0';
rd : process (fw_addr, sts, msk, c_lost)
variable v : std_logic_vector(15 downto 0);
begin
v := (others => '0');
case fw_addr is
when A_STATUS => v(NEV - 1 downto 0) := sts;
when A_MASK => v(NEV - 1 downto 0) := msk;
when A_PEND => v(NEV - 1 downto 0) := sts and msk;
when others => v := std_logic_vector(c_lost);
end case;
fw_rdata <= v;
end process rd;
n_lost <= c_lost;
end architecture rtl;11. The Testbench
// =====================================================================
// tb_usb_irq_status -- Verilog-2005 testbench for usb_irq_status.
//
// PHASES
// 1 EXHAUSTIVE every (status bit, event, firmware action) triple:
// 6 bits x 2 event values x 4 actions x 2 mask values
// = 96 combinations, each entered from a built and
// PROVED state
// 2 COLLISION the set/clear race, swept across all six bits and
// both directions
// 3 SCENARIO the named integration traps, one each
// 4 RANDOM supplementary, audited
// =====================================================================
`timescale 1ns/1ps
module tb_usb_irq_status;
localparam integer NEV = 6;
reg clk = 1'b0;
reg rst_n;
reg [NEV-1:0] ev;
reg fw_we;
reg [1:0] fw_addr;
reg [15:0] fw_wdata;
wire [15:0] fw_rdata;
wire irq;
wire [15:0] n_lost;
usb_irq_status #(.NEV(NEV)) dut (
.clk(clk), .rst_n(rst_n), .ev(ev),
.fw_we(fw_we), .fw_addr(fw_addr), .fw_wdata(fw_wdata),
.fw_rdata(fw_rdata), .irq(irq), .n_lost(n_lost)
);
always #5 clk = ~clk;
// ---- the independent reference model ----
reg [NEV-1:0] rm_sts, rm_msk;
reg [15:0] rm_lost;
integer chk_dir, chk_rnd, err, in_random;
integer m_set, m_clr, m_collide, m_coincide, m_masked_ev, m_irq,
m_setupfail;
integer b, e, a, mk, i, j;
// A one-hot vector of the declared width. `1 << b` is an unsized
// expression and cannot be concatenated, which Icarus says plainly
// and many tools do not.
function [NEV-1:0] bit_of;
input integer n;
begin bit_of = {{(NEV-1){1'b0}}, 1'b1} << n; end
endfunction
task bump; begin
if (in_random) chk_rnd = chk_rnd + 1; else chk_dir = chk_dir + 1;
end endtask
task ck;
input [255:0] what;
input [31:0] got;
input [31:0] exp;
begin
bump;
if (got !== exp) begin
err = err + 1;
if (!in_random && err <= 40)
$display(" ** %0s: got %0d expected %0d (t=%0t)", what, got, exp, $time);
end
end
endtask
task ref_step;
reg [NEV-1:0] clr, coin;
integer k;
begin
if (!rst_n) begin
rm_sts = 0; rm_msk = 0; rm_lost = 0;
end else begin
clr = (fw_we && fw_addr == 2'd0) ? fw_wdata[NEV-1:0] : {NEV{1'b0}};
// An event onto a bit that is already set AND is not being
// cleared this cycle is a multiplicity the sticky bit cannot
// represent. If firmware IS clearing it, nothing is lost: the
// event simply sets the bit again.
coin = ev & rm_sts & ~clr;
for (k = 0; k < NEV; k = k + 1)
if (coin[k]) rm_lost = rm_lost + 1;
// The clear applies to the OLD value; the event is set after.
rm_sts = (rm_sts & ~clr) | ev;
if (fw_we && fw_addr == 2'd1) rm_msk = fw_wdata[NEV-1:0];
// tallies
if (|ev) m_set = m_set + 1;
if (|clr) m_clr = m_clr + 1;
if (|(ev & clr)) m_collide = m_collide + 1;
if (|coin) m_coincide = m_coincide + 1;
if (|(ev & ~rm_msk)) m_masked_ev = m_masked_ev + 1;
end
end
endtask
task cmp;
begin
fw_addr = 2'd0; #1;
ck("STATUS", {16'd0, fw_rdata}, {26'd0, rm_sts});
fw_addr = 2'd1; #1;
ck("MASK", {16'd0, fw_rdata}, {26'd0, rm_msk});
fw_addr = 2'd2; #1;
ck("PEND", {16'd0, fw_rdata}, {26'd0, (rm_sts & rm_msk)});
fw_addr = 2'd3; #1;
ck("LOST", {16'd0, fw_rdata}, {16'd0, rm_lost});
ck("irq", {31'd0, irq}, {31'd0, (|(rm_sts & rm_msk))});
ck("n_lost", {16'd0, n_lost}, {16'd0, rm_lost});
if (|(rm_sts & rm_msk)) m_irq = m_irq + 1;
fw_addr = 2'd0;
end
endtask
task step; begin
#1;
@(posedge clk);
ref_step;
#1;
cmp;
ev = 0; fw_we = 0; fw_wdata = 0;
end endtask
task idle; begin step; end endtask
task hard_reset; begin
rst_n = 0; ev = 0; fw_we = 0; fw_addr = 0; fw_wdata = 0;
repeat (3) begin @(posedge clk); ref_step; end
#1; rst_n = 1;
@(posedge clk); ref_step; #1; cmp;
end endtask
task pulse; input [NEV-1:0] e; begin ev = e; step; end endtask
task wr_mask; input [NEV-1:0] m;
begin fw_we = 1; fw_addr = 2'd1; fw_wdata = {10'd0, m}; step; end
endtask
task wr_clear; input [NEV-1:0] c;
begin fw_we = 1; fw_addr = 2'd0; fw_wdata = {10'd0, c}; step; end
endtask
// -----------------------------------------------------------------
// PHASE 1 -- the exhaustive sweep.
//
// DENOMINATOR, derived:
// which bit six events, and they are independent 6
// that bit's state already set, or clear 2
// firmware action nothing / clear this bit / write mask /
// clear a DIFFERENT bit 4
// mask this bit masked, or not 2
// -----------------------------------------------------------------
// 6 x 2 x 4 x 2 = 96
//
// The six bits are swept rather than collapsed because "the bits are
// independent" is a CLAIM about the design, and a per-bit sweep is
// what turns it into a measurement. A design with a shared clear or a
// mis-indexed mask passes a one-bit test.
// -----------------------------------------------------------------
task phase_sweep;
reg [15:0] rd;
begin
for (b = 0; b < NEV; b = b + 1)
for (e = 0; e < 2; e = e + 1)
for (a = 0; a < 4; a = a + 1)
for (mk = 0; mk < 2; mk = mk + 1) begin
hard_reset;
wr_mask(mk ? bit_of(b) : {NEV{1'b0}});
if (e) begin
pulse(bit_of(b));
// prove the state was built
fw_addr = 2'd0; #1; rd = fw_rdata; fw_addr = 2'd0;
bump;
if (rd[b] !== 1'b1) begin
err = err + 1; m_setupfail = m_setupfail + 1;
$display(" ** setup: bit %0d did not set", b);
end
end
case (a)
0: idle;
1: wr_clear(bit_of(b));
2: wr_mask({NEV{1'b1}});
3: wr_clear(bit_of((b + 1) % NEV));
endcase
idle;
end
end
endtask
// -----------------------------------------------------------------
// PHASE 2 -- the collision, swept over every bit.
// An event and firmware's clear of the SAME bit, in the same cycle.
// The event is newer than the decision to clear it, so it wins.
// -----------------------------------------------------------------
task phase_collision;
reg [15:0] rd;
begin
for (b = 0; b < NEV; b = b + 1) begin
// the bit is set, firmware clears it, and it fires again at the
// same instant
hard_reset; wr_mask({NEV{1'b1}});
pulse(bit_of(b));
ev = bit_of(b); fw_we = 1; fw_addr = 2'd0;
fw_wdata = {10'd0, bit_of(b)};
step;
fw_addr = 2'd0; #1; rd = fw_rdata; fw_addr = 2'd0;
ck("collision: the set wins", {31'd0, rd[b]}, 32'd1);
ck("collision: irq stays up", {31'd0, irq}, 32'd1);
// and the same clear with NO event is an ordinary clear
hard_reset; wr_mask({NEV{1'b1}});
pulse(bit_of(b));
wr_clear(bit_of(b));
fw_addr = 2'd0; #1; rd = fw_rdata; fw_addr = 2'd0;
ck("clear alone works", {31'd0, rd[b]}, 32'd0);
ck("irq drops", {31'd0, irq}, 32'd0);
end
end
endtask
// -----------------------------------------------------------------
// PHASE 3 -- the named integration traps.
// -----------------------------------------------------------------
task phase_traps;
reg [15:0] rd;
begin
// T1 masking HIDES, it does not discard. A driver that masks
// during a critical section must not lose what happened in it.
hard_reset;
wr_mask(6'b000000);
pulse(6'b000010);
ck("T1 masked: no interrupt", {31'd0, irq}, 32'd0);
fw_addr = 2'd0; #1; rd = fw_rdata; fw_addr = 2'd0;
ck("T1 but the status is set", {31'd0, rd[1]}, 32'd1);
wr_mask(6'b111111);
ck("T1 unmasking reveals it", {31'd0, irq}, 32'd1);
// T2 a write to MASK must not disturb STATUS.
hard_reset; wr_mask(6'b111111);
pulse(6'b101010);
wr_mask(6'b000001);
fw_addr = 2'd0; #1; rd = fw_rdata; fw_addr = 2'd0;
ck("T2 status untouched by a mask write", {26'd0, rd[NEV-1:0]}, 32'b101010);
// T3 a sticky bit cannot count. Two events before firmware
// services the first are one bit -- and the loss is recorded.
hard_reset; wr_mask(6'b111111);
pulse(6'b000100);
pulse(6'b000100);
ck("T3 still one bit", {16'd0, n_lost}, 32'd1);
pulse(6'b000100);
ck("T3 and another", {16'd0, n_lost}, 32'd2);
// T4 clearing one bit leaves the others alone, and the interrupt
// stays asserted while any unmasked bit remains.
hard_reset; wr_mask(6'b111111);
pulse(6'b010100);
wr_clear(6'b000100);
fw_addr = 2'd0; #1; rd = fw_rdata; fw_addr = 2'd0;
ck("T4 only that bit cleared", {26'd0, rd[NEV-1:0]}, 32'b010000);
ck("T4 interrupt still up", {31'd0, irq}, 32'd1);
wr_clear(6'b010000);
ck("T4 now it drops", {31'd0, irq}, 32'd0);
// T5 all six at once, then cleared in one write.
hard_reset; wr_mask(6'b111111);
pulse(6'b111111);
ck("T5 interrupt up", {31'd0, irq}, 32'd1);
wr_clear(6'b111111);
ck("T5 all cleared", {31'd0, irq}, 32'd0);
ck("T5 nothing lost", {16'd0, n_lost}, 32'd0);
end
endtask
// -----------------------------------------------------------------
// PHASE 4 -- random, audited.
// -----------------------------------------------------------------
task phase_random;
integer r;
begin
in_random = 1;
hard_reset; wr_mask(6'b111111);
for (j = 0; j < 6000; j = j + 1) begin
r = {$random} % 100;
if (r < 40) begin
pulse({$random} % 64);
end else if (r < 62) begin
wr_clear({$random} % 64);
end else if (r < 72) begin
wr_mask({$random} % 64);
end else if (r < 88) begin
// the collision, steered: an event and a clear of an
// OVERLAPPING set of bits in the same cycle. Unsteered, two
// random six-bit masks overlap often enough -- but the bit
// that matters is one that is ALREADY SET, and that is rarer.
ev = {$random} % 64;
fw_we = 1; fw_addr = 2'd0; fw_wdata = {10'd0, ev};
step;
end else begin
idle;
end
end
in_random = 0;
end
endtask
initial begin
chk_dir = 0; chk_rnd = 0; err = 0; in_random = 0;
m_set=0; m_clr=0; m_collide=0; m_coincide=0; m_masked_ev=0; m_irq=0;
m_setupfail=0;
phase_sweep;
$display(" phase 1 exhaustive : %0d checks, %0d errors (96 combinations)",
chk_dir, err);
phase_collision;
$display(" phase 2 collision : %0d checks, %0d errors (%0d bits x 2)",
chk_dir, err, NEV);
phase_traps;
$display(" phase 3 traps : %0d checks, %0d errors", chk_dir, err);
$display(" ---- DIRECTED-ONLY : %0d checks, %0d errors ----", chk_dir, err);
phase_random;
$display("");
$display(" measured reachability (all phases)");
$display(" cycles with an event ... %0d", m_set);
$display(" cycles with a clear .... %0d", m_clr);
$display(" set+clear same bit ..... %0d", m_collide);
$display(" event onto a set bit ... %0d", m_coincide);
$display(" event while masked ..... %0d", m_masked_ev);
$display(" cycles asserting irq ... %0d", m_irq);
$display(" setup failures ......... %0d", m_setupfail);
$display("");
$display(" directed checks ........ %0d", chk_dir);
$display(" random checks .......... %0d", chk_rnd);
$display(" TOTAL checks ........... %0d", chk_dir + chk_rnd);
$display(" ERRORS ................. %0d", err);
if (err == 0) $display(" PASS"); else $display(" FAIL");
$finish;
end
endmoduleThe exhaustive phase sweeps 96 combinations and the collision phase sweeps twelve. The second number is smaller and it is the one that matters:
BASE, full bench directed 0 total 0
C-M1, full bench directed 30 total 18,173
C-M1, collision phase REMOVED directed 0 total 18,143
BASE, collision phase removed directed 0 total 0The 96-combination exhaustive sweep catches the ordering defect zero times. Its axes are which bit, that bit's prior state, the firmware action and the mask — four axes, correctly derived, genuinely exhausted, and simultaneity is not one of them. 30.2 §10 draws the general conclusion; this is where it was measured.
The same bench in the other two languages, presenting the same directed stimulus: 3,096 directed checks in each, identical to the digit.
// =====================================================================
// tb_usb_irq_status_sv -- SystemVerilog testbench for usb_irq_status_sv.
//
// Phases 1-3 present the SAME directed stimulus as the Verilog bench,
// so their directed check counts must agree to the digit.
//
// PHASES
// 1 EXHAUSTIVE every (status bit, event, firmware action) triple:
// 6 bits x 2 event values x 4 actions x 2 mask values
// = 96 combinations, each entered from a built and
// PROVED state
// 2 COLLISION the set/clear race, swept across all six bits and
// both directions
// 3 SCENARIO the named integration traps, one each
// 4 RANDOM supplementary, audited
// =====================================================================
`timescale 1ns/1ps
module tb_usb_irq_status_sv;
localparam int NEV = 6;
logic clk = 1'b0;
logic rst_n;
logic [NEV-1:0] ev;
logic fw_we;
logic [1:0] fw_addr;
logic [15:0] fw_wdata;
wire [15:0] fw_rdata;
wire irq;
wire [15:0] n_lost;
usb_irq_status_sv #(.NEV(NEV)) dut (
.clk(clk), .rst_n(rst_n), .ev(ev),
.fw_we(fw_we), .fw_addr(fw_addr), .fw_wdata(fw_wdata),
.fw_rdata(fw_rdata), .irq(irq), .n_lost(n_lost)
);
always #5 clk = ~clk;
// ---- the independent reference model ----
logic [NEV-1:0] rm_sts, rm_msk;
logic [15:0] rm_lost;
int chk_dir, chk_rnd, err;
bit in_random;
int m_set, m_clr, m_collide, m_coincide, m_masked_ev, m_irq,
m_setupfail;
int b, e, a, mk, i, j;
// A one-hot vector of the declared width. `1 << b` is an unsized
// expression and cannot be concatenated, which Icarus says plainly
// and many tools do not.
function logic [NEV-1:0] bit_of(int n);
return NEV'(1) << n;
endfunction
task bump; begin
if (in_random) chk_rnd = chk_rnd + 1; else chk_dir = chk_dir + 1;
end endtask
task ck(string what, logic [31:0] got, logic [31:0] exp);
begin
bump;
if (got !== exp) begin
err = err + 1;
if (!in_random && err <= 40)
$display(" ** %s: got %0d expected %0d (t=%0t)", what, got, exp, $time);
end
end
endtask
task ref_step;
logic [NEV-1:0] clr, coin;
int k;
begin
if (!rst_n) begin
rm_sts = 0; rm_msk = 0; rm_lost = 0;
end else begin
clr = (fw_we && fw_addr == 2'd0) ? fw_wdata[NEV-1:0] : {NEV{1'b0}};
// An event onto a bit that is already set AND is not being
// cleared this cycle is a multiplicity the sticky bit cannot
// represent. If firmware IS clearing it, nothing is lost: the
// event simply sets the bit again.
coin = ev & rm_sts & ~clr;
for (k = 0; k < NEV; k = k + 1)
if (coin[k]) rm_lost = rm_lost + 1;
// The clear applies to the OLD value; the event is set after.
rm_sts = (rm_sts & ~clr) | ev;
if (fw_we && fw_addr == 2'd1) rm_msk = fw_wdata[NEV-1:0];
// tallies
if (|ev) m_set = m_set + 1;
if (|clr) m_clr = m_clr + 1;
if (|(ev & clr)) m_collide = m_collide + 1;
if (|coin) m_coincide = m_coincide + 1;
if (|(ev & ~rm_msk)) m_masked_ev = m_masked_ev + 1;
end
end
endtask
task cmp;
begin
fw_addr = 2'd0; #1;
ck("STATUS", {16'd0, fw_rdata}, {26'd0, rm_sts});
fw_addr = 2'd1; #1;
ck("MASK", {16'd0, fw_rdata}, {26'd0, rm_msk});
fw_addr = 2'd2; #1;
ck("PEND", {16'd0, fw_rdata}, {26'd0, (rm_sts & rm_msk)});
fw_addr = 2'd3; #1;
ck("LOST", {16'd0, fw_rdata}, {16'd0, rm_lost});
ck("irq", {31'd0, irq}, {31'd0, (|(rm_sts & rm_msk))});
ck("n_lost", {16'd0, n_lost}, {16'd0, rm_lost});
if (|(rm_sts & rm_msk)) m_irq = m_irq + 1;
fw_addr = 2'd0;
end
endtask
task step; begin
#1;
@(posedge clk);
ref_step;
#1;
cmp;
ev = 0; fw_we = 0; fw_wdata = 0;
end endtask
task idle; step; endtask
task hard_reset; begin
rst_n = 0; ev = 0; fw_we = 0; fw_addr = 0; fw_wdata = 0;
repeat (3) begin @(posedge clk); ref_step; end
#1; rst_n = 1;
@(posedge clk); ref_step; #1; cmp;
end endtask
task pulse(logic [NEV-1:0] e); ev = e; step; endtask
task wr_mask(logic [NEV-1:0] m);
fw_we = 1; fw_addr = 2'd1; fw_wdata = {10'd0, m}; step;
endtask
task wr_clear(logic [NEV-1:0] c);
fw_we = 1; fw_addr = 2'd0; fw_wdata = {10'd0, c}; step;
endtask
// -----------------------------------------------------------------
// PHASE 1 -- the exhaustive sweep.
//
// DENOMINATOR, derived:
// which bit six events, and they are independent 6
// that bit's state already set, or clear 2
// firmware action nothing / clear this bit / write mask /
// clear a DIFFERENT bit 4
// mask this bit masked, or not 2
// -----------------------------------------------------------------
// 6 x 2 x 4 x 2 = 96
//
// The six bits are swept rather than collapsed because "the bits are
// independent" is a CLAIM about the design, and a per-bit sweep is
// what turns it into a measurement. A design with a shared clear or a
// mis-indexed mask passes a one-bit test.
// -----------------------------------------------------------------
task phase_sweep;
logic [15:0] rd;
begin
for (b = 0; b < NEV; b = b + 1)
for (e = 0; e < 2; e = e + 1)
for (a = 0; a < 4; a = a + 1)
for (mk = 0; mk < 2; mk = mk + 1) begin
hard_reset;
wr_mask(mk ? bit_of(b) : {NEV{1'b0}});
if (e) begin
pulse(bit_of(b));
// prove the state was built
fw_addr = 2'd0; #1; rd = fw_rdata; fw_addr = 2'd0;
bump;
if (rd[b] !== 1'b1) begin
err = err + 1; m_setupfail = m_setupfail + 1;
$display(" ** setup: bit %0d did not set", b);
end
end
case (a)
0: idle;
1: wr_clear(bit_of(b));
2: wr_mask({NEV{1'b1}});
3: wr_clear(bit_of((b + 1) % NEV));
endcase
idle;
end
end
endtask
// -----------------------------------------------------------------
// PHASE 2 -- the collision, swept over every bit.
// An event and firmware's clear of the SAME bit, in the same cycle.
// The event is newer than the decision to clear it, so it wins.
// -----------------------------------------------------------------
task phase_collision;
logic [15:0] rd;
begin
for (b = 0; b < NEV; b = b + 1) begin
// the bit is set, firmware clears it, and it fires again at the
// same instant
hard_reset; wr_mask({NEV{1'b1}});
pulse(bit_of(b));
ev = bit_of(b); fw_we = 1; fw_addr = 2'd0;
fw_wdata = {10'd0, bit_of(b)};
step;
fw_addr = 2'd0; #1; rd = fw_rdata; fw_addr = 2'd0;
ck("collision: the set wins", {31'd0, rd[b]}, 32'd1);
ck("collision: irq stays up", {31'd0, irq}, 32'd1);
// and the same clear with NO event is an ordinary clear
hard_reset; wr_mask({NEV{1'b1}});
pulse(bit_of(b));
wr_clear(bit_of(b));
fw_addr = 2'd0; #1; rd = fw_rdata; fw_addr = 2'd0;
ck("clear alone works", {31'd0, rd[b]}, 32'd0);
ck("irq drops", {31'd0, irq}, 32'd0);
end
end
endtask
// -----------------------------------------------------------------
// PHASE 3 -- the named integration traps.
// -----------------------------------------------------------------
task phase_traps;
logic [15:0] rd;
begin
// T1 masking HIDES, it does not discard. A driver that masks
// during a critical section must not lose what happened in it.
hard_reset;
wr_mask(6'b000000);
pulse(6'b000010);
ck("T1 masked: no interrupt", {31'd0, irq}, 32'd0);
fw_addr = 2'd0; #1; rd = fw_rdata; fw_addr = 2'd0;
ck("T1 but the status is set", {31'd0, rd[1]}, 32'd1);
wr_mask(6'b111111);
ck("T1 unmasking reveals it", {31'd0, irq}, 32'd1);
// T2 a write to MASK must not disturb STATUS.
hard_reset; wr_mask(6'b111111);
pulse(6'b101010);
wr_mask(6'b000001);
fw_addr = 2'd0; #1; rd = fw_rdata; fw_addr = 2'd0;
ck("T2 status untouched by a mask write", {26'd0, rd[NEV-1:0]}, 32'b101010);
// T3 a sticky bit cannot count. Two events before firmware
// services the first are one bit -- and the loss is recorded.
hard_reset; wr_mask(6'b111111);
pulse(6'b000100);
pulse(6'b000100);
ck("T3 still one bit", {16'd0, n_lost}, 32'd1);
pulse(6'b000100);
ck("T3 and another", {16'd0, n_lost}, 32'd2);
// T4 clearing one bit leaves the others alone, and the interrupt
// stays asserted while any unmasked bit remains.
hard_reset; wr_mask(6'b111111);
pulse(6'b010100);
wr_clear(6'b000100);
fw_addr = 2'd0; #1; rd = fw_rdata; fw_addr = 2'd0;
ck("T4 only that bit cleared", {26'd0, rd[NEV-1:0]}, 32'b010000);
ck("T4 interrupt still up", {31'd0, irq}, 32'd1);
wr_clear(6'b010000);
ck("T4 now it drops", {31'd0, irq}, 32'd0);
// T5 all six at once, then cleared in one write.
hard_reset; wr_mask(6'b111111);
pulse(6'b111111);
ck("T5 interrupt up", {31'd0, irq}, 32'd1);
wr_clear(6'b111111);
ck("T5 all cleared", {31'd0, irq}, 32'd0);
ck("T5 nothing lost", {16'd0, n_lost}, 32'd0);
end
endtask
// -----------------------------------------------------------------
// PHASE 4 -- random, audited.
// -----------------------------------------------------------------
task phase_random;
int r;
begin
in_random = 1;
hard_reset; wr_mask(6'b111111);
for (j = 0; j < 6000; j = j + 1) begin
r = $urandom_range(99);
if (r < 40) begin
pulse(NEV'($urandom_range(63)));
end else if (r < 62) begin
wr_clear(NEV'($urandom_range(63)));
end else if (r < 72) begin
wr_mask(NEV'($urandom_range(63)));
end else if (r < 88) begin
// the collision, steered: an event and a clear of an
// OVERLAPPING set of bits in the same cycle. Unsteered, two
// random six-bit masks overlap often enough -- but the bit
// that matters is one that is ALREADY SET, and that is rarer.
ev = NEV'($urandom_range(63));
fw_we = 1; fw_addr = 2'd0; fw_wdata = {10'd0, ev};
step;
end else begin
idle;
end
end
in_random = 0;
end
endtask
initial begin
chk_dir = 0; chk_rnd = 0; err = 0; in_random = 0;
m_set=0; m_clr=0; m_collide=0; m_coincide=0; m_masked_ev=0; m_irq=0;
m_setupfail=0;
phase_sweep;
$display(" phase 1 exhaustive : %0d checks, %0d errors (96 combinations)",
chk_dir, err);
phase_collision;
$display(" phase 2 collision : %0d checks, %0d errors (%0d bits x 2)",
chk_dir, err, NEV);
phase_traps;
$display(" phase 3 traps : %0d checks, %0d errors", chk_dir, err);
$display(" ---- DIRECTED-ONLY : %0d checks, %0d errors ----", chk_dir, err);
phase_random;
$display("");
$display(" measured reachability (all phases)");
$display(" cycles with an event ... %0d", m_set);
$display(" cycles with a clear .... %0d", m_clr);
$display(" set+clear same bit ..... %0d", m_collide);
$display(" event onto a set bit ... %0d", m_coincide);
$display(" event while masked ..... %0d", m_masked_ev);
$display(" cycles asserting irq ... %0d", m_irq);
$display(" setup failures ......... %0d", m_setupfail);
$display("");
$display(" directed checks ........ %0d", chk_dir);
$display(" random checks .......... %0d", chk_rnd);
$display(" TOTAL checks ........... %0d", chk_dir + chk_rnd);
$display(" ERRORS ................. %0d", err);
if (err == 0) $display(" PASS"); else $display(" FAIL");
$finish;
end
endmodule-- =====================================================================
-- tb_usb_irq_status -- VHDL-2008 testbench for usb_irq_status.
-- Phases 1-3 present the SAME directed stimulus as the Verilog and
-- SystemVerilog benches, so their directed counts must agree.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all;
entity tb_usb_irq_status is
end entity tb_usb_irq_status;
architecture sim of tb_usb_irq_status is
constant NEV : natural := 6;
constant HALF : time := 10 ns;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal ev : std_logic_vector(NEV-1 downto 0) := (others => '0');
signal fw_we : std_logic := '0';
signal fw_addr : unsigned(1 downto 0) := "00";
signal fw_wdata : std_logic_vector(15 downto 0) := (others => '0');
signal fw_rdata : std_logic_vector(15 downto 0);
signal irq : std_logic;
signal n_lost : unsigned(15 downto 0);
signal done_flag : boolean := false;
function b2i (s : std_logic) return integer is
begin
if s = '1' then return 1; else return 0; end if;
end function b2i;
function bit_of (n : natural) return std_logic_vector is
variable v : std_logic_vector(NEV-1 downto 0) := (others => '0');
begin
v(n) := '1';
return v;
end function bit_of;
begin
dut : entity work.usb_irq_status
generic map (NEV => NEV)
port map (clk => clk, rst_n => rst_n, ev => ev,
fw_we => fw_we, fw_addr => fw_addr, fw_wdata => fw_wdata,
fw_rdata => fw_rdata, irq => irq, n_lost => n_lost);
clkgen : process
begin
while not done_flag loop
clk <= '0'; wait for HALF;
clk <= '1'; wait for HALF;
end loop;
wait;
end process clkgen;
stim : process
variable chk_dir, chk_rnd, errs, shown : natural := 0;
variable in_random : boolean := false;
variable rm_sts, rm_msk : std_logic_vector(NEV-1 downto 0) := (others => '0');
variable rm_lost : natural := 0;
variable m_set, m_clr, m_collide, m_coincide, m_masked_ev, m_irq : natural := 0;
variable m_setupfail : natural := 0;
variable seed1 : positive := 190_337;
variable seed2 : positive := 77_003;
procedure bump is
begin
if in_random then chk_rnd := chk_rnd + 1; else chk_dir := chk_dir + 1; end if;
end procedure bump;
procedure ck (what : string; got : integer; exp : integer) is
begin
bump;
if got /= exp then
errs := errs + 1;
if (not in_random) and shown < 40 then
shown := shown + 1;
report " ** " & what & ": got " & integer'image(got) &
" expected " & integer'image(exp) severity warning;
end if;
end if;
end procedure ck;
procedure ref_step is
variable clr, coin : std_logic_vector(NEV-1 downto 0);
variable zero : std_logic_vector(NEV-1 downto 0) := (others => '0');
begin
if rst_n = '0' then
rm_sts := (others => '0'); rm_msk := (others => '0'); rm_lost := 0;
else
if fw_we = '1' and fw_addr = "00" then
clr := fw_wdata(NEV-1 downto 0);
else
clr := (others => '0');
end if;
coin := ev and rm_sts and (not clr);
for k in 0 to NEV-1 loop
if coin(k) = '1' then rm_lost := rm_lost + 1; end if;
end loop;
rm_sts := (rm_sts and (not clr)) or ev;
if fw_we = '1' and fw_addr = "01" then
rm_msk := fw_wdata(NEV-1 downto 0);
end if;
if ev /= zero then m_set := m_set + 1; end if;
if clr /= zero then m_clr := m_clr + 1; end if;
if (ev and clr) /= zero then m_collide := m_collide + 1; end if;
if coin /= zero then m_coincide := m_coincide + 1; end if;
if (ev and (not rm_msk)) /= zero then m_masked_ev := m_masked_ev + 1; end if;
end if;
end procedure ref_step;
procedure cmp is
variable zero : std_logic_vector(NEV-1 downto 0) := (others => '0');
begin
fw_addr <= "00"; wait for 1 ns;
ck("STATUS", to_integer(unsigned(fw_rdata)), to_integer(unsigned(rm_sts)));
fw_addr <= "01"; wait for 1 ns;
ck("MASK", to_integer(unsigned(fw_rdata)), to_integer(unsigned(rm_msk)));
fw_addr <= "10"; wait for 1 ns;
ck("PEND", to_integer(unsigned(fw_rdata)),
to_integer(unsigned(rm_sts and rm_msk)));
fw_addr <= "11"; wait for 1 ns;
ck("LOST", to_integer(unsigned(fw_rdata)), rm_lost);
if (rm_sts and rm_msk) /= zero then
ck("irq", b2i(irq), 1);
m_irq := m_irq + 1;
else
ck("irq", b2i(irq), 0);
end if;
ck("n_lost", to_integer(n_lost), rm_lost);
fw_addr <= "00";
end procedure cmp;
procedure step is
begin
wait for 1 ns;
wait until rising_edge(clk);
ref_step;
wait for 1 ns;
cmp;
ev <= (others => '0'); fw_we <= '0'; fw_wdata <= (others => '0');
end procedure step;
procedure idle is begin step; end procedure;
procedure hard_reset is
begin
rst_n <= '0'; ev <= (others => '0'); fw_we <= '0';
fw_addr <= "00"; fw_wdata <= (others => '0');
for i in 0 to 2 loop wait until rising_edge(clk); ref_step; end loop;
wait for 1 ns; rst_n <= '1';
wait until rising_edge(clk); ref_step; wait for 1 ns; cmp;
end procedure hard_reset;
procedure pulse (e : std_logic_vector(NEV-1 downto 0)) is
begin ev <= e; step; end procedure;
procedure wr_mask (m : std_logic_vector(NEV-1 downto 0)) is
variable d : std_logic_vector(15 downto 0) := (others => '0');
begin
d(NEV-1 downto 0) := m;
fw_we <= '1'; fw_addr <= "01"; fw_wdata <= d; step;
end procedure;
procedure wr_clear (c : std_logic_vector(NEV-1 downto 0)) is
variable d : std_logic_vector(15 downto 0) := (others => '0');
begin
d(NEV-1 downto 0) := c;
fw_we <= '1'; fw_addr <= "00"; fw_wdata <= d; step;
end procedure;
impure function rnd (n : positive) return natural is
variable x : real;
begin
uniform(seed1, seed2, x);
return natural(real(n - 1) * x);
end function rnd;
variable rd : integer;
variable zero : std_logic_vector(NEV-1 downto 0) := (others => '0');
variable allo : std_logic_vector(NEV-1 downto 0) := (others => '1');
variable d16 : std_logic_vector(15 downto 0);
variable r : natural;
begin
-- ---- PHASE 1 : 6 x 2 x 4 x 2 = 96 ----
for b in 0 to NEV-1 loop
for e in 0 to 1 loop
for a in 0 to 3 loop
for mk in 0 to 1 loop
hard_reset;
if mk = 1 then wr_mask(bit_of(b)); else wr_mask(zero); end if;
if e = 1 then
pulse(bit_of(b));
fw_addr <= "00"; wait for 1 ns;
rd := to_integer(unsigned(fw_rdata)); fw_addr <= "00";
bump;
if (rd / (2**b)) mod 2 /= 1 then
errs := errs + 1; m_setupfail := m_setupfail + 1;
report " ** setup: bit did not set" severity warning;
end if;
end if;
case a is
when 0 => idle;
when 1 => wr_clear(bit_of(b));
when 2 => wr_mask(allo);
when others => wr_clear(bit_of((b + 1) mod NEV));
end case;
idle;
end loop;
end loop;
end loop;
end loop;
report " phase 1 exhaustive : " & integer'image(chk_dir) &
" checks, " & integer'image(errs) & " errors (96 combinations)";
-- ---- PHASE 2 : the collision, every bit ----
for b in 0 to NEV-1 loop
hard_reset; wr_mask(allo);
pulse(bit_of(b));
d16 := (others => '0'); d16(NEV-1 downto 0) := bit_of(b);
ev <= bit_of(b); fw_we <= '1'; fw_addr <= "00"; fw_wdata <= d16;
step;
fw_addr <= "00"; wait for 1 ns;
rd := to_integer(unsigned(fw_rdata)); fw_addr <= "00";
ck("collision: the set wins", (rd / (2**b)) mod 2, 1);
ck("collision: irq stays up", b2i(irq), 1);
hard_reset; wr_mask(allo);
pulse(bit_of(b));
wr_clear(bit_of(b));
fw_addr <= "00"; wait for 1 ns;
rd := to_integer(unsigned(fw_rdata)); fw_addr <= "00";
ck("clear alone works", (rd / (2**b)) mod 2, 0);
ck("irq drops", b2i(irq), 0);
end loop;
report " phase 2 collision : " & integer'image(chk_dir) &
" checks, " & integer'image(errs) & " errors (6 bits x 2)";
-- ---- PHASE 3 : the named traps ----
hard_reset;
wr_mask("000000");
pulse("000010");
ck("T1 masked: no interrupt", b2i(irq), 0);
fw_addr <= "00"; wait for 1 ns;
rd := to_integer(unsigned(fw_rdata)); fw_addr <= "00";
ck("T1 but the status is set", (rd / 2) mod 2, 1);
wr_mask("111111");
ck("T1 unmasking reveals it", b2i(irq), 1);
hard_reset; wr_mask("111111");
pulse("101010");
wr_mask("000001");
fw_addr <= "00"; wait for 1 ns;
rd := to_integer(unsigned(fw_rdata)); fw_addr <= "00";
ck("T2 status untouched by a mask write", rd, 42);
hard_reset; wr_mask("111111");
pulse("000100");
pulse("000100");
ck("T3 still one bit", to_integer(n_lost), 1);
pulse("000100");
ck("T3 and another", to_integer(n_lost), 2);
hard_reset; wr_mask("111111");
pulse("010100");
wr_clear("000100");
fw_addr <= "00"; wait for 1 ns;
rd := to_integer(unsigned(fw_rdata)); fw_addr <= "00";
ck("T4 only that bit cleared", rd, 16);
ck("T4 interrupt still up", b2i(irq), 1);
wr_clear("010000");
ck("T4 now it drops", b2i(irq), 0);
hard_reset; wr_mask("111111");
pulse("111111");
ck("T5 interrupt up", b2i(irq), 1);
wr_clear("111111");
ck("T5 all cleared", b2i(irq), 0);
ck("T5 nothing lost", to_integer(n_lost), 0);
report " phase 3 traps : " & integer'image(chk_dir) &
" checks, " & integer'image(errs) & " errors";
report " ---- DIRECTED-ONLY : " & integer'image(chk_dir) &
" checks, " & integer'image(errs) & " errors ----";
-- ---- PHASE 4 : random ----
in_random := true;
hard_reset; wr_mask(allo);
for j in 0 to 5999 loop
r := rnd(100);
if r < 40 then
pulse(std_logic_vector(to_unsigned(rnd(64), NEV)));
elsif r < 62 then
wr_clear(std_logic_vector(to_unsigned(rnd(64), NEV)));
elsif r < 72 then
wr_mask(std_logic_vector(to_unsigned(rnd(64), NEV)));
elsif r < 88 then
d16 := (others => '0');
d16(NEV-1 downto 0) := std_logic_vector(to_unsigned(rnd(64), NEV));
ev <= d16(NEV-1 downto 0);
fw_we <= '1'; fw_addr <= "00"; fw_wdata <= d16;
step;
else
idle;
end if;
end loop;
in_random := false;
report " measured reachability (all phases)";
report " cycles with an event ... " & integer'image(m_set);
report " cycles with a clear .... " & integer'image(m_clr);
report " set+clear same bit ..... " & integer'image(m_collide);
report " event onto a set bit ... " & integer'image(m_coincide);
report " event while masked ..... " & integer'image(m_masked_ev);
report " cycles asserting irq ... " & integer'image(m_irq);
report " setup failures ......... " & integer'image(m_setupfail);
report " directed checks ........ " & integer'image(chk_dir);
report " random checks .......... " & integer'image(chk_rnd);
report " TOTAL checks ........... " & integer'image(chk_dir + chk_rnd);
report " ERRORS ................. " & integer'image(errs);
if errs = 0 then report " PASS"; else report " FAIL" severity failure; end if;
done_flag <= true;
wait;
end process stim;
end architecture sim;12. Mutations, Mapped To Review Items
MUT REVIEW QUESTION V-DIR SV-DIR VH-DIR
C-M1 does a hardware SET beat a firmware CLEAR? 30 30 30
C-M2 does masking HIDE an event or DISCARD it? 78 78 78
C-M3 is the interrupt masked at the right place? 51 51 51
C-M4 is the loss counter counting the right thing? 12 12 12BASE reads zero in all nine columns and every directed column is identical
across the three languages.
C-M2 — a mask write that also clears status — scores 78 and is the one to internalise, because the failure it produces is the hardest kind to attribute. A driver masks interrupts to enter a critical section; every event that occurs inside the section is destroyed rather than deferred; the device appears to work except under the load that makes critical sections long. The bug is in hardware, the trigger is in software, and the symptom is in neither.
13. The Checklist
1 CLOCKS
[ ] every clock has a named source, a frequency range and an owner
[ ] for each: can it stop? when? who decides?
[ ] for each that can stop: is the state PAUSED, LOST, or
UNDEFINED -- and undefined is the finding
[ ] anything that must respond while a clock is stopped is not in
that domain
2 RESETS
[ ] a register x reset-type table exists: cleared / preserved /
undefined, with a reason in every cell
[ ] a USB bus reset clears the data state and the toggles, and NOT
the endpoint configuration
[ ] for every PRESERVED cell: does the writer know the reset
happened, and how?
[ ] software reset scope is written down somewhere other than the
datasheet
3 DOMAIN CROSSINGS
[ ] every crossing is classified by WHAT crosses: level, pulse,
event, incremental multi-bit, arbitrary multi-bit, stream
[ ] the mechanism matches the classification
[ ] no multi-bit value is synchronised bit by bit
[ ] the CDC report has been read, not just generated
[ ] everyone involved knows simulation cannot model metastability
4 THE REGISTER INTERFACE
[ ] a hardware SET beats a firmware CLEAR of the same bit, in one
expression, and the ORDER inside it is deliberate
[ ] masking hides; it does not discard
[ ] a mask write has no other side effect
[ ] the interrupt is derived from status and mask, with no state
[ ] a loss counter exists for events a sticky bit cannot represent
[ ] every bit is swept independently
5 MEMORY AND OWNERSHIP
[ ] one owner per buffer at every instant
[ ] ownership transfer is ONE observable event
[ ] "owned by nobody" is impossible, or is detected
[ ] reset, error and cancellation each have an ownership answer
[ ] if the memory is cacheable, the maintenance operation has an
owner and it is firmware
6 POWER
[ ] the layers are listed and their state machines are separate
[ ] resume detection is not in the domain suspend stops
[ ] what survives each power state is written down
[ ] firmware policy is a layer, and it is somebody's
7 THE SEAM ITSELF
[ ] the datasheet says what the hardware does, including the
collisions
[ ] firmware's assumptions have been read back to the RTL team
[ ] every "must not happen" has a counter firmware can read14. Exercises
1 THE CLOCK TABLE
Write the clock table for a full-speed device controller with
suspend support. Three clocks minimum. For each: source, can it
stop, what state is paused and what is lost.
2 THE RESET TABLE
Take the 30.1 specimen's registers and build the register x
reset-type table. Justify every PRESERVED cell by naming the agent
that must know.
3 CDC CLASSIFICATION
Classify these five crossings and pick a mechanism for each: a
suspend request; a frame counter; a completed-transfer byte count;
an endpoint-halt command; a received data stream.
4 VERILOG
Add a second mask so that one set of events goes to the CPU and
another goes to a power controller. What does irq become? What
does the loss counter now mean?
5 SYSTEMVERILOG
Implement it. Does any type construct help with the second mask?
Say honestly if the answer is no.
6 VHDL
Implement it, and declare the range of the loss increment now that
there are two consumers.
7 THE COLLISION AXIS
Extend the exhaustive sweep so that simultaneity IS an axis. Derive
the new denominator. Compare it to the twelve-case collision phase
and say which you would keep if you could only have one.
8 OWNERSHIP
A transfer is cancelled by firmware while the controller has a
descriptor owned. Write the sequence that returns ownership safely,
and name the two ways it can go wrong.
9 DEBUG PREP
For each of the four mutations in section 12, write the single
register read that would distinguish it from the other three in a
lab.15. The Interview Answer
"The USB controller IP passed its own verification. What do you check when you drop it into an SoC?"
Everything it assumed about the chip around it, because that is where integration bugs live — an assumption held by one team and not by the other, with neither team wrong about its own block.
I start with clocks, and specifically with a list: source, frequency, owner, and can it stop. A USB controller never has one clock. There is a 48 MHz domain derived from a crystal, the bus clock from the fabric, and often the PHY's. For each one that can stop I want the answer to "what happens to the state" to be paused or lost, not undefined, and undefined is the common answer. The USB example that catches people is suspend: the device must respond to resume signalling with its main clock stopped, so the resume detector cannot be in the domain that suspend stops. That is a clocking requirement generated by a protocol requirement three layers away.
Then resets, as a table rather than a sentence. Four different events — power on, USB bus reset, endpoint halt cleared, software reset — with four different scopes, and every register in exactly one category per event. The integration question that the block-level review does not ask is about the preserved cells: if the endpoint configuration survives a bus reset, which is correct, then firmware's picture of the device is now stale and firmware had no event for it. Somebody has to raise one.
Then domain crossings, classified by what is crossing rather than by the fact that something is. A stable level takes two flip-flops and that is the only case where two flip-flops is the answer. A one-cycle pulse needs a toggle or a handshake. A multi-bit value needs Gray coding or a stable bus plus a handshake, because two flops per bit can sample a combination the source never held. And I would say out loud that none of this is settled by simulation: a correct synchroniser and a broken one are identical in every simulator, so CDC is settled in review and by static analysis.
Then the register seam, which is where I would actually spend the time, because two independent agents write the same bits and one of them is software. The specific question is what happens when a hardware event and a firmware write-one-to-clear land on the same cycle. The event is newer than firmware's decision to clear, so the set must win — and it is one expression whose operand order is the whole decision. I have seen the wrong order written under a comment describing the right one, with the testbench's reference model copying the same expression and agreeing with it for forty thousand cycles.
And the thing people skip: masking must hide an event, not discard it. A driver that masks during a critical section and loses everything that happened inside it produces a device that works except under the load that makes critical sections long.
16. What Carries Forward
THE SEAMS
o clocks, resets, domain crossings, the register interface, memory
ownership, power -- each has two owners and no shared document
o every clock needs source, owner, and "can it stop"; and if it can,
the state is PAUSED, LOST or -- the finding -- UNDEFINED
o resume detection cannot be in the domain that suspend stops
o a reset table has a cell per register per reset TYPE, and the
PRESERVED cells are the integration question: does the writer know?
CROSSINGS
o classify by WHAT crosses -- level, pulse, event, incremental
multi-bit, arbitrary multi-bit, stream -- because two flip-flops is
the right answer for exactly one of the six
o two flops per bit on a multi-bit bus samples values that never
existed
o RTL simulation cannot model metastability, so CDC is settled in
review and by static analysis, not by a testbench
THE REGISTER SEAM
o a hardware SET beats a firmware CLEAR, in ONE expression, and the
operand ORDER is the entire decision -- all three languages spell
both versions identically and no type system has an opinion
o masking HIDES; a mask write that clears status destroys exactly the
events a critical section was protecting
o a sticky bit cannot count, so the lost multiplicity needs a counter
o sweep every bit: "the bits are independent" is a claim
OWNERSHIP
o one owner per buffer per instant; transfer is ONE event
o "owned by nobody" is a hang, not a corruption, and reviews skip it
o reset, error and cancellation each need an ownership answer
o cacheable memory adds a maintenance step that belongs to firmware
MEASURED HERE
o the ordering defect was in this module's own specimen, and its
reference model contained the same wrong expression
o a 96-combination exhaustive sweep catches it ZERO times, because
simultaneity was not one of its four axesThe next chapter assumes all of this has already failed, in a rack, eighteen months from now, and asks what to look at first.
Continue learning
Related tutorials
- Related topic
USB in Embedded Devices
On a microcontroller the controller is a peripheral, and the protocol can be perfectly correct while the device goes deaf. Everything turns on one question — who owns this buffer right now — answered by one bit per buffer and exhausted over 132 transitions in three languages.
- 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 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.
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.
