
上个月一位做数据采集的朋友问我他搞了半年FPGA想把板子上采集到的8路ADC数据发到上位机显示听说可以用网口传结果翻了几天资料越看越晕。我特别理解这种状态因为FPGA的网络通信和STM32这类单片机完全不同——单片机调一下库、跑一下lwIP协议栈基本就能收发了而FPGA这边协议栈、MAC、PHY、时序约束每个环节都得自己兜着任何一个细节没接上网线插上去就是一片灰。这一篇是这个系列的第七篇前面的内容里已经聊过Verilog基础语法、组合逻辑、时序逻辑、简单总线、时序约束也折腾过一些常见外设接口。这次要聊的是FPGA开发里非常实用、但第一次上手最容易卡壳的方向网络通信设计。我会按自己实际做项目的路径来写从分层概念、协议拆解到发送接收路径、ARP处理和上板调试尽量做到让没接触过网络协议的FPGA工程师也能照着搭出一条能跑通的千兆UDP通路。1. 先建立整体认知FPGA网络通信的分层与硬件链路1.1 用寄快递的类比理解OSI分层很多初学者拿到网络通信第一反应是去翻TCP/IP协议栈源码然后当场劝退。其实FPGA做网络通信根本不需要把整个协议栈啃下来关键是要搞清楚每一层各自干什么活。可以拿寄快递来类比你要寄一个包裹包裹里面的货物对应应用层数据快递单上写的收件人电话对应传输层的端口号收件人的城市地址对应网络层的IP地址小区门牌号对应数据链路层的MAC地址最后运输用的货车和公路对应物理层。FPGA在这条链路里干的事可以用大白话说它负责把应用数据按规矩装进一个信封以太网帧塞给PHY芯片由PHY芯片把电信号发到网线上反过来从网线上收进来的数据FPGA负责拆信封、检查有没有损坏、把货物取出来交给内部逻辑。面对TCP/IP五层模型通常我们在FPGA里会自己实现数据链路层MAC子层的全部功能然后用轻量逻辑实现网络层的IP封装和传输层的UDP封装。至于TCP那种带连接管理、滑动窗口、重传定时器、拥塞控制的协议虽然也能在FPGA里写但工程复杂度完全不在一个量级后面会细说。1.2 板级硬件链路FPGA、PHY、变压器各管什么先看一条典型的以太网硬件链路FPGAMAC层功能→ RGMII接口 → PHY芯片 → 磁性变压器 → RJ45网口 → 网线这中间每一级都有明确分工PHY芯片负责物理层也就是把数字信号变成网线上的模拟差分信号。它内部有编解码千兆一般用8b/10b编码、时钟恢复、自动协商、链路状态检测等功能。磁性变压器负责隔离直流分量、抑制共模干扰顺便提升一下抗雷击浪涌的能力。它通常集成在RJ45座子里比如HR911105A这类带变压器的网口。RJ45就不用说了物理接口。实际项目里常见的PHY芯片有瑞昱的RTL8211EG、美满的88E1512、Microchip的KSZ9031、国产的YT8512等。这些芯片大部分都支持RGMII接口这是FPGA和PHY之间最主流的连接方式。驱动PHY的方式是通过MDIO接口读写PHY的寄存器比如配置速率、双工模式、PHY地址、回环模式等。1.3 千兆RGMII接口的信号与时序RGMII是Reduced Gigabit Media Independent Interface的缩写它把GMII的8根数据线砍成了4根把时钟从25MHz提高到125MHz并在时钟的上下沿都传输数据这样4根线就能在125MHz下完成千兆速率的数据传输。一个RGMII接口包含这些关键信号信号方向信号名作用MAC→PHYTX_CLK125MHz发送时钟由MAC产生MAC→PHYTXD[3:0]发送数据上升沿送低4位下降沿送高4位MAC→PHYTX_CTL发送控制信号上升沿对应TX_EN下降沿对应TX_EN XOR TX_ERPHY→MACRX_CLK125MHz接收时钟由PHY从网线数据中恢复PHY→MACRXD[3:0]接收数据同样上下沿双采样PHY→MACRX_CTL接收控制信号含义与TX_CTL对应这里有一个经常被忽视的点千兆RGMII下TX_CLK是MAC送给PHY的而不是PHY给MAC的。很多老工程师从百兆MII接口转过来一直习惯“时钟由PHY提供”结果在RGMII上栽了跟头具体方向还是要以自己手里那颗PHY的数据手册为准。正因为这里涉及125MHz时钟和上下沿采样FPGA工程里必须给RGMII的输入输出信号做时序约束比如set_input_delay、set_output_delay否则上板就会随机出现错帧。时序约束这个坎躲不掉但它本质上就是告诉工具“我外部的数据相对于时钟延迟了多少”不用想得太玄。2. 方案定型UDP优先以及系统模块怎么切2.1 UDP与TCP的取舍直接决定工作量FPGA做以太网通信第一件事不是写代码而是先确定要用UDP还是TCP。我的建议是绝大多数场景无脑选UDP除非你的应用对“不能丢包”有硬性要求而且你有足够的资源去处理TCP的复杂度。对比项UDPTCP连接管理无连接直接发三次握手建立连接、四次挥手断开可靠性不保证丢了就丢了序号、确认、重传保证可靠流量控制没有滑动窗口机制拥塞控制没有慢启动、拥塞避免FPGA实现复杂度轻量状态机简单工程量爆炸一般不用纯逻辑实现典型场景实时数据采集、图像传输、工业控制文件传输、需要可靠交互的业务我见过有人非要在纯FPGA里用Verilog写一个完整的TCP服务端最后项目延期了俩月。不是说完全做不到市面上确实有商业IP核能实现但自己从零写一个能抗住真实网络环境的TCP工作量不是“近似0基础”的人该碰的。实际做数据采集、图像传输这类项目UDP丢那么一两包往往无伤大雅或者可以在应用层做简单的确认重发机制。PC端用C#写个UdpClient、用Python写个socket几十行代码就能收数据开发效率非常高。2.2 自研MAC还是直接用IP核定了UDP之后接下来纠结的是MAC层怎么做。这里有两个方向方向一是用FPGA厂商自带的MAC IP核比如Xilinx的Tri-Mode Ethernet MAC、Intel的TSE MAC。这些IP核经过充分验证支持各种标准特性配置起来有图形化界面适合项目工期紧、不想碰底层细节的场合。但问题也很明显使用IP核会被它内部的总线结构、数据位宽、时序要求框住出了问题排查起来反而不直观。方向二是自研一个轻量MAC只实现我们需要的功能组帧、CRC校验、RGMII收发。这个工作量其实没有想象中那么大对于一个面向UDP传输的轻量MAC完整写下来差不多也就几百行状态机代码。而且因为是自己的代码每一帧里哪些字段该填什么、字节怎么对齐心里一清二楚。对这个系列的读者来说我的建议是先自研跑通不要一上来就挂IP核。自研机制的过程就是理解以太网本质的过程。后面如果真要到产品化阶段再换IP核或者换带MAC硬核的SoC芯片心里也有底。2.3 系统模块划分与数据流走向整个UDP通信系统可以简单分成下面几块发送FIFO用户逻辑写进来的待发送数据先缓冲一下。发送状态机从FIFO读数据按以太网帧格式封包计算CRC输出到RGMII。接收状态机从RGMII收数据解出帧头校验CRC把有效载荷写入接收FIFO。ARP模块处理上位机发来的ARP请求回一个ARP应答让上位机能拿到FPGA的MAC地址。MDIO配置模块上电时配置PHY芯片的寄存器。这套模块划分基本是固定范式不管用哪家FPGA逻辑架构都是这个骨架只是RGMII的引脚位置、PHY的寄存器定义略有不同。关于FIFO这里多说一句很多初学者会问用户逻辑和MAC都在同一片FPGA里直接用信号传递不好吗为什么要插一层FIFO因为用户逻辑的工作时钟往往和RGMII的125MHz时钟不是同一个时钟域。比如采集模块可能是100MHz主时钟跑的逻辑和125MHz的RGMII发送时钟是异步关系。如果不加FIFO做跨时钟域缓冲数据在传递过程中就会出现亚稳态问题轻则偶尔错一帧重则整个模块直接跑飞。3. 协议细节不能含糊帧格式、字节序与CRC32的计算逻辑3.1 以太网帧格式逐字段拆解搞清楚了硬件架构和模块划分下一步就到了最关键的地方以太网帧本身。先把标准以太网帧不含VLAN标签的每个字段看清楚。字段长度内容前导码7字节每个字节0x55用来同步时钟帧起始定界符SFD1字节0xD5表示帧头开始目的MAC地址6字节对端网卡的MAC源MAC地址6字节本机FPGA的MAC类型/长度字段2字节0x0800表示上层是IPv4数据载荷46~1500字节实际承载的数据FCS校验字段4字节CRC32校验值有几个细节容易踩坑前导码和SFD总共8字节中间没有空隙。SFD的值0xD5看起来像0x55的变形它的作用就是让接收端知道“接下来是真正的帧头了”。数据载荷字段最少46字节。如果你要发的UDP数据很短比如只有10个字节的有效数据那么整个IP包加UDP头加MAC地址算下来不足60字节时必须在数据后面补零填充到46字节保证整帧不含FCS时最少60字节。这个填充规则很多初学者容易忽略。帧与帧之间还必须有至少12字节的帧间隙IPG否则部分PC网卡会把连续两帧当成一帧处理直接丢包。3.2 字节序是最容易翻车的地方以太网协议里最折磨人的不是复杂的逻辑而是字节顺序。这里需要形成一个条件反射网络传输是“大端模式”先发最高字节但每个字节内部在RGMII物理线上又是“低位先发”。举个例子当你需要发送IPv4类型字段0x0800时在MAC内部如果是8位数据总线那么先发送0x08再发送0x00。这从人眼看起来是正常的。但如果内部使用的是32位总线你写了一个32位的数值0x08004500想去发IP头就必须考虑清楚是高低字节互换还是直接按字节顺序发。再具体到RGMII的4位接口因为RGMII只有4根数据线一字节8位要拆成两次发送时钟上升沿先送低4位下降沿再送高4位。这时候如果你把内部的8位数据和RGMII引脚直接连接不做上下沿的位宽切换出来的字节顺序一定是乱的。所以我在做发送模块时有个习惯不管内部总线多宽先把核心封包逻辑写成“按字节流水输出”的模型每个时钟周期输出一个字节。然后再根据RGMII的DDR要求做4位拆分。这样逻辑清晰也不容易在字节序上出问题。3.3 CRC32算不对就全是坏帧以太网帧末尾的FCS字段是CRC32校验它的计算规则很多教程只给一个多项式但实际写代码时还有三个关键规则漏一个就错一是多项式固定为0x04C11DB7标准CRC32二是计算初值是0xFFFFFFFF不是0三是输入数据的位顺序是反序的也就是每个字节从最低位开始逐位参与计算而且最终得到的32位结果要按位取反还要把位顺序也倒过来才能作为FCS发送出去。这里分享一个我核对CRC实现的方法先用一个已知的标准以太网帧在在线CRC计算工具里算一遍再把自己的Verilog仿真结果和它对比。如果发送的帧是“全0的MAC地址、类型0x0000”那么帧内容的FCS也有已知值可以查。仿真和工具一致了再上板千万别跳步。下面是一个最基础的逐位CRC32计算模块骨架逻辑清晰适合先跑通功能再优化性能module crc32_d1 ( input wire clk, input wire rst_n, input wire crc_en, input wire crc_data, output reg [31:0] crc_reg ); localparam CRC_POLY 32h04C11DB7; always (posedge clk or negedge rst_n) begin if (!rst_n) crc_reg 32hFFFFFFFF; else if (crc_en) begin if (crc_reg[31] ^ crc_data) crc_reg {crc_reg[30:0], 1b0} ^ CRC_POLY; else crc_reg {crc_reg[30:0], 1b0}; end end endmodule这个模块每个时钟周期处理1位数据计算完成后把crc_reg取反并按位反序就是帧尾的FCS。为了提升性能工程上通常会把8位数据并行处理一次性算出一个字节的CRC增量本质上是把多轮移位展开成组合逻辑原理不变只是逻辑数量成倍增长。4. 发送路径状态机设计、FIFO缓冲与RGMII时序生成4.1 从用户数据到网线信号发送侧数据流发送路径的物理顺序已经很清楚了现在要把它写成代码。整体流程可以分成三级第一级是用户逻辑把要发送的数据写入发送FIFO。第二级是发送状态机在检测到FIFO非空后开始组帧输出前导码和SFD再输出目的MAC、源MAC、类型字段然后是IP头、UDP头接着从FIFO里读出用户数据最后补零填充、计算CRC、输出FCS。第三级是RGMII接口逻辑把8位并行数据按上下沿拆成4位连同TX_CTL一起送给PHY。在这个设计里IP头和UDP头可以做成固定值。比如源IP固定为192.168.1.10目的IP固定为192.168.1.2源端口固定为4000目的端口固定为4000。因为UDP是无连接协议只要IP和端口匹配上位机就能收到数据。这样可以省去一整块协议栈逻辑。需要注意UDP长度字段和IP总长度字段不是固定的它们依赖本次发送的实际数据长度。通常做法是发送开始时已知FIFO里有多少数据或者约定每次固定发多少字节。如果数据长度不固定可以把整个帧先缓存起来在发送IP头之前提前算好长度字段或者约定每次发送固定长度比如一律发送512字节没填满的补零这样最省事、逻辑最简单。4.2 发送状态机的核心结构发送状态机可以用一个典型的状态序列来描述localparam S_IDLE 3d0; localparam S_PREAMBLE 3d1; localparam S_HEADER 3d2; localparam S_PAYLOAD 3d3; localparam S_CRC 3d4; localparam S_IFG 3d5;各状态的工作内容如下S_PREAMBLE发送前导码8字节内容是0x55重复7次再跟一个0xD5。S_HEADER依次发送目的MAC6字节、源MAC6字节、类型2字节、IP头20字节、UDP头8字节。这里所有数据可以存在ROM或常量数组里每个时钟周期输出一个字节。S_PAYLOAD从FIFO读出数据并发送同时把读出的每个字节送入CRC计算器计数器控制发送总字节数。S_CRC发送4字节FCS。前面说过CRC结果是32位需要按位取反、反序再按字节顺序逐字节送出。S_IFG发送帧间隙至少12字节的空闲时间然后回到S_IDLE等待下一帧。这里我想强调一个细节CRC计算必须在前导码之后开始并且把SFD也算进去吗标准的计算范围是从目的MAC的第一个字节开始的不包含前导码和SFD。很多第一次实现的人会把前导码也算进去结果CRC必然不对。发送状态机的核心伪代码结构可以参考always (posedge clk) begin case (state) S_IDLE: begin if (fifo_empty 1b0) state S_PREAMBLE; end S_PREAMBLE: begin if (byte_count 7) state S_HEADER; end S_HEADER: begin if (byte_count 41) // 66220842? 自行按实际长度调整 state S_PAYLOAD; end S_PAYLOAD: begin if (byte_count tx_length fifo_empty) state S_CRC; end S_CRC: begin if (byte_count 3) state S_IFG; end S_IFG: begin if (byte_count 11) state S_IDLE; end endcase end每个状态都要有一个字节计数器状态迁移条件就是计数器的值。编写时注意S_HEADER的长度要对别数错字节。我建议把IP头UDP头统一定义成一个常量组写一个注释把每个字节的内容标出来后期改IP、改端口时一目了然。4.3 RGMII输出时序ODDR、TX_CTL与时序约束在Xilinx系列FPGA里RGMII的4位发送数据线需要通过ODDR原语输出这样才能在时钟的上下沿都采到数据。一个典型的ODDR连接方式是把8位发送总线的低4位接到ODDR的D1端高4位接到D2端。OTX_CTL信号同理上升沿输出TX_EN下降沿输出TX_EN异或TX_ER。因为我们的UDP发送是全速正常发送没有流控和错误插入需求所以TX_ER固定为0下降沿时TX_CTL输出的就是TX_EN本身。所以实际操作中往往简化为数据期TX_CTL为高前导码和帧间隙为低。这里还有一个非常容易忽略的问题RTL8211EG这类PHY芯片在千兆模式下对TX_CLK和TXD之间的相对延迟有要求。很多FPGA板卡会在RGMII数据线上做PCB走线等长设计但如果板子设计时没有特意做延迟补偿FPGA需要把TX信号相对时钟做一点相移传统做法是使用OSERDESE2的DDR模式配合IO延迟单元IDELAYE2或者直接在逻辑里通过调整ODDR的时钟相位来实现。时序约束方面RGMII最核心的是对RX_CLK和RXD、RX_CTL之间的set_input_delay以及TX_CLK对TXD输出的set_output_delay。这个我确实不建议逃课Vivado里写好XDC之后跑一下时序收敛检查比瞎猜信号相位要可靠得多。5. 接收路径解帧、校验与数据缓冲的完整逻辑5.1 接收侧状态机与RX_CTL的使用接收方向和发送方向正好相反物理信号从PHY进入FPGA。一般用IDDR原语把RGMII的上下沿数据合并成8位数据再交给接收状态机去解帧。接收状态机的状态设计可以是IDLE → SFD_DETECT → HEADER → DATA → CHECK → DONE。它和发送状态机最大的区别在于发送是自己掌握节拍接收完全依赖RX_CTL信号。RGMII的RX_CTL在数据有效期间为高在空闲和帧间隙期间为低。所以接收状态机的主循环可以设计成在IDLE状态等待RX_CTL拉高一旦拉高开始检测前导码和SFDSFD之后进入HEADER状态解析目的MAC、源MAC、类型字段然后进入DATA状态读取有效载荷当RX_CTL拉低时表示帧结束进入CHECK状态核对CRC。最后一个状态很关键帧结束不代表帧有效。只有收到完整一帧、并且CRC校验通过、长度合法的帧才能把数据写入接收FIFO否则整帧丢弃。5.2 CRC校验与错误帧丢弃接收端的CRC校验有两种常用实现方式我推荐后一种用起来最省事。第一种是在收到完整帧后用本地计算的CRC值逐字节和收到的FCS字段比较一致则有效。第二种是把收到的FCS字段也送入CRC计算器继续算4个字节因为CRC算法的特性如果前面数据正确计算器最终会落在一个固定值上。这样实现上可以少写一个比较寄存器只要在帧结束时检查计算器是否正确落到期望值即可。不过这个期望值各家资料有些微差异第一次做的话我更建议用第一种直接比较的方式逻辑上更直观。另外接收链路里还有两个常见的判断条件目的MAC地址如果不是自己的MAC也不是广播地址直接丢帧以太网类型字段如果不是0x0800说明上层不是IP包也直接丢。这些过滤条件可以在状态机里用几个判断就完成不需要额外逻辑资源。5.3 接收FIFO与如何只提取UDP有效数据接收侧FIFO缓冲的是“剥离掉MAC头、IP头、UDP头之后的有效载荷”不是把整个帧都存起来。这样做的好处是用户逻辑拿到的数据已经是干净的原始数据不用再写解析逻辑。实现思路也不复杂在接收状态机解析SFD之后维护一个字段计数器。当计数值超过14字节的MAC头加20字节IP头加8字节UDP头之后进入数据载荷阶段此时产生FIFO写使能把后续字节写入接收FIFO。不过这里有一个隐含要求IP头长度和UDP头长度对IPv4来说通常是固定的20字节和8字节但IP头里有个IHL字段可以表示其他长度比如带选项的IP包。对轻量UDP链路来说直接约定不支持IP选项、固定IHL5表示20字节就行遇到带选项的包直接当异常丢弃。UDP头本身长度固定8字节校验和可以填0表示不校验这在IPv4里是合法选择能省不少逻辑。还有一点理论上一帧最多可以容纳1500字节的有效数据去掉各种头部UDP载荷上限通常是1472字节。接收FIFO的深度一定要按最大可能帧来设计否则收到大包时FIFO写满剩下的数据只能丢弃会导致IP重组失败或上位机收包不完整。6. ARP应答让上位机找到FPGA的最短路径6.1 为什么必须处理ARP请求很多人把发送和接收状态机写完CtrlS一按就觉得大功告成结果上电一看上位机发UDP包过去FPGA一个字节都没收到。查来查去发现问题出在ARP上。先理清PC发UDP包之前做了什么PC要往192.168.1.10发数据但它只知道目的IP不知道目的MAC。于是PC先在自己的ARP缓存表里查找查不到就向局域网广播一个ARP请求内容是“谁是192.168.1.10请把你的MAC地址告诉我”。如果FPGA不回ARP应答PC会在ARP表里填一个“未完成”状态随后直接丢弃要发送的数据包。更直白地说FPGA不回ARPPC压根就不会把UDP包发到网线上。所以在收UDP之前必须先让MAC层的ARP应答链路通起来。很多教程把ARP留作“扩展内容”但从实战角度看它应该被提到UDP接收之前因为它就是UDP通路的前置条件。6.2 最小ARP应答的实现方式ARP请求包和应答包的格式完全一致区别只在操作码字段1表示请求2表示应答和目标MAC字段是否填写。ARP字段长度请求包内容应答包内容硬件类型2字节0x0001以太网同左协议类型2字节0x0800IPv4同左硬件地址长度1字节6同左协议地址长度1字节4同左操作码2字节0x00010x0002发送方MAC6字节PC的MACFPGA的MAC发送方IP4字节PC的IPFPGA的IP目标MAC6字节全0PC的MAC目标IP4字节FPGA的IPPC的IP最小ARP模块的处理流程可以压缩成三步第一步接收状态机识别出以太网帧类型字段是0x0806判定这是ARP包把后面的ARP字段缓存起来。第二步检查硬件类型、协议类型、操作码是否满足“以太网IPv4请求”。再检查目标IP是不是192.168.1.10是才继续不是直接丢弃。第三步构造应答帧目的MAC填PC的MAC、源MAC填FPGA的MAC、类型填0x0806、操作码填0x0002、目标MAC填PC的MAC、目标IP填PC的IP。然后按普通发送帧的流程发出去。这个模块实际上就是一个特殊的“帧生成器”它比UDP发送模块更简单因为整个应答帧是固定长度的内容来源是收到的PC的MAC和IP不需要FIFO。实现时只要在接收状态机里加一个分支检测到ARP请求就跳转到ARP发送状态发送完成再回到正常接收状态即可。6.3 测试阶段的偷懒技巧静态ARP绑定如果只做纯数据下发测试还有一条极简路径在PC上把FPGA的IP和MAC静态绑定就能跳过FPGA的ARP模块直接向固定MAC发送UDP包。Windows下用管理员权限执行arp -s 192.168.1.10 00-11-22-33-44-55Linux下执行sudo arp -s 192.168.1.10 00:11:22:33:44:55绑定完成之后PC不再发ARP请求UDP包会直接发往绑定的MAC地址。这个方法在调试阶段非常好用可以先把发送接收链路的逻辑问题解决掉最后再回来补全ARP模块。不过我也说不建议测试完就把ARP模块删掉。静态绑定只适合PC固定IP固定MAC的实验室环境换一台电脑或重启后绑定就失效了。真正做一个能给别人用的板卡ARP是必须实现的基础功能。7. 实测记录抓包、排查链路与常见坑的解决思路7.1 搭建一套高效的联调环境写完了代码仿真也过了真正决定项目成败的是上板实测。我先说一套我自己用起来很顺手的联调环境网线上连开发板网口直接一根网线连PC网卡。注意如果是千兆开发板PC网卡也要支持千兆否则会自动降到百兆模式因为RGMII接口用的是千兆模式链路协商不到千兆会出现各种莫名问题。IP规划设置PC网卡IP为192.168.1.2子网掩码255.255.255.0。FPGA端固定IP为192.168.1.10MAC地址可以自己定义一个本地地址比如00:11:22:33:44:55只要不和局域网内其他设备冲突就行。抓包工具PC上开Wireshark抓包过滤条件直接写udp port 4000或者arp。如果FPGA发出的帧格式有问题Wireshark会直接给出标记比如“Invalid frame”或者“FCS incorrect”这比看FPGA内部信号直观多了。内部观察Vivado的ILA逻辑分析仪是FPGA调试的另一个眼睛。把抓RGMII的RXD、RX_CTL、RX_CLK以及接收状态机的状态寄存器全部探出来触发条件设为收到SFD接收状态机从IDLE跳出采样深度开到1024就够看一帧了。ILA采IP层信号和老式逻辑分析仪最大的区别是可以即时在线观测不用反复改动再综合。7.2 常见问题清单与定位方法联调阶段踩过的坑汇总一下基本是这几类现象可能原因定位手段Link灯不亮PHY未配置成功、PHY地址不对、网络变压器虚焊、网线本身问题查看PHY寄存器0/1检查MDIO配置时序先回环测试抓到乱帧、长度怪CRC计算错误、字节序错误、前导码与SFD没对齐Wireshark看帧内容对比各字段十六进制值能发出去但上位机收不到目的IP/端口不匹配、ARP没回应、路由不对查Wireshark中目标IP是否为PC地址查ARP缓存能收到UDP请求但无应答发送侧状态机长度计数错误、CRC错误导致网卡丢帧用ILA单帧触发逐步看发送状态和FIFO数据收包偶尔丢包FIFO溢出、帧间隙不足、接收侧在CRC阶段处理太慢观察FIFO满信号增加深度查看状态机吞吐量接收数据全乱IDDR采样时序不对、RX_CTL采样错误导致多采或少采一个字节查看RX_CTL和RXD的时序关系调整IDELAY参数最常见的原因集中在两类一类是CRC相关另一类是收发时序未对齐。CRC相关问题的定位方法很直接Wireshark的“帧校验序列”列会显示correct或incorrect如果显示incorrect先把抓到的帧导出十六进制手工用在线CRC工具验一遍发送端算出的值就能区分是算法问题还是字节序问题。时序未对齐的典型症状是第一个字段MAC地址总是对不上比如发送方明明写了00:11:22:33:44:55Wireshark里看到的是22:33:44:55:00:11这说明数据在字节切分那里错位了问题多半出在RGMII的上下沿拆包或者字节计数器上而不是CRC。7.3 回环测试是收敛问题最快的手段如果上板第一步就发现链路不通不要急着查自己的MAC逻辑先确认PHY本身工作是否正常。几乎所有千兆PHY芯片都支持PHY内部回环模式。通过MDIO写PHY的控制寄存器把PHY配置为发送数据被PHY回环回接收方向不做线路传输。这个模式下FPGA发出的帧不经过网线就能被PHY送回FPGA的接收端。如果回环模式下FPGA能收能发说明FPGA到PHY之间的RGMII链路没问题剩下要查的就是PHY到网线、网线到PC这一段。回环测试也是验证链路层逻辑的利器在FPGA里发一个固定内容的帧同时观察接收端能收到一模一样的帧说明本地MAC、PHY接口、内部FIFO链路已经通了。此时再把回环关掉插上网线焦点就集中在PHY自动协商和ARP这些外围问题上。7.4 调试期间的两个实用小经验说两个我每次调网络都用的方法简单但效果显著。第一个是固定内容重复发包。发送模块里先写死一段可辨认的递增数据比如从0x00递增到0x3F再循环然后用上位机或Wireshark看收包内容。这样无论哪一段数据不对一眼就能看出来是第几个字节开始错的进而映射到状态机的哪个状态出了问题。第二个是先用最简条件排除干扰。调试初期把ARP模块临时屏蔽用静态ARP绑定替代把PC防火墙关掉把网卡的“IPv6协议”临时禁用。很多莫名其妙的问题最后发现不是FPGA的问题而是PC端网络协议栈在作怪。这套联调走完之后再回头看项目最初那些让自己焦虑的问题时序约束怎么写、CRC怎么算、FIFO怎么跨时钟域、状态机怎么划分心里都有一套非常清晰的答案了。FPGA网络通信说穿了就是“物理层链路打通MAC层装对帧应用层定时定量发数据”每一部分都是可以独立验证、独立排查的。先把这条链路跑通后面的TCP、VLAN、多端口、流量控制都是在这个骨架上逐步加肉的事。