文章目录
摘要
工业网关、数据采集终端这类设备经常需要不依赖无线环境的有线联网方案,而STM32上跑软件协议栈lwIP又面临Flash/RAM占用大、调试门槛高的现实问题。本文基于STM32F103C8T6与W5500硬件TCP/IP协议栈,通过SPI1接口实现TCP服务端回环通信,并构建了覆盖网线热插拔、对端异常断电两种场景的断线检测与自动重连状态机。实测:SPI时钟9MHz下TCP吞吐4.38 Mbps,ICMP响应平均1.8 ms,对端异常断电后1.71 s内检测到连接失效,网线热插拔20次全部在2.1 s内恢复监听。文中给出变长SPI帧格式、TX指针回绕机制的寄存器级拆解,以及6类典型故障的完整排查链。
前言
给单片机加联网能力的方案远不止一种,选型阶段我在三个方向之间犹豫了很久:用带以太网MAC的F407外接PHY芯片跑lwIP,用ESP8266/ESP32这类WiFi模组走无线,或者用W5500这种把协议栈做进硬件的专用芯片。三条路的开发体验差异很大,我把它整理成下面这张对比表。
| 对比维度 | W5500 硬件协议栈 | F407内置MAC + lwIP | ESP8266 WiFi |
|---|---|---|---|
| 协议栈载体 | 芯片内硬件逻辑 | MCU软件栈(占Flash约45KB/RAM约16KB) | 模组固件 |
| MCU资源占用 | 仅SPI接口,CPU占用<5% | ETH外设+Pbuf管理,CPU占用10%~20% | AT指令/透传,占用串口 |
| TCP吞吐上限 | 受SPI时钟限制(实测约4~5 Mbps) | 可达40+ Mbps | 1~3 Mbps且波动大 |
| 开发调试难度 | 寄存器少,状态机直观 | lwIP API+内存池调优,门槛高 | AT时序/掉线重连黑盒多 |
| 适用场景 | 工业现场、有线可靠联网 | 高吞吐需求(视频/大文件) | 免布线、消费类设备 |
我的应用是工业采集终端,需要抗干扰、延迟稳定的有线链路,对吞吐要求反而不高------W5500的短板(吞吐受限)正好落在可接受区间,而它的长处(协议栈免维护、CPU零负担)正是工业场景最看重的。于是定了方案:STM32F103C8T6 + W5500模块(板载HR911105A带网络变压器的RJ45一体座)。
本文目标有三个:一是把W5500的变长SPI帧格式和TX/RX指针回绕机制讲到寄存器级,这两块是所有坑的根源;二是给出TCP服务端的完整实现和吞吐实测,定位清楚SPI时钟与TCP吞吐的关系;三是实现一套能同时应对物理断线和静默掉线的重连状态机。前置条件:读者需要熟悉STM32 HAL库开发流程和SPI基础,有一块W5500模块和网线即可复现。
相关阅读:《STM32F407+W5500以太网实战:HAL库驱动移植与TCP通信》 --- 了解WIZnet官方ioLibrary驱动在F4平台的移植思路,与本文裸机自实现可以互相印证。
本文完整工程代码可在 CSDN 下载频道 获取(VIP 免费)。
W5500的核心机制:把TCP/IP做进硬件
W5500和普通SPI外设最大的区别在于:它不是"被动应答寄存器读写"的外设,而是一台独立的网络协处理器。TCP的拆包、确认、重传、滑动窗口全部由芯片内部逻辑完成,MCU要做的事情被压缩到三件------配置网络参数、创建/管理Socket、在Socket缓冲区里搬数据。理解了这一点,很多"奇怪现象"就都能解释了。
整体数据通路如下:
#mermaid-svg-i4fBfZSNGojbFJA3{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-i4fBfZSNGojbFJA3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-i4fBfZSNGojbFJA3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-i4fBfZSNGojbFJA3 .error-icon{fill:#552222;}#mermaid-svg-i4fBfZSNGojbFJA3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-i4fBfZSNGojbFJA3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-i4fBfZSNGojbFJA3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-i4fBfZSNGojbFJA3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-i4fBfZSNGojbFJA3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-i4fBfZSNGojbFJA3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-i4fBfZSNGojbFJA3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-i4fBfZSNGojbFJA3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-i4fBfZSNGojbFJA3 .marker.cross{stroke:#333333;}#mermaid-svg-i4fBfZSNGojbFJA3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-i4fBfZSNGojbFJA3 p{margin:0;}#mermaid-svg-i4fBfZSNGojbFJA3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-i4fBfZSNGojbFJA3 .cluster-label text{fill:#333;}#mermaid-svg-i4fBfZSNGojbFJA3 .cluster-label span{color:#333;}#mermaid-svg-i4fBfZSNGojbFJA3 .cluster-label span p{background-color:transparent;}#mermaid-svg-i4fBfZSNGojbFJA3 .label text,#mermaid-svg-i4fBfZSNGojbFJA3 span{fill:#333;color:#333;}#mermaid-svg-i4fBfZSNGojbFJA3 .node rect,#mermaid-svg-i4fBfZSNGojbFJA3 .node circle,#mermaid-svg-i4fBfZSNGojbFJA3 .node ellipse,#mermaid-svg-i4fBfZSNGojbFJA3 .node polygon,#mermaid-svg-i4fBfZSNGojbFJA3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-i4fBfZSNGojbFJA3 .rough-node .label text,#mermaid-svg-i4fBfZSNGojbFJA3 .node .label text,#mermaid-svg-i4fBfZSNGojbFJA3 .image-shape .label,#mermaid-svg-i4fBfZSNGojbFJA3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-i4fBfZSNGojbFJA3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-i4fBfZSNGojbFJA3 .rough-node .label,#mermaid-svg-i4fBfZSNGojbFJA3 .node .label,#mermaid-svg-i4fBfZSNGojbFJA3 .image-shape .label,#mermaid-svg-i4fBfZSNGojbFJA3 .icon-shape .label{text-align:center;}#mermaid-svg-i4fBfZSNGojbFJA3 .node.clickable{cursor:pointer;}#mermaid-svg-i4fBfZSNGojbFJA3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-i4fBfZSNGojbFJA3 .arrowheadPath{fill:#333333;}#mermaid-svg-i4fBfZSNGojbFJA3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-i4fBfZSNGojbFJA3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-i4fBfZSNGojbFJA3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-i4fBfZSNGojbFJA3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-i4fBfZSNGojbFJA3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-i4fBfZSNGojbFJA3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-i4fBfZSNGojbFJA3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-i4fBfZSNGojbFJA3 .cluster text{fill:#333;}#mermaid-svg-i4fBfZSNGojbFJA3 .cluster span{color:#333;}#mermaid-svg-i4fBfZSNGojbFJA3 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-i4fBfZSNGojbFJA3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-i4fBfZSNGojbFJA3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-i4fBfZSNGojbFJA3 .icon-shape,#mermaid-svg-i4fBfZSNGojbFJA3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-i4fBfZSNGojbFJA3 .icon-shape p,#mermaid-svg-i4fBfZSNGojbFJA3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-i4fBfZSNGojbFJA3 .icon-shape .label rect,#mermaid-svg-i4fBfZSNGojbFJA3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-i4fBfZSNGojbFJA3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-i4fBfZSNGojbFJA3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-i4fBfZSNGojbFJA3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 100BASE-TX
SPI1 @ 9MHz
CS/SCK/MOSI/MISO
PC / 上位机
192.168.1.100
交换机
或直连网线
RJ45 + 网络变压器
HR911105A
W5500
硬件TCP/IP协议栈
MAC + PHY + 8路Socket
16KB TX / 16KB RX 缓冲池
STM32F103C8T6
Cortex-M3 @ 72MHz
USART1
调试日志输出
ICMP应答(ping)完全由W5500硬件完成,MCU甚至感知不到有ping包进来------这就是为什么ping延迟可以低到毫秒级且和MCU负载无关。
寄存器模型与SPI帧格式
W5500把存储空间分成三类块:通用寄存器(网络参数、PHY状态)、8路Socket各自的寄存器区、8路Socket各自的收发缓冲区。MCU通过SPI帧访问这些空间,而块选择全靠SPI帧里的1字节控制段,这是W5500与常见SPI外设(如W25Q64先发指令再发地址)最大的结构差异。
变长数据模式(Variable Data Length Mode)下的SPI帧格式为:控制段1字节 + 地址段2字节(大端)+ 数据段N字节。控制段结构如下:
| 位域 | 名称 | 说明 |
|---|---|---|
| Bit7:4 | BSB | 块选择:0x0=通用寄存器,Socket n寄存器区=4n+1,Socket n TX缓冲=4n+2,Socket n RX缓冲=4n+3 |
| Bit3 | RWB | 0=写,1=读 |
| Bit2:0 | OM | 0b00=变长模式,同一片选内地址自动递增 |
这就是一个必须讲清楚的配置级决策:读Socket 0的RX缓冲和控制读Socket 0的寄存器,地址可能完全相同,但BSB不同就指向完全不同的物理空间。我在封装驱动时定义了统一的块编号宏,避免手工拼BSB出错:
c
/* 块编号(非移位值),控制段 = (块编号 << 4) | 读写标志 */
#define BSB_COMMON(n) (0x0 + (n)) /* 通用寄存器,n恒为0 */
#define BSB_SOCK_REG(n) (0x1 + (n) * 4) /* Socket n 寄存器区 */
#define BSB_SOCK_TXBUF(n) (0x2 + (n) * 4) /* Socket n 发送缓冲 */
#define BSB_SOCK_RXBUF(n) (0x3 + (n) * 4) /* Socket n 接收缓冲 */
#define CTRL_READ 0x08 /* RWB=1,OM=00 变长模式 */
#define CTRL_WRITE 0x00 /* RWB=0,OM=00 变长模式 */
读写函数把BSB编码进控制段,一次片选拉低完成"控制段+地址段+N字节数据"的整帧传输,地址在片选保持期间自动递增:
c
void W5500_ReadBurst(uint8_t block, uint16_t addr, uint8_t *buf, uint16_t len)
{
uint8_t ctrl = (uint8_t)((block << 4) | CTRL_READ);
HAL_GPIO_WritePin(W5500_CS_GPIO_Port, W5500_CS_Pin, GPIO_PIN_RESET);
SPI1_TxRxByte((uint8_t)(addr >> 8)); /* 地址高字节 */
SPI1_TxRxByte((uint8_t)(addr & 0xFF)); /* 地址低字节 */
SPI1_TxRxByte(ctrl); /* 控制段 */
for (uint16_t i = 0; i < len; i++) {
buf[i] = SPI1_TxRxByte(0xFF); /* 全双工读:MOSI填哑元 */
}
HAL_GPIO_WritePin(W5500_CS_GPIO_Port, W5500_CS_Pin, GPIO_PIN_SET);
}
通用寄存器里最常用的几个:SHAR(0x0009,源MAC地址6字节)、GAR(0x0001,网关)、SUBR(0x0005,子网掩码)、SIPR(0x000F,本机IP)、PHYCFGR(0x002E,PHY链路状态)、VERSIONR(0x0039,恒读0x04,用于SPI链路自检)。Socket寄存器区按Socket n基址 = n×0x0400排布,每个Socket内部偏移固定:Sn_MR(0x0000,协议模式,bit3:0=0b0001为TCP)、Sn_CR(0x0001,命令寄存器,OPEN=0x01/LISTEN=0x02/CONNECT=0x04/CLOSE=0x10/SEND=0x20/RECV=0x40)、Sn_SR(0x0003,状态寄存器)、Sn_TX_FSR(0x0020,发送缓冲空闲字节数)、Sn_TX_WR(0x0024,发送写指针)、Sn_RX_RSR(0x0026,接收缓冲已存字节数)、Sn_RX_RD(0x0028,接收读指针)。
TX/RX指针回绕:理解不了的坑都源自这里
W5500的Socket缓冲区访问机制是它最"反直觉"的设计。Sn_TX_WR和Sn_RX_RD都是16位自由累加指针------芯片不会把它们限制在缓冲区大小范围内,而是让它们从0x0000一路加到0xFFFF再回绕到0x0000。真正映射到物理缓冲区时才做掩码:
物理偏移 = 指针值 & (缓冲区分配大小 × 1024 − 1)
以Socket 0分配2KB TX缓冲为例:掩码0x07FF,当Sn_TX_WR走到0x0800时,物理偏移回到0x0000------数据从缓冲区头部继续写入,而指针本身保留完整的累计值。收发双方的数据量对账不靠指针绝对值,靠的是回绕安全的差值运算 :(uint16_t)(wr - rd)。C语言的无符号溢出规则保证了这个差值在指针回绕时依然正确,前提是差值本身不超过65535。
这套机制的好处是芯片内部逻辑简单、收发计数天然对账;代价是驱动作者一旦试图"优化"------比如用有符号数算差值、或把指针截断到缓冲区大小------就会踩进深坑。后面失败路径一节会展开一个我亲手踩进去的案例。
相关阅读:《W5500以太网芯片实战避坑指南:PHY匹配、寄存器状态机与MII时序精调》 --- 对Sn_MR写入时序与状态机未定义分支的分析很细,值得对照阅读。
硬件连接与复位时序
接线沿用最小系统方案,SPI1引脚与多数W25Q64工程兼容,方便复用调试环境:
| W5500模块引脚 | STM32F103C8T6 | 说明 |
|---|---|---|
| SCK | PA5 | SPI1_SCK |
| MOSI | PA7 | SPI1_MOSI |
| MISO | PA6 | SPI1_MISO |
| SCSn | PA4 | 片选(软件控制) |
| RSTn | PB0 | 硬件复位,必须接 |
| INTn | 悬空 | 本文用轮询,不接中断 |
| 3V3 / GND | 3.3V / GND | 严禁接5V |
有两个工具链不会替你做、但漏了就起不来的步骤,单独说明。
第一步是复位时序。RSTn拉低至少500ns后释放(我实测拉低10ms更稳),释放后W5500内部PLL需要时间锁定,数据手册给出的建议是等待150ms。我的实测是:释放后立即读VERSIONR,10次里6次读到0x00或0xFF;固定等待200ms后,30次全部读到0x04。所以驱动里做成了"等待+重试确认"的双重保险:
c
int8_t W5500_HardwareReset(void)
{
HAL_GPIO_WritePin(W5500_RST_GPIO_Port, W5500_RST_Pin, GPIO_PIN_RESET);
HAL_Delay(10); /* 远大于500ns,留足余量 */
HAL_GPIO_WritePin(W5500_RST_GPIO_Port, W5500_RST_Pin, GPIO_PIN_SET);
for (uint8_t retry = 0; retry < 3; retry++) {
HAL_Delay(200); /* PLL锁定,实测150ms内偶有未稳 */
uint8_t ver = 0;
W5500_ReadBurst(BSB_COMMON(0), VERSIONR, &ver, 1);
if (ver == 0x04) return 0; /* 0x04是W5500固定版本号 */
}
return -1; /* SPI链路或模块本身异常 */
}
如果重试3次仍读不到0x04,问题一定在SPI链路或供电上,不必再往下调网络参数------这也是后面故障排查第1类的判定锚点。
第二步是PHY链路等待。复位成功只代表MCU↔W5500通了,W5500↔交换机之间还要完成100BASE-TX自协商。轮询PHYCFGR(0x002E)的bit0(LNK位)直到为1,才能认为物理层就绪。我用的是1秒超时+循环等待,避免没插网线时死等。
还有个极容易忽略的细节:MAC地址首字节的bit0必须是0。以太网规定首字节最低位为1是组播地址,交换机会直接丢弃来自组播源MAC的帧------症状就是版本号正常、LNK为1,但ping死活不通。自定MAC推荐用02:xx:xx:xx:xx:xx开头(02=本地管理+单播),或直接用WIZnet的OUI前缀00:08:DC。
SPI时钟选型:为什么9MHz是最优解
W5500手册标注SPI接口上限33.3MHz,按这个数字,72MHz的F103似乎可以直接用SPI1的4分频(18MHz)。但模块能不能跑到上限,取决于你的物理连接质量。我没有直接照抄手册上限,而是做了一组分频扫描。
先说CubeMX配置。SPI1配Full-Duplex Master,Prescaler按扫描结果选,CPOL=Low/CPHA=1Edge(即模式0,对应SPI_CR1寄存器CPOL=0、CPHA=0),软件片选(PA4配GPIO_Output),NSS模式选Disable。其他保持默认即可,W5500的SPI时序对模式0和模式3都兼容,但两种模式不能在通信中途切换。
分频扫描结果如下(每档传输10MB回环数据,PC端用iperf风格的自写脚本统计):
| APB2分频 | SPI时钟 | TCP平均吞吐 | 稳定性表现 |
|---|---|---|---|
| /64 | 1.125 MHz | 0.51 Mbps | 稳定 |
| /32 | 2.25 MHz | 1.05 Mbps | 稳定 |
| /16 | 4.5 MHz | 2.21 Mbps | 稳定 |
| /8 | 9 MHz | 4.38 Mbps | 稳定 |
| /4 | 18 MHz | 6.12→1.7 Mbps | 大数据块偶发错位,10MB传输3次失败2次 |
18MHz档的失败很有意思:短接线(5cm排针直连)时能稳定跑完,换成20cm杜邦线后MISO波形用逻辑分析仪看出现了明显的上升沿振铃,个别字节采样错位。这印证了一个经验------W5500手册的33.3MHz上限是芯片引脚级指标,模块级实现(排线、走线、网络变压器座附近布局)往往先到瓶颈。最终选型9MHz:性能稳定、对连接质量不敏感,产物量产时可预期。
顺带把时钟实测和理论对了个账:逻辑分析仪测得SCK实际8.94MHz,与理论9MHz偏差-0.7%,来源是HSE晶振的±20ppm偏差加上HAL配置的分频取整------完全在正常范围。
TCP服务端实现与一次典型的缓冲区覆盖事故
Socket 0的生命周期由Sn_SR状态寄存器驱动,TCP服务端的状态流转如下:
#mermaid-svg-GQaRcoqnnL4s5Bpp{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-GQaRcoqnnL4s5Bpp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GQaRcoqnnL4s5Bpp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GQaRcoqnnL4s5Bpp .error-icon{fill:#552222;}#mermaid-svg-GQaRcoqnnL4s5Bpp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GQaRcoqnnL4s5Bpp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GQaRcoqnnL4s5Bpp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GQaRcoqnnL4s5Bpp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GQaRcoqnnL4s5Bpp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GQaRcoqnnL4s5Bpp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GQaRcoqnnL4s5Bpp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GQaRcoqnnL4s5Bpp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GQaRcoqnnL4s5Bpp .marker.cross{stroke:#333333;}#mermaid-svg-GQaRcoqnnL4s5Bpp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GQaRcoqnnL4s5Bpp p{margin:0;}#mermaid-svg-GQaRcoqnnL4s5Bpp defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-GQaRcoqnnL4s5Bpp g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-GQaRcoqnnL4s5Bpp g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-GQaRcoqnnL4s5Bpp g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-GQaRcoqnnL4s5Bpp g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-GQaRcoqnnL4s5Bpp g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-GQaRcoqnnL4s5Bpp .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-GQaRcoqnnL4s5Bpp .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-GQaRcoqnnL4s5Bpp .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-GQaRcoqnnL4s5Bpp .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-GQaRcoqnnL4s5Bpp .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-GQaRcoqnnL4s5Bpp .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-GQaRcoqnnL4s5Bpp .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-GQaRcoqnnL4s5Bpp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GQaRcoqnnL4s5Bpp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GQaRcoqnnL4s5Bpp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GQaRcoqnnL4s5Bpp .edgeLabel .label text{fill:#333;}#mermaid-svg-GQaRcoqnnL4s5Bpp .label div .edgeLabel{color:#333;}#mermaid-svg-GQaRcoqnnL4s5Bpp .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-GQaRcoqnnL4s5Bpp .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-GQaRcoqnnL4s5Bpp .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-GQaRcoqnnL4s5Bpp .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-GQaRcoqnnL4s5Bpp .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-GQaRcoqnnL4s5Bpp .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GQaRcoqnnL4s5Bpp .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GQaRcoqnnL4s5Bpp #statediagram-barbEnd{fill:#333333;}#mermaid-svg-GQaRcoqnnL4s5Bpp .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GQaRcoqnnL4s5Bpp .cluster-label,#mermaid-svg-GQaRcoqnnL4s5Bpp .nodeLabel{color:#131300;}#mermaid-svg-GQaRcoqnnL4s5Bpp .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-GQaRcoqnnL4s5Bpp .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-GQaRcoqnnL4s5Bpp .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-GQaRcoqnnL4s5Bpp .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-GQaRcoqnnL4s5Bpp .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-GQaRcoqnnL4s5Bpp .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-GQaRcoqnnL4s5Bpp .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-GQaRcoqnnL4s5Bpp .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-GQaRcoqnnL4s5Bpp .note-edge{stroke-dasharray:5;}#mermaid-svg-GQaRcoqnnL4s5Bpp .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-GQaRcoqnnL4s5Bpp .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-GQaRcoqnnL4s5Bpp .statediagram-note text{fill:black;}#mermaid-svg-GQaRcoqnnL4s5Bpp .statediagram-note .nodeLabel{color:black;}#mermaid-svg-GQaRcoqnnL4s5Bpp .statediagram .edgeLabel{color:red;}#mermaid-svg-GQaRcoqnnL4s5Bpp #dependencyStart,#mermaid-svg-GQaRcoqnnL4s5Bpp #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-GQaRcoqnnL4s5Bpp .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GQaRcoqnnL4s5Bpp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 上电
Sn_CR = OPEN
Sn_CR = LISTEN
客户端接入\nSn_IR.CON置位
收到对端FIN
Sn_CR = CLOSE
重传超时或收到RST
主动Sn_CR = CLOSE
SOCK_CLOSED
SOCK_INIT
SOCK_LISTEN
SOCK_ESTABLISHED
SOCK_CLOSE_WAIT
初始化流程按依赖顺序执行:复位确认版本 → 等PHY链路 → 写MAC/子网掩码/网关/本机IP → 配置Socket 0缓冲区大小 → OPEN → LISTEN。其中写Sn_TXBUF_SIZE(Socket 0偏移0x001F)必须在OPEN之前完成,芯片在OPEN时刻按该值锁定缓冲区分配。
c
int8_t TCP_Server_Init(void)
{
uint8_t mac[6] = {0x02, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E}; /* 首字节02:本地管理+单播 */
uint8_t sub[4] = {255, 255, 255, 0};
uint8_t gw[4] = {192, 168, 1, 1};
uint8_t ip[4] = {192, 168, 1, 128};
if (W5500_HardwareReset() != 0) return -1; /* 先确认SPI链路,再做网络配置 */
W5500_WaitLinkUp(5000); /* LNK位等待,5s超时 */
W5500_WriteBurst(BSB_COMMON(0), SHAR, mac, 6);
W5500_WriteBurst(BSB_COMMON(0), SUBR, sub, 4);
W5500_WriteBurst(BSB_COMMON(0), GAR, gw, 4);
W5500_WriteBurst(BSB_COMMON(0), SIPR, ip, 4);
W5500_WriteByte(BSB_SOCK_REG(0), Sn_TXBUF_SIZE, 0x02); /* 2KB,值即KB数 */
W5500_WriteByte(BSB_SOCK_REG(0), Sn_RXBUF_SIZE, 0x02);
W5500_WriteByte(BSB_SOCK_REG(0), Sn_MR, 0x01); /* Sn_MR[3:0]=0001 TCP */
W5500_WriteByte(BSB_SOCK_REG(0), Sn_CR, CR_OPEN);
if (W5500_WaitState(BSB_SOCK_REG(0), Sn_SR, SOCK_INIT, 100) != 0) return -2;
uint8_t port[2] = {0x13, 0x88}; /* 5000端口,大端字节序 */
W5500_WriteBurst(BSB_SOCK_REG(0), Sn_PORT, port, 2);
W5500_WriteByte(BSB_SOCK_REG(0), Sn_CR, CR_LISTEN);
if (W5500_WaitState(BSB_SOCK_REG(0), Sn_SR, SOCK_LISTEN, 100) != 0) return -3;
return 0;
}
回环收发的核心循环里,发送必须遵循"查FSR→写缓冲→推WR指针→发SEND命令"的固定次序。WR指针更新和缓冲区写入用同一个回绕掩码,收发两侧都做了防越界:
c
#define S0_TXBUF_SIZE 2048u
#define TXBUF_MASK (S0_TXBUF_SIZE - 1u)
int32_t TCP_Send(uint8_t *data, uint16_t len)
{
/* 次序敏感:FSR就绪检查必须先于缓冲写入,否则会覆盖未发完的数据 */
uint32_t tick = HAL_GetTick();
while (1) {
uint16_t fsr = W5500_ReadWord(BSB_SOCK_REG(0), Sn_TX_FSR);
if (fsr >= len) break;
if ((HAL_GetTick() - tick) > 1000) return -1; /* 1s等不到缓冲即异常退出 */
}
uint16_t wr = W5500_ReadWord(BSB_SOCK_REG(0), Sn_TX_WR);
uint16_t offset = wr & TXBUF_MASK;
uint16_t first = S0_TXBUF_SIZE - offset; /* 尾部剩余空间 */
if (first >= len) {
W5500_WriteBurst(BSB_SOCK_TXBUF(0), offset, data, len);
} else { /* 跨缓冲区尾部回绕,分两段写 */
W5500_WriteBurst(BSB_SOCK_TXBUF(0), offset, data, first);
W5500_WriteBurst(BSB_SOCK_TXBUF(0), 0, data + first, len - first);
}
W5500_WriteWord(BSB_SOCK_REG(0), Sn_TX_WR, wr + len); /* 16位指针自由累加,自然回绕 */
W5500_WriteByte(BSB_SOCK_REG(0), Sn_CR, CR_SEND);
return len;
}
失败路径:我最初省掉FSR等待,数据"静默错乱"
第一版回环代码我没有实时轮询Sn_TX_FSR------当时的想法是:应用层每次发500字节,2KB缓冲肯定装得下,读FSR是浪费4个SPI字节。联调时用PC端脚本连续推送大文件,前300KB一切正常,之后对方收到的数据开始成段重复和错位,但设备端串口日志显示每次send都"成功"了。
排查从工具入手:逻辑分析仪挂在SPI上,抓发送500字节的完整事务。对比正常时刻和异常时刻的波形,发现异常时刻每次发送前没有读Sn_TX_FSR的事务,且紧邻的SEND命令间隔明显缩短。由此假设:W5500内部SEND尚未完成(数据还没从TX缓冲搬进MAC发出去),应用层的新数据就写进了TX缓冲,把未发完的旧数据覆盖了------FSR在SEND执行期间是小于2KB的,只是我从来不看它。
验证这个假设做了两件事:一是在每包发送后读一次FSR,打印发现SEND命令后FSR需要数百微秒才恢复满额,期间强制等待确实会卡住;二是把发送间隔人为拉大到10ms,错乱消失。根因确认:不是缓冲区算术错误,而是省略FSR实时检查破坏了"写入量≤空闲量"的隐含契约。修复就是上面TCP_Send里的实现:循环等FSR≥len,附1秒超时兜底。修复后连续传输100MB做CRC校验,零错包。
这个案例的教训比代码本身值钱:W5500的TX缓冲是"生产者-消费者"队列,SEND命令不是同步调用而是异步启动,任何"我确定装得下"的假设都该被FSR寄存器否决。
测试验证:吞吐数据与偏差分解
测试环境:直连网线(不经交换机),PC端192.168.1.100,设备192.168.1.128:5000,回环模式(收到即回发),传输数据用CRC32校验。三组关键数据如下。
吞吐与理论对照。 按SPI帧结构估算链路理论上限:发1460字节TCP载荷需要控制段3+地址2+数据1460,共1465字节@9MHz;再叠加读FSR、推WR、写CR的开销事务约20字节,SPI链路单向有效带宽约8.9 Mbps。TCP实测只有4.38 Mbps,偏差约50%,分解下来并不冤枉:
| 项目 | 数值 | 说明 |
|---|---|---|
| SPI链路理论上限(1460B块) | ~8.9 Mbps | 控制段开销后净速率 |
| TCP实测吞吐 | 4.38 Mbps | 2KB块回环,10MB×5次 |
| ACK反向流量占用 | ~-1.5 Mbps | 每个数据包的TCP ACK也走SPI |
| FSR等待窗口 | ~-1.2 Mbps | SEND期间缓冲不可写 |
| W5500内部MAC封装时延 | ~-1.8 Mbps | 帧间隔、重传定时器粒度 |
结论:SPI时钟翻倍(18MHz)理论上限也只到约13 Mbps,且牺牲连接质量容错。对"采集数据上云"这类应用,4.38 Mbps绰绰有余;若真要跑大吞吐,正确路径是F407+lwIP而不是死磕W5500。
延迟。 PC端ping 192.168.1.128,100次平均1.8 ms(ICMP硬件应答,与MCU负载无关);TCP请求-响应往返平均2.3 ms。
对端断电检测时间对照。 这个参数的理论值可以精确算出来:W5500的TCP重传机制由RTR(通用寄存器0x0019,重传超时,单位100μs,默认2000=200ms)和RCR(0x001B,重传次数,默认8)控制。对端断电后无ACK应答,W5500按固定间隔重传,第8次重传后置Sn_IR的TIMEOUT位并将Socket拉回SOCK_CLOSED。理论总时长 = 200ms × 8 = 1.6 s。实测20次拔电测试,检测耗时1.63~1.79 s,平均1.71 s,偏差+6.9%------多出来的部分是主循环100ms轮询周期的采样粒度,行为完全符合预期。
断线检测与自动重连:三种断法要分开治
调试重连逻辑时我才意识到"断线"不是一个状态,而是三种机理完全不同的场景,检测手段必须分开设计:
| 断线场景 | 物理表现 | 检测手段 | 检测耗时实测 |
|---|---|---|---|
| 网线拔出 | PHY失锁 | 轮询PHYCFGR.LNK=0 | ≤100 ms(轮询周期) |
| 对端异常断电 | LNK仍为1,无任何报文 | Sn_IR.TIMEOUT(RTR/RCR重传耗尽)或应用层心跳超时 | 1.71 s(RTR×RCR) |
| 对端正常关闭 | 收到FIN报文 | Sn_SR变0x1C(CLOSE_WAIT) | 毫秒级 |
第三种最隐蔽也最危险:如果对端是工控机死机(网口芯片还活着,LNK=1、不发FIN),设备侧会觉得"一切正常",连接永远挂在SOCK_ESTABLISHED上,用户以为数据在传,实际对端早就收不到了。工业现场这叫"静默掉线",必须靠应用层心跳兜底。
完整重连状态机如下:
#mermaid-svg-grAzMEi6GdmIVXCK{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-grAzMEi6GdmIVXCK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-grAzMEi6GdmIVXCK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-grAzMEi6GdmIVXCK .error-icon{fill:#552222;}#mermaid-svg-grAzMEi6GdmIVXCK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-grAzMEi6GdmIVXCK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-grAzMEi6GdmIVXCK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-grAzMEi6GdmIVXCK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-grAzMEi6GdmIVXCK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-grAzMEi6GdmIVXCK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-grAzMEi6GdmIVXCK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-grAzMEi6GdmIVXCK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-grAzMEi6GdmIVXCK .marker.cross{stroke:#333333;}#mermaid-svg-grAzMEi6GdmIVXCK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-grAzMEi6GdmIVXCK p{margin:0;}#mermaid-svg-grAzMEi6GdmIVXCK .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-grAzMEi6GdmIVXCK .cluster-label text{fill:#333;}#mermaid-svg-grAzMEi6GdmIVXCK .cluster-label span{color:#333;}#mermaid-svg-grAzMEi6GdmIVXCK .cluster-label span p{background-color:transparent;}#mermaid-svg-grAzMEi6GdmIVXCK .label text,#mermaid-svg-grAzMEi6GdmIVXCK span{fill:#333;color:#333;}#mermaid-svg-grAzMEi6GdmIVXCK .node rect,#mermaid-svg-grAzMEi6GdmIVXCK .node circle,#mermaid-svg-grAzMEi6GdmIVXCK .node ellipse,#mermaid-svg-grAzMEi6GdmIVXCK .node polygon,#mermaid-svg-grAzMEi6GdmIVXCK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-grAzMEi6GdmIVXCK .rough-node .label text,#mermaid-svg-grAzMEi6GdmIVXCK .node .label text,#mermaid-svg-grAzMEi6GdmIVXCK .image-shape .label,#mermaid-svg-grAzMEi6GdmIVXCK .icon-shape .label{text-anchor:middle;}#mermaid-svg-grAzMEi6GdmIVXCK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-grAzMEi6GdmIVXCK .rough-node .label,#mermaid-svg-grAzMEi6GdmIVXCK .node .label,#mermaid-svg-grAzMEi6GdmIVXCK .image-shape .label,#mermaid-svg-grAzMEi6GdmIVXCK .icon-shape .label{text-align:center;}#mermaid-svg-grAzMEi6GdmIVXCK .node.clickable{cursor:pointer;}#mermaid-svg-grAzMEi6GdmIVXCK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-grAzMEi6GdmIVXCK .arrowheadPath{fill:#333333;}#mermaid-svg-grAzMEi6GdmIVXCK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-grAzMEi6GdmIVXCK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-grAzMEi6GdmIVXCK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-grAzMEi6GdmIVXCK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-grAzMEi6GdmIVXCK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-grAzMEi6GdmIVXCK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-grAzMEi6GdmIVXCK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-grAzMEi6GdmIVXCK .cluster text{fill:#333;}#mermaid-svg-grAzMEi6GdmIVXCK .cluster span{color:#333;}#mermaid-svg-grAzMEi6GdmIVXCK 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-grAzMEi6GdmIVXCK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-grAzMEi6GdmIVXCK rect.text{fill:none;stroke-width:0;}#mermaid-svg-grAzMEi6GdmIVXCK .icon-shape,#mermaid-svg-grAzMEi6GdmIVXCK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-grAzMEi6GdmIVXCK .icon-shape p,#mermaid-svg-grAzMEi6GdmIVXCK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-grAzMEi6GdmIVXCK .icon-shape .label rect,#mermaid-svg-grAzMEi6GdmIVXCK .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-grAzMEi6GdmIVXCK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-grAzMEi6GdmIVXCK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-grAzMEi6GdmIVXCK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} LNK=0
Sn_IR.TIMEOUT 或心跳超时
Sn_SR=0x1C
先DISCON再CLOSE
否,等待
是
NORMAL
SOCK_ESTABLISHED 服务运行
LINK_LOST
物理断开
PEER_DEAD
对端失联
CLOSE_WAIT
对端已关闭
强制 Sn_CR=CLOSE
PHYCFGR.LNK=1 ?
重新 OPEN → LISTEN
返回NORMAL
失败路径:热插拔20次,第3次开始连不上
第一版重连逻辑只处理了LNK=0的场景:检测到物理断开就close再listen。实测拔插网线,前两次都能在1秒内恢复,第三次开始PC永远连接超时。串口打印Sn_SR,发现值停在0x1C------SOCK_CLOSE_WAIT。
这里我最初的假设是W5500芯片状态机卡死,甚至怀疑模块批次问题。翻手册Table 15(Socket状态定义)才发现:0x1C的语义是"收到对端FIN、本端尚未关闭",它是一个合法的中间态。还原现场后逻辑通了:第二次拔线时,PC端TCP客户端还挂着半开连接,插回网线的瞬间Windows重传了FIN报文,W5500收到后进入CLOSE_WAIT;而我的状态机只认LNK和ESTABLISHED/LISTEN三种状态,CLOSE_WAIT被当成"未知态"跳过了close动作,Socket就永远卡在这个不能收发的中间态。
根因是状态机覆盖不全:重连逻辑必须对"任何非ESTABLISHED/LISTEN的Socket状态"统一执行close→等待→重新listen。修复后状态机如上图,热插拔20次全部恢复,平均1.2 s,最差2.1 s(发生在PC端ARP缓存过期需要重新解析时)。
心跳与保活的实现上,W5500提供了两层机制。硬件层:Sn_KPALVTR(Socket 0偏移0x002F,单位5秒)设为非0后,连接空闲超过设定时间芯片自动发TCP Keepalive探测;或对ESTABLISHED状态的Socket写Sn_CR=0x22(SEND_KEEP)手动触发一次。软件层:应用每10秒发一帧自定义心跳报文,连续2次无回包判定对端失联。我两层都启了------硬件keepalive负责维持NAT表项(将来接路由器上云时有用),应用层心跳负责业务级判活,超时后统一走状态机的PEER_DEAD分支。
关键代码里,链路状态标志必须volatile(主循环与潜在的定时器上下文共享),状态机对状态值做了防越界白名单校验:
c
volatile uint8_t g_link_state = LINK_NORMAL; /* 中断/主循环共享,必须volatile */
void TCP_Reconnect_Task(void)
{
uint8_t sr = W5500_ReadByte(BSB_SOCK_REG(0), Sn_SR);
uint8_t lnk = W5500_ReadByte(BSB_COMMON(0), PHYCFGR) & 0x01;
if (!lnk) {
g_link_state = LINK_LOST;
W5500_WriteByte(BSB_SOCK_REG(0), Sn_CR, CR_CLOSE);
return; /* 等LNK恢复后再重建,避免空转 */
}
if (g_link_state == LINK_NORMAL && sr == SOCK_ESTABLISHED) return;
/* 白名单:ESTABLISHED/LISTEN之外的一切状态统一走重建 */
g_link_state = LINK_RECOVER;
if (sr == SOCK_CLOSE_WAIT) {
W5500_WriteByte(BSB_SOCK_REG(0), Sn_CR, CR_DISCON);
HAL_Delay(20);
}
W5500_WriteByte(BSB_SOCK_REG(0), Sn_CR, CR_CLOSE);
if (W5500_WaitState(BSB_SOCK_REG(0), Sn_SR, SOCK_CLOSED, 200) != 0) return;
W5500_WriteByte(BSB_SOCK_REG(0), Sn_CR, CR_OPEN);
if (W5500_WaitState(BSB_SOCK_REG(0), Sn_SR, SOCK_INIT, 100) != 0) return;
uint8_t port[2] = {0x13, 0x88};
W5500_WriteBurst(BSB_SOCK_REG(0), Sn_PORT, port, 2);
W5500_WriteByte(BSB_SOCK_REG(0), Sn_CR, CR_LISTEN);
if (W5500_WaitState(BSB_SOCK_REG(0), Sn_SR, SOCK_LISTEN, 100) == 0) {
g_link_state = LINK_NORMAL;
}
}
相关阅读:《STM32驱动W5500作为客户端进行通讯》 --- 客户端视角的初始化与缓冲区配置示例,与本文服务端实现互补。
故障排查:6类问题与完整排查链
问题1:读VERSIONR恒为0x00或0xFF。 最常见原因是SPI链路本身不通。排查链:把SPI降到2.25MHz(/32)重试,若稳定读到0x04,说明是时钟过高导致采样错位------逻辑分析仪看MISO波形,20cm杜邦线在18MHz出现上升沿振铃;若降频也读不到,检查RST引脚是否真的拉低过(万用表量复位瞬间的电平变化)、CS是否通信期间保持低电平、模块供电是否3.3V。修复:缩短连线+锁定9MHz时钟。验证:连续上电30次版本号全部0x04。
问题2:版本号正常但ping不通。 排查链:先看PHYCFGR的LNK位,为0说明网线或水晶头问题(换线、检查RJ45座焊接);LNK为1则查IP配置------PC与设备是否同网段、子网掩码是否一致;再核对MAC首字节bit0是否为0(我复现过一次把MAC首字节写成0x03导致交换机丢帧);最后才怀疑PC防火墙拦ICMP。验证:换用抓包工具看ICMP Echo Reply是否发出。
问题3:PC连接被拒,Sn_SR停在0x13。 0x13是SOCK_INIT,说明LISTEN没有生效。排查链:确认Sn_CR写入顺序是先OPEN(进入0x13)再LISTEN;检查Sn_PORT是否按大端写入(5000端口要写0x13、0x88两个字节,直接写0x8813就是常见的端口错乱);确认PC端连接的IP与SIPR一致。修复:按大端重组端口字节。验证:netstat看到PC端连接进入ESTABLISHED。
问题4:大数据传输内容错乱但send"成功"。 即前文失败路径案例。排查链:逻辑分析仪抓SPI事务序列→发现缺少FSR读取→假设SEND未完成时缓冲被覆盖→人为加大发送间隔10ms验证错乱消失→确认根因→修复为FSR实时等待。验证:100MB传输CRC零错。
问题5:热插拔若干次后永远连不上。 即前文CLOSE_WAIT案例。排查链:串口打印Sn_SR发现0x1C→查手册确认是合法中间态→还原现场确认PC重传FIN触发→假设状态机漏覆盖→补全白名单统一close逻辑。验证:20次热插拔全部恢复。
问题6:对端死机但设备"感觉不到"。 LNK为1、无FIN,Socket长期挂在0x17。排查链:在PC端抓包确认对端确实无任何报文→确认这类静默掉线物理层无法感知→算RTR×RCR理论超时1.6s并实测1.71s验证TIMEOUT中断路径→再对"对端活着但应用僵死"的场景补应用层心跳(10s周期、2次超时判死)。验证:PC端断电20次,设备平均1.71s进入重连;PC端kill进程,心跳5~20s内判死重连。
总结
把整篇收拢成五条核心要点:其一,W5500是网络协处理器而非普通SPI外设,BSB块选择贯穿一切读写,驱动封装第一步就把块编号宏定死;其二,TX/RX指针是16位自由累加+物理掩码的设计,回绕差值必须用无符号运算,任何"优化"FSR检查的念头都要被否决;其三,SPI时钟选型不能照抄手册上限,9MHz是连接质量与吞吐的实测平衡点;其四,断线有三种机理(物理失锁/静默掉电/正常关闭),检测手段分别是LNK轮询、RTR×RCR超时+应用层心跳、Sn_SR状态监听;其五,重连状态机要对所有非正常Socket状态兜底close,CLOSE_WAIT这种合法中间态最容易被漏掉。
适用边界方面:本文方案适合数据采集、工业网关、远程控制这类吞吐1~5 Mbps以内的有线联网需求;如果目标是视频流或大文件服务,W5500的SPI瓶颈决定它不是正确工具,应转向F407/H7的内置MAC+lwIP路线。已知局限:当前驱动为阻塞式轮询,主循环空转等待FSR时CPU并不释放,若系统里有高实时任务,应把Socket事件改为INTn中断驱动;多Socket并发(8路全开)时的缓冲区分配策略也值得单独展开。
扩展方向上,下一步可以做的三件事:把TX/RX缓冲搬运改成SPI DMA以进一步释放CPU;引入多Socket并发(例如Socket0跑Modbus TCP、Socket1跑私有协议);以及接MQTT协议栈对接云平台,与之前ESP8266 MQTT那篇形成有线/无线双通道的对照实验。
如需获取本文完整代码和更多实战项目,可开通 CSDN 技术会员。
版本备注
- 硬件平台:STM32F103C8T6(Cortex-M3 @ 72MHz)+ W5500以太网模块(板载HR911105A网络变压器一体RJ45)
- 软件版本:Keil MDK 5.38 + STM32CubeMX 6.10 + STM32CubeF1 HAL库 1.8.5
- 兼容说明:W5500代码可直接用于F103全系列;F4/F7平台仅需替换SPI初始化部分,寄存器层完全一致;ioLibrary官方库用户请对照Sn_TX_FSR轮询与重连状态机两处自行移植
- API 变更风险:WIZnet ioLibrary 4.x的getSn_TX_FSR()入参为socket号,与3.x的旧接口签名不同;HAL库1.8.x的HAL_SPI_TransmitReceive参数顺序与早期版本存在差异,移植时以实际工程头文件为准
参考资料
- STM32F407+W5500以太网实战:HAL库驱动移植与TCP通信 --- ioLibrary驱动移植与TCP回环的F4平台参照
- W5500以太网芯片实战避坑指南:PHY匹配、寄存器状态机与MII时序精调 --- Sn_MR写入时序与状态机未定义分支的深度分析
- STM32驱动W5500作为客户端进行通讯 --- 客户端模式初始化与Socket缓冲配置示例
- STM32F407VE开发板实测可用的W5500以太网工程(HAL库官方最新驱动) --- 工业终端场景的W5500工程化实践