FPGA实现TCP/IP通信:方案选型与实战调试全攻略
2026/9/2 9:52:31 网站建设 项目流程

简介:面向FPGA开发者的一套TCP/IP通信工程,定位于有硬件设计基础的工程师与学生,解决在Vivado或Quartus II中实现千兆/百兆网口协议栈并开展回环测试的需求。工程覆盖从MAC层帧封装、IP路由到TCP握手与可靠传输的完整逻辑,同时适配10M/100M/1000M速率,适用于网络设备验证、数据回环测试及嵌入式网络系统设计等场景。压缩包共289个文件、约26.36MB,文件类型以cdb/hdb工程数据库、sv/v/tdf硬件描述源码为主,另有rpt/summary综合报告、sof/sdc下载与约束文件等,目录结构由工程工具自动生成,便于按模块检索和编译部署。目前已有644人学习下载。通过学习这套工程,读者既能梳理TCP/IP各层协议在FPGA内部的实现分工,也可参照工程配置进行回环测试与线上调试,直接移植到自己的项目中二次开发,省去从零搭建协议栈的时间。

基于FPGA的TCP/IP通信

做嵌入式或者通信方向的朋友,迟早会遇到一个需求:用FPGA直接收发网络数据。我第一次在FPGA上调通TCP/IP的时候,整整折腾了两周,中间踩了无数坑,回头再看其实很多问题都是对协议栈实现细节不够了解造成的。这篇就专门聊聊FPGA上实现TCP/IP通信这件事,从方案选型到协议栈适配,再到验证调试,把关键点一次说透。

这篇文章适合三类人看:准备在FPGA上做网络通信但不知道怎么入门的,已经把UDP调通但TCP一直搞不定的,以及想在现有工程里集成以太网功能但担心资源不够的。内容偏实战,原理部分我会用尽量直白的方式讲清楚,让你看完能照着做。

1. 方案选型:先搞明白你要走哪条路

1.1 软核方案与硬核方案的区别

FPGA上做TCP/IP,第一条岔路就是选软核还是硬核方案。所谓软核,就是用FPGA的逻辑资源(LUT、FF、BRAM)搭出MAC层和IP层逻辑,协议栈可以自己写Verilog,也可以跑在软核处理器上(比如MicroBlaze、NIOS II)用C语言实现。硬核方案则是指FPGA芯片内部集成了以太网MAC硬核(比如Xilinx 7系列的TEMAC、Intel Cyclone V的EMAC),你只需要在外围配置PHY芯片,把数据通路交给硬核处理。

这个选择没有绝对的好坏,关键看你的应用场景。如果是做纯数据转发、协议转换,且对时延极其敏感(比如金融高频交易、工业实时控制),硬核或者裸逻辑方案更合适。如果是要跑HTTP、FTP这类应用层协议,需要操作系统支持,那软核跑完整的LwIP协议栈几乎是唯一选择。

我见过不少初学者一上来就想着“纯逻辑实现完整TCP/IP”,结果写了三个月连ARP都还没调通,最后项目延期。合理的路径是先评估需求,再选方案,千万不要为了炫技把自己埋了。

1.2 三种主流实现路线对比

为了让你更直观地做决策,我把FPGA上实现TCP/IP的三种主流路线整理成了一个表,你照着选就行。

实现路线典型工具/组件资源占用开发周期时延适用场景
纯Verilog/VHDL自研协议栈自定义RTL代码高(占用大量LUT/BRAM)数月到一年+极低(纳秒级)定制化需求、科研、产品化
软核处理器+LwIP协议栈MicroBlaze/NIOS II + LwIP中(CPU+LwIP内存开销)数周到数月中等(微秒到毫秒级)需要跑应用层协议的嵌入式系统
开源IP核集成为主Corundum、Verilog-Ethernet、Xilinx TEMAC低到中几天到数周(集成)标准以太网收发、NIC功能、高速数据采集

这中间有个关键认知要建立:自研TCP/IP协议栈的难度不在“发送以太网帧”这件事上,而在“可靠传输”这四个字。TCP的拥塞控制、重传机制、状态机迁移(LISTEN、SYN_SENT、ESTABLISHED、FIN_WAIT...),每一个单独拎出来都能写几万行代码。所以如果只是做“FPGA与上位机通信”,我强烈建议从UDP开始,或者直接上软核跑LwIP,先把系统跑通再谈优化。

1.3 Corundum项目值不值得引入

热搜词里出现了Corundum,这个我必须多说两句。Corundum是一个开源的、基于FPGA的高性能网卡(NIC)实现,支持10G/25G/100G以太网,实现了完整的DMA引擎、PCIe接口和部分TCP卸载功能。它做到了从PHY到MAC再到传输层的闭环,代码质量很高,而且使用了模块化设计,可移植性不错。

Corundum的移植工作量集中在平台适配层(platform-specific部分),比如PHY的初始化时序、时钟管理单元(MMCM/PLL)、PCIe的板级配置。如果你手头是Xilinx VCU118、Alveo U250这类高性能板卡,移植起来会比较顺畅。如果用的是国产FPGA或者低端Artix-7,就要做好改大量代码的准备。

我个人对Corundum的建议是:把它当作学习资料的价值大于直接使用的价值。它的代码风格非常规范,接口划分清晰,非常适合研究“为什么TCP卸载引擎要这么设计”。但真要拿到产品里用,你需要评估团队对RTL的驾驭能力——这毕竟是一个偏向NIC定位的实现,不是为通用嵌入式通信场景设计的。

2. 从零开始:搭建一个能跑通UDP的最小系统

2.1 硬件连接:FPGA网口底板怎么接

无论最终目标是UDP还是TCP,第一步都是把物理链路搭起来。FPGA的以太网接口一般由三部分组成:MAC控制器(FPGA内部逻辑)、PHY芯片(板上外部芯片)、RJ45连接器(带网络变压器)。

PHY芯片最常用的是RTL8211E(千兆)、YT8512(百兆)、88E1512(千兆)。MAC和PHY之间的接口协议,我推荐先用GMII或者RGMII。GMII是8位数据线+时钟,时钟频率125MHz(千兆),引脚占用比较多,但时序逻辑简单。RGMII是4位DDR双沿采样,引脚省一半,但时序约束比较复杂。第一次调试建议用GMII,减少变量。

这里有个细节容易被忽视:PHY地址和模式配置。大部分PHY芯片都有几个配置引脚(比如PHYAD[4:0]),板子上会通过上下拉电阻设好PHY地址。你需要在初始化时正确读出PHY的ID寄存器,确认MDIO读写正常,否则后面所有操作都是白搭。我调试的时候习惯先写一个MDIO回读测试,读取PHY芯片的寄存器0(BMCR)和寄存器1(BMSR),能正确读回0x1000和0x7949之类的值,才说明物理链路是通的。

2.2 引脚约束和时钟规划:BANK选择有讲究

关于引脚约束,我踩过一个印象很深的坑。Xilinx 7系列FPGA的BANK供电电压不同,以太网接口一定要选HR(High Range)还是HP(High Performance)BANK要看PHY的IO电平标准。RGMII/GMII通常用2.5V或1.8V的LVCMOS电平,PHY和FPGA之间必须电平匹配。

如果你用的是1.8V的PHY,就得接在HP BANK上,因为HP BANK支持1.8V的LVCMOS。如果强行接在HR BANK上用2.5V电平,轻则通信不可靠,重则烧毁IO。选型的时候先看FPGA板卡的原理图,确认以太网PHY连接到了哪个BANK、供电是多少,再决定IO标准。

时钟方面,RGMII/GMII的TX时钟由FPGA内部PLL产生,RX时钟由PHY恢复后送回FPGA。这两个时钟的频率、相位关系要在约束文件里明确声明。我常用的写法是:

set_property PACKAGE_PIN G3 [get_ports eth_tx_clk] set_property PACKAGE_PIN F4 [get_ports eth_rx_clk] set_property IOSTANDARD LVCMOS25 [get_ports eth_tx_clk] set_property IOSTANDARD LVCMOS25 [get_ports eth_rx_clk] create_clock -period 8.000 -name tx_clk [get_ports eth_tx_clk] create_clock -period 8.000 -name rx_clk [get_ports eth_rx_clk]

注意RGMII是DDR采样,还需要用set_input_delayset_output_delay约束IO延时,否则综合实现后很容易出现时序违例,表现为偶发丢包、错包。这个环节没有捷径,只能一遍遍迭代时序报告。

2.3 最小RTL数据通路:从PHY到MAC再到IP层

物理链路准备好之后,RTL侧要打通一条最小数据通路。我习惯的层次划分是:

  • eth_rx_mac:接收MAC,解析前导码、SFD、FCS校验,输出完整的以太网帧头和payload。
  • eth_tx_mac:发送MAC,本地生成前导码、SFD、FCS,从用户FIFO读取数据发出。
  • arp_rx/tx:ARP报文解析与应答,这是让上位机“能ping通FPGA”的最小基础。
  • ip_rx/tx:IP报文解析与发送,支持UDP/TCP载荷的剥壳与封装。
  • udp_rx/tx:UDP引擎,解析端口号,决定数据去向。

一个极其常见的新手错误是:只关心payload数据,不关心MAC层的FCS校验。以太网帧正确性由CRC32保证。如果接收MAC直接丢弃FCS错误的帧,而你的PCS/PMA层又有问题,就会出现“完全收不到数据”的假象。排查的时候先抓RX侧的CRC错误计数,能快速定位问题出在物理层还是协议层。

下面是一个简化的接收MAC状态机核心片段,注意状态切换的边界条件:

localparam IDLE = 3'd0; localparam PREAMBLE = 3'd1; localparam RECEIVING = 3'd2; localparam FCS = 3'd3; always @(posedge clk) begin if (!rst_n) begin state <= IDLE; end else begin case (state) IDLE: begin if (dv && rxd == 8'h55) state <= PREAMBLE; end PREAMBLE: begin if (dv && rxd == 8'hd5) state <= RECEIVING; end RECEIVING: begin if (!dv) state <= FCS; end FCS: begin state <= IDLE; end endcase end end

PREAMBLE阶段要连续收到至少6个0x55,再收到1个0xD5,才算完整的以太网前导码加SFD。很多PHY会把前导码剥掉再把数据给MAC,具体看PHY的配置寄存器,一定要确认清楚。

3. 协议栈核心实现:ARP、IP、UDP/TCP的处理方式

3.1 ARP协议的RTL实现细节

ARP是TCP/IP协议栈里最简单但也最容易出岔子的一环。它的作用是把IP地址解析为MAC地址。FPGA作为通信节点,需要处理两种场景:收到ARP请求时回复应答;发送数据前查询目标MAC。

RTL层实现ARP的关键点是缓存表。FPGA资源有限,你可以把ARP缓存做成单表项或少量表项的CAM结构。单表项的意思是,FPGA只记住最近通信的那个设备的MAC地址。这在“FPGA只和PC通信”的场景下完全够用,但如果上位机、交换机都参与,建议做成4~8项的简单缓存。

ARP应答的构造有一个细节:当收到ARP请求时,你需要用请求里的Sender MAC和Sender IP填充应答的Target字段,再把自身的MAC和IP填入Sender字段。操作码OP要设为2(Reply)。同时注意以太网帧头里的目的MAC是请求帧的源MAC,不是广播地址。

我见过一个很隐蔽的坑:ARP请求里的Sender MAC是设备自己的MAC,但你构造应答帧的时候,如果直接把收到帧的源MAC填进去,通常没问题。但如果上层PC配置了虚拟网卡、多网卡绑定,ARP请求的源MAC可能不是实际发送网卡的真实MAC。这种情况下按请求回MAC会引发间歇性通信失败,建议在应答前做一次映射校验,确认Sender IP确实在你许可的IP段内。

3.2 IP层校验和计算的流水线设计

IP协议要求每个IP头都用16位的Internet校验和(one's complement)做正确性检查。RTL实现里,这个计算要做得快,尤其是千兆接口下每个周期都要能处理新数据。

16位校验和的计算原理是:把IP头按16位一组对齐,取反累加,最高位的进位回卷到最低位,最后取反。RTL里可以做成一个两级流水线:

  • 第一级:每时钟累加一个32位数据中的两个16位半字。
  • 第二级:做进位回卷,即将累加结果的高16位和低16位相加,直到无进位输出。
reg [31:0] sum_reg; wire [15:0] sum_lo = sum_reg[15:0]; wire [15:0] sum_hi = sum_reg[31:16]; always @(posedge clk) begin sum_reg <= sum_reg + {16'd0, data_in[15:0]} + {16'd0, data_in[31:16]}; end wire [15:0] folded = sum_lo + sum_hi; wire [15:0] checksum = ~(folded[15:0] + folded[16]);

这里有个细节:RFC 1071规定,当累加过程中产生进位时,必须把进位加回到最低位,这叫“进位回卷”。如果不做回卷,校验和计算结果会偏。不过现代CPU在软件层面对这个已经做了优化,RTL里实现时务必逐周期推演,最好用SystemVerilog写一个参考模型(Reference Model)在仿真时做比对。

3.3 UDP和TCP:为什么UDP简单,TCP难在哪

UDP是TCP/IP里最容易上手的传输层协议。UDP头只有8个字节:源端口、目的端口、长度、校验和。RTL实现里甚至可以把校验和省略(IPv4下可选为0),直接实现端口匹配和数据转发即可。

UDP通信的最小状态机只有三个状态:空闲等待、解析头部、载荷传输。端口匹配成功就接收,不匹配就丢弃。发送则更简单,组好UDP头就发。很多FPGA数据采集项目用UDP搞定80%的需求,真的很实用。

TCP就不一样了。TCP是面向连接的可靠字节流协议,状态机远比UDP复杂。标准TCP状态机包含11个状态,转场条件几十种,再加上超时重传、滑动窗口、拥塞控制,纯RTL实现的工程量非常大。说实话,我不建议在普通项目里自研TCP协议栈,除非你有半年以上的时间和极强的RTL功底。

如果你确实需要TCP,我建议至少先做这三件事:

  • 搞清楚你的数据业务是否适合“一次性发送、不需要长连接”。很多嵌入式的TCP通信其实只用到了“连接建立-发送数据-连接断开”这个闭环,可以裁剪掉拥塞窗口等复杂机制。
  • 使用开源实现做裁剪,比如Alveo项目里的TCP切片、或者某些厂商提供的RTL TCP IP核,不要从零造轮子。
  • 在系统层面理解TCP连接建立与断开的时序,尤其关注上一次连接未正常关闭时,新连接可能出现SYN重传、TIME_WAIT等状态残留。

3.4 自己写UDP还是用LwIP?做个取舍

我接触过很多工程师,面对“FPGA通信”时最容易摇摆的就是:到底自己写RTL UDP,还是用MicroBlaze跑LwIP?

我的判断标准很简单:如果数据通路是“ADC/DAC直连FPGA,FPGA做实时信号处理后打包传给PC”,自己写UDP/RTL是正路,因为LwIP跑在软核上会有明显的中断延迟和吞吐瓶颈。但如果业务逻辑复杂,比如要做HTTP服务、远程配置、多会话管理,那么LwIP配合软核是更务实的方案,开发效率高一个量级。

如果决定自己写UDP,建议把数据通路做成AXI-Stream接口规范,这样后续无论是接DMA、接FIFO、还是接MicroBlaze的AXI总线,都能无缝对接。我自己习惯把UDP引擎封装成两个AXI-Stream从接口(RX、TX)和一组配置寄存器(本地IP、端口),这样上层逻辑只关心payload流,不关心以太网封装细节。

4. 仿真验证与硬件调试:把问题逼出来

4.1 仿真环境搭建:Testbench写什么

FPGA工程里,仿真验证的质量直接决定硬件调试的时间。我见过太多人写完RTL就上板,结果一个逻辑错误调了一周。正确的做法是先写完整的Testbench,把关键协议交互在仿真里跑通,再上板验证。

TCP/IP相关的Testbench,至少要覆盖这几个场景:

  • PHY模型(可以用Xilinx的GMII接口仿真模型,或者自己写一个数据源)。
  • PC端回环模型:收到FPGA发出的ARP请求,自动回复ARP应答。
  • UDP回环模型:收到UDP数据后,自动改端口号发回来,验证收发通路。
  • 时序仿真:加上时序约束后的门级仿真,看关键路径是否满足。

如果仿真是异步FIFO跨时钟域,你还需要在Testbench里做两个参考时钟的抖动模拟,确保复位释放和数据同步没有问题。这一点很容易漏,但往往就是硬件上偶发数据错乱的真凶。

4.2 上板调试三板斧:ILA抓包法、Wireshark对照法、错误计数器法

仿真过了不代表上板就稳,硬件调试才是真正见真章的地方。

第一招是ILA(Integrated Logic Analyzer)。把ILA挂在RGMII/GMII接收侧和UDP解析输出侧,按条件触发抓包。触发条件一般设置为“任意字节等于0x0800(IP头标识)”,这样就能抓到IP报文进入FPGA的完整过程。看ILA波形的时候,重点看RX_DV和RX_DATA的时序对齐,如果发现数据偏移了半拍,多半是RGMII的DDR采样时钟相位没调好。

第二招是Wireshark对照法。FPGA发出的数据,先用PC端Wireshark抓包看一眼。如果你的帧格式不对(比如MAC地址错了、IP头长度字段有误),Wireshark会直接标红,能帮你节省大量时间。反方向也一样:用Wireshark发出自定义的UDP包,FPGA收到后通过ILA确认内部状态和数据是否一致。

第三招是错误计数器。在RTL里给每个处理模块增加错误计数寄存器,比如RX CRC错误计数、IP校验错误计数、ARP表项溢出计数。通过串口或调试接口把这些计数读出来,能快速判断问题出在哪一层。我调试时习惯先清零计数,再跑一轮通信,读计数变化,几十秒就能定位大致方向。

4.3 常见问题速查表

把这段时间整理的调试经验浓缩成一张速查表,建议收藏。

现象可能原因排查手段
PHY Link灯不亮网络变压器接法错误、PHY地址配置不对、差分线极性反万用表查供电、示波器查差分管脚、读PHY寄存器
能ping通,UDP收不到UDP端口不匹配、RX方向IP头校验和计算错误Wireshark对照端口、ILA抓IP头、检查校验和逻辑
丢包严重RX FIFO深度不足、跨时钟域亚稳态、CRC或FCS被提前截断加大FIFO、加同步器、检查FCS与CRC状态机边界
发送数据错乱RGMII接口时序约束不够、TX时钟与数据相位未对齐检查IO delay约束、调整器件SKEW寄存器
TCP连接建立失败三方握手状态机遗漏、SYN重传超时时间过短ILA抓TCP握手状态迁移、对照RFC 793
Wireshark显示Checksum offload错误PC网卡开启了硬件校验和卸载,抓包工具显示的是未计算的值关闭网卡的TCP/UDP Checksum Offload,再抓包

4.4 时序收敛:FPGA通信工程的命门

TCP/IP在FPGA上做RTL实现,到最后大概率被时序收敛卡住。原因是整个数据通路里穿插了很多大位宽的FIFO、CAM和校验逻辑,组合逻辑路径一旦过长,时钟频率就上不去。

处理时序违例我有一套优先级排序的操作:

  1. 加流水级:在长路径中插入寄存器,拆短组合逻辑。
  2. 改结构:比如校验和计算从串行累加改为树形并行累加。
  3. 时序约束:确保时钟约束、IO时序约束准确,别让工具瞎猜。
  4. 布局规划:关键路径的手工布局(pblock)是最后的招,不要一开始就用。

这里有一个教训:不要为了“省几个LUT”牺牲流水设计,FPGA的资源设计理念本来就是用寄存器换频率,LUT不够可以再加,频率上不去整块板子就废了。

5. 性能优化与扩展方向

5.1 吞吐量突破:从百兆到千兆再到万兆

当你的UDP通信在百兆下跑通后,自然会想往千兆、万兆走。这个过程的瓶颈通常不在协议栈逻辑本身,而在数据调度和存储带宽。

千兆以太网的理论吞吐是125MB/s,如果你用的是DDR3作为数据缓存,理论带宽足够,但实际效率取决于DMA的burst长度和arbitration策略。AXI DMA的默认配置往往不是最优的,我在实测中发现,把DMA的burst length从16调整到64,吞吐可以提升30%以上。

万兆方案则建议直接用硬核MAC加高速收发器(如Xilinx SFP+)。这个阶段资源量和复杂度已经不是普通工程师单打独斗能驾驭的,Corundum这种开源项目就有相当的参考价值。

5.2 FIFO深度的选择:一个改了三版才定下来的参数

FIFO深度是网络通信系统中最大的“坑”之一。UDP通信时,PC端突发发送速率往往高于FPGA端处理速率,FIFO小了直接丢包。但FIFO开大了又浪费BRAM资源。

我在一个项目中,UDP RX侧FIFO深度从256改到512再改到1024,才勉强覆盖PC端最坏突发情况。具体深度怎么选,可以用这个公式做估算:FIFO深度(字节)≥ 突发长度 × 包长 - 处理延迟 × 计划吞吐。

举个例子,如果你的PC端发送程序一次突发发送256个UDP包,每包1000字节,FPGA从收到第一个包到处理完最后一个包需要处理时间,若处理吞吐只有接收带宽的80%,那么:

FIFO深度 >= 256 × 1000 - (256 × 1000 × 0.8) = 51200 字节

这个量级用BRAM做,占用还算可控,但如果突发长度再翻倍就要考虑DDR缓存了。

5.3 从UDP平滑演进到TCP的思路

如果你的项目最终必须上TCP,从一个稳态的UDP系统迁移过去,我建议分三步走:

  1. 保持UDP数据通路不变,增加一个旁路TCP控制通道,先实现参数配置、状态查询这类非实时功能。
  2. 把命令下发和状态上报切到TCP,验证链路的稳定性,再考虑数据传输。
  3. 如果TCP数据通路依然需要,评估是否需要引入专用TCP卸载引擎,或者干脆换SoC方案。

这个渐进式方案的好处是风险可控,每一步都有可回退的路径。不要试图一夜之间把整个通信架构从UDP翻到TCP,那基本等于重新写一遍系统。

按我自己的经验,绝大多数项目止步于UDP已经完全够用。真正需要TCP的场景,用集成方案(Zynq+Linux+LwIP或者纯ARM方案)反而是更优的工程决策。

6. 写在最后的实操心得

最后说点实在的。FPGA做TCP/IP通信,难的不是某一根线或者某个模块,而是整个链条的完整性:物理层信号质量、PHY芯片配置、MAC时序、协议状态机、FIFO深度、时序收敛,每一环都要站得住脚。很多人调不通,问题往往出在最不起眼的地方——比如百兆PHY和千兆PHY的配置寄存器不同、RGMII的时钟沿极性、PHY寄存器读取时MDIO时序不对。

我个人实际项目中,最浪费时间的一次排查是RGMII的RX_CLK没有加约束,导致一个板子通信成功率只有90%,看起来像偶发丢包,实则是时序不稳定。后来加上了set_input_delay约束,问题立刻消失。所以我的建议很直接:上板调试前,先把你所有和时序相关的约束文件逐行过一遍,不要嫌烦。

优先选择UDP起步,优先选择开源组件,优先把系统跑通再追求性能。希望这篇内容能帮你少走点弯路,祝一次点亮网口。

本文还有配套的精品资源,点击获取

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

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

立即咨询