FPGA网络通信实战:从RGMII接口到UDP协议栈的完整设计
2026/9/6 1:26:37 网站建设 项目流程

1. 搞网络通信之前,先想明白FPGA在这条链路里的位置

做FPGA开发有一个很有意思的现象:很多人能在Vivado里把LED灯点亮、把UART收发调通、甚至能把DDR3跑起来,但一听到“网络通信”四个字就心里发怵。这个现象很常见,因为网口通信看起来确实比UART复杂得多——UART只有几根线,每秒传几万个字节已经算不错了;而千兆网口一秒能传上百兆字节,一边是简单到极致的串行协议,一边是带着各种分层、各种封装的TCP/IP协议栈,认知跨度太大。

先给一个核心定调:FPGA做网络通信,并不是让你在FPGA里完整移植一套Linux内核协议栈。FPGA的优势是硬逻辑、低延迟、可定制,现实中绝大多数FPGA网口项目只做几件事——接收数据帧、解析帧内容、根据规则转发或处理、再发送出去。你要处理的数据流是可控的、格式是明确的,不需要去应对操作系统里那种几万条并发连接和复杂路由表跳转的场景。

理解这个问题,就要先搞清楚一个网络数据包从网口进来之后,到底是怎么在FPGA内部走的。物理层由外部PHY芯片负责,它负责把差分信号转成数字逻辑电平,同时完成时钟恢复。FPGA和PHY芯片之间的接口通常是RGMII或GMII,FPGA内部要做的事情是把这个接口的数据接收到MAC层,剥掉以太网帧头、剥掉IP头、剥掉UDP/TCP头,然后把净荷数据送出去。如果要做发送,就反过来一层一层把数据封起来。就这么一件事,真没有想象中那么玄乎。

你会面临的问题其实很具体,比如:

  • RGMII接口的信号该怎么约束、数据该在哪个时钟沿采样
  • PHY芯片的寄存器怎么通过MDIO接口去读写,自协商和速率怎么配置
  • 以太网帧格式的头部到底有哪些字节,CRC校验怎么算
  • ARP协议怎么应答,UDP怎么封装和解析,IP校验和在哪一步算
  • 接收和发送的FIFO怎么处理跨时钟域
  • 千兆速率下,逻辑代码的时序怎么收敛,综合后跑到不到125MHz怎么办

这一篇的内容,就是围绕这些问题逐个击破。我会把它当成一个真实项目来拆解,从硬件接口到协议解析再到数据通路设计,尽可能把每个细节讲透,还会穿插一些实际调板子时踩过的坑。

我的建议是,看到这篇文章的时候,你手上最好已经有一块带千兆PHY芯片的FPGA开发板,不一定要很贵,能用就行。因为网络通信这个东西,纯看不练很难形成手感,很多东西真的是在示波器上看到波形、在Wireshark里看到报文、在串口打印里看到状态机跳转之后,才能彻底想明白。

2. 硬件链路拆解:从RJ45到FPGA引脚,信号是怎么一步步走过来的

2.1 RGMII和GMII,选哪个接口最合适

先看最底层。FPGA开发板上有一颗PHY芯片,常见的型号有RTL8211、88E1512、KSZ9031之类,这些芯片的MAC侧接口,通常同时支持MII、GMII和RGMII三种模式。MII是百兆模式用的,数据线只有4根,工作时钟25MHz;GMII是千兆模式,数据线有8根,发送和接收各有一组8位数据线,工作时钟125MHz。RGMII则是把8根数据线砍半成4根,用双沿(DDR)采样方式在125MHz下传输数据,等效带宽和GMII一样。

绝大多数现代FPGA开发板都会把PHY配置成RGMII模式,原因很直接:节省引脚。GMII一个接口就要吃掉十几根信号线,RGMII只用不到一半的线就能达到同样速率。以我常用的XC7A35T为例,整个器件的可用IO也就200个左右,如果接口设计得费引脚,留给其他功能的空间就太紧张了。

RGMII接口的信号大致如下:

信号名方向功能说明
eth_rxcPHY到FPGA接收时钟,125MHz
eth_rx_ctlPHY到FPGA接收控制信号,DDR采样,高电平表示有效数据
eth_rxd[3:0]PHY到FPGA接收数据,DDR采样
eth_txcFPGA到PHY发送时钟,125MHz
eth_tx_ctlFPGA到PHY发送控制信号
eth_txd[3:0]FPGA到PHY发送数据
mdio双向管理接口数据线
mdcFPGA到PHY管理接口时钟

这里有一个特别容易让新手困惑的点:RGMII的所有数据信号都是在时钟的上升沿和下降沿双沿采样的,低4位在上升沿采样,高4位在下降沿采样,组合起来就是完整的一个字节。发送方向也是同理,你需要把8位数据拆成两个4位,分别在时钟的上升沿和下降沿送出去。很多初次做的人觉得DDR逻辑很麻烦,其实ISE/Vivado里有一个叫IDDRODDR的原语,就是专门干这个的,直接例化使用即可。

还有一个很隐蔽的坑:RGMII标准中,接收时钟eth_rxc是从PHY芯片输出的,对于FPGA的逻辑来说,它和一个普通的外部时钟输入没有区别,直接用这个时钟去采数据就行。但发送时钟eth_txc的情况不一样,它是FPGA输出的,而且RGMII规范要求在源同步接口中,接收端在时钟的两个沿都采样数据,所以很多时候需要在发送时故意把eth_txc做90度相移,保证数据在时钟沿处是稳定的。

实际工程中怎么处理最简单?答案是用Vivado的时钟管理单元MMCM/PLL把125MHz时钟分出一路90度相移的时钟给发送逻辑用。如果你用的是Artix-7系列,直接在Clocking Wizard IP核里配置50%占空比、90度相移输出,然后接到ODDR原语的时钟端就行。很多人第一次调不通,八成就卡在这个位置上——数据采样不稳定的表现是丢包或者收到的数据乱序,尤其在温度变化或者线缆移动的时候症状尤其明显。

2.2 MDIO配置PHY,你只需要搞懂这几个寄存器

PHY芯片本身是一个小型处理器,它负责物理层的编码解码、自协商等工作,这些功能可以通过MDIO接口来配置和读取。MDIO只有两根线:一根时钟MDC,由MAC侧提供;一根数据MDIO,双向传输。协议格式类似SPI,每个时钟周期传输1位,通过一个固定帧结构来读写寄存器。

FPGA侧要做的就是一个MDIO控制器状态机,或者更省事一点,直接在逻辑里用一个移位寄存器把帧内容拼出来,按bit发出去,再用一个小状态机等待接收数据。不需要把它想得太复杂,MDIO的最高时钟频率是2.5MHz,在125MHz系统时钟下,一个位周期大概要占用50个时钟周期,逻辑上有充足的时间去处理。

对网络通信设计来说,有几个寄存器必须认识:

  • 寄存器0(Basic Control)和寄存器1(Basic Status):控制和查看自协商状态
  • 寄存器4(Auto-Negotiation Advertisement):配置自协商广播的能力
  • 寄存器5(Auto-Negotiation Link Partner Base Page Ability):查看对端能力

在实际使用时,最简单的做法是:上电后等一段时间让PHY完成上电复位,然后读取寄存器1,检查自协商完成位和链路状态位。自协商完成之前,PHY可能还没有锁定速率,如果你在这个时间点就开始收发数据,基本注定是失败的。

我曾经遇到过一种情况:开发板上PHY的上电复位时间比FPGA配置时间更长,FPGA加载完bit文件后立即去读写PHY寄存器,读回来的全是0xFF或者0x00。排查了半天才反应过来,是MDIO时序太早,PHY还没准备好。后来在逻辑里加了一个上电延时计数器,等100毫秒左右再开始访问MDIO,问题立刻消失了。

2.3 上电后必做的三件事:复位、延时、读状态

说到这里,我想总结一下拿到一块带PHY的FPGA开发板之后,第一次跑网络通信应该做的准备工作。

第一步,打开原理图,找到PHY芯片的引脚连接。确认RGMII接口每个信号的FPGA引脚位置、MDIO连线、以及PHY的复位引脚。有的PHY芯片的复位是低有效,有的是高有效,务必看好再做约束。第二步,给PHY复位信号写一个上电延迟释放逻辑。第三步,等PHY稳定之后,通过MDIO读取PHY的芯片ID寄存器,很多PHY的寄存器2和寄存器3是出厂固化好的厂商ID和芯片型号。能读到预期值,说明你的MDIO时序没问题,硬件连接也没问题,这才是后面继续调试的基础。

有位工程师朋友的做法很有意思,他每次拿到一块新板子,第一件事就是写一个用串口打印PHY所有寄存器内容的小程序,通过串口辅助调试网络模块。这个做法看起来很笨,实际上非常好用,因为MDIO读写是网络通信里最简单的一个模块,它能独立调通,后面你调MAC层、协议层时至少能确定底层管理通道是好的,排查范围可以缩小一半。

3. 协议裁剪:FPGA里的以太网帧、ARP和UDP,跟电脑上有什么不一样

3.1 以太网帧到底长什么样,CRC该怎么算

很多教程一上来就给帧格式图,但很少有人解释为什么要关心帧格式,以及收到一个帧之后该怎么判断它是不是有效的。这里我把帧格式和检查逻辑一起讲。

一个标准的以太网帧(不包含前导码)从目的MAC地址开始,依次是6字节目的MAC、6字节源MAC、2字节类型(或者长度)、46到1500字节负载数据、4字节帧校验CRC32。在千兆以太网里,前导码(7字节0x55)和帧起始定界符(1字节0xD5)是由PHY自动处理的,FPGA里收到RGMII数据时,看到的已经是从目的MAC开始的内容。

这里有一个很重要的细节:对于RGMII接口,数据是以字节为单位进来的,但RGMII本身只有4根数据线,每个时钟周期实际上只传输半个字节。所以你在逻辑里看到的现象是:时钟上升沿来一个rxd[3:0]是低4位,下降沿来一个rxd[3:0]是高4位,两个沿拼起来才是完整字节。也就是说,FPGA内部收到一个byte,需要两个时钟沿的配合。多数设计里会用IDDR原语把这两个半字节拼起来,然后用一个由接收时钟产生的字节级使能信号来标记“当前这个字节有效”。

帧尾的CRC32校验最让人头疼。CRC32的算法原理不难,本质是多项式除法,但千兆速率下逐比特计算是完全来不及的,必须用并行CRC。好在Xilinx提供了一个CRC IP核,可以自动生成任意位宽输入的并行CRC;如果你不想依赖IP核,网上也能找到crc32_8bit这类现成的并行实现代码,一个字节一个字节地输入,等效8位并行计算。

还有一个容易忽略的政策性问题:CRC校验应该包含从目的MAC到负载数据的全部字节,不包含前导码和帧起始定界符。CRC的数值有一个有趣的特性——如果传输没有错误,接收端对整个帧(包括CRC字段本身)再做一次CRC32计算,结果会固定为0xC704DD7B。这个特性可以用来做一个非常简单的错误检测:把所有字节算完后,看看结果是否等于这个魔数。

如果你收到的帧CRC校验失败,一般有几种原因:RGMII采样时序不对导致数据位出错、跨时钟域处理不当导致丢字节、或者发送方向拼接数据时字节顺序搞反。我建议先在逻辑里加一个帧错误计数器,统计CRC失败次数,配合SignalTap或者ILA抓RGMII波形排查,很快能定位到是时序问题还是逻辑问题。

3.2 ARP应答:你的FPGA板卡在网络里怎么证明自己存在

联网之后,电脑要往FPGA发送数据,首先要知道FPGA的MAC地址和IP地址。问题在于,电脑一开始只知道对方的IP地址,不知道MAC地址,所以要发一个ARP广播请求,问“谁知道这个IP的MAC地址”。FPGA收到ARP请求之后,需要回复一个ARP应答:告知自己的IP和MAC。

这个交互过程不复杂,但FPGA实现时有一个设计问题要处理好:ARP应答是必须及时处理的,因为电脑发出ARP请求之后会有超时机制,如果在几百毫秒内没收到应答,它就会认为目标不存在。你在FPGA逻辑里写了一个复杂的TCP/IP协议栈处理流程,但接收FIFO还在被其他数据占用,ARP请求帧被堵住了,电脑那边就显示网络不可达。解决思路是,在接收路径上做一个简单的帧类型判断,如果是ARP帧,走优先通道尽快处理应答,不要让它和普通数据帧排同一个队列。

具体到一个ARP请求帧的处理流程:

  1. 收到帧后,检查目的MAC是否为广播地址(FF:FF:FF:FF:FF:FF)或本机MAC
  2. 检查帧类型字段是否为0x0806,确认是ARP帧
  3. 从帧负载中提取目标IP地址,和本机IP比对
  4. 如果匹配,构造ARP应答帧,源MAC填本机MAC,目的MAC填请求方的源MAC,操作字段填2(应答)

有几个容易出错的细节。一是ARP帧的硬件类型字段填1、协议类型字段填0x0800、硬件地址长度6、协议地址长度4,这些固定值不能错。二是ARP应答帧的发送方MAC和IP要填本机的,接收方MAC和IP要填请求方的。三是应答帧的目的MAC地址不是广播地址,而是请求方的单播MAC地址。四是操作字段(OP)是16位的,请求为0x0001,应答为0x0002,注意字节序的问题——网络字节序是大端,低地址存高位字节,在构造帧时0x0002要按0x02, 0x00的顺序写入。

3.3 进一步裁剪:为什么很多FPGA项目只做UDP而不做TCP

嵌入式网络设备选择协议时,常常遇到UDP和TCP的取舍。TCP提供可靠的流式连接,有确认重传、滑动窗口、拥塞控制这些机制,但这些机制在FPGA里往往意味着巨大的状态机和缓存开销。FPGA的资源是固定的,几百KB的BRAM可能连一个像样的TCP会话缓冲都撑不起来。而UDP就是“发了就不管”的简单报文,帧结构固定,不需要维持连接状态,非常适合FPGA实现。

这不是说TCP完全不能在FPGA里做,而是说在资源受限和实时性要求高的场景下,UDP几乎总是更合适的选择。像高速数据采集、图像传输、信号处理这类应用,数据流是持续不断的大批量数据,丢一两个包影响不大,重传反而会增加系统复杂度和处理延迟。如果是控制类指令传输,可以在应用层做简单的“发送-确认-超时重发”机制,替代TCP提供的可靠传输。

我自己做过的项目中,有一个是通过FPGA把ADC采样数据通过千兆网实时上传到PC,采用的就是UDP组播方式。PC端用Wireshark抓包验证数据正确性,丢包率控制得很好。整条链路的协议栈只用了ARP和UDP两个模块,加起来几百行状态机代码,调试周期很短。如果换成TCP,光是三次握手和重传机制就够写好几个星期了。

4. 数据通路的核心设计:RGMII收发、FIFO缓冲和时钟域处理

4.1 接收通路:RGMII数据进来之后,具体每一步怎么处理

千兆以太网的接收通路设计,是整个网络通信模块最核心的部分。我们做一个标准的处理链:IDDR采样 → 字节拼接 → 帧同步 → 帧内容解析 → FIFO缓冲 → 用户逻辑读取。

先看IDDR采样。接收信号eth_rx_ctleth_rxd[3:0]eth_rxc的上下沿都在变化。用IDDR原语把上升沿和下降沿的数据分别采出来,上升沿的数据对应字节的低4位,下降沿的数据对应高4位,拼在一起就是一个完整字节。代码片段长这样:

IDDR #( .DDR_CLK_EDGE("SAME_EDGE_PIPELINED"), .INIT_Q1(1'b0), .INIT_Q2(1'b0), .SRTYPE("SYNC") ) u_iddr_data0 ( .Q1(rxd_low[0]), .Q2(rxd_high[0]), .C(eth_rxc), .CE(1'b1), .D(eth_rxd[0]), .R(1'b0), .S(1'b0) ); always @(posedge eth_rxc) begin if (eth_rx_ctl_valid) begin rx_byte <= {rxd_high, rxd_low}; end end

注意,eth_rx_ctl在DDR采样下也分高半字节和低半字节:数据有效时,控制信号在两个沿都是高电平;数据结束后的第一个沿,控制信号的高半字节变成低电平,表示接收结束。有些PHY芯片在空闲时rx_ctl会拉低,这个状态可以直接作为帧同步的起点。

字节拼好之后,进入接收状态机,主要状态包括:IDLE(空闲)、RECEIVE_DATA(接收数据)、FCS_CHECK(校验)。IDLE状态等待控制信号拉高,一拉高就进入RECEIVE_DATA;RECEIVE_DATA状态下,每个字节都存入FIFO,同时计算CRC;当控制信号拉低时,进入FCS_CHECK状态,把CRC结果和帧尾的4字节CRC对比,通过比较结果或者0xC704DD7B判断是否通过。

这里有一个很实际的细节:目的MAC、源MAC、类型字段这些头信息在RECEIVE_DATA状态下被逐字节解析和提取,但千万别在接收的同时就把有效数据往用户逻辑里送。严谨的做法是:整个完整帧先存到接收FIFO里,CRC校验通过之后,再给用户逻辑一个“有效帧到达”的信号。否则,用户逻辑拿到了一半的帧数据,后面CRC失败都不知道该不该丢弃,处理起来非常被动。

4.2 发送通路:从用户逻辑到RGMII,数据是怎么一层层包起来的

发送方向和接收方向刚好相反。用户逻辑往发送FIFO里写入数据,发送状态机负责把这些数据封装成以太网帧,加上目的MAC、源MAC、类型字段和CRC,最后通过ODDR原语按4位双沿发送给PHY。

发送状态机可以设计为:IDLE → SEND_PREAMBLE → SEND_MAC → SEND_PAYLOAD → SEND_CRC → 回到IDLE。

SEND_PREAMBLE阶段,FPGA逻辑可以自己生成前导码和帧起始定界符,即7字节0x55加1字节0xD5。可能有人会有疑问,接收时PHY不是自动去掉前导码了吗?为什么发送时还要自己加?原因在于,RGMII接口发送方向,PHY并不自动生成前导码,这些是MAC侧要负责的。

后续状态依次发送6字节目的MAC、6字节源MAC、2字节类型,然后进入SEND_PAYLOAD状态,逐个从发送FIFO读数据,直到用户逻辑告诉发送模块“这一帧的数据发完了”,接着进入SEND_CRC状态,送出之前并行CRC算好的4字节校验值。

实际项目中,发送控制往往用握手信号来做:用户逻辑拉高tx_start,并把数据写入FIFO,发送模块收到tx_start后开始组帧,最后一位数据发出后自动收尾。这样用户逻辑不用关心底层RGMII的时序细节,只要按照FIFO接口的约定写入数据就行。

这里有一个常见误区:很多人为了实现“分片发送”功能,在发送状态机里再加各种判断,结果状态机搞得复杂无比。其实对于大多数FPGA应用场景,一帧数据就是一条完整的UDP报文,发送方并不需要为了做大包而做IP分片。把发送逻辑做简单、做稳定,远比做复杂更实用。

4.3 跨时钟域和缓存:125MHz的网口时钟和用户逻辑时钟不是一回事

FPGA内部很少直接用125MHz时钟去跑所有业务逻辑,因为这样会使时序收敛压力很大,而且用户逻辑往往跑在一个更低的时钟域,比如100MHz或者150MHz。接收方向和发送方向都需要在不同时钟域之间传递数据,标准做法就是异步FIFO。

具体到实现上,接收端用eth_rxc作为写时钟,把接收到的字节流写入FIFO;用户逻辑用系统时钟作为读时钟,从FIFO读走数据。发送方向反过来,用户逻辑用系统时钟写入FIFO,发送模块用eth_txc作为读时钟,从FIFO中读出数据组帧。

异步FIFO的深度可以根据项目需求选择。一般接收FIFO至少要能容纳一个最大帧(1518字节)的长度,同时留出余量。如果接收速度太快,用户逻辑来不及消费数据,FIFO就会溢出,这时需要考虑背压机制:接收模块知道FIFO快满了,就暂时不向PHY发出有效信号,或者干脆丢弃这一帧并记录丢帧计数。千兆以太网不像UART那样有硬件流控,所以这个背压逻辑得自己实现,不然大数据量下FIFO溢出之后整个数据流就乱了。

我通常会做两个计数器放在调试界面里:一个是接收FIFO写入的帧总数,一个是接收FIFO因溢出而丢弃的帧数。有了这两个值,数据流量大不大、处理速度够不够,一眼就能看出来,比在PC端看Wireshark统计直观得多。

5. 一步一步打通网络:从第一次PING通,到PC和FPGA互发UDP

5.1 把ARM逻辑和协议栈代码搭建起来的步骤

现在把之前的内容串起来,给出一条可复现的完整路径。下面的步骤基于Xilinx Vivado环境,使用Verilog和Vivado自带的IP核,不依赖其他第三方库。

工程结构可以分成这几层:

  • rgmii_interface:负责RGMII信号采样、IDDR/ODDR原语例化、发送和接收时钟管理
  • mac_rx:接收MAC层状态机,完成帧同步、CRC校验、头信息提取
  • mac_tx:发送MAC层状态机,完成组帧、CRC生成、时序输出
  • arp_module:ARP请求检测和应答帧生成
  • udp_module:UDP报文解析,把负载数据写入FIFO;用户数据组UDP帧交给MAC层发送
  • async_fifo:跨时钟域缓冲
  • user_logic:模拟一个简单的应用——收到UDP数据后,把数据长度和最后一个字节通过串口或LED显示出来,同时把收到的数据原样回发

在Vivado里,新建工程,添加芯片型号(比如xc7a35t),创建约束文件。引脚约束按照开发板原理图填写。时序约束方面,主要需要约束的是两处:RGMII的接收时钟eth_rxc要声明为主时钟输入;发送方向如果使用MMCM输出的90度相移时钟,也要同步约束好。建议在约束文件里加一条对eth_rxccreate_clock,给Vivado一个明确的时钟定义,否则时序分析可能给出很多貌似无误但实际不可靠的结果。

5.2 Vivado时序约束和综合参数,注意这些细节就不会跑偏

给一个可以直接抄的约束模板(以Artix-7和RTL8211为例):

create_clock -period 8.000 -name eth_rxc [get_ports {eth_rxc}] create_clock -period 8.000 -name eth_txc_intf [get_pins {mmcm_tx_out/CLKOUT0}]

这里eth_rxc的约束是8ns周期,对应125MHz。发送方向若用MMCM输出90度相移时钟,约束在MMCM的输出引脚上。综合时建议打开-flatten_hierarchy,并retiming设置为自动。如果逻辑跑不到时序收敛,优先检查发送方向FIFO读侧逻辑是否过长,以及MAC层状态机是否简单清晰。

关于发送时钟的90度相移,在实际使用时有一个更成熟的思路:让MMCM输出两路时钟,一路0度送给ODDR作为时钟,另一路90度送给发送状态机作为逻辑时钟。这样ODDR输出的数据变化沿比状态机采样沿晚半个周期,PHY在时钟上升沿采数据时,数据已经完全稳定了。这个方案在许多参考设计里都能看到,直接照做即可。

5.3 用PING验证ARP和链路,用UDP工具验证数据通路

硬件工程编译完成、烧录比特流后,进入调试验证阶段。

第一步,电脑网口直连FPGA开发板的网口,给电脑配置一个和FPGA板卡同一个网段的静态IP,比如FPGA是192.168.1.10,电脑就设192.168.1.100。然后在命令行执行:

ping 192.168.1.10

正常情况下,电脑会立刻收到ARP应答和ICMP回显应答。这里要注意一个细节:PING用的是ICMP协议,如果你的FPGA只实现了ARP和UDP,PING是可能不通的。所以有两种做法:要么在FPGA里再补一个简单的ICMP回显模块,要么就用一个UDP调试工具直接发包验证。

最省事的验证方式是UDP。在电脑上用一个网络调试助手,往192.168.1.10的某个端口(比如8080)发一串数据。FPGA收到之后,做的处理是:直接把数据从发送通路回发,端口不变,源IP和目的IP调换。这样在电脑侧就能收到回包,整个环路就证明了接收通路、解析模块、发送通路都是通的。

不要小看这种“回环测试”,它能帮你把整个数据通路从头到尾验证一遍。回环通了之后,再考虑接入你自己的业务逻辑,比如把FPGA采集到的数据打包上传,或接收电脑下发的控制命令。每次只在原来基础上改动一小部分,出了问题也容易定位。

5.4 网络不通时,怎么快速定位问题在哪一层

实际调试过程中,网络不通的情况太常见了。下面是我摸索出来的一套排查顺序,按这个顺序走,绝大多数问题都能在半小时内定位。

先从物理层看。板上PHY的Link LED是否点亮?不亮的话,检查PHY复位、时钟晶振是否工作、MDIO配置是否正常。用MDIO读取PHY寄存器1,检查自协商状态是否完成。这个环节最容易发现的问题是PHY型号对应的初始化序列不完全相同,有的PHY需要额外配置某些寄存器才能工作在RGMII模式,比如要通过寄存器配置默认的RGMII时钟相移、驱动电流等参数。

物理层正常后,看数据链路层。用ILA/Vivado Logic Analyzer抓eth_rxceth_rx_ctl的波形,看看是否有合法的数据帧活动。如果没有,检查PHY到FPGA的引脚连接是否正确、约束有没有写错。如果有数据帧活动,检查CRC错误计数器是不是在增长。CRC错误很多的话,优先怀疑采样时序问题。

链路层正常后,看ARP。电脑执行arp -d清除缓存,再执行ping,用Wireshark抓包看有没有ARP请求发出,FPGA有没有回ARP应答。FPGA没回的话,检查ARP逻辑中目标IP地址匹配、应答帧目的MAC字段是否正确。这里有个经验:Wireshark里的过滤条件用arp或者icmp,能非常方便地看清交互过程。

最后看UDP。Wireshark里过滤udp,如果FPGA内部发出了UDP数据帧,但电脑没收到,检查发送状态机的组帧是否正确、目标MAC地址是否正确填入、IP首部校验和是否正确计算。UDP的接收方如果发现校验和错误,会直接丢包,而且不回任何错误提示,所以这种问题特别隐蔽。

另外补充一个实际经验:Vivado的ILA虽然方便,但采样深度有限,而且占用资源。调试网络模块时,可以在逻辑里预设几个状态寄存器,比如“最近一帧的目的MAC”“最近一帧的源MAC”“CRC错误计数”“ARP请求计数”“UDP接收计数”。出现问题时,通过串口在需要时打印这些寄存器值,比反复改ILA触发条件要高效得多。这也是为什么很多有经验的FPGA工程师在调试时,总喜欢先做一个串口调测模块放在工程里。

6. 深入几个高频抗坑点:时钟约束、CRC、组帧顺序和PHY配置

6.1 时钟约束不做好,后面全是玄学问题

时钟约束在整个网络通信设计中占了极高的优先级。不少人调不通网口,不是逻辑写错了,而是约束没写对导致时序收敛状态不真实。比如发送时钟如果没正确约束成主时钟或MMCM时钟,Vivado时序分析会默认给一个比较宽松的约束,综合后认为时序收敛,实际用起来数据偏移就出了问题,表现为随机丢帧、偶发错误。

时钟约束的核心就一句话:让Vivado知道你所有的时钟从哪里来、什么频率、相位关系如何。对这类RGMII工程而言,至少要有:

  • eth_rxc作为输入时钟约束(125MHz)
  • MMCM输出到ODDR的时钟约束(125MHz,相位90度)
  • 用户系统时钟约束(比如100MHz或150MHz)

如果这些都没问题,但仍然通过不了时序验证,检查FIFO的读侧逻辑:异步FIFO的读时钟域逻辑不要太长,尽量把FIFO读数据直接馈送给发送状态机,而不是经过多级组合逻辑。组合逻辑过长往往是时序不收敛主因,优化思路是“加流水级”或者“重新安排每个状态下的数据计算”。

6.2 组帧顺序和字节序问题,最容易让人一卡就是半天

网络协议是网络字节序大端序,也就是高字节在前。在FPGA里,当你以字节流的方式构造一帧数据时,发送顺序就是网络传输顺序,所以必须按照“目的MAC地址最高字节先发”的规则填数据。例如目的MAC是00:11:22:33:44:55,发送的顺序就是0x00, 0x11, 0x22, 0x33, 0x44, 0x55。UDP协议的端口号是16位的,比如目标端口8080(十进制),十六进制是0x1F90,在帧里先发0x1F后发0x90。很多人习惯性按小端序写,一开头顺序就反了,整个报文都识别不出来。

另外IP头的校验和计算也容易踩坑。IP头校验和算法是“把IP头按16位为单位,全部累加,如果有进位则回卷加回来,最后取反”。UDP头里也有校验和,但UDP校验和还需要加入一个伪头部,包含源IP地址、目的IP地址、协议号和UDP长度,这个伪头部不参与传输,只在校验和计算时使用。

在实际项目中,如果不想自己写IP校验和模块,有一个更省事的思路:当FPGA收到一个UDP包需要原样回发时,可以把IP头、UDP头里的IP地址和端口做交换后,重新计算校验和。计算量并不大,而且这个逻辑写完以后可以反复复用。

6.3 PHY芯片的工作模式配置,和速率、相移、引脚都有关系

PHY芯片默认工作模式不一定是RGMII千兆模式,有可能需要外部引脚上下拉配置。比如RTL8211系列,它的工作模式、PHY地址是通过芯片外围的电阻配置的。开发板原理图上一般已经把PHY地址设成一个固定值,你在MDIO里要用对地址。如果MDIO总线上PHY地址不对,读出来的寄存器要么全0要么全F。

还有一些PHY芯片(比如88E1512)需要在上电后通过MDIO写入特定寄存器配置RGMII时序的TX延迟和RX延迟,否则数据采样时序不对。这个配置一般在芯片手册里的“RGMII Timing”章节有说明。开发板出厂的时候可能会在PHY的硬件引脚上做了默认配置,但不一定都适合你手里的FPGA工程,所以最好先读一遍PHY寄存器了解当前状态,再决定要不要额外配置。

我个人推荐的做法是:在FPGA工程里做一个简化的MDIO初始化序列,上电后自动对PHY写几个关键寄存器,确保PHY工作在RGMII千兆全双工、内部延迟适当的模式。这样即使换了一块开发板、换了一种PHY型号,也只需要修改寄存器配置表,不用改整个MAC层逻辑。

7. 从一个能PING通的板卡,到真正能用的网络模块

当你的FPGA板卡能被电脑PING通,或者电脑和板卡可以互发UDP数据包,这意味着底层的RGMII、MAC层、ARP、UDP组帧解析这些模块都基本正常了。但是,从“Demo能跑”到“项目能交付”,中间还有不短的一段路要走。

首先是数据吞吐量。回环测试时数据量小,帧与帧之间没有压力;当你要让FPGA持续以接近千兆速率发送UDP数据时,发送带宽的计算就很重要了。每个以太网帧有固定的前导码、MAC头、IP头、UDP头和CRC开销,如果每个帧只带几百字节的UDP负载,有效带宽会低很多。设计时要在“帧大小”和“发送速率”之间做权衡,必要时会让用户逻辑把多个小数据块合并成一个大的UDP报文再发,以提高有效带宽。

其次是丢包处理。UDP没有重传机制,数据量一大,接收FIFO就可能会溢出。设计时可以利用UDP的端口号和帧序号特性,在应用层做丢包检测。比如在FPGA发送的数据报文里附加一个递增的16位序号,PC端收到后检测序号是否有跳过,以此判断丢包情况。这个思路在很多高速数据采集项目里都有应用,实现成本极低,却极大地提高了可观测性。

最后是系统集成。网络通信模块很少独立工作,它通常和ADC/DAC控制、外部存储器、信号处理逻辑协同工作。我的习惯是,从一开始就把网络通信模块设计成一个独立的IP,对外只有标准的FIFO读写接口和控制寄存器接口。这样不管后续做什么项目、换什么主控逻辑,网络模块都能直接粘贴复用,不需要从头再写一遍。这套架构在我做了几个项目之后越来越顺手,如果你也是长期做FPGA开发,建议一开始就往这个方向设计。

可以说,当你把网络通信模块真正做到项目落地,回看整个学习和调试过程,最宝贵的收获反而不是那些协议细节,而是那种“从物理信号到逻辑行为逐层打通”的排查方式。这种能力,才是FPGA开发中最值钱的部分。

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

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

立即咨询