用FPGA实现以太网UDP通信:基于verilog-ethernet开源工程的实战指南

用FPGA实现以太网UDP通信:基于verilog-ethernet开源工程的实战指南 我这个系列写到这里已经到第十期了。前几期带大家点亮过流水灯、驱动过数码管、跑过串口收发也简单接触过状态机和时序分析。说句实话那些内容做完很多人会陷入一种“我好像会了但又不知道能做什么”的状态。如果你也有这种感觉那这一期应该能让你兴奋起来——因为我准备带你玩一个真正有“网络味”的东西用FPGA实现以太网UDP通信而且是基于verilog-ethernet这个开源工程来学。verilog-ethernet是FPGA圈子里一个很出名的纯Verilog以太网协议栈实现涵盖MAC、ARP、IP、UDP等常用协议层接口采用的是AXI-Stream流式协议特别适合嵌入到Xilinx等主流FPGA的开发环境里。对咱们这种从零开始、没写过几年Verilog的人来说它最大的价值不是让你闭门造车从协议文档开始写代码而是提供一个“能跑、能仿真、能上板、能抓包”的完整范本。你可以先把它当黑盒用起来再顺着代码一层一层拆开看最后按自己的需求去改去裁这个学习路径比直接啃RFC文档要友好太多。这一篇我会从“为什么选这个工程”讲起带你把数据通路捋一遍然后逐个模块拆解再把工程搭建、仿真验证、上板抓包的完整流程走通最后分享一些我实际调试中踩过的坑。不管你手里是Xilinx还是其他厂商的板子只要有一颗以太网PHY芯片基本都能跟上。建议你打开工程对着看配合这期文章信息量会大很多但每一步我都尽量讲清楚“为什么这么做”尽量让零基础的读者也能跟上节奏。1. 该不该自己写协议栈聊聊我为什么选这个开源工程很多人在学FPGA网络通信时第一反应是“我自己写一个UDP收发模块”。有这个想法很正常我刚接触时也这么想过。但实际情况是以太网协议栈远比你想象中复杂哪怕只用到UDP/IP也要同时处理MAC帧校验、ARP请求应答、IP首部校验和、UDP校验和、跨时钟域数据缓存等问题。你自己写不是不行但大概率要折腾几周甚至几个月遇到的坑比自己学到的知识还多。选择verilog-ethernet这个开源工程最核心的原因有三点。第一它模块化做得非常干净。MAC层、ARP层、IP层、UDP层相互独立每一层都有单独的发送和接收通路内部通过标准的AXI-Stream接口对接你甚至可以只拿其中某一个模块用到自己的工程里。第二它自带完整testbench仿真环境写得非常专业。对于零基础的初学者来说读testbench学到的验证思路和读业务代码学到的设计思路同等重要。第三它的代码风格接近工业级但又不至于晦涩注释和命名都很规范非常适合当范文反复研读。当然也有人会问“直接用Xilinx的Tri-Mode Ethernet MAC IP核不就行了吗”确实可以但那又是一个不同的学习路径。商业IP核会帮你把MAC层细节全部封装成黑盒你看到的只有一堆配置界面和接口信号很难理解真正的协议交互过程。而verilog-ethernet的代码全部摆在那里想看哪一层就看哪一层甚至可以自己改一版加个VLAN标签功能这种自由度是IP核给不了的。对于“近似零基础”的学习者我反而建议先用开源工程建立全局认知之后再回头去看商业IP核你会发现自己看配置界面的眼光都会不一样。2. 先看懂数据通路从网线到FPGA内部一串数据经历了什么在打开代码之前我强烈建议你先在脑子里建立一条完整的数据通路图景。否则你一头扎进密密麻麻的module例化里很容易看两页就晕。咱们从发送方向说起。你在FPGA逻辑里要发一条UDP数据这条数据首先会进入mii_axis_tx或者对应你PHY芯片接口的模块它把用户侧AXI-Stream总线上的数据做了一次协议格式的打包生成MAC帧然后通过MII/GMII/RGMII物理接口送给PHY芯片PHY再把并行数据变成串行差分信号往网线上推。这里的重点是MAC层至少会在你的数据前面加上目的MAC地址、源MAC地址、长度/类型字段再在尾部附上CRC校验值。这些动作eth_mac模块已经帮我们做好了。接收方向正好反过来。PHY芯片从网线上收下来的串行数据先被转换成并行数据送到FPGAeth_mac模块会做CRC校验、去前导码、解析出完整的以太网帧然后把有效的负载数据通过AXI-Stream接口送给上层。上层拿到之后arp_eth_rx和ip_eth_rx分别判断这个帧是ARP报文还是IP报文。如果是ARP请求就会查表、应答如果是IP报文则提取IP首部信息交给udp_ip_rx去判断目的端口号最终把真正的UDP负载解出来。这中间有一个特别容易忽略但很重要的点整个链路里各模块之间是以“数据流”的方式衔接的而不是传统意义上“写一个函数然后调用”。打个比方流水线上每个工位只干一件事做完就交给下一个工位大家手上同时都有活这是流式处理。对应的接口就是AXI-Stream里的valid信号和ready信号握手机制保证数据不会丢也不会重复。你后面读代码时如果一眼看到一堆axis_tvalid、axis_tready、axis_tdata千万别慌它们只是流水线上的“放行指令”。3. 工程架构拆解verilog-ethernet的模块地图3.1 模块总览这个工程里都有什么verilog-ethernet仓库名alexforencich/verilog-ethernet的rtl目录下模块非常多但不要被数量吓到。我把它按照功能简单分一下类你心里就有谱了。物理接口适配层就是和外部PHY芯片打交道的部分比如rgmii_interface、gmii_interface、mii_interface。这一层直接决定你板子上那颗PHY芯片能用哪种模式对接。MAC核心层包括eth_mac_1g、eth_mac_10g等负责以太网帧的封装与解封装。总线中转层包括eth_axis_rx、eth_axis_tx它们把MAC层和网络协议层接起来。然后是协议层arp_eth_rx、arp_eth_tx、arp_cache、arp_arb_mux负责ARPip_eth_rx、ip_eth_tx、ip_arb_mux、ip_mux、ip_prefix_lookup负责IPudp_ip_rx、udp_ip_tx、udp_checksum_gen、udp_checksum_calc负责UDP。辅助模块也不少主要就是各种FIFO和同步器比如axis_fifo、axis_async_fifo、sync_reset这些在跨时钟域处理和数据缓存时特别有用。3.2 核心模块职责从发送到接收谁在干什么先说发送方向。用户逻辑要发UDP数据数据先进入udp_ip_tx它负责生成UDP首部。这个首部里有源端口、目的端口、UDP长度和校验和。注意udp_ip_tx默认会自动计算并插入UDP校验和这个功能在udp_checksum_gen模块里实现。如果你的数据经过IP层ip_eth_tx会再接过来生成IP首部并计算IP首部校验和。然后数据交给ip_arb_mux这个模块的作用是让“普通IP数据”和“ARP应答报文”共用同一条发送通路你得把它理解成一个优先仲裁器。接着eth_axis_tx把数据转成MAC帧格式最后送到eth_mac_1g的发送侧加上CRC后从物理接口发出去。接收方向也有对应的流水线。PHY收上来的数据进eth_mac_1g接收侧如果CRC校验通过就进入eth_axis_rx它会把MAC帧的有效负载提取出来同时判断是IP帧还是ARP帧。ARP帧给arp_eth_rx处理IP帧则给ip_eth_rx处理。ip_eth_rx做完IP首部校验后会看协议类型TCP帧或者UDP帧再做分流。UDP帧进入udp_ip_rx它会检查目的IP、目的端口最终把有效负载送到你的用户逻辑。我在刚开始学这个工程时最喜欢用的方法是在Vivado里把整个工程综合出来然后打开原理图视图Schematic把鼠标悬停在每个module实例上看输入输出连线。那种“一条数据流从一个模块流到另一个模块”的视觉感受比单纯看代码要直观十倍。你要是做题做累了完全可以拿这个当电子游戏来玩。4. 把工程跑起来源码获取、IP核生成与最小系统搭建4.1 获取源码与工程目录结构这个工程的源代码在GitHub上直接搜alexforencich/verilog-ethernet就能找到。下载ZIP或者用git clone都行建议clone因为后续更新方便。下载完你会发现目录下有rtl、tb、lib等文件夹其中rtl是核心源码目录tb是测试平台目录lib里面还有一些公用的AXI组件。拿到源码后先别急着创建Vivado工程。我建议你先看一眼里面的rtl/eth_mac_1g.v和rtl/udp_ip_tx.v虽然不一定全看懂但至少对代码风格有个初步印象。你会发现这个工程的代码命名很统一接口信号都有明显的前缀比如s_axis_开头的信号是从用户逻辑进来的m_axis_开头的信号是出去到下游模块的。养成看信号前缀判断数据流向的习惯后面读代码会轻松很多。4.2 创建Vivado工程两个关键选择我在Vivado里新建工程时一般会选RTL Project而不是其他模板因为我们需要逐个添加源码文件。工程建好后添加verilog-ethernet源码时要注意并不是所有rtl/*.v都需要加进去。你需要按自己板子的PHY接口类型来裁剪。比如你的板子PHY芯片支持RGMII接口那就需要rgmii_interface.v、eth_mac_1g.v、eth_axis_rx.v、eth_axis_tx.v这四个基础模块如果你的PHY是GMII接口则换成gmii_interface.v。为了减少添加文件的麻烦我提供一个最简方案写一个顶层模块叫udp_test_top.v只例化rgmii_interfaceeth_mac_1geth_axis_rxeth_axis_txarp相关 ip相关 udp相关。这样的最小系统就已经能完成UDP回环了。之后再把你自己写的用户逻辑挂到udp_ip_rx的输出和udp_ip_tx的输入上就是你的“第一个FPGA网络应用”。4.3 时钟与复位最基础也最容易翻车的环节以太网口的时钟设计非常关键而且不同PHY芯片、不同接口模式的时钟频率各不相同。以RGMII为例GMII的时钟是125MHzRGMII同样也是125MHz时钟但是数据在时钟的上下沿都采样等价于250Mbps每根线的传输效率。你的FPGA通常会有两种做法一是由板载晶振提供125MHz参考时钟给PHY芯片PHY内部锁相后回传一个RX_CLK给FPGA发送时钟则由FPGA自己产生二是使用FPGA内部的MMCM/PLL把时钟分频到125MHz。无论哪种做法都牵涉到跨时钟域的问题PHY回传的RX_CLK是125MHz而你的用户逻辑可能跑在100MHz甚至更低这就需要用到异步FIFO来隔离时钟域。verilog-ethernet工程里用了不少axis_async_fifo原因就在这里。复位的处理也值得单独说。FPGA的复位信号如果没有做同步处理很容易产生亚稳态导致模块偶尔工作异常。很多初学者喜欢直接把拨码开关的复位信号接进所有模块的rst引脚这种做法很不推荐。工程里提供了sync_reset这个模块它可以把异步复位信号同步到目标时钟域再输出给其他模块。你上板调试时如果发现偶尔能工作、偶尔不能工作先查复位有没有同步、复位释放时间够不够多半问题就出在这里。5. 从0跑仿真让虚拟网线替你试错学FPGA最幸福的事情之一就是可以在电脑上仿真。毕竟你每烧一次板子都要几十秒等久了效率极低而在仿真器里跑一轮测试只要几秒钟。verilog-ethernet工程自带的testbench质量很高但如果你只想快速验证自己搭的最小系统我建议先跑通一个最简单的收发仿真而不是直接跑全工程。5.1 搭建最小的testbench我自己习惯用Vivado自带的XSim做仿真因为省事不需要额外去配QuestaSim这些工具。测试平台里我把rgmii_interface的GMII侧接到一个虚拟的MAC模型上这个MAC模型可以自动发送一段固定数据然后我用一个task产生复位、产生时钟再用一个task模拟PHY侧的数据接收把FPGA发送出来的数据读回来做比对。这里有一个操作上的细节RGMII的仿真核心是时钟和数据的关系。RGMII接收方向PHY会在RX_CLK的上升沿和下降沿都送数据而发送方向FPGA要在TXC的上下沿都要采样输出所以仿真时你的驱动task必须处理好这些边沿否则你会看到数据错位或者CRC永远不过的现象。不要问我怎么知道的我第一版testbench就是只驱动了上升沿结果仿真到了接收侧数据从udp_ip_rx出来的tdata全是乱的查了半天才想到是边沿问题。5.2 仿真时重点抓哪些信号跑仿真时信号列表不要一次性全加进去会看得眼花缭乱。我习惯分三组看。第一组是物理接口层rgmii_rx_ctl、rgmii_rxd、rgmii_tx_ctl、rgmii_txd确认PHY侧数据在正常收发。第二组是MAC层到协议层的核心握手信号eth_axis_rx_axis_tvalid、eth_axis_rx_axis_tready、eth_axis_rx_axis_tlast确认MAC帧能正常解析出来然后是ip_eth_rx_ip_hdr_valid、udp_ip_rx_udp_hdr_valid确认IP和UDP首部能被正确识别。第三组是我们自己用户逻辑的收尾信号用户侧的axis_rx_tdata、axis_rx_tvalid这里能看到真正的UDP负载内容。如果你发现udp_ip_rx_udp_hdr_valid始终为0先查UDP目的端口是不是匹配了。工程默认是允许外部配置端口号的如果你没有给udp_ip_rx模块的正确接口赋值它很可能会一直认为目标端口不对而丢弃数据。5.3 仿真环境的一个实用套路用task封装收发我后来写testbench时会封装两个task。一个是send_udp_packet它负责把一串十六进制数据从用户侧塞进udp_ip_tx并自动生成AXI-Stream上升沿、valid/ready握手另一个是expect_udp_packet负责从udp_ip_rx读取数据并和期望数据比对。这样在做功能测试时主测试流程就非常简洁看起来就像在写脚本一样。这个思想其实是受工程自带testbench的影响它也是用类似方法把复杂的时序操作封装成一个个task。6. 上板实测用Wireshark见证你的第一个UDP包仿真都通过了下一步就是上板。这个过程最激动人心但也是坑最多的一步。我按照自己的操作顺序把关键步骤都列出来你按这个顺序来做能省不少时间。6.1 硬件连接与工程约束上板之前先确认板子上的PHY芯片型号比如Realtek的RTL8211系列、Marvell的88E1512系列都很常见。最稳妥的方式是去开发板厂商的例程里翻一翻看他们是怎么例化PHY芯片配置的比如MDIO接口、复位引脚、时钟模式怎么设置。关键的问题是约束文件XDC。我强烈建议你仔细检查关于RGMII引脚约束的写法。常规IO约束只需要set_property PACKAGE_PIN和IOSTANDARD但RGMII的信号往往有特殊的时序要求。比如RGMII接收数据线rgmii_rxd[3:0]相对于rgmii_rx_clk是存在一个内部延迟的为了保证FPGA采样正确通常需要额外配置IO Delay或者配合数据引脚的可编程延迟资源。某些板卡例程还会把rx_clk送入PLL做相位调整这时候你需要同步检查PLL设置是否正确。很多初学者第一次上板遇到收不到数据查来查去最后发现是XDC里忘了约束set_property -maxdelay或者忘了设置IDELAY_CTRL导致PHY送过来的数据不能被正确采样。这种问题仿真完全看不出来只能上板排查。你如果在Wireshark里看到FPGA发出来的包CRC错误首先要怀疑发送时序如果你发现FPGA根本收不到PC发来的包就先测PHY芯片的链路状态寄存器看看link是否已经up了。6.2 回环测试从最简单的数据开始我的建议是第一次上板别急着发复杂的数据先做一个最简单的回环PC通过UDP调试工具向FPGA发送一串固定字节FPGA收到后原封不动地发回去。这样只要PC端的调试工具显示收到了相同的数据就说明整条链路基本通了。跑通回环之后我把数据内容改成自己设计的一个小逻辑生成的递增序列比如每次发送4字节计数器值然后PC端连续接收检查是否有丢包或者乱序。这能快速验证你的UDP回环会不会因为时序问题偶发丢数据。如果你发现PC收到的数据偶尔会缺几个字节那大概率是跨时钟域FIFO的深度不够或者握手机制逻辑没处理好导致在某个时刻数据被覆盖或者被丢弃。6.3 用Wireshark抓包比任何调试器都直观很多UDP调试工具只能显示收发内容看不到网络报文细节。我建议直接用Wireshark抓包把FPGA开发板的网口用一根网线连接到电脑的另一个网卡上或者通过交换机连接然后抓包观察。抓包时重点看两个东西一是PC发给FPGA的包FPGA是否在MAC层做了应答比如ARP应答是否正常二是FPGA发回来的UDP包IP首部、UDP首部、校验和是否填对了。如果你发现FPGA发回来的包Wireshark报“校验和错误”先别急着怀疑FPGA很多情况下是网卡在发送时开启了硬件校验和卸载checksum offloadWireshark抓到的是已经被网卡修改过的包。你可以先在网卡属性里关闭“IP校验和卸载”和“UDP校验和卸载”再抓一次大概率就正常了。这个坑我当年踩了整整一个晚上。7. 学习复盘从“会用”到“能改”再到“能造”跑通了UDP回环你的FPGA网络开发算是正式入门了。这时候你最该做的是趁热打铁继续深入。我的建议是按照“会用、能改、能造”三个阶段来规划自己的学习节奏。会用阶段你已经完成了。能改阶段我推荐做这几个小练习把FPGA回环改成“只回复特定端口的数据”把udp_ip_tx的源IP、目的IP改成可配置寄存器运行时通过串口命令修改给ARP缓存模块写一个主动请求逻辑让FPGA定时去Ping网关。这些练习都不难但能逼你认真读代码、改代码比单纯看十遍文档都管用。能造阶段等你把udp_ip_rx和udp_ip_tx的数据接口摸透之后就可以尝试自己写一个“FPGA UDP图像传输”的小项目了。思路很简单用OV5640摄像头采集图像经过简单灰度化后存进BRAM或DDR然后通过UDP分包发到PC上位机显示。这个项目几乎会用到你前九期学过的所有东西而且做出来很有成就感。verilog-ethernet工程里没有给你做应用层的规矩应用层本来就是留给你的自由发挥空间。8. 常见问题与调试技巧实录最后这部分我把自己调试过程中遇到的最典型问题和排查经验整理成一张速查表。你在学习过程中如果卡住先来这里翻一遍很多时候能少走好几个小时弯路。现象可能原因排查思路仿真中UDP数据从udp_ip_rx输出为全零数据没有正确握手或者目的端口不匹配看udp_ip_rx_udp_hdr_valid是否拉高检查目的端口寄存器赋值是否生效仿真中数据能收到但CRC报错testbench没有正确处理RGMII双沿采样检查测试逻辑是否在时钟上下沿都采样或驱动上板后PC收不到FPGA发送的UDP包PHY链路未up或者发送时序有问题读取PHY状态寄存器确认link状态用ILA观察rgmii_txd、rgmii_tx_ctlPC能收到包但Wireshark报UDP校验和错误网卡硬件校验和卸载干扰或FPGA发送校验和错误先关闭网卡校验和卸载再抓包再看udp_checksum_gen接口配置数据偶发丢失或乱序跨时钟域FIFO深度不足或用户逻辑没有正确拉低ready检查axis_async_fifo实例化时的深度参数确认用户逻辑ready信号的生成是否受复位影响系统刚上电时偶尔不工作复位后正常复位没有做同步处理或复位释放太快使用工程自带的sync_reset模块检查复位信号是否满足所有模块的低电平宽度要求ARP总是失败PC端无法Ping通FPGAARP缓存模块内没有配置本机MAC/IP或者应答逻辑出错检查arp_cache模块的local_mac、local_ip参数是否写对仿真观测arp_eth_tx输出除了表格里的问题我再分享一个自己很受用的调试习惯每改一次代码都坚持先在仿真里跑通再上板验证。很多人图快直接上板看现象但FPGA内部信号你是看不见的最后不得不用ILA去抓内部信号效率反而更低。仿真虽然慢一点但你能看到所有信号定位问题的速度远比上板快。另外如果你在Vivado里用ILA调试RGMII接口信号要注意采样时钟的选择。ILA的采样时钟最好用rgmii_rx_clk而不是全局125MHz时钟否则采样到rgmii_rxd时很容易因为相位不对而抓到错乱的数据。这一点在调试PHY接收侧时特别重要。对我来说FPGA开发最吸引人的地方就在于你能亲手把一条看起来高大上的网络数据通路从物理层到传输层一层层拆开、看透、掌控。网线那头传来的一把字节经过你的代码加工后变成你想要的行为那种掌控感是别的地方很难体会的。而这只是FPGA网络应用的第一步。下一步的DDR、PCIe、图像处理又会是全新的世界。