☰
STM32+LAN8720A+LWIP裸机跑通PING:从CubeMX配置到避坑记录
2026/10/17 3:56:54 网站建设 项目流程

简介:嵌入式以太网通信是物联网设备的基础能力,STM32内置MAC配合外部PHY芯片即可实现网络接入。RMII接口作为MAC与PHY之间的标准总线,依赖精确的50MHz参考时钟,而PHY芯片的寄存器访问与地址配置则是驱动稳定工作的关键前提。在无操作系统环境下移植LWIP协议栈,需要合理规划内存池并采用主循环轮询机制,才能保证PING响应可靠、不丢包。该方案广泛应用于工业控制、智能网关等联网外设场景。本文基于STM32+LAN8720A+LWIP的裸机环境,详细梳理硬件引脚映射、CubeMX时钟与ETH配置、PHY驱动适配、lwipopts.h关键宏,并总结“PING不通”的典型踩坑现场,帮助开发者快速定位问题、顺利打通以太网通信链路。

1. 这套环境为什么总在“PING通”这一步卡住

带以太网的STM32项目,十块板子有八块最终要跑一个“PING通”才算起步。很多开发者拿到带网口的板子,第一反应是找个例程改改就上,结果HAL库默认以太网驱动是按DP83848这颗PHY芯片写的,而手头板子用的是LAN8720A,一改PHY地址就是一堆报错;就算地址改对了,无操作系统模式下LWIP的内存和tick配置又和带系统的写法人鬼殊途,最后卡在“link up了,PING却一直超时”这种玄学问题上。

这套方案——STM32 + LAN8720A + LWIP,在无RTOS的裸机环境里跑通PING,是很多联网外设项目的基础门槛。它比带系统方案少了一层调度,看着简单,实际上ETH外设的RMII时序、PHY驱动适配、LWIP内存池分配,每一处都能让新手折腾一整天。这篇文章按我实际装过的一套环境,把硬件引脚、CubeMX配置、驱动移植、LWIP适配和踩坑记录一次写全,适合正在调以太网、被PING不通折磨的开发者照着一步步走。

2. 硬件与前置准备:先把LAN8720A和STM32的关系理清

2.1 RMII接口和50MHz时钟来源

LAN8720A是一颗RMII接口的百兆PHY芯片,和STM32的MAC层之间走的是RMII总线,总共只需要几根信号线:TXD0、TXD1、RXD0、RXD1、CRS_DV、TX_EN、MDC、MDIO,外加一颗50MHz的参考时钟。这里有一个容易忽略的点:RMII的REF_CLK必须精确到50MHz,不能拿一个“差不多”的时钟糊弄,PHY芯片内部没有PLL去倍频,时钟不对,link状态、收发数据全是乱的。

这颗50MHz时钟的来源,常见有两种方式:一是板子上放一颗50MHz有源晶振直接给LAN8720A,同时把CLKOUT引脚接回STM32的ETH_RMII_REF_CLK;二是不放晶振,让STM32的MCO引脚输出50MHz给LAN8720A。第二种做法更省器件,也是很多开发板的标准接法,但要额外占用一个MCO引脚,时钟树里也要配置好。我用的是MCO输出方案,接下来第3章会具体写时钟树的配置。

LAN8720A的PHY地址由PHYAD0~PHYAD2引脚的电平决定。大多数模块上PHYAD0引脚被拉低,地址就是0x00;也有个别板子把PHYAD0拉高,地址变成0x01。这个地址必须在代码里和硬件严格对上一一不对,HAL库初始化时读不到PHY ID,返回超时,整个以太网直接启动失败。

2.2 引脚连接与原理图核对

不管用哪颗MCU,RMII的信号引脚基本是硬编码在芯片内部的,不能随意映射。以常见的F407/F429系列为例,RMII用的引脚是固定的:TXD0在PB12,TXD1在PB13,TX_EN在PB11,RXD0在PC4,RXD1在PC5,CRS_DV在PA7,MDC在PC1,MDIO在PA2,REF_CLK在PA1。做板子或者挑模块的时候,对着原理图逐根确认,比到了调代码阶段再拿万用表量省时间得多。

我习惯先把完整的引脚对应关系列成一张表,贴在工作台上方,后面配置CubeMX就对照着填。下面是这套方案里STM32和LAN8720A的信号映射,按典型板卡的接法:

功能STM32引脚LAN8720A引脚说明
ETH_RMII_REF_CLKPA1REF_CLK50MHz,来自MCO2
ETH_RMII_CRS_DVPA7CRS_DV载波侦听/数据有效
ETH_RMII_TXD0PB12TXD0发送数据bit0
ETH_RMII_TXD1PB13TXD1发送数据bit1
ETH_RMII_TX_ENPB11TX_EN发送使能
ETH_RMII_RXD0PC4RXD0接收数据bit0
ETH_RMII_RXD1PC5RXD1接收数据bit1
ETH_MDCPC1MDC管理时钟
ETH_MDIOPA2MDIO管理数据
MCO2PC9无输出50MHz给PHY

如果板卡的原理图和这张表不同,比如RXD接到了别的引脚,那多半是RMII复用的另一组映射,要么改硬件跳线,要么选一颗支持该映射的MCU,代码里强行改引脚是没有意义的,MAC外设的引脚复用逻辑不允许随意挪。

2.3 软件环境清单与版本选择

开发工具我建议直接用STM32CubeMX加对应系列的HAL库,版本选你现在能下载到的最新稳定版就行。这里提醒一句:CubeMX版本和HAL库版本最好配套,老工程升级后重新生成代码,stm32f4xx_hal_conf.h里新增的宏没同步,编译会报一堆匪夷所思的错误。

  • STM32CubeMX:任何近一两年的稳定版本均可,工程配置界面差异不大
  • STM32 HAL库:F4系列对应stm32f4xx_hal_driver,版本跟随CubeMX生成
  • 编译器:ARM GCC或Keil MDK均可,本篇代码不依赖编译环境
  • LWIP:CubeMX中间件里自带的版本,无操作系统模式下用lib/贡献的源码
  • 硬件:STM32F407或F429开发板 + LAN8720A模块(模块带50MHz晶振或引出了REF_CLK)

这里有个前提必须说明:本文方案基于带内置以太网MAC的STM32型号,比如F407、F429、F767等。如果你用的是F103这种没有MAC外设的芯片,需要外接SPI接口的W5500之类的方案,那就完全是另一套玩法了,不在本文讨论范围。

3. CubeMX配置:把外设和中间件一次性生成对

3.1 时钟树:MCO2输出50MHz给PHY

打开CubeMX,先选好芯片型号,然后进Clock Configuration页面。AHB1总线时钟按芯片最高频率配,F407配到168MHz,F429配到180MHz,这个频率直接影响CPU跑速和以太网DMA的性能,能拉多高就拉多高。关键是让MCO2输出准确的50MHz。

MCO2的时钟源可以选择PLLCLK,然后通过分频系数得到50MHz。配置公式是:PLLCLK频率除以分频数等于50M。以AHB1为168MHz为例,PLLCLK通常也是168MHz(或者你配置的PLL频率),分频4就是42MHz不对,分频需要算准——168MHz除以4等于42MHz,除以3等于56MHz,都凑不出精确50MHz;更可靠的做法是在CubeMX的时钟树里看到MCO2那一栏,把输出频率设成50MHz,软件会自动计算分频系数。实际操作中,我就是把时钟树右侧的时间线一路配置到50MHz,光标移到MCO2上确认它是50MHz而不是48MHz,差2MHz都会让PHY产生的link状态异常。

时钟树配好后,CubeMX会自动生成一段MCO配置代码:

HAL_RCC_MCOConfig(RCC_MCO2, RCC_MCO2SOURCE_PLLCLK, RCC_MCODIV_4);

这段代码出现在main函数里HAL_Init之后、以太网初始化之前。参数含义是:选择MCO2引脚作为时钟输出,时钟源为PLLCLK,分频系数为4。如果你的PLL配置不同,分频系数要跟着变,目标只有一个——MCO2引脚上必须是干净的50MHz方波。上电后拿示波器量PC9引脚,能看到50MHz波形,再往下走;看不到波形,后面所有调试都白搭。

3.2 ETH外设:RMII模式下的参数填写

在Pinout & Configuration页面搜索ETH,把它使能。接口模式选RMII,因为LAN8720A只支持RMII,选MII它也不理你。使能ETH后,CubeMX会自动把前面表格里的那一组引脚分配到对应功能上,不用手动去Pinout视图里逐个点,软件知道哪根引脚能复用成ETH_RMII。

ETH参数页里需要留意的几个字段:

参数推荐值说明
PHY Address0与硬件PHYAD引脚一致,读错直接Init失败
MAC Address随意,如02:00:00:00:00:01开发阶段随意,批量生产再统一分配
DMA Descriptor Count4或6收发描述符数量,裸机内存紧张可设4
RX/TX Buffer Size1536无操作系统模式建议保持默认
Media InterfaceRMII与PHY硬件一致

这里强烈建议先把PHY Address设成0跑通,再考虑改其他值。LAN8720A模块上PHYAD0基本是拉低的,地址0覆盖大多数情况;如果你的板子地址不是0,HAL_ETH_Init会直接返回HAL_TIMEOUT,那就要回头查硬件原理图,而不是在软件里乱猜。这一点我会在第5章单独列一条经验。

3.3 LWIP中间件:无操作系统模式的关键开关

在Middleware里勾选LWIP,然后进入它的参数页面。这里的选择决定后面代码长什么样——Operating System选“No Operating System”,对应NO_SYS等于1;不要选带RTOS的选项,选了之后生成的代码里会多出一堆信号量和队列,裸机环境根本跑不起来。

LWIP参数页里的内存配置,推荐按下面的值填,这套参数在F407上跑PING测试是够用的:

  • Memory Heap Size:默认值即可,裸机环境不建议开太大,ESP8266那种大堆在这里没必要
  • Number of Simultaneous TCP:4,够用就好
  • Number of Simultaneous UDP:4,UDP回环测试需要
  • IP Version:IPv4,百兆内网项目暂时不需要IPv6
  • Enable UDP:勾上,PING只用ICMP,但后续测试会用到UDP
  • Enable TCP:勾上,WebServer测试要用

生成代码后,CubeMX会生成一个lwipopts.h文件,里面定义了NO_SYS、MEM_SIZE这些宏。很多新手在这里翻车:CubeMX生成的lwipopts.h里默认NO_SYS是0,也就是带系统模式,你生成工程后不回看,直接编译跑,LWIP就会尝试调用系统相关的接口,裸机哪里来的系统调用?所以生成完代码第一件事,去lwipopts.h里确认NO_SYS已经是1。这个细节属于那种“错得莫名其妙、查半天发现是配置选项没勾”的典型坑。

3.4 生成代码后必须检查的三个点

CubeMX生成工程后,不要急着编译,先花两分钟自查三处。第一处,main.c里MCO初始化代码是否已经出现,确认它在ETH_Init之前;MCO输出如果配置在以太网之后才生效,PHY启动时拿不到时钟,后面link永远起不来。第二处,stm32f4xx_hal_conf.h里的HAL_ETH_MODULE_ENABLED是否被注释掉,有些版本生成的默认配置不启用ETH模块,宏没打开,整个以太网驱动编译出来是空的。第三处,ethernetif.c文件是否存在,这是CubeMX为你生成的LWIP底层驱动,无系统模式下它负责注册网络接口,缺失的话LWIP根本不知道硬件在哪。

这三处检查完再进IDE编译。我见过太多人代码还没看就按编译键,报错几百条,最后发现是HAL_ETH_MODULE_ENABLED没开,白白浪费一下午。配置层面的问题,编译前花两分钟就能避免,比编译后再排错划算得多。

4. 驱动移植与代码修改:从HAL库默认PHY到LAN8720A

4.1 PHY地址与HAL库默认值的不一致

CubeMX生成完工程,HAL库的以太网驱动里有一个默认的PHY地址定义,那是对着DP83848写的。stm32f4xx_hal_eth.c的开头或者对应的头文件里,会有一处类似这样的定义:

/* 修改前:HAL库默认的PHY地址,对应DP83848 */ #define PHY_ADDRESS 0x01U

LAN8720A的地址是0(大多数模块),这一行不改,初始化时HAL去访问地址1的PHY,拿回来的寄存器全是0xFF或0x00,芯片ID校验必然失败。改成:

/* 修改后:适配LAN8720A,地址0 */ #define PHY_ADDRESS 0x00U

同时,在ethernetif.c的底层驱动里,结构体heth的PhyAddress字段也直接接收了这个地址。改动就这么一行,但漏掉的后果是整个初始化流程直接中断。我在这个位置卡过一次,当时进度条卡在ETH初始化,调了三个小时才想起来硬件地址是0——这就是典型的“软件写了但和硬件对不上”。

4.2 修正PHY寄存器地址与增加软复位

HAL库驱动里有一段读PHY芯片ID的逻辑,用来确认PHY存在。它读的是DP83848的ID寄存器地址,LAN8720A的ID寄存器编号不同,需要同步修改。打开ethernetif.c或者lan8720相关的源文件,找到读ID的代码段,按下面对照修改:

/* HAL库默认读DP83848的ID寄存器 */ #define PHY_DS_REG 0x02U /* 原:DP83848的ID寄存器地址 */ #define PHY_DS_REG_2 0x03U /* 原:DP83848的第二ID寄存器地址 */ /* LAN8720A的ID寄存器是0x01和0x02 */ #define PHY_DS_REG 0x01U #define PHY_DS_REG_2 0x02U

改完之后,HAL_ETH_Init里读到的PHY ID才是0x0007,这是LAN8720A的厂商ID。如果这里不改,HAL会以为PHY不存在,初始化返回错误。注意一点:不同HAL库版本里这两个宏的名字可能不一样,有的叫PHY_DS_REG,有的放在结构体里直接读,找到读寄存器地址那两行改就行。

另外,LAN8720A上电后默认是处于复位状态的,但HAL库的PHY初始化流程里并没有软复位这一步。为了让PHY稳定工作,我习惯在初始化时主动发一次软复位,操作PHY的BMCR寄存器,把bit15写1,等待100毫秒再继续:

uint16_t tmp = 0; /* 读取BMCR寄存器(地址0) */ HAL_ETH_ReadPHYRegister(&heth, PHY_ADDRESS, 0x00U, &tmp); /* 置位SWR(soft reset)位 */ tmp |= 0x8000U; /* 写回 */ HAL_ETH_WritePHYRegister(&heth, PHY_ADDRESS, 0x00U, tmp); /* 等待PHY复位完成 */ HAL_Delay(100);

这段代码放在ETH初始化之后、LWIP接口注册之前。BMCR寄存器的bit15就是软复位位,置1后PHY内部所有寄存器恢复默认值,包括link状态。为什么HAL默认流程没有这一步?因为DP83848对软复位的依赖不如LAN8720A明显,但LAN8720A的官方驱动流程里软复位是标配。加上这一个操作,能够大幅减少“上电后link状态不对、要重新插拔网线才好”的概率。

4.3 LWIP无操作系统模式:lwipopts.h关键宏

无操作系统模式真正跑起来,lwipopts.h里这几个宏必须确认到位:

/* lwipopts.h 关键配置片段 */ #define NO_SYS 1 /* 无RTOS,裸机运行 */ #define MEM_SIZE 1600 /* 堆内存大小,够用即可 */ #define MEMP_NUM_PBUF 16 /* PBUF数量,以太网收发够用 */ #define PBUF_POOL_SIZE 16 /* PBUF池大小,接收数据包用 */ #define LWIP_DHCP 0 /* 固定IP,不启用DHCP */

NO_SYS必须为1,这是无操作系统方案的根基。MEM_SIZE控制LWIP内部堆的大小,开太大浪费RAM,开太小TCP传输时内存不足会丢包。LAN8720A百兆场景下1600字节够用,如果你的代码里还跑了HTTP Server之类的应用,再往上调到2400也不迟。PBUF_POOL_SIZE关系到接收缓冲区,设16个,每个默认1500字节左右,F407的RAM完全扛得住。

固定IP而不启用DHCP,是我在裸机场景下的习惯。裸机项目没有文件系统,DHCP获取的IP不好保存回查,而且路由器换了网段就要重新获取,调试期极不方便。把LWIP_DHCP设为0,在ethernetif.c里静态配置IP地址,比如192.168.1.10,子网掩码255.255.255.0,网关192.168.1.1,PC网线直连开发板,手动给网卡配同网段IP,PING测试最省心。

4.4 主循环轮询与PHY link状态检测

无操作系统模式下,LWIP的所有协议处理都靠主循环驱动。CubeMX生成的工程里,main函数的while(1)里已经帮你放好了ethernetif_input的调用,但生产级代码还要补上超时处理和link状态检测。

/* main.c 主循环 */ uint8_t linkup = 0; uint16_t regval = 0; while (1) { /* 从以太网接口取数据送入LWIP协议栈 */ ethernetif_input(&gnetif); /* 处理LWIP超时:ARP请求、TCP重传、定时任务 */ sys_check_timeouts(); /* 周期性读取PHY link状态,1秒检查一次即可 */ if (tick_count % 1000 == 0) { HAL_ETH_ReadPHYRegister(&heth, PHY_ADDRESS, 0x01U, &regval); linkup = (regval & 0x0004U) ? 1 : 0; /* BSR寄存器的bit2是link状态 */ } HAL_Delay(1); tick_count++; }

这段代码里,ethernetif_input把网卡收到的数据帧交给LWIP,这是裸机方案的数据入口,漏掉它再多的协议栈配置文件也白搭。sys_check_timeouts处理ARP缓存老化、TCP超时重传这类周期任务,没有它,网络的定时行为全是停摆的。PHY link检测读的是LAN8720A的BSR寄存器(地址0x01),bit2为1代表链路已连通,串口打印这个状态值,能直观看到网线插拔的变化。

无操作系统模式和带系统模式的本质差别就在这里:带RTOS的系统里,ethernetif_input扔给一个独立任务,中断收包、协议栈跑在另一个任务里;裸机没有调度器,所有事情都挤在主循环里挨个处理。主循环里不能有阻塞操作,比如HAL_Delay(1000)这种长延时,一阻塞,网卡接收缓冲区就来不及清空,数据包被DMA溢出丢弃,表现就是PING时通时不通。

5. 避坑记录:五个PING不通的典型现场

5.1 现象:HAL_ETH_Init返回HAL_TIMEOUT

初始化直接超时,串口打印停在ETH Init失败。查代码逻辑通篇没看出问题,PHY地址也改成了0,读寄存器还是超时。原因是CubeMX生成的工程里,PHY Address不是从宏定义读取的,而是直接硬编码在eth的结构体里,你改了宏定义但结构体里的值是1,HAL初始化拿地址1去访问,当然超时。解决:找到heth.Init.PhyAddress这个赋值语句,直接把它改成0,注意宏定义只是给其他模块用的,这里才是HAL真正读到的地址。从那以后我每次生成工程都会搜索一遍PhyAddress这个字段,确认它和硬件一致再编译。

5.2 现象:PHY ID读出来全0xFF

上电后发现HAL_ETH_Init返回正常,但打印PHY ID读出来是0xFFFF,正常应该是0x0007。查了地址、复位都正常,最后拿示波器看MDIO引脚的波形,发现有波形但幅度不对。原因是板子上MDIO的上拉电阻没焊,或者阻值太大,导致数据线电平不稳定。解决:补一颗10kΩ上拉电阻到3.3V。这里有一个规律:MDC和MDIO两根线上的上拉电阻,是PHY芯片正常工作的前提,很多开发板原理图上有画,但实际贴片时漏了住。排查时优先看这两根线的电平和波形,比反复改代码高效得多。

5.3 现象:MCO2引脚量不到50MHz波形

示波器探针点在PC9上,看到的是一团杂波或者没有波形,但CubeMX时钟树配置明明选了50MHz输出。查了代码,HAL_RCC_MCOConfig是有了,但MCO2引脚的GPIO复用没配置。CubeMX正常情况下会自动配好引脚,但如果你在引脚配置页面手动动过PC9,软件生成的MX_GPIO_Init里就可能把它覆盖成普通GPIO了。解决:去GPIO配置页面确认PC9的复用功能是AF0(MCO)。另外注意量测点:MCO信号是高速方波,示波器要用短地线夹,探头带宽不低于100MHz,否则看到的是被衰减过的模糊波形。

5.4 现象:link状态正常但PING第一次总是超时

ARP缓存还没有建立起对方的表项,又或者PC网卡没有正确配置同网段地址。PING测试时,第一发请求发出,LWIP要先发ARP广播解析PC的MAC地址,如果PC没有响应,第一发就丢了。解决:先ping一次建立ARP表项,或者在PC端先ping一下开发板的IP(PC主动发起,ARP表先建好),然后再从PC ping开发板就通了。这种问题不算bug,是ARP机制的正常现象,知道原理后就不慌,测试时先互相ping两轮再下结论。

5.5 现象:发送正常但接收偶尔掉包

串口打印发送成功的计数一直在涨,但PC远程PING开发板时,丢包率不正常,比正常值高很多,甚至达到50%。打开ETH DMA的中断优先级设置页,发现接收中断优先级比发送低,同时系统里还有别的外部中断在频繁触发。解决:把ETH的DMA中断优先级拉到最高,至少高于DOUT串口这类低速中断。DMA接收缓冲区一旦满了,新的数据包直接丢弃,这是硬件行为,代码层感知不到。调整中断优先级后,丢包问题消失。CAN和串口这类外设的中断处理很快,问题不大,但如果有外部按键之类的低优先级中断在忙循环里跑,ETH的DMA接收就会被卡。

6. 验证跑通:PING测试的完整套路与进阶检查

环境装好、代码改完,最后验证这一步不能只按一下“PING通”就结束。我的标准验证流程分四步:第一步,开发板上电,串口打印出PHY芯片ID,确认读到0x0007;第二步,网线连接PC,打印link up状态;第三步,PC配置静态IP,比如192.168.1.100,开发板IP从代码里确认是192.168.1.10;第四步,开始PING测试,用下面这组命令做完整检查:

ping 192.168.1.10 -n 100

连续ping100个包,观察丢包率。正常情况丢包率应该是0%,延时不超过几毫秒。如果这个测试通过,我会继续做一个进阶验证:开发板上跑一个UDP回环测试,从PC发送UDP数据包到开发板的指定端口,代码里收到后原样发回。这一步确认的不只是ICMP通,而是整条UDP链路都是通的,为后续TCP协议栈的信号量分配、内存池检查做铺垫。裸机场景下我还会顺手看一下ETH DMA的错误计数器,读ETH外设的DMA状态寄存器,确认没有溢出或错误帧。

完整的无操作系统以太网环境,验证到最后其实就是两句话:PHY初始化要稳定,主循环轮询不能阻塞。我那套环境调完之后跑了48小时连续PING测试,丢包率0%,才敢说这套方案真正落地。从那以后,我每次搭STM32以太网环境都强制走一遍完整流程:先看硬件引脚表,再配时钟树和ETH外设,确认PHY地址,补软复位,检查lwipopts.h,然后才进主循环联调。这套流程看起来比直接开干多花了半小时,但这半小时换来的,是从“PING不通到处猜”变成“按顺序排就能找到问题”,希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询