1项目要求
具体要求:随机点亮板上的一个LED,按下板上的一个按键,在显示屏上显示出从灯亮到按键之间的时间。
实现方式:通过软件产生随机数,程序启动以后在随机数控制的时间下点亮板上的LED,被测试者按下按键以后,处理器计算从点亮灯到接收到按键之间的时间差,并将时间差通过USB显示在PC上,也可以将OLED用起来,在OLED上显示时间信息。
2完成的功能及达到的性能
2.1随机显示LED
在1~12个LED灯中随机打开一个LED,颜色默认为白色,该颜色可以在程序中进行自定义。
2.2预备信号灯
在用户准备完成后(即按下k2键),黄灯亮,提示用户进行准备;在经过随机时间后显示随机LED灯,这段时间红灯亮,提示用户按下k1进行反应;在用户按下k1键后,绿灯亮,提示用户已经按下k1键并且完成检测。
2.3按键检测
k1键设定为反应键,用于在用户看到亮灯后按下按键计算反应时间;
k2键设定为准备键,按下后开始新的一轮测试;
k1,k2键均有消抖功能,在测试中表现良好;
2.4屏幕对应显示
在开始时屏幕显示Reaction Test,在用户准备完成后(即按下k2键),屏幕显示Ready!,提示用户进行准备;在经过随机时间后显示随机LED灯,这段时间屏幕显示Press!,提示用户按下k1进行反应;在用户按下k1键后,屏幕显示一行Reaction Time,下一行显示保留三位小数的反应时间(e.g. 0.123s)提示用户已经按下k1键并且完成检测,将反应时间显示出来。
2.5随机亮灯时间
使用MicroPython的random库随机一个延迟时间,用于延迟后显示LED供用户进行反应测试。
2.6计算反应时间
使用MicroPython的time库的time.ticks_ms()函数计算亮灯后到用户按下的时间。
3实现思路
- 检测按键,完成按键消抖
- 延迟随机时间
- 显示随机LED灯
- 检测用户的反应时间
- 相应图形显示
4实现过程
4.1程序流程图
4.2 LED相关
首先需要在代码中定义开发板上的LED引脚,以便控制LED的亮灭,设定对应的显示颜色,以点亮LED,需要将LED颜色重新设定为#000000,以关闭LED。这一部分在ws2812b.py中已经定义完成,只需要调用ws2812b.on(n)即可打开第n个LED,ws2812b.off(n) 即可关闭第n个LED;
首先需要在代码中定义开发板上的LED引脚,以便控制LED的亮灭,选择要点亮的LED之后,需要将相应的LED引脚设为输出,设定高电平,以点亮LED,设定为低电平,以关闭LED。这一部分在led.py中已经定义完成,只需要调用r.on()即可打开红色LED,r.off()即可关闭红色LED,同理黄色LED、绿色LED、蓝色LED;
from board import pin_cfg
import time
from machine import Pin
r = Pin(pin_cfg.red_led, Pin.OUT)
g = Pin(pin_cfg.green_led, Pin.OUT)
b = Pin(pin_cfg.blue_led, Pin.OUT)
y = Pin(pin_cfg.yellow_led, Pin.OUT)
4.3 随机数相关
之后需要使用随机数生成器来随机选择要点亮的LED,我使用的是MicroPython中的random库,其中有int(random.uniform(0, 12))函数用于生成一个1~12的整数用于选择LED;
在测试中,使用random.uniform(2, 5)生成一个2~5的浮点数,在等待该数时间后开始亮起一个随机LED进行测试。
import random
4.4 按键相关
项目中k1、k2按键均由button.py进行了声明,在该文件中定义了引脚,并且进行了按键消抖,在主程序循环中只需要检测k1.value()即可完成对于k1按键的检测。
import time
from board import pin_cfg
from machine import Pin
class button:
def __init__(self, pin, callback=None, trigger=Pin.IRQ_RISING, min_ago=200):
#print("button init")
self.callback = callback
self.min_ago = min_ago
self._next_call = time.ticks_add(time.ticks_ms(), self.min_ago)
self.pin = Pin(pin, Pin.IN, Pin.PULL_UP)
self.pin.irq(trigger=trigger, handler=self.debounce_handler)
self._is_pressed = False
def call_callback(self, pin):
#print("call_callback")
self._is_pressed = True
if self.callback is not None:
self.callback(pin)
def debounce_handler(self, pin):
#print("debounce")
if time.ticks_diff(time.ticks_ms(), self._next_call) > 0:
self._next_call = time.ticks_add(time.ticks_ms(), self.min_ago)
self.call_callback(pin)
def value(self):
p = self._is_pressed
self._is_pressed = False
return p
k1 = button(pin_cfg.k1)
k2 = button(pin_cfg.k2)
4.5 屏幕相关
在项目中从Pico输出到屏幕的驱动由oled.py、ssd1306.py进行了声明,在该文件中定义了屏幕引脚对应,屏幕的初始化方式,数据写入屏幕的方式和基本的图形函数、文字函数;在进行屏幕编辑时,使用oled.text()即可编辑文字,将编辑好的屏幕展示出来需要oled.show(),屏幕重新写需要oled.fill(0)首先进行屏幕清除,然后再将写好的屏幕展示出来。
from machine import Pin, SPI
from ssd1306 import SSD1306_SPI
import framebuf
from board import pin_cfg
spi = SPI(1, 100000, mosi=Pin(pin_cfg.spi1_mosi), sck=Pin(pin_cfg.spi1_sck))
oled = SSD1306_SPI(128, 64, spi, Pin(pin_cfg.spi1_dc),Pin(pin_cfg.spi1_rstn), Pin(pin_cfg.spi1_cs))
# MicroPython SSD1306 OLED driver, I2C and SPI interfaces
from micropython import const
import framebuf
# register definitions
SET_CONTRAST = const(0x81)
SET_ENTIRE_ON = const(0xA4)
SET_NORM_INV = const(0xA6)
SET_DISP = const(0xAE)
SET_MEM_ADDR = const(0x20)
SET_COL_ADDR = const(0x21)
SET_PAGE_ADDR = const(0x22)
SET_DISP_START_LINE = const(0x40)
SET_SEG_REMAP = const(0xA0)
SET_MUX_RATIO = const(0xA8)
SET_IREF_SELECT = const(0xAD)
SET_COM_OUT_DIR = const(0xC0)
SET_DISP_OFFSET = const(0xD3)
SET_COM_PIN_CFG = const(0xDA)
SET_DISP_CLK_DIV = const(0xD5)
SET_PRECHARGE = const(0xD9)
SET_VCOM_DESEL = const(0xDB)
SET_CHARGE_PUMP = const(0x8D)
# Subclassing FrameBuffer provides support for graphics primitives
# http://docs.micropython.org/en/latest/pyboard/library/framebuf.html
class SSD1306(framebuf.FrameBuffer):
def __init__(self, width, height, external_vcc):
self.width = width
self.height = height
self.external_vcc = external_vcc
self.pages = self.height // 8
self.buffer = bytearray(self.pages * self.width)
super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)
self.init_display()
def init_display(self):
for cmd in (
SET_DISP, # display off
# address setting
SET_MEM_ADDR,
0x00, # horizontal
# resolution and layout
SET_DISP_START_LINE, # start at line 0
SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0
SET_MUX_RATIO,
self.height - 1,
SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0
SET_DISP_OFFSET,
0x00,
SET_COM_PIN_CFG,
0x02 if self.width > 2 * self.height else 0x12,
# timing and driving scheme
SET_DISP_CLK_DIV,
0x80,
SET_PRECHARGE,
0x22 if self.external_vcc else 0xF1,
SET_VCOM_DESEL,
0x30, # 0.83*Vcc
# display
SET_CONTRAST,
0xFF, # maximum
SET_ENTIRE_ON, # output follows RAM contents
SET_NORM_INV, # not inverted
SET_IREF_SELECT,
0x30, # enable internal IREF during display on
# charge pump
SET_CHARGE_PUMP,
0x10 if self.external_vcc else 0x14,
SET_DISP | 0x01, # display on
): # on
self.write_cmd(cmd)
self.fill(0)
self.show()
def poweroff(self):
self.write_cmd(SET_DISP)
def poweron(self):
self.write_cmd(SET_DISP | 0x01)
def contrast(self, contrast):
self.write_cmd(SET_CONTRAST)
self.write_cmd(contrast)
def invert(self, invert):
self.write_cmd(SET_NORM_INV | (invert & 1))
def rotate(self, rotate):
self.write_cmd(SET_COM_OUT_DIR | ((rotate & 1) << 3))
self.write_cmd(SET_SEG_REMAP | (rotate & 1))
def show(self):
x0 = 0
x1 = self.width - 1
if self.width != 128:
# narrow displays use centred columns
col_offset = (128 - self.width) // 2
x0 += col_offset
x1 += col_offset
self.write_cmd(SET_COL_ADDR)
self.write_cmd(x0)
self.write_cmd(x1)
self.write_cmd(SET_PAGE_ADDR)
self.write_cmd(0)
self.write_cmd(self.pages - 1)
self.write_data(self.buffer)
class SSD1306_I2C(SSD1306):
def __init__(self, width, height, i2c, addr=0x3C, external_vcc=False):
self.i2c = i2c
self.addr = addr
self.temp = bytearray(2)
self.write_list = [b"\x40", None] # Co=0, D/C#=1
super().__init__(width, height, external_vcc)
def write_cmd(self, cmd):
self.temp[0] = 0x80 # Co=1, D/C#=0
self.temp[1] = cmd
self.i2c.writeto(self.addr, self.temp)
def write_data(self, buf):
self.write_list[1] = buf
self.i2c.writevto(self.addr, self.write_list)
class SSD1306_SPI(SSD1306):
def __init__(self, width, height, spi, dc, res, cs, external_vcc=False):
self.rate = 10 * 1024 * 1024
dc.init(dc.OUT, value=0)
res.init(res.OUT, value=0)
cs.init(cs.OUT, value=1)
self.spi = spi
self.dc = dc
self.res = res
self.cs = cs
import time
self.res(1)
time.sleep_ms(1)
self.res(0)
time.sleep_ms(10)
self.res(1)
super().__init__(width, height, external_vcc)
def write_cmd(self, cmd):
self.spi.init(baudrate=self.rate, polarity=0, phase=0)
self.cs(1)
self.dc(0)
self.cs(0)
self.spi.write(bytearray([cmd]))
self.cs(1)
def write_data(self, buf):
self.spi.init(baudrate=self.rate, polarity=0, phase=0)
self.cs(1)
self.dc(1)
self.cs(0)
self.spi.write(buf)
self.cs(1)
4.6 时间相关
引用了MicroPython中的time库,在测试LED灯亮起的同时开始计时,将该时间记录在Start_time变量中,在用户反应按下按键k1后将时间记录在End_time中,计算react_time=End_time-Start_time即为用户的反应时间
import time
4.6 程序代码
from button import k1,k2
from board import pin_cfg
from led import r,g,b,y
from oled import oled
import time
import ws2812b
import random
rand_time=0
react_time=0
Start_time=0
End_time=0
cs_led=0
oled.text("Reaction Test",12,20)
oled.show()
def YON():
y.on()
r.off()
g.off()
def RON():
y.off()
r.on()
g.off()
def GON():
y.off()
r.off()
g.on()
def YRGoff():
y.off()
r.off()
g.off()
YRGoff()
ws2812b.off_all()
while True:
if k2.value():
oled.fill(0)
oled.show()
rand_time=random.uniform(2,5)
cs_led=int(random.uniform(0,12))
oled.text("Ready!",12,20)
oled.show()
YON()
time.sleep(rand_time)
oled.fill(0)
oled.show()
oled.text("Press!",12,20)
oled.show()
ws2812b.on(cs_led)
RON()
Start_time=time.ticks_ms()
while True:
if k1.value():
End_time=time.ticks_ms()
oled.fill(0)
oled.show()
GON()
ws2812b.off(cs_led)
break
react_time=End_time-Start_time
oled.text("Reaction Time",12,20)
oled.text('{:3.3f}s'.format(react_time/1000),12,40)
oled.show()
print('Reaction Time:'+str(react_time/1000)+'s')
time.sleep(0.3)
print('Ending...')
5遇到的主要难题
使用STEP Pico时需要进行初始化,将上一次测试中的LED灯关闭,在每一次屏幕刷新时也需要将屏幕重新刷新;在程序的开头完成了屏幕LED等器件的初始化。
测试器的时间精度不够高,在测试过程中,可能会出现延迟问题,例如按键响应速度慢或显示屏显示延迟等。应当在程序中进行适当的延迟和缓冲来确保测试的准确性和可靠性。已经通过程序中将两次计时之间运行的代码削减到最少。
测试器需要具备良好的用户友好性,如合理的界面设计和交互方式等。需要考虑用户的使用体验,并在程序中提供必要的提示和帮助,以确保用户能够正确地操作测试器。已经通过屏幕文字提示和RGYLED灯进行了很大程序上的提示。
6未来展望
该项目后续可以引入更多的交互方式,比如蜂鸣器buzzer的提示功能,对用户的听觉进行刺激,让用户达到更快的反应速度。
该项目可以将测试器设计成类似于游戏的形式,引入奖励机制、排行榜等元素,以吸引更多的用户使用并提高其趣味性和互动性。
该项目可以使用更高精度的计时器和传感器,以提高测试器的精度和稳定性,从而更好地满足用户的需求和期望。