简介:一套面向FPGA开发者的UDP通信回环工程,基于Quartus开发环境实现UDP报文收发、自环返回和在线验证,适合网络通信学习、接口调试及协议栈正确性测试。资源包共297个文件,压缩后约23.93MB,包含Verilog/VHDL与AHDL硬件描述语言文件、Quartus工程配置文件、C++辅助源码、可下载的sof配置比特流以及PDF设计文档;其中v、vhd、tdf文件构成收发与回环核心逻辑,qpf、qsf、qdb等文件用于工程管理、综合布局和引脚约束,cpp、h文件实现上位机或测试辅助功能,说明文档则梳理了设计思路与使用步骤。目前已有722人学习下载,工程展示了UDP报文头组装、MAC层封装、校验和计算、接收解封装以及数据回环重发等关键流程,并附带UDP摄像头数据接收示例,便于对照源码理解FPGA与以太网交互的完整链路;同时提供完整的目录结构和测试脚本,可帮助读者快速掌握FPGA实现UDP通信的方法。与纯软件模拟不同,FPGA实现还需关注时钟同步、位宽匹配和MAC接口时序,本工程在这些方面均给出可参考设计。
1. 为什么说 UDP 回环是 FPGA 通信链路里最容易“假通过”的一环
把 FPGA 的 UDP 收发做到回环,是绝大多数人验证以太网链路的第一步,但这步恰恰最容易踩空:数据确实回来了,但回来的可能不是“你发给它的那一段”,而是硬件自发自收、MAC 核没复位好、或者 VIP 测试激励里同一份数据被读了两遍。标题里的 v4 指的是 IPv4,不是某个软件版本,整条链路要处理的是 IPv4 报文和 UDP 数据报。
回环测的不是“有没有通”,而是“收进来和发出去的路径是否是同一份数据”。适合谁做?刚把三速 MAC IP 核调到不报错的 FPGA 入门者,以及要在自测板上把 UDP 通道打通再交给软件同事联调的工程师。下面这套方案是我在 Artix-7 上用双 FIFO 加一个回环状态机做完的,逻辑开销很小,但能挡住多数“看起来通了”的坑。
2. 先把回环模式定下来:L2 回帧、L3 回包还是 L4 回数据
2.1 在协议栈的哪一层做回环,决定了你要写多少逻辑
UDP 回环不等于“原样把以太网帧丢回去”。原样丢回的是 L2 回帧,MAC 地址不用改,CRC 也不用重算(MAC IP 核会自动处理),逻辑最少,但验证价值也最低:它只能证明 PHY 和 MAC 收发通路是活的,证明不了你写的 UDP 解析和封装逻辑。真正要验证的是 L4 回数据——把 UDP 载荷解出来、缓存、再重新封装成 IPv4/UDP 报文发出去,MAC 地址、IP 地址、端口、校验和全都要重填。
| 回环层级 | 做法 | 逻辑开销 | 能验证什么 | 适合场景 |
|---|---|---|---|---|
| L2 回帧 | 收帧直接转发出 MAC | 几乎为零 | PHY/MAC 通路、时钟恢复 | 首次上电点亮链路 |
| L3 回 IP 包 | 解析 IP 头后整包转发 | 低 | IP 头解析、长度字段处理 | 验证 IP 层重组 |
| L4 回 UDP 数据 | 提取载荷、重封装、回发 | 中 | UDP 头、端口过滤、校验和 | 上位机联调、业务前自测 |
我一般建议直接做到 L4,因为回环测试的目的本来就是给后续业务逻辑铺路。L2 回帧虽然能在一个小时内跑通,但等软件同事拿 UDP 工具连上来发现不通,排查成本反而更高。标题里的“udp收发”两个方向都必须覆盖:接收方向要做端口与地址过滤,发送方向要把缓存的数据按序取出来重新组包。
2.2 FPGA UDP 协议栈选型:MAC IP 核加开源栈是多数人的做法
FPGA 厂商没有免费的完整 UDP/IP 协议栈 IP 核可以用,Vivado 里的三速以太网 MAC 只解决到 MAC 层,IPv4 和 UDP 的解析需要自己写,或者集成开源 Verilog 以太网 IP 核。常见做法是 MAC 用厂商 IP,UDP 协议栈用开源方案:GitHub 上引用较多的 verilog-ethernet 项目提供了完整的 AXI-Stream 接口 UDP 栈,包含 IPv4、ARP、ICMP 回显,接收和发送路径分离,回环设计只需要把它的 rx_udp_payload 接到 tx_udp_payload。
不推荐自己从头写完整的 IPv4 分片重组,多数 UDP 业务包不会超过 MTU 1500 字节,回环测试更不需要分片。需要把握的是 UDP 数据报格式:8 字节 UDP 头(源端口、目的端口、长度、校验和)加上载荷,IPv4 头 20 字节。回环里真正要动的是 IPv4 头的源/目的 IP 互换、UDP 头的源/目的端口互换,以及重新计算首部校验和。这些字段在代码里都是固定偏移量,用状态机按字节计数处理最直接。
2.3 地址过滤怎么设:目标 MAC、目标 IP 与 UDP 端口是一组“与”关系
回环收包前必须先做过滤,否则板卡会把广播帧、ARP 请求以及发给别的主机的报文都收进来。过滤条件是 AND 关系:目的 MAC 等于本机 MAC,目的 IP 等于本机 IP,目的 UDP 端口等于本机端口,三者同时命中才认为这是送往本 UDP 应用的报文。
端口过滤的坑在字节序。UDP 头里端口是网络字节序(大端),而 FPGA 代码里经常直接用 parameter 定义一个 16 位常量,比如 16‘d5001 表示十进制 5001,在 wire 上和报文里的字节排列正好相反。没有经验的工程师常在这里调到怀疑人生。一个可靠的写法是定义端口常量时写成 16’h1389 这种十六进制,并在注释里标注端口号,或者用一个交换字节序的宏:
wire [15:0] dst_port_swapped = {rx_data[7:0], rx_data[15:8]}; if (dst_port_swapped == 16‘d5001) // 注意:这里赋的是主机序的十进制度上面的写法能避免字节序混乱。源 IP 不需要过滤,回环回发时只用保存这个 IP 值。建议把 MAC 地址、IP 地址、端口号都放在模块顶部的 parameter 里,并且只定义一套“本机参数”,后续跨板联调时只需要改这四个常量。
3. Verilog 实现一个最小 UDP 回环状态机:接收解析、缓存载荷、重新封装
3.1 回环状态机的基本骨架:IDLE、解析、转发
整个回环逻辑用三段式状态机实现,接收侧按字节流解析以太网/IPv4/UDP 头,命中过滤条件后把载荷写入缓存,帧尾(tlast)到来后启动发送路径,先重发头部,再读缓存回发载荷。下面是一个可参考的骨架,接口对齐三速 MAC IP 的 AXI-Stream 从口和主口。
module udp_loopback #( parameter [47:0] MY_MAC = 48‘h00_11_22_33_44_55, parameter [31:0] MY_IP = 32’hC0_A8_01_0A, // 192.168.1.10 parameter [15:0] MY_PORT = 16‘d5001 )( input wire clk, input wire rst_n, // 接收侧 AXI-Stream 从口 input wire [7:0] rx_tdata, input wire rx_tvalid, output wire rx_tready, input wire rx_tlast, // 发送侧 AXI-Stream 主口 output reg [7:0] tx_tdata, output reg tx_tvalid, input wire tx_tready, output reg tx_tlast ); localparam S_IDLE = 3‘d0; localparam S_PARSE = 3’d1; localparam S_RX_PAY = 3‘d2; localparam S_TX_HDR = 3’d3; localparam S_TX_PAY = 3‘d4; reg [2:0] state; reg [10:0] byte_cnt; reg [47:0] peer_mac; reg [31:0] peer_ip; reg [15:0] peer_port; reg [15:0] udp_len; reg frm_hit; // 帧接收控制:IDLE 进入解析,tlast 结束回到 IDLE always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= S_IDLE; byte_cnt <= 0; end else begin case (state) S_IDLE: if (rx_tvalid && rx_tready) begin byte_cnt <= 0; state <= S_PARSE; end S_PARSE: begin if (rx_tvalid && rx_tready) begin byte_cnt <= byte_cnt + 1; // 0-3 字节目的MAC,4-9 源MAC,12-15源IP,16-19目的IP // 20-21任意,22-23协议号,26-27源端口,28-29目的端口 if (byte_cnt == 29) begin // 所有过滤与字段采集在这里完成 if (frm_hit) state <= S_RX_PAY; else state <= S_IDLE; end end end S_RX_PAY: begin if (rx_tvalid && rx_tready) begin wr_en <= rx_tvalid; wr_data <= rx_tdata; if (rx_tlast) begin state <= S_TX_HDR; byte_cnt <= 0; end end end S_TX_HDR: // 重发头部,14 字节 MAC + 20 字节 IP + 8 字节 UDP S_TX_PAY: // 从缓存读载荷,发到 tlast endcase end end endmodule这段骨架抓住一个关键设计:解析阶段不缓存整帧,而是边收边判断,到第 30 个字节时已经能确定 5 元组过滤结果,决定继续收载荷还是直接丢帧。这样对无效帧只多花了 30 个时钟周期,不会占满缓存。frm_hit 的判定逻辑需要提前在每个字节处并行比较,用寄存器暂存已收到的部分字段,最后一个字节到达时用组合逻辑产生命中信号。
回发时头部不是存下来的原始字节,而是按格式重新填写的。以太网头部 14 字节,目的 MAC 填收到的源 MAC,源 MAC 填 MY_MAC,类型字段填 0x0800;IPv4 头 20 字节,版本/服务类型/Total Length 按捕获后的值改写,源 IP 填收到的目的 IP,目的 IP 填收到的源 IP,头部校验和需要重算;UDP 头 8 字节,源端口填收到的目的端口,目的端口填收到的源端口,长度就是 udp_len。这个重新组包过程用 ROM 查表或 case 语句按 byte_cnt 输出即可,不必引入复杂逻辑。
3.2 用双端口 RAM 做载荷缓存,收和发不抢带宽
载荷缓存建议用双端口 RAM 而不是 FIFO,原因是回环的收和发在时间上是错开的:先完整收完一帧,再开始发送。双端口 RAM 的 A 口接接收侧写,B 口接发送侧读,读写指针完全独立,不需要关心空满标志,只要在发完前不从 RX 侧新写入即可。
缓存宽度选 32 位还是 8 位取决于 MAC IP 核的接口配置。如果 AXI-Stream 从口是 8 位,就配一个 8 位宽的简单双端口 RAM,地址从 0 写到 udp_len 的大小。注意 UDP 载荷长度不一定是 4 的整数倍,发送时要用 udp_len 控制读取字节数,不能用“地址为 0 就停”的常见做法。有的开源 UDP 协议栈内部已经做了数据重组,把缓存地址和长度索引暴露出来了,这时你只需要在 IP 核的接口上做映射,连状态机都可以省掉。
回环之所以经常卡在这一步,是因为忽略了“索引”和“重组”这两个动作:UDP 载荷不是连续的字节流,它前面有固定长度的协议头,你的写地址必须从偏移量 42(14 字节 MAC + 20 字节 IP + 8 字节 UDP 头)处开始清零计数,而不是从帧头第 0 字节开始。用 byte_cnt 减去 42 作为写地址是最直观的写法,但要注意减法后的地址不要越过 RAM 深度。IP 核缓存索引重组的机制也是一样的思路:用长度字段做边界,所有数据都按偏移量落位。
3.3 单时钟域回环与反压:这里最容易被“应该没问题”带沟里
UDP 回环在自测环境里通常只有一个用户时钟,MAC 核的 RX 和 TX 都跑同一个频率,很多人因此认为不存在跨时钟域问题,时序一定收敛。这只是表象:即使时钟同源,接收路径和发送路径通过 RAM 交换数据时,依然存在读指针滞后于写指针一个周期的判断问题。
真正的坑在反压。回环状态下,RX 侧 MAC 持续送数,如果 TX 侧 tready 拉低,S_TX_PAY 状态就不能推进,但 RAM 的读指针也要停。另一个隐藏问题是 tlast 的处理:MAC 核发送时要求 tx_tlast 伴随最后一个有效字节同时出现,滞后一拍会导致上一帧的尾巴和下一帧的头粘连,而 ILA 里看单帧波形完全看不出这个错误。
处理反压的标准做法是给 RX 侧加回压:当 RAM 写满或正在回发时,把 rx_tready 拉低,让 MAC 核暂停上送新数据。三速 MAC IP 的 AXI-Stream 接口本身支持 tready 反压,但时序上要求 tready 必须在 tvalid 拉高之前至少一个周期有效,这个约束在回环状态机里容易漏。经验是:S_TX_HDR 和 S_TX_PAY 期间无条件拉低 rx_tready,保证不会出现“正在发、又收新帧”的双写冲突。
3.4 校验和与 CRC 到底抄不抄近路
以太网帧的 CRC 由 MAC IP 核在发送时自动追加,接收时自动校验,不需要用户逻辑处理,这一层可以不操心。需要决定的是 IPv4 头校验和与 UDP 校验和要不要计算。
IPv4 头校验和是必须的,很多网卡和抓包工具会丢弃或标记校验失败的 IP 包。但这个校验和只覆盖 20 字节 IPv4 头,不含数据,实现起来非常便宜:把 20 字节按 16 位累加,再加回进位,取反。UDP 校验和则有两条路可以选:RFC 768 规定 UDP 校验和可以填 0 表示不校验,很多轻量 FPGA UDP 栈为了省逻辑直接填 0,Wireshark 也不会报错;但如果上层软件对数据完整性有要求,或者要经过某些严格检查的交换机,就必须算真校验和。
回环场景的推荐是 IPv4 头校验和必算,UDP 校验和填 0。原因有两个:回环测的是连通性,不是链路误码;UDP 校验和伪包头里要拼上源 IP、目标 IP、协议号、UDP 长度,跨时钟域用 16 位累加器会增加判断逻辑,等回环通了再补也来得及。等真正做业务时再把校验和模块打开,那是一个独立于回环状态机的纯组合逻辑模块。
4. 上板验证:Vivado 连 ILA,用 Wireshark 和 iperf3 确认回环真通
4.1 最小 Vivado 工程怎么连:三速 MAC、时钟、ILA 三样就够
不要在第一次回环验证时就引入大型数据通路。最小工程包含四个部分:Clock Wizard 产生用户时钟和 MAC 参考时钟,三速以太 MAC IP 核配置 GMII/RGMII 接口,你的回环模块,以及一个 ILA 调试核。用 Vivado 自带的回环测试按键连在回环模块的复位端即可,不需要 PDMA 或 AXI 总线。
Vivado 工程里要确认的一个参数是 MAC IP 的“Support non-incrementing addresses”选项,如果你的 AXI-Stream 数据接口是连续推送的字节流,这个选项不需要开;但如果你打算后续在数据通路里做 DMA 或分组缓存,这里先选上能减少之后 IP 核配置的改动。时钟频率方面,1GbE 使用 125 MHz 用户时钟,100MbE 是 25 MHz,RGMII 的 IDELAY 在 1G 模式下必须开启,具体数值由 Vivado 根据器件自动生成,不要手动调。
工程综合布局布线通过后,先别急着烧板。把 ILA 的触发条件设为 rx_tvalid 上升沿,采样深度 1024,这样可以第一时间看到接收路径是否真的在进数。上板后按下复位按键,再查看 ILA 波形里 rx_tlast 是否周期性出现——如果 rx_tvalid 一直有效但 rx_tlast 从不出现,问题几乎都在 MAC IP 的配置上,而不是回环逻辑。
4.2 用 ILA 抓时序里最容易翻车的复位和 tlast
回环设计最容易翻车的两个信号是复位和 tlast。先看复位:MAC IP 核的复位要求保持至少若干个用户时钟周期,并且复位释放后要等待内部 PHY 配置完成。如果回环模块与 MAC IP 共用一个异步复位按键,释放时复位取消和时钟沿的相对关系不确定,会导致状态机初始化异常。规范做法是把 MAC 核的复位完成信号通过同步器打两拍后作为自己模块的复位释放条件。
tlast 的检查放在第二个通道。ILA 的触发条件设成tx_tlast == 1’b1,抓一次发送过程,重点看 tx_tvalid 与 tx_tlast 是否在同一拍:tlast 比 tvalid 晚一拍,就说明你的计数器多计了一个字节,下一帧的第一个字节会被当成上一帧的填充字节发出去。这个错误在连续回环时表现为偶发坏帧,单次测试很难复现,最好的验证方式是用后面讲的 iperf3 持续打流,观察丢包率是否稳定为 0。
ILA 的采样深度建议设置 16384,只保留 BRAM,不用分布式 RAM。回环包的 UDP 载荷如果大于几百字节,太浅的 ILA 只能抓到帧头,看不到整帧数据,无法确认载荷缓存的读写地址是否连续。
4.3 用 Wireshark 和 iperf3 做收发包比对
ILA 只能证明 RTL 内部行为正确,还要从上位机侧确认整条链路。先给开发板配置固定 IP 192.168.1.10,电脑网卡配置 192.168.1.2,用网线直连,避免交换机干扰。打开 Wireshark 抓包,过滤条件写:
udp && ip.src == 192.168.1.2 && ip.dst == 192.168.1.10这个过滤器能排除 ARP 和 LLDP 等其他广播帧。用网络调试助手向开发板 5001 端口发送一串递增数据,然后在 Wireshark 里看回包:回包的 ip.src 应该是 192.168.1.10,udp.srcport 是 5001,载荷应该和发送内容完全一致。如果只能看到请求没有回包,直接看回环模块的 ILA 波形,多数是地址过滤条件没命中,frm_hit 一直为低。
打流量用 iperf3,UDP 模式看带宽和丢包:
iperf3 -u -c 192.168.1.10 -p 5001 -b 100M -t 10 -l 1400这里-b 100M是目标速率,-l 1400是 UDP 载荷大小,把载荷设成接近 MTU 可以顺带验证长包回环。UDP 测试要同时看客户端(sender)和服务端(receiver)的输出,iperf3 的输出里 Sender 栏显示的是本机发出的速率,Receiver 栏显示的是对端实际收到的速率。因为 FPGA 回环会把包原路打回,iperf3 的 server 端在同一台电脑上会同时收到回环数据包,实际观察两组数据的一致性比只看某一端更有意义。正常回环测试应该是 0% loss,iperf3 的 jitter 列有一个非零抖动值属于正常现象,不必在意。
如果电脑没有 iperf3,也可以用 tshark 做更粗暴的统计:抓包一分钟,统计抓到的回环包数量与发送的 UDP 包数量是否一致,命令如下:
tshark -i eth0 -Y "udp.srcport == 5001 && ip.src == 192.168.1.10" -T fields -e udp.length | wc -l这条命令统计的是 FPGA 回传给电脑的 UDP 包数,和 iperf3 报告里的发送包数比对即可。
5. 回环通过后,把“环”拆掉之前要补的三件事
5.1 跨板联调时优先改的 4 个地址参数
回环代码里地址和端口都是参数化定义的,跨板联调要改的是四个常量:MY_MAC、MY_IP、MY_PORT 以及你预期对端使用的目标端口。一个常见错误是改完 MY_IP 后 ARP 缓存没清,电脑上arp -d清一下再 ping 一次。UDP 端口号建议避开 5000、8000 之类常用端口,选一个不容易被通信库默认占用的高位端口,比如 37000 附近。
5.2 加一个 CRC 错误计数,防止“假通过”
回环测试看着包全回来了,但可能是 MAC 核收了坏帧后仍然向上抛。给回环模块加两个计数器:rx_frame_cnt 在每收到一个完整帧且 tlast 出现时加一,rx_crc_err_cnt 在 MAC 核输出的 rx_error 信号拉高时加一。这两个计数器的值读到 ILA 里比对,如果 CRC 错误帧占比超过万分之一,说明物理链路或时钟有问题。这个习惯从回环阶段就建立,后面接业务时才能快速定位问题在哪一段。CAN 通信里经常出现“数据帧回环测试没问题但标准模式无法发送”的现象,本质就是只验证了本地收发路径,没有验证对端确认与链路仲裁,UDP 回环同理,别把“自己收到自己发的帧”等同于“对端能收到”。
5.3 换个带背压的数据通路,才能从回环走向真实吞吐
回环状态机验证完协议栈之后,要回发的数据不再来自 RX 侧,而是来自你的业务模块(图像、采集数据或 AXI DMA),背压方向就反过来了:TX 侧 MAC 核的 tready 决定业务模块能不能继续送数据。一个直接的方案是把双端口 RAM 换成异步 FIFO,写时钟用业务时钟,读时钟用 MAC 用户时钟,FIFO 的 prog_full 信号作为业务模块的暂停信号。到这里,UDP 收发链路的骨架已经完整了,剩下的就是填充业务逻辑,再回头去补 UDP 校验和与分片重组。
本文还有配套的精品资源,点击获取