├── README.md
├── WWVB5.ino
├── WWVB7.ino
└── WWVB8.ino
/README.md:
--------------------------------------------------------------------------------
1 | # WWVBClock
2 | WWVB radio clock parts list
3 |
4 | Arduino Uno
5 |
6 | Receiver module
7 |
8 |
9 |
10 |
11 | Receiver IC
12 | ```
13 | #define RADIO_POWERDOWN_PIN 6 // P1
14 | #define RADIO_OUT_PIN 7 // T
15 | ```
16 |
17 | Sparkfun DeadOn Real Ttime Clock
18 |
19 |
20 |
21 | ```
22 | #define RTC_SELECT_PIN 10
23 | #define RTC_INTERRUPT_PIN 2
24 | // GND - GND
25 | // VCC - 5V
26 | // SQW - D2
27 | // CLK - D13 ** conflicts with LED_BUILTIN!
28 | // MISO - D12
29 | // MOSI - D11
30 | // SS - D10
31 | ```
32 |
33 | 8 x 7 segment LED display module (DFR0090)
34 |
35 |
36 |
37 | ```
38 | #define LED_LATCH_PIN 8
39 | #define LED_CLOCK_PIN 3
40 | #define LED_DATA_PIN 9
41 | ```
42 |
--------------------------------------------------------------------------------
/WWVB5.ino:
--------------------------------------------------------------------------------
1 | #include
2 | // Library from https://learn.sparkfun.com/tutorials/deadon-rtc-breakout-hookup-guide
3 |
4 | #define DEBUG_PIN 5
5 |
6 | #define CENTURY 2000
7 |
8 | // WWVB reference https://www.nist.gov/sites/default/files/documents/2017/04/28/SP-432-NIST-Time-and-Frequency-Services-2012-02-13.pdf
9 | // Indices for parts of WWVB frame
10 | enum {
11 | FPRM, // Frame reference marker: .8L+.2H
12 | FPUU, // Unweighted: .2L+.8H
13 | // d1: .5L+.5H / 0 = .2L+.8H
14 | FPM1, // 10 minutes
15 | FPM2, // 1 minutes
16 | FPH1, // 10 hours
17 | FPH2, // 1 hours
18 | FPD1, //100 days
19 | FPD2, // 10 days
20 | FPD3, // 1 days
21 | FPUS, // UTC sign
22 | FPUC, // UTC correction
23 | FPY1, // 10 years
24 | FPY2, // 1 years
25 | FPLY, // Leap year
26 | FPLS, // Leap second
27 | FPDS, // Daylight saving time
28 | FPEF}; // End of frame same as FPRM
29 | // Order of received frame, one per second
30 | const byte FramePattern[] =
31 | // .0 .1 .2 .3 .4 .5 .6 .7 .8 .9
32 | /*0.*/{FPRM,FPM1,FPM1,FPM1,FPUU,FPM2,FPM2,FPM2,FPM2,FPRM,
33 | /*1.*/ FPUU,FPUU,FPH1,FPH1,FPUU,FPH2,FPH2,FPH2,FPH2,FPRM,
34 | /*2.*/ FPUU,FPUU,FPD1,FPD1,FPUU,FPD2,FPD2,FPD2,FPD2,FPRM,
35 | /*3.*/ FPD3,FPD3,FPD3,FPD3,FPUU,FPUU,FPUS,FPUS,FPUS,FPRM,
36 | /*4.*/ FPUC,FPUC,FPUC,FPUC,FPUU,FPY1,FPY1,FPY1,FPY1,FPRM,
37 | /*5.*/ FPY2,FPY2,FPY2,FPY2,FPUU,FPLY,FPLS,FPDS,FPDS,FPEF};
38 | #define FRAME_SIZE 60
39 |
40 | // Receiver module http://canaduino.ca/downloads/60khz.pdf
41 | // Receiver IC http://canaduino.ca/downloads/MAS6180C.pdf
42 | /*
43 | P.2 Note.2 OUT = VSS(low) when carrier amplitude at maximum;
44 | OUT = VDD(high) when carrier amplitude is reduced (modulated)
45 | P.7 Table.5 Recommended pulse width recognition limits for WWVB
46 | Symbol Min Max Unit
47 | T 200ms 100 300 ms
48 | T 500ms 400 600 ms
49 | T 800ms 700 900 ms
50 | */
51 | #define RADIO_POWERDOWN_PIN 6 // P1
52 | #define RADIO_OUT_PIN 7 // T
53 | uint8_t radioPort, radioBit;
54 | #define SAMPLE_HZ 25 // must be a factor of 62500: 2, 4, 5, 10, 20, 25, 50, 100, 125, and less than 128
55 | byte samples, carrierHigh, carrierLast;
56 | volatile byte *patp; // sequence through FramePattern
57 | short decode[FPEF]; // accumulate parts of frame
58 | volatile short frame[FPEF]; // read out parts of frame
59 | boolean received; // full frame received
60 | int fails = 0;
61 | unsigned long cyclesSinceTimeSet = 0x80000000;
62 |
63 | byte sampleSave[FRAME_SIZE]; // samples for debug write
64 |
65 | /* Timer 1 interrupt to measure signal
66 | http://www.robotshop.com/letsmakerobots/arduino-101-timers-and-interrupts
67 | Timer0 8bit used for the timer functions, like delay(), millis() and micros()
68 | Timer1 16bit the Servo library uses timer1 on Arduino Uno (timer5 on Arduino Mega)
69 | Timer2 8bit the tone() function uses timer2
70 | Timer 3,4,5 16bit only available on Arduino Mega boards
71 | */
72 | ISR(TIMER1_COMPA_vect) {
73 | // OUT = VSS(low) when carrier amplitude at maximum;
74 | // OUT = VDD(high) when carrier amplitude is reduced (modulated)
75 | // Recording when carrier at maximum since that is more likely signal, than reduced
76 | // which is more likely noise. (makes no difference, in fact)
77 | // !digitalRead(RADIO_OUT_PIN)
78 | if ((*portInputRegister(radioPort) & radioBit) == 0) ++carrierHigh; // count carrier high if read LOW
79 | ++cyclesSinceTimeSet;
80 | if (--samples == 0) {
81 | // end of one second sample interval
82 | byte c = carrierLast = carrierHigh;
83 | samples = SAMPLE_HZ; carrierHigh = 0; // clear for next sample
84 | byte p = *patp;
85 | sampleSave[patp-FramePattern] = c;
86 | boolean rec = false;
87 | switch (p) {
88 | case FPRM: // Frame reference marker: .8L+.2H
89 | if (c >= (SAMPLE_HZ * 1) / 10 && c < (SAMPLE_HZ * 3) / 10) {
90 | rec = true; // recognize P
91 | }
92 | break;
93 | case FPEF: // End of frame same as FPRM
94 | if (c >= (SAMPLE_HZ * 1) / 10 && c < (SAMPLE_HZ * 3) / 10) {
95 | memcpy(frame, decode, sizeof frame);
96 | received = true;
97 | // leave rec unset, to reset pattern and buffer for next frame
98 | }
99 | break;
100 | case FPUU: // Unweighted: .2L+.8H
101 | if (c >= (SAMPLE_HZ * 7) / 10 && c < (SAMPLE_HZ * 9) / 10) {
102 | rec = true; // recognize 0
103 | }
104 | break;
105 | default: // bit 1: .5L+.5H / 0 = .2L+.8H
106 | /*
107 | if (p < 0 || p >= FPEF || (patp-FramePattern) > FRAME_SIZE) {
108 | digitalWrite(LED_BUILTIN, HIGH); // show indexing error
109 | }
110 | */
111 | // binary coding
112 | if (c >= (SAMPLE_HZ * 7) / 10 && c < (SAMPLE_HZ * 9) / 10) {
113 | rec = true; // recognize 0
114 | decode[p] = (decode[p] << 1);
115 | } else if (c >= (SAMPLE_HZ * 4) / 10 && c < (SAMPLE_HZ * 6) / 10) {
116 | rec = true; // recognize 1
117 | decode[p] = (decode[p] << 1) | 1;
118 | }
119 | break;
120 | }
121 | if (rec) {
122 | ++patp; fails = 0;
123 | } else {
124 | // unmatched, reset pattern and buffer to restart
125 | //need some sience to replace this guesswork
126 | if ((patp-FramePattern) < 10 && ++fails > (FRAME_SIZE*2)
127 | || ++fails > 120) {
128 | // try to align cycle
129 | --samples; fails = 0;
130 | }
131 | patp = FramePattern;
132 | memset(decode,0,sizeof decode);
133 | }
134 | }
135 | }
136 |
137 | // Sparkfun DeadOn Real Ttime Clock https://learn.sparkfun.com/tutorials/deadon-rtc-breakout-hookup-guide
138 | #define RTC_SELECT_PIN 10
139 | #define RTC_INTERRUPT_PIN 2
140 | // GND - GND
141 | // VCC - 5V
142 | // SQW - D2
143 | // CLK - D13 ** conflicts with LED_BUILTIN!
144 | // MISO - D12
145 | // MOSI - D11
146 | // SS - D10
147 |
148 | // 8 x 7 segment LED display module (DFR0090) https://www.dfrobot.com/wiki/index.php/3-Wire_LED_Module_(SKU:DFR0090)
149 | #define LED_LATCH_PIN 8
150 | #define LED_CLOCK_PIN 3
151 | #define LED_DATA_PIN 9
152 | // Table of segments for digits 0-9
153 | const byte LED_Digit_Segments[] = {
154 | 0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90};
155 | #define LED_SEGMENTS_OFF 0xFF
156 | // Table of segments for letters A-Z
157 | byte LED_Letter_Segments[]={
158 | // A B C D E F G H I J K L M
159 | 0xA0,0x83,0xa7,0xa1,0x86,0x8e,0xc2,0x8b,0xe6,0xe1,0x89,0xc7,0xaa,
160 | // N O P Q R S T U V W X Y Z
161 | 0xc8,0xa3,0x8c,0x98,0xce,0x9b,0x87,0xc1,0xe3,0xd5,0xb6,0x91,0xb8};
162 | byte display_segments[8];
163 |
164 | void displayShift(byte segments) {
165 | digitalWrite(LED_LATCH_PIN, LOW);
166 | shiftOut(LED_DATA_PIN, LED_CLOCK_PIN, MSBFIRST, segments);
167 | digitalWrite(LED_LATCH_PIN, HIGH);
168 | }
169 |
170 | void displaySend(void) {
171 | for (int d = 7; d >= 0; d--) displayShift(display_segments[d]);
172 | }
173 |
174 | int zoneHours = 0;
175 |
176 | boolean timeSet = false;
177 | byte daysInMonth[] = {0,31,28,31,30,31,30,31,31,30,31,30,31};
178 | // JanFebMarAprMayJunJulAugSepOctNovDec
179 |
180 | void setTime(void) {
181 | // Pull out frame parts
182 | int mn, hr, dy, us, uc, yr, ly, ls, ds;
183 | mn = frame[FPM1]*10 + frame[FPM2];
184 | hr = frame[FPH1]*10 + frame[FPH2];
185 | dy = frame[FPD1]*100 + frame[FPD2]*10 + frame[FPD3];
186 | us = frame[FPUS];
187 | uc = frame[FPUC];
188 | yr = frame[FPY1]*10 + frame[FPY2] + CENTURY;
189 | ly = frame[FPLY];
190 | ls = frame[FPLS];
191 | ds = frame[FPDS];
192 | if (Serial) {
193 | Serial.print("Y"); Serial.print(yr);
194 | Serial.print("D"); Serial.print(dy);
195 | Serial.print("H"); Serial.print(hr);
196 | Serial.print("M"); Serial.print(mn);
197 | Serial.print("US"); if (us==5) Serial.write('+'); else if (us==2) Serial.write('-'); else Serial.print(us);
198 | Serial.print("UC"); Serial.print(uc);
199 | Serial.print("LY"); Serial.print(ly);
200 | Serial.print("LS"); Serial.print(ls);
201 | Serial.print("DS"); Serial.print(ds);
202 | Serial.write('\r'); Serial.write('\n');
203 | }
204 | // Correct for 1 minute coding delay from on-time point
205 | mn += 1;
206 | if (mn >= 60) {
207 | hr += 1; mn = 0;
208 | if (hr >= 24) {
209 | dy += 1; hr = 0;
210 | if (dy >= 365+ly) {
211 | yr += 1; dy = 1;
212 | }
213 | }
214 | }
215 | // Update crystal clock
216 | rtc.setSecond(0); //TODO correct for delay from time of reception
217 | rtc.setMinute(mn);
218 | rtc.setHour(hr);
219 | rtc.setYear(yr);
220 | int mo=1, dim;
221 | while (1) {
222 | dim = daysInMonth[mo];
223 | if (mo == 2 && ly == 1) dim += 1;
224 | if (dy <= dim) break;
225 | dy -= dim; mo += 1;
226 | }
227 | rtc.setMonth(mo);
228 | rtc.setDay(dy);
229 | /*
230 | if (Serial) {
231 | Serial.print(yr); Serial.write('-');
232 | Serial.print(mo); Serial.write('-');
233 | Serial.print(dy); Serial.write('\r'); Serial.write('\n');
234 | }
235 | */
236 | timeSet = true;
237 | cyclesSinceTimeSet = 0;
238 | }
239 |
240 | void timeToDisplay(void) {
241 | rtc.update();
242 | unsigned long sinceTimeSet = cyclesSinceTimeSet / (SAMPLE_HZ * 86400L); // days
243 | if (sinceTimeSet == 0) {
244 | sinceTimeSet = cyclesSinceTimeSet / (SAMPLE_HZ * 8640L); // tenth days
245 | //if (rtc.second() == 0) { Serial.print("sinceTimeSet "); Serial.println(sinceTimeSet); }
246 | if (sinceTimeSet > 9) display_segments[7] = 0xB6; // X
247 | else display_segments[7] = LED_Digit_Segments[sinceTimeSet];
248 | display_segments[6] = 0x47; // L.
249 | } else if (sinceTimeSet <= 9) {
250 | display_segments[7] = LED_Digit_Segments[sinceTimeSet];
251 | display_segments[6] = 0xB6; // X
252 | } else {
253 | display_segments[7] = display_segments[6] = 0xB6; // X X
254 | }
255 | int d = rtc.second();
256 | display_segments[5] = LED_Digit_Segments[d % 10];
257 | display_segments[4] = LED_Digit_Segments[d / 10];
258 | d = rtc.minute();
259 | display_segments[3] = LED_Digit_Segments[d % 10];
260 | display_segments[2] = LED_Digit_Segments[d / 10];
261 | d = rtc.hour();
262 | display_segments[1] = LED_Digit_Segments[d % 10];
263 | display_segments[0] = LED_Digit_Segments[d / 10];
264 | displaySend();
265 | }
266 |
267 | void setup(void) {
268 | Serial.begin(115200);
269 | pinMode(LED_BUILTIN, OUTPUT);
270 | pinMode(DEBUG_PIN, INPUT_PULLUP);
271 | pinMode(RADIO_POWERDOWN_PIN, OUTPUT);
272 | pinMode(RADIO_OUT_PIN, INPUT);
273 | pinMode(RTC_INTERRUPT_PIN, INPUT_PULLUP);
274 | pinMode(LED_LATCH_PIN, OUTPUT);
275 | pinMode(LED_DATA_PIN, OUTPUT);
276 | pinMode(LED_CLOCK_PIN, OUTPUT);
277 | // Clear display
278 | for (int d = 7; d >= 0; d--) displayShift(LED_SEGMENTS_OFF);
279 | // Initialize RT clock library
280 | rtc.begin(RTC_SELECT_PIN);
281 | rtc.writeSQW(SQW_SQUARE_1); // 1Hz signal on RTC_INTERRUPT_PIN
282 | if (rtc.readFromSRAM(0) == 'Z') {
283 | zoneHours = '0' - rtc.readFromSRAM(1);
284 | Serial.print("Zone hours ");
285 | Serial.println(zoneHours);
286 | }
287 | // Start radio
288 | digitalWrite(RADIO_POWERDOWN_PIN, LOW); // turn on radio
289 | radioPort = digitalPinToPort(RADIO_OUT_PIN); // for optimized digitalRead
290 | radioBit = digitalPinToBitMask(RADIO_OUT_PIN);
291 | samples = SAMPLE_HZ; carrierHigh = 0;
292 | patp = FramePattern; received = false;
293 | // Start timer1 for periodic interrupt at SAMPLE_HZ per second
294 | noInterrupts();
295 | TCCR1A = 0;
296 | TCCR1B = 0;
297 | TCNT1 = 0;
298 | OCR1A = F_CPU / 256 / SAMPLE_HZ - 1; // compare match register 16MHz / 256 prescaler / SAMPLE_HZ
299 | // https://github.com/ahooper/WWVBClock/issues/1
300 | TCCR1B |= (1 << WGM12); // CTC mode
301 | TCCR1B |= (1 << CS12); // 256 prescaler
302 | TIMSK1 |= (1 << OCIE1A); // enable timer compare interrupt
303 | interrupts();
304 | }
305 |
306 | void printSamples(int n) {
307 | for (int x = 0; x < n; x++) Serial.write('0'+(sampleSave[x]*10)/SAMPLE_HZ);
308 | Serial.write('\r'); Serial.write('\n');
309 | }
310 |
311 | int maxMatched = 0, lastCycle = HIGH;
312 | byte signalSegments[] = {
313 | /*0*/~(0x80),
314 | /*1*/~(0x08),
315 | /*2*/~(0x08),
316 | /*3*/~(0x08|0x04|0x10),
317 | /*4*/~(0x08|0x40),
318 | /*5*/~(0x08|0x40),
319 | /*6*/~(0x08|0x40|0x04|0x10),
320 | /*7*/~(0x08|0x40|0x01),
321 | /*8*/~(0x08|0x40|0x01),
322 | /*9*/~(0x08|0x40|0x01|0x04|0x10),
323 | /*10*/~(0x08|0x40|0x01|0x02|0x20),
324 | };
325 | int previousSerial = 0;
326 |
327 | void loop(void) {
328 | if (received) {
329 | setTime();
330 | //if (Serial) printSamples(FRAME_SIZE);
331 | received = false;
332 | maxMatched = 0;
333 | } else if (digitalRead(RTC_INTERRUPT_PIN) != lastCycle) {
334 | // 1 Hz signal
335 | lastCycle = digitalRead(RTC_INTERRUPT_PIN);
336 | if (lastCycle == LOW) {
337 | if (timeSet) timeToDisplay();
338 | else {
339 | // show carrier samples while waiting for lock
340 | displayShift(signalSegments[(carrierLast*10)/SAMPLE_HZ]);
341 | }
342 | }
343 | } else if (Serial && Serial.available()) {
344 | int r = Serial.read();
345 | if (previousSerial == 'Z') {
346 | if (r >= '0' && r <= '9') {
347 | zoneHours = '0' - r;
348 | rtc.writeToSRAM(0,'Z'); rtc.writeToSRAM(1,r);
349 | Serial.print("Zone hours ");
350 | Serial.println(zoneHours);
351 | }
352 | }
353 | previousSerial = r;
354 | } else if ((!digitalRead(DEBUG_PIN)) && Serial) {
355 | int m = patp - FramePattern;
356 | if (m > maxMatched) {
357 | Serial.print(m); Serial.write(' ');
358 | printSamples(m);
359 | maxMatched = m;
360 | }
361 | }
362 | }
363 |
364 |
--------------------------------------------------------------------------------
/WWVB7.ino:
--------------------------------------------------------------------------------
1 | #include
2 | // Library from https://learn.sparkfun.com/tutorials/deadon-rtc-breakout-hookup-guide
3 |
4 | #define DEBUG_PIN 5
5 |
6 | #define CENTURY 2000
7 |
8 | // WWVB reference https://www.nist.gov/sites/default/files/documents/2017/04/28/SP-432-NIST-Time-and-Frequency-Services-2012-02-13.pdf
9 | // Indices for parts of WWVB frame
10 | enum {
11 | FPRM, // Frame reference marker: .8L+.2H
12 | FPUU, // Unweighted: .2L+.8H
13 | // d1: .5L+.5H / 0 = .2L+.8H
14 | FPM1, // 10 minutes
15 | FPM2, // 1 minutes
16 | FPH1, // 10 hours
17 | FPH2, // 1 hours
18 | FPD1, //100 days
19 | FPD2, // 10 days
20 | FPD3, // 1 days
21 | FPUS, // UTC sign
22 | FPUC, // UTC correction
23 | FPY1, // 10 years
24 | FPY2, // 1 years
25 | FPLY, // Leap year
26 | FPLS, // Leap second
27 | FPDS, // Daylight saving time
28 | FPEF}; // End of frame same as FPRM
29 | // Order of received frame, one per second
30 | const byte FramePattern[] =
31 | // .0 .1 .2 .3 .4 .5 .6 .7 .8 .9
32 | /*0.*/{FPRM,FPM1,FPM1,FPM1,FPUU,FPM2,FPM2,FPM2,FPM2,FPRM,
33 | /*1.*/ FPUU,FPUU,FPH1,FPH1,FPUU,FPH2,FPH2,FPH2,FPH2,FPRM,
34 | /*2.*/ FPUU,FPUU,FPD1,FPD1,FPUU,FPD2,FPD2,FPD2,FPD2,FPRM,
35 | /*3.*/ FPD3,FPD3,FPD3,FPD3,FPUU,FPUU,FPUS,FPUS,FPUS,FPRM,
36 | /*4.*/ FPUC,FPUC,FPUC,FPUC,FPUU,FPY1,FPY1,FPY1,FPY1,FPRM,
37 | /*5.*/ FPY2,FPY2,FPY2,FPY2,FPUU,FPLY,FPLS,FPDS,FPDS,FPEF};
38 | #define FRAME_SIZE 60
39 |
40 | // Receiver module http://canaduino.ca/downloads/60khz.pdf
41 | // Receiver IC http://canaduino.ca/downloads/MAS6180C.pdf
42 | /*
43 | P.2 Note.2 OUT = VSS(low) when carrier amplitude at maximum;
44 | OUT = VDD(high) when carrier amplitude is reduced (modulated)
45 | P.7 Table.5 Recommended pulse width recognition limits for WWVB
46 | Symbol Min Max Unit
47 | T 200ms 100 300 ms
48 | T 500ms 400 600 ms
49 | T 800ms 700 900 ms
50 | */
51 | #define RADIO_POWERDOWN_PIN 6 // P1
52 | #define RADIO_OUT_PIN 7 // T
53 | uint8_t radioPort, radioBit;
54 | #define SAMPLE_HZ 100 // must be a factor of 62500: 2, 4, 5, 10, 20, 25, 50, 100, 125, and less than 128
55 | byte samples, samplesHigh, samplesLow, prevMod;
56 | #define CODE_N 0
57 | #define CODE_U 1
58 | #define CODE_W 2
59 | #define CODE_P 3
60 | #define CODE_X 4
61 | byte code = CODE_N;
62 | unsigned long cyclesSinceTimeSet = 0x80000000;
63 |
64 | /* Timer 1 interrupt to measure signal
65 | http://www.robotshop.com/letsmakerobots/arduino-101-timers-and-interrupts
66 | Timer0 8bit used for the timer functions, like delay(), millis() and micros()
67 | Timer1 16bit the Servo library uses timer1 on Arduino Uno (timer5 on Arduino Mega)
68 | Timer2 8bit the tone() function uses timer2
69 | Timer 3,4,5 16bit only available on Arduino Mega boards
70 | */
71 | ISR(TIMER1_COMPA_vect) {
72 | // OUT = VSS(low) when carrier amplitude at maximum;
73 | // OUT = VDD(high) when carrier amplitude is reduced (modulated)
74 | // !digitalRead(RADIO_OUT_PIN)
75 | byte modulated = *portInputRegister(radioPort) & radioBit;
76 | if (modulated & prevMod) {
77 | samplesLow++;
78 | } else if (!(modulated | prevMod)) {
79 | samplesHigh++;
80 | }
81 | prevMod = modulated;
82 | ++cyclesSinceTimeSet;
83 | if (--samples == 0) {
84 | // end of one sample interval
85 | if (samplesLow > 63*SAMPLE_HZ/100 && samplesLow < 90*SAMPLE_HZ/100) code = CODE_P;
86 | else if (samplesLow > 33*SAMPLE_HZ/100 && samplesLow < 60*SAMPLE_HZ/100) code = CODE_W;
87 | else if (samplesLow > 5*SAMPLE_HZ/100 && samplesLow < 30*SAMPLE_HZ/100) code = CODE_U;
88 | else code = CODE_X;
89 | samples = SAMPLE_HZ; samplesLow = samplesHigh = 0; // clear for next sample
90 | }
91 | }
92 |
93 | // 8 x 7 segment LED display module (DFR0090) https://www.dfrobot.com/wiki/index.php/3-Wire_LED_Module_(SKU:DFR0090)
94 | #define LED_LATCH_PIN 8
95 | #define LED_CLOCK_PIN 3
96 | #define LED_DATA_PIN 9
97 | // Table of segments for digits 0-9
98 | const byte LED_Digit_Segments[] = {
99 | // 0 1 2 3 4 5 6 7 8 9
100 | 0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90};
101 | #define LED_SEGMENTS_OFF 0xFF
102 | // Table of segments for letters A-Z
103 | byte LED_Letter_Segments[]={
104 | // A B C D E F G H I J K L M
105 | 0xA0,0x83,0xa7,0xa1,0x86,0x8e,0xc2,0x8b,0xe6,0xe1,0x89,0xc7,0xaa,
106 | // N O P Q R S T U V W X Y Z
107 | 0xc8,0xa3,0x8c,0x98,0xce,0x9b,0x87,0xc1,0xe3,0xd5,0xb6,0x91,0xb8};
108 | byte display_segments[8];
109 |
110 | void displayShift(byte segments) {
111 | digitalWrite(LED_LATCH_PIN, LOW);
112 | shiftOut(LED_DATA_PIN, LED_CLOCK_PIN, MSBFIRST, segments);
113 | digitalWrite(LED_LATCH_PIN, HIGH);
114 | }
115 |
116 | void displaySend(void) {
117 | for (int d = 7; d >= 0; d--) displayShift(display_segments[d]);
118 | }
119 |
120 | // Sparkfun DeadOn Real Ttime Clock https://learn.sparkfun.com/tutorials/deadon-rtc-breakout-hookup-guide
121 | #define RTC_SELECT_PIN 10
122 | #define RTC_INTERRUPT_PIN 2
123 | // GND - GND
124 | // VCC - 5V
125 | // SQW - D2
126 | // CLK - D13 ** conflicts with LED_BUILTIN!
127 | // MISO - D12
128 | // MOSI - D11
129 | // SS - D10
130 |
131 | int zoneHours = 0;
132 |
133 | byte frame[FRAME_SIZE], frameIndex = 0;
134 | short decode[FPEF]; // accumulated parts of frame
135 | boolean timeSet = false;
136 | byte daysInMonth[] = {0,31,28,31,30,31,30,31,31,30,31,30,31};
137 | // JanFebMarAprMayJunJulAugSepOctNovDec
138 |
139 | void decodeAndSetTime(void) {
140 | // Decode frame parts
141 | memset(decode,0,sizeof decode);
142 | for (int x = 0; x < FRAME_SIZE; x++) {
143 | byte p = FramePattern[x], c = frame[x];
144 | switch (p) {
145 | case FPRM: // Frame reference marker: .8L+.2H
146 | if (c != CODE_P) return;
147 | break;
148 | case FPEF: // End of frame same as FPRM
149 | if (c != CODE_P) return;
150 | break;
151 | case FPUU: // Unweighted: .2L+.8H
152 | if (c != CODE_U) return;
153 | break;
154 | default: // bit 1: .5L+.5H / 0 = .2L+.8H
155 | // binary coding
156 | if (c == CODE_U) decode[p] = (decode[p] << 1);
157 | else if (c == CODE_W) decode[p] = (decode[p] << 1) | 1;
158 | else return;
159 | break;
160 | }
161 | }
162 | // Combine decimal digits
163 | int mn, hr, dy, us, uc, yr, ly, ls, ds;
164 | mn = decode[FPM1]*10 + decode[FPM2];
165 | hr = decode[FPH1]*10 + decode[FPH2];
166 | dy = decode[FPD1]*100 + decode[FPD2]*10 + decode[FPD3];
167 | us = decode[FPUS];
168 | uc = decode[FPUC];
169 | yr = decode[FPY1]*10 + decode[FPY2] + CENTURY;
170 | ly = decode[FPLY];
171 | ls = decode[FPLS];
172 | ds = decode[FPDS];
173 | if (Serial) {
174 | Serial.print("Y"); Serial.print(yr);
175 | Serial.print("D"); Serial.print(dy);
176 | Serial.print("H"); Serial.print(hr);
177 | Serial.print("M"); Serial.print(mn);
178 | Serial.print("US"); if (us==5) Serial.write('+'); else if (us==2) Serial.write('-'); else Serial.print(us);
179 | Serial.print("UC"); Serial.print(uc);
180 | Serial.print("LY"); Serial.print(ly);
181 | Serial.print("LS"); Serial.print(ls);
182 | Serial.print("DS"); Serial.print(ds);
183 | Serial.write('\r'); Serial.write('\n');
184 | }
185 | // Correct for 1 minute coding delay from on-time point
186 | mn += 1;
187 | if (mn >= 60) {
188 | hr += 1; mn = 0;
189 | if (hr >= 24) {
190 | dy += 1; hr = 0;
191 | if (dy >= 365+ly) {
192 | yr += 1; dy = 1;
193 | }
194 | }
195 | }
196 | // Update crystal clock
197 | rtc.setSecond(1); //TODO correct for delay from time of reception
198 | rtc.setMinute(mn);
199 | rtc.setHour(hr);
200 | rtc.setYear(yr);
201 | int mo=1, dim;
202 | while (1) {
203 | dim = daysInMonth[mo];
204 | if (mo == 2 && ly == 1) dim += 1;
205 | if (dy <= dim) break;
206 | dy -= dim; mo += 1;
207 | }
208 | rtc.setMonth(mo);
209 | rtc.setDay(dy);
210 | timeSet = true;
211 | cyclesSinceTimeSet = 0;
212 | }
213 |
214 | void timeToDisplay(void) {
215 | rtc.update();
216 | unsigned long sinceTimeSet = cyclesSinceTimeSet / (SAMPLE_HZ * 86400L); // days
217 | if (sinceTimeSet == 0) {
218 | sinceTimeSet = cyclesSinceTimeSet / (SAMPLE_HZ * 8640L); // tenth days
219 | if (sinceTimeSet > 9) display_segments[7] = 0xB6; // X
220 | else display_segments[7] = LED_Digit_Segments[sinceTimeSet];
221 | display_segments[6] = 0x47; // L.
222 | } else if (sinceTimeSet <= 9) {
223 | display_segments[7] = LED_Digit_Segments[sinceTimeSet];
224 | display_segments[6] = 0xB6; // X
225 | } else {
226 | display_segments[7] = display_segments[6] = 0xB6; // X X
227 | }
228 | int d = rtc.second();
229 | display_segments[5] = LED_Digit_Segments[d % 10];
230 | display_segments[4] = LED_Digit_Segments[d / 10];
231 | d = rtc.minute();
232 | display_segments[3] = LED_Digit_Segments[d % 10];
233 | display_segments[2] = LED_Digit_Segments[d / 10];
234 | d = rtc.hour();
235 | display_segments[1] = LED_Digit_Segments[d % 10];
236 | display_segments[0] = LED_Digit_Segments[d / 10];
237 | displaySend();
238 | }
239 |
240 | void setup(void) {
241 | Serial.begin(115200);
242 | pinMode(LED_BUILTIN, OUTPUT);
243 | pinMode(DEBUG_PIN, INPUT_PULLUP);
244 | pinMode(RADIO_POWERDOWN_PIN, OUTPUT);
245 | pinMode(RADIO_OUT_PIN, INPUT);
246 | pinMode(RTC_INTERRUPT_PIN, INPUT_PULLUP);
247 | pinMode(LED_LATCH_PIN, OUTPUT);
248 | pinMode(LED_DATA_PIN, OUTPUT);
249 | pinMode(LED_CLOCK_PIN, OUTPUT);
250 | // Clear display
251 | for (int d = 7; d >= 0; d--) displayShift(LED_SEGMENTS_OFF);
252 | // Initialize RT clock library
253 | rtc.begin(RTC_SELECT_PIN);
254 | rtc.writeSQW(SQW_SQUARE_1); // 1Hz signal on RTC_INTERRUPT_PIN
255 | if (rtc.readFromSRAM(0) == 'Z') {
256 | zoneHours = '0' - rtc.readFromSRAM(1);
257 | Serial.print("Zone hours ");
258 | Serial.println(zoneHours);
259 | }
260 | // Start radio
261 | digitalWrite(RADIO_POWERDOWN_PIN, LOW); // turn on radio
262 | radioPort = digitalPinToPort(RADIO_OUT_PIN); // for optimized digitalRead
263 | radioBit = digitalPinToBitMask(RADIO_OUT_PIN);
264 | samples = SAMPLE_HZ; samplesLow = samplesHigh = 0;
265 | // Start timer1 for periodic interrupt at SAMPLE_HZ per second
266 | noInterrupts(); {
267 | TCCR1A = 0;
268 | TCCR1B = 0;
269 | TCNT1 = 0;
270 | OCR1A = F_CPU / 256 / SAMPLE_HZ - 1; // compare match register 16MHz / 256 prescaler / SAMPLE_HZ
271 | // https://github.com/ahooper/WWVBClock/issues/1
272 | TCCR1B |= (1 << WGM12); // CTC mode
273 | TCCR1B |= (1 << CS12); // 256 prescaler
274 | TIMSK1 |= (1 << OCIE1A); // enable timer compare interrupt
275 | } interrupts();
276 | }
277 |
278 | byte prevCode = CODE_N;
279 | // _N _U _W _P _X
280 | char printCode[CODE_X+1] = {' ','.','-','|','*'};
281 | int lastCycle = HIGH, prevSerial = 0;
282 | byte signalSegments[] = {
283 | /*0*/~(0x80),
284 | /*1*/~(0x08),
285 | /*2*/~(0x08),
286 | /*3*/~(0x08|0x04|0x10),
287 | /*4*/~(0x08|0x40),
288 | /*5*/~(0x08|0x40),
289 | /*6*/~(0x08|0x40|0x04|0x10),
290 | /*7*/~(0x08|0x40|0x01),
291 | /*8*/~(0x08|0x40|0x01),
292 | /*9*/~(0x08|0x40|0x01|0x04|0x10),
293 | /*10*/~(0x08|0x40|0x01|0x02|0x20),
294 | };
295 |
296 | void loop(void) {
297 | if (code > CODE_N) {
298 | // once per second
299 | if (code == CODE_P && prevCode == CODE_P) {
300 | // once per minute
301 | if (frameIndex == FRAME_SIZE) {
302 | decodeAndSetTime();
303 | }
304 | if (Serial) { Serial.write('\r'); Serial.write('\n'); }
305 | frameIndex = 0;
306 | }
307 | if (frameIndex < FRAME_SIZE) frame[frameIndex++] = code;
308 | if (Serial) { Serial.write(printCode[code]); }
309 | prevCode = code; code = CODE_N;
310 | } else if (digitalRead(RTC_INTERRUPT_PIN) != lastCycle) {
311 | // 1 Hz signal from crystal clock
312 | lastCycle = digitalRead(RTC_INTERRUPT_PIN);
313 | if (lastCycle == LOW) {
314 | timeToDisplay();
315 | /*
316 | if (timeSet) timeToDisplay();
317 | else {
318 | // show carrier samples while waiting for lock
319 | displayShift(signalSegments[(samplesLow*10)/SAMPLE_HZ]);
320 | }
321 | */
322 | }
323 | } else if (Serial && Serial.available()) {
324 | int r = Serial.read();
325 | if (prevSerial == 'Z') {
326 | // Set time zone
327 | if (r >= '0' && r <= '9') {
328 | zoneHours = '0' - r;
329 | rtc.writeToSRAM(0,'Z'); rtc.writeToSRAM(1,r);
330 | Serial.print("Zone hours ");
331 | Serial.println(zoneHours);
332 | }
333 | }
334 | prevSerial = r;
335 | }
336 | }
337 |
--------------------------------------------------------------------------------
/WWVB8.ino:
--------------------------------------------------------------------------------
1 | #include
2 | // Library from https://learn.sparkfun.com/tutorials/deadon-rtc-breakout-hookup-guide
3 |
4 | #define CENTURY 2000
5 |
6 | // WWVB reference https://www.nist.gov/sites/default/files/documents/2017/04/28/SP-432-NIST-Time-and-Frequency-Services-2012-02-13.pdf
7 | // Indices for parts of WWVB frame
8 | enum {
9 | FPRM, // Frame reference marker: .8L+.2H
10 | FPUU, // Unweighted: .2L+.8H
11 | // d1: .5L+.5H / 0 = .2L+.8H
12 | FPM1, // 10 minutes
13 | FPM2, // 1 minutes
14 | FPH1, // 10 hours
15 | FPH2, // 1 hours
16 | FPD1, //100 days
17 | FPD2, // 10 days
18 | FPD3, // 1 days
19 | FPUS, // UTC sign
20 | FPUC, // UTC correction
21 | FPY1, // 10 years
22 | FPY2, // 1 years
23 | FPLY, // Leap year
24 | FPLS, // Leap second
25 | FPDS, // Daylight saving time
26 | FPEF}; // End of frame same as FPRM
27 | // Order of received frame, one per second
28 | const byte FramePattern[] =
29 | // .0 .1 .2 .3 .4 .5 .6 .7 .8 .9
30 | /*0.*/{FPRM,FPM1,FPM1,FPM1,FPUU,FPM2,FPM2,FPM2,FPM2,FPRM,
31 | /*1.*/ FPUU,FPUU,FPH1,FPH1,FPUU,FPH2,FPH2,FPH2,FPH2,FPRM,
32 | /*2.*/ FPUU,FPUU,FPD1,FPD1,FPUU,FPD2,FPD2,FPD2,FPD2,FPRM,
33 | /*3.*/ FPD3,FPD3,FPD3,FPD3,FPUU,FPUU,FPUS,FPUS,FPUS,FPRM,
34 | /*4.*/ FPUC,FPUC,FPUC,FPUC,FPUU,FPY1,FPY1,FPY1,FPY1,FPRM,
35 | /*5.*/ FPY2,FPY2,FPY2,FPY2,FPUU,FPLY,FPLS,FPDS,FPDS,FPEF};
36 | #define FRAME_SIZE 60
37 |
38 | // Receiver module http://canaduino.ca/downloads/60khz.pdf
39 | // Receiver IC http://canaduino.ca/downloads/MAS6180C.pdf
40 | /*
41 | P.2 Note.2 OUT = VSS(low) when carrier amplitude at maximum;
42 | OUT = VDD(high) when carrier amplitude is reduced (modulated)
43 | P.7 Table.5 Recommended pulse width recognition limits for WWVB
44 | Symbol Min Max Unit
45 | T 200ms 100 300 ms
46 | T 500ms 400 600 ms
47 | T 800ms 700 900 ms
48 | */
49 | #define RADIO_POWERDOWN_PIN 6 // P1
50 | #define RADIO_IN_PIN 7 // T
51 | uint8_t radioPort, radioBit;
52 |
53 | #define SAMPLE_HZ 50 // must be a factor of 62500: 2, 4, 5, 10, 20, 25, 50, 100, 125
54 | #define CODE_N 0
55 | #define CODE_U 1
56 | #define CODE_W 2
57 | #define CODE_P 3
58 | #define CODE_X 4
59 | byte code = CODE_N;
60 | unsigned long cyclesSinceTimeSet = 0x80000000;
61 |
62 | // Moving average filter http://www.dspguide.com/CH15.PDF
63 | #define AVERAGING_LENGTH 10
64 | uint8_t past[AVERAGING_LENGTH];
65 | uint8_t avg, xpast, modcount, dur, pr;
66 | #define EMPTY 0xFF
67 |
68 | /* Timer 1 interrupt to measure signal
69 | http://www.robotshop.com/letsmakerobots/arduino-101-timers-and-interrupts
70 | Timer0 8bit used for the timer functions, like delay(), millis() and micros()
71 | Timer1 16bit the Servo library uses timer1 on Arduino Uno (timer5 on Arduino Mega)
72 | Timer2 8bit the tone() function uses timer2
73 | Timer 3,4,5 16bit only available on Arduino Mega boards
74 | */
75 | ISR(TIMER1_COMPA_vect) {
76 | // OUT = VSS(low) when carrier amplitude at maximum;
77 | // OUT = VDD(high) when carrier amplitude is reduced (modulated)
78 | // (-------digitalRead(RADIO_IN_PIN)--------)
79 | uint8_t modulated = (*portInputRegister(radioPort) & radioBit) ? 1 : 0;
80 | // Moving average filter (does not need to actually divide for an average)
81 | avg += modulated - past[xpast];
82 | past[xpast] = modulated;
83 | if (++xpast >= AVERAGING_LENGTH) xpast = 0;
84 | pr = avg;
85 | if (avg >= AVERAGING_LENGTH*5/10) {
86 | ++modcount;
87 | } else {
88 | if (modcount) { dur = modcount; modcount = 0; }
89 | }
90 | }
91 |
92 | // 8 x 7 segment LED display module (DFR0090) https://www.dfrobot.com/wiki/index.php/3-Wire_LED_Module_(SKU:DFR0090)
93 | #define LED_LATCH_PIN 8
94 | #define LED_CLOCK_PIN 3
95 | #define LED_DATA_PIN 9
96 | // Table of segments for digits 0-9
97 | const byte LED_Digit_Segments[] = {
98 | // 0 1 2 3 4 5 6 7 8 9
99 | 0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90};
100 | #define LED_SEGMENTS_OFF 0xFF
101 | // Table of segments for letters A-Z
102 | byte LED_Letter_Segments[]={
103 | // A B C D E F G H I J K L M
104 | 0xA0,0x83,0xa7,0xa1,0x86,0x8e,0xc2,0x8b,0xe6,0xe1,0x89,0xc7,0xaa,
105 | // N O P Q R S T U V W X Y Z
106 | 0xc8,0xa3,0x8c,0x98,0xce,0x9b,0x87,0xc1,0xe3,0xd5,0xb6,0x91,0xb8};
107 | byte display_segments[8];
108 |
109 | void displayShift(byte segments) {
110 | digitalWrite(LED_LATCH_PIN, LOW);
111 | shiftOut(LED_DATA_PIN, LED_CLOCK_PIN, MSBFIRST, segments);
112 | digitalWrite(LED_LATCH_PIN, HIGH);
113 | }
114 |
115 | void displaySend(void) {
116 | for (int d = 7; d >= 0; d--) displayShift(display_segments[d]);
117 | }
118 |
119 | // Sparkfun DeadOn Real Ttime Clock https://learn.sparkfun.com/tutorials/deadon-rtc-breakout-hookup-guide
120 | #define RTC_SELECT_PIN 10
121 | #define RTC_INTERRUPT_PIN 2
122 | // GND - GND
123 | // VCC - 5V
124 | // SQW - D2
125 | // CLK - D13 ** conflicts with LED_BUILTIN!
126 | // MISO - D12
127 | // MOSI - D11
128 | // SS - D10
129 |
130 | int zoneHours = 0;
131 |
132 | byte frame[FRAME_SIZE], frameIndex = 0;
133 | short decode[FPEF]; // accumulated parts of frame
134 | boolean timeSet = false;
135 | byte daysInMonth[] = {0,31,28,31,30,31,30,31,31,30,31,30,31};
136 | // JanFebMarAprMayJunJulAugSepOctNovDec
137 |
138 | void decodeAndSetTime(void) {
139 | // Decode frame parts
140 | /*
141 | decodeAndSetTime() is working with the table in FramePattern[], that
142 | provides the order of the components in the one-minute frame. It checks
143 | that the fixed markers (frame reference, end of frame, unweighted) come at
144 | the expected slots in the one-minute, and also accumulates the bits of the
145 | binary codes for the time and date decimal digit components into decode[].
146 | The various decimal digit components each have a unique index in decode[]
147 | defined by the enumeration FPxx. If the fixed markers don't match at the
148 | expected points, the decode fails by return and the real-time clock is not
149 | updated.
150 |
151 | decodeAndSetTime() is called from loop(), which is looking at the count of
152 | timer-interrupt signal samples over a one second interval to determine the
153 | length of the modulation using empirical thresholds. It saves the
154 | one-seconds counts in frame[] and calls decodeAndSetTime() when it sees two
155 | successive frame markers indicating the end of one frame and start of the next.
156 | */
157 | memset(decode,0,sizeof decode);
158 | for (int x = 0; x < FRAME_SIZE; x++) {
159 | byte p = FramePattern[x], c = frame[x];
160 | switch (p) {
161 | case FPRM: // Frame reference marker: .8L+.2H
162 | if (c != CODE_P) return;
163 | break;
164 | case FPEF: // End of frame same as FPRM
165 | if (c != CODE_P) return;
166 | break;
167 | case FPUU: // Unweighted: .2L+.8H
168 | if (c != CODE_U) return;
169 | break;
170 | default: // bit 1: .5L+.5H / 0 = .2L+.8H
171 | // binary coding
172 | if (c == CODE_U) decode[p] = (decode[p] << 1);
173 | else if (c == CODE_W) decode[p] = (decode[p] << 1) | 1;
174 | else return;
175 | break;
176 | }
177 | }
178 | // Combine decimal digits
179 | int mn, hr, dy, us, uc, yr, ly, ls, ds;
180 | mn = decode[FPM1]*10 + decode[FPM2];
181 | hr = decode[FPH1]*10 + decode[FPH2];
182 | dy = decode[FPD1]*100 + decode[FPD2]*10 + decode[FPD3];
183 | us = decode[FPUS];
184 | uc = decode[FPUC];
185 | yr = decode[FPY1]*10 + decode[FPY2] + CENTURY;
186 | ly = decode[FPLY];
187 | ls = decode[FPLS];
188 | ds = decode[FPDS];
189 | if (Serial) {
190 | Serial.print("Y"); Serial.print(yr);
191 | Serial.print("D"); Serial.print(dy);
192 | Serial.print("H"); Serial.print(hr);
193 | Serial.print("M"); Serial.print(mn);
194 | Serial.print("US"); if (us==5) Serial.write('+'); else if (us==2) Serial.write('-'); else Serial.print(us);
195 | Serial.print("UC"); Serial.print(uc);
196 | Serial.print("LY"); Serial.print(ly);
197 | Serial.print("LS"); Serial.print(ls);
198 | Serial.print("DS"); Serial.print(ds);
199 | }
200 | // Correct for 1 minute coding delay from on-time point
201 | mn += 1;
202 | if (mn >= 60) {
203 | hr += 1; mn = 0;
204 | if (hr >= 24) {
205 | dy += 1; hr = 0;
206 | if (dy >= 365+ly) {
207 | yr += 1; dy = 1;
208 | }
209 | }
210 | }
211 | int mo=1, dim;
212 | while (1) {
213 | dim = daysInMonth[mo];
214 | if (mo == 2 && ly == 1) dim += 1;
215 | if (dy <= dim) break;
216 | dy -= dim; mo += 1;
217 | }
218 | // Update crystal clock
219 | rtc.setSecond(1); //TODO correct for delay from time of reception
220 | rtc.setMinute(mn);
221 | rtc.setHour(hr);
222 | rtc.setYear(yr-CENTURY);
223 | rtc.setMonth(mo);
224 | rtc.setDate(dy);
225 | timeSet = true;
226 | cyclesSinceTimeSet = 0;
227 | /*
228 | // Interim display decoded day and time
229 | int d;
230 | d = mn;
231 | display_segments[7] = LED_Digit_Segments[d % 10];
232 | display_segments[6] = LED_Digit_Segments[d / 10];
233 | d = hr;
234 | display_segments[5] = LED_Digit_Segments[d % 10];
235 | display_segments[4] = LED_Digit_Segments[d / 10];
236 | d = dy;
237 | display_segments[3] = LED_Digit_Segments[d % 10];
238 | display_segments[2] = LED_Digit_Segments[d / 10];
239 | d = mo;
240 | display_segments[1] = LED_Digit_Segments[d % 10];
241 | display_segments[0] = LED_Digit_Segments[d / 10];
242 | displaySend();
243 | */
244 | }
245 |
246 | void dateToDisplay(void) {
247 | int d = rtc.date();
248 | display_segments[7] = LED_Digit_Segments[d % 10];
249 | display_segments[6] = LED_Digit_Segments[d / 10];
250 | display_segments[5] = LED_SEGMENTS_OFF;
251 | d = rtc.month();
252 | display_segments[4] = LED_Digit_Segments[d % 10];
253 | display_segments[3] = LED_Digit_Segments[d / 10];
254 | display_segments[2] = LED_SEGMENTS_OFF;
255 | d = rtc.year();
256 | display_segments[1] = LED_Digit_Segments[d % 10];
257 | display_segments[0] = LED_Digit_Segments[d / 10];
258 | displaySend();
259 | }
260 |
261 |
262 | void timeToDisplay(void) {
263 | rtc.update();
264 | unsigned long sinceTimeSet = cyclesSinceTimeSet / (SAMPLE_HZ * 86400L); // days
265 | if (sinceTimeSet == 0) {
266 | sinceTimeSet = cyclesSinceTimeSet / (SAMPLE_HZ * 8640L); // tenth days
267 | if (sinceTimeSet > 9) display_segments[7] = 0xB6; // X
268 | else display_segments[7] = LED_Digit_Segments[sinceTimeSet];
269 | display_segments[6] = 0x47; // L.
270 | } else if (sinceTimeSet <= 9) {
271 | display_segments[7] = LED_Digit_Segments[sinceTimeSet];
272 | display_segments[6] = 0xB6; // X
273 | } else {
274 | display_segments[7] = display_segments[6] = 0xB6; // X X
275 | }
276 | int d = rtc.second();
277 | if (d < 3) {
278 | // show the date at the start of the minute
279 | dateToDisplay();
280 | return;
281 | }
282 | display_segments[5] = LED_Digit_Segments[d % 10];
283 | display_segments[4] = LED_Digit_Segments[d / 10];
284 | d = rtc.minute();
285 | display_segments[3] = LED_Digit_Segments[d % 10];
286 | display_segments[2] = LED_Digit_Segments[d / 10];
287 | d = rtc.hour();
288 | display_segments[1] = LED_Digit_Segments[d % 10];
289 | display_segments[0] = LED_Digit_Segments[d / 10];
290 | displaySend();
291 | }
292 |
293 | char X[] = " 56789!@#";
294 | //char X[] = " 123456789!@#";
295 | // 1 2 3 4 5
296 | // 012345678901234567890123456789012345678901234567890123456789
297 | char L[] = ".abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
298 | int wrap, n;
299 |
300 | byte prevCode = CODE_N;
301 | // _N _U _W _P _X
302 | char printCode[CODE_X+1] = {' ','.','-','|','*'};
303 | byte signalSegments[] = {
304 | /*0*/~(0x80),
305 | /*1*/~(0x08),
306 | /*2*/~(0x08),
307 | /*3*/~(0x08|0x04|0x10),
308 | /*4*/~(0x08|0x40),
309 | /*5*/~(0x08|0x40),
310 | /*6*/~(0x08|0x40|0x04|0x10),
311 | /*7*/~(0x08|0x40|0x01),
312 | /*8*/~(0x08|0x40|0x01),
313 | /*9*/~(0x08|0x40|0x01|0x04|0x10),
314 | /*10*/~(0x08|0x40|0x01|0x02|0x20),
315 | };
316 | int lastCycle = HIGH, prevSerial = 0;
317 |
318 | void loop(void) {
319 | if (pr != EMPTY) {
320 | uint8_t p = pr; pr = EMPTY;
321 | if (dur) {
322 | uint8_t d = dur; dur = 0;
323 | if (d >= 68*SAMPLE_HZ/100 && d < 90*SAMPLE_HZ/100) code = CODE_P;
324 | else if (d >= 38*SAMPLE_HZ/100 && d < 60*SAMPLE_HZ/100) code = CODE_W;
325 | else if (d >= 8*SAMPLE_HZ/100 && d < 30*SAMPLE_HZ/100) code = CODE_U;
326 | else code = CODE_X;
327 | if (Serial) {
328 | if (code != CODE_X) Serial.write(printCode[code]);
329 | else Serial.write(L[d]); // diagnostic detail
330 | }
331 | // show codes while waiting for lock
332 | // displayShift(signalSegments[code]);
333 | static_assert(SAMPLE_HZ=SAMPLE_HZ) {
351 | // Serial.write('\r'); Serial.write('\n');
352 | // wrap=0;
353 | // Serial.write('0'+n);
354 | // if (++n>=10) n = 0;
355 | // }
356 | }
357 | if (digitalRead(RTC_INTERRUPT_PIN) != lastCycle) {
358 | // 1 Hz signal from crystal clock
359 | lastCycle = digitalRead(RTC_INTERRUPT_PIN);
360 | if (lastCycle == LOW) {
361 | timeToDisplay();
362 | }
363 | }
364 | if (Serial && Serial.available()) {
365 | int r = Serial.read();
366 | if (prevSerial == 'Z') {
367 | // Set time zone
368 | if (r >= '0' && r <= '9') {
369 | zoneHours = '0' - r;
370 | rtc.writeToSRAM(0,'Z'); rtc.writeToSRAM(1,r);
371 | Serial.print("Zone hours ");
372 | Serial.println(zoneHours);
373 | }
374 | }
375 | prevSerial = r;
376 | }
377 | }
378 |
379 | void setup(void) {
380 | Serial.begin(115200);
381 | pinMode(LED_BUILTIN, OUTPUT);
382 | pinMode(RADIO_POWERDOWN_PIN, OUTPUT);
383 | pinMode(RADIO_IN_PIN, INPUT);
384 | pinMode(LED_LATCH_PIN, OUTPUT);
385 | pinMode(LED_DATA_PIN, OUTPUT);
386 | pinMode(LED_CLOCK_PIN, OUTPUT);
387 | // Clear display
388 | for (int d = 7; d >= 0; d--) displayShift(LED_SEGMENTS_OFF);
389 | // Initialize RT clock library
390 | rtc.begin(RTC_SELECT_PIN);
391 | rtc.writeSQW(SQW_SQUARE_1); // 1Hz signal on RTC_INTERRUPT_PIN
392 | if (rtc.readFromSRAM(0) == 'Z') {
393 | zoneHours = '0' - rtc.readFromSRAM(1);
394 | if (Serial) {
395 | Serial.print("Zone hours ");
396 | Serial.println(zoneHours);
397 | }
398 | }
399 | // Start radio
400 | digitalWrite(RADIO_POWERDOWN_PIN, LOW); // turn on radio
401 | radioPort = digitalPinToPort(RADIO_IN_PIN); // for optimized digitalRead
402 | radioBit = digitalPinToBitMask(RADIO_IN_PIN);
403 | avg = 0; xpast = 0; pr = EMPTY;
404 | wrap = 0; n = 0;
405 | // Start timer1 for periodic interrupt at SAMPLE_HZ per second
406 | noInterrupts(); {
407 | TCCR1A = 0;
408 | TCCR1B = 0;
409 | TCNT1 = 0;
410 | OCR1A = F_CPU / 256 / SAMPLE_HZ - 1; // compare match register 16MHz / 256 prescaler / SAMPLE_HZ
411 | // https://github.com/ahooper/WWVBClock/issues/1
412 | TCCR1B |= (1 << WGM12); // CTC mode
413 | TCCR1B |= (1 << CS12); // 256 prescaler
414 | TIMSK1 |= (1 << OCIE1A); // enable timer compare interrupt
415 | } interrupts();
416 | }
417 |
418 |
--------------------------------------------------------------------------------