简介:以STM32F429为核心、整合LAN8720A百兆PHY驱动与LWIP轻量级TCP/IP协议栈移植的以太网例程包,面向具备一定Cortex-M4与HAL库基础、希望打通MCU联网链路的嵌入式开发者。资源包为rar压缩,约46.15MB,上游未提供文件总数与类型明细;据描述包含以太网驱动与LWIP应用示例代码、STM32CubeMX工程配置文件、协议栈源码及HAL库头文件、说明编译烧录测试的README文档。内容覆盖GPIO复用配置、MIIM接口读写PHY寄存器、MAC地址设置与收发使能、DMA高速传输以及LWIP网卡接入与回调处理等环节,可帮助读者理清从物理层到协议栈的完整通路。已有968人学习,可作为物联网与工业自动化方向联网实验的参考案例。
1. 网口灯亮却 ping 不通,问题多半不在协议栈
STM32 以太网这条路上,最容易让人误判的地方其实在 PHY 那个 50MHz 参考时钟,LwIP 的代码反倒不是首要难点。拿到 STM32F429_ETH_LWIP_LAN8720A 这个压缩包,先别急着点编译;它把 F429 片内 MAC、LwIP 协议栈、LAN8720A 这颗 RMII PHY 串成了一条完整链路:MAC 负责 DMA 收发和帧过滤,PHY 负责物理层编码与自动协商,协议栈跑在两者之上。这份资源适合已经能点灯、能跑通串口,却卡在“网口灯亮着、电脑却 ping 不到”的人;也适合想把 RMII 时钟链、DMA 描述符、lwipopts.h 这几处掰开看清楚的熟手。下面按硬件约束、协议栈落地、收发调试、踩坑记录、压力验证五段推进,每段都落到能复现的操作。
2. 硬件链路的三个硬约束:RMII 引脚、50MHz 时钟、PHY 地址
STM32 以太网在硬件上绕不开三条硬约束,软件写得再干净,只要其中一条对不上,整个链路就只能对着空气发帧:RMII 的引脚复用、REF_CLK 的 50MHz 来源、PHY 的 SMI 地址。包里附带的工程已经把这几项配好了,但只要换板子、换 PHY 封装或者自己重画 PCB,这三条还得自己从头核一遍,因为它们的错误表现形式都很像“代码没问题但就是不通”。
2.1 先把 50MHz 时钟链画出来,再谈外设初始化
RMII 相比 MII 把数据线从 4 位压到 2 位,代价是 MAC 和 PHY 必须共用同一个 50MHz 参考时钟。F429 的 ETH 模块本身不产生这个时钟,只负责接收,所以 REF_CLK 一定来自芯片外部。实际项目里能见到的接法基本是这三种:
| 方案 | 时钟来源 | 优点 | 常见坑 |
|---|---|---|---|
| A | 25MHz 无源晶振接 LAN8720A 的 XTAL1/XTAL2,PHY 的 nINT/REFCLKO 输出 50MHz 给 MCU 的 PA1 | 成本低,元件少 | PHY 内部 REF_CLK 输出默认可能没开,必须读寄存器确认 |
| B | MCU 的 MCO1 输出 25MHz 给 PHY 的 XTAL1,PHY 再回送 50MHz | 少一颗晶振 | MCO 的时钟源要单独配,HSE 不是 25MHz 时得走 PLL,抖动变大 |
| C | 外部 50MHz 有源晶振,一路给 PHY,一路给 MCU | 最稳,两边同源 | 多一颗器件和一路供电,布线要求高 |
多数参考设计走的是方案 A。这里有个血泪经验:LAN8720A 复位后 nINT/REFCLKO 引脚的功能取决于内部寄存器 0x17 第 3 位(REF_CLK 输出使能),不同批次的模块出厂状态不一定一样。稳妥的做法是初始化时先读一次 0x17,如果该位是 0 就写 1 再回读确认。如果这一步漏了,PA1 上什么波形都没有,MAC 的接收时钟就是死的,表现出来的现象是 RX 描述符的 own 位永远不归零,串口里一行错都不报。
/* LAN8720A:确认并打开 50MHz REF_CLK 输出 */ #define PHY_REG_MODE_CTRL 0x17u #define PHY_REFCLK_OUT_EN (1u << 3) static void lan8720a_enable_refclk(uint16_t phy_addr) { uint32_t val = 0; /* 不同 HAL 版本 HAL_ETH_ReadPHYRegister 的签名略有差异,有的把地址作为独立参数 */ HAL_ETH_ReadPHYRegister(&heth, PHY_REG_MODE_CTRL, &val); if ((val & PHY_REFCLK_OUT_EN) == 0u) { val |= PHY_REFCLK_OUT_EN; HAL_ETH_WritePHYRegister(&heth, PHY_REG_MODE_CTRL, val); HAL_ETH_ReadPHYRegister(&heth, PHY_REG_MODE_CTRL, &val); /* 回读确认 */ } if ((val & PHY_REFCLK_OUT_EN) == 0u) { printf("REF_CLK output still disabled, check PHY strap / 25MHz crystal\r\n"); } }这段代码只做一件事:把 PHY 内部那个 50MHz 时钟输出打开,并回读验证。参数phy_addr就是后面 2.3 节要确认的 SMI 地址;如果读出来全是 0xFFFF,说明 MDIO 线上根本没有器件应答,先别往下走。写完之后建议拿示波器或者逻辑分析仪点一下 PA1,确认有稳定 50MHz、幅度 3.3V 的方波,这一步花十分钟,能省掉后面几小时的瞎猜。
2.2 RMII 引脚映射与 MAC 的模式选择位
F429 的 RMII 引脚是固定的,换封装也只是可用性不同,映射关系本身不变:
| 信号 | STM32F429 引脚 | 方向 | LAN8720A 侧 | 备注 |
|---|---|---|---|---|
| ETH_RMII_REF_CLK | PA1 | 输入 | nINT/REFCLKO | 50MHz,必须由 PHY 或外部晶振提供 |
| ETH_MDIO | PA2 | 双向 | MDIO | 需要 1.5k~2.2k 上拉到 3.3V |
| ETH_MDC | PC1 | 输出 | MDC | 分频后建议不超过 2.5MHz |
| ETH_RMII_CRS_DV | PA7 | 输入 | CRS_DV | RMII 下由 CRS 与 DV 合并 |
| ETH_RMII_RXD0 | PC4 | 输入 | RXD0 | |
| ETH_RMII_RXD1 | PC5 | 输入 | RXD1 | |
| ETH_RMII_TX_EN | PB11 | 输出 | TXEN | |
| ETH_RMII_TXD0 | PB12 | 输出 | TXD0 | |
| ETH_RMII_TXD1 | PB13 | 输出 | TXD1 | |
| PHY_nRST | 任意 GPIO | 输出 | nRST | 建议独立 GPIO 控制,别直接接复位芯片 |
引脚配好之后还有一步经常被 CubeMX 自动生成、但手写工程里容易漏掉的操作:把 MAC 从 MII 模式切到 RMII。这个位在 SYSCFG 里,必须在HAL_ETH_Init之前置上:
/* 必须在 HAL_ETH_Init 之前执行,否则 MAC 按 MII 时序采样,收不到任何有效帧 */ __HAL_RCC_SYSCFG_CLK_ENABLE(); SYSCFG->PMC |= SYSCFG_PMC_MII_RMII_SEL;这个位不置的典型症状是:PHY 侧一切正常,自动协商能完成,链路灯也亮,但 LwIP 的netif->input一次都不会被调用。原因是 MII 需要 4 位数据和额外的 RX_CLK/TX_CLK,而 RMII 只有 2 位数据,采样时序完全对不上。翻车点在于这个位没有“读回来不对”的报错,只能靠现象反推,所以我一般会在main里初始化 ETH 之前加一句assert_param(SYSCFG->PMC & SYSCFG_PMC_MII_RMII_SEL)之类的自检,把它变成一次性的显式确认。
2.3 PHY 地址、复位时序与寄存器自检清单
LAN8720A 的 SMI 地址由 PHYAD0 引脚在复位时的电平决定,拉低是 0,拉高是 1。很多模块把 PHYAD0 直接下拉到地,所以默认地址是 0;也有的板子为了兼容多 PHY 会拉高。工程里heth.Init.PhyAddress写错,HAL_ETH_Init会直接返回错误,但如果错误码被吞掉,后面就是一连串莫名其妙的超时。
地址不确定时,最省事的办法是扫一遍:
/* 扫描 0~31 号 SMI 地址,能读出非 0xFFFF 的 ID 就是 PHY 所在地址 */ static int phy_scan(void) { for (uint16_t addr = 0; addr < 32u; addr++) { uint32_t id1 = 0, id2 = 0; if (HAL_ETH_ReadPHYRegister(&heth, 0x02u, &id1) != HAL_OK) continue; if (HAL_ETH_ReadPHYRegister(&heth, 0x03u, &id2) != HAL_OK) continue; if (id1 != 0xFFFFu && id1 != 0u) { printf("PHY found at addr %u, ID = 0x%04X%04X\r\n", addr, (unsigned)id1, (unsigned)id2); return (int)addr; } } return -1; }LAN8720A 的 ID 寄存器读出来应当是 0x0007 和 0xC0F1,拼起来是 0x0007C0F1。如果不是这个值,先怀疑地址扫错了,再怀疑 MDIO 上拉电阻没焊。扫描的时候注意HAL_ETH_ReadPHYRegister内部有时序等待,一次完整的 32 地址扫描大概几十毫秒,别放在中断里做。
复位时序同样是个容易被忽略的小细节。LAN8720A 要求 nRST 低电平至少保持 100us,释放后内部初始化还需要一段时间才能响应 SMI 访问。我一般按下面的余量写,宁可多等几十毫秒:
static void lan8720a_hard_reset(void) { HAL_GPIO_WritePin(PHY_RST_GPIO_Port, PHY_RST_Pin, GPIO_PIN_RESET); HAL_Delay(20); /* 手册要求 >=100us,这里留足余量 */ HAL_GPIO_WritePin(PHY_RST_GPIO_Port, PHY_RST_Pin, GPIO_PIN_SET); HAL_Delay(30); /* 释放后等内部初始化完成,再访问 SMI */ }复位之后按顺序读这几个寄存器,基本能把硬件问题挡在软件之外:寄存器 0x00(BMCR)的 bit15 复位自清零,说明复位生效;0x01(BMSR)的 bit2 是链路状态、bit5 是自动协商完成;0x1F(特殊模式)的 bit2 反映当前协商结果。这些值读不对,就先别碰 LwIP。
3. 协议栈落地:CubeMX 的三个必改项、lwipopts.h 的五处内存、两套移植骨架
硬件确认完之后才轮到软件。这一步的核心不是“把 LwIP 加进工程”,而是让 ETH 外设的 DMA 描述符、LwIP 的内存池、以及运行环境(裸机还是 RTOS)三者对齐。三者里任何一处参数不匹配,表现都是“能通一会儿然后挂”,很难靠单步调试定位。
3.1 ETH 外设初始化:描述符数量、接收模式和中断优先级
CubeMX 生成MX_ETH_Init之后,有三处我每次都会手动改。第一处是接收模式:默认的轮询模式在裸机下能用,但和 LwIP 配合时接收路径会变得很别扭,改成中断模式、在HAL_ETH_RxCpltCallback里把帧交给ethernetif_input更顺。第二处是描述符和缓冲区的数量,CubeMX 默认给的是 4 个 RX 描述符,跑 TCP 压测时会明显不够。第三处是中断优先级,这一处在 FreeRTOS 下是硬性约束。
heth.Instance = ETH; heth.Init.AutoNegotiation = ETH_AUTONEGOTIATION_ENABLE; heth.Init.Speed = ETH_SPEED_100M; heth.Init.DuplexMode = ETH_MODE_FULLDUPLEX; heth.Init.PhyAddress = 0; /* 用 2.3 节的扫描结果填入 */ heth.Init.MACAddr = mac; /* 6 字节,别用全 0 或广播地址 */ heth.Init.RxMode = ETH_RXINTERRUPT_MODE; /* 中断接收,交给 ethernetif_input */ heth.Init.ChecksumMode = ETH_CHECKSUM_BY_HARDWARE; heth.Init.MediaInterface = ETH_MEDIA_INTERFACE_RMII; if (HAL_ETH_Init(&heth) != HAL_OK) { Error_Handler(); /* 别吞掉这个返回值 */ }参数里最值得说两句的是ChecksumMode。硬件校验和能省 CPU,但它只在收到的帧确实带校验和、且 PHY 没有做卸载时才可靠;如果中间经过了某些交换芯片,硬件校验和可能被标记为“已校验但实际没校验”,LwIP 侧就会丢弃本该接收的包。排查这类问题时,临时改成ETH_CHECKSUM_BY_SOFTWARE对比一下,能快速缩小范围。
中断优先级这条在裸机下无所谓,在 FreeRTOS 下是红线:ETH 中断服务函数里会调用xSemaphoreGiveFromISR之类的 API,所以它的抢占优先级数值必须大于等于configMAX_SYSCALL_INTERRUPT_PRIORITY。写反了不会立刻报错,而是在某次收包时随机进 HardFault,属于典型的“玄学”故障。
3.2 lwipopts.h 里的五个内存参数,比代码更影响稳定性
LwIP 的默认配置是给资源紧张的 MCU 准备的,直接拿来跑 F429 会明显偏保守。下面这五个参数是我在 F429(192KB SRAM)上验证过的一组实用值:
| 宏 | 默认倾向 | 建议值 | 作用 | 调过头的后果 |
|---|---|---|---|---|
| MEM_SIZE | 偏小 | 16~32KB | LwIP 堆,TCP 收发缓冲从这里分配 | 太大会挤占其他全局变量,链接时才发现溢出 |
| PBUF_POOL_SIZE | 4~8 | 16~32 | pbuf 池数量 | 太小在高并发时直接丢包,lwip_stats里能看到 |
| PBUF_POOL_BUFSIZE | 592 左右 | 1524 或 1536 | 单个 pbuf 容量 | 小于 1524 时一个以太网帧要拆成链式 pbuf,吞吐明显下降 |
| TCP_MSS | 536 | 1460 | TCP 单段最大负载 | 与 MTU 不匹配会导致额外分片 |
| TCP_SND_BUF / TCP_WND | 2~4 个 MSS | 4~8 个 MSS | 发送/接收窗口 | 太小吞吐上不去,太大吃内存 |
PBUF_POOL_BUFSIZE这个参数值得单独强调:以太网帧最大 1514 字节,加上对齐和 LwIP 自己的头部预留,1536 是个比较稳的取值。如果你在压测时发现小包延迟正常、大包吞吐只有理论值的几分之一,先看这里,再看TCP_SND_BUF。
另外两个容易漏的:MEMP_NUM_TCP_PCB决定同时能有多少条 TCP 连接,默认 5 在做 echo 测试时够用,但一旦有客户端反复短连接,很容易被占满;MEMP_NUM_PBUF是单独分配的 pbuf 数量,和池化的PBUF_POOL_SIZE不是一回事,发送路径上会用得更多。
3.3 裸机和 FreeRTOS 两套骨架的差异点
裸机(NO_SYS=1)的骨架很简单:主循环里周期性调sys_check_timeouts(),接收中断里把帧推进ethernetif_input,所有 LwIP 调用都在主循环上下文完成。
/* 裸机主循环:所有 LwIP API 都在这个上下文里调用,不要在中断里调 */ while (1) { if (rx_frame_ready) { rx_frame_ready = 0; ethernetif_input(&gnetif); /* 内部会走 netif->input */ } sys_check_timeouts(); /* 驱动 LwIP 的定时器,ARP/ TCP 重传都靠它 */ MX_LWIP_Process(); /* CubeMX 生成的封装,本质同上 */ }FreeRTOS(NO_SYS=2)则要把ethernetif_input放到tcpip_thread的上下文里执行,中断只负责发信号量。这一步用错了,现象是“跑几分钟就死”,因为 LwIP 的很多内部结构不是可重入的。
还有一个 F429 特有的硬约束必须在这里提:ETH 的 DMA 描述符和收发缓冲区绝对不能放在 CCM(0x10000000 起始的那 64KB)里,因为 CCM 挂的是 CPU 私有总线,DMA 根本访问不到。表现出来就是发送描述符的 own 位永远不归零、一帧都发不出去。同时 F429 带 D-Cache,如果 ETH 缓冲区落在可缓存区又没做 MPU 配置,收上来的帧可能是旧数据,典型的“看起来收到了但内容不对”。
/* F429:把 ETH 缓冲区所在区域配成 non-cacheable,避免 DMA 与 D-Cache 不一致 */ MPU_Region_InitTypeDef mpu = {0}; HAL_MPU_Disable(); mpu.Enable = MPU_REGION_ENABLE; mpu.Number = MPU_REGION_NUMBER0; mpu.BaseAddress = 0x2001C000; /* SRAM3,放 ETH 缓冲区 */ mpu.Size = MPU_REGION_SIZE_32KB; mpu.SubRegionDisable = 0x00; mpu.TypeExtField = MPU_TEX_LEVEL1; mpu.AccessPermission = MPU_REGION_FULL_ACCESS; mpu.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE; mpu.IsShareable = MPU_ACCESS_NOT_SHAREABLE; mpu.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; /* 关键:不走 D-Cache */ mpu.IsBufferable = MPU_ACCESS_BUFFERABLE; HAL_MPU_ConfigRegion(&mpu); HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);这段配置的作用范围要按你实际链接脚本里 ETH 段的位置来改,0x2001C000只是 F429 上 SRAM3 的起始地址示例。改完之后记得同步改链接脚本,否则编译器会把 ETH 缓冲区放回 SRAM1,MPU 配了个寂寞。
4. 从 ping 到 TCP echo:三个里程碑和对应的观测手段
调试 STM32 以太网最忌讳“一把梭”——把所有代码写完再上电,然后面对一个不工作的系统。我习惯把它拆成三个可独立验证的里程碑,每个里程碑都有明确的观测手段,过不了就不往下走。
4.1 里程碑一:MDIO 能稳定读到 PHY ID
这一步只依赖 SMI 两根线(MDC/MDIO)和 PHY 的供电、复位,跟 MAC 的 DMA、LwIP 都没关系。能读到 0x0007C0F1,说明 PHY 活着、地址对、MDC 时序没问题。读不到就三件事:地址错、上拉电阻没焊、PHY 没复位起来。
顺带在这里把链路状态也读一次:
/* 轮询链路状态:自动协商需要时间,别在初始化里死等 */ static uint8_t phy_link_up(uint16_t addr) { uint32_t bsr = 0; HAL_ETH_ReadPHYRegister(&heth, 0x01u, &bsr); /* BMSR */ return ((bsr & (1u << 2)) != 0u) ? 1u : 0u; /* bit2: link status */ }BMSR 的 bit2 是只读且锁存为低的,读一次就会清掉“曾经断链”的标志,适合做边沿检测。上电后自动协商通常需要 1~3 秒,初始化里写死HAL_Delay(2000)不算错,但更稳的做法是把链路检测放到主循环里轮询,链路建立后再调netif_set_link_up。
4.2 里程碑二:ARP 和 ICMP 能通
链路起来之后,先用静态 IP 保证netif配置正确,再谈 DHCP。静态 IP 的配置错误率低得多,出问题时也容易排除是 DHCP 服务器的问题。
IP4_ADDR(&ipaddr, 192, 168, 1, 50); IP4_ADDR(&netmask, 255, 255, 255, 0); IP4_ADDR(&gw, 192, 168, 1, 1); netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, ðernetif_init, ðernet_input); netif_set_default(&gnetif); netif_set_up(&gnetif);配完之后从 PC 上ping 192.168.1.50。ping 不通时第一件事不是改代码,而是用 Wireshark 抓 PC 网口的包,看两件事:PC 有没有发出 ARP 请求,开发板有没有回 ARP 应答。如果 ARP 有来有回但 ICMP 不通,问题在 IP 层往上;如果 ARP 请求发出去没有任何回应,问题在 MAC 或 PHY 层,回到里程碑一。
这一步还有个小技巧:把开发板的 MAC 地址设成一个可识别的值(比如02:00:00:00:00:01),抓包时一眼就能在 Wireshark 里认出来,比翻 IP 快。
4.3 里程碑三:一个能跑通的 TCP echo 服务
TCP 这一层最容易出的问题是“连接建起来了,一发数据就断”。下面这个 echo 回调是最小可用版本,关键点在tcp_recved和发送窗口检查:
static err_t echo_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err) { if (err != ERR_OK) { /* 上层已经报错,直接释放 */ if (p != NULL) pbuf_free(p); return err; } if (p == NULL) { /* 对端关闭连接 */ tcp_close(tpcb); return ERR_OK; } tcp_recved(tpcb, p->tot_len); /* 必须调用,否则接收窗口会越来越小直到为 0 */ if (tcp_sndbuf(tpcb) >= p->tot_len) { tcp_write(tpcb, p->payload, p->tot_len, TCP_WRITE_FLAG_COPY); } else { /* 发送窗口不够:返回 ERR_MEM,LwIP 会保留 pbuf 稍后重试 */ return ERR_MEM; } pbuf_free(p); return ERR_OK; }参数说明:tcp_recved的第二个参数是本次消费的字节数,漏调它是最常见的“跑一会儿就不动了”的原因;TCP_WRITE_FLAG_COPY让 LwIP 复制一份数据,这样调用方可以立刻释放 pbuf,代价是多一次内存拷贝,在 F429 上这点开销可以接受。ERR_MEM的语义是“我现在处理不了,你稍后再来”,LwIP 会保留 pbuf 并在窗口恢复后重新回调,所以返回它的时候千万不要自己pbuf_free。
注册回调的顺序也有讲究:先tcp_arg,再tcp_recv,最后tcp_accept里返回ERR_OK。顺序反了在部分 LwIP 版本上会出现 accept 回调里arg为空的情况。
5. 避坑排查:五条真实踩坑记录
这一节记的都是我在这套组合上真实遇到过的现象。每一条都按“现象、原因、解决”写,方便你对照自己板子的表现快速定位。
5.1 网口灯亮,ping 不通,Wireshark 里连 ARP 都没有
现象:PHY 的链路灯常亮,PC 侧网口也显示已连接,但抓包看不到任何来自开发板的帧,开发板也收不到 ARP 请求。
原因:MAC 没有切到 RMII 模式,或者 REF_CLK 没起来。前者是SYSCFG->PMC的MII_RMII_SEL位没置,后者是 PHY 的 50MHz 输出没使能。
解决:先按 2.2 节确认那个位,再用示波器量 PA1 有没有 50MHz。两件事都确认了还不行,去看 ETH 的 DMA 状态寄存器,DMASR里的RUS(接收缓冲不可用)和TUS(发送缓冲不可用)位能告诉你 DMA 是不是根本没启动。
5.2 开发板上电后,PHY 有时能协商成功,有时一直不亮
现象:同一块板子,冷启动十次里有两三次链路起不来,重新上电又好了。
原因:PHY 复位释放和 SMI 首次访问之间的时序不够。LAN8720A 释放复位后内部初始化需要时间,如果HAL_ETH_Init紧接着就去读寄存器做自动协商,偶尔会读到无效值,协商流程直接走偏。
解决:把 2.3 节的HAL_Delay(30)加大到 50ms 以上,并且在HAL_ETH_Init之前先读一次 PHY ID 确认能通信。更稳的做法是把自动协商改成异步:初始化里只启动协商,主循环里轮询 BMSR 的 bit5,协商完成后再netif_set_link_up。
5.3 ping 通,TCP 一建连就断,或者连上后收不到数据
现象:ICMP 完全正常,PC 上用工具连 80 端口,连接建立后立刻被 RST,或者数据发过去没有任何回应。
原因:三种可能——tcp_accept回调没注册或者返回了非ERR_OK;MEMP_NUM_TCP_PCB被占满(短连接场景下尤其常见);接收回调里漏了tcp_recved,窗口很快降到 0。
解决:先看lwip_stats.memp[MEMP_TCP_PCB]->used是不是贴着上限,再检查 accept 回调的返回值,最后在tcp_recv回调里加一行打印确认它被调用了。漏tcp_recved的典型特征是“前几个包正常,之后彻底静默”。
5.4 小包通、大包丢,ping 64 字节正常,ping 1472 字节超时
现象:小包一切正常,一旦包长超过某个值就丢,TCP 传输大文件时吞吐极低。
原因:PBUF_POOL_BUFSIZE太小,一个完整以太网帧要拆成多个链式 pbuf;或者 ETH 的接收缓冲区尺寸配置得比 MTU 小;再或者 D-Cache 没处理好,收上来的帧内容被缓存里的旧数据覆盖。
解决:先把PBUF_POOL_BUFSIZE改到 1536,再看ETH_RX_BUF_SIZE是不是也够。如果改完还是丢,检查 MPU 配置有没有覆盖到 ETH 缓冲区所在的段,用SCB_CleanInvalidateDCache_by_Addr在接收路径上做一次手动维护也能验证这个猜测。
5.5 跑几分钟到几十分钟后死机,串口最后一行是 HardFault
现象:短时间测试完全正常,长时间运行后随机进 HardFault,没有固定复现路径。
原因:最常见的是 ETH 中断优先级配错——在 FreeRTOS 下调用了 ISR 版本的 API,但抢占优先级数值小于configMAX_SYSCALL_INTERRUPT_PRIORITY。其次是 pbuf 泄漏,某条错误路径上pbuf_free漏了,内存池慢慢耗尽后pbuf_alloc返回 NULL,后面的代码没判空。
解决:先把 ETH 中断优先级调成数值更大的值(优先级更低),再打开LWIP_STATS和MEMP_STATS,周期性打印各池的used和max。如果某个池的used随时间单调上升,基本就能锁定泄漏点了。
6. 压测与长期验证:把“能跑”变成“敢交付”
功能通了只是及格线,真正决定这份代码能不能交付的,是吞吐和长期稳定性这两个指标。我一般会在 TCP echo 跑通之后做两件事。
第一件是吞吐基线。PC 端用 iperf 或者一个简单的 TCP 发送脚本,MCU 端只收不回,统计一分钟内的平均带宽。F429 加 LAN8720A 这套组合在 100M 全双工、TCP_MSS=1460、TCP_SND_BUF给到 8 个 MSS 的条件下,跑到 40~60Mbps 是正常区间;如果只有几 Mbps,依次检查PBUF_POOL_BUFSIZE、D-Cache 配置、以及tcp_write是不是每次都带TCP_WRITE_FLAG_COPY导致多了一次拷贝。
第二件是 7×24 稳定性。做法很土但有效:PC 端脚本每 5 秒发一次 ping、每 30 秒建一条短 TCP 连接发 1KB 数据再关掉,开发板端每 10 分钟往串口打一行统计:
#if LWIP_STATS /* 周期性打印内存池水位,比等 HardFault 再看要好得多 */ printf("mem %u/%u pbuf %u/%u tcp_pcb %u/%u\r\n", lwip_stats <p> <a href="https://download.csdn.net/download/weixin_43482414/12895186" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>