UART · Module 6
Complete RX RTL Architecture
One synthesizable receiver assembled from the module's five preceding chapters — assumptions stated first, walked block by block with the invariant each maintains, then reviewed the way a reviewer would, including the defects found during its own development.
Five chapters have specified a receiver completely. Chapter 6.1 partitioned it and settled the timing origin; Chapter 6.2 enumerated eight transitions; Chapter 6.3 derived the counters and stated their semantics; Chapter 6.4 added validation and the publish discipline; Chapter 6.5 chose the output interface and the overrun policy.
This chapter assembles them into one module and then does the thing that separates working RTL from reviewable RTL: it challenges its own design.
That is not a rhetorical device. Two genuine defects were found while developing this receiver — one by reasoning about a path no test covered, one by a numeric check contradicting a published claim — and both are described where they occurred rather than quietly corrected. A design review chapter that presents only successes teaches the wrong thing.
1. What Was Actually Verified
Stated first, because the rest of the chapter depends on knowing it — and because "the RTL is correct" means nothing without saying what checked it.
| HDL compiler available | Icarus Verilog 13.0 (stable) |
| Command | iverilog -g2012 -o sim uart_rx.sv tb_uart_rx.sv |
| Elaboration | clean — no errors, no warnings except that unique qualities are ignored |
| Simulation | 73 checks, 0 failures |
| Every figure, table and trace in Module 6 | extracted from this simulation, not computed by hand |
What that does and does not prove. It proves the design elaborates as SystemVerilog-2012, that its behaviour matches an independent reference model across the cases in §8's matrix, and that every published trace is the design's real behaviour.
It does not prove synthesisability. Icarus is a simulator; it does not run inference rules, it does not report latches, and it does not know whether a construct maps to hardware. The RTL is written to the constraints Chapter 6.2 and Chapter 6.3 set out — single clock, enables not generated clocks, every branch assigning, no combinational loops — and that is a reviewed claim, not a tool-verified one. A synthesis run is the missing evidence, and Module 12 is where it belongs.
2. Assumptions
Before the code, what this receiver is — and is not.
| Clock | one fabric clock, clk. No generated clock anywhere. |
| Reset | rst_n, asynchronous assert, active low. Synchronous-release discipline is Module 12's. |
| RX synchronisation | two stages, reset to the idle level. rx_i fans out to nothing else. |
| Timing source | os_tick_i, a one-cycle enable, OVERSAMPLE per bit interval. Module 8 generates it. |
| Oversampling factor | OVERSAMPLE parameter, default 16, any value ≥ 2. |
| Data width | DATA_W parameter, default 8. Elaboration-time. |
| Bit order | LSB first, per Chapter 3.2. |
| Parity | runtime input: none / even / odd / mark. |
| Stop bits | one, validated at its centre sample. |
| Sampling | single sample at the interval centre. |
| Output interface | held valid with rx_ready_i accept. |
| Error policy | corrupt frames are delivered with their flags. |
| Overrun policy | preserve the held byte, drop the new frame, raise rx_overrun_o. |
| Buffering | one holding register. No FIFO — Module 10. |
| Break handling | deferred — Module 9. |
| Idle detection | deferred — Module 9. See §7's mis-framing case. |
| Sticky status | deferred — Module 9. Flags here are strictly per-frame. |
| Two stop bits | not supported. Only the first stop interval is validated. |
| Majority voting | not implemented. Chapter 5.4 showed it is a trade, not an upgrade. |
The last four matter. Unsupported behaviour must look deliberate, and a receiver whose assumptions block is silent about two stop bits is one where a reviewer cannot tell whether the omission was a decision or an oversight.
3. The Assembled Receiver
// ============================================================================
// Synthesizable SystemVerilog — complete UART receiver.
//
// Compiled and simulated with Icarus Verilog 13.0 (-g2012); 73 checks, 0
// failures. Every trace published in Module 6 was extracted from this code.
//
// Assumptions are stated in §2 and are not repeated here. Chapter references
// point at the derivation of each decision rather than restating it.
// ============================================================================
package uart_parity_pkg;
typedef enum logic [1:0] {
PARITY_NONE = 2'b00,
PARITY_EVEN = 2'b01,
PARITY_ODD = 2'b10,
PARITY_MARK = 2'b11 // constant 1 — no detection, Chapter 3.3
} parity_mode_t;
endpackage
module uart_rx
import uart_parity_pkg::*;
#(
parameter int unsigned DATA_W = 8,
parameter int unsigned OVERSAMPLE = 16
) (
input logic clk,
input logic rst_n,
input logic rx_i, // the only asynchronous input
input logic os_tick_i, // enable, M per bit — Module 8
input parity_mode_t parity_mode_i,
output logic [DATA_W-1:0] rx_data_o,
output logic rx_valid_o,
input logic rx_ready_i,
output logic rx_parity_err_o,
output logic rx_frame_err_o,
output logic rx_overrun_o
);
// ---- derived constants — Chapter 6.3 §2, §5 ----------------------------
localparam int unsigned CENTER = OVERSAMPLE / 2;
localparam int unsigned PH_W = (OVERSAMPLE <= 1) ? 1 : $clog2(OVERSAMPLE);
localparam int unsigned BI_W = (DATA_W <= 1) ? 1 : $clog2(DATA_W);
// ---- parameter legality — §6 of this chapter ---------------------------
initial begin
if (OVERSAMPLE < 2)
$fatal(1, "uart_rx: OVERSAMPLE = %0d has no centre to sample", OVERSAMPLE);
if (DATA_W < 1)
$fatal(1, "uart_rx: DATA_W = %0d is not a frame", DATA_W);
end
typedef enum logic [2:0] {
S_IDLE = 3'd0,
S_START = 3'd1,
S_DATA = 3'd2,
S_PARITY = 3'd3,
S_STOP = 3'd4
} rx_state_e;
rx_state_e state_q;
logic rx_meta_q, rx_sync_q, rx_sync_d_q;
logic [PH_W-1:0] phase_q;
logic [BI_W-1:0] bit_idx_q;
logic [DATA_W-1:0] shreg_q;
logic par_acc_q;
logic par_err_q;
logic start_cand;
logic sample_now;
// ---- input boundary — Chapter 5.1 §3 -----------------------------------
// Reset to the IDLE level, never to zero: a receiver leaving reset must
// not present a fabricated departure from idle to the edge detector.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rx_meta_q <= 1'b1;
rx_sync_q <= 1'b1;
rx_sync_d_q <= 1'b1;
end else begin
rx_meta_q <= rx_i;
rx_sync_q <= rx_meta_q;
rx_sync_d_q <= rx_sync_q;
end
end
// ---- the two named events — Chapters 5.2 §1 and 6.3 §3 -----------------
assign start_cand = rx_sync_d_q && !rx_sync_q;
assign sample_now = os_tick_i && (phase_q == PH_W'(CENTER - 1));
// ---- the receiver's expectation — Chapter 6.4 §2 -----------------------
function automatic logic expected_parity(input parity_mode_t m, input logic acc);
case (m)
PARITY_EVEN: expected_parity = acc;
PARITY_ODD: expected_parity = ~acc;
default: expected_parity = 1'b1; // MARK
endcase
endfunction
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state_q <= S_IDLE;
phase_q <= '0;
bit_idx_q <= '0;
shreg_q <= '0;
par_acc_q <= 1'b0;
par_err_q <= 1'b0;
rx_data_o <= '0;
rx_valid_o <= 1'b0;
rx_parity_err_o <= 1'b0;
rx_frame_err_o <= 1'b0;
rx_overrun_o <= 1'b0;
end else begin
// Accept releases the holding register and ALL of its status
// together — Chapter 6.5 §3. Written first so that a delivery in
// the same cycle overrides it, which is what lets a byte be
// accepted and replaced on one edge.
if (rx_valid_o && rx_ready_i) begin
rx_valid_o <= 1'b0;
rx_parity_err_o <= 1'b0;
rx_frame_err_o <= 1'b0;
rx_overrun_o <= 1'b0;
end
if (state_q == S_IDLE) begin
// The only state that does not wait for the timing event.
if (start_cand) begin
state_q <= S_START;
phase_q <= '0; // THE origin — Chapter 6.1 §4
bit_idx_q <= '0;
par_acc_q <= 1'b0;
par_err_q <= 1'b0; // status is per-frame: clear at entry
end
end else if (os_tick_i) begin
// Explicit wrap. Natural overflow is correct only when
// OVERSAMPLE is a power of two — Chapter 6.3 §2.
phase_q <= (phase_q == PH_W'(OVERSAMPLE - 1)) ? '0 : phase_q + 1'b1;
if (sample_now) begin
case (state_q)
S_START: begin
// The sampled level here is the QUALIFICATION
// result, not payload — Chapter 6.2 §1.
if (rx_sync_q) state_q <= S_IDLE; // false start
else state_q <= S_DATA;
end
S_DATA: begin
// Right shift, MSB insert: LSB-first arrival order
// reconstructed without a write address — 6.3 §6.
shreg_q <= {rx_sync_q, shreg_q[DATA_W-1:1]};
par_acc_q <= par_acc_q ^ rx_sync_q;
bit_idx_q <= bit_idx_q + 1'b1;
// Compared against DATA_W-1: bit_idx_q still holds
// the index of the bit arriving now, and the
// increment above is concurrent — Chapter 6.3 §4.
if (bit_idx_q == BI_W'(DATA_W - 1)) begin
// The ONLY place configuration steers control.
if (parity_mode_i == PARITY_NONE) state_q <= S_STOP;
else state_q <= S_PARITY;
end
end
S_PARITY: begin
// Held INTERNALLY. Writing rx_parity_err_o here
// would let this frame rewrite the status of a
// byte still held from the previous one — the
// defect described in Chapter 6.4 §4.
par_err_q <= (rx_sync_q != expected_parity(parity_mode_i, par_acc_q));
state_q <= S_STOP;
end
S_STOP: begin
// Unconditional. Waiting for the line to return to
// mark wedges the receiver on a break — 6.2 §4.
state_q <= S_IDLE;
if (rx_valid_o && !rx_ready_i) begin
// Preserve the held frame, drop this one, and
// record the loss — Chapter 6.5 §4.
rx_overrun_o <= 1'b1;
end else begin
// THE publish point. Data and both status
// flags cross to the consumer on one edge.
rx_data_o <= shreg_q;
rx_valid_o <= 1'b1;
rx_parity_err_o <= par_err_q;
rx_frame_err_o <= !rx_sync_q;
end
end
default: state_q <= S_IDLE; // recovery — 6.2 §4
endcase
end
end
end
end
endmodule4. The Walkthrough
Each part, and the invariant it maintains.
Parameters and interface. Invariant: the hardware's shape is fixed at elaboration and its behaviour is selected at runtime. DATA_W and OVERSAMPLE change register widths; parity_mode_i selects a path. Chapter 6.1 §5 gives the test that separates them.
Derived constants. Invariant: no position in this design is a literal. CENTER, PH_W and BI_W all follow the parameters, so changing OVERSAMPLE from 16 to 8 moves every sampling position correctly. Chapter 5.3 §6 showed what a hard-coded 7 costs.
Parameter legality. Invariant: a parameter for which the arithmetic is undefined fails at elaboration, not in silicon. Both guards mark genuinely broken configurations rather than merely unusual ones — see §6.
State type. Invariant: the machine's states are named and enumerable. Five values in three bits, with three encodings unreachable and a default that recovers from them.
Synchronisation boundary. Invariant: exactly one representation of the line exists, and rx_i reaches nothing else. Chapter 6.1 §9 explains why two synchronisers would let the receiver hold contradictory beliefs about the wire.
Named events. Invariant: start_cand and sample_now are each produced in exactly one place. Scattering phase_q == 7 through the design is the defect Chapter 6.3 §3 named; one assignment means one thing to change.
Phase counter. Invariant: phase zero is the start candidate, and the sample fires on the tick that carries the count to CENTER. This is Chapter 6.1 §4's single origin, and the reason all eleven samples in Chapter 6.3 §7's trace read phase_q == 7.
Bit counter. Invariant: bit_idx_q is the index of the data bit arriving now, and nothing outside S_DATA reads it. That second clause is what makes the wrap from 7 to 0 harmless at DATA_W = 8.
Shift register. Invariant: after n stores, the first n arrived bits occupy the top n positions, and after DATA_W stores they are correctly placed. Verified by the 00, 80, C0, 60, 30, 98, 4C, A6 trace.
Parity accumulator. Invariant: it holds the XOR of the payload bits stored so far, and nothing else. Cleared at the candidate; the received parity bit never enters it.
Stop validation. Invariant: the framing result describes the stop interval alone. One comparison, at the stop sample, published in the same branch as the data.
Holding register and status. Invariant: data and all three flags describe one frame and move together. The single publish point, and Chapter 6.5 §3's stability property.
Control and recovery. Invariant: the machine is always in a legal state and always leaves every state. No conditional exits except the qualification test; default covers the rest.
5. Review Checklist
The questions a reviewer should ask, with this design's answers. Every no would be a finding.
| Question | Here |
|---|---|
Does raw rx_i fan out anywhere? | No — only into rx_meta_q |
| One clock domain? | Yes — one clk, no generated clock |
| Are timing events clock enables? | Yes — os_tick_i and sample_now |
| What is phase count zero? | The start candidate, stated in 6.3 §1 |
| Is the centre sample derived or magic? | Derived: CENTER - 1 from OVERSAMPLE |
| Is the bit-counter convention documented? | Yes — index of the bit arriving now |
| Is LSB-first reconstruction correct? | Verified against 0xA6 and eight other patterns |
| Is the parity accumulator cleared at the right time? | At the candidate, with par_err_q |
| Does the received parity bit enter the accumulator? | No |
| Does disabled parity skip the state cleanly? | Yes — one branch, one place |
| Is stop validated at the intended sample? | Yes — 10.5 UI in 8E1, verified in simulation |
| Are error flags tied to the correct frame? | Yes — via par_err_q and a single publish point |
Can rx_valid_o be missed? | No — held until accepted |
| What happens if the consumer stalls? | Held byte preserved, new frame dropped, overrun raised |
| Back-to-back frames? | Received at full rate, verified |
| Reset mid-frame? | In-flight byte discarded; see §7 |
| Illegal state? | default returns to S_IDLE |
| Can any configuration create a zero-width counter? | No — both guards, §6 |
| Are parameters checked? | Yes, at elaboration |
| Is any status sticky across frames? | No — deliberately, Module 9 owns that |
6. Parameter Legality, and Not Over-Engineering It
Two checks, and both mark configurations where the arithmetic above is undefined:
if (OVERSAMPLE < 2) $fatal(1, ...); // CENTER = 0, so CENTER-1 underflows
if (DATA_W < 1) $fatal(1, ...); // a zero-width shift registerOVERSAMPLE = 1 gives CENTER = 0, and CENTER - 1 on an unsigned localparam is a very large number — the comparison never matches and the receiver never samples. That is worth a fatal rather than a comment.
What is deliberately not checked is as informative:
Odd OVERSAMPLE. CENTER truncates, placing the sample half a tick before the true centre. That is a real but tiny asymmetry, and Chapter 5.3's standalone module warned about it. Here it is legal.
DATA_W outside 5–9. UART configurations conventionally span 5 to 9 data bits, and this receiver works for any width its parameters can express. Restricting it to the conventional range would reject a valid use of the block for no engineering reason.
Two stop bits. Not a parameter at all — the assumptions block says one, which is more honest than a parameter that silently ignores its second value.
7. Corner Cases
Reset released onto a live line. The receiver returns to S_IDLE correctly and the in-flight byte is discarded — and then it mis-frames on the remainder and delivers a byte that was never sent. Simulation produced 0xFA from an interrupted 0xA6. Nothing malfunctioned: the next mark-to-space transition it saw was a data edge, and nothing in a UART frame identifies itself as a continuation. Chapter 6.2 §7 develops it. A testbench must allow an idle period after a mid-frame reset, and a system resetting a UART during traffic should expect one spurious byte.
Line stuck at space. The receiver validates a stop of 0, reports rx_frame_err_o, returns to S_IDLE, and then — correctly — does nothing, because a candidate needs a mark-to-space transition and there is no mark to leave. It resumes the moment a real frame arrives. This is the behaviour Chapter 6.2 §4's unconditional stop exit exists to produce; the conditional version wedges here permanently.
Consumer never reads. One byte held indefinitely, rx_overrun_o set at the second frame and remaining set. The receiver does not escalate or give up. This is intentional: escalation policy belongs to the system, and Module 9 is where it is built.
Accept and delivery on the same cycle. rx_valid_o && rx_ready_i clears the register set, and the S_STOP branch may write it in the same cycle. The clear is written first in the always_ff, so the delivery's non-blocking assignments take precedence — the byte is accepted and replaced on one edge with no gap and no overrun. Verified by the back-to-back test with a prompt consumer.
Glitch narrower than half a bit. Rejected by qualification: the sample at 0.5 UI finds mark and the machine returns to S_IDLE. Chapter 5.2 established the exact class this rejects — disturbances shorter than the validation delay, and nothing more.
8. Verification Matrix and Results
The receiver-focused matrix, with what each axis is for:
| Axis | Values exercised | Catches |
|---|---|---|
| Data patterns | 00 FF 55 AA 01 80 A5 A6 53 | see below |
| Parity mode | none / even / odd | mode swap, accumulator convention |
| Parity corruption | inverted parity bit, both modes | check polarity, status ownership |
| Stop corruption | stop driven to space | framing detection, wedging |
| False start | glitch of 1/8 UI | qualification |
| Back-to-back | no idle gap | delivery latency, return-to-idle |
| Consumer stall | ready held low across a frame | overrun policy, status ownership |
| Reset timing | asserted mid-S_DATA | reset semantics, mis-framing |
| Clock mismatch | ±1%, ±3%, both signs | sample placement, 5.5's model |
Why these data patterns, since a list without reasons is not a plan:
| Pattern | Purpose |
|---|---|
0x00, 0xFF | longest runs — expose sampling that drifts without transitions to re-anchor on. Both are palindromic, so neither can detect a reversed shift register. |
0x55, 0xAA | maximum transition density — expose setup/hold and synchroniser issues. Reverse into each other. |
0x01, 0x80 | single bit at each end — expose bit-order reversal unambiguously, since 0x01 reverses to 0x80. |
0xA5, 0xA6 | asymmetric, differing in one bit — 0xA6 reverses to 0x65, and the pair differs in parity-relevant weight. |
0x53 | asymmetric and odd-looking — reverses to 0xCA, the classic reversal signature. |
Note the parity consequence from Chapter 6.4 §2: seven of these nine bytes have even weight, so the odd-parity branch is exercised only by 0x01 and 0x80. A pattern set chosen for visual variety rather than by weight leaves half the parity logic untested.
Results, from the run in §1:
TOTAL CHECKS: 73 FAILURES: 0
RESULT: ALL TESTS PASSEDWhat the matrix does not cover, stated so it is not mistaken for completeness: randomised start phase against a free-running clock (the testbench drives on the clock grid); mismatch beyond ±3%; DATA_W other than 8 in simulation, though the arithmetic was checked across 5–9; OVERSAMPLE other than 16 in simulation; and any form of constrained-random or coverage-driven verification, which is Modules 14–16.
9. Debugging: Symptom to Mechanism
10. What This Means on an FPGA
Thirty-five flip-flops at DATA_W = 8, OVERSAMPLE = 16. Nothing here needs optimising, and anything that appears to want it is a sign a parameter has been pushed somewhere a UART does not go.
The synchroniser is the only structure needing a constraint, and it is Module 12's to specify. Its placement is fixed here: adjacent to the pin, with nothing tapping rx_i.
Expose state_q, sample_now, rx_valid_o and rx_ready_i during bring-up — four signals that resolve most of §9's table at a glance. They are debug aids, not interface: a production instantiation need not carry them, and a bring-up build should.
Check the synthesis report for a latch. There should be none; every branch in both always_ff blocks either assigns or holds a register. This is a reviewed claim, per §1 — an actual synthesis run is what would turn it into a verified one.
11. Understanding Check
12. Summary
One synthesizable receiver, assembled from five chapters of specification, compiled with Icarus Verilog 13.0 and simulated to 73 checks with 0 failures — and every trace published across Module 6 extracted from that simulation rather than computed by hand. MDX compiling and SystemVerilog compiling are different facts, and both were established; synthesisability is a reviewed claim awaiting Module 12.
The assumptions block states what the receiver is not: no FIFO, no break handling, no idle detection, no sticky status, no second stop bit, no majority voting — each deferred to a named module, because unsupported behaviour must look deliberate.
Every part maintains a stated invariant, and the walkthrough names them: one representation of the line, one origin, one publish point, one place where configuration steers control flow, and a counter whose value is read only where it is meaningful.
Two parameters and one runtime input, and even that small space has corners — DATA_W = 8 wraps the bit counter, a non-power-of-two OVERSAMPLE exposes the natural-wrap defect. Every parameter multiplies the verification burden, which is why the design has two rather than eight.
The corner cases are documented rather than discovered: mis-framing after reset onto a live line, correct inaction on a line stuck at space, indefinite holding when the consumer never reads, and accept-plus-delivery on one edge.
Two real defects were found during development — status ownership, found by reasoning about a path no nominal test reaches, and a flip-flop count contradicted by computing it. Both are reported where they occurred.
13. Where Module 6 Leaves You
Six chapters have turned Module 5's timing mechanisms into a receiver.
Chapter 6.1 partitioned it, wrote the interface before the implementation, and settled the timing origin that Module 5 deliberately left open. Chapter 6.2 built the control machine as a table of eight transitions and showed which three implementations get wrong. Chapter 6.3 derived both counters from the parameters, stated every semantic before its RTL, and traced a byte through the shift register. Chapter 6.4 added validation and the publish discipline that keeps status attached to the frame it describes. Chapter 6.5 chose the output interface from the fact that a receiver cannot tell the wire to wait. This chapter assembled, compiled, simulated and reviewed the result.
An engineer holding this chain can design a receiver from a specification, derive its counter widths and semantics, defend its interface, review someone else's, and diagnose a failure from a symptom — without memorising the final RTL, which is the test Chapter 6.1 §1 set for the interface and which applies equally to the module.
What remains is the other half of the link, and everything the receiver deferred.
14. What Comes Next
Module 7 builds the transmitter — the simpler half, done properly. It has no start detection, no qualification and no sampling: it executes a schedule it wrote. What it does have is a producer-facing contract that is easy to get subtly wrong, a frame sequencer whose shape must match this receiver's exactly, and a back-to-back behaviour that decides whether the link runs at its rated throughput or at some fraction of it.
After that: Module 8 builds the os_tick both halves have been consuming; Module 9 takes the per-frame status this module produces and builds the error, break and recovery architecture on top of it — including the idle detection that would suppress §7's fabricated byte; Module 10 adds the buffering that moves the overrun threshold; and Modules 11 onward assemble an IP, integrate it, and verify it properly.
Browse the full path on the UART tutorials index. For the sampling mechanisms this receiver assembles, read back to Chapter 5.3; for the budget that decides whether it works at a given rate, Chapter 4.5.
Continue learning
Related tutorials
- Related topic
Why Receiving Is Harder Than Transmitting
A transmitter executes a schedule it wrote itself. A receiver must decide whether something is happening, whether it was real, where the positions are, and what value was there — four judgements from one edge on an input it does not control.
- Related topic
The RX FSM
Five states, eight transitions, and a rule that keeps them enumerable: the next state depends on the current state, the configuration and one timing event — never on a data bit. Specified as a table first, with the RTL derived from it and checked against it.
- Related topic
Data-Valid Generation and the Receiver Handshake
A receiver cannot tell the far end to wait. That one fact decides the shape of the output interface, forces a policy for the frame that arrives while the last is still held, and explains why the answer is not a FIFO.
- Related topic
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.
Where this fits
Part of the UART curriculum.
