Over the last few weeks (months maybe?) I've been playing around with 7-segment displays and an old clock display I had in my pile of hoarded things.
My most recent blog post in the series was a throwback to the 1980's: The VCR Emulator
But now here comes the moment we've all been waiting for (right?) ---> this is it! the finale of all of these seemingly meaningless blog posts... the project I was aiming for! It's a Crazy Countdown Timer!!
Complete with wires to cut! kinda like those stereotypical movie time bombs where the hero of the story has to cut the correct wire to save the world... that might have been a 1980s thing too In any case, for reasons of political correctness and to avoid calls from important government people, I gave this project a toned-down PG kind of title.
Now, those of you who have been paying very careful attention for the last dozen years might have noticed a similarity to a project I posted about, right here on element14, nearly exactly 11 years ago!
In fact, this was MY VERY FIRST PROJECT on element14 !! Woot!!, for that year's "Summer of Design" contest, using the XL-Star board! How cool is that? It's also the same clock display module
Back then I was just getting back into electronics, and I think I had just purchased a few Texas Instruments MSP430 boards, which were surprisingly just $4.30 (Canadian even, if I recall correctly) with free shipping to boot, but I digress (as I so often do), and I had just started playing around with firmware. Theirs was a fairly complicated system at the time, with the IDE being in Eclipse still. I think the XL-Star was similarly complex to program -- check out the source code!
I didn't win any prize for my countdown timer project, but because I was so new to the electronics hobby and dev boards, I had been asking a whole lot of questions that summer, and that led me to have more posts than most people I guess and as a result I won the very first "member of the month" prize! That turned out to be somewhat life-changing, as the care package element14 sent me included a few different dev boards, including my very first Arduino-compatible board. That made dev board programming so much easier, and made the hobby much more enjoyable for me. Ever since then I've been mostly Arduino based for my hobby projects.
So this project was a really cool throw-back for me. What was also very interesting to see is how much easier it is to do a nearly identical project today, using Python on the Pi Pico. The new version doesn't include a bump sensor, but even that should be fairly easy to add if desired.
In case you're curious, this is what happens when you cut the yellow wire:
The setup for this project is the same as my last few blog posts: The VCR Emulator and The Countdown Timer.
The biggest change is that I added 3 wires, which are set up as switches that are "on" by default and "off" when cut (using the built-in pull-down resistors). The source code gives the pins to connect those wires to - GP 7, 8, and 9.
I also changed the timer to just count ticks instead of trying to be precise for time - this allows us to more easily speed up the count-down clock when the wrong wire is cut.
As was hinted at in my Pi Pico Sound blog post, I also added a beeper for when the timer runs out.
Here is the source code:
#
# The Crazy Timer!
#
# The timer starts at 3 minutes
#
#
from machine import Pin, Timer, PWM
import time
# For my clock display, the common wires connects to pins that must be set HIGH to enable each position
# -> Note that this means we have to turn a pin "off" to light the segment LED, and "on" to turn it off
#
# You can test the display by using a small 3.3v button battery (CR2032 seems cheap and plentiful)
# Hopefully the display you have will have a model number you can look up to find the pinout.
# Define the segment GPIO connections
# hook up the segments as per the defined constants below
# Use a current limiting resistor for each segment (you'll need 7!)
# The resistors are also necessary to keep a constant brightness on the display
SEG_A_PIN = 13
SEG_B_PIN = 19
SEG_C_PIN = 17
SEG_D_PIN = 16
SEG_E_PIN = 12 # oops, I put the display too close to the Pi Pico, so pin 15 is covered.
SEG_F_PIN = 14
SEG_G_PIN = 18
# The clock display has 4 digit positions
# I'm calling them positions, as it could be hr:mm or mm:ss
POSITION_1 = 10
POSITION_2 = 11
POSITION_3 = 20
POSITION_4 = 21
DIVIDER_COLON = 22
# Python allows us to define global variables anywhere all willy-nilly,
# but for clarity lets define them here at the top like good little programmers
# The type is here just for clarity too - Python allows us to change it at any time
DIGITS :[[Pin]] = []
BLANK_DIGIT : [Pin] = []
POSITIONS : [Pin] = []
# Add the crazy wires
STOP_WIRE_PIN = 7 # green wire, stops the timer
ZERO_WIRE_PIN = 8 # yellow wire, sets time down to 1 second
PANIC_WIRE_PIN = 9 # red wire, speeds up the timer
stop_wire : Pin
zero_wire : Pin
panic_wire : Pin
BUZZER_PIN = 6 # Piezo buzzer + is connected to GP6, - is connected to the GND right beside GP6
buzzer : PWM
# time.time() returns float number of seconds since epoch
MINUTE : float = 60
HOUR = MINUTE * 60
TOTAL_TIME = 3 * MINUTE
tickerTimer = Timer(-1) # For more info on Timers see https://docs.micropython.org/en/latest/library/machine.Timer.html
time_left = TOTAL_TIME
ticksCounted = 0
colon_visible = False
display_buffer : [[Pin]] = [BLANK_DIGIT, BLANK_DIGIT, BLANK_DIGIT, BLANK_DIGIT] # this will hold the 4 digits currently being displayed - the display needs continuous refreshing in order to work.
flashing = False
vcr_mode = False
timer_stopped = False
timer_panic = False
timer_zero = False
music_played = False
def setup():
# Define each segment
SEG_A = Pin(SEG_A_PIN, Pin.OUT)
SEG_B = Pin(SEG_B_PIN, Pin.OUT)
SEG_C = Pin(SEG_C_PIN, Pin.OUT)
SEG_D = Pin(SEG_D_PIN, Pin.OUT)
SEG_E = Pin(SEG_E_PIN, Pin.OUT)
SEG_F = Pin(SEG_F_PIN, Pin.OUT)
SEG_G = Pin(SEG_G_PIN, Pin.OUT)
# Define which segments make up each digit
DIGIT_0 = [SEG_A, SEG_B, SEG_C, SEG_D, SEG_E, SEG_F ]
DIGIT_1 = [ SEG_B, SEG_C ]
DIGIT_2 = [SEG_A, SEG_B, SEG_D, SEG_E, SEG_G]
DIGIT_3 = [SEG_A, SEG_B, SEG_C, SEG_D, SEG_G]
DIGIT_4 = [ SEG_B, SEG_C, SEG_F, SEG_G]
DIGIT_5 = [SEG_A, SEG_C, SEG_D, SEG_F, SEG_G]
DIGIT_6 = [SEG_A, SEG_C, SEG_D, SEG_E, SEG_F, SEG_G]
DIGIT_7 = [SEG_A, SEG_B, SEG_C ]
DIGIT_8 = [SEG_A, SEG_B, SEG_C, SEG_D, SEG_E, SEG_F, SEG_G]
DIGIT_9 = [SEG_A, SEG_B, SEG_C, SEG_D, SEG_F, SEG_G]
# Note that we are not limited to decimal digits. We could continue to add A through F for hexadecimal
POS_1 = Pin(POSITION_1, Pin.OUT)
POS_2 = Pin(POSITION_2, Pin.OUT)
POS_3 = Pin(POSITION_3, Pin.OUT)
POS_4 = Pin(POSITION_4, Pin.OUT)
global divider_colon
divider_colon = Pin(DIVIDER_COLON, Pin.OUT)
global DIGITS
DIGITS = [DIGIT_0, DIGIT_1, DIGIT_2, DIGIT_3, DIGIT_4, DIGIT_5, DIGIT_6, DIGIT_7, DIGIT_8, DIGIT_9, BLANK_DIGIT]
global POSITIONS
POSITIONS = [POS_1, POS_2, POS_3, POS_4]
global stop_wire, zero_wire, panic_wire
stop_wire = Pin(STOP_WIRE_PIN, Pin.IN, Pin.PULL_DOWN)
zero_wire = Pin(ZERO_WIRE_PIN, Pin.IN, Pin.PULL_DOWN)
panic_wire = Pin(PANIC_WIRE_PIN, Pin.IN, Pin.PULL_DOWN)
global buzzer
buzzer = PWM(Pin(BUZZER_PIN, Pin.OUT))
soundOff()
displayOff()
def showPosition(position):
positionsOff()
position.on()
def displayDigit(digit):
#start by turning off all the segments
segmentsOff()
for segment in digit:
segment.off() # gpio "off" turns on the LED
def positionsOff():
for pos in POSITIONS:
pos.off()
def segmentsOff():
for segment in DIGITS[8]:
segment.on() # gpio "on" turns off the LED
def displayOff():
# turn off all the digit positions
positionsOff()
# turn off all the segments
segmentsOff()
def timeTick(timer):
global time_left, ticksCounted, TOTAL_TIME, colon_visible, flashing
if timer_stopped :
colon_visible = True
return
colon_visible = not colon_visible
if vcr_mode or time_left == 0 : return
# For this Crazy Timer we will now be counting ticks instead of using the time() function.
# This allows us to increase the countdown speed when the wrong wire is disconnected
# We are not concerned about time accuracy for this specific project.
ticksCounted = ticksCounted + 1
time_left : int = TOTAL_TIME - (ticksCounted // 2)
if time_left <= 0 or timer_zero :
time_left = 0
resetTickerTimer(2)
flashing = True
showTime(time_left)
def startSecondsTicker():
global tickerTimer
tickerTimer.init(freq=2, mode=Timer.PERIODIC, callback=timeTick)
def resetTickerTimer(frequency) :
global tickerTimer
tickerTimer.deinit()
tickerTimer.init(freq=frequency, mode=Timer.PERIODIC, callback=timeTick)
def displayRefresh():
global display_buffer, POSITIONS, colon_visible, divider_colon, flashing
if not colon_visible and flashing:
displayOff()
divider_colon.on() # turns the LEDs off
return
led_on_time = 0.0025
# too much on time will cause flickering - the display must keep each position lit up just long enough to repeat it again fast enough to fool the eye.
for index, position in enumerate(POSITIONS) :
showPosition(position)
displayDigit(display_buffer[index])
time.sleep(led_on_time)
# You can use a bigger delay here for debugging purposes, to make it easier to see the digits being shown in turn.
displayOff() # otherwise it will linger on the last digit, making it brighter than the rest
if colon_visible :
divider_colon.off() # turns the LEDs on
time.sleep(led_on_time)
divider_colon.on() # turns the LEDs off
def showTwelve():
display_buffer[0] = DIGITS[1]
display_buffer[1] = DIGITS[2]
display_buffer[2] = DIGITS[0]
display_buffer[3] = DIGITS[0]
def showTime(total_seconds):
global display_buffer
#print("show time total seconds = ", total_seconds)
if total_seconds <= 0 :
display_buffer[0] = DIGITS[0]
display_buffer[1] = DIGITS[0]
display_buffer[2] = DIGITS[0]
display_buffer[3] = DIGITS[0]
return
# convert seconds to hours, minutes, and seconds
hours = total_seconds // 3600 # the // is "floor division" which avoids fractions
leftover = total_seconds % 3600
minutes = leftover // 60
seconds = leftover % 60
# print("time = ", hours, ":", minutes, ":", seconds)
if hours > 0 :
first_number = hours
second_number = minutes
else :
first_number = minutes
second_number = seconds
if hours > 99 :
display_buffer[0] = DIGITS[8]
display_buffer[1] = DIGITS[8]
display_buffer[2] = DIGITS[8]
display_buffer[3] = DIGITS[8]
else :
if first_number > 9 :
display_buffer[0] = DIGITS[first_number // 10]
else :
display_buffer[0] = BLANK_DIGIT
display_buffer[1] = DIGITS[first_number % 10]
display_buffer[2] = DIGITS[second_number // 10]
display_buffer[3] = DIGITS[second_number % 10]
def soundOff() :
global buzzer
buzzer.duty_u16(0) # loudness set to 0 = sound off
def playNote(frequency, duration, pause) :
global buzzer
buzzer.duty_u16(5000) # adjust loudness: smaller number is quieter.
buzzer.freq(frequency)
time.sleep(duration)
buzzer.duty_u16(0) # loudness set to 0 = sound off
time.sleep(pause)
def playMusic() :
#notes = [440, 494, 523, 587, 659, 698, 784]
#notes = [440, 523, 659, 784]
notes = [523, 440, 494, 523, 440, 494, 523]
for note in notes :
playNote(note, 0.2, 0.01)
# Start main code
setup()
startSecondsTicker()
if vcr_mode :
flashing = True
showTwelve()
else :
showTime(TOTAL_TIME)
# Using a Timer to refresh the display seems to cause it to hang after a while
# but using this loop we can go nuts refreshing the display with all the spare clock cycles
while True:
displayRefresh()
# check the wires - the following checks if they were disconnected (and pull-down makes them LOW)
if not stop_wire.value() :
timer_stopped = True
elif not zero_wire.value() :
timer_zero = True
elif not panic_wire.value() :
if not timer_panic : # only do this once
timer_panic = True
resetTickerTimer(20)
if time_left == 0 and not music_played :
music_played = True
playMusic()
Just for fun, here's a snippet of the source code from the original project (so you can see how much nicer the new Python code looks!). The full source code is in the zip file that was included in the original project.
TPM1SC = 0; // Stop the timer
TPM1SC_TOF = 0; // Clear the TOF flag
TPM1C0SC = 0; // Configure the timer
TPM1MOD = count; // the number of 32 microsecond increments to wait
TPM1CNT = 0;
TPM1SC = 0x0f; // Restart the timer: CLKS = 01 (Use BUSCLK), PS=111 (Divide-by-128)
// Frequency = fbus / 128, so period = 128 / 4 MHz = 32 microseconds
/* Keep showing the display while waiting for timer to expire */
while (!_TPM1SC.Bits.TOF) {
// Channel select LEDs are being used to indicate status of the crazy wires.
// when the wires are connected to GND, PTE2,3, and 4 are at 0
// When wires are disconnected from GND, they go to 1
if ( !PTED_PTED2 ) {
PTAD_PTAD5 = 0; // "FreeFall" LED on
timer_stopped = 0;
}
else {
PTAD_PTAD5 = 1; // off - BLUE wire was disconnected = stop the timer
timer_stopped = 1;
}
if ( !PTED_PTED3 ) {
PTAD_PTAD6 = 0; // "Transient" LED on
}
else {
PTAD_PTAD6 = 1; // off - YELLOW wire was disconnected = set time to 0
hours = 0;
minutes = 0;
seconds = 0;
}
if ( !PTED_PTED4 ) {
PTAD_PTAD7 = 0; // "orientation" LED on
}
else {
PTAD_PTAD7 = 1; // off - RED wire was disconnected = panic
flash = 1;
}
Edit June 16th:
javagoza Had a really fun idea of randomizing the function of the wires.
Here's the code that can be used for that - simply replace the existing 3 lines of code that define the action pins (the definition of STOP_WIRE_PIN, ZERO_WIRE_PIN, and PANIC_WIRE_PIN).
import random
def shuffleArray(theArray):
numberOfItems = len(theArray)
startArray = theArray.copy() # duplicate the array so it doesn't break the original array. ie, no side effects.
newArray = []
for iterate in range(numberOfItems): # iterate for as many items as there are in the original array
numberOfItemsLeft = len(startArray)
index = random.randint(0,numberOfItemsLeft-1) # pick a random index to pull the item from
newArray.append(startArray.pop(index)) # add that item to the new array, and remove it from the starting array
return newArray
# Add the crazy wires
# To randomize which pin does which, start with the 3 pins
ACTION_PINS = [7, 8, 9]
randomizedActionPins = shuffleArray(ACTION_PINS)
STOP_WIRE_PIN = randomizedActionPins[0] # stops the timer
ZERO_WIRE_PIN = randomizedActionPins[1] # sets time down to 1 second
PANIC_WIRE_PIN = randomizedActionPins[2] # speeds up the timer
To add more wires to make it even harder to guess, simply add more GPIO wires and add their positions to the ACTION_PINS array. Only the first three of the GPIO wires in the randomized array will be the 3 action pins - the rest will be decoys.
Enjoy!
-Nico