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 条异或表达式。

其形态大致是(注意输入是已按线序位反转的字节):

verilog 复制代码
// 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 骨架(示意代码)

verilog 复制代码
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 会污染结果,还得再做"剥离补零影响"的修正,

又多一拍延迟。而级联结构里前缀结果本来就是现成的 :

verilog 复制代码
// 帧尾拍:按 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 输出同域,可直接数值比对:

python 复制代码
import zlib, random

def ref_model(data: bytes) -> int:
    return zlib.crc32(data) & 0xFFFFFFFF   # 与 RTL 的 crc_lenN 输出同域,可直接比对

# 已知答案自检:"123456789" -> 0xCBF43926
assert ref_model(b"123456789") == 0xCBF43926

# 随机帧:整帧切成 8 字节节拍(尾拍不足补零、用 keep 标记有效字节数)
frame = bytes(random.randrange(256) for _ in range(random.randrange(64, 1520)))
fcs   = ref_model(frame).to_bytes(4, "little")     # 以太网 FCS 按低位字节先发存放
assert zlib.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 更宽的通路,

    都是"单字节(或粗粒度)公式 + 级联 + 前缀引出"这一个套路的复制与变形。

相关推荐
落雨无风13 小时前
fpga板级调试常见问题
fpga开发
码农小韩21 小时前
FPGA学习(一)——LED点灯与开发流程
学习·fpga开发·入门·环境配置·仿真·点灯
FPGA小迷弟1 天前
FPGA 与千兆以太网:各类物理接口的形式、区别、使用方法与应用场景详解
tcp/ip·macos·fpga开发·udp·fpga
NamoAmitabha1281 天前
Zynq 的 AXI GPIO,是“ARM 通过一条国际标准高速公路(AXI 总线),去遥控 FPGA 里的一个寄存器”。---deepseek
fpga开发·zynq--ps
NamoAmitabha1281 天前
ARM 不懂寄存器,它只懂内存地址---deepseek
fpga开发·zynq--ps
FPGA小迷弟1 天前
FPGA 千兆以太网帧格式全解:从 IEEE 802.3 帧结构到 Verilog 收发实现
人工智能·5g·fpga开发·架构·硬件架构
幻智星科技1 天前
FPGA数据采集接口选型终极指南
fpga开发·udp·硬件架构
星夜离尘561 天前
用 SystemVerilog 写一个可背压的流式 RLE 编码器:计数拆分、包尾与 flush 的完整实现
fpga开发
NamoAmitabha1281 天前
ARM 不懂寄存器,它只懂内存地址--deepseek
fpga开发·zynq--ps