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:
| Cause | What is actually wrong | The fix |
|---|---|---|
| Service latency | software or DMA is not draining fast enough | reduce interrupt latency, enlarge the FIFO, or use DMA |
| Insufficient headroom | flow-off was asserted too late to take effect | lower the threshold |
| Flow control ignored | the remote never stops | wiring, 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:
headroom needed = bytes the remote can send during one round trip
= reaction time / byte time3. 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
// ---------------------------------------------------------------------------
// 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
endmoduleSystemVerilog
// ---------------------------------------------------------------------------
// 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
endmoduleVHDL
-- ---------------------------------------------------------------------------
-- 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.
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 103Scenario 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 cyclesScenario 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:
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 designedThe 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
`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
endmoduleSystemVerilog
`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
endmoduleVHDL
-- ---------------------------------------------------------------------------
-- 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
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 killedM13 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:
- 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.
- 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.
- High-water mark. The measured margin. Near the depth means the next latency spike is a drop, whether or not anything has dropped yet.
- 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.
- 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
Related tutorials
- Related topic
Reading a UART Waveform as Evidence
Separating what a capture observes from what it implies, the run-length structure that makes a bit period measurable, a measuring block in three HDLs, and the ordinary payload that makes it report nine times the right answer.
- Related topic
Baud Mismatch and Sampling-Error Signatures
Why sampling error accumulates across a frame and corrupts the high bits first, the arithmetic that fixes the tolerance at 5.26 percent, and the measured drift table for five receiver dividers sharing one wire.
- Related topic
Bit Order, Parity and Framing Failure Signatures
Why 0xA5 cannot detect a reversed byte, why parity is structurally blind to bit order, why one frame can never separate a parity misconfiguration from noise, and a classifier in three HDLs that resolves all four faults.
- Related topic
Missing Start Bits, False Starts and Noise
A naive start detector and a majority-vote qualifier racing on one wire, a rejection boundary measured rather than assumed, and the counter that separates a transmitter that never sent from a receiver that never listened.
Where this fits
Part of the UART curriculum.
