1. Pico 引脚布局与 GPIO 映射:先搞清楚物理引脚和编号的关系
拿到树莓派 Pico,第一眼看到的是那块长条形的开发板,两侧各有 20 个金属引脚,总共 40 个物理引脚。这块板子本身的尺寸和一块口香糖差不多,引脚却藏着不少门道。新手最容易踩的坑就是把物理引脚编号和 GPIO 编号搞混。
Pico 板上的引脚分三类信号:电源引脚(3V3、5V、GND)、控制引脚(GP0~GP28)、以及几个特殊功能引脚(如 ADC_VREF、AGND、RUN)。很多人以为板子上丝印标注的数字就是 GPIO 编号,其实不然。Pico 的丝印标注的是物理引脚序号,从 1 到 40,而 MicroPython 里操作时用的是 GPIO 编号。
我整理了一张对应关系表,方便你速查:
| 物理引脚 | GPIO 编号 | 说明 |
|---|---|---|
| 1 | GP0 | 通用输入输出,可复用 PWM/ADC |
| 2 | GP1 | 通用输入输出,可复用 PWM/ADC |
| 4 | GP2 | 通用输入输出 |
| 6 | GP3 | 通用输入输出 |
| 9 | GP4 | 通用输入输出 |
| 11 | GP5 | 通用输入输出,SPI 片选 |
| 14 | GP6 | 通用输入输出 |
| 16 | GP7 | 通用输入输出 |
| 21 | GP16 | 通用输入输出 |
| 24 | GP18 | 通用输入输出 |
| 26 | GP20 | 通用输入输出 |
| 31 | GP26 | ADC0 专用输入 |
| 32 | GP27 | ADC1 专用输入 |
| 34 | GP28 | ADC2 专用输入 |
板子右上角那颗小小的板载 LED,接在 GP25 上。调试时想快速验证代码能不能跑,直接操作这颗 LED 就是最快的方案,不需要接任何外部元器件。
还有一点必须说明:Pico 的 GPIO 电平是 3.3V,不是 5V。如果你以前玩过 Arduino,习惯了 5V 逻辑电平,换到 Pico 上直接驱动 5V 器件,轻则读不到高电平,重则烧毁引脚。往 GPIO 上接外部模块时,先确认模块是否兼容 3.3V 逻辑电平。
2. machine.Pin 类核心方法:构造、模式、读写究竟怎么用
在 MicroPython 中,操作 GPIO 的统一入口就是machine.Pin类。理解了这个类的设计思路,后面写再复杂的控制逻辑都能举一反三。
2.1 构造一个 Pin 对象:必须搞懂的 id、mode、pull
创建一个 Pin 对象的基本语法如下:
from machine import Pin # 最基本的形式:只指定引脚编号,默认输出模式 led = Pin(25, Pin.OUT) # 完整形式:指定编号、模式、默认电平、上下拉电阻 btn = Pin(16, Pin.IN, Pin.PULL_UP)这里有两个关键参数:mode和pull。
mode只有Pin.OUT和Pin.IN两种取值,分别对应输出模式和输入模式。Pico 的每个 GPIO 引脚内部结构决定了它同一时刻只能作为一种模式工作,你想让它既读又写(比如读取按键的同时驱动 LED),那需要两个不同的 Pin 对象,或者用复用功能。
pull参数只在输入模式下有意义,它控制引脚内部的上拉或下拉电阻。Pico 内部集成了可编程的上拉/下拉电阻,取值为Pin.PULL_UP、Pin.PULL_DOWN或None(默认,不启用内部电阻)。这个设计很实用,比如接一个按键到 GND 和某个 GPIO 之间,只需要启用内部上拉,按键未按下时读到高电平,按下时读到低电平,完全不需要外部电阻。
2.2 输出模式:value、toggle、高低电平的底层逻辑
创建了输出模式的 Pin 对象后,最常用的方法就是value()。这个方法有两种角色:无参数调用时是读取引脚当前电平,有参数调用时是设置引脚输出电平。
from machine import Pin import time led = Pin(25, Pin.OUT) # 设置 LED 亮 led.value(1) # 等效于 led.on() time.sleep(1) # 设置 LED 灭 led.value(0) # 等效于 led.off() time.sleep(1) # 翻转电平 led.toggle()value(1)会让引脚输出高电平,value(0)输出低电平。另外还有on()和off()两个简写方法,逻辑语义更清晰:on()对应高电平,off()对应低电平。
toggle()方法没有参数,每次调用把当前电平取反。这个在闪烁灯、状态机切换场景里非常方便,省去了先读再写的操作。
底层来看,value(1)的本质是把引脚的输出数据寄存器对应位写 1,电平转换时间在微秒级。Pico 主频 133MHz,一次电平翻转大概需要 7 个时钟周期左右,也就是约 50 纳秒。这个速度对大多数传感器读取、LED 控制、继电器开关绰绰有余,但如果你要做高速 PWM 输出或者精确时序通信,建议直接用硬件外设(PWM、SPI、I2C),而不是靠手动翻转 GPIO。
2.3 输入模式:读取外部信号的关键细节
把引脚配置为输入模式后,核心方法是value(),它返回 0 或 1。但在实际使用中,有几个绕不开的细节:
悬空引脚的问题。如果你把引脚配置为输入模式,既不用上拉也不用下拉,引脚就处于高阻态。这时读取到的电平完全取决于外部环境——可能受旁边的走线干扰,也可能受手指触摸影响,结果不稳定。所以读取外部信号前,要么启用内部上拉/下拉,要么外部接电阻,确保引脚电平是确定的。
按键消抖。机械按键在按下和释放的瞬间,触点会产生 10~20ms 的抖动,电平在这段窗口内会快速跳变多次。如果不做消抖处理,一次按键可能被误判成多次。常见的软件消抖思路是延时后再确认一次:
from machine import Pin import time btn = Pin(16, Pin.IN, Pin.PULL_UP) led = Pin(25, Pin.OUT) while True: if btn.value() == 0: # 检测到按下 time.sleep_ms(10) # 延时避开抖动窗口 if btn.value() == 0: # 再次确认确实按下 led.toggle() while btn.value() == 0: # 等待松开 time.sleep_ms(10)这种"检测-延时-再检测"的消抖方式,原理简单且足够应对大部分场景。更精细的做法是用定时器定期采样引脚状态,结合状态机判断按键事件,但对普通项目来说,第一种方案已经够用了。
3. GPIO 的 8 种工作模式:从原理到选择
标题提到 GPIO 的 8 种工作模式,这是从 STM32 那套体系里流传出来的概念。在 STM32 中,GPIO 可配置为输入浮空、输入上拉、输入下拉、模拟输入、开漏输出、推挽输出、复用开漏输出、复用推挽输出这 8 种模式。Pico 的 machine.Pin 并没有完全照搬这 8 种模式,但理解这些模式可以帮助你更好地理解外设接口设计。
Pico 的 MicroPython 固件中,Pin 类支持的模式其实就两种:Pin.IN和Pin.OUT,但结合上下拉电阻和复用功能,可以模拟出类似的效果。对应关系大致如下:
| STM32 模式 | Pico 等价实现 | 典型应用场景 |
|---|---|---|
| 输入浮空 | Pin.IN(不启用上拉/下拉) | 外部已接上拉/下拉电阻的信号读取 |
| 输入上拉 | Pin.IN, Pin.PULL_UP | 按键到 GND 读取 |
| 输入下拉 | Pin.IN, Pin.PULL_DOWN | 按键到 3.3V 读取 |
| 模拟输入 | machine.ADC类而不是 Pin 类 | 模拟电压采样 |
| 推挽输出 | Pin.OUT | LED 驱动、电平输出 |
| 开漏输出 | Pin.OUT不直接支持,需外部上拉 | I2C 总线、电平转换 |
| 复用推挽输出 | 由外设驱动(如 PWM、SPI) | 波形输出、通信协议 |
| 复用开漏输出 | I2C 外设自动配置 | I2C 通信 |
模式选择的判断逻辑其实很简单:这个引脚是负责读外部信号还是输出控制信号?读的话,外部信号是主动拉高拉低还是需要内部电阻帮忙确定电平?输出的话,是驱动常规数字器件还是需要开漏结构?
开漏输出这点值得展开说。开漏输出的特点是:引脚只能主动拉低到 GND,无法主动输出高电平。高电平必须靠外部上拉电阻提供。这种设计特别适合两个场景:I2C 总线的线与逻辑,以及不同电压域之间的电平转换。
Pico 的 machine.Pin 没有直接暴露开漏模式,但 MicroPython 固件内部用 I2C 外设时会自动把 SDA/SCL 配置为开漏结构,不需要你手动干预。如果你非要用普通 GPIO 模拟开漏效果,也不是不行,但效率很低不推荐。
4. 从 LED 到按钮:一个完整的气象站示例串联全部知识点
前面讲了一堆零散的知识点,现在用一个完整的实例把它们全部串起来感受一下。假设你要做一个室内气象站,功能是:
- 用 DHT11 温湿度传感器读取温度和湿度
- 用两个按键切换显示模式
- 通过 I2C 接口的 OLED 屏幕显示数据
- LED 在数据更新时闪烁一次作为反馈
这个项目会用到 GPIO 的输入模式(按键)、输出模式(LED)、以及两个复用功能(I2C 的 SDA/SCL 引脚)。接线方案如下:
| 外设 | 引脚 | GPIO |
|---|---|---|
| DHT11 数据引脚 | 物理引脚 4 | GP2 |
| 按键1 | 物理引脚 16 | GP16 |
| 按键2 | 物理引脚 18 | GP17 |
| OLED SDA | 物理引脚 21 | GP16? 不对,是 GP16 已被按键占用 |
看完上面这段你可能已经警觉了:GP16 被按键占用了,OLED 该接哪里?Pico 的 I2C 外设支持多个引脚组合,常用的有 GP0/GP1、GP4/GP5、GP18/GP19。选择 GP4(物理引脚 6)和 GP5(物理引脚 7)作为 SDA 和 SCL,按键改接 GP16 和 GP17。
完整代码如下:
from machine import Pin, I2C, ADC import dht import time # 输出模式:板载 LED led = Pin(25, Pin.OUT) # 输入模式:两个按键,启用内部上拉 btn1 = Pin(16, Pin.IN, Pin.PULL_UP) btn2 = Pin(17, Pin.IN, Pin.PULL_UP) # DHT11 数据引脚 sensor = dht.DHT11(Pin(2)) # I2C 初始化,GP4=SDA, GP5=SCL i2c = I2C(0, scl=Pin(5), sda=Pin(4), freq=400000) # OLED 驱动 from ssd1306 import SSD1306_I2C oled = SSD1306_I2C(128, 64, i2c) display_mode = 0 # 0:温度 1:湿度 while True: try: sensor.measure() temp = sensor.temperature() hum = sensor.humidity() # 数据更新,LED 闪烁一次 led.value(1) time.sleep_ms(50) led.value(0) # 按键切换显示模式 if btn1.value() == 0: time.sleep_ms(20) if btn1.value() == 0: display_mode = 0 if btn2.value() == 0: time.sleep_ms(20) if btn2.value() == 0: display_mode = 1 # OLED 显示 oled.fill(0) if display_mode == 0: oled.text("Temp: {} C".format(temp), 0, 0) else: oled.text("Humidity: {}%".format(hum), 0, 0) oled.show() except Exception as e: print("Error:", e) time.sleep_ms(1000)这个项目的价值在于把输入输出、复用功能、消抖、外设驱动全部整合在一起。后面做更复杂的项目,无非是在这个框架上增加更多外设,但 GPIO 操作的底层逻辑完全一致。
5. 实战避坑:程序反复报错的常见常见问题排查
第一次上手 Pico GPIO 的人,十个里有八个会在下面这些问题上卡壳。我把踩过的坑按频率排了个序,你对照着排查能省不少时间。
坑一:引脚的编号写错。GPIO 编号不是物理引脚序号。GP0 对应物理引脚 1,GP1 对应物理引脚 2,GP2 对应物理引脚 4。物理引脚 3、5、7 这些奇数编号都是 GND 或电源引脚,不是 GPIO。新手最容易犯的错误是把物理引脚 3 当 GP3 用,实际上 GP3 是物理引脚 5。
坑二:忘了 Pico 是 3.3V 逻辑电平。驱动 5V 继电器模块、5V LED 灯带这类设备,直接用 GPIO 引脚接过去就是自寻短路。解决办法是加电平转换模块,或者用光耦隔离。
坑三:输入悬空导致误触发。按键、传感器这类输入引脚,必须在软件上启用内部上拉或下拉,否则引脚电平漂浮不定,读到的数据毫无意义。
坑四:ADC 引脚当成普通 GPIO。GP26、GP27、GP28 这 3 个引脚既是普通 GPIO,也是 ADC 通道。但 ADC 输入电压范围是 0~3.3V,超过这个范围必须分压,否则直接烧毁引脚。
坑五:PWM 和中断的引脚冲突。Pico 虽然每个引脚几乎都支持 PWM,但同一时刻一个引脚只能承担一种复用功能。如果代码里先初始化 PWM,又尝试在这个引脚上读取中断触发,大概率会得到不符合预期的行为。
坑六:MicroPython 里没有GPIO.setmode()之类的函数。按照 Arduino 和 STM32 的惯性,很多人想知道如何配置引脚模式。在 MicroPython 中,创建 Pin 对象时直接指定 mode 参数就完成了配置,不存在独立的配置函数。
6. 进阶玩法:从 GPIO 到舵机控制
你注意到热搜词里有"树莓派 pico 控制舵机",控制舵机用的是 PWM 功能,恰好是对 GPIO 概念的延伸。这里展开说说。
舵机的控制原理是:以 50Hz(20ms 周期)的频率发送 PWM 信号,脉宽 0.5ms~2.5ms 对应舵机角度 0°~180°。Pico 的任何一个 GPIO 引脚在 MicroPython 里都能配置成 PWM 输出,不需要额外硬件。
from machine import Pin, PWM import time # 将 GP15 初始化为 PWM servo = PWM(Pin(15)) servo.freq(50) # 50Hz 对应 20ms 周期 # 计算占空比 # 脉宽 1ms -> 占空比 = 1ms / 20ms = 5% # MicroPython PWM 的 duty_u16 范围是 0~65535 duty_1ms = int(65535 * 0.05) # 脉宽 1ms duty_1_5ms = int(65535 * 0.075) # 脉宽 1.5ms,舵机中位 duty_2ms = int(65535 * 0.1) # 脉宽 2ms # 从 0° 到 180° servo.duty_u16(duty_1ms) time.sleep(1) servo.duty_u16(duty_1_5ms) time.sleep(1) servo.duty_u16(duty_2ms) time.sleep(1) # 停止输出 servo.deinit()注意这里的duty_u16()方法接收的值是 0~65535 的占空比级别,不是直接用毫秒数。换算公式是:
duty = 脉宽(ms) / 周期(ms) × 65535这个坑我踩过:一开始我按直觉把duty_u16(1500)当成 1.5ms 用,结果舵机直接转到奇怪的角度。后来才反应过来,MicroPython 的 PWM 占空比用的是一整个 16 位寄存器的值,不是微秒。
7. 引脚冲突的全景排查:一次外设无法工作的完整链路排错
最后分享一次真实的排错经历。当时做一个环境监控项目,I2C 接的 OLED 屏幕死活不显示数据,但代码逻辑检查多次没有任何问题。排查过程如下,大家可以看看排查思路:
第一步:检查接线。SDA 接 GP4(物理引脚 6),SCL 接 GP5(物理引脚 7),都核对无误。VCC 和 GND 分别为 3.3V 和 GND。
第二步:硬件检测。用万用表量 SDA 和 SCL 的电平,发现两个引脚都是 0V。正常应该是 3.3V 高电平(I2C 空闲时靠上拉电阻拉高)。怀疑是总线被拉死了。
第三步:软件检查。查阅 I2C 初始化代码:
i2c = I2C(0, scl=Pin(5), sda=Pin(4), freq=400000)看着没问题,但再用i2c.scan()扫描设备地址时,返回了空列表。
第四步:排查硬件故障。拔掉 OLED,单测 SDA 和 SCL 电平,发现仍然是 0V。问题不在 OLED,而在 Pico 本身。
第五步:回溯引脚复用。检查自己的代码,发现之前初始化过一个 Button 对象,刚好用了物理引脚 5(GPIO 4)做输入。虽然代码逻辑上没有冲突,但在 MicroPython 中,同一个物理引脚如果被前面代码占用了,后面的 I2C 初始化不会报错,但引脚电平已经被前面的配置改变了。释放引脚需要调用Pin.deinit()或重启。
第六步:重启板子并调整初始化顺序。让 I2C 初始化放在按键之前,问题解决。
这个案例暴露了一个容易被忽视的问题:MicroPython 不支持重复初始化同一个引脚。同一个物理引脚在代码中被创建了多次 Pin 对象,每次初始化的配置参数可能不同,后面的初始化可能无效或者覆盖前一个配置。严格来说,一个引脚最好只初始化一次。
另外,I2C 的 SDA/SCL 引脚需要接上拉电阻(约 4.7kΩ),这是 I2C 协议的硬件要求。Pico 板载了部分引脚的 I2C 上拉电阻,但不是所有引脚组合都有。如果你用的是非板载默认的 I2C 引脚组合,记得在外部加上拉电阻。
根据我个人经验,做 GPIO 相关项目前,先画一张引脚分配表,把每个 GPIO 分配给哪个外设、用到了什么复用功能写清楚。看似多花了五分钟,实际能省下后面几小时的排错时间。Pico 的引脚资源虽然丰富,但一旦牵扯到复用功能,冲突排查就很痛。希望这篇能帮你把 Pico 的 GPIO 玩得顺手。