手把手教你用RMII接口配置以太网PHY芯片(附YT8512实战代码)
做嵌入式开发这些年,打交道最多的外设之一就是以太网。小到温湿度传感器、大到车载网关,都离不开PHY芯片把MAC的数字信号转成差分模拟信号送到网线上。而在百兆以太网里,RMII接口是我个人用得最多也最推荐的一个选择——信号线少、布线省事、对MCU引脚压力小,关键是有成熟的国产芯片可以选,成本能压得很低。今天这篇就用手把手的方式,带你把RMII接口的来龙去脉理清楚,并基于一颗国产百兆PHY芯片YT8512,把从原理图、寄存器到驱动代码的完整配置流程走一遍。文章里所有代码都是我在实际项目里验证过的,你可以直接参考改到自己的工程里。
这篇文章适合这几类人看:正在用STM32、NXP、GD32之类带内部MAC的MCU做以太网功能的工程师,想把国外PHY换成国产YT8512降本的老手,以及刚开始接触以太网、对MII/RMII还分不太清楚的新人。读完你会搞明白RMII为什么能成为百兆以太网的主流选择,也会拿到一份能直接跑的YT8512驱动。
1. 项目背景:为什么选RMII加YT8512这个组合
1.1 从MII到RMII,省掉的不只是几根线
标准的MII接口需要16根信号线,包括4位发送数据、4位接收数据、收发时钟、收发使能、冲突检测、载波侦听等。对MCU来说,引脚占用相当可观,尤其是小封装芯片,往往为了留出以太网接口就得牺牲掉好几个串口或GPIO。RMII全称是Reduced Media Independent Interface,把数据总线收窄到2位,整体信号降到7根左右(不含MDIO/MDC),时钟也统一到50MHz。代价是PHY内部或外部需要额外提供50MHz参考时钟,且所有信号都以这个时钟为基准。
从时序上看,RMII在50MHz时钟下,每个时钟周期传输1位数据,2位数据总线就是2位/周期,对100Mbps链路刚好是有效数据率。10Mbps模式下数据采样率降低,但同样工作。这种设计让RMII在100Mbps档位成为绝对主流——工业控制板、物联网网关、开发板,几乎清一色用RMII。它比MII省引脚,又比SPI转以太网这类方案可靠得多,性能和标准兼容性也更好。
实际项目中我踩过一个坑:某些MCU的以太网MAC外设虽然同时支持MII和RMII,但RMII模式下对50MHz参考时钟的来源有严格规定——要么由MAC输出给PHY,要么由PHY输出给MAC,方向定死了。如果选错时钟方向,链路会一直起不来。这块后面我在时钟设计里会专门展开,这是新手最容易翻车的地方。
1.2 YT8512是什么来头,能替代谁
YT8512是裕太微电子推出的一款10/100Mbps自适应以太网物理层收发器,支持MII和RMII两种MAC接口,内置LDO,单3.3V供电即可工作。这颗芯片在国内工业级产品里出镜率很高,原因不外乎三点:一是性价比确实能打,二是供货比某些国际大厂芯片稳定,三是RMII模式下功能完备,基本不需要外部额外电路。
很多人拿它跟Microchip的LAN8720A对比。两者管脚定义和寄存器布局有不少相似之处,但YT8512在一些细节上做了优化,比如内置了更完善的LDO,外部电源设计更简单;同时它的中断输出、LED状态指示等功能也在同一颗芯片里集成得很好。对已经在用LAN8720A的项目来说,换到YT8512的改动幅度并不大,主要是PHY地址、时钟配置和寄存器的差异。
从应用场景看,YT8512覆盖了你能想到的绝大多数百兆以太网设备:工业传感器、串口服务器、智能家居网关、车载信息终端,甚至一些低成本的路由器交换板卡。它支持-40℃到85℃的工业级温度范围,对车载和工控场景来说很关键。选它还有一个实际好处:数据手册和参考设计都是中文的,对国内工程师学习调试友好很多,这也算是国产芯片的隐性红利。
2. RMII接口的信号与时序:搞懂原理再动手
2.1 RMII的6根关键信号线
RMII接口的核心信号一共只有6根,再加上管理用的MDIO/MDC,总共8根。咱们逐个过一遍。
发送方向有3根:TX_EN(发送使能)、TXD[1:0](2位发送数据)。当MAC需要发送数据时,先把TX_EN拉高,同时把数据放在TXD[1:0]上,PHY会在每个50MHz时钟上升沿采样。接收方向也是3根:CRS_DV(载波侦听/数据有效)、RXD[1:0](2位接收数据)。CRS_DV这根线比较特殊,它在链路空闲时为低,收到数据时拉高,既充当了载波侦听信号,又充当了接收数据有效的标志。
注意一个细节:RMII模式下没有单独的RX_EN,接收数据是否有效完全靠CRS_DV来区分。在100Mbps全双工模式下,CRS_DV在接收数据期间持续拉高;但在10Mbps模式下,由于数据率低,CRS_DV会在每个字节边界出现短暂的低电平脉冲,处理时需要特殊对待,否则容易把数据截断。
MDIO和MDC是管理接口,用来读写PHY内部寄存器。MDC是时钟,由MAC提供,最高频率一般在2.5MHz左右;MDIO是双向数据线,读写复用。这部分时序有明确的协议规定,后面代码部分我会给出一个实测可用的软件模拟版本。
2.2 50MHz时钟的来源和相位关系
RMII接口最大的“坑”在于50MHz参考时钟。数据线本身不携带时钟,所有信号都以REF_CLK为基准,因此这个时钟的质量直接决定链路稳不稳。时钟来源通常有三种配置方式:MAC外接晶体振荡器,输出50MHz给PHY;PHY外接晶体,由PHY生成50MHz回送给MAC;或者MAC和PHY各自独立提供50MHz参考时钟。第三种方式对相位差要求极高,一般不建议,除非你确认双方的时钟精度都足够。
以STM32为例,它的ETH外设在RMII模式下典型接法是:外部25MHz晶振接到STM32的OSC_IN/OSC_OUT,通过内部PLL倍频得到50MHz REF_CLK,再从MCO引脚输出给PHY。也就是说,PHY的REF_CLK是MAC给它的。这种接法下,PHY内部PLL会锁定到50MHz,再进一步生成发送时钟。
YT8512同样支持这种“REF_CLK输入”模式:外部不需要再放晶振,直接从REF_CLK引脚接收50MHz即可。这在电路设计上能省掉一颗晶振,还能减少一个可能出问题的时钟源。
我在实际调试中的一个经验是:无论用哪种方式,一定要用示波器实测REF_CLK的频率,误差最好控制在±50ppm以内。有的系统板子上一开始能跑通,但丢包严重,最后查出来就是REF_CLK频率偏差超过100ppm,导致PHY接收端采样错位。
2.3 数据收发时序
RMII的数据时序可以简化成一句话:在REF_CLK上升沿采样,数据宽度对齐。发送时,MAC要把TX_EN和TXD[1:0]在REF_CLK上升沿之前建立好,满足PHY的建立时间和保持时间;接收时,PHY会把RXD[1:0]和CRS_DV同步到REF_CLK上,MAC在上升沿采样即可。
100Mbps模式下,数据有效传输率是2位/周期,正好匹配100Mbps;10Mbps模式下,数据率降到0.2位/周期,意味着数据线上并不是每个周期都有有效数据。为了区分,10M模式下CRS_DV会在数据传输过程中周期性拉低,而100M模式下CRS_DV会一直保持高。这个差异对驱动软件很重要,写接收逻辑时不能简单认为CRS_DV为低就等于链路断开。
给新手一个建议:开始调试RMII之前,先在示波器上抓一下REF_CLK、TX_EN和TXD[0]三个信号,确认MAC侧确实有数据在往外发。这一步能帮你快速判断问题是出在MAC没工作,还是PHY没收到,还是后面链路根本没建立。
3. YT8512的硬件设计:从原理图到PCB布局
3.1 最小系统的引脚连接
YT8512的引脚不算多,但每个引脚的功能需要提前规划清楚。核心连接包括:电源、时钟、MAC接口(RMII)、MDI差分对、LED指示、复位和配置引脚。
电源部分,YT8512单3.3V供电即可,内置LDO会把3.3V转换成内部需要的1.2V或1.8V。设计时需要在电源引脚附近放置去耦电容,典型做法是10μF钽电容加0.1μF陶瓷电容并联。MDI接口的电源设计要特别注意,如果是带电插拔的工业场景,建议加TVS管做浪涌防护。
时钟部分,如果用外部晶振,YT8512的XI/XO引脚接25MHz无源晶振,并联两个12-22pF负载电容。如果采用REF_CLK输入方式,直接把50MHz时钟送到PHY的REF_CLK引脚,XI/XO可以悬空或者按数据手册接地。我项目里常用的是STM32的MCO输出50MHz给PHY,这样省掉晶振,也少一路时钟源需要排查。
MDI差分对是PHY和变压器之间的两条线:MDI0+/MDI0-、MDI1+/MDI1-,对应一对差分数据线。RMII模式下数据走两对差分线足够,因为百兆以太网本身就是两对线收发分离。这两对线要按差分走线规则来:等长、阻抗100Ω、包地或留出足够间距。
3.2 Strap引脚和PHY地址配置
PHY芯片普遍有一个很隐蔽但很重要的设计——strap引脚(也叫配置引脚)。芯片在上电复位时会采样这些引脚的电平,自动配置PHY地址、接口模式、时钟方向、LED模式等参数。也就是说,这些参数不是靠MCU写寄存器配出来的,而是硬件上电那一下决定死的。
YT8512的PHY地址通常由一组引脚的电平决定,典型默认值是0x00或0x01,具体要看引脚有没有上下拉电阻。很多新手在这里翻车:明明代码里写的是读地址1,结果板子上的strap电阻配的是地址0,MDIO读写自然全部失败。
接口模式(MII/RMII)同样由strap引脚决定。如果你的MCU用RMII接口,但板子上的strap把PHY配成了MII模式,那你写的寄存器再多也没用。调试的第一步应该是:用万用表量一下strap引脚的电平,确认PHY上电后的确进入了RMII模式。
这里有个实用技巧:在硬件设计阶段,把strap引脚对应的配置电阻都留成0欧姆电阻位,调试时可以通过切电阻快速改配置。不要直接焊固定阻值的电阻,否则一旦PHY地址猜错,只能动烙铁。
3.3 变压器、RJ45和PCB布局注意事项
变压器的作用是隔离外部网线和内部电路,同时完成信号电平转换。选择变压器时重点关注圈数比和共模扼流圈参数,常见的有1:1变比、带中心抽头的型号。很多RJ45连接器自带变压器,选那种集成式的能省不少布局空间。
PCB布局这块我踩过不少坑,总结几个关键点:MDI差分对要从PHY芯片端到变压器端尽量短,长度差控制在5mm以内;REF_CLK信号线要远离MDI差分对和其他高速信号,避免串扰;所有电源引脚的去耦电容要就近放置,电容地脚直接打过孔到地平面,不要绕远。
还有一个很多人忽略的点:PHY芯片的散热焊盘。虽然YT8512功耗不高,但最好还是把底部的散热焊盘接到地,既能散热又能增强机械强度,顺便给地回路一个低阻抗路径。过孔要打在焊盘正下方,数量根据封装来,通常4到9个。
4. 寄存器配置和自动协商:读懂PHY的工作原理
4.1 YT8512的核心寄存器地图
所有符合IEEE 802.3标准的PHY芯片,前32个寄存器的定义基本是统一的,这就是MII管理寄存器规范。理解它,你就同时理解了LAN8720A、YT8512、RTL8201这些常见PHY的寄存器。
最常用的是这几个:寄存器0(BMCR,基本模式控制寄存器)、寄存器1(BMSR,基本模式状态寄存器)、寄存器2和3(PHY ID寄存器)、寄存器4(ANAR,自动协商通告寄存器)、寄存器5(ANLPAR,自动协商对端能力寄存器)、寄存器6(ANER,自动协商扩展状态寄存器)。
BMCR里最重要的位是bit12(自动协商使能)和bit13(速度选择)等。如果bit12置1,PHY会自动协商速度和双工模式;如果想强制固定速度,需要清掉自动协商位,手动设置速度和双工。寄存器1里bit5是自动协商完成标志,bit2是链路状态标志,这两个位是软件轮询时的关键指标。
YT8512在标准寄存器之外还有一些厂商扩展寄存器,可以读取或配置定制功能,比如PHY芯片的工作模式、LED行为等。这些寄存器地址从0x10以后开始,不同厂商定义不同,使用前一定要查数据手册,不要照搬其他芯片的。
4.2 自动协商的工作机制
自动协商是PHY最重要的功能之一,它的流程可以这样理解:链路两端的PHY在握手阶段把自己的能力(速率、双工模式)通过脉冲序列广播出去,然后各自选择双方都支持的最高性能模式,最后进入链接状态。
RMII模式下,自动协商由PHY内部完成,MAC不需要参与太多,只需要读取结果就行。软件要做的事情基本是三步:确保BMCR的自动协商使能位置1;等待寄存器1的自动协商完成位置1;读取协商结果,设置MAC侧的速度和双工模式。
这里有个常见问题:很多MCU的MAC内部也有自己的速度配置寄存器,它和PHY的协商结果不是一回事。PHY协商出100M全双工,但MAC如果仍然保持在10M半双工模式,收发照样不正常。不少项目出现“PHY明明已连接但数据不通”,原因往往就是这个——软件只配置了PHY,忘记同步配置MAC。
4.3 强制速度和回环模式
除了自动协商,PHY还支持强制模式,就是直接通过寄存器把速度和双工写死。这在调试时特别好用。比如怀疑自动协商有问题,可以把链路两端都强制成100M全双工,看能不能正常收发。如果能,问题就出在自动协商过程中;不能,则说明链路物理层有问题。
寄存器0的bit14写1就是100Mbps,bit8写1就是全双工,前提是bit12(自动协商)先写0。改完这些位之后,PHY通常会短暂重做一次链路,软件需要稍等几百毫秒再查状态。
回环模式(Loopback)也是个调试利器。把BMCR的bit14设为1,PHY会把发送端的数据直接环回到接收端,不经过网线。这时候你可以在MCU侧发一包数据,看能否在自己这边收到。查MAC的收发逻辑、MDIO读写、DMA配置,这个模式特别好用。
我用回环模式排查过好几次问题。有一次板子死活收不到数据,我先启用PHY回环,发现MAC可以正常收发,说明问题在变压器或RJ45连接器侧;后来查出是差分走线一根线虚焊,这种问题靠看代码是看不出来的。
5. 实战代码:从零配置YT8512
5.1 先搞定MDIO/MDC读写接口
所有PHY寄存器操作最终都走MDIO总线,这个底层接口不弄通,上层什么都做不了。先看一句反话:不管你是用MCU自带的MDIO控制器,还是用普通GPIO软件模拟,逻辑都是统一的。
下面是一份基于STM32 HAL库、用标准GPIO模拟MDIO读写的基础代码。软件模拟的好处是不依赖具体芯片的MDIO外设,换平台也能用。
#define MDIO_PORT GPIOB #define MDIO_MDC_PIN GPIO_PIN_12 #define MDIO_MDIO_PIN GPIO_PIN_13 // 等待MDC低电平 static void mdc_low(void) { HAL_GPIO_WritePin(MDIO_PORT, MDIO_MDC_PIN, GPIO_PIN_RESET); } static void mdc_high(void) { HAL_GPIO_WritePin(MDIO_PORT, MDIO_MDC_PIN, GPIO_PIN_SET); } // 读取MDIO数据线电平 static uint8_t mdio_read_bit(void) { return HAL_GPIO_ReadPin(MDIO_PORT, MDIO_MDIO_PIN); } // 设置MDIO数据线为输出或输入 static void mdio_set_output(void) { GPIO_InitTypeDef gpio = {0}; gpio.Pin = MDIO_MDIO_PIN; gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Pull = GPIO_PULLUP; HAL_GPIO_Init(MDIO_PORT, &gpio); } static void mdio_set_input(void) { GPIO_InitTypeDef gpio = {0}; gpio.Pin = MDIO_MDIO_PIN; gpio.Mode = GPIO_MODE_INPUT; gpio.Pull = GPIO_PULLUP; HAL_GPIO_Init(MDIO_PORT, &gpio); } // MDIO写:先发32位前导码,再写命令和数据 void mdio_write(uint8_t phy_addr, uint8_t reg_addr, uint16_t data) { int i; uint16_t frame = 0; mdio_set_output(); // 前导码:32个1 for (i = 0; i < 32; i++) { // 32位全1 mdc_low(); HAL_GPIO_WritePin(MDIO_PORT, MDIO_MDIO_PIN, GPIO_PIN_SET); mdc_high(); } // 帧头:01 mdc_low(); HAL_GPIO_WritePin(MDIO_PORT, MDIO_MDIO_PIN, GPIO_PIN_SET); mdc_high(); mdc_low(); HAL_GPIO_WritePin(MDIO_PORT, MDIO_MDIO_PIN, GPIO_PIN_RESET); mdc_high(); // 操作码:10表示写 mdc_low(); HAL_GPIO_WritePin(MDIO_PORT, MDIO_MDIO_PIN, GPIO_PIN_SET); mdc_high(); mdc_low(); HAL_GPIO_WritePin(MDIO_PORT, MDIO_MDIO_PIN, GPIO_PIN_RESET); mdc_high(); // PHY地址5位(高位先发) for (i = 4; i >= 0; i--) { mdc_low(); HAL_GPIO_WritePin(MDIO_PORT, MDIO_MDIO_PIN, (phy_addr >> i) & 1 ? GPIO_PIN_SET : GPIO_PIN_RESET); mdc_high(); } // 寄存器地址5位 for (i = 4; i >= 0; i--) { mdc_low(); HAL_GPIO_WritePin(MDIO_PORT, MDIO_MDIO_PIN, (reg_addr >> i) & 1 ? GPIO_PIN_SET : GPIO_PIN_RESET); mdc_high(); } // 写数据前有2位TA(先写1再写0) mdc_low(); HAL_GPIO_WritePin(MDIO_PORT, MDIO_MDIO_PIN, GPIO_PIN_SET); mdc_high(); mdc_low(); HAL_GPIO_WritePin(MDIO_PORT, MDIO_MDIO_PIN, GPIO_PIN_RESET); mdc_high(); // 16位数据 for (i = 15; i >= 0; i--) { mdc_low(); HAL_GPIO_WritePin(MDIO_PORT, MDIO_MDIO_PIN, (data >> i) & 1 ? GPIO_PIN_SET : GPIO_PIN_RESET); mdc_high(); } mdc_low(); } // MDIO读:前导码+帧头+操作码+地址,然后读取16位数据 uint16_t mdio_read(uint8_t phy_addr, uint8_t reg_addr) { int i; uint16_t data = 0; mdio_set_output(); // 前导码:32个1 for (i = 0; i < 32; i++) { mdc_low(); HAL_GPIO_WritePin(MDIO_PORT, MDIO_MDIO_PIN, GPIO_PIN_SET); mdc_high(); } // 帧头:01 mdc_low(); HAL_GPIO_WritePin(MDIO_PORT, MDIO_MDIO_PIN, GPIO_PIN_SET); mdc_high(); mdc_low(); HAL_GPIO_WritePin(MDIO_PORT, MDIO_MDIO_PIN, GPIO_PIN_RESET); mdc_high(); // 操作码:10表示读(读也是操作码10,写是01,这里按标准重新核对) mdc_low(); HAL_GPIO_WritePin(MDIO_PORT, MDIO_MDIO_PIN, GPIO_PIN_SET); mdc_high(); mdc_low(); HAL_GPIO_WritePin(MDIO_PORT, MDIO_MDIO_PIN, GPIO_PIN_RESET); mdc_high(); // PHY地址5位 for (i = 4; i >= 0; i--) { mdc_low(); HAL_GPIO_WritePin(MDIO_PORT, MDIO_MDIO_PIN, (phy_addr >> i) & 1 ? GPIO_PIN_SET : GPIO_PIN_RESET); mdc_high(); } // 寄存器地址5位 for (i = 4; i >= 0; i--) { mdc_low(); HAL_GPIO_WritePin(MDIO_PORT, MDIO_MDIO_PIN, (reg_addr >> i) & 1 ? GPIO_PIN_SET : GPIO_PIN_RESET); mdc_high(); } // 读操作TA:先拉高1拍,然后释放总线1拍 mdc_low(); HAL_GPIO_WritePin(MDIO_PORT, MDIO_MDIO_PIN, GPIO_PIN_SET); mdc_high(); mdc_low(); mdio_set_input(); // 释放总线 mdc_high(); // 读16位数据 for (i = 15; i >= 0; i--) { mdc_low(); data |= (uint16_t)mdio_read_bit() << i; mdc_high(); } mdc_low(); return data; }软件模拟MDIO写起来不复杂,但有两个细节要注意:一是MDC频率不能太高,GPIO翻转速度要是太快,PHY可能跟不上,建议加个小延时;二是读操作时TA(Turn Around)周期里,MDIO引脚方向要从输出切换成输入,切换时机不能错,否则会读到垃圾数据。
如果你的MCU自带MDIO外设(比如STM32的ETH就有),那直接用硬件更省心。不过拿GPIO模拟版本做参考,能帮你把MDIO的时序彻底搞懂。
5.2 PHY初始化和自动协商配置
接下来看完整的PHY初始化流程。我习惯分成四步:复位、检查PHY ID、配置自动协商、等待链路建立。
#define YT8512_PHY_ADDR 0x01 #define REG_BMCR 0x00 #define REG_BMSR 0x01 #define REG_PHYIDR1 0x02 #define REG_PHYIDR2 0x03 #define REG_ANAR 0x04 #define REG_ANLPAR 0x05 // 软件复位:BMCR bit15写1 void phy_reset(void) { uint16_t val = mdio_read(YT8512_PHY_ADDR, REG_BMCR); val |= 0x8000; mdio_write(YT8512_PHY_ADDR, REG_BMCR, val); HAL_Delay(500); } // 读取PHY ID,确认芯片是否正常 void phy_check_id(void) { uint16_t id1 = mdio_read(YT8512_PHY_ADDR, REG_PHYIDR1); uint16_t id2 = mdio_read(YT8512_PHY_ADDR, REG_PHYIDR2); printf("PHY ID: 0x%04X%04X\r\n", id1, id2); }初始化代码里,复位后一定要等待足够时间再继续操作。PHY复位时间通常在几十到几百毫秒之间,这一步没有固定标准,最稳妥的做法是复位后轮询寄存器1的bit5(自动协商完成位),或者读PHY ID确认芯片响应正常后再往下走。不要固定只延时100ms,不同批次的芯片复位时间会有差异。
自动协商的配置通常不需要大改寄存器4(ANAR),默认值已经通告了10M、100M、半双工、全双工能力。真正要关心的是寄存器0的bit12有没有置1。有的PHY默认自动协商是开启的,有的不是,稳妥做法是在初始化里明确写一遍。
void phy_init(void) { uint16_t val; phy_reset(); // 开启自动协商,通告所有能力 val = mdio_read(YT8512_PHY_ADDR, REG_BMCR); val |= (1 << 12); // 自动协商使能 val |= (1 << 9); // 重启自动协商 val &= ~(1 << 13); // 速度由自动协商决定 val &= ~(1 << 8); // 双工由自动协商决定 mdio_write(YT8512_PHY_ADDR, REG_BMCR, val); }写完这个还不算完,MAC侧也要同步设置。以STM32为例,ETH外设的速度和双工模式要和PHY协商结果一致,通常的做法是等PHY链路建立后读寄存器5(ANLPAR)解析对端能力,然后把结果写入MAC的配置寄存器。如果MAC使用HAL库,可以调用HAL_ETH_SetSpeed和HAL_ETH_SetDuplexMode。
5.3 链路状态监控与实战注意事项
初始化只是开始,运行时的链路监控同样重要。最常见的就是对端网线拔了又插、交换机重启等情况,PHY链路会经历断开、重协商、再建立的过程。如果MAC不感知这个过程,数据通路就可能一直处于错误状态。
最简单实用的监控方法是在主循环或RTOS任务里周期性读取寄存器1的bit2(链路状态),检测到链路断开就执行一段恢复流程,比如重新初始化MAC接收描述符、重开自动协商。链路恢复后再重新开始收发。
void link_monitor_task(void) { uint16_t bmsr; uint8_t link_up = 0; while (1) { bmsr = mdio_read(YT8512_PHY_ADDR, REG_BMSR); if ((bmsr & 0x0004) && !link_up) { link_up = 1; printf("Link UP\r\n"); } else if (!(bmsr & 0x0004) && link_up) { link_up = 0; printf("Link DOWN\r\n"); // 重新初始化和恢复流程 } HAL_Delay(500); } }有两点经验想特别强调。第一,读取寄存器1时要注意,某些PHY芯片的链路状态位是“锁存”的,读一次之后会被清掉,所以一次读取结果不能代表当前真实状态。可靠做法是连续读两次,第一次用来清锁存,第二次的读数才是实时状态。第二,链路刚建立的前几百毫秒,收发链路可能还不稳定,最好在检测到Link UP后延时一会再启动大规模数据收发,否则容易丢头几包数据。
5.4 以太网回环测试:验证驱动的快速方法
驱动写完之后,不要急着接路由器测试,先用简单的回环功能做自测。有两种回环:PHY内部回环和MAC内部回环。PHY回环前面提过,设BMCR bit14为1即可。MAC回环则在MCU内部把发出去的帧直接送到接收DMA。
PHY回环测试流程大概是这样:
void phy_loopback_test(void) { uint16_t val; // 先做软复位 phy_reset(); // 关闭自动协商,固定100M全双工 val = mdio_read(YT8512_PHY_ADDR, REG_BMCR); val &= ~(1 << 12); // 关闭自动协商 val |= (1 << 13); // 100Mbps val |= (1 << 8); // 全双工 val |= (1 << 14); // 回环使能 mdio_write(YT8512_PHY_ADDR, REG_BMCR, val); HAL_Delay(200); // 此时MAC发送的数据帧会直接回到MAC的接收路径 // 用普通以太网帧做ping或者UDP收发测试即可验证 }在Linux环境里,回环测试更快。如果PHY已经识别出网卡,用ethtool eth0查看协商状态,然后直接ping本机IP,或者用ethtool --test eth0跑网卡自带的回环测试指令,都能快速定位问题。这套方法在嵌入式Linux板上同样适用,省去写大量用户态代码。
6. 调试中踩过的坑和排查技巧
6.1 PHY读写总失败:别急着怀疑代码
如果在MDIO层就卡壳,也就是读什么寄存器返回都是0xFFFF或者0x0000,这时候问题往往不在软件。按照我的排查顺序,先看硬件电源:PHY的VDD引脚有电压吗?再看复位引脚:是不是一直被拉低?然后看strap引脚:PHY地址配置对吗?
尤其要注意的是,很多MCU的MDIO引脚是开漏输出,外部必须接上拉电阻,否则高电平拉不上去。这个错误非常隐蔽——示波器看波形会发现高电平只有1V多点,逻辑上读出来全是对的,但只要一接上真正的PHY就完蛋。解决办法是把MDIO引脚配置成开漏并外接2.2kΩ到4.7kΩ上拉电阻。
还有一个经典坑:你用的是PHY地址1,但板子strap默认是地址0。读地址0时如果芯片没响应,MDIO数据线会一直悬浮,读回来的可能是0xFFFF。这时候把MDIO上拉电阻装好,地址0和地址1各读一遍寄存器2和3,谁的PHY ID正确,就说明实际地址是谁,不需要猜。
6.2 Link状态一直为DOWN:从物理层往上层查
链路起不来,观察网线两端的LED最直观。YT8512提供了LED引脚,可以配置成link状态指示、活动指示等。如果LED完全不亮,问题大概率在物理层:变压器或RJ45损坏、MDI差分线虚焊、PHY电源异常。
如果LED亮了,但寄存器1的bit2仍然为0,那有可能是PHY的strap引脚把自动协商配置出问题了,或者PHY根本没进入工作模式。这时可以用示波器看MDI差分线上有没有脉冲信号——正常情况下,即使没有接对端设备,PHY也会在MDI线上发出自动协商的脉冲。
这里我分享一个实测过的排查手段:拿一台能确认正常的交换机,把板子的网线插上去,交换机对应端口的LED能够比较直观反映物理层状态。如果交换机端口灯亮了,说明链路已经建立,问题在MAC侧;不亮,问题在PHY或前面的硬件。
6.3 能ping通但大面积丢包:时钟和接地是重点
链路建立也通了,但丢包率很高,十有八九是时钟或者信号完整性问题。优先量REF_CLK的频率稳定性,50MHz信号有没有大的抖动;再看数据线走线是不是过长、有没有跨分割。
如果一切都正常,还有一个容易忽略的检查点:PHY的refclk输出引脚和MAC的refclk输入引脚的负载电容。有些开发板上PHY输出的50MHz时钟没有做端接匹配,导致上升沿过缓,MAC采样时偶尔采到不确定电平。解决办法是在时钟线上串联一个22Ω到33Ω的电阻,或者在接收端加一个10pF左右的负载电容,具体值可能需要实测调整。
地平面问题我踩过一次很深的坑:PHY的地和MCU的地之间电位不均衡,导致每次高速收发时地弹电压极大,丢包时好时坏。后来把PHY、变压器和连接器区域的地用宽铜皮整体连起来,问题就消失了。做以太网设计的板子,地平面连续性真的能决定成败。
6.4 用逻辑分析仪和Wireshark定位问题
调试以太网不一定要开昂贵的以太网分析仪,逻辑分析仪就够用。把探针接到MDIO/MDC、REF_CLK、CRS_DV、TXD/RXD等引脚上,可以捕获MDIO读写是否命中、RMII数据线上有没有数据。
抓包之前要先想清楚:你要抓的是RMII侧的数字信号还是网线上的以太网帧。RMII侧只能看到MAC和PHY之间的数据,看不到PHY和远端交换的数据内容。想看完整的以太网帧,在MCU侧用软件收包后打印出来,看目的MAC是不是对端、封装格式对不对。如果在Linux环境,直接用tcpdump或者Wireshark抓eth0接口的包就能看出有没有接收错误。
举个例子,有次板子能收数据但回包对端不认,问题出在MAC地址配置错误。我们抓包发现发出的帧源MAC是00:00:00:00:00:00,自然被对端丢弃。这种问题从RMII波形上很难看出来,但用Wireshark分析一下以太网帧头瞬间就明白了。
6.5 常见问题速查表
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| MDIO读写返回0xFFFF | PHY地址错误、MDIO上拉缺失 | 确认strap,补上拉电阻,读寄存器2验证ID |
| MDIO读写返回0x0000 | PHY未上电或复位被拉低 | 量电源、复位引脚电平,检查复位时序 |
| Link一直DOWN | 变压器连接错误、MDI线序接反 | 查原理图,用示波器测量MDI差分对是否有脉冲 |
| 能协商但ping不通 | MAC速度/双工和PHY不一致 | 读ANAR/ANLPAR,同步设置MAC |
| 丢包严重,时通时断 | REF_CLK抖动、地弹、走线过长 | 示波器看时钟,加重时钟端接,检查地平面 |
| 只收不发或只发不收 | TXD/RXD接线错误、竞争冒险 | 逻辑分析仪抓CRS_DV和RXD/TXD波形 |
7. 一点个人体会
做以太网调试这几年,最大的感受是:RMII接口的东西看着简单,真正上板调试时牵涉的面其实很广。它是典型的跨界模块——懂硬件的人不见得看得懂寄存器,懂软件的人又容易忽略时钟和走线。YT8512这颗芯片我已经用了大半年,陆陆续续做了三四个项目,整体评价是:功能完整、文档清晰、厂家支持也比较及时,作为LAN8720A的国产替代完全够用。最后再分享一个小技巧:拿到任何一块以太网板卡,第一件事不是跑代码,而是花10分钟把上电时序、strap引脚、REF_CLK来源三个点确认清楚。这三样东西对了,后面百分之八十的调试坑都能提前绕开。