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.

HDL compilerIcarus Verilog 13.0 (stable)
Commandiverilog -g2012 -o sim uart_tx.sv tb_uart_tx.sv
Compile exit0
Warningsnone from the design; the testbench draws the usual "$display cannot be synthesized" notes
Simulation245 checks, 0 failures
Elaboration at other widthsDATA_W = 1, 5, 7, 8, 9 — all elaborate
Every trace, latency and duration in Module 7extracted from this design, 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 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.

6. 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?Yesmode_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

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

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

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

10. Debugging: Symptom to Mechanism

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

12. Understanding Check

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

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

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