Skip to content
VLSI Mentor

UART · Module 9

Overrun, Underrun and Data Loss

Overrun is a protocol-level consequence of a full holding register; transmit starvation is an architecture-dependent system event. Both lose information an error flag records but cannot recover.

Chapter 9.1 and Chapter 9.2 covered frames that arrived and were wrong. This chapter covers a frame that arrived perfectly and was lost, because nothing read it in time.

That is a different kind of failure and it deserves different language. A framing error is a statement about the wire. An overrun is a statement about the system — about the relationship between how fast bytes arrive and how fast something consumes them — and the wire was blameless throughout.

Chapter 6.5 established the receiver's side of this and chose a policy. This chapter states the taxonomy properly, isolates the holding register so the policy can be tested on its own, and makes a point no error flag can make for itself:

A status bit records that data was lost. It does not tell you what was lost, and nothing can recover it.

1. Three Failures, Not One

These get lumped together as "data loss" and they have different mechanisms, different owners and different fixes.

Receive overrunTransmit starvationLoss in flight
What happensa completed byte cannot be storedno byte available when one could be senta byte was corrupted on the wire
Causethe consumer is behindthe producer is behindelectrical or timing
Detected bythe receiver, exactlythe transmitter, if it chooses toframing or parity — imperfectly
Is it a protocol event?the consequence is — a byte the far end sent is goneno — the line simply goes idleyes
Recoverable?nonothing was lostno
Ownerthis chapter, then Module 10§5 — architecture-dependent9.1 and 9.2

Only the first is an error in the sense the other chapters use the word. The second is usually not an error at all, and §5 is about why calling it one causes confusion.

2. Overrun, Defined

Against the one-entry holding register Chapter 6.5 established:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
OVERRUN occurs when a frame completes AND the holding register is
occupied AND it is not being emptied on that same clock edge.

Three conditions, all necessary. The third matters: a consumer accepting on the very cycle a new byte arrives frees the slot in time, and that is not an overrun — a distinction Chapter 6.5 §4 built into the ordering of its assignments and §4 below preserves.

The timing budget is generous and exact. At 115,200 baud an 8N1 frame is ten intervals — 86.81 µs (Chapter 4.1). That is how long the consumer has, from the moment a byte is presented, before the next one can complete. At a 100 MHz fabric clock that is 8,681 clock cycles.

A consumer missing that budget is almost never too slow to execute. It is blocked, inside a long interrupt handler, or polling on a timer slower than the line rate — which is why the overrun flag is a pointer at software scheduling rather than at the UART.

3. The Policy: Which Byte Survives

Two frames, one slot. Something is lost, and the design must choose what:

PolicyBehaviourConsequence
Overwritethe new byte replaces the held onethe byte the consumer was already told about disappears
Preserve and drop (this design)the held byte stays, the new one is discardedthe newer byte is lost

Both lose exactly one frame, so the choice is not about quantity. It is about which loss the consumer can reason about, and that is the argument Chapter 6.5 §4 made:

The held byte has already been announced. data_valid_o is high and the consumer may be partway through reading it. Replacing the data underneath produces a torn read the consumer cannot detect, and it violates the stability property that lets data and status be collected across several cycles.

Overwriting loses a byte the consumer knew about; dropping loses one it never saw. A consumer that reads one byte and is told a later one was lost has a coherent picture. A consumer that reads a byte different from the one it was offered has been misinformed.

The opposite policy is defensible for a consumer that prefers freshness to coherence — telemetry where the latest sample matters and history does not. What is not defensible is silently overwriting, which is what a design does when nobody decided.

4. The RTL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------------
//  9.3 — one-entry receive holding register.
//  POLICY: preserve the held byte, DROP the newly arrived one, flag overrun.
//  Same policy as Chapter 6.5; isolated here so it can be stated and tested.
// ---------------------------------------------------------------------------
module uart_rx_holding #(
    parameter int unsigned DATA_W = 8
) (
    input  logic              clk,
    input  logic              rst_n,
    input  logic              rx_char_valid_i,   // one cycle: a frame completed
    input  logic [DATA_W-1:0] rx_char_data_i,
    output logic [DATA_W-1:0] data_o,
    output logic              data_valid_o,
    input  logic              data_ready_i,
    output logic              overrun_evt_o      // ONE cycle: a character was dropped
);
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            data_o        <= '0;
            data_valid_o  <= 1'b0;
            overrun_evt_o <= 1'b0;
        end else begin
            overrun_evt_o <= 1'b0;

            // Accept first, so a same-cycle arrival can refill the slot.
            if (data_valid_o && data_ready_i)
                data_valid_o <= 1'b0;

            if (rx_char_valid_i) begin
                if (data_valid_o && !data_ready_i) begin
                    // Slot occupied and not being emptied this cycle: the
                    // NEW character is lost. The held one is not disturbed.
                    overrun_evt_o <= 1'b1;
                end else begin
                    data_o       <= rx_char_data_i;
                    data_valid_o <= 1'b1;
                end
            end
        end
    end
endmodule

The accept is written before the store, deliberately. Both are non-blocking assignments in the same always_ff, so the later write wins — and the later write is the store. That ordering is what makes the third condition of §2 work: a consumer accepting on the same edge that a new byte arrives frees the slot and the new byte lands, with no gap and no spurious overrun. Reversing the two would drop a byte that should have been delivered, intermittently, under exactly the traffic pattern that is hardest to reproduce.

overrun_evt_o is a one-cycle pulse, not a level — the same shape Chapter 9.1 §2 established for every error source in this module, so Chapter 9.6 can aggregate them uniformly.

The held data is not touched on the overrun path. That is the policy of §3 expressed structurally rather than by comment: the else branch is the only place data_o is written.

The hardware is DATA_W flip-flops, one valid bit, and a small amount of control. Nine flip-flops at DATA_W = 8, plus the event pulse. This is the smallest storage that makes the problem observable, and deliberately not one byte more.

The timeline, from simulation

cyclechar_validdata_ovalidreadyoverrunevent
2100000frame A completes
303C100A presented
513C100frame B completes, A still held
603C101B dropped, A untouched
903C110consumer accepts
1003C000slot free
1313C000frame C completes
1405A100C stored normally

At cycle 6 the data does not move. That single row is the policy: data_o stays at 0x3C while the overrun event fires, and frame B — a perfectly well-formed frame that arrived without a framing or parity error — is gone.

Frame B lost while frame A is held

16 cycles
A trace of sixteen fabric clock cycles from a one-entry receive holding register. At cycle two a character-valid pulse stores the byte 3C hexadecimal, which appears on the data output with valid asserted at cycle three. The consumer does not assert ready, so the byte is held. At cycle five a second character-valid pulse arrives while the register is still occupied and not being emptied, so at cycle six the overrun event pulses while the data output remains unchanged at 3C — the newly arrived byte is discarded and never appears. At cycle nine the consumer asserts ready and valid clears at cycle ten. A third character arriving at cycle thirteen is stored normally and appears as 5A hexadecimal at cycle fourteen.B completes — slot occupiedB completes — slot occupiedB DROPPED, data unchangedB DROPPED, data unchangedC stored normallyC stored normallyrx_char_valid_idata_o0000003C3C3C3C3C3C3C3C3C3C3C5A5Adata_valid_odata_ready_ioverrun_evt_ot0t1t2t3t4t5t6t7t8t9t10t11t12t13t14t15
Figure 1 — the overrun, from simulation. Columns are FABRIC CLOCK CYCLES. The held byte is visibly unchanged across the overrun event: the flat bus row through cycle 6 is the preserve-and-drop policy made observable, and frame B never appears on the output at all.
A sequence showing how a receive overrun occurs. The far end transmits the first frame on its own schedule and the receiver assembles it correctly, presenting the byte to its consumer with valid asserted. The consumer does not accept it, so the byte is held. The far end, which has no knowledge of the receiver's state, transmits a second frame. The receiver assembles that frame correctly too, and it passes its framing and parity checks, but the holding register is still occupied and is not being emptied, so the newly assembled byte is discarded and an overrun event is pulsed. The held byte is left untouched. When the consumer finally accepts, it receives the first byte and the overrun indication, and the second byte no longer exists anywhere in the design.Far endReceiverHolding regConsumerframe A — on its ownscheduleassembled, framing +parity cleandata_valid_o = 1busy — does notacceptframe B — far endknows nothingassembled, ALSOcleanslot occupied ->DROP B, keep Aoverrun_evt_o — B isgone, unrecoverable
Figure 2 — the overrun as a sequence. Time runs downward. The far end transmits on its own schedule and is never informed of anything; the consumer's failure to accept is entirely local. Frame B completes correctly on the wire and is destroyed inside the receiver, which is why the flag points at scheduling rather than at the link.

5. The Same Holding Register in Three Languages

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  uart_rx_holding_v — Synthesizable Verilog-2001
//
//  One-entry receive holding register.
//  POLICY (not a UART requirement — an architecture choice this IP makes):
//  on a collision the HELD byte is preserved and the NEWLY ARRIVED one is
//  dropped. The alternative — overwrite with the new byte — is equally
//  implementable and gives the opposite data-loss behaviour. Chapter 9.3 §4
//  argues the choice; Module 10 replaces the single slot with a FIFO.
//===========================================================================
module uart_rx_holding_v #(
    parameter DATA_W = 8
) (
    input  wire              clk,
    input  wire              rst_n,
    input  wire              rx_char_valid_i,
    input  wire [DATA_W-1:0] rx_char_data_i,
    output reg  [DATA_W-1:0] data_o,
    output reg               data_valid_o,
    input  wire              data_ready_i,
    output reg               overrun_evt_o
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            data_o        <= {DATA_W{1'b0}};
            data_valid_o  <= 1'b0;
            overrun_evt_o <= 1'b0;
        end else begin
            overrun_evt_o <= 1'b0;

            // Accept first, so a same-cycle arrival can refill the slot the
            // consumer is emptying. Ordering these two blocks the other way
            // turns a legal hand-off into a spurious overrun.
            if (data_valid_o && data_ready_i)
                data_valid_o <= 1'b0;

            if (rx_char_valid_i) begin
                if (data_valid_o && !data_ready_i) begin
                    overrun_evt_o <= 1'b1;        // the NEW character is lost
                end else begin
                    data_o       <= rx_char_data_i;
                    data_valid_o <= 1'b1;
                end
            end
        end
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--==========================================================================
--  uart_rx_holding -- Synthesizable VHDL-2008
--
--  One-entry receive holding register. POLICY: keep the held byte, drop the
--  new one, flag overrun. An architecture choice, not a UART requirement.
--
--  data_valid_o must be READ by the process (the collision test needs it),
--  and VHDL forbids reading an out port -- so the flop is an internal signal
--  with a concurrent assignment to the pin. One flop, one net, in all three
--  languages; VHDL simply names them separately.
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;

entity uart_rx_holding is
    generic (
        DATA_W : positive := 8
    );
    port (
        clk             : in  std_logic;
        rst_n           : in  std_logic;
        rx_char_valid_i : in  std_logic;
        rx_char_data_i  : in  std_logic_vector(DATA_W-1 downto 0);
        data_o          : out std_logic_vector(DATA_W-1 downto 0);
        data_valid_o    : out std_logic;
        data_ready_i    : in  std_logic;
        overrun_evt_o   : out std_logic
    );
end entity uart_rx_holding;

architecture rtl of uart_rx_holding is
    signal data_q  : std_logic_vector(DATA_W-1 downto 0);
    signal valid_q : std_logic;
begin
    data_o       <= data_q;
    data_valid_o <= valid_q;

    process (clk, rst_n)
    begin
        if rst_n = '0' then
            data_q        <= (others => '0');
            valid_q       <= '0';
            overrun_evt_o <= '0';
        elsif rising_edge(clk) then
            overrun_evt_o <= '0';

            -- Accept first, so a same-cycle arrival can refill the slot.
            if valid_q = '1' and data_ready_i = '1' then
                valid_q <= '0';
            end if;

            if rx_char_valid_i = '1' then
                if valid_q = '1' and data_ready_i = '0' then
                    overrun_evt_o <= '1';          -- the NEW character is lost
                else
                    data_q  <= rx_char_data_i;
                    valid_q <= '1';
                end if;
            end if;
        end if;
    end process;
end architecture rtl;
PropertyVerilog-2001SystemVerilogVHDL-2008Agree?
depthone characterone characterone characteryes
collision policydrop the NEW bytesamesameyes
overrun_evt_o widthone clockone clockone clockyes
accept-then-load orderingaccept firstaccept firstaccept firstyes
same-cycle hand-offaccepted, no overrunsamesameyes
reading data_valid internallyreads the regreads the logicinternal valid_qno — language rule

The one difference is VHDL's, and it is the same rule Module 7 met: a process may not read an out port, and the collision test needs the current value of data_valid. The remedy is an internal signal plus a concurrent assignment — one flip-flop and one net in all three, with VHDL naming the flop separately from the pin.

6. The Self-Checking Testbenches

INDEPENDENCE: the checker keeps its own record of what the consumer actually received and compares it against the sequence the stimulus offered minus the bytes the policy says were dropped. It never asks the DUT what the policy was.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  tb_uart_rx_holding_v — self-checking Verilog-2001 testbench
//
//  INDEPENDENCE: the checker keeps its own model of what SHOULD have reached
//  the consumer — a queue of the bytes the stimulus offered, minus the ones
//  the policy says were dropped — and compares that sequence against what
//  actually came out. It never inspects data_valid_o to decide what the
//  policy was; it decides from the stimulus it drove.
//
//  The POLICY under test (an architecture choice, not a UART rule): on a
//  collision the HELD byte survives and the NEW one is lost.
//===========================================================================
`timescale 1ns/1ps

module tb_uart_rx_holding_v;

    localparam DATA_W = 8;

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

    reg              rx_char_valid = 1'b0;
    reg  [DATA_W-1:0] rx_char_data = 8'h00;
    reg              data_ready    = 1'b0;
    wire [DATA_W-1:0] data;
    wire             data_valid, overrun_evt;

    uart_rx_holding_v #(.DATA_W(DATA_W)) dut (
        .clk(clk), .rst_n(rst_n),
        .rx_char_valid_i(rx_char_valid), .rx_char_data_i(rx_char_data),
        .data_o(data), .data_valid_o(data_valid), .data_ready_i(data_ready),
        .overrun_evt_o(overrun_evt)
    );

    integer checks = 0, failures = 0;

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

    // ---- independent observation of what the consumer actually received --
    reg [DATA_W-1:0] got [0:63];
    integer n_got = 0, n_over = 0, over_wide = 0;
    reg over_prev = 1'b0;

    always @(posedge clk) if (rst_n) begin
        if (data_valid && data_ready) begin
            got[n_got] = data;
            n_got = n_got + 1;
        end
        if (overrun_evt) begin
            n_over = n_over + 1;
            if (over_prev) over_wide = over_wide + 1;
        end
        over_prev = overrun_evt;
    end

    task offer;                 // one-cycle arrival of a character
        input [DATA_W-1:0] d;
        begin
            @(negedge clk) rx_char_data = d; rx_char_valid = 1'b1;
            @(negedge clk) rx_char_valid = 1'b0;
        end
    endtask

    // overrun_evt_o is registered, and the observer samples a registered
    // output, so an event is two active edges behind the stimulus that
    // caused it. Counts are only meaningful after this settles -- reading
    // them earlier tests the testbench's timing, not the design's.
    task settle;
        begin repeat (2) @(negedge clk); end
    endtask

    integer base_got, base_over, i;

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

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

        rst_n = 1'b0; data_ready = 1'b0;
        repeat (4) @(negedge clk);
        check(data_valid === 1'b0 && overrun_evt === 1'b0, "reset: empty, no overrun");
        @(negedge clk) rst_n = 1'b1;
        @(negedge clk);

        // ---- one character, then a consumer read --------------------------
        base_got = n_got; base_over = n_over;
        offer(8'h41);
        check(data_valid === 1'b1,   "first character: slot becomes full");
        check(data === 8'h41,        "first character: payload correct");
        check(n_over == base_over,   "first character: no overrun");
        @(negedge clk) data_ready = 1'b1;
        @(negedge clk) data_ready = 1'b0;
        @(negedge clk);
        check(data_valid === 1'b0,   "after the read: slot empties");
        check(n_got == base_got + 1, "after the read: one byte delivered");
        check(got[base_got] == 8'h41,"delivered byte is the one offered");

        // ---- THE collision: a character arrives while the slot is full ----
        base_got = n_got; base_over = n_over;
        offer(8'h42);                       // fills the slot
        offer(8'h43);                       // collides: policy drops 0x43
        settle;
        check(n_over == base_over + 1, "collision: exactly one overrun event");
        check(data_valid === 1'b1,    "collision: slot still full");
        check(data === 8'h42,         "collision: the HELD byte survives");
        @(negedge clk) data_ready = 1'b1;
        @(negedge clk) data_ready = 1'b0;
        @(negedge clk);
        check(got[n_got-1] == 8'h42,  "collision: 0x42 delivered, 0x43 lost");

        // ---- the same-cycle hand-off must NOT be an overrun ---------------
        // Slot full, consumer asserting ready, a new character arriving on
        // the same clock: the slot is emptied and refilled in one cycle.
        base_over = n_over;
        offer(8'h44);
        @(negedge clk) data_ready = 1'b1; rx_char_data = 8'h45; rx_char_valid = 1'b1;
        @(negedge clk) data_ready = 1'b0; rx_char_valid = 1'b0;
        @(negedge clk);
        check(n_over == base_over,  "same-cycle handoff raises no overrun");
        check(data_valid === 1'b1,  "same-cycle handoff: slot refilled");
        check(data === 8'h45,       "same-cycle handoff: new byte held");
        @(negedge clk) data_ready = 1'b1;
        @(negedge clk) data_ready = 1'b0;
        @(negedge clk);

        // ---- sustained traffic with a keeping-up consumer -----------------
        base_got = n_got; base_over = n_over;
        data_ready = 1'b1;
        for (i = 0; i < 8; i = i + 1) begin
            @(negedge clk) rx_char_data = 8'h50 + i[7:0]; rx_char_valid = 1'b1;
            @(negedge clk) rx_char_valid = 1'b0;
        end
        repeat (4) @(negedge clk);
        data_ready = 1'b0;
        settle;
        check(n_over == base_over,     "consumer keeping up: no overrun at all");
        check(n_got == base_got + 8,   "consumer keeping up: all 8 delivered");
        check(got[n_got-1] == 8'h57,   "last delivered byte is the last sent");

        // ---- sustained traffic with a consumer that never reads ----------
        base_got = n_got; base_over = n_over;
        for (i = 0; i < 5; i = i + 1) offer(8'h60 + i[7:0]);
        settle;
        check(n_got == base_got,       "consumer asleep: nothing delivered");
        check(n_over == base_over + 4, "consumer asleep: 4 of 5 dropped");
        check(data === 8'h60,          "consumer asleep: the FIRST byte is the one kept");

        // ---- reset while full ---------------------------------------------
        @(negedge clk) rst_n = 1'b0;
        repeat (2) @(negedge clk);
        check(data_valid === 1'b0, "reset while full: slot empties");
        @(negedge clk) rst_n = 1'b1;
        @(negedge clk);

        check(over_wide == 0, "overrun event never wider than one clock");

        $display("== %0d checks, %0d failures ==", checks, failures);
        if (failures == 0) $display("   RESULT: ALL VERILOG HOLDING TESTS PASSED");
        else               $display("   RESULT: VERILOG HOLDING TESTS FAILED");
        $finish;
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  tb_uart_rx_holding_sv — self-checking Verilog-2001 testbench
//
//  INDEPENDENCE: the checker keeps its own model of what SHOULD have reached
//  the consumer — a queue of the bytes the stimulus offered, minus the ones
//  the policy says were dropped — and compares that sequence against what
//  actually came out. It never inspects data_valid_o to decide what the
//  policy was; it decides from the stimulus it drove.
//
//  The POLICY under test (an architecture choice, not a UART rule): on a
//  collision the HELD byte survives and the NEW one is lost.
//===========================================================================
`timescale 1ns/1ps

module tb_uart_rx_holding_sv;

    localparam DATA_W = 8;

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

    logic            rx_char_valid = 1'b0;
    logic[DATA_W-1:0] rx_char_data = 8'h00;
    logic            data_ready    = 1'b0;
    logic [DATA_W-1:0] data;
    logic             data_valid, overrun_evt;

    uart_rx_holding #(.DATA_W(DATA_W)) dut (
        .clk(clk), .rst_n(rst_n),
        .rx_char_valid_i(rx_char_valid), .rx_char_data_i(rx_char_data),
        .data_o(data), .data_valid_o(data_valid), .data_ready_i(data_ready),
        .overrun_evt_o(overrun_evt)
    );

    int unsigned checks = 0, failures = 0;

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

    // ---- independent observation of what the consumer actually received --
    logic [DATA_W-1:0] got [0:63];
    int n_got = 0, n_over = 0, over_wide = 0;
    logic over_prev = 1'b0;

    always @(posedge clk) if (rst_n) begin
        if (data_valid && data_ready) begin
            got[n_got] = data;
            n_got = n_got + 1;
        end
        if (overrun_evt) begin
            n_over = n_over + 1;
            if (over_prev) over_wide = over_wide + 1;
        end
        over_prev = overrun_evt;
    end

    task offer;                 // one-cycle arrival of a character
        input [DATA_W-1:0] d;
        begin
            @(negedge clk) rx_char_data = d; rx_char_valid = 1'b1;
            @(negedge clk) rx_char_valid = 1'b0;
        end
    endtask

    // overrun_evt_o is registered, and the observer samples a registered
    // output, so an event is two active edges behind the stimulus that
    // caused it. Counts are only meaningful after this settles -- reading
    // them earlier tests the testbench's timing, not the design's.
    task settle;
        begin repeat (2) @(negedge clk); end
    endtask

    int base_got, base_over, i;

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

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

        rst_n = 1'b0; data_ready = 1'b0;
        repeat (4) @(negedge clk);
        check(data_valid === 1'b0 && overrun_evt === 1'b0, "reset: empty, no overrun");
        @(negedge clk) rst_n = 1'b1;
        @(negedge clk);

        // ---- one character, then a consumer read --------------------------
        base_got = n_got; base_over = n_over;
        offer(8'h41);
        check(data_valid === 1'b1,   "first character: slot becomes full");
        check(data === 8'h41,        "first character: payload correct");
        check(n_over == base_over,   "first character: no overrun");
        @(negedge clk) data_ready = 1'b1;
        @(negedge clk) data_ready = 1'b0;
        @(negedge clk);
        check(data_valid === 1'b0,   "after the read: slot empties");
        check(n_got == base_got + 1, "after the read: one byte delivered");
        check(got[base_got] == 8'h41,"delivered byte is the one offered");

        // ---- THE collision: a character arrives while the slot is full ----
        base_got = n_got; base_over = n_over;
        offer(8'h42);                       // fills the slot
        offer(8'h43);                       // collides: policy drops 0x43
        settle();
        check(n_over == base_over + 1, "collision: exactly one overrun event");
        check(data_valid === 1'b1,    "collision: slot still full");
        check(data === 8'h42,         "collision: the HELD byte survives");
        @(negedge clk) data_ready = 1'b1;
        @(negedge clk) data_ready = 1'b0;
        @(negedge clk);
        check(got[n_got-1] == 8'h42,  "collision: 0x42 delivered, 0x43 lost");

        // ---- the same-cycle hand-off must NOT be an overrun ---------------
        // Slot full, consumer asserting ready, a new character arriving on
        // the same clock: the slot is emptied and refilled in one cycle.
        base_over = n_over;
        offer(8'h44);
        @(negedge clk) data_ready = 1'b1; rx_char_data = 8'h45; rx_char_valid = 1'b1;
        @(negedge clk) data_ready = 1'b0; rx_char_valid = 1'b0;
        @(negedge clk);
        check(n_over == base_over,  "same-cycle handoff raises no overrun");
        check(data_valid === 1'b1,  "same-cycle handoff: slot refilled");
        check(data === 8'h45,       "same-cycle handoff: new byte held");
        @(negedge clk) data_ready = 1'b1;
        @(negedge clk) data_ready = 1'b0;
        @(negedge clk);

        // ---- sustained traffic with a keeping-up consumer -----------------
        base_got = n_got; base_over = n_over;
        data_ready = 1'b1;
        for (i = 0; i < 8; i = i + 1) begin
            @(negedge clk) rx_char_data = 8'h50 + i[7:0]; rx_char_valid = 1'b1;
            @(negedge clk) rx_char_valid = 1'b0;
        end
        repeat (4) @(negedge clk);
        data_ready = 1'b0;
        settle();
        check(n_over == base_over,     "consumer keeping up: no overrun at all");
        check(n_got == base_got + 8,   "consumer keeping up: all 8 delivered");
        check(got[n_got-1] == 8'h57,   "last delivered byte is the last sent");

        // ---- sustained traffic with a consumer that never reads ----------
        base_got = n_got; base_over = n_over;
        for (i = 0; i < 5; i = i + 1) offer(8'h60 + i[7:0]);
        settle();
        check(n_got == base_got,       "consumer asleep: nothing delivered");
        check(n_over == base_over + 4, "consumer asleep: 4 of 5 dropped");
        check(data === 8'h60,          "consumer asleep: the FIRST byte is the one kept");

        // ---- reset while full ---------------------------------------------
        @(negedge clk) rst_n = 1'b0;
        repeat (2) @(negedge clk);
        check(data_valid === 1'b0, "reset while full: slot empties");
        @(negedge clk) rst_n = 1'b1;
        @(negedge clk);

        check(over_wide == 0, "overrun event never wider than one clock");

        $display("== %0d checks, %0d failures ==", checks, failures);
        if (failures == 0) $display("   RESULT: ALL SYSTEMVERILOG HOLDING TESTS PASSED");
        else               $display("   RESULT: SYSTEMVERILOG HOLDING TESTS FAILED");
        $finish;
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--==========================================================================
--  tb_uart_rx_holding -- self-checking VHDL-2008 testbench
--
--  Same 22 checks as the Verilog and SystemVerilog testbenches. The checker
--  records what the CONSUMER actually received and compares it against the
--  sequence the stimulus offered minus the bytes the policy drops; it never
--  decides the policy by looking at the DUT.
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.env.finish;

entity tb_uart_rx_holding is
end entity tb_uart_rx_holding;

architecture sim of tb_uart_rx_holding is

    constant DATA_W     : positive := 8;
    constant CLK_PERIOD : time     := 10 ns;

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

    signal rx_char_valid : std_logic := '0';
    signal rx_char_data  : std_logic_vector(DATA_W-1 downto 0) := (others => '0');
    signal data_ready    : std_logic := '0';
    signal data          : std_logic_vector(DATA_W-1 downto 0);
    signal data_valid    : std_logic;
    signal overrun_evt   : std_logic;

    type byte_arr is array (0 to 63) of std_logic_vector(DATA_W-1 downto 0);
    signal got       : byte_arr := (others => (others => '0'));
    signal n_got     : natural  := 0;
    signal n_over    : natural  := 0;
    signal over_wide : natural  := 0;

begin

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

    dut : entity work.uart_rx_holding
        generic map (DATA_W => DATA_W)
        port map (clk => clk, rst_n => rst_n,
                  rx_char_valid_i => rx_char_valid, rx_char_data_i => rx_char_data,
                  data_o => data, data_valid_o => data_valid,
                  data_ready_i => data_ready, overrun_evt_o => overrun_evt);

    obs : process (clk)
        variable g, o, w : natural := 0;
        variable prev    : std_logic := '0';
        variable arr     : byte_arr := (others => (others => '0'));
    begin
        if rising_edge(clk) and rst_n = '1' then
            if data_valid = '1' and data_ready = '1' then
                arr(g) := data;
                g := g + 1;
            end if;
            if overrun_evt = '1' then
                o := o + 1;
                if prev = '1' then w := w + 1; end if;
            end if;
            prev      := overrun_evt;
            got       <= arr;
            n_got     <= g;
            n_over    <= o;
            over_wide <= w;
        end if;
    end process obs;

    watchdog : process
    begin
        wait for 300 us;
        report "watchdog: simulation did not finish" severity error;
        report "   RESULT: VHDL HOLDING TESTS FAILED (timeout)" severity failure;
        wait;
    end process watchdog;

    stim : process
        variable checks   : natural := 0;
        variable failures : natural := 0;
        variable base_got, base_over : natural;

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

        procedure offer(d : std_logic_vector) is
        begin
            wait until falling_edge(clk); rx_char_data <= d; rx_char_valid <= '1';
            wait until falling_edge(clk); rx_char_valid <= '0';
        end procedure;

        -- overrun_evt_o is registered and the observer samples a registered
        -- output, so counts are two active edges behind their stimulus.
        procedure settle is
        begin
            for i in 0 to 1 loop wait until falling_edge(clk); end loop;
        end procedure;
    begin
        report "== uart_rx_holding (VHDL) : self-checking testbench ==";

        rst_n <= '0'; data_ready <= '0';
        for i in 0 to 3 loop wait until falling_edge(clk); end loop;
        check(data_valid = '0' and overrun_evt = '0', "reset: empty, no overrun");
        rst_n <= '1';
        wait until falling_edge(clk);

        base_got := n_got; base_over := n_over;
        offer(x"41");
        check(data_valid = '1',      "first character: slot becomes full");
        check(data = x"41",          "first character: payload correct");
        check(n_over = base_over,    "first character: no overrun");
        wait until falling_edge(clk); data_ready <= '1';
        wait until falling_edge(clk); data_ready <= '0';
        wait until falling_edge(clk);
        check(data_valid = '0',      "after the read: slot empties");
        check(n_got = base_got + 1,  "after the read: one byte delivered");
        check(got(base_got) = x"41", "delivered byte is the one offered");

        base_got := n_got; base_over := n_over;
        offer(x"42");
        offer(x"43");
        settle;
        check(n_over = base_over + 1, "collision: exactly one overrun event");
        check(data_valid = '1',       "collision: slot still full");
        check(data = x"42",           "collision: the HELD byte survives");
        wait until falling_edge(clk); data_ready <= '1';
        wait until falling_edge(clk); data_ready <= '0';
        wait until falling_edge(clk);
        check(got(n_got-1) = x"42",   "collision: 0x42 delivered, 0x43 lost");

        base_over := n_over;
        offer(x"44");
        wait until falling_edge(clk); data_ready <= '1'; rx_char_data <= x"45"; rx_char_valid <= '1';
        wait until falling_edge(clk); data_ready <= '0'; rx_char_valid <= '0';
        wait until falling_edge(clk);
        check(n_over = base_over,   "same-cycle handoff raises no overrun");
        check(data_valid = '1',     "same-cycle handoff: slot refilled");
        check(data = x"45",         "same-cycle handoff: new byte held");
        wait until falling_edge(clk); data_ready <= '1';
        wait until falling_edge(clk); data_ready <= '0';
        wait until falling_edge(clk);

        base_got := n_got; base_over := n_over;
        data_ready <= '1';
        for i in 0 to 7 loop
            wait until falling_edge(clk);
            rx_char_data <= std_logic_vector(to_unsigned(16#50# + i, DATA_W));
            rx_char_valid <= '1';
            wait until falling_edge(clk); rx_char_valid <= '0';
        end loop;
        for i in 0 to 3 loop wait until falling_edge(clk); end loop;
        data_ready <= '0';
        settle;
        check(n_over = base_over,      "consumer keeping up: no overrun at all");
        check(n_got = base_got + 8,    "consumer keeping up: all 8 delivered");
        check(got(n_got-1) = x"57",    "last delivered byte is the last sent");

        base_got := n_got; base_over := n_over;
        for i in 0 to 4 loop
            offer(std_logic_vector(to_unsigned(16#60# + i, DATA_W)));
        end loop;
        settle;
        check(n_got = base_got,        "consumer asleep: nothing delivered");
        check(n_over = base_over + 4,  "consumer asleep: 4 of 5 dropped");
        check(data = x"60",            "consumer asleep: the FIRST byte is the one kept");

        wait until falling_edge(clk); rst_n <= '0';
        for i in 0 to 1 loop wait until falling_edge(clk); end loop;
        check(data_valid = '0', "reset while full: slot empties");
        wait until falling_edge(clk); rst_n <= '1';
        wait until falling_edge(clk);

        check(over_wide = 0, "overrun event never wider than one clock");

        report "== " & integer'image(checks) & " checks, " &
               integer'image(failures) & " failures ==";
        if failures = 0 then
            report "   RESULT: ALL VHDL HOLDING TESTS PASSED";
        else
            report "   RESULT: VHDL HOLDING TESTS FAILED" severity error;
        end if;

        done <= '1';
        finish;
    end process stim;

end architecture sim;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ iverilog -g2001 -o sim uart_rx_holding_v.v tb_uart_rx_holding_v.v && ./sim
  PASS first character: slot becomes full
  PASS collision: exactly one overrun event
  PASS collision: the HELD byte survives
  PASS collision: 0x42 delivered, 0x43 lost
  PASS same-cycle handoff raises no overrun
  PASS consumer keeping up: no overrun at all
  PASS consumer keeping up: all 8 delivered
  PASS consumer asleep: 4 of 5 dropped
  PASS consumer asleep: the FIRST byte is the one kept
  PASS reset while full: slot empties
  PASS overrun event never wider than one clock
== 22 checks, 0 failures ==
   RESULT: ALL VERILOG HOLDING TESTS PASSED

22 checks in each language, 0 failures.

The "4 of 5 dropped" check is the policy stated as a number. Five characters arrive at a consumer that never reads; the first is kept and four are lost, so there are four overrun events and the byte still in the slot is the first one. Under the opposite policy the same stimulus would leave the last byte and still report four overruns — the event count alone cannot tell the two policies apart, which is why the testbench checks the surviving data as well.

Proving these checks can fail

MutationWhat it breaksFailures
collision overwrites the held byte§3's policy3 / 22

Only three checks fail, and that is the correct shape rather than a weakness: the inverted policy still reports the right number of overruns and still delivers the right count of bytes. What changes is which bytes survive, so the checks that fail are precisely the three that inspect payload identity. A testbench that only counted events would have passed a design with the opposite data-loss behaviour.

7. Underrun Is Not a UART Error

This is where the taxonomy earns its place, because "underrun" is applied to two very different things.

On the transmit side of a basic UART, running out of data is not an error at all. Chapter 7.4 built a transmitter that asserts tx_ready_o when it can accept a byte; if the producer offers none, the transmitter finishes the frame in flight, returns to idle, and holds the line at mark. That is the correct steady state of an idle link. Nothing was lost, nothing was corrupted, and the far end sees exactly what a healthy quiet line looks like.

Calling that an underrun creates a false alarm on every link that is not saturated — which is most of them.

A starvation event becomes meaningful only when a system contract says the stream must be continuous. A DMA-fed transmitter that is supposed to deliver a fixed-rate stream, or a protocol layer above UART that requires back-to-back frames, has such a contract — and then "the producer failed to supply a byte before the transmitter needed one" is a real, reportable system event.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
protocol violation      : a byte the far end sent was destroyed  (receive overrun)
system availability     : a byte our own producer failed to supply (transmit starvation)

The first is about the link. The second is about us. They deserve different names, different status bits and different responses, and a design that reports both through one "underrun/overrun" pair will confuse the two in the field.

Where it does apply, the smallest honest definition is: the transmitter reached a point where it could have begun a frame, the system contract said one should have been available, and none was. That requires the contract to exist — which is why this cannot be a universal UART status bit, and why the base transmitter of Module 7 has none.

8. A Flag Cannot Recover the Data

Worth stating plainly because the presence of a status bit invites the opposite assumption.

overrun_evt_o records that a byte was lost. It does not record which byte, how many, or anything about their content — and no amount of status can, because the information was never stored. Frame B in §4's trace existed on the wire, was assembled correctly, passed its framing and parity checks, and was then discarded because there was nowhere to put it.

The consequences follow directly:

A byte stream with an overrun has a hole of unknown size. If the consumer stalls for three frame times, three bytes are lost and the flag looks identical to a single loss. A counter distinguishes those and is strictly more useful than a flag — one reason Chapter 9.6 treats aggregation as a design decision rather than a formality.

A protocol above UART cannot resynchronise from a byte stream with silent holes unless it carries its own framing. This is the practical argument for message-level structure — length fields, delimiters, sequence numbers — on any UART link that matters, and it is the same argument Chapter 6.2 §7 made when a mid-frame reset fabricated a byte.

Buffering moves the threshold; it does not remove it. Chapter 6.5 §7 made this point and it is worth repeating with numbers: one register overruns when the consumer is more than 86.81 µs behind; a sixteen-deep FIFO overruns when it is more than 1.39 ms behind. If the consumer's average rate is below the line rate, every depth eventually overflows — buffering converts a hard real-time requirement into an average-rate requirement, which is a genuine and large improvement and is not a solution. Module 10 chooses the depth with the facts that decide it.

9. Verification

The positive control is the test that matters most here. An overrun detector that fires on every character passes every negative test:

StimulusExpectedResult
consumer ready, one characterno overrunpass
consumer stalled, first characterheld, valid assertedpass
consumer stalled, second characteroverrun eventpass
— same case, data outputunchanged — held byte preservedpass
consumer acceptsslot freed, valid clearspass

The fourth row is the policy under test, not the flag. Asserting that overrun_evt_o rose proves something was noticed; asserting that data_o did not change proves which byte survived, which is the design decision §3 made.

Sweep the consumer's latency across a frame time. Accepting at 0, half, one and two frame times after valid covers the comfortable case, the marginal case, the first overrun and sustained loss. The boundary at exactly one frame time is where a design with extra delivery latency begins failing.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Assertion — an overrun is reported ONLY when the slot was genuinely
// unavailable. This is the positive control expressed formally.
property p_overrun_only_when_blocked;
    @(posedge clk) disable iff (!rst_n)
        overrun_evt_o |-> $past(rx_char_valid_i && data_valid_o && !data_ready_i);
endproperty
assert property (p_overrun_only_when_blocked);

// Assertion — the held byte is never disturbed by an overrun.
// This is the retention policy, stated as a property.
property p_overrun_preserves_data;
    @(posedge clk) disable iff (!rst_n)
        overrun_evt_o |-> $stable(data_o);
endproperty
assert property (p_overrun_preserves_data);

// Assertion — a character arriving into a free slot is always stored.
// Nothing accepted may be silently dropped.
property p_free_slot_always_stores;
    @(posedge clk) disable iff (!rst_n)
        (rx_char_valid_i && (!data_valid_o || data_ready_i)) |=> data_valid_o;
endproperty
assert property (p_free_slot_always_stores);

The third is the one that catches an over-eager overrun condition: a detector that treats any arrival while data_valid_o is high as an overrun drops the same-cycle-accept case, and this property fails on it immediately.

10. Debugging

11. What This Means on an FPGA

The block is nine flip-flops plus control, and the overrun detection is three AND terms. There is no version of this that is too expensive to include.

Probe data_valid_o and data_ready_i together. Valid high and never falling is a consumer that is not accepting — immediately distinguishable from every line-side fault. Adding overrun_evt_o to the capture says whether data has already been lost.

Bring out a saturating overrun counter during bring-up. Four bits is enough to tell "one hiccup" from "continuously losing", and it costs nothing next to a UART.

Consider where the consumer actually is. If the consuming logic is software, the 86.81 µs budget is an interrupt-latency requirement and belongs in the system's timing analysis rather than the UART's. If it is hardware, it is usually a handshake that stalls — and a hardware consumer that cannot accept within thousands of cycles is doing something that deserves examination on its own.

12. Understanding Check

13. Summary

Three failures, not one. A receive overrun destroys a byte the far end really sent; transmit starvation is usually not an error at all; loss in flight is what 9.1 and 9.2 cover.

Overrun requires three conditions: a frame completes, the register is occupied, and it is not being emptied on that edge. The third makes a same-cycle accept legal, and a detector that omits it drops bytes under prompt back-to-back traffic.

The deadline is one frame time — 86.81 µs at 115,200 8N1, or 8,681 cycles at 100 MHz. A consumer missing that is blocked, not slow.

This design preserves the held byte and drops the new one, because the held byte was already announced and overwriting it produces an undetectable torn read. Simulation shows data_o flat at 0x3C across the overrun event while frame B disappears entirely.

Underrun is not a universal UART error. A transmitter with nothing to send goes idle, which is correct. Starvation is meaningful only where a system contract demands a continuous stream — a statement about our producer, not about the link.

A flag records that data was lost and cannot recover it, or even count it. A byte stream with silent holes of unknown size is why a UART link carrying structured data needs its own framing above the byte layer.

Buffering moves the threshold: one register tolerates 86.81 µs, sixteen entries tolerate 1.39 ms, and no depth survives a consumer whose average rate is below the line rate.

14. What Comes Next

Every failure so far has been a frame that went wrong. The next one is a line that stops carrying frames at all.

Chapter 9.4 takes the break condition: a line held at the start-bit level past a full frame. It is the one condition that cannot be defined by a single sample — a start bit is also low — so detection requires duration, which means a counter, a threshold derived from the frame configuration, and a saturation policy so a long break cannot wrap and un-declare itself. It also separates a deliberately generated break from a stuck wire, which look identical to the receiver and are not the same problem.

Browse the full path on the UART tutorials index. For the receiver interface this chapter isolates, read back to Chapter 6.5.

Continue learning

Where this fits

Part of the UART curriculum.