ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ESP32+W5500有线以太网实战:SPI初始化、接线与排错

ESP32+W5500有线以太网实战:SPI初始化、接线与排错 搞 ESP32 的人,十个里有八个第一次碰 W5500 都会卡在同一个地方:线接好了,代码跑了,串口一直在打印no hardware found。我第一次上手的时候更离谱,把 W5500 的SCSn接到了 GPIO2,结果板子一上电就开始抽风,折腾到半夜才发现 GPIO2 在 ESP32 上挂着板载 LED 同时还参与启动引导。后来把 SPI 的时序、片选、复位这些最基础的东西重新捋了一遍,才明白问题从来不在W5500 好不好用,而在 SPI 这套东西到底是怎么跟芯片对话的。这篇就围绕ESP32 SPI W5500 有线以太网这条链路,把例程从第一行拆到最后一行。SPI 的四个模式谁对应谁、25MHz 晶振旁边两颗电容怎么选、Ethernet.init()为什么必须写在Ethernet.begin()前面、hardwareStatus()和linkStatus()各自报的是哪一层的信息,这些都会讲到。内容偏实战,适合已经会点 Arduino、手里有块 W5500 模块、但被 SPI 时序和网络初始化顺序卡住的人,也适合想给 ESP32 项目加一条有线网络冗余通道的开发者。看完能直接抄,也能看懂每一行为什么要那么写。1. W5500 凭什么让 ESP32 让出一整路 SPI1.1 软件协议栈和硬件协议栈,差的到底是什么ESP32 本身是自带 WiFi 的,那为什么还有人非要挂一颗 W5500 去做有线?原因通常不复杂:WiFi 在工业现场、机柜内部、金属外壳里信号极不稳定,而一根网线插上去就能跑,链路质量可预期。问题在于,ESP32 上加一片普通 PHY(比如 LAN8720)之后,TCP/IP 协议栈还是要在 ESP32 里跑,中断、缓冲、内存占用全都压在 MCU 身上。W5500 走的是另一条路。它内部集成了10/100 Mbps 以太网 MAC PHY,同时把 TCP、UDP、IPv4、ICMP、ARP、IGMP 这些协议用硬件逻辑实现了。也就是说,你在 ESP32 上写的不是发一个 TCP 包、等 ACK、处理超时重传,而是把数据丢进 Socket 0 的发送缓冲,拉一下 SEND 命令,剩下的事 W5500 自己干完。这个差别在实际项目里很直观:CPU 占用:W5500 方案下 ESP32 基本不参与协议处理,主循环该采样采样、该刷屏刷屏。实时性:TCP 重传、ACK 延迟这些时间抖动由硬件决定,不受 MCU 主循环阻塞影响。内存压力:协议缓冲用的是 W5500 内部的 32KB SRAM,不占 ESP32 的堆。断线行为:硬件自己处理 keep-alive 和超时,不会因为你某个delay()就把连接拖死。代价也很明确——SPI 是唯一的瓶颈。W5500 的吞吐上限基本由 SPI 时钟和你的读缓冲策略决定,这点后面会专门讲。1.2 W5500 内部装了什么把 W5500 当成一颗挂 SPI 的网卡芯片就对了。它的内部结构大致是这几块:模块内容实际意义硬件 TCP/IP 核心TCP/UDP/IPv4/ICMP/ARP/IGMP/PPPoE协议由硬件跑,MCU 不管Socket 引擎8 个独立 Socket可以同时开 8 条连接收发缓存32KB 片上 SRAM(TXRX 合计)默认每 Socket 2KB,可重分配MAC PHY10/100 Mbps,带 Auto-MDIX直连或交叉网线都能通主机接口SPI,支持模式 0 和模式 3最高时钟按手册标称可达 80MHz那 32KB 缓存的分配是可以在初始化阶段改的。默认状态是 8 个 Socket 各 2KB 发送 2KB 接收,合计正好 32KB。如果你的应用只用一个 Socket 传大文件,完全可以把它调成 16KB/16KB,吞吐会有肉眼可见的提升。改法就是写Sn_TXBUF_SIZE(偏移 0x001F)和Sn_RXBUF_SIZE(偏移 0x001E)这两个寄存器,规则是所有 Socket 的 TX 之和不超过 16KB、RX 之和不超过 16KB。1.3 什么场景该选它,什么场景该放弃我自己的判断标准很粗糙,但一直很好用:要稳定长连接、要低 CPU 占用、数据量在几 Mbps 以内→ W5500 很合适。比如 MQTT 长连接上报、Modbus TCP 网关、HTTP 数据回传。要跑到几十 Mbps 以上、或者要同时跑 TLS→ 别用 W5500。硬件协议栈的 TLS 你是跑不了的,加密还得落在 ESP32 上,这时候 SPI 的瓶颈反而更明显。要省引脚、板子已经很挤→ 慎重。SPI 至少吃 4 根线,加上 RST 就 5 根,再加 INT 就 6 根。用 LAN8720 走 RMII 也是 9 根,但那是功能更强的代价。纯电池供电的便携设备→ 也得掂量。W5500 加上 PHY 工作时,电流峰值在 130~190mA 这个量级,对低功耗设计并不友好。选定之后,剩下所有工作都围绕一件事:让 SPI 这条链路先通,再让它稳。2. 把 SPI 的三个参数掰开:模式、时钟、片选2.1 CPOL/CPHA 与 W5500 只认的两个模式SPI 是全双工同步串行总线,四根线的分工是固定的:SCLK 出时钟、MOSI 主发从收、MISO 主收从发、CS 片选低有效。真正让人头大的只有两个参数:CPOL(时钟极性)和CPHA(时钟相位)。这两个一组合,就有了四种模式。CPOL 0:时钟空闲时为低电平。CPOL 1:时钟空闲时为高电平。CPHA 0:在第一个时钟边沿采样数据。CPHA 1:在第二个时钟边沿采样数据。组合下来就是模式 0(0,0)、模式 1(0,1)、模式 2(1,0)、模式 3(1,1)。W5500 明确只支持模式 0 和模式 3,这一点在手册的主机接口章节里写得很清楚。为什么是这两个?因为模式 0 和模式 3 的共同点是在时钟的空闲电平上先摆好数据,再用有效边沿采样。模式 1 和模式 2 则要求第一个边沿就采样,对从设备的建立时间要求更苛刻。W5500 内部是这么设计的,你硬要用模式 1 去试,表现就是读回来的寄存器值全是 0x00 或 0xFF,hardwareStatus()直接返回EthernetNoHardware。我一般直接用模式 0,理由很简单:Arduino 的 Ethernet 库内部就是按SPI_MODE0走beginTransaction()的,你自己再去改反而容易和库打架。如果非要换模式 3,得自己实现底层读写函数,不划算。2.2 时钟频率不是越高越好W5500 手册标称 SPI 最高可以到 80MHz,但这个数字有前提——短走线、干净的电源、良好的信号完整性。实际项目里我踩过的坑是这样的:8MHz:几乎是个保底值。杜邦线接的模块、面包板搭的电路,8MHz 基本都能跑通。20MHz:排线长度控制在 10cm 以内、模块带 33Ω 串联电阻的,通常没问题。30MHz 以上:需要 PCB 走线、阻抗控制、必要时串阻匹配。杜邦线上跑 30MHz,你会看到偶发的读错数据,表现是网络时通时断、DHCP 偶尔失败。参数怎么定?我的做法是先跑通再提速。先用 8MHz 让系统完整跑起来,确认能拿到 IP、能建 TCP 连接、能连续收发,然后再把时钟改到 20MHz 跑压力测试。测试方法是连续读VERSIONR寄存器几万次,统计出错次数——这个寄存器是只读常量,回读值永远是 0x04,任何偏差都是通信错误。// VERSIONR 位于通用寄存器区,偏移 0x0039,固定返回 0x04 uint8_t w5500_read_version() { uint8_t tx[4] { 0x00, 0x39, 0x04, 0x00 }; uint8_t rx[4] { 0 }; digitalWrite(PIN_W5500_CS, LOW); SPI.beginTransaction(SPISettings(20000000, MSBFIRST, SPI_MODE0)); for (int i 0; i 4; i) rx[i] SPI.transfer(tx[i]); SPI.endTransaction(); digitalWrite(PIN_W5500_CS, HIGH); return rx[3]; // 期望 0x04 }上面这 4 个字节不是随便凑的,后面讲帧格式的时候会展开。2.3 片选的软硬之分与 W5500 的那条特殊规则片选(CS)这件事,分硬件片选和软件片选两种做法。硬件片选是 SPI 外设自己拉 CS,时序由硬件保证;软件片选是你用普通 GPIO 手动拉高拉低。ESP32 的 SPI 外设两种都支持,Arduino 的 Ethernet 库走的是软件片选——Ethernet.init(cs)传进去的引脚,库内部用digitalWrite控制。这里有个很多人忽略的细节:W5500 要求 CS 在整个帧期间保持低电平。一帧包含地址段、控制段、数据段三部分,三部分之间的 CS 不能抬起来。如果你用硬件片选,而 SPI 外设又在每传输一个字节后把 CS 拉高,就容易出问题。软件片选虽然慢一点,但控制权完全在你手里,反而更省心。还有一条:CS 拉低和第一个时钟沿之间要有建立时间。W5500 手册里给了这个参数,量级在纳秒级,正常情况下不用管。但如果你的走线特别长、容性负载大,拉低 CS 后立刻发时钟,第一字节可能被吃掉。这种情况下在digitalWrite(CS, LOW)后面加一句__asm__ volatile(nop)或者干脆给个极短的延时,能解决不少玄学问题。另外,ESP32 上选 CS 引脚有个硬性避坑点:别用 GPIO12。GPIO12 是 MTDI 启动引脚,上电时如果被外部拉高,芯片会认为要切换到 1.8V flash 电压,轻则启动失败,重则反复复位。HSPI 的默认 MISO 恰好是 GPIO12,这就是为什么很多人从默认引脚直接抄过来会翻车。我现在一律用 VSPI 的默认组:SCK18、MISO19、MOSI23、CS5。3. 硬件连线:从 25MHz 晶振到 RJ45 的每一处细节3.1 电源与地:最常见的翻车点先说结论:W5500 不要直接从 ESP32 开发板的 3.3V 引脚取电,除非你确认那颗 LDO 有足够余量。原因很实在。W5500 内部 MAC PHY 在百兆链路下工作时,电流峰值在 130mA 到 190mA 之间,加上 25MHz 晶振起振时的冲击,瞬时电流还会更高。而大多数 ESP32 开发板上那颗 LDO(常见的是 AMS1117-3.3)标称输出 800mA,看起来够用,但那是理想散热条件下的数字。板子上的 ESP32 本身在 WiFi 发射时会拉走 240mA 左右,两颗芯片叠加,再算上 LDO 的压降和发热,很容易在链路建立的那一瞬间把 3.3V 拉出一个坑。我的建议做法:单独给 W5500 配一颗 LDO(比如 ME6211 或者 SPX3819 这类低压差、低噪声的),输入端从 5V 取,输出 3.3V 至少留 500mA 余量。W5500 的 VDD 引脚旁边放100nF 10µF的组合去耦,100nF 尽可能贴近引脚。AVDD(模拟电源)和 DVDD(数字电源)分开走线,最后在一点汇合,别让数字地的回流穿过模拟区域。如果实在只能用一路 3.3V,至少在 W5500 的电源入口加一个 100µF 的钽电容做能量缓冲。这个坑的症状很有意思:设备放着不动没事,一插网线、链路协商的瞬间就复位或者网络卡死。很多人会以为是软件问题,其实量一下电源波形就明白了。3.2 晶振、RBIAS 与复位三件套W5500 需要外接25MHz 晶振,内部 PLL 再倍频到工作频率。这部分有三处要注意:晶振负载电容。这个值不是固定的,得看晶振本身的规格。常见 25MHz 贴片晶振的负载电容是 12pF 或 20pF,对应到电路上,两颗接地电容的取值大约在 18pF 到 27pF 之间。取值不对的表现是晶振起振慢、频率偏、偶尔不起振。如果手头没有精确规格书,先用 22pF 试,然后用示波器或频率计看 25MHz 输出是否稳定。有些晶振方案还会并联一颗 1MΩ 电阻帮助起振。RBIAS 偏置电阻。W5500 有一根RSET_BG(也叫 RBIAS)引脚,需要接一颗精度 1% 的电阻到地。这个电阻决定内部模拟偏置电流,进而影响 PHY 的发送电平。取值要严格按手册,通常在这个系列里是12.1kΩ,误差超过 1% 就可能导致链路能协商上但误码率高。这个电阻一定要用 1% 精度的金属膜电阻,别用普通碳膜。内部 1.8V 输出。W5500 内部有 LDO 产生 1.8V 供内核使用,会从一根引脚输出,需要外接去耦电容。按手册配对应的容值(常见是几微法量级),这个电容不能省,否则内核供电不稳,表现是随机性的通信错误。复位电路。RSTn低有效,建议接10kΩ 上拉到 3.3V,同时并一颗100nF 到地做滤波。这样上电时 RC 常数会给出一段自然的复位低电平时间。软件里再主动拉低一次会更稳妥,后面例程部分会写。3.3 网络侧变压器与 RJ45 的接法网络侧这部分,如果你用的是带隔离变压器的 RJ45 座(比如常见的 HR911105A 这类集成座),那就省事很多——变压器、共模扼流圈、Bob Smith 端接都集成在里面了。市面上绝大多数W5500 以太网模块用的都是这种方案,直接排针引出 SPI,拿来就能用。如果你想自己画 W5500 的板子,网络侧的要点是这些:发送侧:TXP/TXN通过 49.9Ω 电阻连到变压器初级,变压器中心抽头经去耦网络接 AVDD。接收侧:RXIP/RXIN同样通过 49.9Ω 到变压器次级,中心抽头也接 AVDD。变压器变比是 1:1,这是 10/100Base-T 的标准做法。Bob Smith 端接:变压器线路侧的四根线各通过 75Ω 电阻汇到一点,再用一颗 1000pF 高压电容接到机壳地。这能显著降低共模辐射,过 EMC 认证时少不了。LED 引脚:W5500 有 LED1/LED2 输出,通过限流电阻接 LED 到 3.3V 即可,分别指示 Link 和 Activity。3.4 引脚对照表与 INT 的实际用法把接线整理成一张表,照着接就行:ESP32 引脚W5500 引脚方向说明GPIO18SCLK输出SPI 时钟,模式 0 空闲低GPIO19MISO输入主入从出GPIO23MOSI输出主出从入GPIO5SCSn输出片选,低有效GPIO4RSTn输出复位,低有效GPIO34INTn输入中断,低有效,可选3V3(独立 LDO)VDD / AVDD电源需 300mA 以上余量GNDGND / AGND地单点共地关于INTn,我的态度是:第一版可以不接,但一定要留焊盘。不用中断也能跑,库内部靠轮询Sn_IR寄存器判断收发状态,功能完整,只是主循环里得频繁问 W5500你有事吗。接了中断之后,可以做到有数据才醒,空闲时 CPU 完全不碰 SPI,功耗和效率都好很多。如果不用中断,GPIO34 悬空即可。GPIO34 到 GPIO39 这几根在 ESP32 上是纯输入引脚,没有内部上下拉,所以别指望用INPUT_PULLUP去配它——需要上拉的话只能外置电阻。4. 逐行拆解初始化例程:从 SPI.begin() 到 linkStatus()4.1 头文件、宏定义与引脚声明先把骨架搭出来。这部分看着琐碎,但每一行都有它的位置。#include SPI.h #include Ethernet.h // 引脚定义,按实际接线改 #define PIN_W5500_CS 5 #define PIN_W5500_RST 4 #define PIN_W5500_INT 34 #define PIN_SPI_SCK 18 #define PIN_SPI_MISO 19 #define PIN_SPI_MOSI 23 // 本地 MAC,必须全局唯一 byte mac[] { 0x02, 0x00, 0x00, 0x12, 0x34, 0x56 }; // 静态 IP 方案用这四个 IPAddress ip(192, 168, 1, 177); IPAddress dns(192, 168, 1, 1); IPAddress gateway(192, 168, 1, 1); IPAddress subnet(255, 255, 255, 0); EthernetClient client;mac[]这里有个细节值得说。头一个字节0x02不是随便写的。MAC 地址的最高字节里,bit0 是单播/组播标志,bit1 是全局/本地标志。0x02展开成二进制是0000 0010,bit1 为 1,表示这是一个本地管理地址,不会和任何厂商申请的 OUI 冲突。很多新手直接写{0x00,0x00,0x00,0x00,0x00,0x00}或者全0xFF,前者交换机会直接丢弃,后者的行为不可预测。用0x02打头是最稳妥的做法。IPAddress这几个对象只在静态 IP 方案里用。如果你想走 DHCP,这四个可以完全不定义。4.2 上电复位时序为什么不能省W5500 内部有 PLL、有寄存器状态机,上电之后需要一个干净的复位脉冲才能进入确定状态。手册给的复位低电平最小宽度是500µs,但这个最小值是在理想条件下的数字,实际电路里RSTn上挂着 100nF 滤波电容,边沿会被拉缓,所以我一般给到50ms,足够宽裕。void setup() { Serial.begin(115200); while (!Serial millis() 3000) { } // 1. 硬复位 W5500 pinMode(PIN_W5500_RST, OUTPUT); digitalWrite(PIN_W5500_RST, LOW); delay(50); digitalWrite(PIN_W5500_RST, HIGH); delay(150);复位拉高之后的delay(150)也不能省。W5500 内部 PLL 锁定、晶振稳定都需要时间,手册给的建议是复位释放后等待一段时间再开始 SPI 通信,量级在几十到一百多毫秒。我给 150ms 是个保守值,不影响启动速度,但能避免大量偶尔读不到芯片的偶发问题。还有一个顺序问题:复位必须在 SPI 初始化之前完成。如果先SPI.begin()再复位,复位期间 SPI 引脚上可能有电平跳变,在 W5500 处于未初始化状态时被误识别成命令,虽然概率不高,但没必要冒这个险。4.3 SPI.begin() 与 Ethernet.init() 的先后顺序// 2. 启动 SPI,重映射到我们指定的引脚 SPI.begin(PIN_SPI_SCK, PIN_SPI_MISO, PIN_SPI_MOSI, PIN_W5500_CS); // 3. 把 CS 引脚告诉 Ethernet 库 Ethernet.init(PIN_W5500_CS);SPI.begin(sck, miso, mosi, ss)这个四参数版本在 ESP32 上做的是引脚重映射。ESP32 的 SPI 外设通过 GPIO 矩阵可以接到几乎任意引脚,这个函数就是把默认的 VSPI 引脚重新指到你要用的位置。注意第四个参数虽然叫 ss,但这里只是告诉 SPI 外设 SS 是哪根线,并不改变Ethernet.init()的参数,两处要写同一个引脚,不然会错位。Ethernet.init(cs)的作用是让库内部记录下片选引脚号,后面所有 SPI 事务都会用这根线。这个调用必须写在Ethernet.begin()之前,因为begin()里面第一件事就是读取VERSIONR判断芯片型号,如果那时 CS 还是未定义状态,读回来的自然是垃圾值。ESP32 上还有个隐藏点:Arduino-ESP32 里的默认SPI对象对应的是 VSPI(FSPI/SPI2)。如果你非要用 HSPI,需要额外把 SPI 对象传进库,而标准 Ethernet 库的 API 并不直接支持,得自己改库或者用 ESP-IDF 的esp_eth驱动。我的建议是第一版别折腾,老实走 VSPI。4.4 Ethernet.begin() 两种形态:静态与 DHCPEthernet.begin()有两个常用重载,对应的就是两种上电策略。静态 IP 版本:Ethernet.begin(mac, ip, dns, gateway, subnet);参数顺序是 MAC、IP、DNS、网关、子网掩码,五个参数一个都不能少。这个版本的好处是启动快,网络拓扑固定的时候最稳,工业现场基本都用这个。缺点是 IP 改一次就要重新烧固件,如果现场有多台设备,得在代码里把 IP 做成可配置项。DHCP 版本:Ethernet.begin(mac);就一个 MAC 参数,剩下的等路由器分配。这个版本内部会阻塞一段时间去等 DHCP 响应,超时之后如果没有拿到地址,Ethernet.localIP()会返回0.0.0.0。所以用 DHCP 一定要检查返回值:if (Ethernet.localIP() IPAddress(0, 0, 0, 0)) { Serial.println([ETH] DHCP failed, fallback to static); Ethernet.begin(mac, ip, dns, gateway, subnet); }这个 fallback 逻辑在实际项目里非常值得加。DHCP 失败的原因太多了——路由器重启中、网段被占满、网线插在了一个没有 DHCP 服务的交换机上。加个兜底,设备至少还能用静态地址活下来。Ethernet.begin()是阻塞的,DHCP 模式下最长可能卡住几十秒。如果你的设备有看门狗或者启动时间要求,得把这段放到独立的初始化阶段,别塞在主循环的第一帧里。4.5 hardwareStatus() 与 linkStatus() 各管什么这两个函数是排查问题的第一把钥匙,很多人搞混它们的含义。if (Ethernet.hardwareStatus() EthernetNoHardware) { Serial.println([ETH] no W5500 found, check SPI wiring); while (true) delay(1000); } if (Ethernet.hardwareStatus() EthernetW5500) { Serial.println([ETH] W5500 detected); } if (Ethernet.linkStatus() LinkOFF) { Serial.println([ETH] cable not plugged); }hardwareStatus()报的是SPI 这一层通不通。库内部会去读 W5500 的VERSIONR,读到 0x04 就认为芯片是 W5500,返回EthernetW5500;读到 0x03 会认为是 W5100 或 W5200;什么都读不到就返回EthernetNoHardware。这个判断和网线插不插完全无关。linkStatus()报的是PHY 这一层链路是否建立。网线插好、对端设备在线、协商完成,才会返回LinkON。如果 SPI 通了但网线没插,hardwareStatus()是EthernetW5500,linkStatus()是LinkOFF。这两个状态一交叉,问题就被切成四块:hardwareStatuslinkStatus含义排查方向NoHardware—SPI 不通接线、CS、模式、供电、复位W5500LinkOFF芯片通了,链路没起来网线、对端设备、变压器、RBIASW5500LinkON物理层正常继续查 IP、DHCP、TCP 连接W5100/W5200—型号识别异常大概率是 SPI 时序不稳导致读错这张表我建议贴在手边。每次出问题先跑一次自检打印,能省掉一大半盲猜时间。5. 把数据真正发出去:Socket 编程与心跳保活5.1 一个最小的 TCP 客户端闭环物理层通了只是开始,真正干活的是 Socket。W5500 有 8 个独立 Socket,库层面用EthernetClient对象来抽象,一个对象占一个 Socket。IPAddress serverIP(192, 168, 1, 100); const uint16_t serverPort 6000; unsigned long lastConnectTry 0; const unsigned long RETRY_INTERVAL 3000; void loop() { if (client.connected()) { // 已连接:发心跳 收数据 client.print(hb ); client.println(millis()); client.flush(); while (client.available()) { char c client.read(); Serial.write(c); } delay(2000); } else { // 未连接:按间隔重试 if (millis() - lastConnectTry RETRY_INTERVAL) { lastConnectTry millis(); Serial.print([TCP] connecting... ); if (client.connect(serverIP, serverPort)) { Serial.println(ok); } else { Serial.println(fail); client.stop(); } } } }client.flush()这一句很多人会漏。它的作用是把库内部缓冲区里的数据真正推给 W5500,不然数据可能在你下一次操作之前还躺在 ESP32 的内存里。对时序敏感的应用,写完就 flush 是好习惯。client.connected()也不只是查一个本地标志位,库内部会去读 Socket 状态寄存器Sn_SR,判断它是不是还处于SOCK_ESTABLISHED状态。所以这个调用是有 SPI 开销的,别放进高频循环里,几秒查一次就够了。5.2 client.connect() 返回 false 的几种真实原因connect()失败是最常见的现象,但原因差别很大。按我踩过的顺序列一下:对端根本没在监听。这是最高频的。先用 PC 上的工具确认目标端口是开的,connect()会等一段时间然后超时返回 false。IP 和网关配错。目标在另一个网段,但网关填错了,包出不去。用Ethernet.gatewayIP()打印出来核对一遍。子网掩码写错。比如实际是255.255.255.0你写成了255.255.0.0,同网段的判断就会出错。这个错误很隐蔽,因为 ping 可能还是通的。Socket 数量耗尽。每个EthernetClient占一个 Socket,如果你在循环里反复new对象而不stop(),八个 Socket 很快用完,后面全部失败。W5500 内部状态卡死。TCP 连接异常断开后,Socket 可能停在SOCK_CLOSE_WAIT状态,需要显式client.stop()才能释放。我在重连逻辑里一律先stop()再connect()。EthernetClient::connect()是阻塞的,超时时间由库内部决定,通常几秒。如果你不方便阻塞,得换成非阻塞写法自己管状态机。5.3 断线检测与自动重连状态机上面那个简单版本在小项目里够用,但真正上线跑,我会写成状态机。理由是:网络异常的种类太多,单一connected()判断覆盖不全。enum NetState { NET_IDLE, NET_CONNECTING, NET_ONLINE }; NetState netState NET_IDLE; unsigned long stateTimer 0; unsigned long lastRxTime 0; const unsigned long RX_TIMEOUT 30000;核心逻辑是三层判断:链路层:定期查Ethernet.linkStatus(),网线被拔了直接回NET_IDLE。连接层:client.connected()为假就回NET_IDLE,触发重连。应用层:记录最后一次收到数据的时间,超过 30 秒没有数据就主动stop()重建连接。第三层是很多人不做但非常必要的。TCP 连接有时会进入一种看起来还连着,但实际已经死了的假死状态——对端断电、中间路由器丢掉了会话表、NAT 表项超时,这些情况下connected()可能还会返回真。加一个应用层的心跳超时,能把这些假死状态揪出来。5.4 W5500 硬件 Keep-Alive 的寄存器配置如果你不想自己在应用层发心跳,W5500 提供了硬件级的 TCP Keep-Alive 功能。它是这样工作的:连接建立后,如果一段时间没有数据往来,W5500 自动发送保活探测包,对端没有响应就自动断开连接。配置方法是写 Socket 的Sn_KPALVTR寄存器(偏移0x002F)。写入的数值单位是5 秒,写 0 表示关闭,写 12 表示每 60 秒发一次探测。走库的时候没法直接写这个寄存器,得自己拼 SPI 帧。W5500 的 SPI 帧结构是这样的:阶段长度内容地址段2 字节16 位偏移地址,大端序控制段1 字节BSB[4:0] RWB OM[1:0]数据段N 字节实际读写的数据控制字节的位分配是:bit7~bit3 是BSB(块选择),bit2 是RWB(0 写 / 1 读),bit1~bit0 是OM(操作模式:00 变长、01 固定 1 字节、10 固定 2 字节、11 固定 4 字节)。BSB 的取值决定了你访问哪块区域:BSB 值区域00000通用寄存器00001 ~ 01000Socket 0 ~ 7 寄存器01001 ~ 10000Socket 0 ~ 7 发送缓冲10001 ~ 11000Socket 0 ~ 7 接收缓冲回到前面那个读VERSIONR的例子:0x00 0x39是地址,控制字节0x04拆开看就是 BSB00000(通用寄存器)、RWB1(读)、OM00(变长),最后再补一个0x00把数据时钟出来,返回的就是版本号。现在写Sn_KPALVTR就很清楚了。假设要配 Socket 0,BSB 是 00001,偏移 0x002F,写操作 RWB0,变长模式 OM00:void w5500_write_u8(uint8_t bsb, uint16_t addr, uint8_t val) { digitalWrite(PIN_W5500_CS, LOW); SPI.beginTransaction(SPISettings(20000000, MSBFIRST, SPI_MODE0)); SPI.transfer((addr 8) 0xFF); SPI.transfer(addr 0xFF); SPI.transfer(((bsb 0x1F) 3) | (0 2) | 0x00); // 写,变长 SPI.transfer(val); SPI.endTransaction(); digitalWrite(PIN_W5500_CS, HIGH); } // 给 Socket 0 配置 60 秒硬件保活 w5500_write_u8(0b00001, 0x002F, 12);这段代码的价值不只是配保活。它把 W5500 的帧格式讲透了,后面你想读Sn_SR(偏移 0x0002)、查Sn_RX_RSR(接收数据长度,偏移 0x0026),都是同一套逻辑,换 BSB 和地址而已。库没暴露的功能,靠这几行就能补上。6. 排错实录:链路不通、拿不到 IP、通信丢包的完整排查链路6.1 第一步永远是分离SPI 通不通和网通不通我给自己定过一条规矩:任何 W5500 问题,先看hardwareStatus()。这一句话能砍掉一半无用功。如果hardwareStatus()返回EthernetNoHardware,就别去管 IP、网关、DHCP 了,问题百分之百在 SPI 这一侧。按这个顺序查:量电源。W5500 的 VDD 引脚上是不是稳定的 3.3V?用示波器看有没有跌落。这是最快能排除的。量复位。RSTn拉高之后是不是稳定在高电平?如果量出来在 1.5V 左右晃,说明上拉电阻太大或者被什么拉住了。量晶振。用示波器探 25MHz 晶振的一脚,应该能看到稳定振荡。没有振荡就说明晶振或负载电容有问题。读 VERSIONR。用前面那段w5500_read_version()打印结果。返回 0x00 通常是 MISO 没接对或者 CS 没拉对;返回 0xFF 通常是 MISO 被拉死或者模块没供电;返回 0x04 说明 SPI 是通的,那hardwareStatus()报NoHardware只可能是你 SPI 时钟配太高导致间歇性读错,降速再试。看波形。有逻辑分析仪的话,抓一次VERSIONR读取:确认 SCLK 空闲是低电平(模式 0)、CS 在整帧期间保持低、MISO 上确实有回应。这三条对上,基本就通了。6.2 DHCP 拿不到地址的检查点hardwareStatus()和linkStatus()都正常,但localIP()是0.0.0.0,这种情况我列过一张检查清单:网线是否插在了有 DHCP 服务的口上。有些交换机是纯二层设备,不做 DHCP;有些企业网口做了端口隔离。网线本身。用同一条线插电脑试试,先把线排除掉。路由器 DHCP 地址池是否耗尽。设备多的时候很常见,尤其是地址租约时间设得长的场景。MAC 是否重复。同一网段里两台设备用了同一个 MAC,DHCP 服务器会混乱,表现就是一会儿能拿到地址一会儿拿不到。防火墙或 DHCP Snooping。某些网络会丢弃非白名单设备的 DHCP 请求。Ethernet.begin()是否被调用过两次。有些代码在setup()里调一次,在主循环里又调一次,第二次会打乱状态。超时时间是否太短。标准 DHCP 流程包含 DISCOVER、OFFER、REQUEST、ACK 四步,网络慢的时候可能超过库的默认超时。静态 IP 和 DHCP 混用。先用静态配好了,又调Ethernet.begin(mac)走 DHCP,寄存器里的地址会被覆盖成不一致的状态。子网里有没有 IP 冲突。静态 IP 方案下,如果这个地址已经被别的设备占了,链路看起来正常但通信会丢包。排查手段很简单:在 PC 上装个抓包工具,抓几次 DHCP 交互,能看到到底是哪一步断了。DISCOVER 发出去没有 OFFER,说明请求没到服务器;有 OFFER 但没有 ACK,说明是服务器拒绝或者 ACK 丢了。6.3 大包丢数据与时序问题小包能通、大包丢数据,是 W5500 项目里很典型的一种症状。原因通常是 SPI 读取节奏跟不上 W5500 的接收速度。具体机制是这样的:数据到达后 W5500 存进 Socket 的接收缓冲区,同时置位接收中断标志。你的程序通过读Sn_RX_RSR知道有多少字节,然后读Sn_RX_RSR次的接收缓冲。如果你读得太慢,缓冲区满了之后 W5500 会直接丢弃后续数据——TCP 层的流控会生效,但因为窗口变小,吞吐就下来了。几个优化方向:提高 SPI 时钟。这是最直接的,8MHz 到 20MHz 吞吐能翻一倍多。把接收缓冲区调大。用Sn_RXBUF_SIZE把不用的 Socket 的缓冲匀给当前 Socket,单 Socket 最大可以到 16KB。用批量读。控制段用OM 00(变长模式),一次读多个字节,而不是每个字节都重新拼一次帧头。我第一次写底层驱动时就是每字节都发一次 3 字节头,结果有效数据率只有三分之一,慢得离谱。缩短主循环里的阻塞操作。delay()、串口打印、WiFi 相关操作都会影响读数据的及时性。还有一个隐蔽的坑:SPI 时钟太快导致的偶发误码。这种问题的表现是有时候好有时候坏,重传次数增多,但不会完全断。判断方法就是跑几万次VERSIONR读取统计错误率,超过万分之一就要降速或者改硬件。6.4 长时间运行的稳定性问题还有一类问题只在设备连续跑几天之后才出现。我遇到过的有这几种:Socket 泄漏。连接异常断开时没有调stop(),Socket 一直停在SOCK_CLOSE_WAIT,几次之后八个 Socket 全被占死。解决办法是在重连逻辑里强制先stop()。内存碎片。如果代码里频繁new EthernetClient,堆会碎。把 client 对象做成全局变量,或者用固定数组管理。看门狗复位。Ethernet.begin()在 DHCP 模式下会阻塞较久,如果这时看门狗没喂,芯片会复位。解决办法是把网络初始化放在看门狗启动之前,或者初始化期间临时喂狗。温度漂移。这个比较少见但确实存在。W5500 的 PHY 在高温下链路可能变得不稳,配合电源余量不足时尤其明显。这种问题的排查方向是测工作温度下的电源纹波和 SPI 眼图。判断是不是 W5500 的问题,一个很实用的手段是打印 SPI 错误计数。在主循环里定期读VERSIONR,统计连续出现的异常次数,出现异常就记录时间戳。这样跑几天之后你手上就有了一份完整的错误日志,能看出是周期性还是随机性,是集中在某个时段还是均匀分布。这比凭感觉猜要靠谱得多。7. 进阶:多 Socket 并发、中断驱动与吞吐优化7.1 8 个 Socket 与 32KB 缓存的分配策略W5500 的 8 个 Socket 是完全独立的,每个都有自己的状态机、寄存器组和缓冲。这意味着你可以同时开一个 TCP 服务端、一个 TCP 客户端、一个 UDP 收发端口,互不干扰。但片上缓存总共就 32KB,是 8 个 Socket 共享的。默认每 Socket 2KB TX 2KB RX,刚好 32KB。如果你的应用只用 4 个 Socket,可以这样分配:用途Socket 数单 Socket TX单 Socket RX小计主数据通道18KB8KB16KB控制通道14KB4KB8KB备用22KB2KB8KB合计4——32KB分配的方式是在初始化阶段写Sn_TXBUF_SIZE和Sn_TXBUF_SIZE。约束条件是:所有 Socket 的 TX 之和不能超过 16KB,RX 之和也不能超过 16KB,这是 W5500 内部的物理划分。写之前必须先关闭对应 Socket,写完再打开。缓冲区调大之后,大包传输的丢包会明显减少,因为 W5500 有更多空间去容忍你读取的延迟。但要注意别把缓冲设太满,如果某个 Socket 缓冲区过大,你每次读取都要搬很多数据,主循环的响应性会变差。7.2 从轮询到中断轮询模式的问题在于,你即使没事也要去问 W5500。每次问都要走一次 SPI 事务,累加起来是实实在在的 CPU 开销。用中断之后,INTn引脚会在这些事件上拉低:收到数据、发送完成、连接状态变化、超时。你只需要把INTn接到一根能触发中断的 GPIO 上,在中断服务函数里置个标志位,主循环看到标志位再去处理。volatile bool w5500_irq_flag false; void IRAM_ATTR onW5500Interrupt() { w5500_irq_flag true; } void setup_interrupt() { pinMode(PIN_W5500_INT, INPUT); attachInterrupt(digitalPinToInterrupt(PIN_W5500_INT), onW5500Interrupt, FALLING); }几个实践要点:中断服务函数里别做 SPI 操作。SPI 事务耗时较长,放在 ISR 里会阻塞其他中断,触发看门狗。ISR 只置标志,处理放主循环。要清中断标志。W5500 的中断源在Sn_IR寄存器里,处理完必须写 1 清除,否则不会产生下一次中断。用库的时候这些由库负责,自己写底层驱动就得注意。中断引脚要加去抖。机械振动或者电源噪声可能让 INT 误触发,软件侧加个时间窗过滤会稳很多。不用中断也不是不行,只是主循环里得保证有一个固定的轮询周期,比如 10ms 一次,别让某段代码长时间阻塞。7.3 吞吐实测与参数取舍W5500 在 ESP32 上的实际吞吐,很大程度由 SPI 时钟和读取效率决定。我实测过几组配置,数据仅供参考,具体数值和你的走线、电源质量强相关:SPI 时钟单 Socket 缓冲读取方式大致吞吐8MHz2KB逐字节1.5 ~ 2 Mbps8MHz2KB变长批量读3 ~ 4 Mbps20MHz2KB变长批量读7 ~ 9 Mbps20MHz16KB变长批量读10 ~ 12 Mbps有个规律很直观:从逐字节读改成变长批量读,收益比单纯提高时钟还大。因为每帧有 3 字节的固定开销,逐字节读的话有效数据率只有 25%;批量读之后,开销被摊薄到几乎可以忽略。还有一个容易被忽略的影响因素是 TCP 窗口大小。W5500 的接收缓冲直接决定了它能通告的窗口大小,窗口小的话,对端一次只能发这么多,吞吐自然上不去。这也是为什么把缓冲调大之后吞吐会提升——不只是本地读得下,对端也发得更多了。取舍上我的建议是:先按最保守的参数跑通,拿到基线数据,然后一次只改一个变量(先提时钟,再改批量读,最后调缓冲),每次跑一段时间观察稳定性。同时改多个参数,出问题的时候你分不清是谁的责任。最后分享一个我在调试现场用得很顺手的小习惯:在设备上留一个串口诊断命令,输入stat就打印当前的链路状态、IP、网关、Socket 状态、SPI 错误计数、最后一次收发时间。这东西平时没什么用,但一旦现场出问题,你让对方敲两个字母就能拿到全套信息,比让他描述现象、你远程猜要高效得多。第一次做这个命令可能花半小时,后面每次现场支持都能省几个小时,这笔账怎么算都划算。
返回列表