Skip to content
VLSI Mentor

UART · Module 11

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.

Chapter 11.1 drew the diagram and listed six decisions that exist only at the top level. This chapter makes four of them, and they share a character worth naming up front:

Each is invisible from inside any single block, and each has a wrong answer that looks reasonable.

busy_o was the example Chapter 11.1 §6 gave, and it deadlocked simulation. The three here are the same shape: enable fan-out, flush behaviour, and status aggregation — all of them connections rather than logic, and all of them places where a careless answer produces a defect no unit test can see.

1. One Generator, Fanned Out

Chapter 8.4 established the result and the top level is where it is applied: one fractional generator at OVERSAMPLE × BAUD_HZ, with the receiver consuming the oversample enable directly and the transmitter taking every sixteenth of them.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Synthesizable SystemVerilog — one instance, two enables.
logic os_tick, baud_tick;
uart_timing_gen #(.CLK_HZ(CLK_HZ), .BAUD_HZ(BAUD_HZ), .OVERSAMPLE(OVERSAMPLE))
    u_timing (.clk(clk), .rst_n(rst_n), .os_tick_o(os_tick), .baud_tick_o(baud_tick));

Two independent generators would have been the obvious structure and it is the worse one. Chapter 8.4 §3 quantified why: a shared fractional base is 58× more accurate than independent integer dividers, and a shared integer base would have been 73× worse — the choice of generator matters more than the choice to share.

Sharing also makes the two enables structurally coherent. Measured over 200,000 fabric clocks in the integrated IP:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
os_tick   = 31,999
baud_tick =  1,999
ratio     = 16, exactly — and it cannot drift, because one is a subsample
            of the other rather than an independently generated signal

The fan-out is unremarkable as a load. os_tick reaches the receiver's phase counter, its FSM and the break detector; baud_tick reaches the transmitter alone. A few dozen endpoints at under 2 MHz is nothing a tool will comment on.

Enable fan-out and datapath connections within a UART IP. A single timing generator instance produces an oversample enable and a bit-rate enable, the latter being a subsample of the former so their ratio is exact. The oversample enable reaches the receiver's sampling logic and the break detector; the bit-rate enable reaches the transmitter alone. On the transmit path a write port pushes bytes into the transmit FIFO, whose show-ahead head is offered to the transmitter only when both the hardware and software flow control mechanisms permit it. On the receive path the receiver assembles each character together with its parity and framing status and pushes both as one wider entry into the receive FIFO, whose occupancy separately drives the watermark comparator and the flow control block. The break detector observes the same receive line independently of the receiver's state machine.uart_timing_genONE instanceos_tick31,999 measuredbaud_tick1,999 — subsampleuart_txadvances on baudwrite portvalid / readyTX FIFOhead = tx_data_ipermissionsCTS AND !pauseduart_rxsamples on os_tickread portbyte + statusRX FIFODATA_W + 2 widerx_levelwatermark + flowbreak detect2nd line consumerdivide by Madvancesamplecountpushheadofferbyte+statuspopoccupancy12
Figure 1 — enable fan-out and the two datapaths. One generator instance produces both enables, and the transmit enable is a subsample of the receive one rather than an independently generated signal, so their ratio is exact by construction. Note that the break detector is a second consumer of the receive line, running alongside the receiver rather than inside it.

2. The TX Datapath Connection

Three blocks in series, and the interesting part is the launch condition:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Synthesizable SystemVerilog — TX path assembly.
assign tx_push    = wr_valid_i && !tx_fifo_full && !cfg_tx_flush_i;
assign wr_ready_o = !tx_fifo_full;

// Both flow-control mechanisms are permissions. EITHER can withhold;
// neither overrides the other — Chapters 10.5 and 10.6.
assign tx_offer = launch_ok && !tx_paused;
assign tx_pop   = tx_offer && tx_ready;

tx_pop is the conjunction of an offer and an acceptance, which is Chapter 10.2's contract meeting Chapter 7.4's. The FIFO's show-ahead head is the transmitter's tx_data_i, !empty combined with permission is its tx_valid_i, and the transmitter's tx_ready_o is the FIFO's pop_i. The two handshakes compose without an adapter because both were built to the same shape — which was not an accident but is easy to take for granted.

tx_busy_i is tied low at the gate, deliberately. Chapter 10.5 §7 established that permission gates the launch and never truncates a frame in flight; wiring the transmitter's busy into the gate would have re-introduced exactly the behaviour that chapter rejected.

3. The RX Datapath Connection

The receive side has one extra concern — the status must travel with the byte:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Synthesizable SystemVerilog — RX path assembly.
localparam int unsigned RX_ENTRY_W = DATA_W + 2;

assign rx_push_entry = {rx_perr, rx_ferr, rx_char};   // status ALONGSIDE data
assign rx_push       = rx_char_valid && !cfg_rx_flush_i;
assign rx_char_ready = !rx_fifo_full;                 // backpressure the engine

assign {rd_parity_err_o, rd_frame_err_o, rd_data_o} = rx_pop_entry;
assign rd_valid_o = !rx_fifo_empty;

rx_char_ready = !rx_fifo_full is the receiver's holding register being backpressured by the queue, which is Chapter 10.1 §6's connection. It works completely — and reaches no further than the receiver, which is Chapter 10.4 §5's limit and the reason flow control exists.

The pack and unpack are a single concatenation each, with no intermediate registers. Widening the entry costs two bits per entry and preserves the association Chapter 6.4 built the publish discipline to establish.

4. Flush

A flush discards buffered data on command. The question is what "discard" means when something is already moving.

TX flushRX flush
Discardsqueued bytes not yet launchedqueued bytes not yet read
Does not discardthe frame currently being transmittedthe character currently being received
Why notfreezing mid-frame corrupts it — 10.5 §7the receiver has no abort; it completes and is then dropped
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Synthesizable SystemVerilog — flush as a FIFO reset, not an engine reset.
uart_sync_fifo #(.WIDTH(DATA_W), .DEPTH(TX_DEPTH)) u_tx_fifo (
    .clk(clk), .rst_n(rst_n && !cfg_tx_flush_i),   // flush resets the QUEUE
    .push_i(tx_push), ...
);

5. Status Aggregation

The IP has several distinct points at which a byte can be lost, and they are not the same event:

Loss pointSignalBuilt in
receiver's holding register fullrx_overrun_o6.5
RX FIFO full, push rejectedoverflow_evt_o10.2
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Synthesizable SystemVerilog — both loss points feed ONE overrun flag.
uart_err_status u_status (
    .clk(clk), .rst_n(rst_n),
    .frame_error_evt_i (rx_char_valid && rx_char_ready && rx_ferr),
    .parity_error_evt_i(rx_char_valid && rx_char_ready && rx_perr),
    .overrun_evt_i     (rx_ovr | rx_ovf_evt),   // BOTH points
    .break_evt_i       (break_evt),
    .clear_i(cfg_err_clear_i),
    ...
);

Both must feed the flag, and reporting only one is a real defect. In a buffered receiver the FIFO fills first and the holding register overruns second, so a design that wires only overflow_evt_o misses nothing in the common case — and a design that wires only rx_overrun_o misses the case where the consumer is fast enough to keep the holding register moving but not fast enough to drain the queue. From software's point of view a byte was lost either way, and the aggregate flag answers that question rather than saying where.

The framing and parity events are qualified by acceptance, rx_char_valid && rx_char_ready. That is deliberate: a character the FIFO refused is not stored, so its status is not stored either, and counting it in the sticky flags would report an error against a byte the consumer will never see. The loss itself is reported — by the overrun term — which is the honest attribution.

6. The Assembled RTL

The listing below is the file that was compiled and simulated, and it already carries the three corrections Chapter 11.4 makes: the break threshold derived from DATA_W rather than hardcoded, a width-safe comparison for the software-flow codes, and rx_active taken from a published port on the receiver instead of a hierarchical reference into its state register — each found by asking what would happen at a different parameter set — and the second loopback Chapter 11.5 adds.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ===========================================================================
//  Module 11 — the complete UART IP.
//
//  Every sub-block below was built and verified independently in Modules 6-10
//  and is instantiated here UNMODIFIED. What this file adds is the integration:
//  enable fan-out, configuration distribution, status aggregation, flush,
//  loopback, and the decisions that only exist once the blocks meet.
//
//  Deliberately NOT here: the bus-facing register map, interrupt controller
//  and DMA hooks (Module 13); CDC and reset methodology (Module 12).
// ===========================================================================
module uart_ip
    import uart_parity_pkg::*;
#(
    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,   // service when level >= this
    parameter int unsigned TX_TRIGGER  = 4,    // refill  when level <= this
    parameter int unsigned FC_STOP     = 12,   // withdraw permission at/above
    parameter int unsigned FC_RESUME   = 6     // restore permission at/below
) (
    input  logic clk,
    input  logic rst_n,

    // ── serial pins ──────────────────────────────────────────────────────
    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: tx engine -> rx engine
    input  logic         cfg_line_loop_i,   // LINE: rx pin -> tx pin, for a remote tester
    input  logic         cfg_tx_flush_i,      // one cycle
    input  logic         cfg_rx_flush_i,      // one cycle
    input  logic         cfg_err_clear_i,     // one cycle

    // ── write side: producer pushes bytes to transmit ────────────────────
    input  logic [DATA_W-1:0] wr_data_i,
    input  logic              wr_valid_i,
    output logic              wr_ready_o,

    // ── read side: consumer pops received bytes + their status ───────────
    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 (Module 13 exposes these) ──────────────────────
    output logic tx_trigger_o,
    output logic rx_trigger_o,
    output logic tx_empty_o,
    output logic rx_empty_o,
    output logic busy_o,
    output logic err_frame_o,
    output logic err_parity_o,
    output logic err_overrun_o,
    output logic err_break_o,
    output logic break_active_o,
    output logic any_error_o
);
    // RX entries carry their per-character status — Chapter 10.2 §8.
    localparam int unsigned RX_ENTRY_W = DATA_W + 2;

    // The break threshold is a property of the CONFIGURED frame, not of this
    // file — Chapter 11.4. The longest LEGAL low run is start + DATA_W zero
    // data bits + a zero parity bit; the threshold must sit one interval past
    // it, which is exactly the longest frame this IP can be told to send.
    localparam int unsigned FRAME_BITS_MAX = DATA_W + 3;  // start + data + parity + stop

    // Software flow control compares 8-bit control codes. Narrower characters
    // zero-extend; wider ones compare on the low byte, where the codes live.
    localparam int unsigned SW_CMP_W = (DATA_W < 8) ? DATA_W : 8;

    // Parameter legality, checked at ELABORATION — Chapter 11.4. An `initial`
    // block would only complain once somebody ran a simulation; these stop the
    // build, so an illegal set can never reach synthesis.
    if (RX_TRIGGER > RX_DEPTH) begin : g_bad_rx_trigger
        $error("uart_ip: RX_TRIGGER exceeds RX_DEPTH");
    end
    if (TX_TRIGGER > TX_DEPTH) begin : g_bad_tx_trigger
        $error("uart_ip: TX_TRIGGER exceeds TX_DEPTH");
    end
    if (FC_RESUME >= FC_STOP) begin : g_bad_hysteresis
        $error("uart_ip: FC_RESUME must be below FC_STOP");
    end
    if (FC_STOP > RX_DEPTH) begin : g_bad_fc_stop
        $error("uart_ip: FC_STOP exceeds RX_DEPTH");
    end
    if (DATA_W < 5 || DATA_W > 9) begin : g_bad_data_w
        $error("uart_ip: DATA_W outside the 5..9 range a UART frame supports");
    end
    if (OVERSAMPLE < 4) begin : g_bad_oversample
        $error("uart_ip: OVERSAMPLE below 4 leaves no usable sampling window");
    end
    if (CLK_HZ < BAUD_HZ * OVERSAMPLE) begin : g_bad_clock
        $error("uart_ip: CLK_HZ cannot produce BAUD_HZ at this OVERSAMPLE");
    end

    // ── ONE timing generator, fanned out to both halves — Chapter 8.4 ────
    logic os_tick, baud_tick;
    uart_timing_gen #(.CLK_HZ(CLK_HZ), .BAUD_HZ(BAUD_HZ), .OVERSAMPLE(OVERSAMPLE))
        u_timing (.clk(clk), .rst_n(rst_n), .os_tick_o(os_tick), .baud_tick_o(baud_tick));

    // ── configuration capture: frozen while either half is active ────────
    // Chapter 11.3. A change is held until the link is quiet, so a frame in
    // flight is never reformatted underneath itself.
    parity_mode_t cfg_parity_q;
    logic         cfg_pending_q;
    parity_mode_t cfg_parity_shadow_q;
    logic         link_quiet;

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            cfg_parity_q        <= PARITY_NONE;
            cfg_parity_shadow_q <= PARITY_NONE;
            cfg_pending_q       <= 1'b0;
        end else begin
            if (cfg_parity_i != cfg_parity_shadow_q) begin
                cfg_parity_shadow_q <= cfg_parity_i;
                cfg_pending_q       <= 1'b1;
            end
            if (cfg_pending_q && link_quiet) begin
                cfg_parity_q  <= cfg_parity_shadow_q;
                cfg_pending_q <= 1'b0;
            end
        end
    end

    // ── serial routing, including internal loopback — Chapter 11.5 ───────
    // ── the two loopbacks — Chapter 11.5 ─────────────────────────────────
    // They face opposite directions and are independent: internal loopback
    // lets the IP test itself without the board; line loopback lets the far
    // end test the board without trusting this IP's engines. Both set is
    // legal and not a loop, because rx_i is an input.
    logic tx_line, rx_line;
    assign tx_o    = cfg_line_loop_i ? rx_i    : tx_line;
    assign rx_line = cfg_loopback_i  ? tx_line : rx_i;

    // ── TX FIFO ──────────────────────────────────────────────────────────
    logic [DATA_W-1:0] tx_fifo_head;
    logic tx_fifo_empty, tx_fifo_full, tx_pop;
    logic [$clog2(TX_DEPTH+1)-1:0] tx_level;
    logic tx_push;

    assign tx_push    = wr_valid_i && !tx_fifo_full && !cfg_tx_flush_i;
    assign wr_ready_o = !tx_fifo_full;

    uart_sync_fifo #(.WIDTH(DATA_W), .DEPTH(TX_DEPTH)) u_tx_fifo (
        .clk(clk), .rst_n(rst_n && !cfg_tx_flush_i),
        .push_i(tx_push), .push_data_i(wr_data_i),
        .pop_i(tx_pop),   .pop_data_o(tx_fifo_head),
        .empty_o(tx_fifo_empty), .full_o(tx_fifo_full), .level_o(tx_level),
        .overflow_evt_o(), .underflow_evt_o());

    // ── flow-control permission: hardware AND software — Ch 10.5 / 10.6 ──
    logic tx_paused, xon_consume, launch_ok;

    uart_tx_gate u_gate (
        .clk(clk), .rst_n(rst_n),
        .cts_n_i(cfg_hw_flow_en_i ? cts_n_i : 1'b0),   // disabled = permitted
        .tx_busy_i(1'b0),                              // NOT in the decision
        .tx_fifo_empty_i(tx_fifo_empty),
        .launch_allowed_o(launch_ok));

    // ── transmitter ──────────────────────────────────────────────────────
    logic tx_ready, tx_busy;
    logic tx_offer;

    // Offer a byte only when the FIFO has one AND both flow-control
    // mechanisms permit it. Either can withhold; neither overrides.
    assign tx_offer = launch_ok && !tx_paused;
    assign tx_pop   = tx_offer && tx_ready;

    uart_tx #(.DATA_W(DATA_W)) u_tx (
        .clk(clk), .rst_n(rst_n), .baud_tick_i(baud_tick),
        .tx_data_i(tx_fifo_head), .parity_mode_i(cfg_parity_q),
        .tx_valid_i(tx_offer), .tx_ready_o(tx_ready),
        .tx_o(tx_line), .tx_busy_o(tx_busy));

    // ── receiver ─────────────────────────────────────────────────────────
    logic [DATA_W-1:0] rx_char;
    logic rx_char_valid, rx_char_ready, rx_perr, rx_ferr, rx_ovr;

    logic rx_active;
    uart_rx #(.DATA_W(DATA_W), .OVERSAMPLE(OVERSAMPLE)) u_rx (
        .clk(clk), .rst_n(rst_n), .rx_i(rx_line), .os_tick_i(os_tick),
        .parity_mode_i(cfg_parity_q),
        .rx_data_o(rx_char), .rx_valid_o(rx_char_valid), .rx_ready_i(rx_char_ready),
        .rx_parity_err_o(rx_perr), .rx_frame_err_o(rx_ferr), .rx_overrun_o(rx_ovr),
        .rx_active_o(rx_active));

    // ── RX FIFO, entries carrying status ─────────────────────────────────
    logic [RX_ENTRY_W-1:0] rx_push_entry, rx_pop_entry;
    logic rx_fifo_empty, rx_fifo_full, rx_push, rx_ovf_evt;
    logic [$clog2(RX_DEPTH+1)-1:0] rx_level;

    assign rx_push_entry = {rx_perr, rx_ferr, rx_char};
    assign rx_push       = rx_char_valid && !cfg_rx_flush_i;
    assign rx_char_ready = !rx_fifo_full;          // backpressure the receiver

    uart_sync_fifo #(.WIDTH(RX_ENTRY_W), .DEPTH(RX_DEPTH)) u_rx_fifo (
        .clk(clk), .rst_n(rst_n && !cfg_rx_flush_i),
        .push_i(rx_push), .push_data_i(rx_push_entry),
        .pop_i(rd_ready_i && rd_valid_o), .pop_data_o(rx_pop_entry),
        .empty_o(rx_fifo_empty), .full_o(rx_fifo_full), .level_o(rx_level),
        .overflow_evt_o(rx_ovf_evt), .underflow_evt_o());

    assign {rd_parity_err_o, rd_frame_err_o, rd_data_o} = rx_pop_entry;
    assign rd_valid_o = !rx_fifo_empty;

    // ── watermarks ───────────────────────────────────────────────────────
    uart_fifo_watermark #(.DEPTH(RX_DEPTH), .RX_TRIGGER(RX_TRIGGER), .TX_TRIGGER(RX_DEPTH))
        u_rx_wm (.level_i(rx_level), .rx_trigger_o(rx_trigger_o), .tx_trigger_o());
    uart_fifo_watermark #(.DEPTH(TX_DEPTH), .RX_TRIGGER(TX_DEPTH), .TX_TRIGGER(TX_TRIGGER))
        u_tx_wm (.level_i(tx_level), .rx_trigger_o(), .tx_trigger_o(tx_trigger_o));

    // ── hardware flow control from RX occupancy ──────────────────────────
    logic rts_n_int, allow_remote;
    uart_flow_ctrl #(.DEPTH(RX_DEPTH), .STOP_LEVEL(FC_STOP), .RESUME_LEVEL(FC_RESUME))
        u_fc (.clk(clk), .rst_n(rst_n), .level_i(rx_level),
              .rts_n_o(rts_n_int), .allow_remote_o(allow_remote));

    // Disabled hardware flow control drives the permitted level, not the pin's
    // idle state — an unconfigured link must not appear to be backpressuring.
    assign rts_n_o = cfg_hw_flow_en_i ? rts_n_int : 1'b0;

    // ── software flow control watches the RECEIVED character stream ──────
    uart_xon_xoff u_xon (
        .clk(clk), .rst_n(rst_n),
        .rx_char_valid_i(rx_char_valid && rx_char_ready),
        .rx_char_data_i(8'(rx_char[SW_CMP_W-1:0])),
        .sw_flow_en_i(cfg_sw_flow_en_i),
        .tx_paused_o(tx_paused), .consume_char_o(xon_consume));

    // ── break detection, as an observer — Chapter 9.4 ────────────────────
    logic break_evt;
    uart_break_detect #(.OVERSAMPLE(OVERSAMPLE), .FRAME_BITS(FRAME_BITS_MAX)) u_break (
        .clk(clk), .rst_n(rst_n), .os_tick_i(os_tick), .rx_sync_i(rx_line),
        .break_active_o(break_active_o), .break_evt_o(break_evt),
        .break_released_o());

    // ── status aggregation — Chapter 11.2 §5 ─────────────────────────────
    uart_err_status u_status (
        .clk(clk), .rst_n(rst_n),
        .frame_error_evt_i (rx_char_valid && rx_char_ready && rx_ferr),
        .parity_error_evt_i(rx_char_valid && rx_char_ready && rx_perr),
        .overrun_evt_i     (rx_ovr | rx_ovf_evt),   // BOTH loss points
        .break_evt_i       (break_evt),
        .clear_i(cfg_err_clear_i),
        .frame_error_sticky_o (err_frame_o),
        .parity_error_sticky_o(err_parity_o),
        .overrun_sticky_o     (err_overrun_o),
        .break_sticky_o       (err_break_o),
        .any_error_o          (any_error_o));

    assign tx_empty_o = tx_fifo_empty;
    assign rx_empty_o = rx_fifo_empty;

    // busy_o means WORK IN PROGRESS, not "data exists somewhere".
    //
    // Unread bytes sitting in the RX FIFO are NOT busy — they are reported by
    // rx_empty_o and rx_trigger_o, which is what a consumer polls. Including
    // RX occupancy here makes busy_o impossible to clear until software has
    // read everything, so a driver that waits for !busy before sleeping never
    // sleeps. Chapter 11.2 §5 develops this; it was a real defect in the first
    // version of this file and simulation deadlocked on it.
    assign busy_o     = tx_busy | !tx_fifo_empty | rx_active;

    // link_quiet is a STRICTER condition used only to commit configuration:
    // nothing transmitting, nothing queued to transmit, no frame being
    // received. It also excludes RX occupancy, for the same reason.
    assign link_quiet = !tx_busy && tx_fifo_empty && !rx_active;
endmodule

7. The Review Questions for an Integration

A top level is reviewed differently from a block. The block questions — widths, reset, off-by-one — were answered where each block was built. What remains is about connections, and this list is the one worth running:

QuestionHere
Is any sub-block edited rather than instantiated?No — every one is used as published
Does every crossing signal fit one of the four kinds?Yes — enable, valid/ready, event, level
Is any enable used as a clock?No — one clk throughout
Are both asynchronous inputs synchronised at their boundary?Yes — rx_i in the receiver, cts_n_i in the gate
Does any status signal aggregate things that are not alike?Checked — §5; both overrun points are alike, busy_o was not
Does flush reach anything other than the queues?No — §4
Can two permissions conflict?No — they are ANDed; either may withhold
Does a disabled feature drive a safe value?Checked11.3 §4
Is any block's contract stretched by how it is connected?No — tx_busy_i tied low is the closest, and it is deliberate

The fifth row is where busy_o failed. Chapter 11.1 §6 described the defect; the review question that would have caught it is does this signal aggregate things that are alike? — and receive occupancy is not alike to work in progress.

8. Verification

Integration testing answers a different question from unit testing, and both suites are kept.

Loopback is the strongest single test. Routing tx_o internally to the receiver makes the whole datapath self-checking: a byte written must emerge unchanged and in order, having passed through the TX FIFO, the launch gate, the transmitter, the wire, the receiver, and the RX FIFO. Chapter 11.5 builds it properly.

From the integration suite:

ScenarioResult
reset: line idle, both FIFOs empty, not busy, no errorspass
enable ratio over 200,000 cycles31,999 : 1,999 = exactly 16
loopback 8N1, eight standard patterns8/8 in order, no errors
loopback 8E1, 8O1pass
RX FIFO past FC_STOPrx_trigger and rts_n assert
drain below FC_RESUMErts_n released
TX flushqueue empties, engine undisturbed
RX flushqueue empties
error clearaggregate clears
busy_o with data unreadclears
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
TOTAL CHECKS: 53   FAILURES: 0   (27 bytes looped)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Assertion — the two enables keep an exact ratio, because one is a
// subsample of the other. This fails if someone "decouples" them into
// independent generators, which is the most likely restructuring error.
property p_baud_implies_os;
    @(posedge clk) disable iff (!rst_n)  baud_tick |-> os_tick;
endproperty
assert property (p_baud_implies_os);

// Assertion — flush never disturbs a frame in flight.
property p_flush_spares_the_engine;
    @(posedge clk) disable iff (!rst_n)
        cfg_tx_flush_i |=> $stable(tx_o) || $past(baud_tick);
endproperty
assert property (p_flush_spares_the_engine);

// Assertion — a byte accepted by the write port is never silently discarded
// except by an explicit flush.
property p_accepted_write_is_queued;
    @(posedge clk) disable iff (!rst_n)
        (wr_valid_i && wr_ready_o && !cfg_tx_flush_i) |=> (dut_tx_level > 0);
endproperty

// Assertion — busy_o does not depend on receive occupancy. This is the
// §5 / Chapter 11.1 §6 defect, stated so a later edit cannot reintroduce it.
property p_busy_excludes_rx_fifo;
    @(posedge clk) disable iff (!rst_n)
        (!tx_busy && tx_fifo_empty && rx_idle) |-> !busy_o;
endproperty
assert property (p_busy_excludes_rx_fifo);

The last is worth writing even though the code is now correct. It encodes a decision that was made wrongly once, and the reasonable-looking alternative is one line away.

9. Debugging

10. What This Means on an FPGA

Integration adds almost no logic. The top level is concatenations, a handful of AND terms and two comparators' worth of tie-offs. Essentially all the area is in the blocks it instantiates.

The enable fan-out is the only broadcast, and at UART scale it is unremarkable — a few dozen endpoints toggling at under 2 MHz.

Probe at the block boundaries. os_tick, baud_tick, both FIFO levels, rx_valid/rx_ready, tx_ready, and the status set localise a failure to one block in a single capture. That is the practical dividend of having built and verified them separately.

Keep the flush pulses one cycle. A held flush holds its FIFO in reset, which presents as a queue that is permanently empty and a write port that is permanently ready while nothing is transmitted — an easy defect to create in a register decode and a confusing one to read.

11. Understanding Check

12. Summary

One generator, fanned out. The receiver takes the oversample enable and the transmitter every sixteenth of them — 31,999 : 1,999 measured, exactly 16 — and the ratio cannot drift because one is a subsample of the other.

The TX and RX handshakes compose without adapters, because the FIFO contract and the engine contracts were built to the same valid/ready shape. tx_busy_i is tied low at the gate deliberately: permission gates the launch and never truncates a frame.

The RX entry packs status with data in one concatenation each way, preserving the association through the buffer.

Flush resets the queues and not the engines. Resetting an engine mid-character corrupts the link rather than cleaning it, and the documented consequence is that one more character may appear on the wire after a TX flush.

Two distinct loss points feed one overrun flag, because from software's view they are the same event — while framing and parity events are qualified by acceptance, so no error is reported against a byte the consumer will never see.

Top-level review is about connections: no block edited, every crossing fitting one of four disciplines, no enable used as a clock, both asynchronous inputs synchronised, and no status aggregating things that are not alike — which is where busy_o failed.

Verified: 53 checks, 0 failures, 27 bytes round-tripped in three parity modes.

13. What Comes Next

Configuration has been treated as a signal that arrives and is used. Chapter 11.3 asks the harder question: what happens when it changes.

A parity mode altered while a frame is in flight would reformat that frame underneath itself — Chapter 7.5 §3 solved that for the transmitter alone, and at the top level the same change must reach both halves consistently, or the two ends of the same IP disagree about the frame they are exchanging. That chapter builds the deferral, and confronts what a disabled feature should drive.

Browse the full path on the UART tutorials index. For the blocks this chapter connects, read back to Chapter 10.2 and Chapter 8.4.

Continue learning

Where this fits

Part of the UART curriculum.