
ESP32 这颗芯片玩过的人都知道外设资源丰富得有点过剩I2C、SPI、UART、I2S、CAN 一应俱全。但真到要把某个外设调通、还要跟一颗具体芯片对接的时候很多人就卡住了——尤其是 SPI。原因不复杂SPI 本身协议简单可一旦落到 ESP-IDF 的驱动层spi_bus_initialize、spi_bus_add_device、spi_device_transmit这一套 API 加上各种结构体字段新手很容易被绕晕。再叠加上 W5500 这种硬件 TCP/IP 协议栈芯片接线、片选、时钟模式、字节序任何一个环节出错现象都是ping 不通或者时通时断排查起来毫无头绪。这篇内容就是冲着这个痛点来的。我会把 ESP32 通过 SPI 驱动 W5500 做有线以太网的完整链路拆开讲从 SPI 四种模式的本质、ESP-IDF 里 SPI 主机驱动的初始化顺序到 W5500 的寄存器读写时序、片选和中断处理再到实际跑通之后那些文档里不会写、但一定会遇到的坑。目标很明确——你看完之后能拿着自己的 ESP32 开发板和 W5500 模块照着接线、照着配置把有线网络跑起来并且知道每一行代码为什么这么写。适合已经会点 Arduino 或 ESP-IDF、但 SPI 一直没吃透的嵌入式开发者也适合想给 ESP32 加一路稳定有线网口的物联网项目。1. 先把 SPI 这件事说透为什么 W5500 偏偏要用它1.1 SPI 四种模式不是玄学是时钟极性和相位的组合很多人调 SPI 出问题根子在于没搞明白 CPOL 和 CPHA 到底在控制什么。我用一句话概括CPOL 决定空闲时时钟线是高还是低CPHA 决定数据在时钟的哪个边沿被采样。这两个参数一组合就是所谓的 Mode 0 到 Mode 3。模式CPOLCPHA空闲电平采样边沿输出边沿Mode 000低上升沿下降沿Mode 101低下降沿上升沿Mode 210高下降沿上升沿Mode 311高上升沿下降沿W5500 的数据手册写得很清楚它支持SPI Mode 0 和 Mode 3。这两个模式的共同点是 CPHA0也就是数据在第一个边沿就被采样。为什么 W5500 选这两个因为它内部逻辑是时钟空闲态稳定后第一个跳变沿锁存 MOSI 上的数据这样设计对芯片内部的移位寄存器最省事。你在 ESP-IDF 里配置的时候spi_device_interface_config_t结构体里的mode字段填 0 就行这是最稳妥的选择。我见过有人图省事填了 Mode 1结果读回来的版本号寄存器W5500 的VERSIONR地址0x0039永远是0x00或者0xFF。这就是典型的采样边沿错位——芯片在上升沿把数据推出来你却在下降沿去采采到的自然是上一个比特或者垃圾值。所以调 SPI 的第一步永远是确认从设备的模式然后死磕这个模式。1.2 全双工的本质MOSI 和 MISO 是两条独立的移位通道SPI 叫全双工同步串行总线全双工这三个字经常被忽略。它的意思是主机在 MOSI 上发一个比特的同时从机也在 MISO 上回一个比特。这两个动作由同一个时钟驱动同时发生。理解这一点非常关键因为它解释了一个新手常犯的困惑我只是想读一个寄存器为什么还要发数据答案就在全双工机制里——SPI 没有单独的读命令读操作本质上是一次写一个地址、同时把数据挤出来的过程。主机必须产生时钟而从机只有在时钟驱动下才会把内部数据移位到 MISO 上。所以哪怕你只想读也得发够时钟周期。W5500 的读写时序就是基于这个原理设计的。它用地址段的最高位来区分读和写发送的 3 字节地址里如果最高位是 1表示读是 0表示写。后面跟着控制字节指定哪个区块和实际数据。整个过程中MOSI 和 MISO 同时在工作主机发地址的时候从机可能已经在准备数据了。1.3 片选硬件片选和软件片选到底怎么选SPI 总线可以挂多个从设备靠片选CS/SS来区分。ESP32 的 SPI 主机控制器支持硬件片选也支持软件片选这两者在 ESP-IDF 里的配置方式完全不同。硬件片选是 ESP32 的 SPI 外设自动控制的。你在spi_bus_add_device的时候指定一个 CS 引脚之后每次spi_device_transmit硬件会自动拉低这个引脚、传输完自动拉高。好处是时序精准、不占 CPU坏处是 CS 引脚必须是 SPI 外设支持的 IO_MUX 引脚灵活性差一点。软件片选则是你自己用gpio_set_level手动控制一个普通 GPIO。好处是任意引脚都能用坏处是每次传输前后要自己拉低拉高如果中间被打断时序就乱了。W5500 这种对时序敏感的芯片我强烈建议用硬件片选。原因很实际W5500 在一次 SPI 事务里要求 CS 保持低电平贯穿整个 4 字节地址控制数据的传输如果软件片选在中间被高优先级任务抢占CS 被意外拉高W5500 内部状态机就会复位这次读写直接作废。用硬件片选这些都由 SPI 外设硬件保证你只管调spi_device_transmit就行。提示如果你非要用软件片选务必在传输前后加临界区保护或者确保该任务优先级足够高、不会被频繁打断。但说实话能硬件就硬件别给自己找麻烦。2. ESP-IDF 里 SPI 主机驱动的初始化顺序一步都不能乱2.1 总线初始化spi_bus_initialize 到底做了什么ESP-IDF 的 SPI 主机驱动分两层总线层bus和设备层device。这个分层设计是理解整个驱动 API 的钥匙。spi_bus_initialize负责初始化 SPI 外设本身它做的事情包括配置 SPI 时钟源、设置默认的 IO_MUX 引脚映射、分配 DMA 通道如果用了 DMA、初始化总线互斥锁。这个函数一个 SPI 主机比如 SPI2_HOST只能调用一次重复调用会返回ESP_ERR_INVALID_STATE。spi_bus_config_t buscfg { .mosi_io_num GPIO_NUM_11, .miso_io_num GPIO_NUM_13, .sclk_io_num GPIO_NUM_12, .quadwp_io_num -1, .quadhd_io_num -1, .max_transfer_sz 4096, }; ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, buscfg, SPI_DMA_CH_AUTO));这里有几个点值得说。max_transfer_sz设的是单次传输的最大字节数W5500 一帧最多也就几 KB设 4096 足够。SPI_DMA_CH_AUTO让驱动自动选 DMA 通道如果你传输的数据量很小比如就几个字节的寄存器读写其实可以传SPI_DMA_DISABLED省点资源。但 W5500 收发网络包的时候数据量会上去建议还是开 DMA。quadwp_io_num和quadhd_io_num是给 Quad SPI 用的比如接 Flash普通 SPI 从设备填 -1 就行。这两个字段新手经常忘了填导致结构体里有垃圾值初始化直接失败。2.2 设备添加spi_bus_add_device 的参数怎么定总线初始化完之后要往总线上挂设备。W5500 就是一个设备。spi_device_interface_config_t devcfg { .clock_speed_hz 20 * 1000 * 1000, .mode 0, .spics_io_num GPIO_NUM_10, .queue_size 7, .command_bits 0, .address_bits 0, .dummy_bits 0, }; spi_device_handle_t spi; ESP_ERROR_CHECK(spi_bus_add_device(SPI2_HOST, devcfg, spi));clock_speed_hz我填了 20MHz。W5500 手册标称最高支持 80MHz但实际用下来20MHz 到 30MHz 是比较稳的区间。为什么不上 80MHz因为走线长度、PCB 质量、杜邦线接触电阻都会影响信号完整性频率越高越容易出错。先用 20MHz 跑通稳定了再往上试这是工程上的稳妥做法。queue_size是传输队列深度。如果你在多个任务里并发调用 SPI 传输队列大一点能减少阻塞。W5500 的驱动一般在一个网络任务里串行操作设 7 够用。command_bits、address_bits、dummy_bits这三个字段是给那些地址和数据分离的 SPI 协议用的。W5500 的地址是作为数据的一部分发出去的不需要驱动帮我们分离所以全填 0。这一点很多人搞混以为 W5500 的地址要填到address_bits里结果发出去的时序完全不对。2.3 传输函数spi_device_transmit 和它的兄弟们ESP-IDF 提供了好几个传输函数常用的有spi_device_transmit阻塞式等传输完成才返回spi_device_polling_transmit轮询式不依赖中断适合在中断上下文或对延迟敏感的场景spi_device_queue_transspi_device_get_trans_result异步式适合高吞吐场景W5500 的寄存器读写我一般用spi_device_polling_transmit。原因是它不涉及中断和任务切换时序最干净而且 W5500 的读写都是短事务轮询的开销可以接受。spi_transaction_t t { .length 8 * 4, // 4 字节单位是比特 .tx_buffer tx_data, .rx_buffer rx_data, }; ESP_ERROR_CHECK(spi_device_polling_transmit(spi, t));注意length的单位是比特不是字节。这个坑我踩过——填了 4 以为是 4 字节结果只发了 4 个比特读回来的数据全是错的。ESP-IDF 的文档里写了但新手看结构体定义的时候容易忽略。3. W5500 的寄存器读写时序把数据手册翻译成人话3.1 通用帧格式地址、控制、数据三段式W5500 的每一次 SPI 事务都是固定格式2 字节地址 1 字节控制 N 字节数据。地址是 16 位的控制字节决定操作哪个区块、是读还是写。控制字节的位定义是这样的位名称含义B7RWB1读0写B6-B3BSB区块选择B2RWB 保留固定 0B1-B0保留固定 0BSBBlock Select Bits用来选 W5500 内部的哪个寄存器区块比如通用寄存器、Socket 0 寄存器、Socket 0 的 TX/RX 缓冲区等。这个设计让 W5500 用 16 位地址就能访问远超 64KB 的空间。举个实际例子读 W5500 的版本寄存器VERSIONR通用寄存器区块偏移0x0039// 读 VERSIONR uint8_t tx[4] {0x00, 0x39, 0x08, 0x00}; // 地址高字节, 地址低字节, 控制字节(读通用寄存器), 占位 uint8_t rx[4] {0}; spi_transaction_t t { .length 8 * 4, .tx_buffer tx, .rx_buffer rx, }; spi_device_polling_transmit(spi, t); // rx[3] 就是 VERSIONR 的值正常应该是 0x04控制字节0x08拆开看B70写不对这里是读……等等我上面表格写的是 B71 为读。那0x08的二进制是0000 1000B70这是写操作啊。这里我要纠正一下W5500 的控制字节里读操作 B71写操作 B70。所以读 VERSIONR 的控制字节应该是0x08 | 0x80 0x88不对通用寄存器区块的 BSB 是000读的话 B71所以控制字节是1000 00000x80。我上面代码里写0x08是错的正确的读操作控制字节是0x80。这个细节特别容易错因为不同资料对位域的描述方式不一样。我的建议是直接对着 W5500 数据手册的表格把每个区块的读写控制字节算出来做成宏定义别每次现算。#define W5500_BSB_COMMON 0x00 #define W5500_BSB_S0_REG 0x01 #define W5500_BSB_S0_TX 0x02 #define W5500_BSB_S0_RX 0x03 #define W5500_READ(bsb) (0x80 | ((bsb) 3)) #define W5500_WRITE(bsb) (0x00 | ((bsb) 3))这样读通用寄存器就是W5500_READ(W5500_BSB_COMMON)0x80写 Socket 0 寄存器就是W5500_WRITE(W5500_BSB_S0_REG)0x08。清晰多了。3.2 读操作的完整流程为什么读之前要先发一个空字节W5500 的读时序有个特点发送完地址和控制字节后需要再发一个 dummy 字节通常是 0x00来产生时钟从机才会把数据放到 MISO 上。这个 dummy 字节不是 W5500 要求的特殊命令而是 SPI 全双工机制的自然结果——主机必须持续产生时钟从机才能持续移位输出。所以一次完整的读操作是 4 个字节地址高、地址低、控制字节、dummy。前 3 个字节主机在 MOSI 上发地址和控制第 4 个字节主机发 0x00内容无所谓同时从机在 MISO 上把目标寄存器的值送回来。如果是连续读多个字节比如读 RX 缓冲区里的网络包流程是地址控制dummy然后继续发 N-1 个 dummy 字节每个字节都会带回一个数据字节。W5500 内部地址会自动递增。3.3 写操作的完整流程地址之后直接跟数据写操作简单一些地址高、地址低、控制字节然后直接跟要写的数据。不需要 dummy 字节因为主机在 MOSI 上发的就是有效数据从机直接接收。// 写 Socket 0 的 Sn_PROTO 寄存器偏移 0x0014设为 TCP(6) uint8_t tx[4] {0x00, 0x14, W5500_WRITE(W5500_BSB_S0_REG), 0x06}; spi_transaction_t t { .length 8 * 4, .tx_buffer tx, .rx_buffer NULL, }; spi_device_polling_transmit(spi, t);这里rx_buffer传 NULL 是允许的表示只写不读。但要注意即使你不关心读回来的数据SPI 事务本身还是全双工的MISO 上会有数据只是驱动不帮你存而已。3.4 批量读写网络包收发时的缓冲区操作W5500 收发网络包的时候数据量会大很多。比如收一个 1500 字节的以太网帧需要从 RX 缓冲区连续读 1500 多个字节。这时候如果还用上面那种每次 4 字节的方式效率太低。W5500 支持连续读发一次地址控制dummy然后连续发 N 个 dummy就能连续读 N 个字节。ESP-IDF 的 SPI 驱动支持一次事务传输任意长度受max_transfer_sz限制所以可以这样写// 从 Socket 0 RX 缓冲区读 len 字节 uint8_t *tx_buf heap_caps_malloc(len 3, MALLOC_CAP_DMA); uint8_t *rx_buf heap_caps_malloc(len 3, MALLOC_CAP_DMA); tx_buf[0] 0x00; tx_buf[1] 0x00; // RX 缓冲区读指针偏移实际由 W5500 内部管理 tx_buf[2] W5500_READ(W5500_BSB_S0_RX); memset(tx_buf 3, 0x00, len); spi_transaction_t t { .length 8 * (len 3), .tx_buffer tx_buf, .rx_buffer rx_buf, }; spi_device_polling_transmit(spi, t); // rx_buf[3] 开始就是网络包数据注意这里用了heap_caps_malloc并指定MALLOC_CAP_DMA因为开了 DMA 之后传输缓冲区必须在 DMA 可访问的内存区域。用普通的malloc在 ESP32 上可能分配到 PSRAM 或者非 DMA 区域导致传输失败。这个坑很隐蔽现象是传输返回错误或者数据全 0。4. 接线、供电和那些看起来没问题的硬件坑4.1 引脚分配SPI 信号线之外还有复位和中断ESP32 和 W5500 之间的连线除了 SPI 四线MOSI、MISO、SCLK、CS还有两根容易被忽略的复位RST和中断INT。ESP32 引脚W5500 引脚说明GPIO11MOSI主机输出从机输入GPIO13MISO主机输入从机输出GPIO12SCLK时钟GPIO10CS片选低有效GPIO9RST复位低有效GPIO8INT中断输出低有效RST 引脚一定要接。W5500 上电后需要一次复位才能进入正常工作状态如果你不接 RST 让它悬空芯片可能处于不确定状态表现为 SPI 读不到正确的版本号。我的做法是上电后先拉低 RST 至少 500 微秒再拉高然后等 1 毫秒让芯片内部稳定再去读 VERSIONR 验证。INT 引脚建议接上虽然不用中断也能跑轮询方式但用中断能大幅降低 CPU 占用。W5500 收到数据、连接建立、连接断开等事件都会拉低 INTESP32 配置一个下降沿中断就能及时响应。4.2 供电3.3V 是底线电流要够W5500 是 3.3V 器件和 ESP32 电平匹配不需要电平转换。但供电电流要注意W5500 在收发数据时峰值电流能到 150mA 左右加上 ESP32 本身的功耗如果用 USB 口供电要确保 USB 口能提供足够电流一般 500mA 以上没问题但劣质线材和 Hub 会拖后腿。我遇到过一次诡异的问题W5500 能读到版本号但一发包就断连。查了半天最后发现是开发板上的 3.3V LDO 带载能力不足W5500 一工作电压就跌到 3.0V 以下。换了个供电方案就好了。所以如果你遇到能初始化但一通信就挂的现象先量一下 W5500 的 VCC 引脚电压。4.3 走线和接触电阻杜邦线是调试利器也是隐患调试阶段用杜邦线连接没问题但要注意两点一是线尽量短超过 15 厘米在 20MHz 以上就容易出问题二是接触要牢杜邦线用久了会松导致时通时断。我有个习惯调 SPI 的时候先用示波器或者逻辑分析仪抓一下 SCLK 和 MOSI 的波形确认时钟频率、模式、数据内容都对。没有逻辑分析仪的话至少用万用表量一下各条线的通断。很多代码问题其实是线没接好。注意W5500 模块上一般有 3.3V 稳压和电平转换电路但不同厂家的模块设计不一样。买之前看清楚模块的输入电压范围有些模块标称 5V 输入板载 LDO有些只支持 3.3V。接错电压会烧芯片。5. 跑通之后才发现的坑从能 ping 通到稳定运行5.1 版本号读不对先查模式和片选如果你读 VERSIONR 得到0x00或0xFF按这个顺序排查SPI 模式确认devcfg.mode 0。片选确认 CS 引脚配置正确且传输时确实被拉低。用逻辑分析仪看 CS 波形最直接。时钟频率降到 1MHz 试试如果低频能读对、高频读不对就是信号完整性问题。接线MOSI 和 MISO 有没有接反这两个接反了现象就是读不到数据。复位确认 RST 时序正确芯片已经退出复位状态。我见过最离谱的一次是 MOSI 和 MISO 接反了但因为是全双工发出去的数据在 MISO 上被自己读回来了看起来有数据但值完全不对。这种时候对照数据手册的预期值一看就露馅了。5.2 ping 时断时续SPI 事务被抢占的典型症状W5500 跑起来之后如果 ping 的时候时通时断大概率是 SPI 事务被高优先级任务或中断打断了。前面说过W5500 要求一次事务里 CS 保持低电平如果中间被拉高这次读写就废了。解决办法有两个一是用硬件片选让 SPI 外设保证 CS 时序二是把 W5500 的 SPI 操作放在一个独立任务里给足够高的优先级并且在事务前后加互斥锁防止其他任务插进来。// 用互斥锁保护 SPI 事务 static SemaphoreHandle_t spi_mutex; void w5500_spi_transaction(uint8_t *tx, uint8_t *rx, size_t len) { xSemaphoreTake(spi_mutex, portMAX_DELAY); spi_transaction_t t { .length 8 * len, .tx_buffer tx, .rx_buffer rx, }; spi_device_polling_transmit(spi, t); xSemaphoreGive(spi_mutex); }5.3 长时间运行后连不上W5500 的 socket 状态机要维护W5500 内部有 8 个 socket每个 socket 有独立的状态机。如果程序里没有正确处理 socket 的关闭和重连跑几天之后可能出现 socket 卡在某个状态、无法再建立连接的情况。我的做法是在应用层维护一个 socket 状态检查定期读取Sn_SR寄存器如果发现 socket 处于异常状态比如SOCK_CLOSED但应用层以为还连着就主动执行close再重新open。另外W5500 的Sn_IR中断寄存器要记得读并清除否则中断会一直触发或者丢失。5.4 字节序和地址对齐读回来的数据差一位W5500 的寄存器是 8 位宽的但网络包里的多字节字段比如 IP 地址、端口号是网络字节序大端。如果你在应用层直接把这些字节拼成uint16_t或uint32_t要注意 ESP32 是小端需要手动转换。// 从 W5500 读回的端口号大端转成主机字节序 uint16_t port (rx_buf[0] 8) | rx_buf[1];这个转换如果忘了做现象就是连接能建立但端口号不对或者数据解析出来是乱的。网络编程里字节序是永恒的话题SPI 读回来的数据只是第一步后面还有一层转换。6. 一份可以直接抄的初始化代码框架6.1 从复位到验证版本号的完整流程把前面所有点串起来一个可靠的 W5500 初始化流程是这样的void w5500_init(void) { // 1. 硬件复位 gpio_set_level(W5500_RST_GPIO, 0); vTaskDelay(pdMS_TO_TICKS(10)); gpio_set_level(W5500_RST_GPIO, 1); vTaskDelay(pdMS_TO_TICKS(50)); // 2. SPI 总线初始化 spi_bus_config_t buscfg { .mosi_io_num W5500_MOSI_GPIO, .miso_io_num W5500_MISO_GPIO, .sclk_io_num W5500_SCLK_GPIO, .quadwp_io_num -1, .quadhd_io_num -1, .max_transfer_sz 4096, }; ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, buscfg, SPI_DMA_CH_AUTO)); // 3. 添加 W5500 设备 spi_device_interface_config_t devcfg { .clock_speed_hz 20 * 1000 * 1000, .mode 0, .spics_io_num W5500_CS_GPIO, .queue_size 7, }; ESP_ERROR_CHECK(spi_bus_add_device(SPI2_HOST, devcfg, w5500_spi)); // 4. 验证版本号 uint8_t ver w5500_read_version(); if (ver ! 0x04) { ESP_LOGE(TAG, W5500 version mismatch: 0x%02X, ver); return; } ESP_LOGI(TAG, W5500 detected, version 0x%02X, ver); // 5. 后续配置网络参数、打开 socket w5500_config_network(); }6.2 网络参数配置IP、掩码、网关、MAC版本号验证通过后下一步是配置网络参数。W5500 的通用寄存器区块里有GAR网关地址、SUBR子网掩码、SHAR本机 MAC、SIPR本机 IP这几个寄存器每个都是 4 字节或 6 字节。void w5500_config_network(void) { uint8_t mac[6] {0x02, 0x00, 0x00, 0x12, 0x34, 0x56}; uint8_t ip[4] {192, 168, 1, 100}; uint8_t sub[4] {255, 255, 255, 0}; uint8_t gw[4] {192, 168, 1, 1}; w5500_write(W5500_BSB_COMMON, 0x0009, mac, 6); // SHAR w5500_write(W5500_BSB_COMMON, 0x000F, ip, 4); // SIPR w5500_write(W5500_BSB_COMMON, 0x0005, sub, 4); // SUBR w5500_write(W5500_BSB_COMMON, 0x0001, gw, 4); // GAR }MAC 地址建议用02开头这是本地管理地址不会和真实厂商的 MAC 冲突。IP 要和你所在网段匹配否则 ping 不通。6.3 打开一个 TCP 服务端 socket配置完网络参数就可以打开 socket 了。以 TCP 服务端为例void w5500_open_tcp_server(uint8_t sock, uint16_t port) { // 设置 socket 为 TCP 模式 w5500_write_byte(W5500_BSB_S0_REG sock, 0x0014, 0x01); // Sn_MR TCP // 设置本地端口 uint8_t p[2] {port 8, port 0xFF}; w5500_write(W5500_BSB_S0_REG sock, 0x0004, p, 2); // Sn_PORT // 执行 OPEN 命令 w5500_write_byte(W5500_BSB_S0_REG sock, 0x0001, 0x01); // Sn_CR OPEN // 等待命令完成 while (w5500_read_byte(W5500_BSB_S0_REG sock, 0x0001)); // 执行 LISTEN 命令 w5500_write_byte(W5500_BSB_S0_REG sock, 0x0001, 0x02); // Sn_CR LISTEN while (w5500_read_byte(W5500_BSB_S0_REG sock, 0x0001)); }这里Sn_CR命令寄存器写入命令后W5500 会执行执行完自动清零。所以等待它变回 0 就表示命令完成。这个轮询等待是必要的不能写完命令就往下走。7. 调试工具和验证手段别靠猜靠看7.1 逻辑分析仪SPI 调试的眼睛调 SPI 最有效的工具是逻辑分析仪。几十块钱的 8 通道 USB 逻辑分析仪就够用接上 SCLK、MOSI、MISO、CS 四根线抓一次读 VERSIONR 的波形所有问题一目了然时钟频率对不对、模式对不对、CS 有没有在传输期间保持低、MOSI 上发出去的地址和控制字节对不对、MISO 上回来的数据对不对。我调 W5500 的时候第一次抓波形就发现 CS 在传输中间被拉高了原因是软件片选被一个高优先级中断打断。换成硬件片选之后问题消失。如果没有逻辑分析仪这个问题可能要查很久。7.2 ping 和 socket 工具验证网络层SPI 层通了之后网络层的验证就简单了。给 W5500 配好 IP从同一网段的电脑上 ping 它。能 ping 通说明 ARP、IP、ICMP 都正常。然后可以用网络调试助手比如 TCP 客户端工具连接 W5500 打开的端口测试 TCP 收发。如果 ping 不通先确认电脑和 W5500 在同一网段、子网掩码一致。然后检查 W5500 的PHYCFGR寄存器确认 PHY 链路已经建立bit 0 为 1 表示 link up。网线也要确认是好的换一根试试。7.3 常见问题速查表现象可能原因排查方向读 VERSIONR 为 0x00/0xFF模式错、片选错、接线错查 mode、CS 波形、MOSI/MISO 是否接反能读版本号但 ping 不通网络参数错、PHY 未 link查 IP/掩码/网关、PHYCFGR 寄存器ping 时断时续SPI 事务被抢占改硬件片选、加互斥锁跑几天后连不上socket 状态机异常定期检查 Sn_SR、主动重连数据解析乱码字节序未转换检查大端小端转换传输返回错误DMA 缓冲区不在 DMA 内存用 heap_caps_malloc 指定 MALLOC_CAP_DMA这张表是我自己踩坑总结出来的基本上覆盖了 90% 的常见问题。遇到问题先对号入座能省很多时间。8. 几个容易被忽略但很关键的细节8.1 SPI 事务的 length 字段单位是比特这个前面提过但值得再强调一次。spi_transaction_t.length的单位是比特不是字节。发 4 个字节要填 32。我见过不止一个人在这里栽跟头现象是读回来的数据只有第一个字节对后面全是 0 或者垃圾。8.2 W5500 的 SPI 事务之间需要间隔吗严格来说不需要W5500 支持连续事务。但实际调试中发现如果两次事务之间没有任何间隔偶尔会出现第二次事务读回的数据是上一次的。这可能是 SPI 外设的流水线效应。我的做法是在关键事务之间加一个微秒级的esp_rom_delay_us(1)代价可以忽略稳定性提升明显。8.3 中断引脚的电平保持W5500 的 INT 引脚是低有效而且在中断寄存器被读取并清除之前它会一直保持低电平。这意味着如果你用中断方式在中断服务程序里必须读取Sn_IR或IR寄存器并写 1 清除对应位否则中断会反复触发CPU 被拖死。这个坑我在早期项目里踩过现象是系统跑着跑着就卡死了查了半天才发现是中断没清。8.4 多 socket 并发时的资源竞争W5500 有 8 个 socket但 SPI 总线只有一条。如果多个 socket 同时有数据要收发SPI 事务必须串行化。这时候互斥锁就很重要了。另外W5500 的 TX/RX 缓冲区是每个 socket 独立的但总容量有限一般 16KB 或 32KB8 个 socket 平分的话每个只有 2KB 到 4KB。如果某个 socket 要发大包需要调整缓冲区分配。8.5 固件升级时的 SPI 冲突如果你的 ESP32 项目同时用了外部 Flash走 SPI0/SPI1和 W5500走 SPI2一般不会有冲突因为 ESP-IDF 把 Flash 的 SPI 和通用 SPI 分得很清楚。但如果你用了 SPI1它和 Flash 共用引脚就会出问题。所以 W5500 一定要挂在 SPI2 或 SPI3 上别碰 SPI0 和 SPI1。9. 从能跑到好用稳定性优化的几个实践9.1 降低 SPI 时钟频率换稳定性20MHz 是个平衡点但如果你的板子走线不好或者用杜邦线可以降到 10MHz 甚至 5MHz。W5500 的吞吐量对大多数物联网应用来说10MHz 已经绰绰有余理论上 10MHz SPI 能跑满 100Mbps 以太网的大部分场景。别为了追求高频牺牲稳定性。9.2 用中断代替轮询处理网络事件轮询方式下CPU 要不断读 W5500 的状态寄存器效率低。用 INT 引脚加中断W5500 有事件时才通知 ESP32CPU 占用能降一个数量级。中断服务程序里只做标记实际处理放到任务里做这是嵌入式里的标准做法。9.3 定期健康检查长时间运行的项目建议加一个定时任务每隔几十秒检查一次 W5500 的 PHY 链路状态和 socket 状态。发现异常就主动复位 W5500 并重新初始化。这个看门狗机制能大幅提升长期运行的可靠性。我自己做的几个现场设备加了健康检查之后连续运行几个月都不用重启。9.4 日志要分级别在中断里打印ESP-IDF 的日志系统很方便但要注意绝对不要在中断服务程序里调用 ESP_LOGI 或 printf因为日志输出可能涉及锁和阻塞在中断上下文里会出问题。中断里只做标记日志放到任务里打。另外量产固件里建议把日志级别调到 WARN 或 ERROR减少不必要的输出开销。调 W5500 这件事说到底就是 SPI 时序、寄存器操作、网络协议三层知识的叠加。SPI 层把模式、片选、时钟搞对寄存器层把地址、控制字节、读写流程搞对网络层把 IP、端口、socket 状态机搞对剩下的就是耐心调试。我自己的经验是第一次调通可能要花一两天但把上面这些坑都踩过一遍之后再调第二块板子半小时就能跑起来。硬件调试没有捷径但有方法——逻辑分析仪加数据手册比盲目改代码有效得多。