STM32F207 HAL库+lwIP+FreeRTOS以太网驱动升级实践

发布时间:2026/8/29 15:24:42
STM32F207 HAL库+lwIP+FreeRTOS以太网驱动升级实践 手上这个项目用了 STM32F207也就是 F2x7 系列带以太网 MAC 的那一档跑着 FreeRTOS lwIP。原来用的以太网驱动是好几年前基于标准外设库写的当时能用但最近要做功能升级顺手把驱动整体换成了 STM32CubeF2 HAL 库的 ETH 驱动lwIP 也从 1.4.1 升到 2.1.2。整个过程不是简单的“替换文件”驱动底层的 DMA 描述符管理、中断回调、RTOS 同步方式、PHY 处理逻辑全都要跟着调。这篇文章把我这次驱动更新的完整思路、关键改动点、实际操作步骤和遇到的坑都记下来给同样在 F2x7 上做 Ethernet 驱动升级的朋友做个参考。不废话直接进入正文。1. 项目背景与驱动更新思路1.1 为什么要动这块驱动老驱动的来源已经不可考大概率是从标准外设库的 ETH 例程改过来的配合 lwIP 1.4.1跑的是裸机轮询 中断混合模式。最初功能简单只要 TCP 通信、远程参数读取所以一直没出大问题。但今年要加 OTA 升级对 TCP 传输的稳定性和吞吐有更高要求老驱动的问题就暴露了。先说现象长时间大流量传输时偶发丢包偶尔出现网卡“假死”ping 不通只有复位芯片才能恢复。查了老驱动的代码接收描述符的 OWN 位赋值、环形缓冲区的头尾指针更新都是直接操作寄存器没有统一的状态机管理一旦在中断里被更高优先级任务打断极容易丢描述符。而且老驱动里 PHY 的初始化是写死寄存器地址换个板子 PHY 芯片不同就可能起不来。这些痛点直接催生了驱动更新。另外标准外设库早就停止维护厂商主推 HAL 库。HAL 库对 ETH 的封装更完整提供了 DMA 描述符初始化、接收/发送完成回调、PHY 读写抽象配合 FreeRTOS 时可以把回调里的事件转成信号量或任务通知逻辑清楚很多。所以这次更新既是功能需求也是技术债清偿。1.2 更新方案选型HAL 库 vs 标准外设库选择 STM32CubeF2 的 HAL 库驱动不是因为标准库不能用了而是 HAL 库在几个关键点上明显更省心。第一个是描述符管理。HAL 库把 ETH DMA 描述符封装成ETH_DMADescTypeDef提供了HAL_ETH_DescAssignMemory、HAL_ETH_Start、HAL_ETH_ReadData等 API接收和发送的状态转移都在 HAL 内部维护不再需要手动操作 RDES0/TDES0 的 OWN 位。虽然底层还是那些寄存器但封装之后业务代码不会轻易搞坏描述符链。第二个是中断回调机制。HAL 库允许通过弱函数覆盖HAL_ETH_RxCpltCallback、HAL_ETH_TxCpltCallback这和 FreeRTOS 很搭。接收中断里把 DMA 收到的数据交给 RTOS 任务发送完成中断里释放 pbuf这些都能用回调干净地实现。第三个是 PHY 驱动抽象。HAL 库只负责 MDIO 读写PHY 芯片的具体操作可以封装成独立模块更换 PHY 时不需要大改上层代码。这次我们板子上从 LAN8720A 换到了 DP83848只改了 PHY 地址和寄存器配置上层 lwIP 网络接口完全没动。唯一需要评估的是代码量。HAL 库文件大内存占用比标准库略高。不过 F207 有 128KB RAM跑 FreeRTOS lwIP 加 HAL 驱动完全够不用太纠结。如果是资源极紧的小芯片可以考虑只保留 HAL 的 ETH 文件把用不到的外设 HAL 全部裁剪掉这样代码量能压下来不少。2. 新驱动核心改动盘点FreeRTOS 视角2.1 DMA 描述符与缓冲区管理F2x7 的以太网 DMA 使用描述符链表管理收发缓冲区。每个描述符对应一块数据缓冲区描述符本身包含缓冲区地址、数据长度、OWN 位、状态位等。DMA 循环遍历描述符发送或者接收数据。标准库时代描述符数组和缓冲区分配通常是这样ETH_DMADescTypeDef DMARxDscrTab[ETH_RXBUFNB]; ETH_DMADescTypeDef DMATxDscrTab[ETH_TXBUFNB]; uint8_t Rx_Buff[ETH_RXBUFNB][ETH_RX_BUF_SIZE]; uint8_t Tx_Buff[ETH_TXBUFNB][ETH_TX_BUF_SIZE];问题在于这些数组的对齐约束。STM32 的 ETH DMA 要求描述符地址按 4 字节对齐缓冲区地址按 4 字节对齐但在实际工程里编译器分配的地址可能满足也可能不满足一旦不对齐DMA 搬运数据就会异常。老驱动里虽然写了__align(4)之类的属性但换编译器版本后经常失效。HAL 库驱动里推荐在low_level_init中显式初始化描述符和缓冲区并强制对齐__ALIGN_BEGIN ETH_DMADescTypeDef DMARxDscrTab[ETH_RX_DESC_CNT] __ALIGN_END; __ALIGN_BEGIN ETH_DMADescTypeDef DMATxDscrTab[ETH_TX_DESC_CNT] __ALIGN_END; __ALIGN_BEGIN uint8_t Rx_Buff[ETH_RX_DESC_CNT][ETH_RX_BUF_SIZE] __ALIGN_END; __ALIGN_BEGIN uint8_t Tx_Buff[ETH_TX_DESC_CNT][ETH_TX_BUF_SIZE] __ALIGN_END;__ALIGN_BEGIN在 IAR 下是__align(32)在 GCC 下是__attribute__((aligned(32)))MDK 下也能自动适配。为什么建议 32 字节而不是 4 字节因为 F2 虽然没有 Cache但 32 字节对齐可以给 DMA 缓存行对齐留余量如果以后代码移植到 F4/F7不至于因为 Cache 一致性产生诡异问题。这次更新时我特意把描述符和缓冲区全部按 32 字节对齐后面跑压力测试没有出现数据错乱。缓冲区大小ETH_RX_BUF_SIZE我直接设成了 1524 字节也就是标准以太网帧上限 1518 6 字节 CRC或 4 字节 CRC 2 字节 padding实际 DMA 使用一般定义为ETH_MAX_PACKET_SIZE。如果 lwIP 开启了 VLAN 支持建议再留 4 字节余量改成 1528。太小会丢大帧太大浪费内存。F207 的 RAM 足够我保守起见用 1524。注意一个容易忽略的点ETH_RX_DESC_CNT和ETH_TX_DESC_CNT的数量越多DMA 越能吸收突发流量但也越消耗内存。F207 的 ETH 描述符支持 1~128 个一般项目 4 个接收描述符 4 个发送描述符就够。如果吞吐要求高可以加到 8。个人经验4 个收/4 个发TCP 吞吐能到 60Mbps 以上如果并发连接多加到 8 个接收描述符会明显减少丢包。2.2 中断回调与 FreeRTOS 同步机制这次更新的重头戏是把以太网的中断处理从“在中断函数里裸奔”改成“中断只做事件标记RTOS 任务处理数据”。这里关键是理解 FreeRTOS 的临界区和中断优先级机制。HAL 库的驱动在接收完成时会调用HAL_ETH_RxCpltCallback这个回调函数运行在以太网中断上下文中。我们不能在回调里直接调用pvPortMalloc或者任何可能阻塞的 lwIP API但可以调用带FromISR后缀的 FreeRTOS API比如xSemaphoreGiveFromISR、vTaskNotifyGiveFromISR、xEventGroupSetBitsFromISR。我采用的方式是接收中断回调里给一个二值信号量专门有一个接收任务等这个信号量信号量拿到后调用ethernetif_input旧 lwIP 里叫ethernetif_input或tcpip_input把数据从 DMA 描述符转成 pbuf然后投递给 lwIP 的 tcpip_thread。代码结构void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef *heth) { BaseType_t xHigherPriorityTaskWoken pdFALSE; xSemaphoreGiveFromISR(RxSemaphoreHandle, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } void ethernetif_input_task(void *argument) { for (;;) { if (xSemaphoreTake(RxSemaphoreHandle, portMAX_DELAY) pdTRUE) { ethernetif_input(argument); } } }这里有个极易踩的坑HAL_ETH_RxCpltCallback只表示“至少有一个包接收完成”但接收描述符里可能有多个包排队。如果每次回调只取一个包在高流量下会积压。正确做法是在ethernetif_input里遍历所有接收描述符直到取完为空。lwIP 提供的low_level_input一般会有 while 循环但有时因为我们中断和任务的配合问题导致low_level_input只处理第一个包就返回。我这次专门做了压力测试确保满负荷 100Mbps 时接收任务不会因为只取单包而掉链子。发送方向也类似。low_level_output发送 pbuf 时需要等待 DMA 发送完成。我们用信号量阻塞在发送任务上发送完成回调里给出信号量。但是注意HAL_ETH_TxCpltCallback是在中断上下文如果发送完成信号量给了之后发送任务被唤醒中途又来了新的发送请求两个任务同时操作发送描述符就会冲突。这里需要给发送路径加互斥锁我用的是xSemaphoreCreateMutex保证同一时间只有一个任务在写发送描述符。2.3 链路状态轮询与 PHY 管理老驱动对 PHY 的操作几乎为零初始化时把 PHY 的 BCR/BSR 寄存器配置一遍然后就不管了。这就导致一个问题网线拔了再插链路状态不会主动反映到 lwIPTCP 连接挂在那儿直到超时才发现对方不可达。HAL 库驱动不会替你管 PHY但配合 lwIP 的 netif 状态检测我们可以建一个任务周期轮询 PHY 状态检测到链路 down/up 就调用netif_set_link_down、netif_set_link_up让上层及时感知。F2x7 的 PHY 管理走 MDIO 接口HAL 提供了HAL_ETH_ReadPHYRegister和HAL_ETH_WritePHYRegister。轮询逻辑如下uint32_t phy_reg; HAL_ETH_ReadPHYRegister(heth, PHY_ADDRESS, PHY_BSR_REG, phy_reg); if ((phy_reg PHY_LINKED_STATUS) (netif-flags NETIF_FLAG_LINK_UP) 0) { netif_set_link_up(netif); } else if (!(phy_reg PHY_LINKED_STATUS) (netif-flags NETIF_FLAG_LINK_UP)) { netif_set_link_down(netif); }这个任务的周期我设为 500ms实际够了。如果周期太短MDIO 读取频繁会占用总线太长则链路恢复时间感知慢。500ms 能兼顾响应速度和资源占用。PHY 地址不能写死。原来板子上的 PHY 是 LAN8720A地址 0x00这次换成 DP83848地址 0x01。如果还是写死 0x00初始化时读寄存器全是 0xFFFFDMA 根本不会正常收发。更通用的做法是上电后扫描 1~31 号地址读 PHY ID 寄存器地址 2/3找到合法的 ID 再用。参考代码uint32_t id_value; for (uint8_t addr 1; addr 32; addr) { if (HAL_ETH_ReadPHYRegister(heth, addr, PHY_IMR? 或 PHY_IDR1, id_value) HAL_OK) { if (id_value ! 0x0000 id_value ! 0xFFFF) { PHY_ADDRESS addr; break; } } }这个扫描只在初始化时做一次不费时间换 PHY 芯片再也不用改代码。3. 实操记录移植与配置完整步骤3.1 准备工作拿到新驱动文件用的是 STM32CubeF2 固件包当前版本 1.7.0。里面有完整的 HAL 驱动文件主要用到这几个stm32f2xx_hal_eth.hstm32f2xx_hal_eth.cstm32f2xx_hal_eth_ex.h某些模式才需要stm32f2xx_hal_gpio.cstm32f2xx_hal_rcc.c我习惯把 HAL 库的源文件直接添加到工程的 Drivers/STM32F2xx_HAL_Driver 目录下然后统一管理编译路径。注意stm32f2xx_hal_conf.h要开启HAL_ETH_MODULE_ENABLED否则 HAL ETH 文件不会被编译进去#define HAL_ETH_MODULE_ENABLED同时要确认stm32f2xx_hal_conf.h里的HAL_MAX_DELAY、HAL_GPIO_MODULE_ENABLED等宏正常这些会影响编译。另外老工程里可能有标准外设库的stm32f2xx_eth.c一定要从编译列表里移除否则两个文件里都有ETH_Init、ETH_Start等同名函数链接会冲突。我一开始没删干净编译直接报重复定义排查了半天。3.2 修改以太网外设初始化HAL 库初始化 ETH 的流程比标准库长但结构更清晰。核心步骤使能时钟、配置 GPIO、配置 MAC、配置 DMA、启动、使能中断。GPIO 部分要注意 RMII 模式。F207 用 RMII 时TX_CLK 不需要物理引脚而是由外部 50MHz 时钟源提供。我们板子上用的是 25MHz 晶振 PHY 内部倍频到 50MHz或者外部有源晶振直接给 PHY 的 REF_CLK。STM32 的 RMII 接口对时钟要求比较高务必确认时钟源稳定否则会出现随机丢包。初始化代码参考ETH_MACConfigTypeDef MACConf {0}; ETH_DMAConfigTypeDef DMAConf {0}; heth.Instance ETH; heth.Init.MACAddr (uint8_t *)MAC_Addr; heth.Init.MediaInterface ETH_MEDIA_INTERFACE_RMII; heth.Init.PhyAddress PHY_ADDRESS; heth.Init.Mode ETH_MODE_FULLDUPLEX; heth.Init.Speed ETH_SPEED_100M; heth.Init.ChecksumMode ETH_CHECKSUM_BY_HARDWARE; heth.Init.RxMode ETH_RXINTERRUPT_MODE; HAL_ETH_Init(heth); DMAConf.RxDescriptorNumber ETH_RX_DESC_CNT; DMAConf.TxDescriptorNumber ETH_TX_DESC_CNT; DMAConf.RxDescList DMARxDscrTab; DMAConf.TxDescList DMATxDscrTab; DMAConf.SetMacAddress 0; // 使用 MACAddr HAL_ETH_DMAConfig(heth, DMAConf); HAL_ETH_Start(heth); HAL_ETH_Start_IT(heth);HAL_ETH_Init里会调用复位函数把 MAC 和 DMA 恢复到默认状态。这个复位需要一定时间HAL 内部有等待超时一般没问题。但如果在HAL_ETH_Init之前没有复位 PHY可能导致 PHY 状态异常。我建议在 GPIO 初始化时把 PHY 的 RST 引脚拉低至少 10ms再拉高等 100ms再执行 HAL_ETH_Init。这里有个细节HAL_ETH_Start和HAL_ETH_Start_IT的区别。HAL_ETH_Start只启动 MAC 和 DMA不使能中断HAL_ETH_Start_IT会额外使能接收/发送/错误中断。我们用中断接收所以必须调用HAL_ETH_Start_IT。如果只调用了HAL_ETH_Start中断永远不会触发接收任务一直等信号量ping 不通。这个坑我在移植时踩过半天没找出问题。3.3 接入 FreeRTOS 任务与信号量FreeRTOS 部分需要新建两个东西接收任务 信号量。接收任务负责把 lwIP 包从以太网接口提取出来。信号量用于中断和任务之间的同步。在lwipopts.h里需要设置一堆和 RTOS 相关的宏。我用的核心配置如下#define NO_SYS 0 #define LWIP_TIMERS 1 #define LWIP_NETCONN 1 #define LWIP_SOCKET 1 #define TCPIP_THREAD_STACKSIZE 1024 #define TCPIP_THREAD_PRIO (configMAX_PRIORITIES - 2) #define DEFAULT_ACCEPTMBOX_SIZE 5 #define DEFAULT_RECVMBOX_SIZE 8 #define PBUF_POOL_SIZE 16 #define PBUF_POOL_BUFSIZE 1524 #define MEM_SIZE 65536 #define TCP_MSS 1460 #define TCP_WND (4 * TCP_MSS)这里要特别说明TCPIP_THREAD_PRIO不宜太高。如果 tcpip_thread 优先级高于所有应用任务它可能独占 CPU如果太低网络吞吐会下降。我放在configMAX_PRIORITIES - 2比空闲任务高很多应用任务一般在configMAX_PRIORITIES - 4以下保证网络优先但不至于饿死其他任务。接收任务创建RxSemaphoreHandle xSemaphoreCreateBinary(); xTaskCreate(ethernetif_input_task, eth_input, 512, netif, configMAX_PRIORITIES - 3, NULL);任务栈大小 512 words 在 F207 上够用实测ethernetif_input路径中最深的调用链是 pbuf 分配 lwIP 协议栈处理512 words 不会溢出。如果 lwIP 开了 NAT、IGMP 等复杂功能建议加到 768。最好用uxTaskGetStackHighWaterMark检查一下别拍脑袋。发送路径的互斥锁TxMutexHandle xSemaphoreCreateMutex();在low_level_output里取锁后再等待发送完成信号量。这个信号量是二值信号量但要注意二值信号量可能留下“已给过但没人取”的状态导致下一次发送不等 DMA。更严谨的做法是使用队列通知或者计数信号量。个人经验如果发送任务只有一个二值信号量通常够但为了安全我在每次发送前用xSemaphoreTake(TxDoneSemaphore, 0)清空信号量再去等待效果更稳定。3.4 编译链接常见错误修正移植完编译第一步大概率报错。我遇到的几个典型错误和解决方式第一个是stm32f2xx_hal_eth.c编译时找不到__DMB()等内建函数。这个和编译器相关MDK 用 AC5/AC6 都没问题GCC 需要加-mthumb和-mcpucortex-m3。如果裸机工程没启用__DMB可以在主头文件里包含stm32f2xx.h它会自动带上cmsis_gcc.h或对应编译器头文件。第二个是ETH_DMACltbCR之类的寄存器名称冲突。标准外设库头文件和 HAL 库头文件同时被 include 时寄存器结构体定义重名。解决方法是把标准外设库的 include 路径彻底移除或者只保留下面的stm32f2xx.h芯片头文件不要 include 老的stm32f2xx_eth.h。第三个是链接错误undefined symbol HAL_ETH_Init。原因通常是stm32f2xx_hal_conf.h没有启用HAL_ETH_MODULE_ENABLED或者stm32f2xx_hal_eth.c没被编译到工程里。检查这两个地方基本能解决。第四个是 lwIP 升级到 2.x 后的 API 兼容问题。lwIP 1.4.1 里err_t ethernetif_init(struct netif *netif)的返回值是err_t2.1.2 里也是但 netif 结构体的字段有变化比如netif-output不再是直接调用而是netif-linkoutput。老代码里的netif-input参数类型也从struct pbuf *变为struct pbuf *但ethernetif_input函数签名要改。我参考新版ethernetif.c重新写了low_level_init、low_level_output、low_level_input避免兼容性问题。4. 调试记录与问题排查4.1 一上电就 HardFault移植完第一次上电系统直接进 HardFault调试器定位到HAL_ETH_Init里的某个寄存器操作。第一反应是时钟没使能。F207 的 ETH 时钟挂在 AHB1 总线上需要使能RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_ETHMAC, ENABLE)HAL 里对应__HAL_RCC_ETH_CLK_ENABLE()。如果时钟使能了还 HardFault检查描述符内存访问权限。DMARxDscrTab和DMATxDscrTab如果在 RAM 里但地址超出了可访问范围比如放在了只读的 Flash 里DMA 写描述符时会触发总线错误。还有如果ETH_RX_BUF_SIZE定义过小DMA 收到大包时会越界写入相邻变量破坏栈也会 HardFault。建议将描述符和缓冲区数组放在__ALIGN_BEGIN修饰的全局变量区域不要放在局部数组里局部变量栈空间有限且对齐不可控。我还遇到一种情况在 MX 工具生成的SystemClock_Config中以太网 PLL 时钟配置不正确。F207 的 RMII 需要 50MHz 参考时钟如果 PLL 配置不对PHY 的 REF_CLK 频率不对DMA 虽然能启动但数据链路完全不通。最好是让 PHY 的时钟来自外部 50MHz 有源晶振F207 的 ETH 外设只要 MAC 时钟正常即可。4.2 ping 不通的检查清单ping 不通是驱动更新最常见的问题原因也五花八门。我整理了排查顺序照着做能省很多时间。检查 PHY 地址。用 MDIO 扫描确认HAL_ETH_ReadPHYRegister能读到正常的 PHY ID。读不到 0xFFFF 就说明地址错或者 MDIO 引脚配置错。检查 PHY 复位时序。拉低、拉高、等待。很多 PHY 上电后需要 50ms 才能完成内部上电初始化如果复位后立即访问寄存器可能失败。检查 MAC 地址是否有效。MAC 地址不能全是 0x00 或 0xFF否则网络包会被丢弃。用HAL_ETH_Init里传入的heth.Init.MACAddr确认。检查 RMII/MII 模式。如果固件和硬件模式不匹配时钟和信号线都会错。检查 lwIP 的 netif 初始化和链接状态。在调试器里看netif-flags是否包含NETIF_FLAG_LINK_UP、NETIF_FLAG_UP。如果 link up 没设置ping 请求不会进入协议栈。检查中断。调试器里看HAL_ETH_IRQHandler是否定期被调用。如果中断没触发问题通常在中断配置或HAL_ETH_Start_IT没调用。检查 ARP 表。PC 端 arp -d 清缓存再 ping 一次排除 PC 端 ARP 缓存问题。我这次遇到的是第 6 项NVIC 中断优先级配置不正确导致接收中断被其他高优先级中断一直抢占ETH 中断根本没机会执行。F207 的 ETH 中断优先级我设为 5数字越小优先级越高同时把configMAX_SYSCALL_INTERRUPT_PRIORITY设为 5确保中断里能调用xSemaphoreGiveFromISR。如果 ETH 中断优先级数值大于configMAX_SYSCALL_INTERRUPT_PRIORITY就不能在回调里调用任何 FreeRTOS API否则系统会断言失败。这个在《FreeRTOS 权威指南》里也特别强调过但实际工程里还是会有人踩。4.3 高中断频率导致任务卡死更新驱动后高流量测试发现跑几分钟后整个系统像死了一样所有任务都不执行只剩中断在闪烁。后来用调试器挂住发现接收任务一直收不到信号量或者收到了信号量但ethernetif_input内部卡在某个 lwIP 函数里。问题出在两处。第一中断优先级设置过高把 ETH 中断优先级设为 0最高它在中断里给出的信号量唤醒接收任务后接收任务因为调度器无法打断更高优先级的中断不FreeRTOS 中中断能打断任务但如果 ETH 中断占用时间太长比如在中断里直接处理网络包RTOS tick 会被无限推迟导致任务调度失效。而且如果HAL_ETH_IRQHandler里处理了所有接收包且不退出中断系统确实会卡死。正确做法中断只做一个“快速标记”尽量不做复杂处理。我后来把接收中断里对描述符的循环遍历全部移到接收任务里。中断回调只给信号量接收任务拿到信号量后再while(1)取所有包直到取空。这样中断处理时间只有几条指令不会阻塞 RTOS 调度。第二接收任务的优先级比 tcpip_thread 高而且ethernetif_input调用netif-input会直接向 tcpip_thread 的 mailbox 发送数据。如果 tcpip_thread 处理不过来mailbox 满了netif-input会阻塞等待。接收任务阻塞在等待 mailbox 空间上如果 tcpip_thread 又被其他任务饿死就会死锁。我的解决方法是限制接收任务每次最多处理 8 个包然后taskYIELD()给 tcpip_thread 调度的机会同时增大DEFAULT_RECVMBOX_SIZE到 8。4.4 常见问题速查表现象可能原因解决方法上电 HardFault描述符未 32 字节对齐使用__ALIGN_BEGIN...__ALIGN_END修饰描述符和缓冲区上电 HardFaultETH 时钟未使能调用__HAL_RCC_ETH_CLK_ENABLE()ping 间歇性失败RMII 时钟不稳换用外部有源 50MHz 晶振ping 不通且 PHY ID 读不到PHY 地址错误扫描 1~31 PHY 地址ping 不通但 PHY ID 正常lwIP 链路状态未 up轮询 PHY调netif_set_link_up高流量卡死中断里处理大量数据中断只给信号量处理逻辑放到任务里发送后对方收不到MAC 地址无效检查heth.Init.MACAddr指向的内容接收丢包严重接收描述符太少增大ETH_RX_DESC_CNT到 8系统断言异常ETH 中断优先级太高设置中断优先级数值 configMAX_SYSCALL_INTERRUPT_PRIORITY这张表是我这次调试过程中实际遇到的问题和解决办法分享给踩坑的朋友。5. 实测数据与性能表现5.1 吞吐量测试驱动更新完成后我用 iperf 做了吞吐测试。测试环境STM32F207 作为服务器PC 作为客户端通过百米六类网线直连使用 RMII 接口PHY 为 DP83848100Mbps 全双工。TCP 下载方向PC 发送板子接收实测稳定在 62Mbps 左右。这个数字不算顶尖但已经接近 F207 在 RMII 模式下的大致极限。CPU 占用率用 FreeRTOS run-time stats 统计接收任务和 tcpip_thread 的总占比大约 22%其余应用任务不受影响。TCP 上传方向板子发送PC 接收实测 58Mbps。上传略低于下载因为 DMA 发送路径需要等待发送完成信号量增加了少量调度开销。如果追求更高可以把发送描述符数量增加到 8并在low_level_output中避免每次发送都等待上一个包完成改成“发送后立即返回只有描述符耗尽时才等待”但这样会占用更多描述符代码复杂度更高。对当前项目来说58Mbps 足够。UDP 单向吞吐实测能到 80Mbps。UDP 没有 TCP 窗口和 ACK 的延迟影响所以吞吐更高。不过这个数字并不可喜因为 F207 的以太网 DMA 在每次接收中断后需要重新设置描述符的 OWN 位中断频率高时DMA 和 CPU 之间会有竞争。如果想进一步提升 UDP 吞吐可以考虑使用轮询模式 lwIP 的零拷贝但这部分工作量较大暂时没有做。5.2 CPU 占用与中断延迟用 FreeRTOS 的vTaskGetRunTimeStats统计了一分钟内各任务运行占比。空闲任务约 62%接收任务约 9%tcpip_thread 约 13%其他应用任务合计约 16%。这个分布比较健康网络栈没有吃掉过多 CPU应用还有余量。中断延迟方面ETH 中断优先级设为 5实测中断响应到回调执行大约 2~4us包含从中断向量到 HAL 库处理的时间。这个延迟可以接受。如果系统里还有更紧急的中断比如电机控制 PWM 中断需要把那些中断优先级设得比 ETH 高数值更小ETH 中断不能抢占它们。反之ETH 中断优先级如果过低接收包处理不及时会导致缓冲区溢出。个人建议 ETH 中断优先级在 F207 上不要低于 6数值否则高负载下丢包会变多。5.3 稳定性与长时间运行稳定性测试跑了 48 小时持续 TCP 数据流 每 10 秒一次 ping。最终结果TCP 吞吐稳定ping 丢包率 0%无死机无描述符泄漏。过程中观察了 lwIP 的stats统计接收链路没有出现drop或memerr异常增加。长时间运行最容易出问题的就是内存泄漏。我特意检查了heap_4的剩余内存48 小时后只比初始少了 32 字节属于正常波动没有持续下降。这里建议在 lwIP 中开启内存统计LWIP_STATS和LWIP_STATS_DISPLAY通过串口打印stats能及时发现 pbuf 泄漏和内存碎片问题。另一个容易被忽视的点是单片机里如果用了看门狗网络空闲时不能让看门狗误判。我们把链路轮询任务和空闲任务部分代码放到vApplicationTickHook里喂狗保证即使网络流量为零系统也能正常喂狗。否则长 ping 间隔期间如果协议栈没有调用任何回调看门狗可能超时复位。这次没踩但之前别的项目遇到过提醒一句。6. 最后分享几个实操小技巧这次驱动更新搞完回头总结有几个小技巧确实能省心不少也算是我个人经验的沉淀。第一个升级 lwIP 时别急着全量替换。先把驱动层ethernetif.c替换掉保留 lwIP 1.4.1 的用户层 API编译跑通后再升级到 2.x。这样至少能把“硬件驱动”和“协议栈 API”两个问题域分开排查。这次我是一步到位结果合在一起排查时分不清是 lwIP 初始化问题还是 DMA 问题浪费了不少时间。第二个保留一份旧驱动在版本库里。不要因为新驱动能编译过就直接删掉旧的。有新问题出现时可以随时 git 切换旧代码做对比测试。很多时候新驱动的问题只是一个小配置比如 PHY 地址不对但因为有旧版对照就能很快确认是不是新代码引入的。我们团队一直用 git这次驱动更新全程分支开发回滚成本几乎为零。第三个用抓包工具辅助调试。在 PC 端用 Wireshark 抓包能看到板子发出的 ARP 请求、TCP SYN、重传包。如果板子根本没有发出任何 ARP说明驱动数据链路层还没通如果只发了 ARP 但没收到回复说明 PHY 或者 DMA 收发路径有问题。这个方法比单看串口日志高效太多尤其是 ping 不通时能快速定位问题在哪一层。第四个硬件上给 PHY 复位引脚加个上拉。如果 PHY 复位引脚悬空上电时可能进入一个不确定状态导致 MDIO 读不到 ID或者自动协商失败。我们在新板子上给 RST 引脚加了一个 10k 上拉同时用 MCU 的 GPIO 控制软件驱动里再拉低/拉高复位问题就再没出现过。还有一点想特别提一下关于 FreeRTOS 和 HAL 库的中断优先级匹配。使用 HAL 库时如果开启了USE_RTOS1有些 HAL 版本支持HAL 内部会调HAL_NVIC_SetPriority但 ETH 中断的优先级还是需要我们自己设置。建议在配置 ETH 中断时使用HAL_NVIC_SetPriority(ETH_IRQn, 5, 0); HAL_NVIC_EnableIRQ(ETH_IRQn);同时保证configMAX_SYSCALL_INTERRUPT_PRIORITY的数值不小于 5。这里说的“数值不小于”可能有点绕实际上 FreeRTOS 优先级数值越大优先级别越低configMAX_SYSCALL_INTERRUPT_PRIORITY是个阈值只有优先级数值大于等于这个阈值的“低优先级中断”才能调用 FreeRTOS API。所以 ETH 中断优先级 5 和configMAX_SYSCALL_INTERRUPT_PRIORITY设为 5 是匹配的。一定要避免把 ETH 中断设为 0那样中断里直接调用 FreeRTOS API 会引发严重异常。这是 FreeRTOS HAL 驱动最常见的崩溃源踩过一次之后我就格外注意。这次驱动更新严格来说不算惊天动地的大工程但把标准外设库换成 HAL 库、把 lwIP 升到 2.x、把网络栈接入 FreeRTOS整个过程涉及底层的 DMA、中断、RTOS 同步、协议栈配置非常考验细节。希望这篇经验分享能帮你少走几个坑。如果你也在 F2x7 系列上做类似升级欢迎对照我说的步骤试一遍有什么问题再交流。