// "The Perfect Clock" // Copyright (C) 2019-2023 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. // 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. // 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 // 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(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.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() { // 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(); if (m != previous_millis) { millisecond += (m - previous_millis); if (millisecond >= millis_per_minute) { millisecond -= millis_per_minute; ++minute; if (minute > 59) { minute = 0; ++hour; if (hour > 23) { hour = 0; } } } } previous_millis = m; int pulse_length; 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 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; calibrate(); // calibrate the software timer } if (framesync) { // yellow LED = we currently have frame sync digitalWrite(cleantimecodeled, HIGH); // (we run it at a low intensity) } else { digitalWrite(cleantimecodeled, 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(last24led, HIGH); } else { digitalWrite(last24led, 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 = 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; }