STM32F103 + W5500 TCP通信实战:硬件协议栈与SPI驱动详解
简介STM32F103与W5500组合的TCP通信例程面向需要快速实现嵌入式联网的物联网开发者。工程以MDK项目形式提供包含SPI接口驱动、W5500寄存器配置、TCP客户端/服务器收发逻辑的完整源码涉及协议栈与状态机管理适合串口转以太网、数据采集上报等场景作参考模板。压缩包共183个文件以45个h头文件与43个c源码文件为核心辅以uvprojx工程配置、hex烧录文件、axf调试镜像及清理脚本覆盖源码阅读到固件烧录的完整流程打包体积仅3.48MB。已有371人学习浏览入门门槛低、实战参考价值较高。研读初始化配置、断开重连与中断收发部分可掌握W5500的SPI时序配置、网络参数设置与数据收发调试方法借助网络调试助手完成同网段联调验证缩短TCP通信功能的上手周期。1. 一块自带 TCP/IP 协议栈的网卡芯片stm32f103 加 W5500 打通网络传输很多人第一次让单片机联网第一反应是移植 LwIP。真要动手才发现协议栈裁剪、网卡驱动的 DMA 收发、内存池调试任何一个环节都可能把节奏拖慢一周。W5500 这颗芯片把 TCP/IP 协议栈用硬件实现内部还集成了 MAC 和 PHYMCU 只需通过 SPI 读写寄存器就能完成 TCP 连接和数据收发。这篇要拆的 stm32f103 W5500 TCP 例程正是这样一套去掉协议栈负担的落地样板从硬件接线、SPI 通信、寄存器操作到 TCP 客户端和服务器的完整收发流程以及联调中常见的问题排查适合正在做数据采集、工业网关、远程控制想把设备稳定接入局域网的开发者。2. 硬件连接与 SPI 通道三根信号线把协议栈接进 MCU2.1 最小电路一块 W5500 模块需要接哪些信号W5500 是 3.3V 供电的以太网控制芯片内部完成了 MAC PHY 两层工作物理层只需要外接一个 25MHz 晶振和带网络变压器的 RJ45 座。市面上常见的 W5500 模块基本把晶振、RJ45、电源去耦全做齐了直接用杜邦线连 MCU 就能开始调。下面这份引脚分配表对应例程的默认配置用的是 STM32F103 的 SPI1 外设STM32F103 引脚W5500 信号方向说明PA5SCLK输出SPI 时钟线PA6MISO输入W5500 数据回传PA7MOSI输出MCU 数据输出PA4SCS输出片选低电平有效PA1RSTN输出硬件复位低电平有效PA0INTn输入中断标志输出可悬空轮询3V3VCC电源3.3V 供电GNDGND地与 MCU 共地如果你用的是 SPI1PA5、PA6、PA7 是硬件固定的不能随便挪SCS、RSTN、INTn 是普通 GPIO想换到其他引脚完全没问题但要记得同步修改 GPIO 初始化代码。最常见的翻车点就是只换了片选引脚GPIO 初始化没改SPI 通信看起来有波形实际一个字节都读不回来。供电方面有两件事要注意一是 W5500 芯片内核和 PHY 都需要 3.3V模块上别直接怼 5V二是模块和 MCU 必须共地不共地的话 SPI 信号电平参考点不一致表现就是数据偶发错误这类问题用示波器都很难一眼看出来只能靠排查习惯规避。2.2 SPI 帧格式16 位地址 8 位控制 数据W5500 的 SPI 不是简单的主从收发每一次寄存器访问都遵循固定帧格式前两个字节是 16 位寄存器地址第三个字节是控制字节里面包含读写方向、访问哪个寄存器区、后面跟几个数据字节再往后才是真正的寄存器数据。// 读取 W5500 单个寄存器 uint8_t w5500_read_reg(uint16_t addr) { uint8_t buf[3]; buf[0] addr 8; // 地址高字节 buf[1] addr 0xFF; // 地址低字节 buf[2] 0x00; // 控制字节读模式 CS_LOW(); HAL_SPI_Transmit(hspi1, buf, 3, 10); uint8_t val 0; HAL_SPI_Receive(hspi1, val, 1, 10); // 接收一个数据字节 CS_HIGH(); return val; } // 写入 W5500 单个寄存器 void w5500_write_reg(uint16_t addr, uint8_t val) { uint8_t buf[4]; buf[0] addr 8; buf[1] addr 0xFF; buf[2] 0x04; // 控制字节写模式 buf[3] val; CS_LOW(); HAL_SPI_Transmit(hspi1, buf, 4, 10); CS_HIGH(); }控制字节里的细节要看芯片手册的位定义最高位是读写标志中间几位选择访问公共寄存器区还是某个 Socket 的寄存器区低两位表示本次操作的数据字节长度。上面这两个函数只覆盖了单字节读写例程里还有批量读写原理完全一样只是控制字节的低两位改成 2 或 4然后连续传多个数据字节。这里有一个容易踩的坑CS 片选必须在整个帧传输期间保持低电平也就是从发地址一直保持到最后一个数据字节结束。如果 CS 在中间被拉高W5500 会认为这次传输作废读出来的是 0xFF写进去的没生效。用 HAL 库时尤其注意不要在每个字节之间手动拉高拉低 CS一次事务处理完整帧。提示控制字节的位定义各厂商驱动库写法略有差异建议以下载例程里的头文件宏定义为准不要自己凭记忆拼字节。2.3 配置 SPI 参数Mode 0、10MHz 上下最稳SPI 通信要稳定关键是时钟极性和相位要和 W5500 对齐。W5500 支持 SPI Mode 0 和 Mode 3例程里配置的是 Mode 0这也是大多数 W5500 例程的默认选择。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; // CPOL 0 hspi1.Init.CLKPhase SPI_PHASE_1EDGE; // CPHA 0 hspi1.Init.NSS SPI_NSS_SOFT; // 软件片选 hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_8; // 72MHz / 8 9MHzW5500 的 SPI 时钟上限按手册标称可以到 33.3MHz但对 STM32F103 这种级别的板子例程普遍把频率压在 9MHz 或 10MHz 左右。频率不是越高越好杜邦线连接时超过 20MHz 很容易出现偶发读写错误走 PCB 板可以稍微激进一点。如果你改过时钟树记得同步调整分频系数保证 SPI 时钟落在 10MHz 上下。2.4 硬件复位时序先拉低 500us 再干活W5500 上电后必须有一段可靠的硬件复位时序否则内部 PHY 的状态可能不对表现是寄存器能读能写但就是 ping 不通。很多第一次用 W5500 的开发者会在这里卡上一整天最后发现只是复位脉冲宽度不够。void w5500_hw_reset(void) { RSTN_LOW(); // 拉低复位引脚 HAL_Delay(1); // 保持至少 500us这里给 1ms 余量 RSTN_HIGH(); // 释放复位 HAL_Delay(10); // 等 PHY 内部稳定再开始 SPI 通信 }这段代码放在 SPI 初始化和网络参数写入之前顺序不能反。如果先初始化 SPI 再复位芯片复位期间 W5500 还没就绪第一次写进去的网络参数可能直接丢失。3. 核心代码拆解从初始化到 TCP 数据收发的完整链路3.1 Socket 寄存器布局先看懂地址怎么排W5500 的寄存器分两大块公共寄存器区管网关、子网掩码、MAC、IP 这些全局参数Socket 寄存器区管每个独立连接的端口、状态、缓存指针。芯片一共提供 8 个独立 Socket每个 Socket 有 2KB 发送缓存和 2KB 接收缓存这些缓存空间通过 Sn_TX_WR、Sn_RX_RD 这类指针寄存器来管理。// Socket n 寄存器基址Socket0 从 0x0400 开始每个 Socket 间隔 0x0100 #define SOCKET_BASE(n) (0x0400 (n) * 0x0100) #define Sn_MR(n) (SOCKET_BASE(n) 0x00) // Socket 模式寄存器 #define Sn_CR(n) (SOCKET_BASE(n) 0x01) // Socket 命令寄存器 #define Sn_SR(n) (SOCKET_BASE(n) 0x03) // Socket 状态寄存器 #define Sn_TX_FSR(n) (SOCKET_BASE(n) 0x20) // 发送缓存剩余空间 #define Sn_TX_WR(n) (SOCKET_BASE(n) 0x24) // 发送写指针 #define Sn_RX_RSR(n) (SOCKET_BASE(n) 0x26) // 接收缓存已收字节数 #define Sn_RX_RD(n) (SOCKET_BASE(n) 0x28) // 接收读指针这些 16 位寄存器都是高字节在前、低字节在后读写时要分两次传。理解这套指针机制是看懂 W5500 收发逻辑的关键发送数据时要把数据写到发送缓存区然后把 Sn_TX_WR 指针往后挪再发一条 SEND 命令触发芯片把数据发出去接收数据时先看 Sn_RX_RSR 有多少字节从接收缓存区读走数据再把 Sn_RX_RD 指针往后挪发一条 RECV 命令释放空间。3.2 网络参数初始化网关、掩码、MAC、IP 一次写完网络参数全部写在公共寄存器区偏移地址固定。例程里的初始化函数长这样void w5500_network_init(void) { uint8_t gw[4] {192, 168, 1, 1}; uint8_t mask[4] {255, 255, 255, 0}; uint8_t mac[6] {0x02, 0x00, 0x00, 0x12, 0x34, 0x56}; uint8_t ip[4] {192, 168, 1, 100}; // 网关地址 GAR偏移 0x0001 w5500_write_bytes(0x0001, gw, 4); // 子网掩码 SUBR偏移 0x0005 w5500_write_bytes(0x0005, mask, 4); // 源 MAC 地址 SHAR偏移 0x0009共 6 字节 w5500_write_bytes(0x0009, mac, 6); // 源 IP 地址 SIPR偏移 0x000F w5500_write_bytes(0x000F, ip, 4); }这里的几个参数直接影响能不能连上网络。MAC 地址在同一个局域网内不能和其他设备重复否则 ARP 会打架IP 必须和你的调试电脑在同一网段子网掩码要一致如果板子和电脑直连、不经过路由器网关可以填路由器的 IP 也可以不填但子网掩码错了就一定不通。3.3 TCP 客户端发起连接并等待状态机TCP 客户端的建立分三步打开 Socket 并设置为 TCP 模式、写入目标服务器 IP 和端口、发送 CONNECT 命令。之后就是等状态寄存器从 INIT 变成 ESTABLISHED。void tcp_client_connect(void) { uint8_t server_ip[4] {192, 168, 1, 50}; uint16_t server_port 6000; // 第 1 步Socket0 设为 TCP 模式Sn_MR 0x01发送 OPEN 命令 w5500_write_reg(Sn_MR(0), 0x01); w5500_write_reg(Sn_CR(0), 0x01); // OPEN // 第 2 步写入目标 IP 和端口Sn_DIPR 偏移 0x000CSn_DPORT 偏移 0x0010 w5500_write_bytes(0x000C, server_ip, 4); // 目标 IP w5500_write_reg(0x0010, server_port 8); // 端口高字节 w5500_write_reg(0x0011, server_port 0xFF); // 端口低字节 w5500_write_reg(Sn_CR(0), 0x04); // CONNECT // 第 3 步轮询状态寄存器0x17 是 SOCK_ESTABLISHED while (w5500_read_reg(Sn_SR(0)) ! 0x17) { if (w5500_read_reg(Sn_SR(0)) 0x1C) { // 0x1C SOCK_CLOSED // 连接失败重新走一遍初始化流程 tcp_client_connect(); return; } HAL_Delay(10); } }W5500 的状态机流转依赖 Sn_SR 寄存器几个常用的状态值要背下来0x13 是 SOCK_INIT0x17 是 SOCK_ESTABLISHED0x1C 是 SOCK_CLOSED。CONNECT 命令发出去之后芯片内部会自己完成 TCP 三次握手MCU 这边只需要轮询状态。局域网内握手耗时通常只有几毫秒但代码里一定要做失败重试不能死等在 ESTABLISHED 上否则服务器没启动时板子会一直卡在循环里。3.4 数据收发读指针和写指针的配合TCP 通信的收发逻辑是整份例程最核心的部分也是和普通 SPI 外设最大的区别。发送数据不是直接写寄存器而是先写数据到缓存区再更新指针、发命令。int16_t tcp_send(const uint8_t *buf, uint16_t len) { uint16_t free_size 0, wr 0; uint8_t tmp[2]; // 检查发送缓存剩余空间 Sn_TX_FSR不够直接返回 w5500_read_bytes(Sn_TX_FSR(0), tmp, 2); free_size (tmp[0] 8) | tmp[1]; if (free_size len) return -1; // 读取当前发送写指针 Sn_TX_WR w5500_read_bytes(Sn_TX_WR(0), tmp, 2); wr (tmp[0] 8) | tmp[1]; // 把数据写到发送缓存区缓存区基址由例程宏决定 w5500_write_txbuf(0, wr, buf, len); // 写指针向后移动超过 2048 要回卷 wr (wr len) % 2048; w5500_write_reg(Sn_TX_WR(0), wr 8); w5500_write_reg(Sn_TX_WR(0) 1, wr 0xFF); // 发送 SEND 命令芯片开始发数据 w5500_write_reg(Sn_CR(0), 0x20); // SEND return len; }发送逻辑里最容易犯的错是跳过 FSR 检查或者不更新 Sn_TX_WR 就发 SEND。W5500 发送缓存只有 2KB如果 FSR 剩余空间不够还硬写会覆盖掉还没发出去的数据表现就是串口显示发送成功服务器却收到乱码或者字节残缺。回卷处理也很关键2KB 的缓存写到最后会回到起始地址不做取模运算指针就飞了。接收逻辑是发送的镜像操作int16_t tcp_recv(uint8_t *buf, uint16_t max_len) { uint16_t rx_len 0, rd 0; uint8_t tmp[2]; // 读 Sn_RX_RSR 得到已接收字节数为 0 说明没数据 w5500_read_bytes(Sn_RX_RSR(0), tmp, 2); rx_len (tmp[0] 8) | tmp[1]; if (rx_len 0) return 0; if (rx_len max_len) rx_len max_len; // 读 Sn_RX_RD 得到接收读指针 w5500_read_bytes(Sn_RX_RD(0), tmp, 2); rd (tmp[0] 8) | tmp[1]; // 从接收缓存区读数据注意也是回卷结构 w5500_read_rxbuf(0, rd, buf, rx_len); // 更新读指针发送 RECV 命令释放缓存 rd (rd rx_len) % 2048; w5500_write_reg(Sn_RX_RD(0), rd 8); w5500_write_reg(Sn_RX_RD(0) 1, rd 0xFF); w5500_write_reg(Sn_CR(0), 0x40); // RECV return rx_len; }RECV 命令不带不行。W5500 的接收缓存是一块固定大小的环形缓冲每发一次 RECV 命令芯片才认为这部分空间可以被新的网络数据覆盖。漏掉这步Sn_RX_RSR 会一直增长直到缓存满了之后新到的数据直接丢弃。3.5 主循环1ms 轮询一次处理收发状态例程的主循环不复杂核心是一个轮询函数加延时int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); uart_init(); // 串口日志波特率 115200 w5500_hw_reset(); // 硬件复位 w5500_network_init(); // 写 IP、MAC、网关 tcp_server_init(6000); // 作为服务端监听 6000 端口 while (1) { tcp_poll(); // 轮询 Socket 状态处理收发 HAL_Delay(1); } }轮询间隔 1ms 对大多数采集和透传场景足够TCP 本身有缓冲区机制MCU 不会因为慢几百微秒丢数据。如果产品对实时性要求高可以把 INTn 引脚接到 MCU 的外部中断上W5500 在接收到新数据或连接状态变化时会产生中断MCU 在中断服务函数里读 SIR 寄存器判断是哪个 Socket 有事件再去做收发处理这样可以省掉轮询的空转开销。4. 编译、下载与联调把例程跑通并验证数据链路4.1 工程目录找到三个要动的地方这份例程的 Keil 工程目录结构很清晰拆开看就几块目录 / 文件职责二次开发时是否要动App/main.c主流程、网络参数、演示逻辑需要改 IP/端口/业务App/tcp_demo.cTCP 客户端/服务端示例流程按业务改收发逻辑BSP/串口、GPIO、SPI 底层初始化换引脚时改W5500/w5500.cW5500 寄存器读写、缓存操作一般不用动W5500/w5500.h寄存器地址宏、缓存基址定义改缓存分配时动MDK-ARM/Keil 工程文件编译入口拿到工程先别急着编译打开 App/main.c 把网络参数过一遍。这套例程把参数集中在文件头部用宏定义管理改起来很直观。4.2 四项必改参数MAC、IP、网关、端口例程默认的参数是写死的直接烧录到你的板子上大概率不通。要改的就这四个地方参数位置默认值改成什么MAC 地址App/main.c 的 mac[]02:00:00:12:34:56局域网内不重复即可本机 IPApp/main.c 的 ip[]192.168.1.100与电脑同网段网关App/main.c 的 gw[]192.168.1.1路由器管理地址目标端口App/main.c 的 server_port6000与调试助手一致改的时候注意 IP 别跟局域网里其他设备冲突最简单的验证方法是在电脑上 ping 一下这个 IP通了就说明被占了。MAC 地址只要保证局域网内唯一就行不用纠结是不是厂商标记软件随便编一个合法的 48 位地址都能用。4.3 编译下载与串口日志用 Keil 打开 MDK-ARM 目录下的工程文件首次编译前确认两个事情一是芯片型号选的是 STM32F103 对应型号二是 Include Path 把 W5500 和 BSP 目录都包含了。这两项没问题直接编译就能过AC5 和 AC6 编译器都兼容。下载用 ST-Link 或者任何你手头顺手的调试器烧录完成后打开串口助手波特率 115200、8 位数据、1 位停止位、无校验。上电后串口会打印初始化日志正常情况下依次能看到复位完成、网络参数写入成功、Socket 打开成功这几行。如果卡在某一行不动对照第 5 章的排查思路去定位。4.4 用网络调试助手完成一次完整验证验证链路通不通最快的办法是电脑上开一个网络调试助手作为 TCP 服务端。操作流程如下电脑上新建一个 TCP Server监听 6000 端口确保电脑防火墙允许程序入站。给板子上电串口日志会打印连接成功的状态。在调试助手发送区输入hello stm32点发送。板子串口会打印收到的数据同时回传一条ok。抓包工具里能看到完整的 TCP 三次握手过程以及后续的 ACK 确认。这五步全部走通说明从 MCU 到 W5500、再到电脑的整条网络传输链路没有问题。后面做业务逻辑开发时这套验证流程就是你的回归测试手段每次改完代码都跑一遍能挡住大多数低级错误。5. 常见问题排查W5500 连不上网时先查这五件事5.1 读寄存器频繁返回 0xFF先怀疑 SPI 参数和复位时序现象代码里读任何寄存器都返回 0xFF写进去的数据读出来全不对SPI 总线上能看到波形但数据全是无效值。原因百分之八十是 SPI Mode 配置和 W5500 不匹配或者硬件复位时序没走完就开始通信。剩下的情况是 CS 片选在事务中间被拉高了。解决把 SPI 配置成 CPOL0、CPHA0Mode 0确认 CS 在整个帧传输期间保持低电平。然后在所有 SPI 操作之前先执行一次完整的硬件复位拉低 RSTN 至少 500us 再释放等待 10ms 再开始读写。按这个顺序排查基本上一抓一个准。5.2 ping 不通但 SPI 通信正常查 25MHz 晶振和网络变压器现象寄存器读写完全正常网络参数也写进去了但电脑 ping 板子的 IP 永远超时。原因W5500 的 PHY 部分工作异常。最常见的是 25MHz 晶振没起振或者频率偏了导致内部 PHY 时钟不对信号发不出去其次是网线直连时用了交叉线而两端网口不支持自动翻转或者 RJ45 座上的网络变压器虚焊。解决用示波器量晶振引脚确认有没有 25MHz 波形幅度是否正常。没有示波器的话先换一根已知正常的网线再检查 RJ45 座和模块的焊接。W5500 模块一般内部带了变压器买带变压器的 RJ45 座或者直接用现成模块能省掉很多物理层问题。5.3 TCP 连接不上检查网段、端口和 Socket 状态现象能 ping 通但 TCP 客户端一直连不上服务器或者服务端模式等不到客户端接入。原因首先是网络参数问题板子的 IP 和电脑不在同一个网段或者网关、子网掩码配置错误其次是端口没配好服务器的监听端口和板子连接的目标端口不一致再就是 Socket 状态没有正确流转比如没有发 OPEN 命令就直接 CONNECT。解决先确认板子和电脑在同一个网段用调试助手监听端口时注意选对协议类型TCP Server。然后在代码里加打印把 Sn_SR 的状态值实时打出来对照状态表看 Socket 卡在哪一步。连接失败时如果是 SOCK_CLOSED重新走一遍完整初始化流程再说。5.4 发送成功但服务端收不到发送缓冲区的顺序错了现象串口打印发送成功网络调试助手那边却一个字节都没收到。原因发送流程的顺序不对。很多人把数据写到发送缓存区后忘记更新 Sn_TX_WR 指针或者忘了发 SEND 命令W5500 根本没触发发送动作另一种情况是没检查 Sn_TX_FSR数据写进了已经被占用的缓存空间把还没发出去的旧数据覆盖了。解决严格按照「检查 FSR - 写缓存区 - 更新 Sn_TX_WR - 发 SEND 命令」的顺序执行每一步做完可以读回寄存器验证。发送缓存只有 2KB超过 2048 字节要注意回卷。把这四个步骤封装成一个函数避免在业务代码里零散操作。5.5 连接用一段时间就掉线缺保活和重连机制现象TCP 连接建立后能正常收发但运行几分钟或几小时后突然断开板子再也连不上服务器。原因TCP 长连接在空闲时可能被中间网络设备回收也可能是服务器主动断开了空闲连接。W5500 本身不会自动重连Socket 状态变成 SOCK_CLOSED 之后如果没有代码去处理板子就一直停在那里。解决在轮询函数里检查 Socket 状态一旦发现是 SOCK_CLOSED立刻执行完整的重连流程重新 OPEN、重新 CONNECT。不要只补一次 connect要设置重试计数和延时防止服务器还没恢复时板子在快速重连循环里空转。这类问题的本质不是硬件玄学而是长连接方案里必须考虑的状态恢复逻辑。6. 二次开发把 TCP 例程改造成 UDP、Modbus 或多 Socket6.1 从 TCP 客户端改成 UDP只改三处UDP 比 TCP 简单得多不需要连接状态机。把 Sn_MR 从 TCP 模式值 0x01 改成 UDP 模式值 0x02初始化后直接通过 Sn_DIPR、Sn_DPORT 指定目标然后就能收发数据。发送用 SEND 命令接收用 RECV 命令指针管理逻辑和 TCP 完全一样唯一区别是省掉了 CONNECT 和状态等待。UDP 的接收函数要处理对方的 IP 和端口如果做广播或多设备通信需要读取 Sn_DIPR 和 Sn_DPORT 来判断数据来源。6.2 在 TCP 链路上叠一个 Modbus 应用层做工业采集时TCP 收发的裸数据通常要包装成 Modbus TCP 格式。改造思路是在 tcp_recv 拿到数据后不直接透传而是先进一个 Modbus 解析函数校验事务标识、协议标识、长度字段和功能码然后构造响应报文通过 tcp_send 发回去。协议解析这层和 W5500 完全解耦你只需要关心 buf 里的字节流W5500 那套寄存器和指针操作不用再碰。6.3 多 Socket 并行一个端口做服务一个端口做监控W5500 有 8 个 Socket可以同时跑不同的协议。改造方法是把例程里的 Socket0 逻辑复制一份到 Socket1注意所有宏定义里的 n 参数跟着变再在 tcp_poll 里轮询两个 Socket 的状态。比如 Socket0 监听 6000 端口做数据服务Socket1 连接到一个远程监控端口上报心跳互不干扰。启动时两个 Socket 各自初始化收发函数通过参数区分底层那套 SPI 通信是共用的不会冲突。这套代码我前后改过好几版最早也栽在复位时序上读了一天寄存器全 0xFF最后用示波器量了才发现是复位脉冲不够宽。后来每次拿到 W5500 相关的新板子我都强制先走一遍硬件复位、SPI 参数、网络配置、Socket 状态这个顺序再碰业务逻辑再也没有被这类基础问题绊住。希望这份拆解能帮你把 stm32f103 加 W5500 的 TCP 例程顺利跑起来少走我当年走过的弯路。本文还有配套的精品资源点击获取