☰
STM32H7+LAN8720A+LWIP以太网配置全攻略:原理、踩坑与排障
2026/10/5 5:02:18 网站建设 项目流程

1. 写在前面:为什么折腾了这么久,最后要单独写一篇“End”

先交代一下背景。我之前几篇文章把STM32H7 + LAN8720A + LWIP这套东西从零到能跑通的完整过程都梳理了一遍,从CubeMX图形化配置,到PHY芯片调通,再到LWIP协议栈移植、Ping通、TCP收发,每一步都踩了不少坑。本来以为到“能Ping通”就已经结束了,结果后面做实际项目时才意识到,真正折磨人的不是“跑通demo”,而是“稳定可靠地跑在实际板子上”。

这个系列的最后一篇,我想把整个配置链路里那些最容易翻车的细节点全部摊开讲一遍。标题里的“End”不是指我以后再也不碰这玩意儿了,而是指这套知识体系在我这边已经收口,再遇到类似问题基本都能快速定位。文章会聚焦在“配置”二字上,包含ETH外设本身、LAN8720A这颗PHY芯片、以及LWIP协议栈三者的协同工作,把那些CubeMX默认配置不会告诉你的底层机制和工程经验一次性讲透。

这篇内容适合什么人看呢?如果你正准备用STM32H7系列做带以太网的设备,或者你已经能用标准库/老版本HAL库把网络跑起来,但升级到最新STM32CubeMX时遇到了PHY地址对不上、Link灯不亮、Ping不通或者跑半小时就死机这类问题,这篇文章应该能帮你省下至少一周的排查时间。如果你只是随便看看,那也没关系,里面关于PHY芯片复位时序、RMII时钟同源、LWIP内存池调优这些知识,换个平台换颗芯片也一样用得上。

提示:我用的开发环境是STM32CubeMX 6.x版本 + STM32H7 HAL库 1.11.x,MCU是STM32H743ZIT6(Nucleo-144板载LAN8720A那颗),PHY是自带25MHz晶振方案的LAN8720A。不同硬件版本和库版本在细节上可能有差异,但核心原理是通用的。

2. 整体方案设计与底层机制拆解

2.1 为什么选STM32H7而不是F4/F7

在正式聊配置之前,先把方案选型这件事说清楚。很多人看到LAN8720A第一反应是“这是F407的老搭档了”,确实,STM32F407 + LAN8720A是当年最经典的以太网组合,网上教程一大把。但如果你想做稍微复杂一点的业务,比如同时跑MQTT + HTTP服务器 + TLS加密,F407的CPU主频和RAM就有点捉襟见肘了。

STM32H7的优势在于:主频最高能到480MHz(我这个型号跑400MHz),内核是Cortex-M7,带DP-FPU(双精度浮点)和DSP指令集,而且H7的以太网MAC是支持IEEE 1588精确时间协议的增强版。更关键的是H7的ETH外设挂在不同总线域上,配合大容量RAM(H743是1MB RAM),可以在不用DMA描述符频繁搬运的情况下实现比较高效的收发。在实际项目中,H7跑LWIP做TCP服务器,同时还要采集ADC、处理传感器数据、跑一个轻量级文件系统,CPU占用和内存占用都还在可控范围内。

再一个很朴素的理由:H7系列的芯片价格这些年已经降下来了,很多量产项目用H7做网关或者控制器,性价比是划算的。如果你只是做个小玩具或者学习用途,F407完全够用,但如果你要做产品,H7的余量会让你后续加功能的时候不用换平台。

2.2 LAN8720A在链路中的角色和RMII接口原理

ETH外设和PHY芯片之间是MAC层和物理层的分工关系。STM32H7自带的ETH外设是MAC层,负责处理帧的组装、CRC校验、DMA传输这些“数据链路层”的活;而LAN8720A是物理层收发器,负责把MAC层交过来的并行数据转换成差分串行信号发到网线上,同时把网线收到的差分信号解调还原成并行数据交给MAC。两者之间通过MII或RMII接口连接。

这里必须重点说RMII。RMII(Reduced Media Independent Interface)是精简版的MII接口,数据线从MII的4位变成了2位,时钟频率提高到了50MHz。对于STM32H7来说,RMII接口需要用到的引脚包括:

  • ETH_REF_CLK(参考时钟,50MHz)
  • ETH_CRS_DV(载波侦听/数据有效)
  • ETH_RXD0、ETH_RXD1(接收数据2位)
  • ETH_TXD0、ETH_TXD1(发送数据2位)
  • ETH_TX_EN(发送使能)
  • ETH_MDC、ETH_MDIO(管理接口,用于读写PHY寄存器)

注意,RMII的50MHz参考时钟从哪里来,这是配置里最容易埋雷的地方。LAN8720A这颗芯片比较特殊,它内部集成了一个可以输出50MHz时钟的PLL,但外部需要给它提供一个25MHz的参考时钟(通常由板上25MHz无源晶振提供)。更重要的是,这个50MHz时钟既可以由LAN8720A自己生成并通过REF_CLK引脚输出给MCU,也可以由MCU的MCO引脚输出50MHz时钟给LAN8720A。两种方式必须二选一,而且要求ETH外设的时钟和PHY的时钟必须是同源的关系,否则数据收发会出现随机错误。

这是LAN8720A原理图设计时就要确定的硬件方案,软件上无法改变。Nucleo-144开发板采用的是LAN8720A自身提供50MHz时钟的方式,也就是PHY给MCU供时钟。所以你在CubeMX配置ETH时,RMII时钟源那一项要选对,不然初始化就会失败。

2.3 软件分层:HAL库ETH驱动与LWIP协议栈的配合关系

从软件分层来看,整条链路由三层组成。最底层是HAL库的ETH驱动,它负责初始化MAC寄存器、配置DMA描述符、收发数据包;中间层是LwIP协议栈,它本身不直接操作硬件,而是通过一个叫ethernetif.c的接口文件与HAL层对接;最上层才是你的应用程序,通过LwIP提供的socket API或netconn API进行网络通信。

这里要明白一个关键点:LwIP的数据包接收并不是“来一个包就处理一个包”,而是靠中断+轮询组合的方式。STM32H7的ETH外设收到数据帧后,DMA会把数据写到内存描述符指向的缓冲区,然后触发接收中断。在中断服务函数里,我们会调用HAL_ETH_ReadData函数把数据复制到LwIP的PBUF结构体中,再通过netif->input()把数据交给协议栈处理。发送则是相反流程:应用程序把数据打包成PBUF,调用netif->linkoutput(),最终经HAL_ETH_Transmit发送出去。

理解这个流程后,你就能明白为什么“配置问题”往往出在硬件抽象层和协议栈的交接处,而不在协议栈内部。这也是我写这篇文章想强调的核心观点:当你能把“ETH外设已经正常收到数据”和“LwIP协议栈没有正确处理数据”这两件事区分开时,排查问题的效率会提升一个量级。

3. CubeMX图形化配置逐项拆解:每个参数背后的为什么

3.1 引脚复用与原理图对照:有些坑从画板子时就埋下了

如果你是用现成开发板,引脚配置直接照搬CubeMX预设就行。但如果你是画了自己的板子,第一步应该先打开原理图核对LAN8720A的接线,而不是急着开CubeMX。

LAN8720A有几个关键引脚需要特别关注:

  • PHYAD0引脚:这个引脚的电平决定了PHY的I2C(其实是MDIO)地址。LAN8720A默认地址是0,但如果你的板子把PHYAD0拉高了,地址就变成了1。很多人在CubeMX的ETH配置里填PHY Address = 0,结果MDIO通信失败,明明接线没问题就是读不到PHY寄存器,大概率就是这里的问题。
  • nINT/REGOFF引脚:这个引脚如果拉低,内部1.2V稳压器会被关闭,此时需要外部提供1.2V电源。绝大多数设计是拉高,但如果你画板子时随手接地了,PHY直接不工作。
  • LED0/LED1引脚:这两个引脚除了驱动指示灯,还兼有配置功能,比如LED1(通常在引脚号19)的电平状态会影响是否进入某些测试模式。开发板上通常已经处理好了,自制板要留意。

我个人的习惯是,拿到一块新板子,先用示波器量两处:一是LAN8720A的XTAL1/CLKIN引脚是否有25MHz时钟,二是REF_CLK引脚是否有50MHz时钟。如果这两处都正常,PHY的初始化才有可能成功;反之,软件上再怎么折腾都是白费。

3.2 时钟树配置:RMII 50MHz同源问题的正确解法

这是整个配置里最关键也最容易出错的地方。前面说了,STM32H7作为MAC侧,它的RMII接口需要一个50MHz的参考时钟。这个时钟由RCC时钟树里的PLL2P输出提供。

在CubeMX的Clock Configuration页面,操作步骤如下:

  1. 选择HSE(外部高速晶振)作为时钟源,我这里HSE是25MHz的。
  2. 配置PLL2,让PLL2P输出50MHz。具体做法是:PLL2M = 5,PLL2N = 20,PLL2P = 2,这样25MHz / 5 * 20 / 2 = 50MHz。
  3. 在ETH外设的配置里,将RMII的时钟源选择为“PLL2P”或“外部时钟”(取决于你的硬件方案,Nucleo-144选择的是PHY提供时钟的选项,即External,但系统时钟树里PLL2P还是要配出来作为ETH外设的参考)。

这里有一个非常隐蔽的问题:时钟树界面里可能同时存在“ETH”和“ETH_RMII”两个时钟选项,很多人只配了系统主时钟就以为完事了,ETH的时钟其实没有生效。正确做法是在RCC配置里把Ethernet RMII时钟勾选上,然后查看右边的时钟树图,确保50MHz确实到达了ETH模块。

还有一点,PHY侧的50MHz和MCU侧的50MHz必须是同源的。在Nucleo板上,LAN8720A自己产生50MHz给MCU,所以MCU的ETH外设时钟必须配置成“从外部接收”这种模式。如果你自己在板子上用的是MCU输出50MHz给PHY的方案,那CubeMX里选的时钟源就完全不同了。这两种方案不能混,混了就会出现一种很诡异的现象:复位后偶尔能通,重启就断,或者发数据频繁出错。

3.3 ETH参数配置项逐项说明

在CubeMX的Connectivity -> ETH配置界面,有几个参数需要手动确认,不要全部依赖默认值。

  • PHY Address:前面说了,LAN8720A默认是0,但也可能是1。怎么确认?读PHY寄存器1(PHYIDR1)的返回值是0x0007,寄存器2(PHYIDR2)是0x0200(LAN8720A的ID)。如果MDIO能正确读到这两个值,说明PHY地址没填错。
  • PHY Reset GPIO:设置复位PHY所用的引脚。H7的HAL库支持在初始化时自动拉低复位引脚再释放,这个功能叫PHY Reset。建议明确指定一个GPIO,防止PHY上电时处于不确定状态。初始化的时序要求是复位低电平至少保持一段时间(局域网PHY通常要求几十微秒以上),HAL库底层使用的是HAL_GPIO_WritePin加延时实现,不同版本的库延时时长可能有差别,如果PHY偶尔初始化失败,检查这里。
  • Ethernet Speed(速率):LAN8720A是10/100M自适应的,选择AutoNegotiation即可。
  • DMA描述符数量和缓冲区大小:CubeMX默认是4个发送描述符和4个接收描述符,每个描述符对应一个固定大小的缓冲区,默认值一般是1524字节(刚好容纳一整个以太网帧,包含CRC)。如果项目上有大包收发需求,可以适当增加描述符数量或增大缓冲区,但这个数不建议盲目调大,因为每个描述符的缓冲区都是从RAM里静态分配的,调太大会白白占用内存。

3.4 LWIP配置项逐项说明

LwIP部分的配置同样要过一遍。在Middleware -> LWIP配置界面里,有几个关键参数对稳定性影响很大:

  • LWIP版本:CubeMX一般提供2.0.3、2.1.2等版本。我推荐用2.1.2或更高,因为2.1.x修复了旧版里若干TCP重传和内存管理的bug,而且API变化不大,网上资料也比较多。
  • Memory Heap Size(内存堆大小):这个参数定义了LwIP使用malloc方式申请内存的总量,PBUF结构体、TCP控制块、UDP控制块都从这里分配。默认值可能是几十KB,对于复杂应用建议至少改到 50 * 1024 或更大。太小会导致TCP连接建立失败,症状是连接时好时坏,甚至直接报No memory。
  • Thread Settings:LwIP协议栈可以跑在独立线程里,CubeMX默认会创建一个叫lwip_thread的线程,优先级和栈大小都可以设置。栈大小建议不要低于1024字节,实际项目里TCP跑大流量时,栈太小会触发堆栈溢出,表现是运行一段时间后系统 HardFault,极其难查。
  • TCP/IP Protocol 选项:按需勾选TCP、UDP、ICMP等。如果你的应用只需要UDP通信,尽量别勾TCP,因为TCP控制块会占用内存。

这里有一个经验法则:LwIP的配置不是“越大越好”,而是“按需分配”。内存池配太大了,单片机的剩余RAM就少了;配太小了,连接不稳定。建议先用默认配置跑通基本功能,再根据实际压力测试结果逐步调整。

4. 核心代码实现与关键机制解读

4.1 初始化流程:从上电到能Ping通的完整路径

硬件上电后,系统的启动流程大致是:复位向量 -> 系统时钟初始化 -> GPIO初始化 -> ETH MAC初始化 -> PHY芯片复位 -> MDIO读取PHY ID -> 配置PHY寄存器 -> LwIP协议栈初始化 -> 启动LwIP线程。

在HAL库框架下,ETH的初始化函数是MX_ETH_Init(),它内部会调用HAL_ETH_Init()。这个函数的执行过程包括:

  1. 配置MAC地址(从CubeMX生成的huart结构体里读取,但默认是全零,需要手动填入你的板卡MAC地址)。
  2. 初始化DMA描述符链表。
  3. 配置MAC寄存器,包括帧过滤模式、FCS处理、自动协商等。
  4. 启动DMA传输。

需要注意的是,HAL_ETH_Init()本身不会操作PHY寄存器。PHY的初始化是独立的一步,通常放在MX_ETH_Init()之后,由用户代码调用HAL_ETH_ReadPHYRegister和HAL_ETH_WritePHYRegister来完成。CubeMX生成的代码模板里,PHY初始化是放在main()函数中通过调用HAL_ETH_Start之前的一段扩展代码实现的。

让我把重要的流程代码列出来,你会看到PHY复位和读取ID是整个初始化中最关键的两步。以CubeMX生成的代码为基础,我一般会这样补全PHY的初始化逻辑:

uint32_t phyreg; uint8_t timeout = 0; // 复位PHY(假设PHY_RST连接到PG2) HAL_GPIO_WritePin(GPIOG, GPIO_PIN_2, GPIO_PIN_RESET); HAL_Delay(50); HAL_GPIO_WritePin(GPIOG, GPIO_PIN_2, GPIO_PIN_SET); HAL_Delay(100); // 等待PHY上电复位完成 // 读取PHY ID,验证MDIO通信正常 do { HAL_ETH_ReadPHYRegister(&heth, PHY_ADDRESS, PHY_IDR1, &phyreg); timeout++; } while ((phyreg != 0x0007) && (timeout < 10)); if (timeout >= 10) { Error_Handler(); // 读不到PHY ID说明接线或地址有问题 }

注意:HAL_ETH_ReadPHYRegister的最后一个参数是指针,用来保存读到的值。如果你用的是旧版HAL,函数签名可能是返回值的方式,区别很大,编译的时候会直接报错,很好排查。

PHY ID读取成功后,还需要配置PHY的工作模式。LAN8720A可以通过MDIO写寄存器0(BCR,基本控制寄存器)来实现软复位和设置自动协商,也可以通过寄存器31(PHY特殊控制寄存器)来设置其他功能。我通常会在初始化最后强制开启LAN8720A的“自适应交叉检测”功能,这样就算网线是交叉线或者直通线,都能自动匹配。这个功能默认是开启的,但如果你发现直连PC能通、接路由器不通,重点查这里。

4.2 以太网中断与数据接收的协作机制

STM32H7的ETH外设支持接收中断,每收到一帧数据,DMA都会触发一次中断。在CubeMX生成的代码中,接收中断的入口是ETH_IRQHandler(),HAL库已经帮你写好了中断处理流程,你需要关心的是它的回调函数HAL_ETH_RxCpltCallback()。

我的以太网接收实现思路是:在回调函数里调用LwIP协议栈的netif->input(),把数据交给LwIP处理。一般不会直接在回调里进行协议解析,因为LwIP内部有自己的线程和锁机制,直接在中断上下文调用复杂的协议栈接口容易引发优先级反转或死锁。正确的做法是使用LwIP提供的“独立线程 + 信号量”模式:中断回调里只释放一个二值信号量,LwIP线程等待信号量后统一处理。

CubeMX生成的ethernetif.c文件里已经实现了这套机制,它使用sys_sem_signal函数通知LwIP。但是有一个坑:如果你同时在多个地方调用HAL_ETH_ReadData,比如在中断里读一次,又在主循环里读一次,就会造成描述符状态混乱,轻则丢包,重则死机。所以一定不要多个地方同时操作同一个ETH句柄的接收流程。

4.3 数据发送时的内存模型与注意事项

发送数据时,LwIP会通过low_level_output()函数把数据送入HAL层。HAL层需要把数据从LwIP的PBUF内存复制到DMA描述符对应的缓冲区,然后启动DMA发送。

这里有个性能优化点:如果DMA描述符的缓冲区大小足够大,而且数据对齐满足要求,HAL库会采用零拷贝方式直接让DMA从PBUF内存里读取数据;如果不满足条件,就只能做一次memcpy。对于H7这种内置大缓存的高性能MCU,拷贝一次的开销往往可以接受,但如果你追求极致性能,要确保ETH_TX_DESC_CNT每个描述符对应缓冲区大小能容纳最大帧,并且LwIP的PBUF内存地址对齐到32字节。在CubeMX中,ETH_RX_BUF_SIZE变量影响接收缓冲对齐,确保它是4的倍数。实际测试下来,H743在400MHz主频下,DMA方式收发一百兆带宽时CPU占用率可以控制在可接受范围。

4.4 时钟同步和MAC地址设置的细节

MAC地址在很多例程里被忽略,直接用默认全零地址就能Ping通,因为LwIP默认会随机生成一个MAC。但在实际项目中,设备接入路由器或交换机时,如果MAC地址和别的设备冲突,就会出现网络时断时续的情况。建议使用烧录在MCU OTP区域或外部Flash里的唯一ID来生成MAC地址:

uint32_t uid[3]; uid[0] = HAL_GetUIDw0(); uid[1] = HAL_GetUIDw1(); uid[2] = HAL_GetUIDw2(); // 把UID映射成MAC地址的低3字节,高3字节固定为厂商段 uint8_t mac[6] = {0x02, 0x00, 0x00, (uint8_t)(uid[0] & 0xFF), (uint8_t)(uid[1] & 0xFF), (uint8_t)(uid[2] & 0xFF)};

注意,MAC地址第一位的最低位表示单播/组播,第二位表示全球唯一/本地管理。手工生成MAC时,把第一个字节设为0x02开头,是标准做法,能避免和真实网卡冲突。另外H7的UID寄存器地址在不同系列上可能不同,CubeMX的HAL库已经封装了这三个读取函数,直接用就行。

5. 实操过程中踩过的坑:常见问题与排查实录

5.1 PHY地址对不上:读不到PHY ID怎么办

这是我遇到过最普遍的问题。表现为CubeMX生成的工程下载后,程序卡死在Error_Handler(),检查代码发现是HAL_ETH_ReadPHYRegister返回超时。

排查步骤建议按这个顺序来:

  1. 确认MDIO引脚复用是否正确。很多自制板会把ETH_MDC和ETH_MDIO引脚复用错位,比如本应是PA2但配置成了PC2。用万用表量MDC引脚有没有方波信号,正常情况下初始化期间MDC引脚一直有2.5MHz左右的管理时钟(实际频率取决于AHB分频)。
  2. 确认PHY地址是0还是1。用示波器或万用表量PHYAD0引脚的状态,如果悬空一般默认为0,如果上拉就是1。然后在代码里分别用0和1尝试读取PHY ID,实测能读通的就是正确地址。
  3. 降低MDC时钟频率试试。如果MDIO总线上有其他负载,或者走线较长导致信号质量差,HAL库默认的MDC频率可能太高导致读失败。在HAL_ETH_Init之前通过修改heth.Init.MediaInterface和heth.Init.PhyAddress之外的地方(例如RCC时钟配置中降低ETH外设时钟)来间接降低MDC频率,可以解决一部分兼容性问题。

5.2 能Ping通但一段时间后Ping不通:排查Link中断和自动协商

有一个典型故障:系统刚上电时Ping通,跑个几分钟后Ping不通,重启又恢复了。这个现象大概率与PHY的Link状态监测和LwIP的链路状态轮询有关。

CubeMX生成的LwIP代码默认在ethernetif.c里启用了ETH_LINK_POLL_INTERVAL的定时器,每隔2秒读取一次PHY的寄存器,判断网线是否插好。问题是,当PHY的状态寄存器读到“连接已断开”时,LwIP会认为链路Down,关闭网络接口;当再次读到“连接已建立”时,会重新初始化低速协商。如果这个过程出现错误,比如在网线稳定连接的情况下PHY的Link状态波动,就会导致接口反复Down/Up,表现为一段时间无法Ping通。

解决思路是:检查PHY寄存器0的Bit 2(Link Status,自动协商完成/连接状态),这个位在标准BCR中,但它是否稳定取决于PHY供应商的实现。LAN8720A的Link状态位在寄存器1(状态寄存器)的Bit 2,这个位只读,并且在实际网线断开后会跳变。用示波器或反复插拔网线来验证这一位的变化是否正常。如果发现Link状态频繁抖动,可能需要检查LAN8720A的供电,特别是vddcr引脚(芯片内部1.2V输出)的滤波电容是否足够,贴片电容虚焊会导致PHY工作电压纹波大,从而产生误判。

5.3 只有发送没有接收或接收丢包:DMA描述符和缓冲对齐

这个问题通常在开启D-Cache后出现。STM32H7的Cortex-M7内核带有D-Cache,而ETH的DMA是直接访问内存的,不会经过Cache。当CPU写数据到DMA缓冲区时,数据会先停留在Cache中,DMA读取到的可能是旧数据;反过来,DMA写入了接收数据,CPU从Cache读到的可能不是最新值。

解决办法有两种:

  • 在CubeMX里关闭D-Cache。简单粗暴,但会影响整个系统的性能,特别是涉及图形或数据处理的应用。
  • 对ETH相关缓冲区做MPU配置,设置成不缓存(Non-cacheable)区域。这是更好的办法。在main()函数里用MPU_Config(),把ETH DMA描述符和接收发送缓冲区的地址段标注为Device或Non-cacheable,这样DMA和CPU看到的都是同一份内存。

这里需要特别说明一个误区:很多人把H7的DMA缓冲区放在任意位置,结果发现D-Cache一开启数据就乱了。正确的做法是,在链接脚本或系统初始化中为ETH的DMA描述符和缓冲区预留一段独立的内存区域,然后将整个区域配置为MPU Non-cacheable。CubeMX默认生成的MPU配置其实已经包含ETH相关区域的保护,但如果你修改了缓冲区大小或描述符数量,MPU配置也要同步调整。我调试时遇到过一次诡异现象:接收缓存数组定义在512字节对齐的边界上,但描述符数量从4改到8之后忘记调整MPU区域大小,结果接收到的数据总是错位,排查了整整一天才意识到是MPU覆盖范围不够。

5.4 LWIP内存耗尽导致TCP连接失败

LwIP的内存管理有两种方式:内存池(memp)和内存堆(mem)。TCP控制块、UDP控制块、PBUF结构体、网卡接口结构体等都是通过memp的方式预分配固定数量,而PBUF的数据区、TCP报文段数据区等是通过mem堆来动态分配的。

如果你发现TCP连接建立失败,或者建立后传输数据到一半就断,且lwip_stats里的mem_used数值持续增长不下降,基本可以断定是内存泄漏或内存池耗尽。排查要点:

  1. 减少不必要的高水位缓冲。比如TCP的接收窗口默认可以调大,但如果应用数据量不大,没必要开那么大。
  2. 检查是否有调用tcp_abort后没有释放PBUF。这种情况多发生在异常处理分支,比如连接超时后直接丢弃了接收队列而没有调用pbuf_free。
  3. 适当增大PBUF池数量。在lwipopts.h里找到PBUF_POOL_SIZE,默认可能只有16个,如果你的应用一次性并发接收多个连接的数据,建议调到32或更多。但要注意每个PBUF占用内存,调太多也会吃RAM。

我自己的项目里用100个TCP连接做压力测试时,出现过内存不足的问题。后来把MEMP_NUM_TCP_PCB从默认值调大到32,PBUF_POOL_SIZE从16调大到32,才稳定下来。这个值不是一个固定指标,要根据你的实际场景来权衡。

5.5 板上网络指示灯状态与PHY状态不对应

LAN8720A的LED引脚是开漏输出,通过电阻上拉到3.3V。如果你发现网线插上后,PHY能正常工作但指示灯不亮,先查LED引脚的封装和外部上拉电阻。如果指示灯逻辑反了,可以写PHY寄存器改LED模式。LAN8720A的LED配置在寄存器20(LEDCR)的Bit 0和Bit 1,00表示Link状态(Link up时亮),01表示10M模式,10表示100M模式。我这个项目里把LED0配成了Link/Activity模式(Bit 0=0, Bit 1=0),这样插上网线常亮,有数据时闪烁,调试起来非常直观。

另外,LAN8720A的EXP引脚(Pin 14)需要特别注意,很多原理图上它会通过一个RC串联到地,这个引脚上的电平影响PHY的休眠模式。部分开发板的原理图直接用跳线决定是否拉低,默认要让它处于正常工作电平。

6. 性能调优与稳定性加固:让以太网真正能上生产

6.1 发送缓冲区和描述符数量的平衡选择

CubeMX默认提供4个TX描述符、4个RX描述符。对于简单的Ping和轻量TCP通信,这个配置足够。但如果你要跑比较大的数据并发,比如一边通过UDP接收传感器数据,一边通过TCP上传到服务器,4个TX描述符很可能成为瓶颈。

原因在于:当LwIP上层应用连续调用netif->linkoutput()发送数据时,HAL层会把数据填入TX描述符,然后启动DMA。如果描述符都被占满了(DMA还没发送完),HAL函数会返回超时或错误码,LwIP会重传,重传又占用描述符,形成恶性循环。

通常我会把TX描述符设为8,RX描述符设为8,然后观察实际丢包率和CPU占用率。这个数量不是越多越好,因为每个描述符都有一块独立的DMA缓冲区,8个描述符意味着8个1524字节的接收缓冲区,在H7这种大RAM MCU上还可以接受,但在F4上就可能影响可用RAM。H7的优势在这里体现得很明显:1MB RAM,开8个描述符根本不心疼。

6.2 Nagle算法与TCP粘包问题

在做TCP数据传输时,很多初学者会发现:发送端连续调用多次send(),接收端一次性收到一大包数据,这被称为TCP粘包。这其实是Nagle算法起的作用:TCP会合并小数据包减少网络开销,Nagle算法的功能是当发送方有未确认数据包时,不再发送小数据包,而是等待数据累积或收到ACK后再发送。

如果你的应用场景是实时性较强的指令控制,这种延迟可能无法接受。解决方法是关闭Nagle算法。在LwIP中,创建TCP连接前调用tcp_nagle_disable()可以动态关闭该连接上的Nagle算法:

struct tcp_pcb *pcb = tcp_new(); tcp_nagle_disable(pcb);

当然,关闭Nagle会增加网络中的小包数量,增大带宽占用。对于视频流这类大流量数据,Nagle反而可以提升效率。至于怎么取舍,取决于你的业务场景。我在做远程控制设备时果断关闭了Nagle,因为设备端对响应延迟非常敏感;做固件升级传输时又重新开启,因为这个场景带宽利用率更重要。

6.3 LWIP线程优先级与MCU主循环的配合

LWIP协议栈在CubeMX生成的工程里是运行在一个独立线程中的。这个线程的优先级不能设置得太高也不能太低。优先级太高,会影响实时任务(比如PID控制、电机控制)的响应;优先级太低,网络数据的处理就会被其他任务阻塞,导致TCP重传增加,速度下降。

我的经验是:如果主循环里有对实时性要求极高的任务(比如1ms中断里做ADC采样和控制算法),LWIP线程优先级应该设为低于定时器中断,但高于一般后台任务。FreeRTOS下可以设为osPriorityAboveNormal这样的级别,其他普通任务设为osPriorityNormal。在实际调试时,可以先用较低优先级测试网络性能,再逐步提高观察系统其他任务是否出现卡顿,找到一个平衡点。

还有一个细节:LwIP线程默认的while(1)循环里会sys_check_timeouts()处理超时事件(TCP重传、ARP老化等)。如果你的系统长时间空闲,这个回调的执行频率很低,一些超时事件处理不及时,可能导致TCP连接被误判为超时断开。建议在系统空闲时让LwIP线程以一定的频率主动调用该函数,比如每250ms一次,保证TCP状态机正常运行。

6.4 大规模数据收发时的实测心得

我基于这套方案做了一块带以太网的数据采集板,用TCP回环做了72小时连续收发压力测试。数据量是每100ms发一次,一次发2KB,同时接收端偶尔会收到ACK。测试期间没有出现死机、丢包或内存泄漏问题。稳定性的关键点总结下来有三个:

一是确保收发缓冲区和描述符都在非缓存区域。H7跑以太网,MPU配置如果不对,D-Cache一开必出问题,这是所有问题里最隐蔽也最致命的一个。

二是LWIP的内存池配置要留足余量,特别是在连接建立和断开频繁的场景里,TCP控制块如果不足,会导致新连接无法建立,而旧连接资源又没有及时释放。

三是PHY的供电质量决定了长期稳定性。LAN8720A的模拟部分对电源纹波较敏感,如果板上3.3V是由DC-DC直接供电且纹波较大,建议在PHY电源引脚附近加一个10uF+0.1uF的退耦电容组合。测试中发现,电源纹波大的板子上,Link状态会出现间歇性跳变,网络表现为每隔几分钟断一次,非常难查。

7. 几个容易忽略但能让体验翻倍的细节

7.1 用自编的小工具快速验证PHY通信

调试以太网时,最烦的是“不知道是硬件坏了还是软件配置错了”。这里分享一个实用技巧:在连接LwIP之前,先写一个独立的PHY寄存器读写测试函数,通过MDIO读取PHY ID、读取状态寄存器、写自定义寄存器再读出来,验证MDIO通信链路是通的。

uint32_t reg_value; HAL_ETH_ReadPHYRegister(&heth, PHY_ADDRESS, 0x1D, &reg_value); printf("Reg 0x1D = 0x%04X\r\n", reg_value); HAL_ETH_WritePHYRegister(&heth, PHY_ADDRESS, 0x1D, 0xAAAA); HAL_ETH_ReadPHYRegister(&heth, PHY_ADDRESS, 0x1D, &reg_value); printf("After write, Reg 0x1D = 0x%04X\r\n", reg_value);

如果写0xAAAA能读回0xAAAA,说明MDIO通信没有问题,PHY芯片本身也在正常工作。把这一步骤跑通了再去调LwIP,能省下很多排查时间。

7.2 把LWIP调试信息和状态统计打开

LwIP本身提供了很完整的调试统计信息接口,只要在lwipopts.h里打开相应的宏,就能通过串口输出内存使用、TCP连接状态、丢包统计等信息。我一般会打开LWIP_STATS和LWIP_DEBUG(按需打开模块级调试),这在定位复杂网络问题时非常有用,尤其是内存管理问题时,能看到memp池剩余数量,判断是否需要调大MEMP_NUM_TCP_PCB等参数。

需要提醒的是,打开调试宏会增加代码体积和运行开销,生产版本务必关闭。

7.3 没有网络环境的调试办法

如果你手头暂时没有路由器或交换机,可以通过网线直连PC来调试。PC需要手动设置一个静态IP,比如192.168.1.100,开发板设置成192.168.1.10,子网掩码255.255.255.0。注意LAN8720A支持自适应交叉检测,所以直通线或交叉线都可以用。

直连时最容易犯的错是把PC和板子设置成了不同网段。先Ping通再说其他。如果Ping不通,先用arp -a命令查看ARP缓存里有没有板子MAC地址的条目,如果有这个条目但Ping不通,说明链路层是通的,问题在IP层;如果ARP条目都没有,问题大概率在以太网链路层或PHY初始化。

8. 写完这篇End之后,我还想再交代一句

从最初照着例程改引脚,到后来能相对从容地处理PHY异常、性能瓶颈和各类资源冲突,整个过程其实就是对“配置”二字的反复打磨。很多人觉得能用CubeMX图形化点一点生成了代码就完事了,但实际项目里,最耗时的是理解每个配置项背后到底改了哪些寄存器,改错了会有什么后果,以及如何从现象反推根因。这篇“End”记录下的内容,是我把这套方案完全收口后的完整复述,希望对正在踩坑的你有一点帮助。

如果你手头的板子也遇到了类似的问题,不妨按我上面的排查顺序走一遍:先确认PHY硬件复位和时钟,再验证MDIO通信,然后看MPU和DMA缓冲区,最后才去怀疑LwIP内部的问题。这个顺序能帮你过滤掉大部分低级错误。

最后再分享一个小习惯:每次拿到新的以太网板子,无论是开发板还是自制板,我都会先写一个最小的“PHY Loopback测试”——通过MDIO把LAN8720A配置成内部回环模式,然后在MCU侧发送数据,看能否收到相同数据。这个测试能把PHY芯片和MCU之间的数据通路完整验证一遍,排除掉一大堆布线或焊接问题。具体操作是写PHY寄存器0的Bit 14设为1,同时关闭自动协商,然后以固定速率发送。能收到回环数据,说明MAC到PHY这一路完全没有问题,接下来就可以放心去调试LwIP了。

这套板子的以太网功能,至此告一段落。后续如果再碰到有意思的问题,我会单独开新篇写,不在这个系列里续了。

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

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

立即咨询