Skip to content
VLSI Mentor

UART · Module 10

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.

Chapter 6.5 built a receiver with one holding register, and Chapter 9.3 showed exactly what that costs: a byte is lost whenever the consumer is more than one frame time behind. At 115,200 baud with 8N1 that deadline is 86.81 µs — 8,681 cycles of a 100 MHz fabric clock.

The obvious response is "add a buffer", and it is the right response. What is worth getting precise is what a buffer actually buys, because the intuition is usually wrong in a way that produces designs which fail later and more confusingly:

Depth buys time. It does not buy bandwidth.

A FIFO absorbs a consumer that is temporarily behind. It does nothing for a consumer that is permanently slower than the line, and no depth ever will. Getting that distinction right is the difference between sizing a FIFO from a latency budget and guessing at a number because sixteen sounds reasonable.

1. The Arrival Rate Is Fixed and Knowable

Unlike most producers in a system, a UART's arrival rate is exactly determined by configuration. From Chapter 4.1, an 8N1 frame is ten bit intervals for eight payload bits, so:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
                 baud                            frame_bits
bytes_per_s  =  ─────────          T_frame  =  ──────────────      [s]
                frame_bits                         baud
baudframe bitsbytes/sT_frame
9,60010960.0001041.667 µs
115,2001011,520.00086.806 µs
1,000,00010100,000.00010.000 µs
3,000,00010300,000.0003.333 µs

T_frame is the consumer's deadline with one holding register, and it is the number that matters. At 115,200 baud a driver has 86.8 µs; at 3 Mbaud it has 3.33 µs — about 333 cycles of a 100 MHz clock, which is inside the range where interrupt entry alone can miss it.

That last row is why FIFOs stopped being optional. A UART at 9,600 baud gives a millisecond of grace and tolerates almost any software; the same design at 3 Mbaud gives three microseconds and tolerates almost none.

2. What Depth Buys

With D usable entries and a consumer doing nothing at all, the buffer absorbs D characters before the next one is lost:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
T_buffer  ≈  D × T_frame          [s]

Stated assumptions, because the formula is only as good as they are: the FIFO starts empty, characters arrive back-to-back at the line rate, and the consumer removes nothing. It is a worst-case bound, not a typical case.

Machine-computed:

baudD=1D=4D=8D=16D=32D=64
9,6001.04 ms4.17 ms8.33 ms16.67 ms33.33 ms66.67 ms
115,20086.81 µs347.22 µs694.44 µs1.39 ms2.78 ms5.56 ms
1,000,00010.00 µs40.00 µs80.00 µs160.00 µs320.00 µs640.00 µs
3,000,0003.33 µs13.33 µs26.67 µs53.33 µs106.67 µs213.33 µs

The D=1 column is Chapter 9.3's receiver, so the table reads as "what depth buys against the deadline you already have". Sixteen entries at 115,200 baud turns 86.81 µs into 1.39 ms — a factor of sixteen, which moves the requirement from "the ISR must run almost immediately" to "the ISR must run within a millisecond", and those are very different engineering problems.

At 3 Mbaud even 64 entries give only 213 µs, which is a useful reminder that depth is measured in characters while the deadline is measured in time, and the conversion factor is the line rate.

3. Burst Absorption Is Not Rate Conversion

This is the chapter's central distinction and the most common misconception in the module.

4. Where the Fluid Approximation Breaks

T = H / r treats arrival and service as continuous flows. UART arrival is not continuous — it is one character every T_frame, exactly — and service is usually burstier still: a driver that reads everything when it runs and nothing in between.

For that pattern the useful question is not a rate at all:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Will the consumer's WORST-CASE gap between service events exceed D × T_frame?

That is a comparison of two times, and it is the form Chapter 10.3 uses to choose a threshold. The rate form of §3 is the right model for sustained behaviour; the deadline form is the right model for bursty service. A design usually needs both — the deadline form to size depth, the rate form to confirm the consumer can keep up at all.

This is as far into queueing theory as a UART needs to go. Arrival is deterministic and the interesting cases are worst-case rather than average, so distributions add little that the two bounds above do not already give.

5. The Structural Change

A comparison of two UART receive paths. In the first, the receive engine delivers each completed character to a single holding register, whose output goes directly to a consumer such as software or a bus interface; because there is only one storage location, the consumer must accept each byte within one frame time or the next arriving character is lost. In the second, the receive engine pushes each completed character into a first-in first-out queue with many entries, and the consumer pops from that queue; the deadline becomes the number of entries multiplied by the frame time. The receive engine is identical in both paths, so the buffering is an addition at its output rather than a change to the receiver.RX engineModule 61 registerChapter 6.5consumersoftware / bus86.81 usdeadline @115,200RX engineUNCHANGEDFIFO, D entriesChapter 10.2consumersame consumerD x 86.81 us1.39 ms at D=16charvalid/readymust meetpushpopmust meet12
Figure 1 — what changes. Above the dashed line, the receiver's output must be consumed within one frame time or the next character is lost. Below it, the same receiver feeds a queue, and the deadline becomes the queue's depth multiplied by the frame time. The receiver itself is unchanged in both cases — the buffering is added at its output, not inside it.

Three back-to-back characters, D=1 versus D=4

6 cycles
A trace of four character slots in a continuous UART stream, with each column representing one complete character time. Under a single holding register, the first character is stored and held because the consumer is busy; when the second character completes there is nowhere to put it and it is dropped, raising an overrun, and the third is dropped as well. Under a four-entry queue the same three characters are all stored, the occupancy rises from one to three, no byte is lost, and the consumer services the queue once afterwards, recovering all three characters in order.consumer not servingconsumer not servingB dropped — one registerB dropped — one registerconsumer finally runsconsumer finally runscharA doneB doneC doneidleconsumer runsidleD=1 storedAAAAAAD=1 lostD=4 level123300D=4 lostt0t1t2t3t4t5
Figure 2 — the same traffic against two depths. Columns are BIT INTERVALS of a continuous 8N1 stream; each character completes at the end of its tenth interval. With one register, character B is lost because the consumer has not yet read A. With four entries the same traffic is absorbed entirely, and the consumer services the queue once at the end rather than three times under deadline.

6. A Minimal Buffer, to Make the Point Concrete

The full architecture is Chapter 10.2's. What this chapter needs is only enough to show that the receiver's interface does not change:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Conceptual SystemVerilog — the connection, not the FIFO.
// The receiver of Module 6 is untouched: its held-valid output becomes the
// queue's push port, and the consumer pops instead of reading the register
// directly. Chapter 10.2 builds the queue itself.
assign fifo_push      = rx_valid_o && !fifo_full;   // accept when there is room
assign rx_ready_i     = !fifo_full;                 // backpressure the RECEIVER
assign consumer_data  = fifo_head;
assign consumer_valid = !fifo_empty;

rx_ready_i = !fifo_full is the interesting line. It backpressures the receiver's holding register, which is a local handshake and works. What it cannot do is backpressure the wireChapter 6.5 §4 established that a UART receiver has no way to tell the far end to wait, and a FIFO does not change that. When the queue is full the receiver's register fills, and the character after that is lost exactly as before.

So a FIFO moves the overrun threshold and does not remove the overrun. Chapter 10.4 is about what happens at the new threshold, and Chapter 10.5 is about the only mechanism that can actually reach the far end.

7. Choosing a Depth

The procedure, in the order the facts become available:

1. Compute T_frame from the configured baud and frame length. It is exact.

2. Establish the consumer's worst-case service gap. Not its average — its worst case. For software this is interrupt latency plus the time the handler may be blocked by higher-priority work; for a bus master it is worst-case arbitration plus transfer time. This number is usually the hardest to obtain and the most important.

3. Require D × T_frame ≥ worst-case gap, with margin. Chapter 10.3 refines this, because a trigger level means service begins before the queue is full and the usable headroom is D − T rather than D.

4. Separately confirm μ ≥ λ on average. If it is not, stop — no depth is sufficient and §3 is the reason.

Worked, at 115,200 baud 8N1 with a 500 µs worst-case ISR latency:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
T_frame        = 86.806 µs
required D     = 500 / 86.806 = 5.76  ->  6 entries minimum
with margin    = 8 or 16 entries

And at 1 Mbaud with the same software:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
T_frame        = 10.000 µs
required D     = 500 / 10 = 50  ->  50 entries minimum

The rate rose by a factor of 8.68 and the required depth rose by the same factor — depth scales linearly with line rate for a fixed software latency, which is the relationship that makes depth a system-level decision rather than a UART one.

8. Verification

Test the deadline, not just the function. A FIFO testbench that pushes and pops at a comfortable rate demonstrates nothing about the property the FIFO exists for. The useful test drives characters at the line rate and delays the consumer by a parameterised amount, then asserts that no byte is lost while the delay is below D × T_frame and that the expected byte is lost above it.

Test the sustained-mismatch case explicitly, and assert that it does overflow. A design whose suite only contains cases that pass has not characterised the boundary — and §3's whole point is that this failure is invisible until it is looked for.

Measure occupancy over time rather than only checking it at the end. The high-water mark across a run is more informative than the final level, and it is what tells you whether a chosen depth has any margin at all in practice.

9. What This Means on an FPGA

Depth is cheap until it is not. Sixteen bytes is 128 bits — trivially registers or distributed RAM on any device. A few hundred entries starts to want block RAM, which changes the read timing and the reset behaviour; Chapter 10.2 §9 covers what that implies.

The depth decision is usually made with the wrong information. It is chosen early, from a guess about software, and revisited only after data loss in the field. The arithmetic in §7 takes minutes and needs one number from the software team — worst-case service gap — that is worth asking for explicitly rather than assuming.

Instrument the high-water mark. A register holding the maximum occupancy ever reached costs a comparator and a few flip-flops, and it answers the question no amount of reasoning can: how much margin the chosen depth actually has on the real system. A high-water mark that sits at D − 1 is a design about to fail.

A FIFO does not relax the receiver's timing. The receive engine still samples at the line rate and still has the margin Chapter 5.5 computed. Buffering is entirely downstream of that, and no depth compensates for a rate mismatch on the wire.

10. Understanding Check

11. Summary

A UART's arrival rate is exactly knowable from configuration: bytes/s = baud / frame_bits, and T_frame is the consumer's deadline with one register — 86.81 µs at 115,200 8N1, falling to 3.33 µs at 3 Mbaud, which is where software stops being able to keep up.

Depth multiplies that deadline: T_buffer ≈ D × T_frame with the FIFO starting empty and the consumer doing nothing. Sixteen entries at 115,200 baud turns 86.81 µs into 1.39 ms.

Depth buys time, not bandwidth. A FIFO absorbs a consumer that is temporarily behind and does nothing for one that is permanently slow: at a 0.17% shortfall, 64 entries last 3.2 seconds and quadrupling the depth quadruples that and nothing more. Size depth from a latency budget, never from a throughput deficit.

The fluid model is right for sustained behaviour; a deadline comparison is right for bursty service. A design usually needs both.

A FIFO moves the overrun threshold and does not remove the overrun, because rx_ready_i = !fifo_full backpressures the receiver's register and cannot reach the wire.

And the depth procedure is four steps, of which the hard one is obtaining the consumer's worst-case service gap — a number that must be asked for rather than assumed.

12. What Comes Next

This chapter has treated the FIFO as a box with a depth. Chapter 10.2 opens it.

It settles the question the module's title makes tempting to get wrong — 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 — then builds the pointers, the occupancy counter, and the contract that every later chapter of this module uses unchanged: what a push request means, when it is accepted, what the read port shows, and what happens when a push and a pop land on the same clock edge.

Browse the full path on the UART tutorials index. For the overrun this buffering is aimed at, read back to Chapter 9.3.

Continue learning

Where this fits

Part of the UART curriculum.