news 2026/9/18 10:20:51

FPGA以太网UDP协议栈实战:verilog-ethernet仿真到上板

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FPGA以太网UDP协议栈实战:verilog-ethernet仿真到上板

折腾到第 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目录和 Makefile,Verilator 和 Icarus Verilog 都能跑,甚至提供了 cocotb 的 Python 测试。这意味着你在没板子的情况下,就能把整个数据通路跑通,看到 AXI-Stream 上的数据流一波波过去。对于从零起步的人来说,"先在仿真里看清楚,再上板调"这条路径能省掉大量盲猜时间。

注意:这个工程是社区开源项目,不同 commit 的目录结构和模块命名会有差异,有些版本把udp_complete.v拆成了独立的udp_ip_rx.vudp_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,含跨时钟 FIFO
ARP/IP/UDPudp_complete内部三层协议解析与组装
跨时钟axis_async_fifo125MHz 与用户时钟域隔离

2. 拆开 UDP 协议栈:帧结构、字段与模块对应关系

2.1 一个 UDP 包从外到内到底有哪几个字段要填

先建立一个数量概念。一个标准的 UDP over IPv4 over Ethernet 帧,从网线上看是这样的:

  • 前导码 7 字节,全是 0x55,SFD 1 字节 0xD5,这两个由 MAC 层自动生成,你不用管。
  • 目的 MAC 6 字节,源 MAC 6 字节,这是决定"发给谁、从哪来"的关键。
  • 类型/长度 2 字节,0x0800 表示后面是 IPv4,0x0806 表示 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,十六进制是 8'h11,这个值填错,对端直接当未知协议丢弃,连日志都不给你留。

还有一个校验和的坑。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上打数据、用tvalidtready握手、最后一个字节用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一般填16'h0800s_udp_ip_protocol8'h11s_udp_ip_ttl填 64 或 128 都行,传统习惯是 64。这些默认值在工程自带的例子里都能找到,照着抄一遍,再对着协议字段表核对一遍,比死记硬背有效得多。

2.3 校验和这块怎么处理才不拖后腿

UDP 校验和是很多人的第一个效率陷阱。它需要把伪首部(源 IP、目的 IP、保留字节 0、协议号 17、UDP 长度)、UDP 首部和用户数据全部按 16 位累加,再取反码。麻烦之处在于,UDP 长度在首部里,用户数据长度又不确定,所以要等整包数据都到齐才能算出最终校验和,但校验和字段本身又在首部里、在数据之前发出去。

这个工程的处理方式是"两遍走"或者用 FIFO 缓存整个载荷,先算完校验和再回头填首部。这在 1Gbps 下会造成额外的缓冲需求。如果你不需要严格校验,直接把s_udp_udp_checksum16'h0000,让接收方忽略,逻辑瞬间简单一大截。我在板子直连 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应该是4'b0011(小端序)或者4'b1100(大端序,取决于实现),填错就会多出两个垃圾字节。

第二个嫌疑是字节序。以太网和 IP 层是大端序,也就是网络字节序。你在用户逻辑里组装的源 IP、目的 IP,如果是按32'hC0A80180这种形式直接填,注意它对应的就是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,方法论是一样的:先找分层,再找握手,最后找时序。这个套路我试下来,比任何教程都管用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 10:18:32

Colibri:面向MoE模型的C语言级轻量推理引擎

1. 项目概述:Colibri 是什么,它解决的不是“跑模型”而是“让模型在真实设备上稳住”Colibri 这个名字乍一听像某种蜂鸟——轻盈、敏捷、高频振翅。这恰恰是它最核心的设计隐喻。它不是另一个大语言模型(LLM)本体,也不…

作者头像 李华
网站建设 2026/9/18 10:18:26

Jetson Xavier NX 刷 JetPack 5.1.7 到 NVMe

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 10:16:56

用Draw.io快速画出专业微服务架构图:分层布局与图层复用全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 10:16:55

Pandas+SQL+API+RAG:构建自动入库的知识库管道

手里攒了大半年数据,散落在各个业务接口和遗留数据库里,想给团队搭一个能直接提问的内部知识库。刚开始我也想过最简单粗暴的方案:写脚本把数据导成 CSV,再手动塞进现成的 RAG 工具。结果数据一多、更新一频繁,这条路根…

作者头像 李华
网站建设 2026/9/18 10:16:52

大模型写作技术:现状、挑战与优化策略

1. 大模型写作的现状与挑战去年我用GPT-4完成了一本专业书籍的初稿写作,整个过程既让我惊叹于大模型的潜力,也深刻体会到它的局限性。大模型写作正在重塑内容创作领域,但距离真正替代人类创作者还有很长的路要走。目前主流的大模型写作应用主…

作者头像 李华