/* Pinball clock controller: - for the arduino duemilenove - read and decode the WWVB time signal - drive the relays to show the time Written by Mark Gibson */ #define Debug 1 // enable debug messages to serial port // values returned from the GetNextBit routine #define Zero 0 #define One 1 #define Position 2 // pulse charactaristics #define StdPulseWidth 25 // coil on for 25ms #define StdPulsePeriod 150 // coil fires every 150 ms #define HourChimePeriod 1000 // hour chime fires every second #define StdPulseInterval (StdPulsePeriod - StdPulseWidth) #define HourChimeInterval (HourChimePeriod - StdPulseWidth) // delay between clock samples #define SampleDelay 50 // parameters for the SampleSignal routine #define NumSamples 8 #define LowThreshold 105 #define HighThreshold 670 // digital IOs used by the various coils #define TimeZoneOut 2 // pin used to read the time zone switch #define TenHourReset 3 #define OneHourReset 4 #define TenMinReset 5 #define OneMinReset 6 #define StepUnitDecr 7 #define OneMinIncr 8 #define TenMinIncr 9 #define OneHourIncr 10 #define Chime 11 #define StepUnitIncr 12 #define LEDPin 13 // Analog inputs #define RadioReceiverInput 0 #define TimeZoneBit0 5 #define TimeZoneBit1 4 #define TimeZoneBit2 3 // Error threshold for radio receiver // or, the number of bad readings can be tolerated before resetting #define TimeErrorThreshold 2 // global variables int samples[NumSamples]; // last few analog input values int Hours; // = 12; int Minutes; // = 55; int PrevDaylightSavingsTime; int DaylightSavingsTime; int NextHours; int NextMinutes; int TimeErrorCount; int PrevTimeZone; int TimeZone; // Are we there yet? boolean ResetDone = false; // Delay between LCD messages #define lcdDelay 2500 // Clear the serial LCD screen void clearLCD(){ Serial.print(0xFE, BYTE); //command flag Serial.print(0x01, BYTE); //clear command. } // Put the cursor at the beginning of the 1st line void selectLineOne(){ Serial.print(0xFE, BYTE); //command flag Serial.print(128, BYTE); //position } // Put the cursor at the beginning of the 2nd line void selectLineTwo(){ Serial.print(0xFE, BYTE); //command flag Serial.print(192, BYTE); //position } // Dump two strings to the LCD void printTwoLines (char String1[], char String2[]) { clearLCD (); selectLineOne (); Serial.print (String1); selectLineTwo (); Serial.print (String2); } // Print a string one character at a time followed by a space void spellString (char Str[], int len) { int i; for (i = 0; i < len; i++) { Serial.print (Str[i]); delay (60); } Serial.print (" "); } // Show a gratuitous banner void DisplayBanner () { int line1 = 128; int line2 = 192; int i; clearLCD (); Serial.print (0xFE, BYTE); // command flag Serial.print (line1+2, BYTE); // cursor position: line 1 space 2 spellString ("The", 3); spellString ("Atomic", 6); Serial.print (0xFE, BYTE); // command flag Serial.print (line2+1, BYTE); // cursor position: line 2 space 1 spellString ("Pinball", 7); spellString ("Clock", 5); //delay (2*lcdDelay); } //--------------------------------------- // print the current time on the 1st line //--------------------------------------- void PrintTimeOnLineOne () { clearLCD (); selectLineOne (); Serial.print (" Time is "); if (Hours == 0) { Serial.print ("??:??"); } else { Serial.print (Hours, DEC); Serial.print (":"); if (Minutes < 10) { Serial.print ("0"); } Serial.print (Minutes, DEC); } delay (2*lcdDelay); } //---------------------------------------------------- // Sample an analog pin input avoiding the transitions //---------------------------------------------------- boolean StableSample (int AnalogInput) { int i; int average = (LowThreshold + HighThreshold)/2; int total = average * NumSamples; int SampleIndex = 0; int val; // initialize to an intermediate value for (i = 0; i < NumSamples; i++) samples[i] = average; // wait for a steady high or low sample while ((average > LowThreshold) && (average < HighThreshold)) { total = total - samples[SampleIndex]; samples[SampleIndex] = analogRead(AnalogInput); total = total + samples[SampleIndex]; SampleIndex = SampleIndex + 1; if (SampleIndex >= NumSamples) SampleIndex = 0; // roll over the index average = total >> 3; // Same as total / NumSamples of 8 } // return the logical value of the stable sample if (average < LowThreshold) return (false); return (true); } //---------------------------------------------------------- // Read the time zone switch to know what time zone we're in //---------------------------------------------------------- void ReadTimeZone (int message) { boolean TimeZoneBit0val; boolean TimeZoneBit1val; boolean TimeZoneBit2val; // Drive power to the Time Zone BCD switch through a digital pin digitalWrite (TimeZoneOut, HIGH); // Read the three pins of the Time Zone BCD switch through analog inputs TimeZoneBit0val = StableSample(TimeZoneBit0); TimeZoneBit1val = StableSample(TimeZoneBit1); TimeZoneBit2val = StableSample(TimeZoneBit2); // All done. Cut power to the Time Zone BCD switch digitalWrite (TimeZoneOut, LOW); // Remember the previous time zone in case the user changed the switches PrevTimeZone = TimeZone; TimeZone = ( (TimeZoneBit0val == HIGH) | ((TimeZoneBit1val == HIGH) << 1) | ((TimeZoneBit2val == HIGH) << 2)); // Pacific Standard time is 8 hours back, but there's no 8 on the BCD switch if (TimeZone == 0) TimeZone = 8; // reuturn without printing the time zone if (message == 0) return; // dump the current time zone if (Debug) { clearLCD (); selectLineOne (); Serial.print (" Time Zone from"); selectLineTwo (); Serial.print ("dial switch is "); Serial.print (TimeZone, DEC); delay (lcdDelay); } } //------------------------------------------------------------------- // Return the next bit in the signal by measuring the low pulse width //------------------------------------------------------------------- int NextBit () { int SampleCount = 1; // wait for a low sample while (StableSample(RadioReceiverInput) == true) { delay (SampleDelay); } // we've entered a low pulse and taken the first sample // measure the duration of the pulse delay (SampleDelay); while (StableSample(RadioReceiverInput) == false) { SampleCount = SampleCount + 1; delay (SampleDelay); } // back in the high pulse. // return the logical value or the position marker indicator if (SampleCount < 6) return (Zero); if (SampleCount < 12) return (One); return (Position); } //---------------------------------- // Wait for the start of a new frame //---------------------------------- void AwaitNewFrameMarker () { // At the top of each minute, a new frame starts with // a pair of consecutive low 800ms pulses. Wait for those. // The last two bits received before the Frame Marker determine // Daylight Savings vs Standard time. Record those too. int PreviousValue3; // value 3 cycles ago int PreviousValue2; // value 2 cycles ago int PreviousValue1; // previous value int CurrentValue; if (Debug) { delay (2*lcdDelay); printTwoLines ("Waiting for the", "next time signal"); delay (3*lcdDelay); DisplayBanner (); } PreviousValue3 = NextBit(); PreviousValue2 = NextBit(); PreviousValue1 = NextBit(); CurrentValue = NextBit(); // wait for consecutive position markers while ((PreviousValue1 != Position) | (CurrentValue != Position)) { PreviousValue3 = PreviousValue2; PreviousValue2 = PreviousValue1; PreviousValue1 = CurrentValue; CurrentValue = NextBit(); } if (Debug) { printTwoLines (" Receiving the", "WWVB time signal"); } // Set Daylight Saving Time // Note: The values 2 cycles before the frame marker (bits 57 & 58) indicate // daylight savings time: // Bit 57 Bit 58 // 0 0 Standard Time // 1 0 Change from Standard Time to Daylight Savings Time today at 2am // 1 1 Daylight Savings Time // 0 1 Change from Daylight Savings Time to Standard Time today at 2am // To simplify, just use bit 57 to indicate Daylight Savings Time. It will be wrong // from midnight to 2am on the days of transition. // When we get here we've found a new frame marker. PreviousValue3 has bit 57 // of the time transmission. PrevDaylightSavingsTime = DaylightSavingsTime; DaylightSavingsTime = 0; if (PreviousValue3 == One) DaylightSavingsTime = 1; } //---------------- // GetTime routine //---------------- boolean GetTime () { // This routine waits for the beginning of the next frame // and decodes the current time. The decode takes the first // 20 seconds of each minute leaving 40 seconds to drive // the drum units and call this routine again. // Figure out what time zone we're in ReadTimeZone (0); // don't print the time zone // If the user changed the time zone switch, punt and start over if ((ResetDone == true) && (PrevTimeZone != TimeZone)) { return false; } // Clear the time Hours = 0; Minutes = 0; digitalWrite(LEDPin, LOW); // sets the LED off // park here until the next frame starts AwaitNewFrameMarker(); // new frame started. decode minutes and hours // 40 minute bit digitalWrite(LEDPin, HIGH); // sets the LED on if (NextBit() == One) Minutes = Minutes + 40; // 20 minute bit digitalWrite(LEDPin, LOW); // sets the LED off if (NextBit() == One) Minutes = Minutes + 20; // 10 minute bit digitalWrite(LEDPin, HIGH); // sets the LED on if (NextBit() == One) Minutes = Minutes + 10; digitalWrite(LEDPin, LOW); // sets the LED off NextBit(); // skip one bit // 8 minute bit digitalWrite(LEDPin, HIGH); // sets the LED on if (NextBit() == One) Minutes = Minutes + 8; // 4 minute bit digitalWrite(LEDPin, LOW); // sets the LED off if (NextBit() == One) Minutes = Minutes + 4; // 2 minute bit digitalWrite(LEDPin, HIGH); // sets the LED on if (NextBit() == One) Minutes = Minutes + 2; // 1 minute bit digitalWrite(LEDPin, LOW); // sets the LED off if (NextBit() == One) Minutes = Minutes + 1; // Position bit digitalWrite(LEDPin, HIGH); // sets the LED on if (Debug) { if (NextBit() != Position) { printTwoLines ("Biffed receiving", "time position 1!"); } } else { NextBit(); } digitalWrite(LEDPin, LOW); // sets the LED off NextBit(); // skip two more bits digitalWrite(LEDPin, HIGH); // sets the LED on NextBit(); // 20 hour bit digitalWrite(LEDPin, LOW); // sets the LED off if (NextBit() == One) Hours = Hours + 20; // 10 hour bit digitalWrite(LEDPin, HIGH); // sets the LED on if (NextBit() == One) Hours = Hours + 10; digitalWrite(LEDPin, LOW); // sets the LED off NextBit(); // skip one bit // 8 hour bit digitalWrite(LEDPin, HIGH); // sets the LED on if (NextBit() == One) Hours = Hours + 8; // 4 hour bit digitalWrite(LEDPin, LOW); // sets the LED off if (NextBit() == One) Hours = Hours + 4; // 2 hour bit digitalWrite(LEDPin, HIGH); // sets the LED on if (NextBit() == One) Hours = Hours + 2; // 1 hour bit digitalWrite(LEDPin, LOW); // sets the LED off if (NextBit() == One) Hours = Hours + 1; // Position bit digitalWrite(LEDPin, HIGH); // sets the LED on if (Debug) { if (NextBit() != Position) printTwoLines ("Biffed receiving", "time position 2!"); } else { NextBit (); } digitalWrite(LEDPin, LOW); // sets the LED off if (0) { clearLCD (); selectLineOne (); Serial.print ("Daylight Savings"); selectLineTwo (); Serial.print ("is: "); Serial.print (PrevDaylightSavingsTime, DEC); Serial.print (", was "); Serial.print (DaylightSavingsTime, DEC); delay (lcdDelay); } // Convert UTC hours to local hours Hours = Hours - TimeZone + DaylightSavingsTime; // Correct the hour if (Hours < 0) Hours = Hours + 24; if (Hours > 12) Hours = Hours - 12; // return good status if we're still resetting or we got the expected time if ((ResetDone == false) || (Hours == NextHours) && (Minutes == NextMinutes)) { return true; } // Didn't read the expected time - dump received and expected results if (Debug) { clearLCD (); selectLineOne (); Serial.print("Error: got "); Serial.print (Hours, DEC); Serial.print (":"); if (Minutes < 10) { Serial.print ("0"); } Serial.print (Minutes, DEC); selectLineTwo (); Serial.print ("expected "); Serial.print (NextHours, DEC); Serial.print (":"); if (NextMinutes < 10) { Serial.print ("0"); } Serial.print (NextMinutes, DEC); delay (lcdDelay); } // got a bad reading somehow, return the expected time and bad status Hours = NextHours; Minutes = NextMinutes; return false; } //--------------------------- // set the expected next time //--------------------------- void SetNextTime () { // if (Debug) Serial.println("Entered SetNextTime routine."); // Figure out the time to expect in the next minute NextMinutes = Minutes + 1; NextHours = Hours; if (NextMinutes == 60) { NextMinutes = 0; NextHours = NextHours + 1; if (NextHours == 13) { NextHours = 1; } } // drive the time to the LCD if (Debug) { clearLCD (); // print the next expected time on the 2nd line first // so the 1st line can persist while the "Waiting..." message shows. selectLineTwo (); // print the current time on the 1st line PrintTimeOnLineOne (); // Print the error count since the last good time reading selectLineTwo (); if (TimeErrorCount == 0) { Serial.print (" No errors"); } else if (TimeErrorCount == 1) { Serial.print (" 1 error!"); } else { Serial.print (" "); Serial.print (TimeErrorCount, DEC); Serial.print (" errors!"); } } } //---------------------------------- // send pulses to the specified coil //---------------------------------- void PulseCoil (int Coil, int Count) { int i; // then delay and pulse more if necessary for (i = 0; i < Count; i++) { digitalWrite (Coil, HIGH); // coil on delay (StdPulseWidth); // wait... digitalWrite (Coil, LOW); // coil off delay (StdPulseInterval); // wait... } } //-------------------------------------------------- // pulse the minute coil and the chime coil together //-------------------------------------------------- void PulseMinuteWithChime () { digitalWrite (OneMinIncr, HIGH); // minute and digitalWrite (Chime, HIGH); // chime on delay (StdPulseWidth); // wait... digitalWrite (OneMinIncr, LOW); // minute and digitalWrite (Chime, LOW); // chime off } //--------------------------------------------------- // pulse all drum unit coils 9 times to set them to 0 //--------------------------------------------------- void ResetDrumUnits () { int i; for (i = 0; i < 5; i++) { // pick a pattern that looks fun while resetting PulseCoil (TenHourReset, 1); PulseCoil (TenMinReset, 1); PulseCoil (OneHourReset, 1); PulseCoil (OneMinReset, 1); PulseCoil (Chime, 1); PulseCoil (OneHourReset, 1); PulseCoil (TenMinReset, 1); PulseCoil (TenHourReset, 1); PulseCoil (OneMinReset, 1); PulseCoil (Chime, 1); } PulseCoil (StepUnitDecr, 9); } //---------------------------------------------- // Reset the drum units and set the current time //---------------------------------------------- void ResetAndSetTime () { int OneMin; // Ones digit for current time minutes int TenMin; // Tens digit for current time minutes if (Debug) { printTwoLines ("Resetting clock", "& starting over"); delay (lcdDelay); } // initialize stuff again ResetDone = false; TimeErrorCount = 0; DaylightSavingsTime = 0; PrevDaylightSavingsTime = 0; ReadTimeZone(1); // print the time zone PrevTimeZone = TimeZone; //printTwoLines (" The Atomic"," Pinball Clock"); DisplayBanner (); // reset the drum units ResetDrumUnits (); // show the unknown time on the LCD PrintTimeOnLineOne (); // set the current time GetTime (); // set the time on the clock // break out a digit for each drum unit OneMin = Minutes; while (OneMin > 9) OneMin = OneMin - 10; TenMin = Minutes/10; // Show the received time PrintTimeOnLineOne (); // Count out to the minute about to show on the minute reel PulseCoil (StepUnitIncr, OneMin); // and count down again PulseCoil (StepUnitDecr, OneMin); // Show the time on the reels PulseCoil (OneHourIncr, Hours); PulseCoil (TenMinIncr, TenMin); PulseCoil (OneMinIncr, OneMin); // Chime 5 times to show that reset is done PulseCoil (Chime, 5); // Figure out the time to expect in the next minute SetNextTime (); // All done ResetDone = true; } //-------------------- // Initialization code //-------------------- void setup () { Hours = 0; // set the digital pins as output pinMode(TimeZoneOut, OUTPUT); pinMode(TenHourReset, OUTPUT); pinMode(OneHourReset, OUTPUT); pinMode(TenMinReset, OUTPUT); pinMode(OneMinReset, OUTPUT); pinMode(OneMinIncr, OUTPUT); pinMode(TenMinIncr, OUTPUT); pinMode(OneHourIncr, OUTPUT); pinMode(Chime, OUTPUT); pinMode(StepUnitIncr, OUTPUT); pinMode(StepUnitDecr, OUTPUT); pinMode(LEDPin, OUTPUT); // initialize serial communication with computer if (Debug) Serial.begin(9600); // Reset the drum units and set the time ResetAndSetTime (); } //------------- // Main routine //------------- void loop () { int i; int OneMin; // ones digit for current time minutes // After initialization, the clock should already // be showing the current time. // read the time from the WWVB atomic clock receiver. // the routine returns true if the read time is the expected time. if (GetTime()) { // read the expected time - clear the error counter TimeErrorCount = 0; } else { // didn't read the expected time TimeErrorCount = TimeErrorCount + 1; if (Debug) { clearLCD (); selectLineOne (); Serial.print ("Err. Count is "); Serial.print (TimeErrorCount, DEC); selectLineTwo (); Serial.print ("Limit is "); Serial.print (TimeErrorThreshold, DEC); delay (lcdDelay); } } // Reset and start over if we've gotten a few bad reads from the receiver, // or if the time zone or daylight savings time has changed if ((TimeErrorCount > TimeErrorThreshold) | (PrevTimeZone != TimeZone) | (PrevDaylightSavingsTime != DaylightSavingsTime)) { if (Debug) { clearLCD (); selectLineOne (); Serial.print ("Resetting due to"); selectLineTwo (); if (TimeErrorCount > TimeErrorThreshold) { Serial.print ("Error count > "); Serial.print (TimeErrorThreshold, DEC); } else if (TimeZone != PrevTimeZone) { Serial.print ("TimeZone changed"); } else if (PrevDaylightSavingsTime != DaylightSavingsTime) { Serial.print ("Daylight Savings"); } delay (lcdDelay); } // Punt and start over. TimeErrorCount = 0; ResetAndSetTime (); } else { // Everything is ok. Proceed with the new time SetNextTime (); // count out to the number about to show on the minute score reel // and count back down again OneMin = Minutes; while (OneMin > 9) OneMin = OneMin - 10; PulseCoil (StepUnitIncr, OneMin); PulseCoil (StepUnitDecr, OneMin); // make a correction if we're advancing to 1:00 from 12:59 if ((Hours == 1) && (Minutes == 00)) { // Set the Hours back to 00 PulseCoil (TenHourReset, 9); PulseCoil (OneHourReset, 9); } // make a correction if we're stepping from 59 to 00 minutes if (Minutes == 00) { // set the 10 minutes digit to 9 PulseCoil (TenMinIncr, 4); } // display should show 0099. advance one minute to show 0100. PulseMinuteWithChime (); // count off the Hours if we're at the top of the hour if (Minutes == 00) { delay (3000); for (i = 0; i < Hours; i++) { digitalWrite (Chime, HIGH); // chime on delay (StdPulseWidth); // wait... digitalWrite (Chime, LOW); // chime off delay (HourChimeInterval); // wait... } } } } // To Do: // - compute the time ahead of time and advance on the frame marker, not after