**这是本文档旧的修订版!**
反应时间测试
使用一个LED和按钮创建一个简单的反应计时游戏,可以供一个人玩,也可以两个人一起玩。
微控制器不仅出现在工业设备中,还为家庭中的许多电子产品提供动力,包括玩具和游戏。在本章中,我们将设计一款简单的反应时间测试游戏,看看你的朋友中谁会在灯熄灭的一瞬间能最快点击按键。
对反应时间的研究被称为心理计时法,虽然这是一门硬科学,它也是许多基于技能的游戏的基础,包括你即将创建的游戏。你的反应时间 - 你的大脑来响应并判断、发送信号去执行的过程,以毫秒计,人类的平均反应时间大约是200-250毫秒, 但有些人喜欢得更快的反应时间, 给他们一个真正的优势在游戏中!
对于这个项目,你需要用到你的Pico核心板、学习板上任何颜色的LED,一个或两个按键开关。
1. 单人游戏
在这个游戏的程序中,使用学习板上的一颗LED作为输出设备,取代了你通常在游戏机上使用的电视;使用学习板上的一个按键进行控制,而你的Pico是游戏主机,尽管比你通常看到的要小得多!
现在你需要真正地编写游戏。像往常一样,将你的Pico连接到你的树莓派或其他电脑,并加载Thonny、创建一个新程序、并通过导入machine库来启动它,这样你就可以控制你的Pico的GPIO引脚:
import machine
你还需要utime库:
import utime
此外,你将需要一个新的库:urandom,它是一个创建随机数的库,它在这个游戏中是一个关键的部分,使得游戏更有趣,并使用在这个游戏中,以防止玩家谁已经玩它之前简单地倒数固定的秒点击运行按钮。
接下来,设置一个按下的变量为False(稍后详细介绍),并设置您正在使用的两个引脚:
- 用于LED的GP16(黄色的LED)
- 用于按钮开关的GP12
pressed = False led = machine.Pin(16, machine.Pin.OUT) button = machine.Pin(12, machine.Pin.IN, machine.Pin.PULL_UP)
在前面的章节中,我们已经学会如何在主程序或单独的线程中处理按键的响应。不过,这一次我们将采用一种不同的、更灵活的方法 - 中断请求(IRQs)。这个名字听起来很复杂,但其实很简单,想象你正在一页一页地读一本书,有人走到你面前问你一个问题。这个人正在执行一个中断请求: 要求你停止正在做的事情,回答他们的问题,然后让你回去读你的书。
MicroPython中断请求以完全相同的方式工作,它允许某些东西中断主程序,在本例中是按下一个按钮开关。在某些方面,它类似于一个线程,因为有一段代码位于主程序之外。不过,与线程不同的是代码不会持续运行,它只在触发中断时运行。
首先为中断定义一个处理程序,这被称为回调函数,是中断触发时运行的代码。与任何类型的嵌套代码一样,处理程序的代码——第一行之后的所有代码——每一层都需要缩进四个空格,Thonny会自动为你做这件事。
def button_handler(pin): global pressed if not pressed: pressed=True print(pin)
这个处理程序首先检查被按下的变量的状态,然后将其设置为True以忽略进一步的按键按压(从而结束游戏)。然后输出有关触发中断的引脚的信息。这不是太重要的时刻,你只有一个引脚配置为一个输入,GP12,所以中断将总是来自那个引脚,但让你测试你的中断很容易。
继续下面的程序,记住删除Thonny自动创建的缩进,以下代码不是处理程序的一部分:
led.value(0) utime.sleep(urandom.uniform(5, 10)) led.value(1)
这段代码对你来说马上就很熟悉了:
- 第一行将连接到GP16引脚的LED打开;
- 下一行暂停了程序;
- 最后一行再次关闭LED,这是给玩家的信号,该按按键了。
不是使用固定的延迟,它利用urandom库暂停程序5到10秒之间-“均匀”部分指的是这两个数字之间的均匀分布。
不过,目前还没有什么东西等着按下按钮。你需要设置中断,通过在你的程序底部输入以下行:
button.irq(trigger=machine.Pin.IRQ_FALLING, handler=button_handler)
设置一个中断需要两个东西:触发器和处理程序。触发器告诉你的Pico它应该寻找一个有效的信号来中断它正在做的事情,我们在前面的程序中定义的处理程序是触发中断后运行的代码。在这个程序中,我们的触发信号为IRQ_FALLING,这意味着中断被触发时,引脚的值从高(由于内部上拉电阻的作用,默认状态为高电平)到低,当连接到“地”的按钮被按下,
IRQ_RISING的触发信号会做相反的事情,当引脚从低到高时触发中断。
在我们的电路中,一旦按下按键,IRQFALLING将被触发, IRQRISING只在按钮被释放时触发。
中断申请的上升沿和下降沿
如果你需要编写一个触发中断的程序,无论管脚信号是上升还是下降都可以触发,你可以使用管道或竖条符号( | )来组合这两个触发信号:
button.irq(trigger= machine.Pin.IRQ_RISING | machine.Pin.IRQ_FALLING,handler = button_handler)
你的程序应该是这样的:
import machine import utime import urandom pressed = False led = machine.Pin(16, machine.Pin.OUT) button = machine.Pin(12, machine.Pin.IN, machine.Pin.PULL_UP) def button_handler(pin): global pressed if not pressed: pressed=True print(pin) led.value(0) utime.sleep(urandom.uniform(5, 10)) led.value(1) button.irq(trigger=machine.Pin.IRQ_FALLING, handler=button_handler)
单击“运行”按钮并将程序作为Reaction_Game.py 保存到您的Pico。 您会看到LED亮起:这是您的手指放在按钮上准备就绪的信号。
当LED熄灭时,尽快按下按钮。
当您按下按钮时,它会触发您之前编写的处理程序代码。 查看Shell区域:您会看到Pico打印了一条消息,确认中断是由引脚GP16触发的。您还将看到另一个详细信息:mode=IN告诉您该引脚已配置为输入。 不过,这条信息对游戏的意义不大:为此,您需要一种方法来计时玩家的反应速度。首先从按钮处理程序中删除行 print(pin) - 您不再需要它了。
转到程序底部并添加一个新行,就在设置中断的位置上方:
timer_start = utime.ticks_ms()
这将创建一个名为timer_start的新变量,
并用utime.ticks_ms()函数的输出填充它,该函数计算自utime库开始计数以来经过的毫秒数。这提供了一个参考点:刚好在LED熄灭之后和就在中断触发器准备好读取按钮按下之前的时间。
接下来,返回按键处理程序并添加以下两行,记住它们需要缩进四个空格,以便MicroPython知道它们构成嵌套代码的一部分:
timer_reaction = utime.ticks_diff(utime.ticks_ms(), timer_start) print("Your reaction time was " + str(timer_reaction) + " milliseconds!")
第一行创建了另一个变量,这次是实际触发中断的时间——换句话说,当你按下按键时。不过,它不是像以前那样简单地从utime.ticksms()读取数据,而是使用 utime.ticksdiff()一个函数它提供了触发这行代码的时间与变量timer_start中保存的参考点之间的差异。
第二行打印结果,但使用连接来很好地格式化它。文本或字符串的第一位告诉用户后面的数字是什么意思; + 表示接下来的任何内容都应该与该字符串一起打印。在这种情况下,接下来是内容timer_reaction 变量 - 以毫秒为单位获取计时器参考点与按下按钮并触发中断之间的差异。
最后,最后一行再连接一个字符串,这样用户就知道这个数字是以毫秒为单位测量的,而不是其他一些单位,如秒或微秒。注意间距:你会看到'was'之后和第一个字符串的结束引号之前有一个尾随空格,第二个字符串的开引号之后和'milliseconds'这个词之前有一个前导空格。如果没有这些,连接的字符串将打印类似“您的反应时间为323毫秒”的内容。
你的程序现在应该是这样的:
import machine import utime import urandom pressed = False led = machine.Pin(16, machine.Pin.OUT) button = machine.Pin(12, machine.Pin.IN, machine.Pin.PULL_UP) def button_handler(pin): global pressed if not pressed: pressed=True timer_reaction = utime.ticks_diff(utime.ticks_ms(), timer_start) print("Your reaction time was " + str(timer_reaction) + " milliseconds!") led.value(0) utime.sleep(urandom.uniform(5, 10)) led.value(1) timer_start = utime.ticks_ms() button.irq(trigger=machine.Pin.IRQ_FALLING, handler=button_handler)
挑战:定制
你能调整你的游戏让LED保持点亮更长时间吗?保持点亮的时间更短怎么办? 您能否个性化打印到Shell区域的消息,并添加第二条祝贺玩家的消息?
再次单击运行按钮,等待LED熄灭,然后按下按钮。这一次,您将看到一条信息,告诉您按下按钮的速度,而不是有关触发中断的引脚的报告 - 测量您的反应时间。再次单击“运行”按钮,看看这次您是否可以更快地按下按钮 - 在这个游戏中,你要争取尽可能低的分数!
2. 两人游戏
单人游戏很有趣,但让您的朋友参与进来就更好了。您可以先邀请他们玩您的游戏,然后比较您的高分(或者说低分),看看谁的反应时间最快。然后,您可以修改您的游戏,让您进行正面交锋!
我们需要另外的一个按钮,返回到您在Thonny中的程序,找到您设置第一个按钮的位置。在这一行的正下方,添加:
right_button = machine.Pin(13, machine.Pin.IN, machine.Pin.PULL_UP)
修改一下前面的代码,让先前的按键跟这个按键的命名一致起来:
left_button = machine.Pin(12, machine.Pin.IN, machine.Pin.PULL_UP)
您也需要在程序的其它地方进行相同的更改。滚动到底部,我们的代码并将设置中断触发器的行更改为:
left_button.irq(trigger=machine.Pin.IRQ_FALLING, handler=button_handler)
在其下方添加另一行以在新按钮上设置中断触发器:
right_button.irq(trigger=machine.Pin.IRQ_FALLING, handler=button_handler)
你的程序现在应该是这样的:
import machine import utime import urandom pressed = False led = machine.Pin(16, machine.Pin.OUT) left_button = machine.Pin(12, machine.Pin.IN, machine.Pin.PULL_UP) right_button = machine.Pin(13, machine.Pin.IN, machine.Pin.PULL_UP) def button_handler(pin): global pressed if not pressed: pressed=True timer_reaction = utime.ticks_diff(utime.ticks_ms(), timer_start) print("Your reaction time was " + str(timer_reaction) + " milliseconds!") led.value(0) utime.sleep(urandom.uniform(5, 10)) led.value(1) timer_start = utime.ticks_ms() right_button.irq(trigger=machine.Pin.IRQ_FALLING, handler=button_handler) left_button.irq(trigger=machine.Pin.IRQ_FALLING, handler=button_handler)
中断和处理程序
您创建的每个中断都需要一个处理程序,但一个处理程序可以处理任意数量的中断。 在这个程序的情况下,你有两个中断都进入同一个处理程序——这意味着无论哪个中断触发,它们都会运行相同的代码。 一个不同的程序可能有两个处理程序,让每个中断运行不同的代码——这完全取决于你需要你的程序做什么。
单击“运行”图标,等待LED熄灭,然后按下左侧的按钮开关:您将看到游戏与以前相同,将您的反应时间打印到Shell区域。 再次单击“运行”图标,但这次当LED熄灭时,按右侧按钮:游戏将正常运行,正常打印您的反应时间。
为了让游戏更刺激一点,你可以让它报告两个玩家中的哪一个是第一个按下按钮的。 返回程序顶部,就在您设置LED和两个按钮的下方,并添加以下内容:
fastest_button = None
这会设置一个新变量,fastest_button,并将其初始值设置为None, 因为还没有按下任何按钮。 接下来,转到按钮处理程序的底部并删除处理计时器和打印的两行 - 然后将它们替换为:
global fastest_button fastest_button = pin
请记住,这些行需要缩进四个空格,以便MicroPython知道它们是函数的一部分。这两行允许你的函数改变,而不是仅仅读取,fastest_button变量,并将它设置为包含触发中断的引脚的详细信息,你的游戏在本章前面打印到Shell区域的相同细节,包括触发引脚的编号。 现在转到程序的底部,并添加以下两行:
while fastest_button is None: utime.sleep(1)
这会创建一个循环,但它不是一个无限循环:在这里,您已经告诉MicroPython 只有当fast_button变量仍然为零时才在循环中运行代码 - 这是在程序开始时初始化的值。 实际上,这会暂停程序的主线程,直到中断处理程序更改变量的值。 如果两个玩家都没有按下按钮,程序将简单地暂停。 最后,您需要一种方法来确定哪位玩家获胜并祝贺他们。 在程序底部键入以下内容,确保删除Thonny在第一行为您创建的四个空格缩进——这些行不构成循环的一部分:
if fastest_button is left_button: print("Left Player wins!") elif fastest_button is right_button: print("Right Player wins!")
第一行设置了一个“if”条件,它查看fastestbutton变量是否is leftbutton - 表示IRQ是由左侧按钮触发的。 如果是这样,它将打印一条消息——下面的行缩进四个空格,以便MicroPython知道只有在条件为真时才应该运行它——祝贺左手玩家,其按钮连接到GP12。
下一行不应缩进,将条件扩展为“elif”——“else if”的缩写,表示“如果第一个条件不为真,请检查下一个条件”。 这次它查看fastestbutton变量是否为rightbutton ——如果是,则打印一条消息祝贺右手玩家,其按钮连接到GP13。
您完成的程序应如下所示:
import machine import utime import urandom pressed = False led = machine.Pin(16, machine.Pin.OUT) left_button = machine.Pin(12, machine.Pin.IN, machine.Pin.PULL_UP) right_button = machine.Pin(13, machine.Pin.IN, machine.Pin.PULL_UP) fastest_button = None def button_handler(pin): global pressed if not pressed: pressed=True global fastest_button fastest_button = pin led.value(0) utime.sleep(urandom.uniform(5, 10)) led.value(1) timer_start = utime.ticks_ms() left_button.irq(trigger=machine.Pin.IRQ_FALLING, handler=button_handler) right_button.irq(trigger=machine.Pin.IRQ_FALLING, 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!")
按下运行按钮并等待LED熄灭,但现在不要按下任何一个按钮开关。 您会看到Shell区域保持空白,并且没有带回»›提示符;那是因为主线程仍在运行,位于您创建的循环中。
现在按下连接到引脚GP12的左侧按钮。 您会看到一条祝贺您打印到Shell的消息——您的左手是赢家! 再次单击运行并尝试推送 LED熄灭后右侧的按钮:您会看到另一条消息打印出来,这次是祝贺您的右手。 再次单击“运行”,这一次将一根手指放在每个按钮上:同时按下它们,看看你的右手还是左手更快!
现在您已经创建了一个两人游戏,您可以邀请您的朋友一起玩,看看你们中谁的反应时间最快!
挑战:时序
你能修改打印的消息吗?
可以加第三个按钮,让三个人同时玩吗?
有没有上限,您可以添加多少个按钮?
您能否将计时器重新添加到您的程序中,以便告诉获胜玩家他们的反应时间有多快?