news 2026/9/7 2:33:22

FPGA实现UDP网络通信:从RGMII到ARP的完整实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FPGA实现UDP网络通信:从RGMII到ARP的完整实战指南

先问一句:你手头那个"数据采集+FPGA"的项目,最后是不是都卡在数据传输上?串口太慢,FIFO只能在板子里自嗨,SD卡又没法实时看波形。这一篇part.7,我们来把网口这件事彻底做通——在FPGA里写一个最精简但能实际运行的UDP网络通信模块,配上板载PHY芯片,让板卡和电脑之间通过网线实现双向用户数据收发。核心关键词就是FPGA网络通信设计,不讲大而全的协议栈,只讲从物理层到UDP打包这几层最容易被新手卡住的地方。

这个方案适用的人群很明确:已经会Verilog基础语法、写过状态机和FIFO,但还没碰过以太网的进阶学习者。别担心,eth看起来像一堆协议,实际上FPGA里要做的只是"按固定字节表把数据发出去",再把收到的包按字节表拆开,没那么玄乎。

1. 先搭好整体思路:从物理层到UDP的完整链路

1.1 为什么第一套协议栈选UDP而不是TCP

一说要搞网络通信,很多人第一反应是TCP。我劝你冷静。TCP在软件里很成熟,但在FPGA里实现,等于要自己用Verilog写一整套状态机来维护连接建立、序号确认、超时重传、滑动窗口。光是连接状态和重传定时器就能把初学者的耐心耗光,更别说出错后的调试难度。

UDP就不一样:无连接、无状态、无重传,通信双方直接往网络上丢数据报就行。链路通不通、对方收没收到,全部由应用层自己负责。恰恰是这种"不靠谱",给了FPGA用简单寄存器逻辑实现网络通信的可能。

而且从实际项目看,FPGA负责的往往是高速数据流——图像采集、ADC采样、过程控制,这类场景全都是"我发你收",丢一个包在应用层加个序号就能解决。所以不要觉得选UDP低级,嵌入式界一直这么干,这是性价比最高的方案。

1.2 硬件平台:开发板上的PHY和网口

做FPGA网络通信,硬件上最少需要三样:FPGA芯片、PHY芯片、RJ45座子(通常带网络变压器)。目前主流开发板,比如黑金、正点原子悟空、米联客的板子,多数都集成了千兆PHY,常见型号有RTL8211、YT8531、88E1512。这些PHY基本都支持RGMII接口,这也是FPGA最常用的连接方式。

如果你的板子上没有网口,也可以单独买一块PHY小模块,用引脚和FPGA连起来。这时候要先确认两件事:PHY的地址(一般由硬件引脚决定,常见0x00或0x01),以及PHY的工作模式(RGMII千兆还是RMII百兆)。多数开发板上PHY的RGMII/千兆模式通过硬件上下拉已经固定好了,不需要写寄存器也能跑,只用MDIO做可选配置。这一点后面会专门讲。

1.3 模块划分:把协议栈拆成容易写的小块

协议栈听上去复杂,实际上FPGA里面只做链路层+网络层+传输层的子集。我用到的模块划分是这样:

模块功能所处时钟域
rgmii_tx / rgmii_rx处理RGMII物理层DDR收发TXC / RXC
mac_frame_tx / mac_frame_rx组装/解析以太网帧,计算CRCTXC / RXC
arp_module侦听ARP请求,构造ARP应答用户时钟域
ip_udp_tx用户数据封装成IP+UDP帧用户时钟域
ip_udp_rx解析IP+UDP头,提取payload用户时钟域
async_fifo跨时钟域缓存用户数据双口

顶层把用户逻辑产生的数据写进发送FIFO,ip_udp_tx从FIFO读出,依次拼上UDP头、IP头、MAC头、CRC,交给rgmii_tx发出去。接收方向反着来,rgmii_rx收到数据,mac_frame_rx剥掉帧头,ip_udp_rx再把UDP payload提取出来写进接收FIFO,供用户模块读取。

这样一拆,你会发现每个模块其实只干一件事,比写一个"万能网络模块"容易十倍。

2. 动手第一步:RGMII接口和时钟,最容易被轻视的物理层

2.1 RGMII的信号与时序

RGMII全称是Reduced Gigabit Media Independent Interface,引脚非常少,收发各只需要4根数据线加1根控制线。千兆以太网线速是1Gbps,但数据线只有4根,所以靠DDR技术解决:时钟频率125MHz,在时钟上升沿传低4位,下降沿传高4位,等效每个时钟周期传8bit,正好凑出千兆速度。

信号名方向说明
TXCFPGA→PHY发送时钟,125MHz
TXD[3:0]FPGA→PHY上升沿发低4bit,下降沿发高4bit
TX_CTLFPGA→PHY上升沿=TX_EN,下降沿=TX_ER
RXCPHY→FPGA接收时钟,由PHY恢复
RXD[3:0]PHY→FPGA上升沿采低4bit,下降沿采高4bit
RX_CTLPHY→FPGA上升沿=RX_DV,下降沿=RX_ER

这里最容易踩坑的是时钟。发送时钟TXC必须由FPGA侧产生,是精确的125MHz,不能用逻辑自己分频去驱动引脚,必须走PLL/MMCM。很多开发板的PHY还有一根外接25MHz晶振,那是给PHY自己用的,和FPGA里的TXC不是一回事。

2.2 发送方向:ODDR控制数据和时钟

发送方向的本质是:在每个时钟沿把对应的4bit放到TXD上。在Xilinx FPGA里我一般直接用ODDR原语来完成DDR输出,而不是自己写两个always块——后者综合容易出问题。ODDR的原语调用大概这样:

ODDR #(.DDR_CLK_EDGE("SAME_EDGE")) u_txd0 ( .Q (txd[0]), .D1 (tx_byte[0]), // 上升沿发出的bit .D2 (tx_byte[4]), // 下降沿发出的bit .C (clk_125m), .CE (1'b1), .R (1'b0), .S (1'b0) );

TX_CTL也是同样的道理,把发送使能tx_en接到D1、错误指示tx_er接到D2即可。TXC本身一般用MMCM生成125MHz后,再通过一个ODDR把时钟搬出来。需要注意,TXC和TXD的相位关系必须对齐,大多数PHY要求数据相对时钟中心对齐,FPGA端只要保证两者来自同一个时钟源、路径长度接近,普通板子都能正常工作。

2.3 接收方向:RXC时钟域与异步FIFO

接收方向是很多新手翻车的地方。RXC是PHY从数据中恢复出来的时钟,它和FPGA的系统时钟完全是两个不同的时钟源。必须用RXC去采样RXD和RX_CTL,然后用异步FIFO把数据转到用户时钟域,绝不能用系统125MHz时钟去直接采RXD,那样会偶尔采到亚稳态,出现"帧偶尔丢一个字节"这种极其难查的bug。

接收侧DDR解出8bit数据之后,连同rx_valid一起写进一个深度256的异步FIFO就够缓存一个帧了。用户逻辑从FIFO读侧再逐字节拿出来解析。这个异步FIFO是整个设计里最容易偷懒也最容易埋雷的地方,建议直接用Xilinx的FIFO Generator IP,设置"独立时钟"模式,比手写稳得多。

还有一种情况:RGMII接收方向上,PHY输出的RXD和RXC之间会存在一定的走线延时,如果板子布线不好或者PHY没有开内部延迟,你可能会发现收到的首字节偶尔错位。解决办法有两个:改PHY寄存器的rx delay,或者在FPGA里用IDELAY原语调整采样点。多数开发板默认已经开好,如果遇到抓包首字节全是垃圾,优先去查PHY手册里的delay寄存器。

3. 写入和解析以太网帧:MAC逻辑与CRC32实现

3.1 以太网帧格式:只有四段需要操心

以太网帧在链路上按顺序是这么排的:

字段长度内容说明
前导码7字节每个字节0x55
帧起始定界符SFD1字节0xD5
目的MAC6字节目标设备的MAC地址
源MAC6字节本端MAC地址
类型/长度2字节0x0800=IPv4,0x0806=ARP
数据载荷46~1500字节上层协议数据
FCS4字节CRC32校验

发送时,前导码和SFD用状态机固定依次送出。目标MAC、源MAC、类型这些拼在帧头。当载荷不足46字节时,必须填充到46字节再算CRC,这是以太网最小帧长64字节的要求,否则对端网卡会直接丢弃。

帧和帧之间还要留至少12字节的帧间隙IFG,PC网卡才认。很多初学者发完一帧立刻发下一帧,导致wireshark里看着连续,实际上PC丢包率很高。

3.2 发送状态机的字段拼装细节

FPGA发送MAC帧通常用一个状态机来完成逐字节输出,状态机设计成:IDLE → PREAMBLE → DA → SA → TYPE → PAYLOAD → FCS → IFG。每个状态稳定输出一个时钟周期的字节和valid信号。

关键点在于PAYLOAD状态的地址和长度控制。在用户数据进入发送FIFO之前,需要先把要发的UDP报文长度固定下来,然后由发送状态机从FIFO里读多少字节、不足填充0,都要在PAYLOAD状态里用一个计数器精确控制。CRC要算整个DA到PAYLOAD的内容,所以常用实现是把所有待发字节一边送给RGMII发送,一边同时送给CRC计算模块,到FCS状态时把CRC结果送出去,而不是先算好再发。

3.3 CRC32:多项式、初值和最容易错的地方

CRC32是FPGA网络设计里最容易翻车的地方。标准还太容易搞混的点:多项式是0x04C11DB7,需要做位反射,初值全1,结果需要再异或全1。很多同学直接用软件算法搬到Verilog,结果发出去wireshark里的FCS怎么都对不上。

说实话,在FPGA里手写并行CRC32对新手不友好,我一开始也是靠抄表。你可以用两种办法之一:

  • 用Xilinx的CRC Generator IP核,选CRC-32、多项式0x04C11DB7、初值0xFFFFFFFF、输出异或0xFFFFFFFF、按字节输入,直接生成并行CRC计算器。
  • 用查表法:写一个8位CRC表(256项),状态机每处理一个字节查一次表,1个时钟处理1字节,速度完全够。

这里提醒一个字节序的坑:CRC结果的32位在写入FCS字段时,不是直接按计算值的最高字节在前发送,而是要看链路层的位反转。实战中最快的验证方法是:发一个已知payload的包,用wireshark看它期望的FCS是多少,和你的CRC输出比较。我第一次调就卡在这个字节序上,折腾了两天才发现结果是"bit反射之后从低字节开始发送"。

4. 让FPGA能被Ping通:ARP协议的最简实现

4.1 为什么没有ARP就发不出UDP包

看到这里你可能急着写UDP。别急,先搞清一个事实:如果要让PC往FPGA发UDP包,PC必须知道FPGA的MAC地址;但如果PC不知道,它第一个动作是发一个ARP广播包,询问"192.168.1.10的MAC地址是谁"。FPGA如果不应答ARP,PC发的UDP包根本到不了板子。

反过来,FPGA要向PC发包,也必须知道PC的MAC地址。最简单办法:PC先ping一下FPGA,FPGA从收到的ARP请求里学到对应IP的MAC,然后缓存起来;或者干脆在代码里人为配置PC的MAC(不过这样换台电脑就麻烦了)。所以先做ARP应答,让FPGA能被ping通,是一个标志性里程碑——代表物理层、MAC层、解析逻辑全部跑通。

4.2 最小ARP应答模块的设计思路

ARP请求帧的以太网类型是0x0806,MAC层解包后会得到一个28字节的ARP报文。报文格式如下:

字段长度说明
硬件类型2字节以太网=1
协议类型2字节IPv4=0x0800
硬件地址长度1字节6
协议地址长度1字节4
操作码2字节1=请求,2=应答
发送方MAC6字节请求方MAC
发送方IP4字节请求方IP
目标MAC6字节请求时通常全0
目标IP4字节本机IP

最小ARP模块要做的事:接收端检测到以太网类型0x0806且操作码为1,再比对目标IP是不是本端的IP,是就当场构造一个28字节的应答包。应答包里的目标MAC填请求方MAC,目标IP填请求方IP,源MAC填自己,然后交给MAC发送模块发出去。不需要缓存表,不需要额外状态机维护。

4.3 MAC地址与IP配置:参数化才能方便调

本机MAC和本机IP建议做成模块参数或者寄存器,方便后续修改。测试环境我建议这么配:

  • PC静态IP:192.168.1.100,掩码255.255.255.0
  • FPGA端IP:192.168.1.10
  • FPGA端MAC:例如00:11:22:33:44:55(只要不冲突就行)

两块设备直接用一根网线连,不需要交换机,加载完bit后Ping一下。如果通了,说明FPGA的RGMII发送、MAC发送、ARP解析全通了,这是一个非常值得庆祝的时刻。

5. 真正的数据通路:UDP封装与回环收发设计

5.1 IP头和UDP头怎么拼,校验和怎么算

UDP报文封装在IP包里,IP包封装在MAC帧里。写给FPGA的发送状态机时,本质就是按固定顺序输出以下字节序列:

MAC头(目的MAC+源MAC+0x0800)→ IP头20字节 → UDP头8字节 → 用户数据payload。

IP头字段如下:

字段长度常见值/说明
版本/IHL1字节0x45(IPv4,头长5个32位字)
DSCP/ECN1字节0x00
总长度2字节IP头20 + UDP头8 + payload长度
标识ID2字节每个包递增即可,小流量不用管
Flags/分片偏移2字节0x0000(不分片)
TTL1字节0x40或0x80
协议1字节17(UDP)
头校验和2字节只算IP头
源IP4字节FPGA的IP
目的IP4字节PC的IP

IP校验和的算法很简单:把IP头按16bit一组累加,高位溢出回卷,最后取反。用Verilog的函数实现大概是:

function [15:0] ip_checksum; input [159:0] hdr; // 20字节IP头 integer i; reg [31:0] sum; begin sum = 0; for (i = 0; i < 10; i = i + 1) sum = sum + hdr[i*16 +: 16]; sum = (sum & 16'hFFFF) + (sum >> 16); sum = (sum & 16'hFFFF) + (sum >> 16); ip_checksum = ~sum; end endfunction

UDP头就4个字段:源端口2字节、目的端口2字节、UDP长度2字节(8+payload长度)、校验和2字节。在IPv4里,UDP校验和可以填0,表示接收方跳过校验。初期千万不要去写UDP校验和,因为需要加伪头参与计算,非常容易错,先跑通再优化。

5.2 发送链路:从用户数据到上行帧

顶层设计里,用户逻辑先把要发的数据写入发送FIFO,ip_udp_tx模块收到"开始发送"信号后进入打包状态机。我习惯把发送长度做成寄存器可配置,上位机可以通过另一条UDP命令设置payload长度,默认1024字节。

状态机输出顺序:MAC头 → IP头 → UDP头 → payload → CRC。IP头里的总长度参数,需要在开始前就定好,因为状态机是一边发一边算CRC,没有回头机会。标识ID字段可以每发一帧累加1,PC端看到ID连续递增,基本能确认数据包的完整性。

这里有个小技巧:把FIFO的读信号和MAC发送状态机的字节流对齐。要让FIFO在读使能有效后的第一个时钟周期就把数据送到MAC的数据线上,这需要仔细设计FIFO读时序。我用的是标准做法:在PAYLOAD状态,FIFO读使能拉高,下一拍从FIFO输出读到的字节并同时计入CRC,直到计数器到达配置长度。如果配置长度没有填满46字节,MAC层的填充逻辑会把后面的填充字节补上。

5.3 接收链路与回环模式

接收链路是发送的镜像。mac_frame_rx剥掉前导码,检测到类型0x0800后把整个IP包交给ip_udp_rx。ip_udp_rx解析IP头,确认协议字段是17,再解析UDP头拿到端口和长度,把payload写进接收FIFO,同时产生一个rx_pkt_end信号通知用户逻辑。

最直接验证整条链路的方式是做"回环":顶层模块把接收FIFO读出的数据,原封不动写进发送FIFO。这样PC发任何UDP包,FPGA都会立刻原样发回来,现象就是"PC发什么,PC收什么"。这一步能通,就证明双向通路全部OK,后面替换成采集数据或命令解析逻辑就水到渠成。

要注意接收和发送端口可以不一样。比如PC往FPGA发5000端口,FPGA从5001端口回环出去。Wireshark里能同时看到这两个包,回环源IP会显示成FPGA的IP,源MAC也会变成FPGA的MAC。

6. 上板实测:从Ping到UDP回环的调试流程

6.1 链路Up之前的检查顺序

如果插上网线后PC显示"网络电缆被拔出"或者Link灯不亮,先别急着查FPGA代码,十有八九是PHY层面的问题。我一般按顺序排查:

  • 上电后,检查PHY的复位引脚是否释放,开发板一般由FPGA控制复位,可能是低有效,确认复位后过几十毫秒再操作。
  • 用ILA抓PHY的中断/状态寄存器,或者看板子上的Link LED是否亮起。
  • 确认PC网卡IP是静态配置,Windows里面把防火墙对"文件和打印机共享"之外的双向通信都放行,或者直接用调试工具。

如果PHY已经Link Up但ping不通,问题就在MAC逻辑或ARP逻辑,要用ILA逐级抓包。

6.2 用ILA抓关键信号的经验

ILA抓信号是调试RGMII的最有力手段,但新手经常抓手不对。RXD[3:0]是DDR信号,直接抓原始的RXD是抓不出有效波形的,要在IDDR转换之后、根据RXC时钟域提取出的8bit信号上抓。也就是说ILA的时钟应该选择RXC域里恢复出来的采样时钟,触发条件可以设为SFD检测到(8'hD5)或者rx_valid拉高。

如果你写的是同步设计,ILA时钟选系统125MHz,然后挂在async_fifo读侧也是可以的,触发信号选rx_pkt_end。先用同一次触发把整条接收链路抓全,从FIFO读侧往前对比,能看到帧头就是巨大胜利。

还有个小坑:ILA不能直接在RGMII的TXC/RXC引脚上抓,因为引脚已经不是普通逻辑信号了。要抓就抓原语之前的逻辑节点,比如ODDR的D1/D2或者IDDR的Q1/Q2。

6.3 Wireshark怎么确认协议正确性

PC端验证工具我推荐两个:wireshark抓包看协议细节,网络调试助手或者Python socket发送UDP数据。先用wireshark打开,筛选udp。第一次做UDP设计时,看到很多包显示"IP checksum offload"或者"udp checksum offload",不用慌——这是网卡驱动的校验和卸载功能,不影响数据内容本身。

重点检查这几项:

  • PC发出的UDP包,目的IP是不是FPGA的IP,目的端口对不对。
  • FPGA回环的包,源IP是不是FPGA的IP,源端口是不是设置的5001,payload和PC发出的是不是一致。
  • FCS错误是否很多。wireshark如果显示少量CRC错误,可能是PHY的rx delay没调好,需要去配置PHY寄存器;如果错误出现在PC收到的FPGA包上,那是FPGA发送CRC的问题。

调通后的标准动作是:用网络调试助手往FPGA发一串内容,比如"hello_fpga_01",然后看收到的回环内容是不是一字不差。能对上,这个网络通信模块就算正式可用了。

7. 踩过坑之后的经验和下一步方向

7.1 时钟约束与复位,不处理好就会随机出错

这个项目对时序约束要求很高,尤其TXC的125MHz时钟必须是"约束过的生成时钟",不是随便一个时钟就能直接驱动ODDR输出。在Vivado里,如果MMCM的125MHz输出没有正确声明create_generated_clock,静态时序分析会报错,但bit文件还是能生成,上板以后就可能出现"偶尔正常、偶尔卡死"的状况。

RXC是PHY传入的异步时钟,也要做set_input_delay约束。如果你对约束不熟,至少要做到:先把TXC所连接的MMCM输出引脚约束到TXC引脚上,让工具自动布线时意识到这是一条时钟输出路径。

复位更要注意:不要一上电就立即发UDP包。PHY芯片从上电到准备好,通常要几十毫秒,建议在顶层做一个上电计数器,延迟100ms后再开始发送和ARP应答。另外MAC和用户逻辑的复位要分开,避免整个链路同时复位。

7.2 PHY的MDIO配置:不配也能跑,配了更可控

我遇到的开发板里,约有一半默认就能跑RGMII千兆模式,另一半需要写MDIO寄存器。MDIO是两根线的管理接口,时钟MDC和双向数据MDIO,用来读写PHY内部寄存器。最常改的寄存器是:

  • 0x00控制寄存器:把ANEN(自动协商使能)置1。
  • 0x04/0x05状态寄存器:读Link状态。
  • 扩展RGMII delay寄存器:不同PHY地址不同,比如RTL8211的寄存器0x1E、0x1F用于选择RX/TX时钟延迟。

如果板子的PHY默认没有开启RX时钟延迟,收数据会不定时丢字节。我的经验是:先按默认配置跑,通过wireshark的CRC统计判断需要不需要改delay;确定需要改的话再写MDIO。你不需要在FPGA里写一个复杂的MDIO控制器,一个简单的状态机,每次操作发32个周期管理帧即可。

7.3 下一步可以怎么走:光口、PCIe还是TCP

UDP调通之后,FPGA网络通信就进入了一个"真能用"的状态。如果你的下一个场景是更大带宽的项目,可以往两个方向走:一是光口收发,核心是FPGA内的高速收发器GTX/GTH,配合SFP+模块实现万兆级通信,逻辑上跟RGMII类似,但物理层和时钟恢复都有更严格的要求;二是PCIe,通过DMA把大批量数据搬到主机内存,适合上位机需要千兆以上吞吐的场景,但学习曲线陡得多。

至于TCP,如果真的需要一个TCP服务器,我建议不要用纯FPGA硬写协议栈。更合理的方案是FPGA保持UDP数据通路,把TCP协议栈交给旁边的小型处理器比如Zynq的ARM核,两者之间用AXI总线互联——这才是工程上最常见的做法。

下次如果有人问你怎么让FPGA上网,你就可以从容地把这篇文章丢过去,告诉他先去写个能ping通的板子吧。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 2:32:09

Maven仓库与打包全解析:从本地依赖到可执行jar的实战指南

简介&#xff1a;面向 Java 开发者的 Maven 实践资源&#xff0c;紧密围绕仓库机制、本地 JAR 引入和可执行 JAR 打包三个核心主题展开。内容先比较本地仓库、远程仓库与中央仓库的定位和协作关系&#xff0c;再说明如何将本地 JAR 按 groupId/artifactId/version 坐标放入本地…

作者头像 李华
网站建设 2026/9/7 2:31:49

PDF编辑与OCR识别:从扫描件到批量处理的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 2:31:20

全能Agent养成记:从Skills设计到腾讯云部署的最佳实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 2:26:38

相控阵雷达原理与工程实践:从相位差到有源阵列

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华