FPGA实战:从RGMII到ARP回环,搭建以太网通信最小系统

发布时间:2026/9/7 22:07:31
FPGA实战:从RGMII到ARP回环,搭建以太网通信最小系统 讲真网络通信是FPGA从控制逻辑往系统级应用迈过去时绕不开的一道坎。前六篇咱们把LED、按键、UART、SPI玩了个遍之后你会发现这些外设全是点对点的距离稍微拉远就没戏了。这也是我在第七篇安排以太网的原因——它有一套完整的协议栈有帧格式、有地址、有校验、有冲突检测一个工程能带出一堆硬件基本功。别慌FPGA做网络通信没有想象中那么玄乎。抛开物理层那些模拟信号问题实际上你要打交道的接口就是RGMII要实现的协议就是从ARP和ICMP开始凑够一个能通的最小系统。这篇文章以Xilinx Artix-7加一颗千兆PHY芯片为例从协议概念到工程框架从RGMII时序到上板抓包把整个链路完整捋一遍。哪怕是刚写完流水灯的水平只要耐着性子把状态机的思路理清这块也能啃下来。1. 先搞清楚分工FPGA在网络通信里到底负责哪一段1.1 别上来就啃协议栈先分清MAC和PHY以太网通信的软硬件划分非常清晰PHY负责把数字信号变成差分电信号或光信号也管自适应、链路检测、MDI/MDIX翻转这些脏活MAC负责组帧、拆帧、CRC校验、流量控制、冲突检测这些逻辑层的活儿。FPGA做网络通信绝大多数情况是你自己用Verilog实现MAC然后外接一颗PHY芯片。市面上常见的千兆PHY有瑞昱RTL8211系列、微芯KSZ9031、美满88E1512等都提供RGMII接口差别主要在寄存器位和一些细小时序上。板子不同、PHY型号不同但顶层接口基本一致。你写RTL的时候只跟RGMII打交道不用碰PMA/PCS那些模拟前端的东西PHY芯片已经把物理层的活儿全包了。学习路径上千万别一上来就背OSI七层模型。做FPGA网络通信实际够用的知识边界是数据链路层你写MAC网络层你处理ARP、IP、ICMP传输层的UDP/TCP要么软核处理器扩展、要么拆成流水线状态机做。物理层的事交给PHY会话层以上跟FPGA没关系。1.2 以太网帧格式的最小拆解一个ARP请求长什么样要写MAC收发逻辑第一件事就是背帧格式。标准以太网帧DIX v2从目的MAC开始算共14字节头加4字节FCS字段长度说明前导码7字节0x55交替用于接收时钟同步SFD1字节0xD5帧起始定界符目的MAC6字节可以是单播、广播或多播地址源MAC6字节本机MAC地址EtherType2字节0x0806是ARP0x0800是IPv40x86DD是IPv6载荷46~1500字节不足46字节时要以PAD补齐FCS4字节CRC32校验值从目的MAC到载荷末尾注意很多PHY芯片在做接收时已经帮你把前导码和SFD吃掉了所以进入FPGA的逻辑通常从目的MAC开始到FCS结束。学到后面你会发现个别PHY的配置选项还会影响是否把FCS一起交给MAC这个细节到了调试阶段会让你特别上头。ARP请求是网络层里最简单的报文之一。以太网头14字节加ARP数据28字节一共42字节按最小帧要求还得补4个字节的PAD凑够46字节。ARP请求的内容大致是硬件类型1表示以太网协议类型0x0800表示IP硬件地址长度6、协议地址长度4操作码1表示请求、2表示应答接着是发送方MAC/IP和目标MAC/IP。目标MAC在请求时为全0因为请求方就是想通过IP查出MAC。你要做回环实验的话核心就是识别这类请求然后把操作码改成2、把自己的MAC填进目标MAC位置把目标IP镜像回发送方IP发回去。IP地址比较、MAC地址比较都是纯组合逻辑不复杂但要注意很多初学者会在字节序上栽跟头后面专门说。2. 工程搭建第一步选平台、看懂RGMII再动手写代码2.1 我使用的平台与PHY选型参考做入门实验我推荐直接用你手头现成的板子不推荐为了学这个专门买高价开发板。Xilinx Artix-7、Zynq-7000、Intel Cyclone V这些主流器件都能做因为MAC逻辑不依赖特殊资源只要引脚电平标准支持2.5V或3.3V的LVCMOS、有足够的BANK供电就行。我自己实验用的是Artix-7搭配板载RTL8211E这颗PHY芯片。PHY选型的时候重点关注三点接口模式必须是RGMII或者GMII如果你板子上只有MII说明PHY是百兆的那也能做实验但速度慢一些、时钟频率也更低。MDIO接口别省调试时读PHY寄存器状态至关重要像链路是否建立、是否自动协商完成都要从PHY的状态寄存器读没有MDIO等于蒙眼开车。晶体频率RTL8211E这颗芯片的25MHz基准时钟可以来自晶振也可以由FPGA引脚提供。但做千兆通信时FPGA侧还需要一个独立的125MHz参考时钟给RGMII的TXC这个时钟往往由板载晶振直接供给FPGA。2.2 RGMII引脚级时序别在这里省时间从GMII到RGMII最大的变化是引脚减半、时钟变成DDR。RGMII的数据线只有4根发送、4根接收TXD[3:0]在时钟上升沿发送字节的高4位下降沿发送低4位。控制信号也编码成两个信号上升沿是TX_EN发送使能下降沿是TX_ERR。接收方向同理RXD[3:0]在上升沿给高4位下降沿给低4位RX_CTL上升沿是RX_DV接收数据有效下降沿是RX_ERR。这里最容易犯迷糊的是RGMII标准名义上是源同步但实际应用中PHY芯片的输出相对时钟是有偏移的具体偏移量由PHY内部寄存器配置可能提前也可能滞后。很多PHY自带TX delay和RX delay的寄存器开关就是为了调这个相位。我用RTL8211E时一个典型配置是把TX内部的delay打开接收方向在FPGA内部用IDELAY或者约束一个负的input delay来处理。在实验阶段如果板子的PCB走线做得规整、时钟源稳定有些情况下不调delay也能通只是温度或电压一漂就可能随机丢包。所以原理上还是要理解RGMII数据在时钟沿前后都有很短的建立保持窗口需要在约束文件里声明让工具把你的逻辑布局布线优化到这个时序窗口内。PHY的时钟来源也要捋清楚。千兆模式TXC是125MHz百兆模式是25MHz十兆模式是2.5MHz。有些PHY支持TXC由MAC提供这种情况简单有些PHY是需要自带25MHz晶振然后从内部倍频到125MHz再通过RXC送出来。做约束时所有时钟都要正确创建时钟组否则工具可能把125MHz当作未知关系时钟布局结果完全没法看。3. 核心逻辑动手写MAC收发状态机与缓存设计3.1 接收状态机从RGMII字节流到完整帧的组装接收方向PHY已经把RGMII的并行数据给你了但你拿到的是4bit的DDR信号要先把4bit转成8bit。一般做法是内部生成一个125MHz的时钟在它的上升沿同时采TXD[3:0]的高4位和低4位拼在一起得到一个字节。接下来是接收状态机的编写空闲状态等待RX_DV有效。一旦RX_DV拉高说明PHY检测到SFD之后开始送帧数据了每个时钟周期采一个字节。接收状态持续累加字节计数同时把数据写入FIFO。这里要边收边检查两个东西一个是字节计数是否超过上限比如1518字节超了就置ERROR另一个是帧结束的时候RX_DV有没有正常拉低。帧结束处理如果收到的字节数小于64说明是残帧runt frame直接丢弃。如果CRC校验在完整帧结束时算出来不对也丢弃。这些判断逻辑不复杂但每个周期都在做比较写的时候要小心状态寄存器被覆盖。接收状态机里有个关键设计决策是先缓存整帧再交给上层还是边收边交给上层入门阶段强烈建议先缓存整帧。因为ARP回环实验里你要等收到完整的目标MAC、源MAC、操作码、IP地址之后才能决定怎么回复。如果边收边处理状态机的分支会非常乱而且遇到CRC错误时你根本来不及撤销已经发出的数据。3.2 发送状态机组装帧头、填类型、发数据、补PAD发送方向相对简单。状态机从空闲开始收到上层模块给的发送请求信号后就依次送出目的MAC、源MAC、EtherType、载荷、PAD、FCS。一个标准的发送状态机可以是空闲等待发送请求发目的MAC和源MAC这两个地址分别来自ARP模块计算出来的目标地址和板卡的固定MAC。发EtherTypeARP帧填0x0806发载荷从发送FIFO按字节读出补PAD和发FCS当载荷不足46字节时自动补0然后输出CRC32结果。这里有几个容易出错的细节第一发送使能TX_EN要在整个帧期间保持为高不要中间有毛刺第二发送时钟和接收时钟是同源的话简单非同源的话发送和接收之间的FIFO做异步处理。第三总线上的8bit数据在转4bit DDR输出时顺序跟接收是镜像关系高4位在上升沿、低4位在下降沿。3.3 缓存机制异步FIFO是跨时钟域的常规解法接收FIFO和发送FIFO我建议都用异步FIFO深度至少512字节方便缓冲突发帧。Xilinx平台可以用FIFO Generator IP核或者直接用分布式RAM手写一个简化版异步FIFO。手写异步FIFO需要处理格雷码跨时钟域同步对初学者要求稍高我建议第一次做直接用IP核把精力放在状态机逻辑上。真正要花心思的是FIFO水位管理。比如接收FIFO里存了一整帧上层ARP模块怎么知道现在有完整帧可以读了常用做法是接收状态机在帧结束时往FIFO里写一个帧长度字再通过另一个信号通知上层。上层读长度字再依据长度读取对应字节数。这样能避免FIFO里残留半帧垃圾数据导致后面解析错位。4. CRC32计算原理、实现和踩到的一个反直觉坑4.1 CRC32不是玄学多项式算明白就行以太网FCS用的是CRC32生成多项式0x04C11DB7初值是全1结果要异或全1数据是按LSB first的方式进位的。网上能看到很多Verilog写法有的按字节处理、有的按位处理你不需要把数学完全推导一遍但至少要知道几点FCS是发送方把目的MAC到载荷末尾的每一个字节按位从低位到高位送入移位寄存器算出来的。接收方校验时可以重新算一遍也可以把收到的FCS一起送入计算最终结果应该等于0x2144DF1C这个魔数。查表法适合软件FPGA里面积功耗有限用LFSR展开按字节算更合适。Xilinx官网也有免费的CRC IP核直接用或者参考它的源码都行。按位算最简单4个周期算一个字节代码好写但浪费时钟。千兆接口125MHz时钟下按位算完全来得及因为一个字节需要8个周期而帧间隙早就超过这个数了。但如果后面要做多通道或者线速转发就要换成按字节并行算法那时候建议直接用IP核。4.2 字节序和位序根本原因在RGMII的LSB first我在这个坑里折腾了一整个晚上。现象是FPGA发给PC的MAC地址和IP地址全都反了序。比如我明明是AA:BB:CC:DD:EE:FFPC端Wireshark抓出来却是FF:EE:DD:CC:BB:AA。后来才意识到RGMII本身是按GMII定义的bit序传输的GMII里每个字节的bit0先发也就是低位先发。而很多初学者写代码时把RGMII的4bit高低位拼成字节的顺序搞反了导致整个帧的字节内容左右翻转。解决办法就是严格按标准来RGMII在跳变沿采到的4bit拼成8bit时第一个沿的是高4位还是低4位要跟你的PHY芯片手册对表。RTL8211E手册里的时序图明确画着TXD[3:0]对应GMII的高4位还是低4位照着手册来不要凭感觉。4.3 发送FCS时的坑FCS字段本身不算在CRC内还有一个反直觉的地方CRC计算是把目的MAC、源MAC、类型、载荷、PAD全部算进去但FCS字段本身不参与计算。有些初学者写发送状态机时把最后一个字节的CRC结果也顺便回灌给CRC计算模块导致帧尾多错4个字节。我的建议是发送时做一个标志信号标记FCS期间不更新CRC计算器或者干脆在计算完载荷之后锁存CRC值再在4个时钟周期里逐字节发出去。5. 上板实测从仿真到ARP回环抓包的完整排错链路5.1 先仿真再上板不要直接网线怼交换机写完RTL之后先做行为仿真这是最便宜的错误查找方式。仿真时你的TB要模拟PHY的RGMII收发行为比如一个时钟上升沿给你TXD[7:4]下降沿给你TXD[3:0]同时把TX_EN/TX_ERR按真实PHY行为拉高拉低。你可以构造一个ARP请求帧的byte序列送进接收FIFO观察你的状态机能不能正确输出ARP应答。等仿真通过了再上板。上板第一步别插网线先用ILA集成逻辑分析仪抓内部信号确认接收链路有数据进来。怎么才能有数据进来一种办法是直接连到PC的网口PC发一个ping包网线一插PHY的RX_DV就会跳起来。如果ILA里RX_DV始终为低那要先查物理链路PHY链路状态寄存器有没有置1网线是直通还是交叉千兆自适应有没有协商成功5.2 用LED把调试信息打印出来没有ILA或者ILA用不利索也没关系把你的状态机关键状态信号引到LED上。比如接收状态机在空闲、接收、校验这三个状态分别让不同LED亮你一眼就能看出卡在哪一步。这是FPGA调试最朴素也最有效的办法尤其是在Vivado仿真跑不完大工程、ILA触发逻辑又没写对的时候。还有一个小技巧把收到的帧字节计数器的最高几位接LED插上网线ping一下如果LED在闪烁说明有数据在流进来只是后面的校验或解析有问题。这时候再借助ILA抓帧头数据比对Wireshark看到的MAC地址基本能定位是位序还是状态机转移问题。5.3 Wireshark端到端验证的完整流程当FPGA的ARP应答逻辑写好、ILA看到TX链路送出数据后就可以端到端测试了。操作流程是PC网口连FPGA板子的RJ45。PC手动配置一个静态IP比如192.168.1.20掩码255.255.255.0。FPGA内部给自己分配IP 192.168.1.10MAC地址写死一个非0的地址。PC端cmd执行arp -d清空ARP缓存然后ping 192.168.1.10。打开Wireshark监听对应网卡观察是否有ARP请求发出、是否有ARP应答回来。如果Wireshark里只能看到PC发的ARP请求看不到FPGA的应答先看FPGA侧ILA收到的请求内容是不是对的特别是目标IP字段的值是否跟你在FPGA代码里固定下的IP一致。如果不一致问题基本出在接收端字节拼接顺序或者PHY配置上。如果内容一致但没回应查你的状态机是否卡在比较的某个分支比如MAC地址比较长度搞错了或者FIFO读使能没拉。5.4 你一定会遇到的问题ping大包不通、小包通小包能通、大包不通是经典问题。原因通常是接收FIFO深度不够、或者发送状态机在帧长超过某个值时把FIFO读空、又或者MAC帧长上限设置被国内厂商的默认寄存器值限制在1518字节而PC的ping包默认是1472字节ICMP载荷加28字节头等于1500字节再算上14字节MAC头加4字节FCS是1518字节刚好卡线。这还没算VLAN标签如果有VLAN帧长上限得到1522。如果你的板子PHY开了巨型帧支持那可以轻松通过大包测试如果没开那么把ping包调小到1000字节左右一般就能通。这个现象本身不是bug而是以太网标准对帧长上限的定义。但如果你以后做UDP图像传输一定会碰大载荷到那时候就需要认真处理FIFO深度和帧长计数器的位宽而不是简单把包调小。6. 网上资料不会明说的经验时序约束、PHY寄存器与后续进阶6.1 时序约束是通了才总结的坑很多实验通过之后你可能会觉得约束文件不写也行。短时间确实可以但一旦逻辑稍微复杂一点、布局随机失败你就要回头补约束。RGMII接口的约束有几个必须写的东西创建虚拟时钟作为PHY侧时钟的参考并在输入端设置input delay。输出TXC和数据TXD要设置output delay或者用ODDR原语把TXC做成可靠的时钟输出而不是直接用内部时钟引脚。如果PHY要求TXC和数据同步翻转别用组合逻辑产生TXC要专用输出时钟引脚配合ODDR否则时序完全不可预测。Vivado里有一个report_clock_interaction工具可以看所有时钟域之间的关系。初学者做完第一个ARP回环后一定要打开看看你会发现PHY的RXC和你的系统时钟之间有很多交互路径那些路径如果不做约束会让工具在优化时乱猜。6.2 MDIO读写比想象中更需要PHY芯片的上电默认配置能不能满足你的链路需求通常可以但调试时MDIO几乎必用。我建议在设计里留一个简单的MDIO主控制器模块哪怕不支持任意读写先把几个关键寄存器读回来显示到LED上控制寄存器0x0、状态寄存器0x1、协商伙伴能力寄存器0x5。通过状态寄存器的bit2链路状态能立刻判断网线插没插好、对端有没有在发信号。有些PHY的MDIO地址由芯片引脚决定比如RTL8211E是0x01但有的板子把PHY地址配置为0x00或0x04你得看原理图确认。MDC频率不要超过25MHz上电后还要等PHY内部初始化完成一般延迟个100ms再读寄存器比较稳妥。这些信息你翻芯片手册的寄存器表都能找到但实际工程里写着写着就会忘了加这个延时模块然后在调试时觉得怎么读回来全是0。6.3 下一步进阶方向从ARP到UDP/TCP以及软核协同做完ARP回环你就已经掌握MAC收发通路、FIFO缓存、CRC校验、时序约束这一整套基本功。下一步的进阶路径可以是实现ICMP应答让PC的ping能通这是最简单的有去有回链路。在ICMP基础上加一个UDP收发模块把收到的UDP数据解析出来映射到板载寄存器实现远程控制LED、远程读取ADC采样值。如果要用TCP协议严格来说不建议纯RTL手写因为TCP状态机复杂得多不如用Xilinx的TCP/IP核或软核处理器跑LwIPFPGA只做MAC和DMA接口可用性和可维护性都好很多。如果是Zynq平台还可以用AXI总线把MAC接到PS侧让ARM核跑协议栈这是工程实践中最常见的分工方式PL负责高速数据通路PS负责复杂协议和上层应用。6.4 做实验时的一种靠谱心态别指望一天把ARP、ICMP、UDP全拿下。第一周能把ARP请求认出来、回一个应答、Wireshark抓到正确帧已经算是非常优秀的入门进度。这个过程中你会反复怀疑字节序对不对、状态机有没有漏状态、CRC算得对不对这种折磨恰恰是FPGA开发最值钱的训练。我记得自己第一次跑通ARP的时候Wireshark里看到FPGA回了一个FF:FF:FF:FF:FF:FF的广播应答还能正常被PC接收虽然异常但也说明协议栈健壮性比想象中强回头一想其实是自己的目的MAC填成了广播地址之后才真正理解ARP应答的目的MAC为什么必须填请求方的单播地址。所以给后面的朋友一个建议所有报文内容都用Wireshark对照协议字段逐字节核对不要只看ping通了就完事。通只是链路通字段全对才叫协议实现对。