FPGA以太网UDP协议栈实战:verilog-ethernet仿真到上板 📅 发布时间:2026/9/18 10:20:54 👁 浏览次数: 折腾到第 10 篇才动协议栈不是因为我懒而是因为前面九篇把 LED、数码管、串口、状态机、仿真流程这些东西踩得差不多了回头看才发现一件事FPGA 里真正难啃的从来不是语法而是多个模块按一套约定协同工作这套思维。verilog-ethernet 这个开源工程恰好是练这套思维的好材料——它把以太网 MAC、ARP、IP、UDP 一层层拆成独立模块用 AXI-Stream 串起来代码风格统一注释清楚还自带仿真测试平台。你不需要从零写一个 MAC也不用去啃几千行的商用 IP 手册就能把一个 UDP 包怎么从 FPGA 的引脚发出去、又怎么从网线收进来这件事看得明明白白。这篇就围绕这个工程从选型理由、协议字段、模块分层、仿真搭建、上板调试到故障排查把我自己走过一遍的路完整摊开适合刚学完 Verilog 基础语法、想碰一碰真实总线协议的人。1. 为什么第 10 篇才碰协议栈verilog-ethernet 的定位与选型1.1 自己撸一个 MAC 还是直接用现成工程算一次成本账刚接触 FPGA 网络通信的人第一个念头往往是我是不是得自己写一个以太网控制器。这个念头很正常但如果真的动手你会发现工作量远超预期。一个能跑起来的最小千兆 MAC至少要处理前导码与 SFD 识别、CRC32 生成与校验、发送侧的载波扩展与最小帧长填充、接收侧的错误帧丢弃、时钟域转换还有 RGMII 那种双沿采样接口的时序约束。这些细节单拎出来都不难凑在一起就是几百行的边角逻辑而且每一项出错的表现都是ping 不通没有任何报错信息告诉你错在哪。verilog-ethernet 把这些问题都解决了。它以 GMII/RGMII/MII/XGMII 这些标准物理接口为输入内部把 MAC、ARP、IPv4、UDP 分成了清晰的层次每层之间用 AXI-Stream 握手连接。你可以只用到 MAC 层也可以直接用到 UDP 层。更关键的是它的代码里没有藏黑盒eth_axis_rx.v里 CRC 校验怎么做的、udp_ip_tx.v里校验和怎么累加的都是几十行可读的 Verilog对着看一遍比读任何文档都管用。选它的另一个理由是仿真友好。仓库里带了sim目录和 MakefileVerilator 和 Icarus Verilog 都能跑甚至提供了 cocotb 的 Python 测试。这意味着你在没板子的情况下就能把整个数据通路跑通看到 AXI-Stream 上的数据流一波波过去。对于从零起步的人来说先在仿真里看清楚再上板调这条路径能省掉大量盲猜时间。注意这个工程是社区开源项目不同 commit 的目录结构和模块命名会有差异有些版本把udp_complete.v拆成了独立的udp_ip_rx.v、udp_ip_tx.v例化方式也不一样。学习时先确认自己手上版本的顶层文件叫什么别拿着旧教程的代码硬套。1.2 工程分层从网线到 UDP 载荷每一层归谁管把这个工程想象成一栋楼。最底层是 PHY 接口层负责和外部 PHY 芯片打交道GMII 是 8 位数据加 125MHz 时钟RGMII 是 4 位数据 DDR 双沿采样本质上速率都是 1Gbps。往上一层是 MAC 层做的是帧级的事加前导码、算 CRC、判断帧是否合法、处理最小帧长和最大帧长。再往上是以太网帧解析层把 MAC 载荷里的目的 MAC、源 MAC、类型字段拆出来交给上层判断这是 ARP 包还是 IP 包。再往上走是 IPv4 层负责解析和处理 IP 首部包括版本、首部长度、总长度、TTL、协议号、首部校验和以及源 IP 和目的 IP。最后是 UDP 层把 IP 载荷里的源端口、目的端口、长度、校验和解析出来剩下的就是用户数据。这个分层带来的好处是你可以在任意一层截胡。比如你只想做个自定义以太网协议不想碰 IP 和 UDP那就在 MAC 层之上接自己的逻辑判断s_eth_type是不是你约定的值是就往自己的通路走不是就丢掉。仓库里提供了eth_arb_mux这类仲裁模块本质上就是按类型字段做分流。理解了这套分层你再看任何网络协议栈的代码都会有似曾相识的感觉。层次主要模块旧版命名核心职责PHY 接口axis_gmii_rx/axis_gmii_tx8 位 GMII 与 AXI-Stream 互转MACeth_axis_rx/eth_axis_tx前导码、CRC32、帧长校验帧解析eth_mac_1g_fifo等顶层拼接 MAC 与 PHY含跨时钟 FIFOARP/IP/UDPudp_complete内部三层协议解析与组装跨时钟axis_async_fifo125MHz 与用户时钟域隔离2. 拆开 UDP 协议栈帧结构、字段与模块对应关系2.1 一个 UDP 包从外到内到底有哪几个字段要填先建立一个数量概念。一个标准的 UDP over IPv4 over Ethernet 帧从网线上看是这样的前导码 7 字节全是 0x55SFD 1 字节 0xD5这两个由 MAC 层自动生成你不用管。目的 MAC 6 字节源 MAC 6 字节这是决定发给谁、从哪来的关键。类型/长度 2 字节0x0800 表示后面是 IPv40x0806 表示 ARP。IP 首部 20 字节包含版本号 4、首部长度 5、服务类型、总长度、标识、标志与片偏移、TTL、协议号、首部校验和、源 IP、目的 IP。UDP 首部 8 字节包含源端口、目的端口、长度、校验和。用户数据最少 0 字节最大受 MTU 限制常见以太网 MTU 是 1500 字节减去 IP 和 UDP 首部用户数据最多 1472 字节。FCS 4 字节CRC32由 MAC 层自动算。这里有几个容易被忽略但很致命的点。第一UDP 长度字段是UDP 首部加用户数据的总长度不是只有用户数据很多人在这里少算 8 字节导致对端解析时多读或少读。第二IP 总长度字段包含 IP 首部和 IP 载荷如果你用的是标准 20 字节 IP 首部那就是 20 加 UDP 长度。第三协议号字段在 IP 首部里必须填 17十六进制是 8h11这个值填错对端直接当未知协议丢弃连日志都不给你留。还有一个校验和的坑。IPv4 里 UDP 校验和是可选的填 0 表示发送方没有计算校验和接收方应当忽略校验和字段。verilog-ethernet 支持把校验和设成 0也支持真正计算。如果你的链路本身可靠比如就是板子直连 PC 用短网线把 UDP 校验和设成 0 能省掉一整块累加逻辑和流水线延迟调试阶段很实用。2.2 模块划分与数据流向发送和接收是两条独立的流水线理解这个工程的关键是意识到发送和接收是完全分开的两条通路。发送侧是用户给数据模块往外吐帧接收侧是模块从网线收帧用户取数据。两边各有自己的 AXI-Stream 握手信号互不干扰。发送侧用户要做的动作分两步。第一步是给出报文头把目的 MAC、源 MAC、源 IP、目的 IP、TTL、协议号、源端口、目的端口、UDP 长度、UDP 校验和这些字段准备好了然后把s_udp_hdr_valid拉高等s_udp_hdr_ready响应。第二步是给用户数据通过在s_udp_payload_axis_tdata上打数据、用tvalid和tready握手、最后一个字节用tlast标记结束同时注意最后一个 cycle 的tkeep要正确指示哪些字节有效。接收侧反过来。模块先把m_udp_hdr_valid拉高告诉你头部信息到了这些信息里包含源 IP、源端口你可以根据这些决定要不要收。你如果决定收把m_udp_hdr_ready拉高然后就等着从m_udp_payload_axis_tdata上读数据读到tlast就是这一个包的结尾。这个先看头、再决定收不收的设计很实用天然支持按端口过滤只有目标端口匹配的数据才需要你真正处理。下面是一个简化的例化骨架省略了部分端口实际使用时以你手上版本的端口定义为准udp_complete #( .DATA_WIDTH(8), .KEEP_ENABLE(1), .KEEP_WIDTH(1), .UDP_ENABLE(1), .CHECKSUM_ENABLE(1), .OUT_ENABLE(1) ) udp_complete_inst ( .clk(clk), .rst(rst), // GMII 侧接 PHY .gmii_rxd(gmii_rxd), .gmii_rx_dv(gmii_rx_dv), .gmii_rx_er(gmii_rx_er), .gmii_rx_clk(gmii_rx_clk), .gmii_txd(gmii_txd), .gmii_tx_en(gmii_tx_en), .gmii_tx_er(gmii_tx_er), .gmii_tx_clk(gmii_tx_clk), // 发送侧用户接口 .s_udp_hdr_valid(s_udp_hdr_valid), .s_udp_hdr_ready(s_udp_hdr_ready), .s_udp_eth_dest_mac(s_udp_eth_dest_mac), .s_udp_eth_src_mac(s_udp_eth_src_mac), .s_udp_eth_type(s_udp_eth_type), .s_udp_ip_src(s_udp_ip_src), .s_udp_ip_dest(s_udp_ip_dest), .s_udp_ip_ttl(s_udp_ip_ttl), .s_udp_ip_protocol(s_udp_ip_protocol), .s_udp_udp_source_port(s_udp_udp_source_port), .s_udp_udp_dest_port(s_udp_udp_dest_port), .s_udp_udp_length(s_udp_udp_length), .s_udp_udp_checksum(s_udp_udp_checksum), .s_udp_payload_axis_tdata(s_udp_payload_axis_tdata), .s_udp_payload_axis_tkeep(s_udp_payload_axis_tkeep), .s_udp_payload_axis_tvalid(s_udp_payload_axis_tvalid), .s_udp_payload_axis_tready(s_udp_payload_axis_tready), .s_udp_payload_axis_tlast(s_udp_payload_axis_tlast), .s_udp_payload_axis_tuser(s_udp_payload_axis_tuser), // 接收侧用户接口 .m_udp_hdr_valid(m_udp_hdr_valid), .m_udp_hdr_ready(m_udp_hdr_ready), .m_udp_eth_dest_mac(m_udp_eth_dest_mac), .m_udp_eth_src_mac(m_udp_eth_src_mac), .m_udp_ip_src(m_udp_ip_src), .m_udp_ip_dest(m_udp_ip_dest), .m_udp_ip_protocol(m_udp_ip_protocol), .m_udp_udp_source_port(m_udp_udp_source_port), .m_udp_udp_dest_port(m_udp_udp_dest_port), .m_udp_udp_length(m_udp_udp_length), .m_udp_payload_axis_tdata(m_udp_payload_axis_tdata), .m_udp_payload_axis_tvalid(m_udp_payload_axis_tvalid), .m_udp_payload_axis_tready(m_udp_payload_axis_tready), .m_udp_payload_axis_tlast(m_udp_payload_axis_tlast) );这里面s_udp_eth_type一般填16h0800s_udp_ip_protocol填8h11s_udp_ip_ttl填 64 或 128 都行传统习惯是 64。这些默认值在工程自带的例子里都能找到照着抄一遍再对着协议字段表核对一遍比死记硬背有效得多。2.3 校验和这块怎么处理才不拖后腿UDP 校验和是很多人的第一个效率陷阱。它需要把伪首部源 IP、目的 IP、保留字节 0、协议号 17、UDP 长度、UDP 首部和用户数据全部按 16 位累加再取反码。麻烦之处在于UDP 长度在首部里用户数据长度又不确定所以要等整包数据都到齐才能算出最终校验和但校验和字段本身又在首部里、在数据之前发出去。这个工程的处理方式是两遍走或者用 FIFO 缓存整个载荷先算完校验和再回头填首部。这在 1Gbps 下会造成额外的缓冲需求。如果你不需要严格校验直接把s_udp_udp_checksum填16h0000让接收方忽略逻辑瞬间简单一大截。我在板子直连 PC 的场景里几乎都是这么干的链路本身有 FCS 保护误码率极低没必要为了一个理论上的完整性再堆一块 RAM 和一堆时序约束。真要用校验和要注意一个细节累加时如果中间结果是 0xFFFF补码运算时不能简单取反成 0x0000标准要求把它变成 0xFFFF 再处理。这类边界情况在源码的注释里一般有说明读的时候别跳过去。注意如果你把 UDP 校验和设成 0某些操作系统或库尤其是做了严格校验的实现可能仍然会验证。测试阶段建议先用 Wireshark 抓一个包确认校验和字段确实是 0对端也接受了再往下走。3. 让工程真的动起来从仿真到上板的完整实操3.1 仿真环境与测试激励的搭建在碰板子之前务必先在仿真里把数据通路跑通。这一步能帮你排除掉 90% 的语法和握手问题剩下的才是板级时序问题。仓库的sim目录里一般有现成的测试平台比如tb_udp_complete.v这类文件用 Verilator 或 Icarus 跑一个 Makefile 目标就行。跑起来之后你会看到波形里 GMII 信号上出现前导码、目的 MAC、类型字段然后是 AXI-Stream 上的握手。第一次看到这些信号按协议规规矩矩地出现比读十遍协议文档都直观。如果你想自己写激励最简单的做法是做一个回环验证让发送侧发出一串递增的数据接收侧收回来比对是否一致。这个测试不需要真的 PHY也不需要网线纯逻辑层面就能验证发送和接收两条通路都工作正常。写激励时注意几点时钟要稳复位要足够长至少 10 个周期发送数据要有变化并且长度覆盖奇偶、覆盖短包和长包接收侧要能背压把m_udp_hdr_ready随机拉低这样才能测出握手逻辑的边界问题。这一点非常重要很多人写的测试激励永远不背压结果上板一遇到 PC 端处理不过来就丢包还以为是硬件问题。如果你用 Verilator注意它默认不支持 Verilog 的某些行为级语法比如initial块里的延时、#10这种遇到编译报错先看是不是用了不可综合的写法。这些测试平台通常都是可综合风格加少量仿真专用代码问题不大但骨架要清楚。3.2 上板三件套PHY 接口、时钟、复位仿真过了接下来是真正的考验。上板要盯住三件事。第一件是 PHY 接口。如果你用的是 GMII那比较简单PHY 芯片会输出 125MHz 的rx_clk数据是 8 位同步的。如果你用的是 RGMII就复杂得多4 位数据在时钟双沿传输通常需要在 FPGA 内部用 IDELAY 原语调整采样点或者依赖 PHY 侧的延时配置。Xilinx 器件上一般用IDELAYE2或者ODELAYE2具体参数要看你的板子和 PHY 型号时序裕量不够的表现就是链路起来但大量 CRC 错误。还有一点很多 PHY 芯片默认通过 strapping 引脚配置成自协商模式如果你的板子没有专门的 MDIO 配置逻辑就要确保 PHY 的硬件配置引脚状态正确能自协商到 1000M 全双工。verilog-ethernet 本身不包含 MDIO 控制器需要你自己写或者借用厂商示例工程里的那一小段。这是上板前必须确认的第一件事。第二件是时钟。MAC 工作在 125MHz你的用户逻辑如果简单可以直接用同一个时钟如果用户逻辑要跑到更高频率或者用的是别的时钟源就必须用axis_async_fifo做跨时钟。这个 FIFO 模块支持独立的读写时钟深度可以配是工程里跨域的标准做法。千万不要自己写一个双口 RAM 加格雷码指针糊弄跨时钟域的握手时序问题一旦出现就是偶发丢包极难定位。第三件是复位。异步复位同步释放这是基本原则。MAC 的复位要和 PHY 的时钟同步用户逻辑的复位要和用户时钟同步跨时钟域之间用 FIFO 的复位同步机制。我曾经因为一个复位信号用了板子的按键直接打进去没有做同步结果上电十次有两次链路起不来排查了两天才发现是复位释放时机不对。3.3 和 PC 打通先确认 ARP再打 UDP 流这里有个必须提前说清楚的坑verilog-ethernet 的udp_complete只处理 ARP 和 UDP不处理 ICMP。也就是说你的 PCping板子大概率是不通的而且不通并不代表工程有问题。很多新手在这里卡一整天反复检查代码其实代码完全正常。正确的验证顺序是这样的。先把 PC 网卡配成静态 IP比如192.168.1.100/24把 FPGA 侧的目的 IP 设成192.168.1.128两个地址在同一网段。然后先验证 ARP在 PC 上执行arp -d清掉缓存再执行ping 192.168.1.128虽然 ping 不会通但 ARP 请求会发出去如果 FPGA 的 ARP 模块正常响应PC 上的arp -a就能看到192.168.1.128对应的 MAC 地址。这一步过了说明物理链路、MAC 层、ARP 层全都正常。ARP 通了之后用 Python 写个最简单的 UDP 收发脚本就能测数据通路。PC 侧socket.socket(socket.AF_INET, socket.SOCK_DGRAM)绑定端口往192.168.1.128:1234发数据FPGA 侧收到后可以做个回环把收到的载荷原样发回源 IP 和源端口PC 侧就能收到。回环是验证 UDP 通路最快的方式因为发送和接收两条路径同时被验证了。如果要做吞吐测试可以用打流工具往 FPGA 灌 UDP 包注意要选 UDP 模式的测试命令里带-u参数带宽从低到高往上加。一开始别直接拉满先试 10Mbps看丢包率再逐步增加。观察到某个带宽点开始丢包那个点就是你的实际吞吐上限这个数据比任何理论计算都有说服力。# PC 侧启动 UDP 接收监听 5001 端口 iperf3 -s -u -p 5001 # 另一台设备作为发送端打 100Mbps 的 UDP 流持续 30 秒 iperf3 -c 192.168.1.128 -u -b 100M -t 30 -p 5001注意Windows 防火墙默认会拦截入站 UDP 包测试前先把对应端口的入站规则放开或者临时关闭防火墙测试。Linux 上如果用ufw也要检查。这个坑我踩过抓包能看到包发出去了但应用层收不到折腾半天才发现是防火墙。4. 踩坑实录常见故障与排查顺序4.1 链路起不来的排查链条链路不通是最常见也最让人抓狂的问题因为它没有任何报错。我的排查顺序是固定的从物理层往上一层一层确认不要跳。第一步看 PHY 的 link 指示灯。大多数板子上 PHY 芯片旁边有个 LED链路建立后会常亮或者闪烁。如果不亮先查网线、查 PC 网卡是不是也被识别成未连接。如果 PC 侧显示的是网络电缆被拔出那问题百分之百在物理层。第二步看rx_clk。用示波器或者 ILA 抓一下确认 PHY 输出了 125MHz 时钟。如果没有时钟说明 PHY 没有进入工作状态多半是自协商没完成或者硬件配置引脚不对。第三步看gmii_rx_dv上有没有流量。最简单的做法是在 PC 上持续ping板子然后抓gmii_rx_dv正常的化应该能看到周期性的脉冲。如果完全没有说明 PHY 没有把数据送进来。第四步看 MAC 层的 CRC 错误计数。如果gmii_rx_dv有脉冲但 CRC 大量报错那就是 RGMII 采样相位问题需要调 IDELAY。第五步看 ARP 有没有响应。ARP 通了说明从物理层到 ARP 层全链路正常问题就只可能在上层逻辑。这套顺序的价值在于每一步都有明确的观测点不会让你在到底是哪一层的问题里反复横跳。我见过太多人一上来就去改 UDP 发送逻辑结果发现根本是网线没插好。4.2 数据错位与丢包的几个高频原因链路通了之后问题往往从不通变成通但不对。最常见的是数据错位表现为收到的数据整体偏移一两个字节或者长度总是差几个。第一个嫌疑是tkeep。AXI-Stream 的最后一个 cycle 用tkeep指示哪些字节有效如果你没有正确设置接收端可能把无效字节也算进长度或者提前截断。检查方法是抓一次完整的传输看tlast那一拍tkeep的值是不是刚好覆盖有效字节数。在 8 位数据宽度下tkeep永远是 1 位没问题在 32 位宽度下tkeep是 4 位最后一拍如果只有 2 个有效字节tkeep应该是4b0011小端序或者4b1100大端序取决于实现填错就会多出两个垃圾字节。第二个嫌疑是字节序。以太网和 IP 层是大端序也就是网络字节序。你在用户逻辑里组装的源 IP、目的 IP如果是按32hC0A80180这种形式直接填注意它对应的就是192.168.1.128因为高位在前。但如果你用了 Verilog 的位拼接或者从内存里读数据很容易搞反。判断方法是抓包用 Wireshark 看源 IP 字段如果显示成128.1.168.192那就是字节序反了。第三个嫌疑是发送侧握手。s_udp_hdr_valid拉高之后必须一直保持到s_udp_hdr_ready有效中间不能撤销这是 AXI-Stream 的基本规则。如果你的状态机写得比较随意在 ready 没来的时候就把 valid 撤了会直接丢包。而且这个错误的表现为偶发丢包频率取决于两边时钟的相对速度非常难查。稳妥的做法是把 valid 和 data 一起在时钟沿更新用寄存器输出不要在组合逻辑里直接生成。4.3 时序收敛与时延问题在 125MHz 这个频率上大部分逻辑都能收敛但有两类地方容易出问题。一类是大的组合逻辑。比如校验和累加、大位宽的位拼接、长链路的优先级仲裁。工程里的模块本身就是流水化的但如果你自己在外面又加了一层组合逻辑很容易把关键路径拉长。解决办法是插入寄存器把长组合逻辑切成两级流水。另一类是跨时钟域。GMII 的 125MHz 和用户时钟如果不是同源就必须用异步 FIFO。用 FIFO 的时候要注意深度太浅会在突发流量下溢出表现为丢包太深浪费资源。一般 512 或 1024 深度的 FIFO 在 1Gbps 下够用。还有一类是 RGMII 的时序。这个比较特殊因为它是 DDR 接口时钟和数据之间有明确的相位关系。如果板子上的走线长度不理想或者 PHY 的延时配置不对采样点就会偏离。常见做法是在 FPGA 侧用 IDELAY 逐级扫描找到一个稳定窗口然后固化下来。这个过程有点像调收音机的频率你要在完全收不到和能收到但偶尔有噪声之间找一个最清晰的位置。花这个时间值得因为一旦调好后续所有问题都和它无关了。现象优先怀疑对象排查手段典型解决方式link 灯不亮网线、PHY 自协商换线、看 PC 网卡状态检查 PHY 配置引脚无 rx_clkPHY 未工作示波器测时钟引脚排查 PHY 供电与复位CRC 大量错误RGMII 采样相位ILA 抓 rx_dv 与数据调 IDELAY 延时值ping 不通但 ARP 有响应正常现象arp -a看表项用 UDP 脚本直接测偶发丢包AXI-Stream 握手违规ILA 抓 valid/ready寄存器化输出禁止中途撤 valid收到数据整体偏移tkeep 设置错误Wireshark 抓包比对按有效字节数正确置 keep高带宽下丢包异步 FIFO 溢出看 FIFO 的满标志加深 FIFO 或降速5. 跑通之后的二次开发方向5.1 提升吞吐的几种做法如果你只是跑了个回环可能感觉不出差异。但一旦你想用它传图像、传传感器数据流吞吐就成了核心指标。有几个方向可以优化。第一个是加宽数据通路。默认的DATA_WIDTH是 8 位配合 125MHz 刚好是 1Gbps这是线路速率的天花板理论上跑不满。如果把用户侧数据宽度加到 32 位用户时钟就能降到 31.25MHz逻辑时序压力小很多而且同样的数据量下握手次数减少四分之三。这个改动主要影响你自己的用户逻辑和跨时钟 FIFO 的宽度配置工程内部已经支持参数化。第二个是减少每包的额外开销。UDP 包如果很小每包都要走一次帧头、ARP 查询、校验和有效载荷占比很低。做大数据传输时把包做大接近 MTU 上限的 1472 字节效率会明显提升。不过要注意包越大中途出错的代价越高而且某些网络设备对超大帧支持不一致。第三个是流水化。发送侧的头部和载荷其实是分开握手的理论上可以做到上一包的载荷还在发下一包的头部已经准备好形成流水。实现这个需要你自己写一个发送控制器维护一个小队列。这个改动对吞吐提升非常明显但复杂度也上来了建议先把单包做实再考虑流水。第四个是确认接收侧不背压。如果你的用户逻辑处理不过来m_udp_hdr_ready长时间拉低上游 FIFO 就会溢出丢包就是这么来的。解决办法是在接收侧加一块缓冲 RAM先把数据落进去慢慢处理。FI FO 深度和 RAM 大小要根据你的实际数据速率算别凭感觉。5.2 往上接应用层把 UDP 变成可用的数据通道跑通收发之后下一步是让它真正服务于一个业务。这里要做的第一件事是设计一个简单的应用层协议。比如你要做一个图像传输系统可以在 UDP 载荷里定义自己的头部4 个字节的帧号2 个字节的分片序号2 个字节的分片总数然后是图像数据。接收侧根据帧号和分片序号重组图像丢了哪一片就通过 UDP 回一个重传请求。这套东西不复杂但能把 UDP 从能收发变成能可靠传输。另一个方向是做命令通道。PC 端发一个短 UDP 包给 FPGA里面带命令字和参数FPGA 解析后执行动作比如改 PWM 占空比、切换采样率、读取某个寄存器然后回一个应答包。这就是最朴素的控制协议很多工业设备内部就是这么干的。实现的时候注意命令和数据的端口要分开别混在一起否则解析逻辑会互相干扰。还有一点值得说就是接收过滤。m_udp_hdr_valid里带了源端口和目的端口你可以只对特定端口的数据做处理其他的直接丢弃。这样即使网络上有广播或者其他设备的流量也不会干扰你的业务。丢弃的动作也很简单把m_udp_hdr_ready拉高把头部收掉然后把m_udp_payload_axis_tready一直拉高把载荷吃掉就行不需要额外的复杂逻辑。我自己在这个工程上折腾了几轮之后最大的感受是它把网络协议栈从神秘的黑盒变成了看得见的流水线。你不再需要背协议字段只需要对着代码看信号怎么流动对照着 Wireshark 的抓包结果两边一比对一切就都对上了。剩下的调试工作无非是拿示波器、ILA 和抓包工具反复验证把一个模糊的不通拆成若干确定的观测点。这个过程走顺了再去看别的协议栈比如 CAN、USB、PCIe方法论是一样的先找分层再找握手最后找时序。这个套路我试下来比任何教程都管用。