很多朋友把LED、按键、UART、SPI都跑通之后看着开发板上那个千兆网口心里都会痒这东西到底能不能用FPGA做我的答案是能而且没那么玄乎。FPGA做网络通信本质上就两件事跟PHY芯片把RGMII时序谈拢再用状态机把ARP、ICMP、UDP这几层协议串起来。这篇是《从近似0基础开始FPGA开发》系列的第7篇默认你已经有Verilog基础、会看Vivado波形、知道FIFO怎么例化。我会围绕一块市面上最常见的Artix-7开发板讲我手头是黑金AX7103网口PHY是88E1512工程基于Vivado 2020.2从PHY初始化一路讲到上板后PC能ping通、能收发UDP数据。1. 想玩网络通信先看清FPGA站在整个链路哪一层1.1 一条以太网帧从PC到FPGA要走多少步很多初学者把“FPGA网络通信”想成一个黑盒子觉得要么很底层、要么需要抄一堆IP核。其实以太网是一个分层协作的系统每一层只处理自己该处理的事。PC端发出的数据经过应用层、TCP/UDP层、IP层、MAC层最终在PHY物理层收发器里变成差分信号送到网线。FPGA这边正好反过来PHY芯片先把差分信号还原成并行数据FPGA内部再逐层剥掉以太网头、IP头、UDP头最后拿到真正的有效载荷。所以FPGA并不是直接去操作网线上的模拟信号那些编码、时钟恢复、线路驱动工作全由PHY芯片完成。FPGA和PHY之间通过一个标准接口对接最常见的就是RGMIIReduced Gigabit Media Independent Interface这也是本篇的重点。FPGA真正要做的是把MAC层的逻辑用状态机实现组装以太网帧、解析以太网帧、处理ARP、响应ICMP、收发UDP包。明白了这个分工你就能理解为什么FPGA做网络通信没有想象中难——因为协议逻辑都是数字电路状态机写清楚就行。1.2 平台选型为什么我建议用GMII/RGMII硬干做FPGA以太网通信方案不止一种。用W5500这类集成MACPHY的芯片通过SPI操作很方便但它本质是把网络协议的脏活累活都封装好了你学不到FPGA内部怎么处理时序而且吞吐率上限就在那里不适合高速场景。用Zynq的PS端GEM控制器也是一个选择但那是ARM在干活纯FPGA逻辑的戏份变少。用Xilinx官方的三速以太网MAC IP核当然最省事但问题是对0基础起步的人来说IP核配置界面里一堆概念本来就够劝退调出问题还不好定位它把该学的底层细节都藏起来了。我建议在入门阶段用RGMII加自研协议栈的方案不是因为它简单而是因为它把以太网的关键知识全部暴露在你面前PCS/PMA是PHY芯片的事但你得理解RGMII的DDR采样MAC封装是FPGA的事你就得亲手拼CRC和前导码ARP和UDP的状态机也得自己写。这套走通之后再用任何现成IP核都是降维打击。硬件上就用常规开发板自带的千兆网口PHY型号常见的是88E1512、RTL8211FD、YT8531它们的RGMII接口逻辑大同小异寄存器细节查数据手册即可。2. PHY芯片与RGMII接口先把第一层握手打通2.1 RGMII的DDR时序到底怎么回事RGMII在千兆模式下有8根数据线加上时钟和控制线TXD[3:0]和RXD[3:0]各4位但时钟是125MHz数据在时钟上升沿和下降沿都有效所以一个时钟周期能传8位正好凑成千兆。TX_CTL和RX_CTL在上升沿表示TX_EN/RX_DV下降沿表示TX_ER/RX_ER。我用一个比较土的类比来记RGMII像一趟双向两车道的县道数据不是一辆车占一整条路而是同一时刻上下行各有一辆车但每辆车都可以“侧挂”半个方向的信息。这个DDR特性是RGMII真正麻烦的地方。FPGA内部逻辑一般是单沿时钟想要处理RGMII要么用IDDR原语把上下沿数据拆成两份要么直接让RXC时钟通过BUFIO进IOB逻辑。发送方向也一样ODDR原语把两个bit合并到一根线上分别在时钟上下沿送出。很多朋友第一次写RGMII代码以为像SPI那样在某个时钟沿采样就行结果上板后要么一直收不到正确帧要么CRC疯狂报错根源往往就是DDR采样没做对。2.2 MDIO配置与PHY上电复位流程PHY芯片除了数据接口还有一根管理接口叫MDIO两根线MDC时钟和MDIO数据。通过MDIO可以读PHY的状态寄存器也能配置工作模式。初次上板我建议先干一件事把MDIO时序写对然后反复读寄存器0x01BMSR确认bit2的Link Status为1再读速度协商结果寄存器确认协商成千兆全双工。如果MDIO连不上后面全是空中楼阁。MDIO写起来有几点容易翻车。首先MDC频率不要太高标准里允许最高25MHz很多人图省事把系统时钟直接接上去结果PHY不回应我习惯用一个分频时钟把MDC降到2.5MHz左右稳定第一。其次MDIO的读写操作需要严格按照帧格式来32位前导码、起始码、操作码、PHY地址、寄存器地址等一位都不能错。最后大部分PHY的地址由硬件引脚决定常见值是0x00或0x01具体看开发板原理图别想当然。上电复位时序也值得一提。我用的88E1512要求复位引脚保持低电平至少10ms释放后PHY还要几百毫秒到一两秒做自动协商。有些人复位信号只拉低几十微秒就释放PHY根本没起来MDIO当然读不到数据。建议在上电后用一个计数器延时至少50ms再释放RST_N然后每隔一段时间轮询Link Status确认link up后再跑后续协议逻辑。2.3 不亮link、状态寄存器读不到怎么排查我遇到过好几次“MDIO读回来全是F”的情况最后定位原因五花八门。最常见的是PHY的复位引脚没接对默认被拉低导致PHY一直处于复位状态其次是MDIO的上拉电阻缺失数据线不能正确回读还有电源时序问题PHY核心电压起来太慢MDIO能写但读不稳定。这类问题用波形分析仪或者ILA抓MDIO线上的波形可以很快定位如果没有仪器就在FPGA侧把MDIO读回来的原始bit流打出来对照协议帧格式一个一个看比瞎猜高效得多。Link不亮也有几个高频原因网线插错口或者对方设备没开这类人为因素要排除PHY的LED引脚配置不同有些板子的LED不亮不代表没link最好通过寄存器判断交换机和PC网口只支持百兆而你把RGMII强制配成千兆自然协商失败。调试时给PC网卡设置固定千兆全双工是常见手段但也别忽略检查网线是否是千兆八芯线四芯百兆线在千兆模式下是肯定协商不起来的。3. 最小协议栈怎么设计ARP、ICMP、UDP一个都不能少3.1 为什么先做UDP而不碰TCPFPGA上实现TCP不是不行但对入门阶段来说性价比太低。TCP为了保证可靠传输要维护序列号、确认号、滑动窗口、超时重传、拥塞控制这些逻辑在状态机里写起来又长又绕而且很多行为要等到真实网络环境下才能验证调试成本高。UDP就简单得多无连接、无确认、没有重传你只需要把数据塞进UDP头、外面套IP头、再套以太网头发出去就完事。对数据采集、图像传输、工业控制这类“丢一帧重发一帧”或“丢包可容忍”的场景UDP已经足够。不要觉得用UDP就是“低端方案”实际上很多高速数据采集设备在FPGA和上位机之间走的就是UDP关键是设计好帧协议和丢包检测机制。入门阶段先跑通UDP后面遇到更高要求再研究TCP或者用Zynq的PS端协议栈都可以。3.2 帧结构、CRC32和帧间隙这三个细节决定你能否被交换机认可以太网帧发送顺序是7字节前导码0x55、1字节帧起始定界符0xD5然后才是目的MAC6字节、源MAC6字节、以太类型2字节ARP是0x0806IPv4是0x0800后面跟净荷最后4字节CRC32校验。CRC计算范围是从目的MAC到净荷结束不包含前导码、SFD也不包含CRC本身这是新手最容易算错的地方。以太网CRC32用的是反射多项式0x04C11DB7初值为0xFFFFFFFF结果还要取反。帧间隙IFG也是很多人忽略的细节。以太网规定两个帧之间至少要有96bit时间的间隔千兆下就是96ns。如果FPGA发帧时不管不顾地连续发交换机可能直接丢弃。我自己的发送状态机里专门有一个IFG计数器每帧发完后强制等待至少12字节时间稳定得很。最小帧长64字节、最大帧长1518字节也是硬性规定UDP净荷太短时要在后面填充到46字节以上净荷超过1500字节必须分片或限制上层数据大小否则网络设备直接不认。3.3 接收解析和发送组装的状态机骨架协议栈说穿了就是两个状态机一个收一个发。我习惯把接收解析分成IDLE、PARSE_MAC、PARSE_IP、PARSE_UDP、WRITE_DATA几个状态。在IDLE状态等RGMII的RX_DV拉高开始收前导码之后逐字段解析MAC头如果以太类型是0x0806就转ARP处理是0x0800就继续解析IP头IP头里看协议字段0x11就是UDP0x01是ICMPUDP头解析出端口后把有效载荷写进FIFO。这里的关键是不要在一个状态里塞太多事每收到一个字节就做一个判断状态转移要清晰否则定位问题的时候会疯掉。发送状态机我分成IDLE、WAIT_DATA、SEND_PREAMBLE、SEND_MAC、SEND_IP、SEND_UDP、SEND_CRC、WAIT_IFG。从FIFO里拿到数据后开始组装帧算IP头校验和、UDP长度、CRC32。这里建议先组帧缓存到一个RAM再统一算CRC而不是边发边算因为CRC需要整帧数据才能算完边发边算的话CRC字段根本来不及补进帧尾。发送状态机里还有一个细节CRC算完后要补在帧尾然后立刻进入WAIT_IFG状态不能直接回IDLE去抢发下一帧。3.4 FIFO深度与跨时钟域数据通路网络PHY的RXC时钟是125MHz而FPGA内部协议逻辑通常跑在系统时钟域两个时钟不同源直接拿系统时钟去采样RXC过来的数据必然出问题。正确做法是RXC时钟域内先把RGMII数据还原成字节流写入异步FIFO系统时钟域再从FIFO读出做协议解析。这个设计和UART的跨时钟域处理是同一个思路区别只是速率从几MHz变成了125MHz对FIFO的读写时序要求更高。FIFO深度怎么选接收方向我建议至少2048字节起步因为需要等一整帧校验通过后上层才敢把数据拿走也就是所谓的“帧缓冲”模式。如果后续要连续收多个大帧比如图像传输场景FIFO深度就得开到16KB甚至更大或者外挂DDR。发送方向也类似系统时钟域把数据写入发送FIFO发送状态机从FIFO里按帧取数FIFO深度至少能容纳一两个最大以太网帧也就是1518字节乘2比较保险。如果FIFO深度小于一帧大小但又把整帧数据都安排好了很可能会出现FIFO溢出丢数据的情况这类问题在Wireshark里表现为“PC发了很多包但FPGA只收到一部分”。4. 时序约束综合工具不知道你的RGMII该怎么采4.1 千兆RGMII的相位关系以及IDELAY的必要性RGMII接口标准规定发送方输出的数据和时钟是边沿对齐还是中心对齐在千兆模式下有讲究。很多PHY在接收方向会输出一个内部移相后的时钟让数据在时钟中心稳定但不同的PHY实现不一样。88E1512这类芯片可以通过寄存器配置是否启用内部的RX delay、TX delay如果硬件设计时把对应的strap引脚设置错了就会出现“数据刚好在时钟边沿上翻转”的尴尬状态采样结果不稳定表现为时好时坏、CRC偶发错误。遇到数据与时钟边沿对齐的情况FPGA侧可以通过IDELAY原语把RXD路径上的数据向后延迟1~2ns让数据对齐到时钟中心。7系列FPGA的IDELAYE2每个tap大约78ps具体延迟值可以在调试时通过ILA观察数据是否稳定来确定。我自己的经历是某块板子同样的RGMII逻辑换个PHY型号就要重新调整延迟参数这也是为什么我不建议把RGMII代码写得“自适应”而是在工程里保留可配置的IDELAY参数。4.2 输入延迟和输出延迟约束怎么填综合工具不知道RGMII接口外面的芯片时序需要你用约束告诉它。Vivado里最核心的就是set_input_delay和set_output_delay。RXC时钟周期是8ns125MHzRXD相对RXC的有效数据窗口由PHY芯片数据手册给出比如某PHY手册上写着RXD相对RXC的skew范围是-0.4ns到2.4ns那么输入延迟约束就类似这样set_input_delay -clock [get_clocks rgmii_rxc] -max 2.400 [get_ports {rgmii_rxd[*]}] set_input_delay -clock [get_clocks rgmii_rxc] -min -0.400 [get_ports {rgmii_rxd[*]}]这里max和min不是随便写的它们决定了工具能否让内部采样逻辑避开建立时间和保持时间违规。输出方向同理要约束TXD和GTXCLK的延迟关系保证对端PHY能稳定采到数据。我见过很多人写完RGMII逻辑后完全不写时序约束综合运行时序的人一脸茫然上板后偶尔能通偶尔不能通最后发现是工具布局布线的结果不在期望相位上。4.3 异步复位、时钟域划分与后端工具才是稳不稳的关键除了IO延迟约束还有几个影响上板稳定性的因素。RXC时钟域里的复位一定不能直接用全局异步复位否则FIFO内部可能出现复位释放时序不一致的问题最好做异步复位同步释放。系统时钟域的复位也同样处理。另一个点是RGMII数据线最好没经过普通LUT逻辑直接进IOB里的IDDR原语否则路径延迟难控制工具也很难满足时序约束。时钟域划分也建议提前规划清楚RXC时钟域只管RGMII接收和异步FIFO写入系统时钟域管协议解析、ARP、发送状态机、发送FIFO读取。每个时钟域内部逻辑尽量保持简单跨时钟域的地方全部用FIFO或两级同步器不要图方便在RXC时钟域里直接去读FIFO的空满信号。这样划分后后端实现工具能更容易地把关键路径收敛好上板稳定性会高很多。5. 上板联调实录从PC ping不通到连续发几千帧不丢5.1 调测过程的分层定位法上板联调我强烈建议按层来不要一上来就发UDP。第一步用ILA抓MDIO和PHY状态寄存器确认PHY已经link up、协商成千兆全双工。第二步只实现ARP应答逻辑在PC上ping FPGA的IP观察PC端能否学到FPGA的MAC地址。第三步实现ICMP Echo应答让PC能ping通FPGA。第四步做UDP回环PC发什么FPGA就原样发回来Wireshark里确认包内容和校验都正常。每一步都有明确的“成功标志”出了问题时也能快速定位是PHY的问题、MAC层问题还是协议层问题。调试时Wireshark是神器但要用对。建议把PC网卡IP固定成192.168.1.10FPGA的IP设成192.168.1.88避免动态IP干扰。在Wireshark里配合过滤语法eth.type0x0806看ARP、icmp看ping、udp.port你定义的端口看UDP。如果RX方向能看到PC发来的包但FPGA没有回应基本可以断定是FPGA接收解析逻辑的问题如果FPGA侧ILA里收到了完整数据但发不出去那就查发送状态机和CRC。5.2 一个真实案例ARP能回ping却不通最后卡在ICMP type判断这个故事很有代表性。我的ARP应答逻辑已经通了PC能正确学到FPGA的MAC地址但ping的时候一片超时。我用ILA抓接收侧信号发现ICMP请求已经进到了协议栈内部ICMP type也确实是8Echo请求但回包一直没出来。查了大半天最后发现是发送回包时把ICMP头里的Type字段从8改成0后忘了重算ICMP的校验和。PC端收到Type8的“Echo Reply”直接判定包无效就丢了表现就是ping不通。这类问题很隐蔽因为不重算校验和的代码在语法上没问题查CRC也不一定能立刻发现毕竟以太网层的CRC是好的ICMP报文内部校验才是坏的。后来我在设计里定了一条规矩凡是修改IP头或ICMP/UDP头中的任意字段必须重新计算对应校验和。IP头校验和的计算方法是把20字节的IP头按16bit一组求和进位回卷最后取反写成一个子模块复用省得每次手工算。5.3 高频问题排查表与Wireshark使用要点我把网络联调阶段遇到过的典型问题整理成一张表方便卡住时快速对号入座。现象优先排查方向处理思路ARP请求收到但FPGA不回复目标IP比较、ARP头字段解析在ILA里抓ARP请求的目标IP确认和你配置的本机IP是否一致ping不通但ARP能通ICMP回包校验和、Type/Code处理重点查ICMP头16bit校验和用Wireshark看回包中ICMP checksum是否显示incorrect收到包CRC错误多RGMII相位、IDELAY、PHY内外延迟调整RXD路径IDELAY或PHY内部的RX delay寄存器FPGA发出去的帧CRC错误TX delay、CRC计算范围、前导码SFD用Wireshark抓本机发出的包确认CRC strip后剩余字节数是否正确UDP回环一半成功一半丢包帧间隙、FIFO溢出、发送状态机检查IFG计时是否够96nsFIFO深度是否容纳整帧发送状态机是否被新数据打断连续发包速度稍高就丢帧FIFO深度、背压机制、速率匹配接收侧用帧缓冲模式发送侧用AXI-Stream式FIFO并把ready信号做全Wireshark里还有一个容易误判的点PC网卡开启了checksum offload后Wireshark抓到的PC发出包可能显示校验和错误其实那些包是好的只是抓包点在校验和填充之前。这时候不要急着怀疑FPGA先对比发送端和接收端的完整链路再决定往哪边查。5.4 调通之后能做哪些事从回环测试到图像数据以太网传输能ping通、能UDP回环之后恭喜你FPGA网络通信这个方向你已经入门了。我通常会建议下一步做一个简单的串口转以太网或ADC数据以太网传输把UART或SPI采集到的数据按自定协议打包成UDP帧发给上位机。这个过程中你会遇到新的带宽计算问题比如用UART 115200bps收20字节一帧以太网每帧至少64字节算下来以太网带宽利用率低得可怜需要做多包合并缓冲。又比如SPI ADC 10Msps 16bit数据率是20MB/s千兆以太网线速是125MB/s理论带宽够但中间有协议开销还得算帧头、填充、CRC这些实战换算比单纯跑demo有价值得多。再往后可以做图像传输项目FPGA把摄像头采集的图像经过缓存、打包成UDP帧发给PC这又引入了一个新的设计问题图像数据按行/按帧拆包上位机怎么通过IP头里的ID字段和UDP payload序号重组。这类方向和图像处理、DDR3/4缓存也经常组合在一起。真到了需要更高吞吐量或更可靠传输的阶段再回头用Xilinx三速以太网MAC IP核或者Zynq PS端的GEM你会明显感觉到自己对底层原理的理解完全不同了。最后分享一个调网络时让我很受益的小习惯每个模块都留一个独立的调试寄存器比如PHY状态、收到帧计数、CRC错误计数、FIFO溢出标志通过串口或简单的AXI-Lite接口读出来。这样在PC端ping半天不通的时候你能直接通过读寄存器快速判断是PHY没link还是接收错误太多不用一次次接ILA重新综合。调网络通信这种事定位问题的速度往往比写代码的速度更能决定项目能走多远。