Skip to content

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.

the-interface.v — one byte in, a framed stream out; a framed stream in, one byte out
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // ---- 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) FIRST

2. 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 flow
The UART frame over time — idle-high line, START, 8 data LSB-first, STOP, back to idlefalling edgeafter 1 bit-timeafter 8 bit-timescheck STOP == 1IDLE = 1line rests high; a falling edge means a byte startsSTART = 01 bit-time low; its FALLING edge syncs the receiverd0..d7 LSB8 bit-times; d0 (LSB) first, d7 last; sampled at MID-BITSTOP = 11 bit-time high; if not 1 -> frame_err; returns line to idleIDLE = 1line high again; next START edge is now visible
One 8-N-1 frame, left to right in time, one cell per bit-time at the agreed baud. The line idles at 1; the START bit is a single 0 whose falling edge is the ONLY event the receiver can synchronize to; the eight data bits follow LSB-first (d0 first); the STOP bit is a single 1 that both returns the line to idle and lets the receiver check framing. The receiver samples each DATA cell at its CENTER (mid-bit), not its edge — the point of maximum margin against a baud mismatch. Get the sample point wrong and a drifting clock reads the neighbouring cell. This frame is identical across SystemVerilog, Verilog, and VHDL; only the RTL that produces and consumes it differs in spelling.

The 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 flow
Block diagram — baud generator feeds the TX FSM/shifter and the RX oversampler/FSM/shifterbaud_tickload / shift_enshift_reg[0] -> txos_tick (16x)mid-bit sample pulsebaud gen (3.5)DIV = CLK_HZ/BAUD -> baud_tick; DIV/16 -> os_tick (16x)TX FSM (4.7)IDLE/START/DATA/STOP, Moore tx, advances on baud_tickTX shifter (2.6)loads tx_data, shifts right -> tx = LSB firstRX oversampleros_cnt on os_tick; START edge -> center at os_cnt==8RX shifter (2.6)samples at MID-BIT, deserializes rx_data, checks STOP
One baud generator, two consumers. The generator divides the fast system clock into a bit-rate baud_tick (for the transmitter) and a 16x oversample os_tick (for the receiver). The TX path is exactly the 4.7 worked controller: an FSM walks IDLE -> START -> DATA -> STOP advancing on baud_tick, a shift register serializes tx_data LSB-first, and tx is a Moore output. The RX path is the capstone's new work: an oversample counter runs on os_tick, the START falling edge arms it, and it produces a sample pulse at the CENTER of each bit (os_cnt == 8 for the first cell, then every 16) that a shift register uses to deserialize rx_data and check the STOP bit for framing. Nothing here knows it is a UART; it divides, shifts, counts, and samples on command. Identical in structure across SystemVerilog, Verilog, and VHDL.

The 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 tx idles high, both FSMs sit in IDLE, and the receiver's rx input 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 bit 16 os_ticks later, STOP the same. frame_err is the STOP cell not reading 1.

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.

baud_gen.sv — one counter, two strobes (os_tick 16x, baud_tick 1x per bit)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

uart.sv — integrated UART: baud gen + TX (FSM/shifter) + RX (oversample/FSM/shifter/framing)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

uart_loopback_tb.sv — TX -> RX loopback: send random bytes, assert rx_data == tx_data, framing OK
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

uart_rx_sample.sv — BUGGY (sample at cell start) vs FIXED (mid-bit)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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
uart_rx_sample_tb.sv — mismatched RX clock: the edge-sampler garbles, the mid-bit sampler survives
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

uart.v — integrated UART in Verilog (localparam states, os-counter mid-bit sampling)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
uart_loopback_tb.v — self-checking Verilog loopback: rx==tx over several bytes, print PASS/FAIL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

uart_rx_sample.v — BUGGY (osc==0, cell edge) vs FIXED (osc==OS-1, mid-bit)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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
   endmodule

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

uart.vhd — integrated UART in VHDL (numeric_std, os-counter mid-bit sampling)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
uart_loopback_tb.vhd — self-checking VHDL loopback: rx = tx, assert rx_data = sent (severity error)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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.

uart_rx_sample.vhd — BUGGY (osc = 0, cell edge) vs FIXED (osc = OS-1, mid-bit)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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_data when rx_valid strobes) must equal the byte sent, with frame_err low — 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 tx to rx, push a byte with tx_start, and on each rx_valid compare rx_data against 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-0 or all-1 byte can hide a bit-count or ordering error that 0xB5 exposes. The three testbenches do exactly this (SV assert (rx_data === exp), Verilog if (rx_data !== last_sent) $display("FAIL…"), VHDL assert rx_data = last_sent … severity error).
  • Multiple CLK/baud ratios. Re-run the loopback at a couple of CLK_HZ / BAUD ratios (e.g. DIV = 16 and DIV = 174). A design that passes at one ratio can fail at another if the os-divisor or the mid-bit arithmetic assumed a specific DIV — 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 assert frame_err goes high and rx_valid does 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_start as soon as tx_busy falls 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. tx idles high, drops for one bit-time (START), presents d0..d7 each for one bit-time (DATA), rises for one bit-time (STOP), idles high. On the receive side, rx_valid pulses once, one bit-time after the STOP cell's center, with rx_data equal to the transmitted byte and frame_err low. An rx_data that 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.

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 / BAUD for baud_tick and DIV/16 for the 16x os_tick.
  • Pulse & Strobe Generators — Chapter 3.x; the one-cycle baud_tick/os_tick/rx_valid strobes 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, and OS parameterize 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):

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's 1 returns the line to idle and lets the receiver check framing.
  • The baud math must be exact. DIV = CLK_HZ / BAUD clocks per bit-time; DIV/16 clocks per os_tick, so 16 os_ticks per bit. Reload the divider at DIV-1, not DIV, 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 rx through two flops first.
  • Prove it with a mismatched-clock loopback, not a shared clock. Wire tx to rx, send random and corner bytes, and assert rx_data == tx_data with frame_err low — 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.