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