☰
STM32H750+LAN8720A移植LWIP实战:CubeMX配置与热插拔详解
2026/10/5 5:02:43 网站建设 项目流程

1. 项目概览与方案选型

如果你准备在 STM32H750VB 上做以太网通信,大概率会走上这么一条路:STM32CubeMX 生成工程,协议栈用 LWIP,PHY 选 LAN8720A,再挂上 FreeRTOS 做多任务,最后拿 IAR 8.32 编译下载。我最近刚把一个项目按这个方案完整落地,过程不算复杂,但坑确实不少,尤其是网线热插拔和 H7 的 Cache 一致性问题,网上资料比较零散,今天干脆整理一篇能直接照着做的记录。

这个方案能解决什么问题?简单说就是给 MCU 加一个 100M 以太网口,既能做 TCP 服务器让上位机主动连进来,也能做 UDP 收发数据,还能在网线拔插之后自动恢复通信。适合正在用 CubeMX + LWIP 做 H7 以太网、被局域网通信或者断线重连折磨的朋友。

先聊一下为什么选这套组合。STM32H750VB 是 Cortex-M7 内核 480MHz,集成了 Ethernet MAC,但是内部 Flash 只有 128KB,这点后面编译时会很“惊喜”。LAN8720A 是 Microchip 旗下很常见的一颗百兆 PHY,RMII 接口,引脚少,外围就一个 25MHz 晶振加几个电阻电容,非常适合做低成本网口。LWIP 则是嵌入式领域事实标准的轻量级 TCP/IP 协议栈,支持 TCP、UDP、ARP、ICMP,和 FreeRTOS 配合起来能跑 TCP/UDP 服务器。开发环境你写的是 IAR 8.32,这个版本相对老,后面有一些适配上的问题需要处理。

1.1 为什么是 STM32H750VB + LAN8720A

STM32H750 在 H7 系列里算比较特殊的,它的 Flash 小,但主频高、RAM 大,尤其是大容量的 RAM 对 LWIP 这种比较吃内存的协议栈很友好。以太网相关的外设也齐全:一个支持 10/100/1000M 的 MAC,带 DMA,配合 RMII 接口可以很轻量地和百兆 PHY 对接。H750 的 RAM 分成好几块,这点和 F1/F4 完全不同,后面配置不到位的话 DMA 会莫名其妙不工作。

选 LAN8720A 的核心理由是便宜、资料多、RMII 接口方便。RMII 模式下 MAC 和 PHY 之间只需要 7 根数据线:TXD0、TXD1、TX_EN、RXD0、RXD1、CRS_DV、REF_CLK,加上 MDC/MDIO 管理口,总共 9 根,对 PCB 布局很友好。相比 MII 接口 16 根线的庞大阵仗,RMII 在 H750VB 这种 100 脚封装上更好布线。

RMII 的 REF_CLK 是 50MHz,这个时钟通常由 PHY 提供,不是由 MCU 输出。这是新手特别容易搞反的地方,我一开始也栽在这里,后面硬件部分会详细说。

1.2 CubeMX 一键生成与 IAR 8.32 的兼容性

STM32CubeMX 做这类项目确实省事,外设时钟、引脚复用、LWIP 中间件、FreeRTOS 中间件都可以图形化配置,自动生成初始化代码。但注意 CubeMX 生成的工程默认针对某种 IDE 环境,你选择生成 IAR 工程之后,IAR 8.32 打开时可能会遇到一个问题:8.32 版本发布比较早,对 STM32H750 这种后出的型号可能没有内置设备描述,会提示找不到设备或者器件型号不支持。

我当时的处理方式是在 IAR 的 General Options -> Target -> Device 里手动选同系列的 H743VB,因为 H750 和 H743 的核心、外设基本一致,Flash 大小和部分外设配置有差异,但跑 LWIP 和 FreeRTOS 完全没问题。如果有条件,也可以去 IAR 官网下载对应版本的 device support pack 装上,会比手动替换更规范。

另外要注意 IAR 8.32 编译 H7 工程时,默认优化级别如果不够高,LWIP 加 FreeRTOS 加 HAL 库加起来很容易超过 H750 的 128KB Flash。建议 LWIP 相关的源文件开 High 优化,调试文件保持 Low;同时关闭不必要的中间件和 HAL 功能,这些都是能实打实压缩体积的手段。

1.3 整体软件架构

整个软件跑起来之后的分工大概是这样的:底层是 HAL 库的 ETH 驱动,负责 MAC 和 DMA;往上一层是 LWIP 协议栈,跑在 FreeRTOS 的 tcpip_thread 线程里;再往上一层是按功能拆分的应用任务,包括 TCP 服务器任务、UDP 收发任务、网线状态检测任务。这种结构的好处是模块独立,TCP 和 UDP 互不影响,任何一方卡住都不会拖垮系统。

在 CubeMX 生成的代码里,LWIP 初始化会在MX_LWIP_Init()中完成,包括lwip_init()、网卡添加、IP 设置、netif_set_up()。FreeRTOS 启动之后,LWIP 的内部线程会跑起来,然后我们自己创建的应用任务就可以自由调用 netconn API 或者 socket API 做网络收发。这里我推荐用 netconn API,代码比 raw API 好写太多,也基本不影响性能。

2. 硬件设计与协议栈基础

2.1 RMII 接口与时钟方案:最容易翻车的点

RMII 是 MAC 与 PHY 之间的接口,数据线少,但时序上有个硬性要求:MAC 侧需要接收 50MHz 的 REF_CLK 作为收发时钟基准。这个 50MHz 时钟有三种常见来源:

  • 外部直接给一个 50MHz 有源晶振,同时接到 MAC 和 PHY 的 REF_CLK 脚;
  • LAN8720A 自身外接 25MHz 晶振,芯片内部 PLL 产生 50MHz,从 CLKOUT 引脚输出给 STM32 的 ETH_RMII_REF_CLK;
  • 由 MCU 某引脚输出 50MHz 给 PHY,这种设计不太常见,多见于某些非标开发板。

多数开发板用的是第二种,也就是 LAN8720A 的 CLKOUT 脚接 STM32 的ETH_RMII_REF_CLK。所以你在 CubeMX 里看到 PA1 被自动分配为 ETH_RMII_REF_CLK 时,那是输入引脚,不要再想着从 PA8 MCO 输出时钟给它。网上有些文章会说用 PA8 输出 50MHz 到 PHY,如果你的板子原理图不是这么画的,照抄只会让 PHY 不工作。

判断方法很简单:检查 LAN8720A 的 CLKOUT 引脚是否连到了 STM32 的 PA1,如果是,就对了。再用示波器或者万用表量一下 CLKOUT 是否有 50MHz,没有的话查晶振和 LAN8720A 的供电。

RMII 下 LAN8720A 还需要一个 25MHz 的晶振,接在 XI 和 XO 引脚之间,两个引脚各接一个 22pF 左右的负载电容到地。晶振起振后 PHY 内部会把 25MHz 倍频到 50MHz,再通过 CLKOUT 输送给 MAC。如果这个时钟链路断了,MAC 那边连 PHY 的 ID 都读不到,HW 阶段就会卡死。

2.2 PHY 地址、复位和状态引脚

LAN8720A 的 PHY 地址由 PHYAD0 引脚决定,常见板子会把 PHYAD0 接地,所以 PHY 地址是 0x00;也有板子接上拉电阻,地址是 0x01。这个地址必须在 CubeMX 的 ETH 配置里写对,否则 HAL 库通过 MDIO 读取 PHY 寄存器时会一直超时。

LAN8720A 的 nRST 引脚是低电平复位,很多电路会用一颗 10k 电阻上拉到 3.3V,靠电容的充放电完成上电复位。如果你的板子用 GPIO 控制复位,必须在初始化 ETH 之前把该引脚拉高并延时,否则 LAN8720A 会一直处于复位状态,PHY 寄存器同样读不到。

PHY 的 nINT 引脚可以接 MCU 的外部中断,用来做网线插拔检测,这是比较高效的方案。但实际项目里我更喜欢轮询 PHY 寄存器,简单可靠,200ms 周期读一次Basic Status Register(寄存器地址 0x01)的 Bit2,看到 Link Status 变化就知道了。轮询不需要额外占一个外部中断,也不会因为 PHY 中断配置不对导致漏事件。

2.3 LWIP + FreeRTOS 的内存与 Cache 问题

H7 系列有一个和 F1/F4 完全不同的坑:内部 RAM 分了很多块,而部分 RAM 区域无法被 DMA 访问。比如 DTCM 区(0x20000000 起始)只接在 CPU 内核总线上,外设 DMA 是访问不到的。如果 ETH 的 DMA 描述符或者收发缓冲区被链接到 DTCM,结果是网卡完全收不到数据,但程序不报错。

所以硬件初始化的第一步,就要确认 ETH DMA 描述符和 buffer 是用 AXI SRAM(0x24000000)或者 SRAM1/2/3(0x30000000 附近),绝对不要落到 DTCM。这一步我建议在 CubeMX 生成工程之后直接检查链接脚本(.icf 文件)的 RAM region 定义,确保全局变量的默认放置区域和 DMA 可访问区域一致。

另一个大坑是 D-Cache。Cortex-M7 默认情况下 SRAM 是 Write-Back Cacheable,CPU 写数据后数据可能还在 Cache 里没有真正落到内存,而 DMA 直接访问内存,两边数据不一致就会出现:TCP 发送出去的数据是乱的,UDP 偶发丢包,ping 时好时坏。解决办法是在 MPU 里把 ETH DMA 涉及的 RAM 区域配置成 Non-Cacheable,或者每次收发前后手动做 Clean/Invalidate。手动做 Cache 维护很麻烦,而且容易漏,我建议直接用前者。

后面第 4 章会给出一段 MPU 配置代码,把 0x24000000 开头的 256KB 设为 Non-Cacheable,这样 LWIP 的 buffer、DMA 描述符、FreeRTOS 堆都可以安心放在这个区域,省去很多痛苦。

3. 基于 CubeMX 的完整配置流程

3.1 时钟树与 ETH 引脚配置

打开 STM32CubeMX,选择 STM32H750VB 这个芯片。RCC 这里按你板子的实际晶振配置,常见是 25MHz 外部高速晶振 HSE,然后让 PLL 输出 480MHz 主频。我习惯把SYS -> Timebase Source从 SysTick 改成 TIM6 或者 TIM7,因为 FreeRTOS 会用 SysTick 做系统节拍,两者不能共用。如果 Timebase 不换,编译能过,但运行起来 HAL 库延时和 FreeRTOS 调度会互相干扰。

ETH 外设使能之后,Mode 选择 RMII。此时 CubeMX 会自动把 PB11、PB12、PB13、PC4、PC5、PA7、PA1、PC1、PA2 这几个引脚复用成 ETH 信号。如果你的板子有特殊定义,注意手动调整,但大概率就是这些默认引脚。PHY Address 一栏填 0 或 1,以上面原理图为准。RMII 的时钟引脚 PA1 在 CubeMX 里显示为 ETH_RMII_REF_CLK,实际方向是输入,由 PHY 提供 50MHz,这一点已经强调过。

MAC 地址和 IP 地址在 CubeMX 中配置。IP 如果不用 DHCP,就直接写一个静态地址,比如 192.168.1.100,子网掩码 255.255.255.0,网关 192.168.1.1。多台板子同时调试时记得改 MAC 地址,最好每块板子不同,否则局域网内会冲突。

3.2 LWIP 中间件与 FreeRTOS 配置

在 Middleware and Software Packs 里勾选 LWIP,启用LWIP服务。关键参数里我一般按照下面的实际经验调整:

  • LWIP_RTOS选择 CMSIS OS(也就是和 FreeRTOS 对接);
  • IP versionIPv4 即可;
  • Memory Size也就是 MEM_SIZE,建议不低于 40960,实际数据量大的话可以再提到 80KB;
  • PBUF Pool Size建议 16~32,默认通常够用;
  • TCP_WND不要太小,建议 4~8 个 TCP_MSS;
  • TCP_SND_BUF影响发送缓冲,建议 8 个 TCP_MSS 左右。

这些参数本质上是 LWIP 内存池的配额,配小了表现为“TCP 连接建立失败”、“数据收发一段时间后卡死”。如果你的串口日志里看到类似memp: out of memory的输出,基本都是这几个值不够。

FreeRTOS 的配置在 Middleware -> FREERTOS 里,开启 CMSIS-V2 或者 V1 都行,IAR 8.32 都能编译。由于我们要自己写应用任务,可以只保留一个默认任务,也可以在代码里用xTaskCreate动态创建,这样更灵活。注意 FreeRTOS 的 heap 大小要足够,LWIP 的 tcpip_thread 也会参与调度,建议 heap 至少开 16KB,如果代码里用到大量动态创建任务和网络 buffer,32KB 更稳妥。

3.3 生成 IAR 工程后的快速检查

生成 IAR 工程后,不要立刻编译下载,先做三个快速检查。

第一,确认 IAR 的设备型号。如果 8.32 不识别 H750VB,选 H743VB 或更新 device support pack。打开编译选项,确保--cpu Cortex-M7等选项正常。

第二,检查链接脚本里 RAM 区域。打开 .icf 文件,看RW_IRAM1的起始地址。如果起始地址是 0x20000000,意味着全局变量默认放在 DTCM,这对 ETH 来说风险很大。建议增加或者调整一个 RAM 段,让 ETH buffer 和 FreeRTOS heap 落在 AXI SRAM。

第三,确认生成代码里 LWIP 的lwipopts.h中NO_SYS为 0,LWIP_TIMERS为 1,这样才能和 FreeRTOS 的 tcpip_thread 正常配合。CubeMX 默认生成的一般没问题,但改配置时容易被覆盖,检查一眼不费事。

4. 核心代码实现:TCP 服务器、UDP 与热插拔

4.1 TCP 服务器任务(netconn API)

我直接用 LWIP 的 netconn API 写 TCP 服务器,因为代码比 raw API 少很多,逻辑更容易保持正确。下面这一段是核心逻辑,可以作为参照。

#include "lwip/api.h" #include "lwip/netif.h" #include "FreeRTOS.h" #include "task.h" #include "main.h" #define TCP_SERVER_PORT 8080 static void TcpServerTask(void *argument) { struct netconn *server; struct netconn *conn; struct netbuf *buf; char *data; u16_t len; err_t err; server = netconn_new(NETCONN_TCP); if (server == NULL) { vTaskDelete(NULL); } err = netconn_bind(server, IP_ADDR_ANY, TCP_SERVER_PORT); if (err != ERR_OK) { netconn_delete(server); vTaskDelete(NULL); } netconn_listen(server); for (;;) { err = netconn_accept(server, &conn); if (err != ERR_OK) { vTaskDelay(pdMS_TO_TICKS(10)); continue; } /* 给每条连接设置接收超时,避免网线断开后 recv 永远卡死 */ netconn_set_recvtimeout(conn, 5000); while (netconn_recv(conn, &buf) == ERR_OK) { if (netbuf_data(buf, (void **)&data, &len) == ERR_OK) { if (len > 0) { /* 原样回发,也可以在这里做业务处理 */ netconn_write(conn, data, len, NETCONN_COPY); } } netbuf_delete(buf); } netconn_close(conn); netconn_delete(conn); } }

这段代码最关键的地方是netconn_set_recvtimeout(conn, 5000)。假如没有这一句,当客户端和服务器之间网络异常断开时,服务器端的netconn_recv可能一直阻塞不下线,连接资源被占住。设置超时后,5 秒无数据就返回错误,连接被清理,accept 循环随时可以处理新连接。

另外注意netbuf_data拿到的 data 指针是 LWIP 内部 pbuf 的地址,直接使用没问题,但在netbuf_delete之后就不能引用了。往客户端写回数据时我加了一个NETCONN_COPY标志,这样netconn_write会先把数据拷贝到发送缓冲,函数返回后我们可以放心释放原始 buffer。

4.2 UDP 收发任务

UDP 比 TCP 简单,不用 listen,不用 accept,接收和发送都基于同一个 netconn。下面是一个 UDP echo 任务,绑定 8081 端口,收到什么就回什么。

#define UDP_SERVER_PORT 8081 static void UdpEchoTask(void *argument) { struct netconn *udpconn; struct netbuf *buf; void *data; u16_t len; err_t err; udpconn = netconn_new(NETCONN_UDP); if (udpconn == NULL) { vTaskDelete(NULL); } err = netconn_bind(udpconn, IP_ADDR_ANY, UDP_SERVER_PORT); if (err != ERR_OK) { netconn_delete(udpconn); vTaskDelete(NULL); } for (;;) { err = netconn_recv(udpconn, &buf); if (err == ERR_OK) { if (netbuf_data(buf, &data, &len) == ERR_OK) { /* 原样回发给发送方 */ netconn_send(udpconn, buf); } netbuf_delete(buf); } } }

UDP 任务和 TCP 任务建议分开创建,因为 netconn_recv 是阻塞式的,如果你在同一个任务里同时等 TCP 数据和 UDP 数据,其中一个接口没有数据时,另一个接口也得不到处理。分开两个任务后,两者完全独立。需要转发业务的话,可以在回调或者队列里做数据交换,不要在中断里处理网络收发。

4.3 网线热插拔检测的实现

网线热插拔不是硬件上搞定就完事了,还要让 LWIP 知道链路状态发生变化。简单说就是周期性读取 PHY 寄存器,发现 Link Status 变化后调用netif_set_link_up()或者netif_set_link_down()。

PHY 寄存器地址 0x01 是 Basic Status Register,Bit2 是 Link Status。读出来的值是 1,表示物理链路已经建立;是 0,表示网线断开了。

#define PHY_REG_BSR 0x01U #define PHY_LINK_STATUS_BIT 0x0004U static uint8_t last_link_state = 0xFF; static void LinkCheckTask(void *argument) { uint32_t reg = 0; uint8_t link_state; for (;;) { if (HAL_ETH_ReadPHYRegister(&heth, PHY_REG_BSR, &reg) == HAL_OK) { link_state = (reg & PHY_LINK_STATUS_BIT) ? 1 : 0; if (link_state != last_link_state) { if (link_state == 1) { netif_set_link_up(&gnetif); } else { netif_set_link_down(&gnetif); } last_link_state = link_state; } } vTaskDelay(pdMS_TO_TICKS(200)); } }

这里把检测周期设为 200ms。太短会增加 MDIO 总线负载,太长会让插拔事件响应迟钝。200ms 在用户体感上基本是“秒切”。

调用netif_set_link_down()之后,LWIP 会停止通过这个网卡发送数据,TCP 连接会因为无法发送而逐步超时。调用netif_set_link_up()之后,网络接口恢复数据收发。由于我们在 TCP 接收任务里已经设置了 recv 超时,断开期间旧连接会在几秒内被清理,重新插上网线后,TCP 服务器立即可以接受新连接。UDP 则更简单,链路恢复后马上就能收发,不需要额外处理。

如果你的项目对断线恢复时间要求很高,可以用 LAN8720A 的 nINT 引脚接 MCU 外部中断,在中断里通过 FreeRTOS 队列通知任务,这样插拔响应可以做到毫秒级。但大多数工业场景 200ms 轮询足够,而且轮询代码更不容易出幺蛾子。

4.4 任务优先级与内存配置建议

任务优先级分配上,我建议把 LinkCheckTask 放在最高优先级,TCP 和 UDP 任务次之,tcpip_thread 再低一点。原因是链路状态变化会直接影响底层收发,必须优先处理;而 TCP、UDP 属于业务层,偶尔让它们等待一下问题不大。

例如在main.c中:

xTaskCreate(TcpServerTask, "TCP", 1024, NULL, 4, NULL); xTaskCreate(UdpEchoTask, "UDP", 1024, NULL, 4, NULL); xTaskCreate(LinkCheckTask, "LINK", 512, NULL, 6, NULL);

任务栈大小可以根据实际打印的uxTaskGetStackHighWaterMark()来调整。LWIP 的 netconn 收发需要一点栈空间,1024 字节通常够用;如果里面再调用串口打印、格式化字符串,就考虑 1536 或 2048。

内存方面,你需要在 MPU 配置里把 DMA 使用的 RAM 区域设为 Non-Cacheable。CubeMX 生成的代码里有MPU_Config(),我把 H7 的 AXI SRAM 前 256KB 设置为 Non-Cacheable,参考配置如下:

static void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct = {0}; HAL_MPU_Disable(); MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x24000000; MPU_InitStruct.Size = MPU_REGION_SIZE_256KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER7; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigMemoryRegion(&MPU_InitStruct); HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); }

这样设置之后,这个区域内的变量读写不会经过 Cache,CPU 和 DMA 看到的数据就是一致的。代价是性能比 Cacheable 区域稍低,但对嵌入式以太网应用完全够用。如果项目对性能要求很高,可以把 ETH 描述符和 buffer 单独分配在一个小 Non-Cacheable 段,其他 RAM 保持 Cacheable,这个需要配合 IAR 的链接脚本做 section 放置,操作复杂一些,但不难,后续有时间可以单独写一篇。

注意:如果你同时把 FreeRTOS 的堆也放在这个 0x24000000 区域,那堆本身也是 Non-Cacheable 的,任务切换和数据拷贝速度略受影响,但稳定压倒一切,实际体验差别很小。

5. 常见问题与排查技巧实录

5.1 典型问题速查表

现象可能原因排查/解决办法
PHY 寄存器读不到,MDIO 超时PHY 地址错误查原理图 PHYAD0,改成 0 或 1 再试
PHY 寄存器读不出来LAN8720A 复位引脚没拉高上电后延时再释放复位,或用 GPIO 控制
ping 不通但 PHY Link 灯亮eth buffer 落在 DTCM确认链接脚本 RAM 区域不是 0x20000000
ping 偶尔通偶尔不通D-Cache 未处理配置 Non-Cacheable MPU 区域,或关闭 D-Cache
TCP 客户端连不上服务器LWIP 内存池不够调大 MEM_SIZE、MEMP_NUM_TCP_PCB、MEMP_NUM_TCP_SEG
网线拔掉再插上,TCP 无法恢复没做 Link 状态检测添加 PHY Link 轮询任务,调用 netif_set_link_up/down
网线拔掉后,服务器卡很久才有反应accept 后的 recv 没有超时给连接设置 netconn_set_recvtimeout
编译后 Flash 超过 128KB优化没开,中间件太多对 LWIP/FreeRTOS 开 High 优化,精简 HAL 模块
IAR 8.32 打开工程报器件不存在设备支持包太老选 H743VB,或升级 device support pack
收发数据中出现随机错乱Cache 导致 DMA 和 CPU 数据不一致用 MPU 配置 Non-Cacheable 区域

5.2 我踩过的几个深坑详细记录

先讲 PHY 地址的坑。我最初在 CubeMX 里填的是 0,结果 HAL_ETH_Init 直接超时。后来拿万用表量 PHYAD0 引脚,发现被板子的电阻接到了高电平,地址其实是 1。这问题不难查,但如果你不往原理图方向想,光在软件里折腾能浪费一下午。

再讲 RMII 时钟的坑。我一开始看到一个例程用 PA8 MCO 输出 50MHz 给 PHY,照抄后发现 PHY 完全不工作。原因是我的板子 LAN8720A 自己有 25MHz 晶振,CLKOUT 输出 50MHz 给 PA1,PA8 那一路 MCO 配置纯属多余,甚至会造成信号冲突。后来把 PA8 的 MCO 功能去掉,网络立刻通了。所以多引脚复用项目里,一定要先画清楚信号流向。

还有一次最隐蔽的坑是开 D-Cache 后,串口打印一切正常,程序也不卡死,但 TCP 发送几十个包之后对端收到乱码。一开始以为是协议栈配置问题,查了半天,最后在 MPU 配置里加上 Non-Cacheable 区域解决。H7 的 Cache 不一致不会导致崩溃,往往就是“看似正常但数据偶发异常”,排查起来特别耗时间。

另外提一个 IAR 8.32 的细节:如果编译时报错找不到stm32h7xx_hal_eth.h之类的东西,先检查有没有把 HAL 库的 ETH 模块添加到工程里。CubeMX 生成时如果没勾选 ETH,LWIP 又强制依赖这个驱动,就会缺文件。正常流程是 CubeMX 勾选 ETH 后自动把 HAL 库对应源文件加进去,不太会遇到,但被人为删除过工程文件的情况另说。

5.3 调试技巧:串口日志和 PHY 寄存器检查

嵌入式网络调试最忌讳上来就跑完整应用。我习惯先写一个最小的启动流程:初始化 ETH + LWIP,不开任何业务任务,只在 main 里循环读取 PHY 寄存器 0x02 和 0x03(ID 寄存器),把结果通过串口打印出来。LAN8720A 的 ID 寄存器读到固定值,说明 MDIO 链路、PHY 复位、时钟都正常。如果读不到,后面就不用继续了。

PHY 正常之后,第二步是确认 LWIP 网络通不通。先禁用 DHCP,用固定 IP,然后从电脑上 ping。TCP/UDP 任务不要一开始就加,因为 ping 是最基本的链路测试,能通再往上加应用逻辑。ping 通了再开 TCP 服务器,用网络调试助手连上,做回环测试。最后加上 UDP,做收发包测试。这一步一 步来,即使出问题也容易定位。

调试过程中建议把 LWIP 的调试信息打开一部分,例如LWIP_DEBUG设置成 1,再配合LWIP_DBG_ON和LWIP_DBG_TYPES_ON。LWIP 的 debug 输出默认打到一个printf函数,你可以把它重定向到串口。它会打印 ARP、ICMP、TCP 状态机等信息,对定位“为什么连不上”非常有用。正常跑起来之后再关闭 debug,避免影响性能。

5.4 网线热插拔恢复慢的进一步优化

如果你做完前面那些步骤,发现热插拔已经能工作,但 TCP 连接恢复需要等十几秒甚至更久,问题基本出在旧连接没有被及时清理。原因是网线断开后,对端可能已经不在线,TCP 协议栈要等重传超时才会放弃,这个时间往往很长。

我建议的优化手段是给 TCP 服务器每条连接设置一个较短的空闲超时,配合应用层心跳。比如服务器收到数据后记录最后活跃时间,超过 10 秒没有数据就主动关闭连接。这样即使 PHY 状态没来得及反应,服务器也不会被半死连接拖住。这个逻辑可以用一个简单的 tick 计数器实现,比依赖 TCP 自身的超时机制可靠很多。

另外也可以用一个更“粗暴但有效”的方式:在 LinkCheckTask 检测到netif_set_link_down()之后,把 TCP 服务器的 netconn 全部 delete 掉,再等链路恢复后重新创建。这个做法会打断所有正在传输的数据,但如果你的场景允许断线重启,那它是最不容易留残留状态的方案。

6. 最后分享几点实际操作体会

这个项目做完之后,最大的感受是:H750 上跑 LWIP 并没有想象中那么难,难的是 H7 家族特有的 RAM 架构和 Cache 机制,以及 RMII 时钟方向这些“一看就会、一调就废”的细节。如果你用的也是 IAR 8.32,建议早点把设备支持包和链接脚本检查清楚,不然越往后越痛苦。

我个人实际开发中会把代码分层:netconn API 的收发逻辑单独放一个文件,PHY 寄存器操作单独放一个文件,业务逻辑再独立出来。这样即使 CubeMX 重新生成代码,只要我把自定义代码写在 USER CODE 区域,冲突也不会太多。强烈建议初始化代码和应用代码分目录管理,别让 CubeMX 生成的东西和你手写的业务逻辑混在一个文件里,否则后面加功能会越来越难受。

最后再分享一个小技巧:接入网络调试工具时,PC 端防火墙有时候会挡掉 TCP 连接,导致你以为是板子的问题。验证的时候先用ping 192.168.1.100确认网络链路,再用 TCP 调试工具连接,如果 ping 通但 TCP 连不上,先看看是不是端口号占用了,或者防火墙拦截了。这个看起来低级,但我确实遇到不止一次,排查到最后发现是电脑端的问题。调试嵌式网络项目,软件、硬件、PC 端三个方向都要兼顾,只盯着一块看很容易死磕很久。

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

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

立即咨询