3、GPIO基础控制:输出模式点亮LED、输入模式读取按键、PWM呼吸灯、软件防抖处理

GPIO,说白了就是芯片的“手脚”。你想让它输出高电平点亮LED,或者读取外部是高还是低,都得靠它。这一章我们就把GPIO的几种基本玩法过一遍,都是实战里天天用的东西。

本章核心知识点一览

  • 输出模式:推挽输出点亮LED,控制电平高低
  • 输入模式:读取按键状态,判断按下/释放
  • PWM呼吸灯:用软件PWM模拟呼吸效果
  • 软件防抖:消除按键机械抖动,避免误判
GPIO 基础控制知识体系 GPIO 控制 输出模式 输入模式 PWM 呼吸灯 软件防抖 点亮 LED 读取按键 软件 PWM 延时消抖 状态机消抖 💡 输出模式控制设备,输入模式感知外部,PWM 实现渐变,防抖保证可靠 四者结合,就能完成大部分基础硬件交互任务

3.1 输出模式:点亮LED

输出模式是最简单的。你把GPIO设为推挽输出,然后写高电平,LED就亮了。写低电平,LED就灭了。就这么直接。

我个人习惯用树莓派或者Jetson Nano做实验,Python的RPi.GPIO或者Jetson.GPIO库都支持。先看代码:

import RPi.GPIO as GPIO
import time

LED_PIN = 18

GPIO.setmode(GPIO.BCM)
GPIO.setup(LED_PIN, GPIO.OUT)  # 设为输出模式

# 循环闪烁
while True:
    GPIO.output(LED_PIN, GPIO.HIGH)  # 点亮
    time.sleep(0.5)
    GPIO.output(LED_PIN, GPIO.LOW)   # 熄灭
    time.sleep(0.5)

这里有个坑——GPIO.setmode一定要在最前面调用。我刚开始学的时候,忘了设模式,结果LED死活不亮,查了半天才发现是模式没设对。嗯,这种低级错误谁都会犯。

小提示:推挽输出模式下,GPIO可以直接驱动小功率LED(串一个220Ω~1kΩ限流电阻)。如果是大功率负载,比如继电器、电机,必须加驱动电路,别直接怼GPIO上,会烧引脚的。

3.2 输入模式:读取按键

输入模式就是读引脚电平。按键按下,引脚接地(低电平);松开,引脚被上拉电阻拉到高电平。代码也很简单:

BUTTON_PIN = 17

GPIO.setup(BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)  # 内部上拉

while True:
    if GPIO.input(BUTTON_PIN) == GPIO.LOW:
        print("按键按下")
    else:
        print("按键松开")
    time.sleep(0.1)

为什么用内部上拉?省一个外部电阻啊。你想想看,如果每个按键都配一个10kΩ上拉电阻,板子面积和成本都上去了。内部上拉虽然弱一点(一般50kΩ左右),但对付机械按键足够了。

不过,直接这么读会有一个问题——抖动。按键是机械触点,按下和松开的时候会弹跳几次,电平会在高低之间快速跳变。如果不处理,一次按键可能被误判成好几次。

3.3 软件防抖处理

防抖的原理很简单:检测到电平变化后,等一段时间(通常10~20ms),再确认一次电平。如果还是那个状态,就认为是有效动作。

我常用的两种方法:

方法 原理 优点 缺点
延时消抖 检测到变化后,delay 20ms再读一次 实现简单,代码直观 阻塞延时,影响其他任务
状态机消抖 连续采样N次,状态一致才确认 非阻塞,适合多任务 代码稍复杂

延时消抖示例:

def debounce_read(pin, delay_ms=20):
    if GPIO.input(pin) == GPIO.LOW:
        time.sleep(delay_ms / 1000.0)
        if GPIO.input(pin) == GPIO.LOW:
            return True  # 确认按下
    return False

状态机消抖示例(非阻塞):

class DebounceFSM:
    def __init__(self, pin, stable_count=5):
        self.pin = pin
        self.stable_count = stable_count
        self.count = 0
        self.last_state = GPIO.HIGH
        self.current_state = GPIO.HIGH

    def update(self):
        raw = GPIO.input(self.pin)
        if raw == self.last_state:
            self.count += 1
            if self.count >= self.stable_count:
                self.current_state = raw
        else:
            self.count = 0
            self.last_state = raw
        return self.current_state

注意:延时消抖虽然简单,但如果在中断服务函数里用time.sleep,会阻塞整个系统。我曾经在一个项目里这么干过,结果按键一按,整个UI都卡死了……后来改成状态机才解决。

3.4 PWM呼吸灯

PWM就是脉宽调制。通过改变高电平占空比,让LED看起来从暗到亮渐变。硬件PWM精度高,但很多GPIO不支持。没关系,我们用软件PWM模拟。

软件PWM的原理:在一个周期内,控制高电平的时间比例。比如周期20ms,高电平占10ms就是50%占空比。

import RPi.GPIO as GPIO
import time

LED_PIN = 18
GPIO.setmode(GPIO.BCM)
GPIO.setup(LED_PIN, GPIO.OUT)

def soft_pwm(pin, duty, period=0.02):
    """duty: 0~100"""
    high_time = period * duty / 100.0
    low_time = period - high_time
    GPIO.output(pin, GPIO.HIGH)
    time.sleep(high_time)
    GPIO.output(pin, GPIO.LOW)
    time.sleep(low_time)

# 呼吸效果:占空比从0渐变到100,再回到0
while True:
    for duty in range(0, 101, 2):   # 渐亮
        soft_pwm(LED_PIN, duty)
    for duty in range(100, -1, -2): # 渐暗
        soft_pwm(LED_PIN, duty)

这里有个细节:time.sleep的精度有限,周期太短(比如1ms)会导致占空比不准。我一般把周期设在20ms左右,人眼看起来还是连续的。

优化建议:如果追求更平滑的呼吸效果,可以用指数曲线调整占空比变化率,而不是线性变化。人眼对亮度的感知是对数的,线性变化看起来会“中间跳变快,两端慢”。

3.5 综合实战:按键控制呼吸灯

把上面几个知识点串起来:按键按下时,LED开始呼吸;再按一次,LED停止呼吸。同时加入防抖处理。

import RPi.GPIO as GPIO
import time

LED_PIN = 18
BTN_PIN = 17

GPIO.setmode(GPIO.BCM)
GPIO.setup(LED_PIN, GPIO.OUT)
GPIO.setup(BTN_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)

breathing = False
last_btn_state = GPIO.HIGH

def soft_pwm(pin, duty, period=0.02):
    high_time = period * duty / 100.0
    low_time = period - high_time
    GPIO.output(pin, GPIO.HIGH)
    time.sleep(high_time)
    GPIO.output(pin, GPIO.LOW)
    time.sleep(low_time)

def debounce(pin, delay_ms=20):
    if GPIO.input(pin) == GPIO.LOW:
        time.sleep(delay_ms / 1000.0)
        return GPIO.input(pin) == GPIO.LOW
    return False

try:
    while True:
        # 检测按键(带防抖)
        if debounce(BTN_PIN):
            if not breathing:
                breathing = True
                print("呼吸灯开启")
            else:
                breathing = False
                GPIO.output(LED_PIN, GPIO.LOW)
                print("呼吸灯关闭")
            # 等待按键松开,避免重复触发
            while GPIO.input(BTN_PIN) == GPIO.LOW:
                time.sleep(0.01)

        # 呼吸效果
        if breathing:
            for duty in range(0, 101, 2):
                if not breathing:
                    break
                soft_pwm(LED_PIN, duty)
            for duty in range(100, -1, -2):
                if not breathing:
                    break
                soft_pwm(LED_PIN, duty)

except KeyboardInterrupt:
    GPIO.cleanup()

这个例子虽然简单,但包含了输出、输入、PWM、防抖四个核心知识点。你想想看,实际项目里很多功能都是这些基础操作的组合。

本章要点回顾:

  • 输出模式:GPIO.setup(pin, GPIO.OUT) + GPIO.output(pin, level)
  • 输入模式:GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_UP) + GPIO.input(pin)
  • 软件防抖:延时确认或状态机采样,避免机械抖动误判
  • 软件PWM:通过控制高低电平时间比例模拟占空比,实现呼吸灯效果