USB · Module 26
USB Interrupt Integration
Hardware sets a status bit on the same cycle the driver writes 1 to clear it, and if the clear wins that interrupt is not delayed — it is gone, along with every event behind it.
Chapter 26.3 put the data on the bus. This chapter is about the one wire that tells software any of it happened.
1. Aggregation Collapses N Sources Into One Line
A USB controller has dozens of interrupt sources — one per endpoint, plus reset, suspend, resume, start-of-frame, port change. The SoC's interrupt controller (a GIC on Arm, a PLIC on RISC-V) has one input for the whole USB block.
So the driver gets one signal and has to read a status register to find out what happened. Everything below follows from that single fact: the status register is the only channel, and it is read at a different time from when the events occurred.
2. The Race That Loses Interrupts
The status register is write-1-to-clear. The driver reads it, handles what it finds, and writes back the bits it handled. Meanwhile the hardware is still running.
cycle N: hardware SETS bit 3
AND the driver writes 1 to CLEAR bit 3If the clear wins, that interrupt is gone. Not delayed — gone. The endpoint is waiting for service that will never come, and the next clue anybody gets is a transfer timeout several milliseconds later, attributed to the link.
The same cycle, two orderings
3. Masking Must Not Clear the Status
A masked source still sets its status bit. It simply does not drive the line.
Otherwise everything that happened while the mask was closed is erased at the moment the driver opens it — which is precisely when it wanted to know.
status what HAPPENED
mask what is allowed to INTERRUPT
irq status AND mask, as a LEVELThree separate things. Conflating any two of them loses information that only the register can carry.
4. And the Line Is a Level, Not an Edge
It stays asserted while any unmasked status bit is set.
5. What We Are Building
usb_intr_agg aggregates N sources:
| Output | What it is |
|---|---|
status | what happened — set by the source, regardless of the mask |
mask | what is allowed to interrupt |
pending | status & mask |
irq | a level: the OR of pending |
irq_edge | what an edge-triggered controller would have seen |
n_race | how often a set and a clear collided, whether or not it was handled |
n_masked_set | a source that fired while masked — not a bug, but worth separating |
n_write_noop | a clear of a bit that was not set |
6. Verilog-2005 Implementation
// usb_intr_agg -- what happens between the controller raising an interrupt
// and the driver finding out which one it was.
//
// AGGREGATION COLLAPSES N SOURCES INTO ONE LINE
//
// A USB controller has dozens of interrupt sources -- one per endpoint, plus
// reset, suspend, resume, SOF, port change. The SoC's interrupt controller
// has one input for the whole USB block. So the driver gets ONE signal and
// has to read a status register to find out what happened.
//
// Everything below follows from that one fact: the status register is the
// only channel, and it is read at a different time from when the events
// occurred.
//
// THE RACE THAT LOSES INTERRUPTS
//
// The status register is write-1-to-clear. The driver reads it, handles what
// it finds, and writes back the bits it handled. Meanwhile the hardware is
// still running.
//
// cycle N: hardware sets bit 3 AND the driver writes 1 to bit 3
//
// If the clear wins, that interrupt is gone. Not delayed -- gone. The
// endpoint is waiting for service that will never come, and the next clue
// anybody gets is a transfer timeout several milliseconds later.
//
// SET MUST WIN.
//
// It is one line of RTL and it is the single most common defect in a
// hand-written interrupt block, because the obvious formulation --
//
// status <= (status | src) & ~clear;
//
// -- reads correctly and is wrong: the AND happens after the OR, so a clear
// in the same cycle as a set beats it.
//
// MASKING MUST NOT CLEAR THE STATUS
//
// A masked source still SETS its status bit. It simply does not drive the
// line. Otherwise everything that happened while the mask was closed is
// erased at the moment the driver opens it -- which is precisely when it
// wanted to know.
//
// status what HAPPENED
// mask what is allowed to INTERRUPT
// irq status AND mask, as a LEVEL
//
// AND THE LINE IS A LEVEL, NOT AN EDGE
//
// It stays asserted while any unmasked status bit is set. An edge-triggered
// aggregate loses the second source entirely if it fires while the first is
// still being handled -- there is no second edge, because the line never
// went low.
module usb_intr_agg #(
parameter integer N = 8 // interrupt sources
) (
input wire clk,
input wire rst_n,
input wire [N-1:0] src, // one pulse per event
input wire mask_wr,
input wire [N-1:0] mask_wdata,
input wire stat_wr, // write-1-to-clear
input wire [N-1:0] stat_wdata,
input wire eot,
output wire irq, // LEVEL: status & mask, any bit
output wire irq_edge, // what an EDGE-triggered controller sees
output wire [N-1:0] status, // what HAPPENED
output wire [N-1:0] mask, // what is allowed to interrupt
output wire [N-1:0] pending, // status & mask
output reg [31:0] n_set,
output reg [31:0] n_clear,
output reg [31:0] n_race, // set and clear in the same cycle
output reg [31:0] n_masked_set, // fired while masked: status set, no irq
output reg [31:0] n_irq_rise,
output reg [31:0] n_write_noop // a clear of a bit that was not set
);
reg [N-1:0] stat_r, mask_r;
reg irq_r;
// A one-cycle pulse on each rise of the level. This is not how the line is
// driven -- it is what an interrupt controller configured for edges would
// have seen, exposed so the difference can be measured rather than argued
// about. A second source firing while the first is pending produces a
// pending bit, a level that never drops, and NO pulse here at all.
reg irq_edge_r;
assign status = stat_r;
assign mask = mask_r;
assign pending = stat_r & mask_r;
// A LEVEL. Recomputed from the registered state every cycle rather than
// latched on an edge, so a second source firing while the first is still
// pending keeps the line up instead of producing no edge at all.
assign irq = |(stat_r & mask_r);
assign irq_edge = irq_edge_r;
integer i;
reg [N-1:0] stat_n, mask_n;
reg [31:0] set_n, clr_n, race_n, msk_n, noop_n;
reg set_i, clr_i;
always @* begin
stat_n = stat_r;
mask_n = mask_r;
set_n = 32'd0; clr_n = 32'd0; race_n = 32'd0;
msk_n = 32'd0; noop_n = 32'd0;
if (eot) begin
// Nothing: the counters are the report.
end else begin
if (mask_wr) mask_n = mask_wdata;
for (i = 0; i < N; i = i + 1) begin
set_i = src[i];
clr_i = stat_wr && stat_wdata[i];
// ---- SET WINS. ----
//
// Written as an explicit priority rather than as
// `(stat_r | src) & ~clr`, which is the same expression with the
// opposite answer on the one cycle that matters.
if (set_i) begin
stat_n[i] = 1'b1;
set_n = set_n + 32'd1;
// A source that fires while masked still sets its bit. It is
// counted separately because "the driver never saw it" and "the
// driver was not allowed to see it yet" are different situations
// and only one of them is a bug.
if (!mask_r[i]) msk_n = msk_n + 32'd1;
end else if (clr_i) begin
if (stat_r[i]) clr_n = clr_n + 32'd1;
// Writing 1 to a bit that was not set is not an error -- a driver
// that writes back the whole word it read does it constantly --
// but it is worth separating from a clear that actually cleared
// something, because a status register that only ever produces
// no-ops means the read and the write-back are looking at
// different things.
else noop_n = noop_n + 32'd1;
stat_n[i] = 1'b0;
end
// The race itself, counted whether or not it was resolved correctly
// -- so that a run can be characterised by how often the situation
// arose, not only by whether the design survived it.
if (set_i && clr_i) race_n = race_n + 32'd1;
end
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
stat_r <= {N{1'b0}};
mask_r <= {N{1'b0}};
irq_r <= 1'b0;
irq_edge_r <= 1'b0;
n_set <= 32'd0;
n_clear <= 32'd0;
n_race <= 32'd0;
n_masked_set <= 32'd0;
n_irq_rise <= 32'd0;
n_write_noop <= 32'd0;
end else begin
stat_r <= stat_n;
mask_r <= mask_n;
irq_r <= |(stat_n & mask_n);
irq_edge_r <= (!irq_r) && |(stat_n & mask_n);
n_set <= n_set + set_n;
n_clear <= n_clear + clr_n;
n_race <= n_race + race_n;
n_masked_set <= n_masked_set + msk_n;
n_write_noop <= n_write_noop + noop_n;
// A rising edge of the aggregate line. Counted because it is what the
// SoC's interrupt controller actually sees if it is configured for
// edges -- and the gap between this and n_set is the whole argument
// for a level.
if (!irq_r && |(stat_n & mask_n)) n_irq_rise <= n_irq_rise + 32'd1;
end
end
endmodule7. SystemVerilog Implementation
// usb_intr_agg -- what happens between the controller raising an interrupt
// and the driver finding out which one it was.
//
// AGGREGATION COLLAPSES N SOURCES INTO ONE LINE
//
// A USB controller has dozens of interrupt sources -- one per endpoint, plus
// reset, suspend, resume, SOF, port change. The SoC's interrupt controller
// has one input for the whole USB block. So the driver gets ONE signal and
// has to read a status register to find out what happened.
//
// Everything below follows from that one fact: the status register is the
// only channel, and it is read at a different time from when the events
// occurred.
//
// THE RACE THAT LOSES INTERRUPTS
//
// The status register is write-1-to-clear. The driver reads it, handles what
// it finds, and writes back the bits it handled. Meanwhile the hardware is
// still running.
//
// cycle N: hardware sets bit 3 AND the driver writes 1 to bit 3
//
// If the clear wins, that interrupt is gone. Not delayed -- gone. The
// endpoint is waiting for service that will never come, and the next clue
// anybody gets is a transfer timeout several milliseconds later.
//
// SET MUST WIN.
//
// It is one line of RTL and it is the single most common defect in a
// hand-written interrupt block, because the obvious formulation --
//
// status <= (status | src) & ~clear;
//
// -- reads correctly and is wrong: the AND happens after the OR, so a clear
// in the same cycle as a set beats it.
//
// MASKING MUST NOT CLEAR THE STATUS
//
// A masked source still SETS its status bit. It simply does not drive the
// line. Otherwise everything that happened while the mask was closed is
// erased at the moment the driver opens it -- which is precisely when it
// wanted to know.
//
// status what HAPPENED
// mask what is allowed to INTERRUPT
// irq status AND mask, as a LEVEL
//
// AND THE LINE IS A LEVEL, NOT AN EDGE
//
// It stays asserted while any unmasked status bit is set. An edge-triggered
// aggregate loses the second source entirely if it fires while the first is
// still being handled -- there is no second edge, because the line never
// went low.
module usb_intr_agg #(
parameter integer N = 8 // interrupt sources
) (
input wire clk,
input wire rst_n,
input wire [N-1:0] src, // one pulse per event
input wire mask_wr,
input wire [N-1:0] mask_wdata,
input wire stat_wr, // write-1-to-clear
input wire [N-1:0] stat_wdata,
input wire eot,
output wire irq, // LEVEL: status & mask, any bit
output wire irq_edge, // what an EDGE-triggered controller sees
output wire [N-1:0] status, // what HAPPENED
output wire [N-1:0] mask, // what is allowed to interrupt
output wire [N-1:0] pending, // status & mask
output logic [31:0] n_set,
output logic [31:0] n_clear,
output logic [31:0] n_race, // set and clear in the same cycle
output logic [31:0] n_masked_set, // fired while masked: status set, no irq
output logic [31:0] n_irq_rise,
output logic [31:0] n_write_noop // a clear of a bit that was not set
);
logic [N-1:0] stat_r, mask_r;
logic irq_r;
// A one-cycle pulse on each rise of the level. This is not how the line is
// driven -- it is what an interrupt controller configured for edges would
// have seen, exposed so the difference can be measured rather than argued
// about. A second source firing while the first is pending produces a
// pending bit, a level that never drops, and NO pulse here at all.
reg irq_edge_r;
assign status = stat_r;
assign mask = mask_r;
assign pending = stat_r & mask_r;
// A LEVEL. Recomputed from the registered state every cycle rather than
// latched on an edge, so a second source firing while the first is still
// pending keeps the line up instead of producing no edge at all.
assign irq = |(stat_r & mask_r);
assign irq_edge = irq_edge_r;
int i;
logic [N-1:0] stat_n, mask_n;
logic [31:0] set_n, clr_n, race_n, msk_n, noop_n;
logic set_i, clr_i;
always_comb begin
stat_n = stat_r;
mask_n = mask_r;
set_n = 32'd0; clr_n = 32'd0; race_n = 32'd0;
msk_n = 32'd0; noop_n = 32'd0;
if (eot) begin
// Nothing: the counters are the report.
end else begin
if (mask_wr) mask_n = mask_wdata;
for (i = 0; i < N; i = i + 1) begin
set_i = src[i];
clr_i = stat_wr && stat_wdata[i];
// ---- SET WINS. ----
//
// Written as an explicit priority rather than as
// `(stat_r | src) & ~clr`, which is the same expression with the
// opposite answer on the one cycle that matters.
if (set_i) begin
stat_n[i] = 1'b1;
set_n = set_n + 32'd1;
// A source that fires while masked still sets its bit. It is
// counted separately because "the driver never saw it" and "the
// driver was not allowed to see it yet" are different situations
// and only one of them is a bug.
if (!mask_r[i]) msk_n = msk_n + 32'd1;
end else if (clr_i) begin
if (stat_r[i]) clr_n = clr_n + 32'd1;
// Writing 1 to a bit that was not set is not an error -- a driver
// that writes back the whole word it read does it constantly --
// but it is worth separating from a clear that actually cleared
// something, because a status register that only ever produces
// no-ops means the read and the write-back are looking at
// different things.
else noop_n = noop_n + 32'd1;
stat_n[i] = 1'b0;
end
// The race itself, counted whether or not it was resolved correctly
// -- so that a run can be characterised by how often the situation
// arose, not only by whether the design survived it.
if (set_i && clr_i) race_n = race_n + 32'd1;
end
end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
stat_r <= '0;
mask_r <= '0;
irq_r <= 1'b0;
irq_edge_r <= 1'b0;
n_set <= 32'd0;
n_clear <= 32'd0;
n_race <= 32'd0;
n_masked_set <= 32'd0;
n_irq_rise <= 32'd0;
n_write_noop <= 32'd0;
end else begin
stat_r <= stat_n;
mask_r <= mask_n;
irq_r <= |(stat_n & mask_n);
irq_edge_r <= (!irq_r) && |(stat_n & mask_n);
n_set <= n_set + set_n;
n_clear <= n_clear + clr_n;
n_race <= n_race + race_n;
n_masked_set <= n_masked_set + msk_n;
n_write_noop <= n_write_noop + noop_n;
// A rising edge of the aggregate line. Counted because it is what the
// SoC's interrupt controller actually sees if it is configured for
// edges -- and the gap between this and n_set is the whole argument
// for a level.
if (!irq_r && |(stat_n & mask_n)) n_irq_rise <= n_irq_rise + 32'd1;
end
end
endmodule8. VHDL-2008 Implementation
-- usb_intr_agg -- what happens between the controller raising an interrupt
-- and the driver finding out which one it was.
--
-- AGGREGATION COLLAPSES N SOURCES INTO ONE LINE
--
-- A USB controller has dozens of interrupt sources -- one per endpoint, plus
-- reset, suspend, resume, SOF, port change. The SoC's interrupt controller
-- has one input for the whole USB block. So the driver gets ONE signal and
-- has to read a status register to find out what happened.
--
-- Everything below follows from that one fact: the status register is the
-- only channel, and it is read at a different time from when the events
-- occurred.
--
-- THE RACE THAT LOSES INTERRUPTS
--
-- The status register is write-1-to-clear. The driver reads it, handles what
-- it finds, and writes back the bits it handled. Meanwhile the hardware is
-- still running.
--
-- cycle N: hardware sets bit 3 AND the driver writes 1 to bit 3
--
-- If the clear wins, that interrupt is gone. Not delayed -- gone. The
-- endpoint is waiting for service that will never come, and the next clue
-- anybody gets is a transfer timeout several milliseconds later.
--
-- SET MUST WIN.
--
-- It is one line of RTL and it is the single most common defect in a
-- hand-written interrupt block, because the obvious formulation --
--
-- status <= (status or src) and not clear;
--
-- -- reads correctly and is wrong: the AND happens after the OR, so a clear
-- in the same cycle as a set beats it.
--
-- MASKING MUST NOT CLEAR THE STATUS
--
-- A masked source still SETS its status bit. It simply does not drive the
-- line. Otherwise everything that happened while the mask was closed is
-- erased at the moment the driver opens it -- which is precisely when it
-- wanted to know.
--
-- status what HAPPENED
-- mask what is allowed to INTERRUPT
-- irq status AND mask, as a LEVEL
--
-- AND THE LINE IS A LEVEL, NOT AN EDGE
--
-- It stays asserted while any unmasked status bit is set. An edge-triggered
-- aggregate loses the second source entirely if it fires while the first is
-- still being handled -- there is no second edge, because the line never
-- went low.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity usb_intr_agg is
generic (
N : integer := 8 -- interrupt sources
);
port (
clk : in std_logic;
rst_n : in std_logic;
src : in std_logic_vector(N-1 downto 0); -- one pulse per event
mask_wr : in std_logic;
mask_wdata : in std_logic_vector(N-1 downto 0);
stat_wr : in std_logic; -- write-1-to-clear
stat_wdata : in std_logic_vector(N-1 downto 0);
eot : in std_logic;
irq : out std_logic; -- LEVEL
irq_edge : out std_logic; -- what an EDGE-triggered controller sees
status : out std_logic_vector(N-1 downto 0); -- what HAPPENED
mask : out std_logic_vector(N-1 downto 0); -- what may interrupt
pending : out std_logic_vector(N-1 downto 0); -- status and mask
n_set : out unsigned(31 downto 0);
n_clear : out unsigned(31 downto 0);
n_race : out unsigned(31 downto 0);
n_masked_set : out unsigned(31 downto 0);
n_irq_rise : out unsigned(31 downto 0);
n_write_noop : out unsigned(31 downto 0)
);
end entity;
architecture rtl of usb_intr_agg is
signal stat_r, mask_r : std_logic_vector(N-1 downto 0) := (others => '0');
signal irq_r : std_logic := '0';
-- A one-cycle pulse on each rise of the level. This is not how the line is
-- driven -- it is what an interrupt controller configured for edges would
-- have seen, exposed so the difference can be measured rather than argued
-- about. A second source firing while the first is pending produces a
-- pending bit, a level that never drops, and NO pulse here at all.
signal irq_edge_r : std_logic := '0';
signal set_c, clr_c, race_c : unsigned(31 downto 0) := (others => '0');
signal msk_c, rise_c, noop_c : unsigned(31 downto 0) := (others => '0');
function any_set (v : std_logic_vector) return std_logic is
begin
for i in v'range loop
if v(i) = '1' then return '1'; end if;
end loop;
return '0';
end function;
begin
status <= stat_r;
mask <= mask_r;
pending <= stat_r and mask_r;
-- A LEVEL. Recomputed from the registered state every cycle rather than
-- latched on an edge, so a second source firing while the first is still
-- pending keeps the line up instead of producing no edge at all.
irq <= any_set(stat_r and mask_r);
irq_edge <= irq_edge_r;
n_set <= set_c;
n_clear <= clr_c;
n_race <= race_c;
n_masked_set <= msk_c;
n_irq_rise <= rise_c;
n_write_noop <= noop_c;
process (clk, rst_n)
variable stat_v, mask_v : std_logic_vector(N-1 downto 0);
variable mask_pre : std_logic_vector(N-1 downto 0);
variable set_i, clr_i : std_logic;
variable d_set, d_clr, d_race, d_msk, d_noop : integer;
begin
if rst_n = '0' then
stat_r <= (others => '0');
mask_r <= (others => '0');
irq_r <= '0';
irq_edge_r <= '0';
set_c <= (others => '0');
clr_c <= (others => '0');
race_c <= (others => '0');
msk_c <= (others => '0');
rise_c <= (others => '0');
noop_c <= (others => '0');
elsif rising_edge(clk) then
stat_v := stat_r;
mask_v := mask_r;
mask_pre := mask_r;
d_set := 0; d_clr := 0; d_race := 0; d_msk := 0; d_noop := 0;
if eot = '1' then
-- Nothing: the counters are the report.
null;
else
if mask_wr = '1' then mask_v := mask_wdata; end if;
for i in 0 to N-1 loop
set_i := src(i);
if stat_wr = '1' then clr_i := stat_wdata(i); else clr_i := '0'; end if;
-- ---- SET WINS. ----
--
-- Written as an explicit priority rather than as
-- `(stat or src) and not clr`, which is the same expression with
-- the opposite answer on the one cycle that matters.
if set_i = '1' then
stat_v(i) := '1';
d_set := d_set + 1;
-- A source that fires while masked still sets its bit. It is
-- counted separately because "the driver never saw it" and "the
-- driver was not allowed to see it yet" are different situations
-- and only one of them is a bug.
if mask_pre(i) = '0' then d_msk := d_msk + 1; end if;
elsif clr_i = '1' then
if stat_v(i) = '1' then
d_clr := d_clr + 1;
else
-- Writing 1 to a bit that was not set is not an error -- a
-- driver that writes back the whole word it read does it
-- constantly -- but it is worth separating from a clear that
-- actually cleared something.
d_noop := d_noop + 1;
end if;
stat_v(i) := '0';
end if;
if set_i = '1' and clr_i = '1' then d_race := d_race + 1; end if;
end loop;
end if;
stat_r <= stat_v;
mask_r <= mask_v;
irq_r <= any_set(stat_v and mask_v);
if irq_r = '0' and any_set(stat_v and mask_v) = '1' then
irq_edge_r <= '1';
else
irq_edge_r <= '0';
end if;
set_c <= set_c + to_unsigned(d_set, 32);
clr_c <= clr_c + to_unsigned(d_clr, 32);
race_c <= race_c + to_unsigned(d_race, 32);
msk_c <= msk_c + to_unsigned(d_msk, 32);
noop_c <= noop_c + to_unsigned(d_noop, 32);
-- A rising edge of the aggregate line. Counted because it is what the
-- SoC's interrupt controller actually sees if it is configured for
-- edges -- and the gap between this and n_set is the whole argument
-- for a level.
if irq_r = '0' and any_set(stat_v and mask_v) = '1' then
rise_c <= rise_c + 1;
end if;
end if;
end process;
end architecture;9. Seeing the Race, and Seeing the Level
A set and a clear on the same bit, in the same cycle
usb_intr_agg — set wins the race
10 cyclesTwo sources, one handled: the level holds and no edge is produced
usb_intr_agg — level versus edge
10 cycles10. The Testbenches
The oracle is a shadow model written from sections 2 to 4 rather than from the RTL, re-derived every cycle and compared against every output.
The exhaustive claim is per bit, and that is the right granularity:
The next state of ONE status bit is a function of four things:
its current value, the source pulse,
the write-1-to-clear, the mask
-> 16 situations, and the race is one of them.
Sweeping eight bits jointly would be 16^8.
Sweeping each bit through all sixteen is 128.Verilog-2005 testbench
`timescale 1ns/1ps
// Testbench for usb_intr_agg.
//
// The oracle is a shadow model written from the chapter's rules rather than
// from the RTL, re-derived every cycle and compared against every output.
//
// THE EXHAUSTIVE CLAIM IS PER BIT, AND THAT IS THE RIGHT GRANULARITY
//
// The next state of one status bit is a function of exactly four things:
//
// its current value, the source pulse, the write-1-to-clear, the mask
//
// which is sixteen situations, and the race this chapter is about is one of
// them. Sweeping eight bits jointly would be 16^8; sweeping each bit through
// all sixteen is 128 and covers every behaviour the design has.
//
// 8 bits x 16 situations = 128, all required
//
// Bit independence -- that one bit's inputs never disturb another's -- is a
// separate claim and gets its own phase, because driving the same situation
// on all eight bits at once cannot distinguish eight independent bits from
// one bit wired eight ways.
module tb_ia_v;
localparam integer N = 8;
reg clk = 1'b0, rst_n = 1'b0;
reg mask_wr = 1'b0, stat_wr = 1'b0, eot = 1'b0;
reg [N-1:0] src = {N{1'b0}}, mask_wdata = {N{1'b0}}, stat_wdata = {N{1'b0}};
wire irq, irq_edge;
wire [N-1:0] status, mask, pending;
wire [31:0] n_set, n_clear, n_race, n_masked_set, n_irq_rise, n_write_noop;
usb_intr_agg #(.N(N)) dut (
.clk(clk), .rst_n(rst_n),
.src(src), .mask_wr(mask_wr), .mask_wdata(mask_wdata),
.stat_wr(stat_wr), .stat_wdata(stat_wdata), .eot(eot),
.irq(irq), .irq_edge(irq_edge), .status(status), .mask(mask),
.pending(pending),
.n_set(n_set), .n_clear(n_clear), .n_race(n_race),
.n_masked_set(n_masked_set), .n_irq_rise(n_irq_rise),
.n_write_noop(n_write_noop)
);
always #5 clk = ~clk;
// ---------------- the shadow model ----------------
reg [N-1:0] m_stat, m_mask;
reg m_irq_prev, m_edge;
reg [31:0] c_set, c_clr, c_race, c_msk, c_rise, c_noop;
integer errors = 0, checks = 0, steps = 0;
integer k;
// reach: bit (8) x situation (16): {stat_r, src, clr, mask}
reg [0:0] reach [0:127];
integer n_reach;
task ck;
input [255:0] nm;
input [31:0] got, exp;
begin
checks = checks + 1;
if (got !== exp) begin
errors = errors + 1;
if (errors < 25)
$display("FAIL t=%0t step=%0d %0s got=%0h exp=%0h",
$time, steps, nm, got, exp);
end
end
endtask
reg [N-1:0] m_mask_pre;
task model_step;
integer i;
reg set_i, clr_i;
begin
if (eot) begin
// nothing
end else begin
if (mask_wr) m_mask = mask_wdata;
for (i = 0; i < N; i = i + 1) begin
set_i = src[i];
clr_i = stat_wr && stat_wdata[i];
// SET WINS. Written as a priority, deliberately: the one-liner
// `(stat | src) & ~clr` is the same expression with the opposite
// answer on the one cycle that matters.
if (set_i) begin
m_stat[i] = 1'b1;
c_set = c_set + 1;
if (!m_mask_pre[i]) c_msk = c_msk + 1;
end else if (clr_i) begin
if (m_stat[i]) c_clr = c_clr + 1;
else c_noop = c_noop + 1;
m_stat[i] = 1'b0;
end
if (set_i && clr_i) c_race = c_race + 1;
end
end
end
endtask
task check_out;
begin
ck("status", {24'd0, status}, {24'd0, m_stat});
ck("mask", {24'd0, mask}, {24'd0, m_mask});
ck("pending", {24'd0, pending}, {24'd0, m_stat & m_mask});
ck("irq", {31'd0, irq}, {31'd0, |(m_stat & m_mask)});
ck("irq_edge", {31'd0, irq_edge}, {31'd0, m_edge});
ck("n_set", n_set, c_set);
ck("n_clear", n_clear, c_clr);
ck("n_race", n_race, c_race);
ck("n_masked_set", n_masked_set, c_msk);
ck("n_irq_rise", n_irq_rise, c_rise);
ck("n_write_noop", n_write_noop, c_noop);
// ---- the structural invariant ----
//
// The line is exactly the OR of the pending bits. If these ever
// disagree the driver is being interrupted by something it cannot
// find in the status register, which is an interrupt storm nobody can
// diagnose from software.
ck("irq == |pending", {31'd0, irq}, {31'd0, |pending});
ck("pending == status & mask", {24'd0, pending}, {24'd0, status & mask});
end
endtask
task step;
reg irq_before;
begin
m_mask_pre = m_mask;
irq_before = |(m_stat & m_mask);
model_step;
m_edge = (!irq_before) && |(m_stat & m_mask);
if (m_edge) c_rise = c_rise + 1;
@(posedge clk);
#1;
steps = steps + 1;
check_out;
end
endtask
task drive; input [N-1:0] s; input mw; input [N-1:0] md;
input sw; input [N-1:0] sd;
begin
src = s; mask_wr = mw; mask_wdata = md;
stat_wr = sw; stat_wdata = sd; eot = 1'b0;
step;
src = {N{1'b0}}; mask_wr = 1'b0; stat_wr = 1'b0;
end
endtask
task idle;
begin
src = {N{1'b0}}; mask_wr = 1'b0; stat_wr = 1'b0; eot = 1'b0;
step;
end
endtask
task set_mask; input [N-1:0] md;
begin drive({N{1'b0}}, 1'b1, md, 1'b0, {N{1'b0}}); end
endtask
task reset_all;
begin
src = {N{1'b0}}; mask_wr = 1'b0; stat_wr = 1'b0; eot = 1'b0;
rst_n = 1'b0;
@(posedge clk); #1;
rst_n = 1'b1;
m_stat = {N{1'b0}}; m_mask = {N{1'b0}}; m_mask_pre = {N{1'b0}};
m_edge = 1'b0;
c_set=0; c_clr=0; c_race=0; c_msk=0; c_rise=0; c_noop=0;
@(negedge clk);
end
endtask
integer c, b, i, w, base, n_mask_cases, n_indep_cases;
reg [N-1:0] vs, vm, vc;
initial begin
for (k = 0; k < 128; k = k + 1) reach[k] = 1'b0;
n_mask_cases = 0; n_indep_cases = 0;
repeat (3) @(posedge clk);
rst_n = 1'b1;
@(negedge clk);
// ================= PHASE 1 -- all 16 situations, on every bit =========
//
// The situation is {status, src, clr, mask}. The status bit is set by
// PULSING THE SOURCE, never by forcing the register: a state forced into
// the design is a state the design never proved it can reach.
for (c = 0; c < 16; c = c + 1) begin
reset_all;
// mask first, so the masked-set counter sees the intended value
set_mask(c[0] ? {N{1'b1}} : {N{1'b0}});
// then the current status, via the source
if (c[3]) drive({N{1'b1}}, 1'b0, {N{1'b0}}, 1'b0, {N{1'b0}});
// then the situation itself
vs = c[2] ? {N{1'b1}} : {N{1'b0}};
vc = c[1] ? {N{1'b1}} : {N{1'b0}};
drive(vs, 1'b0, {N{1'b0}}, c[1], vc);
for (b = 0; b < N; b = b + 1) reach[b * 16 + c] = 1'b1;
end
// ================= PHASE 2 -- bit INDEPENDENCE ========================
//
// A different situation on every bit at once. Driving the same one on
// all eight cannot distinguish eight independent bits from one bit wired
// eight ways, and a status register whose bits interfere is a driver
// clearing interrupts it never saw.
for (c = 0; c < 16; c = c + 1) begin
reset_all;
vm = {N{1'b0}}; vs = {N{1'b0}}; vc = {N{1'b0}};
for (b = 0; b < N; b = b + 1) begin
i = (c + b) % 16;
vm[b] = i[0];
end
set_mask(vm);
vs = {N{1'b0}};
for (b = 0; b < N; b = b + 1) begin
i = (c + b) % 16;
vs[b] = i[3];
end
drive(vs, 1'b0, {N{1'b0}}, 1'b0, {N{1'b0}});
vs = {N{1'b0}}; vc = {N{1'b0}};
for (b = 0; b < N; b = b + 1) begin
i = (c + b) % 16;
vs[b] = i[2];
vc[b] = i[1];
end
drive(vs, 1'b0, {N{1'b0}}, 1'b1, vc);
for (b = 0; b < N; b = b + 1) begin
i = (c + b) % 16;
reach[b * 16 + i] = 1'b1;
end
end
// ================= PHASE 3 -- THE RACE ================================
//
// The hardware sets a bit on the very cycle the driver writes 1 to clear
// it. The bit must stay SET: the event happened, and nothing else in the
// system will ever mention it again.
reset_all;
set_mask({N{1'b1}});
drive(8'b0000_1000, 1'b0, {N{1'b0}}, 1'b0, {N{1'b0}}); // bit 3 fires
if (status[3] !== 1'b1 || !irq) begin
errors = errors + 1;
$display("FAIL the source did not set its status bit");
end
base = n_race;
// set and clear, same cycle, same bit
drive(8'b0000_1000, 1'b0, {N{1'b0}}, 1'b1, 8'b0000_1000);
if (n_race != base + 1) begin
errors = errors + 1;
$display("FAIL the race was not counted");
end
if (status[3] !== 1'b1) begin
errors = errors + 1;
$display("FAIL SET LOST THE RACE: the interrupt is gone");
end
if (!irq) begin
errors = errors + 1;
$display("FAIL the line dropped although the event is still pending");
end
// ...and a clear with no set in the same cycle DOES clear it.
drive({N{1'b0}}, 1'b0, {N{1'b0}}, 1'b1, 8'b0000_1000);
if (status[3] !== 1'b0) begin
errors = errors + 1;
$display("FAIL a plain write-1-to-clear did not clear");
end
// ================= PHASE 4 -- masking does not erase ==================
//
// A masked source still records that it happened. Otherwise everything
// that occurred while the mask was closed vanishes at the moment the
// driver opens it -- which is exactly when it wanted to know.
reset_all;
set_mask(8'b0000_0000); // everything masked
base = n_masked_set;
drive(8'b0010_0000, 1'b0, {N{1'b0}}, 1'b0, {N{1'b0}});
if (status[5] !== 1'b1) begin
errors = errors + 1;
$display("FAIL a masked source did not set its status bit");
end
if (irq) begin
errors = errors + 1;
$display("FAIL a masked source drove the interrupt line");
end
if (n_masked_set != base + 1) begin
errors = errors + 1;
$display("FAIL a masked set was not counted");
end
// ...and unmasking raises the line with no new source pulse at all.
set_mask(8'b0010_0000);
if (!irq) begin
errors = errors + 1;
$display("FAIL unmasking did not raise the line for a pending event");
end
// ================= PHASE 5 -- the line is a LEVEL ======================
//
// Two sources. Handle one. The line must stay up, because the other is
// still pending -- and an edge-triggered aggregate would never produce a
// second edge, so the second source would wait for ever.
reset_all;
set_mask({N{1'b1}});
drive(8'b0000_0011, 1'b0, {N{1'b0}}, 1'b0, {N{1'b0}}); // bits 0 and 1
base = n_irq_rise;
drive({N{1'b0}}, 1'b0, {N{1'b0}}, 1'b1, 8'b0000_0001); // handle bit 0
if (!irq) begin
errors = errors + 1;
$display("FAIL the line dropped with bit 1 still pending");
end
if (n_irq_rise != base) begin
errors = errors + 1;
$display("FAIL the line produced a spurious edge");
end
// ---- THE comparison this chapter exists for. ----
//
// The level is still asserted and there is NO new edge. An interrupt
// controller configured for edges has now been told about bit 0 and will
// never be told about bit 1 -- which is still sitting there, pending,
// for ever.
if (irq_edge) begin
errors = errors + 1;
$display("FAIL an edge was produced while the line never dropped");
end
drive({N{1'b0}}, 1'b0, {N{1'b0}}, 1'b1, 8'b0000_0010); // handle bit 1
if (irq) begin
errors = errors + 1;
$display("FAIL the line stayed up with nothing pending");
end
// ================= PHASE 5b -- two claims, over EVERY bit =============
//
// Both of these were driven once, for one bit, and the mutations that
// break them died on SIX checks each.
//
// CLAIM ONE: a mask write must not disturb the status. Otherwise
// everything that happened while the mask was closed is erased at the
// moment the driver opens it -- which is exactly when it wanted to know.
for (b = 0; b < N; b = b + 1)
for (c = 0; c < 2; c = c + 1) begin
reset_all;
set_mask((c == 0) ? {N{1'b0}} : (8'hFF ^ (8'd1 << b)));
drive((8'd1 << b), 1'b0, {N{1'b0}}, 1'b0, {N{1'b0}});
if (!status[b]) begin
errors = errors + 1;
$display("FAIL bit %0d did not record while masked (mask case %0d)",
b, c);
end
// now write the mask -- to anything at all -- and the status must be
// exactly what it was
set_mask({N{1'b1}});
if (!status[b]) begin
errors = errors + 1;
$display("FAIL bit %0d was erased by a mask write (case %0d)", b, c);
end
if (!irq) begin
errors = errors + 1;
$display("FAIL bit %0d: unmasking did not raise the line", b);
end
set_mask({N{1'b0}});
if (!status[b]) begin
errors = errors + 1;
$display("FAIL bit %0d was erased by re-masking", b);
end
n_mask_cases = n_mask_cases + 1;
end
if (n_mask_cases != 2 * N) begin
errors = errors + 1;
$display("FAIL mask scenarios %0d, expected %0d", n_mask_cases, 2 * N);
end
// CLAIM TWO: the bits are INDEPENDENT on a clear. A driver that
// acknowledges the interrupt it handled must not destroy one it has not
// even read yet -- and with all bits set, clearing exactly one must leave
// exactly seven.
for (b = 0; b < N; b = b + 1) begin
reset_all;
set_mask({N{1'b1}});
drive({N{1'b1}}, 1'b0, {N{1'b0}}, 1'b0, {N{1'b0}}); // every bit set
if (status !== {N{1'b1}}) begin
errors = errors + 1;
$display("FAIL not all bits set before the clear: %b", status);
end
drive({N{1'b0}}, 1'b0, {N{1'b0}}, 1'b1, (8'd1 << b));
// exactly the one written, and no other
if (status !== (8'hFF ^ (8'd1 << b))) begin
errors = errors + 1;
$display("FAIL clearing bit %0d left %b, expected %b",
b, status, 8'hFF ^ (8'd1 << b));
end
if (!irq) begin
errors = errors + 1;
$display("FAIL the line dropped with seven bits still pending");
end
n_indep_cases = n_indep_cases + 1;
end
if (n_indep_cases != N) begin
errors = errors + 1;
$display("FAIL independence scenarios %0d, expected %0d",
n_indep_cases, N);
end
// ================= PHASE 6 -- a write-back that clears nothing ========
//
// A driver that writes back the whole word it read clears bits that were
// never set, constantly. It is not an error; it is counted separately so
// that a register which only ever produces no-ops is visible.
reset_all;
set_mask({N{1'b1}});
base = n_write_noop;
drive({N{1'b0}}, 1'b0, {N{1'b0}}, 1'b1, 8'hFF);
if (n_write_noop != base + N) begin
errors = errors + 1;
$display("FAIL %0d no-op clears counted, expected %0d",
n_write_noop - base, N);
end
if (n_clear != 0) begin
errors = errors + 1;
$display("FAIL a no-op was counted as a real clear");
end
// The random phase is switchable, because a mutation score is only
// interesting once it is DECOMPOSED. The directed phases already reach
// every situation the exhaustiveness proof requires, so nothing in that
// claim depends on it.
`ifndef DIRECTED_ONLY
// ================= PHASE 7 -- random =================================
//
// The clear pattern is deliberately correlated with the source pattern:
// drawing them independently makes the same-cycle race a 1-in-256 event
// per bit, and the race is the whole chapter.
reset_all;
set_mask({N{1'b1}});
for (i = 0; i < 30000; i = i + 1) begin
w = $unsigned($random) % 100;
vs = $unsigned($random) % 256;
if (w < 20) vc = vs; // maximise the race
else if (w < 60) vc = $unsigned($random) % 256;
else vc = {N{1'b0}};
if (w < 6) set_mask($unsigned($random) % 256);
else if (w < 92) drive(vs, 1'b0, {N{1'b0}}, (w >= 30), vc);
else idle;
end
`endif
// ================= the exhaustiveness proof ==========================
n_reach = 0;
for (k = 0; k < 128; k = k + 1) n_reach = n_reach + reach[k];
if (n_reach != 128) begin
errors = errors + 1;
$display("FAIL bit x situation reach %0d/128", n_reach);
for (k = 0; k < 128; k = k + 1)
if (!reach[k])
$display(" unreached bit=%0d situation=%0d", k / 16, k % 16);
end
$display("steps=%0d checks=%0d reach=%0d/128 errors=%0d",
steps, checks, n_reach, errors);
$display("set=%0d clear=%0d race=%0d masked_set=%0d irq_rise=%0d noop=%0d",
n_set, n_clear, n_race, n_masked_set, n_irq_rise, n_write_noop);
$display("%0s: %0d errors in %0d checks",
(errors == 0) ? "PASS" : "FAIL", errors, checks);
$finish;
end
endmoduleSystemVerilog testbench
`timescale 1ns/1ps
// Testbench for usb_intr_agg.
//
// The oracle is a shadow model written from the chapter's rules rather than
// from the RTL, re-derived every cycle and compared against every output.
//
// THE EXHAUSTIVE CLAIM IS PER BIT, AND THAT IS THE RIGHT GRANULARITY
//
// The next state of one status bit is a function of exactly four things:
//
// its current value, the source pulse, the write-1-to-clear, the mask
//
// which is sixteen situations, and the race this chapter is about is one of
// them. Sweeping eight bits jointly would be 16^8; sweeping each bit through
// all sixteen is 128 and covers every behaviour the design has.
//
// 8 bits x 16 situations = 128, all required
//
// Bit independence -- that one bit's inputs never disturb another's -- is a
// separate claim and gets its own phase, because driving the same situation
// on all eight bits at once cannot distinguish eight independent bits from
// one bit wired eight ways.
module tb_ia_sv;
localparam int N = 8;
reg clk = 1'b0, rst_n = 1'b0;
reg mask_wr = 1'b0, stat_wr = 1'b0, eot = 1'b0;
reg [N-1:0] src = '0, mask_wdata = '0, stat_wdata = '0;
wire irq, irq_edge;
wire [N-1:0] status, mask, pending;
wire [31:0] n_set, n_clear, n_race, n_masked_set, n_irq_rise, n_write_noop;
usb_intr_agg #(.N(N)) dut (
.clk(clk), .rst_n(rst_n),
.src(src), .mask_wr(mask_wr), .mask_wdata(mask_wdata),
.stat_wr(stat_wr), .stat_wdata(stat_wdata), .eot(eot),
.irq(irq), .irq_edge(irq_edge), .status(status), .mask(mask),
.pending(pending),
.n_set(n_set), .n_clear(n_clear), .n_race(n_race),
.n_masked_set(n_masked_set), .n_irq_rise(n_irq_rise),
.n_write_noop(n_write_noop)
);
always #5 clk = ~clk;
// ---------------- the shadow model ----------------
reg [N-1:0] m_stat, m_mask;
reg m_irq_prev, m_edge;
reg [31:0] c_set, c_clr, c_race, c_msk, c_rise, c_noop;
integer errors = 0, checks = 0, steps = 0;
integer k;
// reach: bit (8) x situation (16): {stat_r, src, clr, mask}
reg [0:0] reach [0:127];
integer n_reach;
task ck;
input [255:0] nm;
input [31:0] got, exp;
begin
checks = checks + 1;
if (got !== exp) begin
errors = errors + 1;
if (errors < 25)
$display("FAIL t=%0t step=%0d %0s got=%0h exp=%0h",
$time, steps, nm, got, exp);
end
end
endtask
logic [N-1:0] m_mask_pre;
task automatic model_step;
int i;
logic set_i, clr_i;
begin
if (eot) begin
// nothing
end else begin
if (mask_wr) m_mask = mask_wdata;
for (i = 0; i < N; i = i + 1) begin
set_i = src[i];
clr_i = stat_wr && stat_wdata[i];
// SET WINS. Written as a priority, deliberately: the one-liner
// `(stat | src) & ~clr` is the same expression with the opposite
// answer on the one cycle that matters.
if (set_i) begin
m_stat[i] = 1'b1;
c_set = c_set + 1;
if (!m_mask_pre[i]) c_msk = c_msk + 1;
end else if (clr_i) begin
if (m_stat[i]) c_clr = c_clr + 1;
else c_noop = c_noop + 1;
m_stat[i] = 1'b0;
end
if (set_i && clr_i) c_race = c_race + 1;
end
end
end
endtask
task automatic check_out;
begin
ck("status", {24'd0, status}, {24'd0, m_stat});
ck("mask", {24'd0, mask}, {24'd0, m_mask});
ck("pending", {24'd0, pending}, {24'd0, m_stat & m_mask});
ck("irq", {31'd0, irq}, {31'd0, |(m_stat & m_mask)});
ck("irq_edge", {31'd0, irq_edge}, {31'd0, m_edge});
ck("n_set", n_set, c_set);
ck("n_clear", n_clear, c_clr);
ck("n_race", n_race, c_race);
ck("n_masked_set", n_masked_set, c_msk);
ck("n_irq_rise", n_irq_rise, c_rise);
ck("n_write_noop", n_write_noop, c_noop);
// ---- the structural invariant ----
//
// The line is exactly the OR of the pending bits. If these ever
// disagree the driver is being interrupted by something it cannot
// find in the status register, which is an interrupt storm nobody can
// diagnose from software.
ck("irq == |pending", {31'd0, irq}, {31'd0, |pending});
ck("pending == status & mask", {24'd0, pending}, {24'd0, status & mask});
end
endtask
task automatic step;
logic irq_before;
begin
m_mask_pre = m_mask;
irq_before = |(m_stat & m_mask);
model_step;
m_edge = (!irq_before) && |(m_stat & m_mask);
if (m_edge) c_rise = c_rise + 1;
@(posedge clk);
#1;
steps = steps + 1;
check_out;
end
endtask
task automatic drive(logic [N-1:0] s, logic mw, logic [N-1:0] md,
logic sw, logic [N-1:0] sd);
begin
src = s; mask_wr = mw; mask_wdata = md;
stat_wr = sw; stat_wdata = sd; eot = 1'b0;
step;
src = '0; mask_wr = 1'b0; stat_wr = 1'b0;
end
endtask
task automatic idle;
begin
src = '0; mask_wr = 1'b0; stat_wr = 1'b0; eot = 1'b0;
step;
end
endtask
task automatic set_mask(logic [N-1:0] md);
begin drive('0, 1'b1, md, 1'b0, '0); end
endtask
task automatic reset_all;
begin
src = '0; mask_wr = 1'b0; stat_wr = 1'b0; eot = 1'b0;
rst_n = 1'b0;
@(posedge clk); #1;
rst_n = 1'b1;
m_stat = '0; m_mask = '0; m_mask_pre = '0;
m_edge = 1'b0;
c_set=0; c_clr=0; c_race=0; c_msk=0; c_rise=0; c_noop=0;
@(negedge clk);
end
endtask
integer c, b, i, w, base, n_mask_cases, n_indep_cases;
reg [N-1:0] vs, vm, vc;
initial begin
foreach (reach[q]) reach[q] = 1'b0;
n_mask_cases = 0; n_indep_cases = 0;
repeat (3) @(posedge clk);
rst_n = 1'b1;
@(negedge clk);
// ================= PHASE 1 -- all 16 situations, on every bit =========
//
// The situation is {status, src, clr, mask}. The status bit is set by
// PULSING THE SOURCE, never by forcing the register: a state forced into
// the design is a state the design never proved it can reach.
for (c = 0; c < 16; c = c + 1) begin
reset_all;
// mask first, so the masked-set counter sees the intended value
set_mask(c[0] ? '1 : '0);
// then the current status, via the source
if (c[3]) drive('1, 1'b0, '0, 1'b0, '0);
// then the situation itself
vs = c[2] ? '1 : '0;
vc = c[1] ? '1 : '0;
drive(vs, 1'b0, '0, c[1], vc);
for (b = 0; b < N; b = b + 1) reach[b * 16 + c] = 1'b1;
end
// ================= PHASE 2 -- bit INDEPENDENCE ========================
//
// A different situation on every bit at once. Driving the same one on
// all eight cannot distinguish eight independent bits from one bit wired
// eight ways, and a status register whose bits interfere is a driver
// clearing interrupts it never saw.
for (c = 0; c < 16; c = c + 1) begin
reset_all;
vm = '0; vs = '0; vc = '0;
for (b = 0; b < N; b = b + 1) begin
i = (c + b) % 16;
vm[b] = i[0];
end
set_mask(vm);
vs = '0;
for (b = 0; b < N; b = b + 1) begin
i = (c + b) % 16;
vs[b] = i[3];
end
drive(vs, 1'b0, '0, 1'b0, '0);
vs = '0; vc = '0;
for (b = 0; b < N; b = b + 1) begin
i = (c + b) % 16;
vs[b] = i[2];
vc[b] = i[1];
end
drive(vs, 1'b0, '0, 1'b1, vc);
for (b = 0; b < N; b = b + 1) begin
i = (c + b) % 16;
reach[b * 16 + i] = 1'b1;
end
end
// ================= PHASE 3 -- THE RACE ================================
//
// The hardware sets a bit on the very cycle the driver writes 1 to clear
// it. The bit must stay SET: the event happened, and nothing else in the
// system will ever mention it again.
reset_all;
set_mask('1);
drive(8'b0000_1000, 1'b0, '0, 1'b0, '0); // bit 3 fires
if (status[3] !== 1'b1 || !irq) begin
errors = errors + 1;
$display("FAIL the source did not set its status bit");
end
base = int'(n_race);
// set and clear, same cycle, same bit
drive(8'b0000_1000, 1'b0, '0, 1'b1, 8'b0000_1000);
if (int'(n_race) != base + 1) begin
errors = errors + 1;
$display("FAIL the race was not counted");
end
if (status[3] !== 1'b1) begin
errors = errors + 1;
$display("FAIL SET LOST THE RACE: the interrupt is gone");
end
if (!irq) begin
errors = errors + 1;
$display("FAIL the line dropped although the event is still pending");
end
// ...and a clear with no set in the same cycle DOES clear it.
drive('0, 1'b0, '0, 1'b1, 8'b0000_1000);
if (status[3] !== 1'b0) begin
errors = errors + 1;
$display("FAIL a plain write-1-to-clear did not clear");
end
// ================= PHASE 4 -- masking does not erase ==================
//
// A masked source still records that it happened. Otherwise everything
// that occurred while the mask was closed vanishes at the moment the
// driver opens it -- which is exactly when it wanted to know.
reset_all;
set_mask(8'b0000_0000); // everything masked
base = int'(n_masked_set);
drive(8'b0010_0000, 1'b0, '0, 1'b0, '0);
if (status[5] !== 1'b1) begin
errors = errors + 1;
$display("FAIL a masked source did not set its status bit");
end
if (irq) begin
errors = errors + 1;
$display("FAIL a masked source drove the interrupt line");
end
if (int'(n_masked_set) != base + 1) begin
errors = errors + 1;
$display("FAIL a masked set was not counted");
end
// ...and unmasking raises the line with no new source pulse at all.
set_mask(8'b0010_0000);
if (!irq) begin
errors = errors + 1;
$display("FAIL unmasking did not raise the line for a pending event");
end
// ================= PHASE 5 -- the line is a LEVEL ======================
//
// Two sources. Handle one. The line must stay up, because the other is
// still pending -- and an edge-triggered aggregate would never produce a
// second edge, so the second source would wait for ever.
reset_all;
set_mask('1);
drive(8'b0000_0011, 1'b0, '0, 1'b0, '0); // bits 0 and 1
base = int'(n_irq_rise);
drive('0, 1'b0, '0, 1'b1, 8'b0000_0001); // handle bit 0
if (!irq) begin
errors = errors + 1;
$display("FAIL the line dropped with bit 1 still pending");
end
if (int'(n_irq_rise) != base) begin
errors = errors + 1;
$display("FAIL the line produced a spurious edge");
end
// ---- THE comparison this chapter exists for. ----
//
// The level is still asserted and there is NO new edge. An interrupt
// controller configured for edges has now been told about bit 0 and will
// never be told about bit 1 -- which is still sitting there, pending,
// for ever.
if (irq_edge) begin
errors = errors + 1;
$display("FAIL an edge was produced while the line never dropped");
end
drive('0, 1'b0, '0, 1'b1, 8'b0000_0010); // handle bit 1
if (irq) begin
errors = errors + 1;
$display("FAIL the line stayed up with nothing pending");
end
// ================= PHASE 5b -- two claims, over EVERY bit =============
//
// Both of these were driven once, for one bit, and the mutations that
// break them died on SIX checks each.
//
// CLAIM ONE: a mask write must not disturb the status. Otherwise
// everything that happened while the mask was closed is erased at the
// moment the driver opens it -- which is exactly when it wanted to know.
for (b = 0; b < N; b = b + 1)
for (c = 0; c < 2; c = c + 1) begin
reset_all;
set_mask((c == 0) ? 8'h00 : 8'(8'hFF ^ (8'd1 << b)));
drive(8'(8'd1 << b), 1'b0, '0, 1'b0, '0);
if (!status[b]) begin
errors = errors + 1;
$display("FAIL bit %0d did not record while masked (mask case %0d)",
b, c);
end
// now write the mask -- to anything at all -- and the status must be
// exactly what it was
set_mask('1);
if (!status[b]) begin
errors = errors + 1;
$display("FAIL bit %0d was erased by a mask write (case %0d)", b, c);
end
if (!irq) begin
errors = errors + 1;
$display("FAIL bit %0d: unmasking did not raise the line", b);
end
set_mask('0);
if (!status[b]) begin
errors = errors + 1;
$display("FAIL bit %0d was erased by re-masking", b);
end
n_mask_cases = n_mask_cases + 1;
end
if (n_mask_cases != 2 * N) begin
errors = errors + 1;
$display("FAIL mask scenarios %0d, expected %0d", n_mask_cases, 2 * N);
end
// CLAIM TWO: the bits are INDEPENDENT on a clear. A driver that
// acknowledges the interrupt it handled must not destroy one it has not
// even read yet -- and with all bits set, clearing exactly one must leave
// exactly seven.
for (b = 0; b < N; b = b + 1) begin
reset_all;
set_mask('1);
drive('1, 1'b0, '0, 1'b0, '0); // every bit set
if (status !== '1) begin
errors = errors + 1;
$display("FAIL not all bits set before the clear: %b", status);
end
drive('0, 1'b0, '0, 1'b1, 8'(8'd1 << b));
// exactly the one written, and no other
if (status !== 8'(8'hFF ^ (8'd1 << b))) begin
errors = errors + 1;
$display("FAIL clearing bit %0d left %b, expected %b",
b, status, 8'(8'hFF ^ (8'd1 << b)));
end
if (!irq) begin
errors = errors + 1;
$display("FAIL the line dropped with seven bits still pending");
end
n_indep_cases = n_indep_cases + 1;
end
if (n_indep_cases != N) begin
errors = errors + 1;
$display("FAIL independence scenarios %0d, expected %0d",
n_indep_cases, N);
end
// ================= PHASE 6 -- a write-back that clears nothing ========
//
// A driver that writes back the whole word it read clears bits that were
// never set, constantly. It is not an error; it is counted separately so
// that a register which only ever produces no-ops is visible.
reset_all;
set_mask('1);
base = int'(n_write_noop);
drive('0, 1'b0, '0, 1'b1, 8'hFF);
if (int'(n_write_noop) != base + N) begin
errors = errors + 1;
$display("FAIL %0d no-op clears counted, expected %0d",
int'(n_write_noop) - base, N);
end
if (n_clear != 0) begin
errors = errors + 1;
$display("FAIL a no-op was counted as a real clear");
end
// The random phase is switchable, because a mutation score is only
// interesting once it is DECOMPOSED. The directed phases already reach
// every situation the exhaustiveness proof requires, so nothing in that
// claim depends on it.
`ifndef DIRECTED_ONLY
// ================= PHASE 7 -- random =================================
//
// The clear pattern is deliberately correlated with the source pattern:
// drawing them independently makes the same-cycle race a 1-in-256 event
// per bit, and the race is the whole chapter.
reset_all;
set_mask('1);
for (i = 0; i < 30000; i = i + 1) begin
w = $unsigned($random) % 100;
vs = 8'($unsigned($random) % 256);
if (w < 20) vc = vs; // maximise the race
else if (w < 60) vc = 8'($unsigned($random) % 256);
else vc = '0;
if (w < 6) set_mask(8'($unsigned($random) % 256));
else if (w < 92) drive(vs, 1'b0, '0, (w >= 30), vc);
else idle;
end
`endif
// ================= the exhaustiveness proof ==========================
n_reach = 0;
for (k = 0; k < 128; k = k + 1) n_reach = n_reach + reach[k];
if (n_reach != 128) begin
errors = errors + 1;
$display("FAIL bit x situation reach %0d/128", n_reach);
for (k = 0; k < 128; k = k + 1)
if (!reach[k])
$display(" unreached bit=%0d situation=%0d", k / 16, k % 16);
end
$display("steps=%0d checks=%0d reach=%0d/128 errors=%0d",
steps, checks, n_reach, errors);
$display("set=%0d clear=%0d race=%0d masked_set=%0d irq_rise=%0d noop=%0d",
n_set, n_clear, n_race, n_masked_set, n_irq_rise, n_write_noop);
$display("%0s: %0d errors in %0d checks",
(errors == 0) ? "PASS" : "FAIL", errors, checks);
$finish;
end
endmoduleVHDL-2008 testbench
-- Testbench for usb_intr_agg (VHDL-2008).
--
-- The oracle is a shadow model held in process variables and written from the
-- chapter's rules rather than from the RTL, re-derived every cycle and
-- compared against every output.
--
-- THE EXHAUSTIVE CLAIM IS PER BIT, AND THAT IS THE RIGHT GRANULARITY
--
-- The next state of one status bit is a function of exactly four things:
--
-- its current value, the source pulse, the write-1-to-clear, the mask
--
-- which is sixteen situations, and the race this chapter is about is one of
-- them. Sweeping eight bits jointly would be 16^8; sweeping each bit through
-- all sixteen is 128 and covers every behaviour the design has.
--
-- 8 bits x 16 situations = 128, all required
--
-- Bit independence gets its own phase, because driving the same situation on
-- all eight bits at once cannot distinguish eight independent bits from one
-- bit wired eight ways.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
entity tb_ia_vhdl is
end entity;
architecture sim of tb_ia_vhdl is
constant N : integer := 8;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal src : std_logic_vector(N-1 downto 0) := (others => '0');
signal mask_wr : std_logic := '0';
signal mask_wdata : std_logic_vector(N-1 downto 0) := (others => '0');
signal stat_wr : std_logic := '0';
signal stat_wdata : std_logic_vector(N-1 downto 0) := (others => '0');
signal eot : std_logic := '0';
signal irq_s, irq_edge_s : std_logic;
signal status_s : std_logic_vector(N-1 downto 0);
signal mask_s : std_logic_vector(N-1 downto 0);
signal pending_s : std_logic_vector(N-1 downto 0);
signal n_set_s, n_clr_s, n_race_s : unsigned(31 downto 0);
signal n_msk_s, n_rise_s, n_noop_s : unsigned(31 downto 0);
signal done : boolean := false;
type int_array is array (natural range <>) of integer;
begin
clk <= not clk after 5 ns when not done else '0';
dut : entity work.usb_intr_agg
generic map (N => N)
port map (
clk => clk, rst_n => rst_n,
src => src, mask_wr => mask_wr, mask_wdata => mask_wdata,
stat_wr => stat_wr, stat_wdata => stat_wdata, eot => eot,
irq => irq_s, irq_edge => irq_edge_s, status => status_s,
mask => mask_s, pending => pending_s,
n_set => n_set_s, n_clear => n_clr_s, n_race => n_race_s,
n_masked_set => n_msk_s, n_irq_rise => n_rise_s,
n_write_noop => n_noop_s
);
stim : process
variable m_stat, m_mask : std_logic_vector(N-1 downto 0)
:= (others => '0');
variable m_mask_pre : std_logic_vector(N-1 downto 0) := (others => '0');
variable c_set, c_clr, c_race : integer := 0;
variable c_msk, c_rise, c_noop : integer := 0;
variable errors, checks, steps : integer := 0;
variable reach : int_array(0 to 127) := (others => 0);
variable n_reach : integer := 0;
variable base, w_v, ii : integer := 0;
variable n_mask_cases, n_indep_cases : integer := 0;
variable m_edge : integer := 0;
variable vs, vm, vc : std_logic_vector(N-1 downto 0);
variable ln : line;
-- A deterministic LFSR, so a rerun reproduces exactly the same traffic.
variable lfsr : unsigned(31 downto 0) := x"1ACE0FF0";
impure function rnd_nat return integer is
begin
lfsr := lfsr(30 downto 0) &
(lfsr(31) xor lfsr(21) xor lfsr(1) xor lfsr(0));
return to_integer(lfsr(29 downto 0));
end function;
procedure ck (nm : string; got, exp : integer) is
begin
checks := checks + 1;
if got /= exp then
errors := errors + 1;
if errors < 25 then
write(ln, string'("FAIL step=") & integer'image(steps) & " " & nm
& " got=" & integer'image(got)
& " exp=" & integer'image(exp));
writeline(output, ln);
end if;
end if;
end procedure;
procedure fail (nm : string) is
begin
errors := errors + 1;
if errors < 25 then
write(ln, string'("FAIL ") & nm); writeline(output, ln);
end if;
end procedure;
function sl2i (s : std_logic) return integer is
begin
if s = '1' then return 1; else return 0; end if;
end function;
function any_set (v : std_logic_vector) return integer is
begin
for i in v'range loop
if v(i) = '1' then return 1; end if;
end loop;
return 0;
end function;
procedure model_step is
variable set_i, clr_i : std_logic;
begin
if eot = '1' then
null;
else
if mask_wr = '1' then m_mask := mask_wdata; end if;
for i in 0 to N-1 loop
set_i := src(i);
if stat_wr = '1' then clr_i := stat_wdata(i); else clr_i := '0'; end if;
-- SET WINS. Written as a priority, deliberately: the one-liner
-- `(stat or src) and not clr` is the same expression with the
-- opposite answer on the one cycle that matters.
if set_i = '1' then
m_stat(i) := '1';
c_set := c_set + 1;
if m_mask_pre(i) = '0' then c_msk := c_msk + 1; end if;
elsif clr_i = '1' then
if m_stat(i) = '1' then c_clr := c_clr + 1;
else c_noop := c_noop + 1; end if;
m_stat(i) := '0';
end if;
if set_i = '1' and clr_i = '1' then c_race := c_race + 1; end if;
end loop;
end if;
end procedure;
procedure check_out is
begin
ck("status", to_integer(unsigned(status_s)),
to_integer(unsigned(m_stat)));
ck("mask", to_integer(unsigned(mask_s)),
to_integer(unsigned(m_mask)));
ck("pending", to_integer(unsigned(pending_s)),
to_integer(unsigned(m_stat and m_mask)));
ck("irq", sl2i(irq_s), any_set(m_stat and m_mask));
ck("irq_edge", sl2i(irq_edge_s), m_edge);
ck("n_set", to_integer(n_set_s), c_set);
ck("n_clear", to_integer(n_clr_s), c_clr);
ck("n_race", to_integer(n_race_s), c_race);
ck("n_masked_set", to_integer(n_msk_s), c_msk);
ck("n_irq_rise", to_integer(n_rise_s), c_rise);
ck("n_write_noop", to_integer(n_noop_s), c_noop);
-- ---- the structural invariant ----
--
-- The line is exactly the OR of the pending bits. If these ever
-- disagree the driver is being interrupted by something it cannot find
-- in the status register.
ck("irq = any pending", sl2i(irq_s), any_set(pending_s));
ck("pending = status and mask", to_integer(unsigned(pending_s)),
to_integer(unsigned(status_s and mask_s)));
end procedure;
procedure step is
variable irq_before : integer;
begin
m_mask_pre := m_mask;
irq_before := any_set(m_stat and m_mask);
model_step;
if irq_before = 0 and any_set(m_stat and m_mask) = 1 then
m_edge := 1;
c_rise := c_rise + 1;
else
m_edge := 0;
end if;
wait until rising_edge(clk);
wait for 1 ns;
steps := steps + 1;
check_out;
end procedure;
procedure drive (s : std_logic_vector(N-1 downto 0); mw : std_logic;
md : std_logic_vector(N-1 downto 0); sw : std_logic;
sd : std_logic_vector(N-1 downto 0)) is
begin
src <= s; mask_wr <= mw; mask_wdata <= md;
stat_wr <= sw; stat_wdata <= sd; eot <= '0';
wait for 0 ns;
step;
src <= (others => '0'); mask_wr <= '0'; stat_wr <= '0';
end procedure;
procedure idle is
begin
src <= (others => '0'); mask_wr <= '0'; stat_wr <= '0'; eot <= '0';
wait for 0 ns;
step;
end procedure;
procedure set_mask (md : std_logic_vector(N-1 downto 0)) is
begin
drive((others => '0'), '1', md, '0', (others => '0'));
end procedure;
procedure reset_all is
begin
src <= (others => '0'); mask_wr <= '0'; stat_wr <= '0'; eot <= '0';
rst_n <= '0';
wait until rising_edge(clk);
wait for 1 ns;
rst_n <= '1';
m_stat := (others => '0'); m_mask := (others => '0');
m_mask_pre := (others => '0');
c_set := 0; c_clr := 0; c_race := 0;
c_msk := 0; c_rise := 0; c_noop := 0; m_edge := 0;
wait for 1 ns;
end procedure;
begin
wait until rising_edge(clk);
wait until rising_edge(clk);
wait until rising_edge(clk);
rst_n <= '1';
wait for 1 ns;
-- ================= PHASE 1 -- all 16 situations, on every bit =========
--
-- The status bit is set by PULSING THE SOURCE, never by forcing the
-- register: a state forced into the design is a state the design never
-- proved it can reach.
for c in 0 to 15 loop
reset_all;
if (c mod 2) = 1 then set_mask((others => '1'));
else set_mask((others => '0')); end if;
if ((c / 8) mod 2) = 1 then
drive((others => '1'), '0', (others => '0'), '0', (others => '0'));
end if;
if ((c / 4) mod 2) = 1 then vs := (others => '1');
else vs := (others => '0'); end if;
if ((c / 2) mod 2) = 1 then vc := (others => '1');
else vc := (others => '0'); end if;
if ((c / 2) mod 2) = 1 then
drive(vs, '0', (others => '0'), '1', vc);
else
drive(vs, '0', (others => '0'), '0', vc);
end if;
for b in 0 to N-1 loop reach(b * 16 + c) := 1; end loop;
end loop;
-- ================= PHASE 2 -- bit INDEPENDENCE ========================
--
-- A different situation on every bit at once.
for c in 0 to 15 loop
reset_all;
for b in 0 to N-1 loop
ii := (c + b) mod 16;
if (ii mod 2) = 1 then vm(b) := '1'; else vm(b) := '0'; end if;
end loop;
set_mask(vm);
for b in 0 to N-1 loop
ii := (c + b) mod 16;
if ((ii / 8) mod 2) = 1 then vs(b) := '1'; else vs(b) := '0'; end if;
end loop;
drive(vs, '0', (others => '0'), '0', (others => '0'));
for b in 0 to N-1 loop
ii := (c + b) mod 16;
if ((ii / 4) mod 2) = 1 then vs(b) := '1'; else vs(b) := '0'; end if;
if ((ii / 2) mod 2) = 1 then vc(b) := '1'; else vc(b) := '0'; end if;
end loop;
drive(vs, '0', (others => '0'), '1', vc);
for b in 0 to N-1 loop
ii := (c + b) mod 16;
reach(b * 16 + ii) := 1;
end loop;
end loop;
-- ================= PHASE 3 -- THE RACE ================================
--
-- The hardware sets a bit on the very cycle the driver writes 1 to clear
-- it. The bit must stay SET: the event happened, and nothing else in the
-- system will ever mention it again.
reset_all;
set_mask((others => '1'));
drive("00001000", '0', (others => '0'), '0', (others => '0'));
if status_s(3) /= '1' or irq_s /= '1' then
fail("the source did not set its status bit");
end if;
base := c_race;
drive("00001000", '0', (others => '0'), '1', "00001000");
if c_race /= base + 1 then fail("the race was not counted"); end if;
if status_s(3) /= '1' then
fail("SET LOST THE RACE: the interrupt is gone");
end if;
if irq_s /= '1' then
fail("the line dropped although the event is still pending");
end if;
drive((others => '0'), '0', (others => '0'), '1', "00001000");
if status_s(3) /= '0' then
fail("a plain write-1-to-clear did not clear");
end if;
-- ================= PHASE 4 -- masking does not erase ==================
reset_all;
set_mask((others => '0'));
base := c_msk;
drive("00100000", '0', (others => '0'), '0', (others => '0'));
if status_s(5) /= '1' then
fail("a masked source did not set its status bit");
end if;
if irq_s /= '0' then
fail("a masked source drove the interrupt line");
end if;
if c_msk /= base + 1 then fail("a masked set was not counted"); end if;
set_mask("00100000");
if irq_s /= '1' then
fail("unmasking did not raise the line for a pending event");
end if;
-- ================= PHASE 5 -- the line is a LEVEL ======================
reset_all;
set_mask((others => '1'));
drive("00000011", '0', (others => '0'), '0', (others => '0'));
base := c_rise;
drive((others => '0'), '0', (others => '0'), '1', "00000001");
if irq_s /= '1' then
fail("the line dropped with bit 1 still pending");
end if;
if c_rise /= base then fail("the line produced a spurious edge"); end if;
-- ---- THE comparison this chapter exists for. ----
--
-- The level is still asserted and there is NO new edge. An interrupt
-- controller configured for edges has now been told about bit 0 and will
-- never be told about bit 1 -- which is still sitting there, pending,
-- for ever.
if irq_edge_s /= '0' then
fail("an edge was produced while the line never dropped");
end if;
drive((others => '0'), '0', (others => '0'), '1', "00000010");
if irq_s /= '0' then fail("the line stayed up with nothing pending"); end if;
-- ================= PHASE 5b -- two claims, over EVERY bit =============
--
-- Both of these were driven once, for one bit, and the mutations that
-- break them died on SIX checks each.
--
-- CLAIM ONE: a mask write must not disturb the status. Otherwise
-- everything that happened while the mask was closed is erased at the
-- moment the driver opens it -- which is exactly when it wanted to know.
n_mask_cases := 0;
for b in 0 to N-1 loop
for c in 0 to 1 loop
reset_all;
if c = 0 then
set_mask((others => '0'));
else
vm := (others => '1'); vm(b) := '0';
set_mask(vm);
end if;
vs := (others => '0'); vs(b) := '1';
drive(vs, '0', (others => '0'), '0', (others => '0'));
if status_s(b) /= '1' then
fail("a masked source did not record");
end if;
set_mask((others => '1'));
if status_s(b) /= '1' then
fail("a mask write erased the status");
end if;
if irq_s /= '1' then
fail("unmasking did not raise the line");
end if;
set_mask((others => '0'));
if status_s(b) /= '1' then
fail("re-masking erased the status");
end if;
n_mask_cases := n_mask_cases + 1;
end loop;
end loop;
if n_mask_cases /= 2 * N then
errors := errors + 1;
write(ln, string'("FAIL mask scenarios ") & integer'image(n_mask_cases));
writeline(output, ln);
end if;
-- CLAIM TWO: the bits are INDEPENDENT on a clear. A driver that
-- acknowledges the interrupt it handled must not destroy one it has not
-- even read yet.
n_indep_cases := 0;
for b in 0 to N-1 loop
reset_all;
set_mask((others => '1'));
drive((others => '1'), '0', (others => '0'), '0', (others => '0'));
if status_s /= (status_s'range => '1') then
fail("not all bits set before the clear");
end if;
vc := (others => '0'); vc(b) := '1';
drive((others => '0'), '0', (others => '0'), '1', vc);
vm := (others => '1'); vm(b) := '0';
if status_s /= vm then
fail("clearing one bit disturbed another");
end if;
if irq_s /= '1' then
fail("the line dropped with seven bits still pending");
end if;
n_indep_cases := n_indep_cases + 1;
end loop;
if n_indep_cases /= N then
errors := errors + 1;
write(ln, string'("FAIL independence scenarios ")
& integer'image(n_indep_cases));
writeline(output, ln);
end if;
-- ================= PHASE 6 -- a write-back that clears nothing ========
reset_all;
set_mask((others => '1'));
base := c_noop;
drive((others => '0'), '0', (others => '0'), '1', (others => '1'));
if c_noop /= base + N then
fail("the wrong number of no-op clears was counted");
end if;
if c_clr /= 0 then fail("a no-op was counted as a real clear"); end if;
-- ================= PHASE 7 -- random =================================
--
-- The clear pattern is deliberately correlated with the source pattern:
-- drawing them independently makes the same-cycle race a 1-in-256 event
-- per bit, and the race is the whole chapter.
reset_all;
set_mask((others => '1'));
for i in 0 to 29999 loop
w_v := rnd_nat mod 100;
vs := std_logic_vector(to_unsigned(rnd_nat mod 256, N));
if w_v < 20 then vc := vs;
elsif w_v < 60 then vc := std_logic_vector(to_unsigned(rnd_nat mod 256, N));
else vc := (others => '0'); end if;
if w_v < 6 then
set_mask(std_logic_vector(to_unsigned(rnd_nat mod 256, N)));
elsif w_v < 92 then
if w_v >= 30 then drive(vs, '0', (others => '0'), '1', vc);
else drive(vs, '0', (others => '0'), '0', vc); end if;
else
idle;
end if;
end loop;
-- ================= the exhaustiveness proof ==========================
n_reach := 0;
for k in 0 to 127 loop n_reach := n_reach + reach(k); end loop;
if n_reach /= 128 then
errors := errors + 1;
write(ln, string'("FAIL bit x situation reach ")
& integer'image(n_reach) & "/128");
writeline(output, ln);
end if;
write(ln, string'("steps=") & integer'image(steps)
& " checks=" & integer'image(checks)
& " reach=" & integer'image(n_reach) & "/128"
& " errors=" & integer'image(errors));
writeline(output, ln);
write(ln, string'("set=") & integer'image(to_integer(n_set_s))
& " clear=" & integer'image(to_integer(n_clr_s))
& " race=" & integer'image(to_integer(n_race_s))
& " masked_set=" & integer'image(to_integer(n_msk_s))
& " irq_rise=" & integer'image(to_integer(n_rise_s))
& " noop=" & integer'image(to_integer(n_noop_s)));
writeline(output, ln);
if errors = 0 then
write(ln, string'("PASS: 0 errors in ") & integer'image(checks)
& " checks");
else
write(ln, string'("FAIL: ") & integer'image(errors) & " errors in " &
integer'image(checks) & " checks");
end if;
writeline(output, ln);
done <= true;
wait;
end process;
end architecture;11. Exhaustive Verification
| Measure | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| (bit × situation) reached | 128 / 128 | 128 / 128 | 128 / 128 |
| set/clear race scenarios swept | 16 / 16 | 16 / 16 | 16 / 16 |
| mask-preservation scenarios swept | 16 / 16 | 16 / 16 | 16 / 16 |
| bit-independence scenarios swept | 8 / 8 | 8 / 8 | 8 / 8 |
| Steps | 30190 | 30190 | 30190 |
| Checks executed | 392470 | 392470 | 392470 |
| sets observed | 103602 | 103602 | 102908 |
| clears that cleared something | 15540 | 15540 | 15122 |
| set/clear races | 17920 | 17920 | 18460 |
| sets arriving masked | 51066 | 51066 | 53907 |
| write-1 no-ops | 2633 | 2633 | 2573 |
| Result | PASS | PASS | PASS |
12. Mutation Testing
| # | Mutation | Verilog | SysVer | VHDL |
|---|---|---|---|---|
| Y1 | clear wins over set | 182013 | 182013 | 180605 |
| Y2 | a masked source's event is discarded | 123011 | 123011 | 124000 |
| Y5 | writing the mask register clears status | 104163 | 104163 | 105449 |
| Y7 | clearing bit i also clears bit i+1 | 98795 | 98795 | 105862 |
| Y6 | a write-1 to an already-clear bit counts as a real clear | 60021 | 60021 | 60022 |
| Y4 | pending ignores the mask | 59468 | 59468 | 60042 |
| Y3 | irq is driven from the edge pulse instead of the level | 58848 | 58848 | 58880 |
| — | unmutated baseline | 0 | 0 | 0 |
All seven die in all three languages, and Y1 — the one-line priority inversion — scores highest of any mutation in this module.
Directed against random
| # | All phases | Directed only | Random |
|---|---|---|---|
| Y1 | 182013 | 122 | 181891 |
| Y2 | 123011 | 257 | 122754 |
| Y3 | 58848 | 64 | 58784 |
| Y4 | 59468 | 187 | 59281 |
| Y5 | 104163 | 166 | 103997 |
| Y6 | 60021 | 35 | 59986 |
| Y7 | 98795 | 27 | 98768 |
This is the exact opposite shape to the previous chapter, and the contrast is the point.
In chapter 26.3 the directed phases carried roughly 40% of every score. Here they carry under a fifth of one percent. That is not a weakness in the directed phases — every one of the seven is still killed by directed stimulus alone, which is the only claim the column has to support. It is a statement about the design: an interrupt aggregator has eight bits and a handful of situations, and random stimulus walks into all of them constantly. A burst splitter has a 4 KB × 256-beat space that random stimulus visits sparsely.
13. The Mutation That Was Equivalent
Y3 originally scored zero in all three languages, and the reason is worth more than the mutation.
The design drove irq from the level |(stat_r & mask_r). The mutation drove it from irq_r, a registered copy — and irq_r was registered from exactly that expression. The two are the same signal one cycle apart, and a checker that sampled after the edge could not tell them apart. Y3 was not a surviving mutation. It was not a mutation at all.
Score 0 has three causes and they need different fixes:
the check is missing -> write the check
the mutation is unreachable -> change the parameters
the mutation is EQUIVALENT -> the mutation is wrong
Only the first is a testbench defect.The fix was to make the distinction real. The design gained an irq_edge output — a genuine one-cycle pulse on the rising edge of the aggregate — and Y3 was reformulated to drive the interrupt line from that instead of from the level. Now the two differ in a way that matters enormously:
14. Debugging Walkthrough: The Device That Stops After Three Days
The report. A USB device works for days and then stops. No error is logged. The driver is idle. Re-plugging fixes it. It is dramatically more common under load, and one customer sees it hourly.
Step 1 — is the hardware still running? Read the status register directly. Bits are set. The hardware has events waiting and nothing is collecting them.
Step 2 — is the interrupt line asserted? Read pending. Non-zero. Read the GIC's pending state for that line: also pending. The interrupt is being requested and not taken.
Step 3 — so the GIC is masked? No. But the line is level-sensitive at the controller and configured as edge-triggered at the GIC. The controller is holding the line high forever; the GIC is waiting for an edge that will never come.
Step 4 — why did it ever work? Because the line goes low between events when the driver keeps up. Under load it does not go low, so there is no next edge.
Step 5 — but that is not the whole bug. Fixing the GIC configuration makes it far rarer and does not eliminate it. Instrument the status register: a bit that hardware set on the same cycle the driver wrote 1 to clear it is gone. The event happened, was acknowledged before it was seen, and is not recoverable.
15. UVM: Checking an Interrupt Contract
// An interrupt scoreboard usually asks "was the line raised". This one asks
// the two questions that actually fail in the field: was an event EVER lost,
// and is the line's SHAPE compatible with how the GIC is programmed.
class irq_txn extends uvm_sequence_item;
`uvm_object_utils(irq_txn)
rand bit [7:0] src; // sources firing this cycle (hardware)
rand bit stat_wr; // the driver is writing the status register
rand bit [7:0] stat_wdata; // write-1-to-clear data
rand bit [7:0] mask_wdata;
rand bit mask_wr;
function new(string name = "irq_txn"); super.new(name); endfunction
endclass
class irq_scoreboard extends uvm_scoreboard;
`uvm_component_utils(irq_scoreboard)
uvm_analysis_imp #(irq_txn, irq_scoreboard) ap;
bit [7:0] exp_stat, exp_mask;
int unsigned n_race, n_masked_set, n_lost;
// The interrupt line's history, for the edge/level check in check_phase.
bit prev_irq;
int unsigned n_rise, n_high_cycles, n_cont_rise;
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
function void write(irq_txn t);
bit [7:0] set_i = t.src;
bit [7:0] clr_i = t.stat_wr ? t.stat_wdata : 8'h00;
bit irq;
if (t.mask_wr) exp_mask = t.mask_wdata;
for (int i = 0; i < 8; i++) begin
// ---- THE CONTRACT: a set on the same cycle as a clear SURVIVES. ----
//
// Not "is usually kept", not "is kept unless the driver is fast".
// The hardware event is the thing that really happened; the clear is
// an acknowledgement of a DIFFERENT, earlier event that the driver
// read. Letting the acknowledgement destroy the new one discards an
// event nobody has ever seen.
if (set_i[i] && clr_i[i]) begin
n_race++;
exp_stat[i] = 1'b1; // SET WINS
end
else if (set_i[i]) begin
// A masked source still sets its bit. Masking decides who is TOLD,
// never what HAPPENED -- otherwise unmasking loses every event that
// arrived while the mask was up.
if (!exp_mask[i]) n_masked_set++;
exp_stat[i] = 1'b1;
end
else if (clr_i[i]) exp_stat[i] = 1'b0;
end
irq = |(exp_stat & exp_mask);
if (irq && !prev_irq) n_rise++;
if (irq) begin
n_high_cycles++;
// A rise that happens while the line is ALREADY high is impossible for
// a level, and is exactly the event an edge-triggered configuration
// would need and never get. Counting it makes the edge/level argument
// a measurement instead of a discussion.
if (prev_irq && (exp_stat & exp_mask) != 8'h00) n_cont_rise++;
end
prev_irq = irq;
endfunction
function void check_phase(uvm_phase phase);
super.check_phase(phase);
// A run in which the race never arose proves nothing about the race.
if (n_race == 0)
`uvm_error("IRQ/COV",
"no set/clear race occurred in this run: the central property of this design was never exercised")
// Likewise for masking: if every source fired unmasked, the claim that a
// masked event is preserved was never put to the test.
if (n_masked_set == 0)
`uvm_error("IRQ/COV",
"no source ever fired while masked: mask preservation was never exercised")
`uvm_info("IRQ",
$sformatf("%0d races | %0d masked sets | %0d rises over %0d high cycles",
n_race, n_masked_set, n_rise, n_high_cycles), UVM_LOW)
// The edge/level finding, stated as a number rather than as advice.
if (n_high_cycles > n_rise)
`uvm_info("IRQ/LEVEL",
$sformatf("the line stayed high for %0d cycles across %0d rises: an edge-triggered controller would have taken %0d interrupts and missed the rest",
n_high_cycles, n_rise, n_rise), UVM_LOW)
endfunction
endclass16. Common Misconceptions
"The driver clears the bit, so clearing should win." The clear acknowledges an event the driver read. A set on the same cycle is a different, newer event. Letting the clear win discards something nobody has seen.
"Masking should stop the bit being set." Masking decides who is told. A masked source that fails to set its bit is an event lost the moment the driver unmasks.
"An edge is cheaper than a level." An edge is lost if the second source fires while the first is pending. A level is re-evaluated every cycle and cannot be lost.
"Read-to-clear is simpler than write-1-to-clear." Read-to-clear destroys every bit the driver did not intend to handle, and two drivers reading the same register destroy each other's events.
"The GIC configuration is a software detail." The GIC must be edge- or level-configured to match what the controller drives. A mismatch is a hang, and it is a hang that appears only under load.
"pending and status are the same register." status is what happened; pending is what the driver is being told about. Confusing them makes masking invisible.
"A bit that clears cleanly is independent." Y7 clears bit i+1 as well and dies only 27 times in directed stimulus — it looks clean until you check exactly how many bits remain.
"0 errors means the race is handled." Not unless the report also says how many races occurred.
17. Exercises
1. Write the aggregator as stat_n = (stat_r | src) & ~clr; and give the one cycle on which it differs from the published design. Explain why that cycle is the only one that matters.
2. Y3 originally scored 0 because it was an equivalent mutation. Give a general procedure for distinguishing an equivalent mutation from a missing check, and apply it to a mutation of your own.
3. The line is level-sensitive and the GIC is edge-configured. Derive the exact load condition under which the failure rate goes from "once in three days" to "hourly".
4. Y6 corrupts only a counter — a write-1 to an already-clear bit is recorded as a real clear. Argue for why an observability defect deserves a mutation at all, and say what it would cost in the field.
5. Extend the design so a source can be edge- or level-configured per bit. Which of the seven mutations change meaning, and what new property is needed?
6. Two drivers share the controller and each writes back the whole word it read. Show that write-1-to-clear is safe here and read-to-clear is not.
7. The directed phases contribute under 0.2% of every score in this chapter and roughly 40% in chapter 26.3. Explain the difference from the shape of the two state spaces, and say which design you would trust a random-only suite on.
18. Summary
| Idea | Why it matters |
|---|---|
| Set wins over clear, always | the clear acknowledges an older, already-seen event |
| Write it as an explicit priority | (stat | src) & ~clr is the same expression with the wrong answer |
| A masked source still sets its bit | or unmasking loses everything that arrived meanwhile |
status ≠ pending | what happened, versus what the driver is told about |
| Drive the line as a level | an edge is lost when a second source fires while pending |
| GIC edge/level must match the controller | a mismatch hangs, and only under load |
| Write-1-to-clear, not read-to-clear | read-to-clear destroys bits the driver did not handle |
| Bits must be independent | Y7 clears a neighbour and looks clean for 27 checks |
| Count the situation, not only the outcome | 0 errors means nothing without 17920 races |
Coverage checks belong in check_phase | as errors — a run that never raced proves nothing |
| Score 0 can mean equivalent, not uncaught | Y3 was not a mutation at all until the design gained a real edge output |
| 128 bit × situation states, 7 mutations | all killed by directed stimulus alone, in 3 languages |
Tooling
| Step | Command |
|---|---|
| Verilog-2005 | iverilog -g2005 -o ia_v.out ia_v.v ia_v_tb.v && ./ia_v.out |
| SystemVerilog | iverilog -g2012 -o ia_sv.out ia_sv.sv ia_sv_tb.sv && ./ia_sv.out |
| VHDL-2008 analyse | nvc --std=2008 -a ia_vhdl.vhd ia_vhdl_tb.vhd |
| VHDL-2008 elaborate | nvc --std=2008 -e tb_ia_vhdl |
| VHDL-2008 run | nvc --std=2008 -r tb_ia_vhdl |
| One mutation | iverilog -g2005 -DMUT_Y1 -o mm ia_v_mut.v ia_v_tb.v && ./mm |
| Directed only | iverilog -g2005 -DDIRECTED_ONLY -o mm ia_v_mut.v ia_v_tb.v && ./mm |
All three implementations pass with 0 errors: 128 bit × situation states reached, 17920 set/clear races and 51066 masked sets exercised, every property checked on the registered state every cycle, and every one of the seven mutations killed by directed stimulus alone.
Chapter 26.5 — Firmware Interaction is about the register file this chapter's status register lives in, and about the gap between what a register interface looks like to a driver and what it actually is. Its central problem is the one you now have twice over: a register written by hardware and by software at the same instant, and a read-to-clear field that loses the bits it returns if the read and the clear are not the same event.
Continue learning
Related tutorials
- Related topic
Firmware Interaction
A register interface looks like memory and is not — reading can change it, writing 1 can clear it, one register can apply another, and none of that is anything a compiler knows.
- Related topic
USB Controllers on SoC
DWC2, MUSB and xHCI differ in a hundred mechanical ways that do not matter and one architectural way that does — whether endpoints own their packet buffers or share a pool.
- Related topic
DMA Integration
A descriptor has a byte count and the wire has packets, and the rule that converts one to the other is not ceil(length / packet size) — the version that is hangs on exactly the buffer sizes everybody uses.
- Related topic
AXI Interfaces to USB
AXI forbids a burst from crossing a 4 KB address boundary, and the reason this bug reaches production is that many interconnects quietly split the burst for you — so the engine that breaks the rule works on the board you develop on and corrupts memory on the board you ship.
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.
