☰
STM32裸机LWIP移植:HAL库+LAN8720A跑通PING实战指南
2026/10/17 8:09:48 网站建设 项目流程

简介:面向 STM32F429 与 LAN8720A 的以太网通信开发者,这套基于 HAL 库的 C 语言工程,演示了在无操作系统环境下将 lwIP 2.1.2 协议栈移植到 STM32,并通过 RMII 接口驱动 PHY 芯片、借助 SMI 接口读写寄存器,最终在内网实现 PING 连通。工程由 STM32CubeMX 6.6.1 生成,适配 Keil MDK 5.27 与 HAL V1.27.1,适合希望绕过 RTOS、快速上手轻量级 TCP/IP 网络开发的嵌入式工程师学习与改造。

压缩包共 1300 个文件,整体约 31.16MB。主体是 678 个 C 源文件与 450 个头文件,覆盖 lwIP 核心协议、HAL 以太网驱动、CMSIS 设备支持层;另有 46 个链接脚本与汇编启动文件、ARM 数学库、hex 可烧录镜像、CubeMX 工程文件及文本说明,模块划分清楚,能够帮助读者理解从 PHY 寄存器初始化到协议栈收发包的完整调用链,并可直接导入集成开发环境二次开发。

当前已有 1988 人学习下载。压缩包内还附带 keilkill 清理脚本等实用工具,便于反复验证 RMII 引脚时序、SMI 读写时序和网口调试排错。无论用于毕业设计、竞赛准备还是产品原型验证,这份工程都是一份难得的无操作系统 lwIP 移植参考。

1. 给 STM32 加网口:HAL 库、LAN8720A、LWIP 无系统跑通 PING

做工业采集器或者小型网关的时候,给 STM32 加一个以太网口,听起来就是「外设初始化 + 协议栈移植」的事,但真上手你会发现,官方例程几乎默认带 RTOS,裸机跑 LWIP 的资源少得可怜。去年我做一台设备的数据上云模块,MCU 资源紧张到塞不下系统,只好自己搭一套 HAL 库 + LAN8720A + LWIP 无操作系统方案,从 RMII 接线到最终 PING 通,前后花了两个晚上。这份资源就是这条路的完整记录:适合正在选型、或者已经卡在 PING 不通的开发者,你会看到接线要点、时钟来源、LWIP 裸机移植步骤,以及我踩过的五个典型坑。

2. LAN8720A 与 STM32 的硬件连接:RMII 引脚和 50MHz 时钟是第一个分水岭

2.1 为什么选 LAN8720A:引脚少、成本低、裸机友好

STM32 内部集成了 MAC 控制器,但 PHY(物理层收发器)必须外接。PHY 芯片的选择直接决定 PCB 面积和布线难度,LAN8720A 是低功耗 10/100M 以太网 PHY,支持 RMII 和 MII 两种接口。RMII 模式只需要 10 根信号线,比 MII 的 16 根少了一大截,在小板子上布线和走等长都轻松很多。价格上,LAN8720A 在批量市场里属于便宜的那一档,单颗成本不到主流 PHY 的一半,这对做量产设备的开发者来说是很现实的选型理由。

另一个关键点是 LAN8720A 的驱动方式:它通过 MDIO/MDC 两根线跟 MAC 通信,寄存器映射和常见 PHY 非常接近,HAL 库自带的 HAL_ETH_ReadPHYRegister / HAL_ETH_WritePHYRegister 直接就能操作,不需要额外写复杂的时序驱动。这对于裸机项目来说,意味着你在 CubeMX 里勾选以太网外设后,PHY 的初始化代码几乎是现成的。

2.2 RMII 接线表与 REF_CLK 时钟来源:这是最容易翻车的地方

RMII 接口的信号线分为数据、控制和管理三类。数据线是 TXD[1:0] 和 RXD[1:0],各 2 位;控制线是 TX_EN(发送使能)和 CRS_DV(载波侦听/数据有效);管理线是 MDIO 和 MDC。还有一个关键的 REF_CLK,必须由外部提供 50MHz 时钟,RMII 的数据收发都同步在这个时钟上。

REF_CLK 的 50MHz 来源有两种常见做法:

  • 外部 50MHz 有源晶振,直接接到 STM32 的 ETH_REF_CLK 引脚和 LAN8720A 的 XTAL1 引脚;
  • 用 STM32 的 MCO 引脚输出 50MHz,接到 LAN8720A 的 XTAL1/CLKIN。

我一般直接用 STM32 的 MCO1 输出,省掉一颗有源晶振。前提是你在 CubeMX 的时钟树里配置正确:HSE 必须倍频到 50MHz,然后通过 MCO1 引脚输出。如果你忽视了这一步,PHY 根本不会工作,但 CubeMX 又不会报错——这是「配置全对但 PING 不通」的第一大元凶。

RMII 模式下的信号连接关系如下表:

STM32 引脚LAN8720A 引脚说明
ETH_TXD0TXD0发送数据位 0
ETH_TXD1TXD1发送数据位 1
ETH_TX_ENTX_EN发送使能
ETH_RXD0RXD0接收数据位 0
ETH_RXD1RXD1接收数据位 1
ETH_CRS_DVCRS_DV载波侦听/数据有效
ETH_MDCMDC管理时钟
ETH_MDIOMDIO管理数据
MCO1 或外部 50MHzREF_CLK / XTAL150MHz 参考时钟

注意 LAN8720A 的 PHYAD0 引脚:如果接地,PHY 地址为 0x00;如果接上拉,则地址变成 0x01。大部分模块默认接地,也就是说 HAL 库里 PHY 地址宏定义要用 0x00,配错了会直接导致 MDIO 读写失败,初始化时读不到 PHY 的 ID 寄存器,代码会一直卡死在等待 PHY 就绪的逻辑里。

2.3 MDIO/MDC 读写时序与 PHY 地址确认

MDIO 是串行管理接口,MDC 是时钟,最高频率 2.5MHz。HAL 库的 HAL_ETH_ReadPHYRegister 函数已经封装好了帧格式,你只需要在初始化时读一次 PHY 的 ID 寄存器(地址 0x02 和 0x03)来确认链路通不通。LAN8720A 的 PHY ID 是 0x0007 和 0xC0F1,如果你读出来不是这个值,先别查 LWIP,回头查硬件接线和地址配置。

我一般会在 main 函数刚开始时加一段调试代码,把 PHY ID 打印出来:

uint32_t phy_id_hi = 0, phy_id_lo = 0; HAL_ETH_ReadPHYRegister(&heth, PHY_REG_ID_HI, &phy_id_hi); HAL_ETH_ReadPHYRegister(&heth, PHY_REG_ID_LO, &phy_id_lo); printf("PHY ID: 0x%04X 0x%04X\r\n", phy_id_hi, phy_id_lo);

这段代码的逻辑是:如果 MDIO 通信正常,LAN8720A 会返回固定的 ID 值;如果你读到的都是 0xFFFF 或者 0x0000,说明 MDIO 线上的时序、地址或上拉电阻出了问题。PHY 的 MDIO 引脚通常需要 1.5kΩ 到 10kΩ 的上拉电阻,很多集成模块上已经有,但如果你是自己画的板子,漏掉这个上拉,MDIO 读出来的数据会随机跳变,非常难排查。

3. CubeMX 初始化:从引脚映射到 LWIP 参数的一次性配置

3.1 引脚映射:RMII 模式选中后必须核对 AF 复用

在 CubeMX 中,启用以太网外设并选择 RMII 模式后,软件会自动分配引脚,但你要检查 MCU 的具体封装是否支持这些引脚的复用功能。有些 LQFP 封装引脚紧张,ETH_TXD1 和某个 UART 的 TX 冲突,你在 CubeMX 里会看到橙色的冲突提示,需要手动调整。另外,ETH_REF_CLK 引脚必须选择正确的复用功能(通常是 AF11),如果选成 GPIO 输出,时钟信号根本进不了 MAC。

我习惯在 CubeMX 的 Pinout 视图中逐根核对:

  • ETH_TXD0、ETH_TXD1、ETH_TX_EN、ETH_RXD0、ETH_RXD1、ETH_CRS_DV 六根线是 RMII 数据和控制,必须全部配置为 AF 模式;
  • ETH_MDIO、ETH_MDC 两根管理线同样是 AF 模式;
  • 如果使用 MCO1 输出 50MHz,还要额外配置 PA8 为 MCO1 功能,并在时钟树里设置输出频率为 50MHz。

配置完成后,生成代码前还要检查时钟树。以太网外设的时钟源通常是 PLL 输出的 50MHz,而这个 50MHz 同时要供给 MCO1 和 MAC。CubeMX 会自动计算,但你要确认 PLL M/N/P/Q 的数值不是默认值改出来的奇怪组合,否则 MAC 的时钟频率不对,PING 包会随机失败。

3.2 以太网 DMA 描述符配置:数量和内存对齐不能想当然

HAL 库的以太网驱动使用 DMA 描述符来管理数据包收发,CubeMX 的 Ethernet 配置界面里有两个关键参数:Tx Descriptor 数量和 Rx Descriptor 数量。建议 TX 和 RX 各配置 4 个描述符,太低容易出现数据包丢弃,太高又浪费 RAM。Cortex-M 系列的 DMA 描述符要求 32 字节对齐,CubeMX 生成的数组是链接器自动对齐的,但如果你把描述符内存手动指定到某个地址,必须检查对齐。

这里有个容易忽略的点:DMA 描述符是存放在 AXI SRAM 还是普通 SRAM,影响性能但不影响正确性。如果 MCU 有多个 RAM 区域,比如 M7 内核的 AXIM 和 DTCM,建议把描述符放在 AXI SRAM,因为 DTCM 的总线带宽有限,高负载下会丢包。我通常在链接脚本里为以太网描述符单独预留一段地址空间,避免和 LWIP 的内存堆互相挤压。

CubeMX 生成的以太网初始化代码中,HAL_ETH_Init 函数会调用 HAL_ETH_MspInit 完成 GPIO、时钟和 DMA 的整体配置,你在用户代码里不需要手动操作寄存器。但有一个关键点必须注意:HAL_ETH_Init 内部会复位 PHY,然后等待 PHY 自检完成。LAN8720A 的上电初始化时间约为 10ms,如果复位信号持续时间不够,PHY 会一直处于未知状态。

3.3 LWIP 无系统模式的参数选择与内存预算

在 CubeMX 的 Middleware 选项中启用 LWIP 时,平台选项一定要选择「No OS」或「Standalone」,这决定了 LWIP 内部使用 raw API 而不是 tcpip_thread 加邮箱的模式。无系统的 LWIP 没有独立线程,所有协议栈处理都在你的主循环里轮询完成,因此内存配置和中断优先级直接决定稳定性。

关键参数按这个经验值配置:

  • MEM_SIZE(堆内存总大小):建议 1460 * 2 * 4 = 11680 以上,我一般给 20480,太小的话 TCP 传输窗口打不开;
  • PBUF_POOL_SIZE(数据包缓冲池数量):至少 8 个,每个 1518 字节,这样才能容纳一个完整的以太网帧;
  • MEMP_NUM_TCP_PCB:同时活动的 TCP 连接数,一般 4 个足够做透传网关;
  • MEMP_NUM_NETBUF:如果跑 TCP,建议 4 个以上,避免 netbuf 耗尽导致接收失败。

这些值可以在 lwipopts.h 文件里手动覆盖 CubeMX 生成的默认配置。我踩过的坑是:为了省 RAM 把 PBUF_POOL_SIZE 设成 4,结果跑 TCP 文件传输时丢包严重,PING 倒是通的,但大包 100% 失败——这个场景我后面在避坑章节专门讨论。

3.4 生成代码后的检查清单:三个必改点

CubeMX 生成代码并不能直接编译过、直接 PING 通,至少有三处要手动改:

第一,eth_platform.c 里的 PHY 地址宏。有些板卡用的 PHY 地址不是 0x00,你需要读原理图确认。HAL 库里 ETH_PHY_ADDRESS 默认是 0x00,如果板子上的 PHYAD0 引脚接了高电平,要改成 0x01,否则 MDIO 通信建立不起来。

第二,ethernetif.c 里的 low_level_init 函数。这个函数里设置 MAC 地址的部分是固定的,我建议改成从 OTP 或外部 EEPROM 读取,否则每台设备的 MAC 都相同,放到同一交换机上会互相干扰。临时测试可以不改,但量产必须处理。

第三,interrupt 优先级。裸机 LWIP 依赖以太网中断触发接收通知,IRQ 优先级不要设为最高,否则在中断服务函数里调用 LWIP 处理函数时,会把其他实时性要求更高的中断阻塞掉。我一般把 ETH 中断优先级设置为低于定时器中断、高于 UART,具体数值看你的系统里哪些中断不能丢。

HAL_NVIC_SetPriority(ETH_IRQn, 5, 0); HAL_NVIC_EnableIRQ(ETH_IRQn);

这段配置的意思是:ETH 中断抢占优先级为 5,如果你的系统里定时器中断优先级是 0 到 3,那么 ETH 不会打断它们;而当 UART 中断优先级是 6 或 7 时,ETH 可以打断 UART,保证网络帧不被长时间挂起。裸机环境的这个区别非常微妙,优先级太激进会让你无法调试串口日志,太保守则会导致网络高负载时丢包。

4. 无操作系统 LWIP 移植:raw API 与网卡驱动的衔接

4.1 裸机 LWIP 的驱动模型:三个必须实现的接口

无系统的 LWIP 不像 RTOS 版本那样有独立的 tcpip_thread,协议栈的所有事件处理都发生在你的主循环调用里。因此网卡驱动和 LWIP 的衔接就集中在三个函数:low_level_init 负责初始化 MAC 和 DMA 描述符;low_level_output 负责把 LWIP 的 pbuf 链发送出去;ethernetif_input 负责把接收到的帧转交给 LWIP 协议栈。

这三个函数的实现是移植工作的核心。我在 ethernetif.c 里按标准结构实现它们,代码风格参考了 LWIP 官方 baremetal 示例,再根据 HAL 库做适配。low_level_init 里最关键的操作是把 DMA 描述符的地址和环形链表结构建立好,并将每个 RX 描述符的缓冲区指针指向预先分配的接收缓冲区数组。

static err_t low_level_init(struct netif *netif) { int i; eth_dma_desc_t *dma_rx_desc; uint8_t *buffer; netif->link_layer_type = 1; netif->mtu = 1500; netif->hwaddr_len = 6; netif->hwaddr[0] = 0x02; netif->hwaddr[1] = 0x00; netif->hwaddr[2] = 0x00; netif->hwaddr[3] = 0x00; netif->hwaddr[4] = 0x00; netif->hwaddr[5] = 0x01; MACaddr_init(&heth, netif->hwaddr); dma_rx_desc = &dma_rx_descriptor_table[0]; for (i = 0; i < ETH_RX_DESC_CNT; i++) { buffer = (uint8_t *)&rx_buffers[i]; dma_rx_desc[i].DESC0 = (uint32_t)buffer; dma_rx_desc[i].DESC1 = 0; dma_rx_desc[i].DESC2 = 0; dma_rx_desc[i].DESC3 = 0; } HAL_ETH_Start(&heth); return ERR_OK; }

这段代码的逻辑是:先把 netif 结构体里的硬件地址填上,然后调用 MACaddr_init 把 MAC 地址写入 MAC 外设寄存器,最后把 RX 描述符的 DESC0 字段指向实际的缓冲区。HAL_ETH_Start 启动接收功能,DMA 开始轮询,任何到达的数据包都会写入描述符指向的缓冲区并触发接收中断。参数方面,netif->mtu 设置 1500 是标准以太网帧上限,如果你要支持 jumbo frame 可以改大,但 LAN8720A 是百兆 PHY,不建议超过 1500。

4.2 发送路径:low_level_output 的实现要点

low_level_output 接收 LWIP 传下来的 pbuf 链,它的难点在于 pbuf 可能是多个片段(payload 不连续),而 DMA 发送描述符只接受连续内存。解决办法有两种:一是把 pbuf 链拷贝到连续发送缓冲区,二是用多个 DMA 描述符分别描述每个片段。对于百兆接口的裸机场景,第一种方式更简单可靠,代价是 CPU 占用率稍高。

static err_t low_level_output(struct netif *netif, struct pbuf *p) { struct pbuf *q; uint8_t *buffer = (uint8_t *)&tx_buffer[0]; int i = 0; for (q = p; q != NULL; q = q->next) { memcpy(buffer + i, q->payload, q->len); i += q->len; } HAL_ETH_TransmitFrame(&heth, buffer, i, 0); return ERR_OK; }

这段代码的循环里遍历整条 pbuf 链,把每个节点的 payload 顺序拷贝到一块连续的发送缓冲区。拷贝完成后,调用 HAL_ETH_TransmitFrame 一次性发送。这个函数有两个隐含细节:返回后只是帧已交给 DMA,不代表已发送完毕;第二个参数是数据长度,如果帧长度小于 60 字节,HAL 库会自动补零到最小以太网帧长度。我在测试时发现 HAL 库不会自动补零,所以 60 字节以下的帧必须在拷贝时手动填充,否则对端交换机可能会丢弃。

4.3 接收路径与主循环调度:ethernetif_input 的轮询方式

裸机环境下没有独立线程调用 tcpip_input,接收数据的入口是你在主循环里调用 ethernetif_input。实现上,main 循环每转一圈就调用一次,函数内部检查是否收到新帧,如果有就构造 pbuf 并交给 netif->input。

void ethernetif_input(struct netif *netif) { uint32_t len = 0; uint8_t *buffer = (uint8_t *)&rx_buffer[0]; struct pbuf *p; len = HAL_ETH_ReadData(&heth, buffer, 0); if (len > 0) { p = pbuf_alloc(PBUF_RAW, len, PBUF_POOL); if (p != NULL) { pbuf_take(p, buffer, len); if (netif->input(p, netif) != ERR_OK) { pbuf_free(p); } } } }

这段代码的含义是:从 HAL 层读取一帧数据到临时接收缓冲区,然后分配一个 PBUF_POOL 类型的 pbuf,把数据拷进去,调用 netif->input 送入 LWIP 协议栈。PBUF_POOL 类型能从内存池快速分配,不会失败,但 pool 大小不够时会申请不到,数据包被丢弃。主循环里调用这个函数的频率直接决定网络吞吐量:如果主循环里其他任务耗时过长,接收缓冲会溢出,数据包被 DMA 覆盖,现象就是 PING 通但大包吞吐率上不去。我一般把 ethernetif_input 放在主循环最前面,并且保证一次循环不超过 1ms。

4.4 raw API 的 TCP 服务器示例:三行核心代码

无系统的 LWIP 用 raw API 写 TCP 服务器,核心是三个回调:connected、recv、error。你不需要为每个连接创建线程,只需在初始化时注册这些回调。下面是一个最简示例:

static struct tcp_pcb *server_pcb; static err_t server_recv(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err) { if (p != NULL) { tcp_write(pcb, p->payload, p->len, 1); pbuf_free(p); tcp_sent(pcb, NULL); } else { tcp_close(pcb); } return ERR_OK; } static err_t server_accept(void *arg, struct tcp_pcb *newpcb, err_t err) { tcp_recv(newpcb, server_recv); return ERR_OK; } void tcp_server_init(void) { server_pcb = tcp_new(); tcp_bind(server_pcb, IP_ADDR_ANY, 8080); server_pcb = tcp_listen(server_pcb); tcp_accept(server_pcb, server_accept); }

tcp_new 创建协议控制块,tcp_bind 绑定本地端口 8080,tcp_listen 让 PCB 进入监听模式,tcp_accept 注册连接建立回调。连接建立后,新产生的 pcb 会传给 server_accept,你在那里注册 recv 回调。recv 回调里用 tcp_write 原样回显数据,再释放 pbuf,实现最简单的 TCP echo 服务。raw API 的特点是所有回调都在 tcpip_input 的调用上下文里执行,这也是为什么主循环必须频繁调用 ethernetif_input,否则回调永远不执行。

5. 避坑记录:PING 不通的五个典型原因与排查顺序

5.1 现象:PHY ID 读出来全是 0xFFFF,初始化卡死

原因:MDIO 通信失败,最常见的是 PHY 地址不对,或者 MDIO 引脚没有上拉电阻。

解决:先读原理图或模块丝印,确认 PHYAD0 引脚的电平状态。如果 PHYAD0 接地,地址是 0x00;如果悬空或接高,地址是 0x01。在 eth_platform.c 里把 PHY 地址宏改成对应值,再用万用表量 MDIO 引脚电压,正常空闲时应该接近 3.3V,如果接近 0V 说明漏了上拉电阻,需要补一颗 4.7kΩ 到 10kΩ。

5.2 现象:PHY ID 正常,但 PING 不通,抓包看到 STM32 一直在发 ARP

原因:ARP 请求发出去了,但 MAC 地址没有正确写入 netif,或者回复的 ARP 包校验错误被交换机丢弃。

解决:确认 low_level_init 里 netif->hwaddr 数组的值,以及 HAL_ETH_SetMACAddress 是否被调用。最简单的验证方法是上位机执行 arp -a,看是否有 STM32 的 IP 对应的 MAC 地址。如果 ARP 表里有记录,说明 MAC 层通了,问题在 IP 层或 LWIP 回调;如果没有记录,检查 MAC 地址是否全零或全 F,这类非法地址会被交换机直接丢弃。

5.3 现象:PING 小包(小于 100 字节)通,大包(大于 1000 字节)全丢

原因:天线效应并不存在,实际是 pbuf 池或 DMA 描述符数量不足,大包需要多个 pbuf 链,内存池耗尽导致丢包。

解决:在 lwipopts.h 里调大 PBUF_POOL_SIZE,从默认值改到 16 或者更多。同时检查 DMA 描述符数量,如果只有 2 个 RX 描述符,高负载下描述符会被占满,新帧进来没有地方写,直接丢弃。我自己的配置是 RX 描述符 6 个,TX 描述符 4 个,PBUF_POOL_SIZE 为 16,PING 1500 字节的包连续 1000 个无一丢失。

5.4 现象:PING 通,但 TCP 连接建立后马上断开

原因:TCP 三次握手完成后,LWIP 的接收窗口太小,或者 netif->mtu 与对端不一致,导致窗口协商失败。常见于你复制了别人的配置,MTU 改成 9000,而 LAN8720A 不支持巨型帧。

解决:把 netif->mtu 恢复成 1500。如果确认 MTU 没问题,检查 MEMP_NUM_TCP_SEG 的数量,这是 TCP 发送队列里可以缓存的段数,太小会导致发送窗口打不开,对端收到 RST 后关闭连接。我把这个值设为 8,TCP 传输就稳定了。

5.5 现象:设备长时间运行后网络卡死,必须重启才能恢复

原因:裸机 LWIP 没有看门狗机制,一旦某个回调里出现异常分支(比如 pbuf 泄漏),协议栈状态会越来越差,最终完全停滞。另一个常见原因是 DMA 描述符的 ownership 位被破坏,HAL 库无法回收描述符。

解决:在主循环里加一个 LWIP 状态检查,周期性发送 ARP 探测确认链路存活;同时开一个定时器,每 10 秒打印一次 pbuf 池的剩余量(LWIP_STATS 宏开启时可用)。如果 pbuf 剩余量持续下降,说明有泄漏点,重点检查 recv 回调里是否在异常路径忘了 pbuf_free。从那以后,我每次初始化网络外设都会开 LWIP_STATS 编译选项,跑一个小时的 PING 压测,看内存曲线是否平稳。

6. 验证与进阶:从 PING 通到长时间稳定运行的实测技巧

6.1 PING 延时的量化测试与判断标准

PING 通只是第一步,你要进一步确认延迟是否在正常范围。百兆以太网的理论单向延迟在 10 微秒级,但实际经过 STM32 的 MAC、DMA 和 LWIP 协议栈处理后,PING 回显时间通常在 0.5ms 到 2ms 之间。如果你的 PING 延迟超过 10ms,说明要么主循环里 ethernetif_input 的调用频率太低,要么 LWIP 的内存池碎片导致处理变慢。

我一般用两个命令做压测:

ping 192.168.1.100 -n 100 -l 32 ping 192.168.1.100 -n 100 -l 1400

第一行测试小包延迟和丢包率,第二行测试接近最大帧长度的大包稳定性。正常情况下两种包的丢包率都应该为 0,平均延迟差不超过 1ms。如果大包延迟明显上升,检查 DMA 描述符是否够用,PBUF_POOL_SIZE 是否足以支撑单帧多 pbuf 链的场景。

6.2 断线自动重连的实现思路

有线的以太网也会遇到网线被意外拔掉的情况。LAN8720A 通过 BSR 寄存器(地址 0x1F)的 bit2 表示 Link 状态,你可以周期性地读这个寄存器来检测网线状态。检测到 Link down 后,停止 LWIP 通信并让 PHY 进入低功耗模式;检测到 Link up 后,重新协商速率并恢复通信。

我的实现是在主循环里每 500ms 调用一次链接检测函数:

uint32_t link_status = 0; HAL_ETH_ReadPHYRegister(&heth, 0x1F, &link_status); if (link_status & 0x04) { netif_set_link_up(&g_netif); } else { netif_set_link_down(&g_netif); }

这段代码的逻辑是:读 PHY 状态寄存器,判断 bit2 是否为 1。为 1 表示链路已建立,通知 LWIP 网络接口处于 up 状态;为 0 表示网线断开,LWIP 会停止向该接口发送数据。要注意的是,网线重新插上后 PHY 需要几百毫秒完成自动协商,如果检测到 up 后立刻发 PING,可能还是会失败,所以实际代码里我加了 1 秒的延时再恢复通信。这个姿态让设备在网络恢复后能自我修复,不需要人工介入。

6.3 一个有效提升稳定性的配置:ETH 中断服务函数里只置标志位

裸机应用中,我强烈不建议在 ETH 中断服务函数里直接处理 LWIP 事件。因为 LWIP 的回调可能涉及内存分配和链表操作,这些操作不是中断安全的,如果在中断里执行,极易破坏堆结构。正确做法是中断里只置一个标志位,主循环检测到标志后调用 ethernetif_input。

volatile uint8_t eth_rx_flag = 0; void ETH_IRQHandler(void) { if (HAL_ETH_GetReceivedFrame_IT(&heth) == HAL_OK) { eth_rx_flag = 1; HAL_ETH_ResumeRxCallback(&heth); } }

这段代码的含义是:ETH 中断到达时,把接收标志位置 1,然后立即恢复 RX 描述符的接收能力,整个中断处理过程不涉及任何 LWIP 函数。主循环里的 ethernetif_input 函数检查这个标志,为 1 时才调用底层读取函数。这样做的好处是中断处理时间极短,不会影响其他高优先级中断;坏处是接收延迟取决于主循环的频率。如果你对延迟要求高,可以把 eth_rx_flag 的检查放在主循环最前面,保证每次循环都处理,实测延迟可以控制在 1ms 以内。从那以后,我每接触一个新的网络项目,都会先跑一遍这套无系统 LWIP 的初始化流程,把 PHY 地址、REF_CLK 来源和 pbuf 池大小这三个变量固定下来,再谈上层业务,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询