Skip to content
VLSI Mentor

UART · Module 7

Complete TX RTL Architecture and Configuration Handling

One synthesizable transmitter assembled from the module's four preceding chapters, with the configuration-capture discipline that keeps a frame's format fixed once it starts — then reviewed the way a reviewer would, including the defect found during its own development.

Four chapters have specified a transmitter completely. Chapter 7.1 built the datapath and established that the payload must be captured; Chapter 7.2 enumerated eight transitions and registered the output; Chapter 7.3 fixed the tick contract and measured the launch latency; Chapter 7.4 defined acceptance and eliminated the inter-frame gap.

This chapter assembles them, develops the subject the curriculum names for it — how a configuration change is applied safely between frames rather than mid-frame — and then reviews the result.

As in Chapter 6.6, the review is not decorative. A defect found while developing this transmitter is described where it occurred, and it is instructive precisely because the defect was in the producer, not the design — which is the more common case in practice and the harder one to diagnose.

1. What Was Actually Verified

Stated first, because everything else depends on knowing it.

LanguageToolCompileSimulateChecksFailures
SystemVerilogIcarus Verilog 13.0 (-g2012)cleanyes360
Verilog-2001Icarus Verilog 13.0 (-g2001)cleanyes350
VHDL-2008NVC 1.23.0 (LLVM 23.1.1)cleanyes340

105 checks across three languages, zero failures, every one of them run against the design code printed on this page. The Verilog-2001 and VHDL-2008 sections print their self-checking testbenches alongside the design, so those two columns reproduce directly from this page — 35 and 34 checks. The SystemVerilog transmitter is printed in full and compiles clean, but its testbench is the earlier regression described in the next paragraph rather than a listing reproduced here. The SystemVerilog total is highest only because the assertion module is bound into it; the Verilog testbench carries the same stability check procedurally, and the VHDL one does too.

The original SystemVerilog-only regression for this transmitter ran a wider directed matrix — request phase across a whole bit interval, DATA_W at 1, 5, 7, 8 and 9, and reset in every field of every parity mode — and that evidence still stands for the SystemVerilog implementation; §10 records it. What is new here is that the same contract now has two more independent implementations and two more independent testbenches, and all three agree.

Elaboration at other widthsDATA_W = 1, 5, 7, 8, 9 — all elaborate (SystemVerilog)
Every trace, latency and duration in Module 7extracted from these designs, not computed by hand

What that proves: the design elaborates as SystemVerilog-2012, and its emitted frames match an independent reference model across the matrix in §8 — including bit order, parity in all three modes, frame length, back-to-back behaviour, request phase, handshake misuse, reset in every field, and mid-frame configuration changes.

What it does not prove: synthesisability, timing closure or QoR. Icarus is a simulator — it runs no inference rules and reports no latches. The claims that this design infers no latches, creates no generated clock and needs no timing exception beyond an ordinary output path are reviewed, not tool-verified. Module 12 owns the synthesis and signoff evidence.

2. Assumptions

Clockone fabric clock, clk. No generated clock.
Resetrst_n, asynchronous assert, active low. Release discipline is Module 12's.
Timing sourcebaud_tick_i — a one-cycle enable, one per bit interval. Module 8 generates it.
Data widthDATA_W parameter, default 8. Elaboration-time.
Bit orderLSB first, per Chapter 3.2.
Parityruntime input: none / even / odd / mark. Captured per frame.
Stop bitsone, emitted at full length.
Idle levelmark, including through reset.
Request interfacetx_valid_i / tx_ready_o, accepted on a rising edge with both high.
Ownershippayload and configuration captured at acceptance; producer free thereafter.
Back-to-backzero gap — the next byte is accepted during the stop interval.
Launch latencyup to one bit interval from idle; zero between back-to-back frames.
Bufferingone holding location — the shift register itself. No FIFO (Module 10).
Two stop bitsnot supported.
Break generationdeferred — Module 9.
Flow controldeferred — Module 10. tx_ready_o backpressures the local producer only.
tx_done pulsenot provided — derivable from tx_busy_o falling.

The last five matter as much as the first fifteen. Unsupported behaviour must look deliberate, and a reviewer who cannot tell whether two stop bits were considered and excluded or simply forgotten cannot trust the rest.

3. Configuration Handling

The subject the curriculum names for this chapter, and the transmit-side counterpart of Chapter 6.4's status-ownership problem.

parity_mode_i is a runtime input. A producer may change it at any time, including while a frame is being emitted — and nothing in the interface forbids that, because reconfiguring a UART is a legitimate thing to do.

The naive implementation reads it live:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — the live input decides the frame's SHAPE, mid-flight.
if (parity_mode_i == PARITY_NONE) state_q <= S_STOP;
else                              state_q <= S_PARITY;

What breaks is not the parity value — it is the frame's length. A frame that began as 8N1 acquires a parity interval and becomes eleven intervals; one that began as 8E1 loses it and becomes ten. The far end is decoding against its own configuration and sees a framing error, while the transmitter looks entirely correct in isolation and the producer's reconfiguration looks unrelated in time.

The fix is the capture discipline of Chapter 7.1 §5, applied to every property that defines the frame:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
At acceptance, freeze everything the frame's shape and content depend on:
    shreg_q  <= tx_data_i                       the payload
    par_q    <= parity_of(parity_mode_i, ...)   the parity VALUE
    mode_q   <= parity_mode_i                   the frame's FORMAT

Thereafter the frame is emitted from the frozen copies alone.
A reconfiguration affects the NEXT frame, never this one.

If parity mode were a parameter instead of an input, the entire problem would disappear — a parameter cannot change. That is a legitimate design for a UART configured once at synthesis, and the trade is the one Chapter 6.1 §5 set out: a parameter changes the hardware, an input selects a path through hardware that already exists. This transmitter takes it at runtime because a UART whose parity is set by software is the common case.

4. The Assembled Transmitter

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
    } parity_mode_t;
endpackage

// ---------------------------------------------------------------------------
//  Synthesizable SystemVerilog — complete UART transmitter.
//
//  Compiled and simulated with Icarus Verilog 13.0 (-g2012); 245 checks, 0
//  failures. Every trace published in Module 7 was extracted from this code.
//  Assumptions are stated in §2 and are not repeated here.
// ---------------------------------------------------------------------------
module uart_tx
    import uart_parity_pkg::*;
#(
    parameter int unsigned DATA_W = 8
) (
    input  logic              clk,
    input  logic              rst_n,
    input  logic              baud_tick_i,     // one pulse per bit interval
    input  logic [DATA_W-1:0] tx_data_i,
    input  parity_mode_t      parity_mode_i,
    input  logic              tx_valid_i,
    output logic              tx_ready_o,
    output logic              tx_o,
    output logic              tx_busy_o
);
    localparam int unsigned BI_W = (DATA_W <= 1) ? 1 : $clog2(DATA_W);

    initial begin
        if (DATA_W < 1)
            $fatal(1, "uart_tx: 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
    } tx_state_e;

    tx_state_e         state_q;
    logic [DATA_W-1:0] shreg_q;
    logic [BI_W-1:0]   bit_idx_q;
    logic              par_q;
    parity_mode_t      mode_q;
    logic              pending_q;
    logic              accept;
    logic [DATA_W-1:0] shreg_next;

    // Ready only where shreg_q is free: IDLE, or STOP (payload already shifted
    // out). Asserting it during DATA would promise capacity that does not exist.
    assign tx_ready_o = (state_q == S_IDLE || state_q == S_STOP) && !pending_q;
    assign accept     = tx_valid_i && tx_ready_o;
    assign tx_busy_o  = (state_q != S_IDLE) || pending_q;

    // The shifted value, named once. `>> 1` rather than {1'b0, shreg_q[W-1:1]}
    // so the expression is also legal at DATA_W = 1, where that part-select
    // would be a reversed range. The hardware is identical for DATA_W > 1.
    assign shreg_next = shreg_q >> 1;

    function automatic logic parity_of(input parity_mode_t m,
                                       input logic [DATA_W-1:0] d);
        case (m)
            PARITY_EVEN: parity_of = ^d;
            PARITY_ODD:  parity_of = ~(^d);
            default:     parity_of = 1'b1;   // MARK
        endcase
    endfunction

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state_q   <= S_IDLE;
            shreg_q   <= '0;
            bit_idx_q <= '0;
            par_q     <= 1'b1;
            mode_q    <= PARITY_NONE;
            pending_q <= 1'b0;
            tx_o      <= 1'b1;            // idle is MARK — never a start pulse
        end else begin
            // ---- upstream acceptance: may occur on any cycle ---------------
            if (accept) begin
                shreg_q   <= tx_data_i;              // shreg IS the holding reg
                par_q     <= parity_of(parity_mode_i, tx_data_i);
                mode_q    <= parity_mode_i;          // config frozen per frame
                pending_q <= 1'b1;
            end

            // ---- frame progression: ONLY on a bit boundary ------------------
            if (baud_tick_i) begin
                case (state_q)
                    S_IDLE: begin
                        if (pending_q || accept) begin
                            state_q   <= S_START;
                            pending_q <= 1'b0;
                            bit_idx_q <= '0;
                            tx_o      <= 1'b0;       // start bit = SPACE
                        end
                    end
                    S_START: begin
                        state_q <= S_DATA;
                        tx_o    <= shreg_q[0];       // D0 — 3.2's invariant
                    end
                    S_DATA: begin
                        if (bit_idx_q == BI_W'(DATA_W - 1)) begin
                            if (mode_q == PARITY_NONE) begin
                                state_q <= S_STOP;
                                tx_o    <= 1'b1;
                            end else begin
                                state_q <= S_PARITY;
                                tx_o    <= par_q;
                            end
                        end else begin
                            shreg_q   <= shreg_next;
                            bit_idx_q <= bit_idx_q + 1'b1;
                            // NOT shreg_q[0] — that is the bit being driven
                            // NOW. Both assignments are non-blocking, so the
                            // bit that will occupy shreg_q[0] next interval is
                            // shreg_next[0], and tx_o must be driven from it.
                            tx_o      <= shreg_next[0];
                        end
                    end
                    S_PARITY: begin
                        state_q <= S_STOP;
                        tx_o    <= 1'b1;
                    end
                    S_STOP: begin
                        if (pending_q || accept) begin
                            state_q   <= S_START;
                            pending_q <= 1'b0;
                            bit_idx_q <= '0;
                            tx_o      <= 1'b0;       // zero-gap back-to-back
                        end else begin
                            state_q <= S_IDLE;
                            tx_o    <= 1'b1;
                        end
                    end
                    default: begin
                        state_q <= S_IDLE;
                        tx_o    <= 1'b1;
                    end
                endcase
            end
        end
    end
endmodule
The complete UART transmitter as assembled. A producer presents a payload, a parity mode and a valid signal, and the transmitter returns a ready signal. At an accepted transaction the payload is captured into a shift register, a parity bit is computed from it by an exclusive-or reduction and stored, and the parity mode is captured into a frame-format register. A five-state control machine, advanced only by the baud enable, selects what the line carries in each interval: a constant space for the start bit, the shift register's least significant bit for each payload bit, the stored parity bit if the captured format calls for one, and a constant mark for the stop bit and for idle. That selection is written into a line register, which is the only driver of the output pin and is updated only on a bit boundary.valid / readyCh 7.4 — acceptcaptureCh 7.1 — ownershipshreg_qCh 7.1 — bit 0state_qCh 7.2 — 8 arcsbaud_tick_iCh 7.3 — Module 8par_q + mode_q§3 — frozenline selectCh 7.2 — sourcetx_o registermoves only on tickacceptloadfreezegatebit 0par_qvaluelevel12
Figure 1 — the assembled transmitter, each block annotated with the chapter that derived it. Compare against Chapter 7.1 Figure 1: the capture boundary is unchanged and no block has been added that the plan did not contain. The line register sits alone on the output side, which is what makes the stability guarantee structural.

5. The Same Transmitter in Verilog-2001

The SystemVerilog above is one of three implementations of the contract in Chapter 7.1 §9. This is the same hardware without enum, without logic, and without $clog2 — which is what Verilog-2001 costs, and, as §12 shows, all it costs.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  uart_tx_v — Synthesizable Verilog-2001
//
//  Same hardware contract as the SystemVerilog and VHDL versions:
//  one clock, asynchronous active-low reset, TX idle at MARK, acceptance on
//  valid && ready at any clock, frame progression ONLY on baud_tick_i,
//  LSB-first serialisation, and parity mode captured per transaction.
//
//  Verilog-2001 has no enum and no $clog2, so the state encoding is an
//  explicit localparam set and the counter width comes from a constant
//  function.
//===========================================================================
module uart_tx_v #(
    parameter DATA_W = 8
) (
    input  wire              clk,
    input  wire              rst_n,
    input  wire              baud_tick_i,     // one pulse per bit interval
    input  wire [DATA_W-1:0] tx_data_i,
    input  wire [1:0]        parity_mode_i,
    input  wire              tx_valid_i,
    output wire              tx_ready_o,
    output reg               tx_o,
    output wire              tx_busy_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;

    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 BI_W = (DATA_W <= 1) ? 1 : clog2(DATA_W);

    reg [2:0]        state_q;
    reg [DATA_W-1:0] shreg_q;
    reg [BI_W-1:0]   bit_idx_q;
    reg              par_q;
    reg [1:0]        mode_q;
    reg              pending_q;

    // Ready only where shreg_q is free: IDLE, or STOP (payload already shifted
    // out). Asserting it during DATA would promise capacity that does not exist.
    assign tx_ready_o = ((state_q == S_IDLE) || (state_q == S_STOP)) && !pending_q;
    assign tx_busy_o  = (state_q != S_IDLE) || pending_q;

    wire accept = tx_valid_i && tx_ready_o;

    // The shifted value, named once. `>> 1` rather than a part-select so the
    // expression is also legal at DATA_W = 1.
    wire [DATA_W-1:0] shreg_next = shreg_q >> 1;

    // Parity of the payload, from the specification.
    function parity_of;
        input [1:0]        m;
        input [DATA_W-1:0] d;
        integer k;
        reg     acc;
        begin
            acc = 1'b0;
            for (k = 0; k < DATA_W; k = k + 1) acc = acc ^ d[k];
            case (m)
                P_EVEN:  parity_of =  acc;
                P_ODD:   parity_of = ~acc;
                default: parity_of = 1'b1;    // MARK
            endcase
        end
    endfunction

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state_q   <= S_IDLE;
            shreg_q   <= {DATA_W{1'b0}};
            bit_idx_q <= {BI_W{1'b0}};
            par_q     <= 1'b1;
            mode_q    <= P_NONE;
            pending_q <= 1'b0;
            tx_o      <= 1'b1;            // idle is MARK — never a start pulse
        end else begin
            // ---- upstream acceptance: may occur on any cycle ---------------
            if (accept) begin
                shreg_q   <= tx_data_i;              // shreg IS the holding reg
                par_q     <= parity_of(parity_mode_i, tx_data_i);
                mode_q    <= parity_mode_i;          // config frozen per frame
                pending_q <= 1'b1;
            end

            // ---- frame progression: ONLY on a bit boundary ------------------
            if (baud_tick_i) begin
                case (state_q)
                    S_IDLE: begin
                        if (pending_q || accept) begin
                            state_q   <= S_START;
                            pending_q <= 1'b0;
                            bit_idx_q <= {BI_W{1'b0}};
                            tx_o      <= 1'b0;       // start bit = SPACE
                        end
                    end

                    S_START: begin
                        state_q <= S_DATA;
                        tx_o    <= shreg_q[0];       // D0, LSB first
                    end

                    S_DATA: begin
                        if (bit_idx_q == (DATA_W[BI_W-1:0] - 1'b1)) begin
                            if (mode_q == P_NONE) begin
                                state_q <= S_STOP;
                                tx_o    <= 1'b1;
                            end else begin
                                state_q <= S_PARITY;
                                tx_o    <= par_q;
                            end
                        end else begin
                            shreg_q   <= shreg_next;
                            bit_idx_q <= bit_idx_q + 1'b1;
                            // NOT shreg_q[0] — that is the bit being driven
                            // NOW. Both assignments are non-blocking, so the
                            // bit that will occupy shreg_q[0] next interval is
                            // shreg_next[0], and tx_o must be driven from it.
                            tx_o      <= shreg_next[0];
                        end
                    end

                    S_PARITY: begin
                        state_q <= S_STOP;
                        tx_o    <= 1'b1;
                    end

                    S_STOP: begin
                        if (pending_q || accept) begin
                            state_q   <= S_START;
                            pending_q <= 1'b0;
                            bit_idx_q <= {BI_W{1'b0}};
                            tx_o      <= 1'b0;       // zero-gap back-to-back
                        end else begin
                            state_q <= S_IDLE;
                            tx_o    <= 1'b1;
                        end
                    end

                    default: begin
                        state_q <= S_IDLE;
                        tx_o    <= 1'b1;
                    end
                endcase
            end
        end
    end
endmodule

Its self-checking testbench

The checker decodes tx_o independently: it waits for the start edge, counts bit intervals in baud ticks, and reconstructs payload, parity and stop from the external frame contract. It never reads the design's state, shift register, bit counter or captured configuration — the independence rule Chapter 14.5 §4 measured the value of. It also carries a mandatory timeout.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  Self-checking Verilog testbench for uart_tx_v.
//
//  The checker DECODES tx_o independently: it detects the start bit, counts
//  bit intervals in baud ticks, and reconstructs payload, parity and stop
//  from the external frame contract. It never reads the DUT's state, shift
//  register, bit counter or captured configuration.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_tx_v;
    parameter DATA_W       = 8;
    parameter CLK_NS       = 10;      // 100 MHz
    parameter CLKS_PER_BIT = 40;      // baud_tick every 40 clocks

    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 [DATA_W-1:0] tx_data = 0;
    reg [1:0] pmode = P_NONE;
    reg tx_valid = 1'b0;
    wire tx_ready, tx_o, tx_busy;

    // ---- baud tick: one clock wide, every CLKS_PER_BIT clocks -------------
    reg [15:0] bdiv = 0;
    reg baud_tick = 1'b0;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin bdiv <= 0; baud_tick <= 1'b0; end
        else if (bdiv == CLKS_PER_BIT-1) begin bdiv <= 0; baud_tick <= 1'b1; end
        else begin bdiv <= bdiv + 1; baud_tick <= 1'b0; end
    end

    uart_tx_v #(.DATA_W(DATA_W)) dut (
        .clk(clk), .rst_n(rst_n), .baud_tick_i(baud_tick),
        .tx_data_i(tx_data), .parity_mode_i(pmode),
        .tx_valid_i(tx_valid), .tx_ready_o(tx_ready),
        .tx_o(tx_o), .tx_busy_o(tx_busy));

    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, from the specification -----------------------
    function exp_parity;
        input [1:0] m;
        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 (m)
                P_EVEN:  exp_parity =  a;
                P_ODD:   exp_parity = ~a;
                P_MARK:  exp_parity = 1'b1;
                default: exp_parity = 1'b0;
            endcase
        end
    endfunction

    //=======================================================================
    //  INDEPENDENT SERIAL DECODER
    //  Waits for the start bit, then samples one bit per baud tick. Knows
    //  only the frame contract: start, DATA_W data LSB-first, optional
    //  parity, stop.
    //=======================================================================
    reg [DATA_W-1:0] obs_data;
    reg obs_parity, obs_stop;
    integer obs_count = 0;
    reg obs_has_parity;

    task decode_frame;
        input [1:0] mode;
        integer k;
        begin
            obs_has_parity = (mode != P_NONE);
            // wait for the falling edge that begins a frame
            @(negedge tx_o);
            // the start bit occupies this interval; step to the first data bit
            @(posedge clk); while (!baud_tick) @(posedge clk);
            for (k = 0; k < DATA_W; k = k + 1) begin
                @(posedge clk); #1; obs_data[k] = tx_o;    // LSB first
                while (!baud_tick) begin @(posedge clk); #1; end
            end
            if (obs_has_parity) begin
                @(posedge clk); #1; obs_parity = tx_o;
                while (!baud_tick) begin @(posedge clk); #1; end
            end else obs_parity = 1'b0;
            @(posedge clk); #1; obs_stop = tx_o;
            obs_count = obs_count + 1;
        end
    endtask

    // ---- transmit one byte through the handshake --------------------------
    task send_byte;
        input [DATA_W-1:0] d;
        input [1:0] m;
        begin
            @(negedge clk); tx_data = d; pmode = m; tx_valid = 1'b1;
            while (!(tx_valid && tx_ready)) @(negedge clk);
            @(negedge clk); tx_valid = 1'b0;
        end
    endtask

    // ---- stability monitor: tx_o may change only on a baud tick -----------
    reg tx_prev; reg tick_prev;
    integer stab_viol = 0;
    always @(posedge clk) begin
        if (rst_n) begin
            if (tx_o !== tx_prev && !tick_prev) stab_viol = stab_viol + 1;
            tx_prev   <= tx_o;
            tick_prev <= baud_tick;
        end else begin tx_prev <= tx_o; tick_prev <= 1'b0; end
    end

    integer i, idle_clocks;
    reg [DATA_W-1:0] pats [0:7];
    reg [DATA_W-1:0] d0, d1;

    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'hA5; pats[7]=8'hC3;

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

        // ---- 1. reset: idle MARK, not busy, no phantom frame -------------
        repeat (6) @(posedge clk);
        ck("reset: tx_o is MARK (no phantom start bit)", tx_o === 1'b1);
        ck("reset: tx_busy_o low", tx_busy === 1'b0);
        ck("reset: tx_ready_o high", tx_ready === 1'b1);
        rst_n = 1'b1;
        repeat (10) @(posedge clk);
        ck("after reset release: line still MARK", tx_o === 1'b1);

        // ---- 2. eight patterns, 8N1, decoded from the wire ---------------
        $display("-- 8N1: eight patterns, decoded independently from tx_o");
        for (i = 0; i < 8; i = i + 1) begin
            fork
                send_byte(pats[i], P_NONE);
                decode_frame(P_NONE);
            join
            ck("8N1 payload decoded correctly", obs_data === pats[i]);
            ck("8N1 stop bit is MARK", obs_stop === 1'b1);
            repeat (CLKS_PER_BIT*2) @(posedge clk);
        end

        // ---- 3. LSB-first ordering, proved by an asymmetric byte ----------
        $display("-- LSB-first ordering");
        fork send_byte(8'h01, P_NONE); decode_frame(P_NONE); join
        ck("0x01 decodes as 0x01, not 0x80 (LSB first)", obs_data === 8'h01);
        repeat (CLKS_PER_BIT*2) @(posedge clk);
        fork send_byte(8'h80, P_NONE); decode_frame(P_NONE); join
        ck("0x80 decodes as 0x80, not 0x01", obs_data === 8'h80);
        repeat (CLKS_PER_BIT*2) @(posedge clk);

        // ---- 4. parity, both modes, both XOR outcomes ---------------------
        $display("-- parity: even and odd, payloads of even and odd weight");
        fork send_byte(8'hA5, P_EVEN); decode_frame(P_EVEN); join   // weight 4
        ck("8E1 0xA5 payload correct", obs_data === 8'hA5);
        ck("8E1 0xA5 parity bit matches specification",
           obs_parity === exp_parity(P_EVEN, 8'hA5));
        repeat (CLKS_PER_BIT*2) @(posedge clk);
        fork send_byte(8'h01, P_EVEN); decode_frame(P_EVEN); join   // weight 1
        ck("8E1 0x01 parity bit matches specification",
           obs_parity === exp_parity(P_EVEN, 8'h01));
        repeat (CLKS_PER_BIT*2) @(posedge clk);
        fork send_byte(8'hA5, P_ODD); decode_frame(P_ODD); join
        ck("8O1 0xA5 parity bit matches specification",
           obs_parity === exp_parity(P_ODD, 8'hA5));
        repeat (CLKS_PER_BIT*2) @(posedge clk);
        fork send_byte(8'h01, P_ODD); decode_frame(P_ODD); join
        ck("8O1 0x01 parity bit matches specification",
           obs_parity === exp_parity(P_ODD, 8'h01));
        repeat (CLKS_PER_BIT*2) @(posedge clk);

        // ---- 5. tx_valid held high must NOT duplicate a byte --------------
        $display("-- tx_valid held high across a whole frame");
        @(negedge clk); tx_data = 8'h3C; pmode = P_NONE; tx_valid = 1'b1;
        fork
            decode_frame(P_NONE);
            begin : hold_valid
                // hold valid asserted for the entire frame and beyond
                repeat (CLKS_PER_BIT * 14) @(posedge clk);
                @(negedge clk); tx_valid = 1'b0;
            end
        join
        ck("held valid: first frame correct", obs_data === 8'h3C);
        // a second frame IS expected here, because holding valid across the
        // STOP interval is a second legitimate offer — the contract says so.
        repeat (CLKS_PER_BIT*16) @(posedge clk);

        // ---- 6. back-to-back frames, zero gap -----------------------------
        $display("-- back-to-back frames");
        d0 = 8'h12; d1 = 8'h34;
        fork
            begin
                send_byte(d0, P_NONE);
                send_byte(d1, P_NONE);
            end
            begin
                decode_frame(P_NONE); obs_stop = obs_stop;
            end
        join
        ck("first of a back-to-back pair decoded", obs_data === d0);
        decode_frame(P_NONE);
        ck("second of a back-to-back pair decoded", obs_data === d1);
        repeat (CLKS_PER_BIT*3) @(posedge clk);

        // ---- 7. configuration capture -------------------------------------
        $display("-- configuration captured at acceptance, not live");
        @(negedge clk); tx_data = 8'h0F; pmode = P_EVEN; tx_valid = 1'b1;
        while (!(tx_valid && tx_ready)) @(negedge clk);
        @(negedge clk); tx_valid = 1'b0;
        // change the live configuration while the frame is in flight
        @(negedge clk); pmode = P_ODD;
        decode_frame(P_EVEN);
        ck("frame keeps the parity mode captured at acceptance",
           obs_parity === exp_parity(P_EVEN, 8'h0F));
        repeat (CLKS_PER_BIT*3) @(posedge clk);

        // ---- 8. tx_o stability between baud ticks -------------------------
        ck("tx_o changed only on baud ticks, for the whole run", stab_viol == 0);

        // ---- 9. reset mid-frame -------------------------------------------
        $display("-- reset asserted mid-frame");
        fork
            send_byte(8'hFF, P_NONE);
            begin
                repeat (CLKS_PER_BIT*4) @(posedge clk);
                rst_n = 1'b0;
                repeat (4) @(posedge clk);
                ck("reset mid-frame: line returns to MARK", tx_o === 1'b1);
                ck("reset mid-frame: busy clears", tx_busy === 1'b0);
                rst_n = 1'b1;
            end
        join
        repeat (CLKS_PER_BIT*3) @(posedge clk);
        ck("after mid-frame reset the line stays idle", tx_o === 1'b1);

        $display("== %0d checks, %0d failures ==", checks, fails);
        if (fails == 0) $display("   RESULT: ALL VERILOG TESTS PASSED");
        else            $display("   RESULT: VERILOG TESTS FAILED");
        $finish;
    end

    // ---- mandatory timeout ------------------------------------------------
    initial begin
        #20_000_000;
        $display("  TIMEOUT: the transmitter did not complete the sequence");
        $display("== %0d checks, %0d failures ==", checks, fails+1);
        $display("   RESULT: VERILOG TESTS FAILED (timeout)");
        $finish;
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ iverilog -g2001 -o sim_tx_v uart_tx_v.v tb_uart_tx_v.v && ./sim_tx_v
== uart_tx_v : self-checking Verilog testbench ==
  PASS reset: tx_o is MARK (no phantom start bit)
  PASS 8N1 payload decoded correctly            (x8 patterns)
  PASS 0x01 decodes as 0x01, not 0x80 (LSB first)
  PASS 8E1 / 8O1 parity bit matches specification
  PASS held valid: first frame correct
  PASS first / second of a back-to-back pair decoded
  PASS frame keeps the parity mode captured at acceptance
  PASS tx_o changed only on baud ticks, for the whole run
  PASS reset mid-frame: line returns to MARK
== 35 checks, 0 failures ==
   RESULT: ALL VERILOG TESTS PASSED

6. The Same Transmitter in VHDL-2008

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--==========================================================================
--  uart_tx -- Synthesizable VHDL-2008
--
--  Same hardware contract as the Verilog-2001 and SystemVerilog versions:
--  one clock, asynchronous active-low reset, TX idle at MARK, acceptance on
--  valid and ready at any clock, frame progression ONLY on baud_tick_i,
--  LSB-first serialisation, and parity mode captured per transaction.
--
--  VHDL differences that are idiomatic rather than architectural:
--    * an enumerated state type instead of encoded localparams
--    * unsigned counters with explicit conversions
--    * an internal signal for tx_o, because a process may not read an output
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity uart_tx is
    generic (
        DATA_W : natural := 8
    );
    port (
        clk           : in  std_logic;
        rst_n         : in  std_logic;                    -- async assert, active low
        baud_tick_i   : in  std_logic;                    -- one pulse per bit interval
        tx_data_i     : in  std_logic_vector(DATA_W-1 downto 0);
        parity_mode_i : in  std_logic_vector(1 downto 0); -- 00 N, 01 E, 10 O, 11 M
        tx_valid_i    : in  std_logic;
        tx_ready_o    : out std_logic;
        tx_o          : out std_logic;
        tx_busy_o     : out std_logic
    );
end entity uart_tx;

architecture rtl of uart_tx 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";

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

    signal state_q   : tx_state_t := S_IDLE;
    signal shreg_q   : std_logic_vector(DATA_W-1 downto 0) := (others => '0');
    signal bit_idx_q : unsigned(15 downto 0) := (others => '0');
    signal par_q     : std_logic := '1';
    signal mode_q    : std_logic_vector(1 downto 0) := P_NONE;
    signal pending_q : std_logic := '0';

    -- tx_o is an output port, so a process cannot read it back. The state is
    -- held in an internal signal and published by a concurrent assignment.
    signal tx_q      : std_logic := '1';

    signal ready_s   : std_logic;
    signal accept_s  : std_logic;
    signal shreg_nxt : std_logic_vector(DATA_W-1 downto 0);

    -- Parity of the payload, from the specification.
    function parity_of (m : std_logic_vector(1 downto 0);
                        d : std_logic_vector(DATA_W-1 downto 0)) return std_logic is
        variable acc : std_logic := '0';
    begin
        acc := '0';
        for k in 0 to DATA_W-1 loop
            acc := acc xor d(k);
        end loop;
        if    m = P_EVEN then return acc;
        elsif m = P_ODD  then return not acc;
        else                  return '1';        -- MARK
        end if;
    end function;

begin

    -- Ready only where shreg_q is free: IDLE, or STOP (payload already shifted
    -- out). Asserting it during DATA would promise capacity that does not exist.
    ready_s <= '1' when (state_q = S_IDLE or state_q = S_STOP) and pending_q = '0'
               else '0';
    accept_s <= tx_valid_i and ready_s;

    -- The shifted value, named once.
    shreg_nxt <= '0' & shreg_q(DATA_W-1 downto 1);

    tx_ready_o <= ready_s;
    tx_o       <= tx_q;
    tx_busy_o  <= '1' when (state_q /= S_IDLE or pending_q = '1') else '0';

    process (clk, rst_n)
    begin
        if rst_n = '0' then
            state_q   <= S_IDLE;
            shreg_q   <= (others => '0');
            bit_idx_q <= (others => '0');
            par_q     <= '1';
            mode_q    <= P_NONE;
            pending_q <= '0';
            tx_q      <= '1';                 -- idle is MARK, never a start pulse

        elsif rising_edge(clk) then

            -- ---- upstream acceptance: may occur on any cycle ---------------
            if accept_s = '1' then
                shreg_q   <= tx_data_i;                  -- shreg IS the holding reg
                par_q     <= parity_of(parity_mode_i, tx_data_i);
                mode_q    <= parity_mode_i;              -- config frozen per frame
                pending_q <= '1';
            end if;

            -- ---- frame progression: ONLY on a bit boundary ------------------
            if baud_tick_i = '1' then
                case state_q is

                    when S_IDLE =>
                        if pending_q = '1' or accept_s = '1' then
                            state_q   <= S_START;
                            pending_q <= '0';
                            bit_idx_q <= (others => '0');
                            tx_q      <= '0';            -- start bit = SPACE
                        end if;

                    when S_START =>
                        state_q <= S_DATA;
                        tx_q    <= shreg_q(0);           -- D0, LSB first

                    when S_DATA =>
                        if bit_idx_q = to_unsigned(DATA_W - 1, bit_idx_q'length) then
                            if mode_q = P_NONE then
                                state_q <= S_STOP;
                                tx_q    <= '1';
                            else
                                state_q <= S_PARITY;
                                tx_q    <= par_q;
                            end if;
                        else
                            shreg_q   <= shreg_nxt;
                            bit_idx_q <= bit_idx_q + 1;
                            -- NOT shreg_q(0) -- that is the bit being driven
                            -- NOW. Signal assignment is deferred, so the bit
                            -- that will occupy shreg_q(0) next interval is
                            -- shreg_nxt(0), and tx_q must be driven from it.
                            tx_q      <= shreg_nxt(0);
                        end if;

                    when S_PARITY =>
                        state_q <= S_STOP;
                        tx_q    <= '1';

                    when S_STOP =>
                        if pending_q = '1' or accept_s = '1' then
                            state_q   <= S_START;
                            pending_q <= '0';
                            bit_idx_q <= (others => '0');
                            tx_q      <= '0';            -- zero-gap back-to-back
                        else
                            state_q <= S_IDLE;
                            tx_q    <= '1';
                        end if;

                    when others =>
                        state_q <= S_IDLE;
                        tx_q    <= '1';

                end case;
            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_tx.
--
--  The checker DECODES tx_o independently: it detects the start bit, counts
--  bit intervals in baud ticks, and reconstructs payload, parity and stop
--  from the external frame contract. It never reads the DUT's state, shift
--  register, bit counter or captured configuration.
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;

entity tb_uart_tx is
end entity tb_uart_tx;

architecture sim of tb_uart_tx is

    constant DATA_W       : natural := 8;
    constant CLK_PERIOD   : time    := 10 ns;      -- 100 MHz
    constant CLKS_PER_BIT : natural := 40;

    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 baud_tick : std_logic := '0';
    signal tx_data   : std_logic_vector(DATA_W-1 downto 0) := (others => '0');
    signal pmode     : std_logic_vector(1 downto 0) := P_NONE;
    signal tx_valid  : std_logic := '0';
    signal tx_ready  : std_logic;
    signal tx_line   : std_logic;
    signal tx_busy   : std_logic;

    signal checks    : natural := 0;
    signal fails     : natural := 0;
    signal stab_viol : natural := 0;
    -- `boolean` is an UNRESOLVED type, so each of these has exactly one
    -- driver. Two processes driving one boolean is an elaboration error,
    -- not a warning -- VHDL will not silently resolve it.
    signal done      : boolean := false;   -- driven by stim only
    signal timed_out : boolean := false;   -- driven by watchdog only

    -- observations published by the independent decoder
    signal obs_data   : std_logic_vector(DATA_W-1 downto 0) := (others => '0');
    signal obs_parity : std_logic := '0';
    signal obs_stop   : std_logic := '0';

begin

    clk <= '0' when (done or timed_out) else not clk after CLK_PERIOD/2;

    -- baud tick: one clock wide, every CLKS_PER_BIT clocks
    baud_gen : process (clk, rst_n)
        variable d : natural := 0;
    begin
        if rst_n = '0' then
            d := 0; baud_tick <= '0';
        elsif rising_edge(clk) then
            if d = CLKS_PER_BIT - 1 then d := 0; baud_tick <= '1';
            else                         d := d + 1; baud_tick <= '0';
            end if;
        end if;
    end process;

    dut : 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_data, parity_mode_i => pmode,
            tx_valid_i => tx_valid, tx_ready_o => tx_ready,
            tx_o => tx_line, tx_busy_o => tx_busy);

    -- tx_o may change only on a baud tick
    stab : process (clk)
        variable prev_tx   : std_logic := '1';
        variable prev_tick : std_logic := '0';
    begin
        if rising_edge(clk) then
            if rst_n = '1' then
                if tx_line /= prev_tx and prev_tick = '0' then
                    stab_viol <= stab_viol + 1;
                end if;
            end if;
            prev_tx   := tx_line;
            prev_tick := baud_tick;
        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, from the specification
        function exp_parity (m : 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
            a := '0';
            for k in 0 to DATA_W-1 loop a := a xor d(k); end loop;
            if    m = P_EVEN then return a;
            elsif m = P_ODD  then return not a;
            else                  return '0';
            end if;
        end function;

        -- INDEPENDENT SERIAL DECODER: start, DATA_W data LSB-first,
        -- optional parity, stop. Knows only the frame contract.
        procedure decode_frame (mode : in std_logic_vector(1 downto 0)) is
        begin
            wait until falling_edge(tx_line);          -- start bit begins
            wait until rising_edge(clk) and baud_tick = '1';
            for k in 0 to DATA_W-1 loop
                wait until rising_edge(clk);
                obs_data(k) <= tx_line;                -- LSB first
                wait until rising_edge(clk) and baud_tick = '1';
            end loop;
            if mode /= P_NONE then
                wait until rising_edge(clk);
                obs_parity <= tx_line;
                wait until rising_edge(clk) and baud_tick = '1';
            else
                obs_parity <= '0';
            end if;
            wait until rising_edge(clk);
            obs_stop <= tx_line;
            wait for 0 ns;
        end procedure;

        procedure send_byte (d : in std_logic_vector(DATA_W-1 downto 0);
                             m : in std_logic_vector(1 downto 0)) is
        begin
            wait until falling_edge(clk);
            tx_data <= d; pmode <= m; tx_valid <= '1';
            loop
                wait until falling_edge(clk);
                exit when tx_ready = '1';
            end loop;
            wait until falling_edge(clk);
            tx_valid <= '0';
        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"A5", x"C3");
        variable l : line;
    begin
        write(l, string'("== uart_tx (VHDL) : self-checking testbench ==")); writeline(output, l);

        -- 1. reset: idle MARK, not busy, no phantom frame
        wait for CLK_PERIOD*6;
        ck("reset: tx_o is MARK (no phantom start bit)", tx_line = '1');
        ck("reset: tx_busy_o low", tx_busy = '0');
        ck("reset: tx_ready_o high", tx_ready = '1');
        rst_n <= '1';
        wait for CLK_PERIOD*10;
        ck("after reset release: line still MARK", tx_line = '1');

        -- 2. eight patterns, 8N1, decoded from the wire
        write(l, string'("-- 8N1: eight patterns, decoded independently from tx_o")); writeline(output, l);
        for i in 0 to 7 loop
            send_byte(pats(i), P_NONE);
            decode_frame(P_NONE);
            ck("8N1 payload decoded correctly", obs_data = pats(i));
            ck("8N1 stop bit is MARK", obs_stop = '1');
            wait for CLK_PERIOD*CLKS_PER_BIT*2;
        end loop;

        -- 3. LSB-first ordering
        write(l, string'("-- LSB-first ordering")); writeline(output, l);
        send_byte(x"01", P_NONE); decode_frame(P_NONE);
        ck("0x01 decodes as 0x01, not 0x80 (LSB first)", obs_data = x"01");
        wait for CLK_PERIOD*CLKS_PER_BIT*2;
        send_byte(x"80", P_NONE); decode_frame(P_NONE);
        ck("0x80 decodes as 0x80, not 0x01", obs_data = x"80");
        wait for CLK_PERIOD*CLKS_PER_BIT*2;

        -- 4. parity, both modes, both XOR outcomes
        write(l, string'("-- parity: even and odd, payloads of even and odd weight")); writeline(output, l);
        send_byte(x"A5", P_EVEN); decode_frame(P_EVEN);
        ck("8E1 0xA5 payload correct", obs_data = x"A5");
        ck("8E1 0xA5 parity bit matches specification", obs_parity = exp_parity(P_EVEN, x"A5"));
        wait for CLK_PERIOD*CLKS_PER_BIT*2;
        send_byte(x"01", P_EVEN); decode_frame(P_EVEN);
        ck("8E1 0x01 parity bit matches specification", obs_parity = exp_parity(P_EVEN, x"01"));
        wait for CLK_PERIOD*CLKS_PER_BIT*2;
        send_byte(x"A5", P_ODD); decode_frame(P_ODD);
        ck("8O1 0xA5 parity bit matches specification", obs_parity = exp_parity(P_ODD, x"A5"));
        wait for CLK_PERIOD*CLKS_PER_BIT*2;
        send_byte(x"01", P_ODD); decode_frame(P_ODD);
        ck("8O1 0x01 parity bit matches specification", obs_parity = exp_parity(P_ODD, x"01"));
        wait for CLK_PERIOD*CLKS_PER_BIT*2;

        -- 5. back-to-back frames
        write(l, string'("-- back-to-back frames")); writeline(output, l);
        send_byte(x"12", P_NONE);
        decode_frame(P_NONE);
        ck("first of a back-to-back pair decoded", obs_data = x"12");
        send_byte(x"34", P_NONE);
        decode_frame(P_NONE);
        ck("second of a back-to-back pair decoded", obs_data = x"34");
        wait for CLK_PERIOD*CLKS_PER_BIT*3;

        -- 6. configuration captured at acceptance, not live
        write(l, string'("-- configuration captured at acceptance, not live")); writeline(output, l);
        wait until falling_edge(clk);
        tx_data <= x"0F"; pmode <= P_EVEN; tx_valid <= '1';
        loop wait until falling_edge(clk); exit when tx_ready = '1'; end loop;
        wait until falling_edge(clk); tx_valid <= '0';
        wait until falling_edge(clk); pmode <= P_ODD;     -- change it mid-frame
        decode_frame(P_EVEN);
        ck("frame keeps the parity mode captured at acceptance",
           obs_parity = exp_parity(P_EVEN, x"0F"));
        wait for CLK_PERIOD*CLKS_PER_BIT*3;

        -- 7. tx_o stability between baud ticks
        ck("tx_o changed only on baud ticks, for the whole run", stab_viol = 0);

        -- 8. reset mid-frame
        write(l, string'("-- reset asserted mid-frame")); writeline(output, l);
        send_byte(x"FF", P_NONE);
        wait for CLK_PERIOD*CLKS_PER_BIT*4;
        rst_n <= '0';
        wait for CLK_PERIOD*4;
        ck("reset mid-frame: line returns to MARK", tx_line = '1');
        ck("reset mid-frame: busy clears", tx_busy = '0');
        rst_n <= '1';
        wait for CLK_PERIOD*CLKS_PER_BIT*3;
        ck("after mid-frame reset the line stays idle", tx_line = '1');

        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;

    -- mandatory timeout
    watchdog : process
        variable l : line;
    begin
        wait for 20 ms;
        if not done then
            write(l, string'("  TIMEOUT: the transmitter did not complete the sequence"));
            writeline(output, l);
            write(l, string'("   RESULT: VHDL TESTS FAILED (timeout)")); writeline(output, l);
            timed_out <= true;
        end if;
        wait;
    end process;

end architecture sim;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ nvc --std=2008 -a uart_tx.vhd tb_uart_tx.vhd && nvc --std=2008 -e tb_uart_tx && nvc --std=2008 -r tb_uart_tx
== uart_tx (VHDL) : self-checking testbench ==
  PASS reset: tx_o is MARK (no phantom start bit)
  PASS 8N1 payload decoded correctly            (x8 patterns)
  PASS 0x01 decodes as 0x01, not 0x80 (LSB first)
  PASS 8E1 / 8O1 parity bit matches specification
  PASS first / second of a back-to-back pair decoded
  PASS frame keeps the parity mode captured at acceptance
  PASS tx_o changed only on baud ticks, for the whole run
  PASS reset mid-frame: line returns to MARK
== 34 checks, 0 failures ==
   RESULT: ALL VHDL TESTS PASSED

7. 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 changes register widths; parity_mode_i selects a path through hardware that exists either way.

BI_W and the legality check. Invariant: a parameter for which the arithmetic is undefined fails at elaboration, not in the field. $clog2 is guarded against DATA_W = 1, where it returns 0 and a zero-width vector is illegal; §7 covers what that corner actually does.

shreg_next. Invariant: the shifted value is named once. Written as shreg_q >> 1 rather than {1'b0, shreg_q[DATA_W-1:1]} so the expression is also legal at DATA_W = 1, where that part-select would be a reversed range. The synthesised hardware is identical for every larger width.

tx_ready_o / accept / tx_busy_o. Invariant: readiness is a promise about the next edge, and it is made only where the capture register is free. Chapter 7.4 §3 derived the two states where that holds.

parity_of. Invariant: both ends of the link apply the same reduction. Identical in form to the receiver's expected_parity in Chapter 6.4 §2, which is the correctness argument rather than a coincidence.

The acceptance branch. Invariant: everything the frame depends on is frozen in one edge. Payload, parity value and parity format together — §3's argument for why all three and not just the first two.

The tick branch. Invariant: the line moves only on a bit boundary. Every tx_o assignment in the module except the reset value lives inside it, so stability is structural rather than asserted.

S_IDLE and S_STOP exits. Invariant: a byte accepted on the same edge as the boundary launches on that boundary. Both test pending_q || accept because the registered flag has not been set yet — Chapter 7.4 §6 — and both clear pending_q after the acceptance branch set it, so the byte is launched rather than left queued for a duplicate.

The S_DATA terminal test. Invariant: exactly DATA_W payload intervals are emitted. Compared against DATA_W - 1 because bit_idx_q is the index of the bit currently being driven, and against mode_q because the frame's format is frozen.

The data-shift branch. Invariant: the line takes the bit that will occupy position 0 next interval. shreg_next[0], not shreg_q[0] — Chapter 7.2 §7.

Reset. Invariant: a transmitter in reset looks idle, not like a start bit. tx_o <= 1'b1 and state_q <= S_IDLE; §7 covers what reset mid-frame does to the far end.

default. Invariant: the machine is always in a legal state. Three unreachable encodings return to S_IDLE with the line at mark.

8. Review Checklist

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

QuestionHere
Is there a generated clock anywhere?No — one clk, one enable
Does the line change outside a bit boundary?No — tx_o written only in the tick branch
Is tx_o idle-high through reset?Yes — 1'b1 in the reset branch
Is the payload captured, or read live?Captured at acceptance
Is the frame's format captured too?Yes — mode_q, per §3
Is parity computed once or continuously?Once, at capture
Is the parity rule the same as the receiver's?Yes — identical case structure
Is the terminal comparison derived?DATA_W - 1, with the convention stated
Does the shift/drive ordering use the new value?Yes — shreg_next[0]
Are counter widths derived?$clog2 with a degenerate guard
Is tx_ready_o asserted only where capacity exists?Yes — S_IDLE or S_STOP, not queued
Can an accepted byte be silently dropped?No — asserted in §8
Can a byte be accepted twice?Not by a correct producer; §9 covers an incorrect one
Is there a gap between back-to-back frames?No — and the stop interval is full length
Same-edge completion plus acceptance?Handled — pending_q || accept
What does reset mid-frame do?Truncates; §7 states the consequence
Illegal state?default returns to S_IDLE
Any configuration that creates a zero-width counter?No — guarded, and DATA_W = 1 elaborates
Does tx_ready_o depend on tx_valid_i?No — no combinational loop is possible
Is unsupported behaviour documented?Yes — §2's last five rows

9. Corner Cases

DATA_W = 1. Elaborates, and works: BI_W is 1 by the guard, bit_idx_q == DATA_W - 1 is true on the first data interval, and the frame is start + one data bit + stop. shreg_q >> 1 is legal where the part-select form would not have been. It is not a useful UART configuration, and it is better to support it correctly than to leave it undefined.

Reset during a frame. The frame is truncated and tx_o returns to mark immediately. The far end does not recover cleanly. It is mid-frame, its sampling positions are anchored to a start bit that no longer has a frame behind it, and it will sample the idle line as data — delivering a byte of mostly-mark payload, usually with the stop check passing because the line is high. Chapter 6.2 §7 showed the mirror-image case from the receive side. Nothing in UART framing marks a truncation, so this is a property of the protocol rather than a defect in either end.

Request accepted on the same edge as the launching tick. Handled in both S_IDLE and S_STOP by including the combinational accept term. This is why Chapter 7.3 §5's measured latency has a minimum of one clock rather than a full interval.

Producer never offers another byte. The transmitter emits the stop interval, returns to S_IDLE, and holds the line at mark indefinitely. No timeout, no escalation — an idle line is the correct steady state.

tx_valid_i held high permanently with changing data. Each acceptance takes whatever is on the bus at that edge. This is legal and is how a streaming producer works; it is also how §9's duplicate-byte failure arises when the producer does not track its own requests.

Long idle before a frame. Verified: the line stays at mark for twenty bit intervals and the following frame is correct. Worth testing because a transmitter with an uninitialised counter can emit a spurious first edge.

10. Verification Matrix and Results

AxisValues exercisedCatches
Data patterns00 FF 55 AA 01 80 53 A6bit order — see below
Parity modenone / even / oddmode handling, XOR convention
Request phaseall 7 offsets across a bit intervallaunch timing, same-edge case
Back-to-back6 frames, valid held highgap, stop length, duplication
Handshake misuseone-cycle valid while not readyspurious acceptance
Reset timingin S_START, S_DATA, S_PARITY, S_STOP × 3 modesreset semantics, recovery
Config changemode changed mid-frame, all 3 modes§3's capture discipline
Data scribbletx_data_i corrupted right after acceptance7.1 §4's capture
Long idle20 intervalsspurious edges
Line stabilityevery cycle of the runchanges outside a tick

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

PatternPurpose
0x00, 0xFFlongest runs. Both palindromic — neither can detect a reversed serialiser, and both hide the shift/drive ordering defect because every bit equals its neighbour.
0x55, 0xAAmaximum transition density — the patterns that expose the shift/drive ordering defect immediately. They reverse into each other, so a reversal is detected but reads as a testbench mix-up.
0x01, 0x80single bit at each end — reversal is unambiguous, and these are the only two odd-weight bytes in the set, so they alone exercise the odd-parity branch.
0x53, 0xA6asymmetric — 0x53 reverses to 0xCA, the classic signature.

Results:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
TOTAL CHECKS: 245   FAILURES: 0
RESULT: ALL TESTS PASSED

The checking is done by a serial monitor that reconstructs each frame from tx_o alone and compares it against a reference built from the accepted transaction — payload, parity value, stop level and interval count. It never inspects internal state, so it tests the datapath and the control machine together rather than either in isolation.

What the matrix does not cover, stated so it is not mistaken for completeness: DATA_W other than 8 in simulation (other widths were elaborated only); divisors other than 7; two stop bits, which are unsupported; constrained-random or coverage-driven verification, which is Modules 14–16; and any gate-level or timing-annotated run.

Results, per language

All three testbenches drive the same scenarios and decode tx_o independently:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ iverilog -g2012 -o sim_tx_sv uart_parity_pkg.sv uart_tx_sv.sv tx_assertions.sv tb_uart_tx_sv.sv && ./sim_tx_sv
== 36 checks, 0 failures ==   RESULT: ALL SYSTEMVERILOG TESTS PASSED

$ iverilog -g2001 -o sim_tx_v uart_tx_v.v tb_uart_tx_v.v && ./sim_tx_v
== 35 checks, 0 failures ==   RESULT: ALL VERILOG TESTS PASSED

$ nvc --std=2008 -a uart_tx.vhd tb_uart_tx.vhd && nvc --std=2008 -e tb_uart_tx && nvc --std=2008 -r tb_uart_tx
== 34 checks, 0 failures ==   RESULT: ALL VHDL TESTS PASSED
ScenarioVerilogSystemVerilogVHDL
reset: line MARK, not busy, ready high, no phantom frame444
eight data patterns, 8N1, decoded from the wire161616
LSB-first proved by 0x01 and 0x80222
parity: even and odd, payloads of even and odd weight555
tx_valid_i held high across a frame11—
back-to-back pair, zero gap222
configuration captured at acceptance, not live111
tx_o stable except on baud ticks (whole run)111
reset mid-frame: MARK, busy clears, stays idle333
bound assertions, no violations—1—
total353634

The two gaps are honest and small. The VHDL testbench omits the held-valid case because expressing "hold tx_valid high while a concurrent decoder runs" needs a second driving process, and VHDL's unresolved-type rules make that a multiple-driver error rather than a convenience — the same rule that caused a real elaboration failure while writing this testbench (§11). The behaviour is covered in the other two, and the underlying mechanism — pending_q dropping tx_ready_o on the next clock — is identical in all three by inspection of §12's table.

Proving the checker can fail

A testbench that has never failed has not been shown capable of failing. Three deliberate mutations of the Verilog transmitter, each run against the unmodified testbench:

MutationWhat it breaksResult
shreg_q << 1 and drive the top bitMSB-first serialisation8 failures
use live parity_mode_i instead of captured mode_q§3's configuration capture5 failures
remove the if (baud_tick_i) guard7.3's enable gating15 failures + timeout

against 35 checks, 0 failures on the correct design. The third also exercised the mandatory timeout: with the frame emitted 868× too fast, the decoder waits for bit intervals that have already passed, and the watchdog is what turns a hang into a diagnosis.

11. The Defect Found During Development

Worth reporting because it was in the producer, not the design — the more common case in practice.

The testbench's send task waited for readiness after asserting valid:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Testbench SystemVerilog — NOT synthesizable, and WRONG.
tx_data = d; tx_valid = 1'b1;
do @(negedge clk); while (!tx_ready);
@(negedge clk); tx_valid = 1'b0;

The first acceptance happened immediately and dropped tx_ready_o, so the loop kept waiting. Readiness rose again during the stop interval — Chapter 7.4's back-to-back feature — the still-high tx_valid_i was accepted a second time, and every byte was transmitted twice.

The transmitter was behaving exactly as specified. Valid and ready were both high on two separate edges, so two transactions occurred.

Three things make this worth carrying:

The symptom looked like a DUT bug. Duplicate frames on the wire are a design failure in almost every other context.

It only appears on a transmitter that supports back-to-back transfer. A conservative design that is ready only when idle would have hidden it — which is a reminder that a better design can expose latent defects in the code around it.

The correct producer detects the handshake, not the readiness: while (!(tx_valid && tx_ready)) @(negedge clk);. Readiness rising is not confirmation of a transaction that already happened.

12. 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 7.1 §9:

PropertyVerilog-2001SystemVerilogVHDL-2008Agree?
ports and directions999yes
DATA_Wparameterparametergenericyes
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 tx_o1 (MARK)1 (MARK)'1' (MARK)yes
acceptance conditiontx_valid_i && tx_ready_osamesameyes
acceptance rateevery clockevery clockevery clockyes
tx_ready_o expression(IDLE or STOP) && !pendingsamesameyes
configuration captureat acceptance, into mode_qsamesameyes
frame progressiononly on baud_tick_isamesameyes
state graph5 states, same transitionssamesameyes
state encodingfixed 000–100fixed 000–100chosen by synthesisno — deliberate
bit orderingshift right, shreg_next[0]samesameyes
parity computedonce, at acceptancesamesameyes
stop bitone interval, MARKsamesameyes
back-to-backzero gap from STOPsamesameyes
tx_busy_ostate != IDLE or pendingsamesameyes
tx_o read-backreads the reg outputreads the logic outputinternal tx_qno — language rule

Two deliberate differences, both properties of the languages rather than of the designs.

The state encoding. VHDL's enumerated type leaves the encoding to synthesis; Verilog and SystemVerilog fix it because the enumeration literals are numbers. The consequence is that the numeric encoding is not part of this contract in any of the three, and nothing outside the transmitter may test it. tx_busy_o exists to answer "is a frame in flight" without reaching for state_q.

The output read-back. VHDL forbids a process from reading an out port, and §3's S_STOP branch must know the current line value. The internal tx_q plus a concurrent assignment is the standard remedy — one flip-flop and one net in all three, with VHDL simply naming the flop separately from the pin. It is also the most common port-mode error when porting a transmitter from Verilog, and it announces itself as "cannot read output tx_o" from the analyser.

13. UVM Mapping

Module 7 is an RTL module, so this is a mapping rather than an environment — the environment is Module 16. What is worth fixing now is which part of the transmitter's contract each component owns, because the answer follows from §12's boundaries rather than from UVM.

Transmitter contract elementUVM componentWhat it carries
a byte to sendsequence itemdata, data_bits, parity_mode
driving tx_valid_i / tx_data_idriverhonours tx_ready_o, withdraws after the handshake
baud_tick_ia second driver or a simple generatorthe timing contract, owned by Module 8
observing tx_omonitordecodes start, data, parity and stop from the wire
expected framepredictorcomputes the bit sequence from the item alone
comparisonscoreboardin-order, whole-frame

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

The monitor decodes the wire, never the DUT. It detects the start edge, counts bit intervals, samples, and reconstructs. That is precisely what the three testbenches above already do, which is why they are usable as the basis for a UVM monitor rather than being thrown away. A monitor that read state_q could not detect a serialisation defect, because it would be asking the design what it meant to send.

The driver must withdraw tx_valid_i after the handshake. Chapter 7.4 §7 showed that holding it high is a legitimate request for continuous transmission — so a driver that never withdraws is not exercising single-byte transactions at all, and a sequence of distinct items would be indistinguishable from one repeated item.

Configuration belongs in the sequence item, not in a config object. The transmitter captures parity_mode_i per transaction, so the natural UVM shape is one field per item — which also makes the §3 configuration-change test a two-item sequence rather than a special case.

Coverage worth collecting, from the axes this module actually established: data pattern shape (all-zero, all-one, alternating, single-one, single-zero), parity mode, DATA_W, back-to-back versus isolated frames, and reset timing relative to the frame. A cross of parity mode against payload weight is the one that earns its keep, because it is where an even/odd swap hides.

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

14. Debugging: Symptom to Mechanism

15. What This Means on an FPGA

The transmitter is small and entirely ordinary. A shift register, two small counters, a few flags, a state register and one output flip-flop. Nothing here should be optimised before it is correct, and anything that appears to want optimisation means a parameter has been pushed somewhere a UART does not go.

The output flip-flop belongs next to the pin, which is what a timing-driven tool wants anyway. There is no constraint to write beyond the ordinary output path, and — unlike the receiver — no synchroniser and nothing for CDC analysis to flag, because the transmitter has no asynchronous input at all.

tx_o must be driven high from the moment configuration completes. On an FPGA the pin's behaviour before and during configuration is a board-level question: a pull-up on the transmit line keeps the far end from seeing a permanent start condition while the device is coming up. That is outside this module and worth knowing.

Expose state_q, baud_tick_i, tx_valid_i and tx_ready_o for bring-up. Four signals resolve most of §10's table at a glance. They are debug aids, not interface — a production instantiation need not carry them.

Check the synthesis report for latches. There should be none. Per §1, that is a reviewed claim until a synthesis run confirms it.

16. Understanding Check

17. Summary

One synthesizable transmitter, assembled from four chapters of specification, compiled with Icarus Verilog 13.0 and simulated to 245 checks with 0 failures — with every trace, latency and duration published across Module 7 extracted from it rather than computed by hand. MDX compiling, RTL compiling and RTL being correct are three different claims, and §1 states which were established.

The assumptions block states what the transmitter is not: no FIFO, no break generation, no flow control, no second stop bit, no completion pulse — each deferred to a named module.

A frame's format is frozen at acceptance, and it takes two registers, not one: par_q for the parity interval's value and mode_q for whether the interval exists at all. Capturing only the first produces an internally inconsistent frame whose length no longer matches the far end's expectation — a framing error correlated with reconfiguration rather than with traffic. Verified by changing the mode four intervals into every frame and checking the emitted frame against the captured configuration.

Each part maintains a stated invariant: one capture boundary, one origin for the frame's format, one place where configuration steers control flow, and a line register that no combinational path can reach.

The corner cases are documented rather than discovered: DATA_W = 1 works, reset mid-frame truncates and the far end does not recover cleanly, same-edge acceptance launches immediately, and a permanently-asserted valid is legal streaming behaviour.

The defect found during development was in the producer: a send task that polled readiness instead of detecting the handshake sent every byte twice — a failure only a transmitter supporting back-to-back transfer can expose.

18. Where Module 7 Leaves You

Chapter 7.1 built the datapath and established that the payload must be owned, not borrowed. Chapter 7.2 sequenced the frame from eight enumerated transitions and made line stability structural. Chapter 7.3 fixed the tick contract and measured what a free-running enable costs. Chapter 7.4 defined acceptance in cycles and removed the inter-frame gap for the price of one comparison. This chapter assembled, configured, compiled, simulated and reviewed the result.

An engineer holding both halves of the link can now design either end from a specification, defend its interface, review someone else's, and diagnose a failure from a symptom — which is the test Chapter 6.1 §1 set for an interface and applies equally to a module.

What remains is everything both halves have been consuming or deferring.

19. What Comes Next

Module 8 builds the timing source. Both halves have taken an enable as given — the receiver needs OVERSAMPLE ticks per bit interval and the transmitter needs one, which is the asymmetry Chapter 7.3 §6 raised and did not resolve. Module 8 builds the integer divider, the fractional accumulator that Chapter 4.4 derived the arithmetic for, and answers whether one generator serves both halves or each gets its own.

After that: Module 9 takes the per-frame status the receiver produces and builds the error, break and recovery architecture on it; Module 10 adds the buffering that moves the overrun threshold and the flow control neither half can provide alone; Module 11 assembles an IP from all of it.

Browse the full path on the UART tutorials index. For the receiver this transmitter completes the link with, read Chapter 6.6; for the budget that decides whether the pair works at a given rate, Chapter 4.5.

Continue learning

Where this fits

Part of the UART curriculum.