Initial commit
authorArt Cancro <ajc@citadel.org>
Tue, 18 Feb 2020 21:42:02 +0000 (16:42 -0500)
committerArt Cancro <ajc@citadel.org>
Tue, 18 Feb 2020 21:42:02 +0000 (16:42 -0500)
the_perfect_clock.ino [new file with mode: 0644]

diff --git a/the_perfect_clock.ino b/the_perfect_clock.ino
new file mode 100644 (file)
index 0000000..92444a3
--- /dev/null
@@ -0,0 +1,297 @@
+// "The Perfect Clock" 
+
+// Copyright (C) 2019-2020 by Art Cancro <ajc@citadel.org>
+
+// My perfect clock has no buttons and cannot be set manually.  This version uses a WWVB receiver module
+// attached to pin D9 of the Arduino, and sets the clock any time it receives a complete frame.  The clock
+// is kept without an RTC, simply using the millis() timer.  When time is set, it is displayed on
+// a 7-segment array connected using an HT16K33 decoder/driver (yes, an Adafruit backpack).  Our display
+// can also display at 15 different brightness levels, so we dim it when the room is dark to avoid
+// blasticating a dark room with super-bright LED display.
+
+// The clock is hard coded to use US Eastern time with DST in effect whenever WWVB is announcing it.
+
+const uint8_t wwvb = 9;       // pin on which WWVB signal will be received
+const uint8_t greenled = 2;   // An LED attached to this pin will illuminate if the time has been set within the last 24 hours
+const uint8_t yellowled = 3;  // An LED attached to this pin will illuminate if we are currently receiving a clean frame
+const uint8_t redled = 4;     // An LED attached to this pin will pulse for 1 ms every second
+const uint8_t photocell = A0; // Attach a photocell with a 10K voltage divider to this pin
+const uint8_t addr = 0x70;    // I2C address of HT16K33 (using Adafruit backpack with digits on 0,1,3,4; dots on 2)
+
+#define MILLISECONDS_PER_MINUTE 60080  // Nominally 60000; adjust if your board runs fast or slow
+
+// This is a simple BCD-to-7-segment font.  It includes 0x0A through 0x0F even though they're not needed for a time clock.
+const uint8_t sevensegfont[] = { 63, 6, 91, 79, 102, 109, 125, 7, 127, 111, 119, 124, 57, 94, 121, 113 };
+const uint8_t firstcolfont[] = { 0, 6, 91 };   // this version of the font is for the first position
+
+#include <Wire.h>                              // I2C library to drive the HT16K33 display
+
+int hour = 0;
+int minute = 0;
+unsigned long millisecond = 0;
+unsigned long previous_millis = 0;
+unsigned long last_sync = -86398000;
+uint16_t displayBuffer[8];    // Digit buffer for HT16K33
+int previous_minute = 61;     // What the minute was previously; we use this to detect whether an update is needed
+int this_pulse = 0;           // Value of the current pulse received
+int previous_pulse = 0;       // Value of the previous pulse received (two "mark" bits == new frame)
+int start_of_pulse = 0;       // The value of the millis() timer when the current pulse began
+uint8_t framebuf[60];         // We store the entire 60-bit frame here
+uint8_t framesync = 0;        // Nonzero if we've received all good pulses since the start of the frame
+int position_in_frame = 0;    // Where we are in the frame (1 bit per second)
+int previous_signal = 0;      // "high" or "low" received on the previous cycle (so we can do edge detection)
+int time_is_set = 0;          // nonzero when time has been set at least once
+
+void setup()
+{
+       int i;
+
+       pinMode(LED_BUILTIN, OUTPUT);   // The built-in LED will display the raw WWVB signal pulses
+       pinMode(greenled, OUTPUT);    // This LED will illuminate if the time has been set within the last 24 hours
+       pinMode(yellowled, OUTPUT);   // This LED will illuminate if we are currently receiving a clean frame
+       pinMode(redled, OUTPUT);      // This LED pulses for 1 ms every second
+       pinMode(wwvb, INPUT);                 // Input pin for WWVB receiver signal
+  pinMode(photocell, INPUT);    // Input pin for photocell
+
+       Wire.begin();                   // Initialize I2C
+
+       Wire.beginTransmission(addr);
+       Wire.write(0x21);               // turn on oscillator
+       Wire.endTransmission();
+
+       Wire.beginTransmission(addr);
+       Wire.write(0xE1);               // brightness (max is 15)
+       Wire.endTransmission();
+
+       Wire.beginTransmission(addr);
+       Wire.write(0x81);               // no blinking or blanking
+       Wire.endTransmission();
+
+       displayBuffer[0] = 0;
+       displayBuffer[1] = 0;
+       displayBuffer[2] = 16;
+       displayBuffer[3] = 0;
+       displayBuffer[4] = 0;
+       show();
+}
+
+
+// Note: only write to the display when the readout needs to be updated.
+// Speaking I2C on every loop iteration jams the WWVB receiver.
+void loop()
+{
+       unsigned long m = millis();
+       digitalWrite(redled, (m%1000) ? LOW : HIGH);
+       if (m != previous_millis) {
+               millisecond += (m - previous_millis);
+               if (millisecond >= MILLISECONDS_PER_MINUTE) {
+                       millisecond -= MILLISECONDS_PER_MINUTE;
+                       ++minute;
+                       if (minute > 59) {
+                               minute = 0;
+                               ++hour;
+                               if (hour > 23) {
+                                       hour = 0;
+                               }
+                       }
+               }
+       }
+       previous_millis = m;
+
+       int pulse_length;
+       int signal = digitalRead(wwvb);                                                         // is the input high or low right now?
+       digitalWrite(LED_BUILTIN, signal);                                              // use the onboard LED to show the signal
+
+       if (signal && (!previous_signal)) {                         // leading edge of pulse detected
+               start_of_pulse = millis();
+       }
+       else if ((!signal) && (previous_signal)) {                  // trailing edge of pulse detected
+               pulse_length = millis() - start_of_pulse;
+
+               if (pulse_length > 150 && pulse_length < 250) {                 // "0" bit ~= 200 ms (represented as "0")
+                       this_pulse = 0;
+               } else if (pulse_length > 450 && pulse_length < 550) {          // "1" bit ~= 500 ms (represented as "1")
+                       this_pulse = 1;
+               } else if (pulse_length > 750 && pulse_length < 850) {          // marker bit ~= 800 ms (represented as "2")
+                       this_pulse = 2;
+               } else {
+                       this_pulse = 15;                                        // bad pulse (represented as "15")
+                       framesync = 0;                                          // throw the whole frame away
+               }
+
+               // BEGIN -- THINGS TO DO AT THE END OF A PULSE
+
+               if ((this_pulse == 2) && (previous_pulse == 2)) {               // start of a new frame!
+
+                       if (framesync == 1) {
+                               set_the_time();                                                         // We have a whole good frame.  Set the clock!
+                       }
+      else if ((!framesync) && (time_is_set)) {
+        snap_to_zero();                                       // We don't have a whole frame, but we know it's :00 seconds now.
+      }
+
+                       framesync = 1;
+                       position_in_frame = 0;
+               }
+
+               if (framesync) {                                                                        // yellow LED = we currently have frame sync
+                       analogWrite(yellowled, 10);                                             // (we run it at a low intensity)
+               }
+               else {
+                       digitalWrite(yellowled, LOW);
+               }
+
+               if ((framesync) && (position_in_frame < 60)) {
+                       framebuf[position_in_frame++] = this_pulse;
+               }
+
+               previous_pulse = this_pulse;
+
+               // END -- THINGS TO DO AT THE END OF A PULSE
+       }
+
+       previous_signal = signal;
+
+       // Update the display only if it's a new minute.
+
+       if (time_is_set && (minute != previous_minute)) {
+               previous_minute = minute;
+               int h12 = (hour % 12) ;
+               if (h12 == 0) h12 = 12;
+               displayBuffer[0] = firstcolfont[h12 / 10];
+               displayBuffer[1] = sevensegfont[h12 % 10];
+               displayBuffer[2] = (hour<12) ? 0x06 : 0x0a;                     // AM or PM dot , colon always on
+               displayBuffer[3] = sevensegfont[minute / 10];
+               displayBuffer[4] = sevensegfont[minute % 10];
+               show();
+       }
+
+       if ((m - last_sync) < 86400000) {                                               // green LED = got a good sync in the last 24 hours
+               digitalWrite(greenled, HIGH);
+       }
+       else {
+               digitalWrite(greenled, LOW);
+       }
+}
+
+
+// Write the display buffer to the display
+void show()
+{
+  // display the time
+       Wire.beginTransmission(addr);
+       Wire.write(0x00);       // start at address 0x0
+       for (int i = 0; i < 5; i++) {
+               Wire.write(displayBuffer[i] & 0xFF);
+               Wire.write(displayBuffer[i] >> 8);
+       }
+       Wire.endTransmission();
+  // set the brightness
+  int light_level = analogRead(photocell) / 64;
+  if (light_level < 1) {
+    light_level = 1;
+  }
+  if (light_level > 15) {
+    light_level = 15;
+  }
+  Wire.beginTransmission(addr);
+  Wire.write(0xE0 + light_level);     // set the display brightness
+  Wire.endTransmission();
+}
+
+
+// Set the software clock to the WWVB time currently in the buffer
+void set_the_time()
+{
+       int i, newhour, newminute, dst;
+
+       // These six positions MUST contain marker bits.
+       // If any of them do not, we are looking at a corrupt frame.
+       int markers[] = { 0, 9, 19, 39, 49, 59 };
+       for (i=0; i<6; ++i) {
+               if (framebuf[markers[i]] != 2) {
+                       return;
+               }
+       }
+
+       newhour = (framebuf[12] ? 20 : 0);
+       newhour += (framebuf[13] ? 10 : 0);
+       newhour += (framebuf[15] ? 8 : 0);
+       newhour += (framebuf[16] ? 4 : 0);
+       newhour += (framebuf[17] ? 2 : 0);
+       newhour += (framebuf[18] ? 1 : 0);
+       if ((newhour < 0) || (newhour > 23)) {
+               return;                         // reject impossible hours
+       }
+
+       newminute = (framebuf[1] ? 40 : 0);
+       newminute += (framebuf[2] ? 20 : 0);
+       newminute += (framebuf[3] ? 10 : 0);
+       newminute += (framebuf[5] ? 8 : 0);
+       newminute += (framebuf[6] ? 4 : 0);
+       newminute += (framebuf[7] ? 2 : 0);
+       newminute += (framebuf[8] ? 1 : 0);
+       if ((newminute < 0) || (newminute > 59)) {
+               return;                         // reject impossible minutes
+       }
+
+       // advance 1 minute because WWVB gives the *previous* minute
+       newminute += 1;
+       if (newminute >= 60) {
+               newminute = newminute % 60;
+               newhour += 1;
+       }
+
+       // US Eastern time (yes it is hard coded)
+       newhour -= 5;
+
+       // DST (FIXME make this adjustable)
+       dst = (framebuf[57] ? 2 : 0);
+       dst += (framebuf[58] ? 1 : 0);
+       switch(dst) {
+               case 0:                         // dst not in effect (make no adjustments)
+                       break;
+               case 2:                         // dst begins today (adjust if local hour > 2)
+                       if (newhour >= 2) {
+                               ++newhour;
+                       }
+                       break;
+               case 3:                         // dst is in effect (always adjust)
+                       ++newhour;
+                       break;
+               case 1:                         // dst ends today (adjust if local hour < 2)
+                       if (newhour < 2) {
+                               ++newhour;
+                       }
+                       break;
+       }
+
+       // If we went back to the previous day, adjust so that hour > 0
+       if (newhour < 0) {
+               newhour += 24;
+       }
+
+       // Set the software clock:
+       // * We have decoded the hour and minute from the signal
+       // * This function always gets called *after* the first pulse at :00, so we set the millisecond to 800
+       hour = newhour;
+       minute = newminute;
+       millisecond = 800;
+       time_is_set = 1;
+
+       // Let's remember the last time we synced the clock
+       last_sync = millis();
+}
+
+
+// Adjust the time to :00.8 seconds at the nearest minute.
+void snap_to_zero()
+{
+  if ((millisecond > 0) && (millisecond < 15000)) {   // If the second is from :00.0 to :15.0
+    millisecond = 800;                                // snap back to :00.8
+  }
+  else if (millisecond > 45000) {                     // If the second is :45.0 or above
+    millisecond = MILLISECONDS_PER_MINUTE + 800;      // snap forward to :00.8 (minute will advance automatically)
+  }
+}