1. 为什么大家都卡在“千兆以太网”这一步
FPGA 开发做到一定阶段,你会发现十个项目里有七八个绕不开网络通信。无论是做高速 ADC 采样后的数据上传、图像采集与实时处理,还是搭建一个自定义协议的数据通路,最终都要靠以太网把数据从板卡上弄出去。千兆以太网这个坎,早过晚过都得过。
这个实验是我在 FPGA 学习路上投入时间最久、踩坑最多,也收获最大的一块内容。它不像流水灯、串口收发那样只要对着时序图写几个状态机就能通,它牵扯到物理层芯片配置、RGMII 接口时序、MAC 帧封装、IP/UDP 协议栈裁剪、CRC 校验、跨时钟域处理,任何一个环节出问题,表现出来都是同样的症状——抓包抓不到,ping 不通,速度上不去。而这恰恰是真实工程项目中排障难度最大的那一类问题。
所以这篇内容主要面向两类人:一是正在做 FPGA 入门到进阶过渡、被 RGMII 时序折腾得头疼的学习者;二是想在板卡上快速跑通一个能用的千兆 UDP 通路、为图像/ADC 数据搬运打基础的开发者。我会按自己实际做过的完整流程,从选型、原理、写码、仿真到上板抓包,把每一步怎么想、怎么写、怎么查,全部分享出来。
2. 整体方案设计:先想清楚你要“干掉”的是哪一层
2.1 先问自己:是“通”就行,还是要“快”?
做千兆以太网之前,我建议你先想明白一个问题:你的目标是把一个 UDP 包从板卡发到 PC 上,还是要把某路数据以线速持续不断地灌进电脑?
这两种需求对应的工程量差异很大。前者你只需要实现一个简单的 MAC + UDP 发送通路,时钟跑通、数据能发出去、PC 端能收到即可,板卡资源占用可能不到 5%。后者则要考虑数据缓冲、突发处理、流控、CRC 计算吞吐率、MAC 层回环效率,甚至要把 DMA 引擎搬进来,让 CPU 或者逻辑直接从 DDR 里搬数据往以太网口上塞。
我在做这个实验时的目标定为“通且快”——先保证链路能通,再逐步优化吞吐。整套方案选用了 UDP 协议而不是 TCP,原因很直接:UDP 协议栈在 FPGA 里实现简单得多,无需维护连接状态、滑动窗口、重传机制,只需把 IP 头和 UDP 头按规范拼好、校验算对就能跑。对于图像传输、ADC 采样数据上传这类丢几包可以接受的场景,UDP 是性价比最高的选择。
提示:如果你后续要考虑“可靠传输”,也建议先从 UDP 通路打通开始,再在应用层加重传逻辑。直接在 FPGA 里写 TCP 状态机,调试成本会翻好几倍。
2.2 开发板与芯片选型:Artix-7 和 Cyclone 系是主流
千兆以太网实验对 FPGA 本身的资源要求不高,但对外部 PHY 芯片和接口定义有要求。我使用的是黑金系列的开发板,主控芯片为 Xilinx Artix-7 系列(具体型号为 AX7A035),PHY 芯片为 Realtek 的 RTL8211E,通过 RGMII 接口与 FPGA 相连。
选 Artix-7 的原因很实在:片上资源足够跑一个 MicroBlaze 软核加各种外设,同时价格也不算离谱,社区资料丰富,遇到问题几乎都能搜到答案。如果你用的是 Cyclone IV 或 Cyclone V 系列也完全没问题,千兆以太网这部分逻辑本身与芯片厂商无关,只是例化 PLL、IDDR、OSERDES 等原语时写法略有差异。
关于 PHY 芯片,我强烈建议你在做实验前先确认清楚板子上用的型号。我最初以为板载 PHY 是千兆的,结果调试了很久速率上不去,后来才确认芯片是 10/100M 的。这个坑浪费了小一周时间。RTL8211E、88E1512、YT8531 这些是千兆板卡常见的 PHY,拿到板子先查数据手册,确认支持 1000BASE-T。
2.3 时钟架构:千兆为什么是 125MHz 和 2.5MHz 一起出现
千兆以太网基于 RGMII 接口时,时钟频率是 125MHz,数据在上升沿和下降沿各采一次,也就是 DDR 模式。这意味着每个时钟周期能传 2 bit,8 根数据线加起来正好是 1000Mbps。而 100M 模式下时钟是 25MHz,10M 模式下是 2.5MHz。
很多初学者在这里会犯迷糊:为什么 PHY 芯片配置成千兆模式后,FPGA 内部逻辑频率要跑到 125MHz?因为 RGMII 的发送时钟 TXC 就是 125MHz,数据线 TXD[3:0] 与 TX_CTL 都在 TXC 的上下沿变化,FPGA 侧需要用 IDDR 原语把 DDR 数据转成单沿数据,处理频率自然就是 125MHz。
我在实验中采用了两套时钟方案:
- 125MHz 主时钟:由板载晶体提供,连接到 PHY 芯片的时钟输入,PHY 内部 PLL 锁定后输出给 FPGA;
- 用户逻辑时钟:由 MMCM/PLL 从 125MHz 分频/倍频出 125MHz,用于 MAC 层及上层协议逻辑。
注意:FPGA 内部的逻辑设计尽量不要直接使用 PHY 芯片输出的 RX 时钟作为全局时钟,因为跨时钟域和路径延迟不可控。正确做法是让 RX 时钟只驱动 IDDR 采样的那一段逻辑,采样后的数据再用异步 FIFO 同步到全局时钟域。
3. RGMII 接口与 MAC 层实现:从引脚到帧的完整链路
3.1 一分钟看懂 RGMII:4 根数据线怎么传 8 bit
RGMII 是 Reduced Gigabit Media Independent Interface 的缩写,把传统 GMII 的 8 根数据线、2 根控制线压缩成了 4 根数据线加 1 根控制线,靠时钟上下沿复用。具体来说:
- TXD[3:0]:发送数据,上升沿发送低 4 位,下降沿发送高 4 位;
- TX_CTL:发送控制信号,上升沿表示 TX_EN,下降沿表示 TX_EN 异或 TX_ER;
- TXC:发送时钟,频率 125MHz,DDR 模式;
- RXD[3:0]、RX_CTL、RXC:接收侧对应信号。
这个设计最大的优点就是引脚少,但代价是时序收敛难度增加。尤其是数据线相对于时钟线的延迟关系必须处理好,这直接决定了你能不能稳定收发。
在 FPGA 内部处理 RGMII 数据,必须使用 IDDR 原语把 DDR 数据转成两个单沿数据。Xilinx 7 系列代码如下:
// RGMII 接收侧:IDDR 采样 wire rx_clk; // PHY 输出 125MHz RXC wire [3:0] rx_data; wire rx_ctl; reg [3:0] rx_data_l; reg [3:0] rx_data_h; reg rx_ctl_l; reg rx_ctl_h; always @(posedge rx_clk) begin rx_data_l <= rx_data; rx_ctl_l <= rx_ctl; end always @(negedge rx_clk) begin rx_data_h <= rx_data; rx_ctl_h <= rx_ctl; end wire [7:0] rx_data_8b = {rx_data_h, rx_data_l}; wire rx_valid = rx_ctl_h & rx_ctl_l; // 正常数据时 TX_EN异或0 = 1这个方案的优点是逻辑简单,在主频不高的 FPGA 里完全够用。如果需要更严格的时序约束,可以用 Xilinx 的 IDDR 原语并把时钟相位调整 90 度,确保数据稳定采样。但从实测来看,如果布线不是特别差,上面的代码在 Artix-7 上稳定跑是没问题的。
3.2 为什么数据要“错相”接收:RGMII 的时序坑
这里有一个非常关键、也是无数人踩坑的点:RGMII 规范里,数据相对于时钟是有偏斜的。
发送侧要求 FPGA 在发送数据时把数据延迟约 2ns 再输出,这样接收端(PHY 芯片)在时钟边沿采样时数据已经稳定。接收侧则反过来,PHY 芯片输出数据和时钟时,数据比时钟提前约 1.5~2ns,FPGA 如果直接在时钟边沿采样,很可能会采到数据翻转的瞬间。
解决方法通常有两种:
- 在 FPGA 内部对 RXC 时钟加 90 度相移再采样;
- 对 RX 数据加约束延迟,让数据对齐到采样窗口中央。
我实测下来最稳妥的是第一种:用 PLL 把 RXC 做 90 度相移,相移后的时钟驱动 IDDR 采样。这样数据和时钟的建立保持时间余量会非常充裕。Xilinx 下可以直接用 IDELAY 或 MMCM 的 CLKOUT 相位配置做到,Altera 系可以在 PLL 设置里把时钟相移设为 90 度。
注意:很多 PHY 芯片自带内部延迟模式,比如 RTL8211E 可以通过寄存器配置开启 RX/TX delay,如果板子的硬件设计已经开启了内部延迟,FPGA 端再做 90 度相移会导致时序错乱。调试时先看 PHY 的寄存器状态,再决定要不要软件补偿。
3.3 MAC 发送状态机:从 FIFO 数据到以太网帧
MAC 层是整个链路里逻辑最重、也最容易出差错的部分。我采用的架构是:上层逻辑把待发送数据写入发送 FIFO,MAC 发送状态机从 FIFO 读出数据,按以太网帧格式逐字节输出。
一个标准的以太网帧结构如下:
- 前导码:7 字节 0x55,用于物理层同步;
- 帧起始定界符:1 字节 0xD5;
- 目的 MAC 地址:6 字节;
- 源 MAC 地址:6 字节;
- 长度/类型字段:2 字节,IPv4 时为 0x0800;
- 数据载荷:46~1500 字节;
- FCS 校验:4 字节 CRC32。
MAC 发送状态机状态划分如下:
- IDLE:等待发送请求;
- PRE:发送前导码和帧起始定界符;
- MAC_HEAD:发送目的/源 MAC 地址和类型字段;
- DATA:从 FIFO 读取数据并逐字节发送;
- PAD:如果数据不足 46 字节,补零;
- CRC:发送 CRC32 校验值;
- DONE:拉高发送完成信号,返回 IDLE。
代码框架:
// 简化版 MAC 发送状态机 parameter IDLE = 4'd0; parameter PRE = 4'd1; parameter MAC = 4'd2; parameter DATA = 4'd3; parameter PAD = 4'd4; parameter CRC = 4'd5; parameter DONE = 4'd6; reg [3:0] state; reg [7:0] tx_byte; reg [15:0] byte_cnt; always @(posedge tx_clk or posedge rst) begin if (rst) begin state <= IDLE; tx_byte <= 8'h00; byte_cnt <= 16'd0; end else begin case (state) IDLE: if (fifo_empty == 1'b0) state <= PRE; PRE: begin if (byte_cnt < 7) tx_byte <= 8'h55; else if (byte_cnt == 7) tx_byte <= 8'hD5; else begin tx_byte <= dst_mac[47:40]; // 首个 MAC 字节 state <= MAC; end byte_cnt <= byte_cnt + 1; end // ... 后续状态类似 endcase end end这里有个细节:CRC32 的计算是 4 字节为单位的,而且以太网 FCS 计算覆盖的是目的 MAC 地址到数据载荷(不含前导码和帧起始定界符),所以 CRC 计算模块要与发送状态机同步启停。很多初学者把前导码也计算进去了,导致 PC 端网卡直接丢弃帧。
4. UDP/IP 协议栈裁剪:FPGA 里不跑 Linux 也照样能发包
4.1 为什么我推荐 UDP 而不是 TCP
前面已经提过,我做的是图像数据高速上传的场景,UDP 是首选。理由再展开一下:
- TCP 有连接管理、序号确认、窗口更新、拥塞控制,这些状态机全部塞进 FPGA 逻辑里,资源开销大、时序收敛难;
- UDP 无状态,只需构建包头,数据直接塞进去发即可;
- 对于局域网点对点传输,UDP 的丢包率在千兆网线下很低,实测 1 米网线丢包率几乎为 0;
- 如果应用层需要可靠传输,可以自行加序列号、重传机制,控制灵活。
这个取舍和做嵌入式 Linux 网络编程完全相反,但在 FPGA 资源受限的场景下非常实用。
4.2 ARP 请求处理:别让它成为“ping 不通”的元凶
在能发 UDP 包之前,PC 和 FPGA 得先互相知道对方的 MAC 地址。PC 在发包前会先发 ARP 请求询问“192.168.1.10 的 MAC 地址是多少”,如果 FPGA 不回应 ARP,PC 永远不知道 FPGA 的 MAC,后续 UDP 包根本不会发出来。
所以一个能联通的 UDP 通路,至少要包含 ARP 响应模块。流程是:
- 接收逻辑解析出目的 MAC 为广播地址(FF:FF:FF:FF:FF:FF)、协议类型为 0x0806、操作码为 1(ARP 请求)的帧;
- 提取发送方 IP 和 MAC,构造 ARP 应答帧;
- 应答帧的目的 MAC 填入请求方 MAC,源 MAC 填本机 MAC,操作码填 2;
- 将应答帧交给 MAC 发送模块发出。
这个模块调试时可以用 PC 端 ping FPGA 的 IP 来验证。如果 ARP 模块没写好,ping 的表现就是“请求超时”,但 Wireshark 里能看到 PC 发了大量 ARP 请求而 FPGA 无响应。
4.3 IP 头与 UDP 头的构建:校验和是个容易忽略的细节
IP 头共 20 字节,需要填写版本号(4)、头长度(5)、总长度、标识、生存时间(64)、协议号(UDP 为 17)、源/目的 IP,以及首部校验和。
UDP 头共 8 字节,包含源/目的端口、长度和校验和。UDP 校验和是可选的,PC 端默认会校验,所以一定要算对。UDP 校验和的计算范围包括:伪 IP 头(源 IP、目的 IP、协议号、UDP 长度)+ UDP 头 + 数据,按 16 位求和取反。
这里我给一个建议:在 FPGA 里做校验和,不要在每一个包发送时用循环累加,那样会在长包传输时导致吞吐量骤降。正确做法是边发送边累加,发完数据后补上计算结果。对于一个 1400 字节的包,边发边算和发完再回头算,速度差异能到 3 倍以上。
IP 首部校验和也一样,只在 IP 头 20 字节内计算,数据不参与。
生成 UDP 包的 Verilog 逻辑片段:
// UDP 头构建:假设发往 192.168.1.100:8080 // 源 IP 192.168.1.10,源端口 9000 reg [7:0] udp_tx_data; // 待发送字节流 // UDP 长度 = 8 + 数据长度 wire [15:0] udp_length = 16'd8 + tx_payload_len; // 发送序列:IP头(20B) + UDP头(8B) + payload实测中我把 IP 头的总长度字段同 UDP 长度联动,如果忘了更新这两个长度字段,Wireshark 抓到包后会标记“短帧”或“长度异常”,网卡可能直接丢弃。
5. 实操记录:从 QSPI 固化到 Wireshark 抓包全流程
5.1 步骤一:PHY 芯片初始化配置
上板调试之前,首先确认 PHY 的复位引脚拉高、时钟输入正常,然后是 MDIO 接口配置。我用的 RTL8211E,通过 MDIO 总线读写寄存器:
- 寄存器 0:基本控制寄存器,bit 6 为全双工使能,bit 13 为速率选择(1000M 时配置为 0b11);
- 寄存器 4:千兆控制寄存器,bit 8 为 1000M 全双工使能;
- 寄存器 0x1F:芯片特定配置寄存器,可能包含 RGMII delay 配置。
MDIO 驱动时序很简单:MDC 时钟 2.5MHz,MDIO 数据在上升沿采样。状态机按协议发前导码、操作码、寄存器地址和写数据即可。如果不想自己写,直接用厂商提供的 PHY 配置参考代码也可以,但强烈建议你至少读懂配置流程,出了问题才知道查哪。
5.2 步骤二:先把“回环”弄通
写完整的上层协议栈之前,先做一个最简单的测试:把 RGMII 接收的数据原样送回发送端。也就是 PHY 接收什么,MAC 就发什么。
这样做的意义在于:只需要验证时钟采样是否正确、IDDR 数据拼接方向对不对、PHY 是否有数据输出,就能快速定位是物理层问题还是上层逻辑问题。
我当时是让 FPGA 处于PHY 芯片内部回环模式(通过 PHY 寄存器配置开启),数据从 MAC 发出去,PHY 自动绕回接收侧。如果回环模式收发一致,说明 FPGA 与 PHY 之间接口时序没有问题,问题大概率在上层协议栈。
5.3 步骤三:PC 与 FPGA 直连,开始抓包
确认回环通过后,直接拿网线把 FPGA 开发板和 PC 连起来(不需要交换机)。设置 PC 网卡 IP 为 192.168.1.100,FPGA 静态 IP 为 192.168.1.10,然后 Wireshark 监听网卡。
做这个实验时,我发现一个非常关键的技巧:Wireshark 抓包时一定要关掉 PC 端防火墙,否则 ARP 回应到了网卡也被系统拦截,表现为“收到但无响应”。
从 FPGA 发 UDP 包到 PC,PC 端 Wireshark 显示该包;从 PC ping FPGA 的 IP,Wireshark 显示 ARP 请求和应答,这条通路就算打通了。
5.4 步骤四:用 ILA 查内部信号,不要对着网线发呆
如果 Wireshark 什么都抓不到,立即把 ILA 逻辑分析仪核加进去,抓 FPGA 内部的关键信号。我会把以下信号全部拉出来:
- PHY 输出 RXC 是否有 125MHz 时钟;
- RX_CTL 是否有有效电平;
- 解出来的 rx_data_8b 是否是前导码 0x55;
- MAC 状态机停在哪个状态;
- 发送 FIFO 是否为空/满。
大多数情况下,问题在前三个信号。如果 PHY 输出时钟都没有,检查晶振、PHY 复位;如果 RX_CTL 拉不高,检查网线质量、对端是否正常发包;如果 rx_data_8b 不是 0x55,检查 PHY 是否配成千兆模式或者数据位是否颠倒了。
这一步做好,比对着 Wireshark 盲猜快得多。我从“Wireshark 抓不到包”到“定位是 PHY 配置问题”,用 ILA 只花了 10 分钟。
6. 吞吐量优化与难题排查:从 100M 跑到接近线速
6.1 为什么实测只有 100Mbps:烧掉的那一晚上
第一次把数据通路调通后,我用 iperf 测吞吐量,结果稳定在 100Mbps 左右,死活上不去。检查了 PHY 寄存器、配置了千兆模式,甚至换了网线、换了 PC 网卡,依然纹丝不动。
后来发现是 MAC 发送模块的设计问题:每次发送完一帧后,状态机会等 12 个时钟的帧间隙(IFG)再检查发送 FIFO 是否有新数据。看起来完全符合以太网规范,但问题在于 MAC 控制信号与 FIFO 读请求之间多插了几个周期的延迟,导致实际帧间隙被拉长了几百纳秒。对于 1000M 以太网,每增加一个周期,就会损失一小块带宽。
优化方案是两个:
- 状态机在 DATA 状态下拉高 FIFO 读请求,而不是在每个状态切换时重新发起读操作;
- 预读策略:在上一帧的 CRC 发送阶段,提前检查 FIFO 并准备好下一帧的状态,CRC 发完即可无缝衔接。
改完这两个点,吞吐量从 100Mbps 直接跳到 850Mbps 以上。如果你的吞吐量卡在一个看起来“神秘”的值,先检查帧间隙、CRC 计算和数据通路的停顿点。
6.2 调优顺序:时钟优先,逻辑其次,最后才看 PHY
我在多次排障后总结出一个调优顺序,分享给你:
- 第一优先级:时钟。125MHz 是否稳定、RXC 相移是否正确、数据采样窗口是否够宽;
- 第二优先级:帧格式。前导码、MAC 地址、类型字段、长度字段、CRC 有没有错;
- 第三优先级:状态机吞吐。有没有不必要的等待周期、FIFO 读写是否匹配、信号有没有跨时钟域的亚稳态风险;
- 第四优先级:PHY 芯片寄存器配置。速率、双工模式、延迟补偿。
大部分“千兆不通”的问题,80% 出在前两级;真正 PHY 配置错误的反而很少。不要一开始就怀疑 PHY,先从自己的逻辑找问题。
6.3 板级调试速查表:常见症状与对应排查点
我整理了一张速查表,是我自己反复用过的:
| 症状 | 最容易忽略的原因 | 排查动作 |
|---|---|---|
| ping 不通 | ARP 模块未响应 | ILA 抓 ARP 请求帧是否被正确解析 |
| Wireshark 看不到任何包 | 网卡防火墙拦截 | 关闭防火墙后重试 |
| 能收到包但长度不对 | IP/UDP 长度字段未更新 | 检查长度点,与 FIFO 计数对比 |
| 吞吐量固定在 100M | MAC 状态机空闲间隙过长 | 检查帧间隙计时逻辑 |
| 数据错位或乱序 | IDDR 高低位拼接方向反了 | 对比前导码 0x55 与 0xD5 顺序 |
| 偶发性丢包 | 跨时钟域亚稳态 | 检查 FIFO 读写时钟是否正确 |
提示:如果 PC 端显示“网络电缆被拔出”,大部分时候是 PHY 芯片没初始化好。检查 PHY 复位时序、VDDIO 电压、MDIO 是否配置成功。
7. 后续扩展方向与经验总结
千兆以太网通路打通后,可以扩展的方向非常多。我自己接下来做的是把图像采集模块和 DMA 引擎接进来,让数据从 DDR 直接搬到以太网 MAC 发送 FIFO,形成一条完整的图像传输链路。这个方向如果你的项目需要高速搬运数据,是绕不开的一步。
另外,很多做高速 ADC 采集的朋友,也可以用类似架构:ADC 采样数据经跨时钟域同步后写入 DDR 环形缓冲,再按帧打包通过 UDP 上传,和我的方案在整体架构上是完全一致的。区别只在于上游数据的类型和速率。
这里也分享两个个人体会,算是我踩过最多的坑:
第一个是不要迷信网上的“直接用”代码。FPGA 开发中,即使代码功能完全一致,在不同板卡上由于引脚分配、时钟频率、PHY 芯片型号不同,表现可能完全不同。参考别人的代码可以,但一定要一条条对照自己的数据手册和原理图。
第二个是先把 PHY 数据手册读三遍再动手。RGMII 延迟补偿方式在不同 PHY 芯片上有差异,有的芯片是内部固定延迟,有的需要通过寄存器调整。一个寄存器位搞错,整个链路就是不通,而且这种问题盲调非常费时间。
如果这个实验之后你还想做更深入的折腾,我建议顺序是:先实现 ARP + ICMP 应答(让 ping 通),再做 UDP 发送,再实现 UDP 接收和回环,最后做 DMA 集成。每一步都有独立的验证方法,不要一口气全写完再上板,那样出了问题都不知道从哪查起。
FPGA 千兆以太网这条路,说难不难,说简单也绝不简单。关键在于把每一层拆开来看,逐层验证,逐步逼近。希望这份完整记录能让你少走几个弯,早点看到自己板卡上飞起来的数据包。