全鏈路與實戰(zhàn)排查)
手里有 ESP32 的兄弟十個有九個是先跑 WiFi 的??烧娴搅艘?7×24 小時不間斷上報數(shù)據(jù)的場合WiFi 那點脾氣就全露出來了路由器重啟一次、信道被鄰居家 AP 擠一下、現(xiàn)場金屬柜門一關連接就斷然后在深更半夜你被電話叫起來重啟設備。這時候很多人會想到加一塊 W5500讓 ESP32 走有線以太網(wǎng)。想法很好但一上手就卡在 SPI 上——四根線怎么接、模式怎么設、為什么讀出來的版本號是 0xFF、為什么網(wǎng)口燈亮著但 DHCP 拿不到地址。這篇就把 ESP32 驅動 W5500 的完整鏈路掰開揉碎講一遍從 SPI 時序到寄存器幀格式再到能直接抄的例程和一份排查表。1. 為什么給 ESP32 配 W5500而不是繼續(xù)用 WiFi1.1 無線在工業(yè)現(xiàn)場的三個真實痛點先說結論不是 WiFi 不好是它的工作方式?jīng)Q定了它不適合做命脈通道。ESP32 的 WiFi 是射頻通信受環(huán)境影響的變量太多——2.4G 頻段擁擠、多徑反射、金屬屏蔽、AP 負載。做過現(xiàn)場設備的人應該都經(jīng)歷過設備在辦公室測試一周沒事裝到配電房旁邊的鐵皮柜里一天斷八次。第二個痛點是資源爭搶。ESP32 只有一個射頻前端WiFi 和藍牙共存時要分時復用協(xié)議棧內部要做仲裁。如果你的項目里同時開了 BLE 配網(wǎng)和 WiFi 上傳再加上 TLS 握手任務堆棧和堆內存會非常緊張表現(xiàn)就是偶發(fā)的連接超時、內存不足重啟。這類問題極難復現(xiàn)也極難定位。第三個痛點是重新入網(wǎng)這件事本身。設備換了個現(xiàn)場、換了路由器或者 AP 的 SSID 改了你就得重新配網(wǎng)。ESP32 上做配網(wǎng)要么開熱點讓用戶連、要么走藍牙、要么走 SmartConfig每一種都增加了現(xiàn)場維護的復雜度。有線就簡單多了插上網(wǎng)線DHCP 拿地址或者干脆靜態(tài) IP插上就跑。1.2 W5500 與 LAN8720 兩條路線的取舍ESP32 做有線以太網(wǎng)主流就兩條路。一條是LAN8720 這類 RMII PHY。它走的是 ESP32 內置的 EMAC 外設用 RMII 接口理論上性能更好直接走 MAC 層吞吐能跑滿 100Mbps 的大部分帶寬。但它的問題也很實在占引腳多TXD0/TXD1、RXD0/RXD1、TX_EN、CRS_DV、REF_CLK 等等七八根起步還需要一個 50MHz 參考時鐘——要么 PHY 自己出、要么 ESP32 出、要么外掛晶振時鐘方向和配置錯一個就跑不起來。論壇上大量LAN8720 用外部 50M 的設計討論絕大多數(shù)都是在踩時鐘這個坑。另一條就是W5500。它是 WIZnet 的硬件協(xié)議棧芯片內置 TCP/IP 硬件邏輯、MAC 和 PHY主機只需要通過 SPI 把它當成一堆寄存器去讀寫就行。占用引腳只有 4 根 SPI 線加一根復位線架構上極其干凈。代價是吞吐受 SPI 帶寬限制而且你失去了 ESP-IDF 里那套成熟的esp_eth網(wǎng)絡棧直通能力——不過這一點在 W5500 上也有官方驅動可以對接后面會講。1.3 SPI 方案對引腳和內存的占用盤點引腳賬很好算。W5500 方案SCLK、MISO、MOSI、CS 四根加上復位 RST、可選的中斷 INT一共 5 到 6 根。LAN8720 方案RMII 數(shù)據(jù)線加控制線通常要 9 根以上還要加上 MDC/MDIO碰上模組還占 PSRAM 引腳的項目基本就分配不開了。內存賬更關鍵。W5500 內部自帶16KB 發(fā)送緩沖 16KB 接收緩沖默認分給 8 個 socket 各 2KB。這意味著 TCP 的重傳、窗口管理、序號維護這些活全在 W5500 芯片里干了ESP32 端的 RAM 消耗極小。而 LAN8720 走的是 ESP32 自己的 LwIP 協(xié)議棧每個連接都要占 PCB 控制塊、收發(fā)緩沖區(qū)8 個并發(fā)連接下來配合 WiFi 和 BLE 一起開內存壓力是肉眼可見的。所以我的選型經(jīng)驗是這樣引腳緊張、內存緊張、連接數(shù)不多幾個到十幾個、吞吐要求幾十 Mbps 以內W5500 是更省心的方案要大帶寬比如視頻流、要低延遲、引腳富裕才考慮 RMII 那套。2. SPI 協(xié)議先啃透四根線是怎么把 32 位地址搬進芯片的2.1 從 CS/SCLK/MOSI/MISO 的角色分工說起很多人用 SPI 用了好幾年但還是說不清這四個信號誰是主誰是從。用一句話概括時鐘永遠由主機給從機跟著時鐘走。SCLK串行時鐘。每個時鐘周期傳輸一位上升沿還是下降沿采樣由 SPI 模式?jīng)Q定。ESP32 作為主機這個腳的時鐘是它產(chǎn)生的。MOSIMaster Out Slave In主機發(fā)、從機收。在 ESP32 驅動 W5500 的場景里命令、地址、要寫入的數(shù)據(jù)都從這里出去。MISOMaster In Slave Out從機發(fā)、主機收。讀 W5500 的寄存器、讀接收緩沖區(qū)的網(wǎng)絡數(shù)據(jù)都從這里回來。CS片選。低電平有效拉低表示我現(xiàn)在要跟你說話??偩€上如果還掛著別的 SPI 設備比如 SPI Flash、SPI 屏就靠這根線來區(qū)分跟誰說話。理解的關鍵點在于SPI 是全雙工的MOSI 和 MISO 在同一個時鐘周期里同時各傳一位。所以你在 Arduino 里看到的SPI.transfer(0xFF)這種寫法意思不是發(fā)一個 0xFF而是在接下來 8 個時鐘周期里MOSI 輸出 0xFF同時把 MISO 上收到的 8 位讀回來。讀寄存器時之所以要發(fā)一個啞字節(jié)dummy byte就是因為時鐘得轉起來從機才能往外吐數(shù)據(jù)。2.2 模式 0 的時序細節(jié)與時鐘極性的坑SPI 有四種模式由 CPOLClock Polarity空閑電平和 CPHAClock Phase采樣邊沿組合而成。W5500 只支持 SPI 模式 0也就是 CPOL0、CPHA0時鐘空閑時為低電平數(shù)據(jù)在上升沿被采樣。為什么這個要說清楚因為如果你用錯了模式現(xiàn)象會非常迷惑——有時候讀出來是全 0有時候是全 0xFF有時候數(shù)值看著像但又不對而且換個批次芯片現(xiàn)象還不一樣。我第一次用軟件模擬 SPI 的時候就在這上面耗了一下午后來拿示波器打了一下發(fā)現(xiàn)時鐘空閑是高采樣沿也在下降沿整個反了。還有一個容易忽略的細節(jié)是位序。W5500 是MSB First先傳最高位。絕大多數(shù) SPI 設備都是 MSB First但配置里忘了設讀出來的字節(jié)就是完全反的比如期望 0x04 讀出 0x20這種情況一看就是位序錯了。傳輸時序上正確的順序是CS 拉低 → 發(fā)地址高字節(jié) → 發(fā)地址低字節(jié) → 發(fā)控制字節(jié) → 收發(fā)數(shù)據(jù) → CS 拉高。整套動作必須在一次 CS 有效期內完成中途 CS 抖動會導致 W5500 狀態(tài)機復位讀出來就是垃圾數(shù)據(jù)。2.3 W5500 三字節(jié)幀頭地址 控制字節(jié)逐位拆解這是整篇文章最關鍵的地方也是大部分例程能跑但沒人講清楚的部分。W5500 的 SPI 幀結構是定長的 3 字節(jié)頭部 數(shù)據(jù)第一個字節(jié)是地址高 8 位第二個字節(jié)是地址低 8 位第三個字節(jié)是控制字節(jié)。控制字節(jié)的 8 位是這樣分配的位域名稱含義bit 7 ~ bit 3BSB[4:0]塊選擇5 位共 32 個塊bit 2RWB0 讀1 寫bit 1 ~ bit 0OM工作模式00 表示可變長度模式BSB 是最需要理解的。W5500 把內部空間劃成了 32 個塊常見的有0x00通用寄存器塊MR、GAR、SHAR、SIPR、VERSIONR 這些0x01 3n第 n 個 socket 的寄存器塊0x02 3n第 n 個 socket 的發(fā)送緩沖區(qū)0x03 3n第 n 個 socket 的接收緩沖區(qū)所以 socket 0 的三個塊分別是 0x01、0x02、0x03socket 1 是 0x04、0x05、0x06以此類推。為什么會這樣設計因為 W5500 內部是 8 個 socket × 3 類空間的規(guī)則矩陣用 5 位塊號就能線性尋址硬件實現(xiàn)簡單軟件算起來也方便。OM 位是很多人沒注意的地方。默認的00是可變長度模式意思是你發(fā)多少字節(jié)我就當多少個數(shù)據(jù)適合收發(fā)網(wǎng)絡報文這種長度不定的場景。另外三種是定長模式1/2/4 字節(jié)在讀單個寄存器時用定長模式理論效率更高但你得自己區(qū)分場景反而容易出錯。我自己的做法是統(tǒng)一用可變長度模式代碼簡單、不容易翻車。2.4 硬件片選和軟件片選差在哪這是搜得最多的 SPI 問題之一。所謂硬件片選是 SPI 外設自己在傳輸開始時把 CS 拉低、結束時拉高軟件片選則是你自己用 GPIO 手動控制。在 ESP32 上Arduino 的 SPI 庫用的是硬件外設但片選實際上還是走 GPIO 手動控制的SPI.begin(sck, miso, mosi, ss)里那個 ss 只是記了個默認值真正的 CS 時序由庫或你的代碼來管。ESP-IDF 的spi_master里則是明確區(qū)分spi_device_interface_config_t的spi_device_interface_config_t::spics_io_num設了就是硬件片選設成 -1 就是不用硬件片選你自己控制。那什么時候需要軟件片選兩種情況。一是多個從機共享總線需要你自己決定誰被選中二是CS 與其它信號有嚴格時序配合比如你需要在 CS 拉低之后再延遲一點才開始傳輸。對 W5500 來說核心要求只有一條一次完整的寄存器訪問必須在一個 CS 低電平窗口內完成。用 Arduino 庫時庫內部處理了這件事自己寫驅動的時候注意別在傳輸中間去動 CS 就行。3. 接線與參考電路上電前必須核對的清單3.1 引腳分配表與 ESP32 腳位禁區(qū)先給一張我常用的接線表基于 ESP32 的 VSPI默認 SPI 外設ESP32 引腳信號說明GPIO 18SCLKSPI 時鐘輸出GPIO 19MISO從 W5500 讀數(shù)據(jù)GPIO 23MOSI向 W5500 寫數(shù)據(jù)GPIO 5CS片選低有效GPIO 4RST硬件復位低有效GPIO 27INT可選W5500 中斷輸出3V3VCC供電GNDGND共地這里有幾個不能碰的引腳新手特別容易踩GPIO 6 ~ 11接內部 SPI Flash占用了就下載不了程序甚至開不了機。GPIO 34 ~ 39只有輸入功能沒有輸出驅動能力絕對不能拿來做 SCLK、MOSI、CS。GPIO 16 / 17很多模組默認給了內部 PSRAM用了會沖突。GPIO 12啟動時的電平會影響 Flash 供電電壓MTDI strapping如果外部上拉導致啟動時是高電平可能直接啟動失敗。所以 HSPI 那組默認引腳里的 MISO12 要特別當心最好換一組引腳映射。GPIO 0 / 2 / 15也都是 strapping 腳用之前先確認上電瞬間的電平是否符合要求。我個人的習慣是做 W5500 項目時把 SPI 映射到 18/19/23 這組默認腳CS 用 5RST 用 4把 0/2/12/15 全部空出來不用。這樣最不容易出玄學問題。3.2 供電、復位、晶振與 12.4k 偏置電阻供電這塊W5500 是 3.3V 單電源但電流不小——空閑狀態(tài)大概幾十毫安收發(fā)數(shù)據(jù)時峰值能到 130mA 以上。這意味著如果你用的是體積很小的 ESP32 模組板載 LDO 的余量可能不夠尤其是同時還要給 WiFi 供電的時候。我的建議是如果 ESP32 板上是 AMS1117 這類能出 800mA 的 LDO直接取 3.3V 通常沒問題如果板子是精簡版、LDO 只有 300mA 級別一定給 W5500 單獨供一路 LDO。同時在 W5500 的 VDD 附近就近放 100nF 和 10uF 的退耦電容這一點在 PCB 設計上別省。復位電路W5500 的 RSTn 是低電平有效數(shù)據(jù)手冊要求低電平保持時間不低于 500 微秒。實際寫代碼時統(tǒng)一給 10ms 更保險。復位拉高之后不要立刻訪問寄存器內部 PLL 需要鎖定時間建議至少等 1ms 以上我一般給 50 到 100ms。晶振W5500 需要外部 25MHz 晶振如果買的是集成晶振的模塊就不用管如果是自己畫板晶振的負載電容要按晶振規(guī)格書配通常 12 到 22pF走線盡量短離芯片近。還有一顆絕對不能少的電阻RSET_BG 引腳到地的 12.4kΩ精度要求 1%。這是 PHY 的偏置電流設定電阻決定發(fā)送驅動電流的大小。我見過一個板子為了省事用了個 10kΩ 的結果就是網(wǎng)口能 link 上但丟包率極高換個網(wǎng)線就好一點、換個交換機就完全不通查了三天才發(fā)現(xiàn)是這顆電阻。這種問題最難查因為現(xiàn)象是半死不活而不是完全不工作。3.3 網(wǎng)口變壓器與差分走線W5500 內部集成了 PHY但沒有集成變壓器。也就是說從芯片出來的 TXP/TXN、RXP/RXN 是差分信號必須經(jīng)過 1:1 的隔離變壓器再到 RJ45。最省事的做法是用帶變壓器的 RJ45 座比如常見的 HR911105A 這類一顆器件解決問題。自己搭分立變壓器的話要注意變壓器中心抽頭的接法各家參考設計略有差異務必以 W5500 官方硬件設計指南和模塊廠家的原理圖為準。差分布線有幾個原則TX 對和 RX 對各自等長對內走線長度差控制在很小范圍內差分阻抗按 100Ω 控制盡量遠離晶振、SPI 時鐘這些干擾源兩條線之間不要走其它信號。這些東西在低速下可能體現(xiàn)不出來但走線亂搞的板子在長網(wǎng)線、惡劣電磁環(huán)境下的誤碼率會明顯上升。再多說一句買模塊是最劃算的選擇。W5500 的模擬部分對布線和器件精度有要求自己畫板省下的那點錢很可能換來幾周的調試時間。市面上成熟的 W5500 模塊已經(jīng)把晶振、變壓器、偏置電阻都做全了接線只剩下電源和 SPI性價比極高。4. 例程逐行拆解從寄存器讀寫到 TCP 回環(huán)4.1 復位與 SPI 初始化先看初始化的部分。以下基于 Arduino 環(huán)境#include SPI.h #define PIN_SCLK 18 #define PIN_MISO 19 #define PIN_MOSI 23 #define PIN_CS 5 #define PIN_RST 4 static void w5500HardwareReset() { pinMode(PIN_RST, OUTPUT); digitalWrite(PIN_RST, LOW); // 拉低復位低有效 delay(10); // 手冊要求 500us這里給足余量 digitalWrite(PIN_RST, HIGH); // 釋放復位 delay(100); // 等內部 PLL 鎖定別急著訪問寄存器 } static void w5500SpiInit() { SPI.begin(PIN_SCLK, PIN_MISO, PIN_MOSI, PIN_CS); pinMode(PIN_CS, OUTPUT); digitalWrite(PIN_CS, HIGH); // 空閑時必須拉高否則總線誤觸發(fā) // 模式 0MSB First時鐘 20MHz SPI.beginTransaction(SPISettings(20000000, MSBFIRST, SPI_MODE0)); }逐行說幾個點。pinMode(PIN_CS, OUTPUT)和digitalWrite(PIN_CS, HIGH)這兩行順序很重要——如果先 begin 后設方向中間會有一小段時間 CS 處于高阻態(tài)可能被誤判成低電平導致 W5500 收到一段垃圾時序。這就是很多人上電第一次通信必失敗、重試就好的原因。SPI.beginTransaction里的 20MHz 是我實測比較穩(wěn)的值。W5500 手冊標稱支持到 80MHz但能不能跑到這個頻率取決于你的走線質量。杜邦線飛線的情況下20MHz 是比較務實的起點調通了再往上試。4.2 通用讀寫函數(shù)怎么封裝有了前面講的幀格式讀寫函數(shù)就非常直白// 寫bsb 是塊號len 是字節(jié)數(shù) static void w5500Write(uint16_t addr, uint8_t bsb, const uint8_t *buf, uint16_t len) { digitalWrite(PIN_CS, LOW); SPI.transfer((uint8_t)(addr 8)); // 地址高字節(jié) SPI.transfer((uint8_t)(addr 0xFF)); // 地址低字節(jié) SPI.transfer((uint8_t)((bsb 3) | 0x04)); // BSB RWB1(寫) OM00 for (uint16_t i 0; i len; i) { SPI.transfer(buf[i]); } digitalWrite(PIN_CS, HIGH); } // 讀 static void w5500Read(uint16_t addr, uint8_t bsb, uint8_t *buf, uint16_t len) { digitalWrite(PIN_CS, LOW); SPI.transfer((uint8_t)(addr 8)); SPI.transfer((uint8_t)(addr 0xFF)); SPI.transfer((uint8_t)((bsb 3) | 0x00)); // RWB0(讀) for (uint16_t i 0; i len; i) { buf[i] SPI.transfer(0x00); // 啞字節(jié)換數(shù)據(jù) } digitalWrite(PIN_CS, HIGH); }(bsb 3)把 5 位塊號挪到控制字節(jié)的高 5 位| 0x04把 RWB 位置 1 表示寫。讀的時候 RWB 是 0所以只留(bsb 3)。這里有個細節(jié)值得注意SPI.transfer(0x00)里那個 0x00 本身沒有任何意義MOSI 上發(fā)什么從機都不關心它只是在產(chǎn)生時鐘。如果你不小心寫成SPI.transfer(buf[i])程序也能跑只是讀回來的數(shù)據(jù)被覆蓋了——這種 bug 特別隱蔽。再封裝一層單字節(jié)讀寫會更好用static void w5500WriteByte(uint16_t addr, uint8_t bsb, uint8_t val) { w5500Write(addr, bsb, val, 1); } static uint8_t w5500ReadByte(uint16_t addr, uint8_t bsb) { uint8_t v 0; w5500Read(addr, bsb, v, 1); return v; }4.3 讀版本號驗證鏈路在寫任何網(wǎng)絡邏輯之前一定要先讀一次版本寄存器這是判斷 SPI 鏈路是否通的最快手段void setup() { Serial.begin(115200); w5500HardwareReset(); w5500SpiInit(); uint8_t ver w5500ReadByte(0x0039, 0x00); // VERSIONR Serial.printf(W5500 VERSIONR 0x%02X\r\n, ver); if (ver ! 0x04) { Serial.println(SPI 鏈路異常先別往下走); while (1) delay(1000); } }0x0039是通用寄存器塊里的版本寄存器地址W5500 固定返回0x04。這個值不對后面所有代碼都是白搭。讀出來是什么基本對應什么問題讀到的值可能原因0x00MISO 沒接好或從機沒被選中CS 沒拉低或 SPI 模式錯0xFFMISO 被上拉、從機沒響應、模塊沒供電、復位沒釋放0x04 但偶發(fā)亂碼時鐘太高、走線太長、CS 時序有毛刺0x20 / 0x08 之類位序搞反了MSB/LSB 設錯這張表我建議截圖存手機里現(xiàn)場調試時比翻手冊快得多。4.4 Socket 開 TCP 服務端8 條寄存器指令走完流程鏈路通了之后就可以走 W5500 的 socket 流程了。這里以 socket 0 開一個 TCP 服務端為例塊號是0x01各寄存器地址按 socket 內偏移計算。#define S0_BASE 0x0000 // socket0 寄存器塊內基址 #define S0_MR 0x0000 // 模式寄存器 #define S0_CR 0x0001 // 命令寄存器 #define S0_SR 0x0003 // 狀態(tài)寄存器 #define S0_PORT 0x0004 // 本地端口(2字節(jié)) #define S0_RXBUF_SIZE 0x001E #define S0_TXBUF_SIZE 0x001F #define S0_TX_FSR 0x0020 // 空閑發(fā)送緩沖大小 #define S0_TX_WR 0x0024 // 發(fā)送寫指針 #define S0_RX_RSR 0x0026 // 已接收字節(jié)數(shù) #define S0_RX_RD 0x0028 // 接收讀指針 #define BSB_S0_REG 0x01 // socket0 寄存器塊 #define BSB_S0_TX 0x02 // socket0 發(fā)送緩沖 #define BSB_S0_RX 0x03 // socket0 接收緩沖 #define CMD_OPEN 0x01 #define CMD_LISTEN 0x02 #define CMD_CLOSE 0x10 #define CMD_SEND 0x20 #define CMD_RECV 0x40 #define SR_CLOSED 0x00 #define SR_INIT 0x13 #define SR_LISTEN 0x14 #define SR_ESTABLISHED 0x17 #define SR_CLOSE_WAIT 0x1C打開服務端總共就兩步void socket0Listen(uint16_t port) { w5500WriteByte(S0_MR, BSB_S0_REG, 0x01); // 0x01 TCP 模式 uint8_t p[2] { (uint8_t)(port 8), (uint8_t)(port 0xFF) }; w5500Write(S0_PORT, BSB_S0_REG, p, 2); // 寫入監(jiān)聽端口 w5500WriteByte(S0_CR, BSB_S0_REG, CMD_OPEN); // 命令OPEN waitSr(SR_INIT); // 等狀態(tài)變 SOCK_INIT w5500WriteByte(S0_CR, BSB_S0_REG, CMD_LISTEN); // 命令LISTEN waitSr(SR_LISTEN); // 等狀態(tài)變 SOCK_LISTEN }Sn_MR寫0x01表示 TCP寫0x02是 UDP這個值的低 4 位就是協(xié)議類型。端口是高字節(jié)在前這是網(wǎng)絡字節(jié)序寫反了就會監(jiān)聽到一個莫名其妙的端口上。Sn_CR是發(fā)令寄存器寫進去一個命令值W5500 硬件就開始執(zhí)行。執(zhí)行完成后命令寄存器會被硬件自動清零所以你不需要手動清。接收數(shù)據(jù)的流程是這樣的bool socket0Recv(uint8_t *buf, uint16_t *len) { uint8_t sr w5500ReadByte(S0_SR, BSB_S0_REG); if (sr ! SR_ESTABLISHED sr ! SR_CLOSE_WAIT) return false; uint8_t rsr[2]; w5500Read(S0_RX_RSR, BSB_S0_REG, rsr, 2); uint16_t rxLen ((uint16_t)rsr[0] 8) | rsr[1]; if (rxLen 0) return false; uint8_t rd[2]; w5500Read(S0_RX_RD, BSB_S0_REG, rd, 2); uint16_t rdPtr ((uint16_t)rd[0] 8) | rd[1]; uint16_t readLen rxLen *len ? *len : rxLen; // 注意接收緩沖的讀地址需要按 16 位偏移去訪問 w5500Read(rdPtr, BSB_S0_RX, buf, readLen); *len readLen; // 更新讀指針并下發(fā) RECV 命令通知芯片這段數(shù)據(jù)已取走 rdPtr readLen; uint8_t newRd[2] { (uint8_t)(rdPtr 8), (uint8_t)(rdPtr 0xFF) }; w5500Write(S0_RX_RD, BSB_S0_REG, newRd, 2); w5500WriteByte(S0_CR, BSB_S0_REG, CMD_RECV); return true; }這段邏輯初學者最容易漏的是最后必須下發(fā) RECV 命令。W5500 的接收緩沖是個環(huán)形區(qū)你不告訴它我已經(jīng)取走了讀指針不推進下一次讀還是同一份數(shù)據(jù)表現(xiàn)就是收到一條消息后瘋狂重復處理同一條。我第一次遇到這個現(xiàn)象時以為是上層重發(fā)查了好久。發(fā)送側相對簡單bool socket0Send(const uint8_t *buf, uint16_t len) { uint8_t fsr[2]; w5500Read(S0_TX_FSR, BSB_S0_REG, fsr, 2); uint16_t freeSize ((uint16_t)fsr[0] 8) | fsr[1]; if (freeSize len) return false; // 緩沖不夠等下一輪 uint8_t wr[2]; w5500Read(S0_TX_WR, BSB_S0_REG, wr, 2); uint16_t wrPtr ((uint16_t)wr[0] 8) | wr[1]; w5500Write(wrPtr, BSB_S0_TX, buf, len); // 數(shù)據(jù)寫進發(fā)送緩沖 wrPtr len; uint8_t newWr[2] { (uint8_t)(wrPtr 8), (uint8_t)(wrPtr 0xFF) }; w5500Write(S0_TX_WR, BSB_S0_REG, newWr, 2); // 更新寫指針 w5500WriteByte(S0_CR, BSB_S0_REG, CMD_SEND); // 命令SEND return true; }關鍵順序是先寫數(shù)據(jù)、再更新寫指針、最后發(fā) SEND 命令。順序錯了就會出現(xiàn)發(fā)送成功但對方收到的是上一條數(shù)據(jù)這種詭異現(xiàn)象。4.5 用現(xiàn)成庫走快車道Arduino 與 ESP-IDF 兩種寫法如果你不想碰寄存器兩條快車道Arduino 環(huán)境用 WIZnet 維護的 Ethernet 庫支持 W5100/W5200/W5500 的那個版本核心就兩行#include SPI.h #include Ethernet.h void setup() { SPI.begin(18, 19, 23, 5); Ethernet.init(5); // 指定 W5500 的片選引腳 byte mac[] {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED}; if (Ethernet.begin(mac) 0) { // DHCP 拿地址 Ethernet.begin(mac, IPAddress(192,168,1,100)); // 失敗就靜態(tài) IP } Serial.println(Ethernet.localIP()); }要提醒的是ESP32 Arduino core 自帶的ETH庫主要針對 RMII 類 PHY不是給 W5500 用的別拿錯了庫。另外Ethernet.begin()里默認會跑一遍鏈路檢測網(wǎng)線沒插會卡住一段時間加個超時邏輯更穩(wěn)妥。ESP-IDF 環(huán)境官方有 W5500 的 MAC 和 PHY 驅動思路是先建 SPI 總線、再建 MAC 和 PHY、然后裝驅動、最后掛到 netif 上spi_bus_config_t buscfg { .miso_io_num 19, .mosi_io_num 23, .sclk_io_num 18, .quadwp_io_num -1, .quadhd_io_num -1, }; spi_bus_initialize(SPI2_HOST, buscfg, SPI_DMA_CH_AUTO); spi_device_interface_config_t devcfg { .clock_speed_hz 20 * 1000 * 1000, .mode 0, .spics_io_num 5, .queue_size 20, }; // 后續(xù)依次調用 esp_eth_mac_new_w5500 / esp_eth_phy_new_w5500 // 再 esp_eth_driver_install 并綁定 esp_netifIDF 這條路的優(yōu)勢是能直接復用 LwIP、事件循環(huán)、mDNS、HTTP 服務這些上層組件做產(chǎn)品的話省很多事。順帶回應一個常見疑問SPI 需要兩個 DMA 嗎這個問法多半來自 STM32 那邊全雙工收發(fā)各占一個 DMA 流。ESP32 這邊 SPI 主機有 DMA但 W5500 這種寄存器級別的短傳輸根本用不上只有在搬運大塊收發(fā)緩沖時 DMA 才有意義。別被這個概念繞進去。5. 常見問題排查速查表與踩坑記錄5.1 現(xiàn)象-定位-處理 對照表現(xiàn)象優(yōu)先懷疑處理方式版本號讀 0x00SPI 未通信查 MISO 接線、確認 CS 拉低、確認模式 0版本號讀 0xFF從機無響應查供電、查復位是否釋放、查 CS 是否被其它設備占用版本號正確但 DHCP 超時網(wǎng)絡層問題查網(wǎng)線、查變壓器、查 12.4k 偏置電阻、查 MAC 地址是否重復網(wǎng)口燈不亮PHY 未工作查 25MHz 晶振、查供電電流是否足夠、查復位時序數(shù)據(jù)能收但重復處理忘了 RECV 命令收完數(shù)據(jù)后更新 RX_RD 并下發(fā) RECV發(fā)送成功但內容錯位寫指針順序錯必須寫數(shù)據(jù) → 更新 TX_WR → 發(fā) SEND傳幾百字節(jié)后卡死發(fā)送緩沖滿每次發(fā)之前讀 TX_FSR不夠就等下次偶發(fā)亂碼、不定時重啟SPI 時鐘太高或電源不穩(wěn)降到 10MHz 試檢查退耦電容和電源余量5.2 幾個不太容易想到的坑第一個坑是網(wǎng)線和交換機。有次調試設備接辦公室交換機一切正常接到現(xiàn)場某臺老交換機就瘋狂丟包。最后發(fā)現(xiàn)是那臺交換機只有 10M 半雙工能力自動協(xié)商沒談攏。W5500 支持自協(xié)商但老設備有時候協(xié)商不干凈??梢酝ㄟ^通用寄存器的 PHY 配置寄存器強制雙工和速率雖然不常見但遇到時能救命。第二個坑是多個 CS 共用時的總線爭搶。如果你的板子上同時掛了 W5500 和 SPI Flash 或 SPI 屏一定要確保每個設備的 CS 在非訪問期間是拉高的。曾經(jīng)有個項目的 SPI 屏 CS 懸空導致它偶爾偷聽發(fā)給 W5500 的命令現(xiàn)象是隨機死機查了一周。第三個坑是復位與上電順序。如果 W5500 和 ESP32 共用同一個 3.3V上電瞬間 W5500 可能比 ESP32 先就緒也可能后就緒。穩(wěn)妥的做法是 ESP32 啟動后主動拉一次 RST 引腳別指望上電復位電路。第四個坑是MAC 地址復用。同一個局域網(wǎng)里兩臺設備的 MAC 地址一樣癥狀是兩臺設備輪流失聯(lián)看起來像網(wǎng)絡不穩(wěn)其實是 ARP 表在打架。批量生產(chǎn)的項目一定要給每臺設備分配唯一 MAC可以從芯片 ID 派生。第五個坑是打印調試拖慢了流程。用串口打印每個寄存器讀寫耗時很長尤其是 115200 波特率下。如果你在 socket 輪詢循環(huán)里頻繁打印會錯過接收窗口表現(xiàn)就是高流量下丟包。調試時用 LED 或計數(shù)器指示狀態(tài)比打印靠譜。6. 進階優(yōu)化多 socket、吞吐與長連接?;?.1 8 個 socket 的分配策略W5500 有 8 個獨立 socket每個都能單獨配成 TCP 或 UDP。分配上有幾個經(jīng)驗原則。別把 8 個 socket 全開成 TCP 服務端。每開一個 socket就占掉一份緩沖和連接狀態(tài)。合理的做法是按功能分組比如socket 0 做配置通道HTTP 或簡單 TCPsocket 1 做數(shù)據(jù)上報MQTT 或自定義協(xié)議socket 2 做調試通道剩下的留給并發(fā)客戶端。緩沖大小可以動態(tài)調。Sn_TXBUF_SIZE和Sn_RXBUF_SIZE默認都是 2KB8 個 socket 一共用滿 16KB 16KB。如果是 UDP 廣播這種小包場景把單個 socket 的緩沖調到 1KB 甚至 0.5KB就能騰出空間給關鍵 socket 用 8KB。但要注意所有 socket 的緩沖之和不能超過芯片總量超了后面的配置會靜默失敗這個坑很隱蔽配置寄存器寫下去讀回來會發(fā)現(xiàn)根本沒生效。關閉不用的 socket 要發(fā) CLOSE 命令。直接不理會它是不行的狀態(tài)機會一直停在某個中間態(tài)占著資源。6.2 吞吐實測與 SPI 時鐘的關系理論上 W5500 的 SPI 能跑到 80MHz但實際吞吐瓶頸往往不在 SPI 時鐘而在你的輪詢循環(huán)效率。粗略算一下SPI 20MHz 意味著每秒 2000 萬個時鐘周期一個字節(jié) 8 位理論極限大約 2.5MB/s。但每次讀接收緩沖都要經(jīng)歷讀 RSR → 讀 RX_RD → 讀數(shù)據(jù) → 寫 RX_RD → 發(fā) RECV這一整套頭部的 SPI 事務開銷占了不少。實際測下來20MHz 配置、單純做 TCP 回環(huán)穩(wěn)定吞吐大概在幾百 KB/s 到 1MB/s 之間。想提高吞吐有幾個方向提高 SPI 時鐘但要看走線質量杜邦線飛線基本到 20-30MHz 就是極限PCB 短走線可以試 40MHz 以上。增大單次讀取長度一次把 RSR 里所有數(shù)據(jù)都讀走而不是一次讀幾十字節(jié)。減少輪詢里的無用操作別每次都去讀一遍全部寄存器可以先讀Sn_IR中斷寄存器判斷有沒有事件。用 INT 引腳讓 W5500 在收到數(shù)據(jù)時主動拉低中斷ESP32 用 GPIO 中斷喚醒處理比死循環(huán)輪詢省 CPU 也更快響應。6.3 斷線重連與心跳設計有線網(wǎng)絡比無線穩(wěn)定得多但交換機重啟、網(wǎng)線被拔、對端服務器掛了這些情況一樣會斷所以重連邏輯必須有。我的做法是在 socket 輪詢里加一個狀態(tài)檢查uint8_t sr w5500ReadByte(S0_SR, BSB_S0_REG); if (sr SR_CLOSED) { // 連接已關閉重新走 OPEN LISTEN 流程 socket0Listen(5000); }對 TCP 客戶端主動連接服務端的場景還要注意CONNECT 命令發(fā)出后會有一個短暫的中間態(tài)不能立刻判斷失敗要給它幾百毫秒轉成 ESTABLISHED。超時了再發(fā) CLOSE 然后重試。心跳方面如果對端是 MQTT 服務器用協(xié)議自帶的心跳就夠了如果是自己寫的 TCP 長連接建議每 30 到 60 秒發(fā)一個字節(jié)的心跳包。有心跳的好處是能及時發(fā)現(xiàn)TCP 連接還在但實際鏈路已斷的僵死狀態(tài)——這種情況在網(wǎng)線被拔出、交換機端口被關閉時很常見光靠 TCP 狀態(tài)機是發(fā)現(xiàn)不了的。還有一點重連要加退避。別一斷開就立刻重連那樣如果對端服務沒起來會形成密集重試。我的習慣是 1 秒、2 秒、4 秒、8 秒封頂 30 秒連上之后重置。最后再分享一個我自己用下來很受用的做法在設備上留一個通過 W5500 訪問的極簡 HTTP 接口返回當前 IP、運行時長、連接狀態(tài)、最近一次收發(fā)時間戳?,F(xiàn)場排查時手機連上同一個網(wǎng)絡打開瀏覽器就能看到設備狀態(tài)比帶電腦接串口方便太多。這個接口只用幾十行代碼但省下的現(xiàn)場時間非常可觀。