FPGA学习笔记(9)以太网UDP数据报文发送电路设计(三)

1.总体设计

1.1 电路功能与性能

本设计实现的核心功能为基于以太网的 UDP 数据收发与回环,并支持回包 payload 追加固定字符串。具体功能与性能指标如下:

1)网络通信功能

  1. FPGA 通过以太网 PHY 与 PC 直连,PC 端发送 UDP 数据报到指定 IP/端口;
  2. FPGA 完成以太网帧、IP、UDP 基本解析,获取 UDP payload 字节流;
  3. FPGA 将接收到的payload 作为回包内容的一部分,并在末尾追加"换行 +字符串 nihaoya,dongwazi~";
  4. FPGA 构造并发送 UDP 回包,PC 端可收到回显结果。

2)数据追加功能(本课设新增,学习正点原子例程:58_eth_udp_loop,60_ov5640_udp_pc修改源码设计)

  1. 在回包 payload 的"原始数据末尾"插入换行符 0x0A;
  2. 追加 ASCII 字符串:回包总追加长度为 18 字节;
  3. 回包 UDP 长度字段与发送字节计数随追加长度同步更新,保证接收端显示无多余/缺失字符。

1.2 关键功能电路设计

总体结构按"PHY 接口层---协议处理层---数据缓存层---追加发送层"组织:

(1)PHY 与 MAC 接口

  1. 通过 RGMII 接口与外部以太网 PHY 连接,完成收发数据与控制信号交互;
  2. 通过 MDC/MDIO 对 PHY 进行必要的管理配置(如自协商/基本寄存器配置等,具体视例程实现而定);
  3. PHY 复位信号 eth_rst_n 由系统复位派生,确保上电初始化稳定。

(2)协议处理层(ARP/IP/UDP)

  1. ARP:用于学习并获得对端 MAC,保证回包能够正确寻址;
  2. UDP 接收:对接收到的 UDP 报文进行目的地址/端口匹配,提取 payload 字节流,形成 udp_rec_en/udp_rec_data 等字节级有效信号;
  3. UDP 发送:在 tx_start_en 触发后,根据 tx_byte_num 指定的 payload 长度,通过 tx_req 请求 FIFO 数据并完成以太网/IP/UDP 头部与 CRC 等字段发送。

(3)数据缓存层(FIFO)

  1. 接收侧:当 udp_rec_en 有效时,将 udp_rec_data 写入 FIFO;
  2. 发送侧:当 udp_tx 发出 tx_req 时,从 FIFO 读出一个字节作为 tx_data;
  3. FIFO 的作用是隔离接收/发送两端时序,避免"边接收边发送"导致的最后字节错位或丢失。

(4)追加发送层(本课设新增模块,工程已规范化封装)

追加发送模块的功能是将"原始 payload"与"追加内容"在 FIFO 写入侧完成拼接,并输出正确的发送触发与长度:

  1. 原始 payload 到来时:直通写 FIFO;
  2. 报文结束(rec_pkt_done)时:自动补写 0x0A 与固定字符串"nihaoya,dongwazi~"到 FIFO;
  3. 同步产生 tx_start_en(发送触发)与 tx_byte_num = 原始字节数 + 追加字节数;
  4. 通过有限状态机对追加字节逐个写入,保证 FIFO 写入顺序与长度完全一致。

1.3 电路功能框图

说明:协议栈部分复用正点原子例程,仅调整一点udp库udp_tx模块,添加了模块udp_payload_append.v,实现在udp_loop回环实验基础上追加发送字符串,修改了顶层模块

1.4 板级验证结果(PC + 开发板)

  1. 网络环境:PC 与开发板直连或同一局域网,配置 PC 与开发板 IP 为同网段;
  2. 使用网络调试助手发送 UDP ASCII 字符串(多组长度),观察回包是否为"原字符串 + 换行 + nihaoya,dongwazi~";
  3. 多次连续发送,验证无丢包、无重复字节、追加内容长度稳定;
  4. 若出现边界字节异常,使用 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 字节,就产生:

  1. udp_rec_en = 1(表示本拍输出字节有效)
  2. 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 为例:

  1. eth_ctrl 接收来自 udp_rx 的 udp_rec_en/udp_rec_data,并在 UDP 有效时将其转换/转发为统一输出 rec_en/rec_data;
  2. 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 发送通道"。仲裁输出的结果体现在:

  1. 统一的 GMII 发送接口 gmii_tx_en/gmii_txd 由 eth_ctrl 输出;
  2. 当仲裁选择 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.vst_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 追加模块。整体思路是:

  1. 接收阶段:由 eth_ctrl 统一输出 rec_en/rec_data,udp_payload_append 在接收时钟域把 payload 逐字节写入 FIFO;
  2. 帧结束阶段:在 rec_pkt_done 到来时,udp_payload_append 锁存元信息(如 rec_byte_num、src_mac、src_ip),并计算修正后的发送长度 tx_byte_num;
  3. 追加阶段:将预设追加字符串(本工程为 "\nnihaoya,dongwazi~",共 17 字节)按字节序继续写入 FIFO;
  4. 启动阶段:追加写入完成后,输出一个 tx_start_en 启动脉冲,通知发送侧开始读取 FIFO 并完成 UDP 回包封装发送。

通过该结构,接收端"吐字节"的节拍与发送端"取字节"的节拍被 FIFO 隔离,发送侧只需依据 tx_req 请求节拍读取 FIFO,即可严格按顺序拿到 原 payload + 追加字符串 的完整数据流,避免因时序对齐问题造成的错位/重读。

2.5.1 udp_payload_append 模块接口与功能说明

udp_payload_append 位于接收输出与 FIFO 写入端之间,工作在接收时钟域(通常为 gmii_rx_clk),

  1. 接收写入阶段:当 rec_en=1 时,模块在每个时钟周期产生 wr_fifo_en=1,并令 wr_fifo_data=rec_data,实现对 payload 的逐字节缓存写入。
  2. 帧结束锁存阶段:当检测到 rec_pkt_done 有效时,模块立即锁存 rec_byte_num、src_mac、src_ip,并计算发送总长度 tx_byte_num = rec_byte_num + APPEND_LEN(本工程 APPEND_LEN=17)。
  3. 追加写入阶段:模块进入追加状态,继续拉高 wr_fifo_en,将追加字符串按字节序写入 FIFO(先写换行 0x0A,再写 "nihaoya,dongwazi~" 的 ASCII 字节序列)。
  4. 发送触发阶段:最后一个追加字节写入 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 中执行:

  1. cd ./sim
  2. 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
  1. 设计目标

验证 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
  1. 辅助变量与复用任务(核心复用逻辑)

关键:

  • out_bytes数组:存储 DUT 输出的wr_fifo_data,8 位宽匹配字节粒度,256 长度覆盖测试场景;
  • send_byte任务:封装单字节发送时序,确保每个字节在时钟上升沿发送,符合 DUT 采样规则;
  • send_payload任务:
    1. str[8*(128-1-i) +: 8]:Verilog 位切片语法,从 128 字符的大端字符串中提取第 i 个字节;
    2. rec_pkt_done脉冲:1 拍宽,符合 FPGA "脉冲型控制信号" 设计规范;
    3. repeat(60):等待 DUT 完成 "追加字符串→写 FIFO→触发 tx_start_en",避免读取未处理完的数据。
  1. 输出数据收集(实时监测 DUT 输出)
  1. 主测试流程
  • 初始化:所有输入信号置默认值(避免仿真初期出现 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" 的最后一位),然后你又追加了 AB 两个字节。

在正点原子的工程基础上修改过的文件链接:通过网盘分享的文件:UDP_4.0.zip

链接: https://pan.baidu.com/s/1EEeW1pDV8nprJfuHEGhFSA 提取码: 1234

相关推荐
头疼的程序员11 小时前
计算机网络:自顶向下方法(第七版)第二章 学习分享(一)
学习·计算机网络
先生沉默先11 小时前
TypeScript 学习_类型与语法(2)
学习·typescript
FPGA_小田老师12 小时前
FPGA例程(4):按键消抖实验
fpga开发·verilog·fpga demo·fpga例程
老朋友此林12 小时前
React Hook原理速通笔记1(useEffect 原理、使用踩坑、渲染周期、依赖项)
javascript·笔记·react.js
FPGA小c鸡12 小时前
FPGA摄像头采集处理显示完全指南:从OV5640到HDMI实时显示(附完整工程代码)
fpga开发
茶猫_12 小时前
C++学习记录-旧题新做-链表求和
数据结构·c++·学习·算法·leetcode·链表
龘龍龙12 小时前
Python基础学习(十一)
python·学习·mysql
jz_ddk12 小时前
[学习] NCO原理与误差分析
fpga开发·gps·gnss·北斗
Chris_121912 小时前
Halcon学习笔记-Day5
人工智能·笔记·python·学习·机器学习·halcon
日更嵌入式的打工仔12 小时前
Ehercat代码解析中文摘录<7>
笔记·ethercat