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 compiler | Icarus Verilog 13.0 (stable) |
| Command | iverilog -g2012 -o sim uart_tx.sv tb_uart_tx.sv |
| Compile exit | 0 |
| Warnings | none from the design; the testbench draws the usual "$display cannot be synthesized" notes |
| Simulation | 245 checks, 0 failures |
| Elaboration at other widths | DATA_W = 1, 5, 7, 8, 9 — all elaborate |
| Every trace, latency and duration in Module 7 | extracted 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
| Clock | one fabric clock, clk. No generated clock. |
| Reset | rst_n, asynchronous assert, active low. Release discipline is Module 12's. |
| Timing source | baud_tick_i — a one-cycle enable, one per bit interval. Module 8 generates it. |
| Data width | DATA_W parameter, default 8. Elaboration-time. |
| Bit order | LSB first, per Chapter 3.2. |
| Parity | runtime input: none / even / odd / mark. Captured per frame. |
| Stop bits | one, emitted at full length. |
| Idle level | mark, including through reset. |
| Request interface | tx_valid_i / tx_ready_o, accepted on a rising edge with both high. |
| Ownership | payload and configuration captured at acceptance; producer free thereafter. |
| Back-to-back | zero gap — the next byte is accepted during the stop interval. |
| Launch latency | up to one bit interval from idle; zero between back-to-back frames. |
| Buffering | one holding location — the shift register itself. No FIFO (Module 10). |
| Two stop bits | not supported. |
| Break generation | deferred — Module 9. |
| Flow control | deferred — Module 10. tx_ready_o backpressures the local producer only. |
tx_done pulse | not 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:
// 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:
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
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
endmodule5. 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.
| Question | Here |
|---|---|
| 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 |
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
| Axis | Values exercised | Catches |
|---|---|---|
| Data patterns | 00 FF 55 AA 01 80 53 A6 | bit order — see below |
| Parity mode | none / even / odd | mode handling, XOR convention |
| Request phase | all 7 offsets across a bit interval | launch timing, same-edge case |
| Back-to-back | 6 frames, valid held high | gap, stop length, duplication |
| Handshake misuse | one-cycle valid while not ready | spurious acceptance |
| Reset timing | in S_START, S_DATA, S_PARITY, S_STOP × 3 modes | reset semantics, recovery |
| Config change | mode changed mid-frame, all 3 modes | §3's capture discipline |
| Data scribble | tx_data_i corrupted right after acceptance | 7.1 §4's capture |
| Long idle | 20 intervals | spurious edges |
| Line stability | every cycle of the run | changes outside a tick |
Why these patterns, since a list without reasons is not a plan:
| Pattern | Purpose |
|---|---|
0x00, 0xFF | longest runs. Both palindromic — neither can detect a reversed serialiser, and both hide the shift/drive ordering defect because every bit equals its neighbour. |
0x55, 0xAA | maximum 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, 0x80 | single 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, 0xA6 | asymmetric — 0x53 reverses to 0xCA, the classic signature. |
Results:
TOTAL CHECKS: 245 FAILURES: 0
RESULT: ALL TESTS PASSEDThe 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:
// 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
Related tutorials
- Related topic
Complete RX RTL Architecture
One synthesizable receiver assembled from the module's five preceding chapters — assumptions stated first, walked block by block with the invariant each maintains, then reviewed the way a reviewer would, including the defects found during its own development.
- Related topic
TX Datapath and Parallel-to-Serial Conversion
The datapath that turns a parallel word into a bit stream — why the transmitter must capture the payload rather than index a live bus, how LSB-first emission falls out of a right shift, and where the frame's fixed bits come from.
- Related topic
The TX FSM and Frame Sequencing
Five states, eight transitions, and one rule that keeps the line honest: tx_o is a register written only inside the bit-boundary branch. Includes the off-by-one that emits nine data bits, and the state table the RTL was checked against.
- Related topic
Baud Enable and TX Bit Timing
What advances the transmitter from one bit interval to the next, stated precisely — the tick's semantics, the measured cost of launching on a free-running enable, and frame durations derived and then confirmed in simulation.
Where this fits
Part of the UART curriculum.
