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 Madvancesamplepushheadofferbyte+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. It also carries Chapter 12.2's correction: the break detector is fed the receiver's synchronised line rather than the raw pin, so the IP has one synchroniser on its one asynchronous input and every consumer sees the same resolved value. And it carries Chapter 13.2's change: the FIFO trigger levels arrive as runtime inputs rather than parameters, so a driver can tune service latency without a re-synthesis. And it carries Chapter 13.4's correction to the receive push, which must be the handshake rather than merely the offer — a defect that only appeared once a DMA engine read the queue back to back while it was full.

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

    // Trigger levels are RUNTIME values — Chapter 13.2. They select among
    // behaviours the comparators already implement, so by Chapter 11.4's
    // test they belong to a register, not to a parameter. The parameters
    // below survive as the RESET VALUES the register block loads.
    input  logic [$clog2(RX_DEPTH+1)-1:0] cfg_rx_trigger_i,
    input  logic [$clog2(TX_DEPTH+1)-1:0] cfg_tx_trigger_i,

    // ── 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,
    // A character was accepted into the RX queue. Published for the
    // receive-idle timeout a register block needs — Chapter 13.3. Another
    // instance of Chapter 11.4's rule: a hierarchical reference is a
    // missing port, and blocks acquire their real interfaces from being used.
    output logic rx_push_evt_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, rx_synced;
    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_sync_o(rx_synced));

    // ── 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};
    // Chapter 13.4 — the push must be the HANDSHAKE, not merely the offer.
    // Gating only on rx_char_valid meant that when the queue was full and a
    // pop happened on the same edge, the FIFO accepted a push while the
    // receiver's handshake did NOT complete — so the same character was
    // written again on every subsequent pop and the queue never drained.
    assign rx_push       = rx_char_valid && rx_char_ready && !cfg_rx_flush_i;
    assign rx_push_evt_o = rx_push;
    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 ───────────────────────────────────────────────────────
    // The same two watermark instances as before — Chapter 10.3. Only the
    // value they compare against has moved from elaboration time to run time.
    uart_fifo_watermark #(.DEPTH(RX_DEPTH)) u_rx_wm (
        .level_i(rx_level), .rx_level_i(cfg_rx_trigger_i),
        .tx_level_i({$clog2(RX_DEPTH+1){1'b1}}),
        .rx_trigger_o(rx_trigger_o), .tx_trigger_o());
    uart_fifo_watermark #(.DEPTH(TX_DEPTH)) u_tx_wm (
        .level_i(tx_level), .rx_level_i({$clog2(TX_DEPTH+1){1'b1}}),
        .tx_level_i(cfg_tx_trigger_i),
        .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 ────────────────────
    // Fed from the receiver's SYNCHRONISED line, not from the pin — Chapter
    // 12.2. Its port is named rx_sync_i because it expects a signal that has
    // already crossed into this clock domain; handing it the raw pin put an
    // asynchronous input straight onto a flop.
    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_synced),
        .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?Checked — 11.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. The Assembled RTL in Three Languages

§6's listing is SystemVerilog. The integration it performs is not, and every decision above survives translation — including the two this chapter argues hardest for, the handshake-gated RX push and busy_o excluding stored data.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  uart_ip_v — the complete UART IP, Synthesizable Verilog-2001
//
//  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.
//
//  Ports are declared Verilog-1995 style because cfg_rx_trigger_i is
//  clog2(RX_DEPTH+1) bits wide and an ANSI port list cannot reference a
//  localparam that does not exist yet -- the same rule Chapter 10.2 met.
//
//  Deliberately NOT here: the bus-facing register map, interrupt controller
//  and DMA hooks (Module 13); CDC and reset methodology (Module 12).
//===========================================================================
module uart_ip_v (
    clk, rst_n,
    rx_i, tx_o, cts_n_i, rts_n_o,
    cfg_parity_i, cfg_sw_flow_en_i, cfg_hw_flow_en_i,
    cfg_loopback_i, cfg_line_loop_i,
    cfg_tx_flush_i, cfg_rx_flush_i, cfg_err_clear_i,
    cfg_rx_trigger_i, cfg_tx_trigger_i,
    wr_data_i, wr_valid_i, wr_ready_o,
    rd_data_o, rd_parity_err_o, rd_frame_err_o, rd_valid_o, rd_ready_i,
    tx_trigger_o, rx_trigger_o, tx_empty_o, rx_empty_o, busy_o,
    err_frame_o, err_parity_o, err_overrun_o, err_break_o,
    break_active_o, any_error_o, rx_push_evt_o
);
    parameter CLK_HZ     = 100000000;
    parameter BAUD_HZ    = 115200;
    parameter DATA_W     = 8;
    parameter OVERSAMPLE = 16;
    parameter TX_DEPTH   = 16;
    parameter RX_DEPTH   = 16;
    parameter FC_STOP    = 12;
    parameter FC_RESUME  = 6;

    function integer clog2;
        input integer value;
        integer i;
        begin
            clog2 = 1;
            for (i = value - 1; i > 1; i = i >> 1) clog2 = clog2 + 1;
        end
    endfunction

    // Verilog-2001 has no packages, so the parity encoding is repeated here.
    // The VALUES must match uart_parity_pkg exactly -- a duplicated constant
    // is the price of the language, and Chapter 11.4 treats it as an
    // integration risk rather than a style question.
    localparam [1:0] PARITY_NONE = 2'b00;

    localparam RX_ENTRY_W    = DATA_W + 2;      // {parity_err, frame_err, data}
    // The longest LEGAL low run is start + DATA_W zero data bits + a zero
    // parity bit; the break threshold sits one interval past it.
    localparam FRAME_BITS_MAX = DATA_W + 3;
    localparam SW_CMP_W       = (DATA_W < 8) ? DATA_W : 8;
    localparam TXC_W          = clog2(TX_DEPTH + 1);
    localparam RXC_W          = clog2(RX_DEPTH + 1);

    input              clk;
    input              rst_n;
    input              rx_i;
    output             tx_o;
    input              cts_n_i;
    output             rts_n_o;
    input  [1:0]       cfg_parity_i;
    input              cfg_sw_flow_en_i;
    input              cfg_hw_flow_en_i;
    input              cfg_loopback_i;
    input              cfg_line_loop_i;
    input              cfg_tx_flush_i;
    input              cfg_rx_flush_i;
    input              cfg_err_clear_i;
    input  [RXC_W-1:0] cfg_rx_trigger_i;
    input  [TXC_W-1:0] cfg_tx_trigger_i;
    input  [DATA_W-1:0] wr_data_i;
    input              wr_valid_i;
    output             wr_ready_o;
    output [DATA_W-1:0] rd_data_o;
    output             rd_parity_err_o;
    output             rd_frame_err_o;
    output             rd_valid_o;
    input              rd_ready_i;
    output             tx_trigger_o;
    output             rx_trigger_o;
    output             tx_empty_o;
    output             rx_empty_o;
    output             busy_o;
    output             err_frame_o;
    output             err_parity_o;
    output             err_overrun_o;
    output             err_break_o;
    output             break_active_o;
    output             any_error_o;
    output             rx_push_evt_o;

    //=======================================================================
    //  ONE timing generator, fanned out to both halves — Chapter 8.4
    //=======================================================================
    wire os_tick, baud_tick;
    uart_timing_gen_v #(.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 — Ch 11.3
    //  A change is held until the link is quiet, so a frame in flight is
    //  never reformatted underneath itself.
    //=======================================================================
    reg  [1:0] cfg_parity_q;
    reg  [1:0] cfg_parity_shadow_q;
    reg        cfg_pending_q;
    wire       link_quiet;

    always @(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

    //=======================================================================
    //  The two loopbacks — Chapter 11.5
    //  They face opposite directions and are independent. Both set is legal
    //  and not a loop, because rx_i is an input.
    //=======================================================================
    wire 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
    //=======================================================================
    wire [DATA_W-1:0] tx_fifo_head;
    wire              tx_fifo_empty, tx_fifo_full;
    wire [TXC_W-1:0]  tx_level;
    wire              tx_push, tx_pop;

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

    uart_sync_fifo_v #(.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
    //=======================================================================
    wire tx_paused, launch_ok;

    uart_tx_gate_v 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
    //=======================================================================
    wire tx_ready, tx_busy, tx_offer;

    // Either mechanism can withhold permission; neither overrides the other.
    assign tx_offer = launch_ok && !tx_paused;
    assign tx_pop   = tx_offer && tx_ready;

    uart_tx_v #(.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
    //=======================================================================
    wire [DATA_W-1:0] rx_char;
    wire rx_char_valid, rx_char_ready, rx_perr, rx_ferr, rx_ovr;
    wire rx_active, rx_synced;

    uart_rx_v #(.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_sync_o(rx_synced));

    //=======================================================================
    //  RX FIFO, entries carrying their per-character status — Ch 10.2
    //=======================================================================
    wire [RX_ENTRY_W-1:0] rx_push_entry, rx_pop_entry;
    wire rx_fifo_empty, rx_fifo_full, rx_push, rx_ovf_evt;
    wire [RXC_W-1:0] rx_level;

    assign rx_push_entry = {rx_perr, rx_ferr, rx_char};
    // The push must be the HANDSHAKE, not merely the offer. Gating only on
    // rx_char_valid let the FIFO accept a push while the receiver's handshake
    // did NOT complete, so the same character was written again on every
    // subsequent pop and the queue never drained.
    assign rx_push       = rx_char_valid && rx_char_ready && !cfg_rx_flush_i;
    assign rx_push_evt_o = rx_push;
    assign rx_char_ready = !rx_fifo_full;          // backpressure the receiver

    uart_sync_fifo_v #(.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 = rx_pop_entry[RX_ENTRY_W-1];
    assign rd_frame_err_o  = rx_pop_entry[RX_ENTRY_W-2];
    assign rd_data_o       = rx_pop_entry[DATA_W-1:0];
    assign rd_valid_o      = !rx_fifo_empty;

    //=======================================================================
    //  Watermarks — Chapter 10.3
    //=======================================================================
    uart_fifo_watermark_v #(.DEPTH(RX_DEPTH)) u_rx_wm (
        .level_i(rx_level), .rx_level_i(cfg_rx_trigger_i),
        .tx_level_i({RXC_W{1'b1}}),
        .rx_trigger_o(rx_trigger_o), .tx_trigger_o());
    uart_fifo_watermark_v #(.DEPTH(TX_DEPTH)) u_tx_wm (
        .level_i(tx_level), .rx_level_i({TXC_W{1'b1}}),
        .tx_level_i(cfg_tx_trigger_i),
        .rx_trigger_o(), .tx_trigger_o(tx_trigger_o));

    //=======================================================================
    //  Hardware flow control from RX occupancy
    //=======================================================================
    wire rts_n_int, allow_remote;
    uart_flow_ctrl_v #(.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 its far end.
    assign rts_n_o = cfg_hw_flow_en_i ? rts_n_int : 1'b0;

    //=======================================================================
    //  Software flow control watches the RECEIVED character stream
    //=======================================================================
    wire [7:0] sw_char;
    assign sw_char = {{(8-SW_CMP_W){1'b0}}, rx_char[SW_CMP_W-1:0]};

    uart_xon_xoff_v u_xon (
        .clk(clk), .rst_n(rst_n),
        .rx_char_valid_i(rx_char_valid && rx_char_ready),
        .rx_char_data_i(sw_char),
        .sw_flow_en_i(cfg_sw_flow_en_i),
        .tx_paused_o(tx_paused), .consume_char_o());

    //=======================================================================
    //  Break detection, as an observer — Chapter 9.4
    //  Fed from the receiver's SYNCHRONISED line, not from the pin.
    //=======================================================================
    wire break_evt;
    uart_break_detect_v #(.OVERSAMPLE(OVERSAMPLE), .FRAME_BITS(FRAME_BITS_MAX)) u_break (
        .clk(clk), .rst_n(rst_n), .os_tick_i(os_tick), .rx_sync_i(rx_synced),
        .break_active_o(break_active_o), .break_evt_o(break_evt),
        .break_released_o());

    //=======================================================================
    //  Status aggregation — Chapter 11.2
    //=======================================================================
    uart_err_status_v 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 in the RX FIFO are NOT busy -- including them makes busy_o
    // impossible to clear until software has read everything, so a driver
    // that waits for !busy before sleeping never sleeps.
    assign busy_o     = tx_busy | !tx_fifo_empty | rx_active;

    // A STRICTER condition, used only to commit configuration.
    assign link_quiet = !tx_busy && tx_fifo_empty && !rx_active;
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--==========================================================================
--  uart_ip -- the complete UART IP, Synthesizable VHDL-2008
--
--  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.
--
--  VHDL needs no port-declaration workaround: a generic is visible to the
--  port clause, so cfg_rx_trigger_i can be sized with a function call where
--  the width belongs.
--
--  Deliberately NOT here: the bus-facing register map, interrupt controller
--  and DMA hooks (Module 13); CDC and reset methodology (Module 12).
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.uart_fifo_pkg.all;

entity uart_ip is
    generic (
        CLK_HZ     : positive := 100_000_000;
        BAUD_HZ    : positive := 115_200;
        DATA_W     : positive := 8;
        OVERSAMPLE : positive := 16;
        TX_DEPTH   : positive := 16;
        RX_DEPTH   : positive := 16;
        FC_STOP    : natural  := 12;
        FC_RESUME  : natural  := 6
    );
    port (
        clk   : in std_logic;
        rst_n : in std_logic;

        -- serial pins -----------------------------------------------------
        rx_i    : in  std_logic;
        tx_o    : out std_logic;
        cts_n_i : in  std_logic;
        rts_n_o : out std_logic;

        -- configuration (Module 13 drives these from registers) -----------
        cfg_parity_i     : in std_logic_vector(1 downto 0);
        cfg_sw_flow_en_i : in std_logic;
        cfg_hw_flow_en_i : in std_logic;
        cfg_loopback_i   : in std_logic;   -- INTERNAL: tx engine -> rx engine
        cfg_line_loop_i  : in std_logic;   -- LINE: rx pin -> tx pin
        cfg_tx_flush_i   : in std_logic;   -- one cycle
        cfg_rx_flush_i   : in std_logic;   -- one cycle
        cfg_err_clear_i  : in std_logic;   -- one cycle

        cfg_rx_trigger_i : in std_logic_vector(cnt_width(RX_DEPTH+1)-1 downto 0);
        cfg_tx_trigger_i : in std_logic_vector(cnt_width(TX_DEPTH+1)-1 downto 0);

        -- write side ------------------------------------------------------
        wr_data_i  : in  std_logic_vector(DATA_W-1 downto 0);
        wr_valid_i : in  std_logic;
        wr_ready_o : out std_logic;

        -- read side -------------------------------------------------------
        rd_data_o       : out std_logic_vector(DATA_W-1 downto 0);
        rd_parity_err_o : out std_logic;
        rd_frame_err_o  : out std_logic;
        rd_valid_o      : out std_logic;
        rd_ready_i      : in  std_logic;

        -- aggregated status -----------------------------------------------
        tx_trigger_o   : out std_logic;
        rx_trigger_o   : out std_logic;
        tx_empty_o     : out std_logic;
        rx_empty_o     : out std_logic;
        busy_o         : out std_logic;
        err_frame_o    : out std_logic;
        err_parity_o   : out std_logic;
        err_overrun_o  : out std_logic;
        err_break_o    : out std_logic;
        break_active_o : out std_logic;
        any_error_o    : out std_logic;
        rx_push_evt_o  : out std_logic
    );
end entity uart_ip;

architecture rtl of uart_ip is

    constant PARITY_NONE : std_logic_vector(1 downto 0) := "00";

    constant RX_ENTRY_W    : positive := DATA_W + 2;   -- {perr, ferr, data}
    -- The longest LEGAL low run is start + DATA_W zero data bits + a zero
    -- parity bit; the break threshold sits one interval past it.
    constant FRAME_BITS_MAX : positive := DATA_W + 3;
    -- `DATA_W when DATA_W < 8 else 8` is a VHDL-2019 conditional
    -- expression. In 2008 the portable form is a function; it is evaluated
    -- at elaboration either way.
    function min_int (a, b : positive) return positive is
    begin
        if a < b then return a; else return b; end if;
    end function;

    constant SW_CMP_W       : positive := min_int(DATA_W, 8);
    constant TXC_W          : positive := cnt_width(TX_DEPTH + 1);
    constant RXC_W          : positive := cnt_width(RX_DEPTH + 1);

    signal os_tick, baud_tick : std_logic;

    signal cfg_parity_q, cfg_parity_shadow_q : std_logic_vector(1 downto 0);
    signal cfg_pending_q : std_logic;
    signal link_quiet    : std_logic;

    signal tx_line, rx_line : std_logic;

    signal tx_fifo_head  : std_logic_vector(DATA_W-1 downto 0);
    signal tx_fifo_empty, tx_fifo_full : std_logic;
    signal tx_level      : std_logic_vector(TXC_W-1 downto 0);
    signal tx_push, tx_pop : std_logic;
    signal tx_rst_n, rx_rst_n : std_logic;

    signal tx_paused, launch_ok, tx_ready, tx_busy, tx_offer : std_logic;
    signal cts_gated : std_logic;

    signal rx_char : std_logic_vector(DATA_W-1 downto 0);
    signal rx_char_valid, rx_char_ready, rx_perr, rx_ferr, rx_ovr : std_logic;
    signal rx_active, rx_synced : std_logic;

    signal rx_push_entry, rx_pop_entry : std_logic_vector(RX_ENTRY_W-1 downto 0);
    signal rx_fifo_empty, rx_fifo_full, rx_push, rx_ovf_evt : std_logic;
    signal rx_level : std_logic_vector(RXC_W-1 downto 0);
    signal rx_pop   : std_logic;
    signal rd_valid_s : std_logic;

    signal rts_n_int, allow_remote : std_logic;
    signal sw_char : std_logic_vector(7 downto 0);
    signal break_evt : std_logic;
    signal tx_busy_s, tx_empty_s, rx_active_s : std_logic;

    signal all_ones_rx : std_logic_vector(RXC_W-1 downto 0) := (others => '1');
    signal all_ones_tx : std_logic_vector(TXC_W-1 downto 0) := (others => '1');

begin

    -- Parameter legality, checked at ELABORATION. VHDL's assert is a first
    -- class elaboration construct, so an illegal generic set stops the build
    -- rather than waiting for somebody to run a simulation.
    assert FC_RESUME < FC_STOP
        report "uart_ip: FC_RESUME must be below FC_STOP" severity failure;
    assert FC_STOP <= RX_DEPTH
        report "uart_ip: FC_STOP exceeds RX_DEPTH" severity failure;
    assert DATA_W >= 5 and DATA_W <= 9
        report "uart_ip: DATA_W outside the 5..9 range a UART frame supports"
        severity failure;
    assert OVERSAMPLE >= 4
        report "uart_ip: OVERSAMPLE below 4 leaves no usable sampling window"
        severity failure;
    assert CLK_HZ >= BAUD_HZ * OVERSAMPLE
        report "uart_ip: CLK_HZ cannot produce BAUD_HZ at this OVERSAMPLE"
        severity failure;

    ------------------------------------------------------------------------
    --  ONE timing generator, fanned out to both halves -- Chapter 8.4
    ------------------------------------------------------------------------
    u_timing : entity work.uart_timing_gen
        generic map (CLK_HZ => CLK_HZ, BAUD_HZ => BAUD_HZ,
                     OVERSAMPLE => OVERSAMPLE, ACC_W => 24)
        port map (clk => clk, rst_n => rst_n,
                  os_tick_o => os_tick, baud_tick_o => baud_tick);

    ------------------------------------------------------------------------
    --  Configuration capture: frozen while either half is active -- Ch 11.3
    ------------------------------------------------------------------------
    cfg_proc : process (clk, rst_n)
    begin
        if rst_n = '0' then
            cfg_parity_q        <= PARITY_NONE;
            cfg_parity_shadow_q <= PARITY_NONE;
            cfg_pending_q       <= '0';
        elsif rising_edge(clk) then
            if cfg_parity_i /= cfg_parity_shadow_q then
                cfg_parity_shadow_q <= cfg_parity_i;
                cfg_pending_q       <= '1';
            end if;
            if cfg_pending_q = '1' and link_quiet = '1' then
                cfg_parity_q  <= cfg_parity_shadow_q;
                cfg_pending_q <= '0';
            end if;
        end if;
    end process cfg_proc;

    ------------------------------------------------------------------------
    --  The two loopbacks -- Chapter 11.5. They face opposite directions and
    --  are independent. Both set is legal and not a loop, because rx_i is an
    --  input.
    ------------------------------------------------------------------------
    tx_o    <= rx_i    when cfg_line_loop_i = '1' else tx_line;
    rx_line <= tx_line when cfg_loopback_i  = '1' else rx_i;

    ------------------------------------------------------------------------
    --  TX FIFO
    ------------------------------------------------------------------------
    tx_push    <= wr_valid_i and not tx_fifo_full and not cfg_tx_flush_i;
    wr_ready_o <= not tx_fifo_full;
    tx_rst_n   <= rst_n and not cfg_tx_flush_i;

    u_tx_fifo : entity work.uart_sync_fifo
        generic map (WIDTH => DATA_W, DEPTH => TX_DEPTH)
        port map (clk => clk, rst_n => tx_rst_n,
                  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 => open, underflow_evt_o => open);

    ------------------------------------------------------------------------
    --  Flow-control permission: hardware AND software
    ------------------------------------------------------------------------
    cts_gated <= cts_n_i when cfg_hw_flow_en_i = '1' else '0';  -- off = permitted

    u_gate : entity work.uart_tx_gate
        port map (clk => clk, rst_n => rst_n,
                  cts_n_i => cts_gated,
                  tx_busy_i => '0',                 -- NOT in the decision
                  tx_fifo_empty_i => tx_fifo_empty,
                  launch_allowed_o => launch_ok);

    ------------------------------------------------------------------------
    --  Transmitter
    ------------------------------------------------------------------------
    tx_offer <= launch_ok and not tx_paused;
    tx_pop   <= tx_offer and tx_ready;

    u_tx : entity work.uart_tx
        generic map (DATA_W => DATA_W)
        port map (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_s);

    ------------------------------------------------------------------------
    --  Receiver
    ------------------------------------------------------------------------
    u_rx : entity work.uart_rx
        generic map (DATA_W => DATA_W, OVERSAMPLE => OVERSAMPLE)
        port map (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_s, rx_sync_o => rx_synced);

    ------------------------------------------------------------------------
    --  RX FIFO, entries carrying their per-character status -- Ch 10.2
    ------------------------------------------------------------------------
    rx_push_entry <= rx_perr & rx_ferr & rx_char;
    -- The push must be the HANDSHAKE, not merely the offer.
    rx_push       <= rx_char_valid and rx_char_ready and not cfg_rx_flush_i;
    rx_push_evt_o <= rx_push;
    rx_char_ready <= not rx_fifo_full;             -- backpressure the receiver
    rx_rst_n      <= rst_n and not cfg_rx_flush_i;
    rx_pop        <= rd_ready_i and rd_valid_s;

    u_rx_fifo : entity work.uart_sync_fifo
        generic map (WIDTH => RX_ENTRY_W, DEPTH => RX_DEPTH)
        port map (clk => clk, rst_n => rx_rst_n,
                  push_i => rx_push, push_data_i => rx_push_entry,
                  pop_i => rx_pop, 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 => open);

    rd_parity_err_o <= rx_pop_entry(RX_ENTRY_W-1);
    rd_frame_err_o  <= rx_pop_entry(RX_ENTRY_W-2);
    rd_data_o       <= rx_pop_entry(DATA_W-1 downto 0);
    rd_valid_s      <= not rx_fifo_empty;
    rd_valid_o      <= rd_valid_s;

    ------------------------------------------------------------------------
    --  Watermarks -- Chapter 10.3
    ------------------------------------------------------------------------
    u_rx_wm : entity work.uart_fifo_watermark
        generic map (DEPTH => RX_DEPTH)
        port map (level_i => rx_level, rx_level_i => cfg_rx_trigger_i,
                  tx_level_i => all_ones_rx,
                  rx_trigger_o => rx_trigger_o, tx_trigger_o => open);

    u_tx_wm : entity work.uart_fifo_watermark
        generic map (DEPTH => TX_DEPTH)
        port map (level_i => tx_level, rx_level_i => all_ones_tx,
                  tx_level_i => cfg_tx_trigger_i,
                  rx_trigger_o => open, tx_trigger_o => tx_trigger_o);

    ------------------------------------------------------------------------
    --  Hardware flow control from RX occupancy
    ------------------------------------------------------------------------
    u_fc : entity work.uart_flow_ctrl
        generic map (DEPTH => RX_DEPTH, STOP_LEVEL => FC_STOP,
                     RESUME_LEVEL => FC_RESUME)
        port map (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.
    rts_n_o <= rts_n_int when cfg_hw_flow_en_i = '1' else '0';

    ------------------------------------------------------------------------
    --  Software flow control watches the RECEIVED character stream
    ------------------------------------------------------------------------
    sw_gen : if SW_CMP_W = 8 generate
        sw_char <= rx_char(7 downto 0);
    else generate
        sw_char <= (7 downto SW_CMP_W => '0') & rx_char(SW_CMP_W-1 downto 0);
    end generate sw_gen;

    u_xon : entity work.uart_xon_xoff
        port map (clk => clk, rst_n => rst_n,
                  rx_char_valid_i => (rx_char_valid and rx_char_ready),
                  rx_char_data_i => sw_char,
                  sw_flow_en_i => cfg_sw_flow_en_i,
                  tx_paused_o => tx_paused, consume_char_o => open);

    ------------------------------------------------------------------------
    --  Break detection, as an observer -- Chapter 9.4. Fed from the
    --  receiver's SYNCHRONISED line, not from the pin.
    ------------------------------------------------------------------------
    u_break : entity work.uart_break_detect
        generic map (OVERSAMPLE => OVERSAMPLE, FRAME_BITS => FRAME_BITS_MAX)
        port map (clk => clk, rst_n => rst_n, os_tick_i => os_tick,
                  rx_sync_i => rx_synced,
                  break_active_o => break_active_o, break_evt_o => break_evt,
                  break_released_o => open);

    ------------------------------------------------------------------------
    --  Status aggregation -- Chapter 11.2
    ------------------------------------------------------------------------
    u_status : entity work.uart_err_status
        port map (clk => clk, rst_n => rst_n,
                  frame_error_evt_i  => (rx_char_valid and rx_char_ready and rx_ferr),
                  parity_error_evt_i => (rx_char_valid and rx_char_ready and rx_perr),
                  overrun_evt_i      => (rx_ovr or 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);

    tx_empty_s <= tx_fifo_empty;
    tx_empty_o <= tx_empty_s;
    rx_empty_o <= rx_fifo_empty;
    tx_busy    <= tx_busy_s;
    rx_active  <= rx_active_s;

    -- busy_o means WORK IN PROGRESS, not "data exists somewhere". Unread
    -- bytes in the RX FIFO are NOT busy.
    busy_o     <= tx_busy_s or (not tx_empty_s) or rx_active_s;

    -- A STRICTER condition, used only to commit configuration.
    link_quiet <= '1' when tx_busy_s = '0' and tx_empty_s = '1' and rx_active_s = '0'
                  else '0';

end architecture rtl;
PropertyVerilog-2001SystemVerilogVHDL-2008Agree?
sub-blocks instantiated111211yes — SV splits the timing base out
one timing generator, fanned outyesyesyesyes
config committed on link_quietyesyesyesyes
RX push gated on the handshakeyesyesyesyes
busy_o excludes RX occupancyyesyesyesyes
flush via gated resetyesyesyesyes
disabled HW flow drives permittedyesyesyesyes
parity encoding sourcelocalparamuart_parity_pkgplain vectorno — see below
elaboration-time legality checksnone$error in generateassertno — see 11.4
port declaration stylenon-ANSIANSIANSIno — the 10.2 rule again

Three deliberate differences, and each one is a property of the language rather than of the design.

The parity encoding is duplicated in Verilog-2001. There are no packages, so the four mode values are repeated as localparams in every file that needs them. A project that changed the encoding would have to find all of them. VHDL takes the values as a plain two-bit vector at the port, which avoids the duplication by declining to name the values at all — a different trade, not a better one. This is an integration risk, not a style question, and §5 of Chapter 11.4 is where it is managed.

Verilog-2001 cannot check its own parameters at elaboration. The SystemVerilog uses $error inside generate scopes and the VHDL uses concurrent assert; both stop the build on an illegal generic set. The Verilog documents the legal ranges in a header comment and relies on the integrator. That is a real capability gap and worth knowing before a project standardises on 2001.

VHDL needed one substitution. DATA_W when DATA_W < 8 else 8 is a VHDL-2019 conditional expression; in 2008 the portable form is a function, evaluated at elaboration exactly the same way:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
function min_int (a, b : positive) return positive is
begin
    if a < b then return a; else return b; end if;
end function;

constant SW_CMP_W : positive := min_int(DATA_W, 8);

9. The Integration Testbenches

These testbenches deliberately do not re-test the sub-blocks. Every one of the twelve already has its own self-checking testbench in Modules 6–10, and repeating those checks here would prove nothing new. What is tested is what only exists once the blocks meet:

ScenarioWhy it cannot be tested at block level
end-to-end integrity through internal loopbackneeds TX, RX, both FIFOs and the timing generator at once
busy_o excluding stored databusy_o does not exist below the top level
write-side backpressurethe FIFO's full must reach the IP's wr_ready_o
RX saturation and which bytes survivethe receiver, its holding register and the FIFO must be wired together
receiving while the transmitter is idlelink_quiet combines two sub-blocks' status
flushthe gated reset reaches one FIFO and must not disturb the other
the two loopbackspure integration — neither exists in any sub-block
status aggregation and software clearthe aggregator's inputs come from four different blocks

INDEPENDENCE: each checker keeps its own queue of what it wrote and compares it against what it read. None of them inspects a FIFO pointer, the transmitter's state, or any other sub-block internal. The one exception is explicitly marked: the VHDL testbench reads cfg_parity_q through a VHDL-2008 external name to observe when a configuration commits, and that value is used only for that diagnostic, never to decide a data check.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  tb_uart_ip_v — self-checking Verilog-2001 integration testbench
//
//  This is an INTEGRATION testbench, and it checks the things that only
//  exist once the blocks meet. Every sub-block already has its own unit
//  testbench in Modules 6-10; repeating those checks here would prove
//  nothing new. What is tested instead:
//
//    end-to-end data integrity through internal loopback
//    the write/read handshakes and their backpressure
//    busy_o meaning WORK IN PROGRESS, not "data exists somewhere"
//    the two loopbacks being independent and oppositely directed
//    flush discarding queued data without disturbing the other direction
//    status aggregation and the software clear
//    configuration deferred until the link is quiet
//
//  INDEPENDENCE: the checker keeps its own queue of what it WROTE and
//  compares it against what it READ. It never inspects a FIFO pointer, the
//  transmitter's state, or any internal of the twelve sub-blocks.
//
//  Timing is scaled (32 MHz / 1 Mbaud) so a frame is 32 clocks rather than
//  8,680. The RATIOS are identical to the 100 MHz / 115,200 case; only the
//  wall-clock cost of the regression changes.
//===========================================================================
`timescale 1ns/1ps

module tb_uart_ip_v;

    localparam CLK_HZ     = 32000000;
    localparam BAUD_HZ    = 1000000;
    localparam DATA_W     = 8;
    localparam OVERSAMPLE = 16;
    localparam TX_DEPTH   = 16;
    localparam RX_DEPTH   = 16;
    localparam CW         = 5;             // clog2(16+1)
    localparam BIT_CLKS   = CLK_HZ / BAUD_HZ;          // 32
    localparam FRAME_CLKS = BIT_CLKS * 10;             // 8N1

    localparam [1:0] PARITY_NONE = 2'b00, PARITY_EVEN = 2'b01, PARITY_ODD = 2'b10;

    reg clk = 1'b0, rst_n = 1'b0;
    always #5 clk = ~clk;

    reg  [1:0] cfg_parity   = PARITY_NONE;
    reg        cfg_sw_flow  = 1'b0, cfg_hw_flow = 1'b0;
    reg        cfg_loopback = 1'b0, cfg_line_loop = 1'b0;
    reg        cfg_tx_flush = 1'b0, cfg_rx_flush = 1'b0, cfg_err_clear = 1'b0;
    reg  [CW-1:0] cfg_rx_trig = 5'd12, cfg_tx_trig = 5'd4;

    reg  [DATA_W-1:0] wr_data = 8'h00;
    reg               wr_valid = 1'b0;
    wire              wr_ready;

    wire [DATA_W-1:0] rd_data;
    wire              rd_perr, rd_ferr, rd_valid;
    reg               rd_ready = 1'b0;

    reg  rx_pin = 1'b1, cts_n = 1'b0;
    wire tx_pin, rts_n;

    wire tx_trig, rx_trig, tx_empty, rx_empty, busy;
    wire e_frame, e_parity, e_overrun, e_break, brk_active, any_err, rx_push_evt;

    uart_ip_v #(.CLK_HZ(CLK_HZ), .BAUD_HZ(BAUD_HZ), .DATA_W(DATA_W),
                .OVERSAMPLE(OVERSAMPLE), .TX_DEPTH(TX_DEPTH), .RX_DEPTH(RX_DEPTH),
                .FC_STOP(12), .FC_RESUME(6)) dut (
        .clk(clk), .rst_n(rst_n),
        .rx_i(rx_pin), .tx_o(tx_pin), .cts_n_i(cts_n), .rts_n_o(rts_n),
        .cfg_parity_i(cfg_parity), .cfg_sw_flow_en_i(cfg_sw_flow),
        .cfg_hw_flow_en_i(cfg_hw_flow), .cfg_loopback_i(cfg_loopback),
        .cfg_line_loop_i(cfg_line_loop),
        .cfg_tx_flush_i(cfg_tx_flush), .cfg_rx_flush_i(cfg_rx_flush),
        .cfg_err_clear_i(cfg_err_clear),
        .cfg_rx_trigger_i(cfg_rx_trig), .cfg_tx_trigger_i(cfg_tx_trig),
        .wr_data_i(wr_data), .wr_valid_i(wr_valid), .wr_ready_o(wr_ready),
        .rd_data_o(rd_data), .rd_parity_err_o(rd_perr), .rd_frame_err_o(rd_ferr),
        .rd_valid_o(rd_valid), .rd_ready_i(rd_ready),
        .tx_trigger_o(tx_trig), .rx_trigger_o(rx_trig),
        .tx_empty_o(tx_empty), .rx_empty_o(rx_empty), .busy_o(busy),
        .err_frame_o(e_frame), .err_parity_o(e_parity), .err_overrun_o(e_overrun),
        .err_break_o(e_break), .break_active_o(brk_active), .any_error_o(any_err),
        .rx_push_evt_o(rx_push_evt));

    integer checks = 0, failures = 0;

    task check;
        input cond;
        input [8*60-1:0] name;
        begin
            checks = checks + 1;
            if (cond) $display("  PASS %0s", name);
            else begin failures = failures + 1; $display("  FAIL %0s", name); end
        end
    endtask

    //-----------------------------------------------------------------------
    //  The independent oracle: two queues, kept by the testbench.
    //-----------------------------------------------------------------------
    reg [DATA_W-1:0] sent [0:255];
    reg [DATA_W-1:0] got  [0:255];
    integer n_sent = 0, n_got = 0;
    integer got_perr = 0, got_ferr = 0;

    // Collect every completed read handshake. This is the ONLY way the
    // testbench learns what the IP delivered.
    always @(posedge clk) if (rst_n) begin
        if (rd_valid && rd_ready) begin
            got[n_got] = rd_data;
            if (rd_perr) got_perr = got_perr + 1;
            if (rd_ferr) got_ferr = got_ferr + 1;
            n_got = n_got + 1;
        end
    end

    // Write one byte, honouring wr_ready. Records it in the model.
    task write_byte;
        input [DATA_W-1:0] d;
        begin
            @(negedge clk);
            while (!wr_ready) @(negedge clk);
            wr_data = d; wr_valid = 1'b1;
            @(negedge clk);
            wr_valid = 1'b0;
            sent[n_sent] = d; n_sent = n_sent + 1;
        end
    endtask

    // Offer a byte without recording it -- used when the write is EXPECTED
    // to be refused or discarded.
    task poke_byte;
        input [DATA_W-1:0] d;
        begin
            @(negedge clk) wr_data = d; wr_valid = 1'b1;
            @(negedge clk) wr_valid = 1'b0;
        end
    endtask

    task drain;                 // let the consumer read for N frame times
        input integer frames;
        begin
            rd_ready = 1'b1;
            repeat (frames * FRAME_CLKS) @(posedge clk);
            @(negedge clk) rd_ready = 1'b0;
        end
    endtask

    task settle;
        begin repeat (4) @(negedge clk); end
    endtask

    integer i, mism, base_got;

    // Drive one 8N1 frame onto rx_i at the configured baud. This lets the
    // testbench RECEIVE while the transmitter is idle, which is the only way
    // to exercise rx_active independently of tx_busy.
    task drive_frame;
        input [7:0] d;
        integer b;
        begin
            @(negedge clk) rx_pin = 1'b0;                 // start
            repeat (BIT_CLKS) @(posedge clk);
            for (b = 0; b < 8; b = b + 1) begin
                @(negedge clk) rx_pin = d[b];             // LSB first
                repeat (BIT_CLKS) @(posedge clk);
            end
            @(negedge clk) rx_pin = 1'b1;                 // stop
            repeat (BIT_CLKS) @(posedge clk);
        end
    endtask

    integer i2, base2;
    reg [1:0] cfg_mid_frame;
    reg       rx_active_mid;


    initial begin
        #20_000_000;
        $display("  FAIL watchdog: simulation did not finish");
        $display("== %0d checks, %0d failures ==", checks+1, failures+1);
        $display("   RESULT: VERILOG UART-IP TESTS FAILED (timeout)");
        $finish;
    end

    initial begin
        $display("== uart_ip_v : self-checking Verilog integration testbench ==");

        //-------------------------------------------------------------------
        //  Reset contract
        //-------------------------------------------------------------------
        rst_n = 1'b0;
        repeat (8) @(negedge clk);
        check(tx_empty === 1'b1 && rx_empty === 1'b1, "reset: both FIFOs empty");
        check(busy === 1'b0,                          "reset: not busy");
        check(any_err === 1'b0,                       "reset: no sticky errors");
        check(wr_ready === 1'b1,                      "reset: ready to accept a write");
        check(rd_valid === 1'b0,                      "reset: nothing to read");
        check(tx_pin === 1'b1,                        "reset: TX line idles MARK");
        @(negedge clk) rst_n = 1'b1;
        settle;

        //-------------------------------------------------------------------
        //  End-to-end through INTERNAL loopback, 8N1
        //-------------------------------------------------------------------
        @(negedge clk) cfg_loopback = 1'b1;
        settle;
        base_got = n_got;
        for (i = 0; i < 8; i = i + 1) write_byte(8'h30 + i[7:0]);
        drain(14);
        settle;

        check(n_got == base_got + 8, "loopback: all 8 bytes came back");
        mism = 0;
        for (i = 0; i < 8; i = i + 1)
            if (got[base_got + i] !== sent[i]) mism = mism + 1;
        check(mism == 0,             "loopback: values and ORDER preserved");
        check(got_perr == 0,         "loopback: no parity errors reported");
        check(got_ferr == 0,         "loopback: no framing errors reported");
        check(any_err === 1'b0,      "loopback: no sticky error raised");
        check(busy === 1'b0,         "after the burst: busy clears");
        check(tx_empty === 1'b1,     "after the burst: TX FIFO drained");

        //-------------------------------------------------------------------
        //  busy_o must NOT include unread RX occupancy
        //-------------------------------------------------------------------
        // Send one byte, let it arrive, and do NOT read it. The IP has no
        // work in progress, so busy must be low even though data exists.
        write_byte(8'h5A);
        repeat (3 * FRAME_CLKS) @(posedge clk);
        settle;
        check(rx_empty === 1'b0, "one byte waiting in the RX FIFO");
        check(busy === 1'b0,     "busy is LOW with unread data -- work, not storage");
        drain(2);
        settle;

        //-------------------------------------------------------------------
        //  Write-side backpressure
        //-------------------------------------------------------------------
        // Fill the TX FIFO faster than the line can drain it. wr_ready must
        // fall, and no byte may be accepted while it is low.
        // TX_DEPTH pushes leave the FIFO one entry short of full, because
        // the transmitter pops the first byte the moment it is offered.
        // Two extra pushes guarantee the boundary is actually reached.
        for (i = 0; i < TX_DEPTH + 2; i = i + 1) poke_byte(8'hC0 + i[7:0]);
        settle;
        check(wr_ready === 1'b0, "TX FIFO full: wr_ready deasserts");
        check(busy === 1'b1,     "TX FIFO full: busy asserted (work queued)");
        drain(24);
        settle;
        check(wr_ready === 1'b1, "after draining: wr_ready returns");

        //-------------------------------------------------------------------
        //  TX flush discards queued bytes and leaves RX alone
        //-------------------------------------------------------------------
        @(negedge clk) cfg_loopback = 1'b0;      // do not loop these back
        settle;
        for (i = 0; i < 6; i = i + 1) poke_byte(8'hE0 + i[7:0]);
        settle;
        check(tx_empty === 1'b0, "bytes queued before the flush");
        @(negedge clk) cfg_tx_flush = 1'b1;
        @(negedge clk) cfg_tx_flush = 1'b0;
        settle;
        check(tx_empty === 1'b1, "tx flush empties the TX FIFO");
        check(rx_empty === 1'b1, "tx flush left the RX side untouched");

        //-------------------------------------------------------------------
        //  RX flush
        //-------------------------------------------------------------------
        @(negedge clk) cfg_loopback = 1'b1;
        settle;
        write_byte(8'h77);
        write_byte(8'h88);
        repeat (4 * FRAME_CLKS) @(posedge clk);
        settle;
        check(rx_empty === 1'b0, "bytes received before the flush");
        @(negedge clk) cfg_rx_flush = 1'b1;
        @(negedge clk) cfg_rx_flush = 1'b0;
        settle;
        check(rx_empty === 1'b1, "rx flush empties the RX FIFO");
        n_sent = 0;                              // the model forgets them too

        //-------------------------------------------------------------------
        //  LINE loopback is a different path and faces the other way
        //-------------------------------------------------------------------
        @(negedge clk) cfg_loopback = 1'b0; cfg_line_loop = 1'b1;
        settle;
        @(negedge clk) rx_pin = 1'b0;
        settle;
        check(tx_pin === 1'b0, "line loopback: rx pin drives the tx pin");
        @(negedge clk) rx_pin = 1'b1;
        settle;
        check(tx_pin === 1'b1, "line loopback: follows the rx pin back to mark");
        @(negedge clk) cfg_line_loop = 1'b0;
        settle;
        check(tx_pin === 1'b1, "line loopback off: tx returns to the engine");

        //-------------------------------------------------------------------
        //  Status aggregation and the software clear
        //-------------------------------------------------------------------
        // A break on the line raises the sticky break flag and any_error.
        @(negedge clk) rx_pin = 1'b0;
        repeat (16 * FRAME_CLKS) @(posedge clk);
        settle;
        check(brk_active === 1'b1, "sustained low: break declared");
        check(e_break === 1'b1,    "break raises its sticky flag");
        check(any_err === 1'b1,    "any_error aggregates it");
        @(negedge clk) rx_pin = 1'b1;
        repeat (2 * FRAME_CLKS) @(posedge clk);
        settle;
        check(brk_active === 1'b0, "line released: break condition clears");
        check(e_break === 1'b1,    "but the STICKY flag survives the release");
        @(negedge clk) cfg_err_clear = 1'b1;
        @(negedge clk) cfg_err_clear = 1'b0;
        settle;
        check(e_break === 1'b0,    "software clear drops the sticky flag");
        check(any_err === 1'b0,    "and any_error with it");

        // A break is a long run of SPACE, which the receiver decodes as a
        // stream of malformed characters. They are legitimately in the RX
        // FIFO now, and the end-to-end comparisons below would read them as
        // the bytes they just wrote -- so the queue is flushed and the
        // checker's own tally is reset with it.
        @(negedge clk) cfg_rx_flush = 1'b1;
        @(negedge clk) cfg_rx_flush = 1'b0;
        settle;
        check(rx_empty === 1'b1, "break residue flushed from the RX FIFO");
        n_got = 0; got_perr = 0; got_ferr = 0;

        //-------------------------------------------------------------------
        //  Hardware flow control follows RX occupancy
        //-------------------------------------------------------------------
        @(negedge clk) cfg_hw_flow = 1'b0;
        settle;
        check(rts_n === 1'b0, "hw flow DISABLED: rts_n held permitted, not idle");
        @(negedge clk) cfg_hw_flow = 1'b1; cfg_loopback = 1'b1;
        settle;
        check(rts_n === 1'b0, "hw flow enabled, RX empty: still permitting");

        //-------------------------------------------------------------------
        //  Configuration is deferred until the link is quiet
        //-------------------------------------------------------------------
        // Queue a burst, change parity mid-burst, and require that every
        // byte still arrives intact -- the change must not reformat a frame
        // that is already in flight.
        @(negedge clk) cfg_hw_flow = 1'b0;
        settle;
        n_sent = 0; base_got = n_got;
        rd_ready = 1'b1;
        for (i = 0; i < 4; i = i + 1) write_byte(8'h90 + i[7:0]);
        repeat (FRAME_CLKS) @(posedge clk);
        @(negedge clk) cfg_parity = PARITY_EVEN;      // mid-burst change
        repeat (13 * FRAME_CLKS) @(posedge clk);
        @(negedge clk) rd_ready = 1'b0;
        settle;
        check(n_got == base_got + 4, "config change mid-burst: all 4 bytes arrived");
        mism = 0;
        for (i = 0; i < 4; i = i + 1)
            if (got[base_got + i] !== sent[i]) mism = mism + 1;
        check(mism == 0,            "config change mid-burst: values intact");
        check(got_ferr == 0,        "config change mid-burst: no framing errors");

        //-------------------------------------------------------------------
        //  The new parity mode is in force afterwards, and still loops back
        //-------------------------------------------------------------------
        n_sent = 0; base_got = n_got;
        for (i = 0; i < 4; i = i + 1) write_byte(8'hA0 + i[7:0]);
        drain(10);
        settle;
        check(n_got == base_got + 4, "even parity: 4 more bytes round-tripped");
        mism = 0;
        for (i = 0; i < 4; i = i + 1)
            if (got[base_got + i] !== sent[i]) mism = mism + 1;
        check(mism == 0,             "even parity: values intact");
        check(got_perr == 0,         "even parity: no parity errors on a clean loop");

        //-------------------------------------------------------------------
        //  RX FIFO saturation: backpressure, overrun, and the survivors
        //-------------------------------------------------------------------
        // Every earlier test drained the RX FIFO promptly, so the receive
        // side was never pushed to its boundary. This is where the
        // handshake, the backpressure and the second overrun source live.
        @(negedge clk) cfg_rx_flush = 1'b1;
        @(negedge clk) cfg_rx_flush = 1'b0;
        @(negedge clk) cfg_err_clear = 1'b1;
        @(negedge clk) cfg_err_clear = 1'b0;
        settle;
        n_got = 0; got_perr = 0; got_ferr = 0; n_sent = 0;

        // Send more than the RX FIFO can hold, and never read.
        rd_ready = 1'b0;
        for (i = 0; i < RX_DEPTH + 6; i = i + 1) write_byte(8'h20 + i[7:0]);
        repeat ((RX_DEPTH + 10) * FRAME_CLKS) @(posedge clk);
        settle;

        check(rx_empty === 1'b0,   "RX saturation: queue holds data");
        check(e_overrun === 1'b1,  "RX saturation: overrun sticky raised");
        check(any_err === 1'b1,    "RX saturation: any_error aggregates it");
        check(rx_trig === 1'b1,    "RX saturation: the watermark is asking for service");

        // Drain and check WHICH bytes survived. The queue keeps the OLDEST
        // entries -- 10.2's policy -- so the first RX_DEPTH bytes sent are
        // the ones that come back, in order.
        base_got = n_got;
        drain(RX_DEPTH + 6);
        settle;
        // The system's receive capacity is RX_DEPTH + 1, not RX_DEPTH: the
        // receiver of Module 6 has its OWN one-entry holding register, and
        // it is still holding a character when the FIFO refuses the next
        // push. That extra entry is invisible in any block-level test and
        // only appears once the two are wired together -- which is exactly
        // what an integration testbench is for.
        $display("  INFO RX saturation: sent %0d, survived %0d (RX_DEPTH=%0d + 1 holding)",
                 n_sent, n_got - base_got, RX_DEPTH);
        check(n_got == base_got + RX_DEPTH + 1,
              "RX saturation: DEPTH+1 survived -- FIFO plus the RX holding register");
        mism = 0;
        for (i = 0; i < RX_DEPTH + 1; i = i + 1)
            if (got[base_got + i] !== sent[i]) mism = mism + 1;
        check(mism == 0,            "RX saturation: the OLDEST bytes survived, in order");
        check(rx_empty === 1'b1,    "RX saturation: queue drains completely afterwards");

        @(negedge clk) cfg_err_clear = 1'b1;
        @(negedge clk) cfg_err_clear = 1'b0;
        settle;

        //-------------------------------------------------------------------
        //  Receiving while the transmitter is idle
        //-------------------------------------------------------------------
        // link_quiet must include rx_active, not just tx_busy. With internal
        // loopback the two always coincide, so this drives the pin directly.
        @(negedge clk) cfg_loopback = 1'b0; cfg_parity = PARITY_NONE;
        settle;
        repeat (4 * FRAME_CLKS) @(posedge clk);       // let it commit while quiet
        settle;
        check(busy === 1'b0,      "idle: not busy before an external frame");

        // Sample the committed configuration WHILE the frame is still in
        // flight. The earlier form of this check was `parity_q == NONE ||
        // !pending`, which a design that committed immediately satisfies
        // through the second term -- it passed a mutant it was written to
        // catch. The strict form samples one value at one instant.
        cfg_mid_frame = 2'bxx;
        fork
            drive_frame(8'h6C);
            begin
                repeat (BIT_CLKS * 3) @(posedge clk);
                check(busy === 1'b1, "receiving with TX idle: busy is asserted");
                @(negedge clk) cfg_parity = PARITY_ODD;
                repeat (BIT_CLKS * 3) @(posedge clk);
                cfg_mid_frame = dut.cfg_parity_q;      // sampled MID-FRAME
                rx_active_mid = dut.rx_active;
            end
        join
        settle;
        check(rx_active_mid === 1'b1,
              "the sample really was taken while a frame was in flight");
        check(cfg_mid_frame === PARITY_NONE,
              "config change mid-receive was NOT applied while receiving");
        repeat (3 * FRAME_CLKS) @(posedge clk);
        settle;
        check(dut.cfg_parity_q === PARITY_ODD, "and commits once the line is quiet");

        base_got = n_got;
        drain(3);
        settle;
        check(n_got == base_got + 1,     "externally driven frame was received");
        check(got[base_got] === 8'h6C,   "externally driven frame arrived intact");

        @(negedge clk) cfg_parity = PARITY_NONE; cfg_loopback = 1'b1;
        @(negedge clk) cfg_err_clear = 1'b1;
        @(negedge clk) cfg_err_clear = 1'b0;
        repeat (4 * FRAME_CLKS) @(posedge clk);
        settle;

        //-------------------------------------------------------------------
        //  Reset while loaded
        //-------------------------------------------------------------------
        for (i = 0; i < 4; i = i + 1) poke_byte(8'hF0 + i[7:0]);
        settle;
        check(tx_empty === 1'b0, "data queued before reset");
        @(negedge clk) rst_n = 1'b0;
        repeat (4) @(negedge clk);
        check(tx_empty === 1'b1 && rx_empty === 1'b1, "reset empties both FIFOs");
        check(any_err === 1'b0,                       "reset clears sticky status");
        check(busy === 1'b0,                          "reset clears busy");
        @(negedge clk) rst_n = 1'b1;
        settle;

        $display("== %0d checks, %0d failures ==", checks, failures);
        if (failures == 0) $display("   RESULT: ALL VERILOG UART-IP TESTS PASSED");
        else               $display("   RESULT: VERILOG UART-IP TESTS FAILED");
        $finish;
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  tb_uart_ip_sv — self-checking Verilog-2001 integration testbench
//
//  This is an INTEGRATION testbench, and it checks the things that only
//  exist once the blocks meet. Every sub-block already has its own unit
//  testbench in Modules 6-10; repeating those checks here would prove
//  nothing new. What is tested instead:
//
//    end-to-end data integrity through internal loopback
//    the write/read handshakes and their backpressure
//    busy_o meaning WORK IN PROGRESS, not "data exists somewhere"
//    the two loopbacks being independent and oppositely directed
//    flush discarding queued data without disturbing the other direction
//    status aggregation and the software clear
//    configuration deferred until the link is quiet
//
//  INDEPENDENCE: the checker keeps its own queue of what it WROTE and
//  compares it against what it READ. It never inspects a FIFO pointer, the
//  transmitter's state, or any internal of the twelve sub-blocks.
//
//  Timing is scaled (32 MHz / 1 Mbaud) so a frame is 32 clocks rather than
//  8,680. The RATIOS are identical to the 100 MHz / 115,200 case; only the
//  wall-clock cost of the regression changes.
//===========================================================================
`timescale 1ns/1ps

module tb_uart_ip_sv;

    import uart_parity_pkg::*;

    localparam CLK_HZ     = 32000000;
    localparam BAUD_HZ    = 1000000;
    localparam DATA_W     = 8;
    localparam OVERSAMPLE = 16;
    localparam TX_DEPTH   = 16;
    localparam RX_DEPTH   = 16;
    localparam CW         = 5;             // clog2(16+1)
    localparam BIT_CLKS   = CLK_HZ / BAUD_HZ;          // 32
    localparam FRAME_CLKS = BIT_CLKS * 10;             // 8N1

    // parity_mode_t comes from uart_parity_pkg

    logic clk = 1'b0, rst_n = 1'b0;
    always #5ns clk = ~clk;

    logic [1:0] cfg_parity   = PARITY_NONE;
    logic cfg_sw_flow  = 1'b0, cfg_hw_flow = 1'b0;
    logic cfg_loopback = 1'b0, cfg_line_loop = 1'b0;
    logic cfg_tx_flush = 1'b0, cfg_rx_flush = 1'b0, cfg_err_clear = 1'b0;
    logic [CW-1:0] cfg_rx_trig = 5'd12, cfg_tx_trig = 5'd4;

    logic [DATA_W-1:0] wr_data = 8'h00;
    logic wr_valid = 1'b0;
    logic wr_ready;

    logic [DATA_W-1:0] rd_data;
    logic rd_perr, rd_ferr, rd_valid;
    logic rd_ready = 1'b0;

    logic rx_pin = 1'b1, cts_n = 1'b0;
    logic tx_pin, rts_n;

    logic tx_trig, rx_trig, tx_empty, rx_empty, busy;
    logic e_frame, e_parity, e_overrun, e_break, brk_active, any_err, rx_push_evt;

    uart_ip #(.CLK_HZ(CLK_HZ), .BAUD_HZ(BAUD_HZ), .DATA_W(DATA_W),
                .OVERSAMPLE(OVERSAMPLE), .TX_DEPTH(TX_DEPTH), .RX_DEPTH(RX_DEPTH),
                .FC_STOP(12), .FC_RESUME(6)) dut (
        .clk(clk), .rst_n(rst_n),
        .rx_i(rx_pin), .tx_o(tx_pin), .cts_n_i(cts_n), .rts_n_o(rts_n),
        .cfg_parity_i(cfg_parity), .cfg_sw_flow_en_i(cfg_sw_flow),
        .cfg_hw_flow_en_i(cfg_hw_flow), .cfg_loopback_i(cfg_loopback),
        .cfg_line_loop_i(cfg_line_loop),
        .cfg_tx_flush_i(cfg_tx_flush), .cfg_rx_flush_i(cfg_rx_flush),
        .cfg_err_clear_i(cfg_err_clear),
        .cfg_rx_trigger_i(cfg_rx_trig), .cfg_tx_trigger_i(cfg_tx_trig),
        .wr_data_i(wr_data), .wr_valid_i(wr_valid), .wr_ready_o(wr_ready),
        .rd_data_o(rd_data), .rd_parity_err_o(rd_perr), .rd_frame_err_o(rd_ferr),
        .rd_valid_o(rd_valid), .rd_ready_i(rd_ready),
        .tx_trigger_o(tx_trig), .rx_trigger_o(rx_trig),
        .tx_empty_o(tx_empty), .rx_empty_o(rx_empty), .busy_o(busy),
        .err_frame_o(e_frame), .err_parity_o(e_parity), .err_overrun_o(e_overrun),
        .err_break_o(e_break), .break_active_o(brk_active), .any_error_o(any_err),
        .rx_push_evt_o(rx_push_evt));

    int unsigned checks = 0, failures = 0;

    task automatic check(input bit cond, input string name);
        checks++;
        if (cond) $display("  PASS %0s", name);
        else begin failures++; $display("  FAIL %0s", name); end
    endtask

    //-----------------------------------------------------------------------
    //  The independent oracle: two queues, kept by the testbench.
    //-----------------------------------------------------------------------
    logic [DATA_W-1:0] sent [0:255];
    logic [DATA_W-1:0] got  [0:255];
    int n_sent = 0, n_got = 0;
    int got_perr = 0, got_ferr = 0;

    // Collect every completed read handshake. This is the ONLY way the
    // testbench learns what the IP delivered.
    always @(posedge clk) if (rst_n) begin
        if (rd_valid && rd_ready) begin
            got[n_got] = rd_data;
            if (rd_perr) got_perr = got_perr + 1;
            if (rd_ferr) got_ferr = got_ferr + 1;
            n_got = n_got + 1;
        end
    end

    // Write one byte, honouring wr_ready. Records it in the model.
    task automatic write_byte(input logic [DATA_W-1:0] d);
            @(negedge clk);
            while (!wr_ready) @(negedge clk);
            wr_data = d; wr_valid = 1'b1;
            @(negedge clk);
            wr_valid = 1'b0;
            sent[n_sent] = d; n_sent = n_sent + 1;
    endtask

    // Offer a byte without recording it -- used when the write is EXPECTED
    // to be refused or discarded.
    task automatic poke_byte(input logic [DATA_W-1:0] d);
            @(negedge clk) wr_data = d; wr_valid = 1'b1;
            @(negedge clk) wr_valid = 1'b0;
    endtask

    task drain;                 // let the consumer read for N frame times
        input integer frames;
        begin
            rd_ready = 1'b1;
            repeat (frames * FRAME_CLKS) @(posedge clk);
            @(negedge clk) rd_ready = 1'b0;
        end
    endtask

    task automatic settle(); repeat (4) @(negedge clk); endtask

    int i, mism, base_got;

    // Drive one 8N1 frame onto rx_i at the configured baud. This lets the
    // testbench RECEIVE while the transmitter is idle, which is the only way
    // to exercise rx_active independently of tx_busy.
    task automatic drive_frame(input logic [7:0] d);
        @(negedge clk) rx_pin = 1'b0;                 // start
        repeat (BIT_CLKS) @(posedge clk);
        for (int b = 0; b < 8; b++) begin
            @(negedge clk) rx_pin = d[b];             // LSB first
            repeat (BIT_CLKS) @(posedge clk);
        end
        @(negedge clk) rx_pin = 1'b1;                 // stop
        repeat (BIT_CLKS) @(posedge clk);
    endtask

    int i2, base2;
    logic [1:0] cfg_mid_frame;
    logic rx_active_mid;


    initial begin
        #20ms;
        $display("  FAIL watchdog: simulation did not finish");
        $display("== %0d checks, %0d failures ==", checks+1, failures+1);
        $display("   RESULT: SYSTEMVERILOG UART-IP TESTS FAILED (timeout)");
        $finish;
    end

    initial begin
        $display("== uart_ip : self-checking SystemVerilog integration testbench ==");

        //-------------------------------------------------------------------
        //  Reset contract
        //-------------------------------------------------------------------
        rst_n = 1'b0;
        repeat (8) @(negedge clk);
        check(tx_empty === 1'b1 && rx_empty === 1'b1, "reset: both FIFOs empty");
        check(busy === 1'b0,                          "reset: not busy");
        check(any_err === 1'b0,                       "reset: no sticky errors");
        check(wr_ready === 1'b1,                      "reset: ready to accept a write");
        check(rd_valid === 1'b0,                      "reset: nothing to read");
        check(tx_pin === 1'b1,                        "reset: TX line idles MARK");
        @(negedge clk) rst_n = 1'b1;
        settle();

        //-------------------------------------------------------------------
        //  End-to-end through INTERNAL loopback, 8N1
        //-------------------------------------------------------------------
        @(negedge clk) cfg_loopback = 1'b1;
        settle();
        base_got = n_got;
        for (i = 0; i < 8; i = i + 1) write_byte(8'h30 + i[7:0]);
        drain(14);
        settle();

        check(n_got == base_got + 8, "loopback: all 8 bytes came back");
        mism = 0;
        for (i = 0; i < 8; i = i + 1)
            if (got[base_got + i] !== sent[i]) mism = mism + 1;
        check(mism == 0,             "loopback: values and ORDER preserved");
        check(got_perr == 0,         "loopback: no parity errors reported");
        check(got_ferr == 0,         "loopback: no framing errors reported");
        check(any_err === 1'b0,      "loopback: no sticky error raised");
        check(busy === 1'b0,         "after the burst: busy clears");
        check(tx_empty === 1'b1,     "after the burst: TX FIFO drained");

        //-------------------------------------------------------------------
        //  busy_o must NOT include unread RX occupancy
        //-------------------------------------------------------------------
        // Send one byte, let it arrive, and do NOT read it. The IP has no
        // work in progress, so busy must be low even though data exists.
        write_byte(8'h5A);
        repeat (3 * FRAME_CLKS) @(posedge clk);
        settle();
        check(rx_empty === 1'b0, "one byte waiting in the RX FIFO");
        check(busy === 1'b0,     "busy is LOW with unread data -- work, not storage");
        drain(2);
        settle();

        //-------------------------------------------------------------------
        //  Write-side backpressure
        //-------------------------------------------------------------------
        // Fill the TX FIFO faster than the line can drain it. wr_ready must
        // fall, and no byte may be accepted while it is low.
        // TX_DEPTH pushes leave the FIFO one entry short of full, because
        // the transmitter pops the first byte the moment it is offered.
        // Two extra pushes guarantee the boundary is actually reached.
        for (i = 0; i < TX_DEPTH + 2; i = i + 1) poke_byte(8'hC0 + i[7:0]);
        settle();
        check(wr_ready === 1'b0, "TX FIFO full: wr_ready deasserts");
        check(busy === 1'b1,     "TX FIFO full: busy asserted (work queued)");
        drain(24);
        settle();
        check(wr_ready === 1'b1, "after draining: wr_ready returns");

        //-------------------------------------------------------------------
        //  TX flush discards queued bytes and leaves RX alone
        //-------------------------------------------------------------------
        @(negedge clk) cfg_loopback = 1'b0;      // do not loop these back
        settle();
        for (i = 0; i < 6; i = i + 1) poke_byte(8'hE0 + i[7:0]);
        settle();
        check(tx_empty === 1'b0, "bytes queued before the flush");
        @(negedge clk) cfg_tx_flush = 1'b1;
        @(negedge clk) cfg_tx_flush = 1'b0;
        settle();
        check(tx_empty === 1'b1, "tx flush empties the TX FIFO");
        check(rx_empty === 1'b1, "tx flush left the RX side untouched");

        //-------------------------------------------------------------------
        //  RX flush
        //-------------------------------------------------------------------
        @(negedge clk) cfg_loopback = 1'b1;
        settle();
        write_byte(8'h77);
        write_byte(8'h88);
        repeat (4 * FRAME_CLKS) @(posedge clk);
        settle();
        check(rx_empty === 1'b0, "bytes received before the flush");
        @(negedge clk) cfg_rx_flush = 1'b1;
        @(negedge clk) cfg_rx_flush = 1'b0;
        settle();
        check(rx_empty === 1'b1, "rx flush empties the RX FIFO");
        n_sent = 0;                              // the model forgets them too

        //-------------------------------------------------------------------
        //  LINE loopback is a different path and faces the other way
        //-------------------------------------------------------------------
        @(negedge clk) cfg_loopback = 1'b0; cfg_line_loop = 1'b1;
        settle();
        @(negedge clk) rx_pin = 1'b0;
        settle();
        check(tx_pin === 1'b0, "line loopback: rx pin drives the tx pin");
        @(negedge clk) rx_pin = 1'b1;
        settle();
        check(tx_pin === 1'b1, "line loopback: follows the rx pin back to mark");
        @(negedge clk) cfg_line_loop = 1'b0;
        settle();
        check(tx_pin === 1'b1, "line loopback off: tx returns to the engine");

        //-------------------------------------------------------------------
        //  Status aggregation and the software clear
        //-------------------------------------------------------------------
        // A break on the line raises the sticky break flag and any_error.
        @(negedge clk) rx_pin = 1'b0;
        repeat (16 * FRAME_CLKS) @(posedge clk);
        settle();
        check(brk_active === 1'b1, "sustained low: break declared");
        check(e_break === 1'b1,    "break raises its sticky flag");
        check(any_err === 1'b1,    "any_error aggregates it");
        @(negedge clk) rx_pin = 1'b1;
        repeat (2 * FRAME_CLKS) @(posedge clk);
        settle();
        check(brk_active === 1'b0, "line released: break condition clears");
        check(e_break === 1'b1,    "but the STICKY flag survives the release");
        @(negedge clk) cfg_err_clear = 1'b1;
        @(negedge clk) cfg_err_clear = 1'b0;
        settle();
        check(e_break === 1'b0,    "software clear drops the sticky flag");
        check(any_err === 1'b0,    "and any_error with it");

        // A break is a long run of SPACE, which the receiver decodes as a
        // stream of malformed characters. They are legitimately in the RX
        // FIFO now, and the end-to-end comparisons below would read them as
        // the bytes they just wrote -- so the queue is flushed and the
        // checker's own tally is reset with it.
        @(negedge clk) cfg_rx_flush = 1'b1;
        @(negedge clk) cfg_rx_flush = 1'b0;
        settle();
        check(rx_empty === 1'b1, "break residue flushed from the RX FIFO");
        n_got = 0; got_perr = 0; got_ferr = 0;

        //-------------------------------------------------------------------
        //  Hardware flow control follows RX occupancy
        //-------------------------------------------------------------------
        @(negedge clk) cfg_hw_flow = 1'b0;
        settle();
        check(rts_n === 1'b0, "hw flow DISABLED: rts_n held permitted, not idle");
        @(negedge clk) cfg_hw_flow = 1'b1; cfg_loopback = 1'b1;
        settle();
        check(rts_n === 1'b0, "hw flow enabled, RX empty: still permitting");

        //-------------------------------------------------------------------
        //  Configuration is deferred until the link is quiet
        //-------------------------------------------------------------------
        // Queue a burst, change parity mid-burst, and require that every
        // byte still arrives intact -- the change must not reformat a frame
        // that is already in flight.
        @(negedge clk) cfg_hw_flow = 1'b0;
        settle();
        n_sent = 0; base_got = n_got;
        rd_ready = 1'b1;
        for (i = 0; i < 4; i = i + 1) write_byte(8'h90 + i[7:0]);
        repeat (FRAME_CLKS) @(posedge clk);
        @(negedge clk) cfg_parity = PARITY_EVEN;      // mid-burst change
        repeat (13 * FRAME_CLKS) @(posedge clk);
        @(negedge clk) rd_ready = 1'b0;
        settle();
        check(n_got == base_got + 4, "config change mid-burst: all 4 bytes arrived");
        mism = 0;
        for (i = 0; i < 4; i = i + 1)
            if (got[base_got + i] !== sent[i]) mism = mism + 1;
        check(mism == 0,            "config change mid-burst: values intact");
        check(got_ferr == 0,        "config change mid-burst: no framing errors");

        //-------------------------------------------------------------------
        //  The new parity mode is in force afterwards, and still loops back
        //-------------------------------------------------------------------
        n_sent = 0; base_got = n_got;
        for (i = 0; i < 4; i = i + 1) write_byte(8'hA0 + i[7:0]);
        drain(10);
        settle();
        check(n_got == base_got + 4, "even parity: 4 more bytes round-tripped");
        mism = 0;
        for (i = 0; i < 4; i = i + 1)
            if (got[base_got + i] !== sent[i]) mism = mism + 1;
        check(mism == 0,             "even parity: values intact");
        check(got_perr == 0,         "even parity: no parity errors on a clean loop");

        //-------------------------------------------------------------------
        //  RX FIFO saturation: backpressure, overrun, and the survivors
        //-------------------------------------------------------------------
        // Every earlier test drained the RX FIFO promptly, so the receive
        // side was never pushed to its boundary. This is where the
        // handshake, the backpressure and the second overrun source live.
        @(negedge clk) cfg_rx_flush = 1'b1;
        @(negedge clk) cfg_rx_flush = 1'b0;
        @(negedge clk) cfg_err_clear = 1'b1;
        @(negedge clk) cfg_err_clear = 1'b0;
        settle();
        n_got = 0; got_perr = 0; got_ferr = 0; n_sent = 0;

        // Send more than the RX FIFO can hold, and never read.
        rd_ready = 1'b0;
        for (i = 0; i < RX_DEPTH + 6; i = i + 1) write_byte(8'h20 + i[7:0]);
        repeat ((RX_DEPTH + 10) * FRAME_CLKS) @(posedge clk);
        settle();

        check(rx_empty === 1'b0,   "RX saturation: queue holds data");
        check(e_overrun === 1'b1,  "RX saturation: overrun sticky raised");
        check(any_err === 1'b1,    "RX saturation: any_error aggregates it");
        check(rx_trig === 1'b1,    "RX saturation: the watermark is asking for service");

        // Drain and check WHICH bytes survived. The queue keeps the OLDEST
        // entries -- 10.2's policy -- so the first RX_DEPTH bytes sent are
        // the ones that come back, in order.
        base_got = n_got;
        drain(RX_DEPTH + 6);
        settle();
        // The system's receive capacity is RX_DEPTH + 1, not RX_DEPTH: the
        // receiver of Module 6 has its OWN one-entry holding register, and
        // it is still holding a character when the FIFO refuses the next
        // push. That extra entry is invisible in any block-level test and
        // only appears once the two are wired together -- which is exactly
        // what an integration testbench is for.
        $display("  INFO RX saturation: sent %0d, survived %0d (RX_DEPTH=%0d + 1 holding)",
                 n_sent, n_got - base_got, RX_DEPTH);
        check(n_got == base_got + RX_DEPTH + 1,
              "RX saturation: DEPTH+1 survived -- FIFO plus the RX holding register");
        mism = 0;
        for (i = 0; i < RX_DEPTH + 1; i = i + 1)
            if (got[base_got + i] !== sent[i]) mism = mism + 1;
        check(mism == 0,            "RX saturation: the OLDEST bytes survived, in order");
        check(rx_empty === 1'b1,    "RX saturation: queue drains completely afterwards");

        @(negedge clk) cfg_err_clear = 1'b1;
        @(negedge clk) cfg_err_clear = 1'b0;
        settle();

        //-------------------------------------------------------------------
        //  Receiving while the transmitter is idle
        //-------------------------------------------------------------------
        // link_quiet must include rx_active, not just tx_busy. With internal
        // loopback the two always coincide, so this drives the pin directly.
        @(negedge clk) cfg_loopback = 1'b0; cfg_parity = PARITY_NONE;
        settle();
        repeat (4 * FRAME_CLKS) @(posedge clk);       // let it commit while quiet
        settle();
        check(busy === 1'b0,      "idle: not busy before an external frame");

        // Sample the committed configuration WHILE the frame is still in
        // flight. The earlier form of this check was `parity_q == NONE ||
        // !pending`, which a design that committed immediately satisfies
        // through the second term -- it passed a mutant it was written to
        // catch. The strict form samples one value at one instant.
        cfg_mid_frame = parity_mode_t'(2'b11);
        fork
            drive_frame(8'h6C);
            begin
                repeat (BIT_CLKS * 3) @(posedge clk);
                check(busy === 1'b1, "receiving with TX idle: busy is asserted");
                @(negedge clk) cfg_parity = PARITY_ODD;
                repeat (BIT_CLKS * 3) @(posedge clk);
                cfg_mid_frame = dut.cfg_parity_q;      // sampled MID-FRAME
                rx_active_mid = dut.rx_active;
            end
        join
        settle();
        check(rx_active_mid === 1'b1,
              "the sample really was taken while a frame was in flight");
        check(cfg_mid_frame === PARITY_NONE,
              "config change mid-receive was NOT applied while receiving");
        repeat (3 * FRAME_CLKS) @(posedge clk);
        settle();
        check(dut.cfg_parity_q === PARITY_ODD, "and commits once the line is quiet");

        base_got = n_got;
        drain(3);
        settle();
        check(n_got == base_got + 1,     "externally driven frame was received");
        check(got[base_got] === 8'h6C,   "externally driven frame arrived intact");

        @(negedge clk) cfg_parity = PARITY_NONE; cfg_loopback = 1'b1;
        @(negedge clk) cfg_err_clear = 1'b1;
        @(negedge clk) cfg_err_clear = 1'b0;
        repeat (4 * FRAME_CLKS) @(posedge clk);
        settle();

        //-------------------------------------------------------------------
        //  Reset while loaded
        //-------------------------------------------------------------------
        for (i = 0; i < 4; i = i + 1) poke_byte(8'hF0 + i[7:0]);
        settle();
        check(tx_empty === 1'b0, "data queued before reset");
        @(negedge clk) rst_n = 1'b0;
        repeat (4) @(negedge clk);
        check(tx_empty === 1'b1 && rx_empty === 1'b1, "reset empties both FIFOs");
        check(any_err === 1'b0,                       "reset clears sticky status");
        check(busy === 1'b0,                          "reset clears busy");
        @(negedge clk) rst_n = 1'b1;
        settle();

        $display("== %0d checks, %0d failures ==", checks, failures);
        if (failures == 0) $display("   RESULT: ALL SYSTEMVERILOG UART-IP TESTS PASSED");
        else               $display("   RESULT: SYSTEMVERILOG UART-IP TESTS FAILED");
        $finish;
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--==========================================================================
--  tb_uart_ip -- self-checking VHDL-2008 integration testbench
--
--  Same 46 checks as the Verilog and SystemVerilog integration testbenches.
--  It tests what only exists once the twelve blocks meet: end-to-end
--  integrity, the handshakes and their backpressure, busy_o's meaning, the
--  two loopbacks, flush, status aggregation, and deferred configuration.
--
--  INDEPENDENCE: the checker keeps its own queue of what it WROTE and
--  compares against what it READ. It never inspects a sub-block internal.
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.env.finish;
use work.uart_fifo_pkg.all;

entity tb_uart_ip is
end entity tb_uart_ip;

architecture sim of tb_uart_ip is

    constant CLK_HZ     : positive := 32_000_000;
    constant BAUD_HZ    : positive := 1_000_000;
    constant DATA_W     : positive := 8;
    constant OVERSAMPLE : positive := 16;
    constant TX_DEPTH   : positive := 16;
    constant RX_DEPTH   : positive := 16;
    constant CW         : positive := cnt_width(RX_DEPTH+1);
    constant BIT_CLKS   : positive := CLK_HZ / BAUD_HZ;      -- 32
    constant FRAME_CLKS : positive := BIT_CLKS * 10;         -- 8N1
    constant CLK_PERIOD : time     := 10 ns;

    constant PARITY_NONE : std_logic_vector(1 downto 0) := "00";
    constant PARITY_EVEN : std_logic_vector(1 downto 0) := "01";

    signal clk, rst_n, done : std_logic := '0';

    signal cfg_parity   : std_logic_vector(1 downto 0) := PARITY_NONE;
    signal cfg_sw_flow, cfg_hw_flow   : std_logic := '0';
    signal cfg_loopback, cfg_line_loop: std_logic := '0';
    signal cfg_tx_flush, cfg_rx_flush, cfg_err_clear : std_logic := '0';
    signal cfg_rx_trig : std_logic_vector(CW-1 downto 0) := std_logic_vector(to_unsigned(12, CW));
    signal cfg_tx_trig : std_logic_vector(CW-1 downto 0) := std_logic_vector(to_unsigned(4, CW));

    signal wr_data  : std_logic_vector(DATA_W-1 downto 0) := (others => '0');
    signal wr_valid : std_logic := '0';
    signal wr_ready : std_logic;

    signal rd_data  : std_logic_vector(DATA_W-1 downto 0);
    signal rd_perr, rd_ferr, rd_valid : std_logic;
    signal rd_ready : std_logic := '0';

    signal rx_pin : std_logic := '1';
    signal cts_n  : std_logic := '0';
    signal tx_pin, rts_n : std_logic;

    signal tx_trig, rx_trig, tx_empty, rx_empty, busy : std_logic;
    signal e_frame, e_parity, e_overrun, e_break      : std_logic;
    signal brk_active, any_err, rx_push_evt           : std_logic;

    type byte_arr is array (0 to 255) of std_logic_vector(DATA_W-1 downto 0);
    signal sent    : byte_arr := (others => (others => '0'));
    signal got     : byte_arr := (others => (others => '0'));
    signal n_sent  : natural := 0;
    signal n_got   : natural := 0;
    signal got_perr, got_ferr : natural := 0;
    signal clr_tally : std_logic := '0';

    -- Mid-frame observation. busy_o with TX idle IS rx_active, so the
    -- testbench can tell whether a frame is in flight without reaching into
    -- the DUT. The committed parity is observed through a VHDL-2008
    -- external name, which is the language's sanctioned way to look at a
    -- signal a design does not publish -- and it is used ONLY for this
    -- diagnostic, never to decide a data check.
    signal req_parity       : std_logic := '0';
    signal sample_done      : std_logic := '0';
    signal busy_mid         : std_logic := '0';
    signal parity_mid       : std_logic := '0';
    signal parity_committed : std_logic := '0';

begin

    clk <= not clk after CLK_PERIOD/2 when done = '0' else '0';

    dut : entity work.uart_ip
        generic map (CLK_HZ => CLK_HZ, BAUD_HZ => BAUD_HZ, DATA_W => DATA_W,
                     OVERSAMPLE => OVERSAMPLE, TX_DEPTH => TX_DEPTH,
                     RX_DEPTH => RX_DEPTH, FC_STOP => 12, FC_RESUME => 6)
        port map (clk => clk, rst_n => rst_n,
                  rx_i => rx_pin, tx_o => tx_pin, cts_n_i => cts_n, rts_n_o => rts_n,
                  cfg_parity_i => cfg_parity, cfg_sw_flow_en_i => cfg_sw_flow,
                  cfg_hw_flow_en_i => cfg_hw_flow, cfg_loopback_i => cfg_loopback,
                  cfg_line_loop_i => cfg_line_loop,
                  cfg_tx_flush_i => cfg_tx_flush, cfg_rx_flush_i => cfg_rx_flush,
                  cfg_err_clear_i => cfg_err_clear,
                  cfg_rx_trigger_i => cfg_rx_trig, cfg_tx_trigger_i => cfg_tx_trig,
                  wr_data_i => wr_data, wr_valid_i => wr_valid, wr_ready_o => wr_ready,
                  rd_data_o => rd_data, rd_parity_err_o => rd_perr,
                  rd_frame_err_o => rd_ferr, rd_valid_o => rd_valid,
                  rd_ready_i => rd_ready,
                  tx_trigger_o => tx_trig, rx_trigger_o => rx_trig,
                  tx_empty_o => tx_empty, rx_empty_o => rx_empty, busy_o => busy,
                  err_frame_o => e_frame, err_parity_o => e_parity,
                  err_overrun_o => e_overrun, err_break_o => e_break,
                  break_active_o => brk_active, any_error_o => any_err,
                  rx_push_evt_o => rx_push_evt);

    -- The ONLY way the testbench learns what the IP delivered.
    collect : process (clk)
        variable g, pe, fe : natural := 0;
        variable arr : byte_arr := (others => (others => '0'));
    begin
        if rising_edge(clk) and rst_n = '1' then
            if clr_tally = '1' then
                g := 0; pe := 0; fe := 0;
            elsif rd_valid = '1' and rd_ready = '1' then
                arr(g) := rd_data;
                if rd_perr = '1' then pe := pe + 1; end if;
                if rd_ferr = '1' then fe := fe + 1; end if;
                g := g + 1;
            end if;
            got <= arr; n_got <= g; got_perr <= pe; got_ferr <= fe;
        end if;
    end process collect;

    -- parity_committed is a continuous observation through a VHDL-2008
    -- external name -- the language's sanctioned way to look at a signal a
    -- design does not publish. Used ONLY for this diagnostic, never to
    -- decide a data check.
    parity_committed <=
        '1' when << signal .tb_uart_ip.dut.cfg_parity_q : std_logic_vector(1 downto 0) >> = "10"
        else '0';

    watchdog : process
    begin
        wait for 20 ms;
        report "   RESULT: VHDL UART-IP TESTS FAILED (timeout)" severity failure;
        wait;
    end process watchdog;

    stim : process
        variable checks, failures, i, mism, base_got : natural := 0;
        variable snt : byte_arr := (others => (others => '0'));
        variable ns  : natural := 0;

        procedure check(cond : boolean; name : string) is
        begin
            checks := checks + 1;
            if cond then report "  PASS " & name;
            else failures := failures + 1; report "  FAIL " & name severity error; end if;
        end procedure;

        procedure settle is
        begin
            for k in 0 to 3 loop wait until falling_edge(clk); end loop;
        end procedure;

        procedure write_byte(d : std_logic_vector) is
        begin
            wait until falling_edge(clk);
            while wr_ready = '0' loop wait until falling_edge(clk); end loop;
            wr_data <= d; wr_valid <= '1';
            wait until falling_edge(clk);
            wr_valid <= '0';
            snt(ns) := d; ns := ns + 1;
        end procedure;

        procedure poke_byte(d : std_logic_vector) is
        begin
            wait until falling_edge(clk); wr_data <= d; wr_valid <= '1';
            wait until falling_edge(clk); wr_valid <= '0';
        end procedure;

        procedure drain(frames : natural) is
        begin
            rd_ready <= '1';
            for k in 1 to frames * FRAME_CLKS loop wait until rising_edge(clk); end loop;
            wait until falling_edge(clk); rd_ready <= '0';
        end procedure;

        procedure waitf(frames : natural) is
        begin
            for k in 1 to frames * FRAME_CLKS loop wait until rising_edge(clk); end loop;
        end procedure;

        -- Drive one 8N1 frame onto rx_i at the configured baud. This lets the
        -- testbench RECEIVE while the transmitter is idle, which is the only
        -- way to exercise rx_active independently of tx_busy.
        procedure drive_frame(d : std_logic_vector(7 downto 0)) is
        begin
            wait until falling_edge(clk); rx_pin <= '0';           -- start
            for k in 1 to BIT_CLKS loop wait until rising_edge(clk); end loop;
            for b in 0 to 7 loop
                wait until falling_edge(clk); rx_pin <= d(b);      -- LSB first
                for k in 1 to BIT_CLKS loop wait until rising_edge(clk); end loop;
            end loop;
            wait until falling_edge(clk); rx_pin <= '1';           -- stop
            for k in 1 to BIT_CLKS loop wait until rising_edge(clk); end loop;
        end procedure;

        -- Drive a frame AND request a parity change part-way through it,
        -- sampling what the IP has committed while the frame is still in
        -- flight. Both happen from this one process, so cfg_parity keeps
        -- exactly ONE driver -- a second driver on a resolved signal would
        -- quietly resolve rather than error.
        procedure drive_frame_and_change(d : std_logic_vector(7 downto 0)) is
        begin
            wait until falling_edge(clk); rx_pin <= '0';           -- start
            for k in 1 to BIT_CLKS loop wait until rising_edge(clk); end loop;
            for b in 0 to 7 loop
                wait until falling_edge(clk); rx_pin <= d(b);
                for k in 1 to BIT_CLKS loop wait until rising_edge(clk); end loop;
                if b = 2 then
                    cfg_parity <= "10";                            -- PARITY_ODD
                end if;
                if b = 5 then
                    busy_mid   <= busy;                            -- still receiving?
                    parity_mid <= parity_committed;                -- applied yet?
                end if;
            end loop;
            wait until falling_edge(clk); rx_pin <= '1';           -- stop
            for k in 1 to BIT_CLKS loop wait until rising_edge(clk); end loop;
        end procedure;
    begin
        report "== uart_ip (VHDL) : self-checking integration testbench ==";

        rst_n <= '0';
        for k in 0 to 7 loop wait until falling_edge(clk); end loop;
        check(tx_empty = '1' and rx_empty = '1', "reset: both FIFOs empty");
        check(busy = '0',     "reset: not busy");
        check(any_err = '0',  "reset: no sticky errors");
        check(wr_ready = '1', "reset: ready to accept a write");
        check(rd_valid = '0', "reset: nothing to read");
        check(tx_pin = '1',   "reset: TX line idles MARK");
        rst_n <= '1'; settle;

        -- end-to-end through INTERNAL loopback ---------------------------
        wait until falling_edge(clk); cfg_loopback <= '1';
        settle;
        base_got := n_got; ns := 0;
        for i in 0 to 7 loop
            write_byte(std_logic_vector(to_unsigned(16#30# + i, DATA_W)));
        end loop;
        drain(14); settle;

        check(n_got = base_got + 8, "loopback: all 8 bytes came back");
        mism := 0;
        for i in 0 to 7 loop
            if got(base_got + i) /= snt(i) then mism := mism + 1; end if;
        end loop;
        check(mism = 0,        "loopback: values and ORDER preserved");
        check(got_perr = 0,    "loopback: no parity errors reported");
        check(got_ferr = 0,    "loopback: no framing errors reported");
        check(any_err = '0',   "loopback: no sticky error raised");
        check(busy = '0',      "after the burst: busy clears");
        check(tx_empty = '1',  "after the burst: TX FIFO drained");

        -- busy_o must NOT include unread RX occupancy ---------------------
        ns := 0;
        write_byte(x"5A");
        waitf(3); settle;
        check(rx_empty = '0', "one byte waiting in the RX FIFO");
        check(busy = '0',     "busy is LOW with unread data -- work, not storage");
        drain(2); settle;

        -- write-side backpressure -----------------------------------------
        -- TX_DEPTH pushes leave the FIFO one short of full, because the
        -- transmitter pops the first byte the moment it is offered.
        for i in 0 to TX_DEPTH + 1 loop
            poke_byte(std_logic_vector(to_unsigned((16#C0# + i) mod 256, DATA_W)));
        end loop;
        settle;
        check(wr_ready = '0', "TX FIFO full: wr_ready deasserts");
        check(busy = '1',     "TX FIFO full: busy asserted (work queued)");
        drain(24); settle;
        check(wr_ready = '1', "after draining: wr_ready returns");

        -- TX flush ---------------------------------------------------------
        wait until falling_edge(clk); cfg_loopback <= '0';
        settle;
        for i in 0 to 5 loop
            poke_byte(std_logic_vector(to_unsigned(16#E0# + i, DATA_W)));
        end loop;
        settle;
        check(tx_empty = '0', "bytes queued before the flush");
        wait until falling_edge(clk); cfg_tx_flush <= '1';
        wait until falling_edge(clk); cfg_tx_flush <= '0';
        settle;
        check(tx_empty = '1', "tx flush empties the TX FIFO");
        check(rx_empty = '1', "tx flush left the RX side untouched");

        -- RX flush ---------------------------------------------------------
        wait until falling_edge(clk); cfg_loopback <= '1';
        settle;
        ns := 0;
        write_byte(x"77"); write_byte(x"88");
        waitf(4); settle;
        check(rx_empty = '0', "bytes received before the flush");
        wait until falling_edge(clk); cfg_rx_flush <= '1';
        wait until falling_edge(clk); cfg_rx_flush <= '0';
        settle;
        check(rx_empty = '1', "rx flush empties the RX FIFO");

        -- LINE loopback ------------------------------------------------------
        wait until falling_edge(clk); cfg_loopback <= '0'; cfg_line_loop <= '1';
        settle;
        wait until falling_edge(clk); rx_pin <= '0';
        settle;
        check(tx_pin = '0', "line loopback: rx pin drives the tx pin");
        wait until falling_edge(clk); rx_pin <= '1';
        settle;
        check(tx_pin = '1', "line loopback: follows the rx pin back to mark");
        wait until falling_edge(clk); cfg_line_loop <= '0';
        settle;
        check(tx_pin = '1', "line loopback off: tx returns to the engine");

        -- status aggregation and the software clear --------------------------
        wait until falling_edge(clk); rx_pin <= '0';
        waitf(16); settle;
        check(brk_active = '1', "sustained low: break declared");
        check(e_break = '1',    "break raises its sticky flag");
        check(any_err = '1',    "any_error aggregates it");
        wait until falling_edge(clk); rx_pin <= '1';
        waitf(2); settle;
        check(brk_active = '0', "line released: break condition clears");
        check(e_break = '1',    "but the STICKY flag survives the release");
        wait until falling_edge(clk); cfg_err_clear <= '1';
        wait until falling_edge(clk); cfg_err_clear <= '0';
        settle;
        check(e_break = '0',    "software clear drops the sticky flag");
        check(any_err = '0',    "and any_error with it");

        -- a break decodes as malformed characters; discard them -------------
        wait until falling_edge(clk); cfg_rx_flush <= '1';
        wait until falling_edge(clk); cfg_rx_flush <= '0';
        settle;
        check(rx_empty = '1', "break residue flushed from the RX FIFO");
        wait until falling_edge(clk); clr_tally <= '1';
        wait until falling_edge(clk); clr_tally <= '0';
        settle;

        -- hardware flow control ----------------------------------------------
        wait until falling_edge(clk); cfg_hw_flow <= '0';
        settle;
        check(rts_n = '0', "hw flow DISABLED: rts_n held permitted, not idle");
        wait until falling_edge(clk); cfg_hw_flow <= '1'; cfg_loopback <= '1';
        settle;
        check(rts_n = '0', "hw flow enabled, RX empty: still permitting");

        -- configuration deferred until the link is quiet ----------------------
        wait until falling_edge(clk); cfg_hw_flow <= '0';
        settle;
        ns := 0; base_got := n_got;
        rd_ready <= '1';
        for i in 0 to 3 loop
            write_byte(std_logic_vector(to_unsigned(16#90# + i, DATA_W)));
        end loop;
        waitf(1);
        wait until falling_edge(clk); cfg_parity <= PARITY_EVEN;
        waitf(13);
        wait until falling_edge(clk); rd_ready <= '0';
        settle;
        check(n_got = base_got + 4, "config change mid-burst: all 4 bytes arrived");
        mism := 0;
        for i in 0 to 3 loop
            if got(base_got + i) /= snt(i) then mism := mism + 1; end if;
        end loop;
        check(mism = 0,     "config change mid-burst: values intact");
        check(got_ferr = 0, "config change mid-burst: no framing errors");

        -- the new mode is in force afterwards ---------------------------------
        ns := 0; base_got := n_got;
        for i in 0 to 3 loop
            write_byte(std_logic_vector(to_unsigned(16#A0# + i, DATA_W)));
        end loop;
        drain(10); settle;
        check(n_got = base_got + 4, "even parity: 4 more bytes round-tripped");
        mism := 0;
        for i in 0 to 3 loop
            if got(base_got + i) /= snt(i) then mism := mism + 1; end if;
        end loop;
        check(mism = 0,     "even parity: values intact");
        check(got_perr = 0, "even parity: no parity errors on a clean loop");

        ----------------------------------------------------------------
        --  RX FIFO saturation: backpressure, overrun, and the survivors
        ----------------------------------------------------------------
        wait until falling_edge(clk); cfg_rx_flush <= '1';
        wait until falling_edge(clk); cfg_rx_flush <= '0';
        wait until falling_edge(clk); cfg_err_clear <= '1';
        wait until falling_edge(clk); cfg_err_clear <= '0';
        settle;
        wait until falling_edge(clk); clr_tally <= '1';
        wait until falling_edge(clk); clr_tally <= '0';
        settle;
        ns := 0;

        rd_ready <= '0';
        for i in 0 to RX_DEPTH + 5 loop
            write_byte(std_logic_vector(to_unsigned((16#20# + i) mod 256, DATA_W)));
        end loop;
        waitf(RX_DEPTH + 10);
        settle;

        check(rx_empty = '0',  "RX saturation: queue holds data");
        check(e_overrun = '1', "RX saturation: overrun sticky raised");
        check(any_err = '1',   "RX saturation: any_error aggregates it");
        check(rx_trig = '1',   "RX saturation: the watermark is asking for service");

        -- The system's receive capacity is RX_DEPTH + 1: the receiver of
        -- Module 6 has its OWN one-entry holding register and is still
        -- holding a character when the FIFO refuses the next push. That
        -- extra entry is invisible in any block-level test.
        base_got := n_got;
        drain(RX_DEPTH + 6);
        settle;
        check(n_got = base_got + RX_DEPTH + 1,
              "RX saturation: DEPTH+1 survived -- FIFO plus the RX holding register");
        mism := 0;
        for i in 0 to RX_DEPTH loop
            if got(base_got + i) /= snt(i) then mism := mism + 1; end if;
        end loop;
        check(mism = 0,      "RX saturation: the OLDEST bytes survived, in order");
        check(rx_empty = '1',"RX saturation: queue drains completely afterwards");

        wait until falling_edge(clk); cfg_err_clear <= '1';
        wait until falling_edge(clk); cfg_err_clear <= '0';
        settle;

        ----------------------------------------------------------------
        --  Receiving while the transmitter is idle
        ----------------------------------------------------------------
        wait until falling_edge(clk); cfg_loopback <= '0'; cfg_parity <= PARITY_NONE;
        settle;
        waitf(4);
        settle;
        check(busy = '0', "idle: not busy before an external frame");

        -- With TX idle and its FIFO empty, busy_o IS rx_active -- which is
        -- how this testbench observes the receiver without reaching inside.
        drive_frame_and_change(x"6C");
        settle;
        check(busy_mid = '1', "receiving with TX idle: busy is asserted");
        check(busy_mid = '1',
              "the sample really was taken while a frame was in flight");
        check(parity_mid = '0',
              "config change mid-receive was NOT applied while receiving");
        waitf(3);
        settle;
        check(parity_committed = '1', "and commits once the line is quiet");

        base_got := n_got;
        drain(3);
        settle;
        check(n_got = base_got + 1, "externally driven frame was received");
        check(got(base_got) = x"6C","externally driven frame arrived intact");

        wait until falling_edge(clk); cfg_parity <= PARITY_NONE; cfg_loopback <= '1';
        wait until falling_edge(clk); cfg_err_clear <= '1';
        wait until falling_edge(clk); cfg_err_clear <= '0';
        waitf(4);
        settle;

        -- reset while loaded ---------------------------------------------------
        for i in 0 to 3 loop
            poke_byte(std_logic_vector(to_unsigned(16#F0# + i, DATA_W)));
        end loop;
        settle;
        check(tx_empty = '0', "data queued before reset");
        wait until falling_edge(clk); rst_n <= '0';
        for k in 0 to 3 loop wait until falling_edge(clk); end loop;
        check(tx_empty = '1' and rx_empty = '1', "reset empties both FIFOs");
        check(any_err = '0',                     "reset clears sticky status");
        check(busy = '0',                        "reset clears busy");
        wait until falling_edge(clk); rst_n <= '1';
        settle;

        report "== " & integer'image(checks) & " checks, " &
               integer'image(failures) & " failures ==";
        if failures = 0 then report "   RESULT: ALL VHDL UART-IP TESTS PASSED";
        else report "   RESULT: VHDL UART-IP TESTS FAILED" severity error; end if;
        done <= '1';
        finish;
    end process stim;

end architecture sim;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ iverilog -g2001 -o sim <11 sub-blocks> uart_ip_v.v tb_uart_ip_v.v && ./sim
== uart_ip_v : self-checking Verilog integration testbench ==
  PASS reset: both FIFOs empty
  PASS reset: TX line idles MARK
  PASS loopback: all 8 bytes came back
  PASS loopback: values and ORDER preserved
  PASS busy is LOW with unread data -- work, not storage
  PASS TX FIFO full: wr_ready deasserts
  PASS tx flush empties the TX FIFO
  PASS tx flush left the RX side untouched
  PASS line loopback: rx pin drives the tx pin
  PASS sustained low: break declared
  PASS but the STICKY flag survives the release
  PASS software clear drops the sticky flag
  INFO RX saturation: sent 22, survived 17 (RX_DEPTH=16 + 1 holding)
  PASS RX saturation: DEPTH+1 survived -- FIFO plus the RX holding register
  PASS RX saturation: the OLDEST bytes survived, in order
  PASS receiving with TX idle: busy is asserted
  PASS config change mid-receive was NOT applied while receiving
  PASS and commits once the line is quiet
  PASS reset empties both FIFOs
== 60 checks, 0 failures ==
   RESULT: ALL VERILOG UART-IP TESTS PASSED

60 checks in each language, 0 failures — the same sixty.

Proving these checks can fail

Eight integration defects, each run against the unmodified testbench:

MutationWhat it breaksFailures
the two loopback controls swapped§1 of 11.59 / 60
RX push gated on the offer, not the handshake§3's handshake rule4 / 60
receiver never backpressured§3's flow rule2 / 60
busy_o includes unread RX occupancy§5's definition1 / 60
disabled HW flow drives the pin's idle level§4 of 11.31 / 60
link_quiet ignores rx_active§3 of 11.31 / 60
status aggregates only one overrun source§50 — equivalent, see below
break detector fed the raw lineChapter 12.20 — see below

10. 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.

11. Debugging

12. 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.

13. Understanding Check

14. 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.

15. 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.