简介:面向STM32F103与W5500的TCP通信开发场景,这份rar压缩包提供了一套可直接运行的完整例程,适用于嵌入式物联网初学者、网络通信应用开发工程师以及需要快速验证TCP传输方案的硬件工程师。包内共有183个文件,压缩后大小3.48MB,以45个h头文件和43个c源文件为主,覆盖SPI接口初始化、W5500硬件驱动、TCP客户端/服务器模式切换等核心代码。同时附带uvprojx/uvoptx工程文件、hex烧录文件、axf调试文件及clean_prj.bat清理脚本,便于直接打开、编译、下载和二次开发。已有371人学习下载,资源完整度较好。通过该例程,开发者可以重点学习STM32F103与W5500之间SPI通信的寄存器配置、网络参数(IP、子网掩码、网关)设置、三次握手连接流程以及数据收发处理逻辑。例程还保留了d、o、crf等编译中间文件,有助于对照构建过程理解整个项目结构,尤其适合在局域网中配合“网络传输助手”进行联调,快速定位收发问题并验证设备间的稳定传输。
1. 从STM32F103到W5500:一片网卡芯片能省下多少开发时间
当你的项目需要一个网口,但MCU资源不够跑协议栈时,STM32F103加W5500几乎是入门首选。W5500是一颗带硬件TCP/IP协议栈的以太网控制器,它把TCP、UDP、IP、ARP那些事全包了,MCU只负责用SPI读写数据。这套方案能让你在三天内跑通一个TCP客户端,而不是花三周去调LWIP内存池。适合做智能家居网关、工业数据采集、或是任何需要把传感器数据送到上位机的场景。这份教程的资源就是一套完整的STM32F103 + W5500 TCP例程,从寄存器读写到Socket收发,到手就能编译。下段会按调试顺序,把硬件接线、SPI初始化、TCP连接状态机、以及那些容易被忽略的坑逐一拆开。你不用从头看手册,跟着做就能把数据发到电脑上。
2. 硬件连接与SPI初始化:先让W5500跑起来
2.1 引脚分配与硬件接线
W5500需要SPI接口,一般用STM32F103的SPI1或SPI2。我习惯用SPI1,因为PA5-PA7三个引脚正好是SPI1的SCK、MISO、MOSI,省得映射。片选CS可以随意选一个GPIO,常见做法是PA4,因为PA4也是SPI1的NSS,用来做软件片选最顺手。复位引脚RST接一个GPIO,比如PC0,要能拉低拉高。如果不打算用中断,INT引脚可以悬空或接普通IO用于查询。
完整接线表:
| W5500引脚 | STM32F103引脚 | 说明 |
|---|---|---|
| SCLK | PA5 | SPI1 SCK |
| MISO | PA6 | SPI1 MISO |
| MOSI | PA7 | SPI1 MOSI |
| SCS | PA4 | 软件片选,低有效 |
| RSTN | PC0 | 复位,低有效 |
| INT | PC1 | 可选,中断引脚 |
| GND | GND | 共地 |
注意:W5500的供电需要3.3V,如果板载变压器或者分离器件,确保电源纹波不要太大。很多开发者用开发板自带的W5500模块,模块上已经有网络变压器和RJ45口,接线只连SPI和复位即可。如果自己画板子,注意网络变压器中心抽头的接法,否则信号质量会差。另外,MISO、MOSI线尽量短,如果超过5cm,建议在SPI时钟线上串联一个22Ω电阻,减少振铃。
电源方面,W5500在发射时电流峰值可能到150mA左右,如果直接从STM32F103的3.3V引脚供电,遇到网络链路不稳定时可能把MCU拉死。我一般是从LDO出3.3V,并联一个100uF电解电容和10uF陶瓷电容,再给W5500单独走线。开发板模块一般已经处理好,但如果自己设计,务必算一下整板功耗。
2.2 SPI配置与复位时序
SPI配置要设为模式0(CPOL=0,CPHA=0),8位数据,最高速率可以到几十MHz,不过STM32F103的SPI最高18MHz,直接用18MHz就行。但有些模块在高速率下不稳定,我一般先降到4.5MHz调试,稳定后再提上去。如果板子走线混乱,甚至用2.25MHz,反正TCP协议栈在硬件里,SPI慢一点不会影响吞吐率太多。
代码示例基于标准外设库,HAL库的写法大同小异:
void W5500_GPIO_Init(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); // SCK、MOSI、CS配置为推挽输出,MISO为浮空输入 gpio.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7 | GPIO_Pin_4; gpio.GPIO_Mode = GPIO_Mode_Out_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); gpio.GPIO_Pin = GPIO_Pin_6; gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &gpio); // RST、INT gpio.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; gpio.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOC, &gpio); // 初始状态:CS拉高,RST拉高 GPIO_SetBits(GPIOA, GPIO_Pin_4); GPIO_SetBits(GPIOC, GPIO_Pin_0); }这里有个细节:MISO配置为浮空输入没问题,但如果你的板上MISO线上有上拉电阻,也可以配置为上拉输入。RST和INT我统一配置成推挽输出,因为INT引脚在测试时我通常直接读电平,不开启中断,这样简单一点。如果后续要上中断,再把INT改为浮空输入并开启外部中断。
接下来是复位时序。W5500的复位要求RSTN拉低至少500us,然后拉高,等待至少100ms让芯片内部PHY稳定。严格来说上电后要有至少10ms的复位低电平,然后恢复高电平后等待150ms左右再访问寄存器。我把这段写成一个函数:
void W5500_Reset(void) { GPIO_ResetBits(GPIOC, GPIO_Pin_0); delay_ms(50); GPIO_SetBits(GPIOC, GPIO_Pin_0); delay_ms(150); }如果省略复位等待,会出现SPI能写但读回错误、或者芯片随机锁死的情况。这属于经验值,不需要太精确,但宁多勿少。还有一点:如果W5500模块上自带复位芯片,它的复位引脚可能悬空,你依然可以接MCU强制复位,这样调试更方便。
SPI速率的选择直接影响TCP吞吐率,但前提是通信不出错。我在一块布局紧凑的板子上测试过:18MHz时版本寄存器读写正常,但持续收发100KB数据会出现约0.1%的错误,降到9MHz后错误消失。所以如果你的板子没有做阻抗匹配,建议先跑9MHz。网络传输速率本身不是瓶颈,W5500硬件协议栈能跑到10Mbps,SPI 9MHz也能覆盖。
2.3 寄存器读写验证
W5500的寄存器访问通过SPI控制帧。控制帧包含块选择、偏移地址和读写标志,但手工拼寄存器地址很容易出错。资源里的例程已经把这一层封装成类似 W5500_Read(block, offset) 和 W5500_Write(block, offset, data) 的函数,你不需要关心具体位域。但要在项目里自己写驱动,至少要理解:W5500把寄存器分为Common区、Socket区、PHY区等,每个区有独立偏移。Common区偏移0x0039是版本寄存器(VERSIONR),固定返回0x04。我把读版本作为SPI通信的自检入口。
uint8_t W5500_ReadVersion(void) { return W5500_Read(COMMON_REG, 0x0039); // 版本寄存器 }在main函数里这样验证:
uint8_t version = W5500_ReadVersion(); if(version == 0x04) printf("W5500 SPI OK, version=%d\n", version); else printf("W5500 read fail, version=%d\n", version);注意:这里的printf默认走串口,需要重定向。如果你用的是资源包里的完整工程,串口输出已经配好。如果读回不是0x04,先去查SPI引脚和时钟,不要去查W5500芯片,因为绝大多数情况是MCU侧配置不对。读版本正常后,再继续初始化网络参数。
3. TCP例程的初始化流程:从MAC地址到Socket创建
3.1 初始化W5500:网络参数设置
W5500上电后,寄存器默认值并不是可用状态,需要先设置MAC地址、本机IP、子网掩码、网关。这组参数写入Common区的SHAR、SIPR、SUBR、GAR寄存器。手动逐个写容易错,常见做法是用一个结构体封装,调用驱动库的初始化函数。资源包里的例程也是这样组织的:
wiz_NetInfo netInfo = { .mac = {0x00, 0x08, 0xDC, 0x12, 0x34, 0x56}, .ip = {192, 168, 1, 100}, .sn = {255, 255, 255, 0}, .gw = {192, 168, 1, 1}, .dns = {8, 8, 8, 8}, .dhcp = 0 }; WIZCHIP_Initialze(); // 内部会做SPI配置、复位、PHY初始化 WIZCHIP_NetinfoSet(&netInfo); // 写入网络参数参数说明:MAC地址前三个字节是厂商OUI,后三个字节随意,但同一局域网内不能有重复,否则会出现ARP冲突,表现为网络时通时断。IP地址要和你的电脑在同一个网段,比如电脑是192.168.1.10,板子就设192.168.1.100。子网掩码保持默认,网关填路由器地址,如果你只是用网线直连电脑,网关可以不填,但填上也无妨。DNS在静态IP模式下不会用到,保留即可。
注意:WIZCHIP_Initialze()这个函数名是开源驱动里的经典接口,它内部会拉低RST复位,然后等待PHY稳定。不同驱动版本可能叫wizchip_init(),对应起来就行。使用前确认你的驱动库版本和例程匹配,否则接口参数会不一致。我见过有的开发者把另一个版本的库强行替换进去,结果编译通过但运行时程序死在PHY寄存器等待上。
3.2 Socket配置与TCP连接
W5500有8个独立的Socket,编号0到7,每个Socket有自己的一组寄存器。把某个Socket配置为TCP客户端,需要设置协议模式、本地端口,然后调用connect。典型代码如下:
uint8_t sock = 0; uint16_t local_port = 5000; uint8_t dest_ip[4] = {192, 168, 1, 10}; uint16_t dest_port = 8080; socket(sock, Sn_MR_TCP, local_port, 0); connect(sock, dest_ip, dest_port);注意,socket函数的最后一个参数是Socket的协议标志,对于TCP填0。调用socket后,Socket会进入SOCK_INIT状态。connect函数发送SYN包后立即返回,并不会阻塞等待握手完成,这和你平时在电脑上调用阻塞socket不一样。W5500硬件协议栈在后台完成三次握手,你要做的是轮询Socket状态寄存器:
if(getSn_SR(sock) == SOCK_ESTABLISHED) { // 连接成功,可以开始收发 }我这里使用getSn_SR(sock)读取状态,它返回一个8位状态值,SOCK_ESTABLISHED是0x17。如果你读到的状态一直停留在SOCK_SYNSENT,说明服务器没有响应SYN,先抓包或检查服务器端口。
本地端口的选择:可以固定,也可以每次连接随机。固定端口方便服务器端检测,但如果程序异常断开后快速重连,同一个端口可能被占用,需要调用socket函数之前先close一次。随机端口可以避开这个问题,但服务器要能识别设备身份,否则连接恢复后找不到对应设备。我一般固定端口,但重连前强制disconnect和close,确保端口释放。
3.3 数据收发与轮询机制
收发数据使用send和recv函数,它们直接操作Socket的TX/RX缓冲区。W5500内部有32KB收发缓存,默认分配是每个Socket各2KB,你可以通过Sn_TXBUF_SIZE和Sn_RXBUF_SIZE寄存器调整。对于TCP长连接,建议调大buffer,因为2KB在突发数据时时容易溢出丢包。下面的资源配置把Socket0的收发缓冲区都改成8KB:
uint8_t txbuf[8] = {8, 8, 8, 8, 0, 0, 0, 0}; uint8_t rxbuf[8] = {8, 8, 8, 8, 0, 0, 0, 0}; setSn_TXBUF_SIZE(sock, txbuf); setSn_RXBUF_SIZE(sock, rxbuf);注意:W5500的缓冲区大小值不是直接KB数,而是以KB为单位的寄存器值,比如8表示8KB。8个Socket的缓冲区之和不能超过32KB。这个配置要在打开Socket之前执行,否则不生效。发送数据时:
uint8_t data[10] = "hello tcp"; uint16_t send_len = send(sock, data, strlen((char*)data)); if(send_len < strlen((char*)data)) { // 缓冲区满,等待或重试 }send函数返回实际写入发送缓冲区的字节数,如果小于请求长度,说明缓冲区空间不足。正确做法是等一会儿再发送,或者拆包。接收数据时先查询可读字节数,再读取:
uint16_t available = getSn_RX_RSR(sock); if(available > 0) { uint8_t buf[256]; uint16_t recv_len = recv(sock, buf, sizeof(buf)); if(recv_len > 0) { // 处理数据 } }这里有一个容易踩的坑:getSn_RX_RSR返回的是“环形缓冲区中尚未读取的字节数”,但如果你一次性读取的长度跨过了缓冲区回卷边界,recv内部会处理分段,但前提是单次读取长度不能大于接收缓冲区的剩余空间。如果你的数据包超过缓冲区大小,建议先把缓冲区设到8KB以上,并把应用层包长控制在1KB左右。
轮询机制的实现:在main循环里,每隔一定时间扫描Socket。为什么不设置中断?因为使用中断时需要处理事件标志、读中断寄存器、清标志,而且W5500的中断引脚是低电平有效,处理不好会反复进入中断。对于初学者,轮询更直观。等调试稳定后,再考虑移植到中断或RTOS。一个典型的轮询代码放在主循环里:
void TcpClient_Poll(void) { if(getSn_SR(sock) == SOCK_ESTABLISHED) { uint16_t available = getSn_RX_RSR(sock); if(available > 0) { uint8_t buf[256]; uint16_t len = recv(sock, buf, sizeof(buf)); if(len > 0) { printf("recv %d bytes: %s\n", len, buf); } } } else { // 非连接状态,做重连处理 } }注意:轮询频率不宜过高,否则会浪费CPU在SPI通信上。我一般设置20ms周期。如果在轮询中频繁调用send发送大量数据,要控制发送节奏,避免W5500的发送缓冲被填满后主循环卡死。
TCP是字节流,没有消息边界。如果你每次发送一个结构体,接收端可能一次收到半个包或两个包。常见做法是定义应用层帧格式:帧头+长度+数据+校验。接收时先把数据存入环形缓冲,再根据长度字段解析出完整帧后处理。例程里保留了最简单的透传方式,方便你后续扩展。
4. 避坑:W5500 TCP调试的常见问题
W5500的调试和纯软件排错不同,很多问题指向硬件时序和PHY状态,所以要抓住“SPI通不通、PHY连不连、TCP状态对不对”三个层次。下面五条是我在实际调试中碰过、也看别人踩过的高频问题,每条按现象、原因、解决给出,排序按照从底层到应用层,排查时从前往后过一遍能最快定位。
4.1 现象:SPI读取版本寄存器返回0xFF
原因:最常见是CS片选没有正确拉低,或者SPI引脚接错。也可能是W5500没有从复位中恢复正常,电源供电不足导致芯片处于未启动状态。
解决:用万用表量一下W5500的供电脚,确认是3.3V。然后用逻辑分析仪或者示波器看SPI时序,确认CS在读写期间为低电平。还有一个容易忽略的:W5500的SCS是低有效,但有些模块上拉了不同电平,如果模块上有跳线,确认跳线是否正确。如果还不行,把SPI时钟降到1MHz,排除信号完整性干扰。另外,检查SPI的NSS引脚是否和W5500的SCS一致,有些库默认使用PA4,如果你改成了PB12,要在库的配置头文件里对应修改,否则驱动库内部拉低的GPIO和你实际接的引脚不一致。我遇到过一块坏模块,版本寄存器始终读回0xFF,换一块就好了,芯片也有坏的时候。
4.2 现象:能Ping通但TCP连接失败
原因:Ping使用ICMP协议,硬件协议栈直接响应,不经过socket。而TCP连接失败可能是端口冲突、目标端口未开放、或者本地端口被复用。也可能你设置了防火墙。
解决:先用PC上的TCP调试助手监听端口,确保服务器端代码正常。然后检查W5500的本地端口是否被其他socket占用,换一个端口。还可以在电脑上用抓包工具抓包,看看SYN是否发出、是否有SYN-ACK返回。如果有SYN-ACK返回但connect状态不跳转,考虑W5500的TCP重传次数和超时配置。常用做法是修改Sn_RTIMR寄存器设置重传时间,以及Sn_RCR重传次数。另外,双方MTU不一致会导致大包被丢弃,可以试着把TCP MSS调小,W5500的Sn_MSSR寄存器默认1460,改为1200或更小测试。如果SYN-ACK收到了但connect状态不变,可以查看Sn_CR寄存器确认当前命令是否已经执行完毕,有时候寄存器写入命令后要等待命令位清零,表示命令完成。
4.3 现象:TCP连接稳定但收发数据偶尔丢包
原因:发送端和接收端的buffer大小配置不匹配,或者发送频率过快导致W5500发送缓冲冲突。也可能是SPI速率过高导致数据错误。
解决:先检查socket的TX/RX buffer大小,确保大于最大包长。然后测试持续长包发送,如果丢包,把SPI分频系数调大,比如从2分频改为8分频。我实测过,在飞线连接的情况下,18MHz SPI经常出现数据错位,降到4.5MHz就稳定。另外,注意发送时不能连续写两次send而不判断返回值,send函数会返回实际发送字节数。如果返回值小于待发送长度,说明buffer满,要轮询等待。接收端也要及时取走缓冲区数据,否则W5500的接收缓冲区溢出后新数据会被丢弃。还有,如果你使用串口打印接收数据,注意串口中断和SPI中断共享优先级可能导致数据丢失,优先保证SPI访问不被串口打断。
4.4 现象:复位后寄存器能读能写,但插上网线没有任何反应
原因:PHY没有link up。检查RJ45口指示灯,如果没亮,可能是网线不通或网口变压器损坏。如果指示灯亮但ping不通,检查网络参数是否与路由器同一网段。
解决:读取PHY状态寄存器(Common区偏移0x0036)确认link位。常见做法是在初始化后延时一下,再读取PHY寄存器,因为PHY需要时间同步。示例代码如下:
uint8_t phy_status = W5500_Read(COMMON_REG, 0x0036); if((phy_status & 0x01) == 0) { printf("PHY link down\n"); }有些W5500模块的复位引脚连接到MCU,但上电瞬间如果为高,模块可能没复位完全。这时候手动拉一次复位再初始化。还有一种情况:W5500的PHY默认使用内部时钟,但部分模块需要外部晶振,如果板上没有25MHz晶振,PHY无法完成链路同步,读PHY寄存器会出现某些位异常。购买模块时确认是否配套晶振。
4.5 现象:程序跑一段时间后TCP连接自动断开,无法重连
原因:socket进入了TIME_WAIT或者CLOSE_WAIT状态,没有正确关闭。W5500硬件协议栈虽然管理状态机,但MCU需要及时调用disconnect和close清理socket。
解决:在主循环里增加socket状态机的处理,当检测到SOCK_CLOSE_WAIT时,执行disconnect,再执行close,把socket重新放回SOCK_INIT。另外,如果服务器异常断开,W5500可能不会立即感知,需要启用TCP超时重传和keepalive机制。keepalive可以通过周期发送心跳包实现,也可以使用硬件寄存器配置。我习惯应用层心跳,因为可以附带业务数据,理论也更简单。在实际应用中,如果服务器主动关闭连接,W5500会进入SOCK_CLOSE_WAIT,如果你不处理,Socket一直占用无法重新connect。所以状态机里必须对每个非ESTABLISHED的稳定状态做清理动作。
5. 进阶:把例程改造成可靠的TCP客户端
5.1 增加断线重连与心跳
基础例程通常在main loop里一遍遍connect,不成功就一直重试。可靠的做法是维护一个状态机:IDLE -> CONNECTING -> ESTABLISHED -> RECONNECT。心跳可以定期发送一个固定包,超过超时后主动disconnect并重连。实际项目里我用的心跳周期是30秒,超时3次就判定断线。
void TcpClient_Handler(void) { static uint8_t state = STATE_IDLE; static uint32_t last_heartbeat = 0; switch(state) { case STATE_IDLE: connect(sock, dest_ip, dest_port); state = STATE_CONNECTING; break; case STATE_CONNECTING: if(getSn_SR(sock) == SOCK_ESTABLISHED) { last_heartbeat = millis(); state = STATE_ESTABLISHED; } else if(getSn_SR(sock) == SOCK_CLOSED) { state = STATE_IDLE; delay_ms(5000); } break; case STATE_ESTABLISHED: if(millis() - last_heartbeat > 30000) { send(sock, "hb", 2); last_heartbeat = millis(); } if(getSn_SR(sock) != SOCK_ESTABLISHED) { close(sock); state = STATE_IDLE; } break; } }参数说明:心跳包长度要小于发送缓冲。超时时间根据网络环境调整,局域网可以短,公网要长。STATE_IDLE后的5秒延时防止连续重连导致服务器开启大量TIME_WAIT连接。首次连接时Socket会经历SOCK_SYNSENT,所以状态机里不处理中间状态,只等最终结果。
5.2 中断与RTOS的使用建议
如果主循环轮询不够用,可以将W5500的INT引脚接到STM32外部中断,中断服务函数里只置标志,主循环再处理收发。注意W5500中断是低电平触发,要读取Sn_IR寄存器清除标志,否则会一直进入中断。上RTOS后,把W5500的访问统一放到一个独立任务中,用互斥锁保护SPI总线,其它任务通过消息队列交互。不要把SPI读写放进中断服务函数,因为阻塞太久会影响实时任务。
最后再说一个验证技巧:每次初始化后,按“SPI读版本—查PHY Link—测TCP握手”顺序打印三个状态。从那以后我每调试新板子,都强制走一遍这个流程,能省下半天查线时间。希望帮到你。
本文还有配套的精品资源,点击获取