
今年年初老客戶那邊反饋設備在項目現場偶發網絡中斷而且部分設備重啟之后能恢復另一部分怎么重啟都連不上。這批設備的主控是 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 配合之后再遇到網絡疑難雜癥至少知道往哪個方向查。希望這篇經驗能幫你少走幾步彎路。