1 前言
CRC(Cyclic Redundancy Check,循环冗余校验)是数字系统、网络协议、存储系统和片上互连中最常见的数据完整性检测方法之一。它的目标不是加密,也不是纠错,而是在 packet、frame、block 或 flit 级别尽可能高概率地发现传输或存储过程中发生的数据错误。
在 RTL 设计中,CRC 看起来只是一个 LFSR(Linear Feedback Shift Register,线性反馈移位寄存器),但工程中真正容易出错的地方通常集中在:
- polynomial 选择;
- bit 顺序和 byte 顺序;
- 初始 seed;
- 最终 invert / xorout;
- serial CRC 和 parallel CRC 的等价性;
- 输入数据宽度展开,例如 8b、32b、64b、512b;
- packet 边界上的 clear、init、update、finalize;
- generator 和 checker 的接口语义;
- pipeline 切分和时序收敛;
- 与协议规范中 CRC32 / CRC32C / Ethernet FCS 定义是否完全一致。
本文按照“先概念、再数学模型、再串行 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 学习路径图:

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
需要注意:0x04C11DB7 和 0xEDB88320 表示的是同一个 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^3、x^2、G(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^26、x^23、x^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。因此看到 0x04C11DB7 和 0xEDB88320 时,不要简单认为它们是两个完全不同的 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 结构图:

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_valid 和 crc 相比 data_valid 延后一拍。这样可以把 512b CRC 组合逻辑与下游 timing 解耦,但输入侧仍然需要在一个周期内完成 data -> crc_final_comb -> output register 的路径。
7 Packet 级 CRC32 生成与校验
7.1 Generator / Checker 接口语义
对固定 64B packet,一个最小 generator 只需要 data_valid 和 data。但实际协议常常会有不同长度 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 = 1,check_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_expected 与 data_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 = 0xffffffff、xorout = 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。

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 与参考模型完全不同,优先检查:
- polynomial 是否用错;
- MSB-first / LSB-first 是否反了;
- byte lane 顺序是否反了;
- seed 是否一致;
- final xor 是否漏掉或重复;
- reference model 是否与 RTL 使用同一种约定。
10.2 只有某些 packet 不对
如果短 packet 正确、长 packet 错误,常见原因是:
- packet 边界
init没有正确拉高; - streaming 多 beat 模式中 valid/ready bubble 未处理;
- pipeline 中 metadata 和 CRC 状态错拍;
- tail byte mask 没有正确作用;
- checker 使用了上一个 packet 的
crc_expected。
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,而是明确一整套参数和接口语义:
- polynomial 决定反馈 tap 和错误检测能力;
- reflected / non-reflected 决定 bit 顺序和 polynomial 常量;
- seed 和 xorout 必须与协议一致;
- packet 开始需要重新初始化 CRC 状态;
- 并行 CRC 本质上是串行 LFSR 多步更新的组合展开;
- 512b 并行 CRC 要重点关注 XOR 网络时序;
- checker 需要保证
crc_expected与crc_calc对齐; - testbench 必须包含 known vector、边界数据、随机 packet 和错误注入。
本文给出的串行 CRC32、512b 并行 CRC32 generator 和 checker 都采用 SystemVerilog 编写,并配套了覆盖常见场景的 testbench。实际项目中可以在此基础上继续扩展 variable length packet、byte enable、ready/valid streaming、多级 pipeline 和协议特定 FCS byte ordering。
12 参考资料
-
Ross N. Williams, A Painless Guide to CRC Error Detection Algorithms
https://zlib.net/crc_v3.txt -
reveng CRC Catalogue, CRC-32/ISO-HDLC and CRC-32C parameters
https://reveng.sourceforge.io/crc-catalogue/all.htm -
IEEE 802.3 Ethernet Standard, Frame Check Sequence / CRC32
https://standards.ieee.org/standard/802_3-2022.html -
Wikipedia, Cyclic redundancy check
https://en.wikipedia.org/wiki/Cyclic_redundancy_check -
Koopman, CRC Polynomial Zoo
https://users.ece.cmu.edu/~koopman/crc/ -
SystemVerilog IEEE Std 1800
https://ieeexplore.ieee.org/document/8299595