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.
// 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) FIRST2. 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 flowThe 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 flowOne 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 toIDLEand forcingtxto 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 offstart) — the clock-enable discipline of 3.5. One fastclk; the machine runs at the bit rate. - Outputs are default-assigned in every state — the latch-free, glitch-free style of 4.5.
txis 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.
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
endmoduleTwo 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.
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
endmodule4b. 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.
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 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
endmodule4c. 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.
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; 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.
// 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 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
endmoduleThe 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.
// 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 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 onebaud_tickper bit-time, sampletxat each bit-time, and reassemble START + 8 data (LSB-first) + STOP. The reassembled byte must equal the input byte, the START bit must be0, and the STOP bit must be1.
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: pulsebaud_tick, sampletxat that bit-time, and store it. Sample eleven bit-times — one START, eight DATA (intorx_byte[0..7], LSB-first), one STOP — then assertstart_bit == 0,stop_bit == 1, andrx_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 (SVassert (rx_byte === exp_byte), Verilogif (rx_byte !== exp_byte) $display("FAIL…"), VHDLassert rx_byte = exp_byte report … severity error). - The output==input invariant. Stated as a property that holds for every byte: the sequence sampled off
txat bit-times, stripped of its START and STOP, LSB-first, equalsdata. If it ever fails, the serializer is broken. Sweep several bytes —0x00,0xFF,0xB5,0x01,0x80, a few random — because a byte like0x00(all data bits0) or0xFF(all1) can hide a bit-count error that a mixed byte like0xB5exposes; use both. - Framing and idle invariants. Assert
tx == 1after reset and in IDLE (the line idles high, or a receiver sees a phantom START). Assertbusyrises whenstartis accepted and stays high through the frame, then falls in IDLE; assertdonepulses 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, andbit_cnt. Drive many idle (non-tick) cycles between ticks in the testbench (as the §4 tasks do) and confirmtxdoes 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
startpulse during a frame must be ignored (the FSM is not in IDLE, so it does not re-load) — verify a mid-framestartdoes not corrupt the in-flight byte. Astarton the same cycle as abaud_tick; back-to-back frames (a secondstartright afterdone); 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
txidles high; onstartit goes low for one bit-time (START), then presentsdata[0], data[1], …, data[7]each for one bit-time (DATA), then high for one bit-time (STOP), then idle high.busyframes the whole active window;doneis a single-cycle pulse at the end. Atxthat 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.
10. Related Tutorials
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
txis 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_statelatch-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
caseblocks, and wheredefault/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
txa 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 onbit_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, andnext_stateare 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), samplingtxat 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.