ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

FPGA实现UDP网络通信:从RGMII接口到ARP/ICMP回环设计

FPGA实现UDP网络通信:从RGMII接口到ARP/ICMP回环设计 1. 网络通信设计的前期准备与方案选型最开始得说句实在话FPGA做网络通信听起来很唬人实际上跟单片机玩TCP/IP完全是两个思路。MCU上你只要调库、填结构体、等中断就行协议栈都是现成的CPU帮你把活干了。FPGA里没有CPU帮你兜底一切要靠硬件逻辑“拼”出来——但反过来这也正是它的优势所在你可以用纯逻辑把网络数据处理到线速延迟做到微秒级以下这是任何带操作系统的处理器都很难做到的。这篇文章是这个系列的第7篇我默认你已经会写基本的Verilog、能把LED点亮、能操作UART、对AXI总线有个模糊的概念。这篇我们聊的是怎么让FPGA真正上网——不光能收包发包还要能解析、能过滤、能组包、能跑通ARP和ICMP最后实现一个完整的UDP回环通信。整个过程我会按照我实际跑项目的顺序来写包括我踩过的坑和回头重新设计的地方。先解决一个看起来基础但很多人一开始没想明白的问题FPGA做网络通信到底该选什么方案业内最常见的三条路纯逻辑实现MAC 外部PHY芯片比如RGMII接口接一个88E1512或者YT8531。这是最正统、最适合新手理解整个流程的方案。FPGA内部用硬核MAC比如Xilinx 7系列内置的Tri-Mode Ethernet MAC通过GMII/RGMII接外部PHY。这套方案省逻辑但你得跟IP核的AXI接口打交道。Zynq这种带ARM硬核的SoC把Linux跑起来用内核协议栈FPGA只做DMA搬运。这个叫“网络加速”而不是“FPGA做网络”适合后期做高速数据处理。我推荐第一条路。原因很直接你要真理解网络数据是怎么从网线进到FIFO里的就必须亲手把MAC层的状态机写一遍。用IP核确实快但出了问题你根本不知道去哪查。这就像学开车先开手动挡把手脚配合练熟了再去开自动挡。1.1 硬件平台与接口选型做网络通信你手里得有一套能跑起来的硬件。别用那种只有几十个引脚的入门级板子因为网络通信至少要占用一组RGMII引脚约12根信号线再加上调试用的UART、LED引脚就紧张了。我的建议配置组件推荐型号说明FPGA芯片Xilinx Artix-7 35T/100T 或国产安路EG4X20资源充足逻辑单元在20K以上能放下MACFIFODMAPHY芯片88E1512、YT8531、RTL8211FD千兆PHY支持RGMII接口驱动配置简单变压器HR911105A带网口的RJ45座集成变压器集成方案省事别自己画分立变压器时钟25MHz有源晶振给PHYFPGA侧根据接口模式提供125MHz或50MHz注意RGMII的时钟拓扑调试接口USB-UART、JTAG串口打印调试信息JTAG抓波形接口上我强烈建议选RGMII。原因有三引脚少12根搞定速率够用千兆场景125MHz DDR时序约束比起GMII那种一大把等长线要友好得多。Vivado里约束RGMII的时序主要就是管好一个时钟组相对省心。注意如果你用的PHY芯片是88E1512上电配置要留意芯片的默认地址是0x00还是0x10。部分板卡硬件设计时会把PHY地址拉高导致你MDIO访问不到寄存器这个后面排查的时候能坑你一晚上。1.2 时钟域与复位设计的整体规划做网络通信之前先花时间把系统的时钟树理清楚。这是决定项目成败的第一件事很多人上来就写MAC状态机写到一半发现时序收敛不了整段废掉。FPGA网络通信里至少有四个时钟域系统时钟一般100MHz或200MHz用于FPGA内部逻辑和FIFO读侧。如果后面要接DDR或者PCIe系统时钟一般由专用时钟芯片生成。接收时钟RGMII模式下由PHY恢复出来的RX_CLK跟数据同步频率取决于链路速率千兆125MHz、百兆25MHz。发送时钟TX_CLK由FPGA内部PLL生成千兆模式下125MHz并且要保证数据和时钟的相位关系RGMII要求时钟上升沿和下降沿分别采样/发送不同的位。MDIO配置时钟一般2.5MHz左右由FPGA分频产生用于读写PHY寄存器。这四个时钟之间是异步关系跨越时钟域的数据必须经过异步FIFO或打拍同步不能直接用。我当时的处理方式是接收方向PHY采到的RX_CLK直接驱动RX MAC的逻辑把数据解析完之后写入异步FIFO读侧用系统时钟读出来发送方向系统时钟域把数据写入异步FIFOTX侧用TX_CLK去读FIFO然后发给PHY。复位设计也要统一。我最开始图省事所有逻辑都用同一个全局复位结果发现PHY配置模块还没配好、FIFO还没复位完RX MAC就开始往FIFO里写数据了丢包丢得莫名其妙。后面改成分层复位上电后先复位PHY配置模块配置完成标志拉高后再释放MAC层的复位MAC层复位释放后再释放应用层的FIFO复位。每一层的复位都跟各自时钟域同步比如RX侧复位用RX_CLK打两拍TX侧复位用TX_CLK打两拍跨时钟域的地方用异步复位同步释放的方式处理。2. 网络协议栈的精简实现做FPGA网络通信你不需要把TCP/IP那一整套都搬进来。资源有限逻辑复杂实际项目中99%的场景只需要实现ARP、ICMP、UDP就够用了。TCP那个状态机在FPGA里也能实现但复杂度会成倍上升而且TCP的滑动窗口、重传定时器这些东西在纯逻辑里写起来又长又容易出错如果不是特殊需求不建议新手一上来就碰。我做的这个项目定位就是“网络透传”FPGA通过UDP收到数据处理后原样回发顺便支持Ping通。应用场景是远程采集、远程控制或者作为数据链路层测试的探针。2.1 为什么只做ARP、ICMP、UDP而不做TCP这几个协议不是随便选的整个逻辑链路是配套的ARP解决“广播域里找到对方MAC地址”的问题。你给PC发UDP包第一件事就是得知道PC的MAC地址。如果不知道就得发ARP请求等PC回ARP响应。ICMP解决“能不能通”的问题。PC上先Ping一下通了说明链路通了、MAC和IP都配对了、FPGA的收发通路是正常的。这是最基础的调试手段。UDP解决“传业务数据”的问题。UDP头只有8字节组包和解析都很简单没有握手、没有确认非常适合FPGA这种“能处理多少就发多少”的模式。TCP在FPGA里不是不能做但一个完整的TCP状态机写下来至少2000行Verilog起步还要维护发送缓存、超时重传、乱序重组工程量大不说调试的时候一个边界条件漏了就跟你玩“假死”。我见过有人用FPGA实现了TCP的简化版只做数据的透明传输但最终性能和稳定性都不太理想工程量还拖了两个月。所以我的建议是先把UDP链路跑通这是性价比最高的路径。真到了产品阶段必须要TCP可以用Zynq跑Linux协议栈FPGA只做桥接这个组合才叫合理分工。2.2 以太网帧结构从网线到FIFO的数据流先重温一下以太网帧的结构这是整个设计的地基。最低层是物理层PHY芯片负责把网线上的模拟信号转成数字信号。这里有个很关键的概念PHY转发给MAC的并不包含前导码Preamble和帧起始定界符SFD这两个东西PHY已经帮你剥掉了。PHY输出的数据从“目的MAC地址”开始到FCS帧校验序列结束一共是目的MAC地址6字节源MAC地址6字节类型/长度字段2字节0x0800表示IPv40x0806表示ARP0x86DD表示IPv6数据载荷46到1500字节FCS4字节也就是说PHY给到FPGA的数据里已经带上了FCS而FPGA发送给PHY的数据PHY会自己算好FCS加上去。这个细节特别重要不然你调试的时候会怀疑人生明明PHY输出的数据里带着CRC尾巴你按标准帧解析怎么都对不上。接收方向FPGA拿到MAC帧之后要做的第一件事就是判断这个帧是不是发给自己的。判断标准有三条目的MAC是全FF广播帧或者等于自己配置的MAC地址。类型字段是0x0806ARP或0x0800IPv4才处理其他直接丢弃。如果是IPv4还要继续检查IP头里的目的IP是不是自己不是就丢。处理完了剩下的载荷数据才写入FIFO等上层逻辑来读。2.3 ARP协议模块的实现要点ARP模块是整个协议栈里的“后勤部长”没有它你的UDP包发不出去。当FPGA收到一个ARP请求时帧头里的信息很有用发送方的MAC和IP地址都能拿到。如果请求的目标IP是自己就要回一个ARP响应把自己的MAC填进去然后把这个对端IP-MAC映射缓存下来比如存在寄存器里。以后给这个IP发UDP直接用缓存好的MAC不用再发ARP请求了。ARP请求的帧数据格式不长硬件类型2字节以太网填1、协议类型2字节IPv4填0x0800、硬件地址长度1字节填6、协议地址长度1字节填4、操作码2字节1是请求2是响应、发送方MAC 6字节、发送方IP 4字节、目标MAC 6字节、目标IP 4字节。整个判断逻辑其实就是一个大型的比较器和选择器收到帧后先核对前导的几个字段如果匹配再把操作码和IP都解析出来根据情况生成响应帧。因为ARP帧很短无需要很大的FIFO一个发送缓存用双端口RAM或者寄存器堆就行。实操心得如果你用PC来Ping FPGAPC会先发一个ARP请求找FPGA的MAC地址。FPGA回了ARP响应之后PC会再发ICMP Echo Request。所以调试顺序永远是ARP先通再谈ICMP。2.4 IP层校验和计算的硬件实现IP头里有校验和字段它的算法是把IP头按16位一组全部相加如果进位就回卷累加最后取反。这个算法在软件里几行代码就写完了但在FPGA里要仔细安排加法器的时序。我给你算一个IP头校验和的例子。假设IPv4头部是下面这20个字节45 00 00 3c 00 00 00 00 40 11 00 00 c0 a8 01 0a c0 a8 01 14其中IP总长是0x003c即60字节协议字段是0x11即UDP源IP是192.168.1.10目的IP是192.168.1.20校验和暂记为0x0000。计算过程如下45 00 00 3c 0x453C0x453C 00 00 0x453C0x453C 00 00 0x453C0x453C 40 11 0x854D0x854D 00 00 0x854D0x854D c0 a8 0x145F5进位1回卷得0x45F60x45F6 01 0a 0x47000x4700 c0 a8 0x107A8进位1回卷得0x07A90x07A9 01 14 0x08BD取反得到0xF742这就是校验和硬件实现这个算法的思路是用一个加法器和一个寄存器16位加法器输出drop掉进位再回加。每个时钟周期处理一个16位字10个周期出结果。如果头长是20字节取10个16位字累加即可。如果你要追求更快的速度可以把加法器树展开成并行结构但计算转发场景也没必要这么快一个周期处理一个16位字完全够用。关键点是最后必须做“取反”这一步忘了取反的话PC上Ping永远不通。3. RGMII接口的时序设计与收发通路搭建这一部分是硬件逻辑的重头戏也是新手最容易翻车的地方。RGMII接口说白了就是12根线的并行接口信号方向说明TX_CLKFPGA→PHY发送时钟千兆下125MHzTXD[3:0]FPGA→PHY发送数据DDR模式上升沿发低4位下降沿发高4位TX_CTLFPGA→PHY发送控制上升沿发TX_EN下降沿发TX_EN与TX_ER的异或RX_CLKPHY→FPGA接收时钟由PHY恢复千兆下125MHzRXD[3:0]PHY→FPGA接收数据DDR模式上升沿采低4位下降沿采高4位RX_CTLPHY→FPGA接收控制上升沿采RX_DV下降沿采RX_DV与RX_ER的异或为什么要用DDR的方式在上下沿各传4位因为这样千兆速率下数据信号只需要125MHz降低了PCB布线和信号完整性的难度。代价是FPGA内部处理时要先把上下沿的数据拼成8位再丢给MAC层。3.1 发送通路从FIFO到PHY发送侧的整套流程是上层逻辑把要发的数据写入发送FIFOMAC发送状态机检测到FIFO非空且PHY处于空闲就开始组帧先插入前导码7字节0x551字节0xD5然后依次读出目的MAC、源MAC、类型、IP层数据最后把FCS这个字段留给PHY去计算。这里有个细节前导码在标准里是MAC层该干的活但不同PHY或者不同MAC IP核处理方式不一样。如果你用的是自己写的MAC每个包都要记得从7字节0x551字节0xD5开始发。如果你用的是Xilinx或国产FPGA的MAC IP核IP核会自动帮你加前导码那就不要自己再加了。搞混了帧就会畸形。RGMII发送的核心是IO时序也就是DDR输出。在Vivado或国产EDA里直接调用原语// Xilinx 7系列下RGMII发送DDR输出 ODDR #( .DDR_CLK_EDGE(SAME_EDGE) ) oddr_txd0 ( .Q(txd[0]), .C(tx_clk), .CE(1b1), .D1(txd_low[0]), // 上升沿输出的位 .D2(txd_high[0]) // 下降沿输出的位 ); ODDR #( .DDR_CLK_EDGE(SAME_EDGE) ) oddr_tx_ctl ( .Q(tx_ctl), .C(tx_clk), .CE(1b1), .D1(tx_en), .D2(tx_err_en) // TX_EN与TX_ER异或 );关键是ODDR原语的D1和D2要做“SAME_EDGE”对齐在同一个时钟上升沿输出两个比特一个用来在上升沿发出去另一个锁存到下降沿发出去。这样内部逻辑可以完全按125MHz单沿来准备数据不用去管双沿细节。约束文件里要把TX_CLK和TXD、TX_CTL放在同一个输出时钟组里保证相位对齐否则PHY采样容易出错。3.2 接收通路IDDR采样与数据对齐接收方向PHY会把恢复的RX_CLK和已经对齐的数据RXD、RX_CTL一起送给FPGA。FPGA侧要用IDDR原语在上升沿和下降沿分别采样低4位和高4位然后拼成一个8位字节。这看起来简单但有几个很隐蔽的坑IDDR里的SAME_EDGE模式IDDR的输出在同一个时钟沿给出两个字节但这两个字节对应的是相邻时钟周期的相位。你得自己搞清楚哪个是“第一拍”哪个是“第二拍”。我的做法是先拼出一个16位的数据总线然后根据采样控制信号把正确的两路数据取出来。RX_CTL下降沿采的是RX_DV与RX_ER的异或。正常收发数据时RX_ER为0所以下降沿采到的就是RX_DV本身。但这只是“正常”情况如果PHY检测到链路错误RX_ER就会拉高此时如果还按照普通数据去解析就会解析出错误的帧。所以接收状态机一定不要忽略RX_CTL的下降沿信息。还有一个非常关键的时序收敛问题RX_CLK是PHY恢复的时钟跟FPGA内部系统时钟完全异步。你从IDDR采到的数据要先经过一个小FIFO或者寄存器同步链才能交给系统时钟域的逻辑处理。不要试图直接用系统时钟去采RX_CLK的沿因为RGMII数据是在两个时钟沿上翻转的系统时钟采到的数据很有可能跑在建立/保持时间之外结果就是偶发坏数。3.3 FIFO深度的选择与背压机制数据跨时钟域之后就要面临FIFO深度选多大的问题。很多新手拍脑袋定深度要么选太小造成丢包要么选太大浪费BRAM资源。FIFO深度的计算公式可以这么理解上游产生数据的速度乘以最大突发长度如果下游来不及处理需要缓存多少数据。UDP数据包最大约1500字节如果应用侧把整个包都缓存下来再转发接收FIFO至少需要1500字节。考虑到还有帧头帧尾以及FIFO读写效率损失我一般留1.5倍余量也就是4KB用BRAM实现很划算。发送FIFO同理如果上层要一次性把一整帧写进来再发起发送也需要4KB以上。但FIFO深了也有问题如果应用层逻辑不读FIFO数据积累到一定程度会溢出。溢出时的行为要设计好是丢弃新到的帧还是保留旧帧我的做法是加一个“溢出丢弃”逻辑在接收侧检查到FIFO快满时直接拉高给PHY的反压信号或者简单粗暴地丢弃当前正在接收的帧同时更新一个丢包计数器。这样至少能知道系统丢了多少包而不是默默丢了一堆数据完全没感知。4. UDP回环通信的应用层设计与完整验证协议栈的底层通路做完之后业务逻辑反而是最简单的。我做的这个UDP回环模块应用层其实就是一个状态机收到UDP包之后把载荷数据原样写回发送FIFO同时把IP头里的源IP和目的IP对调MAC地址也做相应的交换源端口和目的端口互换然后重新计算UDP长度和IP校验和。4.1 UDP组包与校验和的实现细节UDP回环时最容易被忽略的就是UDP校验和。UDP校验和的计算范围包括伪IP头源IP、目的IP、协议号、UDP长度 UDP头 UDP数据。如果计算结果为0要把校验和字段填成0xFFFF这是一个规范里的隐藏条件。在FPGA里做这个计算内存带宽够的话可以一边收数据一边累加数据收完校验和也差不多算好了不增加额外时延。UDP的组包顺序也有讲究。正确的UDP包排列是IP头20字节协议类型填0x11即UDP UDP头8字节源端口2字节、目的端口2字节、UDP长度2字节、校验和2字节 载荷数据最多1480字节因为IP包最大1500字节减去20字节IP头我设计的时候没有把整个以太网帧在内存里拼好了再发送而是用了一个“流式组包”的思路MAC发送状态机按顺序发送——先是固定的以太网头然后IP头然后UDP头逐字节从寄存器里取等这些头都发完了再去FIFO里读载荷数据。这样做的好处是内存占用小而且不需要一个完整的“帧缓冲区”。4.2 上位机联调流程与踩坑记录硬件和逻辑都做好之后联调这一步能筛掉一半以上“看起来能跑”的设计。我建议的调试顺序是第一步先用串口或者逻辑分析仪确认PHY芯片的寄存器配置正确。比如读PHY的链接状态寄存器看是不是已经检测到网线插入并且协商到了千兆模式。如果这个状态不对后面全部白搭。这一步连FPGA MAC逻辑都不用写先用一个最简单的MDIO读写模块去操作PHY寄存器确认芯片活着。第二步把FPGA的MAC发固定数据——比如循环发“0x55 0xAA”这种交替模式——然后用示波器或者逻辑分析仪看TX_CLK和TXD。这里确认RGMII发送时序和数据值都是对的再去搞协议。第三步跑ARP。PC端ping FPGA的IP抓包看ARP请求有没有进来FPGA有没有回ARP响应。用Wireshark在PC上抓包最直观。第三步如果通了基本说明接收通路和发送通路都能工作。接着再跑ICMP Echo如果Ping通了说明IP层校验和没问题。最后才是UDP业务数据的联调。写一个简单的Python脚本用socket发送UDP数据FPGA回环回来PC端接收比对数据是否一致。我调试UDP回环时遇到过一个问题PC端能收到回环数据但数据的前两个字节总是错的。查了很久最后发现是发送状态机的“帧间间隔”太短——以太网标准规定两次发送之间至少要有96比特时间的间隔换算成千兆就是960纳秒。我当时的发送状态机前一帧发送完成之后立刻开始下一帧的前导码没有等够IFG。这个在“收发包”场景下可能不见得会出错但如果对端是严格的交换芯片或者网卡就会莫名其妙地丢包。后面我在发送状态机里加了一个计数器帧结束后至少等96个时钟周期再开始下一帧问题立竿见影地消失。4.3 常见问题速查表现象可能原因排查方法PHY链路状态灯不亮PHY供电或复位异常MDIO配置没写进去先测PHY的电源电压和复位信号用MDIO读寄存器0确认芯片能响应能收到ARP请求但不回响应MAC地址比较逻辑出错ARP响应帧组包有问题用逻辑分析仪抓RX侧数据对照Wireshark上的ARP请求内容逐一比对Ping不同但ARP能通IP校验和计算有误ICMP类型代码字段填错在FPGA侧把收到的IP头和ICMP头全部通过串口打出来跟Wireshark抓包对比收到UDP数据但回环后PC收到乱码数据拼接时高低字节顺序错FIFO读写顺序不对用0123456789...这样的递增数据测试看错位的方式能快速定位大包1400字节以上容易丢FIFO深度不够IFG时间不足异步FIFO跨时钟域没处理好先检查FIFO的满信号是否提前拉高再确认IFG计数器是否设置正确长时间跑之后时不时丢一包跨时钟域处理有亚稳态残留复位释放时序有问题用约束报告检查RX_CLK到系统时钟之间的路径是否加了同步器确认复位释放按层次进行4.4 性能实测与扩展方向全部调试完之后我做了个简单的吞吐测试PC通过千兆网卡持续发送1400字节的UDP包到FPGAFPGA回环发回PC统计接收率。实测下来在100MHz系统时钟下处理能力大概能到600-700Mbps瓶颈出在接收FIFO的读速度和MAC状态机的处理效率上。如果把系统时钟提到150MHz或200MHz并且把FIFO宽度从8位改成32位跑满千兆线速是没有问题的。这个项目后续可以扩展的方向很多。如果你有图像采集的需求可以把UDP载荷换成图像数据配合上位机显示就成了一个零CPU占用率的实时视频传输方案。如果要接更多传感器或更复杂的控制逻辑可以把回环改成“查表转发”——根据UDP包里的目标端口分发到不同的内部模块。再往前走一步就是独立做一个带PCIe接口的千兆网卡用DMA把数据从网口直接搬到主机内存那就是一个完整的智能网卡雏形了。不过那是另一个大工程等哪天我把PCIe的坑也踩完了再写出来跟大家分享。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进