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
endmodule
Final 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
endmodule
Final 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
endmodule
TX 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

NameRole
WIDTHNumber of CRC bits; sets the register size and the implied polynomial degree.
POLYGenerator polynomial without its top term, e.g. 0x07 for CRC-8, 0x1021 for CRC-16-CCITT.
INITValue loaded on reset before the first bit; 0x00 for CRC-8, 0xFFFF for CRC-16-CCITT.
feedbackXOR of the incoming bit and the top register bit; decides whether the polynomial is applied this cycle.
en / validGates 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 + 1 is written 0x07, not 0x107.
  • Index width bugs. Use crc[WIDTH-1] for the top bit and crc[WIDTH-2:0] for the shift, never hardcoded crc[7], or the module breaks the moment you change WIDTH.

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.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.
▶ Try it in the ChipXpert Lab

Open this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.

Run it in the Lab

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
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD