尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

树莓派Pico UDP回显服务器实战:用MicroPython+W5500打通嵌入式网络通信

树莓派Pico UDP回显服务器实战:用MicroPython+W5500打通嵌入式网络通信 1. 开篇UDP回显服务器到底解决什么问题这个系列写到现在前几篇我们把以太网的基础链路打通了也顺手跑过TCP的收发流程。到了第四篇我特意把UDP单独拎出来做一期原因很简单UDP太容易被低估了。很多人觉得TCP有连接、有确认、有重传看起来很“可靠”于是一上来就各种用TCP但真到了做项目的时候尤其是嵌入式设备之间的通信、设备发现、日志上报、音视频传输UDP反而才是那个更常见、更合适的答案。这一篇的项目标题是“Pico 以太网实战系列 4/6”里的UDP实操核心任务是在树莓派Pico上用MicroPython写一个UDP回显服务器。所谓回显服务器就是“你发什么我原样发回什么”听起来很简单但它却是验证协议栈是否正常、网络链路是否通畅、驱动程序是否工作最直接也最有效的手段。这个项目适合谁我觉得有三类人很适合跟着做一遍一是刚接触嵌入式网络通信、想搞明白UDP和TCP到底差在哪的初学者二是手头有W5500、ENC28J60这类以太网模块想在MicroPython里把UDP收发跑通再继续做项目的开发者三是做上位机联调、调试工具开发需要一台“永远不会拒绝你”的测试服务器的工程师。说白了这个回显服务器就是一个最干净、最可控的网络调试靶场。2. 硬件选型与固件环境准备2.1 以太网方案对比为什么项目中常用W5500先说硬件背景。树莓派Pico用的是RP2040芯片这颗芯片本身没有集成以太网的MAC和PHY所以想让它上网必须外接以太网芯片。目前MicroPython社区里最主流的方案有两种W5500和ENC28J60。W5500是WIZnet家的硬协议栈芯片用SPI接口跟MCU通信片上自带TCP/IP协议栈也就是说TCP、UDP这些协议的封包、解包、校验和计算芯片自己就干了MCU只需要通过SPI读写数据缓冲区就行。这个特点在MicroPython这种解释执行、性能有限的环境里非常友好因为大量协议处理被卸载到了芯片硬件上。ENC28J60则是Microchip的芯片价格更便宜但它的驱动更依赖软件协议栈在MicroPython下跑TCP这种有状态协议时性能和稳定性都不如W5500。我自己测试下来UDP这种无状态协议两者差距不大但只要涉及TCPW5500的体验明显更稳。所以项目中选的是W5500模块带SPI接口的那种板上一般会引出CS、SCK、MOSI、MISO、RST、INT六个脚。接线很简单我用的SPI0引脚如下Pico引脚W5500模块GP16MISOGP17CSGP18SCKGP19MOSIGP20RSTGP21INT本示例未使用3V3VCCGNDGND注意不同厂商的W5500模块引脚定义可能有细微差异接线前一定要看模块背面的丝印别想当然。2.2 固件烧录与基础网络验证第一步是烧录支持W5500的MicroPython固件。Raspberry Pi官方的MicroPython固件已经内置了WIZNET5K驱动直接去MicroPython官网下载对应Pico的UF2文件就行烧录方式跟普通Pico完全一样按住BOOTSEL键插USB把UF2文件拖进出现的U盘里即可。固件烧好后先把硬件接好然后在REPL里跑一段初始化代码验证网络层是否通了。这里有一个细节W5500在MicroPython中注册为network.WIZNET5K不需要额外import驱动库直接用就能看到模块。import network from machine import Pin, SPI spi SPI(0, baudrate20_000_000, mosiPin(19), misoPin(16), sckPin(18)) eth network.WIZNET5K(spi, Pin(17), Pin(20)) # spi, cs, rst eth.active(True) eth.ifconfig((192.168.1.200, 255.255.255.0, 192.168.1.1, 8.8.8.8)) print(eth.ifconfig())跑完后如果打印出(192.168.1.200, 255.255.255.0, 192.168.1.1, 8.8.8.8)说明SPI通信和驱动初始化都正常了。这时候在电脑上用ping 192.168.1.200试一下能通说明网络链路OK可以进入下一步。我自己在项目里被坑过的点有两个一个是SPI的baudrate不能盲目的往高了调有的W5500模块在20MHz以上会不稳定表现为初始化成功但收发丢包后来我统一降到12MHz或者20MHz稳定优先另一个是eth.active(True)必须在ifconfig之前调用否则直接设置IP不会生效这也是MicroPython WIZNET5K驱动的一个习惯用法。3. UDP回显服务器的核心代码实现3.1 socket创建与绑定UDP的“无连接”到底指什么写UDP回显服务器之前得先把一个概念理清楚TCP是打电话UDP是发快递。TCP通信前要建立连接双方要完成三次握手然后在这条“通话线路”上按顺序收发数据挂了电话连接就断了。UDP则根本不管对方在不在直接把数据包丢给IP层让它们自己想办法送到目的地。这个区别反映在代码上非常直观。TCP服务器要listen、要accept接受一个客户端连接之后跟这个客户端单独通信UDP服务器不需要这些只要创建一个socket、绑定一个端口然后等着收数据就行了同一个socket可以跟任何人通信。我们来看MicroPython里怎么实现UDP socketimport socket udp_socket socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udp_socket.bind((0.0.0.0, 12345))socket.SOCK_DGRAM里的DGRAM就是数据报datagram的意思对应UDP。bind把socket绑定到0.0.0.0:123450.0.0.0表示“接受来自任何IP地址的数据包”如果只想收局域网某个固定IP也可以填具体地址但测试场景一般用全零地址最省心。3.2 回显循环recvfrom与sendto的配合创建好socket之后核心回显循环其实只有几行。MicroPython的socket API跟CPython几乎完全兼容但要注意的是recvfrom返回两个值数据和来源地址这两个都要接住否则要么报错要么后续没法用sendto回数据。while True: data, addr udp_socket.recvfrom(1024) print(收到来自, addr, 的数据:, data) udp_socket.sendto(data, addr)recvfrom的1024是缓冲区大小也就是单次最多能接收1024字节。实际应用中UDP数据包的最大长度跟网络MTU有关以太网标准MTU是1500字节去掉IP头20字节和UDP头8字节应用层最多能塞进1472字节。所以1024这个值在以太网环境下是合理的既能覆盖绝大多数业务场景又不会因为缓冲区太大浪费单片机那点可怜的内存。sendto(data, addr)把收到的数据原样发回给addr指定的地址。这里值得多说一句UDP是“无连接”的但每一个数据包都自带来源地址recvfrom返回的addr就是这个包的来路所以服务器不需要维护连接状态天然支持多客户端——谁发来就给谁回同一段代码同时对多个客户端服务。3.3 完整程序把网络初始化和回显逻辑合在一起下面是一个可以直接跑通的完整程序。我习惯把网络初始化代码封装成一个函数方便以后复用回显循环里加了一些调试打印方便观察收发情况。import network import socket import time from machine import Pin, SPI # 以太网初始化 def init_ethernet(ip192.168.1.200, subnet255.255.255.0, gateway192.168.1.1, dns8.8.8.8): spi SPI(0, baudrate20_000_000, mosiPin(19), misoPin(16), sckPin(18)) eth network.WIZNET5K(spi, Pin(17), Pin(20)) eth.active(True) eth.ifconfig((ip, subnet, gateway, dns)) print(以太网已初始化:, eth.ifconfig()) return eth # UDP回显服务器 def udp_echo_server(port12345): udp socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udp.bind((0.0.0.0, port)) print(UDP回显服务器已启动端口:, port) while True: try: data, addr udp.recvfrom(1024) print(收到 {} 的 {} 字节数据: {}.format(addr, len(data), data)) udp.sendto(data, addr) except Exception as e: print(处理数据出错:, e) init_ethernet() udp_echo_server()把这段代码保存为main.py传到Pico上或者直接在REPL里运行就能看到板子进入等待状态。此时以太网指示灯会正常闪烁控制台打印出“UDP回显服务器已启动”就算完成。提示MicroPython在REPL里运行无限循环时CtrlC可以中断程序。如果发现程序卡死或无法中断按板子上的RESET键重启即可。4. 联调测试用网络调试助手验证回显4.1 测试环境搭建与预期效果回显服务器跑起来了怎么验证它真的在工作最直观的方法是借助电脑上的网络调试工具。Windows平台我用过好几款最顺手的是NetAssist网络调试助手也用过SocketTool、MobaXterm自带的网络工具功能大同小异核心能力就是可以自己选TCP或UDP填上目标IP和端口就能发数据、收数据。测试环境很简单Pico通过网线连到路由器或交换机电脑跟它在同一个网段。Pico的IP是192.168.1.200电脑是192.168.1.x两个能互相ping通就行。打开NetAssist协议类型选UDP本地IP填电脑的IP本地端口随便填一个比如60000然后在下方的“目标IP”填192.168.1.200“目标端口”填12345。这样配置的意思是电脑这个工具会向Pico的12345端口发送UDP数据包同时监听自己的60000端口等待回包。发送一条“hello pico”过去如果一切正常一秒内你就能看到接收窗口里出现一模一样的“hello pico”。同时Pico侧的控制台会打印出“收到 (192.168.1.x, 60000) 的 10 字节数据: bhello pico”。4.2 边界测试长数据、中文数据和连续多发回显通了之后别急着收工。真实网络环境里数据包不会像“hello pico”这么客气所以我建议多做几组边界测试。第一组测长数据。发一个1000字节左右的字符串回显服务器应该原样返回。注意观察两个细节一是返回的数据长度对不对二是板子控制台打印的内容有没有被截断。MicroPython的print在REPL里打印长字节串时偶尔会换行截断这是显示问题不影响实际传输的数据。第二组测中文和二进制数据。在NetAssist里发“你好Pico”UDP的传输是字节层面的不会帮你“翻译”编码所以回显回来的就是原始字节电脑端如果以UTF-8解码就能正确显示。另外也可以直接在发送区填十六进制数据比如01 02 03 FF验证非文本数据的收发。第三组测连续发送。写个小脚本或手动连续点发送20次每次都带序号比如“msg-001”到“msg-020”然后看回显是不是按顺序全部回来。UDP不保证顺序也不保证不丢包但在局域网这种干净环境下丢包和乱序几乎不会发生所以正常情况应该20条全部原样返回。4.3 用Wireshark抓包理解UDP数据包格式到这里如果你对网络协议有兴趣强烈建议用Wireshark抓一次包亲眼看看UDP数据包长什么样。Wireshark的安装和基本用法这里不展开我只说跟本项目相关的部分。抓包时过滤条件填udp.port 12345然后从NetAssist发一条数据就能看到对应的UDP包。展开之后可以看到UDP头部只有四个字段Source Port源端口、Dest Port目的端口、Length长度、Checksum校验和一共8字节。对比一下TCP头部至少20字节还要管序号、确认号、窗口、标志位这些从这点就能直观感受到UDP有多“轻”。再往下一层IP头里能看到源IP和目的IP。再往下是以太网帧头能看到源MAC、目的MAC和Type字段0x0800表示IPv4这些信息凑在一起就是一个完整的UDP数据包从网线到应用层的封装过程。我建议你把这个抓包分析当成一个必做练习因为只有亲眼看到数据包的封装过程你才会真正理解“UDP是无连接的”这句话为什么是对的TCP通信在发送数据之前你会先看到SYN、SYN-ACK、ACK三次握手包然后才是数据包UDP则完全没有这些前戏直接就是一个数据包扔过去干净利落。5. 常见问题与排查技巧实录5.1 收不到数据先查这三件事项目调试中最常见的问题就是“服务器好像没反应”。我在多个环境里踩过类似的坑总结下来90%的情况出在以下三处。第一Ping不通。如果电脑ping不通Pico的IP回显肯定不可能成功。先回头看网络初始化是否成功eth.ifconfig()是否打印了正确IP再用网线检测仪器看链路是否建立。W5500模块上的Link灯不亮的话说明物理层就没通可能是网线问题也可能是模块供电不稳。第二电脑和板子不在同一网段。我的板子IP是192.168.1.200如果电脑接的路由器是192.168.31.x网段两个设备根本不在一个局域网里UDP包发过去要么根本发不出去要么被路由器丢弃。解决方法是把电脑和Pico接到同一个路由器/交换机上或者手动把电脑的IP改成跟板子同网段。第三网络调试助手的“目标IP/端口”填错了。这里有个很隐蔽的坑UDP调试工具通常需要填“目标IP”和“目标端口”这两个值才是数据包真正的目的地址。很多新手把“本地端口”填成了12345目标端口也填12345结果包发到了自己电脑上自然收不到回显。现象可能原因排查方法完全收不到回显物理链路不通看W5500模块和交换机指示灯ping测试能ping通但UDP无响应目标端口错误确认发送的端口与udp.bind一致只有第一次有效电脑防火墙拦截回包临时关闭防火墙或添加入站规则回显偶尔丢失网络拥堵或缓冲区溢出降低发送频率确认MTU不超过14725.2 数据被截断缓冲区与MTU的边界如果你用回显服务器测特别大的数据包可能发现超过一定长度后数据要么发不出去要么被截断了。这里有两个边界必须搞清楚。一个是UDP应用层的最大长度。以太网MTU1500字节IP头占20字节UDP头占8字节所以应用层最多传1472字节。超过这个值IP层就会分片MicroPython的socket API大概率不会自动处理分片重组就会出现数据不完整或者直接发送失败。工程上建议单条UDP报文控制在1400字节以内。另一个是MicroPython socket的接收缓冲区大小。前面代码里recvfrom(1024)表示单次最多接收1024字节如果对方发来1500字节多余部分会被内核丢弃。所以如果你预期会收到大包就得把缓冲区调大比如recvfrom(1472)。但在Pico这种内存只有264KB的板子上缓冲区不是越大越好要根据业务实际灵活调整。5.3 丢包和乱序UDP的“尽力而为”到底够不够用调试中如果发现连续发很多包偶尔丢一两个或者顺序乱了先别急着怀疑代码这是UDP的“自带属性”。UDP协议本身就是尽力而为、不保证可靠交付的它只在IP层之上加了端口分用和校验和没有ACK、没有重传、没有序号管理。但在局域网环境下正常情况下丢包率极低。如果你发现丢包很频繁就要怀疑硬件或驱动层面的问题了。我用W5500时遇到过的一个真实案例是SPI频率调到40MHz时UDP回显一切正常但收发频率一高就开始丢包降到20MHz后问题消失。后来查资料才发现是布线质量不好高频SPI信号反射导致数据错位。嵌入式开发里很多玄学问题最后都指向信号完整性和电气连接这个排查方向要记住。另一个隐藏坑是W5500的收发缓冲区只有8KB。如果上位机突发大量数据超过芯片内部缓冲多余的数据包会被丢弃这是硬件层面的瓶颈软件层面只能通过recvfrom及时取走来缓解。如果是大量数据场景建议升级到W5500的兄弟芯片或改用STM32LWIP方案。6. 扩展思路从回显服务器到真实项目6.1 改造为“命令解析服务器”回显服务器虽然只是个雏形但它完全可以作为更复杂项目的地基。最简单的一个改造是加一层解析逻辑收到数据后不是原样返回而是先判断内容再执行对应动作。比如收到字符串led_on就把Pico上的LED点亮收到led_off就关掉收到status就回复当前状态。这其实就是一个极简的物联网控制协议雏形。做这个改造的时候有一个小技巧把协议设计成“首字节代表命令类型后续字节代表参数”比如b\x01\x01表示开灯、b\x01\x00表示关灯比解析字符串更高效也不容易出错。我在项目里就是这么做的最后给设备加了一个UDP遥控器手机端用Packet Sender这类工具直接发命令控制Pico。6.2 利用来源地址实现多客户端识别前面提到过UDP天然支持多客户端因为recvfrom返回的addr包含了每个数据包的来源IP和端口。你可以利用这一点在Pico上维护一个“客户端列表”每个客户端第一次发数据时把它记下来之后就可以定向回复。做个简单的温度采集上报系统就是这样的结构几个传感器节点通过UDP往Pico发数据Pico根据addr区分数据来自哪个节点再做一些汇总处理。这个模型比TCP多客户端省心得多因为TCP需要为每个客户端维护连接还要关心断线重连UDP则完全不需要哪个节点“喊”一声服务器听到就回应听不到拉倒。6.3 做UDP广播实现设备发现UDP还有一个TCP很难做到的事情就是广播。往255.255.255.255这种地址发数据包同一网段的所有设备都能收到。利用这个特性可以做设备自动发现。我在项目里是这么做的Pico上电后每隔5秒往255.255.255.255:8888发一条广播内容是自己IP和名字电脑端监听8888端口收到广播就知道“这里有一台Pico设备上线了”。反过来也可以电脑先广播问“谁是在线设备”Pico收到后单播回复自己地址这就是很多物联网设备的发现协议的基本思路。import socket import time # 广播方Pico sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) while True: sock.sendto(bPICO_DEVICE_192.168.1.200, (255.255.255.255, 8888)) time.sleep(5)注意这里setsockopt(SO_BROADCAST, 1)必须在sendto之前调用否则广播包会被系统直接丢弃。我最初写的时候漏了这一行折腾了半天才发现是这个问题。6.4 性能与稳定性方面的几个注意点最后再分享几个性能和稳定性方面的经验。第一不要在回显循环里做耗时操作。UDP服务器收到数据后如果要在主循环里处理图片、写Flash这类耗时任务会导致后续数据包积压、溢出。如果业务复杂建议把“接收数据”和“处理数据”拆开用队列缓存但MicroPython的线程支持比较弱Pico双核可以用_thread要将就着用复杂场景最好换C语言开发。第二定期检查网线状态。W5500这类模块的物理链路很依赖网线质量插拔网线时最好在程序里做链路检测如果eth.active()返回False就重新初始化。不过MicroPython的WIZNET5K驱动对断线重连的支持有限一般靠复位模块来恢复。第三UDP端口的选择。工程上尽量选1024以上的高位端口避开常见的服务端口。我习惯用6000到9000之间的端口既有辨识度又不容易跟系统服务冲突。第四如果要做固件OTA升级或者文件传输建议换TCP。UDP丢包、乱序的特性决定了它不适合传关键数据。很多新手在这里犯迷糊明明UDP性能好为啥文件断点续传不用它因为文件任何一个字节错了都可能导致整个文件损坏而UDP不保证所有数据都到达还得应用层自己设计重传机制那工作量比直接用TCP大多了。协议选型不是看谁快而是看谁适合你的业务场景。我个人在实际操作中的体会是回显服务器这个项目虽然小但它把网络通信的整条链路完整打通了一遍从物理层到IP层到传输层再到应用层的socket API你能亲眼看到、亲手验证每一个环节。做完这个项目之后再去碰MQTT、HTTP这些更上层的协议你会比那些只跑过demo的人多一份底气因为你知道底层到底是怎么流转的。最后再分享一个小技巧把Pico的UDP回显服务器跟Wireshark配合使用在电脑上构造各种奇葩数据包超长包、广播包、空数据包发给Pico观察它的反应这是训练网络协议直觉和调试能力非常高效的方法。我到现在做网络相关项目仍然习惯先写一个回显服务器把链路“打热”再开始写业务逻辑——这个习惯帮我省掉了大量前期排查时间。
返回列表