文章目录
-
- 摘要
- 前言
- [W5500 是什么:把协议栈搬进硅片](#W5500 是什么:把协议栈搬进硅片)
-
- 硬件协议栈与软件协议栈的路线之争
- [芯片内部结构与 32KB 缓冲的分配逻辑](#芯片内部结构与 32KB 缓冲的分配逻辑)
- [SPI 帧格式:读懂 25 字节,等于读懂半颗芯片](#SPI 帧格式:读懂 25 字节,等于读懂半颗芯片)
-
- [一帧 SPI 传输的结构](#一帧 SPI 传输的结构)
- 寄存器地图速查
- 硬件选型与接线
- [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 不是"网卡",而是一台需要按状态机伺候的"网络协处理器"------命令寄存器要写、状态寄存器要轮询、收发缓冲有独立的指针体系,每一步都有时序讲究。
读完本文,你将掌握:
- W5500 内部结构与"硬件协议栈 vs 软件协议栈"的选型逻辑;
- SPI 帧格式(16 位地址 + 8 位控制 + 数据)与三块寄存器区(通用、Socket、收发缓冲)的寻址方式;
- CubeMX 配置中每个选项对应的寄存器位,以及工具不会自动生成的复位时序与 PHY 配置;
- TCP Server 的 Socket 状态机完整流转与常见卡死点的排查方法;
- 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 的配置如下,每个选项我标注了对应的寄存器位:
- Mode:Full-Duplex Master(SPI_CR1 的 MSTR=1、BIDIMODE=0、RXONLY=0),选择全双工主机模式,W5500 是 SPI 从机,MISO 需要主机时钟驱动才能输出,必须全双工;
- Hardware NSS Signal:Disable(SPI_CR1 的 SSM=1、SSI=1),片选用普通 GPIO 手动控制------因为 W5500 的 VDM 变长模式要求 CS 与整帧传输严格同步,硬件 NSS 由外设自动拉低,无法保证"一帧一拉"的边界;
- Prescaler:8 (SPI_CR1 的 BR2:0=010,APB1 36MHz ÷ 8 = 4.5MHz),先用低速打通,再提速。这是调试期最重要的纪律:SPI 线上有杂散电容、杜邦线过长时,高频下数据会偶发错位,读 VERSIONR 一会儿 0x04 一会儿 0xFF,极易误判成芯片问题;
- 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 头号新人坑;
- 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 移植期的高频问题,尤其是缓冲分配和中断处理部分,值得收藏对照。
总结
核心要点回顾
- W5500 把 MAC、PHY、TCP/IP 协议栈全部硬件化,MCU 只做 SPI 数据搬运,F103 这类无 MAC 芯片也能低成本联网;
- SPI 帧 = 16 位地址 + 8 位控制(BSB/RWB/模式)+ 数据,块选择位决定访问哪块区域,拼错 BSB 等于访问错误内存;
- 上电必须执行硬复位时序(≥500us + 150ms),并主动配置 PHYCFGR 脱离复位模式,这两步工具不会自动生成;
- TCP 收发有固定四步流程(读 RSR → 搬数据 → 更新 RD → 发 RECV),漏一步就缓冲耗尽假死;
- 吞吐瓶颈在 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 免费)。
参考资料
- WIZnet W5500 Datasheet v1.0.9 --- 寄存器地图(2.3/2.4 节)、PHYCFGR(4.2 节)
- WIZnet ioLibrary_Driver GitHub 仓库 --- Socket API 参考实现
- 《MINI STM32 移植 W5500 技术解析》
- 《W5500 硬件协议栈深度解析:如何优化 STM32F103 的以太网通信性能》
- 《STM32F4+W5500 移植与开发常见问题》
- 《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*)有差异,移植时需核对