Skip to content
VLSI Mentor

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:

BlockBuilt inRole at the top level
uart_timing_gen8.4one generator, two enables
uart_tx7.5serialiser and frame sequencer
uart_rx6.6sampler, assembler, per-frame status
uart_sync_fifo ×210.2TX queue, RX queue
uart_fifo_watermark ×210.3trigger levels
uart_flow_ctrl10.5occupancy → backpressure
uart_tx_gate10.5launch permission
uart_xon_xoff10.6in-band pause detector
uart_break_detect9.4duration observer
uart_err_status9.6sticky 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

A complete UART intellectual property block. A single fabric clock drives one timing generator, which produces an oversample enable consumed by the receiver and a bit-rate enable consumed by the transmitter. On the transmit path, a producer writes bytes into a transmit FIFO; a launch gate combines hardware clear-to-send permission and software pause state to decide whether the transmitter may take the next byte, and the transmitter serialises it onto the transmit pin. On the receive path, the receive pin feeds the receiver, which assembles characters and their per-character parity and framing status and pushes both together into a receive FIFO whose entries are wider than the data. A consumer pops bytes and their status from that FIFO. The receive FIFO's occupancy drives both a watermark comparator producing a service trigger and a flow control block producing the request-to-send output. A break detector observes the receive line independently of the receiver. An error status block aggregates framing, parity, overrun and break events into sticky flags. Configuration enters the block and is distributed to both halves; aggregated status leaves for the register interface built in a later module.clkone domaintiming genCh 8.4os_tick16 per bitbaud_tick1 per bitwrite portvalid / readyTX FIFOCh 10.2launch gateCTS + XOFFuart_txCh 7.5read portbyte + statusRX FIFODATA_W + 2uart_rxCh 6.6tx_o / rx_ithe wireconfigurationCh 11.3flow ctrlCh 10.5break detectCh 9.4 — observerstatusCh 9.6 stickypushheadofferadvancetx_orx_isamplebyte+statuspoplevelobserveeventsbreakparityparity12
Figure 1 — the complete IP. One clock and one timing generator feed everything; the two serial pins and the two flow-control pins are the only signals leaving the device. Configuration enters from the left and is distributed; status is aggregated and leaves on the right. Nothing in this picture is new — every box is a module from an earlier chapter.

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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:

AbsentWhyOwner
bus interfacethe IP should not know whether it is on AXI, APB or a custom busModule 13
register mapaddresses and bit positions are a software contract, not a UART oneModule 13
interrupt controllermasking, priority and clearing are system policyModule 13
DMA request logicbuilt from tx_trigger_o / rx_trigger_o, which are exposedModule 13
clock-domain crossingthe IP is single-clock; a second domain is a system decisionModule 12
reset synchronisationrelease discipline is a project methodologyModule 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:

KindExampleDiscipline
enableos_tick, baud_tickone cycle, broadcast, never a clock
valid/readyFIFO ↔ engine, port ↔ FIFOrequest and acceptance are distinct
event pulseoverflow_evt, break_evtone cycle, consumed by the aggregator
levelrx_level, break_activea 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:

DecisionChapter
what busy_o means§6, above
whether one timing generator or two11.2
how flush interacts with a frame in flight11.2
which events feed the sticky aggregator11.2
when a configuration change takes effect11.3
what a disabled feature drives11.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:

ScenarioResult
reset state: line idle, FIFOs empty, not busy, no errorspass
loopback 8N1, all eight standard patternspass — 8/8 returned in order
loopback 8E1pass
loopback 8O1pass
configuration change while quietapplied
configuration change mid-trafficdeferred, in-flight bytes complete under the old format
RX FIFO filled past FC_STOPrx_trigger and rts_n both assert
drain below FC_RESUMErts_n released
busy_o with data still unreadclears — §6's fix
TX flush / RX flushboth empty their queue
error clearaggregate clears
loopback disabledreceive follows the external pin
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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

Where this fits

Part of the UART curriculum.