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.
| Language | Tool | Compile | Simulate | Checks | Failures |
|---|---|---|---|---|---|
| SystemVerilog | Icarus Verilog 13.0 (-g2012) | clean | yes | 45 | 0 |
| Verilog-2001 | Icarus Verilog 13.0 (-g2001) | clean | yes | 44 | 0 |
| VHDL-2008 | NVC 1.23.0 (LLVM 23.1.1) | clean | yes | 44 | 0 |
133 checks across three languages, zero failures, every one of them run against the design code printed on this page.
What you can re-run from this page: §4 and §5 print the Verilog-2001 and VHDL-2008 receivers together with their self-checking testbenches, so those two columns reproduce directly — 44 checks each, which is what they produce here. §3 prints the SystemVerilog receiver in full and it compiles clean under the same tool, but its testbench is the original regression for this receiver and is not reproduced on the page; the 45 is that run, and the extra check over Verilog is the bound assertion module described below.
The SystemVerilog count includes one check that the bound assertion module recorded no violations across the whole run. The Verilog testbench runs the same scenarios without that assertion binding, which is the only reason the two totals differ by one.
Every figure, table and trace in Module 6 was extracted from these simulations rather than computed by hand.
What that does and does not prove. It proves the design elaborates as SystemVerilog-2012, that its behaviour matches an independent reference model across the cases in §8's matrix, and that every published trace is the design's real behaviour.
It does not prove synthesisability. Icarus is a simulator; it does not run inference rules, it does not report latches, and it does not know whether a construct maps to hardware. The RTL is written to the constraints Chapter 6.2 and Chapter 6.3 set out — single clock, enables not generated clocks, every branch assigning, no combinational loops — and that is a reviewed claim, not a tool-verified one. A synthesis run is the missing evidence, and Module 12 is where it belongs.
2. Assumptions
Before the code, what this receiver is — and is not.
| 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); 45 checks, 0
// failures. One of three implementations of the same contract -- see the
// Verilog and VHDL versions in sections 4 and 5, and the equivalence table
// in section 11. Every trace published in Module 6 came from these runs.
//
// Assumptions are stated in §2 and are not repeated here. Chapter references
// point at the derivation of each decision rather than restating it.
// ============================================================================
package uart_parity_pkg;
typedef enum logic [1:0] {
PARITY_NONE = 2'b00,
PARITY_EVEN = 2'b01,
PARITY_ODD = 2'b10,
PARITY_MARK = 2'b11 // constant 1 — no detection, Chapter 3.3
} parity_mode_t;
endpackage
module uart_rx
import uart_parity_pkg::*;
#(
parameter int unsigned DATA_W = 8,
parameter int unsigned OVERSAMPLE = 16
) (
input logic clk,
input logic rst_n,
input logic rx_i, // the only asynchronous input
input logic os_tick_i, // enable, M per bit — Module 8
input parity_mode_t parity_mode_i,
output logic [DATA_W-1:0] rx_data_o,
output logic rx_valid_o,
input logic rx_ready_i,
output logic rx_parity_err_o,
output logic rx_frame_err_o,
output logic rx_overrun_o,
// Published so an integrator never has to reach inside for the FSM state.
// Chapter 11.4 — a hierarchical reference is a missing port, not a shortcut.
output logic rx_active_o,
// The synchronised line, published so an integration has ONE synchroniser
// on the asynchronous pin and every consumer sees the SAME resolved value.
// Chapter 12.2 — two synchronisers on one async signal can disagree.
output logic rx_sync_o
);
// ---- derived constants — Chapter 6.3 §2, §5 ----------------------------
localparam int unsigned CENTER = OVERSAMPLE / 2;
localparam int unsigned PH_W = (OVERSAMPLE <= 1) ? 1 : $clog2(OVERSAMPLE);
localparam int unsigned BI_W = (DATA_W <= 1) ? 1 : $clog2(DATA_W);
// ---- parameter legality — §6 of this chapter ---------------------------
initial begin
if (OVERSAMPLE < 2)
$fatal(1, "uart_rx: OVERSAMPLE = %0d has no centre to sample", OVERSAMPLE);
if (DATA_W < 1)
$fatal(1, "uart_rx: DATA_W = %0d is not a frame", DATA_W);
end
typedef enum logic [2:0] {
S_IDLE = 3'd0,
S_START = 3'd1,
S_DATA = 3'd2,
S_PARITY = 3'd3,
S_STOP = 3'd4
} rx_state_e;
rx_state_e state_q;
logic rx_meta_q, rx_sync_q, rx_sync_d_q;
logic [PH_W-1:0] phase_q;
logic [BI_W-1:0] bit_idx_q;
logic [DATA_W-1:0] shreg_q;
logic par_acc_q;
logic par_err_q;
logic start_cand;
logic sample_now;
// The ONLY definition of "this receiver is mid-frame". Encoding-independent,
// which is what lets the VHDL version leave its state encoding to synthesis.
assign rx_active_o = (state_q != S_IDLE);
assign rx_sync_o = rx_sync_q;
// ---- input boundary — Chapter 5.1 §3 -----------------------------------
// Reset to the IDLE level, never to zero: a receiver leaving reset must
// not present a fabricated departure from idle to the edge detector.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rx_meta_q <= 1'b1;
rx_sync_q <= 1'b1;
rx_sync_d_q <= 1'b1;
end else begin
rx_meta_q <= rx_i;
rx_sync_q <= rx_meta_q;
rx_sync_d_q <= rx_sync_q;
end
end
// ---- the two named events — Chapters 5.2 §1 and 6.3 §3 -----------------
assign start_cand = rx_sync_d_q && !rx_sync_q;
assign sample_now = os_tick_i && (phase_q == PH_W'(CENTER - 1));
// ---- the receiver's expectation — Chapter 6.4 §2 -----------------------
function automatic logic expected_parity(input parity_mode_t m, input logic acc);
case (m)
PARITY_EVEN: expected_parity = acc;
PARITY_ODD: expected_parity = ~acc;
default: expected_parity = 1'b1; // MARK
endcase
endfunction
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state_q <= S_IDLE;
phase_q <= '0;
bit_idx_q <= '0;
shreg_q <= '0;
par_acc_q <= 1'b0;
par_err_q <= 1'b0;
rx_data_o <= '0;
rx_valid_o <= 1'b0;
rx_parity_err_o <= 1'b0;
rx_frame_err_o <= 1'b0;
rx_overrun_o <= 1'b0;
end else begin
// Accept releases the holding register and ALL of its status
// together — Chapter 6.5 §3. Written first so that a delivery in
// the same cycle overrides it, which is what lets a byte be
// accepted and replaced on one edge.
if (rx_valid_o && rx_ready_i) begin
rx_valid_o <= 1'b0;
rx_parity_err_o <= 1'b0;
rx_frame_err_o <= 1'b0;
rx_overrun_o <= 1'b0;
end
if (state_q == S_IDLE) begin
// The only state that does not wait for the timing event.
if (start_cand) begin
state_q <= S_START;
phase_q <= '0; // THE origin — Chapter 6.1 §4
bit_idx_q <= '0;
par_acc_q <= 1'b0;
par_err_q <= 1'b0; // status is per-frame: clear at entry
end
end else if (os_tick_i) begin
// Explicit wrap. Natural overflow is correct only when
// OVERSAMPLE is a power of two — Chapter 6.3 §2.
phase_q <= (phase_q == PH_W'(OVERSAMPLE - 1)) ? '0 : phase_q + 1'b1;
if (sample_now) begin
case (state_q)
S_START: begin
// The sampled level here is the QUALIFICATION
// result, not payload — Chapter 6.2 §1.
if (rx_sync_q) state_q <= S_IDLE; // false start
else state_q <= S_DATA;
end
S_DATA: begin
// Right shift, MSB insert: LSB-first arrival order
// reconstructed without a write address — 6.3 §6.
shreg_q <= {rx_sync_q, shreg_q[DATA_W-1:1]};
par_acc_q <= par_acc_q ^ rx_sync_q;
bit_idx_q <= bit_idx_q + 1'b1;
// Compared against DATA_W-1: bit_idx_q still holds
// the index of the bit arriving now, and the
// increment above is concurrent — Chapter 6.3 §4.
if (bit_idx_q == BI_W'(DATA_W - 1)) begin
// The ONLY place configuration steers control.
if (parity_mode_i == PARITY_NONE) state_q <= S_STOP;
else state_q <= S_PARITY;
end
end
S_PARITY: begin
// Held INTERNALLY. Writing rx_parity_err_o here
// would let this frame rewrite the status of a
// byte still held from the previous one — the
// defect described in Chapter 6.4 §4.
par_err_q <= (rx_sync_q != expected_parity(parity_mode_i, par_acc_q));
state_q <= S_STOP;
end
S_STOP: begin
// Unconditional. Waiting for the line to return to
// mark wedges the receiver on a break — 6.2 §4.
state_q <= S_IDLE;
if (rx_valid_o && !rx_ready_i) begin
// Preserve the held frame, drop this one, and
// record the loss — Chapter 6.5 §4.
rx_overrun_o <= 1'b1;
end else begin
// THE publish point. Data and both status
// flags cross to the consumer on one edge.
rx_data_o <= shreg_q;
rx_valid_o <= 1'b1;
rx_parity_err_o <= par_err_q;
rx_frame_err_o <= !rx_sync_q;
end
end
default: state_q <= S_IDLE; // recovery — 6.2 §4
endcase
end
end
end
end
endmodule4. The Same Receiver in Verilog-2001
The SystemVerilog above is one of three implementations of Chapter 6.1 §6's contract. This is the same hardware without enum, without logic, and without $clog2 — which is what Verilog-2001 costs and, as §11 shows, all it costs.
//===========================================================================
// uart_rx — Synthesizable Verilog-2001
//
// Same hardware contract as the SystemVerilog and VHDL versions:
// one clock, asynchronous active-low reset, 2-flop input synchroniser,
// centre sampling at phase OVERSAMPLE/2-1, LSB-first assembly, and status
// written once at the stop sample so it belongs to the held character.
//
// Verilog-2001 has no enum and no packed struct, so the state encoding is
// an explicit localparam set and the width discipline is manual.
//===========================================================================
module uart_rx_v #(
parameter DATA_W = 8,
parameter OVERSAMPLE = 16
) (
input wire clk,
input wire rst_n,
input wire rx_i,
input wire os_tick_i,
input wire [1:0] parity_mode_i,
output reg [DATA_W-1:0] rx_data_o,
output reg rx_valid_o,
input wire rx_ready_i,
output reg rx_parity_err_o,
output reg rx_frame_err_o,
output reg rx_overrun_o,
output wire rx_active_o,
output wire rx_sync_o
);
// ---- parity encodings (shared with the other two implementations) -----
localparam [1:0] P_NONE = 2'b00,
P_EVEN = 2'b01,
P_ODD = 2'b10,
P_MARK = 2'b11;
// ---- explicit state encoding: no enum in Verilog-2001 -----------------
localparam [2:0] S_IDLE = 3'd0,
S_START = 3'd1,
S_DATA = 3'd2,
S_PARITY = 3'd3,
S_STOP = 3'd4;
// clog2 as a constant function — Verilog-2001 has no $clog2.
function integer clog2;
input integer value;
integer i;
begin
clog2 = 1;
for (i = value - 1; i > 1; i = i >> 1) clog2 = clog2 + 1;
end
endfunction
localparam CENTER = OVERSAMPLE / 2; // sample at CENTER-1
localparam PH_W = (OVERSAMPLE <= 1) ? 1 : clog2(OVERSAMPLE);
localparam BI_W = (DATA_W <= 1) ? 1 : clog2(DATA_W);
reg [2:0] state_q;
reg [PH_W-1:0] phase_q;
reg [BI_W-1:0] bit_idx_q;
reg [DATA_W-1:0] shreg_q;
reg par_acc_q;
reg par_err_q;
reg rx_meta_q, rx_sync_q, rx_sync_d_q;
assign rx_active_o = (state_q != S_IDLE);
assign rx_sync_o = rx_sync_q;
// ---- input boundary: 2-flop synchroniser, reset to MARK ---------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rx_meta_q <= 1'b1;
rx_sync_q <= 1'b1;
rx_sync_d_q <= 1'b1;
end else begin
rx_meta_q <= rx_i;
rx_sync_q <= rx_meta_q;
rx_sync_d_q <= rx_sync_q;
end
end
// Start candidate: a falling edge on the SYNCHRONISED line only.
wire start_cand = rx_sync_d_q && !rx_sync_q;
// ONE free-running phase counter serves every active state: it is
// zeroed at the start edge and never re-zeroed, so phase CENTER-1
// recurs at the centre of every subsequent bit interval.
wire sample_now = os_tick_i && (phase_q == (CENTER[PH_W-1:0] - 1'b1));
// Expected parity bit for the accumulated data.
function expected_parity;
input [1:0] mode;
input acc;
begin
case (mode)
P_EVEN: expected_parity = acc;
P_ODD: expected_parity = ~acc;
default: expected_parity = 1'b1; // MARK
endcase
end
endfunction
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state_q <= S_IDLE;
phase_q <= {PH_W{1'b0}};
bit_idx_q <= {BI_W{1'b0}};
shreg_q <= {DATA_W{1'b0}};
par_acc_q <= 1'b0;
par_err_q <= 1'b0;
rx_data_o <= {DATA_W{1'b0}};
rx_valid_o <= 1'b0;
rx_parity_err_o <= 1'b0;
rx_frame_err_o <= 1'b0;
rx_overrun_o <= 1'b0;
end else begin
// Consumer handshake: the holding register frees, and EVERY status
// bit clears with it. Parity, framing and overrun therefore all
// describe the same holding-register epoch.
if (rx_valid_o && rx_ready_i) begin
rx_valid_o <= 1'b0;
rx_parity_err_o <= 1'b0;
rx_frame_err_o <= 1'b0;
rx_overrun_o <= 1'b0;
end
if (state_q == S_IDLE) begin
if (start_cand) begin
state_q <= S_START;
phase_q <= {PH_W{1'b0}}; // anchor phase 0 at the start edge
bit_idx_q <= {BI_W{1'b0}};
par_acc_q <= 1'b0;
par_err_q <= 1'b0; // status is per-frame
end
end else if (os_tick_i) begin
// Free-running modulo-OVERSAMPLE counter, shared by every state.
if (phase_q == (OVERSAMPLE[PH_W-1:0] - 1'b1)) phase_q <= {PH_W{1'b0}};
else phase_q <= phase_q + 1'b1;
if (sample_now) begin
case (state_q)
S_START:
// Re-validate at the centre of the start bit.
if (rx_sync_q) state_q <= S_IDLE; // false start
else state_q <= S_DATA;
S_DATA: begin
// LSB first: shift right, insert at the top.
shreg_q <= {rx_sync_q, shreg_q[DATA_W-1:1]};
par_acc_q <= par_acc_q ^ rx_sync_q;
bit_idx_q <= bit_idx_q + 1'b1;
// bit_idx_q still holds the index of the bit arriving
// NOW, so the last data bit compares against DATA_W-1.
if (bit_idx_q == (DATA_W[BI_W-1:0] - 1'b1)) begin
if (parity_mode_i == P_NONE) state_q <= S_STOP;
else state_q <= S_PARITY;
end
end
S_PARITY: begin
// Held INTERNALLY. Publishing here would let the next
// frame rewrite the held character's status.
par_err_q <= (rx_sync_q != expected_parity(parity_mode_i, par_acc_q));
state_q <= S_STOP;
end
S_STOP: begin
if (rx_valid_o && !rx_ready_i) begin
// Holding register still occupied: drop the NEW
// character, keep the held one, report overrun.
rx_overrun_o <= 1'b1;
end else begin
rx_data_o <= shreg_q;
rx_parity_err_o <= par_err_q;
rx_frame_err_o <= !rx_sync_q;
rx_valid_o <= 1'b1;
end
state_q <= S_IDLE;
end
default: state_q <= S_IDLE;
endcase
end
end
end
end
endmoduleIts self-checking testbench
The testbench drives the serial line directly, at a bit period passed as an argument. It never uses the design's transmitter, its tick, or its internal state to decide what to expect, and it computes parity from the specification rather than calling the design's function — the independence rule Chapter 14.5 §4 measured the value of.
//===========================================================================
// Self-checking Verilog testbench for uart_rx_v.
// Drives the serial line directly at a chosen bit period — it never uses
// the DUT's transmitter, its tick, or its internal state to decide what to
// expect. Every check prints PASS or FAIL and the run prints a total.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_rx_v;
parameter DATA_W = 8;
parameter OVERSAMPLE = 16;
parameter CLK_NS = 10; // 100 MHz
parameter real TBIT = 8680.5556; // 115200 baud
parameter integer CLKS_PER_OS = 54; // ~TBIT/OVERSAMPLE/CLK_NS
localparam [1:0] P_NONE = 2'b00, P_EVEN = 2'b01, P_ODD = 2'b10, P_MARK = 2'b11;
reg clk = 1'b0; always #(CLK_NS/2) clk = ~clk;
reg rst_n = 1'b0;
reg rx_line = 1'b1;
reg [1:0] pmode = P_NONE;
reg rx_ready = 1'b1;
wire [DATA_W-1:0] rx_data;
wire rx_valid, rx_perr, rx_ferr, rx_ovr, rx_active, rx_sync;
// Oversample tick generator: one pulse every CLKS_PER_OS clocks.
reg [7:0] osdiv = 8'd0;
reg os_tick = 1'b0;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin osdiv <= 8'd0; os_tick <= 1'b0; end
else if (osdiv == CLKS_PER_OS-1) begin osdiv <= 8'd0; os_tick <= 1'b1; end
else begin osdiv <= osdiv + 8'd1; os_tick <= 1'b0; end
end
uart_rx_v #(.DATA_W(DATA_W), .OVERSAMPLE(OVERSAMPLE)) dut (
.clk(clk), .rst_n(rst_n), .rx_i(rx_line), .os_tick_i(os_tick),
.parity_mode_i(pmode),
.rx_data_o(rx_data), .rx_valid_o(rx_valid), .rx_ready_i(rx_ready),
.rx_parity_err_o(rx_perr), .rx_frame_err_o(rx_ferr),
.rx_overrun_o(rx_ovr), .rx_active_o(rx_active), .rx_sync_o(rx_sync));
integer checks = 0, fails = 0;
task ck;
input [511:0] name;
input cond;
begin
checks = checks + 1;
if (cond) $display(" PASS %0s", name);
else begin fails = fails + 1; $display(" FAIL %0s", name); end
end
endtask
// ---- independent parity, computed from the specification -------------
function exp_parity;
input [1:0] mode;
input [DATA_W-1:0] d;
integer k; reg a;
begin
a = 1'b0;
for (k = 0; k < DATA_W; k = k + 1) a = a ^ d[k];
case (mode)
P_EVEN: exp_parity = a;
P_ODD: exp_parity = ~a;
P_MARK: exp_parity = 1'b1;
default: exp_parity = 1'b0;
endcase
end
endfunction
// ---- line driver: bit period is an ARGUMENT, so baud error is testable
task send_frame;
input [DATA_W-1:0] d;
input [1:0] mode;
input real tbit;
input integer corrupt; // 0 none, 1 parity, 2 stop, 3 false start
integer k; reg p;
begin
if (corrupt == 3) begin
rx_line = 1'b0; #(tbit/4.0); rx_line = 1'b1; #(tbit*2.0);
end else begin
p = exp_parity(mode, d);
if (corrupt == 1) p = ~p;
rx_line = 1'b0; #(tbit); // start
for (k = 0; k < DATA_W; k = k + 1) begin
rx_line = d[k]; #(tbit); // LSB first
end
if (mode != P_NONE) begin rx_line = p; #(tbit); end
rx_line = (corrupt == 2) ? 1'b0 : 1'b1; #(tbit); // stop
rx_line = 1'b1; #(tbit);
end
end
endtask
// ---- capture at the handshake ----------------------------------------
reg [DATA_W-1:0] got_d; reg got_pe, got_fe;
integer n_rx = 0, n_ovr = 0;
reg ovr_d = 1'b0;
always @(posedge clk) begin
if (rst_n && rx_valid && rx_ready) begin
got_d = rx_data; got_pe = rx_perr; got_fe = rx_ferr; n_rx = n_rx + 1;
end
// rx_overrun_o is a HELD flag: count its RISING edge, not its level.
ovr_d <= rx_ovr;
if (rst_n && rx_ovr && !ovr_d) n_ovr = n_ovr + 1;
end
integer i, base;
reg [DATA_W-1:0] pats [0:7];
initial begin
pats[0]=8'h00; pats[1]=8'hFF; pats[2]=8'h55; pats[3]=8'hAA;
pats[4]=8'h01; pats[5]=8'h80; pats[6]=8'h7F; pats[7]=8'hC3;
$display("== uart_rx_v : self-checking Verilog testbench ==");
// ---- 1. reset state ---------------------------------------------
repeat (6) @(posedge clk);
ck("reset: rx_valid low", rx_valid === 1'b0);
ck("reset: rx_active low", rx_active === 1'b0);
ck("reset: rx_sync is MARK (never looks like a start bit)", rx_sync === 1'b1);
rst_n = 1'b1;
repeat (20) @(posedge clk);
// ---- 2. all eight patterns, 8N1 ---------------------------------
$display("-- 8N1, eight data patterns");
pmode = P_NONE;
for (i = 0; i < 8; i = i + 1) begin
base = n_rx;
send_frame(pats[i], P_NONE, TBIT, 0);
#(TBIT);
ck("8N1 byte received", n_rx == base + 1);
ck("8N1 data correct", got_d === pats[i]);
ck("8N1 no status set", (got_pe === 1'b0) && (got_fe === 1'b0));
end
// ---- 3. parity modes --------------------------------------------
$display("-- parity modes, clean frames");
pmode = P_EVEN; #(TBIT);
base = n_rx; send_frame(8'hA5, P_EVEN, TBIT, 0); #(TBIT);
ck("8E1 data correct", (n_rx == base+1) && (got_d === 8'hA5));
ck("8E1 no parity error", got_pe === 1'b0);
pmode = P_ODD; #(TBIT);
base = n_rx; send_frame(8'hA5, P_ODD, TBIT, 0); #(TBIT);
ck("8O1 data correct", (n_rx == base+1) && (got_d === 8'hA5));
ck("8O1 no parity error", got_pe === 1'b0);
// ---- 4. injected parity error -----------------------------------
$display("-- injected parity error");
pmode = P_EVEN; #(TBIT);
base = n_rx; send_frame(8'h3C, P_EVEN, TBIT, 1); #(TBIT);
ck("parity error reported", got_pe === 1'b1);
ck("byte still delivered", (n_rx == base+1) && (got_d === 8'h3C));
// ---- 5. injected framing error ----------------------------------
$display("-- injected framing error");
pmode = P_NONE; #(TBIT*2);
base = n_rx; send_frame(8'h5A, P_NONE, TBIT, 2); #(TBIT*2);
ck("framing error reported", got_fe === 1'b1);
ck("byte still delivered", (n_rx == base+1) && (got_d === 8'h5A));
// ---- 6. false start is rejected ---------------------------------
$display("-- false start");
#(TBIT*2);
base = n_rx; send_frame(8'h00, P_NONE, TBIT, 3); #(TBIT*2);
ck("false start produces no character", n_rx == base);
ck("receiver returned to idle", rx_active === 1'b0);
// ---- 7. back-to-back frames -------------------------------------
$display("-- back-to-back frames, no inter-frame gap");
base = n_rx;
send_frame(8'h12, P_NONE, TBIT, 0);
send_frame(8'h34, P_NONE, TBIT, 0);
#(TBIT);
ck("both back-to-back frames received", n_rx == base + 2);
ck("second byte correct", got_d === 8'h34);
// ---- 8. overrun --------------------------------------------------
$display("-- overrun: consumer stops reading");
rx_ready = 1'b0; #(TBIT);
base = n_ovr;
send_frame(8'h11, P_NONE, TBIT, 0); // fills the holding register
send_frame(8'h22, P_NONE, TBIT, 0); // must be dropped
#(TBIT);
ck("overrun flag raised exactly once", n_ovr == base + 1);
ck("held character kept, not overwritten", rx_data === 8'h11);
ck("overrun is HELD while the consumer stalls", rx_ovr === 1'b1);
rx_ready = 1'b1; #(TBIT);
// ---- 9. baud tolerance -------------------------------------------
$display("-- baud offsets");
base = n_rx; send_frame(8'h55, P_NONE, TBIT/1.03, 0); #(TBIT*2);
ck("+3% baud: byte correct", (n_rx == base+1) && (got_d === 8'h55));
base = n_rx; send_frame(8'h55, P_NONE, TBIT/0.97, 0); #(TBIT*2);
ck("-3% baud: byte correct", (n_rx == base+1) && (got_d === 8'h55));
$display("== %0d checks, %0d failures ==", checks, fails);
if (fails == 0) $display(" RESULT: ALL VERILOG TESTS PASSED");
else $display(" RESULT: VERILOG TESTS FAILED");
$finish;
end
initial begin #40_000_000; $display("TIMEOUT"); $finish; end
endmodule5. The Same Receiver in VHDL-2008
--==========================================================================
-- uart_rx -- Synthesizable VHDL-2008
--
-- Same hardware contract as the Verilog-2001 and SystemVerilog versions:
-- one clock, asynchronous active-low reset, 2-flop input synchroniser,
-- centre sampling at phase CENTER-1, LSB-first assembly, and status
-- written once at the stop sample so it belongs to the held character.
--
-- VHDL differences that are idiomatic rather than architectural:
-- * an enumerated state type instead of encoded localparams
-- * unsigned counters with explicit conversions
-- * one process with rising_edge and an asynchronous reset branch
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity uart_rx is
generic (
DATA_W : natural := 8;
OVERSAMPLE : natural := 16
);
port (
clk : in std_logic;
rst_n : in std_logic; -- async assert, active low
rx_i : in std_logic; -- asynchronous line
os_tick_i : in std_logic; -- one-clock enable
parity_mode_i : in std_logic_vector(1 downto 0); -- 00 N, 01 E, 10 O, 11 M
rx_data_o : out std_logic_vector(DATA_W-1 downto 0);
rx_valid_o : out std_logic;
rx_ready_i : in std_logic;
rx_parity_err_o : out std_logic;
rx_frame_err_o : out std_logic;
rx_overrun_o : out std_logic;
rx_active_o : out std_logic;
rx_sync_o : out std_logic
);
end entity uart_rx;
architecture rtl of uart_rx is
constant P_NONE : std_logic_vector(1 downto 0) := "00";
constant P_EVEN : std_logic_vector(1 downto 0) := "01";
constant P_ODD : std_logic_vector(1 downto 0) := "10";
constant P_MARK : std_logic_vector(1 downto 0) := "11";
-- Sample at CENTER-1 of a zero-based phase count, exactly as the other
-- two implementations do.
constant CENTER : natural := OVERSAMPLE / 2;
type rx_state_t is (S_IDLE, S_START, S_DATA, S_PARITY, S_STOP);
signal state_q : rx_state_t := S_IDLE;
signal phase_q : unsigned(15 downto 0) := (others => '0');
signal bit_idx_q : unsigned(15 downto 0) := (others => '0');
signal shreg_q : std_logic_vector(DATA_W-1 downto 0) := (others => '0');
signal par_acc_q : std_logic := '0';
signal par_err_q : std_logic := '0';
signal rx_meta_q : std_logic := '1';
signal rx_sync_q : std_logic := '1';
signal rx_sync_d_q : std_logic := '1';
signal valid_q : std_logic := '0';
-- Expected parity bit for the accumulated data, from the specification.
function expected_parity (mode : std_logic_vector(1 downto 0);
acc : std_logic) return std_logic is
begin
if mode = P_EVEN then return acc;
elsif mode = P_ODD then return not acc;
else return '1'; -- MARK
end if;
end function;
begin
-- Published outputs that are pure functions of state.
rx_active_o <= '0' when state_q = S_IDLE else '1';
rx_sync_o <= rx_sync_q;
rx_valid_o <= valid_q;
-- A start candidate is a falling edge on the SYNCHRONISED line only.
process (clk, rst_n)
variable start_cand : std_logic;
variable sample_now : std_logic;
begin
if rst_n = '0' then
rx_meta_q <= '1'; -- reset to MARK so reset
rx_sync_q <= '1'; -- never looks like a start
rx_sync_d_q <= '1';
state_q <= S_IDLE;
phase_q <= (others => '0');
bit_idx_q <= (others => '0');
shreg_q <= (others => '0');
par_acc_q <= '0';
par_err_q <= '0';
rx_data_o <= (others => '0');
valid_q <= '0';
rx_parity_err_o <= '0';
rx_frame_err_o <= '0';
rx_overrun_o <= '0';
elsif rising_edge(clk) then
-- 2-flop synchroniser plus one history flop for edge detection.
rx_meta_q <= rx_i;
rx_sync_q <= rx_meta_q;
rx_sync_d_q <= rx_sync_q;
start_cand := rx_sync_d_q and (not rx_sync_q);
if os_tick_i = '1' and phase_q = to_unsigned(CENTER - 1, phase_q'length) then
sample_now := '1';
else
sample_now := '0';
end if;
-- Consumer handshake: the holding register frees, and EVERY status
-- bit clears with it. Parity, framing and overrun therefore all
-- describe the same holding-register epoch.
if valid_q = '1' and rx_ready_i = '1' then
valid_q <= '0';
rx_parity_err_o <= '0';
rx_frame_err_o <= '0';
rx_overrun_o <= '0';
end if;
if state_q = S_IDLE then
if start_cand = '1' then
state_q <= S_START;
phase_q <= (others => '0'); -- anchor phase 0 at the edge
bit_idx_q <= (others => '0');
par_acc_q <= '0';
par_err_q <= '0';
end if;
elsif os_tick_i = '1' then
-- One free-running modulo-OVERSAMPLE counter for every state.
if phase_q = to_unsigned(OVERSAMPLE - 1, phase_q'length) then
phase_q <= (others => '0');
else
phase_q <= phase_q + 1;
end if;
if sample_now = '1' then
case state_q is
when S_START =>
-- Re-validate at the centre of the start bit.
if rx_sync_q = '1' then
state_q <= S_IDLE; -- false start
else
state_q <= S_DATA;
end if;
when S_DATA =>
-- LSB first: shift right, insert at the top.
shreg_q <= rx_sync_q & shreg_q(DATA_W-1 downto 1);
par_acc_q <= par_acc_q xor rx_sync_q;
bit_idx_q <= bit_idx_q + 1;
-- bit_idx_q still holds the index of the bit arriving
-- NOW, so the last data bit compares against DATA_W-1.
if bit_idx_q = to_unsigned(DATA_W - 1, bit_idx_q'length) then
if parity_mode_i = P_NONE then
state_q <= S_STOP;
else
state_q <= S_PARITY;
end if;
end if;
when S_PARITY =>
-- Held INTERNALLY. Publishing here would let the next
-- frame rewrite the held character's status.
if rx_sync_q /= expected_parity(parity_mode_i, par_acc_q) then
par_err_q <= '1';
else
par_err_q <= '0';
end if;
state_q <= S_STOP;
when S_STOP =>
if valid_q = '1' and rx_ready_i = '0' then
-- Holding register still occupied: drop the NEW
-- character, keep the held one, report overrun.
rx_overrun_o <= '1';
else
rx_data_o <= shreg_q;
rx_parity_err_o <= par_err_q;
if rx_sync_q = '0' then
rx_frame_err_o <= '1';
else
rx_frame_err_o <= '0';
end if;
valid_q <= '1';
end if;
state_q <= S_IDLE;
when others =>
state_q <= S_IDLE;
end case;
end if;
end if;
end if;
end process;
end architecture rtl;Its self-checking testbench
--==========================================================================
-- Self-checking VHDL testbench for uart_rx.
-- Drives the serial line directly at a chosen bit period. It never uses the
-- DUT's internal state, its tick, or its parity function to decide what to
-- expect. Every check reports PASS or FAIL and the run reports a total.
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
entity tb_uart_rx is
end entity tb_uart_rx;
architecture sim of tb_uart_rx is
constant DATA_W : natural := 8;
constant OVERSAMPLE : natural := 16;
constant CLK_PERIOD : time := 10 ns; -- 100 MHz
constant TBIT : time := 8680 ns; -- ~115200 baud
constant CLKS_PER_OS: natural := 54;
constant P_NONE : std_logic_vector(1 downto 0) := "00";
constant P_EVEN : std_logic_vector(1 downto 0) := "01";
constant P_ODD : std_logic_vector(1 downto 0) := "10";
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal rx_line : std_logic := '1';
signal os_tick : std_logic := '0';
signal pmode : std_logic_vector(1 downto 0) := P_NONE;
signal rx_ready : std_logic := '1';
signal rx_data : std_logic_vector(DATA_W-1 downto 0);
signal rx_valid : std_logic;
signal rx_perr : std_logic;
signal rx_ferr : std_logic;
signal rx_ovr : std_logic;
signal rx_active : std_logic;
signal rx_sync : std_logic;
signal checks : natural := 0;
signal fails : natural := 0;
signal n_rx : natural := 0;
signal n_ovr : natural := 0;
signal got_d : std_logic_vector(DATA_W-1 downto 0) := (others => '0');
signal got_pe : std_logic := '0';
signal got_fe : std_logic := '0';
signal ovr_d : std_logic := '0';
signal done : boolean := false;
begin
clk <= '0' when done else not clk after CLK_PERIOD/2;
-- Oversample tick: one pulse every CLKS_PER_OS clocks.
os_gen : process (clk, rst_n)
variable d : natural := 0;
begin
if rst_n = '0' then
d := 0; os_tick <= '0';
elsif rising_edge(clk) then
if d = CLKS_PER_OS - 1 then d := 0; os_tick <= '1';
else d := d + 1; os_tick <= '0';
end if;
end if;
end process;
dut : entity work.uart_rx
generic map (DATA_W => DATA_W, OVERSAMPLE => OVERSAMPLE)
port map (
clk => clk, rst_n => rst_n, rx_i => rx_line, os_tick_i => os_tick,
parity_mode_i => pmode,
rx_data_o => rx_data, rx_valid_o => rx_valid, rx_ready_i => rx_ready,
rx_parity_err_o => rx_perr, rx_frame_err_o => rx_ferr,
rx_overrun_o => rx_ovr, rx_active_o => rx_active, rx_sync_o => rx_sync);
-- Capture at the handshake, exactly as a consumer would.
cap : process (clk)
begin
if rising_edge(clk) then
if rst_n = '1' and rx_valid = '1' and rx_ready = '1' then
got_d <= rx_data;
got_pe <= rx_perr;
got_fe <= rx_ferr;
n_rx <= n_rx + 1;
end if;
-- rx_overrun_o is a HELD flag: count its RISING edge, not its level.
ovr_d <= rx_ovr;
if rst_n = '1' and rx_ovr = '1' and ovr_d = '0' then
n_ovr <= n_ovr + 1;
end if;
end if;
end process;
stim : process
procedure ck (name : in string; cond : in boolean) is
variable l : line;
begin
checks <= checks + 1;
if cond then
write(l, string'(" PASS ") & name);
else
fails <= fails + 1;
write(l, string'(" FAIL ") & name);
end if;
writeline(output, l);
wait for 0 ns;
end procedure;
-- Independent parity, computed from the specification.
function exp_parity (mode : std_logic_vector(1 downto 0);
d : std_logic_vector(DATA_W-1 downto 0))
return std_logic is
variable a : std_logic := '0';
begin
for k in 0 to DATA_W-1 loop
a := a xor d(k);
end loop;
if mode = P_EVEN then return a;
elsif mode = P_ODD then return not a;
else return '0';
end if;
end function;
-- corrupt: 0 none, 1 parity, 2 stop, 3 false start
procedure send_frame (d : in std_logic_vector(DATA_W-1 downto 0);
mode : in std_logic_vector(1 downto 0);
tbit : in time;
corrupt : in natural) is
variable p : std_logic;
begin
if corrupt = 3 then
rx_line <= '0'; wait for tbit/4;
rx_line <= '1'; wait for tbit*2;
else
p := exp_parity(mode, d);
if corrupt = 1 then p := not p; end if;
rx_line <= '0'; wait for tbit; -- start
for k in 0 to DATA_W-1 loop
rx_line <= d(k); wait for tbit; -- LSB first
end loop;
if mode /= P_NONE then
rx_line <= p; wait for tbit;
end if;
if corrupt = 2 then rx_line <= '0'; else rx_line <= '1'; end if;
wait for tbit;
rx_line <= '1'; wait for tbit;
end if;
end procedure;
type pat_array is array (0 to 7) of std_logic_vector(7 downto 0);
constant pats : pat_array := (x"00", x"FF", x"55", x"AA",
x"01", x"80", x"7F", x"C3");
variable base : natural;
variable l : line;
begin
write(l, string'("== uart_rx (VHDL) : self-checking testbench ==")); writeline(output, l);
-- 1. reset state
wait for CLK_PERIOD*6;
ck("reset: rx_valid low", rx_valid = '0');
ck("reset: rx_active low", rx_active = '0');
ck("reset: rx_sync is MARK (never looks like a start bit)", rx_sync = '1');
rst_n <= '1';
wait for CLK_PERIOD*20;
-- 2. eight data patterns, 8N1
write(l, string'("-- 8N1, eight data patterns")); writeline(output, l);
pmode <= P_NONE;
for i in 0 to 7 loop
base := n_rx;
send_frame(pats(i), P_NONE, TBIT, 0);
wait for TBIT;
ck("8N1 byte received", n_rx = base + 1);
ck("8N1 data correct", got_d = pats(i));
ck("8N1 no status set", got_pe = '0' and got_fe = '0');
end loop;
-- 3. parity modes
write(l, string'("-- parity modes, clean frames")); writeline(output, l);
pmode <= P_EVEN; wait for TBIT;
base := n_rx; send_frame(x"A5", P_EVEN, TBIT, 0); wait for TBIT;
ck("8E1 data correct", n_rx = base+1 and got_d = x"A5");
ck("8E1 no parity error", got_pe = '0');
pmode <= P_ODD; wait for TBIT;
base := n_rx; send_frame(x"A5", P_ODD, TBIT, 0); wait for TBIT;
ck("8O1 data correct", n_rx = base+1 and got_d = x"A5");
ck("8O1 no parity error", got_pe = '0');
-- 4. injected parity error
write(l, string'("-- injected parity error")); writeline(output, l);
pmode <= P_EVEN; wait for TBIT;
base := n_rx; send_frame(x"3C", P_EVEN, TBIT, 1); wait for TBIT;
ck("parity error reported", got_pe = '1');
ck("byte still delivered", n_rx = base+1 and got_d = x"3C");
-- 5. injected framing error
write(l, string'("-- injected framing error")); writeline(output, l);
pmode <= P_NONE; wait for TBIT*2;
base := n_rx; send_frame(x"5A", P_NONE, TBIT, 2); wait for TBIT*2;
ck("framing error reported", got_fe = '1');
ck("byte still delivered", n_rx = base+1 and got_d = x"5A");
-- 6. false start
write(l, string'("-- false start")); writeline(output, l);
wait for TBIT*2;
base := n_rx; send_frame(x"00", P_NONE, TBIT, 3); wait for TBIT*2;
ck("false start produces no character", n_rx = base);
ck("receiver returned to idle", rx_active = '0');
-- 7. back-to-back frames
write(l, string'("-- back-to-back frames, no inter-frame gap")); writeline(output, l);
base := n_rx;
send_frame(x"12", P_NONE, TBIT, 0);
send_frame(x"34", P_NONE, TBIT, 0);
wait for TBIT;
ck("both back-to-back frames received", n_rx = base + 2);
ck("second byte correct", got_d = x"34");
-- 8. overrun
write(l, string'("-- overrun: consumer stops reading")); writeline(output, l);
rx_ready <= '0'; wait for TBIT;
base := n_ovr;
send_frame(x"11", P_NONE, TBIT, 0);
send_frame(x"22", P_NONE, TBIT, 0);
wait for TBIT;
ck("overrun flag raised exactly once", n_ovr = base + 1);
ck("held character kept, not overwritten", rx_data = x"11");
ck("overrun is HELD while the consumer stalls", rx_ovr = '1');
rx_ready <= '1'; wait for TBIT;
-- 9. baud offsets
write(l, string'("-- baud offsets")); writeline(output, l);
base := n_rx; send_frame(x"55", P_NONE, TBIT*100/103, 0); wait for TBIT*2;
ck("+3% baud: byte correct", n_rx = base+1 and got_d = x"55");
base := n_rx; send_frame(x"55", P_NONE, TBIT*100/97, 0); wait for TBIT*2;
ck("-3% baud: byte correct", n_rx = base+1 and got_d = x"55");
write(l, string'("== ")); write(l, checks); write(l, string'(" checks, "));
write(l, fails); write(l, string'(" failures ==")); writeline(output, l);
if fails = 0 then
write(l, string'(" RESULT: ALL VHDL TESTS PASSED"));
else
write(l, string'(" RESULT: VHDL TESTS FAILED"));
end if;
writeline(output, l);
done <= true;
wait;
end process;
end architecture sim;6. The Walkthrough
Each part, and the invariant it maintains.
Parameters and interface. Invariant: the hardware's shape is fixed at elaboration and its behaviour is selected at runtime. DATA_W and OVERSAMPLE change register widths; parity_mode_i selects a path. Chapter 6.1 §5 gives the test that separates them.
Derived constants. Invariant: no position in this design is a literal. CENTER, PH_W and BI_W all follow the parameters, so changing OVERSAMPLE from 16 to 8 moves every sampling position correctly. Chapter 5.3 §6 showed what a hard-coded 7 costs.
Parameter legality. Invariant: a parameter for which the arithmetic is undefined fails at elaboration, not in silicon. Both guards mark genuinely broken configurations rather than merely unusual ones — see §6.
State type. Invariant: the machine's states are named and enumerable. Five values in three bits, with three encodings unreachable and a default that recovers from them.
Synchronisation boundary. Invariant: exactly one representation of the line exists, and rx_i reaches nothing else. Chapter 6.1 §9 explains why two synchronisers would let the receiver hold contradictory beliefs about the wire.
Named events. Invariant: start_cand and sample_now are each produced in exactly one place. Scattering phase_q == 7 through the design is the defect Chapter 6.3 §3 named; one assignment means one thing to change.
Phase counter. Invariant: phase zero is the start candidate, and the sample fires on the tick that carries the count to CENTER. This is Chapter 6.1 §4's single origin, and the reason all eleven samples in Chapter 6.3 §7's trace read phase_q == 7.
Bit counter. Invariant: bit_idx_q is the index of the data bit arriving now, and nothing outside S_DATA reads it. That second clause is what makes the wrap from 7 to 0 harmless at DATA_W = 8.
Shift register. Invariant: after n stores, the first n arrived bits occupy the top n positions, and after DATA_W stores they are correctly placed. Verified by the 00, 80, C0, 60, 30, 98, 4C, A6 trace.
Parity accumulator. Invariant: it holds the XOR of the payload bits stored so far, and nothing else. Cleared at the candidate; the received parity bit never enters it.
Stop validation. Invariant: the framing result describes the stop interval alone. One comparison, at the stop sample, published in the same branch as the data.
Holding register and status. Invariant: data and all three flags describe one frame and move together. The single publish point, and Chapter 6.5 §3's stability property.
Control and recovery. Invariant: the machine is always in a legal state and always leaves every state. No conditional exits except the qualification test; default covers the rest.
7. Review Checklist
The questions a reviewer should ask, with this design's answers. Every no would be a finding.
| 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 |
8. 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.
9. Corner Cases
Reset released onto a live line. The receiver returns to S_IDLE correctly and the in-flight byte is discarded — and then it mis-frames on the remainder and delivers a byte that was never sent. Simulation produced 0xFA from an interrupted 0xA6. Nothing malfunctioned: the next mark-to-space transition it saw was a data edge, and nothing in a UART frame identifies itself as a continuation. Chapter 6.2 §7 develops it. A testbench must allow an idle period after a mid-frame reset, and a system resetting a UART during traffic should expect one spurious byte.
Line stuck at space. The receiver validates a stop of 0, reports rx_frame_err_o, returns to S_IDLE, and then — correctly — does nothing, because a candidate needs a mark-to-space transition and there is no mark to leave. It resumes the moment a real frame arrives. This is the behaviour Chapter 6.2 §4's unconditional stop exit exists to produce; the conditional version wedges here permanently.
Consumer never reads. One byte held indefinitely, rx_overrun_o set at the second frame and remaining set. The receiver does not escalate or give up. This is intentional: escalation policy belongs to the system, and Module 9 is where it is built.
Accept and delivery on the same cycle. rx_valid_o && rx_ready_i clears the register set, and the S_STOP branch may write it in the same cycle. The clear is written first in the always_ff, so the delivery's non-blocking assignments take precedence — the byte is accepted and replaced on one edge with no gap and no overrun. Verified by the back-to-back test with a prompt consumer.
Glitch narrower than half a bit. Rejected by qualification: the sample at 0.5 UI finds mark and the machine returns to S_IDLE. Chapter 5.2 established the exact class this rejects — disturbances shorter than the validation delay, and nothing more.
10. Verification Matrix and Results
The receiver-focused matrix, with what each axis is for:
| 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, per language, from the runs in §1. The Verilog and SystemVerilog testbenches drive the same scenarios and differ only in the assertion binding:
$ iverilog -g2012 -o sim_sv uart_parity_pkg.sv uart_rx_sv.sv rx_assertions.sv tb_uart_rx_sv.sv && ./sim_sv
== uart_rx_sv : self-checking SystemVerilog testbench ==
...
PASS assertions: no violations during the whole run
== 45 checks, 0 failures ==
RESULT: ALL SYSTEMVERILOG TESTS PASSED
$ iverilog -g2001 -o sim_v uart_rx_v.v tb_uart_rx_v.v && ./sim_v
== uart_rx_v : self-checking Verilog testbench ==
...
== 44 checks, 0 failures ==
RESULT: ALL VERILOG TESTS PASSED
$ nvc --std=2008 -a uart_rx.vhd tb_uart_rx.vhd && nvc --std=2008 -e tb_uart_rx && nvc --std=2008 -r tb_uart_rx
== uart_rx (VHDL) : self-checking testbench ==
...
PASS overrun is HELD while the consumer stalls
PASS +3% baud: byte correct
PASS -3% baud: byte correct
== 44 checks, 0 failures ==
RESULT: ALL VHDL TESTS PASSED| Scenario | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| reset state — valid low, active low, sync at MARK | 3 | 3 | 3 |
| eight data patterns, 8N1 | 24 | 24 | 24 |
| 8E1 and 8O1, clean | 4 | 4 | 4 |
| injected parity error — reported and delivered | 2 | 2 | 2 |
| injected framing error — reported and delivered | 2 | 2 | 2 |
| false start rejected, returns to idle | 2 | 2 | 2 |
| back-to-back frames, no gap | 2 | 2 | 2 |
| overrun — flag raised once, held, byte preserved | 3 | 3 | 3 |
| baud offset ±3% | 2 | 2 | 2 |
| bound assertions, no violations | — | 1 | — |
| total | 44 | 45 | 44 |
All three testbenches run the same nine scenarios. The SystemVerilog total is one higher only because the assertion module is bound into it; nothing else differs. Together that is 133 checks and zero failures, and each language's testbench was written against the contract rather than translated from another, which is why §11's equivalence claim means something.
The assertions were mutation-checked. Rebuilding the receiver with a one-clock pulse valid — Chapter 6.5 §2's mistake, made deliberately — produced:
A2 violations on the pulse-valid mutant: 2
== 45 checks, 4 failures ==against 45 checks, 0 failures and 0 violations on the correct design. An assertion that has never fired has not been shown capable of firing.
What the matrix does not cover, stated so it is not mistaken for completeness: randomised start phase against a free-running clock (the testbench drives on the clock grid); mismatch beyond ±3%; DATA_W other than 8 in simulation, though the arithmetic was checked across 5–9; OVERSAMPLE other than 16 in simulation; and any form of constrained-random or coverage-driven verification, which is Modules 14–16.
11. Cross-Language Equivalence
Three implementations of one contract are only worth having if they agree — and all three were simulated, so this is a behavioural claim rather than a reading of the source. Row by row, against Chapter 6.1 §6:
| Property | Verilog-2001 | SystemVerilog | VHDL-2008 | Agree? |
|---|---|---|---|---|
| ports and directions | 13 | 13 | 13 | yes |
DATA_W / OVERSAMPLE | parameters | parameters | generics | yes |
| parity encoding | localparam 2-bit | enum logic [1:0] | std_logic_vector(1 downto 0) | yes — same values |
| reset style | async assert, active low | async assert, active low | async assert, active low | yes |
| reset value of the synchroniser | 1 (MARK) | 1 (MARK) | '1' (MARK) | yes |
| sample phase | CENTER-1 | CENTER-1 | CENTER-1 | yes |
| phase counter | one, free-running mod OVERSAMPLE | same | same | yes |
| bit ordering | shift right, insert at MSB | same | same | yes |
| state graph | 5 states, 8 transitions | same | same | yes |
| state encoding | fixed 000–100 | fixed 000–100 | chosen by synthesis | no — deliberate |
rx_valid_o | level, held to handshake | same | same | yes |
| status lifetime | cleared on handshake | same | same | yes |
rx_overrun_o | held, not pulsed | held | held | yes — after §7 of 6.4 |
latency, start edge to rx_valid_o | identical | identical | identical | yes |
One deliberate difference, and it is a property of the languages rather than of the designs. VHDL's enumerated state type does not fix an encoding; synthesis chooses one unless an attribute constrains it. Verilog and SystemVerilog fix it by construction because the enumeration literals are numbers.
That difference has a consequence worth stating: the numeric state encoding is not part of this contract in any of the three languages, and code outside the receiver must never depend on it. Chapter 11.4 §3 found exactly that dependency in a later integration and replaced it with the published rx_active_o port — which exists in all three implementations precisely so nobody needs the encoding.
12. UVM Mapping
Module 6 is an RTL module, so this is a mapping rather than an environment — the environment itself is Module 16. What is worth fixing now is which part of the receiver's contract each UVM component would own, because the answer follows from Chapter 6.1 §6's boundaries rather than from UVM.
| Receiver contract element | UVM component | What it carries |
|---|---|---|
| a frame on the wire | sequence item | data, data_bits, parity_mode, baud_offset, parity_corrupt, stop_corrupt |
driving rx_i | driver | turns one item into a waveform at an arbitrary bit period |
observing rx_i | input monitor | reconstructs what was actually driven |
| observing the read port | output monitor | {data, parity_err, frame_err} sampled at rx_valid_o && rx_ready_i |
rx_ready_i policy | sequence on a second sequencer | always-ready, random-stall, or never-ready for the overrun case |
| expected result | predictor | computes status from the item alone, never from the DUT |
| comparison | scoreboard | in-order, whole-tuple |
Three things in that table are decided by this module, not by UVM.
The output monitor samples at the handshake, not when rx_valid_o rises. §8's contract says data and status are stable from the rise until the handshake; sampling at the rise would work and would miss a design that lets them move afterwards. Sampling at the handshake checks the property the consumer actually depends on.
rx_ready_i needs its own sequence, not a fixed value. The overrun path only exists when ready is low at a stop sample, so a testbench that ties ready high cannot reach it — and Chapter 6.5 §5's overrun trace could not have been produced.
The predictor must not import the design's parity function. Chapter 14.5 §4 measured what that costs: the same mutant produced 8 mismatches from an independent checker and 0 from one that reused the design's own function. The testbenches in §4 and §5 above already follow the rule — each computes parity from the specification — which is why they are usable as the basis for the UVM driver and predictor rather than being thrown away.
What is deliberately not here. No uvm_component boilerplate, no factory registration, no config-db plumbing. None of it would teach anything about a UART receiver, and all of it is Module 16's subject where it can be built once and used properly.
13. Debugging: Symptom to Mechanism
14. What This Means on an FPGA
Thirty-five flip-flops at DATA_W = 8, OVERSAMPLE = 16. Nothing here needs optimising, and anything that appears to want it is a sign a parameter has been pushed somewhere a UART does not go.
The synchroniser is the only structure needing a constraint, and it is Module 12's to specify. Its placement is fixed here: adjacent to the pin, with nothing tapping rx_i.
Expose state_q, sample_now, rx_valid_o and rx_ready_i during bring-up — four signals that resolve most of §9's table at a glance. They are debug aids, not interface: a production instantiation need not carry them, and a bring-up build should.
Check the synthesis report for a latch. There should be none; every branch in both always_ff blocks either assigns or holds a register. This is a reviewed claim, per §1 — an actual synthesis run is what would turn it into a verified one.
15. Understanding Check
16. Summary
One synthesizable receiver, written three times from one contract and simulated to 133 checks with 0 failures — 45 in SystemVerilog and 44 in Verilog under Icarus Verilog 13.0, and 44 in VHDL under NVC 1.23.0 — with every trace published across Module 6 extracted from those runs rather than computed by hand. MDX compiling and HDL compiling are different facts, and both were established for all three languages; synthesisability remains a reviewed claim awaiting Module 12.
The assumptions block states what the receiver is not: no FIFO, no break handling, no idle detection, no sticky status, no second stop bit, no majority voting — each deferred to a named module, because unsupported behaviour must look deliberate.
Every part maintains a stated invariant, and the walkthrough names them: one representation of the line, one origin, one publish point, one place where configuration steers control flow, and a counter whose value is read only where it is meaningful.
Two parameters and one runtime input, and even that small space has corners — DATA_W = 8 wraps the bit counter, a non-power-of-two OVERSAMPLE exposes the natural-wrap defect. Every parameter multiplies the verification burden, which is why the design has two rather than eight.
The corner cases are documented rather than discovered: mis-framing after reset onto a live line, correct inaction on a line stuck at space, indefinite holding when the consumer never reads, and accept-plus-delivery on one edge.
Two real defects were found during development — status ownership, found by reasoning about a path no nominal test reaches, and a flip-flop count contradicted by computing it. Both are reported where they occurred.
17. Where Module 6 Leaves You
Six chapters have turned Module 5's timing mechanisms into a receiver.
Chapter 6.1 partitioned it, wrote the interface before the implementation, and settled the timing origin that Module 5 deliberately left open. Chapter 6.2 built the control machine as a table of eight transitions and showed which three implementations get wrong. Chapter 6.3 derived both counters from the parameters, stated every semantic before its RTL, and traced a byte through the shift register. Chapter 6.4 added validation and the publish discipline that keeps status attached to the frame it describes. Chapter 6.5 chose the output interface from the fact that a receiver cannot tell the wire to wait. This chapter assembled, compiled, simulated and reviewed the result.
An engineer holding this chain can design a receiver from a specification, derive its counter widths and semantics, defend its interface, review someone else's, and diagnose a failure from a symptom — without memorising the final RTL, which is the test Chapter 6.1 §1 set for the interface and which applies equally to the module.
What remains is the other half of the link, and everything the receiver deferred.
18. What Comes Next
Module 7 builds the transmitter — the simpler half, done properly. It has no start detection, no qualification and no sampling: it executes a schedule it wrote. What it does have is a producer-facing contract that is easy to get subtly wrong, a frame sequencer whose shape must match this receiver's exactly, and a back-to-back behaviour that decides whether the link runs at its rated throughput or at some fraction of it.
After that: Module 8 builds the os_tick both halves have been consuming; Module 9 takes the per-frame status this module produces and builds the error, break and recovery architecture on top of it — including the idle detection that would suppress §7's fabricated byte; Module 10 adds the buffering that moves the overrun threshold; and Modules 11 onward assemble an IP, integrate it, and verify it properly.
Browse the full path on the UART tutorials index. For the sampling mechanisms this receiver assembles, read back to Chapter 5.3; for the budget that decides whether it works at a given rate, Chapter 4.5.
Continue learning
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.
