ESP32与W5500有线以太网:SPI帧、寄存器驱动与Socket实战

ESP32与W5500有线以太网:SPI帧、寄存器驱动与Socket实战 一块 ESP32 加上一片 W5500是很多人做有线以太网方案时最先想到的组合我当年第一次上手也是这么搭的。例程烧进去串口什么都没打印去读 W5500 的版本寄存器永远是 0xFF那会儿我连这颗芯片的 SPI 帧分成几段都没搞清楚。后来把逻辑分析仪挂上去一帧一帧看时序才发现问题简单得可笑SPI 模式选错了。这篇就把 ESP32 的 SPI 和 W5500 这套组合从硬件到例程完整拆一遍重点讲清楚每一行代码为什么要那么写最后你拿过去改几个 IP 就能直接用。1. 为什么在 ESP32 上外挂 W5500 做有线以太网1.1 无线不是万能的先想清楚你为什么要插网线先说场景。我接触过的项目里主动要求必须有线的基本集中在几类设备装在一个金属机柜里或者旁边就是变频器、伺服电机、大功率继电器现场要求毫秒级的确定性延迟不允许因为信道重传抖动还有就是客户明确写了不允许设备接入无线网络。这几种情况用 ESP32 自带的 WiFi 都能做但会很难受。金属外壳本身就是个屏蔽笼得开天线窗、想办法把天线引出来强电磁环境里丢包率上去了你的应用层就得写一堆重传和超时逻辑更麻烦的是配网产线上一批设备要逐个连热点这一套流程光人力成本就很可观。一根网线插上去链路层就通了这件事的确定性是 WiFi 给不了的。所以ESP32 W5500 做有线以太网本质上不是在解决能不能联网而是在解决联网的过程和结果是否可预测。1.2 W5500 和 RMII 方案的区别不是快慢问题ESP32 其实还有另一条路外挂一颗 RMII 接口的 PHY比如常见的 LAN8720、DP83848 之类。ESP32 内部有以太网 MAC配上 PHY 就能组成完整的以太网口走的是 RMII 接口速率能跑满 100Mbps。那为什么还有那么多人选 W5500关键在于 W5500 是一颗带完整硬件 TCP/IP 协议栈的芯片。你给它的不是以太网帧而是我要往这个 IP 的这个端口发这串数据剩下的事——ARP、IP、ICMP、TCP 三次握手、滑动窗口、重传、校验——全在芯片内部完成。MCU 侧只做一件事通过 SPI 把数据搬进去再把数据搬出来。这两种方案的取舍我列个表更直观对比项W5500SPIRMII PHYLAN8720 等协议栈位置W5500 内部硬件实现ESP32 内部软件 LwIP占用引脚4 根 SPI 复位 中断约 6 根RMII 约 9~10 根还要时钟MCU 主频压力很小只需要搬数据较大协议栈跑在 MCU 上实测吞吐约 10~30Mbps取决于 SPI 时钟可到 90Mbps 以上硬件成本SPI 简单走线容易需要 50MHz 时钟布线稍讲究适合场景数据量不大、要求稳定、MCU 资源紧需要高带宽、视频流、大文件传输一个很实际的判断标准如果你单个连接的数据量在每秒几兆比特以内W5500 的稳定性和开发成本都更划算如果要做视频推流或者大文件传输那别犹豫直接上 RMII。1.3 什么情况下反而不该用 W5500也有几个场景我劝你换方案。需要 IPv6 的W5500 不支持。需要组播、IGMP、复杂的 UDP 广播发现的W5500 的硬件协议栈能力有限做起来会很别扭。需要同一时刻开几十个 TCP 连接的W5500 只有 8 个硬件 socket够用但不宽裕。还有就是需要精细控制 TCP 行为比如自定义拥塞控制、Nagle 算法的细节调整的场合硬件协议栈意味着你什么都改不了接受它的默认行为就行。想清楚这几点再决定要不要往下看。2. W5500 的 SPI 帧到底长什么样2.1 先纠正一个常见误解它不是发命令读数据我发现很多人对 W5500 SPI 卡壳根源在于脑子里装着另一类芯片比如 SPI Flash的模型先发一个命令字节再发地址然后读数据。W5500 不是这个路子。W5500 的每一次 SPI 传输是一段固定结构的帧由三段拼成地址相Address Phase2 字节就是你要访问的寄存器或缓冲区在块内的偏移地址高字节先发。控制相Control Phase1 字节里面打包了三样东西——块选择 BSB、读写方向 RWB、操作模式 OM。数据相Data PhaseN 字节真正读或写的内容。也就是说每次访问寄存器前面一定有 3 个字节的头。这 3 个字节既不是可选也不是可有可无一次都不能少。理解了这一点后面代码里那三行s_tx[0] / s_tx[1] / s_tx[2]你就一眼看懂了。2.2 控制字节的三个字段怎么拼控制相那一个字节比特位的分配是固定的bit7 bit6 bit5 bit4 bit3 | bit2 | bit1 bit0 BSB[4:0] | RWB | OM[1:0]BSB 占高 5 位RWB 占第 2 位OM 占最低 2 位。所以拼装的时候就是(bsb 3) | (rwb 2) | om。BSB 是块选择W5500 内部把地址空间分成若干个块不同块访问的是不同的物理位置。最常用的几块BSB 值含义0x00通用寄存器块Common Register0x01 nSocket n 的寄存器块n 从 0 到 70x09 2nSocket n 的发送缓冲区TX Buffer0x0A 2nSocket n 的接收缓冲区RX Buffer可以看到规律Socket n 的寄存器块是 1~8之后 TX/RX 缓冲区成对往后排所以 TX(n) 就是 2n9RX(n) 就是 2n10。记不住没关系写成宏就行。RWB 就一位0 表示读1 表示写。很多人会记反建议你直接定义成RWB_READ 0x00和RWB_WRITE 0x04这两个宏用名字而不是用字面量出错概率立刻降下去。OM 是操作模式两位OM 值含义0x00变长模式数据段长度由 CS 拉高决定0x01固定 1 字节0x02固定 2 字节0x03固定 4 字节我一般统一用变长模式0x00因为一次传输里长度是多少SPI 主机自己知道帧结束时拉高 CS 就收尾了不用为每种寄存器长度单独改代码。如果你做极限优化访问单字节寄存器时用固定 1 字节模式可以少一个字节的额外时钟周期但这点收益在实际项目里基本看不出来。提示CS 在 W5500 这里是有帧边界含义的。一帧结束必须拉高 CS下一帧再拉低。千万不要像访问某些 Flash 那样把 CS 一直按着不放那样芯片根本不知道你这一帧什么时候结束。2.3 为什么用 SPI 模式 0时钟能跑多快W5500 支持 SPI 模式 0CPOL0CPHA0和模式 3CPOL1CPHA1我强烈建议用模式 0理由有两个一是绝大多数例程和调试工具默认就是模式 0排错的时候少一个变量二是模式 0 下时钟空闲电平是低示波器上看着清爽抓波形的时候不用反复确认触发极性。时钟速率方面W5500 数据手册标的上限是 80MHz但这是理想 PCB 上的数字。实际用模块加杜邦线搭的时候我建议从1MHz 起步先确认通信正常再往上加。经验值是杜邦线飞线的情况下20~30MHz 通常能稳PCB 上走线短、有地平面的话40MHz 常见想逼近 80MHz得看你的走线质量、是否有串阻匹配、以及 ESP32 那几个 IO 有没有走 IO_MUX。我自己的项目里 30MHz 是个舒服的平衡点。注意ESP32 的 SPI 时钟不是任意值都能生成的驱动会就近取一个分频结果。所以你在代码里写 30MHz实际可能跑在 26.67MHz 或者 40MHz想精确知道当前速率可以查spi_device_get_actual_freq。这点在排查为什么降速后就好了的时候很有用。3. 参考电路里几个不能省也不能改错的元件3.1 RSET_BG 那颗 12.4kΩ 电阻这是我最想强调的一条。W5500 有一颗引脚叫 RSET_BG需要外接一颗12.4kΩ、精度 1%的电阻到地它给芯片内部的基准电流源定标。为什么强调这个因为我见过不止一次有人从别人那里抄了个原理图把那颗电阻当成了可选的上拉直接省掉结果芯片能识别、寄存器能读、SPI 通信完全正常但网口就是不通PHY 链路始终起不来。这种故障特别难查因为所有数字部分的迹象都是好的。绕开这个坑的办法只有一个照着官方参考电路一颗一颗对看到不认识的元件不要自作主张删掉。3.2 3.3V 供电和去耦W5500 是 3.3V 单电源但内部有 LDO 产生 1.8V 给内核用这个 1.8V 会有对应的输出引脚需要外接退耦电容通常是 10µF 加 100nF。同样地VDD 侧每一组电源引脚旁边都要有 100nF 就近去耦整体再放一颗 10µF 的钽电容或者陶瓷电容。供电质量这件事在以太网芯片上比在普通数字芯片上敏感得多因为 PHY 部分对电源噪声很挑。如果你的板子上同时有大电流的继电器、电机驱动建议 W5500 这一路单独走一条电源通道哪怕只是串一颗磁珠加一组 LC 也好。3.3 RJ45 和网络隔离变压器标准的做法是RJ45 座子或带变压器的 RJ45 一体座内部包含隔离变压器和共模扼流圈W5500 的 TX/TX-/RX/RX- 差分对接到变压器初级变压器次级接 RJ45 的对应引脚。这里两张图必须看清楚一是Bob Smith 终端也就是差分对通过 75Ω 电阻接到一个公共点再通过一颗 1000pF/2kV 的电容接到机壳地它的作用是泄放共模能量、降低辐射二是中心抽头变压器初级侧的中心抽头要接到 3.3V发送侧并通过电容到地接收侧具体接法看变压器型号的手册。用现成的 W5500 模块的话这些都在模块上做好了你只需要一根网线。自己做板的话这部分千万别图省事尤其是 Bob Smith 那几颗电阻电容。3.4 复位、中断和晶振W5500 的 RST 引脚是低电平复位手册要求复位低电平保持一定时间释放后还要等内部 PLL 稳定。我在代码里习惯复位低电平保持 10ms释放后等 50~60ms 再开始 SPI 通信这个时间富裕一点没坏处。INT 引脚是中断输出低电平有效建议加上拉电阻并接到 ESP32 的一个 GPIO 上。用中断的好处是接收数据不用轮询CPU 占用低。不用中断也行轮询Sn_RX_RSR一样能跑只是效率差一点。晶振方面W5500 需要 25MHz 晶振模块上一般都带了自己做板的话注意负载电容的取值要按晶振手册来别照抄别人的值。3.5 和 ESP32 之间的连线W5500 是 3.3V 器件ESP32 的 IO 也是 3.3V可以直接连不需要电平转换。连线就 4 根 SPI 加复位加中断W5500ESP32本例说明SCSnGPIO14片选低有效SCLKGPIO18时钟建议走 IO_MUXMOSIGPIO23主机输出MISOGPIO19主机输入INTnGPIO27中断低有效RSTnGPIO26复位低有效VCC / GND3V3 / GND供电关于 GPIO 选择有个细节值得说ESP32 经典款的 SPI3也就是常说的 VSPI有固定的 IO_MUX 引脚正好是 GPIO18/19/23。走 IO_MUX 的时候信号路径短、延迟小高速时钟下更稳如果随便挑 GPIO信号要走 GPIO 矩阵绕一圈高速下容易出现建立/保持时间余量不足。所以 SCLK、MOSI、MISO 这三根尽量用 IO_MUX 引脚CS 和 INT、RST 无所谓走 GPIO 矩阵就行。另外避开几个坑GPIO6~11 接了内部 Flash绝对不能碰如果开了 PSRAMGPIO16/17 也可能被占用GPIO0、2、5、12、15 是启动配置引脚用作 CS 的时候要确认上电瞬间的电平不会影响启动模式。我上面选的 14/18/19/23/26/27 都不在这几个雷区里。4. 自己撸寄存器驱动还是直接用官方组件4.1 两条路各自的价值这里有个选择题ESP-IDF 里已经有官方的 W5500 组件包含在 esp_eth 里一句话就能装好驱动并跑起 DHCP但同时你也可以自己写寄存器读写完全掌控底层。我的建议是两条路都走一遍但顺序很重要第一步先自己写最小版本的寄存器读写 版本号读取。目标只有一个——把VERSIONR读出0x04。这一步过了说明你的硬件连线、SPI 模式、时钟、CS 时序、帧结构全都是对的后面再怎么折腾都不会是硬件问题。第二步再切到官方组件写应用。因为官方组件的抽象层次很高一旦出问题你很难判断是硬件坏了还是配置错了。有了第一步建立的可信基线排错范围直接砍掉一半。4.2 官方组件这条路的打开方式官方组件不需要额外下载它是 ESP-IDF 自带的。你要做的是idf.py menuconfig然后进到Component config - Ethernet - Support SPI to Ethernet Module勾上W5500 Ethernet。同时可以在Ethernet SPI Clock里设置时钟默认 20MHz可以先不动。用官方组件的时候spi_bus_initialize还是你自己调但spi_bus_add_device不用调W5500 驱动内部会自己挂设备。这一点很容易踩坑——如果你同时调了spi_bus_add_device又把它传给驱动同一个 CS 上会挂两个设备句柄行为就不可预期了。用 Arduino 环境的话更简单核心库里已经有以太网库直接#define ETH_PHY_TYPE ETH_PHY_W5500 ETH.begin(/* cs */ 14, /* irq */ 27, /* rst */ 26, /* miso */ 19, /* mosi */ 23, /* sck */ 18);一行就起来了。但这也意味着你完全看不到里面发生了什么一旦不通还是得回到下面这套自撸的调试路径上。5. 逐行拆解 SPI 初始化与三个底层读写函数5.1 先把常量和句柄定义好#include driver/spi_master.h #include driver/gpio.h #include esp_log.h #include string.h #define PIN_SCLK 18 #define PIN_MOSI 23 #define PIN_MISO 19 #define PIN_CS 14 #define PIN_INT 27 #define PIN_RST 26 #define W5500_SPI_HOST SPI3_HOST /* 经典 ESP32 上就是 VSPI */ #define W5500_SPI_CLK (30 * 1000 * 1000) /* 块选择 BSB */ #define BSB_COMMON 0x00 #define BSB_SOCK_REG(n) (0x01 (n)) #define BSB_SOCK_TX(n) (0x09 2 * (n)) #define BSB_SOCK_RX(n) (0x0A 2 * (n)) /* 读写方向 RWB */ #define RWB_READ 0x00 #define RWB_WRITE 0x04 /* 操作模式 OM变长 */ #define OM_VDM 0x00 #define W5500_BUF_MAX 1520 static spi_device_handle_t s_spi; static uint8_t s_tx[3 W5500_BUF_MAX] __attribute__((aligned(4))); static uint8_t s_rx[3 W5500_BUF_MAX] __attribute__((aligned(4)));这里有两个点值得说。W5500_SPI_HOST选SPI3_HOST是为了能对上 GPIO18/19/23 这三个 IO_MUX 引脚。两个缓冲区之所以aligned(4)是因为走 DMA 的时候驱动会检查对齐同时注意它们必须是DMA 可访问的内存——如果你开了 PSRAM千万不要用malloc随手分配再传给 SPI要用MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL显式申请。静态数组放在内部 RAM 里是安全的。缓冲区大小给 1520 是有讲究的以太网最大帧是 1518 字节W5500 的 socket 缓冲区里搬一次最多也就这么大一次传输覆盖得住省得拆包。5.2 总线初始化的每一行在做什么static void w5500_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, /* 不用 QSPI 的 WP */ .quadhd_io_num -1, /* 不用 QSPI 的 HD */ .max_transfer_sz 3 W5500_BUF_MAX, }; ESP_ERROR_CHECK(spi_bus_initialize(W5500_SPI_HOST, buscfg, SPI_DMA_CH_AUTO)); spi_device_interface_config_t devcfg { .mode 0, /* CPOL0 CPHA0 */ .clock_speed_hz W5500_SPI_CLK, .spics_io_num PIN_CS, .queue_size 8, .cs_ena_pretrans 1, .cs_ena_posttrans 1, }; ESP_ERROR_CHECK(spi_bus_add_device(W5500_SPI_HOST, devcfg, s_spi)); }max_transfer_sz一定要显式设大到 1523 以上这个参数如果不给默认只有 4096 字节不同 IDF 版本略有差异看起来够用但如果你后面想一次把整个 socket 接收缓冲区搬出来比如 16KB 的配置就会在运行时被驱动拒绝。而queue_size是事务队列深度做异步传输的时候有用同步轮询模式下给个 8 就够。cs_ena_pretrans 1这个参数是 W5500 的救命稻草。它的含义是在第一个时钟沿到来之前先提前 1 个 SPI 时钟把 CS 拉低。W5500 对 CS 建立时间有要求尤其是时钟跑得快的时候如果 CS 和第一个时钟沿几乎同时出现芯片可能还没识别到帧开始第一个字节就丢了。现象就是寄存器偶尔读对、偶尔读错或者版本号时好时坏。我一开始不设这个参数20MHz 下没问题一上 30MHz 就开始抽风加上它之后再也没出过。cs_ena_posttrans 1是收尾时多给一个时钟再拉高 CS同理是给最后的保持时间留余量。5.3 核心传输函数三字节头加数据static esp_err_t w5500_xfer(uint8_t bsb, uint16_t addr, bool is_write, uint8_t *buf, uint16_t len) { if (len W5500_BUF_MAX) { return ESP_ERR_INVALID_SIZE; } /* 地址相2 字节高字节先发 */ s_tx[0] (uint8_t)(addr 8); s_tx[1] (uint8_t)(addr 0xFF); /* 控制相BSB[4:0] | RWB | OM[1:0] */ s_tx[2] (uint8_t)((bsb 3) | (is_write ? RWB_WRITE : RWB_READ) | OM_VDM); if (is_write) { memcpy(s_tx[3], buf, len); } else { /* 读的时候 MOSI 要发占位字节否则时钟不会产生 */ memset(s_tx[3], 0x00, len); } spi_transaction_t t { .length (size_t)(3 len) * 8, .tx_buffer s_tx, .rx_buffer s_rx, }; esp_err_t err spi_device_polling_transmit(s_spi, t); if (err ! ESP_OK) { return err; } if (!is_write) { /* 前 3 字节是头真正的数据从这里开始 */ memcpy(buf, s_rx[3], len); } return ESP_OK; }这个函数有四个地方值得逐条说清楚。第一为什么读写共用一个函数。因为 W5500 的帧结构除了控制字节里的一个位其余完全一样。分成两个函数代码会重复一大截容易改一处忘一处。用一个is_write参数区分维护起来省事。第二为什么读的时候要给 TX 缓冲填 0。SPI 是全双工的时钟由主机产生主机要一直发东西时钟才会一直跳。如果你tx_buffer传 NULL驱动在纯接收模式下仍然会产生时钟ESP32 的 SPI 硬件支持这种模式但有些场景下用显式占位字节更可控、更符合所见即所得的调式习惯。代价是多消耗一点内存带宽可以忽略。第三为什么读的时候要跳过前 3 个字节。W5500 在地址相和控制相期间 MISO 是处于未驱动的状态这段时间采到的数据没有意义通常读到的是上一次残留或者浮空的高电平。真正的数据从控制相结束后的第一个字节开始所以memcpy(buf, s_rx[3], len)里面的偏移就是 3。这个 3 如果写错成 0 或者 2你会读到一个整体偏移的、看起来像是数据但又不对的字节流非常难查。第四length的单位是比特不是字节。(3 len) * 8这里千万别忘了乘 8。我见过有人写漏了结果只传了 1/8 的数据读出来全乱。5.4 单字节和双字节的便捷封装static inline esp_err_t w5500_wr8(uint8_t bsb, uint16_t addr, uint8_t v) { return w5500_xfer(bsb, addr, true, v, 1); } static inline uint8_t w5500_rd8(uint8_t bsb, uint16_t addr) { uint8_t v 0; w5500_xfer(bsb, addr, false, v, 1); return v; } static inline esp_err_t w5500_wr16(uint8_t bsb, uint16_t addr, uint16_t v) { uint8_t b[2] { (uint8_t)(v 8), (uint8_t)(v 0xFF) }; return w5500_xfer(bsb, addr, true, b, 2); } static inline uint16_t w5500_rd16(uint8_t bsb, uint16_t addr) { uint8_t b[2] { 0, 0 }; w5500_xfer(bsb, addr, false, b, 2); return ((uint16_t)b[0] 8) | b[1]; }这里藏着一个特别容易犯的错W5500 的所有多字节寄存器都是大端序高字节在前。网络字节序本身就是大端所以 W5500 内部直接沿用。如果你按小端去拼比如v 0xFF放前面写进去的端口号就变成了高位和低位互换表现为端口明明是 5000对端却连不上或者地址写的是 192.168.1.100抓包看到的是 100.1.168.192。这类错误排查起来挺折磨人的因为寄存器读回来和你写进去的字节一模一样看起来完全正常。Sn_TX_FSR、Sn_RX_RSR这类长度寄存器也是大端读的时候必须按大端拼装否则算出来的可用空间会大得离谱然后你就往一个实际上没空间的缓冲区里塞数据。6. 上电初始化序列从软复位到链路建立6.1 硬件复位和软复位static void w5500_hard_reset(void) { gpio_config_t io { .pin_bit_mask 1ULL PIN_RST, .mode GPIO_MODE_OUTPUT, }; gpio_config(io); gpio_set_level(PIN_RST, 0); vTaskDelay(pdMS_TO_TICKS(10)); gpio_set_level(PIN_RST, 1); vTaskDelay(pdMS_TO_TICKS(60)); }复位低电平 10ms、释放后等 60ms这两个数字是我从多个项目里摸出来的富裕值。手册给的内部 PLL 锁定时间比这个小但上电阶段电源还在爬升等久一点更稳。等到你发现上电偶尔不通、断电重上就好这种间歇性故障的时候就会理解这几十毫秒的价值。软复位走的是 MR 寄存器static esp_err_t w5500_soft_reset(void) { w5500_wr8(BSB_COMMON, 0x0000, 0x80); /* MR 的 bit7 是 RST写 1 触发 */ for (int i 0; i 100; i) { if ((w5500_rd8(BSB_COMMON, 0x0000) 0x80) 0) { return ESP_OK; /* 硬件完成后自动清 0 */ } vTaskDelay(pdMS_TO_TICKS(1)); } return ESP_ERR_TIMEOUT; }MR 的复位位是自清零的你写 1 进去芯片开始复位完成后自己把它变回 0。所以判断复位有没有完成就是轮询这一位什么时候变成 0。如果 100ms 之后还是 1说明通信根本没建立起来别往下走了回头查 SPI。6.2 第一个里程碑读版本寄存器#define W5500_VERSIONR 0x0039 uint8_t ver w5500_rd8(BSB_COMMON, W5500_VERSIONR); ESP_LOGI(TAG, W5500 VERSIONR 0x%02X, ver); if (ver ! 0x04) { ESP_LOGE(TAG, 版本号不对先别往下走); }VERSIONR在通用寄存器块的0x0039位置W5500 固定返回0x04。这是整个调试过程中最有价值的一个断点。读出 0x04说明地址相、控制相、数据相、字节序、SPI 模式、CS 时序全对读不出来后面所有代码都没有意义。我建议你把这一句单独做成一个自检函数每次上电都跑一遍出了问题立刻能定位。6.3 网络参数、重传参数和 socket 缓冲区static void w5500_set_netinfo(void) { const uint8_t mac[6] { 0x02, 0x08, 0xDC, 0x11, 0x22, 0x33 }; const uint8_t ip[4] { 192, 168, 1, 201 }; const uint8_t gw[4] { 192, 168, 1, 1 }; const uint8_t mask[4] { 255, 255, 255, 0 }; w5500_xfer(BSB_COMMON, 0x0009, true, (uint8_t *)mac, 6); /* SHAR */ w5500_xfer(BSB_COMMON, 0x000F, true, (uint8_t *)ip, 4); /* SIPR */ w5500_xfer(BSB_COMMON, 0x0001, true, (uint8_t *)gw, 4); /* GAR */ w5500_xfer(BSB_COMMON, 0x0005, true, (uint8_t *)mask, 4); /* SUBR */ w5500_wr16(BSB_COMMON, 0x0019, 2000); /* RTR2000 x 100us 200ms */ w5500_wr8 (BSB_COMMON, 0x001B, 8); /* RCR重传 8 次 */ }MAC 地址这块有个硬性要求第一个字节的最低位必须是 0也就是必须是单播地址同时整个 MAC 不能是全 0。我习惯用0x02开头这是本地管理地址段不会和真实厂商的 OUI 撞车。如果你图省事填了个0x01开头的那是个组播地址W5500 会直接拒绝工作表现为链路起来了但一个包都发不出去。RTR 的单位是 100µs2000 对应 200ms这是 TCP 超时重传的基准时间。RCR 是最大重传次数8 次。这两个参数决定了对端掉线时你能多快感知到局域网环境里 200ms 和 8 次是比较舒服的组合。想更快检测断连就减小 RTR 或者 RCR。6.4 16KB 内存怎么切给 8 个 socketW5500 内部只有 16KB 发送缓冲和 16KB 接收缓冲8 个 socket 共享。你要通过Sn_TXBUF_SIZE和Sn_RXBUF_SIZE手工分配static void w5500_alloc_buffers(void) { for (int i 0; i 8; i) { uint8_t rx (i 0) ? 16 : 0; uint8_t tx (i 0) ? 16 : 0; w5500_wr8(BSB_SOCK_REG(i), 0x001E, rx); w5500_wr8(BSB_SOCK_REG(i), 0x001F, tx); } }这两个寄存器的取值只能是 0、1、2、4、8、16 这几个数单位 KB写别的值芯片不认。而且所有 socket 加起来不能超过 16KB。上面这段代码的意思是只给 socket 0 分满 16KB 收发缓冲其他 7 个都关掉。如果你要开 4 个连接并发通常是每个分 4KB如果做 8 路并发那只能各分 2KB。缓冲区小了会明显影响吞吐因为 TCP 的滑动窗口受限于接收缓冲区大小2KB 的窗口在高延迟链路上跑不出速度。这个权衡在设计阶段就要想清楚别到后面才发现分错了还得重来。6.5 PHY 启动和等待链路static esp_err_t w5500_phy_start(uint32_t timeout_ms) { uint8_t cfg w5500_rd8(BSB_COMMON, 0x002E); /* PHYCFGR */ cfg (uint8_t)~0x07; /* 清掉地位的状态位 */ cfg | (uint8_t)(0x07 3); /* OPMDC 111全功能自动协商 */ cfg | 0x80; /* RST 1解除 PHY 复位 */ w5500_wr8(BSB_COMMON, 0x002E, cfg); TickType_t deadline xTaskGetTickCount() pdMS_TO_TICKS(timeout_ms); while (xTaskGetTickCount() deadline) { uint8_t now w5500_rd8(BSB_COMMON, 0x002E); if (now 0x01) { /* bit0 是链路状态 LNK */ ESP_LOGI(TAG, PHY 链路已建立, PHYCFGR 0x%02X, now); return ESP_OK; } vTaskDelay(pdMS_TO_TICKS(50)); } return ESP_ERR_TIMEOUT; }PHYCFGR的最低位是链路状态1 表示网线和交换机之间已经协商成功。这里轮询的时候顺便把整个字节打出来你还能从其他位看出协商速率和双工模式——比如读到0xBB之类通常意味着 100Mbps 全双工。这个技巧在判断网线是不是只有两对通的时候很好用。OPMDC 111是让 PHY 工作在全功能自动协商模式它会自己和交换机商量出一个双方都支持的最高速率和双工方式。除非你有特殊需求要强制 10Mbps 半双工否则都用这个。7. 打开 socket 收发数据的完整流程7.1 socket 命令寄存器和 SR 状态机W5500 的 socket 操作走一个写命令寄存器、然后等它归零的模式。所有命令都往Sn_CR写命令值如下命令值作用OPEN0x01按 Sn_MR 里配置的协议打开 socketLISTEN0x02作为服务端开始监听CONNECT0x04作为客户端发起连接DISCON0x08断开CLOSE0x10关闭SEND0x20发送数据RECV0x40释放接收缓冲区每次写完 Sn_CR 都要轮询直到它变成 0因为芯片执行完命令后会把这一位清零。如果你不等紧接着写下一个命令大概率会被忽略。我见过一种写了 SEND 但数据没发出去的现象十有八九就是没等命令完成。Sn_SR是状态寄存器几个关键值0x00 表示 CLOSED0x13 表示 INIT已打开但没在监听或连接0x14 是 LISTEN0x17 是 ESTABLISHED连接已建立0x1C 是 CLOSE_WAIT对端关闭了。7.2 打开 socketstatic esp_err_t w5500_socket_open(uint8_t sn, uint16_t port) { w5500_wr8(BSB_SOCK_REG(sn), 0x0001, 0x10); /* 先 CLOSE 清干净 */ while (w5500_rd8(BSB_SOCK_REG(sn), 0x0001)); w5500_wr8(BSB_SOCK_REG(sn), 0x0000, 0x01); /* Sn_MR TCP */ w5500_wr16(BSB_SOCK_REG(sn), 0x0003, port); /* Sn_PORT */ w5500_wr8(BSB_SOCK_REG(sn), 0x0001, 0x01); /* OPEN */ while (w5500_rd8(BSB_SOCK_REG(sn), 0x0001)); return (w5500_rd8(BSB_SOCK_REG(sn), 0x0002) 0x13) ? ESP_OK : ESP_FAIL; }开头那一次 CLOSE 是防御性的用来把 socket 恢复到一个确定的初始状态。如果你的程序会在异常情况下重启socket 可能还残留在某个中间状态不先清一遍就会出现OPEN 之后 SR 不是 0x13这种怪事。Sn_MR的低四位是协议选择0x01 是 TCP0x02 是 UDP。注意 bit4 是 NDNo Delayed ACK位bit7 是组播位这些按需设置。7.3 作为客户端连接static esp_err_t w5500_tcp_connect(uint8_t sn, const uint8_t *ip, uint16_t port) { w5500_xfer(BSB_SOCK_REG(sn), 0x000B, true, (uint8_t *)ip, 4); /* Sn_DIPR */ w5500_wr16(BSB_SOCK_REG(sn), 0x000F, port); /* Sn_DPORT */