
基于 FPGA 实现高位宽数据流的 CRC 校验的一种方式 —— 以万兆以太网 64bit 数据流的 CRC32 为例对于FPGA 上的万兆网MAC 层或者XGMII 对接的数据通路是64bit 156.25 MHz。收发两个方向都要处理以太网 FCSCRC32。本文把其中高位宽数据流 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] : 32d0; 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] : 32d0; // …… st_out[g][2] ~ st_out[g][31] 同理 …… // 各级前缀结果寄存并取反后输出空闲时输出固定 0xFFFFFFFF always (posedge clk or posedge rst) begin if (rst) crc_q[g] 32hffff_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 32hffff_ffff; else if (!dat_vld) fcs_r 32hffff_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) 8b0000_0001: fcs_tail crc_len1; 8b0000_0011: fcs_tail crc_len2; 8b0000_0111: fcs_tail crc_len3; 8b0000_1111: fcs_tail crc_len4; 8b0001_1111: fcs_tail crc_len5; 8b0011_1111: fcs_tail crc_len6; 8b0111_1111: fcs_tail crc_len7; default : fcs_tail crc_len8; // 8b1111_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 MHz6.4 ns下可以收敛每一级输出都有寄存器是天然的时序切分点。更高频率怎么办把 8 级拆成 44 分两拍即可翻倍时序余量代价是帧尾选路多等一拍或把基本单元升级为 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(b123456789)0xCBF43926# 随机帧整帧切成 8 字节节拍尾拍不足补零、用 keep 标记有效字节数framebytes(random.randrange(256)for_inrange(random.randrange(64,1520)))fcsref_model(frame).to_bytes(4,little)# 以太网 FCS 按低位字节先发存放assertzlib.crc32(framefcs)0x2144DF1C# 含 FCS 整帧的固定魔数仿真激励建议覆盖短帧60 B 最小帧与整 8 字节边界帧帧尾有效字节数 1~7逐一遍历背靠背帧dat_vld连续拉高跨帧验证按帧自动复位全 0 / 全 FF / 随机数据接收侧整帧含 FCS 灌入后比较状态魔数。十、小结本文方案一句话概括把宽度换成深度——用 8 份相同的单字节 CRC 组合逻辑级联在一拍内完成 8 次状态转移并把每级前缀结果全部引出从而同时解决高位宽吞吐与变长帧尾两个问题。数学上成立CRC 是线性状态机可任意复合中间态即前缀 CRC工程上划算公式由工具生成资源几百个 LUT156 MHz 轻松收敛帧尾零额外延迟推广上通用4 字节千兆 GMII、16 字节、乃至 512bit 更宽的通路都是单字节或粗粒度公式 级联 前缀引出这一个套路的复制与变形。CRC 组合逻辑级联在一拍内完成 8 次状态转移并把每级前缀结果全部引出从而同时解决高位宽吞吐与变长帧尾两个问题。**数学上成立CRC 是线性状态机可任意复合中间态即前缀 CRC工程上划算公式由工具生成资源几百个 LUT156 MHz 轻松收敛帧尾零额外延迟推广上通用4 字节千兆 GMII、16 字节、乃至 512bit 更宽的通路都是单字节或粗粒度公式 级联 前缀引出这一个套路的复制与变形。