ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FPGA上UDP通信实战:verilog-ethernet开源协议栈解析

FPGA上UDP通信实战:verilog-ethernet开源协议栈解析 从第一次抢到一片 FPGA 开发板到前几篇能自己把流水灯、串口收发、简单状态机跑起来这个过程大多数人都走得挺顺。可一旦到了“用 FPGA 做网络通信”这一步很多人会突然卡住网络上资料不少但要么是教你调 Xilinx 的 IP 核要么就是贴一段看着很玄的 Verilog没人告诉你整条链路是怎么串起来的。我自己当初就是在这种状态下翻到了 Alex Forencich 的开源项目 verilog-ethernet用它把 UDP 从仿真跑到板卡才算是把“FPGA 上的网络”这件事彻底想通了。这篇 part.10 就是要把我接触这个开源协议栈工程时的完整过程拆开来讲。它不是一份简单的 README 翻译而是从模块划分、数据流向、仿真验证到实际抓包的一条学习路径。如果你现在对 UDP 的理解只停留在“无连接、不可靠、头比较短”这个层面同时手里又有一块差不多的 FPGA 板子那这篇文章应该能帮你少走不少弯路。1. 先搞清楚这个开源工程到底给了你什么1.1 模块地图别急着看代码先看目录verilog-ethernet 这个工程和很多开源 Verilog 项目一样把功能拆得特别碎。第一次打开仓库目录时很容易在几十个.v文件里迷失方向。我建议你先别管细节先按功能把这几个核心模块看明白模块/文件作用难度eth_mac_1g/eth_mac_10g以太网 MAC 层负责处理帧间隙、前导码、CRC 校验中eth_axis_rx/eth_axis_txMAC 层和 AXI-Stream 接口之间的桥接中arp_cachearp_eth_rx/txARP 缓存表、ARP 请求/应答帧处理中偏高udp_ip_rx/udp_ip_txUDP/IP 协议的接收和发送核心高udp_checksum_gen/udp_checksum_calcUDP 校验和生成与校验中axis_fifo/eth_axis_fifo跨时钟域、数据缓冲低但绕不开如果你用的是 1G 以太网那重点就是eth_mac_1g、udp_ip_tx、udp_ip_rx、arp_cache这四个模块。它们之间的连接关系大概是上层应用通过 AXI-Stream 把用户数据交给udp_ip_tx它负责拼出 UDP 头和 IP 头再把整包数据交给eth_axis_tx后者补上以太网头和 CRC最终通过eth_mac_1g发出去。收包则是完全相反的一条链路。这个结构第一次看会觉得复杂但它其实是一个非常标准的“协议分层”思路和你学 TCP/IP 时接触的分层模型完全一致。FPGA 里的分层不是说分软件的任务而是把每一层用硬件逻辑单独实现这也是这个工程最适合学习的地方你能在代码里同时看到 ARP、IP、UDP 三种协议各自的处理逻辑而不是被一大堆操作系统抽象层包着。1.2 阅读顺序按数据流读不要按文件名读有朋友问我是不是要从eth_mac_1g开始读我个人的体验是不要从 MAC 层开始。MAC 层牵扯到太多的时序细节尤其是 CRC 和帧间隙处理对初学者来说很容易劝退。我的建议是先读 UDP 发送方向先看udp_ip_tx理解“用户数据怎么变成 UDP/IP 报文”再看arp_cache和arp_eth_tx理解“怎么知道目的 MAC 地址”最后看eth_axis_tx和eth_mac_1g理解“报文怎么变成比特流发到线上”。这其实也是你在实际开发时最容易遇到 bug 的顺序先是报文内容不对再是目的地址不对最后才是物理接口不通。1.3 学习前你需要掌握的三个基础严格说是两个半。第一个必须是 AXI-Stream 总线协议这是 verilog-ethernet 通用的数据接口。只要tvalid、tready、tlast、tdata这四个信号搞不清楚后面所有代码读起来都是天书。第二个是抓包工具的基本使用Wireshark 就够不需要学太多但必须能看懂一个 UDP 报文在以太网帧里的排列方式。剩下的半个是 Verilog 的 Testbench 基础包括initial、#延时、$display、波形查看这些因为我们要靠仿真来理解行为。如果你这三个基础还没到位先不要碰协议栈回去把 AXI-Stream 握手搞明白。这不是浪费时间因为在 FPGA 里做网络本质上就是“把一串数据按协议要求从一个接口搬到另一个接口”而 AXI-Stream 就是那个“搬家公司”的通用规则。2. ARP 与寻路看起来最不起眼却最容易翻车2.1 为什么发 UDP 之前要先搞定 ARP很多刚接触协议栈的人都会有个疑问UDP 不是只需要 IP 地址和端口吗为什么 FPGA 内部还要维护一个 ARP 缓存道理其实很简单。以太网是链路层协议数据在局域网内传输时网卡和交换机只认 MAC 地址。IP 地址是为了互联网层设计的逻辑地址到了真正往网线上发数据那一刻必须把目的 IP“翻译”成目的 MAC。这个翻译过程就是 ARP。你的 PC 通过操作系统自带协议栈早就维护好了 ARP 表你感知不到但 FPGA 上没有任何操作系统帮你做这件事所以这个表得用硬件逻辑自己维护。verilog-ethernet 里arp_cache做的事情就是收到对端发来的 ARP 应答时把 IP 和 MAC 的对应关系存进一张表当需要发送 UDP 报文时先从表里查目的 IP 对应的 MAC如果没有则发起一个 ARP 请求帧等对方回复后再把真正要发的 UDP 数据送出去。2.2 一个 ARP 请求帧的长相学习硬件协议栈的一个好处是你必须把一个帧的每一个字节都落清楚。ARP 请求帧的以太网头部里目的 MAC 是全FF:FF:FF:FF:FF:FF源 MAC 是自己的 MACEtherType 是0x0806。接着是 ARP 报文其中硬件类型是0x0001以太网协议类型是0x0800IPv4硬件地址长度是 6协议地址长度是 4操作码0x0001表示请求。这些字段在arp_eth_tx模块里会一个字节一个字节地拼出来绝大多数情况下并不需要你手动去构造但理解它有两个实际好处一是调试时抓包能看懂每个字节的含义二是如果你想让 FPGA 主动告诉对方“我在这里”有时需要手动实现 gratuitous ARP 或者提前触发一次 ARP 请求。2.3 ARP 表项超时是等你遇到时才能体会的坑arp_cache内部的表项是有寿命的不是永久有效。默认情况下表项会在一定时间后失效。这在 PC 上问题不大因为操作系统会定期刷新。但在 FPGA 上如果你的应用连续发送 UDP 但中间隔了很久ARP 表项可能已经过期下一次发送时协议栈会先重新发起 ARP 请求这就会增加几十毫秒到几百毫秒不等的延迟。更常见的坑是你的 PC 端软件设置了比较短的发送间隔但 FPGA 端应用层没有做 ARP 预请求导致每次发 UDP 前都要等待 ARP 应答。我调某个演示工程时就遇到过 PC 端每 500ms 发一次查询、FPGA 每 500ms 才回一个 UDP但 PC 端总是显示“超时”。后来抓包才发现FPGA 发出的 ARP 请求 PC 收到了但 PC 的 ARP 应答在 FPGA 这边被当成无效表项丢弃了因为arp_cache的例化参数里超时设置太短。把超时配大之后问题立刻消失。所以当你看到“PC 收不到 FPGA 的 UDP 数据”时第一件事不应该是怀疑 UDP 模块本身而是先抓包看有没有 ARP 请求、有没有 ARP 应答这一步能排除掉一半的问题。3. 首选通过仿真把协议学明白跑 testbench 真的不难3.1 拿到仓库后我建议你第一个跑起来的仿真很多新手拿到开源工程第一反应是直接上板子。我强烈不建议这样做。verilog-ethernet 仓库里的模块大部分都带了配套的 testbench比如tb_udp_ip_tx.v、tb_udp_ip_rx.v运行仿真并不需要真实网卡和 PHY 芯片只需要仿真工具。以tb_udp_ip_tx为例它会在仿真环境里生成一个完整的发送流程产生一个带tlast的用户数据包送入udp_ip_tx同时模拟 ARP 表已经命中。你不需要改动任何 RTL 代码直接跑一遍仿真就能看到 UDP 报文在输出端的波形。这是理解协议栈行为最快速的方式也远比读代码直观。具体操作上如果你用 Vivado可以直接把相关源文件和 testbench 添加进工程然后在 Simulation 里跑行为仿真如果你喜欢命令行用 iverilog 也很方便。iverilog 加 GTKWave 的组合对学习足够用了跑一个 testbench 基本就是几条命令的事情。3.2 仿真时重点盯住 AXI-Stream 的握手细节打开仿真波形后我建议你第一个关注点放在s_axis_udp_tvalid和s_axis_udp_tready这两个信号上。AXI-Stream 的规则是只有当tvalid和tready同时为高时tdata上的数据才是有效传输。发送端拉高tvalid后必须等到接收端拉高tready才能开始传接收端如果还没准备好就可以一直不拉高tready。在实际仿真波形里你会看到udp_ip_tx内部拿到用户数据后并不是立刻就能输出它会先构造 IP 头和 UDP 头等这些头部的准备工作完成才拉高输出端的m_axis_ip_tvalid。这个过程中数据可能短暂地“卡住”也就是tready被拉低。理解这个握手机制是读懂后续所有模块的基础因为在数据通路上几乎每个模块之间都靠 AXI-Stream 连接。还有tlast它标志一个包的结束。对 UDP 来说一个 UDP 报文对应一组tvalid/tready握手序列最后一个 beat 必须拉高tlast。如果应用层忘了拉tlast协议栈会一直等你“还有后续数据”永远拼不成一个完整报文这在调试时也是一个非常隐蔽的坑。3.3 通过仿真验证校验和一次亲手算明白UDP 的校验和计算是初学者最容易头晕的地方。它不像 IP 头校验和那样只算头部而是要对“伪首部 UDP 头 数据”整体做计算。所谓伪首部源 IP、目的 IP、协议号、UDP 长度这五个字段拼出来的 12 个字节它并不真正出现在网络上只是用来参与校验计算。verilog-ethernet 里有独立的校验和模块发送方向有udp_checksum_gen接收方向有udp_checksum_calc。你在仿真里可以看到它把数据分成 16 位一组做二进制反码求和再把结果取反得到校验和。仿真波形里虽然不好直接看到这个求和过程但你可以通过$display打印中间结果来辅助理解。这里我要提醒一个偷懒的办法UDP 校验和在 IPv4 里是可选的全 0 表示“没有计算”。很多简单的硬件 UDP 工程会直接把 UDP 校验和设为 0这时候 Wireshark 抓到包会提示“校验和为 0”或者“需要 hardware offload”。这种做法在局域网内通常能正常工作但不规范。verilog-ethernet 提供的模块是完整支持校验和计算的我不建议你为了省事去关掉它把校验和跑通才是真正理解 UDP。4. 跑到板卡上一款看得见抓得着的 UDP 回环 demo4.1 硬件顶层的最简结构仿真通过后接下来就是把协议栈放到真实板卡上。我先说说最简的硬件结构不涉及具体型号和厂商但思路是通用的PHY 芯片提供物理层接口通过 RGMII或 GMII/SGMII与 FPGA 相连FPGA 内部eth_mac_1g负责 MAC 层把 RGMII 接口和内部数据通路连接起来eth_axis_rx/eth_axis_tx把 MAC 层数据转成 AXI-Stream 格式UDP 协议栈核心与数据 FIFO 配合完成 IP/UDP 解包你自己的应用逻辑比如从拨码开关读数、从 UART 获得的数据通过 FIFO 进入协议栈。这个结构听上去模块不少但在 Vivado 里用 Block Design 连接其实很简单。放一个eth_mac_1g软核、一个eth_axis_rx、一个eth_axis_tx、一个udp_ip_rx、一个udp_ip_tx、一个arp_cache再加上几个 FIFO 转发跨时钟域的数据就构成了一个最小的 UDP 通信链路。4.2 跨时钟域是初学者最容易漏掉的问题协议栈里有一个不那么显眼、但几乎必踩的模块FIFO。为什么需要 FIFO因为协议栈内部有多个时钟域。最典型的是MAC 层跑的是 125MHz 的 GTX 时钟千兆以太网而你的应用逻辑可能跑在 100MHz 或者更低的时钟下。跨时钟域不能直接用寄存器打拍解决必须借助异步 FIFO。verilog-ethernet 仓库里的axis_fifo就是一个通用的 AXI-Stream FIFO你可以把它放在应用逻辑和 UDP 协议栈之间。它的配置项里有数据位宽、地址位宽决定深度、以及是否允许 FWFTFirst Word Fall Through。在实际工程里我习惯在发送方向和接收方向各放一个 FIFO这样即使上下游速率不匹配也不会丢数据。这里有个细节值得注意FIFO 的复位信号必须正确释放否则会漏掉第一个或者最后一个数据。不少人在仿真时没发现上板后偶尔出现丢包就是这个原因。建议在顶层设计里把 FIFO 的复位做一个异步复位同步释放处理。4.3 PC 端验证用 Wireshark 加 Python 双保险硬件通路搭好以后验证最简单的方式是让 FPGA 主动周期性地发一个固定 UDP 报文PC 端用 Wireshark 抓包。第一次看到自己的 FPGA 发出来的报文被 Wireshark 解析出来那种感觉是仿真完全无法替代的。Wireshark 里你会看到典型的输出Frame: 74 bytes on wire (592 bits) Ethernet II, Src: aa:bb:cc:dd:ee:ff, Dst: 11:22:33:44:55:66 Internet Protocol Version 4, Src: 192.168.1.10, Dst: 192.168.1.20 User Datagram Protocol, Src Port: 12345, Dst Port: 54321如果你愿意用 Python 写个接收脚本还能顺便验证数据内容是不是你预期的。发送端如果是计数器那接收端收到的数据应该是每次递增的序列。如果发现数据对不上可以先用简单的固定数比如0xAA发送验证通路没问题后再改成计数序列。很多人到这一步会觉得“已经成功了”但我想多说一句这只是打通了单方向。更常见也更有价值的是做回环测试就是 FPGA 收到 PC 发来的 UDP 报文后原封不动地再发回去。PC 端发什么就收什么这样既能验证收方向也能验证发方向。回环测试建议用固定的递增序列来做因为一旦数据错位你能立刻从递增序列的断裂处判断问题出在哪。5. 实测里我踩过的几个坑抓包、校验和、时序5.1 抓包工具看不到包先分层排查“PC 抓不到 FPGA 发的包”是所有人都会经历的第一道坎。我的排查顺序基本是固定的分享给你参考看 PHY 是否有 Link 状态也就是 PHY 芯片的链接指示 LED 或者通过 MDIO 读状态寄存器。没有 Link说明物理层就没通后面都是空谈用信号分析仪或者逻辑分析仪看 RGMII 引脚上有没有数据活动。没有活动说明 MAC 侧就没发出来问题在 FPGA 内部在协议栈内部加载几个观测点比如把udp_ip_tx的m_axis_ip_tvalid引到一个 GPIO 上看它有没有拉高。一直没拉高说明应用数据根本没送进来最后再怀疑协议栈配置比如 IP 地址、MAC 地址是不是配对。这个顺序的核心思想是“从物理层到应用层逐级排查”不要一上来就改代码。网络问题用排除法定位是最快的。5.2 校验和与总长度字段两个最容易写错的地方在校验和工作正常的情况下仍然存在一个常见问题IP 头里的总长度字段和 UDP 头里的长度字段写错。这两个字段都是 16 位总长度是 IP 头加 UDP 头加数据的总字节数UDP 长度是 UDP 头加数据的字节数。如果你的应用数据长度是固定的这两个字段还可以在模块内部自动算出但如果你在应用层手动拼过这个模块就很容易把 UDP 长度写成包含 IP 头的值导致 Wireshark 里显示“截断的 UDP 数据包”。这种 bug 在仿真里其实很不好发现因为仿真工具通常不检查长度字段哪怕你写错了它也能通过。只有到 Wireshark 里协议分析器会严格按长度字段拆包才能看出问题。所以我建议你在仿真通过后直接把抓包作为验收标准把所有字段的十六进制对照着 Wireshark 的解析结果核对一遍。5.3 RGMII 时序和复位看起来是玄学其实是规则如果你的 PHY 用的 RGMII 接口那还会遇到另一个经典问题时钟相位。RGMII 是双沿采样时钟的上升沿和下降沿各传 4 位数据。为了保证时序正确MAC 输出的时钟和 PHY 输出的时钟之间可能需要一定的相位偏移。不同 PHY 的规格各有差别通常 PHY 芯片会有 RXC 延迟控制寄存器或者 FPGA 内部的 IODELAY 可以调节。如果你在硬件上发现“Link 是好的但一个包都收不到”大概率就是 RGMII 的时序问题。我调过的板子里有一个就是必须把 PHY 的 RX clock 做 2ns 延迟才能稳定工作。这个参数不是写死在代码里的通常需要实测微调。另外就是复位。FPGA 复位策略里亚稳态是个老生常谈的问题在协议栈里尤为明显。因为协议栈内部有多个模块互相之间用 AXI-Stream 连接如果所有模块用同一个异步复位但复位释放的时刻不一致就可能出现某个模块已经开始处理数据而它下游的模块还在复位状态。最直观的现象是头几个包丢失或者偶尔出现坏包。我现在的习惯是用一个同步复位器把所有模块的复位统一成同步复位并且保证释放时钟沿一致这个问题就基本消失了。6. 零基础上手协议栈我的动作清单最后整理一下我走完这个流程后的心得。如果你也是近乎零基础开始我建议你严格按这个顺序来不要跳步先把 AXI-Stream 握手练熟这个没得商量用 iverilog 或 Vivado 跑通tb_udp_ip_tx在波形上看着数据从应用侧流到 MAC 侧打开 Wireshark对着仿真波形里的报文自己画一张“以太网头 IP 头 UDP 头 负载”的字节图理解每个字段的来源用开发板搭一个最小系统先固定发一个 UDP 包通过 Wireshark 验证字段正确再做回环测试验证收方向最后再做 ARP 请求实测拔掉网线再插上看看 FPGA 是怎么发现 ARP 表项失效并发起新请求的。这套流程走完之后你对 FPGA 上做网络通信的理解就不再是“调用某个 IP 核”而是真正有了“数据链路是分层构建”的工程直觉。接下来如果想继续深挖可以去看tcp_ip_rx/tx模块TCP 的状态机和重传逻辑又会是另一层复杂度但有了 UDP 协议栈的底子你已经能读进去那些代码了。我现在回头看当初选择 verilog-ethernet 当学习材料最大的收获其实不是“会调 UDP”这件事而是通过读源码形成了一套硬件协议栈的阅读方法先看接口再看时序然后追数据流最后才抠细节。这套方法放在任何通信协议模块上都能复用。写到这我的 part.10 也该收尾了下一块可以选择的方向还挺多可能是把 UDP 收发带宽压到极限也可能是往 TCP 方向推进看自己项目需求吧。
返回列表