Reduced hysteresis to 3 samples. Expanded valid pulse width range.
[the_perfect_clock.git] / old_version_with_seconds / the_perfect_clock-oldversionwithseconds.ino
1 // "The Perfect Clock" 
2
3 // Copyright (C) 2019 by Art Cancro <ajc@citadel.org>
4
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).
9
10 // The clock is hard coded to use US Eastern time with DST in effect whenever WWVB is announcing it.
11
12 const uint8_t wwvb = 9;                 // pin on which WWVB signal will be received
13 const uint8_t greenled = 2;             // Attach a green LED to this pin
14 const uint8_t yellowled = 3;            // Attach a yellow LED to this pin
15 const uint8_t redled = 4;               // Attach a red LED to this pin
16 const uint8_t addr = 0x70;              // I2C address of HT16K33 (using Adafruit backpack with digits on 0,1,3,4; dots on 2)
17
18 #define MILLISECONDS_PER_SECOND 1002    // Nominally 1000, but the timer on my Nano runs fast and we don't have an RTC
19
20 // This is a simple BCD-to-7-segment font.  It includes 0x0A through 0x0F even though they're not needed for a time clock.
21 const uint8_t sevensegfont[] = { 63, 6, 91, 79, 102, 109, 125, 7, 127, 111, 119, 124, 57, 94, 121, 113 };
22 const uint8_t firstcolfont[] = { 0, 6, 91 };    // this version of the font is for the first position
23
24 #include <Wire.h>                       // I2C library
25
26 int hour = 0;
27 int minute = 0;
28 int second = 0;
29 unsigned long millisecond = 0;
30 unsigned long previous_millis = 0;
31 unsigned long last_sync = -86398000;
32 uint16_t displayBuffer[8];              // Digit buffer for HT16K33
33 int previous_minute = 61;               // What the minute was previously; we use this to detect whether an update is needed
34 int this_pulse = 0;                     // Value of the current pulse received
35 int previous_pulse = 0;                 // Value of the previous pulse received (two "mark" bits == new frame)
36 int start_of_pulse = 0;                 // The value of the millis() timer when the current pulse began
37 uint8_t framebuf[60];                   // We store the entire 60-bit frame here
38 uint8_t framesync = 0;                  // Nonzero if we've received all good pulses since the start of the frame
39 int position_in_frame = 0;              // Where we are in the frame (this happens to also be the second of the minute)
40 int previous_signal = 0;                // "high" or "low" received on the previous cycle (so we can do edge detection)
41 int time_is_set = 0;                    // nonzero when time has been set at least once
42
43 void setup()
44 {
45         int i;
46
47         pinMode(LED_BUILTIN, OUTPUT);   // The built-in LED will display the raw WWVB signal pulses
48         pinMode(greenled, OUTPUT);
49         pinMode(yellowled, OUTPUT);
50         pinMode(redled, OUTPUT);
51         pinMode(wwvb, INPUT);           // Input pin for WWVB receiver signal
52
53         Wire.begin();                   // Initialize I2C
54
55         Wire.beginTransmission(addr);
56         Wire.write(0x21);               // turn on oscillator
57         Wire.endTransmission();
58
59         Wire.beginTransmission(addr);
60         Wire.write(0xE1);               // brightness (max is 15)
61         Wire.endTransmission();
62
63         Wire.beginTransmission(addr);
64         Wire.write(0x81);               // no blinking or blanking
65         Wire.endTransmission();
66
67         displayBuffer[0] = 0;
68         displayBuffer[1] = 0;
69         displayBuffer[2] = 16;
70         displayBuffer[3] = 0;
71         displayBuffer[4] = 0;
72         show();
73 }
74
75
76 // Note: only write to the display when the readout needs to be updated.
77 // Speaking I2C on every loop iteration jams the WWVB receiver.
78 void loop()
79 {
80         unsigned long m = millis();
81         digitalWrite(redled, (m%1000) ? LOW : HIGH);
82         if (m != previous_millis) {
83                 millisecond += (m - previous_millis);
84                 if (millisecond >= MILLISECONDS_PER_SECOND) {
85                         millisecond -= MILLISECONDS_PER_SECOND;
86                         ++second;
87                         if (second > 59) {
88                                 second = 0;
89                                 ++minute;
90                                 if (minute > 59) {
91                                         minute = 0;
92                                         ++hour;
93                                         if (hour > 23) {
94                                                 hour = 0;
95                                         }
96                                 }
97                         }
98                 }
99         }
100         previous_millis = m;
101
102         int pulse_length;
103         int signal = digitalRead(wwvb);                                         // is the input high or low right now?
104         digitalWrite(LED_BUILTIN, signal);                                      // use the onboard LED to show the signal
105
106         if (signal && (!previous_signal)) {
107                 start_of_pulse = millis();
108         }
109         else if ((!signal) && (previous_signal)) {
110                 pulse_length = millis() - start_of_pulse;
111
112                 if (pulse_length > 150 && pulse_length < 250) {                 // "0" bit ~= 200 ms (represented as "0")
113                         this_pulse = 0;
114                 } else if (pulse_length > 450 && pulse_length < 550) {          // "1" bit ~= 500 ms (represented as "1")
115                         this_pulse = 1;
116                 } else if (pulse_length > 750 && pulse_length < 850) {          // marker bit ~= 800 ms (represented as "2")
117                         this_pulse = 2;
118                 } else {
119                         this_pulse = 15;                                        // bad pulse (represented as "15")
120                         framesync = 0;                                          // throw the whole frame away
121                 }
122
123                 // BEGIN -- THINGS TO DO AT THE END OF A PULSE
124
125                 if ((this_pulse == 2) && (previous_pulse == 2)) {               // start of a new frame!
126
127                         if (framesync == 1) {
128                                 set_the_time();                                 // We have a whole good frame.  Set the clock!
129                         }
130
131                         framesync = 1;
132                         position_in_frame = 0;
133                 }
134
135                 if (framesync) {                                                // yellow LED = we currently have frame sync
136                         analogWrite(yellowled, 10);                             // (we run it at a low intensity)
137                 }
138                 else {
139                         digitalWrite(yellowled, LOW);
140                 }
141
142                 if ((framesync) && (position_in_frame < 60)) {
143                         framebuf[position_in_frame++] = this_pulse;
144                 }
145
146                 previous_pulse = this_pulse;
147
148                 // END -- THINGS TO DO AT THE END OF A PULSE
149         }
150
151         previous_signal = signal;
152
153         // Update the display only if it's a new minute.
154
155         if (time_is_set && (minute != previous_minute)) {
156                 previous_minute = minute;
157                 int h12 = (hour % 12) ;
158                 if (h12 == 0) h12 = 12;
159                 displayBuffer[0] = firstcolfont[h12 / 10];
160                 displayBuffer[1] = sevensegfont[h12 % 10];
161                 displayBuffer[2] = (hour<12) ? 0x06 : 0x0a;             // AM or PM dot , colon always on
162                 displayBuffer[3] = sevensegfont[minute / 10];
163                 displayBuffer[4] = sevensegfont[minute % 10];
164                 show();
165         }
166
167         if ((m - last_sync) < 86400000) {                               // green LED = got a good sync in the last 24 hours
168                 digitalWrite(greenled, HIGH);
169         }
170         else {
171                 digitalWrite(greenled, LOW);
172         }
173 }
174
175
176 // Write the display buffer to the display
177 void show()
178 {
179         Wire.beginTransmission(addr);
180         Wire.write(0x00);       // start at address 0x0
181         for (int i = 0; i < 5; i++) {
182                 Wire.write(displayBuffer[i] & 0xFF);
183                 Wire.write(displayBuffer[i] >> 8);
184         }
185         Wire.endTransmission();
186 }
187
188
189 // Set the software clock to the WWVB time currently in the buffer
190 void set_the_time()
191 {
192         int i, newhour, newminute, dst;
193
194         // These six positions MUST contain marker bits.
195         // If any of them do not, we are looking at a corrupt frame.
196         int markers[] = { 0, 9, 19, 39, 49, 59 };
197         for (i=0; i<6; ++i) {
198                 if (framebuf[markers[i]] != 2) {
199                         return;
200                 }
201         }
202
203         newhour = (framebuf[12] ? 20 : 0);
204         newhour += (framebuf[13] ? 10 : 0);
205         newhour += (framebuf[15] ? 8 : 0);
206         newhour += (framebuf[16] ? 4 : 0);
207         newhour += (framebuf[17] ? 2 : 0);
208         newhour += (framebuf[18] ? 1 : 0);
209         if ((newhour < 0) || (newhour > 23)) {
210                 return;                         // reject impossible hours
211         }
212
213         newminute = (framebuf[1] ? 40 : 0);
214         newminute += (framebuf[2] ? 20 : 0);
215         newminute += (framebuf[3] ? 10 : 0);
216         newminute += (framebuf[5] ? 8 : 0);
217         newminute += (framebuf[6] ? 4 : 0);
218         newminute += (framebuf[7] ? 2 : 0);
219         newminute += (framebuf[8] ? 1 : 0);
220         if ((newminute < 0) || (newminute > 59)) {
221                 return;                         // reject impossible minutes
222         }
223
224         // advance 60 seconds because WWVB gives the *previous* minute
225         newminute += 1;
226         if (newminute >= 60) {
227                 newminute = newminute % 60;
228                 newhour += 1;
229         }
230
231         // US Eastern time (FIXME make this adjustable)
232         newhour -= 5;
233
234         // DST (FIXME make this adjustable)
235         dst = (framebuf[57] ? 2 : 0);
236         dst += (framebuf[58] ? 1 : 0);
237         switch(dst) {
238                 case 0:                         // dst not in effect (make no adjustments)
239                         break;
240                 case 2:                         // dst begins today (adjust if local hour > 2)
241                         if (newhour > 2) {
242                                 ++newhour;
243                         }
244                         break;
245                 case 3:                         // dst is in effect (always adjust)
246                         ++newhour;
247                         break;
248                 case 1:                         // dst ends today (adjust if local hour < 2)
249                         if (hour < 2) {
250                                 ++newhour;
251                         }
252                         break;
253         }
254
255         // If we went back to the previous day, adjust so that hour > 0
256         if (newhour < 0) {
257                 newhour += 24;
258         }
259
260         // Set the software clock:
261         // * We have decoded the hour and minute from the signal
262         // * This function always gets called *after* the first pulse at :00, so we set the second to :00 and millisecond to 800
263         hour = newhour;
264         minute = newminute;
265         second = 0;
266         millisecond = 800;
267         time_is_set = 1;
268
269         // Let's remember the last time we synced the clock
270         last_sync = millis();
271 }