Skip to content
VLSI Mentor

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   cycle N:   hardware SETS bit 3
              AND the driver writes 1 to CLEAR 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, attributed to the link.

The same cycle, two orderings

A sequence diagram between the hardware, the status register and the driver. The hardware sets bit 3 on the same cycle the driver writes 1 to clear bit 3. In the incorrect ordering the clear is applied after the set and the bit ends up zero, so the interrupt line drops and the event is lost. In the correct ordering the set has priority, the bit stays at one, and the line remains asserted.A set and a clear arriving togetherHardwareStatusDriverread: bit 3 is setSET bit 3 (a newevent)write 1 to bit 3(the OLD one)clear wins: bit 3 =0 — the new event isGONEset wins: bit 3 = 1— the line stays up
On the left the clear is applied last and erases a bit that was set in the same cycle; the event is destroyed and the line drops. On the right the set has priority, the bit survives, and the line stays asserted until the driver comes back for it.

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   status   what HAPPENED
   mask     what is allowed to INTERRUPT
   irq      status AND mask, as a LEVEL

Three 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:

OutputWhat it is
statuswhat happened — set by the source, regardless of the mask
maskwhat is allowed to interrupt
pendingstatus & mask
irqa level: the OR of pending
irq_edgewhat an edge-triggered controller would have seen
n_racehow often a set and a clear collided, whether or not it was handled
n_masked_seta source that fired while masked — not a bug, but worth separating
n_write_noopa clear of a bit that was not set

6. Verilog-2005 Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endmodule

7. SystemVerilog Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endmodule

8. VHDL-2008 Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 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 cycles
A ten-cycle waveform. Bit 3 is set by a source pulse and the interrupt line rises. On a later cycle the source pulses bit 3 again at the same time as a write of 1 to clear bit 3; the race counter advances from zero to one, the status bit remains set and the line stays high. On the next cycle the same clear is written with no source pulse and the bit clears, dropping the line.set and clear, same cycle, same bitset and clear, same cycle,same bitthe bit SURVIVES: set winsthe bit SURVIVES: set winsa plain clear does clear ita plain clear does clear itclksrc08000008000000000000stat_wrstat_wdata00008080808080808status00080808080800000000irqn_race0000111111n_clear0000001111t0t1t2t3t4t5t6t7t8t9
Bit 3 is already set and the driver writes 1 to clear it — on the very cycle the hardware sets it again. The bit stays set, the race counter advances, and the line never drops. The following cycle, with no concurrent event, the same write does clear it.

Two sources, one handled: the level holds and no edge is produced

usb_intr_agg — level versus edge

10 cycles
A ten-cycle waveform. Two sources fire together setting bits 0 and 1, and the interrupt level rises with a single edge pulse. The driver writes 1 to clear bit 0; the status drops to bit 1 alone, the level stays high, and no new edge pulse is produced. The driver then clears bit 1 and the level falls.two sources, ONE edgetwo sources, ONE edgebit 0 handled; the level holdsbit 0 handled; the levelholdsno second edge, everno second edge, everclksrc03000000000000000000stat_wrstat_wdata000101020202020202status00030302020000000000irqirq_edgen_irq_rise0111111111t0t1t2t3t4t5t6t7t8t9
Bits 0 and 1 both fire. The driver acknowledges bit 0 and the line stays asserted because bit 1 is still pending — and crucially there is no new edge. A controller configured for edges has been told once, and would never be told about bit 1.

10. 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`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
endmodule

SystemVerilog testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`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
endmodule

VHDL-2008 testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 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

MeasureVerilogSystemVerilogVHDL
(bit × situation) reached128 / 128128 / 128128 / 128
set/clear race scenarios swept16 / 1616 / 1616 / 16
mask-preservation scenarios swept16 / 1616 / 1616 / 16
bit-independence scenarios swept8 / 88 / 88 / 8
Steps301903019030190
Checks executed392470392470392470
sets observed103602103602102908
clears that cleared something155401554015122
set/clear races179201792018460
sets arriving masked510665106653907
write-1 no-ops263326332573
ResultPASSPASSPASS

12. Mutation Testing

#MutationVerilogSysVerVHDL
Y1clear wins over set182013182013180605
Y2a masked source's event is discarded123011123011124000
Y5writing the mask register clears status104163104163105449
Y7clearing bit i also clears bit i+19879598795105862
Y6a write-1 to an already-clear bit counts as a real clear600216002160022
Y4pending ignores the mask594685946860042
Y3irq is driven from the edge pulse instead of the level588485884858880
—unmutated baseline000

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 phasesDirected onlyRandom
Y1182013122181891
Y2123011257122754
Y3588486458784
Y45946818759281
Y5104163166103997
Y6600213559986
Y7987952798768

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endclass

16. 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

IdeaWhy it matters
Set wins over clear, alwaysthe 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 bitor unmasking loses everything that arrived meanwhile
status ≠ pendingwhat happened, versus what the driver is told about
Drive the line as a levelan edge is lost when a second source fires while pending
GIC edge/level must match the controllera mismatch hangs, and only under load
Write-1-to-clear, not read-to-clearread-to-clear destroys bits the driver did not handle
Bits must be independentY7 clears a neighbour and looks clean for 27 checks
Count the situation, not only the outcome0 errors means nothing without 17920 races
Coverage checks belong in check_phaseas errors — a run that never raced proves nothing
Score 0 can mean equivalent, not uncaughtY3 was not a mutation at all until the design gained a real edge output
128 bit × situation states, 7 mutationsall killed by directed stimulus alone, in 3 languages

Tooling

StepCommand
Verilog-2005iverilog -g2005 -o ia_v.out ia_v.v ia_v_tb.v && ./ia_v.out
SystemVerilogiverilog -g2012 -o ia_sv.out ia_sv.sv ia_sv_tb.sv && ./ia_sv.out
VHDL-2008 analysenvc --std=2008 -a ia_vhdl.vhd ia_vhdl_tb.vhd
VHDL-2008 elaboratenvc --std=2008 -e tb_ia_vhdl
VHDL-2008 runnvc --std=2008 -r tb_ia_vhdl
One mutationiverilog -g2005 -DMUT_Y1 -o mm ia_v_mut.v ia_v_tb.v && ./mm
Directed onlyiverilog -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

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.