STM32F407+FreeRTOS移植LWIP 2.1.2实战:从PHY到协议栈全记录

发布时间:2026/10/2 23:27:09
STM32F407+FreeRTOS移植LWIP 2.1.2实战:从PHY到协议栈全记录
做嵌入式的朋友应该都有过这种经历单板已经开工硬件、驱动、应用逻辑都跑起来了唯独把联网功能加进去的那一刻整个系统的“复杂性”瞬间上一个台阶。STM32F4 FreeRTOS LWIP 2.1.2这套组合在工业控制、智能家居、物联网网关里太常见了——F4内置以太网MACFreeRTOS轻量易用LWIP则是嵌入式界事实标准的开源TCP/IP协议栈。可真正落地的时候光是把LWIP和OS之间的适配层改对、把LAN8720这颗PHY芯片伺候明白就够折腾一阵子。这篇内容是我从零开始把LWIP 2.1.2跑在STM32F407 FreeRTOS上的全记录。适合手里有F4系列板子、想在FreeRTOS下加网络功能但被官方例程绕得一头雾水的朋友也适合刚从裸机转向RTOS、想搞懂协议栈和操作系统怎么配合的初学者。文章会把原理讲清楚再把配置步骤、代码片段、避坑经验一条条列出来让你照着做能少走很多弯路。1. 项目起步先看明白这套组合真正难在哪1.1 为什么偏偏是STM32F4 FreeRTOS LWIPSTM32F4系列内置了符合IEEE 802.3的以太网MAC控制器带DMA配上外置PHY芯片就能做10M/100M以太网。LAN8720A是当前最流行的百兆PHY之一价格低、封装小、外围电路简单和F4是黄金搭档。FreeRTOS就不用说了嵌入式领域使用范围最广的RTOS源码开放、资料齐全、CubeMX直接集成几乎成为工业产品的默认选择。LWIP实现了TCP/IP协议栈支持TCP、UDP、DHCP、DNS、socket API等常用功能裁剪灵活能塞进资源有限的单片机。但这个组合的难度不在任何一个单独组件而在它们之间那层胶水代码。LWIP本身在设计上假设底层有一个操作系统在提供服务信号量、邮箱、定时器、新建线程。在FreeRTOS上跑LWIP真正的任务不是去改协议栈内部而是写一个sys_arch适配层把LWIP的sys_xxx函数映射到FreeRTOS的API上。我见过不少人一上来就翻LWIP内核的TCP源码方向完全错了。1.2 移植前必须先想明白的四件事第一件事LWIP 2.1.x里有一个核心线程叫tcpip_thread所有协议栈内部操作都在这个线程里执行。用户任务通过netconn或socket API发请求请求通过邮箱和信号量传进tcpip_thread网络接口收到数据后也要把数据帧“投递”给tcpip_thread。所以sys_mbox和sys_sem实现得对不对直接决定整个系统能不能跑起来。第二件事以太网链路要分成两级看。F4内部集成的是MAC和DMA负责把内存里的帧搬到线上、把线上的帧搬进内存物理层信号的编码、电平转换、自动协商、速度/双工协商全是PHY芯片的活。F4通过MDIO总线MDC时钟线 MDIO数据线读写PHY寄存器。这个分层搞不清楚后面调Ping不通时会一头雾水。第三件事内存是有预算的。LWIP的PBUF池、TCP_SND_BUF发送缓冲、接收池、各线程自己的堆栈都要占用有限SRAM。F407系列一般有192KB内存配置参数如果拍脑袋填大系统会在运行几十分钟后莫名其妙HardFault。移植前期先用保守值跑通后再按需裁剪。第四件事调试手段要先想好。至少要保证串口能打印FreeRTOS和LWIP的日志最好能在PC上用Wireshark抓包辅助。后面排查问题光靠眼睛看代码是看不出来的。1.3 项目硬件与软件准备我这边用的是一块STM32F407VET6核心板外接独立LAN8720A网口模块RMII接口PHY地址0x00。这种网口模块国内很常见接线也简单F407的ETH_RMII引脚与模块对应连接RMII参考时钟由模块上的25MHz晶振和内部PLL产生模块的CLKOUT直接接到STM32的ETH_RMII_REF_CLK脚。软件方面STM32CubeMX负责生成底层初始化IDE用Keil MDK 5.3xFreeRTOS用CubeMX自带的V10.x内核LWIP用2.1.2源码包再加一个串口调试助手。开发前把LAN8720A数据手册和STM32F4参考手册RM0090放在手边遇事查手册比瞎猜强。2. 环境准备与基础工程搭建2.1 CubeMX里先把ETH和PHY时钟安排明白用CubeMX新建工程以后第一件事是配时钟树因为以太网对时钟的要求比普通外设严格得多。LAN8720工作在RMII模式时需要一个50MHz的参考时钟。这个时钟从哪来取决于你的硬件设计主要有三种路子第一种PHY模块上用25MHz晶体LAN8720内部PLL倍频到50MHz再从CLKOUT引脚输出给STM32的ETH_RMII_REF_CLK。很多现成的LAN8720模块就是这么设计的包括我用的大多数“原子式”模块。这种情况下你不需要设置STM32的MCO输出只要把PHY配置为25MHz CLKIN模式即可。第二种用一个独立的50MHz有源晶振或外部振荡器直接给PHY的XTAL1/CLKIN脚供时钟PHY再把50MHz的参考时钟提供给MAC。这种方案最省心但对硬件成本略微增加。第三种用STM32的MCO1输出50MHz给PHY。这种做法要特别注意MCO1输出的是PLLQ时钟而PLLQ和系统主频是绑定在同一个PLL下的不一定能恰好凑出50MHz。比如外部8MHz晶振、主频168MHz的经典配置下PLLQ常见是48MHz不是50MHzPHY虽然能容忍一定偏差但不稳定。真要走这条路需要调整PLL参数比如把系统主频设成100MHz、PLLQ8得到50MHz否则就是用25MHz晶振配PLLM25、PLLQ8凑50MHz。总体而言第一种和第二种方案更常见、更稳。时钟确认好以后ETH外设配置很简单在CubeMX里把ETH模式改成RMIIPHY Address填0x00其余默认。CubeMX会自动把PA1、PA2、PA7、PC1、PC4、PC5分配成RMII接口不用手动一个个查映射。2.2 FreeRTOS配置的七个坑CubeMX生成FreeRTOS工程很快但有几个地方要手工改否则后面会挂得很惨。第一堆大小。CubeMX默认的configTOTAL_HEAP_SIZE往往只有8KB跑一个tcpip_thread加几个应用任务就不够用建议直接给到48KB以上等调试稳定后再回头看剩余量裁剪。第二时间基准。FreeRTOS默认用SysTick做系统心跳而HAL库的时基也依赖SysTick。我习惯把HAL时基改成TIM6避免两个系统互相干扰。否则调度器启动后HAL_Delay和vTaskDelay混用经常出诡异问题。第三中断优先级设置。FreeRTOS要求NVIC分组为4也就是每个中断用4位优先级。configMAX_SYSCALL_INTERRUPT_PRIORITY默认是5含义是优先级数值不大于5的中断里才能调用带FromISR尾缀的FreeRTOS API。ETH中断优先级如果设成15默认低在ISR里发信号量不会立即报错但行为不可预期偶发死机。我通常把ETH中断设为优先级4DMA发送完成中断也是4。第四定时器服务任务建议打开。LWIP的超时处理可以放在tcpip_thread里完成但FreeRTOS自己的定时器任务打开以后后面用socket选项、超时控制会方便很多。第五默认任务堆栈太小。CubeMX给每个任务的默认尺寸经常是128个word根本不够网络任务用。我建议把和LWIP相关的任务堆栈至少配到512 word以上。第六确认代码生成用的是cmsis_os2还是老版API。LWIP移植一般用原生FreeRTOS API更直接我在sys_arch.c里直接包含FreeRTOS.h和task.h不依赖cmsis_os封装少一层转换少一层坑。第七ETH中断的NVIC使能要确认打开。CubeMX生成ETH配置时有时不会自动使能ETH中断这时候收发数据只能靠轮询性能差而且容易丢包。一定要到NVIC配置页把ETH中断勾上。2.3 初始化顺序先PHY还是先协议栈这块我吃过亏。正确的顺序是系统时钟 → ETH MAC/DMA初始化 → LAN8720复位并等待就绪 → 配置PHY自动协商 → 启动FreeRTOS调度器 → tcpip_init创建tcpip_thread → netif_add添加网卡 → netif_set_default和netif_set_up → 最后才创建应用任务。为什么先PHY后协议栈因为netif_add里会注册网卡的init函数这个init函数在协议栈启动时被调用那时需要已经能正常操作PHY。如果PHY还没起来low_level_init里读PHY ID、配置PHY就容易失败而且失败返回值常常被忽略后续链路状态永远是false。我最初图省事把LWIP初始化放在调度器启动之后、PHY延时只给了10ms结果PHY ID读出来全是0xFFFF后来改成上电后先拉低复位线100ms再拉高等10ms以上再读才正常。3. LWIP 2.1.2源码接入与核心配置3.1 源码文件怎么组织不踩雷LWIP源码包解压后真正需要加入工程的只有src目录下的几个子目录core是协议栈主体api是netconn和socket APInetif是通用网络接口实现。port层需要自己准备或参照ST官方例程sys_arch.c、sys_arch.h、ethernetif.c、lwipopts.h、arch/cc.h。文件组织上我是这样放的这样层次最清晰Middlewares/Third_Party/LwIP/src 完整放一份协议栈源码。Middlewares/Third_Party/LwIP/port 放应用相关移植文件包括lwipopts.h和sys_arch系列。编译器头文件路径一定要加全src/include、port目录、port/arch目录。很多编译不过的问题都是路径漏了报错找不到lwip/opt.h或者lwip/init.h跟代码本身没关系。另外LWIP 2.1.2默认带了不少可选功能比如SNMP、IGMP、PPP。不需要的就在lwipopts.h里关掉既省内存也省编译时间。我实际编译时只保留了TCP、UDP、DHCP、ICMP、DNS和socket API其他全关。3.2 lwipopts.h如何配出一份“能跑”的配置lwipopts.h是LWIP行为的总开关。第一次移植建议直接用这组保守值#define NO_SYS 0 #define LWIP_PROVIDE_ERRNO 1 #define LWIP_TIMERS 1 #define MEM_ALIGNMENT 4 #define LWIP_TCP 1 #define LWIP_UDP 1 #define LWIP_ICMP 1 #define LWIP_DHCP 1 #define LWIP_DNS 1 #define LWIP_SOCKET 1 #define LWIP_NETCONN 1 #define MEM_SIZE (12 * 1024) #define PBUF_POOL_SIZE 20 #define PBUF_POOL_BUFSIZE 1512 #define TCP_MSS 1460 #define TCP_WND (4 * TCP_MSS) #define TCP_SND_BUF (4 * TCP_MSS) #define LWIP_TCPIP_THREAD_STACKSIZE 2048 #define LWIP_NETIF_STATUS_CALLBACK 1 #define LWIP_NETIF_LINK_CALLBACK 1 #define LWIP_DEBUG 1 #define LWIP_STATS 1逐个解释关键项。NO_SYS0是告诉LWIP底层有OSLWIP会调用sys_arch提供的接口。LWIP_PROVIDE_ERRNO1解决编译时errno未定义的问题这个必须开否则GCC和Keil的某些头文件下会直接编译报错。MEM_ALIGNMENT4是STM32F4的硬性要求DMA描述符和缓冲区的内存必须4字节对齐。TCP_MSS用1460标准以太网MTU是1500去掉IP头和TCP头各20字节就是1460。TCP_WND和TCP_SND_BUF都设成4个MSS即5840字节这个值对吞吐量比较友好F407的内存也能承受。LWIP_TCPIP_THREAD_STACKSIZE这个值在sys_thread_new里会被除以4作为FreeRTOS的堆栈word数我实际给到2048除以4也就是512 word约2KB够用但不宽裕。调试阶段把LWIP_DEBUG和LWIP_STATS开着用串口可以打印协议栈内部的统计信息和调试日志但注意只开放ICMP_DEBUG和TCP_DEBUG不然ARP、DHCP的日志刷屏到根本看不过来。3.3 sys_arch适配层怎么把LWIP和FreeRTOS拧到一起sys_arch是整个移植的核心也是很多编译错误的重灾区。LWIP 2.1.2要求sys_arch至少实现这些函数sys_sem_new/sys_sem_free/sys_sem_signal/sys_arch_sem_wait、sys_mbox_new/sys_mbox_free/sys_mbox_trypost/sys_arch_mbox_fetch/sys_arch_mbox_tryfetch、sys_mutex_new/free/lock/unlock、sys_thread_new、sys_now。我用FreeRTOS原生API实现信号量用二进制信号量邮箱用FreeRTOS队列。有一个很关键的小坑LWIP把sys_sem_t和sys_mbox_t当作不透明类型传递不能直接把semaphore句柄typedef过去否则函数传参时类型不匹配或者资源泄漏。稳妥做法是定义成结构体包装typedef struct { SemaphoreHandle_t s; } sys_sem_t; typedef struct { QueueHandle_t q; } sys_mbox_t;sys_now直接返回毫秒u32_t sys_now(void) { return (u32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS); }sys_thread_new要把LWIP传进来的task_function和arg塞给xTaskCreate。注意LWIP传进来的栈大小是字节数还是word数不同版本解释不一样我在sys_arch里统一处理成“除以4转成FreeRTOS的word单位”。这个转换漏掉也是常见坑任务栈会忽大忽小跑一段时间就栈溢出。邮箱的sys_mbox_trypost会在中断上下文被调用所以内部一定要用xQueueSendFromISR带上FromISR尾缀否则在中断里调用普通的xQueueSend会直接触发断言或死机。sys_mbox_free则要注意等待队列被真正释放之后再删对象否则并发任务还握着已删除的队列直接卡死。sys_arch.h里还要定义LWIP_ARCH_H编译开关cc.h里要定义字节序、数据类型和snprintf等基础函数。F4是Cortex-M4小端处理器所以在cc.h里明确LITTLE_ENDIAN。3.4 ethernetif.c把ETH外设接进协议栈ethernetif.c是网卡驱动核心要实现low_level_init、low_level_input、low_level_output和ethernetif_input。low_level_init里要做的事包括初始化PHY也可以放到外部做、分配RX/TX描述符数组、保证4字节对齐、把每个RX描述符的接收缓冲区挂上对应的LWIP pbuf。在FreeRTOS环境下数据接收的推荐做法是ETH中断里只发一个接收信号量然后由专门的网络任务或直接在tcpip_thread上下文里调用ethernetif_input在函数内读取DMA描述符、组装pbuf、再调用tcpip_input把帧送进协议栈。这样ISR非常短不会阻塞系统调度。我最初图省事在ETH_IRQHandler里反复调用ethernetif_input结果中断里做了大量DMA描述符操作一旦某个描述符忙整个中断就被拖得很长还容易和主循环竞争。low_level_output发送路线上需要用一个发送完成信号量等待DMA发送结束否则连续发送时TX描述符会被写坏。每次发送前检查是否有空闲TX描述符没有就等待TX完成中断唤醒。具体代码我会在第五章展开。4. LAN8720硬件与驱动配置避坑4.1 RMII接口接法50MHz时钟到底谁来管很多人在第一步就倒了因为LAN8720需要的50MHz参考时钟来源没有搞清楚。RMII协议本身要求参考时钟50MHz但这50MHz可以由PHY内部产生也可以由外部输入。最典型的模块设计是LAN8720A旁边焊一个25MHz晶振PHY内部PLL倍频后从CLKOUT引脚输出50MHz给STM32的ETH_RMII_REF_CLK使用。这种情况下STM32的PA1引脚是输入方向不能主动在PA8上输出时钟。我见过有人看到“RMII Ref Clock 50MHz”就去配置MCO输出结果两个时钟源打架PHY时而认时钟时而不认排查到怀疑人生。如果你的模块没有25MHz晶振而是要求外部输入50MHz时钟才用MCO方案或者直接接一个有源50MHz振荡器。所以拿到模块的第一步是看原理图确认它到底用哪种时钟模式。最直接的验证方法上电后用示波器量CLKOUT引脚如果有干净的50MHz方波说明PHY自带了时钟如果CLKOUT悬空或者电平不对就要考虑外部输入时钟。4.2 PHY地址、复位时序与寄存器初始化LAN8720A的PHY地址由PHYAD0引脚电平决定多数模块固定为0x00。MDIO总线上最多有32个物理地址如果你在CubeMX里填了0x00但模块实际是0x01读出来的寄存器就全是0xFFFF。遇到读不到数据先扫描0到31这些地址看哪个地址能返回合法PHY ID这是最快定位方法。复位时序是另一个大坑。LAN8720A要求复位低电平至少保持几十毫秒我实际驱动里是拉低100ms再拉高然后等至少10ms让PHY内部时钟稳定。不要只延时1ms就急着读寄存器PHY还没ready读什么都是0xFFFF。随后通过MDIO读PHY_ID寄存器地址0x02和0x03。LAN8720A的PHYIDR1应读到0x0007PHYIDR2应读到0xC0F1。如果对不上不要继续往下走先解决时钟和复位。然后做软复位写寄存器0x00的bit15为1轮询该bit直到硬件自动清零。软复位完成后配置自动协商相关寄存器比如广告寄存器0x04让PHY广播10M/100M全双工能力再把控制寄存器0x00写成自动协商并重新启动的值。4.3 链接状态检测别等到Ping不通才发现链路断了PHY链接状态可以通过读取基本状态寄存器0x01的bit2来判断1表示已连接0表示断开。但这个bit是只读的不同厂商的“自清零”行为也不一样最稳妥的做法是定时轮询。我实际用的是单独一个低优先级FreeRTOS任务每500ms读一次链接状态一旦发现up变down或down变up就调用netif_set_link_up/netif_set_link_down通知LWIP。这种方法比依赖PHY的中断引脚要少踩很多坑代码简单、逻辑清楚。注意这个任务优先级要放到最低不要和tcpip_thread抢时间否则会加重调度负担。链路状态没有正确通知LWIP还有一个隐蔽症状网线拔掉再插回去以后TCP连接卡死新连接也建立不起来必须重启板子。因为链路down的时候ARP表和TCP的路径没有得到及时清理。所以链路检测不是降级需求是稳定性的刚需。4.4 实测能用的LAN8720初始化代码片段static int LAN8720_Init(void) { uint32_t id1, id2, status, timeout; /* 硬件复位 */ HAL_GPIO_WritePin(PHY_RST_GPIO_Port, PHY_RST_Pin, GPIO_PIN_RESET); HAL_Delay(100); HAL_GPIO_WritePin(PHY_RST_GPIO_Port, PHY_RST_Pin, GPIO_PIN_SET); HAL_Delay(10); /* 等待PHY就绪读取ID */ timeout 0; do { id1 PHY_ReadReg(PHY_ADDR, 0x02); id2 PHY_ReadReg(PHY_ADDR, 0x03); timeout; } while ((id1 0xFFFF || id2 0xFFFF) timeout 1000); if (id1 ! 0x0007 || id2 ! 0xC0F1) { printf(PHY ID error: %08X %08X\r\n, id1, id2); return -1; } /* 软复位 */ PHY_WriteReg(PHY_ADDR, 0x00, 0x8000); timeout 0; do { status PHY_ReadReg(PHY_ADDR, 0x00); timeout; } while ((status 0x8000) timeout 1000); /* 广告支持10M/100M全双工 */ PHY_WriteReg(PHY_ADDR, 0x04, PHY_ReadReg(PHY_ADDR, 0x04) | 0x0140); /* 自动协商并重新启动 */ PHY_WriteReg(PHY_ADDR, 0x00, 0x1200); return 0; }读寄存器时一定要加while超时保护否则PHY异常会导致死循环。另外HAL_Delay在FreeRTOS调度器启动前可以直接用调度器启动后在任务中再调用就要确保HAL时基没被FreeRTOS搞乱这一点在2.2里说过了。5. 数据通道打通以太网帧如何进入LWIP5.1 一条网线数据进来系统到底经历了什么我建议在脑子里把整条数据链路画出来网络线上的模拟信号 → LAN8720做串并转换和解码 → 通过RMII引脚把4位数据交给F407的ETH MAC → MAC解析以太网帧头、校验FCS → DMA把整帧数据搬进描述符指向的内存缓冲区 → DMA置位接收完成标志 → 触发ETH中断 → FreeRTOS信号量被发送 → tcpip_thread被唤醒 → 读取RX描述符、组装成pbuf → tcpip_input把帧递交给协议栈 → 协议栈处理IP、TCP、UDP → 用户任务收到消息。任何一个环节断了表现都是“网络不通”但现象细节不同。比如PHY有Link但收不到包多半是DMA描述符或接收中断没起来能收到包但应用收不到数据再往协议栈下游查。这条链路一定要画在自己案头调试时一项项过比东一榔头西一棒子高效得多。5.2 RX中断如何配合FreeRTOSETH中断服务函数里收到数据包后不能马上做协议栈解析因为协议栈要访问各种表和队列不适合在中断上下文执行。标准做法是发信号量void ETH_IRQHandler(void) { BaseType_t xHigherPriorityTaskWoken pdFALSE; if (ETH_GetRxStatus() ETH_BUFFER_AVAILABLE) { xSemaphoreGiveFromISR(rxSemaphore, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } HAL_ETH_IRQHandler(heth); ETH_DMAClearITPendingBit(ETH_DMA_IT_R); }tcpip_thread的职责是调用ethernetif_input而这个函数通常放在port层代码里拿到RX信号量后循环读取DMA描述符如果描述符显示一帧数据接收完成就组装pbuf、调用tcpip_input把数据交给协议栈。这个流程LWIP的STM32移植模板已经写好了我们主要是确保中断和信号量配置正确。另一个经验是中断优先级不能乱设。ETH中断优先级数值要小于等于configMAX_SYSCALL_INTERRUPT_PRIORITY否则xSemaphoreGiveFromISR行为不可预期。我这里是优先级4DMA发送完成中断也是4。5.3 TX路径与发送完成信号量发送流程经常被忽略直到并发数据量上来才发现问题。low_level_output先申请一个空闲TX描述符把数据从pbuf拷进描述符缓冲区然后启动DMA发送。LWIP在FreeRTOS环境下tcpip_thread发送数据时如果TX描述符都被占满需要等发送完成信号量。这里建议使用带超时的信号量等待比如pdMS_TO_TICKS(1000)超时后返回错误给上层而不是无限等。否则TCP发送卡死后连tcpip_thread都动不了整个协议栈就瘫了。发送完成中断里要释放TX描述符并把发送完成信号量归还给等待者。注意多描述符连续性如果一帧数据占用了多个TX描述符释放时必须把属于同一帧的所有描述符一并释放否则描述符池会错乱。5.4 网络接口启动顺序MAC、PHY、netif三者的时序我把启动顺序整理成一个清晰版本HAL_Init、SystemClock_Config。复位LAN8720并等待PHY就绪。MX_ETH_Init完成MAC和DMA初始化。启动FreeRTOS调度器先创建一个启动任务。启动任务里调用tcpip_init创建tcpip_thread。netif_add注册网卡绑定ethernetif驱动。netif_set_default、netif_set_up。创建用户应用任务之后才能安全使用socket API。第二步和第三步谁先谁后不同例程有差异但只要保证low_level_init里能正常访问PHY即可。我把PHY复位放在MAC初始化之前原因是很多PHY在RMII模式下需要先起来MAC侧的参考时钟才稳定。tcpip_init在调度器启动后调用这样LWIP内部的线程创建能直接取决于FreeRTOS调度器省去很多初始化竞态问题。5.5 最小联通性测试先Ping通再谈业务接业务逻辑之前先做一个“裸Ping”测试设备静态IP 192.168.1.100子网掩码255.255.255.0PC设置同网段IP在PC上ping设备。能通说明PHY、MAC、DMA、协议栈、时钟整个链路是通的。然后写一个简单的TCP回环服务器端口8888PC上网络调试助手连接并收发验证socket API和内存分配。最后才往上挂业务逻辑。我实际遇到过“能Ping通但一开TCP就HardFault”的情况原因就是PBUF_POOL_SIZE配得太小TCP_PCB和TCP_SND_BUF又开得大内存池耗尽最终定位到内存分配失败。所以业务上线前用LWIP自带的内存统计宏打印运行数值看MEM_STATS和PBUF_STATS确认内存余量足够再继续下一层。6. 常见问题与排查技巧实录6.1 高频问题速查表现象可能原因排查手段PHY寄存器读不到全是0xFFFF复位没完成、50MHz时钟没给、PHY地址不对量PHY CLKOUT波形扫描MDIO地址延长复位时间MDIO能读ID但Link bit一直为0网线没插、对端不工作、自动协商失败检查RJ45指示灯读基本状态寄存器0x01 bit2Ping不通MAC地址异常、IP配置不对、RX/TX DMA没配对、协议栈未起串口打印netif状态PC端抓包看ARP请求能Ping通但TCP建立失败内存不足、TCP_WND或MSS太小、对端防火墙调大TCP_WND打印PBUF/MEM统计运行一段时间后死机任务栈溢出、中断里调用非FromISR接口、内存泄漏开栈检测查中断现场检查pbuf释放拔插网线后网络不恢复链路状态没有通知LWIP轮询PHY链接bit并调用netif_set_link_down/up这张表建议打印出来遇到问题先对号入座不要盲目改代码。我遇到最多的情况就是“读不到PHY寄存器”而这些问题的根往往不是代码而是硬件上电时序或时钟没搞定。6.2 排查三板斧寄存器、日志、抓包第一板斧看寄存器。用串口命令或调试变量手动读取PHY寄存器0x00到0x1F结合LAN8720A数据手册逐位判断。比如寄存器0x00的bit12是自动协商完成位bit13是100M能力位读一遍基本能确定PHY侧有没有问题。这一招比盲改代码效率高十倍。第二板斧看日志。LWIP的printf调试功能非常强大把LWIP_DEBUG打开后配合LWIP_DEBUG内核配置项能看到ARP、ICMP、TCP的状态迁移。串口日志一定要分级至少区分PHY、以太网驱动、协议栈三块否则数据量太大反而找不到有效信息。我把日志打印函数统一做了一个带开关的printf不同模块用不同的宏控制这样正式发布时一键关掉。第三板斧抓包。PC端用Wireshark抓包同时去ping设备。看有没有ARP请求到达PC有没有ARP应答返回。如果没有ARP应答问题一定在设备侧的下三层有应答但ping不通问题在ICMP/IP以上层。抓包是最客观的判据比靠猜强得多。6.3 三个几乎零成本的土办法如果手头一时没有示波器和网络分析仪我常用的土办法有三个。土办法一用LED指示状态。我在PHY复位和初始化函数里加了一个GPIO翻转每完成一个阶段翻转一次通过LED闪烁次数判断卡在哪个阶段。比如闪3次表示PHY ID读取成功闪5次表示自动协商完成。这套办法在产线调试时很好用看灯就知道大概位置。土办法二用socket自测。设备上开一个TCP服务器端口固定用电脑或手机的网络调试助手主动连接。连接建立但发送无响应重点查RX路径连接建立但收不到数据重点查TX路径。这个方法不需要额外硬件逻辑也非常直观。土办法三读FreeRTOS运行状态。用vTaskList把任务列表打印出来如果tcpip_thread栈剩余只有几十字节说明栈配小了如果系统Tick增长异常说明有死循环或中断风暴。系统先健康网络才有得谈。6.4 我踩过的那几个坑浓缩成一句话第一个PHY复位时间不够寄存器永远读0xFFFF白白查了一个晚上。第二个ETH中断优先级设置不被FreeRTOS允许导致系统偶发死机问题最隐蔽因为它不是必现的。第三个PBUF_POOL_SIZE配得过大SRAM被耗尽tcpip_thread启动直接失败。记住这三个坑你至少能避开初学者最容易踩的八成问题。最后分享一点个人体会。STM32F4 FreeRTOS LWIP 2.1.2这套方案本身不难难的是把每一层的边界分清楚并且每次只怀疑一层。我后来再做新的联网项目基本流程都是先确认PHY时钟再读PHY ID再测Link状态然后跑Ping最后加业务。这个顺序省了我无数时间。如果你也打算开始移植建议先找一块带LAN8720模块的开发板照着上面的顺序走一遍遇到问题再翻到常见问题表里找对应项大概率都能自己解决。等基础跑通后面加DHCP、DNS、MQTT就都是水到渠成的事了。