// ******************************* // * * // * GPS2MSF.c * // * * // * GPS to MSF/DCF transcoder * // * * // ******************************* // // C code for Microchip MPLab + C30 compiler on dsPIC33FJ12GP201 // // Reads PPS timing pulse (__-__) and serial data from Garmin 18x LVC OEM GPS receiver // and outputs a "perfect" MSF or DCF formatted time-code pulse stream with 1us // accurate (to UTC) second-markers. // // ------------------------------------------------------------------------------ // // Revision History: // // 27-01-11 V100 Started. // 03-02-11 V101 First working version with MSF & DCF encoders, and full DST support. // 27-03-11 V102 Bug fix: serial GPS data arrival time now less critical => fewer missing pulses. // + Debug serial out (p16) mirrors $GPRMC sentence payload. // 28/03/11 V103 Eliminate missing o/p pulses due to the late arrival of GPS data by using the // existing (old) TimeBuf incremented by 1 sec. // 29/03/11 Changed debug serial out to represent time code output. // 17/05/11 V104 Fix further issues associated with late arrival of serial GPS data, we now wait // until dSec#1 for GPS serial data (at which point we also tx the debug time-code). // We also impose a start point of no earlier than dSec#5 for GPS serial data. // 22/05/11 V105 Input baud rate now 19200, restart/reset GPS serial rx @dSec#5 // Improved DST and DSTWARN calculation, debug message "%" whenever GPS time // doesn't match local UTC accumulator. // 23-05-11 V106 In the event of late GPS serial data (and the output pulse therefore having been // based on dead reckoning) double check the previous output pulse's tCode against // the late data and goto supress mode if there was an error. Supress mode results in // the suppression of all future o/p pulses until the end of the current minute. // Note that this has the effect of supressing all o/ps from power-up until the // start of the new minute. // 18-06-11 v107 Firmware update of 18xLVC sensor resolves the late data problem, so... // revert back to 4800 baud and dSec#9 time limit on GPS data etc etc // 25-09-11 v108 Fix DST month bug October is month 10 (not 9) // 26-03-12 v109 Fix dayofwk() calculation, leap years now have correct DOW // 30-06-12 v110 Fix bug in pty calculation for DCF on Sundays. // ??-??-?? v111 Updated UARTRx to work also witn Quectel GPS NMEA data format // 26-03-16 v111 Consolidated two disperate code versions to make the above "works on the hardware" version // // ------------------------------------------------------------------------------ // To Do: // // // ------------------------------------------------------------------------------ // // Quectel implememtation // // The NMEA data from the Quectel part comes by default at 9600, it is only // possible to change this to 4800 once all but the $GPRMC sentences have been // stopped. So supress the uneccessary sentences first then change the baud-rate. // Note that the UTC time field is presented in an extended format by the Quectel // device. V109 can cope with both this and the Garmin format. // // ------------------------------------------------------------------------------ // // Garmin 18x firmware update: // // In some older firmware revisions (before v3.7), the 18x serial data output bursts // are occasionally delivered FAR too late >1000ms after the corresponding PPS pulse. // To perform a firmware update use www.marvellconsultants.com/gps/garmin_update.zip // // ------------------------------------------------------------------------------ // // Limitations: // // Leap seconds: The Garmin prototcol provides no prior warning of leap second insertion, // and the extra second is added (in the form of a repeated second #0) at the beginning // of the first minute of the calendar month instead of at the end of the last minute // in the (previous) month. // Consequently DCF's A2 alert bit #19 is not implemented and the elongated minute // occurs in the first minute of the month (rather than the last). // // DCF's MeteoTime weather data bits (#1..14) and call bit #15 are obviously not // implemented and are set to 0. // // MSF's DUT1 bits are similarly unimplemented and are also set to 0. // // ------------------------------------------------------------------------------ // // MSF/DCF selection: // // Strap J2.4 (RB0) to J2.3 (GND) to select DCF. // // ------------------------------------------------------------------------------ // // Inputs // // RB14 p15 RP14=>IC1 'PPS' +ve going Pulse-Per-Second from GPS rxvr, triggers IC1 // RB4 p8 RP4=>U1RX 'RXD' Serial Rx 19200bd from GPS rxvr // RB0 p4 'DCF' Lo (J3.3 = J3.4) to request DCF format // // Outputs // // RA4 p9 'LED+' Hi to turn the LED off, Lo for green, pulsing off // RB7 p10 'RES' Lo pulse forces MSF/DCF o/p active lo // RB9 p12 'SET' Lo pulse forces MSF/DCF o/p inactive hi // RB8 p11 'D' Lo to enable propagation of next second marker pulse // Hi to inhibit next second pulse (eg DCF sec59 or no GPS) // RB15 p16 RP15=>U1TX 'TXD' Debug serial out, n/c // // ------------------------------------------------------------------------------ #include "p33FJ12GP201.h" // Configuration reg settings: // _FOSCSEL(FNOSC_FRCPLL) // FRC oscillator with PLL _FOSC(FCKSM_CSDCMD & OSCIOFNC_ON & POSCMD_NONE) // clk sw dis, clk mon dis, IO on OSC2, Posc = off, FRC+PLL _FWDT(FWDTEN_OFF) // watchdog off _FPOR(FPWRT_PWR128) // pwr on rst timer = 128ms _FICD(ICS_PGD1 & JTAGEN_OFF) // ICD on PGEx1, no JTAG // Defines... #define PLL_M 16 // osc PLL feedback divisor Fcy = 14.74 from nom 7.37MHz FRC #define T2RATE 23031 // nominal Timer 2 counter modulus -> 10Hz, 100ms // Set operating baud rate: // Assumes master clock set for Fcy = 14.74MHz // BRG = (14740000 / (16 * BAUD_RATE)) - 1 // 191 => 4800 // 95 => 9600 #define UART_BRG 95 // Global constants... const unsigned char DIM[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31}; // days in month // Gobal Variables... unsigned char DST; // Summer-time when 1 unsigned char DSTWARN; // DST change due within the next hour when 1 // ------------------------ subroutines & functions ------------------------------- // Read data from uart rx buffer // return char if there is one else return 0 // char RdUART(void) { char rxch; // rxch = 0; if (U1STAbits.OERR == 1) U1STAbits.OERR = 0; if (U1STAbits.URXDA == 1) rxch = U1RXREG; return rxch; } // Calculate the day of the week from a date in UARTRx buffer format (DDMMYY) // SUN=0 .. SAT=6 // unsigned char dayofwk(char * buf6) { const char DBMLUT1[13] = {0,0,3,3,6,1,4,6,2,5,0,3,5}; // normal year LUT const char DBMLUT2[13] = {0,6,2,3,6,1,4,6,2,5,0,3,5}; // leap year LUT (remember we've already added the extra day by the time we come to use this LUT) const unsigned char DBY2K = 6; unsigned char d,m,y; unsigned char dow; // d = (buf6[1] & 15) + (10 * (buf6[0] & 15)); m = (buf6[3] & 15) + (10 * (buf6[2] & 15)); y = (buf6[5] & 15) + (10 * (buf6[4] & 15)); dow = DBY2K + y + (y >> 2) + d; if ((y & 3) == 0) dow += DBMLUT2[m]; // leap year else dow += DBMLUT1[m]; // normal year return dow % 7; } // Checks that the buffer contains a valid time in ASCII // Returns 1 if valid, 0 if not // unsigned char TCheck(char * buf6) { unsigned char h,m,s; // if ((buf6[0] > '9') || (buf6[0] < '0')) return 0; if ((buf6[1] > '9') || (buf6[1] < '0')) return 0; if ((buf6[2] > '9') || (buf6[2] < '0')) return 0; if ((buf6[3] > '9') || (buf6[3] < '0')) return 0; if ((buf6[4] > '9') || (buf6[4] < '0')) return 0; if ((buf6[5] > '9') || (buf6[5] < '0')) return 0; // h = (buf6[1] & 15) + (10 * (buf6[0] & 15)); m = (buf6[3] & 15) + (10 * (buf6[2] & 15)); s = (buf6[5] & 15) + (10 * (buf6[4] & 15)); if (h > 23) return 0; if (m > 59) return 0; if (s > 59) return 0; return 1; } // Calculates the number of days in the given month (and year) // unsigned char DaysInMonth(unsigned char m, unsigned char y) { unsigned char d; // d = DIM[m]; if (((y & 3) == 0) && (m == 2)) d++; return d; } // Checks that the buffer contains a valid date in ASCII // Returns 1 if valid, 0 if not // unsigned char DCheck(char * buf6) { unsigned char d,m,y; // if ((buf6[0] > '9') || (buf6[0] < '0')) return 0; if ((buf6[1] > '9') || (buf6[1] < '0')) return 0; if ((buf6[2] > '9') || (buf6[2] < '0')) return 0; if ((buf6[3] > '9') || (buf6[3] < '0')) return 0; if ((buf6[4] > '9') || (buf6[4] < '0')) return 0; if ((buf6[5] > '9') || (buf6[5] < '0')) return 0; // d = (buf6[1] & 15) + (10 * (buf6[0] & 15)); m = (buf6[3] & 15) + (10 * (buf6[2] & 15)); y = (buf6[5] & 15) + (10 * (buf6[4] & 15)); if ((m > 12) || (m < 1)) return 0; if (d < 1) return 0; if (d > DaysInMonth(m, y)) return 0; return 1; } // Grabs serial GPS data a byte at a time and extracts time and date info // from the $GPRMC sentence // Adds the day of week to the recieved date SUN=0 // Posts results to the supplied 13-byte buffer once data is fully assembled // If the supplied pointer is nul then received chars are ignored // Returns a status byte indicating buffer filled 1, or no action 0 // Time and date info in the buffer is sanity checked before returning status 1. // unsigned char UARTRx(char * MainBuf) { static char Buf6[6] = {0,0,0,0,0,0}; // hold most recent 6 chars received static char tBuf[6] = {0,0,0,0,0,0}; // hold time while waiting for date static unsigned char cc = 0; // comma counter = current state unsigned char r; char chr; // if (MainBuf == 0) { cc = 0; Buf6[5] = 0; do chr = RdUART(); while (chr != 0); // flush uart buffer return 0; } // r = 0; chr = RdUART(); if (chr == 0) return 0; chr &= 127; // if ((cc > 0) && (cc < 10)) U1TXREG = chr; // echo the GPRMC sentence, DEBUG ONLY // // &&& To do: check time since last char, if > 20ms then cc=0; // if (chr == '$') cc = 0; if (chr < ' ') cc = 0; // if (chr != ',') { // We want to buffer only the first 6 chars (until the next comma). // So, stop buffering when there's no nul char left in the buffer. // if ((Buf6[0]==0) || (Buf6[1]==0) || (Buf6[2]==0) || (Buf6[3]==0) || (Buf6[4]==0) || (Buf6[5]==0)) { Buf6[0] = Buf6[1]; // shift buffer along and add new char Buf6[1] = Buf6[2]; Buf6[2] = Buf6[3]; Buf6[3] = Buf6[4]; Buf6[4] = Buf6[5]; Buf6[5] = chr; } } else { // we've received a comma, examine the buffer... // switch(cc) { case 0: // waiting for a $GPRMC sentence // if (Buf6[0] != '$') break; if (Buf6[1] != 'G') break; if (Buf6[2] != 'P') break; if (Buf6[3] != 'R') break; if (Buf6[4] != 'M') break; if (Buf6[5] != 'C') break; cc++; // U1TXREG = 13; // CR, DEBUG break; case 1: // grab the time // if (TCheck(Buf6) == 1) { tBuf[0] = Buf6[0]; // hh tBuf[1] = Buf6[1]; tBuf[2] = Buf6[2]; // mm tBuf[3] = Buf6[3]; tBuf[4] = Buf6[4]; // ss tBuf[5] = Buf6[5]; cc++; } else { cc = 0; } break; case 9: // grab the date straight to MainBuf // if (DCheck(Buf6) == 1) { MainBuf[0] = Buf6[4] & 15; // yr MainBuf[1] = Buf6[5] & 15; MainBuf[2] = Buf6[2] & 15; // month MainBuf[3] = Buf6[3] & 15; MainBuf[4] = Buf6[0] & 15; // day MainBuf[5] = Buf6[1] & 15; // // Add day of week // MainBuf[6] = dayofwk(Buf6); // DOW // // Copy time to MainBuf // MainBuf[7] = tBuf[0] & 15; // hour MainBuf[8] = tBuf[1] & 15; MainBuf[9] = tBuf[2] & 15; // min MainBuf[10] = tBuf[3] & 15; MainBuf[11] = tBuf[4] & 15; // sec MainBuf[12] = tBuf[5] & 15; tBuf[0] = 0; r = 1; cc++; } else { cc = 0; } break; case 10: // stay in state 10, consuming chars, until sentence ends // break; default: // cc++; // move on to next state } Buf6[5] = 0; } return r; } void DSet(unsigned char d) { if (d == 0) { LATBbits.LATB9 = 1; // deactivate /SET LATBbits.LATB7 = 0; // activate /RES LATBbits.LATB8 = 0; // set D low LATBbits.LATB7 = 1; // release /RES } else { LATBbits.LATB7 = 1; // deactivate /RES LATBbits.LATB9 = 0; // activate /SET LATBbits.LATB8 = 1; // set D high LATBbits.LATB9 = 1; // relaese /SET } } // Even Parity calculator // unsigned int ePty(unsigned char in) { unsigned char p; // // We ignore the top 4 bits of in // return a bit to make the overall parity (incl the returned bit) odd // p = 0; p += in & 1; p += (in >> 1) & 1; p += (in >> 2) & 1; p += (in >> 3) & 1; p &= 1; return p; } // Odd Parity calculator // unsigned int oPty(unsigned char in) { unsigned char p; // // We ignore the top 4 bits of in // return a bit to make the overall parity (incl the returned bit) odd // p = 1; p += in & 1; p += (in >> 1) & 1; p += (in >> 2) & 1; p += (in >> 3) & 1; p &= 1; return p; } // DCF numbers the days of the week MON=1...SUN=7 // This code (and MSF) use SUN=0...SAT=6 // Here we return the DCFized version of the DOW // unsigned char DCFDOW(char tDOW) { unsigned char i; i = tDOW; if (i == 0) i = 7; return i; } // MSF pulse stream engine... // unsigned char MSF(char * tDate) { unsigned int r; unsigned char sec; // // Must return a result (tCode) which represents the binary sequence for the next 500ms // for the current second (sec) using the hour, minute and date held in tDate // The lsb of the returned data is the second marker, and is always 0 in MSF, // bit 1 is the primary data, bit 2 = secondary data bit // bits 3 & 4 are set low only to form the minute marker // r = 0b11111110; sec = (10 * tDate[11]) + tDate[12]; switch (sec) { case 0: r = 0b11100000; break; // minute marker // // cases 1..16 are the DUT1 bits, unfortunately we don't have that information // case 17: if (tDate[ 0] & 8) r -= 2; break; // Year 80 case 18: if (tDate[ 0] & 4) r -= 2; break; // Year 40 case 19: if (tDate[ 0] & 2) r -= 2; break; // Year 20 case 20: if (tDate[ 0] & 1) r -= 2; break; // Year 10 case 21: if (tDate[ 1] & 8) r -= 2; break; // Year 8 case 22: if (tDate[ 1] & 4) r -= 2; break; // Year 4 case 23: if (tDate[ 1] & 2) r -= 2; break; // Year 2 case 24: if (tDate[ 1] & 1) r -= 2; break; // Year 1 // case 25: if (tDate[ 2] & 1) r -= 2; break; // Month 10 case 26: if (tDate[ 3] & 8) r -= 2; break; // Month 8 case 27: if (tDate[ 3] & 4) r -= 2; break; // Month 4 case 28: if (tDate[ 3] & 2) r -= 2; break; // Month 2 case 29: if (tDate[ 3] & 1) r -= 2; break; // Month 1 // case 30: if (tDate[ 4] & 2) r -= 2; break; // Day 20 case 31: if (tDate[ 4] & 1) r -= 2; break; // Day 10 case 32: if (tDate[ 5] & 8) r -= 2; break; // Day 8 case 33: if (tDate[ 5] & 4) r -= 2; break; // Day 4 case 34: if (tDate[ 5] & 2) r -= 2; break; // Day 2 case 35: if (tDate[ 5] & 1) r -= 2; break; // Day 1 // case 36: if (tDate[ 6] & 4) r -= 2; break; // DOW 4 case 37: if (tDate[ 6] & 2) r -= 2; break; // DOW 2 case 38: if (tDate[ 6] & 1) r -= 2; break; // DOW 1 // case 39: if (tDate[ 7] & 2) r -= 2; break; // Hour 20 case 40: if (tDate[ 7] & 1) r -= 2; break; // Hour 10 case 41: if (tDate[ 8] & 8) r -= 2; break; // Hour 8 case 42: if (tDate[ 8] & 4) r -= 2; break; // Hour 4 case 43: if (tDate[ 8] & 2) r -= 2; break; // Hour 2 case 44: if (tDate[ 8] & 1) r -= 2; break; // Hour 1 // case 45: if (tDate[ 9] & 4) r -= 2; break; // Min 40 case 46: if (tDate[ 9] & 2) r -= 2; break; // Min 20 case 47: if (tDate[ 9] & 1) r -= 2; break; // Min 10 case 48: if (tDate[10] & 8) r -= 2; break; // Min 8 case 49: if (tDate[10] & 4) r -= 2; break; // Min 4 case 50: if (tDate[10] & 2) r -= 2; break; // Min 2 case 51: if (tDate[10] & 1) r -= 2; break; // Min 1 // //case 52: // Framing code 0: start // case 53: // Framing code 1 + BST change warning (61 warnings) r = 0b11111100; if (DSTWARN > 0) r -= 4; // BST warn flag to secondary bit break; // case 54: if (oPty(tDate[0] ^ tDate[1]) ) r -= 6; else r -= 2; break; // Framing code 1 + Pty(Yr) case 55: if (oPty(tDate[2] ^ tDate[3] ^ tDate[4] ^ tDate[5]) ) r -= 6; else r -= 2; break; // Framing code 1 + Pyt(Day+Month) case 56: if (oPty(tDate[6]) ) r -= 6; else r -= 2; break; // Framing code 1 + Pty(DOW) case 57: if (oPty(tDate[7] ^ tDate[8] ^ tDate[9] ^ tDate[10])) r -= 6; else r -= 2; break; // Framing code 1 + Pty(Hr+Min) break; // case 58: // Framing code 1 + BST bit r = 0b11111100; if (DST > 0) r -= 4; // BST flag to secondary bit break; // //case 59: // Framing code 0: end // } return r; } // DCF pulse stream engine... // unsigned char DCF(char * tDate) { unsigned char sec; unsigned int r; // // Must return a result (tCode) which represents the binary sequence for the next 500ms // for the current second (sec) using the hour, minute and date held in tDate // The lsb of the returned data is the second marker, and is normally 0 in DCF, // but is supressed for sec 59 by way of a minute marker. // bit 1 is the data bit, DCF does not have secondary data bits // sec = (10 * tDate[11]) + tDate[12]; r = 0b11111110; switch (sec) { case 59: r = 0b11111111; break; // sec 59 marker suppressed case 58: if (ePty(tDate[0] ^ tDate[1] ^ tDate[2] ^ tDate[3] ^ tDate[4] ^ tDate[5] ^ DCFDOW(tDate[6]))) r -=2; break; // P3 - date pty // case 57: if (tDate[ 0] & 8) r -= 2; break; // Year 80 case 56: if (tDate[ 0] & 4) r -= 2; break; // Year 40 case 55: if (tDate[ 0] & 2) r -= 2; break; // Year 20 case 54: if (tDate[ 0] & 1) r -= 2; break; // Year 10 case 53: if (tDate[ 1] & 8) r -= 2; break; // Year 8 case 52: if (tDate[ 1] & 4) r -= 2; break; // Year 4 case 51: if (tDate[ 1] & 2) r -= 2; break; // Year 2 case 50: if (tDate[ 1] & 1) r -= 2; break; // Year 1 // case 49: if (tDate[ 2] & 1) r -= 2; break; // Month 10 case 48: if (tDate[ 3] & 8) r -= 2; break; // Month 8 case 47: if (tDate[ 3] & 4) r -= 2; break; // Month 4 case 46: if (tDate[ 3] & 2) r -= 2; break; // Month 2 case 45: if (tDate[ 3] & 1) r -= 2; break; // Month 1 // case 44: if (DCFDOW(tDate[6]) & 4) r -= 2; break; // DOW 4 case 43: if (DCFDOW(tDate[6]) & 2) r -= 2; break; // DOW 2 case 42: if (DCFDOW(tDate[6]) & 1) r -= 2; break; // DOW 1 // case 41: if (tDate[ 4] & 2) r -= 2; break; // Day 20 case 40: if (tDate[ 4] & 1) r -= 2; break; // Day 10 case 39: if (tDate[ 5] & 8) r -= 2; break; // Day 8 case 38: if (tDate[ 5] & 4) r -= 2; break; // Day 4 case 37: if (tDate[ 5] & 2) r -= 2; break; // Day 2 case 36: if (tDate[ 5] & 1) r -= 2; break; // Day 1 // case 35: if (ePty(tDate[7] ^ tDate[8])) r -=2; break; // P2 - hour pty // case 34: if (tDate[ 7] & 2) r -= 2; break; // Hour 20 case 33: if (tDate[ 7] & 1) r -= 2; break; // Hour 10 case 32: if (tDate[ 8] & 8) r -= 2; break; // Hour 8 case 31: if (tDate[ 8] & 4) r -= 2; break; // Hour 4 case 30: if (tDate[ 8] & 2) r -= 2; break; // Hour 2 case 29: if (tDate[ 8] & 1) r -= 2; break; // Hour 1 // case 28: if (ePty(tDate[9] ^ tDate[10])) r -=2; break; // P1 - minute pty // case 27: if (tDate[ 9] & 4) r -= 2; break; // Min 40 case 26: if (tDate[ 9] & 2) r -= 2; break; // Min 20 case 25: if (tDate[ 9] & 1) r -= 2; break; // Min 10 case 24: if (tDate[10] & 8) r -= 2; break; // Min 8 case 23: if (tDate[10] & 4) r -= 2; break; // Min 4 case 22: if (tDate[10] & 2) r -= 2; break; // Min 2 case 21: if (tDate[10] & 1) r -= 2; break; // Min 1 // case 20: r -= 2; break; // start bit, always 1 // // case 19: // A2 leap second alert not implemetable case 18: if (DST == 0) r -= 2; break; // Z2 inactive during DST case 17: if (DST > 0) r -= 2; break; // Z1 active during DST case 16: if (DSTWARN > 0) r -= 2; break; // A1 DST change alert // case 15: // R call bit not implementable // cases 1..14 MeteoTime data not implementable // case 0: minute marker // } return r; } // Determine if we are within DST period // Return 1 if so, 0 if not // Requires UTC time in tDate buffer (not local time) // European DST runs from 01:00 UTC on last SUN in March to 01:00 UTC on last SUN in Oct // // If (Month = Apr..Sep) return 1 // If (Month < Mar) return 0 // If (Month > Oct) return 0 // If (Month == Mar): // If (date >= 25) // If (DOW > 0) // If ((date - DOW) >= 25) return 1 // after the last SUN // else // If (hr >= 1) return 1 // return 0 // If (Month == Oct): // If (date >= 25) // If (DOW > 0) // If ((date - DOW) >= 25) return 0 // after the last SUN // else // If (hr >= 1) return 0 // return 1 // // unsigned char DSTime(char * tDate) { unsigned char h,d,m, DOW; // h = (10 * tDate[7]) + tDate[8]; d = (10 * tDate[4]) + tDate[5]; m = (10 * tDate[2]) + tDate[3]; DOW = tDate[6]; // if ((m > 3) && (m < 10)) return 1; if ((m < 3) || (m > 10)) return 0; if (m == 3) { if (d >= 25) { if (DOW > 0) { if ((d - DOW) >= 25) return 1; } else if (h >= 1) return 1; } return 0; } if (m == 10) { if (d >= 25) { if (DOW > 0) { if ((d - DOW) >= 25) return 0; } else if (h >= 1) return 0; } return 1; } return 0; } // Determine if we are within the DST-warn period // ie the first hour of the last SUN in Mar & Oct // Requires UTC time in tDate buffer (not local time) // Return 1 if so, 0 if not // unsigned char DSTWarnTime(char * tDate) { unsigned char h,d,m, DOW; // m = (10 * tDate[2]) + tDate[3]; DOW = tDate[6]; // if ((DOW == 0) && ((m == 3) || (m == 10))) { h = (10 * tDate[7]) + tDate[8]; d = (10 * tDate[4]) + tDate[5]; if ((d >= 25) && (h == 0)) return 1; } return 0; } // Adds {ss} seconds, {ms} minutes & {hs} hours to the time/date in {tDate} // Returns seconds in binary 0..59 // Note that during a leap-second-extended minute (ie the first minute in // July or Jan) we'll see two second #1s rather than the more correct // extra second #60 in the last minute of Jun30 or Dec31, this is due to // the way the Garmin receiver works. Also, as we have no forewarning of // a leap second we cannot support DCF's leap second alert mechanism. // Oh well. // unsigned char IncTime(char * tDate, unsigned char hs, unsigned char ms, unsigned char ss) { unsigned char s, mi, h, DOW, d, mo, y; // // increment secs & mins // s = (10 * tDate[11]) + tDate[12] + ss; mi = (10 * tDate[9]) + tDate[10] + ms; if (s > 59) mi++; s %= 60; tDate[11] = s / 10; tDate[12] = s % 10; if (mi > 59) hs++; mi %= 60; tDate[9] = mi / 10; tDate[10] = mi % 10; if (hs == 0) return s; // // increment hours by hs // h = (10 * tDate[7]) + tDate[8] + hs; if (h > 23) hs = 1; else hs = 0; h %= 24; tDate[7] = h / 10; tDate[8] = h % 10; if (hs == 0) return s; // // Need to increment the DOW & date... // DOW = tDate[6]; tDate[6] = (DOW + 1) % 7; d = (10 * tDate[4]) + tDate[5] + 1; if (d > 28) { // // May need to inrcrement months... // mo = (10 * tDate[2]) + tDate[3]; y = (10 * tDate[0]) + tDate[1]; if (d > DaysInMonth(mo, y)) { d = 1; mo++; if (mo > 12) { mo = 1; y = (y + 1) % 100; tDate[0] = y / 10; tDate[1] = y % 10; } tDate[2] = mo / 10; tDate[3] = mo % 10; } } tDate[4] = d / 10; tDate[5] = d % 10; return s; } unsigned char Calc_tCode(char * UTCTimeBuf, char * TimeBuf) { unsigned char tCode, uch, i; // // Calulate new TimeBuf, DST, DSTWARN and tCode from current UTC time accumulator) // DSTWARN = DSTWarnTime(UTCTimeBuf); // set global DST-Warning flag DST = uch = DSTime(UTCTimeBuf); // set global DST flag, uch = 1 if we're in DST if (PORTBbits.RB0 == 0) uch++; // we're in DCF mode, add an(other) hour // // now calculate local pre-emptive time -> TimeBuf // for (i = 0; i < 13; i++) TimeBuf[i] = UTCTimeBuf[i]; IncTime(TimeBuf, uch, 0, 0); // if (PORTBbits.RB0 == 0) tCode = DCF(TimeBuf); // get this second's DCF tCode else tCode = MSF(TimeBuf); // get this second's MSF tCode // return tCode; } int main() { char GPSTimeBuf[13] = {127,0,0,0,0,0,0,0,0,0,0,0,0}; // {YYMMDDdHHMMSS} char UTCTimeBuf[13] = {127,0,0,0,0,0,0,0,0,0,0,0,0}; // {YYMMDDdHHMMSS} char TimeBuf[13] = {127,0,0,0,0,0,0,0,0,0,0,0,0}; // {YYMMDDdHHMMSS} unsigned int ui; unsigned char dSecs; // deci-second counter unsigned int PR2nom = T2RATE; // nominal T2 counts per 100ms second int i, t0, newPh; // Timer/counter fine tuning mechanism char nextSec; // predicive second number unsigned char tCode; // binary sequence for a single MSF/DCF o/p pulse char GPSData; // >0 when GPSTimeBuf becomes ready char EFlag; // >0 if pre-emptive(GPS) != local copy of UTC // Configure the PLL to operate the device at Fcy = 14.74Mhz from the nom 7.37MHz FRC // Fcy = Fin * M / (N1 * N2 * 2) // Fcy = 7.37 * PLL_M / (2 * 2 * 2) // // Configure PLL etc // CLKDIVbits.FRCDIV = 0; // FRC/1 to PLL PLLFBD = PLL_M - 2; // PLL Feedback Factor = M - 2 CLKDIVbits.PLLPOST = 0; // N1 = 2 (min) CLKDIVbits.PLLPRE = 0; // N2 = 2 (min) // while(OSCCONbits.LOCK != 1); // wait for PLL to lock // Set up UART // U1BRG = UART_BRG; // Setup baud rate // 0bFEDCBA9876543210 U1MODE = 0b1000100000000000; // Enabled, non-stop, no IRDA, no RTS, TX+RX only, BRGH=0, 8N1 U1STA = 0b0000010000000000; // Tx en + default settings // Setup timer 2 for generation of nom 10Hz from a 14740000 / 64 = 230312 KHz input, ie count to 23031 // Counter will go from 0 to 23030 // // 0bFEDCBA9876543210 T2CON = 0b1000000000100000; // ON, no gate, 1:64pre, 16b, int clk TMR2 = 0; PR2 = PR2nom; // reload reg = rqd cycle count - 1 // Setup input Capture ch 1 to capture timer2 on PPS rising edge // // 0bFEDCBA9876543210 IC1CON = 0b0000000010000011; // tmr2, int every... rising edge // Turn off all analog pins, enable digital IO function // AD1PCFGL = 0xFFFF; // Re-map special purpose pins... // // We must first unlock the IO remapper by clearing the IOLOCK bit in OSCCON.6 // ui = OSCCON & 255; __builtin_write_OSCCONL(ui & 0b10111111); // unlock the port re-mapper... // // re-map RP15 (RB15 pin16) as output for U1TX // RPOR7bits.RP15R = 0b00011; // RP15 -OUT-> U1TX // // re-map RP4 (RB4 pin8) as input for U1RX // RPINR18bits.U1RXR = 4; // U1RXR <-IN- RP4 // // re-map RP14 (RB14 pin15) as input for IC1 // RPINR7bits.IC1R = 14; // IC1 <-IN- RP15 // // re-lock the remapper // __builtin_write_OSCCONL(ui | 0b01000000); // lock it back up // Set initial state for outputs // LATAbits.LATA0 = 0; // set lo LATAbits.LATA1 = 0; // set lo LATAbits.LATA2 = 0; // set lo LATAbits.LATA3 = 0; // set lo LATAbits.LATA4 = 1; // start 'LED+' hi for red LATBbits.LATB7 = 1; // start 'RES' hi LATBbits.LATB8 = 1; // start 'D' hi LATBbits.LATB9 = 0; // pulse 'SET' lo // enable digital outputs // TRISAbits.TRISA0 = 0; // Set RA0 o/p n/u TRISAbits.TRISA1 = 0; // Set RA1 o/p n/u TRISAbits.TRISA2 = 0; // Set RA2 o/p n/u TRISAbits.TRISA3 = 0; // Set RA3 o/p n/u TRISAbits.TRISA4 = 0; // Set RA4 o/p n/u TRISBbits.TRISB7 = 0; // Set RB7 o/p = RES TRISBbits.TRISB8 = 0; // Set RB8 o/p = D TRISBbits.TRISB9 = 0; // Set RB9 o/p = SET // RB15 o/p re-mapped as U1TX // digital inputs // TRISBbits.TRISB0 = 1; // RB0 input MSF/DCF [ISP_DAT] TRISBbits.TRISB1 = 1; // RB1 input -n/u- [ISP_CK] TRISBbits.TRISB4 = 1; // RB4 input RXD -REMAPPED-> U1RX TRISBbits.TRISB14 = 1; // RB14 input PPS -REMAPPED-> IC1 // Set pull-ups on all inputs // CNPU1bits.CN4PUE = 1; // RB0, MSF/DCF CNPU1bits.CN5PUE = 1; // RB1 CNPU1bits.CN1PUE = 1; // RB4, RXD CNPU1bits.CN12PUE = 1; // RB14, PPS // -------------------------------- main loop ------------------------------- // while(1) { // initial PPS sync-up // LATAbits.LATA4 = 1; // LED off PR2nom = T2RATE; // start with nominal timings PR2 = PR2nom; DSet(1); // hold output high while (IC1CONbits.ICBNE != 0) t0 = IC1BUF; // flush all data from IC1 buffer while (IC1CONbits.ICBNE == 0) TMR2 = 0; // hold timer 2 until next PPS IFS0bits.T2IF = 0; // clear any pending timer 2 intr flag t0 = IC1BUF; // flush IC1's buffers again dSecs = 0; // we're sync'd to dSec=0 nextSec = -1; // don't yet know which second we're on tCode = 0b11111111; // keep o/p high in the short term LATAbits.LATA4 = 0; // LED enabled UARTRx(0); // initialise serial rx state U1TXREG = '*'; // debug message * = "restart" GPSData = -1; // flush GPS serial data // --- Sync'd-up loop --- // while(dSecs < 10) // loop exits to re-sync if dSecs ever > 9 { // Service GPS serial rx task // if (GPSData == 0) GPSData = UARTRx(GPSTimeBuf); // UARTRx() returns 0 if no data is ready yet else UARTRx(0); // PPS rising edges trigger IC1 to capture the current timer2 count // we use this information to adust TMR2 to run as close as possible to 10Hz // (TMR2 is used to control MSF/DCF pulse width timing) // if (IC1CONbits.ICBNE != 0) { t0 = IC1BUF; // read IC1's result // // But ignore the pulse if dSecs != 9, assume it's a glitch // if (dSecs == 9) { TMR2 = 0; // reset timer 2... dSecs = 0; // ...and dSecs U1TXREG = '0' + (~(tCode >> 1) & 7); // tx second marker waveform for debug GPSData = 0; // // Fine tune timer2 reloads to keep them aligned with the PPS // newPh = t0 - PR2nom; // // newPh is the lateness of our timer wrt the PPS pulse // use it to make a proportionate adjustment to the PR2 nominal period // to compensate for any drift or static error in the PIC's FRC oscillator. // We reduce the gain of this loop to slug its response time a bit. // PR2nom += newPh >> 5; // += newPh / 10 for fastest response time, /32 for more robustness? PR2 = PR2nom; } } // Every (well 9 out of 10) 100ms we get a timer2 interrupt flag // if ((IFS0bits.T2IF == 1) && (dSecs < 10)) // poll for timer 2 interrupts { IFS0bits.T2IF = 0; // clear the interrupt flag dSecs++; // increment deci-seconds counter // // Time for a change to the o/p bit stream? // switch(dSecs) { case 1: // o/p DCF's data bit / MSF's primary data bit DSet((tCode >> 1) & 1); break; case 2: // o/p MSF's secondary data bit DSet((tCode >> 2) & 1); break; case 3: // o/p MSF's minute marker DSet((tCode >> 3) & 1); break; case 4: // o/p MSF's minute marker DSet((tCode >> 4) & 1); break; case 9: // // Time to check for GPS serial data and compare it with the local UTC time accumulator, // generate the tCode for the second marker that's currently running and // check if we need DST time, DST/WARN flags etc // if (GPSData > 0) { // GPSTimeBuf has new GPS time & date - but it's retrospective UTC // We need therefore to add 1 minute and 1 second because MSF/DCF data is pre-emptive // at minute resolution and the GPS serial data is retrospective at second resolution. // IncTime(GPSTimeBuf, 0, 1, 1); // calculate the pre-emptive UTC time // // Copy to inc'd UTCTimeBuf with compare - report error if mis-matched // This is only to help with long-term debugging of IncTime() // EFlag = 0; if (UTCTimeBuf[0] != 127) { IncTime(UTCTimeBuf, 0, 0, 1); for (i = 0; i < 13; i++) { if (UTCTimeBuf[i] != GPSTimeBuf[i]) { EFlag++; UTCTimeBuf[i] = GPSTimeBuf[i]; } } if (EFlag > 0) U1TXREG = '%'; } else for (i = 0; i < 13; i++) UTCTimeBuf[i] = GPSTimeBuf[i]; } else { // we didn't get GPS data in time, supress the output pulse // UTCTimeBuf[0] = 127; U1TXREG = '!'; } // // Now we may need to add an hour for DCF's German time-zone // and possibly another hour for BST/EST during the summer months // it's all handled by Calc_tCode() // But do nothing if we don't yet have a fix // tCode = 0b11111111; if (UTCTimeBuf[0] != 127) tCode = Calc_tCode(UTCTimeBuf, TimeBuf); // // Setup for PPS hardware clock edge to DLatch // LATBbits.LATB9 = 1; // deactivate /SET LATBbits.LATB7 = 1; // and /RES LATBbits.LATB8 = tCode & 1; // setup D // PR2 = 0xFFFF; // We let timer2 run on at phase 9 so that PPS-triggered capture can work break; default: DSet(1); } } } } }