今年年初老客户那边反馈设备在项目现场偶发网络中断而且部分设备重启之后能恢复另一部分怎么重启都连不上。这批设备的主控是 STM32F207跑着 FreeRTOS 加 lwIP以太网驱动还是当年从 ST 官网直接扒下来的 stm32f2x7_eth_driver配套标准外设库一用就是五六年。借着这个契机我把 STM32F2x7 的 Ethernet 驱动链路完整更新了一遍顺手解决了 PHY 换型后识别不了、大流量吞吐跑不满两个积压已久的问题。整个过程踩了不少坑也把驱动从标准库平滑挪到了 HAL 库风格并重新梳理了与 FreeRTOS 的配合方式。这篇就从头到尾把这个驱动更新的思路、关键差异和实测数据写透给正在折腾 STM32F2x7 以太网的朋友一个可以直接抄的作业。1. 为什么旧驱动扛不住了性能瓶颈和新 PHY 的不兼容1.1 现场问题的三个典型表现先说最直观的现场症状。老代码在主控刚上电时一切正常能通过 DHCP 拿到地址也能和上位机保持 TCP 长连接但设备运行一两天后偶尔会出现 TCP 连接断开且无法重连的情况。抓日志发现lwIP 的链路回调一直报 link down但 PHY 寄存器读回来又显示 link up两个状态互相矛盾。另一个硬件同事反馈因为原型号 PHY 停产采购把 PHY 换成了另一家的兼容型号结果 MDIO 读不到芯片 ID驱动里基于原厂 PHY 的初始化逻辑完全失效。第三个问题更烦设备做 100M 以太网大文件传输测试时实际速度一直在 4 到 6 MB/s 徘徊和标称 100Mbps 差了一大截。这三个问题表面看是不同故障实际都指向同一个根因旧版 stm32f2x7_eth_driver 太老了它是以 ST 早期的评估板驱动为蓝本写的对 PHY 的假设非常强轮询方式也太粗糙。1.2 旧驱动在架构上的硬伤我后来重新翻了旧驱动的代码问题很清楚。它把以太网 DMA 的收发逻辑放在一个 while 循环里轮询DMA 描述符的 ownership 位靠 CPU 反复读取判断这本身体现不出来问题但当系统里同时跑着 FreeRTOS 的多个任务时轮询以太网驱动会持续占用 CPU导致其他任务被饿死。而且旧驱动对 PHY 的寄存器初始化是写死的它默认 PHY 的中断引脚、复位引脚和 MDIO 地址都是评估板上的接法碰到实际产品里 PHY 引脚改过、PHY 换型号的情况第一步就卡住了。另外一个隐藏很深的坑是描述符结构体。旧驱动把 ST 官方标准库里的大结构体直接暴露给应用层如果应用层代码稍微改了几个字段或者编译器对齐方式不同DMA 描述符内存布局就乱了。这类问题不会在编译时报错只会在运行一段时间后莫名其妙收不到数据。1.3 判断你的项目是否也该做驱动更新不是所有项目都需要折腾驱动更新但如果命中下面几条我建议尽早动手需要支持新的 PHY 芯片而当前驱动里没有对应初始化代码网络吞吐长期达不到理论带宽的 80% 以上设备在长时间运行后出现网络假死、连接断开无法恢复ST 官方已经停止维护你手里的标准外设库版本新的 CubeMX 工具链无法直接生成兼容代码想把网络收发逻辑从裸机轮询改成中断驱动并更好地和 FreeRTOS 调度配合我当时项目四条全占这不做更新不行了。2. 更新前的资源盘点芯片丝印、PHY 芯片和 50MHz 时钟链路2.1 先确认你手里的芯片具体是哪个型号STM32F2x7 是一个系列的说法具体到芯片常见的有 STM32F207VCT6、STM32F207VGT6、STM32F217VGT6。F207 和 F217 在以太网外设上完全一致区别主要在 Flash 大小、加密模块和部分封装引脚。真正要区分的是同一个封装下 F205/215 和 F207/217 的差别前者没有以太网 MAC后者才有。网上很多教程直接写 STM32F2x7 驱动默认都是带以太网的型号。我这次手上的板子是 STM32F207VGT6100 引脚 LQFP内部有 1MB Flash主频跑 120MHz以太网 MAC 支持 10M/100M带独立的 DMA 控制器。只有确认了具体丝印和参考手册才敢照着寄存器手册去改驱动否则后面一步错步步错。2.2 看原理图明确 PHY 芯片型号与接口模式STM32F2x7 内置以太网 MAC 要接外部 PHY 芯片才能干活。PHY 和 MAC 之间的接口有两种MII 和 RMII。MII 需要 16 根数据/控制线信号多但时序要求相对宽松RMII 只需要 7 根线但要求 PHY 提供 50MHz 的 REF_CLK 时钟。我这次产品板子上用的是 RMII 接法PHY 是 Microchip 的 LAN8720A地址默认是 0也可以通过外部引脚拉成 1。原理图上 PHY 的 REF_CLK 是从 STM32F207 的 MCO 引脚引过去的 50MHz。这种接法在低成本产品里非常常见但更新驱动时必须要确认 PHY 实际的 MDIO 地址和时钟来源不要照抄评估板的配置。2.3 时钟链路是这次更新里最容易踩的坑STM32F2x7 的以太网 MAC 需要两个时钟源一个是系统 AHB 总线时钟用于 MAC 内核寄存器访问另一个是 RMII 模式下 PHY 提供的 50MHz REF_CLK。这里的坑在于lwIP 的 BSP 里面通常直接把 MAC 内核时钟频率写进 HAL_ETH_Init 的 pInit 结构体里。如果系统主频是 120MHzAHB 分频后给 ETH 的是 60MHz 或 120MHz那么初始化以太网 MAC 时必须把EthInitStructure.ClockRange或 HAL 对应字段设置为匹配的值。我看到过有人把老驱动里的 25MHz 参数原样拷到新板子上导致 MAC 收发数据包时偶发错位表现就是 ping 大包总丢小包没问题。这种问题用示波器查不到只能从配置参数上排查。2.4 复位引脚和中断引脚不能省PHY 的复位引脚通常接到 MCU 的 GPIO有的产品直接接 RC 上电复位。可以用 GPIO 控制复位的话驱动初始化前先拉低再拉高确保 PHY 处于确定状态。中断引脚可选但我强烈建议留出来接到 MCU 的外部中断上这样 PHY 检测到链路变化时可以主动通知 MCU而不必靠轮询。旧驱动里如果没接这个引脚程序只能靠周期读 PHY 状态寄存器判断链路实时性差还容易漏事件。3. 新驱动文件落地从标准库到 HAL 的关键差异与寄存器对照3.1 新旧驱动的文件结构对比老项目用的是 ST 标准外设库风格驱动文件是 stm32f2x7_eth.c 和 stm32f2x7_eth.h再加上应用层的 ethernetif.c。新版驱动换成 HAL 库形式后核心文件是 stm32f2xx_hal_eth.c、stm32f2xx_hal_eth.h底层仍然依赖 stm32f2xx_hal_rcc.c 和 stm32f2xx_hal_gpio.c。两者名字看上去都是 ETH但 API 设计完全不同不能直接改名替换。对比项旧驱动标准外设库新驱动HAL 库初始化入口ETH_Init(ETH_InitTypeDef*)HAL_ETH_Init(ETH_HandleTypeDef*)DMA 描述符管理直接操作 ETH_DMADescTypeDef 数组HAL 内部管理 Tx/Rx 描述符链表PHY 读写直接 MDIO 寄存器操作HAL_ETH_ReadPHYRegister / WritePHYRegister中断处理ETH_IRQHandler 手动清标志HAL_ETH_IRQHandler 统一分发链接状态应用层自己轮询HAL_ETH_GetLinkState / 回调机制这个表格里的差异不是简单的函数改名而是驱动边界的变化。HAL 库把很多细节封装到了内部应用层代码更干净但代价是如果出了问题追踪起来需要更熟悉 HAL 的实现方式。3.2 初始化流程的对应关系旧驱动的初始化顺序是使能 ETH 时钟和 GPIO 时钟配置 GPIO 复用功能然后调用 ETH_Init 一次性设置 MAC、DMA、描述符。新驱动的初始化顺序类似但被拆成了多个步骤ETH_HandleTypeDef heth; heth.Instance ETH; heth.Init.MACAddr mac_addr; heth.Init.MediaInterface HAL_ETH_RMII_MODE; heth.Init.TxDesc (ETH_DMADescTypeDef *)tx_desc_array; heth.Init.RxDesc (ETH_DMADescTypeDef *)rx_desc_array; heth.Init.RxBuffLen 1536; HAL_ETH_Init(heth); HAL_ETH_ConfigMAC(heth, mac_init); HAL_ETH_ConfigDMA(heth, dma_init); HAL_ETH_Start_IT(heth);看起来步骤差不多但因为 HAL 的HAL_ETH_Init内部会自动读取 PHY 的 ID 寄存器如果 PHY 没有正常复位或 MDIO 引脚配置不对这个函数会直接返回 HAL_ERROR。老驱动里通常没有这么严格的检查所以同样的板子老代码能跑新代码初始化失败先从 PHY 复位时序和 MDIO 引脚配置查起。3.3 描述符结构DMA 能访问到的内存才行STM32F2x7 的以太网 DMA 使用描述符链表管理收发缓冲区每个描述符由 4 个 32 位字组成。旧驱动习惯把描述符定义成一个全局数组编译器放在哪儿都能用。新版驱动中描述符数组必须放在 DMA 可以访问的内存区域而且地址要按 4 字节对齐。我在这次更新里遇到了一个奇怪现象编译过后程序烧进去能进网但发几个包后 DMA 就卡死。查到最后是描述符数组被链接到了外部 SDRAM 的区域虽然地址落在 4 字节对齐的边界但外部存储器的访问时序和以太网 DMA 的 Burst 模式不匹配。解决办法是把描述符数组放到内部 SRAM使用__attribute__((section(.ARM.__at_0x20000000)))这类方式显式指定位置同时确认缓冲区也做了对齐。__attribute__((aligned(4))) ETH_DMADescTypeDef tx_desc[ETH_TX_DESC_CNT]; __attribute__((aligned(4))) ETH_DMADescTypeDef rx_desc[ETH_RX_DESC_CNT]; __attribute__((aligned(4))) uint8_t rx_buff[ETH_RX_DESC_CNT][1536];这个习惯要养成不要依赖编译器的默认放置。3.4 寄存器级避坑MACCR 和 DMABMR 的几个位不能乱动如果 HAL 封装不能满足你的定制需求还是得回去翻寄存器。STM32F2x7 的以太网核心寄存器主要有 MACCR、MACFFR、DMAOMR、DMABMR 等。MACCR 里的 RE、TE 位控制 MAC 收发使能更新驱动时如果有人直接操作寄存器容易把之前配置的自动协商、回环模式这些位覆盖掉。HAL 库里面的HAL_ETH_ConfigMAC会按传入参数设置这些位相对安全。DMABMR 里有两个位需要特别注意FBFixed Burst和 DADMA Arbitration。开启 FB 后 DMA 使用固定突发长度吞吐量会明显提升但外部 SDRAM 或某些 AHB 桥不支持这种突发模式时会导致 DMA 传输失败。这次调吞吐量时我把 FB 打开速度从 5MB/s 提到了 8MB/s但跑到高负载时偶尔卡死关掉 FB 后稳定性恢复速度略降到 7MB/s。最终权衡下来还是开着 FB但把内存区域放到了内部 SRAM问题就消失了。3.5 PHY 驱动从写死改成参数化旧驱动里 PHY 初始化是硬编码的比如把 BCR 寄存器直接设置为 0x1000 开启自动协商。不同 PHY 的寄存器布局虽然基本遵循 IEEE 802.3但厂商的扩展寄存器差异很大例如 LAN8720A 的 RXER 计数器、DP83848 的 LED 控制。新驱动里我建议把 PHY 驱动单独拆出一个文件通过一个结构体描述 PHY 的 MDIO 地址、ID、复位引脚、中断引脚以及需要在初始化阶段写入的寄存器列表。这样以后换 PHY 型号只需要改表的配置不用动驱动主流程。4. FreeRTOS 接管后的任务切换、中断优先级和 DMA 内存规划4.1 以太网驱动挂在任务里还是中断里这个问题我在项目初期纠结了很久。旧驱动是纯轮询方式在 while 循环里不断调用 ethernetif_input 处理接收包。升级驱动后如果仍然把接收逻辑放到一个高优先级任务里它和主任务抢 CPU 的问题不会消失。更好的方案是把以太网 DMA 接收中断打开在中断服务函数里只做两件事清中断标志、给 tcpip_thread 发信号量。真正的数据包解析、协议栈处理由 lwIP 的 tcpip_thread 完成。这样以太网驱动不会占着 CPU 不放系统整体响应更好。4.2 中断优先级配置不是越大越好越小反而危险FreeRTOS 的configMAX_SYSCALL_INTERRUPT_PRIORITY决定了哪些中断可以调用 FreeRTOS 的 API。以太网中断优先级必须设置得比这个“安全阈值”更低也就是数值上更大否则中断里调xSemaphoreGiveFromISR时可能破坏临界区。STM32F2x7 使用 4 位优先级数值从 0 到 150 最高。我项目里把configMAX_SYSCALL_INTERRUPT_PRIORITY设为 5ETH 中断优先级设为 6PHY 外部中断设为 7。这样保证以太网和 PHY 中断都能正常调用 FreeRTOS 的 FromISR API同时又不和 systick、PendSV 冲突。#define configMAX_SYSCALL_INTERRUPT_PRIORITY 5 // HAL_NVIC_SetPriority(ETH_IRQn, 6, 0); // HAL_NVIC_SetPriority(ETH_WKUP_IRQn, 7, 0);4.3 中断服务程序设计不要在中断里搬数据以太网中断触发后DMA 已经把数据放到了 rx_buff 数组里中断里不需要再搬一次数据。我看到有些人习惯在中断里调用 memcpy 把数据拷到另一个缓冲区这在低负载时没问题但在高速收发时会导致中断处理时间过长后续中断被淹掉。正确做法是中断里只设置一个标志并唤醒接收任务接收任务里再把 rx_buff 中的数据交给 lwIP 的 PBUF。如果使用零拷贝连 memcpy 都省掉直接把新分配的 pbuf 地址映射到 DMA 描述符缓冲区这需要更细致的内存池设计。我这次更新先用了普通拷贝版本稳定性优先等测到性能瓶颈再考虑零拷贝。4.4 DMA 缓冲区内存规划内部 SRAM 优先STM32F207 内部 SRAM 总共 128KB其中 64KB 是普通 SRAM164KB 是 SRAM2。以太网 DMA 描述符和缓冲区放在 SRAM1 和 SRAM2 都能访问但要注意 4 字节对齐。我最终给以太网驱动划分的内存布局如下项目起始地址大小说明Tx 描述符数组0x200000004 * 4B * 8 128BTX_DESC_CNT 8Rx 描述符数组0x200001004 * 4B * 8 128BRX_DESC_CNT 8Rx 数据缓冲区0x200002008 * 1536 12288B每个缓冲区 1536 字节FreeRTOS 堆栈其余 SRAM动态分配heap_4 管理不要把描述符和缓冲区放在 CCM RAM 里STM32F2 系列没有 CCM 这种 DMA 不可访问的区域但如果你用 F4 的代码习惯可能会把内存放到__attribute__((section(CCM)))F2 上没有这个区编译直接报错。4.5 lwIP 内存配置也要跟着驱动一起更新驱动更新容易忽略 lwIP 的配套参数。lwIP 的内存池大小和 pbuf 数量如果太小网络在高负载下一样丢包。根据 1536 字节的 MTU我调整了以下几个关键配置#define MEM_ALIGNMENT 4 #define PBUF_POOL_SIZE 16 #define MEMP_NUM_PBUF 16 #define MEMP_NUM_TCP_SEG 32 #define TCP_SND_BUF 8 * 1024 #define TCP_WND 16 * 1024 #define MEM_SIZE (12 * 1024)这些值不是越大越好因为每多一份内存就少一分给应用任务的空间。我实测下来8 个收发描述符加 16 个 pbuf 池在 100M 网络下已经能稳定跑满 90% 带宽。5. 实测数据从能 Ping 通到稳定 11.5MB/s 的调优路径5.1 第一步先把链路层调通别急着跑 TCP/IP更新驱动后的第一个测试不是 ping而是看 PHY 的链接状态。我写了一段最简单的测试代码上电后循环读取 PHY 的 BSR 寄存器打印 link status 和 speed/duplex。确认link up后再用 MDIO 读 PHY ID 寄存器和 datasheet 上的值对照。这一步通过后才去配置 MAC 和 DMA。如果这一步就卡住回去查 PHY 复位、MDIO 上拉电阻、REF_CLK 是不是真的有 50MHz 方波。特别是 REF_CLK我很推荐用示波器量一下比盲猜快得多。5.2 ping 通了但不稳定描述符数量和中断阈值的关系我当时第一次跑通 ping 后连续 ping 10000 个包总有十来个包延迟偏高偶发丢包。排查了很久最终发现是 DMA 接收中断触发条件太激进。STM32F2x7 的 DMAOMR 寄存器里可以配置接收中断在收到多少帧后触发RTC 00每收到一帧都触发RTC 01每收到两帧触发RTC 10每收到四帧触发RTC 11每收到八帧触发如果每帧都触发CPU 中断负载太高如果四帧触发一次延迟又变大。我最后设置成两帧触发一次配合 FreeRTOS 的接收任务丢包率降到 0。5.3 用 iperf 测吞吐对比新旧驱动我在 PC 端用 iperf 测试 TCP 发送和接收吞吐设备端跑 lwIP 的 TCP echo 服务结果对比如下测试项旧驱动轮询新驱动中断 HALTCP 下行PC→设备5.2 MB/s11.5 MB/sTCP 上行设备→PC4.1 MB/s10.8 MB/sUDP 下行 1472B4.8 MB/s11.2 MB/sCPU 占用率满速时87%41%11.5MB/s 大约等于 92Mbps已经接近 100M 以太网的理论上限约为 11.8MB/s。优化的主要功劳来自三块中断驱动替代轮询、DMA 固定突发模式、FreeRTOS 的及时调度。旧驱动用轮询方式不仅速度慢CPU 也被吃掉大半几乎没办法同时跑其他任务。5.4 测试时长必须够长别信五分钟数据吞吐测试只能说明瞬时性能长期稳定性是另一回事。我在调优阶段让设备持续跑 TCP 发送、接收、双向混合三种模式每种至少跑 12 小时。期间监控任务栈高水位、FreeRTOS 堆剩余量、以太网 DMA 描述符的使用情况。有个很值得注意的现象新驱动在刚启动时表现很好但连续跑几个小时后如果某个描述符状态没有及时清理接收方向会慢慢降速。这个问题在普通测试中看不出来必须跑长时间才能发现。根因是 HAL 的接收中断处理里如果一帧数据校验失败描述符的 ownership 位没有正确归还给 DMA导致后续帧无法接收。处理办法是在接收中断里检查描述符错误标志错误帧直接归还描述符不做协议栈处理。5.5 用任务运行时间统计验证调度合理性调优最后我打开 FreeRTOS 的configGENERATE_RUN_TIME_STATS实际看每个任务占用 CPU 的百分比。tcpip_thread 在高负载时大约占 60% CPU接收任务占 20%主业务任务只占 10%剩下给空闲任务。这个分布说明以太网驱动已经把大量工作交给了 lwIP 自己的线程没有在主任务里阻塞太久整体调度是健康的。如果接收任务占满 CPU多半是每次中断后没有及时给 tcpip_thread 让出执行权或者描述符数量太少导致接收任务频繁空转。6. 三个反复踩的坑以及我最终留下的稳定配置6.1 PHY 复位时序导致的初始化失败第一次用新驱动时HAL_ETH_Init老是返回 HAL_ERROR但同样的硬件在旧驱动里明明能跑。我一度以为新驱动有问题后来抓了时序才发现PHY 的复位引脚是 RC 上电复位芯片上电后 PHY 需要十几毫秒才能准备好 MDIO 通信。旧驱动没有做 MDIO 读操作自然感知不到这个时序问题新驱动在初始化 MAC 前会尝试读 PHY 的 ID所以 PHY 没准备好就直接失败了。解决办法是在调用 HAL_ETH_Init 前加上延时并且让驱动每次都主动控制 PHY 复位引脚HAL_GPIO_WritePin(PHY_RST_PORT, PHY_RST_PIN, GPIO_PIN_RESET); HAL_Delay(50); HAL_GPIO_WritePin(PHY_RST_PORT, PHY_RST_PIN, GPIO_PIN_SET); HAL_Delay(100);之后 MDIO 读 PHY ID 一次通过。这个经验提醒我新驱动检查更严格不代表兼容性更差它只是把以前被掩盖的问题暴露出来了。6.2 中断标志未清干净导致的死循环HAL 库版本的以太网中断处理里如果没有正确清除中断标志会频繁进入中断表现为程序卡在HAL_ETH_IRQHandler中出不来。我遇到的情况是 ETH DMA 的接收中断标志只清了一半导致进入中断后马上再次触发。调试方法是在 KEIL 里打断点观察 ETH-DMASR 寄存器的值每次进入中断后记录哪些标志位被置 1。正常流程下应该同时确认DMASR的 NIS 和 RIS 位被清除。HAL 库的HAL_ETH_IRQHandler内部会处理大部分标志但如果你在中断里额外做了HAL_ETH_ReadPHYRegisterMDIO 操作会因为占用总线而延迟不要放在中断里做。6.3 -O2 优化等级下的描述符状态异常这个坑比较隐蔽。程序在 -O0 下跑得好好的换成 -O2 优化后网络吞吐直接掉到原来的一半偶尔还出现发送卡死。查了几天才发现编译器认为描述符里的状态字段在循环中不会被外部修改于是把某些读取操作优化掉了。DMA 硬件在后台写描述符CPU 读到的却是缓存的值。解决方法是把描述符结构体里的状态字段声明成 volatile或者在读取前加内存屏障#define ETH_READ_DESC_OWN(desc) (((volatile ETH_DMADescTypeDef *)(desc))-Status)STM32F2 的 Cortex-M3 没有数据缓存不像 F7/H7 那样有 cache 一致性问题但在高优化等级下编译器对内存访问的重排和优化仍会造成类似现象定义 volatile 是最稳妥的处理。6.4 最终固化的稳定配置经过一周的折腾我把最终验证过的配置整理成一张表之后换板子、换 PHY 都直接按这个模板套配置项取值说明HAL 库版本STM32Cube_FW_F2_V1.27较新稳定版lwIP 版本2.1.2配合 HAL 的 ethernetif 示例PHY 芯片LAN8720AMDIO 地址 0RMIIREF_CLKMCO 输出 50MHz测量确认频率稳定Tx 描述符数8可调Rx 描述符数8可调Rx 缓冲区大小1536B对齐到 4 字节中断优先级ETH6, PHY7configMAX_SYSCALL_INTERRUPT_PRIORITY5DMA 突发模式Fixed Burst内部 SRAM 时开启lwIP PBUF_POOL_SIZE16实测稳定这套配置在压力和长稳测试下跑了半个月表现稳定。最后说一句很实际的体会驱动更新最怕的不是代码量大而是对底层机制理解不透彻碰到问题只能靠试。我在这次更新中最大的收获其实是把 STM32F2x7 以太网从“黑盒”变成“灰盒”知道它怎么初始化、怎么搬数据、怎么和 FreeRTOS 配合之后再遇到网络疑难杂症至少知道往哪个方向查。希望这篇经验能帮你少走几步弯路。