Raspberry Pi Python GPIO编程: LED与传感器控制项目

内容简介

本文将深入探讨树莓派GPIO编程的核心技术,通过构建两个经典项目——“按钮控制LED"与"温度监控系统”,全面讲解Python在硬件控制中的应用。文章将详细对比并使用Python领域主流的RPi.GPIO库与简化易用的gpiozero库,涵盖引脚编号规则、输入输出配置、上拉下拉电阻原理、边沿检测中断机制以及多线程并发处理。无论你是刚入门的创客还是寻求进阶的开发者,这篇万字长文都将为你打下坚实的物联网开发基础。


第一章:树莓派与GPIO——开启物理世界的大门

在物联网时代,树莓派凭借其低廉的价格、强大的性能和丰富的接口,成为了连接数字世界与物理世界的桥梁。而GPIO(General Purpose Input/Output,通用输入输出)接口,则是这座桥梁上最基础的通道。

1.1 什么是GPIO?

GPIO是树莓派芯片上的一组引脚,它们可以被软件控制,用作信号的输入或输出。

  • 输出模式:树莓派作为“大脑”,向外部设备发送指令。例如,控制一个LED灯的亮灭,或者驱动一个继电器开启家电。
  • 输入模式:树莓派作为“感知者”,接收外部设备的信号。例如,读取按钮是否被按下,或者接收温度传感器传回的数据。

树莓派(以4B为例)拥有40个引脚,其中包括电源引脚(3.3V, 5V, GND)和可编程的GPIO引脚。理解这些引脚的布局是编程的第一步。

1.2 引脚编号的两大流派:BOARD 与 BCM

在开始编写第一行代码之前,必须搞清楚一个核心概念:引脚编号。在Python编程中,主要有两种编号方式,这也是初学者最容易混淆的地方:

  1. BOARD编号
    这种编号方式基于物理位置。无论树莓派的硬件版本如何迭代,物理引脚的位置是固定的。例如,物理上的第11号引脚永远是BOARD 11。这种方式的优点是通用性强,不需要关心芯片内部的编号规则。

  2. BCM编号
    这种编号方式基于Broadcom芯片的通道号。这是芯片原生的引脚名称。例如,物理第11号引脚对应的是BCM 17号通道。这种方式的优点是与官方文档和Linux内核定义一致,但在不同版本的树莓派上可能需要调整代码。

本文策略:为了兼顾底层理解与代码可移植性,我们将在讲解底层原理时使用RPi.GPIO库(支持两种编号),而在高级应用中使用gpiozero库(默认使用BCM编号)。

1.3 开发环境搭建

在编写代码前,我们需要确保树莓派上已安装必要的库。现代树莓派操作系统通常预装了Python 3。

更新系统与安装依赖:
打开终端,执行以下命令:

sudo apt-get update
sudo apt-get upgrade
sudo apt-get install python3-rpi.gpio python3-gpiozero
  • RPi.GPIO:经典、底层、灵活性高,适合需要精细控制的场景。
  • gpiozero:现代、简洁、面向对象,极大降低了入门门槛。

第二章:硬件准备与电路基础

“工欲善其事,必先利其器”。在编写代码前,我们需要搭建物理电路。以下是本项目所需的核心硬件清单。

2.1 硬件清单

  1. 树莓派开发板(推荐4B或3B+)。
  2. 面包板:用于搭建临时电路,免焊接。
  3. 杜邦线:公对母、公对公若干,用于连接引脚。
  4. LED灯(发光二极管):红色、绿色或黄色各若干。
  5. 电阻
    • 220Ω 或 330Ω(用于保护LED)。
    • 10kΩ(用于按钮的下拉/上拉电路,虽然树莓派内部支持,但了解外部原理很重要)。
  6. 按钮开关:四脚微动开关。
  7. 温度传感器:DHT11或DS18B20(本文以DHT11为例,兼顾性价比与编程教学)。

2.2 电路安全须知

树莓派的GPIO引脚逻辑电平为3.3V。切勿将5V电源直接接入GPIO引脚,否则会烧毁芯片! 在连接电路时,建议断电操作,检查无误后再上电。

2.3 欧姆定律与限流电阻

为什么要串联电阻?LED通常有额定的正向电流(约10-20mA)和压降(约1.8V-2.2V)。
假设GPIO输出3.3V,LED压降为2.0V,则电阻两端电压为1.3V。根据欧姆定律 I = V / R I = V / R I=V/R

  • 若不加电阻,电流过大,LED烧毁,GPIO引脚损坏。
  • 若使用330Ω电阻,电流 I = 1.3 V / 330 Ω ≈ 4 m A I = 1.3V / 330Ω \approx 4mA I=1.3V/330Ω4mA,既安全又足够点亮LED。

第三章:项目一——按钮控制LED(基础篇)

我们将通过控制LED来学习GPIO输出,通过读取按钮状态来学习GPIO输入。

3.1 使用RPi.GPIO库实现LED闪烁

这是硬件界的“Hello World”。我们将连接一个LED到BCM 17号引脚(物理引脚11)。

电路连接:

  • GPIO 17 (BCM) -> 电阻 -> LED长脚(正极)。
  • LED短脚(负极) -> GND。

代码实现:

import RPi.GPIO as GPIO
import time

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

# 定义LED引脚
LED_PIN = 17

# 设置引脚为输出模式
GPIO.setup(LED_PIN, GPIO.OUT)

print("LED闪烁程序启动 (按 Ctrl+C 退出)")

try:
    while True:
        GPIO.output(LED_PIN, GPIO.HIGH)  # 点亮LED
        print("LED ON")
        time.sleep(1)                    # 延时1秒

        GPIO.output(LED_PIN, GPIO.LOW)   # 熄灭LED
        print("LED OFF")
        time.sleep(1)

except KeyboardInterrupt:
    print("程序被用户中断")

finally:
    GPIO.cleanup()  # 清理GPIO资源,这是一个良好的编程习惯

代码深度解析:

  • GPIO.setmode(GPIO.BCM):告诉程序我们使用芯片通道号。
  • GPIO.setup(LED_PIN, GPIO.OUT):初始化引脚,必须先设置模式才能操作。
  • GPIO.output(pin, state):控制高低电平。
  • try...finally 结构:这是嵌入式Python编程的标准范式。GPIO.cleanup() 极其重要,如果不调用,程序退出后引脚可能保持高电平,导致下次运行异常或硬件误动作。

3.2 使用gpiozero库简化代码

gpiozero库将硬件封装为对象,代码可读性极强。

from gpiozero import LED
from time import sleep

led = LED(17) # 自动完成setup和cleanup

while True:
    led.on()
    print("LED ON")
    sleep(1)
    led.off()
    print("LED OFF")
    sleep(1)

# gpiozero通常不需要显式调用cleanup,退出时会自动处理

对比可见,gpiozero省去了繁琐的初始化和清理步骤,让开发者专注于逻辑。

3.3 按钮输入与上拉下拉电阻

读取按钮状态比控制LED复杂。按钮是一个开关,连接电路的通断。但问题来了:当按钮未按下时,引脚处于什么状态?
答案是:悬空。悬空引脚极易受到电磁干扰,读数会在0和1之间随机跳变。

为了解决这个问题,我们需要上拉电阻下拉电阻

  • 下拉电阻:将引脚通过电阻连接到GND。未按下时默认为低电平(0),按下时连接VCC,变为高电平(1)。
  • 上拉电阻:将引脚通过电阻连接到VCC。未按下时默认为高电平(1),按下时连接GND,变为低电平(0)。

树莓派芯片内部集成了可配置的上拉/下拉电阻,我们可以通过软件直接启用,无需外接电阻。

电路连接(使用内部上拉电阻):

  • 按钮一端接 GPIO 27 (BCM)。
  • 按钮另一端接 GND。
  • 配置引脚为输入,并开启内部上拉。

代码实现(轮询方式):

import RPi.GPIO as GPIO
import time

GPIO.setmode(GPIO.BCM)
BUTTON_PIN = 27

# 设置为输入模式,pull_up_down=GPIO.PUD_UP 表示启用内部上拉电阻
GPIO.setup(BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)

print("按钮检测程序启动")

try:
    while True:
        # 由于是上拉,默认高电平,按下时接地为低电平
        if GPIO.input(BUTTON_PIN) == GPIO.LOW:
            print("按钮被按下!")
            time.sleep(0.2) # 简单的软件消抖
except KeyboardInterrupt:
    pass
finally:
    GPIO.cleanup()

第四章:项目一——进阶篇(中断与PWM)

轮询方式虽然简单,但在while循环中不断查询状态非常浪费CPU资源。想象一下,你在等快递,轮询就是每隔一分钟就去门口看一眼;而中断则是你在做别的事,快递员按门铃通知你。

4.1 边沿检测与中断

在数字电路中,信号从低变高称为“上升沿”,从高变低称为“下降沿”。中断机制允许我们在检测到特定边沿时触发回调函数。

代码实现(事件检测):

import RPi.GPIO as GPIO
import time

GPIO.setmode(GPIO.BCM)
BUTTON_PIN = 27
LED_PIN = 17

GPIO.setup(LED_PIN, GPIO.OUT)
GPIO.setup(BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)

# 定义一个回调函数
def button_callback(channel):
    print("检测到按钮按下(中断触发)")
    # 切换LED状态
    current_state = GPIO.input(LED_PIN)
    GPIO.output(LED_PIN, not current_state)

# 添加事件检测
# bouncetime=200 设置软件防抖时间为200ms,防止一次按下触发多次中断
GPIO.add_event_detect(BUTTON_PIN, GPIO.FALLING, callback=button_callback, bouncetime=200)

print("程序运行中,请按下按钮...")

try:
    # 主循环可以做其他事情,或者单纯休眠
    while True:
        time.sleep(1) 

except KeyboardInterrupt:
    GPIO.cleanup()

关键技术点:

  • GPIO.FALLING:因为我们使用了上拉电阻,按下按钮时电平从高变低,所以检测下降沿。
  • bouncetime:机械按钮在接触瞬间会产生物理抖动,导致电平在极短时间内多次跳变。这个参数是中断编程的“安全带”。

4.2 PWM脉冲宽度调制:呼吸灯效果

简单的开关太枯燥了?PWM可以控制LED的亮度,甚至做出呼吸灯效果。PWM通过快速切换高低电平,利用人眼的视觉暂留效应,通过改变“占空比”来控制平均输出电压。

  • 频率:每秒切换的次数。
  • 占空比:高电平时间占周期的比例。50%占空比意味着LED亮度约为50%。

代码实现(软件PWM):

import RPi.GPIO as GPIO
import time

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

# 创建PWM实例,频率为1000Hz
pwm = GPIO.PWM(LED_PIN, 1000)

# 启动PWM,初始占空比为0(熄灭)
pwm.start(0)

try:
    while True:
        # 渐亮
        for duty in range(0, 101, 5):
            pwm.ChangeDutyCycle(duty)
            time.sleep(0.05)

        # 渐暗
        for duty in range(100, -1, -5):
            pwm.ChangeDutyCycle(duty)
            time.sleep(0.05)

except KeyboardInterrupt:
    pass
finally:
    pwm.stop() # 停止PWM
    GPIO.cleanup()

第五章:项目二——温度监控系统

现在我们将目光转向传感器。温度监控是智能家居的核心应用之一。我们将使用DHT11传感器,它能同时输出温度和湿度数据。

5.1 DHT11传感器原理

DHT11使用单总线通信协议。它不像I2C或SPI那样有独立的时钟线。主机(树莓派)发送一个启动信号,传感器响应并回传40位数据(湿度整数+小数+温度整数+小数+校验和)。

由于时序要求严格,直接用Python控制GPIO模拟时序虽然可行但效率低且易出错。我们将使用Adafruit开发的Adafruit_DHT库。

安装库:

sudo pip3 install Adafruit_DHT

5.2 电路连接

  • DHT11 VCC -> 3.3V 或 5V
  • DHT11 GND -> GND
  • DHT11 DATA -> GPIO 22 (BCM) (建议串联一个4.7kΩ上拉电阻到VCC,虽然部分模块自带,但外接更稳定)

5.3 代码实现:单次读取

import Adafruit_DHT
import time

# 定义传感器类型和引脚
DHT_SENSOR = Adafruit_DHT.DHT11
DHT_PIN = 22

print("温度监控系统启动...")

try:
    while True:
        # 尝试读取数据
        humidity, temperature = Adafruit_DHT.read_retry(DHT_SENSOR, DHT_PIN)

        if humidity is not None and temperature is not None:
            print(f"温度: {temperature}°C, 湿度: {humidity}%")
        else:
            print("传感器读取失败,请检查连线或重试...")

        # DHT11采样周期约为1秒,不宜频繁读取
        time.sleep(2)

except KeyboardInterrupt:
    print("监控停止")

5.4 扩展:温度报警系统

我们将LED与温度传感器结合。当温度超过阈值时,LED闪烁报警。

import Adafruit_DHT
import RPi.GPIO as GPIO
import time

# 配置
DHT_SENSOR = Adafruit_DHT.DHT11
DHT_PIN = 22
LED_PIN = 17
TEMP_THRESHOLD = 28  # 设定报警温度阈值

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

print(f"温度报警系统启动,阈值: {TEMP_THRESHOLD}°C")

try:
    while True:
        humidity, temperature = Adafruit_DHT.read_retry(DHT_SENSOR, DHT_PIN)

        if temperature is not None:
            print(f"当前温度: {temperature}°C")

            if temperature >= TEMP_THRESHOLD:
                print("警告:温度过高!")
                # 快速闪烁报警
                for _ in range(3):
                    GPIO.output(LED_PIN, True)
                    time.sleep(0.2)
                    GPIO.output(LED_PIN, False)
                    time.sleep(0.2)
            else:
                GPIO.output(LED_PIN, False) # 确保灯灭
        else:
            print("读取失败")

        time.sleep(1)

except KeyboardInterrupt:
    pass
finally:
    GPIO.cleanup()

第六章:进阶核心——多线程编程

在实际项目中,我们往往需要同时做多件事。例如,在监控温度的同时,还要实时响应按钮的按下,或者驱动显示屏。如果温度读取使用阻塞延时,按钮的响应就会变得迟钝。这就需要引入多线程。

6.1 Python threading 模块基础

Python提供了threading模块,允许程序创建子线程并行执行任务。

  • 主线程:程序启动时默认的线程。
  • 子线程:在主线程中创建的并发执行流。

6.2 场景:多任务并发系统

需求

  1. 任务一:每2秒读取一次温度并打印。
  2. 任务二:实时监测按钮,按下按钮点亮LED,松开熄灭(或切换状态)。
  3. 两个任务互不干扰。

代码实现:

import RPi.GPIO as GPIO
import Adafruit_DHT
import time
import threading

# --- 硬件配置 ---
LED_PIN = 17
BUTTON_PIN = 27
DHT_PIN = 22
DHT_SENSOR = Adafruit_DHT.DHT11

# --- GPIO初始化 ---
GPIO.setmode(GPIO.BCM)
GPIO.setup(LED_PIN, GPIO.OUT)
GPIO.setup(BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)

# --- 全局变量(线程间通信) ---
running = True # 用于控制线程退出的标志

# --- 线程函数1:温度监控 ---
def temperature_monitor():
    print("温度监控线程启动")
    while running:
        humidity, temperature = Adafruit_DHT.read_retry(DHT_SENSOR, DHT_PIN)
        if temperature is not None:
            print(f"[线程1] 温度: {temperature}°C, 湿度: {humidity}%")
        else:
            print("[线程1] 传感器读取错误")
        time.sleep(2) # 阻塞只影响当前线程
    print("温度监控线程退出")

# --- 线程函数2:按钮响应 ---
def button_monitor():
    print("按钮监控线程启动")
    # 使用中断方式在子线程中也是可行的,但这里演示线程内的轮询逻辑
    # 为了演示多线程轮询,我们使用较短的sleep
    while running:
        if GPIO.input(BUTTON_PIN) == GPIO.LOW:
            GPIO.output(LED_PIN, GPIO.HIGH)
        else:
            GPIO.output(LED_PIN, GPIO.LOW)
        time.sleep(0.05) # 极短延时,保证响应速度
    print("按钮监控线程退出")

# --- 主程序 ---
try:
    # 创建线程对象
    t_temp = threading.Thread(target=temperature_monitor)
    t_btn = threading.Thread(target=button_monitor)

    # 启动线程
    t_temp.start()
    t_btn.start()

    print("主线程:系统运行中...按Ctrl+C退出")

    # 主线程保持运行,等待子线程(实际上子线程是死循环,需通过标志位退出)
    while True:
        time.sleep(1)

except KeyboardInterrupt:
    print("\n主线程:正在停止系统...")
    running = False # 修改标志位,通知子线程退出
    # 等待子线程安全退出(join方法会阻塞直到线程结束)
    t_temp.join()
    t_btn.join()
    print("所有线程已停止")

finally:
    GPIO.cleanup()

6.3 多线程编程的陷阱与最佳实践

  1. 资源竞争:如果两个线程同时操作同一个GPIO引脚(例如一个线程开灯,一个线程关灯),可能会产生冲突。在复杂系统中,应使用threading.Lock()来确保同一时间只有一个线程操作特定资源。
  2. 全局解释器锁(GIL):Python的多线程在CPU密集型任务中无法利用多核优势,但在IO密集型任务(如传感器读取、网络请求、GPIO等待)中效果显著。我们的GPIO项目属于典型的IO密集型,多线程非常适用。
  3. 优雅退出:使用running标志位控制循环退出是标准做法。切勿直接强制结束进程,可能导致GPIO状态未清理。

第七章:gpiozero库的高级应用与封装

虽然RPi.GPIO让我们理解了底层,但gpiozero提供的抽象层级更高,能极大简化多线程和复合设备的编程。

7.1 使用gpiozero构建复合设备

gpiozero内置了许多“合成”类。例如,我们可以用Button控制LED,甚至不需要写任何循环逻辑。

代码示例:按钮控制LED(面向对象版)

from gpiozero import LED, Button
from signal import pause

led = LED(17)
button = Button(27) # Button类默认处理了上拉电阻和消抖

# 当按钮按下时,执行led.on
button.when_pressed = led.on
# 当按钮松开时,执行led.off
button.when_released = led.off

print("程序运行中...")

# pause()让程序保持运行,等待事件触发(内部自动处理循环)
# gpiozero在后台自动管理线程,无需手动创建
pause()

7.2 gpiozero实现温度监控与阈值控制

虽然gpiozero主要针对GPIO,但它也支持一些常用传感器。对于DHT11,我们可以结合底层库和gpiozero的输出设备。

from gpiozero import LED
import Adafruit_DHT
import time

led = LED(17)
sensor = Adafruit_DHT.DHT11
pin = 22

while True:
    h, t = Adafruit_DHT.read_retry(sensor, pin)
    if t is not None:
        print(f"Temp: {t}")
        if t > 28:
            led.blink(on_time=0.1, off_time=0.1) # 非阻塞闪烁
        else:
            led.off()
    time.sleep(1)

7.3 为什么还要学RPi.GPIO?

既然gpiozero这么方便,为什么还要花大篇幅讲解RPi.GPIO

  1. 底层控制权:在需要精确时序控制(如驱动非标准协议传感器、红外发射)时,RPi.GPIO的底层操作更灵活。
  2. 兼容性:许多老旧的开源项目使用的是RPi.GPIO
  3. 原理理解:学习RPi.GPIO能让你深刻理解中断、上拉电阻、PWM生成原理,这对排查硬件故障至关重要。

第八章:故障排查与工程经验

在GPIO编程中,代码报错只是冰山一角,更多的问题来自硬件和物理环境。

8.1 常见问题与解决方案

问题1:引脚编号搞混

  • 现象:代码逻辑正确,但硬件没反应。
  • 排查:拿出万用表,测量引脚电压。确认代码中setmodeBCM还是BOARD。建议在面包板旁贴一张标签纸,写明当前使用的编号体系。

问题2:引脚未清理

  • 现象:程序运行一次正常,第二次运行报错或LED状态异常。
  • 原因:上次程序异常退出,未执行cleanup(),引脚保持上次的输出状态。
  • 解决:养成try...finally习惯。或者在程序开始时强制执行一次GPIO.cleanup()(注意这会影响其他正在运行的进程)。

问题3:传感器读数失败或全是0

  • 现象:DHT11读取超时。
  • 原因:接线松动、上拉电阻缺失、读取频率过快。
  • 解决:检查杜邦线连接(面包板接触不良是常见问题)。DHT11两次读取间隔至少1秒。

问题4:按钮抖动

  • 现象:按一下按钮,LED闪烁多次,或计数器增加多次。
  • 解决
    • 软件:bouncetime参数或time.sleep()延时。
    • 硬件:在按钮两端并联一个0.1uF的电容,实现硬件消抖。

8.2 性能优化建议

  1. 减少CPU占用:在while True循环中,务必加入time.sleep()。哪怕是time.sleep(0.01),也能让CPU喘口气,避免程序占满100%资源导致系统卡顿。
  2. 使用中断代替轮询:对于实时性要求高的输入信号,永远优先使用add_event_detect,它能释放主线程去处理其他逻辑。
  3. 权限问题:如果使用Python 3直接运行脚本提示Operation not permitted,可能需要sudo权限来访问底层内存映射。不过现代树莓派OS通过gpio用户组机制,通常普通用户即可运行GPIO程序。

第九章:总结与展望

通过这篇长文,我们从最基础的GPIO概念出发,经历了点亮LED、处理按钮抖动、理解中断机制、编写多线程并发程序,最终构建了一个完整的温度监控系统。这不仅是代码的堆砌,更是从软件思维向嵌入式硬件思维的转变。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐