Skip to content
VLSI Mentor

UART · Module 6

Complete RX RTL Architecture

One synthesizable receiver assembled from the module's five preceding chapters — assumptions stated first, walked block by block with the invariant each maintains, then reviewed the way a reviewer would, including the defects found during its own development.

Five chapters have specified a receiver completely. Chapter 6.1 partitioned it and settled the timing origin; Chapter 6.2 enumerated eight transitions; Chapter 6.3 derived the counters and stated their semantics; Chapter 6.4 added validation and the publish discipline; Chapter 6.5 chose the output interface and the overrun policy.

This chapter assembles them into one module and then does the thing that separates working RTL from reviewable RTL: it challenges its own design.

That is not a rhetorical device. Two genuine defects were found while developing this receiver — one by reasoning about a path no test covered, one by a numeric check contradicting a published claim — and both are described where they occurred rather than quietly corrected. A design review chapter that presents only successes teaches the wrong thing.

1. What Was Actually Verified

Stated first, because the rest of the chapter depends on knowing it — and because "the RTL is correct" means nothing without saying what checked it.

LanguageToolCompileSimulateChecksFailures
SystemVerilogIcarus Verilog 13.0 (-g2012)cleanyes450
Verilog-2001Icarus Verilog 13.0 (-g2001)cleanyes440
VHDL-2008NVC 1.23.0 (LLVM 23.1.1)cleanyes440

133 checks across three languages, zero failures, every one of them run against the design code printed on this page.

What you can re-run from this page: §4 and §5 print the Verilog-2001 and VHDL-2008 receivers together with their self-checking testbenches, so those two columns reproduce directly — 44 checks each, which is what they produce here. §3 prints the SystemVerilog receiver in full and it compiles clean under the same tool, but its testbench is the original regression for this receiver and is not reproduced on the page; the 45 is that run, and the extra check over Verilog is the bound assertion module described below.

The SystemVerilog count includes one check that the bound assertion module recorded no violations across the whole run. The Verilog testbench runs the same scenarios without that assertion binding, which is the only reason the two totals differ by one.

Every figure, table and trace in Module 6 was extracted from these simulations rather than computed by hand.

What that does and does not prove. It proves the design elaborates as SystemVerilog-2012, that its behaviour matches an independent reference model across the cases in §8's matrix, and that every published trace is the design's real behaviour.

It does not prove synthesisability. Icarus is a simulator; it does not run inference rules, it does not report latches, and it does not know whether a construct maps to hardware. The RTL is written to the constraints Chapter 6.2 and Chapter 6.3 set out — single clock, enables not generated clocks, every branch assigning, no combinational loops — and that is a reviewed claim, not a tool-verified one. A synthesis run is the missing evidence, and Module 12 is where it belongs.

2. Assumptions

Before the code, what this receiver is — and is not.

Clockone fabric clock, clk. No generated clock anywhere.
Resetrst_n, asynchronous assert, active low. Synchronous-release discipline is Module 12's.
RX synchronisationtwo stages, reset to the idle level. rx_i fans out to nothing else.
Timing sourceos_tick_i, a one-cycle enable, OVERSAMPLE per bit interval. Module 8 generates it.
Oversampling factorOVERSAMPLE parameter, default 16, any value ≥ 2.
Data widthDATA_W parameter, default 8. Elaboration-time.
Bit orderLSB first, per Chapter 3.2.
Parityruntime input: none / even / odd / mark.
Stop bitsone, validated at its centre sample.
Samplingsingle sample at the interval centre.
Output interfaceheld valid with rx_ready_i accept.
Error policycorrupt frames are delivered with their flags.
Overrun policypreserve the held byte, drop the new frame, raise rx_overrun_o.
Bufferingone holding register. No FIFO — Module 10.
Break handlingdeferred — Module 9.
Idle detectiondeferred — Module 9. See §7's mis-framing case.
Sticky statusdeferred — Module 9. Flags here are strictly per-frame.
Two stop bitsnot supported. Only the first stop interval is validated.
Majority votingnot implemented. Chapter 5.4 showed it is a trade, not an upgrade.

The last four matter. Unsupported behaviour must look deliberate, and a receiver whose assumptions block is silent about two stop bits is one where a reviewer cannot tell whether the omission was a decision or an oversight.

3. The Assembled Receiver

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ============================================================================
//  Synthesizable SystemVerilog — complete UART receiver.
//
//  Compiled and simulated with Icarus Verilog 13.0 (-g2012); 45 checks, 0
//  failures. One of three implementations of the same contract -- see the
//  Verilog and VHDL versions in sections 4 and 5, and the equivalence table
//  in section 11. Every trace published in Module 6 came from these runs.
//
//  Assumptions are stated in §2 and are not repeated here. Chapter references
//  point at the derivation of each decision rather than restating it.
// ============================================================================
package uart_parity_pkg;
    typedef enum logic [1:0] {
        PARITY_NONE  = 2'b00,
        PARITY_EVEN  = 2'b01,
        PARITY_ODD   = 2'b10,
        PARITY_MARK  = 2'b11        // constant 1 — no detection, Chapter 3.3
    } parity_mode_t;
endpackage

module uart_rx
    import uart_parity_pkg::*;
#(
    parameter int unsigned DATA_W     = 8,
    parameter int unsigned OVERSAMPLE = 16
) (
    input  logic              clk,
    input  logic              rst_n,
    input  logic              rx_i,            // the only asynchronous input
    input  logic              os_tick_i,       // enable, M per bit — Module 8
    input  parity_mode_t      parity_mode_i,

    output logic [DATA_W-1:0] rx_data_o,
    output logic              rx_valid_o,
    input  logic              rx_ready_i,
    output logic              rx_parity_err_o,
    output logic              rx_frame_err_o,
    output logic              rx_overrun_o,
    // Published so an integrator never has to reach inside for the FSM state.
    // Chapter 11.4 — a hierarchical reference is a missing port, not a shortcut.
    output logic              rx_active_o,
    // The synchronised line, published so an integration has ONE synchroniser
    // on the asynchronous pin and every consumer sees the SAME resolved value.
    // Chapter 12.2 — two synchronisers on one async signal can disagree.
    output logic              rx_sync_o
);
    // ---- derived constants — Chapter 6.3 §2, §5 ----------------------------
    localparam int unsigned CENTER = OVERSAMPLE / 2;
    localparam int unsigned PH_W   = (OVERSAMPLE <= 1) ? 1 : $clog2(OVERSAMPLE);
    localparam int unsigned BI_W   = (DATA_W     <= 1) ? 1 : $clog2(DATA_W);

    // ---- parameter legality — §6 of this chapter ---------------------------
    initial begin
        if (OVERSAMPLE < 2)
            $fatal(1, "uart_rx: OVERSAMPLE = %0d has no centre to sample", OVERSAMPLE);
        if (DATA_W < 1)
            $fatal(1, "uart_rx: DATA_W = %0d is not a frame", DATA_W);
    end

    typedef enum logic [2:0] {
        S_IDLE   = 3'd0,
        S_START  = 3'd1,
        S_DATA   = 3'd2,
        S_PARITY = 3'd3,
        S_STOP   = 3'd4
    } rx_state_e;

    rx_state_e         state_q;
    logic              rx_meta_q, rx_sync_q, rx_sync_d_q;
    logic [PH_W-1:0]   phase_q;
    logic [BI_W-1:0]   bit_idx_q;
    logic [DATA_W-1:0] shreg_q;
    logic              par_acc_q;
    logic              par_err_q;
    logic              start_cand;
    logic              sample_now;

    // The ONLY definition of "this receiver is mid-frame". Encoding-independent,
    // which is what lets the VHDL version leave its state encoding to synthesis.
    assign rx_active_o = (state_q != S_IDLE);
    assign rx_sync_o   = rx_sync_q;

    // ---- input boundary — Chapter 5.1 §3 -----------------------------------
    // Reset to the IDLE level, never to zero: a receiver leaving reset must
    // not present a fabricated departure from idle to the edge detector.
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            rx_meta_q   <= 1'b1;
            rx_sync_q   <= 1'b1;
            rx_sync_d_q <= 1'b1;
        end else begin
            rx_meta_q   <= rx_i;
            rx_sync_q   <= rx_meta_q;
            rx_sync_d_q <= rx_sync_q;
        end
    end

    // ---- the two named events — Chapters 5.2 §1 and 6.3 §3 -----------------
    assign start_cand = rx_sync_d_q && !rx_sync_q;
    assign sample_now = os_tick_i && (phase_q == PH_W'(CENTER - 1));

    // ---- the receiver's expectation — Chapter 6.4 §2 -----------------------
    function automatic logic expected_parity(input parity_mode_t m, input logic acc);
        case (m)
            PARITY_EVEN: expected_parity = acc;
            PARITY_ODD:  expected_parity = ~acc;
            default:     expected_parity = 1'b1;   // MARK
        endcase
    endfunction

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state_q         <= S_IDLE;
            phase_q         <= '0;
            bit_idx_q       <= '0;
            shreg_q         <= '0;
            par_acc_q       <= 1'b0;
            par_err_q       <= 1'b0;
            rx_data_o       <= '0;
            rx_valid_o      <= 1'b0;
            rx_parity_err_o <= 1'b0;
            rx_frame_err_o  <= 1'b0;
            rx_overrun_o    <= 1'b0;
        end else begin
            // Accept releases the holding register and ALL of its status
            // together — Chapter 6.5 §3. Written first so that a delivery in
            // the same cycle overrides it, which is what lets a byte be
            // accepted and replaced on one edge.
            if (rx_valid_o && rx_ready_i) begin
                rx_valid_o      <= 1'b0;
                rx_parity_err_o <= 1'b0;
                rx_frame_err_o  <= 1'b0;
                rx_overrun_o    <= 1'b0;
            end

            if (state_q == S_IDLE) begin
                // The only state that does not wait for the timing event.
                if (start_cand) begin
                    state_q   <= S_START;
                    phase_q   <= '0;      // THE origin — Chapter 6.1 §4
                    bit_idx_q <= '0;
                    par_acc_q <= 1'b0;
                    par_err_q <= 1'b0;    // status is per-frame: clear at entry
                end
            end else if (os_tick_i) begin
                // Explicit wrap. Natural overflow is correct only when
                // OVERSAMPLE is a power of two — Chapter 6.3 §2.
                phase_q <= (phase_q == PH_W'(OVERSAMPLE - 1)) ? '0 : phase_q + 1'b1;

                if (sample_now) begin
                    case (state_q)
                        S_START: begin
                            // The sampled level here is the QUALIFICATION
                            // result, not payload — Chapter 6.2 §1.
                            if (rx_sync_q) state_q <= S_IDLE;     // false start
                            else           state_q <= S_DATA;
                        end
                        S_DATA: begin
                            // Right shift, MSB insert: LSB-first arrival order
                            // reconstructed without a write address — 6.3 §6.
                            shreg_q   <= {rx_sync_q, shreg_q[DATA_W-1:1]};
                            par_acc_q <= par_acc_q ^ rx_sync_q;
                            bit_idx_q <= bit_idx_q + 1'b1;
                            // Compared against DATA_W-1: bit_idx_q still holds
                            // the index of the bit arriving now, and the
                            // increment above is concurrent — Chapter 6.3 §4.
                            if (bit_idx_q == BI_W'(DATA_W - 1)) begin
                                // The ONLY place configuration steers control.
                                if (parity_mode_i == PARITY_NONE) state_q <= S_STOP;
                                else                              state_q <= S_PARITY;
                            end
                        end
                        S_PARITY: begin
                            // Held INTERNALLY. Writing rx_parity_err_o here
                            // would let this frame rewrite the status of a
                            // byte still held from the previous one — the
                            // defect described in Chapter 6.4 §4.
                            par_err_q <= (rx_sync_q != expected_parity(parity_mode_i, par_acc_q));
                            state_q   <= S_STOP;
                        end
                        S_STOP: begin
                            // Unconditional. Waiting for the line to return to
                            // mark wedges the receiver on a break — 6.2 §4.
                            state_q <= S_IDLE;
                            if (rx_valid_o && !rx_ready_i) begin
                                // Preserve the held frame, drop this one, and
                                // record the loss — Chapter 6.5 §4.
                                rx_overrun_o <= 1'b1;
                            end else begin
                                // THE publish point. Data and both status
                                // flags cross to the consumer on one edge.
                                rx_data_o       <= shreg_q;
                                rx_valid_o      <= 1'b1;
                                rx_parity_err_o <= par_err_q;
                                rx_frame_err_o  <= !rx_sync_q;
                            end
                        end
                        default: state_q <= S_IDLE;   // recovery — 6.2 §4
                    endcase
                end
            end
        end
    end
endmodule
The complete UART receiver as assembled. The asynchronous receive input enters a two-stage synchroniser that is reset to the idle level. Its output feeds an edge detector producing a one-cycle start candidate pulse, and also feeds the sampling logic directly. A single phase counter, loaded with zero at the start candidate, counts oversample ticks and produces one named sampling event at each bit interval centre. A five-state control machine consumes that event and decides which field of the frame is arriving. Under the machine's direction, a shift register assembles the payload least significant bit first, a bit counter tracks the arriving index, and a parity accumulator runs an exclusive-or over the stored bits. At the stop sample a single publish event transfers the assembled payload, the parity result and the framing result together into the holding register and status flags, which the consumer reads using a held valid and ready handshake.sync 2FFCh 5.1 — idle resetstart_candCh 5.2 — evidencephase_qCh 6.1 — one originstate_qCh 6.2 — 8 arcsshreg_qCh 6.3 — LSB firstpar_acc_qCh 6.4 — XORpublishCh 6.4 — one edgevalid / readyCh 6.5 — heldsyncph=0samplestoreaccstopbyte12
Figure 1 — the assembled receiver, with each block annotated by the chapter that derived it. Compare against Chapter 6.1 Figure 1: the partition is unchanged, which is the point. The module was specified before it was written, and the assembled code adds no block the plan did not contain and moves no boundary the plan did not draw.

4. The Same Receiver in Verilog-2001

The SystemVerilog above is one of three implementations of Chapter 6.1 §6's contract. This is the same hardware without enum, without logic, and without $clog2 — which is what Verilog-2001 costs and, as §11 shows, all it costs.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  uart_rx — Synthesizable Verilog-2001
//
//  Same hardware contract as the SystemVerilog and VHDL versions:
//  one clock, asynchronous active-low reset, 2-flop input synchroniser,
//  centre sampling at phase OVERSAMPLE/2-1, LSB-first assembly, and status
//  written once at the stop sample so it belongs to the held character.
//
//  Verilog-2001 has no enum and no packed struct, so the state encoding is
//  an explicit localparam set and the width discipline is manual.
//===========================================================================
module uart_rx_v #(
    parameter DATA_W     = 8,
    parameter OVERSAMPLE = 16
) (
    input  wire              clk,
    input  wire              rst_n,
    input  wire              rx_i,
    input  wire              os_tick_i,
    input  wire [1:0]        parity_mode_i,

    output reg  [DATA_W-1:0] rx_data_o,
    output reg               rx_valid_o,
    input  wire              rx_ready_i,
    output reg               rx_parity_err_o,
    output reg               rx_frame_err_o,
    output reg               rx_overrun_o,
    output wire              rx_active_o,
    output wire              rx_sync_o
);
    // ---- parity encodings (shared with the other two implementations) -----
    localparam [1:0] P_NONE = 2'b00,
                     P_EVEN = 2'b01,
                     P_ODD  = 2'b10,
                     P_MARK = 2'b11;

    // ---- explicit state encoding: no enum in Verilog-2001 -----------------
    localparam [2:0] S_IDLE   = 3'd0,
                     S_START  = 3'd1,
                     S_DATA   = 3'd2,
                     S_PARITY = 3'd3,
                     S_STOP   = 3'd4;

    // clog2 as a constant function — Verilog-2001 has no $clog2.
    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

    localparam CENTER = OVERSAMPLE / 2;         // sample at CENTER-1
    localparam PH_W   = (OVERSAMPLE <= 1) ? 1 : clog2(OVERSAMPLE);
    localparam BI_W   = (DATA_W     <= 1) ? 1 : clog2(DATA_W);

    reg [2:0]        state_q;
    reg [PH_W-1:0]   phase_q;
    reg [BI_W-1:0]   bit_idx_q;
    reg [DATA_W-1:0] shreg_q;
    reg              par_acc_q;
    reg              par_err_q;
    reg              rx_meta_q, rx_sync_q, rx_sync_d_q;

    assign rx_active_o = (state_q != S_IDLE);
    assign rx_sync_o   = rx_sync_q;

    // ---- input boundary: 2-flop synchroniser, reset to MARK ---------------
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            rx_meta_q   <= 1'b1;
            rx_sync_q   <= 1'b1;
            rx_sync_d_q <= 1'b1;
        end else begin
            rx_meta_q   <= rx_i;
            rx_sync_q   <= rx_meta_q;
            rx_sync_d_q <= rx_sync_q;
        end
    end

    // Start candidate: a falling edge on the SYNCHRONISED line only.
    wire start_cand = rx_sync_d_q && !rx_sync_q;
    // ONE free-running phase counter serves every active state: it is
    // zeroed at the start edge and never re-zeroed, so phase CENTER-1
    // recurs at the centre of every subsequent bit interval.
    wire sample_now = os_tick_i && (phase_q == (CENTER[PH_W-1:0] - 1'b1));

    // Expected parity bit for the accumulated data.
    function expected_parity;
        input [1:0] mode;
        input       acc;
        begin
            case (mode)
                P_EVEN:  expected_parity =  acc;
                P_ODD:   expected_parity = ~acc;
                default: expected_parity = 1'b1;   // MARK
            endcase
        end
    endfunction

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state_q         <= S_IDLE;
            phase_q         <= {PH_W{1'b0}};
            bit_idx_q       <= {BI_W{1'b0}};
            shreg_q         <= {DATA_W{1'b0}};
            par_acc_q       <= 1'b0;
            par_err_q       <= 1'b0;
            rx_data_o       <= {DATA_W{1'b0}};
            rx_valid_o      <= 1'b0;
            rx_parity_err_o <= 1'b0;
            rx_frame_err_o  <= 1'b0;
            rx_overrun_o    <= 1'b0;
        end else begin
            // Consumer handshake: the holding register frees, and EVERY status
            // bit clears with it. Parity, framing and overrun therefore all
            // describe the same holding-register epoch.
            if (rx_valid_o && rx_ready_i) begin
                rx_valid_o      <= 1'b0;
                rx_parity_err_o <= 1'b0;
                rx_frame_err_o  <= 1'b0;
                rx_overrun_o    <= 1'b0;
            end

            if (state_q == S_IDLE) begin
                if (start_cand) begin
                    state_q   <= S_START;
                    phase_q   <= {PH_W{1'b0}};     // anchor phase 0 at the start edge
                    bit_idx_q <= {BI_W{1'b0}};
                    par_acc_q <= 1'b0;
                    par_err_q <= 1'b0;             // status is per-frame
                end
            end else if (os_tick_i) begin
                // Free-running modulo-OVERSAMPLE counter, shared by every state.
                if (phase_q == (OVERSAMPLE[PH_W-1:0] - 1'b1)) phase_q <= {PH_W{1'b0}};
                else                                          phase_q <= phase_q + 1'b1;

                if (sample_now) begin
                    case (state_q)
                        S_START:
                            // Re-validate at the centre of the start bit.
                            if (rx_sync_q) state_q <= S_IDLE;   // false start
                            else           state_q <= S_DATA;

                        S_DATA: begin
                            // LSB first: shift right, insert at the top.
                            shreg_q   <= {rx_sync_q, shreg_q[DATA_W-1:1]};
                            par_acc_q <= par_acc_q ^ rx_sync_q;
                            bit_idx_q <= bit_idx_q + 1'b1;
                            // bit_idx_q still holds the index of the bit arriving
                            // NOW, so the last data bit compares against DATA_W-1.
                            if (bit_idx_q == (DATA_W[BI_W-1:0] - 1'b1)) begin
                                if (parity_mode_i == P_NONE) state_q <= S_STOP;
                                else                         state_q <= S_PARITY;
                            end
                        end

                        S_PARITY: begin
                            // Held INTERNALLY. Publishing here would let the next
                            // frame rewrite the held character's status.
                            par_err_q <= (rx_sync_q != expected_parity(parity_mode_i, par_acc_q));
                            state_q   <= S_STOP;
                        end

                        S_STOP: begin
                            if (rx_valid_o && !rx_ready_i) begin
                                // Holding register still occupied: drop the NEW
                                // character, keep the held one, report overrun.
                                rx_overrun_o <= 1'b1;
                            end else begin
                                rx_data_o       <= shreg_q;
                                rx_parity_err_o <= par_err_q;
                                rx_frame_err_o  <= !rx_sync_q;
                                rx_valid_o      <= 1'b1;
                            end
                            state_q <= S_IDLE;
                        end

                        default: state_q <= S_IDLE;
                    endcase
                end
            end
        end
    end
endmodule

Its self-checking testbench

The testbench drives the serial line directly, at a bit period passed as an argument. It never uses the design's transmitter, its tick, or its internal state to decide what to expect, and it computes parity from the specification rather than calling the design's function — the independence rule Chapter 14.5 §4 measured the value of.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  Self-checking Verilog testbench for uart_rx_v.
//  Drives the serial line directly at a chosen bit period — it never uses
//  the DUT's transmitter, its tick, or its internal state to decide what to
//  expect. Every check prints PASS or FAIL and the run prints a total.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_rx_v;
    parameter DATA_W     = 8;
    parameter OVERSAMPLE = 16;
    parameter CLK_NS     = 10;                 // 100 MHz
    parameter real TBIT  = 8680.5556;          // 115200 baud
    parameter integer CLKS_PER_OS = 54;        // ~TBIT/OVERSAMPLE/CLK_NS

    localparam [1:0] P_NONE = 2'b00, P_EVEN = 2'b01, P_ODD = 2'b10, P_MARK = 2'b11;

    reg clk = 1'b0;   always #(CLK_NS/2) clk = ~clk;
    reg rst_n = 1'b0;
    reg rx_line = 1'b1;
    reg [1:0] pmode = P_NONE;
    reg rx_ready = 1'b1;

    wire [DATA_W-1:0] rx_data;
    wire rx_valid, rx_perr, rx_ferr, rx_ovr, rx_active, rx_sync;

    // Oversample tick generator: one pulse every CLKS_PER_OS clocks.
    reg [7:0] osdiv = 8'd0;
    reg os_tick = 1'b0;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin osdiv <= 8'd0; os_tick <= 1'b0; end
        else if (osdiv == CLKS_PER_OS-1) begin osdiv <= 8'd0; os_tick <= 1'b1; end
        else begin osdiv <= osdiv + 8'd1; os_tick <= 1'b0; end
    end

    uart_rx_v #(.DATA_W(DATA_W), .OVERSAMPLE(OVERSAMPLE)) dut (
        .clk(clk), .rst_n(rst_n), .rx_i(rx_line), .os_tick_i(os_tick),
        .parity_mode_i(pmode),
        .rx_data_o(rx_data), .rx_valid_o(rx_valid), .rx_ready_i(rx_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_sync));

    integer checks = 0, fails = 0;
    task ck;
        input [511:0] name;
        input cond;
        begin
            checks = checks + 1;
            if (cond) $display("  PASS %0s", name);
            else begin fails = fails + 1; $display("  FAIL %0s", name); end
        end
    endtask

    // ---- independent parity, computed from the specification -------------
    function exp_parity;
        input [1:0] mode;
        input [DATA_W-1:0] d;
        integer k; reg a;
        begin
            a = 1'b0;
            for (k = 0; k < DATA_W; k = k + 1) a = a ^ d[k];
            case (mode)
                P_EVEN:  exp_parity =  a;
                P_ODD:   exp_parity = ~a;
                P_MARK:  exp_parity = 1'b1;
                default: exp_parity = 1'b0;
            endcase
        end
    endfunction

    // ---- line driver: bit period is an ARGUMENT, so baud error is testable
    task send_frame;
        input [DATA_W-1:0] d;
        input [1:0] mode;
        input real  tbit;
        input integer corrupt;      // 0 none, 1 parity, 2 stop, 3 false start
        integer k; reg p;
        begin
            if (corrupt == 3) begin
                rx_line = 1'b0; #(tbit/4.0); rx_line = 1'b1; #(tbit*2.0);
            end else begin
                p = exp_parity(mode, d);
                if (corrupt == 1) p = ~p;
                rx_line = 1'b0; #(tbit);                      // start
                for (k = 0; k < DATA_W; k = k + 1) begin
                    rx_line = d[k]; #(tbit);                  // LSB first
                end
                if (mode != P_NONE) begin rx_line = p; #(tbit); end
                rx_line = (corrupt == 2) ? 1'b0 : 1'b1; #(tbit);   // stop
                rx_line = 1'b1; #(tbit);
            end
        end
    endtask

    // ---- capture at the handshake ----------------------------------------
    reg [DATA_W-1:0] got_d; reg got_pe, got_fe;
    integer n_rx = 0, n_ovr = 0;
    reg ovr_d = 1'b0;
    always @(posedge clk) begin
        if (rst_n && rx_valid && rx_ready) begin
            got_d = rx_data; got_pe = rx_perr; got_fe = rx_ferr; n_rx = n_rx + 1;
        end
        // rx_overrun_o is a HELD flag: count its RISING edge, not its level.
        ovr_d <= rx_ovr;
        if (rst_n && rx_ovr && !ovr_d) n_ovr = n_ovr + 1;
    end

    integer i, base;
    reg [DATA_W-1:0] pats [0:7];

    initial begin
        pats[0]=8'h00; pats[1]=8'hFF; pats[2]=8'h55; pats[3]=8'hAA;
        pats[4]=8'h01; pats[5]=8'h80; pats[6]=8'h7F; pats[7]=8'hC3;

        $display("== uart_rx_v : self-checking Verilog testbench ==");

        // ---- 1. reset state ---------------------------------------------
        repeat (6) @(posedge clk);
        ck("reset: rx_valid low",  rx_valid === 1'b0);
        ck("reset: rx_active low", rx_active === 1'b0);
        ck("reset: rx_sync is MARK (never looks like a start bit)", rx_sync === 1'b1);
        rst_n = 1'b1;
        repeat (20) @(posedge clk);

        // ---- 2. all eight patterns, 8N1 ---------------------------------
        $display("-- 8N1, eight data patterns");
        pmode = P_NONE;
        for (i = 0; i < 8; i = i + 1) begin
            base = n_rx;
            send_frame(pats[i], P_NONE, TBIT, 0);
            #(TBIT);
            ck("8N1 byte received", n_rx == base + 1);
            ck("8N1 data correct",  got_d === pats[i]);
            ck("8N1 no status set", (got_pe === 1'b0) && (got_fe === 1'b0));
        end

        // ---- 3. parity modes --------------------------------------------
        $display("-- parity modes, clean frames");
        pmode = P_EVEN; #(TBIT);
        base = n_rx; send_frame(8'hA5, P_EVEN, TBIT, 0); #(TBIT);
        ck("8E1 data correct", (n_rx == base+1) && (got_d === 8'hA5));
        ck("8E1 no parity error", got_pe === 1'b0);
        pmode = P_ODD; #(TBIT);
        base = n_rx; send_frame(8'hA5, P_ODD, TBIT, 0); #(TBIT);
        ck("8O1 data correct", (n_rx == base+1) && (got_d === 8'hA5));
        ck("8O1 no parity error", got_pe === 1'b0);

        // ---- 4. injected parity error -----------------------------------
        $display("-- injected parity error");
        pmode = P_EVEN; #(TBIT);
        base = n_rx; send_frame(8'h3C, P_EVEN, TBIT, 1); #(TBIT);
        ck("parity error reported", got_pe === 1'b1);
        ck("byte still delivered",  (n_rx == base+1) && (got_d === 8'h3C));

        // ---- 5. injected framing error ----------------------------------
        $display("-- injected framing error");
        pmode = P_NONE; #(TBIT*2);
        base = n_rx; send_frame(8'h5A, P_NONE, TBIT, 2); #(TBIT*2);
        ck("framing error reported", got_fe === 1'b1);
        ck("byte still delivered",   (n_rx == base+1) && (got_d === 8'h5A));

        // ---- 6. false start is rejected ---------------------------------
        $display("-- false start");
        #(TBIT*2);
        base = n_rx; send_frame(8'h00, P_NONE, TBIT, 3); #(TBIT*2);
        ck("false start produces no character", n_rx == base);
        ck("receiver returned to idle", rx_active === 1'b0);

        // ---- 7. back-to-back frames -------------------------------------
        $display("-- back-to-back frames, no inter-frame gap");
        base = n_rx;
        send_frame(8'h12, P_NONE, TBIT, 0);
        send_frame(8'h34, P_NONE, TBIT, 0);
        #(TBIT);
        ck("both back-to-back frames received", n_rx == base + 2);
        ck("second byte correct", got_d === 8'h34);

        // ---- 8. overrun --------------------------------------------------
        $display("-- overrun: consumer stops reading");
        rx_ready = 1'b0; #(TBIT);
        base = n_ovr;
        send_frame(8'h11, P_NONE, TBIT, 0);      // fills the holding register
        send_frame(8'h22, P_NONE, TBIT, 0);      // must be dropped
        #(TBIT);
        ck("overrun flag raised exactly once", n_ovr == base + 1);
        ck("held character kept, not overwritten", rx_data === 8'h11);
        ck("overrun is HELD while the consumer stalls", rx_ovr === 1'b1);
        rx_ready = 1'b1; #(TBIT);

        // ---- 9. baud tolerance -------------------------------------------
        $display("-- baud offsets");
        base = n_rx; send_frame(8'h55, P_NONE, TBIT/1.03, 0); #(TBIT*2);
        ck("+3% baud: byte correct", (n_rx == base+1) && (got_d === 8'h55));
        base = n_rx; send_frame(8'h55, P_NONE, TBIT/0.97, 0); #(TBIT*2);
        ck("-3% baud: byte correct", (n_rx == base+1) && (got_d === 8'h55));

        $display("== %0d checks, %0d failures ==", checks, fails);
        if (fails == 0) $display("   RESULT: ALL VERILOG TESTS PASSED");
        else            $display("   RESULT: VERILOG TESTS FAILED");
        $finish;
    end
    initial begin #40_000_000; $display("TIMEOUT"); $finish; end
endmodule

5. The Same Receiver in VHDL-2008

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--==========================================================================
--  uart_rx -- Synthesizable VHDL-2008
--
--  Same hardware contract as the Verilog-2001 and SystemVerilog versions:
--  one clock, asynchronous active-low reset, 2-flop input synchroniser,
--  centre sampling at phase CENTER-1, LSB-first assembly, and status
--  written once at the stop sample so it belongs to the held character.
--
--  VHDL differences that are idiomatic rather than architectural:
--    * an enumerated state type instead of encoded localparams
--    * unsigned counters with explicit conversions
--    * one process with rising_edge and an asynchronous reset branch
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity uart_rx is
    generic (
        DATA_W     : natural := 8;
        OVERSAMPLE : natural := 16
    );
    port (
        clk             : in  std_logic;
        rst_n           : in  std_logic;                    -- async assert, active low
        rx_i            : in  std_logic;                    -- asynchronous line
        os_tick_i       : in  std_logic;                    -- one-clock enable
        parity_mode_i   : in  std_logic_vector(1 downto 0); -- 00 N, 01 E, 10 O, 11 M

        rx_data_o       : out std_logic_vector(DATA_W-1 downto 0);
        rx_valid_o      : out std_logic;
        rx_ready_i      : in  std_logic;
        rx_parity_err_o : out std_logic;
        rx_frame_err_o  : out std_logic;
        rx_overrun_o    : out std_logic;
        rx_active_o     : out std_logic;
        rx_sync_o       : out std_logic
    );
end entity uart_rx;

architecture rtl of uart_rx is

    constant P_NONE : std_logic_vector(1 downto 0) := "00";
    constant P_EVEN : std_logic_vector(1 downto 0) := "01";
    constant P_ODD  : std_logic_vector(1 downto 0) := "10";
    constant P_MARK : std_logic_vector(1 downto 0) := "11";

    -- Sample at CENTER-1 of a zero-based phase count, exactly as the other
    -- two implementations do.
    constant CENTER : natural := OVERSAMPLE / 2;

    type rx_state_t is (S_IDLE, S_START, S_DATA, S_PARITY, S_STOP);

    signal state_q     : rx_state_t := S_IDLE;
    signal phase_q     : unsigned(15 downto 0) := (others => '0');
    signal bit_idx_q   : unsigned(15 downto 0) := (others => '0');
    signal shreg_q     : std_logic_vector(DATA_W-1 downto 0) := (others => '0');
    signal par_acc_q   : std_logic := '0';
    signal par_err_q   : std_logic := '0';

    signal rx_meta_q   : std_logic := '1';
    signal rx_sync_q   : std_logic := '1';
    signal rx_sync_d_q : std_logic := '1';

    signal valid_q     : std_logic := '0';

    -- Expected parity bit for the accumulated data, from the specification.
    function expected_parity (mode : std_logic_vector(1 downto 0);
                              acc  : std_logic) return std_logic is
    begin
        if    mode = P_EVEN then return acc;
        elsif mode = P_ODD  then return not acc;
        else                     return '1';        -- MARK
        end if;
    end function;

begin

    -- Published outputs that are pure functions of state.
    rx_active_o <= '0' when state_q = S_IDLE else '1';
    rx_sync_o   <= rx_sync_q;
    rx_valid_o  <= valid_q;

    -- A start candidate is a falling edge on the SYNCHRONISED line only.
    process (clk, rst_n)
        variable start_cand : std_logic;
        variable sample_now : std_logic;
    begin
        if rst_n = '0' then
            rx_meta_q       <= '1';                 -- reset to MARK so reset
            rx_sync_q       <= '1';                 -- never looks like a start
            rx_sync_d_q     <= '1';
            state_q         <= S_IDLE;
            phase_q         <= (others => '0');
            bit_idx_q       <= (others => '0');
            shreg_q         <= (others => '0');
            par_acc_q       <= '0';
            par_err_q       <= '0';
            rx_data_o       <= (others => '0');
            valid_q         <= '0';
            rx_parity_err_o <= '0';
            rx_frame_err_o  <= '0';
            rx_overrun_o    <= '0';

        elsif rising_edge(clk) then
            -- 2-flop synchroniser plus one history flop for edge detection.
            rx_meta_q   <= rx_i;
            rx_sync_q   <= rx_meta_q;
            rx_sync_d_q <= rx_sync_q;

            start_cand := rx_sync_d_q and (not rx_sync_q);
            if os_tick_i = '1' and phase_q = to_unsigned(CENTER - 1, phase_q'length) then
                sample_now := '1';
            else
                sample_now := '0';
            end if;

            -- Consumer handshake: the holding register frees, and EVERY status
            -- bit clears with it. Parity, framing and overrun therefore all
            -- describe the same holding-register epoch.
            if valid_q = '1' and rx_ready_i = '1' then
                valid_q         <= '0';
                rx_parity_err_o <= '0';
                rx_frame_err_o  <= '0';
                rx_overrun_o    <= '0';
            end if;

            if state_q = S_IDLE then
                if start_cand = '1' then
                    state_q   <= S_START;
                    phase_q   <= (others => '0');   -- anchor phase 0 at the edge
                    bit_idx_q <= (others => '0');
                    par_acc_q <= '0';
                    par_err_q <= '0';
                end if;

            elsif os_tick_i = '1' then
                -- One free-running modulo-OVERSAMPLE counter for every state.
                if phase_q = to_unsigned(OVERSAMPLE - 1, phase_q'length) then
                    phase_q <= (others => '0');
                else
                    phase_q <= phase_q + 1;
                end if;

                if sample_now = '1' then
                    case state_q is

                        when S_START =>
                            -- Re-validate at the centre of the start bit.
                            if rx_sync_q = '1' then
                                state_q <= S_IDLE;          -- false start
                            else
                                state_q <= S_DATA;
                            end if;

                        when S_DATA =>
                            -- LSB first: shift right, insert at the top.
                            shreg_q   <= rx_sync_q & shreg_q(DATA_W-1 downto 1);
                            par_acc_q <= par_acc_q xor rx_sync_q;
                            bit_idx_q <= bit_idx_q + 1;
                            -- bit_idx_q still holds the index of the bit arriving
                            -- NOW, so the last data bit compares against DATA_W-1.
                            if bit_idx_q = to_unsigned(DATA_W - 1, bit_idx_q'length) then
                                if parity_mode_i = P_NONE then
                                    state_q <= S_STOP;
                                else
                                    state_q <= S_PARITY;
                                end if;
                            end if;

                        when S_PARITY =>
                            -- Held INTERNALLY. Publishing here would let the next
                            -- frame rewrite the held character's status.
                            if rx_sync_q /= expected_parity(parity_mode_i, par_acc_q) then
                                par_err_q <= '1';
                            else
                                par_err_q <= '0';
                            end if;
                            state_q <= S_STOP;

                        when S_STOP =>
                            if valid_q = '1' and rx_ready_i = '0' then
                                -- Holding register still occupied: drop the NEW
                                -- character, keep the held one, report overrun.
                                rx_overrun_o <= '1';
                            else
                                rx_data_o       <= shreg_q;
                                rx_parity_err_o <= par_err_q;
                                if rx_sync_q = '0' then
                                    rx_frame_err_o <= '1';
                                else
                                    rx_frame_err_o <= '0';
                                end if;
                                valid_q <= '1';
                            end if;
                            state_q <= S_IDLE;

                        when others =>
                            state_q <= S_IDLE;
                    end case;
                end if;
            end if;
        end if;
    end process;

end architecture rtl;

Its self-checking testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--==========================================================================
--  Self-checking VHDL testbench for uart_rx.
--  Drives the serial line directly at a chosen bit period. It never uses the
--  DUT's internal state, its tick, or its parity function to decide what to
--  expect. Every check reports PASS or FAIL and the run reports a total.
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;

entity tb_uart_rx is
end entity tb_uart_rx;

architecture sim of tb_uart_rx is

    constant DATA_W     : natural := 8;
    constant OVERSAMPLE : natural := 16;
    constant CLK_PERIOD : time    := 10 ns;          -- 100 MHz
    constant TBIT       : time    := 8680 ns;        -- ~115200 baud
    constant CLKS_PER_OS: natural := 54;

    constant P_NONE : std_logic_vector(1 downto 0) := "00";
    constant P_EVEN : std_logic_vector(1 downto 0) := "01";
    constant P_ODD  : std_logic_vector(1 downto 0) := "10";

    signal clk       : std_logic := '0';
    signal rst_n     : std_logic := '0';
    signal rx_line   : std_logic := '1';
    signal os_tick   : std_logic := '0';
    signal pmode     : std_logic_vector(1 downto 0) := P_NONE;
    signal rx_ready  : std_logic := '1';

    signal rx_data   : std_logic_vector(DATA_W-1 downto 0);
    signal rx_valid  : std_logic;
    signal rx_perr   : std_logic;
    signal rx_ferr   : std_logic;
    signal rx_ovr    : std_logic;
    signal rx_active : std_logic;
    signal rx_sync   : std_logic;

    signal checks    : natural := 0;
    signal fails     : natural := 0;
    signal n_rx      : natural := 0;
    signal n_ovr     : natural := 0;
    signal got_d     : std_logic_vector(DATA_W-1 downto 0) := (others => '0');
    signal got_pe    : std_logic := '0';
    signal got_fe    : std_logic := '0';
    signal ovr_d     : std_logic := '0';
    signal done      : boolean := false;

begin

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

    -- Oversample tick: one pulse every CLKS_PER_OS clocks.
    os_gen : process (clk, rst_n)
        variable d : natural := 0;
    begin
        if rst_n = '0' then
            d := 0; os_tick <= '0';
        elsif rising_edge(clk) then
            if d = CLKS_PER_OS - 1 then d := 0; os_tick <= '1';
            else                        d := d + 1; os_tick <= '0';
            end if;
        end if;
    end process;

    dut : 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 => pmode,
            rx_data_o => rx_data, rx_valid_o => rx_valid, rx_ready_i => rx_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_sync);

    -- Capture at the handshake, exactly as a consumer would.
    cap : process (clk)
    begin
        if rising_edge(clk) then
            if rst_n = '1' and rx_valid = '1' and rx_ready = '1' then
                got_d  <= rx_data;
                got_pe <= rx_perr;
                got_fe <= rx_ferr;
                n_rx   <= n_rx + 1;
            end if;
            -- rx_overrun_o is a HELD flag: count its RISING edge, not its level.
            ovr_d <= rx_ovr;
            if rst_n = '1' and rx_ovr = '1' and ovr_d = '0' then
                n_ovr <= n_ovr + 1;
            end if;
        end if;
    end process;

    stim : process

        procedure ck (name : in string; cond : in boolean) is
            variable l : line;
        begin
            checks <= checks + 1;
            if cond then
                write(l, string'("  PASS ") & name);
            else
                fails <= fails + 1;
                write(l, string'("  FAIL ") & name);
            end if;
            writeline(output, l);
            wait for 0 ns;
        end procedure;

        -- Independent parity, computed from the specification.
        function exp_parity (mode : std_logic_vector(1 downto 0);
                             d    : std_logic_vector(DATA_W-1 downto 0))
                             return std_logic is
            variable a : std_logic := '0';
        begin
            for k in 0 to DATA_W-1 loop
                a := a xor d(k);
            end loop;
            if    mode = P_EVEN then return a;
            elsif mode = P_ODD  then return not a;
            else                     return '0';
            end if;
        end function;

        -- corrupt: 0 none, 1 parity, 2 stop, 3 false start
        procedure send_frame (d       : in std_logic_vector(DATA_W-1 downto 0);
                              mode    : in std_logic_vector(1 downto 0);
                              tbit    : in time;
                              corrupt : in natural) is
            variable p : std_logic;
        begin
            if corrupt = 3 then
                rx_line <= '0'; wait for tbit/4;
                rx_line <= '1'; wait for tbit*2;
            else
                p := exp_parity(mode, d);
                if corrupt = 1 then p := not p; end if;
                rx_line <= '0'; wait for tbit;                 -- start
                for k in 0 to DATA_W-1 loop
                    rx_line <= d(k); wait for tbit;            -- LSB first
                end loop;
                if mode /= P_NONE then
                    rx_line <= p; wait for tbit;
                end if;
                if corrupt = 2 then rx_line <= '0'; else rx_line <= '1'; end if;
                wait for tbit;
                rx_line <= '1'; wait for tbit;
            end if;
        end procedure;

        type pat_array is array (0 to 7) of std_logic_vector(7 downto 0);
        constant pats : pat_array := (x"00", x"FF", x"55", x"AA",
                                      x"01", x"80", x"7F", x"C3");
        variable base : natural;
        variable l    : line;
    begin
        write(l, string'("== uart_rx (VHDL) : self-checking testbench ==")); writeline(output, l);

        -- 1. reset state
        wait for CLK_PERIOD*6;
        ck("reset: rx_valid low",  rx_valid = '0');
        ck("reset: rx_active low", rx_active = '0');
        ck("reset: rx_sync is MARK (never looks like a start bit)", rx_sync = '1');
        rst_n <= '1';
        wait for CLK_PERIOD*20;

        -- 2. eight data patterns, 8N1
        write(l, string'("-- 8N1, eight data patterns")); writeline(output, l);
        pmode <= P_NONE;
        for i in 0 to 7 loop
            base := n_rx;
            send_frame(pats(i), P_NONE, TBIT, 0);
            wait for TBIT;
            ck("8N1 byte received", n_rx = base + 1);
            ck("8N1 data correct",  got_d = pats(i));
            ck("8N1 no status set", got_pe = '0' and got_fe = '0');
        end loop;

        -- 3. parity modes
        write(l, string'("-- parity modes, clean frames")); writeline(output, l);
        pmode <= P_EVEN; wait for TBIT;
        base := n_rx; send_frame(x"A5", P_EVEN, TBIT, 0); wait for TBIT;
        ck("8E1 data correct", n_rx = base+1 and got_d = x"A5");
        ck("8E1 no parity error", got_pe = '0');
        pmode <= P_ODD; wait for TBIT;
        base := n_rx; send_frame(x"A5", P_ODD, TBIT, 0); wait for TBIT;
        ck("8O1 data correct", n_rx = base+1 and got_d = x"A5");
        ck("8O1 no parity error", got_pe = '0');

        -- 4. injected parity error
        write(l, string'("-- injected parity error")); writeline(output, l);
        pmode <= P_EVEN; wait for TBIT;
        base := n_rx; send_frame(x"3C", P_EVEN, TBIT, 1); wait for TBIT;
        ck("parity error reported", got_pe = '1');
        ck("byte still delivered",  n_rx = base+1 and got_d = x"3C");

        -- 5. injected framing error
        write(l, string'("-- injected framing error")); writeline(output, l);
        pmode <= P_NONE; wait for TBIT*2;
        base := n_rx; send_frame(x"5A", P_NONE, TBIT, 2); wait for TBIT*2;
        ck("framing error reported", got_fe = '1');
        ck("byte still delivered",   n_rx = base+1 and got_d = x"5A");

        -- 6. false start
        write(l, string'("-- false start")); writeline(output, l);
        wait for TBIT*2;
        base := n_rx; send_frame(x"00", P_NONE, TBIT, 3); wait for TBIT*2;
        ck("false start produces no character", n_rx = base);
        ck("receiver returned to idle", rx_active = '0');

        -- 7. back-to-back frames
        write(l, string'("-- back-to-back frames, no inter-frame gap")); writeline(output, l);
        base := n_rx;
        send_frame(x"12", P_NONE, TBIT, 0);
        send_frame(x"34", P_NONE, TBIT, 0);
        wait for TBIT;
        ck("both back-to-back frames received", n_rx = base + 2);
        ck("second byte correct", got_d = x"34");

        -- 8. overrun
        write(l, string'("-- overrun: consumer stops reading")); writeline(output, l);
        rx_ready <= '0'; wait for TBIT;
        base := n_ovr;
        send_frame(x"11", P_NONE, TBIT, 0);
        send_frame(x"22", P_NONE, TBIT, 0);
        wait for TBIT;
        ck("overrun flag raised exactly once", n_ovr = base + 1);
        ck("held character kept, not overwritten", rx_data = x"11");
        ck("overrun is HELD while the consumer stalls", rx_ovr = '1');
        rx_ready <= '1'; wait for TBIT;

        -- 9. baud offsets
        write(l, string'("-- baud offsets")); writeline(output, l);
        base := n_rx; send_frame(x"55", P_NONE, TBIT*100/103, 0); wait for TBIT*2;
        ck("+3% baud: byte correct", n_rx = base+1 and got_d = x"55");
        base := n_rx; send_frame(x"55", P_NONE, TBIT*100/97, 0); wait for TBIT*2;
        ck("-3% baud: byte correct", n_rx = base+1 and got_d = x"55");

        write(l, string'("== ")); write(l, checks); write(l, string'(" checks, "));
        write(l, fails); write(l, string'(" failures ==")); writeline(output, l);
        if fails = 0 then
            write(l, string'("   RESULT: ALL VHDL TESTS PASSED"));
        else
            write(l, string'("   RESULT: VHDL TESTS FAILED"));
        end if;
        writeline(output, l);
        done <= true;
        wait;
    end process;

end architecture sim;
fsm
The complete receive state machine as implemented in all three languages. From Idle, a start candidate moves the machine to Start. In Start the first sampling event re-validates the start bit: if the line is still at the space level the machine advances to Data, and if it has returned to mark the candidate was false and the machine returns to Idle without writing any status. In Data each sampling event stores one bit and the machine remains in Data until the final data index, at which point the configured parity mode selects the next state, Parity when parity is enabled and Stop directly when it is not. From Parity the next sampling event moves unconditionally to Stop. From Stop the next sampling event moves unconditionally back to Idle, publishing the character and its status. An eighth transition returns any unreachable state encoding to Idle, which exists in the netlist of all three implementations because each carries a default or when-others branch.S_IDLES_STARTS_DATAS_PARITYS_STOP1: start_cand1: start_cand3: still space3: stillspace2: false start2: falsestartstore bitstore bit4: parity on4: parity on5: no parity5: no parity6: sample6: sample7: always7: always
Figure 2 — the complete state graph as implemented, including the recovery edge. All three language versions implement exactly these eight transitions; only the encoding of the states differs.

6. The Walkthrough

Each part, and the invariant it maintains.

Parameters and interface. Invariant: the hardware's shape is fixed at elaboration and its behaviour is selected at runtime. DATA_W and OVERSAMPLE change register widths; parity_mode_i selects a path. Chapter 6.1 §5 gives the test that separates them.

Derived constants. Invariant: no position in this design is a literal. CENTER, PH_W and BI_W all follow the parameters, so changing OVERSAMPLE from 16 to 8 moves every sampling position correctly. Chapter 5.3 §6 showed what a hard-coded 7 costs.

Parameter legality. Invariant: a parameter for which the arithmetic is undefined fails at elaboration, not in silicon. Both guards mark genuinely broken configurations rather than merely unusual ones — see §6.

State type. Invariant: the machine's states are named and enumerable. Five values in three bits, with three encodings unreachable and a default that recovers from them.

Synchronisation boundary. Invariant: exactly one representation of the line exists, and rx_i reaches nothing else. Chapter 6.1 §9 explains why two synchronisers would let the receiver hold contradictory beliefs about the wire.

Named events. Invariant: start_cand and sample_now are each produced in exactly one place. Scattering phase_q == 7 through the design is the defect Chapter 6.3 §3 named; one assignment means one thing to change.

Phase counter. Invariant: phase zero is the start candidate, and the sample fires on the tick that carries the count to CENTER. This is Chapter 6.1 §4's single origin, and the reason all eleven samples in Chapter 6.3 §7's trace read phase_q == 7.

Bit counter. Invariant: bit_idx_q is the index of the data bit arriving now, and nothing outside S_DATA reads it. That second clause is what makes the wrap from 7 to 0 harmless at DATA_W = 8.

Shift register. Invariant: after n stores, the first n arrived bits occupy the top n positions, and after DATA_W stores they are correctly placed. Verified by the 00, 80, C0, 60, 30, 98, 4C, A6 trace.

Parity accumulator. Invariant: it holds the XOR of the payload bits stored so far, and nothing else. Cleared at the candidate; the received parity bit never enters it.

Stop validation. Invariant: the framing result describes the stop interval alone. One comparison, at the stop sample, published in the same branch as the data.

Holding register and status. Invariant: data and all three flags describe one frame and move together. The single publish point, and Chapter 6.5 §3's stability property.

Control and recovery. Invariant: the machine is always in a legal state and always leaves every state. No conditional exits except the qualification test; default covers the rest.

7. Review Checklist

The questions a reviewer should ask, with this design's answers. Every no would be a finding.

QuestionHere
Does raw rx_i fan out anywhere?No — only into rx_meta_q
One clock domain?Yes — one clk, no generated clock
Are timing events clock enables?Yes — os_tick_i and sample_now
What is phase count zero?The start candidate, stated in 6.3 §1
Is the centre sample derived or magic?Derived: CENTER - 1 from OVERSAMPLE
Is the bit-counter convention documented?Yes — index of the bit arriving now
Is LSB-first reconstruction correct?Verified against 0xA6 and eight other patterns
Is the parity accumulator cleared at the right time?At the candidate, with par_err_q
Does the received parity bit enter the accumulator?No
Does disabled parity skip the state cleanly?Yes — one branch, one place
Is stop validated at the intended sample?Yes — 10.5 UI in 8E1, verified in simulation
Are error flags tied to the correct frame?Yes — via par_err_q and a single publish point
Can rx_valid_o be missed?No — held until accepted
What happens if the consumer stalls?Held byte preserved, new frame dropped, overrun raised
Back-to-back frames?Received at full rate, verified
Reset mid-frame?In-flight byte discarded; see §7
Illegal state?default returns to S_IDLE
Can any configuration create a zero-width counter?No — both guards, §6
Are parameters checked?Yes, at elaboration
Is any status sticky across frames?No — deliberately, Module 9 owns that

8. Parameter Legality, and Not Over-Engineering It

Two checks, and both mark configurations where the arithmetic above is undefined:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if (OVERSAMPLE < 2) $fatal(1, ...);   // CENTER = 0, so CENTER-1 underflows
if (DATA_W < 1)     $fatal(1, ...);   // a zero-width shift register

OVERSAMPLE = 1 gives CENTER = 0, and CENTER - 1 on an unsigned localparam is a very large number — the comparison never matches and the receiver never samples. That is worth a fatal rather than a comment.

What is deliberately not checked is as informative:

Odd OVERSAMPLE. CENTER truncates, placing the sample half a tick before the true centre. That is a real but tiny asymmetry, and Chapter 5.3's standalone module warned about it. Here it is legal.

DATA_W outside 5–9. UART configurations conventionally span 5 to 9 data bits, and this receiver works for any width its parameters can express. Restricting it to the conventional range would reject a valid use of the block for no engineering reason.

Two stop bits. Not a parameter at all — the assumptions block says one, which is more honest than a parameter that silently ignores its second value.

9. Corner Cases

Reset released onto a live line. The receiver returns to S_IDLE correctly and the in-flight byte is discarded — and then it mis-frames on the remainder and delivers a byte that was never sent. Simulation produced 0xFA from an interrupted 0xA6. Nothing malfunctioned: the next mark-to-space transition it saw was a data edge, and nothing in a UART frame identifies itself as a continuation. Chapter 6.2 §7 develops it. A testbench must allow an idle period after a mid-frame reset, and a system resetting a UART during traffic should expect one spurious byte.

Line stuck at space. The receiver validates a stop of 0, reports rx_frame_err_o, returns to S_IDLE, and then — correctly — does nothing, because a candidate needs a mark-to-space transition and there is no mark to leave. It resumes the moment a real frame arrives. This is the behaviour Chapter 6.2 §4's unconditional stop exit exists to produce; the conditional version wedges here permanently.

Consumer never reads. One byte held indefinitely, rx_overrun_o set at the second frame and remaining set. The receiver does not escalate or give up. This is intentional: escalation policy belongs to the system, and Module 9 is where it is built.

Accept and delivery on the same cycle. rx_valid_o && rx_ready_i clears the register set, and the S_STOP branch may write it in the same cycle. The clear is written first in the always_ff, so the delivery's non-blocking assignments take precedence — the byte is accepted and replaced on one edge with no gap and no overrun. Verified by the back-to-back test with a prompt consumer.

Glitch narrower than half a bit. Rejected by qualification: the sample at 0.5 UI finds mark and the machine returns to S_IDLE. Chapter 5.2 established the exact class this rejects — disturbances shorter than the validation delay, and nothing more.

10. Verification Matrix and Results

The receiver-focused matrix, with what each axis is for:

AxisValues exercisedCatches
Data patterns00 FF 55 AA 01 80 A5 A6 53see below
Parity modenone / even / oddmode swap, accumulator convention
Parity corruptioninverted parity bit, both modescheck polarity, status ownership
Stop corruptionstop driven to spaceframing detection, wedging
False startglitch of 1/8 UIqualification
Back-to-backno idle gapdelivery latency, return-to-idle
Consumer stallready held low across a frameoverrun policy, status ownership
Reset timingasserted mid-S_DATAreset semantics, mis-framing
Clock mismatch±1%, ±3%, both signssample placement, 5.5's model

Why these data patterns, since a list without reasons is not a plan:

PatternPurpose
0x00, 0xFFlongest runs — expose sampling that drifts without transitions to re-anchor on. Both are palindromic, so neither can detect a reversed shift register.
0x55, 0xAAmaximum transition density — expose setup/hold and synchroniser issues. Reverse into each other.
0x01, 0x80single bit at each end — expose bit-order reversal unambiguously, since 0x01 reverses to 0x80.
0xA5, 0xA6asymmetric, differing in one bit — 0xA6 reverses to 0x65, and the pair differs in parity-relevant weight.
0x53asymmetric and odd-looking — reverses to 0xCA, the classic reversal signature.

Note the parity consequence from Chapter 6.4 §2: seven of these nine bytes have even weight, so the odd-parity branch is exercised only by 0x01 and 0x80. A pattern set chosen for visual variety rather than by weight leaves half the parity logic untested.

Results, per language, from the runs in §1. The Verilog and SystemVerilog testbenches drive the same scenarios and differ only in the assertion binding:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ iverilog -g2012 -o sim_sv uart_parity_pkg.sv uart_rx_sv.sv rx_assertions.sv tb_uart_rx_sv.sv && ./sim_sv
== uart_rx_sv : self-checking SystemVerilog testbench ==
  ...
  PASS assertions: no violations during the whole run
== 45 checks, 0 failures ==
   RESULT: ALL SYSTEMVERILOG TESTS PASSED

$ iverilog -g2001 -o sim_v uart_rx_v.v tb_uart_rx_v.v && ./sim_v
== uart_rx_v : self-checking Verilog testbench ==
  ...
== 44 checks, 0 failures ==
   RESULT: ALL VERILOG TESTS PASSED

$ nvc --std=2008 -a uart_rx.vhd tb_uart_rx.vhd && nvc --std=2008 -e tb_uart_rx && nvc --std=2008 -r tb_uart_rx
== uart_rx (VHDL) : self-checking testbench ==
  ...
  PASS overrun is HELD while the consumer stalls
  PASS +3% baud: byte correct
  PASS -3% baud: byte correct
== 44 checks, 0 failures ==
   RESULT: ALL VHDL TESTS PASSED
ScenarioVerilogSystemVerilogVHDL
reset state — valid low, active low, sync at MARK333
eight data patterns, 8N1242424
8E1 and 8O1, clean444
injected parity error — reported and delivered222
injected framing error — reported and delivered222
false start rejected, returns to idle222
back-to-back frames, no gap222
overrun — flag raised once, held, byte preserved333
baud offset ±3%222
bound assertions, no violations—1—
total444544

All three testbenches run the same nine scenarios. The SystemVerilog total is one higher only because the assertion module is bound into it; nothing else differs. Together that is 133 checks and zero failures, and each language's testbench was written against the contract rather than translated from another, which is why §11's equivalence claim means something.

The assertions were mutation-checked. Rebuilding the receiver with a one-clock pulse valid — Chapter 6.5 §2's mistake, made deliberately — produced:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  A2 violations on the pulse-valid mutant: 2
  == 45 checks, 4 failures ==

against 45 checks, 0 failures and 0 violations on the correct design. An assertion that has never fired has not been shown capable of firing.

What the matrix does not cover, stated so it is not mistaken for completeness: randomised start phase against a free-running clock (the testbench drives on the clock grid); mismatch beyond ±3%; DATA_W other than 8 in simulation, though the arithmetic was checked across 5–9; OVERSAMPLE other than 16 in simulation; and any form of constrained-random or coverage-driven verification, which is Modules 14–16.

11. Cross-Language Equivalence

Three implementations of one contract are only worth having if they agree — and all three were simulated, so this is a behavioural claim rather than a reading of the source. Row by row, against Chapter 6.1 §6:

PropertyVerilog-2001SystemVerilogVHDL-2008Agree?
ports and directions131313yes
DATA_W / OVERSAMPLEparametersparametersgenericsyes
parity encodinglocalparam 2-bitenum logic [1:0]std_logic_vector(1 downto 0)yes — same values
reset styleasync assert, active lowasync assert, active lowasync assert, active lowyes
reset value of the synchroniser1 (MARK)1 (MARK)'1' (MARK)yes
sample phaseCENTER-1CENTER-1CENTER-1yes
phase counterone, free-running mod OVERSAMPLEsamesameyes
bit orderingshift right, insert at MSBsamesameyes
state graph5 states, 8 transitionssamesameyes
state encodingfixed 000–100fixed 000–100chosen by synthesisno — deliberate
rx_valid_olevel, held to handshakesamesameyes
status lifetimecleared on handshakesamesameyes
rx_overrun_oheld, not pulsedheldheldyes — after §7 of 6.4
latency, start edge to rx_valid_oidenticalidenticalidenticalyes

One deliberate difference, and it is a property of the languages rather than of the designs. VHDL's enumerated state type does not fix an encoding; synthesis chooses one unless an attribute constrains it. Verilog and SystemVerilog fix it by construction because the enumeration literals are numbers.

That difference has a consequence worth stating: the numeric state encoding is not part of this contract in any of the three languages, and code outside the receiver must never depend on it. Chapter 11.4 §3 found exactly that dependency in a later integration and replaced it with the published rx_active_o port — which exists in all three implementations precisely so nobody needs the encoding.

12. UVM Mapping

Module 6 is an RTL module, so this is a mapping rather than an environment — the environment itself is Module 16. What is worth fixing now is which part of the receiver's contract each UVM component would own, because the answer follows from Chapter 6.1 §6's boundaries rather than from UVM.

Receiver contract elementUVM componentWhat it carries
a frame on the wiresequence itemdata, data_bits, parity_mode, baud_offset, parity_corrupt, stop_corrupt
driving rx_idriverturns one item into a waveform at an arbitrary bit period
observing rx_iinput monitorreconstructs what was actually driven
observing the read portoutput monitor{data, parity_err, frame_err} sampled at rx_valid_o && rx_ready_i
rx_ready_i policysequence on a second sequenceralways-ready, random-stall, or never-ready for the overrun case
expected resultpredictorcomputes status from the item alone, never from the DUT
comparisonscoreboardin-order, whole-tuple

Three things in that table are decided by this module, not by UVM.

The output monitor samples at the handshake, not when rx_valid_o rises. §8's contract says data and status are stable from the rise until the handshake; sampling at the rise would work and would miss a design that lets them move afterwards. Sampling at the handshake checks the property the consumer actually depends on.

rx_ready_i needs its own sequence, not a fixed value. The overrun path only exists when ready is low at a stop sample, so a testbench that ties ready high cannot reach it — and Chapter 6.5 §5's overrun trace could not have been produced.

The predictor must not import the design's parity function. Chapter 14.5 §4 measured what that costs: the same mutant produced 8 mismatches from an independent checker and 0 from one that reused the design's own function. The testbenches in §4 and §5 above already follow the rule — each computes parity from the specification — which is why they are usable as the basis for the UVM driver and predictor rather than being thrown away.

What is deliberately not here. No uvm_component boilerplate, no factory registration, no config-db plumbing. None of it would teach anything about a UART receiver, and all of it is Module 16's subject where it can be built once and used properly.

13. Debugging: Symptom to Mechanism

14. What This Means on an FPGA

Thirty-five flip-flops at DATA_W = 8, OVERSAMPLE = 16. Nothing here needs optimising, and anything that appears to want it is a sign a parameter has been pushed somewhere a UART does not go.

The synchroniser is the only structure needing a constraint, and it is Module 12's to specify. Its placement is fixed here: adjacent to the pin, with nothing tapping rx_i.

Expose state_q, sample_now, rx_valid_o and rx_ready_i during bring-up — four signals that resolve most of §9's table at a glance. They are debug aids, not interface: a production instantiation need not carry them, and a bring-up build should.

Check the synthesis report for a latch. There should be none; every branch in both always_ff blocks either assigns or holds a register. This is a reviewed claim, per §1 — an actual synthesis run is what would turn it into a verified one.

15. Understanding Check

16. Summary

One synthesizable receiver, written three times from one contract and simulated to 133 checks with 0 failures — 45 in SystemVerilog and 44 in Verilog under Icarus Verilog 13.0, and 44 in VHDL under NVC 1.23.0 — with every trace published across Module 6 extracted from those runs rather than computed by hand. MDX compiling and HDL compiling are different facts, and both were established for all three languages; synthesisability remains a reviewed claim awaiting Module 12.

The assumptions block states what the receiver is not: no FIFO, no break handling, no idle detection, no sticky status, no second stop bit, no majority voting — each deferred to a named module, because unsupported behaviour must look deliberate.

Every part maintains a stated invariant, and the walkthrough names them: one representation of the line, one origin, one publish point, one place where configuration steers control flow, and a counter whose value is read only where it is meaningful.

Two parameters and one runtime input, and even that small space has corners — DATA_W = 8 wraps the bit counter, a non-power-of-two OVERSAMPLE exposes the natural-wrap defect. Every parameter multiplies the verification burden, which is why the design has two rather than eight.

The corner cases are documented rather than discovered: mis-framing after reset onto a live line, correct inaction on a line stuck at space, indefinite holding when the consumer never reads, and accept-plus-delivery on one edge.

Two real defects were found during development — status ownership, found by reasoning about a path no nominal test reaches, and a flip-flop count contradicted by computing it. Both are reported where they occurred.

17. Where Module 6 Leaves You

Six chapters have turned Module 5's timing mechanisms into a receiver.

Chapter 6.1 partitioned it, wrote the interface before the implementation, and settled the timing origin that Module 5 deliberately left open. Chapter 6.2 built the control machine as a table of eight transitions and showed which three implementations get wrong. Chapter 6.3 derived both counters from the parameters, stated every semantic before its RTL, and traced a byte through the shift register. Chapter 6.4 added validation and the publish discipline that keeps status attached to the frame it describes. Chapter 6.5 chose the output interface from the fact that a receiver cannot tell the wire to wait. This chapter assembled, compiled, simulated and reviewed the result.

An engineer holding this chain can design a receiver from a specification, derive its counter widths and semantics, defend its interface, review someone else's, and diagnose a failure from a symptom — without memorising the final RTL, which is the test Chapter 6.1 §1 set for the interface and which applies equally to the module.

What remains is the other half of the link, and everything the receiver deferred.

18. What Comes Next

Module 7 builds the transmitter — the simpler half, done properly. It has no start detection, no qualification and no sampling: it executes a schedule it wrote. What it does have is a producer-facing contract that is easy to get subtly wrong, a frame sequencer whose shape must match this receiver's exactly, and a back-to-back behaviour that decides whether the link runs at its rated throughput or at some fraction of it.

After that: Module 8 builds the os_tick both halves have been consuming; Module 9 takes the per-frame status this module produces and builds the error, break and recovery architecture on top of it — including the idle detection that would suppress §7's fabricated byte; Module 10 adds the buffering that moves the overrun threshold; and Modules 11 onward assemble an IP, integrate it, and verify it properly.

Browse the full path on the UART tutorials index. For the sampling mechanisms this receiver assembles, read back to Chapter 5.3; for the budget that decides whether it works at a given rate, Chapter 4.5.

Continue learning

Where this fits

Part of the UART curriculum.