前面九篇我们一直在 Verilog 语法、仿真流程、时序约束这些基础里打转练习工程也就做到点灯、跑个计数状态机这个量级。写到第 10 篇我打算换个玩法不再自己从零堆以太网 MAC也不再手写 UDP 协议栈而是直接找一个成熟的开源工程来读——verilog-ethernet。这个工程在 FPGA 圈子里算是老熟人了1G、10G、25G 的以太网 MAC加上 IP、ARP、UDP 这些协议层都在里面写的是纯 Verilog接口统一走 AXI Stream。我第一次把它跑通是在一块 Artix-7 开发板上从拉代码到 PC 端能 ping 通、能用 UDP 收发数据前后大概折腾了三个晚上。这篇不是十分钟上手的爽文。我更想把读这套代码时遇到的岔路、踩过的坑、以及那些文档里不会写的判断依据一条条摊开讲。假设你现在的水平是Verilog 能看懂 always 块知道什么是时钟和复位仿真跑过几个小模块但没碰过以太网也没系统学过 AXI 协议——这个起点完全够用因为这件事考的是能不能读懂别人写的代码而不是能不能从零设计一个协议栈。读完你应该能做到四件事把仓库跑起来、看懂 UDP 这条数据通路、知道参数该动哪几个、上板时提前避开那几个固定的坑。1. 先算一笔账手搓以太网发送逻辑到底要填多少坑1.1 一个看起来能发 UDP 包的最小实现实际要处理多少件事前几篇有朋友问过我说想自己写个以太网发送模块把数据从 FPGA 发到 PC。我给的建议很直接如果只是想验证链路能不能通别写如果是想学协议细节写完再说。原因是一个能发 UDP 包的最小实现至少要正确处理下面这些事情前导码和 SFD7 个 0x55 加 1 个 0xD5、以太网帧头目的 MAC、源 MAC、类型字段、IP 首部版本、首部长度、总长度、标识、TTL、协议号、首部校验和、UDP 首部源端口、目的端口、长度、校验和、帧尾的 FCSCRC32、帧间最小间隔、以及发送时钟和 PHY 接口之间的时序关系。这里面每一项单独看都不难难的是它们串在一起。CRC32 少算一个字节、长度字段写错一个字节、帧间隙少等几个时钟现象全都一样——PC 那边什么都收不到而你在示波器上看到的波形却很正常。我最早那版实现里CRC 用的是 MSB-first 的 LFSR 写法只要把字节处理顺序搞反收到的包就全是 FCS 错误Wireshark 里直接标红。那个下午我基本是在数校验和里度过的而且是在完全没有对照物的情况下数。还有一类坑更阴间最短帧的长度限制。以太网规定去掉 FCS 后最小帧是 60 字节扣掉 14 字节帧头和 20 字节 IP 首部、8 字节 UDP 首部剩给 UDP 载荷的只有 18 字节。也就是说如果你发一个 4 字节的 UDP 包载荷必须补齐到 18 字节。这一条如果在自己的实现里忘了抓包会看到 runt 帧被丢弃现象是小包发不出去大包反而正常特别容易让人以为是逻辑代码写错了。1.2 verilog-ethernet 把这些活接走了但它不是黑盒这个开源工程最大的价值是把上面那张清单里和协议相关、但和你的业务无关的部分全部接管了。你面对它的时候接口已经退化成一根 AXI Stream 加一组头部信号把载荷按字节塞进去前导码、SFD、FCS、帧间隙、CRC 计算全部由 MAC 层处理。所以有一条经验必须记住——在 AXI Stream 这一层绝对不要再自己算一遍 CRC算了反而会多发 4 个字节出去PC 端直接判定长度异常。它的模块分层大致是这样的最底下是面向 PHY 的接口转换比如 axis_gmii_rx / axis_gmii_tx 负责把 GMII 上的 8 位数据和 dv / er 信号转成 AXI Stream中间是 eth_mac_1g、eth_mac_10g 这类 MAC 本体负责帧封装、CRC、帧间隙和统计计数再往上是 eth_axis_rx / eth_axis_tx把 MAC 的流拆出以太网头部最顶上才是 ip_complete、udp_complete 这种协议栈层。命名规律其实很好记axis_ 开头的是接口转换eth_ 开头的是以太网层ip_ / udp_ / arp_ 开头的是网络层和传输层。每个模块基本都是参数化的而且大部分都提供带 FIFO和不带 FIFO两个版本比如 eth_mac_1g 和 eth_mac_1g_fifo 就是一对。这个设计很实用不带 FIFO 的版本面积小、延迟低适合上游已经有稳定时序的场景带 FIFO 的版本自带跨时钟域缓冲省得你自己做。我现在的习惯是只要上游数据源不是稳定连续输出的就直接用带 FIFO 的版本省下来的调试时间远比省下来的那点逻辑资源值钱。1.3 别漏掉它依赖的 verilog-axis这是新手最容易翻车的一步verilog-ethernet 不是自包含工程它依赖同一作者的另一个仓库verilog-axis里面是 AXI Stream 的基础件——FIFO、仲裁器、位宽转换、广播、分支、开关等等。verilog-ethernet 自己的 rtl 目录里主要放协议相关模块那些通用的流处理组件是借用 verilog-axis 的。实际操作上你要把两个仓库下载下来放在同一个父目录下综合或者仿真脚本里的文件路径按相对路径去找。目录层级一旦错位工具就会满屏报 module not found而且报的模块名往往是你根本没听说过的比如 axis_fifo很容易让人以为是仓库缺文件。我第一次就中招了对着报错找了一圈最后发现是我把两个仓库放成了嵌套目录。顺便提一句这类开源工程一般用比较宽松的开源许可学习和自用都没问题但你要把它塞进商业产品之前最好自己把许可条款读一遍尤其是对版权声明保留的要求。这不是小事我见过有团队直接把这套代码封进产品里连文件头注释都删掉了这种操作在合规审查时是要出问题的。2. 从把代码拉下来到波形跑起来一个晚上能做完的准备工作2.1 先把目录结构认全别急着改代码打开仓库你会看到这几个目录认路时间花十分钟后面省几个小时rtl/所有可综合的 Verilog 源码基本一个模块一个文件文件名和模块名一致找代码很方便。test/仿真测试用的是 cocotb 框架每个模块配一个 Python 测试脚本测试用例写得挺细。syn/综合脚本针对不同厂商的工具分别准备比如 Quartus 和 Vivado 各一份。根目录下是 README、LICENSE以及一些配置和说明文件。这里我得强调一句README 一定要先读。它列出了所有模块以及关键参数说明这份清单的价值在于你能一眼看出整个工程提供了哪些能力而不是靠翻文件去猜。我第一次就是跳过 README 直接看代码结果在 udp_complete 里绕了很久回头才发现 README 里已经把模块层次图画清楚了。2.2 工具链准备iverilog 加 cocotb 是最省事的组合仓库自带的测试是用 cocotb 驱动的底层仿真器通常用 Icarus Verilog。这套组合的好处是完全免费、跨平台、部署快缺点也明显——iverilog 对较新的 SystemVerilog 语法支持有限跑比较大的设计会比较慢。所以它的定位是功能验证不适合跑长数据流压测。安装步骤不复杂iverilog 用系统包管理器装就行Windows 上建议走 MSYS2 或者干脆用 WSLcocotb 用 pip 装到一个独立的虚拟环境里。这里有个非常现实的坑cocotb 的版本兼容性。仓库里的测试脚本如果写得比较早可能还在用一些后来被移除或改名的接口比如旧版里的 fork 类调用新版里就换成别的写法了。你装了最新版 cocotb 直接跑很可能第一行就 AttributeError。我的处理方式是给这个工程单独建一个 venv装一个和仓库年代接近的 cocotb 版本实在不行就把测试脚本里的旧接口临时改一下。千万别图省事用全局 Python 环境我上次把系统 Python 搞乱之后花了半天收拾现场——这种事一次就够了。2.3 单个模块怎么单独跑起来不要一上来就想着跑通整个 UDP 协议栈那个测试链条太长出错时你根本不知道是哪一层的问题。我的顺序是先跑 eth_mac_1g 自己的测试确认 MAC 层收发正常再分别跑 ip、udp、arp 这些单模块测试最后才碰 udp_complete 这种集成测试。这个由下往上的顺序本质上是在建立每一层的输入输出我都见过的直觉。进入 test 目录里面通常有个 Makefile执行 make 就会用 cocotb 起仿真。想只跑某一个模块直接在命令行指定对应的测试文件就行。仿真过程会写出波形文件用 GTKWave 打开就能看。这里给个很具体的建议第一次跑通之后别急着往下走先把波形打开盯着 tvalid / tready / tlast / tkeep 这四组信号完整地看一遍一次收发握手是怎么完成的。看波形十分钟比读十页 AXI 协议文档管用得多因为它把概念变成了时序。2.4 仿真跑不起来先查这四处按我的经验八成的问题落在这四个地方模块找不到几乎都是 verilog-axis 没放对位置或者 Makefile 里的文件列表没包含它。先确认路径再确认文件列表。Python 报导入错误cocotb 版本问题或者虚拟环境没激活先 pip list 看一眼再确认当前 shell 用的是不是一个环境。编译阶段语法报错多半是 iverilog 不认某些写法把报错位置和仓库里的写法对一下必要时换个仿真器验证。能跑完但结果全错先确认仿真时间够不够、时钟有没有起振、复位是不是一直没放开。很多测试里复位是低电平有效搞反了就是全程不工作。这四条看起来很基础但实际排查中它们覆盖了绝大多数情况。我习惯的做法是把 test 目录下的 Makefile 复制一份改成自己的这样既能保留原版对照又能放心改路径和参数。3. 顺着 UDP 这条通路走一遍从 GMII 引脚到 AXI Stream 载荷3.1 接收方向三个模块是怎么串起来的接收方向的链路从 PHY 引脚开始往下走是这样一串axis_gmii_rx 把 GMII 信号转成 AXI Stream喂给 eth_mac_1gMAC 层处理完以后由 eth_axis_rx 拆出以太网头部和载荷再按类型字段分流到 ip_complete 或者 udp_complete。先说 axis_gmii_rx它做的事非常单一把 GMII 接口上跟着 rx_clk 同步的 rxd[7:0]、rx_dv、rx_er 攒成 AXI Stream。收到错误标记时它会做相应传递rx_dv 拉低时它会拉高 tlast 作为帧结束。这一步最需要关注的是时钟GMII 的接收时钟是 PHY 提供的 125MHz这个模块的输出天然落在一个外来时钟域里后面必须做跨时钟域处理否则亚稳态迟早教你做人。很多人第一次上板发现偶发丢包、重启就好追下去基本都是这里。接着是 eth_mac_1g。它在接收方向会做 CRC 校验、剥掉前导码和 FCS吐出一帧干净的 AXI Stream长度异常的帧会被丢弃并计数。这时候统计计数就派上大用场了——上板调试时看错误计数是不是在涨比盯着波形猜要高效得多。我自己就吃过这个亏有一次死活找不到问题最后接上统计输出才发现 CRC 错误数一直在涨方向立刻就明确了。3.2 eth_axis_rx 拆出来的 eth_hdr 到底是什么eth_axis_rx 输出的头部就三个字段目的 MAC48 位、源 MAC48 位、以太网类型16 位。它用一对 valid / ready 握手告诉你头部准备好了同时把后面的载荷从 AXI Stream 上推出来。这个设计的妙处在于头部信息只在握手那一拍有效模块内部会自己寄存你只需要接住就行。以太网类型是最关键的一个字段0x0800 是 IPv40x0806 是 ARP0x86DD 是 IPv6。你要做的分流基本就是拿这个字段做判断——ARP 帧交给 ARP 处理分支IPv4 帧交给 IP 层其他的直接丢。这一步写错的现象很有辨识度PC 的 ARP 请求进来了但你没有回应于是 PC 认为这个 IP 根本不存在连 ping 都不会发出去。所以有一条排查原则特别值得记住第一次上板如果完全收不到任何数据先去看 ARP 有没有回而不是先怀疑 PHY。PHY 层面的问题通常是链路灯不亮或者错误计数暴涨而链路正常但什么都没发生几乎都指向协议层的回应缺失。3.3 udp_complete 里面到底装了什么以我手上这版代码为例udp_complete 大致就是两块东西一个 ip_complete 和一个 udp 模块。ip_complete 把 IP 层和 ARP 相关逻辑包在一起负责 IP 首部的组装与解析、首部校验和以及 ARP 请求与应答的处理具体是内嵌 arp 还是配一张 arp 缓存表不同版本可能略有差异打开文件看实例化列表最准。udp 模块负责传输层源端口、目的端口、长度、校验和。它内部还挂着一个专门算校验和的子模块而 UDP 校验和有个容易被忽略的细节——它需要构造伪首部。伪首部包括源 IP、目的 IP、一个保留字节、协议号UDP 是 17、以及 UDP 长度这五项和 UDP 首部加载荷拼在一起做 16 位反码求和。如果你自己写校验和逻辑而漏了伪首部算出来的值永远是错的接收方全部丢包。这里还有一条规律值得记IP 首部校验和是必算的UDP 校验和在 IPv4 下允许置 0表示不做校验。置 0 能省掉一部分逻辑资源和计算延迟很多实现默认就是这么干的。但你要清楚它是有代价的某些接收端或者中间环节会检查这个字段置 0 过去可能被丢弃。我的原则是实验阶段为了先跑通可以先置 0等要做数据完整性验证时再把校验和打开并且一定用真实数据对一遍。3.4 发送方向头部和载荷是分步提交的发送方向和接收方向是镜像的但顺序上有个特别容易搞混的地方。以 udp_complete 为例发送侧你要按层次依次提供以太网头部目的 MAC、源 MAC、类型、IP 头部字段源 IP、目的 IP、TTL、协议号、UDP 头部字段源端口、目的端口、长度最后才开始推载荷的 AXI Stream。每一层的头部都是用 valid / ready 单独握手提交的只在那一拍有效模块会寄存下来然后等你把载荷流喂进去。这就要求你的状态机严格按顺序走先拉高以太网头部有效信号等握手完成再提交 IP 头部再提交 UDP 头部最后才开始推载荷。我见过有人把 UDP 头部和载荷第一个 beat 同时拉起来结果那一拍的握手没完成头部信息就丢了——现象是偶尔能发出去偶尔发出去对方解析出来的端口号是 0这种间歇性故障最难查。还有一个更隐蔽的问题在载荷的最后一拍。tkeep 的位宽是跟着数据位宽走的数据位宽 8 位时 tkeep 只有 1 位位宽 64 位时 tkeep 是 8 位每一位对应一个字节。当载荷字节数不是 (数据位宽/8) 的整数倍时最后一个 beat 只有低几位是有效的。如果 tkeep 全填 1MAC 就会按完整的一拍发出去帧长度比你以为的多几个字节PC 端要么报长度不符要么直接 FCS 错误。这个问题在 8 位位宽下不明显一旦你切到 64 位位宽就会突然冒出来。4. 参数和端口哪些旋钮必须动哪些最好别碰4.1 AXI Stream 的五个使能位别闭着眼睛全开这套代码和它依赖的 AXI Stream 库普遍带一组参数形如 KEEP_ENABLE、LAST_ENABLE、ID_ENABLE、DEST_ENABLE、USER_ENABLE有的还配独立的 ID_WIDTH、DEST_WIDTH、USER_WIDTH。新手最容易犯的错是看到就全开结果综合出来发现端口对不上反过来的错更麻烦——明明需要 tlast却把它关掉了模块压根不产生帧结束信号下游永远等不到一帧完整数据。我的判断法很简单做以太网数据通路LAST_ENABLE 必须开因为帧必须有边界KEEP_ENABLE 强烈建议开除非你的载荷长度永远刚好是数据位宽的整数倍ID 和 DEST 只在多路复用、分流场景下才有意义单路直连可以关掉省资源USER 一般用来传错误标记或者旁路信息不需要就关。还有一条连带关系必须注意DATA_WIDTH 一改tkeep 位宽跟着变上下游所有模块的宽度都要一起改别只改其中一个。4.2 校验和相关的开关怎么选udp_complete 和 udp 上一般会有 UDP 校验和生成、校验和检查这些参数IP 层也有对应的开关。前面说过IPv4 下 UDP 校验和允许置 0所以想省资源可以把生成关掉发送时把校验和字段填 0但接收方向的检查建议开着否则收到脏数据你也不知道。下面这张表是我自己的默认选择可以直接拿去抄参数影响范围我的默认选择DATA_WIDTHAXI Stream 数据位宽和 MAC 层保持一致尽量避免位宽转换KEEP_ENABLE是否使用 tkeep开除非长度永远是整数倍LAST_ENABLE是否使用 tlast必开帧边界依赖它ID_ENABLE / DEST_ENABLE通道标识、目的地标识单路直连时关闭USER_ENABLE旁路信息传递需要传错误标记时开UDP 校验和生成发送方向的 UDP 校验和先关跑通验证阶段再打开4.3 位宽和时钟频率几个必须记牢的换算1G 以太网的 GMII 是 8 位数据、125MHz、单沿采样8 × 125M 正好是 1GbpsRGMII 是 4 位数据、125MHz、双沿采样4 × 125M × 2 也是 1Gbps。10G 的 XGMII 常见组合是 64 位、156.25MHz、双沿采样64 × 156.25M × 2 等于 10Gbps。再往上 25G 走的是另一套位宽和时钟组合换算逻辑一样只是数字不同。为什么要记这些数因为你选 AXI Stream 位宽、定 FIFO 深度、设计跨时钟域方案全都围绕这个速率来定。举个很现实的例子如果你想把 64 位的 10G 流转换成 8 位的 1G 流这不是位宽转换能解决的问题那是速率差了十倍必须配合流控或者丢包策略单纯的位宽转换模块只会让上游疯狂反压、下游大量丢包。还有一个数字必须刻在脑子里标准 MTU 下 IP 载荷最大 1500 字节减掉 20 字节 IP 首部和 8 字节 UDP 首部UDP 载荷最大 1472 字节。你写上位机测试脚本时如果一次发 2000 字节要么走 IP 分片要么直接发不出去——而这套实现基本不做分片重组超长帧会被丢掉。我的建议是先用 100 字节左右的小包跑通流程再慢慢加大到 1472别一上来就用大包压出了问题你也分不清是长度问题还是逻辑问题。4.4 端口命名规律看一眼就知道方向和角色这套代码的端口命名非常有规律认熟了能省下大量读代码的时间s_ 前缀slave 侧也就是输入给这个模块的信号。m_ 前缀master 侧模块输出给下游的信号。_hdr_valid / _hdr_ready头部信息的握手对一个周期内完成。payload_axis载荷的 AXI Stream后面接 tdata、tkeep、tvalid、tready、tlast。_clk / _rst时钟和复位多时钟模块会分别标出 rx 和 tx。搞清这几个前缀打开任何一个模块的文件光看端口列表就能猜出它大概干什么。比如 s_eth_hdr_valid、s_eth_payload_axis_tdata、m_udp_hdr_valid、m_udp_payload_axis_tready 这一组同时出现基本可以断定这是从以太网帧接收数据、往外发 UDP的模块。这个技巧在你看 udp_complete 这种集成模块时特别有用因为它的端口非常多硬读会很痛苦。5. 上板之前要把这几件事做对ARP、时钟相位、复位、自检顺序5.1 ARP 应答不通后面的测试全都免谈PC 要给你发 UDP 包之前先得知道你的 MAC 地址这个信息靠 ARP 请求来问。所以 FPGA 端必须能正确应答 ARP 请求并且把我是谁、我的 IP 是多少、我的 MAC 是多少这几个信息配对正确。这就要求你至少正确配置三样东西本地 MAC、本地 IP、子网掩码涉及跨网段时还有网关 IP。我踩过的坑是子网掩码。有一次把掩码写成了 255.255.0.0本地 IP 是 192.168.1.10PC 是 192.168.1.100本来应该是同网段直连结果因为掩码配错FPGA 判断目标是不同网段于是它不去直接解析目标 IP而是转身去解析网关——可网关根本不存在包就发不出去了。现象极具迷惑性PC 侧判断正常ping 得出去但 FPGA 收不到任何回应两边看起来都没问题。这个坑花了我很久才定位到。还有个更细的点ARP 缓存是有寿命的条目过期后需要重新解析。如果你的缓存更新逻辑有问题比如只在收到请求时更新、不理会应答就会出现通一会儿、断一会儿的间歇性故障。上板初期建议做几分钟的稳定性测试看丢包率是不是稳定为 0别只看第一秒钟通了就以为大功告成。5.2 RGMII 的接收时钟相位第一个真正属于硬件层的坑RGMII 用 DDR 传输数据在时钟的两个沿上切换PHY 会提供一个随路时钟。为了保证采样正确通常需要把接收时钟的相位搬移大约 90 度让采样点落在数据眼图的中央。在常见的 7 系列器件上一般用 IDELAYE2 配合 IDELAYCTRL需要一个参考时钟来做微调或者在 MMCM 上做相位偏移。这一步不注意会怎样现象很典型能收到帧但 CRC 错误计数疯狂增长或者干脆一帧都收不到。我第一次就遇到了改了半天的逻辑代码最后发现是 IDELAY 需要的那个参考时钟根本没接上——这个原语要额外的时钟输入非常容易在例化和约束里被漏掉。逻辑代码一行没错时钟少了一根结果就是收不到包。我的建议是RGMII 部分的时序约束一定按厂家模板写输入输出延迟的具体数值参考官方参考设计不要自己拍脑袋填。同时对接收侧的延迟值做一次扫描找出丢包率最低的那个值。发送侧相对简单一些通常靠 MMCM 相位加约束就能解决。RGMII 这部分是整个工程里唯一必须对着硬件调的环节别指望一次写对。5.3 复位别一上电就放开要等锁定信号FPGA 上电后PLL 或者 MMCM 需要一段时间才能锁定输出时钟稳定下来又要一点时间。如果你的复位信号在上电后立刻放开逻辑会在时钟还不稳定的状态下开始跑各种难以复现的诡异现象就来了状态机跑飞、计数器溢出、偶尔收到一帧乱码。这类问题最讨厌的地方是它不稳定——每次上电的表现都不一样让人以为是逻辑问题。正确的做法是把复位和锁定信号绑在一起PLL 的 locked 信号为低时保持复位有效locked 拉高后再延迟若干个时钟周期才释放复位。这套代码里大部分模块的复位都是高电平有效具体以你手上的版本为准打开文件确认最稳妥所以要保证复位释放的时机是同步的、边沿干净的。我见过有人直接用按键做异步复位按下的时候确实复位了但释放的那一刻引入了亚稳态结果偶尔会卡死。5.4 一份可以照着走的上板自检顺序把上面的东西串起来我上板时的自检顺序是这样按现象对照排查比漫无目的试错快很多步骤期望现象不对时先查什么1. 上电看链路PHY 链路灯亮、link upPHY 复位、PHY 地址、管理接口配置2. PC 端 ping有正常回复ARP 应答、本地 IP 与掩码配置3. 抓包看 ARP请求和应答双向都有以太网头部信息、ARP 处理分支4. 用小包发 UDPFPGA 侧收包计数增加类型字段分流、目的端口过滤5. FPGA 回发 UDP抓包能看到完整帧头部提交顺序、tkeep 与 tlast6. 连续跑几分钟丢包率稳定为 0时钟相位、FIFO 深度、背压处理上位机这边抓包我一般用常见的网络协议分析工具它能直接把 CRC 错误、长度异常这些标出来比自己在 FPGA 侧猜快得多。做带宽测试的时候UDP 模式的打流工具可以拿来压一压但要注意先确认链路已经稳定否则你压出来的丢包率根本分不清是链路的锅还是工具参数的问题。6. 用逻辑分析仪抓一次真实收发之后我记下的四个坑6.1 tkeep 和长度字段对不上多发了几个字节这是我卡得最久的一个问题。当时的实现里载荷长度是按字节算的最后一拍 tkeep 我图省事直接全填 1。在 8 位位宽下这个问题不明显因为 tkeep 只有 1 位填 1 也差不多对等我切到更宽的位宽问题立刻暴露——PC 抓到的包长度总比我预期多几个字节帧尾的校验全部失败。排查过程其实很简单就是把 FPGA 侧送给 MAC 的最后一拍数据抓出来数一数字节数再和长度字段对一遍。问题是当时我一直在怀疑上位机的代码绕了很久。现在的习惯是只要载荷长度不是数据位宽的整数倍先把 tkeep 和 tlast 的时序抓出来看一眼这是最快的定位方式。另外提醒一句不同位宽下 tkeep 的语义是一样的一位对应一个字节但位宽越宽出错的机会越多因为要填的位多了。6.2 背压来了你在干什么AXI Stream 的握手是双向的tvalid 由发送方决定tready 由接收方决定两者同时为高的那一拍才算数据传输成功。新手最常见的错误是我拉高 tvalid 了数据就该算发出去了然后立刻切换状态开始准备下一拍。实际上下游完全可能因为内部缓冲满而把 tready 拉低那一拍数据传输根本没发生你就把数据丢了。正确的心态是只要 tvalid 拉高了就必须一直保持数据不变、tvalid 不撤直到看见 tready 为高。这是 AXI 协议的基本要求违反它会导致数据错位、重复或者丢失而且往往不是每次都会错所以特别难查。我在抓波形时最常看的组合就是 tvalid、tready、以及当前状态机的状态三者一起看一眼就能看出是不是有数据被吞了。还有一个连带问题如果下游长时间不 ready你上游能不能承受这种阻塞如果你的上游是 FIFO那没问题如果上游是 AD 采样这种不能停的数据源就必须设计丢弃策略比如满了就整帧丢而不是丢一半——半帧数据比丢帧更麻烦接收端会收到一帧看起来正常但内容错位的数据。6.3 MAC 地址和 IP 地址的拼装顺序这个坑说出来有点丢人但确实很典型。MAC 地址是 48 位IP 地址是 32 位在 Verilog 里最直观的写法就是用一个向量表示比如 48h112233445566 和 32hC0A8010A对应 192.168.1.10。我当时的错误是把 MAC 地址按字节数组的方式去拼结果字节顺序反了PC 抓到的包显示来自异常地址然后直接丢弃。排查方法很简单让 PC 端抓一次包对着源 MAC 和目的 MAC 看是不是你写的那个值。如果反了一眼就能看出来。IP 地址这边相对简单因为协议本身要求网络序大端你在 Verilog 里声明 32hC0A8010A 表示 192.168.1.10这是最直观也最不容易出错的写法。建议所有地址配置都用参数集中放在文件顶部改的时候只改一处别散落在状态机里。6.4 学会用统计计数器比盯着波形猜强太多最后一条是我调了几个晚上之后最大的体会。这套代码里配套有统计相关的模块能给出收发包数量、错误数量、CRC 错误数量这类计数器。上板调试的时候这些计数器就是你的仪表盘链路灯亮着但收包计数一直是 0问题在物理层或者分流逻辑收包计数在涨但错误计数也在涨问题在时钟相位或者 CRC收包正常但发包计数不动问题在发送侧的状态机。我一开始特别迷信抓波形觉得看到信号才踏实。问题是逻辑分析仪的采样深度有限你抓到的窗口可能根本没覆盖到出错的那一帧而计数器是持续累加的能告诉你这段时间里到底发生了什么。所以我的建议是把计数器接到一个能读出来的地方比如通过管理接口读或者直接引到指示灯上做粗粒度观察先靠计数器定位到大致范围再用波形去精确定位具体是哪一拍出的问题。如果你也在从零走这条路我个人的体会是这套代码不值得一次读完也不值得只看 README。真正让我搞懂它的是三次具体的调试——一次是 tkeep 少填了一格一次是 ARP 没有回应一次是接收时钟相位差了一点。每次调完再回头看对应模块的源码理解都会深一层。我打算接下来把 10G 那条通路也摸一遍尤其是高速接口和 FIFO 配合的部分那里面还有不少细节值得单独写一篇。