From: Art Cancro Date: Tue, 18 Feb 2020 21:42:02 +0000 (-0500) Subject: Initial commit X-Git-Url: https://code.citadel.org/?p=the_perfect_clock.git;a=commitdiff_plain;h=addce46764744c46daae043cd598b8228aa9e060 Initial commit --- addce46764744c46daae043cd598b8228aa9e060 diff --git a/the_perfect_clock.ino b/the_perfect_clock.ino new file mode 100644 index 0000000..92444a3 --- /dev/null +++ b/the_perfect_clock.ino @@ -0,0 +1,297 @@ +// "The Perfect Clock" + +// Copyright (C) 2019-2020 by Art Cancro + +// 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 // 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) + } +}