CRC数据完整性检测与RTL实现


1 前言

        CRC(Cyclic Redundancy Check,循环冗余校验)是数字系统、网络协议、存储系统和片上互连中最常见的数据完整性检测方法之一。它的目标不是加密,也不是纠错,而是在 packet、frame、block 或 flit 级别尽可能高概率地发现传输或存储过程中发生的数据错误。

        在 RTL 设计中,CRC 看起来只是一个 LFSR(Linear Feedback Shift Register,线性反馈移位寄存器),但工程中真正容易出错的地方通常集中在:

        本文按照“先概念、再数学模型、再串行 RTL、再并行 RTL、再 512b packet 级生成与校验”的顺序展开。重点围绕 CRC32 实现,学习目标是理解 packet 级数据完整性检测,并能够设计一个支持 64B / 512b 并行输入的 CRC32 generator 和 checker。


2 阅读路线与文章结构

        CRC 的学习很容易陷入公式和协议参数细节。为了更系统地理解,建议分成六层:

基础术语
  -> CRC 数学模型
  -> LFSR 串行实现
  -> 并行 CRC 展开
  -> CRC32 packet 级生成与校验
  -> pipeline、bit 顺序和工程 checklist

        对应的学习目标如下:

层次 内容 对应章节
基础概念 CRC、polynomial、seed、xorout、reflect、residue 第 3 节
数学模型 GF(2) 除法、生成多项式、错误检测能力 第 4 节
串行 RTL 1 bit/cycle LFSR CRC32 generator/checker 第 5 节
并行 RTL 512b combinational 展开、64B packet 更新 第 6 节
Packet 级设计 init、update、last、crc_valid、check_pass 第 7 节
工程收束 pipeline、时序、验证、参考资料 第 8 ~ 11 节

        下图适合放置 CRC 学习路径图:

CRC学习路径图

image2 prompt:生成一张中文技术博客风格的 CRC 学习路径图。画面从左到右依次为“基础术语”“多项式与 GF(2)”“串行 LFSR”“并行展开”“CRC32 生成/校验”“Pipeline 与验证”。风格简洁,蓝绿色科技配色,适合数字 IC / FPGA 教程文章。


3 基础概念和常用术语

3.1 CRC 是什么

        CRC 是一种基于 GF(2) 多项式除法的校验码。发送端把 payload 数据看成一个二进制多项式,使用约定好的生成多项式 polynomial 做模 2 除法,得到余数 remainder。这个余数就是 CRC 值。接收端收到 payload 和 CRC 后,使用相同参数重新计算,判断数据是否被破坏。

        CRC 的典型使用流程如下:

发送端:
payload -> CRC generator -> append crc -> packet

接收端:
packet payload + received crc -> CRC checker -> pass / fail

        CRC 不是密码学 hash,不能防止恶意篡改;CRC 也不是 ECC,通常不能定位并纠正错误。它主要用于检测随机 bit error、burst error、截断、拼接或传输损坏。

3.2 常用术语

术语 含义
CRC width CRC 余数宽度,例如 CRC32 的宽度为 32 bit
Polynomial / Poly 生成多项式,决定 LFSR feedback tap,也决定错误检测能力
MSB-first 先处理每个字节或 bit vector 的最高位
LSB-first / Reflected 先处理最低位,常见于 Ethernet、ZIP、PNG 等 CRC32 实现
Init / Seed CRC 初始值,常见 CRC32 初始值为 32'hffff_ffff
Xorout / Final XOR 输出前与固定值异或,常见 CRC32 xorout 为 32'hffff_ffff
Reflect In 输入 byte 内 bit 是否反转处理
Reflect Out 输出 CRC 是否反转
Residue 对完整 codeword 做 CRC 后得到的固定余数,可用于 checker
FCS Frame Check Sequence,很多协议中对追加 CRC 字段的称呼
Syndrome 校验结果,常用于表示 checker 得到的错误综合信息
Packet-level check 以一个完整 packet 为单位计算和比较 CRC

3.3 CRC32 常见参数

        “CRC32”并不是唯一算法。工程中必须同时确认 polynomial、init、xorout、reflect 等参数。常见配置如下:

名称 Polynomial 正向表示 Reflected polynomial Init Reflect In/Out Xorout 常见场景
CRC-32/ISO-HDLC 0x04C11DB7 0xEDB88320 0xffffffff true / true 0xffffffff Ethernet FCS、ZIP、PNG
CRC-32C/Castagnoli 0x1EDC6F41 0x82F63B78 0xffffffff true / true 0xffffffff iSCSI、SCTP、存储系统
MPEG-2 CRC32 0x04C11DB7 不反射 0xffffffff false / false 0x00000000 MPEG-2

        本文 RTL 使用最常见的 reflected CRC-32/ISO-HDLC 参数:

WIDTH     = 32
POLY      = 32'hEDB8_8320  // reflected polynomial
INIT      = 32'hFFFF_FFFF
XOROUT    = 32'hFFFF_FFFF
bit order = LSB-first

        需要注意:0x04C11DB70xEDB88320 表示的是同一个 CRC32 polynomial 的两种 bit 顺序视角。MSB-first RTL 常用 0x04C11DB7,LSB-first/reflected RTL 常用 0xEDB88320

3.4 CRC 与 checksum 的区别

方法 运算方式 优点 局限
Parity 所有 bit 异或 极简单 检测能力弱,只能可靠检测奇数个 bit 翻转
Checksum 加法累加后取反或取模 软件实现简单 对 bit 顺序和某些重排错误不敏感
CRC GF(2) 多项式除法 对 burst error 检测能力强,适合硬件 LFSR 参数和 bit 顺序容易混淆
ECC 加冗余位定位错误 可纠错 面积和复杂度更高

4 CRC 数学模型:GF(2) 多项式除法

4.1 GF(2) 运算

        CRC 的底层运算位于 GF(2),也就是每个系数只有 0 和 1。GF(2) 中的加法和减法都等价于 XOR:

0 + 0 = 0
0 + 1 = 1
1 + 0 = 1
1 + 1 = 0

所以:
加法 = 减法 = XOR

        这也是为什么 CRC 在硬件中通常只需要 XOR gate 和 register 就能实现。

4.2 多项式表示

        在 CRC 文章中经常会看到 x^3x^2G(x) 这样的写法。这里的 x 不是要求解的未知数,也不是 RTL 里的信号,而是多项式表示中的一个形式符号。它的作用是标记 bit 所在的位置,或者说标记这个 bit 对应的“阶数”。

        可以先把 x 理解成一个占位符:

x^0 表示第 0 位
x^1 表示第 1 位
x^2 表示第 2 位
x^3 表示第 3 位
...

        因此,一个二进制串可以按照 bit 位置写成多项式。假设数据为 1101,并且左边是最高位、右边是最低位:

data bits = 1101

bit 位置:
bit3 bit2 bit1 bit0
 1    1    0    1

对应多项式:
1*x^3 + 1*x^2 + 0*x^1 + 1*x^0

        因为系数为 0 的项可以省略,x^0 = 1,所以可以简写成:

x^3 + x^2 + 1

        这个写法只是把 bit 串换成另一种等价表达。它不是说 x 要取某个数值,也不是说硬件里真的要做乘法或幂运算。CRC 的硬件实现依然只是在做 shift 和 XOR。

        用表格看会更直观:

bit 串 展开写法 省略 0 系数后的多项式
1011 1*x^3 + 0*x^2 + 1*x^1 + 1*x^0 x^3 + x + 1
1000 1*x^3 + 0*x^2 + 0*x^1 + 0*x^0 x^3
0110 0*x^3 + 1*x^2 + 1*x^1 + 0*x^0 x^2 + x
0001 0*x^3 + 0*x^2 + 0*x^1 + 1*x^0 1

        CRC32 的生成多项式宽度为 32,正向写法通常表示为:

G(x) = x^32 + x^26 + x^23 + x^22 + x^16 + x^12 + x^11
     + x^10 + x^8 + x^7 + x^5 + x^4 + x^2 + x + 1

        这里 G(x) 的意思是“生成多项式 generator polynomial”。最高项是 x^32,表示这是一个 32 阶生成多项式,所以最终 CRC 余数宽度是 32 bit。后面的 x^26x^23x^22 等项表示对应位置有 tap,也就是 LFSR 反馈路径中需要参与 XOR 的位置。

        工程中常见的 0x04C11DB7 是这个多项式去掉最高项 x^32 后的二进制编码。原因是 CRC32 的最高项固定为 1,用 32 bit 常量记录时不再额外保存这个隐含的最高项。

G(x) 去掉 x^32 后:
x^26 + x^23 + x^22 + x^16 + x^12 + x^11
+ x^10 + x^8 + x^7 + x^5 + x^4 + x^2 + x + 1

对应 32 bit 常量:
0x04C11DB7

        如果使用 reflected / LSB-first 算法,同一个生成多项式会按 bit 反射形式写成 0xEDB88320。因此看到 0x04C11DB70xEDB88320 时,不要简单认为它们是两个完全不同的 CRC;它们通常是同一个 CRC32 polynomial 在 MSB-first 和 LSB-first 两种实现视角下的表示。

4.3 Generator 和 Checker 的基本思想

        发送端计算:

crc = remainder(payload)
transmit = payload || crc

        接收端可以有两种常见检查方式:

检查方式 方法 说明
Recompute and compare 对 payload 重新算 CRC,再与 received CRC 比较 最直观,RTL 容易写
Residue check 对 payload + crc 全部喂入 CRC checker,检查最终 residue 协议中常用,需要注意 crc 字段 bit 顺序

        本文为了避免混淆,packet checker 使用 recompute and compare:对 payload 重新计算 CRC32,输出 crc_calc,再与输入的 crc_expected 比较得到 check_pass


5 串行 CRC32:1 bit/cycle LFSR 实现

5.1 LFSR 更新规则

        对 reflected CRC32,LSB-first 的一 bit 更新规则如下:

feedback = crc_q[0] ^ data_bit;

if (feedback) begin
    crc_d = (crc_q >> 1) ^ POLY;
end else begin
    crc_d = (crc_q >> 1);
end

        其中 POLY = 32'hEDB8_8320。每输入一个 bit,CRC 状态更新一次。一个 byte 需要更新 8 次,一个 64B packet 需要更新 512 次。

        下面位置适合放置 reflected CRC32 LFSR 结构图:

CRC32 LFSR结构图

image2 prompt:生成一张 reflected CRC32 LFSR 结构图。包含 32 个寄存器 bit,右移方向,输入 data_bit 与 crc[0] 异或产生 feedback,feedback 根据 polynomial 0xEDB88320 连接到 tap XOR。标注 LSB-first、seed=0xffffffff、final xor=0xffffffff。中文标注,适合 RTL 教程。

5.2 串行 CRC32 Generator RTL

        下面的模块每周期最多处理 1 bit。init 用于 packet 开始时装载 seed,data_valid 表示本周期输入 bit 有效,finish 表示输出最终 CRC。为了保持接口简单,finish 不改变内部状态,只决定 crc_out 的可观察值。

module crc32_serial #(
    parameter logic [31:0] CRC_INIT  = 32'hffff_ffff,
    parameter logic [31:0] CRC_POLY  = 32'hedb8_8320,
    parameter logic [31:0] CRC_XOROUT = 32'hffff_ffff
) (
    // Clock and reset
    input  logic        clk,
    input  logic        rst_n,

    // Control
    input  logic        init,
    input  logic        data_valid,
    input  logic        data_bit,
    input  logic        finish,

    // Status
    output logic [31:0] crc_state,
    output logic [31:0] crc_out
);

    logic [31:0] crc_q;
    logic [31:0] crc_next;
    logic        feedback;

    assign feedback = crc_q[0] ^ data_bit;

    always_comb begin
        crc_next = crc_q >> 1;
        if (feedback) begin
            crc_next = crc_next ^ CRC_POLY;
        end
    end

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            crc_q <= CRC_INIT;
        end else if (init) begin
            crc_q <= CRC_INIT;
        end else if (data_valid) begin
            crc_q <= crc_next;
        end
    end

    assign crc_state = crc_q;
    assign crc_out   = finish ? (crc_q ^ CRC_XOROUT) : crc_q;

endmodule

        这个模块适合学习 LFSR 的状态更新过程,但对于 64B / 512b packet,如果只用 1 bit/cycle,需要 512 个周期才能处理完一个 packet,吞吐率通常不能满足高速 datapath 要求。因此工程中会展开成并行 CRC。

5.3 串行 CRC32 Testbench

`timescale 1ns/1ps

module tb_crc32_serial;

    localparam logic [31:0] CRC_INIT   = 32'hffff_ffff;
    localparam logic [31:0] CRC_XOROUT = 32'hffff_ffff;

    logic        clk;
    logic        rst_n;
    logic        init;
    logic        data_valid;
    logic        data_bit;
    logic        finish;
    logic [31:0] crc_state;
    logic [31:0] crc_out;

    crc32_serial #(
        .CRC_INIT  (CRC_INIT),
        .CRC_XOROUT(CRC_XOROUT)
    ) u_dut (
        .clk,
        .rst_n,
        .init,
        .data_valid,
        .data_bit,
        .finish,
        .crc_state,
        .crc_out
    );

    initial clk = 1'b0;
    always #5 clk = ~clk;

    function automatic logic [31:0] crc32_ref_byte(
        input logic [31:0] crc_i,
        input logic [7:0]  data_i
    );
        logic [31:0] crc_t;
        logic        feedback;
        crc_t = crc_i;
        for (int bit_idx = 0; bit_idx < 8; bit_idx++) begin
            feedback = crc_t[0] ^ data_i[bit_idx];
            crc_t    = crc_t >> 1;
            if (feedback) begin
                crc_t = crc_t ^ 32'hedb8_8320;
            end
        end
        return crc_t;
    endfunction

    function automatic logic [31:0] crc32_ref_string(input string msg);
        logic [31:0] crc_t;
        crc_t = CRC_INIT;
        for (int char_idx = 0; char_idx < msg.len(); char_idx++) begin
            crc_t = crc32_ref_byte(crc_t, msg[char_idx]);
        end
        return crc_t ^ CRC_XOROUT;
    endfunction

    task automatic drive_idle();
        @(negedge clk);
        init       = 1'b0;
        data_valid = 1'b0;
        data_bit   = 1'b0;
        finish     = 1'b0;
    endtask

    task automatic apply_init();
        @(negedge clk);
        init       = 1'b1;
        data_valid = 1'b0;
        data_bit   = 1'b0;
        finish     = 1'b0;
        @(negedge clk);
        init = 1'b0;
    endtask

    task automatic send_byte(input logic [7:0] data_i);
        for (int bit_idx = 0; bit_idx < 8; bit_idx++) begin
            @(negedge clk);
            data_valid = 1'b1;
            data_bit   = data_i[bit_idx];
        end
        @(negedge clk);
        data_valid = 1'b0;
        data_bit   = 1'b0;
    endtask

    task automatic check_string(input string msg, input logic [31:0] expected_crc);
        logic [31:0] ref_crc;

        apply_init();

        for (int char_idx = 0; char_idx < msg.len(); char_idx++) begin
            send_byte(msg[char_idx]);
        end

        @(negedge clk);
        finish = 1'b1;
        #1;

        ref_crc = crc32_ref_string(msg);

        assert(crc_out == expected_crc)
            else $fatal("Known CRC mismatch: msg=%s got=%08x exp=%08x",
                        msg, crc_out, expected_crc);

        assert(crc_out == ref_crc)
            else $fatal("Reference CRC mismatch: msg=%s got=%08x ref=%08x",
                        msg, crc_out, ref_crc);

        drive_idle();
    endtask

    initial begin
        rst_n      = 1'b0;
        init       = 1'b0;
        data_valid = 1'b0;
        data_bit   = 1'b0;
        finish     = 1'b0;

        repeat (4) @(negedge clk);
        rst_n = 1'b1;
        repeat (2) @(negedge clk);

        assert(crc_state == CRC_INIT)
            else $fatal("Reset state mismatch");

        check_string("",          32'h0000_0000);
        check_string("123456789", 32'hcbf4_3926);
        check_string("a",         32'he8b7_be43);

        // Back-to-back packets must not affect each other after init.
        check_string("hello",     crc32_ref_string("hello"));
        check_string("world",     crc32_ref_string("world"));

        $display("tb_crc32_serial PASS");
        $finish;
    end

endmodule

        串行 CRC32 testbench 覆盖的测试场景如下:

编号 测试场景 主要激励 期望结果
1 Reset state rst_n = 0 后释放 crc_state = CRC_INIT
2 Empty message init 后不输入任何 bit,直接 finish 输出 CRC_INIT ^ XOROUT = 0
3 标准 check vector 输入 ASCII 字符串 "123456789" 输出标准 CRC32 32'hcbf4_3926
4 单 byte packet 输入 "a" 输出 32'he8b7_be43
5 Back-to-back packet 连续测试 "hello""world",每个 packet 前 init packet 之间互不污染
6 Reference model 对比 testbench 内部使用同样数学定义计算 reference DUT 与 reference 完全一致

5.4 串行 CRC 时序

        下面的 WaveDrom 展示了 packet 开始时 init 装载 seed,随后连续输入 bit,最后 finish 输出 crc_out 的过程。

{
  signal: [
    { name: "clk",        wave: "p..........." },
    { name: "rst_n",      wave: "01.........." },
    { name: "init",       wave: "0.10........" },
    { name: "data_valid", wave: "0...11110..." },
    { name: "data_bit",   wave: "x...=.=.=...", data: ["b0", "b1", "b2"] },
    { name: "crc_state",  wave: "x.=.=.=.=...", data: ["INIT", "s1", "s2", "s3", "s4"] },
    { name: "finish",     wave: "0........10." },
    { name: "crc_out",    wave: "x........=..", data: ["state^xorout"] }
  ],
  head: {
    text: "Serial CRC32: init, bit update, finish"
  }
}

6 并行 CRC32:512b / 64B 一拍更新

6.1 为什么需要并行 CRC

        在高速 packet datapath 中,数据总线常见宽度为 64b、128b、256b 或 512b。如果 CRC 模块每拍只处理 1 bit,就会成为吞吐瓶颈。例如 512b datapath 每拍输入一个 64B packet,需要 CRC 模块在一拍内完成 512 次 LFSR 等价更新,或者通过 pipeline 分多拍但保持高吞吐。

        并行 CRC 的核心思想是:串行 LFSR 的多次更新可以在组合逻辑中展开。也就是说,一拍内执行一个 for loop,综合工具会把它展开成 XOR 网络。

crc_t = crc_i;
for (int bit_idx = 0; bit_idx < DATA_WIDTH; bit_idx++) begin
    feedback = crc_t[0] ^ data_i[bit_idx];
    crc_t    = crc_t >> 1;
    if (feedback) begin
        crc_t = crc_t ^ CRC_POLY;
    end
end

        这个 for loop 在 RTL 语义上是组合逻辑,不是运行时循环。综合后会形成由 DATA_WIDTH 层 XOR 和 mux-equivalent 逻辑组成的网络。

6.2 512b 并行 CRC32 Generator RTL

        下面给出一个 packet 级 CRC32 generator。它支持固定 512b 输入,即每个 packet 正好 64B。data_valid 为 1 时一拍处理完整 512b payload;crc_valid 在同一拍组合输出有效结果,也可以通过 OUT_REG_EN 选择打一拍输出以改善时序。

module crc32_parallel_512 #(
    parameter int          DATA_WIDTH = 512,
    parameter logic [31:0] CRC_INIT   = 32'hffff_ffff,
    parameter logic [31:0] CRC_POLY   = 32'hedb8_8320,
    parameter logic [31:0] CRC_XOROUT = 32'hffff_ffff,
    parameter bit          OUT_REG_EN = 1'b1
) (
    // Clock and reset
    input  logic                  clk,
    input  logic                  rst_n,

    // Input packet
    input  logic                  data_valid,
    input  logic [DATA_WIDTH-1:0] data,

    // Output CRC
    output logic                  crc_valid,
    output logic [31:0]           crc
);

    logic [31:0] crc_comb;
    logic [31:0] crc_final_comb;

    function automatic logic [31:0] crc32_update_parallel(
        input logic [31:0]           crc_i,
        input logic [DATA_WIDTH-1:0] data_i
    );
        logic [31:0] crc_t;
        logic        feedback;

        crc_t = crc_i;
        for (int bit_idx = 0; bit_idx < DATA_WIDTH; bit_idx++) begin
            feedback = crc_t[0] ^ data_i[bit_idx];
            crc_t    = crc_t >> 1;
            if (feedback) begin
                crc_t = crc_t ^ CRC_POLY;
            end
        end
        return crc_t;
    endfunction

    initial begin
        assert (DATA_WIDTH > 0)
            else $fatal(1, "DATA_WIDTH must be greater than 0");
        assert ((DATA_WIDTH % 8) == 0)
            else $fatal(1, "DATA_WIDTH must be byte aligned");
    end

    always_comb begin
        crc_comb       = crc32_update_parallel(CRC_INIT, data);
        crc_final_comb = crc_comb ^ CRC_XOROUT;
    end

    generate
        if (OUT_REG_EN) begin : OUT_REG
            always_ff @(posedge clk or negedge rst_n) begin
                if (!rst_n) begin
                    crc_valid <= 1'b0;
                    crc       <= '0;
                end else begin
                    crc_valid <= data_valid;
                    if (data_valid) begin
                        crc <= crc_final_comb;
                    end
                end
            end
        end else begin : OUT_COMB
            assign crc_valid = data_valid;
            assign crc       = crc_final_comb;
        end
    endgenerate

endmodule

        这个模块的输入 data[0] 是第一个被处理的 bit,也就是 LSB-first。若协议或上游总线定义为 byte0 在低 8 bit、byte 内 LSB-first,则可以直接连接。如果上游是 MSB-first 或 byte lane 顺序不同,必须在进入 CRC 前做 bit/byte reorder。

6.3 512b 并行 CRC32 Testbench

`timescale 1ns/1ps

module tb_crc32_parallel_512;

    localparam int DATA_WIDTH = 512;

    logic                  clk;
    logic                  rst_n;
    logic                  data_valid;
    logic [DATA_WIDTH-1:0] data;
    logic                  crc_valid;
    logic [31:0]           crc;

    crc32_parallel_512 #(
        .DATA_WIDTH(DATA_WIDTH),
        .OUT_REG_EN(1'b1)
    ) u_dut (
        .clk,
        .rst_n,
        .data_valid,
        .data,
        .crc_valid,
        .crc
    );

    initial clk = 1'b0;
    always #5 clk = ~clk;

    function automatic logic [31:0] crc32_ref_512(input logic [DATA_WIDTH-1:0] data_i);
        logic [31:0] crc_t;
        logic        feedback;

        crc_t = 32'hffff_ffff;
        for (int bit_idx = 0; bit_idx < DATA_WIDTH; bit_idx++) begin
            feedback = crc_t[0] ^ data_i[bit_idx];
            crc_t    = crc_t >> 1;
            if (feedback) begin
                crc_t = crc_t ^ 32'hedb8_8320;
            end
        end
        return crc_t ^ 32'hffff_ffff;
    endfunction

    task automatic drive_packet(input logic [DATA_WIDTH-1:0] data_i);
        logic [31:0] exp_crc;

        exp_crc = crc32_ref_512(data_i);

        @(negedge clk);
        data_valid = 1'b1;
        data       = data_i;

        @(posedge clk);
        #1;
        assert(crc_valid == 1'b1)
            else $fatal("crc_valid should be asserted");
        assert(crc == exp_crc)
            else $fatal("CRC mismatch: got=%08x exp=%08x", crc, exp_crc);

        @(negedge clk);
        data_valid = 1'b0;
        data       = '0;
    endtask

    initial begin
        rst_n      = 1'b0;
        data_valid = 1'b0;
        data       = '0;

        repeat (4) @(negedge clk);
        rst_n = 1'b1;
        repeat (2) @(negedge clk);

        assert(crc_valid == 1'b0);

        // All-zero 64B packet.
        drive_packet('0);

        // All-one 64B packet.
        drive_packet({DATA_WIDTH{1'b1}});

        // Walking-one pattern.
        for (int bit_idx = 0; bit_idx < DATA_WIDTH; bit_idx += 37) begin
            logic [DATA_WIDTH-1:0] walking_data;
            walking_data = '0;
            walking_data[bit_idx] = 1'b1;
            drive_packet(walking_data);
        end

        // Byte ramp pattern: byte0 = 0, byte1 = 1, ...
        begin
            logic [DATA_WIDTH-1:0] ramp_data;
            ramp_data = '0;
            for (int byte_idx = 0; byte_idx < DATA_WIDTH / 8; byte_idx++) begin
                ramp_data[byte_idx*8 +: 8] = 8'(byte_idx);
            end
            drive_packet(ramp_data);
        end

        // Random packets.
        for (int pkt_idx = 0; pkt_idx < 200; pkt_idx++) begin
            logic [DATA_WIDTH-1:0] rand_data;
            for (int word_idx = 0; word_idx < DATA_WIDTH / 32; word_idx++) begin
                rand_data[word_idx*32 +: 32] = $urandom();
            end
            drive_packet(rand_data);
        end

        $display("tb_crc32_parallel_512 PASS");
        $finish;
    end

endmodule

        512b 并行 CRC32 testbench 覆盖的测试场景如下:

编号 测试场景 主要激励 期望结果
1 Reset 复位释放后不输入 packet crc_valid = 0,输出寄存器处于已知状态
2 All-zero packet 512b 全 0 DUT 输出与 reference model 一致
3 All-one packet 512b 全 1 DUT 输出与 reference model 一致
4 Walking-one 每隔 37 bit 生成一个单 bit 置 1 的 packet 覆盖各 bit lane 对 CRC 的影响
5 Byte ramp 64 个 byte 依次为 0x00 ~ 0x3f 覆盖 byte lane 顺序
6 Random packet 200 个随机 512b packet 大量随机数据下 DUT 与 reference model 一致

6.4 并行 CRC 时序

{
  signal: [
    { name: "clk",        wave: "p........" },
    { name: "rst_n",      wave: "01......." },
    { name: "data_valid", wave: "0.1010..." },
    { name: "data",       wave: "x.=x.=x..", data: ["pkt0_512b", "pkt1_512b"] },
    { name: "crc_valid",  wave: "0..1010.." },
    { name: "crc",        wave: "x..=x.=x.", data: ["crc0", "crc1"] }
  ],
  head: {
    text: "512b Parallel CRC32 with registered output"
  }
}

        当 OUT_REG_EN = 1 时,crc_validcrc 相比 data_valid 延后一拍。这样可以把 512b CRC 组合逻辑与下游 timing 解耦,但输入侧仍然需要在一个周期内完成 data -> crc_final_comb -> output register 的路径。


7 Packet 级 CRC32 生成与校验

7.1 Generator / Checker 接口语义

        对固定 64B packet,一个最小 generator 只需要 data_validdata。但实际协议常常会有不同长度 packet、SOP/EOP、keep mask、streaming beat 等。为了聚焦 64B / 512b 并行实现,本文使用固定长度接口:

信号 方向 含义
data_valid input 本周期 data 是一个完整 64B payload
data[511:0] input payload,按 LSB-first bit 顺序计算
crc_valid output 输出 CRC 有效
crc[31:0] output 发送端要追加到 packet 的 CRC
crc_expected[31:0] input checker 接收到的 CRC
check_valid output checker 输出有效
check_pass output crc_calc == crc_expected
crc_calc[31:0] output checker 重新计算出的 CRC

7.2 Packet Checker RTL

module crc32_checker_512 #(
    parameter int DATA_WIDTH = 512
) (
    // Clock and reset
    input  logic                  clk,
    input  logic                  rst_n,

    // Input packet and expected CRC
    input  logic                  data_valid,
    input  logic [DATA_WIDTH-1:0] data,
    input  logic [31:0]           crc_expected,

    // Check result
    output logic                  check_valid,
    output logic                  check_pass,
    output logic [31:0]           crc_calc
);

    logic        gen_crc_valid;
    logic [31:0] gen_crc;
    logic [31:0] crc_expected_q;

    crc32_parallel_512 #(
        .DATA_WIDTH(DATA_WIDTH),
        .OUT_REG_EN(1'b1)
    ) u_crc_gen (
        .clk,
        .rst_n,
        .data_valid,
        .data,
        .crc_valid(gen_crc_valid),
        .crc(gen_crc)
    );

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            crc_expected_q <= '0;
        end else if (data_valid) begin
            crc_expected_q <= crc_expected;
        end
    end

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            check_valid <= 1'b0;
            check_pass  <= 1'b0;
            crc_calc    <= '0;
        end else begin
            check_valid <= gen_crc_valid;
            if (gen_crc_valid) begin
                crc_calc   <= gen_crc;
                check_pass <= (gen_crc == crc_expected_q);
            end
        end
    end

endmodule

        这里 crc_expected 必须与 data_valid 同周期输入。由于 generator 输出打一拍,checker 也打一拍保存 crc_expected_q,保证比较时序对齐。

7.3 Packet Checker Testbench

`timescale 1ns/1ps

module tb_crc32_checker_512;

    localparam int DATA_WIDTH = 512;

    logic                  clk;
    logic                  rst_n;
    logic                  data_valid;
    logic [DATA_WIDTH-1:0] data;
    logic [31:0]           crc_expected;
    logic                  check_valid;
    logic                  check_pass;
    logic [31:0]           crc_calc;

    crc32_checker_512 #(
        .DATA_WIDTH(DATA_WIDTH)
    ) u_dut (
        .clk,
        .rst_n,
        .data_valid,
        .data,
        .crc_expected,
        .check_valid,
        .check_pass,
        .crc_calc
    );

    initial clk = 1'b0;
    always #5 clk = ~clk;

    function automatic logic [31:0] crc32_ref_512(input logic [DATA_WIDTH-1:0] data_i);
        logic [31:0] crc_t;
        logic        feedback;

        crc_t = 32'hffff_ffff;
        for (int bit_idx = 0; bit_idx < DATA_WIDTH; bit_idx++) begin
            feedback = crc_t[0] ^ data_i[bit_idx];
            crc_t    = crc_t >> 1;
            if (feedback) begin
                crc_t = crc_t ^ 32'hedb8_8320;
            end
        end
        return crc_t ^ 32'hffff_ffff;
    endfunction

    task automatic send_check_case(
        input logic [DATA_WIDTH-1:0] data_i,
        input logic [31:0]           crc_i,
        input bit                    exp_pass
    );
        @(negedge clk);
        data_valid  = 1'b1;
        data        = data_i;
        crc_expected = crc_i;

        @(posedge clk);
        #1;
        assert(check_valid == 1'b1)
            else $fatal("check_valid should be asserted");
        assert(check_pass == exp_pass)
            else $fatal("check_pass mismatch: got=%0b exp=%0b crc_calc=%08x crc_exp=%08x",
                        check_pass, exp_pass, crc_calc, crc_i);

        @(negedge clk);
        data_valid  = 1'b0;
        data        = '0;
        crc_expected = '0;
    endtask

    initial begin
        rst_n        = 1'b0;
        data_valid   = 1'b0;
        data         = '0;
        crc_expected = '0;

        repeat (4) @(negedge clk);
        rst_n = 1'b1;
        repeat (2) @(negedge clk);

        // Correct CRC should pass.
        begin
            logic [DATA_WIDTH-1:0] pkt;
            logic [31:0]           crc_ref;

            pkt = '0;
            for (int word_idx = 0; word_idx < DATA_WIDTH / 32; word_idx++) begin
                pkt[word_idx*32 +: 32] = 32'h1000_0000 + word_idx;
            end
            crc_ref = crc32_ref_512(pkt);
            send_check_case(pkt, crc_ref, 1'b1);
        end

        // Corrupted payload should fail when expected CRC is from original payload.
        begin
            logic [DATA_WIDTH-1:0] pkt;
            logic [DATA_WIDTH-1:0] pkt_bad;
            logic [31:0]           crc_ref;

            pkt     = {DATA_WIDTH{1'b0}};
            pkt_bad = pkt;
            pkt_bad[123] = ~pkt_bad[123];

            crc_ref = crc32_ref_512(pkt);
            send_check_case(pkt_bad, crc_ref, 1'b0);
        end

        // Corrupted CRC should fail.
        begin
            logic [DATA_WIDTH-1:0] pkt;
            logic [31:0]           crc_ref;

            pkt = {DATA_WIDTH{1'b1}};
            crc_ref = crc32_ref_512(pkt);
            send_check_case(pkt, crc_ref ^ 32'h0000_0001, 1'b0);
        end

        // Random pass/fail mix.
        for (int pkt_idx = 0; pkt_idx < 100; pkt_idx++) begin
            logic [DATA_WIDTH-1:0] rand_pkt;
            logic [31:0]           crc_ref;
            bit                    inject_error;

            for (int word_idx = 0; word_idx < DATA_WIDTH / 32; word_idx++) begin
                rand_pkt[word_idx*32 +: 32] = $urandom();
            end

            crc_ref      = crc32_ref_512(rand_pkt);
            inject_error = $urandom_range(0, 1);

            if (inject_error) begin
                send_check_case(rand_pkt, crc_ref ^ (32'h1 << $urandom_range(0, 31)), 1'b0);
            end else begin
                send_check_case(rand_pkt, crc_ref, 1'b1);
            end
        end

        $display("tb_crc32_checker_512 PASS");
        $finish;
    end

endmodule

        Packet checker testbench 覆盖的测试场景如下:

编号 测试场景 主要激励 期望结果
1 Correct CRC 输入固定 packet 和 reference CRC check_valid = 1check_pass = 1
2 Payload bit flip 使用原始 payload 的 CRC,但翻转 payload 中 1 bit check_pass = 0
3 CRC bit flip payload 正确,但翻转 crc_expected 中 1 bit check_pass = 0
4 Random pass/fail mix 100 个随机 packet,随机选择是否注入 CRC error 未注错时 pass,注错时 fail
5 时序对齐 crc_expecteddata_valid 同周期输入,内部打一拍对齐 比较使用对应 packet 的 CRC,不发生错拍

7.4 Packet Checker 时序

{
  signal: [
    { name: "clk",          wave: "p........" },
    { name: "rst_n",        wave: "01......." },
    { name: "data_valid",   wave: "0.10.10.." },
    { name: "data",         wave: "x.=x.=x..", data: ["pkt0", "pkt1"] },
    { name: "crc_expected", wave: "x.=x.=x..", data: ["exp0", "exp1"] },
    { name: "check_valid",  wave: "0..10.10." },
    { name: "crc_calc",     wave: "x..=x.=x.", data: ["calc0", "calc1"] },
    { name: "check_pass",   wave: "0..=0.=0.", data: ["pass0", "pass1"] }
  ],
  head: {
    text: "CRC32 Checker: expected CRC alignment with registered generator"
  }
}

8 Polynomial、bit 顺序、seed 和 invert 的工程注意事项

8.1 Polynomial 选择

        Polynomial 决定 CRC 的错误检测能力。工程中通常不要自己随意选择 polynomial,而应优先遵循协议规范或行业标准。例如:

场景 常见选择
Ethernet / PNG / ZIP CRC-32/ISO-HDLC
iSCSI / SCTP / NVMe 等存储或链路场景 CRC-32C/Castagnoli
USB token CRC5
USB data packet CRC16
PCIe TLP LCRC CRC32 变体,需按协议 bit ordering 实现

        CRC32C 在某些长度范围内比传统 CRC32 有更好的 Hamming distance,常用于现代存储和网络协议。但设计时最重要的是与系统两端保持一致。

8.2 Bit 顺序和 byte 顺序

        CRC RTL 最常见 bug 是 bit ordering 错。需要明确以下三个层次:

层次 需要确认的问题
Byte lane 顺序 data[7:0] 是 packet 第一个 byte,还是最后一个 byte
Byte 内 bit 顺序 先处理 bit0 还是 bit7
CRC 输出顺序 输出 crc[31:0] 是否还需要 byte swap 或 bit reverse 后发送

        本文约定:

data[7:0]      = packet byte0
data[15:8]     = packet byte1
...
data[511:504]  = packet byte63

每个 byte 内按 bit0 -> bit7 处理
crc 输出为 reflected CRC32 final value

        如果协议规定线上发送 FCS 时低 byte 先发,需要在 packet assembly 阶段再确认 crc[7:0]crc[15:8] 等 byte lane 的放置顺序。

8.3 Seed 和 invert

        CRC32 常见 init = 0xffffffffxorout = 0xffffffff。这样做可以提升对前导零和尾部零某些模式的检测能力,也与很多历史协议保持兼容。

参数 影响
CRC_INIT packet 开始时 LFSR 初值,不同 seed 会导致整个 CRC 结果不同
CRC_XOROUT 输出前最终异或,不影响内部 LFSR 状态转移,但影响最终比较值
CRC_POLY 决定反馈 tap 和检测能力
reflect 决定数据进入 LFSR 的方向和 polynomial 表示

8.4 Pipeline 切分

        512b 并行 CRC 的组合逻辑可能很深。若目标频率较高,可以把 512b 切成多个 64b 或 128b stage:

512b one-cycle:
  data[511:0] -> CRC update 512 bits -> crc

512b 4-stage pipeline:
  stage0: update data[127:0]
  stage1: update data[255:128]
  stage2: update data[383:256]
  stage3: update data[511:384]

        pipeline 后延迟增加,但吞吐率仍可保持每拍一个 packet 或每拍一个 beat,前提是每一级都能接受新数据并传递 valid。

CRC32 Pipeline切分图

image2 prompt:生成一张 512b CRC32 pipeline 切分架构图。输入 512b data 被拆成 4 个 128b stage,每级包含 CRC update XOR network 和 pipeline register,valid 信号逐级传递,最终输出 crc_valid 和 crc。中文标注,简洁工程框图风格。


9 架构分析重点

9.1 Serial、parallel 和 pipeline 的权衡

实现方式 每拍处理宽度 面积 时序 延迟 适用场景
串行 LFSR 1 bit 最小 最容易 低速控制通路、教学
Byte-wise CRC 8 bit 容易 UART、低速 packet
64b parallel 64 bit 常规 SoC datapath
512b parallel one-cycle 512 bit 压力大 最低 低频宽总线、简单 packet
512b pipelined 512 bit 可控 多拍 高频宽总线

9.2 并行展开方式

        并行 CRC 有两类常见实现:

方法 说明 优点 缺点
for loop 展开 用 SystemVerilog function 描述连续 bit update 易读、参数化方便 综合出的 XOR 网络可能不够优化
预生成 XOR equation 用脚本生成每个 crc_next bit 的 XOR 方程 面积/时序可控,适合签核 代码较长,可读性较差

        对教学和中等频率设计,for loop 展开足够清晰。对高频 ASIC,可以用 Python/Perl/Tcl 生成矩阵化 XOR equation,并让综合工具做 XOR tree 优化。

9.3 Checklist

检查项 说明
Polynomial 是否正确 CRC32 与 CRC32C 不可混用
reflected / non-reflected 是否一致 对应 polynomial 常量不同
byte lane 顺序是否匹配协议 尤其是 512b 宽总线
seed 是否在每个 packet 开始重新装载 避免 packet 间状态污染
xorout 是否只在最终输出使用 不要错误参与中间状态
checker 比较是否时序对齐 crc_expected 需要与计算结果同 packet 对齐
pipeline valid 是否逐级传递 防止 CRC 与 packet metadata 错配
empty packet 是否定义清楚 CRC32/ISO-HDLC 空消息为 0x00000000
reset 后输出是否已知 便于仿真和门级检查
testbench 是否包含 known vector "123456789" -> 0xcbf43926 是必测项

10 常见问题与调试方法

10.1 CRC 值完全不对

        如果 CRC 与参考模型完全不同,优先检查:

  1. polynomial 是否用错;
  2. MSB-first / LSB-first 是否反了;
  3. byte lane 顺序是否反了;
  4. seed 是否一致;
  5. final xor 是否漏掉或重复;
  6. reference model 是否与 RTL 使用同一种约定。

10.2 只有某些 packet 不对

        如果短 packet 正确、长 packet 错误,常见原因是:

10.3 用标准向量定位 bit 顺序

        推荐从以下测试开始:

输入 CRC-32/ISO-HDLC 期望值
空字符串 0x00000000
"a" 0xe8b7be43
"123456789" 0xcbf43926

        如果 "123456789" 不对,可以尝试打印每个 byte 后的中间 crc_state,与软件 reference 逐 byte 比较。这样通常可以快速定位是第一个 byte 的 bit 顺序错误,还是 final xor / byte swap 错误。


11 总结

        CRC 是 packet 级数据完整性检测中非常基础但也非常容易实现出错的模块。对 RTL 设计而言,掌握 CRC 的关键不只是写出 LFSR,而是明确一整套参数和接口语义:

        本文给出的串行 CRC32、512b 并行 CRC32 generator 和 checker 都采用 SystemVerilog 编写,并配套了覆盖常见场景的 testbench。实际项目中可以在此基础上继续扩展 variable length packet、byte enable、ready/valid streaming、多级 pipeline 和协议特定 FCS byte ordering。


12 参考资料

  1. Ross N. Williams, A Painless Guide to CRC Error Detection Algorithms
    https://zlib.net/crc_v3.txt

  2. reveng CRC Catalogue, CRC-32/ISO-HDLC and CRC-32C parameters
    https://reveng.sourceforge.io/crc-catalogue/all.htm

  3. IEEE 802.3 Ethernet Standard, Frame Check Sequence / CRC32
    https://standards.ieee.org/standard/802_3-2022.html

  4. Wikipedia, Cyclic redundancy check
    https://en.wikipedia.org/wiki/Cyclic_redundancy_check

  5. Koopman, CRC Polynomial Zoo
    https://users.ece.cmu.edu/~koopman/crc/

  6. SystemVerilog IEEE Std 1800
    https://ieeexplore.ieee.org/document/8299595

Back to Archive
WeChat QR Code

Scan to connect