简介面向 STM32 嵌入式开发者的以太网通信示例工程基于 STM32F429IGT6 与 LAN8720A 以太网 PHY 芯片配合 HAL 库、STM32CubeMX 6.6.1 生成的工程骨架和 LwIP 2.1.2 协议栈在无操作系统环境中实现 PING 通主机的网络联通验证。工程包含 1300 个文件以 678 个 C 源码、450 个头文件为主另有启动汇编、链接脚本、工程配置及 Hex 烧录文件等压缩包约 31.16MB可直接查看库函数调用、PHY 寄存器读写和网络数据包处理逻辑。已有 1988 人浏览学习适合入门 STM32 以太网外设、RMII 接口时序或 LwIP 移植调试的开发者参考。通过工程源码可了解 LAN8720A 的 SMI 寄存器配置、RMII 数据收发路径以及无 RTOS 环境下 LwIP 的初始化流程对搭建同类以太网应用或排查 Ping 不通问题有直接帮助。1. 立项先别急STM32 加 LAN8720A 这套以太网组合装不好就是三天起步先把话说在前面在 STM32 的 HAL 库生态里以太网 RMII 接口加一颗 LAN8720A PHY 芯片再挂上无操作系统版 LWIP 协议栈是我见过翻车率最高的外设组合之一。不是因为难而是因为步骤太多任何一个环节错了现象都不是“报错”而是“PING 不通”你连从哪里下手都不知道。很多开发者拿着某开发板的例程抄改个引脚就烧进去结果网络完全沉默查了一天发现是 PHY 地址没对上。这类血泪经验我在项目里攒了不少。这篇文章面向的是手里已经有 STM32 最小系统板或者某款以太网扩展板、正准备自己接线和移植 LWIP 的 C 语言开发者。我会把 RMII 接线、CubeMX 初始化、LWIP 无操作系统集成、IP 和内存池配置、以及 PING 不通时的排查路径全部走一遍。如果你正好卡在“为什么我的板子 PING 不通”这篇文章就是给你准备的。装好一套能 PING 通的环境后面 TCP/UDP 通信才有地基。2. 硬件连接与初始化RMII 这 12 根线一根都不能错2.1 RMII 模式下 LAN8720A 与 STM32 的引脚对应关系LAN8720A 是常见的 10/100M 以太网 PHY 芯片支持 RMII 接口。RMII 相比 MII 线少但只有两种选择要么按标准接要么全部重来。RMII 需要 6 根数据/控制线TXD0、TXD1、TX_EN、RXD0、RXD1、CRS_DV再加一根 REF_CLK 时钟两根 MDIO 和 MDC 管理线以及一个 nRST 复位引脚。有的板子把 nINT 中断引脚也连出来了如果你不用中断可以悬空或者接 GPIO 输入。我一般先看 STM32 的数据手册里的 RMII 映射表但更可靠的方法是用厂商提供的图形化配置工具打开以太网外设它会自动把可用引脚标出来。以常见的某款 F407 芯片为例RMII 的 REF_CLK 只能在 PA1 上TXD0 是 PB12TXD1 是 PB13TX_EN 是 PB11RXD0 是 PC4RXD1 是 PC5CRS_DV 是 PA7MDC 是 PC1MDIO 是 PA2。这些引脚在不同的板子上可能被占用所以第一步就是对照原理图确认你的 PHY 芯片的每个信号接到了 STM32 的哪个引脚。接线的坑主要在 REF_CLK。LAN8720A 的时钟有两种模式一种是从 STM32 输出 50MHz REF_CLK 给 PHY另一种是 PHY 自己用 25MHz 晶振生成 50MHz 时钟并输给 STM32。我踩过最大的坑是某些扩展板把 REF_CLK 接到了 STM32 的 MCO1 引脚上但配置工具里默认却被我设成了 PA1 的以太网时钟输入结果时钟冲突PHY 完全没反应。正确做法是看原理图确认你的 REF_CLK 是“STM32 输出给 PHY”还是“PHY 输出给 STM32”然后在时钟配置里选对来源。另外LAN8720A 的 nINT 中断引脚如果没接就别在代码里使能 PHY 中断否则你会等处在一个永远无法触发的中断里。复位引脚也重要有的板子把 nRST 接到了 STM32 的普通 GPIO需要在初始化代码里拉低再拉高如果悬空PHY 状态可能不稳偶尔 PING 不通就是它引起的。2.2 用配置工具生成以太网外设初始化代码打开你的 HAL 库工程先在图形化配置工具里找到以太网外设ETH选择 RMII 接口模式。然后按上面确认的引脚关系把 TX、RX、MDIO、MDC、REF_CLK 和 CRS_DV 都分配到对应引脚。分配完之后核对一下 PINOUT 视图看看有没有冲突的复用功能。接下来是时钟配置。以太网外设需要 50MHz 的 REF_CLK以及 25MHz 的 PHY 时钟如果 PHY 使用外部晶振则不需要 STM32 提供。常见的配置是把系统时钟设为 168MHzF407 典型值然后让 ETH 的时钟源来自 PLL 的 50MHz 输出。我在配置时会在“Clock Configuration”里把 ETH 的时钟源选为 PLLQ并确认输出是 50MHz。如果这里没设对生成代码后再怎么调 LWIP 都是白搭因为 PHY 根本没有时钟工作。生成代码后你会得到一个eth.c文件里面有MX_ETH_Init函数。这个函数会填充一个ETH_InitTypeDef结构体包括 PHY 地址、自动协商、速度和双工模式等。我通常会把自动协商打开这样插上网线时能自适应 10/100M。但注意无操作系统版 LWIP 中PHY 的初始化并不是只靠这个函数就完成的HAL 库的以太网驱动里还需要你提供HAL_ETH_MspInit和HAL_ETH_MspDeInit这两个回调函数引脚和时钟的初始化如果配置工具没有帮你生成需要自己补写。void HAL_ETH_MspInit(ETH_HandleTypeDef *heth) { GPIO_InitTypeDef GPIO_InitStruct {0}; if (heth-Instance ETH) { __HAL_RCC_ETH_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE(); /* 配置 REF_CLK、MDIO、CRS_DV 等引脚 */ GPIO_InitStruct.Pin GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_VERY_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); /* 配置 TXD0、TXD1、TX_EN */ GPIO_InitStruct.Pin GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); /* 配置 RXD0、RXD1 */ GPIO_InitStruct.Pin GPIO_PIN_4 | GPIO_PIN_5; HAL_GPIO_Init(GPIOC, GPIO_InitStruct); } }这个函数里的引脚分组必须和你的实际接线一一对应。很多人直接复制例程的MspInit结果自己的板子引脚不一样以太网自然是死的。看没看到__HAL_RCC_ETH_CLK_ENABLE()这行是使能以太网外设的时钟漏了它后面的任何寄存器操作都会触发 hardfault。另外引脚模式要设成GPIO_MODE_AF_PP和GPIO_SPEED_FREQ_VERY_HIGHRMII 信号频率高速度等级不够会波形畸变。2.3 PHY 地址与复位时序的确定方法LAN8720A 的 PHY 地址通常由芯片的 PHYAD0 和 PHYAD1 引脚电平决定。常见的模块上这两个引脚都接地所以地址是 0x00也有的是接上拉地址变成 0x01。我不知道你的板子是什么样所以我一般会先看原理图找不到的话就去读芯片手册手册里会有 PHYAD[1:0] 的真值表。在MX_ETH_Init里phyAddress字段就填这个地址。如果填错了HAL 库在HAL_ETH_Init阶段去读 PHY 的 BSR 寄存器时读回来的全是 0xFF 或者 0x00HAL_ETH_ReadPHYRegister就会返回超时错误。我见过有人在这个字段填了 0x01结果整整折腾了一个晚上最后才发现模块上的 PHYAD0 和 PHYAD1 其实是悬空内部下拉接地地址是 0x00。复位时序也是个容易被忽略的点。很多板子把 nRST 接在 STM32 的一个 GPIO 上但这根线如果不做控制PHY 上电后可能处于复位状态也可能不在。稳妥的做法是在main函数里在MX_ETH_Init之前手动拉低复位引脚延时 10 毫秒再拉高然后再初始化 ETH。如果复位引脚没连接那就要靠 PHY 自带的电源上电复位电路一旦上电时序不满足后续所有寄存器操作都会飘忽不定。HAL_GPIO_WritePin(GPIOE, GPIO_PIN_1, GPIO_PIN_RESET); HAL_Delay(10); HAL_GPIO_WritePin(GPIOE, GPIO_PIN_1, GPIO_PIN_SET); HAL_Delay(50); MX_ETH_Init();这段代码的前面两行是强制让 PHY 重新复位第三行等它稳定后面才初始化以太网外设。注意复位引脚是哪个脚不同板子差异极大。如果 HSE 起振慢建议把复位后的延时加大到 100 毫秒。这种情况下HAL_Delay 依赖 SysTick如果你在初始化 LWIP 之前禁用了 SysTick 中断延时函数会卡死这是另一个我踩过的坑。3. 移植 LWIP 无操作系统版三大文件的取舍与改动3.1 LWIP 源码的版本选择与目录结构在无操作系统的裸机环境下LWIP 以源码形式嵌入到工程中。我不建议你从零去网上下载最新版 LWIP而是优先使用芯片厂商固件包里自带的 LWIP 版本。因为厂商已经把它与 HAL 库的以太网驱动做了适配你只需要复制目录并加入工程即可。以某个 HAL 库开发包为例它的Middlewares/Third_Party/LwIP目录下就有现成的src和system两个子目录。src是 LWIP 核心协议栈system里是厂商提供的移植文件包括ethernetif.c和lwipopts.h的默认配置。版本太新反而可能带来问题。我试过把 LWIP 从厂商配套的 2.0.3 升级到 2.1.2结果ethernetif.c里的 API 有变更比如tcpip_thread_input变成了tcpip_input裸机版没有 TCP/IP 线程这些线程相关的接口就需要重新适配。所以在没有必须升级的理由时用厂商固件包里的配套版本最省心。如果确实要升级记得对比ethernetif.c中的low_level_input和low_level_output这两个函数看它们调用的 LWIP 核心函数是否仍然存在。目录放在工程里的方式我惯例是建一个LWIP文件夹把src和system都塞进去然后在 IDE 里把src/core、src/netif、src/api裸机其实用不到 api 层但编译需要头文件以及src/port的路径全部加进 include。如果你图省事只加了顶层src路径编译时会报找不到lwip/opt.h这种头文件就是因为路径层级不够。# 伪代码示意工程中的 include 路径 INCLUDES \ -I./LWIP/src/include \ -I./LWIP/src/include/lwip \ -I./LWIP/src/include/netif \ -I./LWIP/system \参数-I./LWIP/system别省ethernetif.c里会 includelwipopts.h和arch/cc.h这些都在 system 目录下。有些开发者把lwipopts.h放在用户目录然后手动在编译选项里加路径这种方式可行但容易和 system 目录下的默认配置冲突编译时出现重定义。我的习惯是只保留 system 目录下的一份lwipopts.h所有配置都改它别到处复制。3.2 lwipopts.h 中必须开和必须关的宏lwipopts.h是 LWIP 的配置总闸裸机环境下的配置比带系统时简单但有几个宏直接决定你能不能 PING 通。首先是NO_SYS裸机必须设为 1这样 LWIP 不会尝试调用任何与操作系统相关的信号量或线程接口。如果不设在无系统环境下编译会报一堆sys_sem_new之类的未定义错误。其次是LWIP_DHCP。如果你只是想先 PING 通内网固定 IP把 DHCP 关掉手动设置静态 IP。因为 DHCP 流程在裸机上需要额外的定时器处理如果定时器没配好DHCP 会反复超时PING 也进不了正常状态。等静态 IP 验证通过后再回头开 DHCP 也不迟。内存配置是另一个重点。MEM_SIZE决定 LWIP 内核堆大小PBUF_POOL_SIZE决定 PBUF 池数量PBUF_POOL_BUFSIZE决定每个 PBUF 的缓冲区大小。这些参数直接关系到大帧的收发。如果PBUF_POOL_BUFSIZE小于网卡最大帧长比如默认 1518 字节而你的 MTU 设成 1500加上以太网头 14 字节和 FCS 4 字节1518 是刚好的。如果PBUF_POOL_BUFSIZE只有 256那大报文会被丢弃PING 大包就会失败但小包 PING 正常这种玄学问题特别坑。#define NO_SYS 1 #define LWIP_DHCP 0 #define LWIP_STATS 1 #define LWIP_ICMP 1 #define LWIP_RAW 1 #define MEM_SIZE 1024 * 20 #define PBUF_POOL_SIZE 16 #define PBUF_POOL_BUFSIZE 1518 #define LWIP_NETIF_STATUS_CALLBACK 1LWIP_NETIF_STATUS_CALLBACK设为 1 之后你可以在网卡状态变化时得到回调调试链路起来很有用。LWIP_RAW如果不开某些调试工具无法使用原始套接字但不影响 ICMP。LWIP_STATS建议打开协议栈内部收发包统计会放在全局变量里出问题时直接看lwip_stats就能判断是哪个环节丢包。还有CHECKSUM_GEN_ICMP和CHECKSUM_CHECK_ICMP默认是 1不用动。我见过有人为了“增加性能”把 IP 和 ICMP 的校验全关了结果局域网内 PING 是通的但数据内容完全损坏跨网段必炸。除了优化场景千万别关校验。3.3 ethernetif.c 与 PHY 驱动low_level_init 里的硬核改动ethernetif.c是 LWIP 与 HAL 以太网驱动的桥梁它的核心函数是low_level_init。这里要完成几件事包括设置网卡 MAC、初始化描述符、设置 PHY 工作模式、注册 netif 的回调函数。我一般不会直接改整个文件而是把它复制到用户代码目录再修改里面的low_level_init。low_level_init里首先要调用HAL_ETH_Init这个函数会读 PHY 的 BSR 寄存器并配置自动协商。然后是手动设置描述符LWIP 裸机版通常使用连续内存块作为 DMA 描述符和缓冲区所以需要定义一个很大的数组比如ETH_TxDesc[ETH_TX_DESC_CNT]和ETH_RxDesc[ETH_RX_DESC_CNT]。HAL_ETH_Init之后你会调用HAL_ETH_ConfigDMA把这些描述符首地址配置给 DMA 控制器。static void low_level_init(struct netif *netif) { #if defined ( __ICCARM__ ) #pragma data_alignment4 #endif static uint8_t buffer[ETH_RX_DESC_CNT * ETH_RX_DESC_SIZE 4]; ETH.TxDesc (ETH_DMADescTypeDef *) (uint32_t) buffer[0]; ETH.RxDesc (ETH_DMADescTypeDef *) (uint32_t) buffer[ETH_RX_DESC_SIZE]; HAL_ETH_Init(heth); HAL_ETH_ConfigDMA(heth); netif-hwaddr_len ETH_MAC_ADDR_LEN; netif-hwaddr[0] 0x00; netif-hwaddr[1] 0x80; netif-hwaddr[2] 0xE1; netif-hwaddr[3] 0x00; netif-hwaddr[4] 0x00; netif-hwaddr[5] 0x01; netif-mtu 1500; netif-flags NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP; netif-output etharp_output; netif-linkoutput low_level_output; netif-tx low_level_output; }这一段里最容易出问题的是描述符的对齐要求。DMA 描述符的内存地址必须 32 字节对齐所以用#pragma data_alignment4其实不太够更严谨的做法是用__attribute__((aligned(32)))。我在 GCC 环境下不会用 pragma而是直接用 C 编译器的对齐属性。数组布局也有讲究ETH_TxDesc和ETH_RxDesc中间要留出描述符的空间代码里buffer[ETH_RX_DESC_SIZE]就是给 TxDesc 分配在 RxDesc 之后。MAC 地址在netif-hwaddr数组里填。我填的00:80:E1:00:00:01是通用的本地测试地址不要和局域网内其他设备冲突。有的路由器可能会过滤本地管理地址如果 PING 不通可以改成你真实网卡的 MAC 格式。netif-flags里必须有NETIF_FLAG_ETHARP否则 ARP 请求发不出去连自己都 PING 不通。PHY 驱动的部分HAL 库提供了HAL_ETH_ReadPHYRegister和HAL_ETH_WritePHYRegister。LAN8720A 的寄存器布局和通用 PHY 不完全一致。比如它的 BSR 寄存器地址是 0x01但比特位定义里Link Status 是 bit2而不是通用 PHY 的 bit2 也是 Link 状态这里倒是比较标准。但读取速度状态时LAN8720A 的 BCR 寄存器0x00里的 SPEED 位是 bit13和标准一致。我在写read_phy函数时会先读 PHY 的 ID 寄存器地址 0x02 和 0x03确认读出的值等于 0x0007 和 0x0080这是 LAN8720A 的标识。如果读出来完全不对大概率是 MDIO 引脚接错或者 PHY 地址错了。4. 核心配置IP、MAC 与内存池决定 PING 通的质量4.1 静态 IP 和 MAC 地址在代码里的填法LWIP 的网卡配置主要在tcpip_init如果是带系统的或者裸机里的lwip_init之后进行。无操作系统版里我们通常在main函数中调用netif_add来注册网卡同时传入 IP 地址、子网掩码和网关。这三个地址是写在ip4addr结构体里的别直接在netif_add里写字符串那样很多人会弄错字节序。struct netif lwip_netif; ip4_addr_t ipaddr, netmask, gw; IP4_ADDR(gw, 192, 168, 1, 1); IP4_ADDR(ipaddr, 192, 168, 1, 100); IP4_ADDR(netmask, 255, 255, 255, 0); netif_add(lwip_netif, ipaddr, netmask, gw, NULL, ethernetif_init, ethernet_input); netif_set_default(lwip_netif); netif_set_up(lwip_netif);netif_add最后一个参数是ethernet_input这是 LWIP 从网卡接收线程传包的入口。在无系统环境下你需要周期性地调用这个函数它会从 HAL 库的接收缓存中取出数据并送入 LWIP 栈。ethernetif_init则由 LWIP 在初始化时自动调用来执行low_level_init。注意顺序先netif_set_default再netif_set_up有些教程顺序反了默认网卡没设置路由时选择失败PING 网关就会报错。网关和 IP 要在一个子网内。如果你用的是电脑直连开发板而不是经过路由器网关可以填开发板自己的 IP也可以填电脑的 IP但要保证两个设备能互相 ARP。我调试时喜欢用 192.168.1.100 之类的高地址避免和电脑的 192.168.1.1 网关冲突。子网掩码别乱填 255.255.255.0如果你的电脑用的是 10.0.0.x 网段这里就得改。MAC 地址的作用容易被低估。同一个局域网里如果有另一台设备的 MAC 和你填的一样ARP 缓存会互相打架表现为 PING 时而通时而断。所以low_level_init里的netif-hwaddr一定要填一个独一无二的地址。我习惯用芯片的 UID唯一标识生成低 24 位 MAC高 24 位用一个固定的厂商前缀这样每块板子的 MAC 都不同。4.2 内存池参数对 PING 大包和并发的影响LWIP 在裸机环境下完全依赖内存池。PBUF_POOL_SIZE通常设成不少于 16太小的话当 CPU 正在处理上一个接收包而网卡又连续收到多个广播包时没有空闲 PBUFLWIP 会直接把新帧丢弃。因为丢包是静默的你会看到 PING 丢失但协议栈统计的lwip_stats.link.drop在增加。我调试时会临时把这个值加到 32确认问题是否消失然后再调回合理的数值。MEM_SIZE则影响 ARP 表、路由表和 UDP/TCP 控制块等内核内存。无系统环境下LWIP 使用一段静态数组作为内存堆这个数组大小就是MEM_SIZE。如果太小调用tcp_new()或udp_new()时会返回 NULL但 PING 不需要 TCP/UDP所以 PING 通并不代表MEM_SIZE够用。我建议在应用层开始加 TCP 服务器之前把MEM_SIZE从默认的 20K 至少放大到 32K否则后续跑连接会很痛苦。还有一个隐形参数是TCP_MSS它和 MTU 强相关。如果TCP_MSS设置超过PBUF_POOL_BUFSIZE - 40IP头TCP头TCP 分段就会出错。虽然 PING 走的是 ICMP不涉及 MSS但既然你是为后续通信打底此时应该把TCP_MSS设为 1460同时确认PBUF_POOL_BUFSIZE不小于 1500。不然等你调 TCP 的时候又得回头改这个不如现在一次到位。#define TCP_MSS 1460 #define PBUF_POOL_BUFSIZE 1518这两个值组合在一起表示一个 TCP 报文1460 字节载荷 20 字节 IP 头 20 字节 TCP 头能刚好放进一个 1518 字节的 PBUF。如果 PBUF 太小LWIP 会自动做分片发送但接收端重组会消耗额外的内存和时间。所以确保 PBUF 尺寸能装下最大 TCP 分段是减少丢包的关键。4.3 中断与轮询模式的选择裸机也可以不裸奔无操作系统不代表一定要用轮询。HAL 库的以太网驱动支持两种接收模式中断模式和轮询模式。在 LWIP 无系统版本中最常见的做法是打开以太网接收中断在中断回调里直接调用HAL_ETH_ReadData然后把数据投递给netif-input。但由于 LWIP 协议栈内部不是线程安全的在中断里直接调用ethernet_input很危险因为主循环可能正在修改 PBUF 链表。我推荐的做法是分两步。第一步在HAL_ETH_RxCpltCallback中把收到的帧复制到一个环形缓冲区然后置一个标志位。第二步在主循环里检测到这个标志位后调用ethernetif_input实际就是ethernet_input处理数据。这样既不受中断频繁触发的影响又能保证协议栈的操作都在主线程上下文。void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef *heth) { if (heth-Instance ETH) { g_eth_rx_flag 1; HAL_ETH_ReadData(heth, rx_buffer[0]); } } while (1) { if (g_eth_rx_flag) { g_eth_rx_flag 0; ethernetif_input(lwip_netif); } sys_check_timeouts(); }这里的HAL_ETH_ReadData会从 DMA 接收描述符中取出数据到rx_buffer。注意rx_buffer必须足够大至少PBUF_POOL_BUFSIZE。sys_check_timeouts()是 LWIP 的定时器处理函数你必须周期性地调用它否则 ARP 表中的旧条目不会过期DHCP 超时永远不会发生重传定时器也会卡住。我在裸机主循环中会把对它的调用放在最显眼的位置因为它和 PING 的延迟表现直接相关。ethernetif_input在无系统版本里被实现为一个宏或者直接调用ethernet_input。主循环里的调用频率不能太低否则接收帧在缓冲区里堆积丢包率会上升。我一般把主循环周期控制在 10 毫秒以内也就是每秒至少 100 次调用足以应对大多数低速网络流量。5. STM32 LAN8720A 避坑指南PING 不通先查这五处5.1 现象PING 完全不通PHY 的 LINK LED 都不亮原因基本上可以集中在三个方向PHY 没有上电、复位没有拉起来、或者 RMII 的 REF_CLK 没给到。先量 PHY 的供电电压LAN8720A 需要一路 3.3V 和一路 1.2V内部 LDO 可从 3.3V 转出。如果板子上有电源指示灯但 PHY 芯片上没有去看数据手册确认 LED 引脚是否有输出。另外用示波器量 REF_CLK 引脚应该有 50MHz 方波。没有波形就先检查接线的时钟来源。解决方法是如果是 STM32 输出时钟确认 ETH 的 PLL 配置为 50MHz如果是 PHY 晶振确认晶振是否起振直接换一颗 25MHz 晶振试试。我在调试这个现象时发现过最离谱的原因PHY 地址配置成了 0x00但板子的 PHYAD0 实际接高真实地址是 0x01。导致HAL_ETH_Init里读 PHY 寄存器失败代码直接跳到Error_Handler。不过看起来像“没初始化成功”而不是“PING 不通”但最终表现就是这个样子。解决方式很简单把phyAddress改成 0x01或者用 I2C 工具读一下 PHY 的寄存器地址。另外网络变压器也有影响。有的板子用的变压器是带中心抽头的自耦变压器和 STM32 的共模电平不匹配RMII 信号能发出但收发异常。这个不好排查只能看原理图确认变压器的类型。我用的板子没这个问题但如果你买的是杂牌扩展板优先看它的原理图里网络变压器是否标准。5.2 现象能 PING 通开发板但 PING 不通网关PING 通开发板但网关不通说明你的物理链路和 LWIP 转发没问题问题在 IP 配置或 ARP 处理上。先检查开发板的网关 IP 是否和路由器同一网段。我的板子 IP 是 192.168.1.100网关就应该是 192.168.1.1。如果网段不匹配LWIP 会认为网关不可达所有跨子网流量都会被丢弃。还有一个常见问题是NETIF_FLAG_ETHARP没设置。没这个标志LWIP 不会发送 ARP 请求自然也无法解析网关 MAC。检查一下netif-flags的赋值。再看etharp的定时器是否在运行ARP 老化靠sys_check_timeouts周期触发。如果主循环没调用sys_check_timeoutsARP 缓存里初始的条目过期后新的 ARP 请求永远发不出去。这种情况 PING 开发板没问题因为同子网不需要 ARP不对同子网也需要但你电脑的 ARP 缓存里已经有开发板了所以通但网关需要新的 ARP 请求然后就卡住了。解决在lwipopts.h里确保LWIP_ARP为 1并在主循环定时调用sys_check_timeouts()。把网关填对后再用arp -d清空电脑的 ARP 缓存重新 PING。5.3 现象PING 偶尔通、偶尔超时特别是传输大数据时这个现象往往不是协议栈问题而是底层驱动丢包。优先检查ethernetif.c中的low_level_output是不是用了阻塞等待 DMA 完成的方式。在无系统环境下发送函数会在循环里等待HAL_ETH_GetTxDataFreeDescCount大于 0但如果 RX 中断和主循环抢占不当可能 TX 也没有及时释放描述符。另外接收描述符数量太少也会导致丢包。如果ETH_RX_DESC_CNT是 4而你的网卡突发接收大量帧DMA 描述符被占满后来的帧就会丢弃。加大ETH_RX_DESC_CNT到 8 或 16 能明显改善。内存池PBUF_POOL_SIZE也要跟着加大否则 DMA 扔进来的帧没有 PBUF 可用一样丢。还有一点PING 发送间隔太短有些低成本的 PHY 芯片在 100M 模式下处理 auto-negotiation 和 link change 会有短暂延迟。我遇到过在热插拔网线后立刻 PING前几包必丢的情况。这属于 PHY 重新协商的物理行为可以在low_level_init里加一个网线插入检测HAL_ETH_ReadPHYRegister读 BSR 的 bit2等它稳定后再把netif_set_link_up调用出去。如果你没实现NETIF_FLAG_LINK_UP状态机那么链路刚插上时丢包就不可避免。解决方式在ethernetif.c中增加 PHY 链路状态检测任务或者至少在low_level_init里循环读 BSR 寄存器等到 bit2 置位后再返回。注意不要让这个等待时间太长否则netif_add会阻塞主流程。5.4 现象烧录程序后第一次 PING 必失败重启板子后才能通这是典型的 PHY 复位与初始化时序问题。芯片上电后内部 LDO 需要时间稳定如果你在main里上电后立刻初始化 ETHPHY 可能还没完成上电复位导致HAL_ETH_Init里读 PHY 寄存器失败或者读到零值。HAL 库的HAL_ETH_Init会等待超时但超时后不会返回错误因为很多驱动代码里忽略了返回值。这解释了我们第一次 PING 失败第二次复位板子后 PHY 已经充分上电所以能通。解决办法是在main函数启动最前面加 200 毫秒延时再进行MX_ETH_Init。如果你有复位引脚复位信号的从拉低到拉高也要在延时之后。把复位拉到低等 100 毫秒拉高再等 200 毫秒然后再初始化。我自己在项目中遇到过无复位引脚控制的板子只能用这种延时大法效果稳定。有人会在HardFault_Handler里看到错误但那其实不是 hardfault而是 HAL 库在 PHY 未就绪时反复尝试的循环超时。你在调试模式下可能会看到HAL_ETH_ReadPHYRegister的返回值是HAL_TIMEOUT那就说明 PHY 没有在预期时间内响应。加大延时重新上电就能避开。5.5 现象开发板能 PING 通但响应延迟高甚至超过 10msPING 延迟高先确认是不是你自己的主循环太慢。无系统 LWIP 的主循环要同时处理ethernetif_input和sys_check_timeouts。如果HAL_Delay或者某个传感器读取函数在主循环里占了 50 毫秒PING 响应自然卡顿。解决是不要把 PING 的接收处理和慢速任务放在同一个循环里或者把慢速任务挪到中断里。检查LWIP_ICMP的响应是否走了高效的路径。LWIP 的 ICMP echo reply 是在协议栈里直接构造报文不走 TCP/IP 的套接字层所以延迟不会太高。如果延迟高大概率是ethernetif_input没有被及时调用。把主循环里ethernetif_input的调用频率提升到至少 200Hz 以上也就是 5 毫秒一次PING 均值就能回到 1ms 内。另一个隐藏原因是HAL_ETH_ReadData里使用了memcpy把 DMA buffer 拷贝到 PBUF如果 DMA buffer 和 PBUF 都是 32 位对齐可以改用直接修改 PBUF 指针的方式避免拷贝。但那样做会破坏 LWIP 的内存管理代价太大。我觉得在裸机上 1ms 的拷贝延迟完全可接受不要因为优化引入内存越界。6. 装完怎么验证从命令行 PING 到寄存器级确认6.1 第一步用直接连接的网线做最小环境验证拿一根网线一头插开发板一头插电脑然后把电脑的有线网卡 IP 设为 192.168.1.1子网掩码 255.255.255.0。注意不要插路由器因为路由器的 DHCP 可能会给你分配一个不同网段的 IP然后又多了一层网关的变量。直连环境下你的 PING 就是对开发板的 ADDR 和 MAC 地址的直接验证。在电脑的命令行里先清掉 ARP 缓存然后把开发板烧录程序按复位键等待串口打印或者观察 PHY LED看到 LINK LED 亮起后开始 PINGping 192.168.1.100 -t-t是持续 PINGWindows 下的参数Linux 下用ping 192.168.1.100加 CtrlC 结束。观察是否每个包都有回复。我在这个阶段会把电脑网卡的网线拔插一次确认 PHY 能重新协商并且在热插拔后 PING 依然能通因为热插拔是生产环境里的必然事件。PING 通只能代表 ICMP 链路通不代表 LWIP 的 TCP 或者 UDP 正常。所以 PING 通过后下一步我会用电脑上的简单工具去连接开发板的 7 号 UDP 端口因为我在 LWIP 里开了 UDP echo如果 UDP echo 回复那就说明 DMA、PHY、LWIP 的完整收发链路都正常。如果没有 UDP echo再回头看ethernetif_input的调用频率是否足够。6.2 第二步抓包看 ARP 和 ICMP 的请求/回复序列在电脑上打开抓包工具我用的是某款开源多平台抓包工具选中网线所在的网卡开始捕获。然后再次 PING 开发板抓包里应该能看到这样的序列电脑发出 ARP 广播“谁是 192.168.1.100”开发板回复 ARP 应答然后电脑发 ICMP echo request开发板发 ICMP echo reply。如果只看到 ARP 请求不断重发没有应答说明开发板的 ARP 响应没有发出去那问题在 LWIP 的 etharp 或者板子的发送路径。如果 ARP 正常但你只看到 request 没有 reply问题在 ICMP 的处理。这样一层层剥很快就能定位。抓包时还要注意帧长度。标准 PING 默认包长 32 字节但加上 IP 头 20 字节和以太网头 14 字节总帧长为 66 字节。如果你用ping -l 1400发大包长度应该接近 1472 字节。如果大包发出去后没有回复说明 MTU 或 PBUF 尺寸有问题回到第 4 章设置PBUF_POOL_BUFSIZE至少为 1518。No. Source Destination Protocol Info 1 192.168.1.1 192.168.1.100 ARP Who has 192.168.1.100? Tell 192.168.1.1 2 192.168.1.100 192.168.1.1 ARP 192.168.1.100 is at 00:80:E1:00:00:01 3 192.168.1.1 192.168.1.100 ICMP Echo request 4 192.168.1.100 192.168.1.1 ICMP Echo reply上面的四条记录就是一次完整的 PING 成功序列。如果第 2 条不出现查看etharp是否禁用了如果第 4 条不出现检查统计变量lwip_stats.icmp或者etharp的发送计数是否增长这能告诉我是在哪个回调函数里丢的。6.3 第三步直接读写 PHY 注册表确认硬件状态如果抓包都正常但 PING 依然有丢包最后一步我会在调试器里直接读 PHY 的寄存器确认硬件协商状态。LAN8720A 的基本寄存器可以通过 HAL 库函数读取比如HAL_ETH_ReadPHYRegister我在串口命令里加一个调试命令输入phy就打印一次 0x01BSR和 0x00BCR的值。BSR 寄存器地址 0x01的 bit2 表示 Link Status为 1 表示链路已经建立。bit5 表示 Auto-Negotiation Complete为 1 表示自协商完成。BCR 寄存器地址 0x00的 bit12 表示 Auto-Negotiation Enable正常情况下应为 1。如果 BCR 的 bit12 是 0而 BSR 的 bit5 又是 0说明自动协商没有打开PHY 可能工作在强制模式这会干扰连接的建立。uint32_t phy_bsr, phy_bcr; HAL_ETH_ReadPHYRegister(heth, PHY_BSR_REG_ADDR, phy_bsr); HAL_ETH_ReadPHYRegister(heth, PHY_BCR_REG_ADDR, phy_bcr); printf(BSR0x%04X BCR0x%04X\r\n, (unsigned int)phy_bsr, (unsigned int)phy_bcr);如果读到 BSR 的 Link Status 一直为 0那说明物理层没到 link 状态检查网线、对端设备是否在同一个速率模式但通常直连电脑没问题。如果 BCR 的 Speed 位bit13和 Duplex 位bit8和你预期的网卡速率不匹配那可能是自协商失败可以尝试强制 100M 全双工。强制时把MX_ETH_Init里的自动协商关闭直接设置Init.MediumInterface ETH_MEDIUM_INTERFACE_RMII、Init.FullDuplex ETH_MODE_FULLDUPLEX、Init.Speed ETH_SPEED_100M这招在老旧交换机上很管用。从那以后我再也不在拿到板子的第一时间就烧程序开 PING。我会先跑一遍上面这三步直连小环境、抓包看 ARP/ICMP 序列、读 PHY 寄存器确认协商状态三板斧走完基本不会再有找不到头的玄学问题。尤其是 PHY 寄存器那个命令虽然平时用不上但一旦遇到疑难杂症它比瞎改代码有效十倍。希望这篇文章能帮你少走一轮弯路让那片以太网在 STM32 上稳定地响起来。本文还有配套的精品资源点击获取