1.总体设计
1.1 电路功能与性能
本设计实现的核心功能为基于以太网的 UDP 数据收发与回环,并支持回包 payload 追加固定字符串。具体功能与性能指标如下:
1)网络通信功能
- FPGA 通过以太网 PHY 与 PC 直连,PC 端发送 UDP 数据报到指定 IP/端口;
- FPGA 完成以太网帧、IP、UDP 基本解析,获取 UDP payload 字节流;
- FPGA 将接收到的payload 作为回包内容的一部分,并在末尾追加"换行 +字符串 nihaoya,dongwazi~";
- FPGA 构造并发送 UDP 回包,PC 端可收到回显结果。
2)数据追加功能(本课设新增,学习正点原子例程:58_eth_udp_loop,60_ov5640_udp_pc修改源码设计)
- 在回包 payload 的"原始数据末尾"插入换行符 0x0A;
- 追加 ASCII 字符串:回包总追加长度为 18 字节;
- 回包 UDP 长度字段与发送字节计数随追加长度同步更新,保证接收端显示无多余/缺失字符。
1.2 关键功能电路设计
总体结构按"PHY 接口层---协议处理层---数据缓存层---追加发送层"组织:
(1)PHY 与 MAC 接口
- 通过 RGMII 接口与外部以太网 PHY 连接,完成收发数据与控制信号交互;
- 通过 MDC/MDIO 对 PHY 进行必要的管理配置(如自协商/基本寄存器配置等,具体视例程实现而定);
- PHY 复位信号 eth_rst_n 由系统复位派生,确保上电初始化稳定。
(2)协议处理层(ARP/IP/UDP)
- ARP:用于学习并获得对端 MAC,保证回包能够正确寻址;
- UDP 接收:对接收到的 UDP 报文进行目的地址/端口匹配,提取 payload 字节流,形成 udp_rec_en/udp_rec_data 等字节级有效信号;
- UDP 发送:在 tx_start_en 触发后,根据 tx_byte_num 指定的 payload 长度,通过 tx_req 请求 FIFO 数据并完成以太网/IP/UDP 头部与 CRC 等字段发送。
(3)数据缓存层(FIFO)
- 接收侧:当 udp_rec_en 有效时,将 udp_rec_data 写入 FIFO;
- 发送侧:当 udp_tx 发出 tx_req 时,从 FIFO 读出一个字节作为 tx_data;
- FIFO 的作用是隔离接收/发送两端时序,避免"边接收边发送"导致的最后字节错位或丢失。
(4)追加发送层(本课设新增模块,工程已规范化封装)
追加发送模块的功能是将"原始 payload"与"追加内容"在 FIFO 写入侧完成拼接,并输出正确的发送触发与长度:
- 原始 payload 到来时:直通写 FIFO;
- 报文结束(rec_pkt_done)时:自动补写 0x0A 与固定字符串"nihaoya,dongwazi~"到 FIFO;
- 同步产生 tx_start_en(发送触发)与 tx_byte_num = 原始字节数 + 追加字节数;
- 通过有限状态机对追加字节逐个写入,保证 FIFO 写入顺序与长度完全一致。
1.3 电路功能框图
说明:协议栈部分复用正点原子例程,仅调整一点udp库udp_tx模块,添加了模块udp_payload_append.v,实现在udp_loop回环实验基础上追加发送字符串,修改了顶层模块
1.4 板级验证结果(PC + 开发板)
- 网络环境:PC 与开发板直连或同一局域网,配置 PC 与开发板 IP 为同网段;
- 使用网络调试助手发送 UDP ASCII 字符串(多组长度),观察回包是否为"原字符串 + 换行 + nihaoya,dongwazi~";
- 多次连续发送,验证无丢包、无重复字节、追加内容长度稳定;
- 若出现边界字节异常,使用 SignalTap 抓取 rec_pkt_done、udp_rec_en/data、FIFO 写使能/写数据、tx_req 等关键波形定位问题。

5.问题
(1)

-
回包里少了你发的最后 1 个字符 (比如
dongwazi变成dongwaz□,123456789变成12345678) -
追加的那两个字节里有不可打印字节: 末尾出现
□44
说明:
-
□:一般代表不可打印 ASCII(比如 0x12) -
如果你不想看见方块,把你追加的变量改成可打印字符,比如:
-
8'h41='A' -
8'h42='B' -
下面版本一测试追加两固定字符'A''B'
-
(2)少了最后 1 个原始字符 + 变成 ABB(多 1 个 B)

本质:FIFO 里实际只写进去了 N+1 个字节(丢了原 payload 的最后 1 字节),但 UDP_TX 还按 N+2 去发,于是最后 1 个字节没数据可读 → FIFO 输出保持上一拍的 0x42('B') → 你看到 多出来一个 B。
(3)解决:这里只说明(2),(1)在eth_ctrl 模块说明
关键 bug:你写 FIFO 用的是 rec_en/rec_data,但包结束用的是 rec_pkt_done;其中rec_en/rec_data来自eth_ctrl 模块(协议处理与发送仲裁的核心控制模块),rec_pkt_done来自udp_rx模块
很多版本的 eth_ctrl 会把 udp_rec_en/udp_rec_data 再打一拍(或者做选择/整形),这就导致:
-
当
rec_pkt_done到了,你这边看到的rec_data还停留在 倒数第 2 个字节 -
你立刻进
ADD0/ADD1开始写 A/B -
结果:最后 1 个 payload 字节没被写进去 → 发送时就变成
... + A + B + (B重复一次)
这正好解释你看到的:
123456 -> 12345ABB(少了 6,多了一个 B)

udp_rec_data被打一拍了。

2.模块设计
2.1 以太网 PHY 接口模块设计(RGMII ↔ GMII)
本设计采用外部 PHY 芯片提供的 RGMII 接口,FPGA 内部通过接口转换模块完成 RGMII(4bit DDR) 与 GMII(8bit SDR) 的互换,从而将后续协议栈处理统一成"8bit 数据 + 有效使能"的形式,便于解析与组帧。模块接口框图如下:

(1)RGMII接收:rgmii_rx模块:
将 GMII 8bit SDR 发送数据转换为 RGMII 4bit DDR 形式输出到 PHY,同时输出 RGMII 发送时钟与发送控制信号。rgmii_tx 模块接口信号表如下:

(2)RGMII发送:rgmii_tx u_rgmii_tx
从 PHY 侧接收 RGMII 4bit DDR 数据与控制信号,恢复为 GMII 8bit SDR 接收数据与数据有效信号,并输出内部 GMII 接收时钟。rgmii_rx 模块接口信号表如下:

2.2 协议处理与帧仲裁模块设计(eth_ctrl + udp_rx)
本课设以以太网帧为基本处理对象,完成 ARP 学习/应答、UDP 数据接收与回包发送等功能。其中,udp_rx 模块负责把"以太网帧中的 UDP payload"提取为字节流接口;eth_ctrl 模块负责把多协议接收结果进行统一输出,并在多发送源之间进行仲裁与 GMII 发送复用。两者共同构成协议处理与帧仲裁的核心通路:
向上:输出标准化的 payload 字节流接口 rec_en/rec_data,以及包结束指示 rec_pkt_done、长度 rec_byte_num、对端地址 src_mac/src_ip 等关键控制信息,为"FIFO缓存/追加发送"模块提供输入;
向下:输出统一的 GMII 发送字节流接口 gmii_tx_en/gmii_txd,并对 UDP 发送侧的读数请求 udp_tx_req 与缓存读出数据 tx_data 做握手转接,确保"请求---取数---封装发送"严格同步。
2.3 协议处理与帧仲裁模块设计(eth_ctrl + udp_rx)
2.3.1 udp_rx 模块功能与接口
udp_rx 的核心任务是:在接收侧完成 IP/UDP 头部解析,筛选目的为本机的数据报,并将 UDP payload 以字节流形式输出。UDP 接收侧关键接口信号说明( udp_rx 输出,供上层使用)

其逻辑可概括为"识别---定位---吐数---结束"四个阶段:
1)协议识别与过滤
udp_rx 对输入以太网帧进行类型字段解析:当以太网类型为 IPv4 且 IP 协议字段为 UDP 时,判定为 UDP 报文;同时对目的 IP/端口进行匹配,仅对"目的为本机"的 UDP 报文进入 payload 输出阶段。
2)payload 定位与长度确定
udp_rx 解析 UDP 首部的长度字段,计算得到 payload 长度,该长度最终通过rec_byte_num[15:0] 输出,为发送侧长度修正提供基准。
3)payload 字节流输出接口
在 payload 有效区间内,udp_rx 每接收一个 payload 字节,就产生:
- udp_rec_en = 1(表示本拍输出字节有效)
- udp_rec_data[7:0] = 当前 payload 字节
同时内部对字节数进行累加,形成最终 rec_byte_num。
4)包结束与源地址信息输出
当最后一个 payload 字节输出结束后,udp_rx 产生单周期结束指示 rec_pkt_done ,作为上层"锁存长度/地址、追加、触发发送"的关键时序基准;同时地输出对端址信息 src_mac[47:0] 与 src_ip[31:0],供回包时作为目的地址使用。
总结:udp_rx 将复杂的 UDP 报文解析"抽象"为统一的字节流接口 udp_rec_en/udp_rec_data + 结束指示 rec_pkt_done + 元信息 rec_byte_num/src_mac/src_ip,使上层模块不需要关心协议细节即可完成缓存与回包。
2.3.2 eth_ctrl 模块功能与接口
eth_ctrl 是协议处理与发送仲裁的核心控制模块,承担两类关键职责:接收侧分流统一与发送侧仲裁复用。
(1)接收侧:分流统一与标准接口输出
由于工程中可能同时存在 ARP、ICMP、UDP 等多类接收结果,eth_ctrl 在接收侧的目标是把"不同协议模块的输出"统一映射为一套上层可用的标准接口。以 UDP 为例:
- eth_ctrl 接收来自 udp_rx 的 udp_rec_en/udp_rec_data,并在 UDP 有效时将其转换/转发为统一输出 rec_en/rec_data;
- eth_ctrl 同时转发 rec_pkt_done、rec_byte_num、src_mac/src_ip 等控制信息,确保上层模块在 rec_pkt_done 到来时能够可靠锁存"长度与回包地址"。
这种"统一输出"的设计使 FIFO 写入与追加发送模块只需要对接 rec_en/rec_data/rec_pkt_done,无需区分底层来自哪类协议。
(2)发送侧:多源仲裁与 GMII 发送复用
发送侧可能存在多个发送源,例如:ARP 应答发送、UDP 回包发送等。eth_ctrl 通过内部仲裁逻辑确保"同一时刻只有一个协议源占用 GMII 发送通道"。仲裁输出的结果体现在:
- 统一的 GMII 发送接口 gmii_tx_en/gmii_txd 由 eth_ctrl 输出;
- 当仲裁选择 UDP 作为当前发送源时,eth_ctrl 进入 UDP 发送通道占用状态,直到 udp_tx_done 置位后释放通道,回到空闲等待。

(3)UDP 发送侧握手转接:保证取数与封装严格同步
UDP 发送模块(udp_tx)在发送 payload 时会产生"需要下一个字节"的请求 udp_tx_req。为了让 FIFO 的读出节拍与 udp_tx 的封装节拍严格一致,eth_ctrl 负责完成如下转接关系:
将 udp_tx 的请求信号转接为上层 FIFO 读请求:udp_tx_req → tx_req;

即组合逻辑udp_tx_data 和tx_data不延迟1clk,udp_tx_data , udp_tx_req_d0 , tx_data 同拍;其中 udp_tx_req_d0为udp_tx_req打一拍。
时序对齐 :FIFO 读请求(tx_req)和 FIFO 输出(tx_data)之间有 1 拍延迟,udp_tx_req_d0 刚好匹配这个延迟,让 udp_tx_data 能准确拿到 FIFO 的有效数据;

在 FIFO 响应读请求输出 tx_data[7:0] 后,eth_ctrl 将其转发给 udp_tx:tx_data → udp_tx_data;
从而形成"udp_tx_req 发起 → FIFO 读出 tx_data → udp_tx_data 输入封装"的闭环握手链路,避免出现读早/读晚导致的数据错位、重复或丢失。
总结:eth_ctrl 一方面把接收侧"协议差异"屏蔽掉,向上提供统一的 rec_* 接口;另一方面把发送侧"多个发送源"统一仲裁并复用到 gmii_tx_*,同时在 UDP 发送过程中完成 udp_tx_req 与 FIFO 读数的握手对齐。
波形图:

(4)问题:为什么看到追加 □44

你追加的是:
-
var0 = 8'h12(ASCII 里是不可显示控制码)→ NetAssist 显示成□ -
var1 = 8'h34(ASCII 就是字符'4')
正常情况追加两字节,你应该看到:... □4,但你现在看到:... □44,这意味着 最后一个字节 0x34 被发送了两次(重复了最后一字节)。
真正的问题:tx_req 提前停了 1 个字节
在你工程的 rtl/udp/udp_tx.v 的 st_tx_data 里,除了你截图那段 data_cnt,下面还有一段类似:
if(data_cnt == tx_data_num - 16'd2) tx_req <= 1'b0;
这句太早了。
还是波形时序:最后一字节重复 
2.3.3 接收---缓存---发送在 eth_ctrl 主导下的时序关系
在一帧 UDP 回环场景中,系统行为可描述为如下闭环:
当 udp_rx 识别到目的为本机的 UDP 报文后,进入 payload 输出阶段,并以 udp_rec_en/udp_rec_data 逐字节输出有效数据。eth_ctrl 在接收侧对该字节流进行统一转发,形成 rec_en/rec_data ,从而驱动上层缓存模块按字节写入 FIFO。
进入发送阶段后,eth_ctrl 通过仲裁选择 UDP 发送源占用 GMII 通道。udp_tx 在发送 payload 时产生 udp_tx_req,eth_ctrl 将其转接为 FIFO 读请求 tx_req,并把 FIFO 读出的 tx_data 及时转发为 udp_tx_data,从而保证发送端封装的每个 payload 字节均来自 FIFO 的正确顺序数据。待 udp_tx 发出 udp_tx_done 后,eth_ctrl 释放发送通道并回到空闲状态,等待下一帧接收与回包。

2.4 UDP发送模块设计(udp_tx)
udp_tx 模块完成 UDP 报文的封装与发送,按"以太网头 + IP头 + UDP头 + payload + CRC"时序在 GMII 口输出。payload 数据本工程来自 FIFO,通过 tx_req/tx_data 握手获取。
本模块的接口信号表如下:

工作流程与时序说明
(1)tx_start_en 到来后,udp_tx 锁存目的地址与 payload 长度,进入组帧状态;
(2)按协议字段顺序依次输出 MAC/IP/UDP 头部;
(3)进入 payload 发送阶段:udp_tx 拉高 tx_req 请求数据;上层将 FIFO 数据送入 tx_data;
(4)udp_tx 在 gmii_tx_en 有效周期输出 tx_data 到 gmii_txd,并同步更新 CRC;
(5)payload 发送结束后输出 CRC 字节,最后产生 tx_done 脉冲返回空闲。

2.5 缓存与 Payload 追加模块设计( FIFO + udp_payload_append )
为保证 UDP 回环发送过程不丢字节、不重复字节、字节顺序不乱,本设计在"接收侧输出 payload 字节流"与"发送侧按需取数封装帧"之间引入 FIFO 缓存,并在 FIFO 写入端增加 udp_payload_append 追加模块。整体思路是:
- 接收阶段:由 eth_ctrl 统一输出 rec_en/rec_data,udp_payload_append 在接收时钟域把 payload 逐字节写入 FIFO;
- 帧结束阶段:在 rec_pkt_done 到来时,udp_payload_append 锁存元信息(如 rec_byte_num、src_mac、src_ip),并计算修正后的发送长度 tx_byte_num;
- 追加阶段:将预设追加字符串(本工程为 "\nnihaoya,dongwazi~",共 17 字节)按字节序继续写入 FIFO;
- 启动阶段:追加写入完成后,输出一个 tx_start_en 启动脉冲,通知发送侧开始读取 FIFO 并完成 UDP 回包封装发送。
通过该结构,接收端"吐字节"的节拍与发送端"取字节"的节拍被 FIFO 隔离,发送侧只需依据 tx_req 请求节拍读取 FIFO,即可严格按顺序拿到 原 payload + 追加字符串 的完整数据流,避免因时序对齐问题造成的错位/重读。
2.5.1 udp_payload_append 模块接口与功能说明
udp_payload_append 位于接收输出与 FIFO 写入端之间,工作在接收时钟域(通常为 gmii_rx_clk),
- 接收写入阶段:当 rec_en=1 时,模块在每个时钟周期产生 wr_fifo_en=1,并令 wr_fifo_data=rec_data,实现对 payload 的逐字节缓存写入。
- 帧结束锁存阶段:当检测到 rec_pkt_done 有效时,模块立即锁存 rec_byte_num、src_mac、src_ip,并计算发送总长度 tx_byte_num = rec_byte_num + APPEND_LEN(本工程 APPEND_LEN=17)。
- 追加写入阶段:模块进入追加状态,继续拉高 wr_fifo_en,将追加字符串按字节序写入 FIFO(先写换行 0x0A,再写 "nihaoya,dongwazi~" 的 ASCII 字节序列)。
- 发送触发阶段:最后一个追加字节写入 FIFO 完成后,模块在确保写入完成的时序条件下产生一个 tx_start_en 脉冲,作为发送侧启动依据,避免出现"请求节拍早到导致末字节漏发/重发"的问题。
2.5.2 FIFO 缓存接口与收发解耦说明
FIFO 作为收发解耦缓冲,数据宽度为 8 bit。其接口从系统功能上分为写端与读端:
写端(接收时钟域)作用:接收阶段写入"原 payload",追加阶段继续写入"追加字符串",两者在 FIFO 内按时间顺序自然拼接。
读端(发送时钟域)作用:发送侧严格遵循 tx_req 的节拍"要一个字节读一个字节",确保封装发送与数据读取一一对应。
因此,FIFO 使系统形成稳定的"接收写入(rec_en)---缓存(FIFO)---发送按需读取(tx_req)"闭环,避免接收与发送在不同节拍下直接硬对接造成的数据错位问题。该模块接口如下:

电路模块示意图:

3.代码输入与仿真
3.1 仿真与测试的功能列表
功能验证分为两部分:
(1)RTL 功能仿真(ModelSim ) :构造接收侧激励,重点验证新增"追加发送"模块在各种时序关系下的行为是否正确(本章内容);
(2)板级下载与联机测试:下载 SOF 后通过网络调试助手进行收发验证(在第4章中结合上位机现象与抓包/波形进行说明)。
RTL 仿真重点检查的功能如下:
1)复位功能:异步复位有效期间,内部寄存器清零/初始化,写 FIFO 与发送触发均不应产生误动作;
2)payload 透传写入 FIFO:rec_en 有效的每一拍,wr_fifo_en 应同步有效,wr_fifo_data 与 rec_data 严格一致,不丢字节、不重复字节;
3)接收完成锁存:rec_pkt_done 到来时应正确锁存 rec_byte_num、src_mac、src_ip,为后续回包目的地址与发送长度计算提供依据;
4)追加字符串正确性:在 payload 末尾应按字节顺序追加换行符与字符串"nihaoya,dongwazi~"(ASCII),追加字节数与内容正确;
5)发送长度修正:tx_byte_num 应等于原 payload 长度 + 追加长度(含换行符),且在启动发送前稳定有效;
6)发送触发时序:tx_start_en 必须在"最后一个追加字节写入 FIFO 完成后"再产生,保证下游 UDP 发送端按 tx_req 取数时不会读空或读到旧数据;
7)回包地址正确性:des_mac/des_ip 应由 src_mac/src_ip 锁存得到,确保回包能发送到对端。
3.2 仿真平台构建
由于完整以太网链路涉及 PHY、GMII/RGMII 时序与多层协议栈,直接对全链路做激励成本较高且不利于定位新增逻辑问题。本课设采用模块级仿真思路:对新增模块 udp_payload_append 搭建独立 testbench,通过"模拟 udp_rx 输出的标准接口信号"来验证追加逻辑与发送触发逻辑的正确性。
仿真平台构成如下:
1)时钟与复位 :在 testbench 内产生 gmii_rx_clk(用于模拟接收侧写 FIFO 的时钟域),并产生 sys_rst_n 异步复位信号;
2)输入激励构造 :按字节依次拉高 rec_en 并送入 rec_data,模拟接收 payload(例如"dongwazi"),随后拉高 rec_pkt_done 表示本帧接收结束,同时给定 rec_byte_num、src_mac、src_ip;
3)观测信号:重点观测 wr_fifo_en/wr_fifo_data(写 FIFO 行为)、tx_start_en(发送触发)、tx_byte_num(修正后的长度)、des_mac/des_ip(回包目的地址锁存结果),以及内部追加过程计数信号(如 append_idx/out_cnt 等,用于辅助分析)。
为简化操作,本课设提供 ModelSim 执行脚本(.do 文件),脚本内包含 vlib/vlog/vsim 等命令,可一键完成"编译 DUT + 编译 TB + 启动仿真 + 添加关键波形"。因此在使用脚本方式仿真时,无需在 GUI 中手动逐个 Add File,只需在 Transcript 中执行:
- cd ./sim
- do modelsim_run_udp_payload_append_v2.do
即可完成仿真环境搭建与波形加载。


3.3 代码学习
`timescale 1ns/1ps
//==============================
// Testbench: udp_payload_append
// 目标:验证"回环 payload + 追加(\n + nihaoya,dongwazi~)"
//==============================
module udp_payload_append_tb;
// ---- clock/reset ----
reg gmii_rx_clk;
reg sys_rst_n;
// ---- DUT inputs ----
reg rec_en;
reg [7:0] rec_data;
reg rec_pkt_done;
reg [15:0] rec_byte_num;
reg [47:0] src_mac;
reg [31:0] src_ip;
// ---- DUT outputs ----
wire wr_fifo_en;
wire [7:0] wr_fifo_data;
wire tx_start_en;
wire [15:0] tx_byte_num;
wire [47:0] des_mac;
wire [31:0] des_ip;
// ---- Instantiate DUT ----
udp_payload_append dut(
.clk (gmii_rx_clk),
.rst_n (sys_rst_n),
.rec_en (rec_en),
.rec_data (rec_data),
.rec_pkt_done (rec_pkt_done),
.rec_byte_num (rec_byte_num),
.src_mac (src_mac),
.src_ip (src_ip),
.wr_fifo_en (wr_fifo_en),
.wr_fifo_data (wr_fifo_data),
.tx_start_en (tx_start_en),
.tx_byte_num (tx_byte_num),
.des_mac (des_mac),
.des_ip (des_ip)
);
// ---- 125MHz-ish clock (8ns period) ----
initial begin
gmii_rx_clk = 1'b0;
forever #4 gmii_rx_clk = ~gmii_rx_clk;
end
// ---- helpers ----
integer i;
integer out_cnt;
reg [7:0] out_bytes [0:255];
task send_byte(input [7:0] b);
begin
@(posedge gmii_rx_clk);
rec_en <= 1'b1;
rec_data <= b;
@(posedge gmii_rx_clk);
rec_en <= 1'b0;
rec_data <= 8'h00;
end
endtask
// 发送一个 payload(len字节),最后打一拍 rec_pkt_done
task send_payload;
input integer len;
input [8*128-1:0] str; // 最大 128 字符,够用
reg [7:0] ch;
begin
rec_byte_num <= len[15:0];
// payload 字节
for(i=0; i<len; i=i+1) begin
ch = str[8*(128-1-i) +: 8];
send_byte(ch);
end
// packet done 脉冲
@(posedge gmii_rx_clk);
rec_pkt_done <= 1'b1;
@(posedge gmii_rx_clk);
rec_pkt_done <= 1'b0;
// 等待 DUT 把追加字节全写完 + 拉起 tx_start_en
//(一般几十拍内结束,保险等 60 拍)
repeat(60) @(posedge gmii_rx_clk);
end
endtask
// 收集 wr_fifo 写入的数据
always @(posedge gmii_rx_clk) begin
if(!sys_rst_n) begin
out_cnt <= 0;
end else if(wr_fifo_en) begin
out_bytes[out_cnt] <= wr_fifo_data;
out_cnt <= out_cnt + 1;
end
end
// ---- main ----
initial begin
// init
rec_en = 1'b0;
rec_data = 8'h00;
rec_pkt_done = 1'b0;
rec_byte_num = 16'd0;
src_mac = 48'h00_11_22_33_44_55;
src_ip = 32'hC0A8_0166; // 192.168.1.102
sys_rst_n = 1'b0;
repeat(5) @(posedge gmii_rx_clk);
sys_rst_n = 1'b1;
repeat(5) @(posedge gmii_rx_clk);
// CASE1: "dongwazi" (8字节)
out_cnt = 0;
send_payload(8, {"dongwazi", {120{8'h00}}});
$display("CASE1 tx_byte_num=%0d (expect 26 = 8 + 18)", tx_byte_num);
$display("CASE1 tx_start_en pulse seen? (check waveform)");
// 打印前 26 个输出字节(payload + 0A + nihaoya,dongwazi~)
$write("CASE1 out_bytes: ");
for(i=0; i<26; i=i+1) begin
$write("%02h ", out_bytes[i]);
end
$write("\n");
// CASE2: "123456789" (9字节)
out_cnt = 0;
send_payload(9, {"123456789", {119{8'h00}}});
$display("CASE2 tx_byte_num=%0d (expect 27 = 9 + 18)", tx_byte_num);
$write("CASE2 out_bytes: ");
for(i=0; i<27; i=i+1) begin
$write("%02h ", out_bytes[i]);
end
$write("\n");
$display("TB done.");
#100;
$stop;
end
endmodule
- 设计目标
验证 udp_payload_append 模块(DUT)的核心功能:
- 接收 UDP payload 数据(
rec_en/rec_data); - 在原 payload 后追加固定字符串 :
\n(0x0A) +nihaoya,dongwazi~(共 18 字节); - 输出正确的:
- 写 FIFO 数据(
wr_fifo_en/wr_fifo_data):包含 "原 payload + 追加内容"; - 发送控制信号(
tx_start_en):数据处理完成后触发发送; - 总字节数(
tx_byte_num):原长度 + 18; - 回环地址(
des_mac/des_ip):等于输入的src_mac/src_ip。
- 写 FIFO 数据(
- 辅助变量与复用任务(核心复用逻辑)
关键:
out_bytes数组:存储 DUT 输出的wr_fifo_data,8 位宽匹配字节粒度,256 长度覆盖测试场景;send_byte任务:封装单字节发送时序,确保每个字节在时钟上升沿发送,符合 DUT 采样规则;send_payload任务:str[8*(128-1-i) +: 8]:Verilog 位切片语法,从 128 字符的大端字符串中提取第 i 个字节;rec_pkt_done脉冲:1 拍宽,符合 FPGA "脉冲型控制信号" 设计规范;repeat(60):等待 DUT 完成 "追加字符串→写 FIFO→触发 tx_start_en",避免读取未处理完的数据。
- 输出数据收集(实时监测 DUT 输出)

- 主测试流程
-
初始化:所有输入信号置默认值(避免仿真初期出现 X 态),
src_mac/src_ip设固定值(便于验证回环); -
复位流程:
sys_rst_n拉低 5 拍(确保 DUT 完全复位),再释放并稳定 5 拍,符合 FPGA 复位规范; -
测试用例:
-
CASE1:8 字节 payload,预期总长度 26=8+18;
-
CASE2:9 字节 payload,预期总长度 27=9+18;
-
每个用例前
out_cnt=0,避免不同用例数据混叠;
-
-
结果验证:
-
$display:打印tx_byte_num,验证总长度是否正确; -
$write:打印out_bytes,验证 "原 payload + 追加内容" 是否正确; -
tx_start_en:提示查看波形(脉冲信号需看时序图确认);
-
-
$stop:仿真完成后停止,避免无限循环。
3.3仿真结果


就图2可见,在 rec_pkt_done 到来后,模块首先锁存源地址信息(src_mac/src_ip=>desmac/ip)并启动追加过程。追加数据在 wr_fifo_data 上依次输出:
- 0Ah(换行符),
- 6Eh/69h/68h/61h/6Fh/79h/61h/2Ch/64h/6Fh/6Eh/67h/77h/61h/7Ah/69h/7Eh(对应"nihaoya,dongwazi~")。
同时 tx_byte_num 由原始长度 0008h 修正为 001Ah(8 + 1 + 17 = 26),说明"长度修正 = 原 payload + 换行 + 追加字符串"的计算逻辑正确。该结果表明新增追加模块能够在不改变原 payload 内容的前提下,正确完成尾部追加与发送长度更新。
4.设计的FPGA实现及板级测试

这是之前调试整的signaltap的波形:
你这张 SignalTap 波形其实已经把"为啥多了个 B / 为啥多了 69h"这俩问题都解释清楚了,结论是:
- 你现在的工程里存在一个"数据打一拍(1clk 延迟)"的坑:你写 FIFO 用的那个
rec_data不是"当前拍的 udp_rec_data",而是被寄存过的上一拍数据,所以包尾最容易错位,最终表现成"丢最后字节 / 最后字节重复 / 追加字节错位"。
波形:你抓到的 wr_fifo_data 末尾是:
-
69h=0x69= 'i' -
41h=0x41= 'A' -
42h=0x42= 'B'
也就是说 FIFO 里最后这段实际上是:i A B
即:你原来的字符串本来就有个 i(比如 "dongwazi" 的最后一位),然后你又追加了 A、B 两个字节。
在正点原子的工程基础上修改过的文件链接:通过网盘分享的文件:UDP_4.0.zip
链接: https://pan.baidu.com/s/1EEeW1pDV8nprJfuHEGhFSA 提取码: 1234
