简介:STM32F103与W5500组合实现TCP网络通信的嵌入式开发例程包,面向使用ARM Cortex-M3内核进行物联网设备联网开发的工程师与学习者。资源基于SPI接口驱动W5500硬件协议栈,涵盖初始化配置、TCP客户端/服务器建立连接、数据收发以及网络参数设置等完整流程,对理解硬件TCP/IP卸载与嵌入式以太网应用具有直接参考价值。压缩包共183个文件,包含45个h头文件、43个c源码文件以及编译生成的o目标文件、crf交叉引用文件等,工程配置齐全,便于在Keil环境下直接编译烧录调试。整体包体约3.48MB,轻量紧凑。目前已有369人学习下载。例程中还提供主机服务器同网段联调的思路,可配合网络调试助手验证通信效果,适合需要快速掌握STM32+W5500 TCP通信开发方法的开发者借鉴与二次开发。 今天这篇东西聊一个在嵌入式圈子里很经典的组合:STM32F103搭配W5500做TCP通信,并给出可以直接抄走的例程。如果你正在给设备加网口,或者想快速落地一个数据上报、远程控制的小项目,这个组合几乎是性价比最高的入门路线。W5500把TCP/IP协议栈做进了芯片硬件里,MCU端不需要跑lwIP这类软件协议栈,只要会用SPI读写寄存器,就能稳定地在局域网甚至互联网上收发数据包。从零跑到一个能用的TCP例程,按我下面这套流程走,一两天时间足够。
1. 方案选型:为什么偏偏是F103配W5500
1.1 组合优势与适用边界
先说MCU。STM32F103系列是意法半导体的经典Cortex-M3芯片,主频最高72MHz,Flash从16KB到512KB,常见的F103C8T6板子几十块就能拿到,外设齐全、资料极多,国内嵌入式圈子基本人手一片。再说W5500,这是WIZnet推出的硬件TCP/IP协议栈芯片,内部集成了完整的TCP/IP协议,对外只给MCU留一个SPI接口,MCU应用层写代码就像操作普通外设一样简单。
这个组合最大的价值是分工明确:W5500负责底层头痛的TCP状态机、校验和计算、分片重组,F103只干应用层的活。F103的RAM通常只有20KB到64KB,如果硬跑软件协议栈,光缓冲区和协议栈内存就要吃掉一大块,而且调试起来非常折腾。W5500内部自带32KB收发缓冲,8个独立Socket,MCU这边不需要承担任何协议栈内存开销。实测下来,一个TCP回环服务器从建工程到跑通,比用lwIP省至少一半精力。
不过也得说清楚边界。W5500适合中小数据量、对实时性要求不高的TCP/UDP通信,比如每秒上报几条传感器数据、远程执行一下开关控制,这完全没问题。但如果要做百兆大流量转发、视频流这类高吞吐应用,这个方案就不合适了,得换带MAC的MCU配合软件协议栈。
1.2 选型对比参考
| 方案 | 协议栈位置 | CPU占用 | 硬件成本 | 开发难度 | 典型场景 |
|---|---|---|---|---|---|
| F103 + W5500 | 芯片硬件实现 | 低 | 中 | 低 | 传感器上云、设备控制、工业网关 |
| F103 + lwIP(软件协议栈) | MCU软件实现 | 高 | 低 | 高 | 协议深度定制、RAM足够 |
| F407 + ETH MAC + PHY | MAC + 软件协议栈 | 中高 | 中 | 高 | 高速传输、大吞吐场景 |
选型这件事,核心思路是看你的瓶颈在哪。如果你需要的是快速稳定出产品,而不是研究协议栈内部机制,W5500这条路线就是最优解。尤其是做工业设备、智能家居网关,稳定压倒一切,硬件协议栈的确定性比软件协议栈好很多,不会因为某个中断优先级没调对就随机丢包。
2. 硬件连接与电路设计细节
2.1 SPI引脚连接与电平匹配
W5500和MCU之间走的是标准4线SPI,外加一根片选和一根复位。以STM32F103的SPI1为例,典型接线如下:
- SCLK:SPI时钟,接PA5(SPI1_SCK)
- MOSI:主机输出从机输入,接PA7(SPI1_MOSI)
- MISO:主机输入从机输出,接PA6(SPI1_MISO)
- SCS:片选,任意GPIO都行,习惯上接PA4
- RSTN:复位,任意GPIO,比如PA3
- PMODE0~2:配置W5500引脚工作模式,模块板上一般已经处理好了
这里有个电平匹配问题:W5500供电是3.3V,SPI端口也是3.3V逻辑,跟F103的IO电平完全兼容,可以直接接,不需要电平转换。但如果你用的是5V的MCU或者别的板子,就一定要加电平转换,否则长时间运行很可能把W5500的IO口打坏。
网口变压器和RJ45在模块上基本都集成好了,自己画板的话也可以参考官方参考设计。网上搜"W5500原理图"或"W5500应用电路"能找到一堆现成图纸,建议照抄官方设计,网口部分的走线和阻抗匹配不是新手能随便调的。
2.2 电源、复位与晶振注意事项
W5500的电源设计看起来简单,实际上有几个坑。
电源部分,3.3V引脚附近务必放10uF和0.1uF的去耦电容并联,模拟电源AVDD和数字电源DVDD尽量分开走线,分别滤波。W5500内部有模拟电路(比如PLL和PHY),电源纹波大直接导致网络不稳定,会表现成"时通时断"或者"长时间跑就丢包"。我见过一个项目,板子布板时图省事把AVDD和DVDD直接连一起,结果网络吞吐就是上不去。
复位电路相对简单,RSTN低电平复位需要至少500us的低电平脉冲,可以用阻容复位,但我更推荐用MCU的GPIO直接控制。这样做的好处是上电时序可控,你可以在SPI初始化之前先主动复位W5500,等它稳定了就拉高,然后再开始配置网络参数,避免上电一瞬间W5500还没准备好就乱写寄存器。
晶振方面,W5500需要25MHz外部晶振,搭配两个22pF负载电容就行,很多现成模块也是这么设计的。自己画板时晶振走线要短,尽量远离大电流线路和变压器部分,不然时钟抖动会导致以太网误码率上升,这个间题排查起来很头疼。
3. 软件移植与驱动配置
3.1 官方库还是自己写驱动
W5500官方提供了完整的ioLibrary驱动库,代码开源,API风格接近BSD Socket,上手门槛非常低。直接拿官方库跑例程是最快的路径,我的建议是不要拒绝官方库,先跑通再谈优化。
但如果你想更进一步,从零写W5500驱动其实也不复杂。核心只有一件事:通过SPI帧读写寄存器。W5500的SPI帧格式是:第一个字节是控制字节,包含块选择(公共寄存器区、Socket寄存器区还是收发缓冲区)、读/写标志、寄存器地址高5位;第二个字节是寄存器地址低8位;如果是写操作,后面直接跟数据,读操作则要先读一个字节的虚拟数据,然后才是有效数据。把这个时序看明白,整个驱动就通了。
我建议第一次接触W5500的朋友,先用官方库跑通一个TCP例程,然后对照数据手册把socket()、connect()、listen()、send()、recv()这几个函数的源码翻一遍,看看它们到底操作了哪些寄存器。这个过程花半天时间,但对底层机制的理解会跨一个台阶,以后出问题就知道去哪查了。
3.2 STM32F103的SPI初始化要点
用标准库配置SPI1的代码大致如下:
void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA = SPI_CPHA_Low; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }几个实际工程里总结的要点:
- SPI时钟频率别拉满。F103的APB2外设时钟最高36MHz,SPI1最高能到18MHz(72MHz主频分频4)。实测分频8即9MHz非常稳定,分频4也可以跑,但如果你用杜邦线飞线连接模块,就不建议满分频了,线间电容和干扰会在高速时钟下引发随机的通信错误。这种错误极其难排查,还不如一开始就留足余量。
- 时钟极性相位选模式0:CPOL=0、CPHA=0。W5500官方驱动默认支持SPI模式0,别为了“试试”改成模式3。改模式3也不是绝对不行,但官方库里的某些时序细节可能受影响,纯属给自己找麻烦。
- 片选用软件控制。虽然W5500支持硬件片选配合方式,但标准做法是软件拉低、拉高,始终把SPI_NSS配置成Soft模式,即SPI_NSS_Soft。
- W5500的SCS片选不是普通的"选通"信号,它在SPI帧协议里承担了帧起始和帧结束的作用,每次通信都必须先拉低SCS,发完一帧再拉高。如果把SCS固定接地,W5500根本分不清数据帧边界,通信必然失败。
3.3 网络参数配置与Socket基础概念
初始化W5500除了SPI配置,还需要设置MAC地址、IP地址、子网掩码、网关。官方库的wizchip_init()函数和setSHAR()、setSIPR()这些函数就是干这件事的。MAC地址千万不要和局域网内其他设备冲突,不然轻则通信断断续续,重则直接导致整个局域网广播风暴。批量生产时,MAC地址一定要留一个可以软件配置的入口,不能固死在代码里。
Socket概念上,W5500的每个Socket类似一台独立的“小电脑”,都有自己的Sn_MR模式寄存器、Sn_PORT端口号寄存器、Sn_CR命令寄存器、Sn_SR状态寄存器,以及独立的发送和接收缓冲区。用官方库封装好,MCU端就是几个简单API调用,但心里要清楚底层寄存器大概对应什么。比如socket(s, Sn_MR_TCP, port, 0)这个函数,本质上就是写Sn_MR寄存器为TCP模式,写Sn_PORT寄存器为指定端口号,然后在Sn_CR寄存器下发OPEN命令。
4. 完整TCP例程的实现:服务器与客户端模式
4.1 初始化流程
整理一下从零到TCP通信的完整初始化顺序:
- 复位W5500:拉低RSTN,延时至少500us,拉高
- STM32F103的SPI1基础配置
- 调用W5500官方库的wizchip_init()初始化网络
- 设置MAC、IP、子网掩码、网关、DNS
- 调用socket()打开Socket,指定TCP/UDP模式和端口
实际操作里,我习惯把初始化包在一个函数里,做成“PowerOn_Net()”,然后在主循环的初始化段调用。初始化时顺手把W5500的版本寄存器读回来打印一下,能确认SPI链路是不是通的。这一步能帮你把“SPI没通”和“TCP连不上”这两个问题隔离清楚。
4.2 TCP服务器模式:等待PC端连接
下面这段是典型的服务器模式代码,目标是用网络调试助手在PC上连接开发板,然后做数据回环:
#include "wizchip_conf.h" #include "socket.h" #define TCP_SERVER_PORT 5000 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} }; void TCP_Server_Task(void) { uint8_t buffer[1024] = {0}; int32_t ret; ret = socket(0, Sn_MR_TCP, TCP_SERVER_PORT, 0); if (ret != 0) { return; } while (1) { ret = listen(0); if (ret < 0) { return; } // 等待客户端连接完成三次握手 while (getSn_SR(0) != SOCK_ESTABLISHED) { if (getSn_SR(0) == SOCK_CLOSED) { break; } if (timeout_expired()) { break; } delay_ms(10); } if (getSn_SR(0) == SOCK_CLOSED) { continue; } while (1) { ret = recv(0, buffer, sizeof(buffer)); if (ret > 0) { send(0, buffer, ret); } if (getSn_SR(0) == SOCK_CLOSED) { close(0); break; } } } }这段代码的核心是一个无限循环:监听->等待连接->收发数据->连接断开->再次监听。看起来简单,但它覆盖了TCP服务器最核心的所有状态流转。
实际测试时,PC端用网络调试助手新建一个TCP客户端,目标IP填开发板的IP(比如上面代码里的192.168.1.100),端口填5000,点连接。一旦连接成功,你在PC上发什么,开发板就原样回什么。
4.3 客户端模式:主动连接PC或服务器
设备主动连接服务器的场景更常见,比如数据上报、指令下发。客户端模式的核心是connect函数:
uint8_t server_ip[4] = {192, 168, 1, 50}; uint16_t server_port = 8080; void TCP_Client_Task(void) { uint8_t buffer[1024] = {0}; int32_t ret; ret = socket(0, Sn_MR_TCP, 0, 0); if (ret != 0) { return; } ret = connect(0, server_ip, server_port); if (ret < 0) { close(0); return; } // 等待连接建立 while (getSn_SR(0) != SOCK_ESTABLISHED) { delay_ms(10); if (getSn_SR(0) == SOCK_CLOSED) { close(0); return; } } char *msg = "hello from stm32f103 w5500\n"; send(0, (uint8_t *)msg, strlen(msg)); }注意两个细节。第一,connect()之前Socket必须处于SOCK_CLOSED或SOCK_INIT状态,如果上一次连接没有正确close,直接再connect会失败。第二,connect一个不存在的IP地址时,W5500会花一段时间等待超时(取决于网络重发机制),所以connect之后必须加超时保护,不要无限等下去。我做工程时一般设5秒超时,超时后close掉重新走初始化流程。
4.4 深入理解W5500里的TCP状态流转
如果你想真正搞懂调试,一定要理解W5500内部Socket状态机和TCP三次握手的关系。这里我拆细一点:
- socket()调用后,Socket从SOCK_CLOSED进入SOCK_INIT
- 服务器模式调用listen()后,进入SOCK_LISTEN
- 客户端发来SYN包,W5500硬件自动回SYN+ACK
- 客户端再回ACK,三次握手完成,Socket进入SOCK_ESTABLISHED
- 此时getSn_SR()返回SOCK_ESTABLISHED,MCU就可以收发数据了
整个过程MCU基本不用参与,W5500芯片自己就完成了握手。你要做的只是在主循环里轮询Socket状态,及时响应状态变化。有一种情况比较隐蔽:如果三次握手迟迟没完成,Socket可能一直停在SOCK_SYNSENT(客户端已发SYN)或SOCK_LISTEN(服务器等连接)状态,超时后W5500会自己关闭Socket,程序要能识别并重新打开。
5. 连接失败的常见原因与排查手段
5.1 硬件与链路层排查
把多年实战中踩过的坑整理成一张速查表,遇到问题直接对着查:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| ping不通 | MAC/IP配置错误 | 核对网络参数,确认和PC同网段 |
| ping不通且网口灯不亮 | 网线松动、RJ45焊接不良 | 换网线,检查模块网口 |
| SPI读写全是0xFF | 时钟极性/相位不对 | 检查CPOL=0、CPHA=0 |
| 初始化就失败 | 复位时序不对 | RSTN低电平至少500us |
| SPI时通时断 | 时钟分频过高、飞线过长 | 分频改为8或更大,缩短杜邦线 |
| 能连接但一收数据就断 | Socket收发缓冲区配置错误 | 检查wizchip_init的缓冲区数组分配 |
| 能连上但一段时间后断开 | 路由器/防火墙踢连接 | 软件里加重连机制 |
| W5500发热异常 | 电源短路或3.3V过压 | 断电后用万用表检查VCC对GND |
5.2 软件逻辑高频雷区
排查完硬件,软件层面的几个坑几乎每个人都踩过。
第一个雷是忘了设置网络参数。只初始化SPI没设置IP地址,W5500的IP默认是0.0.0.0,这种状态下不可能连上任何东西。我见过有人花了一下午调TCP,最后发现压根没调用setSIPR()。
第二个雷是缓冲区数组配错。wizchip_init(bufsize, bufsize, 8, 2)这个函数签名里,第二个数组参数负责给每个Socket分配收发缓冲大小,出参方式,这里要特别小心。举例来说,如果给Socket0配16KB、Socket1配16KB,就刚好用完32KB缓冲池;如果还想给Socket2加8KB,总容量就超了,初始化会失败。所以分配前先在纸上算一下总账。
第三个雷是状态轮询不够。W5500默认没有硬件中断来接MCU(除非你配置Socket中断并接INT引脚),所以必须在主循环里定时查询getSn_SR()。我习惯10ms查询一次,这个间隔对绝大多数应用足够,对CPU的占用也低。千万别在收到一次数据后就死等,TCP是异步的,状态随时会变。
5.3 用抓包工具定位问题
如果代码看起来都对,但TCP连接就是建立不起来,这时候别再死磕代码了,上Wireshark抓包。把电脑网口和开发板用一根网线直连(不经过路由器),跑一个抓包,观察是否有SYN包发出、对方是否回SYN+ACK、再是否回ACK。三次握手任何一步断掉,都能从抓包里看出来:
- 只有SYN包,没有SYN+ACK,说明服务器没到或端口不对
- 有SYN+ACK,但没回ACK,说明开发板侧状态异常,可能是Socket没配好
- 三次握手都看到了还连不上,问题出在应用层或者防火墙
6. 从例程到工程:状态机与协议设计
6.1 必须有重连机制
例程里那种一条路走到黑的写法,测试可以,但做成产品一定要加状态机。网络环境是不稳定的,路由器重启、网线松动、服务器端超时断开,随时可能发生。不加重连机制,设备一旦断线就要人工断电重启,这在现场运维中是绝对不能接受的。
我自己的通用状态机设计如下:
- TCP_DISCONNECTED:初始态,负责调用socket()和connect()
- TCP_CONNECTING:connect()调用后,轮询等待SOCK_ESTABLISHED,带超时
- TCP_CONNECTED:连接建立,正常收发数据,同时持续检查是否断开
- TCP_REMOTECLOSED:对端主动断开,执行close()后回到TCP_DISCONNECTED
这个状态机让设备在网络抖动或服务器重启后能自动重新连上,用户基本无感知。代码量不大,但可靠性和体验提升巨大。
6.2 消息边界:不依赖TCP的粘包/分包机制
W5500的recv()一次返回多少字节,取决于当前接收缓冲区里的数据量和传入的缓冲区长度,不保证一次recv能拿到一条完整消息。如果你从对端发一个500字节的包,而传入的buffer只有100字节,那只能先拿到100字节,剩下400字节等下一次recv才有。
因此应用层一定要设计协议格式。最简单的做法是固定长度头+变长体,比如:
// 4字节帧头:0xA5 0x5A + 2字节数据长度 typedef struct { uint8_t head[2]; uint16_t length; } FrameHeader;接收端先收4字节头,解析出数据长度,再根据长度把剩余数据收齐,最后拼成完整帧交给业务层。自己实现一个简单的“帧组装器”字段缓冲,逻辑不复杂,但能彻底避免“这条数据不完整”或“两条数据粘在一起”的问题。
6.3 基于这条TCP链路的扩展方向
TCP链路打通之后,上层的玩法就多了:
- Modbus TCP:把W5500作为一个Modbus TCP从站,PLC和组态软件可以通过以太网直接读写F103的寄存器,很多HMI设备就是这么和单片机通讯的。
- MQTT上云:用TCP连接到MQTT Broker(比如公共测试节点或自建服务器),实现设备数据上云、远程控制。W5500提供的是可靠TCP链路,MQTT应用层协议自己实现或者移植一个轻量级客户端都可以。
- HTTP请求:封装一个极简HTTP客户端,定时POST JSON数据到服务器。在服务端写个简单的接收接口,就能组成一个完整的数据采集物联网系统。
这些扩展的核心,其实都建立在“TCP链路稳定可用”这个前提上。所以不要着急做功能,先把一条TCP链路彻底跑通,后续所有上层协议都是在这个基础上做数据封装和解包。
7. 实际操作中的经验沉淀
7.1 调试工具组合
我平时调试W5500的标配是:网络调试助手模拟对端、一根短网线直连电脑网口、Wireshark抓包,再加一个串口打印。这四样东西组合起来,几乎能解决所有连接问题。开发板刚上电时,先用串口打印出初始化的每一步状态,比如SPI版本读回值、Socket打开是否成功、当前IP是多少。等TCP建立成功后,再把Socket状态的变化过程打印出来,比如SOCK_INIT转SOCK_LISTEN转SOCK_ESTABLISHED,这样整个通讯过程就是透明的。
特别提醒一句:调试时优先用网线直连,不要经过路由器。开发板和电脑直连,物理链路最简单,不会受到路由器防火墙、DHCP分配、广播风暴这些变量干扰。等到直连全通,再插路由器验证跨网段场景。
7.2 工程习惯
虽然是例程,也建议按工程标准写。网络参数(IP、MAC、端口)集中放一个头文件里,用宏定义而不是散落在代码各处;Socket编号用宏定义;状态查询和超时保护封装成独立函数。这样后面扩展、维护、换网络环境都会方便很多。我见过不少项目,为了改一个端口号要全局搜索字符串,这种麻烦真的没必要。
7.3 最后想说的
STM32F103和W5500这个组合,硬件成本不高,软件开发门槛低,稳定性却有硬件协议栈兜底,特别适合小团队快速推出带网口的产品。它可能不是性能最强的方案,但一定是把“快速跑通”这件事做到极致的方案。如果你正在犹豫以太网接入方案,不妨先按这篇文章搭一个最小验证环境,跑一次TCP回环测试,切身感受一下整个流程,再决定要不要在这个方向上深入。
调试W5500时最后再分享一个小技巧:PC端开一个TCP服务器监听固定端口,开发板反复去尝试连接,同时在串口里把每个阶段的状态值打出来,比如将getSn_SR()的返回值换算成对应的Socket状态名称。这样一次异常断开,你能立刻看到断在了哪个状态,网络问题基本不用瞎猜,顺着状态就定位到了。
本文还有配套的精品资源,点击获取