Skip to content
VLSI Mentor

UART · Module 13

Interrupts: Sources, Enables and Clear Semantics

Which conditions deserve to be interrupt sources, why a receive-idle timeout is not optional, and the clear-semantics decision a driver lives with for the life of the chip — with a real defect found and fixed.

An interrupt is a promise: when this becomes true, I will tell you. Everything difficult about interrupts comes from the second half — how does it stop being true?

Chapter 13.2 shipped a register map in which the interrupt status was latched and write-one-to-clear, which is the conventional shape and the one most register maps use. This chapter examines it, and finds that for this set of sources it is wrong in two directions at once. The design changed as a result.

1. The Five Sources

BitSourceConditionCleared by
0RX_TRIGreceive queue at or above its triggerdraining the queue
1TX_TRIGtransmit queue at or below its triggerfilling the queue
2RX_TMOdata waiting, and the line has gone idledraining the queue
3ERRany sticky error flag setwriting ERR
4BREAKbreak condition active on the linethe far end releasing it

Read the right-hand column, because it is the whole chapter. Every source has a natural clearing action, and in every case that action is doing the work the interrupt was asking for. None of them is cleared by acknowledging it.

All five are levels. Not one is an event — a thing that happens at an instant and is then over. That observation looks pedantic and it determines the entire design of the status register.

2. Level Sources and Event Sources Want Opposite Things

Level sourceEvent source
Examplequeue above thresholda pulse that lasts one cycle
Still true when the handler runs?yes, unless servicedno — it already happened
Must the hardware remember it?no — the condition is still there to readyes, or it is lost
How it is clearedremove the causeacknowledge it
Correct status registertransparent: raw & enablelatched, write-one-to-clear

A latch exists to remember something that would otherwise be lost. A level source cannot be lost — it is still asserted — so latching it adds nothing and takes something away.

3. What the Latch Actually Cost

Chapter 13.2's map latched all five sources and cleared them with a write:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The conventional shape — and wrong for these sources.
always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) irq_stat_q <= '0;
    else        irq_stat_q <= (irq_stat_q & ~irq_w1c) | (irq_raw & irq_en_q);
end

Measured against it, two distinct failures:

Clearing without servicing never terminates.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 2. driver writes W1C but does NOT drain the FIFO
  pass RAW: cause still present
  pass STATUS re-asserts immediately — the cause was never removed
  pass irq still asserted: this is an INTERRUPT STORM
      cleared 5 times without servicing: re-asserted 5 times

This one is arguably correct — the condition really is still true — and it is a trap regardless, because the write-one-to-clear interface invites a driver author to believe that clearing the flag is what ends the interrupt. It is not. Five acknowledgements produced five immediate re-interrupts.

And the latch reports conditions that are no longer true.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 3. drain below the trigger, then inspect BEFORE clearing
  pass RAW: condition gone once drained
      IRQ_STAT with the cause GONE but not yet cleared = 1
      -> the latch holds a condition that is no longer true
  pass irq still asserted from the stale latch

This one is a defect. The queue was drained, the raw condition went away, and the status register still said "receive queue above threshold". A driver reading it would go looking for data that is not there — and the interrupt line stayed asserted for a condition that had already been serviced.

4. The Fix: Report the Cause

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// TRANSPARENT, not latched — Chapter 13.3. Every source above is a LEVEL
// with its own natural clearing action: drain the queue, fill the queue,
// write ERR, or wait for the line to return to mark. Latching a level and
// clearing it with a W1C write is wrong in both directions: the bit
// re-asserts immediately if the cause was not serviced, and it stays set
// after the cause has gone, so a driver is interrupted for a condition
// that is no longer true. An interrupt is cleared by removing its cause.
logic [IRQ_N-1:0] irq_stat;
assign irq_stat = irq_raw & irq_en_q;
assign irq_o    = |irq_stat;

Three lines, no state. IRQ_STAT becomes read-only: there is nothing to write, because there is nothing being remembered.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 2. write W1C without servicing: does anything change?
  pass STATUS still set — a write cannot clear a level source
      -> the register is transparent: it reports the CAUSE, not a latch
-- 3. drain below the trigger and look WITHOUT writing anything
  pass RAW: condition gone once drained
  pass STATUS clears with NO write — no stale interrupt
  pass irq deasserted by servicing alone

The interface now tells the truth about what it is. A read-only status register cannot mislead a driver into thinking a write is the acknowledgement, and the only way to end an interrupt is to do the thing it was asking for.

ERR keeps its write-one-to-clear, and that is not an inconsistency — it is the same rule applied. The sticky error flags are history, deliberately remembered past the event that set them (Chapter 9.6 §1), so they genuinely need an acknowledgement. The error interrupt is a level derived from them, and clearing it means writing ERR — servicing the cause, exactly like every other source.

A derivation showing how each UART interrupt source is cleared. The receive trigger and receive timeout sources are both conditions on the receive queue, and both are cleared by the driver reading bytes until the queue falls below its threshold. The transmit trigger is a condition on the transmit queue and is cleared by the driver writing more bytes into it. The error source is a level derived from the sticky error flags, and is cleared by writing one to those flags, which is the only write-one-to-clear register in the chain because it is the only place genuinely remembering history. The break source is a condition on the line itself and is cleared only when the far end releases the break, which the local driver cannot do at all. Every path ends at the work the interrupt was requesting rather than at an acknowledgement of the interrupt.SourceConditionDriver actionInterrupt endsRX_TRIG / RX_TMO:queue has dataread DATA untilbelow triggercondition gone — nowrite neededTX_TRIG: queue hasroomwrite DATA untilabove triggerERR: a sticky flagis setwrite ERR — the ONEW1C in the chainBREAK: the line isheld lowonly the FAR END canclear this
Figure 1 — how each source is cleared. Every path ends at the work the interrupt was asking for, never at an acknowledgement of the interrupt itself. The one write-one-to-clear register in the chain is the sticky error history, which is genuinely remembering something.

Latched versus transparent status

7 cycles
A trace of seven events comparing a latched interrupt status register with a transparent one, against the same receive trigger condition. Initially the queue is below its trigger and nothing is asserted. Bytes arrive and the raw condition asserts, and both the latched and transparent status registers assert with it. The driver then writes one to clear without draining the queue: the latched version clears for an instant and immediately re-asserts because the cause is untouched, while the transparent version never moved at all. The driver then drains the queue and the raw condition goes away: the transparent status follows it down immediately, while the latched status remains asserted, reporting a condition that is no longer true, until a further write clears it. The final event shows both quiet only after that extra write.stalestalewrite does not clear the causewrite does not clear thecauselatch is now STALElatch is now STALEeventidlearriveW1CfulldraindrainedW1C #2raw conditionlatchedtransparentirq (latched)t0t1t2t3t4t5t6
Figure 2 — the same receive-trigger condition through a latched status register and a transparent one. Columns are events, not clock cycles. The latched version stays asserted after the queue is drained and needs a write it should not need; the transparent version follows the cause exactly.

5. The Enable Mask

Three registers, and the distinction between them matters to a driver:

RegisterShowsPurpose
IRQ_RAWthe conditions, unmaskedthe truth, always
IRQ_ENwhich ones may interruptpolicy
IRQ_STATRAW & ENwhat to service now

Masking hides the interrupt and never the condition, which is what lets a polling driver work without enabling anything:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 4. the enable mask
  pass masking clears STATUS
  pass but RAW still reports the truth
  pass irq deasserted by masking
  pass unmasking re-asserts it — no event was lost while masked

The last line is the property that makes masking usable as a critical section. A driver can mask a source, do something that would otherwise race the handler, and unmask — and with level sources nothing can be lost in the interval, because the condition is still there to be re-observed. With latched event sources this would not hold, and masking would need to be paired with careful re-checking.

6. The Receive-Idle Timeout

Without it, a design with a trigger level has a hole: bytes below the trigger are never announced.

A message of five bytes arriving at a UART with a trigger of twelve raises nothing. The bytes sit in the queue, correct and complete, and the driver is never told. The link looks dead while working perfectly.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Bytes below the trigger level would otherwise sit in the FIFO forever.
// The counter restarts on every arrival and on every read, and the
// timeout only means anything while the FIFO is NOT empty.
always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        idle_cnt_q   <= '0;
        rx_timeout_q <= 1'b0;
    end else if (rx_empty || rx_activity) begin
        idle_cnt_q   <= '0;
        rx_timeout_q <= 1'b0;
    end else if (idle_cnt_q >= (timeout_bits_q * CLKS_PER_BIT)) begin
        rx_timeout_q <= 1'b1;               // LEVEL: holds until serviced
    end else begin
        idle_cnt_q <= idle_cnt_q + 1'b1;
    end
end

Three details carry it. The counter restarts on arrival as well as on read, so a burst still in progress does not time out mid-message. It is held at zero while the queue is empty, so an idle link never generates a timeout. And the result is a level, held until the queue is drained, so it behaves like every other source in §1.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 5. receive-idle timeout
  pass one byte alone does NOT raise the trigger interrupt
  pass idle timeout fires so the lone byte is not stranded
  pass STATUS also exposes the timeout
  pass the stranded byte reads back correctly
  pass timeout clears once the FIFO is emptied

The default is 40 bit times — four character times at 8N1, matching the 16550 convention. The register makes it tunable because the right value depends on the protocol above: a request-response protocol wants it short so a reply is not delayed, while a streaming protocol wants it long so a burst is not chopped into pieces.

The timeout is measured in bit times and counted in clocks, converted once:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
localparam int unsigned CLKS_PER_BIT = CLK_HZ / BAUD_HZ;

That is Chapter 11.4 §1's rule about derived values: the unit software thinks in is the protocol's, the unit hardware counts in is the clock's, and the conversion belongs in one place next to its inputs.

7. When Write-One-to-Clear Is Right

Nothing above argues against W1C in general. It is the correct mechanism for an event — and this design has one, in the layer below.

The sticky error flags of Chapter 9.6 latch a framing or parity error that lasted one character and is long gone. That is a genuine memory of something that would otherwise be lost, so it needs an acknowledgement, and ERR is write-one-to-clear.

The test to apply to any status bit:

QuestionIf yesIf no
Is the condition still observable when the handler runs?transparent, raw & enlatched, W1C
Would the information be lost without a latch?latched, W1Ctransparent

And set-dominance is only a question for the latched case. Chapter 9.6 §5 made the sticky flags set-dominant so an error arriving in the same cycle as its clear survives; a transparent status register has no such race because it has no state. Removing the latch removed a class of race along with it — which is worth noticing, because the usual instinct on finding a race is to add logic rather than to ask whether the state should exist.

8. The Interrupt Block in Three Languages

Sections 4 and 6 gave the two halves separately: three lines of masking with no state, and a counter that measures idleness. This is both of them as one module, which is how they ship.

The proportion is the point. The masking is three assignments and the timeout is the only flip-flop in the file — and the timeout is not remembering anything either. It is measuring elapsed quiet, and its output is a level that the same servicing action clears.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ===========================================================================
//  uart_irq — Synthesizable SystemVerilog
//
//  Chapter 13.3. Five interrupt sources, and the entire design follows from
//  one observation about them: ALL FIVE ARE LEVELS.
//
//    bit 0  RX_TRIG   receive queue at or above its trigger
//    bit 1  TX_TRIG   transmit queue at or below its trigger
//    bit 2  RX_TMO    data waiting, and the line has gone idle
//    bit 3  ERR       any sticky error flag set
//    bit 4  BREAK     break condition active on the line
//
//  A latch exists to remember something that would otherwise be lost. A
//  level cannot be lost — it is still asserted — so latching one adds
//  nothing and takes something away. Every source here has a natural
//  clearing action, and in every case that action is DOING THE WORK the
//  interrupt was asking for: drain the queue, fill the queue, write ERR, or
//  wait for the far end to release the break.
//
//  So there is no latch, IRQ_STAT is read-only, and an interrupt ends when
//  its cause does. The only state in this module is the idle-timeout
//  counter, which is measuring something rather than remembering it.
// ===========================================================================
module uart_irq #(
    parameter int unsigned CLK_HZ  = 100_000_000,
    parameter int unsigned BAUD_HZ = 115_200,
    parameter int unsigned TMO_W   = 16
) (
    input  logic             clk,
    input  logic             rst_n,

    // ---- the raw conditions, all computed elsewhere ----------------------
    input  logic             rx_trig_i,      // queue >= its trigger
    input  logic             tx_trig_i,      // queue <= its trigger
    input  logic             err_any_i,      // any sticky ERR flag set
    input  logic             break_i,        // break active ON THE LINE NOW

    // ---- inputs to the receive-idle timeout ------------------------------
    input  logic             rx_empty_i,
    input  logic             rx_activity_i,  // a byte arrived, or was read
    input  logic [TMO_W-1:0] timeout_bits_i, // from the TIMEOUT register

    // ---- the enable mask, from IRQ_EN ------------------------------------
    input  logic [4:0]       irq_en_i,

    output logic [4:0]       irq_raw_o,      // before masking  (IRQ_RAW)
    output logic [4:0]       irq_stat_o,     // raw & enable    (IRQ_STAT)
    output logic             irq_o,          // to the interrupt controller
    output logic             rx_timeout_o    // also appears in STATUS bit 7
);
    // The unit software thinks in is the protocol's; the unit hardware counts
    // in is the clock's; the conversion belongs in ONE place next to its
    // inputs. Chapter 11.4 section 1.
    localparam int unsigned CLKS_PER_BIT = CLK_HZ / BAUD_HZ;
    localparam int unsigned CNT_W        = 32;

    // =======================================================================
    //  The receive-idle timeout (Chapter 13.3 section 6)
    //
    //  Without it a design with a trigger level has a hole: bytes BELOW the
    //  trigger are never announced. Five bytes arriving at a UART with a
    //  trigger of twelve raise nothing, sit in the queue correct and
    //  complete, and the driver is never told. The link looks dead while
    //  working perfectly.
    //
    //  Three details carry it:
    //    - the counter restarts on ARRIVAL as well as on read, so a burst
    //      still in progress does not time out mid-message;
    //    - it is held at zero while the queue is EMPTY, so an idle link
    //      never generates a timeout;
    //    - the result is a LEVEL, held until the queue is drained, so it
    //      behaves like every other source above.
    // =======================================================================
    logic [CNT_W-1:0] idle_cnt_q;
    logic             rx_timeout_q;

    wire [CNT_W-1:0] tmo_clks = CNT_W'(timeout_bits_i) * CNT_W'(CLKS_PER_BIT);

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            idle_cnt_q   <= '0;
            rx_timeout_q <= 1'b0;
        end else if (rx_empty_i || rx_activity_i) begin
            idle_cnt_q   <= '0;
            rx_timeout_q <= 1'b0;
        end else if (idle_cnt_q >= tmo_clks) begin
            rx_timeout_q <= 1'b1;               // LEVEL: holds until serviced
        end else begin
            idle_cnt_q <= idle_cnt_q + 1'b1;
        end
    end

    assign rx_timeout_o = rx_timeout_q;

    // =======================================================================
    //  Three registers, and the distinction matters to a driver:
    //    IRQ_RAW  — what is true, regardless of what is enabled
    //    IRQ_STAT — what is true AND enabled; this is what caused irq_o
    //    irq_o    — the single line to the controller
    //
    //  Not one of them is a latch. Chapter 13.3 section 4: three lines, no
    //  state. IRQ_STAT is read-only because there is nothing to write.
    // =======================================================================
    assign irq_raw_o  = {break_i, err_any_i, rx_timeout_q, tx_trig_i, rx_trig_i};
    assign irq_stat_o = irq_raw_o & irq_en_i;
    assign irq_o      = |irq_stat_o;
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  uart_irq_v — Synthesizable Verilog-2001
//
//  Chapter 13.3. Five interrupt sources, and the entire design follows from
//  one observation about them: ALL FIVE ARE LEVELS.
//
//    bit 0  RX_TRIG   receive queue at or above its trigger
//    bit 1  TX_TRIG   transmit queue at or below its trigger
//    bit 2  RX_TMO    data waiting, and the line has gone idle
//    bit 3  ERR       any sticky error flag set
//    bit 4  BREAK     break condition active on the line
//
//  A latch exists to remember something that would otherwise be lost. A
//  level cannot be lost -- it is still asserted -- so latching one adds
//  nothing and takes something away. Every source here has a natural
//  clearing action, and in every case that action is DOING THE WORK the
//  interrupt was asking for.
//
//  So there is no latch, IRQ_STAT is read-only, and an interrupt ends when
//  its cause does. The only state is the idle-timeout counter, which is
//  measuring something rather than remembering it.
//===========================================================================
module uart_irq_v #(
    parameter CLK_HZ  = 100000000,
    parameter BAUD_HZ = 115200,
    parameter TMO_W   = 16
) (
    input  wire             clk,
    input  wire             rst_n,

    input  wire             rx_trig_i,
    input  wire             tx_trig_i,
    input  wire             err_any_i,
    input  wire             break_i,

    input  wire             rx_empty_i,
    input  wire             rx_activity_i,
    input  wire [TMO_W-1:0] timeout_bits_i,

    input  wire [4:0]       irq_en_i,

    output wire [4:0]       irq_raw_o,
    output wire [4:0]       irq_stat_o,
    output wire             irq_o,
    output wire             rx_timeout_o
);
    // The conversion from the unit software thinks in (bit times) to the one
    // hardware counts in (clocks) belongs in ONE place, next to its inputs.
    localparam CLKS_PER_BIT = CLK_HZ / BAUD_HZ;
    localparam CNT_W        = 32;

    //=======================================================================
    //  The receive-idle timeout (Chapter 13.3 section 6)
    //
    //  Without it, bytes BELOW the trigger are never announced: five bytes
    //  arriving at a UART with a trigger of twelve raise nothing and sit in
    //  the queue forever. The link looks dead while working perfectly.
    //
    //  Three details carry it: the counter restarts on ARRIVAL as well as on
    //  read, so a burst in progress does not time out mid-message; it is
    //  held at zero while the queue is EMPTY, so an idle link never times
    //  out; and the result is a LEVEL, held until the queue is drained.
    //=======================================================================
    reg [CNT_W-1:0] idle_cnt_q;
    reg             rx_timeout_q;

    wire [CNT_W-1:0] tmo_clks = timeout_bits_i * CLKS_PER_BIT;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            idle_cnt_q   <= {CNT_W{1'b0}};
            rx_timeout_q <= 1'b0;
        end else if (rx_empty_i || rx_activity_i) begin
            idle_cnt_q   <= {CNT_W{1'b0}};
            rx_timeout_q <= 1'b0;
        end else if (idle_cnt_q >= tmo_clks) begin
            rx_timeout_q <= 1'b1;               // LEVEL: holds until serviced
        end else begin
            idle_cnt_q <= idle_cnt_q + 1'b1;
        end
    end

    assign rx_timeout_o = rx_timeout_q;

    //=======================================================================
    //  Three registers, none of them a latch:
    //    IRQ_RAW  -- what is true, regardless of what is enabled
    //    IRQ_STAT -- what is true AND enabled; this is what caused irq_o
    //    irq_o    -- the single line to the controller
    //=======================================================================
    assign irq_raw_o  = {break_i, err_any_i, rx_timeout_q, tx_trig_i, rx_trig_i};
    assign irq_stat_o = irq_raw_o & irq_en_i;
    assign irq_o      = |irq_stat_o;
endmodule

VHDL gets the neatest statement of the interrupt line, because VHDL-2008 has a unary reduction operator: or (irq_raw_s and irq_en_i) says "any enabled condition is true" in the same number of characters as the idea takes in English. The architecture cannot read its own out ports, so the raw vector is kept internally and driven onto the port — the same ceremony Chapter 12.3 needed.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--===========================================================================
--  uart_irq — Synthesizable VHDL-2008
--
--  Chapter 13.3. Five interrupt sources, and the entire design follows from
--  one observation about them: ALL FIVE ARE LEVELS.
--
--    bit 0  RX_TRIG   receive queue at or above its trigger
--    bit 1  TX_TRIG   transmit queue at or below its trigger
--    bit 2  RX_TMO    data waiting, and the line has gone idle
--    bit 3  ERR       any sticky error flag set
--    bit 4  BREAK     break condition active on the line
--
--  A latch exists to remember something that would otherwise be lost. A
--  level cannot be lost -- it is still asserted -- so latching one adds
--  nothing and takes something away. Every source here has a natural
--  clearing action, and in every case that action is DOING THE WORK the
--  interrupt was asking for.
--
--  So there is no latch, IRQ_STAT is read-only, and an interrupt ends when
--  its cause does. The only state is the idle-timeout counter, which is
--  measuring something rather than remembering it.
--===========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity uart_irq is
    generic (
        CLK_HZ  : positive := 100000000;
        BAUD_HZ : positive := 115200;
        TMO_W   : positive := 16
    );
    port (
        clk   : in std_logic;
        rst_n : in std_logic;

        rx_trig_i : in std_logic;
        tx_trig_i : in std_logic;
        err_any_i : in std_logic;
        break_i   : in std_logic;

        rx_empty_i     : in std_logic;
        rx_activity_i  : in std_logic;
        timeout_bits_i : in std_logic_vector(TMO_W-1 downto 0);

        irq_en_i : in std_logic_vector(4 downto 0);

        irq_raw_o    : out std_logic_vector(4 downto 0);
        irq_stat_o   : out std_logic_vector(4 downto 0);
        irq_o        : out std_logic;
        rx_timeout_o : out std_logic
    );
end entity uart_irq;

architecture rtl of uart_irq is
    -- The conversion from the unit software thinks in (bit times) to the one
    -- hardware counts in (clocks) belongs in ONE place, next to its inputs.
    constant CLKS_PER_BIT : positive := CLK_HZ / BAUD_HZ;
    constant CNT_W        : positive := 32;

    signal idle_cnt_q   : unsigned(CNT_W-1 downto 0);
    signal rx_timeout_q : std_logic;
    signal tmo_clks     : unsigned(CNT_W-1 downto 0);

    -- The architecture cannot read its own out ports, so irq_raw is kept
    -- internally and driven onto the port concurrently.
    signal irq_raw_s : std_logic_vector(4 downto 0);
begin

    tmo_clks <= resize(unsigned(timeout_bits_i) * to_unsigned(CLKS_PER_BIT, 32),
                       CNT_W);

    --=======================================================================
    --  The receive-idle timeout (Chapter 13.3 section 6)
    --
    --  Without it, bytes BELOW the trigger are never announced: five bytes
    --  arriving at a UART with a trigger of twelve raise nothing and sit in
    --  the queue forever. The link looks dead while working perfectly.
    --
    --  Three details carry it: the counter restarts on ARRIVAL as well as on
    --  read, so a burst in progress does not time out mid-message; it is
    --  held at zero while the queue is EMPTY, so an idle link never times
    --  out; and the result is a LEVEL, held until the queue is drained.
    --=======================================================================
    tmo_proc : process (clk, rst_n)
    begin
        if rst_n = '0' then
            idle_cnt_q   <= (others => '0');
            rx_timeout_q <= '0';
        elsif rising_edge(clk) then
            if rx_empty_i = '1' or rx_activity_i = '1' then
                idle_cnt_q   <= (others => '0');
                rx_timeout_q <= '0';
            elsif idle_cnt_q >= tmo_clks then
                rx_timeout_q <= '1';            -- LEVEL: holds until serviced
            else
                idle_cnt_q <= idle_cnt_q + 1;
            end if;
        end if;
    end process tmo_proc;

    rx_timeout_o <= rx_timeout_q;

    --=======================================================================
    --  Three registers, none of them a latch:
    --    IRQ_RAW  -- what is true, regardless of what is enabled
    --    IRQ_STAT -- what is true AND enabled; this is what caused irq_o
    --    irq_o    -- the single line to the controller
    --=======================================================================
    irq_raw_s  <= break_i & err_any_i & rx_timeout_q & tx_trig_i & rx_trig_i;
    irq_raw_o  <= irq_raw_s;
    irq_stat_o <= irq_raw_s and irq_en_i;
    irq_o      <= or (irq_raw_s and irq_en_i);   -- VHDL-2008 unary reduction

end architecture rtl;

9. Testbenches That Prove the Absence of Something

This module's defining property is a negative: there is no latch. That is harder to test than a positive, and testing it badly is easy.

The tempting test is to assert a condition, check the status bit sets, remove the condition, and check it clears. That is a fine test and it does catch a plain latch. It also passes on any number of half-latched designs — one that holds for a few cycles, one that holds only some bits, one that latches only when two sources assert together. Each of those is a real bug and each survives a handful of directed transitions.

So the property is checked as a continuous invariant instead, on every clock of a 400-step pseudo-random walk over all five raw conditions and the five enable bits:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Checked on EVERY clock, from the outputs and the stimulus only. A latch of
// any depth, on any bit, under any combination, breaks this the first time a
// condition is removed.
if (irq_stat !== (irq_raw & irq_en)) stat_bad++;
if (irq      !== (|(irq_raw & irq_en))) irq_bad++;

The randomisation is doing something specific here, and it is worth naming: it generates the removals. Directed stimulus tends to assert conditions, because that is what the interesting cases look like. A random walk spends half its transitions turning things off, which is exactly the half a latch survives.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  tb_uart_irq — self-checking SystemVerilog testbench
//
//  Chapter 13.3's claim is that these five sources need no latch. A
//  testbench can do better than assert that: it can demonstrate the
//  property the latch would break, which is
//
//      THE STATUS REGISTER FOLLOWS ITS CAUSE, ALWAYS, WITH NO WRITE.
//
//  So the suite drives the raw conditions through a long pseudo-random
//  sequence and checks, on EVERY clock, that irq_stat is exactly
//  raw & enable. A latched implementation passes while a condition is
//  asserting and fails the moment one goes away -- which is precisely the
//  stale-interrupt symptom of section 3.
//
//  The timeout is checked by MEASURING it at two different register values
//  rather than by waiting "long enough" and looking.
//
//  Clock and baud are scaled down (1 kHz / 100 baud, so ten clocks per bit)
//  purely so the timeout is a few tens of cycles instead of a few tens of
//  thousands. The arithmetic under test is identical.
//===========================================================================
`timescale 1ns/1ps

module tb_uart_irq;

    localparam CLK_HZ = 1000, BAUD_HZ = 100, TMO_W = 16;
    localparam CLKS_PER_BIT = CLK_HZ / BAUD_HZ;      // 10

    logic clk = 1'b0;
    always #5 clk = ~clk;
    logic rst_n = 1'b0;

    logic rx_trig = 1'b0, tx_trig = 1'b0, err_any = 1'b0, brk = 1'b0;
    logic rx_empty = 1'b1, rx_activity = 1'b0;
    logic [TMO_W-1:0] timeout_bits = 16'd4;            // 4 bit times = 40 clocks
    logic [4:0] irq_en = 5'b11111;

    wire [4:0] irq_raw, irq_stat;
    wire       irq, rx_timeout;

    uart_irq #(.CLK_HZ(CLK_HZ), .BAUD_HZ(BAUD_HZ), .TMO_W(TMO_W)) dut (
        .clk(clk), .rst_n(rst_n),
        .rx_trig_i(rx_trig), .tx_trig_i(tx_trig),
        .err_any_i(err_any), .break_i(brk),
        .rx_empty_i(rx_empty), .rx_activity_i(rx_activity),
        .timeout_bits_i(timeout_bits), .irq_en_i(irq_en),
        .irq_raw_o(irq_raw), .irq_stat_o(irq_stat),
        .irq_o(irq), .rx_timeout_o(rx_timeout));

    // ---- THE whole-run invariant ------------------------------------------
    // Checked on every clock, from the OUTPUTS and the stimulus only.
    int stat_bad = 0, irq_bad = 0, obs = 0;
    always @(posedge clk) if (rst_n) begin
        obs++;
        if (irq_stat !== (irq_raw & irq_en)) stat_bad++;
        if (irq      !== (|(irq_raw & irq_en))) irq_bad++;
        a_transparent: assert (irq_stat === (irq_raw & irq_en))
            else $error("IRQ_STAT is not raw & enable -- something is latched");
        a_irq_line: assert (irq === (|(irq_raw & irq_en)))
            else $error("irq_o is not the OR of the masked status");
    end

    int checks = 0, failures = 0;
    task automatic check(input logic cond, input string name);
        checks++;
        if (cond) $display("  PASS %0s", name);
        else begin failures++; $display("  FAIL %0s", name); end
    endtask

    int lat, i;
    logic [15:0] lfsr = 16'hACE1;

    task automatic settle; repeat (2) @(negedge clk); endtask

    // Measure how many clocks the timeout takes from the last activity.
    task measure_timeout;
        output int n;
        begin
            @(negedge clk) rx_empty = 1'b0; rx_activity = 1'b1;
            @(negedge clk) rx_activity = 1'b0;
            n = 0;
            while (rx_timeout !== 1'b1 && n < 5000) begin
                @(posedge clk); #1; n++;
            end
        end
    endtask

    initial begin
        #2_000_000;
        $display("  FAIL watchdog: simulation did not finish");
        $display("== %0d checks, %0d failures ==", checks+1, failures+1);
        $display("   RESULT: SYSTEMVERILOG IRQ TESTS FAILED (timeout)");
        $finish;
    end

    initial begin
        $display("== uart_irq : self-checking SystemVerilog testbench ==");

        rst_n = 1'b0;
        repeat (4) @(negedge clk);
        check(irq_raw === 5'b0 && irq === 1'b0, "reset: nothing pending, no interrupt");
        check(rx_timeout === 1'b0, "reset: the idle timeout is clear");
        rst_n = 1'b1;
        settle;

        //=== each source reaches its own bit ================================
        @(negedge clk) rx_trig = 1'b1; settle;
        check(irq_raw == 5'b00001, "RX_TRIG is bit 0");
        @(negedge clk) rx_trig = 1'b0; tx_trig = 1'b1; settle;
        check(irq_raw == 5'b00010, "TX_TRIG is bit 1");
        @(negedge clk) tx_trig = 1'b0; err_any = 1'b1; settle;
        check(irq_raw == 5'b01000, "ERR is bit 3");
        @(negedge clk) err_any = 1'b0; brk = 1'b1; settle;
        check(irq_raw == 5'b10000, "BREAK is bit 4");
        @(negedge clk) brk = 1'b0; settle;

        //=== the enable mask ================================================
        @(negedge clk) rx_trig = 1'b1; tx_trig = 1'b1;
        @(negedge clk) irq_en = 5'b00001; settle;
        check(irq_raw  == 5'b00011, "IRQ_RAW shows both conditions regardless of enables");
        check(irq_stat == 5'b00001, "IRQ_STAT shows only the enabled one");
        check(irq === 1'b1,         "and the line is asserted");
        @(negedge clk) irq_en = 5'b00000; settle;
        check(irq_raw == 5'b00011 && irq === 1'b0,
              "masking everything silences the LINE but not the RAW register");
        @(negedge clk) irq_en = 5'b11111; settle;

        //=== no latch: removing the cause clears the status ==================
        check(irq_stat == 5'b00011, "both sources pending before servicing");
        @(negedge clk) rx_trig = 1'b0; tx_trig = 1'b0; settle;
        check(irq_stat == 5'b00000,
              "servicing the cause clears the status -- with NO write anywhere");
        check(irq === 1'b0, "and deasserts the interrupt line");

        //=== the receive-idle timeout ========================================
        timeout_bits = 16'd4;                     // 4 bit times = 40 clocks
        measure_timeout(lat);
        check(lat == 4*CLKS_PER_BIT + 1,
              "timeout fires after timeout_bits x CLKS_PER_BIT clocks of quiet");
        check(irq_raw[2] === 1'b1, "and appears as RX_TMO in bit 2");

        // It is a LEVEL: it holds until the queue is serviced.
        repeat (50) @(negedge clk);
        check(rx_timeout === 1'b1, "the timeout HOLDS -- it is a level, not a pulse");
        @(negedge clk) rx_empty = 1'b1; settle;   // drain the queue
        check(rx_timeout === 1'b0, "draining the queue clears it, with no write");

        //=== it scales with the register =====================================
        timeout_bits = 16'd12;
        measure_timeout(lat);
        check(lat == 12*CLKS_PER_BIT + 1,
              "a different TIMEOUT value produces a proportionally different delay");
        @(negedge clk) rx_empty = 1'b1; settle;

        //=== an idle link never times out =====================================
        timeout_bits = 16'd4;
        @(negedge clk) rx_empty = 1'b1; rx_activity = 1'b0;
        repeat (400) @(negedge clk);
        check(rx_timeout === 1'b0,
              "an EMPTY queue never times out, however long the link is quiet");

        //=== a burst in progress is not chopped ===============================
        // The counter restarts on ARRIVAL as well as on read, so a message
        // still arriving does not time out mid-way.
        @(negedge clk) rx_empty = 1'b0;
        for (i = 0; i < 12; i++) begin
            repeat (30) @(negedge clk);           // less than the 40-clock timeout
            @(negedge clk) rx_activity = 1'b1;
            @(negedge clk) rx_activity = 1'b0;
        end
        check(rx_timeout === 1'b0,
              "twelve arrivals inside the window never trigger a mid-message timeout");
        repeat (60) @(negedge clk);
        check(rx_timeout === 1'b1, "and once the arrivals stop, it does fire");
        @(negedge clk) rx_empty = 1'b1; settle;

        //=== a long pseudo-random soak =======================================
        for (i = 0; i < 400; i++) begin
            @(negedge clk);
            lfsr = {lfsr[14:0], lfsr[15]^lfsr[13]^lfsr[12]^lfsr[10]};
            rx_trig = lfsr[0]; tx_trig = lfsr[1];
            err_any = lfsr[2]; brk     = lfsr[3];
            irq_en  = lfsr[8:4];
        end
        settle;

        //=== whole-run invariants =============================================
        check(obs > 1500, "the invariant observer ran on enough clocks to judge");
        check(stat_bad == 0,
              "IRQ_STAT was ALWAYS exactly raw & enable -- never once latched");
        check(irq_bad == 0, "and irq_o was always the OR of that");

        $display("== %0d checks, %0d failures ==", checks, failures);
        if (failures == 0) $display("   RESULT: ALL SYSTEMVERILOG IRQ TESTS PASSED");
        else               $display("   RESULT: SYSTEMVERILOG IRQ TESTS FAILED");
        $finish;
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  tb_uart_irq_v — self-checking Verilog-2001 testbench
//
//  Chapter 13.3's claim is that these five sources need no latch. A
//  testbench can do better than assert that: it can demonstrate the
//  property the latch would break, which is
//
//      THE STATUS REGISTER FOLLOWS ITS CAUSE, ALWAYS, WITH NO WRITE.
//
//  So the suite drives the raw conditions through a long pseudo-random
//  sequence and checks, on EVERY clock, that irq_stat is exactly
//  raw & enable. A latched implementation passes while a condition is
//  asserting and fails the moment one goes away -- which is precisely the
//  stale-interrupt symptom of section 3.
//
//  The timeout is checked by MEASURING it at two different register values
//  rather than by waiting "long enough" and looking.
//
//  Clock and baud are scaled down (1 kHz / 100 baud, so ten clocks per bit)
//  purely so the timeout is a few tens of cycles instead of a few tens of
//  thousands. The arithmetic under test is identical.
//===========================================================================
`timescale 1ns/1ps

module tb_uart_irq_v;

    localparam CLK_HZ = 1000, BAUD_HZ = 100, TMO_W = 16;
    localparam CLKS_PER_BIT = CLK_HZ / BAUD_HZ;      // 10

    reg clk = 1'b0;
    always #5 clk = ~clk;
    reg rst_n = 1'b0;

    reg rx_trig = 1'b0, tx_trig = 1'b0, err_any = 1'b0, brk = 1'b0;
    reg rx_empty = 1'b1, rx_activity = 1'b0;
    reg [TMO_W-1:0] timeout_bits = 16'd4;            // 4 bit times = 40 clocks
    reg [4:0] irq_en = 5'b11111;

    wire [4:0] irq_raw, irq_stat;
    wire       irq, rx_timeout;

    uart_irq_v #(.CLK_HZ(CLK_HZ), .BAUD_HZ(BAUD_HZ), .TMO_W(TMO_W)) dut (
        .clk(clk), .rst_n(rst_n),
        .rx_trig_i(rx_trig), .tx_trig_i(tx_trig),
        .err_any_i(err_any), .break_i(brk),
        .rx_empty_i(rx_empty), .rx_activity_i(rx_activity),
        .timeout_bits_i(timeout_bits), .irq_en_i(irq_en),
        .irq_raw_o(irq_raw), .irq_stat_o(irq_stat),
        .irq_o(irq), .rx_timeout_o(rx_timeout));

    // ---- THE whole-run invariant ------------------------------------------
    // Checked on every clock, from the OUTPUTS and the stimulus only.
    integer stat_bad = 0, irq_bad = 0, obs = 0;
    always @(posedge clk) if (rst_n) begin
        obs = obs + 1;
        if (irq_stat !== (irq_raw & irq_en)) stat_bad = stat_bad + 1;
        if (irq      !== (|(irq_raw & irq_en))) irq_bad = irq_bad + 1;
    end

    integer checks = 0, failures = 0;
    task check;
        input cond;
        input [8*80-1:0] name;
        begin
            checks = checks + 1;
            if (cond) $display("  PASS %0s", name);
            else begin failures = failures + 1; $display("  FAIL %0s", name); end
        end
    endtask

    integer lat, i;
    reg [15:0] lfsr = 16'hACE1;

    task settle; begin repeat (2) @(negedge clk); end endtask

    // Measure how many clocks the timeout takes from the last activity.
    task measure_timeout;
        output integer n;
        begin
            @(negedge clk) rx_empty = 1'b0; rx_activity = 1'b1;
            @(negedge clk) rx_activity = 1'b0;
            n = 0;
            while (rx_timeout !== 1'b1 && n < 5000) begin
                @(posedge clk); #1; n = n + 1;
            end
        end
    endtask

    initial begin
        #2_000_000;
        $display("  FAIL watchdog: simulation did not finish");
        $display("== %0d checks, %0d failures ==", checks+1, failures+1);
        $display("   RESULT: VERILOG IRQ TESTS FAILED (timeout)");
        $finish;
    end

    initial begin
        $display("== uart_irq_v : self-checking Verilog testbench ==");

        rst_n = 1'b0;
        repeat (4) @(negedge clk);
        check(irq_raw === 5'b0 && irq === 1'b0, "reset: nothing pending, no interrupt");
        check(rx_timeout === 1'b0, "reset: the idle timeout is clear");
        rst_n = 1'b1;
        settle;

        //=== each source reaches its own bit ================================
        @(negedge clk) rx_trig = 1'b1; settle;
        check(irq_raw == 5'b00001, "RX_TRIG is bit 0");
        @(negedge clk) rx_trig = 1'b0; tx_trig = 1'b1; settle;
        check(irq_raw == 5'b00010, "TX_TRIG is bit 1");
        @(negedge clk) tx_trig = 1'b0; err_any = 1'b1; settle;
        check(irq_raw == 5'b01000, "ERR is bit 3");
        @(negedge clk) err_any = 1'b0; brk = 1'b1; settle;
        check(irq_raw == 5'b10000, "BREAK is bit 4");
        @(negedge clk) brk = 1'b0; settle;

        //=== the enable mask ================================================
        @(negedge clk) rx_trig = 1'b1; tx_trig = 1'b1;
        @(negedge clk) irq_en = 5'b00001; settle;
        check(irq_raw  == 5'b00011, "IRQ_RAW shows both conditions regardless of enables");
        check(irq_stat == 5'b00001, "IRQ_STAT shows only the enabled one");
        check(irq === 1'b1,         "and the line is asserted");
        @(negedge clk) irq_en = 5'b00000; settle;
        check(irq_raw == 5'b00011 && irq === 1'b0,
              "masking everything silences the LINE but not the RAW register");
        @(negedge clk) irq_en = 5'b11111; settle;

        //=== no latch: removing the cause clears the status ==================
        check(irq_stat == 5'b00011, "both sources pending before servicing");
        @(negedge clk) rx_trig = 1'b0; tx_trig = 1'b0; settle;
        check(irq_stat == 5'b00000,
              "servicing the cause clears the status -- with NO write anywhere");
        check(irq === 1'b0, "and deasserts the interrupt line");

        //=== the receive-idle timeout ========================================
        timeout_bits = 16'd4;                     // 4 bit times = 40 clocks
        measure_timeout(lat);
        check(lat == 4*CLKS_PER_BIT + 1,
              "timeout fires after timeout_bits x CLKS_PER_BIT clocks of quiet");
        check(irq_raw[2] === 1'b1, "and appears as RX_TMO in bit 2");

        // It is a LEVEL: it holds until the queue is serviced.
        repeat (50) @(negedge clk);
        check(rx_timeout === 1'b1, "the timeout HOLDS -- it is a level, not a pulse");
        @(negedge clk) rx_empty = 1'b1; settle;   // drain the queue
        check(rx_timeout === 1'b0, "draining the queue clears it, with no write");

        //=== it scales with the register =====================================
        timeout_bits = 16'd12;
        measure_timeout(lat);
        check(lat == 12*CLKS_PER_BIT + 1,
              "a different TIMEOUT value produces a proportionally different delay");
        @(negedge clk) rx_empty = 1'b1; settle;

        //=== an idle link never times out =====================================
        timeout_bits = 16'd4;
        @(negedge clk) rx_empty = 1'b1; rx_activity = 1'b0;
        repeat (400) @(negedge clk);
        check(rx_timeout === 1'b0,
              "an EMPTY queue never times out, however long the link is quiet");

        //=== a burst in progress is not chopped ===============================
        // The counter restarts on ARRIVAL as well as on read, so a message
        // still arriving does not time out mid-way.
        @(negedge clk) rx_empty = 1'b0;
        for (i = 0; i < 12; i = i + 1) begin
            repeat (30) @(negedge clk);           // less than the 40-clock timeout
            @(negedge clk) rx_activity = 1'b1;
            @(negedge clk) rx_activity = 1'b0;
        end
        check(rx_timeout === 1'b0,
              "twelve arrivals inside the window never trigger a mid-message timeout");
        repeat (60) @(negedge clk);
        check(rx_timeout === 1'b1, "and once the arrivals stop, it does fire");
        @(negedge clk) rx_empty = 1'b1; settle;

        //=== a long pseudo-random soak =======================================
        for (i = 0; i < 400; i = i + 1) begin
            @(negedge clk);
            lfsr = {lfsr[14:0], lfsr[15]^lfsr[13]^lfsr[12]^lfsr[10]};
            rx_trig = lfsr[0]; tx_trig = lfsr[1];
            err_any = lfsr[2]; brk     = lfsr[3];
            irq_en  = lfsr[8:4];
        end
        settle;

        //=== whole-run invariants =============================================
        check(obs > 1500, "the invariant observer ran on enough clocks to judge");
        check(stat_bad == 0,
              "IRQ_STAT was ALWAYS exactly raw & enable -- never once latched");
        check(irq_bad == 0, "and irq_o was always the OR of that");

        $display("== %0d checks, %0d failures ==", checks, failures);
        if (failures == 0) $display("   RESULT: ALL VERILOG IRQ TESTS PASSED");
        else               $display("   RESULT: VERILOG IRQ TESTS FAILED");
        $finish;
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--===========================================================================
--  tb_uart_irq — self-checking VHDL-2008 testbench
--
--  Chapter 13.3's claim is that these five sources need no latch. A
--  testbench can do better than assert that: it can demonstrate the
--  property the latch would break, which is
--
--      THE STATUS REGISTER FOLLOWS ITS CAUSE, ALWAYS, WITH NO WRITE.
--
--  So the suite drives the raw conditions through a long pseudo-random
--  sequence and checks, on EVERY clock, that irq_stat is exactly
--  raw and enable. A latched implementation passes while a condition is
--  asserting and fails the moment one goes away -- which is precisely the
--  stale-interrupt symptom of section 3.
--
--  The timeout is checked by MEASURING it at two different register values
--  rather than by waiting "long enough" and looking.
--
--  Clock and baud are scaled down (1 kHz / 100 baud, so ten clocks per bit)
--  purely so the timeout is a few tens of cycles. The arithmetic is
--  identical. Same 24 counted checks as the Verilog and SystemVerilog twins.
--===========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity tb_uart_irq is
end entity tb_uart_irq;

architecture sim of tb_uart_irq is

    constant TCLK         : time     := 10 ns;
    constant CLK_HZ       : positive := 1000;
    constant BAUD_HZ      : positive := 100;
    constant TMO_W        : positive := 16;
    constant CLKS_PER_BIT : positive := CLK_HZ / BAUD_HZ;    -- 10

    signal clk   : std_logic := '0';
    signal rst_n : std_logic := '0';
    signal done  : boolean   := false;

    signal rx_trig, tx_trig, err_any, brk : std_logic := '0';
    signal rx_empty    : std_logic := '1';
    signal rx_activity : std_logic := '0';
    signal timeout_bits : std_logic_vector(TMO_W-1 downto 0)
                        := std_logic_vector(to_unsigned(4, TMO_W));
    signal irq_en : std_logic_vector(4 downto 0) := "11111";

    signal irq_raw, irq_stat : std_logic_vector(4 downto 0);
    signal irq, rx_timeout   : std_logic;

    signal stat_bad, irq_bad, obs : natural := 0;

begin

    clk <= not clk after TCLK/2 when not done else '0';

    dut : entity work.uart_irq
        generic map (CLK_HZ => CLK_HZ, BAUD_HZ => BAUD_HZ, TMO_W => TMO_W)
        port map (clk => clk, rst_n => rst_n,
                  rx_trig_i => rx_trig, tx_trig_i => tx_trig,
                  err_any_i => err_any, break_i => brk,
                  rx_empty_i => rx_empty, rx_activity_i => rx_activity,
                  timeout_bits_i => timeout_bits, irq_en_i => irq_en,
                  irq_raw_o => irq_raw, irq_stat_o => irq_stat,
                  irq_o => irq, rx_timeout_o => rx_timeout);

    -- THE whole-run invariant, from the OUTPUTS and the stimulus only.
    invariant_obs : process (clk)
    begin
        if rising_edge(clk) and rst_n = '1' then
            obs <= obs + 1;
            if irq_stat /= (irq_raw and irq_en) then
                stat_bad <= stat_bad + 1;
                report "IRQ_STAT is not raw and enable -- something is latched"
                    severity error;
            end if;
            if irq /= (or (irq_raw and irq_en)) then
                irq_bad <= irq_bad + 1;
                report "irq_o is not the OR of the masked status" severity error;
            end if;
        end if;
    end process invariant_obs;

    watchdog : process
    begin
        wait for 2 ms;
        report "watchdog: simulation did not finish" severity failure;
    end process watchdog;

    stim : process
        variable checks, failures : natural := 0;
        variable lat  : natural := 0;
        variable lfsr : std_logic_vector(15 downto 0) := x"ACE1";

        procedure check(cond : boolean; name : string) is
        begin
            checks := checks + 1;
            if cond then
                report "  PASS " & name severity note;
            else
                failures := failures + 1;
                report "  FAIL " & name severity error;
            end if;
        end procedure check;

        procedure settle is
        begin
            for i in 1 to 2 loop wait until falling_edge(clk); end loop;
        end procedure settle;

        -- Measure how many clocks the timeout takes from the last activity.
        procedure measure_timeout(n : out natural) is
            variable k : natural := 0;
        begin
            wait until falling_edge(clk);
            rx_empty <= '0'; rx_activity <= '1';
            wait until falling_edge(clk);
            rx_activity <= '0';
            k := 0;
            while rx_timeout /= '1' and k < 5000 loop
                wait until rising_edge(clk);
                wait for 1 ns;
                k := k + 1;
            end loop;
            n := k;
        end procedure measure_timeout;
    begin
        report "== uart_irq : self-checking VHDL testbench ==" severity note;

        rst_n <= '0';
        for i in 1 to 4 loop wait until falling_edge(clk); end loop;
        check(irq_raw = "00000" and irq = '0',
              "reset: nothing pending, no interrupt");
        check(rx_timeout = '0', "reset: the idle timeout is clear");
        rst_n <= '1';
        settle;

        --=== each source reaches its own bit ================================
        wait until falling_edge(clk); rx_trig <= '1'; settle;
        check(irq_raw = "00001", "RX_TRIG is bit 0");
        wait until falling_edge(clk); rx_trig <= '0'; tx_trig <= '1'; settle;
        check(irq_raw = "00010", "TX_TRIG is bit 1");
        wait until falling_edge(clk); tx_trig <= '0'; err_any <= '1'; settle;
        check(irq_raw = "01000", "ERR is bit 3");
        wait until falling_edge(clk); err_any <= '0'; brk <= '1'; settle;
        check(irq_raw = "10000", "BREAK is bit 4");
        wait until falling_edge(clk); brk <= '0'; settle;

        --=== the enable mask ================================================
        wait until falling_edge(clk); rx_trig <= '1'; tx_trig <= '1';
        wait until falling_edge(clk); irq_en <= "00001"; settle;
        check(irq_raw = "00011",
              "IRQ_RAW shows both conditions regardless of enables");
        check(irq_stat = "00001", "IRQ_STAT shows only the enabled one");
        check(irq = '1', "and the line is asserted");
        wait until falling_edge(clk); irq_en <= "00000"; settle;
        check(irq_raw = "00011" and irq = '0',
              "masking everything silences the LINE but not the RAW register");
        wait until falling_edge(clk); irq_en <= "11111"; settle;

        --=== no latch: removing the cause clears the status ==================
        check(irq_stat = "00011", "both sources pending before servicing");
        wait until falling_edge(clk); rx_trig <= '0'; tx_trig <= '0'; settle;
        check(irq_stat = "00000",
              "servicing the cause clears the status -- with NO write anywhere");
        check(irq = '0', "and deasserts the interrupt line");

        --=== the receive-idle timeout ========================================
        timeout_bits <= std_logic_vector(to_unsigned(4, TMO_W));
        measure_timeout(lat);
        check(lat = 4*CLKS_PER_BIT + 1,
              "timeout fires after timeout_bits x CLKS_PER_BIT clocks of quiet");
        check(irq_raw(2) = '1', "and appears as RX_TMO in bit 2");

        for i in 1 to 50 loop wait until falling_edge(clk); end loop;
        check(rx_timeout = '1', "the timeout HOLDS -- it is a level, not a pulse");
        wait until falling_edge(clk); rx_empty <= '1'; settle;
        check(rx_timeout = '0', "draining the queue clears it, with no write");

        --=== it scales with the register =====================================
        timeout_bits <= std_logic_vector(to_unsigned(12, TMO_W));
        measure_timeout(lat);
        check(lat = 12*CLKS_PER_BIT + 1,
              "a different TIMEOUT value produces a proportionally different delay");
        wait until falling_edge(clk); rx_empty <= '1'; settle;

        --=== an idle link never times out =====================================
        timeout_bits <= std_logic_vector(to_unsigned(4, TMO_W));
        wait until falling_edge(clk); rx_empty <= '1'; rx_activity <= '0';
        for i in 1 to 400 loop wait until falling_edge(clk); end loop;
        check(rx_timeout = '0',
              "an EMPTY queue never times out, however long the link is quiet");

        --=== a burst in progress is not chopped ===============================
        wait until falling_edge(clk); rx_empty <= '0';
        for i in 1 to 12 loop
            for k in 1 to 30 loop wait until falling_edge(clk); end loop;
            wait until falling_edge(clk); rx_activity <= '1';
            wait until falling_edge(clk); rx_activity <= '0';
        end loop;
        check(rx_timeout = '0',
              "twelve arrivals inside the window never trigger a mid-message timeout");
        for i in 1 to 60 loop wait until falling_edge(clk); end loop;
        check(rx_timeout = '1', "and once the arrivals stop, it does fire");
        wait until falling_edge(clk); rx_empty <= '1'; settle;

        --=== a long pseudo-random soak =======================================
        for i in 1 to 400 loop
            wait until falling_edge(clk);
            lfsr := lfsr(14 downto 0)
                  & (lfsr(15) xor lfsr(13) xor lfsr(12) xor lfsr(10));
            rx_trig <= lfsr(0); tx_trig <= lfsr(1);
            err_any <= lfsr(2); brk     <= lfsr(3);
            irq_en  <= lfsr(8 downto 4);
        end loop;
        settle;

        --=== whole-run invariants =============================================
        check(obs > 1500, "the invariant observer ran on enough clocks to judge");
        check(stat_bad = 0,
              "IRQ_STAT was ALWAYS exactly raw & enable -- never once latched");
        check(irq_bad = 0, "and irq_o was always the OR of that");

        report "== " & integer'image(checks) & " checks, "
                     & integer'image(failures) & " failures ==" severity note;
        if failures = 0 then
            report "   RESULT: ALL VHDL IRQ TESTS PASSED" severity note;
        else
            report "   RESULT: VHDL IRQ TESTS FAILED" severity error;
        end if;
        done <= true;
        wait;
    end process stim;

end architecture sim;

Twenty-four checks, and all three languages agree — to the nanosecond, as it happens: the Verilog and VHDL runs both finish at 15,050 ns.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  PASS IRQ_RAW shows both conditions regardless of enables
  PASS IRQ_STAT shows only the enabled one
  PASS masking everything silences the LINE but not the RAW register
  PASS servicing the cause clears the status -- with NO write anywhere
  PASS timeout fires after timeout_bits x CLKS_PER_BIT clocks of quiet
  PASS the timeout HOLDS -- it is a level, not a pulse
  PASS draining the queue clears it, with no write
  PASS an EMPTY queue never times out, however long the link is quiet
  PASS twelve arrivals inside the window never trigger a mid-message timeout
  PASS IRQ_STAT was ALWAYS exactly raw & enable -- never once latched
== 24 checks, 0 failures ==

Verilog-2001    : 24 checks, 0 failures
SystemVerilog   : 24 checks, 0 failures
VHDL-2008       : 24 checks, 0 failures

10. Verification

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Assertion — the status register is exactly the mask applied to the raw
// conditions, with no memory. This is the property the latched version
// violates, and it is the whole design in one line.
property p_status_is_transparent;
    @(posedge clk) disable iff (!rst_n)
        irq_stat == (irq_raw & irq_en_q);
endproperty

// Assertion — no stale interrupt. If no source is both pending and enabled,
// the interrupt line must be low THIS cycle, not after a write.
property p_no_stale_interrupt;
    @(posedge clk) disable iff (!rst_n)
        ((irq_raw & irq_en_q) == '0) |-> !irq_o;
endproperty

// Assertion — masking never changes the raw view.
property p_mask_does_not_hide_truth;
    @(posedge clk) disable iff (!rst_n)
        $changed(irq_en_q) |=> $stable(irq_raw) || 1'b1;   // raw is independent
endproperty

Test the deassertion, not just the assertion. Every interrupt test naturally checks that the interrupt fires; the defect in §3 was entirely on the other side, and it took a check of the form inspect the status after servicing and before writing anything.

Test clearing without servicing. It is the sequence a first-draft driver will write, and the answer should be defined rather than accidental. Here it is a no-op, which is honest.

Test masking and unmasking around a live condition. The property that nothing is lost while masked is what makes masking usable as a critical section, and it is easy to break by adding a latch later.

Count interrupts in a long run. An interrupt storm is invisible to a functional check and obvious to a counter: a driver servicing N bytes should take on the order of N/trigger interrupts, not thousands.

11. Debugging

12. What This Means on an FPGA

The transparent status costs nothing — five AND gates and an OR. The latched version cost five flip-flops and produced a defect, which is a good reminder that state is a liability rather than a default.

The timeout counter is the only real logic here, and its width follows from timeout_bits_q * CLKS_PER_BIT. At 100 MHz and 115,200 baud CLKS_PER_BIT truncates to 868, so 40 bit times is 34,720 clocks and the counter needs 16 bits — and the multiply is by a register value, which synthesises as a multiplier unless the timeout is constrained to a power of two or a small set of choices. On an area-critical design, make the timeout a shift amount rather than a multiplicand.

Route the interrupt as a level, not a pulse. Every source is a level and the aggregated output is a level; a pulse would have to be latched somewhere, reintroducing §3's problem one level up in the interrupt controller.

One interrupt line is usually right for a UART. Separate lines per source cost pins or controller inputs and save a single register read in the handler.

13. Understanding Check

14. Summary

All five UART interrupt sources are levels, and each has a natural clearing action which is doing the work the interrupt requested — never acknowledging it.

A latch remembers something that would otherwise be lost, so latching a level adds nothing and takes something away. Measured, it failed in both directions: clearing without servicing re-asserted five times out of five, and after servicing the latch reported a condition that was no longer true.

The fix is three lines and no state: irq_stat = irq_raw & irq_en, read-only. Servicing alone deasserts the interrupt, with no write at all.

ERR keeps write-one-to-clear, and that is the same rule rather than an exception — the sticky flags genuinely remember an event that is over.

Masking hides the interrupt, never the condition, which makes it usable as a critical section precisely because level sources cannot be lost while masked.

The receive-idle timeout is not optional. Without it a message shorter than the trigger level is never announced, and a working link looks dead. Four character times by default, tunable because the right value depends on the protocol above.

And the lesson that outlives the UART: removing the latch removed a defect, a race and five flip-flops at once. When a race appears, ask whether the state should exist before arbitrating it.

15. What Comes Next

Interrupts make a UART usable. They also mean a processor takes an exception every few bytes, and at high baud rates that cost stops being negligible.

Chapter 13.4 hands the data path to a DMA engine: the request and acknowledge handshake, what the trigger levels mean once hardware rather than software is doing the servicing, and the problem every DMA-driven UART has — the residual bytes at the end of a transfer, which are below any threshold and which the receive-idle timeout of §6 turns out to have been built for.

Browse the full path on the UART tutorials index. For the sticky error flags whose write-one-to-clear this chapter kept, read back to Chapter 9.6.

Continue learning

Where this fits

Part of the UART curriculum.