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
| Bit | Source | Condition | Cleared by |
|---|---|---|---|
| 0 | RX_TRIG | receive queue at or above its trigger | draining the queue |
| 1 | TX_TRIG | transmit queue at or below its trigger | filling the queue |
| 2 | RX_TMO | data waiting, and the line has gone idle | draining the queue |
| 3 | ERR | any sticky error flag set | writing ERR |
| 4 | BREAK | break condition active on the line | the 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 source | Event source | |
|---|---|---|
| Example | queue above threshold | a pulse that lasts one cycle |
| Still true when the handler runs? | yes, unless serviced | no — it already happened |
| Must the hardware remember it? | no — the condition is still there to read | yes, or it is lost |
| How it is cleared | remove the cause | acknowledge it |
| Correct status register | transparent: raw & enable | latched, 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:
// 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);
endMeasured against it, two distinct failures:
Clearing without servicing never terminates.
-- 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 timesThis 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.
-- 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 latchThis 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
// 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.
-- 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 aloneThe 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.
Latched versus transparent status
7 cycles5. The Enable Mask
Three registers, and the distinction between them matters to a driver:
| Register | Shows | Purpose |
|---|---|---|
IRQ_RAW | the conditions, unmasked | the truth, always |
IRQ_EN | which ones may interrupt | policy |
IRQ_STAT | RAW & EN | what to service now |
Masking hides the interrupt and never the condition, which is what lets a polling driver work without enabling anything:
-- 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 maskedThe 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.
// 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
endThree 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.
-- 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 emptiedThe 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:
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:
| Question | If yes | If no |
|---|---|---|
| Is the condition still observable when the handler runs? | transparent, raw & en | latched, W1C |
| Would the information be lost without a latch? | latched, W1C | transparent |
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.
// ===========================================================================
// 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//===========================================================================
// 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;
endmoduleVHDL 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.
--===========================================================================
-- 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:
// 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.
//===========================================================================
// 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//===========================================================================
// 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--===========================================================================
-- 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.
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 failures10. Verification
// 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
endpropertyTest 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
Related tutorials
- Related topic
UART as a Memory-Mapped Peripheral
The register block between a bus and the UART core, and the read and write side effects that make UART registers unlike memory — with a measured demonstration of getting one of them wrong.
- Related topic
Control, Configuration, Data and Status Registers
A complete, simulated UART register map — and the reasoning behind each placement, including a parameter that turned out to belong in a register and the elaboration check that became a runtime clamp.
- Related topic
DMA Interaction and Bulk Transfer
Request and acknowledge handshaking with a DMA engine, where the residual bytes at the end of a transfer go, and a real defect that only a DMA-style reader could expose.
- Related topic
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.
Where this fits
Part of the UART curriculum.
