// // wwvb3.c - Calculate, display, and generate WWVB subcode // // - This version takes command line date/time parameters. // - And generates time code pulses using the serial port. // // 06-Feb-2005 /tvb // #include #include #include #include // Define basic types typedef unsigned char byte; typedef unsigned int uint; typedef unsigned long ulong; // Define time code elements struct timecode { uint min; uint hour; uint days; uint year; uint leapyear; uint dst; int ut1; uint leapsec; } tco; // Define forward referenced function prototypes void tick_set (time_t t, struct timecode *tc); void tick_show (struct timecode *tc); void tick_tock (struct timecode *tc); int wwvb_telegram (struct timecode *tc, byte code[]); int dcf77_telegram (struct timecode *tc, byte code[]); int GetOptions (int argc, char *argv[]); void transmit_init (void); void transmit_bit (int bit); void calc_rate (void); void final_rate (void); double elapsed_seconds (); // Define comm function prototypes void CommOpen (int Port, int Baud, int Parity, int Flow); ulong CommRead (char *Buffer, ulong Size); void CommTimeout (ulong Interval, ulong Multiplier, ulong Constant); void CommWrite (char *Buffer, ulong Size); void CommClose (void); // Define global data #define minimum_baud_rate 1200 #define maximum_baud_rate 19200 uint nominal_baud = 9600; double precise_baud_rate; #define pad_limit 175 int pad_fixed; int pad_rand; double total_bytes; double total_codes; int Debug = 0; int mode_wwvb = 1; int mode_dcf77 = 0; // Define usage message char Usage[] = "Usage: wwvb3 [options] [port] [precise baud] [max minutes]\n" "\n" "Options:\n" " /year:# - set year (0 to 99)\n" " /days:# - set day of year (1 to 365/366)\n" " /hour:# - set hour (0 to 23)\n" " /min:# - set minute (0 to 59)\n" " /ut1:# - set ut1 offset (-900 to +900)\n" " /dst:# - set both dst bits (0 to 3)\n" " /ly:# - set leap year bit (0/1)\n" " /ls:# - set leap second warning bit (0/1)\n" "\n" " /pad:# - add/sub milliseconds to pulse widths\n" " /rand:# - random pad by +/- milliseconds\n" "\n" "Examples:\n" " - generate 10 minutes of WWVB subcode on COM1 to check rate:\n" " wwvb3 1\n" "\n" " - generate 30 minutes of precise WWVB subcode on COM1:\n" " wwvb3 1 9616.5 30\n" "\n" "Questions or comments:\n" " See www.LeapSecond.com for contact info\n" ; int main (int argc, char *argv[]) { int i, n; long minutes; time_t now, later; int port, start; if (argc == 1) { fputs(Usage, stderr); exit(0); } // Set default WWVB time from PC time time(&now); tick_set(now, &tco); // Then allow command line options to override the time fields n = GetOptions(argc, argv); if (n == 0) { fprintf(stderr, "** No timecode options specified (defaulting to PC time)\n"); } argc -= n; argv += n; // Get serial port number if (argc > 1) { port = atoi(argv[1]); } else { fprintf(stderr, "** Serial port not specified (defaulting to 1)\n"); port = 1; } // Get precise baud rate if (argc > 2) { precise_baud_rate = atof(argv[2]); } else { fprintf(stderr, "** Precise baud rate not specified (defaulting to %.1lf)\n", nominal_baud); precise_baud_rate = nominal_baud; } // Get number of minutes if (argc > 3) { minutes = atol(argv[3]); if (minutes <= 0) { fprintf(stderr, "Invalid number of minutes: %s\n", argv[3]); exit(1); } } else { fprintf(stderr, "** Number of minutes not specified (defaulting to 10)\n"); minutes = 10; } // No more arguments. if (argc > 4) { fprintf(stderr, "Unexpected argument: %s\n", argv[4]); exit(1); } // Open COM port CommOpen(port, nominal_baud, 0, 0); transmit_init(); fprintf(stderr, "\nNIST WWVB subcode test generator\n\n"); fprintf(stderr, "PC local time: %s", ctime(&now)); // Synchronize program to the second against PC clock do { time(&later); } while (later == now); // Check for minute rollover during PC clock sync if ((later % 60) < (now % 60)) { tick_tock(&tco); } printf("WWVB timecode: "); tick_show(&tco); start = later % 60; // Now begin writing a continuous stream of high voltage and low // voltage bytes which when low pass filtered will generate the // time code envelope. do { byte bits[61]; if (Debug) { printf("'%.2d+%.3d %.2d:%.2d ", tco.year, tco.days, tco.hour, tco.min); } if (mode_wwvb) { n = wwvb_telegram(&tco, bits); } if (mode_dcf77) { n = dcf77_telegram(&tco, bits); } for (i = 0; i < start; i += 1) { printf(" "); } for (i = start; i < n; i += 1) { printf("%d", bits[i]); transmit_bit(bits[i]); } calc_rate(); printf("\n"); tick_tock(&tco); start = 0; } while (--minutes > 0) ; final_rate(); CommClose(); return 0; } // Extract one binary coded decimal bit int BCD (int n, int d) { if (n < 0) { n = -n; } while (d >= 10) { n /= 10; d /= 10; } return ((n % 10) & d) ? 1 : 0; } // Convert date and time to 60 bit WWVB time code // http://www.boulder.nist.gov/timefreq/stations/wwvbtimecode.htm int wwvb_telegram (struct timecode *tc, byte code[]) { int n; code[0] = 2; code[1] = BCD(tc->min, 40); code[2] = BCD(tc->min, 20); code[3] = BCD(tc->min, 10); code[4] = 0; code[5] = BCD(tc->min, 8); code[6] = BCD(tc->min, 4); code[7] = BCD(tc->min, 2); code[8] = BCD(tc->min, 1); code[9] = 2; code[10] = 0; code[11] = 0; code[12] = BCD(tc->hour, 20); code[13] = BCD(tc->hour, 10); code[14] = 0; code[15] = BCD(tc->hour, 8); code[16] = BCD(tc->hour, 4); code[17] = BCD(tc->hour, 2); code[18] = BCD(tc->hour, 1); code[19] = 2; code[20] = 0; code[21] = 0; code[22] = BCD(tc->days, 200); code[23] = BCD(tc->days, 100); code[24] = 0; code[25] = BCD(tc->days, 80); code[26] = BCD(tc->days, 40); code[27] = BCD(tc->days, 20); code[28] = BCD(tc->days, 10); code[29] = 2; code[30] = BCD(tc->days, 8); code[31] = BCD(tc->days, 4); code[32] = BCD(tc->days, 2); code[33] = BCD(tc->days, 1); code[34] = 0; code[35] = 0; code[36] = tc->ut1 >= 0; code[37] = !code[36]; code[38] = code[36]; code[39] = 2; code[40] = BCD(tc->ut1, 800); code[41] = BCD(tc->ut1, 400); code[42] = BCD(tc->ut1, 200); code[43] = BCD(tc->ut1, 100); code[44] = 0; code[45] = BCD(tc->year, 80); code[46] = BCD(tc->year, 40); code[47] = BCD(tc->year, 20); code[48] = BCD(tc->year, 10); code[49] = 2; code[50] = BCD(tc->year, 8); code[51] = BCD(tc->year, 4); code[52] = BCD(tc->year, 2); code[53] = BCD(tc->year, 1); code[54] = 0; code[55] = tc->leapyear; code[56] = tc->leapsec; code[57] = (tc->dst & 2) == 2; code[58] = (tc->dst & 1) == 1; code[59] = 2; // Handle positive or negative leap second exception n = 60; if (tc->leapsec != 0 && tc->hour == 23 && tc->min == 59) { if (tc->ut1 > 0) { tc->ut1 -= 1000; n = 59; } else if (tc->ut1 < 0) { tc->ut1 += 1000; n = 61; code[60] = 2; } tc->leapsec = 0; } return n; } // Sum parity to make even parity int parity (char *p, int from, int to) { int i; p[to] = 0; for (i = from; i < to; i += 1) { p[to] += p[i]; } return p[to] & 1; } // Convert date and time to 60 bit DCF77 time code int dcf77_telegram (struct timecode *tc, byte code[]) { code[0] = 0; code[1] = 0; code[2] = 0; code[3] = 0; code[4] = 0; code[5] = 0; code[6] = 0; code[7] = 0; code[8] = 0; code[9] = 0; code[10] = 0; code[11] = 0; code[12] = 0; code[13] = 0; code[14] = 0; code[15] = 0; // normal antenna // code[16] = tc->DCF.Dst; // code[17] = BCD(tc->TimeZone, 1); // code[18] = BCD(tc->TimeZone, 2); code[19] = tc->leapsec; code[20] = 1; // start bit code[21] = BCD(tc->min, 1); code[22] = BCD(tc->min, 2); code[23] = BCD(tc->min, 4); code[24] = BCD(tc->min, 8); code[25] = BCD(tc->min, 10); code[26] = BCD(tc->min, 20); code[27] = BCD(tc->min, 40); code[28] = parity(code, 21, 28); code[29] = BCD(tc->hour, 1); code[30] = BCD(tc->hour, 2); code[31] = BCD(tc->hour, 4); code[32] = BCD(tc->hour, 8); code[33] = BCD(tc->hour, 10); code[34] = BCD(tc->hour, 20); code[35] = parity(code, 29, 35); code[36] = BCD(tc->days, 1); code[37] = BCD(tc->days, 2); code[38] = BCD(tc->days, 4); code[39] = BCD(tc->days, 8); code[40] = BCD(tc->days, 10); code[41] = BCD(tc->days, 20); // code[42] = BCD(tc->week, 1); // code[43] = BCD(tc->week, 2); // code[44] = BCD(tc->week, 4); // code[45] = BCD(tc->month, 1); // code[46] = BCD(tc->month, 2); // code[47] = BCD(tc->month, 4); // code[48] = BCD(tc->month, 8); // code[49] = BCD(tc->month, 10); code[50] = BCD(tc->year, 1); code[51] = BCD(tc->year, 2); code[52] = BCD(tc->year, 4); code[53] = BCD(tc->year, 8); code[54] = BCD(tc->year, 10); code[55] = BCD(tc->year, 20); code[56] = BCD(tc->year, 40); code[57] = BCD(tc->year, 80); code[58] = parity(code, 36, 58); code[59] = 2; #if 0 for (i = tc->Second; i < 60; i += 1) { Wait1PPS(); switch (Code[i]) { case 0 : PowerLow(100); break; case 1 : PowerLow(200); break; case 2 : PowerLow(980); break; } PowerHigh(0); } if (AddLeapSecond(tc)) { PowerLow(100); PowerHigh(900); } #endif return 60; } // Set timecode from PC clock void tick_set (time_t t, struct timecode *tc) { struct tm *tm; tm = gmtime(&t); tc->min = tm->tm_min; tc->hour = tm->tm_hour; tc->days = 1 + tm->tm_yday; tc->year = tm->tm_year % 100; tc->leapyear = (tc->year % 4) == 0; tc->dst = 0; // normal tc->ut1 = 0; // place holder tc->leapsec = 0; // normal return; } // Display timecode values void tick_show (struct timecode *tc) { printf("year=%.2d days=%.3d hour=%.2d min=%.2d ", tc->year, tc->days, tc->hour, tc->min); printf("dst=%d ut1=%d ly=%d ls=%d\n", tc->dst, tc->ut1, tc->leapyear, tc->leapsec); return; } // Increment timecode by one minute void tick_tock (struct timecode *tc) { tc->min += 1; if (tc->min >= 60) { tc->min = 0; tc->hour += 1; if (tc->hour >= 24) { tc->hour = 0; tc->days += 1; if (tc->days >= 365 + tc->leapyear) { tc->days = 1; tc->year += 1; tc->year %= 100; tc->leapyear = (tc->year % 4) == 0; } } } return; } // Parse and validate options int GetOptions (int argc, char *argv[]) { int argn, i; argn = 0; while (argc > 1 && argv[1][0] == '/') { if (strcmp(argv[1], "/help") == 0 || argv[1][1] == '?') { fputs(Usage, stderr); exit(0); } else if (strncmp(argv[1], "/year:", i = 6) == 0) { tco.year = atoi(&argv[1][i]); if (tco.year > 1000) { fprintf(stderr, "** Truncating 4-digit year to 2-digits\n"); tco.year %= 100; } if (tco.year > 99) { fprintf(stderr, "Invalid year (0 to 99): %s\n", argv[1]); exit(1); } } else if (strncmp(argv[1], "/days:", i = 6) == 0) { tco.days = atoi(&argv[1][i]); if (tco.days < 1 || tco.days > 366) { fprintf(stderr, "Invalid days (1 to 366): %s\n", argv[1]); exit(1); } } else if (strncmp(argv[1], "/hour:", i = 6) == 0) { tco.hour = atoi(&argv[1][i]); if (tco.hour > 23) { fprintf(stderr, "Invalid hour (0 to 23): %s\n", argv[1]); exit(1); } } else if (strncmp(argv[1], "/min:", i = 5) == 0) { tco.min = atoi(&argv[1][i]); if (tco.min > 59) { fprintf(stderr, "Invalid minute (0 to 59): %s\n", argv[1]); exit(1); } } else if (strncmp(argv[1], "/ut1:", i = 5) == 0) { tco.ut1 = atoi(&argv[1][i]); if ((tco.ut1 % 100) != 0 || tco.ut1 < -900 || tco.ut1 > 900) { fprintf(stderr, "Invalid ut1 (-900 to +900): %s\n", argv[1]); exit(1); } } else if (strncmp(argv[1], "/dst:", i = 5) == 0) { tco.dst = atoi(&argv[1][i]); if (tco.dst > 3) { fprintf(stderr, "Invalid dst (0 to 3): %s\n", argv[1]); exit(1); } } else if (strncmp(argv[1], "/ly:", i = 4) == 0) { tco.leapyear = atoi(&argv[1][i]); if (tco.leapyear > 1) { fprintf(stderr, "Invalid leap year (0 or 1): %s\n", argv[1]); exit(1); } } else if (strncmp(argv[1], "/ls:", i = 4) == 0) { tco.leapsec = atoi(&argv[1][i]); if (tco.leapsec > 1) { fprintf(stderr, "Invalid leap second (0 or 1): %s\n", argv[1]); exit(1); } } else if (strncmp(argv[1], "/pad:", i = 5) == 0) { pad_fixed = atoi(&argv[1][i]); if (abs(pad_fixed) > pad_limit) { fprintf(stderr, "Invalid pad value: %s\n", argv[1]); exit(1); } } else if (strncmp(argv[1], "/rand:", i = 6) == 0) { pad_rand = atoi(&argv[1][i]); if (abs(pad_rand) > pad_limit) { fprintf(stderr, "Invalid rand value: %s\n", argv[1]); exit(1); } } else if (strncmp(argv[1], "/baud:", i = 6) == 0) { nominal_baud = atoi(&argv[1][i]); if (nominal_baud < 1200 || nominal_baud > 19200) { fprintf(stderr, "Invalid baud rate: %s\n", argv[1]); exit(1); } } else if (strcmp(argv[1], "/dcf77") == 0) { mode_wwvb = 0; mode_dcf77 = 1; } else { fprintf(stderr, "Unknown option: %s\n", argv[1]); exit(1); } argn += 1; argc -= 1; argv += 1; } if (tco.leapsec && (tco.ut1 == 0)) { fprintf(stderr, "Invalid leap second and ut1 combination\n"); exit(1); } if (tco.leapyear && (tco.year & 0x3)) { fprintf(stderr, "Invalid leap year and year combination\n"); exit(1); } if (abs(pad_fixed) + abs(pad_rand) > pad_limit) { fprintf(stderr, "Pad amounts too large (|%d| fixed + |%d| rand > %d limit)\n", pad_fixed, pad_rand, pad_limit); exit(1); } return argn; } // Fill byte arrays for high and low signal #define BAUD_RATE 9600 #define MAX_BAUD 10000 #define ARRAY_SIZE (MAX_BAUD / 10) byte high[ARRAY_SIZE]; byte low[ARRAY_SIZE]; // Allocate buffers void transmit_init (void) { int i; for (i = 0; i < ARRAY_SIZE; i += 1) { high[i] = 0x00; // makes Tx approx +10 Volts low[i] = 0xFF; // makes Tx approx -10 Volts } } // Convert milliseconds to a count of bytes at the given baud rate #define ms_to_bytes(ms,baud) ((long)(((baud) / 10.0) * ((ms) / 1000.0) + 0.5)) // Output a single subcode bit double resid; void transmit_bit (int bit) { long ms, pad, bytes; pad = 0; if (pad_fixed || pad_rand) { double f; // make f a random number from -1.0 to +1.0 f = 1.0 - 2.0 * (double)rand() / 32767.0; pad = pad_fixed + (int)(f * pad_rand); } ms = 200 + (bit * 300) + pad; printf(" ms=%d ", ms); bytes = ms_to_bytes(ms, precise_baud_rate); CommWrite(high, bytes); total_bytes += bytes; ms = 1000 - ms; bytes = ms_to_bytes(ms, precise_baud_rate); CommWrite(low, bytes); total_bytes += bytes; total_codes += 1; } // Display actual measured byte rate (UART clock vs. PC clock) double prev_bytes; double prev_time; double sample_count; double mean_baud; double time_error; double start_time_stamp; double start_byte_count; double current_time_stamp; double current_byte_count; void calc_rate (void) { double db, dt, baud; double time_stamp; static int once = 1; // Rates calculated starting with first complete minute time_stamp = elapsed_seconds(); if (prev_bytes == 0) { start_time_stamp = time_stamp; start_byte_count = total_bytes; } else { db = total_bytes - prev_bytes; dt = time_stamp - prev_time; time_error += dt - 60; baud = 10.0 * db / dt; if (sample_count < 20) { sample_count += 1; } mean_baud = (mean_baud * (sample_count - 1) + baud) / sample_count; printf(" %.2lf", dt); printf(" %.2lf", mean_baud); printf(" %.0lf", time_error * 1e3); if (once && time_error < -0.5) { printf("\n** More than half second behind now; baud rate seems too low"); once = 0; } if (once && time_error > 0.5) { printf("\n** More than half second ahead now; baud rate seems too high"); once = 0; } } prev_bytes = total_bytes; prev_time = time_stamp; } void final_rate (void) { double net_bytes; double net_seconds; double baud_rate; net_bytes = prev_bytes - start_byte_count; net_seconds = prev_time - start_time_stamp; printf("\nSummary:\n"); printf("%12.3lf seconds net time error (%s)\n", time_error, time_error < 0 ? "baud rate too low" : "baud rate too high"); printf("%12.0lf total subcode bits\n", total_codes); printf("%12.0lf total UART bytes\n", total_bytes); if (net_seconds != 0) { printf("%12.0lf full-minute bytes\n", net_bytes); printf("%12.3lf full-minute seconds elapsed\n", net_seconds); baud_rate = 10.0 * net_bytes / net_seconds; printf("%12.3lf baud (actual measured rate)\n", baud_rate); } } // Begin MS Windows specific code #define WIN32_LEAN_AND_MEAN #include // Return arbitrary elapsed seconds, 100 ns resolution double elapsed_seconds (void) { static int once = 1; static ulong base; if (once) { base = GetTickCount(); once = 0; } return (double)(GetTickCount() - base) * 1e-3; } double elapsed_seconds2 (void) { static DWORD HighDateTime = 0; FILETIME ft; __int64 t64; GetSystemTimeAsFileTime(&ft); if (HighDateTime == 0) { HighDateTime = ft.dwHighDateTime; } t64 = ft.dwHighDateTime - HighDateTime; t64 <<= 32; t64 |= ft.dwLowDateTime; return (double) t64 / 1e7; } // Windows serial port functions #define ASSERT_SUCCESS(Function) \ if (!Success) { \ fprintf(stderr, "File %s Line %d Function %s failed (%.8x)\n", \ __FILE__, __LINE__, #Function, GetLastError()); \ exit(1); \ } HANDLE hCom; void CommOpen (int Port, int Baud, int Parity, int Flow) { char CommPath[100]; DCB Dcb; BOOL Success; // Open COM port sprintf(CommPath, "\\\\.\\COM%d", Port); fprintf(stderr, "** Opening %s (%d baud)\n", CommPath, Baud); hCom = CreateFile(CommPath, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if (hCom == INVALID_HANDLE_VALUE) { fprintf(stderr, "CreateFile failed: %s (%.8x)\n", CommPath, GetLastError()); exit(1); } // Set comm parameters memset(&Dcb, 0x0, sizeof(DCB)); Dcb.DCBlength = sizeof (DCB); Success = GetCommState(hCom, &Dcb); ASSERT_SUCCESS(GetCommState); // Set parity Dcb.Parity = Parity; if (Parity == 0) { Dcb.Parity = NOPARITY; } else if (Parity & 1) { Dcb.Parity = ODDPARITY; } else { Dcb.Parity = EVENPARITY; } // Set frame size Dcb.BaudRate = Baud; Dcb.ByteSize = (Dcb.Parity == NOPARITY) ? 8 : 7; Dcb.StopBits = ONESTOPBIT; // Set raw mode Dcb.fDsrSensitivity = FALSE; Dcb.fOutxCtsFlow = FALSE; Dcb.fOutX = FALSE; Dcb.fInX = FALSE; Dcb.fNull = FALSE; Dcb.fBinary = TRUE; // PC write flow control Dcb.fOutxCtsFlow = FALSE; Dcb.fOutxDsrFlow = FALSE; // PC read flow control Dcb.fDtrControl = Flow ? DTR_CONTROL_HANDSHAKE : DTR_CONTROL_ENABLE; Dcb.fRtsControl = Flow ? RTS_CONTROL_HANDSHAKE : RTS_CONTROL_ENABLE; Success = SetCommState(hCom, &Dcb); ASSERT_SUCCESS(SetCommState); SetupComm(hCom, 4096, 4096); CommTimeout(2000, 0, 2000); return; } void CommTimeout (ulong Interval, ulong Multiplier, ulong Constant) { COMMTIMEOUTS Cto; BOOL Success; Success = GetCommTimeouts(hCom, &Cto); ASSERT_SUCCESS(GetCommTimeouts); Cto.ReadIntervalTimeout = Interval; Cto.ReadTotalTimeoutMultiplier = Multiplier; Cto.ReadTotalTimeoutConstant = Constant; Success = SetCommTimeouts(hCom, &Cto); ASSERT_SUCCESS(SetCommTimeouts); return; } void CommWrite (char *Buffer, ulong Size) { DWORD Count; BOOL Success; Success = WriteFile(hCom, Buffer, Size, &Count, 0); ASSERT_SUCCESS(WriteFile); if (Count != Size) { fprintf(stderr, "WriteFile Count (%d) != Size (%d)\n", Count, Size); exit(1); } return; } ulong CommRead (char *Buffer, ulong Size) { DWORD Count; BOOL Success; Success = ReadFile(hCom, Buffer, Size, &Count, 0); ASSERT_SUCCESS(ReadFile); return Count; } void CommClose (void) { CloseHandle(hCom); hCom = NULL; return; }