UART · Module 11
Top-Level UART IP Architecture
Ten independently verified blocks and the interfaces that join them. The top level adds no new function; what it adds is fan-out, configuration distribution, status aggregation and the decisions that only exist once blocks meet.
Ten modules have built a UART one block at a time, and every one of them was verified alone. That was deliberate: Chapter 10.2 noted that a FIFO whose correctness depends on the transmitter it feeds is a FIFO nobody can reason about, and the same argument applied to every block before it.
This module assembles them, and the first thing to be clear about is what assembly does and does not involve:
The top level adds no new function. Every behaviour the IP exhibits was built and verified in Modules 6 through 10. What the top level adds is fan-out, distribution, aggregation and arbitration — and a set of decisions that have no meaning until two blocks are connected.
Those decisions are the subject of Module 11, and there are more of them than the phrase "wire it together" suggests. One of them was got wrong in the first version of this IP's RTL, and simulation deadlocked on it — §6 describes it, because it is exactly the kind of defect that only exists at the top level.
1. The Inventory
Eleven instances, and not one of them is new:
| Block | Built in | Role at the top level |
|---|---|---|
uart_timing_gen | 8.4 | one generator, two enables |
uart_tx | 7.5 | serialiser and frame sequencer |
uart_rx | 6.6 | sampler, assembler, per-frame status |
uart_sync_fifo ×2 | 10.2 | TX queue, RX queue |
uart_fifo_watermark ×2 | 10.3 | trigger levels |
uart_flow_ctrl | 10.5 | occupancy → backpressure |
uart_tx_gate | 10.5 | launch permission |
uart_xon_xoff | 10.6 | in-band pause detector |
uart_break_detect | 9.4 | duration observer |
uart_err_status | 9.6 | sticky aggregation |
One parameterised FIFO serves both directions at different widths — Chapter 10.2 §1's point made structural. Likewise one watermark module serves both, with the unused trigger tied off.
The payoff of building them separately is visible here: each arrived with a stated contract and a passing testbench, so integration is about connections rather than about whether anything works.
2. The Block Diagram
Three features of that picture are worth naming.
One clock reaches everything. Chapter 8.1 argued for enables rather than derived clocks, and the payoff appears here: the IP has one clock domain and exactly two asynchronous inputs — rx_i and cts_n_i — each handled by a synchroniser at its own boundary.
The break detector sits beside the receiver, not inside it. Chapter 6.1 §3 required this so the receive FSM's transitions stay a function of state and configuration alone, and Chapter 9.4 §9 kept it. At the top level it is visible as a second consumer of the same line.
The RX FIFO is wider than the TX FIFO. Chapter 10.2 §8: receive entries carry their per-character status so buffering does not destroy the association. It is the only asymmetry between the two instances.
3. The Port List
// Conceptual SystemVerilog — the top-level interface. Chapter 11.2 builds
// the body; this is the contract it must satisfy.
module uart_ip #(
parameter int unsigned CLK_HZ = 100_000_000,
parameter int unsigned BAUD_HZ = 115_200,
parameter int unsigned DATA_W = 8,
parameter int unsigned OVERSAMPLE = 16,
parameter int unsigned TX_DEPTH = 16,
parameter int unsigned RX_DEPTH = 16,
parameter int unsigned RX_TRIGGER = 12,
parameter int unsigned TX_TRIGGER = 4,
parameter int unsigned FC_STOP = 12,
parameter int unsigned FC_RESUME = 6
) (
input logic clk,
input logic rst_n,
// serial pins — the only signals leaving the device
input logic rx_i,
output logic tx_o,
input logic cts_n_i,
output logic rts_n_o,
// configuration — Module 13 drives these from registers
input parity_mode_t cfg_parity_i,
input logic cfg_sw_flow_en_i,
input logic cfg_hw_flow_en_i,
input logic cfg_loopback_i, // internal — Chapter 11.5
input logic cfg_line_loop_i, // line — Chapter 11.5
input logic cfg_tx_flush_i,
input logic cfg_rx_flush_i,
input logic cfg_err_clear_i,
// write side
input logic [DATA_W-1:0] wr_data_i,
input logic wr_valid_i,
output logic wr_ready_o,
// read side — byte AND its status, together
output logic [DATA_W-1:0] rd_data_o,
output logic rd_parity_err_o,
output logic rd_frame_err_o,
output logic rd_valid_o,
input logic rd_ready_i,
// aggregated status
output logic tx_trigger_o, rx_trigger_o,
output logic tx_empty_o, rx_empty_o,
output logic busy_o,
output logic err_frame_o, err_parity_o, err_overrun_o, err_break_o,
output logic break_active_o,
output logic any_error_o
);Five groups, and the grouping is the architecture.
Serial pins are the only signals that leave the device. Everything else is internal to the SoC.
Configuration in, status out — and neither is a bus. This IP presents its configuration as signals, not as a register file, which is Chapter 11.4's subject and the reason Module 13 can attach any bus to it without the UART knowing which.
The read side carries status alongside data. rd_data_o, rd_parity_err_o and rd_frame_err_o are valid together, which is what Chapter 6.4's publish discipline and Chapter 10.2's wide entry existed to make possible.
Both data ports are valid/ready. The same handshake as every block inside, so the IP composes with the same reasoning.
4. What Is Deliberately Absent
An IP's boundary is defined as much by what it excludes, and each exclusion here has a named owner:
| Absent | Why | Owner |
|---|---|---|
| bus interface | the IP should not know whether it is on AXI, APB or a custom bus | Module 13 |
| register map | addresses and bit positions are a software contract, not a UART one | Module 13 |
| interrupt controller | masking, priority and clearing are system policy | Module 13 |
| DMA request logic | built from tx_trigger_o / rx_trigger_o, which are exposed | Module 13 |
| clock-domain crossing | the IP is single-clock; a second domain is a system decision | Module 12 |
| reset synchronisation | release discipline is a project methodology | Module 12 |
The configuration ports are the seam. Module 13 will drive cfg_parity_i from a register bit and rx_trigger_o into an interrupt — and neither change touches this file. That separation is what makes the IP reusable, and Chapter 11.4 develops why it matters more than parameterisation does.
5. The Internal Interfaces
Between blocks there are only four kinds of connection, which is what makes the top level readable:
| Kind | Example | Discipline |
|---|---|---|
| enable | os_tick, baud_tick | one cycle, broadcast, never a clock |
| valid/ready | FIFO ↔ engine, port ↔ FIFO | request and acceptance are distinct |
| event pulse | overflow_evt, break_evt | one cycle, consumed by the aggregator |
| level | rx_level, break_active | a condition, read continuously |
Every signal crossing a block boundary is one of those four, and each carries a discipline established where it was built. A connection that does not fit one of them is usually a sign that a block's contract is being stretched — which is the review question Chapter 11.2 §7 asks of each one.
6. The Decisions That Only Exist Here
This is the chapter's point, and it is why "wire it together" understates the work.
Six decisions of that kind exist in this IP, and the rest of Module 11 works through them:
| Decision | Chapter |
|---|---|
what busy_o means | §6, above |
| whether one timing generator or two | 11.2 |
| how flush interacts with a frame in flight | 11.2 |
| which events feed the sticky aggregator | 11.2 |
| when a configuration change takes effect | 11.3 |
| what a disabled feature drives | 11.3 |
None of them is a sub-block's to make, and none of them appears until the blocks are connected.
7. Verification
Loopback is the decisive integration test, and Chapter 11.5 builds it properly. Its value here is structural: routing tx_o back to the receiver internally makes the entire datapath self-checking — a byte written to wr_data_i must emerge at rd_data_o, unchanged and in order, having been serialised, sampled, reassembled and buffered on the way.
That single test exercises every block at once, which is precisely what the sub-block tests could not do. From the integration suite:
| Scenario | Result |
|---|---|
| reset state: line idle, FIFOs empty, not busy, no errors | pass |
| loopback 8N1, all eight standard patterns | pass — 8/8 returned in order |
| loopback 8E1 | pass |
| loopback 8O1 | pass |
| configuration change while quiet | applied |
| configuration change mid-traffic | deferred, in-flight bytes complete under the old format |
RX FIFO filled past FC_STOP | rx_trigger and rts_n both assert |
drain below FC_RESUME | rts_n released |
busy_o with data still unread | clears — §6's fix |
| TX flush / RX flush | both empty their queue |
| error clear | aggregate clears |
| loopback disabled | receive follows the external pin |
TOTAL CHECKS: 53 FAILURES: 0 (27 bytes looped)The sub-block suites still run and still pass, and they are not superseded. Integration testing finds a different class of defect — §6's is the example — and a suite that replaces unit tests with a loopback has traded away the ability to localise a failure.
8. What This Means on an FPGA
The IP is small. Roughly 35 flip-flops for the receiver, a similar count for the transmitter, a 25-bit accumulator for timing, two FIFOs at the chosen depths, and a few dozen for status and flow control. At 16-entry FIFOs the whole thing is a few hundred flip-flops and a small amount of LUT logic.
Two asynchronous inputs, both handled at their boundary. rx_i in the receiver and cts_n_i in the launch gate. Those are the only structures timing analysis needs to be told about, and Module 12 owns what it is told.
Depth dominates the resource profile. Everything else is fixed; the FIFOs scale with the depth chosen from Chapter 10.1's latency budget, and past a few hundred entries the implementation changes character.
Probe at the block boundaries, not inside. os_tick, baud_tick, the two FIFO levels, rx_valid/rx_ready, and the status set localise a failure to one block in a single capture — which is the practical benefit of having built them separately.
9. Understanding Check
10. Summary
The top level adds no function. Every behaviour was built and verified in Modules 6 through 10; assembly adds fan-out, distribution, aggregation and arbitration.
Eleven instances, none of them new, including one parameterised FIFO used twice at different widths and one watermark module used twice with opposite senses.
One clock reaches everything — the payoff of the enable architecture — leaving exactly two asynchronous inputs, rx_i and cts_n_i, each synchronised at its own boundary.
The break detector sits beside the receiver, preserving the property that the receive FSM's transitions depend only on state and configuration.
Five port groups, of which the important one is that configuration and status are signals, not a bus — which is why Module 13 can attach any fabric without this file changing.
The read port carries status with the data, which is what the publish discipline and the wide FIFO entry existed to make possible.
Six decisions exist only at the top level, and busy_o is the example: defining it as anything in flight anywhere includes unread receive data, so the flag never clears for the consumer who is polling it. Simulation deadlocked on exactly that, and no sub-block test could have caught it.
Integration verified: 53 checks, 0 failures, with 27 bytes round-tripped through loopback in three parity modes.
11. What Comes Next
This chapter drew the diagram. Chapter 11.2 connects it, and the details are where integration actually lives: whether the two halves share one timing generator or get their own, how a flush interacts with a frame already in flight, and which of the several places a byte can be lost should feed the same overrun flag.
Browse the full path on the UART tutorials index. For the blocks this chapter inventories, read back to Chapter 6.6, Chapter 7.5 and Chapter 10.2.
Continue learning
Related tutorials
- Related topic
Integrating TX, RX, Baud Generation and FIFOs
The details that only appear at the top level: one generator feeding both halves, a flush that must not corrupt a frame in flight, and several distinct loss points that must feed one honest overrun flag.
- Related topic
Configuration, Control and Status Plumbing
Routing configuration to both halves and status back, with defined behaviour when a change arrives mid-traffic — because a format applied to one half and not the other is worse than not applying it at all.
- Related topic
RX Datapath and Block Partitioning
Module 5 built the receiver's timing mechanisms as separate demonstrators. Assembling them means deciding which blocks exist, what each owns, and what contract joins them — including the two oversample counters that must merge into one.
- Related topic
RX and TX Baud Timing: One Generator or Two?
A receiver needs sixteen ticks per bit interval and a transmitter needs one. Sharing a base is excellent when that base is fractional and 73 times worse when it is not — and it does nothing at all for the receiver's phase uncertainty.
Where this fits
Part of the UART curriculum.
