ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32 LwIP网线插拔自动恢复:轮询与中断方案详解

STM32 LwIP网线插拔自动恢复:轮询与中断方案详解 说实话这标题我太有共鸣了。搞过 STM32 联网项目的工程师基本都栽过同一个跟头板子刚开始调通 LwIP 的时候网线插上 ping 得通拔了再插十几秒后怎么 ping 都没反应。接着就是关电源重上电网络又活了。客户那边一句“这设备断个网线就死机”你这个月基本就搭进去了。这个问题说穿了不是 LwIP 没移植好而是整个链路缺一个“感知网线插拔并通知协议栈”的机制。MAC 不知道网线断了PHY 知道但没人问LwIP 还傻乎乎地认为物理链路是通的。结果就是拔线之后协议栈里的 ARP 表、路由状态、DHCP 租约全停留在旧状态等网线再接回去这些“过期状态”就成了通信障碍。这篇文章就来把这些事掰开揉碎讲清楚。目标人群是那些已经把 CubeMX 生成的 STM32 LwIP 工程跑起来、但网线插拔恢复还不稳的开发者。你不需要精通 LwIP 源码只需要理解它判断链路状态的基本逻辑再照着我的思路把检测链路和通知协议栈这两件事做踏实插拔自动恢复就是水到渠成的事。1. 链路状态与协议栈脱节自动恢复到底难在哪很多人第一次遇到这个问题时都是懵的明明程序没跑飞串口还能打印 RTOS 任务调度信息为什么网络就是不通如果你用 Wireshark 在 PC 端抓包可能连 ARP 广播都发不出去或者发出去没人回。这时候最容易判断失误以为是板子的网口驱动坏了。1.1 最常见的现象拔线后再插上网络“看着像死了”我调试过的项目里这种“拔插后假死”通常分三种表现。第一种是 ping 不通但板子程序本身完全正常。串口照常输出日志别的功能按键都响应就是网络不通。这种情况十有八九是链路状态没通知到协议栈。LwIP 内部维护了一个NETIF_FLAG_LINK_UP标志位协议栈发包之前会看这个标志。网线断了以后没人把它清掉协议栈照样往网卡描述符里塞数据包这些包全发到一根断了的线上同时收包路径也不刷新 ARP 表于是整个网络栈就“滞留”在通联状态。第二种是 DHCP 拿不到地址。拔线之前板子通过 DHCP 拿到了一个租约拔线之后如果链路没被协议栈感知DHCP 状态机一直停在 BOUND 状态永远不会重新发起 Discover。等网线插回来原来的 IP 可能还在有效期但交换机侧、路由器侧对这个 MAC 的绑定关系已经变了这个旧 IP 就成了“僵尸地址”别人 ping 不通板子自己也收不到任何回包。第三种是最坑的TCP 长连接断开后应用层不重连。比如板子和服务器保持一个 MQTT 连接拔线后服务端早把连接断开了板子这边 TCP 状态还挂在 ESTABLISHED。等链路恢复板子想发数据TCP 重传个几百次也收不到 ACK业务层如果没做超时重连这个设备就相当于永久离网了。1.2 自动恢复要解决的三层问题做自动恢复不是简单检测到网线插入就完事它实际是一个三层问题。物理链路层要能感知插拔事件。这一层由 PHY 芯片负责它有一个 Link Status 状态位网线断开后这个位会从 1 变 0接上后会变回 1。但 PHY 不会主动告诉你“网线断了”这件事需要控制器主动去读寄存器或者接一根 PHY 的中断引脚让 PHY 主动喊你。这就是第一步链路状态检测。MAC 和 DMA 数据通路要注意稳定性。拔线瞬间如果正好有大量数据收发DMA 描述符可能进入死锁状态有些 PHY 还会在拔线瞬间产生连续的错误中断。这一层如果处理不好即使链路状态检测对了后面收发数据还是会卡死。所以拔插恢复和网卡驱动的容错处理经常是一起做的。协议栈和应用层要做出响应。检测到链路变化后必须调用 LwIP 提供的netif_set_link_down()和netif_set_link_up()通知协议栈。只有这样LwIP 才会更新链路标志、重新触发 DHCP、让 TCP 连接快速失败并触发应用层的重连逻辑。这层没做前面链路检测做得再准也白搭。2. 移植前理清角色MAC、PHY、LwIP 各自该干什么想通第一部分的逻辑后你应该明白了自动恢复的关键不在 LwIP 本身而在“链路检测”和“协议栈联动”。但在写代码之前我建议你再花十分钟把 MAC、PHY、LwIP 这三者的分工捋一遍。很多人就是在这一步没想清楚后面调试的时候东改一个宏、西改一个回调越改越乱。2.1 数据通路与状态通路两条完全不同的路STM32 的以太网模块里ETH 外设承担 MAC 层功能负责和外部 PHY 通过 MII/RMII 接口交换数据同时通过 SMI 接口管理 PHY 的寄存器。画个不太严谨但很好懂的类比MAC 像公司前台负责收发快递以太网帧但不认识快递里面的内容PHY 像门口的保安负责看电缆有没有接好网络电压信号正不正常LwIP 像公司的业务员只关心快递内容里的 IP 层、 TCP/UDP 层信息至于“大门开没开”这件事它默认前台会告诉它。这三者的关系一定要心里有数数据包走的路径是“LwIP → MAC → PHY → 网线”但网线插拔这个状态变化只能通过“PHY 寄存器 → SMI 接口 → MAC → LwIP”这条状态通路传上来。也就是说你在 LwIP 里面怎么改配置都替代不了对 PHY 寄存器的读取。反过来说如果你只读到了 PHY 的 Link 状态却不告诉 LwIP协议栈还是活在梦里。2.2 CubeMX 里 ETH LwIP 的关键配置用 CubeMX 生成工程的时候很多人图省事直接默认配置。这里我提醒几个和自动恢复强相关的点。PHY 地址要填对。LAN8720A 的默认地址通常是 0x00DP83848 通常是 0x01KSZ8081 可以通过引脚配置成 0x00 到 0x1F。CubeMX 里有 PHY Address 这一栏一定要和你的硬件设计一致。我见过一个项目CubeMX 里填的 0x00实际板子上 PHY 地址是 0x01结果读出来的 Link 状态永远是 0插拔检测自然全废。RMII 模式下PHY 的 REF_CLK 50MHz 时钟源要确认好。有些板子用的是 STM32 MCO 引脚输出有些用的是外部有源晶振直连 PHY。如果时钟不对PHY 可能能读到寄存器但数据收发永远不通这种问题排查起来极其恶心。生成工程后打开 lwipopts.h检查有没有#define LWIP_NETIF_LINK_CALLBACK 1。这个宏默认在很多 CubeMX 生成的工程里是没开的但它恰恰是 LwIP 支持链路状态回调的总开关。没开这个宏即你后面调用了netif_set_link_up/down也不会触发你注册的链路回调函数应用层就感知不到插拔变化。2.3 常见 PHY 的寄存器速查Link 状态读哪里先给一张我用得最多的表方便你查PHY 型号常见 PHY 地址Link 状态寄存器Link 状态位备注LAN8720A0x00寄存器 1 (BSR)bit21Link Up常用低功耗设计要额外处理DP838480x01寄存器 1 (BSR)bit21Link Up经典10/100M PHYKSZ80810x01~0x1F寄存器 1 (BSR)bit21Link Up引脚可配地址KSZ90310x02寄存器 1 (BSR)bit21Link Up千兆 PHY注意速率协商注意上面这些 PHY 的寄存器 1 都遵循 IEEE 802.3 标准bit2 是 Link Status。如果你用的是比较偏门的 PHY第一件事是去查数据手册别凭经验猜。部分车载以太网 PHY如百兆 T1 系列的链路状态位就不是这个位置。读取寄存器的方式很简单HAL 库已经封装好了uint32_t phy_val 0; HAL_ETH_ReadPHYRegister(heth, PHY_ADDRESS, 0x01, phy_val); uint8_t link_up (phy_val (1U 2)) ? 1 : 0;2.4 自动恢复由谁来检测更合适很多初学者会想我能不能在 LwIP 的eth_rx_thread或者网卡接收回调里顺便读 PHY 寄存器我的建议是别这么干。接收线程的职责是处理网络数据任何阻塞式的 SMI 读操作都会影响收包实时性。而且网线插拔事件是低频事件没必要放进高频路径。正确的做法是独立一条检测路径两种主流实现轮询方式和中断方式。下面一章专门把这两种方案的原理、代码和取舍讲清楚。3. 链路检测的两套方案轮询与中断我分别怎么选在实际项目里我见过有人用定时器每 10ms 读一次 PHY 寄存器也见过有人把 PHY 中断引脚接 STM32 的 EXTI各有各的坑。这章我把两种方案都展开讲代码可以直接抄。3.1 方案一轮询方案简单可靠适合裸机和 RTOS轮询方案的思路很直接周期性地读 PHY 的 Link 状态位发现状态和上次不一样就触发 LwIP 的链路切换。它最大的优点是逻辑简单不依赖 PHY 的中断引脚和配置只要 SMI 通信正常就能工作。我一般用独立任务或者在主循环里做轮询周期在 200ms 到 500ms 之间。为什么不能太快因为 PHY 的 Link 状态在网线刚插上时有一个自动协商过程100M 链路协商通常需要 1~3 秒千兆更久。如果轮询太快可能在链路还没稳定时就读到一个中间状态反而容易产生“Link up 后立刻 Link down”的抖动。太慢也不行用户体验差插上半天才恢复。先写一个检测函数#define PHY_ADDRESS 0x00U #define PHY_REG_BSR 0x01U #define PHY_LINK_STATUS_BIT (1U 2U) static uint8_t eth_check_phy_link(void) { uint32_t reg_val 0; if (HAL_ETH_ReadPHYRegister(heth, PHY_ADDRESS, PHY_REG_BSR, reg_val) ! HAL_OK) { return 0; } return (reg_val PHY_LINK_STATUS_BIT) ? 1 : 0; }轮询的核心逻辑void ethernet_link_poll(void) { static uint8_t last_link_status 0xFF; // 初始化为一个不可能的值 uint8_t current_status eth_check_phy_link(); if (current_status last_link_status) { return; } last_link_status current_status; if (current_status) { netif_set_link_up(gnetif); } else { netif_set_link_down(gnetif); } }这个函数放在哪调用看你的工程结构裸机工程放在 while(1) 主循环里配合HAL_GetTick()判断 300ms 间隔RTOS 工程可以专门建一个link_check_task任务里osDelay(300)循环后调用一次。还有一个细节容易踩坑有的 PHY 的 Link 状态位是锁存式的也就是它检测到 Link Down 后会锁住 0直到你读了一次它才恢复实时状态。如果代码只读一次就可能一直认为链路是断的。所以我在项目里有一个习惯连续读两次 PHY 寄存器两次结果一致才确认状态。这个防抖逻辑很土但非常实用。static uint8_t eth_check_phy_link_debounce(void) { uint8_t cnt 0; for (uint8_t i 0; i 3; i) { cnt eth_check_phy_link(); HAL_Delay(20); } return (cnt 2) ? 1 : 0; }3.2 方案二中断方案响应最快但多一个引脚多一份配置如果产品对链路恢复时间有硬性要求或者你希望网线一插上应用层尽快感知就要用到 PHY 的中断引脚。绝大多数 PHY 都有一个 INT 引脚配置相应寄存器后Link 状态变化时引脚会输出脉冲或拉低电平。以 LAN8720A 为例它的中断配置寄存器在寄存器 17Interrupt Mask和寄存器 18Interrupt Status需要写寄存器使能 Link Down 和 Link Up 中断#define PHY_REG_INT_MASK 0x11U #define PHY_REG_INT_STATUS 0x12U // 使能 Link Up Link Down 中断 HAL_ETH_WritePHYRegister(heth, PHY_ADDRESS, PHY_REG_INT_MASK, 0x0002U | 0x0004U);硬件上PHY 的 INT 引脚接到 STM32 的一个 EXTI 输入引脚。中断触发方式建议用下降沿触发或低电平触发具体看 PHY 手册。配置好之后中断服务函数里不要直接读 PHY 寄存器因为 MDIO 读操作耗时且可能阻塞正确的姿势是给任务发一个信号量extern osSemaphoreId_t link_semaphore; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin PHY_INT_PIN) { osSemaphoreRelease(link_semaphore); } }然后在链路检测任务里等信号量void ethernet_link_task(void *argument) { uint8_t last_status 0xFF; for (;;) { osSemaphoreAcquire(link_semaphore, osWaitForever); // 稍微延时等 PHY 状态稳定 osDelay(50); uint8_t current_status eth_check_phy_link_debounce(); if (current_status last_status) { continue; } last_status current_status; if (current_status) { netif_set_link_up(gnetif); } else { netif_set_link_down(gnetif); } } }有些 PHY 支持配置为只在状态变化时给出一个短脉冲所以任务里加一层防抖非常必要。中断方式最大的好处是链路变化后 MCU 能在几十毫秒内感知不用干等轮询周期缺点是占一个引脚而且 PHY 的寄存器配置比轮询方式复杂。3.3 两套方案的取舍我直接把两套方案的对比列出来对比维度轮询方案中断方案实时性取决于轮询周期一般 300ms 内毫秒级响应硬件资源不需要额外引脚需要 PHY INT 引脚软件复杂度很低较高需要调 PHY 中断寄存器稳定性高防抖后很难误判受 PHY 中断配置影响个别 PHY 有中断丢事件问题调试难度较低较高中断寄存器手册要看仔细适用场景绝大多数产品尤其对功耗不敏感对链路恢复时间有硬要求的场景做产品我建议首选轮询。理由很实际中断方案收益不大但坑不少。比如有些 PHY 在低功耗模式下中断输出逻辑会变干扰系统唤醒还有些 PHY 的中断状态寄存器读后清零的时序没处理对会出现中断永远触发不上的怪问题。轮询多花的那几十毫秒在绝大多数业务场景里根本感知不到。4. 链路状态如何正确传给 LwIP 协议栈前面讲的都是让 MCU 知道网线插拔了但还差最关键的临门一脚把状态告诉 LwIP并且让应用层也能感知到。这章是整篇文章的核心价值所在也是很多人卡壳的地方。4.1 回调总开关LWIP_NETIF_LINK_CALLBACK 不能漏LwIP 里判断一个网口是否可用的标志是NETIF_FLAG_LINK_UP它定义在 netif 的 flags 里。你可能不会直接操作这个标志位因为 LwIP 提供了两个标准 APIvoid netif_set_link_down(struct netif *netif); void netif_set_link_up(struct netif *netif);这两个函数做的事包括修改NETIF_FLAG_LINK_UP标志、维护链路状态统计、触发链路回调。但要注意netif_set_link_up/down内部触发用户回调的前提是编译宏LWIP_NETIF_LINK_CALLBACK已经开启。打开你的 lwipopts.h确认这一行存在#define LWIP_NETIF_LINK_CALLBACK 1CubeMX 生成的工程里有些版本默认没开我见过好几个开发者在那里调了半天回调函数就是不执行最后发现是宏没定义。这个检查放在最开始做省得后面做无用功。4.2 调用时机和线程安全别在主中断里乱调接着就是那两个函数在哪调用的问题。我在前面代码里直接调用了但这里要特别提醒线程安全。LwIP 有两种锁机制一种是用户自己加的LWIP_ASSERT_CORE_LOCKED核心锁另一种是 tcpip 线程专用的信号量保护。netif_set_link_up/down在函数定义里没有强制要求在 tcpip 线程上下文调用但你要明白它们内部会修改 netif 状态的。如果你的工程用了 RTOS链路检测任务和 tcpip 线程是两个不同任务直接调用可能会造成资源竞争。最稳妥的做法是加锁包一层void app_netif_set_link_up(void) { LOCK_TCPIP_CORE(); netif_set_link_up(gnetif); UNLOCK_TCPIP_CORE(); } void app_netif_set_link_down(void) { LOCK_TCPIP_CORE(); netif_set_link_down(gnetif); UNLOCK_TCPIP_CORE(); }如果你的 LwIP 配置了NO_SYS1裸机模式整个协议栈都在主循环里跑不存在并发现象你可以直接调用。但只要用了 RTOS我建议就别省这个锁。4.3 注册链路回调让应用层也收到通知netif_set_link_up/down还有一个隐藏功能会调用你注册在 netif 上的link_callback函数。这个回调是在链路状态变化的瞬间同步执行的适合用来做“通知应用层”的事但不适合做耗时操作。注册代码一般在 LwIP 初始化完成后执行gnetif.link_callback ethernet_link_status_changed;回调函数长这样static void ethernet_link_status_changed(struct netif *netif) { if (netif_is_link_up(netif)) { printf([lwip] link up\r\n); // 通知 MQTT / TCP 客户端任务重新建连 osMessageQueuePut(app_event_queue, APP_EVT_LINK_UP, 0, 0); } else { printf([lwip] link down\r\n); // 通知业务任务停止发送数据 osMessageQueuePut(app_event_queue, APP_EVT_LINK_DOWN, 0, 0); } }这里我强调一点回调里尽量不要直接做什么网络发送的操作。因为此时链路刚恢复LwIP 的协议栈可能还没完全从 down 状态恢复立刻发数据很容易失败。正确的做法是发一个事件给业务任务让业务任务自己决定什么时候重连、重连几次、怎么处理断线期间的缓存数据。4.4 链路恢复后 TCP 和 DHCP 的真实表现做完前面这些LwIP 对网线插拔的响应就有了一套完整闭环。实测效果一般是这样的ping 直连场景下网线插回后 3 到 5 秒内可以重新 ping 通。因为 ARP 表在 link down 时会被清掉或者标记无效链路恢复后第一次 ping 会触发新的 ARP 请求。DHCP 场景下链路恢复后 LwIP 的 DHCP 客户端会自动重新走 Discover/Offer/Request/Ack 流程一般 10 秒左右能拿到地址。这里要啰嗦一句如果你的应用对 IP 变化敏感比如服务器端绑定了设备 IP链路恢复后拿到的可能是新 IP业务层要做适配。TCP 长连接不会因为链路恢复而自动重连。LwIP 只负责把链路层状态修正TCP 连接是应用层的资源应用层必须在链路回调里决定是等 TCP 重传超时还是主动关闭 socket 重新连接。我自己写业务代码时习惯在 link down 事件里直接关闭旧 socketlink up 事件里延迟 3 秒重新拨号这样恢复最干净。5. 一套完整可抄的自动恢复实现前面几章已经把原理讲透了这章把工程级的实现串起来。手把手从 CubeMX 工程结构出发整理一份可以照着抄的代码框架同时也把我调试时踩过的一些细节标注出来。5.1 基于 CubeMX 生成的工程结构该怎么组织CubeMX 生成的以太网工程里LwIP 初始化在lwip.c的MX_LWIP_Init()函数中里面定义了全局变量gnetif。这个gnetif就是后面所有链路操作要用到的网卡对象。你在自己的应用代码里可以直接extern struct netif gnetif;不需要自己再创建一个 netif 结构体。链路检测的代码我建议单独放一个文件比如eth_link_monitor.c不要把逻辑全堆在 main.c 里。这样模块边界清晰后续换 PHY 型号时只需要改这一个文件里的寄存器配置。5.2 完整实现轮询版本我先给出一个适合大多数项目的轮询版本。这个版本基于 FreeRTOS CMSIS_OS2CubeMX 生成标准工程后把下面代码放进一个独立 C 文件即可。eth_link_monitor.h#ifndef __ETH_LINK_MONITOR_H #define __ETH_LINK_MONITOR_H void ethernet_link_monitor_init(void); void ethernet_link_monitor_poll(void); #endifeth_link_monitor.c#include eth_link_monitor.h #include lwip/netif.h #include cmsis_os2.h extern struct netif gnetif; #define PHY_ADDRESS 0x00U #define PHY_REG_BSR 0x01U #define PHY_LINK_STATUS_BIT (1U 2U) static uint8_t last_link_status 0xFF; static uint8_t read_phy_link_status(void) { uint32_t reg_val 0; if (HAL_ETH_ReadPHYRegister(heth, PHY_ADDRESS, PHY_REG_BSR, reg_val) ! HAL_OK) { return 0; } return (reg_val PHY_LINK_STATUS_BIT) ? 1U : 0U; } static uint8_t read_phy_link_status_debounce(void) { uint8_t cnt 0; for (uint8_t i 0; i 3; i) { cnt read_phy_link_status(); osDelay(20); } return (cnt 2) ? 1U : 0U; } void ethernet_link_monitor_init(void) { last_link_status read_phy_link_status_debounce(); } void ethernet_link_monitor_poll(void) { uint8_t current_status read_phy_link_status_debounce(); if (current_status last_link_status) { return; } last_link_status current_status; LOCK_TCPIP_CORE(); if (current_status) { netif_set_link_up(gnetif); } else { netif_set_link_down(gnetif); } UNLOCK_TCPIP_CORE(); }然后在任务里调用void ethernet_task(void *argument) { ethernet_link_monitor_init(); for (;;) { osDelay(300); ethernet_link_monitor_poll(); } }任务创建可以在 MX_FREERTOS_Init 里osThreadNew(ethernet_task, NULL, link_task_attr);这个框架的好处是主循环不阻塞即使网络没插线系统其他任务也能正常跑。5.3 完整实现中断版本如果要用中断方式我在任务框架上补一个信号量定义。注意 PHY 引脚的中断优先级要设置得比 tcpip 线程的临界区优先级低一些避免在中断里调用了可能触发优先级翻转的系统调用导致死锁。eth_link_monitor.c修改版核心部分#include cmsis_os2.h osSemaphoreId_t link_event_sem; extern struct netif gnetif; void ethernet_link_monitor_init(void) { link_event_sem osSemaphoreNew(1, 0, NULL); last_link_status read_phy_link_status_debounce(); // 使能 PHY 的 Link 中断以 LAN8720A 为例 HAL_ETH_WritePHYRegister(heth, PHY_ADDRESS, 0x11U, 0x0002U | 0x0004U); } void PHY_INT_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin PHY_INT_PIN) { BaseType_t higher_priority_task_woken pdFALSE; osSemaphoreRelease(link_event_sem); portYIELD_FROM_ISR(higher_priority_task_woken); } } void ethernet_task(void *argument) { ethernet_link_monitor_init(); for (;;) { osSemaphoreAcquire(link_event_sem, osWaitForever); osDelay(50); // 状态稳定 uint8_t current_status read_phy_link_status_debounce(); if (current_status last_link_status) { continue; } last_link_status current_status; LOCK_TCPIP_CORE(); if (current_status) { netif_set_link_up(gnetif); } else { netif_set_link_down(gnetif); } UNLOCK_TCPIP_CORE(); } }注意osSemaphoreRelease在 FreeRTOS 的 CMSIS_OS2 封装中可以在中断里调用但中断优先级不能高于configMAX_SYSCALL_INTERRUPT_PRIORITY。如果发现拔插网线时系统偶尔死机先把中断优先级调低再测。5.4 实测拔插后恢复时间的真实表现以一块 STM32F407 LAN8720A 的板子为例静态 IP 场景下我实测轮询 300ms 的恢复链路流程是插回网线约 1.2 秒后 PHY 状态稳定任务轮询发现状态变化调用 netif_set_link_up此时立刻 ping 可能丢 1 到 2 个包第 3 个包开始通。整体恢复时间约 2~3 秒。DHCP 场景下会慢一些因为 DHCP 重新协商要走四步握手实测大概 8 到 15 秒取决于路由器和 DHCP 服务器的响应速度。这个时间对于大多数工业产品完全能接受。如果用的是中断方案链路恢复时间理论上能压到 1 秒内但实际瓶颈在 DHCP 协商所以不是所有场景都能感受到差异。6. 踩坑实录恢复不上的排查清单做过的项目多了遇到的坑也就多了。这一部分我把网线插拔恢复这个功能上最常见的故障现象和解决方法整理成清单你可以直接当排查手册用。6.1 插拔后 ping 不通但复位板子就好这个现象十有八九是“链路状态变化根本没传递到 LwIP”。排查第一步在netif_set_link_up/down前后加串口打印看看网线插拔时这条代码到底有没有被执行到。如果没执行回到 PHY 地址和 Link 位定义检查如果执行了但还是不通检查 lwipopts.h 里LWIP_NETIF_LINK_CALLBACK有没有开。还有一个偏门原因netif 的 flags 里原本就没有设置NETIF_FLAG_LINK_UP或者你的板子启动后在 LwIP 初始化完成后被其他代码错误地清掉过 flags。我当时排查过一个问题就是某个驱动库在初始化时调用了netif_set_down()导致 netif 一直处于 DOWN 状态。这个函数会把整个网口置为不可用链路恢复也不会自动把它拉起来必须检查应用代码里有没有误调用。6.2 DHCP 恢复太慢或一直拿不到地址先确认路由器侧有没有这个板子的残留租约。有些路由器在租约没到期时对同一 MAC 的新 Discover 报文可能延迟响应这是路由器策略问题不是板子问题。排查方法是恢复前先看路由器管理页面清除旧租约再测。板子侧的原因通常是 DHCP 客户端在 link down 后没有正确进入 INIT 状态。LwIP 的 DHCP 状态机确实会监听链路事件但前提是你已经调用了netif_set_link_down/up。如果之前做过 DHCP 手动重置之类的骚操作也可能会干扰。建议检查标准流程启用 DHCP 后只需要dhcp_start(gnetif)其余链路事件 LwIP 自己处理不要手动重置 DHCP 状态。6.3 PHY 指示灯正常但 LwIP 里 link 状态一直 down这种情况非常诡异网线插着PHY 的 Link 灯亮着说明硬件链路是通的但代码读出来状态不对。八成是 PHY 寄存器地址或者寄存器位定义搞错了。比如有的 PHY 用寄存器 0x01 的 bit2但我遇到过一款国产 PHY 把 Link 状态放在寄存器 0x1A 的 bit0手册不看清楚真的会被坑。还有可能是在 RMII 模式下 PHY 的 CRS_DV 引脚没接对导致 PHY 的状态没有正确反映到 MAC 侧。这种硬件问题只能靠示波器查波形了。6.4 拔线瞬间系统 HardFault 或看门狗复位拔线瞬间触发 HardFault 通常是 DMA 描述符出错导致的。拔线的一瞬间如果 MAC 正在接收一个残帧接收描述符可能进入错误状态而 HAL 库的默认错误处理是调HAL_ETH_ErrorCallback如果不处理后续 DMA 就卡住了。解决思路是在以太网错误回调里恢复 DMA 状态。简单做法是把 ETH 外设做一次软复位重新启动但要注意复位期间不要调用 LwIP 的收发函数。更细一点可以检查描述符的状态位看有没有 owner 错误把出错的描述符手动清掉。如果工程开了看门狗还要注意链路检测任务里的HAL_Delay或osDelay会不会导致任务被长时间占用尤其是中断版本里如果 PHY 中断引脚悬空误触发可能造成链路检测任务被频繁打断看门狗喂不上。这种问题我一般把链路检测任务优先级设低一点确保喂狗任务能准时执行。6.5 排查问题速查表把上面这些浓缩成一张表现象可能原因排查方法插拔后 ping 不通没调用 netif_set_link_up/down加打印确认代码路径插拔后 ping 不通LWIP_NETIF_LINK_CALLBACK 未开启确认宏定义PHY 灯亮但代码检测不到PHY 地址/寄存器位错误查 datasheet打印读到的寄存器值DHCP 恢复慢路由器租约残留清租约后重测DHCP 恢复慢链路状态未快速上报中断方式替代轮询拔线瞬间 HardFaultDMA 描述符错误未被处理在错误回调中复位 ETH DMA频繁插拔后死机PHY 中断优先级配置过高降低中断优先级link up 后仍无法收发RMII 时钟不稳示波器测量 REF_CLK 50MHz6.6 调试时常用的工具和技巧调试这块我常用的三板斧串口打印、抓包、读寄存器。串口打印是最直接的我建议在链路回调里把状态变化和时间戳一起打出来配合测试脚本能很快定位是检测层问题还是协议栈层问题。打印模板printf([%lu] link up\r\n, (unsigned long)HAL_GetTick());抓包永远是最有说服力的工具。PC 端用 Wireshark 监听网卡插拔网线后观察是否有 ARP 请求发出。如果板子发出 ARP 但 PC 没响应问题在网络协议栈如果根本没抓到 ARP问题在链路检测或者网卡驱动。读寄存器这个操作我在 HAL 库基础上封装了一个调试命令手机通过串口发指令就能实时读 PHY 寄存器。比如发一个phy 1就能打印出当前 PHY 的寄存器 1 的值。这只读功能在排查链路检测时真的救命能直接看到 Link 状态位的变化过程。7. 最后再分享一个小细节网线插拔自动恢复这个功能做完以后不要把测试流程只停留在“拔掉再插上”。实际项目里还有更变态的场景拔掉半根网线物理上接触不良、网线中间用劣质转接头、交换机端口被网管软件强制 shutdown 再 enable。这些情况下 PHY 的 Link 状态表现很不一样有的会稳定变 0有的会以某个频率反复 toggle。如果你的产品需要面对这些场景建议在链路检测里再加一层“连续 N 次 Link Down 才真正判定链路断开”的滤波逻辑防止抖动导致业务层频繁断连重连。另外有条件的话做一轮老化测试把网线插拔动作重复几千次观察看门狗和内存占用有没有增长。链路检测跑得再漂亮系统得稳定运行才是最终目标。
返回列表