CRC Verilog Code: Serial LFSR Generator with Testbench
A cyclic redundancy check (CRC) is the workhorse of error detection on serial links, memory interfaces, and protocol framing. The transmitter appends a few check bits computed from the message; the receiver recomputes them and flags any mismatch. This guide walks through writing CRC Verilog code as a serial, bit-at-a-time linear-feedback shift register (LFSR) that you can drop into real RTL. Everything here is plain Verilog-2005 and runs under the open-source Icarus Verilog toolchain.
We will build one crc_serial module whose width, polynomial, and initial value are parameters, then reuse it as both a CRC-8 and a CRC-16-CCITT generator. Each design ships with a self-checking testbench that compares the result against a published check value, so you know the hardware is correct and not just plausible.
How a serial CRC LFSR works
A CRC treats the message as a long binary number and divides it, modulo-2, by a fixed generator polynomial. The remainder of that division is the CRC. Modulo-2 arithmetic has no carries, so “subtraction” is just XOR, which is exactly what makes it cheap in hardware.
The serial form processes one message bit per clock. A register holds the running remainder. On each bit you compute a single feedback term, the XOR of the incoming bit and the top (most significant) register bit. The register then shifts left by one position, and if the feedback bit is set, the generator polynomial is XORed into the register. After the last message bit is clocked in, the register contents are the CRC.
Three constants fully describe a configuration: the width (number of CRC bits), the polynomial (the divisor, with its highest term implied), and the init value preloaded before the first bit. The examples below are MSB-first with no input or output bit reflection and no final XOR, which matches the classic CRC-8 and CRC-16-CCITT definitions.
Parameterized CRC-8 Verilog code
The module below is the complete generator. rst is synchronous and loads INIT; while en is high, one bit on bit_in is absorbed every rising edge. The whole datapath is a single line: concatenate the lower bits with a zero to shift left, then conditionally XOR the polynomial. The testbench feeds the ASCII string “123456789” and checks the canonical CRC-8 result, 0xF4.
// crc_serial.v -- parameterized serial (bit-at-a-time) CRC with an LFSR.
// MSB-first, no input/output reflection, no final XOR.
module crc_serial #(
parameter WIDTH = 8, // CRC register width in bits
parameter [WIDTH-1:0] POLY = 8'h07, // generator polynomial (x^WIDTH term implied)
parameter [WIDTH-1:0] INIT = 8'h00 // value loaded on reset
) (
input wire clk,
input wire rst, // synchronous: reloads INIT
input wire en, // 1 = absorb bit_in this clock
input wire bit_in, // one message bit, fed MSB-first
output reg [WIDTH-1:0] crc
);
// Feedback is the XOR of the incoming bit and the top CRC bit.
wire feedback = bit_in ^ crc[WIDTH-1];
always @(posedge clk) begin
if (rst)
crc <= INIT;
else if (en)
// Shift left one place, then conditionally XOR the polynomial.
crc <= {crc[WIDTH-2:0], 1'b0} ^ (feedback ? POLY : {WIDTH{1'b0}});
end
endmodule
// Testbench: CRC-8 (poly 0x07, init 0x00) over the ASCII string "123456789".
// The canonical check value for this configuration is 0xF4.
module tb_crc8;
reg clk = 1'b0;
reg rst, en, bit_in;
wire [7:0] crc;
crc_serial #(.WIDTH(8), .POLY(8'h07), .INIT(8'h00)) dut (
.clk(clk), .rst(rst), .en(en), .bit_in(bit_in), .crc(crc)
);
always #5 clk = ~clk; // 10 time-unit period
// 9-byte message "123456789"
reg [7:0] msg [0:8];
integer i, b;
initial begin
msg[0]=8'h31; msg[1]=8'h32; msg[2]=8'h33; msg[3]=8'h34; msg[4]=8'h35;
msg[5]=8'h36; msg[6]=8'h37; msg[7]=8'h38; msg[8]=8'h39;
// Reset for one clock to load INIT.
rst = 1'b1; en = 1'b0; bit_in = 1'b0;
@(posedge clk); #1 rst = 1'b0;
// Feed every byte, MSB first, one bit per clock.
for (b = 0; b < 9; b = b + 1) begin
for (i = 7; i >= 0; i = i - 1) begin
en = 1'b1; bit_in = msg[b][i];
@(posedge clk);
end
end
en = 1'b0;
#1;
$display("Final CRC-8 = 0x%02h (expected 0xF4)", crc);
if (crc === 8'hF4) $display("RESULT: PASS");
else $display("RESULT: FAIL");
$finish;
end
endmoduleFinal CRC-8 = 0xf4 (expected 0xF4) RESULT: PASS /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp7dw2uxmf/crc8_serial.v:64: $finish called at 726 (1s)
Reusing the module as CRC-16-CCITT
Because width, polynomial, and init are parameters, the same RTL becomes a 16-bit generator with three overrides: WIDTH = 16, POLY = 0x1021, and INIT = 0xFFFF. This is the configuration often labelled CRC-16-CCITT-FALSE, whose published check value over “123456789” is 0x29B1. Note how nothing inside the datapath changed. That is the payoff of parameterizing by width instead of hardcoding bit indices.
// crc16_ccitt.v -- same parameterized serial LFSR, configured as CRC-16-CCITT.
module crc_serial #(
parameter WIDTH = 16,
parameter [WIDTH-1:0] POLY = 16'h1021,
parameter [WIDTH-1:0] INIT = 16'hFFFF
) (
input wire clk,
input wire rst,
input wire en,
input wire bit_in,
output reg [WIDTH-1:0] crc
);
wire feedback = bit_in ^ crc[WIDTH-1];
always @(posedge clk) begin
if (rst)
crc <= INIT;
else if (en)
crc <= {crc[WIDTH-2:0], 1'b0} ^ (feedback ? POLY : {WIDTH{1'b0}});
end
endmodule
// Testbench: CRC-16-CCITT-FALSE (poly 0x1021, init 0xFFFF) over "123456789".
// The published check value for this configuration is 0x29B1.
module tb_crc16;
reg clk = 1'b0;
reg rst, en, bit_in;
wire [15:0] crc;
crc_serial #(.WIDTH(16), .POLY(16'h1021), .INIT(16'hFFFF)) dut (
.clk(clk), .rst(rst), .en(en), .bit_in(bit_in), .crc(crc)
);
always #5 clk = ~clk;
reg [7:0] msg [0:8];
integer i, b;
initial begin
msg[0]=8'h31; msg[1]=8'h32; msg[2]=8'h33; msg[3]=8'h34; msg[4]=8'h35;
msg[5]=8'h36; msg[6]=8'h37; msg[7]=8'h38; msg[8]=8'h39;
rst = 1'b1; en = 1'b0; bit_in = 1'b0;
@(posedge clk); #1 rst = 1'b0;
for (b = 0; b < 9; b = b + 1) begin
for (i = 7; i >= 0; i = i - 1) begin
en = 1'b1; bit_in = msg[b][i];
@(posedge clk);
end
end
en = 1'b0;
#1;
$display("Final CRC-16-CCITT = 0x%04h (expected 0x29B1)", crc);
if (crc === 16'h29B1) $display("RESULT: PASS");
else $display("RESULT: FAIL");
$finish;
end
endmoduleFinal CRC-16-CCITT = 0x29b1 (expected 0x29B1) RESULT: PASS /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmphxh15dzz/crc16_ccitt.v:58: $finish called at 726 (1s)
Byte-at-a-time variant and the receiver check
Real buses rarely deliver one bit per clock. The next module unrolls the same LFSR eight times inside a combinational function, so it folds a whole byte per clock while producing bit-identical results. The testbench does two useful things: it computes the CRC-8 of the two-byte message {0xAB, 0xCD} (expected 0xE2), then demonstrates the receiver property. Feeding the message followed by its CRC byte makes the remainder collapse to 0x00, which is how a receiver detects a clean frame without comparing values.
// crc8_byte.v -- byte-at-a-time CRC-8 built from a serial LFSR unrolled 8x.
// Same polynomial math as the bit-serial version, but one byte per clock.
module crc8_byte (
input wire clk,
input wire rst, // synchronous: reloads 0x00
input wire valid, // 1 = absorb data_in this clock
input wire [7:0] data_in,
output reg [7:0] crc
);
localparam [7:0] POLY = 8'h07;
// Combinational: fold one byte into the running CRC, MSB-first.
function [7:0] next_crc;
input [7:0] c_in;
input [7:0] d;
integer k;
reg [7:0] c;
reg feedback;
begin
c = c_in;
for (k = 7; k >= 0; k = k - 1) begin
feedback = d[k] ^ c[7];
c = {c[6:0], 1'b0} ^ (feedback ? POLY : 8'h00);
end
next_crc = c;
end
endfunction
always @(posedge clk) begin
if (rst) crc <= 8'h00;
else if (valid) crc <= next_crc(crc, data_in);
end
endmodule
// Testbench: compute CRC-8 of {0xAB, 0xCD} (expected 0xE2), then show the
// receiver property -- feeding the message plus its CRC byte leaves 0x00.
// Stimulus changes on the negative edge so inputs are stable at each posedge.
module tb_crc8_byte;
reg clk = 1'b0;
reg rst, valid;
reg [7:0] data_in;
wire [7:0] crc;
crc8_byte dut (.clk(clk), .rst(rst), .valid(valid),
.data_in(data_in), .crc(crc));
always #5 clk = ~clk;
initial begin
// ---- Transmitter: CRC over {0xAB, 0xCD} ----
rst = 1'b1; valid = 1'b0; data_in = 8'h00;
@(negedge clk); rst = 1'b0; // one clock in reset
@(negedge clk); valid = 1'b1; data_in = 8'hAB;
@(negedge clk); data_in = 8'hCD;
@(negedge clk); valid = 1'b0; // both bytes now absorbed
$display("TX CRC-8 = 0x%02h (expected 0xE2)", crc);
if (crc === 8'hE2) $display("RESULT: PASS"); else $display("RESULT: FAIL");
// ---- Receiver: data + CRC should fold back to 0x00 ----
@(negedge clk); rst = 1'b1;
@(negedge clk); rst = 1'b0;
@(negedge clk); valid = 1'b1; data_in = 8'hAB;
@(negedge clk); data_in = 8'hCD;
@(negedge clk); data_in = 8'hE2;
@(negedge clk); valid = 1'b0;
$display("RX remainder = 0x%02h (expected 0x00)", crc);
if (crc === 8'h00) $display("RESULT: PASS"); else $display("RESULT: FAIL");
$finish;
end
endmoduleTX CRC-8 = 0xe2 (expected 0xE2) RESULT: PASS RX remainder = 0x00 (expected 0x00) RESULT: PASS /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpb2xii4_a/crc8_byte.v:69: $finish called at 100 (1s)
Signals and parameters explained
| Name | Role |
|---|---|
WIDTH | Number of CRC bits; sets the register size and the implied polynomial degree. |
POLY | Generator polynomial without its top term, e.g. 0x07 for CRC-8, 0x1021 for CRC-16-CCITT. |
INIT | Value loaded on reset before the first bit; 0x00 for CRC-8, 0xFFFF for CRC-16-CCITT. |
feedback | XOR of the incoming bit and the top register bit; decides whether the polynomial is applied this cycle. |
en / valid | Gates absorption so the CRC only advances on real data beats. |
Common mistakes
- Wrong bit order. This code is MSB-first. Many standards (CRC-32, CRC-16-MODBUS) reflect input and output, meaning LSB-first. If your check value is off, confirm the reflection convention before touching the polynomial.
- Forgetting the init value. CRC-16-CCITT starts at
0xFFFF, not zero. Loading the wrong init silently produces a valid-looking but wrong remainder. - Testbench timing races. If stimulus changes on the same rising edge the register samples, the DUT may capture the new or old value nondeterministically. Drive inputs on the opposite (negative) edge so they are stable at every sampling edge, as the byte-wide testbench does.
- Misreading the polynomial. The top term is implied. A degree-8 polynomial is stored in 8 bits, so CRC-8’s
x^8 + x^2 + x + 1is written0x07, not0x107. - Index width bugs. Use
crc[WIDTH-1]for the top bit andcrc[WIDTH-2:0]for the shift, never hardcodedcrc[7], or the module breaks the moment you changeWIDTH.
Compile and run any example with iverilog -g2005 -o sim crc8_serial.v followed by vvp sim. A passing run prints the computed value alongside the expected one and the line RESULT: PASS.
Quick check
Why is the feedback term computed before the shift, not after?
The feedback uses the current top bit of the register. Shifting first would discard that bit, so you must XOR the incoming data with crc[WIDTH-1] while it still holds the pre-shift value, then shift and conditionally apply the polynomial.
What does the receiver remainder of 0x00 tell you?
When the message and its appended CRC are run through the same generator (with init 0x00 and no final XOR), a zero remainder means no bit errors were detected. A non-zero remainder means the frame is corrupt.
How do you turn this CRC-8 module into CRC-16-CCITT without editing the datapath?
Override the parameters at instantiation: set WIDTH to 16, POLY to 0x1021, and INIT to 0xFFFF. The shift-and-XOR logic is written in terms of WIDTH, so it scales automatically.
Why does the byte-wide version give the same result as the bit-serial one?
The combinational function simply unrolls the identical feedback-and-shift operation eight times, once per bit of the byte, MSB-first. It performs the same modulo-2 division, just in a single clock instead of eight.
Share your question in comments or talk to our mentor team for batch guidance.
Open this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses