这套组合我在实际项目里已经折腾过一轮最近又有朋友问起 STM32F4 上移植 LWIP 的事情干脆把整个过程系统整理出来。芯片用的是带以太网 MAC 的 STM32F407协议栈选 LWIP 2.1.2RTOS 用 FreeRTOSPHY 芯片是 LAN8720A。这套搭配在国内嵌入式项目里出镜率极高几乎每个做物联网网关、工业采集、设备联网升级的工程师都绕不开。LWIP 是专为嵌入式设计的轻量级 TCP/IP 协议栈配合 FreeRTOS 做多任务调度再通过 LAN8720 这颗百兆 RMII 接口 PHY 芯片完成物理层通信整体方案性价比高、参考资料多。但这套标准答案里藏了不少细节坑特别是 LAN8720 的复位时序、REF_CLK 时钟、PHY 地址这几个点网上文章往往一笔带过实际调起来却最容易卡住。下面把从零移植到稳定通信的关键步骤、配置逻辑和踩坑全过程都记下来给正准备做同样事情的读者一份直接能参考的实操手册。1. 为什么是F4 FreeRTOS LWIP LAN8720这个固定搭配1.1 选择 STM32F4 作为 MAC 控制器的理由STM32F4 系列不是所有型号都带以太网外设。选型时要看清型号常见的 F407、F417、F427、F429、F437、F439 等才内置 10/100M 以太网 MAC 控制器而 F401、F411、F412、F423 这些是没有 ETH 外设的只能外接 SPI 接口网络芯片性能和灵活性都差一截。带 MAC 的 F4 主频基本都在 168MHz 以上有独立的 DMA 控制器为以太网收发服务这意味着 MAC 收到数据后可以直接通过 DMA 搬运到内存CPU 不需要逐字节参与。F4 的 RAM 也够用一般都有 128KB 甚至 192KB给 LWIP 的收发缓冲、协议栈堆和 FreeRTOS 任务栈留出了充足空间。加上 HAL 库和 CubeMX 的成熟支持网络外设的初始化代码可以直接生成社区里大量的开源项目也都是基于 F4 跑的 LWIP遇到问题搜索一下基本都有答案。1.2 LWIP 2.1.2 相比旧版本到底改了什么早期很多项目还在用 LWIP 1.4.1那个版本代码相对古老IPv6 支持基本是摆设DHCP 客户端实现也比较粗糙。到 2.0.x 之后核心代码做过一次大规模重构内存管理、pbuf 结构、netconn 和 socket API 都更清晰了。LWIP 2.1.2 属于 2.1 分支的一个稳定版本修复了之前不少边界情况和 STM32CubeF4 固件包的配合也最成熟。选择 2.1.2 还有一个很现实的原因当前主流开发资料、正点原子和野火的例程、ST 官方的应用笔记大多是基于 2.0.3 或 2.1.2 写的。如果你的 CubeMX 版本较新它生成的工程里直接带的就是 LWIP 2.1.2省去了自己下载移植的麻烦。当然如果你要手工移植也可以从 LWIP 官方站点或 STM32CubeF4 包里提取源码。对新手来说建议先用 CubeMX 生成一个参考工程再对照着手工移植版本排查问题效率会高很多。1.3 FreeRTOS 在协议栈移植中承担的角色LWIP 有两种运行模式一种是不带操作系统的 NO_SYS 模式协议栈跑在一个主循环里通过周期性调用tcpip_thread之类的方式处理另一种就是带 RTOS 的模式LWIP 作为一个独立任务运行由 RTOS 负责调度。在 STM32F4 这种资源充足的 MCU 上几乎都会选择后者。FreeRTOS 在这里承担三件事第一ETH 中断产生后中断服务函数只负责给任务发信号量真正耗时的协议栈处理放到线程上下文中避免中断里做重活导致系统卡死第二应用层的 TCP 客户端、HTTP 服务器、MQTT 等逻辑各自跑独立任务与网络协议栈任务互不干扰第三FreeRTOS 的信号量、队列机制可以直接映射成 LWIP 的 sys_arch 层LWIP 的邮箱mbox用队列实现互斥锁用互斥信号量实现移植起来很顺手。内核建议用 10.x 版本比如 V10.4.6稳定且资料多。2. LAN8720 硬件细节正式移植前必须先交代的几件事2.1 RMII 接口信号与 REF_CLK 时钟方案LAN8720A 支持 RMII 接口相比 MII 接口引脚数量少了很多这也是它受欢迎的原因。RMII 模式下的关键信号如下表所示实际连接时不同开发板引脚可能有差异以自己板子的原理图为准。LAN8720A 信号STM32F4 引脚常见方向说明TXD[1:0]PB12/PB13 或 PD4/PD5MCU - PHY发送数据TX_ENPB11 或 PD8MCU - PHY发送使能RXD[1:0]PC4/PC5 或 PD3/PD2PHY - MCU接收数据CRS_DVPA7 或 PD9PHY - MCU载波侦听/数据有效REF_CLKPA1输入外部时钟源提供50MHz 参考时钟MDCPC1MCU - PHY管理接口时钟MDIOPA2MCU - PHY管理接口数据这里最大的坑就是 REF_CLK。RMII 模式要求 50MHz 的参考时钟而且 STM32 的 ETH_RMII_REF_CLK 引脚通常是 PA1必须收到和 PHY 同源的时钟两边才能对齐数据采样时刻。常见时钟方案有两种。第一种是外部 50MHz 有源晶振直接给 LAN8720 的 XI 引脚提供时钟同时把 50MHz 送到 STM32 的 PA1。这种方案时钟最干净不容易出问题代价是多一颗有源晶振。第二种是 STM32 的 MCO 引脚输出 50MHz 给 PHY同时 PA1 也要接收这个时钟。MCO 方案省晶振但要求 MCU 主 PLL 正好能分频出精确的 50MHz不是所有主频都满足。比如系统主频 168MHz 时PLLCLK 是 168MHzMCO2 的预分频只能得到 168/284MHz、168/356MHz、168/442MHz都得不到 50MHz。如果硬要 MCO 出 50MHz得把 PLL 配置成 200MHz 再 4 分频或者用其他可整除的组合这就意味着 CPU 主频不是常规值得权衡。如果 REF_CLK 不稳定或者频率不准表现就是 PHY 能读寄存器、链路可能也能 up但数据收发全是错的ping 成功率极低调试起来非常痛苦。我的建议是除非你的板子原理图明确写了 MCO 方案且时钟树算过否则优先用外部 50MHz 有源晶振方案省心。2.2 复位时序与 PHY 地址确认LAN8720A 的硬件复位引脚是 nRST低电平有效。手册要求上电后 nRST 保持低电平一小段时间再拉高之后 PHY 才能正常工作。如果复位时间不够MDIO 读 PHY 寄存器会读出 0xFFFF或者表现出供电后第一次通信失败、按一下复位又好一阵的怪现象。稳妥做法有两种硬件上用 RC 延时电路比如 10k 电阻到 3.3V、1uF 电容到地复位脚接在电容端上电后电容充电产生几十毫秒的低电平保持软件上用 GPIO 控制初始化时拉低、延时 50ms 甚至 100ms 再拉高然后等待 PHY 完成内部复位。两种方案都用是最稳的尤其量产板要注意电源上升时间和复位时序的配合。PHY 地址这块特别容易被忽略。LAN8720 只有 PHYAD0 这一个地址配置引脚复位时芯片采样该引脚电平确定自己的 SMI 地址。PHYAD0 接地时地址是 0x00接高电平或通过跳线接 3.3V 时地址是 0x01。市面上常见的 LAN8720 模块有的默认拉低地址是 0有的模块通过跳线电阻配置成 1。如果你在代码里用地址 0 去读而实际 PHY 地址是 1MDIO 读回来的全是 0xFFFF。排查方法很简单看原理图确认引脚电平再用 SMI 命令扫描地址 0 和 1读到的 PHY ID 寄存器地址 2/3应该是 0x0007 和 0xC0F1 之类的值。2.3 硬件检查清单与走线经验在开始写代码之前强烈建议先按下面的清单把硬件快速过一遍MDIO 引脚是否有上拉电阻一般 10k 到 3.3VnRST 复位脚电平是否稳定低电平保持时间是否满足要求REF_CLK 是否有 50MHz 稳定时钟频率用示波器或频率计确认PHYAD0 引脚确定的 PHY 地址和代码里初始化时要匹配网络变压器的中心抽头电容、Bob Smith 端接是否按照数据手册接好电源 3.3V 去耦电容是否足够LAN8720 这种百兆 PHY 对电源纹波比价敏感。走线方面需要注意RMII 虽然信号不多但 REF_CLK 是 50MHz 的时钟信号走线要尽量短不要跨分割最好有完整的地平面。RXD、TXD 信号线也要尽量等长、远离晶振和电源电感。PHY 芯片到 RJ45 连接器之间的布局遵循PHY 靠近连接器、变压器紧挨连接器的原则。实际项目里我遇到过把 PHY 和 MCU 布线拉得太长导致链路能起来但速率上不去、偶发丢包的情况重新布局之后问题消失。3. 软件工程骨架源码选择、目录结构与内存策略3.1 源码获取与版本匹配移植 LWIP 有两条路。一条是用 CubeMX 直接勾选 FreeRTOS 和 LWIP生成一个可运行的模板工程然后在此基础上按需调整。另一条是手工从源码开始移植把 LWIP 源码、FreeRTOS 源码、适配层文件一个一个添加进工程。两条路不冲突我的建议是先手工理解流程再用 CubeMX 验证或者相反先用 CubeMX 跑通再回去读代码理解每一层的作用。如果手工移植推荐从 STM32CubeF4 固件包里提取资料。下载并解压后在Projects目录下找Applications/LwIP/LwIP_HTTP_Server_Netconn_RTOS这类例程里面有现成的ethernetif.c和sys_arch.c文件。ethernetif.c是网卡底层驱动接口负责 DMA 收发和 PHY 操作sys_arch.c是 LWIP 和 FreeRTOS 之间的操作系统抽象层。这两份文件是移植中最容易写错的部分有官方版本作为起点能省很多时间。LWIP 源码则在Middlewares/Third_Party/LwIP目录下包括src、system等子目录版本号通常在src/include/lwip/init.h的LWIP_VERSION宏里能看到。FreeRTOS 源码同样可以从 CubeF4 包里取也可以从官方仓库下载。核心部分是Source目录下的内核代码加上针对 Cortex-M4F 的portable/GCC/ARM_CM4F移植文件以及内存管理文件portable/MemMang/heap_4.c。3.2 工程目录组织方式一段典型的工程目录结构如下可以参考Project/ ├── Core/ │ ├── Inc/ │ ├── Src/ │ └── Startup/ ├── Drivers/ │ ├── CMSIS/ │ └── STM32F4xx_HAL_Driver/ └── Middlewares/ └── Third_Party/ ├── FreeRTOS/ │ ├── Source/ │ │ ├── include/ │ │ ├── portable/ │ │ └── MemMang/ │ └── CMSIS_RTOS_V2/ └── LwIP/ ├── src/ │ ├── api/ │ ├── core/ │ ├── include/ │ └── netif/ └── system/ └── OS/ └── FreeRTOS/ ├── sys_arch.c └── sys_arch.hethernetif.c可以放在Core/Src或者 LwIP 的 port 目录下看个人习惯关键是头文件路径要包含完整。文件路径设置不对编译时会报找不到lwip/opt.h或者FreeRTOS.h这是初学者最常见的编译错误。3.3 内存策略从 heap_4 到 DMA buffer 的安全区FreeRTOS 的内存管理文件有 heap_1 到 heap_5 几个版本。移植 LWIP 的项目强烈建议用 heap_4它支持内存块的释放和碎片合并。任务栈、队列、信号量这些内核对象都会从这个堆里分配。FreeRTOSConfig.h里的configTOTAL_HEAP_SIZE要根据项目实际需要配置一般网络应用建议给 32KB 到 64KB 起步如果跑 HTTP 服务器或 TLS 等占用大的功能还需要往上加。LWIP 协议栈自己也有内存管理机制默认情况下不占用 FreeRTOS 的堆。LWIP 内部有一个mem.c实现的堆管理器大小由lwipopts.h里的MEM_SIZE宏控制另外还有 pbuf 内存池由PBUF_POOL_SIZE控制。这两个和 FreeRTOS 的堆是各自独立的。好处是出了问题容易隔离排查任务创建失败看 FreeRTOS 堆pbuf分配失败看 LWIP 统计。还有一个特别容易踩的坑STM32F407 内部有 64KB 的 CCM RAM地址在 0x10000000这个内存区域 CPU 可以访问但 DMA 控制器访问不到。以太网 DMA 描述符和收发缓冲区如果定义在 CCM RAM 里收发功能会直接失效表现为 DMA 不工作、数据完全收不到。定义 DMA buffer 时一定要放在普通 SRAM 区域或者用属性声明时明确指定不放到 CCM。4. lwipopts.h 与 FreeRTOS 的协议栈对接配置的核心逻辑4.1 NO_SYS0 之后sys_arch 层要提供什么lwipopts.h是整个 LWIP 移植的配置核心。最关键的一个宏是NO_SYS决定 LWIP 是否使用操作系统。我们要在 FreeRTOS 环境下运行必须设置NO_SYS0。当NO_SYS0时LWIP 内部通过sys_arch.h和sys_arch.c调用操作系统原语。具体来说需要提供这几类接口sys_thread_new()创建线程LWIP 会用它创建tcpip_thread以及 netconn 接口需要的其他线程sys_mbox_new()、sys_mbox_free()、sys_mbox_post()、sys_mbox_fetch()邮箱操作LWIP 用它在线程之间传递消息sys_sem_new()、sys_sem_signal()、sys_sem_wait()信号量操作控制协议栈线程的阻塞和唤醒sys_mutex_new()、sys_mutex_lock()、sys_mutex_unlock()互斥锁保护临界资源sys_now()返回当前系统时间单位毫秒LWIP 的定时器机制依赖它。在 FreeRTOS 上实现这些接口时邮箱可以用队列来模拟信号量直接用 FreeRTOS 的二进制或计数信号量互斥锁用互斥信号量。ST 官方例程里的sys_arch.c已经把这些写好了直接拿来改改就能用。需要注意的是sys_now()不要用HAL_GetTick()之外的复杂实现简单可靠即可如果系统节拍配置成 1000Hz直接用xTaskGetTickCount()也可以。4.2 关键宏配置表与实际影响lwipopts.h里的宏非常多但不是每个都要改。下面列出移植到 STM32F4 FreeRTOS 时必须关注的配置每个宏的取值会直接影响网络性能和稳定性宏名称推荐值影响说明NO_SYS00 表示使用 RTOS1 表示裸机LWIP_NETCONN1启用 netconn APITCP/UDP 应用常用LWIP_SOCKET1启用 socket API更接近标准编程模型LWIP_DHCP1启用 DHCP 客户端按需配置固定 IP 可关LWIP_DNS1启用 DNS 客户端做域名请求时需要MEM_SIZE16384 起LWIP 内部堆大小太小会导致协议栈内存分配失败PBUF_POOL_SIZE24 起pbuf 池数量太小高负载时丢包严重MEMP_NUM_TCP_SEG32 起TCP 分段数量影响 TCP 发送队列深度TCP_MSS1460最大分段大小与 MTU 1500 匹配TCP_WND4 * TCP_MSSTCP 接收窗口窗口太小吞吐率上不去TCP_SND_BUF4 * TCP_MSSTCP 发送缓冲区大小ETH_RX_BUFFER_SIZE1536以太网接收缓冲区需覆盖最大帧 1518 对齐ETH_TX_BUFFER_SIZE1536以太网发送缓冲区ETH_RX_DESC_CNT8接收 DMA 描述符数量太少容易丢包ETH_TX_DESC_CNT8发送 DMA 描述符数量LWIP_STATS1开启协议栈统计调试时很有用量产可关这里的TCP_WND和PBUF_POOL_SIZE是最影响实际吞吐的。如果TCP_WND只有 1 个 MSS 大小TCP 协议受滑动窗口限制吞吐率会非常低大文件传输时可能只有十几 Mbps。建议起步配置TCP_WND 4 * TCP_MSS内存允许的话可以继续增大到 8 倍或 16 倍。PBUF_POOL_SIZE太小网络突发流量到来时 pbuf 申请失败数据包被丢弃表现就是 ping 偶尔超时、TCP 传输卡顿。4.3 优先级的分配原则FreeRTOS 下IRQ 优先级和任务优先级都影响网络稳定。STM32 的 NVIC 优先级数值越小优先级越高而 FreeRTOS 要求能被中断安全的 API 调用的中断其优先级数值不能小于configMAX_SYSCALL_INTERRUPT_PRIORITY。ETH 中断是触发信号量给协议栈喂包的关键优先级要合理设置一般建议取中间值比如数值 5 或 6对应 PendSV 和 SysTick 的数值要更低即优先级更高。如果 ETH 中断优先级设置得太高且中断服务里调用了 FreeRTOS 的 API可能导致系统调度异常。任务优先级方面tcpip_thread是 LWIP 协议栈的主线程建议设置成较高优先级但不要超过硬件实时任务。比如一个典型配置任务名称优先级栈大小字说明lwipTcpipTask34096LWIP 协议栈线程lwipEthInputTask21024从网卡收包喂给协议栈AppTask12048应用逻辑任务SysMonTask0512系统监控/日志如果你的应用是设备端实时控制任务需要最高优先级如果只是数据采集和上传tcpip_thread优先级可以适当调高减少网络处理延迟。5. ethernetif.c 驱动改写DMA 描述符、中断喂包与 PHY 操作5.1 low_level_init 里真正重要的事情ethernetif.c里low_level_init()是网卡初始化的核心函数。这个函数做的工作包括初始化 MAC 地址配置以太网外设的工作模式RMII初始化 DMA 描述符复位 PHY 并等待完成配置 PHY 的自协商模式。MAC 地址这一点要特别注意。很多参考代码里给的 MAC 地址是一个默认值如果直接全 0 或者和局域网内另一台设备冲突会导致 ARP 无法正常应答表现为 ping 不通或者时通时断。给每个设备分配一个全局唯一的 MAC 地址是最稳妥的批量产品可以在出厂时写入唯一编号程序启动时从备份区读取并配置。RMII 模式配置在ETH_InitStruct里关键字段是Eth_Mode ETH_MODE_RMII。如果这里配成了 MII而硬件实际走的是 RMIIMAC 无法正确解析 PHY 送来的信号链路状态可能正常但数据完全收不到。还需要检查ETH_InitStruct里的Eth_MediaInterfaceHAL 库里不同版本字段名可能不同用错就出问题。5.2 DMA 描述符与 pbuf 的映射关系LWIP 的收发路径和 DMA 描述符是紧密结合的。以接收为例DMA 描述符指向一块内存缓冲区当 PHY 收到以太网帧后DMA 会把数据写入这块缓冲区并更新描述符状态。ethernetif.c的接收处理流程大致是检查 DMA 描述符的状态确认是否有新帧到达调用HAL_ETH_GetRxDataBuffer()获取指向接收缓冲区的句柄获取帧长度分配一个 LWIP 的 pbuf把数据从 DMA 缓冲区拷贝到 pbuf调用netif-input(pbuf, netif)把包送入协议栈。注意这里的 pbuf 分配和 DMA 缓冲区是两套内存。DMA 缓冲区是静态分配的连续内存供 DMA 控制器直接写入pbuf 是 LWIP 协议栈管理的内存。数据需要从 DMA 缓冲区拷贝到 pbuf。有的移植为了省一次拷贝试图让 DMA 直接写入 pbuf 指向的内存这在某些 DMA 架构下可行但 F4 的 HAL 库里默认实现还是拷贝方式简单可靠性能影响对百兆网口来说完全可以接受。发送路径类似low_level_output()收到一个待发送的 pbuf 链从链上逐个取出数据拷贝到 DMA 发送缓冲区然后启动 DMA 发送。发送完成后要记得释放 pbuf否则会内存泄漏。我见过不少项目跑久了网络越来越慢最后查出来就是发送路径漏释放 pbuf。5.3 中断回调与 FreeRTOS 信号量的接法ETH 外设的接收完成中断需要在中断服务函数里处理。标准写法是void ETH_IRQHandler(void) { HAL_ETH_IRQHandler(heth); } void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef *heth) { BaseType_t xHigherPriorityTaskWoken pdFALSE; xSemaphoreGiveFromISR(s_xSemaphoreRx, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); }这里的中断服务只做了给信号量这一个动作实际的数据读取和 pbuf 分配放在以太网接收任务里完成。接收任务的主循环类似static void ethernet_rx_task(void *argument) { for (;;) { xSemaphoreTake(s_xSemaphoreRx, portMAX_DELAY); while (HAL_ETH_GetRxDataBuffer(heth, rxBuffer) HAL_OK) { HAL_ETH_GetRxDataLength(heth, framelength); process_received_frame(rxBuffer, framelength); HAL_ETH_BuildRxDescriptors(heth); } } }这种中断加任务的模式是典型的嵌入式网络驱动写法好处是中断里绝不进行重操作数据量大时不会长时间关闭其他中断导致系统卡顿。portYIELD_FROM_ISR保证了信号量释放后如果接收任务优先级足够高会立刻切换过去执行。5.4 PHY 状态读取与自协商处理LAN8720A 的自协商是 PHY 芯片自动完成的但软件要给它足够的时间和正确的启动流程。初始化时建议按这个顺序操作硬件复位后延时 100ms通过 SMI 向 PHY 的 BMCR 寄存器地址 0写入软复位命令等待 BMCR 的第 15 位自动清零表示软复位完成设置 BMCR 启动自协商或者直接使用默认的自动协商模式轮询 BMSR 寄存器地址 1的第 5 位等待自协商完成读取 PHY 状态寄存器确认协商得到的速率和双工模式。自协商完成时间不固定快的几百毫秒慢的可能要 2 到 3 秒这取决于对端交换机和网线质量。初始化代码里一定要有等待逻辑不能一上来就上报链路 up。否则应用层数据发出去了PHY 实际还没准备好就会出现初始化完成后立刻通信失败等几秒又恢复正常的现象。6. 避坑实录三次疑难故障的完整排查链路这一章记录我在实际调试中遇到的三个典型故障每个都花了不少时间把排查思路完整列出来比直接给结论更有参考价值。6.1 故障一MDIO 读 PHY 寄存器全部返回 0xFFFF现象是初始化代码里第一步读取 PHY ID 寄存器返回的值就是 0xFFFF任何地址都一样。排查链路先用万用表/示波器确认 PHY 的复位引脚电平。结果发现 nRST 在上电后很快就变高了但 MCU 代码里又对 PHY 做了一次软复位怀疑是时序问题用示波器测量 REF_CLK 引脚确实有 50MHz 波形。于是排除时钟问题检查 PHY 地址模块原理图上 PHYAD0 引脚通过一个 0 欧电阻接到了 3.3V说明地址应该是 0x01而代码里用的是 0x00改掉之后 MDIO 能正常读到 ID 了。根因就是 PHY 地址不匹配。这个坑非常普遍因为 LAN8720 模块的种类太多有的默认地址是 0有的是 1而且资料里往往不会明确写。建议移植时第一步就用 SMI 扫描 0 到 31 的全部地址打印出每个地址读到的 ID亲眼确认 PHY 在哪个地址上再写死到代码里。6.2 故障二PHY 能读 ID 且 Link 状态正常但 Ping 不通这个时候链路状态寄存器显示已经协商到 100M 全双工PHY 的 Link 灯也亮了但 PC 上 ping 开发板 100% 丢包。排查链路先看 RX 中断有没有触发。在HAL_ETH_RxCpltCallback里加一个计数器用串口打印。结果发现收到的帧非常少偶尔有一两帧用示波器抓 RMII 接口的 CRS_DV 和 RXD[1:0]发现在 PC 发起 ping 时CRS_DV 会拉高RXD 上也有数据跳动说明 PHY 已经把数据送到了 STM32接下来怀疑 STM32 的 MAC 配置。检查ETH_InitStruct发现Eth_Mode配的是 MII而硬件明明是 RMII。改成ETH_MODE_RMII之后再测试ping 通了回头总结为什么 Link 正常但收不到数据MII 和 RMII 的信号定义和时序完全不同MAC 用 MII 去解析 RMII 的帧自然解析不出来。但 PHY 的 link 状态是基于物理层信号的电平检测和 MAC 模式没关系所以 Link 灯照样亮。这个坑藏在配置结构体里比较容易忽略。核对ETH_Mode和Eth_MediaInterface字段时必须和原理图实际接口严格对应。6.3 故障三能 Ping 通但 TCP 大包传输极慢且周期性丢包现象是 ICMP 小包完全正常但用网络调试助手发 TCP 大包速度只有几 Mbps偶尔出现卡住几秒又恢复的现象。排查链路先用 LWIP 的统计功能打开LWIP_STATS和MEMP_STATS打印pbuf分配失败次数发现pbuf池耗尽的情况频繁出现看lwipopts.hPBUF_POOL_SIZE只配置了 8MEMP_NUM_TCP_SEG也是默认值。百兆网络下如果接收缓冲区不够突发流量一来就把pbuf池挖空了协议栈只能丢包把PBUF_POOL_SIZE加大到 32MEMP_NUM_TCP_SEG加到 32同时把TCP_WND从 2 个 MSS 调整到 4 个 MSS再测试速度快了很多接着继续观察发现传输一段时间后仍然会出现一次卡顿。用调试器查看 FreeRTOS 任务状态tcpip_thread的栈使用率接近 100%怀疑协议栈线程栈不够导致栈溢出。把tcpip_thread的栈加大到 4096 字后长时间传输稳定下来。这个故障的根因是多个配置项叠加pbuf 池不够、TCP 窗口太小、协议栈线程栈偏小。网络性能问题很少是单一原因需要对照统计数据和任务状态一项一项排除。6.4 排查工具与调试手段遇到网络问题我建议准备这些工具和手段示波器测 REF_CLK 频率、复位时序、RMII 信号完整性这是硬件问题排查的第一步串口日志在网卡初始化、PHY 状态读取、收发回调里打印关键信息成本低且有效LWIP 统计开启LWIP_STATS后通过netif或者调试函数导出协议的丢包、内存不足计数FreeRTOS 任务状态用vTaskList()或uxTaskGetStackHighWaterMark()监测任务栈使用率排查栈溢出简单 TCP/UDP 测试脚本在 PC 上用 Python 写几行代码就能连续收发数据观察丢包和吞吐变化。7. 实测验证与稳定性调优从能通到好用的距离7.1 基础连通性验证清单移植完成、灯光全绿之后先做一轮基础验证。推荐按下面的顺序测试测试项目操作方式通过标准静态 IP Ping开发板固定 192.168.1.10/24PC 同网段ping -t100 包 0 丢包RTT 小于 1ms大包 Pingping -l 1472无分片长时间无丢包UDP 收发网络调试助手向板子发 UDP 包板子回发双向无丢包TCP 收发PC 做 TCP 客户端/服务端板子对连传输大文件传输不中断DHCP 测试连接路由器开启 DHCP获取 IP 后 Ping 通自动获取 IP 并正常通信大包 ping 到 1472 字节是有讲究的1472 加上 IP 头 20 字节、ICMP 头 8 字节正好是 1500 的 MTU 上限能验证网络对最大帧的处理能力。如果大包 ping 不通而小包正常大概率是 MTU 配置或 RX 缓冲区大小问题。7.2 吞吐与压力观察基础功能通过后重点关注压力下的表现。用 TCP 传输大文件观察平均速度百兆以太网的理论极限是约 12.5MB/s实际能跑到 8 到 10MB/s 就已经说明协议栈和驱动配置相当合理了。如果速度长期低于 5MB/s优先检查这几个参数TCP_WND是否足够大窗口过小会导致确认往返延迟限制吞吐PBUF_POOL_SIZE和MEMP_NUM_TCP_SEG是否在满载时被耗尽观察统计计数TX/RX DMA 描述符数量是否充足编译优化等级是否设成了 -O0低优化会明显影响吞吐tcpip_thread优先级是否被更频繁的任务抢占导致协议栈处理不及时。压力测试建议跑半个小时以上同时观察设备温度、内存使用曲线和丢包计数排除初始正常、长时间运行后恶化的内存泄漏或描述符消耗问题。7.3 稳定性方面的几个进阶优化点基础功能都稳定之后如果产品要量产下面几个优化点值得做一是链路变化检测。很多移植代码只在初始化时读取一次 PHY 状态后面就不管了。如果网线被拔掉然后又插上PHY 会重新自协商但 LWIP 的网络接口还认为链路是 up 的发送的数据全部超时。建议用定时器每 500ms 或 1s 读一次 PHY 的 Link 状态发现变化时主动调用netif_set_link_down()和netif_set_link_up()同时重启 DHCP 或重新上报应用层状态。二是给协议栈任务喂看门狗。网络协议栈万一进入死循环或长时间阻塞独立看门狗能帮忙复位设备。但要注意喂狗时机要合理比如在tcpip_thread对应的回调里喂不能简单在主循环喂。三是关注 FreeRTOS 堆和 LWIP 内存的长期稳定性。内存泄漏是网络设备最隐蔽的问题建议在调试阶段每个小时记录一次内存水位如果持续下降重点排查发送路径的pbuf释放和netconn连接关闭分支。四是运行日志分级管理。不要把调试信息全部留到量产版本里串口日志打印本身会占用 CPU 时间影响网络吞吐。用日志级别宏控制开关上线版本关掉不必要的调试输出。7.4 常见问题速查表现象可能原因检查方向MDIO 读全部 0xFFFFPHY 地址错误、复位时序不足、MDIO 无上拉扫描地址、测量复位、检查上拉Link 正常但 Ping 不通MAC 模式配置错误、MAC 地址全 0、REF_CLK 不稳检查 RMII/MII 配置、MAC 地址、时钟频率小包正常大包不通RX 缓冲太小、MTU 配置不匹配调大ETH_RX_BUFFER_SIZE、确认 MTU瞬时卡顿pbuf 池耗尽、描述符不足、栈溢出看LWIP_STATS、任务栈余量速度上不去TCP 窗口小、CPU 优化等级低、描述符少调TCP_WND、-O2、增加描述符数量复位后偶发不通PHY 复位时间不够、未等待自协商完成加长等待时间、轮询 BMSR移植 LWIP 最忌讳一上来照着网上的代码猛抄然后出问题了再瞎猜。做这类工程我个人的习惯是先把硬件这几件事确认到位REF_CLK 频率、复位时序、PHY 地址再动手写软件软件层面遇到问题优先打开统计、看寄存器、量波形用数据说话。LAN8720A 这颗芯片本身很稳定坑大多出在时钟、复位和地址这三件小事上。把这三件事搞清楚F4 平台上移植 LWIP 2.1.2 基本就是一路顺风。