做了好几个采用STM32F407加LAN8720跑以太网的项目之后,我发现自己每次重新搭工程,还是会在同样的几个坑里打转。F407的MAC外设本身不难,LAN8720这颗PHY也便宜、大碗、开发板用得最多,但问题往往出在CubeMX配置、PHY地址、时钟来源、中断优先级这些“看起来无关紧要”的地方。这篇文章就把我从CubeMX 6.4建工程开始,到LWIP在FreeRTOS上跑起来、最终Ping通的全过程做一个完整复盘,把最容易踩的坑提前标出来,给准备用这套组合做项目的朋友一个可以直接抄的作业路径。
如果你正在做MQTT网关、工业数据采集、OTA升级、板间通信这类需要以太网能力的嵌入式设备,而且主控恰好是F407,这篇文章应该能帮你省下至少一整天的排查时间。文章里涉及到的PHY地址修正、RMII时钟来源判断、LWIP内存参数调整、FreeRTOS中断优先级配合这几个关键点,都是我在实际调试中真正卡过的地方,我会把现象、原因、解决办法都写清楚,尽量让不同基础的读者都能照着做出来。
1. 项目定位与整体方案拆解
1.1 为什么选STM32F407加LAN8720这套组合
STM32F407是ST在Cortex-M4系列里的常青树,主频168MHz,内置10/100M以太网MAC,支持MII和RMII两种接口。以太网MAC负责数据链路层的帧收发,但它不处理物理层的编码、电平转换、时钟恢复这些事情,所以必须外接一颗PHY芯片。LAN8720是Microchip(原SMSC)家族的10/100M低功耗PHY,价格便宜、外围电路简单、封装也有QFN的小体积版本,在国产开发板和工业板卡上出镜率非常高。
F407自带MAC加上一颗LAN8720,是很多以太网产品的基础方案。相比使用STM32F429或F767,F407的优势是够用且成本可控;相比串口转以太网模块(比如W5500),F407加LAN8720的优势是可以直接跑LWIP协议栈,灵活性更高、吞吐量也更大,不依赖专用芯片内部的协议栈实现。
这套组合适合的场景包括:设备数据采集上报、远程固件升级、多设备局域网通信、边缘网关的以太网接入。如果你需要在产品里实现一个稳定、可控、成本不高的以太网接口,F407加LAN8720基本上是最合适的起点。
1.2 RMII硬件设计到底有哪些关键点
在决定用CubeMX搭工程之前,先得把硬件接口关系理清楚。STM32F407和LAN8720之间支持MII和RMII两种模式,MII需要16根数据线外加控制线,RMII把数据线缩减到2根发送加2根接收,一共只需要9根信号线,是绝大多数板子采用的接法。
RMII模式下的一个核心要求是:参考时钟必须是50MHz。这个时钟可以由MCU这边产生,也可以由PHY这边产生,关键是两边必须保持同步。常见的板子方案有两种。一种是LAN8720的XI和XO引脚之间接一个25MHz无源晶振,PHY内部通过PLL倍频产生50MHz,然后把REF_CLK引脚作为输出,送到STM32F407的PA1(ETH_RMII_REF_CLK),此时PA1配置为输入。另一种是直接用外部50MHz有源晶振接到LAN8720的XI,同样由PHY把REF_CLK送给MCU。
还有一种不太常见但确实存在的接法:用STM32F407的PA8(MCO1)输出50MHz给PHY的REF_CLK。但这种做法的限制比较多,因为F407的MCO1实际上是从PLL主时钟分频得到,要在系统时钟168MHz的基础上精确得到50MHz并不容易,通常需要专门调整PLL参数,而且往往会影响PLL48CLK(USB和RNG需要这个时钟)。所以我个人强烈建议优先采用前两种由PHY提供参考时钟的方案,硬件设计上更省心,软件上也少一个变量。
RMII模式下固定的引脚映射是这样的:
| 信号 | MCU引脚 | 说明 |
|---|---|---|
| ETH_RMII_REF_CLK | PA1 | 50MHz参考时钟,通常由PHY输出 |
| ETH_RMII_CRS_DV | PA7 | 载波侦听/数据有效 |
| ETH_RMII_TX_EN | PB11 | 发送使能 |
| ETH_RMII_TXD0 | PB12 | 发送数据位0 |
| ETH_RMII_TXD1 | PB13 | 发送数据位1 |
| ETH_RMII_RXD0 | PC4 | 接收数据位0 |
| ETH_RMII_RXD1 | PC5 | 接收数据位1 |
| ETH_MDC | PC1 | 管理接口时钟 |
| ETH_MDIO | PA2 | 管理接口数据 |
拿到一块新板子,第一件事是翻原理图确认REF_CLK是谁给谁的,以及LAN8720的复位引脚接在哪里。很多开发板的PHY复位脚直接接在系统复位上,上电时由整个MCU的复位过程带起来,这种情况软件上不需要额外控制;但如果你板上PHY的复位脚单独接了一个GPIO,就必须在初始化代码里先拉低再拉高,让PHY完成上电复位。这个细节没处理好的话,PHY会一直处于复位状态,MDIO读写全部失败,后面怎么做都是白搭。
1.3 软件架构:LWIP和FreeRTOS到底怎么配合
LWIP是嵌入式领域最常用的开源TCP/IP协议栈,它本身有一套独立于操作系统的实现方式。在没有RTOS的环境下,LWIP靠周期调用sys_check_timeouts函数来驱动定时器,接收则通过中断或轮询方式处理。在引入FreeRTOS之后,LWIP可以有更清晰的任务模型:一个专门的接收任务等待中断信号,收到以太网帧后交给协议栈处理;另一个低优先级任务周期调用超时检查函数;应用层任务则直接使用socketAPI或RAW API。
在CubeMX生成的默认工程里,这套结构已经帮你搭好了。ethernetif.c里有一个接收线程,通过信号量等待ETH中断回调,收到信号后从DMA描述符里取数据,调用netif->input把数据交给LWIP的tcpip线程。另一个lwip线程循环调用sys_check_timeouts,负责TCP重传、ARP超时、DHCP定时等周期任务。
这个架构的好处是:LWIP的协议栈处理和应用层完全隔离,应用层任务不会被网络中断频繁打断。缺点是必须处理好中断优先级和FreeRTOS的配合,这也是后面最容易出问题的环节之一。如果ETH中断优先级设得太高,超过了FreeRTOS允许调用API的最高优先级,在中断回调里使用osSemaphoreRelease等函数时就会触发断言或进入HardFault。
2. CubeMX 6.4配置实操
2.1 时钟树:168MHz系统时钟和RMII参考时钟不能混
打开CubeMX新建工程后,第一件事是配置时钟树。STM32F407的以太网MAC使用AHB1总线时钟,所以只要系统时钟正常跑起来,MAC本身不会有大问题。关键是RMII参考时钟的50MHz来源必须和硬件接法一致。
这里我把两种情况分开讲。
如果你的板子是LAN8720自带25MHz晶振、通过REF_CLK引脚把50MHz输出给MCU,那么CubeMX时钟树里不需要做任何特殊配置,只需要正常把系统时钟配到168MHz即可。编译生成的代码里,PA1会自动被定义为复用功能ETH_RMII_REF_CLK,MCU侧接收PHY送来的50MHz时钟。
如果你的板子比较特殊,是外部有源50MHz晶振直接接到PHY的XI而不是由PHY输出REF_CLK给MCU,接线和上面是一样的,MCU的PA1仍然是输入模式。如果真的遇到非要用MCO1输出50MHz给PHY的板子,你需要在CubeMX时钟树里找到MCO1输出,选好时钟源和分频系数,确保输出精确50MHz,同时还要确保系统时钟和PLL48CLK仍然满足要求。但前面说过,这个方案不推荐,遇到这种硬件设计我只能说尽量改板子。
需要注意的是,不要看到网上有人说“PA8输出50MHz”就不加思考地照搬。正点原子和野火的F407开发板,默认都是LAN8720模块自带25MHz晶振,PHY内部倍频后由REF_CLK输出给PA1。这一点以你自己手头板子的原理图为准。
具体到时钟树参数,以最常见的8MHz外部晶振为例:PLL_M配置为4,PLL_N配置为168,PLL_P配置为2,这样VCO输出336MHz,最终系统时钟168MHz;PLL_Q配置为7,得到48MHz给PLL48CLK。如果外部晶振是25MHz,那PLL_M等于25,参数含义是一样的。用CubeMX的时钟树图形界面点几下就能自动算好,不需要手算,但一定要留意右下角有没有红色报错。
2.2 ETH外设配置:模式、PHY地址和中断优先级
在Pinout视图中搜索ETH,启用ETH外设,然后在Configuration里把接口模式选为RMII。CubeMX会自动把PA1、PA2、PA7、PB11、PB12、PB13、PC1、PC4、PC5这9个引脚设置为对应的复用功能,不需要手动逐个配置。
重点在ETH的参数配置页面。打开ETH配置后,你会看到一个PHY Address的输入框,这是整个工程里第一个大坑。CubeMX的默认值通常是1,但很多LAN8720模块的PHY地址实际上是0。PHY地址由芯片的PHYAD0引脚电平决定,LAN8720的PHYAD0引脚默认下拉为0,如果板子上没有专门接上拉电阻,那么PHY地址就是0,而不是1。
如果PHY地址不对,HAL_ETH_Init初始化时会去读取PHY的ID寄存器,读到的值不对,初始化直接返回HAL_ERROR,后面的以太网功能自然全部失效。判断PHY地址最可靠的方法是读取PHY ID寄存器,我后面在代码调试部分会写一个简单的验证函数。但既然现在在配置界面,先根据板子原理图把PHY Address填对再说。
ETH中断的优先级也要在NVIC设置里配好。由于LWIP的接收线程依赖ETH中断回调来发信号量,而这个回调在FreeRTOS环境中会调用osSemaphoreRelease,所以ETH中断优先级数值不能小于FreeRTOS配置的configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY。在STM32的优先级表达里,数值越大优先级越低,通常把ETH全局中断优先级设置为5到7是比较安全的。如果设置成0或1,表面看中断响应更快,但一旦在中断回调里调用FreeRTOS的API,轻则断言失败,重则直接HardFault。
2.3 LWIP中间件配置:内存参数和协议开关
在Middleware里启用LWIP后,需要重点关注LWIP配置页面里的几个关键参数。这个页面里的选项非常多,但不是每个都需要动,真正影响项目稳定性的主要有这几个:
MEM_SIZE是整个LWIP协议栈的堆内存大小,用于分配TCP报文段、路由表、接口数据结构等。默认的1600字节对于只跑UDP的简单应用勉强够用,但如果你要跑TCP客户端或服务器,建议调到4096以上,否则连接建立后很容易出现内存分配失败,表现为连接建立后传输停滞或直接断开。
PBUF_POOL_SIZE是数据包缓冲池的数量,默认16,建议保持16或增大到32。PBUF_POOL_BUFSIZE是每个缓冲池里单个缓冲区的大小,必须在1518以上,因为以太网最大帧长就是1518字节,太小会导致大包被丢弃。这两个参数直接影响网络吞吐量,如果项目有较大的数据发送需求,PBUF_POOL_SIZE可以适当加大。
TCP_WND、TCP_SND_BUF这两个参数决定TCP窗口大小和发送缓冲大小,默认值通常偏小。对于一般的数据上报业务,把TCP_WND设置为4倍TCP_MSS以上、TCP_SND_BUF设置为4倍TCP_MSS以上,传输性能会明显改善。如果你不跑TCP,只跑UDP,这些参数可以不动。
DNS、DHCP这类功能按需打开。DHCP适合产品调试阶段自动获取IP,省去手动配置的麻烦,但正式产品里往往需要固定IP,所以我一般建议用静态IP加DHCP备用的方案。此外,在LWIP配置页面里设置的静态IP地址、子网掩码、网关地址,会在生成的代码里直接生效,后面不需要再手动改。
2.4 FreeRTOS配置:任务优先级和堆栈
在Middleware里启用FreeRTOS时,我建议使用CMSIS-RTOS V2封装层,这样CubeMX生成的代码风格统一,后面创建任务、信号量、队列都方便。
FreeRTOS的堆大小(FreeRTOS Heap Size)建议保持在默认的15360字节以上,如果应用层任务比较多,或者TCP缓冲需求大,可以适当增加到32768甚至更大。LWIP的接收任务和tcpip线程都要从堆里分配任务控制块和栈空间,堆太小会导致任务创建失败。
任务优先级方面,CubeMX默认生成的lwip任务和eth_rx_thread任务优先级偏高,这个默认配置其实是可以用的,但你要注意自己的业务任务不要和它们抢占得太厉害。我的经验是:网络接收任务优先级略高,保证数据不丢;超时检查任务可以稍低;应用层业务任务放在网络任务之下,避免业务任务里做耗时操作时影响TCP重传和ARP超时处理。
如果应用层任务很多,建议把ETHRX线程的优先级保持在默认水平或略高,不要低于普通业务任务。LWIP线程(负责超时检查)如果优先级太低,在网络繁忙时可能出现TCP重传超时不准的问题,表现为连接异常断开。最稳妥的做法是先用默认优先级把系统跑通,再根据实际业务调整。
3. 代码生成后的必要改造与核心实现
3.1 修正PHY地址并验证PHY ID
CubeMX生成代码之后,进入eth.c文件,找到HAL_ETH_MspInit或MX_ETH_Init函数。在MX_ETH_Init里,你会看到一行类似这样的代码:
heth.Init.PhyAddress = 1;这行代码必须和你的硬件匹配。如果LAN8720的PHYAD0引脚是接地的,把这里的值改成0:
heth.Init.PhyAddress = 0;改完之后,怎么确认PHY地址是对的?在main.c里的用户代码区加一段调试代码,复位PHY后读取PHY的ID寄存器:
uint32_t phy_id1 = 0; uint32_t phy_id2 = 0; HAL_ETH_ReadPHYRegister(&heth, 2U, &phy_id1); HAL_ETH_ReadPHYRegister(&heth, 3U, &phy_id2); printf("PHY ID1: 0x%04X, ID2: 0x%04X\r\n", phy_id1, phy_id2);LAN8720正常的返回值是ID1等于0x0007,ID2等于0xC0F1。如果读出来是0xFFFF,说明MDIO通信失败,优先检查PHY地址、PHY复位引脚、以及MDIO和MDC的引脚复用是否正确。
如果你的板子PHY地址确实是1,那CubeMX生成的默认值就不用改。但不管怎样,这个验证步骤都不能省,它能帮你把PHY层面的问题在5分钟内定位出来,避免后面辛辛苦苦调半天发现是PHY地址不对。
3.2 网卡初始化顺序与LWIP启动流程
在CubeMX生成的默认代码里,MX_LWIP_Init函数中会依次完成这几件事:设置MAC地址、注册网卡接口、把网卡状态设置为UP、创建LWIP相关的线程。
MAC地址这个细节很容易被忽略。CubeMX生成的代码里默认MAC地址是全零或者一个固定值,为了确保设备在网络中不冲突,建议设置一个唯一的MAC地址。嵌入式产品的MAC地址一般从厂商分配的地址段中取,或者使用本地管理地址范围。一个常见的做法是:
#define MAC_ADDR0 2U #define MAC_ADDR1 0U #define MAC_ADDR2 0U #define MAC_ADDR3 0x12U #define MAC_ADDR4 0x34U #define MAC_ADDR5 0x56U第一个字节的最低位为1,表示这是本地管理的单播MAC地址,不会和全球唯一的MAC冲突。
初始化顺序上,CubeMX生成的代码已经帮我们做好了。MX_ETH_Init先初始化MAC和DMA,MX_LWIP_Init再基于MAC初始化LWIP协议栈。你只需要确保MX_FREERTOS_Init在MX_LWIP_Init之后执行,因为LWIP创建任务时依赖FreeRTOS的内核已经就绪。在main.c里,这个顺序是已经排好的,不需要额外调整。
3.3 静态IP设置与Ping通的第一步
如果你在CubeMX的LWIP配置页面里填好了静态IP地址,比如192.168.1.10,子网掩码255.255.255.0,网关192.168.1.1,那么生成的lwip.c里会自动包含对应的配置。在你的开发板上电后,网卡会使用这个IP地址启动。
此时把电脑的有线网卡配置为同一网段,比如192.168.1.100,子网掩码255.255.255.0,然后用网线直接连接电脑和开发板。在电脑的CMD里执行:
ping 192.168.1.10 -t这是整个调试过程中最令人期待的一步。如果通了,意味着MAC、PHY、DMA、LWIP、FreeRTOS这一整条链路全部正常,后面的业务逻辑开发就有了一个坚实的基础。
如果不通,不要慌。先检查电脑网卡有没有识别到链路,正常情况下插上网线后,电脑右下角网络图标会从红叉变成正在识别或已连接。如果电脑仍然显示网络断开,说明物理层链路就没建立起来,问题基本锁定在PHY一侧:时钟没起来、复位没完成、或者LAN8720的供电有问题。此时可以通过读写PHY寄存器来进一步确认PHY是否处于正常工作状态。
3.4 应用层任务与LWIP的交互写法
在FreeRTOS环境下跑LWIP时,应用层任务最重要的一个原则是:不要在中断回调或以太网接收线程里做耗时操作,也不要在协议栈内部回调函数里直接发送大块数据。我一般把业务任务和网络任务分开,业务任务里调用LWIP的socketAPI,或者通过队列把待发送数据发给一个专门负责TCP/UDP发送的任务。
举一个最简单的TCP客户端写法:
void tcp_client_task(void *argument) { struct netconn *conn; struct netbuf *buf; err_t err; ip_addr_t server_ip; IP4_ADDR(&server_ip, 192, 168, 1, 100); conn = netconn_new(NETCONN_TCP); while (1) { if (netconn_connect(conn, &server_ip, 8080) == ERR_OK) { // 连接成功后循环发送数据 while (1) { buf = netbuf_new(); netbuf_alloc(buf, 64); memcpy(buf->p->payload, "hello", 5); netconn_write(conn, buf->p->payload, 5, NETCONN_COPY); netbuf_delete(buf); vTaskDelay(pdMS_TO_TICKS(1000)); } } vTaskDelay(pdMS_TO_TICKS(3000)); } }这个例子只是演示了任务里使用netconn API的连接和发送流程,实际项目中要注意连接失败时的重连退避策略,以及多客户端连接时需要使用线程安全机制。
4. 常见问题排查实录
4.1 PHY读不到ID,MDIO通信全失败
这个问题的现象是:程序执行到HAL_ETH_Init时返回HAL_ERROR,或者在读取PHY寄存器时得到0xFFFF。
排查顺序是这样的:先确认PHY地址对不对,这占了大约六成的可能性。然后确认PHY的复位引脚状态,很多板子的LAN8720复位由MCU的一个GPIO控制,代码里必须在初始化PHY之前把它拉高,否则PHY一直处于复位状态。接着确认MDIO和MDC配置是否正确,这两个引脚需要在CubeMX里被设置为ETH_MDC和ETH_MDIO的复用功能。最后检查供电,LAN8720通常需要3.3V和1.2V两组电源,某些模块还有单独的VDDIO引脚,供电异常也会导致MDIO读不到数据。
我在一个定制板卡上曾经遇到一个很奇怪的现象:PHY ID时而能读到时而读不到。最后定位到是PHY复位引脚在其它初始化函数里被重新配置成了普通GPIO,导致PHY被周期性复位。这种排查思路值得记录一下:如果PHY时好时坏,优先检查有没有代码在ETH初始化之后又改动过相关GPIO的状态。
4.2 链路状态一直Down,网口灯不亮
网口灯不亮或者链路状态寄存器始终显示未连接,这个问题通常和网络数据通路没有关系,问题出在物理链路层面。
先检查网线本身是否正常,换一根已知好的网线测试。然后检查PHY的参考时钟是否为50MHz,用示波器测量REF_CLK引脚,这是最直接的方法。如果没有示波器,可以通过修改PHY的寄存器从LED状态间接判断,但效率低很多。再检查变压器和RJ45座子的焊接,这个问题在新打样的板子上尤其常见,看起来焊好了实际上虚焊,用放大镜看一圈能省不少时间。
还有一个容易被忽略的坑是LAN8720的RMII模式配置。这颗PHY芯片在上电或复位后,默认情况下未必是RMII模式,需要确认PHY的配置寄存器里工作模式正确。如果你在CubeMX里把STM32配成了RMII,但PHY实际工作在MII或者别的模式,链路层的信号就对不上,即使物理链路有信号也Ping不通。这种情况一般通过读取和设置PHY特殊控制寄存器来处理,开发板上出厂已经配置好的话通常不用管。
4.3 Ping不通的完整排查路径
当链路状态已经UP、但Ping不通的时候,就要按照网络分层的思路来排查。我把这个步骤写成了一套固定流程,每次遇到都按这个顺序走:
第一步,检查电脑和板卡的IP地址是否在同一网段。板卡192.168.1.10,电脑192.168.1.100,掩码255.255.255.0,这个组合是最不容易出错的。
第二步,关闭电脑的防火墙。Windows的防火墙默认会拦截来自开发板的ICMP回显请求。测试时我都是直接在Windows防火墙里把ICMP回显请求设置为允许,或者干脆临时关闭防火墙。
第三步,用arp -a命令查看电脑的ARP缓存表里有没有开发板的MAC地址。如果在Ping了之后ARP表里出现了板卡的MAC,说明二层链路是通的,问题出在IP层的发送或接收;如果ARP表里始终看不到板卡的MAC,说明开发板发出的ARP请求没有到达电脑,问题在PHY、DMA或LWIP接收链路。
第四步,用Wireshark抓包。在电脑有线网卡上打开抓包,再Ping一次,观察是否有开发板发出的ARP请求或应答。这一招可以直接定位问题是在物理层、数据链路层还是更高层的协议配置上,非常高效。
第五步,确认板卡终端有没有打印LWIP的初始化日志。CubeMX生成的LWIP代码默认会向printf输出一些调试信息,如果能看到类似"registered 0"或网卡信息,说明协议栈注册成功;如果什么都没有,检查串口重定向是否正常。
4.4 死机与HardFault:中断优先级和内存泄漏
在FreeRTOS环境下跑LWIP,死机问题主要集中在两个地方:ETH中断优先级和内存分配。
ETH中断优先级的问题前面已经提过:在FreeRTOS配置里,configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY默认是5,如果你的ETH中断优先级数值小于5(也就是优先级高于内核),那么中断回调调用osSemaphoreRelease时会导致系统进入断言失败或者HardFault。解决方法是把ETH中断优先级改成5或6,或者把configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY调整为和ETH中断优先级匹配的值。
内存泄漏的问题通常在长时间运行后暴露:设备刚开始一切正常,跑了几个小时或几天后开始出现Ping不通、连接失败、自动复位等故障。排查时重点关注LWIP的内存堆和PBUF池是否耗尽。可以在LWIP里打开内存统计功能,通过串口周期性打印剩余内存和PBUF数量,观察数值是否持续下降。如果持续下降,说明某个地方在持续分配内存却没有释放,常见的元凶是TCP连接异常关闭时缓冲区没有被正确回收,或者应用层发送数据时没有及时释放netbuf。
4.5 LWIP内存分配失败导致连接断开
如果传输数据量较大,或者并发连接数较多,LWIP可能因为内存不足而分配失败。表现为TCP连接建立后,传一会儿数据就断开,重连后又能传一会儿,如此循环。
这类问题本质上就是LWIP的MEM_SIZE、PBUF_POOL_SIZE、PBUF_POOL_BUFSIZE这几个参数和实际业务不匹配。我的建议是:哪怕当前项目只用UDP,也把MEM_SIZE先调到4096以上,给协议栈留足余量。跑TCP时,TCP_SND_BUF和TCP_WND不要低于四倍TCP_MSS,否则大文件的传输效率会非常低。
如果做完这些调整仍然频繁分配失败,那就说明内存确实不够用,需要在CubeMX里增大FreeRTOS的堆大小,因为LWIP的内存池本质上是从FreeRTOS的堆里静态分配的。
5. Ping通实测与稳定性验证
5.1 完整Ping通测试流程
当整个工程配置完成、代码编译下载之后,我有一套固定的验证流程,确保不是侥幸Ping通。
先用串口助手打开板卡的调试串口,观察启动日志。正常情况下,ETH初始化和LWIP初始化成功后,串口会打印出网卡注册信息。把电脑网卡IP改成192.168.1.100,子网掩码255.255.255.0,网关留空或填192.168.1.1都行。然后用网线直连板卡,执行ping 192.168.1.10 -t。
如果通了,先不急着庆祝,我用一个长ping来验证稳定性:连续ping 2000个包,观察丢包率。一个健康的F407加LAN8720方案,在没有业务流量的情况下,局域网内的丢包率应该是0,平均延迟一般在1ms以内。
如果在长ping过程中出现个别超时,优先怀疑PHY芯片的散热、电源纹波、以及网线质量。这类偶发性丢包和软件配置的关系不大,更多是硬件设计上的细节问题。
5.2 用TCP/UDP进一步验证网络通路
Ping通只能验证ICMP协议,也就是IP层和ARP层的工作情况,应用层业务能不能跑通还需要用TCP或UDP实测。
我习惯用两个方法验证。第一个是在电脑上用网络调试助手开一个TCP服务器,监听8080端口,然后让板卡主动连接并周期发送数据。如果能稳定收到数据,说明TCP连接、数据分包、重传机制都是正常的。第二个方法是用板卡开一个TCP服务器或UDP服务,电脑端周期发送数据,板卡接收后原样回发,在电脑上验证回显内容是否正确。
这一步非常重要,因为很多问题在Ping阶段暴露不出来,只有在TCP连接多次建立、断开、重连之后才会暴露。比如TCP连接反复断开重连时,如果LWIP的TIME_WAIT状态处理不当,会在内存里积累大量残留连接,最终导致新的连接无法建立。
5.3 提升稳定性的几个实践建议
跑通只是第一步,真正可靠的产品还要经过稳定性打磨。在我的项目里,有几个做法对提升整个以太网链路的稳定性帮助很大。
第一个是给PHY做定期的链路状态监控。通过任务周期读取PHY的基本状态寄存器,检测link状态是否变化。对于工业应用,网线松动、对端设备断电都是常见故障,检测到link down后在应用层做相应的报警或重连处理,比让协议栈自己去碰运气要可靠得多。
第二个是TCP连接发送数据的节奏控制。嵌入式设备的内存资源有限,如果应用层在短时间内连续向网络上发送大量数据,LWIP内部缓冲会迅速打满。我一般在业务逻辑里做简单的发送节流,比如把大数据分成小块,每块之间加几毫秒间隔,或者依赖TCP的发送缓冲区判断,缓冲区满了就等待。
第三个是设计好异常恢复路径。以太网设备在实际使用中一定会遇到网线拔出、对端重启、路由器切换这些情况。如果软件里没有对应的恢复机制,网络恢复后设备可能无法重新建立连接。我的做法是在应用层任务里加入连接状态监控,断线后自动重连,重连次数和间隔采用指数退避策略,避免在网络不稳定的情况下对PHY和路由器造成持续的连接风暴。
最后分享一个我自己的调试习惯
每次拿到一块新的F407加LAN8720板子,我从来不急着开CubeMX,而是先做三件事:翻原理图确认REF_CLK从哪来、确认PHYAD0引脚是拉高还是拉低、确认PHY复位脚接在哪里。这三个信息确定了,编译出来的第一版代码八成就能跑通。软件调试时遇到问题,我也习惯先验证PHY层的寄存器读写,再往上层查,因为从底层往上层排查,效率最高,也最不会绕远路。
这套组合虽然从配置到Ping通的过程不算太轻松,但只要跑通一次,后面的项目复用起来就会非常顺手。CubeMX生成的工程结构、LWIP和FreeRTOS的协作方式、PHY层的调试方法,这些沉淀下来之后,再做新的以太网设备就只是业务逻辑和时间问题。