做物联网网关、设备监控或者板间通信,总避不开“STM32H750VBT6 + 以太网”这套玩法。前阵子我替客户做一块数据采集板,MCU选型锁定了STM32H750VBT6,网络接口自然就想到LAN8720A这颗经典RMII接口PHY,协议栈用LWIP,系统跑FreeRTOS,开发环境则用ST官方的CubeIDE。整套组合从CubeMX图形化配置到最终稳定跑TCP通信,前后折腾了两天。这篇东西就是把我实际踩过的坑和验证过的配置一次说清楚,适合正在从零开始搭H7网络工程的开发者,也适合遇到ping不通、PHY读不到ID、跑一会儿就死机之类问题的朋友。
项目本身不复杂:H750的主频跑到480MHz,硬件自带以太网MAC,外部挂一颗LAN8720A做物理层收发,再通过带网络变压器的RJ45座连到交换机,软件侧用FreeRTOS调度LWIP协议栈,最后在应用层起一个TCP服务,用于接收上位机下发的指令。下面把硬件选型、Cube配置、代码逻辑和排查经验全部拆开讲,内容足够“保姆级”,但每一步我会解释为什么要这么选、这么配。
1. 项目背景与方案选型思路
1.1 为什么是STM32H750VBT6
STM32H750VBT6这颗片子,本质是H743的“青春版”,但它的性能一点都不青春:Cortex-M7内核,主频最高480MHz,带双精度FPU、32KB I-Cache和32KB D-Cache,内置硬件以太网MAC,支持RMII和MII两种接口。100脚LQFP封装,板级布线压力小。最关键的是这颗芯片的价格比同系列的H743便宜不少,做成本敏感的网关类产品很有吸引力。
不过它有一个很“坑”的短板:内置Flash只有128KB。官方手册上写的是2MB,但那是通过QSPI外接Flash映射出来的,不是片上真Flash。很多第一次用H750的人,会把编译器的链接脚本改成2MB,结果量产时固件一跑就崩。128KB空间跑一个FreeRTOS + LWIP的工程,只要设置合理是足够的,但你必须知道怎么省Flash,后面专门讲。
选H750还有一个原因:它的以太网MAC自带DMA,收发数据不占用CPU太多时间,配合LAN8720A这种带RMII接口的PHY,硬件连接只需要十来根线,非常适合做小型协议转换器、数据采集网关。
1.2 LAN8720A这颗PHY的江湖地位
说实话,10/100M以太网PHY芯片可选的不少,比如LAN8710A、LAN8742、DP83848,但LAN8720A在国产开发板上的出镜率最高,原因很朴素:便宜、外围电路简单、资料多、不容易买假货。它支持RMII接口,时钟方案灵活,可以用25MHz晶振或外部50MHz时钟,内带1.2V稳压器,3.3V单电源供电即可。
从功能上看,LAN8720A支持10M/100M自适应,支持自动协商、极性检测和纠正、MDI/MDIX自动翻转。10M半双工、100M全双工这些常规模式全部覆盖。对于绝大多数嵌入式网络应用,比如TCP上传传感器数据、UDP广播、HTTP协议交互,这颗芯片完全够用,而且多年量产验证下来稳定性也不错。
我个人的习惯是,选PHY一定要看开发板的成熟方案。LAN8720A的参考电路我已经抄过好几轮,基本没出过问题,这也说明它的容错度比较高。
1.3 FreeRTOS + LWIP的组合逻辑
为什么整体方案一定要引入FreeRTOS和LWIP?先说FreeRTOS。H750裸机跑也不是不行,但一旦网络协议栈需要处理重传、超时、socket连接管理,裸机代码会越来越难维护。FreeRTOS提供了任务调度、信号量、消息队列这些基本设施,可以把“网络协议栈”和“业务逻辑”彻底隔离开。
LWIP则是开源的轻量级TCP/IP协议栈,专门为嵌入式系统设计。它不需要依赖Linux内核,可以在裸机或RTOS上运行,支持TCP、UDP、ICMP、DHCP、HTTP等常见协议。LWIP为了适配FreeRTOS,提供了sys层适配接口,比如创建线程、使用信号量、邮箱等,CubeMX生成代码时会自动把这些组合起来。
用一句话总结这套方案的逻辑:FreeRTOS负责“何时跑”,LWIP负责“怎么收发”,H750的ETH外设负责“数据进出”,LAN8720A负责“物理介质上的电信号”。四层配合,各管一段。
1.4 数据流的直观理解
为了后面调试方便,我在动手前会把数据流画在纸上:
应用层任务调用netconn_write → LWIP协议栈封装成TCP报文 → 通过ETH DMA描述符送入MAC → MAC通过RMII接口把数据交给LAN8720A → PHY编码后从RJ45送出去。接收方向完全相反:LAN8720A收到网络信号 → RMII接口给MAC → DMA搬运到内存Ring buffer → 协议栈解析 → 应用层任务通过netconn_recv读到数据。
记住这条链路,后面排查问题时只需要逐段定位。硬件问题多半出在“RJ45到PHY”这一段,软件问题多半出在“MAC到内存”这一段。
2. 硬件准备与关键电路检查
2.1 物料清单和实物连接
动手之前,先把清单列清楚:
- STM32H750VBT6核心板或自绘PCB板(本文以100脚封装为例)
- LAN8720A模块,或者自己贴片LAN8720A + RJ45带网络变压器
- ST-Link或J-Link调试器一个
- USB转TTL串口模块,方便输出调试日志
- 网线、路由器或交换机,电脑一台
- 12V或5V电源给板子供电
我这次用的是自己画的最小系统板,PHY部分直接参考LAN8720A官方数据手册的参考电路,网络变压器集成在RJ45座子内部,买的是HR911105A这颗座子。如果你用现成模块,接线就按模块丝印来,注意事项少很多。
2.2 LAN8720A参考电路的几个关键点
LAN8720A的参考电路不算复杂,但有几个点必须抠细节:
第一,时钟方案。RMII接口要求50MHz参考时钟。最稳妥的接法是:LAN8720A的XTAL1和XTAL2之间接一个25MHz无源晶振,然后把LAN8720A的CLKOUT引脚接到STM32的ETH_RMII_REF_CLK引脚。因为LAN8720A内部有PLL,会把25MHz倍频到50MHz,从CLKOUT输出给MCU。这种接法PHY自己产生时钟,MCU这边只做输入,时序最安全。
第二,PHY地址配置。LAN8720A的PHY地址由PHYAD0引脚决定:接地为0x0,接上拉为0x1。大多数模块默认接地,所以SMI访问的PHY地址是0。如果读到ID全0xFFFF,先检查这个引脚的焊接和电平。
第三,复位引脚。LAN8720A的nRST是低有效复位,一般由STM32的GPIO控制。复位信号至少保持低电平25us再拉高,否则PHY可能启动异常。我见过有人偷懒直接接RC复位电路,但最好还是用IO控制,这样软件可以随时硬复位PHY。
第四,LED指示灯。LAN8720A的LED0和LED1引脚可以做状态指示,最常见配置是LED0表示Link状态、LED1表示活动状态。调试时这两个灯非常有用——网线插上后Link灯必须亮,如果灯不亮,后面谈什么都是空的。
下面是STM32H750VBT6和LAN8720A之间的RMII信号连线表,确认接线前先对着查一遍:
| 功能信号 | STM32引脚 | LAN8720A引脚 | 说明 |
|---|---|---|---|
| REF_CLK | PA1 | CLKOUT | 50MHz参考时钟,由PHY输出给MAC |
| TXD0 | PG13 | TXD0 | 发送数据位0 |
| TXD1 | PG14 | TXD1 | 发送数据位1 |
| TX_EN | PG11 | TX_EN | 发送使能 |
| RXD0 | PC4 | RXD0 | 接收数据位0 |
| RXD1 | PC5 | RXD1 | 接收数据位1 |
| CRS_DV | PA7 | CRS_DV | 载波侦听/数据有效 |
| MDC | PC1 | MDC | 管理接口时钟 |
| MDIO | PA2 | MDIO | 管理接口数据 |
2.3 电源和网络变压器注意事项
LAN8720A的供电是3.3V,内部稳压输出1.2V供核心逻辑。要注意它的VDDCR引脚上需要接一个1uF的去耦电容,放置要尽量靠近引脚。PHY对电源纹波比较敏感,如果电源不干净,会出现随机断链、数据丢包这类很难查的问题。
网络变压器的作用是隔离共模信号和静电,它内部其实有一个中心抽头,中心抽头通过RC接地还是接电源,不同厂家的模块设计有差异。如果你买的是带变压器的RJ45一体座,不用操心这个;如果是分离式变压器,按数据手册接。我吃过一次亏:分离式网络变压器中心抽头悬空,导致link指示灯不亮,查了半天。
另外提醒一句,RJ45的差分线从PHY到变压器之间要等长、靠近,长度差控制在5mm以内。PCB空间太紧张时,至少保证REF_CLK这根线尽量短,因为它跑50MHz,串扰和反射都会造成间歇性通信异常。
2.4 硬件上电后的第一件事
连线完成后,不要急着写代码。先用示波器或万用表确认三件事:
- 3.3V供电是否正常,电流是否在100mA量级
- PHY的25MHz晶振是否起振,如果是用示波器看,CLKOUT引脚应能看到50MHz方波
- 网线插上交换机后,H750板子上如果接了LED,Link灯是否点亮
我调试时最常用的工具是一根带指示灯的通断测试网线,几块钱一根,能快速排除线序问题。Link灯不亮,先换网线、换交换机端口,再考虑配置问题。Link灯亮,说明PHY物理层已经通了,接下来才是软件部分。
3. CubeIDE + CubeMX 网络工程配置实战
3.1 新建工程与时钟树配置
打开CubeIDE,在文件菜单里新建STM32项目,芯片型号输入STM32H750VBT6。需要注意的是,CubeMX可能默认让你选H750VBT6的封装和Flash配置,没问题,直接确认。
进入图形化配置界面之后,先改时钟树。我用的外部晶振是25MHz,在HSE处填入25,然后在Clock Configuration里设置PLL源为HSE,让SYSCLK跑到480MHz。CubeMX会自动算出AHB总线240MHz、APB1和APB2各120MHz。这里要留意ETH外设的时钟源,不同的CubeMX版本界面略有差异,但基本都能在图里看到“Ethernet”相关时钟选项,确保它为50MHz即可。
如果不会调PLL,可以直接选CubeMX左上角的“Clock”小工具,它会根据你选的外部晶振频率自动生成一套合法配置。个人建议还是自己手动调一遍,因为后面如果改PHY时钟方案,你至少知道这个50MHz是从哪来的。
3.2 配置ETH外设
在Categories里面找到Connectivity → ETH,勾选ETH激活。在Mode选项卡里,把Interface选为RMII。这样CubeMX会自动分配RMII所需的引脚,你只需要核对PA1、PA2、PA7、PC1、PC4、PC5、PG11、PG13、PG14这些引脚是不是都对上了。
接下来的关键项是LAN8720A的PHY地址。CubeMX的ETH配置里有一个Advanced Parameters,里面有PHY Address字段,默认可能是0。LAN8720A如果PHYAD0接地就是0,那就保持默认;如果接高电平,要改成1。这一步不填对,后面读PHY ID就会失败。
还有一项是PHY寄存器的定义。CubeMX生成代码时会包含lwip相关的PHY驱动,但它默认支持的可能是LAN8742,和LAN8720A寄存器大体兼容,但有些细节差异。我建议在生成代码后去stm32h7xx_hal_conf.h或者eth.c里检查一下PHY地址宏,确保用的是同一个地址。
ETH的DMA参数一般保持默认,但要注意把DMA描述符数量、接收缓冲区大小这些项目留够内存余量,后面如果跑大数据量TCP,默认配置可能不够。
3.3 配置LWIP中间件
在Middleware里找到LWIP,勾选Enabled。这里有几个要点:
- 协议栈版本,CubeMX一般给你LwIP 2.1.2或2.2.x,我用的是2.1.2,稳定就好。
- API模式,选择“Netconn API”而不是“Raw API”。理由很简单:我们在FreeRTOS上开发,Netconn API是阻塞式、线程安全的,写业务代码更像传统socket,逻辑清晰。Raw API虽然性能更高,但回调满天飞,维护成本高。
- IP版本选IPv4,然后可以开启DHCP。我建议前期调试用静态IP,比如192.168.1.100,子网掩码255.255.255.0,网关192.168.1.1。等确认通了再切DHCP,否则你连设备IP都不知道。
在Memory Settings里,有几个参数是按需调大的:
- MEM_SIZE:堆内存池大小,默认上千字节,如果TCP窗口大可以适当调大。
- PBUF_POOL_SIZE:网络包缓冲池数量,我一般改成8个以上,否则并发稍多就丢包。
- TCP_WND、TCP_SND_BUF:TCP接收窗口和发送缓冲区,要和协议交互的数据量匹配。我之前测试大数据传输,把这两个提到4096以上才顺畅。
不要一上来把每个参数都改大,MCU内存有限,够用就行。LWIP内存太小会一直报Out of memory,太大又会撞上H750的RAM上限,后面会专门讲内存平衡。
3.4 配置FreeRTOS
在Middleware里选择FreeRTOS,勾选Enabled。CMSIS版本选V2,它是ST官方推荐的API接口,你如果用CubeMX的osThreadNew、osMessageQueue这些函数,就需要V2。
在Tasks里可以看到系统默认创建的defaultTask,它的堆栈大小默认可能是128 words,但我们后面要跑TCP任务,这个堆栈明显偏小,建议改成1024甚至2048 words。别心疼RAM,H750有充足的内部SRAM,但如果堆栈设太小,一旦递归或深层调用触发溢出,RTOS会跑飞,而且十分难查。
FreeRTOS的配置里还有一项:USE_DAEMON_TASK,如果后面要用软件定时器才需要开。网络应用如果用不上,保持默认即可。
FreeRTOS和LWIP的配合有一个优先级问题:LWIP的核心线程tcpip_thread负责协议栈处理,日志或业务任务的优先级如果比它高,可能会长时间抢占CPU,导致协议栈超时重传。建议CMD虚拟任务优先级不要设太高,我用的是osPriorityNormal。
3.5 生成代码后必须手动检查的几处
CubeMX生成的代码不是开箱即用的,有几个地方必须在烧录前检查:
第一,检查main.c里的MX_ETH_Init函数,确认PHY地址和你硬件实际一致。读一下类似#define LAN8720A_PHY_ADDRESS 0x00的宏。
第二,检查LWIP初始化时,如果用的是Netconn API模式,CubeMX会自动创建tcpip_thread,不要自己在main函数里重复调用lwip_init。看main.c中MX_LWIP_Init()的调用顺序,它应该放在外设初始化之后、启动RTOS调度器之前。
第三,MPU配置。这是H750和H743最容易出问题的环节。H7有D-Cache,如果数据被DMA写进内存,CPU读到的可能是缓存里的旧数据,反之CPU写数据但DMA读到的可能是内存里的旧数据。CubeMX生成代码里通常会有一个MX_MPU_Config函数,把ETH的DMA描述符和缓冲区设置成Non-cacheable。如果你的CubeMX版本没自动生成,你必须在主函数最开始调用MPU_Config,并且给以太网缓冲区所在RAM区域配置Non-cacheable属性。我第一次就是这个没配好,现象是ping偶尔通、数据随机错位,排查了整整半天。
4. 应用层任务代码实现
4.1 初始化顺序的合理安排
main函数里,外设初始化顺序按CubeMX生成就行,但有几个原则:
- ETH、LWIP要晚于引脚和时钟初始化,但早于RTOS调度器启动。
- FreeRTOS任务创建要在调度器开始前完成,但TCP Server任务里的具体逻辑等调度器跑起来再执行也没问题。
- 日志串口初始化尽量放前,方便从程序一启动就输出信息。
我的main函数大致结构是:
- HAL_Init()
- MPU_Config()
- SystemClock_Config()
- MX_GPIO_Init()
- MX_ETH_Init()
- MX_LWIP_Init()
- 创建TCP Server任务
- osKernelStart()
注意,MX_LWIP_Init里其实会启动tcpip_thread,这个线程不是业务线程,它是LWIP内部处理协议栈消息的。你真正要做的,是再创建一个自己的业务线程,比如tcp_server_task,专门负责监听和收发。
4.2 TCP Server任务,用Netconn API写起来像socket
下面直接给一个可在H750上跑的TCP Server任务示例,监听8080端口,收到数据后原样回发。这个任务用Netconn API,代码不长,但你可以照着扩展成自己的协议解析逻辑:
void tcp_server_task(void *argument) { struct netconn *conn, *newconn; struct netbuf *buf; err_t err; char rx_buffer[256]; conn = netconn_new(NETCONN_TCP); if (conn == NULL) { for(;;) { vTaskDelay(pdMS_TO_TICKS(1000)); } } netconn_bind(conn, IP_ADDR_ANY, 8080); netconn_listen(conn); for (;;) { err = netconn_accept(conn, &newconn); if (err != ERR_OK) { vTaskDelay(pdMS_TO_TICKS(10)); continue; } while ((err = netconn_recv(newconn, &buf)) == ERR_OK) { do { u16_t len = buf->p->len; if (len > sizeof(rx_buffer)) { len = sizeof(rx_buffer); } memcpy(rx_buffer, buf->p->payload, len); rx_buffer[len] = '\0'; /* 这里做业务解析 */ netconn_write(newconn, rx_buffer, len, NETCONN_COPY); } while (netbuf_next(buf) >= 0); netbuf_delete(buf); } netconn_close(newconn); netconn_delete(newconn); } }这里有几个容易踩的细节:
- netconn_recv是阻塞函数,如果连接断开会返回非ERR_OK,跳出内层循环。
- buf->p是pbuf结构,payload指向实际数据,len是当前pbuf长度。如果数据跨多个pbuf,需要用netbuf_next遍历,不能只看第一个。
- netconn_write最后一个参数选NETCONN_COPY,表示数据会拷贝到内部缓冲区再发送,这样你在调用后马上修改rx_buffer也没问题。代价是多一次内存拷贝,但代码安全性高很多。
4.3 数据收发机制与内存拷贝策略
Netconn API的数据流向是:内核把收到的数据放在pbuf链里,netconn_recv拿到的是netbuf。你的应用层需要尽快把数据拷贝走,然后调用netbuf_delete释放。如果你不拷贝直接拿指针长时间保存,很容易导致缓冲区被内核复用,数据被覆盖。
发送数据时也同理,使用NETCONN_COPY模式最安全,适合小数据量场景。如果你要发送大块数据、对性能有要求,可以用NETCONN_NOCOPY,但必须保证发送期间数据缓冲区不被修改。我的建议是前期调试一律用COPY,跑通了再根据性能测试决定是否优化。
TCP是流协议,数据到达边界可能不是整包。别看上面代码简单,实际业务中你可能需要自己做分包和粘包处理。最常用办法是自定义协议头,比如前4字节表示长度,收到数据后先积累到环形缓冲区,长度够了再解析。
4.4 再加一个UDP广播任务做设备发现
很多网关设备需要被上位机自动发现,这里再附加一个UDP广播的示例,简单实用:
void udp_broadcast_task(void *argument) { struct netconn *udpconn; struct netbuf *ubuf; char msg[] = "H750-Gateway-Online"; udpconn = netconn_new(NETCONN_UDP); if (udpconn != NULL) { netconn_bind(udpconn, IP_ADDR_ANY, 8081); } for (;;) { ubuf = netbuf_new(); if (ubuf != NULL) { u32_t dst = ipaddr_addr("192.168.1.255"); netbuf_ref(ubuf, msg, strlen(msg)); netconn_sendto(udpconn, ubuf, (const ip_addr_t *)&dst, 8082); netbuf_delete(ubuf); } vTaskDelay(pdMS_TO_TICKS(5000)); } }UDP广播的地址要跟你的网段匹配,这里写的是192.168.1.255,表示向192.168.1.x整个子网广播。上位机在8082端口监听,就能收到设备上线信息。这个功能很适合做设备自动发现,推荐加上。
4.5 调试日志怎么打
网络工程调试时,日志是救命稻草。我会在关键位置打印三个级别的日志:信息、错误、警告。用串口输出最省事,波特率115200,串口引脚根据你的板子来定。
比如在TCP Server任务里,连接建立和断开时打印“accept new connection from 192.168.1.x”和“connection closed”。LWIP本身有较完整的日志系统,你可以打开LWIP_DEBUG,但默认全开会刷屏,调试完一定要关掉,否则极其拖慢性能和Flash占用。
5. 编译优化与烧录调试
5.1 128KB Flash怎么够用
STM32H750VBT6最需要动脑筋的就是128KB片上Flash。好在CubeIDE默认使用GCC,编译参数改成-Os优化大小后,一个完整的FreeRTOS + LWIP工程能压到90KB左右,甚至更低。这是我实测比较稳定的方案。
在项目属性里找到C/C++ Build → Settings → Tool Settings → MCU GCC Compiler → Optimization,把Optimization Level改成Optimize for size(-Os)。同时把Linker里的Optimization也打开。
另外,避免使用printf的完整版。CubeIDE默认可能把printf重定向到半主机模式,这会引入不小的库代码。建议用轻量级printf,或者直接用串口中断发送字符串的函数。如果不需要浮点格式化输出,就别用%f。
一个实用的诀窍是:查看map文件,看哪些.o文件占的Flash最多。有时你会发现只是包含了一个未使用的功能,就吃掉好几KB。
5.2 内存布局与任务栈大小
H750VBT6的RAM其实不小,有DTCM、AXI SRAM、SRAM1/2/3等好几块,但CubeMX默认的内存分配不一定会均衡使用。编译时会看到类似“region RAM overflowed”的错误,那就是内存不够了。
解决思路有三个:
- 把LWIP的内存池适度调小,比如PBUF_POOL_SIZE从默认的5改成4,MEM_SIZE减小。
- 优化任务栈大小:TCP Server任务我用1024 words,UDP广播任务512 words,defaultTask保持128 words。不要把每个任务都设成2048,内存再大也经不起浪费。
- 检查是否有巨型全局数组。如果接收缓冲区定义成1KB×4,不会占太多,但你要是开了大图片、大音频缓存,那就另说。
我一般会定期查看编译输出里的RAM占用百分比,保持70%以下比较安全,给堆栈的峰值留余量。
5.3 烧录方式与首次运行判断
H750可以用ST-Link通过SWD接口烧录。CubeIDE的Debug配置里选STM32 Cortex-M C/C++ Application,连接ST-Link后直接下载。首次烧录成功后,代码会停留在main函数入口。
首次烧录后建议先跑一个最简单的测试:板子连上网线,启动后串口日志打印“LWIP init OK”,然后打印当前IP地址。如果能看到IP,说明PHY和协议栈都已经正常起来了。
我常加一行代码,在MX_LWIP_Init()后延时2秒,然后打印当前网卡IP:
extern struct netif gnetif; printf("IP address: %s\n", ipaddr_ntoa((const ip_addr_t *)&gnetif.ip_addr));连上路由器时这里应该显示你配置的静态IP,或者是DHCP分配到的地址。如果这里打印的IP是0.0.0.0,说明DHCP还没成功或者网线没接好。
5.4 跑通Ping的成就感
配置好之后,先别急着写高级应用。拿电脑ping一下设备,这是最直观的验证。
我的测试步骤是:
- 将设备IP设为192.168.1.100,电脑IP设为192.168.1.150,直连或接交换机都行。
- 在电脑命令行执行ping 192.168.1.100 -t,观察响应。
- 正常情况应看到连续“来自192.168.1.100的回复: 时间=1ms”。
第一次ping通的时候,状态栏会显示“Reply from 192.168.1.100: bytes=32 time=1ms TTL=64”,那一刻就说明H750的MAC、PHY、LWIP协议栈、FreeRTOS调度已经整条链路全部跑通了。这是整个项目最关键的里程碑。
6. 常见问题与避坑速查
6.1 读不到PHY ID
这是整个调试过程中概率最高、最容易打击信心的问题。现象是在CubeIDE调试器里看到HAL_ETH_ReadPHYRegister返回错误,读出来的寄存器值全是0xFFFF或者0x0000。
排查顺序:
- 确认PHY地址。LAN8720A的SMI地址是PHYAD0引脚电平决定的,再次核对你的宏定义和硬件连接。
- 确认MDIO和MDC引脚。在H750上,MDIO是PA2,MDC是PC1,引脚冲突或者被其它外设占用,就读不到。
- 确认复位引脚已经拉高且维持足够时间。我常用的办法是给复位引脚一个100ms的低电平再拉高,然后在读PHY ID前加10ms延时。
- 确认REF_CLK时钟已经就绪。如果没有50MHz时钟,PHY内部逻辑跑不起来,SMI接口也不会响应。
我见过最隐蔽的一个坑是:有些LAN8720A模块上PHYAD0引脚默认接了上拉,但你以为是接地,结果实际地址是1而不是0。所以不要再猜,用示波器或万用表实测PHYAD0引脚电平。
6.2 link指示灯不亮
Link灯不亮,说明PHY没有物理连接,这时候软件再怎么调也没用。按下面顺序排查:
- 先用一根确定没问题的网线,把板子和路由器直接相连,不要过墙壁面板。
- 观察RJ45座的另一端,如果网络变压器的变压器中心抽头有虚焊,Link灯也不会亮。
- 用示波器量PHY的CLKOUT引脚,确认是否输出50MHz。如果这引脚没有时钟,PHY可能还在复位状态或电源没起来。
- 检查PHY供电引脚电压,尤其是1.2V内部稳压输出是否正常。
还有一种情况是:Link灯亮了,但不是闪烁,而是常亮或者常灭。LAN8720A的LED配置由寄存器控制,默认配置下LED0和LED1的含义可以通过读寄存器得到。如果灯接反了引脚,Link状态可能被接到LED1上,视觉效果会不一样。
6.3 ping不通,但link灯亮
到了这一步,硬件物理层没问题,问题多半出在软件或协议栈配置。
- IP地址冲突:确认设备IP和电脑IP不在同一网段,或者设备IP和路由器地址冲突。
- 静态IP的网关设置不对:如果只是同一局域网内ping,网关其实无所谓,但如果你跨网段,必须设置正确网关。
- 配置了DHCP但路由器没开DHCP:设备一直拿不到IP,所以ping 192.168.1.100自然不通。这个情况建议用静态IP调试。
- 电脑防火墙拦截了ICMP应答:有时候设备其实已经通了,但电脑ping不到,因为有防火墙。可以在Windows防火墙里临时关闭ICMP回显。
- LWIP的代码使用了太长定时器导致ARP超时:这个比较少见,如果多次ping只通一次,多半是ARP缓存老化,可以尝试减少LWIP的ARP_TMR_INTERVAL。
还有一种很隐蔽的情况:如果你用的是一个老版本LAN8720A,它的自动协商可能出现兼容性问题。建议在PHY初始化时强制配置成100M全双工,排除自动协商的问题。
6.4 系统卡死和Cache一致性
H7上跑FreeRTOS+LWIP,卡死的头号嫌疑是Cache一致性。这是H750特有的大坑。当你使能了D-Cache,但MPU没有把ETH DMA内存区域配置为Non-cacheable时,数据不一致会造成:
- 接收时,DMA把数据写进内存,但CPU读到的是缓存里的旧数据,导致收到的报文错乱。
- 发送时,CPU把数据写进内存,但DMA可能读取到旧的内存数据,导致发出去的是垃圾包。
解决办法有两个:
- 在MX_MPU_Config里把ETH DMA描述符和缓冲池所在RAM区域配置为Non-cacheable。CubeMX通常会自动生成,但你一定要确认它生效。
- 如果你不想改MPU,可以在每次收发前后调用SCB_CleanDCache和SCB_InvalidateDCache,但这样性能会打折,而且容易遗漏某个路径,所以我强烈建议用MPU方案。
卡死还有一个常见原因:ETH中断优先级设置不当。FreeRTOS要求中断服务函数中调用FromISR系列API时,中断优先级不能高于configMAX_SYSCALL_INTERRUPT_PRIORITY。如果ETH中断优先级设得太高,网络中断一来FreeRTOS就懵了。解决办法是在NVIC设置里把ETH中断优先级调低一点,我的做法是设为5或6(数字越大优先级越低)。
6.5 常见问题速查表
| 现象 | 优先检查项 | 进阶排查 |
|---|---|---|
| 读PHY ID返回0xFFFF | PHY地址、MDIO/MDC接线 | 复位时序、REF_CLK |
| Link灯不亮 | 网线、交换机端口、网络变压器 | PHY供电、CLKOUT输出 |
| Link灯亮但ping不通 | IP地址冲突、DHCP设置 | 电脑防火墙、ARP表 |
| ping通但数据传输错乱 | MPU/D-Cache配置 | DMA描述符数量、PBUF大小 |
| 跑一段时间后卡死 | ETH中断优先级 | 堆栈溢出检测、内存泄漏 |
| 编译报RAM溢出 | 减小LWIP内存池 | 调整任务栈大小、检查全局数组 |
最后再分享一个个人习惯
这篇文章写到这里,核心配置和坑都讲得差不多了。我最后还想强调一点:做H750的网络工程,不要一上来就追求跑满带宽,先把Ping通了、TCP回显通了,然后再逐步加业务。这个过程就像拼图,先把硬件拼好、把协议栈拼好、把任务调度拼好,任何一个环节出了问题,都能准确定位。我自己在H750上做的那个数据采集网关,用的就是这套FreeRTOS+LWIP框架,现在已经在现场连续运行了大半年,中途只因为换网线重启过一次。这台设备稳定运行给我的信心是:这套方案的底层框架是成熟的,剩下的就看你怎么实现业务逻辑。如果你现在正在调试LAN8720A遇到问题,建议先回去把PHY ID读出来,这一步都过不去的话,后面的网络通信全都是空中楼阁。