Skip to content

RTL Design Patterns · Chapter 4 · FSM Design

FSM Worked Controller

This capstone composes every FSM pattern from Chapter 4 into one complete, verified controller: a UART-style byte transmitter. It takes a start pulse and an 8-bit byte and serializes it on a single wire as one start bit, eight data bits sent least significant first, and one stop bit, sending one bit per baud tick and raising busy while it runs and done when the frame completes. It is a genuine composition: a safe FSM walks through idle, start, data, and stop; a shift register holds and shifts the byte; a bit counter counts the eight data bits; and a baud-tick clock-enable paces the whole thing at one bit-time per tick. Because a composed controller's correctness lives in the bit-and-tick accounting, you verify it by reassembling the whole frame and checking it equals the byte you sent, in SystemVerilog, Verilog, and VHDL.

Intermediate16 min readRTL Design PatternsFSMUART TXShift RegisterBit CounterBaud TickControl and Datapath

Chapter 4 · Section 4.7 · FSM Design

1. The Engineering Problem

You have a byte to send and a single wire to send it on. A parallel bus can carry all eight bits at once; a serial link — a UART line, an SPI wire, any 1-bit channel — can carry exactly one bit per bit-time. Somewhere between the parallel byte the rest of your chip produces and the one-bit tx wire the outside world listens on, something must serialize: take the eight bits, wrap them in a framing bit before and a framing bit after so the receiver can find their boundaries, and clock them out one at a time at an agreed rate. That "something" is a controller, and building it is the exam Chapter 4 has been preparing you for.

The frame is fixed by the protocol. The line idles high (tx = 1). To send a byte the transmitter drives a START bit (a single 0, whose falling edge tells the receiver "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). Eleven bit-times total: 0 · d0 d1 d2 d3 d4 d5 d6 d7 · 1. Get the count wrong by one, drop the STOP bit, or advance on the wrong tick, and the receiver's sampling clock drifts off the bit boundaries and every subsequent byte is garbage — not a corrupted bit here and there, a total loss of frame sync.

None of the individual pieces is new to you. A shift register (Chapter 2.6) can hold the byte and shift a new bit out each step. A counter (Chapter 3) can count the eight data bits and tell you when you are done. A clock-enable derived from a slow tick (Chapter 3.5) can pace everything at the baud rate without a second clock domain. And a finite-state machine (Chapter 4.1) can sequence the phases — idle, start, data, stop — deciding when each piece acts. What is new is the composition: making a controller drive a datapath, gated by a tick, with the bit-and-tick accounting exactly right, and proving it right. This page builds that controller once, end to end, and the interface it presents is deliberately small.

the-interface.v — the whole contract: a byte in, a framed serial stream out
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // INPUTS
   input  wire        clk;         // system clock (fast)
   input  wire        rst;         // synchronous, active-high -> back to IDLE, tx idles high
   input  wire        start;       // one-cycle pulse: "send this byte"
   input  wire [7:0]  data;        // the byte to transmit, sampled when start is seen
   input  wire        baud_tick;   // one-cycle enable pulse, once per BIT-TIME (from 3.5)
 
   // OUTPUTS
   output wire        tx;          // the serial line: idles high, START(0) + 8 data LSB-first + STOP(1)
   output wire        busy;        // high from the moment start is accepted until the frame ends
   output wire        done;        // one-cycle pulse at end of frame (STOP bit sent, back to IDLE)
 
   // The frame on tx, one bit per baud_tick:
   //   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 controller has one brain and three limbs, plus the interface that binds them. Take them in turn, then see the two views that matter: the block diagram (who drives whom) and the state diagram (how the brain walks the frame).

The controller — a safe three-piece FSM. This is the machine from 4.1: a state register, next-state logic, and output logic, written in the latch-free default-first style of 4.5. Its four states are the phases of the frame: IDLE (line high, waiting for start), START (drive the START bit 0 for one bit-time), DATA (drive the shift register's LSB, shift, and count — repeat for 8 bit-times), and STOP (drive the STOP bit 1 for one bit-time, then pulse done and return to IDLE). The FSM advances only on a baud tick; on non-tick cycles it holds. It carries no data — it reads exactly one status bit from the datapath (bit_cnt reached the last data bit) and emits a handful of one-bit controls.

The shift register — the byte serializer (Chapter 2.6). An 8-bit register shift_reg that loads data when start is accepted and shifts right by one on each data-phase tick, so its bit 0 (shift_reg[0]) always presents the current data bit and shifting brings the next bit down to position 0. Right-shift with the byte loaded normally gives LSB-first transmission for free — data[0] is at position 0 at load time, data[1] arrives there after one shift, and so on. This is the PISO (parallel-in, serial-out) direction of the shift-register pattern.

The bit counter — the beat counter (Chapter 3). A small counter bit_cnt (3 bits, counts 0..7) that increments once per data-phase tick and tells the FSM when all eight data bits have gone out. Whether you count up to 7 then leave or count 8 shifts is the exact accounting the DebugLab turns on — there are eight data bits, so the DATA phase must last eight ticks, no more and no fewer. The counter is the FSM's status source: "have we sent the eighth bit yet?"

The baud tick — the metronome (Chapter 3.5). A one-cycle clock-enable that pulses once per bit-time, derived from the system clock by a divider (clk_freq / baud_rate cycles per tick). Here it is an input so the transmitter is decoupled from the exact baud generator, but conceptually it is the strobe from 3.5. Every register in the design updates only on baud_tick (except reset) — that is what makes the machine run at the bit rate on a single fast clock.

The control/status interface — the seam. Control down (FSM -> datapath): load (capture data into shift_reg, clear bit_cnt), shift_en (shift the register and count on this tick). Status up (datapath -> FSM): last_bit (bit_cnt == 7, i.e. the eighth data bit is going out this tick). Outputs of the whole block: tx (Moore, from state and shift_reg[0]), busy, done. The interface is narrow on purpose — one control pair down, one status bit up — which is exactly why you can verify the datapath's serialization and the FSM's sequencing along the same seam.

Block diagram — the FSM conducts the shift register and bit counter, paced by baud_tick

data flow
Block diagram — the FSM conducts the shift register and bit counter, paced by baud_tickadvance on tickload / shift_enload / countlast_bit (== 7)shift_reg[0]drive bystatebaud_tick (3.5)one enable pulse per BIT-TIME — the metronomeFSM controller(4.1)IDLE / START / DATA / STOP — safe, latch-free (4.5)shift_reg (2.6)loads data, shifts right — LSB (shift_reg[0]) firstbit_cnt (3.x)counts the 8 data bits — raises last_bit at 7tx / busy / doneMoore outputs (4.2): tx from state + shift_reg[0]
One controller, three datapath pieces, one metronome. The baud tick tells the FSM when a beat happens; on each beat the FSM commands load/shift_en to the shift register and load/count to the bit counter; the counter reports last_bit (bit_cnt == 7) back up so the FSM knows the eighth data bit is going out. The line tx is a Moore output: in START it is 0, in DATA it is shift_reg[0] (the current serial bit), in STOP and IDLE it is 1. Nothing here knows it is a UART — it only loads, shifts, counts, and drives on command. Identical in structure across SystemVerilog, Verilog, and VHDL.

The second view is the FSM itself — the frame walked as a state diagram, with the transition guards that make the accounting explicit.

State diagram — the TX FSM walks the frame, advancing one state per baud tick

data flow
State diagram — the TX FSM walks the frame, advancing one state per baud tickstart pulsebaud_ticktick and notlast_bittick and last_bit (== 7)tick andlast_bit (=…baud_tick -> doneIDLEtx=1, busy=0. On start: load byte, -> STARTSTARTtx=0, busy=1. On tick: -> DATA (bit_cnt=0)DATAtx=shift_reg[0], busy=1. On tick: shift+countSTOPtx=1, busy=1. On tick: pulse done, -> IDLE
The controller is the three-piece FSM of 4.1 in the safe, default-first style of 4.5. IDLE holds the line high until a start pulse loads the byte; START drives the START bit for one bit-time; DATA is the loop — on each tick it shifts a new bit onto tx and increments bit_cnt, staying in DATA while not last_bit and leaving to STOP on the tick where last_bit (bit_cnt == 7) is true, so DATA lasts EXACTLY eight ticks; STOP drives the STOP bit for one bit-time, pulses done, and returns to IDLE. The whole accounting — eight data bits, one START, one STOP — is encoded in the DATA self-loop's exit guard. Get that guard off by one and you drop or duplicate a bit (§7).

One reasoning thread runs under both views: the FSM only ever acts on a baud tick, and every output is a function of state (Moore). On a non-tick cycle the state register holds, the shift register holds, and the counter holds — the whole machine freezes between beats. On a tick, the FSM reads last_bit (a combinational function of the current bit_cnt), decides its next state, and asserts the control that shifts/counts on that same edge. Because tx is driven purely from state and a register bit — never from start or baud_tick directly — it is stable and glitch-free for a full bit-time, which is exactly what a receiver sampling at mid-bit needs (4.2). Getting the tick-gating and the DATA exit guard right is the entire subtlety; getting the guard wrong is the signature failure (§7).

4. Real RTL Implementation

This is the core of the page. We build the complete UART-TX-lite controller — the safe FSM, the shift register, and the bit counter, all gated by baud_tick — as one module, and show it in SystemVerilog, Verilog, and VHDL, each with a self-checking clocked testbench that reassembles the serial frame and asserts it equals the transmitted byte. Then §4d isolates the signature bug — the DATA-phase bit-count off-by-one / dropped STOP — as buggy-vs-fixed RTL in all three languages.

Three conventions run through every block, and each is a Chapter-4 lesson made concrete:

  • Reset is synchronous, active-high (rst), returning the FSM to IDLE and forcing tx to its idle-high value (2.3 / 4.5's safe state). At power-up and after reset the line must idle high, or a receiver sees a spurious START.
  • Every register advances only on baud_tick (except the load path off start) — the clock-enable discipline of 3.5. One fast clk; the machine runs at the bit rate.
  • Outputs are default-assigned in every state — the latch-free, glitch-free style of 4.5. tx is a Moore output (state + shift_reg[0]), never a function of the raw inputs (4.2).

4a. The complete controller in SystemVerilog

The FSM, shift register, and bit counter live in one module. The state register and next-state logic are the two-block form of 4.1; the output logic is default-first (4.5); the datapath (shift_reg, bit_cnt) is gated by baud_tick and the FSM's shift_en.

uart_tx_lite.sv — complete UART-TX-lite: safe FSM + shift register + bit counter, baud-gated
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module uart_tx_lite (
       input  logic       clk,
       input  logic       rst,          // synchronous, active-high -> IDLE, tx idles high
       input  logic       start,        // one-cycle "send this byte" pulse
       input  logic [7:0] data,         // the byte to transmit
       input  logic       baud_tick,    // one-cycle enable, once per bit-time (from 3.5)
       output logic       tx,           // serial line: START(0) + 8 data LSB-first + STOP(1)
       output logic       busy,         // high from acceptance of start until frame end
       output logic       done          // one-cycle pulse at end of frame
   );
       // 4.1: three-piece FSM. Enumerated state (4.4 leaves encoding to the tool here).
       typedef enum logic [1:0] { IDLE, START, DATA, STOP } state_t;
       state_t state, next_state;
 
       // Datapath (4.6): shift register (2.6) + bit counter (3.x), both baud-gated (3.5).
       logic [7:0] shift_reg;           // holds the byte; shift_reg[0] is the current data bit
       logic [2:0] bit_cnt;             // 0..7 — counts the 8 DATA bits
       logic       last_bit;            // STATUS up to the FSM: eighth data bit is going out
 
       assign last_bit = (bit_cnt == 3'd7);   // combinational status over CURRENT bit_cnt
 
       // STATE REGISTER: advances only on a baud tick (3.5 clock-enable). rst -> IDLE (safe).
       always_ff @(posedge clk) begin
           if (rst)            state <= IDLE;
           else if (baud_tick) state <= next_state;   // hold between beats
       end
 
       // NEXT-STATE LOGIC: default-first (4.5) so next_state is driven on EVERY path.
       always_comb begin
           next_state = state;                        // DEFAULT: hold (no latch, safe)
           unique case (state)
               IDLE:  if (start)    next_state = START;   // start accepted (tick-independent)
               START:               next_state = DATA;    // one bit-time of START, then data
               DATA:  if (last_bit) next_state = STOP;    // leave AFTER the 8th data bit
                      else          next_state = DATA;     //   otherwise keep shifting
               STOP:                next_state = IDLE;     // one bit-time of STOP, then home
           endcase
       end
 
       // DATAPATH: load on accepted start; shift + count once per DATA-phase tick.
       always_ff @(posedge clk) begin
           if (rst) begin
               shift_reg <= 8'h00;
               bit_cnt   <= 3'd0;
           end else if (state == IDLE && start) begin
               shift_reg <= data;                     // 2.6: load the byte
               bit_cnt   <= 3'd0;                     //      reset the bit count
           end else if (state == DATA && baud_tick) begin
               shift_reg <= {1'b0, shift_reg[7:1]};   // 2.6: shift right -> next bit to [0], LSB-first
               bit_cnt   <= bit_cnt + 3'd1;           // 3.x: count this data bit
           end
       end
 
       // OUTPUT LOGIC (Moore, 4.2): tx is a function of state + shift_reg[0] only, never
       // of start/baud_tick, so it is stable and glitch-free for a whole bit-time.
       always_comb begin
           tx   = 1'b1;                               // DEFAULT: line idles HIGH (safe, 4.5)
           busy = 1'b1;                               // DEFAULT busy; IDLE overrides below
           done = 1'b0;
           unique case (state)
               IDLE:  begin tx = 1'b1; busy = 1'b0;              end   // idle high, not busy
               START: begin tx = 1'b0;                          end   // START bit = 0
               DATA:  begin tx = shift_reg[0];                   end   // current serial bit
               STOP:  begin tx = 1'b1; done = (baud_tick);       end   // STOP=1; done on the tick out
           endcase
       end
   endmodule

Two lines carry the accounting. last_bit = (bit_cnt == 3'd7) fires on the tick that transmits the eighth data bit (bits 0..7 = eight bits), and the FSM leaves DATA on that tick's next edge, so the DATA phase is exactly eight bit-times. shift_reg <= {1'b0, shift_reg[7:1]} shifts right, which — because the byte was loaded straight — sends data[0] first: LSB-first, for free. The clocked testbench does not trust an eyeball of the first bits; it advances baud ticks, samples tx at each bit-time, reassembles START + 8 data + STOP, and asserts the reassembled byte equals the one it sent.

uart_tx_lite_tb.sv — clocked self-check: transmit 0xB5, reassemble the frame, assert equal
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module uart_tx_lite_tb;
       logic clk = 1'b0, rst, start, baud_tick, tx, busy, done;
       logic [7:0] data;
       int   errors = 0;
 
       uart_tx_lite dut (.clk(clk), .rst(rst), .start(start), .data(data),
                         .baud_tick(baud_tick), .tx(tx), .busy(busy), .done(done));
 
       always #5 clk = ~clk;                          // system clock
 
       // Sample tx at the current bit-time and return it. Called ON a baud tick.
       task automatic tick_and_sample(output logic bit_out);
           baud_tick = 1'b1; @(posedge clk); #1; bit_out = tx; baud_tick = 1'b0;
           @(posedge clk); #1;                        // a few idle (non-tick) cycles between beats
           @(posedge clk); #1;
       endtask
 
       initial begin
           logic [7:0] exp_byte = 8'hB5;              // 1011_0101 — the byte under test
           logic start_bit, stop_bit;
           logic [7:0] rx_byte;
 
           rst = 1'b1; start = 1'b0; baud_tick = 1'b0; data = 8'h00;
           @(posedge clk); #1; rst = 1'b0;
           assert (tx === 1'b1) else begin $error("line did not idle high after reset"); errors++; end
 
           // Pulse start with the byte; FSM accepts and loads (tick-independent).
           data = exp_byte; start = 1'b1; @(posedge clk); #1; start = 1'b0;
 
           // Now clock out the frame one bit per tick and reassemble it.
           tick_and_sample(start_bit);                // START phase -> the START bit
           for (int i = 0; i < 8; i++)                // EXACTLY 8 data bits, LSB-first
               tick_and_sample(rx_byte[i]);
           tick_and_sample(stop_bit);                 // STOP phase -> the STOP bit
 
           // FULL-FRAME self-check — not a spot check of the first bits.
           assert (start_bit === 1'b0)  else begin $error("START bit != 0 (got %b)", start_bit); errors++; end
           assert (stop_bit  === 1'b1)  else begin $error("STOP bit != 1 (got %b)",  stop_bit);  errors++; end
           assert (rx_byte === exp_byte)
               else begin $error("frame decode: rx=%h exp=%h", rx_byte, exp_byte); errors++; end
           assert (done === 1'b0 || busy === 1'b1)     // done never fires without the frame having run
               else begin $error("done/busy timing"); errors++; end
 
           if (errors == 0) $display("PASS: 0xB5 reassembled exactly (START=0, 8 data LSB-first, STOP=1)");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

4b. The complete controller in Verilog

The Verilog form is the same machine with reg/wire typing and localparam state codes instead of an enum. The three pieces — FSM, shift register, bit counter — are unchanged; only the spelling differs.

uart_tx_lite.v — complete UART-TX-lite in Verilog (localparam states, baud-gated)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module uart_tx_lite (
       input  wire       clk,
       input  wire       rst,          // synchronous, active-high
       input  wire       start,
       input  wire [7:0] data,
       input  wire       baud_tick,    // one-cycle enable, once per bit-time
       output reg        tx,
       output reg        busy,
       output reg        done
   );
       localparam [1:0] IDLE  = 2'd0,   // 4.1 three-piece FSM; simple binary encoding (4.4)
                        START = 2'd1,
                        DATA  = 2'd2,
                        STOP  = 2'd3;
 
       reg  [1:0] state, next_state;
       reg  [7:0] shift_reg;            // 2.6: holds the byte; bit 0 is the current data bit
       reg  [2:0] bit_cnt;              // 3.x: 0..7
       wire       last_bit = (bit_cnt == 3'd7);   // STATUS: eighth data bit going out
 
       // STATE REGISTER — advances only on a baud tick (3.5). rst -> IDLE (safe).
       always @(posedge clk) begin
           if (rst)            state <= IDLE;
           else if (baud_tick) state <= next_state;
       end
 
       // NEXT-STATE — default-first (4.5): next_state driven on every path, no latch.
       always @(*) begin
           next_state = state;                     // DEFAULT: hold
           case (state)
               IDLE:  if (start)    next_state = START;
               START:               next_state = DATA;
               DATA:  if (last_bit) next_state = STOP;   // leave AFTER the 8th bit
                      else          next_state = DATA;
               STOP:                next_state = IDLE;
               default:             next_state = IDLE;    // safe: any illegal code -> home
           endcase
       end
 
       // DATAPATH — load on accepted start; shift + count once per DATA-phase tick.
       always @(posedge clk) begin
           if (rst) begin
               shift_reg <= 8'h00;
               bit_cnt   <= 3'd0;
           end else if (state == IDLE && start) begin
               shift_reg <= data;                  // load the byte
               bit_cnt   <= 3'd0;
           end else if (state == DATA && baud_tick) begin
               shift_reg <= {1'b0, shift_reg[7:1]};   // shift right -> LSB-first serialization
               bit_cnt   <= bit_cnt + 3'd1;
           end
       end
 
       // OUTPUT LOGIC (Moore, 4.2) — default-first so every output is driven in every state.
       always @(*) begin
           tx   = 1'b1;                            // DEFAULT: idle high (safe)
           busy = 1'b1;
           done = 1'b0;
           case (state)
               IDLE:  begin tx = 1'b1; busy = 1'b0; end
               START: begin tx = 1'b0;              end   // START bit
               DATA:  begin tx = shift_reg[0];      end   // current serial bit
               STOP:  begin tx = 1'b1; done = baud_tick; end   // STOP=1; done on tick out
               default: begin tx = 1'b1; busy = 1'b0; end
           endcase
       end
   endmodule
uart_tx_lite_tb.v — self-checking Verilog TB: reassemble frame, print PASS/FAIL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module uart_tx_lite_tb;
       reg        clk, rst, start, baud_tick;
       reg  [7:0] data;
       wire       tx, busy, done;
 
       reg  [7:0] exp_byte, rx_byte;
       reg        start_bit, stop_bit;
       integer    i, errors;
 
       uart_tx_lite dut (.clk(clk), .rst(rst), .start(start), .data(data),
                         .baud_tick(baud_tick), .tx(tx), .busy(busy), .done(done));
 
       initial clk = 1'b0;
       always #5 clk = ~clk;
 
       // Pulse one baud tick, sample tx at that bit-time; hold non-tick cycles between beats.
       task tick_and_sample;
           output sampled;
           begin
               baud_tick = 1'b1; @(posedge clk); #1; sampled = tx; baud_tick = 1'b0;
               @(posedge clk); #1;
               @(posedge clk); #1;
           end
       endtask
 
       initial begin
           errors   = 0;
           exp_byte = 8'hB5;                       // 1011_0101
           rst = 1'b1; start = 1'b0; baud_tick = 1'b0; data = 8'h00;
           @(posedge clk); #1; rst = 1'b0;
           if (tx !== 1'b1) begin $display("FAIL: line not idle-high after reset"); errors = errors + 1; end
 
           data = exp_byte; start = 1'b1; @(posedge clk); #1; start = 1'b0;
 
           tick_and_sample(start_bit);             // START bit
           for (i = 0; i < 8; i = i + 1)           // EXACTLY 8 data bits, LSB-first
               tick_and_sample(rx_byte[i]);
           tick_and_sample(stop_bit);              // STOP bit
 
           if (start_bit !== 1'b0) begin $display("FAIL: START bit != 0 (%b)", start_bit); errors = errors + 1; end
           if (stop_bit  !== 1'b1) begin $display("FAIL: STOP bit != 1 (%b)",  stop_bit);  errors = errors + 1; end
           if (rx_byte !== exp_byte) begin
               $display("FAIL: frame decode rx=%h exp=%h", rx_byte, exp_byte);
               errors = errors + 1;
           end
 
           if (errors == 0) $display("PASS: 0xB5 reassembled exactly (START + 8 LSB-first + STOP)");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

4c. The complete controller in VHDL

VHDL states the same machine with an enumerated state_type, numeric_std for the counter arithmetic, and rising_edge(clk) for the clocked processes. The shift register uses a slice-and-concatenate; the counter uses unsigned. The output logic is a combinational process defaulted first — VHDL's answer to the latch-free discipline.

uart_tx_lite.vhd — complete UART-TX-lite in VHDL (state_type, numeric_std, rising_edge)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity uart_tx_lite is
       port (
           clk       : in  std_logic;
           rst       : in  std_logic;                 -- synchronous, active-high
           start     : in  std_logic;
           data      : in  std_logic_vector(7 downto 0);
           baud_tick : in  std_logic;                 -- one-cycle enable, once per bit-time
           tx        : out std_logic;
           busy      : out std_logic;
           done      : out std_logic
       );
   end entity;
 
   architecture rtl of uart_tx_lite is
       type state_type is (IDLE, START, DATA, STOP);   -- 4.1 three-piece FSM
       signal state, next_state : state_type;
       signal shift_reg : std_logic_vector(7 downto 0);  -- 2.6: holds the byte
       signal bit_cnt   : unsigned(2 downto 0);          -- 3.x: 0..7
       signal last_bit  : std_logic;                     -- STATUS: eighth data bit going out
   begin
       last_bit <= '1' when bit_cnt = to_unsigned(7, 3) else '0';
 
       -- STATE REGISTER: advances only on a baud tick (3.5). rst -> IDLE (safe).
       state_reg : process (clk)
       begin
           if rising_edge(clk) then
               if rst = '1' then
                   state <= IDLE;
               elsif baud_tick = '1' then
                   state <= next_state;               -- hold between beats
               end if;
           end if;
       end process;
 
       -- NEXT-STATE: default-first (4.5) so next_state is assigned on every path.
       next_logic : process (all)
       begin
           next_state <= state;                       -- DEFAULT: hold
           case state is
               when IDLE  => if start = '1' then next_state <= START; end if;
               when START => next_state <= DATA;
               when DATA  => if last_bit = '1' then next_state <= STOP;   -- leave AFTER 8th bit
                             else next_state <= DATA; end if;
               when STOP  => next_state <= IDLE;
           end case;
       end process;
 
       -- DATAPATH: load on accepted start; shift + count once per DATA-phase tick.
       datapath : process (clk)
       begin
           if rising_edge(clk) then
               if rst = '1' then
                   shift_reg <= (others => '0');
                   bit_cnt   <= (others => '0');
               elsif state = IDLE and start = '1' then
                   shift_reg <= data;                 -- load the byte
                   bit_cnt   <= (others => '0');
               elsif state = DATA and baud_tick = '1' then
                   shift_reg <= '0' & shift_reg(7 downto 1);   -- shift right -> LSB-first
                   bit_cnt   <= bit_cnt + 1;                   -- count this data bit
               end if;
           end if;
       end process;
 
       -- OUTPUT LOGIC (Moore, 4.2): defaulted first; tx from state + shift_reg(0) only.
       out_logic : process (all)
       begin
           tx   <= '1';                               -- DEFAULT: idle high (safe)
           busy <= '1';
           done <= '0';
           case state is
               when IDLE  => tx <= '1'; busy <= '0';
               when START => tx <= '0';
               when DATA  => tx <= shift_reg(0);
               when STOP  => tx <= '1'; done <= baud_tick;
           end case;
       end process;
   end architecture;
uart_tx_lite_tb.vhd — self-checking VHDL TB: reassemble 0xB5, assert equal (severity error)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity uart_tx_lite_tb is
   end entity;
 
   architecture sim of uart_tx_lite_tb is
       signal clk       : std_logic := '0';
       signal rst       : std_logic;
       signal start     : std_logic := '0';
       signal data      : std_logic_vector(7 downto 0) := (others => '0');
       signal baud_tick : std_logic := '0';
       signal tx, busy, done : std_logic;
       constant CLK_HALF : time := 5 ns;
   begin
       dut : entity work.uart_tx_lite
           port map (clk => clk, rst => rst, start => start, data => data,
                     baud_tick => baud_tick, tx => tx, busy => busy, done => done);
 
       clk <= not clk after CLK_HALF;
 
       stim : process
           constant exp_byte : std_logic_vector(7 downto 0) := x"B5";   -- 1011_0101
           variable rx_byte  : std_logic_vector(7 downto 0);
           variable start_bit, stop_bit : std_logic;
 
           -- Pulse one baud tick, sample tx at that bit-time.
           procedure tick_and_sample (signal s : out std_logic; variable sampled : out std_logic) is
           begin
               baud_tick <= '1'; wait until rising_edge(clk); wait for 1 ns; sampled := tx;
               baud_tick <= '0'; wait until rising_edge(clk); wait for 1 ns;
                                 wait until rising_edge(clk); wait for 1 ns;
           end procedure;
           variable dummy : std_logic;
       begin
           rst <= '1'; wait until rising_edge(clk); wait for 1 ns; rst <= '0';
           assert tx = '1' report "line did not idle high after reset" severity error;
 
           data <= exp_byte; start <= '1'; wait until rising_edge(clk); wait for 1 ns; start <= '0';
 
           tick_and_sample(baud_tick, start_bit);          -- START bit
           for i in 0 to 7 loop                            -- EXACTLY 8 data bits, LSB-first
               tick_and_sample(baud_tick, dummy);
               rx_byte(i) := dummy;
           end loop;
           tick_and_sample(baud_tick, stop_bit);           -- STOP bit
 
           -- FULL-FRAME self-check.
           assert start_bit = '0' report "START bit /= 0" severity error;
           assert stop_bit  = '1' report "STOP bit /= 1"  severity error;
           assert rx_byte = exp_byte
               report "frame decode: rx /= exp" severity error;
           report "uart_tx_lite self-check complete (0xB5 reassembled)" severity note;
           wait;
       end process;
   end architecture;

4d. The signature bug — DATA bit-count off-by-one / dropped STOP (buggy vs fixed)

Every failure of a composed controller like this one lives in the bit-and-tick accounting, and the sharpest example is the DATA-phase count. The bug is subtle because it looks right: the machine still sequences IDLE -> START -> DATA -> STOP and the first bits on the wire are correct — it just leaves DATA one tick too early (seven data bits, not eight) or falls out of the frame without a proper STOP. The fix is a one-character change to the exit guard, and the reason it is dangerous is that only a full-frame decode catches it.

uart_tx_bug.sv — BUGGY (7 data bits / dropped STOP) vs FIXED (8 bits + STOP)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY next-state: leaves DATA when bit_cnt == 6, so only SEVEN data bits are
   //   sent (d0..d6); d7 is dropped and every subsequent frame is misaligned.
   //   (A twin bug: STOP -> IDLE unconditionally on any cycle, dropping the STOP bit.)
   module uart_tx_next_bad (
       input  logic [1:0] state,        // IDLE=0 START=1 DATA=2 STOP=3
       input  logic       start, last_bit_wrong,   // last_bit_wrong = (bit_cnt == 6)
       output logic [1:0] next_state
   );
       always_comb begin
           next_state = state;
           unique case (state)
               2'd0: if (start)          next_state = 2'd1;
               2'd1:                     next_state = 2'd2;
               2'd2: if (last_bit_wrong) next_state = 2'd3;   // BUG: leaves after 7 bits
                     else                next_state = 2'd2;
               2'd3:                     next_state = 2'd0;
           endcase
       end
   endmodule
 
   // FIXED: leave DATA only when bit_cnt == 7 (the EIGHTH data bit), so DATA lasts
   //   exactly eight ticks and STOP gets its own full bit-time before IDLE.
   module uart_tx_next_good (
       input  logic [1:0] state,
       input  logic       start, last_bit,          // last_bit = (bit_cnt == 7)
       output logic [1:0] next_state
   );
       always_comb begin
           next_state = state;
           unique case (state)
               2'd0: if (start)    next_state = 2'd1;
               2'd1:               next_state = 2'd2;
               2'd2: if (last_bit) next_state = 2'd3;   // FIX: leave AFTER the 8th data bit
                     else          next_state = 2'd2;
               2'd3:               next_state = 2'd0;    // STOP gets a full bit-time
           endcase
       end
   endmodule
uart_tx_bug_tb.sv — the full-frame decode that a first-bit eyeball misses
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module uart_tx_bug_tb;
       // Bind the DUT's next-state to _good to PASS; to _bad to see 7-bit frames fail.
       logic clk = 1'b0, rst, start, baud_tick, tx, busy, done;
       logic [7:0] data;
       int   errors = 0;
 
       uart_tx_lite dut (.clk(clk), .rst(rst), .start(start), .data(data),
                         .baud_tick(baud_tick), .tx(tx), .busy(busy), .done(done));
       always #5 clk = ~clk;
 
       task automatic tick_and_sample(output logic b);
           baud_tick = 1'b1; @(posedge clk); #1; b = tx; baud_tick = 1'b0;
           @(posedge clk); #1; @(posedge clk); #1;
       endtask
 
       initial begin
           logic [7:0] exp_byte = 8'hB5, rx_byte;
           logic sbit, pbit;
           rst = 1'b1; start = 0; baud_tick = 0; data = 0;
           @(posedge clk); #1; rst = 0;
           data = exp_byte; start = 1; @(posedge clk); #1; start = 0;
 
           tick_and_sample(sbit);                       // an EYEBALL of START + first bits passes...
           for (int i = 0; i < 8; i++) tick_and_sample(rx_byte[i]);
           tick_and_sample(pbit);
 
           // ...but the FULL-FRAME decode fails for the 7-bit-DATA bug: rx_byte's high
           // bit is wrong and the STOP bit lands where d7 should have been.
           assert (sbit === 1'b0 && pbit === 1'b1 && rx_byte === exp_byte)
               else begin $error("frame misframed: rx=%h exp=%h stop=%b", rx_byte, exp_byte, pbit); errors++; end
           if (errors == 0) $display("PASS: full 8-bit frame reassembles (bit accounting correct)");
           else             $display("FAIL: %0d — the DATA count / STOP bit is off", errors);
           $finish;
       end
   endmodule

The Verilog and VHDL buggy/fixed pairs tell the identical story — the only difference between broken and correct is the constant the DATA exit guard compares against (6 vs 7) and whether STOP gets its own bit-time.

uart_tx_bug.v — BUGGY (bit_cnt==6) vs FIXED (bit_cnt==7) in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: exits DATA at bit_cnt == 6 -> only 7 data bits, d7 dropped, frame misaligned.
   module uart_tx_next_bad (
       input  wire [1:0] state,
       input  wire       start,
       input  wire [2:0] bit_cnt,
       output reg  [1:0] next_state
   );
       always @(*) begin
           next_state = state;
           case (state)
               2'd0: if (start)             next_state = 2'd1;
               2'd1:                        next_state = 2'd2;
               2'd2: if (bit_cnt == 3'd6)   next_state = 2'd3;   // BUG: 7 bits only
                     else                   next_state = 2'd2;
               2'd3:                        next_state = 2'd0;
               default:                     next_state = 2'd0;
           endcase
       end
   endmodule
 
   // FIXED: exits DATA at bit_cnt == 7 -> all 8 data bits, then a full STOP bit-time.
   module uart_tx_next_good (
       input  wire [1:0] state,
       input  wire       start,
       input  wire [2:0] bit_cnt,
       output reg  [1:0] next_state
   );
       always @(*) begin
           next_state = state;
           case (state)
               2'd0: if (start)             next_state = 2'd1;
               2'd1:                        next_state = 2'd2;
               2'd2: if (bit_cnt == 3'd7)   next_state = 2'd3;   // FIX: leave after 8th bit
                     else                   next_state = 2'd2;
               2'd3:                        next_state = 2'd0;
               default:                     next_state = 2'd0;
           endcase
       end
   endmodule
uart_tx_bug.vhd — BUGGY (= 6) vs FIXED (= 7) in VHDL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   -- BUGGY: leaves DATA at bit_cnt = 6 -> seven data bits, d7 dropped.
   entity uart_tx_next_bad is
       port ( state : in std_logic_vector(1 downto 0);
              start : in std_logic;
              bit_cnt : in unsigned(2 downto 0);
              next_state : out std_logic_vector(1 downto 0) );
   end entity;
   architecture rtl of uart_tx_next_bad is
   begin
       process (all) begin
           next_state <= state;
           case state is
               when "00" => if start = '1' then next_state <= "01"; end if;
               when "01" => next_state <= "10";
               when "10" => if bit_cnt = to_unsigned(6, 3) then next_state <= "11";  -- BUG
                            else next_state <= "10"; end if;
               when others => next_state <= "00";
           end case;
       end process;
   end architecture;
 
   -- FIXED: leaves DATA at bit_cnt = 7 -> all eight data bits, then a full STOP.
   entity uart_tx_next_good is
       port ( state : in std_logic_vector(1 downto 0);
              start : in std_logic;
              bit_cnt : in unsigned(2 downto 0);
              next_state : out std_logic_vector(1 downto 0) );
   end entity;
   architecture rtl of uart_tx_next_good is
   begin
       process (all) begin
           next_state <= state;
           case state is
               when "00" => if start = '1' then next_state <= "01"; end if;
               when "01" => next_state <= "10";
               when "10" => if bit_cnt = to_unsigned(7, 3) then next_state <= "11";  -- FIX
                            else next_state <= "10"; end if;
               when others => next_state <= "00";
           end case;
       end process;
   end architecture;

Across all three languages the lesson is one sentence: a composed controller is correct only if its bit-and-tick accounting is exact — eight data bits means the DATA phase lasts eight ticks, and only a full-frame decode, not a glance at the first bits, proves it.

5. Verification Strategy

A serializer's correctness is not "did the first few bits look right" — it is "does the whole frame reassemble into the byte I sent, with the framing bits where they belong." That is the one invariant, and it is what the §4 testbenches enforce.

Drive a known byte, pulse start, advance one baud_tick per bit-time, sample tx at each bit-time, and reassemble START + 8 data (LSB-first) + STOP. The reassembled byte must equal the input byte, the START bit must be 0, and the STOP bit must be 1.

Everything below is that invariant made checkable, and it maps directly onto the clocked testbenches in §4.

  • Full-frame self-check (the core test). Load a byte, pulse start, then loop: pulse baud_tick, sample tx at that bit-time, and store it. Sample eleven bit-times — one START, eight DATA (into rx_byte[0..7], LSB-first), one STOP — then assert start_bit == 0, stop_bit == 1, and rx_byte == exp_byte. Because the whole byte must match, a dropped or duplicated bit anywhere in the frame fails the check — this is what catches the §7 off-by-one that a first-bit glance misses. The three testbenches do exactly this (SV assert (rx_byte === exp_byte), Verilog if (rx_byte !== exp_byte) $display("FAIL…"), VHDL assert rx_byte = exp_byte report … severity error).
  • The output==input invariant. Stated as a property that holds for every byte: the sequence sampled off tx at bit-times, stripped of its START and STOP, LSB-first, equals data. If it ever fails, the serializer is broken. Sweep several bytes — 0x00, 0xFF, 0xB5, 0x01, 0x80, a few random — because a byte like 0x00 (all data bits 0) or 0xFF (all 1) can hide a bit-count error that a mixed byte like 0xB5 exposes; use both.
  • Framing and idle invariants. Assert tx == 1 after reset and in IDLE (the line idles high, or a receiver sees a phantom START). Assert busy rises when start is accepted and stays high through the frame, then falls in IDLE; assert done pulses for exactly one cycle at the STOP-to-IDLE transition and never fires mid-frame. These are the control/status timing checks of 4.6 applied to the block's outputs.
  • Tick-gating check. Between baud ticks the machine must freeze: hold state, shift_reg, and bit_cnt. Drive many idle (non-tick) cycles between ticks in the testbench (as the §4 tasks do) and confirm tx does not change and no bit is lost — if the machine advanced without a tick, the frame would run at the wrong rate and the sampled bits would double up.
  • Corner cases. A start pulse during a frame must be ignored (the FSM is not in IDLE, so it does not re-load) — verify a mid-frame start does not corrupt the in-flight byte. A start on the same cycle as a baud_tick; back-to-back frames (a second start right after done); reset asserted mid-frame (the line must snap back to idle-high and the next frame must be clean). Each is a place where the tick-gating or the load-vs-shift priority could be wrong.
  • Expected waveform. On a correct run tx idles high; on start it goes low for one bit-time (START), then presents data[0], data[1], …, data[7] each for one bit-time (DATA), then high for one bit-time (STOP), then idle high. busy frames the whole active window; done is a single-cycle pulse at the end. A tx that shows only seven distinct data bit-times before returning high, or a STOP that is missing (line goes straight from d6/d7 back to a START-looking low), is the visual signature of the §7 bug.

6. Common Mistakes

Off-by-one on the DATA bit count (7 vs 8). The signature failure. There are eight data bits (d0..d7), so the DATA phase must last eight ticks. Exiting when bit_cnt == 6 (or counting bit_cnt < 7 as "keep going" but incrementing after the compare) sends only seven bits and drops d7; exiting when bit_cnt == 8 sends a ninth phantom bit. The count and the exit guard must agree: with bit_cnt running 0..7 and incrementing each DATA tick, leave DATA on the tick where bit_cnt == 7 — the eighth bit. Full post-mortem in §7; buggy-vs-fixed RTL in §4d.

Dropping the STOP bit / not returning to idle-high. The STOP bit is a real bit-time, not a formality: the line must be held high for one full bit-time after the last data bit, and only then return to IDLE. Falling straight from the last data bit back toward IDLE (or letting IDLE's low-going START logic start immediately) denies the receiver the guaranteed high level it needs to detect the next START's falling edge — so the following byte loses sync. Give STOP its own state and its own tick, and make IDLE (and reset) drive tx = 1.

Advancing the shift/counter on the wrong tick — control/status timing (4.6). The status last_bit is a combinational function of the current bit_cnt; the FSM must read it and act in the same cycle, before the enable it asserts advances the counter on the next edge. Shift or count on a cycle where baud_tick is low, or read last_bit one cycle late, and the frame runs at the wrong rate or exits DATA at the wrong bit. Every datapath update is gated by baud_tick and state == DATA; the FSM branches on the current status, not a registered copy of it.

A non-safe / latched control output. If the output logic (or next-state logic) fails to assign tx, busy, done, or next_state on some state path, the combinational block infers a latch and the output holds stale (4.5). The fix is default-first: assign every output a safe default at the top of the block (tx = 1, busy = 1, done = 0, next_state = state), then override per state. A latched tx in particular can hold a data bit into the STOP window and corrupt the frame silently.

Sampling tx at the wrong instant (a verification bug, not an RTL bug). tx is valid for a whole bit-time, but a testbench that samples it on a non-tick cycle, or before the combinational output has settled after the tick edge, reads the wrong value and reports a false failure — or worse, a false pass. Sample tx at the same phase of every bit-time (the §4 tasks sample just after the tick edge, with a settling delay), mirroring where a real receiver samples (mid-bit). Getting the sampling instant wrong makes a correct serializer look broken and a broken one look fine.

Loading the wrong bit order (MSB-first instead of LSB-first). UART sends LSB-first. Loading the byte and shifting left (presenting shift_reg[7]) transmits MSB-first — every byte arrives bit-reversed. Load straight and shift right so shift_reg[0] is data[0] first; if the protocol wanted MSB-first you would reverse at load or shift the other way, but the direction must be deliberate and matched to the receiver.

7. DebugLab

The transmitter whose first bits look perfect and whose bytes are all wrong — a DATA-count off-by-one

The engineering lesson: when you compose FSM patterns into one controller, the bug is almost never in any single pattern — it is in the accounting that ties them together, and it hides at the boundary a casual test never reaches. The tell here is a frame whose beginning is always correct and whose byte is always wrong: that split is the fingerprint of an off-by-one in the phase length, not a broken state machine. Two habits make composed controllers trustworthy, and they are the same across SystemVerilog, Verilog, and VHDL: make the bit-and-tick accounting explicit and match the exit guard to the exact count (eight data bits, eight ticks, leave on bit_cnt == 7), and verify with a full-frame self-check — reassemble the whole frame and compare it to the input — never a spot check of the first bits.

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses composing patterns and getting the accounting right.

Exercise 1 — Add a parity bit

Extend the frame to START + 8 data (LSB-first) + one parity bit + STOP (even parity over the 8 data bits). State (i) what new state (or what change to the existing states) you need, (ii) where the parity is computed and whether it belongs in the datapath or the controller, and (iii) exactly how the DATA-exit and STOP accounting change so no bit is dropped. Name which Chapter-4 pattern each change draws on.

Exercise 2 — Predict the misframe

An engineer writes the DATA exit as if (bit_cnt == 8) next_state = STOP while bit_cnt runs 0..7 and increments each DATA tick. Describe precisely what the machine does — does it ever leave DATA, and if so when? — and what the receiver sees. Then state the one-line fix and the full-frame test that distinguishes this bug from the == 6 bug of §7 (hint: they misframe in opposite directions).

Exercise 3 — Make it parameterizable

Turn the fixed 8-bit transmitter into a DATA_BITS-generic one (5–9 data bits, as real UARTs support). State (i) the new width of bit_cnt and the exit-guard constant in terms of DATA_BITS, (ii) why the STOP and START logic is unchanged, and (iii) what you must re-verify to trust the generic version (echo the "verify generic, not assume generic" discipline). Give the exit guard as an expression in DATA_BITS.

Exercise 4 — Sample like a real receiver

The §4 testbench samples tx just after each baud-tick edge. A real UART receiver oversamples (e.g. 16× per bit) and samples at mid-bit. Explain (i) why mid-bit sampling is more robust than edge-adjacent sampling, (ii) what a testbench that sampled at the wrong instant would report for a correct DUT, and (iii) how you would restructure the testbench's sampling so it mirrors a real receiver and would still catch the §7 off-by-one.

The Chapter-4 patterns this controller composes (the lessons you are putting together here):

  • FSM Fundamentals — Chapter 4.1; the three-piece machine (state register, next-state logic, output logic) that is this controller's skeleton.
  • Moore vs Mealy — Chapter 4.2; why tx is a Moore output — glitch-free and stable for a full bit-time, which a serial line needs.
  • FSM Coding Styles — Chapter 4.3; the two-/three-block coding style used for the FSM here.
  • FSM State Encoding — Chapter 4.4; binary vs one-hot for the four states, and why the tool's choice is fine for a small controller.
  • Safe FSMs & Latch-Free Outputs — Chapter 4.5; the default-first discipline that keeps tx/busy/done/next_state latch-free and the reset landing in a safe idle-high state.
  • FSM + Datapath — Chapter 4.6; the control-down / status-up split this page applies to the shift register and bit counter.

The datapath pieces this controller drives:

  • Shift Registers — Chapter 2.6; the PISO serializer that holds the byte and shifts it out LSB-first.
  • Clock Dividers & Timers — Chapter 3.5; the baud-tick clock-enable that paces the transmitter at the bit rate on one clock.
  • The Register Pattern (D-FF) — Chapter 2.1; the enabled registers under every datapath element.
  • Synchronous Reset — Chapter 2.3; the synchronous, active-high reset that returns the machine to idle-high.

Backward / method:

  • The RTL Design Mindset — Chapter 0.2; the Interface → State → Datapath → Control → Verification lenses this worked example walks end to end.
  • What Is an RTL Design Pattern? — Chapter 0.1; why composing patterns — not just knowing them — is the design skill this capstone rehearses.

Forward (unlock as they ship):

  • UART Case Study (/rtl-design-patterns/case-study-uart) — Chapter 13.1; the full UART (TX + RX + baud generator) this page previews.
  • valid/ready Handshake (/rtl-design-patterns/valid-ready-handshake) — Chapter 9.1; the standard way to feed bytes to a transmitter like this one without dropping them.
  • Adders & Subtractors (/rtl-design-patterns/adders-subtractors) — Chapter 5; the datapath arithmetic a richer controller would drive.

Verilog v1 prerequisites (the grammar this controller transcribes into):

  • Case Statements — the construct behind the next-state and output case blocks, and where default/latch-inference live.
  • Blocking and Non-Blocking Assignments<= in the clocked state/datapath registers, = in the combinational next-state/output logic.
  • If-Else Statements — the if (start) / if (last_bit) transition guards inside the FSM.
  • Concatenation — the {1'b0, shift_reg[7:1]} shift-right that serializes the byte LSB-first.

11. Summary

  • A composed controller is patterns wired together, not a new pattern. The UART-TX-lite transmitter is a safe three-piece FSM (4.1, 4.5) conducting a shift register (2.6) and a bit counter (3.x), all paced by a baud-tick clock-enable (3.5), with the line tx a Moore output (4.2) and a narrow control-down / status-up interface (4.6). Each piece is a lesson you already had; the capstone is composing them.
  • The frame is fixed accounting. START (0) + 8 DATA bits LSB-first + STOP (1), one bit per baud tick, on a line that idles high. Eleven bit-times, and the DATA phase must last exactly eight ticks — leave on bit_cnt == 7, the eighth bit.
  • The FSM only acts on a baud tick. One fast clock; every register advances on the tick, holds between beats. The FSM reads last_bit (a combinational function of the current count) and steers in the same cycle — the control/status timing of 4.6.
  • Every output is decided in every state. tx, busy, done, and next_state are default-assigned first, then overridden per state, so the block is latch-free and glitch-clean (4.5), and reset lands the line idle-high (2.3).
  • Correctness is in the bit-and-tick accounting, and a full-frame decode is what proves it. The signature bug — leaving DATA after seven bits, or dropping the STOP bit — passes an eyeball of the first bits and fails a whole-frame reassembly. Verify by transmitting a known byte (here 0xB5), sampling tx at each bit-time, reassembling START + 8 data (LSB-first) + STOP, and asserting it equals the byte sent — self-checking clocked testbenches shown in SystemVerilog, Verilog, and VHDL.