用树莓派 Pico 编一个拼手速游戏

微控制器(MCU)不仅存在于工业设备中,它们为包括玩具和游戏在内的许多家庭电子产品 供算力。在这一章中,你将创建一个简单的反应计时游戏,看看你的朋友中谁会在灯熄灭时第一个按下按钮。

你的反应时间,大脑来处理的时间以毫秒计:人类的平均反应时间大约是 200 – 250 毫秒。

对于这个项目,你需要:
– 树莓派 Pico
– 面包板
– 任何颜色的 LED 灯
– 一个 330Ω 电阻
– 两个按钮开关
– 若干公对公跳线
– 一根 microUSB 数据线

将 Pico 连接到树莓派或其他运行 Thonny MicroPython IDE 的计算机。

单人游戏

如图所示在面包板上搭建电路。LED 和 330Ω 限流电阻串联在 Pico 的 GP15 和 GND 引脚之间。

接下来,添加按钮开关。将按钮一侧的引脚连接到 Pico 的 GP14,另一侧的引脚接到 Pico 的 3V3 引脚上。

为什么要连接 3v3?请记住,开关和 LED 一样,需要电阻器才能正确工作,而且
Pico 上 GPIO 是有可编程电阻器的。在这本文的项目中,我们将它们设置为下拉电阻,这意味着当按钮按下时,引脚电压必须被拉高。

现在你的电路已经具备了作为一个单人游戏所需要的一切,LED 是输出设备(类似电视机的作用),按钮开关为控制器,而 Pico 是游戏主机,尽管它比你通常看到的要小得多!

现在你需要真正编写游戏。和往常一样,把树莓派上的 Thonny 打开。创建一个新程序:

import machine
import utime

此外,你将需要一个新的库:urandom,它用来创建随机数并在这个游戏中使用,以防止曾经玩过它的玩家简单地倒数一个固定的秒数点击按钮而一招制胜。

接下来,设置好 LED 和按钮的引脚:

led = machine.Pin(15, machine.Pin.OUT)
button = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_DOWN)

在前面的章节中,你已经了解如何在主程序或单独的线程中使用按钮。这一次我们将采 用一种不同的、更灵活的方法:中断请求(IRQs)来处理按钮的反馈。

这个名字听起来很复杂,但其实很简单。想象一下,你正在一页一页地阅读一本书,有人走过来问你一个问题。那个人在执行一个打断请求,要求你停止正在做的事情,回答他们的问题,然后让你继续读你的书。

MicroPython 中断请求以完全相同的方式工作,它允许某些东西(在这种情况下是按下按钮 开关)中断主程序。在某些方面,它和线程很相似,在主程序之外有一段代码。然而,与线程不 同的是,代码不是持续运行的,它只在中断被触发时运行。

首先定义中断的处理程序。这个被称为回调函数的代码在中断被触发时运行。

def button_handler(pin):
	button.irq(handler=None)
	print(pin)

这两行代码首先关闭中断,这样它只触发一次,然后打印有关触发中断的引脚编号。

继续下面的程序:

led.value(1)
utime.sleep(urandom.uniform(5, 10))
led.value(0)

第一行将 LED 点亮,下一行暂停程序,最后一行再次关闭 LED 灯。玩家按下按钮之后,LED 被点亮的时间并不是固定的,而是利用 urandom 库将程序暂停 5 到 10 秒,换句话说,就是 LED 会亮 5 到 10 秒。

然而,目前还没有什么东西在等待着按钮被按下。你需要为此设置中断,方法是在程序末尾增加一行:

button.irq(trigger=machine.Pin.IRQ_RISING, handler=button_handler)

设置中断需要两个东西:触发器和处理程序。触发器告诉 Pico 它应该寻找什么作为中断它正在做的事情的有效信号;handler 就是中断被触发后运行的函数名。

在这个程序中,你的触发器是 IRQ_RISING,这意味着中断是由引脚电压从低电平升到高电平时触发。而 IRQ_FALLING 这是引脚电压从高电平到低电平时触发。如果你需要写一个程序,在一个引脚改变时触发一个中断,而不关心它是上升还是下降,你可以使用「|」组合这两个触发器:

button.irq(trigger=machine.Pin.IRQ_RISING | machine.Pin.IRQ_FALLING, andler=button_handler)

本项目中,代码将变成下面这样:

import machine
import utime
import urandom

led = machine.Pin(15, machine.Pin.OUT)
button = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_DOWN)

def button_handler(pin):
	button.irq(handler=None)
	print(pin)

led.value(1)
utime.sleep(urandom.uniform(5, 10))
led.value(0)
button.irq(trigger=machine.Pin.IRQ_RISING, handler=button_handler)

单击 Run 按钮,并将程序保存到 Pico 上命名为 Reaction_Game.py。你会看到 LED 灯亮起来,这是信号,用你的手指放在按钮上。当 LED 熄灭时,尽可能快地按下按钮。

当你按下按钮时,它会触发你之前编写的处理程序代码。查看 Shell 区域,你将看到 Pico 打印了一条消息,确认中断是由 GP14 引脚触发的。你还会看到另一个细节:mode=IN 告诉你引脚被配置为输入。不过,这个信息并没有给游戏造成多大的影响,为此,你需要一种方法来加快玩家的反应速度。首先从按钮处理程序中删除 print(pin) 这一行,你不再需要它了。

转到程序的底部并添加一条新行,就在你设置中断的位置的正上方:

timer_start = utime.ticks_ms()

这里创建了一个名为 timer_start 的新变量,并赋予了 utime.ticks_ms() 函数的输出,该函数计算自 utime 库开始计数以来已过的毫秒数。这给在 LED 熄灭之后和中断触发器准备好读取按钮之前,提供了一个参考的时间点。

接下来,回到按钮处理程序,添加以下两行:

timer_reaction = utime.ticks_diff(utime.ticks_ms(), timer_start)
print("Your reaction time was " + str(timer_reaction) + "milliseconds!")

第一行创建了另一个变量,这一次是中断实际触发的时刻,换句话说,就是按下按钮的时 候。但是,它不像以前那样简单地从 utime.ticks_ms() 中读取数据,而是使用 utime.ticks_diff() 这个函数,它得到了触发这行代码的时间与变量 timer_start 中保存的参考点之间的差异。

第二行代码打印出计算结果。

最后的代码如下:

import machine
import utime
import urandom

led = machine.Pin(15, machine.Pin.OUT)
button = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_DOWN)

def button_handler(pin):
	button.irq(handler=None)
	timer_reaction = utime.ticks_diff(utime.ticks_ms(), timer_start)
	print("Your reaction time was " + str(timer_reaction) + " milliseconds!")

led.value(1)
utime.sleep(urandom.uniform(5, 10))
led.value(0)
timer_start = utime.ticks_ms()
button.irq(trigger=machine.Pin.IRQ_RISING, handler=button_handler)

再次点击 Run 按钮,等待 LED 熄灭,然后按下按钮。这一次,你将看到一条消息,告诉你按下按钮的速度,而不是触发中断的针的报告,这是对你反应时间的测量。

再次点击运行按钮,看看你是否可以更快的速度按下按钮,在这个游戏中,你正在尝试尽可能低的分数!

双人游戏

单人游戏很有趣,但是让你的朋友参与进来会更好。你可以先邀请他们玩你的游戏,比较你 的高分或低分,看看谁的反应最快。然后,你可以修改你的游戏,让他们和你一起玩。

首先在你的电路中添加第二个按钮。如图所示。确保两个按钮之间有足够的距离,以便玩家能够将手指放在按钮上。

虽然第二个按钮现在已经连接到 Pico,但它还不知道如何使用它。回到你在 Thonny 的程序 中,找到你设置第一个按钮的地方。在这一行下面,添加:

right_button = machine.Pin(16, machine.Pin.IN, machine.Pin.PULL_DOWN)

你将注意到,名称现在指定了你正在使用的按钮(右侧的按钮)。为了避免混淆,请编辑
上面的一行,这样你就可以清楚地看到,原来黑板上唯一的按钮现在变成了左边的按钮:

left_button = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_DOWN)

你还需要在程序的其他地方进行相同的更改。转到按钮处理器功能并更改行:

button.irq(handler=None)

它读取:

left_button.irq(handler=None)

接下来,为第二个按钮添加:

right_button.irq(handler=None)

向下滚动到程序的底部,并更改设置中断触发器的行,以便它进行读取:

left_button.irq(trigger=machine.Pin.IRQ_RISING, handler=button_handler)

同样,在它下面添加另一行,以在新按钮上设置中断触发器:

right_button.irq(trigger=machine.Pin.IRQ_RISING, handler=button_handler)

你的程序现在应该看起来像这样:

import machine
import utime
import urandom

led = machine.Pin(15, machine.Pin.OUT)
left_button = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_DOWN)
right_button = machine.Pin(16, machine.Pin.IN, machine.Pin.PULL_DOWN)

def button_handler(pin):
	left_button.irq(handler=None)
	right_button.irq(handler=None)
	timer_reaction = utime.ticks_diff(utime.ticks_ms(), timer_start)
	print("Your reaction time was " + str(timer_reaction) + " milliseconds!")

led.value(1)
utime.sleep(urandom.uniform(5, 10))
led.value(0)
right_button.irq(trigger=machine.Pin.IRQ_RISING, handler=button_handler)
left_button.irq(trigger=machine.Pin.IRQ_RISING, handler=button_handler)

点击 Run 图标,等待 LED 熄灭,然后按下左边的按钮开关,你会看到游戏和之前一样,将你的反应时间打印到 Shell 区域。再次点击运行图标,但这一次,当 LED 熄灭时,按右边的按钮也在正常工作,打印你们的反应时间。

中断和中断处理函数

你创建的每个中断都需要一个处理程序,但单个处理程序可以处理任意数量的中断。在这个程序中,有两个中断都指向同一个处理程序,这意味着无论触发哪个中断,它们都将运行相同的代码。不同的程序可能有两个处理程序,让每个中断运行不同的代码,这完全取决于你需要你的程序做什么。

为了让游戏更精彩一点,你可以让它报告两个玩家中哪一个是第一个按下按钮的。回到程序的 顶部,就在下面,你可以设置 LED 和两个按钮,并添加以下内容:

fastest_button = None

这将设置一个新变量 fastest_button,并将其初始值设置为 None,因为还没有按下任何按钮。

接下来,到按钮处理程序的底部,删除处理计时器和打印的两行,然后用以下代码替换它们:

global fastest_button
fastest_button = pin

这两行代码让 fastest_button 成为变量,并将其设置为相应按钮的引脚编号。

现在直接转到程序的底部,并添加以下两行:

while fastest_button is None:
	utime.sleep(1)

这里创建了一个循环,但它不是一个无限循环。这里,你告诉 MicroPython 只有在 fastest_button 变量仍然为 None 时才在循环中运行代码。实际上,这会暂停程序的主线程,直到中断处理程序更改了变量的值。

如果两个玩家都没有按下按钮,程序就会暂停。

最后,你需要一种方法来确定哪位选手获胜,并向他们表示祝贺。在程序的底部输入以下代码:

if fastest_button is left_button:
	print("Left Player wins!")
elif fastest_button is right_button:
	print("Right Player wins!")

第一行设置了一个 if 条件,用于查看 fastest_button 变量是否为 left_button(这意味着 IRQ 是由左手按钮触发的)。如果是这样,它将打印一条消息祝贺左边的玩家(他的按钮连接到 GP14 引脚)。

如果条件不成立,它将查看 fastest_button 变量是否为 right_button。如果是,则打印一条消息祝贺右边的玩家,该玩家的按钮已连接到 GP16。

完成的程序如下:

import machine
import utime
import urandom

led = machine.Pin(15, machine.Pin.OUT)
left_button = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_DOWN)
right_button = machine.Pin(16, machine.Pin.IN, machine.Pin.PULL_DOWN)

fastest_button = None

def button_handler(pin):
	left_button.irq(handler=None)
	right_button.irq(handler=None)
	global fastest_button
	fastest_button = pin

led.value(1)
utime.sleep(urandom.uniform(5, 10))
led.value(0)
left_button.irq(trigger=machine.Pin.IRQ_RISING, handler=button_handler)
right_button.irq(trigger=machine.Pin.IRQ_RISING, handler=button_handler)

while fastest_button is None:
	utime.sleep(1)

if fastest_button is left_button:
	print("Left Player wins!")
elif fastest_button is right_button:
	print("Right Player wins!")

点击 Run 按钮运行程序,等待 LED 熄灭,但不要按下任何一个按钮开关。

你将看到 Shell 区域仍然是空白的,并且不会返回「>>>」提示符。这是因为主线程仍在运行,处于你创建的循环中。

现在按左手按钮(GP14)。你将看到一条祝贺你的消息「Left Player wins!」打印到 Shell 上。

再次单击 Run 运行,并尝试在 LED 熄灭后按下右手按钮。你将看到另一条消息「Right Player wins!」打印出来,这一次祝贺你的右手。

再次点击 Run 运行,这次每个按钮上都有一个手指,同时按下它们,看看你的右手还是左手更快!

现在你已经创造了一个双人游戏,你可以邀请你的朋友一起玩,看看你们谁的反应速度最快!

你还可以:
查看系列教程中的其他文章「树莓派 Pico 上手指南(在 Pico 上使用 MicroPython)」
购买本教程所用到的 Pico 上手套件



坐沙发

发表评论

你的邮件地址不会公开


*