// "The Perfect Clock"
-// Copyright (C) 2019-2020 by Art Cancro <ajc@citadel.org>
+// Copyright (C) 2019-2023 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
// 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)
+// This software is made available to you conditionally upon you accepting the following terms and conditions:
+// 1. You agree that it is called "open source", not "free software".
+// 2. You agree that the Linux operating system is not called "GNU/Linux".
+// 3. You agree that Corey Ehmke is a scumbag, as are all social justice warriors.
+// 4. You promise never to vote democrat in any election.
+// 5. Under no circumstances may you use this program and also maintain a Facebook account.
+// Aside from these conditions, the program is made available to you under the terms of the GNU General Public License.
-#define MILLISECONDS_PER_MINUTE 60080 // Nominally 60000; adjust if your board runs fast or slow
+// On my clock, there is a green LED on 2, a yellow LED on 3, and a red LED on 4.
+
+const uint8_t wwvb = 9; // pin on which WWVB signal will be received
+const uint8_t last24led = 2; // An LED attached to this pin will illuminate if the time has been set within the last 24 hours
+const uint8_t cleantimecodeled = LED_BUILTIN; // An LED attached to this pin will illuminate if we are currently receiving a clean frame
+const uint8_t timecodeled = 3;
+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)
+
+long millis_per_minute = 60000; // 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
+#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()
-{
+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
+ pinMode(timecodeled, OUTPUT); // The built-in LED will display the raw WWVB signal pulses
+ pinMode(last24led, OUTPUT); // This LED will illuminate if the time has been set within the last 24 hours
+ pinMode(cleantimecodeled, OUTPUT); // This LED will illuminate if we are currently receiving a clean frame
+ pinMode(wwvb, INPUT); // Input pin for WWVB receiver signal
+ pinMode(photocell, INPUT); // Input pin for photocell
- Wire.begin(); // Initialize I2C
+ Wire.begin(); // Initialize I2C
Wire.beginTransmission(addr);
- Wire.write(0x21); // turn on oscillator
+ Wire.write(0x21); // turn on oscillator
Wire.endTransmission();
Wire.beginTransmission(addr);
- Wire.write(0xE1); // brightness (max is 15)
+ Wire.write(0xE1); // brightness (max is 15)
Wire.endTransmission();
Wire.beginTransmission(addr);
- Wire.write(0x81); // no blinking or blanking
+ Wire.write(0x81); // no blinking or blanking
Wire.endTransmission();
displayBuffer[0] = 0;
// 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()
-{
+void loop() {
+
+ // Reading it three times and taking the average gives us some hysteresis
+ int signal = (digitalRead(wwvb) + digitalRead(wwvb) + digitalRead(wwvb)) / 3;
+
+ // has the timer ticked?
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;
+ if (millisecond >= millis_per_minute) {
+ millisecond -= millis_per_minute;
++minute;
if (minute > 59) {
minute = 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
+ if (signal) {
+ analogWrite(timecodeled, 5); // it's too bright on my board so we dim it; change to digitalWrite() if not needed
+ }
+ else {
+ digitalWrite(timecodeled, LOW);
+ }
+
+ if (signal && (!previous_signal)) { // leading edge of pulse detected
start_of_pulse = millis();
}
- else if ((!signal) && (previous_signal)) { // trailing edge of pulse detected
+ 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")
+ 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")
+ }
+ 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")
+ }
+ 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
+ }
+ 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 ((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!
+ 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.
}
- 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;
+ calibrate(); // calibrate the software timer
}
- if (framesync) { // yellow LED = we currently have frame sync
- analogWrite(yellowled, 10); // (we run it at a low intensity)
+ if (framesync) { // yellow LED = we currently have frame sync
+ digitalWrite(cleantimecodeled, HIGH); // (we run it at a low intensity)
}
else {
- digitalWrite(yellowled, LOW);
+ digitalWrite(cleantimecodeled, LOW);
}
if ((framesync) && (position_in_frame < 60)) {
if (time_is_set && (minute != previous_minute)) {
previous_minute = minute;
- int h12 = (hour % 12) ;
- if (h12 == 0) h12 = 12;
+ 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[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);
+ if ((m - last_sync) < 86400000) { // green LED = got a good sync in the last 24 hours
+ digitalWrite(last24led, HIGH);
}
else {
- digitalWrite(greenled, LOW);
+ digitalWrite(last24led, LOW);
}
}
// Write the display buffer to the display
-void show()
-{
- // display the time
+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] >> 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 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()
-{
+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) {
+ for (i = 0; i < 6; ++i) {
if (framebuf[markers[i]] != 2) {
return;
}
newhour += (framebuf[17] ? 2 : 0);
newhour += (framebuf[18] ? 1 : 0);
if ((newhour < 0) || (newhour > 23)) {
- return; // reject impossible hours
+ return; // reject impossible hours
}
newminute = (framebuf[1] ? 40 : 0);
newminute += (framebuf[7] ? 2 : 0);
newminute += (framebuf[8] ? 1 : 0);
if ((newminute < 0) || (newminute > 59)) {
- return; // reject impossible minutes
+ return; // reject impossible minutes
}
// advance 1 minute because WWVB gives the *previous* minute
// 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)
+ 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 1: // dst ends today (adjust if local hour < 2)
- if (newhour < 2) {
- ++newhour;
- }
- break;
+ }
+ 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
// 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)
- }
+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 = millis_per_minute + 800; // snap forward to :00.8 (minute will advance automatically)
+ }
+}
+
+
+// By determining how many timer ticks elapsed between two minute markers, we can calibrate our software clock.
+// Nominally it is 60000 milliseconds, but the software clock tends to drift.
+// So we start with an array of all 60000 ms, and we keep ten calibrations and average them.
+void calibrate() {
+
+ static unsigned long mpm_array[10] = { 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000 };
+ static int mpm = 0; // next one to update
+
+ static unsigned long last_calib = -86398000;
+ unsigned long m = millis();
+ unsigned long mm = m - last_calib;
+ if ((mm > 50000) && (mm < 70000)) {
+ mpm_array[mpm++] = mm;
+ if (mpm >= 10) {
+ mpm = 0;
+ }
+ millis_per_minute = (mpm_array[0] + mpm_array[1] + mpm_array[2] + mpm_array[3]
+ + mpm_array[4] + mpm_array[5] + mpm_array[6] + mpm_array[7]
+ + mpm_array[8] + mpm_array[9]) / 10;
+ }
+ last_calib = m;
}