fpga-wide-data-crc32

发布时间:2026/11/5 3:49:06
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 新状态。

它有两个关键性质:

  1. 可复合:处理 k 个字节,就是把F连续套 k 次:s4 = F(F(F(F(s0,b0),b1),b2),b3)。
  2. 中间态即前缀 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 做比对。不需要补零、不需要重算、不需要额外等待。

七、位序、初值这些坑

  1. 字节内位序:XGMII 是字节内 LSB 先发,而生成公式假设输入为"线路位序",
    所以送进公式前必须做位反转(db = reverse(byte))。位序弄反的典型症状是:
    CRC 随机错、且与 Wireshark 等工具显示的校验值对不上。
  2. 初值与终值:初值0xFFFFFFFF由状态寄存器在帧间隙自动装载;最终取反放在输出寄存器处。
    状态域与输出域不要混着用——比较、寄存、加 FCS 时先想清楚自己处在哪个域。
  3. 切分点一致性:crc_lenN这类中间结果只有在"从帧首字节起逐拍连续"的前提下才有意义。
    中途丢一拍,整帧 CRC 即作废,应直接丢帧、状态重新初始化。
  4. 接收侧的魔数校验:最省事的收端校验法是"整帧(含 FCS)全部灌进同一个引擎",
    最后直接比较状态寄存器是否等于固定魔数。在本实现的状态域(初值全 1、中间不取反)下
    该魔数为0xDEBB20E3;若在取反后的输出域比较则为0x2144DF1C。
    部分资料按相反位序写作0xC704DD7B——三者是同一物理事实的不同表示,
    务必先与自己的实现仿真对拍一次再固化。
  5. 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 更宽的通路,
    都是"单字节(或粗粒度)公式 + 级联 + 前缀引出"这一个套路的复制与变形。