ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FPGA实现UDP协议栈:基于verilog-ethernet的RGMII千兆网通信实战

FPGA实现UDP协议栈:基于verilog-ethernet的RGMII千兆网通信实战 1. 先从为什么选它说起开源UDP协议栈的可选路线1.1 自己写还是用现成的三分钟想清楚学FPGA这件事到了网络通信这一步很多人会卡住。我也不例外。前面几篇笔记里我还在玩数码管、串口、SPI突然想让自己做的板卡能通过网线和电脑传数据一下子面对的不再是某个外设的驱动而是一整套从物理层到应用层的协议栈。最开始我确实想过完全自己写UDP。毕竟网上也有不少人贴出过简化版代码感觉工作量似乎能接受。可真把需求掰开看就发现事情没那么简单。你要自己做MAC层得处理8字节前导码、帧起始定界符SFD、CRC32校验、帧间隙还要在RGMII接口上用双沿采样收发数据。这些逻辑单独写一个能跑的版本不难但要在各种异常场景下稳定工作就需要大量测试和修补。加上IP层的头部构造、校验和算法、ARP缓存管理、超时重传我粗略估了一下业余时间投入至少三到四周而且出来的东西大概率还有隐蔽bug。走厂商IP核路线也可以。拿Xilinx的Tri-Mode Ethernet MAC举例功能完整、时序经过验证但它是个商业IP核内部被封装成黑盒。一旦出现为什么对端收不到包这种问题你能看到的只有一堆接口信号和文档排错路径拉得很长。而且这种IP核的配置项非常多对新手来说光是搞明白那一堆端口的含义就是一件折磨人的事。最后我把目光落在GitHub上的alexforencich/verilog-ethernet。它用纯Verilog写成MIT许可证代码完全公开模块拆分得很干净。我不仅可以直接拿来用还能在出问题时打开源码一行一行查。从学习角度来说这个价值比抄一个现成可用的模块大得多。1.2 工程概览它到底包含哪些东西verilog-ethernet不是单一的一个UDP协议栈文件而是一整套以太网相关的RTL模块集合。打开rtl目录按功能大概分这么几类MAC层eth_mac_1g_gmii、eth_mac_1g_rgmii、eth_mac_10g、eth_mac_25g等对应不同速率的以太网接口。链路层辅助eth_crcCRC32计算、eth_shift字节对齐。网络层ipv4_engineIPv4收发、arp_cacheARP缓存、eth_arpARP帧收发、eth_arpreqARP请求。传输层udp_engineUDP收发、udp_checksum_gen/checkUDP校验和。其他eth_icmpICMP回显处理也就是让FPGA能被ping通、mdioPHY管理接口控制器、ssio源同步IO处理。这意味着你可以按需裁剪。比如我的板卡PHY是RGMII接口那就用eth_mac_1g_rgmii搭配ipv4_engine和udp_engine再接上arp_cache和eth_icmp一套最小可用的UDP收发链路就出来了。如果以后换10G板卡MAC层换成eth_mac_10g就可以上层逻辑完全不用动。这种分层设计本身就是很好的学习范本。1.3 关键技术点AXI4-Stream接口整个工程看起来最陌生、其实也最关键的概念是AXI4-Stream接口。如果你之前只接触过AXI-Lite第一次看到tdata、tvalid、tready、tlast、tuser这些信号时可能会懵。简单理解AXI4-Stream是一条有握手协议、有分包标记的流水线。tvalid表示当前数据有效tready表示下游模块准备好接收只有两者同时为高才算完成一次数据传输。tlast用来标示一帧数据的最后一个beat这样接收端就知道包到哪里结束。举个例子udp_engine发送一帧数据时应用层的逻辑只需要把tvalid拉高逐拍送入tdata最后一拍把tlast置为1。收包方向相反当m_axis_udp_tvalid拉高时说明有数据到了你逐拍读取看到tlast后就知道这一帧结束。tuser的作用比较灵活不同模块会用它携带错误标志、起始位置偏移等信息具体到每个模块都有注释说明。吃透这个接口之后读verilog-ethernet的源码就会顺畅很多。因为你会发现从MAC层到UDP引擎所有模块之间的数据传递都是用AXI-Stream串起来的。2. 在动手前先把RGMII调试链路理清楚2.1 RGMII信号与PHY的相互关系如果板卡上的PHY芯片是RGMII接口FPGA侧就要面对RGMII那套别扭的时序。RGMII最烦人的地方在于双沿采样4位数据线和1位控制信号在时钟上升沿和下降沿各传一次组合起来才是8位数据。细化到发送方向FPGA输出RGMII_TXC、RGMII_TXD[3:0]和RGMII_TX_CTL。其中TX_CTL在上升沿表示发送使能TX_EN下降沿表示发送错误标志TX_ER。接收方向相反RGMII_RXC由PHY提供FPGA在上下沿分别采样RGMII_RXD[3:0]和RGMII_RX_CTL然后拼出完整字节。幸好eth_mac_1g_rgmii模块把DDR转换和双沿采样都处理好了。从外部看它提供的接口就是一个在125MHz时钟下的8位数据总线。所以你在例化这个模块的时候不需要自己写IDDR/ODDR只需要给它正确的时钟和引脚约束就行。但有一点要提醒RGMII的PCB布线、终端电阻、时钟相位都要遵循PHY数据手册的要求。我第一块板卡就因为RGMII_RXC上多串了一个电阻导致高速模式下偶尔采错数据一度以为是FPGA代码问题排查了整整两天。2.2 PHY配置MDIO是绕不开的一环RGMII只是数据面PHY芯片还需要管理配置。verilog-ethernet仓库里有一个mdio模块就是用来通过MDIO接口读写PHY内部寄存器的。我最初有一个误区以为PHY上电后就会自动协商成千兆模式。实际上不少PHY默认就开启了自动协商链接确实能建立但如果你想修改PHY地址、关闭自适应、强制百兆模式或者读取链路状态都绕不开MDIO配置。MDIO接口基本结构很简单一根管理时钟MDC、一根双向数据线MDIO。读写一帧数据时先发前导码然后发操作码、PHY地址、寄存器地址再是数据。对应到寄存器最常用的是0x00控制寄存器速度选择、自动协商使能、软件复位。0x01状态寄存器链接状态、协商能力。几个厂商自定义寄存器用于读取交叉检测、MDI/MDIX状态等。我在上板前先在仿真里用mdio模块读了一遍PHY的ID寄存器确认MDIO时序完全正确再上板跑真实链路。这个习惯帮我节省了很多时间因为一旦PHY状态不对网络根本不通而你很难通过抓包软件判断到底是PHY没工作还是FPGA逻辑有问题。2.3 复位与时钟体系verilog-ethernet里的模块大多是同步复位、高有效。看起来简单实际操作中有个容易踩的坑整个工程可能分布在多个时钟域每个模块都需要复位信号如果每个人复位释放时机不一致模块之间就可能出现初始化不同步表现为偶发的一帧错位。我习惯做一个异步复位同步释放电路把板级复位信号同步到用户时钟域然后再分发给各个模块。标准写法大致是这样reg rst_r1 1b1; reg rst_r2 1b1; always (posedge clk or posedge arst_n) begin if (arst_n) begin rst_r1 1b1; rst_r2 1b1; end else begin rst_r1 1b0; rst_r2 rst_r1; end end assign rst ~rst_r2;注意我这里的arst_n是低有效外部复位经过同步释放后输出高有效rst。这样所有模块的复位释放都会发生在同一条时钟沿上最大程度避免亚稳态和初始化不一致。上板实测下来这个细节对协议栈的稳定性影响很大。3. 读源码顺序从UDP引擎倒着看效率最高3.1 为什么从udp_engine开始读verilog-ethernet的模块分层是eth_mac→arp/ipv4_engine→udp_engine。我一开始按这个顺序读结果被MAC层大量的位操作、字节对齐、CRC状态机劝退了。后来换个思路从最上层的udp_engine开始因为它的输入输出都是AXI-Stream不涉及具体引脚时序和普通逻辑模块没有太大区别很容易建立整体认识。udp_engine对外接口分两类应用侧s_axis_udp_tdata、s_axis_udp_tvalid、s_axis_udp_tready、s_axis_udp_tlast接收方向对应m_axis_udp_*。网络侧udp_ip_tx_*、ip_udp_rx_*这些信号通常是直接连到ipv4_engine的你不太需要关心内部握手细节。想发一个包往s_axis_udp_*上按AXI-Stream时序写数据tlast置1模块就会自动给你加UDP头。想收包等m_axis_udp_tvalid拉高后读数据直到tlast。这种封装非常友好应用逻辑可以完全独立于协议细节。3.2 发送方向UDP头是怎么拼出来的打开udp_engine源码核心就是一个状态机一开始在IDLE状态检测到s_axis_udp_tvalid且配置有效就跳到发送UDP头状态依次把源端口、目的端口、长度字段推到数据总线上然后进入发送payload状态转发应用数据直到tlast最后收尾。这个过程中最容易搞混的两个长度字段我专门在代码里做了一行注释提醒自己UDP长度字段8字节UDP头加上payload长度。IP总长度字段20字节IP头加8字节UDP头加payload长度。如果你在抓包软件里看到的长度对不上先检查这两个数值。另一个细节是UDP校验和默认可以直接填0表示发送端不计算校验和。verilog-ethernet也提供了udp_checksum_gen/check模块但要计算校验和就得在整个payload累加会多用不少逻辑资源。对普通实验板卡来说设0完全够用。再往下看ipv4_engine它会接着拼IPv4头。IP头里的header checksum是反码求和算法verilog-ethernet的实现是每拍做一个16位累加效率还不错。IP校验和只覆盖20字节IP头不包含payload这一点和UDP校验和的覆盖范围不同看代码时要注意区分。3.3 接收方向怎么确认这个包是我的接收方向同样是一个状态机但多了过滤逻辑。ipv4_engine从MAC层收到完整帧后先判断IP头里版本字段是否为IPv4协议号是否为17UDP目的IP是否等于本地配置的IP或者是广播地址IP头校验和是否正确。这些条件全部满足数据才会被交给udp_engine。udp_engine收到后继续检查UDP目的端口、源端口是否匹配匹配才向应用侧输出。所以使用udp_engine时你必须正确配置local_ip、local_port、remote_ip、remote_port。任何一个不匹配包都会被静默丢弃。上板调试时如果收不到数据优先检查这些寄存器值是否和软件端对得上。3.4 ARP与ICMP想让电脑ping得通光有UDP不够实际上如果FPGA只实现UDP不处理ARP电脑发来的第一个包可能根本到不了UDP引擎。因为在以太网里发送方先要发送ARP请求拿到目的MAC地址才能把IP包封装成以太网帧。所以verilog-ethernet里有eth_arp和arp_cache模块。eth_arp负责解析收到的ARP请求如果请求的目标IP是本机就自动回复一个ARP应答。arp_cache维护一张本机已知的IP到MAC地址映射表如果发送IP包时查不到对应条目eth_arpreq模块会挂起当前包的发送主动发一个ARP请求等回复回来后继续发送。还想让电脑ping通FPGA就得额外挂eth_icmp模块。它处理收到的ICMP回显请求把类型、标识符、序号原样放回回复包计算好ICMP校验和。挂上之后ping就能通了。这个部分我一开始也犯过迷糊以为FPGA能回UDP自然就能被ping通。实际上两者走的是完全不同的协议路径需要分别使能。4. 自己写一个testbench把收发回环跑起来4.1 搭建仿真环境读源码读到感觉自己懂了接下来就是动手验证。verilog-ethernet官方仓库自带了一些C测试平台和makefile它们主要用于验证IP核对刚接触FPGA的开发者来说门槛偏高。我没直接用而是在Vivado里建了一个工程把需要的RTL文件加进去自己写testbench。仿真环境的模块清单至少包括eth_mac_1g_rgmiiipv4_engineudp_engineeth_arp、eth_arpreq、arp_cacheeth_icmp一个简单的应用模块用来发起UDP发送并接收回显testbench里最关键的一步是把MAC的发送引脚输出直接接到MAC接收引脚输入构成一个数字回环。因为RGMII信号在仿真中没有实际PHY芯片参与这条回环路径能让一个UDP包从发送侧发出后立刻进入接收侧验证整个协议栈的收发链路。如果想要更真实可以写一个简易PHY模型把RGMII信号解码再回环但这一步不是必须的。4.2 testbench的关键信号与时序时钟我用125MHz复位拉高保持至少10个周期后释放。接下来给udp_engine配置本地IP、目的IP、端口等信息再往s_axis_udp_tdata上写入测试数据并拉高tlast。之后观察m_axis_udp_tdata上是否有回环回来的数据。写testbench时一个特别容易被忽略的点是AXI-Stream握手要求tvalid和tready同时为高才算一拍有效。我第一版testbench里发送侧每拍都拉高tvalid但没有真正处理tready反压结果在后面某个时刻数据错位了。正确做法是写一个可以响应tready的发送任务task send_packet; input [7:0] data [0:15]; integer i; begin for (i 0; i 16; i i 1) begin s_axis_udp_tdata data[i]; s_axis_udp_tvalid 1b1; s_axis_udp_tlast (i 15); wait (s_axis_udp_tready 1b1); (posedge clk); end s_axis_udp_tvalid 1b0; s_axis_udp_tlast 1b0; end endtask这段代码的意思很直白每拍把数据放到总线上等tready拉高后等下一个时钟沿才切换到下一个数据。这样就能真实模拟应用侧在反压情况下的发送行为。4.3 我第一次仿真的失败与排除过程第一次搭完testbench跑出来的结果是MAC完全没有输出数据。我盯着波形看了很久最终发现问题是复位释放前我就给udp_engine写入了一个控制寄存器而模块在复位期间采样到了错误配置。把写配置的语句移到复位释放后再执行问题立刻消失。另一个差点让我抓狂的问题是在testbench里忘了把MAC的接收时钟和发送时钟信号连好。eth_mac_1g_rgmii内部发送时钟是逻辑时钟接收时钟是外部RGMII_RXC引脚在数字回环里如果不把这两个时钟关联起来接收侧就会一直处于无时钟状态表现得像数据全丢了。查这种问题记得先看时钟有没有、再看复位有没有释放、再看信号有没有握手顺序很重要。5. 板级调试真实网络里的那些坑5.1 引脚约束和时序约束的写法仿真通过之后上板才是真正的校验场。RGMII是源同步接口FPGA侧必须做正确的时序约束否则在千兆速率下很容易出现低速偶然能通、高速必丢包的问题。一个典型的XDC约束片段假设RGMII_RXC是由PHY输入FPGA的接收时钟create_clock -name rgmii_rxc -period 8.000 [get_ports {rgmii_rxc}] set_input_delay -clock rgmii_rxc -max 2.0 [get_ports {rgmii_rxd rgmii_rx_ctl}] set_input_delay -clock rgmii_rxc -min 0.5 [get_ports {rgmii_rxd rgmii_rx_ctl}]这段约束告诉综合工具RGMII接收数据相对RXC存在一定的建立保持时间。如果时序违例严重工具布线后的结果就不能保证在真实硬件上稳定工作。我刚开始上板时不加约束直接跑小包低速能通换成大批量数据后就开始丢包。补上约束重新跑问题消失。发送方向同理使用ODDR输出数据时也要对应的output delay约束。verilog-ethernet的examples目录下有完整的Vivado约束文件直接参考比自己盲写靠谱。5.2 上板第一件事用Wireshark抓包上板调试我推荐先用Wireshark抓包比用逻辑分析仪或者ILA方便得多。具体做法把我板卡的IP固定为192.168.1.10PC网卡IP设为192.168.1.100。板卡上电后PC先ping 192.168.1.10观察有没有ARP请求发出。如果ping不通抓包看PC有没有发出ARP请求FPGA有没有回ARP应答。能ping通之后再测UDP收发用Python脚本从PC发UDP包给FPGAFPGA收到后回一个包。我第一次上板ping不通抓包发现PC发出ARP请求后无人应答。最后定位到两个问题一是引脚约束文件里PHY芯片的MDIO地址填错导致配置根本没写进去二是local_ip寄存器在上电初始化流程里没有赋值成功。这些通过Wireshark抓包都能快速把问题范围缩小到物理层、链路层还是上层配置非常有效。5.3 实测丢包问题的定位过程能ping通、UDP也能收发之后我开始压力测试FPGA连续发送5000个UDP包给PC。结果Wireshark上显示每隔几百个包就缺几个。整个排查链路大致是先怀疑PHY丢弃用ILA观察MAC发送侧外部接口发现MAC自身已经完整送出了每一个包说明PHY之前没有丢。怀疑MAC内部FIFOverilog-ethernet的MAC收发路径里有缓存FIFO但核心引擎本身没有大缓冲区如果应用逻辑瞬间发送速率超过MAC能处理的速率就会产生反压。最后定位到我的应用逻辑我在一个always块里同时产生数据并发送完全没有处理s_axis_udp_tready反压导致udp_engine输入侧丢帧。解决办法是应用侧先加一个简单的AXI-Stream FIFO把要发送的包缓存下来等tready拉高后再逐步送出。或者降低应用发包速率比如发完一个包后等待若干个周期再发下一个。后来我做图像数据UDP上送时这个经验又一次派上了用场。突发数据必须经过缓冲平滑否则任何协议栈都扛不住瞬间尖峰。5.4 带宽估算为什么图像数据经常跑不满很多朋友做FPGA图像处理最后想把图像通过UDP发到电脑。这里有一个很现实的带宽问题。千兆网口理论带宽是125MB/s但减去以太网前导码、帧间隙、IP/UDP头实际有效载荷大约在100MB/s左右。如果一张1080p的2MB图像理想情况下每秒钟最多也就传50帧左右。所以设计数据通路的早期就要算清楚像素位宽和UDP数据位宽是否匹配。是否需要用DDR做缓存把突发的图像数据平滑成网络可控发送的节奏。单个UDP包最大不超过1472字节也就是1500字节MTU减去20字节IP头和8字节UDP头。超过就需要分片但verilog-ethernet不支持IP分片应用层最好主动把包限制在这个长度以内。这些计算看起来很简单但我见过太多项目因为前期没算明白后期发现带宽无论如何都达不到需求才回头改架构。6. 留给自己的延展清单这次把verilog-ethernet学完最明显的收获不是会调UDP了而是终于看明白了一个成熟的多层协议栈是怎么组织代码的。作者在处理状态机时几乎每个跳转条件都布满了异常保护在处理跨层数据传递时宁可多写一拍延迟也要保证时序简单清晰。这些习惯对我自己写模块有很大启发。我给自己列了一个延展计划下一步准备做研究10G MAC版本看看在更高带宽下AXI-Stream的频率和位宽会有什么变化。试着给verilog-ethernet增加一个多端口UDP复用逻辑让单一网口能同时服务多种应用。找到IPv6的处理方式和IPv4的差异加深对网络层整体设计的理解。结合之前用过的DDR4做一个网口接收→DDR缓存→图像处理→网口发送的完整通路。最后分享一个读这种开源工程的技巧不要从头到尾逐行读那很容易在MAC层就被劝退。先确定一个最小可跑通的场景比如从PC发一个UDP包FPGA收到后回显然后在仿真里把这条路径完全打通再根据调试过程中产生的疑问回头查代码。这个顺序我认为是新手接触协议栈类开源项目最有效的方式。
返回列表