/* digital_clock.pde - Don Cross - http://cosinekitty.com Firmware for my own digital clock design. See: http://cosinekitty.com/digitalclock The clock hardware requires a 9VDC supply for power. It uses the microcontroller's timer interrupt for timing, with experimental fudge factor adjustment for accuracy of better than 1 second per day. The display consists of hours (00..23), minutes (00..59), and seconds (00..59). */ #define ENABLE_SERIAL_COMMANDS 1 #define BINARY(a,b,c,d,e,f,g,h) ((a)<<7|(b)<<6|(c)<<5|(d)<<4|(e)<<3|(f)<<2|(g)<<1|(h)) long TimerValue() { // Returns the current number of timer ticks. extern volatile unsigned long timer0_overflow_count; // used for backup timing cli(); // disable interrupts so that we don't get a half-baked value long value = timer0_overflow_count; sei(); // enable interrupts return value; } // The following is a reminder of how to read the now-obsolete power sense input. // I'm leaving this here just in case I need a digital input some day... // the hardware is still available. // #define GET_POWER_SENSE() (PINB & 1) #define SET_PULSE_LED_HIGH() (PORTB |= (1<<5)) #define SET_PULSE_LED_LOW() (PORTB &= ~(1<<5)) #define SET_PULSE_LED_STATE(bit) ((bit) ? SET_PULSE_LED_HIGH() : SET_PULSE_LED_LOW()) #define SET_ERROR_LED_HIGH() (PORTB |= (1<<2)) #define SET_ERROR_LED_LOW() (PORTB &= ~(1<<2)) #define GET_ERROR_LED_STATE() (1 & (PORTB >> 2)) const char ERROR_CODE_LED_CHECK_BIT_MISMATCH = 0; const char NUM_ERROR_CODES = 2; const char JIFFIES_SHORT_BEAT = 6; const char JIFFIES_LONG_BEAT = 5 * JIFFIES_SHORT_BEAT; const char JIFFIES_SPACE_BEAT = 10 * JIFFIES_SHORT_BEAT; class ErrorBlinker { // manages zero or more error states, blinking red LED on and off to indicate each public: void Init() { for (char i=0; i < NUM_ERROR_CODES; ++i) { errorStateArray[i] = false; errorBlinkPattern[i] = ""; } currentPattern = 0; currentErrorIndex = 0; remainingJiffyCountOn = 0; remainingJiffyCountOff = 0; } void DefineBlinkPattern (char errorCode, const char *pattern) // pattern is morse code, e.g. "-.-." { if (errorCode >= 0 && errorCode < NUM_ERROR_CODES) { if (pattern != 0) { errorBlinkPattern[errorCode] = pattern; } } } void SetErrorState (char errorCode, bool errorState) { if (errorCode >= 0 && errorCode < NUM_ERROR_CODES) { // check array bounds errorStateArray[errorCode] = errorState; } } void JiffyUpdate() // called once every jiffy, to update red LED blink state machine { if (remainingJiffyCountOn > 0) { // We have already turned on the red LED... see if it is time to turn it off if (--remainingJiffyCountOn == 0) { SET_ERROR_LED_LOW(); remainingJiffyCountOff = JIFFIES_SHORT_BEAT; } } else if (remainingJiffyCountOff > 0) { // The error LED is off, but we need to see if it is time to turn it on again... if (--remainingJiffyCountOff == 0) { // We are done with this dark period. Are there more dots/dashes in this pattern? if (currentPattern != 0) { switch (*currentPattern++) { case '\0': SET_ERROR_LED_LOW(); currentPattern = 0; remainingJiffyCountOff = JIFFIES_SPACE_BEAT; // space between symbols break; case '-': SET_ERROR_LED_HIGH(); remainingJiffyCountOn = JIFFIES_LONG_BEAT; break; case '.': default: SET_ERROR_LED_HIGH(); remainingJiffyCountOn = JIFFIES_SHORT_BEAT; break; } } } } else { // Search for an error condition to display... if (errorStateArray[currentErrorIndex]) { currentPattern = errorBlinkPattern[currentErrorIndex]; remainingJiffyCountOff = 1; // prime the pump to turn LED on, next jiffy } currentErrorIndex = (1 + currentErrorIndex) % NUM_ERROR_CODES; } } private: bool errorStateArray [NUM_ERROR_CODES]; const char *errorBlinkPattern [NUM_ERROR_CODES]; const char *currentPattern; char currentErrorIndex; char remainingJiffyCountOn; char remainingJiffyCountOff; }; ErrorBlinker TheErrorBlinker; //-------------------------------------------------------------------------------- // DisplayDriver class begins here. // I will try to keep this fairly re-usable for multiple 7-segment digits. // This is designed for serial shift register output. // For efficiency, I am hardcoding the affected digital output pins for now. // SDI (serial data in) = pin 7 // CLK (display clock) = pin 6 // LE (latch enable) = pin 5 // OE (output enable) = pin 4 NOTE that this is active-low #define LEDMASK(a,b,c,d,e,f,g,h) BINARY(h,g,f,e,d,c,b,a) #define DISPLAY_NUM_DIGITS 6 #define SYMBOL_SPACE 16 class DisplayDriver { public: void Init () { expectedCheckBit = 2; // this is a special value that means "we don't know" currentCheckBit = 2; for (char i=0; i < DISPLAY_NUM_DIGITS; ++i) { digit[i] = SYMBOL_SPACE; } Reset(); } void Reset() // blank the display { Disable(); SetLatchEnable (0); SetClockPin (0); SetSerialDataPin (0); } void SetDigit (char index, char symbol) // sets a digit value but does not display it { if (index >= 0 && index < DISPLAY_NUM_DIGITS) { // ignore invalid index if (symbol < 0 || symbol > SYMBOL_SPACE) { // convert any invalid symbol to space symbol = SYMBOL_SPACE; } digit[index] = symbol; } } void Write () // sends all digits to the display at once { for (char i=DISPLAY_NUM_DIGITS-1; i >= 0; --i) { SendMask (SYMBOL_TABLE[digit[i]]); } SetLatchEnable (1); SetLatchEnable (0); if (expectedCheckBit != 2) { // if we know what to expect... if (currentCheckBit != expectedCheckBit) { TheErrorBlinker.SetErrorState (ERROR_CODE_LED_CHECK_BIT_MISMATCH, true); } } // Capture the least significant bit of the LED mask we just sent out. // This will be bar "A" in the unit seconds numeral. // This will be the next bit we should see in serial data out (SDO) // fed back from the high-order LED driver chip. expectedCheckBit = SYMBOL_TABLE[digit[0]] & 1; } void Enable () // allow LEDs to reflect the written data { SetOutputEnable (0); // this looks backwards because Output Enable is active-low } void Disable () // turn off all the LEDs without affecting written data { SetOutputEnable (1); // this looks backwards because Output Enable is active-low } protected: void SetDisplayOutputPin (char pin, char bit) { PORTD = bit ? (PORTD | (1<<(pin))) : (PORTD & ~(1<<(pin))); } void SetSerialDataPin (char bit) { SetDisplayOutputPin (7, bit); } void SetClockPin (char bit) { SetDisplayOutputPin (6, bit); } void SetLatchEnable (char bit) { SetDisplayOutputPin (5, bit); } void SetOutputEnable (char bit) { SetDisplayOutputPin (4, bit); } void SendMask (char mask) { for (char i=7; i >= 0; --i) { // send most significant bit first char bit = 1 & (mask >> i); SetSerialDataPin (bit); SetClockPin (1); SetSerialDataPin (0); currentCheckBit = ReadCheckBit(); SetClockPin (0); } } char ReadCheckBit() { return 1 & (PINB >> 1); } private: char digit [DISPLAY_NUM_DIGITS]; // least significant first char expectedCheckBit; char currentCheckBit; static char SYMBOL_TABLE[]; }; char DisplayDriver::SYMBOL_TABLE[] = { // A B C D E F G H <=== LED bits LEDMASK(1,1,1,0,1,1,1,0), // 0 LEDMASK(0,0,1,0,0,1,0,0), // 1 LEDMASK(1,0,1,1,1,0,1,0), // 2 LEDMASK(1,0,1,1,0,1,1,0), // 3 LEDMASK(0,1,1,1,0,1,0,0), // 4 LEDMASK(1,1,0,1,0,1,1,0), // 5 LEDMASK(1,1,0,1,1,1,1,0), // 6 LEDMASK(1,0,1,0,0,1,0,0), // 7 LEDMASK(1,1,1,1,1,1,1,0), // 8 LEDMASK(1,1,1,1,0,1,1,0), // 9 0, // a 0, // b 0, // c 0, // d 0, // e 0, // f 0 // 16 = SPACE (all LEDs off) }; DisplayDriver TheDisplay; // end of DisplayDriver class //-------------------------------------------------------------------------------- const char NUM_COUNTERS = 5; const char INDEX_JIFFIES = 0; // A jiffy is 1/60 of a second in this program: http://en.wikipedia.org/wiki/Jiffy_%28time%29 const char INDEX_SECONDS = 1; const char INDEX_MINUTES = 2; const char INDEX_HOURS = 3; const char INDEX_DAYOFWEEK = 4; char CounterArray [NUM_COUNTERS] = { 0, 0, 0, 0, 0 }; // This is where the clock's time is stored! const char LimitArray [NUM_COUNTERS] = { 60, 60, 60, 24, 7 }; // Defines when each part of the clock rolls over. const long JIFFIES_PER_TICK = 6144L; const long MICROJIFFIES_PER_TICK = 371555L; // determined experimentally for particular 16 MHz crystal long TickAccumulator = 0; // in units of 10^( -5) jiffies long MicroTicksAccumulator = 0; // in units of 10^(-11) jiffies long PrevTimerValue = 0; const char *DayOfWeekName[] = { "Sun", // 0 "Mon", // 1 "Tue", // 2 "Wed", // 3 "Thu", // 4 "Fri", // 5 "Sat" // 6 }; long OnLoopCount = 0; long OffLoopCount = 0; long OnTimerDeltaValue = 0; long OffTimerDeltaValue = 0; int CalcMillisecondsPortion() { long mj = 1000L * (long)CounterArray[INDEX_JIFFIES]; // milli-jiffies mj += TickAccumulator / 100L; int ms = (int) (mj / 60L); return ms; } #if ENABLE_SERIAL_COMMANDS const char MAX_COMMAND_CHARS = 20; char UserCommand [1 + MAX_COMMAND_CHARS]; char UserCommandIndex = 0; const char CounterSymbol[] = "jsmhd"; // jiffy, second, minute, hour, day_of_week void DisplayFirmwareVersion() { // The '*' at the beginning of the first 2 lines is important. // It tells DigitalClockConsole.exe to keep reading lines a line without '*' is received. Serial.println ("* Digital Clock - by Don Cross - http://cosinekitty.com"); Serial.println ("* Firmware built " __DATE__ " " __TIME__); Serial.println (); } #endif // ENABLE_SERIAL_COMMANDS bool TimeCorrectedSinceBoot = false; // true only if somebody adjusted the time since the last power outage #if ENABLE_SERIAL_COMMANDS void TransmitTimeToSerialPort() { Serial.print (DayOfWeekName[CounterArray[INDEX_DAYOFWEEK]]); Serial.print (' '); for (char i=INDEX_HOURS; i >= INDEX_SECONDS; --i) { if (i != INDEX_HOURS) { Serial.print (':'); } if (CounterArray[i] < 10) { Serial.print ('0'); } Serial.print ((int) CounterArray[i]); } Serial.print ('.'); int ms = CalcMillisecondsPortion(); if (ms < 100) { Serial.print ('0'); if (ms < 10) { Serial.print ('0'); } } Serial.print (ms); if (!TimeCorrectedSinceBoot) { Serial.print(" ?"); // warn the user that the power may have gone out } Serial.println(); } #endif // ENABLE_SERIAL_COMMANDS void WriteTimeToDisplay() { TheDisplay.SetDigit (0, CounterArray[INDEX_SECONDS] % 10); TheDisplay.SetDigit (1, CounterArray[INDEX_SECONDS] / 10); TheDisplay.SetDigit (2, CounterArray[INDEX_MINUTES] % 10); TheDisplay.SetDigit (3, CounterArray[INDEX_MINUTES] / 10); TheDisplay.SetDigit (4, CounterArray[INDEX_HOURS] % 10); TheDisplay.SetDigit (5, CounterArray[INDEX_HOURS] / 10); TheDisplay.Write(); } //------------------------------------------------------------------------------- char ButtonFeedbackCounter = 0; void AnyButtonOn() { TimeCorrectedSinceBoot = true; // no longer warn the user that the time might be wrong ButtonFeedbackCounter = 2; SET_ERROR_LED_HIGH(); WriteTimeToDisplay(); } void AnyButtonOff() { if (ButtonFeedbackCounter > 0) { if (--ButtonFeedbackCounter == 0) { SET_ERROR_LED_LOW(); } } } char ButtonArray [NUM_COUNTERS] = { 0, 0, 0, 0, 0 }; const char BUTTON_LEVEL_PRESSED = 2; // debounce level const char BUTTON_LEVEL_REPEAT = 60; // after a full second, start repeating const char BUTTON_REPEAT_INTERVAL = 12; // number of jiffies to repeat after char UpdateButtonState (char index, char pressed) { char action = 0; static bool seconds_pressed = false; if (pressed) { if (index == INDEX_SECONDS) { // Special case: reset seconds and jiffies to zero, but only at the beginning of the button push if (seconds_pressed) { // seconds still held down... do nothing } else { seconds_pressed = true; CounterArray[INDEX_JIFFIES] = 0; CounterArray[INDEX_SECONDS] = 0; action = 1; AnyButtonOn(); } } else { // This is either the minutes button or the hours button. if (ButtonArray[index] < BUTTON_LEVEL_REPEAT) { switch (++ButtonArray[index]) { case BUTTON_LEVEL_PRESSED: // The button has been pressed a long enough fraction of a second that // we assume it is not because of any switch noise. action = 1; break; case BUTTON_LEVEL_REPEAT: // The user has been holding down the button a while. // Let's automatically repeat the action of this button a few times a second... action = 1; ButtonArray[index] -= BUTTON_REPEAT_INTERVAL; // re-prime for next repeat break; } if (action) { CounterArray[index] = (1 + CounterArray[index]) % LimitArray[index]; AnyButtonOn(); } } } } else { ButtonArray[index] = 0; // button is no longer pressed, so reset counter if (index == INDEX_SECONDS) { seconds_pressed = false; } } return action; } void CheckPushButtons() { char action = UpdateButtonState (INDEX_HOURS, 1 & (PIND >> 2)); action += UpdateButtonState (INDEX_MINUTES, 1 & (PIND >> 3)); action += UpdateButtonState (INDEX_SECONDS, 1 & (PINB >> 3)); if (action == 0) { AnyButtonOff(); } } //------------------------------------------------------------------------------- class HourChimer { public: HourChimer (char _startHour, char _finalHour) { startHour = _startHour; finalHour = _finalHour; chimesRemaining = 0; toneState = false; } int TwelveHour (char hour) { if (hour >= 13) { hour -= 12; } return hour; } void OnHour (char hour) { if ((hour >= startHour) && (hour <= finalHour)) { chimesRemaining = TwelveHour (hour); SET_ERROR_LED_HIGH(); toneState = true; } } void OnSecond (char second) { if (chimesRemaining > 0) { if (toneState) { SET_ERROR_LED_LOW(); --chimesRemaining; } else { SET_ERROR_LED_HIGH(); } toneState = !toneState; } } private: char startHour; char finalHour; char chimesRemaining; bool toneState; }; HourChimer TheHourChimer (8, 20); // 8:00 AM to 8:00 PM policy //------------------------------------------------------------------------------- void IncrementJiffy() { static char led_jiffy_countdown = 0; char i; for (i=INDEX_JIFFIES; i 0) { // The green pulse LED is turned on. Is it time to turn it back off? if (--led_jiffy_countdown == 0) { SET_PULSE_LED_LOW(); } } break; case INDEX_SECONDS: SET_PULSE_LED_HIGH(); led_jiffy_countdown = 2; TheHourChimer.OnSecond (CounterArray[INDEX_SECONDS]); break; case INDEX_MINUTES: SET_PULSE_LED_HIGH(); led_jiffy_countdown = 30; break; case INDEX_HOURS: // Hour chime: the beeper is now hooked up to the RED led TheHourChimer.OnHour (CounterArray[INDEX_HOURS]); break; case INDEX_DAYOFWEEK: break; default: // This happens once a week break; } CheckPushButtons(); // see if the user is adjusting the time settings if (i >= INDEX_SECONDS) { WriteTimeToDisplay(); } TheErrorBlinker.JiffyUpdate(); } //----------------------------------------------------------------------------------------- #if ENABLE_SERIAL_COMMANDS void Command_WriteCounter (char whichCounter, const char *numeric) { char n = atoi(numeric); char i; for (i=0; CounterSymbol[i]; ++i) { if (CounterSymbol[i] == whichCounter) { if (n >= 0 && n < LimitArray[i]) { CounterArray[i] = n; if (i == INDEX_SECONDS) { CounterArray[INDEX_JIFFIES] = 0; // make this exactly on the second } TimeCorrectedSinceBoot = true; // no longer warn the user that the time might be wrong WriteTimeToDisplay(); // give immediate feedback when user changes time TransmitTimeToSerialPort(); } else { Serial.println ("Numeric parameter out of range"); } break; } } if (CounterSymbol[i] == 0) { Serial.println ("Invalid counter symbol specified"); } } long MillisecondsTimeStamp() { long stamp = 0; for (char i = INDEX_DAYOFWEEK; i >= INDEX_SECONDS; --i) { stamp = (LimitArray[i] * stamp) + CounterArray[i]; } stamp = (1000L * stamp) + CalcMillisecondsPortion(); return stamp; } void SetClockFromTimeStamp (long milliseconds) { const long MILLISECONDS_PER_WEEK = 7L * 24L * 3600L * 1000L; // 604,800,000 // Normalize the value we have been given into a non-negative // value that is within the number of milliseconds per week. while (milliseconds < 0) { // Note that 0x7fffffff / MILLISECONDS_PER_WEEK = 3.55, so this cannot loop more than 4 times. milliseconds += MILLISECONDS_PER_WEEK; } milliseconds %= MILLISECONDS_PER_WEEK; // Now we have a normalized value in the range 0 .. (MILLISECONDS_PER_WEEK-1). long ms_only = milliseconds % 1000L; long decode = milliseconds / 1000L; // Tricky math... set the jiffies and hundred-thousandths of jiffies counters... TickAccumulator = ms_only * 6000L; // a value from 0 .. 5,994,000 CounterArray[INDEX_JIFFIES] = TickAccumulator / 100000L; // a value from 0..59 TickAccumulator %= 100000L; // The rest is simple... set the seconds, minutes, hours, and days counters... for (char index = INDEX_SECONDS; index <= INDEX_DAYOFWEEK; ++index) { CounterArray[index] = decode % LimitArray[index]; decode /= LimitArray[index]; } } void Command_AdjustMilliseconds (long deltaMilliseconds) { long stamp = MillisecondsTimeStamp(); SetClockFromTimeStamp (stamp + deltaMilliseconds); TransmitTimeToSerialPort(); // acknowledge adjustment with updated time } void Command_TransmitMillisecondsTimeStamp() { long stamp = MillisecondsTimeStamp(); Serial.println (stamp); } void ExecuteCommand (const char *command) { // Write Counter: w[j|d|h|m|s]N; switch (command[0]) { case 'a': // adjust time by specified number of milliseconds Command_AdjustMilliseconds (atol (&command[1])); break; case 'd': // duty cycle stats Serial.print ("on:"); Serial.print (OnLoopCount); Serial.print (", off:"); Serial.println (OffLoopCount); break; case 'm': Command_TransmitMillisecondsTimeStamp(); break; case 'r': TransmitTimeToSerialPort(); break; case 'v': DisplayFirmwareVersion(); break; case 'w': Command_WriteCounter (command[1], &command[2]); break; default: Serial.println ("Unknown command"); break; } } void ReadOneSerialCharacter() { char x = Serial.read(); if (x == ';') { ExecuteCommand (UserCommand); UserCommandIndex = 0; UserCommand[0] = '\0'; } else { if (UserCommandIndex < MAX_COMMAND_CHARS) { UserCommand[UserCommandIndex++] = x; UserCommand[UserCommandIndex] = '\0'; } } } #endif // ENABLE_SERIAL_COMMANDS //----------------------------------------------------------------------------------------- void SetAllDigits (char digit, int millis) { char i; for (i=0; i<6; ++i) { TheDisplay.SetDigit (i, digit); } TheDisplay.Write(); delay (millis); } void TestDisplay() { SetAllDigits (8, 2000); for (char x=0; x < 10; ++x) { SetAllDigits (x, 500); } SetAllDigits (SYMBOL_SPACE, 0); } //----------------------------------------------------------------------------------------- void TestCpuBoardLeds() { for (int i=0; i < 5; ++i) { SET_PULSE_LED_HIGH(); delay (200); SET_PULSE_LED_LOW(); SET_ERROR_LED_HIGH(); delay (200); SET_ERROR_LED_LOW(); } } //----------------------------------------------------------------------------------------- void setup() { // Hardware port cheat sheet for the ATMEGA8 microcontroller: // DDRx = pin definition for port x (input=0, output=1) // PINx = read input from bits in port x // PORTx = write output to bits in port x (can also read to get currently written bits) //------------------------------------------ // Port B - define input/output settings // 0 : in : 60 Hz jiffy signal // 1 : in : LED driver / SDO check bit : used to verify that the LED driver board is working // 2 : out : red error LED // 3 : in : push button : zero seconds // 4 : // 5 : out : green pulse LED DDRB = BINARY(0,0,1,1,0,1,0,0); // bits 7, 6, 5, 4, 3, 2, 1, 0 //------------------------------------------ // Port C - define input/output settings // 0 : // 1 : // 2 : // 3 : // 4 : // 5 : DDRC = BINARY(0,0,0,0,0,0,0,0); // bits 7, 6, 5, 4, 3, 2, 1, 0 //------------------------------------------ // Port D - define input/output settings // 0 : in : serial RX // 1 : out : serial TX // 2 : in : push button : increment hour // 3 : in : push button : increment minute // 4 : out : LED driver / OE (output enable: note that this one is active low) // 5 : out : LED driver / LE (latch enable) // 6 : out : LED driver / CLK (serial clock) // 7 : out : LED driver / SDI (serial data) DDRD = BINARY(1,1,1,1,0,0,0,1); // bits 7, 6, 5, 4, 3, 2, 1, 0 SET_PULSE_LED_LOW(); SET_ERROR_LED_LOW(); TheDisplay.Init(); TheDisplay.Disable(); WriteTimeToDisplay(); #if ENABLE_SERIAL_COMMANDS Serial.begin (38400); DisplayFirmwareVersion(); #endif // ENABLE_SERIAL_COMMANDS TestCpuBoardLeds(); TheErrorBlinker.Init(); TheErrorBlinker.DefineBlinkPattern (ERROR_CODE_LED_CHECK_BIT_MISMATCH, ".-.."); TheDisplay.Enable(); PrevTimerValue = TimerValue(); } //----------------------------------------------------------------------------------------- void loop() { long now = TimerValue(); int elapsed = (int) (now - PrevTimerValue); // we don't care if it wraps around every 9 hours if (elapsed > 0) { // Note that 1 tick = 1.024 ms. // Also, 1 jiffy = 1/60 second = 1000/60 ms = 16.666... ms. // Therefore, 1 jiffy = (16.666.../1.024) = 16.27604166666.... ticks. // The reciprocal is exactly 0.06144 jiffy/tick = 6144/100000 jiffy/tick. // We are going to track everything in units of 10^(-5) jiffies PrevTimerValue = now; TickAccumulator += (JIFFIES_PER_TICK * elapsed); MicroTicksAccumulator += (MICROJIFFIES_PER_TICK * elapsed); TickAccumulator += MicroTicksAccumulator / 1000000L; MicroTicksAccumulator %= 1000000L; while (TickAccumulator >= 100000L) { TickAccumulator -= 100000L; IncrementJiffy(); } } #if ENABLE_SERIAL_COMMANDS if (Serial.available()) { ReadOneSerialCharacter(); } #endif // ENABLE_SERIAL_COMMANDS } /* $Log: digital_clock.pde,v $ Revision 1.43 2008/10/19 16:07:01 Don.Cross Adjusted tuning constant from 0.06144374277 jiffies/tick to 0.06144371555 jiffies/tick. Too bad this revision comment will cause the source code to no longer be exactly 40,000 bytes! Revision 1.42 2008/09/14 09:22:17 Don.Cross Added hourly chime of the hours. Using 12-hour chimes, even though this is a 24-hour clock, just to keep the numbers small. It also will sound more traditional that way, compared to cuckoo clocks and such. Right now it is designed to chime only from 8:00 AM until 8:00 PM. Revision 1.41 2008/09/14 03:00:22 Don.Cross I have changed the hardware so that the LEDs are now powered separately from battery backup. When wall power is lost, the LEDs automatically lose power, so there is no longer any need for software to handle this. Therefore, I have removed all the power sense logic. Revision 1.40 2008/09/07 23:10:00 Don.Cross No code change. Just recording that current code size is 5190 bytes. Revision 1.39 2008/09/07 23:08:34 Don.Cross Wait 5 seconds instead of 0.5 seconds of stable power before turning LEDs back on. No longer wait several milliseconds to turn power off. We want a HAIR TRIGGER! Turn off LEDs as quickly as possible whenever the power sense tells us we are losing power. Revision 1.38 2008/09/06 19:32:04 Don.Cross On reboot, quickly turn off LEDs, then perform lengthy tests, then display 00:00:00 before starting to increment. Revision 1.37 2008/09/06 19:24:35 Don.Cross Finished implementing the 'a' command for adjusting the clock with a delta-milliseconds value. Code size is now 5146 bytes. Revision 1.36 2008/09/06 18:59:52 Don.Cross Fixed bug in digital clock firmware: now calcualting millisecond time stamps correctly for 'm' command. Fixed bug in DigitalClockConsole: properly handle running this program before clock is fully booted up. Revision 1.35 2008/09/06 18:48:17 Don.Cross Adding ability to adjust the digital clock time using a millisecond increment. It is not working yet, so it needs more debugging. Adding generic console mode in DigitalClockConsole.exe. Print diagnostic messages in NistClock.exe if it has to retry. Revision 1.34 2008/09/06 01:29:45 Don.Cross Based on regression analysis from 6 days of measurement, the best fit drift rate of the digital clock was +0.0367 seconds per day (i.e. the clock is running fast by 36.7 milliseconds each day). Using the drift_data.xls spreadsheet, on the "Adjustment" page, I am doing my second-order correction from 376889 to 374277 for the fudge factor called MICROJIFFIES_PER_TICK. Revision 1.33 2008/08/30 14:45:05 Don.Cross Now when transmitting the time to the serial port, print to the nearest millisecond, not just tenths of a second. Revision 1.32 2008/08/29 11:44:22 Don.Cross I decided to make the power-on and power-off thresholds different: The clock still turns off the LEDs 3 milliseconds after detecting power loss, but will wait 500 milliseconds before turning the LEDs on after sensing power resumption. I just didn't like the idea of rapidly turning the LEDs on and off during a fluctuating brownout. Revision 1.31 2008/08/20 03:01:39 Don.Cross I realized it was hard to synchronize the seconds by pressing the seconds button because I need the clock to start counting while I still have the button pressed: it works better when the clock senses the edge of the button press, not the release. Revision 1.30 2008/08/16 21:11:59 Don.Cross Updated header comments. Revision 1.29 2008/08/16 21:10:45 Don.Cross Removed all vestiges of 60 Hz AC timing code. Now always use internal timer interrupt for timing. Revision 1.28 2008/08/16 01:32:40 Don.Cross Minor fix: should reset micro tick counter to 0 each time switching to battery power. Restarted experiment at 21:33:00 EDT. Revision 1.27 2008/08/16 01:28:45 Don.Cross Based on experimental data, the clock is drifting by -5.3 seconds per day (the negative sign means that it is running slow). This means it needed to be speeded up by a factor of (5.3 + 24*3600)/(24*3600). I have added a microjiffies per tick counter as well as a jiffies per tick counter for fine tuning. This experiment is running now as of 21:24:30 EDT, 15 August 2008. Revision 1.26 2008/08/13 03:03:37 Don.Cross Enhanced logic so that the clock can run on battery power when there is no AC timing signal: Use internal timer overflow counter to reckon time when AC signal is missing. Turn off LED display when AC signal is missing, assuming that means we are running on battery power. It may turn out that the internal timer is more accurate than using external AC timing signal, in which case I will always use internal timing and rework the software and/or hardware to deduce battery power some other way. Revision 1.25 2008/08/12 18:33:33 Don.Cross Streamlined and simplified so that profiler includes only the on/off timers. Enabled/disabled in tandem withs serial command options. Revision 1.24 2008/08/12 18:23:29 Don.Cross Remove obsolete debug code that was commented out anyway. Wrap all serial code with conditional compilation based on ENABLE_SERIAL_COMMANDS.. Revision 1.23 2008/08/09 20:52:29 Don.Cross Got H, M, S pushbuttons working exactly the way I want! Also shortened the power on CPU board LED test. Revision 1.22 2008/08/09 16:31:58 Don.Cross I just added the 3 push-buttons for adjusting the time without using the serial port. Added debug code here to confirm the hardware is functioning properly. It is! Revision 1.21 2008/08/09 14:55:34 Don.Cross Summarized purpose of this code in comments at the top of the file. Listed all input and output pins in comments in setup() function. Put conditional compilation around serial initialization and brag screen. (More conditional code will be needed if I decide to completely remove serial library usage to save code space later.) Revision 1.20 2008/08/08 21:54:59 Don.Cross Added code to test CPU board LEDs. Commented out code to test display LEDs for now, because it is time consuming. I may reinstate it later. Revision 1.19 2008/08/08 21:34:07 Don.Cross Replaced simply turning on the red error LED forever with Morse Code indicators! This way I can support lots of error codes, and multiple at the same time (like in Apollo 13). Revision 1.18 2008/08/04 19:52:20 Don.Cross Added sanity check code and changed hardware design to look at the Serial Data Output (SDO) pin on chip #3, to check to see if the previous least significant bit is what we knew we sent to it last time. This corresponds to bar A on the unit seconds numeral, which changes quite frequently. If we don't get the bit back that we expect, we turn on a new red error LED, indicating a circuit fault. Later I may have multiple fault checks, and make the red LED have different blink codes to distinguish them. For now, once the red LED comes on, it stays on until reboot. Revision 1.17 2008/07/29 01:10:07 Don.Cross Factored out code into new function WriteTimeToDisplay(). This can be called any time we want to update the current time to the LED display. I may need to rename this function and make it more complex to handle multi-mode display of alarm clock, time setting, chrono, etc. When the user sets the time via serial port, update the display immediately. I have noticed that writing the time back to the serial port takes a considerable fraction of a second, so it looks like in the "production" version of the firmware, I should remove this serial transmit code. I see no reason why the receive code needs to be removed though. Revision 1.16 2008/07/29 00:55:02 Don.Cross Now in power-on test, leave all LEDs on for 2 seconds, then cycle through all numeric values for half a second each. This way we test not only that all the LEDs are powered, but that they are hooked up correctly. Revision 1.15 2008/07/28 23:46:42 Don.Cross Added code to set the remaining digits on the display. Revision 1.14 2008/07/27 21:59:45 Don.Cross Fixed a bug in the bit mask for the numeral 7: segment C was off, but should have been on. Successfully tested hand-made unit seconds digit, a.k.a. digit[0]. Revision 1.13 2008/07/27 01:39:50 Don.Cross Did a lot of work today trying to get LED driver shift registers to work. I think I am close, but it is hard to tell because I just realized the LED digit display I am using will not work with the STP16C596 driver because the LEDs are all common cathode, but the driver requires common anode; I had all the LEDs hooked up backwards, and there is no way to hook them up forwards! I am going to have to build my own LED display after all. Revision 1.12 2008/07/20 21:44:20 Don.Cross Display firmware build date and time in boot message. Revision 1.11 2008/07/20 21:40:53 Don.Cross Replaced simplistic detection of going from low to high as the jiffy event trigger, to an accumulator that counts up to 100 when the input is high, and down to 0 when the input is low. The jiffy event fires only when the counter reaches 100 and a ReadyToFire flag is true. When this jiffy event fires, we set the ReadyToFire flag to false, so that it cannot fire again until the accumulator goes back down to zero and ReadyToFire is set back to true. Current profiling data indicates that we are looping at 53,000 iterations per second, or 883 iterations per jiffy. The hardware I have built has a duty cycle such that the clock is high 60% of the time, low 40% of the time. Multiplying 40% by 883 gives 353 loops in the low cycle, so 100 is a good value to guarantee recognizing high and low states, while filtering out any brief noise in the input. Revision 1.10 2008/07/20 20:34:22 Don.Cross Moved variable PrevClockInput from global scope to static local scope inside loop(). When nobody has manually adjusted the time since boot, assume the time is wrong and warn the user with a '?'. Revision 1.9 2008/07/20 20:12:47 Don.Cross Turned profiler back on, but made it togglable, and off by default. Added vanity print message upon boot up. Revision 1.8 2008/07/20 13:11:15 Don.Cross Reworked the code so jiffies are just another counter in the array, and therefore renamed IncrementSecond to IncrementJiffy. Moved logic for turning LED on and off into IncrementJiffy. When transmitting time to the serial port, include tenths of a second. Revision 1.7 2008/07/20 00:35:05 Don.Cross Only transmit time when requested (I am concerned about time accuracy drifting). Code size = 2414 bytes (of 7168 maximum). Revision 1.6 2008/07/20 00:17:35 Don.Cross Display the day of the week. Revision 1.5 2008/07/20 00:13:02 Don.Cross Lowered baud rate to 38,400 because I kept having weird problems sending data reliably to the microcontroller. Fixed bug: when setting the seconds counter, need to reset the pulse counter to zero so that seconds are exact. Revision 1.4 2008/07/20 00:01:11 Don.Cross Implemented serial commands for writing to the days, hours, minutes, and seconds counters. Revision 1.3 2008/07/19 22:13:47 Don.Cross Created conditional compilation symbol ENABLE_PROFILER for turning loop counter code on/off. Set to off for now. Revision 1.2 2008/07/19 22:10:56 Don.Cross Transmit formatted time string to serial port on every minute. Discovered that loop() is being called at a rate of about 367,000 times per second, or 6,117 times per input pulse. Revision 1.1 2008/07/19 20:52:13 Don.Cross First version of digital clock software: counts 60 Hz pulses and represents seconds, minutes, hours, and day of week in memory. */