先把结论放前面STM32F4 系列跑 FreeRTOS 再挂一个 LwIP 协议栈搭配 LAN8720 这颗百兆 PHY是很多物联网设备、工业采集器、远程升级模块的经典组合。这套组合能跑通但绝不轻松。我得说哪怕你已经在别的平台上玩过 LwIP到了 STM32F407 LAN8720 这里照样会踩进几个隐蔽的坑里而且其中一个坑能让你排查整整一个下午。这篇文章要解决的就是从零开始把 LwIP 2.1.2 移植到 STM32F4 FreeRTOS 上并且把 LAN8720 这个百兆 PHY 配置好让它真正能 ping 通、能跑 TCP 通信。我会把你当成一个有一定 STM32 基础、但对网络协议栈不算熟悉的开发者所有步骤都是我在实际板上验证过的。文章里会讲清楚方案为什么这么选、CubeMX 里每个参数背后的含义、代码改哪些地方、PHY 寄存器怎么看以及在调试阶段最容易出现的几个“诡异现象”到底怎么排查。适合看这篇文章的人正在用 STM32F407/417 之类带 MAC 的芯片准备做以太网通信但不想用 W5500 这种 SPI 网卡想直接上内置 MAC 外置 PHY 方案的开发者或者你已经被 LAN8720 的 PHY 地址、REF_CLK 时钟折磨得头疼想找一份完整排查思路的朋友。读完你会对“STM32 内置 MAC 外部 PHY RTOS LwIP”这条链路有一个系统级的理解而不是只复制粘贴代码。1. 移植前需要想清楚的几件事1.1 为什么是 LwIP 2.1.2 LAN8720先聊选型。STM32F407 这颗芯片内部自带以太网 MAC 控制器但物理层还需要一颗 PHY 芯片来处理差分信号、编码解码这些事。市面上常见的 PHY 有 LAN8720、DP83848、LAN8742、RTL8201 等而 LAN8720 几乎是 STM32F407 开发板上出现频率最高的一颗原因很直接RMII 接口、引脚少、3.3V 单电源、成本低。它和 STM32F4 的 RMII 接口配合时只需要 7 根数据/控制线TXD0、TXD1、TX_EN、RXD0、RXD1、CRS_DV、REF_CLK加上 MDC/MDIO 两根管理线一共 9 根信号线就能跑起来。相比之下如果选 MII 接口的 PHY数据线要翻一倍还多PCB 布线也更痛苦。所以从实际项目出发LAN8720 是很务实的选择。再说 LwIP 版本。LwIP 2.1.2 发布于 2018 年是 2.1.x 系列里很稳定的一个版本。相比老掉牙的 1.4.1它的 API 更规范netconn 和 socket 层的兼容性更好修复了 TCP 重传、内存管理好多个历史 bug。相比 2.0.x2.1.x 又增加了不少新特性比如更完善的 IPv6 支持、RAW API 的优化。在 STM32F407 这种资源不算特别充裕的 MCU 上2.1.2 属于稳定性和功能之间的平衡点。需要提一句的是这套方案适合的项目是需要对 TCP/UDP 传输、HTTP 配置页面、MQTT 上报、固件升级这类功能。如果你只想用一个非常简单的 UDP 收发甚至可以用裸机 LwIP 无操作系统模式NO_SYS1代码量更少。但既然项目里已经需要跑多个任务、管理多个外设FreeRTOS 几乎是逃不掉的选择那 LwIP 在 RTOS 环境下的优势就体现出来了。1.2 用 CubeMX 生成基础工程还是纯手动移植很多人第一次做 LwIP 移植时会纠结要不要用 CubeMX。我的建议非常明确用 CubeMX 生成基础框架但别完全信任它生成的 PHY 驱动。CubeMX 能为你在图形界面里完成三件工作量很大的事第一配置 STM32F407 内部的 ETH 外设和 DMA 描述符第二把 RMII 对应的引脚复用关系全部安排好第三自动生成 FreeRTOS 和 LwIP 的代码框架包括ethernetif.c底层接口文件。但问题也很明显CubeMX 默认带的是LAN8742 的驱动不是 LAN8720。有人会说这俩都是 SMSC 系的 PHY寄存器结构差不多但两者的 PHY 地址不一样默认情况下 LAN8742 是 0x00LAN8720 是 0x01。你在 CubeMX 里选了 LAN8742生成的代码里 PHY 地址就是 0x00直接跑MDIO 总线根本读不到 LAN8720 的任何寄存器。我的做法是用 CubeMX 生成工程骨架和初始化代码然后自己动手改写 PHY 驱动层把 LAN8742 的底包换成 LAN8720或者至少修改 PHY 地址和相关寄存器访问代码。后面第 3 章会详细说这一步怎么操作。1.3 FreeRTOS 和 LwIP 到底怎么配合简单说LwIP 是一个独立于操作系统的 TCP/IP 协议栈它可以跑在裸机上以轮询方式驱动也可以跑在 RTOS 上。当lwipopts.h里的NO_SYS宏被定义为 0 时LwIP 就会被编译成带操作系统依赖的版本此时协议栈内部通过信号量、邮箱、互斥锁这些 OS 原语来保护共享数据。在 FreeRTOS 环境下LwIP 会启动一个名叫tcpip_thread的核心线程这个线程负责处理协议栈里的定时器、TCP 重传、IP 分片、网络接口状态变化等任务。你的业务线程通过 netconn API 或 socket API 与 tcpip_thread 通信比如你要发一个 TCP 数据包业务线程把数据丢给 tcpip_thread由它来实际完成协议封装和网卡发送。这里有个关键点ETH 中断服务和 DMA 接收完成回调应当在很短的时间内完成只负责把数据从硬件搬运到内存池并通过信号量唤醒 tcpip_thread真正复杂的协议解析都在 tcpip_thread 里完成。CubeMX 生成的代码已经帮你把ETH_IRQHandler和HAL_ETH_RxCpltCallback这些钩子函数挂好了你不需要从头写但要理解这个流程否则后续排查问题会一头雾水。2. LAN8720 硬件连接时钟、复位和地址是三大坑2.1 LAN8720 管脚速查与典型板级接法LAN8720 是一颗 QFN-24 封装的百兆 PHY管脚不多但每一根都不能接错。先看 RMII 接口下 STM32F407 与 LAN8720 的典型对照接法STM32F407 引脚STM32F407 端口功能LAN8720 引脚LAN8720 功能PA1ETH_RMII_REF_CLKREF_CLK50MHz 参考时钟输出PA2ETH_RMII_MDIOMDIO管理数据输入输出PA7ETH_RMII_CRS_DVCRS_DV载波侦听/数据有效PC1ETH_RMII_MDCMDC管理时钟PC4ETH_RMII_RXD0RXD0接收数据位 0PC5ETH_RMII_RXD1RXD1接收数据位 1PB11ETH_RMII_TX_ENTX_EN发送使能PB12ETH_RMII_TXD0TXD0发送数据位 0PB13ETH_RMII_TXD1TXD1发送数据位 1除开这些数据线LAN8720 还有几个“命运攸关”的引脚NRST硬件复位引脚低电平有效。一般用 STM32 一个 GPIO 控制或者接 RC 复位电路。PHYAD0PHY 地址配置引脚。下拉接地时地址为 0x01上拉时可能是 0x00具体取决于内部默认值和外部接法。大多数模块出厂默认接地地址是 0x01。INT/REGOFF这个引脚是复用功能必须拉低以启用内部 1.2V 稳压器输出。如果这个脚悬空或者被拉高PHY 可能直接不工作表现就是 MDIO 读什么都是 0xFF。XI/XO晶振输入输出脚接 25MHz 时钟源下面重点讲。LED0/LED1网络状态指示接两个 LED 可以直观看到 Link 状态和收发活动。在模块化的 LAN8720 板子比如网上常见的 10Pin 排针小板上这些引脚基本都被引出到排针了你只需要把排针对应接到 STM32F407 的引脚上。但如果自己画板请务必参照数据手册检查每个引脚的上下拉尤其不能忽略 REGOFF 这种功能引脚。2.2 REF_CLK 50MHz 从哪来时钟接错最隐蔽这是我见过最多人栽跟头的地方得掰开了讲。RMII 模式要求 MAC 和 PHY 共享一个 50MHz 的参考时钟这是接口规范决定的STM32F4 内部的 MAC 不会自己产生这个 50MHz。REF_CLK 必须从外部进来接到 PA1。但 LAN8720 这颗 PHY 本身非常“聪明”它内部自带一个 PLL可以用 25MHz 的晶振输入在内部倍频到 50MHz然后通过 REF_CLK 引脚把 50MHz 参考时钟输出给 MCU。这就是绝大多数开发板采用的方案LAN8720 的 XI/XO 之间接一颗 25MHz 无源晶振然后 REF_CLK 输出 50MHz 到 STM32 的 PA1。整个时钟链路按顺序是25MHz 晶振 → LAN8720 内部 PLL 倍频 → REF_CLK 引脚输出 50MHz → STM32F407 PA1ETH_RMII_REF_CLK。还有一种方案如果你不想在 LAN8720 旁边放晶振可以用 STM32F407 的 MCO 引脚输出 25MHz 时钟接到 LAN8720 的 XI。比如用 PA8MCO1输出 HSE 二分频后的 25MHzLAN8720 内部照常倍频REF_CLK 依然输出 50MHz。这种做法的好处是省一颗晶振但代码里必须额外配置 MCO 时钟。最坑的地方来了。有朋友会以为 STM32F407 的 PA1 可以直接接 25MHz 或者认为 PHY 会把 25MHz 转成 RMII 时钟而不需要 REF_CLK 输出。实测结果是链接指示灯可能亮、PHY 寄存器也能访问但 ping 永远不通或者能收到包但发不出去因为 MAC 采样的参考时钟频率不对数据位全都错乱了。调试时我没有示波器怎么办有一个笨办法先确认 PHY 是否正常工作的前提下PHY ID 能读出来、Link 状态是 up你还收不到任何数据大概率就是 REF_CLK 没进来或者频率不对。这时候重点检查 PA1 这一路信号看看板子上的 LAN8720 模块是否把 REF_CLK 引出来了以及 25MHz 晶振是否起振。用万用表量晶振引脚电压能粗略判断是否起振但最可靠的还是拿示波器量一下 PA1 有没有 50MHz。2.3 PHY 地址、软复位和模式引脚PHY 地址是 STM32 通过 MDIO/MDC 总线访问 PHY 寄存器时的“设备地址”标准的 MDIO 协议里地址范围是 0~31。LAN8720 的 PHYAD0 引脚决定它的 SMI 地址。这里有一个巨大无比的历史坑ST 官方探索板板载的是 LAN8742地址默认为 0x00而市面上买到的单个 LAN8720 模块PHYAD0 默认被拉低地址为0x01。CubeMX 的 PHY 下拉列表里没有 LAN8720只有 LAN8742很多人就直接选了 LAN8742生成的代码里LAN8742_PHY_ADDR宏是 0x00结果 MDIO 总线在地址 0x00 上读出了 0xFF 0xFF于是开始疯狂怀疑硬件。解决方式很简单把 PHY 地址宏改成 0x01或者直接换用 LAN8720 的驱动代码。复位也有讲究。LAN8720 硬件复位至少需要把 NRST 拉低一段时间数据手册建议几十微秒到几毫秒然后拉高再等待 PHY 内部初始化完成。我在代码里通常的做法是HAL_GPIO_WritePin(PHY_RESET_GPIO_Port, PHY_RESET_Pin, GPIO_PIN_RESET); HAL_Delay(50); HAL_GPIO_WritePin(PHY_RESET_GPIO_Port, PHY_RESET_Pin, GPIO_PIN_SET); HAL_Delay(200);拉低 50ms 实际比数据手册要求长很多但这足够保证 PHY 完全复位拉高后再等 200ms 让 PHY 内部 PLL 锁定和自协商启动。不要嫌这个延时太长初始化阶段多等几百毫秒在绝大多数嵌入式系统里都能接受。除了硬件复位PHY 还支持软件复位。方法是在寄存器 0BMCR的 bit15 写 1然后轮询等待该位自动清零。软件复位在一些需要“热重启 PHY”的场景下很有用排查问题也经常用后面第 4 章会给出完整函数。3. CubeMX 配置与关键代码落地3.1 时钟树和 ETH 外设配置在 CubeMX 里的操作顺序我建议这样来能少走不少弯路。第一步配置时钟树。这里以 STM32F407VET6 为例外部晶振 HSE 8MHz系统时钟跑到 168MHzAPB1 分频系数为 4 得到 42MHzAPB2 分频系数为 2 得到 84MHz。ETH 外设的时钟挂在 AHB1 上所以 AHB1 不分频也就是 168MHz。这些在 Clock Configuration 页面里鼠标点一下就能配好需要注意的就是别为了省电把 AHB 分频搞太大否则 MAC 和 DMA 的时钟不够用。第二步在左侧 Categories 里勾选Connectivity - ETHMode 选择RMII。CubeMX 会自动把前面表格里那些 GPIO 复用关系配好不再需要手动去折腾寄存器。第三步在 ETH 配置里要留意一个叫PHY Address的参数把它从默认的 0 改成 1。不同 CubeMX 版本这个参数的显示位置可能略有差异有的在 ETH 配置的Advanced Parameters里有的直接可见。改完以后生成的eth句柄结构体里heth.Init.PhyAddress就是 1后续 HAL 库用这个地址访问 PHY 寄存器。第四步如果你决定用 MCO 输出 25MHz 给 LAN8720需要额外配置RCC - MCO。我建议新手一开始直接用板上 25MHz 晶振方案让 MCO 这层先别碰减少变量。配置完后先不急着加入 LwIP可以先编译一次确保 ETH 底层初始化没有大问题。然后再回到 CubeMX在Middleware - LWIP里把 LwIP 打开Mode 选择Raw API或者Netconn API都行。如果你的业务比较简单我建议选 Netconn API写起来更接近 socket 编程逻辑更清晰。3.2 LwIP 与 FreeRTOS 参数调整CubeMX 生成 LwIP 工程后lwipopts.h里默认的参数偏向“能工作”而不是“好用”你需要按实际需求调整。我最先动的是内存相关参数。STM32F407 有 192KB SRAMFreeRTOS 自己也要占堆所以 LwIP 的内存不能给得太小否则 TCP 窗口一大就会内存不足。我的经验值#define MEM_ALIGNMENT 4 #define MEM_SIZE (40 * 1024) #define PBUF_POOL_SIZE 16 #define PBUF_POOL_BUFSIZE 1518 #define TCP_MSS 1460 #define TCP_WND (4 * TCP_MSS) #define TCP_SND_BUF (4 * TCP_MSS)MEM_SIZE是 LwIP 动态内存池的总大小默认可能只有几千字节太少了。我调到 40KB 是基于 F407 的资源来考虑的如果你用的是 F429 甚至 H7可以再加大。PBUF_POOL_SIZE是接收缓冲区池大小调大一些能防止突发流量下丢包。NO_SYS这个宏必须为 0。如果你到 lwipopts.h 里看到NO_SYS 1那说明 LwIP 被编译成了裸机模式不会创建 tcpip_thread一定要改过来。FreeRTOS 这边CubeMX 生成的代码会自动处理tcpip_thread和default_task的创建你不用自己动手。但建议检查一下任务的优先级和栈大小tcpip_thread优先级不宜太高如果它抢占得太狠你的业务任务可能饿死但也不能太低否则网络事件响应慢。我用默认优先级一般没问题任务栈给它 512 或 1024 字节看编译后的链接情况再调。还有一点容易被忽略HAL 库本身也依赖一个时基timebase默认用 SysTick。但 FreeRTOS 也占用 SysTick两者会冲突。在 CubeMX 的SYS配置里把Timebase Source从SysTick改成TIM7之类的外设定时器。这一步不做FreeRTOS 跑起来会非常诡异。3.3 替换 PHY 驱动把 LAN8742 改成 LAN8720CubeMX 生成工程时如果选了 LAN8742会生成lan8742.c和lan8742.h。这些代码在ethernetif.c里被调用完成 PHY 的复位、初始化、链路状态获取。LAN8742 和 LAN8720 底层寄存器基本兼容但 PHY 地址不一样这是需要处理的第一个差异。最省事的做法是直接编辑lan8742.c里的 PHY 地址宏// 原来 #define LAN8742_PHY_ADDR ((uint8_t)0x00U) // 改成 #define LAN8742_PHY_ADDR ((uint8_t)0x01U)改完以后大部分功能其实已经能跑了。但我在实际项目里还是建议你换成 LAN8720 的专用驱动或者自己写一个精简版。原因有两个第一LAN8742 驱动里针对该芯片做的寄存器配置序列与 LAN8720 并不完全一致虽然很多值能通用但不如原生驱动放心第二如果后续 PHY 芯片换品牌比如 DP83848你必须自己掌握底层逻辑。下面是一个简化的 PHY 底层操作代码包括读取 PHY ID、软件复位、获取链路状态#define PHY_BMCR_REG 0x00U #define PHY_BMSR_REG 0x01U #define PHY_ID1_REG 0x02U #define PHY_ID2_REG 0x03U #define PHY_BMCR_RESET (1U 15) #define PHY_BMSR_LINK_STATUS (1U 2) uint8_t lan8720_Init(void) { uint32_t id1 0, id2 0; // 硬件复位 HAL_GPIO_WritePin(PHY_RESET_GPIO_Port, PHY_RESET_Pin, GPIO_PIN_RESET); HAL_Delay(50); HAL_GPIO_WritePin(PHY_RESET_GPIO_Port, PHY_RESET_Pin, GPIO_PIN_SET); HAL_Delay(200); // 读取 PHY ID确认芯片在线 HAL_ETH_ReadPHYRegister(heth, PHY_ID1_REG, id1); HAL_ETH_ReadPHYRegister(heth, PHY_ID2_REG, id2); if (id1 ! 0x0007U || (id2 0xFFF0U) ! 0xA120U) { return 1; // PHY 不在线或不是 LAN8720 } // 软件复位 HAL_ETH_WritePHYRegister(heth, PHY_BMCR_REG, PHY_BMCR_RESET); uint32_t bmcr 1; while ((bmcr PHY_BMCR_RESET) ! 0U) { HAL_ETH_ReadPHYRegister(heth, PHY_BMCR_REG, bmcr); } return 0; }这段代码里的PHY_RESET_GPIO_Port、PHY_RESET_Pin、heth都要根据你实际工程里的符号名替换。其中HAL_ETH_ReadPHYRegister和HAL_ETH_WritePHYRegister是 STM32 HAL 库自带的接口不需要自己实现。还有一点ethernetif.c里的low_level_init函数需要在初始化的时候调用 PHY 驱动去读取链路状态并把这个状态传给netif。如果 PHY 驱动写得不完善会出现netif的网络状态一直不对导致 LwIP 认为网线没插。所以务必确认ethernetif.c里调用 PHY 驱动的位置能正确拿到 Link 状态。3.4 一个最小可用的 TCP Server Demo驱动层打通以后验证协议栈最直接的方式是跑一个简单的 TCP Server。我用 netconn API 写了一个最小可用的例程在 FreeRTOS 的任务里直接跑逻辑很清晰适合初学者照着改#include lwip/netconn.h static void tcp_server_thread(void *argument) { struct netconn *conn netconn_new(NETCONN_TCP); if (conn NULL) { return; } netconn_bind(conn, IP_ADDR_ANY, 8080); netconn_listen(conn); struct netconn *client; struct netbuf *buf; for (;;) { err_t err netconn_accept(conn, client); if (err ! ERR_OK) { continue; } while (netconn_recv(client, buf) ERR_OK) { void *data; u16_t len; netbuf_data(buf, data, len); // 这里处理收到的数据比如回显或者解析指令 netconn_write(client, data, len, NETCONN_COPY); netbuf_delete(buf); } netconn_close(client); netconn_delete(client); } }创建任务的方式取决于你用什么方式管理任务。CubeMX 生成的 FreeRTOS 代码里可以直接在MX_FREERTOS_Init里添加osThreadNew如果是手工工程用xTaskCreate也一样。需要注意 netconn API 必须在 tcpip_thread 启动之后才能正常工作所以任务创建顺序要放在协议栈初始化完成之后CubeMX 生成环境下一般不会有问题。这个 TCP Server 监听 8080 端口收到什么就回什么。板子启动后你先 ping 通再用网络调试助手连 8080 端口发消息能回显就算链路线、协议栈、PHY 驱动全通了。4. 验证方法与坑位排查实录4.1 先确认 PHY 层Link 状态和 PHY ID我一直主张一个原则网络问题必须从底往上查。不要一上来就 ping 网关先确认 PHY 这一层有没有正常工作。最直接的手段就是读 PHY 寄存器。前面 3.3 的代码里读 PHY ID 就是一个很好的自检手段如果 ID1 读到 0x0007、ID2 读到 0xA120说明 MDIO/MDC 通信正常PHY 芯片在线且供电正常。第二步看 Link 状态。读寄存器 1BMSR的 bit2uint32_t bmsr 0; HAL_ETH_ReadPHYRegister(heth, PHY_BMSR_REG, bmsr); if (bmsr PHY_BMSR_LINK_STATUS) { // 链路已建立 } else { // 链路未建立检查网线和对端设备 }注意Link 状态位在 BMSR 寄存器里是个只读状态位如果读出来是 0先别怪软件。拿根网线把板子接到路由器或者电脑上看 LAN8720 模块上的 LED 有没有亮。LED 不亮基本可以确定是硬件链路问题。常见原因网线不通、对端网口没起来、PHY 没复位成功、REGOFF 引脚接错导致内部稳压器关闭。4.2 ping 不通的排查顺序先说结论我在调试过程中遇到的 ping 不通八成都不是协议栈问题而是底层没跑通。按这个顺序排查效率最高第一确认 PHY ID 能正确读出来。读出来是 0xFFFF 或者 0x0000基本是 PHY 地址不对、复位引脚没拉高、MDIO/MDC 引脚复用错误。这时候检查heth.Init.PhyAddress是不是 1检查 CubeMX 里的 ETH 引脚配置检查 PHY 的供电脚和 REGOFF 引脚。第二确认 REF_CLK 是 50MHz。用示波器量 PA1 引脚。没有示波器的话至少用逻辑分析仪或者万用表测一下 PA1 的直流电平有 50MHz 时钟时这里会有明显的 DC 偏置。如果完全没波形查 LAN8720 的 25MHz 晶振是否起振查 REF_CLK 引脚有没有接出来。第三确认 PHY 的 Link 状态。PHY 没协商成功MAC 收不到数据ping 自然不通。如果 Link 状态是 up再看下一步。第四确认 LwIP 的网络接口状态。在代码里可以周期性检查netif_is_link_up(gnetif)如果为 0说明ethernetif.c里的链路检测逻辑没把状态更新到 LwIP。这个细节很多人会漏比如 PHY 驱动里的get_link_status函数返回类型或者位判断写错导致 LwIP 一直认为链路断开。第五检查 IP 配置。板子的 IP 地址、子网掩码、网关和电脑要在一个网段。最稳妥的做法是先用静态 IP把 DHCP 关掉。电脑端要关掉防火墙或者放行 ICMP 协议。不少新手在这里卡住觉得代码没问题其实是 Windows 防火墙把 ping 请求拦了。最后如果以上都没问题还 ping 不通用 Wireshark 抓包看一下板子有没有回 ARP 请求。板子回 ARP说明二层是通的问题可能在 IP 层连 ARP 都不回问题基本还在更底层。4.3 TCP 连接不稳定的原因分析ping 通了之后TCP 连接不稳定是另一个高频问题。现象通常是TCP 能建立连接但传输大文件时会断或者连接建立后发送数据对端收不到完整数据又或者过一段时间应用层就无响应。这里最隐蔽的坑其实是内存不足。LwIP 的 TCP 协议栈发送数据时需要分配内存存放分段TCP_SND_BUF决定了单条 TCP 连接的发送缓冲区大小。如果这个值太小比如只有 2K而你要一次发送 10K 数据发送缓冲区就爆了协议栈会返回内存不足错误。还有一个容易被忽略的点DMA 描述符数量不够。STM32F4 的 ETH DMA 使用描述符环形队列CubeMX 生成代码里默认定义了一个ETH_RX_DESC_CNT和ETH_TX_DESC_CNT。如果板子在高速接收数据时出现 HALT 或者溢出错误可以适当增加接收描述符数量。但注意每增加一个描述符都会多占 RAM需要权衡。另外要注意网线质量。百兆以太网要求网线是 4 对 8 芯的如果是早期那种只有 4 芯的百兆线虽然也能通但信号质量在稍长距离上就会变差表现出来就是 ping 偶发超时、TCP 重传频繁。我做调试时换过一根质量差的网线整整排查了两天才发现是线的问题从那以后我调试网络项目都会先换一根新网线排除干扰。4.4 常见问题速查表把我在这个项目里遇到过的典型问题整理成一张表方便你对照排查现象可能原因解决办法MDIO 读 PHY ID 全是 0xFFFFPHY 地址不对 / PHY 未上电 / REGOFF 悬空改PhyAddress为 1检查 PHY 供电REGOFF 拉低PHY ID 能读但 Link 始终 down网线问题 / 对端没启动 / PHY 未正确复位换网线查对端设备加大复位延时能 link 但 ping 不通REF_CLK 不是 50MHz / MAC 时钟异常用示波器量 PA1查 25MHz 晶振和 REF_CLK 连接ping 偶尔通偶尔超时网线质量差 / DMA 描述符不足 / 内存池太小换网线加大ETH_RX_DESC_CNT调大MEM_SIZETCP 连接建立但发不出去数据TCP_SND_BUF 太小 / 内存耗尽增大TCP_SND_BUF和MEM_SIZEFreeRTOS 任务不调度SysTick 被 HAL 占用冲突在 CubeMX 里把 HAL 时基改为 TIM7系统跑一段时间后 HardFault任务栈溢出 / LWIP 内存访问越界调大任务栈检查heap_4.c堆大小增加栈溢出检测最后说点我的实际感受这套东西我做过不止一次从最早在野火指南者上跑通到后来在公司产品上用 STM32F407 LAN8720 做远程升级功能中间踩过的坑基本都写在上面了。要说最重要的心得就是别急着写业务代码一定要先确保 PHY 能被稳定地访问Link 状态和 PHY ID 能读出来再往上层走。很多人一上来就搜“LwIP ping 不通”其实问题压根不在 LwIP而在 MDIO 总线上那颗 PHY 就没被正确识别。另外一个小建议调试网络时无论你用 Keil 还是 IAR都可以把ethernetif.c里关键函数加上调试输出通过串口把 PHY ID、Link 状态、DMA 错误标记打出来比盯着调试器变量窗口直观得多。如果串口输出正常而网络依然异常再集中排查硬件连线。这套组合的灵活性在于掌握了 LAN8720 的驱动方式换成 RTL8201、DP83848 等 PHY 也是类似的思路无非是寄存器地址、PHY ID、自协商配置不同。等你把底层打通一次后面再做网络相关的功能基本就是写应用层的事了。