Skip to content
VLSI Mentor

UART · Module 18

Case Study: SoC Boot and Debug Console UART

The 16550 interrupt identification register in three HDLs, the one extra register read that permanently hangs a console, and the counter imbalance that turns a silent board into a measurement.

The console UART is the chip's only voice. Everything else — DRAM training, clock trees, secondary cores, the boot loader — reports through it, which means every other bring-up problem is discovered via the console and none of them can be discovered before it.

That gives console failures a distinctive character. The board is alive. The cores are running. Something is very definitely happening. And the console is silent, so there is no way to find out what.

This chapter builds the register that causes the most common version of that silence, and shows why the failure is permanent rather than intermittent.

1. What Boots on the Console

The console exists before almost anything else does. That constrains it more than its complexity suggests:

ConstraintConsequence
It runs before DRAM is trainedno buffering beyond the hardware FIFO; the driver is polling or a tiny ISR
It runs before the interrupt controller is configuredthe first output must work with interrupts entirely disabled
It runs before the MMUphysical addresses, no caching, every access goes to the device
It is the only way to report failuresa console bug masquerades as a failure of whatever it was reporting on

The last row is the one that makes console bugs expensive. When DRAM training fails and the console is also broken, the symptom is a silent board, and the DRAM problem is invisible behind the console problem.

2. The Register Map, and Its Acknowledge Rules

A 16550-compatible console presents a small register set. What matters for this chapter is not the layout but the acknowledge rule for each interrupt source, because they are not the same:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  source              IIR   priority   acknowledged by
  -----------------   ---   --------   -----------------------------------
  line status         0x6      1       reading LSR
  data available      0x4      2       reading RBR (the data itself)
  transmit empty      0x2      3       reading IIR  <-- or writing THR
  modem status        0x0      4       reading MSR
  (nothing pending)   0x1      -       -

Three of the four are acknowledged by touching the thing the interrupt is about. Read the status to clear the status interrupt; read the data to clear the data interrupt.

Transmit-empty is the exception. It is acknowledged either by writing new data — which is what servicing it means — or by reading the interrupt identification register, which is what asking about it means.

A block diagram of an SoC console interrupt path. Four interrupt sources feed a priority encoder: line status, receive data available, transmit holding register empty, and modem status. The priority encoder produces the interrupt identification value and a single interrupt line to the CPU. The CPU's driver reads the identification register to discover which source fired, and that read path is marked because it also retires the transmit empty source as a side effect. The driver's separate write to the transmit holding register is the action that actually services the transmitter and keeps the console running. The diagram shows that the read path and the service path are different, which is why asking what happened and doing something about it are not the same operation.LINE STATUSpriority 1RX AVAILpriority 2TX EMPTYpriority 3PRIORITYhighest winsIIRread retiresCPU DRIVERone read onlyTHR WRITEreal service0x60x40x2codereadserveclears12

3. The Register

Verilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------------
// uart_irq_ident -- the 16550 interrupt identification register, including the
// read-to-clear behaviour that makes a live board look dead.
//
// Four sources share one interrupt line, and the driver discovers which one
// fired by reading IIR. The register reports only the HIGHEST-priority pending
// source, and each source is acknowledged by a different action:
//
//   priority 1  0x6  line status      cleared by reading LSR
//   priority 2  0x4  data available   cleared by reading RBR
//   priority 3  0x2  transmit empty   cleared by reading IIR  <-- the trap
//   priority 4  0x0  modem status     cleared by reading MSR
//                0x1  no interrupt pending
//
// THRE is the odd one out: it is acknowledged by the very act of ASKING what
// happened. A handler that reads IIR twice -- to log it, or because a shared
// handler re-reads before dispatching -- consumes the transmit interrupt with
// the first read and sees "no interrupt pending" on the second. It then
// returns without writing THR, no further THRE interrupt is generated because
// the condition never re-asserts, and the console stops dead while every other
// part of the chip keeps running.
//
// n_thre_by_iir_o counts THRE interrupts retired by an IIR read. Comparing it
// against the number of THR writes is what turns that hang from a mystery into
// a measurement.
// ---------------------------------------------------------------------------
module uart_irq_ident (
    input  wire        clk,
    input  wire        rst_n,

    // ---- interrupt sources ----
    input  wire        lsr_err_i,     // overrun / parity / framing / break
    input  wire        rx_avail_i,    // receive data available (level)
    input  wire        thre_i,        // transmit holding register went empty
    input  wire        msr_chg_i,     // modem status changed

    // ---- interrupt enables: {msr, lsr, thre, rx} ----
    input  wire [3:0]  ier_i,

    // ---- register accesses ----
    input  wire        rd_iir_i,
    input  wire        rd_lsr_i,
    input  wire        rd_msr_i,
    input  wire        wr_thr_i,

    output wire [3:0]  iir_o,
    output wire        irq_o,

    // ---- instrumentation ----
    output reg  [15:0] n_iir_rd_o,      // IIR reads
    output reg  [15:0] n_thre_by_iir_o, // THRE retired by an IIR read
    output reg  [15:0] n_thr_wr_o       // THR writes (actual transmit service)
);

    localparam [3:0] IIR_NONE = 4'h1,
                     IIR_LSR  = 4'h6,
                     IIR_RX   = 4'h4,
                     IIR_THRE = 4'h2,
                     IIR_MSR  = 4'h0;

    // ier_i bit assignment
    localparam IER_RX = 0, IER_THRE = 1, IER_LSR = 2, IER_MSR = 3;

    reg lsr_pend;
    reg thre_pend;
    reg msr_pend;
    reg thre_q;

    // Receive-data-available is a LEVEL, not a latched event: it is pending
    // exactly while there is data to read, and reading RBR clears it by
    // emptying the FIFO rather than by touching this block.
    wire rx_pend = rx_avail_i;

    wire lsr_act  = lsr_pend  && ier_i[IER_LSR];
    wire rx_act   = rx_pend   && ier_i[IER_RX];
    wire thre_act = thre_pend && ier_i[IER_THRE];
    wire msr_act  = msr_pend  && ier_i[IER_MSR];

    // Strict priority. Only the highest-priority active source is reported,
    // which is why a driver must loop until IIR reads back "none".
    assign iir_o = lsr_act  ? IIR_LSR  :
                   rx_act   ? IIR_RX   :
                   thre_act ? IIR_THRE :
                   msr_act  ? IIR_MSR  :
                              IIR_NONE;

    assign irq_o = lsr_act || rx_act || thre_act || msr_act;

    // The IIR read retires THRE only when THRE is what it reported.
    wire iir_retires_thre = rd_iir_i && (iir_o == IIR_THRE);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            lsr_pend        <= 1'b0;
            thre_pend       <= 1'b0;
            msr_pend        <= 1'b0;
            thre_q          <= 1'b0;
            n_iir_rd_o      <= 16'd0;
            n_thre_by_iir_o <= 16'd0;
            n_thr_wr_o      <= 16'd0;
        end else begin
            thre_q <= thre_i;

            // ---- line status: set on the event, cleared by reading LSR ---
            if (lsr_err_i)      lsr_pend <= 1'b1;
            else if (rd_lsr_i)  lsr_pend <= 1'b0;

            // ---- modem status: set on change, cleared by reading MSR -----
            if (msr_chg_i)      msr_pend <= 1'b1;
            else if (rd_msr_i)  msr_pend <= 1'b0;

            // ---- transmit empty: the read-to-clear source -----------------
            // Set on the rising edge of THRE. Retired either by servicing it
            // (writing THR) or merely by asking about it (reading IIR).
            if (thre_i && !thre_q) begin
                thre_pend <= 1'b1;
            end else if (wr_thr_i || iir_retires_thre) begin
                thre_pend <= 1'b0;
            end

            // ---- instrumentation ----
            if (rd_iir_i)         n_iir_rd_o      <= n_iir_rd_o + 16'd1;
            if (iir_retires_thre) n_thre_by_iir_o <= n_thre_by_iir_o + 16'd1;
            if (wr_thr_i)         n_thr_wr_o      <= n_thr_wr_o + 16'd1;
        end
    end

endmodule

SystemVerilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------------
// uart_irq_ident -- the 16550 interrupt identification register, including the
// read-to-clear behaviour that makes a live board look dead.
//
// Four sources share one interrupt line, and the driver discovers which one
// fired by reading IIR. The register reports only the HIGHEST-priority pending
// source, and each source is acknowledged by a different action:
//
//   priority 1  0x6  line status      cleared by reading LSR
//   priority 2  0x4  data available   cleared by reading RBR
//   priority 3  0x2  transmit empty   cleared by reading IIR  <-- the trap
//   priority 4  0x0  modem status     cleared by reading MSR
//                0x1  no interrupt pending
//
// THRE is the odd one out: it is acknowledged by the very act of ASKING what
// happened. A handler that reads IIR twice -- to log it, or because a shared
// handler re-reads before dispatching -- consumes the transmit interrupt with
// the first read and sees "no interrupt pending" on the second. It then
// returns without writing THR, no further THRE interrupt is generated because
// the condition never re-asserts, and the console stops dead while every other
// part of the chip keeps running.
//
// n_thre_by_iir_o counts THRE interrupts retired by an IIR read. Comparing it
// against the number of THR writes is what turns that hang from a mystery into
// a measurement.
// ---------------------------------------------------------------------------
module uart_irq_ident (
    input  logic       clk,
    input  logic       rst_n,

    // ---- interrupt sources ----
    input  logic       lsr_err_i,     // overrun / parity / framing / break
    input  logic       rx_avail_i,    // receive data available (level)
    input  logic       thre_i,        // transmit holding register went empty
    input  logic       msr_chg_i,     // modem status changed

    // ---- interrupt enables: {msr, lsr, thre, rx} ----
    input  logic [3:0] ier_i,

    // ---- register accesses ----
    input  logic       rd_iir_i,
    input  logic       rd_lsr_i,
    input  logic       rd_msr_i,
    input  logic       wr_thr_i,

    output logic [3:0] iir_o,
    output logic       irq_o,

    // ---- instrumentation ----
    output logic [15:0] n_iir_rd_o,      // IIR reads
    output logic [15:0] n_thre_by_iir_o, // THRE retired by an IIR read
    output logic [15:0] n_thr_wr_o       // THR writes (actual transmit service)
);

    localparam [3:0] IIR_NONE = 4'h1,
                     IIR_LSR  = 4'h6,
                     IIR_RX   = 4'h4,
                     IIR_THRE = 4'h2,
                     IIR_MSR  = 4'h0;

    // ier_i bit assignment
    localparam IER_RX = 0, IER_THRE = 1, IER_LSR = 2, IER_MSR = 3;

    logic lsr_pend;
    logic thre_pend;
    logic msr_pend;
    logic thre_q;

    // Receive-data-available is a LEVEL, not a latched event: it is pending
    // exactly while there is data to read, and reading RBR clears it by
    // emptying the FIFO rather than by touching this block.
    wire rx_pend = rx_avail_i;

    wire lsr_act  = lsr_pend  && ier_i[IER_LSR];
    wire rx_act   = rx_pend   && ier_i[IER_RX];
    wire thre_act = thre_pend && ier_i[IER_THRE];
    wire msr_act  = msr_pend  && ier_i[IER_MSR];

    // Strict priority. Only the highest-priority active source is reported,
    // which is why a driver must loop until IIR reads back "none".
    assign iir_o = lsr_act  ? IIR_LSR  :
                   rx_act   ? IIR_RX   :
                   thre_act ? IIR_THRE :
                   msr_act  ? IIR_MSR  :
                              IIR_NONE;

    assign irq_o = lsr_act || rx_act || thre_act || msr_act;

    // The IIR read retires THRE only when THRE is what it reported.
    wire iir_retires_thre = rd_iir_i && (iir_o == IIR_THRE);

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            lsr_pend        <= 1'b0;
            thre_pend       <= 1'b0;
            msr_pend        <= 1'b0;
            thre_q          <= 1'b0;
            n_iir_rd_o      <= 16'd0;
            n_thre_by_iir_o <= 16'd0;
            n_thr_wr_o      <= 16'd0;
        end else begin
            thre_q <= thre_i;

            // ---- line status: set on the event, cleared by reading LSR ---
            if (lsr_err_i)      lsr_pend <= 1'b1;
            else if (rd_lsr_i)  lsr_pend <= 1'b0;

            // ---- modem status: set on change, cleared by reading MSR -----
            if (msr_chg_i)      msr_pend <= 1'b1;
            else if (rd_msr_i)  msr_pend <= 1'b0;

            // ---- transmit empty: the read-to-clear source -----------------
            // Set on the rising edge of THRE. Retired either by servicing it
            // (writing THR) or merely by asking about it (reading IIR).
            if (thre_i && !thre_q) begin
                thre_pend <= 1'b1;
            end else if (wr_thr_i || iir_retires_thre) begin
                thre_pend <= 1'b0;
            end

            // ---- instrumentation ----
            if (rd_iir_i)         n_iir_rd_o      <= n_iir_rd_o + 16'd1;
            if (iir_retires_thre) n_thre_by_iir_o <= n_thre_by_iir_o + 16'd1;
            if (wr_thr_i)         n_thr_wr_o      <= n_thr_wr_o + 16'd1;
        end
    end

endmodule

VHDL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- ---------------------------------------------------------------------------
-- uart_irq_ident -- the 16550 interrupt identification register, including the
-- read-to-clear behaviour that makes a live board look dead.
--
-- Four sources share one interrupt line, and the driver discovers which one
-- fired by reading IIR. The register reports only the HIGHEST-priority pending
-- source, and each source is acknowledged by a different action:
--
--   priority 1  0x6  line status      cleared by reading LSR
--   priority 2  0x4  data available   cleared by reading RBR
--   priority 3  0x2  transmit empty   cleared by reading IIR  <-- the trap
--   priority 4  0x0  modem status     cleared by reading MSR
--                0x1  no interrupt pending
--
-- THRE is the odd one out: it is acknowledged by the very act of ASKING what
-- happened. A handler that reads IIR twice -- to log it, or because a shared
-- handler re-reads before dispatching -- consumes the transmit interrupt with
-- the first read and sees "no interrupt pending" on the second. It then
-- returns without writing THR, no further THRE interrupt is generated because
-- the condition never re-asserts, and the console stops dead while every other
-- part of the chip keeps running.
--
-- n_thre_by_iir_o counts THRE interrupts retired by an IIR read. Comparing it
-- against the number of THR writes is what turns that hang from a mystery into
-- a measurement.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity uart_irq_ident is
    port (
        clk             : in  std_logic;
        rst_n           : in  std_logic;

        -- interrupt sources
        lsr_err_i       : in  std_logic;   -- overrun / parity / framing / break
        rx_avail_i      : in  std_logic;   -- receive data available (level)
        thre_i          : in  std_logic;   -- transmit holding register empty
        msr_chg_i       : in  std_logic;   -- modem status changed

        -- interrupt enables: (msr, lsr, thre, rx)
        ier_i           : in  std_logic_vector(3 downto 0);

        -- register accesses
        rd_iir_i        : in  std_logic;
        rd_lsr_i        : in  std_logic;
        rd_msr_i        : in  std_logic;
        wr_thr_i        : in  std_logic;

        iir_o           : out unsigned(3 downto 0);
        irq_o           : out std_logic;

        -- instrumentation
        n_iir_rd_o      : out unsigned(15 downto 0);  -- IIR reads
        n_thre_by_iir_o : out unsigned(15 downto 0);  -- THRE retired by a read
        n_thr_wr_o      : out unsigned(15 downto 0)   -- THR writes (real service)
    );
end entity uart_irq_ident;

architecture rtl of uart_irq_ident is

    constant IIR_NONE : unsigned(3 downto 0) := x"1";
    constant IIR_LSR  : unsigned(3 downto 0) := x"6";
    constant IIR_RX   : unsigned(3 downto 0) := x"4";
    constant IIR_THRE : unsigned(3 downto 0) := x"2";
    constant IIR_MSR  : unsigned(3 downto 0) := x"0";

    -- ier_i bit assignment
    constant IER_RX   : natural := 0;
    constant IER_THRE : natural := 1;
    constant IER_LSR  : natural := 2;
    constant IER_MSR  : natural := 3;

    signal lsr_pend : std_logic := '0';
    signal thre_pend: std_logic := '0';
    signal msr_pend : std_logic := '0';
    signal thre_q   : std_logic := '0';

    -- Receive-data-available is a LEVEL, not a latched event: it is pending
    -- exactly while there is data to read, and reading RBR clears it by
    -- emptying the FIFO rather than by touching this block.
    signal rx_pend  : std_logic;

    signal lsr_act, rx_act, thre_act, msr_act : std_logic;
    signal iir              : unsigned(3 downto 0);
    signal iir_retires_thre : std_logic;

    signal n_iir_rd, n_thre_by_iir, n_thr_wr : unsigned(15 downto 0)
        := (others => '0');

begin

    rx_pend  <= rx_avail_i;

    lsr_act  <= lsr_pend  and ier_i(IER_LSR);
    rx_act   <= rx_pend   and ier_i(IER_RX);
    thre_act <= thre_pend and ier_i(IER_THRE);
    msr_act  <= msr_pend  and ier_i(IER_MSR);

    -- Strict priority. Only the highest-priority active source is reported,
    -- which is why a driver must loop until IIR reads back "none".
    iir <= IIR_LSR  when lsr_act  = '1' else
           IIR_RX   when rx_act   = '1' else
           IIR_THRE when thre_act = '1' else
           IIR_MSR  when msr_act  = '1' else
           IIR_NONE;

    iir_o <= iir;
    irq_o <= lsr_act or rx_act or thre_act or msr_act;

    -- The IIR read retires THRE only when THRE is what it reported.
    iir_retires_thre <= '1' when (rd_iir_i = '1' and iir = IIR_THRE) else '0';

    n_iir_rd_o      <= n_iir_rd;
    n_thre_by_iir_o <= n_thre_by_iir;
    n_thr_wr_o      <= n_thr_wr;

    process (clk, rst_n)
    begin
        if rst_n = '0' then
            lsr_pend      <= '0';
            thre_pend     <= '0';
            msr_pend      <= '0';
            thre_q        <= '0';
            n_iir_rd      <= (others => '0');
            n_thre_by_iir <= (others => '0');
            n_thr_wr      <= (others => '0');
        elsif rising_edge(clk) then
            thre_q <= thre_i;

            -- ---- line status: set on the event, cleared by reading LSR ---
            if lsr_err_i = '1' then
                lsr_pend <= '1';
            elsif rd_lsr_i = '1' then
                lsr_pend <= '0';
            end if;

            -- ---- modem status: set on change, cleared by reading MSR -----
            if msr_chg_i = '1' then
                msr_pend <= '1';
            elsif rd_msr_i = '1' then
                msr_pend <= '0';
            end if;

            -- ---- transmit empty: the read-to-clear source -----------------
            -- Set on the rising edge of THRE. Retired either by servicing it
            -- (writing THR) or merely by asking about it (reading IIR).
            if thre_i = '1' and thre_q = '0' then
                thre_pend <= '1';
            elsif wr_thr_i = '1' or iir_retires_thre = '1' then
                thre_pend <= '0';
            end if;

            -- ---- instrumentation ----
            if rd_iir_i = '1' then
                n_iir_rd <= n_iir_rd + 1;
            end if;
            if iir_retires_thre = '1' then
                n_thre_by_iir <= n_thre_by_iir + 1;
            end if;
            if wr_thr_i = '1' then
                n_thr_wr <= n_thr_wr + 1;
            end if;
        end if;
    end process;

end architecture rtl;

The instrumentation at the bottom is the part most real IPs lack and the part that matters here. n_thre_by_iir_o counts transmit-empty interrupts retired by an IIR read; n_thr_wr_o counts actual transmit service. In a healthy system those track each other. §5 shows what their divergence means.

4. Two Drivers, Identical Hardware

Here are the two interrupt handlers, reduced to the part that differs:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  CORRECT                              BUGGY
  -----------------------------        -----------------------------
  id = read(IIR);                      id = read(IIR);   // for the log
                                       log(id);
                                       id = read(IIR);   // now dispatch
  if (id == THRE) write(THR, ch);      if (id == THRE) write(THR, ch);

The buggy version is not careless in any way that looks careless. Reading a register twice is free on every other source. Logging before dispatching is good practice. Shared interrupt handlers commonly re-read an identification register after calling into a sub-handler. Each individual decision is defensible.

A sequence diagram comparing a correct and a buggy interrupt handler against the same UART hardware. In the correct case the transmitter becomes empty and raises an interrupt, the driver reads the identification register once and is told transmit empty, and it then writes a character to the transmit holding register, which keeps the console running. In the buggy case the transmitter becomes empty and raises the same interrupt, the driver reads the identification register once for logging, which silently retires the transmit interrupt, then reads it a second time to decide what to do and is told no interrupt is pending. The driver therefore returns without writing a character. The transmitter stays empty, no new interrupt is ever generated because the condition never re-asserts, and the console is permanently dead.The same interrupt, handled two waysUARTDriverIRQ — transmit emptyread IIR returns 0x2(THRE)write THR — consolelivesIRQ — transmit emptyread IIR for the log— retires itread IIR againreturns 0x1 —nothingno write — consoledies here

5. The Measurement

Both handlers were run against the same register block:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  handler    IIR reads   THRE retired by read   THR writes   IRQ after
  --------   ---------   --------------------   ----------   ---------
  correct            1                      1            1          0
  buggy              2                      1            0          0

The correct handler's counters balance: one interrupt retired, one transmit serviced.

The buggy handler's do not. One interrupt was retired and nothing was transmitted. That imbalance is the signature, and it is directly readable from two counters.

One extra read, and the interrupt is gone

12 cycles
A timing trace of the buggy interrupt handler. The transmit empty condition rises and the interrupt line asserts. The driver performs its first read of the identification register, which returns the transmit empty code and, as a side effect, retires the pending interrupt, so the interrupt line falls. The driver then performs a second read intended to decide what to do, and that read returns the no interrupt pending code. Seeing nothing pending, the driver returns without writing the transmit holding register. The transmit empty condition remains true for the rest of the trace, so no new rising edge occurs and no further interrupt is ever generated, leaving the console permanently silent.the only chance to servicethe only chance to serviceno interrupt will ever comeno interrupt will ever comeread 1 — retires itread 1 — retires itread 2 — nothing pendingread 2 — nothing pendingtx emptyirqiir rdiir val010202020101010101010101thr wrt0t1t2t3t4t5t6t7t8t9t10t11

Why it never recovers

The interrupt line is low after both handlers, which looks the same. It is not:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  200 clocks later:   irq = 0   iir = 0x1 (nothing pending)

The transmit-empty interrupt is generated on the rising edge of the empty condition. After the buggy handler, the transmitter is still empty — it was never given anything to send — so the condition is still true and will not rise again. No further interrupt is possible.

Meanwhile the driver is waiting for an interrupt that cannot come, and the transmitter is waiting for data that will not arrive. Neither side is spinning, neither side has crashed, and nothing will ever change.

6. What Makes This Survive Review

The bug is in the driver, and the hardware is behaving exactly as the 16550 specification requires. So it is worth asking why it keeps happening.

  • It is invisible in the common case. A driver that reads IIR once works perfectly. The second read is added later — for logging, for a shared handler, for a debug build — by someone who has no reason to think a read has a side effect.
  • It passes on other sources. Reading IIR twice is harmless when the pending source is data-available or line-status, because those are acknowledged elsewhere. The bug only appears when the pending source is transmit-empty, which on a console is most output.
  • It looks like a different bug. The console dies mid-line, often part-way through a message, which reads as a crash in whatever was printing.
  • It is timing-dependent at first. Early boot output is often polled, not interrupt-driven. The failure appears only once the driver switches to interrupts, which on many systems is after the first few lines have already printed successfully — making the console look like it was working.

7. The Testbench

Verilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`timescale 1ns/1ps
// ---------------------------------------------------------------------------
// Testbench for uart_irq_ident.
//
// Two driver models are run against identical hardware. The only difference
// between them is that one reads IIR once and the other reads it twice -- a
// change that looks entirely harmless and is the difference between a working
// console and a dead one.
//
// T5 does not merely show the hang. It shows the hang PERSISTING: after the
// buggy handler returns, the interrupt line stays low forever, because THRE is
// edge-triggered and the transmitter has no reason to become empty again. The
// board is alive and the console is gone.
// ---------------------------------------------------------------------------
module tb_uart_irq_ident;

    localparam [3:0] IIR_NONE=4'h1, IIR_LSR=4'h6, IIR_RX=4'h4,
                     IIR_THRE=4'h2, IIR_MSR=4'h0;

    reg        clk = 1'b0;
    reg        rst_n = 1'b0;
    reg        lsr_err = 1'b0, rx_avail = 1'b0, thre = 1'b0, msr_chg = 1'b0;
    reg  [3:0] ier = 4'hF;
    reg        rd_iir = 1'b0, rd_lsr = 1'b0, rd_msr = 1'b0, wr_thr = 1'b0;

    wire [3:0]  iir;
    wire        irq;
    wire [15:0] n_iir_rd, n_thre_by_iir, n_thr_wr;

    integer checks = 0;
    integer fails  = 0;
    reg [3:0] seen;                  // what the last IIR read returned

    always #5 clk = ~clk;

    uart_irq_ident dut (
        .clk(clk), .rst_n(rst_n),
        .lsr_err_i(lsr_err), .rx_avail_i(rx_avail), .thre_i(thre), .msr_chg_i(msr_chg),
        .ier_i(ier),
        .rd_iir_i(rd_iir), .rd_lsr_i(rd_lsr), .rd_msr_i(rd_msr), .wr_thr_i(wr_thr),
        .iir_o(iir), .irq_o(irq),
        .n_iir_rd_o(n_iir_rd), .n_thre_by_iir_o(n_thre_by_iir), .n_thr_wr_o(n_thr_wr));

    task chk;
        input [255:0] name;
        input integer got;
        input integer exp;
        begin
            checks = checks + 1;
            if (got !== exp) begin
                fails = fails + 1;
                $display("  FAIL %0s: got %0h expected %0h", name, got, exp);
            end
        end
    endtask

    task do_reset;
        begin
            lsr_err=0; rx_avail=0; thre=0; msr_chg=0; ier=4'hF;
            rd_iir=0; rd_lsr=0; rd_msr=0; wr_thr=0; rst_n=0;
            repeat (3) @(posedge clk);
            @(negedge clk); rst_n = 1'b1;
            repeat (2) @(posedge clk);
        end
    endtask

    // ---- bus cycles, each one clock wide ----
    task read_iir;                   // captures the value the read returned
        begin
            @(negedge clk); rd_iir = 1'b1; seen = iir;
            @(posedge clk);
            @(negedge clk); rd_iir = 1'b0;
        end
    endtask

    task read_lsr; begin @(negedge clk); rd_lsr=1'b1; @(posedge clk); @(negedge clk); rd_lsr=1'b0; end endtask
    task read_msr; begin @(negedge clk); rd_msr=1'b1; @(posedge clk); @(negedge clk); rd_msr=1'b0; end endtask
    task write_thr; begin @(negedge clk); wr_thr=1'b1; @(posedge clk); @(negedge clk); wr_thr=1'b0; end endtask

    task pulse_thre;                 // the transmitter drains and goes empty
        begin
            @(negedge clk); thre = 1'b0;
            @(posedge clk);
            @(negedge clk); thre = 1'b1;
            @(posedge clk);
        end
    endtask

    // ---- the two driver models ----
    // Correct: read IIR once, dispatch on what it returned.
    task driver_good;
        begin
            read_iir;
            if (seen == IIR_THRE) write_thr;
        end
    endtask

    // Buggy: read IIR to log it, then read it again to dispatch. The first
    // read has already retired THRE, so the second returns "no interrupt".
    task driver_bad;
        begin
            read_iir;                // "let's log what fired"
            read_iir;                // "now let's see what to do"
            if (seen == IIR_THRE) write_thr;
        end
    endtask

    integer i;

    initial begin
        // ---------------- T1: strict priority -----------------------------
        do_reset;
        @(negedge clk); lsr_err=1'b1; rx_avail=1'b1; msr_chg=1'b1;
        @(posedge clk);
        @(negedge clk); lsr_err=1'b0; msr_chg=1'b0;
        pulse_thre;
        #1;
        $display("T1 all four pending      : iir=%0h irq=%0b", iir, irq);
        chk("T1 line status wins",  iir, IIR_LSR);
        chk("T1 interrupt asserted", irq, 1);

        // ---------------- T2: they drain in priority order ----------------
        read_lsr;  #1; chk("T2 then data available", iir, IIR_RX);
        @(negedge clk); rx_avail = 1'b0; #1;
        chk("T2 then transmit empty", iir, IIR_THRE);
        read_iir;  #1;                          // retires THRE
        chk("T2 then modem status",  iir, IIR_MSR);
        read_msr;  #1;
        $display("T2 fully drained         : iir=%0h irq=%0b", iir, irq);
        chk("T2 nothing left",        iir, IIR_NONE);
        chk("T2 interrupt deasserted", irq, 0);

        // ---------------- T3: a masked source raises nothing --------------
        do_reset;
        @(negedge clk); ier = 4'b0000;
        pulse_thre;
        #1;
        chk("T3 masked THRE is not reported", iir, IIR_NONE);
        chk("T3 masked THRE raises no irq",   irq, 0);
        @(negedge clk); ier = 4'b0010;          // enable THRE only
        #1;
        chk("T3 unmasking reveals it", iir, IIR_THRE);

        // ---------------- T4: the correct driver --------------------------
        do_reset;
        pulse_thre;
        #1;
        chk("T4 THRE is pending", iir, IIR_THRE);
        driver_good;
        #1;
        $display("T4 correct driver        : iir_reads=%0d thre_by_iir=%0d thr_writes=%0d irq=%0b",
                 n_iir_rd, n_thre_by_iir, n_thr_wr, irq);
        chk("T4 the transmitter was serviced", n_thr_wr, 1);
        chk("T4 service balances the retire",  n_thre_by_iir, n_thr_wr);
        chk("T4 interrupt cleared",            irq, 0);

        // ---------------- T5: the buggy driver ----------------------------
        // Identical hardware. One extra IIR read.
        do_reset;
        pulse_thre;
        #1;
        chk("T5 THRE is pending", iir, IIR_THRE);
        driver_bad;
        #1;
        $display("T5 buggy driver          : iir_reads=%0d thre_by_iir=%0d thr_writes=%0d irq=%0b",
                 n_iir_rd, n_thre_by_iir, n_thr_wr, irq);
        chk("T5 the second read saw nothing", seen, IIR_NONE);
        chk("T5 the transmitter was NOT serviced", n_thr_wr, 0);
        chk("T5 but the interrupt WAS retired",    n_thre_by_iir, 1);

        // ---------------- T5b: and the hang is permanent ------------------
        // THRE is edge-triggered and the transmitter is already empty, so no
        // further interrupt will ever be generated. Nothing recovers this.
        for (i = 0; i < 200; i = i + 1) @(posedge clk);
        #1;
        $display("T5b 200 clocks later     : irq=%0b iir=%0h  <-- console is dead", irq, iir);
        chk("T5b still no interrupt", irq, 0);
        chk("T5b still nothing pending", iir, IIR_NONE);
        chk("T5b the imbalance is the signature",
            (n_thre_by_iir > n_thr_wr) ? 1 : 0, 1);

        // ---------------- T6: writing THR also acknowledges ---------------
        do_reset;
        pulse_thre;
        #1;
        chk("T6 THRE pending", iir, IIR_THRE);
        write_thr;
        #1;
        $display("T6 THR write ack         : iir=%0h thre_by_iir=%0d", iir, n_thre_by_iir);
        chk("T6 THR write cleared it",        iir, IIR_NONE);
        chk("T6 and no IIR read was needed",  n_thre_by_iir, 0);

        // ---------------- T7: THRE needs a fresh edge to re-arm -----------
        pulse_thre;
        #1;
        chk("T7 a new edge re-arms it", iir, IIR_THRE);

        $display("");
        $display("== %0d checks, %0d failures ==", checks, fails);
        if (fails == 0) $display("   RESULT: ALL VERILOG IRQ-IDENT TESTS PASSED");
        else            $display("   RESULT: %0d FAILURE(S)", fails);
        $finish;
    end

endmodule

SystemVerilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`timescale 1ns/1ps
// ---------------------------------------------------------------------------
// Testbench for uart_irq_ident.
//
// Two driver models are run against identical hardware. The only difference
// between them is that one reads IIR once and the other reads it twice -- a
// change that looks entirely harmless and is the difference between a working
// console and a dead one.
//
// T5 does not merely show the hang. It shows the hang PERSISTING: after the
// buggy handler returns, the interrupt line stays low forever, because THRE is
// edge-triggered and the transmitter has no reason to become empty again. The
// board is alive and the console is gone.
// ---------------------------------------------------------------------------
module tb_uart_irq_ident;

    localparam [3:0] IIR_NONE=4'h1, IIR_LSR=4'h6, IIR_RX=4'h4,
                     IIR_THRE=4'h2, IIR_MSR=4'h0;

    logic clk = 1'b0;
    logic rst_n = 1'b0;
    logic lsr_err = 1'b0, rx_avail = 1'b0, thre = 1'b0, msr_chg = 1'b0;
    logic [3:0] ier = 4'hF;
    logic rd_iir = 1'b0, rd_lsr = 1'b0, rd_msr = 1'b0, wr_thr = 1'b0;

    logic [3:0]  iir;
    logic        irq;
    logic [15:0] n_iir_rd, n_thre_by_iir, n_thr_wr;

    integer checks = 0;
    integer fails  = 0;
    logic [3:0] seen;                  // what the last IIR read returned

    always #5 clk = ~clk;

    uart_irq_ident dut (
        .clk(clk), .rst_n(rst_n),
        .lsr_err_i(lsr_err), .rx_avail_i(rx_avail), .thre_i(thre), .msr_chg_i(msr_chg),
        .ier_i(ier),
        .rd_iir_i(rd_iir), .rd_lsr_i(rd_lsr), .rd_msr_i(rd_msr), .wr_thr_i(wr_thr),
        .iir_o(iir), .irq_o(irq),
        .n_iir_rd_o(n_iir_rd), .n_thre_by_iir_o(n_thre_by_iir), .n_thr_wr_o(n_thr_wr));

    task automatic chk(input string name, input int got, input int exp);
        begin
            checks = checks + 1;
            if (got !== exp) begin
                fails = fails + 1;
                $display("  FAIL %0s: got %0h expected %0h", name, got, exp);
            end
        end
    endtask

    task automatic do_reset();
        begin
            lsr_err=0; rx_avail=0; thre=0; msr_chg=0; ier=4'hF;
            rd_iir=0; rd_lsr=0; rd_msr=0; wr_thr=0; rst_n=0;
            repeat (3) @(posedge clk);
            @(negedge clk); rst_n = 1'b1;
            repeat (2) @(posedge clk);
        end
    endtask

    // ---- bus cycles, each one clock wide ----
    task automatic read_iir();                   // captures the value the read returned
        begin
            @(negedge clk); rd_iir = 1'b1; seen = iir;
            @(posedge clk);
            @(negedge clk); rd_iir = 1'b0;
        end
    endtask

    task automatic read_lsr(); begin @(negedge clk); rd_lsr=1'b1; @(posedge clk); @(negedge clk); rd_lsr=1'b0; end endtask
    task automatic read_msr(); begin @(negedge clk); rd_msr=1'b1; @(posedge clk); @(negedge clk); rd_msr=1'b0; end endtask
    task automatic write_thr(); begin @(negedge clk); wr_thr=1'b1; @(posedge clk); @(negedge clk); wr_thr=1'b0; end endtask

    task automatic pulse_thre();                 // the transmitter drains and goes empty
        begin
            @(negedge clk); thre = 1'b0;
            @(posedge clk);
            @(negedge clk); thre = 1'b1;
            @(posedge clk);
        end
    endtask

    // ---- the two driver models ----
    // Correct: read IIR once, dispatch on what it returned.
    task automatic driver_good();
        begin
            read_iir;
            if (seen == IIR_THRE) write_thr;
        end
    endtask

    // Buggy: read IIR to log it, then read it again to dispatch. The first
    // read has already retired THRE, so the second returns "no interrupt".
    task automatic driver_bad();
        begin
            read_iir;                // "let's log what fired"
            read_iir;                // "now let's see what to do"
            if (seen == IIR_THRE) write_thr;
        end
    endtask

    integer i;

    initial begin
        // ---------------- T1: strict priority -----------------------------
        do_reset;
        @(negedge clk); lsr_err=1'b1; rx_avail=1'b1; msr_chg=1'b1;
        @(posedge clk);
        @(negedge clk); lsr_err=1'b0; msr_chg=1'b0;
        pulse_thre;
        #1;
        $display("T1 all four pending      : iir=%0h irq=%0b", iir, irq);
        chk("T1 line status wins",  iir, IIR_LSR);
        chk("T1 interrupt asserted", irq, 1);

        // ---------------- T2: they drain in priority order ----------------
        read_lsr;  #1; chk("T2 then data available", iir, IIR_RX);
        @(negedge clk); rx_avail = 1'b0; #1;
        chk("T2 then transmit empty", iir, IIR_THRE);
        read_iir;  #1;                          // retires THRE
        chk("T2 then modem status",  iir, IIR_MSR);
        read_msr;  #1;
        $display("T2 fully drained         : iir=%0h irq=%0b", iir, irq);
        chk("T2 nothing left",        iir, IIR_NONE);
        chk("T2 interrupt deasserted", irq, 0);

        // ---------------- T3: a masked source raises nothing --------------
        do_reset;
        @(negedge clk); ier = 4'b0000;
        pulse_thre;
        #1;
        chk("T3 masked THRE is not reported", iir, IIR_NONE);
        chk("T3 masked THRE raises no irq",   irq, 0);
        @(negedge clk); ier = 4'b0010;          // enable THRE only
        #1;
        chk("T3 unmasking reveals it", iir, IIR_THRE);

        // ---------------- T4: the correct driver --------------------------
        do_reset;
        pulse_thre;
        #1;
        chk("T4 THRE is pending", iir, IIR_THRE);
        driver_good;
        #1;
        $display("T4 correct driver        : iir_reads=%0d thre_by_iir=%0d thr_writes=%0d irq=%0b",
                 n_iir_rd, n_thre_by_iir, n_thr_wr, irq);
        chk("T4 the transmitter was serviced", n_thr_wr, 1);
        chk("T4 service balances the retire",  n_thre_by_iir, n_thr_wr);
        chk("T4 interrupt cleared",            irq, 0);

        // ---------------- T5: the buggy driver ----------------------------
        // Identical hardware. One extra IIR read.
        do_reset;
        pulse_thre;
        #1;
        chk("T5 THRE is pending", iir, IIR_THRE);
        driver_bad;
        #1;
        $display("T5 buggy driver          : iir_reads=%0d thre_by_iir=%0d thr_writes=%0d irq=%0b",
                 n_iir_rd, n_thre_by_iir, n_thr_wr, irq);
        chk("T5 the second read saw nothing", seen, IIR_NONE);
        chk("T5 the transmitter was NOT serviced", n_thr_wr, 0);
        chk("T5 but the interrupt WAS retired",    n_thre_by_iir, 1);

        // ---------------- T5b: and the hang is permanent ------------------
        // THRE is edge-triggered and the transmitter is already empty, so no
        // further interrupt will ever be generated. Nothing recovers this.
        for (i = 0; i < 200; i = i + 1) @(posedge clk);
        #1;
        $display("T5b 200 clocks later     : irq=%0b iir=%0h  <-- console is dead", irq, iir);
        chk("T5b still no interrupt", irq, 0);
        chk("T5b still nothing pending", iir, IIR_NONE);
        chk("T5b the imbalance is the signature",
            (n_thre_by_iir > n_thr_wr) ? 1 : 0, 1);

        // ---------------- T6: writing THR also acknowledges ---------------
        do_reset;
        pulse_thre;
        #1;
        chk("T6 THRE pending", iir, IIR_THRE);
        write_thr;
        #1;
        $display("T6 THR write ack         : iir=%0h thre_by_iir=%0d", iir, n_thre_by_iir);
        chk("T6 THR write cleared it",        iir, IIR_NONE);
        chk("T6 and no IIR read was needed",  n_thre_by_iir, 0);

        // ---------------- T7: THRE needs a fresh edge to re-arm -----------
        pulse_thre;
        #1;
        chk("T7 a new edge re-arms it", iir, IIR_THRE);

        $display("");
        $display("== %0d checks, %0d failures ==", checks, fails);
        if (fails == 0) $display("   RESULT: ALL SYSTEMVERILOG IRQ-IDENT TESTS PASSED");
        else            $display("   RESULT: %0d FAILURE(S)", fails);
        $finish;
    end

endmodule

VHDL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- ---------------------------------------------------------------------------
-- Testbench for uart_irq_ident.
--
-- Two driver models are run against identical hardware. The only difference
-- between them is that one reads IIR once and the other reads it twice -- a
-- change that looks entirely harmless and is the difference between a working
-- console and a dead one.
--
-- T5 does not merely show the hang. It shows the hang PERSISTING: after the
-- buggy handler returns, the interrupt line stays low forever, because THRE is
-- edge-triggered and the transmitter has no reason to become empty again. The
-- board is alive and the console is gone.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity tb_uart_irq_ident is
end entity tb_uart_irq_ident;

architecture sim of tb_uart_irq_ident is

    constant TCLK : time := 10 ns;

    constant IIR_NONE : unsigned(3 downto 0) := x"1";
    constant IIR_LSR  : unsigned(3 downto 0) := x"6";
    constant IIR_RX   : unsigned(3 downto 0) := x"4";
    constant IIR_THRE : unsigned(3 downto 0) := x"2";
    constant IIR_MSR  : unsigned(3 downto 0) := x"0";

    signal clk      : std_logic := '0';
    signal rst_n    : std_logic := '0';
    signal lsr_err  : std_logic := '0';
    signal rx_avail : std_logic := '0';
    signal thre     : std_logic := '0';
    signal msr_chg  : std_logic := '0';
    signal ier      : std_logic_vector(3 downto 0) := "1111";
    signal rd_iir   : std_logic := '0';
    signal rd_lsr   : std_logic := '0';
    signal rd_msr   : std_logic := '0';
    signal wr_thr   : std_logic := '0';
    signal sim_done : boolean := false;

    signal iir : unsigned(3 downto 0);
    signal irq : std_logic;
    signal n_iir_rd, n_thre_by_iir, n_thr_wr : unsigned(15 downto 0);

    signal seen : unsigned(3 downto 0) := IIR_NONE;  -- last value IIR returned

begin

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

    dut : entity work.uart_irq_ident
        port map (clk => clk, rst_n => rst_n,
                  lsr_err_i => lsr_err, rx_avail_i => rx_avail,
                  thre_i => thre, msr_chg_i => msr_chg, ier_i => ier,
                  rd_iir_i => rd_iir, rd_lsr_i => rd_lsr,
                  rd_msr_i => rd_msr, wr_thr_i => wr_thr,
                  iir_o => iir, irq_o => irq,
                  n_iir_rd_o => n_iir_rd, n_thre_by_iir_o => n_thre_by_iir,
                  n_thr_wr_o => n_thr_wr);

    stim : process
        variable checks, fails : integer := 0;

        procedure chk (name : string; got : integer; exp : integer) is
        begin
            checks := checks + 1;
            if got /= exp then
                fails := fails + 1;
                report "  FAIL " & name & ": got " & integer'image(got) &
                       " expected " & integer'image(exp) severity error;
            end if;
        end procedure;

        procedure do_reset is
        begin
            lsr_err <= '0'; rx_avail <= '0'; thre <= '0'; msr_chg <= '0';
            ier <= "1111"; rd_iir <= '0'; rd_lsr <= '0'; rd_msr <= '0';
            wr_thr <= '0'; rst_n <= '0';
            for i in 0 to 2 loop wait until rising_edge(clk); end loop;
            wait until falling_edge(clk); rst_n <= '1';
            for i in 0 to 1 loop wait until rising_edge(clk); end loop;
        end procedure;

        -- ---- bus cycles, each one clock wide ----
        procedure read_iir is        -- captures the value the read returned
        begin
            wait until falling_edge(clk);
            rd_iir <= '1'; seen <= iir;
            wait until rising_edge(clk);
            wait until falling_edge(clk); rd_iir <= '0';
        end procedure;

        procedure read_lsr is
        begin
            wait until falling_edge(clk); rd_lsr <= '1';
            wait until rising_edge(clk);
            wait until falling_edge(clk); rd_lsr <= '0';
        end procedure;

        procedure read_msr is
        begin
            wait until falling_edge(clk); rd_msr <= '1';
            wait until rising_edge(clk);
            wait until falling_edge(clk); rd_msr <= '0';
        end procedure;

        procedure write_thr is
        begin
            wait until falling_edge(clk); wr_thr <= '1';
            wait until rising_edge(clk);
            wait until falling_edge(clk); wr_thr <= '0';
        end procedure;

        procedure pulse_thre is      -- the transmitter drains and goes empty
        begin
            wait until falling_edge(clk); thre <= '0';
            wait until rising_edge(clk);
            wait until falling_edge(clk); thre <= '1';
            wait until rising_edge(clk);
        end procedure;

        -- ---- the two driver models ----
        -- Correct: read IIR once, dispatch on what it returned.
        procedure driver_good is
        begin
            read_iir;
            if seen = IIR_THRE then write_thr; end if;
        end procedure;

        -- Buggy: read IIR to log it, then read it again to dispatch. The first
        -- read has already retired THRE, so the second returns "no interrupt".
        procedure driver_bad is
        begin
            read_iir;                -- "let's log what fired"
            read_iir;                -- "now let's see what to do"
            if seen = IIR_THRE then write_thr; end if;
        end procedure;

    begin
        -- ---------------- T1: strict priority -----------------------------
        do_reset;
        wait until falling_edge(clk);
        lsr_err <= '1'; rx_avail <= '1'; msr_chg <= '1';
        wait until rising_edge(clk);
        wait until falling_edge(clk); lsr_err <= '0'; msr_chg <= '0';
        pulse_thre;
        wait for 1 ns;
        report "T1 all four pending      : iir=" & integer'image(to_integer(iir)) &
               " irq=" & std_logic'image(irq)(2);
        chk("T1 line status wins",   to_integer(iir), to_integer(IIR_LSR));
        chk("T1 interrupt asserted", to_integer(unsigned'("" & irq)), 1);

        -- ---------------- T2: they drain in priority order ----------------
        read_lsr; wait for 1 ns;
        chk("T2 then data available", to_integer(iir), to_integer(IIR_RX));
        wait until falling_edge(clk); rx_avail <= '0'; wait for 1 ns;
        chk("T2 then transmit empty", to_integer(iir), to_integer(IIR_THRE));
        read_iir; wait for 1 ns;                 -- retires THRE
        chk("T2 then modem status",   to_integer(iir), to_integer(IIR_MSR));
        read_msr; wait for 1 ns;
        report "T2 fully drained         : iir=" & integer'image(to_integer(iir)) &
               " irq=" & std_logic'image(irq)(2);
        chk("T2 nothing left",         to_integer(iir), to_integer(IIR_NONE));
        chk("T2 interrupt deasserted", to_integer(unsigned'("" & irq)), 0);

        -- ---------------- T3: a masked source raises nothing --------------
        do_reset;
        wait until falling_edge(clk); ier <= "0000";
        pulse_thre;
        wait for 1 ns;
        chk("T3 masked THRE is not reported", to_integer(iir), to_integer(IIR_NONE));
        chk("T3 masked THRE raises no irq",   to_integer(unsigned'("" & irq)), 0);
        wait until falling_edge(clk); ier <= "0010";   -- enable THRE only
        wait for 1 ns;
        chk("T3 unmasking reveals it", to_integer(iir), to_integer(IIR_THRE));

        -- ---------------- T4: the correct driver --------------------------
        do_reset;
        pulse_thre;
        wait for 1 ns;
        chk("T4 THRE is pending", to_integer(iir), to_integer(IIR_THRE));
        driver_good;
        wait for 1 ns;
        report "T4 correct driver        : iir_reads=" & integer'image(to_integer(n_iir_rd)) &
               " thre_by_iir=" & integer'image(to_integer(n_thre_by_iir)) &
               " thr_writes=" & integer'image(to_integer(n_thr_wr)) &
               " irq=" & std_logic'image(irq)(2);
        chk("T4 the transmitter was serviced", to_integer(n_thr_wr), 1);
        chk("T4 service balances the retire",  to_integer(n_thre_by_iir), to_integer(n_thr_wr));
        chk("T4 interrupt cleared",            to_integer(unsigned'("" & irq)), 0);

        -- ---------------- T5: the buggy driver ----------------------------
        -- Identical hardware. One extra IIR read.
        do_reset;
        pulse_thre;
        wait for 1 ns;
        chk("T5 THRE is pending", to_integer(iir), to_integer(IIR_THRE));
        driver_bad;
        wait for 1 ns;
        report "T5 buggy driver          : iir_reads=" & integer'image(to_integer(n_iir_rd)) &
               " thre_by_iir=" & integer'image(to_integer(n_thre_by_iir)) &
               " thr_writes=" & integer'image(to_integer(n_thr_wr)) &
               " irq=" & std_logic'image(irq)(2);
        chk("T5 the second read saw nothing",      to_integer(seen), to_integer(IIR_NONE));
        chk("T5 the transmitter was NOT serviced", to_integer(n_thr_wr), 0);
        chk("T5 but the interrupt WAS retired",    to_integer(n_thre_by_iir), 1);

        -- ---------------- T5b: and the hang is permanent ------------------
        -- THRE is edge-triggered and the transmitter is already empty, so no
        -- further interrupt will ever be generated. Nothing recovers this.
        for i in 0 to 199 loop wait until rising_edge(clk); end loop;
        wait for 1 ns;
        report "T5b 200 clocks later     : irq=" & std_logic'image(irq)(2) &
               " iir=" & integer'image(to_integer(iir)) & "  <-- console is dead";
        chk("T5b still no interrupt",   to_integer(unsigned'("" & irq)), 0);
        chk("T5b still nothing pending", to_integer(iir), to_integer(IIR_NONE));
        if n_thre_by_iir > n_thr_wr then
            chk("T5b the imbalance is the signature", 1, 1);
        else
            chk("T5b the imbalance is the signature", 0, 1);
        end if;

        -- ---------------- T6: writing THR also acknowledges ---------------
        do_reset;
        pulse_thre;
        wait for 1 ns;
        chk("T6 THRE pending", to_integer(iir), to_integer(IIR_THRE));
        write_thr;
        wait for 1 ns;
        report "T6 THR write ack         : iir=" & integer'image(to_integer(iir)) &
               " thre_by_iir=" & integer'image(to_integer(n_thre_by_iir));
        chk("T6 THR write cleared it",       to_integer(iir), to_integer(IIR_NONE));
        chk("T6 and no IIR read was needed", to_integer(n_thre_by_iir), 0);

        -- ---------------- T7: THRE needs a fresh edge to re-arm -----------
        pulse_thre;
        wait for 1 ns;
        chk("T7 a new edge re-arms it", to_integer(iir), to_integer(IIR_THRE));

        report "";
        report "== " & integer'image(checks) & " checks, " &
               integer'image(fails) & " failures ==";
        if fails = 0 then
            report "   RESULT: ALL VHDL IRQ-IDENT TESTS PASSED";
        else
            report "   RESULT: " & integer'image(fails) & " FAILURE(S)" severity error;
        end if;
        sim_done <= true;
        wait;
    end process;

end architecture sim;

Twenty-five checks per language. T1 and T2 establish the priority encoding and that sources drain in order; T3 covers masking; T4 and T5 are the two handlers; T5b runs 200 clocks past the hang to show it is permanent; T6 and T7 confirm the other acknowledge path and the edge-triggered re-arm.

8. Proving the Tests Can Fail

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  mutation                                                 checks failed   verdict
  ------------------------------------------------------   -------------   -------
  M4  let any IIR read retire THRE, not just a THRE read                1    killed
  M5  swap data-available and transmit-empty priority                   1    killed
  M6  remove read-to-clear entirely                                     6    killed

M6 is the interesting one, because it is a mutation that fixes the bug. Removing the IIR read's ability to retire THRE makes the buggy driver work.

Six checks fail — the ones that assert the hang happens. That is correct and important: this behaviour is specified, other software depends on the single-read idiom, and a test suite that did not notice its removal would not be protecting the specification. A test that pins down a surprising but required behaviour is doing exactly its job, and it is the reason M6 must be killed rather than tolerated.

9. Design Guidance

For the hardware:

  • Instrument the acknowledge paths. Two counters — interrupts retired by a read, and actual services — cost almost nothing and turn this class of hang into a register read.
  • Consider a non-destructive alias. A read-only mirror of IIR that reports without retiring makes logging safe. Several modern UARTs provide one.
  • Document the side effect at the register, not in a footnote. "Reading this register clears the THRE interrupt" belongs in the same table row as the field description.

For the driver:

  • Read IIR exactly once per interrupt and dispatch from that value. Never re-read to decide.
  • Loop on the value you already have, servicing sources until IIR reports nothing pending — the priority encoding means one read per iteration, not one per source.
  • Be suspicious of any register read in a log statement. On a device, a read is a transaction with side effects, not an inspection.

Continue learning

Where this fits

Part of the UART curriculum.