UART · Module 10
Overflow, Overrun and Backpressure
At the boundary the buffer is full and the line does not stop. Two events describe the same loss at different points, and the ready signal that backpressures a local producer has no path to the far end.
Chapter 10.3 built the warning. This chapter is about what happens when the warning is not enough — because the consumer is genuinely too slow, because the burst was larger than any headroom, or because nobody chose the threshold from a latency budget.
Two things need to be precise, and they are usually blurred together.
Which byte is lost. The FIFO is full and a character completes. Something must go, and which thing determines whether the consumer ends up with a coherent picture or a misleading one.
Why ready cannot help. A full FIFO drives ready low, which stops a local producer immediately and completely. It has no effect whatsoever on the far end, and understanding exactly why is what motivates the two chapters after this one:
ready = !fullis a wire between two blocks in the same design. The transmitter that is about to overrun you is in a different device, on the other end of a cable, and has never heard of it.
1. Terminology, Stated
Datasheets use overflow and overrun inconsistently — sometimes interchangeably, sometimes for different events, occasionally with the senses reversed. The model used throughout Module 10:
| Term | Meaning here | Produced by |
|---|---|---|
| FIFO overflow | a push was requested and not accepted | overflow_evt_o, Chapter 10.2 |
| RX overrun | a completed character could not be stored anywhere | the receive path as a whole |
| FIFO underflow | a pop was requested and not accepted | underflow_evt_o |
In a buffered receiver these are two views of one event. The character completes, the receiver tries to hand it to the FIFO, the FIFO is full and rejects the push — so overflow_evt_o fires. The same instant, a byte the far end really transmitted has been destroyed, which is an overrun in Chapter 9.3's sense.
They are not redundant. overflow_evt_o is a statement about the queue and is useful for sizing and instrumentation; the overrun is a statement about the link and is what software needs to know. A design that reports only the first tells software the buffer was busy; a design that reports only the second loses the ability to distinguish a queue that is merely full from one that is chronically undersized.
2. Exactly When Overflow Occurs
From Chapter 10.2 §4, the acceptance condition is:
push_fire = push_i && (!full_o || pop_fire)so the overflow event is its complement:
overflow_evt_o = push_i && !push_fire
= push_i && full_o && !pop_fireThe !pop_fire term is the one that matters. A full FIFO with a consumer popping on the same edge accepts the push — the slot is free by the end of the cycle — and no overflow is reported, correctly. A design that defines overflow as push_i && full_o reports a loss that did not happen, on every cycle where producer and consumer coincide at the boundary, which is the steady state under sustained load.
This is the same boundary Chapter 10.2 §6 argued for on the acceptance side, and the two must agree: if the push is accepted, there is no overflow; if it is rejected, there is. Deriving one from the other rather than writing them independently is what keeps them consistent.
3. Which Byte Is Lost
The FIFO holds DEPTH bytes and a new one arrives. Two policies:
| Policy | Behaviour | What the consumer sees |
|---|---|---|
| Drop newest (this design) | queue untouched, arriving byte discarded | a contiguous run, then a gap |
| Overwrite oldest | the oldest queued byte is replaced | a gap inside what looked like a contiguous run |
This design drops the newest, and it is the same policy the one-entry receiver of Chapter 9.3 §3 chose, for a reason that scales:
The queued bytes are already committed. Everything in the FIFO has been accepted, ordered, and — in the RX case — paired with its per-character status. Overwriting the oldest destroys a byte the consumer was going to receive next, and it does so silently within the ordered stream, so the consumer reads a sequence that looks contiguous and is not.
Dropping the newest keeps the invariant that survives. What the consumer reads is a correct, in-order prefix followed by a reported gap. That is a picture a protocol layer can reason about; the alternative is not.
Overwrite-oldest is defensible where freshness beats history — a telemetry stream where the latest sample matters and older ones are worthless. The requirement is that it be chosen, because a design that overwrites by accident looks identical from the outside to one that chose to.
Two characters rejected at a full FIFO
8 cycles4. Underflow Is a Different Kind of Statement
underflow_evt_o fires when a pop is requested on an empty FIFO. Unlike overflow, nothing is lost — the consumer asked for a byte that does not exist, and was correctly given nothing.
On the RX side it usually indicates a consumer bug: software reading a data register without first checking that data is available. It is worth reporting for exactly that reason, and it is diagnostic rather than a link event.
On the TX side it is not an error at all, and this is where Chapter 9.3 §5's distinction matters. A transmit engine that pops an empty FIFO has simply run out of data to send; the correct response is to finish the frame in flight and let the line go idle, which is the steady state of a link with nothing to say. Reporting that as an error produces an alarm on every link that is not saturated.
RX underflow : the consumer read something that was not there -> consumer bug
TX underflow : there was nothing to send -> normal idleThe same FIFO signal, two meanings, determined entirely by which side the engine is on. A UART IP that wires both to one "underflow" status bit conflates a software defect with ordinary quiet operation. The base FIFO produces the event; deciding what it means belongs to whatever instantiates it.
5. Backpressure, and Exactly Where It Stops
A full FIFO can stop its local producer completely:
// Synthesizable SystemVerilog — local backpressure. This works, and its
// reach is precisely the boundary of the design.
assign producer_ready = !fifo_full;For the TX FIFO that is the whole story. The producer is software or a bus master inside the same design; ready low stalls it, nothing is lost, and the mechanism is complete. A TX FIFO genuinely cannot overflow unless its producer ignores ready — which is a protocol violation on a local interface, not a link event.
For the RX FIFO it is not the whole story at all, and the gap is the entire reason Module 10 continues past this chapter.
The asymmetry is worth stating plainly, because it is the module's pivot:
| TX FIFO | RX FIFO | |
|---|---|---|
| Producer | local — software or a bus | remote — another device |
ready reaches it? | yes, completely | no, not at all |
| Can overflow? | only if ready is violated | yes, whenever the consumer is late |
| Remedy | none needed | flow control — 10.5, 10.6 |
A TX FIFO's backpressure is a solved problem. An RX FIFO's is not, and no amount of internal signalling changes that: the producer is on the other side of a cable, and the only way to influence it is to send it something it can observe.
6. Verification
The positive control is the same-cycle pop. A design that reports overflow whenever push_i && full_o passes every test that pushes into a full FIFO with an idle consumer, and is wrong exactly when the consumer is active — the case that matters.
From this module's simulation of the FIFO contract:
| Stimulus | Expected | Result |
|---|---|---|
fill to DEPTH, then push | overflow event, level unchanged | pass |
| full + simultaneous push and pop | both accepted, no overflow | pass |
| pop from empty | underflow event | pass |
| empty + simultaneous push and pop | push accepted, underflow reported | pass |
| randomised traffic, six configurations | events fire only on rejected requests | pass |
The overflow and underflow counts in the randomised run were non-zero in every configuration — 19 to 82 overflows and 85 to 316 underflows depending on depth — which confirms the boundaries were genuinely exercised rather than avoided.
// Assertion — overflow is reported exactly when a push is rejected.
// Derived from the acceptance condition rather than written independently,
// so the two cannot drift apart.
property p_overflow_iff_rejected_push;
@(posedge clk) disable iff (!rst_n)
(push_i && !push_fire) |=> overflow_evt_o;
endproperty
assert property (p_overflow_iff_rejected_push);
// Assertion — and NOT reported when the push is accepted, including the
// same-cycle-pop case. This is the positive control, formalised.
property p_no_overflow_on_accepted_push;
@(posedge clk) disable iff (!rst_n)
push_fire |=> !overflow_evt_o;
endproperty
assert property (p_no_overflow_on_accepted_push);
// Assertion — the retention policy. A rejected push never disturbs the
// queue: neither the head nor the occupancy may move.
property p_overflow_preserves_queue;
@(posedge clk) disable iff (!rst_n)
overflow_evt_o |-> $stable(level_o);
endproperty
assert property (p_overflow_preserves_queue);The third is the policy under test rather than the flag. Asserting that overflow_evt_o rose proves a rejection was noticed; asserting that occupancy did not move proves which byte survived, which is the design decision §3 made.
Test the sustained case and assert that it does overflow. A suite containing only cases that pass has not characterised the boundary, and Chapter 10.1 §3 showed that sustained mismatch is the failure that hides longest.
7. Debugging
8. What This Means on an FPGA
Overflow detection is three AND terms on signals the FIFO already computes. There is no version of this worth omitting.
Add a saturating loss counter. Four bits alongside a 16-entry FIFO is negligible and turns "we sometimes lose data" into a rate that can be correlated with load, temperature and configuration. It is the single most useful addition to a bring-up build in this module.
Wire RX and TX underflow to different status, per §4. Merging them reports normal idle operation as an error on every link that is not saturated.
Probe full_o, push_fire and pop_fire together. Those three answer the whole question: was the queue full, was a push attempted, and was a pop freeing a slot at the same moment. full_o alone is ambiguous and push_i alone is misleading.
The TX FIFO needs none of this vigilance. Its producer honours ready or violates a local contract, and neither is a link condition. Instrumentation effort belongs on the receive side.
9. Understanding Check
10. Summary
Overflow occurs when push_i && full_o && !pop_fire. The third term is essential: a full FIFO with a simultaneous pop accepts the push, and a definition omitting it reports losses that never happened under exactly the traffic that matters.
FIFO overflow and RX overrun are two views of one event — one about the queue, one about the link — and both are worth reporting because they support different decisions.
This design drops the newest byte, matching the one-entry policy of Chapter 9.3. Both policies lose one byte; dropping the newest leaves a correct prefix and a reported gap, while overwriting the oldest hides a gap inside an apparently contiguous run. A flag cannot say how many were lost — a counter can, for four flip-flops.
Underflow loses nothing. On the receive side it indicates a consumer reading without checking; on the transmit side it is normal idle operation, and merging the two into one status bit reports a quiet link as a fault.
Local backpressure is complete for TX and useless for RX. ready = !full stops a local producer entirely and has no path off the device. The far end's transmitter has its own clock, its own schedule, and no knowledge of this design — which is the module's pivot and the reason two chapters follow.
Verified across six configurations with overflow and underflow genuinely exercised in every one, including the full-plus-simultaneous-pop case that the naive definition gets wrong.
11. What Comes Next
The gap this chapter identified has exactly one kind of solution: send the far end something it can observe.
Chapter 10.5 takes the hardware form. A dedicated signal, out of band, that says stop. The interesting part is not the wire — it is that asserting it when the FIFO is full is already too late: the far end may be mid-frame, may have another frame committed, and takes time to notice at all. That chapter derives the headroom required to absorb the bytes still in flight, and shows why the threshold that triggers flow control is a different number from the one that triggers an interrupt.
Browse the full path on the UART tutorials index. For the one-entry version of this boundary, read back to Chapter 9.3.
Continue learning
Related tutorials
- Related topic
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.
- Related topic
Why UART IP Needs FIFOs
A single holding register makes the service deadline one frame time. Depth multiplies that deadline — and buys time rather than bandwidth, which is the distinction that decides whether a buffer helps at all.
- Related topic
TX and RX FIFO Architecture
A UART FIFO is an asynchronous FIFO only when its two sides genuinely sit in different clock domains — which, for a receiver whose input was already synchronised, is usually not the case.
- Related topic
Thresholds, Watermarks and Trigger Levels
Full and empty are boundaries, not warnings. A trigger level is chosen by comparing the headroom it leaves against the consumer's worst-case latency — and against the interrupt load it costs.
Where this fits
Part of the UART curriculum.
