fpga-wide-data-crc32
基于 FPGA 实现高位宽数据流的 CRC 校验的一种方式 —— 以万兆以太网 64bit 数据流的 CRC32 为例
对于FPGA 上的万兆网,MAC 层或者XGMII 对接的数据通路是64bit @ 156.25 MHz。
收发两个方向都要处理以太网 FCS(CRC32)。本文把其中"高位宽数据流 CRC"的实现思路整理成文,
一、问题:线速带来的算力缺口
万兆以太网用户接口一拍(6.4 ns)送来8 个字节,而以太网 FCS 是标准的 CRC32:
- 生成多项式:
G(x) = x^32 + x^26 + x^23 + ... + 1,即0x04C11DB7(反射域为0xEDB88320); - 初值
0xFFFFFFFF,最终结果整体取反(即 CRC-32/ISO-HDLC,与 zlib、以太网一致); - 发送与接收侧都按位反射方式处理(字节内 LSB 先上线路)。
如果沿用入门时写的逐位 LFSR,一拍总线数据要滚 64 拍;逐字节查表法也要滚 8 拍。
要在 156.25 MHz 线速下既不丢带宽、又不大容量缓存,唯一出路是:一拍总线数据,一拍算完。
如果沿用入门时写的逐位 LFSR,一拍总线数据要滚 64 拍;逐字节查表法也要滚 8 拍。要在 156.25 MHz 线速下既不丢带宽、又不大容量缓存,唯一出路是:一拍总线数据,一拍算完。
二、数学基础:CRC 是一个线性状态机
把 CRC 看成一个 32bit 状态机:每输入 1 个字节b,状态从s转移到F(s, b)。F是纯异或的组合函数——32bit 旧状态 + 8bit 数据,线性组合出 32bit 新状态。
它有两个关键性质:
- 可复合:处理 k 个字节,就是把
F连续套 k 次:s4 = F(F(F(F(s0,b0),b1),b2),b3)。 - 中间态即前缀 CRC:套到第 k 次的输出,恰好就是"前 k 个字节"的 CRC。
于是得到本文的核心做法:
用工具生成一份"32bit 状态 + 1 字节 → 32bit 新状态"的单字节组合逻辑,复制 8 份串成级联链,
在同一拍内连续完成 8 次状态转移;同时把每一级的输出都引出寄存,
1~8 字节各前缀长度的 CRC 就"顺便"全都有了——这正是变长帧尾最需要的东西。
几种常见方案的对比:
| 方案 | 思路 | 短板 |
|---|---|---|
| 逐位 LFSR | 每拍移 1 bit | 时延大 64 倍,线速下不可行 |
| 8 份查找表 | 8 张 256×32b 表 | 表占资源大;表间串行难塞进一拍 |
| 直接推导 64bit 公式 | 一次性生成"32bit 状态 + 64bit 数据"表达式 | 表达式庞大且定死位宽,1~7 字节的变长帧尾还要额外电路 |
| 单字节公式级联(本文) | 8 份相同的单字节公式串行级联 | 资源略多,但天然支持 1~8 字节任意前缀 |
三、整体结构
结构要点:
- 级联关系:第
i级的输入状态 = 第i-1级的输出状态(纯组合,同拍内流动);
只有第 0 级接状态寄存器,只有第 7 级的输出在拍末写回状态寄存器。 - 前缀引出:每级输出各自打一拍寄存后输出,
crc_len1 ~ crc_len8分别对应
1~8 个字节参与运算的 CRC(已含最终取反),空闲时输出固定0xFFFFFFFF可兼作无效标志。 - 按帧自动复位:数据无效(帧间隙)时状态寄存器回装初值
0xFFFFFFFF,输出同步清零——
不需要额外的"帧首清零"状态机。
四、单字节公式从哪来
不用手推。用常见的 CRC 代码生成工具(Easics CRC tool、outputlogic 的 CRC Generator、
开源的crcgen脚本等),选 CRC-32、数据位宽 8bit,即可生成 32 条异或表达式。
其形态大致是(注意输入是已按线序位反转的字节):
// st_in: 32bit 旧状态, db: 8bit 数据(位反转后), st_out: 32bit 新状态 assign st_out[0] = db[6] ^ db[0] ^ st_in[24] ^ st_in[30]; assign st_out[1] = db[7] ^ db[6] ^ db[1] ^ db[0] ^ st_in[24] ^ st_in[25] ^ st_in[30] ^ st_in[31]; assign st_out[2] = db[7] ^ db[6] ^ db[2] ^ db[1] ^ db[0] ^ st_in[24] ^ st_in[25] ^ st_in[26] ^ st_in[30] ^ st_in[31]; // …… st_out[3] ~ st_out[30] 由工具生成,篇幅所略 …… assign st_out[31] = db[5] ^ st_in[23] ^ st_in[29];单个单元约 130 个异或项,全部是 LUT 的家常菜。
五、RTL 骨架(示意代码)
module crc32_fcs_cascade ( input clk , input rst , input dat_vld , // 本拍数据有效;拉低即帧间隙,状态自动回装初值 input [7:0] dat_b0 , // 本拍最先发送的字节 input [7:0] dat_b1 , input [7:0] dat_b2 , input [7:0] dat_b3 , input [7:0] dat_b4 , input [7:0] dat_b5 , input [7:0] dat_b6 , input [7:0] dat_b7 , // 本拍最后发送的字节 output [31:0] crc_len1, // 仅第 1 字节参与运算后的 CRC(已取反) output [31:0] crc_len2, // 前 2 字节参与运算后的 CRC output [31:0] crc_len3, output [31:0] crc_len4, output [31:0] crc_len5, output [31:0] crc_len6, output [31:0] crc_len7, output [31:0] crc_len8 // 全部 8 字节参与运算后的 CRC ); reg [31:0] fcs_r ; // 帧内滚动状态(纯余数域,不取反) wire [7 :0] db [0:7]; // 位反转后的输入字节 wire [31:0] st_in [0:7]; // 各级输入状态 wire [31:0] st_out[0:7]; // 各级输出状态 reg [31:0] crc_q [0:7]; // 各级寄存后的输出 // 8字节内 LSB 先上线路,公式按线序书写,先做位反转 assign db[0] = {dat_b0[7],dat_b0[6],dat_b0[5],dat_b0[4], dat_b0[3],dat_b0[2],dat_b0[1],dat_b0[0]}; // …… db[1] ~ db[7] 同理 …… genvar g; generate for (g = 0; g < 8; g = g + 1) begin : UNIT // 级联:第 0 级接状态寄存器,其后每一级接上一级输出 if (g == 0) assign st_in[0] = fcs_r; else assign st_in[g] = st_out[g-1]; // 单字节 CRC 更新公式(生成工具产出,此处节选 2 条) assign st_out[g][0] = dat_vld ? db[g][6] ^ db[g][0] ^ st_in[g][24] ^ st_in[g][30] : 32'd0; assign st_out[g][1] = dat_vld ? db[g][7] ^ db[g][6] ^ db[g][1] ^ db[g][0] ^ st_in[g][24] ^ st_in[g][25] ^ st_in[g][30] ^ st_in[g][31] : 32'd0; // …… st_out[g][2] ~ st_out[g][31] 同理 …… // 各级前缀结果寄存并取反后输出;空闲时输出固定 0xFFFFFFFF always @(posedge clk or posedge rst) begin if (rst) crc_q[g] <= 32'hffff_ffff; else crc_q[g] <= ~st_out[g]; end end endgenerate assign {crc_len1, crc_len2, crc_len3, crc_len4, crc_len5, crc_len6, crc_len7, crc_len8} = crc_q; // 状态写回:数据有效时滚动;无效时回装初值(按帧自动复位) always @(posedge clk or posedge rst) begin if (rst) fcs_r <= 32'hffff_ffff; else if (!dat_vld) fcs_r <= 32'hffff_ffff; else fcs_r <= st_out[7]; end endmodule几个设计取舍说明:
- 输出取反放在寄存器端,状态寄存器
fcs_r始终保持"纯多项式余数域"。
这样接收侧可以直接拿fcs_r做魔数比较(见第七节),理解上也不容易混淆。 dat_vld同时门控组合输出与状态写回:无效拍的输出被强制清零(寄存后为0xFFFFFFFF),
避免无效数据污染各级结果;同时状态回装初值,一石二鸟。- 中间结果全部寄存引出,代价是 8×32 个触发器,换来的是任意前缀长度零延迟可选。
六、帧尾变长:用 keep 选结果,而不是补零重算
以太网帧长不一定是 8 的倍数,帧尾一拍的keep(字节有效掩码)往往非全 1。
常见做法是把无效字节补零再算——但补零参与 CRC 会污染结果,还得再做"剥离补零影响"的修正,
又多一拍延迟。而级联结构里前缀结果本来就是现成的:
// 帧尾拍:按 keep 给出的有效字节数 N,直接选取对应前缀长度的 CRC wire [7:0] tail_keep; // 字节有效掩码,bit0 对应 dat_b0 reg [31:0] fcs_tail; always @(*) begin case (tail_keep) 8'b0000_0001: fcs_tail = crc_len1; 8'b0000_0011: fcs_tail = crc_len2; 8'b0000_0111: fcs_tail = crc_len3; 8'b0000_1111: fcs_tail = crc_len4; 8'b0001_1111: fcs_tail = crc_len5; 8'b0011_1111: fcs_tail = crc_len6; 8'b0111_1111: fcs_tail = crc_len7; default : fcs_tail = crc_len8; // 8'b1111_1111 endcase end对齐关系也顺理成章:crc_lenN是"本拍前 N 个有效字节参与运算后"的结果(当拍组合、
下一拍呈现在寄存器输出上),发送侧把它作为 FCS 字段紧随数据之后写入总线即可;
接收侧同样可用它对收到的 FCS 做比对。不需要补零、不需要重算、不需要额外等待。
七、位序、初值这些坑
- 字节内位序:XGMII 是字节内 LSB 先发,而生成公式假设输入为"线路位序",
所以送进公式前必须做位反转(db = reverse(byte))。位序弄反的典型症状是:
CRC 随机错、且与 Wireshark 等工具显示的校验值对不上。 - 初值与终值:初值
0xFFFFFFFF由状态寄存器在帧间隙自动装载;最终取反放在输出寄存器处。
状态域与输出域不要混着用——比较、寄存、加 FCS 时先想清楚自己处在哪个域。 - 切分点一致性:
crc_lenN这类中间结果只有在"从帧首字节起逐拍连续"的前提下才有意义。
中途丢一拍,整帧 CRC 即作废,应直接丢帧、状态重新初始化。 - 接收侧的魔数校验:最省事的收端校验法是"整帧(含 FCS)全部灌进同一个引擎",
最后直接比较状态寄存器是否等于固定魔数。在本实现的状态域(初值全 1、中间不取反)下
该魔数为0xDEBB20E3;若在取反后的输出域比较则为0x2144DF1C。
部分资料按相反位序写作0xC704DD7B——三者是同一物理事实的不同表示,
务必先与自己的实现仿真对拍一次再固化。 - FCS 的字节序:FCS 在帧内按 32bit 字的低位字节先发;把
crc_lenN拼进发送总线时
注意按线序拆分字节,别整字大端扔上去。
八、时序与资源
- 资源:8 级 × 32bit 异或网络,合计几百个 LUT、256 个输出触发器,7 系列器件完全无感。
- 关键路径:8 级级联的组合链,每级约 2~4 层 LUT,总深度 20 层上下,
在 156.25 MHz(6.4 ns)下可以收敛;每一级输出都有寄存器,是天然的时序切分点。 - 更高频率怎么办:把 8 级拆成 4+4 分两拍即可翻倍时序余量(代价是帧尾选路多等一拍);
或把基本单元升级为 2 字节粒度,级数减半。400GE 的 512bit 数据通路用的就是同一思路的粗粒度变体。
九、仿真验证
用 Python 的zlib.crc32做参考模型最方便——它就是 CRC-32/ISO-HDLC,
与本文 RTL 的crc_lenN输出同域,可直接数值比对:
importzlib,randomdefref_model(data:bytes)->int:returnzlib.crc32(data)&0xFFFFFFFF# 与 RTL 的 crc_lenN 输出同域,可直接比对# 已知答案自检:"123456789" -> 0xCBF43926assertref_model(b"123456789")==0xCBF43926# 随机帧:整帧切成 8 字节节拍(尾拍不足补零、用 keep 标记有效字节数)frame=bytes(random.randrange(256)for_inrange(random.randrange(64,1520)))fcs=ref_model(frame).to_bytes(4,"little")# 以太网 FCS 按低位字节先发存放assertzlib.crc32(frame+fcs)==0x2144DF1C# 含 FCS 整帧的固定魔数仿真激励建议覆盖:
- 短帧(60 B 最小帧)与整 8 字节边界帧;
- 帧尾有效字节数 1~7逐一遍历;
- 背靠背帧(
dat_vld连续拉高跨帧,验证按帧自动复位); - 全 0 / 全 FF / 随机数据;
- 接收侧整帧含 FCS 灌入后比较状态魔数。
十、小结
本文方案一句话概括:把"宽度"换成"深度"——用 8 份相同的单字节 CRC 组合逻辑级联,
在一拍内完成 8 次状态转移,并把每级前缀结果全部引出,从而同时解决高位宽吞吐与变长帧尾两个问题。
数学上成立:CRC 是线性状态机,可任意复合,中间态即前缀 CRC;
工程上划算:公式由工具生成,资源几百个 LUT,156 MHz 轻松收敛,帧尾零额外延迟;
推广上通用:4 字节(千兆 GMII)、16 字节、乃至 512bit 更宽的通路,
都是"单字节(或粗粒度)公式 + 级联 + 前缀引出"这一个套路的复制与变形。
CRC 组合逻辑级联,
在一拍内完成 8 次状态转移,并把每级前缀结果全部引出,从而同时解决高位宽吞吐与变长帧尾两个问题。**数学上成立:CRC 是线性状态机,可任意复合,中间态即前缀 CRC;
工程上划算:公式由工具生成,资源几百个 LUT,156 MHz 轻松收敛,帧尾零额外延迟;
推广上通用:4 字节(千兆 GMII)、16 字节、乃至 512bit 更宽的通路,
都是"单字节(或粗粒度)公式 + 级联 + 前缀引出"这一个套路的复制与变形。