Skip to main content

Example Walkthrough: A Serial Frame Receiver in Verilog

Digital Design's FSM Example Walkthrough designed a complete serial frame receiver controller — state diagram, state table, Gray-coded state assignment, excitation equations — entirely on paper. This page is that same design, coded in Verilog, using nearly every construct this topic has covered: modules and ports, vectors, a shift register, a counter, a 3-process FSM, and a testbench to exercise it.

The system, as a block diagram​

RX feeds both the Control FSM and the SIPO shift register; the FSM drives shift_en/count_clear into the shift register and counter, the mod-8 counter reports count_done back to the FSM, bit_tick drives the FSM, and the FSM outputs done/framing_error

This is the exact division of labor from the Digital Design page — the FSM decides when, the datapath (shift register + counter) does the what — reproduced here unchanged, because Verilog doesn't alter the design, only how it's expressed.

The datapath: SIPO shift register and mod-8 counter​

module sipo_register (
input clk,
input rst,
input shift_en,
input rx,
output [7:0] rx_data
);
reg [7:0] shreg;

always @(posedge clk or posedge rst) begin
if (rst) shreg <= 8'b0;
else if (shift_en) shreg <= {rx, shreg[7:1]}; // shift rx in from the MSB side
end

assign rx_data = shreg;
endmodule
module mod8_counter (
input clk,
input rst,
input clear,
input en,
output count_done
);
reg [2:0] count;

always @(posedge clk or posedge rst) begin
if (rst) count <= 3'b0;
else if (clear) count <= 3'b0;
else if (en) count <= count + 1'b1;
end

assign count_done = (count == 3'd7);
endmodule

Both are direct, unremarkable applications of earlier pages: sipo_register is a clocked shift register using the concatenation operator ({rx, shreg[7:1]}) from Operators and Expressions to shift one new bit in every cycle; mod8_counter is a synchronous counter using the sequential always rules from Combinational vs. Sequential always. Neither module contains a single line of FSM logic — exactly the separation of concerns the block diagram shows.

The FSM, in 3-process style​

Reusing the exact Gray-coded state assignment from the Digital Design walkthrough:

module frame_fsm (
input clk,
input rst,
input rx,
input bit_tick,
input count_done,
output reg shift_en,
output reg count_clear,
output reg done,
output reg framing_error
);
localparam IDLE = 3'b000,
START = 3'b001,
DATA = 3'b011,
STOP = 3'b010,
DONE = 3'b110,
ERROR = 3'b111;

reg [2:0] state, next_state;

// Process 1: state register
always @(posedge clk or posedge rst) begin
if (rst) state <= IDLE;
else state <= next_state;
end

// Process 2: next-state logic
always @(*) begin
next_state = state;
case (state)
IDLE: next_state = rx ? IDLE : START;
START: if (bit_tick) next_state = DATA;
DATA: if (bit_tick && count_done) next_state = STOP;
STOP: if (bit_tick) next_state = rx ? DONE : ERROR;
DONE: next_state = IDLE;
ERROR: next_state = IDLE;
default: next_state = IDLE;
endcase
end

// Process 3: Moore output logic
always @(*) begin
shift_en = 0;
count_clear = 0;
done = 0;
framing_error = 0;
case (state)
IDLE: count_clear = 1;
START: count_clear = 1;
DATA: shift_en = 1;
DONE: done = 1;
ERROR: framing_error = 1;
endcase
end
endmodule

This is a direct transcription of the Digital Design page's state table — every case branch here corresponds exactly to one row of that table, and the default-assignment pattern at the top of Process 3 is the latch-avoidance technique from Conditional and Case Statements, applied to five outputs at once instead of writing every output in every branch by hand.

Top-level: wiring the FSM to the datapath​

module frame_receiver (
input clk,
input rst,
input rx,
input bit_tick,
output [7:0] rx_data,
output done,
output framing_error
);
wire shift_en, count_clear, count_done;

frame_fsm u_fsm (
.clk(clk), .rst(rst), .rx(rx), .bit_tick(bit_tick),
.count_done(count_done),
.shift_en(shift_en), .count_clear(count_clear),
.done(done), .framing_error(framing_error)
);

sipo_register u_shreg (
.clk(clk), .rst(rst), .shift_en(shift_en), .rx(rx), .rx_data(rx_data)
);

mod8_counter u_counter (
.clk(clk), .rst(rst), .clear(count_clear), .en(shift_en), .count_done(count_done)
);
endmodule

Three module instances, named ports throughout (per the recommendation in Modules and Ports), wired exactly per the block diagram — frame_fsm's shift_en/count_clear outputs become the datapath's control inputs, and mod8_counter's count_done feeds back into the FSM, closing the loop shown at the top of this page.

A testbench to exercise it​

module frame_receiver_tb;
reg clk = 0, rst, rx, bit_tick;
wire [7:0] rx_data;
wire done, framing_error;

frame_receiver dut (
.clk(clk), .rst(rst), .rx(rx), .bit_tick(bit_tick),
.rx_data(rx_data), .done(done), .framing_error(framing_error)
);

always #5 clk = ~clk; // 10-unit-period clock, per Timing Controls

task send_bit(input b);
begin
rx = b;
@(posedge clk); bit_tick = 1;
@(posedge clk); bit_tick = 0;
end
endtask

initial begin
rst = 1; rx = 1; bit_tick = 0;
@(posedge clk); rst = 0;

send_bit(0); // start bit
send_bit(1); send_bit(0); send_bit(1); send_bit(1);
send_bit(0); send_bit(0); send_bit(1); send_bit(0); // 8 data bits: 10110010
send_bit(1); // stop bit

@(posedge clk);
if (done && rx_data == 8'b01001101)
$display("PASS: received %b", rx_data);
else
$display("FAIL: done=%b rx_data=%b", done, rx_data);

$finish;
end
endmodule

send_bit is exactly the task construct from Functions and Tasks — it bundles "drive rx, then pulse bit_tick for one clock" into one reusable, callable unit, avoiding ten near-identical copies of that same three-line sequence. The final check is the self-checking pattern from Simple Testbenches: comparing the DUT's actual output against the expected byte in code, rather than relying on a human reading a waveform.

What this example ties together​

Every page in this topic contributed something to this one design: modules and ports structured it, vectors and operators built its datapath, all three modeling styles were available for any given block (this example leans behavioral throughout, the style that scales best to designs this size), the blocking/nonblocking and combinational/sequential rules kept every always block correct, the 3-process FSM style organized the control logic predictably, and a self-checking testbench closed the loop by proving the RTL matches the design intent — the same intent Digital Design worked out on paper, several topics ago.

Beyond this topic

This topic covered the Verilog language itself — enough to read and write real, synthesizable RTL. It deliberately doesn't cover verification methodology (structured stimulus generation, functional coverage, scoreboards, reusable verification environments) — that's the entire subject of SystemVerilog, Testbench, and UVM, which build directly on the language foundation from this topic to answer a different question: not "how do I describe this hardware," but "how do I prove this hardware is correct."