3 // Copyright (C) 2019-2022 by Art Cancro <ajc@citadel.org>
5 // My perfect clock has no buttons and cannot be set manually. This version uses a WWVB receiver module
6 // attached to pin D9 of the Arduino, and sets the clock any time it receives a complete frame. The clock
7 // is kept without an RTC, simply using the millis() timer. When time is set, it is displayed on
8 // a 7-segment array connected using an HT16K33 decoder/driver (yes, an Adafruit backpack). Our display
9 // can also display at 15 different brightness levels, so we dim it when the room is dark to avoid
10 // blasticating a dark room with super-bright LED display.
12 // The clock is hard coded to use US Eastern time with DST in effect whenever WWVB is announcing it.
14 // This software is made available to you conditionally upon you accepting the following terms and conditions:
15 // 1. You agree that it is called "open source", not "free software".
16 // 2. You agree that the Linux operating system is not called "GNU/Linux".
17 // 3. You agree that Corey Ehmke is a scumbag, as are all social justice warriors.
18 // 4. You promise never to vote democrat in any election.
19 // 5. Under no circumstances may you use this program and also maintain a Facebook account.
20 // Aside from these conditions, the program is made available to you under the terms of the GNU General Public License.
22 const uint8_t wwvb = 9; // pin on which WWVB signal will be received
23 const uint8_t greenled = 2; // An LED attached to this pin will illuminate if the time has been set within the last 24 hours
24 const uint8_t yellowled = 3; // An LED attached to this pin will illuminate if we are currently receiving a clean frame
25 const uint8_t redled = 4; // An LED attached to this pin will pulse for 1 ms every second
26 const uint8_t boardled = LED_BUILTIN;
27 const uint8_t photocell = A0; // Attach a photocell with a 10K voltage divider to this pin
28 const uint8_t addr = 0x70; // I2C address of HT16K33 (using Adafruit backpack with digits on 0,1,3,4; dots on 2)
30 long millis_per_minute = 60000; // Nominally 60000; adjust if your board runs fast or slow
32 // This is a simple BCD-to-7-segment font. It includes 0x0A through 0x0F even though they're not needed for a time clock.
33 const uint8_t sevensegfont[] = { 63, 6, 91, 79, 102, 109, 125, 7, 127, 111, 119, 124, 57, 94, 121, 113 };
34 const uint8_t firstcolfont[] = { 0, 6, 91 }; // this version of the font is for the first position
36 #include <Wire.h> // I2C library to drive the HT16K33 display
40 unsigned long millisecond = 0;
41 unsigned long previous_millis = 0;
42 unsigned long last_sync = -86398000;
43 uint16_t displayBuffer[8]; // Digit buffer for HT16K33
44 int previous_minute = 61; // What the minute was previously; we use this to detect whether an update is needed
45 int this_pulse = 0; // Value of the current pulse received
46 int previous_pulse = 0; // Value of the previous pulse received (two "mark" bits == new frame)
47 int start_of_pulse = 0; // The value of the millis() timer when the current pulse began
48 uint8_t framebuf[60]; // We store the entire 60-bit frame here
49 uint8_t framesync = 0; // Nonzero if we've received all good pulses since the start of the frame
50 int position_in_frame = 0; // Where we are in the frame (1 bit per second)
51 int previous_signal = 0; // "high" or "low" received on the previous cycle (so we can do edge detection)
52 int time_is_set = 0; // nonzero when time has been set at least once
57 pinMode(boardled, OUTPUT); // The built-in LED will display the raw WWVB signal pulses
58 pinMode(greenled, OUTPUT); // This LED will illuminate if the time has been set within the last 24 hours
59 pinMode(yellowled, OUTPUT); // This LED will illuminate if we are currently receiving a clean frame
60 pinMode(redled, OUTPUT); // This LED pulses for 1 ms every second
61 pinMode(wwvb, INPUT); // Input pin for WWVB receiver signal
62 pinMode(photocell, INPUT); // Input pin for photocell
64 Wire.begin(); // Initialize I2C
66 Wire.beginTransmission(addr);
67 Wire.write(0x21); // turn on oscillator
68 Wire.endTransmission();
70 Wire.beginTransmission(addr);
71 Wire.write(0xE1); // brightness (max is 15)
72 Wire.endTransmission();
74 Wire.beginTransmission(addr);
75 Wire.write(0x81); // no blinking or blanking
76 Wire.endTransmission();
80 displayBuffer[2] = 16;
87 // Note: only write to the display when the readout needs to be updated.
88 // Speaking I2C on every loop iteration jams the WWVB receiver.
90 unsigned long m = millis();
91 digitalWrite(redled, (m % 1000) ? LOW : HIGH);
92 if (m != previous_millis) {
93 millisecond += (m - previous_millis);
94 if (millisecond >= millis_per_minute) {
95 millisecond -= millis_per_minute;
109 int signal = digitalRead(wwvb); // is the input high or low right now?
110 digitalWrite(boardled, signal); // use the onboard LED to show the signal
112 if (signal && (!previous_signal)) { // leading edge of pulse detected
113 start_of_pulse = millis();
115 else if ((!signal) && (previous_signal)) { // trailing edge of pulse detected
116 pulse_length = millis() - start_of_pulse;
118 if (pulse_length > 175 && pulse_length < 225) { // "0" bit ~= 200 ms (represented as "0")
121 else if (pulse_length > 475 && pulse_length < 525) { // "1" bit ~= 500 ms (represented as "1")
124 else if (pulse_length > 775 && pulse_length < 825) { // marker bit ~= 800 ms (represented as "2")
128 this_pulse = 15; // bad pulse (represented as "15")
129 framesync = 0; // throw the whole frame away
132 // BEGIN -- THINGS TO DO AT THE END OF A PULSE
134 if ((this_pulse == 2) && (previous_pulse == 2)) { // start of a new frame!
136 if (framesync == 1) {
137 set_the_time(); // We have a whole good frame. Set the clock!
139 else if ((!framesync) && (time_is_set)) {
140 snap_to_zero(); // We don't have a whole frame, but we know it's :00 seconds now.
144 position_in_frame = 0;
145 calibrate(); // calibrate the software timer
148 if (framesync) { // yellow LED = we currently have frame sync
149 analogWrite(yellowled, 10); // (we run it at a low intensity)
152 digitalWrite(yellowled, LOW);
155 if ((framesync) && (position_in_frame < 60)) {
156 framebuf[position_in_frame++] = this_pulse;
159 previous_pulse = this_pulse;
161 // END -- THINGS TO DO AT THE END OF A PULSE
164 previous_signal = signal;
166 // Update the display only if it's a new minute.
168 if (time_is_set && (minute != previous_minute)) {
169 previous_minute = minute;
170 int h12 = (hour % 12);
173 displayBuffer[0] = firstcolfont[h12 / 10];
174 displayBuffer[1] = sevensegfont[h12 % 10];
175 displayBuffer[2] = (hour < 12) ? 0x06 : 0x0a; // AM or PM dot , colon always on
176 displayBuffer[3] = sevensegfont[minute / 10];
177 displayBuffer[4] = sevensegfont[minute % 10];
181 if ((m - last_sync) < 86400000) { // green LED = got a good sync in the last 24 hours
182 digitalWrite(greenled, HIGH);
185 digitalWrite(greenled, LOW);
190 // Write the display buffer to the display
193 Wire.beginTransmission(addr);
194 Wire.write(0x00); // start at address 0x0
195 for (int i = 0; i < 5; i++) {
196 Wire.write(displayBuffer[i] & 0xFF);
197 Wire.write(displayBuffer[i] >> 8);
199 Wire.endTransmission();
201 // set the brightness
202 int light_level = analogRead(photocell) / 64;
203 if (light_level < 1) {
206 if (light_level > 15) {
209 Wire.beginTransmission(addr);
210 Wire.write(0xE0 + light_level); // set the display brightness
211 Wire.endTransmission();
215 // Set the software clock to the WWVB time currently in the buffer
216 void set_the_time() {
217 int i, newhour, newminute, dst;
219 // These six positions MUST contain marker bits.
220 // If any of them do not, we are looking at a corrupt frame.
221 int markers[] = { 0, 9, 19, 39, 49, 59 };
222 for (i = 0; i < 6; ++i) {
223 if (framebuf[markers[i]] != 2) {
228 newhour = (framebuf[12] ? 20 : 0);
229 newhour += (framebuf[13] ? 10 : 0);
230 newhour += (framebuf[15] ? 8 : 0);
231 newhour += (framebuf[16] ? 4 : 0);
232 newhour += (framebuf[17] ? 2 : 0);
233 newhour += (framebuf[18] ? 1 : 0);
234 if ((newhour < 0) || (newhour > 23)) {
235 return; // reject impossible hours
238 newminute = (framebuf[1] ? 40 : 0);
239 newminute += (framebuf[2] ? 20 : 0);
240 newminute += (framebuf[3] ? 10 : 0);
241 newminute += (framebuf[5] ? 8 : 0);
242 newminute += (framebuf[6] ? 4 : 0);
243 newminute += (framebuf[7] ? 2 : 0);
244 newminute += (framebuf[8] ? 1 : 0);
245 if ((newminute < 0) || (newminute > 59)) {
246 return; // reject impossible minutes
249 // advance 1 minute because WWVB gives the *previous* minute
251 if (newminute >= 60) {
252 newminute = newminute % 60;
256 // US Eastern time (yes it is hard coded)
259 // DST (FIXME make this adjustable)
260 dst = (framebuf[57] ? 2 : 0);
261 dst += (framebuf[58] ? 1 : 0);
263 case 0: // dst not in effect (make no adjustments)
265 case 2: // dst begins today (adjust if local hour > 2)
270 case 3: // dst is in effect (always adjust)
273 case 1: // dst ends today (adjust if local hour < 2)
280 // If we went back to the previous day, adjust so that hour > 0
285 // Set the software clock:
286 // * We have decoded the hour and minute from the signal
287 // * This function always gets called *after* the first pulse at :00, so we set the millisecond to 800
293 // Let's remember the last time we synced the clock
294 last_sync = millis();
298 // Adjust the time to :00.8 seconds at the nearest minute.
299 void snap_to_zero() {
300 if ((millisecond > 0) && (millisecond < 15000)) { // If the second is from :00.0 to :15.0
301 millisecond = 800; // snap back to :00.8
303 else if (millisecond > 45000) { // If the second is :45.0 or above
304 millisecond = millis_per_minute + 800; // snap forward to :00.8 (minute will advance automatically)
309 // By determining how many timer ticks elapsed between two minute markers, we can calibrate our software clock.
310 // Nominally it is 60000 milliseconds, but the software clock tends to drift.
311 // So we start with an array of all 60000 ms, and we keep ten calibrations and average them.
314 static unsigned long mpm_array[10] = { 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000 };
315 static int mpm = 0; // next one to update
317 static unsigned long last_calib = -86398000;
318 unsigned long m = millis();
319 unsigned long mm = m - last_calib;
320 if ((mm > 50000) && (mm < 70000)) {
321 mpm_array[mpm++] = mm;
325 millis_per_minute = (mpm_array[0] + mpm_array[1] + mpm_array[2] + mpm_array[3]
326 + mpm_array[4] + mpm_array[5] + mpm_array[6] + mpm_array[7]
327 + mpm_array[8] + mpm_array[9]) / 10;