树莓派GPIO控制:Shell脚本操作Sysfs接口实战指南
1. 从点亮一个LED开始:为什么Shell脚本是GPIO控制的首选
如果你刚拿到树莓派,想用它来控制一个LED灯闪烁,或者读取一个按钮的状态,你可能会立刻想到去学Python。这没错,Python的RPi.GPIO库确实强大且易用。但今天我想分享一个更底层、更直接、也常常被新手忽略的路径:使用Shell脚本控制GPIO。
为什么是Shell脚本?想象一下,你需要在系统启动时自动执行一个简单的硬件初始化,比如打开一个继电器;或者你想在远程SSH登录后,快速测试一下某个引脚的电平,而不想打开一个Python编辑器。在这些场景下,写一个几行的Shell脚本,用echo和cat命令直接操作/sys/class/gpio下的文件,速度之快、依赖之少,会让你感到惊喜。它绕过了任何额外的库和解释器,直接与Linux内核提供的GPIO Sysfs接口对话。这对于理解Linux下“一切皆文件”的哲学,以及构建轻量级、高可靠性的自动化脚本(比如结合cron定时任务)来说,是绝佳的学习和实践切入点。
在本文中,我将带你从零开始,通过几个具体的例子,手把手掌握用Shell脚本操控树莓派GPIO的完整流程。我们会从最基础的导出引脚、设置方向,到实现LED闪烁、按钮读取,最后封装成可复用的函数,并探讨在实际项目中如何避免常见陷阱。无论你是想深化对Linux系统的理解,还是追求极致的脚本自动化效率,这篇内容都将为你提供可直接“抄作业”的实战指南。
2. 理解基石:GPIO Sysfs接口的工作机制
在动手写脚本之前,我们必须先搞明白树莓派(或者说,大多数Linux系统)是如何通过文件系统来管理GPIO的。这个机制的核心就是Sysfs(System File System)。它不是磁盘上的普通文件夹,而是内核提供的一个虚拟文件系统,挂载在/sys目录下,用于以文件的形式展示内核对象(如设备、驱动、模块)的状态和配置,并允许用户空间程序通过读写这些文件来与内核交互。
对于GPIO,相关的接口就位于/sys/class/gpio。这个目录的结构和操作逻辑,是我们所有脚本的基础。
2.1 Sysfs GPIO目录结构解析
当你进入/sys/class/gpio,通常会看到以下内容:
ls -l /sys/class/gpio/输出可能包含:
export unexport gpiochip0 gpiochip504 ...export和unexport:这是两个只写(write-only)文件。它们是控制GPIO引脚“使用权”的开关。你想使用某个GPIO引脚前,必须向export文件写入该引脚的编号,内核才会在/sys/class/gpio下为你创建对应的控制目录。用完后再向unexport写入编号,内核就会删除那个目录,释放资源。gpiochipX:这些目录代表了SoC上不同的GPIO控制器(chip)。树莓派通常有多个GPIO控制器,gpiochip0通常对应着主板40针排针上的那些通用GPIO。每个gpiochipX目录下有base、ngpio、label等文件,告诉你这个控制器管理的GPIO编号范围。对我们而言,最重要的是GPIO的全局编号。
注意:引脚编号的“坑”:这里最容易混淆。树莓派有至少三种编号方式:物理引脚号(BOARD编号)、BCM编号(GPIO编号)、WiringPi编号。Sysfs接口使用的是BCM编号。例如,物理引脚第11脚(GPIO17),在Sysfs中你需要操作的就是
gpio17。务必对照树莓派GPIO引脚图来确认,写错编号是导致操作无效的最常见原因。
2.2 一个GPIO引脚的控制文件
当你成功导出(例如导出GPIO17)后,/sys/class/gpio下会出现一个gpio17的目录。进去看看:
ls -l /sys/class/gpio/gpio17/你会看到类似以下文件:
direction value edge active_low ...direction:控制引脚方向。写入in设置为输入模式,写入out设置为输出模式。读取它可以获取当前方向。value:这是核心文件。在输出模式下,写入1或0来设置引脚高电平或低电平;在输入模式下,读取它(cat命令)来获取当前引脚的电平状态(1为高,0为低)。edge:用于配置中断触发边沿,仅对输入模式有效。可以写入none(默认,无中断)、rising(上升沿)、falling(下降沿)或both(双边沿)。配置后,可以通过poll()或select()系统调用在用户空间监听中断,但在Shell脚本中更常用的方式是轮询value文件。active_low:这是一个有趣的反相器。写入1可以反转value的逻辑:此时在输出模式下,写入1会使引脚实际输出低电平,写入0输出高电平;在输入模式下亦然。这在驱动共阳极LED或处理低电平有效的传感器时非常有用。
理解这些文件的作用后,用Shell脚本控制GPIO的本质就变得极其简单:用echo命令向特定文件写入字符串,用cat命令从特定文件读取字符串。接下来,我们就用这些命令来点亮第一个LED。
3. 实战第一步:用Shell脚本让LED闪烁起来
理论清楚了,我们开始真正的硬件操作。你需要准备:一个树莓派(任何型号均可)、一个LED、一个220Ω-1kΩ的电阻(用于限流,保护LED和GPIO引脚)、若干杜邦线。
将LED的长脚(阳极)通过电阻连接到树莓派的GPIO17(物理引脚11),短脚(阴极)连接到树莓派的GND(例如物理引脚9)。接线务必确认无误,反接LED不会亮,但一般也不会损坏。
3.1 基础脚本:手动操作点亮与熄灭
我们先写一个最直接的脚本,一步步操作,理解每个命令的意义。创建一个名为led_blink_basic.sh的文件:
#!/bin/bash # 定义要使用的GPIO引脚编号(BCM编号) GPIO=17 # 1. 导出GPIO引脚,获取控制权 echo "$GPIO" > /sys/class/gpio/export # 执行后,检查是否成功创建了目录 ls -d /sys/class/gpio/gpio$GPIO 2>/dev/null && echo "GPIO$GPIO导出成功" || echo "导出失败,请检查编号或权限" # 稍作延时,等待内核完成目录创建 sleep 0.1 # 2. 设置引脚方向为输出 echo "out" > /sys/class/gpio/gpio$GPIO/direction # 3. 点亮LED(设置高电平) echo "1" > /sys/class/gpio/gpio$GPIO/value echo "LED 点亮" sleep 2 # 保持亮2秒 # 4. 熄灭LED(设置低电平) echo "0" > /sys/class/gpio/gpio$GPIO/value echo "LED 熄灭" sleep 2 # 5. 清理:取消导出GPIO引脚,释放资源 echo "$GPIO" > /sys/class/gpio/unexport echo "GPIO$GPIO 已释放"给脚本添加执行权限并运行:
chmod +x led_blink_basic.sh sudo ./led_blink_basic.sh注意:为什么需要sudo?因为
/sys/class/gpio目录下的文件通常属于root用户和gpio组。默认情况下,普通用户没有写入权限。你可以选择每次都加sudo运行脚本,或者一劳永逸地将你的用户加入gpio组:sudo usermod -a -G gpio $USER,然后注销并重新登录生效。之后运行脚本就不需要sudo了。本文后续示例假设你已进行此配置。
如果一切正常,你将看到LED点亮2秒,然后熄灭2秒,最后脚本退出。恭喜你,完成了Shell控制GPIO的第一次交互!
3.2 进阶脚本:实现自动化闪烁与优雅退出
上面的脚本只能执行一次。一个更实用的场景是让LED持续闪烁,直到我们按下Ctrl+C中断。同时,我们需要确保无论脚本如何退出(正常结束或被中断),都能正确地清理GPIO资源,避免引脚被占用导致下次无法使用。这涉及到信号捕获(trap)。
创建一个更健壮的led_blink_advanced.sh:
#!/bin/bash GPIO=17 # 设置闪烁间隔时间(秒) INTERVAL=0.5 # 定义一个清理函数,用于捕获退出信号时执行 cleanup() { echo -e "\n检测到中断,正在清理..." echo "0" > /sys/class/gpio/gpio$GPIO/value # 先熄灭LED echo "$GPIO" > /sys/class/gpio/unexport # 再取消导出 echo "GPIO$GPIO 资源已释放。" exit 0 } # 捕获 SIGINT (Ctrl+C) 和 SIGTERM 信号,并执行清理函数 trap cleanup SIGINT SIGTERM # 导出并设置GPIO echo "$GPIO" > /sys/class/gpio/export sleep 0.1 echo "out" > /sys/class/gpio/gpio$GPIO/direction echo "LED闪烁开始 (按 Ctrl+C 停止)..." # 使用循环实现持续闪烁 while true; do echo "1" > /sys/class/gpio/gpio$GPIO/value sleep $INTERVAL echo "0" > /sys/class/gpio/gpio$GPIO/value sleep $INTERVAL done # 理论上循环不会结束,所以这里的清理是冗余的,但保持结构完整 cleanup这个脚本的精华在于trap cleanup SIGINT SIGTERM这一行。它告诉系统:当脚本收到SIGINT(通常是Ctrl+C)或SIGTERM(终止信号)时,不要立即退出,而是先去执行cleanup函数。在函数里,我们确保了LED被熄灭,并且GPIO资源被正确释放。这是一个非常好的编程实践,能避免留下“僵尸”GPIO状态。
运行这个脚本,LED会以0.5秒的间隔持续闪烁,直到你按下Ctrl+C,它会优雅地熄灭LED并退出。
4. 读取输入:用Shell脚本检测按钮状态
控制输出只是GPIO的一半功能,另一半是读取输入,比如连接一个按钮。我们准备一个常开按钮,一端连接GPIO27(物理引脚13),另一端连接GND。同时,我们需要启用树莓派内部的上拉电阻,这样当按钮未按下时,GPIO引脚通过上拉电阻连接到3.3V,读取为高电平(1);当按钮按下时,引脚直接连接到GND,读取为低电平(0)。
4.1 配置上拉电阻与轮询检测
在Sysfs接口中,上拉/下拉电阻的配置在树莓派上比较特殊。早期的Sysfs标准并未直接提供pullup/pulldown文件。对于树莓派,更可靠的方法是在设置方向为in时,通过direction文件的特殊语法来同时配置上拉。但请注意,这个特性可能因内核版本和树莓派型号而异。最通用且可靠的方法是:先设置为输出并输出高电平(1)来使能上拉,然后再设置为输入。这是因为当引脚从输出模式(输出高)切换到输入模式时,内部的上拉电阻可能仍然保持有效状态(取决于硬件设计)。但这种方法并非百分百标准。
另一种更清晰的方法是使用设备树(Device Tree)重叠(Overlay)或像raspi-gpio这样的专用工具预先配置。但对于简单的Shell脚本,我们可以采用一种实践有效的“先输出高再转输入”的技巧,或者直接依赖物理上拉电阻。
这里我们写一个脚本,假设使用内部上拉(通过先输出高实现),并采用轮询方式检测按钮是否被按下。
#!/bin/bash BUTTON_GPIO=27 # 清理函数 cleanup() { echo -e "\n清理中..." echo "$BUTTON_GPIO" > /sys/class/gpio/unexport 2>/dev/null exit 0 } trap cleanup SIGINT SIGTERM # 导出GPIO echo "$BUTTON_GPIO" > /sys/class/gpio/export sleep 0.2 # 方法:尝试通过先设置为输出高电平来启用内部上拉 echo "out" > /sys/class/gpio/gpio$BUTTON_GPIO/direction echo "1" > /sys/class/gpio/gpio$BUTTON_GPIO/value sleep 0.1 # 然后设置为输入模式 echo "in" > /sys/class/gpio/gpio$BUTTON_GPIO/direction echo "按钮检测开始 (按 Ctrl+C 退出)。未按下时应为高电平(1)。" echo "按下按钮时,引脚接地,应读到低电平(0)。" prev_value="2" # 初始化为一个不可能的值,用于检测第一次变化 while true; do # 读取当前电平值。注意:`cat`命令会带一个换行符,用`tr -d '\n'`去掉 current_value=$(cat /sys/class/gpio/gpio$BUTTON_GPIO/value | tr -d '\n') if [ "$current_value" != "$prev_value" ]; then timestamp=$(date '+%Y-%m-%d %H:%M:%S') if [ "$current_value" = "0" ]; then echo "[$timestamp] 按钮按下 (电平: 0)" else echo "[$timestamp] 按钮释放 (电平: 1)" fi prev_value="$current_value" fi sleep 0.05 # 轮询间隔50ms,避免过高CPU占用 done这个脚本演示了几个关键点:
- 上拉配置的变通方法:通过先
out再写1,最后切回in,尝试激活内部上拉。 - 轮询(Polling):在
while循环中不断读取value文件的值。这是Shell脚本中最简单的输入检测方式,但会持续占用CPU。对于实时性要求不高的场景(如检测按钮)是可以接受的。 - 状态变化检测:通过比较当前值和前一个值,只在状态发生变化时打印信息,避免刷屏。
- 去除换行符:
cat读取文件会包含结尾的换行符\n,直接用[ "$current_value" = "0" ]比较会失败,因为变量实际是"0\n"。使用tr -d '\n'或命令替换$(< file)可以解决。
运行脚本,按下和松开按钮,你应该能在终端看到对应的按下/释放消息。
4.2 使用中断与poll命令的进阶思路
轮询简单但低效。对于需要快速响应或低功耗的场景,更好的方式是使用中断。Sysfs的edge文件就是为此设计的。你可以通过Shell配合poll命令来监听文件变化事件。
但请注意,标准的poll命令可能不存在,我们可以使用inotifywait(来自inotify-tools包)来模拟类似效果,但它监听的是文件本身的修改事件,而非严格的内核中断。一个更接近硬件中断的方法是使用epoll或select的系统调用,但这在纯Bash中实现复杂,通常需要借助其他语言。
这里给出一个使用inotifywait监听value文件变化的示例思路,它比纯轮询更高效,因为只在文件内容改变时才会唤醒进程:
#!/bin/bash # 需要安装 inotify-tools: sudo apt install inotify-tools BUTTON_GPIO=27 echo "$BUTTON_GPIO" > /sys/class/gpio/export sleep 0.1 echo "in" > /sys/class/gpio/gpio$BUTTON_GPIO/direction echo "both" > /sys/class/gpio/gpio$BUTTON_GPIO/edge # 设置双边沿触发 VALUE_FILE="/sys/class/gpio/gpio$BUTTON_GPIO/value" echo "监听按钮事件 (双边沿触发)..." # 使用 inotifywait 持续监控文件内容修改事件 inotifywait -m -e modify "$VALUE_FILE" | while read -r event_info; do # 当文件被修改时,读取其当前值 current_val=$(cat "$VALUE_FILE" | tr -d '\n') timestamp=$(date '+%H:%M:%S') if [ "$current_val" = "0" ]; then echo "[$timestamp] 按钮按下" else echo "[$timestamp] 按钮释放" fi done # 清理 echo "$BUTTON_GPIO" > /sys/class/gpio/unexport这个脚本更优雅,CPU占用率极低。但它依赖于inotify-tools,且inotify是文件系统事件,与硬件中断在时序精度上仍有差异。对于绝大多数非严苛实时应用,这已经是一个非常好的解决方案。
5. 工程化实践:编写可复用的GPIO Shell函数库
当你的项目涉及多个GPIO引脚时,在每个脚本里重复写导出、设置方向、清理的代码会非常冗余且容易出错。一个好的实践是将这些通用操作封装成函数,放在一个单独的库文件中(例如gpio_lib.sh),然后在主脚本中引用(source)。
5.1 创建GPIO函数库
创建文件gpio_lib.sh:
#!/bin/bash # gpio_lib.sh - 树莓派 GPIO Shell 函数库 # 全局变量,记录已导出的GPIO,用于批量清理 declare -a EXPORTED_GPIOS=() # 函数: gpio_export # 参数: $1 - GPIO引脚编号 (BCM) # 功能: 导出GPIO引脚,并记录到数组 gpio_export() { local gpio=$1 if [ ! -d "/sys/class/gpio/gpio$gpio" ]; then echo "$gpio" > /sys/class/gpio/export 2>/dev/null # 检查是否导出成功 sleep 0.05 if [ -d "/sys/class/gpio/gpio$gpio" ]; then EXPORTED_GPIOS+=("$gpio") echo "已导出 GPIO$gpio" else echo "错误: 无法导出 GPIO$gpio (编号错误或无权限?)" >&2 return 1 fi else echo "警告: GPIO$gpio 已被导出,跳过。" >&2 fi } # 函数: gpio_unexport # 参数: $1 - GPIO引脚编号 (BCM) # 功能: 取消导出单个GPIO引脚,并从数组中移除 gpio_unexport() { local gpio=$1 if [ -d "/sys/class/gpio/gpio$gpio" ]; then echo "$gpio" > /sys/class/gpio/unexport 2>/dev/null # 从数组中删除该元素 for i in "${!EXPORTED_GPIOS[@]}"; do if [[ "${EXPORTED_GPIOS[$i]}" = "$gpio" ]]; then unset 'EXPORTED_GPIOS[$i]' break fi done echo "已释放 GPIO$gpio" fi } # 函数: gpio_set_direction # 参数: $1 - GPIO引脚编号, $2 - 方向 ("in" 或 "out") # 功能: 设置GPIO方向 gpio_set_direction() { local gpio=$1 local dir=$2 if [[ "$dir" != "in" && "$dir" != "out" ]]; then echo "错误: 方向必须是 'in' 或 'out'" >&2 return 1 fi echo "$dir" > "/sys/class/gpio/gpio$gpio/direction" 2>/dev/null && echo "GPIO$gpio 方向设置为: $dir" || echo "设置方向失败" >&2 } # 函数: gpio_set_value # 参数: $1 - GPIO引脚编号, $2 - 值 (0 或 1) # 功能: 设置输出值 (仅当方向为out时有效) gpio_set_value() { local gpio=$1 local val=$2 echo "$val" > "/sys/class/gpio/gpio$gpio/value" 2>/dev/null && echo "GPIO$gpio 输出值设置为: $val" || echo "设置输出值失败" >&2 } # 函数: gpio_get_value # 参数: $1 - GPIO引脚编号 # 返回值: 打印引脚的当前值 (0 或 1) gpio_get_value() { local gpio=$1 cat "/sys/class/gpio/gpio$gpio/value" 2>/dev/null | tr -d '\n' } # 函数: gpio_cleanup_all # 参数: 无 # 功能: 清理所有通过本库导出的GPIO引脚 gpio_cleanup_all() { echo "正在清理所有GPIO资源..." # 倒序清理,避免可能的依赖问题(虽然GPIO通常没有) for (( idx=${#EXPORTED_GPIOS[@]}-1 ; idx>=0 ; idx-- )) ; do gpio="${EXPORTED_GPIOS[idx]}" gpio_set_value "$gpio" 0 2>/dev/null # 尝试先设为低电平 gpio_unexport "$gpio" done echo "所有GPIO资源已释放。" } # 注册退出时的清理函数 trap gpio_cleanup_all EXIT这个库提供了完整的GPIO操作封装:
gpio_export/gpio_unexport: 安全地导出和释放GPIO。gpio_set_direction/gpio_set_value/gpio_get_value: 核心控制函数。gpio_cleanup_all: 最重要的函数,它会在脚本退出时(无论是正常退出还是被信号中断)自动调用,清理所有通过本库导出的GPIO。这通过trap gpio_cleanup_all EXIT实现,确保了资源永不泄漏。- 使用数组
EXPORTED_GPIOS跟踪所有导出的引脚,便于批量管理。
5.2 使用函数库重写LED闪烁脚本
现在,主脚本变得非常简洁和健壮。创建led_blink_with_lib.sh:
#!/bin/bash # 引入GPIO函数库 source ./gpio_lib.sh LED_GPIO=17 INTERVAL=0.3 echo "=== 使用GPIO库控制LED闪烁 ===" # 使用库函数操作 gpio_export $LED_GPIO gpio_set_direction $LED_GPIO "out" echo "开始闪烁,按 Ctrl+C 停止..." while true; do gpio_set_value $LED_GPIO 1 sleep $INTERVAL gpio_set_value $LED_GPIO 0 sleep $INTERVAL done # 注意:不需要显式调用清理函数,trap会在脚本退出时自动处理运行这个脚本,你会发现逻辑清晰多了,而且完全不用担心忘记清理GPIO。即使脚本因为错误中途退出,trap机制也会保证gpio_cleanup_all被执行。
6. 避坑指南与性能考量:Shell脚本控制GPIO的局限性
虽然Shell脚本控制GPIO简单直接,但在实际项目中,你必须清楚它的边界和潜在问题,才能做出合适的技术选型。
6.1 常见“坑点”与解决方案
权限问题:如前所述,操作
/sys/class/gpio需要root或gpio组成员的权限。解决方案:将用户加入gpio组是最佳实践。检查命令:groups $USER,添加命令:sudo usermod -a -G gpio $USER(需重新登录)。GPIO编号混淆:这是最大的新手陷阱。物理引脚号、BCM编号、WiringPi编号傻傻分不清。解决方案:永远使用BCM编号对应Sysfs。手边常备一张树莓派GPIO引脚图(标注了BCM编号),或者使用
pinout命令(需安装raspi-utils)在终端查看。引脚复用冲突:树莓派的某些GPIO引脚有复用功能(如I2C、SPI、UART)。如果这些接口已在
/boot/config.txt中被启用,那么对应的GPIO引脚可能无法通过Sysfs正常操作。解决方案:检查/boot/config.txt,注释掉或禁用你不使用的硬件接口(如dtparam=i2c_arm=on,dtparam=spi=on等),然后重启。或者,选择没有复用的普通GPIO引脚。导出失败:向
export文件写入编号后,没有出现对应的gpioX目录。可能原因:- 编号错误(使用了物理引脚号)。
- 该引脚是电源或地线(如3.3V, 5V, GND),根本不是GPIO。
- 该引脚已被内核或其他进程占用。解决方案:仔细核对BCM编号;使用
raspi-gpio get命令查看所有GPIO的当前状态和功能。
响应速度与CPU占用:Shell脚本的循环和文件读写(
echo,cat)开销较大,速度慢。实测数据:在一个树莓派4B上,简单的while循环配合echo和sleep,最高翻转频率很难超过几百Hz,且一个核心的CPU占用率可能接近100%。解决方案:对于需要高速或精确时序的控制(如PWM、精确脉冲生成),Shell脚本完全不适用,应选择Python(使用RPi.GPIO或gpiozero库的底层C扩展)、C语言直接操作寄存器,或使用硬件PWM/定时器。并发与竞争条件:如果多个脚本或进程同时操作同一个GPIO引脚,会产生不可预知的结果。解决方案:通过文件锁(
flock命令)或设计良好的单进程控制来管理GPIO资源访问。
6.2 Shell脚本 vs. Python/C 语言控制GPIO
如何选择?这里有一个简单的决策流:
选择Shell脚本,如果:
- 需求非常简单(开关控制、状态读取)。
- 希望脚本轻量,无需安装额外依赖(Python库)。
- 需要与大量其他Shell命令、系统服务(如cron, systemd)无缝集成。
- 用于快速原型验证或一次性任务。
- 追求极致的“Linux原生”体验,用于教学理解Sysfs。
选择Python(RPi.GPIO/gpiozero),如果:
- 需要中等速度的控制(kHz级别)。
- 需要产生软件PWM。
- 项目逻辑复杂,需要高级数据结构、函数库、网络通信等。
- 开发者更熟悉Python生态。
选择C语言(wiringPi/libbcm2835),如果:
- 需要最高性能(MHz级别的翻转)和最低延迟。
- 需要精确的硬件定时或中断处理。
- 项目是长期运行、对稳定性要求极高的嵌入式应用。
6.3 一个综合案例:简易安防报警器
让我们用一个综合案例结束本文。假设我们想用树莓派做一个简易门磁报警器:一个常闭型干簧管(门磁传感器)接在GPIO23和GND之间(门关闭时电路导通,GPIO被拉低;门打开时电路断开,GPIO被内部上拉拉高)。当门被打开(GPIO变高)时,触发一个蜂鸣器(接GPIO24)响,同时点亮一个红色LED(接GPIO25)作为视觉警报。
脚本door_alarm.sh:
#!/bin/bash source ./gpio_lib.sh SENSOR_GPIO=23 # 门磁传感器,输入,内部上拉 BUZZER_GPIO=24 # 蜂鸣器,输出 LED_ALARM_GPIO=25 # 报警LED,输出 ALARM_ACTIVE=false # 初始化 gpio_export $SENSOR_GPIO gpio_set_direction $SENSOR_GPIO "in" # 尝试配置上拉 (先输出高,再切输入) gpio_set_direction $SENSOR_GPIO "out" gpio_set_value $SENSOR_GPIO 1 sleep 0.05 gpio_set_direction $SENSOR_GPIO "in" gpio_export $BUZZER_GPIO gpio_set_direction $BUZZER_GPIO "out" gpio_set_value $BUZZER_GPIO 0 # 初始关闭蜂鸣器 gpio_export $LED_ALARM_GPIO gpio_set_direction $LED_ALARM_GPIO "out" gpio_set_value $LED_ALARM_GPIO 0 # 初始关闭LED echo "简易门磁报警系统启动。门关闭时传感器值应为0。" echo "等待门被打开..." # 主监控循环 while true; do sensor_state=$(gpio_get_value $SENSOR_GPIO) if [ "$sensor_state" = "1" ]; then # 门被打开(传感器断开,上拉至高电平) if [ "$ALARM_ACTIVE" = false ]; then echo "$(date '+%H:%M:%S') - 警报!门被打开!" ALARM_ACTIVE=true # 启动声光报警(蜂鸣器间歇鸣叫) gpio_set_value $LED_ALARM_GPIO 1 fi # 蜂鸣器发出“嘀嘀”声 gpio_set_value $BUZZER_GPIO 1 sleep 0.2 gpio_set_value $BUZZER_GPIO 0 sleep 0.2 else # 门关闭 if [ "$ALARM_ACTIVE" = true ]; then echo "$(date '+%H:%M:%S') - 警报解除。门已关闭。" ALARM_ACTIVE=false gpio_set_value $LED_ALARM_GPIO 0 gpio_set_value $BUZZER_GPIO 0 fi sleep 0.5 # 门关闭时降低检测频率 fi done # trap 会自动清理所有GPIO这个脚本融合了输入检测、输出控制、状态管理和简单的用户交互,是一个完整的微型项目。它展示了如何用Shell脚本构建一个具备实用功能的硬件交互系统。当然,你可以在此基础上扩展,比如增加日志记录、通过网络发送通知等。
通过这一系列从基础到进阶的探索,你应该已经掌握了使用Shell脚本驾驭树莓派GPIO的全套技能。记住,工具没有绝对的好坏,只有是否适合场景。Shell脚本在自动化、系统集成和快速验证方面的独特优势,使其在树莓派玩家的工具箱中始终占有一席之地。下次当你需要快速让硬件动起来时,不妨先打开终端,试试这几行echo和cat命令,感受一下最接近硬件的控制乐趣。