简介面向FPGA与网络协议开发学习者的千兆/百兆自适应以太网UDP传输工程聚焦链路速率检测与UDP收发硬件实现可用于理解GMII与RGMII转换、CRC32校验、FIFO缓冲及发送控制等关键环节适合具备Verilog基础、希望深入以太网协议栈实践的中高级读者。压缩包共19个文件约69KB主体为12个Verilog源文件辅以3个XML与2个XCI配置、1个Tcl脚本和1个XDC约束文件覆盖逻辑设计、IP核配置与引脚时序约束便于在Vivado中复现与二次修改。已有1302人学习下载说明该方向具有稳定关注度。工程将速率自适应、UDP数据封装与硬件加速思路落在可综合代码中读者可据此梳理收发数据通路、核对CRC与FIFO衔接方式并借鉴IP核与约束组织方法为自建以太网传输模块提供参考。1. 一块板子两种速率千兆-百兆自适应 UDP 传输到底解决什么问题手上有一块 FPGA 板子千兆以太网 UDP 收发跑得很稳换到底座上只支持百兆的 PHY整套逻辑就得推倒重来——这类事在工业采集、图像传输的项目里并不少见。这个资源要解决的正是这一层麻烦一份 Verilog 写的千兆-百兆自适应以太网 UDP 传输工程从 PHY 接口、MAC、ARP、IPv4 一路做到 UDP 与应用层回环速率切换靠 PHY 的速率指示脚或寄存器读回来决定上层发包代码不用改。它适合正在用 FPGA 做网口数据通道、又不想为两种速率维护两套 RTL 的开发者。下面按目录、核心模块、时序、上板排查、二次开发五步拆开讲。2. 工程骨架UDP/IP/MAC 三层在 Verilog 里怎么切开2.1 目录结构与模块职责划分FPGA 网络协议栈最容易写成一坨MAC、IP、UDP 全部塞进一个 always 块最后连自己都说不清哪个信号属于哪一层。这份工程按协议层次切目录每个目录只做自己那一层的事跨层只通过流式握手信号通信这个划分方式值得直接抄。目录大致是这样组织的eth_udp_1g_100m/ ├── rtl/ │ ├── eth_udp_top.v // 顶层例化各层、绑定板级 IO │ ├── phy/ │ │ ├── phy_speed_detect.v // 速率/双工检测与去抖 │ │ ├── gmii_tx_if.v // 千兆发送 8bit 125MHz │ │ ├── gmii_rx_if.v │ │ └── mii_tx_if.v // 百兆发送 4bit靠使能节拍 │ ├── mac/ │ │ ├── mac_tx_ctrl.v // 前导码、FCS、帧间隔 │ │ ├── mac_rx_ctrl.v // 前导码对齐、CRC 校验、长度统计 │ │ └── crc32_d8.v // 字节并行 CRC32 │ ├── net/ │ │ ├── arp_cache.v // 单表项 ARP带老化 │ │ ├── ipv4_tx.v / ipv4_rx.v │ │ └── udp_tx.v / udp_rx.v │ └── common/ │ ├── async_fifo.v // 应用时钟 ↔ 网络时钟 │ └── rst_sync.v ├── sim/ │ ├── tb_udp_loopback.v │ └── eth_pkt_gen.v // 激励包生成可注入错包 ├── constr/ │ └── eth_top.xdc └── doc/ └── reg_map.md层次关键文件输入/输出依赖的时钟PHY 接口gmii_tx_if.v/mii_tx_if.v8bit 字节流 → 板级引脚125MHz 主时钟MACmac_tx_ctrl.v帧体 → 带前导码和 FCS 的帧同上网络层ipv4_tx.vUDP 报文 → IP 报文同上传输层udp_tx.v用户数据 → UDP 报文同上应用侧async_fifo.v用户时钟域数据用户时钟这样切的好处很实在调百兆的时候只动phy/目录net/和mac/一行不用改改协议字段的时候只动net/不会碰到底层时序。血泪经验是很多工程把 UDP 长度字段算在了 MAC 层里结果换速率时长度单位对不上包直接被对端丢掉。2.2 发送链路一个字节从 FIFO 走到差分对的全过程发送路径是一条严格的流水线。应用侧把数据按 AXI4-Stream 风格的tvalid/tready/tdata/tlast写进异步 FIFOFIFO 读侧出来先到udp_tx加上 8 字节 UDP 首部再进ipv4_tx前面拼 20 字节 IP 首部并算好校验和到mac_tx_ctrl时补 7 个0x55加 1 个0xD5的前导码帧体发完再追加 4 字节 FCS最后插 12 字节帧间隔回到空闲。每一级之间的握手只有三个信号valid表示本级有数据要送、ready表示下级能接、last标记一帧结束。这种写法比用一堆start/done/busy好维护得多因为反压能顺着ready一路传回 FIFOFIFO 快满时自然就把应用侧顶住了。我一般会在mac_tx_ctrl里额外加一个frame_len_cnt一边发一边数字节和首部里写的长度做比对长度不一致直接置错误标志位——这比抓包抓半天快得多。还有一个细节FCS 的计算必须从目的 MAC 的第一个字节开始一直算到 payload 的最后一个字节前导码不参与。工程里mac_tx_ctrl专门有个crc_en信号在帧体第一个有效字节拉高、last之后一拍拉低中间既不漏拍也不多算。2.3 接收链路与跨时钟域的处理接收侧反过来gmii_rx_if把 8bit 数据打两拍同步到主时钟域检测到0xD5后进入帧接收状态把前导码之后的字节全部送进mac_rx_ctrl后者边收边算 CRC同时数长度收到帧尾把本地算出的 CRC 和帧里带的 4 字节比对一致才把rx_valid放开给ipv4_rx。IP 层做三件事版本号必须是 4、首部校验和必须对、目的 IP 必须匹配本机或者广播通过了才交给udp_rx按目的端口分流。跨时钟域是这类工程翻车最多的地方。GMII 是 125MHz、MII 是 25MHz而用户逻辑往往跑在 100MHz 或 50MHz两边的数据必须过异步 FIFO。FIFO 深度我建议至少按一个最大帧1518 字节来算也就是 2K 深度的 8bit FIFO深度不够时对端连续发两个大包就会溢出表现是偶尔丢一包很难复现。FIFO 的满信号要接到ipv4_rx的ready上做反压让上层直接不接收这个包而不是收进来再丢——丢掉一个已经算过 CRC 的完整包比在门口拒绝要浪费得多。2.4 参数集中管理速率相关的常数只写一处自适应工程里最容易出错的是常数散落。前导码长度、帧间隔、最大帧长、超时计数在千兆和百兆下取值不同严格说 IPG 都是 12 字节但对应的时钟拍数一个是 12 拍、一个是 60 拍。工程在eth_udp_top.v里用一个speed_mode信号统一分发各模块内部再用speed_mode换算成自己需要的拍数而不是各自维护一套参数。// eth_udp_top.v —— 速率相关的拍数统一换算避免各模块各写一份 localparam SPEED_1G 1b0; localparam SPEED_100M 1b1; // 125MHz 主时钟下一个字节对应的时钟拍数 // 千兆1 拍 1 字节百兆10 拍 1 字节每 5 拍一个 4bit 半字节 wire [4:0] byte_period (speed_mode SPEED_1G) ? 5d1 : 5d10; // 12 字节帧间隔换算成时钟拍数 wire [9:0] ipg_cycles 10d12 * byte_period;byte_period这个信号是整个自适应逻辑的核心后面 PHY 层的节拍生成、MAC 层的超时计数、接收侧的帧结束判定都从它派生。把它做成一个信号而不是宏综合后就是几个 LUT不占资源但可读性好了不止一点。改速率的时候只要speed_mode一变下游所有拍数自动跟着走。3. 核心模块拆解发送状态机、首部组装与 CRC32 三条主线3.1 发送状态机从 IDLE 到 IPG 的六个阶段MAC 发送是一段标准的六状态机没有任何花哨的地方但状态划分必须干净否则前导码少一个字节或者 FCS 多算一拍对端就是一个 CRC 错包。工程里的状态定义是这样的// mac_tx_ctrl.v —— 以太网发送主状态机 localparam S_IDLE 3d0; localparam S_PREAMBLE 3d1; // 7 x 0x55 1 x 0xD5 localparam S_HEADER 3d2; // 目的MAC/源MAC/类型 localparam S_PAYLOAD 3d3; // IP 报文整体 localparam S_FCS 3d4; // 4 字节 CRC localparam S_IPG 3d5; // 帧间隔 always (posedge clk) begin if (rst_n 1b0) begin state S_IDLE; end else if (beat_en) begin // beat_en 由速率决定见第 4 章 case (state) S_IDLE: if (tx_req) state S_PREAMBLE; S_PREAMBLE: if (cnt PRE_LEN-1) state S_HEADER; S_HEADER: if (hdr_done) state S_PAYLOAD; S_PAYLOAD: if (payload_done) state S_FCS; S_FCS: if (cnt 4d3) state S_IPG; S_IPG: if (ipg_done) state S_IDLE; default: state S_IDLE; endcase end endstate的推进被beat_en门控这是整个工程能同时兼容两种速率的关键状态机本身完全不关心当前是千兆还是百兆它只认该出下一个字节了这个使能。PRE_LEN固定为 8S_FCS里cnt数到 3 说明 4 个字节发完ipg_done由前面算出的ipg_cycles决定。状态机里所有动作都挂在beat_en上唯一例外是tx_req的采样——它在S_IDLE里必须每拍都看否则使能没来的时候请求会被漏掉。有一处容易写错S_PREAMBLE计数器要在进入状态时清零而不是在S_IDLE里清。很多写法是在S_IDLE里把cnt 0然后S_PREAMBLE第一拍就判cnt 7前导码直接少发一个字节对端收到后帧定界失败抓包上看就是发出去的包格式不对。3.2 UDP 与 IPv4 首部组装校验和怎么算才不返工UDP 首部就 8 个字节但字段顺序和字节序必须按网络字节序大端摆。工程里用一个 64bit 寄存器一次性拼好出数时从高字节往低字节取// udp_tx.v —— 组装 8 字节 UDP 首部大端 // [0:1] 源端口 [2:3] 目的端口 [4:5] 长度 [6:7] 校验和 always (*) begin udp_hdr[63:48] src_port; udp_hdr[47:32] dst_port; udp_hdr[31:16] payload_len 16d8; // 长度含 8 字节首部 udp_hdr[15:0] 16h0000; // 校验和置 0 表示不校验 endpayload_len是纯用户数据长度UDP 长度字段要把首部 8 字节加进去这一条几乎每个新手都会错一次。至于 UDP 校验和IPv4 下允许置 0 表示不校验工程为了省资源默认关掉真要开的话得先把伪首部源 IP、目的 IP、协议号、UDP 长度一起参与累加代码量翻三倍收益不大。IP 首部 20 字节校验和的算法是 16 位一组累加再取反注意回卷// ipv4_tx.v —— IP 首部校验和输入首部里校验和字段先填 0 function [15:0] ip_csum; input [159:0] hdr; // 20 字节 160bit integer i; reg [31:0] sum; begin sum 32d0; for (i 0; i 10; i i 1) sum sum {16h0000, hdr[i*16 : 16]}; sum sum[31:16] sum[15:0]; // 第一次回卷 sum sum[31:16] sum[15:0]; // 第二次回卷防止进位残留 ip_csum ~sum[15:0]; end endfunction回卷要写两次只写一次在某些特殊取值下会漏掉进位表现出来就是大部分包能通偶尔 ping 超时。这个坑非常隐蔽因为错的概率和 IP 地址有关换个 IP 就好了很容易被当成玄学。校验和算完写回首部第 10、11 字节注意这里同样是大端hdr[79:64]对应第 10、11 字节。3.3 CRC32 字节并行实现与小端输出以太网 FCS 用的是 CRC32多项式0x04C11DB7但实际计算用反射形式0xEDB88320初始值全 1结果取反后按小端字节序发出。工程里做成一个可复用的函数MAC 发送和接收共用同一份逻辑避免两边算法细节对不上// crc32_d8.v —— 每拍处理 1 字节反射多项式 0xEDB88320 function [31:0] crc32_next; input [31:0] crc_in; input [7:0] data; integer i; reg [31:0] c; begin c crc_in ^ {24h000000, data}; for (i 0; i 8; i i 1) c c[0] ? ((c 1) ^ 32hEDB88320) : (c 1); crc32_next c; end endfunction always (posedge clk) begin if (crc_clr) crc_reg 32hFFFFFFFF; // 帧首复位为全 1 else if (crc_en) crc_reg crc32_next(crc_reg, crc_din); end // 发送时结果取反并按低字节在前输出 assign fcs_byte ~crc_reg (8 * fcs_idx); // fcs_idx: 0→3逐字节循环是组合逻辑8 次迭代在 125MHz 下能过但时序比较紧如果综合报 setup 违例就改成 4bit 两级流水或者用工具提供的 CRC 原语。接收侧比对时不能用直接比 32 位——收到的 4 字节是低字节在前得先拼回 32bit 再和~crc_reg比较直接比字节序必错。3.4 ARP 缓存与接收端过滤顺序UDP 发包前得知道对端 MAC工程里放了一个单表项的 ARP 缓存收到 ARP 请求就回自己的 MAC收到 ARP 应答就把对方的 IP-MAC 写进去带一个老化计时器超时清空重来。// arp_cache.v —— 单表项 ARP收到应答即更新 always (posedge clk) begin if (rst_n 1b0) begin arp_vld 1b0; end else if (arp_reply_vld) begin arp_mac arp_src_mac; arp_ip arp_src_ip; arp_vld 1b1; arp_timer 32d300_000_000; // 100MHz 下约 3s按需放大 end else if (arp_timer ! 0) begin arp_timer arp_timer - 1b1; if (arp_timer 1) arp_vld 1b0; end end接收端的过滤顺序同样重要工程里是固定的一条链帧长合法 → CRC 正确 → 目的 MAC 匹配或广播 → 类型是 0x0800 或 0x0806 → IP 校验和正确 → 协议号 17 → 目的端口匹配。顺序不能随便换先做 CRC 再做 IP 校验可以把错包挡在最外层省下后面的逻辑翻转和功耗。有人喜欢先判端口号结果坏包一路走到 UDP 层才被丢掉逻辑资源白烧。4. 速率切换125MHz 单时钟加时钟使能怎么喂饱两种 PHY4.1 为什么不做双时钟而是单时钟加使能直观想法是千兆用 125MHz、百兆用 25MHz两个时钟各自驱动一套 MAC。这条路能走通但等于把 MAC 层复制一份或者加一堆时钟选择逻辑跨时钟域的握手会变得非常难验证而且两种速率下 MAC 的计数器位宽还不一样。工程选择的是单时钟方案整个 MAC 及以上全部跑在 125MHz百兆模式下靠一个每 5 拍有效一次的使能信号来降速。这样 MAC 层只认beat_en完全不知道外面接的是什么速率的 PHY逻辑只有一份验证也只验一份。代价是百兆时逻辑利用率低但网络这块的 LUT 占用本来就不大这点浪费完全划得来。// phy_speed_detect.v —— 采样 PHY 速率指示脚去抖后输出模式 localparam SPEED_1G 1b0; localparam SPEED_100M 1b1; reg [1:0] sync_ff; reg [15:0] stable_cnt; reg speed_stable; always (posedge clk) begin sync_ff {sync_ff[0], phy_speed_sel}; // 两级同步防亚稳态 if (sync_ff[1] sync_ff[0]) begin if (stable_cnt ! 16hFFFF) stable_cnt stable_cnt 1b1; else speed_stable sync_ff[1]; end else begin stable_cnt 16d0; end end assign speed_mode speed_stable ? SPEED_100M : SPEED_1G;phy_speed_sel是 PHY 芯片输出的速率指示不同厂商极性可能相反接反了就是千兆板子上被识别成百兆。工程里留了一个SPEED_SEL_INV参数改一个 bit 就能翻转判断不用动逻辑。去抖计数按 125MHz 算65535 拍大约 0.5ms足够滤掉上电瞬间的抖动。4.2 从字节流到半字节流MII 侧的节拍换算千兆 GMII 是 8bit 每拍一个字节百兆 MII 是 4bit且在 25MHz 下每拍一个半字节。主时钟 125MHz 下125/25 5所以每 5 拍产生一个半字节使能10 拍完成一个字节。节拍生成就三行// phy_tick_gen.v —— 生成 PHY 侧发送/接收节拍 // 千兆每拍一个字节百兆每 5 拍一个半字节 reg [2:0] div_cnt; always (posedge clk) begin if (speed_mode SPEED_1G) div_cnt 3d0; else if (div_cnt 3d4) div_cnt 3d0; else div_cnt div_cnt 1b1; end assign nib_en (speed_mode SPEED_1G) ? 1b1 : (div_cnt 3d4); assign beat_en (speed_mode SPEED_1G) ? 1b1 : (div_cnt 3d4) nib_sel;nib_en控制 MII 引脚上什么时候出一个新的半字节beat_en控制 MAC 层什么时候推进一个完整字节——只有两个半字节都送完了才算一个字节。nib_sel在 0/1 之间翻转0 时送高 4 位、1 时送低 4 位顺序错了就是经典现象能 ping 通但 UDP 数据全是乱码。MII 输出侧// mii_tx_if.v —— 字节拆半字节输出 always (posedge clk) begin if (rst_n 1b0) begin mii_tx_en 1b0; nib_sel 1b0; end else if (nib_en) begin mii_txd nib_sel ? tx_byte[3:0] : tx_byte[7:4]; mii_tx_en tx_active; nib_sel ~nib_sel; end end注意nib_sel只在nib_en有效时翻转不能在每拍都翻否则千兆模式下这个信号会乱跳虽然千兆用不到它但会影响综合时的公共逻辑。tx_active表示当前处于有效帧内帧间隔期间mii_tx_en必须为低PHY 靠这个判断帧边界。4.3 时序约束与 RGMII 的 IO 延时千兆接口如果走 RGMII是 DDR 双沿采样约束写不对就是跑一会儿错几个包。关键是把接收时钟当独立时钟建约束数据用 input delay 描述相对时钟的偏移# constr/eth_top.xdc —— RGMII 接收侧源同步约束 create_clock -name rgmii_rxc -period 8.000 [get_ports rgmii_rxc] set_input_delay -clock rgmii_rxc -max 1.200 [get_ports {rgmii_rxd[*]}] set_input_delay -clock rgmii_rxc -min 0.300 [get_ports {rgmii_rxd[*]}] set_input_delay -clock rgmii_rxc -clock_fall -max 1.200 [get_ports {rgmii_rxd[*]}] -add_delay set_input_delay -clock rgmii_rxc -clock_fall -min 0.300 [get_ports {rgmii_rxd[*]}] -add_delay-clock_fall这两行是 DDR 的关键漏了就只有上升沿被约束下降沿的数据完全靠运气。具体数值要按 PHY 手册和 PCB 走线长度微调工程里给的是保守估计值先跑通再优化。发送侧的rgmii_txc一般要加 90 度相移用时钟原语或者 IO 延时实现各平台写法不一样。如果是 GMII 直连8bit SDR约束简单得多接收时钟进 BUFG数据打两拍同步即可MII 更简单25MHz 基本不涉及时序问题但也不能完全不约束至少给mii_rx_clk建个 40ns 的时钟让工具知道这是个真实时钟域。4.4 两种速率下的关键参数对照参数千兆 (GMII/RGMII)百兆 (MII)代码里的对应信号PHY 侧时钟125MHz25MHz主时钟 125MHz 使能数据位宽8bitRGMII 为 4bit DDR4bitgmii_txd/mii_txd每字节拍数110byte_period前导码7×0x55 0xD5同左PRE_LEN 8帧间隔12 字节 12 拍12 字节 120 拍ipg_cycles最小帧长64 字节64 字节收发两侧都做长度检查这张表里的每字节拍数是唯一需要区分速率的地方其他参数在两种速率下字节数相同只是换算成时钟拍数不同——这也是单时钟方案能成立的根本原因。5. 上板排查从 ARP 有应答到 ping 不通的五类现场问题5.1 现象ARP 能回ping 不通原因基本锁定在 IP 首部。最常见的两种一是校验和字段没填发出去的首部第 10、11 字节是 0对端算出来不匹配直接丢二是校验和填了但字节序反了把csum[15:8]放在了前面。这两种错法在抓包工具里长得一模一样都是IP header checksum incorrect。解决方式是先在仿真里把发出的 IP 首部打印出来手工算一遍校验和再在ipv4_tx里加一个自检对拼好的 20 字节首部重新算一次校验和结果必须是0x0000包含校验和字段本身再累加一次应该为零。这个自检我通常留在代码里综合时用一个常量开关关掉出问题时打开比抓包快。5.2 现象千兆一切正常切到百兆所有包 CRC 错原因通常有两个。一是半字节顺序反了nib_sel的初始值和期望相反导致每个字节的高低位颠倒CRC 自然全错。二是nib_en的节拍相位不对第一个半字节在帧开始前就发出去了前导码错位。解决方式是拿仿真波形对一遍帧开始后第一个nib_en有效拍上mii_txd应该等于0x5前导码0x55的高半字节第二个应该是0x5一路数到第 15 个半字节是0xD。对不上就查nib_sel的复位值。另外把mii_tx_en和nib_en的时序对齐也检查一遍正常时序是mii_tx_en先拉高、下一个nib_en才出第一个半字节。5.3 现象小包正常大包必丢原因在接收 FIFO 深度不够或者反压没接。UDP 大包 1472 字节加上各层首部接近 1518如果 FIFO 只有 512 深度一个大包进来写到一半就满了后面的数据被丢掉tlast也没了接收状态机卡死在S_PAYLOAD里等不到帧结束后续所有包都收不进来。解决办法是把 FIFO 深度提到 2048并且把 FIFO 的prog_full可编程满比如设到 1800接到 IP 层的ready上做反压。还有一点容易被忽略状态机要加超时保护进S_PAYLOAD之后如果超过最大帧长对应的拍数还没等到last强制回S_IDLE并清空 FIFO。没有这个保护一个坏包就能让整个接收通道永久卡死现场表现为重启才能恢复。5.4 现象实验室好好的现场长线跑千兆偶发错包原因是 RGMII 的 IO 延时余量不够。实验室用短跳线走线延迟小随便约束都能过现场换成长网线PHY 内部的延时补偿加上 PCB 走线偏差采样点漂到数据边沿附近就开始偶发误码表现是跑几个小时错一两个包。解决方式是先用工具里的时序报告看 RGMII 接收路径的 setup/hold 余量余量小于 0.5ns 就要调set_input_delay的值或者用 IDELAY 对每个数据位做精细对齐。更稳的做法是在 PHY 侧开启内部延时很多 PHY 支持 RGMII 内部延时模式把 PCB 上的时序要求放宽代价是延迟增加一点。这类问题没有捷径只能靠余量留够。5.5 现象上电后链路一直起不来速率检测永远是千兆原因是phy_speed_sel的极性判断反了或者这根线根本没接、悬空被上拉成高电平。前者改一个参数就行后者麻烦一点如果硬件上确实没接速率指示脚就得改成读 PHY 的寄存器通过 MDIO来获取速率工程里phy_speed_detect.v留了 MDIO 读的接口但默认没启用。解决顺序是先量引脚电平百兆下这根脚应该是低或者高看 PHY 手册量出来不对就是硬件问题。软件上再加一层兜底如果链路一直 downphy_link为低超过 2 秒就强制按百兆模式跑一次因为百兆对时钟精度的容忍度更高能通说明物理层没问题再回头查速率检测。这招在调试阶段救过我好几次。6. 二次开发把定长回环改成带包长的可变长数据通道原始工程的应用层是定长回环收到一个 UDP 包把 payload 原样发回去长度写死成一个参数。真要用在采集系统里第一件事就是把定长改成可变长——上位机发什么长度板子就回什么长度或者板子自己按采样节拍往外推不定长的数据。改动点集中在三处。第一处是udp_tx.v的长度字段把原来写死的localparam PAYLOAD_LEN换成一个寄存器由应用侧在发数之前写进来// udp_tx.v —— 长度改为运行时可配 reg [15:0] payload_len_r; always (posedge clk) begin if (rst_n 1b0) payload_len_r 16d32; // 上电默认值 else if (len_load) // 应用侧单拍脉冲 payload_len_r len_in; // 本次要发的字节数 end // UDP 长度字段 payload 8注意别漏掉首部 assign udp_len_field payload_len_r 16d8;len_load必须和tx_req在同一拍或者早一拍给出晚了就会用上一包的长度表现出来是偶尔多回几个字节。我一般会在应用侧做一个小小的长度 FIFO写数据的时候顺便把长度压进去发送侧从 FIFO 里取天然对齐。第二处是 FIFO 的帧边界。定长回环可以靠计数器判断包尾变长之后必须依赖tlast。写侧在最后一个字节拉高tlast读侧把它透传出去mac_tx_ctrl用tlast而不是计数器判断payload_done。同时把长度计数器保留下来做交叉校验tlast到达时的计数值必须等于payload_len_r不等就置一个错误标志方便定位上游给的数据和长度不一致的问题。第三处是背压。变长之后单包最大可能到 1472 字节接收侧 FIFO 必须能容纳一整包前面说的 2048 深度在这里是硬要求。另外建议在udp_rx里加一个简单的包过滤目的端口不匹配的直接丢不做 CRC 校验之外的任何处理避免无关广播包把 FIFO 占满。做完这三处回环就从玩具变成了能接真实数据源的通道。还有一个我自己的习惯每次改完协议字段先在sim/tb_udp_loopback.v里跑一遍自环——发送侧接接收侧源端口对目的端口数据从 FIFO 进去再出来逐字节比对。这一步能在上板前抓掉九成的字节序和长度问题。从那以后我每次动网络层的代码不管改动多小都强制先跑一遍自环仿真再综合省下来的抓包时间远比写 testbench 的时间多。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?