YT8521SH RGMII调试实战:从U-Boot驱动到FPGA时序的完整排查
1. 为什么一块网络子卡能让整机联调卡了三天先说结论YT8521SH这颗国产PHY芯片本身性能够用、价格友好但它在RGMII模式下的默认行为和很多SoC/MAC控制器的预期完全不一样。如果你直接按参考设计画板、按默认寄存器配置启动U-Boot大概率会碰到link起来了但ping不通LED灯死活不亮千兆速率下偶发丢包这类玄学问题。我这次是在一块基于国产FPGAARM架构的板卡上做网络联调MAC侧用的是RGMII接口PHY选的正是YT8521SH。板子烧进U-Boot后第一步就遇到问题mii info能看到PHY地址mii dump也能读到寄存器但网口就是起不来。用示波器抓RGMII的TX_CLK和TX_CTL发现时钟有、数据有可对端交换机就是识别不到有效链路。后来把问题拆开看真正卡住我的其实是三个层面的事情硬件上RGMII的RX_CLK延迟、LED引脚上下拉配置决定了PHY上电后处于什么工作模式U-Boot驱动层面phy驱动的probe流程、RGMII TX/RX内部延迟IDR的寄存器配置直接影响MAC和PHY之间能不能正常收发时序约束层面如果MAC侧是FPGA逻辑实现的RGMII接口那IDDR、ODDR的时序约束写得不到位就算PHY配置全对数据依然是错的。这篇文章就是把这三条线的坑全部踩平之后整理出来的完整复盘。代码基于U-Boot 2023.04PHY驱动基于内核通用的motorcomm驱动框架改写MAC侧是FPGA逻辑实现原理和结论同样适用于Zynq、i.MX、RK等常见平台。2. RGMII的时序本质与YT8521SH的默认行为2.1 RGMII和GMII最本质的区别在时钟沿做嵌入式网络调试很多人对RGMII的理解停留在引脚少、速率高这个层面但真正决定你能不能调通的是RGMII的时钟-数据相位关系。GMII接口里TX_CLK和RX_CLK都是独立的数据和时钟沿对齐接收端直接采样就行。但到了RGMII为了省引脚收发各只保留一根时钟线数据在时钟的双沿上升沿和下降沿都传输。更关键的是RGMII协议规定发送端发出的数据和时钟默认是边沿对齐的接收端必须自己把时钟延迟2ns左右再用来采样数据。这就引出了RGMII调试中最重要的一个概念——RGMII IDRInternal Delay Register内部延迟。MAC侧发送数据时要么MAC自己把TX_CLK延迟2ns再送出去要么PHY在接收侧把RX_CLK延迟2ns反过来接收方向同理。2.2 YT8521SH的默认IDR状态和预期不一致YT8521SH的RGMII模式内部延迟是通过0x000A寄存器RGMII Config Register的bit 6和bit 7控制的bit 7RGMII_TX_IDR发送方向内部延迟1表示开启2ns延迟bit 6RGMII_RX_IDR接收方向内部延迟1表示开启2ns延迟。芯片上电默认值是多少默认是0。也就是说YT8521SH在上电后RGMII收发方向都不做内部延迟补偿。问题就在这里。很多SoC的MAC控制器比如Zynq的GEM、i.MX的FEC硬件上在RGMII模式下已经默认做了时钟延迟有些是在SoC内部有些是通过设备树配置。如果你用的MAC侧本身不产生延迟而PHY侧默认也不延迟那两边都以为对方会处理最后时钟采样沿正好落在数据跳变沿上出来的就是链路通、数据错的诡异现象。用示波器抓波形会看到TX_CLK的上升沿和TX_DATA的电平转换完全对齐接收端采样时正好采到跳变沿数据自然就是乱的。2.3 延迟到底该由谁来做MAC侧还是PHY侧这是RGMII调试里最典型的分工问题。经验法则是如果MAC侧是FPGA逻辑自己写的RGMII接口没有专门的延迟硬件优先让PHY来做延迟也就是把YT8521SH的TX_IDR和RX_IDR都打开如果MAC侧是硬核控制器Zynq GEM、RK GMAC这类一般SoC内部已经有延迟配置选项需要你把设备树里MAC的tx-internal-delay-ps和rx-internal-delay-ps设好PHY这边就不需要再开IDR绝对不能两边都开延迟否则等于延迟了2ns的时钟又被额外延迟采样点反而更差。我这次FPGA的RGMII逻辑用的是最简单的IDDR/ODDR方案没有额外的延迟原语所以最终配置是让PHY侧开启TX_IDR和RX_IDR也就是把0x000A寄存器写成0xC000高两位为1。注意不同驱动框架对0x000A寄存器的操作位置不一样。内核通用的motorcomm驱动里YT8521SH的config_init函数会统一处理这个寄存器但U-Boot的PHY驱动未必包含完整实现需要自己确认并补上。3. 硬件设计上最容易忽略的LED与模式引脚3.1 YT8521SH的LED引脚同时承担配置功能很多人画板子的时候把YT8521SH当普通PHY对待LED引脚随意接个电阻点亮就完事。但YT8521SH的LED0/LED1/LED2不单纯是指示引脚它们同时还是上电默认模式的配置引脚。看一下YT8521SH数据手册里的strap说明引脚默认模式配置项说明LED0复用PHY地址bit 0与PHYAD[0]共同决定上电地址LED1复用工作模式选择高电平RGMII低电平RMIILED2复用时钟源选择决定使用外部晶振还是时钟输入如果LED引脚的外部上下拉电阻画错最常见的结果就是PHY上电后工作在RMII模式你U-Boot里怎么配RGMII都没用。更坑的是有些板子LED串了电阻连到GPIOGPIO默认输出低电平相当于给strap引脚加了外部下拉直接把PHY拉进了错误模式。我这次板子上的LED是直接通过排针引出到前面板的没有接上下拉。实测上电后读寄存器发现PHY工作模式正常但LED完全不亮。后来查0xA000寄存器LED Mode Select发现默认值把LED功能配置成了act/idle状态指示而不是常见的link/act模式导致网线插上、链路建立后LED的状态和我们预期的不一致。3.2 建议的LED硬件接法YT8521SH的LED输出是开漏结构内部驱动能力有限。数据手册推荐的是每个LED串一个1kΩ左右的限流电阻到VDDIO通常3.3V或2.5V取决于PHY的VDDIO供电如果PCB上LED离PHY较远电阻可以减小到470Ω到1kΩ之间防止压降过大导致LED亮度不足关键LED引脚上不要再外接其他负载不要直接连到MCU的GPIO做读取否则会影响LED驱动波形极端情况下会干扰PHY内部逻辑。如果你需要在Linux系统里通过GPIO模拟读取link状态不要直接复用LED引脚建议用PHY芯片的中断输出引脚YT8521SH的INTB引脚来实现或者通过读取PHY状态寄存器0x0001的bit 2来获取link状态。3.3 LED模式怎么配置成符合直觉的状态YT8521SH的LED行为由0xA000寄存器LED Mode Select控制其bit [3:0]控制LED0bit [7:4]控制LED1bit [11:8]控制LED2。常用的几种模式寄存器值LED0LED1LED20xDDDD10M/100M/1000M activitylink/actlink/act0xE0E01000M link100M link10M link0xAAAAactlink1000M link我自己调试时最终选了0xE0E0这种速率指示模式因为调试千兆网口时一眼就能看出PHY协商到了什么速率比link/act灯直观得多。U-Boot里设置方法很简单/* 读取当前LED配置 */ val phy_read(phydev, MDIO_DEVAD_NONE, 0xA000); /* 设置为速率指示模式 */ val 0xE0E0; phy_write(phydev, MDIO_DEVAD_NONE, 0xA000, val);注意0xA000寄存器属于YT8521SH的扩展寄存器空间在U-Boot的PHY驱动里需要通过标准的MDIO读写在MDIO_DEVAD_NONE也就是0x1F地址段操作不能走C45流程。4. U-Boot下从零适配YT8521SH的完整流程4.1 驱动注册的正确姿势U-Boot的PHY驱动框架相对Linux内核要精简得多。要让U-Boot认识并正确配置YT8521SH核心是把驱动结构体注册进系统的PHY驱动链表。我基于drivers/net/phy/motorcomm.c做了适配。骨架如下#include phy.h static int yt8521_config(struct phy_device *phydev) { /* 1. 软件复位 */ phy_reset(phydev); mdelay(10); /* 2. 确认RGMII模式开启内部延迟 */ phy_write(phydev, MDIO_DEVAD_NONE, 0x000A, 0xC000); /* 3. 配置LED模式 */ phy_write(phydev, MDIO_DEVAD_NONE, 0xA000, 0xE0E0); return 0; } static int yt8521_probe(struct phy_device *phydev) { /* 读取PHY ID寄存器确认芯片 */ unsigned int phyid phydev-phy_id; if ((phyid 0xFFFF0FFF) ! 0x0000F100) /* YT8521的ID掩码 */ return -ENODEV; return 0; } static struct phy_driver yt8521_driver { .name YT8521SH, .uid 0x0000F100, .mask 0xFFFF0FFF, .features PHY_GBIT_FEATURES, .config yt8521_config, .probe yt8521_probe, .startup genphy_startup, .shutdown genphy_shutdown, }; int phy_yt8521_init(void) { phy_register(yt8521_driver); return 0; }然后在板级初始化里调用比如在board_init或phy_init中int board_phy_init(void) { phy_yt8521_init(); return 0; }4.2 确认PHY地址的步骤YT8521SH的PHY地址由PHYAD[4:0]引脚决定上电时被锁存。板卡上我用的地址是0x04通过LED0复用的strap位保持默认U-Boot里启动日志就能看到PHY: 0x04 - Detected Motorcomm YT8521SH如果U-Boot没有自动探测到先确认硬件地址。常见做法是直接在U-Boot命令行手动扫描 mii device MII devices: ethernetff180000 phy4 mii info PHY 0x04: OUI0x0000, Model0x0F, Rev0x01如果mii info读到的OUI不是0x0000、Model不是0x0F那大概率是PHY地址不对或者PHY芯片根本没正常工作供电、时钟、复位这三个先查一遍。4.3 关键寄存器的验证方法在U-Boot命令行下直接用mii命令读寄存器验证配置有没有生效 mii read 0x04 0x000A 0xC000如果读回来是0x0000说明前面调用的yt8521_config没有执行或者执行失败。这时候检查两件事U-Boot是否已经把phydev-addr正确设为0x04PHY驱动是否真的被注册并匹配上了U-Boot会按uid自动匹配。LED模式寄存器同理 mii read 0x04 0xA000 0xE0E04.4 U-Boot网络命令验证配置完成后用最基础的命令确认网络通断 dhcp lan78xx: phy power up lan78xx: phy link is up (1000/FULL)看到1000/FULL说明链路协商成功。这时候可以再ping一下网关 ping 192.168.1.1 Using ethernetff180000 device host 192.168.1.1 is alive如果dhcp能拿到IP但ping不通先别急着查U-Boot大概率是时序问题后面详细展开。5. 时序约束FPGA侧RGMII的IDDR/ODDR实现与验证5.1 FPGA逻辑侧RGMII的基本架构如果你的MAC侧是FPGA逻辑RGMII接口必须用IDDR/ODDR原语实现。核心逻辑并不复杂但时序约束写不好数据采样就会出问题。发送方向MAC - PHY用ODDR把数据在时钟上升沿和下降沿都打出去ODDR #( .DDR_CLK_EDGE(SAME_EDGE) ) oddr_tx_ctl ( .Q(tx_ctl), .C(tx_clk), .CE(1b1), .D1(tx_ctl_pos), .D2(tx_ctl_neg), .R(1b0), .S(1b0) );接收方向PHY - MAC用IDDR把双沿数据恢复成单沿并行数据IDDR #( .DDR_CLK_EDGE(SAME_EDGE_PIPELINED) ) iddr_rx_ctl ( .Q1(rx_ctl_pos), .Q2(rx_ctl_neg), .C(rx_clk), .CE(1b1), .D(rx_ctl), .R(1b0), .S(1b0) );5.2 最容易犯的时序约束错误很多FPGA工程师在写RGMII接口时直接对输入时钟和输入数据分批约束结果布线后时序收敛但实际跑起来数据错。根本原因是RGMII的RX_CLK和数据之间的相位关系在FPGA内部已经被IDDR的时钟路径破坏了。PHY送出的RX_CLK和RXD本身是边沿对齐的到了FPGA内部后如果直接用这个RX_CLK去采样RXD采样点正好落在数据跳变沿上。所以必须在约束里对RX_CLK做额外的延迟或者使用FPGA内部的I/O延迟原语IDELAYE2。常用做法是给RX_CLK加set_input_delay约束延迟值取2ns1000Mbps下RGMII的标准延迟set_input_delay -clock [get_clocks rx_clk] -max 2.5 [get_ports {rgmii_rxd[*] rgmii_rx_ctl}] set_input_delay -clock [get_clocks rx_clk] -min 0.5 [get_ports {rgmii_rxd[*] rgmii_rx_ctl}]如果PHY侧已经把RX_IDR内部延迟打开了FPGA侧就不需要额外约束如果PHY侧没开就靠上面的set_input_delay约束来保证采样窗口。我这次调试时把PHY的RX_IDR打开之后FPGA侧的set_input_delay就不需要再加了时序收敛更容易实测也更稳定。5.3 时序问题的典型现象link up但收不到数据如果你遇到的是PHY协商正常link upU-Boot能拿到IP但ping大包严重丢包甚至不通优先怀疑接收方向时序。用示波器同时抓RX_CLK和RXD[0]如果看到时钟上升沿正好和数据跳变沿对齐说明RX方向需要额外延迟。如下RX_CLK ____|‾‾‾‾|____|‾‾‾‾|____ RXD[0] ‾‾‾|____|‾‾‾‾|____|‾‾‾上升沿所在的位置RXD[0]刚好在跳变这种波形的采样结果几乎必然出错。改成RX_IDR开启后PHY内部会给RX_CLK增加约2ns延迟这时波形变成RX_CLK ____|‾‾|______|‾‾|______ RXD[0] ‾‾‾‾|____|‾‾‾‾|____|‾‾‾上升沿采样时数据电平已经稳定这才是正确的采样窗口。5.4 实测验证的方法时序调整后怎么确认改动生效我用的方法是寄存器回读法确认PHY侧的0x000A寄存器bit 6和bit 7确实是1长包压力测试在U-Boot里用tftpboot加载一个大文件比如10MB以上看是否完全一致。如果中间断流或校验错误说明时序还是有残余问题Linux内核验证如果U-Boot阶段方便的话直接加载Linux内核在内核里跑iperf或ping -f更精确地判断丢包率。这里分享一个经验不要只测U-Boot的ping命令因为U-Boot的协议栈对重传和超时的容忍度比较高偶尔丢一两个包根本看不出来。真正有效的是在Linux里做UDP/TCP流量打满丢包率只要超过万分之一时序大概率还有问题。6. 三个典型案例的完整排查链路6.1 案例一读不到PHY ID现象mii info报PHY 0x00: OUI0x0000, Model0x00, Rev0x00。排查顺序万用表量PHY供电VDDIO必须等于MAC侧的I/O电压RGMII电平标准要一致示波器抓PHY的时钟输入确认时钟频率正确YT8521SH支持25MHz外部晶振或时钟输入确认复位引脚电平低有效复位结束后必须拉高并且释放复位到MDIO能访问之间至少等10ms用逻辑分析仪抓MDIO上的读操作确认MDC/MDIO时序正常、PHY地址匹配。最终我这边的问题是PHY地址拨码开关没有拨对。板卡上PHYAD[2:0]通过拨码开关控制默认拨到了0x00但U-Boot里配置的phy地址是0x04改成一致之后马上就能读到ID。6.2 案例二link up后所有回包校验失败现象U-Boot能dhcp但ping不通或者TCP握手能建立但数据传输错乱。这个案例是最典型的RGMII时序问题。排查过程mii read 0x04 0x0001确认link状态为1speed位为千兆mii read 0x04 0x000A发现寄存器值为0x0000说明IDR没开FPGA侧查看RGMII RX方向的时序约束时序报告显示setup裕量只有0.1ns接近负裕量修改U-Boot驱动把0x000A写成0xC000开启PHY侧收发内部延迟重新编译U-Bootdhcp正常、ping正常、tftpboot加载内核完全成功。这里的关键教训是不要只依赖FPGA的时序报告做判断最终以实际收发结果为准。时序报告报正裕量不代表真实硬件就一定没问题因为时序模型是静态的实际芯片的PVT工艺、电压、温度偏移可能很大。6.3 案例三LED灯指示和实际link状态不一致现象网线插上后PHY寄存器显示link up但LED灯不亮或常亮不闪。这个问题的根源是LED模式配置不对。YT8521SH上电默认的LED模式为link/act指示但因为LED引脚配置成速率模式后10M/100M/1000M对应的灯位不同插上千兆交换机时只有1000M那个灯亮如果板子上的丝印标的是Link/Act就会觉得灯不对。我用0xE0E0模式后三个LED分别对应10M/100M/1000M速率调试时一眼就知道协商速率比默认模式直观很多。如果你更关注活动状态闪烁那就用默认模式或0xAAAA模式。7. 把调试时间从三天压缩到半天的方法论7.1 按层级排查别跳步我在多次PHY调试中总结的排查顺序按性价比排序供电和时钟万用表示波器5分钟VDDIO电平和MAC侧一致时钟频率和幅值正常复位时序示波器5分钟复位释放后至少等10ms再访问MDIO很多PHY在上电初始化没完成时MDIO访问会返回全0或全1MDIO通信验证逻辑分析仪10分钟确认地址匹配、读写操作正常IDR/延迟配置寄存器回读10分钟确认0x000A的bit 6/7符合预期实际通信验证U-Boot ping/tftpboot20分钟能通再看稳定性不通回头检查1-4。7.2 必备的三个调试命令组合在U-Boot环境下我经常用下面三条命令组合能快速定位80%的问题mii dump 0x04 0x0001 # 查看link状态、速率、双工 mii read 0x04 0x000A # 查看RGMII延迟配置 mii read 0x04 0xA000 # 查看LED模式如果这三步读出来的值全正常但网络还是不通那就把目光从PHY转回MAC和FPGA时序不要继续在PHY上死磕。7.3 一个常被忽略的细节U-Boot环境下PHY驱动的config调用时机U-Boot的PHY驱动和Linux内核有一个很大的不同U-Boot在phy_startup阶段之前不一定会调用驱动的config函数。有些板级代码要求你在board_init阶段必须显式调phy_config否则PHY会一直跑默认配置。这就导致很多人在U-Boot里用mii read读到0x000A的值为0以为是驱动配置没生效。其实真正的原因是config函数没被调用不是配置错了。解决办法是在board_eth_init里显式调用phydev phy_connect(bus, addr, dev, PHY_INTERFACE_MODE_RGMII); phy_config(phydev);这个坑我在Linux内核里从来不会遇到因为内核的PHY驱动框架会保证config_init被调用但U-Boot的框架相对简化必须确认调用链完整。7.4 关于YT8521SH的功耗与散热最后提一个容易被忽略的事YT8521SH在全速率工作时的功耗比经典PHY比如RTL8211系列要高一些如果PCB散热设计一般长时间跑满吞吐会让芯片温度明显升高。温度漂移会影响内部延迟电路的准确性进而导致RGMII时序裕量变差。如果你的板子测试环境温度能到60度以上建议在初始化代码里加一个简单的寄存器回读检查比如每100秒读一次PHY状态寄存器防止出现热了就不通的隐性故障。8. 最后再分享一个实用的调试技巧调试RGMII时序的时候别只盯着寄存器配置把PHY的环回模式用起来能省很多时间。YT8521SH支持通过0x0000寄存器BMCR的bit 14开启near-end loopback。配置方法 mii write 0x04 0x0000 0x4000开启环回后MAC发出的数据会直接回到MAC的接收路径不经过外部网络。这时候跑ping如果通了说明MAC侧逻辑和时序没问题如果不通问题就在MAC侧和PHY无关。反过来说如果环回模式下一切正常但接上外部交换机就不通那问题就在PHY的对外路径上优先检查PHY的TX/RX延迟配置以及外部变压器的连接。这个技巧的价值在于它帮你把MAC侧和PHY侧的问题快速隔离开。没有这个手段你只能对着波形猜效率低很多。YT8521SH这颗芯片本身没有太多坑它只是需要你在初始化阶段明确告诉它工作模式和延迟策略不像一些欧美系PHY那样自动协商好一切。把这几个关键寄存器配好、时序约束写对U-Boot下用起来和其他主流PHY没有任何区别。希望这篇文章能帮你把三天调试时间压缩到半天。