news 2026/10/3 3:36:20

STM32F407+FreeRTOS移植LWIP实战:LAN8720A驱动与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F407+FreeRTOS移植LWIP实战:LAN8720A驱动与避坑指南

这套组合我在实际项目里已经折腾过一轮,最近又有朋友问起 STM32F4 上移植 LWIP 的事情,干脆把整个过程系统整理出来。芯片用的是带以太网 MAC 的 STM32F407,协议栈选 LWIP 2.1.2,RTOS 用 FreeRTOS,PHY 芯片是 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 是 168MHz,MCO2 的预分频只能得到 168/2=84MHz、168/3=56MHz、168/4=42MHz,都得不到 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 地址是 1,MDIO 读回来的全是 0xFFFF。排查方法很简单:看原理图确认引脚电平,再用 SMI 命令扫描地址 0 和 1,读到的 PHY ID 寄存器(地址 2/3)应该是 0x0007 和 0xC0F1 之类的值。

2.3 硬件检查清单与走线经验

在开始写代码之前,强烈建议先按下面的清单把硬件快速过一遍:

  • MDIO 引脚是否有上拉电阻,一般 10k 到 3.3V;
  • nRST 复位脚电平是否稳定,低电平保持时间是否满足要求;
  • 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.h

ethernetif.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_SYS=0 之后,sys_arch 层要提供什么

lwipopts.h是整个 LWIP 移植的配置核心。最关键的一个宏是NO_SYS,决定 LWIP 是否使用操作系统。我们要在 FreeRTOS 环境下运行,必须设置NO_SYS=0。

当NO_SYS=0时,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 表示使用 RTOS,1 表示裸机
LWIP_NETCONN1启用 netconn API,TCP/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,而硬件实际走的是 RMII,MAC 无法正确解析 PHY 送来的信号,链路状态可能正常但数据完全收不到。还需要检查ETH_InitStruct里的Eth_MediaInterface,HAL 库里不同版本字段名可能不同,用错就出问题。

5.2 DMA 描述符与 pbuf 的映射关系

LWIP 的收发路径和 DMA 描述符是紧密结合的。以接收为例,DMA 描述符指向一块内存缓冲区,当 PHY 收到以太网帧后,DMA 会把数据写入这块缓冲区并更新描述符状态。ethernetif.c的接收处理流程大致是:

  1. 检查 DMA 描述符的状态,确认是否有新帧到达;
  2. 调用HAL_ETH_GetRxDataBuffer()获取指向接收缓冲区的句柄;
  3. 获取帧长度,分配一个 LWIP 的 pbuf;
  4. 把数据从 DMA 缓冲区拷贝到 pbuf;
  5. 调用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 芯片自动完成的,但软件要给它足够的时间和正确的启动流程。初始化时,建议按这个顺序操作:

  1. 硬件复位后延时 100ms;
  2. 通过 SMI 向 PHY 的 BMCR 寄存器(地址 0)写入软复位命令;
  3. 等待 BMCR 的第 15 位自动清零,表示软复位完成;
  4. 设置 BMCR 启动自协商,或者直接使用默认的自动协商模式;
  5. 轮询 BMSR 寄存器(地址 1)的第 5 位,等待自协商完成;
  6. 读取 PHY 状态寄存器,确认协商得到的速率和双工模式。

自协商完成时间不固定,快的几百毫秒,慢的可能要 2 到 3 秒,这取决于对端交换机和网线质量。初始化代码里一定要有等待逻辑,不能一上来就上报链路 up。否则应用层数据发出去了,PHY 实际还没准备好,就会出现"初始化完成后立刻通信失败,等几秒又恢复正常"的现象。

6. 避坑实录:三次疑难故障的完整排查链路

这一章记录我在实际调试中遇到的三个典型故障,每个都花了不少时间,把排查思路完整列出来,比直接给结论更有参考价值。

6.1 故障一:MDIO 读 PHY 寄存器全部返回 0xFFFF

现象是初始化代码里第一步读取 PHY ID 寄存器,返回的值就是 0xFFFF,任何地址都一样。

排查链路:

  1. 先用万用表/示波器确认 PHY 的复位引脚电平。结果发现 nRST 在上电后很快就变高了,但 MCU 代码里又对 PHY 做了一次软复位,怀疑是时序问题;
  2. 用示波器测量 REF_CLK 引脚,确实有 50MHz 波形。于是排除时钟问题;
  3. 检查 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% 丢包。

排查链路:

  1. 先看 RX 中断有没有触发。在HAL_ETH_RxCpltCallback里加一个计数器,用串口打印。结果发现收到的帧非常少,偶尔有一两帧;
  2. 用示波器抓 RMII 接口的 CRS_DV 和 RXD[1:0],发现在 PC 发起 ping 时,CRS_DV 会拉高,RXD 上也有数据跳动,说明 PHY 已经把数据送到了 STM32;
  3. 接下来怀疑 STM32 的 MAC 配置。检查ETH_InitStruct,发现Eth_Mode配的是 MII,而硬件明明是 RMII。改成ETH_MODE_RMII之后,再测试,ping 通了;
  4. 回头总结,为什么 Link 正常但收不到数据:MII 和 RMII 的信号定义和时序完全不同,MAC 用 MII 去解析 RMII 的帧,自然解析不出来。但 PHY 的 link 状态是基于物理层信号的电平检测,和 MAC 模式没关系,所以 Link 灯照样亮。

这个坑藏在配置结构体里,比较容易忽略。核对ETH_Mode和Eth_MediaInterface字段时,必须和原理图实际接口严格对应。

6.3 故障三:能 Ping 通但 TCP 大包传输极慢,且周期性丢包

现象是 ICMP 小包完全正常,但用网络调试助手发 TCP 大包,速度只有几 Mbps,偶尔出现"卡住几秒又恢复"的现象。

排查链路:

  1. 先用 LWIP 的统计功能,打开LWIP_STATS和MEMP_STATS,打印pbuf分配失败次数,发现pbuf池耗尽的情况频繁出现;
  2. 看lwipopts.h,PBUF_POOL_SIZE只配置了 8,MEMP_NUM_TCP_SEG也是默认值。百兆网络下如果接收缓冲区不够,突发流量一来就把pbuf池挖空了,协议栈只能丢包;
  3. 把PBUF_POOL_SIZE加大到 32,MEMP_NUM_TCP_SEG加到 32,同时把TCP_WND从 2 个 MSS 调整到 4 个 MSS,再测试,速度快了很多;
  4. 接着继续观察,发现传输一段时间后仍然会出现一次卡顿。用调试器查看 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/24,PC 同网段,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 基本就是一路顺风。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 3:36:08

Flutter适配鸿蒙实战:桥接、音频与字幕同步全解析

年底接了个有点特殊的活儿&#xff1a;在鸿蒙设备上跑一个英语听力练习App&#xff0c;团队技术栈是Flutter&#xff0c;没有人写过一行ArkTS。当时市面上关于“Flutter跨端鸿蒙”的资料还很零散&#xff0c;大部分停留在“能不能跑”的层面&#xff0c;真正把业务流程跑通的案…

作者头像 李华
网站建设 2026/10/3 3:35:46

PostgreSQL SELECT FOR UPDATE SKIP LOCKED 源码解析与演进

做后端的人应该都遇到过这种场景&#xff1a;多个 worker 同时从一张任务表里取数据&#xff0c;大家都执行SELECT ... LIMIT 1准备认领任务&#xff0c;结果两个进程取到同一行&#xff0c;后面一个UPDATE要么长时间阻塞&#xff0c;要么干脆死锁报错。PostgreSQL 9.5 引入的S…

作者头像 李华
网站建设 2026/10/3 3:35:44

InnoDB缓冲池实战调优:从误判内存泄漏到精准运维

1. 从一次诡异的“内存泄漏”说起&#xff1a;分清操作系统缓冲池与数据库缓冲池先讲个我上周遇到的事。群里有人发截图&#xff0c;说Windows 11任务管理器显示“非分页缓冲池”占用高达4GB&#xff0c;怀疑数据库把内存泄漏了&#xff0c;疯狂重启MySQL&#xff0c;问题依然存…

作者头像 李华
网站建设 2026/10/3 3:35:44

MySQL SQL入门教程:从建库建表到增删改查的完整实战指南

刚接触后端开发的朋友&#xff0c;十有八九都会从数据库开始碰壁。“MySQL”这个名字天天听见&#xff0c;但是真要自己装一个、建几张表、写几条SQL语句&#xff0c;各种报错一下子就涌上来了。我最初踩坑的时候&#xff0c;最头疼的不是SQL语法记不住&#xff0c;而是不知道一…

作者头像 李华
网站建设 2026/10/3 3:34:58

FPGA数字频率计完整实战:VHDL设计、Quartus仿真与避坑指南

数字频率计这个项目&#xff0c;我愿称之为FPGA入门路上最有“性价比”的一课。名字听起来有点教材气&#xff0c;但真把它在开发板上跑起来&#xff0c;你会发现VHDL语法、时序设计、EDA工具链、甚至连模拟前端整形电路都被一张小电路串起来了。我当年做这个项目时&#xff0c…

作者头像 李华
网站建设 2026/10/3 3:34:54

ComfyUI JoyCaption 2 插件安装全攻略:本地图像描述打标工作流实操

ComfyUI 玩到一定阶段&#xff0c;你会发现最磨人的不是怎么把图算出来&#xff0c;而是怎么把图“说清楚”。做 LoRA 训练要打标&#xff0c;做图生文要理解画面&#xff0c;做自动化工作流要批量处理数据集——这些全都绕不开图像描述这一步。以前大家伙普遍用 WD14 Tagger 或…

作者头像 李华