ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

FPGA纯硬件UDP协议栈:verilog-ethernet以太网链路实战

FPGA纯硬件UDP协议栈:verilog-ethernet以太网链路实战 把 FPGA 的网络通路真正跑通是我这一年里最有成就感也最折磨人的一段经历。前面九篇笔记从工具链安装、第一个计数器、数码管动态扫描一路写过来全都停留在板子自己跟自己玩的阶段到了第十篇终于要碰一个有工程含金量的东西开源工程 verilog-ethernet 里那套纯硬件的 UDP 协议栈。这套代码的定位很明确把以太网 MAC、ARP、IP、UDP 四层全部用 Verilog 写进 FPGA 的时序逻辑里不依赖任何软核和操作系统收发路径上没有一纳秒浪费在软件中断和内存拷贝上。适合谁看如果你已经能跑通一个带时钟和复位的计数器工程看得懂 always (posedge clk) 块里在干什么那这篇就是写给你的如果你连 Vivado 的工程目录都还没建利索也别急着划走每个环节的为什么我都会摊开讲你按顺序抄作业一样能跑起来。1. 为什么值得把 verilog-ethernet 这套开源代码啃下来1.1 从点亮一个灯到跑通一条网络链路到底隔着什么点亮 LED 的工程本质上是一个分频器加一个输出寄存器你在仿真里看到波形翻转上板就能看到灯闪链路是闭环、可见、可预测的。网络完全是另一回事。第一个坎是时钟域GMII 接口上接收时钟和发送时钟各走各的 125MHz两边异步RGMII 更狠数据是 DDR 双边沿采样还带 1.5ns 到 2ns 的相位偏移要求。很多新手第一次综合 RGMII 工程时序报告一片飘红就是因为把 RGMII 当普通同步接口处理了。第二个坎是协议的分层握手。UDP 报文不是一个字节流那么简单它外面套着 IP 头、以太网帧头前面还有 7 个字节的前导码和 1 个字节的 SFD尾巴上挂着 4 字节 FCS。你的逻辑必须知道包从哪开始、头在哪结束、载荷有多长、校验对不对这些信息得在硬件状态机里被逐拍解析出来晚一拍、早一拍都会导致后面的数据错位。第三个坎最要命链路是隐形的。PC 端因为 ARP 没解析成功、子网掩码配错、网卡驱动把包扔了你在 FPGA 侧对着波形查一整天也查不出原因。所以这套工程真正的价值不只是提供了协议栈代码而是它把每一层的边界都用标准 AXI-Stream 接口切得干干净净出问题的时候你可以一层一层拿示波器式的思路去定位先看 PHY 有没有收到有效帧再看 ARP 有没有应答再看 IP 校验和过不过最后才看 UDP 端口和载荷。这种可分层定位的架构比手写一堆揉在一起的 always 块要值钱得多。1.2 工程目录的地图式读法我拿到这套代码的第一个动作不是编译而是把 rtl 目录按层次在心里画了一张图。底层是指向物理层的接口模块比如 axis_gmii_rx 和 axis_gmii_tx 负责把 8 位 GMII 总线上原始字节流整理成带 tvalid/tready 的 AXI-Stream再往上是 MAC 层eth_mac_1g 把收发路径和 FCS 计算、前导码生成封在一起eth_mac_1g_fifo 额外加了收发 FIFO专门用来解决跨时钟域问题中间层是 eth_axis_rx 和 eth_axis_tx负责把以太网帧的帧头、类型字段、FCS 错误标志翻译成通用信号再往上是 IP 层的 ip.v、ARP 层的 arp.v 和 arp_cache.v最顶上是 udp.v 和 udp_checksum_gen.v最后 udp_complete.v 把这些一块拼起来。读码顺序我建议反着来先看 udp.v只看它的端口列表和状态机跳转搞明白一个 UDP 包进来之后模块对外吐出了什么然后往上追一层看 ip.v 怎么把 UDP 段包进 IP 头再往上追 arp.v最后才回头看 MAC 和 PHY。原因很简单越靠上的模块越贴近你的业务逻辑越靠下的模块越偏向物理电气特性而物理层的坑大部分已经被作者踩平了你做二次开发时改动最少的就是那一层。提示这份工程在持续迭代模块名和参数名在不同 commit 之间可能略有出入。我下面提到的具体参数名以我手上这份为例你 clone 下来之后先对着端口声明和 parameter 列表过一遍别直接照抄。1.3 纯硬件 UDP 还是软核跑 lwIP这笔账怎么算刚入门的时候我脑子里全是上个软核跑个 lwIP写 C 代码不香吗。真上手做过之后我的结论是看你的指标要求。把几种常见方案摆在一起对比取舍就非常清楚了。方案典型吞吐逻辑与存储占用开发门槛适用场景软核 裸机 自制简易收发10 到 100 Mbps中等软核本身就要占逻辑和 BRAM中要写 C设备配置、命令下发软核 lwIP100 Mbps 到 1 Gbps偏大需要较多 BRAM 和片内 RAM偏高协议栈调试成本大需要完整 TCP/IP 的场合纯硬件 UDPverilog-ethernet接近线速偏小几千 LUT 加几块 BRAM前期陡峭后期省心高速采集、低延迟控制厂商 MAC IP 自研 UDP 层线速小高要啃厂商文档量产产品选择纯硬件 UDP 的核心逻辑在于UDP 是无连接协议没有握手、没有重传、没有滑动窗口硬件上只需要一个解析头部 搬运载荷的状态机外加一个校验和累加器就够了。相比之下 TCP 的状态机复杂度高出好几倍重传定时器、拥塞窗口这些逻辑用纯硬件实现起来非常难受。但这笔账的另一面是UDP 没有流控和重传包丢了就是丢了你必须在上层自己设计序列号、确认机制和限速逻辑。我在做图像传输的时候就是因为没做限速一口气灌满千兆PC 端网卡缓冲区顶不住丢包率直接飙到三成后来在发送侧加了一个简单的令牌桶才稳下来。2. UDP 协议栈的硬件架构拆解一个包是怎样被层层剥开的2.1 从 PHY 接口到 MAC原始字节流是怎么被接住的以太网线进来的是一串差分信号PHY 芯片把它还原成 GMII 上的 8 位数据和两个控制信号接收侧数据在 rx_clk 的上升沿更新并且只在 rx_dv 有效的时候才是真数据。RGMII 是它的精简版数据线和控制线都复用成 DDR同样 125MHz 的时钟下每拍传两位。这一层的核心任务其实只有两件事找帧头和算 FCS。找帧头的逻辑作者是用一个滑动窗口去匹配前导码 0x55 重复七次之后紧跟的 0xD5。在硬件里这通常写成移位寄存器加比较器一旦匹配上就进入接收状态开始把后续字节往 AXI-Stream 上推。这里有个容易忽略的细节前导码和 SFD 一共 8 个字节它们不应该出现在你的 AXI-Stream 数据里而是应该被消化掉。我第一次看波形的时候还纳闷为什么数据流开头少了 8 个字节后来翻代码才发现是故意丢掉的这是标准行为。FCS 是 CRC-32生成多项式是 0x04C11DB7 的反转形式。发送侧要为每个帧计算并追加 4 字节接收侧要把最后 4 字节剥掉并校验。这个校验在硬件里就是一个 32 位线性反馈移位寄存器每个时钟周期吃一个字节不需要任何乘法器。我手上这份代码里接收 FCS 校验是通过一个参数开关的打开之后会多出一个错误指示输出我在调试阶段一定会把它打开然后接一个计数器挂到 LED 或 ILA 上看错包数量这是判断物理链路质量好不好最直接的手段。注意MAC 层通常还会关心帧长下限。标准以太网帧载荷不足 46 字节时要补齐到 64 字节总长很多实现把这个补齐动作放在发送路径的用户侧或者 MAC 内部的参数里。如果你发一个小包发现对方收不到先怀疑是不是没补齐、帧长小于 64 被接收端当异常帧丢了。2.2 IP 层与 ARP最容易被跳过也最容易卡住的一层ARP 这个名字听着像配角实际上是新人最常摔倒的地方。你的 FPGA 上电之后想给 PC 发一个 UDP 包它手里只有目标 IP 地址没有目标 MAC 地址以太网帧根本拼不出来。ARP 协议就是干这个的广播一个谁是这个 IP的请求对方回一个我是我的 MAC 是 XX的应答然后你把这条映射关系缓存起来。arp_cache 这个模块做的事情就是维护这张表。它对外给了几组配置端口让你告诉它本机的 MAC、本机的 IP、网关 IP 和子网掩码同时它内部有一个缓存的请求队列。这个设计比较贴心的地方在于如果你的发送请求命中了一个还没解析的 IP它会把请求先排队等 ARP 应答回来之后再自动重发出去不需要你自己在应用层写重试逻辑。配置项作用配错的典型症状local_mac本机 MAC 地址自己发的包 PC 全当垃圾丢抓包能看到帧但协议栈不认local_ip本机 IP 地址ARP 请求能收但不应答PC 显示无法访问目标主机gateway_ip网关地址同网段通信正常跨网段全丢subnet_mask子网掩码判断对端是否同网段出错走错发送路径IP 层本身做的事情相对机械验证收到的 IP 头校验和、判断目标 IP 是不是发给自己或广播地址、按协议号把载荷分发给上层。发送侧则要填好版本号、头长度、总长度、标识、分片标志、生存时间、协议号、源和目的 IP然后算校验和。IP 头校验和的算法是16 位反码求和再取反硬件实现里就是一个 16 位累加器加一个进位回卷。我拿一个真实的 20 字节头举个例子字节序列是 45 00 00 3C 1C 46 40 00 40 11 00 00 C0 A8 00 01 C0 A8 00 02把它按 16 位分组相加注意校验和字段本身先填 00x4500 0x003C 0x453C加上 0x1C46 得 0x6182加上 0x4000 得 0xA182加上 0x4011 得 0xE193加上 0x0000 得 0xE193加上 0xC0A8 得 0x1A23B超过 16 位把高位的 1 回卷到低位得 0xA23C加上 0x0001 得 0xA23D加上 0xC0A8 得 0x162E5回卷得 0x62E6加上 0x0002 得 0x62E8总和是 0x62E8取反得到 0x9D17这就是应该填进校验和字段的值。硬件上这个回卷不需要像纸上这样一步步做通常是先累加到一个更宽的寄存器里比如 20 位最后一次性把高位折叠回来这样每个时钟周期只需要一次加法时序压力小得多。2.3 UDP 层头部解析、端口过滤和校验和的那些坑UDP 的头部只有 8 个字节源端口、目的端口、长度、校验和各两个字节理论上是最简单的一层但坑一点都不少。最容易踩的是长度字段的语义它包含 UDP 头本身也就是载荷长度加 8。我见过不止一个同学在这里翻车把长度写成纯载荷长度结果对面解析出来的载荷凭空多出 8 个字节全是脏数据。UDP 校验和比 IP 校验和麻烦因为它要算一个伪头部。伪头部不是真正传输的字节只是参与计算包含源 IP 四字节、目的 IP 四字节、一个字节的零、一个字节的协议号UDP 是 17、两字节的 UDP 长度。把伪头部、UDP 头和载荷一起按 16 位反码求和再取反就是校验和。伪头部字段长度值源 IP 地址4 字节与 IP 头中的源地址一致目的 IP 地址4 字节与 IP 头中的目的地址一致保留字节1 字节固定 0x00协议号1 字节固定 0x11也就是 17UDP 长度2 字节头部 8 字节加载荷长度与 UDP 头里的长度字段一致这里有一个非常经典的规范细节很多自己写协议栈的人根本不知道UDP 校验和算出来如果是 0x0000发送时必须填成 0xFFFF。原因是校验和字段本身等于 0 有特殊含义表示发送方没有计算校验和接收方可以选择忽略校验。而计算结果是 0和没有计算这两种情况如果不区分开接收端的行为就没法定义了。硬件实现里这只是一个判断加一个多路选择器但如果你漏了就会出现千分之一的概率对面拒收而且这种 bug 极难复现。提示如果你的场景是 FPGA 到 PC 单向传输且对可靠性要求不高可以考虑把 UDP 校验和关闭接收侧直接忽略校验字段。代价是失去了对错误数据的基本筛查物理层的误码会直接传导到业务层所以只建议在实验室环境下用。2.4 AXI-Stream 握手和背压整条链路的呼吸机制整套代码的接口风格统一是 AXI-Stream核心握手规则只有一句话数据在 tvalid 和 tready 同时为高的那个时钟沿被传输。发端拉高 tvalid 表示我有数据收端拉高 tready 表示我能收两者同时高才算一次成功传输。这套机制最妙的地方是背压天然可组合下游处理不过来的时候tready 拉低压力会一层一层往上游传最后传到你的应用逻辑你只要保证自己不在 tready 为低的时候改数据就行。数据通路上还有几个辅助信号要理解。tlast 标记一个包的最后一个数据拍接收侧必须靠它来划分包边界tkeep 只有在位宽大于 8 位的时候才有意义用来标记最后一个拍里哪几个字节是有效的UDP 长度不是 8 的整数倍时全靠它tuser 的语义在不同模块里不一致有些地方用来标记错误用之前一定要看模块的端口注释我在这一点上吃过亏想当然地以为它是首字节标志结果逻辑全是坏的。verilog-ethernet 的 UDP 模块在接口设计上有个很值得学习的巧思它把包头信息和载荷流分开了。rx_udp_hdr_valid 和 rx_udp_hdr_ready 单独走一次握手握手成功的那一拍总线上同时呈现出源端口、目的端口、源 IP、目的 IP、UDP 长度等全部头部字段紧接着载荷才通过另一个 AXI-Stream 出来。这么做的好处是你的应用逻辑可以先看端口和 IP 决定要不要这个包如果不要直接不给载荷通道的 tready或者干脆把流引到垃圾桶里丢掉不需要先接收完再判断省了很多缓冲。我第一次看懂这个设计时脑子里哦了一声这确实是硬件工程师才会想出来的接口。3. 动手实操把例子工程跑通并联通 PC3.1 仿真先行不上板子也能验证协议逻辑我的习惯是任何网络相关的东西先在仿真里跑通再看板子因为板子上出问题你只能靠 ILA 抓有限的信号而仿真里你可以看任何信号、随时暂停、反复重跑。这份工程带了基于 cocotb 的测试环境配套的 Python 生态里有专门的以太网和 AXI 扩展包能直接在 Python 里构造以太网帧、UDP 包然后喂给被测模块再断言输出的包长什么样。这套东西配置起来需要装 Python 环境和仿真器看起来比点开 Vivado 麻烦但收益非常高。仿真里我重点关注四个握手点缺一个都可能白跑。第一是 PHY 接口的接收侧看 tvalid 拉高的时候 tdata 上的字节序对不对第一个有效字节是不是应该是目的 MAC 的第一个字节。第二是 MAC 层的 tlast一个包结束之后 tlast 是不是恰好拉高了一拍拉高多了说明包边界切错了。第三是 UDP 头握手的时序hdr_valid 和 payload 的第一个 tvalid 之间不能有额外的空拍否则有些实现会认为头部和载荷不对应。第四是校验和把模块算出来的校验和和 Python 里参考实现算出来的值对比任何一个字节能对上你后面上板就有八成的把握。3.2 时钟、复位和跨时钟域这一层不处理干净后面全是玄学GMII 接口上接收时钟 rx_clk 和发送时钟 tx_clk 来自 PHY标称都是 125MHz但它们是两个独立的时钟源相位关系完全不确定。你的应用逻辑如果只用其中一个时钟就必须在另一条路径上做跨时钟域处理。作者提供的做法是用带 FIFO 的 MAC 封装收发各挂一个异步 FIFO一侧接 PHY 时钟另一侧接你的应用时钟天然把两个域隔开。这个思路比自己在外面手写三级同步器要靠谱得多因为数据总线跨时钟域需要的是 FIFO 或者握手单纯的打三拍只适用于单比特控制信号。复位这块我踩过的坑值得单独说。FPGA 的复位信号如果直接来自一个按键或者外部引脚它跟内部时钟是异步的直接拿它去复位一大堆寄存器很容易出现一部分寄存器复位了、另一部分没有的复位释放不同步现象表现为上电之后时序逻辑偶尔跑飞、重下比特流又好了。标准做法是用一个复位同步器把异步复位信号在两个时钟域里各同步一次保证每个域内的寄存器在同一个时钟沿看到复位释放。代码不长但是这个小小的模块决定了系统上电之后能不能稳定工作我在这个上面浪费过整整两天。跨域类型推荐做法反面教材单比特电平信号两级或三级同步器直接跨域使用输出抖动单比特脉冲信号脉冲展宽加同步器或握手直接同步脉冲丢失多位数据总线异步 FIFO 或格雷码计数每比特各打三拍数据错位复位信号复位同步器每域一份全局异步复位释放不同步3.3 约束、引脚和上板时序报告要一个字一个字看上板之前有两件事必须做扎实引脚约束和时钟约束。引脚约束就是告诉工具哪个信号连到哪个管脚电平标准是什么这一点在开发板原理图上都能查到。时钟约束是新手最容易糊弄过去的部分很多人写了一个 create_clock 就完事但实际上你还得告诉工具输入输出的时序要求比如 GMII 接收侧的建立保持时间窗口、输出侧的延迟范围。约束写得不完整工具会默认这些路径我不管综合布线能过上板就是偶发错误而且你很难查。RGMII 接口更麻烦因为它是 DDR 采样而且通常要求 PHY 侧或者 FPGA 侧做 1.5ns 到 2ns 的时钟数据相位偏移。有些 PHY 芯片支持内部延迟可以配置成FPGA 输出对齐时钟PHY 自己延迟有些则要求 FPGA 内部用 IDELAY 原语做精细调整。这块我建议直接参考你开发板厂商的示例工程别自己从零推它跟具体 PHY 型号强相关通用性很低。上板之后我第一件事是看几个指示灯PHY 链路是否 up、接收错包计数器是否在涨、ARP 是否已经解析到 PC 的 MAC。这几个信号我一般都会从内部引到 LED 上虽然土但是调试效率极高比每次开 ILA 快多了。3.4 PC 侧联调一半的问题其实出在电脑上FPGA 侧搞定了接下来要把 PC 配好。我的做法是给 PC 的网口配一个同网段的静态 IP比如 FPGA 是 192.168.0.10PC 配 192.168.0.100子网掩码 255.255.255.0然后先 ping。ping 这个过程其实同时在验证两件事ARP 能不能解析ICMP 能不能应答。如果你的设计里没有实现 ICMP 应答ping 不通是正常的那就改用抓包或者直接看 ARP 缓存。Wireshark 是必开的神器。抓包的时候我最常看三个地方有没有 ARP 请求发出来、有没有 ARP 应答回去、UDP 包的 IP 和 UDP 校验和是不是被标成错误。Wireshark 会直接把校验和错的包标红这是验证你校验和实现最快的办法根本不用自己算。打流测试方面很多人会想到 iperf3。这里要说清楚一个前提iperf3 的 UDP 模式需要两端都有 iperf3 进程互相通信而 FPGA 里跑的是你自己的逻辑不是 iperf3 服务端。所以用 iperf3 往 FPGA 灌包你只能看到 PC 侧发了多少看不到对面收了多少。我更推荐自己写一个几十行的 Python socket 脚本带序列号和时间戳发出去FPGA 侧把收到的包原样回传或者只回传统计信息这样丢包率、吞吐、乱序情况你全都能算出来比 iperf3 可控得多。iperf3 更适合用来验证 PC 到交换机这一段链路本身有没有问题作为对照实验。4. 二次开发把协议栈接进你自己的数据通路4.1 一个能跑的最小收发逻辑长什么样把这套协议栈用起来最核心的概念是两个状态机在跑协议栈自己有一个接收状态机在解析包你的应用侧需要一个状态机去响应包头。我写过一个最短的应答逻辑思路是这样的等待 rx_udp_hdr_valid 拉高一旦拉高就把源 IP、源端口、目的端口全部锁存下来同时判断目的端口对不对不对就把接下来的载荷全部丢弃对的话拉高 tx_udp_hdr_valid 到发送模块把目的 IP 设成刚才锁存的源 IP、目的端口设成刚才的源端口然后等发送侧的载荷通道 ready 之后把接收到的载荷原样转发过去。这个先握手头、再搬载荷的流程是这套架构的精髓也是最容易写错的地方。常见的错误是忘记处理头握手了但载荷通道没准备好这种情况导致载荷丢字节。另一个错误是在载荷转发的时候没有正确处理 tlast回包长度和声明长度不一致对面直接丢弃。我的经验是接收和发送这两条流之间一定要加一级 FIFO哪怕深度只有几十因为它能把两边的握手解耦开你就不用小心翼翼地保证两边时序完全咬合。4.2 端口过滤、广播和组播的处理方式端口过滤是最简单也最实用的优化。UDP 的头部独立握手给了你一个天然的分流点在 hdr_valid 那一拍判断 rx_udp_dest_port不匹配就直接把后续载荷流丢弃。这样无关的广播包、扫描包根本不会进到你的业务逻辑也就不会占用你的缓冲资源。我在做多路数据采集的时候就是用不同端口区分不同的数据流一个端口一路应用逻辑简单得像写 if-else。广播包的处理需要额外注意。目的 MAC 是 FF:FF:FF:FF:FF:FF 的帧大部分 MAC 实现都会接收ARP 请求就是靠广播发的。如果你的 MAC 层开了只接收目的地址匹配的帧这种过滤ARP 就废了因为它永远收不到广播请求。组播更复杂因为组播 MAC 地址到 IP 组播地址的映射是多对一的标准做法是在 MAC 层维护一张哈希表你加入某个组播组就往表里写一条。这份工程里对组播的支持程度跟具体模块版本有关如果要做组播应用建议先把这块的代码通读一遍再动手。4.3 资源、时序和吞吐三者之间的取舍综合报告出来之后你大概会看到这个量级一套完整的千兆 UDP 协议栈LUT 用量在几千这个数量级寄存器数量相近BRAM 主要被收发 FIFO 吃掉。如果 FIFO 深度设成 4096、位宽 8 位那就是 32K 比特差不多正好一块 36Kb 的 BRAM收发各一个就是两块。这个开销在大部分中低端器件上完全可以接受但如果你的板子还要跑别的重负载逻辑就要考虑把 FIFO 深度降下来。深度降下来是有代价的。以太网的帧可能一下子来得很密FIFO 太浅下游稍微卡一下就会溢出丢包。我的经验值是如果你的应用逻辑每包的处理时间不超过几十个时钟周期深度 512 到 1024 就够了如果要做复杂的后处理、要跨模块协调那就老老实实给 4096。另一个思路是用更宽的位宽换取更浅的深度比如用 32 位数据通路同样深度下能缓存的包数是一样的但每拍搬运的字节数变成四倍对时序的要求也更高。至于时序收敛最大的风险点通常是校验和计算路径和 MAC 层那个 32 位 CRC。校验和那个累加器如果做成组合逻辑一路加到输出路径会很长常见做法是流水线化把累加分两三级做代价是引入延迟需要在状态机上补偿。CRC 那条路径工具一般能自动处理好如果真的收不上去就考虑把接收路径的位宽从 8 位拆开或者降低时钟频率看看是不是组合逻辑的问题。5. 常见问题与排查技巧实录5.1 收不到包的时候我按这个顺序往下查第一层看物理。PHY 芯片的链路指示灯亮不亮如果是紫光或者常暗说明自协商没成功大概率是网线、PHY 配置或者时钟没给对。这一层不用看代码先看硬件。第二层看接收错包计数器。如果接收 FCS 错误计数一直在涨说明有帧进来了但 CRC 对不上问题在物理层或者采样时序上跟协议层无关。如果计数是零说明根本没有有效帧进来要么是前导码匹配逻辑没跑起来要么是接收时钟压根没在跳。第三层看 ARP。如果 FCS 计数正常但 PC 上 arp -a 看不到 FPGA 的条目说明你的 ARP 模块没应答检查本机 IP 和 MAC 配置。ARP 通了链路层就没问题了。第四层看 IP 和 UDP。用 Wireshark 抓包看发出的包有没有被标成校验和错误。如果是就是你校验和算错了如果不是但对面应用程序收不到就是端口号错了或者长度字段错了。第五层才看 UDP 载荷。前面全通过了载荷还是不对那问题一定在 tlast、tkeep 或者数据位序上这几个信号在仿真里最容易验证在板子上最难验证。5.2 常见问题速查表现象最可能的原因排查动作PC ping 不通 FPGAARP 未应答或 ICMP 未实现抓包看有无 ARP 应答或直接看 ARP 缓存能收到包但 Wireshark 标红IP 或 UDP 校验和计算错误逐字节对比参考实现算出的校验和大包正常小包丢失未做帧长补齐帧长小于 64 字节检查发送侧的补齐参数是否打开偶发丢包重传可恢复跨时钟域处理不当或 FIFO 溢出检查 CDC 实现加大 FIFO 深度上电后行为随机复位释放不同步加复位同步器每个时钟域一份发送吞吐远低于预期握手反压传递效率低或每包间隔太大检查状态机是否有冗余空拍高负载下丢包严重无流控PC 接收缓冲溢出应用层加限速或加确认重传机制注意这张表里的现象和原因不是一一对应的比如偶发丢包可能是 CDC 也可能是 FIFO 溢出还可能是物理链路误码。我的做法是每次只改一个变量改完重跑同一组测试这样才能确定是哪一条起了作用。5.3 我在这套代码上踩过的坑第一个坑是关于数据位序的。以太网是字节流但你的系统内部可能习惯用 32 位宽的数据通路这时候大端小端的转换必须在某一层做掉而且只能做一次。我在一开始同时修改了两个模块的位序结果字节被翻转了两次等于没翻调试了半天才反应过来。第二个坑是关于仿真的时钟模型。我早期图省事在 testbench 里让接收和发送用同一个时钟仿真里一切正常上板就各种诡异。后来改成两个独立时钟、随机相位差问题立刻复现了。这件事给我留下了很深的印象仿真环境的真实程度直接决定了它能不能发现真问题。第三个坑是关于 tready 的处理。我在接收侧写了一个简单的丢弃逻辑做法是在不需要这个包的时候一直不给 tready结果发现上游完全卡死了因为背压一路传到了 MAC 的接收 FIFOFIFO 满了之后新的帧进不来整个接收通路瘫痪。正确做法是照常给 tready 把数据收下来丢掉或者用专门的流终止逻辑。这个小细节在文档里很少被强调但实际写业务逻辑时几乎一定会遇到。第四个坑是关于工具版本的。同一份代码在不同版本的开发工具下综合出来的结果可能不一样我遇到过某次升级之后时序突然收不上去了原因是工具换了默认的综合策略。遇到这种情况别急着改代码先把综合策略调回上一版的设置试试。最后分享一个我个人觉得很有用的小技巧在协议栈的每一个层次边界上都挂一个小的计数器组分别统计收到多少帧、多少帧 FCS 错、多少帧 IP 校验错、多少帧 UDP 端口不匹配、多少包进了应用层。这五组计数器通过一个简单的状态机上传到 PC你随时可以看一眼各层的数据漏斗哪一层掉得最厉害一目了然。这套东西写起来不到两百行但它在排查问题时省下的时间比我写过的任何调试代码都要多。
返回列表