很多做FPGA的朋友,跑到网络通信这一步都会卡一下。你在学校或平时练习里写的逻辑大多是自己和自己玩,一旦把数据从FPGA里送出去、送给PC或者另一块板子,整个思维模式就变了——从“时序对不对”变成了“协议对不对、包能不能通、数据会不会丢”。这篇是系列的第7篇,我们不聊那些高深莫测的PCIE或RoCE,就踏踏实实把千兆以太网这一关过了。这对后续做高速数据采集、图像传输、多板卡互联都特别有用,也是FPGA应用方向里招聘需求量比较大的技能点。
1. 为什么网络通信是FPGA项目里绕不开的一关,也是性价比最高的一条路
先说个我自己的经历。早年间做第一块带网口的板子,画的还是RTL8211之类的千兆PHY,当时觉得“不就是把MAC跑通吗,有什么难的”。真正调起来才发现,硬件焊上去只是开始,你写的逻辑要和PHY芯片对上话,要处理自协商、时钟切换、FIFO反压,还要应对上位机那边“莫名其妙的丢包”。那种感觉就像你开车上了高速,发现不仅要会踩油门,还得懂交规、会看路牌、能处理突发状况。
FPGA为什么适合做网络通信?因为网络通信的本质是“吞吐+时序确定性”,而这两点恰好是CPU的短板、FPGA的长处。CPU处理网络包要经过中断、协议栈、内核态用户态拷贝,延迟是微秒甚至毫秒级的,抖动还大;FPGA这边,数据从一个端口进来,过一个流水线,从另一个端口出去,延迟是纳秒级的,而且完全可预测。你如果要做的是“实时控制”“协议转换”“高速数据采集前处理”,FPGA是绕不开的选择。
但FPGA做网络也有它的坑:MAC核、PHY芯片、变压器、连接器,每一个环节都是“潜在的折腾源”。我在帮别人review板子时,经常看到有人把PHY地址配错、时钟线走太长、复位信号没处理好,导致网口死活link不上。所以这篇文章,我打算从一个近似0基础的视角,把从硬件选型到逻辑跑通的完整链路讲清楚。
1.1 网络通信在FPGA项目里的典型角色
FPGA里的网络通信,通常不是“为了网络而网络”,而是为了解决数据搬运问题。常见的有这么几类场景:
- 高速数据采集:ADC采回来的数据,通过以太网实时送到PC做分析。这是最常见的应用,常见于软件无线电、振动监测、医疗影像设备的前端。
- 图像/视频传输:CMOS摄像头数据进FPGA,FPGA做ISP或者简单的预处理,再通过千兆网口把图像流送出去,替代传统的CameraLink或USB3.0。
- 控制面和数据面分离:CPU(比如Zynq的ARM核或者外挂的STM32/龙芯等)跑协议栈,FPGA负责高速数据通道。FPGA要解决的,是“如何把用户逻辑的数据高效地塞进MAC,再塞进网线”。
- 多板卡间的数据交换:比如雷达信号处理阵列,每块FPGA板卡有自己的数据要和其他板卡共享,这种场景用万兆甚至25G网络,但对初学者来说,先玩转千兆是打地基。
不管场景怎么变,底层的活都差不多:你要有一个MAC,有一个PHY,有一条能跑通UDP/ARP的链路。把这个地基打好,后面做什么都不慌。
1.2 为什么从千兆以太网入门是最优选择
有的朋友可能会问:现在都2025年了,万兆都普及了,还从千兆学起是不是过时了?我的看法是:千兆是你打通“网络通信设计全流程”知识链路的最低成本方案。
万兆以太网用的是XAUI/10GBase-R,走SerDes,逻辑层是64B/66B编码,MAC层和PCS层复杂很多,调试工具也不便宜。而千兆以太网(1000BASE-T)用的是GMII/RGMII接口,时钟只有125MHz,逻辑层是8B/10B编码,很多细节可以直接用逻辑分析仪抓。你在千兆上理解的“MAC发送状态机”“FIFO反压”“CRC校验”“PHY配置流程”,搬到万兆上,骨架是一样的,只是数据通路更宽、时序更紧、约束更严。
而且对绝大多数中小型项目来说,千兆的带宽(理论125MB/s,实际能跑到100MB/s以上)已经足够用。你要传4K视频可能需要万兆,但传个1080P、传个几十MB/s的采样数据,千兆绰绰有余。
2. 硬件基础与接口选型:7系FPGA + RTL8211的经典组合
动手写逻辑之前,先把硬件底子弄清楚。FPGA网络通信硬件的链路是:FPGA(MAC) -> GMII/RGMII总线 -> PHY芯片 -> 网络变压器 -> RJ45。任何一个环节掉链子,网络都通不了。
2.1 FPGA侧MAC的选择:硬核还是软核
Xilinx的7系列FPGA,有一部分型号自带以太网MAC硬核,比如Zynq-7000的PS侧有GEM,Artix-7里也有集成10/100/1000M MAC的型号(比如XC7A200T带GTX,但以太网MAC通常是软核IP)。对纯FPGA逻辑开发(没有ARM核参与)的场景,核心选择就两个:
选项A:用Xilinx官方三速以太网MAC IP(Tri-Mode Ethernet MAC,TEMAC)这是最稳妥的方案。IP核里包含完整MAC功能:发送/接收状态机、FIFO、CRC生成与校验、流控帧处理、MDIO配置接口等。你需要做的就是:把用户逻辑的数据按照IP核要求的时序写进发送FIFO,再从接收FIFO里读数据。相当于你雇了个靠谱的司机,你只需要告诉他去哪。
选项B:完全自己写MAC逻辑这个只建议学习用,不建议项目里这么做。自己写MAC意味着你要自己搞定:前导码和SFD插入、CRC32计算、帧间隙(IFG)处理、碰撞检测(半双工)、流控、接收侧的错误帧过滤……每一项单独拿出来都不难,但合在一起就是个大工程。如果你是0基础,先不要挑战这个。
我在实战里用的最多的,是TEMAC IP核 + RGMII接口 + RTL8211E的搭配。这几个东西都是千兆以太网里的“标准答案”,网上资料多、参考设计多,出了问题也好排查。
2.2 PHY芯片与接口模式的选择
PHY芯片是把MAC的逻辑电平信号转换成线缆上的模拟信号的部件。选PHY就三个考虑点:接口电平方式、功耗/温度、采购渠道。
接口方式常见两种:
- GMII(Gigabit Media Independent Interface):数据位宽8bit,发送/接收时钟125MHz,DDR不是必须的(时钟和数据都是单沿采样)。信号数量多,一共24根左右,占IO多,一般只在连接器方便时才用。
- RGMII(Reduced Gigabit Media Independent Interface):数据位宽4bit,时钟125MHz,DDR方式,上升沿采样低4位,下降沿采样高4位。信号只有12根左右,IO占用少,是目前最主流的接法。代价是时序约束稍微麻烦点,尤其是发送侧要处理时钟延迟(PCB上做延迟或者FPGA里面用IDELAY调节)。
在Xilinx 7系上,RGMII接收侧因为有内部延迟单元(IODELAY),可以把时钟对齐问题消化掉;发送侧需要你在FPGA内部把TXC时钟延迟约2ns输出(或者用ODDR模式实现类似效果)。这些细节后面会细说。
我推荐的硬件搭配是这样:
| 部件 | 型号/方案 | 原因 |
|---|---|---|
| FPGA | Artix-7 XC7A35T/75T/100T | 性价比高,逻辑资源够用,千兆网络完全不占压力 |
| MAC | Xilinx TEMAC IP核 | 官方维护,经过大量验证,省力 |
| PHY | Realtek RTL8211E-VB-CG | 便宜、易买、资料多,千兆10/100/1000自适应 |
| 变压器 | 或者带变压器的RJ45座(HR911105A等) | 集成化设计简化硬件 |
| 时钟 | 外部25MHz晶振给PHY,PHY输出125MHz给FPGA | 省一个125M晶振的钱,但时序更复杂;如果图省事,也可以FPGA侧另外做125M时钟源 |
2.3 硬件设计要点:最容易犯的错误
这里基于我的实际经验,列出硬件设计里几个“看似没问题、实际坑到哭”的点:
PHY地址别和别的设备冲突。RTL8211的PHY地址默认是靠引脚上下拉配置的。如果你板上还有其他MDIO设备,或者地址拨码开关拨错了,MDIO读写就会失败,PHY配置不进去,自然link不上。常见PHY地址是0x00、0x01、0x04,设计前先规划好。
RGMII的VDDIO电平要对上。FPGA的bank电压如果是3.3V,PHY的IO电源也用3.3V,别混用。如果FPGA的bank接了2.5V,PHY还接3.3V,电平不匹配,信号采进来全是乱的。
复位信号的处理。PHY的复位脚不要直接接FPGA的某个普通IO就完事,要保证上电后有足够的复位时间。很多PHY要求复位低电平持续时间至少10ms,然后还要等内部时钟稳定。建议用一个RC延时电路或者FPGA控制复位。
差分时钟走线。125MHz的时钟速率不算特别高,但也要注意走线等长、阻抗匹配(通常50Ω单端或100Ω差分)。RGMII的数据线建议等长控制在±50mil以内,时钟线尽量短。
如果你对硬件动手能力不是很有信心,可以直接买一块带网口的开发板来学,比如黑金、正点原子、米联客的板子基本都带RTL8211。用开发板的好处是硬件不用你操心,可以把精力全放在逻辑上。
3. 送帧不丢数:从用户逻辑到MII接口的握手时序
硬件准备到位,接下来是重头戏:逻辑设计。网络通信的FPGA逻辑,核心就一句话——把你要发的数据,按MAC/IP/UDP的格式组织好,按正确的时序塞给MAC核;同时把MAC核收上来的数据,按正确的格式解析出来。难点不在“协议格式”,而在“时序握手”和“数据流控”。
3.1 先看TEMAC核:它的接口到底长什么样
Xilinx TEMAC IP核的使用,第一步是配置。在Vivado里,IP Catalog搜“Tri-Mode Ethernet MAC”,配置界面会让你选:
- 接口类型:选RGMII(如果你做千兆,且硬件是RGMII)
- 是否包含DMA/Bridge:初学者不用选,我们直接用用户侧接口(User-side interface)
- 发送/接收FIFO深度:默认一般是2K/4K,可以按需改大
- MDIO接口:打开,因为要通过MDIO配置PHY
配置完成后,IP核会给你一组接口,其中核心的信号是:
gtx_clk:125MHz,这是发送侧的用户时钟,由PHY或板载时钟提供。所有发送侧逻辑都以它为时钟。rx_clk:125MHz,这是接收侧时钟,由PHY恢复出来。所有接收侧逻辑都以它为时钟。tx_axis_fifo_tdata/tvalid/tready/tlast/tkeep:发送侧AXI-Stream接口。rx_axis_fifo_tdata/tvalid/tready/tlast/tkeep:接收侧AXI-Stream接口。mdio_*:MDIO配置接口。
这里的关键认知是:用户侧是AXI-Stream,不是老式的一根valid一根ready那种简单握手,但AXI-Stream其实本质也一样,valid表示数据有效,ready表示下游能收,两个同时拉高一拍数据才算传出去。很多0基础的朋友第一次看到tready和tvalid同时有效才传数据,会觉得很麻烦,但实际上这正是网络通信里“反压”的关键。
3.2 发送通路设计:打包的逻辑和反压的处理
发送端你要做的事情是:组一个以太网帧,然后发送出去。一个标准的以太网帧长这样:
- 前导码和帧起始定界符(SFD):MAC核自己加,你不用操心
- 目的MAC(6字节)+ 源MAC(6字节)
- 长度/类型字段(2字节):如果是IP包,这里是0x0800
- IP头(20字节)
- UDP头(8字节)
- UDP数据(任意长度,整个帧最小46字节,最大1500字节)
- FCS帧校验(4字节):MAC核自己算自己加,不用操心
如果你只是用UDP传数据,那么实际要组的部分是“以太网头 + IP头 + UDP头 + payload”。这几十个字节,用一个小状态机就能搞定。
发送侧最简单的逻辑设计长这样:
- 收到用户逻辑的“开始发送”脉冲。
- 状态机依次输出:目的MAC(6拍)、源MAC(6拍)、类型0x0800(2拍)、IP头(20拍)、UDP头(8拍)、然后进入数据搬运状态。
- 数据搬运状态里,从用户数据FIFO读数据,AXI-Stream发送给MAC核。数据读完(读到用户给的最后一个数据标记),拉高tlast,结束一个帧。
- 如果发送FIFO的tready拉低,数据就停在那里等,直到下一拍tready恢复有效。
这个过程听起来简单,但有两个细节经常让人卡很久:
细节一:checksum的计算。IP头和UDP头里各有一个校验和,UDP校验和还涉及伪头部(pseudo header),就是把源IP、目的IP、UDP长度、协议类型合在一起算一遍。这个计算如果用软件逻辑去“每来一包算一次”,会占用不少时钟周期,而且有依赖关系。实战里的做法是:如果数据是发给自己PC的、且网络环境相对封闭,IP头校验和可以算好写死(针对固定IP),UDP校验和可以让它填0。UDP校验和为0表示“发送端未计算校验”,接收端一般会忽略。但不是所有设备都允许,有些严格的中继设备会丢包。正式项目里,建议还是用硬件算,分两段:先算伪头部和UDP头,再把Payload累加进去,最后取反。核心是利用“增量校验和”(incremental checksum)技巧,具体算法可以查RFC 1624。
细节二:数据FIFO的深度和反压。MAC核的发送FIFO是有限的(TEMAC默认可能只有2KB-4KB),如果你的用户逻辑突发写入速率超过MAC核发出去的速率,FIFO就会满,tready拉低。一旦tready拉低,你的发送逻辑必须能“暂停”在当下的状态,而不是继续跑。很多初学者写状态机时,默认发送过程是连续不中断的,没有处理tready拉低的情况,结果数据就丢了。我的建议是:用户数据先进一个比较大的FIFO(比如16KB),发送逻辑的搬运状态完全跟着tready走,tready低就停,高了就走。这样即使网络拥堵,数据也只是在FIFO里攒着,不会丢。
3.3 接收通路设计:解析逻辑和跨时钟域
接收侧的活比发送侧麻烦一点,因为数据不是你自己发出去的,是网络上传进来的,你无法控制到达节奏。
接收侧逻辑要做的事:
- 等待MAC核上报接收数据(rx_axis_fifo_tvalid拉高)。
- 剥离开头:前14字节是以太网头(MAC + 类型);如果是IPv4(类型0x0800),继续解析IP头,确认协议是UDP(17)。
- 核对IP头的目的IP是不是自己的(如果不核对,板上只有一个网口时无所谓,但如果做多网口就要过滤)。
- 进入UDP头解析,取出端口号,据此把数据分发到不同的处理逻辑。
- 把payload写入用户约定的FIFO,同时把长度、源IP、源端口等信息登记到寄存器/FIFO里。
这里最容易出问题的,是接收侧的“背靠背帧”和“错误帧”。网络上的数据是连续到达的,MAC核接收FIFO里可能同时有多个帧,一个tlast结束一个帧,紧接着可能下一拍就是一个新帧的tvalid。你的解析状态机必须能“干净地复位”到帧起始检测状态。另外,MAC核会丢弃CRC错误、长度错误、对齐错误的帧,但有一些“半好半坏”的帧它可能会上报,需要你根据MAC核的状态信号(比如rx_axis_filter_tuser)来判断。
还有跨时钟域的问题。接收侧的数据和时钟(rx_clk)来自PHY的恢复时钟,它是从网线信号里提取的,频率和板载时钟可能有几十ppm的差异,相位更是完全不同。所以接收FIFO的数据要搬到用户逻辑时钟域(比如你自己的100MHz或125MHz)时,必须经过异步FIFO。TEMAC核自带的接收FIFO本身是异步的,但如果你在逻辑里又加了自己的FIFO,也要选异步FIFO,别用同步的。
3.4 一个小例子:最简单的UDP发送模块
我不贴特别复杂的代码,但这里可以给一个简化的UDP发送状态机框架,帮助你理解打包这个过程(这里的代码不是完整可综合版本,只展示思路):
localparam ETH_TYPE_IP = 16'h0800; localparam IP_PROTO_UDP = 8'h11; localparam S_IDLE = 4'd0; localparam S_DST_MAC = 4'd1; localparam S_SRC_MAC = 4'd2; localparam S_ETH_TYPE = 4'd3; localparam S_IP_HEADER = 4'd4; localparam S_UDP_HEADER = 4'd5; localparam S_PAYLOAD = 4'd6; localparam S_DONE = 4'd7; always @(posedge user_clk) begin if (!rst_n) begin state <= S_IDLE; // ... end else begin case (state) S_IDLE: begin if (tx_start) begin state <= S_DST_MAC; byte_cnt <= 0; // latch packet length etc. end end S_DST_MAC: begin if (axis_tready) begin axis_tdata <= DST_MAC_BYTES[byte_cnt]; axis_tvalid <= 1'b1; axis_tlast <= 1'b0; if (byte_cnt == 5) state <= S_SRC_MAC; byte_cnt <= byte_cnt + 1; end end // ... similar for other header sections S_PAYLOAD: begin if (axis_tready) begin if (payload_cnt == packet_len - 1) begin axis_tdata <= last_byte; axis_tlast <= 1'b1; state <= S_DONE; end else begin axis_tdata <= user_data_fifo_dout; payload_cnt <= payload_cnt + 1; end end end S_DONE: begin axis_tvalid <= 1'b0; axis_tlast <= 1'b0; state <= S_IDLE; end endcase end end这里的关键就是:只有tready为高的时候,才允许改变状态。很多人写的状态机没有把这个条件加上,导致数据发不出去或者发错位。
4. 和数据手册较劲:PHY芯片的配置与Link调试
硬件焊好了、TEMAC核也生成好了,但你会发现网口link灯根本不亮,或者PC上拿Ping包死活不通。别急,这是正常开局。接下来进入FPGA网络通信设计的“下马威”阶段——PHY配置和链路调试。
4.1 MDIO接口:让FPGA和PHY对话的门
MDIO(Management Data Input/Output)是一个两线的管理接口,用来读写PHY芯片的寄存器。频率一般2.5MHz,时序和I2C有点像但不完全相同。Xilinx TEMAC核里自带MDIO控制器,你可以直接用AXI-Lite接口读取PHY寄存器状态。
MDIO读写协议要点:
- 一个MDIO帧分两部分:前导码+起始码(32bit前导码+01起始),然后是操作码(01=写,10=读),5bit PHY地址,5bit寄存器地址,接着是2bit的 turnaround(读操作时这里PHY和控制器切换方向),然后是16bit数据。
- 所以一次完整的操作是64个MDIO时钟周期。
TEMAC核内部已经把这些底层时序处理完了,你只需要在用户侧做一个简单的AXI-Lite从机,就能读写PHY寄存器。如果不想用AXI-Lite,也可以直接自己写一个MDIO控制器,这个本质上就是时序图转状态机,写一次就熟悉了。
4.2 必看的PHY寄存器:拿到PHY先读这些
以RTL8211E为例,有几个寄存器你必须会读:
| 寄存器 | 地址 | 含义 | 调试时的用途 |
|---|---|---|---|
| BMCR | 0x00 | 基本控制:复位、速度、双工、自协商使能 | 写0x8000复位PHY;写0x1000开启1000M自协商 |
| BMSR | 0x01 | 基本状态:link状态、自协商完成、能力 | 读bit2判断link是否建立,bit5判断自协商是否完成 |
| PHYID1/ID2 | 0x02/0x03 | PHY厂商ID | 确认MDIO通路正常,读到0x001C(Realtek ID)说明通路OK |
| GMII/CSI status | 0x11(RTL8211E的千兆状态寄存器) | 速度和双工状态 | 确认协商结果是1000M全双工 |
| PHY Specific Status | 0x10 | 具体状态 | 部分PHY在此寄存器报告实际协商速度 |
调试的第一个步骤,就是上电后等一阵,然后读寄存器0x01,看bit2是不是1。如果不是1,说明物理层就有问题——先别碰逻辑,先查硬件。可能的原因:
- PHY没有复位成功(复位时间不够,或者复位脚被焊短路)
- 晶振没起振(用示波器量PHY的CLK_OUT有没有时钟输出)
- 网络变压器/RJ45没焊好,或者线序接错
- 对端设备没有插好线,或者对端设备没上电
注意:千兆以太网(1000BASE-T)的自协商比百兆慢,通常需要2-3秒。如果你上电不到1秒就去读link状态,读出来0是正常的,等一会儿再读。
4.3 配置PHY:复位、自协商、确认速度
MDIO能通之后,常规配置流程是:
- 向寄存器0x00写0x8000,触发PHY软复位。
- 等待100ms(读0x00,bit15变成0表示复位完成)。
- 向0x00写0x1200(0x1000千兆自协商使能 + 0x2000自动协商使能),也可以加0x0100(全双工)确保协商成全双工。
- 等待自协商完成,读0x01 bit5。
- 确认link建立(0x01 bit2),读取速度/双工寄存器,确认是1000M Full。
这几个步骤做完,理论上PHY这边就绪了。但也有个常见坑——TEMAC核里要配置的“PHY地址”和实际硬件上的PHY地址要一致。TEMAC的MDIO接口在配置时有PHY地址参数,如果配置成0x00而实际PHY地址是0x04,读写会失败。这个错误很隐蔽,为什么?因为有些PHY芯片在MDIO读写失败时不会给你任何报错,你只能看到“link不亮”这种表象。排查方法就是读0x02寄存器,看能不能读回PHY ID,读不到就是地址搞错了。
4.4 链路层验证:Ping通才是真的通
PHY配置好了、TX/RX都跑起来了,下一步用PC端测试。办法很简单:FPGA板子连到电脑的网口,给FPGA逻辑设一个固定的IP和MAC(比如192.168.1.10),PC设同网段(比如192.168.1.2),然后PC ping 192.168.1.10。
注意一个关键细节:如果你只实现了UDP,没有实现ICMP,那ping永远是不通的。因为ping走的是ICMP协议,不是UDP。我用过的一种做法是:在FPGA里搞定ARP应答和ICMP echo应答,这样ping就通了,调试链路也方便。这个方法的好处是,ping能通说明从PHY到MAC到FPGA逻辑这条发送和接收链路都是好的,后面调试UDP就不用怀疑链路层。
如果你不想实现ICMP,也可以在PC上用一些网络调试工具直接发UDP包,看FPGA有没有回包或者收到包。但“Ping通”作为第一步验证,确实是最直观的。
4.5 链路调试的抓包方法:不要用眼睛debug
嵌入式网络调试有个铁律:不要靠感觉和猜测判断包有没有发出去,要用抓包工具。PC上装个Wireshark,把网卡设置成混杂模式,抓包看你发的UDP包格式对不对。
如果FPGA给PC发数据,PC收不到,Wireshark里可能看到几种情况:
- 网卡上完全没有任何收到包的迹象:说明物理链路不行,要么网线/PHY/RJ45的问题,要么MAC发送逻辑压根没工作。
- Wireshark能看到包,但是显示“Bad checksum”:说明你校验和算错了,或者填了0但网卡驱动/wireshark不认。
- 能看到包,但长度不对或帧格式不对:检查你的帧头字段是不是写错。
- 能看到自己的ARP请求但没人应答:FPGA的ARP逻辑没写好,或者PHY接收方向有问题。
Wireshark真的是FPGA网络调试的第三只眼。曾经有个项目,我和同事查了三天丢包问题,最后发现是上位机网卡的“大量卸载”功能把UDP小包的checksum给改了,Wireshark里看到的包是“完整”的,但Windows驱动对某些校验和做了特殊处理导致包到不了应用层。这种问题,没有抓包工具,你永远猜不到。
5. 坚实但有限:自己写UDP协议栈的取舍与设计
到了设计自己的网络通信逻辑时,核心问题来了:你到底需要实现哪些协议?完整TCP/IP协议栈在FPGA里是不现实的——那是PC/ARM干的活。FPGA里要做的,是裁剪到极致、满足特定场景的协议集合。
5.1 你真正需要的只有ARP和UDP
在绝大多数FPGA数据采集/传输场景里,真正需要的只有这两个协议:
- ARP:用于IP地址到MAC地址的解析。你在FPGA里把PC的MAC地址查出来缓存起来,然后才能把UDP包正确封装发出去。如果PC的IP是静态的、MAC也是固定的,你甚至可以在FPGA逻辑里写死对方的MAC地址,完全不做ARP。但这样做有个问题——如果PC换了网卡(MAC变了),你就要重新编译逻辑。所以稳妥起见还是动态做一次ARP。
- UDP:无连接、不可靠,但简单、开销小。FPGA做UDP,收发都能精确控制时序。而TCP是流式、有重传、有拥塞控制,状态机复杂得多。在局域网封闭环境里,UDP丢包率很低,完全够用。
至于IGMP、DHCP这些,通常都用不上。你给FPGA设一个静态IP就行。
5.2 自己做还是调现成IP:两种方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 自己写ARP+UDP逻辑 | 代码可控、时序可预测、占资源少、不依赖第三方 | 工作量中等、细节多(比如ARP超时重传)、需要自己调试 |
| 调现成的开源UDP协议栈(比如Xilinx应用笔记里的lwIP移植、或第三方IP) | 功能全、和上位机兼容性好 | 资源占用大、配置复杂、很多代码模块你看不到内部逻辑、要在SoC上才方便 |
我的建议是:如果你是学习、或者你的FPGA里不需要跑系统(纯PL),自己写一套极简ARP+UDP是性价比极高的。代码量其实不大,一个模块总共几百行,但你会对整个网络通信链路有非常深的理解。如果你是项目里用Zynq、且时间特别紧,可以优先考虑用PS侧的lwIP,PL只做数据通路。
5.3 极简UDP协议栈的状态机设计
我把自己用的一套结构画出来(这里不画图,只用文字描述模块划分):
- 接收路径:MAC RX -> 帧解析模块(去以太网头、判断IP/UDP)-> payload + 元信息 -> 用户FIFO
- 发送路径:用户FIFO -> UDP打包模块(组UDP头、IP头、以太网头)-> MAC TX
- ARP处理:收到ARP请求 -> 判断目标IP -> 如果是自己 -> 发送ARP应答。发送UDP前,查ARP缓存表,没有则发ARP请求等待应答。
发送UDP前要“查ARP表并等待响应”,这个环节是最容易让“第一版UDP通信”失败的:PC端的ARP表缓存可能已经过期,FPGA发出的UDP包因为没有目的MAC,只能广播(全F),但PC收到目的MAC为广播、IP却是自己的包,会直接丢弃。所以必须先有一次ARP请求/应答,让FPGA缓存到正确的目的MAC。
这里还有个技巧:上电初始化时,FPGA主动发一次ARP请求给PC,PC会回应并把自己的MAC告诉FPGA,同时PC端也会缓存FPGA的MAC和IP映射。这样后续FPGA发UDP包时,PC的ARP缓存里已经有对应表项,能直接处理。这个“主动ARP”可以省去很多调试时的“先ping一下、再发包”的步骤。
5.4 UDP校验和:能省则省,但要知道为什么能省
前面提过UDP校验和填0能被大多数设备接受。这里补充一个真实的坑:如果PC上位机用的是Windows系统,且网卡驱动开启了“校验和卸载”(很多默认开启),Wireshark抓到UDP包的checksum可能是0x0000,因为网卡驱动在发送前没算。这种情况是可以用的。
但如果你的FPGA发出去的是填0校验和的UDP包,Windows收到的包是正常的;但如果你在Linux主机上收,有些严格配置的防火墙/应用可能直接丢弃。为了保险,项目里还是建议把IP和UDP校验和都算了。计算也不复杂,核心是checksum的累加逻辑。用Verilog实现时注意字节序(大端序),每个16bit按网络字节序相加,进位回卷(carry wrap-around),最后取反。我实现过很多次,大约是50行代码。
5.5 使用FPGA network通信时,还有一个隐藏的坑:MAC地址
有些初学者在调试的时候,FPGA的MAC地址用的全0、或者全F,然后PC那边ARP会应答失败、包会被丢弃。标准规定,MAC地址不能是全0、不能是全F(广播)、bit0的“本地管理位”设置为1表示这是私有地址。建议给你的FPGA板子编一个类似02:00:00:00:00:01这样的地址,既满足本地管理地址要求,又不容易和别人冲突。
6. 不只是“能通”:吞吐、丢包率与上位机联调的经验
能把UDP包收发起来,只是入门;真正项目的考验是:持续高速传输不丢包。这部分是我实际项目中花时间最多的地方。
6.1 面向吞吐的发送通路设计:不要一包一停
很多一开始跑通了UDP通信的朋友,代码是这样写的:上位机发一个“请求”,FPGA回一个包含N字节数据的UDP包。这种一问一答模式,吞吐率很可怜,因为每发一包都要等上位机回请求,期间链路是空闲的。
要跑满千兆带宽,正确的做法是连续发送流:FPGA把数据源源不断地组包发出去,上位机只管接收。发送侧逻辑要能做到“上一帧刚发完,下一帧马上跟上”,不要产生超过标准以太网帧间隔(IFG,96bit时间,千兆下约96ns)的空白。要做到这点,就是前面说的:不要每帧都去等外部触发,而要用一个独立的“发送调度器”,一旦FIFO里有数据就开始组包发送。
当数据吞吐特别高时,还有一个潜在瓶颈:MAC核的发送FIFO写入带宽。如果你的用户逻辑时钟是125MHz,数据位宽是8bit,那理论上写入带宽也是1Gbps,正好等于线速。但如果你的用户逻辑时钟是100MHz,数据位宽8bit,那写入带宽只有800Mbps,是跑不满千兆的。
6.2 实测千兆能跑多快:别被理论值骗了
我做过一个数据采集板,FPGA通过千兆以太网上传ADC数据。蜜月期的测试结果让我挺惊讶:
| 场景 | 实际吞吐 |
|---|---|
| 单UDP端口,payload 1472字节(1500 MTU减28字节头) | 约118MB/s |
| 单UDP端口,payload 1024字节 | 约105MB/s |
| 单UDP端口,payload 512字节 | 约85MB/s |
| 单UDP端口,payload 256字节 | 约60MB/s |
注意,这不是MAC或PHY的限制,而是上位机处理能力的限制。以太网帧有固定的开销(前导码、帧头、FCS、IFG),帧越小,有效载荷占比越低,而且每秒钟需要处理的中断/系统调用次数越多,CPU压力越大。PC在接收小包时很容易出现瓶颈。
如果你想在PC侧达到最高吞吐,建议:
- 使用更大的UDP payload(1472字节是UDP在标准MTU下的上限,再大会触发IP分片,分片包容易丢)。
- 上位机使用多线程/多队列接收,或者使用DPDK这类绕过内核的库。
- 接收端缓冲区调大。
你自己做测试时,不要一上来就追求“接近千兆满速”,先用小数据量(比如每秒几百包)验证正确性,再逐步加压找瓶颈。
6.3 丢包的本质:反压和背压的设计
丢包的本质是“某个环节的FIFO满了但数据还在往里进”。以太网本身是不可靠的(对UDP来说),没有重传机制。所以你要做的是在系统的每一层都加上“反压”或“丢弃策略”:
- 用户数据源侧:如果发送FIFO快满,需要通知上游(比如ADC采集逻辑)暂停采集或丢弃新数据。这个在FPGA内部好做,就是拉高一个ready信号或者暂停信号。
- FPGA内部FIFO:要有阈值(almost full)信号,提前预警。
- 接收侧:如果收到大量数据但上位机来不及处理,你要么把数据缓存在板载DDR里,要么直接丢弃并给上位机发一个“丢包计数”的UDP包,让上位机知道丢了哪些包。
曾经遇到过一个典型问题:ADC连续采集,数据源源不断进入发送FIFO,但网络偶尔拥堵导致tready拉低,发送FIFO溢出,整个传输流程卡死。后来我在发送FIFO的前一级加了一个“丢弃优先级最低的数据”的逻辑——当FIFO即将满时,优先丢新数据而不是卡住整个采集流程。这样做虽然会丢包,但至少系统不会死锁,且丢包数量是可控可统计的。
6.4 上位机联调时的一些经验和工具
联调时,上位机建议先用简单的网络调试助手(比如Windows上的“网络调试助手”,或Linux上的nc -u命令、Python socket脚本)验证,别一上来就搞复杂的GUI程序。调通了之后,再用自己写的上位机。
Python接收UDP代码非常简单,适合快速验证:
import socket sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind(("192.168.1.2", 5005)) sock.settimeout(1.0) count = 0 while True: try: data, addr = sock.recvfrom(2048) count += 1 except socket.timeout: print(f"timeout, received {count} packets") break这个脚本虽然简单,但验证“FPGA是否在持续发包”足够了。
还有一个经常忽视的工具是ARP表。在PC的命令行里跑arp -a,可以看到PC的ARP缓存里有没有FPGA的IP-MAC映射。如果没有,说明FPGA的ARP功能有问题或者物理链路不通。这是个特别快的排查步骤,经常能在2分钟内定位问题。
6.5 时序约束:RGMII和跨时钟域的约束要点
最后,网络通信设计里无法绕过的一环是时序约束。这个在综合实现后,如果时序违规,会出现“时好时坏”“工作不稳定”的现象。网络通信相关的时序约束主要有这几类:
- 时钟约束:125MHz时钟(
gtx_clk、rx_clk)要用create_clock正确约束。PHY提供的时钟可能没有在XDC里声明,导致综合工具不认识,时序分析全乱。 - 输入延迟约束:RGMII接收侧的数据相对时钟,可能有1-2ns的偏移。用
set_input_delay约束,让工具知道数据到达窗口。 - 输出延迟约束:RGMII发送侧的输出延迟,包括PHY的
tSKEW参数,需要根据PHY数据手册计算。 - 异步FIFO的约束:跨时钟域的地方用了异步FIFO,要加
set_false_path或set_clock_groups来告诉工具“这里不需要做时序收敛”。
初学者可以不深究每条约束的细节,但至少要保证“每个时钟都创建了、跨时钟域路径被正确处理了”,否则实现后时序报告一堆violation,跑起来不稳,你都不知道从哪下手。
7. 进阶方向:PCIE、DMA与多网口系统的思路
文章写到这里,千兆以太网的核心链路基本讲透了。如果你跟着做下来,至少应该能实现“FPGA和PC通过UDP双向通信”这个目标。如果你继续往前走,有几个必然的方向,我简单提一下,也许能帮你规划后续的学习路径。
7.1 从UDP向PCIE/DMA演进
当数据量超过千兆以太网能承载的范围,或者你需要更低的延迟、更可靠的传输时,下一个方向就是PCIE + DMA。PCIE的带宽远高于千兆以太网(Gen2 x1就有500MB/s),延迟更低,而且是内存语义的——上位机直接读写内存,不需要经过网络协议栈。
但PCIE的复杂度也上了一个量级:你要处理TLP(Transaction Layer Packet)、DMA引擎设计、中断机制、驱动开发。这也是FPGA网络通信方向“高薪岗位”的常见必备技能。建议先把以太网吃透,再学PCIE。因为PCIE的逻辑更抽象,没有网络通信这套“看得见摸得着”的调试手段。
7.2 多网口与流量控制
在某些场景下,一个网口带宽不够,需要两个甚至四个网口跑链路聚合。多网口的难点在于:多个PHY/MAC的时钟、中断、数据调度要在一个FPGA里管理。你可以把一个简化版的“交换逻辑”做进FPGA里,根据目的MAC/IP把数据分发到不同网口。前提是:单个网口的发送通路已经完全可靠,否则多个网口一起系统会炸到你怀疑人生。
7.3 与FMC/图像处理之类的场景对接
我看很多朋友搜本系列的时候,也关注“FPGA图像处理”“FMC通信”这些词。这个方向的实际项目,往往不是单一功能,而是“ADC/FMC采集图像数据 -> FPGA内部做算法/缓存 -> 网络/UART/PCIE输出”。网络通信在这里的角色是“输出通道”。你一旦把网络这关过了,整个系统的数据通路才算完整——前面是采集、中间是处理、后面是输出,每一环都要打通。
所以这篇算是一个分水岭:前面几篇你可能一直在写“独立的逻辑模块”,从网络通信开始,你写的代码要开始学会“和别人打交道”“按协议办事”“处理对方的节奏”。这种思维上的转变,比学会某个具体协议更重要。
回想我带过的新人,凡是能独立把这一套“硬件检查、MAC配置、PHY寄存器读写、UDP收发、Wireshark抓包定位、吞吐调优”走一遍的,后面上手复杂的PCIE/DDR/图像处理项目,速度都明显更快。不是我讲的这些技术有多难,而是这个过程教会了他们最重要的东西——怎么在一个数据跨设备流动的复杂系统里,找到出问题的那一环。