从零基础开始碰FPGA网络通信的那些事,这一篇我尽量讲点实在的。
先说明一下背景,我是做嵌入式出身,接触FPGA大概一年半,之前一直在写STM32和Linux驱动,对硬件描述语言的认识基本停留在“能点亮LED”的水平。这个项目是给一块我们自己画的板子做网络通信,FPGA用的是Xilinx Artix-7系列,PHY芯片是RTL8211,接口类型是RGMII。刚开始查资料的时候,说实话看什么都是懵的,什么千兆MAC、DDR接口、时序约束、CRC校验,每一个词都认识,组合到一起就完全不知道从哪儿下手。但这篇文章想告诉你的核心结论就是:FPGA做网络通信没有你想象中那么难,关键是先把框架拆开,一部分一部分去吃掉它。这篇博客会从整体方案设计、接口时序、MAC层处理、UDP协议栈实现、上板调试几个维度,把我在实际开发中的完整思路和踩坑过程写下来,给那些跟我一样从近似0基础开始、想自己做FPGA网络通信的朋友一个可参考的路径。
1. 搞明白FPGA做网络通信到底在做什么
1.1 先别急着写代码,把数据从网口到板子的路径理清楚
很多新手拿到FPGA网口项目,第一反应就是去搜“FPGA UDP例程”然后复制粘贴,结果工程编译不过、上板跑不通,最后卡在两三个星期都出不了结果。我自己就经历过这个阶段,后来发现问题的根源在于:对数据路径没有全局概念。
网络通信数据的流动大概是这样的:外部网线进来的模拟信号,经过网络变压器到PHY芯片,PHY负责把模拟信号转成数字信号,同时完成时钟恢复和串并转换,然后通过RGMII接口把数据交给FPGA。FPGA这一侧要做的事情分为三层:最底层的MAC层负责处理帧的收发时序、CRC校验、帧间隙;中间层是网络协议层,一般我们需要处理ARP和UDP/IP;最上层才是用户应用逻辑,比如你要把ADC采到的数据通过UDP发出去,或者接收上位机发来的控制指令。
这个路径听起来简单,但每一层都有非常多的细节。比如RGMII接口上,数据是在时钟的双沿采样的,也就是上升沿和下降沿各采4 bit,一个时钟周期就能传8 bit,所以千兆模式下时钟是125MHz,数据接口却只有4根线。而百兆模式下,时钟被拉到25MHz,数据仍然在双沿采样,那数据速率就是100Mbps。很多人第一次看到RGMII时序图的时候会蒙圈,其实就是这个原因——一根线上同时包含了两个bit的信息,需要有一个机制把这两个bit拆分出来。
1.2 MCU做网络和FPGA做网络到底差在哪儿
在我拿到FPGA网口项目之前,我用STM32H743做过以太网通信,用的是STM32自带的MAC控制器,跑的是LwIP协议栈,整体开发体验其实是不错的,应用层写个socket就能收发。但是FPGA完全没有现成的协议栈,也没有操作系统,所有的事情都要用硬件逻辑去实现,这就会带来一个思维转变的问题。
我自己的体会是:MCU的思路是“配置外设、跑协议栈、处理中断”,FPGA的思路是“设计状态机、规划数据流、控制时序”。MCU里面网络协议栈是别人写好的,你只需要调用接口;而FPGA里面整个MAC和IP层都是你自己写的,每一个字节的收发、每一个校验的计算都由硬件电路完成。
但FPGA也有MCU没法比的优点。MCU实现网络通信,CPU占用率高,中断频繁,数据拷贝多,真正做到线速收发是很困难的。FPGA不一样,整个网络收发通路是纯硬件流水线,数据从PHY进来之后一直到用户逻辑,中间不需要CPU参与,可以实现真正的线速处理和极低延迟。在需要高速数据传输的场合,比如高速数据采集板上送512路ADC数据,或者视频图像实时传输,FPGA的方案无论是带宽还是延迟表现,都远优于MCU加软件协议栈的组合。
1.3 技术选型:为什么我选了RGMII + UDP而不是PCIe或者TCP
先说接口选型。RGMII是Reduced Gigabit Media Independent Interface的缩写,就是把原来GMII的8根数据线减少到4根,用双沿采样来保持带宽不变。通常FPGA的GMII接口和MAC核之间还会有一个转换,不过对于绝大多数板卡来说,PHY芯片都支持RGMII,FPGA侧引脚数量也更紧张,所以RGMII成了最常见的方案。
然后是协议选型。TCP在FPGA里实现起来非常痛苦,因为TCP有状态机管理、滑动窗口、重传机制、拥塞控制,这些在软件里都是很常规的逻辑,用硬件实现代码量和复杂度都会陡增。UDP就简单得多,无连接、无状态、没有确认重传,只需要拼好IP头和UDP头,算好校验和,就能发出去。对于数据采集、图像传输、工业控制这类应用,UDP丢几个包问题不大,或者说可以在应用层做简单的重传,所以UDP是FPGA网络通信的主流选择。
我自己实现的方案里,MAC层自己写RGMII收发逻辑,数据链路层实现了ARP和UDP,IP层实现了基本的IPv4收发,上层给用户一个简单的FIFO接口。整条链路用纯Verilog实现,没有调用Xilinx的MAC硬核,这样做的好处是代码完全可控,方便移植,也方便理解。
2. 核心细节拆解:RGMII接口和时钟到底怎么处理
2.1 RGMII引脚定义和接口时序,一图看懂收发关系
RGMII接口的信号其实不多,主要就是以下几根:TXC(发送时钟)、TX_CTL(发送控制)、TXD[3:0](发送数据)、RXC(接收时钟)、RX_CTL(接收控制)、RXD[3:0](接收数据)。发送方向是从FPGA到PHY,接收方向是从PHY到FPGA,两边是独立时钟域。
这里的重点在于TX_CTL和RX_CTL在双沿采样下承担了两个信号的功能:上升沿是TX_EN(发送使能),下降沿是TX_ER(发送错误)。数据也一样,TXD上升沿发送的是低4bit,下降沿发送的是高4bit。所以FPGA发送侧内部必须先做一个“双沿合并”的操作:在逻辑里维护一个4bit的DDR输出寄存器,时钟上升沿送出低4bit,时钟下降沿送出高4bit。
// RGMII 发送侧双沿数据组合示例 // 将8bit数据拆成高4bit和低4bit,分别在时钟上升沿和下降沿输出 reg [3:0] rgmii_td_hi; reg [3:0] rgmii_td_lo; always @(posedge tx_clk) begin rgmii_td_hi <= tx_data[7:4]; rgmii_td_lo <= tx_data[3:0]; end // 使用ODDR原语分别在上升沿和下降沿输出数据 ODDR #( .DDR_CLK_EDGE("SAME_EDGE") ) oddr_txd0_inst ( .Q(rgmii_txd[0]), .C(tx_clk), .CE(1'b1), .D1(rgmii_td_hi[0]), .D2(rgmii_td_lo[0]), .R(1'b0), .S(1'b0) );这个ODDR原语在Xilinx的7系列里是必须的,因为内部逻辑默认是单沿采样,如果想利用DDR输出就得靠原语把双沿数据送到IOB上。同样的道理,接收侧则要用IDDR原语把双沿数据恢复成8bit总线。
2.2 时钟方案:125MHz和125MHz 90度偏移必须分开处理
RGMII千兆模式下的TX时钟是125MHz,但PHY芯片要求FPGA提供这个时钟。这里有个很多新手都会踩的坑:RGMII发送时钟需要90度相移。
为什么要做90度相移?因为RGMII接口规定数据和时钟的对齐方式。在发送数据的时候,PHY是在时钟的上升沿和下降沿采样数据,但实际PCB走线会有延迟,如果时钟沿刚好在数据跳变沿上,采样就会不稳定。所以协议要求源同步时钟相对数据偏移90度,也就是让时钟的跳变沿落在数据位的中间位置,这样才能保证采样的可靠性。
在Vivado里实现这个90度相移有几种方法,最简单可靠的是调用MMCM或PLL生成一个相位偏移的时钟。
MMCM 配置要点: - 输入时钟:125MHz(通常由板载晶振或PHY的CLK_OUT提供) - CLKOUT0:125MHz,相位偏移90度 - 时钟资源:BUFG输出到发送时钟引脚要注意的是,不能直接把偏移时钟接到逻辑时钟网络上,因为FPGA里逻辑时钟需要经过BUFG走全局时钟网络,而RGMII的TXC引脚需要的是普通的IOB输出。我是这样处理的:125MHz偏移时钟先经过BUFG进入全局时钟网络,然后从内部逻辑驱动一个ODDR原语,ODDR的输出经过OBUF接到TXC引脚,这样既保证了时钟偏移,又保证了信号质量。
2.3 接收侧时钟和数据的对齐,以及IDELAY怎么调
接收方向是PHY把RXC和RXD同时送给FPGA,PHY发送数据时会同样做90度偏移,所以数据相对RXC是中心对齐的。但是在实际板卡上,由于PCB走线长度不同、温度变化等因素,数据相对时钟的相位会漂移,这时候就需要FPGA侧做动态调整。
7系列FPGA在IOB里集成了IDELAYE2原语,可以对输入信号进行可编程的延迟调整,延迟步进大概是78ps。我调试的时候是把ILA接在RXD和RXC上,然后用Vivado的Hardware Manager在线调整IDELAY的tap值,观察采样到的数据是否正确。当时调的RXD[3:0]是10个tap左右,RX_CTL是8个tap左右,不同板子可能不同,所以这部分调试经验很重要。
3. MAC层协议处理的实操:CRC校验、帧间隙、状态机设计
3.1 发送MAC:组帧、CRC、FIFO节拍,一个FIFO一个状态机搞定
完成RGMII接口后就要开始处理MAC层的帧格式。以太网帧的结构是:前导码(7字节0x55)+帧起始符(1字节0xD5)+目的MAC(6字节)+源MAC(6字节)+长度/类型(2字节)+数据(46~1500字节)+FCS(4字节CRC32)。
发送的时候,FPGA内部需要维护一个发送FIFO,用户逻辑把完整的一帧数据(包括MAC地址和IP头)写入FIFO,然后MAC层按节拍读出并通过RGMII发送。这样做的好处是用户逻辑不用关心时序细节,只要把数据准备好,MAC层会自己处理前导码和FCS。
发送状态机的大致结构是这样的:
- IDLE状态:等待FIFO非空,一旦有数据就开始发送前导码
- PREAMBLE状态:发送0x55七次,发0xD5一次
- DATA状态:从FIFO读数据,一个时钟一个字节往RGMII接口送
- FCS状态:在数据结束后发送4字节的CRC32校验结果
- 发送完成后回到IDLE,同时等待帧间隙
帧间隙是指两帧之间的最小间隔,以太网规定最短是96bit时间,千兆模式下就是12个时钟周期。如果不满足帧间隙,交换机可能丢弃你的包,我一开始没注意这个问题,抓包能看到连续的包被丢弃,后来加了一个简单的计数器才解决。
CRC32的计算是最容易写错的地方。以太网用的是CRC32,多项式是0x04C11DB7,初始值是0xFFFFFFFF,结果要取反后按字节序发送。Verilog实现CRC32有两种方式:一种是查表法,每个时钟处理一个字节;另一种是逐bit计算,一个时钟处理一个bit。在千兆模式下,逐bit肯定不行,因为一个时钟就得处理8bit数据,所以我用的是字节型CRC模块,网上有现成代码,但要注意字节序和取反逻辑。
3.2 接收MAC:缓存、解帧、错帧丢弃,别忘了一个关键标志
接收MAC比发送要麻烦一点,因为你需要处理各种异常情况。我的接收模块主要做以下几件事:
- 检测前导码,锁定帧头
- 接收完整帧数据并写入FIFO,同时计算CRC
- 一帧结束后检查CRC是否正确,不正确就把FIFO里的这一帧丢弃
- 检查帧长度,小于64字节的碎片帧丢弃,大于1518字节的超长帧按截断处理
- 把接收到的字节数、帧状态作为一个侧信道信息送给上层
这里最容易被忽略的是“长度不对”的情况。有些PHY在链路异常时会产生短帧,如果不丢弃,上层协议栈会把它当成有效数据来处理,很容易解析出错。我一开始就是有这个问题,抓包工具里能看到CRC错误包,但FPGA内部还是把它送进了FIFO,导致UDP校验老是不对。
接收MAC内部的FIFO我用的是Xilinx FIFO IP核,配置成标准模式,写侧时钟是125MHz,读侧时钟用用户逻辑的时钟。要注意的是,因为FIFO的数据宽度是8bit,而UDP包最大可以到1500字节,所以FIFO深度至少要4096,我直接配了8192,这样在突发接收时不会丢数据。
3.3 时序约束和跨时钟域的处理经验
FPGA开发的灵魂在于时序约束和跨时钟域设计,网络通信模块尤其明显。RGMII接收侧的时钟RXC是PHY提供的,和FPGA内部系统时钟完全异步,需要做跨时钟域处理。我的做法是把RXC时钟域的数据先经过FIFO同步到系统时钟域,所有跨时钟域的握手信号都用两级同步寄存器打拍,确保metastability不会传递到逻辑里。
发送侧相对简单,因为TXC就是我们自己生成的时钟,和系统时钟同源,直接使用时序约束保证路径收敛就行。Vivado里我会把125MHz和125MHz偏移90度这两个时钟设置成不同时钟组,避免工具在RGMII接口上做过多的时序优化导致布局布线问题。
3.4 一个实际的发送FIFO写接口代码
用户逻辑和MAC层之间的发送接口我设计成AXI4-Stream风格,简化版大概长这样:
module mac_tx_user_if ( input wire clk, input wire rst_n, input wire [31:0] user_tdata, input wire [3:0] user_tkeep, input wire user_tvalid, output wire user_tready, input wire user_tlast, output reg [31:0] fifo_wr_data, output reg [3:0] fifo_wr_keep, output reg fifo_wr_en ); // 这里做数据宽度转换,把32bit的AXI4-Stream转成MAC层需要的8bit字节流 // 用一个小状态机或者位宽转换FIFO来实现 endmodule因为这个接口是拿给上层逻辑用的,所以一定要用简洁的握手协议,不然应用工程师用起来会很痛苦。我自己是把Mac层的发送接口定义成“数据+使能+结束标志”三个信号,不搞复杂的AXI协议,让上层逻辑一看到发送使能就往里送数据。
4. 网络层和传输层:UDP协议栈自己写其实没有想象中复杂
4.1 ARP协议:板子能ping通的关键
如果你只想自己发数据出去,不做ARP也能跑,但要真正和电脑通信、通过交换机收发,ARP就绕不开。ARP的作用是把IP地址解析成MAC地址,你向电脑发UDP包之前,必须知道电脑网卡的MAC地址。
我的做法是做一个ARP请求发送模块和一个ARP响应解析模块:
- 当FPGA需要发送数据给某个IP但不知道它的MAC时,先广播发送一个ARP请求,问“谁是192.168.1.100,请告诉你的MAC地址”
- 电脑收到ARP请求后会自动回复ARP应答,里面包含它的MAC地址
- FPGA解析出MAC地址并缓存到一个寄存器里,后续发送UDP包就用这个MAC作为目的地址
- 同时,FPGA还要能响应别人发来的ARP请求,这样电脑也能通过ping来获取FPGA的MAC地址
这里有一个细节:FPGA的MAC地址和IP地址要提前固化在代码里,方便调试的话也可以做成寄存器让上位机配置。我的板子固定设置成MAC地址00:11:22:33:44:55,IP地址192.168.1.10,子网掩码255.255.255.0,方便测试。
ARP协议的Verilog实现其实就是一个状态机加一些计数器。请求状态机包括发送以太网广播帧头、ARP头、填充字段,然后等待应答;应答解析逻辑检测到ARP包的目的IP是自己时,提取源MAC和源IP存入寄存器。
4.2 IP协议头封装和校验和计算
UDP包外面套的是IP包,IP头固定20字节,头部结构如下:版本(4bit IPv4)、首部长度(4bit,固定5)、服务类型TOS(1字节置0)、总长度(2字节)、标识(2字节)、标志和片偏移(2字节置0)、TTL(1字节,一般64)、协议(1字节,UDP是17)、首部校验和(2字节)、源IP(4字节)、目的IP(4字节)。
IP首部校验和的计算方法是:把IPv4头部按16bit一组进行二进制反码求和,然后对结果取反。这个计算可以放在发送端一个字节一个字节算好,然后固化到包头里。接收端要做逆运算检查,发现校验和错误就丢掉。
很多初学者会忽略IP首部校验和,因为UDP自带校验和,感觉IP校验和多余。但实际上不正确的IP头校验会让很多协议栈直接丢弃你的包,pc抓包工具能看到包,但应用层收不到数据,排查起来特别费劲。我自己就在这个问题上卡了半天,后来用Wireshark一看,IP头校验和标记为incorrect,才发现是发送端忘算这玩意了。
4.3 UDP头封装和伪头部校验和的坑
UDP头只有8字节:源端口(2字节)、目的端口(2字节)、UDP长度(2字节,包括UDP头和数据的长度)、UDP校验和(2字节)。
UDP校验和的计算方法比较特殊,它除了UDP头和数据外,还需要加上一个12字节的“伪头部”参与计算。伪头部的内容是源IP、目的IP、协议号(17)、UDP长度。这样做是为了防止IP层把包错传给其他协议。我在实现时就是用纯组合逻辑+流水线做校验计算,发送时把校验和字段置0,然后对整个UDP包加伪头部做16bit反码求和,最后把结果填回校验和字段。
// UDP校验和计算伪代码逻辑 // 1. 将UDP头+数据按16bit分组 // 2. 加上伪头部(源IP高16bit、源IP低16bit、目的IP高16bit、 // 目的IP低16bit、协议号0x0011、UDP长度) // 3. 每组进行二进制反码累加 // 4. 对累加结果取反,写入UDP校验和字段这里特别提醒一下,UDP校验和是可选字段,如果设置为0表示发送端没有计算校验和,接收端可以选择不校验。但实际测试发现很多上位机软件对校验和为0的UDP包会有警告,所以最好还是老老实实算正确。
4.4 一个简单的UDP发包状态机范例
我给大家展示一下我的UDP发送状态机核心部分,整体逻辑不复杂,主要就是把ARP解析到的MAC、IP、端口信息拼装成一个标准以太网帧:
localparam S_IDLE = 5'd0; localparam S_ARP_CHECK = 5'd1; localparam S_ARP_SEND = 5'd2; localparam S_WAIT_ARP = 5'd3; localparam S_ETH_PREAM = 5'd4; localparam S_ETH_HEAD = 5'd5; localparam S_IP_HEAD = 5'd6; localparam S_UDP_HEAD = 5'd7; localparam S_DATA = 5'd8; localparam S_FCS = 5'd9; always @(posedge clk) begin if (!rst_n) begin state <= S_IDLE; end else begin case (state) S_IDLE: begin if (send_req && !send_busy) begin if (arp_table_valid) state <= S_ETH_PREAM; else begin state <= S_ARP_SEND; end end end // ... 省略中间状态 S_DATA: begin // 从用户FIFO读取数据,每读一字节发送一字节 if (tx_len_cnt == total_len - 1) state <= S_FCS; end S_FCS: begin // 发送CRC32后回到IDLE,同时拉高发送完成标志 state <= S_IDLE; end endcase end end这个状态机的核心就是“发送前的ARP检查”和“按序拼装报文”。如果ARP表里没有目标IP的MAC,就先去发ARP请求,等拿到应答后再继续发UDP包。整个过程对上层是透明的,上层只看到“发送请求”和“发送完成”两个信号。
5. 网络通信模块的上板调试与问题排查
5.1 调试环境搭建:ILA抓波形、Wireshark抓包、回环测试三板斧
网络通信模块做完逻辑设计后,真正上板调试才是最有意思的部分。我调试时用的工具组合是Vivado自带的ILA逻辑分析仪加PC上的Wireshark,另外用了一块普通的交换机把FPGA板子和电脑连起来。
第一步先做回环测试。我直接在FPGA内部把发送侧的FIFO接到接收侧的FIFO,让数据不经过PHY直接回环,用板上按键触发一次发送,然后用ILA看接收FIFO里有没有数据。这一步能验证MAC层和上层逻辑的正确性。
第二步做PHY回环。RTL8211的寄存器里有loopback模式,可以把发送的数据在PHY内部直接环回到接收端。通过MDIO接口配置PHY的寄存器,然后发送数据,看接收侧能不能收到。这一步能验证RGMII接口和PHY的配置是否有问题。
第三步才是真正连网线。把FPGA的IP设置成192.168.1.10,电脑设置成192.168.1.100,然后从电脑ping FPGA的IP。如果ping通了,说明ARP和ICMP的响应逻辑没问题;如果ping不同,就开始在两个工具之间来回抓。
ILA抓信号的时候,我建议把关键信号都引出来:RGMII接收的4bit数据和控制信号、MAC层的状态机状态、FIFO的读写计数、用户接口的握手信号。这样不管出问题在哪一层,都能快速定位。
5.2 常见问题速查表与解决方案实录
问题1:电脑ping不通FPGA,但ILA能看到ARP请求进来了
解决方案:检查ARP应答逻辑。ILA里看应答包里的目的MAC是不是填对了,源MAC是不是填成了自己的物理地址。我犯过的错误是在应答包里填了广播地址,导致电脑丢弃了应答。
问题2:FPGA发UDP包,Wireshark能看到包,但校验和总是incorrect
解决方案:IP和UDP的校验和计算一定要算对,IP头校验和只覆盖IP头,UDP校验和要覆盖伪头部+UDP头+数据。有一个容易忽略的地方是UDP长度字段必须和实际长度一致,不然校验和算出来永远是错的。
问题3:数据能收到,但偶尔乱序或者丢包
解决方案:检查FIFO的深度和复位逻辑。如果接收侧FIFO深度不够,突发数据会覆盖旧数据;如果复位信号跨时钟域没有同步,可能导致FIFO读写指针错乱。建议FIFO读侧和写侧的复位信号分别用各自时钟域的逻辑复位。
问题4:RGMII接口在高温下偶尔误码,抓包看到CRC错误
解决方案:一是检查PCB走线的等长设计,二是调整IDELAY的tap值。IDELAY需要根据温度和电压变化做动态调整,如果要求高可靠性,可以考虑实现一个简单的自适应校准逻辑。
问题5:链路能通,但速率上不去,只有百兆
解决方案:检查PHY的配置,是不是工作在了百兆模式。RTL8211默认是千兆模式,但上电时PHY会通过mode strap引脚来选择工作模式,要检查硬件原理图上的配置电阻是否正确。
问题6:Vivado布局布线后时序违例,特别是RGMII接口路径
解决方案:查看时序报告里是setup还是hold违例。如果是setup违例,检查逻辑级数是否太多,可以加流水线;如果是hold违例,检查是不是跨时钟域路径没有约束,或者ODDR原语的DDR_CLK_EDGE配置不对。
5.3 性能验证:实测千兆线速、延迟和资源占用
板子调通之后,我做了个简单的性能测试。用FPGA内部产生一个计数器,每计数一次就组装一个UDP包发出去,目的端口是5000,数据长度设定为1400字节,这样整包长度接近以太网最大帧长。
实测结果表明,在千兆模式下,FPGA发送带宽可以达到940Mbps左右,几乎接近千兆以太网的线速极限。帧与帧之间的间隔是严格按照96bit的帧间隙来控制的。延迟方面,从FPGA内部产生数据到电脑的Wireshark上看到包,整个链路延迟在微秒量级,比软件协议栈动辄上百微秒的延迟好太多。
资源占用方面,由于我实现的是简化的MAC加协议栈,没有使用Xilinx的MAC硬核,纯Verilog逻辑消耗的资源并不多。在Artix-7 35T上,LUT大概用了3000个左右,FF用了2000多个,BRAM用了一个18K的FIFO。对于入门级FPGA芯片来说非常友好,剩下的资源可以大量留给用户逻辑做数据采集和图像处理。
5.4 后续可以扩展的方向
完成了基础的UDP通信后,整个系统的框架就搭起来了,后续扩展非常方便。一是可以加入TCP/IP协议栈,比如用开源的三方协议栈,实现更加可靠的数据传输;二是加入PCIe接口,把FPGA作为一个高速数据采集卡插到主机里,通过PCIe和主机通信;三是加入DDR缓存,解决大流量数据缓存不够的问题;四是加入RGMII转GMII的接口,兼容老PHY芯片。
不过这些扩展方向很多都是大工程了,建议先把基础UDP收发玩到滚瓜烂熟,再去碰PCIe和DDR。
6. 最后分享几个我认为最有价值的经验
整个项目下来,如果说要总结几个对新手最有帮助的具体经验,我想说三点。
第一点是关于调试心态的。FPGA网络通信涉及的层次很多,出了问题不要一上来就怀疑硬件,先确认每一层的逻辑是否正确。我的调试顺序是:先用ILA确认RGMII接口有没有数据进来,再用Wireshark看FPGA发的包结构对不对,最后才去查协议栈的细节。这样每确认一层,就能排除一半的问题。
第二点是关于代码结构的。把MAC层、ARP、UDP、用户接口这几个模块严格分开,用清晰的握手信号连接,不要在模块内部搞各种奇怪的依赖。这样就算出了问题,也能快速定位到具体模块。
第三点是关于工程管理的。Vivado工程里把时序约束和引脚约束统一放在XDC文件里,每个关键信号加上注释,每个模块加上版本号和修改日期。网络通信项目调试周期长,代码版本管理一定要做好,我自己就经历过改来改去最后不记得哪个版本能用的尴尬。
最后再分享一个小技巧:调试UDP发送时,可以先用FPGA固定往一个目的IP发特定的数据模式(比如计数器的值),电脑上开一个UDP接收工具或者用Python写个简单的socket脚本接收,这样就能实时验证链路是否正常。Python接收代码很简单,几行就行,但调试效率非常高。这里我就直接把代码贴出来,方便大家直接抄作业。
import socket import struct # 接收UDP数据的Python脚本示例 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind(("192.168.1.100", 5000)) sock.settimeout(5) while True: try: data, addr = sock.recvfrom(2048) print(f"Received {len(data)} bytes from {addr}") # 检查数据是不是计数器递增模式 first_value = struct.unpack("!I", data[0:4])[0] second_value = struct.unpack("!I", data[4:8])[0] if second_value == first_value + 1: print("Data pattern correct") else: print(f"Data pattern error: {first_value} -> {second_value}") except socket.timeout: print("Timeout waiting for data")在调试过程中我还发现一个细节:Windows防火墙默认会阻止UDP接收程序,调网络通信的时候最好临时关闭防火墙或者在弹窗里允许Python通过。这个坑不分享一下真的对不起自己踩过的时间。