RTL Design Patterns · Chapter 13 · Case Studies
UART (FSM + Shift Register + Baud Timer)
This is the first end-to-end case study, where three patterns you learned in isolation become one real peripheral: a complete UART serial link. A baud generator, which is just a clock divider and timer, makes one bit-time tick and one 16x oversample tick. A transmitter FSM drives a shift register to send a frame out least-significant-bit first: a start bit, eight data bits, and a stop bit, one bit per baud tick. The receiver is the harder half. It oversamples the idle-high line, catches the falling edge of the start bit, waits half a bit-time to reach the center of the cell, then samples every bit at mid-bit for maximum margin, deserializes into a shift register, and checks the stop bit for framing. You build all four pieces, then close a loopback and prove every received byte equals the byte sent, in SystemVerilog, Verilog, and VHDL.
Advanced20 min readRTL Design PatternsUARTBaud GeneratorShift RegisterFSMOversamplingMid-Bit SamplingFraming
Chapter 13 · Section 13.1 · Case Studies
1. The Engineering Problem
You have two chips that must exchange bytes, and between them run exactly two wires — one for each direction — with no shared clock. The transmitter cannot hand the receiver an edge to latch on; it can only agree, in advance, on a rate: so many bits per second, the baud rate. Everything the UART does exists to make a reliable byte pipe out of that one agreement. On the transmit side you must take a parallel byte and clock it out one bit at a time at the baud rate, wrapped in framing bits so the far end can find where each byte begins and ends. On the receive side you must recover those bytes from a lone wire, with only your own clock — which runs at a slightly different frequency than the sender's — to time the sampling by.
The frame is the protocol's contract. The line idles high (tx = 1). To send a byte the transmitter drives a START bit (a single 0, whose falling edge announces 'a byte is coming'), then the 8 DATA bits LSB-first (bit 0 first, bit 7 last), then a STOP bit (a single 1 that returns the line to idle and guarantees the next START's falling edge is visible). Ten bit-times for an 8-N-1 frame: 0 · d0 d1 d2 d3 d4 d5 d6 d7 · 1. The transmitter's job is to emit that sequence at exactly one bit per bit-time; the receiver's job is to sample each cell at the right instant and reassemble the byte.
The receive side is where the difficulty concentrates, and it is the whole reason a UART is a capstone and not a fourth FSM exercise. The receiver has no edge to align to except the START bit's falling edge, and its clock is not the transmitter's clock — the two drift. If it samples each bit at the boundary of the cell, even a tiny rate mismatch puts the sample point on the wrong side of the edge and it reads the previous or the next bit. The standard answer — the one that makes UARTs work across cheap oscillators — is to oversample: run an internal clock at 16 times the baud rate, use the START edge to find bit boundaries, and sample each data bit in the middle of its cell, where the margin against a rate mismatch is largest. Get that sample point wrong and the link works only when both clocks happen to be identical in simulation and fails in the field.
// ---- transmit side ----
input wire tx_start; // one-cycle pulse: send tx_data
input wire [7:0] tx_data; // the byte to transmit (sampled on tx_start)
output wire tx; // serial line out: idles high, START(0)+8 LSB-first+STOP(1)
output wire tx_busy; // high from acceptance of tx_start until frame end
// ---- receive side ----
input wire rx; // serial line in (from the far end's tx)
output wire [7:0] rx_data; // the received byte
output wire rx_valid; // one-cycle strobe when rx_data is fresh and framed OK
output wire frame_err; // STOP bit was not 1 -> mis-framed byte
// The frame on the wire, one bit per bit-time, at the agreed BAUD:
// idle=1 ... START=0 ... d0 d1 d2 d3 d4 d5 d6 d7 ... STOP=1 ... idle=1
// ^^^^^^^^^^^^^^^^^^^^^^^^^ 8 bits, LSB (d0) FIRST2. Mental Model
3. Pattern Anatomy
The UART is three blocks bound by two strobes. Take the frame first (what must appear on the wire and when), then the block diagram (who drives whom), then the two pieces of arithmetic that decide correctness: the baud-divisor math and the mid-bit sample point.
The frame — idle-high, START, data LSB-first, STOP. The line rests at 1. A byte is 0 (START) then d0..d7 (LSB-first) then 1 (STOP). The START bit's falling edge is the only synchronization event the receiver gets; the STOP bit's guaranteed 1 both returns the line to idle and lets the receiver validate framing — if the cell where STOP should be is not 1, the byte was mis-framed (a rate that drifted a whole bit, or noise). LSB-first is not a choice; it is the protocol, and it falls out for free if the transmitter loads the byte straight and shifts right.
The UART frame over time — idle-high line, START, 8 data LSB-first, STOP, back to idle
data flowThe block diagram — one baud generator, a TX path, an RX path. The baud generator divides the system clock into two strobes: baud_tick (once per bit-time, for the transmitter) and os_tick (16x per bit-time, for the receiver). The TX path is the 4.7 controller: an FSM sequences the frame, a shift register serializes the byte, a bit counter counts the data bits, tx is a Moore output. The RX path is the new work: an oversample counter running on os_tick, a start-edge detector on the (synchronized) rx line, a mid-bit sampler that deserializes into a shift register, a bit counter, and framing logic that checks the STOP cell and strobes rx_valid.
Block diagram — baud generator feeds the TX FSM/shifter and the RX oversampler/FSM/shifter
data flowThe baud-divisor math. With a system clock of CLK_HZ and a target BAUD, one bit-time is DIV = CLK_HZ / BAUD system clocks. A counter that counts 0 .. DIV-1 and emits a one-cycle baud_tick when it wraps produces exactly one tick per bit-time. The subtlety — and the second DebugLab row — is the reload boundary: a counter that counts to DIV (i.e. DIV+1 states) makes each bit-time one clock too long, and over ten bit-times that drift accumulates until a late sample lands in the wrong cell. The rule is exact: DIV states per bit means the counter runs 0 .. DIV-1 and ticks on the transition from DIV-1 back to 0. For the receiver, the oversample tick is os_tick every DIV/16 clocks, so there are exactly 16 os_ticks per baud_tick — which is what lets the receiver count os_ticks to locate the center of a bit.
The mid-bit sample point. The receiver's clock is not the transmitter's; call the mismatch e (a fraction of the bit period). If the receiver samples at the start of each cell, a mismatch e per bit accumulates over the frame, and by the last data bit the sample point has walked ~8e of a bit toward the neighbouring cell — with even a couple of percent mismatch the eighth bit is sampled in the wrong cell. If instead it samples at the center, it starts half a bit from either edge, so it can tolerate accumulated drift approaching half a bit before a sample crosses an edge — the maximum possible margin. Concretely: after the START falling edge the receiver counts 8 os_ticks (half of 16) to reach the center of the START cell, re-checks it is still 0, then samples every 16 os_ticks thereafter — each sample landing dead-center in a data cell, then the STOP cell.
The TX and RX state sequences. TX: IDLE -> START -> DATA (x8) -> STOP -> IDLE, advancing one state per baud_tick. RX: IDLE -> START (find center, re-check 0) -> DATA (x8, sample each at mid-bit) -> STOP (sample; frame_err = not 1) -> IDLE, advancing on os_tick with an os-counter marking the mid-bit points. The whole receiver reduces to that os-counter arithmetic; get it right and the byte reassembles, get it off by half a bit and you read edges.
4. Real RTL Implementation
This is the core of the page. We build the UART incrementally — (a) the baud generator, (b) the transmitter, (c) the receiver, then (d) the integrated, parameterized uart — and show the integrated block plus its loopback self-checking testbench and the RX mid-bit bug-vs-fix in SystemVerilog, Verilog, and VHDL. The idea is identical across the three; only the spelling differs.
Three conventions run through every block, each an earlier lesson made concrete:
- One system clock; two derived strobes.
baud_tick(once per bit) paces the transmitter;os_tick(16x per bit) paces the receiver. Every register advances on a strobe, not on a second clock — the clock-enable discipline of 3.5. - Synchronous, active-high reset to a safe idle. After reset
txidles high, both FSMs sit in IDLE, and the receiver'srxinput is passed through a two-flop synchronizer first (it is asynchronous to this clock) — otherwise a metastable start-edge would corrupt the very sync event the receiver depends on. - Sample DATA at MID-BIT, and check the STOP cell. The receiver's correctness is the os-counter arithmetic: center of the START cell at
os_cnt == 8, each data bit16os_ticks later, STOP the same.frame_erris the STOP cell not reading1.
4a. Building block (i) — the baud generator (a divider, from 3.5)
The generator is a single counter that emits one os_tick every DIV/16 clocks and one baud_tick every 16th os_tick — so the two strobes are phase-locked and there are exactly 16 os_ticks per bit. Counting 0 .. OS_DIV-1 (not 0 .. OS_DIV) is the off-by-one the second DebugLab row turns on.
module baud_gen #(
parameter int CLK_HZ = 1_600_000, // system clock
parameter int BAUD = 100_000, // target baud rate
parameter int OS = 16, // oversample factor (RX)
localparam int OS_DIV = CLK_HZ / (BAUD*OS) // clocks per os_tick (=DIV/16)
)(
input logic clk,
input logic rst, // synchronous, active-high
output logic os_tick, // 16x per bit-time (for RX)
output logic baud_tick // 1x per bit-time (for TX)
);
// 3.5: a modulo-OS_DIV counter. os_tick pulses one cycle when it WRAPS, so the
// period is exactly OS_DIV clocks. Counting 0..OS_DIV-1 (NOT 0..OS_DIV) is what
// keeps the bit period exact — an off-by-one here drifts the whole frame (§7).
logic [$clog2(OS_DIV)-1:0] os_cnt;
logic [$clog2(OS)-1:0] bit_cnt; // counts os_ticks: 16 per baud_tick
always_ff @(posedge clk) begin
if (rst) begin
os_cnt <= '0; bit_cnt <= '0; os_tick <= 1'b0; baud_tick <= 1'b0;
end else begin
os_tick <= 1'b0; // default: strobes are one cycle wide
baud_tick <= 1'b0;
if (os_cnt == OS_DIV-1) begin // WRAP at OS_DIV-1 -> exact period
os_cnt <= '0;
os_tick <= 1'b1; // one os_tick every OS_DIV clocks
if (bit_cnt == OS-1) begin // every 16th os_tick...
bit_cnt <= '0;
baud_tick <= 1'b1; // ...is one baud_tick (one per bit-time)
end else begin
bit_cnt <= bit_cnt + 1'b1;
end
end else begin
os_cnt <= os_cnt + 1'b1;
end
end
end
endmoduleThe rest of §4 uses os_tick and baud_tick as given; whether the generator is this counter or a fancier fractional divider, the transmitter and receiver only care that os_tick pulses 16 times per bit and baud_tick once. The integrated module in §4d instantiates exactly this generator.
4b. Building block (ii) — the transmitter (FSM + shift register, from 4.7)
The transmitter is the 4.7 worked controller verbatim: a safe FSM (IDLE, START, DATA, STOP), a shift register that loads tx_data and shifts right to serialize it LSB-first, a bit counter for the eight data bits, and a Moore tx. It advances only on baud_tick. This is shown here as part of the integrated uart (§4d); the standalone reasoning — leave DATA on bit_cnt == DATA_BITS-1, hold tx = 1 in IDLE/STOP — is exactly the 4.7 lesson and is not re-derived.
4c. Building block (iii) — the receiver (oversample, start-edge, mid-bit sample, deserialize, framing)
The receiver is the capstone's new work, so build it as a small FSM on os_tick. IDLE: hold, watching the synchronized rx line for a falling edge (1 -> 0). On that edge go to START and start an os-counter. In START, count os_ticks; at os_cnt == OS/2 (the center of the START cell, 8 for OS=16) re-check rx == 0 — if it went back high it was a glitch, so return to IDLE (start-bit re-validation); if still low, reset the os-counter and go to DATA. In DATA, count os_ticks and each time os_cnt reaches OS-1 (a full bit-time since the last sample, i.e. the center of the next cell) sample rx into the receive shift register, LSB-first, and increment the data-bit counter; after DATA_BITS samples go to STOP. In STOP, at the mid-bit of the STOP cell sample rx: frame_err = (rx != 1), then strobe rx_valid, publish rx_data, and return to IDLE. The full RTL is in the integrated module below.
4d. The integrated UART — parameterized, in SystemVerilog
One module: baud_gen + the TX FSM/shifter + the RX oversampler/FSM/shifter + framing, parameterized on CLK_HZ, BAUD, DATA_BITS, and OS. Read tx (Moore) and rx_data/rx_valid/frame_err off the two paths.
module uart #(
parameter int CLK_HZ = 1_600_000,
parameter int BAUD = 100_000,
parameter int DATA_BITS = 8,
parameter int OS = 16 // oversample factor
)(
input logic clk,
input logic rst, // synchronous, active-high
// TX side
input logic tx_start, // one-cycle: send tx_data
input logic [DATA_BITS-1:0] tx_data,
output logic tx, // serial out (idles high)
output logic tx_busy,
// RX side
input logic rx, // serial in (asynchronous)
output logic [DATA_BITS-1:0] rx_data,
output logic rx_valid, // one-cycle strobe
output logic frame_err // STOP bit was not 1
);
// ---- baud generator (3.5): os_tick 16x/bit, baud_tick 1x/bit ----
logic os_tick, baud_tick;
baud_gen #(.CLK_HZ(CLK_HZ), .BAUD(BAUD), .OS(OS))
bg (.clk(clk), .rst(rst), .os_tick(os_tick), .baud_tick(baud_tick));
// =========================== TRANSMITTER (4.7) ===========================
typedef enum logic [1:0] { T_IDLE, T_START, T_DATA, T_STOP } tstate_t;
tstate_t tstate;
logic [DATA_BITS-1:0] tx_sr; // 2.6: shift register, bit 0 = current bit
logic [$clog2(DATA_BITS)-1:0] tx_bit; // counts the data bits
always_ff @(posedge clk) begin
if (rst) begin
tstate <= T_IDLE; tx <= 1'b1; tx_busy <= 1'b0;
tx_sr <= '0; tx_bit <= '0;
end else begin
case (tstate)
T_IDLE: begin
tx <= 1'b1; tx_busy <= 1'b0;
if (tx_start) begin
tx_sr <= tx_data; // load the byte
tx_bit <= '0;
tx_busy <= 1'b1;
tstate <= T_START;
end
end
T_START: if (baud_tick) begin
tx <= 1'b0; // START bit = 0 for one bit-time
tstate <= T_DATA;
end else tx <= 1'b0;
T_DATA: begin
tx <= tx_sr[0]; // current serial bit (LSB-first)
if (baud_tick) begin
tx_sr <= {1'b0, tx_sr[DATA_BITS-1:1]}; // shift right
if (tx_bit == DATA_BITS-1) tstate <= T_STOP; // after 8th bit
else tx_bit <= tx_bit + 1'b1;
end
end
T_STOP: begin
tx <= 1'b1; // STOP bit = 1
if (baud_tick) begin tstate <= T_IDLE; tx_busy <= 1'b0; end
end
endcase
end
end
// ============================= RECEIVER ==================================
// Synchronize the asynchronous rx line (11.2 two-flop) before using it.
logic rx_s1, rx_s, rx_prev;
always_ff @(posedge clk) begin
if (rst) begin rx_s1 <= 1'b1; rx_s <= 1'b1; rx_prev <= 1'b1; end
else begin rx_s1 <= rx; rx_s <= rx_s1; rx_prev <= rx_s; end
end
typedef enum logic [1:0] { R_IDLE, R_START, R_DATA, R_STOP } rstate_t;
rstate_t rstate;
logic [$clog2(OS)-1:0] os_cnt; // 0..OS-1 within a bit cell
logic [$clog2(DATA_BITS)-1:0] rx_bit;
logic [DATA_BITS-1:0] rx_sr;
always_ff @(posedge clk) begin
if (rst) begin
rstate <= R_IDLE; os_cnt <= '0; rx_bit <= '0; rx_sr <= '0;
rx_data <= '0; rx_valid <= 1'b0; frame_err <= 1'b0;
end else begin
rx_valid <= 1'b0; // default: strobe is one cycle
case (rstate)
R_IDLE: if (rx_prev == 1'b1 && rx_s == 1'b0) begin // START falling edge
os_cnt <= '0; rstate <= R_START; // arm the sampler
end
R_START: if (os_tick) begin
if (os_cnt == (OS/2 - 1)) begin // CENTER of START cell
if (rx_s == 1'b0) begin // re-validate START
os_cnt <= '0; rx_bit <= '0; rstate <= R_DATA;
end else rstate <= R_IDLE; // glitch -> abort
end else os_cnt <= os_cnt + 1'b1;
end
R_DATA: if (os_tick) begin
if (os_cnt == OS-1) begin // MID-BIT of next cell
os_cnt <= '0;
rx_sr <= {rx_s, rx_sr[DATA_BITS-1:1]}; // deserialize LSB-first
if (rx_bit == DATA_BITS-1) rstate <= R_STOP;
else rx_bit <= rx_bit + 1'b1;
end else os_cnt <= os_cnt + 1'b1;
end
R_STOP: if (os_tick) begin
if (os_cnt == OS-1) begin // MID-BIT of STOP cell
frame_err <= ~rx_s; // STOP must be 1
rx_data <= rx_sr;
rx_valid <= rx_s; // publish only if framed
os_cnt <= '0;
rstate <= R_IDLE;
end else os_cnt <= os_cnt + 1'b1;
end
endcase
end
end
endmoduleTwo lines carry the receiver's correctness. os_cnt == (OS/2 - 1) locates the center of the START cell — half a bit-time (8 os_ticks) after the falling edge — where START is re-validated; os_cnt == OS-1 in DATA/STOP then lands every subsequent sample a full bit-time later, i.e. dead-center in each following cell. Sample at os_cnt == 0 (the cell edge) instead and the receiver reads bit boundaries — the §7 bug. The loopback testbench wires tx straight into rx and proves a random byte survives the round trip.
module uart_loopback_tb;
localparam int CLK_HZ = 1_600_000, BAUD = 100_000, DATA_BITS = 8, OS = 16;
logic clk = 1'b0, rst, tx_start, tx, tx_busy, rx_valid, frame_err;
logic [DATA_BITS-1:0] tx_data, rx_data;
int errors = 0, sent = 0;
// LOOPBACK: the transmitter's serial output feeds the receiver's serial input.
uart #(.CLK_HZ(CLK_HZ), .BAUD(BAUD), .DATA_BITS(DATA_BITS), .OS(OS)) dut (
.clk(clk), .rst(rst), .tx_start(tx_start), .tx_data(tx_data),
.tx(tx), .tx_busy(tx_busy),
.rx(tx), // <-- loopback: rx = tx
.rx_data(rx_data), .rx_valid(rx_valid), .frame_err(frame_err));
always #5 clk = ~clk; // system clock
// Check every received byte against the byte we sent.
logic [DATA_BITS-1:0] expect_q[$]; // queue of bytes in flight
always @(posedge clk) if (rx_valid) begin
logic [DATA_BITS-1:0] exp = expect_q.pop_front();
if (frame_err) begin $error("frame_err on 0x%02h", exp); errors++; end
if (rx_data !== exp) begin $error("rx=0x%02h exp=0x%02h", rx_data, exp); errors++; end
end
task automatic send_byte(input logic [DATA_BITS-1:0] b);
@(posedge clk); tx_data = b; tx_start = 1'b1; expect_q.push_back(b);
@(posedge clk); tx_start = 1'b0;
wait (!tx_busy); // let the frame finish (back-to-back capable)
@(posedge clk);
endtask
initial begin
rst = 1'b1; tx_start = 1'b0; tx_data = '0;
repeat (4) @(posedge clk); rst = 1'b0;
// sweep corner bytes + random, back-to-back
send_byte(8'h00); send_byte(8'hFF); send_byte(8'hB5); send_byte(8'h01); send_byte(8'h80);
for (int i = 0; i < 16; i++) send_byte($urandom);
sent = expect_q.size();
// drain any last in-flight byte
repeat (40*OS) @(posedge clk);
if (errors == 0) $display("PASS: all bytes survived TX->RX loopback (rx==tx, framed OK)");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmodule4e. The signature bug — RX samples at the bit EDGE, not MID-BIT (buggy vs fixed)
Every UART failure that survives a bench test lives in the receiver's sample point. The sharpest is sampling at the cell edge (or the start of the cell) instead of its center: with the transmitter's clock even slightly off the receiver's, the sample lands right on a bit boundary and occasionally catches the previous or next bit. It 'works' only when the two clocks are identical in simulation — and drifts into garbage the instant they are not.
// BUGGY RX DATA phase: after START, it samples IMMEDIATELY at the cell boundary
// (os_cnt == 0) instead of the center, so every data bit is read at its EDGE.
// With ANY baud mismatch the sample sits on the transition and reads the wrong cell.
module rx_data_bad #(parameter int OS = 16, parameter int DATA_BITS = 8) (
input logic clk, os_tick, in_data,
input logic [$clog2(OS)-1:0] os_cnt,
input logic [$clog2(DATA_BITS)-1:0] rx_bit,
output logic sample_now
);
// BUG: fires at os_cnt == 0 -> the START of the cell, i.e. the bit EDGE.
assign sample_now = os_tick & (os_cnt == '0);
endmodule
// FIXED RX DATA phase: after re-validating START at its center, it samples every
// full bit-time later (os_cnt == OS-1) -> the MID-BIT of each following cell, the
// point of maximum margin against a baud mismatch.
module rx_data_good #(parameter int OS = 16, parameter int DATA_BITS = 8) (
input logic clk, os_tick, in_data,
input logic [$clog2(OS)-1:0] os_cnt,
input logic [$clog2(DATA_BITS)-1:0] rx_bit,
output logic sample_now
);
// FIX: fires at os_cnt == OS-1 -> a full bit after the last sample = mid-bit.
assign sample_now = os_tick & (os_cnt == OS-1);
endmodule module uart_rx_sample_tb;
// Drive the DUT with a slightly FASTER transmitter than the receiver expects, so
// the sample point drifts. The MID-BIT receiver tolerates it; the EDGE one fails.
localparam int CLK_HZ = 1_600_000, BAUD = 100_000, OS = 16;
logic clk = 1'b0, rst, tx_start, tx, tx_busy, rx_valid, frame_err;
logic [7:0] tx_data, rx_data;
int errors = 0;
// Receiver clocked ~3% slower than the transmitter's baud implies (BAUD_RX slightly off).
uart #(.CLK_HZ(CLK_HZ), .BAUD(103_000), .DATA_BITS(8), .OS(OS)) dut (
.clk(clk), .rst(rst), .tx_start(tx_start), .tx_data(tx_data),
.tx(tx), .tx_busy(tx_busy), .rx(tx),
.rx_data(rx_data), .rx_valid(rx_valid), .frame_err(frame_err));
always #5 clk = ~clk;
always @(posedge clk) if (rx_valid) begin
if (rx_data !== 8'hB5 || frame_err)
begin $error("garbled: rx=0x%02h err=%b (edge sampler would fail here)", rx_data, frame_err); errors++; end
end
initial begin
rst = 1'b1; tx_start = 0; tx_data = 8'hB5;
repeat (4) @(posedge clk); rst = 0;
@(posedge clk); tx_start = 1; @(posedge clk); tx_start = 0;
repeat (40*OS) @(posedge clk);
// The FIXED (mid-bit) DUT tolerates the ~3% mismatch: rx_data === 0xB5.
// Rebuilt with the EDGE sampler, the same stimulus reads a shifted/garbled byte.
if (errors == 0) $display("PASS: mid-bit sampler recovers 0xB5 despite baud mismatch");
else $display("FAIL: %0d — sampling off-center reads the neighbouring cell", errors);
$finish;
end
endmodule4f. The integrated UART in Verilog
The same three blocks with reg/wire typing and localparam states. The receiver's os-counter arithmetic — center at OS/2-1, mid-bit at OS-1 — is unchanged; only the spelling differs.
module uart #(
parameter CLK_HZ = 1600000, BAUD = 100000, DATA_BITS = 8, OS = 16
)(
input wire clk, rst,
input wire tx_start,
input wire [DATA_BITS-1:0] tx_data,
output reg tx, tx_busy,
input wire rx,
output reg [DATA_BITS-1:0] rx_data,
output reg rx_valid, frame_err
);
localparam OS_DIV = CLK_HZ / (BAUD*OS); // clocks per os_tick
// ---- baud generator (3.5) ----
reg [15:0] os_c; reg [4:0] os_bit; reg os_tick, baud_tick;
always @(posedge clk) begin
if (rst) begin os_c<=0; os_bit<=0; os_tick<=0; baud_tick<=0; end
else begin
os_tick<=0; baud_tick<=0;
if (os_c == OS_DIV-1) begin // WRAP at OS_DIV-1 (off-by-one -> drift, §7)
os_c<=0; os_tick<=1;
if (os_bit == OS-1) begin os_bit<=0; baud_tick<=1; end
else os_bit<=os_bit+1'b1;
end else os_c<=os_c+1'b1;
end
end
// ---- transmitter (4.7): FSM + shift register ----
localparam [1:0] TI=0, TS=1, TD=2, TP=3;
reg [1:0] ts; reg [DATA_BITS-1:0] tsr; reg [3:0] tbit;
always @(posedge clk) begin
if (rst) begin ts<=TI; tx<=1'b1; tx_busy<=1'b0; tsr<=0; tbit<=0; end
else case (ts)
TI: begin tx<=1'b1; tx_busy<=1'b0;
if (tx_start) begin tsr<=tx_data; tbit<=0; tx_busy<=1'b1; ts<=TS; end end
TS: begin tx<=1'b0; if (baud_tick) ts<=TD; end
TD: begin tx<=tsr[0];
if (baud_tick) begin tsr<={1'b0,tsr[DATA_BITS-1:1]}; // shift right, LSB-first
if (tbit==DATA_BITS-1) ts<=TP; else tbit<=tbit+1'b1; end end
TP: begin tx<=1'b1; if (baud_tick) begin ts<=TI; tx_busy<=1'b0; end end
endcase
end
// ---- receiver: synchronize rx, then oversample/FSM/shifter/framing ----
reg rx1, rxs, rxp;
always @(posedge clk) begin
if (rst) begin rx1<=1'b1; rxs<=1'b1; rxp<=1'b1; end
else begin rx1<=rx; rxs<=rx1; rxp<=rxs; end
end
localparam [1:0] RI=0, RS=1, RD=2, RP=3;
reg [1:0] rs; reg [4:0] osc; reg [3:0] rbit; reg [DATA_BITS-1:0] rsr;
always @(posedge clk) begin
if (rst) begin rs<=RI; osc<=0; rbit<=0; rsr<=0; rx_data<=0; rx_valid<=0; frame_err<=0; end
else begin
rx_valid<=1'b0;
case (rs)
RI: if (rxp==1'b1 && rxs==1'b0) begin osc<=0; rs<=RS; end // START falling edge
RS: if (os_tick) begin
if (osc == (OS/2 - 1)) begin // CENTER of START
if (rxs==1'b0) begin osc<=0; rbit<=0; rs<=RD; end // re-validate
else rs<=RI; end // glitch -> abort
else osc<=osc+1'b1; end
RD: if (os_tick) begin
if (osc == OS-1) begin // MID-BIT sample
osc<=0; rsr<={rxs, rsr[DATA_BITS-1:1]}; // deserialize LSB-first
if (rbit==DATA_BITS-1) rs<=RP; else rbit<=rbit+1'b1; end
else osc<=osc+1'b1; end
RP: if (os_tick) begin
if (osc == OS-1) begin // MID-BIT of STOP
frame_err<=~rxs; rx_data<=rsr; rx_valid<=rxs; // publish if framed
osc<=0; rs<=RI; end
else osc<=osc+1'b1; end
endcase
end
end
endmodule module uart_loopback_tb;
parameter CLK_HZ=1600000, BAUD=100000, DATA_BITS=8, OS=16;
reg clk, rst, tx_start; reg [7:0] tx_data;
wire tx, tx_busy, rx_valid, frame_err; wire [7:0] rx_data;
integer errors, k;
reg [7:0] last_sent;
uart #(.CLK_HZ(CLK_HZ),.BAUD(BAUD),.DATA_BITS(DATA_BITS),.OS(OS)) dut (
.clk(clk),.rst(rst),.tx_start(tx_start),.tx_data(tx_data),
.tx(tx),.tx_busy(tx_busy),.rx(tx), // loopback rx=tx
.rx_data(rx_data),.rx_valid(rx_valid),.frame_err(frame_err));
initial clk=1'b0; always #5 clk=~clk;
// On each received byte, compare against the last byte sent (one in flight at a time).
always @(posedge clk) if (rx_valid) begin
if (frame_err) begin $display("FAIL: frame_err on %02h", last_sent); errors=errors+1; end
if (rx_data !== last_sent) begin $display("FAIL: rx=%02h exp=%02h", rx_data, last_sent); errors=errors+1; end
end
task send_byte; input [7:0] b; begin
@(posedge clk); tx_data=b; last_sent=b; tx_start=1'b1;
@(posedge clk); tx_start=1'b0;
wait (!tx_busy); repeat (2*OS) @(posedge clk); // let RX finish the frame
end endtask
initial begin
errors=0; rst=1'b1; tx_start=1'b0; tx_data=8'h00;
repeat (4) @(posedge clk); rst=1'b0;
send_byte(8'h00); send_byte(8'hFF); send_byte(8'hB5);
send_byte(8'h01); send_byte(8'h80); send_byte(8'h5A);
if (errors==0) $display("PASS: all bytes survived TX->RX loopback (rx==tx, framed OK)");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleThe Verilog RX bug-vs-fix pair is the same one-line change to the sample instant: osc == 0 (edge) is broken, osc == OS-1 (mid-bit) is correct.
// BUGGY: sample at the START of the cell -> reads the bit EDGE; any baud mismatch garbles.
module rx_data_bad #(parameter OS=16) (
input wire os_tick, input wire [4:0] osc, output wire sample_now );
assign sample_now = os_tick & (osc == 5'd0); // BUG: cell edge
endmodule
// FIXED: sample a full bit after the last -> the MID-BIT, maximum margin vs mismatch.
module rx_data_good #(parameter OS=16) (
input wire os_tick, input wire [4:0] osc, output wire sample_now );
assign sample_now = os_tick & (osc == OS-1); // FIX: mid-bit
endmodule4g. The integrated UART in VHDL
VHDL states the same machines with enumerated state types, numeric_std counters, and rising_edge(clk). The receiver samples at mid-bit with the identical os-counter guards.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity uart is
generic ( CLK_HZ : integer := 1600000; BAUD : integer := 100000;
DATA_BITS : integer := 8; OS : integer := 16 );
port (
clk, rst : in std_logic;
tx_start : in std_logic;
tx_data : in std_logic_vector(DATA_BITS-1 downto 0);
tx : out std_logic;
tx_busy : out std_logic;
rx : in std_logic;
rx_data : out std_logic_vector(DATA_BITS-1 downto 0);
rx_valid : out std_logic;
frame_err : out std_logic );
end entity;
architecture rtl of uart is
constant OS_DIV : integer := CLK_HZ / (BAUD*OS); -- clocks per os_tick
signal os_tick, baud_tick : std_logic;
-- baud generator counters
signal os_c : integer range 0 to OS_DIV-1 := 0;
signal os_bit : integer range 0 to OS-1 := 0;
-- TX
type tstate_t is (T_IDLE, T_START, T_DATA, T_STOP);
signal ts : tstate_t := T_IDLE;
signal tsr : std_logic_vector(DATA_BITS-1 downto 0) := (others=>'0');
signal tbit : integer range 0 to DATA_BITS-1 := 0;
-- RX
signal rx1, rxs, rxp : std_logic := '1';
type rstate_t is (R_IDLE, R_START, R_DATA, R_STOP);
signal rs : rstate_t := R_IDLE;
signal osc : integer range 0 to OS-1 := 0;
signal rbit : integer range 0 to DATA_BITS-1 := 0;
signal rsr : std_logic_vector(DATA_BITS-1 downto 0) := (others=>'0');
begin
-- ---- baud generator (3.5): WRAP at OS_DIV-1 (off-by-one -> frame drift, §7) ----
baud_p : process (clk) begin
if rising_edge(clk) then
if rst = '1' then
os_c <= 0; os_bit <= 0; os_tick <= '0'; baud_tick <= '0';
else
os_tick <= '0'; baud_tick <= '0';
if os_c = OS_DIV-1 then
os_c <= 0; os_tick <= '1';
if os_bit = OS-1 then os_bit <= 0; baud_tick <= '1';
else os_bit <= os_bit + 1; end if;
else os_c <= os_c + 1; end if;
end if;
end if;
end process;
-- ---- transmitter (4.7): FSM + shift register ----
tx_p : process (clk) begin
if rising_edge(clk) then
if rst = '1' then
ts <= T_IDLE; tx <= '1'; tx_busy <= '0'; tsr <= (others=>'0'); tbit <= 0;
else
case ts is
when T_IDLE => tx <= '1'; tx_busy <= '0';
if tx_start = '1' then tsr <= tx_data; tbit <= 0; tx_busy <= '1'; ts <= T_START; end if;
when T_START => tx <= '0'; if baud_tick = '1' then ts <= T_DATA; end if;
when T_DATA => tx <= tsr(0);
if baud_tick = '1' then
tsr <= '0' & tsr(DATA_BITS-1 downto 1); -- shift right, LSB-first
if tbit = DATA_BITS-1 then ts <= T_STOP; else tbit <= tbit + 1; end if;
end if;
when T_STOP => tx <= '1'; if baud_tick = '1' then ts <= T_IDLE; tx_busy <= '0'; end if;
end case;
end if;
end if;
end process;
-- ---- receiver: synchronize rx, oversample, mid-bit sample, deserialize, framing ----
rx_p : process (clk) begin
if rising_edge(clk) then
if rst = '1' then
rx1<='1'; rxs<='1'; rxp<='1';
rs<=R_IDLE; osc<=0; rbit<=0; rsr<=(others=>'0');
rx_data<=(others=>'0'); rx_valid<='0'; frame_err<='0';
else
rx1 <= rx; rxs <= rx1; rxp <= rxs; -- two-flop synchronizer
rx_valid <= '0';
case rs is
when R_IDLE => if rxp='1' and rxs='0' then osc<=0; rs<=R_START; end if; -- START edge
when R_START => if os_tick='1' then
if osc = (OS/2 - 1) then -- CENTER of START
if rxs='0' then osc<=0; rbit<=0; rs<=R_DATA; -- re-validate
else rs<=R_IDLE; end if; -- glitch -> abort
else osc<=osc+1; end if;
end if;
when R_DATA => if os_tick='1' then
if osc = OS-1 then -- MID-BIT sample
osc<=0; rsr <= rxs & rsr(DATA_BITS-1 downto 1); -- deserialize LSB-first
if rbit = DATA_BITS-1 then rs<=R_STOP; else rbit<=rbit+1; end if;
else osc<=osc+1; end if;
end if;
when R_STOP => if os_tick='1' then
if osc = OS-1 then -- MID-BIT of STOP
frame_err <= not rxs; rx_data <= rsr; rx_valid <= rxs;
osc<=0; rs<=R_IDLE;
else osc<=osc+1; end if;
end if;
end case;
end if;
end if;
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity uart_loopback_tb is end entity;
architecture sim of uart_loopback_tb is
constant DATA_BITS : integer := 8;
signal clk : std_logic := '0';
signal rst, tx_start, tx, tx_busy, rx_valid, frame_err : std_logic;
signal tx_data, rx_data : std_logic_vector(DATA_BITS-1 downto 0);
signal last_sent : std_logic_vector(DATA_BITS-1 downto 0) := (others=>'0');
begin
-- LOOPBACK: rx driven by tx.
dut : entity work.uart
generic map (CLK_HZ=>1600000, BAUD=>100000, DATA_BITS=>DATA_BITS, OS=>16)
port map (clk=>clk, rst=>rst, tx_start=>tx_start, tx_data=>tx_data,
tx=>tx, tx_busy=>tx_busy, rx=>tx,
rx_data=>rx_data, rx_valid=>rx_valid, frame_err=>frame_err);
clk <= not clk after 5 ns;
-- checker: every received byte must equal the last byte sent, framed OK
check : process (clk) begin
if rising_edge(clk) and rx_valid = '1' then
assert frame_err = '0' report "frame_err asserted" severity error;
assert rx_data = last_sent report "rx_data /= sent byte" severity error;
end if;
end process;
stim : process
procedure send_byte (b : std_logic_vector(DATA_BITS-1 downto 0)) is
begin
wait until rising_edge(clk); tx_data <= b; last_sent <= b; tx_start <= '1';
wait until rising_edge(clk); tx_start <= '0';
wait until tx_busy = '0'; -- frame started/finished
for i in 0 to 40*16 loop wait until rising_edge(clk); end loop;
end procedure;
begin
rst <= '1'; tx_start <= '0'; tx_data <= (others=>'0');
for i in 0 to 3 loop wait until rising_edge(clk); end loop; rst <= '0';
send_byte(x"00"); send_byte(x"FF"); send_byte(x"B5");
send_byte(x"01"); send_byte(x"80"); send_byte(x"5A");
report "uart loopback self-check complete (rx == tx, framed OK)" severity note;
wait;
end process;
end architecture;The VHDL RX bug-vs-fix pair is the same one-character change to the sample instant: sampling at osc = 0 reads the cell edge and garbles under any baud mismatch; sampling at osc = OS-1 reads the mid-bit.
library ieee;
use ieee.std_logic_1164.all;
-- BUGGY: sample at the START of the cell -> the bit EDGE; any baud mismatch garbles.
entity rx_data_bad is
generic ( OS : integer := 16 );
port ( os_tick : in std_logic;
osc : in integer range 0 to OS-1;
sample_now : out std_logic );
end entity;
architecture rtl of rx_data_bad is
begin
-- BUG: fires at the cell edge (osc = 0), zero margin against drift.
sample_now <= '1' when (os_tick = '1' and osc = 0) else '0';
end architecture;
-- FIXED: sample a full bit after the last -> the MID-BIT, maximum margin.
entity rx_data_good is
generic ( OS : integer := 16 );
port ( os_tick : in std_logic;
osc : in integer range 0 to OS-1;
sample_now : out std_logic );
end entity;
architecture rtl of rx_data_good is
begin
-- FIX: fires at osc = OS-1, a full bit after the last sample = mid-bit.
sample_now <= '1' when (os_tick = '1' and osc = OS-1) else '0';
end architecture;Across all three languages the receiver's whole correctness is one sentence: find the center of the START cell (half an oversample period after the START edge), re-validate it is still low, then sample every full bit-time at MID-BIT — the point of maximum margin against a baud mismatch — and check the STOP cell for framing.
5. Verification Strategy
A UART's correctness is not 'did the first bits look right' — it is 'does a byte pushed into the transmitter come out of the receiver identical, framed correctly, across the clock ratios and small baud mismatches you will actually meet.' The one invariant, and the whole §4 loopback testbench, is:
Wire the transmitter's serial output straight into the receiver's serial input, send a byte, and the byte the receiver produces (
rx_datawhenrx_validstrobes) must equal the byte sent, withframe_errlow — for every byte, at every supported clock-to-baud ratio, and within the baud tolerance the mid-bit sample point buys you.
Everything below is that invariant made checkable, and it maps onto the loopback testbenches in §4.
- Loopback self-check (the core test). Connect
txtorx, push a byte withtx_start, and on eachrx_validcomparerx_dataagainst the byte you sent (a queue of in-flight bytes, or one at a time). Sweep corner bytes —0x00(all-zero data),0xFF(all-one),0xB5(mixed),0x01,0x80— plus random bytes, because an all-0or all-1byte can hide a bit-count or ordering error that0xB5exposes. The three testbenches do exactly this (SVassert (rx_data === exp), Verilogif (rx_data !== last_sent) $display("FAIL…"), VHDLassert rx_data = last_sent … severity error). - Multiple CLK/baud ratios. Re-run the loopback at a couple of
CLK_HZ / BAUDratios (e.g.DIV = 16andDIV = 174). A design that passes at one ratio can fail at another if the os-divisor or the mid-bit arithmetic assumed a specificDIV— verify generic, do not assume generic. - Baud-mismatch tolerance (the mid-bit payoff). Instantiate the receiver at a baud a few percent off the transmitter's (as §4e does) and confirm the byte still recovers. A correct mid-bit receiver tolerates roughly ±(half a bit / number of bits) of accumulated drift; the §7 edge-sampler fails immediately under the same mismatch. This is the test that distinguishes a UART that works on the bench from one that works in the field.
- Framing and re-validation. Force a mis-framed byte (drive the STOP cell low, or a wrong
DIV) and assertframe_errgoes high andrx_validdoes not publish a bad byte. Drive a lone glitch low on the idle line (shorter than half a bit) and confirm the START re-validation at mid-cell rejects it — no phantom byte. - Back-to-back frames. Send a second
tx_startas soon astx_busyfalls and confirm both bytes arrive intact — the receiver must return to IDLE and re-arm on the next START edge without dropping the following frame. - Expected waveform.
txidles high, drops for one bit-time (START), presentsd0..d7each for one bit-time (DATA), rises for one bit-time (STOP), idles high. On the receive side,rx_validpulses once, one bit-time after the STOP cell's center, withrx_dataequal to the transmitted byte andframe_errlow. Anrx_datathat is bit-shifted or garbled only when the two clocks differ is the visual signature of the §7 edge-sampling bug; a byte that drifts wrong on long frames is the baud-divisor off-by-one.
6. Common Mistakes
RX sampling at the wrong point — the bit edge instead of mid-bit. The signature failure and the reason the receiver is a capstone. Sampling each data bit at the start or edge of its cell (rather than the center) leaves zero margin: with the transmitter's clock even slightly faster or slower, the sample lands on the bit boundary and catches the previous or next bit. It 'passes' only when both clocks are bit-identical in simulation and garbles the moment they are not. Sample at the center: count half an oversample period (8 of 16 os_ticks) after the START edge to reach the middle of the START cell, then sample every full bit-time thereafter. Full post-mortem in §7 row 1; buggy-vs-fixed RTL in §4e.
Baud-divisor off-by-one — reload at DIV instead of DIV-1. A counter that counts 0 .. DIV (that is DIV+1 states) makes each bit-time one system clock too long; one that misses a reload makes it one too short. Either way each bit is slightly the wrong length, and over ten bit-times the error accumulates until a late (or early) sample lands in the wrong cell — the last data bit or the STOP cell is where it shows. The rule is exact: DIV clocks per bit means the counter runs 0 .. DIV-1 and ticks on the wrap from DIV-1 to 0. Post-mortem in §7 row 2.
Missing the START-bit re-validation. The START edge is the only sync event, so a glitch on the idle line — a brief low from noise — can be mistaken for a START and launch a phantom byte. The fix is to re-check the line at the center of the START cell: if it has gone back high, it was a glitch, abort to IDLE. Skipping this re-check means every line glitch produces a spurious rx_valid (usually with frame_err, but sometimes a plausible-looking byte).
STOP-bit / framing not checked. The STOP cell must read 1; if it does not, the frame drifted a whole bit (or the line was noisy) and the byte is suspect. A receiver that ignores the STOP cell accepts a mis-framed byte silently and, worse, may have lost sync so every following byte is wrong too. Sample the STOP cell at its center, set frame_err if it is not 1, and gate rx_valid on a good frame.
Not synchronizing the rx input. The rx line is asynchronous to the receiver's clock — it comes from another chip's oscillator. Feeding it directly into the start-edge detector means the very edge the receiver synchronizes on can be sampled metastable, corrupting the one event the whole receiver depends on. Pass rx through a two-flop synchronizer first (11.2) and detect the edge on the synchronized signal.
LSB/MSB order flipped. UART is LSB-first. If the transmitter shifts left (presenting shift_reg[MSB]) or the receiver deserializes into the wrong end, every byte arrives bit-reversed. Load the byte straight and shift right on TX so shift_reg[0] is data[0] first; on RX, shift the sampled bit into the top and let it walk down, so the first bit sampled ends up as the LSB. The direction must be deliberate and matched on both ends.
7. DebugLab
The UART that works in sim and dies in the field — an off-center sample point and a drifting bit clock
The engineering lesson: a UART is correct only if two independent clocks stay aligned to the bit cells across the whole frame — and both signature bugs are the same crime, a sample point that walks off the cell, committed at two different stations. The first walks it off within the cell (sampling at the edge instead of the center, so there is no margin); the second walks it off across the cell (a bit period that is one clock wrong, so the drift accumulates to the frame's tail). Both hide in a shared-clock loopback and both surface the instant a real, differently-clocked partner appears. Three habits make the receiver trustworthy, identical across SystemVerilog, Verilog, and VHDL: sample every bit at MID-BIT (half an oversample period after the START edge, then every full bit-time), make the bit period exact (reload the divisor at DIV-1, not DIV), and verify against a baud MISMATCH, not just a shared clock — a loopback where the receiver runs a few percent off is the test that separates a UART that works on the bench from one that works in the field.
8. Interview Q&A
9. Exercises
Design and reasoning exercises — no syntax drills. Each rehearses composing the pieces and getting the timing accounting right.
Exercise 1 — Size the baud tolerance
For an 8-N-1 frame (1 START + 8 DATA + 1 STOP) with mid-bit sampling, derive the maximum baud-rate mismatch between transmitter and receiver that still recovers every byte. State (i) how far the last-bit sample has drifted as a function of the per-bit error e, (ii) the condition that keeps it inside the cell, and (iii) the resulting tolerance as a percentage. Then say how the tolerance changes for an 8-E-2 frame (parity + two stop bits) and why longer frames are less forgiving.
Exercise 2 — Predict the off-center failure
An engineer samples each data bit at os_cnt == 4 (a quarter of the way into a 16x cell) instead of the center. Describe precisely (i) what a shared-clock loopback reports, (ii) what happens against a receiver clocked 2% slower than the transmitter, and (iii) which bits of the frame fail first and why. Then give the one-line fix and the testbench change that would have exposed it.
Exercise 3 — Make the oversample factor a parameter
Turn the fixed OS = 16 receiver into one generic on OS (support 8x and 16x). State (i) the new width of os_cnt and the START-center and mid-bit guard constants in terms of OS, (ii) why 8x oversampling gives a coarser sample point and less mismatch tolerance than 16x, and (iii) what you must re-verify to trust the generic version. Give the START-center guard as an expression in OS.
Exercise 4 — Add a receive-side FIFO
The current receiver produces one byte per rx_valid strobe; if the consumer is slow, the next byte overwrites rx_data. Add a small FIFO between the receiver and the consumer. State (i) where the FIFO's write strobe comes from, (ii) how deep it must be to survive a burst of B back-to-back frames while the consumer is stalled, and (iii) what new flag the receiver needs when the FIFO is full and a byte arrives (an overrun error). Name which earlier pattern the FIFO and the overrun flag draw on.
10. Related Tutorials
The patterns this UART composes (the lessons you are assembling here):
- Shift Registers — Chapter 2.6; the PISO serializer on the transmit side and the SIPO deserializer on the receive side — load, shift, present the bit.
- Clock Dividers & Timers — Chapter 3.5; the baud generator —
DIV = CLK_HZ / BAUDforbaud_tickandDIV/16for the 16xos_tick. - Pulse & Strobe Generators — Chapter 3.x; the one-cycle
baud_tick/os_tick/rx_validstrobes that pace and mark every event in the design. - FSM Worked Controller — Chapter 4.7; the transmitter is this UART-TX-lite controller verbatim — the safe FSM + shift register + bit counter you now reuse.
- FSM Coding Styles — Chapter 4.3; the two-/three-block style behind both the TX and RX state machines.
- FSM + Datapath — Chapter 4.6; the control-down / status-up split between each FSM and its shift register / counter.
- Datapath / Control Split — Chapter 4.x; the discipline of separating the sequencing brain from the shifting/counting datapath, applied twice here.
- Parameter Patterns — Chapter 6.x; how
CLK_HZ,BAUD,DATA_BITS, andOSparameterize one module across baud rates and frame formats.
Backward / method:
- The RTL Design Mindset — Chapter 0.2; the Interface → State → Datapath → Control → Verification lenses this case study walks end to end.
- What Is an RTL Design Pattern? — Chapter 0.1; why composing patterns into a real peripheral — not inventing a new one — is the design skill this capstone rehearses.
Forward — the other case studies (unlock as they ship):
- SPI Master Case Study (
/rtl-design-patterns/case-study-spi-master) — Chapter 13.2; the same FSM + shift register composition, but source-synchronous (a clock travels with the data) instead of oversampled. - Streaming FIFO Case Study (
/rtl-design-patterns/case-study-streaming-fifo) — Chapter 13.3; buffering a byte stream like the one this UART produces. - Arbitered Bus Case Study (
/rtl-design-patterns/case-study-arbitered-bus) — Chapter 13.4; many masters, one shared resource, resolved by an arbiter. - CDC Stream Case Study (
/rtl-design-patterns/case-study-cdc-stream) — Chapter 13.5; moving a stream safely across two unrelated clocks.
Verilog v1 prerequisites (the grammar these controllers transcribe into):
- Case Statements — the construct behind the TX and RX next-state and output
caseblocks. - Blocking and Non-Blocking Assignments —
<=in the clocked FSM/datapath registers throughout the design.
11. Summary
- A UART is patterns composed into a real peripheral, not a new pattern. It is a baud generator (3.5) feeding a transmitter (the 4.7 safe FSM + shift register 2.6) and a receiver (an oversampler + FSM + shift register + framing). Each block is a lesson you already had; the capstone is wiring them with the timing exact.
- The frame is fixed. Idle-high line, START (
0), 8 DATA bits LSB-first, STOP (1), one bit per bit-time. The START's falling edge is the receiver's only sync event; the STOP's1returns the line to idle and lets the receiver check framing. - The baud math must be exact.
DIV = CLK_HZ / BAUDclocks per bit-time;DIV/16clocks peros_tick, so 16 os_ticks per bit. Reload the divider atDIV-1, notDIV, or every bit is a hair too long and the frame drifts late — worst at the tail (§7 row 2). - The receiver's whole correctness is the sample point. Oversample 16x, find the center of the START cell (8 os_ticks after the falling edge), re-validate START, then sample every full bit-time at MID-BIT — the point of maximum margin against a baud mismatch. Sample at the cell edge and the link works only when both clocks are identical in sim and garbles in the field (§7 row 1). Synchronize
rxthrough two flops first. - Prove it with a mismatched-clock loopback, not a shared clock. Wire
txtorx, send random and corner bytes, and assertrx_data == tx_datawithframe_errlow — at a couple of CLK/baud ratios and with the receiver a few percent off the transmitter's baud. That mismatch test is what separates a UART that works on the bench from one that works in silicon — self-checking loopback testbenches shown in SystemVerilog, Verilog, and VHDL.