FPGA 与千兆以太网校验和:从原理到 Verilog 实现

本文聚焦 FPGA 实现千兆以太网时绕不开的校验和问题:以太网 CRC-32、IPv4 首部校验和、UDP 与 TCP 校验和的计算原理与硬件实现。面向从事 FPGA 网络协议栈开发的工程师与学习者,读完可掌握各校验字段的算法细节、线速下的 Verilog 实现与流水线设计,并规避常见错误。

如果需要系统学习FPGA,这里有一份整理的FPGA汇总课程资料,包含BD教程、Vivado教程、IP 核教程等,有需要的朋友可以保存:链接: https://pan.baidu.com/s/1pIFS8GEYu_jH1np4ZLG2Kw?pwd=wbbw 提取码: wbbw 整理不易,尽快保存

1. 引言:为什么校验和是千兆以太网绕不开的一环

在千兆以太网(Gigabit Ethernet,1000BASE-X / 1000BASE-T)的 FPGA 实现中,数据链路层与网络层的完整性校验是绕不开的核心问题。以太网帧本身带有 32 位帧校验序列(FCS,CRC-32),IP 层带有 16 位首部校验和,UDP 与 TCP 又各自带有 16 位校验和。这些校验字段共同构成了一个多层次的错误检测体系:链路层负责发现物理传输过程中的比特翻转,网络层负责发现路由转发过程中的首部损坏,传输层负责发现端到端的整包损坏。

对于使用 FPGA 直接实现千兆以太网 MAC、IP、UDP/TCP 协议栈的工程师而言,校验和的计算与验证是必须亲手实现的部分。原因在于:FPGA 通常没有操作系统提供的协议栈,所有校验逻辑都要在硬件中完成;而千兆以太网线速为 1 Gbps,即每秒 1.25 亿字节,在 125 MHz 时钟下每个时钟周期需要处理 8 位数据,在 125 MHz 的 8 位 GMII 接口上,校验和计算必须做到每周期处理一个字节而不产生任何停顿,否则就会丢包。

如果需要系统学习FPGA,这里有一份整理的FPGA汇总课程资料,包含BD教程、Vivado教程、IP 核教程等,有需要的朋友可以保存:链接: https://pan.baidu.com/s/1pIFS8GEYu_jH1np4ZLG2Kw?pwd=wbbw 提取码: wbbw 整理不易,尽快保存

本文只讨论 FPGA 与千兆以太网中的校验和问题,不涉及其他网络技术。全文将从校验和的基本数学原理出发,逐一剖析以太网 CRC-32、IPv4 首部校验和、UDP 校验和、TCP 校验和的计算方法,然后给出在 FPGA 中实现这些校验和的完整 Verilog 代码,最后讨论线速处理、流水线设计、资源优化与验证方法。全文力求准确,所有算法细节均与 RFC 及 IEEE 802.3 标准一致。

如果需要系统学习FPGA,这里有一份整理的FPGA汇总课程资料,包含BD教程、Vivado教程、IP 核教程等,有需要的朋友可以保存:链接: https://pan.baidu.com/s/1pIFS8GEYu_jH1np4ZLG2Kw?pwd=wbbw 提取码: wbbw 整理不易,尽快保存

2. 校验和的基本数学原理

2.1 校验和的本质:冗余信息与错误检测能力

校验和的本质是在原始数据之后附加一段由数据计算得到的冗余信息,接收方用同样的算法重新计算并比对,从而判断数据是否在传输过程中发生了改变。不同的校验算法具有不同的错误检测能力:

  • 简单累加和(如 8 位或 16 位求和)实现简单,但检测能力弱,无法发现字节顺序颠倒、某些多位错误。
  • 反码求和(One's Complement Sum)是 IP、UDP、TCP 校验和的基础,具有较好的检测能力,且实现代价低。
  • 循环冗余校验(CRC)基于多项式除法,检测能力最强,是以太网 FCS 的基础。

在千兆以太网中,链路层使用 CRC-32,网络层与传输层使用 16 位反码求和校验和。这两类算法在 FPGA 中的实现方式完全不同:CRC 适合用线性反馈移位寄存器(LFSR)实现,反码求和适合用加法器树实现。

2.2 反码求和(One's Complement Sum)

反码求和的核心规则是:将所有 16 位字相加,若产生进位,则将进位加回最低位(称为"端回进位",End-Around Carry),最终结果取反码即为校验和。

设数据被划分为 n 个 16 位字 W1, W2, ..., Wn,则:

复制代码
S = W1 + W2 + ... + Wn

在求和过程中,每当产生第 17 位的进位,就将该进位加回到第 0 位。最终校验和字段为 S 的反码(按位取反)。

反码求和有一个重要性质:如果接收方将所有字(包括校验和字段本身)相加,结果应为 0xFFFF(全 1)。这是因为校验和字段恰好是数据部分反码和的补,相加后进位回卷得到全 1。这个性质是接收方验证校验和的依据。

2.3 反码求和的端回进位细节

端回进位是反码求和最容易出错的地方。考虑两个 16 位字 0xFFFF 与 0x0001 相加:

复制代码
  0xFFFF
+ 0x0001
= 0x10000

产生进位 1,将进位加回最低位:0x0000 + 0x0001 = 0x0001。所以 0xFFFF + 0x0001 的反码和结果为 0x0001。

再考虑 0xFFFF + 0xFFFF:

复制代码
  0xFFFF
+ 0xFFFF
= 0x1FFFE

进位 1 加回:0xFFFE + 0x0001 = 0xFFFF。所以结果为 0xFFFF。

在硬件实现中,端回进位可以通过在加法器输出端将进位位反馈到输入端实现,也可以先做普通加法,最后统一处理进位。对于多个字相加,通常采用"先累加,再折叠"的策略:用足够宽的累加器(如 32 位或 64 位)累加所有字,最后将高 16 位与低 16 位相加,重复直到高 16 位为 0。

如果需要系统学习FPGA,这里有一份整理的FPGA汇总课程资料,包含BD教程、Vivado教程、IP 核教程等,有需要的朋友可以保存:链接: https://pan.baidu.com/s/1pIFS8GEYu_jH1np4ZLG2Kw?pwd=wbbw 提取码: wbbw 整理不易,尽快保存

2.4 CRC 的数学基础:GF(2) 上的多项式除法

CRC 基于有限域 GF(2) 上的多项式运算。在 GF(2) 中,加法和减法都等价于异或运算,没有进位和借位。CRC 计算本质上是将数据视为一个多项式,除以一个生成多项式,取余数作为校验值。

设数据多项式为 M(x),生成多项式为 G(x),阶数为 r,则 CRC 余数 R(x) 满足:

复制代码
M(x) * x^r = Q(x) * G(x) + R(x)

其中 R(x) 的阶数小于 r。CRC 值就是 R(x) 的系数序列。

以太网使用的 CRC-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

对应的十六进制表示为 0x04C11DB7。这个多项式是 IEEE 802.3 标准规定的,所有以太网设备必须使用。

2.5 CRC 的位运算实现

CRC 的逐位实现基于 LFSR。对于 CRC-32,寄存器宽度为 32 位。每处理一个数据位,将数据位与寄存器最高位异或,若结果为 1,则寄存器左移一位并与生成多项式异或;否则仅左移一位。

伪代码如下:

复制代码
crc = 0xFFFFFFFF
for each bit b in data:
    msb = (crc >> 31) & 1
    crc = (crc << 1) & 0xFFFFFFFF
    if (msb ^ b):
        crc = crc ^ 0x04C11DB7

处理完所有数据后,将 crc 取反得到最终结果。注意以太网 CRC-32 的初始值为 0xFFFFFFFF,最终结果取反,这是标准规定的。

3. 千兆以太网帧结构与校验和字段分布

3.1 以太网帧格式

IEEE 802.3 定义的以太网帧格式如下:

字段 长度(字节) 说明
前导码(Preamble) 7 0x55 重复 7 次,用于时钟同步
帧起始定界符(SFD) 1 0xD5,标志帧开始
目的 MAC 地址 6 接收方 MAC
源 MAC 地址 6 发送方 MAC
长度/类型 2 小于 1536 为长度,大于等于 1536 为类型
数据与填充 46~1500 上层数据,不足 46 字节需填充
帧校验序列(FCS) 4 CRC-32 校验值

FCS 覆盖范围从目的 MAC 地址开始,到数据与填充结束,不包括前导码和 SFD。这一点非常关键:CRC 计算从目的 MAC 地址的第一个字节开始,到数据字段的最后一个字节结束。

3.2 最小帧与填充对校验和的影响

以太网规定最小帧长为 64 字节(不含前导码和 SFD),即从目的 MAC 地址到 FCS 共 64 字节。其中 FCS 占 4 字节,所以目的 MAC 到数据字段至少 60 字节。由于 MAC 头占 14 字节,数据字段至少需要 46 字节。如果上层数据不足 46 字节,必须填充到 46 字节。

填充字节参与 FCS 计算,这是必须注意的。很多初学者在 FPGA 中实现时忘记填充,导致 FCS 计算错误。填充字节通常为 0x00,但标准并未规定填充内容,接收方也不检查填充内容,只检查 FCS。

3.3 IP 首部校验和字段

IPv4 首部格式中,校验和字段位于首部的第 11、12 字节(从 0 开始计数为第 10、11 字节)。IPv4 首部校验和只覆盖 IP 首部,不覆盖数据部分。首部长度可变(20~60 字节),校验和计算时需要根据 IHL 字段确定首部长度。

IPv4 首部校验和的计算方法:将首部按 16 位字划分,校验和字段本身置 0,对所有字做反码求和,结果取反即为校验和。

3.4 UDP 校验和与伪首部

UDP 校验和覆盖范围包括伪首部、UDP 首部和 UDP 数据。伪首部包含源 IP 地址、目的 IP 地址、协议号、UDP 长度等信息,共 12 字节:

字段 长度(字节)
源 IP 地址 4
目的 IP 地址 4
保留(0x00) 1
协议号(17) 1
UDP 长度 2

伪首部不实际传输,仅用于校验和计算。UDP 校验和字段在计算时置 0。如果 UDP 校验和计算结果为 0x0000,应发送 0xFFFF,因为 0x0000 表示"未使用校验和"。

3.5 TCP 校验和与伪首部

TCP 校验和与 UDP 类似,也使用伪首部,但协议号为 6,长度字段为 TCP 长度(首部加数据)。TCP 校验和覆盖 TCP 首部和 TCP 数据。TCP 首部中的校验和字段在计算时置 0。

TCP 与 UDP 的一个重要区别是:TCP 校验和是强制的,UDP 校验和在某些实现中是可选的(IPv4 下可置 0 表示不校验,IPv6 下强制)。

如果需要系统学习FPGA,这里有一份整理的FPGA汇总课程资料,包含BD教程、Vivado教程、IP 核教程等,有需要的朋友可以保存:链接: https://pan.baidu.com/s/1pIFS8GEYu_jH1np4ZLG2Kw?pwd=wbbw 提取码: wbbw 整理不易,尽快保存

3.6 四类校验字段横向对比

把以太网 FCS、IPv4 首部校验和、UDP 校验和、TCP 校验和放在一起对比,可以更清楚地看出它们在覆盖范围、算法类型和 FPGA 实现方式上的差异。下表从六个维度对四类校验字段做横向对比:

对比维度 以太网 FCS(CRC-32) IPv4 首部校验和 UDP 校验和 TCP 校验和
覆盖范围 目的 MAC 地址到数据与填充结束(不含前导码、SFD、FCS 本身) 仅 IPv4 首部(20~60 字节,由 IHL 决定) 伪首部(12 字节)+ UDP 首部(8 字节)+ UDP 数据 伪首部(12 字节)+ TCP 首部(20~60 字节)+ TCP 数据
算法类型 CRC-32,GF(2) 多项式除法,生成多项式 0x04C11DB7 16 位反码求和(One's Complement Sum) 16 位反码求和 16 位反码求和
初始值 0xFFFFFFFF 0x0000 0x0000(伪首部作为初始累加值) 0x0000(伪首部作为初始累加值)
是否强制 强制,所有以太网帧必须携带 强制,IPv4 首部必须携带 IPv4 下可选(置 0 表示不校验),IPv6 下强制 强制,TCP 必须携带
校验和字段置 0 规则 FCS 字段不参与计算,发送方在数据末尾追加 4 字节结果 计算前将首部偏移 10~11 字节的校验和字段置 0 计算前将 UDP 首部偏移 6~7 字节的校验和字段置 0 计算前将 TCP 首部偏移 16~17 字节的校验和字段置 0
接收方验证判据 对含 FCS 的完整帧计算,余数为魔数 0xDEBB20E3(或重算比对) 含校验和字段做反码求和,结果为 0xFFFF 含校验和字段做反码求和,结果为 0xFFFF;收到 0x0000 则跳过校验 含校验和字段做反码求和,结果为 0xFFFF
FPGA 实现方式 LFSR(逐位)或并行异或网络(逐字节,约 200~300 LUT) 加法器树 + 折叠(端回进位) 加法器树 + 折叠,伪首部预加载 加法器树 + 折叠,伪首部预加载,可与 UDP 共用累加器

从表中可以看出,四类校验字段虽然都叫"校验和",但本质上是两类完全不同的算法:以太网 FCS 属于 CRC 类,基于 GF(2) 多项式除法,硬件上用 LFSR 或并行异或网络实现,检测能力最强,能发现任意长度不超过 32 位的突发错误;而 IPv4、UDP、TCP 的校验和属于反码求和类,基于 16 位加法与端回进位,硬件上用加法器树加折叠逻辑实现,资源开销小、延迟低,但对某些多位错误的检测能力弱于 CRC。

另一个值得注意的差异是"是否强制"与"置 0 规则"。以太网 FCS 和 TCP 校验和是强制的,没有"关闭"选项;IPv4 首部校验和虽然强制,但计算时必须先把校验和字段本身置 0,否则会把旧值算进去;UDP 校验和在 IPv4 下允许置 0 表示"未计算",因此发送方在计算结果恰为 0x0000 时要改发 0xFFFF,接收方收到 0x0000 则跳过校验。这些细节在 FPGA 实现中极易出错,需要在状态机中显式处理。

在接收方验证判据上,CRC 类与反码求和类也截然不同:CRC 的判据是一个固定的"魔数"余数(以太网 CRC-32 为 0xDEBB20E3),而反码求和的判据是"全 1"即 0xFFFF。前者依赖生成多项式和初始值/最终取反的约定,后者依赖反码求和的互补性质。理解这一区别,是正确设计接收方向校验逻辑的前提。

4. 以太网 CRC-32 的 FPGA 实现

4.1 逐位实现的 Verilog 代码

最直接的 CRC-32 实现是逐位计算,每个时钟周期处理一位。代码如下:

verilog 复制代码
module crc32_bitwise (
    input  wire        clk,
    input  wire        rst_n,
    input  wire        init,
    input  wire        data_bit,
    input  wire        data_valid,
    output reg  [31:0] crc_out
);

    wire msb = crc_out[31];
    wire feedback = msb ^ data_bit;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            crc_out <= 32'hFFFFFFFF;
        end else if (init) begin
            crc_out <= 32'hFFFFFFFF;
        end else if (data_valid) begin
            crc_out <= {crc_out[30:0], 1'b0} ^ (feedback ? 32'h04C11DB7 : 32'h00000000);
        end
    end

endmodule

这段代码严格对应 2.5 节的伪代码。注意 crc_out <= {crc_out[30:0], 1'b0} 是左移一位,最低位补 0,然后根据 feedback 决定是否异或生成多项式。

4.2 逐字节实现的推导

千兆以太网在 8 位 GMII 接口下每周期传输一个字节,逐位实现需要 8 个周期处理一个字节,无法满足线速。因此需要将 8 位数据一次性处理,即推导 8 位并行 CRC。

逐字节 CRC 的推导方法是:将 8 个位的处理展开,消去中间状态,得到只与当前 CRC 和输入字节有关的表达式。对于 CRC-32,8 位并行的表达式如下(设 crc 为当前 32 位寄存器,d 为输入字节,d7 为最高位):

复制代码
c[0]  = crc[24] ^ crc[30] ^ d[0] ^ d[6]
c[1]  = crc[24] ^ crc[25] ^ crc[30] ^ crc[31] ^ d[0] ^ d[1] ^ d[6] ^ d[7]
c[2]  = crc[24] ^ crc[25] ^ crc[26] ^ crc[30] ^ crc[31] ^ d[0] ^ d[1] ^ d[2] ^ d[6] ^ d[7]
c[3]  = crc[25] ^ crc[26] ^ crc[27] ^ crc[31] ^ d[1] ^ d[2] ^ d[3] ^ d[7]
c[4]  = crc[24] ^ crc[26] ^ crc[27] ^ crc[28] ^ crc[30] ^ d[0] ^ d[2] ^ d[3] ^ d[4] ^ d[6]
c[5]  = crc[24] ^ crc[25] ^ crc[27] ^ crc[28] ^ crc[29] ^ crc[30] ^ crc[31] ^ d[0] ^ d[1] ^ d[3] ^ d[4] ^ d[5] ^ d[6] ^ d[7]
c[6]  = crc[25] ^ crc[26] ^ crc[28] ^ crc[29] ^ crc[30] ^ crc[31] ^ d[1] ^ d[2] ^ d[4] ^ d[5] ^ d[6] ^ d[7]
c[7]  = crc[24] ^ crc[26] ^ crc[27] ^ crc[29] ^ crc[31] ^ d[0] ^ d[2] ^ d[3] ^ d[5] ^ d[7]
c[8]  = crc[0] ^ crc[24] ^ crc[25] ^ crc[27] ^ crc[28] ^ d[0] ^ d[1] ^ d[3] ^ d[4]
c[9]  = crc[1] ^ crc[25] ^ crc[26] ^ crc[28] ^ crc[29] ^ d[1] ^ d[2] ^ d[4] ^ d[5]
c[10] = crc[2] ^ crc[24] ^ crc[26] ^ crc[27] ^ crc[29] ^ d[0] ^ d[2] ^ d[3] ^ d[5]
c[11] = crc[3] ^ crc[24] ^ crc[25] ^ crc[27] ^ crc[28] ^ d[0] ^ d[1] ^ d[3] ^ d[4]
c[12] = crc[4] ^ crc[24] ^ crc[25] ^ crc[26] ^ crc[28] ^ crc[29] ^ crc[30] ^ d[0] ^ d[1] ^ d[2] ^ d[4] ^ d[5] ^ d[6]
c[13] = crc[5] ^ crc[25] ^ crc[26] ^ crc[27] ^ crc[29] ^ crc[30] ^ crc[31] ^ d[1] ^ d[2] ^ d[3] ^ d[5] ^ d[6] ^ d[7]
c[14] = crc[6] ^ crc[26] ^ crc[27] ^ crc[28] ^ crc[30] ^ crc[31] ^ d[2] ^ d[3] ^ d[4] ^ d[6] ^ d[7]
c[15] = crc[7] ^ crc[27] ^ crc[28] ^ crc[29] ^ crc[31] ^ d[3] ^ d[4] ^ d[5] ^ d[7]
c[16] = crc[8] ^ crc[24] ^ crc[28] ^ crc[29] ^ d[0] ^ d[4] ^ d[5]
c[17] = crc[9] ^ crc[25] ^ crc[29] ^ crc[30] ^ d[1] ^ d[5] ^ d[6]
c[18] = crc[10] ^ crc[26] ^ crc[30] ^ crc[31] ^ d[2] ^ d[6] ^ d[7]
c[19] = crc[11] ^ crc[27] ^ crc[31] ^ d[3] ^ d[7]
c[20] = crc[12] ^ crc[28] ^ d[4]
c[21] = crc[13] ^ crc[29] ^ d[5]
c[22] = crc[14] ^ crc[24] ^ d[0]
c[23] = crc[15] ^ crc[24] ^ crc[25] ^ crc[30] ^ d[0] ^ d[1] ^ d[6]
c[24] = crc[16] ^ crc[25] ^ crc[26] ^ crc[31] ^ d[1] ^ d[2] ^ d[7]
c[25] = crc[17] ^ crc[26] ^ crc[27] ^ d[2] ^ d[3]
c[26] = crc[18] ^ crc[24] ^ crc[27] ^ crc[28] ^ crc[30] ^ d[0] ^ d[3] ^ d[4] ^ d[6]
c[27] = crc[19] ^ crc[25] ^ crc[28] ^ crc[29] ^ crc[31] ^ d[1] ^ d[4] ^ d[5] ^ d[7]
c[28] = crc[20] ^ crc[26] ^ crc[29] ^ crc[30] ^ d[2] ^ d[5] ^ d[6]
c[29] = crc[21] ^ crc[27] ^ crc[30] ^ crc[31] ^ d[3] ^ d[6] ^ d[7]
c[30] = crc[22] ^ crc[28] ^ crc[31] ^ d[4] ^ d[7]
c[31] = crc[23] ^ crc[29] ^ d[5]

这些表达式看起来复杂,但都是异或运算,在 FPGA 中可以用查找表(LUT)高效实现。每个输出位是若干输入位的异或,综合工具会自动优化。

4.3 逐字节 CRC 的 Verilog 实现

将上述表达式写成 Verilog:

verilog 复制代码
module crc32_bytewise (
    input  wire        clk,
    input  wire        rst_n,
    input  wire        init,
    input  wire [7:0]  data_in,
    input  wire        data_valid,
    output reg  [31:0] crc_out
);

    wire [31:0] c;
    wire [31:0] crc = crc_out;
    wire [7:0]  d   = data_in;

    assign c[0]  = crc[24] ^ crc[30] ^ d[0] ^ d[6];
    assign c[1]  = crc[24] ^ crc[25] ^ crc[30] ^ crc[31] ^ d[0] ^ d[1] ^ d[6] ^ d[7];
    assign c[2]  = crc[24] ^ crc[25] ^ crc[26] ^ crc[30] ^ crc[31] ^ d[0] ^ d[1] ^ d[2] ^ d[6] ^ d[7];
    assign c[3]  = crc[25] ^ crc[26] ^ crc[27] ^ crc[31] ^ d[1] ^ d[2] ^ d[3] ^ d[7];
    assign c[4]  = crc[24] ^ crc[26] ^ crc[27] ^ crc[28] ^ crc[30] ^ d[0] ^ d[2] ^ d[3] ^ d[4] ^ d[6];
    assign c[5]  = crc[24] ^ crc[25] ^ crc[27] ^ crc[28] ^ crc[29] ^ crc[30] ^ crc[31] ^ d[0] ^ d[1] ^ d[3] ^ d[4] ^ d[5] ^ d[6] ^ d[7];
    assign c[6]  = crc[25] ^ crc[26] ^ crc[28] ^ crc[29] ^ crc[30] ^ crc[31] ^ d[1] ^ d[2] ^ d[4] ^ d[5] ^ d[6] ^ d[7];
    assign c[7]  = crc[24] ^ crc[26] ^ crc[27] ^ crc[29] ^ crc[31] ^ d[0] ^ d[2] ^ d[3] ^ d[5] ^ d[7];
    assign c[8]  = crc[0] ^ crc[24] ^ crc[25] ^ crc[27] ^ crc[28] ^ d[0] ^ d[1] ^ d[3] ^ d[4];
    assign c[9]  = crc[1] ^ crc[25] ^ crc[26] ^ crc[28] ^ crc[29] ^ d[1] ^ d[2] ^ d[4] ^ d[5];
    assign c[10] = crc[2] ^ crc[24] ^ crc[26] ^ crc[27] ^ crc[29] ^ d[0] ^ d[2] ^ d[3] ^ d[5];
    assign c[11] = crc[3] ^ crc[24] ^ crc[25] ^ crc[27] ^ crc[28] ^ d[0] ^ d[1] ^ d[3] ^ d[4];
    assign c[12] = crc[4] ^ crc[24] ^ crc[25] ^ crc[26] ^ crc[28] ^ crc[29] ^ crc[30] ^ d[0] ^ d[1] ^ d[2] ^ d[4] ^ d[5] ^ d[6];
    assign c[13] = crc[5] ^ crc[25] ^ crc[26] ^ crc[27] ^ crc[29] ^ crc[30] ^ crc[31] ^ d[1] ^ d[2] ^ d[3] ^ d[5] ^ d[6] ^ d[7];
    assign c[14] = crc[6] ^ crc[26] ^ crc[27] ^ crc[28] ^ crc[30] ^ crc[31] ^ d[2] ^ d[3] ^ d[4] ^ d[6] ^ d[7];
    assign c[15] = crc[7] ^ crc[27] ^ crc[28] ^ crc[29] ^ crc[31] ^ d[3] ^ d[4] ^ d[5] ^ d[7];
    assign c[16] = crc[8] ^ crc[24] ^ crc[28] ^ crc[29] ^ d[0] ^ d[4] ^ d[5];
    assign c[17] = crc[9] ^ crc[25] ^ crc[29] ^ crc[30] ^ d[1] ^ d[5] ^ d[6];
    assign c[18] = crc[10] ^ crc[26] ^ crc[30] ^ crc[31] ^ d[2] ^ d[6] ^ d[7];
    assign c[19] = crc[11] ^ crc[27] ^ crc[31] ^ d[3] ^ d[7];
    assign c[20] = crc[12] ^ crc[28] ^ d[4];
    assign c[21] = crc[13] ^ crc[29] ^ d[5];
    assign c[22] = crc[14] ^ crc[24] ^ d[0];
    assign c[23] = crc[15] ^ crc[24] ^ crc[25] ^ crc[30] ^ d[0] ^ d[1] ^ d[6];
    assign c[24] = crc[16] ^ crc[25] ^ crc[26] ^ crc[31] ^ d[1] ^ d[2] ^ d[7];
    assign c[25] = crc[17] ^ crc[26] ^ crc[27] ^ d[2] ^ d[3];
    assign c[26] = crc[18] ^ crc[24] ^ crc[27] ^ crc[28] ^ crc[30] ^ d[0] ^ d[3] ^ d[4] ^ d[6];
    assign c[27] = crc[19] ^ crc[25] ^ crc[28] ^ crc[29] ^ crc[31] ^ d[1] ^ d[4] ^ d[5] ^ d[7];
    assign c[28] = crc[20] ^ crc[26] ^ crc[29] ^ crc[30] ^ d[2] ^ d[5] ^ d[6];
    assign c[29] = crc[21] ^ crc[27] ^ crc[30] ^ crc[31] ^ d[3] ^ d[6] ^ d[7];
    assign c[30] = crc[22] ^ crc[28] ^ crc[31] ^ d[4] ^ d[7];
    assign c[31] = crc[23] ^ crc[29] ^ d[5];

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            crc_out <= 32'hFFFFFFFF;
        end else if (init) begin
            crc_out <= 32'hFFFFFFFF;
        end else if (data_valid) begin
            crc_out <= c;
        end
    end

endmodule

这段代码每周期处理一个字节,满足千兆以太网 8 位接口的线速要求。综合后通常占用约 200~300 个 LUT,具体取决于器件和综合策略。

4.4 CRC 的初始值与最终取反

以太网 CRC-32 的初始值为 0xFFFFFFFF,处理完所有数据后取反输出。这个"初始全 1、最终取反"的约定来自标准,目的是让前导零也能被检测到。在 FPGA 实现中,初始值在复位或 init 信号时加载,最终取反在帧结束时进行。

需要注意的是,FCS 字段本身不参与 CRC 计算。发送方计算完数据部分的 CRC 后,将结果取反后附加到帧尾。接收方对包括 FCS 在内的整个帧计算 CRC,若结果为固定值(0xDEBB20E3 的某种形式,取决于实现约定),则认为帧正确。

如果需要系统学习FPGA,这里有一份整理的FPGA汇总课程资料,包含BD教程、Vivado教程、IP 核教程等,有需要的朋友可以保存:链接: https://pan.baidu.com/s/1pIFS8GEYu_jH1np4ZLG2Kw?pwd=wbbw 提取码: wbbw 整理不易,尽快保存

4.5 接收方向的 CRC 校验

接收方校验 FCS 有两种方式:

第一种是"重算比对":对收到的数据(不含 FCS)重新计算 CRC,与收到的 FCS 比对。这种方式需要缓存整个帧或延迟判断。

第二种是"整体校验":对包括 FCS 在内的整个帧计算 CRC,若最终结果为某个固定值则认为正确。对于以太网 CRC-32,若对完整帧(含 FCS)计算,最终寄存器值应为 0xDEBB20E3(这是 CRC-32 的"魔数",即对正确帧计算得到的余数)。这种方式不需要缓存,适合流水线实现。

在 FPGA 中,第二种方式更常用,因为它可以在数据流过的同时计算,帧结束时直接判断结果。

5. IPv4 首部校验和的 FPGA 实现

5.1 IPv4 首部结构回顾

IPv4 首部最小 20 字节,格式如下:

偏移(字节) 字段 长度(字节)
0 版本 + 首部长度 1
1 服务类型 1
2~3 总长度 2
4~5 标识 2
6~7 标志 + 片偏移 2
8 生存时间 1
9 协议 1
10~11 首部校验和 2
12~15 源 IP 地址 4
16~19 目的 IP 地址 4
20~39 选项(可选) 0~40

首部长度由 IHL 字段(第 0 字节低 4 位)决定,单位为 4 字节,最小为 5(20 字节),最大为 15(60 字节)。

5.2 首部校验和的计算步骤

IPv4 首部校验和的计算步骤:

  1. 将校验和字段(偏移 10~11)置 0。
  2. 将首部按 16 位字划分,共 IHL*2 个字。
  3. 对所有字做反码求和。
  4. 将结果取反,填入校验和字段。

接收方验证时,将包括校验和字段在内的所有字做反码求和,若结果为 0xFFFF 则正确。

5.3 16 位反码求和模块的 Verilog 实现

下面是一个通用的 16 位反码求和模块,支持逐字累加:

verilog 复制代码
module ones_complement_adder (
    input  wire        clk,
    input  wire        rst_n,
    input  wire        init,
    input  wire [15:0] data_in,
    input  wire        data_valid,
    output reg  [31:0] sum_out
);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sum_out <= 32'd0;
        end else if (init) begin
            sum_out <= 32'd0;
        end else if (data_valid) begin
            sum_out <= sum_out + {16'd0, data_in};
        end
    end

endmodule

这个模块用 32 位累加器累加 16 位字,最多可累加 65535 个字而不溢出(因为 32 位可容纳 2^32,而 65535 * 65535 < 2^32)。对于 IPv4 首部最多 60 字节即 30 个字,完全不会溢出。

累加完成后,需要做折叠处理,将高 16 位与低 16 位相加,直到高 16 位为 0:

verilog 复制代码
module ones_complement_fold (
    input  wire [31:0] sum_in,
    output wire [15:0] sum_out
);

    wire [16:0] fold1 = {1'b0, sum_in[15:0]} + {1'b0, sum_in[31:16]};
    wire [16:0] fold2 = {1'b0, fold1[15:0]} + {1'b0, fold1[16]};
    assign sum_out = fold2[15:0] + {15'd0, fold2[16]};

endmodule

折叠两次即可保证结果正确,因为 32 位累加器的高 16 位最多为 0xFFFF,折叠一次后最多产生 1 位进位,再折叠一次即可消除。

5.4 首部校验和的完整计算流程

在 FPGA 中,IPv4 首部校验和的计算通常与首部解析流水线结合。流程如下:

  1. 检测到 IP 首部开始时,初始化累加器。
  2. 逐 16 位字累加,跳过校验和字段(或将其视为 0)。
  3. 首部结束时,折叠累加器,取反得到校验和。
  4. 若为发送方向,将校验和写入首部;若为接收方向,与收到的校验和比对。

由于首部长度可变,需要根据 IHL 字段动态确定累加字数。在硬件中,可以用计数器配合比较器实现。

如果需要系统学习FPGA,这里有一份整理的FPGA汇总课程资料,包含BD教程、Vivado教程、IP 核教程等,有需要的朋友可以保存:链接: https://pan.baidu.com/s/1pIFS8GEYu_jH1np4ZLG2Kw?pwd=wbbw 提取码: wbbw 整理不易,尽快保存

5.5 增量更新:NAT 场景下的校验和修正

在 NAT(网络地址转换)场景中,修改 IP 地址后需要更新首部校验和。如果重新计算整个首部,代价较高。RFC 1624 给出了增量更新方法:

复制代码
新校验和 = ~(~旧校验和 + ~旧值 + 新值)

其中旧值和新值是被修改的 16 位字段。这个公式利用了反码求和的线性性质,只需三次加法和取反即可完成更新,无需重新遍历首部。

在 FPGA 中实现 NAT 时,增量更新可以显著降低延迟和资源消耗。但需要注意 RFC 1624 的细节:当结果为 0x0000 时应改为 0xFFFF(对于 UDP)或保持 0x0000(对于 IP 首部,实际上 IP 首部校验和为 0 是合法的,表示计算结果为 0xFFFF 取反)。

6. UDP 校验和的 FPGA 实现

6.1 UDP 校验和的计算范围

UDP 校验和覆盖伪首部、UDP 首部和 UDP 数据。伪首部 12 字节,UDP 首部 8 字节,数据长度可变。计算时 UDP 校验和字段置 0。

UDP 首部格式:

偏移(字节) 字段 长度(字节)
0~1 源端口 2
2~3 目的端口 2
4~5 UDP 长度 2
6~7 校验和 2

UDP 长度字段包含首部和数据,最小为 8。

如果需要系统学习FPGA,这里有一份整理的FPGA汇总课程资料,包含BD教程、Vivado教程、IP 核教程等,有需要的朋友可以保存:链接: https://pan.baidu.com/s/1pIFS8GEYu_jH1np4ZLG2Kw?pwd=wbbw 提取码: wbbw 整理不易,尽快保存

6.2 伪首部的构造

伪首部在 FPGA 中不实际存储,而是在计算时动态拼接。伪首部包含:

  • 源 IP 地址(4 字节,来自 IP 首部)
  • 目的 IP 地址(4 字节,来自 IP 首部)
  • 0x00(1 字节)
  • 协议号 17(1 字节)
  • UDP 长度(2 字节,来自 UDP 首部)

在硬件中,可以在 UDP 校验和计算开始时,将这几个字段作为初始累加值送入累加器。

6.3 UDP 校验和的 Verilog 实现

下面是一个 UDP 校验和计算模块的框架:

verilog 复制代码
module udp_checksum (
    input  wire        clk,
    input  wire        rst_n,
    input  wire        start,
    input  wire [31:0] src_ip,
    input  wire [31:0] dst_ip,
    input  wire [15:0] udp_len,
    input  wire [15:0] data_in,
    input  wire        data_valid,
    input  wire        data_last,
    output reg  [15:0] checksum,
    output reg         checksum_valid
);

    reg [31:0] sum;
    reg [15:0] pseudo_sum;

    // 伪首部累加:源IP(2字) + 目的IP(2字) + 协议号 + UDP长度
    wire [31:0] pseudo_word0 = src_ip[31:16];
    wire [31:0] pseudo_word1 = src_ip[15:0];
    wire [31:0] pseudo_word2 = dst_ip[31:16];
    wire [31:0] pseudo_word3 = dst_ip[15:0];
    wire [31:0] pseudo_word4 = 32'h0011;  // 0x00 + 协议号17
    wire [31:0] pseudo_word5 = {16'd0, udp_len};

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sum <= 32'd0;
            checksum <= 16'd0;
            checksum_valid <= 1'b0;
        end else if (start) begin
            sum <= pseudo_word0 + pseudo_word1 + pseudo_word2 +
                   pseudo_word3 + pseudo_word4 + pseudo_word5;
            checksum_valid <= 1'b0;
        end else if (data_valid) begin
            sum <= sum + {16'd0, data_in};
            if (data_last) begin
                // 折叠并取反
                checksum <= ~(sum[15:0] + sum[31:16] + 
                             ((sum[15:0] + sum[31:16]) >> 16));
                checksum_valid <= 1'b1;
            end
        end
    end

endmodule

这段代码在 start 时累加伪首部,然后逐字累加数据,最后折叠取反。注意折叠逻辑需要处理两次进位,实际实现中应更严谨地展开。

6.4 UDP 校验和为 0 的特殊处理

如果计算得到的 UDP 校验和为 0x0000,发送时应改为 0xFFFF。这是因为 0x0000 在 UDP 中表示"发送方未计算校验和",接收方应忽略校验和字段。在 FPGA 中,这个处理只需在输出前加一个判断:

verilog 复制代码
assign checksum_final = (checksum == 16'h0000) ? 16'hFFFF : checksum;

如果需要系统学习FPGA,这里有一份整理的FPGA汇总课程资料,包含BD教程、Vivado教程、IP 核教程等,有需要的朋友可以保存:链接: https://pan.baidu.com/s/1pIFS8GEYu_jH1np4ZLG2Kw?pwd=wbbw 提取码: wbbw 整理不易,尽快保存

6.5 接收方向的 UDP 校验和验证

接收方验证 UDP 校验和时,将伪首部、UDP 首部(含校验和字段)和数据一起做反码求和,若结果为 0xFFFF 则正确。注意接收方不需要将校验和字段置 0,因为验证的是包含校验和的完整数据。

如果收到的 UDP 校验和为 0x0000,接收方应跳过校验(IPv4 下允许)。

7. TCP 校验和的 FPGA 实现

7.1 TCP 校验和与 UDP 的异同

TCP 校验和与 UDP 校验和的计算方法基本相同,都是伪首部加首部加数据的反码求和。区别在于:

  • 协议号不同:TCP 为 6,UDP 为 17。
  • 长度字段含义不同:TCP 的长度字段是 TCP 首部加数据的总长度,UDP 的长度字段是 UDP 首部加数据的总长度,两者含义类似但字段位置不同。
  • TCP 校验和是强制的,没有"置 0 表示不校验"的约定。

TCP 首部格式:

偏移(字节) 字段 长度(字节)
0~1 源端口 2
2~3 目的端口 2
4~7 序列号 4
8~11 确认号 4
12 数据偏移 + 保留 1
13 标志位 1
14~15 窗口大小 2
16~17 校验和 2
18~19 紧急指针 2
20~39 选项(可选) 0~40

7.2 TCP 伪首部的构造

TCP 伪首部与 UDP 伪首部格式相同,只是协议号字段为 6:

  • 源 IP 地址(4 字节)
  • 目的 IP 地址(4 字节)
  • 0x00(1 字节)
  • 协议号 6(1 字节)
  • TCP 长度(2 字节)

7.3 TCP 校验和的 Verilog 实现

TCP 校验和模块可以复用 UDP 的累加器结构,只需修改协议号和长度字段来源:

verilog 复制代码
module tcp_checksum (
    input  wire        clk,
    input  wire        rst_n,
    input  wire        start,
    input  wire [31:0] src_ip,
    input  wire [31:0] dst_ip,
    input  wire [15:0] tcp_len,
    input  wire [15:0] data_in,
    input  wire        data_valid,
    input  wire        data_last,
    output reg  [15:0] checksum,
    output reg         checksum_valid
);

    reg [31:0] sum;

    wire [31:0] pseudo_word0 = src_ip[31:16];
    wire [31:0] pseudo_word1 = src_ip[15:0];
    wire [31:0] pseudo_word2 = dst_ip[31:16];
    wire [31:0] pseudo_word3 = dst_ip[15:0];
    wire [31:0] pseudo_word4 = 32'h0006;  // 0x00 + 协议号6
    wire [31:0] pseudo_word5 = {16'd0, tcp_len};

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sum <= 32'd0;
            checksum <= 16'd0;
            checksum_valid <= 1'b0;
        end else if (start) begin
            sum <= pseudo_word0 + pseudo_word1 + pseudo_word2 +
                   pseudo_word3 + pseudo_word4 + pseudo_word5;
            checksum_valid <= 1'b0;
        end else if (data_valid) begin
            sum <= sum + {16'd0, data_in};
            if (data_last) begin
                checksum <= ~(sum[15:0] + sum[31:16] +
                             ((sum[15:0] + sum[31:16]) >> 16));
                checksum_valid <= 1'b1;
            end
        end
    end

endmodule

7.4 TCP 校验和的增量更新

TCP 在 NAT 场景下同样需要增量更新校验和。RFC 1624 的公式同样适用。此外,TCP 在传输过程中如果只修改了端口号或 IP 地址,可以用增量更新避免重算整个报文。

需要注意的是,TCP 校验和覆盖整个 TCP 报文,包括数据部分。如果数据部分被修改(如应用层代理修改了载荷),则必须重新计算整个校验和,不能增量更新。

8. 线速处理与流水线设计

8.1 千兆以太网的时序约束

千兆以太网在 8 位 GMII 接口下,时钟频率为 125 MHz,每周期传输 1 字节。这意味着校验和模块必须在 125 MHz 下每周期完成一次运算,且不能有停顿。

对于 CRC-32 逐字节实现,组合逻辑路径包括 32 个异或门的级联,延迟约为 5~8 个 LUT 层级。在 125 MHz(8 ns 周期)下,这个延迟通常可以满足,但在高速器件或低端器件上可能需要插入流水线寄存器。

对于反码求和,16 位加法器的延迟较小,但累加器的反馈路径可能成为关键路径。如果累加器需要每周期更新,反馈路径上的加法器延迟必须小于 8 ns。

8.2 CRC 的流水线优化

如果逐字节 CRC 的组合逻辑延迟过大,可以将其拆分为两级流水线:第一级计算部分异或,第二级完成剩余异或。但这样会引入一个周期的延迟,需要相应的数据延迟对齐。

另一种优化是使用 16 位或 32 位并行 CRC,每周期处理 2 或 4 字节。在千兆以太网 8 位接口下不需要,但在 10G 以太网(64 位接口)下必须使用更宽的并行 CRC。

8.3 反码求和的流水线设计

反码求和的累加器可以采用流水线加法器树。对于每周期输入一个 16 位字的情况,可以用两级流水线:第一级将输入寄存,第二级累加。但累加器的反馈依赖使得流水线化较困难。

一种常见做法是使用多个累加器并行工作,最后合并。例如,用两个累加器交替累加奇偶字,最后将两个累加器结果相加。这样可以将反馈路径的延迟减半。

8.4 校验和与数据通路的对齐

在 FPGA 中,校验和计算通常与数据通路并行进行。数据从 MAC 接收后,同时送入校验和模块和上层处理模块。校验和结果在帧结束时产生,需要与帧结束信号对齐。

如果校验和模块有流水线延迟,需要在数据通路上插入相应的延迟寄存器,确保校验和结果与帧结束信号同时到达判断逻辑。

9. 资源优化与实现技巧

9.1 CRC 的资源优化

CRC-32 逐字节实现的主要资源是异或门。综合工具会将其映射为 LUT。对于 Xilinx 7 系列器件,一个 6 输入 LUT 可以实现 6 输入的任意逻辑函数。CRC 的每个输出位是若干输入的异或,综合工具会自动优化共享子表达式。

减少资源的方法包括:

  • 使用厂商提供的 CRC 硬核(部分器件有)。
  • 使用查找表法:将 CRC 计算转化为查表,但需要较大的 ROM。
  • 对于固定长度的帧,可以预计算部分 CRC。

9.2 反码求和的资源优化

反码求和的主要资源是加法器。16 位加法器在 FPGA 中通常用进位链实现,资源消耗较小。优化方法包括:

  • 使用超前进位加法器减少延迟。
  • 将多个字合并为更宽的字累加,减少加法次数。
  • 使用 DSP 切片实现加法(在大规模累加时)。

如果需要系统学习FPGA,这里有一份整理的FPGA汇总课程资料,包含BD教程、Vivado教程、IP 核教程等,有需要的朋友可以保存:链接: https://pan.baidu.com/s/1pIFS8GEYu_jH1np4ZLG2Kw?pwd=wbbw 提取码: wbbw 整理不易,尽快保存

9.3 共享校验和计算资源

在同时处理多个连接或多种协议的 FPGA 设计中,可以共享校验和计算资源。例如,UDP 和 TCP 校验和可以共用一个累加器,通过时分复用处理不同报文。但需要注意,时分复用会引入延迟,可能影响线速。

9.4 校验和模块的接口设计

良好的接口设计对校验和模块的复用很重要。建议接口包括:

  • 时钟与复位
  • 初始化信号(帧开始)
  • 数据输入与有效信号
  • 数据结束信号
  • 校验和输出与有效信号

这样的接口可以适配不同的协议和不同的数据宽度。

10. 验证方法与常见错误

10.1 使用参考模型验证

验证 CRC 和校验和模块的最可靠方法是与软件参考模型比对。可以用 Python 或 C 实现参考算法,生成测试向量,然后在仿真中比对。

Python 的 zlib.crc32 可以计算 CRC-32,但注意其初始值和最终取反与以太网 CRC-32 一致(zlib 的 crc32 初始值为 0,但内部处理与以太网相同,实际上 zlib.crc32(data) 的结果就是以太网 FCS 去掉取反后的值,需要仔细核对)。

更可靠的方法是使用 binascii.crc32 或手动实现标准算法。

10.2 常见错误一:初始值错误

以太网 CRC-32 的初始值为 0xFFFFFFFF,不是 0。如果初始值设为 0,计算结果会完全不同。这是最常见的错误之一。

10.3 常见错误二:忘记最终取反

以太网 CRC-32 在输出前需要取反。如果忘记取反,FCS 字段会错误。

10.4 常见错误三:字节序问题

以太网 CRC-32 的字节序需要特别注意。CRC 计算时,数据按字节顺序处理,每个字节内按位从高到低(MSB first)处理。但 FCS 字段在帧中的传输顺序是低字节在前(小端)。这两个顺序容易混淆。

具体来说:CRC 计算时,数据字节按帧中的顺序依次送入,每个字节的位从 bit7 到 bit0 处理。计算得到的 32 位 CRC 值,在附加到帧尾时,按字节从低到高排列,即 crc7:0 先传输,crc31:24 最后传输。

10.5 常见错误四:填充字节遗漏

如前所述,以太网最小帧需要填充到 46 字节数据。如果 FPGA 发送短帧时忘记填充,FCS 会错误,且接收方可能丢弃帧。

10.6 常见错误五:伪首部字段错误

UDP/TCP 校验和的伪首部中,协议号字段是 0x00 加协议号,共 2 字节。有些实现错误地将协议号放在高字节或低字节,导致校验和错误。正确做法是:伪首部的第 8 字节为 0x00,第 9 字节为协议号(17 或 6)。

10.7 常见错误六:UDP 校验和为 0 未处理

如果 UDP 校验和计算结果为 0x0000,必须发送 0xFFFF。忘记这个处理会导致接收方认为发送方未计算校验和,从而跳过校验,降低可靠性。

10.8 常见错误七:IP 首部校验和未置 0

计算 IPv4 首部校验和时,必须先将校验和字段置 0。如果忘记置 0,计算结果会包含旧校验和,导致错误。

10.9 常见错误八:TCP 长度字段错误

TCP 伪首部中的长度字段是 TCP 首部加数据的总长度,不是 IP 总长度。混淆这两个长度会导致校验和错误。

10.10 仿真测试平台设计

一个完整的测试平台应包括:

  • 生成随机数据的激励
  • 参考模型计算期望校验和
  • 比对 DUT 输出与期望值
  • 覆盖边界情况:最小帧、最大帧、全 0 数据、全 1 数据、校验和为 0 的情况

11. 完整设计实例:千兆以太网 UDP 发送通路

11.1 系统框图

一个典型的千兆以太网 UDP 发送通路包括:

  • 用户数据接口
  • UDP 首部生成
  • IP 首部生成
  • 以太网帧组装
  • CRC-32 计算
  • GMII 发送接口

校验和计算分布在 UDP 层(UDP 校验和)和 IP 层(IP 首部校验和)以及 MAC 层(FCS)。

11.2 数据流与校验和计算时机

数据流如下:

  1. 用户数据进入,同时送入 UDP 校验和累加器。
  2. UDP 首部生成后,伪首部和首部字段送入累加器。
  3. UDP 校验和计算完成,填入 UDP 首部。
  4. IP 首部生成,计算 IP 首部校验和。
  5. 以太网帧组装,包括 MAC 头、IP 首部、UDP 首部和数据。
  6. 帧数据送入 CRC-32 计算模块。
  7. CRC 结果附加到帧尾,通过 GMII 发送。

11.3 关键时序分析

在 125 MHz 下,每个字节的传输时间为 8 ns。UDP 校验和必须在数据流过的同时计算,不能缓存整个报文。因此累加器必须每周期更新。

IP 首部校验和只涉及 20 字节,可以在首部生成后用一个短流水线计算,不影响数据流。

CRC-32 必须每周期处理一个字节,与数据流同步。

11.4 代码组织建议

建议将校验和模块独立成文件,接口统一,便于复用和验证。顶层模块负责协议组装和校验和模块的调度。

12. 总结

本文围绕 FPGA 与千兆以太网的校验和问题,系统讨论了以太网 CRC-32、IPv4 首部校验和、UDP 校验和、TCP 校验和的原理与实现。核心要点如下:

  • 以太网 CRC-32 使用生成多项式 0x04C11DB7,初始值 0xFFFFFFFF,最终取反,逐字节并行实现可满足千兆线速。
  • IPv4 首部校验和是 16 位反码求和,只覆盖首部,计算时校验和字段置 0。
  • UDP 和 TCP 校验和是伪首部加首部加数据的反码求和,伪首部包含 IP 地址、协议号和长度。
  • 反码求和的端回进位是关键细节,硬件中通过折叠累加器实现。
  • 线速处理要求校验和模块每周期完成一次运算,必要时插入流水线。
  • 常见错误包括初始值错误、忘记取反、字节序错误、填充遗漏、伪首部字段错误等。

掌握这些原理和实现技巧,可以在 FPGA 中可靠地实现千兆以太网的校验和功能,保证数据传输的完整性。

13. 常见错误与排查清单

前面各节分别讨论了 CRC-32、IPv4 首部校验和、UDP 与 TCP 校验和的原理与实现,也零散提到了一些易错点。本节把这些错误集中整理成一张排查清单,按"错误现象 → 根因 → 修正方法"三列组织,便于在调试时逐条对照。表中每一条都对应前文的具体章节,遇到问题时可以回溯查看详细推导。

序号 典型错误 错误现象 根因 修正方法
1 CRC 初始值遗漏或错误 发送帧的 FCS 与软件参考模型(如 zlib.crc32)结果完全不一致,且差异与数据内容无关,呈现固定偏移 以太网 CRC-32 的初始值必须是 0xFFFFFFFF,实现中误用 0x00000000 或复位后未加载初值 在复位或帧起始(init)时把 32 位寄存器加载为 32'hFFFFFFFF;逐位与逐字节实现都要检查这一处,见 4.1、4.3、4.4 节
2 最终取反遗漏 计算出的 CRC 与参考值恰好按位互补(逐位取反后一致),接收方整体校验余数不等于 0xDEBB20E3 以太网 CRC-32 要求处理完数据后对寄存器取反再输出,实现中直接输出了寄存器原值 在帧结束输出 FCS 时加一级 ~crc_out;若采用"整体校验"方式,则接收判据要相应改为对含 FCS 的完整帧计算后比对魔数,见 4.4、4.5 节
3 FCS 覆盖范围误含前导码/SFD 帧内容正确但 FCS 始终校验失败;用抓包工具比对时发现 CRC 计算起点比标准早 8 字节 把 7 字节前导码(0x55)和 1 字节 SFD(0xD5)也送入了 CRC 计算,而标准规定 CRC 从目的 MAC 地址第一个字节开始 CRC 计算使能应在 SFD 之后、目的 MAC 首字节处拉高,前导码与 SFD 只用于时钟同步,不参与 FCS,见 3.1 节
4 填充字节未参与 CRC 短帧(数据不足 46 字节)FCS 错误,长帧正常;接收方因 FCS 错误丢弃短帧 以太网最小帧要求目的 MAC 到数据字段至少 60 字节,不足需填充,而实现中只对有效载荷计算 CRC,漏掉了填充字节 填充必须在 CRC 计算之前完成,填充字节(通常 0x00)与数据一样送入 CRC 模块;建议在帧组装状态机中先补齐长度再启动 CRC,见 3.2 节
5 IPv4 校验和字段未置 0 首部校验和计算结果与参考值不符,且每次重算结果都不同(依赖上一次填入的旧值) 计算 IPv4 首部校验和时未把偏移 10~11 字节的校验和字段清零,旧值被计入反码求和 累加前将该 16 位字强制置 0(或在累加时跳过该字),计算完成后再写回;接收方向验证时则要包含该字段,见 3.3、5.2 节
6 UDP 校验和为 0 未改 0xFFFF 发送方算出的校验和恰为 0x0000 时直接发出,接收方按"未计算校验和"处理并跳过校验,可靠性下降 UDP 协议规定 0x0000 表示发送方未计算校验和,因此计算结果为 0 时必须改发 0xFFFF 在输出前加判断 checksum_final = (checksum == 16'h0000) ? 16'hFFFF : checksum;接收方向收到 0x0000 则跳过校验,见 3.4、6.4 节
7 端回进位折叠不彻底 校验和偶尔差 1(0x0001),或结果比参考值小 0x0001;数据中大量 0xFFFF 时更容易出现 32 位累加器折叠时只做了一次高低 16 位相加,未处理折叠产生的二次进位;或直接用普通加法未做端回进位 折叠需重复到高 16 位为 0:先 sum[15:0] + sum[31:16],再把结果的第 16 位加回低 16 位;硬件中可用两级折叠组合逻辑实现,见 2.3、5.3 节
8 伪首部协议号字节序错误 UDP/TCP 校验和与参考值不符,且差异随 IP 地址变化 伪首部第 8 字节为 0x00、第 9 字节为协议号(UDP 17 / TCP 6),实现中把两者顺序颠倒或拼成了 0x1100 / 0x0600 伪首部该 16 位字应写作 16'h0011(UDP)或 16'h0006(TCP),即高字节 0x00、低字节协议号,见 3.4、6.2、7.2 节
9 伪首部长度字段取错 校验和与参考值不符,且误差随报文长度增大而更明显 UDP/TCP 伪首部中的长度字段是 UDP/TCP 首部加数据的总长度,误用了 IP 总长度(含 IP 首部) 长度字段取 UDP 长度(首部 8 字节 + 数据)或 TCP 长度(首部 + 数据),不要直接搬 IP 首部的总长度字段,见 6.2、7.2 节
10 字节序与位序混淆 FCS 字段在链路上传输后接收方校验失败,但本地回环测试正常 CRC 计算时每字节按 bit7→bit0 处理,而 FCS 写入帧尾时按低字节在前(小端)排列,两者顺序被混用 计算阶段保持 MSB first,输出阶段按 crc7:0、crc15:8、crc23:16、crc31:24 的顺序依次发送,见 10.4 节
11 奇数长度报文末尾字节处理错误 数据长度为奇数字节时校验和错误,偶数长度正常 反码求和按 16 位字划分,奇数长度时最后一个字节需与 0x00 组成一个字(高字节补 0),实现中直接丢弃或错位拼接 末尾单字节应作为低字节、高字节补 0x00 参与累加;在状态机中增加"剩余字节数"计数,见 2.2、6.1 节
12 接收方向误将校验和字段置 0 接收验证结果恒为 0xFFFF 之外的值,正确帧也被判为错误 发送方向计算时需将校验和字段置 0,但接收方向验证时必须包含收到的校验和字段,两者规则不同 发送与接收使用不同的累加使能逻辑:发送跳过/清零校验和字段,接收完整累加全部字段,见 5.2、6.5 节

上表中的错误大多有一个共同特征:在特定数据下才暴露。例如端回进位折叠不彻底,只有当累加结果恰好产生二次进位时才差 1;UDP 校验和为 0 的情况概率约为 1/65536,随机测试很难覆盖。因此排查时不能只依赖随机激励,应针对每一条构造定向测试向量:

  • 对 CRC 相关错误,用全 0 数据、全 1 数据、单字节数据分别与 zlib.crc32 比对,能快速区分初始值、取反、覆盖范围三类问题。
  • 对反码求和相关错误,构造包含 0xFFFF、0x0000、奇数长度、恰好产生二次进位的报文,逐一验证折叠逻辑。
  • 对伪首部相关错误,固定数据内容只改变源/目的 IP 与长度字段,观察校验和变化是否符合预期。

建议在验证阶段就把这张表转成检查项清单,每实现一个校验和模块就对照勾选一遍,比在整帧联调时才发现问题要高效得多。

相关推荐
小虎AI生活10 小时前
WorkBuddy 自动化工作流实测:从分镜脚本到 MP4 成片的 18 步复盘
aigc·ai编程
国科安芯15 小时前
83 倍:ASC0108S 在推挽和开漏之间,差的不是速度,是回程
单片机·嵌入式硬件·架构·电平转换·抗辐射
kennywu41116 小时前
AI 说“4 个测试全部通过”,其实它一个都没跑
ai编程
金銀銅鐵17 小时前
JDK 21 中 List.of 的实现细节:从 List12 到 ListN
java·后端·ai编程
狼爷17 小时前
别再只刷MMLU了:真正的AI能力评测,早已从“做题”走向“干活” —— 深度解读 Artificial Analysis 智能指数 v5.0 方法论
ai编程
码农小韩18 小时前
FPGA学习(二)——LED闪烁
fpga开发·verilog·fpga·led·组合逻辑·分频设计
国科安芯19 小时前
从遥控到自治:ASP4644S2B支撑星载电源健康管理与容错配电
网络·单片机·嵌入式硬件·安全·dcdc·低轨卫星·商业航天
rhythm-ring19 小时前
《汽车热管理系统:电子水泵单线PWM双向诊断机制》
单片机·嵌入式硬件·汽车
明月_清风19 小时前
Meta 开源了一个 AI Native 组件库:Astryx,150+ React 组件,专为 AI Agent 而设计
前端·人工智能·ai编程