491 lines
13 KiB
C++
491 lines
13 KiB
C++
#define __STDC_CONSTANT_MACROS 1
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
|
|
#ifdef AVR
|
|
#define printf(...) (0)
|
|
#endif
|
|
|
|
struct wwvb_t {
|
|
int16_t yday; // 1..365, or 1..366 in leap years
|
|
int8_t hour; // 0..23
|
|
int8_t minute; // 0..60
|
|
int8_t second; // 0..60
|
|
int8_t year; // 0..99, indicating 2000..2099
|
|
unsigned ls:1; // true if a leap second is coming
|
|
unsigned ly:1; // true if it's a leap year, at least after feb 28
|
|
unsigned dst:2; // 2-bit dst indicator code
|
|
};
|
|
|
|
extern void set_time(const wwvb_t &t);
|
|
extern void next_second();
|
|
|
|
namespace {
|
|
int8_t isly(int8_t year) {
|
|
return (year == 0 || year % 100) && ((year % 4) == 0);
|
|
}
|
|
|
|
int16_t last_yday(wwvb_t &t) {
|
|
return 365 + isly(t.year);
|
|
}
|
|
|
|
// When the leap second flag is set, the last minute of the last hour
|
|
// of the last day of that month has an extra second inserted
|
|
//
|
|
// This function returns 59 for most minutes, and 60 for leap minutes.
|
|
//
|
|
// Some documents indicate that leap seconds might occur a the end of any
|
|
// month, while others indicate that they occur only in June and December.
|
|
// Possibly this code should be revised to account for the possibility of
|
|
// leap seconds in other months.
|
|
uint8_t last_second(wwvb_t &t) {
|
|
if(!t.ls) return 59;
|
|
if(t.hour != 23) return 59;
|
|
if(t.minute != 59) return 59;
|
|
if(t.ly) {
|
|
if(t.yday == 182 || t.yday == 366) return 60;
|
|
} else {
|
|
if(t.yday == 181 || t.yday == 365) return 60;
|
|
}
|
|
return 59;
|
|
}
|
|
|
|
// Advance to exactly the beginning of the next minute
|
|
void advance_minute(wwvb_t &t) {
|
|
// If the past minute ended with a leap second, reset the flag
|
|
if(last_second(t) == 60) t.ls = 0;
|
|
|
|
t.second = 0;
|
|
++t.minute;
|
|
if(t.minute < 60) return;
|
|
|
|
// new hour
|
|
t.minute = 0;
|
|
++t.hour;
|
|
if(t.hour < 24) return;
|
|
|
|
// new day
|
|
t.hour = 0;
|
|
++t.yday;
|
|
if(t.yday <= last_yday(t)) return;
|
|
|
|
// new year
|
|
t.yday = 1;
|
|
t.year += 1;
|
|
t.ly = isly(t.year);
|
|
}
|
|
|
|
|
|
// Advance by exactly one second
|
|
void advance_second(wwvb_t &t) {
|
|
++t.second;
|
|
if(t.second <= last_second(t)) return;
|
|
|
|
t.second = 0;
|
|
advance_minute(t);
|
|
}
|
|
|
|
// Advance or decrease the time according to the timezone offset
|
|
// in minutes and seconds. Note: for negative non-hour offsets, h and m
|
|
// should both be negative. e.g., for instance the NST offset of -3:30
|
|
// would be passed as h=-3, m=-30
|
|
void apply_tz(wwvb_t &t, int8_t h, int8_t m) {
|
|
t.minute += m;
|
|
t.hour += h;
|
|
// Now, both hours and minutes can be outside their normal range
|
|
|
|
// Adjust minutes until they are again in the range [0..60)
|
|
if(m < 0) {
|
|
m += 60;
|
|
t.hour -= 1;
|
|
} else if(m > 60) {
|
|
m -= 60;
|
|
t.hour -= 1;
|
|
}
|
|
// Now minutes are in the range [0..60) but hours can be outside
|
|
// the range.
|
|
if(t.hour < 0) {
|
|
t.hour += 24;
|
|
t.yday -= 1;
|
|
} else if(t.hour > 24) {
|
|
t.hour -= 24;
|
|
t.yday += 1;
|
|
}
|
|
// Now, minutes and hours are in their normal range, but the year
|
|
// day can be outside the year.
|
|
// in practice, only one 'if' will run
|
|
if(t.yday <= 0) {
|
|
t.year--;
|
|
t.yday = last_yday(t) - t.yday;
|
|
t.ly = isly(t.year);
|
|
} else if(t.yday > last_yday(t)) {
|
|
t.yday -= last_yday(t);
|
|
t.year++;
|
|
t.ly = isly(t.year);
|
|
}
|
|
|
|
// Now
|
|
}
|
|
|
|
int8_t leapyears_before(int8_t year) {
|
|
return (year + 3) / 4;
|
|
}
|
|
|
|
// monday = 0, sunday = 6
|
|
#define DOW_YDAY1_YEAR0 5 // january 1, 2000 = saturday
|
|
int8_t get_dow(wwvb_t &t) {
|
|
int16_t dow = DOW_YDAY1_YEAR0;
|
|
dow += t.year;
|
|
dow += leapyears_before(t.year);
|
|
dow += t.yday-1;
|
|
return dow % 7;
|
|
}
|
|
|
|
bool operator==(const wwvb_t &a, const wwvb_t &b) {
|
|
return a.year == b.year && a.yday == b.yday
|
|
&& a.hour == b.hour && a.minute == b.minute
|
|
&& a.second == b.second
|
|
&& a.dst == b.dst && a.ly == b.ly
|
|
&& a.ls == b.ls;
|
|
}
|
|
|
|
const uint8_t NSAMPLES = 120;
|
|
const int8_t DEBOUNCE_TC = 10;
|
|
const int16_t COUNTER_SLOP = 100;
|
|
// 10 for DEBOUNCE_TC, 30 for receiver rise/fall time, 5 for light propagation
|
|
const int16_t SIGNAL_DELAY = 40;
|
|
uint8_t wwvb_buf[(NSAMPLES+3)/4];
|
|
int8_t wwvb_pos;
|
|
|
|
enum wwvb_state_t { STATE_FIND_POLARITY, STATE_CAPTURE_TIME };
|
|
|
|
wwvb_state_t wwvb_state;
|
|
bool wwvb_polarity;
|
|
|
|
int8_t wwvb_counter;
|
|
bool wwvb_denoised;
|
|
|
|
int16_t counter;
|
|
int8_t sos_counter;
|
|
|
|
void WWVB_PUT(uint8_t v) {
|
|
uint8_t idx = wwvb_pos / 4;
|
|
uint8_t s = 2 * (wwvb_pos % 4);
|
|
uint8_t m = 3 << s;
|
|
wwvb_buf[idx] = (wwvb_buf[idx] & ~m) | (v << s);
|
|
wwvb_pos ++;
|
|
if(wwvb_pos == NSAMPLES) wwvb_pos = 0;
|
|
}
|
|
|
|
uint8_t WWVB_GET(int8_t i) {
|
|
i = i - NSAMPLES + wwvb_pos;
|
|
if(i < 0) i += NSAMPLES;
|
|
uint8_t idx = i / 4;
|
|
uint8_t s = 2*(i % 4);
|
|
return (wwvb_buf[idx] >> s) & 3;
|
|
}
|
|
|
|
void decode_one_minute(uint8_t pos, wwvb_t &t) {
|
|
// The minute running from pos..pos+60 is already validated
|
|
// so there's no need to check mark bits or "always zero" bits
|
|
// .. everything from WWVB_GET is just a 0 or a 1
|
|
#define G(p, w) (WWVB_GET((p)+pos) ? (w) : 0)
|
|
t.minute = G(1, 40) + G(2, 20) + G(3, 10)
|
|
+ G(5, 8) + G(6, 4) + G(7, 2) + G(8, 1);
|
|
t.hour = G(12, 20) + G(13, 10)
|
|
+ G(15, 8) + G(16, 4) + G(17, 2) + G(18, 1);
|
|
t.yday = G(22, 200) + G(23, 100)
|
|
+ G(25, 80) + G(26, 40) + G(27, 20) + G(28, 10)
|
|
+ G(30, 8) + G(31, 4) + G(32, 2) + G(33, 1);
|
|
t.year = G(45, 80) + G(46, 40) + G(47, 20) + G(48, 10)
|
|
+ G(50, 8) + G(51, 4) + G(52, 2) + G(53, 1);
|
|
t.ly = G(55, 1);
|
|
t.ls = G(56, 1);
|
|
t.dst = G(57, 2) + G(58, 1);
|
|
t.second = 0;
|
|
}
|
|
|
|
void denoise_step(bool &denoised, int8_t &counter, bool value) {
|
|
if(value) {
|
|
if(0 && counter <= 0) counter = 1;
|
|
else if(counter == DEBOUNCE_TC) denoised = value;
|
|
else counter++;
|
|
} else {
|
|
if(0 && counter >= 0) counter = -1;
|
|
else if(counter == -DEBOUNCE_TC) denoised = value;
|
|
else counter--;
|
|
}
|
|
}
|
|
|
|
|
|
bool counter_near(int16_t n) {
|
|
return counter > (n - COUNTER_SLOP) && counter <= (n + COUNTER_SLOP);
|
|
}
|
|
|
|
const char *format_wwvbtime(const wwvb_t &t) {
|
|
static char buf[80];
|
|
snprintf(buf, sizeof(buf), "%4d/%03d %d:%02d:%02d ly=%d ls=%d dst=%d",
|
|
t.year + 2000, t.yday, t.hour, t.minute, t.second, t.ls, t.ly, t.dst);
|
|
return buf;
|
|
}
|
|
|
|
// bit i is 1 if it must be a mark, 0 if it must not be a mark
|
|
const uint64_t markmask =
|
|
(UINT64_C(1) << 0) |
|
|
(UINT64_C(1) << 9) |
|
|
(UINT64_C(1) << 19) |
|
|
(UINT64_C(1) << 29) |
|
|
(UINT64_C(1) << 39) |
|
|
(UINT64_C(1) << 49) |
|
|
(UINT64_C(1) << 59);
|
|
|
|
#define MARK(i) do { if(WWVB_GET(i) != 2 || WWVB_GET(i+60) != 2) return false; } while(0)
|
|
#define NOMARK(i) do { if(WWVB_GET(i) == 2 || WWVB_GET(i+60) == 2) return false; } while(0)
|
|
bool try_set_time(wwvb_t &new_t) {
|
|
// Check for markers over 2 minutes
|
|
for(int i=0; i<60; i++)
|
|
if(markmask & (UINT64_C(1) << i)) MARK(i); else NOMARK(i);
|
|
wwvb_t t0, t1;
|
|
decode_one_minute(0, t0);
|
|
decode_one_minute(60, t1);
|
|
|
|
printf("first minute %s\n", format_wwvbtime(t0));
|
|
printf("second minute %s\n", format_wwvbtime(t1));
|
|
advance_minute(t0);
|
|
printf("advanced %s\n", format_wwvbtime(t0));
|
|
if(t0 == t1) {
|
|
t1.second = 59;
|
|
new_t = t1;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#ifdef TUNE
|
|
/* TIMER1 runs at 16MHz nominal. We want a 1ms interrupt.
|
|
* That makes the nominal TOP value 16000. This wide adjustment range allows
|
|
* us to accomodate real clock frequencies of 15MHz..16MHz. The expected clock
|
|
* variations are actually on the order of ppm, not 6%.
|
|
*/
|
|
extern void set_timer_top(uint16_t top);
|
|
const int16_t MIN_TOP=15000, MAX_TOP=17000, NOMINAL_TOP=16000;
|
|
int16_t top = NOMINAL_TOP;
|
|
static uint8_t counter_history;
|
|
void steer_counter(uint16_t pulse_period_measured) {
|
|
counter_history <<= 2;
|
|
if(measured < 1000) counter_history |= 1;
|
|
else if(measured > 1000) counter_history |= 2;
|
|
if(counter_history == 0x55 && top > min_top)
|
|
top--; /* Last 4 samples were all too short */
|
|
else if(counter_history == 0x88 && top < max_top)
|
|
top++; /* Last 4 samples were all too long */
|
|
set_timer_top(top);
|
|
}
|
|
|
|
#endif
|
|
|
|
bool pending_set_time;
|
|
bool pps_good;
|
|
wwvb_t pending_time;
|
|
int16_t free_running_ms;
|
|
}
|
|
|
|
void wwvb_receive_loop(bool raw_wwvb) {
|
|
bool old_wwvb_denoised = wwvb_denoised;
|
|
denoise_step(wwvb_denoised, wwvb_counter, raw_wwvb);
|
|
|
|
bool edge = wwvb_denoised ^ old_wwvb_denoised;
|
|
bool wwvb = wwvb_denoised ^ wwvb_polarity;
|
|
bool rising_edge = edge && wwvb;
|
|
bool falling_edge = edge && !wwvb;
|
|
|
|
switch(wwvb_state) {
|
|
case STATE_FIND_POLARITY:
|
|
if(rising_edge) {
|
|
if(counter_near(1000)) {
|
|
sos_counter++;
|
|
if(sos_counter == 5) {
|
|
goto set_state_capture_time;
|
|
}
|
|
} else { sos_counter = 0; wwvb_polarity = !wwvb_polarity; }
|
|
}
|
|
|
|
case STATE_CAPTURE_TIME:
|
|
if(rising_edge) {
|
|
if(!counter_near(1000)) {
|
|
goto set_state_find_polarity;
|
|
}
|
|
if(pending_set_time) {
|
|
set_time(pending_time);
|
|
pending_set_time = false;
|
|
free_running_ms = 1000 + SIGNAL_DELAY;
|
|
}
|
|
}
|
|
if(falling_edge) {
|
|
if(counter_near(200)) WWVB_PUT(0);
|
|
else if(counter_near(500)) WWVB_PUT(1);
|
|
else if(counter_near(800)) {
|
|
WWVB_PUT(2);
|
|
pending_set_time = try_set_time(pending_time);
|
|
} else
|
|
goto set_state_find_polarity;
|
|
}
|
|
static uint8_t seconds_unset;
|
|
seconds_unset ++;
|
|
if(seconds_unset == 0) goto set_state_find_polarity;
|
|
|
|
}
|
|
|
|
free_running_ms ++;
|
|
if(free_running_ms >= 1000) {
|
|
free_running_ms -= 1000;
|
|
next_second();
|
|
}
|
|
|
|
if(rising_edge) {
|
|
pps_good = counter_near(1000);
|
|
#ifdef TUNE
|
|
if(pps_good && counter != 1000) steer_counter(pps_good > 1000);
|
|
#endif
|
|
counter = 0;
|
|
} else if(counter > 1000 + COUNTER_SLOP) {
|
|
pps_good = false;
|
|
counter ++;
|
|
} else
|
|
counter ++;
|
|
|
|
return;
|
|
|
|
set_state_capture_time:
|
|
counter = 0;
|
|
memset(wwvb_buf, 0, sizeof(wwvb_buf));
|
|
wwvb_state = STATE_CAPTURE_TIME;
|
|
return;
|
|
|
|
set_state_find_polarity:
|
|
sos_counter = 0;
|
|
wwvb_state = STATE_FIND_POLARITY;
|
|
return;
|
|
}
|
|
|
|
#ifndef AVR
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
|
|
void set_time(const wwvb_t &t) {
|
|
printf("set time %d/%03d %d:%02d:%02d ly=%d ls=%d dst=%d\n",
|
|
t.year + 2000, t.yday, t.hour, t.minute, t.second, t.ls, t.ly, t.dst);
|
|
exit(0);
|
|
}
|
|
|
|
void next_second() {}
|
|
|
|
int main() {
|
|
while(1) {
|
|
int c = getchar();
|
|
if(c == EOF) break;
|
|
wwvb_receive_loop(c == '1');
|
|
}
|
|
printf("failed to set time\n");
|
|
}
|
|
#else
|
|
#include <stdio.h>
|
|
#include <avr/io.h>
|
|
#include <avr/interrupt.h>
|
|
#include <avr/eeprom.h>
|
|
|
|
#define LED (5)
|
|
#define WWVB (0)
|
|
|
|
#define CYCLES_HIGH 16000
|
|
|
|
int uptime;
|
|
|
|
char buf[80];
|
|
void set_time(const wwvb_t &t) {
|
|
snprintf(buf, sizeof(buf),
|
|
"set time %d/%03d %d:%02d:%02d ly=%d ls=%d dst=%d uptime=%d pol=%d\n",
|
|
t.year + 2000, t.yday, t.hour, t.minute, t.second, t.ls, t.ly, t.dst,
|
|
uptime, wwvb_polarity);
|
|
eeprom_write_block(buf, 0, strlen(buf)+1);
|
|
|
|
// set LED on solid
|
|
PORTB |= (1<<LED);
|
|
|
|
// and freeze here
|
|
while(1) {}
|
|
}
|
|
|
|
ISR(TIMER1_CAPT_vect) {
|
|
bool wwvb_raw = !!(PINB & (1<<WWVB));
|
|
wwvb_receive_loop(wwvb_raw);
|
|
|
|
int val;
|
|
|
|
if(wwvb_state == STATE_FIND_POLARITY) {
|
|
val = pps_good;
|
|
} else {
|
|
val = (wwvb_denoised ^ wwvb_polarity ) ? 1 : 16;
|
|
}
|
|
if((free_running_ms & 15) < val)
|
|
PORTB |= (1<<LED);
|
|
else
|
|
PORTB &= ~(1<<LED);
|
|
}
|
|
|
|
void eeprom_to_serial() {
|
|
uint8_t *i = 0;
|
|
uint8_t c;
|
|
while(i < (uint8_t*)256 && (c = eeprom_read_byte(i++))) {
|
|
while(!(UCSR0A & (1<<UDRE0)))
|
|
{}
|
|
UDR0 = c;
|
|
}
|
|
}
|
|
|
|
void next_second() {
|
|
uptime++;
|
|
}
|
|
|
|
void main() {
|
|
// Set up TIMER1 in CTC mode with ICR1 as top
|
|
TCCR1A = (1<WGM11);
|
|
TCCR1B = (1<<WGM13) | (1<<WGM12) | (1<<CS10);
|
|
ICR1 = CYCLES_HIGH;
|
|
TIMSK1 = (1<<ICIE1);
|
|
|
|
// Set the UART to 19.2kb/s.
|
|
#define F_CPU 16000000
|
|
#define BAUD 19200
|
|
#include <util/setbaud.h>
|
|
UBRR0H = UBRRH_VALUE;
|
|
UBRR0L = UBRRL_VALUE;
|
|
#if USE_2X
|
|
UCSR0A |= (1 << U2X0);
|
|
#else
|
|
UCSR0A &= ~(1 << U2X0);
|
|
#endif
|
|
DDRD = (1<<1); // enable TXD as output
|
|
|
|
// Set up the LED as output
|
|
DDRB = (1<<LED);
|
|
|
|
PORTB |= (1<<WWVB); // turn on pull-up on wwvb
|
|
|
|
// enable interrupts
|
|
sei();
|
|
|
|
// perhaps our predecessor left a message
|
|
eeprom_to_serial();
|
|
|
|
// and sleep forever, since things happen in the interrupt only
|
|
#ifdef SLEEP_MODE_IDLE
|
|
set_sleep_mode(SLEEP_MODE_IDLE);
|
|
while(1) sleep_mode();
|
|
#else
|
|
while(1) {}
|
|
#endif
|
|
|
|
}
|
|
#endif
|