1. 为什么LAN8720在CubeMX里总是不通:先搞清楚RMII和时钟的底层逻辑
搞STM32以太网的人,十个里有八个在LAN8720上翻过车。我自己第一次用STM32F407配LAN8720的时候,CubeMX里点几下生成了代码,烧进去ping不通,示波器一打波形发现REF_CLK根本没有输出,折腾了整整一个下午才发现是PHY地址和时钟源的问题。后来陆续用STM32F107、F429、H743配过LAN8720、YT8512、DP83848,踩的坑多了,慢慢总结出一些CubeMX图形化配置界面里“看不见”的细节。
这篇内容主要面向正在用STM32CubeMX配置LAN8720以太网外设的嵌入式开发者,尤其是用RMII接口、自己画板子或者用现成模块的朋友。我会把RMII时钟的来龙去脉、PHY地址的确定方法、CubeMX里那些容易漏掉的勾选项,以及实际调试中怎么用示波器和寄存器读写来定位问题,全部拆开讲清楚。不管你是第一次接触以太网,还是已经调过几块板子但总有些莫名其妙的问题,这些细节都值得过一遍。
LAN8720是一颗10/100M自适应的以太网PHY芯片,支持RMII和MII两种MAC-PHY接口。在STM32的生态里,绝大多数人用的是RMII模式,因为引脚少、接线简单。但RMII有个硬性要求:MAC和PHY必须共享同一个50MHz参考时钟。这个50MHz时钟可以由外部晶振直接提供,也可以由STM32的MCO引脚输出,还可以由LAN8720自己产生。三种方案各有各的坑,CubeMX里对应的配置项也不一样。
很多人以为CubeMX里把ETH选上、引脚分配好就完事了,实际上CubeMX只帮你生成了GPIO和ETH外设的初始化代码,PHY地址、时钟源选择、RMII模式确认这些关键参数,要么在CubeMX里藏得比较深,要么需要你自己在代码里补。更麻烦的是,CubeMX不同版本对这些选项的呈现方式还不一样,有的版本默认值就是错的。
下面我按实际调试中问题出现的频率,从时钟方案开始,一个一个拆。
2. RMII的50MHz参考时钟:三种方案的选择逻辑与CubeMX配置差异
2.1 方案一:外部有源晶振直接喂50MHz
这是最省心也最不容易出问题的方案。板子上放一颗50MHz的有源晶振,输出直接连到LAN8720的REF_CLK引脚(第5脚)和STM32的ETH_RMII_REF_CLK引脚(PA1)。两边同时收到同一个50MHz时钟,MAC和PHY的时序天然对齐。
这种方案在CubeMX里怎么配?打开Connectivity -> ETH,在Parameter Settings里把PHY Interface改成RMII,然后注意看下面有个“RMII Clock Source”或者类似的选项。不同版本的CubeMX这个选项位置不一样,有的在ETH的Parameter Settings里,有的在Clock Configuration页面。如果用的是外部晶振直接供时钟,这个选项要选“External Clock”或者直接不勾选MCO输出。
我实测过几块板子,外部有源晶振方案的成功率最高,基本上只要焊接没问题、晶振起振正常,CubeMX生成代码后直接就能通。但要注意有源晶振的供电和使能引脚,有些封装的有源晶振有个OE引脚,悬空可能不输出,必须拉高或者拉到指定电平。
2.2 方案二:STM32的MCO1输出50MHz给PHY
这个方案在成本敏感的产品里很常见,省掉一颗有源晶振。STM32F4系列的MCO1可以输出PLL分频后的时钟,配置成50MHz输出到PA8,然后PA8接到LAN8720的REF_CLK。
CubeMX里的配置步骤稍微多一点。首先在Clock Configuration页面,找到MCO1的时钟源选择,一般选PLLCLK或者HSE。然后设置MCO1的分频系数,让输出正好是50MHz。比如HSE是8MHz,PLL配置好后PLLCLK是168MHz,MCO1分频系数设成4,168/4=42MHz,不对。得仔细算,让MCO1的输出精确等于50MHz。
这里有个容易忽略的点:MCO1的输出频率精度直接影响RMII通信的稳定性。50MHz允许的偏差一般在±50ppm以内,用PLL分频出来的时钟,如果PLL的参考晶振本身精度不够,或者分频系数算出来不是整数,累积误差可能导致通信不稳定。我遇到过一块板子,MCO输出49.8MHz,ping通但丢包率很高,换成外部晶振后立刻正常。
在CubeMX的ETH配置里,如果选MCO输出,要把“RMII Clock Source”设成“MCO1”或者对应的选项。有些版本的CubeMX不会自动帮你配置MCO1,需要你手动在Clock Configuration里使能。
2.3 方案三:LAN8720自己产生50MHz,反喂给STM32
LAN8720有一个REF_CLK输出模式,可以在它的XTAL1/XTAL2引脚接一颗25MHz晶振,然后内部PLL倍频到50MHz,从REF_CLK引脚输出给STM32。这个方案省了50MHz有源晶振,但多了一颗25MHz无源晶振。
关键点在于LAN8720的REF_CLK引脚方向。在RMII模式下,LAN8720的REF_CLK既可以作为输入(接收外部50MHz),也可以作为输出(产生50MHz给MAC)。这个方向由芯片内部的寄存器或者引脚配置决定。具体来说,LAN8720的nINT/REFCLKO引脚(第14脚)和REGOFF引脚(第20脚)的电平组合决定了时钟模式。
我查过LAN8720的数据手册,当REGOFF拉低、nINT/REFCLKO配置为REFCLKO输出时,LAN8720会从REF_CLK引脚输出50MHz。但这里有个坑:很多现成的LAN8720模块,比如那种十几块钱的蓝色小板,默认是把REF_CLK配置成输入的,也就是期待外部给它50MHz。如果你直接拿这种模块,想让它输出时钟给STM32,需要改板上的电阻或者跳线。
CubeMX里对应的配置是:ETH的RMII Clock Source选“PHY”或者“External PHY Clock”,意思是时钟来自PHY。但CubeMX不会帮你配置LAN8720的寄存器,那部分得自己写代码或者改硬件跳线。
2.4 三种方案的对比与选型建议
| 方案 | 成本 | 配置复杂度 | 稳定性 | 适用场景 |
|---|---|---|---|---|
| 外部50MHz有源晶振 | 高(一颗有源晶振) | 低 | 最高 | 产品原型、对稳定性要求高的场景 |
| STM32 MCO输出 | 低 | 中 | 中 | 成本敏感、STM32主频足够高的场景 |
| LAN8720自产时钟 | 中(一颗25MHz无源晶振) | 高 | 中高 | 模块化设计、想省有源晶振的场景 |
我个人建议,如果是第一次调LAN8720,直接用外部有源晶振方案,先把通信跑通,再考虑换方案省成本。因为时钟问题引起的故障现象很隐蔽,有时候能ping通但丢包,有时候干脆连不上,排查起来非常耗时。
3. PHY地址:从0x00到0x1F,你的LAN8720到底在哪
3.1 PHY地址是怎么确定的
LAN8720的PHY地址由PHYAD0引脚(第10脚)在芯片复位时的电平决定。这个引脚内部有下拉,悬空或者拉低时地址是0x00,拉高时地址是0x01。注意,LAN8720只支持两个地址:0和1。不像有些PHY芯片支持5位地址,LAN8720的地址范围很窄。
但实际使用中,很多模块或者板子会把PHYAD0引脚通过电阻上拉或下拉,还有的模块用LED引脚复用做地址配置。我见过一块板子,PHYAD0通过一个10k电阻上拉到3.3V,地址是0x01,但CubeMX生成的代码里默认PHY地址是0x00,结果HAL_ETH_Init一直返回错误。
3.2 CubeMX里PHY地址在哪里配
CubeMX的ETH配置页面里,有一个“PHY Address”或者“PHY Address Value”的输入框。默认值通常是0。如果你不确定板子上PHYAD0的电平,最稳妥的办法是先用0试,不通再试1。
但这里有个更隐蔽的问题:有些LAN8720模块的PHYAD0引脚被LED电路复用了。比如LAN8720的LED1/PHYAD0是同一个引脚(第10脚),模块上如果接了LED,LED的限流电阻和LED本身会影响复位时的电平。这种情况下,PHY地址可能不是你以为的那个值。
我的做法是:拿一块新板子或者新模块,先用万用表量PHYAD0引脚在复位后的电平,确定地址是0还是1,再去CubeMX里填。如果量不到,就写一段代码,用HAL_ETH_ReadPHYRegister分别读地址0和地址1的PHYID寄存器(地址0x02和0x03),能读出0x0007和0xC0F1的就是LAN8720。
3.3 代码里怎么验证PHY地址
CubeMX生成的代码里,PHY地址通常定义在ethernetif.c或者lan8720.c里,也可能在main.h里有个宏定义。比如:
#define LAN8720_PHY_ADDRESS 0x00你可以写一个简单的测试函数,在ETH初始化之前扫描所有可能的PHY地址:
uint32_t phy_id = 0; for (uint8_t addr = 0; addr < 32; addr++) { HAL_ETH_ReadPHYRegister(&heth, addr, 0x02, &phy_id); if ((phy_id & 0xFFFF) != 0xFFFF && (phy_id & 0xFFFF) != 0x0000) { printf("Found PHY at address: 0x%02X, ID: 0x%08X\n", addr, phy_id); } }这段代码会把总线上所有能响应的PHY地址打出来。LAN8720的PHYID1寄存器(0x02)读出来应该是0x0007,PHYID2(0x03)是0xC0F1。如果你读出来全是0xFFFF,说明MDIO总线有问题,可能是MDIO引脚没配置对或者PHY没供电。
3.4 一个真实的踩坑案例
有一次我用一块STM32F407的开发板配LAN8720模块,CubeMX里PHY地址填的0,死活不通。用上面的扫描代码一跑,发现地址1有响应。回头查原理图,发现开发板上LAN8720的PHYAD0被一个电阻拉高了。改成1之后立刻通了。这个坑的教训是:不要相信默认值,一定要实测。
4. CubeMX里那些默认值会坑你的选项:从GPIO速度到RMII模式确认
4.1 GPIO速度等级:别用默认的Low
CubeMX在配置ETH的RMII引脚时,会自动把相关GPIO配成复用推挽输出。但默认的GPIO速度等级往往是Low或者Medium。对于RMII的50MHz时钟和数据线来说,Low速度的翻转速率不够,会导致信号边沿变缓,眼图变差,通信不稳定。
正确的做法是把ETH相关的GPIO速度设成Very High。具体是哪些引脚?以STM32F407为例:
- PA1: ETH_RMII_REF_CLK
- PA2: ETH_MDIO
- PA7: ETH_RMII_CRS_DV
- PC1: ETH_MDC
- PC4: ETH_RMII_RXD0
- PC5: ETH_RMII_RXD1
- PB11: ETH_RMII_TX_EN
- PB12: ETH_RMII_TXD0
- PB13: ETH_RMII_TXD1
在CubeMX的GPIO配置页面,把这些引脚一个个找出来,Maximum output speed改成Very High。这个操作很繁琐但很关键。我实测过,不改速度的情况下,短距离通信可能没问题,但线缆稍微长一点或者电磁环境复杂一点,丢包率就上去了。
4.2 RMII模式确认:别选成MII
CubeMX的ETH配置里,PHY Interface有两个选项:MII和RMII。默认可能是MII。如果你用的是RMII接线,这里必须改成RMII。改完之后,下面的引脚分配会自动变化,MII需要16根线,RMII只需要9根。
改完接口模式后,一定要检查引脚分配页面,确认所有RMII引脚都正确映射了。有时候CubeMX会因为引脚冲突自动把某个引脚分配到别的位置,比如把REF_CLK从PA1挪到PA8,这时候你要么改硬件接线,要么在CubeMX里手动锁定引脚。
4.3 自动协商与速度双工设置
CubeMX的ETH配置里有个“Auto Negotiation”选项,默认是Enable。这个选项让MAC和PHY自动协商速度和双工模式。对于LAN8720来说,自动协商是支持的,但有时候协商结果不对,比如协商成10M半双工,导致吞吐量上不去。
如果你确定要跑100M全双工,可以在CubeMX里把Auto Negotiation关掉,手动设置Speed为100M,Duplex Mode为Full Duplex。但这样做的风险是,如果对端设备不支持100M全双工,链路可能起不来。我的建议是先用自动协商,通了之后再根据实际需求调整。
4.4 中断和回调:CubeMX不会帮你写的部分
CubeMX生成的ETH初始化代码里,中断优先级和使能需要你自己配。在NVIC配置页面,把ETH全局中断使能,优先级根据你的系统需求设置。一般来说,以太网中断优先级不要设得太高,避免影响其他实时任务。
另外,CubeMX不会自动生成PHY的中断处理代码。LAN8720的中断引脚(nINT)可以接到STM32的某个GPIO上,用于检测链路状态变化。这部分需要你自己写GPIO外部中断的回调函数,在回调里读PHY的状态寄存器,判断链路是否up/down。
5. 调试实战:用示波器和寄存器读写定位问题
5.1 第一步:确认50MHz时钟有没有
不管用什么方案,第一步永远是拿示波器打REF_CLK引脚。探头接地要短,最好用弹簧地针,避免引入噪声。正常的50MHz时钟应该是干净的方波,峰峰值3.3V左右,上升时间在几纳秒以内。
如果REF_CLK没有波形,按以下顺序排查:
- 时钟源有没有输出?外部晶振方案,量晶振输出引脚;MCO方案,量PA8;PHY自产方案,量LAN8720的XTAL1引脚有没有25MHz。
- 时钟路径上的电阻、电容有没有焊错?有些板子在REF_CLK上串了33欧姆的匹配电阻,如果电阻没焊或者焊错阻值,信号可能到不了。
- LAN8720的供电和复位是否正常?量VDD引脚应该是3.3V,nRST引脚在复位后应该是高电平。
5.2 第二步:MDIO总线能不能读到PHY
MDIO是管理接口,用来读写PHY的寄存器。如果MDIO不通,后面的一切都免谈。用示波器打MDC和MDIO引脚,在ETH初始化的时候应该能看到MDC上有时钟脉冲,MDIO上有数据翻转。
如果MDC没有时钟,检查CubeMX里MDC的引脚分配和GPIO配置。MDC是推挽输出,速度建议设成Medium或者High。如果MDIO没有数据,检查MDIO的上拉电阻,RMII规范要求MDIO上拉1.5k到10k欧姆。
用前面提到的扫描代码,能读到PHYID就说明MDIO通了。读不到的话,先确认PHY地址,再确认MDIO引脚。
5.3 第三步:链路状态和自协商结果
MDIO通了之后,读LAN8720的BSR寄存器(地址0x01),bit 2是Link Status,bit 5是Auto-Negotiation Complete。如果Link Status是0,说明网线没插好或者对端设备没通电。如果Auto-Negotiation Complete是0,等一会儿再读,自协商需要时间。
读PHYSTS寄存器(地址0x10)可以看协商结果,bit 2-4是速度指示,bit 0是双工模式。LAN8720的寄存器定义在数据手册里有详细说明,我一般会把关键寄存器的值打印出来,对照手册看。
5.4 第四步:ping测试和丢包率统计
如果链路起来了,PHY寄存器也正常,接下来就是ping测试。在PC上ping STM32的IP地址,看能不能通。通了之后,用ping -t连续ping几百个包,看丢包率。正常的局域网环境丢包率应该是0。如果有丢包,重点查时钟质量和GPIO速度。
我遇到过一次丢包率5%左右的情况,查了半天发现是MCO输出的50MHz时钟抖动太大。换成外部有源晶振后丢包率降到0。所以时钟质量对通信稳定性的影响是决定性的。
5.5 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| REF_CLK无波形 | 时钟源未配置、晶振未起振、路径断 | 示波器逐级测量 |
| MDIO读不到PHY | PHY地址错、MDIO上拉缺失、引脚配置错 | 扫描地址、量上拉电阻 |
| 链路起不来 | 网线问题、对端设备问题、自协商失败 | 换网线、读BSR寄存器 |
| ping通但丢包 | 时钟抖动、GPIO速度低、电磁干扰 | 换时钟方案、改GPIO速度 |
| 初始化卡死 | 中断优先级冲突、HAL库版本问题 | 检查NVIC、更新CubeMX固件包 |
6. 几个CubeMX版本差异和固件包更新的坑
6.1 CubeMX版本不同,ETH配置界面不一样
我用过CubeMX 5.x、6.0、6.5、6.10几个版本,ETH的配置界面改动挺大的。早期版本里RMII Clock Source的选项藏得很深,在Clock Configuration页面里要展开ETH的时钟树才能看到。新版本里直接在ETH的Parameter Settings里就有下拉框。
如果你照着网上的教程配,发现界面跟教程里不一样,先确认CubeMX版本。我建议用6.5以上的版本,对STM32F4和H7的ETH支持比较完善。
6.2 固件包版本对ETH驱动的影响
CubeMX生成代码时会用到一个STM32Cube MCU Package,里面包含HAL库和ETH驱动。不同版本的固件包里,HAL_ETH_Init的实现有差异。比如某些版本的HAL库在ETH初始化时对PHY地址的处理有bug,会导致初始化失败。
我遇到过F4系列固件包1.27.0版本里ETH驱动的一个问题:HAL_ETH_Init里读PHYID的循环没有正确处理超时,PHY没响应时会卡死。后来更新到1.28.0就修复了。所以如果遇到初始化卡死,先检查固件包版本,去ST官网看有没有更新。
6.3 生成代码后的手动修改清单
CubeMX生成的代码不是万能的,以下几处通常需要手动改:
- PHY地址宏定义,改成实际值。
- GPIO速度等级,改成Very High。
- 如果用了MCO输出时钟,确认Clock Configuration里MCO1使能且频率正确。
- 在ethernetif.c里,low_level_init函数中确认ETH初始化参数与硬件一致。
- 如果需要静态IP,在lwip.c或者ethernetif.c里改IP地址、网关、掩码。
这些改动每次重新生成代码都会被覆盖,所以建议把改动集中在一个单独的文件里,或者用CubeMX的User Code区域保护起来。
7. 个人经验:调LAN8720最省时间的路径
调了这么多块板子,我总结出一条最省时间的路径:先确保硬件没问题,再用CubeMX最小配置生成代码,然后按“时钟->MDIO->链路->ping”的顺序逐级验证。
硬件上,我最关注三件事:50MHz时钟的质量、PHYAD0的电平、MDIO的上拉电阻。这三样确认了,软件上基本不会有大问题。CubeMX配置里,我最关注RMII模式、PHY地址、GPIO速度这三个选项。其他的用默认值一般都能跑。
还有一个小技巧:在main函数里ETH初始化之前,加一段延时,等PHY的电源稳定和内部复位完成。LAN8720的上电复位时间大概需要几十毫秒,有些板子的电源上升慢,不加延时可能初始化失败。我一般加HAL_Delay(100)再初始化ETH,从来没出过问题。
最后,如果你用的是现成的LAN8720模块,注意模块上的晶振和跳线配置。不同厂家的模块默认配置可能不一样,有的默认外部50MHz输入,有的默认自产时钟。买回来先用示波器和万用表确认一下,比看卖家给的资料靠谱。