news 2026/10/19 17:13:00

STM32F103+W5500 TCP例程:硬件协议栈实现可靠以太网通信

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103+W5500 TCP例程:硬件协议栈实现可靠以太网通信

简介:面向STM32F103与W5500的TCP通信例程包,专为嵌入式开发者与物联网入门者设计。压缩包共183个文件、大小约3.48MB,其中以45个h头文件、43个c源文件为主体,头文件负责接口声明与寄存器定义,源文件实现具体外设驱动逻辑;另含Keil工程文件、编译生成的axf/hex文件、依赖配置及清理脚本,可直接导入工程编译运行。例程完整覆盖基于SPI接口的W5500驱动初始化、TCP客户端与服务器端的连接建立、数据收发和错误处理流程,并体现标准外设库中定时器、串口等模块的协同配置,有助于理解W5500内部四路独立TCP连接的硬件协议栈实现。使用前需将主机与开发板配置在同一网段,配合网络调试助手即可快速验证以太网数据透传,便于调试分析。目前已有371人学习下载,适合作为嵌入式中低层网络通信开发的直接参考模板。

1. STM32F103 + W5500 TCP例程:不跑协议栈也能实现可靠网络传输

拿到这个例程包的时候,我第一反应是松了一口气。STM32F103做以太网,最头疼的就是协议栈——无论是移植lwIP还是折腾enc28j60,都要处理一堆回调、缓冲区和内存池。而W5500这颗芯片把TCP/IP协议栈做在了硬件里,MCU只管用SPI往寄存器里读写数据,连接、分包、应答全部由芯片自己搞定。这个例程的价值就在这儿:它把F103 + W5500从「听说过能用」变成了「打开就能跑通」的工程模板,包含SPI驱动、寄存器初始化、TCP Server和Client两套收发流程。适合正在做设备联网、数据上报、远程控制这类项目的嵌入式开发者,尤其是那种「不想在协议栈上投入两个月」的团队。

2. 例程包里有什么:从文件结构看懂W5500的代码骨架

拿到例程第一件事不是打开Keil点编译,而是先看文件结构。一个W5500工程能不能快速移植,就看驱动层和应用层分得干不干净。我拆过几种版本的例程,最怕的是那种把寄存器操作和业务逻辑揉在一个文件里的写法——看着功能全,改一个IP地址都要翻半天。

2.1 目录结构:驱动层、协议层、应用层三层分离

常见做法是三层分离。驱动层管SPI读写,协议层管W5500的寄存器配置和Socket操作,应用层管业务逻辑。例程里一般会有这样的划分:

  • Hardware/SPI:F103的SPI1初始化,负责最底层的字节收发
  • W5500:芯片驱动,包括寄存器读写函数、Socket打开/关闭/收发
  • User:main.c里面的TCP Demo,展示Server或Client怎么用
  • Net:网络参数配置,MAC、IP、网关、子网掩码的赋值处
/* W5500驱动层头文件 关键函数声明 */ void W5500_Init(void); // 芯片初始化:复位+网络参数写入 void W5500_WriteByte(uint32_t addr, uint8_t dat); // 写单字节寄存器 uint8_t W5500_ReadByte(uint32_t addr); // 读单字节寄存器 void W5500_WriteBuf(uint32_t addr, uint8_t* buf, uint16_t len); // 写多字节 void W5500_ReadBuf(uint32_t addr, uint8_t* buf, uint16_t len); // 读多字节 uint8_t W5500_SocketOpen(uint8_t sock, uint8_t protocol); // 打开Socket uint8_t W5500_TcpConnect(uint8_t sock, uint8_t* ip, uint16_t port); // TCP连接

寄存器操作函数的地址参数是W5500特有的24位地址格式:前5位是块选择,中间6位是偏移,低位是字节序号。用SPI发送地址时要把这三个部分拼成一个u32整数,然后按高字节到低字节的顺序发出去。很多新手在这里翻车就是因为地址拼错了导致读写位置漂移。

2.2 为什么选W5500而不是软件协议栈

选W5500的核心理由是「确定性」。软件协议栈在收到数据包时要处理中断、链表、超时重传,每个环节都有可能出bug;W5500把这些全部固化在芯片内部,MCU侧的逻辑就变成了简单的「查状态、读数据、写数据」。F103主频只有72MHz,RAM也只有20KB,如果跑lwIP + 操作系统,内存余量很紧张,而且调试网络栈的时间成本远高于调试业务逻辑。

代价是成本高一些,还需要一颗外部晶振。但对于工业设备、电力监控这类对稳定性要求高的场景,多花十几块钱买确定性是划算的。例程里Open、Send、Recv这几个函数封装好的时候,业务开发的复杂度就降到了读写串口差不多的级别。

2.3 硬件接线:除了SPI四根线,别忘了RST和INT

W5500在F103上一般挂SPI1,引脚分配要看板子怎么画。下面是常见接线表,具体以例程里的宏定义为准:

W5500引脚F103引脚说明
SCLKPA5SPI1时钟,最大18MHz
MOSIPA7主机输出,接W5500的MOSI
MISOPA6主机输入,接W5500的MISO
SCSPA4片选,软件控制,拉低选中
RSTPB0复位信号,低电平复位
INTPB1中断输出,可选,轮询方式可不接

RST引脚必须接,不然上电后芯片状态不确定,读寄存器容易读到FF。INT引脚如果用轮询,可以暂时不接,但建议保留——后面调试死等收不到数据的场景时,边沿中断比轮询高效得多。

3. 初始化与移植:SPI、复位时序和网络参数的生效顺序

W5500移植工作量的七成在初始化顺序上。顺序错了,后面怎么调都像是「玄学」。SPI初始化、硬件复位、寄存器配置、Socket打开,这四步必须严格按序走,中间任何一步失败,网络都起不来。

3.1 SPI配置:注意F103的SPI1最大速率的限制

F103的SPI1挂在APB2总线上,时钟最高72MHz,但W5500的SPI时钟上限是18MHz,所以分频至少要4分频。我用HAL库做SPI1初始化时一般这样写:

/* SPI1初始化 时钟18MHz */ SPI_HandleTypeDef hspi1; void SPI1_Init(void) { hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; // 主机模式 hspi1.Init.Direction = SPI_DIRECTION_2LINES; // 全双工 hspi1.Init.DataSize = SPI_DATASIZE_8BIT; // 8位数据 hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // 空闲时钟为低 hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // 第一个边沿采样 hspi1.Init.NSS = SPI_NSS_SOFT; // 软件片选 hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; // 18MHz hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; // 高位先出 HAL_SPI_Init(&hspi1); }

极性和相位要和W5500的数据手册对上——它的CPOL为低、CPHA为第一个边沿,就是模式0。如果改成模式3,芯片可能完全不通;如果改了也不报错,那其实是初始化根本没生效,读到的一直是0xFF。

3.2 复位时序:拉低至少500微秒再释放

复位的坑不在拉低,而在拉低之后等的时长不够。W5500需要至少500微秒的低电平时间,然后拉高,再延时50毫秒左右让内部PLL稳定。例程里一般有W5500_RST函数,核心逻辑是:

/* W5500硬件复位 注意延时顺序 */ void W5500_RST(void) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); // 拉低复位 HAL_Delay(10); // 低电平保持10ms 充裕 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); // 释放复位 HAL_Delay(100); // 等待PLL稳定 }

复位后最好立刻读一下版本寄存器(地址0x0039),正常会读到0x04。如果读出来是0xFF,说明芯片没起来,检查SPI接线、片选极性,或者复位时序是否太短;如果是0x00,可能是SPI模式不对。

3.3 网络参数寄存器配置:网关、MAC、IP写在芯片里

W5500的网络参数寄存器是一块连续区域,包括网关(SHAR/GAR/SUBR)和本机MAC、IP。例程配置如下:

/* 配置W5500网络参数 */ void W5500_NetConfig(void) { uint8_t mac[6] = {0x00, 0x08, 0xDC, 0x12, 0x34, 0x56}; uint8_t ip[4] = {192, 168, 1, 100}; uint8_t gw[4] = {192, 168, 1, 1}; uint8_t sub[4] = {255, 255, 255, 0}; W5500_WriteBuf(0x0000, gw, 4); // 网关地址 GAR W5500_WriteBuf(0x0004, sub, 4); // 子网掩码 SU BR W5500_WriteBuf(0x0008, mac, 6); // 物理地址 SHAR W5500_WriteBuf(0x000F, ip, 4); // IP地址 SIPR }

地址偏移可以直接在头文件里查到——W5500的数据手册里这块寄存器地址是固定的。网关不写的话,跨网段设备包根本发不出去;MAC地址如果两台设备一样,交换机上会互相踢下线。这个例程的寄存器地址是标准定义,不同版本代码的宏命名可能有差异,但偏移量是一样的。

3.4 Socket打开:协议类型、端口和缓冲区大小的绑定

Socket配置在例程里是一个结构体数组,每个Socket有自己的发送/接收缓冲区。打开Socket时要指定协议类型(TCP是0x01),然后绑定本机端口:

/* 打开Socket 0 作为TCP Server */ uint8_t W5500_TCPServerOpen(uint8_t sock, uint16_t port) { W5500_WriteByte(sock + 0x0004, 0x01); // S0_MR:协议类型 TCP W5500_WriteByte(sock + 0x0006, port >> 8); // S0_PORT0:端口高字节 W5500_WriteByte(sock + 0x0007, port & 0xFF); // S0_PORT1:端口低字节 W5500_WriteByte(sock + 0x0001, 0x01); // S0_CR:OPEN命令 HAL_Delay(10); return W5500_ReadByte(sock + 0x0003); // 返回S0_SR Socket状态 }

端口号是高字节在前,如果写反了,监听端口就成了0xXXXX << 8的结果,连不上的时候先查这个。返回的状态如果是0x17(SOCK_INIT),说明Socket初始化成功;如果是0x00,说明命令没有被接受,要检查前面是否先执行了关闭命令。

4. TCP通信实战:Server和Client的收发流程与状态机

Socket打开之后,才真正进入TCP收发环节。例程一般给出Server和Client两种场景,我只写核心差异——Server用LISTEN被动等连,Client用CONNECT主动去连。接收和发送的流程是一致的:查状态、读长度、读数据。

4.1 TCP Server:监听、Accept和一次完整的收发循环

Server的工作分三段:先LISTEN进入监听,然后轮询S0_SR寄存器等客户端连入,再处理收发。核心代码如下:

/* TCP Server 完整收发流程 */ uint8_t W5500_TCPServerPoll(uint8_t sock) { uint8_t status = W5500_ReadByte(sock + 0x0003); // 读Socket状态 if (status == SOCK_INIT) { /* 初始状态 执行LISTEN命令 */ W5500_WriteByte(sock + 0x0001, 0x02); // S0_CR: LISTEN } else if (status == SOCK_ESTABLISHED) { /* 连接建立 可以收发数据了 */ uint16_t len = W5500_ReadRxSize(sock); // 查接收缓冲区数据长度 if (len > 0) { W5500_ReadRxBuf(sock, rx_buffer, len); // 读数据 W5500_WriteTxBuf(sock, rx_buffer, len); // 回写(回声) W5500_WriteByte(sock + 0x0001, 0x20); // S0_CR: SEND } /* 检查连接是否被对端关闭 */ if (W5500_ReadByte(sock + 0x0001) == 0x00) { W5500_WriteByte(sock + 0x0001, 0x04); // S0_CR: DISCON命令 } } else if (status == SOCK_CLOSE_WAIT) { /* 对端关闭 本端也关闭 */ W5500_WriteByte(sock + 0x0001, 0x08); // S0_CR: CLOSE W5500_SocketOpen(sock, 0x01); // 重新打开 回到INIT } return status; }

len的最大值受Socket接收缓冲区大小限制。例程默认是2KB,一次读超过2KB会出错,需要分批读。ReadRxSize返回的是当前接收缓冲区里积压的未读字节数,单位是字节,W5500硬件会自动把收到的数据搬进芯片自带的RAM里,MCU只需要执行「读长度、读数据」两步。

4.2 TCP Client:连接失败重试和Send数据的正确姿势

Client多了一个连接动作,难点在超时重试。TCP建立连接需要握手,失败原因可能是IP不通、端口没开、防火墙拦截,MCU上位机软件不会告诉你为什么,只能自己设超时。例程里的连接逻辑一般是:

/* TCP Client 连接服务器 */ uint8_t W5500_TcpConnect(uint8_t sock, uint8_t* ip, uint16_t port) { W5500_WriteByte(sock + 0x0004, 0x01); // S0_MR: TCP W5500_WriteBuf(sock + 0x000C, ip, 4); // S0_DIPR: 目标IP W5500_WriteByte(sock + 0x0010, port >> 8); // 目标端口高位 W5500_WriteByte(sock + 0x0011, port & 0xFF); // 目标端口低位 W5500_WriteByte(sock + 0x0001, 0x01); // OPEN HAL_Delay(5); W5500_WriteByte(sock + 0x0001, 0x04); // CONNECT /* 轮询等待连接结果 最多等3秒 */ uint32_t timeout = HAL_GetTick() + 3000; while (HAL_GetTick() < timeout) { if (W5500_ReadByte(sock + 0x0003) == SOCK_ESTABLISHED) return 1; // 连接成功 if (W5500_ReadByte(sock + 0x0003) == SOCK_CLOSED) break; // 连接失败 退出等待 HAL_Delay(1); } return 0; // 超时 }

连接超时时间我一般设3秒,这个值取决于现场网络情况。局域网内一般几十毫秒就能建立连接;跨公网会慢一些,但超过3秒还没建立,基本就是IP或端口配置有问题。连接失败后不要立刻重连,至少等1秒再试——TCP本身有重传机制,连续快速重试容易把W5500的Socket状态弄乱,出现连不上的假死现象。

4.3 状态机轮询:别让main函数卡死在等待里

例程里如果只有上面的轮询写法,那是个Demo,不是产品代码。实际项目里main函数要同时处理按键、显示、传感器采集,不能被一个连接超时卡住3秒。我的做法是把TCP收发拆成状态机,放在定时中断或主循环里分片执行:

/* 状态机分片处理TCP 每次调用只处理一步 */ typedef enum { TCP_STATE_IDLE, TCP_STATE_LISTEN, TCP_STATE_ESTABLISHED, TCP_STATE_RECV_WAIT, TCP_STATE_SEND_WAIT } TcpState; void TcpTask(void) { switch (tcp_state) { case TCP_STATE_IDLE: W5500_SocketOpen(0, 0x01); W5500_WriteByte(0, 0x0001, 0x02); // LISTEN tcp_state = TCP_STATE_LISTEN; break; case TCP_STATE_LISTEN: if (W5500_ReadByte(0, 0x0003) == SOCK_ESTABLISHED) tcp_state = TCP_STATE_ESTABLISHED; break; case TCP_STATE_ESTABLISHED: if (W5500_ReadRxSize(0) > 0) tcp_state = TCP_STATE_RECV_WAIT; break; /* RECV_WAIT和SEND_WAIT分别处理收发 */ } }

这种写法的收益是实时性:不管网络慢还是快,主循环始终能转起来。代价是状态变量要定义好,漏了某个状态转移条件,Socket卡在一个奇怪的状态里大半天找不到原因。调试这种状态机时,我一般会在每个分支结尾加打印,先确认状态有没有跳过去,再谈收发的正确性。

5. 避坑与常见问题:连接失败、偶发卡死和SPI超速

W5500的坑不是「不工作」,而是「大部分时候工作,偶尔抽风」。这里整理四条我实测过的踩坑记录,每一条都是线上环境真实让项目交付延期的类型。

5.1 现象:TCP连接死活建立不上,上位机报超时

原因有四个方向:IP地址和上位机不在同一网段、网关没配置、目标端口写反高低字节、Socket没有成功打开。排查时先PING一下W5500的IP通不通;通的话用网络调试助手监听端口,再查代码里S0_DIPR和S0_PORT0/1的赋值。

解决方法是逐个排除:先保证本机IP和上位机同网段,其次把网关手动设置为路由器地址,然后打印S0_SR状态确认Socket真的到了SOCK_INIT,最后核对端口高低字节。这个排查过程我走了三遍,最后发现是端口写反了——S0_PORT0写的是低字节,导致实际监听端口变成了256倍的关系。

5.2 现象:收发一段时间后卡死,看门狗复位

原因大概率是接收缓冲区溢出没有处理。W5500的接收缓冲区是芯片内部RAM,容量固定;如果读速度跟不上接收速度,缓冲写满后芯片会丢弃新包,但状态寄存器不会主动通知。解决办法是每次读完后强制清一下接收缓冲区指针,并且把读取周期缩短。

我在例程里加过保护代码:每次主循环先查接收剩余空间,如果剩余量小于总容量的十分之一,先进行一次批量读取把数据挪走,再继续跑业务。实际效果是连续跑48小时没有复位过,之前是一天大概卡死三次。

5.3 现象:SPI时钟调到36MHz就乱码,降到18MHz正常

原因明确:W5500 SPI最高支持18MHz,F103的SPI1在APB2总线上可以到72MHz,分频2就是36MHz,芯片扛不住。这不是软件bug,是电气特性。

解决方法是分频调成4或以上,也就是时钟不超过18MHz。如果板子走线长,建议降到9MHz,数据线太长时信号反射会导致偶发的位错位。一个判断技巧:通信乱码如果是随机字节错位,优先降SPI时钟;如果是固定位错,检查CPOL/CPHA。

5.4 现象:上电后读版本寄存器是0xFF,芯片好像没工作

原因一般是复位时序不够,或者SCS片选引脚极性反了。W5500的片选是低有效,如果代码里初始化为高电平,芯片就一直处于未选中状态,所有SPI通信都不生效。

解决方法是先测复位引脚电平,上电后应该是高;再测SCS引脚在读写前后有没有拉低拉高的动作;最后用逻辑分析仪抓SPI波形,确认有8字节以上的帧在传输。我遇到过一次极端情况是晶振没起振,示波器量X1引脚没有正弦波,换了一颗晶振就好了。

6. 网络抓包验证与收尾技巧:把TCP收发做成带超时的状态机

例程能收发数据只是起步,但要交付,还差两个动作:抓包验证和超时保护。验证方法是用PC端Wireshark抓以太网包,过滤条件设tcp.port == 5000,观察三次握手有没有完成、数据段有没有被ACK。如果三次握手频繁重传,说明物理链路或者对端Socket有问题;如果握手成功但数据不显示,检查W5500的SEND命令有没有被执行。

一个实用技巧:把所有的Socket操作都加上超时上限。W5500的操作大部分是瞬时完成的,但CPLD/交换机的响应时间不可控,轮询函数如果写成了死等,一旦对端异常,整个系统就卡死。我的习惯是设计一个全局的Tick计时,所有等待循环都带HAL_GetTick()退出条件。

从那以后我每次用W5500做TCP项目,都强制自己走一遍同样的验证路径:先写最小工程确认SPI通,再写寄存器读写确认版本号,然后建TCP连接跑回环测试,最后才接业务逻辑。这套流程能过滤掉九成以上的低级问题,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 17:10:37

Windows下TCP通信从入门到实战:Winsock编程、多线程与select模型详解

1. 从零理解Windows下TCP通信的底层逻辑很多人第一次接触网络编程&#xff0c;脑子里冒出来的第一个问题就是&#xff1a;我写的代码到底是怎么把一串字符从我的电脑送到另一台电脑上的&#xff1f;这个问题如果不想清楚&#xff0c;后面写出来的代码就是照葫芦画瓢&#xff0c…

作者头像 李华
网站建设 2026/10/19 17:09:22

STM32F103 DAC音频输出实验:从波形可控到参数可算的完整方案

简介&#xff1a;这份资源面向嵌入式开发初学者与STM32学习者&#xff0c;围绕STM32F103ZET6内置12位DAC实现音频输出这一实验展开&#xff0c;帮助读者理解数字信号到模拟电压的转换过程&#xff0c;并完成从WAV音频数据到扬声器发声的完整链路。压缩包共181个文件&#xff0c…

作者头像 李华
网站建设 2026/10/19 17:07:28

Open Code Review:CLI+Agent+Git Diff 的自动化评审工作流

1. 项目概述&#xff1a;这不是一个工具&#xff0c;而是一套可落地的开源代码评审工作流“open-code-review”这个词乍一听像某个新发布的开源项目名&#xff0c;但其实它代表的是一种正在快速演进的工程实践范式——把代码评审&#xff08;Code Review&#xff09;这件事&…

作者头像 李华
网站建设 2026/10/19 17:06:21

基于YOLOv11的无人机检测识别系统实战解析

无人机反制是这两年非常热的方向&#xff0c;无论是机场净空保护、大型活动安保&#xff0c;还是军事和公共安全领域&#xff0c;对低空慢速小目标的探测识别需求都非常旺盛。做毕设选了这个题目&#xff0c;等于踩中了一个既有学术价值、又有实际落地场景的赛道。我当初带学生…

作者头像 李华
网站建设 2026/10/19 17:06:18

open-code-review实践:让代码评审从走形式变为聚焦逻辑与架构

1. 代码评审这件事&#xff0c;为什么总在"走形式"和"挑刺"之间反复横跳我是从2022年下半年开始认真对待code review的。在此之前&#xff0c;我和大多数研发团队一样&#xff0c;把评审当成走流程&#xff1a;开了PR&#xff0c;挂两天没人看&#xff0c;…

作者头像 李华
网站建设 2026/10/19 17:06:04

工业缺陷检测数据集实战:AeBAD叶片异常检测与YOLO训练全解析

做工业视觉检测的同行应该都有体会&#xff0c;缺陷检测领域最缺的不是模型&#xff0c;而是干净能用的数据集。常规公开数据集要么目标大而清晰&#xff0c;要么缺陷类别就一两个&#xff0c;拿到真实的产线、孔探场景根本不够用。航空发动机叶片的异常检测更是如此&#xff0…

作者头像 李华