Fast clocks, model railroads, LED displays, and more

Code Walkthrough

Imports (lines 1-4) of Listing 1 are how Python brings in external libraries. Here are the ones I use:

  • pygame handles the graphics and key inputs on the Pi Zero.
  • datetime handles all of the functions to store and manipulate the time of the fast clock.
  • RPi.GPIO lets me access the physical GPIO pins on the Pi. The as gpio lets me access it later in Python with an easier name than typing RPi.GPIO every time.
  • time has functions that measure and control the passing of real time.

Listing 1

Fast Clock Script

001 import pygame
002 import datetime
003 import RPi.GPIO as gpio
004 import time
005
006 class sevenSegment:
007   def __init__ ( self ):
008     #64 -- middle
009     #32 -- lower right
010     #16 -- upper right
011     #8 -- top
012     #4 -- upper left
013     #2 -- lower left
014     #1 -- bottom
015
016     self.digits = list()
017     self.digits.append ( 0b00111111 )           #Zero
018     self.digits.append ( 0b00110000 )           #One
019     self.digits.append ( 0b01011011 )           #Two
020     self.digits.append ( 0b01111001 )           #Three
021     self.digits.append ( 0b01110100 )           #Four
022     self.digits.append ( 0b01101101 )           #Five
023     self.digits.append ( 0b01101111 )           #Six
024     self.digits.append ( 0b00111000 )           #Seven
025     self.digits.append ( 0b01111111 )           #Eight
026     self.digits.append ( 0b01111101 )           #Nine
027
028   def ledVal ( self , number ):
029     LED = 0
030     numString = str ( number )
031     for i in range ( 4 ):
032       digit = self.digits [ int ( numString [ i ] ) ]
033       digit <<= 7 * i
034       LED += digit
035     return LED
036
037 class fiftyfourfifty:
038   def __init__ ( self , clock , data ):
039     self.clock = clock
040     self.data = data
041
042     gpio.setmode(gpio.BCM)
043     gpio.setup ( clock , gpio.OUT )
044     gpio.setup ( data , gpio.OUT )
045
046   def pulseClock ( self ):
047     gpio.output ( self.clock , False )
048     time.sleep ( .01 )
049     gpio.output ( self.clock , True )
050     time.sleep ( .01 )
051     gpio.output ( self.clock , False )
052
053   def startPulse ( self ):
054     gpio.output ( self.data , True )
055     self.pulseClock()
056
057   def set ( self , LED ):
058     self.startPulse()
059     for i in range ( 34 ):
060       if ( LED & 1 ) == 1:
061         gpio.output ( self.data , True )
062       else:
063         gpio.output ( self.data , False )
064       LED >>= 1
065
066       self.pulseClock()
067     self.pulseClock()
068
069 class clock:
070   def __init__ ( self , screen , chip , display ):
071     self.screen = screen
072     self.chip = chip
073     self.display = display
074     self.dt = datetime.datetime.now()
075     self.second = datetime.timedelta ( 0 , 1 )  # One second interval
076     self.mode = "REAL TIME"
077     self.tickTime = 1000
078
079     pygame.font.init()
080     self.clockFont = pygame.font.SysFont ( "" , 300 )
081     self.labelFont = pygame.font.SysFont ( "" , 25 )
082     self.drawMode()
083
084   def tick ( self ):
085     self.dt += self.second
086     self.render()
087
088   def render ( self ):
089     timeSurf = self.clockFont.render ( "{0:02d}:{1:02d}:{2:02d}".format (
        self.dt.hour , self.dt.minute , self.dt.second ) , True , ( 255 , 0 , 0 ) )
090
091     self.screen.fill ( ( 0 , 0 , 0 ) )
092
093     offset = self.screen.get_width() / 2 - timeSurf.get_width() / 2
094     self.screen.blit ( timeSurf , ( offset , 100 ) )
095     self.drawMode()
096
097     self.chip.set (
          self.display.ledVal ( "{0:02d}{1:02d}".format (
          self.dt.hour , self.dt.minute ) ) )
098
099     pygame.display.flip()
100
101   def drawMode ( self ):
102     pygame.draw.rect ( self.screen , ( 0 , 0 , 0 ) , ( 0 , 450 , 100 , 30 ) , 0 )
103     mode = self.labelFont.render ( self.mode , True , ( 0 , 0 , 255 ) )
104     self.screen.blit ( mode , ( 0 , 450 ) )
105     pygame.display.flip()
106
107   def loop ( self ):
108     pygame.time.set_timer ( pygame.USEREVENT , self.tickTime )
109
110     looping = True
111     while looping:
112       for event in pygame.event.get():
113         if event.type == pygame.USEREVENT: self.tick()
114         if event.type == pygame.KEYDOWN:
115           if event.key == ord ( "1" ):
116             self.tickTime = 1000
117             self.mode = "1:1"
118             pygame.time.set_timer ( pygame.USEREVENT , self.tickTime )
119             self.drawMode()
120           elif event.key == ord ( "2" ):
121             self.tickTime = 500
122             self.mode = "2x"
123             pygame.time.set_timer ( pygame.USEREVENT , self.tickTime )
124             self.drawMode()
125           elif event.key == ord ( "4" ):
126             self.tickTime = 250
127             self.mode = "4x"
128             pygame.time.set_timer ( pygame.USEREVENT , self.tickTime )
129             self.drawMode()
130           elif event.key == ord ( "8" ):
131             self.tickTime = 125
132             self.mode = "8x"
133             pygame.time.set_timer ( pygame.USEREVENT , self.tickTime )
134             self.drawMode()
135           elif event.key == ord ( "0" ):
136             self.tickTime = 0
137             self.mode = "PAUSED"
138             pygame.time.set_timer ( pygame.USEREVENT , self.tickTime )
139             self.drawMode()
140           elif event.key == ord ( "r" ):
141             self.dt = datetime.datetime.now()
142             self.tickTime = 1000
143             self.mode = "REAL TIME"
144             pygame.time.set_timer ( pygame.USEREVENT , self.tickTime )
145             self.drawMode()
146           elif event.key == ord ( "q" ):
147             self.dt += datetime.timedelta ( 0 , 0 , 0 , 0 , 0 , 1 )
148             self.render()
149           elif event.key == ord ( "a" ):
150             self.dt -= datetime.timedelta ( 0 , 0 , 0 , 0 , 0 , 1 )
151             self.render()
152           elif event.key == ord ( "w" ):
153             self.dt += datetime.timedelta ( 0 , 0 , 0 , 0 , 1 )
154             self.render()
155           elif event.key == ord ( "s" ):
156             self.dt -= datetime.timedelta ( 0 , 0 , 0 , 0 , 1 )
157             self.render()
158           elif event.key == ord ( "e" ):
159             self.dt += datetime.timedelta ( 0 , 1 )
160             self.render()
161           elif event.key == ord ( "d" ):
162             self.dt -= datetime.timedelta ( 0 , 1 )
163             self.render()
164
165 clockPin = 14
166 dataPin = 15
167 chip = fiftyfourfifty ( clockPin , dataPin )
168 display = sevenSegment()
169
170 pygame.display.init()
171 screen = pygame.display.set_mode ( ( 800 , 480 ) )
172 clk = clock ( screen , chip , display )
173 clk.loop()

The sevenSegment class (lines 6-36) represents the LED segments in the remote displays.

The __init__ function (lines 7-26) shows comments where I note the mapping between bits in a Python variable and segments in the seven-segment display. Line 16 initializes a list to store bit patterns, then lines 17-26 define the bit patterns needed to display each digit.

In the ledVal function (lines 28-35), the 5450 is expecting a string of 34 bits to tell it what to display. ledVal takes a four-digit integer and translates it into a very large (34-bit) integer. This bit pattern represents the provided 4 digits in LED form.

Additionally, LED (line 29) is the integer that will store the result of the calculation. numString (line 30) is the provided number converted to a string. Line 31 sets up a loop to deal with each digit in the number individually, and line 32 retrieves the single-digit bit pattern from self.digits.

Because the first seven-segment display is on 5450 outputs 1-7, the next is on outputs 8-14, and so on. Line 33 shifts the retrieved bit pattern into the appropriate position so that it lines up with its associated display. Then, line 34 adds this to LED. When it's all done, line 35 returns LED, which will be an integer that represents the required bits (discrete LEDs) that will display those digits on the remote display.

The fiftyfourfifty class (lines 37-67) handles talking to the MM5450 LED driver chip via the GPIO port. Here, the __init__ function (lines 38-44) sets up the GPIO. Lines 39 and 40 take the function arguments clock and data and store them in their class equivalents. Line 42 tells the gpio library that I will be using chip numbering to address the pins. Lines 43 and 44 set both clock and data pins to outputs.

The pulseClock function (lines 46-51) sets the clock GPIO pin low (line 47), waits 1/100th of a second (line 48), sets the clock GPIO pin high (line 49), waits another 1/100th of a second (line 50), and then returns the clock to low (line 51). This function will be called whenever a clock pulse is needed.

The startPulse function (lines 53-55) sets data high (line 54) and then calls self.pulseClock (line 55). This step will send a digital 1 to the 5450.

Next, the set function (lines 57-67) sends a complete sequence to the 5450. It starts with a self.startPulse (line 58) and then loops over each bit in LED to send it to the 5450 (line 59). The LED & 1 on line 60 compares only the first bit of LED, which will either be 0 or 1. Lines 61 and 63 set the data pin high or low appropriately. Finally, line 64 shifts the contents of the LED variable one bit to the right. After pulsing the clock (line 66), the entire process repeats. Once the loop has sent all 34 bits of data to the 5450, self.pulseClock (line 67) sends one more clock to load the data.

The clock class (lines 69-163) maintains the fast clock's time, displays the clock on the Pi Zero's screen, and tracks the update rate (how fast the fast clock runs).

The __init__ class (lines 70-82) copies the function parameters into class variables. Line 74 initializes self.dt as the current date and time from datetime.datetime.now. Line 75 creates a datetime.timedelta object containing one second. Then, timedelta objects can be added and subtracted from datetime objects as needed. Line 76 initializes self.mode to "REAL TIME", and line 77 sets self.tickTime to 1000 (milliseconds). This sets up the clock to run in real time (and at real speed) until adjusted otherwise.

Line 79 initializes the pygame.font module, then lines 80 and 81 create the fonts used to draw on the screen. The values 300 and 25 are the size in pixels the font should draw onscreen. Finally, line 82 calls self.drawMode to display the current clock mode in the corner of the screen.

The tick function (lines 84-86) adds one second to the fast clock (line 85) and then redraws the screen (self.render). Next, the render function (lines 88-99) is responsible for drawing the clock on the screen whenever it gets updated. It also sends updated values to the remote displays.

Line 89 calls self.clockFont.render to create a PyGame surface with the current time. Line 91 clears the screen by filling it with black, then line 93 calculates the offset needed to center the clock on the screen. Next, self.screen.blit draws timeSurf onto the screen, and self.drawMode on line 95 displays the current clock speed in the corner of the display.

Line 97 sends the fast clock hour and minute to the 5450 to be displayed on all of the remote displays. Finally, line 99 flips the display so all of the changes are visible on the Pi Zero's monitor.

The drawMode function (lines 101-105) draws the current clock speed in the corner of the display. Line 102 erases the old mode on the screen by drawing a black rectangle over the corner. Line 103 renders the mode into the variable also called mode. Then, line 104 blits the mode surface onto the screen. Line 105 flips the display so everything is displayed on screen.

The loop function (lines 107-163) is the main loop of the fast clock. Line 108 starts a PyGame timer to fire every self.tickTime milliseconds. Initially this will be 1,000 for real-time updates. Line 110 sets looping to True, then line 111 starts a loop watching for PyGame events (line 112). Line 113 calls self.tick every time the PyGame timer is fired.

Line 114 checks whether the PyGame event is a keypress. If so, one of the if/elif blocks on lines 115-164 will handle it. If the key is 1, 2, 4, 8, or 0, then the speed of the fast clock is changing. I'll look at "1" (lines 115-119) for this example. Line 116 sets self.tickTime to the new update rate. Line 117 sets self.mode to the new clock mode. Finally, line 118 updates the PyGame timer to run at the new update rate, and line 119 calls self.drawMode so the new speed is shown onscreen. Lines 120 through 139 work the same way with the different speeds.

Lines 140-145 handle the special case for real time. Line 141 updates self.dt – the fast clock's current time – to the current system time (real time). Lines 146-163 adjust the hours, minutes, or seconds of the clock so that it can be set to any time. To add an hour, for example, a datetime.timedelta object is initialized holding 1 hour and adds it to self.dt (line 147). Then, self.render is called (line 148) to show the new value. The remainder of the function works the same way with different keys, adding/subtracting hours, minutes, and seconds.

Starting Everything

So far, I've defined a number of functions and classes, but nothing has been told to do anything. That all happens on lines 165-173.

First, I define the clock and data pins (lines 165 and 166). Then, I create an instance of the fiftyfourfifty class, passing in the clock and data pins (line 167). Line 168 creates an instance of sevenSegment.

Lines 170-171 initialize the PyGame display. Then, line 172 creates an instance of the fast clock, passing in the screen, chip, and display variables so the class can access them directly. Finally, clk.loop starts the clock running.

Buy this article as PDF

Express-Checkout as PDF

Pages: 8

Price $2.95
(incl. VAT)

Buy Raspberry Pi Geek

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content