Pico+W5500实现TCP客户端:硬件连接、三次握手与状态机详解

发布时间:2026/9/11 16:17:48
Pico+W5500实现TCP客户端:硬件连接、三次握手与状态机详解 1. 项目概述为什么用 Pico W5500 做 TCP 客户端而不是直接上 ESP32MicroPython 在嵌入式物联网开发里是个“轻量但够用”的存在——它不像 C 那样要反复调寄存器、写驱动也不像 Python 全功能版那样吃内存、占 Flash。但问题来了树莓派 Pico 自带的 RP2040 芯片没有原生以太网控制器USB 只支持 Device 模式不能当 Host 接网卡UART、SPI、I2C 这些外设接口倒是一应俱全。所以想让 Pico 跑 TCP必须靠外部 PHYMAC 芯片搭桥。W5500 就是这个桥里最稳、最省心、最“MicroPython 友好”的那一座。你可能看过很多教程用 ESP32 做 TCP 客户端——毕竟它内置 Wi-Fi TCP/IP 协议栈一行import socket就能开连。但现实场景里Wi-Fi 并不万能工厂车间电磁干扰强Wi-Fi 信号衰减快工业现场要求低延迟、高确定性UDP 丢包率动辄 5%~10%TCP 又受 Wi-Fi 重传机制拖累还有些环境压根禁用无线比如防爆区、医疗设备邻近区。这时候有线以太网就成了刚需——而 W5500 正是为这种刚需设计的它把 MAC、PHY、TCP/IP 协议栈全集成在一颗芯片里只留 SPI 接口和几根控制线给主控Pico 只需发指令、读状态、收发数据不用管 ARP 是怎么发的、SYN 包怎么校验、重传超时怎么算——这些全由 W5500 硬件完成。再看热词里反复出现的 “tcp三次握手”、“tcp connect超时”、“客户端和服务端”说明很多人卡在连接建立环节。不是代码写错了而是没理解 W5500 的状态机怎么和 MicroPython 协同工作它不像 Linux socket 那样抽象而是暴露了底层寄存器级的状态位比如 Sn_SR 寄存器里的 SOCK_ESTABLISHED、SOCK_CLOSED。你得主动轮询、判断、清空中断标志否则看似connect()成功了实际只是发出了 SYN还没收到 SYN-ACK。我第一次调试时就栽在这儿——串口打印显示“Connected”但服务器日志根本没记录新连接抓包一看SYN 发出去了SYN-ACK 却被 W5500 丢弃了原因是没及时清除 Sn_IR 寄存器里的 TIMEOUT 中断标志导致后续操作被锁死。关键词里“micropython下载”、“w5500原理图”、“w5500应用电路”也透露出实操门槛很多人买了模块接上线烧完固件一跑代码就报OSError: [Errno 113] EHOSTUNREACH或OSError: [Errno 104] ECONNRESET然后开始怀疑是不是固件版本不对、是不是 Pico 引脚接错了、是不是网线有问题……其实 80% 的问题出在三个地方SPI 速率设太高W5500 最高只支持 33MHz但 Pico 默认 SPI 时钟是 100MHz必须手动降频、CS 引脚电平逻辑反了有些模块低电平选中有些高电平原理图上标的是 ACTIVE LOW 还是 ACTIVE HIGH 得盯紧、以及最关键的——W5500 的硬件复位没执行到位上电后必须拉低 RST 引脚至少 2ms否则内部寄存器处于随机态TCP 初始化必失败。所以这篇不是讲“怎么写 socket 代码”而是讲清楚Pico 怎么通过 SPI 把命令喂给 W5500W5500 怎么把 TCP 状态翻译成寄存器值MicroPython 又怎么读懂这些值并做出正确响应。它面向的是已经会点亮 LED、能用 UART 打印、知道machine.Pin怎么用但第一次面对“外挂网络芯片”手足无措的开发者。如果你正拿着一块蓝色的 W5500 模块、一根杜邦线、一台 Pico在电脑前反复烧录、拔插、抓耳挠腮——那接下来的内容就是为你写的。2. 硬件连接与初始化从原理图到引脚映射一个都不能错2.1 W5500 模块核心信号与 Pico 引脚分配逻辑W5500 是标准 SPI 设备但它的控制信号比普通 SPI 多两根RST复位和 INT中断输出。很多初学者只接了 MOSI、MISO、SCK、CS结果发现w5500.init()永远卡住或者w5500.getSn_SR(0)返回 0x00 ——这不是代码问题是硬件没唤醒。我们先拆解这六根线的真实作用SCK串行时钟Pico 提供时钟W5500 同步采样。注意W5500 数据手册明确标注最大 SCK 频率是 33MHz而 RP2040 的 SPI 外设默认可设到 100MHz。如果初始化时 SPI 时钟设为 50MHzW5500 会采样错误读寄存器返回全 0xFF 或全 0x00。实测下来20MHz 是最稳妥的上限既能保证速度又留足余量应对 PCB 走线容差。MOSI主出从入Pico 发送命令和数据给 W5500。这根线单向无需额外逻辑。MISO主入从出W5500 返回寄存器值或接收缓冲区数据。关键点在于W5500 的 MISO 是开漏输出需要上拉电阻通常模块已内置 4.7kΩ。如果自己画板没加MISO 会浮空读到的数据全是 0。CS片选低电平有效。这里有个易错点不是所有 W5500 模块的 CS 引脚都默认接 GND。有些廉价模块为了省事把 CS 直接连到地变成“常选中”——这会导致多个 SPI 设备冲突比如你同时接了 OLED 和 W5500或者 W5500 无法进入低功耗模式。务必用万用表蜂鸣档确认 CS 引脚对地是否导通。如果是常接地必须剪断跳线改接到 Pico 的 GPIO。RST复位这是最容易被忽略的致命信号。W5500 上电后内部状态机是未定义的。必须由 MCU 主动拉低 RST 至少 2ms再拉高才能完成硬件复位。很多模块的 RST 引脚悬空或接 VCC等于没复位。实测不执行 RSTw5500.init()函数里读VERSIONR寄存器地址 0x0039会返回 0x00而正常值是 0x04W5500 版本号。一旦版本号读错后续所有寄存器操作都无效。INT中断W5500 用这根线通知 Pico “有事发生”比如 TCP 连接建立成功、数据到达、发送完成。虽然 MicroPython 示例代码常把它悬空用轮询代替中断但生产环境强烈建议接上。因为轮询会占用 CPU而 INT 是边沿触发Pico 可以休眠等待中断省电且实时性高。INT 默认是低电平有效需接 Pico 的 GPIO 并配置为Pin.PULL_UP这样空闲时为高电平中断时拉低。我用的是一块常见的蓝色 W5500 模块带 RJ45 接口和磁耦合变压器背面丝印标着 “W5500 V1.0”。对照其原理图来自 WIZnet 官方参考设计关键引脚定义如下模块丝印功能推荐 Pico 引脚说明VCC3.3V 电源VSYS 或 3V3_OUT严禁接 5VW5500 IO 电压是 3.3V接 5V 会烧毁GND地任意 GND必须共地否则 SPI 通信失败SCKSPI 时钟GP10选 SPI0 的 SCK避免与 UART 冲突MOSISPI 数据输出GP11SPI0 的 TXMISOSPI 数据输入GP12SPI0 的 RXCS片选GP13低电平有效软件控制RST复位GP14必须由软件主动控制INT中断输出GP15配置为Pin.IN, Pin.PULL_UP提示GP10-GP15 是 SPI0 的标准引脚组但并非唯一选择。如果你的 Pico 已经用 GP10 做了 LED 控制可以改用 SPI1GP18-SCK, GP19-MOSI, GP16-MISO只要在代码里初始化SPI(1)即可。关键是保持 SCK/MOSI/MISO 三线同属一个 SPI 外设否则时序不同步。2.2 初始化流程四步走缺一不可W5500 的初始化不是调一个函数就完事而是一个严格的状态迁移过程。官方 datasheet 第 12 页的“Initialization Flowchart”画得很清楚但 MicroPython 封装库如w5500.py往往把细节隐藏了。为了 debug 方便我建议手动拆解每一步并加入状态检查第一步硬件复位RSTrst_pin machine.Pin(14, machine.Pin.OUT) rst_pin.value(0) # 拉低 time.sleep_ms(3) # 至少 2ms rst_pin.value(1) # 拉高 time.sleep_ms(10) # 等待内部 PLL 锁定这一步之后W5500 内部寄存器全部清零但还没开始工作。此时读VERSIONR应该是 0x04。第二步SPI 初始化与寄存器读取验证spi machine.SPI(0, baudrate20_000_000, polarity0, phase0, bits8, firstbitmachine.SPI.MSB, sckmachine.Pin(10), mosimachine.Pin(11), misomachine.Pin(12)) cs_pin machine.Pin(13, machine.Pin.OUT) cs_pin.value(1) # CS 默认高电平未选中 # 测试读取 VERSIONR (0x0039) cs_pin.value(0) spi.write(b\x00\x39) # 发送地址高位低位 spi.readinto(buf : bytearray(1)) # 读 1 字节 cs_pin.value(1) print(VERSIONR , buf[0]) # 应输出 4如果输出不是 4检查RST 是否执行、SPI 时钟是否过高、MISO 是否上拉、CS 是否真被拉低。第三步设置 MAC 地址与 IP 参数W5500 不像 ESP32 那样自动获取 IP必须手动配置。即使你用 DHCP也要先设一个临时静态 IP 才能启动 DHCP 客户端。关键寄存器SHAR0x0008~0x000D6 字节 MAC 地址格式00:08:DC:XX:XX:XX前 3 字节是 WIZnet OUI后 3 字节可自定义避免冲突。SIPR0x000F~0x00124 字节本地 IP如192.168.1.100→[192,168,1,100]。GAR0x0001~0x0004网关 IP如192.168.1.1。SUBR0x0005~0x0008子网掩码如255.255.255.0。注意这些寄存器是按字节写入的不能一次写 4 字节。必须循环写# 写 MAC 地址 (SHAR) mac b\x00\x08\xDC\x12\x34\x56 for i, b in enumerate(mac): w5500.write_reg(0x0008 i, b) # 写本地 IP (SIPR) ip b\xC0\xA8\x01\x64 # 192.168.1.100 for i, b in enumerate(ip): w5500.write_reg(0x000F i, b)第四步打开 Socket 并设置为 TCP 客户端模式W5500 有 8 个独立 SocketSn每个可独立配置协议类型。Socket 0 最常用Sn_MR0x0400 n*0x100Socket 模式寄存器写0x01表示 TCP 客户端。Sn_PORT0x0404 n*0x100本地端口号客户端可设为 0由 W5500 自动分配。Sn_DHAR0x0406 n*0x100目标 MAC用于 ARP客户端可先填 0。Sn_DIPR0x040C n*0x100目标 IP如192.168.1.200。Sn_DPORT0x0410 n*0x100目标端口如 8080。注意Sn_DIPR和Sn_DPORT必须在Sn_MR设为0x01之后写否则 W5500 会忽略。这是 datasheet 里埋得很深的坑。2.3 实操心得那些原理图上不会写的细节磁耦合变压器不是摆设W5500 模块上的那个方形黑色元件是网络变压器如 HR601680它隔离高压、抑制共模噪声。如果用杜邦线直连 RJ45 插座绕过变压器通信距离超过 1 米就会丢包。我试过用飞线短接变压器初级结果 ping 都不通——不是芯片坏了是共模干扰让差分信号失真。网线必须是直连线Straight-throughW5500 模块是 MDI-X 端口即交换机模式接 PC 或路由器要用直连线1-1,2-2,3-3,6-6。如果误用交叉线CrossoverTX/RX 对调物理层就通不了。用测线仪测一下或者直接换根新线试试。Pico 的 3V3_OUT 供电能力有限W5500 模块峰值电流约 120mA建立连接时而 Pico 的 3V3_OUT 最大输出 500mA理论上够用。但实测发现如果同时接了 OLED 屏幕100mA和 W5500上电瞬间电压跌到 2.8VW5500 初始化失败。解决方案用外部 3.3V 电源如 AMS1117 模块单独给 W5500 供电Pico 只提供信号线。CS 引脚的“电平保持”陷阱MicroPython 的spi.write()不会自动控制 CS。很多库在write()前拉低 CS写完拉高。但如果代码里有异常比如connect()超时抛出 OSErrorCS 可能卡在低电平导致下次 SPI 操作失败。我的做法是在每个 SPI 操作前后加try/finallydef spi_write(addr, data): cs_pin.value(0) try: spi.write(addr.to_bytes(2, big) data) finally: cs_pin.value(1)3. TCP 客户端核心流程从 connect() 到 send()/recv() 的完整状态机解析3.1 connect() 不是原子操作拆解三次握手的每一帧在 Linux 或 ESP32 上socket.connect()是阻塞调用返回即表示连接建立。但在 W5500 上它只是启动连接流程后续状态必须轮询判断。这是因为 W5500 的 TCP 状态机是硬件实现的MicroPython 只是它的“操作员”。W5500 的 Socket 状态寄存器Sn_SR地址0x0402 n*0x100有 8 种状态其中 TCP 客户端相关的是状态值十六进制名称含义如何触发0x00SOCK_CLOSED关闭初始化后默认状态0x13SOCK_INIT初始化完成写Sn_MR0x01后自动进入0x14SOCK_LISTEN监听服务端用客户端不会进入0x17SOCK_SYNSENT已发 SYNSn_CR0x01CONNECTION后进入0x18SOCK_ESTABLISHED已建立收到 SYN-ACKACK 后进入0x1CSOCK_CLOSE_WAIT对方关闭服务端发 FIN 后进入0x1FSOCK_CLOSED彻底关闭Sn_CR0x10DISCON后进入关键点Sn_CRCommand Register地址0x0401 n*0x100是“触发器”写入特定值会改变状态。0x01表示发起连接0x02表示监听0x10表示断开。所以一个完整的connect()流程是确保Sn_SR 0x13SOCK_INIT写Sn_CR 0x01→ 状态变为0x17SOCK_SYNSENT轮询Sn_SR直到变为0x18SOCK_ESTABLISHED或0x00失败但问题来了W5500 在SOCK_SYNSENT状态下如果 3 秒内没收到 SYN-ACK会自动转为SOCK_CLOSED并置位Sn_IR[0]TIMEOUT 中断。如果你不读Sn_IR清标志下次写Sn_CR会失败。我写的connect()函数实测如下def connect(self, ip, port): # 1. 设置目标 IP 和端口 self._write_socket_reg(0x040C, ip) # Sn_DIPR self._write_socket_reg(0x0410, port.to_bytes(2, big)) # Sn_DPORT # 2. 发起连接 self._write_socket_reg(0x0401, b\x01) # Sn_CR CONNECTION # 3. 轮询状态超时 5 秒 start time.ticks_ms() while time.ticks_diff(time.ticks_ms(), start) 5000: sr self._read_socket_reg(0x0402)[0] # Sn_SR if sr 0x18: # ESTABLISHED return True elif sr 0x00: # CLOSED - 失败 # 读 Sn_IR 清中断 ir self._read_socket_reg(0x0402)[0] # Sn_IR 地址是 0x0402? 不是 0x04020x100*n? 等等查 datasheet... # 实际 Sn_IR 地址是 0x0402 n*0x100但值在 Sn_SR 同一寄存器不Sn_IR 是 0x0402 n*0x100 的低字节 # 正确Sn_IR 地址是 0x0402 n*0x100读 1 字节然后写回 0 清标志 self._write_socket_reg(0x0402, b\x00) # 清 Sn_IR return False time.sleep_ms(10) return False注意Sn_IRInterrupt Register地址是0x0402 n*0x100和Sn_SR同地址读0x0402得到的是Sn_SR写0x0402是清Sn_IR。这是 W5500 的设计同一个地址读是状态写是清中断。很多库文档没写清楚导致清中断失败。3.2 send()数据如何从 Pico 内存流进 W5500 的 TX 缓冲区W5500 的每个 Socket 有独立的 TX 缓冲区默认 2KBsend()的本质是把数据拷贝到这个缓冲区再触发硬件发送。流程分三步第一步检查 TX 缓冲区剩余空间W5500 用Sn_TX_FSR地址0x0420 n*0x100寄存器报告空闲字节数。读 2 字节fsr self._read_socket_reg(0x0420, 2) # 返回 bytes如 b\x07\xD0 2000 free_space int.from_bytes(fsr, big) if free_space len(data): # 缓冲区不够需分片或等待 return 0第二步将数据写入 TX 缓冲区TX 缓冲区地址是0x10000 Sn_TX_WRSocket n 的写指针。Sn_TX_WR地址0x0424 n*0x100是当前写位置每次写完要更新。W5500 要求按 2 字节对齐写入所以数据长度必须是偶数奇数则补 0# 获取当前写指针 wr_ptr self._read_socket_reg(0x0424, 2) wr int.from_bytes(wr_ptr, big) # 计算 TX 缓冲区起始地址固定 0x10000 tx_base 0x10000 tx_addr tx_base wr # 写数据假设 data 是 bytes # W5500 的 SPI 写地址格式0x80 (addr8) (addr0xFF) data addr_bytes bytes([0x80, (tx_addr 8) 0xFF, tx_addr 0xFF]) spi.write(addr_bytes data_padded) # 更新写指针 new_wr (wr len(data_padded)) % 0x800 # TX 缓冲区大小 2KB 0x800 self._write_socket_reg(0x0424, new_wr.to_bytes(2, big))第三步触发发送写完数据后必须写Sn_CR 0x20SEND命令W5500 才会从Sn_TX_WR开始发送self._write_socket_reg(0x0401, b\x20) # Sn_CR SEND此时状态会短暂变为SOCK_ESTABLISHED发送完成后自动回到SOCK_ESTABLISHED并置位Sn_IR[1]SEND_OK 中断。实操心得send()不保证数据立即发出只保证进了 TX 缓冲区。如果网络拥塞W5500 会自动重传。但 MicroPython 代码里不能假设send()返回后对方就收到了——TCP 是可靠传输但“可靠”意味着可能重传、可能排队send()返回只是“提交成功”不是“送达成功”。3.3 recv()如何从 RX 缓冲区安全取数据而不丢包W5500 的 RX 缓冲区默认 2KB是环形队列Sn_RX_RSR地址0x0426 n*0x100报告待读字节数。recv()的难点在于必须先读Sn_RX_RSR再读Sn_RX_RD读指针最后从 RX 缓冲区地址读数据最后更新Sn_RX_RD。顺序错一点数据就乱。RX 缓冲区基地址是0x0000Sn_RX_RD地址0x0428 n*0x100指向下一个要读的位置。步骤# 1. 读待读字节数 rsr self._read_socket_reg(0x0426, 2) to_read int.from_bytes(rsr, big) if to_read 0: return b # 2. 读当前读指针 rd_ptr self._read_socket_reg(0x0428, 2) rd int.from_bytes(rd_ptr, big) # 3. 计算 RX 缓冲区地址0x0000 rd rx_base 0x0000 rx_addr rx_base rd # 4. 读数据最多 to_read 字节 addr_bytes bytes([0x00, (rx_addr 8) 0xFF, rx_addr 0xFF]) data self._spi_read(addr_bytes, to_read) # 自定义 SPI 读函数 # 5. 更新读指针 new_rd (rd to_read) % 0x800 self._write_socket_reg(0x0428, new_rd.to_bytes(2, big)) # 6. 通知 W5500 已读取写 Sn_CR 0x40 self._write_socket_reg(0x0401, b\x40) # Sn_CR RECV关键第 6 步RECV命令必须执行否则 W5500 不会释放 RX 缓冲区空间下次recv()会一直返回 0。这是新手最常犯的错——读了数据但忘了RECV结果缓冲区满了后续数据全丢。3.4 完整 TCP 客户端代码骨架与状态管理基于以上一个健壮的 TCP 客户端类核心结构如下class W5500_TCP_Client: def __init__(self, spi, cs_pin, rst_pin, int_pinNone): self.spi spi self.cs_pin cs_pin self.rst_pin rst_pin self.int_pin int_pin self.socket 0 # 使用 Socket 0 self._init_w5500() def _init_w5500(self): # 执行 RST、SPI 验证、MAC/IP 设置... pass def connect(self, server_ip, server_port): # 如前文所述轮询 Sn_SR pass def send(self, data): # 检查 FSR、写 TX 缓冲区、发 SEND 命令 pass def recv(self, bufsize1024): # 读 RSR、RD、RX 缓冲区、发 RECV 命令 pass def close(self): # 写 Sn_CR 0x10 (DISCON)等待状态变 CLOSED self._write_socket_reg(0x0401, b\x10) time.sleep_ms(100)使用示例# 初始化 client W5500_TCP_Client(spi, cs_pin, rst_pin) # 连接服务器假设服务器 IP 192.168.1.200端口 8080 if client.connect(b\xC0\xA8\x01\xC8, 8080): print(Connected!) # 发送 HTTP GET client.send(bGET / HTTP/1.1\r\nHost: 192.168.1.200\r\n\r\n) # 接收响应 while True: data client.recv(1024) if not data: break print(data.decode(utf-8, errorsignore)) else: print(Connect failed) client.close()4. 常见问题与排查技巧实录从 OSError 到抓包分析的全流程指南4.1 典型 OSError 错误码速查表与根因定位MicroPython 抛出的OSError异常其errno值对应 POSIX 错误码。结合 W5500 硬件行为常见错误及对策如下errno错误名W5500 根因排查步骤解决方案113EHOSTUNREACH目标 IP 不可达ARP 失败1.ping 192.168.1.x是否通2. W5500 的GAR网关是否设对3. 目标设备是否开机、防火墙是否放行检查网关 IP、确认目标在线、关闭目标防火墙104ECONNRESET对方强制关闭连接发 RST1. 服务器日志是否有异常2. 抓包看是否收到 RST3. W5500 的Sn_SR是否在ESTABLISHED后突然变CLOSE_WAIT检查服务器代码逻辑、确认连接未超时、增加心跳包110ETIMEDOUT连接超时SYN 无响应1.Sn_SR是否卡在SYNSENT2. 抓包看 SYN 是否发出3. 目标端口是否监听检查目标端口netstat -tuln | grep 8080、确认防火墙开放101ENETUNREACH网络不可达路由失败1. W5500 的SIPR、SUBR、GAR是否匹配局域网2. 子网掩码是否为255.255.255.0重新设置 IP 参数确保与路由器同网段22EINVAL无效参数如端口 01.Sn_DPORT是否为 02.Sn_MR是否为0x01客户端端口可为 0但目标端口必须非 0确认Sn_MR已设注意errno是 MicroPython 解释器根据 W5500 返回的状态码映射的不是 W5500 原生错误。例如EHOSTUNREACH实际对应 W5500 的Sn_IR[2]UNREACH 中断但库封装后统一为errno113。4.2 抓包分析用 Wireshark 看懂 W5500 的每一帧W5500 是硬件协议栈它发的包和收的包Wireshark 都能捕获。这是最硬核的 debug 方法。步骤第一步在 PC 上安装 Wireshark选择连接 W5500 的网卡如 Ethernet确保 PC 和 W5500 在同一局域网如 PC IP192.168.1.10W5500 IP192.168.1.100第二步过滤 W5500 的流量显示过滤器ip.addr 192.168.1.100或更精确eth.addr 00:08:dc:12:34:56

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询