很多人第一次在STM32F407上外接LAN8720,都会在同一个地方栽跟头:板子画好了,工程建好了,程序烧进去了,网线也插上了,电脑右下角就是倔强地显示“未识别的网络”,或是干脆“网络电缆被拔出”。如果运气好一点,能在路由器后台看到一个设备反复上下线,但就是死活ping不通。
我当年调试这个组合时,前后折腾了整整两个晚上,最后发现问题不在软件,而在RMII的参考时钟配置。后来帮同事排查过几次同样的问题,发现大家踩的坑高度重合。这篇文章就把我自己的调试过程和踩坑记录整理出来,从CubeMX图形化配置到底层HAL库代码修改,再到示波器实测波形、寄存器回环测试,一步步说清楚。不管是刚接触STM32以太网的新手,还是被这个组合折磨过但没找到方向的工程师,照着这篇文章顺一遍,基本能解决九成以上的“网口不通”问题。
1. 项目背景:F407+LAN8720这个组合,难点到底在哪
1.1 为什么都选LAN8720,而不是PHY芯片
STM32F407自带以太网MAC控制器,这个MAC可以工作在MII模式,也可以工作在RMII模式,但它本身不是一个完整的以太网物理层芯片,必须外接一颗PHY芯片才能把数字信号变成网线上跑的差分模拟信号。PHY芯片的选择很多,常见的有LAN8720A、DP83848、KSZ8081这些,而LAN8720A几乎是做小体积嵌入式设备时最常见的搭配。
LAN8720A是Microchip(原SMSC)推出的一款低功耗10/100M以太网物理层芯片,封装小、外围器件少、价格便宜,而且支持RMII模式,用到的MAC引脚数量比MII少很多。MII模式需要16根数据和控制线,RMII模式只需要7根,这对PCB布局来说简直是解脱。同时RMII的工作频率从MII的25MHz降到了50MHz,虽然频率高了,但引脚少、布线简单,整体设计起来反而更容易。
但凡事都有两面。RMII模式节省引脚的关键,在于它把TX和RX数据线从8位收窄到2位,通过提高时钟频率来补偿带宽。这个设计带来一个硬性要求:MAC和PHY必须共享同一个50MHz参考时钟,而且这个时钟的质量直接决定链路能不能建立。很多网口不通的问题,追根溯源都出在这颗50MHz时钟上。
1.2 网口不通的故障类型和排查思路
从我自己的经验来看,F407+LAN8720的“网口不通”通常分成三种情况:
第一,插上网线后电脑完全没有反应,设备管理器和路由器后台都看不到设备。这种情况多半是PHY芯片没有正常工作,优先检查电源、晶振、复位、PHY地址这几个硬件相关项。
第二,电脑能识别到链路,但反复“正在识别”,始终获取不到IP地址。这种情况硬件链路大概率是通的,问题出在软件初始化、MAC地址或者LWIP协议栈配置上。
第三,能获取到IP地址、能ping通局域网,但偶尔掉线、大量丢包。这种情况多半是时钟质量不好、PCB布线干扰、或者中断处理不及时导致的。
下面几个章节,我就按照“硬件检查 → CubeMX配置 → 代码修改 → 实测调试”的顺序,把每一步做什么、为什么这么做、常见坑在哪,全部摊开来讲。
2. 动手之前,先把原理图和硬件检查一遍
2.1 LAN8720的PHY地址是怎么决定的
很多人在CubeMX里配置ETH外设时,都会看到一个“PHY Address”选项,默认值是0。如果这个值和实际硬件不匹配,MDIO总线读写PHY寄存器会全部超时,这时候不管软件怎么写都是白搭。
LAN8720A的PHY地址由芯片的RXER/PHYAD0引脚决定。这个引脚在RMII模式下不承担接收错误指示功能(RMII模式没有RX_ER引脚),而是被复用为PHY地址位。把它拉低,PHY地址就是0x00;把它拉高,PHY地址就是0x01。市面上大部分LAN8720模块和开发板,比如正点原子、野火的各种板子,几乎都把RXER引脚通过电阻拉低,所以PHY地址是0x00。
但这并不是绝对的,有个别模块会把RXER拉高,把地址设置成0x01。我建议拿到模块后先看原理图或者用万用表量一下RXER引脚的电平。如果模块没有原理图,最简单的方式就是写一段读PHY寄存器的代码,把地址0和1都试一遍,看哪个能读到非0xFFFF的芯片ID。LAN8720A的芯片ID在寄存器2和寄存器3里,正常读出来是0x0007C0F1。
2.2 50MHz REF_CLK到底由谁来提供
这是整个项目里最核心的配置点,没有之一。
RMII接口要求MAC和PHY都使用50MHz的参考时钟。问题在于,STM32F407内部并不产生这个50MHz信号。有的朋友可能会想,F407的MCO引脚能输出时钟,比如PA8可以输出PLL分频后的时钟,那是不是可以把PA8配置成50MHz输出,接到LAN8720的REF_CLK?理论上可以,但实际上非常难做,因为F407的MCO输出频率是由系统PLL分频得到的,而PLL是根据系统主频设计的。要让MCO精确输出50MHz,需要把PLL配置成某个能被50MHz整除的频率,这往往意味着要牺牲系统主频,或者使用非常规的时钟树方案,并不推荐。
最靠谱的做法,也是绝大多数官方评估板和第三方模块采用的做法:在LAN8720的XI和XO引脚之间接一颗25MHz的无源晶振,LAN8720内部通过PLL把25MHz倍频到50MHz,然后从CLKOUT引脚输出50MHz信号,接到STM32F407的PA1(ETH_RMII_REF_CLK)。这样MAC和PHY共用同一个时钟源,相位一致,时序完全匹配。
实际操作中要注意两个细节:第一,LAN8720的CLKOUT引脚默认是输出50MHz的,但也有的模块把这个引脚引出来接了一个LED指示或者悬空。拿到模块后要确认一下原理图,确保CLKOUT确实接到了F407的PA1。第二,25MHz晶振的两个负载电容一般取18pF到22pF之间,具体值要看晶振的规格书。如果电容配得明显不对,晶振可能起振困难,或者输出波形幅度不够,这会导致RMII时钟不稳定,网口能识别但丢包严重。
2.3 除了时钟,硬件还有哪些隐蔽的坑
硬件方面我踩过比较典型的坑有三个。
第一个是复位引脚。LAN8720的NRST是低电平复位,有的模块把这根引脚直接接了一个上拉电阻,靠内部的电源上电复位电路自动复位,有的模块则把NRST引出来让MCU控制。如果让MCU控制,上电顺序务必注意:MCU初始化完成后,要把NRST拉低至少1毫秒,再拉高,然后延时200毫秒左右,等PHY内部稳定后再去访问MDIO总线。如果刚上电就去读PHY寄存器,大概率读到的全是0xFFFF。
第二个是网络变压器的中心抽头。LAN8720的发送和接收差分对需要通过网络变压器连接到RJ45。变压器中心抽头的接法直接决定信号质量。很多小模块把中心抽头直接接地,这也没问题,但如果设计自己的底板,要按照PHY芯片和变压器厂家手册的要求来接,有的需要接电源,有的需要接地,接反了会出现信号幅度不足的现象。
第三个是MDIO的上拉电阻。MDIO是双向开漏信号,必须在外部接一个上拉电阻,阻值一般取2.2kΩ到10kΩ。有些模块内部已经自带上拉了,有些则需要自己加。如果MDIO线上没有上拉,PHY寄存器读取会非常不稳定,时好时坏,表现为:偶尔能ping通,复位之后就又不行了。
3. CubeMX配置:图形化界面里的关键设置
3.1 时钟树配置:别让网络时钟偷工减料
打开STM32CubeMX,新建F407系列芯片的工程,首先进入“Clock Configuration”页面。F407系统主频最高168MHz。我一般用外部高速晶振HSE,设为25MHz(正点原子探索者板上的晶振就是25MHz,如果是别的板子请按实际晶振频率修改)。
配置好系统时钟后,我要特意强调一句:网络外设的时钟并不直接等于系统时钟。在STM32F4系列里,以太网MAC的时钟来自AHB1总线,而RMII的REF_CLK是外部输入的。所以时钟树页面其实不需要专门为ETH添加什么特殊配置,系统时钟正常即可。
关键点在于:如果你用的不是LAN8720自带的25MHz晶振和CLKOUT方案,而是想用MCO输出50MHz时钟,那么就必须在时钟树里仔细调整PLL参数。如果你跟我一样使用LAN8720的CLKOUT输出50MHz给F407,那么时钟树这部分就只需要保证系统时钟正常,没有额外负担。
3.2 使能ETH外设并配置RMII引脚
在“Pinout & Configuration”页面左侧找到“Connectivity” → “ETH”,勾选激活以太网外设。在右侧的“Mode”中选择“RMII”模式。这时候你会看到芯片封装图上自动分配好了一组引脚:PA1(REF_CLK)、PA2(MDC)、PA7(CRS_DV)、PB11(TX_EN)、PB12(TXD0)、PB13(TXD1)、PC1(MDIO)、PC4(RXD0)、PC5(RXD1)。
如果发现某些引脚被其他外设占用,比如PC1被用作了ADC通道,那就需要手动解除冲突。RMII这组引脚基本是固定的,F407的以太网外设只有这一组复用功能可选,不能更改。这是硬件设计阶段就要决定的,如果PCB已经画好但引脚对不上,那就只能重新画板了。
我建议在CubeMX里把这些引脚名字在芯片视图上确认一遍,然后看一下是否有引脚没有自动分配。有时候因为芯片封装图显示问题,个别引脚需要手动选中然后选择复用功能“ETH_RMII...”。但正常情况下,勾选ETH的RMII模式后,CubeMX会自动完成引脚分配,不用手动干预。
3.3 参数配置:PHY地址和MAC地址别填错
在ETH外设的参数设置里,有一个“Parameter Settings”选项卡,里面比较重要的是:
- PHY Address:填0。前提是硬件上RXER/PHYAD0引脚拉低,如果硬件拉高则填1。
- MAC Address:这里默认生成一个基于芯片唯一ID的MAC地址,可以直接用。但要注意,MAC地址的第一字节最低两位有特殊含义:bit0是单播/组播标志,必须为0;bit1是全局/本地标志,建议为1(本地管理)。CubeMX生成的MAC地址通常已经处理好了,不用太担心。
在“Advanced”里,ETH的DMA参数一般保持默认即可。主要关注的是接收和发送描述符的数量,CubeMX默认各4个,如果之后要跑高流量应用,可以适当增大到8或16。
另外,如果你打算直接用LWIP协议栈,那么在左侧的“Middleware and Software Packs”中找到“LWIP”,勾选启用。这里有几个关键设置:IP地址默认是192.168.1.10,子网掩码255.255.255.0,网关192.168.1.1。如果想通过DHCP自动获取IP,把“IP Address”设置成“DHCP”即可。内存设置里的“MEM_SIZE”、“PBUF_SIZE”这些参数用默认值就行,大多数场景不用动。
3.4 中断配置:收包靠它了
ETH外设的全局中断“ETH”要勾选使能,注意它位于NVIC设置列表中,名字就叫“ETH”或者“ETH global interrupt”。如果不使能这个中断,LWIP收包只能靠轮询,CPU占用高且容易丢包。使能后,中断优先级建议设一个较低的数值,比如5,避免和SysTick、定时器等实时性要求高的中断冲突。
还有一个容易忽略的中断是“ETH_WKUP”即唤醒中断,这个在普通网络应用中用不到,不需要使能。
CubeMX还有一个贴心的功能:它可以自动生成LWIP的初始化代码和ethernetif.c底层接口文件。但别高兴太早,生成的代码通常在转发收包中断回调这里是空的,需要你在用户代码区自己补充,这个会在下一章详细说。
3.5 代码生成配置:选对工具链
在“Project Manager”里,根据你自己使用的工具链选择IDE,比如MDK-ARM、STM32CubeIDE或者Makefile。CubeMX会生成对应的工程文件。如果之后习惯用VSCode配合Makefile开发,也可以选择Makefile,CubeMX生成的Makefile结构清晰,稍作整理就能在VSCode中编译调试。不过作为新手,我还是建议先用STM32CubeIDE或者Keil把流程跑通,再折腾工具链的配置。
4. HAL库代码层:CubeMX生成的代码还缺什么
4.1 上电后的PHY复位延时
CubeMX生成的main.c中已经调用了MX_LWIP_Init()和MX_ETH_Init(),但这两个初始化是否一定成功,取决于PHY是否已经稳定工作。如果你用的是模块方案,PHY的复位引脚可能直接悬空或者接了上拉,靠内部上电复位,那么硬件上电到PHY稳定的时间通常比较长。代码里在MX_ETH_Init()和MX_LWIP_Init()之前最好加一段延时。
我习惯在main函数里,外设时钟使能完成后,立刻加一个500毫秒的延时,然后再调用MX_ETH_Init()和MX_LWIP_Init()。延时可以用HAL_Delay(500)。如果PHY的复位引脚是由GPIO控制的,那么在此之前还要先执行拉低、延时、拉高的操作,之后再做这个500毫秒延时。
4.2 验证PHY寄存器是否可读
CubeMX生成的ethernetif.c中,有一个low_level_init()函数,里面会调用HAL_ETH_ReadPHYRegister()读取PHY的ID,以验证MDIO通信是否正常。但生成的代码默认是针对STM32官方评估板上的PHY芯片(LAN8742A)的,它的PHY地址默认是0。
如果你用的是LAN8720且硬件上PHY地址是0,那这段代码大概率是能跑通的。但如果你的PHY地址是1,就必须改成1。否则PHY ID读出来全部是0xFFFF,ETH初始化会超时,LWIP无法启动。
有些新手不知道如何确认PHY是否被正确访问,我提供一个简单的方法:在main函数中加入一段手动读取PHY寄存器的代码。用HAL_ETH_ReadPHYRegister(&heth, 0, 0x02)和读寄存器0x03,打印出两个16位的值,如果拼起来是0x0007C0F1,说明MDIO通信正常。这段代码在调试期间很有用,确认无误后可以删掉。
4.3 收包中断回调的补充
这是“能发不能收”的经典原因。CubeMX生成的代码中,ETH的接收中断回调函数HAL_ETH_RxCpltCallback()默认是一个弱定义的空函数。很多人的程序里,网线插上后上电时请求DHCP会有反应,但之后就再也没有数据了,或者只能发给别人数据但收不到别人的数据,问题就在这个回调。
你需要在自己的用户代码中实现这个回调,并把收到的数据包交给LWIP协议栈处理。网络接口的netif结构体在ethernetif.c中已经定义好了,通过extern声明即可引用。典型写法是:
void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef *heth) { struct pbuf *p; // 通知LWIP底层有数据到达 while (HAL_ETH_GetReceivedFrame_IT(heth) == HAL_OK) { p = low_level_input(heth); if (p != NULL) { if (heth->Init.RxMode == ETH_RXINTERRUPT_MODE) { ethernetif_input(&gnetif, p); } else { pbuf_free(p); } } HAL_ETH_Start_IT(heth); } }这段代码在不同CubeMX版本里略有差异,但核心逻辑就是:把收到的帧从DMA描述符里取出,封装成LWIP的pbuf,然后交给ethernetif_input()处理,最后重新使能接收中断。
如果漏掉这个回调,即使底层接收到数据包,协议栈也不知道有数据进来,表现出来就是网络断断续续甚至完全不通。
4.4 发送超时的处理
另一个常见问题是发送流程卡死。HAL库的HAL_ETH_Transmit_IT()或HAL_ETH_Transmit()在某些异常情况下会一直返回HAL_BUSY,然后LWIP线程就卡住了。CubeMX生成的low_level_output()函数中已经有相应的错误处理,但如果你的代码是自己写的,一定要注意在发送失败时释放pbuf,并检查返回错误码,不能无限等待。
有一种情况让我印象很深:网线拔插几十次之后,传输突然中断,再也无法恢复。追查发现是发送描述符在异常情况下没有正确释放,导致DMA一直认为描述符被占用。最终的解决办法是在low_level_output()里增加超时判断,如果连续几次发送超时,就调用HAL_ETH_Stop()再HAL_ETH_Start()重新初始化DMA。虽然粗暴,但在实际产品中很有效。
5. 实测与调试:网口不通的排查手法
5.1 最简单的链路测试:看PHY中断和LED
在没有示波器和逻辑分析仪的情况下,也有一招能粗略判断PHY的状态。LAN8720模块上通常有两个LED指示灯,一个标记“Link/Act”,一个标记“Speed”。插上网线后,如果Link/Act灯亮了,说明PHY已经和交换机/电脑建立起了物理链路,这个阶段PHY的模拟前端、网络变压器、RJ45基本没问题。如果Link/Act灯不亮,重点检查PHY电源、时钟、变压器和RJ45。
需要注意的是,LAN8720默认输出的是10M速度指示,如果网络是100M,Speed灯应该亮。如果Speed灯不亮而Link灯亮,说明PHY可能工作在了错误的速率模式,或者线缆质量差导致协商失败。
5.2 用示波器实测50MHz时钟
调试以太网,示波器是必须的。把示波器探头接到LAN8720的CLKOUT引脚(在模块上通常标注REF_CLK或CLKOUT),或者接到F407的PA1引脚,应该能看到稳定的50MHz方波。这个信号的幅度应当在3.3V左右,上升沿应当干净,不能有过大的振铃。
如果看不到50MHz信号,问题多半出在晶振电路。用示波器看25MHz晶振的XI引脚,确认是否起振。如果晶振不起振,检查负载电容是否合适、晶振是否虚焊、芯片供电是否正常。如果晶振起振但CLKOUT没有输出,可能是芯片内部PLL没锁定,此时需要排查供电和复位。
还有一个值得注意的细节:REF_CLK信号的相位抖动会影响通信稳定性。如果你的示波器带有时钟抖动分析功能,可以看下峰峰值抖动。如果抖动过大,说明时钟源质量不好,很多时候是LAN8720的供电纹波太大,或者PCB布线时REF_CLK走线太长太细。解决方法是:LAN8720的供电引脚就近放一个0.1μF陶瓷电容加上一个10μF钽电容,并且REF_CLK走线越短越好,尽量在地平面完整的地方走。
5.3 MDIO寄存器回环测试:自证清白
当网口一直不通,而又无法确定是MAC问题还是PHY问题的时候,用PHY的回环模式可以快速分割故障范围。
通过MDIO总线,向PHY的寄存器0写入0x4000,使能数字回环。此时PHY会把发送的数据直接回环到接收路径,不经过网络变压器和外网。如果在回环模式下,LWIP能自己ping通自己,说明MAC→MDIO→PHY寄存器→PHY收发通道基本是完好的。再关掉回环,问题就锁定在外部链路(网络变压器、RJ45、网线、对端设备)。
这个操作在应用里不常用,但调试阶段价值极高。我可以提供一段参考代码:
uint16_t reg_val = 0; HAL_ETH_ReadPHYRegister(&heth, PHY_ADDR, 0, ®_val); reg_val |= 0x4000; // 置位回环 HAL_ETH_WritePHYRegister(&heth, PHY_ADDR, 0, reg_val);设置回环之后,如果LWIP的DHCP获取到了IP地址(通常是自己分配的链路本地地址169.254.x.x),或者能ping通自己设置的IP,那就说明整个DMA、描述符、MAC核心、MDIO、PHY数字部分都工作正常。
5.4 用Wireshark抓包定位协议栈问题
有时候物理链路没问题,PHY也正常,但始终获取不到IP地址,此时就需要抓包分析了。在PC上打开Wireshark,选择连接开发板的那个网卡接口,然后给开发板上电。
正常情况下,如果开启了DHCP,能看到开发板发出的DHCP Discover广播报文。如果看不到任何报文,说明LWIP协议栈没有工作,重点检查ethernetif_input是否被正确调用、收包中断是否触发。如果能看到Discover,但没有Offer响应,说明DHCP服务器没收到或者没回复,这时候排查交换机端口、网线,以及路由器的DHCP配置。
如果不想抓包这么麻烦,也可以在调试串口上打开LWIP自带的debug打印,把LWIP_DEBUG打开。CubeMX生成的LWIP默认关闭了debug输出,你可以在lwipopts.h里打开LWIP_DEBUG,并设置LWIP_DHCP为1。调试信息会直接打印到串口,虽然信息比较庞大,但对定位问题非常有帮助。
5.5 用静态IP绕开DHCP的排查捷径
如果你的网络环境没有DHCP服务器,或者路由器配置有问题,那DHCP获取不到IP并不代表网络不通。为了验证基本通信能力,我建议先把LWIP设置成静态IP,比如192.168.1.10,子网掩码255.255.255.0,然后把电脑的网卡手动设置为192.168.1.100,子网掩码255.255.255.0,用一根网线直连开发板和电脑。
如果能ping通192.168.1.10,那恭喜你,网络链路和协议栈都通了,剩下的问题就是网络环境和DHCP配置。如果静态IP都ping不通,再回头检查前面的硬件和初始化步骤。
5.6 中断风暴和CPU占用
还有一个比较隐蔽的问题:如果ETH中断处理函数写得不合理,比如在中断回调里做了大量耗时的操作,或者没有正确清中断标志,会导致中断持续触发,看起来像是系统卡死。
排查方法是:在中断回调函数入口设置一个GPIO翻转,用示波器看这个GPIO的翻转频率。如果翻转频率达到了MHz级别,说明中断风暴。此时需要优化回调逻辑,尽量缩短中断处理时间,把耗时的协议栈处理放到主循环或低优先级线程中。
6. 常见问题速查表与实操心得
整理一下我在项目实战中遇到频率最高的问题和对应的解决办法,做成一个速查表,方便大家在调试时快速定位。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 插网线完全无反应,Link灯不亮 | LAN8720供电异常、晶振不起振、RJ45虚焊 | 检查供电引脚电压,用示波器查看晶振波形,重新焊接RJ45 |
| Link灯亮,但ping不通 | RMII时钟未接或质量差、PHY地址不对、复位时序不对 | 示波器确认PA1处50MHz时钟,确认RXER引脚电平,检查复位延时 |
| 能获取IP,但ping丢包严重 | REF_CLK抖动过大、PCB布线干扰、网线质量差 | 检查电源纹波,缩短REF_CLK走线,更换网线测试 |
| 只能发不能收 | ETH中断未使能、HAL_ETH_RxCpltCallback未实现 | 使能ETH全局中断,在回调里调用ethernetif_input |
| MDIO读写全部超时 | PHY地址不对、MDIO无上拉、PHY没有正常上电 | 确认PHY地址,检查MDIO上拉电阻,检查NRST复位状态 |
| PHY ID读出来是0x0000 | PHY没有稳定复位,上电后访问太早 | 延长复位后的延时,至少等200ms以上再访问MDIO |
| ping通但DHCP失败 | DHCP服务器未开启、LWIP配置错误 | 先手动设置静态IP验证通路,再排查DHCP配置 |
根据我个人的使用经验,调试STM32F407+LAN8720这个组合,最忌讳的就是“一上来就写业务代码”。先把PHY读通、把链路调通,哪怕只是点个灯,也算是在正确的道路上迈出了一大步。CubeMX虽然能省去很多代码工作,但它毕竟只是工具,它生成的是“常规情况”的代码,不是“你的板子”的代码。理解RMII时钟来源、PHY地址、复位时序这几个底层问题,远远比会点鼠标配置界面重要得多。
最后再分享一个小技巧:在调试阶段,把LWIP的DHCP关掉,用固定IP直连电脑,能极大减少变量。等确认物理链路和基本IP通信没毛病之后,再开DHCP去接入真实网络。这是一个看起来很基础、但真的能让调试效率翻倍的经验。