3 // Copyright (C) 2019 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).
10 // The clock is hard coded to use US Eastern time with DST in effect whenever WWVB is announcing it.
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)
18 #define MILLISECONDS_PER_SECOND 1002 // Nominally 1000, but the timer on my Nano runs fast and we don't have an RTC
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
24 #include <Wire.h> // I2C library
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
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
53 Wire.begin(); // Initialize I2C
55 Wire.beginTransmission(addr);
56 Wire.write(0x21); // turn on oscillator
57 Wire.endTransmission();
59 Wire.beginTransmission(addr);
60 Wire.write(0xE1); // brightness (max is 15)
61 Wire.endTransmission();
63 Wire.beginTransmission(addr);
64 Wire.write(0x81); // no blinking or blanking
65 Wire.endTransmission();
69 displayBuffer[2] = 16;
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.
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;
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
106 if (signal && (!previous_signal)) {
107 start_of_pulse = millis();
109 else if ((!signal) && (previous_signal)) {
110 pulse_length = millis() - start_of_pulse;
112 if (pulse_length > 150 && pulse_length < 250) { // "0" bit ~= 200 ms (represented as "0")
114 } else if (pulse_length > 450 && pulse_length < 550) { // "1" bit ~= 500 ms (represented as "1")
116 } else if (pulse_length > 750 && pulse_length < 850) { // marker bit ~= 800 ms (represented as "2")
119 this_pulse = 15; // bad pulse (represented as "15")
120 framesync = 0; // throw the whole frame away
123 // BEGIN -- THINGS TO DO AT THE END OF A PULSE
125 if ((this_pulse == 2) && (previous_pulse == 2)) { // start of a new frame!
127 if (framesync == 1) {
128 set_the_time(); // We have a whole good frame. Set the clock!
132 position_in_frame = 0;
135 if (framesync) { // yellow LED = we currently have frame sync
136 analogWrite(yellowled, 10); // (we run it at a low intensity)
139 digitalWrite(yellowled, LOW);
142 if ((framesync) && (position_in_frame < 60)) {
143 framebuf[position_in_frame++] = this_pulse;
146 previous_pulse = this_pulse;
148 // END -- THINGS TO DO AT THE END OF A PULSE
151 previous_signal = signal;
153 // Update the display only if it's a new minute.
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];
167 if ((m - last_sync) < 86400000) { // green LED = got a good sync in the last 24 hours
168 digitalWrite(greenled, HIGH);
171 digitalWrite(greenled, LOW);
176 // Write the display buffer to the display
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);
185 Wire.endTransmission();
189 // Set the software clock to the WWVB time currently in the buffer
192 int i, newhour, newminute, dst;
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) {
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
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
224 // advance 60 seconds because WWVB gives the *previous* minute
226 if (newminute >= 60) {
227 newminute = newminute % 60;
231 // US Eastern time (FIXME make this adjustable)
234 // DST (FIXME make this adjustable)
235 dst = (framebuf[57] ? 2 : 0);
236 dst += (framebuf[58] ? 1 : 0);
238 case 0: // dst not in effect (make no adjustments)
240 case 2: // dst begins today (adjust if local hour > 2)
245 case 3: // dst is in effect (always adjust)
248 case 1: // dst ends today (adjust if local hour < 2)
255 // If we went back to the previous day, adjust so that hour > 0
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
269 // Let's remember the last time we synced the clock
270 last_sync = millis();