FPGA与PC高速通信:基于verilog-ethernet的UDP协议栈实战解析

发布时间:2026/9/17 13:19:22
FPGA与PC高速通信:基于verilog-ethernet的UDP协议栈实战解析 做FPGA开发到一定阶段你会发现手里攒了一堆“能跑”的模块串口、SPI、I2C、PWM、状态机单看每个都能工作但一旦想和PC高速交互数据就立刻卡住。串口115200bps传个几十KB的波形数据能等到人发霉USB转串口芯片本质上也没有突破串口瓶颈这时候以太网就成了绕不开的方向。我在整个系列的第10篇终于决定啃verilog-ethernet这个开源UDP协议栈工程也把之前一直没想明白的“FPGA怎么和PC用网线通信”这件事彻底搞通了。这篇内容不是单纯翻译一遍官方文档而是把我从0开始摸索这个工程的过程、模块连接原理、仿真验证方法和上板实测踩坑记录都写出来。如果你已经学过Verilog基础、知道怎么写状态机、会用Vivado建工程但还没接触过以太网协议栈那这篇文章正好卡在你的技术断点上。看完之后你能理解UDP协议栈在FPGA里到底是怎么组织的以及怎么改参数、搭回环实验、定位“收发不通”的问题。1. 为什么第10步要选UDP协议栈而不是TCP、PCIe或者DDR先说一个比较现实的问题。FPGA的学习路径到了后面方向特别多有人去做图像处理有人去做高速ADC采集有人去做电机控制但无论哪个方向几乎都躲不开“把FPGA的数据送到PC看看”这个需求。图像处理要传一帧帧的视频流TDC直方图要传密集的时间戳数据干涉仪测向要传多通道采样结果这些数据量用串口根本扛不住。要么挂USB芯片要么走PCIe要么走以太网而以太网是三者里面最容易上手、成本最低、也最通用的方案。我选UDP而不是TCP理由其实很实在。TCP在FPGA里实现要考虑连接管理、序列号、重传、拥塞控制、窗口滑动这些逻辑堆下来代码量直接翻几倍而且协议栈越复杂出bug的概率越高。UDP是无连接的发送端把数据打包扔出去接收端解包拿出来中间没有握手、没有确认数据丢了就丢了。听起来好像很不可靠但用在内部系统通信、实时数据采集、视频传输这些场景下UDP反而更合适。FPGA的优势本来就是低延迟高吞吐如果为了可靠心跳牺牲掉实时性就没有意义了。verilog-ethernet这个开源工程之所以值得学是因为它把以太网链路层、ARP、UDP层都封装成了可复用的Verilog模块。它不是那种只能整个工程例化、改不了内部结构的黑盒IP而是把MAC、RGMII接口、UDP收发、ARP缓存这些部分拆成独立的rtl源码你可以像搭积木一样只拿自己需要的模块。这对学习协议栈原理来说非常友好你可以一个模块一个模块地读源码、做仿真搞清楚每一层到底干了什么。1.1 从串口到以太网我踩过的“升级通信方式”的弯路我在做FPGA和PC通信的时候最早用的是USB转串口也就是CH340或者FT232这类芯片。刚开始觉得只要能发数据就行了后来发现串口有几个很要命的问题。首先是速度瓶颈普通USB转串口跑到921600波特率就到头了每秒最多传90KB左右。其次是没有帧的概念串口就是字节流如果信号一抖帧边界就丢了。我做实时数据的波形显示时经常出现上位机解析错位、曲线乱跳的情况。后来又试过用USB芯片比如CY7C68013A这种能跑到几十MB/s的速率但又要写驱动、又要写固件和PC端SDK对接环境搭建成本很高。PCIe更是要熟悉AXI总线、DMA、驱动开发对一个刚开始做项目的人来说门槛太高。以太网的优势在于PC端不需要额外驱动操作系统原生支持UDP/IP协议栈只要用网线连上交换机上位机写socket代码就能收发数据。而且千兆以太网的带宽是1000Mbps即使UDP协议有开销有效数据率跑到接近100MB/s也很轻松比USB转串口强了几个数量级。1.2 UDP为什么是一个“正常FPGA开发”绕不开的选项很多初学者会问既然有现成的TCP/IP协议栈IP核为什么还要自己去看开源的UDP实现这里我想强调一个点商用IP核比如Xilinx的三速以太网MAC、Altera的TTSE它们解决的是MAC层和PHY层的问题但UDP/IP协议栈那一层还是得你自己处理。而UDP层恰恰是FPGA和PC通信差异最大的地方也是需要根据业务逻辑调整的地方。比如你要传图像数据一帧1080p的RGB图像就有6MB以太网MTU通常是1500字节你必须把图像切分成几千个UDP包来发送。每个包里要带什么序号、怎么描述数据格式、接收端如何重组这些业务逻辑都建在UDP之上。如果你不理解UDP的帧结构、校验和、端口匹配、ARP解析这些底层细节上层业务根本就没法写。verilog-ethernet的UDP代码写得比较规范直接读源码比看协议文档效率要高得多。2. 理解verilog-ethernet工程的整体结构和设计思路刚打开verilog-ethernet的仓库时我第一反应是“怎么这么多文件夹”。rtl目录下面按功能拆了eth_mac_1g_axis、eth_udp、eth_arp、eth_rgmii、axis_fifo这些子目录看起来有点乱。但如果把数据流的方向画出来就会清楚很多。工程本质上是解决“数据从网线进来如何变成用户能用的数据”和“用户数据如何变成网线上的信号”这两个问题。网线上跑的物理信号是模拟电平PHY芯片负责把它们变成RGMII接口上的数字信号。RGMII再被MAC层模块接收完成前导码校验、CRC校验、帧长度检查输出的就是一整帧打好包的以太网数据。这一帧数据里包含目的MAC、源MAC、类型字段、IP头、UDP头和应用负载。UDP收发模块做的就是从这帧数据里剥离出应用负载或者反过来把应用负载加上IP头、UDP头发送出去。这里最关键的设计思想是分层。MAC层不关心你传的是什么协议它只管把以太网帧正确地收进来、发出去。UDP层不关心物理接口是RGMII还是GMII它只处理IP包和UDP协议字段。用户应用层更不关心底层网络细节它看到的只是AXI-Stream接口上的一个数据流。这种分层让每一层都能独立测试和替换也让我在学习的时候可以一次只关注一个模块。2.1 梳理verilog-ethernet文件结构别被仓库吓到我当时为了找到UDP收发到底用哪些文件把rtl目录翻了一遍。整体文件结构分为几个层次最底层是lib目录下的通用模块比如axis_fifo、async_fifo、sync_reset这些基础元件。再往上是MAC相关模块eth_mac_1g_axis负责以太网MACeth_rgmii_rx和eth_rgmii_tx负责RGMII物理接口的接收和发送。再往上是ARP和UDP层eth_arp处理地址解析协议eth_udp_rx和eth_udp_tx处理UDP包的收发。实际使用时如果不使用ARP缓存一个最简单的UDP收发系统可以只例化这些核心模块eth_mac_1g_axis、eth_udp_rx、eth_udp_tx、async_fifo以及RX/TX方向各一个axis_fifo。MAC输出的AXI-Stream数据先经过FIFO跨时钟域再进入eth_udp_rx解析。发送方向则相反eth_udp_tx打包好UDP帧后经过FIFO进入MAC模块发送。我建议初学者不要一开始就想着把所有模块都研究透先把数据流打通。也就是先忽略ARP细节直接把目的MAC地址固定配置好让UDP收发链路跑起来然后再回头研究ARP、协议细节这些进阶内容。2.2 UDP接收路径从网线到应用数据的“解包旅程”接收方向的完整链路是这样的PHY芯片把差分信号转换为RGMII格式的RXD[3:0]、RX_CLK、RX_CTL信号eth_rgmii_rx模块用IDDR原语在每个时钟的上升沿和下降沿各采样一次4位数据拼成8位字节流。然后eth_mac_1g_axis模块开始处理这个字节流它会检查前导码、SFD分隔符、目的MAC、源MAC、长度/类型字段校验FCS CRC。如果帧正确MAC模块会按8位到64位的数据位宽转换把整个以太网帧以AXI-Stream形式输出附带tvalid、tlast、tkeep这些信号。接下来数据进入eth_udp_rx模块。这个模块做的事情非常纯粹它检查以太网类型字段如果是0x0806说明是ARP包就交给ARP模块处理如果是0x0800说明是IPv4包就继续解析IP头。IP头默认20字节里面包含了源IP、目的IP、协议字段。如果协议字段是17即UDP协议再解析UDP头。UDP头8字节包含源端口、目的端口、长度、校验和。eth_udp_rx会检查目的端口是否和自己配置的端口匹配匹配的话把UDP负载部分通过AXI-Stream输出给用户逻辑同时输出解析出来的源IP、源端口等元数据。这个模块最方便的一点是它把各种头部分的剥离开销都做完了用户逻辑不用关心以太网帧格式。但你还是得理解这个过程不然接收时序、AXI-Stream的tlast信号怎么对齐都容易搞错。2.3 UDP发送路径从应用数据到网线的“打包旅程”发送方向是接收方向的逆过程。用户逻辑准备好要发送的数据通过AXI-Stream接口写入eth_udp_tx模块。eth_udp_tx会检查当前是否处于busy状态如果总线空闲它就开始构造以太网帧。第一步是发目标MAC地址、源MAC地址、以太网类型0x0800然后是IP头。IP头里的总长度、标识、校验和字段由模块自动计算。接下来是UDP头源端口和目的端口分别来自配置寄存器和输入数据。UDP头里还有一个校验和字段发送模块会一边发送负载数据一边进行增量校验计算在负载发送完之后把校验值写入帧尾。最后整个帧交给MAC层模块加上前导码和FCS CRC再通过RGMII发送到PHY芯片。一个容易忽略的细节是MAC层会根据帧长度自动做填充。以太网规定最小帧长是64字节如果UDP负载太短整个帧长度不足64字节MAC模块会在负载后面填充0。这个填充动作在接收端会被MAC当作填充数据剥掉所以用户数据从协议栈收出来还是原来的长度不会看到这些填充字节。3. 配置和接口细节MAC地址、IP、端口、时钟这些参数到底怎么设学习verilog-ethernet时参数配置是你第一个会遇到的实际问题。工程里很多模块都有类似parameter src_mac、src_ip、dst_mac、dst_ip的定义。刚开始我直接照搬示例的配置结果在板子上跑的时候发现数据根本发不出去后来才搞明白问题出在目的MAC地址上。PC的网卡只接收发给自己MAC地址的帧如果你的FPGA发送的帧里目的MAC不是PC网卡的MAC地址PC网卡会在硬件层面就把帧丢掉根本不会交给操作系统协议栈。所以最简单的办法是打开PC命令行输入ipconfig /all找到以太网适配器的物理地址然后把这个MAC地址固化到FPGA的dst_mac参数里。如果你用了ARP模块发送前会自动查询PC的MAC但你还是得先把FPGA自己的源MAC地址设成和PC在同一局域网内的值否则交换机不一定转发。IP地址也有讲究FPGA的src_ip要配成和PC同一子网比如PC是192.168.1.100FPGA就配192.168.1.50。目的IP自然是PC的192.168.1.100。子网掩码虽然不影响UDP收发模块但PC发送UDP包时如果目的IP不在同一子网会走网关而不是直连网卡所以测试时最好把FPGA和PC用网线直连避免交换机介入。3.1 MAC、IP、端口三个宏改值前先想清楚我看到有些开发板例程里以太网参数写死在顶层模块里改一次要查找替换好几处。verilog-ethernet的工程其实可以在顶层模块里定义parameter然后把参数传给UDP和MAC模块。修改前建议先给整个系统画一个表列出FPGA的MAC、FPGA的IP、PC的MAC、PC的IP、FPGA要监听的UDP端口、PC要发送的UDP端口、PC要接收的UDP端口、FPGA发送的目的UDP端口。这里容易踩坑的是端口的方向。FPGA如果要把数据发给PC的某个上位机软件目的端口必须和上位机绑定的端口一致。PC如果要把数据发给FPGA目的端口必须等于eth_udp_rx模块配置的监听端口。不要想当然地认为PC发送端口和FPGA接收端口肯定一样很多UDP调试助手默认在同一个本地端口上接收和发送但如果你用Python写socket或者使用特定上位机收发端口可能不同。3.2 跨时钟域为什么以太网工程一定有FIFO我在之前学FPGA时对跨时钟域处理的理解停留在“加两级触发器打拍”这个层面但到了以太网工程里这个办法没用。因为接收方向RGMII RX_CLK是由远端PHY提供的125MHz时钟而用户逻辑通常运行在另一个时钟域里比如200MHz的全局时钟两块数据必须通过异步FIFO才能可靠交接。verilog-ethernet在RX路径上通常放一个axis_fifoTX路径也放一个axis_fifo这样MAC和UDP模块之间的AXI-Stream信号就变成了异步FIFO的读写端口。我从这个工程里学到的经验是FIFO的深度不要用默认的小值至少要能容纳几个MTU大小的数据包。比如整帧最大1518字节如果数据位宽是64位FIFO深度做到2048个字是比较稳妥的。这样做的好处是即使网络有突发流量FIFO不会立刻溢出链路层重传或者背压的时间也足够。3.3 最容易掉进去的坑回环测试中PC网卡的ARP表我第一次做回环实验的时候总是发送不通。后来用Wireshark抓包才发现PC发了ARP请求询问“192.168.1.50的MAC地址是多少”但我FPGA工程里压根没接ARP模块所以PC根本不知道FPGA的MAC地址是多少UDP数据包因为找不到目的MAC而无法发送。解决这个问题的办法有两种。第一种是给PC静态添加ARP条目用管理员身份运行命令arp -s 192.168.1.50 aa:bb:cc:dd:ee:ff强制告诉PC这个IP对应哪个MAC。第二种是在FPGA工程里把eth_arp模块加上让FPGA能够自动响应PC的ARP请求。我推荐第二种因为更接近真实应用。但如果你只是想快速验证UDP通路第一种最省事。值得注意Windows系统的ARP缓存在一段时间后会自动过期有时需要重新添加。另外如果FPGA重启后MAC地址没变ARP缓存还能继续用如果MAC变了缓存里的旧条目就会导致数据发到错误的目标。4. 实操从零搭一个UDP回环工程并跑通理论知识说再多不如实际跑一遍。我这次实操的目标非常简单FPGA收到PC发来的UDP数据然后原封不动地发回给PC。用这个回环实验验证RX和TX两条通路是不是都正常工作。如果再跑通了我再把代码改成自定义数据处理逻辑。我先在Vivado里新建了一个工程芯片用XC7A35T。然后从verilog-ethernet仓库的rtl目录下把这些文件添加到工程rtl/eth_mac_1g_axis/eth_mac_1g_axis.v、rtl/eth_mac_1g_axis/eth_mac_1g_axis_fifo.v、rtl/eth_rgmii/eth_rgmii_rx.v、rtl/eth_rgmii/eth_rgmii_tx.v、rtl/eth_udp/eth_udp_rx.v、rtl/eth_udp/eth_udp_tx.v还有lib目录下的async_fifo、axis_fifo、reset_sync等通用模块。源码里某些模块会被重复引用Vivado会自动处理依赖但如果你是用命令行编译要注意包含顺序。顶层模块我命名叫udp_loopback_top里面例化了一个RX方向的FIFO、一个TX方向的FIFO、eth_udp_rx和eth_udp_tx然后直接用一对assign把RX输出的m_axis_tdata和tkeep接到TX输入的s_axis_tdata和s_axis_tkeep再把tvalid和tready做握手连接。逻辑上看起来很简单实际接线时要严格对齐AXI-Stream信号的时序关系。比如只有tvalid和tready同时为高时才传输数据tlast在整帧最后一个周期拉高tkeep指示最后一周期的有效字节数。4.1 顶层模块搭建先用回环把数据流打通顶层模块的关键例化代码逻辑是这样的。eth_udp_rx解析出UDP负载后把数据送到axis_rx_fifo缓存然后回环逻辑从fifo读出写入eth_udp_tx。这里有个重要的背压关系如果eth_udp_tx因为忙于发送上一帧而拉低s_axis_tready那回环逻辑就必须停下从fifo读数据fifo满了之后eth_udp_rx的m_axis_tready被拉低接收通路自然被阻塞。这样整个系统的流量控制就串起来了。我犯过的错误是把模块的复位信号直接接到了全局复位而没有做异步复位同步释放处理。在以太网工程里复位信号至关重要尤其是MAC模块在复位释放后需要重新同步到RX_CLK和GTX_CLK如果复位不同步模块状态机可能跑飞。verilog-ethernet自带sync_reset模块顶层一定要用这个模块生成各个时钟域的复位信号。4.2 回环逻辑与AXI-Stream握手细节看起来简单但容易写错回环逻辑听起来就是“收到什么发什么”但如果你直接写一个always块把RX数据赋给TX数据多半会有时序问题。因为RX路径上的AXI-Stream主端模块和TX路径上的AXI-Stream从端模块都有自己的一套tvalid和tready握手信号必须一套完整的组合逻辑把读端口和写端口对接。我的做法是用一个简单的状态机空闲状态等待输入FIFO非空读使能拉高从输入FIFO读出一个周期数据写使能拉高把数据和tlast、tkeep一起写入发送FIFO如果这一帧数据还没结束就继续读如果tlast为高就说明一帧结束回到空闲状态。这个状态机非常单薄但能把tvalid/tready的时序搞得清清楚楚。写FIFO时如果发送端一直拉低tready状态机就要在等待状态里停住不能丢掉数据。这里还要注意位宽对齐。eth_udp_rx输出的AXI-Stream数据位宽默认是64位也就是每个周期8字节。PC发的UDP负载长度不一定是8的倍数所以tkeep信号会指示最后一拍有几个字节有效。回环逻辑不能只看tdata还必须原样传递tkeep否则发送端会把无效字节当作有效数据打包导致对端收到的数据长度不对。4.3 上板验证流程上位机配置和波形实测工程综合、布局布线之后把bit文件下载到FPGA开发板。开发板的以太网PHY型号一般是RTL8211E或88E1512这些PHY在上电后需要一段时间完成自协商等链路建立后RGMII接口的时钟才稳定。所以我顶层里做了一个上电延时计数器延时几百毫秒后再释放以太网复位确保PHY初始化和时钟稳定。我的PC端使用一个简单的UDP调试工具先把本地IP设为192.168.1.100网关不用填。FPGA的IP在Verilog里配置为192.168.1.50监听UDP端口5000发送目的IP是192.168.1.100目的端口也是5000。测试时我在调试工具里输入“hello fpga”并发送然后观察接收窗口。如果回环逻辑正常工作几毫秒内就能收到一模一样的“hello fpga”。如果收不到第一时间要用Wireshark在PC网卡上抓包看PC是否发出了UDP包是否收到了ARP回复。我这次实际测试的时候第一次就是Wireshark看到了PC发出的UDP包但FPGA没有返回任何包。排查后发现是PHY芯片复位时间不够RX_CLK没有稳定MAC模块收到的全是乱码。把复位延时加长后问题就解决了。5. 常见问题与排查技巧收不到包、长度不对、偶尔丢包以太网调试最痛苦的现象是“明明代码看起来没问题但就是收不到数据”。我整理了这张速查表可以帮你定位问题。现象可能原因检查方法PC发UDPFPGA完全没反应FPGA的MAC地址没被PC学习ARP解析失败Wireshark抓包看PC有没有发ARP请求FPGA有没有ARP应答PC收不到FPGA发送的数据FPGA发送帧的目的MAC写错了抓包确认FPGA发出的以太网帧目的MAC是否为PC网卡MAC收到数据长度不对AXI-Stream的tkeep没有正确传递检查回环逻辑是否原样传递tlast和tkeep偶尔丢包FIFO深度不够突发流量溢出增大FIFO深度或优化背压逻辑数据内容错乱RGMII时钟相位问题或者PHY芯片工作模式不对查看MAC复位时序、PHY配置寄存器5.1 收不到数据的排查顺序如果收不到数据我的习惯是先抓包再查MAC再查PHY状态最后才查FPGA内部逻辑。因为PC端的网络配置和网卡驱动问题在FPGA开发中非常常见先排除环境因素能省很多时间。第一步在PC上抓包看PC是否把UDP报文发出来了。如果没发出来说明PC的ARP表里没有FPGA的MAC条目或者防火墙拦截了ICMP/UDP报文。如果PC发了UDP但FPGA没反应就要检查RGMII时序。用逻辑分析仪或Vivado的ILA抓FPGA侧RGMII引脚上的RX_CLK和RXD信号确认PHY是否输出了有效的网络波形。如果RX_CLK根本没有那大概率是PHY复位或配置出了问题比如MDIO接口没有正确配置PHY工作方式。第二步检查MAC层状态。用ILA抓eth_rgmii_rx模块输出的8位数据和MAC模块输出的AXI-Stream信号看看有没有完整的一帧数据。如果MAC收到了帧但UDP模块没输出说明问题在UDP层比如目的端口不匹配、IP协议字段不是UDP、或者帧里的Type字段不是0x0800。5.2 组织链路层数据时的常见错误在修改UDP发送端代码时不少人会手动构帧但这几个错误可以说是“经典三连”。第一忘记填充最小帧长导致MAC模块报错或者PHY发不出去。如果不用MAC模块自动填充自己发短帧时要在数据后补0到60字节加上4字节FCS才是最小64字节。第二IP头里的总长度字段没算对它包含IP头本身20字节加上UDP头8字节加上负载长度。第三UDP校验和不计算或计算错误。虽然很多接收端不校验UDP校验和但PC的某些网络驱动会直接丢弃校验错误的包。verilog-ethernet里eth_udp_tx模块会自动处理这些字段但如果你参考它的代码做二次开发一定要理解这些细节。5.3 如何用Wireshark定位“假通”我在测试时遇到过一个很有意思的场景PC用ping命令能ping通FPGA但UDP数据死活不通。表面上看“网络是通的”但实际上ping走的是ICMP协议它和UDP的帧结构完全不同。很多FPGA工程只实现了ICMP应答模块并没有实现UDP解析。所以ping通只能证明MAC层和IP层的接收路径是好的不能证明UDP层能处理数据。用Wireshark抓包后你会发现PC发出的UDP包确实到达了网卡FPGA也收到了但FPGA没有回应。问题大概率出在eth_udp_rx的目的端口匹配或者ip数据包的校验上。所以不要迷信“能ping通”要以UDP回环实验结果为准。同样如果你看到FPGA发送的UDP包在Wireshark里标着“checksum offload”或者“incorrect checksum”那很可能是PC网卡的硬件事务卸载功能在干扰并不代表网络真的有问题。最后的几点感受verilog-ethernet这个工程让我真正理解了“协议分层”在硬件上怎么落地。之前用单片机做以太网时TCP/IP协议栈是软件库帮你搞定的你不需要考虑MAC帧的细节。但在FPGA里所有东西都得用逻辑描述出来这逼着你去读协议文档、去抠每一个时序细节。也正是这个过程让我对以太网的理解比之前任何时候都更扎实。如果你也是刚开始学FPGA以太网我建议第一步不要直接上TCP先从UDP回环做起把MAC、UDP、RGMII这条链路打通了再慢慢加ARP、DHCP、TCP这些进阶功能。verilog-ethernet的源码写得非常规整遇到不懂的模块直接打开源代码跟着数据流看比看任何教程都管用。另外说一个后来对我帮助很大的习惯每次做以太网相关的实验我会先在PC端抓一次包记下正常的收发时序和帧内容然后再改FPGA代码。这个“黄金样本”能让你在改代码出错时快速定位是环境变化还是逻辑错误省下不少排查时间。