ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ESP32 SPI驱动W5500以太网实战:从原理到代码

ESP32 SPI驱动W5500以太网实战:从原理到代码 1. 为什么我劝你先把 SPI 搞明白再碰 W5500很多人拿到 W5500 模块的第一反应是找现成库Ethernet.begin()一调能 ping 通就收工。但真到了产品阶段遇到丢包、断连、初始化失败翻遍例程也找不到问题在哪。根子往往不在 W5500 本身而在 SPI——这个看起来只有四根线的协议藏着不少细节。我接触 ESP32 加 W5500 这套组合最初是为了给一个工业采集终端做有线网络备份。无线在车间里被金属屏蔽得厉害有线才是保底方案。当时踩的坑从片选信号接错到 SPI 时钟频率过高导致数据错位再到 DMA 缓冲区和 W5500 的 32KB 窗口对不上前后折腾了快两周。后来我把 ESP-IDF 里 SPI 主机驱动的例程逐行拆开看了一遍才真正理解每一行代码在干什么。这篇内容就是把这套经验整理出来。核心讲三件事ESP32 的 SPI 主机驱动到底怎么用、W5500 的寄存器操作和 SPI 传输怎么配合、以及一套可以直接复现的完整代码。适合已经会点 ESP32、但被 SPI 和以太网卡住的开发者也适合想从 Arduino 框架转到 ESP-IDF 的人。全程基于 ESP-IDF 的spi_master驱动不依赖第三方以太网库每一行都讲清楚为什么这么写。2. ESP32 SPI 主机驱动核心机制拆解2.1 SPI 四种模式与 W5500 的匹配逻辑SPI 协议有两个关键参数时钟极性CPOL和时钟相位CPHA组合出四种模式。W5500 的数据手册明确写了它支持SPI 模式 0CPOL0, CPHA0也就是时钟空闲为低电平数据在时钟上升沿采样。为什么这个必须确认因为如果模式选错你读回来的寄存器值会整体偏移一位表现为读到的版本号不是0x04而是一些乱七八糟的数。我见过有人把模式设成 3结果 W5500 死活初始化不了查了半天以为是硬件虚焊。在 ESP-IDF 里模式通过spi_device_interface_config_t的mode字段设置spi_device_interface_config_t devcfg { .mode 0, // W5500 固定用模式 0 .clock_speed_hz 20 * 1000 * 1000, // 先给 20MHz稳定后再调 .spics_io_num PIN_CS, .queue_size 7, };这里有个经验初次调试时把时钟降到 1MHz 甚至更低。W5500 标称支持 80MHz但实际能不能跑上去取决于你的 PCB 走线、杜邦线长度、电源质量。我一开始用杜邦线连20MHz 就偶尔出错降到 8MHz 才稳。后来打板走线短了才敢上 30MHz。2.2 事务Transaction机制ESP32 SPI 驱动的灵魂ESP-IDF 的 SPI 主机驱动和 STM32 的 HAL 库思路完全不同。STM32 那边你调HAL_SPI_TransmitReceive()传缓冲区就完事。ESP32 这边核心是spi_transaction_t结构体一次传输叫一个事务。为什么这么设计因为 ESP32 的 SPI 外设支持队列和中断/DMA事务机制让驱动可以在后台把多个传输排好队CPU 不用干等。对 W5500 这种需要发地址→读数据两段式操作的设备事务机制反而更灵活。一个典型的事务长这样spi_transaction_t t { .length 8 * 3, // 3 字节单位是 bit .tx_buffer send_buf, .rx_buffer recv_buf, }; spi_device_transmit(spi_handle, t);注意length的单位是bit不是 byte。这是新手最容易搞错的地方。我第一次写的时候填了 3结果只发了 3 个 bitW5500 收到的地址完全错位。2.3 片选信号的硬件与软件之争W5500 的片选CS有两种控制方式硬件片选由 SPI 外设自动拉低拉高配置spics_io_num即可。软件片选把 CS 当普通 GPIO手动控制。ESP-IDF 默认用硬件片选一次事务开始时自动拉低结束时拉高。这看起来很方便但有个坑W5500 要求 CS 在每段操作之间保持低电平。比如读一个寄存器需要先发地址字节再读数据字节这两段之间 CS 不能抬高否则 W5500 会认为一次操作结束地址失效。解决办法是把发地址读数据合并成一个事务或者用软件片选手动控制。我推荐前者代码更干净// 合并成一个事务3字节发送地址控制1字节接收 uint8_t tx[4] {addr_high, addr_low, control, 0x00}; uint8_t rx[4] {0}; spi_transaction_t t { .length 8 * 4, .tx_buffer tx, .rx_buffer rx, }; spi_device_transmit(spi_handle, t); // rx[3] 就是读到的数据注意W5500 的地址阶段是 2 字节地址加 1 字节控制共 3 字节然后才是数据阶段。很多人只发 1 字节地址那是老款 W5100 的用法W5500 必须发 2 字节。3. W5500 寄存器操作与 SPI 传输的配合细节3.1 W5500 的地址映射为什么是 2 字节地址加 1 字节控制W5500 内部有 32KB 的收发缓冲区加上寄存器区总共需要 16 位地址来寻址。所以地址阶段是 2 字节。第 3 字节是控制字节格式如下位名称含义B7RWB1读0写B6-B5BSB块选择选寄存器区还是哪个 socket 的缓冲区B4-B0保留固定为 0BSB 的取值很关键00000通用寄存器区00001Socket 0 寄存器区00010Socket 0 发送缓冲区00011Socket 0 接收缓冲区以此类推每个 socket 占 4 个块我见过有人读写 socket 寄存器时 BSB 填错结果操作到了别的 socket 上现象是 socket 0 能通、socket 1 死活不通。查这种问题只能靠打印每个寄存器的值逐个比对。3.2 读写时序的完整拆解以读 W5500 版本号寄存器地址0x0039为例完整时序是CS 拉低发送地址高字节0x00发送地址低字节0x39发送控制字节0x01读操作BSB00000接收 1 字节数据应该是0x04CS 拉高用 ESP-IDF 的事务实现uint8_t w5500_read(uint16_t addr, uint8_t bsb, uint8_t *data, size_t len) { uint8_t tx_buf[3 len]; uint8_t rx_buf[3 len]; tx_buf[0] (addr 8) 0xFF; tx_buf[1] addr 0xFF; tx_buf[2] (bsb 3) | 0x01; // 读操作 memset(tx_buf 3, 0x00, len); spi_transaction_t t { .length 8 * (3 len), .tx_buffer tx_buf, .rx_buffer rx_buf, }; spi_device_transmit(spi_handle, t); memcpy(data, rx_buf 3, len); return 0; }写操作类似只是控制字节的 RWB 位为 0且数据阶段发送实际数据。3.3 缓冲区读写一次事务最多传多少W5500 的 SPI 帧没有长度限制理论上可以一次传完整个 socket 缓冲区默认 2KB。但实际中要考虑两个因素ESP32 的 DMA 描述符限制单次事务超过 4092 字节会出问题需要分片。内存占用在栈上开 2KB 的 tx/rx 缓冲区容易爆栈建议用heap_caps_malloc分配。我的做法是每次最多传 1024 字节循环发送。这样既不会触发 DMA 限制内存也好管理。#define W5500_BUF_SIZE 1024 uint8_t *tx_buf heap_caps_malloc(W5500_BUF_SIZE, MALLOC_CAP_DMA); uint8_t *rx_buf heap_caps_malloc(W5500_BUF_SIZE, MALLOC_CAP_DMA);提示MALLOC_CAP_DMA这个标志必须加否则分配的内存可能不在 DMA 可访问区域传输会失败或数据错乱。这个坑我踩过现象是发送正常但接收全是 0。4. 从零搭建 ESP32 加 W5500 有线以太网4.1 硬件接线与电源处理先上接线表这是最基础的接错了后面全白搭ESP32 引脚W5500 引脚说明GPIO 18SCLKSPI 时钟GPIO 19MISO主机输入GPIO 23MOSI主机输出GPIO 5CS片选GPIO 4INT中断可选GPIO 2RST复位3.3VVCC电源GNDGND地电源这块要单独说。W5500 峰值电流能到 150mA 左右如果和 ESP32 共用一路 3.3V LDO且 LDO 输出能力不足会出现网络一通信就复位的情况。我建议 W5500 单独用一路 LDO或者在 VCC 和 GND 之间并一个 100uF 电解电容加 0.1uF 陶瓷电容。复位引脚也别省。W5500 上电后需要至少 1ms 的低电平复位之后等 50ms 才能访问寄存器。我一开始没接 RST靠上电自动复位结果偶尔初始化失败后来老老实实接上 GPIO 控制复位问题消失。4.2 SPI 总线初始化代码逐行讲#include driver/spi_master.h #include driver/gpio.h #define PIN_SCLK 18 #define PIN_MISO 19 #define PIN_MOSI 23 #define PIN_CS 5 #define PIN_RST 2 spi_device_handle_t spi_handle; void spi_init(void) { // 1. 配置总线 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, // 单次最大传输字节数 }; ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, buscfg, SPI_DMA_CH_AUTO)); // 2. 配置设备 spi_device_interface_config_t devcfg { .clock_speed_hz 20 * 1000 * 1000, .mode 0, .spics_io_num PIN_CS, .queue_size 7, .flags 0, }; ESP_ERROR_CHECK(spi_bus_add_device(SPI2_HOST, devcfg, spi_handle)); }逐行解释几个关键点SPI2_HOSTESP32 有 SPI1、SPI2、SPI3 三个主机。SPI1 通常被 flash 占用所以用 SPI2。SPI_DMA_CH_AUTO自动分配 DMA 通道。如果手动指定要注意别和别的外设冲突。max_transfer_sz这个值要大于等于你单次事务的最大字节数否则spi_device_transmit会返回错误。queue_size事务队列深度。设 7 是经验值够用且不浪费内存。4.3 W5500 复位与初始化流程复位代码void w5500_reset(void) { gpio_set_direction(PIN_RST, GPIO_MODE_OUTPUT); gpio_set_level(PIN_RST, 0); vTaskDelay(pdMS_TO_TICKS(10)); gpio_set_level(PIN_RST, 1); vTaskDelay(pdMS_TO_TICKS(100)); // 等 W5500 内部稳定 }初始化流程按顺序做这几件事读版本号确认是0x04。不是的话SPI 通信有问题先查接线和模式。配置网络信息网关、子网掩码、本机 IP、MAC。设置 socket 缓冲区大小。W5500 的 32KB 可以分配给 8 个 socket我一般给 socket 0 分 8KB 收发够用。打开 socket 0设置为 TCP 客户端或服务器模式。配置网络信息的代码void w5500_set_network(uint8_t *ip, uint8_t *mask, uint8_t *gw, uint8_t *mac) { w5500_write(SHAR, 0x00, mac, 6); // MAC 地址 w5500_write(SIPR, 0x00, ip, 4); // 本机 IP w5500_write(SUBR, 0x00, mask, 4); // 子网掩码 w5500_write(GAR, 0x00, gw, 4); // 网关 }这些寄存器地址在 W5500 数据手册的通用寄存器区BSB 都是0x00。4.4 TCP 客户端连接与数据收发打开 socket 0 作为 TCP 客户端void w5500_tcp_client(uint8_t *dest_ip, uint16_t dest_port) { // 设置目标 IP 和端口 w5500_write(Sn_DIPR(0), 0x01, dest_ip, 4); uint8_t port[2] {dest_port 8, dest_port 0xFF}; w5500_write(Sn_DPORT(0), 0x01, port, 2); // 打开 socket模式为 TCP uint8_t mode 0x01; // TCP w5500_write(Sn_MR(0), 0x01, mode, 1); // 发送 CONNECT 命令 uint8_t cmd 0x04; // CONNECT w5500_write(Sn_CR(0), 0x01, cmd, 1); // 等待连接成功 uint8_t status; do { w5500_read(Sn_SR(0), 0x01, status, 1); vTaskDelay(pdMS_TO_TICKS(10)); } while (status ! 0x17); // 0x17 ESTABLISHED }发送数据时先写发送缓冲区再发 SEND 命令void w5500_send(uint8_t *data, uint16_t len) { // 写发送缓冲区 w5500_write(Sn_TX_FSR(0), ...); // 先读空闲空间确认够放 w5500_write(0x0000, 0x02, data, len); // BSB0x02 是 socket 0 发送缓冲区 // 写发送长度 uint8_t len_bytes[2] {len 8, len 0xFF}; w5500_write(Sn_TX_WR(0), 0x01, len_bytes, 2); // 发 SEND 命令 uint8_t cmd 0x20; w5500_write(Sn_CR(0), 0x01, cmd, 1); }接收数据类似先读Sn_RX_RSR看收到多少字节再从接收缓冲区读出来最后发 RECV 命令释放空间。5. 常见问题与排查技巧实录5.1 初始化失败读不到版本号 0x04这是最常见的问题排查顺序如下现象可能原因排查方法读到 0x00MISO 没接或虚焊万用表测通断读到 0xFFMISO 被拉高SPI 没工作示波器看 SCLK 有没有波形读到随机值SPI 模式错或时钟太快降到 1MHz确认 mode0偶尔读到 0x04电源不稳或复位不充分加电容延长复位等待我遇到过一次读到0x00查了半天是 MISO 的杜邦线内部断了外表看不出来。所以杜邦线一定要用万用表测别信外观。5.2 能 ping 通但 TCP 连不上ping 走的是 ICMP由 W5500 硬件处理不经过 socket。TCP 连不上说明 socket 配置有问题。检查这几点Sn_MR是否设成了 TCP0x01Sn_PORT是否和服务器端口一致Sn_CR的 CONNECT 命令是否发出Sn_SR的状态值是什么对照手册查含义常见状态值0x00SOCK_CLOSEDsocket 没打开0x13SOCK_INIT已初始化但没连接0x17SOCK_ESTABLISHED连接成功0x1CSOCK_CLOSE_WAIT对方关闭了连接5.3 运行几天后断连这是 W5500 的经典问题热词里也有人提到正常工作几天后连不上。原因通常是没有心跳保活TCP 连接长时间没数据中间路由或防火墙会断开。解决办法是每隔 30 秒发一个心跳包。socket 缓冲区溢出接收数据没及时读走缓冲区满了之后 W5500 会停止接收。要保证接收循环及时处理。电源纹波长时间运行后 LDO 发热输出纹波变大。换质量好的 LDO加散热。我的做法是加一个看门狗任务每 60 秒检查Sn_SR如果不是0x17就重新连接。同时每次收发都检查返回值出错就复位 W5500。5.4 SPI 通信不生效的隐藏坑热词里有spi通信不生效我总结几个隐蔽原因GPIO 被复用ESP32 某些 GPIO 在上电时有特殊功能比如 GPIO 12 会影响 flash 电压。避开这些引脚。中断优先级如果 SPI 传输在中断里调用而中断优先级配置不当会死锁。SPI 操作尽量放在任务里。DMA 缓冲区对齐虽然 ESP-IDF 会自动处理但手动分配时要注意 4 字节对齐。提示调试 SPI 问题时逻辑分析仪比示波器好用。几十块的那种 8 通道逻辑分析仪能直接解码 SPI 协议看到每个字节的值比猜快得多。6. 可直接抄作业的完整工程结构6.1 文件组织我把代码分成三个文件spi_driver.c/hSPI 总线初始化和 W5500 读写函数w5500.c/hW5500 寄存器定义和网络操作main.c应用逻辑TCP 客户端或服务器这样分层的好处是换 SPI 引脚或换 W5500 到别的 MCU 时只改spi_driver就行。6.2 关键参数速查表参数推荐值说明SPI 时钟20MHz 起调杜邦线降到 8MHzSPI 模式0W5500 固定事务队列7够用不浪费单次传输≤1024 字节避免 DMA 限制复位低电平≥1ms实际给 10ms复位后等待≥50ms实际给 100ms心跳间隔30 秒防断连socket 缓冲区8KB32KB 分 4 个 socket6.3 实测数据我用这套代码在 ESP32-WROOM-32 加 W5500 模块上测过SPI 时钟 20MHz连续传输 1MB 数据误码率 0TCP 回环测试吞吐约 8Mbps连续运行 72 小时无断连功耗W5500 工作时约 120mA空闲约 80mA吞吐上不去的原因主要是 SPI 时钟和 W5500 内部处理速度。想再快可以把时钟提到 30MHz但要确保走线质量。6.4 后续可以扩展的方向这套基础打通后可以往上加MQTT 客户端在 TCP 之上实现 MQTT 协议接云平台Modbus TCP热词里有人提 freemodbus 加 W5500思路一样把 Modbus 帧通过 TCP 发出去多 socket 并发W5500 支持 8 个 socket可以同时做服务器和客户端DHCP目前是静态 IP加 DHCP 需要实现 UDP 广播和解析我个人在实际操作中的体会是SPI 这东西看着简单但每个参数都有它的道理。把 ESP-IDF 的事务机制吃透不光 W5500换任何 SPI 设备都能快速上手。调试时别急着上高速先用低速把功能跑通再逐步提频这样出问题也容易定位。最后再分享一个小技巧W5500 的 INT 引脚接上后可以用中断方式等数据比轮询省 CPU在低功耗场景下很有用。
返回列表