资讯动态

FPGA以太网通信设计详解:从RGMII到UDP协议栈实现

发布时间:2026/9/8 13:24:21 来源:尧图企业网站定制
做到 FPGA 开发的第七个 part前面基本语法、时序逻辑、状态机、FIFO 这些基础应该都积累得差不多了。这一篇要碰一个大家迟早绕不开的东西怎么让 FPGA 和电脑通信。串口太慢PCIE 上手成本又高其实对绝大多数入门到进阶的板卡场景以太网是最合适的一个突破口。FPGA 网络通信设计这件事说白了就是让 FPGA 通过网口把数据包发出去、收进来而这一篇我会把从 PHY 芯片到 RGMII 接口、再到 MAC 和 UDP 协议的完整链路讲清楚把每个环节为什么要这么设计也一并说明。零基础看前面几篇过来的读者跟着这篇做完是可以做到用网线直连电脑、用上位机工具收到 FPGA 发来的数据的。1. 先搞清楚 FPGA 做网络通信到底是在做什么1.1 一个包从网口到 FPGA 的逻辑链路很多初学者拿到带网口的开发板第一反应是“FPGA 是不是自带网络协议栈”。实际上网络协议栈里最复杂的那部分在 FPGA 工程里往往都是我们自己用逻辑搭出来的或者干脆不搭。先看一条完整的数据通路电脑网口发出的电信号经过网线到板子上的网口座子再进到 PHY 芯片由 PHY 芯片完成电平转换、编解码、时钟恢复输出给 FPGA 的是一组并行数据和对应的时钟。这个接口最常见的是 RGMII也就是 Reduced Gigabit Media Independent Interface缩到 4 根数据线、125MHz DDR 采样。数据到了 FPGA 这边就进入我们自己写的 MAC 层逻辑负责识别帧头、帧尾、校验 CRC把物理层送来的比特流还原成完整的以太网帧。再往上MAC 层会把一组字节交给上层逻辑上层逻辑再做 IP 解析、UDP 解析最后把有效载荷取出来给用户模块。整条链路里PHY 芯片是现成的FPGA 里要实现的主要是 MAC 层和 UDP 层。看清楚这个分工你就知道接下来该往哪个方向写代码了。1.2 为什么工程上都选 UDP 而不是 TCP做 FPGA 网络通信大多数人会从 UDP 入门这背后是有现实原因的。TCP 最麻烦的一点是连接管理、序号确认、超时重传、滑动窗口这些机制这些在 CPU 上写都很容易出隐性 bug何况是 RTL 逻辑。把一个完整 TCP 状态机撸出来LV 的代码量至少是 UDP 的十倍以上而且调试难度成倍上升很多时候为了确认一个重传包还要在逻辑分析仪上抓几百 ms 的数据效率非常低。UDP 就简单多了它本质上就是“我发我的包不管对方收没收到”。没有连接状态没有确认回复每个数据报是独立的。拿快递打比方TCP 是快递员打电话让你下楼当面签收UDP 就是往你家院子里扔个包裹扔完就走。对大量数据采集、视频图像传输、实时控制指令这种允许少量丢包的应用场景UDP 是性价比极高的方案。而且对于学习阶段UDP 的好处是你只需要关注三个东西以太网帧格式、IP 头、UDP 头。这三层加起来不到 50 个字节的头部信息手动构造和解析都完全可控。这也是为什么我建议从 UDP 开始做 FPGA 网络通信而不是一上来就碰 TCP。2. 接口层与 PHY 芯片RGMII 是怎么一回事2.1 RGMII 时序拆解FPGA 和 PHY 芯片之间最常用的接口是 RGMII接口信号并不多主要有这么几根信号名方向位宽说明TX_CLKFPGA - PHY1发送参考时钟125MHz千兆DDR 采样TXDFPGA - PHY4发送数据上升沿送低 4 位下降沿送高 4 位TX_CTLFPGA - PHY1上升沿是 TX_EN下降沿是 TX_ERRX_CLKPHY - FPGA1接收参考时钟125MHzRXDPHY - FPGA4接收数据同样 DDR 采样RX_CTLPHY - FPGA1上升沿是 RX_DV下降沿是 RX_ER设计里最容易踩坑的就是这个 DDR 采样。以发送方向为例125MHz 的 TX_CLK 每个时钟上升沿和下降沿都要送 4 位数据两个边沿凑起来是 8 位正好对应一个字节。所以在 RTL 里你不能直接像普通逻辑那样只用 posedge得用 ODDR 原语把两条边沿的数据合到一个引脚上。接收方向也一样RX_CLK 是 PHY 芯片跟随数据一起送过来的源同步时钟FPGA 里得按 DDR 的方式把 RXD 在上升沿和下降沿分别采下来再合并成一个字节。初学者最容易犯的错误是只采样上升沿结果 4 根线收到的数据只有一半解析出来全是乱码。这里有个关键细节RGMII 标准里规定接收时钟 RX_CLK 相对于数据 RXD 是有 90 度相位偏移的这么设计是为了让 FPGA 能直接在时钟中心点采样。但不同 PHY 芯片对这个延迟的处理方式不一样有些 PHY 内部已经做好了延迟有些则要求 FPGA 侧用 IDELAY 之类的原语自己补。比如 RTL8211 系列的 RX 时钟默认就是有内部延迟的直接采就行有些芯片就要看数据手册里的寄存器配置。做板级调试之前最好用 ILA 抓一下 RX_CLK 和 RXD 的相位关系看看采样窗口是否安全。2.2 PHY 芯片选型与复位配置开发板上常见的 PHY 芯片有瑞昱的 RTL8211、美满的 88E1512国产的裕太微、景略也慢慢多了。对学习来说选哪颗芯片影响不大因为 RGMII 接口是通用的FPGA 侧逻辑基本都能适配区别主要在复位时序和 MDIO 配置寄存器上。复位时序是个容易被忽视的点。很多 PHY 芯片要求复位信号拉低至少 10ms释放之后还要等内部 PLL 锁相完成芯片才能正常工作。如果你上电后立刻开始发数据通常会看到 PHY 芯片像“半睡半醒”一样TX_CLK 没有输出或者数据发不出去。我习惯的做法是FPGA 上电后拉低 PHY_RST_N用计数器延时 20ms 再拉高之后再等 100ms 才开始初始化 MDIO 或发数据。这个延时不一定要精确但给足余量绝对能省很多排错时间。MDIO 配置在不同 PHY 上差别较大。学习阶段如果是做千兆直连电脑很多 PHY 默认配置就能工作不写 MDIO 也能收发数据。但我还是建议把 MDIO 接口调通至少能读一下基本控制寄存器和状态寄存器比如看 link 是否建立、当前是 1G 还是 100M 模式。你后面调试链路问题的时候有个 MDIO 读出 PHY 状态排错效率完全不一样。3. 核心模块实现从以太网帧到 UDP 载荷3.1 整体模块划分与数据流把整个网络通信链路在 FPGA 里拆成几个模块各自职责清晰才好写代码。我常用的模块划分是这样的rgmii_rx负责 RGMII 接收把双沿采样的数据拼成字节流并解析出 RX_CTL 得到帧有效信号和帧错误信号。mac_rx在字节流上找前导码和帧起始定界符SFD把整个以太网帧按字节存入接收缓冲同时计算 CRC 并附加到帧尾。mac_tx从发送缓冲读出字节流拼接前导码、SFD、目的 MAC、源 MAC、类型/长度字段、数据和 CRC最后按 RGMII 时序发送出去。udp_stack解析收到的以太网帧提取出 UDP 载荷同时负责组发送方向的 UDP/IP 头部。axis_fifo异步 FIFO用于跨时钟域的数据缓存。数据流向一句话总结PHY 出来的数据进rgmii_rx经mac_rx存入接收 FIFOudp_stack从 FIFO 读走并解析出有效数据发送方向是反过来的用户数据进发送 FIFOudp_stack组帧mac_tx加 MAC 头最后从rgmii_tx送进 PHY。对于只用 UDP 的应用这个结构足够清晰也方便后续扩展。3.2 MAC 接收模块怎么写接收方向的关键是状态机。一个典型mac_rx状态机长这样IDLE等待字节流中出现 0x55 前导码连续检测到 7 个 0x55 后进入下一状态。SFD检测到 0xD5表示帧起始后面的字节就是真正的目标 MAC 地址。这里要留意SFD 的 D5 和前面的 55 在字节流上是连续的很多实现会把 8 个字节的 5555...D5 一起作为前导码检测。DATA不断把字节写入 FIFO同时累加计算 CRC。这里的重点是帧长计数标准以太网帧最小 64 字节最大 1518 字节超出范围要丢弃。FCS接收到最后 4 字节 CRC 值和本地计算值比对一致则帧有效否则丢弃。RGMII 接收方向提供的帧指示信号是 RX_CTL 在上升沿体现的 RX_DV。这有点像串口接收里的帧起始位只不过这里持续一拍表示当前数据线上的字节有效。写代码时要注意RX_DV 高电平期间每个时钟沿上的数据都是有效的DDR 双沿采样后要拼成完整的字节再送入状态机而不能在状态机里直接针对单个边沿处理。还有一个具体问题MAC 帧里的字节序。RGMII 数据线上先出来的 4 位是字节的高 4 位还是低 4 位不同资料里表述容易把人绕晕。最常见的做法是RGMII 的 TXD/RXD 每个边沿传一个 nibble先传高 nibble 还是后传高 nibble由 PHY 的数据手册决定。调试时最直接的办法是构造一个已知 MAC 地址的帧发出去然后抓 RXD 对照一下就知道顺序了不用死记。错误处理也要提前做。PHY 在接收异常时会通过 RX_CTL 下降沿上的 RX_ER 拉高来提示mac_rx 收到这个标志要果断把当前帧丢弃否则一个坏帧可能污染后面一大堆数据。3.3 MAC 发送模块与 CRC 处理发送方向和接收方向刚好是镜像关系先发 7 个 0x55 前导码、再发 0xD5 SFD接着是目的 MAC、源 MAC、类型字段然后数据最后 4 字节 CRC。发送状态机比接收简单因为它不用判断有效性按序把数据推出去就行。唯一要多想的是 CRC32 的计算时机。以太网采用的 CRC 是 CRC-32多项式是0x04C11DB7初始值是全 1结果还要取反。如果对每个字节单独做 CRC高位和低位的处理顺序很容易搞错。实用做法是维护一个 32 位 CRC 寄存器每个字节到达时按位更新。考虑到 FPGA 的并行处理优势可以用查表法把每个字节 8 位一次算完也可以用 LFSR 逐位循环。我自己在工程里常用的是按字节展开的组合逻辑因为处理 1 字节只需要 8 拍在 125MHz 下完全够用。CRC 还有一个容易被坑的点生成多项式在以太网里是按位反转使用的也就是说你如果直接按0x04C11DB7去写 LFSR结果大概率不对得先做 bit-reverse。很多新手第一次调 MAC 发送抓包发现 PC 收到一个“Bad CRC”的帧问题多半就出在这里。更直接的验证办法是先不实现 CRC发出去的帧最后 4 字节填任意值Wireshark 上能看到incorrect提示但前面数据都能正常解析出来。等数据链路确认没问题再去调 CRC难度会小很多。3.4 跨时钟域与 FIFO 设计FPGA 里面做网络通信必然要处理跨时钟域问题。PHY 的 RX_CLK 是 125MHz它是跟着 PHY 数据一起来的外部时钟用户逻辑可能跑在 125MHz 的系统时钟上也可能跑在别的频率比如 100MHz、150MHz。这里就存在两个时钟域的数据交换。最稳妥的解决办法是异步 FIFO。数据从 MAC 接收模块写入 FIFO用户逻辑从 FIFO 读走写入时钟是 RX_CLK读出时钟是系统时钟。Xilinx 里可以用 XPM_FIFO 原语Intel 平台可以例化 dcfifo IP也可以自己手写一个异步 FIFO。需要特别注意的是 FIFO 的深度计算如果后续要缓存一个完整的 UDP 帧那 FIFO 深度至少得大于最大帧长比如 2048x8 起步。短帧场景下 512 深度也够但为了保险我还是建议做 2048。跨时钟域最容易出的问题不在 FIFO 本身而在边沿跨时钟域的传递。比如 RX_DV 这个信号如果在时钟域 A 里拉高一拍直接送到时钟域 B 的 FSM 里就存在亚稳态风险。正确做法是把这个标志信号打两拍同步或者干脆把判断逻辑放在同一时钟域内完成对外只传经过同步的有效数据。FIFO 设计里同样要对空满标志做同步处理否则读侧看到空满标志抖动就会出现偶尔多读一拍、少写一拍的问题。4. UDP 层设计与最小工程实现4.1 UDP 帧解析与组帧有了 MAC 层之后得到的是一整条完整的以太网帧。下一步就是分析里面的 IP 层和 UDP 层。一个标准的以太网帧在 MAC 头之后是 2 字节类型字段当它等于0x0800时表示载荷是 IPv4 报文。IPv4 头部固定部分有 20 字节其中比较关键的是版本/头部长度、总长度、源 IP、目的 IP、校验和。IPv4 头之后是 UDP 头8 字节包括源端口、目的端口、UDP 长度、校验和。接收解析的思路收到完整 MAC 帧后跳过 MAC 头先看类型字段不是0x0800就丢弃继续解析 IP 头确认协议字段是 17UDP并把total_length字段里的值转换成实际载荷长度再跳过 20 字节 IP 头得到 UDP 头从 UDP 头的length字段可以算出真正的 UDP 数据长度整个 UDP 数据的起点就是 MAC 帧里倒数 4 字节 CRC 之前的那一段。组帧方向就是反着来先把 UDP 头和 IP 头填好中间放用户数据前面拼 MAC 头后面加 CRC。这里要提醒一个常见问题IP 总长度字段和 UDP 长度字段必须同时更新而且 UDP 长度的值是 8 字节 UDP 头加实际载荷长度不是单纯载荷长度。之前有朋友调了半天PC 端一直收到length 0的 UDP 包查来查去发现他把这两个长度字段写错了。IP 头校验和计算也值得说一下。IPv4 头校验和是对整个 20 字节 IP 头按照 16 位一组做反码求和再把结果取反。对 FPGA 来说这个计算量很小但需要保证每次组包时的 IP 头字段先拼好再做校验。很多简化做法是把 IP 校验和固定为0x0000跳过这在某些抓包工具里会提示 Checksum offload 错但实际数据能通。作为学习项目可以不先做但正式产品不要省。4.2 最小实现不回 ARP 也能通信的工程技巧新手做到 UDP 基本能收发后就会撞上一个典型问题PC 往开发板发 UDP 包板上能收到但开发板往 PC 发 UDP 包电脑那边收不到Wireshark 上也看不到任何包。原因在于 PC 在没有知道目的 MAC 地址之前会先发 ARP 请求询问“谁是 192.168.1.10”如果 FPGA 不回 ARP 应答PC 就把 UDP 包丢进黑洞了。最简单的解决办法是实现一个最小 ARP 应答模块收到 ARP 请求时解析出请求的 IP如果和自己配置的 IP 相符就回一个 ARP 应答包告诉对方自己的 MAC 地址。这个逻辑相比 UDP 层简单得多状态机也就三四个状态能应对绝大多数场景。如果连 ARP 都不想写还有一个工程技巧在 PC 上用静态 ARP 表把 FPGA 的 IP 绑定到 MAC 地址。Windows 下用arp -s 192.168.1.10 00-0a-35-01-02-03这种命令强制指定之后 PC 的 UDP 包会直接发到 FPGA不再发 ARP 请求。不过这个办法只适用于固定测试环境换个电脑就要重新绑一次。真正做项目AR P应答还是要写的否则板卡没法即插即用。4.3 上板验证流程仿真、抓包、回环测试上板之前建议先做定向仿真不然一上来就接网线调看到乱码都不知道是 MAC 层问题还是 PHY 芯片问题。仿真时搭一个简单的 RGMII 侧 testbench模拟 PHY 发送一个完整的 UDP 帧检查mac_rx能否正确解析出目的 MAC、长度和载荷。上板验证我一般分为三步走第一步是“PHY 链路检查”。接上网线看 PHY 的 link LED 是否亮起同时用 MDIO 读出 PHY 的 link 状态和速率确保物理层是通的。第二步是“MAC 层回环”。FPGA 内部把收到 MAC 帧的载荷原样回传PC 端用网络调试助手往开发板 IP 发一串数据看能不能原样收回来。这一步能验证 RGMII 收发、MAC 收发、FIFO 都是通的。第三步才是“UDP 层解析”。把用户数据做成固定格式比如每 10ms 发一个包含时间戳的 UDP 包PC 端用 Wireshark 抓包看内容和发送间隔是否正确。到这一步链路基本就通了后面再逐步加自己的业务逻辑。抓包工具首推 Wireshark看链路层的底层信息比网络调试助手直观得多。它能直接看到每一帧的 MAC 地址、IP 地址、UDP 端口、CRC 校验结果做 FPGA 网络调试基本离不开它。5. 常见问题速查与调试实录5.1 链路不通先查物理层再查逻辑层调试网络通信最容易犯的错误是上来就抓一个疑点猛查实际应该按“物理层 - 链路层 - 网络层 - 传输层”逐层排查。下面这张表是我把实践里最常碰到的问题和对应思路整理出来的照着它排一遍大多数问题都能定位。现象可能原因排查建议PC 显示网线未连接/无 linkPHY 复位时序不对、PHY 供电问题、网线直连/交换机模式不对检查 PHY 复位拉低时间是否够长用 MDIO 读状态寄存器确认 link 状态能 link 但 RX 数据全乱RGMII 采样边沿不对、RX 时钟相位没对准用 ILA 抓 RX_CLK 和 RXD检查采样点是否落在数据有效窗口内能收到帧但 CRC 全错CRC 多项式位序算反、字节序不对、PAD 填充不对先不管 CRC 抓前面的数据字段确认字节序正确后再调 CRCWireshark 收不到开发板发出的包ARP 未应答、源 MAC 地址配置错误、TX_CTL 极性反了用回环模式把 TX 数据接回 RX确认 MAC 层收发是否正常能收到第一包后续全部丢失FIFO 大小不够、用户逻辑没及时读走数据检查接收 FIFO 的空满标志确认用户侧读取速度是否跟得上5.2 调试中的真实案例案例一有一次调试板子开发板发出去的包电脑完全收不到但 PHY 芯片的 link 灯是亮的。我用 ILA 抓 TXD 和 TX_CTL发现 TX_CTL 始终为低。查到最后是复位信号释放后我立刻开始发数据PHY 还没完成内部配置把 FIFO 里的数据全部吞掉了。加长复位延时后问题消失。这提醒我PHY 的时序余量要给足尤其换了 PHY 型号之后一定要先读数据手册里的上电时序图。案例二某个版本的发送代码在仿真里完全正常上板后却发现 PC 收到的 UDP 包长度不对。仔细对比发现仿真里我是直接用一个任务函数连续写字节而上板时数据是断续地从 FIFO 读出的中间空了几拍。MAC 发送状态机里数据有效信号和字节流之间只差一个节拍结果那个空拍被当成了数据包的一部分。后来加了一个字节计数信号只有当有效数据字节数达到预设长度时才结束帧传输问题就解决了。案例三跨时钟域导致的偶发丢包。接收数据从一个异步 FIFO 里读出来时偶尔会出现某个包的数据少几个字节。抓了很久最后才发现是 FIFO 空标志同步后的延迟导致用户逻辑在 FIFO 还没有数据时就读了一次拿到的是旧数据。修正做法是读侧等empty拉低并且数据稳定后再拉高读使能同时增加一拍延迟保证读到的一定是新数据。5.3 仿真工具的辅助作用除了常规的 ILA 和 SignalTap仿真是排障最快的手段。很多上板后才暴露的问题其实都能在仿真里先暴露。比如 RGMII 接口的 DDR 采样在仿真文件里把 RX_CLK 加入always #4的 125MHz 时钟把 RXD 在上升沿送低 nibble、下降沿送高 nibble对比自己写的数据再看解析结果能快速发现采样顺序错误。另外一个值得养成的好习惯是写一个“帧比对”testbench预先准备一个标准 UDP 帧的十六进制数组仿真时喂给接收模块然后把模块输出的数据逐字节和预期结果比对一不一致立刻报错。这个测试写一次之后每次改代码都能跑能防止改着改着把之前调好的功能改坏。写在最后FPGA 网络通信上手后的路子其实很宽。做完基本的 UDP 收发下一步可以加 CRC 校验、ARP 主动应答、IP 分片处理想做更高速率可以换 SGMII/光口或者上 PCIE想往业务应用走可以把 UDP 载荷接到图像采集或 ADC 数据采集模块上构成一个完整的数据采集系统。我自己做这个 part 的时候最大的体会是网络通信在外行眼里感觉很高深一旦把“PHY 做物理层、FPGA 做 MAC 和 UDP、PC 用抓包工具验证”这个大框架搭起来后面很多问题都能用“查链路、抓波形、看帧格式”这三板斧解决。最后再分享一个实用小技巧调试 UDP 通信时不要只在 PC 上用网络调试助手看看数据对不对建议开一个 Wireshark 实时抓包窗口同时观察链路层的错误统计。很多时候你收到数据看起来“差不多”但帧被补过 PAD、IP 总长度字段有误这些在 Wireshark 里一眼就能看出来。把这个工具用熟FPGA 网络通信的调试速度真的能快上一大截。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价