FPGA实现2.5G UDP高速通信:自研MAC与协议栈实战指南 📅 发布时间:2026/9/17 5:37:18 👁 浏览次数: 如果你玩过一阵FPGA的高速接口应该会有种感觉千兆以太网已经快要变成“基础操作”真正让人头疼的是怎么把数据速率往上推。2.5G这个档位很有意思它不像10G那样对板材、连接器和时钟要求苛刻又比1G多出实实在在的带宽。最近我在一个数据采集项目里需要把ADC的原始码流以超过1Gbps的速率上送到上位机权衡一圈之后选了Xilinx的1G/2.5G Ethernet PCS/PMA IP核配合Verilog自研的MAC和UDP协议栈最终跑通了2.5G UDP通信。这篇文章把整个实战过程包括IP核配置、自研协议栈设计、代码结构、板级调试方法以及我踩过的坑完整记录下来。1. 为什么是2.5G方案选型与链路架构1.1 1G不够、10G太贵的折中点先说项目背景。ADC输出的数据率大约是1.4Gbps用1G以太网根本传不完DDR缓冲最多撑几十毫秒所以“先缓存、再补传”这个思路在我这里行不通必须实时上送。直接上10G不是不行但10G的PHY芯片、SFP光模块、高速SerDes布线、供电和散热成本都高一个数量级。更重要的是10G以太网MAC的IP授权和复杂度也大而2.5G这个速率正好卡在“简单高速串行收发器”能覆盖的范围里。2.5G以太网通常有两种物理形态一种是用支持2.5G SGMII的PHY芯片通过差分线把PCS和PMA分离另一种是直接走2500BASE-X光模块用FPGA的GTX/GTH收发器驱动SFP/SFP。我的方案选了第二种FPGA内部用1G/2.5G Ethernet PCS/PMA IP核完成PCS/PMA功能省掉外部PHY布线和物料都简单很多。如果你手头只有普通千兆PHY那么2.5G SGMII这条路大概率走不通因为多数百兆/千兆PHY的PMA不支持2.5G速率必须在选型阶段确认PHY手册里明确写了“2500 Mbps SGMII”或“2.5G SerDes”。1.2 PCS/PMA IP核在链路里的角色1G/2.5G Ethernet PCS/PMA IP核简单说就是把8B/10B编解码、时钟数据恢复、串并转换、自协商、链路状态同步这些脏活累活包掉。它解决的是“MAC层的并行数据怎么变成物理链路上一对差分信号”。如果外部有PHY芯片IP核工作在SGMII模式只实现PCS部分PMA放在外面如果直连光模块IP核工作在2500BASE-X模式PMA也集成进FPGA的收发器里。这两种模式的配置在IP核定制界面上有明确选项但很多人会在“应不应该配置成MAC模式”这个问题上纠结。我的建议是PCS/PMA IP核永远不包含MAC它不解析以太网帧不做CRC32不做地址过滤。MAC层逻辑要么用Xilinx单独的Ethernet MAC IP要么像我这个项目一样用Verilog自研。热搜词里“sgmii ip核与phy芯片一起使用时应配置成mac模式”这个说法其实是被某些资料误导了准确讲是“当SGMII IP核与外部PHY芯片配合时FPGA内部可能需要一个MAC但MAC不在PCS/PMA IP核里”。1.3 自研MAC vs 现成MAC IP既然IP核不提供MAC那MAC怎么来Xilinx有提供1G/2.5G Ethernet MAC IP它能把帧组装、CRC、帧间隔管理都做了处理器侧通常是AXI4-Stream接口。但我这个项目最终选择用Verilog自研MAC原因是第一我对需要发出去的UDP包有很强的定制需求比如特定的帧头、特定的发送节拍第二自研可以完全控制时序不依赖IP内部的黑盒行为第三调试起来能直接看到每一拍信号的变化。当然代价是工作量大CRC校验、状态机、字节序转换都需要自己处理。如果你赶项目进度并且只是想把数据从A点搬到B点直接用现成MAC IP更稳妥。但如果是学习或想完全掌握链路自研一遍收获完全不同。整体链路架构用文字描述就是发送方向用户逻辑产生UDP负载写入发送FIFOTX MAC从FIFO读出数据填充MAC头、IP头、UDP头计算校验和和帧CRC然后按照MAC帧格式把数据送到PCS/PMA IP核的并行接口IP核完成8B/10B编码、并串转换从GTX收发器的TX差分对发出去。接收方向GTX的RX差分对进来IP核完成串并转换、8B/10B解码、时钟修正把并行数据送到RX MACRX MAC检测帧头、验证CRC、解析MAC/IP/UDP头决定是否接收负载接收到的UDP负载写入接收FIFO再交给后续处理。这条链路只要定义清楚后面写RTL就会很顺手。2. 搭建最小系统PCS/PMA IP核配置与时钟复位设计2.1 配置过程中的关键选项我用的是Vivado里的1G/2.5G Ethernet PCS/PMA IP核不同版本菜单会有些差异但核心配置项是那些。先选“Shared logic in core”这样收发器的GTXE2_CHANNEL、GTXE2_COMMON都放在IP内部顶层会干净很多。接着选“Line Rate”为2.5G如果项目需要兼容千兆可以勾选“Support 1G/2.5G dynamic switching”但动态切换会多出一些控制信号固定速率更简单。如果是接外部PHYPhysical Interface选SGMII如果是接光模块选1000BASE-X/2500BASE-X然后在下面打开“Support 2.5G”。这里容易踩坑1000BASE-X和2500BASE-X在8B/10B编码层面一致差异在自协商机制和速率选错会导致link up不了。对于外部PHY模式还要注意IP核侧可以设置“MAC/PHY interface”为“GMII”还是“SGMII”。我实测下来外部PHY如果本身就带SGMII接口FPGA这边用SGMII点到点连接不经过GMII能够把接口频率降到125MHz/312.5MHz时序压力小很多。如果是用GMII到外部PHY数据位宽变大但速率低实际上2.5G的GMII很少见因为线数太多。所以我的经验是能走SGMII就走SGMII能少一组差分线就少一组。2.2 时钟树的来龙去脉IP核的时钟是比较容易懵的部分。核心输入是gtrefclk即参考时钟接收发器的REFCLK引脚SGMII和2.5G BASE-X一般都要求125MHz这个频率。FPGA的GTX/GTH收发器内部会通过QPLL/CPLL把125MHz倍频到线速率需要的时钟。IP核会输出txoutclk和rxoutclk这两个时钟是收发器恢复出来的不能直接给用户逻辑用通常要经过一个MMCM/PLL产生userclk作为并行数据通路的时钟。我当时用的数据位宽是8bit所以2.5G速率下的userclk是312.5MHz。如果你的逻辑在312.5MHz下时序收敛有困难可以考虑把用户侧数据位宽配置成32bit或64bit但IP核本身提供的是8bit接口你需要在内部把8bit拼成32bit或64bit再进协议栈。这个过程一定要用异步FIFO或者寄存器打拍处理避免跨时钟域问题。时钟顺序上一定要等gtrefclk稳定、QPLL/CPLL锁定之后再释放IP核复位。很多人在仿真里看不到问题因为仿真时钟瞬间稳定但板上上电瞬间参考时钟可能有一段不稳会导致link up失败。我习惯用一个小状态机监测mmcm_locked和gt_reset_done都拉高后再等100us然后释放用户逻辑复位。2.3 复位与Status向量解读IP核的复位接口有个“复位完成”信号通常叫gtwiz_reset_tx_done或者tx_reset_done另外还有rx_reset_done。这两个信号都拉高才代表收发器初始化完成。很多调试问题都出在只看其中一个或者根本没用复位完成信号。IP核还会输出一组status向量里面包含rx_block_lock接收端8B/10B块锁定的标志链路协商完成后应该为1。tx_axis_tready有效但rx侧没有数据过来就要看rx_block_lock是不是一直抖。rx_byte_is_aligned、rx_byte_is_character等用于判断是否对齐到帧边界。我建议在调试初期把core_status[0]对应rx_block_lock直接引到ILA然后观察它在复位释放后的行为。正常情况下应该从0变1后稳定。如果它一直跳变要么是线路没接对要么是差分极性搞反了此时可以试着配置IP核的“RX invert”开关很多情况下是正负极性接反导致无法锁定。3. 自研UDP/IP协议栈要点3.1 帧格式与端到端数据结构PCS/PMA IP核不关心你发的是什么它只负责把字节流搬到线下。MAC层必须保证你送给它的数据是一个完整的以太网帧。以太网帧格式从前往后是前导码和SFD7字节0x55加1字节0xD5这个通常由MAC生成其实PCS/PMA IP内部也有16字节的自动发送前导码功能。我为了调试可控选择在MAC里手动生成前导码。目的MAC、源MAC各6字节。EtherType2字节IPv4是0x0800。IP头固定20字节。UDP头固定8字节。UDP负载可变长度。FCS4字节CRC32覆盖从目的MAC到UDP负载的所有字节。需要注意的是PCS/PMA IP核并不做FCS校验。这在有些资料里有争议我专门抓过线上的数据确认IP核不会替你把CRC补上。它只是把上层来的字节经过8B/10B编码后发出去接收方向也是解码上报。所以CRC必须由MAC层自己算。这也是自研MAC最容易被“坑”死的地方因为CRC32的位宽算法和字节序搞错会直接导致接收端丢弃所有帧。3.2 发送通路设计发送通路的重点是状态机。我的发送MAC状态机分成这几个状态IDLE等待发送使能。PREAMBLE发7字节0x55 1字节0xD5。MAC_HDR发目的MAC、源MAC、EtherType。IP_HDR发IP头20字节里面所有字段提前算好包总长度字段在发送期间需要根据实际负载长度生成。UDP_HDR发UDP源端口、目的端口、长度、校验和。PAYLOAD从用户FIFO里读出数据并发出去。PAD_CRC如果负载不足46字节要补0到46字节然后发4字节CRC。用Verilog写的时候我推荐一个经典写法把需要发送的头部数据按字节索引方式用case语句输出这样代码清晰、不容易错位。wire [7:0] tx_frame_byte; always (*) begin case (tx_idx) 8d0: tx_byte 8h55; // preamble // ... 8d13: tx_byte src_mac[47:40]; // ip header... default: tx_byte fifo_data; endcase end当然如果追求时序性能最好把头部拼成64bit或128bit向量一次输出而不是每拍8bit。但8bit实现简单2.5G下312.5MHz也不难收敛。UDP校验和计算我建议用“边发边累加”的方式。UDP校验和是伪首部源IP/目的IP/UDP长度/协议号加上UDP头和数据按16bit累加取反。你在发送头部之前其实还不知道负载长度和校验和所以常见的做法是在发送的同时把数据累加一遍等发送完PAYLOAD后再补发两个字节的校验和。这样会多出两个字节的发送时间但在MAC层完全可接受。另一种做法是先算完校验和再发送但需要多缓存一整个UDP包对FIFO深度压力大。3.3 接收通路设计接收方向要处理的事情更多。数据从PCS/PMA IP核进来时是连续字节流你需要先找帧头。通常IP核接收方向会有“rx_axis_tvalid”和“rx_axis_tlast”但如果是8bit数据流接口你要自己根据tlast和tvalid判断一个帧的边界。接收状态机大致是LOOK_FOR_SFD检测到0xD5之后进入接收帧头。RECV_HDR收14字节MAC头、20字节IP头、8字节UDP头同时做过滤。RECV_PAYLOAD根据UDP长度确认负载字节数写入FIFO。WAIT_FCS最后收4字节CRC在内部对前面收到的字节做CRC计算并和收到的FCS比较。过滤规则我建议至少支持三种目的MAC地址过滤、目的IP地址过滤、UDP目的端口过滤。如果只想跑通上位机到FPGA的UDP可以先不做ARP协议直接固定ARP表项。上位机的ARP缓存里手动添加一条静态ARP记录把FPGA的IP映射到FPGA的MAC地址上。这样FPGA侧就省去实现ARP协议的负担。缺点是设备换网段要重新配ARP但对板级调试来说完全够用。接收方向的跨时钟域问题更明显。PCS/PMA IP核的rx clk是恢复时钟来自远端数据不是本地稳定的参考时钟所以你从IP核拿到数据后第一件事就是把它同步到自己的用户时钟域。我使用一个异步FIFO写时钟是rx_userclk读时钟是主逻辑时钟避免恢复时钟抖动导致下游逻辑亚稳态。3.4 字节序和位序的坑以太网在传输层是大端序而FPGA习惯先把最低有效位放前面。具体到CRC32算法以太网FCS是“先传低有效位”并且对原始数据做bit reverse。很多教程会给一个按字节输入的CRC32模块但如果你把字节序搞反CRC结果完全对不上。我的经验是不要自己发明CRC算法直接用Xilinx的CRC IP核配置成Ethernet-FCS模式输入数据宽度和你的接口宽度一致它会帮你处理好反射和字节序。如果非要手写先把IEEE 802.3标准里的CRC代码移植过来然后再用一段真实以太网帧做验证不要拿着两个随机数就开始调。4. 与PHY芯片的连接SGMII/2500BASE-X模式下的实战细节4.1 两种模式的选择在这个项目中我一开始走的是SGMII模式因为手头有一批千兆PHY。但很快发现大多数千兆PHY的SGMII接口只支持10/100/1000M不支持2.5G。查了几款芯片手册只有明确写了“2.5G SGMII”或者“HSGMII”的PHY才能工作在2.5G。后来我干脆放弃外部PHY改用2500BASE-X直连光模块FPGA的GTX收发器直接出光接口这样既避免PHY芯片不支持2.5G的问题链路的确定性也更高。两者选择逻辑总结如下场景推荐方案原因有成熟2.5G PHY芯片需要做网口供电/隔离SGMII模式 外部PHY物理层抗干扰好支持网线传输板级背板或光模块互联2500BASE-X模式FPGA直连光模块省PHY链路干净适合高速采集需要兼容千兆交换机SGMII/1G模式动态切换适应现有千兆网络如果你要做2.5G UDP通信对端最好也是2.5G网卡或交换机。普通千兆交换机只协商到1G这时你的PCS/PMA必须切到1G模式。如果IP核不支持动态切换那就只能固定一个速率链路要求会变苛刻。4.2 MDIO配置与自协商采用外部PHY方案时MDIO是绕不开的。MDIO是一个两线管理接口用来读写PHY寄存器。PCS/PMA IP核通常不自带MDIO控制器所以我在FPGA里写了一个简易MDIO master通过状态机按照Clause 22规范产生MDC、MDIO时序。至少需要配置PHY的寄存器0控制、寄存器1状态、寄存器4千兆控制、寄存器9/102.5G相关控制不同PHY寄存器不同。典型的操作序列是上电后先把PHY软复位寄存器0 bit15置1等待自协商完成然后读状态寄存器确认link up、速度是否锁定在2.5G。有几次板子连不上最后发现是PHY的寄存器4里没有把速率设为2.5G而是默认的1G。所以一定要看PHY手册里的2.5G速率设置位。有经验的工程师会先把PHY配置成“uboot/linux驱动控制”但FPGA项目里很多PHY是裸机使用MDIO就必须在RTL里实现。我实现MDIO master的代码不超过100行用查表方式写寄存器最方便。配置完成后把“PHY link up”信号引到LED这样链路状态一眼可见。4.3 约束与信号完整性SGMII和2500BASE-X都属于高速串行信号。如果走SGMII到外部PHYTX/RX两对差分线最好走内层阻抗控制100欧姆差分等长控制在5mil以内。2500BASE-X直连光模块同样要走GTX专用引脚尽量不要绕很远否则容易导致眼图闭合。另外参考时钟引脚必须用FPGA的专用REFCLK引脚不能用普通全局时钟引脚产生抖动很大的时钟给gtrefclk。调试时如果链路不稳定可以用ILA观察IP核的rx_block_lock信号并尝试在IP核配置界面勾选“RX Equalization”不同档位。2.5G速率不算高一般默认等化能通但如果PCB走线特别长可能需要把接收均衡调高一档。5. 系统级验证从仿真到板级联调5.1 搭建testbench用task发送激励仿真阶段最容易发现的问题是协议格式错误。我搭了一个testbench里面用Verilog task模拟完整的以太网帧发送方便生成带错误CRC、带错误IP头等异常包。task send_udp_packet; input [47:0] dst_mac; input [47:0] src_mac; input [31:0] dst_ip; input [31:0] src_ip; input [15:0] dst_port; input [15:0] src_port; input [7:0] payload[]; integer i; begin // send preamble ... // send mac header ... // send ip header ... // send udp header ... // send payload ... // send computed crc ... end endtask好的testbench要在同一点把发出去的包经过一个“损伤注入”后再送回接收路径。比如模拟线路翻转、模拟链路丢失确保接收状态机在异常情况下不会挂死。我在仿真里发现过一个很隐蔽的问题当远端发送的是超长帧超过1518字节时接收FIFO溢出接收状态机陷入死循环。后来在接收状态机的每个状态都加了超时跳转才算解决。5.2 ILA在线调试的抓取策略板级调试离不开ILA IP核。2.5G速率下用户时钟312.5MHzILA的采样深度和触发配置直接影响调试效率。我的经验是不要一股脑把所有信号都抓进去ILA资源有限最好分阶段抓。第一阶段只抓时钟和复位gt_reset_done、rx_block_lock、mmcm_locked、reset_done。第二阶段抓PCS/PMA接口tvalid、tready、tlast、数据线。第三阶段抓自研MAC状态机状态寄存器和FIFO空满。触发条件建议用“rx_block_lock上升沿”作为第一级触发。如果链路都没锁后面数据全是乱的抓了也白抓。另外因为UDP通信是突发包可以在ILA里用“tvalid tready tlast”作为触发条件这样能正好抓到一帧的最后一个数据节拍往回滚动看整帧内容。5.3 实测吞吐量与丢包定位板级联调时我一般用两种测试方式。第一是自发自收FPGA把收到的UDP负载原封不动再发回去上位机通过UDP工具发送数据后统计回包数量这样能快速发现收/发通路是否有丢包。第二是回环模式上位机发送一个固定长度的UDP包FPGA收到后把负载里的字节取反再发回去以此验证数据处理逻辑。实测2.5G UDP吞吐时需要先清楚一个概念UDP最大有效负载由MTU决定如果二层MTU是1500字节扣除IP头20字节和UDP头8字节最大UDP负载是1472字节。2.5Gbps线速相当于每秒约312.5MB按1472字节一个包计算每秒约21.2万包上位机如果不开RSS多队列单核CPU很难全部处理丢包几乎必然。所以高吞吐测试要配合上位机多队列或者使用DPDK否则问题不一定在FPGA侧。我遇到过“FPGA接收没问题但上位机一直丢包”的情况后来发现是上位机把UDP包收到后没及时从socket缓冲区读走缓冲区溢出。跟FPGA侧无关。所以在排查丢包时一定要从物理层、链路层、传输层分别计数FPGA里加一个计数器统计收到的包数量再在上位机统计收到的包数量比对才能定位丢包发生在哪一段。6. 踩坑记录与经验总结6.1 复位顺序导致的链路不稳定第一次上板我发现rx_block_lock偶尔能拉高但过几百毫秒又掉下来。反复测试后定位是复位顺序问题我在gt_reset_done还没拉高时就释放了用户逻辑复位导致MAC侧率先发数据而PCS/PMA内部还没准备好。按照IP核手册要求gtwiz_reset_tx_done和gtwiz_reset_rx_done都要拉高后再延时一段时间最后释放用户逻辑复位。我后来写了一个简单的复位控制器用移位寄存器产生多级延时问题就消失了。6.2 CRC计算不对导致的ping不通排查帧格式问题时如果上位机抓包显示ARP请求有响应但ICMP ping不通多半是IP头校验和或UDP校验和错了。有一种情况很迷惑ARP能通是因为ARP请求/应答不校验IP头校验和或校验方式不同但ICMP数据包里的IP头和UDP头校验一错协议栈直接丢弃。解决方案是先在仿真里把固定IP头校验和算出来再用一个简单的Python脚本把同一个包发出来对比。我当时把IP头长度字段多算了2字节导致IP头偏移错位整个包看起来没什么问题但接收方丢弃。这种低级错误用代码review很难发现最好用Wireshark抓网线接口侧的报文然后再和FPGA内部的ILA数据逐字节对比。6.3 2.5G下AXI4-Stream反压导致丢帧当接收FIFO快满时RX MAC需要拉低tready。但如果PCS/PMA IP核在tready拉低的瞬间已经把部分数据送到接口而MAC侧又没有缓存那些数据就丢掉了。解决方法是FIFO深度设置足够大同时把tready拉低的判断阈值设得早一些比如FIFO填充超过一半就开始反压。另一个细节是tready拉低不能在发送一帧的中间改变只能在帧边界改变否则会撕裂帧。我最初忽略了这一点导致接收端经常出现“半个包”的情况。6.4 不要忽略PHY的默认配置如果是外部PHY方案PHY芯片在上电瞬间通常有配置引脚决定默认速率。有些PHY通过硬件引脚配置为千兆FPGA侧即使是2.5G能力PHY也会把速率协商到1G。我遇到的情况是FPGA配置成2.5G SGMIIPHY link up了但实际速率是1G抓包软件显示“协商到低速”。原因是PHY的strap引脚默认拉低指向1G。这种情况下要么改硬件电阻要么用MDIO在启动时强制写寄存器。所以外部PHY方案务必先确认strap的默认配置再写MDIO初始化流程。整个项目做下来我最深的一个体会是FPGA高速以太网这个方向真正的难点往往不在“高速”而在“协议栈的完整性和时钟/复位这些基础工程问题”。1G/2.5G Ethernet PCS/PMA IP核把最难的高速收发器逻辑封装好了但MAC层、UDP协议栈、MDIO管理、板级调试仍然需要大量经验积累。如果你也想自己动手做一遍建议按这个顺序推进先用现成例程把PCS/PMA的线速回环跑通再做MAC自发自收然后加入UDP协议栈最后再接外部PHY或光模块。每一步验证通过后再往下走会少踩很多坑。最后留一个小技巧所有链路状态信号包括link up、复位完成、FIFO溢出都记得引到板上的LED这样远端调试时不用每次插ILA看灯就能判断问题大概出在哪一层。