☰
STM32H7+LAN8720A以太网调试实战:从PHY寄存器到LWIP稳定百兆
2026/10/4 15:13:00 网站建设 项目流程

从PHY寄存器读回来全是0xFFFF,到局域网里能稳定跑到接近100M带宽,这块STM32H7加LAN8720A的网口前前后后折腾了我挺长时间。作为这个系列文章的最后一篇,我把ETH外设和LWIP协议栈配置过程中踩过的坑、排查思路、最终怎么解决的,按实际操作顺序完整记录下来,如果你正在调H7系列配LAN8720A,这篇应该能帮你少走不少弯路。

先说清楚这篇文章适合谁看:用STM32H743/H750这类H7芯片做以太网通信,PHY选的是LAN8720A,在STM32CubeMX里配ETH和LWIP,然后发现网口要么ping不通、要么通了但一跑大流量就崩。硬件和软件层面的问题我都会覆盖,但不会讲太多协议栈内部实现的理论,重点是把能复现的步骤和能直接抄的配置给出来。

1. 硬件先排雷:RMII引脚、时钟与PHY地址的三方博弈

1.1 原理图上必须查三处的连接关系

很多ETH和LWIP的问题表面看是软件配置不对,实际上根子在硬件上。先看原理图,LAN8720A只支持RMII模式,不支持MII,所以STM32H7的ETH外设必须工作在RMII模式。RMII比MII少了一半信号线,数据引脚只要RXD0/RXD1、TXD0/TXD1、CRS_DV、TX_EN,再加上一个50MHz的REF_CLK,以及MDC/MDIO两根管理引脚,总共九根线。

我调试时第一个发现就是RXD1和CRS_DV这两根线特别容易被画反。因为很多参考原理图上它们的排列顺序不一样,如果画板时照着某个开发板抄,但抄的是不同厂商的网络标号,很容易出问题。RMII模式下RXD1和CRS_DV是两个独立的信号,接反了PHY也能识别出物理链路,但收包的数据完全不对,表现为能ping通但丢包极其严重,或者干脆只能发不能收。

MDC和MDIO这两根管理总线上一般要各接一个4.7k到10k的上拉电阻到3.3V。这两个引脚是开漏/开集输出,没有上拉的话MDIO通信会不稳定,现象是偶尔能读到PHY寄存器、偶尔读到0xFFFF,非常折磨人。

还有LAN8720A的REGOFF引脚,这个脚决定PHY内部1.2V稳压器是否启用。REGOFF接低电平时用内部稳压器,VDDCR引脚需要配一个10uF加0.1uF的退耦电容;REGOFF接高电平则需要外部单独供1.2V。很多低成本的模块直接下拉REGOFF,但电容没接够,导致PHY上电后工作不稳定,link状态时有时无。

1.2 LAN8720A的时钟架构:为什么50MHz这么纠结

RMII协议规定REF_CLK必须是50MHz,这一点和MII的25MHz/2.5MHz不一样。LAN8720A拿到这50MHz参考时钟无非三条路。

第一条路,也是我在板子上用的方案:LAN8720A的XTAL1和XTAL2接一颗25MHz无源晶振,芯片内部通过PLL倍频到50MHz,从CLK_OUT引脚输出,接到STM32H7的ETH_RMII_REF_CLK(通常对应PA1)。这个方案的好处是STM32这边只做输入,不参与时钟生成,MCU只需要保证HSE正常启动就行。

第二条路,用一颗50MHz有源晶振,输出直接同时接到LAN8720A的REF_CLK引脚和STM32H7的ETH_RMII_REF_CLK引脚。这个方案最省事,但对有源晶振的精度和驱动能力有点要求,而且多一颗有源晶振成本更高。

第三条路,用STM32的MCO引脚输出50MHz给LAN8720A。这个我试过一次,能跑通但容易引入额外干扰,而且MCO和ETH引脚的复用关系在不同封装上有差异,不推荐新手用。

重点是第一颗25MHz无源晶振的负载电容。很多人随便焊两个20pF上去,结果晶振不起振。我遇到过一次比较隐蔽的情况:示波器测LAN8720A的CLK_OUT引脚,有50MHz波形,但幅值只有一两百毫伏,PHY内部逻辑根本没工作。最后是换了晶振并联的电阻位置才解决。所以碰到PHY完全没反应,先不要怀疑STM32配置,用示波器看CLK_OUT有没有一个干净的50MHz方波出来。

1.3 PHY地址不是随便拉的

LAN8720A的PHY地址由PHYAD0和PHYAD1两个引脚决定。默认情况下PHYAD0下拉到地,PHY地址就是0x00。如果你的板子上PHYAD0接上拉,那地址就变成1,PHYAD1也接的话又不一样。

这个地址必须和STM32CubeMX里ETH配置的PHY Address保持一致。我见过最典型的错误是:参考一个开发板的原理图,开发板上PHYAD0接了上拉,ETH配置里地址也写了1,但自己画板时把这个引脚改成下拉,CubeMX里还留着1,结果MDIO永远找不到PHY。

调试初期建议直接把PHY地址固定下来,不要指望代码里动态扫描多个地址。LAN8720A的PHY ID1寄存器(REG 2)读出来通常是0x0007,PHY ID2(REG 3)低四位可能有差异。如果MDIO能读到这两个值,说明物理层已经通了。

2. CubeMX配置ETH的正确姿势与翻车现场

2.1 引脚复用:RMII模式下没有太多自由

打开STM32CubeMX,选好芯片型号,左侧Categories找到Connectivity里的ETH,Mode选RMII,这时CubeMX会自动分配引脚。以常见的STM32H743来说,RMII信号基本是固定的:

  • ETH_RMII_REF_CLK:PA1
  • ETH_RMII_CRS_DV:PA7
  • ETH_RMII_RXD0:PC4
  • ETH_RMII_RXD1:PC5
  • ETH_RMII_TX_EN:PB11
  • ETH_RMII_TXD0:PB12
  • ETH_RMII_TXD1:PB13
  • ETH_MDC:PC1
  • ETH_MDIO:PA2

原理图设计前就要对着这个引脚表查一遍,尤其是PA1这个REF_CLK脚。很多人在CubeMX里发现PA1既能复用成ETH_RMII_REF_CLK,也能复用成其他功能,于是怀疑是不是要选MCO输出,实际上如果你的时钟方案是LAN8720A的CLK_OUT输出50MHz给MCU,PA1就是纯粹的输入引脚,不需要配置MCO。

CubeMX里还有个容易忽略的点,ETH外设的GPIO Alternate Function要选对。如果PA7、PC4这些引脚旁的AF设置不对,编译能过,但实际信号根本到不了ETH外设内部。检查方式是CubeMX的Pinout视图里,选中引脚后看Alternate Function是不是ETH_RMII对应的AF值,不要手动去改,除非你对这个芯片的AF映射非常熟。

2.2 ETH参数设置:PHY Address只是开始

在ETH的Parameter Settings里,除了PHY Address,CubeMX还会要求填PHY的BSR寄存器和BCR寄存器地址,以及复位延迟时间。这个是新版本CubeMX增加的内容,因为HAL库的HAL_ETH_ConfigPHY需要知道这些信息来操作PHY。

LAN8720A的BCR寄存器是0x00,BSR寄存器是0x01,PHY Address如果硬件是默认下拉就是0。复位延迟毫秒数一般填100到500都行,太短的话PHY还没准备好,HAL_ETH_Init直接报超时。这里有个坑:如果你用的是旧版本CubeMX生成的代码,里面可能没有这些字段,直接裸用新版本HAL库编译会报错,所以工程模板跟着CubeMX版本走,不要混着升级。

还有一个参数是MAC地址,CubeMX默认生成一个00:80:E1:00:00:00之类的地址。如果你同时调多块板子,这个地址必须改,不然ARP缓存会冲突。建议用一个固定的自定义地址,比如自己公司分配的OUI段,或者至少保证同一局域网内不重复。

2.3 生成代码后的HAL_ETH_Init配置

CubeMX生成的MX_ETH_Init函数里,核心是这句:

heth.Init.MediaInterface = HAL_ETH_RMII_MODE; heth.Init.PhyAddress = 0;

如果PHY地址和硬件不一致,这里就要改。改完还要注意HAL_ETH_Init调用的时机,必须在HAL_ETH_Start或HAL_ETH_Start_IT之前。很多人把ETH初始化放在main函数里靠前的位置,但RTOS还没启动,一旦初始化卡住整个系统就起不来。

我见过一个问题:CubeMX生成的MX_ETH_Init里HAL_ETH_Init返回HAL_TIMEOUT,原因不是PHY坏了,而是此时MDIO所依赖的时钟还没使能完全。CubeMX会自动处理时钟使能,但如果你在配置时钟树时把ETH的时钟源给关掉了,HAL_ETH_Init就会一直超时。检查时钟树时,确认ETH外设的时钟被勾选,不要只看原理图上有信号。

3. LWIP参数调整与FreeRTOS集成中的暗坑

3.1 内存池参数:小马拉大车的下场

在CubeMX里启用LWIP后,第一件事是确认General配置里NO_SYS选的是OS而不是None。如果你要在FreeRTOS环境里跑LWIP,NO_SYS必须为OS,这样才能生成tcpip_thread和对应的信号量机制。

然后是Key Options里的几个内存参数,这里最容易出问题。CubeMX默认生成的MEM_SIZE是1600,PBUF_POOL_SIZE是16,在一来一回的简单通信场景够用,但一旦TCP收发大文件或者同时建多个连接,内存池很快耗尽。PBUF不足时现象很典型:板子刚上电能ping通,传输几KB数据后PC端就显示超时,板子再也收不到包。

我这边实际跑的项目里,最终把PBUF_POOL_SIZE调到24,MEMP_NUM_PBUF调到32,MEMP_NUM_TCP_SEG也调到24,MEM_SIZE改成6400。注意这几个参数不是越大越好,因为STM32H7的RAM虽然有几百KB,但DMA描述符、各种任务栈、协议栈静态内存都从里面分配,胃口太大反而挤占了其他功能。

如果是从CubeMX生成的lwipopts.h里手动改,改完必须重新编译整个工程,LWIP的宏定义不是运行时动态调整的。如果改用ST的扩展包X-CUBE-AZURE或者移植了其他版本的LWIP,这些参数的命名可能略有差异,本质含义一样,把内存段扩大到Ping不掉包为止。

3.2 LWIP与FreeRTOS:任务优先级和信号量

CubeMX生成的LWIP线程默认优先级是osPriorityNormal,堆栈大小是1024字。这个数值在H7上偏小,特别是你还在LWIP线程里做了域名解析或者MQTT这类操作时,很容易触发栈溢出。我建议直接改成2048,H7的任务栈按字算,2048字即8KB,对几百KB RAM的H7来说完全承受得起。

优先级方面,如果LWIP线程优先级比DMA中断的优先级低太多,中断里释放信号量后,LWIP线程迟迟得不到调度,就会出现网口明明有收包,但ping的响应时间忽高忽低,甚至有些ARP请求回不过去。我的做法是把LWIP线程优先级保持在Normal,但ETH的中断优先级不要设成最高,因为H7的中断里如果做太多操作,会影响整个系统的实时性。LWIP中断里只做信号量释放,实际协议栈处理都在tcpip_thread里,这一点CubeMX生成的ethernetif.c已经处理好了,不要去改它的回调函数。

另外必须确认HAL的时间基准和FreeRTOS的时间基准不冲突。CubeMX里默认HAL timebase source是SysTick,而FreeRTOS也要用SysTick,如果不改,程序跑起来之后HAL_Delay直接卡死。在Project Manager里把HAL timebase改成TIM6或TIM7,这个坑不解决,后面所有的调试全是空中楼阁。

3.3 DHCP还是静态IP

调试阶段我强烈建议先用静态IP,把板子固定成192.168.1.100,子网掩码255.255.255.0,网关192.168.1.1,PC端网卡设置成192.168.1.50。这样可以把变量降到最低,因为你一旦开着DHCP,路由器地址池满、DHCP报文被防火墙拦、板子发送DHCP Discover但是没人应答,这些都可能是ping不通的原因,但都不是你ETH和LWIP配置本身的问题。

等静态IP完全通了,再回到CubeMX里把LWIP_DHCP打开。打开DHCP后,板子获取IP需要几秒到几十秒不等,不要一上电就ping,先等一下再看。实际观察发现LAN8720A的link建立速度很快,但DHCP整个过程受路由器响应影响很大,我甚至见过某个路由器要等一分多钟才给地址,这种情况下直接怀疑DHCP配置有问题就冤枉了。

4. ping不通的完整排查链路,以及最终根因

4.1 第一步:PHY寄存器能不能正常读

任何网口调试,第一步都是确认MDIO链路正常。MCU和PHY之间的管理通道都没通,后面全是空中楼阁。CubeMX生成的工程里,可以直接用HAL库函数读PHY寄存器:

uint32_t phy_val = 0; HAL_StatusTypeDef status; status = HAL_ETH_ReadPHYRegister(&heth, 2, &phy_val); if (status == HAL_OK) { printf("PHY ID1: 0x%04X\r\n", phy_val); } else { printf("Read PHY register failed, status=%d\r\n", status); }

LAN8720A的寄存器2是PHY ID1,正常应该读到0x0007,寄存器3是PHY ID2,一般是0xA0xx或者0x0202之类的值。如果读出来是0xFFFF或者0x0000,先不要怀疑PHY芯片坏了,按这个顺序查:PHY Address对不对、MDC/MDIO上拉电阻有没有、LAN8720A复位脚是否在正常电平、25MHz晶振有没有起振、CLK_OUT有没有50MHz输出。

4.2 第二步:检查Link状态与自动协商

MDIO通了之后再查物理链路状态。LAN8720A的寄存器1是BSR寄存器,bit2是Link Status,bit5是Auto-Negotiation Complete。我通常这样判断:

HAL_ETH_ReadPHYRegister(&heth, 1, &phy_val); printf("BSR: 0x%04X\r\n", phy_val);

如果Link Status为0,说明PHY没有和交换机/路由器协商上,问题大概率在网线、变压器、RJ45座或者LAN8720A电源这一侧。你可以观察开发板上的网口指示灯,LAN8720A一般有两个LED引脚,一个指示Link,一个指示Active,如果Link灯都不亮,软件再怎么调也没用。

这一步还有一个容易忽略的地方:有些路由器端口是百兆自适应,但个别老交换机需要强制双工模式。如果LAN8720A的自动协商一直失败,可以用寄存器0(BCR)强制设置,但正常情况不要动,自动协商处理不了的情况极少。我这边基本没遇到需要强制设置双工的场景,更多是网线本身问题,换根线就好了。

4.3 第三步:ARP抓包,确认MAC层收发

PHY和Link都没问题,但ping还是不通,这时候打开PC端的Wireshark,跑一下ping,看PC有没有发出ARP请求,以及板子有没有回ARP应答。

  • PC发了ARP请求,板子没回ARP:问题在板子的接收或发送路径,重点查LWIP线程是否正常运行,DMA描述符是否有数据进来。
  • PC没发ARP请求:可能PC端网卡配置不对,IP不在同一网段,或者PC防火墙屏蔽了ICMP。
  • PC发了ARP,板子回了ARP,但ping timeout:说明IP层通了,但ICMP回包有问题,查LWIP的ICMP和内存配置。

我调这块板子时,Wireshark里能抓到板子发出的ARP,但PC的ARP应答板子收不到。网卡抓包能看到交换机一直在发广播,但板子就是没有任何回应。这就把问题缩小到了板子的接收路径。

4.4 第四步:DMA描述符与Cache一致性,这才是H7特有的坑

拿到STM32H7上,问题一下就明朗了。H7的CPU带D-Cache,ETH的DMA外设通过AXI总线访问RAM,普通SRAM区域默认是cacheable的。这就导致一个经典的数据一致性问题:DMA把收到的数据写进内存了,但CPU读的时候走的还是cache里的旧数据,看起来就是数据没收到;反过来CPU把数据写到cache里还没flush到内存,DMA就去读内存,发出去的全是垃圾数据。

现象就是ARP不发、ping完全不通或者通了但数据错乱,而且非常随机,重启后表现都不一致。CubeMX生成的工程里默认MPU配置可能没有把ETH相关的内存段设为non-cacheable,所以必须手动改MPU配置。

我这边是把LWIP内存池所在的那个RAM段直接配成non-cacheable。在CubeMX的MPU Configuration里可以配,也可以直接在代码里写:

MPU_Region_InitTypeDef MPU_InitStruct = {0}; MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x24000000; MPU_InitStruct.Size = MPU_REGION_SIZE_64KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_REGION_NOT_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_REGION_NOT_CACHEABLE; MPU_InitStruct.IsShareable = MPU_REGION_NOT_SHAREABLE; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct);

这里有几个要点:第一,BaseAddress要和你工程里LWIP使用的RAM段对应。CubeMX默认的H7内存布局里,AXI SRAM在0x24000000,DMA描述符和pbuf很可能就在这个区域。第二,配置MPU必须在使能Cache之前或者至少保证初始化顺序正确,CubeMX生成的main函数里通常是先执行MPU_Config再执行CPU_CACHE_Enable,不要颠倒。第三,如果不想整段non-cacheable,也可以保留cacheable但在DMA收发前后手动调用SCB_CleanDCache和SCB_InvalidateDCache,但如果每个包都手动刷Cache,代码会很啰嗦,我首选还是直接MPU配置成non-cacheable,一劳永逸。

4.5 最终根因复盘

最后定位到我这个板子ping不通的根因就是D-Cache一致性。在这之前PHY地址、时钟、Link状态全部正常,LWIP参数也调过好几轮,但板子就是收不到PC的ARP应答。因为H7的Cache问题不像线路问题那么明显,它不影响PHY寄存器读取,不影响发送,只影响接收方向上DMA写内存和CPU读内存的一致性。这也是为什么在F1/F4上跑同样的代码完全没问题,换到H7就各种诡异现象,这张板卡最终是通过MPU配置解决了。

如果你在排查时也走到了这一步,一定要确认MPU配置里有没有覆盖ETH实际用的内存区域。有时CubeMX生成的工程里MPU只配置了TCM RAM或者外部SDRAM,LWIP内存却在另一个段,等于没配。

5. 调试技巧、常见问题速查与量产经验

5.1 用串口把寄存器状态打出来

调试ETH和LWIP,串口是你最好的朋友。我习惯在初始化流程和关键环节打几行关键信息:

printf("[ETH] Start init...\r\n"); status = HAL_ETH_Init(&heth); printf("[ETH] HAL_ETH_Init status = %d\r\n", status); HAL_ETH_ReadPHYRegister(&heth, 2, &phy_vals); printf("[PHY] ID1 = 0x%04X\r\n", phy_vals); HAL_ETH_ReadPHYRegister(&heth, 1, &phy_vals); printf("[PHY] BSR = 0x%04X\r\n", phy_vals); printf("[LINK] %s\r\n", (phy_vals & 0x04) ? "UP" : "DOWN");

初始化成功之后,在LWIP线程启动并配置完网络接口时打印出当前IP:

printf("[LWIP] IP address: %s\r\n", ipaddr_ntoa(netif_ip_addr4(&gnetif)));

如果这里打印出来的IP是0.0.0.0,说明DHCP还没拿到地址,或者静态IP配置没生效。用串口打印可以在不借助任何调试器的情况下快速判断问题所在。

5.2 常见问题速查表

根据我这段时间的调试经验,把几种高频问题和对应排查方向整理如下:

现象可能原因排查顺序
PHY ID读成0xFFFFPHY地址不对、MDC/MDIO上拉缺失、PHY没复位、晶振没起振先量CLK_OUT和复位脚电平,再查MDIO
Link灯不亮网线、RJ45座、变压器、PHY电源、晶振换网线、量VDDCR、看CLK_OUT
Link灯亮但ping不通D-Cache一致性、LWIP内存不足、PC防火墙、IP网段抓包看ARP,查MPU配置,改静态IP
能ping通但大流量断流PBUF池不足、DMA描述符不足、任务栈溢出调大PBUF_POOL_SIZE和MEMP_NUM_PBUF
DHCP获取不到IP路由器DHCP池满、LWIP_DHCP未使能、获取时间不够先用手动IP测试,再开DHCP等30秒
系统启动卡死HAL timebase和FreeRTOS都用SysTick把HAL timebase改成TIM6/TIM7
频繁系统崩溃LWIP任务栈不足、中断里做了耗时操作栈改2048字,中断只释放信号量

这个表是我遇到问题的真实汇总,每一条几乎都在不同项目里出现过,不是理论推导。

5.3 量产部署时的一些经验

ETH和LWIP配置调通只是第一步,真正量产时还要注意几件事。

MAC地址必须在生产阶段写入唯一值,不能所有设备用同一个MAC,否则同一局域网内设备多了会互相干扰,表现为随机掉线、ARP表疯狂跳动。可以把MAC地址存到EEPROM或者Flash的独立扇区,出厂时烧录,代码里读出来再填进ETH配置结构体。

PHY的复位时序也要重视。如果MCU和PHY共用一个电源,上电时MCU的GPIO可能还没配置成输出模式,PHY复位脚处于不确定状态。我习惯用一个专门的GPIO控制PHY复位,在初始化ETH之前先拉低复位至少10ms,再拉高,然后等待PHY稳定。这个过程不要在RTOS启动之前做太久,否则影响启动时间。

还有一个容易被忽略的点:PHY的LED灯和MCU的引脚如果有复用冲突,CubeMX会报错,但如果你用的是其他功能复用,比如把LAN8720A的nINT引脚接到PC13上,而LED也接到了同一个引脚,PC13在H7上默认好像还有别的功能,这样调试时可能互相干扰。量产设计时把PHY的中断、LED等辅助信号引到专属引脚,不要和用户交互用的IO混用。

再说一句关于网线的事。我调试过程中遇到过一对明明是好的网线,插上去Link灯却不亮的情况,最后发现是线序不对,百兆只用了四根线,但工程现场经常有劣质网线只有两对线导通。量产测试时建议用至少超五类标准的成品网线,同时做线序和连通性测试,避免把网络问题误判成硬件故障。

最后分享一个我个人比较喜欢的验证方法:调通之后,不要急着跑复杂应用,先让板子持续ping PC端半小时,同时用Iperf或者类似工具打一下网络吞吐。如果带宽能稳定在90Mbps以上且没有明显丢包,说明ETH和LWIP这套配置基本是健康的。之后你再去跑TCP长连接、MQTT这些上层协议,心里就有底了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询