尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

FPGA UDP通信模块设计:从零实现以太网高速数据传输

发布时间:2026/9/9 9:16:33

资讯中心
01
ARTICLE

FPGA UDP通信模块设计:从零实现以太网高速数据传输

FPGA UDP通信模块设计:从零实现以太网高速数据传输
1. 为什么先把UDP跑通再谈其他我一直觉得FPGA开发这件事卡住大多数人的不是那些玄乎其玄的算法也不是什么高深的时序收敛而是数据到底怎么从板子出去又怎么从外面进来。前面几篇我们聊了按键、LED、串口、FMC通信这些基础模块到了这一篇终于要碰一个稍微有点“网络味”的东西了——UDP模块的代码设计。在做FPGA图像处理、高速数据采集这类项目时UDP几乎是绕不开的选项。原因很简单TCP虽然可靠但状态机复杂、内存开销大、回包确认机制在FPGA里写起来非常痛苦而UDP协议栈足够简单处理延迟低带宽利用率高特别适合做实时性要求高的数据传输。如果你把FPGA当成一个高速数据采集前端需要把ADC采到的数据、或者摄像头过来的图像数据丢给上位机处理UDP往往就是那个“性价比最高的搬运工”。这篇博客面向的读者是那种“近似0基础”但已经跟着系列文章走过来的朋友。你不需要懂Linux协议栈不需要会写C#上位机甚至不需要太深的Verilog功底只要会写简单的状态机能看懂时序图就能跟着我把一个能用的UDP模块搭起来。我会尽量把每一步为什么这么做讲清楚而不是直接甩一段代码让你去抄。2. 动手之前把UDP帧格式彻底吃透2.1 帧结构拆解你要发出去的东西到底是什么样很多新手上来就找代码找到一段UDP发送的Verilog就粘进去结果上板之后上位机收不到数据也不知道从哪里排查。我建议你先花半小时把UDP帧格式搞清楚后面写代码会顺畅很多。UDP数据在以太网上传输时实际是分层的。你要发一个UDP数据报它会被塞进IP数据包里IP数据包又会被塞进以太网帧里。如果只从FPGA侧看你得在发送端把这三层东西手动组出来也就是一个完整的以太网帧长这样字段长度字节内容说明前导码7同步用一般由PHY芯片自动处理帧起始定界符10xD5PHY自动处理目的MAC地址6对端设备MAC源MAC地址6本板MAC以太网类型20x0800表示IPV4IP头20版本号、总长度、协议号、源IP、目的IP等UDP头8源端口、目的端口、UDP长度、校验和UDP数据N你要传的有效数据FCS4帧校验序列CRC32一般由MAC侧处理看到这里你会发现FPGA发送UDP时MAC层地址和FCS其实可以交给MAC核或者PHY芯片去处理但IP头和UDP头必须自己组。也就是说你真正要关心的是从“目的MAC地址”到“UDP数据”这一整段这段也叫“MAC帧”。2.2 为什么UDP校验和可以填0但CRC必须算对这里有个特别容易让新手混淆的点UDP头里有一个校验和字段IP头里也有一个校验和字段很多教程说“UDP校验和为0表示不校验”于是有人就顺手把所有校验和都填0结果发现包发出去了上位机也能收到就以为万事大吉了。实际上UDP和IP的校验和是否填0要看你的使用场景。在以太网这种本身就带CRC32链路校验的环境里UDP校验和填0完全合法Windows和Linux的socket默认也能接收。但如果你将来要跨网段路由或者某些路由器做了严格校验校验和为0的包可能被丢掉。所以我的建议是IP头校验和老老实实算UDP校验和可以先填0跑通功能后面有空再补上。FCS帧校验序列则由MAC核的发送引擎自动追加不需要你在用户逻辑里自己算。但前提是你用的是Xilinx或Altera官方的MAC IP核并且配置了自动生成FCS。如果你完全用纯逻辑做RGMII接口而没有经过MAC核那CRC32就得自己算这个我后面会单独说。3. UDP发送模块设计与实现3.1 顶层模块划分与时钟复位方案我第一次写UDP发送模块时觉得只要写一个发送状态机把数据搬出去就行结果把模块写得又臭又长。后来重构了几版总结出一个比较清晰的模块划分方式先给你看一下udp_tx_topUDP发送顶层模块负责对外接口与内部子模块例化udp_tx_ctrlUDP发送控制状态机负责组帧与状态跳转udp_tx_data用户数据缓冲RAM负责缓存上位机写入的待发送数据udp_crc32CRC32计算模块如果使用MAC核自动FCS则不需要时钟方案上我用的是125MHz的PHY接口时钟。如果PHY工作在1000MbpsRGMII接口的时钟是125MHzDDR双边沿采样。如果你的板子PHY没有提供125MHz时钟也可以用自己的PLL从50MHz倍频上去。复位方案我强烈建议用异步复位、同步释放避免单纯异步复位带来的亚稳态问题。这个在时序收敛上不一定看得到明显区别但在长时间运行的稳定性上有实打实的影响。3.2 发送状态机到底怎么拆才不乱UDP发送状态机是这整个模块的灵魂。我见过很多新手一上来就把整个UDP帧的发送放在一个状态机里状态数量多到二十几个信号满天飞最后查错查到头秃。我自己现在用的是“三段式状态机数据计数”的方式核心思路很简单状态机只负责控制帧头各字段的依次发送数据部分用单独的计数器控制。帧头发送完了数据阶段按地址从RAM里读数据读多少个字节由你这次发送的长度决定。举个具体的例子发送状态机状态划分如下localparam IDLE 4d0; // 空闲等待发送请求 localparam PRE_MAC 4d1; // 发送目的MAC(6B) 源MAC(6B) 类型(2B) localparam PRE_IP 4d2; // 发送IP头(20B) localparam PRE_UDP 4d3; // 发送UDP头(8B) localparam SEND_DATA 4d4; // 发送有效数据 localparam WAIT_END 4d5; // 等待FCS处理完成你可能会问为什么IP头不拆成好几个状态因为IP头的20个字节在发送时是连续输出的拆得越细状态机越繁琐出错概率越大。这里的关键是状态机只管整段不管逐字节逐字节操作全部用计数器实现。那么23个字节怎么一次发出去方法是状态机进入PRE_IP后启动一个计数器从0数到19每个时钟周期根据计数器当前值去查一张IP头部的常量表用case语句实现把对应字节放到数据总线上。这样一个状态就能搞定20个字节的输出代码量少逻辑还清晰。3.3 用户数据缓冲RAM怎么设计数据缓冲这块有一个新手很容易踩的坑直接用FIFO缓存然后在发送数据阶段一边从FIFO读一边往MAC发结果FIFO读空的时候数据总线出现了空拍帧就断了。UDP帧是一个连续的数据流中途不能出现空闲周期一旦出现空隙MAC核会认为帧结束后面对端解析必然出错。所以我更推荐用双口RAM做缓冲而不是FIFO。具体做法是上位机或者你FPGA内部的逻辑先把待发送数据写入RAM的某个地址区间写完以后给udp_tx_ctrl一个发送请求信号状态机进入发送流程后从RAM里按地址顺序把数据读出来发出去。RAM的读地址在每个数据有效的时钟周期递增直到达到你设置的发送长度减1。这样做的好处是RAM的数据在发送过程中是稳定存在的不会出现FIFO读空的问题。坏处是你要自己在逻辑里管理“写入完成”和“读取完成”这两个时序点。我一般用一个tx_req信号触发发送tx_done信号表示发送完成写侧的逻辑在检测到tx_done后再写入下一包数据。还有一个细节RAM的读延迟。如果你的RAM IP核配置了输出寄存器Output Register读地址给出去之后数据要过一拍甚至两拍才出来。这时候你发数据的时序就要把读延迟算进去否则地址变了数据还没跟上。如果不确定先把RAM的输出寄存器关掉走最简单的组合逻辑输出模式等通了再优化性能。3.4 发送模块的关键代码逻辑为了让你有一个直观的参考我贴一段发送状态机中“UDP头发送”部分的简化代码。注意这里为了排版做了删减实际工程里还要加上字节计数、输出数据选择等逻辑。// UDP头发送: 源端口(2B) 目的端口(2B) 长度(2B) 校验和(2B) // 其中UDP长度 8 数据长度 always (*) begin case (udp_cnt) 4d0: data_out src_port[15:8]; 4d1: data_out src_port[7:0]; 4d2: data_out dst_port[15:8]; 4d3: data_out dst_port[7:0]; 4d4: data_out udp_length[15:8]; 4d5: data_out udp_length[7:0]; 4d6: data_out 8d0; // 校验和高字节先填0 4d7: data_out 8d0; // 校验和低字节 default: data_out 8d0; endcase end这段代码的逻辑很简单udp_cnt在状态机进入PRE_UDP时从0开始递增每周期加1数据总线根据计数器选择对应字节输出。这里udp_length不是固定的要根据外部传入的数据长度实时计算udp_length 8 tx_data_len。3.5 一个关键信号发送请求与发送完成的握手UDP发送模块和外部逻辑之间最核心的接口就是“请求-完成”握手。我在实际项目中通常这样定义tx_start外部逻辑拉高一个周期请求发送一帧UDP数据tx_len[15:0]本次发送的有效数据字节数最大不超过1472以太网MTU 1500减去IP头20字节再减去UDP头8字节tx_done发送完成拉高一个周期握手时序上我碰到过一个问题上位机连续高速发数据时外部逻辑在上一帧还没发完时又拉高了tx_start导致状态机IDLE阶段被重复触发帧数据错乱。解决办法是加一个tx_busy信号外部逻辑只有在tx_busy 0时才能发起新的发送。这个tx_busy在状态机进入第一个非IDLE状态时拉高回到IDLE时拉低。4. UDP接收模块设计与实现4.1 为什么要接收接收模块要做什么有些项目只需要FPGA往电脑发数据不需要接收但大多数实际场景要求双向通信上位机发一个配置参数过来FPGA收到后改变工作模式或者上位机发一个“开始采集”的命令FPGA才开始回传数据。所以一个完整的UDP模块接收侧是必须的。接收模块做的事情可以拆成三步第一从MAC核的接收通道拿到有效的以太网帧数据第二解析以太网帧头识别出IPV4类型第三解析IP头识别出UDP协议第四剥掉以太网头、IP头、UDP头把有效载荷交给用户逻辑。用代码实现时这个“识别-剥头”的过程是一个接收状态机。需要注意的关键点在于数据什么时候有效以及怎么根据头部的长度字段判断有效数据从哪里开始。4.2 接收状态机的状态划分接收状态机相比发送状态机要简单一些因为接收方向的数据是外部进来的时序由MAC核给你只需要跟着数据有效的节奏走localparam RX_IDLE 3d0; // 等待帧开始 localparam RX_MAC_HDR 3d1; // 接收MAC头(14B) localparam RX_IP_HDR 3d2; // 接收IP头(20B) localparam RX_UDP_HDR 3d3; // 接收UDP头(8B) localparam RX_DATA 3d4; // 接收有效数据 localparam RX_CHECK 3d5; // 检查结束和发送状态机一样每个状态内部用一个计数器控制接收的字节数计数器到达预定值就跳转到下一个状态。这里有个经验在RX_MAC_HDR状态中要判断接收到的以太网类型字段是否为0x0800。如果不是比如是ARP帧类型是0x0806那你应该直接跳到RX_CHECK把整个帧丢弃而不是傻乎乎地把ARP的数据当成IP数据处理。同样在RX_IP_HDR状态中判断IP头里的协议号是否为17UDP协议号不是就丢弃。4.3 接收侧的数据缓存策略接收侧的数据缓存很多人的第一反应是“每收到一个字节就往FIFO里写一个字节”。这个做法本身没问题但要注意一个问题你无法提前知道这包数据有多长。UDP头里有一个UDP Length字段这个字段的值包含了UDP头8字节加数据长度。也就是说你在接收UDP头的时候就能算出来后续还有多少数据要收。但问题是当你把数据往FIFO写的时候如果同时有别的逻辑在读这个FIFO那么数据到达和读走的时机就会耦合在一起容易出错。我常用的方案是接收侧数据先写入一个双口RAM等一整帧收完了再触发用户逻辑去读这帧数据。这样做的好处是接收和后续处理之间有一道天然的缓冲隔离帧与帧之间不会相互覆盖。双口RAM的写地址在接收数据时递增写完一帧后把当前写地址记录下来作为“数据帧结束标志”用户逻辑检测到新帧后按地址区间读取。4.4 要不要做CRC校验如果使用的是MAC核接收侧FCS默认是自动剥离并校验的如果CRC错误MAC核会报错或者干脆不把数据发给用户逻辑。所以你在用户逻辑里通常不需要自己算接收方向的CRC32。但我遇到过一种情况FPGA直接连PHY芯片没有使用MAC核用纯逻辑实现了RGMII接口。这种情况下接收方向拿到的原始数据里包含FCS 4字节需要自己校验。不要急着算CRC建议先用Wireshark抓包确认帧的完整性和对齐方式再看CRC算法端口的实现是否正确。很多时候“CRC校验失败”其实是字节序没对齐数据整体错位了不是CRC算法本身写错了。5. ARP应答模块让上位机找得到你5.1 为什么没有ARP你的UDP包发不出去很多新手写好了UDP发送模块兴冲冲地上板测试结果上位机软件那边一直显示“接收不到数据”。排查半天发现是ARP没处理。简单解释一下ARP是干什么的以太网通信靠的是MAC地址寻址但你的上位机软件只知道FPGA的IP地址比如192.168.1.10不知道这个IP对应的MAC地址是什么。于是上位机在发送UDP数据包之前会先广播一个ARP请求“谁的IP是192.168.1.10把你的MAC地址告诉我。”如果你的FPGA不回应这个ARP请求那上位机就永远不知道把UDP包发到哪个MAC地址去。所以要想让UDP通信能通你必须先把ARP应答做了。这件事在纯软件环境下是操作系统自动处理的但在FPGA里没人帮你处理必须自己写逻辑。5.2 怎么用最少的代码实现ARP应答ARP应答模块的核心逻辑是收到ARP请求后判断请求里的目标IP地址是不是自己的IP如果是就回一个ARP应答报文把自己的MAC地址告诉对方。ARP请求和应答的报文结构是固定的28字节算上以太网帧头14字节一共42字节。FPGA这边只需要把请求里对方的MAC地址、IP地址取出来交换位置填上自己的MAC和IP回发出去即可。我实现ARP时没有单独做很复杂的状态机因为ARP报文短且固定我用一个小状态机加上一个“以太网头解析”的状态就够了。核心判断有两点帧类型是否为0x0806ARP以及ARP请求里的目标IP是否等于本机IP。两个条件都满足立刻组装ARP应答帧发出去。有一个坑很多上位机软件比如我自己常用的网络调试助手在打开的时候只会发一次ARP请求如果FPGA当时正在忙没来得及回复那么后面可能就一直不通。所以ARP应答模块必须在系统里拥有“最高优先级”不论当前UDP发送状态机在干什么只要检测到有效的ARP请求就要立刻打断当前发送转去回ARP应答。具体做法是ARP应答逻辑单独写成一个小模块它的发送请求信号优先级高于UDP发送请求。也就是说发送仲裁逻辑在处理“到底发什么帧”的时候ARP请求来了直接先发ARP应答发完再恢复之前的UDP发送。6. 上板调试与验证从零到能传数据6.1 用什么工具验证UDP通信代码写完了最终要上板验证。我自己的调试工具链一般是三层板级调试ILA→ 软件调试Wireshark→ 回环测试网络调试助手。先说ILA集成逻辑分析仪。在UDP发送模块的关键信号上加上ILA探针包括tx_state、tx_cnt、data_out、tx_done等用ILA抓到真实的波形确认状态机跳转和字节计数是否符合预期。这一步可以帮你把“FPGA侧逻辑错误”和“网络通信错误”隔离开来。然后是Wireshark抓包。把FPGA的网口接到电脑或者通过交换机电脑上打开Wireshark选择对应的网卡开始抓包。如果你能从抓包里看到FPGA发过来的UDP包并且包结构解析正常那恭喜你FPGA侧基本没问题了。如果抓不到包优先查PHY芯片的链路状态、RGMII时序、MAC核配置这三样。链路状态可以通过PHY芯片的状态寄存器读取也可以通过查看网口指示灯判断但注意链路指示灯亮不代表数据收发正常只代表物理层协商成功了。最后是网络调试助手。Wireshark能看到包结构但看不到业务数据的正确性。用网络调试助手或者你自己写的Python脚本收包连续接收FPGA发来的数据比对数据内容是否为预期序列。比如FPGA发送0x00到0xFF循环递增的数据上位机接收后检查是否有跳变、重复、错乱。6.2 常见问题排查速查表我把做UDP模块时碰到过的高频问题整理成一个表方便你对照排查现象可能原因排查思路Wireshark完全抓不到包PHY芯片未完成初始化读取PHY寄存器确认link up检查复位时序和配置引脚抓到的包长度不对发送状态机提前结束ILA抓取发送状态确认字节计数是否准确上位机收不到但Wireshark能抓到目的MAC地址填错导致交换机不转发检查目的MAC地址是否对端网卡的MAC收到数据但内容错乱RAM读地址与数据延迟不匹配检查双口RAM读延迟配置调整读地址时序发一个包要等很久ARP表未建立确认ARP应答模块是否正确回复用Wireshark看ARP交互千兆下频繁丢包跨时钟域处理不当检查MAC核接口时钟、FIFO的异步处理是否正确6.3 连续高速发送时怎么保证不丢数据UDP模块做到能收发只是第一步。真正项目中你往往需要FPGA以比较高的速率连续往电脑传数据。比如我做过的一个图像采集项目FPGA以100MB/s的速率向电脑传输图像数据持续几十分钟不能丢包。这个要求听起来简单实际做起来有几个坑第一个坑是CPU处理不过来。如果你的电脑上运行的是弱鸡上位机软件单线程接收上千兆数据很容易因为socket接收缓冲区溢出导致丢包。解决思路有三条上位机开大接收缓冲区在Windows下可以用setsockopt设置SO_RCVBUF上位机开多线程处理一个线程收包一个线程处理数据降低发送速率加一个简单的发送节流机制比如每发128个包等几微秒。第二个坑是FPGA内部的回压机制缺失。如果你的FPGA内部数据产生速度大于发送速度而你又没有做流控那么RAM迟早被写满新数据就会覆盖旧数据。这个问题在FPGA侧必须解决不能指望上位机。我自己比较喜欢的做法是在发送模块里加一个“FIFO水位线”指示器当缓冲区的数据量超过某个阈值时拉高背压信号上游模块据此减少数据产生速率。第三个坑是MAC核的内部FIFO溢出。Xilinx的Tri-Mode Ethernet MAC核内部有发送FIFO如果你的用户逻辑写数据的速度超过MAC核发送的速率FIFO溢出会导致帧被丢弃。解决办法是在FIFO快要满的时候暂停写数据或者在MAC核配置里把FIFO深度调大。6.4 一个完整的验证流程示例我习惯按照下面这个流程来验证UDP模块整个流程走一遍大概两三个小时能覆盖大部分功能点和边界情况第一阶段单项功能验证。先在FPGA内部写一个简单的数据发生器比如发送0x00到0xFF循环的256字节数据。上位机用一个简单的UDP接收脚本Pythonsocket即可接收并校验数据是否正确。第二阶段连续压力测试。把PC和FPGA用网线直连FPGA持续发送数据上位机用Python脚本连续接收统计接收到的包数量和数据正确率。这个阶段主要检测模块在高负载下会不会出现状态机卡死、RAM溢出等问题。第三阶段双向通信验证。上位机向FPGA发送不同的命令字FPGA收到命令后改变发送数据的长度、内容、速率以此来验证接收模块和发送模块的联调是否正常。第四阶段跨交换机测试。把FPGA通过交换机接到电脑上打开Wireshark观察ARP交互和UDP数据包确认跨交换机环境下模块也能正常工作。这一步还会暴露出MAC地址、IP地址配置是否合理的问题。7. 再说几句关于“学习路线”的大实话UDP模块写完以后你会发现FPGA开发中很多模块的思路是相通的状态机负责控制流程、RAM/FIFO负责数据缓存、握手信号负责模块间协调、CRC负责数据完整性校验。这套方法论几乎可以套用到PCIe、MIPI、LVDS这些高速接口的开发中。我个人在实际调试UDP模块时最大的体会是网络通信类模块的调试最大的障碍不是代码逻辑本身而是你没法“看见”数据在线上是怎么走的。相比串口那种一个字节一个字节地看以太网的数据速率太快人眼根本跟不上所以必须借助工具。ILA看波形、Wireshark看协议结构两个工具配合使用能把问题定位的时间从几天缩短到几个小时。另外如果你在学习过程中发现某个波形怎么调都不对我建议你先停下来去把以太网协议那几页文档重新翻一遍。很多时候你觉得代码写得没问题其实是对协议的理解出了问题——比如字段顺序、字节序、长度的计算方式这些细节错一个整个包就是废的。最后分享一个小技巧调试UDP时我习惯在板卡上保留一组LED作为“状态指示灯”——比如发送一帧包就翻转一次LED电平收到一帧包也翻转一次。虽然看起来土但在没有调试环境的情况下它能帮你快速判断模块在不在工作比看波形快多了。这个习惯我一直保留到现在推荐你也可以试试。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。