2026/10/6 6:51:58

FPGA千兆以太网实战:Tri_mode_ethernet_mac源码模式与RGMII时序调优

FPGA千兆以太网实战:Tri_mode_ethernet_mac源码模式与RGMII时序调优 1. 项目缘起与整体设计思路1.1 为什么偏偏是 Core Container 模式用过 Vivado 的人都知道Tri_mode_ethernet_mac 这个 IP 核在配置界面里有一个不起眼但影响深远的选项——Core Container。默认情况下Vivado 会把它勾上很多人在 Example Design 里跑得好好的一旦把 IP 核集成到自己的工程里就开始出各种幺蛾子综合报错、引脚约束找不到、时钟无输出、RGMII 接口时序对不上。我前后做过五六个带千兆以太网的 FPGA 项目从 Artix-7 到 Zynq-7000 都用过这个核踩的坑基本都集中在 Core Container 这个开关上。先说清楚它到底是什么。Core Container 本质上是 Xilinx 给 IP 核套的一层“外壳”它把 IP 的 RTL 源码、约束文件、仿真模型全部打包进一个独立的目录结构里综合时以 OOCOut-of-Context方式单独跑一遍综合生成一个 DCP 文件然后在顶层综合时直接调用这个已经综合好的网表。这么做的初衷是好的——IP 核内部逻辑复杂单独综合能减少顶层综合时间也方便复用。但问题在于一旦你选了 Core ContainerIP 核的引脚、时钟、复位这些接口就不再是普通的 RTL 信号而是被封装过的“黑盒”你想在顶层直接改它的内部逻辑或者加约束就没那么自由了。那为什么还有人选它因为不选 Core Container 的话IP 核会以源码形式直接展开到你的工程里综合时间会明显变长而且每次改顶层都要重新综合整个 IP。对于大工程来说这个时间成本很可观。所以我的建议是如果你的工程规模不大或者你需要深度定制 MAC 内部逻辑比如改 RGMII 接口的 DDR 采样逻辑那就果断把 Core Container 关掉用源码模式。反过来如果你只是拿它当标准外设用接口都是现成的 AXI-Stream 或者 GMII那 Core Container 能帮你省不少综合时间。1.2 RGMII 接口为什么总在 Core Container 下出问题RGMII 这个接口本身就很特殊。它是 DDR双沿采样接口125MHz 时钟下上升沿传低 4 位数据下降沿传高 4 位数据同时还要处理 TXC 和 RXC 的相位关系。在 GMII 模式下MAC 核输出的是 8 位数据加一个 125MHz 时钟你需要在外面自己写 DDR 收发逻辑而在 RGMII 模式下MAC 核内部会帮你做这个 DDR 转换但前提是你要正确配置 PHY 的延时参数。Core Container 模式下MAC 核的 RGMII 接口信号会被封装到 IP 的顶层端口上你没法直接看到内部的 IDDR/ODDR 原语。这时候如果你发现 RGMII 通信不稳定想调一下采样边沿就只能通过 IP 配置界面里的 “PHY 延时” 选项来调或者在外面加 IDELAY 原语。但很多人不知道的是Core Container 模式下 IP 核生成的约束文件里RGMII 接口的时序约束是自动生成的而且往往偏保守如果你直接照搬可能会发现时序报告里 RGMII 路径的 slack 是负的但实际板子又能跑——这就是约束和实际硬件不匹配的典型表现。我遇到最坑的一次是用 Artix-7 接 Realtek 8211 PHYCore Container 模式下 RGMII 死活 ping 不通抓包发现 TX 数据有但 RX 数据全是错包。后来把 Core Container 关掉直接看源码里的 IDDR 逻辑发现 IP 默认用的是 “opposite edge” 采样而我的 PHY 需要 “same edge”改了一行参数就通了。所以这一节的核心结论是Core Container 适合标准场景RGMII 这种对时序敏感的接口最好用源码模式方便你随时改内部 DDR 逻辑。1.3 整体方案选型与工程结构规划基于上面的分析我最终采用的方案是关闭 Core Container使用源码模式集成 Tri_mode_ethernet_mac然后手动修改 RGMII 接口的 IDDR/ODDR 逻辑并补充时序约束。这样做的代价是综合时间会增加大约 30%但换来的是完全可控的接口时序和调试便利性。工程结构上我建议这样组织顶层模块只例化 MAC 核和必要的时钟模块RGMII 接口的 DDR 原语放在 MAC 核外面方便单独约束。时钟方案用 MMCM 生成 125MHz 和 200MHzIDELAYCTRL 需要如果 PHY 提供 125MHz 时钟也可以直接用。约束文件分三个时钟约束、RGMII 输入输出延时约束、跨时钟域约束。Core Container 模式下这些约束是 IP 自动生成的源码模式下需要你自己写但写起来更灵活。下面这张表对比了两种模式的差异方便你快速决策对比项Core Container 模式源码模式综合时间短OOC 综合长全展开RGMII 可调性低只能改 IP 配置高可直接改 IDDR/ODDR约束文件IP 自动生成需手动编写调试便利性差信号被封装好信号可见适用场景标准 GMII/SGMII 接口RGMII 或需要深度定制提示如果你已经选了 Core Container 又不想重做工程可以在 IP 配置界面里把 “Core Container” 取消勾选然后点 “OK”Vivado 会提示你重新生成 IP生成后 IP 源码就会出现在工程里。2. 核心细节解析与实操要点2.1 Tri_mode_ethernet_mac IP 核的关键配置项打开 IP 配置界面第一页是 “Ethernet MAC Features”。这里有几个选项直接决定后续 RGMII 能不能跑通Physical Interface选 RGMII。注意如果你选 GMIIIP 核不会生成 DDR 逻辑你需要自己写选 RGMII 的话IP 核内部会帮你做 DDR 转换但前提是 PHY 延时配置正确。Speed选 1000 Mbps。如果选 10/100RGMII 时钟会变成 25MHz 或 2.5MHzDDR 逻辑会不一样。PHY Address这个要和你的 PHY 硬件地址一致通常由 PHY 的 PHYAD 引脚决定常见的是 0 或 1。MDIO Interface建议勾上方便后续用 MDIO 读写 PHY 寄存器调试。Management Interface选 AXI4-Lite 或独立 MDIO看你的系统需求。第二页是 “Shared Logic”。这里有个关键选项Include Shared Logic in core还是Include Shared Logic in example design。如果你选 Core Container通常选 “Include Shared Logic in core”这样时钟和复位逻辑都在 IP 内部如果选源码模式建议选 “Include Shared Logic in example design”这样时钟逻辑会放在外面方便你替换成自己的 MMCM。第三页是 “Clock Configuration”。这里要填三个时钟频率GTX_CLK125MHz、REF_CLK200MHz给 IDELAYCTRL 用、MDIO_CLK通常 2.5MHz。注意REF_CLK 必须是 200MHz且必须从外部晶振或 MMCM 输出不能随便给一个时钟否则 IDELAYCTRL 不工作RGMII 输入延时校准会失败。2.2 RGMII 接口的 DDR 原语与延时校准RGMII 的核心是 DDR 采样。在源码模式下IP 核会生成一个叫rgmii_interface的模块里面包含 IDDR 和 ODDR 原语。以 Xilinx 7 系列为例典型的 RX 路径是这样的IDDR #( .DDR_CLK_EDGE(SAME_EDGE_PIPELINED), .INIT_Q1(1b0), .INIT_Q2(1b0), .SRTYPE(SYNC) ) iddr_rx_data ( .Q1(rx_data[3:0]), .Q2(rx_data[7:4]), .C(rx_clk), .CE(1b1), .D(rgmii_rxd), .R(1b0), .S(1b0) );这里DDR_CLK_EDGE参数很关键。SAME_EDGE_PIPELINED表示 Q1 和 Q2 在同一个时钟沿输出适合后续用同一个时钟处理OPPOSITE_EDGE表示 Q1 在上升沿输出Q2 在下降沿输出适合后续用双沿处理。大部分 PHY 用SAME_EDGE_PIPELINED就行但如果你发现 RX 数据错位可以试试改成OPPOSITE_EDGE。TX 路径用 ODDRODDR #( .DDR_CLK_EDGE(SAME_EDGE), .INIT(1b0), .SRTYPE(SYNC) ) oddr_tx_data ( .Q(rgmii_txd), .C(tx_clk), .CE(1b1), .D1(tx_data[3:0]), .D2(tx_data[7:4]), .R(1b0), .S(1b0) );TX 这边还要注意 TXC 的相位。RGMII 标准要求 TXC 相对于 TXD 有 1.5~2ns 的延时这个延时通常由 PHY 内部提供但有些 PHY 需要 FPGA 这边加延时。如果你发现 TX 数据眼图很差可以在 ODDR 后面加一个ODELAY原语或者直接在 PCB 上走线绕等长。2.3 时序约束的编写要点源码模式下RGMII 的时序约束需要自己写。核心是两条输入延时和输出延时。输入延时约束以 RX 为例set_input_delay -clock [get_clocks rx_clk] -max 2.0 [get_ports rgmii_rxd] set_input_delay -clock [get_clocks rx_clk] -min 0.5 [get_ports rgmii_rxd] set_input_delay -clock [get_clocks rx_clk] -max 2.0 [get_ports rgmii_rx_ctl] set_input_delay -clock [get_clocks rx_clk] -min 0.5 [get_ports rgmii_rx_ctl]这里的 max/min 值要根据 PHY 的数据手册来填。以 Realtek 8211 为例RX 数据相对于 RX_CLK 的延时典型值是 1.2ns那么 max 可以填 2.0min 填 0.5留一定余量。如果你不确定可以先填一个保守值然后看时序报告再慢慢收紧。输出延时约束以 TX 为例set_output_delay -clock [get_clocks tx_clk] -max 1.5 [get_ports rgmii_txd] set_output_delay -clock [get_clocks tx_clk] -min -0.5 [get_ports rgmii_txd] set_output_delay -clock [get_clocks tx_clk] -max 1.5 [get_ports rgmii_tx_ctl] set_output_delay -clock [get_clocks tx_clk] -min -0.5 [get_ports rgmii_tx_ctl]输出延时通常由 PHY 内部延时决定如果 PHY 内部有 2ns 延时那么 FPGA 这边只需要保证数据在时钟沿前后稳定即可。注意TX_CLK 是 FPGA 输出给 PHY 的所以约束里要把它当成 generated clock 来处理。注意RGMII 的时序约束不要照搬别人的参数一定要结合自己的 PHY 手册和 PCB 走线长度来算。我见过有人直接抄 Xilinx 官方 Example Design 的约束结果板子跑不通就是因为 PHY 型号不一样。3. 实操过程与核心环节实现3.1 工程创建与 IP 核生成第一步新建 Vivado 工程选好器件型号。我用的是一块 Artix-7 XC7A35T 的板子PHY 是 Realtek 8211。工程建好后在 IP Catalog 里搜 “Tri_mode_ethernet_mac”双击打开配置界面。按照 2.1 节的配置项填好重点是Physical Interface 选 RGMIISpeed 选 1000 MbpsPHY Address 填 0我的板子 PHYAD 接地勾上 MDIO。然后在 “Shared Logic” 页把 “Include Shared Logic in core” 改成 “Include Shared Logic in example design”这样时钟逻辑会放在外面。最后点 “OK”Vivado 会提示你生成 IP生成完后在 Sources 窗口里就能看到 IP 的源码了。这里有个小技巧生成 IP 后先别急着例化先打开 IP 的 Example Design 跑一遍仿真。Example Design 里已经帮你把 RGMII 接口、时钟、复位都连好了你可以直接看波形确认 DDR 采样逻辑是对的。仿真通过后再把 Example Design 里的rgmii_interface模块复制到自己的工程里改一改就能用。3.2 RGMII 接口修改与 IDDR/ODDR 调整Example Design 里的rgmii_interface模块默认用的是SAME_EDGE_PIPELINED我的 PHY 需要OPPOSITE_EDGE所以我要改一下。打开rgmii_interface.v找到 IDDR 例化的地方把DDR_CLK_EDGE改成OPPOSITE_EDGE。改完后RX 数据的 Q1 和 Q2 会分别在上升沿和下降沿输出后续处理逻辑也要相应调整——原来是用一个 125MHz 时钟同时采 Q1 和 Q2现在要用双沿或者用 250MHz 时钟分别采。TX 这边ODDR 的DDR_CLK_EDGE保持SAME_EDGE不变但要注意 TXC 的相位。我的 PHY 需要 TXC 相对于 TXD 有 2ns 延时所以我在 ODDR 后面加了一个ODELAY原语ODELAY #( .DELAY_SRC(ODATAIN), .IDELAY_TYPE(FIXED), .ODELAY_VALUE(10) // 10 * 78ps ≈ 0.78ns具体值要算 ) odelay_txc ( .DATAOUT(rgmii_txc), .ODATAIN(tx_clk_int), .CLK(1b0), .CE(1b0), .INC(1b0), .RST(1b0) );ODELAY_VALUE的计算方法是78ps 是 7 系列 IDELAY 的步进如果你需要 2ns 延时就是 2000/78 ≈ 26。但实际值要看时序报告先填一个值然后看 TX 数据的建立保持时间再微调。3.3 时钟方案与 IDELAYCTRL 配置源码模式下时钟需要自己生成。我的方案是用一个 200MHz 的差分晶振经过 MMCM 生成 125MHz给 GTX_CLK和 200MHz给 IDELAYCTRL。MMCM 的配置如下输入时钟200MHz输出时钟 1125MHz用于 GTX_CLK 和 TX 逻辑输出时钟 2200MHz用于 IDELAYCTRL 和 RX 逻辑输出时钟 325MHz用于 MDIOIDELAYCTRL 的例化很简单IDELAYCTRL idelayctrl_inst ( .RDY(idelay_rdy), .REFCLK(ref_clk_200m), .RST(reset) );注意REFCLK 必须是 200MHz ±10ppm否则 IDELAYCTRL 的 RDY 信号不会拉高RGMII 输入延时校准会失败。我一开始用了一个 100MHz 的时钟结果 RDY 一直为低查了半天才发现是频率不对。3.4 约束文件编写与综合实现约束文件分三部分写。第一部分是时钟约束create_clock -period 8.000 -name sys_clk [get_ports sys_clk_p] create_generated_clock -name gtx_clk -source [get_pins mmcm_inst/CLKIN1] -divide_by 1 -multiply_by 5 -divide_by 8 [get_pins mmcm_inst/CLKOUT0] create_generated_clock -name ref_clk_200m -source [get_pins mmcm_inst/CLKIN1] -divide_by 1 [get_pins mmcm_inst/CLKOUT1]第二部分是 RGMII 输入输出延时约束按 2.3 节的方法写。第三部分是跨时钟域约束主要是 RX 逻辑和 TX 逻辑之间的数据交互用set_false_path或者set_max_delay处理。写完约束后跑综合。综合完成后打开时序报告重点看 RGMII 路径的 slack。如果 slack 是负的先别慌看看是不是约束太紧了。我一般会先把输入输出延时放宽 0.5ns跑一遍实现看板子能不能通能通再慢慢收紧。提示Vivado 综合时如果报 “DRC RTSTAT-2” 错误通常是 I/O 引脚没有正确约束。检查你的 XDC 文件里有没有set_property PACKAGE_PIN和set_property IOSTANDARDRGMII 的引脚电平通常是 LVCMOS33 或 LVCMOS25看你的 PHY 供电。4. 常见问题与排查技巧实录4.1 RGMII 通信不稳定的典型原因RGMII 通信不稳定十有八九是时序问题。我整理了一个排查表按优先级从高到低现象可能原因排查方法ping 不通TX 有数据 RX 无数据RX 采样边沿不对改 IDDR 的 DDR_CLK_EDGEping 通但丢包严重TX/RX 延时余量不足看时序报告调 ODELAY/IDELAY千兆不通百兆能通RGMII 时钟频率不对检查 MMCM 输出是否 125MHz上电偶尔不通复位后正常IDELAYCTRL 未就绪检查 RDY 信号确保 REFCLK 200MHz长时间跑出现错包跨时钟域处理不当加异步 FIFO 或握手信号我最常遇到的是第一种和第二种。第一种改 IDDR 参数就能解决第二种需要耐心调延时。调延时的时候不要一次调太多每次调 1~2 个 IDELAY 步进78ps调完跑一次实现看时序报告和实际通信质量。4.2 Core Container 模式下的引脚约束问题如果你坚持用 Core Container 模式引脚约束会有点麻烦。因为 IP 核的 RGMII 接口被封装了你在顶层 XDC 里写set_property PACKAGE_PIN的时候要指向 IP 核例化后的端口名而不是 IP 内部的信号名。比如set_property PACKAGE_PIN F12 [get_ports rgmii_rxd[0]] set_property IOSTANDARD LVCMOS33 [get_ports rgmii_rxd[0]]这里的rgmii_rxd[0]是顶层模块的端口不是 IP 核内部的。如果你在 IP 核内部改了信号名顶层端口名也要跟着改。Core Container 模式下IP 核的端口名是固定的你不能改所以顶层端口名也要保持一致。另外Core Container 模式下 IP 核会自动生成一个 XDC 文件里面有时钟约束和引脚约束。你可以在这个文件的基础上改但不要直接删掉它否则综合会报错。我一般会把 IP 自动生成的 XDC 复制一份改名为user_constraints.xdc然后在工程里把原来的 XDC 禁用用自己改的这份。4.3 仿真与上板调试的差异处理仿真通过不代表上板能通这是 FPGA 开发的常态。RGMII 尤其如此因为仿真里没有 PCB 走线延时也没有 PHY 的内部延时。我的经验是仿真主要验证逻辑功能时序问题必须上板调。上板调试时我一般会先用 MDIO 读一下 PHY 的寄存器确认 PHY 已经 link up并且协商到了 1000Mbps 全双工。如果 PHY 没 link先查硬件晶振有没有起振、复位有没有释放、MDIO 能不能读写。PHY link 上之后再用 ILA 抓 RGMII 的 RX 数据看有没有错包。如果 RX 数据全是 0 或者全是 F说明采样边沿不对回去改 IDDR。还有一个技巧在 TX 路径上发一个固定的测试包比如全 0x55然后用示波器或者逻辑分析仪看 TXD 和 TXC 的相位关系。如果 TXC 的边沿正好在 TXD 的中间说明延时合适如果偏了就调 ODELAY。4.4 常见问题速查表问题解决方法综合报错 “DRC RTSTAT-2”检查 I/O 约束补上 PACKAGE_PIN 和 IOSTANDARD实现后时序 slack 为负放宽输入输出延时约束或加 IDELAY/ODELAYIDELAYCTRL RDY 不拉高检查 REFCLK 是否为 200MHzRGMII RX 数据错位改 IDDR 的 DDR_CLK_EDGE 为 OPPOSITE_EDGETX 眼图差加 ODELAY 调 TXC 相位或检查 PCB 走线MDIO 读写失败检查 MDIO 时钟频率通常 2.5MHz和 PHY 地址千兆不通百兆通检查 GTX_CLK 是否为 125MHzRGMII 是否配置为千兆长时间跑丢包加异步 FIFO 处理跨时钟域检查复位是否同步注意RGMII 的调试不要只盯着 FPGA 侧PHY 侧的寄存器配置也很关键。有些 PHY 默认开启自协商但 RGMII 模式下需要手动配置成千兆全双工否则会协商成百兆。我遇到过好几次PHY 寄存器没配对FPGA 这边怎么调都不通。5. 个人实操心得与后续扩展5.1 几个让我印象深刻的坑第一个坑是 IDELAYCTRL 的 REFCLK。我一开始用了一个 125MHz 的时钟结果 RDY 一直不拉高RGMII 输入延时校准失败RX 数据全是乱的。后来查了 UG471 才知道REFCLK 必须是 200MHz而且精度要求很高。换了一个 200MHz 的时钟后RDY 立刻拉高RX 数据也正常了。这个坑的教训是Xilinx 的文档一定要仔细看尤其是时钟频率这种硬性要求。第二个坑是 Core Container 模式下的引脚约束。我一开始在顶层 XDC 里写set_property PACKAGE_PIN结果综合报错说找不到端口。后来才发现Core Container 模式下 IP 核的端口名和顶层端口名不一致需要在 IP 核的例化模板里找正确的端口名。如果你不确定端口名可以在 IP 核的 Example Design 里看它是怎么连的照抄就行。第三个坑是 RGMII 的 TX 延时。我的 PHY 需要 TXC 相对于 TXD 有 2ns 延时我一开始没加 ODELAY结果 TX 眼图很差ping 通但丢包严重。后来加了一个 ODELAY调了 26 个步进约 2ns眼图立刻变好丢包率降到零。调 ODELAY 的时候不要一次调太多每次调 1~2 个步进调完跑一次实现看时序报告和实际通信质量。5.2 后续可以扩展的方向如果你已经跑通了 RGMII下一步可以试试这几个方向加 UDP/IP 协议栈Xilinx 有免费的 UDP/IP 核可以配合 Tri_mode_ethernet_mac 使用实现网络通信。加 AXI-Stream FIFO把 MAC 的 AXI-Stream 接口接到 FIFO 上方便和处理器交互。加时间戳功能Tri_mode_ethernet_mac 支持 IEEE 1588 时间戳可以做精确时钟同步。换成 SGMII 接口如果你需要更高的速率可以换成 SGMII但需要 GTX 收发器复杂度会高不少。我个人觉得RGMII 是千兆以太网里性价比最高的接口引脚少、逻辑简单适合中小规模 FPGA。但它的时序确实敏感需要耐心调。调通一次之后后面再做类似的项目就快多了因为你知道坑在哪里也知道怎么绕过去。最后再分享一个小技巧如果你用的是 Zynq 系列Tri_mode_ethernet_mac 可以直接接到 PS 的 GEM 接口上这样就不用自己写 RGMII 逻辑了PS 端有现成的驱动。但如果你用的是纯 FPGA那就只能自己写 RGMII 接口这时候源码模式比 Core Container 模式方便得多。