Back

Activity 2.7: Reaction Timer

Digital Input & Timing — Day 1 of 2

divider

Activity 2.7

Reaction Timer

Key Concepts

Digital Input

Measuring Time

Returning a Value

Everything So Far Has Been Output

Five activities of telling pins what to do. Today the board starts listening — and once it can listen, it can react, and once it can react, you can time it.

One Method, Two Jobs

led.value(1) # setting: turn this pin on
state = button.value() # reading: what is this pin right now?

.value() with a number sets the pin. .value() with empty parentheses reads it, and hands you back 0 or 1.

An Input Pin Needs a Default

A pin with nothing connected does not read 0— it reads whatever electrical noise is nearby. So an input pin needs something holding it at a known value when the button is not pressed.

button = Pin(33, Pin.IN, Pin.PULL_DOWN)

Pin.PULL_DOWN switches on a resistor inside the board that quietly holds the pin at 0. Press the button and 3V3 overrules it.

So: Pressed Means 1

Not pressed → 0. Pressed → 1. The same as an LED: 1 is the active state.

You will meet PULL_UP elsewhere, which does the opposite — pressed reads 0. Same idea, inverted. Worth knowing when you read other people's code.

Measuring Milliseconds

start = time.ticks_ms()
# ... something happens ...
stop = time.ticks_ms()
print(time.ticks_diff(stop, start), "ms")

time.ticks_ms() is a running count of milliseconds. Read it twice and compare.

Why Not Just Subtract?

That millisecond counter eventually runs out of digits and rolls back to zero, the way an odometer would. If it rolls over between your two readings, plain subtraction gives a nonsense answer.

time.ticks_diff(stop, start) handles the rollover for you. Use it instead of - whenever you subtract two tick readings.

Functions That Hand Something Back

Every function you wrote in 2.5 and 2.6 did something. None of them gave you a value.

def measure_reaction(leds, button):
...
return time.ticks_diff(stop, start)
result = measure_reaction(leds, button)

return ends the function and hands its answer back to whoever called it — so you can store it, print it, or put it in a list.

Today's Objectives

  • Wiring a button and reading it as a digital input
  • Using an internal pull-down so an unpressed pin reads 0
  • Measuring elapsed time with ticks_ms() and ticks_diff()
  • Returning a value from a function so the caller can use it

Key Terms

Digital Input
A pin the board reads instead of drives, giving 0 or 1.
Pull-Down Resistor
A resistor holding an input pin at 0 when nothing else is driving it. The board has one built in, switched on with Pin.PULL_DOWN.
Return Value
The value a function hands back to whoever called it, using return.
False Start
Pressing before the signal — an attempt that must not be counted as a reaction time.

'F' → Fullscreen

divider

Build

Keep Activity 2.6's five-LED array wired exactly as it is. Create a new MicroPython script named 2-7-reaction-timer.


Task 1: Wire the Button and Read It

  • Wire one push button between the 3V3 pin and GPIO 33.
  • Create the button as an input pin with an internal pull-down.
  • Light the whole array while the button is held, and turn it off when released.

GPIO 33 is the last pin in the block you have been using — immediately to the left of 32:

The SparkFun IoT RedBoard RP2350 with the pin labeled 33 outlined on the top header

No extra resistor is needed. The board already contains one, and Pin.PULL_DOWN is how you switch it on:

Circuit diagram showing a push button between the 3V3 pin and GPIO 33, with a dashed outline indicating the internal pull-down resistor inside the board holding the pin at 0 when the button is open
[PHOTO PLACEHOLDER — Task 1: Button Wired Beside the Five-LED Array]
Reaction Timer
from machine import Pin
leds = [
Pin(28, Pin.OUT),
Pin(29, Pin.OUT),
Pin(30, Pin.OUT),
Pin(31, Pin.OUT),
Pin(32, Pin.OUT),
]
button = Pin(33, Pin.IN, Pin.PULL_DOWN)
def all_on(leds):
for led in leds:
led.value(1)
def all_off(leds):
for led in leds:
led.value(0)
while True:
if button.value() == 1:
all_on(leds)
else:
all_off(leds)

Task 2: Time the Press

  • Import time.
  • When the button goes down, record the tick count. When it comes back up, record it again.
  • Print how many milliseconds the button was held.

The inner while loop does nothing at all on purpose — pass just means "keep checking." The program sits there until the button comes back up.

Reaction Timer
from machine import Pin
import time
leds = [
Pin(28, Pin.OUT),
Pin(29, Pin.OUT),
Pin(30, Pin.OUT),
Pin(31, Pin.OUT),
Pin(32, Pin.OUT),
]
button = Pin(33, Pin.IN, Pin.PULL_DOWN)
def all_on(leds):
for led in leds:
led.value(1)
def all_off(leds):
for led in leds:
led.value(0)
while True:
if button.value() == 1:
start = time.ticks_ms()
all_on(leds)
while button.value() == 1:
pass
stop = time.ticks_ms()
all_off(leds)
print("You held it for", time.ticks_diff(stop, start), "ms")

Task 3: The Reaction Test

  • Import random.
  • Replace the held-button loop with a single test: wait a random 2–5 seconds, then light the whole array as the GO signal.
  • Start timing at GO, stop when the button is pressed, and print the reaction time.

The delay has to be random. A fixed pause lets you anticipate the GO, and then you are measuring your counting, not your reflexes.

Reaction Timer
from machine import Pin
import time
import random
leds = [
Pin(28, Pin.OUT),
Pin(29, Pin.OUT),
Pin(30, Pin.OUT),
Pin(31, Pin.OUT),
Pin(32, Pin.OUT),
]
button = Pin(33, Pin.IN, Pin.PULL_DOWN)
def all_on(leds):
for led in leds:
led.value(1)
def all_off(leds):
for led in leds:
led.value(0)
all_off(leds)
print("Get ready...")
time.sleep(random.uniform(2, 5))
all_on(leds)
start = time.ticks_ms()
while button.value() == 0:
pass
stop = time.ticks_ms()
all_off(leds)
print("Reaction time:", time.ticks_diff(stop, start), "ms")

Task 4: Return the Time

  • Move the whole test into a function measure_reaction(leds, button).
  • Instead of printing inside the function, return the measured time so the caller decides what to do with it.
  • Add wait_for_release(button) at the start, so a still-held button from the last go does not ruin the next one.
Reaction Timer
from machine import Pin
import time
import random
leds = [
Pin(28, Pin.OUT),
Pin(29, Pin.OUT),
Pin(30, Pin.OUT),
Pin(31, Pin.OUT),
Pin(32, Pin.OUT),
]
button = Pin(33, Pin.IN, Pin.PULL_DOWN)
def all_on(leds):
for led in leds:
led.value(1)
def all_off(leds):
for led in leds:
led.value(0)
def wait_for_release(button):
while button.value() == 1:
pass
def measure_reaction(leds, button):
all_off(leds)
wait_for_release(button)
print("Get ready...")
time.sleep(random.uniform(2, 5))
all_on(leds)
start = time.ticks_ms()
while button.value() == 0:
pass
stop = time.ticks_ms()
all_off(leds)
return time.ticks_diff(stop, start)
print("Reaction time:", measure_reaction(leds, button), "ms")

Task 5: Five Attempts

  • Run five attempts in a loop, collecting each reaction time in a list.
  • After the fifth, report the best time and the average.

This is the accumulator pattern from Activity 1.21, with one addition: tracking the smallest value you have seen so far.

Reaction Timer
from machine import Pin
import time
import random
leds = [
Pin(28, Pin.OUT),
Pin(29, Pin.OUT),
Pin(30, Pin.OUT),
Pin(31, Pin.OUT),
Pin(32, Pin.OUT),
]
button = Pin(33, Pin.IN, Pin.PULL_DOWN)
def all_on(leds):
for led in leds:
led.value(1)
def all_off(leds):
for led in leds:
led.value(0)
def wait_for_release(button):
while button.value() == 1:
pass
def measure_reaction(leds, button):
all_off(leds)
wait_for_release(button)
print("Get ready...")
time.sleep(random.uniform(2, 5))
all_on(leds)
start = time.ticks_ms()
while button.value() == 0:
pass
stop = time.ticks_ms()
all_off(leds)
return time.ticks_diff(stop, start)
times = []
attempt = 1
while attempt <= 5:
print()
print("Attempt", attempt)
result = measure_reaction(leds, button)
print("Reaction time:", result, "ms")
times.append(result)
attempt = attempt + 1
print()
best = times[0]
total = 0
for t in times:
total = total + t
if t < best:
best = t
print("Best:", best, "ms")
print("Average:", total // len(times), "ms")

Task 6: Catch the False Start

  • Write pressed_early(button, seconds) that waits the given time but reports back immediately if the button is pressed during it.
  • If the player jumps early, return -1 instead of a time, print a warning, and make them redo that attempt.

Notice why this has to be a while loop and not a for loop: a false start must not use up one of the five attempts, so the counter only moves forward on a real result.

Reaction Timer
from machine import Pin
import time
import random
leds = [
Pin(28, Pin.OUT),
Pin(29, Pin.OUT),
Pin(30, Pin.OUT),
Pin(31, Pin.OUT),
Pin(32, Pin.OUT),
]
button = Pin(33, Pin.IN, Pin.PULL_DOWN)
def all_on(leds):
for led in leds:
led.value(1)
def all_off(leds):
for led in leds:
led.value(0)
def wait_for_release(button):
while button.value() == 1:
pass
def pressed_early(button, seconds):
steps = int(seconds * 100)
for i in range(steps):
if button.value() == 1:
return True
time.sleep(0.01)
return False
def measure_reaction(leds, button):
all_off(leds)
wait_for_release(button)
print("Get ready...")
if pressed_early(button, random.uniform(2, 5)):
return -1
all_on(leds)
start = time.ticks_ms()
while button.value() == 0:
pass
stop = time.ticks_ms()
all_off(leds)
return time.ticks_diff(stop, start)
times = []
attempt = 1
while attempt <= 5:
print()
print("Attempt", attempt)
result = measure_reaction(leds, button)
if result == -1:
print("Too early! That one does not count.")
else:
print("Reaction time:", result, "ms")
times.append(result)
attempt = attempt + 1
print()
best = times[0]
total = 0
for t in times:
total = total + t
if t < best:
best = t
print("Best:", best, "ms")
print("Average:", total // len(times), "ms")

Challenge (Optional): Two-Player Duel

  • Add a second button on GPIO 34 and light the array as before — whoever presses first wins the round.
  • Report both reaction times and the margin between them.
divider

Checkpoint

Your finished program should behave like this:

Terminal window
Attempt 1
Get ready...
Reaction time: 210 ms
Attempt 2
Get ready...
Too early! That one does not count.
Attempt 2
Get ready...
Reaction time: 260 ms
Best: 210 ms
Average: 235 ms
  • The array stays dark during "Get ready..." and lights all at once as the GO signal
  • Reaction times land somewhere around 150–400 ms — a reading of 2 ms means you were already holding the button
  • Pressing before GO prints the warning and repeats the same attempt number
  • After five real attempts, the best and average are reported
divider

Reflection

Answer the following questions before submitting your work.

  1. What does Pin.PULL_DOWN do, and what would an unpressed input pin read without it?
  2. Your functions in Activities 2.5 and 2.6 never used return. Why did measure_reaction() need one?
  3. A false start does not use up one of the five attempts. Explain how the while loop makes that possible, and why a for loop would have made it harder.
divider

Submit

Submit the required files to the appropriate dropbox.

Activity Complete