
1. 为什么我劝你先搞懂 SPI 再碰 W5500很多人拿到 W5500 模块的第一反应是找现成库插上线跑通 ping 就完事。但真到了产品阶段遇到丢包、断连、初始化失败翻遍例程也找不到原因。根子往往不在 W5500而在 ESP32 的 SPI 配置上。SPI 是 W5500 和 ESP32 之间唯一的通信通道这条通道的时序、模式、片选、时钟频率只要有一项对不上W5500 就是一块砖。我见过太多人把 W5500 当 UART 设备用以为接上 TX/RX 就能通。W5500 是纯 SPI 从设备没有 SPI 时钟它连寄存器都读不出来。所以这篇内容的核心思路很明确先把 ESP32 的 SPI 主机驱动吃透再把 W5500 的寄存器操作逻辑讲清楚最后把两者拼成一条完整的有线以太网链路。适合已经会点 Arduino、想往 ESP-IDF 迁移的开发者也适合做过 STM32 以太网、想换 ESP32 平台的硬件工程师。整篇内容围绕 ESP-IDF 的spi_master驱动展开配合 W5500 的 socket 编程最终实现 DHCP 获取 IP、TCP 客户端连接、数据收发。所有代码都可以直接抄但抄之前建议先看懂每一行为什么这么写。2. SPI 协议核心机制与 ESP32 主机驱动拆解2.1 SPI 四线制到底在传什么SPI 全称 Serial Peripheral Interface中文叫串行外设接口。它一共四根线SCLK、MOSI、MISO、CS。SCLK 是时钟由主机产生MOSI 是主机输出从机输入MISO 是主机输入从机输出CS 是片选低电平有效。通信过程可以类比成两个人隔着门递纸条。SCLK 就是敲门节奏每敲一下双方同时交换一位数据。主机把一位放到 MOSI 上从机把一位放到 MISO 上时钟边沿一到双方各自采样。所以 SPI 是全双工的发和收同时进行。这里有个关键点SPI 没有应答机制。主机发了数据从机收没收到、收对没有SPI 协议本身不保证。W5500 靠的是寄存器读写回读来确认这是后话。2.2 CPOL 和 CPHA 决定了采样时刻SPI 有四种模式由 CPOL时钟极性和 CPHA时钟相位组合而成。模式CPOLCPHA空闲时钟采样边沿Mode 000低电平第一个边沿上升Mode 101低电平第二个边沿下降Mode 210高电平第一个边沿下降Mode 311高电平第二个边沿上升W5500 支持 Mode 0 和 Mode 3数据手册里写的是 CPOL0/CPHA0 或 CPOL1/CPHA1。我一般用 Mode 0因为大多数例程和调试工具默认就是 Mode 0排查问题方便。如果你模式选错了现象是读出来的数据全是 0xFF 或者 0x00偶尔蹦出几个看似有规律的值。这时候别怀疑硬件先查模式。2.3 ESP32 的 SPI 主机驱动结构ESP-IDF 把 SPI 主机驱动分成了三层总线初始化、设备添加、事务传输。总线初始化用spi_bus_initialize它负责配置 SCLK、MOSI、MISO 三根共用线以及 DMA 通道。ESP32 有两个 SPI 主机控制器可用SPI2 和 SPI3SPI1 被 Flash 占了。我一般用 SPI2也就是 HSPI。设备添加用spi_bus_add_device它负责配置 CS 引脚、时钟频率、SPI 模式、队列深度。每个从设备一个句柄互不干扰。事务传输用spi_device_transmit或spi_device_polling_transmit。前者走中断和队列适合多设备并发后者是轮询适合单设备高频操作。W5500 这种独占总线的场景轮询就够用还省中断开销。spi_bus_config_t buscfg { .miso_io_num PIN_MISO, .mosi_io_num PIN_MOSI, .sclk_io_num PIN_SCLK, .quadwp_io_num -1, .quadhd_io_num -1, .max_transfer_sz 4096, }; spi_device_interface_config_t devcfg { .clock_speed_hz 20 * 1000 * 1000, .mode 0, .spics_io_num PIN_CS, .queue_size 1, };这段配置里max_transfer_sz设成 4096 是因为 W5500 的 socket 缓冲区一次最多传 2KB留点余量。clock_speed_hz设 20MHz 是 W5500 的极限实际跑 10MHz 更稳。2.4 硬件片选和软件片选的区别ESP-IDF 的 SPI 驱动默认用硬件片选也就是spics_io_num指定一个 GPIO驱动在传输前后自动拉低拉高。这种方式时序精准不占 CPU。但有些场景需要软件片选比如多个 W5500 共用总线或者 CS 引脚被其他功能复用了。这时候把spics_io_num设成 -1自己在传输前手动拉低 GPIO传输后拉高。gpio_set_level(PIN_CS, 0); spi_device_polling_transmit(spi, trans); gpio_set_level(PIN_CS, 1);软件片选的坑在于如果传输过程中被高优先级任务打断CS 可能被拉高又拉低从设备会误判帧边界。所以软件片选一定要配合临界区或者互斥锁。注意W5500 的 CS 下降沿到第一个 SCLK 上升沿之间需要至少 10ns 的建立时间ESP32 的硬件片选自动满足软件片选要加ets_delay_us(1)保险。3. W5500 寄存器模型与 SPI 帧格式3.1 W5500 的地址空间怎么划分W5500 内部有 8 个 socket每个 socket 有独立的寄存器和收发缓冲区。地址空间按块划分区域地址范围说明通用寄存器0x0000 - 0x003F模式、中断、PHY 状态Socket 寄存器0x0000 - 0x002F每个 socket 独立偏移不同TX 缓冲区0x0000 - 0x1FFF每个 socket 2KBRX 缓冲区0x0000 - 0x1FFF每个 socket 2KB访问不同区域靠控制字节里的 BSB 位来切换。BSB 是 Block Select Bits占 5 位决定当前操作哪个块。3.2 一帧 SPI 数据的完整结构W5500 的 SPI 帧分三个阶段地址阶段、控制阶段、数据阶段。地址阶段2 字节大端序表示要访问的偏移地址。 控制阶段1 字节包含 BSB、读写标志、SPI 模式。 数据阶段N 字节实际读写的值。控制字节的位定义位名称说明7RWB1 读0 写6-3BSB块选择2RWB2固定 01-0OM00 可变长度01 固定 1 字节10 固定 2 字节11 固定 4 字节我一般用可变长度模式OM00这样一次事务可以读写任意字节数。// 读 W5500 寄存器 uint8_t addr_phase[2] {addr 8, addr 0xFF}; uint8_t ctrl_phase (bsb 3) | 0x01; // 读操作 spi_transaction_t trans { .length 3 * 8, .tx_buffer tx_buf, .rx_buffer rx_buf, };这里tx_buf是{addr_high, addr_low, ctrl, 0x00}rx_buf第 4 字节才是真正的数据。前 3 字节是地址和控制阶段从机不回数据。3.3 读写时序的实测波形分析我用逻辑分析仪抓过 W5500 的 SPI 波形读通用寄存器MR地址 0x0000的过程是这样的CS 拉低SCLK 开始。前 16 个时钟周期MOSI 上是0x00 0x00MISO 上是0x00 0x00。第 17 到 24 个时钟周期MOSI 上是0x01BSB0读操作MISO 上是0x00。第 25 到 32 个时钟周期MOSI 上是0x00MISO 上是0x08这就是 MR 寄存器的值表示 W5500 处于可变长度模式。整个过程中MISO 在前 24 个周期都是无效的因为 W5500 还没解析完地址和控制字节。所以读数据时接收缓冲区的前 3 字节要丢弃。提示如果你用 DMA 接收注意rx_buffer的长度要包含地址和控制阶段否则数据会错位。3.4 缓冲区读写和普通寄存器的区别普通寄存器一次读写 1 字节缓冲区一次读写 N 字节。W5500 的 TX/RX 缓冲区地址是循环的读写指针由 W5500 内部维护。写 TX 缓冲区时先写Sn_TX_WR寄存器设置写指针然后往 TX 缓冲区写数据最后更新Sn_TX_WR。读 RX 缓冲区时先读Sn_RX_RSR看有多少数据然后读 RX 缓冲区最后更新Sn_RX_RD。这里有个坑Sn_TX_WR和Sn_RX_RD是 16 位寄存器读写要分两次而且中间不能被其他 SPI 事务打断。我一般把这两个寄存器的读写封装成原子操作用互斥锁保护。4. 从零搭建 ESP32 W5500 有线以太网4.1 硬件接线和电源注意事项W5500 模块和 ESP32 的接线W5500ESP32说明VCC3.3V必须 3.3V5V 会烧GNDGND共地SCLKGPIO14SPI 时钟MOSIGPIO13主机输出MISOGPIO12主机输入CSGPIO15片选INTGPIO4中断可选RSTGPIO5复位可选电源是第一个坑。W5500 峰值电流能到 150mAESP32 的 3.3V LDO 如果只给 300mA加上 WiFi 一开就掉电。我建议 W5500 单独用一个 AMS1117-3.3 供电或者至少加个 100uF 电解电容在模块 VCC 和 GND 之间。第二个坑是网口变压器。很多便宜模块省了变压器直接用电容耦合短距离能通长距离丢包严重。买模块时看清楚有没有 HR911105A 或者类似型号的集成变压器。4.2 ESP-IDF 工程配置和 SPI 初始化新建工程用idf.py create-project w5500_demo然后在menuconfig里确认 SPI 主机驱动已经使能。idf.py menuconfig # Component config - ESP32-specific - SPI master driver 使能SPI 初始化代码#include driver/spi_master.h #define PIN_SCLK 14 #define PIN_MOSI 13 #define PIN_MISO 12 #define PIN_CS 15 spi_device_handle_t spi; void spi_init(void) { spi_bus_config_t buscfg { .miso_io_num PIN_MISO, .mosi_io_num PIN_MOSI, .sclk_io_num PIN_SCLK, .quadwp_io_num -1, .quadhd_io_num -1, .max_transfer_sz 4096, }; spi_device_interface_config_t devcfg { .clock_speed_hz 10 * 1000 * 1000, .mode 0, .spics_io_num PIN_CS, .queue_size 1, }; ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, buscfg, SPI_DMA_CH_AUTO)); ESP_ERROR_CHECK(spi_bus_add_device(SPI2_HOST, devcfg, spi)); }SPI_DMA_CH_AUTO让驱动自动选 DMA 通道省心。如果你同时用 SPI3 接其他设备记得手动指定不同通道。4.3 W5500 复位和初始化流程W5500 上电后需要复位把MR寄存器的 RST 位置 1等 1ms再清 0。void w5500_reset(void) { w5500_write(MR, 0x80); // 软件复位 vTaskDelay(pdMS_TO_TICKS(10)); w5500_write(MR, 0x00); // 清除复位 vTaskDelay(pdMS_TO_TICKS(10)); }复位后检查VERSIONR寄存器正常值应该是 0x04。如果读出来是 0x00 或 0xFF说明 SPI 通信有问题回去查模式、时钟、接线。初始化网络参数void w5500_init_network(void) { w5500_write(SHAR, mac); // 设置 MAC w5500_write(SUBR, subnet); // 设置子网掩码 w5500_write(GAR, gateway); // 设置网关 w5500_write(SIPR, ip); // 设置静态 IP w5500_write(RTR, 0x07D0); // 重试超时 200ms w5500_write(RCR, 0x08); // 重试次数 8 }RTR和RCR决定了 TCP 重传行为。RTR0x07D0 是 2000 个 100us也就是 200ms。RCR8 表示重试 8 次。这两个值根据网络质量调整局域网可以设小一点广域网设大一点。4.4 DHCP 获取 IP 的完整实现DHCP 流程分四步DISCOVER、OFFER、REQUEST、ACK。W5500 硬件不自动处理 DHCP需要软件实现。我一般用 W5500 官方 ioLibrary 里的dhcp.c但那个库比较老我把它移植到了 ESP-IDF 的 SPI 驱动上。核心逻辑是打开 socket 0设为 UDP 模式本地端口 68目标端口 67。构造 DHCP DISCOVER 包广播发送。等待 OFFER解析分配的 IP、网关、DNS。发送 REQUEST等待 ACK。把获取到的参数写入 W5500 寄存器。void dhcp_task(void *pvParameters) { uint8_t dhcp_buf[548]; while (1) { if (dhcp_run(dhcp_buf) DHCP_SUCCESS) { w5500_write(SIPR, dhcp_buf 16); w5500_write(GAR, dhcp_buf 24); w5500_write(SUBR, dhcp_buf 28); break; } vTaskDelay(pdMS_TO_TICKS(1000)); } vTaskDelete(NULL); }DHCP 超时时间我设 10 秒超过就回退到静态 IP。产品里最好保留静态 IP 作为 fallback不然路由器挂了设备就失联。5. TCP 客户端与服务器实战5.1 Socket 编程模型和 W5500 的差异W5500 的 socket 编程和 BSD socket 很像但有几个关键差异没有listen和accept的分离服务器模式直接设Sn_MR为 TCP 并写Sn_PORT然后执行LISTEN命令。连接状态靠读Sn_SR寄存器判断不是靠返回值。发送数据前要检查Sn_TX_FSR是否有足够空间接收数据前要检查Sn_RX_RSR。uint8_t socket_status(uint8_t sn) { return w5500_read(Sn_SR(sn)); }Sn_SR的常见值0x00 关闭0x13 初始化0x17 已连接0x1C 已关闭0x14 监听中。5.2 TCP 客户端连接和断线重连客户端流程打开 socket设 TCP 模式设目标 IP 和端口执行 CONNECT 命令等待Sn_SR变成 0x17。bool tcp_connect(uint8_t sn, uint8_t *ip, uint16_t port) { w5500_write(Sn_MR(sn), SOCK_TCP); w5500_write(Sn_DIPR(sn), ip); w5500_write(Sn_DPORT(sn), port); w5500_write(Sn_CR(sn), SOCK_CONNECT); for (int i 0; i 100; i) { if (w5500_read(Sn_SR(sn)) SOCK_ESTABLISHED) return true; vTaskDelay(pdMS_TO_TICKS(10)); } return false; }断线重连是产品必须做的。我的做法是开一个监控任务每 5 秒检查一次Sn_SR如果不是 0x17 就重新连接。重连前先执行CLOSE命令等Sn_SR变成 0x00 再重新打开。注意W5500 的 socket 关闭后需要至少 1ms 才能重新打开太快会失败。我在CLOSE后加vTaskDelay(pdMS_TO_TICKS(5))。5.3 数据收发缓冲区的管理发送数据时先读Sn_TX_FSR看剩余空间如果不够就等。然后写Sn_TX_WR往 TX 缓冲区写数据最后更新Sn_TX_WR并执行SEND命令。uint16_t tcp_send(uint8_t sn, uint8_t *buf, uint16_t len) { uint16_t freesize w5500_read16(Sn_TX_FSR(sn)); if (freesize len) return 0; uint16_t ptr w5500_read16(Sn_TX_WR(sn)); w5500_write_buf(sn, TXBUF, ptr, buf, len); w5500_write16(Sn_TX_WR(sn), ptr len); w5500_write(Sn_CR(sn), SOCK_SEND); while (w5500_read(Sn_CR(sn)) ! 0); return len; }接收数据时先读Sn_RX_RSR看有多少数据然后读Sn_RX_RD从 RX 缓冲区读数据最后更新Sn_RX_RD并执行RECV命令。这里有个细节Sn_RX_RD更新后必须执行RECV否则 W5500 不会释放缓冲区空间下次读Sn_RX_RSR还是旧值。5.4 实测性能数据和优化建议我用 ESP32-WROOM-32 加 W5500 模块实测SPI 时钟 10MHzTCP 发送 1MB 数据耗时约 1.2 秒平均速率 830KB/s。SPI 时钟提到 20MHz速率能到 1.1MB/s但丢包率上升。优化建议用 DMA 传输减少 CPU 占用。发送大块数据时分片大小设成 1460 字节避免 IP 分片。接收缓冲区设成 2KB 以上减少中断频率。如果同时用 WiFi注意 SPI 和 WiFi 的中断优先级冲突把 SPI 中断优先级设低一点。6. 常见问题排查与避坑指南6.1 初始化失败和寄存器读不出来的排查现象读VERSIONR返回 0x00 或 0xFF。排查顺序用万用表量 W5500 的 VCC 和 GND确认 3.3V 稳定。用逻辑分析仪抓 SCLK、MOSI、CS确认有波形。检查 SPI 模式W5500 只支持 Mode 0 和 Mode 3。检查 CS 是否接对有些模块丝印标反了。降低 SPI 时钟到 1MHz 试试排除时序问题。我遇到过最坑的一次是模块上的 CS 和 INT 丝印互换查了两天才发现。6.2 能 ping 通但 TCP 连不上的原因现象W5500 获取到 IP能 ping 通但 TCP 连接超时。原因通常是 socket 没正确打开或者目标端口不对。检查Sn_SR是否变成 0x17如果没有看Sn_CR执行后有没有清零。W5500 的命令寄存器执行完会自动清零如果没清零说明 SPI 写失败。另一个原因是防火墙。有些路由器默认禁止内网设备互访换个路由器或者关掉 AP 隔离。6.3 长时间运行后断连的解决办法现象跑几个小时或几天后W5500 连不上ping 断断续续。这是 W5500 的经典问题根因是 socket 缓冲区溢出或者 PHY 状态异常。解决办法定期读PHYCFGR寄存器检查链路状态。加看门狗检测到Sn_SR异常就复位 W5500。在RTR和RCR之外还要设Sn_KPALVTR保活定时器TCP 空闲时自动发保活包。w5500_write(Sn_KPALVTR(sn), 5); // 5 秒保活保活包能防止 NAT 表项老化对长连接场景很关键。6.4 常见问题速查表现象可能原因解决方法VERSIONR 读 0x00SPI 模式错改 Mode 0VERSIONR 读 0xFFMISO 没接或 CS 没拉低检查接线DHCP 获取不到 IP网线没插或路由器没开 DHCP换网线检查路由器TCP 连接超时socket 没打开或端口错读 Sn_SR 确认状态发送数据丢包TX 缓冲区满检查 Sn_TX_FSR接收数据丢包RX 缓冲区溢出加快读取频率运行几小时后断连PHY 异常或 NAT 老化加保活和看门狗SPI 时钟 20MHz 不稳定线太长或干扰降到 10MHz加屏蔽6.5 我踩过的三个坑第一个坑用 ESP32 的 3.3V 给 W5500 供电WiFi 一开 W5500 就复位。后来单独加了一个 LDO问题消失。第二个坑SPI 时钟设 20MHz短时间正常跑一天后开始丢包。降到 10MHz 后稳定运行一个月。第三个坑忘记设Sn_KPALVTRTCP 连接空闲 5 分钟就被路由器断开。加上保活后连接能维持 24 小时以上。7. 从例程到产品的进阶思路例程跑通只是第一步。产品化要考虑的东西更多OTA 升级、配置存储、异常日志、多 socket 并发。我的建议是先把单 socket 的 TCP 客户端做稳再加第二个 socket 做服务器最后加 UDP 做广播发现。W5500 的 8 个 socket 可以同时用但 SPI 总线是共享的高并发时要注意互斥。我一般给每个 socket 分配独立的发送任务用队列传递数据SPI 访问用互斥锁保护。如果你要做 Modbus TCP 网关W5500 加 ESP32 是很合适的组合。Modbus TCP 的报文格式简单W5500 的硬件校验和能省不少 CPU。Freemodbus 有现成的 W5500 移植但要注意它的 SPI 驱动和 ESP-IDF 的兼容性我改过一版主要是把port.c里的 SPI 读写换成 ESP-IDF 的spi_device_polling_transmit。最后分享一个小技巧调试 W5500 时把Sn_TX_FSR和Sn_RX_RSR的值定期打印出来能提前发现缓冲区泄漏。我靠这个技巧定位过一次内存泄漏原因是RECV命令没执行RX 缓冲区一直不释放。