4、GPIO控制与LED闪烁:用Python让硬件“呼吸”起来

各位同学,欢迎来到第四章。前面我们聊了怎么把传感器数据读进来,但嵌入式系统不光要“听”,还得会“说”。怎么“说”?最基础的方式,就是通过GPIO输出高低电平。

这一章,我们就拿一个最经典的例子——LED闪烁,来彻底搞懂GPIO输出。别小看这个实验,它背后藏着PWM(脉宽调制)的核心思想。你想想看,一个灯能亮能灭,那能不能让它半亮半灭?能不能像呼吸一样渐变?嗯,这就是PWM要解决的问题。

4.1 GPIO是什么?说白了就是“数字开关”

GPIO,全称General Purpose Input Output,通用输入输出口。你可以把它想象成一个数字引脚,要么输出高电平(3.3V或5V),要么输出低电平(0V)。

我刚开始学嵌入式时,总觉得GPIO很神秘。后来发现,它就是个受软件控制的开关。你写个1,它就输出高电平;写个0,它就输出低电平。就这么简单。

核心概念:

  • 高电平(HIGH/1):通常对应3.3V或5V,LED亮
  • 低电平(LOW/0):对应0V,LED灭
  • 输出模式:引脚作为“电源”使用,驱动外部设备
  • 输入模式:引脚作为“检测”使用,读取外部信号

这里要注意,GPIO的输出电流非常有限,一般只有几毫安到几十毫安。直接驱动大功率设备?不行,会烧引脚。我在项目中就吃过这个亏,驱动一个继电器时忘了加三极管放大,结果引脚直接冒烟了……

4.2 硬件连接:LED和电阻是“铁搭档”

在动手写代码前,先把硬件搭好。你需要:

  • 一块开发板(树莓派、ESP32、STM32都行,原理一样)
  • 一个LED灯(颜色随意,我习惯用红色,醒目)
  • 一个220Ω电阻(这个很重要,别省!)
  • 几根杜邦线

连接方式很简单:GPIO引脚 → 电阻 → LED正极(长脚) → LED负极(短脚) → GND。

警告:LED必须串联限流电阻!我曾经见过有人直接把LED接到3.3V上,结果LED瞬间烧毁,还冒出一股焦味。220Ω到1kΩ都可以,阻值越小越亮,但电流也越大。

为什么一定要加电阻?因为LED是电流型器件,它的内阻很小。不加电阻的话,电流会大到烧坏LED甚至GPIO引脚。你想想看,3.3V直接加在LED上,电流可能达到几十毫安,而GPIO最大输出也就十几毫安。嗯,这就是物理定律,绕不过去的。

4.3 Python控制GPIO:从点亮到闪烁

好了,硬件接好了,我们开始写Python代码。这里以树莓派为例,使用RPi.GPIO库。其他平台(如ESP32用MicroPython)语法类似,只是库名不同。

第一步:点亮LED

import RPi.GPIO as GPIO
import time

# 设置GPIO模式为BCM编号
GPIO.setmode(GPIO.BCM)

# 设置GPIO18为输出模式
LED_PIN = 18
GPIO.setup(LED_PIN, GPIO.OUT)

# 输出高电平,点亮LED
GPIO.output(LED_PIN, GPIO.HIGH)

# 保持5秒
time.sleep(5)

# 清理GPIO资源
GPIO.cleanup()

这段代码做了三件事:设置引脚模式、输出电平、最后清理。我个人习惯在程序结束时总是调用GPIO.cleanup(),否则下次运行可能会报错。

第二步:让LED闪烁

闪烁的本质就是“亮-灭-亮-灭”的循环。我们用while循环加上延时来实现:

import RPi.GPIO as GPIO
import time

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

try:
    while True:
        GPIO.output(LED_PIN, GPIO.HIGH)  # 亮
        time.sleep(0.5)                  # 保持0.5秒
        GPIO.output(LED_PIN, GPIO.LOW)   # 灭
        time.sleep(0.5)                  # 保持0.5秒
except KeyboardInterrupt:
    # 按Ctrl+C退出
    GPIO.cleanup()

这里用了try-except结构,确保程序被中断时能正确清理GPIO。你想想看,如果程序异常退出,GPIO引脚可能一直保持高电平,LED就一直亮着。嗯,这虽然不会烧坏硬件,但总归不专业。

4.4 从闪烁到呼吸:理解PWM原理

现在LED能闪烁了,但只有“全亮”和“全灭”两种状态。能不能让LED半亮?或者像呼吸一样慢慢变亮再慢慢变暗?

答案是:用PWM(Pulse Width Modulation,脉宽调制)。

PWM的原理很简单:在一个周期内,让高电平的时间占比(占空比)变化。占空比50%,就是一半时间亮一半时间灭,人眼看起来就是半亮。占空比越高,看起来越亮。

PWM关键参数:

参数 说明 典型值
频率 每秒的周期数 1000Hz(1kHz)
周期 一个完整波形的时间 1ms(频率的倒数)
占空比 高电平时间占周期的比例 0%~100%

为什么人眼能看到“半亮”?因为PWM的频率足够高(通常>50Hz),人眼无法分辨闪烁,只能感受到平均亮度。这就像电影一样,24帧/秒的画面在人眼里就是连续的。

我在项目中用PWM控制过舵机、电机调速、LED调光。说白了,PWM就是把数字信号“模拟化”的一种手段。你想想看,一个只有0和1的引脚,通过改变时间比例,就能实现模拟效果,是不是很巧妙?

4.5 Python实现PWM呼吸灯

树莓派的RPi.GPIO库自带PWM功能,用起来非常方便:

import RPi.GPIO as GPIO
import time

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

# 创建PWM对象,频率1000Hz
pwm = GPIO.PWM(LED_PIN, 1000)
pwm.start(0)  # 初始占空比0%

try:
    while True:
        # 从暗到亮
        for duty in range(0, 101, 1):
            pwm.ChangeDutyCycle(duty)
            time.sleep(0.01)
        # 从亮到暗
        for duty in range(100, -1, -1):
            pwm.ChangeDutyCycle(duty)
            time.sleep(0.01)
except KeyboardInterrupt:
    pwm.stop()
    GPIO.cleanup()

这段代码实现了呼吸灯效果。核心就是不断改变占空比,从0%到100%再回到0%。每次变化间隔10ms,整个呼吸周期大约2秒。

小技巧:你可以调整time.sleep(0.01)的值来控制呼吸速度。数值越小,呼吸越快。我个人喜欢0.02,看起来更柔和。

这里要注意,PWM的频率不能太低。如果频率低于50Hz,人眼就能看到闪烁,那就不是“呼吸”而是“抽风”了。我一般用1000Hz,既平滑又不会产生可闻噪声。

4.6 避坑指南:GPIO控制常见问题

做GPIO控制时,有几个坑我踩过,分享给大家:

  1. 引脚编号搞混:树莓派有BOARD(物理引脚编号)和BCM(芯片引脚编号)两种模式。我建议统一用BCM,因为不同型号的树莓派物理引脚布局可能不同,但BCM编号是固定的。
  2. 忘记清理GPIO:程序退出后,GPIO可能保持最后的状态。下次运行时如果引脚被占用,会报错。记得用try-finally或with语句确保清理。
  3. 电流超限:单个GPIO引脚最大输出电流约16mA,所有引脚总和不超过50mA。驱动多个LED时,建议用三极管或ULN2003驱动芯片。
  4. 电平不匹配:3.3V的GPIO直接驱动5V的设备可能无法正常工作。需要电平转换电路,或者用MOS管做电平匹配。

我曾经在一个项目中,用树莓派直接驱动5V的继电器模块,结果继电器死活不动作。查了半天才发现,树莓派的3.3V高电平对继电器来说太低了,需要加一个NPN三极管做电平转换。嗯,从那以后,我每次接新设备都会先查数据手册,确认电平匹配。

4.7 本章小结

这一章我们做了三件事:

  • 用GPIO输出高低电平,控制LED亮灭
  • 用循环和延时实现LED闪烁
  • 用PWM实现呼吸灯效果,理解占空比的概念

PWM是嵌入式系统里非常重要的技术。除了控制LED,它还广泛应用于电机调速、舵机控制、音频输出、电源管理等领域。你想想看,一个简单的“开关”信号,通过改变时间比例,就能实现如此丰富的功能——这就是数字世界的魅力。

下一章,我们会用PWM来做更有趣的事情:控制舵机转动角度。到时候你会发现,PWM的占空比和舵机角度之间,其实有一个精确的对应关系。嗯,敬请期待。