ARTICLE DETAIL

资讯详情

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

FPGA实现UDP以太网通信:从verilog-ethernet开源工程到上板回环实战

FPGA实现UDP以太网通信:从verilog-ethernet开源工程到上板回环实战 这个系列写到第10篇了前面的实验基本都是数码管、串口、IIC、SPI这些基础玩法。这一篇我决定换口味给自己的目标是在FPGA里跑通一个能和PC真机通信的UDP以太网通路。刚开始也尝试过自己从零写UDP协议栈写了几天就卡在IP头校验和、ARP交互这些协议细节上后来换了思路先把开源工程verilog-ethernet啃下来再基于它做裁剪和回环验证。verilog-ethernet是Alex Forencich维护的开源FPGA以太网项目里面是完整的MAC、IP、ARP、UDP协议栈代码模块划分非常干净。这篇笔记就按我自己的学习顺序记录怎么理解PHY/MAC关系、怎么读代码以及怎么把工程跑上板子并和PC互通的完整过程适合和我一样处于近似0基础阶段的FPGA学习者。1. 学这个工程之前建议先想清楚这三件事1.1 MAC、PHY、媒体接口第一课到底该补什么很多初学者拿到以太网工程第一反应是去找“以太网帧格式”然后就盯着目的MAC、源MAC、类型字段看。这些当然重要但如果你连板子上那颗PHY芯片是干什么的都没搞明白后面接引脚和调时序会非常痛苦。FPGA不是直接把网线变压器出来的差分信号接进可编程逻辑里处理的开发板上一般会有一颗PHY芯片常见的有瑞昱的RTL8211系列、Marvell的88E1512系列等。这颗PHY负责物理层的活线路编码、时钟恢复、自协商、载波侦听它把物理线路上的模拟/串行信号转成MAC层能直接吃的数字接口。FPGA里跑的是MAC层以及MAC之上的IP、ARP、UDP协议逻辑。MAC和PHY之间的数据接口常见有三种GMII、RGMII、SGMII。GMII是8位并行数据加时钟千兆下时钟125MHzRGMII用DDR双沿采样引脚更少SGMII则是通过SerDes串行收发通常需要FPGA内部的GTP/GTX等高速收发器来实现比如Xilinx 7系列里的GTX。verilog-ethernet里专门有eth_mac_1g这样的千兆MAC核也有支持SGMII的示例工程。你把“MAC”理解成负责组帧、算CRC、按包收发把“PHY”理解成负责把MAC给的数据变成能上线的电信号这条主线就清晰了。1.2 为什么FPGA跑协议栈用的是数据流思维而不是回调写单片机或上位机程序时网络通信往往是“调用一个socket API内核去处理然后回调通知你”。FPGA里没有这种“回调”概念代码风格几乎全是数据流。你给一个模块喂字节流它给你吐字节流中间用AXI-Stream这种接口握手机制贯穿。AXI-Stream有几个最关键信号tvalid和tready表示握手tdata是数据tlast表示一帧结束tkeep表示当前拍tdata里哪些字节有效tuser则用来携带错误标志等附加信息。verilog-ethernet里几乎所有模块边界都是这套接口。比如接收链路PHY/MAC把一帧数据转成AXI-Stream流IP层从流里解析出头字段UDP层再把载荷通过AXI-Stream送给用户逻辑。所以学这个工程之前我强烈建议你先写一个简单的AXI-Stream FIFO把tvalid/tready/tlast/tkeep这套握手玩熟。否则你打开udp_complete_rx源码时满屏都是if (s_axis_rx_tvalid s_axis_rx_tready)这类条件会看得很晕。1.3 你的板子适合用GMII、RGMII还是SGMIIverilog-ethernet官方示例里常见的是SGMII方向因为Xilinx开发板很多用GTP/GTX实现SGMII再接外部PHY或者光模块。但如果你手上的板子是国产或者入门级开发板PHY接口往往是GMII或RGMII比如黑金的AX7010、正点原子的开拓者系列很多板载PHY都是RTL8211系列走RGMII或GMII。我建议你先打开自己开发板的原理图找到PHY那一页确认FPGA与PHY之间的接口是什么。如果是RGMII就去看仓库里的rgmii_phy_if相关模块如果是GMII就直接用eth_mac_1g搭配GMII接口如果是SGMII再去参考官方给Xilinx 7系列做的example。这里最容易踩的坑是照着别人的SGMII example抄结果自己板子根本没有GTP/GTX引脚引出或者参考时钟都不对导致完全跑不起来。2. verilog-ethernet这个仓库到底放了什么该先读哪几个文件2.1 rtl目录的核心模块分层打开verilog-ethernet仓库不要先急着看example建议先看rtl目录。核心模块大概可以分成下面几层。模块层级作用eth_mac_1gMAC千兆MAC核处理前导码、帧间隙、FCS校验eth_axis_rx / eth_axis_tx接口适配把MAC收发的数据转成AXI-Streameth_arb_mux发送仲裁多路以太网帧发送仲裁ip_eth_rx / ip_eth_txIP层处理IPv4帧解析和封装ip_complete_rx / ip_complete_txIP层对用户提供完整的IPv4收发接口udp_checksum_gen / udp_checksum_calc校验和UDP校验和生成与校验udp_complete_rx / udp_complete_txUDP层对用户提供完整的UDP收发接口arp_cache / arp_eth_rx / arp_eth_txARP层ARP缓存与请求/应答处理axis_fifo / axis_fifo_adapter基础设施AXI-Stream FIFO和位宽适配在lib/axis下一开始我也试图从eth_mac_1g开始看结果发现它要处理CRC32、流控、帧间隔这些东西细节特别多很容易陷进去。后来调整顺序直接从udp_complete_rx和udp_complete_tx入手因为它们是把UDP收发封装好的“最接近用户”的模块。看完这两个之后再往下钻到ip_complete层最后再去翻eth_mac整个层次感会清楚很多。2.2 建议先读udp_complete再往下钻udp_complete_rx和udp_complete_tx本质上是把IP层、UDP层的一些拆包组包细节封装成一个用户友好的接口。用户侧只需要关心AXI-Stream的数据、有效信号、帧结束信号不需要手动计算IP头长度、UDP长度、校验和。读udp_complete_rx时建议关注两个点一是它内部例化了什么子模块二是它的数据流是怎么走的。你会看到它内部调用了ip_complete_rx、arp缓存、可能的校验和模块等这些调用关系本身就是一张很好的协议栈结构图。读udp_complete_tx时关注它是如何把用户发来的数据拼成完整IP帧的以及它为什么需要在发送前查ARP缓存。如果一上来就对着eth_mac_1g里面的CRC表死磕可能一个星期后你还在调前导码和CRC这对以“跑通UDP”为目标的初学者来说性价比很低。2.3 最小引脚清单时钟、复位、AXI-Stream、PHY接口从顶层往下看一个最小的UDP收发链路用户侧至少关心这些信号时钟clk一般就是MAC接口时钟或经过PLL处理后的逻辑时钟复位rst_n低有效要确保同步释放。发送侧用户接口s_axis_tx_tdata / tvalid / tready / tlast / tkeep把要发出去的数据送进去。接收侧用户接口m_axis_rx_tdata / tvalid / tready / tlast / tkeep从协议栈读数据。PHY侧接口根据你用的是GMII还是RGMII会有TXD、RXD、GTXCLK、RXCLK等引脚。如果走SGMII还要把GTP/GTX的参考时钟和收发方向引脚接出来。很多初学者把注意力全放在数据总线上忽略了PHY接口和时钟复位结果上板后数据线根本没波形。其实调试时可以用逻辑分析仪或者ILA先抓PHY侧的RXDV和RXD确认物理链路是不是通的再往上查IP层和UDP层。3. 接收链路逐段拆解从网线到用户手上的UDP数据3.1 MAC适配层如何把帧变成AXI-Stream接收方向的第一步是PHY把线上信号恢复成MAC层数据然后eth_mac_1g做前导码检测、FCS校验再通过eth_axis_rx把以太网帧打包成AXI-Stream流。这层要关注几个关键点eth_axis_rx会把一帧数据以tlast标记结束如果CRC错误它会把tuser拉高。这样上层协议模块看到错误帧时可以选择丢弃。这就是为什么读verilog-ethernet时不能跳过AXI-Stream握手原因。比如IP解析模块可能正在等一整帧数据过去只有等到tlast才知道帧结束了然后根据长度字段判断IP头里声明的数据长度和实际收到的帧长度是否一致。如果这一帧中途被CRC错误打断tuser会告诉上层这个帧无效。3.2 IP层到底做哪些合法性检查不是一上来就找UDP以太网帧里的EtherType字段是0x0800表示IPv40x0806表示ARP。ip_eth_rx在拿到一个帧后第一步是看EtherType不是IPv4的帧直接丢给其他模块处理。如果是IPv4IP层会检查版本号是不是4IHL是不是标准20字节目的IP是否等于本机配置的IP以及是否广播地址。然后它会计算IP头校验和如果校验和不通过直接丢弃。接着看protocol字段如果是17就交给UDP层如果是1就交给ICMP模块比如ping用的就是ICMP这就是为什么“能ping通”和“UDP通”是两个不同层次的验证。这里有个容易忽略的点IPv4分片。PC默认情况下如果UDP包太大IP层会把数据分片而verilog-ethernet这种硬件协议栈通常不做分片重组。所以我们在PC端发送UDP包时尽量把数据长度控制在1472字节以内也就是MTU减掉IP头和UDP头否则FPGA端可能只会收到第一个分片后面分片会被丢弃或行为异常。3.3 UDP层如何匹配端口并输出负载udp_complete_rx在IP层之上它做的主要事情有解析UDP头里的源端口、目的端口、长度、校验和检查目的端口是不是本模块配置的端口号如果匹配就把UDP载荷部分通过AXI-Stream输出给用户。UDP数据包头固定8字节用户拿到的数据是从UDP头之后开始的。UDP长度字段的值是8字节头加上载荷长度校验和计算时用的是伪头部这部分我在第5节会详细展开。端口匹配不上的包协议栈一般会直接丢弃不会给你产生中断或者报警所以调试时经常出现“明明收到包了用户接口就是没数据”的情况这时候先检查端口号配置对不对。3.4 接收侧的tuser和tlast为什么要处理我见过一些初学者在回环代码里只接了tdata、tvalid、tready没接tlast和tuser结果数据偶发错乱。原因是UDP包数据长度不一定是8字节的整数倍接收侧最后一拍可能只有几个字节有效必须通过tkeep判断而tlast告诉你这一帧到这里就结束了用户逻辑才能对一帧数据进行处理。如果忽略tlast回环可能会把两帧数据糊在一起变成一个大包再发出去PC端解析就会错乱。tuser在接收侧通常表示这一帧是否带错误标记特别是CRC错误严谨的做法是收到错误帧时把当前FIFO里的数据清掉不要送去上层的回环发送。verilog-ethernet内部很多模块已经做了错误丢弃但你自己写的FIFO或用户逻辑不一定做上板后有时会出现“PC收到错误数据”的诡异现象原因往往就在这一层。4. 发送链路逐段拆解用户数据如何变成以太网帧4.1 udp_complete_tx的发送顺序与用户接口发送方向比接收方向更符合直觉用户只需要把UDP载荷写到发送接口udp_complete_tx会按照以太网帧的格式依次生成目的MAC、源MAC、EtherType、IP头、UDP头然后把用户数据填进去最后加上FCS交给MAC层发送。但有一个点很容易忽略发送侧不是“你写一个寄存器它立刻发一帧”而是你要把一帧完整的数据通过AXI-Stream接口“喂”给它。也就是说你的用户逻辑要先把数据拼好然后拉高tvalid等tready握手持续输出直到tlast结束。我在回环设计里就是先接一个FIFO接收侧写进来的数据缓存到FIFO里发送侧则持续读FIFO读到FIFO末尾时拉出tlast。这样即使PC端发送的数据长度不是4字节或者8字节的整数倍FIFO加tkeep的逻辑也能正确处理不至于出现长度字段和实际载荷对不上。4.2 为什么第一次发包会有“延迟”ARP缓存以太网帧的目的MAC地址是要填真实MAC的但我们的UDP协议栈只知道目的IP怎么知道PC的MAC地址答案是ARP。udp_complete_tx发送前会查ARP缓存如果缓存里没有对应目的IP的MAC地址它不会立刻发UDP而是先发出一个ARP请求等PC返回ARP应答学习到MAC地址后再把缓存的UDP包发出去。所以FPGA侧第一次向PC发包时会出现几十毫秒甚至更长的延迟这不是死机是ARP流程。这也是为什么调试时先ping一下FPGA会对后面的UDP测试有帮助。PC ping FPGA时先发出ARP请求FPGA回复ARP应答同时ARP缓存里就学到了PC的IP和MAC对应关系之后UDP发送时就不需要再等ARP了。反过来FPGA先主动发UDP也会一样触发ARP请求只是第一次会因为等待ARP而稍微慢一点。4.3 多模块竞争发送时谁先发response仲裁UDP发送、ICMP回复、ARP回复这些模块最终都要往同一个MAC发送方向上去发数据所以verilog-ethernet里会有eth_arb_mux这样的模块来做仲裁。它的逻辑并不复杂本质就是一个优先级判断同一时刻只允许一路数据占用发送通道。刚开始学习时不需要太深入仲裁细节但你需要知道这个模块存在。因为如果你自己在顶层例化了ARP、IP多个发送源却没有经过仲裁直接把若干路信号接在一个总线上FPGA内部会出现多驱动冲突轻则仿真报错重则布局布线异常。verilog-ethernet的架构值得学的一点就是“每个发送源先各自保持完整帧再通过仲裁器决定谁上总线”这样整个代码结构非常干净。5. 校验和计算最容易被小白写错的部分5.1 IP头校验和与UDP伪头部的区别IP头校验和只覆盖IP头本身也就是20字节。每一个16位字相加高16位溢出回卷最后取反。而UDP校验和覆盖的范围更广它要算一个12字节的伪头部加上UDP头8字节再加上UDP载荷。UDP伪头部格式是源IP地址4字节、目的IP地址4字节、0x00一个字节、协议号0x11一个字节、UDP长度2字节。这个伪头部并不会出现在真实在线路上传输的报文里它只是用于校验和计算。很多初学者第一次写校验和模块时只把UDP头和数据加起来算忘了伪头部导致协议栈发出的包在PC端Wireshark里显示checksum incorrect。verilog-ethernet里udp_checksum_gen和udp_checksum_calc这两个模块分别负责生成和校验。读这两个模块的源码基本就能把校验和的硬件实现逻辑搞明白。5.2 累加器实现的关键溢出回卷计算校验和时最容易写错的不是“按位取反”而是“高16位回卷”。简单写法是每次加完一个16位数如果产生了第17位就把它再加回低16位直到没有进位为止。如果直接用sum sum data16然后截断低16位算出来的校验和是错的。verilog-ethernet里的实现一般不会写成串行循环而是用流水线方式处理因为串行循环做不了高带宽。但对初学者来说先把“回卷”这个逻辑理解清楚更重要。// 一段示意代码仅说明回卷逻辑 reg [16:0] sum_tmp; always (*) begin sum_tmp {1b0, sum} {1b0, data16}; if (sum_tmp[16]) begin sum sum_tmp[15:0] 16d1; end else begin sum sum_tmp[15:0]; end end这段只是演示思路真正的流水线实现还要考虑多拍延迟。重点在于理解校验和的载体是16位但累加过程要保留进位并回卷。5.3 校验和为0不是错误千万分清IPv4规定UDP校验和可以不计算把校验和字段填成0x0000。接收端看到0时会跳过校验不认为这是一个错误。而IPv6里UDP校验和是强制的必须计算。所以调试时如果Wireshark显示“UDP checksum: 0x0000”或者在FPGA端看到收到的UDP包校验和字段为0不要慌这是合法的。反过来如果你希望FPGA端认真校验数据完整性就需要在udp_complete_rx里配置成强制校验非0校验和如果你只是做简单回环可以先接受校验和为0的包这样能少踩一个坑。发送端也是一样如果你图省事不把UDP校验和计算模块接上去发出去的包校验和就是0PC端Wireshark会提示“not set”但PC应用程序仍然能收到数据。不过从学习协议栈的角度我还是建议把udp_checksum_gen接进去因为这才是完整的UDP实现。6. 上板实践把最小UDP回环跑通并与PC互发6.1 选官方example还是自己搭引脚和时钟约束官方example大多基于Xilinx 7系列例如Artix-7开发板的SGMII示例。如果你的板子和example用的板子一模一样可以直接打开Vivado工程生成比特流。但实际上很多人板子不一样直接跑example基本都会因为引脚约束不对而失败。我实际走通的路径是不直接用example的顶层而是把rtl目录下需要的源码复制到自己的工程里然后根据自己的开发板引脚重新写约束文件。虽然多花了一点时间但这样做的好处是你必须把每个引脚的功能搞清楚而不是“碰运气”式地点个Generate Bitstream。时钟方面SGMII示例一般需要GTP参考时钟比如125MHz的gtrefclk还要给GTP的复位逻辑足够的时间。如果是GMII/RGMII接口时钟会更直接比如GMII的GTXCLK由FPGA给PHY提供125MHzRXCLK由PHY给FPGA提供125MHz。上板前先用逻辑分析仪确认PHY的RXCLK存在不然后面都是白调。6.2 回环逻辑怎么接接收端到发送端之间必须过FIFO最简单的回环设计就是接收侧udp_complete_rx输出的数据经过一个FIFO再接到发送侧udp_complete_tx的输入。为什么中间必须加FIFO因为接收侧输出的节拍和发送侧能接收的节拍不一定完全匹配比如发送侧可能因为ARP缓存未命中而暂时反压如果中间没有FIFO缓冲接收数据就会丢。FIFO的读侧要正确处理tlast。我的做法是当FIFO读出一个标记有tlast的数据时把它作为发送侧的tlast信号一起拉出同时如果在接收侧看到tuser错误帧就把FIFO复位把这一帧扔掉。端口配置上入门阶段先固定住源IP、目的IP、源端口、目的端口。比如PC IP设为192.168.1.20FPGA IP设为192.168.1.10UDP端口固定为6000。PC发包时源端口也填6000这样FPGA回环时直接把源端口和目的端口交换数据就能回来。更通用的动态端口配置可以后面再做但第一步先固定参数通路跑通最重要。6.3 PC端验证静态IP、Wireshark、Python发包PC端把有线网卡IP改成和FPGA同网段静态IP避免DHCP干扰。然后用一条网线直连FPGA开发板和PC先ping FPGA的IP。如果ping得通说明ARP、MAC、PHY底层链路都通了。拿到ping通的结果后再用Python发一个UDP包测试回环import socket s socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.settimeout(3) s.sendto(bhello fpga udp, (192.168.1.10, 6000)) data, addr s.recvfrom(1024) print(data, addr)如果回环正常recvfrom会把那个UDP包原样收回来。Wireshark可以同时开着过滤条件写udp.port 6000 || arp重点看几件事有没有ARP请求和应答、UDP包有没有被标记校验和错误、包长度是否正常。6.4 如果ping通但UDP不通下一步查什么我调试过程中最常遇到的场景就是ping通了但是UDP回环收不到数据。这时候我一般按顺序排查检查udp_complete_rx端口匹配逻辑尤其目的端口是否配置成6000。检查PC发的UDP包长度是否超过1472字节导致分片。检查回环FIFO的读侧是不是忘了把tlast送给发送端导致发送侧一直等不到帧结束。检查udp_complete_tx的目的IP和目的MAC是否配置正确。如果先用ping让ARP学到PC MAC发送效率会高很多如果没学到就观察它有没有先发ARP请求。实际上UDP不通的问题八成出在“端口没对上”或“FIFO控制逻辑不完整”不太可能是协议栈本身的问题。开源工程本身是经过大量验证的先怀疑自己的胶水逻辑能省很多时间。7. 我在这个工程里真实踩过的几个坑和排查思路7.1 复位亚稳态为什么你的板子总是第一次不通FPGA里几乎所有模块都用低有效复位rst_n但复位信号的释放时机如果不同步器件很容易进入亚稳态表现出来就是“上电第一次运行不正常按一下复位键又好了”。我之前就是直接把按键信号当全局复位用结果每次上电后PHY那边已经完成配置而协议栈内部还没稳定第一次发包必然失败。后来把两级同步器加到复位通路上并且用一个计数器产生稳定后的复位释放信号才算解决。verilog-ethernet内部模块大都带有自己的同步逻辑但你自己写的用户侧复位逻辑同样要处理好不能只靠模块内部。这个坑比较隐蔽因为仿真时复位都是理想信号上板后真实环境里会有毛刺和亚稳态。如果你也遇到“仿真全对上板时好时坏”第一反应先查复位。7.2 时钟不对SGMII恢复时钟不能直接当用户逻辑时钟用SGMII接口下接收方向的数据时钟来自GTP/GTX的恢复时钟它的频率和相位跟发送端参考时钟不一定是完全同步的。如果你把所有逻辑都用同一个恢复时钟后续跨时钟域设计就会出问题。更合理的做法是把GTX恢复时钟作为RX侧数据通路时钟把用户逻辑统一放到另一个稳定时钟域里中间用异步FIFO过渡。verilog-ethernet示例里通常会在接口处做时钟管理但你自己写顶层时不要想当然地让所有模块共用一个时钟。GMII/RGMII接口相对好一点RXCLK由PHY提供和用户时钟的关系也要根据PHY芯片手册处理。7.3 仿真过但上板丢包先查tkeep、tlast、FIFO深度仿真环境下数据都是规整的比如你总是发64字节的整数倍数据tkeep的问题根本暴露不出来。但真实CPC应用里UDP包长度可能是100字节、500字节这种非8字节整数倍数最后一拍tkeep就不是全有效如果用户逻辑没有按tkeep有效字节写入FIFO最后一个字节会错位。另外回环场景看起来简单其实存在背靠背突发数据的可能。比如PC一下子连续发多个UDP包如果接收侧和发送侧之间只有一个很浅的FIFO并且发送侧因为ARP或其他原因反压就会丢包。后来我把FIFO深度加大到1024连续突发几百个包也稳定了。所以不要小看FIFO深度它是一个典型的“仿真永远发现不了上板压测就露馅”的问题。7.4 工具链问题老版ModelSim跑不了SystemVerilog测试平台verilog-ethernet仓库里的testbench很多是用SystemVerilog写的语法更现代接口更简洁。如果你还在用老的ModelSim版本尤其是某些入门套件自带的ModelSim Intel FPGA Starter Edition很可能在编译testbench时直接报语法错误。我当时的做法是直接用Vivado自带的仿真器跑或者把测试平台里用到的SystemVerilog特性改成Verilog-2001风格的临时测试文件。如果你只是想快速验证自己的回环逻辑也可以不跑官方的完整tb而是用自己构造的简化激励给udp_complete_rx输入一个合法的UDP帧看用户接口是否有数据输出。这样既避免工具链问题也更容易定位问题。最后再分享一个小技巧调试这种多模块级联的网络工程一定要养成“分层验证”的习惯。第一步在PC上用Wireshark抓ARP确认PC和FPGA物理链路通第二步ping确认IP层和ICMP回复通第三步发UDP小包确认UDP层和用户逻辑通。每一步都只验证一个边界不要指望一次把整个链路全调通。我在做回环实验时发现很多网上代码为了省事不处理tkeep和tlast遇到非8字节整数倍的包就会卡死或长度错误所以回环逻辑里一定要以tlast作为一帧结束的边界而不是按固定长度去截。把这条链路走通之后你会发现后面再做FPGA图像传输、高速接口之类的项目心里会踏实很多。
返回列表