☰
FPGA UDP千兆网通信:KC705/KCU105纯Verilog协议栈实现与调试
2026/10/22 11:36:42 网站建设 项目流程

做FPGA高速数据传输的兄弟应该都有体会——正经项目里跑得最多的反而不是TCP,是UDP。尤其像KC705、KCU105这类开发板,接到千兆交换机或者直接和工控机通信,用Verilog从零搭一个UDP协议层,把包解析和封装全部交给逻辑门,比往板子里塞一个软核跑协议栈要省心得多,性能也更稳定。这篇文章就把我在KC705和KCU105上做UDP千兆网通信的完整方案整理出来,核心的协议层是纯Verilog实现,从RGMII物理接口、MAC帧、ARP、IP到UDP端口过滤全部覆盖,文末附上我在调试过程中踩过的坑和定位方法。

这套方案做出来之后,我直接把它复用到图像采集、高速ADC数据回传、以及和上位机做实时控制交互这几个项目里,效果都很稳定。适合那种“数据单向灌给上位机”或“上位机下发短命令”的场景——比如示波器数据传输、功率分析仪波形上传、软件无线电IQ数据搬移。如果你正在为某一款FPGA开发板找不到合适的网络通信方案发愁,或者你手上有KC705/KCU105想跑通千兆网,这篇内容应该能帮你少走很多弯路。

1. 为什么丢开现成协议栈,用Verilog手写UDP

1.1 软核协议栈的吞吐瓶颈

大部分人拿到FPGA开发板,第一反应是在MicroBlaze或者软核里跑LWIP。这个方案的优点是省事:MAC驱动、ARP、IP、TCP/UDP全都有现成的库,只要配置一下DMA和中断就行。但实际跑起来你会发现三个痛点。

第一是吞吐上不去。千兆口的物理带宽是125MB/s,但LWIP跑在中低端软核上,纯接收转发能到400Mbps就不错了,如果你还要同时处理配置、响应中断、拷贝内存,吞吐会进一步掉到200Mbps以下。第二是CPU占用太高,软核大部分时间都在搬运数据,根本没有余量做其他事务处理。第三是延迟抖动,软件协议栈的延迟受中断、缓存、调度影响,波动很大。对数据采集类项目来说,延迟抖动意味着采样点对不齐,比带宽不足还麻烦。

1.2 纯逻辑UDP方案能换到什么

把协议层全部下沉到Verilog之后,数据路径变成了纯硬件流水线:PHY芯片把串行信号变成RGMII并行数据,协议栈状态机直接拆包、查表、拼帧,每一步都是固定的时钟周期。收包延迟可以做到微秒级且完全确定,发包方向只要FIFO里有数据,就能按线速往外发,理论上接近千兆满速。

我实测在KC705和KCU105两套板卡上,用1KB的UDP包做回环测试,纯FPGA转发那一段的速率可以稳定跑到930Mbps以上,剩余开销主要是帧间隙和前导码。这个数字对大多数实时采集应用已经完全够用。

1.3 整体架构先看一眼

下面用文字把数据通路描述一下,和实际工程里的模块划分是一一对应的:

  • 物理层:板载千兆PHY,对外接RJ45,对内输出RGMII信号
  • RGMII接口层:负责时钟域转换、DDR数据采样、拼接成8bit字节流
  • MAC层:以太网帧的前导码检测、FCS生成与校验、帧间隙控制
  • 协议层:ARP请求/应答、IP首部校验和、UDP端口匹配
  • FIFO缓冲层:收包和发包各一组异步FIFO,隔离PHY时钟域与用户逻辑时钟域
  • 用户接口:AXI-Stream读写接口,位宽可以按需设为8/32/64

这个架构不追求花哨,每个模块只干一件事,方便分块仿真和板级调试。

2. 协议栈模块拆解:从RGMII到UDP的完整数据通路

2.1 收包通路:前导码过滤到用户数据输出

先说收包方向。PHY芯片恢复出来的接收时钟是125MHz,RGMII的接收数据线在时钟的上下沿各采一个bit,所以FPGA内部先把4bit DDR数据拼成8bit数据,同时用rx_ctl信号区分当前线上到底是数据还是控制信息。拼接完成之后,MAC层开始扫描前导码和SFD,也就是7个0x55加1个0xD5,一旦匹配成功,后面紧跟的就是真正的以太网帧。

以太网帧头处理比较机械:前6字节是目的MAC,后6字节是源MAC,再2字节是EtherType。EtherType为0x0800表示IPv4,走IP解析;EtherType为0x0806表示ARP,走ARP处理。我是把这两个分支做成两个并行状态机,入口处用握手信号分发,避免互相阻塞。

进入IP层之后,需要检查首部长度、版本号(必须等于4)、协议号(必须等于17也就是UDP)。这一步还要做IP首部校验和验证——和发送端不同,接收端不能用“校验和为0就跳过”的偷懒写法,必须把整个首部按16bit累加、回卷、取反,再和包里的校验和字段比对。

UDP层就简单多了,端口匹配、包长度校验、把payload写入收包FIFO。我在这套设计里做了一个可配置的端口寄存器,通过AXI-Lite接口或者板级拨码设置本机端口,不需要每次改RTL重编译。你在工程里完全可以把端口固定成某个值,比如5000,属于纯写死流派,也能正常工作。

收包通路的最后一步是FCS校验。CRC32的实现在后面第5节会展开,这里只想提一句:如果CRC不对,整包直接丢弃,不给用户逻辑发任何“坏包”信号。因为上层协议(IP/UDP)无法恢复坏帧里的数据,发给用户反而增加处理负担。

2.2 发包通路:状态机拼接以太网帧

发包方向是很多人一开始觉得难、其实最模板化的部分。用户逻辑往发包FIFO里写一串数据,同时通过一个简单的握手接口告诉协议栈:这包数据的长度是多少。协议栈内部自动计算UDP长度、IP长度,然后按顺序往外发以下内容:

// 简化的UDP发送状态机(核心骨架) localparam IDLE = 3'd0; localparam WAIT_MAC = 3'd1; localparam PREAMBLE = 3'd2; localparam HEADER = 3'd3; localparam PAYLOAD = 3'd4; localparam CRC_SEND = 3'd5; localparam IFG = 3'd6; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; end else begin case (state) IDLE: begin if (user_wr_en && arp_mac_ready) begin pkt_len <= user_data_len; state <= PREAMBLE; end // 如果ARP缓存还没有目的MAC,则触发ARP请求并停留在IDLE else if (user_wr_en && !arp_mac_ready) begin arp_request_trigger <= 1'b1; end end PREAMBLE: begin // 发送8字节前导码+SF D,前导码固定55 55 55 55 55 55 55 D5 if (preamble_cnt == 8'd7) state <= HEADER; end HEADER: begin // 依次发送目的MAC、源MAC、EtherType、IP头、UDP头 // 长度计数器header_cnt到达固定值后进入PAYLOAD if (header_cnt == HEADER_TOTAL_LEN) state <= PAYLOAD; end PAYLOAD: begin // 从用户FIFO读数据,每周期发送一个字节 if (payload_cnt == pkt_len - 1) state <= CRC_SEND; end CRC_SEND: begin // 连续发送4字节CRC32结果,发送完成后进入IFG if (crc_cnt == 3'd3) state <= IFG; end IFG: begin // 连续96个bit的空闲周期,然后回到IDLE if (ifg_cnt >= 11'd11) state <= IDLE; end endcase end end

这里有个细节要特别注意:帧间隙IFG最小值是96bit,对应千兆以太网就是12个时钟周期。如果协议栈在发送完成后立刻开始下一包,PHY芯片或对端交换机可能把这包丢弃。我在工程里把IFG设为12个周期,实测没有出现过帧间间隙不足的问题。

IP头里的校验和是这么算的:把IP首部按16bit一组做累加,累加过程中如果产生进位,就回卷加到最低位,最后取反。UDP校验和在IPv4里是可选的,很多实现直接填0,对端也能接受。但为了严谨,我实现了完整的UDP校验和计算,它需要把伪首部(源IP、目的IP、协议号、UDP长度)连同UDP头和数据一起累加。接收端看到非零校验和时也会主动校验,如果算出来不匹配,同样丢包。

2.3 ARP模块:没有它连网线都白插

裸发UDP包之前必须知道目的MAC地址。实际项目里,FPGA对端往往就是一台PC机,PC的MAC地址是可变的、事先不可预知,所以必须走ARP流程。

这套设计的ARP模块分三段:构造并发送ARP请求、处理ARP应答、维护一个简单的MAC缓存寄存器。请求格式是固定的:目的MAC填全F广播,操作码填1,发送方MAC和IP填本机值,目标IP填PC的静态IP。PC收到后会回一个操作码2的应答,ARP模块从应答帧里提取源MAC地址,写进缓存寄存器,同时拉高arp_mac_ready信号,通知发包状态机可以干活了。

缓存机制不需要做成CAM表,因为大多数场景就是一个FPGA对一个PC。我用一个32位超时计数器,如果超过30秒没有ARP应答更新,就把缓存的MAC清掉,下次发包前重新触发ARP请求。之所以要做超时,是因为PC换了网卡或者IP被别的设备占了,老MAC会一直导致发出去的包没人理。

3. 在KC705和KCU105上移植:同样的RTL,不同的物理层

3.1 两套板卡的物理接口差异对照

我最初在KC705上把整个工程调通,然后几乎没改RTL就移植到了KCU105。听起来很顺,实际还是被物理层的差异绊了两跤。

对照项KC705板卡KCU105板卡
FPGA系列Kintex-7Kintex UltraScale
PHY接口模式RGMIIRGMII(也可配置GMII)
板载PHY地址受硬件上下拉影响,默认地址不同默认地址不同
参考时钟来源板上晶振/PHY输出板上晶振/PHY输出
引脚约束需按原理图改管脚需按原理图改管脚
PHY寄存器初始化要重新写MDIO配置要重新写MDIO配置

RTL本身完全不用动,因为协议栈看到的是RGMII接口,至于PHY是哪家芯片、内部寄存器怎么配,都被隔离在物理层之下。但有两件事必须重新做:管脚约束和PHY初始化代码。

3.2 管脚与时钟约束的正确姿势

管脚约束没有技巧,就是对照原理图一个个改。容易出错的是时钟约束。RGMII的接收时钟是PHY恢复出来的,频率精确等于125MHz,这个时钟要作为异步时钟声明给收包逻辑。发送时钟则正好相反,TX_CLK由FPGA内部产生,可以PLL生成,也可以直接由125MHz全局时钟驱动。

# 关键XDC约束示例(实际工程需按板卡原理图调整) set_input_clock -name eth_rx_clk -period 8.000 [get_ports eth_rxc] set_output_clock -name eth_tx_clk -period 8.000 [get_ports eth_txc] # RGMII接收信号输入延迟,按PCB走线长度给出估计值 set_input_delay -clock eth_rx_clk -max 2.500 [get_ports {eth_rx_ctl eth_rxd[*]}] set_input_delay -clock eth_rx_clk -min 1.500 [get_ports {eth_rx_ctl eth_rxd[*]}] # RGMII发送信号输出延迟 set_output_delay -clock eth_tx_clk -max 2.500 [get_ports {eth_tx_ctl eth_txd[*]}] set_output_delay -clock eth_tx_clk -min 1.500 [get_ports {eth_tx_ctl eth_txd[*]}]

上面的输入输出延迟数值并不是拍脑袋写的,RGMII标准要求在时钟沿中心采样,PCB延时加上FPGA内部走线大概就是1.5到2.5ns这个区间。但如果你板卡走线比较长,或者PHY芯片本身有可编程延迟,就需要用在线逻辑分析仪配合发固定pattern来实测。不要试图在RTL里手动加IDELAY来试错,除非你明确知道PHY芯片和PCB的延迟特性。

另外提醒一点:KCU105和KC705的RGMII引脚bank可能不同,VCCO供电电压也不一定一致。综合工具里如果报IO标准不匹配,先检查是不是VCCO接的1.8V而你把IO标准写成了3.3V。

3.3 复位与PHY初始化:上电最容易丢帧的环节

很多人上板后第一件事ping不通,不是协议栈问题,而是PHY根本没有进入千兆工作状态。千兆PHY上电后默认是自协商的,需要几十毫秒才能完成与对端PC网卡或交换机的协商。如果FPGA复位比PHY早释放,协议栈在PHY还没有link up时就开始发包,这些包全部石沉大海。

我在工程里做了一个简单的"PHY ready"判断逻辑:上电后拉低PHY复位至少30微秒,然后通过MDIO接口周期性读取PHY状态寄存器,等到bit表示link up且协商结果为1000M全双工,再释放协议栈复位。两个板卡读的寄存器地址不同,PHY地址也不同,这部分是移植时唯一要小心的寄存器级代码。

3.4 AXI-Stream接口适配的取舍

协议栈内部全是8bit字节流,但用户逻辑往往需要32bit或64bit位宽。我用两组异步FIFO把8bit转成32bit,在AXI-Stream接口上做字节对齐处理。这里有一个小坑:UDP payload长度不一定是4的倍数,所以FIFO读侧必须保留last信号和byte valid信号,否则用户逻辑会把padding数据当成有效内容。

我的做法是:发送方向,用户逻辑提供32bit数据和对应的字节使能(比如wr_strb),协议栈把有效字节拼接成连续的8bit流;接收方向,协议栈把payload按32bit对齐存入FIFO,最后一个字的无效字节用tkeep信号标出来。如果你直接对接DMA控制器或者图像数据流,这个细节能省掉很多上层软件的麻烦。

4. 仿真与上板:怎么验证这套UDP真的能跑起来

4.1 搭建一个能复现帧结构的testbench

RGMII时序在仿真里比较啰嗦,我的建议是分两步走:先用行为级的8bit GMII接口做功能验证,协议栈逻辑跑通了,再加一层RGMII接口转换做时序仿真。testbench里需要构造标准以太网帧,关键代码结构如下:

// testbench中构造一帧UDP数据的核心逻辑 task send_udp_frame(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[]); // 1. 发送前导码 55 55 55 55 55 55 55 D5 // 2. 发送目的MAC和源MAC // 3. 发送EtherType 0x0800 // 4. 发送IP头:版本/长度0x45,总长度=20+8+payload.size() // 协议号17,源IP、目的IP,IP校验和 // 5. 发送UDP头:源端口、目的端口、UDP长度,校验和 // 6. 发送payload // 7. 计算并发送CRC32 endtask

仿真验证顺序很重要:先让PC侧发一个ARP请求,验证ARP模块能正确应答;再让PC侧发一个UDP包,验证收包FIFO拿到正确payload;最后让FPGA主动发包,从testbench侧抓RGMII输出数据流,人工检查帧头字段。这三步都过了,再上板。

4.2 上板联调:从ping通到回环验证

把PC网卡和FPGA板卡的RJ45直连,PC设置静态IP,FPGA在逻辑里也配置好同一个网段的地址。第一步跑命令ping,注意ping不是UDP协议,走得是ICMP。我的协议栈里没有做ICMP响应,所以严格说ping不通才是正常的——但很多第一次上手的人在这里会慌,以为板子坏了。

正确的上板验证顺序是先用网络抓包工具。PC网卡设好IP后,从FPGA持续发送UDP包到PC,PC上用抓包软件能看到源源不断的UDP帧,这说明ARP、发包状态机、RGMII时序全部正常。然后再从PC发UDP包到FPGA,FPGA收到后把源和目的IP、端口对调,原样发回,形成回环。抓包工具里能看到回包,说明收包通路也通了。

4.3 速率和丢包因子:别把PC的锅甩给FPGA

一套链路做速率测试,瓶颈往往不在FPGA端。我遇到过用USB千兆网卡测试怎么都跑不满的情况,换板载PCIe网卡之后速率立刻上去了。Windows系统的UDP接收缓冲默认值也很小,发太快直接丢包,这不是FPGA丢的。

环节可能瓶颈验证手段
FPGA → PCPC网卡驱动/系统UDP缓冲用低速率递增压力,统计首丢包点
PC → FPGAPC发送线程调度/网卡发送队列调整发送缓冲,查看FPGA端计数
直连双工协商网线质量或强制双工不匹配检查PHY状态寄存器

我在FPGA内部放了一组统计计数器:收包数、发送数、CRC错误数、IP校验错误数、FIFO溢出数。上板联调时通过一个串口或GPIO读出来,就能精确确认丢包到底发生在哪个环节。这个习惯强烈推荐保持,比反复抓包定位快得多。

5. 调试记录:最容易卡住进度的三个坑

5.1 CRC校验失败:位序和极性一起坑你

第一次上板时,PC能收到FPGA发的帧,但收包软件提示FCS错误。FPGA发出的帧本身完整,问题出在我生成CRC32时的位序上。

以太网CRC32的输入和输出都是按位处理的,而且FCS字段在线上传输时是按“最低有效位先发”的顺序。很多文档只说“CRC32多项式0x04C11DB7”,不说字节序和位反转,导致直接照抄普通CRC模块会错。

排查过程是这样的:我先用抓包工具抓了一帧正常PC发出的UDP包,把整个帧(不含FCS)喂给我的CRC模块,发现算出的结果和帧尾的FCS对不上。接着对比了几组数据,确认需要在CRC计算开始前把输入字节按位反转,计算完的结果也要按位反转再逐字节发出。改完这两处,FCS校验立刻全对。

5.2 跨时钟域丢包:异步FIFO深度和指针同步

同一套RTL在KC705上连续发几个G数据都没事,换到KCU105上突发大包时偶发丢包。一开始以为是时钟频率偏差,后来在统计计数器里发现fifo_overflow_cnt不为零——收包FIFO溢出了。

原因在于RGMII接收时钟是PHY恢复的,用户逻辑时钟是板上另一个参考时钟,两个时钟频率标称都是125MHz,但实际存在微小偏差。长时间运行后,如果用户逻辑读数据速度稍微慢于PHY接收速度,FIFO就会慢慢堆积,最终溢出。

解决方式不是加大FIFO,而是加背压信号。正确做法是:收包FIFO快满时,协议栈通过MAC层的暂停机制通知对端停止发送。没有暂停帧功能的简化实现,至少要在FIFO水位超过阈值时丢弃新包并且计数,避免水位持续增长导致溢出后指针错乱。

提示:异步FIFO的满空判断必须用格雷码跨时钟域同步,随便用一个计数器的二进制值直接跨时钟比较,会出现满空信号抖动,偶发丢包查都查不到。

5.3 RGMII时序约束问题:仿真正常不代表上板正常

还有一个典型的坑:RGMII接口在仿真里怎么跑都正确,上板后却出现“每几百帧错一帧”的偶发错误。这不是逻辑功能错,而是时序余量不够,属于约束问题。

排查方法是在在线逻辑分析仪里同时抓RGMII的rxd和rx_ctl,对照已知的合法帧头,看是不是rx_ctl和rxd在采样沿上出现了亚稳态。因为RGMII是DDR模式,采样沿和中心对齐的要求比较苛刻,如果输入时钟相位不对,导致setup或hold不满足,就会偶发采错bit。

我在KCU105上实际调了两次输入延迟参数,最后把延迟设在2.0ns左右才稳定。不要相信仿真波形里的漂亮眼睛图,上板测不到的偶发错误,十有八九是时序约束太松。

5.4 调试工具组合:统计计数器比看波形更高效

最后分享一个我个人认为最有价值的调试习惯:在每个版本里都保留统计模块。整套计数器用一组32位寄存器实现,包括MAC收包总数、CRC错误数、ARP请求发送数、ARP应答接收数、IP校验错误数、UDP收包数、UDP发包数、FIFO溢出数。

上板的时候,先用PC端持续发包,隔几秒读一次这些计数器,对比PC发了几包、FPGA收了几包。哪个计数器不涨,问题就在哪个模块;哪个计数器疯涨,就是对应环节在报错。这套方法比在逻辑分析仪里翻波形快十倍,尤其适合你同时调协议栈和上位机软件的阶段。我现在每做一个带网络接口的项目,都会先花十分钟把这个统计框架搭进去。

用Verilog在KC705和KCU105上实现UDP千兆网通信,核心工作量其实不大,难点集中在RGMII时序、CRC正确性和跨时钟域处理这三个地方。只要把这几个硬骨头啃下来,这套协议栈几乎可以平移到任何带千兆PHY的FPGA板卡上。我记得最初两星期都在和CRC死磕,后来整套模块稳定下来,后面几个项目基本就是复制、改引脚、调约束,一晚上就能跑通。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询