//
// www.blinkenlight.net
//
// Copyright 2014 Udo Klein
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/
// Please take care to NOT amplify the output of pin 3.
// Also do not connect pin 3 to a tuned antenna.
// Stay within in legal bounds. You have been warned.
// http://www.mikrocontroller.net/articles/Allgemeinzuteilung
// http://www.bundesnetzagentur.de/SharedDocs/Downloads/DE/Sachgebiete/Telekommunikation/Unternehmen_Institutionen/Frequenzen/Allgemeinzuteilungen/2014_04_InduktiveFunkanwendungen_pdf.pdf?__blob=publicationFile&v=5
// In Germany this implies that you must not exceed
// 42 dBμA/m in a distance of 10m of your transmitter
// If you connect 1k resistor to the output pin then
// the current will stay below 5 mA which will keep
// the output well below regulatory limits.
#define VCC_PIN 0
namespace BCD {
typedef union {
struct {
uint8_t lo:4;
uint8_t hi:4;
} digit;
struct {
uint8_t b0:1;
uint8_t b1:1;
uint8_t b2:1;
uint8_t b3:1;
uint8_t b4:1;
uint8_t b5:1;
uint8_t b6:1;
uint8_t b7:1;
} bit;
uint8_t val;
} bcd_t;
void increment(bcd_t &value);
bcd_t int_to_bcd(const uint8_t value);
uint8_t bcd_to_int(const bcd_t value);
}
namespace Debug {
void bcddigit(uint8_t data);
void bcddigits(uint8_t data);
}
typedef union {
struct {
uint8_t b0:2;
uint8_t b1:2;
uint8_t b2:2;
uint8_t b3:2;
} signal;
uint8_t byte;
} tData;
namespace DCF77 {
typedef enum {
long_tick = 3,
short_tick = 2,
undefined = 1,
sync_mark = 0
} tick_t;
typedef struct {
BCD::bcd_t year; // 0..99
BCD::bcd_t month; // 1..12
BCD::bcd_t day; // 1..31
BCD::bcd_t weekday; // Mo = 1, So = 7
BCD::bcd_t hour; // 0..23
BCD::bcd_t minute; // 0..59
uint8_t second; // 0..60
bool uses_summertime : 1; // false -> wintertime, true, summertime
bool uses_backup_antenna : 1; // typically false
bool timezone_change_scheduled : 1;
bool leap_second_scheduled : 1;
bool undefined_minute_output : 1;
bool undefined_uses_summertime_output : 1;
bool undefined_uses_backup_antenna_output : 1;
bool undefined_timezone_change_scheduled_output : 1;
} time_data_t;
typedef void (*output_handler_t)(const DCF77::time_data_t &decoded_time);
typedef enum {
useless = 0, // waiting for good enough signal
dirty = 1, // time data available but unreliable
free = 2, // clock was once synced but now may deviate more than 200 ms, must not re-lock if valid phase is detected
unlocked = 3, // lock was once synced, inaccuracy below 200 ms, may re-lock if a valid phase is detected
locked = 4, // no valid time data but clock driven by accurate phase
synced = 5 // best possible quality, clock is 100% synced
} clock_state_t;
}
namespace DCF77_Encoder {
// What *** exactly *** is the semantics of the "Encoder"?
// It only *** encodes *** whatever time is set
// It does never attempt to verify the data
void reset(DCF77::time_data_t &now);
uint8_t weekday(const DCF77::time_data_t &now); // sunday == 0
BCD::bcd_t bcd_weekday(const DCF77::time_data_t &now); // sunday == 7
DCF77::tick_t get_current_signal(const DCF77::time_data_t &now);
// This will advance the second. It will consider the control
// bits while doing so. It will NOT try to properly set the
// control bits. If this is desired "autoset" must be called in
// advance.
void advance_second(DCF77::time_data_t &now);
// The same but for the minute
void advance_minute(DCF77::time_data_t &now);
// This will set the weekday by evaluating the date.
void autoset_weekday(DCF77::time_data_t &now);
// This will set the control bits, as a side effect it sets the weekday
// It will generate the control bits exactly like DCF77 would.
// Look at the leap second and summer / wintertime transistions
// to understand the subtle implications.
void autoset_control_bits(DCF77::time_data_t &now);
void debug(const DCF77::time_data_t &clock);
void debug(const DCF77::time_data_t &clock, const uint16_t cycles);
// Bit Bezeichnung Wert Pegel Bedeutung
// 0 M 0 Minutenanfang (
// 1..14 n/a reserviert
// 15 R Reserveantenne aktiv (0 inaktiv, 1 aktiv)
// 16 A1 Ankündigung Zeitzonenwechsel (1 Stunde vor dem Wechsel für 1 Stunde, d.h ab Minute 1)
// 17 Z1 2 Zeitzonenbit Sommerzeit (MEZ = 0, MESZ = 1); also Zeitzone = UTC + 2*Z1 + Z2
// 18 Z2 1 Zeitzonenbit Winterzeit (MEZ = 1, MESZ = 0); also Zeitzone = UTC + 2*Z1 + Z2
// 19 A2 Ankündigung einer Schaltsekunde (1 Stunde vor der Schaltsekunde für 1 Stunde, d.h. ab Minute 1)
// 20 S 1 Startbit für Zeitinformation
// 21 1 Minuten 1er
// 22 2 Minuten 2er
// 23 4 Minuten 4er
// 24 8 Minuten 8er
// 25 10 Minuten 10er
// 26 20 Minuten 20er
// 27 40 Minuten 40er
// 28 P1 Prüfbit 1 (gerade Parität)
// 29 1 Stunden 1er
// 30 2 Stunden 2er
// 31 4 Stunden 4er
// 32 8 Stunden 8er
// 33 10 Stunden 10er
// 34 20 Stunden 20er
// 35 P2 Prüfbit 2 (gerade Parität)
// 36 1 Tag 1er
// 37 2 Tag 2er
// 38 4 Tag 4er
// 39 8 Tag 8er
// 40 10 Tag 10er
// 41 20 Tag 20er
// 42 1 Wochentag 1er (Mo = 1, Di = 2, Mi = 3,
// 43 2 Wochentag 2er (Do = 4, Fr = 5, Sa = 6,
// 44 4 Wochentag 4er (So = 7)
// 45 1 Monat 1er
// 46 2 Monat 2er
// 47 4 Monat 4er
// 48 8 Monat 8er
// 49 10 Monat 10er
// 50 1 Jahr 1er
// 51 2 Jahr 2er
// 52 4 Jahr 4er
// 53 8 Jahr 8er
// 54 10 Jahr 10er
// 55 20 Jahr 20er
// 56 40 Jahr 40er
// 57 80 Jahr 80er
// 58 P3 Prüftbit 3 (gerade Parität)
// 59 sync Sync Marke, kein Impuls (übliches Minutenende)
// 59 0 Schaltsekunde (sehr selten, nur nach Ankündigung)
// 60 sync Sync Marke, kein Impuls (nur nach Schaltsekunde)
// Falls eine Schaltsekunde eingefügt wird, wird bei Bit 59 eine Sekundenmarke gesendet.
// Der Syncimpuls erfolgt dann in Sekunde 60 statt 59. Üblicherweise wird eine 0 als Bit 59 gesendet
// Üblicherweise springt die Uhr beim Wechsel Winterzeit nach Sommerzeit von 1:59:59 auf 3:00:00
// beim Wechsel Sommerzeit nach Winterzeit von 2:59:59 auf 2:00:00
// Die Zeitinformation wird immer 1 Minute im Vorraus übertragen. D.h. nach der Syncmarke hat
// man die aktuelle Zeit
// http://www.dcf77logs.de/SpecialFiles.aspx
// Schaltsekunden werden in Deutschland von der Physikalisch-Technischen Bundesanstalt festgelegt,
// die allerdings dazu nur die international vom International Earth Rotation and Reference Systems
// Service (IERS) festgelegten Schaltsekunden übernimmt. Im Mittel sind Schaltsekunden etwa alle 18
// Monate nötig und werden vorrangig am 31. Dezember oder 30. Juni, nachrangig am 31. März oder
// 30. September nach 23:59:59 UTC (also vor 1:00 MEZ bzw. 2:00 MESZ) eingefügt. Seit der Einführung
// des Systems 1972 wurden ausschließlich die Zeitpunkte im Dezember und Juni benutzt.
}
namespace Debug {
void bcddigit(uint8_t data) {
if (data <= 0x09) {
Serial.print(data, HEX);
} else {
Serial.print('?');
}
}
void bcddigits(uint8_t data) {
bcddigit(data >> 4);
bcddigit(data & 0xf);
}
}
namespace BCD {
void print(const bcd_t value) {
Serial.print(value.val >> 4 & 0xF, HEX);
Serial.print(value.val >> 0 & 0xF, HEX);
}
void increment(bcd_t &value) {
if (value.digit.lo < 9) {
++value.digit.lo;
} else {
value.digit.lo = 0;
if (value.digit.hi < 9) {
++value.digit.hi;
} else {
value.digit.hi = 0;
}
}
}
bcd_t int_to_bcd(const uint8_t value) {
const uint8_t hi = value / 10;
bcd_t result;
result.digit.hi = hi;
result.digit.lo = value-10*hi;
return result;
}
uint8_t bcd_to_int(const bcd_t value) {
return value.digit.lo + 10*value.digit.hi;
}
}
namespace Arithmetic_Tools {
template <uint8_t N> inline void bounded_increment(uint8_t &value) __attribute__((always_inline));
template <uint8_t N>
void bounded_increment(uint8_t &value) {
if (value >= 255 - N) { value = 255; } else { value += N; }
}
template <uint8_t N> inline void bounded_decrement(uint8_t &value) __attribute__((always_inline));
template <uint8_t N>
void bounded_decrement(uint8_t &value) {
if (value <= N) { value = 0; } else { value -= N; }
}
inline void bounded_add(uint8_t &value, const uint8_t amount) __attribute__((always_inline));
void bounded_add(uint8_t &value, const uint8_t amount) {
if (value >= 255-amount) { value = 255; } else { value += amount; }
}
inline void bounded_sub(uint8_t &value, const uint8_t amount) __attribute__((always_inline));
void bounded_sub(uint8_t &value, const uint8_t amount) {
if (value <= amount) { value = 0; } else { value -= amount; }
}
inline uint8_t bit_count(const uint8_t value) __attribute__((always_inline));
uint8_t bit_count(const uint8_t value) {
const uint8_t tmp1 = (value & 0b01010101) + ((value>>1) & 0b01010101);
const uint8_t tmp2 = (tmp1 & 0b00110011) + ((tmp1>>2) & 0b00110011);
return (tmp2 & 0x0f) + (tmp2>>4);
}
inline uint8_t parity(const uint8_t value) __attribute__((always_inline));
uint8_t parity(const uint8_t value) {
uint8_t tmp = value;
tmp = (tmp & 0xf) ^ (tmp >> 4);
tmp = (tmp & 0x3) ^ (tmp >> 2);
tmp = (tmp & 0x1) ^ (tmp >> 1);
return tmp;
}
void minimize(uint8_t &minimum, const uint8_t value) {
if (value < minimum) {
minimum = value;
}
}
void maximize(uint8_t &maximum, const uint8_t value) {
if (value > maximum) {
maximum = value;
}
}
uint8_t set_bit(const uint8_t data, const uint8_t number, const uint8_t value) {
return value? data|(1<<number): data&~(1<<number);
}
}
namespace DCF77_Encoder {
using namespace DCF77;
inline uint8_t days_per_month(const DCF77::time_data_t &now) __attribute__((always_inline));
uint8_t days_per_month(const DCF77::time_data_t &now) {
switch (now.month.val) {
case 0x02:
// valid till 31.12.2399
// notice year mod 4 == year & 0x03
return 28 + ((now.year.val != 0) && ((bcd_to_int(now.year) & 0x03) == 0)? 1: 0);
case 0x01: case 0x03: case 0x05: case 0x07: case 0x08: case 0x10: case 0x12: return 31;
case 0x04: case 0x06: case 0x09: case 0x11: return 30;
default: return 0;
}
}
void reset(DCF77::time_data_t &now) {
now.second = 0;
now.minute.val = 0x00;
now.hour.val = 0x00;
now.day.val = 0x01;
now.month.val = 0x01;
now.year.val = 0x00;
now.weekday.val = 0x01;
now.uses_summertime = false;
now.uses_backup_antenna = false;
now.timezone_change_scheduled = false;
now.leap_second_scheduled = false;
now.undefined_minute_output = false;
now.undefined_uses_summertime_output = false;
now.undefined_uses_backup_antenna_output = false;
now.undefined_timezone_change_scheduled_output = false;
}
uint8_t weekday(const DCF77::time_data_t &now) { // attention: sunday will be ==0 instead of 7
using namespace BCD;
if (now.day.val <= 0x31 && now.month.val <= 0x12 && now.year.val <= 0x99) {
// This will compute the weekday for each year in 2001-2099.
// If you really plan to use my code beyond 2099 take care of this
// on your own. My assumption is that it is even unclear if DCF77
// will still exist then.
// http://de.wikipedia.org/wiki/Gau%C3%9Fsche_Wochentagsformel
const uint8_t d = bcd_to_int(now.day);
const uint16_t m = now.month.val <= 0x02? now.month.val + 10:
bcd_to_int(now.month) - 2;
const uint8_t y = bcd_to_int(now.year) - (now.month.val <= 0x02);
// m must be of type uint16_t otherwise this will compute crap
uint8_t day_mod_7 = d + (26*m - 2)/10 + y + y/4;
// We exploit 8 mod 7 = 1
while (day_mod_7 >= 7) {
day_mod_7 -= 7;
day_mod_7 = (day_mod_7 >> 3) + (day_mod_7 & 7);
}
return day_mod_7; // attention: sunday will be == 0 instead of 7
} else {
return 0xff;
}
}
BCD::bcd_t bcd_weekday(const DCF77::time_data_t &now) {
BCD::bcd_t today;
today.val = weekday(now);
if (today.val == 0) {
today.val = 7;
}
return today;
}
void autoset_weekday(DCF77::time_data_t &now) {
now.weekday = bcd_weekday(now);
}
void autoset_timezone(DCF77::time_data_t &now) {
// timezone change may only happen at the last sunday of march / october
// the last sunday is always somewhere in [25-31]
// Wintertime --> Summertime happens at 01:00 UTC == 02:00 CET == 03:00 CEST,
// Summertime --> Wintertime happens at 01:00 UTC == 02:00 CET == 03:00 CEST
if (now.month.val < 0x03) {
// January or February
now.uses_summertime = false;
} else
if (now.month.val == 0x03) {
// March
if (now.day.val < 0x25) {
// Last Sunday of March must be 0x25-0x31
// Thus still to early for summertime
now.uses_summertime = false;
} else
if (uint8_t wd = weekday(now)) {
// wd != 0 --> not a Sunday
if (now.day.val - wd < 0x25) {
// early March --> wintertime
now.uses_summertime = false;
} else {
// late march summertime
now.uses_summertime = true;
}
} else {
// last sunday of march
// decision depends on the current hour
now.uses_summertime = (now.hour.val > 2);
}
} else
if (now.month.val < 0x10) {
// April - September
now.uses_summertime = true;
} else
if (now.month.val == 0x10) {
// October
if (now.day.val < 0x25) {
// early October
now.uses_summertime = true;
} else
if (uint8_t wd = weekday(now)) {
// wd != 0 --> not a Sunday
if (now.day.val - wd < 0x25) {
// early October --> summertime
now.uses_summertime = true;
} else {
// late October --> wintertime
now.uses_summertime = false;
}
} else { // last sunday of october
if (now.hour.val == 2) {
// can not derive the flag from time data
// this is the only time the flag is derived
// from the flag vector
} else {
// decision depends on the current hour
now.uses_summertime = (now.hour.val < 2);
}
}
} else {
// November and December
now.uses_summertime = false;
}
}
void autoset_timezone_change_scheduled(DCF77::time_data_t &now) {
// summer/wintertime change will always happen
// at clearly defined hours
// http://www.gesetze-im-internet.de/sozv/__2.html
// in doubt have a look here: http://www.dcf77logs.de/
if (now.day.val < 0x25 || weekday(now) != 0) {
// timezone change may only happen at the last sunday of march / october
// the last sunday is always somewhere in [25-31]
// notice that undefined (==0xff) day/weekday data will not cause any action
now.timezone_change_scheduled = false;
} else {
if (now.month.val == 0x03) {
if (now.uses_summertime) {
now.timezone_change_scheduled = (now.hour.val == 0x03 && now.minute.val == 0x00); // wintertime to summertime, preparing first minute of summertime
} else {
now.timezone_change_scheduled = (now.hour.val == 0x01 && now.minute.val != 0x00); // wintertime to summertime
}
} else if (now.month.val == 0x10) {
if (now.uses_summertime) {
now.timezone_change_scheduled = (now.hour.val == 0x02 && now.minute.val != 0x00); // summertime to wintertime
} else {
now.timezone_change_scheduled = (now.hour.val == 0x02 && now.minute.val == 0x00); // summertime to wintertime, preparing first minute of wintertime
}
} else if (now.month.val <= 0x12) {
now.timezone_change_scheduled = false;
}
}
}
void verify_leap_second_scheduled(DCF77::time_data_t &now) {
// If day or month are unknown we default to "no leap second" because this is alway a very good guess.
// If we do not know for sure we are either acquiring a lock right now --> we will easily recover from a wrong guess
// or we have very noisy data --> the leap second bit is probably noisy as well --> we should assume the most likely case
now.leap_second_scheduled &= (now.day.val == 0x01);
// leap seconds will always happen at 00:00 UTC == 01:00 CET == 02:00 CEST
if (now.month.val == 0x01) {
now.leap_second_scheduled &= ((now.hour.val == 0x00 && now.minute.val != 0x00) ||
(now.hour.val == 0x01 && now.minute.val == 0x00));
} else if (now.month.val == 0x07 || now.month.val == 0x04 || now.month.val == 0x10) {
now.leap_second_scheduled &= ((now.hour.val == 0x01 && now.minute.val != 0x00) ||
(now.hour.val == 0x02 && now.minute.val == 0x00));
} else {
now.leap_second_scheduled = false;
}
}
void autoset_control_bits(DCF77::time_data_t &now) {
autoset_weekday(now);
autoset_timezone(now);
autoset_timezone_change_scheduled(now);
// we can not compute leap seconds, we can only verify if they might happen
verify_leap_second_scheduled(now);
}
void advance_second(DCF77::time_data_t &now) {
// in case some value is out of range it will not be advanced
// this is on purpose
if (now.second < 59) {
++now.second;
if (now.second == 15) {
autoset_control_bits(now);
}
} else if (now.leap_second_scheduled && now.second == 59 && now.minute.val == 0x00) {
now.second = 60;
now.leap_second_scheduled = false;
} else if (now.second == 59 || now.second == 60) {
now.second = 0;
advance_minute(now);
}
}
void advance_minute(DCF77::time_data_t &now) {
if (now.minute.val < 0x59) {
increment(now.minute);
} else if (now.minute.val == 0x59) {
now.minute.val = 0x00;
// in doubt have a look here: http://www.dcf77logs.de/
if (now.timezone_change_scheduled && !now.uses_summertime && now.hour.val == 0x01) {
// Wintertime --> Summertime happens at 01:00 UTC == 02:00 CET == 03:00 CEST,
// the clock must be advanced from 01:59 CET to 03:00 CEST
increment(now.hour);
increment(now.hour);
now.uses_summertime = true;
} else if (now.timezone_change_scheduled && now.uses_summertime && now.hour.val == 0x02) {
// Summertime --> Wintertime happens at 01:00 UTC == 02:00 CET == 03:00,
// the clock must be advanced from 02:59 CEST to 02:00 CET
now.uses_summertime = false;
} else {
if (now.hour.val < 0x23) {
increment(now.hour);
} else if (now.hour.val == 0x23) {
now.hour.val = 0x00;
if (now.weekday.val < 0x07) {
increment(now.weekday);
} else if (now.weekday.val == 0x07) {
now.weekday.val = 0x01;
}
if (bcd_to_int(now.day) < days_per_month(now)) {
increment(now.day);
} else if (bcd_to_int(now.day) == days_per_month(now)) {
now.day.val = 0x01;
if (now.month.val < 0x12) {
increment(now.month);
} else if (now.month.val == 0x12) {
now.month.val = 0x01;
if (now.year.val < 0x99) {
increment(now.year);
} else if (now.year.val == 0x99) {
now.year.val = 0x00;
}
}
}
}
}
}
}
DCF77::tick_t get_current_signal(const DCF77::time_data_t &now) {
using namespace Arithmetic_Tools;
if (now.second >= 1 && now.second <= 14) {
// weather data or other stuff we can not compute
return undefined;
}
bool result;
switch (now.second) {
case 0: // start of minute
return short_tick;
case 15:
if (now.undefined_uses_backup_antenna_output) { return undefined; }
result = now.uses_backup_antenna; break;
case 16:
if (now.undefined_timezone_change_scheduled_output) { return undefined; }
result = now.timezone_change_scheduled; break;
case 17:
if (now.undefined_uses_summertime_output) {return undefined; }
result = now.uses_summertime; break;
case 18:
if (now.undefined_uses_summertime_output) {return undefined; }
result = !now.uses_summertime; break;
case 19:
result = now.leap_second_scheduled; break;
case 20: // start of time information
return long_tick;
case 21:
if (now.undefined_minute_output || now.minute.val > 0x59) { return undefined; }
result = now.minute.digit.lo & 0x1; break;
case 22:
if (now.undefined_minute_output || now.minute.val > 0x59) { return undefined; }
result = now.minute.digit.lo & 0x2; break;
case 23:
if (now.undefined_minute_output || now.minute.val > 0x59) { return undefined; }
result = now.minute.digit.lo & 0x4; break;
case 24:
if (now.undefined_minute_output || now.minute.val > 0x59) { return undefined; }
result = now.minute.digit.lo & 0x8; break;
case 25:
if (now.undefined_minute_output || now.minute.val > 0x59) { return undefined; }
result = now.minute.digit.hi & 0x1; break;
case 26:
if (now.undefined_minute_output || now.minute.val > 0x59) { return undefined; }
result = now.minute.digit.hi & 0x2; break;
case 27:
if (now.undefined_minute_output || now.minute.val > 0x59) { return undefined; }
result = now.minute.digit.hi & 0x4; break;
case 28:
if (now.undefined_minute_output || now.minute.val > 0x59) { return undefined; }
result = parity(now.minute.val); break;
case 29:
if (now.hour.val > 0x23) { return undefined; }
result = now.hour.digit.lo & 0x1; break;
case 30:
if (now.hour.val > 0x23) { return undefined; }
result = now.hour.digit.lo & 0x2; break;
case 31:
if (now.hour.val > 0x23) { return undefined; }
result = now.hour.digit.lo & 0x4; break;
case 32:
if (now.hour.val > 0x23) { return undefined; }
result = now.hour.digit.lo & 0x8; break;
case 33:
if (now.hour.val > 0x23) { return undefined; }
result = now.hour.digit.hi & 0x1; break;
case 34:
if (now.hour.val > 0x23) { return undefined; }
result = now.hour.digit.hi & 0x2; break;
case 35:
if (now.hour.val > 0x23) { return undefined; }
result = parity(now.hour.val); break;
case 36:
if (now.day.val > 0x31) { return undefined; }
result = now.day.digit.lo & 0x1; break;
case 37:
if (now.day.val > 0x31) { return undefined; }
result = now.day.digit.lo & 0x2; break;
case 38:
if (now.day.val > 0x31) { return undefined; }
result = now.day.digit.lo & 0x4; break;
case 39:
if (now.day.val > 0x31) { return undefined; }
result = now.day.digit.lo & 0x8; break;
case 40:
if (now.day.val > 0x31) { return undefined; }
result = now.day.digit.hi & 0x1; break;
case 41:
if (now.day.val > 0x31) { return undefined; }
result = now.day.digit.hi & 0x2; break;
case 42:
if (now.weekday.val > 0x7) { return undefined; }
result = now.weekday.val & 0x1; break;
case 43:
if (now.weekday.val > 0x7) { return undefined; }
result = now.weekday.val & 0x2; break;
case 44:
if (now.weekday.val > 0x7) { return undefined; }
result = now.weekday.val & 0x4; break;
case 45:
if (now.month.val > 0x12) { return undefined; }
result = now.month.digit.lo & 0x1; break;
case 46:
if (now.month.val > 0x12) { return undefined; }
result = now.month.digit.lo & 0x2; break;
case 47:
if (now.month.val > 0x12) { return undefined; }
result = now.month.digit.lo & 0x4; break;
case 48:
if (now.month.val > 0x12) { return undefined; }
result = now.month.digit.lo & 0x8; break;
case 49:
if (now.month.val > 0x12) { return undefined; }
result = now.month.digit.hi & 0x1; break;
case 50:
if (now.year.val > 0x99) { return undefined; }
result = now.year.digit.lo & 0x1; break;
case 51:
if (now.year.val > 0x99) { return undefined; }
result = now.year.digit.lo & 0x2; break;
case 52:
if (now.year.val > 0x99) { return undefined; }
result = now.year.digit.lo & 0x4; break;
case 53:
if (now.year.val > 0x99) { return undefined; }
result = now.year.digit.lo & 0x8; break;
case 54:
if (now.year.val > 0x99) { return undefined; }
result = now.year.digit.hi & 0x1; break;
case 55:
if (now.year.val > 0x99) { return undefined; }
result = now.year.digit.hi & 0x2; break;
case 56:
if (now.year.val > 0x99) { return undefined; }
result = now.year.digit.hi & 0x4; break;
case 57:
if (now.year.val > 0x99) { return undefined; }
result = now.year.digit.hi & 0x8; break;
case 58:
if (now.weekday.val > 0x07 ||
now.day.val > 0x31 ||
now.month.val > 0x12 ||
now.year.val > 0x99) { return undefined; }
result = parity(now.day.digit.lo) ^
parity(now.day.digit.hi) ^
parity(now.month.digit.lo) ^
parity(now.month.digit.hi) ^
parity(now.weekday.val) ^
parity(now.year.digit.lo) ^
parity(now.year.digit.hi); break;
case 59:
// special handling for leap seconds
if (now.leap_second_scheduled && now.minute.val == 0) { result = 0; break; }
// standard case: fall through to "sync_mark"
case 60:
return sync_mark;
default:
return undefined;
}
return result? long_tick: short_tick;
}
void debug(const DCF77::time_data_t &clock) {
using namespace Debug;
Serial.print(F(" "));
bcddigits(clock.year.val);
Serial.print('.');
bcddigits(clock.month.val);
Serial.print('.');
bcddigits(clock.day.val);
Serial.print('(');
bcddigit(clock.weekday.val);
Serial.print(',');
bcddigit(weekday(clock));
Serial.print(')');
bcddigits(clock.hour.val);
Serial.print(':');
bcddigits(clock.minute.val);
Serial.print(':');
if (clock.second < 10) {
Serial.print('0');
}
Serial.print(clock.second, DEC);
if (clock.uses_summertime) {
Serial.print(F(" MESZ "));
} else {
Serial.print(F(" MEZ "));
}
if (clock.leap_second_scheduled) {
Serial.print(F("leap second scheduled"));
}
if (clock.timezone_change_scheduled) {
Serial.print(F("time zone change scheduled"));
}
}
}
namespace timer_4 {
// 16000000 / 77500 = 206 + 70/155 = 206 + 14/31
const uint8_t ticks_per_period = 206;
// fractional ticks
const uint8_t nominator = 14;
const uint8_t denominator = 31;
const uint8_t pwm_full_carrier = ticks_per_period / 2; // ~50 % duty cycle
const uint8_t pwm_modulated_carrier = ticks_per_period / 32; // 1/4 Amplitude = 1/16 Power
void setup() {
// disable timer4 interrupts during setup
TIMSK4 = 0;
// enable OC4D pin 6 for output (27 (T0/OC4D/ADC10) PD7 Digital Pin 6 (PWM))
DDRD |= (1 << 6);
// The fast Pulse Width Modulation or fast PWM mode (WGM22:0 = 3 or 7) provides a high fre-
// quency PWM waveform generation option. The fast PWM differs from the other PWM option by
// its single-slope operation. The counter counts from BOTTOM to TOP then restarts from BOT-
// TOM. TOP is defined as 0xFF when WGM2:0 = 3, and OCR2A when MGM2:0 = 7. In non-
// inverting Compare Output mode, the Output Compare (OC2x) is cleared on the compare match
// between TCNT2 and OCR2x, and set at BOTTOM. In inverting Compare Output mode, the out-
// put is set on compare match and cleared at BOTTOM.
TCCR4A = 1<< WGM40 | 1<<WGM41 | // Fast PWM
1<<COM4D1; // Clear OC4D on Compare Match
TCCR4D = 1<<CS40 | // no Prescaler
1<<WGM41; // Fast PWM
OCR4A = ticks_per_period - 1; // period length
OCR4D = pwm_full_carrier; // duty cycle
TIMSK4 = 1<<OCIE4A; // enable match interrupts
}
}
ISR(TIMER4_COMPA_vect) {
static uint8_t accumulated_fractional_ticks = 0;
accumulated_fractional_ticks += timer_4::nominator;
if (accumulated_fractional_ticks < timer_4::denominator) {
OCR4A = timer_4::ticks_per_period - 1;
} else {
OCR4A = timer_4::ticks_per_period;
accumulated_fractional_ticks -= timer_4::denominator;
}
}
namespace timer_0 {
void setup() {
// disable timer 0 interrupts
TIMSK0 = 0;
}
}
namespace timer_1 {
void setup() {
TIMSK1 = 0; // disable timer1 interrupts
TCCR1A = 0; // do not toggle or change timer IO pins
// generate interrupts at 10 Hz
OCR1A = 6250 - 1; // (16000000 / 256 / 10) - 1
TCCR1B = (1<<WGM12) | // CTC using OCR1A
(1<<CS12); // set prescaler to 256
TIMSK1 = (1 << OCIE1A); // enable OCR1A match interrupt
}
}
uint8_t stop_modulation_after_times_100ms = 5;
DCF77::time_data_t now;
namespace Parser {
void show(const DCF77::time_data_t &now) {
cli();
const DCF77::time_data_t l_now = now;
sei();
Debug::bcddigits(l_now.year.val);
Serial.print('.');
Debug::bcddigits(l_now.month.val);
Serial.print('.');
Debug::bcddigits(l_now.day.val);
Serial.print(' ');
Debug::bcddigits(l_now.hour.val);
Serial.print(':');
Debug::bcddigits(l_now.minute.val);
Serial.print(':');
Serial.print(l_now.second / 10);
Serial.print(l_now.second % 10);
Serial.print(' ');
Serial.print(l_now.weekday.val);
Serial.print(' ');
Serial.print(l_now.uses_summertime);
Serial.print(l_now.uses_backup_antenna);
Serial.print(l_now.timezone_change_scheduled);
Serial.println(l_now.leap_second_scheduled);
}
char blocking_read() {
while (!Serial.available()) {}
const char c = Serial.read();
/*
Serial.print('(');
Serial.print(c);
Serial.print(')');
*/
return c;
}
uint8_t get_flag(bool &valid) {
if (valid) {
const char c = blocking_read();
if (c == '0' || c == '1') {
return c - '0';
} else {
valid = false;
}
}
return 0;
}
uint8_t get_digit(bool &valid) {
if (valid) {
const char c = blocking_read();
if (c >= '0' && c <= '9') {
return c - '0';
} else {
valid = false;
}
}
return 0;
}
void assert_char(bool &valid, const char c) {
valid = valid && blocking_read() == c;
}
bool parse_simple(DCF77::time_data_t &now) {
bool valid = true;
now.year.digit.hi = get_digit(valid);
now.year.digit.lo = get_digit(valid);
assert_char(valid, '.');
now.month.digit.hi = get_digit(valid);
now.month.digit.lo = get_digit(valid);
assert_char(valid, '.');
now.day.digit.hi = get_digit(valid);
now.day.digit.lo = get_digit(valid);
assert_char(valid, ' ');
now.hour.digit.hi = get_digit(valid);
now.hour.digit.lo = get_digit(valid);
assert_char(valid, ':');
now.minute.digit.hi = get_digit(valid);
now.minute.digit.lo = get_digit(valid);
assert_char(valid, ':');
now.second = get_digit(valid) * 10;
now.second += get_digit(valid);
return valid;
}
bool parse_extended(DCF77::time_data_t &now) {
bool valid = parse_simple(now);
assert_char(valid, ' ');
now.weekday.val = get_digit(valid);
assert_char(valid, ' ');
now.uses_summertime = get_flag(valid);
now.uses_backup_antenna = get_flag(valid);;
now.timezone_change_scheduled = get_flag(valid);
now.leap_second_scheduled = get_flag(valid);
return valid;
}
void parse(DCF77::time_data_t &now) {
const char mode = blocking_read();
bool valid = false;
DCF77::time_data_t new_time;
switch (mode) {
case 's':
valid = parse_simple(new_time);
if (valid) {
DCF77_Encoder::autoset_timezone(new_time);
DCF77_Encoder::autoset_weekday(new_time);
}
break;
case 'x':
valid = parse_extended(new_time);
break;
}
if (valid) {
cli();
now = new_time;
sei();
} else {
Serial.println();
Serial.println();
Serial.println(F("To set target time use one of the following formats"));
Serial.println();
Serial.println(F("simple mode:"));
Serial.println(F("sYY.MM.DD hh:mm.ss"));
Serial.println();
Serial.println(F("extended mode:"));
Serial.println(F("x:YY.MM.DD hh:mm.ss w sbtl"));
Serial.println();
}
Serial.println(F("current time setup (YY.MM.DD hh:mm.ss w sbtl)"));
Serial.println(F("w = weekday, s = summertime, b = backup antenna, t = timzone change scheduled, l = leap second scheduled"));
show(now);
}
}
void modulate() {
static uint8_t times_100ms = 0;
if (times_100ms == 0) {
// start of second
if (times_100ms != stop_modulation_after_times_100ms) {
OCR4D = timer_4::pwm_modulated_carrier;
}
}
if (times_100ms == stop_modulation_after_times_100ms) {
OCR4D = timer_4::pwm_full_carrier;
}
if (times_100ms < 9) {
++times_100ms;
} else {
// after 900ms)
times_100ms = 0;
DCF77::time_data_t l_now = now;
DCF77_Encoder::advance_second(now);
// Serial.println("test");
Parser::show(now);
const DCF77::tick_t output_tick = DCF77_Encoder::get_current_signal(now);
stop_modulation_after_times_100ms = output_tick == DCF77::long_tick ? 2:
output_tick == DCF77::short_tick? 1:
output_tick == DCF77::undefined? 1:
0;
}
}
ISR(TIMER1_COMPA_vect) {
sei();
// ensure that timer 2 can interrupt timer 1
modulate();
}
void setup() {
timer_0::setup();
timer_1::setup();
timer_4::setup();
pinMode(VCC_PIN, OUTPUT);
digitalWrite(VCC_PIN, HIGH);
Serial.begin(115200);
// compare to http://www.dcf77logs.de/ViewLog.aspx?mode=special&file=06%20-%20Schaltsekunde.log
// set time to a leap second - because I always wanted to "see" one
// - without staying up at late at night:)
now.year.val = 0x09;
now.month.val = 0x01;
now.day.val = 0x01;
now.weekday.val = 4;
now.hour.val = 0x0;
now.minute.val = 0x52;
now.second = 0;
now.uses_summertime = 0;
now.uses_backup_antenna = 0;
now.timezone_change_scheduled = 0;
now.leap_second_scheduled = 1;
Serial.println(F("running"));
Serial.println(F("output on pin D3"));
}
void loop() {
Parser::parse(now);
}
Last active
November 22, 2015 16:43
-
-
Save hryamzik/c0f6aa10d6fb0326bc2f to your computer and use it in GitHub Desktop.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment