STM32F103 SPI 驱动 W5500 以太网模块实战:硬件协议栈寄存器体系、Socket 状态机避坑与 TCP 吞吐实测

文章目录

    • 摘要
    • 前言
    • [W5500 是什么:把协议栈搬进硅片](#W5500 是什么:把协议栈搬进硅片)
    • [SPI 帧格式:读懂 25 字节,等于读懂半颗芯片](#SPI 帧格式:读懂 25 字节,等于读懂半颗芯片)
    • 硬件选型与接线
    • [CubeMX 配置与容易遗漏的步骤](#CubeMX 配置与容易遗漏的步骤)
      • [SPI2 配置(含寄存器位标注)](#SPI2 配置(含寄存器位标注))
      • [容易遗漏的步骤:复位时序与 PHY 配置](#容易遗漏的步骤:复位时序与 PHY 配置)
    • 驱动代码实现
      • [SPI 读写封装(帧格式落地)](#SPI 读写封装(帧格式落地))
      • 初始化序列
      • [TCP Server:Socket 状态机轮询](#TCP Server:Socket 状态机轮询)
    • 测试验证
      • 测试环境
      • [基础连通性:ping 与 RTT](#基础连通性:ping 与 RTT)
      • [量化参数对比一:SPI 时钟频率对 TCP 吞吐的影响](#量化参数对比一:SPI 时钟频率对 TCP 吞吐的影响)
      • [理论 vs 实测对照一:SPI 理论带宽 vs 有效吞吐](#理论 vs 实测对照一:SPI 理论带宽 vs 有效吞吐)
      • [量化参数对比二:RX 缓冲大小对 UDP 丢包率的影响](#量化参数对比二:RX 缓冲大小对 UDP 丢包率的影响)
      • [理论 vs 实测对照二:以太网链路速率 vs 应用层吞吐](#理论 vs 实测对照二:以太网链路速率 vs 应用层吞吐)
    • 故障排查
      • [1. 读 VERSIONR 恒为 0xFF](#1. 读 VERSIONR 恒为 0xFF)
      • [2. 寄存器能读,但 socket 进不了 LISTEN](#2. 寄存器能读,但 socket 进不了 LISTEN)
      • [3. TCP 连上后收几包就卡死](#3. TCP 连上后收几包就卡死)
      • [4. 上电后概率性失败,按复位键就恢复](#4. 上电后概率性失败,按复位键就恢复)
      • [5. UDP 高负载下丢包](#5. UDP 高负载下丢包)
      • [6. 更换网络环境后连不上](#6. 更换网络环境后连不上)
    • 总结
    • 参考资料

摘要

STM32F103 没有以太网 MAC,想联网只能换 F407 跑 LwIP,软件协议栈吃 RAM 又占 CPU。本文改用 W5500 硬件协议栈芯片,MAC、PHY、TCP/IP 全部片内完成,MCU 仅通过 SPI 读写寄存器。实测:VERSIONR 返回 0x04,ping 千包零丢失、RTT 0.31ms;SPI 时钟 4.5→18MHz 时,TCP 回显吞吐 0.36→1.31MB/s;RX 缓冲 2→16KB 时,UDP 千包丢包率 3.2%→0。文末附 6 类排查链与驱动代码。

前言

做物联网设备的人都有过这种纠结:主控想选 STM32F103,一颗几块钱、资源刚刚好,可一看需求单上写着"需要以太网",就不得不往 F407 上靠------因为 F103 根本没有以太网 MAC。换芯片意味着重新画板、重新评估供货,代价不小。

市面上其实还有另一条路:外挂一颗 W5500。这颗芯片把 MAC、PHY 和整个 TCP/IP 协议栈都做进了硅片里,MCU 只负责通过 SPI 搬运数据。我第一次接触它时天真地以为"硬件协议栈 = 插上就能通",结果连着两天卡在同一个现象上:寄存器读得回来,VERSIONR 也返回 0x04,但 socket 就是进不了 LISTEN 状态。后来才明白,W5500 不是"网卡",而是一台需要按状态机伺候的"网络协处理器"------命令寄存器要写、状态寄存器要轮询、收发缓冲有独立的指针体系,每一步都有时序讲究。

读完本文,你将掌握:

  1. W5500 内部结构与"硬件协议栈 vs 软件协议栈"的选型逻辑;
  2. SPI 帧格式(16 位地址 + 8 位控制 + 数据)与三块寄存器区(通用、Socket、收发缓冲)的寻址方式;
  3. CubeMX 配置中每个选项对应的寄存器位,以及工具不会自动生成的复位时序与 PHY 配置;
  4. TCP Server 的 Socket 状态机完整流转与常见卡死点的排查方法;
  5. SPI 时钟、缓冲分配对吞吐量的量化影响。

前置条件:熟悉 STM32CubeMX 基本流程,会使用 HAL 库的 SPI 外设,手头有 STM32F103 最小系统板和一块 W5500 以太网模块(推荐带网络变压器的成品板,避免自己画差分线)。本文完整工程代码可在 CSDN 下载频道 获取(VIP 免费)。

W5500 是什么:把协议栈搬进硅片

硬件协议栈与软件协议栈的路线之争

先看两种方案的架构差异。软件协议栈(LwIP)跑在 MCU 内部,MCU 需要自带以太网 MAC(如 F407、F429),外部再挂一颗 PHY 芯片负责物理层;TCP 三次握手、重传、窗口管理等逻辑全部由 CPU 执行。硬件协议栈(W5500)则把 MAC + PHY + 传输层/网络层逻辑全部集成进一颗芯片,MCU 侧只暴露一个 SPI 从机接口。
#mermaid-svg-S4CnZ7ygnHX1GaRd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-S4CnZ7ygnHX1GaRd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-S4CnZ7ygnHX1GaRd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-S4CnZ7ygnHX1GaRd .error-icon{fill:#552222;}#mermaid-svg-S4CnZ7ygnHX1GaRd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-S4CnZ7ygnHX1GaRd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-S4CnZ7ygnHX1GaRd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-S4CnZ7ygnHX1GaRd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-S4CnZ7ygnHX1GaRd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-S4CnZ7ygnHX1GaRd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-S4CnZ7ygnHX1GaRd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-S4CnZ7ygnHX1GaRd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-S4CnZ7ygnHX1GaRd .marker.cross{stroke:#333333;}#mermaid-svg-S4CnZ7ygnHX1GaRd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-S4CnZ7ygnHX1GaRd p{margin:0;}#mermaid-svg-S4CnZ7ygnHX1GaRd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-S4CnZ7ygnHX1GaRd .cluster-label text{fill:#333;}#mermaid-svg-S4CnZ7ygnHX1GaRd .cluster-label span{color:#333;}#mermaid-svg-S4CnZ7ygnHX1GaRd .cluster-label span p{background-color:transparent;}#mermaid-svg-S4CnZ7ygnHX1GaRd .label text,#mermaid-svg-S4CnZ7ygnHX1GaRd span{fill:#333;color:#333;}#mermaid-svg-S4CnZ7ygnHX1GaRd .node rect,#mermaid-svg-S4CnZ7ygnHX1GaRd .node circle,#mermaid-svg-S4CnZ7ygnHX1GaRd .node ellipse,#mermaid-svg-S4CnZ7ygnHX1GaRd .node polygon,#mermaid-svg-S4CnZ7ygnHX1GaRd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-S4CnZ7ygnHX1GaRd .rough-node .label text,#mermaid-svg-S4CnZ7ygnHX1GaRd .node .label text,#mermaid-svg-S4CnZ7ygnHX1GaRd .image-shape .label,#mermaid-svg-S4CnZ7ygnHX1GaRd .icon-shape .label{text-anchor:middle;}#mermaid-svg-S4CnZ7ygnHX1GaRd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-S4CnZ7ygnHX1GaRd .rough-node .label,#mermaid-svg-S4CnZ7ygnHX1GaRd .node .label,#mermaid-svg-S4CnZ7ygnHX1GaRd .image-shape .label,#mermaid-svg-S4CnZ7ygnHX1GaRd .icon-shape .label{text-align:center;}#mermaid-svg-S4CnZ7ygnHX1GaRd .node.clickable{cursor:pointer;}#mermaid-svg-S4CnZ7ygnHX1GaRd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-S4CnZ7ygnHX1GaRd .arrowheadPath{fill:#333333;}#mermaid-svg-S4CnZ7ygnHX1GaRd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-S4CnZ7ygnHX1GaRd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-S4CnZ7ygnHX1GaRd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-S4CnZ7ygnHX1GaRd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-S4CnZ7ygnHX1GaRd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-S4CnZ7ygnHX1GaRd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-S4CnZ7ygnHX1GaRd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-S4CnZ7ygnHX1GaRd .cluster text{fill:#333;}#mermaid-svg-S4CnZ7ygnHX1GaRd .cluster span{color:#333;}#mermaid-svg-S4CnZ7ygnHX1GaRd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-S4CnZ7ygnHX1GaRd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-S4CnZ7ygnHX1GaRd rect.text{fill:none;stroke-width:0;}#mermaid-svg-S4CnZ7ygnHX1GaRd .icon-shape,#mermaid-svg-S4CnZ7ygnHX1GaRd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-S4CnZ7ygnHX1GaRd .icon-shape p,#mermaid-svg-S4CnZ7ygnHX1GaRd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-S4CnZ7ygnHX1GaRd .icon-shape .label rect,#mermaid-svg-S4CnZ7ygnHX1GaRd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-S4CnZ7ygnHX1GaRd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-S4CnZ7ygnHX1GaRd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-S4CnZ7ygnHX1GaRd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 硬件协议栈方案

F103 + W5500
软件协议栈方案

F407 + PHY
RMII
SPI 最高80MHz
STM32F407

CPU 执行 LwIP
MAC 内部外设
PHY 芯片

LAN8720A
网络变压器
STM32F103

仅做数据搬运
W5500

MAC+PHY+TCP/IP 引擎
内部网络变压器
以太网

方案对比要从成本和资源两个维度看:

对比维度 F407 + LAN8720A + LwIP F103 + W5500
MCU 成本 F407 约 F103 的 3~5 倍 F103 保持低价
协议栈 CPU 占用 高,TCP 重传/校验全在 CPU 极低,硬件自动处理
RAM 占用 LwIP 裸机约 15~25KB 起步 协议栈不吃 MCU RAM,仅 32KB 在 W5500 片内
代码复杂度 高,需移植 lwip 与网卡驱动 低,ioLibrary 封装好 Socket API
最大吞吐 实测约 9.4MB/s(F407 100M) 受 SPI 时钟限制,F103 约 1.3MB/s
适用场景 高吞吐网关、文件服务器 传感器上报、工业控制、小数据量联网

相关阅读:《LAN8720A 寄存器调试避坑指南》 介绍了软件协议栈路线里 PHY 芯片的寄存器配置细节,两篇对照着看,选型思路会更清楚。

这里要泼一盆冷水:W5500 不是万能的。它的 100Mbps 只是 PHY 链路速率,实际吞吐上限被 SPI 接口卡死------F103 的 SPI2 挂在 APB1 上,最高只能到 18MHz,理论峰值约 2.2MB/s,实测 TCP 应用层吞吐还要再打对折。如果你要做的是"嵌入式文件服务器"这类高吞吐场景,老老实实选 F407 + LwIP;如果是"把传感器数据稳定送到服务器",W5500 的简单可靠反而是巨大优势。

芯片内部结构与 32KB 缓冲的分配逻辑

W5500 内部可以看成三部分:SPI 接口模块、寄存器/缓冲存储阵列、TCP/IP 协议引擎。协议引擎由纯硬件逻辑门电路构成,它替你干了四件事:ARP 请求/应答、IP 分片重组、TCP 三次握手与确认重传、UDP 收发。MCU 侧只需要"配置 socket → 等待状态 → 写发送缓冲 → 读接收缓冲"。
#mermaid-svg-TrWV0vpjPBDw9dvG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-TrWV0vpjPBDw9dvG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-TrWV0vpjPBDw9dvG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-TrWV0vpjPBDw9dvG .error-icon{fill:#552222;}#mermaid-svg-TrWV0vpjPBDw9dvG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-TrWV0vpjPBDw9dvG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-TrWV0vpjPBDw9dvG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TrWV0vpjPBDw9dvG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TrWV0vpjPBDw9dvG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-TrWV0vpjPBDw9dvG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TrWV0vpjPBDw9dvG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TrWV0vpjPBDw9dvG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-TrWV0vpjPBDw9dvG .marker.cross{stroke:#333333;}#mermaid-svg-TrWV0vpjPBDw9dvG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TrWV0vpjPBDw9dvG p{margin:0;}#mermaid-svg-TrWV0vpjPBDw9dvG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-TrWV0vpjPBDw9dvG .cluster-label text{fill:#333;}#mermaid-svg-TrWV0vpjPBDw9dvG .cluster-label span{color:#333;}#mermaid-svg-TrWV0vpjPBDw9dvG .cluster-label span p{background-color:transparent;}#mermaid-svg-TrWV0vpjPBDw9dvG .label text,#mermaid-svg-TrWV0vpjPBDw9dvG span{fill:#333;color:#333;}#mermaid-svg-TrWV0vpjPBDw9dvG .node rect,#mermaid-svg-TrWV0vpjPBDw9dvG .node circle,#mermaid-svg-TrWV0vpjPBDw9dvG .node ellipse,#mermaid-svg-TrWV0vpjPBDw9dvG .node polygon,#mermaid-svg-TrWV0vpjPBDw9dvG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-TrWV0vpjPBDw9dvG .rough-node .label text,#mermaid-svg-TrWV0vpjPBDw9dvG .node .label text,#mermaid-svg-TrWV0vpjPBDw9dvG .image-shape .label,#mermaid-svg-TrWV0vpjPBDw9dvG .icon-shape .label{text-anchor:middle;}#mermaid-svg-TrWV0vpjPBDw9dvG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-TrWV0vpjPBDw9dvG .rough-node .label,#mermaid-svg-TrWV0vpjPBDw9dvG .node .label,#mermaid-svg-TrWV0vpjPBDw9dvG .image-shape .label,#mermaid-svg-TrWV0vpjPBDw9dvG .icon-shape .label{text-align:center;}#mermaid-svg-TrWV0vpjPBDw9dvG .node.clickable{cursor:pointer;}#mermaid-svg-TrWV0vpjPBDw9dvG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-TrWV0vpjPBDw9dvG .arrowheadPath{fill:#333333;}#mermaid-svg-TrWV0vpjPBDw9dvG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-TrWV0vpjPBDw9dvG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-TrWV0vpjPBDw9dvG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TrWV0vpjPBDw9dvG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-TrWV0vpjPBDw9dvG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TrWV0vpjPBDw9dvG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-TrWV0vpjPBDw9dvG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-TrWV0vpjPBDw9dvG .cluster text{fill:#333;}#mermaid-svg-TrWV0vpjPBDw9dvG .cluster span{color:#333;}#mermaid-svg-TrWV0vpjPBDw9dvG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-TrWV0vpjPBDw9dvG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-TrWV0vpjPBDw9dvG rect.text{fill:none;stroke-width:0;}#mermaid-svg-TrWV0vpjPBDw9dvG .icon-shape,#mermaid-svg-TrWV0vpjPBDw9dvG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TrWV0vpjPBDw9dvG .icon-shape p,#mermaid-svg-TrWV0vpjPBDw9dvG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-TrWV0vpjPBDw9dvG .icon-shape .label rect,#mermaid-svg-TrWV0vpjPBDw9dvG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TrWV0vpjPBDw9dvG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-TrWV0vpjPBDw9dvG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-TrWV0vpjPBDw9dvG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} W5500 内部
SPI 从机接口

Mode 0/3
寄存器与缓冲控制器

32KB SRAM
通用寄存器区

0x0000~0x002F
8 组 Socket 寄存器区

Sn_MR Sn_CR Sn_SR...
TX 缓冲 16KB

8 socket 按 TMSR 分配
RX 缓冲 16KB

8 socket 按 RMSR 分配
硬件 TCP/IP 引擎

TCP UDP ICMP ARP IGMP PPPoE
MAC
PHY 100Base-TX
STM32F103 SPI2
网络变压器

模块板载
RJ45

缓冲分配的坑藏在两个寄存器里:TMSR(0x001B)和 RMSR(0x001C),各占 2 字节,每个 socket 用 4 位(半个字节)表示缓冲大小。可选值只有五档:0001=1KB、0010=2KB、0100=4KB、1000=8KB、0000=16KB。注意 16KB 的编码是 0000 而不是 1111------我第一次按"值越大缓冲越大"的直觉写了 0xFF,结果所有 socket 的缓冲全乱套了。

4 位编码 缓冲大小 备注
0001 1KB 最小分配
0010 2KB 默认值
0100 4KB 常用
1000 8KB 大流量场景
0000 16KB 编码特殊,非 0xF

约束条件:所有 socket 的 TX 缓冲之和 ≤ 16KB,RX 之和 ≤ 16KB。如果某两个 socket 想各分 16KB,直接爆表,写进去的配置会被硬件忽略,表现就是那个 socket 收发完全无反应。这种"寄存器写成功但行为异常"的问题最难排查,后面故障排查里会专门讲。

SPI 帧格式:读懂 25 字节,等于读懂半颗芯片

一帧 SPI 传输的结构

W5500 的 SPI 帧与普通外设完全不同:先发 2 字节地址,再发 1 字节控制,最后才是数据。地址是 16 位的块内偏移,控制字节里藏着块选择、读写方向和传输模式。

字段 位数 说明
地址段 16 bit 块内偏移地址,先发高字节
控制段 bit7:3 5 bit BSB4:0 块选择,选中通用/Socket/TX/RX 区域
控制段 bit2 1 bit RWB:0=读,1=写(注意与常规相反)
控制段 bit1:0 2 bit 数据模式:00=变长(VDM),01/10/11=固定 1/2/4 字节

块选择位(BSB)是把 32KB 空间切成 25 个区域的钥匙:1 块通用寄存器、8 块 Socket 寄存器、8 块 TX 缓冲、8 块 RX 缓冲。例如读 Socket0 的模式寄存器 Sn_MR,地址就是 0x0000 加上 BSB=00101(Socket0 寄存器区),控制字节为 0b0010100_0。习惯上把这三段拼成一个 32 位整数传给 SPI 驱动,读写函数签名统一成:

c 复制代码
uint8_t wiz_read(uint32_t addr_bsb, uint8_t *buf, uint16_t len);
void   wiz_write(uint32_t addr_bsb, const uint8_t *buf, uint16_t len);

变长模式(VDM)是坑的重灾区 。VDM 模式下数据长度完全由 CS 低电平持续时间决定,没有长度字段------CS 拉低后发地址+控制,然后连续搬 N 个字节,再拉高 CS,硬件就把这 N 个字节当作一帧数据。如果代码里 CS 时序没配对(比如收发字节之间 GPIO 抖动、或者 SPI 时钟边沿与 CS 不同步),数据就会错位。固定长度模式(FDM)反而安全:发完控制字节后硬件自动按 1/2/4 字节截断。官方 ioLibrary 默认走 VDM,所以片选控制必须干净利落,一个 CS 周期内只允许一帧完整的读写

寄存器地图速查

把最常用的寄存器整理成一张表,调试时对照着查,比翻 120 页数据手册快得多:

寄存器 地址 读写 说明
MR 0x0000 R/W 模式寄存器,bit7=RST 软复位(写 1 复位)
SHAR 0x0009 R/W 6 字节 MAC 地址
SIPR 0x000F R/W 4 字节本机 IP
GAR / SUBR 0x0001 / 0x0005 R/W 网关 / 子网掩码
TMSR / RMSR 0x001B / 0x001C R/W TX / RX 缓冲分配(半字节编码)
PHYCFGR 0x002E R/W PHY 配置,bit3=RST、bit6:4=OPMODE(100=自动协商)
VERSIONR 0x0039 R 版本号,固定 0x04,通信自检用
Sn_MR 0x0000+0x0400×n R/W Socket n 模式,bit3:0=协议(0001=TCP,0010=UDP)
Sn_CR 0x0001+0x0400×n W 命令:01=OPEN、02=LISTEN、04=CONNECT、08=DISCON、10=CLOSE、20=SEND、40=RECV
Sn_SR 0x0003+0x0400×n R 状态:00=CLOSED、13=INIT、14=LISTEN、17=ESTABLISHED、1C=CLOSE_WAIT、22=UDP
Sn_IR 0x0002+0x0400×n R/W 中断标志,写 1 清除
Sn_PORT 0x0004+0x0400×n R/W 源端口
Sn_TX_FSR / Sn_RX_RSR 0x0020 / 0x0026+0x0400×n R TX 空闲字节数 / RX 已收字节数
Sn_TX_WR / Sn_RX_RD 0x0024 / 0x0028+0x0400×n R/W TX 写指针 / RX 读指针

注意 Socket 寄存器区的步长是 0x0400(1KB):Socket0 的 Sn_MR 在 0x0000,Socket1 的 Sn_MR 在 0x0400,依此类推。而 Sn_RX_RSR 这类"独立于 socket 之外的公共偏移"写法上容易看错,我在代码里统一用宏封装,避免手算偏移出错。

硬件选型与接线

选型清单

器件 型号/规格 备注
主控 STM32F103C8T6 最小系统板 蓝色药丸板即可
以太网模块 W5500 模块(板载网络变压器+RJ45) 优先选集成变压器的成品板
杜邦线 母对母,10~20cm SPI 走线越短越好
USB-TTL CH340 串口打印调试
路由器/交换机 任意家用路由器 电脑与板子同一网段

模块选购警告:W5500 模块市场上有"裸芯片板"和"带网络变压器成品板"两种。裸芯片板需要你自己画 RJ45 + 网络变压器的差分线,1:1 变压器绕组的中心抽头处理错了,link 灯死活不亮------强烈建议新手直接买成品板(如带 HR911105A 变压器座的型号),引脚直接引到 2.54mm 排针,插上就能用。

接线表

W5500 与 F103 之间只需 6 根线,比串口还少。我选 SPI2(挂在 APB1 上,引脚 PB12~PB15 与 SWD 的 SWCLK/SWDIO 不冲突,调试方便):

STM32F103C8T6 W5500 说明
PB13 (SPI2_SCK) SCLK 时钟
PB15 (SPI2_MOSI) MOSI 主出从入
PB14 (SPI2_MISO) MISO 主入从出
PB12 (GPIO 输出) SCS 片选,低有效
PA8 (GPIO 输出) RST 复位,低电平有效
3.3V / GND VCC / GND 模块必须 3.3V 供电

3.3V 供电是硬性要求。W5500 的 IO 电平是 3.3V 兼容,但部分模块板载了 5V 供电的 LDO,把 VCC 接到 5V 时,SCS/SCLK 等输入引脚会被内部钳位电路拉高到危险电平------F103 是 5V 耐受的,串口芯片不一定。稳妥做法:VCC 统一接 3.3V,模块若带稳压管也不会有问题。另外 INT 引脚(中断输出)可以悬空,本文章节用轮询方式处理事件,后面扩展方向里再说中断用法。

CubeMX 配置与容易遗漏的步骤

SPI2 配置(含寄存器位标注)

CubeMX 里 SPI2 的配置如下,每个选项我标注了对应的寄存器位:

  1. Mode:Full-Duplex Master(SPI_CR1 的 MSTR=1、BIDIMODE=0、RXONLY=0),选择全双工主机模式,W5500 是 SPI 从机,MISO 需要主机时钟驱动才能输出,必须全双工;
  2. Hardware NSS Signal:Disable(SPI_CR1 的 SSM=1、SSI=1),片选用普通 GPIO 手动控制------因为 W5500 的 VDM 变长模式要求 CS 与整帧传输严格同步,硬件 NSS 由外设自动拉低,无法保证"一帧一拉"的边界;
  3. Prescaler:8 (SPI_CR1 的 BR2:0=010,APB1 36MHz ÷ 8 = 4.5MHz),先用低速打通,再提速。这是调试期最重要的纪律:SPI 线上有杂散电容、杜邦线过长时,高频下数据会偶发错位,读 VERSIONR 一会儿 0x04 一会儿 0xFF,极易误判成芯片问题;
  4. Clock Polarity:Low / Clock Phase:1 Edge (CPOL=0、CPHA=0,即 SPI Mode 0)。W5500 支持 Mode 0 和 Mode 3 两种,官方 ioLibrary 默认按 Mode 0 配置。Mode 0 和 Mode 3 的 CPOL 不同(0 vs 1),如果驱动库和 CubeMX 配的不一致,读任何寄存器都返回 0xFF 或垃圾值,这是 W5500 头号新人坑;
  5. Data Size:8 Bits / First Bit:MSB First(SPI_CR1 的 DFF=0、LSBFIRST=0),地址段先发高字节。

相关阅读:《W5500 硬件协议栈深度解析:如何优化 STM32F103 的以太网通信性能》 对 SPI 时钟频率与帧结构的性能影响做了详细推导,提速阶段建议先读它。

容易遗漏的步骤:复位时序与 PHY 配置

这一步是工具链永远不会替你做的,缺了它,W5500 会"看起来活着、实际瘫痪"。STM32 上电后 W5500 不会自动复位到已知状态------它没有 POR 自复位逻辑的完整保证,手册要求主机侧主动执行硬复位:

c 复制代码
/* 硬复位时序:拉低 RST 至少 500us,再拉高,等待 150ms 让芯片完成初始化 */
void w5500_hard_reset(void)
{
    HAL_GPIO_WritePin(RST_GPIO_Port, RST_Pin, GPIO_PIN_RESET); /* 拉低 */
    HAL_Delay(1);    /* 保持低电平 ≥500us,实测 1ms 更稳 */
    HAL_GPIO_WritePin(RST_GPIO_Port, RST_Pin, GPIO_PIN_SET);   /* 释放 */
    HAL_Delay(150);  /* 手册要求复位后 ≥150ms 才能访问寄存器 */
}

遗漏的症状很隐蔽:如果你不主动复位,W5500 上电后寄存器里全是随机值,第一次读 VERSIONR 可能正常(硬件本身没问题),但 TCP 连接建立后行为完全随机。第二个遗漏点是 PHY 配置:W5500 的 PHY 默认处于复位模式(PHYCFGR bit3 RST=1 是上电默认),需要主机写 PHYCFGR 把它切到正常工作并启动自动协商:

c 复制代码
/* PHYCFGR: bit3 RST 写 1 复位 PHY(完成自动清零)
   bit6:4 OPMODE 设为 100 = 自动协商,链路速率由网线对端决定 */
uint8_t phy = 0x08;  /* RST=1 */
wiz_write_reg(0x002E, phy);   /* 触发 PHY 复位,硬件完成后 RST 自动清零 */
HAL_Delay(50);
phy = 0x04;                  /* OPMODE=100(自动协商) */
wiz_write_reg(0x002E, phy);

帧结构说明:这条写操作在 SPI 线上展开后是「地址 0x002E + 控制字节(BSB=00000 通用区、RWB=1 写、VDM 变长模式)+ 数据 0x08」,wiz_write_reg 封装了帧头拼接,你只需关心寄存器地址与值。

这两个步骤正是"工具不自动生成"的经典案例:CubeMX 只负责把 MCU 的 SPI 外设配好,W5500 是颗独立芯片,它的上电流程跟 MCU 毫无关系,必须由你的代码负责。漏掉复位时序的典型症状是"上电概率性失败、按一下板子复位键就好了"。

驱动代码实现

SPI 读写封装(帧格式落地)

c 复制代码
/* SPI2 句柄,由 CubeMX 生成 */
extern SPI_HandleTypeDef hspi2;

/* 片选控制:VDM 变长模式要求一帧一拉,CS 全程由软件掌控 */
static inline void w5500_cs_low(void)  { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); }
static inline void w5500_cs_high(void) { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); }

/**
 * W5500 SPI 单帧传输
 * @param addr_bsb 高 16 位地址 + BSB 块选择(调用方拼好)
 * @param rw       0=读 1=写(帧内 RWB 位)
 * @param buf      数据缓冲
 * @param len      数据长度(VDM 模式下由 CS 拉低时长决定)
 */
static void w5500_frame(uint16_t addr, uint8_t bsb, uint8_t rw, uint8_t *buf, uint16_t len)
{
    uint8_t header[3];

    header[0] = (uint8_t)(addr >> 8);      /* 地址高字节 */
    header[1] = (uint8_t)(addr & 0xFF);    /* 地址低字节 */
    header[2] = (uint8_t)((bsb << 3) | ((rw & 0x01) << 2) | 0x00); /* BSB + RWB + VDM */

    w5500_cs_low();                        /* 必须先拉低 CS 再发地址 */
    /* 关键顺序:地址+控制一次性发出,再搬运数据,全程 CS 不得抬起 */
    if (HAL_SPI_Transmit(&hspi2, header, 3, 100) != HAL_OK) {
        w5500_cs_high();
        return;                            /* 返回值检查:传输失败立即放弃本帧 */
    }
    if (rw == 0) {
        if (HAL_SPI_Receive(&hspi2, buf, len, 1000) != HAL_OK) {
            w5500_cs_high();
            return;      /* 返回值检查:接收失败同样放弃本帧,避免读到脏数据 */
        }
    } else {
        if (HAL_SPI_Transmit(&hspi2, buf, len, 1000) != HAL_OK) {
            w5500_cs_high();
            return;
        }
    }
    w5500_cs_high();                       /* 数据搬完才允许拉高 */
}
c 复制代码
/* 寄存器读写封装:BSB=00000 通用区,RWB 0=读 1=写 */
void wiz_write_reg(uint16_t addr, uint8_t val)
{
    w5500_frame(addr, 0x00, 1, &val, 1);
}

uint8_t wiz_read_reg(uint16_t addr)
{
    uint8_t val = 0;
    w5500_frame(addr, 0x00, 0, &val, 1);
    return val;
}

/* Socket 区读写:bsb = 0x05 + n(Socket n 寄存器区),步长 0x0400 */
void wiz_write_sn_reg(uint8_t n, uint16_t offset, uint8_t val)
{
    w5500_frame(offset, 0x05 + n, 1, &val, 1);
}

为什么 BSB 要这样拼 :W5500 把 32KB 空间按 0x0400 步长切成 25 块,BSB4:0 的编码是:00000=通用寄存器、0000101000=Socket07 寄存器、0100110000=Socket07 TX 缓冲、1000111000=Socket07 RX 缓冲。所以 Socket n 寄存器区 BSB = 0x05 + n,TX 缓冲区 BSB = 0x09 + n,RX 缓冲区 BSB = 0x11 + n。这个映射关系在数据手册 2.1 节有完整表格,写驱动前务必对照一遍,拼错 BSB 的后果是"读的是另一块内存",行为极其诡异。

初始化序列

c 复制代码
#define SOCKET0  0
#define MAX_BUF  2048

volatile uint16_t rx_packet_count = 0;  /* 中断与主循环共享,必须 volatile */

uint8_t w5500_init(void)
{
    uint8_t ver;

    /* 1. 硬复位(顺序注释:必须先复位再访问寄存器,否则寄存器值未定义) */
    w5500_hard_reset();

    /* 2. 通信自检:VERSIONR 固定返回 0x04 */
    ver = wiz_read_reg(0x0039);
    if (ver != 0x04) {
        return 0xFF;   /* SPI 配置或接线错误,返回错误码 */
    }

    /* 3. 网络参数:MAC/IP/网关/子网(通用区 BSB=0,RWB=1 写) */
    uint8_t mac[6] = {0x00, 0x08, 0xDC, 0x11, 0x22, 0x33};
    w5500_frame(0x0009, 0x00, 1, mac, 6);   /* SHAR 6 字节 MAC */
    uint8_t ip[4]  = {192, 168, 1, 100};
    w5500_frame(0x000F, 0x00, 1, ip, 4);    /* SIPR 本机 IP */
    uint8_t gw[4]  = {192, 168, 1, 1};
    w5500_frame(0x0001, 0x00, 1, gw, 4);    /* GAR 网关 */
    uint8_t mask[4] = {255, 255, 255, 0};
    w5500_frame(0x0005, 0x00, 1, mask, 4);  /* SUBR 子网掩码 */

    /* 4. PHY 配置:切出复位模式 + 自动协商 */
    uint8_t phy = 0x08;
    wiz_write_reg(0x002E, phy);
    HAL_Delay(50);
    phy = 0x04;
    wiz_write_reg(0x002E, phy);

    /* 5. 缓冲分配:Socket0 收发各 8KB(TMSR/RMSR 半字节编码 1000=8KB) */
    uint8_t tmsr = 0x80, rmsr = 0x80;
    wiz_write_reg(0x001B, tmsr);
    wiz_write_reg(0x001C, rmsr);

    return 0x00;
}

TCP Server:Socket 状态机轮询

W5500 的 socket 编程本质是"写命令 → 轮询状态 → 搬数据",每一步都不能跳。TCP Server 的完整生命周期:
#mermaid-svg-RUjR44yQNtnwwtdH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RUjR44yQNtnwwtdH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RUjR44yQNtnwwtdH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RUjR44yQNtnwwtdH .error-icon{fill:#552222;}#mermaid-svg-RUjR44yQNtnwwtdH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RUjR44yQNtnwwtdH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RUjR44yQNtnwwtdH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RUjR44yQNtnwwtdH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RUjR44yQNtnwwtdH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RUjR44yQNtnwwtdH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RUjR44yQNtnwwtdH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RUjR44yQNtnwwtdH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RUjR44yQNtnwwtdH .marker.cross{stroke:#333333;}#mermaid-svg-RUjR44yQNtnwwtdH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RUjR44yQNtnwwtdH p{margin:0;}#mermaid-svg-RUjR44yQNtnwwtdH defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-RUjR44yQNtnwwtdH g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-RUjR44yQNtnwwtdH g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-RUjR44yQNtnwwtdH g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-RUjR44yQNtnwwtdH g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-RUjR44yQNtnwwtdH g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-RUjR44yQNtnwwtdH .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-RUjR44yQNtnwwtdH .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-RUjR44yQNtnwwtdH .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-RUjR44yQNtnwwtdH .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-RUjR44yQNtnwwtdH .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-RUjR44yQNtnwwtdH .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-RUjR44yQNtnwwtdH .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-RUjR44yQNtnwwtdH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RUjR44yQNtnwwtdH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-RUjR44yQNtnwwtdH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RUjR44yQNtnwwtdH .edgeLabel .label text{fill:#333;}#mermaid-svg-RUjR44yQNtnwwtdH .label div .edgeLabel{color:#333;}#mermaid-svg-RUjR44yQNtnwwtdH .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-RUjR44yQNtnwwtdH .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-RUjR44yQNtnwwtdH .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-RUjR44yQNtnwwtdH .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-RUjR44yQNtnwwtdH .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-RUjR44yQNtnwwtdH .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RUjR44yQNtnwwtdH .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RUjR44yQNtnwwtdH #statediagram-barbEnd{fill:#333333;}#mermaid-svg-RUjR44yQNtnwwtdH .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RUjR44yQNtnwwtdH .cluster-label,#mermaid-svg-RUjR44yQNtnwwtdH .nodeLabel{color:#131300;}#mermaid-svg-RUjR44yQNtnwwtdH .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-RUjR44yQNtnwwtdH .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-RUjR44yQNtnwwtdH .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-RUjR44yQNtnwwtdH .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-RUjR44yQNtnwwtdH .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-RUjR44yQNtnwwtdH .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-RUjR44yQNtnwwtdH .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-RUjR44yQNtnwwtdH .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-RUjR44yQNtnwwtdH .note-edge{stroke-dasharray:5;}#mermaid-svg-RUjR44yQNtnwwtdH .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-RUjR44yQNtnwwtdH .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-RUjR44yQNtnwwtdH .statediagram-note text{fill:black;}#mermaid-svg-RUjR44yQNtnwwtdH .statediagram-note .nodeLabel{color:black;}#mermaid-svg-RUjR44yQNtnwwtdH .statediagram .edgeLabel{color:red;}#mermaid-svg-RUjR44yQNtnwwtdH #dependencyStart,#mermaid-svg-RUjR44yQNtnwwtdH #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-RUjR44yQNtnwwtdH .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-RUjR44yQNtnwwtdH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 上电默认
Sn_CR=OPEN\n(TCP 模式)
Sn_CR=LISTEN
收到 SYN\n硬件完成三次握手
对端发起 FIN
Sn_CR=CLOSE
Sn_CR=CLOSE
Sn_CR=CLOSE
Sn_CR=CLOSE
CLOSED
INIT
LISTEN
ESTABLISHED
CLOSE_WAIT

c 复制代码
uint8_t socket_tcp_server(uint8_t s, uint16_t port)
{
    uint8_t sr;

    /* 防越界:socket 编号只允许 0~7 */
    if (s > 7) return 0xFF;

    /* 1. 打开 socket(Sn_MR bit3:0 = 0001 TCP) */
    wiz_write_sn_reg(s, 0x0000, 0x01);      /* Sn_MR = TCP */
    wiz_write_sn_reg(s, 0x0001, 0x01);      /* Sn_CR = OPEN */
    HAL_Delay(5);
    sr = wiz_read_reg(0x0400 * s + 0x0003); /* Sn_SR */
    if (sr != 0x13) {                       /* SOCK_INIT */
        return sr;                          /* 失败:通常返回 0x00 说明没打开 */
    }

    /* 2. 绑定端口并监听 */
    wiz_write_sn_reg(s, 0x0004, port >> 8); /* Sn_PORT 高字节 */
    wiz_write_sn_reg(s, 0x0005, port & 0xFF);
    wiz_write_sn_reg(s, 0x0001, 0x02);      /* Sn_CR = LISTEN */
    HAL_Delay(5);
    return 0x00;
}
c 复制代码
/* 主循环:轮询 ESTABLISHED,收一帧回一帧(回显) */
void tcp_echo_task(uint8_t s)
{
    uint8_t  sr;
    uint16_t rx_size;
    uint8_t  buf[2048];

    sr = wiz_read_reg(0x0400 * s + 0x0003);
    if (sr != 0x17) return;   /* 非 ESTABLISHED 直接跳过,不阻塞主循环 */

    /* 读 Sn_RX_RSR 得到已收字节数(16 位寄存器,0x0026 存高字节、0x0027 存低字节) */
    rx_size = (uint16_t)wiz_read_reg(0x0400 * s + 0x0026) << 8;
    rx_size |= wiz_read_reg(0x0400 * s + 0x0027);
    if (rx_size == 0) return;

    /* 防越界:单次最多读 2KB,剩余下轮再处理 */
    if (rx_size > MAX_BUF) rx_size = MAX_BUF;

    /* 从 RX 缓冲读数据(Socket0 RX 区 BSB=0x11,地址 0x0000 起) */
    w5500_frame(0x0000, 0x11 + s, 0, buf, rx_size);
    /* 更新读指针 Sn_RX_RD 并发送 RECV 命令释放缓冲 */
    wiz_write_sn_reg(s, 0x0028, (uint8_t)(rx_size >> 8)); /* Sn_RX_RD 高字节 */
    wiz_write_sn_reg(s, 0x0029, (uint8_t)(rx_size & 0xFF));
    wiz_write_sn_reg(s, 0x0001, 0x40);                    /* Sn_CR = RECV */

    /* 回显:写入 TX 缓冲(Socket0 TX 区 BSB=0x09) */
    uint16_t free_size;
    /* Sn_TX_FSR 16 位寄存器,0x0020 存高字节、0x0021 存低字节 */
    free_size = (uint16_t)wiz_read_reg(0x0400 * s + 0x0020) << 8;
    free_size |= wiz_read_reg(0x0400 * s + 0x0021);
    if (free_size < rx_size) return;   /* TX 空间不足直接丢弃本轮回显 */

    w5500_frame(0x0000, 0x09 + s, 1, buf, rx_size);
    /* 更新写指针 Sn_TX_WR 并发送 SEND 命令 */
    wiz_write_sn_reg(s, 0x0024, (uint8_t)(rx_size >> 8)); /* Sn_TX_WR 高字节 */
    wiz_write_sn_reg(s, 0x0025, (uint8_t)(rx_size & 0xFF));
    wiz_write_sn_reg(s, 0x0001, 0x20);                    /* Sn_CR = SEND */

    rx_packet_count++;                  /* 统计用,volatile 变量 */
}

代码里几处细节对应前面讲的坑:读 Sn_RX_RSR / Sn_TX_FSR 必须按"0x0026/0x0020 高字节在前"的顺序拼 (这两个 16 位寄存器按手册约定,低地址存放高 8 位),很多例程把顺序写反,导致大包永远收不全;读完缓冲必须更新 Sn_RX_RD 并补发 RECV 命令,漏掉这一步 RX 缓冲永远不会释放,几轮之后缓冲占满、socket 假死,这是"TCP 连上后收几包就卡死"的最常见根因。

测试验证

测试环境

项目 配置
主控 STM32F103C8T6 @72MHz
SPI SPI2(APB1=36MHz),Mode 0,片选 GPIO 控制
固件 HAL 1.8.6 + ioLibrary 风格自研驱动(非官方库,便于演示帧结构)
对端 Windows 10 笔记本,iperf 3 / 自写 Python 客户端
网络 家用路由器,同一网段 192.168.1.x

基础连通性:ping 与 RTT

配置 IP 192.168.1.100 后,从电脑 ping 1000 个包:

指标 结果
丢包率 0/1000
平均 RTT 0.31 ms
最小/最大 RTT 0.21 ms / 1.4 ms
首包延迟 启动后第 2 秒内即通(含 ARP 解析)

相关阅读:《MINI STM32 移植 W5500 技术解析》 把 W5500 的移植框架拆得很清楚,如果你打算直接用官方 ioLibrary,可以先看这篇做整体认知。

量化参数对比一:SPI 时钟频率对 TCP 吞吐的影响

TCP 回显模式下,电脑端用 Python 分 1000 次发送 1KB 数据块,统计应用层有效吞吐。SPI 时钟是唯一变量:

SPI 时钟 分频 理论 SPI 峰值 TCP 回显吞吐 相对 4.5MHz
4.5 MHz /8 0.56 MB/s 0.36 MB/s 1.00×
9 MHz /4 1.13 MB/s 0.72 MB/s 2.00×
18 MHz /2 2.25 MB/s 1.31 MB/s 3.64×

18MHz 是 F103 的 SPI2 上限(APB1 36MHz ÷ 2),再往上就只能换 SPI1(APB2 72MHz ÷ 2 = 36MHz)。理论上 SPI1 可以推到 36MHz,但我实测 18MHz 之后吞吐不再线性增长------因为 TCP 回显是"收一帧回一帧"的串行模型,吞吐被 socket 状态机轮询和 ACK 往返时间锁住了,SPI 不再是唯一瓶颈。

理论 vs 实测对照一:SPI 理论带宽 vs 有效吞吐

参数 理论值 实测值 偏差 原因
SPI 峰值带宽(18MHz) 2.25 MB/s 1.31 MB/s -41.8% 每帧 3 字节帧头(16 位地址+8 位控制)开销 + socket 状态轮询 + TCP ACK 往返
数据有效率 100% 58.2% -41.8% 纯数据搬运只占 SPI 总线时间的约 6 成

这个对照很能说明问题:W5500 的 SPI 帧头开销是固定的,地址+控制段 3 字节,即使只读 1 字节数据也要搬 4 字节。小包场景下帧头占比更高,这也是为什么大块连续读写(比如 1KB 一包)比逐字节读写效率高得多------驱动层务必用"整块搬移"而不是"逐寄存器读写"

量化参数对比二:RX 缓冲大小对 UDP 丢包率的影响

UDP 场景:电脑以 10ms 间隔向板子连续发 1000 个 512 字节包,板子不回复只计数,统计应用层收到的包数。Socket0 的 RX 缓冲是唯一变量:

RX 缓冲 RMSR 编码 收到包数 丢包率 说明
2 KB 0010 968 3.2% 默认值,高速下明显丢包
8 KB 1000 996 0.4% 大部分场景够用
16 KB 0000 1000 0% 满载吞吐,代价是其他 socket 无缓冲可用

UDP 没有重传机制,丢包只有两个原因:RX 缓冲溢出(主循环没来得及收)或应用层来不及处理。实测里 2KB 缓冲丢 3.2% 不是偶然------512 字节包 + 帧头开销,2KB 只够装 3~4 包,主循环稍一抖动就溢出。把缓冲提到 16KB 后,1000 包零丢失。

理论 vs 实测对照二:以太网链路速率 vs 应用层吞吐

参数 理论值 实测值 偏差 原因
PHY 链路速率 100 Mbps 100 Mbps 0% 自动协商成功,链路满速
应用层 TCP 吞吐 ≤100 Mbps 10.5 Mbps(1.31MB/s) -89.5% SPI 总线是瓶颈:18MHz 理论峰值 18Mbps,再扣帧头与协议开销
UDP 单包往返延迟 --- 0.45 ms --- 反映 SPI 帧 + 硬件协议栈处理耗时

第二个对照说明了一个很多人误判的点:W5500 标称"100M 以太网"指的是 PHY 链路速率,不是你的应用吞吐。网上不少帖子抱怨"W5500 跑不满百兆",其实是没意识到 SPI 才是通道瓶颈。F103 的 18MHz SPI2 上限决定了这套组合的实用吞吐天花板约 1.3MB/s------对传感器上报、控制指令这类场景绰绰有余,但别指望它当文件服务器。

故障排查

以下 6 类问题是我从调试和社区帖子里总结的高频故障,每类都按"现象 → 排查链 → 根因 → 修复"展开。

1. 读 VERSIONR 恒为 0xFF

现象:初始化第一步读 0x0039 就失败,返回值 0xFF 或 0x00。

排查链 :先量 SCLK 引脚,确认 SPI 时钟确实在输出(示波器看 PB13 有没有方波,排除"外设没启动");再量 SCS 波形,确认 CS 每帧确实被拉低(排除"片选没控制住");排除硬件后回到软件------先查 SPI 模式:CubeMX 里 CPOL/CPHA 与驱动代码必须一致,W5500 用 Mode 0 或 Mode 3 都行,但混搭必然全 0xFF。我最初就是 CubeMX 配了 Mode 0,驱动里手滑写成 Mode 3,查了半小时。其次查 BSB 块选择位是否拼对,读通用寄存器区 BSB 必须为 00000。

根因 :SPI 模式不匹配或帧格式错误。修复 :统一为 Mode 0(CPOL=0, CPHA=0),对照数据手册 2.3 节核对帧结构。验证:读 VERSIONR 稳定返回 0x04,连续读 100 次无异常。

2. 寄存器能读,但 socket 进不了 LISTEN

现象:OPEN 命令后 Sn_SR 一直是 0x00(SOCK_CLOSED),发 LISTEN 命令毫无反应。

排查链 :这个现象我有亲身经历------第一天就卡在这。先读 Sn_SR 确认 OPEN 是否生效,然后怀疑 Sn_MR 写错了 :Socket 寄存器区 BSB 是 0x05+n,偏移 0x0000 是 Sn_MR,我最初把"Socket n 寄存器区偏移 0x0000"错当成"绝对地址 0x0000",直接写了通用区的 MR 寄存器,等于给芯片做了个软复位。用读回验证法:写完 Sn_MR 立刻读回来对比,能立刻暴露"写到了错误地址"。

根因 :Socket 寄存器区寻址错误(BSB 拼错)或 Sn_MR 协议字段写错(TCP 必须是 0x01)。修复 :用 0x0400×n + 偏移 的绝对地址 + 正确的 BSB,写后读回自检。验证:OPEN 后 Sn_SR=0x13(SOCK_INIT),LISTEN 后 0x14(SOCK_LISTEN)。

3. TCP 连上后收几包就卡死

现象:电脑能连上 5000 端口,收发前几包正常,之后完全无响应。

排查链 :先确认不是对端问题(换 Python 重连测试排除);重点查 RX 缓冲是否耗尽 :打印 Sn_RX_RSR,如果持续增长到缓冲上限就不动了,说明"读走了数据但没释放缓冲"。再查代码------是否漏了"更新 Sn_RX_RD + 发 RECV 命令"两步。W5500 的 RX 缓冲是"读走数据后必须主动更新读指针并通知硬件",漏掉 RECV 命令,硬件以为缓冲还是满的,新包进不来。

根因 :接收流程不完整,RX 缓冲被"读而不放"填满。修复 :完整执行"读 Sn_RX_RSR → 搬数据 → 更新 Sn_RX_RD → 发 Sn_CR_RECV"四步。验证:长跑 1 万包收发,Sn_RX_RSR 始终在低位波动。

4. 上电后概率性失败,按复位键就恢复

现象:冷启动时偶发连不上网,手动按板子复位键后一切正常。

排查链 :这是最典型的"遗漏复位时序"症状。先假设是上电时序问题 :用示波器抓 RST 引脚波形和 VCC 上电波形,看 W5500 是否在电源稳定前被释放复位。再看代码------硬复位是否在 SPI 初始化之后才执行 、RST 拉低是否满足 ≥500us。排除法验证:在 main 开头人为加 200ms 延时再跑 w5500_init,若故障消失,确认是时序问题。

根因 :W5500 上电寄存器未初始化(无完整 POR 保证),复位时序不满足或复位太早。修复 :严格按"电源稳定 → 拉低 RST ≥500us → 拉高 → 等待 ≥150ms → 访问寄存器"执行。验证:连续冷启动 20 次,零失败。

5. UDP 高负载下丢包

现象:UDP 发包频率一高就丢包,频率降下来就正常。

排查链 :先确认丢包方向------板子收还是发。收方向 :打印 Sn_RX_RSR 看是否冲到缓冲上限(丢包 = 缓冲溢出),用前面的参数扫描实验定位缓冲需求;发方向:打印 Sn_TX_FSR,若频繁为 0 说明发送缓冲不足,主循环写缓冲时阻塞了应用逻辑。

根因 :RX/TX 缓冲分配不足,或主循环收包不及时。修复 :按实际包速率计算所需缓冲(包大小 × 峰值积压包数),通过 RMSR/TMSR 扩容;必要时把收包逻辑放进定时中断 ,别让主循环里耗时操作拖慢收包。验证:1000 包场景丢包率归零。

6. 更换网络环境后连不上

现象:在家能通,拿到客户现场就连不上,或换路由器后失效。

排查链 :先看路由器网段------W5500 的 IP/网关/掩码是写死在固件里的 (本文用静态 IP 192.168.1.100),换了网段就得改代码重烧。再查 DHCP 需求:W5500 本身不提供 DHCP 服务,需要 MCU 侧用 UDP 实现 DHCP 客户端(官方 ioLibrary 里有 dhcp.c)。最后看 link 状态:读 PHYCFGR bit0(LINK),若为 0 说明物理链路都没建立,查网线、交换机和 PHY 配置。

根因 :静态 IP 与环境网段不匹配,或链路未建立。修复 :产品化必须上 DHCP 或可配置 IP(存 Flash),开发调试用静态 IP 即可。验证:改网关后 DHCP 获取成功,ping 通。

相关阅读:《STM32F4+W5500 移植与开发常见问题》 整理了更多 W5500 移植期的高频问题,尤其是缓冲分配和中断处理部分,值得收藏对照。

总结

核心要点回顾

  1. W5500 把 MAC、PHY、TCP/IP 协议栈全部硬件化,MCU 只做 SPI 数据搬运,F103 这类无 MAC 芯片也能低成本联网;
  2. SPI 帧 = 16 位地址 + 8 位控制(BSB/RWB/模式)+ 数据,块选择位决定访问哪块区域,拼错 BSB 等于访问错误内存;
  3. 上电必须执行硬复位时序(≥500us + 150ms),并主动配置 PHYCFGR 脱离复位模式,这两步工具不会自动生成;
  4. TCP 收发有固定四步流程(读 RSR → 搬数据 → 更新 RD → 发 RECV),漏一步就缓冲耗尽假死;
  5. 吞吐瓶颈在 SPI 而非以太网链路:F103 + SPI2 的实用 TCP 吞吐约 1.3MB/s,选型前先算清带宽需求。

适用边界

这套方案最适合小数据量、高可靠、强实时要求的联网场景(传感器上报、设备控制、Modbus TCP 网关)。不适合高吞吐场景(文件传输、视频流),也不适合需要复杂协议(HTTPS 的 TLS 需要 MCU 侧软件实现,会吃掉硬件协议栈的红利)。

局限性与已知问题

W5500 的硬件协议栈对 TCP 的实现是"够用但固定"的:不支持 SACK、窗口缩放等高级选项,TCP 拥塞控制是简化的;8 个 socket 共享 32KB 缓冲,多 socket 大流量会互相挤占;PHY 的自动协商偶发失败(实测约 0.3%),需要软件定时检查 LINK 位并触发重协商。

扩展方向

下一步可以尝试:DMA + SPI 搬运数据,把 F103 从逐字节搬运中解放出来;利用 INT 引脚做中断驱动收包,替代主循环轮询;在 ioLibrary 基础上叠加 DHCP 与 DNS,做成即插即用的产品级固件;更进一步,把 W5500 与 FreeRTOS 结合,每个 socket 一个任务,构造多路并发的工业网关。

本文完整工程代码(含帧格式驱动、TCP/UDP 例程与故障排查辅助打印)可在 CSDN 下载频道 获取(VIP 免费)。

参考资料

  1. WIZnet W5500 Datasheet v1.0.9 --- 寄存器地图(2.3/2.4 节)、PHYCFGR(4.2 节)
  2. WIZnet ioLibrary_Driver GitHub 仓库 --- Socket API 参考实现
  3. 《MINI STM32 移植 W5500 技术解析》
  4. 《W5500 硬件协议栈深度解析:如何优化 STM32F103 的以太网通信性能》
  5. 《STM32F4+W5500 移植与开发常见问题》
  6. 《LAN8720A 寄存器调试避坑指南》 --- 软件协议栈方案(外部 PHY)对照

版本备注

  • 硬件平台:STM32F103C8T6(72MHz,64KB Flash)+ W5500 以太网模块(板载网络变压器)
  • 软件版本:STM32CubeMX 6.12 + STM32CubeF1 HAL 库 1.8.6 + 自研 W5500 驱动(帧格式与官方 ioLibrary 兼容)
  • 兼容说明:驱动代码可直接用于 F103 全系,仅需按板卡调整 SPI 引脚与时钟分频;F4/L4 系列需修改 SPI 外设时钟频率(F4 可上 21MHz 以上)与引脚映射
  • API 变更风险:HAL 1.8.x 与 1.6.x 的 HAL_SPI_Transmit/Receive 函数签名一致;若改用官方 ioLibrary,wizchip_init 在 1.0.x 与 1.1.x 之间的参数类型(uint8_t* 与 uint16_t*)有差异,移植时需核对
相关推荐
Zhang.Yi3 小时前
STM32-SPI
stm32·单片机·嵌入式硬件
en.en..3 小时前
网络分层封装 与 TCP粘包的因果关系
网络·单片机·嵌入式硬件
天空'之城3 小时前
单片机基础核心知识点汇总(三十九)
单片机·嵌入式硬件
mk0153 小时前
电动自行车汽车工业系统电机驱动100V高压恒定导向芯片进口替代
驱动开发·单片机·硬件工程·智能硬件·pcb工艺
爱吃汽的小橘4 小时前
移位运算hao
嵌入式硬件
逐步前行4 小时前
ESP32-S3-FreeRTOS-同步互斥(IPC)
单片机
无敌贵点大王5 小时前
RTThread学习记录10——C语言实现面向对象的编程
c语言·stm32·学习·链表
单片机仿真设计6 小时前
【proteus仿真】基于 STM32F103 的汽车温度监测系统(仿真图+程序)
stm32·单片机·嵌入式硬件·proteus·课设
anew___6 小时前
舵机——让 Arduino 动起来
c++·stm32·单片机·嵌入式硬件·c