1. 项目概述:为Pico插上网络的翅膀

在嵌入式开发领域,让一个小巧、低成本的微控制器接入互联网,始终是一个充满吸引力又颇具挑战的目标。无论是想做一个能远程上报数据的传感器节点,还是一个能接收云端指令的智能开关,网络连接都是实现这些功能的基础。今天要聊的,就是如何给Raspberry Pi Pico这块性能强劲但“天生”没有网络接口的板子,赋予稳定的以太网通信能力。

核心的解决方案,就是WIZnet Ethernet HAT。这不仅仅是一个简单的网络模块转接板,它的核心是一颗W5100S芯片——一颗硬核的硬件TCP/IP协议栈芯片。这意味着,处理网络协议(如TCP、UDP、IP、ICMP)的繁重任务,从Pico的主控RP2040微控制器上被卸载了,由W5100S专用硬件来完成。对于RP2040这类没有内置MAC和PHY的微控制器来说,这种方案简直是“雪中送炭”。它让开发者无需在资源有限的MCU上移植和运行复杂的软件协议栈(如lwIP),就能轻松实现网络功能,极大地降低了开发门槛和系统负载。

而我们实现这一切的开发环境,是CircuitPython。如果你熟悉Python,那么CircuitPython会让你感到无比亲切。它让嵌入式编程变得像在电脑上写脚本一样简单,无需复杂的编译和烧录过程,通过USB连接,直接编辑板载存储上的代码文件即可运行。将CircuitPython的易用性与WIZnet HAT的硬件网络能力相结合,我们就能快速构建出一个HTTP客户端。这个客户端可以像浏览器一样,向指定的网页服务器发起请求,获取数据,比如天气预报、股票信息,或者向物联网平台发送传感器读数。

这篇文章,就是一份从零开始的手把手指南。无论你是刚接触嵌入式网络的新手,还是想为现有项目添加联网功能的老手,都能从中找到清晰的路径。我们将从硬件连接开始,一步步完成软件环境搭建、库文件配置,最终实现一个能够访问真实网页服务器的HTTP客户端示例,并深入探讨其中的关键细节和避坑技巧。

2. 硬件选型与连接解析

2.1 核心硬件:WIZnet Ethernet HAT与RP2040主控

这个项目的硬件核心是两块板子:作为大脑的Raspberry Pi Pico(基于RP2040芯片),和作为网络器官的WIZnet Ethernet HAT。

Raspberry Pi Pico 的优势在于其极高的性价比和强大的双核ARM Cortex-M0+处理器。它提供了丰富的GPIO和硬件接口,但正如前文所述,它缺少以太网控制器。因此,我们需要一个外部解决方案来弥补这个短板。

WIZnet Ethernet HAT 正是为此而生。所谓HAT(Hardware Attached on Top),是树莓派基金会定义的一种硬件扩展板标准,具有统一的尺寸和引脚排列。WIZnet的这块HAT完全兼容Pico的引脚定义,可以像帽子一样严丝合缝地扣在上面。其灵魂是 W5100S 芯片。与需要软件协议栈的方案不同,W5100S内部集成了完整的TCP/IP协议栈硬件逻辑、10/100M以太网物理层(PHY)和媒体访问控制器(MAC)。它支持最多4个独立的硬件Socket,可以同时处理4个网络连接(例如,两个TCP客户端加一个UDP服务)。更重要的是,它支持 Auto-MDIX (自动介质相关接口交叉),这意味着你使用普通的直连网线或交叉网线连接路由器或交换机,它都能自动识别并正确工作,省去了辨别线序的麻烦。

关于供电 :这块HAT设计了一个精妙的电平转换电路,使其能同时兼容3.3V和5V逻辑电平。虽然Pico的GPIO是3.3V,但HAT上的某些外围电路或与5V设备兼容时,这个特性就非常有用。不过在我们的基础应用中,直接使用Pico的3.3V系统即可。

2.2 硬件连接步骤与要点

硬件连接本身非常简单,但有几个细节决定了成败。

  1. 物理组装 :将WIZnet Ethernet HAT的母座与Raspberry Pi Pico的引脚对齐,轻轻按压,确保所有引脚都牢固接触。要避免引脚弯曲或错位。如果使用的是 W5100S-EVB-Pico (这是一款将RP2040和W5100S集成在一块板上的产品),那么这一步可以跳过,因为它已经是一体板了。

  2. 网络连接 :使用一根标准的RJ45网线,一端插入Ethernet HAT的以太网端口,另一端插入你的路由器或交换机的LAN口。确保路由器已开启DHCP服务,这是后续设备能自动获取IP地址的关键。指示灯的状态是一个重要的诊断工具:通常,连接成功后,以太网端口旁的绿色链路指示灯(Link)会常亮,黄色数据活动指示灯(ACT)在数据传输时会闪烁。

  3. 电源与调试接口 :使用Micro USB线将Pico连接到电脑。这根线缆有两个作用:一是为整个系统供电,二是建立串行通信(Serial COM)通道,用于CircuitPython的代码输出(print语句)和交互式REPL(读取-求值-打印循环)环境。务必使用质量可靠的USB线,接触不良会导致设备反复断开连接。

注意 :在连接USB线之前,最好先插好网线。有些网络设备(如某些交换机)在端口检测到链路后才开始协商,先连网线有助于系统上电后快速完成网络初始化。

3. 软件环境搭建与库配置

3.1 为Pico安装CircuitPython固件

CircuitPython不是Pico出厂自带的,我们需要先为其“刷入”这个新的“操作系统”。

  1. 进入BOOTSEL模式 :按住Pico板上的白色“BOOTSEL”按钮不放,然后将USB线插入电脑。待电脑识别出一个名为“RPI-RP2”的可移动磁盘后,再松开按钮。这个磁盘模式就是Pico的固件更新模式。

  2. 下载并刷写固件 :访问CircuitPython官网的下载页面,找到对应Raspberry Pi Pico的 .uf2 固件文件(例如 adafruit-circuitpython-raspberry_pi_pico-en_US-7.x.x.uf2 )。将下载好的 .uf2 文件直接拖拽或复制到刚刚出现的“RPI-RP2”磁盘中。复制完成后,Pico会自动重启。

  3. 验证安装 :重启后,电脑上会出现一个新的磁盘驱动器,名字类似于“CIRCUITPY”。这就证明CircuitPython固件已经安装成功。这个“CIRCUITPY”磁盘就是Pico的“硬盘”,我们之后所有的代码文件、库文件都将放在这里。

3.2 配置WIZnet以太网库

CircuitPython本身不包含网络驱动,我们需要将控制W5100S芯片的专用库文件放入Pico。

  1. 获取库文件包 :你需要一个包含多个依赖库的集合。最直接的方式是从WIZnet官方为RP2040 HAT提供的GitHub仓库(例如 Wiznet/RP2040-HAT-CircuitPython )下载。通常你需要找到并下载整个仓库的ZIP包,或者使用Git工具克隆。

  2. 复制库文件 :打开下载的库文件夹,找到其中的 lib 子目录。你需要将 lib 目录下的全部内容(而不仅仅是单个文件)复制到Pico的“CIRCUITPY”磁盘下的 lib 文件夹中。如果 CIRCUITPY 盘下没有 lib 文件夹,就新建一个。 关键库文件通常包括:

    • adafruit_wiznet5k/ :这是与WIZnet W5x00系列芯片(包括W5100S)通信的核心驱动库。
    • adafruit_bus_device/ :提供底层SPI、I2C总线操作的抽象,是 adafruit_wiznet5k 的依赖。
    • adafruit_requests.mpy :一个模仿Python经典 requests 库的HTTP客户端库,它依赖于网络接口(在这里就是我们的WIZnet驱动)来发起HTTP请求。这个文件可能是 .mpy 格式(预编译的字节码),运行效率更高。
  3. 验证库完整性 :确保复制过程没有中断,并且所有必要的文件夹和文件都已就位。一个常见的错误是只复制了子文件夹里的内容,而漏掉了文件夹本身,导致Python在导入时找不到模块。

3.3 串口终端工具的准备

为了能看到我们代码打印的调试信息(比如获取的IP地址、HTTP响应内容),我们需要一个串口终端工具。

  1. 识别COM端口 :在Windows上,打开“设备管理器”,展开“端口(COM和LPT)”,你会看到一个类似“USB串行设备(COMx)”的条目,记下这个COMx的数字(如COM3)。在macOS或Linux上,通常设备名为 /dev/tty.usbmodemxx /dev/ttyACM0

  2. 选择终端软件 :PuTTY、Tera Term、Arduino IDE的串口监视器,甚至VS Code搭配PlatformIO插件都可以。它们的功能类似:设置正确的串口号(COMx)、波特率(CircuitPython通常使用115200)、数据位(8)、停止位(1)、无校验位(None)。连接后,你可能会先看到一个空白屏幕。

  3. 进入REPL :在串口终端中,按一下键盘上的 Ctrl+C 。这会中断任何可能正在运行的程序。然后,按 Enter 键,你应该会看到 >>> 提示符。这表示你已进入CircuitPython的交互式REPL环境,可以在这里逐行执行Python命令,这是一个非常强大的调试和探索工具。要运行我们写好的主程序,通常是在REPL里按 Ctrl+D 进行软复位,CircuitPython会自动执行 code.py 文件。

4. HTTP客户端示例代码深度剖析

现在,进入最核心的部分:编写并理解HTTP客户端的代码。我们将以一个基础的示例为蓝本,逐行拆解其工作原理和关键配置。

4.1 网络接口初始化:与W5100S芯片对话

任何网络通信开始前,必须先初始化硬件并建立网络接口。以下代码展示了这一过程:

import board
import busio
import digitalio
from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K
import adafruit_wiznet5k.adafruit_wiznet5k_socket as socket

# 1. 初始化SPI总线 - W5100S与RP2040通过SPI通信
spi = busio.SPI(board.GP10, board.GP11, board.GP12) # SCK, MOSI, MISO
cs = digitalio.DigitalInOut(board.GP13) # 片选引脚GP13
reset = digitalio.DigitalInOut(board.GP15) # 复位引脚GP15(可选,但推荐连接)

# 2. 创建WIZNET5K网络接口对象
eth = WIZNET5K(spi, cs, reset=reset)

# 3. 配置网络(DHCP或静态IP)
# 方式A:使用DHCP自动获取IP(最常见)
print("正在通过DHCP获取IP地址...")
eth.dhcp = True # 启用DHCP客户端
# 等待DHCP分配,超时时间可设
while not eth.ip_address:
    print("等待DHCP响应...")
    time.sleep(1)
    # 在实际项目中,这里应增加超时判断和失败处理

# 方式B:使用静态IP(适用于固定网络环境)
# eth.ifconfig = ('192.168.1.100', '255.255.255.0', '192.168.1.1', '8.8.8.8')

print("网络配置成功!")
print("IP地址:", eth.ip_address)
print("子网掩码:", eth.netmask)
print("网关:", eth.gateway)
print("DNS服务器:", eth.dns)

关键点解析

  • SPI引脚定义 GP10 (SCK), GP11 (MOSI), GP12 (MISO) 是WIZnet HAT与Pico通信的默认SPI引脚。 GP13 是芯片选择(CS)引脚,用于在SPI总线上选中W5100S。 GP15 是复位引脚,可靠的硬件复位能确保芯片从已知状态启动。
  • DHCP过程 eth.dhcp = True 会触发芯片向网络中的DHCP服务器广播请求。 while 循环等待 eth.ip_address 属性被赋值,这个过程通常需要1-3秒。 务必添加超时机制 ,例如循环10次后若仍未获取到IP,则报错或尝试备用方案(如回退到静态IP)。
  • 网络信息打印 :成功获取IP后打印的信息至关重要,它们是后续网络连通性测试的基础。如果这里获取的IP是 0.0.0.0 169.254.x.x (APIPA地址),说明DHCP失败,需要检查网线、路由器DHCP服务或防火墙设置。

4.2 使用adafruit_requests库发起HTTP请求

初始化网络后,我们就可以使用高级的 adafruit_requests 库来发起HTTP请求,这比直接使用socket编程要简单得多。

import adafruit_requests as requests
import time

# 将我们创建的以太网接口‘eth’设置为requests库使用的网络会话(session)
# 这步建立了网络驱动与HTTP库之间的桥梁
requests.set_socket(socket, eth)

# 定义要访问的URL
# 示例1:获取一个简单的文本页面
url_text = "http://httpbin.org/ip" # 这个服务会返回你的公网IP,非常适合测试
# 示例2:获取JSON格式的公开API数据
url_json = "http://worldtimeapi.org/api/timezone/Asia/Shanghai"

print(f"正在连接服务器: {url_text}")
try:
    # 发起HTTP GET请求
    response = requests.get(url_text)
    
    # 检查HTTP状态码,200表示成功
    print("HTTP状态码:", response.status_code)
    
    # 读取并打印响应内容(文本格式)
    print("响应内容:")
    print(response.text)
    
    # 对于JSON响应,可以方便地解析
    # response_json = response.json()
    # print("当前时间:", response_json['datetime'])
    
    # 重要:关闭响应对象,释放网络Socket资源
    response.close()
    
except Exception as e:
    # 异常处理:网络超时、DNS解析失败、连接拒绝等
    print("请求失败:", e)
finally:
    # 确保在任何情况下都尝试关闭连接(如果已建立)
    # requests.get内部已做处理,此处显式写出以示逻辑完整
    pass

print("请求完成。")

代码逻辑与避坑指南

  1. requests.set_socket(socket, eth) :这是连接底层网络驱动和上层HTTP库的关键一行。它告诉 adafruit_requests 库,使用我们指定的 socket 模块(来自WIZnet驱动)和 eth 网络接口来进行所有网络操作。
  2. 异常处理 :网络操作极不稳定,必须用 try...except 包裹。常见异常包括:
    • OSError: [Errno 110] ETIMEDOUT :连接超时,服务器未响应。
    • OSError: [Errno -2] Name or service not known :DNS解析失败,域名无法转换为IP地址。
    • RuntimeError: No socket available :W5100S的4个硬件Socket已用尽,需要检查代码是否及时关闭了连接。
  3. 资源管理 response.close() 至关重要。W5100S只有4个硬件Socket,每个活跃的连接都会占用一个。如果不关闭,Socket很快会被耗尽,导致新的连接无法建立。即使在异常发生时,也应确保在 finally 块中尝试关闭。
  4. DNS依赖 :访问域名(如 httpbin.org )需要DNS服务。代码中使用的 eth.dns 就是在DHCP阶段获取的DNS服务器地址。如果网络环境特殊(如某些企业内网),可能需要手动设置一个可用的DNS,如 ('8.8.8.8', '1.1.1.1')

4.3 构建一个完整的轮询式HTTP客户端

一个实用的嵌入式客户端往往需要周期性地工作。下面是一个整合了以上所有步骤,并加入循环和错误恢复的完整示例框架:

import board
import busio
import digitalio
import time
from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K
import adafruit_wiznet5k.adafruit_wiznet5k_socket as socket
import adafruit_requests as requests

# 硬件初始化函数
def init_ethernet():
    spi = busio.SPI(board.GP10, board.GP11, board.GP12)
    cs = digitalio.DigitalInOut(board.GP13)
    reset = digitalio.DigitalInOut(board.GP15)
    
    eth = WIZNET5K(spi, cs, reset=reset)
    eth.dhcp = True
    
    max_retries = 10
    for i in range(max_retries):
        if eth.ip_address:
            print(f"网络初始化成功!IP: {eth.ip_address}")
            return eth
        print(f"等待DHCP... ({i+1}/{max_retries})")
        time.sleep(1)
    
    raise RuntimeError("DHCP失败,无法获取IP地址。")

# 主循环
def main():
    eth = None
    requests_session = None
    
    try:
        # 1. 初始化网络
        eth = init_ethernet()
        requests.set_socket(socket, eth)
        # 创建一个会话对象,在某些配置下可能更高效
        requests_session = requests.Session()
        
        # 要轮询的API端点
        api_url = "http://api.open-notify.org/iss-now.json" # 国际空间站当前位置
        
        while True:
            print("\n--- 开始新一轮查询 ---")
            try:
                # 2. 发起HTTP请求
                response = requests_session.get(api_url, timeout=5) # 设置5秒超时
                
                if response.status_code == 200:
                    data = response.json()
                    print(f"ISS位置 -> 经度: {data['iss_position']['longitude']}, 纬度: {data['iss_position']['latitude']}")
                    # 这里可以添加处理数据的逻辑,比如控制LED、存储到SD卡等
                else:
                    print(f"服务器返回错误: {response.status_code}")
                
                response.close()
                
            except Exception as e:
                print(f"请求过程中发生错误: {e}")
                # 简单的错误恢复:等待一段时间后继续
                # 对于致命错误,可以考虑软复位 eth.reset() 或整个系统 machine.reset()
            
            # 3. 等待一段时间后再次执行
            time.sleep(10) # 每10秒查询一次
    
    except Exception as e:
        print(f"主程序发生致命错误: {e}")
    finally:
        # 清理工作(如果有)
        print("程序结束或重启。")

if __name__ == "__main__":
    main()

这个框架展示了一个健壮的嵌入式HTTP客户端应有的结构:独立的初始化函数、带重试机制的网络准备、带超时和异常处理的主循环、以及定期的任务执行。你可以将 api_url 和数据处理部分替换成任何你需要的功能,例如从气象站API获取数据并显示在OLED屏幕上。

5. 调试技巧与常见问题排查实录

即使按照步骤操作,也难免会遇到问题。下面是我在实际项目中积累的一些常见问题及其排查思路,希望能帮你快速定位。

5.1 硬件与连接类问题

问题1:Pico上电后,“CIRCUITPY”磁盘未出现。

  • 排查 :首先确认是否成功刷入了CircuitPython固件。重新进入BOOTSEL模式(按住BOOTSEL键上电),检查“RPI-RP2”磁盘是否存在。如果存在,重新复制一次正确的 .uf2 文件。如果“RPI-RP2”磁盘都不出现,检查USB线、电脑USB口,或尝试另一台电脑。

问题2:网口指示灯不亮。

  • 排查
    1. 检查网线是否插紧,尝试更换另一根已知良好的网线。
    2. 检查路由器/交换机对应端口的指示灯是否亮起。
    3. 在代码中初始化后,尝试添加 eth.link_status 检查并打印。如果为False,说明物理链路未建立。
    4. 检查硬件连接,确保HAT与Pico接触良好,无引脚虚焊或弯曲。

问题3:DHCP一直失败,获取不到IP地址(IP为 0.0.0.0)。

  • 排查
    1. 确认网络环境 :你的路由器必须开启DHCP服务器功能。可以先用手机或电脑连接同一个路由器,看是否能自动获取IP。
    2. 检查防火墙 :有些企业网络或高级家用路由器有MAC地址过滤、AP隔离等功能,可能会阻止新设备获取IP。尝试将设备连接到更简单的网络环境(如一个普通家用路由器)进行测试。
    3. 代码超时设置 :增加DHCP等待循环的次数和每次等待的时间。有些DHCP服务器响应较慢。
    4. 尝试静态IP :注释掉DHCP代码,手动设置一个与路由器网段相同的静态IP(如 eth.ifconfig = (‘192.168.1.200‘, ’255.255.255.0‘, ’192.168.1.1‘, ’8.8.8.8‘) )。如果能用静态IP Ping通网关,则问题出在DHCP协商过程。

5.2 软件与代码类问题

问题4:在REPL或串口终端中看不到任何输出。

  • 排查
    1. 确认串口设置 :波特率是否为115200?数据位8,停止位1,无校验,无流控。
    2. 确认COM端口 :设备管理器中的COM口号是否与终端软件中选择的一致?拔插USB线后端口号可能会变。
    3. 检查代码是否运行 :确保你的主程序代码已保存为 CIRCUITPY 磁盘根目录下的 code.py main.py 。CircuitPython会自动运行这两个文件之一。
    4. 强制软复位 :在串口终端中按 Ctrl+C 然后按 Ctrl+D ,这会复位并重新执行 code.py

问题5:导入模块失败(ImportError)。

  • 排查
    1. 检查库文件路径 :确保 adafruit_wiznet5k adafruit_bus_device 等文件夹完整地放在了 CIRCUITPY/lib/ 目录下,而不是只放了文件夹里的文件。
    2. 检查库版本兼容性 :确保下载的库版本与你的CircuitPython固件版本大致兼容。通常GitHub仓库的发布页或README会说明兼容的版本。
    3. 内存不足 :如果错误信息提及内存,可能是程序太大或变量太多。尝试简化代码,或使用 .mpy 格式的库文件以节省内存。

问题6:HTTP请求失败,出现Socket错误或超时。

  • 排查
    1. 先测试网络连通性 :在初始化网络并获取IP后,添加一个测试环节。可以尝试Ping一个公共DNS服务器: import wifi; wifi.radio.ping(‘8.8.8.8’) (注意: wifi.radio.ping 可能不适用于以太网接口,这里更可靠的方法是尝试用Socket连接一个已知IP和端口,但更简单的方法是直接请求一个极其可靠的URL,如 http://1.1.1.1/ 但可能无HTTP响应)。最实用的方法是先请求 http://httpbin.org/ip 这种极其简单的服务。
    2. 检查DNS :如果使用域名失败,但使用IP地址成功(例如尝试访问 http://142.250.185.78 对应某个谷歌服务),那一定是DNS问题。确认 eth.dns 是否正确,或尝试在代码中硬编码DNS eth.set_dns(‘8.8.8.8’)
    3. 检查目标服务器和端口 :确保你访问的URL是HTTP而非HTTPS。CircuitPython的 adafruit_requests 默认不支持HTTPS(SSL/TLS),因为加解密对MCU资源消耗很大。如果需要HTTPS,需要寻找支持SSL的特定库或方案,复杂度会陡增。
    4. Socket泄漏 :确保每个 response 对象在使用后都调用了 .close() 方法。可以在循环前后打印 eth.socket_available 来观察Socket是否被正确释放。

5.3 高级优化与稳定性提升

心得1:增加看门狗(Watchdog) 对于需要长期运行的产品,必须考虑程序的稳定性。RP2040的CircuitPython支持看门狗定时器。在主循环中定期喂狗,如果程序卡死(比如网络请求无限阻塞),看门狗超时后会强制复位整个系统,使其恢复工作。

import microcontroller
wdt = microcontroller.watchdog
wdt.timeout = 10 # 设置超时时间为10秒
wdt.mode = microcontroller.WatchDogMode.RESET
wdt.feed() # 在主循环中定期调用

心得2:实现非阻塞式延迟 time.sleep(10) 这样的长延迟期间,MCU什么也做不了。对于需要同时处理其他任务(如读取传感器按钮)的应用,可以使用时间戳来实现非阻塞延迟:

last_request_time = time.monotonic()
request_interval = 10

while True:
    current_time = time.monotonic()
    if current_time - last_request_time >= request_interval:
        # 执行HTTP请求...
        last_request_time = current_time
    # 这里可以执行其他短任务,如读取传感器、闪烁状态灯
    time.sleep(0.1) # 短时间睡眠,让出CPU

心得3:将配置参数外置 不要把Wi-Fi密码、服务器URL、API密钥等硬编码在代码里。可以创建一个 settings.toml secrets.py 文件放在 CIRCUITPY 磁盘上,程序运行时从中读取。这样更新配置时无需修改主代码,也更安全。

# settings.toml 文件内容
# WEB_API_URL = "http://api.example.com/data"
# UPDATE_INTERVAL = 30
import tomli
with open("/settings.toml", "rb") as f:
    config = tomli.load(f)
api_url = config["WEB_API_URL"]

从硬件连接到软件调试,整个过程就像在搭积木,每一步都建立在稳固的前一步之上。遇到问题时,最有效的方法就是“分段隔离”:先用最简单的代码测试网络初始化(只获取IP并打印),通了之后再测试基本的Socket通信(如Ping),最后再加上HTTP协议层。这种自底向上的排查思路,能帮你迅速锁定问题根源。

更多推荐