
简介STC单片机W5500模块服务端模式例程面向嵌入式开发者展示如何在STC平台上通过W5500以太网控制器实现TCP/UDP服务端程序并采用模拟SPI方式解决硬件SPI资源不足的问题。例程包内含12个文件主要包括C源码、头文件、Keil工程配置文件及备份文件压缩包大小仅44KB结构紧凑便于移植学习已有630人学习/下载。包内提供了完整的服务端初始化流程包括IP配置、端口监听、中断处理和数据收发函数同时模拟SPI实现可帮助开发者理解在缺少SPI外设时如何通过GPIO模拟时序完成与W5500的可靠通信。对于希望将STC单片机接入以太网、实现HTTP或FTP服务器的项目这份例程提供了可参考的工程框架与排错思路适合具备一定单片机基础的开发者借鉴。1. 把 STC 的普通 IO 拉成 W5500 要的 SPI什么时候必须上模拟 SPI一块 W5500 模块一片 STC 单片机两者之间不接硬件 SPI 引脚而是用四根普通 GPIO 一位一位把时钟和数据“捅”出来——这不是偷懒是很多 STC 项目里唯一现实的接法。STC 这一系里带硬件 SPI 的型号不算少但引脚往往和下载口、外部中断、串口复用真正留给以太网模块的常常只剩几个 IO。W5500 的 SPI 从机接口偏偏对时序非常宽容SCK 不连续、频率低都能正常工作于是软件模拟 SPI 反而成为稳定且通用的方案。这篇文章把 STC 单片机 W5500 模块的服务端模式例程拆开讲模拟 SPI 的帧结构、Socket 状态机、最小代码骨架、调试路径以及从例程搬到真实工程时要处理的细节。目标是让一块 STC89C52 或者 STC15 系列单片机接上裸 W5500 模块就能被局域网内的 PC 直接 TCP 连接上来回收发数据。2. W5500 服务端模式的底层准备帧结构、寄存器与 Socket 状态机2.1 W5500 的 SPI 帧2 字节地址、1 字节控制然后是数据W5500 的 SPI 访问方式和 W25Qxx 这类 SPI Flash 不一样。它每一帧都由固定的三段组成16 位地址、8 位控制字节、N 字节数据。地址不是某个寄存器内部的位偏移而是整个芯片统一编址的空间靠控制字节里的“块选择”字段决定这次访问落在通用寄存器、某个 Socket 的寄存器、还是 TX/RX 缓冲区。控制字节是理解 W5500 的关键。bit7~bit5 选块bit4 决定读写方向bit3~bit2 是数据长度模式。工程里最常用的是可变数据长度模式也就是 CS 拉低多久数据段就传多久收完最后一字节再把 CS 拉高一帧结束。bit7~bit5块选择bit4读写bit3~bit2长度模式bit1~bit0000通用寄存器001Socket0 寄存器010Socket1 寄存器011Socket2100Socket3101TX 缓冲110RX 缓冲1读0写00可变数据长度CS 控制帧长01固定数据长度数据前带 2 字节长度保留写 0组装帧头的代码在模拟 SPI 里就三行void w5500_frame_head(unsigned char block, unsigned int addr, unsigned char rw) { spi_write_byte((unsigned char)(addr 8)); // 地址高 8 位先发 spi_write_byte((unsigned char)(addr 0xFF)); // 再发低 8 位 spi_write_byte((block 5) | (rw 4)); // 控制字节块 读写方向 }这里rw传 1 表示读传 0 表示写。W5500 是 MSB first地址高位在前控制字节里的块选择也要左移到位。固定数据长度模式在控制字节后还要再跟 2 字节长度例程用不上不需要管。2.2 服务端模式的 Socket 状态机OPEN、LISTEN、ESTABLISHED服务端模式的本质是把 W5500 的某个 Socket 配成 TCP然后执行 OPEN 打开端口LISTEN 进入监听等待客户端的三次握手完成状态跳到 ESTABLISHED 之后才能收发数据。这些动作不是调库函数而是往 Sn_CR 写命令码再读 Sn_SR 确认状态。Sn_CR 的每个命令执行完后会被硬件自动清零所以标准写法是写完命令后轮询它回到 0再继续下一步。Sn_SR 是只读状态不会自动跳变要用程序轮询。Sn_CR 命令码命令Sn_SR 状态值状态含义0x01OPEN0x00SOCK_CLOSED0x02LISTEN0x13SOCK_INIT0x08DISCON0x14SOCK_LISTEN0x10CLOSE0x17SOCK_ESTABLISHED0x20SEND0x1CSOCK_CLOSE_WAIT0x40RECV——很多第一次写服务端的人会在客户端断开后卡住就是因为只处理了 ESTABLISHED 一个状态。客户端正常断开时 Socket 会先进 CLOSE_WAIT必须对 W5500 执行 DISCON 回到 INIT再 CLOSE 回到 CLOSED最后重新 OPEN LISTEN否则第二次连接永远建立不起来。2.3 模拟 SPI 的时序取舍与 STC 引脚处理模拟 SPI 在代码层面只有四件事拉高 SCK、拉低 SCK、置 MOSI、读 MISO。W5500 支持 SPI 模式 0 和模式 3模拟 SPI 固定用模式 0SCK 空闲为低上升沿采样数据。模式 3 是空闲为高如果之前写过别的器件代码最容易出错的就是这一条。STC 的 IO 模式要单独处理。STC89C52 这类经典 51 只有准双向口MISO 读之前必须先写 1否则读回来全是 0。STC15、STC8 系列可以通过 PnM0/PnM1 寄存器把 SCK、MOSI、CS 配成推挽输出MISO 保持准双向输入// STC15/STC8 系列P1.0(SCK)、P1.1(MOSI)、P1.3(CS) 配推挽 // P1.2(MISO) 保持准双向输入读之前写 1 P1M0 0x0B; // 对应 bit0、bit1、bit3 推挽 P1M1 0x00;模拟 SPI 的速率用 12MHz 的 STC89C52 算一下一条_nop_()约 1us一个 bit 至少 4~6us一个字节约 40~50us读一次 Sn_SR 的完整帧是 3 字节头加 1 字节数据约 200us。这意味着每秒可以轮询几千次 Socket 状态对 TCP 服务端来说完全够用。真正要留意的是别在模拟 SPI 函数里穿插其他耗时操作否则一帧被拆成两半W5500 会把后面的数据当成新帧解析。3. 用模拟 SPI 把 W5500 跑成 TCP 服务端最小例程的完整骨架3.1 模拟 SPI 底层读写函数先保证 VERSIONR 读回 0x04代码按 Keil C51 的写法给出重点是寄存器流程。底层就两个函数写一个字节、读一个字节。写字节时 SCK 拉低后先放 MOSI再拉高让 W5500 在上升沿采样读字节时相反先拉高 SCK 等 MISO 稳定再采样。#include reg52.h #include intrins.h sbit W5500_RST P1^4; sbit W5500_SCK P1^0; sbit W5500_MOSI P1^1; sbit W5500_MISO P1^2; sbit W5500_CS P1^3; #define BLOCK_COMMON 0 // 通用寄存器块 #define BLOCK_SOCK0 1 // Socket0 寄存器块 #define BLOCK_TXBUF 5 // TX 缓冲区 #define BLOCK_RXBUF 6 // RX 缓冲区 static void spi_delay(void) { _nop_(); _nop_(); // 12MHz 下约 2us主频更高时适当加长 } void spi_write_byte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { W5500_SCK 0; W5500_MOSI (dat 0x80) ? 1 : 0; dat 1; spi_delay(); W5500_SCK 1; // 上升沿W5500 采样 MOSI spi_delay(); } W5500_SCK 0; } unsigned char spi_read_byte(void) { unsigned char i, dat 0; for (i 0; i 8; i) { W5500_SCK 1; spi_delay(); // 等 MISO 稳定 dat 1; if (W5500_MISO) dat | 0x01; W5500_SCK 0; // 下降沿W5500 切换下一位 spi_delay(); } return dat; }读函数里每次先拉高 SCK 再读是因为 W5500 在下降沿之后更新 MISO等到 SCK 回到高电平并延时后数据线已经稳定。如果读出来全是 0xFF 或者固定 0x00先检查 MISO 是不是接对再检查读之前有没有把 MISO 引脚写 1。基于这两个函数封装寄存器读写。每次访问都是 CS 拉低、发 3 字节头、传数据、CS 拉高void w5500_write_reg(unsigned char block, unsigned int addr, unsigned char dat) { W5500_CS 0; spi_write_byte((unsigned char)(addr 8)); spi_write_byte((unsigned char)(addr 0xFF)); spi_write_byte(block 5); // 写命令可变数据长度 spi_write_byte(dat); W5500_CS 1; } unsigned char w5500_read_reg(unsigned char block, unsigned int addr) { unsigned char val; W5500_CS 0; spi_write_byte((unsigned char)(addr 8)); spi_write_byte((unsigned char)(addr 0xFF)); spi_write_byte((block 5) | 0x10); // 读命令 val spi_read_byte(); W5500_CS 1; return val; }一个关键点是 CS 必须在整个帧期间保持低电平不能在地址、控制字节、数据之间拉高。模拟 SPI 时如果误把每个字节都包一次 CS 拉高拉低W5500 会把每个字节当成新帧的地址寄存器读写全部错乱。调通底层的验证方法很简单读版本寄存器 VERSIONR地址是通用寄存器块的 0x0039W5500 常见返回值是 0x04unsigned char ver w5500_read_reg(BLOCK_COMMON, 0x0039); // ver 应该等于 0x04否则说明 SPI 时序、接线或复位有问题3.2 网络参数与 Socket 初始化MAC、IP、端口一次配好W5500 上电后要先把网关、子网掩码、MAC、本机 IP 写进通用寄存器。这些寄存器都是多字节全部用可变数据长度模式一帧写完。注意 W5500 是大端字节序IP 地址的 192 要放在高字节。void w5500_net_init(void) { unsigned char mac[6] {0x00, 0x08, 0xDC, 0x01, 0x02, 0x03}; unsigned char ip[4] {192, 168, 1, 200}; unsigned char gw[4] {192, 168, 1, 1}; unsigned char mask[4] {255, 255, 255, 0}; w5500_write_burst(BLOCK_COMMON, 0x0001, gw, 4); // GAR w5500_write_burst(BLOCK_COMMON, 0x0005, mask, 4); // SUBR w5500_write_burst(BLOCK_COMMON, 0x0009, mac, 6); // SHAR w5500_write_burst(BLOCK_COMMON, 0x000F, ip, 4); // SIPR }多字节 Burst 写函数就是把单字节写循环套进同一个 CS 帧里地址只发一次void w5500_write_burst(unsigned char block, unsigned int addr, unsigned char *buf, unsigned int len) { unsigned int i; W5500_CS 0; spi_write_byte((unsigned char)(addr 8)); spi_write_byte((unsigned char)(addr 0xFF)); spi_write_byte(block 5); for (i 0; i len; i) { spi_write_byte(buf[i]); } W5500_CS 1; }Socket0 的 TCP 服务端初始化顺序是固定的先写 Sn_MR 为 TCP 模式再写本地端口 Sn_PORT然后写 Sn_CR 执行 OPEN等 Sn_CR 清 0再等 Sn_SR 变成 SOCK_INIT最后写 LISTEN 命令等状态变成 SOCK_LISTEN。端口号的字节序同样是大端5000 要拆成高字节 0x13、低字节 0x88。void tcp_server_start(unsigned char sock, unsigned int port) { w5500_write_reg(BLOCK_SOCK0 sock, 0x0000, 0x01); // Sn_MR TCP w5500_write_reg(BLOCK_SOCK0 sock, 0x0004, (unsigned char)(port 8)); w5500_write_reg(BLOCK_SOCK0 sock, 0x0005, (unsigned char)(port 0xFF)); w5500_write_reg(BLOCK_SOCK0 sock, 0x0001, 0x01); // Sn_CR OPEN while (w5500_read_reg(BLOCK_SOCK0 sock, 0x0001) ! 0); while (w5500_read_reg(BLOCK_SOCK0 sock, 0x0003) ! 0x13); // SOCK_INIT w5500_write_reg(BLOCK_SOCK0 sock, 0x0001, 0x02); // Sn_CR LISTEN while (w5500_read_reg(BLOCK_SOCK0 sock, 0x0001) ! 0); while (w5500_read_reg(BLOCK_SOCK0 sock, 0x0003) ! 0x14); // SOCK_LISTEN }每个命令后等 Sn_CR 清 0 这一步不能省尤其是 OPEN 后马上写 LISTEN如果 Sn_CR 还在执行中LISTEN 命令会被吞掉Socket 就停在 SOCK_INIT 永远不进监听。3.3 服务端主循环Listen、收发与断线重接进入监听后主循环只需要做三件事查 Sn_SR 看当前状态ESTABLISHED 就收发数据CLOSE_WAIT 就做断开清理CLOSED 就重新初始化。收发数据的核心是四个 16 位寄存器发送前查 Sn_TX_FSR 剩余空间写完数据后更新 Sn_TX_WR接收前查 Sn_RX_RSR 可读字节数读完更新 Sn_RX_RD 并执行 RECV 命令释放缓冲。unsigned int g_tx_wr 0; unsigned int g_rx_rd 0; unsigned int tcp_send(unsigned char sock, unsigned char *buf, unsigned int len) { unsigned int free_size; free_size w5500_read16(BLOCK_SOCK0 sock, 0x0020); // Sn_TX_FSR if (free_size len) return 0; w5500_write_burst(BLOCK_TXBUF, g_tx_wr, buf, len); g_tx_wr len; w5500_write16(BLOCK_SOCK0 sock, 0x0024, g_tx_wr); // Sn_TX_WR w5500_write_reg(BLOCK_SOCK0 sock, 0x0001, 0x20); // Sn_CR SEND while (w5500_read_reg(BLOCK_SOCK0 sock, 0x0001) ! 0); return len; } unsigned int tcp_recv(unsigned char sock, unsigned char *buf, unsigned int len) { unsigned int rx_size; rx_size w5500_read16(BLOCK_SOCK0 sock, 0x0026); // Sn_RX_RSR if (rx_size 0) return 0; if (rx_size len) rx_size len; w5500_read_burst(BLOCK_RXBUF, g_rx_rd, buf, rx_size); g_rx_rd rx_size; w5500_write16(BLOCK_SOCK0 sock, 0x0028, g_rx_rd); // Sn_RX_RD w5500_write_reg(BLOCK_SOCK0 sock, 0x0001, 0x40); // Sn_CR RECV while (w5500_read_reg(BLOCK_SOCK0 sock, 0x0001) ! 0); return rx_size; }w5500_read16和w5500_write16是对 16 位寄存器的封装先读高字节再读低字节和 W5500 的大端序保持一致unsigned int w5500_read16(unsigned char block, unsigned int addr) { unsigned int val; val (unsigned int)w5500_read_reg(block, addr) 8; val | w5500_read_reg(block, addr 1); return val; }主循环里把状态分支写清楚。回显是最容易验证的服务端行为收到什么就原样发回去。void main(void) { unsigned char status; unsigned char rx_buf[64]; unsigned int n; w5500_reset(); // RST 拉低 50ms释放后延时 200ms w5500_net_init(); tcp_server_start(0, 5000); while (1) { status w5500_read_reg(BLOCK_SOCK0, 0x0003); // Sn_SR if (status 0x17) { // SOCK_ESTABLISHED n tcp_recv(0, rx_buf, sizeof(rx_buf)); if (n 0) { tcp_send(0, rx_buf, n); // 回显 } } else if (status 0x1C) { // SOCK_CLOSE_WAIT w5500_write_reg(BLOCK_SOCK0, 0x0001, 0x08); // DISCON while (w5500_read_reg(BLOCK_SOCK0, 0x0001) ! 0); w5500_write_reg(BLOCK_SOCK0, 0x0001, 0x10); // CLOSE while (w5500_read_reg(BLOCK_SOCK0, 0x0001) ! 0); while (w5500_read_reg(BLOCK_SOCK0, 0x0003) ! 0x00); tcp_server_start(0, 5000); } else if (status 0x00) { // SOCK_CLOSED tcp_server_start(0, 5000); } } }CLOSE_WAIT 分支里先 DISCON 再 CLOSE 是标准做法。DISCON 让 Socket 从 CLOSE_WAIT 回到 INITCLOSE 再把它彻底关成 CLOSED之后重新走 OPEN LISTEN。如果不做 DISCON 直接 CLOSE部分固件版本会卡在中间态。3.4 关键参数和常见误用逐条说清先看tcp_send。发送前必须查 Sn_TX_FSR这个寄存器表示当前 Socket 的发送缓冲剩余字节数。W5500 每个 Socket 默认有 2KB TX 缓冲但数据进来不会自动排队写入时超出剩余空间就会覆盖未发送的数据表现出来是粘包、乱码、甚至丢失。空间不足时不能阻塞等待返回值 0 交给上层稍后重试。tcp_recv里最容易漏的是 RECV 命令。Sn_RX_RSR 表示接收缓冲里有多少字节可读读走数据后 W5500 不会自动释放这块缓冲必须把 Sn_RX_RD 更新到读结束位置再执行 RECV 命令缓冲才会回收。漏掉这一步Sn_RX_RSR 会一直保持旧值或越读越乱。缓冲区指针是 16 位的环形边界需要自己处理。g_tx_wr和g_rx_rd增加到接近 0xFFFF 时要考虑分段读写不然数据跨越缓冲区末尾时会写到错误位置。工程里常见的做法是先算剩余到缓冲区末尾的长度不够就拆两次传。字节序问题集中在端口号、IP 地址和 16 位寄存器上。Sn_PORT 高字节在前Sn_TX_WR 高字节在前IP 地址第一字节是高字节。写错过一次端口表现是服务端监听在错误端口PC 端连不上但 Socket 状态又确实在 LISTEN。4. 服务端连不上、收不到、发不出按链路逐段定位 W5500 的坑4.1 第一步永远是 VERSIONRSPI 链路通不通拿到模块先别配 IP先把模拟 SPI 跑通。在主函数最开始读一次版本寄存器unsigned char ver w5500_read_reg(BLOCK_COMMON, 0x0039); // 返回 0x04 说明 SPI 通路正常读回 0xFF 或 0x00 则先查硬件读不到 0x04 的常见原因有三类。第一类是接线MOSI 和 MISO 接反或者模块上的电平转换方向不对。第二类是 SPI 模式极性错了SCK 空闲在高电平上升沿采样改成下降沿W5500 把数据采在错误的边沿上。第三类是 CS 时序帧内字节之间 CS 被拉高过或者 CS 悬空没有默认上拉。用示波器或者逻辑分析仪看 SCK 和 MOSI 是最直接的排查方式没有仪器就降低spi_delay的延时很多高速下不稳定的问题降速后立刻暴露。4.2 链路层通了但连接不上查状态机与网络参数VERSIONR 读回 0x04 之后下一步是 ping。PC 上和 W5500 配同一个网段比如模块是 192.168.1.200PC 就配 192.168.1.10然后ping 192.168.1.200ping 不通基本是网络参数问题。网关、子网掩码、IP 三个寄存器只要有一个写错链路层就不通。ping 通了但 TCP 连不上把焦点转到 Socket 状态上。在 main 循环里把 Sn_SR 读出来看如果停在 0x13 而不是 0x14说明 LISTEN 命令没有真正执行成功最常见是 OPEN 后没等 Sn_CR 清 0 就写了 LISTEN。还有一种隐蔽情况Socket 已经进入 ESTABLISHED但客户端程序没有正常关闭连接就退出了W5500 在 CLOSE_WAIT 状态一直不处理。新连接进来时它会尝试接入已占用的端口表现是偶尔能连上、偶尔连不上或者连接成功后立刻断开。主循环里加 CLOSE_WAIT 分支后这个问题就消失。现象对照表可以直接拿来当排查清单现象最可能原因验证手段VERSIONR 读不到 0x04接线错误、SPI 极性不对、CS 被拉高过检查 MOSI/MISO 是否接反降速重试能读寄存器但 ping 不通IP/掩码/网关配置错误PC 与模块同一网段核对四个通用寄存器能 ping 通但连不上Socket 不在 LISTEN 状态读 Sn_SR停在 0x13 就查 Sn_CR 时序连上但收不到数据没执行 RECV 命令释放缓冲看 Sn_RX_RSR 是否一直增长不清零收发乱码字节序错误、缓冲区指针越界检查 16 位寄存器高低字节顺序4.3 能连但收发异常缓冲指针、字节序和中断引脚的误导连上之后收发不对优先怀疑三个地方。第一个是缓冲指针。发送流程里必须先写 TX 缓冲再把新的写指针回写到 Sn_TX_WR最后发 SEND 命令。有人把 Sn_TX_WR 固定写成 0每次从缓冲头开始发数据全乱。第二个是字节序。调试时在 PC 端发 0x01 0x02 0x03 0x04单片机收回来如果变成 0x04 0x03 0x02 0x01那不是 SPI 位序错而是有人在应用层手动翻了字节序。W5500 的收发缓冲不修改数据内容TCP 是字节流收到什么就是什么。第三个坑在中断引脚。W5500 的 INT 引脚在 Socket 有事件时拉低很多例程用外部中断触发接收。问题是 TCP 数据可以多段到达INT 只在第一次到达时拉低一次后面如果单片机没及时处理、缓冲又没满INT 不会再次触发数据就压在缓冲里没人读。服务端模式里最稳的还是主循环轮询 Sn_RX_RSRINT 只用来做快速唤醒不要当成唯一的数据到达信号。5. 把服务端例程接到工程里分时调度、掉线重连与吞吐验证5.1 把 Socket 状态机放进主循环 tick避免阻塞业务例程的 while(1) 是纯轮询真实工程里不可能让单片机一直耗在 W5500 上。常见做法是把状态机拆成一个 tick 函数定时器产生 5ms 时基主循环在每轮任务间隙调用一次。注意不要在定时器中断里直接跑 W5500 的 SPI 读写模拟 SPI 一帧要几百微秒关中断太久会破坏其他实时任务应该只置一个标志位主循环里处理。每秒轮询 200 次对 TCP 服务端足够。数据到达后 Sn_RX_RSR 会保持可读字节数晚几个毫秒读不会丢只要别让缓冲溢出就行。W5500 的接收缓冲默认 2KB轮询间隔大时要估算最坏情况下的数据积压量必要时调大 Sn_RXBUF_SIZE。缓冲配置寄存器影响的是 W5500 片内 SRAM 划分和单片机 RAM 大小无关STC 这类小内存单片机反而适合把缓冲尽量留给 W5500。5.2 心跳、CLOSE_WAIT 与重连策略W5500 硬件不实现 TCP keepalive连接是否还活着靠应用层自己判断。服务端侧的心跳一般做两件事周期性向已连接的客户端发送自定义心跳包同时记录最后一次收到客户端数据的时间超过阈值就主动断开重连。心跳包不要影响业务数据建议单独用一个端口号或者复用现有连接但靠包头标志区分。重连策略要带退避。客户端拉网线、路由器断电这些情况W5500 侧不会立刻感知Sn_SR 可能长时间停留在 ESTABLISHED。连续心跳超时后执行 DISCON CLOSE 回到 CLOSED再重新 OPEN LISTEN。如果状态机里不加延迟异常情况下会陷入“重连失败立刻再重连”的死循环把模拟 SPI 的带宽全部吃掉。5.3 用 PC 端 Python 脚本验证收发与吞吐验证服务端例程最简单的方式是 PC 上跑一段 Python 脚本。先测回显再测重连import socket import time s socket.create_connection((192.168.1.200, 5000), timeout3) print(connected) total 0 t0 time.time() for i in range(100): s.sendall(bping-%03d % i) data s.recv(64) total len(data) dt time.time() - t0 print(round-trip avg: %.2f ms % (dt * 1000 / 100)) s.close() time.sleep(2) s2 socket.create_connection((192.168.1.200, 5000), timeout3) print(reconnect ok) s2.close()往返延迟能稳定在几十毫秒内说明主循环及时。重连检测要重点看 CLOSE_WAIT 分支是否生效脚本里 close 之后两秒再连如果能成功说明 Socket 状态机正确处理了断开。吞吐方面模拟 SPI 在 12MHz STC89C52 上能跑到十几 KB/s 量级大包测试时发送缓冲一旦不够tcp_send返回 0 的次数会明显增加这是正常的背压表现不要靠延长延时硬扛应该让业务层按返回值做分段发送。本文还有配套的精品资源点击获取