Skip to content
VLSI Mentor

UART · Module 17

FIFO Overrun and Flow-Control Failures

Three scenarios with the identical symptom and three different fixes, an instrumented FIFO in three HDLs that classifies each lost byte, and the headroom arithmetic that explains why RTS often fails on a USB-serial bridge.

An overrun bit tells you a byte was lost. It does not tell you why, and the three common causes have three different fixes — one in software, one in a configuration constant, one in the wiring. Acting on the wrong one is the usual outcome, because all three present identically: bytes go missing under load.

This chapter instruments a receive FIFO so that each lost byte is classified at the moment it is lost, then runs three scenarios that produce the same symptom from different causes and shows the instrument separating them.

1. Loss After Successful Reception

Everything in the three preceding chapters was about bytes that arrived wrong. This chapter is about bytes that arrived right and then vanished, which is a different failure with a different investigation.

The receiver assembled the frame, validated the stop bit, and pushed the byte into a FIFO. From that moment the wire is irrelevant. The byte is lost because the FIFO had no room, and the FIFO had no room for one of three reasons:

CauseWhat is actually wrongThe fix
Service latencysoftware or DMA is not draining fast enoughreduce interrupt latency, enlarge the FIFO, or use DMA
Insufficient headroomflow-off was asserted too late to take effectlower the threshold
Flow control ignoredthe remote never stopswiring, polarity, or remote configuration

The second and third are the ones that get confused, because in both cases flow control is implemented and bytes are still being lost. The distinction is whether the remote had time to react.

2. Flow Control Is a Round Trip, Not a Signal

When a receiver de-asserts RTS it is not stopping the remote. It is requesting that the remote stop, and the request takes time to have an effect:

  • the RTS level has to propagate down the cable,
  • the remote has to notice it — which may mean an interrupt, a driver, a USB transaction, or a polling loop,
  • and any byte already loaded into the remote's shift register will be transmitted regardless.

During that window the remote keeps sending, entirely legitimately. Those bytes are in flight and cannot be recalled. If the FIFO has no room for them, they are lost — and no amount of correctness in the flow-control logic prevents it, because the loss happened during the interval when the remote had not yet been told.

So the threshold is not a comfort setting. It is a deadline:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    headroom needed  =  bytes the remote can send during one round trip
                     =  reaction time / byte time

3. The Instrumented FIFO

The block below is an ordinary receive FIFO with a flow-control threshold, plus the instrumentation that makes the distinction of §1 observable. It times how long flow-off has been asserted at the moment each byte is dropped, and compares that against REACT — the round-trip budget the design was built for.

Verilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------------
// uart_overrun_probe -- an RX FIFO with flow control, instrumented to say WHY
// a byte was lost.
//
// "Bytes are going missing" has at least two causes with completely different
// fixes, and a plain overrun bit cannot tell them apart:
//
//   * IN-FLIGHT loss. Flow-off was asserted, but the remote had already put
//     bytes on the wire and cannot recall them. The FIFO overflowed while the
//     remote was still, legitimately, reacting. The fix is to assert flow-off
//     EARLIER -- lower THRESH so the headroom covers the round trip.
//
//   * IGNORED flow control. Flow-off was asserted long enough ago that the
//     remote must have seen it, and bytes kept coming. No threshold will fix
//     that; the RTS wire, its polarity, or the remote's configuration is wrong.
//
// The probe separates them by timing how long flow-off had been asserted when
// each byte was dropped, and comparing that against REACT -- the round-trip
// reaction time the design is budgeted for.
//
// hwm_o is reported because a design that never drops is not necessarily safe:
// it may simply never have been pushed hard enough. The high-water mark is the
// margin you actually measured.
// ---------------------------------------------------------------------------
module uart_overrun_probe #(
    parameter DEPTH  = 8,            // FIFO depth, in bytes
    parameter THRESH = 8,            // de-assert "ready" at this level
    parameter REACT  = 40            // clocks the remote is allowed to react in
)(
    input  wire        clk,
    input  wire        rst_n,
    input  wire        push_i,       // a byte arrived off the wire
    input  wire        pop_i,        // software read a byte

    output reg  [7:0]  level_o,
    output reg  [7:0]  hwm_o,        // deepest the FIFO ever got
    output wire        rts_rdy_o,    // 1 = the remote may send
    output reg  [15:0] n_push_o,     // bytes accepted
    output reg  [15:0] n_pop_o,      // bytes delivered to software
    output reg  [15:0] n_drop_inflight_o, // lost while the remote was reacting
    output reg  [15:0] n_drop_ignored_o,  // lost after it had time to stop
    output reg  [15:0] n_rts_off_o        // times flow-off was asserted
);

    // A byte is accepted if there is room, OR if software is freeing a slot on
    // this very clock. Getting this rule wrong in either direction produces a
    // probe that disagrees with its own FIFO.
    wire pop_ok  = pop_i  && (level_o != 8'd0);
    wire push_ok = push_i && ((level_o < DEPTH) || pop_ok);
    wire dropped = push_i && !push_ok;

    assign rts_rdy_o = (level_o < THRESH);

    reg [15:0] off_age;              // clocks since flow-off was asserted
    reg        rts_q;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            level_o           <= 8'd0;
            hwm_o             <= 8'd0;
            n_push_o          <= 16'd0;
            n_pop_o           <= 16'd0;
            n_drop_inflight_o <= 16'd0;
            n_drop_ignored_o  <= 16'd0;
            n_rts_off_o       <= 16'd0;
            off_age           <= 16'd0;
            rts_q             <= 1'b1;
        end else begin
            rts_q <= rts_rdy_o;

            // ---- how long has the remote been told to stop? -------------
            if (rts_rdy_o) begin
                off_age <= 16'd0;
            end else begin
                if (off_age != 16'hFFFF) off_age <= off_age + 16'd1;
                if (rts_q) n_rts_off_o <= n_rts_off_o + 16'd1;   // falling edge
            end

            // ---- the FIFO ------------------------------------------------
            if (push_ok && !pop_ok)      level_o <= level_o + 8'd1;
            else if (!push_ok && pop_ok) level_o <= level_o - 8'd1;

            if (push_ok && !pop_ok && (level_o + 8'd1) > hwm_o)
                hwm_o <= level_o + 8'd1;

            if (push_ok) n_push_o <= n_push_o + 16'd1;
            if (pop_ok)  n_pop_o  <= n_pop_o  + 16'd1;

            // ---- and the verdict on anything lost ------------------------
            if (dropped) begin
                if (off_age < REACT) n_drop_inflight_o <= n_drop_inflight_o + 16'd1;
                else                 n_drop_ignored_o  <= n_drop_ignored_o  + 16'd1;
            end
        end
    end

endmodule

SystemVerilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------------
// uart_overrun_probe -- an RX FIFO with flow control, instrumented to say WHY
// a byte was lost.
//
// "Bytes are going missing" has at least two causes with completely different
// fixes, and a plain overrun bit cannot tell them apart:
//
//   * IN-FLIGHT loss. Flow-off was asserted, but the remote had already put
//     bytes on the wire and cannot recall them. The FIFO overflowed while the
//     remote was still, legitimately, reacting. The fix is to assert flow-off
//     EARLIER -- lower THRESH so the headroom covers the round trip.
//
//   * IGNORED flow control. Flow-off was asserted long enough ago that the
//     remote must have seen it, and bytes kept coming. No threshold will fix
//     that; the RTS wire, its polarity, or the remote's configuration is wrong.
//
// The probe separates them by timing how long flow-off had been asserted when
// each byte was dropped, and comparing that against REACT -- the round-trip
// reaction time the design is budgeted for.
//
// hwm_o is reported because a design that never drops is not necessarily safe:
// it may simply never have been pushed hard enough. The high-water mark is the
// margin you actually measured.
// ---------------------------------------------------------------------------
module uart_overrun_probe #(
    parameter int DEPTH  = 8,            // FIFO depth, in bytes
    parameter int THRESH = 8,            // de-assert "ready" at this level
    parameter int REACT  = 40            // clocks the remote is allowed to react in
)(
    input  logic       clk,
    input  logic       rst_n,
    input  logic       push_i,       // a byte arrived off the wire
    input  logic       pop_i,        // software read a byte

    output logic [7:0] level_o,
    output logic [7:0] hwm_o,        // deepest the FIFO ever got
    output logic       rts_rdy_o,    // 1 = the remote may send
    output logic [15:0] n_push_o,     // bytes accepted
    output logic [15:0] n_pop_o,      // bytes delivered to software
    output logic [15:0] n_drop_inflight_o, // lost while the remote was reacting
    output logic [15:0] n_drop_ignored_o,  // lost after it had time to stop
    output logic [15:0] n_rts_off_o        // times flow-off was asserted
);

    // A byte is accepted if there is room, OR if software is freeing a slot on
    // this very clock. Getting this rule wrong in either direction produces a
    // probe that disagrees with its own FIFO.
    wire pop_ok  = pop_i  && (level_o != 8'd0);
    wire push_ok = push_i && ((level_o < DEPTH) || pop_ok);
    wire dropped = push_i && !push_ok;

    assign rts_rdy_o = (level_o < THRESH);

    logic [15:0] off_age;              // clocks since flow-off was asserted
    logic      rts_q;

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            level_o           <= 8'd0;
            hwm_o             <= 8'd0;
            n_push_o          <= 16'd0;
            n_pop_o           <= 16'd0;
            n_drop_inflight_o <= 16'd0;
            n_drop_ignored_o  <= 16'd0;
            n_rts_off_o       <= 16'd0;
            off_age           <= 16'd0;
            rts_q             <= 1'b1;
        end else begin
            rts_q <= rts_rdy_o;

            // ---- how long has the remote been told to stop? -------------
            if (rts_rdy_o) begin
                off_age <= 16'd0;
            end else begin
                if (off_age != 16'hFFFF) off_age <= off_age + 16'd1;
                if (rts_q) n_rts_off_o <= n_rts_off_o + 16'd1;   // falling edge
            end

            // ---- the FIFO ------------------------------------------------
            if (push_ok && !pop_ok)      level_o <= level_o + 8'd1;
            else if (!push_ok && pop_ok) level_o <= level_o - 8'd1;

            if (push_ok && !pop_ok && (level_o + 8'd1) > hwm_o)
                hwm_o <= level_o + 8'd1;

            if (push_ok) n_push_o <= n_push_o + 16'd1;
            if (pop_ok)  n_pop_o  <= n_pop_o  + 16'd1;

            // ---- and the verdict on anything lost ------------------------
            if (dropped) begin
                if (off_age < REACT) n_drop_inflight_o <= n_drop_inflight_o + 16'd1;
                else                 n_drop_ignored_o  <= n_drop_ignored_o  + 16'd1;
            end
        end
    end

endmodule

VHDL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- ---------------------------------------------------------------------------
-- uart_overrun_probe -- an RX FIFO with flow control, instrumented to say WHY
-- a byte was lost.
--
-- "Bytes are going missing" has at least two causes with completely different
-- fixes, and a plain overrun bit cannot tell them apart:
--
--   * IN-FLIGHT loss. Flow-off was asserted, but the remote had already put
--     bytes on the wire and cannot recall them. The FIFO overflowed while the
--     remote was still, legitimately, reacting. The fix is to assert flow-off
--     EARLIER -- lower THRESH so the headroom covers the round trip.
--
--   * IGNORED flow control. Flow-off was asserted long enough ago that the
--     remote must have seen it, and bytes kept coming. No threshold will fix
--     that; the RTS wire, its polarity, or the remote's configuration is wrong.
--
-- The probe separates them by timing how long flow-off had been asserted when
-- each byte was dropped, and comparing that against REACT -- the round-trip
-- reaction time the design is budgeted for.
--
-- hwm_o is reported because a design that never drops is not necessarily safe:
-- it may simply never have been pushed hard enough. The high-water mark is the
-- margin you actually measured.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity uart_overrun_probe is
    generic (
        DEPTH  : natural := 8;                 -- FIFO depth, in bytes
        THRESH : natural := 8;                 -- de-assert "ready" at this level
        REACT  : natural := 40                 -- clocks the remote may react in
    );
    port (
        clk               : in  std_logic;
        rst_n             : in  std_logic;
        push_i            : in  std_logic;     -- a byte arrived off the wire
        pop_i             : in  std_logic;     -- software read a byte

        level_o           : out unsigned(7 downto 0);
        hwm_o             : out unsigned(7 downto 0);   -- deepest it ever got
        rts_rdy_o         : out std_logic;              -- '1' = remote may send
        n_push_o          : out unsigned(15 downto 0);  -- bytes accepted
        n_pop_o           : out unsigned(15 downto 0);  -- bytes delivered
        n_drop_inflight_o : out unsigned(15 downto 0);  -- lost while reacting
        n_drop_ignored_o  : out unsigned(15 downto 0);  -- lost after time to stop
        n_rts_off_o       : out unsigned(15 downto 0)   -- times flow-off asserted
    );
end entity uart_overrun_probe;

architecture rtl of uart_overrun_probe is

    signal level    : unsigned(7 downto 0)  := (others => '0');
    signal hwm      : unsigned(7 downto 0)  := (others => '0');
    signal n_push   : unsigned(15 downto 0) := (others => '0');
    signal n_pop    : unsigned(15 downto 0) := (others => '0');
    signal n_infl   : unsigned(15 downto 0) := (others => '0');
    signal n_ign    : unsigned(15 downto 0) := (others => '0');
    signal n_off    : unsigned(15 downto 0) := (others => '0');
    signal off_age  : unsigned(15 downto 0) := (others => '0');
    signal rts_q    : std_logic := '1';

    signal rts_rdy  : std_logic;
    signal pop_ok   : std_logic;
    signal push_ok  : std_logic;
    signal dropped  : std_logic;

begin

    rts_rdy <= '1' when level < to_unsigned(THRESH, 8) else '0';

    -- A byte is accepted if there is room, OR if software is freeing a slot on
    -- this very clock. Getting this rule wrong in either direction produces a
    -- probe that disagrees with its own FIFO.
    pop_ok  <= '1' when (pop_i = '1' and level /= 0) else '0';
    push_ok <= '1' when (push_i = '1' and
                         (level < to_unsigned(DEPTH, 8) or pop_ok = '1')) else '0';
    dropped <= '1' when (push_i = '1' and push_ok = '0') else '0';

    level_o           <= level;
    hwm_o             <= hwm;
    rts_rdy_o         <= rts_rdy;
    n_push_o          <= n_push;
    n_pop_o           <= n_pop;
    n_drop_inflight_o <= n_infl;
    n_drop_ignored_o  <= n_ign;
    n_rts_off_o       <= n_off;

    process (clk, rst_n)
    begin
        if rst_n = '0' then
            level   <= (others => '0'); hwm     <= (others => '0');
            n_push  <= (others => '0'); n_pop   <= (others => '0');
            n_infl  <= (others => '0'); n_ign   <= (others => '0');
            n_off   <= (others => '0'); off_age <= (others => '0');
            rts_q   <= '1';
        elsif rising_edge(clk) then
            rts_q <= rts_rdy;

            -- ---- how long has the remote been told to stop? -------------
            if rts_rdy = '1' then
                off_age <= (others => '0');
            else
                if off_age /= x"FFFF" then
                    off_age <= off_age + 1;
                end if;
                if rts_q = '1' then                     -- falling edge
                    n_off <= n_off + 1;
                end if;
            end if;

            -- ---- the FIFO ------------------------------------------------
            if push_ok = '1' and pop_ok = '0' then
                level <= level + 1;
            elsif push_ok = '0' and pop_ok = '1' then
                level <= level - 1;
            end if;

            if push_ok = '1' and pop_ok = '0' and (level + 1) > hwm then
                hwm <= level + 1;
            end if;

            if push_ok = '1' then n_push <= n_push + 1; end if;
            if pop_ok  = '1' then n_pop  <= n_pop  + 1; end if;

            -- ---- and the verdict on anything lost ------------------------
            if dropped = '1' then
                if off_age < to_unsigned(REACT, 16) then
                    n_infl <= n_infl + 1;
                else
                    n_ign <= n_ign + 1;
                end if;
            end if;
        end if;
    end process;

end architecture rtl;

Two details in that listing decide whether the probe agrees with its own FIFO.

The acceptance rule is push_i && ((level < DEPTH) || pop_ok). A byte is accepted if there is room or if software is freeing a slot on the same clock. Getting this wrong in either direction produces an instrument that disagrees with the storage it is instrumenting — reporting drops that did not happen, or missing drops that did. Mutation M11 removes the same-clock term and the tests catch it.

And hwm_o records the deepest the FIFO ever got. It is there because a design that never drops is not automatically a design with margin:

4. Three Scenarios, One Symptom

The testbench models the remote's reaction as a real round trip: it observes the flow-control signal through a shift register delayed by LAT clocks, so "bytes already on the wire" are physically present in the model rather than assumed. Then it runs the same traffic under three configurations.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  scenario                          hwm   flow-off   in-flight   ignored
  -------------------------------   ---   --------   ---------   -------
  T1  drain faster than arrival       1          0           0         0
  A   THRESH=8, remote honours        8          6          24         0
  B   THRESH=4, remote honours        8          2           0         0
  C   THRESH=8, remote ignores        8          1           4       103

Scenario A — no headroom. The threshold equals the FIFO depth, so flow-off is asserted only when the FIFO is already full. Every byte the remote sends during its reaction window arrives at a full FIFO. Twenty-four bytes lost, all of them classified in-flight, and the diagnosis is unambiguous: the threshold is too high.

Scenario A — four bytes lost while the remote is still reacting

14 cycles
A timing trace of a receive FIFO overrunning under flow control, drawn with one cycle per byte time. The byte in row shows a byte arriving on each of the first twelve byte times. The FIFO level row climbs from one to eight and then stays at eight. The ready row falls at the moment the level reaches eight, because the threshold equals the depth and there is no headroom. The remote sees row shows the same signal delayed by four byte times, which is the round trip of the link, so the remote continues sending throughout that window. The dropped row shows four bytes lost, one on each byte time during which the FIFO was full and the remote had not yet seen the request to stop. Once the remote sees the flow off request it stops, and no further bytes are lost.round trip — remote still sendinground trip — remote still sendingfour bytes lost in flightfour bytes lost in flightflow-off assertedflow-off assertedremote finally sees itremote finally sees itbyte infifo level12345678888888readyremote seesdroppedt0t1t2t3t4t5t6t7t8t9t10t11t12t13

Scenario B — the fix, demonstrated. Identical traffic, identical remote, identical reaction latency. The only change is THRESH from 8 to 4, giving four bytes of headroom against a reaction window that admits four bytes. Zero losses. This is not an argument that lowering the threshold would help; it is the same experiment with the threshold lowered.

Scenario C — the remote is not listening. Back to no headroom, but now the remote ignores flow control entirely. A handful of drops are classified in-flight — those happened in the first few clocks after flow-off, before the reaction budget expired, and are genuinely indistinguishable from scenario A at that instant. Then the budget expires and 103 further drops are classified ignored. The ratio is the signature: when ignored dominates, no threshold will help, because the remote is not reacting to the threshold at all.

5. Service Latency: the Third Cause

T1 is the case where flow control never engages at all. Bytes arrive every 40 clocks and software drains one every 20, so the level never approaches the threshold, hwm_o reads 1, and the flow-off counter reads zero.

That is what a comfortably provisioned link looks like, and the numbers say so in a way that "no overruns" does not. A link that never asserts flow-off is not relying on flow control, which means its margin is set entirely by service latency — and the high-water mark is the measurement of that margin.

The general relationship, worth committing to memory:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  flow-off count = 0, hwm low        ->  service is comfortably ahead
  flow-off count = 0, hwm near DEPTH ->  no margin; the next latency spike drops bytes
  flow-off engaging, in-flight drops ->  threshold too high for the round trip
  flow-off engaging, ignored drops   ->  the remote is not honouring it
  flow-off engaging, no drops        ->  working as designed

The second row is the dangerous one, because it reports zero errors right up until it does not. An interrupt that is usually serviced in 10 microseconds and occasionally in 500 — because a higher-priority handler ran, or a cache missed, or a debugger paused the core — produces exactly that profile.

6. The Testbench

Verilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`timescale 1ns/1ps
// ---------------------------------------------------------------------------
// Testbench for uart_overrun_probe.
//
// Three scenarios produce the SAME symptom -- bytes going missing -- from three
// different causes. The probe is only useful if it tells them apart:
//
//   A  zero headroom (THRESH = DEPTH), remote honours flow control
//        -> loss is IN-FLIGHT. Fix: assert flow-off earlier.
//   B  the same traffic with headroom (THRESH = 4)
//        -> no loss at all. That is scenario A's fix, demonstrated.
//   C  zero headroom, remote IGNORES flow control
//        -> loss is IGNORED. No threshold can fix it.
//
// The remote's reaction is modelled as a real round trip: it sees rts_rdy_o
// LAT clocks late, through a shift register, so "bytes already on the wire"
// are physically present in the model rather than assumed.
// ---------------------------------------------------------------------------
module tb_uart_overrun_probe;

    localparam DEPTH = 8;
    localparam REACT = 40;
    localparam LAT   = 40;           // remote's round-trip reaction, in clocks

    reg clk = 1'b0;
    reg rst_n = 1'b0;
    reg push = 1'b0, pop = 1'b0;
    reg sel = 1'b0;                  // 0 = watch dutA, 1 = watch dutB

    integer checks = 0;
    integer fails  = 0;

    always #5 clk = ~clk;

    // ---- A: no headroom at all ----
    wire [7:0]  levA, hwmA;
    wire        rtsA;
    wire [15:0] pushA, popA, inflA, ignA, offA;
    uart_overrun_probe #(.DEPTH(DEPTH), .THRESH(8), .REACT(REACT)) dutA (
        .clk(clk), .rst_n(rst_n), .push_i(push), .pop_i(pop),
        .level_o(levA), .hwm_o(hwmA), .rts_rdy_o(rtsA),
        .n_push_o(pushA), .n_pop_o(popA),
        .n_drop_inflight_o(inflA), .n_drop_ignored_o(ignA), .n_rts_off_o(offA));

    // ---- B: four bytes of headroom ----
    wire [7:0]  levB, hwmB;
    wire        rtsB;
    wire [15:0] pushB, popB, inflB, ignB, offB;
    uart_overrun_probe #(.DEPTH(DEPTH), .THRESH(4), .REACT(REACT)) dutB (
        .clk(clk), .rst_n(rst_n), .push_i(push), .pop_i(pop),
        .level_o(levB), .hwm_o(hwmB), .rts_rdy_o(rtsB),
        .n_push_o(pushB), .n_pop_o(popB),
        .n_drop_inflight_o(inflB), .n_drop_ignored_o(ignB), .n_rts_off_o(offB));

    // ---- the remote sees our flow control LAT clocks late ----
    wire        rts_sel = sel ? rtsB : rtsA;
    reg  [63:0] rts_pipe;
    wire        remote_sees = rts_pipe[LAT-1];

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) rts_pipe <= {64{1'b1}};
        else        rts_pipe <= {rts_pipe[62:0], rts_sel};
    end

    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 %0d expected %0d", name, got, exp);
            end
        end
    endtask

    task do_reset;
        begin
            push = 1'b0; pop = 1'b0; rst_n = 1'b0;
            repeat (4) @(posedge clk);
            @(negedge clk); rst_n = 1'b1;
            repeat (2) @(posedge clk);
        end
    endtask

    // Run `n` clocks of traffic. A byte is offered every `pp` clocks, software
    // reads one every `sp` clocks. If `honour` the remote stops sending while
    // it can see flow-off.
    task run_traffic;
        input integer n;
        input integer pp;
        input integer sp;
        input         honour;
        integer t;
        begin
            for (t = 0; t < n; t = t + 1) begin
                @(negedge clk);
                push = (pp != 0) && (t % pp == 0) && (!honour || remote_sees);
                pop  = (sp != 0) && (t % sp == 0);
                @(posedge clk);
            end
            @(negedge clk); push = 1'b0; pop = 1'b0;
            @(posedge clk);
        end
    endtask

    initial begin
        // ---------------- T1: comfortable traffic, nothing lost -----------
        do_reset; sel = 1'b0;
        run_traffic(2000, 40, 20, 1'b1);
        #1;
        $display("T1 drain faster than arrival : push=%0d pop=%0d hwm=%0d drops=%0d",
                 pushA, popA, hwmA, inflA + ignA);
        chk("T1 nothing was dropped", inflA + ignA, 0);
        chk("T1 flow control never engaged", offA, 0);
        chk("T1 the margin was measured", (hwmA <= 2) ? 1 : 0, 1);

        // ---------------- A: zero headroom, remote behaves ----------------
        do_reset; sel = 1'b0;
        run_traffic(1200, 10, 200, 1'b1);
        #1;
        $display("A  THRESH=8 honouring       : hwm=%0d flow-off=%0d  in-flight=%0d ignored=%0d",
                 hwmA, offA, inflA, ignA);
        chk("A bytes were lost",                (inflA > 0) ? 1 : 0, 1);
        chk("A every loss was in-flight",       ignA, 0);
        chk("A flow control did engage",        (offA > 0) ? 1 : 0, 1);
        chk("A the FIFO did reach its depth",   hwmA, DEPTH);

        // ---------------- B: the same traffic, with headroom --------------
        do_reset; sel = 1'b1;
        run_traffic(1200, 10, 200, 1'b1);
        #1;
        $display("B  THRESH=4 honouring       : hwm=%0d flow-off=%0d  in-flight=%0d ignored=%0d",
                 hwmB, offB, inflB, ignB);
        chk("B nothing was lost",            inflB + ignB, 0);
        chk("B flow control still engaged",  (offB > 0) ? 1 : 0, 1);
        chk("B headroom was used, not wasted", (hwmB > 4) ? 1 : 0, 1);

        // ---------------- C: the remote is not listening ------------------
        do_reset; sel = 1'b0;
        run_traffic(1200, 10, 200, 1'b0);
        #1;
        $display("C  THRESH=8 ignoring        : hwm=%0d flow-off=%0d  in-flight=%0d ignored=%0d",
                 hwmA, offA, inflA, ignA);
        chk("C bytes were lost",                    (ignA > 0) ? 1 : 0, 1);
        chk("C the loss is dominated by ignored",   (ignA > inflA) ? 1 : 0, 1);

        // ---------------- T5: a pop on the same clock frees a slot --------
        do_reset; sel = 1'b0;
        run_traffic(200, 10, 0, 1'b0);       // fill it solid, no service
        #1;
        chk("T5 the FIFO is full", levA, DEPTH);
        begin : simul
            integer drops_before;
            drops_before = inflA + ignA;
            @(negedge clk); push = 1'b1; pop = 1'b1;
            @(posedge clk);
            @(negedge clk); push = 1'b0; pop = 1'b0;
            @(posedge clk); #1;
            $display("T5 push+pop while full      : level=%0d drops added=%0d",
                     levA, (inflA + ignA) - drops_before);
            chk("T5 a simultaneous pop makes room", (inflA + ignA) - drops_before, 0);
            chk("T5 the level is unchanged",        levA, DEPTH);
        end

        // ---------------- T6: popping an empty FIFO is harmless -----------
        do_reset; sel = 1'b0;
        run_traffic(200, 0, 10, 1'b0);       // service an empty FIFO
        #1;
        chk("T6 the level did not underflow", levA, 0);
        chk("T6 nothing was delivered",       popA, 0);

        $display("");
        $display("== %0d checks, %0d failures ==", checks, fails);
        if (fails == 0) $display("   RESULT: ALL VERILOG OVERRUN-PROBE 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_overrun_probe.
//
// Three scenarios produce the SAME symptom -- bytes going missing -- from three
// different causes. The probe is only useful if it tells them apart:
//
//   A  zero headroom (THRESH = DEPTH), remote honours flow control
//        -> loss is IN-FLIGHT. Fix: assert flow-off earlier.
//   B  the same traffic with headroom (THRESH = 4)
//        -> no loss at all. That is scenario A's fix, demonstrated.
//   C  zero headroom, remote IGNORES flow control
//        -> loss is IGNORED. No threshold can fix it.
//
// The remote's reaction is modelled as a real round trip: it sees rts_rdy_o
// LAT clocks late, through a shift register, so "bytes already on the wire"
// are physically present in the model rather than assumed.
// ---------------------------------------------------------------------------
module tb_uart_overrun_probe;

    localparam DEPTH = 8;
    localparam REACT = 40;
    localparam LAT   = 40;           // remote's round-trip reaction, in clocks

    logic clk = 1'b0;
    logic rst_n = 1'b0;
    logic push = 1'b0, pop = 1'b0;
    logic sel = 1'b0;                  // 0 = watch dutA, 1 = watch dutB

    integer checks = 0;
    integer fails  = 0;

    always #5 clk = ~clk;

    // ---- A: no headroom at all ----
    logic [7:0]  levA, hwmA;
    logic       rtsA;
    logic [15:0] pushA, popA, inflA, ignA, offA;
    uart_overrun_probe #(.DEPTH(DEPTH), .THRESH(8), .REACT(REACT)) dutA (
        .clk(clk), .rst_n(rst_n), .push_i(push), .pop_i(pop),
        .level_o(levA), .hwm_o(hwmA), .rts_rdy_o(rtsA),
        .n_push_o(pushA), .n_pop_o(popA),
        .n_drop_inflight_o(inflA), .n_drop_ignored_o(ignA), .n_rts_off_o(offA));

    // ---- B: four bytes of headroom ----
    logic [7:0]  levB, hwmB;
    logic       rtsB;
    logic [15:0] pushB, popB, inflB, ignB, offB;
    uart_overrun_probe #(.DEPTH(DEPTH), .THRESH(4), .REACT(REACT)) dutB (
        .clk(clk), .rst_n(rst_n), .push_i(push), .pop_i(pop),
        .level_o(levB), .hwm_o(hwmB), .rts_rdy_o(rtsB),
        .n_push_o(pushB), .n_pop_o(popB),
        .n_drop_inflight_o(inflB), .n_drop_ignored_o(ignB), .n_rts_off_o(offB));

    // ---- the remote sees our flow control LAT clocks late ----
    wire        rts_sel = sel ? rtsB : rtsA;
    logic [63:0] rts_pipe;
    wire        remote_sees = rts_pipe[LAT-1];

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) rts_pipe <= {64{1'b1}};
        else        rts_pipe <= {rts_pipe[62:0], rts_sel};
    end

    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 %0d expected %0d", name, got, exp);
            end
        end
    endtask

    task automatic do_reset();
        begin
            push = 1'b0; pop = 1'b0; rst_n = 1'b0;
            repeat (4) @(posedge clk);
            @(negedge clk); rst_n = 1'b1;
            repeat (2) @(posedge clk);
        end
    endtask

    // Run `n` clocks of traffic. A byte is offered every `pp` clocks, software
    // reads one every `sp` clocks. If `honour` the remote stops sending while
    // it can see flow-off.
    task automatic run_traffic(input int n, input int pp, input int sp,
                               input logic honour);
        int t;
        begin
            for (t = 0; t < n; t = t + 1) begin
                @(negedge clk);
                push = (pp != 0) && (t % pp == 0) && (!honour || remote_sees);
                pop  = (sp != 0) && (t % sp == 0);
                @(posedge clk);
            end
            @(negedge clk); push = 1'b0; pop = 1'b0;
            @(posedge clk);
        end
    endtask

    initial begin
        // ---------------- T1: comfortable traffic, nothing lost -----------
        do_reset; sel = 1'b0;
        run_traffic(2000, 40, 20, 1'b1);
        #1;
        $display("T1 drain faster than arrival : push=%0d pop=%0d hwm=%0d drops=%0d",
                 pushA, popA, hwmA, inflA + ignA);
        chk("T1 nothing was dropped", inflA + ignA, 0);
        chk("T1 flow control never engaged", offA, 0);
        chk("T1 the margin was measured", (hwmA <= 2) ? 1 : 0, 1);

        // ---------------- A: zero headroom, remote behaves ----------------
        do_reset; sel = 1'b0;
        run_traffic(1200, 10, 200, 1'b1);
        #1;
        $display("A  THRESH=8 honouring       : hwm=%0d flow-off=%0d  in-flight=%0d ignored=%0d",
                 hwmA, offA, inflA, ignA);
        chk("A bytes were lost",                (inflA > 0) ? 1 : 0, 1);
        chk("A every loss was in-flight",       ignA, 0);
        chk("A flow control did engage",        (offA > 0) ? 1 : 0, 1);
        chk("A the FIFO did reach its depth",   hwmA, DEPTH);

        // ---------------- B: the same traffic, with headroom --------------
        do_reset; sel = 1'b1;
        run_traffic(1200, 10, 200, 1'b1);
        #1;
        $display("B  THRESH=4 honouring       : hwm=%0d flow-off=%0d  in-flight=%0d ignored=%0d",
                 hwmB, offB, inflB, ignB);
        chk("B nothing was lost",            inflB + ignB, 0);
        chk("B flow control still engaged",  (offB > 0) ? 1 : 0, 1);
        chk("B headroom was used, not wasted", (hwmB > 4) ? 1 : 0, 1);

        // ---------------- C: the remote is not listening ------------------
        do_reset; sel = 1'b0;
        run_traffic(1200, 10, 200, 1'b0);
        #1;
        $display("C  THRESH=8 ignoring        : hwm=%0d flow-off=%0d  in-flight=%0d ignored=%0d",
                 hwmA, offA, inflA, ignA);
        chk("C bytes were lost",                    (ignA > 0) ? 1 : 0, 1);
        chk("C the loss is dominated by ignored",   (ignA > inflA) ? 1 : 0, 1);

        // ---------------- T5: a pop on the same clock frees a slot --------
        do_reset; sel = 1'b0;
        run_traffic(200, 10, 0, 1'b0);       // fill it solid, no service
        #1;
        chk("T5 the FIFO is full", levA, DEPTH);
        begin : simul
            integer drops_before;
            drops_before = inflA + ignA;
            @(negedge clk); push = 1'b1; pop = 1'b1;
            @(posedge clk);
            @(negedge clk); push = 1'b0; pop = 1'b0;
            @(posedge clk); #1;
            $display("T5 push+pop while full      : level=%0d drops added=%0d",
                     levA, (inflA + ignA) - drops_before);
            chk("T5 a simultaneous pop makes room", (inflA + ignA) - drops_before, 0);
            chk("T5 the level is unchanged",        levA, DEPTH);
        end

        // ---------------- T6: popping an empty FIFO is harmless -----------
        do_reset; sel = 1'b0;
        run_traffic(200, 0, 10, 1'b0);       // service an empty FIFO
        #1;
        chk("T6 the level did not underflow", levA, 0);
        chk("T6 nothing was delivered",       popA, 0);

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

endmodule

VHDL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- ---------------------------------------------------------------------------
-- Testbench for uart_overrun_probe.
--
-- Three scenarios produce the SAME symptom -- bytes going missing -- from three
-- different causes. The probe is only useful if it tells them apart:
--
--   A  zero headroom (THRESH = DEPTH), remote honours flow control
--        -> loss is IN-FLIGHT. Fix: assert flow-off earlier.
--   B  the same traffic with headroom (THRESH = 4)
--        -> no loss at all. That is scenario A's fix, demonstrated.
--   C  zero headroom, remote IGNORES flow control
--        -> loss is IGNORED. No threshold can fix it.
--
-- The remote's reaction is modelled as a real round trip: it sees rts_rdy_o
-- LAT clocks late, through a shift register, so "bytes already on the wire"
-- are physically present in the model rather than assumed.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity tb_uart_overrun_probe is
end entity tb_uart_overrun_probe;

architecture sim of tb_uart_overrun_probe is

    constant DEPTH : natural := 8;
    constant REACT : natural := 40;
    constant LAT   : natural := 40;            -- remote's round trip, in clocks
    constant TCLK  : time    := 10 ns;

    signal clk   : std_logic := '0';
    signal rst_n : std_logic := '0';
    signal push  : std_logic := '0';
    signal pop   : std_logic := '0';
    signal sel   : std_logic := '0';           -- '0' = watch dutA, '1' = dutB
    signal done  : boolean   := false;

    signal levA, hwmA : unsigned(7 downto 0);
    signal rtsA       : std_logic;
    signal pushA, popA, inflA, ignA, offA : unsigned(15 downto 0);

    signal levB, hwmB : unsigned(7 downto 0);
    signal rtsB       : std_logic;
    signal pushB, popB, inflB, ignB, offB : unsigned(15 downto 0);

    signal rts_sel     : std_logic;
    signal rts_pipe    : std_logic_vector(63 downto 0) := (others => '1');
    signal remote_sees : std_logic;

begin

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

    -- ---- A: no headroom at all ----
    dutA : entity work.uart_overrun_probe
        generic map (DEPTH => DEPTH, THRESH => 8, REACT => REACT)
        port map (clk => clk, rst_n => rst_n, push_i => push, pop_i => pop,
                  level_o => levA, hwm_o => hwmA, rts_rdy_o => rtsA,
                  n_push_o => pushA, n_pop_o => popA,
                  n_drop_inflight_o => inflA, n_drop_ignored_o => ignA,
                  n_rts_off_o => offA);

    -- ---- B: four bytes of headroom ----
    dutB : entity work.uart_overrun_probe
        generic map (DEPTH => DEPTH, THRESH => 4, REACT => REACT)
        port map (clk => clk, rst_n => rst_n, push_i => push, pop_i => pop,
                  level_o => levB, hwm_o => hwmB, rts_rdy_o => rtsB,
                  n_push_o => pushB, n_pop_o => popB,
                  n_drop_inflight_o => inflB, n_drop_ignored_o => ignB,
                  n_rts_off_o => offB);

    -- ---- the remote sees our flow control LAT clocks late ----
    rts_sel     <= rtsB when sel = '1' else rtsA;
    remote_sees <= rts_pipe(LAT-1);

    process (clk, rst_n)
    begin
        if rst_n = '0' then
            rts_pipe <= (others => '1');
        elsif rising_edge(clk) then
            rts_pipe <= rts_pipe(62 downto 0) & rts_sel;
        end if;
    end process;

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

        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
            push <= '0'; pop <= '0'; rst_n <= '0';
            for i in 0 to 3 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;

        -- Run `n` clocks of traffic. A byte is offered every `pp` clocks,
        -- software reads one every `sp` clocks. If `honour` the remote stops
        -- sending while it can see flow-off.
        procedure run_traffic (n : integer; pp : integer; sp : integer;
                               honour : boolean) is
        begin
            for t in 0 to n-1 loop
                wait until falling_edge(clk);
                if pp /= 0 and (t mod pp) = 0 and
                   ((not honour) or remote_sees = '1') then
                    push <= '1';
                else
                    push <= '0';
                end if;
                if sp /= 0 and (t mod sp) = 0 then
                    pop <= '1';
                else
                    pop <= '0';
                end if;
                wait until rising_edge(clk);
            end loop;
            wait until falling_edge(clk); push <= '0'; pop <= '0';
            wait until rising_edge(clk);
        end procedure;

    begin
        -- ---------------- T1: comfortable traffic, nothing lost -----------
        do_reset; sel <= '0';
        run_traffic(2000, 40, 20, true);
        wait for 1 ns;
        report "T1 drain faster than arrival : push=" & integer'image(to_integer(pushA)) &
               " pop=" & integer'image(to_integer(popA)) &
               " hwm=" & integer'image(to_integer(hwmA)) &
               " drops=" & integer'image(to_integer(inflA) + to_integer(ignA));
        chk("T1 nothing was dropped", to_integer(inflA) + to_integer(ignA), 0);
        chk("T1 flow control never engaged", to_integer(offA), 0);
        if hwmA <= 2 then chk("T1 the margin was measured", 1, 1);
        else              chk("T1 the margin was measured", 0, 1); end if;

        -- ---------------- A: zero headroom, remote behaves ----------------
        do_reset; sel <= '0';
        run_traffic(1200, 10, 200, true);
        wait for 1 ns;
        report "A  THRESH=8 honouring       : hwm=" & integer'image(to_integer(hwmA)) &
               " flow-off=" & integer'image(to_integer(offA)) &
               "  in-flight=" & integer'image(to_integer(inflA)) &
               " ignored=" & integer'image(to_integer(ignA));
        if inflA > 0 then chk("A bytes were lost", 1, 1);
        else              chk("A bytes were lost", 0, 1); end if;
        chk("A every loss was in-flight", to_integer(ignA), 0);
        if offA > 0 then chk("A flow control did engage", 1, 1);
        else             chk("A flow control did engage", 0, 1); end if;
        chk("A the FIFO did reach its depth", to_integer(hwmA), DEPTH);

        -- ---------------- B: the same traffic, with headroom --------------
        do_reset; sel <= '1';
        run_traffic(1200, 10, 200, true);
        wait for 1 ns;
        report "B  THRESH=4 honouring       : hwm=" & integer'image(to_integer(hwmB)) &
               " flow-off=" & integer'image(to_integer(offB)) &
               "  in-flight=" & integer'image(to_integer(inflB)) &
               " ignored=" & integer'image(to_integer(ignB));
        chk("B nothing was lost", to_integer(inflB) + to_integer(ignB), 0);
        if offB > 0 then chk("B flow control still engaged", 1, 1);
        else             chk("B flow control still engaged", 0, 1); end if;
        if hwmB > 4 then chk("B headroom was used, not wasted", 1, 1);
        else             chk("B headroom was used, not wasted", 0, 1); end if;

        -- ---------------- C: the remote is not listening ------------------
        do_reset; sel <= '0';
        run_traffic(1200, 10, 200, false);
        wait for 1 ns;
        report "C  THRESH=8 ignoring        : hwm=" & integer'image(to_integer(hwmA)) &
               " flow-off=" & integer'image(to_integer(offA)) &
               "  in-flight=" & integer'image(to_integer(inflA)) &
               " ignored=" & integer'image(to_integer(ignA));
        if ignA > 0 then chk("C bytes were lost", 1, 1);
        else             chk("C bytes were lost", 0, 1); end if;
        if ignA > inflA then chk("C the loss is dominated by ignored", 1, 1);
        else                 chk("C the loss is dominated by ignored", 0, 1); end if;

        -- ---------------- T5: a pop on the same clock frees a slot --------
        do_reset; sel <= '0';
        run_traffic(200, 10, 0, false);        -- fill it solid, no service
        wait for 1 ns;
        chk("T5 the FIFO is full", to_integer(levA), DEPTH);
        drops_before := to_integer(inflA) + to_integer(ignA);
        wait until falling_edge(clk); push <= '1'; pop <= '1';
        wait until rising_edge(clk);
        wait until falling_edge(clk); push <= '0'; pop <= '0';
        wait until rising_edge(clk); wait for 1 ns;
        report "T5 push+pop while full      : level=" & integer'image(to_integer(levA)) &
               " drops added=" &
               integer'image(to_integer(inflA) + to_integer(ignA) - drops_before);
        chk("T5 a simultaneous pop makes room",
            to_integer(inflA) + to_integer(ignA) - drops_before, 0);
        chk("T5 the level is unchanged", to_integer(levA), DEPTH);

        -- ---------------- T6: popping an empty FIFO is harmless -----------
        do_reset; sel <= '0';
        run_traffic(200, 0, 10, false);        -- service an empty FIFO
        wait for 1 ns;
        chk("T6 the level did not underflow", to_integer(levA), 0);
        chk("T6 nothing was delivered",       to_integer(popA), 0);

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

end architecture sim;

T5 and T6 are the boundary cases that keep the FIFO honest: a push and a pop on the same clock while full must not drop anything and must not change the level, and a pop from an empty FIFO must not underflow the level counter into a large positive number.

7. Proving the Tests Can Fail

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  mutation                                               checks failed   verdict
  ----------------------------------------------------   -------------   -------
  M11  drop the same-clock pop from the acceptance rule               2    killed
  M12  invert the in-flight / ignored test                            3    killed
  M13  never reset the reaction clock                                 3    killed

M13 deserves a note because it is the subtlest of the three and the easiest to write by accident. If off_age is never cleared when flow control re-asserts, it grows monotonically for the whole run, and after the first REACT clocks every subsequent drop is classified as ignored. Scenario A, which is a pure threshold problem, would be reported as a remote that is not honouring flow control — sending the investigation to the wiring instead of to a configuration constant.

That is a failure mode worth dwelling on: the instrument still reports drops, still reports the right count of drops, and is confidently wrong about the only thing it was built to determine.

8. Reading the Counters on a Real System

Given a link that is losing bytes, these five registers answer the question in one read:

  1. Drops, in-flight versus ignored. If ignored dominates, stop looking at thresholds — the remote is not reacting and the fault is in the RTS path or the remote's configuration.
  2. Flow-off assertion count. Zero, with drops occurring, means flow control never engaged: either it is disabled, or the threshold is above the depth, or the loss is pure service latency.
  3. High-water mark. The measured margin. Near the depth means the next latency spike is a drop, whether or not anything has dropped yet.
  4. Accepted versus delivered counts. Their difference is what is currently sitting in the FIFO; a delivered count that stops advancing while the accepted count climbs is a stalled consumer, not a wire problem.
  5. The threshold, against the arithmetic in §2. Compute the bytes in flight for the actual link — and for a USB bridge, use a millisecond, not a microsecond.

The first and third are the two that are usually missing from real debug registers, and they are the two that resolve the most cases.

Continue learning

Where this fits

Part of the UART curriculum.