├── 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 | --------------------------------------------------------------------------------