树莓派Pico GPIO深入解析:从Pin类到PWM与ADC实战
2026/9/6 12:53:57 网站建设 项目流程

1. 从一盏LED开始认识machine.Pin:它到底在管什么

先别急着敲代码。但凡接触过树莓派Pico,第一件事大概率就是点亮板载LED——很多人照着教程敲完LED = machine.Pin(25, machine.Pin.OUT),灯亮了,然后就以为自己会GPIO了。等真要接传感器、控制舵机、读按键的时候,才发现报错一个接一个,查半天资料越查越迷糊。

我一直觉得,GPIO是嵌入门最容易被"秒懂"但也最容易被"误解"的东西。秒懂是因为功能名就摆在明面上——Pin.OUT就是输出,Pin.IN就是输入,Pin.HIGH就是高电平,Pin.LOW就是低电平,看着毫无门槛。误解则藏在很多看不见的地方:Pin编号和物理引脚的映射关系、上下拉电阻的适用场景、中断回调的触发时机、PWM和普通输出的本质区别……这些不搞明白,后面跑舵机、接编码器、做按键扫描,每一个都是坑。

这篇东西我不会从头讲嵌入式基础,直接以MicroPython的machine.Pin类为主线,把Pico的GPIO控制完整捋一遍。覆盖的内容包括:Pin对象怎么建、输出模式怎么用、输入模式怎么稳定读、中断怎么写才不丢事件、PWM和ADC这两个高频复用功能怎么和GPIO配合,以及我实际调试中踩过的若干次坑。

内容面向的读者是这样的:已经有一块Pico(无论Pico 1还是Pico 2,原理基本一致),装好了Thonny或mu,至少跑通过print("hello"),接下来想正经控制外部设备。如果你手里只有一块板子什么外设都没有,那这篇文章也够用——因为所有代码示例我都会优先用板载LED和板载按键(如果有)来演示,没有温湿度传感器、没有OLED屏、没有舵机,一样能跟到底。

在往深处走之前,先确立一个基本认知:GPIO是芯片物理引脚的功能抽象,Pin是MicroPython里对应的那个对象。在Pico的MicroPython固件里,每一个可用的GPIO引脚都对应一个可以创建的Pin实例。你操作的是Pin对象,但它背后真正干活的是芯片的GPIO外设控制器。

2. 引脚编号为什么是25?——GPIO、物理引脚和Pin对象的关系

拿Pico板载LED举例,Pin(25, Pin.OUT)里的25,指的不是电路板上丝印的物理引脚编号。它指的是RP2040芯片内部的GPIO25号引脚——芯片的"功能编号",不是你在板子上看到的那种排针编号。这一点非常多人搞混:板子物理丝印上写着GP0、GP1、GP2……这些和芯片的内部GPIO编号完全对应,没有偏移。但LED是个例外,它没有引出到排针,直接接到了芯片的GPIO25上,所以Pin(25)才能控制它。

Pico的排针物理引脚和GPIO编号的映射关系,看起来有一点绕,但实际规律性很强。以最常见的40引脚Pico为例,左侧从下往上依次是1到20,右侧从下往上依次是21到40。GPIO0在物理引脚1,GPIO1在物理引脚2,GPIO2在物理引脚4,GPIO3在物理引脚5,GPIO4在物理引脚6,GPIO5在物理引脚7——到了GPIO6到GPIO9,它们位于右侧的物理引脚9和10之间的那排内孔里。总之,丝印GP号和芯片GPIO编号是对应的,只有极个别特殊引脚(比如ADC通道、I2C复用)有额外别名。

Pin编号的三种引用方式,这里统一说清楚:

  • 数字编号:Pin(0)Pin(25),最直接。
  • 字符串编号:Pin('GP0')Pin('GPIO25'),比纯数字可读性好一点,适合团队协作或代码审查时使用。
  • 复用别名:例如ADC通道,ADC(0)对应GPIO26。这是ADC类自己的编号方式,和Pin类无关,但很多人混用。

我的建议是,写代码一律用数字编号,同时写注释标明物理引脚号。因为MicroPython固件版本偶尔会调整别名支持,数字是最稳定的。另外注意,Pico的GPIO编号范围是0到28,但并不是所有编号都有物理引出。实际能用的GPIO是GPIO0到GPIO22,以及GPIO26到GPIO28。GPIO23到GPIO25虽然存在,但通常被板载功能占用,一般用户不碰。

下表是我的个人速查表,写代码时放在手边非常有用:

功能单元GPIO编号物理引脚说明
I2C0 SDAGPIO0物理引脚1同时可作为普通GPIO输入输出
I2C0 SCLGPIO1物理引脚2同时可作为普通GPIO
SPI0 RXGPIO16物理引脚21复用SPI功能
SPI0 SCKGPIO18物理引脚24复用SPI功能
ADC0GPIO26物理引脚31唯一真正意义上的模拟输入通道
ADC1GPIO27物理引脚32模拟输入
ADC2GPIO28物理引脚34模拟输入
板载LEDGPIO25无引出反面朝上时靠近USB口那颗

理解编号关系之后,建Pin对象就是水到渠成的事了。

from machine import Pin led_onboard = Pin(25, Pin.OUT) btn = Pin(14, Pin.IN, Pin.PULL_UP)

这两行几乎覆盖了Pin类80%的用法。第一个参数是GPIO编号,第二个是工作模式,第三个(可选)是特殊参数。下面一节详细拆每一个参数的含义和取舍逻辑。

3. 模式选择的判断逻辑:OUT、IN、PULL_UP、PULL_DOWN到底怎么组合

Pin构造函数的完整签名是:

Pin(id, mode=-1, pull=-1, value=None, drive=None, alt=-1)

日常使用中,你基本只需要关心前四个参数。mode就是Pin.OUTPin.INPin.OPEN_DRAIN这三种,pullPin.PULL_UPPin.PULL_DOWN或省略,value只在输出模式下用,用来设定初始电平——这个参数非常实用,但要特别注意它的生效时机。

我见过很多初学者这样写:

p = Pin(5, Pin.OUT) p.value(1)

两步倒也不能说错,但存在一个极短的时间窗口:引脚在Pin对象创建之后、value(1)执行之前,输出的是默认低电平。对外部设备来说,可能出现一个瞬间的"误触发"。更稳妥的写法是:

p = Pin(5, Pin.OUT, value=1)

这样引脚从初始化的那一拍开始就是高电平。尤其在控制继电器、电磁锁这类设备时,这种细节能避免很多奇怪问题。

3.1 OUT模式:输出高低电平,但不等于能直接驱动大负载

输出模式是GPIO最基础的用法。Pico自带的MicroPython固件里,对Pin的value()方法支持整数和布尔值两种参数,1TruePin.HIGH等价,0FalsePin.LOW等价。

from machine import Pin import time led = Pin(25, Pin.OUT) while True: led.toggle() # 翻转电平 time.sleep(0.5)

这里稍微展开讲一下toggle():这个方法每调用一次,就把当前输出电平翻转一次。对于做流水灯、闪烁这类需求,比value(not value())省事得多。但注意,toggle()只适用于输出模式,如果在输入模式下调用,会直接抛异常。此外要在脑海里建立"电流限制"这根弦。Pico的GPIO每个引脚最大输出电流大约12mA(绝对上限),推荐的工作范围是2-4mA左右。点一颗LED串个220欧电阻没问题,但直接驱动一个5V继电器线圈或者直流电机,绝对不行——那得靠三极管、MOS管或专门的驱动模块来中转。

GPIO的八种工作模式是STM32体系里常讲的概念(输入浮空、输入上拉、输入下拉、模拟输入、开漏输出、推挽输出、推挽复用、开漏复用),很多从STM32转过来的朋友会下意识找这些模式在Pin类里对应的定义。machine.Pin并没有把模式拆得那么细,它用三个基础模式加一个开漏模式,覆盖了绝大多数场景:

实际需求STM32对应模式Pico的Pin类写法
读取外部数字信号输入浮空Pin(14, Pin.IN)
读取按键(接地)输入上拉Pin(14, Pin.IN, Pin.PULL_UP)
读取外部上拉信号(接地触发)输入下拉Pin(14, Pin.IN, Pin.PULL_DOWN)
普通高低电平输出推挽输出Pin(15, Pin.OUT)
模拟电压采集模拟输入使用ADC类,不是Pin类
多设备共享总线开漏输出Pin(16, Pin.OPEN_DRAIN)

3.2 IN模式:浮空输入的可怕之处,以及上下拉电阻的救场能力

输入模式看起来就是读电平,但这里的坑最多。最大的坑来自"浮空输入"——引脚什么都不接的时候,读到的值是不确定的。这不是Pico的bug,是所有CMOS芯片的共性:悬空引脚的电平会被环境电磁噪声、手指靠近等外部因素干扰,在高低电平之间反复横跳。

验证这个现象很简单:

from machine import Pin import time floating_pin = Pin(16, Pin.IN) # 什么都不接,也不开上拉下拉 for _ in range(20): print(floating_pin.value()) time.sleep(0.05)

你会看到输出里既有0又有1,完全没有规律。所以接按键、接限位开关、接编码器输出这些场景,必须让引脚有一个确定的默认电平。两种实现方式:

  1. 硬件外部电阻:外部电路里接一个10k欧电阻到3.3V(上拉)或GND(下拉),这种方式更适合需要稳定性的场景,但需要额外元件。
  2. 内部上下拉:直接把Pico内部的上拉或下拉电阻打开。这省事,也是多数场景下的首选。
# 按键接在GPIO14和GND之间,内部上拉 btn = Pin(14, Pin.IN, Pin.PULL_UP) while True: if btn.value() == 0: print("pressed") time.sleep(0.01)

内部上拉的等效电阻大约在50k欧左右,对多数按键场景完全够用。但有两个例外:一是使用较长导线连接外部传感器时,长导线本身像天线,50k上拉可能压不住环境干扰,这时需要外部2k-10k电阻来加强;二是I2C总线,这时候应该用开漏模式加外部上拉,而不是内部上拉——内部上拉阻值太大,I2C的高速通信时序会出问题。

上下拉选择的核心逻辑,一句话说清楚:按键另一端接GND就开上拉,另一端接3.3V就开下拉。让引脚在器件不动作时处于确定的空闲电平,动作时翻转到另一个电平。

3.3 OPEN_DRAIN模式:为什么I2C总线非要它不可

开漏输出是一个相对难理解但很重要的概念。普通输出模式(推挽输出)能主动输出高电平和低电平;开漏输出则只能主动输出低电平,高电平是靠外部上拉电阻"拉"上去的。

画个不严谨但好懂的类比:推挽输出像是双手都能推东西的人,既能推高也能拉低;开漏输出像是只有一只手能往下拉,想让它变高,得靠一个弹簧(外部上拉电阻)帮忙。

为什么要用这种看起来不方便的模式?因为多设备共享一根线。I2C总线上可能挂了好几个设备,如果每个设备都用推挽输出,两个设备同时一个输出高一个输出低,就会形成短路电流打架,轻则数据错误,重则烧引脚。开漏模式下,每个设备只能拉低总线,无法主动拉高,就彻底避免了冲突。

# 手动模拟一个开漏引脚 sda = Pin(0, Pin.OPEN_DRAIN, Pin.PULL_UP) sda.value(0) # 拉低总线 sda.value(1) # 释放总线(不驱动,由上拉电阻拉高)

在Pico上,你实际上很少直接创建Pin.OPEN_DRAIN对象,因为I2C、OneWire这些协议都有现成的库帮你处理好了底层。但理解开漏逻辑仍然重要——当你排查I2C设备拉低总线导致SDA一直为低这类问题时,知道"总线被某个设备拉死了"这个排查思路,就会快很多。

4. value()、high()、low()、toggle():输出控制的方法全景

Pin类在输出控制上提供了几个方法,功能上有重叠,但不完全等价。我把它们全部列出来,对比一下各自的行为边界。

方法作用返回值适用场景
pin.value(x)设置输出电平(x可为0/1/True/False)最通用的电平设定
pin.value()读取当前电平0或1输入模式读取
pin.high()输出高电平语义清晰,可读性好
pin.low()输出低电平语义清晰,可读性好
pin.toggle()翻转当前输出电平闪烁、状态机切换
pin.irq()配置中断回调对象事件触发场景

这里有一个很多人没意识到的细节:value()是有两个重载形态的——带参数是写操作,不带参数是读操作。这在MicroPython里看起来挺自然,但如果你有C语言背景,会觉得这种方法重载有点"不正经"。实际上这正是MicroPython的设计取向:减少要记忆的方法名数量。

输出方向还有一个容易忽略的点:high()low()在Pico固件里确实存在,但有些MicroPython移植版不一定实现这两个方法。如果想让代码具备跨开发板可移植性(比如从Pico换到ESP32),建议统一用value(1)value(0)

再补充一个驱动外部设备时常见的"复用输出"场景。比如你想用一个GPIO控制一个有源蜂鸣器,同时另一个GPIO控制一个LED,如果盲目用toggle()做闪烁,两个信号会完全独立变化,导致逻辑混乱。我在项目里通常的做法是维护一个简单的状态变量,而不是依赖toggle的记忆特性:

from machine import Pin import time buzzer = Pin(13, Pin.OUT, value=0) led = Pin(25, Pin.OUT, value=0) state = False while True: state = not state buzzer.value(state) # 蜂鸣器跟随状态 led.value(state) # LED同步,保持逻辑一致 time.sleep(0.5)

4.1 初始化电平的时序陷阱

输出模式的value=参数有一个隐式行为:它在引脚被初始化的那一瞬间就会生效。这个"瞬间"对整个系统的上电过程来说可能早于你其他代码执行。比如你控制一个加热棒,初始化电平是value=1,板子一上电加热棒就工作了,过了几百毫秒你的主程序才跑到while True去检查按键是否要关闭它——这几百毫秒可能就是事故窗口。

应对方案有几种:

  • 对外设的电源单独用一个MOS管控制,GPIO默认低电平(关闭),需要时再拉高。
  • 初始化时先Pin(15, Pin.OUT, value=0),确保上电默认关闭设备,主循环里再根据业务逻辑打开。
  • 对于必须上电就工作的设备,用硬件电路做反逻辑:GPIO输出低电平让MOS管导通,输出高电平让MOS管截止,这样即使Pico在复位期间引脚是高阻态或高电平,设备也不会误动作。

这个思路在所有单片机项目里都通用,Pico的RP2040也不例外。

5. 输入读取的稳定性问题:消抖、毛刺和PULL配置的最佳实践

输入读取看起来就是pin.value()一行代码的事,但实际用起来你会发现:按键按十次,有八次能正确读到,另外两次要么没反应,要么读到了"幽灵按键"。

这就是抖动在作怪。机械按键按下和松开的瞬间,触点会经历一个短暂的通断振荡过程,通常持续5-20毫秒。如果程序在这个振荡窗口内多次读取引脚,就会读到一串01010101之类的随机电平,而不是干净的0到1跳变。

解决抖动的三种常用手段:

  1. 硬件RC滤波:在按键两端并联一个0.1uF电容,简单粗暴,效果可靠。
  2. 延时消抖:检测到电平变化后,等20毫秒再读一次,确认状态稳定才采纳。
  3. 状态机消抖:连续多次采样,比如50毫秒内连续读到10次相同电平才认为有效。

在MicroPython这种解释型环境里,延时消抖是最常见也最够用的方案:

from machine import Pin import time btn = Pin(14, Pin.IN, Pin.PULL_UP) last_state = 1 debounce_ms = 20 last_debounce_time = 0 while True: now = time.ticks_ms() reading = btn.value() if reading != last_state: last_debounce_time = now if time.ticks_diff(now, last_debounce_time) > debounce_ms: if reading != current_state: current_state = reading if current_state == 0: print("button pressed") last_state = reading

这段代码的逻辑是标准的消抖模式:电平变化时记录时间,但不当即响应;等到电平稳定持续超过debouce_ms,才确认状态真的变了。好处是无论按键按得多快,只要抖动期过去就能正确识别,不会漏事件。

关于输入模式还有一个高价值技巧:用中断替代轮询。如果你的主循环正在做耗时操作(比如驱动多个舵机),轮询按键很容易错过短暂的低电平脉冲。这种情况应该使用Pin.irq(),下一节详细讲。

5.1 把输入配置封装成函数,减少重复劳动

每次写Pin(14, Pin.IN, Pin.PULL_UP)虽然不长,但当一个项目里有六七个按键、四五个传感器时,这种机械代码就会占据大量篇幅。我习惯把输入引脚定义集中放到一个配置函数里,既方便阅读,也方便改引脚:

from machine import Pin def init_inputs(): buttons = { 'up': Pin(2, Pin.IN, Pin.PULL_UP), 'down': Pin(3, Pin.IN, Pin.PULL_UP), 'ok': Pin(4, Pin.IN, Pin.PULL_UP), } sensors = [ Pin(10, Pin.IN, Pin.PULL_UP), # 限位开关1 Pin(11, Pin.IN, Pin.PULL_UP), # 限位开关2 ] return buttons, sensors

这样在主程序里始终只面对名字,而不是裸奔的GPIO编号。工程化思维不一定只在大型项目里才有意义,小项目养成好习惯,后面扩展省的是自己的时间。

6. Pin.irq():中断回调的正确打开方式,避免丢事件和反复触发

轮询的缺点很明显:CPU得不停去看引脚状态,看漏了就丢了。MicroPython的Pin.irq()方法允许你在引脚电平发生变化时,让系统自动调用一个回调函数,主程序可以放心去干其他事情。

基本用法:

from machine import Pin flag_pressed = False def btn_handler(pin): global flag_pressed flag_pressed = True btn = Pin(14, Pin.IN, Pin.PULL_UP) btn.irq(trigger=Pin.IRQ_FALLING, handler=btn_handler) # 主循环只检查标志位 while True: if flag_pressed: flag_pressed = False print("detected")

6.1 触发方式的四种选择和硬件电平的逻辑关系

trigger参数支持以下几种:

触发方式含义什么时候用
Pin.IRQ_RISING电平从低变高外部设备输出高电平脉冲
Pin.IRQ_FALLING电平从高变低按键接地、传感器低电平有效
Pin.IRQ_LOW_LEVEL低电平持续唤醒类应用,但MicroPython支持有限
Pin.IRQ_HIGH_LEVEL高电平持续少数特殊场景

这里有个非常容易搞反的地方:按键接GND配合内部上拉,按下时电平从高变低,所以应该用Pin.IRQ_FALLING。反过来,如果按键接3.3V配合内部下拉,按下时电平从低变高,就应该用Pin.IRQ_RISING

在实际调试时,我常常先在回调里打印一条消息,确认触发条件符合预期,再继续写业务逻辑。

6.2 回调函数里绝对不能做的事:内存分配、延时和print

这一点我要反复强调,因为它在真实项目里导致的bug最隐蔽。MicroPython的中断回调运行在底层中断上下文中,处于该上下文时会限制一些操作:不要在回调里做耗时操作,不要在回调里进行内存分配(比如创建列表、字符串拼接),不要在回调里调用time.sleep(),尽量避免直接调用print()

print()严格来说不是绝对禁止,但在某些固件版本中,中断回调里调用print可能触发内存分配或阻塞,导致系统崩溃或行为异常。正确做法是,回调里只做两件事:置标志位、存数据到预分配的变量或队列里。

from machine import Pin import time event_counter = 0 def btn_handler(pin): global event_counter event_counter += 1 # 只做最快的事 btn = Pin(14, Pin.IN, Pin.PULL_UP) btn.irq(trigger=Pin.IRQ_FALLING, handler=btn_handler) while True: if event_counter: print("events:", event_counter) event_counter = 0 time.sleep(0.05)

6.3 irq的返回值问题:enable和disable

pin.irq()方法的返回值在某些固件版本里是一个IRQ对象,可以调用它的disable()enable()方法来临时开关中断。但在Pico的固件里,irq()的返回行为在不同版本之间不太一致,更稳的做法是直接用一个全局开关变量来控制回调逻辑:

irq_enabled = True def btn_handler(pin): global irq_enabled if not irq_enabled: return irq_enabled = False # 避免重复触发处理 # 业务处理放这里

这个模式可以防止中断频繁触发导致回调重入。对于按键场景,相比依赖底层硬件的去抖,这种软件层面的"一次性锁"更可控。

7. 从GPIO到PWM:让舵机转起来的关键扩展

Pico的GPIO不只有数字输出和输入,它还支持PWM(脉冲宽度调制)。这里的关键认知是:PWM在MicroPython里不属于Pin类,而是独立的PWM。你在Pin上做不了PWM的占空比设置,必须把Pin对象交给PWM类,让它接管输出。

7.1 PWM通道的底层复用规则

RP2040芯片内部有8个PWM切片,每个切片有两个通道(A和B),总共有16个PWM通道。这16个通道映射到不同的GPIO引脚上,但同一个PWM通道不能同时分配给两个引脚——比如GPIO0和GPIO16都可能映射到PWM通道0A,如果你先对GPIO0做了PWM,再对GPIO16做PWM,第二个操作会接管前一个。

常见几个引脚的PWM映射:

GPIOPWM通道
GPIO00A
GPIO10B
GPIO21A
GPIO31B
GPIO42A
GPIO52B
GPIO63A
GPIO73B
GPIO84A
GPIO94B
GPIO156B
GPIO160A
GPIO170B

实际使用中不需要硬背这张表,但遇到"两个PWM引脚同时工作一个没反应"的问题时,去查通道是否有冲突,往往是根因。

7.2 控制SG90舵机的完整参数计算

舵机控制是Pico GPIO教程里最常被搜的需求。以SG90这类模拟舵机为例,它的控制原理是:周期20ms(50Hz)的PWM信号,高电平时间在0.5ms到2.5ms之间,对应0度到180度。

换算成PWM的占空比参数,需要结合Pico的PWM实现。MicroPython在Pico上初始化PWM时需要指定频率和占空比:

from machine import Pin, PWM servo = PWM(Pin(15)) servo.freq(50) # 20ms周期 # 占空比设置方法因固件版本而异 servo.duty_u16(int(0.5 / 20 * 65535)) # 约1638,对应0度 servo.duty_u16(int(2.5 / 20 * 65535)) # 约8192,对应180度

这里我用的是duty_u16(),它的参数范围是0到65535,表示占空比占整个周期的比例。计算方式:

  • 0度:0.5ms / 20ms = 2.5%,乘以65535 ≈ 1638
  • 90度:1.5ms / 20ms = 7.5%,乘以65535 ≈ 4915
  • 180度:2.5ms / 20ms = 12.5%,乘以65535 ≈ 8192

如果你用的是旧版固件,可能会见到duty()方法,它接受0(全低)到1023(全高)之间的值,这是10位分辨率时代的遗留。新版固件推荐duty_u16()。两者别混用,不然舵机角度会完全不对。

7.3 PWM调速的常见坑:频率太低会闪烁,太高会啸叫

用PWM驱动LED做呼吸灯,对频率不敏感,人眼看不出区别。但用PWM驱动电机或者舵机,频率选择就要注意了:

  • 舵机:频率必须是50Hz左右,这是舵机内部电路约定的周期。
  • 直流电机:推荐频率在1kHz到20kHz之间,频率太低电机会发出明显的"嗡嗡"声,频率太高驱动电路可能承受不了。
  • LED调光:200Hz以上就没有可见闪烁了,用1kHz比较合适。

这些参数在写代码前就应该定好,而不是运行时反复改freq()。运行时修改频率会让正在输出的信号中断几个周期,如果此时外部设备正在执行关键动作,可能出现抖动。

8. ADC与GPIO的分工:读模拟量时的引脚选择

GPIO是数字信号接口,只能区分高低电平,读不了真实的电压数值。Pico的ADC(模数转换器)功能专门负责这件事,它的是用machine.ADC类,不是machine.Pin类。但ADC的引脚本质还是GPIO,所以绕不开引脚选择的问题。

8.1 Pico支持哪些模拟输入引脚

标准Pico板卡上,只有GPIO26、GPIO27、GPIO28三个引脚可以接ADC,对应ADC0、ADC1、ADC2通道。另外还有一个内部通道用来读取芯片内部温度传感器。GPIO26-28同时也是普通数字GPIO,但一旦你用ADC(26)创建了ADC对象,这个引脚就被ADC功能接管,不能再当普通GPIO用了。

from machine import ADC adc = ADC(26) # 或者 ADC(Pin(26)) raw = adc.read_u16() # 返回0-65535 voltage = raw * 3.3 / 65535 # 换算成电压 print("voltage:", voltage)

ADC输入电压范围是0到3.3V。注意,不要直接往ADC引脚上加5V电压,会烧毁引脚。如果需要测量更高电压,必须先用电阻分压或专用模块把电压降到3.3V以内。

8.2 ADC读取的稳定性和滤波

ADC在Pico上的精度和稳定性整体不错,但直接读数仍然会有±1到±3个LSB的波动。如果你的应用需要稳定读数(比如测电池电压),建议做软件滤波。常用的办法是多次采样取平均值,或者去掉最大最小值后再平均:

from machine import ADC import time adc = ADC(26) def read_avg(n=16): total = 0 for _ in range(n): total += adc.read_u16() time.sleep_ms(1) return total // n while True: raw = read_avg() voltage = raw * 3.3 / 65535 print("voltage: {:.2f} V".format(voltage)) time.sleep(0.5)

这个简单的移动平均滤波在大多数场景下够用了。如果你的传感器输出阻抗特别高,可能还需要在采样前加一个小的RC低通滤波电路,但那是硬件范畴了,这里不展开。

9. 一个完整案例:按键控制LED亮灭和舵机角度,串起来跑一遍

理论讲了这么多,没有综合案例总觉得不够落地。下面这个小项目把按键输入(带中断)、LED输出、PWM舵机控制全部串起来,作为本文的实践收尾。

项目目标:按键按下一次,LED切换一次亮灭状态;长按按键,舵机缓慢旋转到90度;再次长按,回到0度。

from machine import Pin, PWM import time # 硬件配置 led = Pin(25, Pin.OUT, value=0) btn = Pin(14, Pin.IN, Pin.PULL_UP) servo = PWM(Pin(15)) servo.freq(50) # 舵机角度换算助手 def set_servo_angle(angle): # 0度 -> 0.5ms, 180度 -> 2.5ms, 20ms周期 pulse_ms = 0.5 + (angle / 180.0) * 2.0 duty = int(pulse_ms / 20.0 * 65535) servo.duty_u16(duty) # 变量定义 led_on = False press_count = 0 last_press_time = 0 # 中断回调:只计数 def btn_handler(pin): global press_count press_count += 1 btn.irq(trigger=Pin.IRQ_FALLING, handler=btn_handler) # 主循环:处理按键事件 while True: if press_count: press_count -= 1 now = time.ticks_ms() if time.ticks_diff(now, last_press_time) > 300: # 简单防抖 led_on = not led_on led.value(led_on) if led_on: print("LED ON") else: print("LED OFF") last_press_time = now # 长按检测(每隔一段时间检查按键是否持续按住) if btn.value() == 0: # 按下持续2秒 time.sleep_ms(50) # 注意:主循环内短延时,仅作为演示 # 实际项目不建议在主循环中长时间阻塞 pass

这个例子里press_count模式就是前面讲的中断安全做法——回调只计数,主循环慢慢处理。实际的长按检测在MicroPython里常用time.ticks_ms()配合起始时间来判断,这里为了保持示例简洁没有过度展开,但思路是相通的。

如果你有示波器或者逻辑分析仪,可以把按键引脚和舵机控制引脚的波形抓出来看:按键按下时电平会有一个短促的抖动期,舵机PWM波形则是稳定周期的方波。看波形和看代码的感觉完全不同,强烈建议尝试。

10. 实际调试中躲不开的那些坑:针对Pico GPIO问题的排查思路

这节集中汇总我过去实录的调试经验和网络上高频出现的问题,按"现象、原因、解决"的方式整理,方便当速查手册用。

10.1 上电后引脚电平不确定,外设乱动

现象:Pico一上电,继电器咔哒响一下,或者蜂鸣器叫一声,然后才恢复正常。

原因:RP2040在上电复位期间,所有引脚默认处于高阻输入状态,电平由外部电路决定。如果外部设备是低电平触发且引脚被外部上拉到高电平,反而可能误触发。此外,在MicroPython初始化之前,固件本身也有一个短暂的启动窗口,引脚状态不受你代码控制。

解决:在硬件设计时就让设备的"上电默认状态"为安全状态;软件上,在所有外设初始化之前,先把关键控制引脚设置为Pin.OUT并赋确定电平。

10.2 按键接上之后一直触发中断

现象:没碰按键,中断回调却一直触发,主循环里事件计数不断增长。

原因:最可能是按键接法错误导致引脚悬空。比如按键一端接GPIO,另一端什么都没接,那GPIO就是浮空输入,电平随机抖动,自然频繁触发中断。

解决:检查按键另一端是否确实接到了GND(上拉模式下)。另外,中断回调里如果没做防抖处理,机械按键的抖动也会导致一次按压触发多次中断。

10.3 两个I2C设备都正常,但三个设备时就随机失败

现象:I2C总线上挂两个设备正常,挂第三个设备后偶尔通信失败,时好时坏。

原因:很可能是总线上拉电阻的等效阻值过小或过大。Pico内部上拉约50k欧,对于低速短距离是可以的,但设备多了、导线长了,上升沿变缓,通信就不稳定。通常I2C总线上拉电阻推荐值是2.2k到4.7k欧,视总线电容而定。

解决:外部并接一个4.7k或10k欧上拉电阻到3.3V试试,不要依赖内部上拉。

10.4 用PWM控制舵机,舵机一直抖动或者根本不动

现象:舵机要么不动,要么在一个角度上不停抖动。

原因:90%是PWM频率或者占空比范围不对。SG90舵机要求20ms周期,也就是50Hz,而新手常犯的错误是用duty_u16(65535)把占空比设成了100%,这样高电平时间就是整个20ms,完全超出了舵机0.5ms-2.5ms的输入范围,舵机当然不知所措。

解决:先确认freq(50)已设置;再用前面给的公式计算占空比,不要凭感觉给值;最后用servo.duty_u16(4915)测试90度位置,确认电机能转到中间位置。

10.5 GPIO编号到底用哪个:Pin(0)和Pin('GP0')的差异

现象:有人用Pin('GP0')没问题,有人用Pin('GP0')报错。

原因:MicroPython固件对不同开发板的Pin字符串别名支持程度不一样。Pico的标准固件支持'GP0'这种写法,但部分第三方固件可能只接受数字。

解决:跨固件、跨板卡兼容优先用数字编号。这是最保守但最可靠的做法。

10.6 默认就用PIN 25控制LED,但换了Pico 2之后灯不亮了

现象:同一份代码,在Pico 1上板载LED正常闪烁,换到Pico 2上LED不亮。

原因:RP2350芯片的内核虽然兼容MicroPython API,但不同板卡的LED引脚可能不是GPIO25。比如某些Pico 2的变种板把LED放在了GPIO26或者其他编号上。

解决:拿到一块新板卡的第一件事是查它的原理图,确认LED引脚编号。不要默认所有板子都和原版Pico一样,这不是偷懒的地方。

10.7 ADC读数不准或者跳变剧烈

现象:用ADC读取分压后的电池电压,读数比万用表测出来的值低很多,或者剧烈波动。

原因:ADC输入引脚的高阻抗特性加上传感器输出阻抗过大,导致采样时电压被拉低。另外,Pico的ADC参考电压是3.3V,但如果电源纹波较大,参考电压本身也不稳,读数自然受影响。

解决:采样引脚前加一个0.1uF电容到GND,做稳定滤波;软件上采用多次采样平均;电源部分注意3.3V的稳定性,如果外接了大量大电流设备,考虑用独立的稳压模块给Pico供电。

11. 从GPIO出发的下一步该怎么走

到这里,machine.Pin类的基础控制和常见扩展功能已经基本覆盖完了。但说句实话,这些只是入口——GPIO在真实项目里往往是和I2C、SPI、UART、PIO这些子系统配合使用的。一根引脚在不同协议里扮演的角色完全不同:它可以是I2C的SDA,可以是SPI的MOSI,可以是UART的TX,也可以只是普通的按键输入。理解machine.Pin之后,再去看machine.I2Cmachine.SPI这些类,思路会顺很多,因为它们本质上还是在操作同一批引脚,只是"复用功能"被激活了。

最后分享一个我个人调试Pico项目的小习惯:每搭建一个外设模块,就单独写一个最小测试脚本,只测这个模块,不掺任何其他逻辑。比如拿到一个舵机,先只写PWM输出跑角度;拿到一个按键,先只写按键读取打印状态。全部验证通过之后,再开始拼装完整逻辑。这样做的好处是,最后项目出问题时,问题大概率集中在"模块之间的交互",而不是某个模块本身——排查范围一下子小了很多。

这些小习惯听起来朴素,但实际用下来,能省掉大量反复烧录看日志的时间。GPIO本身不复杂,真正复杂的永远是你的设备逻辑和电路配合,而machine.Pin这个类,就是你手里最趁手的那把螺丝刀。多用,多测,多记,自然就通透了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询