☰
STM32H743+LAN8720以太网调试全攻略:从RMII到LWIP稳定运行
2026/10/5 1:19:17 网站建设 项目流程

做嵌入式以太网这些年,我最怕听到的一句话就是"按教程配完了,但PING不通"。尤其是STM32H743搭配LAN8720这种组合,芯片本身没问题,运行频率480MHz又能跑到1MB RAM,但网口这个外设的坑位密度,远比你想象的高。我最近用CubeMX v6.5.0重新搭了一套LWIP+FreeRTOS的模板,从裸板到稳定跑通花了两天时间,其中至少一天半在处理各种匪夷所思的"隐形问题"。

这篇文章就把整个配置过程、每一处容易踩坑的细节、以及完整的排错链路一次说清楚。适合刚开始用STM32H743做以太网通信的工程师,也适合那些明明照着别人工程抄却跑不起来、正在抓狂的人。我会尽量把"为什么"也讲透——只告诉你怎么点鼠标没意义,你会点不一定知道点错了什么。

1. 硬件底子先自查:LAN8720与H743的RMII连接

很多人在CubeMX里设置了半天,最后发现是硬件连接本身有问题。这个检查必须放在最前面,省得后面白忙活。

1.1 RMII的7根信号线,少一根都不行

STM32H743自带100M以太网MAC控制器,但要跑物理链路还需要一颗PHY芯片。LAN8720是当前性价比最高的选择之一,几块钱一片,10/100M自适应,RMII接口只需要7根信号线就能和MCU对接:TXD0、TXD1、TX_EN、RXD0、RXD1、CRS_DV、REF_CLK,再加上MDIO和MDC管理总线,一共9根。相比MII接口的16根线,PCB走线负担小很多。

H743这边对应的RMII引脚映射是固定的,CubeMX里勾选ETH外设后会自动分配,但你在原理图阶段就得对照确认:

信号H743引脚接到LAN8720
RMII_REF_CLKPA1REF_CLK/CLKOUT
RMII_MDIOPA2MDIO
RMII_MDCPC1MDC
RMII_CRS_DVPA7CRS_DV
RMII_RXD0PC4RXD0
RMII_RXD1PC5RXD1
RMII_TXD0PB12TXD0
RMII_TXD1PB13TXD1
RMII_TX_ENPG11TX_EN

这里最容易出问题的是接线顺序搞反,尤其是TXD和RXD两组。有些人画板子时把TXD0和TXD1弄反了,或者把CRS_DV漏接了,后期软件怎么调都出不来。我吃过一次亏,最后用万用表一根根量才发现RXD0和RXD1互换,飞线解决。

1.2 时钟谁给谁:25M晶振与REF_CLK方向

LAN8720的RMII接口需要50MHz的参考时钟。现在市面上绝大多数的LAN8720模块都是板载一颗25MHz晶振接到PHY的XI/XO引脚,由PHY内部PLL倍频到50MHz,然后通过REF_CLK引脚输出给MCU。这种情况下,H743的PA1(ETH_RMII_REF_CLK)是输入模式,它接收的是PHY吐出来的50MHz时钟。

另一种方案是让MCU输出50MHz给PHY做时钟源,此时H743的ETH_CLK引脚用于输出,LAN8720的REF_CLK作为输入参考。这种方式在H743上需要额外配置时钟输出,一般没那个必要。

你在确认硬件方案时只有一个问题要问:LAN8720模块上有没有25M晶振?有,就说明它是"PHY提供时钟"方案;没有,那大概率是"MCU提供时钟"方案。开发板上最常见的是前者,不必在CubeMX时钟树里再费力气生成50M给PHY。

1.3 PHY地址的默认值:"0"还是"1"?

这是坑中之坑。LAN8720A的SMI地址由PHYAD0引脚决定,绝大多数模块把PHYAD0接地,所以默认地址是0x00。但有些国产兼容芯片或定制板卡会把PHYAD0上拉,地址就变成0x01。如果你的工程里写死了0x00而实际上模块是0x01,结果就是MDIO通信阳光灿烂但PHY完全不应答。

我习惯拿新板子时,先把CubeMX里ETH的PHY地址参数分别试0x00和0x01,配合下面的状态寄存器读取,10秒钟就能确定真实地址。另外一提,DP83848和KSZ8081这类芯片默认地址是0x01,经常有人从F429的DP83848工程迁移到H743+LAN8720时忘了改这个,白白浪费一下午。

2. CubeMX v6.5.0里的ETH配置,哪些参数不能偷懒

2.1 时钟树先摆平,ETH 50M从哪来

CubeMX v6.5.0里选中STM32H743后,第一步我会先把RCC时钟树配到最大480MHz(通过HSE+PLL1),这一步顺手操作就行。但你要注意到时钟树界面上有一个ETH相关的分支,名称通常显示为Ethernet或ETHREFCLK。CubeMX会让你在某个PLL输出里选频率,这里务必保证给ETH的是50MHz。

很多人栽在这一点:MAC控制器内部用的是AHB总线时钟,但RMII接口的时序基准来自外部提供的50MHz REF_CLK。如果你选的是"PHY提供时钟"方案,ETHREFCLK这个时钟树节点只需要设置为"旁路"或"外部时钟"即可;如果你选的是"MCU提供时钟",那就得保证Clock Output产生50MHz。

我的建议是:永远使用PHY本地晶振方案,这样时钟树里的事最少,也最不容易把LAN8720的REF_CLK引脚搞成冲突状态。调试时只要能通过软件读到PHY芯片ID,说明时钟链已经通了。

2.2 ETH参数页的正确填法

CubeMX左侧"Connectivity"里找到ETH,模式选择RMII。进入Parameter Settings,几个关键字段务必手动确认,而不是轻信默认值:

  • Media Interface:RMII,不是MII。
  • PHY Address:按硬件实际地址填,常见是0x00。
  • MAC Address:建议直接填一组自己的地址,比如00:80:E1:00:00:01,别留全零或全FF,否则某些交换机会直接丢弃报文。
  • Checksum offload:H7的MAC支持硬件校验和卸载,建议保持使能,能减轻CPU负担。

还有一个比较隐蔽的选项是MDC时钟分频。HAL库会自动计算,但如果你的SMI总线上MDC频率太高,LAN8720可能反应不过来,表现为偶发读取失败。手动把MDC分频调到2.5MHz以下是一个稳妥的选择,不过我实测H743+LAN8720在默认配置下是稳定的,这个放到后面排错章节细说。

2.3 同时勾选FreeRTOS和LWIP时的顺序问题

在CubeMX里同时启用FreeRTOS和LWIP后,生成的工程会多出两个中间件目录:LWIP/App、LWIP/Target,以及Middlewares/Third_Party/LwIP源码。生成完代码后,最容易被忽视的是main.c里的初始化顺序。CubeMX默认生成的是:

MX_GPIO_Init(); MX_ETH_Init(); MX_LWIP_Init(); MX_FREERTOS_Init();

注意顺序:ETH初始化必须在LWIP之前。因为MX_LWIP_Init内部要调用底层网卡驱动的初始化,底层驱动又依赖ETH的DMA描述符和MAC寄存器已经就绪。如果你手欠调换顺序,轻则协议栈起不来,重则进HardFault。

另外,如果你改了ETH的DMA描述符缓冲区的内存段,比如想把描述符放到DTCM以外的高速RAM,记得同时调整链接脚本里的内存区域定义和使用。CubeMX默认生成的工程里,ETH的DMA描述符放在了ETH_DMA_DESC段,内存对齐要求32字节,这些细节不建议乱动。

3. LWIP不是默认就能跑的:内存与协议参数的调优

3.1 内存池有多重要:MEM_SIZE与PBUF_POOL

LWIP被诟病的地方之一就是内存参数多且晦涩。在CubeMX的Middleware and Software Packs -> LWIP配置页面里,你能看到一大堆零点几几的宏,实质影响跑不跑得起来的就那么几项:

参数建议值说明
MEM_SIZE10*1024或更大协议栈堆内存,用于PBUF、UDP/TCP控制块等
PBUF_POOL_SIZE16接收缓冲区池数量,太少会导致拥塞丢包
PBUF_POOL_BUFSIZE1512单包最大长度,1500MTU+14字节以太网头,留几个字节余量
TCP_MSS1460最大段大小,经典值
TCP_WND4*TCP_MSS接收窗口,影响大流量吞吐
TCP_SND_BUF保持一致发送缓冲,影响TCP发送速率

很多人跑TCP客户端时发现发送卡顿,把TCP_SND_BUF调大就明显改善。而UDP收包丢包严重时,先考虑PBUF_POOL_SIZE是不是只有4或者8。H743有1MB RAM,大方一点没问题,我实际配的是MEM_SIZE=60*1024,PBUF_POOL_SIZE=32。

从裸机移植模型转过来的朋友要特别注意:CubeMX里如果NO_SYS设为0,说明走的是带OS的版本,LWIP内部会创建tcpip_thread和一个邮箱队列。理论上邮箱长度也要足够,LWIP_MBOX_SIZE建议至少等于PBUF_POOL_SIZE,否则高负载下消息堆积会丢事件。

3.2 DHCP还是静态IP,调试期的选择

CubeMX的LWIP配置页有DHCP选项开关。生产环境用DHCP没毛病,但调试网络通不通的第一天,请务必关掉DHCP,使用静态IP。原因很简单:DHCP需要协议栈完整跑起来、底层PHY link也要稳定,才能通过UDP广播拿到地址。一旦DHCP失败,你会分不清是协议栈坏了还是DHCP服务器不响应,排错链路直接多一层迷雾。

静态IP配置非常简单,在CubeMX里给LWIP填入:

#define LWIP_IPADDR "192.168.1.10" #define LWIP_NETMASK "255.255.255.0" #define LWIP_GW "192.168.1.1"

设好之后用一个直连网线连电脑,电脑IP配成192.168.1.x网段,然后直接PING 192.168.1.10。能通,说明底层和协议栈都稳了;不通,恭喜你进入第5章的排错链路。

3.3 任务优先级和tcpip_thread的分工

CubeMX为LWIP生成的线程方案里,tcpip_thread负责协议栈核心循环,它是整个网络的中枢。FreeRTOS里它的优先级默认是osPriorityNormal(CMSIS-RTOS2的140),这个值在大多数场景够用。但如果你主循环里跑着复杂的计算任务,或者有其他高优先级任务频繁抢占,tcpip_thread被饿死就会表现为网络慢、偶发超时。

我的做法是单独建一个网络配置线程,优先级设为osPriorityHigh(类似Normal+2),主要负责建Socket、收数据做业务处理;而tcpip_thread保持默认。至于CubeMX生成的defaultTask,我建议直接改名成一个idle风格的占位任务或干脆删掉,别在里面跑可能导致阻塞的业务逻辑,否则会和LWIP的消息队列抢CPU时间。

4. 中断优先级和FreeRTOS的"红线"

4.1 为什么ETH中断不能设成最高优先级

这句已经是老生常谈了,但还是有人踩。Cortex-M7的NVIC在H743上使用4位抢占优先级,数值范围0~15,数字越小优先级越高。FreeRTOS移植时通常把configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY设为5,意思是从中断里只能调用优先级数值大于等于5的中断服务函数内的API。

ETH中断服务函数里会调用osSemaphoreReleaseFromISR这种FreeRTOS API,所以它的优先级数值必须小于等于5,也就是比FreeRTOS允许的临界区优先级别更低或相等。如果把它设成0~4,一旦ETH中断在FreeRTOS临界区内打断执行,再从ISR里尝试获取调度相关资源,系统就会死锁或HardFault。

CubeMX里ETH的NVIC配置我始终给抢占优先级5,子优先级0。还有个容易忽略的点:DMA相关错误中断也要单独使能,否则某种异常下DMA停摆了你连个中断都看不到,排查起来极其痛苦。

4.2 从ETH中断到tcpip_thread的数据流

搞清楚一条数据从网线进来之后怎么流动,是后期排错的关键。H743的ETH收到一帧后,会通过DMA把报文存入RAM中的接收描述符,然后产生ETH中断。中断服务函数里,HAL库会调用HAL_ETH_RxCpltCallback这个回调,CubeMX在这个回调里释放一个二值信号量RxPktSemaphore。

底层网络线程等在信号量上,一旦拿到就调用tcpip_input(p, &g_eth)把报文喂给LWIP协议栈。协议栈再根据协议头决定是给UDP、TCP还是ICMP去处理,最终数据才会进入你业务线程的Socket里。

理解了这条链路,你排错就有了地图:中断产生了吗?信号量释放了吗?tcpip_input被调用了吗?TCP/IP层回包了吗?每层都有一个明确的判断点。

4.3 任务堆栈给多少才够用

BeCheap的做法是每个线程都给2048字节栈空间。LWIP的tcpip_thread如果配置支持大量TCP连接和较大MSS,栈需求会明显上涨。CubeMX默认给tcpip_thread 1024字节栈,实测跑TCP下载时容易溢出。

建议直接改成2048或4096字节,反正H743 RAM多。还有一个非常实用的办法:开启FreeRTOS的栈溢出检测功能,在FreeRTOS配置里将configCHECK_FOR_STACK_OVERFLOW设为2,然后实现vApplicationStackOverflowHook,在断言处用LED或串口打印提示。这样栈爆了能第一时间发现,而不是系统行为诡异时无从下手。

5. 排错全过程:三个典型故障的排查链路

5.1 故障A:SMI读不到PHY,连ID都读不出来

这是最绝望的故障,代码烧进去,初始化函数没报错,但PHY ID读出来全是0xFF或者0x00。排查链路按顺序走:

  1. 检查PHY地址。先把PHY地址在0x00和0x01之间切换,重新编译烧录,仍读不到继续下一步。
  2. 用示波器或逻辑分析仪抓MDC和MDIO引脚的信号。如果MDC完全没有波形,说明HAL以太网初始化时SMI分频有问题或GPIO复用没生效;如果有波形但MDIO一直是高(没有翻转),说明PHY没有应答,也就是没上电或复位被拉低了。
  3. 检查LAN8720的NRST引脚。很多模块设计里这个引脚直接连MCU的某个GPIO,或者通过RC电路实现上电延迟复位。如果你的板子刚好用一个GPIO控制NRST,但GPIO默认输出低,PHY就永远被按在复位状态,SMI自然读不到任何东西。

我遇到过一只板子的LAN8720电源脚到地只有1uF电容,上电瞬间纹波极大导致芯片没起来,加大到10uF后一切正常。硬件问题优先怀疑电源和复位,这个方向永远不会错。

5.2 故障B:PHY能读到ID,但link状态始终down

SMI通了,芯片ID也对,但PHY的状态寄存器里link bit始终为0,网口灯也不亮。这种情况先排除软件问题:确认你的网线设备端是通迅的(电脑插上去能识别)。然后把PC网卡强制成100M全双工,看link能不能上来,能上来说明LAN8720自适应功能可能被关掉了。

软件上真正常见的坑是RMII时钟没配对。此时PHY芯片虽然活着,但它没有稳定的50MHz REF_CLK参考时钟,内部收发路径起不来,link状态就永远不对。用示波器量PA1引脚,确认是不是有50MHz方波。如果没有,回头检查你的时钟树配置或硬件晶振焊接。

另一个因素是LAN8720的PHY地址寄存器里有一个专门控制link状态的测试位,有个别代码调试PHY时误把寄存器改乱了,重新上电后才能恢复。

5.3 故障C:能PING通但丢包,越来越严重

这属于"能用但不舒服"的情况。PING几十个包丢一两个,压力测试直接超时。最常见的原因是DMA描述符或PBUF池耗尽。接收方向处理不过来,新到的报文被丢弃。

优先检查MEM_SIZE和PBUF_POOL_SIZE是否过小。另外,ETH中断服务函数里如果处理时间过长,也会导致DMA描述符来不及回收。可以试试把ETH中断优先级从5调到6(数值越大优先级越低),反而可能改善——因为高优先级中断频繁打断tcpip_thread,反而拖慢了协议栈处理速度。

还有一次我遇到的丢包是电源噪声引起的:LAN8720的3.3V和H743的3.3V之间缺少磁珠隔离,板子电机一转就疯狂丢包。这种问题纯属硬件,但软件上可以通过观察PHY状态寄存器里的"接收错误计数"来佐证——那个字段疯狂跳说明物理层就有问题。

6. 实测数据与后续扩展

6.1 我这边测出的性能数字

这套配置稳定下来之后,我做了一轮简单压力测试。PC端用iperf往开发板灌TCP数据,测试环境是H743跑480MHz,LAN8720模块,CAT5e网线直连电脑,静态IP,FreeRTOS+LWIP 2.1.2,实测结果如下:

测试项结果
Ping延迟(ping 192.168.1.10,1000个包)平均0.8ms,无丢包
TCP下行吞吐(iperf 60秒)稳定在92Mbps左右
UDP接收(1000字节包,并发速率)约80Mbps开始偶发丢包
持续运行时间72小时无断链,无死机

这个数据在百兆以太网里已经相当能打,性能瓶颈基本在协议栈和PHY,而不是H743的MAC。如果你的测试数据远低于这个,优先怀疑内存池配太小或者中断优先级配得不合理。

6.2 几个越用越顺手的技巧

说几个我用下来觉得非常值的小技巧。第一,调试时在LWIP的ethernetif.c里加入PHY状态打印,每次link状态变化时通过串口输出,能帮你快速定位网线插拔时的问题。第二,开启LWIP的LWIP_STATS宏,在调试阶段观察lwip_stats里的丢包计数和内存使用率,比猜靠谱得多。第三,如果你要上生产环境,记得把LWIP_DEBUG关掉,否则大量调试输出会挤占CPU,直接影响吞吐。

还有一个经验是保存一份"最小可用工程"。每次换芯片型号、换PHY时,在这个基础上改,而不是从零新建工程。CubeMX虽然方便,但每次配置不同中间件版本都会带来一些隐藏差异,拿着一份验证过的工程做底子,能省去大量重复排错时间。

现在这套H743+LAN8720的模板已经成了我们团队做以太网项目的标准起点。你把这篇文章里的配置思路走一遍,大概率也能从"PING不通"顺利走到"稳定跑一周"。如果还有更刁钻的故障,建议先查PHY寄存器、先量时钟信号,九成问题都出在这两者上。

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

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

立即咨询